一、遷移學(xué)習(xí)
機(jī)器學(xué)習(xí)有一個(gè)基本假設(shè):數(shù)據(jù)同分布。可參考之前這篇介紹:《數(shù)據(jù)不同分布,怎么整?》 然而,現(xiàn)實(shí)中的數(shù)據(jù)情況通常有點(diǎn)坎坷,數(shù)據(jù)不同分布的也很常見(jiàn),比如,已有的大量其他領(lǐng)域的數(shù)據(jù)如何適用以當(dāng)前任務(wù)的問(wèn)題,這也是遷移學(xué)習(xí)所要解決的!
遷移學(xué)習(xí),即Transfer learning 可以簡(jiǎn)單定義為:將在源域S的知識(shí)遷移到目標(biāo)域T任務(wù),提高在目標(biāo)域T的任務(wù)下模型預(yù)測(cè)的性能。
顧名思義就是遷移其他領(lǐng)域的數(shù)據(jù)經(jīng)驗(yàn),用于當(dāng)前的任務(wù),以解決當(dāng)前領(lǐng)域任務(wù)的數(shù)據(jù)積累不充分的問(wèn)題。而通常地,其他領(lǐng)域的數(shù)據(jù)和當(dāng)前目標(biāo)域任務(wù)的數(shù)據(jù)分布是有差異的。
在NLP、CV任務(wù)中很天然地適合做遷移學(xué)習(xí),各種的CV、NLP預(yù)訓(xùn)練模型層出不窮(如Bert、GPT、MAE)。一個(gè)例子如文本分類(lèi)任務(wù),我們可以借用在其他文本數(shù)據(jù)已經(jīng)訓(xùn)練好的預(yù)訓(xùn)練模型(word2vec、BERT、GPT等等),用于當(dāng)前的新聞分類(lèi)任務(wù)里面。這樣可以大大減少當(dāng)前任務(wù)需要的數(shù)據(jù),而且預(yù)訓(xùn)練模型是基于大樣本學(xué)習(xí)的經(jīng)驗(yàn),做下微調(diào),應(yīng)用效果也杠杠的。
二、風(fēng)控的遷移學(xué)習(xí)
回到金融風(fēng)控任務(wù),需要寄望于遷移學(xué)習(xí)的場(chǎng)景還是挺多的。很經(jīng)常的,業(yè)務(wù)有擴(kuò)展,引入了新的一個(gè)經(jīng)營(yíng)客群,而新的客群樣本量剛開(kāi)始肯定是很少的,這時(shí)就很需要借助下舊客群的數(shù)據(jù)。
而難點(diǎn)在于,風(fēng)控領(lǐng)域很難像NLP領(lǐng)域那樣的文字表示直接遷移,NLP中一個(gè)任務(wù)的文本表示可能就很適用另一文本任務(wù)。
風(fēng)控面對(duì)的主要是結(jié)構(gòu)化數(shù)據(jù)(Tabular Data),一個(gè)任務(wù)的數(shù)據(jù)組成、特征含義就很復(fù)雜多樣了,盡管可以抽取出同一組特征表示,數(shù)據(jù)分布可能也是天差地別。這種情況下怎么做遷移學(xué)習(xí)呢?
下面結(jié)合風(fēng)控的信用評(píng)分卡的任務(wù),具體介紹遷移學(xué)習(xí)方法及項(xiàng)目代碼實(shí)踐。
首先先做下任務(wù)的背景介紹。信用評(píng)分卡是風(fēng)控領(lǐng)域的核心任務(wù)之一,依據(jù)如個(gè)人基本信息、經(jīng)濟(jì)能力、貸款歷史信息,用于判斷借貸用戶(hù)的按時(shí)還款的概率。本文數(shù)據(jù)來(lái)源github.com/aialgorithm/Blog《一文梳理金融風(fēng)控建模全流程(Python)》
引用下遷移學(xué)習(xí)經(jīng)典論文《A survey on transfer learning》,遷移學(xué)習(xí)可以分為四種基本的方法:
- 基于樣本的遷移
- 基于特征的遷移
- 基于模型的遷移
- 基于關(guān)系的遷移(此項(xiàng)相關(guān)研究較少,本文略)。
2.1 基于樣本的遷移
基于樣本的遷移,是通過(guò)遷移源域的某些樣本或設(shè)定樣本權(quán)重到目標(biāo)域?qū)W習(xí)。
基于樣本應(yīng)該是結(jié)構(gòu)化數(shù)據(jù)任務(wù)應(yīng)用較為廣泛的遷移方法,但這類(lèi)方法通常只在領(lǐng)域間分布差異較小時(shí)有效。最簡(jiǎn)單直接的,可以把原領(lǐng)域的樣本直接加入目標(biāo)領(lǐng)域一起學(xué)習(xí)訓(xùn)練,但由于數(shù)據(jù)分布差異的問(wèn)題,這樣可能效果反而更差。
# 代碼來(lái)源:公眾號(hào)-算法進(jìn)階
# 完整代碼:github.com/aialgorithm/Blog 請(qǐng)見(jiàn)文章對(duì)應(yīng)項(xiàng)目代碼
train_xa = pd.concat([train_x,train_source[filter_feas]]) # 直接把全量原領(lǐng)域數(shù)據(jù)加到目標(biāo)領(lǐng)域訓(xùn)練
train_ya = pd.concat([train_y,train_source['isDefault']])
lgb_source=lightgbm.LGBMClassifier(n_estimators=100, num_leaves=4,class_weight= 'balanced',metric = 'AUC',lambda_l1=1,lambda_l2=1)
lgb_source.fit(train_xa, train_ya)
print(train_ya.value_counts())
# 以目標(biāo)領(lǐng)域的測(cè)試集判斷效果
print('train ',model_metrics(lgb_source,train_x, train_y))
print('test ',model_metrics(lgb_source,test_x,test_y))
對(duì)于本項(xiàng)目直接拼湊兩份領(lǐng)域數(shù)據(jù)用于訓(xùn)練,效果提升并不大。相比暴力拼湊,還有有兩種做法可以減少分布差異。
2.1.1 樣本選擇法
只從原領(lǐng)域遷移與目標(biāo)域分布一致(相似)的那部分樣本。
我們可以通過(guò)像one class-SVM 、孤立森林等異常檢測(cè)方法,以目標(biāo)域樣本為訓(xùn)練單分類(lèi)SVM,選取預(yù)測(cè)原領(lǐng)域的樣本中與目標(biāo)域相似概率(閾值)較高的樣本。
from sklearn.svm import OneClassSVM
from sklearn.ensemble import IsolationForest
ir = IsolationForest()#OneClassir()
ir.fit(train_x.fillna(0))
# 預(yù)測(cè)
train_source['isnormal'] = ir.predict(train_source[filter_feas].fillna(0)) #-1為分布異常的樣本
train_source['normal_score'] = ir.score_samples(train_source[filter_feas].fillna(0))+1 #正常程度數(shù)值
train_source['normal_score'].hist(bins=10)
#篩選出分布正常的樣本,拼接訓(xùn)練集
train_xa = pd.concat([train_x,train_source.loc[train_source['isnormal']==1,filter_feas]])
train_ya = pd.concat([train_y,train_source.loc[train_source['isnormal']==1,'isDefault']])
lgb_source.fit(train_xa, train_ya)
print(train_ya.value_counts())
# 以目標(biāo)領(lǐng)域的測(cè)試集判斷效果
print('train ',model_metrics(lgb_source,train_x, train_y))
print('test ',model_metrics(lgb_source,test_x,test_y))
- 或者另一種方式, 結(jié)合我們的信用違約二分類(lèi)模型的任務(wù),也可以通過(guò)目標(biāo)域樣本訓(xùn)練一個(gè)二分類(lèi)模型,通過(guò)分類(lèi)模型對(duì)原領(lǐng)域樣本的預(yù)測(cè)結(jié)果,選取出在原領(lǐng)域中預(yù)測(cè)較準(zhǔn)的那部分樣本(原領(lǐng)域預(yù)測(cè)較準(zhǔn)的樣本說(shuō)明和目標(biāo)領(lǐng)域?qū)W習(xí)的樣本比較一致)。
其中預(yù)測(cè)概率的閾值是個(gè)經(jīng)驗(yàn)值,可以通過(guò)搜索不同閾值下實(shí)際效果確定。另外,可以結(jié)合實(shí)際效果,進(jìn)行多次迭代選擇。
2.1.2 樣本權(quán)重法
對(duì)與目標(biāo)域分布一致性較高的樣本,就給以較高的樣本學(xué)習(xí)權(quán)重。反之亦然。通過(guò)樣本權(quán)重控制對(duì)原領(lǐng)域樣本的有效利用。
- 簡(jiǎn)單點(diǎn)的方法,可以通過(guò)異常檢測(cè)模型評(píng)價(jià)原領(lǐng)域的樣本的正常的程度作為權(quán)重;
import numpy as np
from sklearn.svm import OneClassSVM
from sklearn.ensemble import IsolationForest
ir = IsolationForest()#OneClassir()
ir.fit(train_x.fillna(0))
# 預(yù)測(cè)
train_source['isnormal'] = ir.predict(train_source[filter_feas].fillna(0)) #-1為與目標(biāo)域分布異常的樣本
train_source['normal_score'] = ir.score_samples(train_source[filter_feas].fillna(0))**2 #正常程度數(shù)值
train_source['normal_score'].hist(bins=10)
#拼接訓(xùn)練集
train_xa = pd.concat([train_x,train_source.loc[:,filter_feas]])
train_ya = pd.concat([train_y,train_source.loc[:,'isDefault']])
weights = np.append(np.array([1]*train_x.shape[0]) ,train_source['normal_score']) #目標(biāo)域權(quán)重固定為1,原領(lǐng)域按照正常程度加權(quán)
lgb_source.fit(train_xa, train_ya,sample_weight=weights)
print(train_ya.value_counts())
# 以目標(biāo)領(lǐng)域的測(cè)試集判斷效果
print('train ',model_metrics(lgb_source,train_x, train_y))
print('test ',model_metrics(lgb_source,test_x,test_y))
也可以對(duì)目標(biāo)域,原領(lǐng)域的樣本分別標(biāo)注為’1‘和’0‘標(biāo)簽,訓(xùn)練一個(gè)分類(lèi)模型,驗(yàn)證分類(lèi)模型的預(yù)測(cè)效果,模型效果越好說(shuō)明兩個(gè)領(lǐng)域的不一樣(區(qū)分)程度越明顯,越有必要做樣本選擇或樣本權(quán)重。 這里原領(lǐng)域的樣本權(quán)重可以用歸一化后的odds作為權(quán)重(odds是指一個(gè)事件的發(fā)生比,為該事件發(fā)生的概率與該事件不發(fā)生的概率的比值)。在這里,原領(lǐng)域的樣本權(quán)重就是歸一化后的【樣本屬于目標(biāo)域的概率p(t|x)除以不屬于目標(biāo)域的概率1-p(t|x)】。
或者還有一種方式, 以我們的信用違約二分類(lèi)模型的任務(wù),可以通過(guò)目標(biāo)域樣本訓(xùn)練一個(gè)二分類(lèi)模型,以分類(lèi)模型對(duì)原領(lǐng)域樣本的預(yù)測(cè)結(jié)果的準(zhǔn)確性為權(quán)重。預(yù)測(cè)準(zhǔn)確的樣本權(quán)重越高。權(quán)重=1- abs(預(yù)測(cè)概率 - 實(shí)際標(biāo)簽值),如實(shí)際標(biāo)簽為1的樣本,模型預(yù)測(cè)的概率為0.9權(quán)重就為0.9,預(yù)測(cè)概率如為0.2,與標(biāo)簽差異比較大,權(quán)重就是0.2 也比較小。
#拼接訓(xùn)練集
train_xa = pd.concat([train_x,train_source.loc[:,filter_feas]])
train_ya = pd.concat([train_y,train_source.loc[:,'isDefault']])
train_source['proba'] = lgb_tar.predict_proba(train_source[filter_feas])[:,1]
train_source['samplew'] = 1-abs(train_source['proba']-train_source['isDefault'])
weights = np.append(np.array([1]*train_x.shape[0]) ,train_source['samplew']) #目標(biāo)域權(quán)重固定為1,原領(lǐng)域按照分類(lèi)準(zhǔn)確程度加權(quán)
lgb_source.fit(train_xa, train_ya,sample_weight=weights)
print(train_ya.value_counts())
# 以目標(biāo)領(lǐng)域的測(cè)試集判斷效果
print('train ',model_metrics(lgb_source,train_x, train_y))
print('test ',model_metrics(lgb_source,test_x,test_y))
以上幾種方法都可以在原領(lǐng)域樣本引入一個(gè)先驗(yàn)的樣本權(quán)重,加入到模型訓(xùn)練的sample_weights。其中,目標(biāo)領(lǐng)域的樣本就沒(méi)必要加樣本權(quán)重或加入個(gè)較大的權(quán)重。當(dāng)然,權(quán)重方法的好壞最終還是要結(jié)合實(shí)際效果。
- TrAdaboost
不同于上面直接給一個(gè)固定的樣本權(quán)重,我們還可以在訓(xùn)練學(xué)習(xí)中動(dòng)態(tài)地調(diào)整好權(quán)重,這里有個(gè)現(xiàn)成的遷移學(xué)習(xí)boosting框架TrAdaboost 《Boosting for Transfer Learning 》
TrAdaboost可以通過(guò)動(dòng)態(tài)調(diào)整樣本權(quán)重,在目標(biāo)領(lǐng)域的任務(wù)上利用好原領(lǐng)域的數(shù)據(jù)。權(quán)重的調(diào)整的大概思路:以目標(biāo)域樣本評(píng)估訓(xùn)練損失,對(duì)于造成較大誤差的原領(lǐng)域的那部分樣本,則認(rèn)為其和目標(biāo)領(lǐng)域數(shù)據(jù)差異較大,不斷降低它的權(quán)重。而對(duì)于目標(biāo)領(lǐng)域分類(lèi)錯(cuò)誤的樣本則提升樣本權(quán)重(這個(gè)和Adaboost難樣本一脈相承)。核心代碼如下
"""" 附錄相關(guān)資料
論文地址:cse.hkust.edu.hk/~qyang/Docs/2007/tradaboost.pdf
代碼來(lái)源:github.com/loyalzc/transfer_learning/blob/master/TrAdaboost
論文解讀:by馬東什么zhuanlan.zhihu.com/p/109540481
""""
class TrAdaboost:
def __init__(self, base_classifier=DecisionTreeClassifier(), N=10):
self.base_classifier = base_classifier
self.N = N
self.beta_all = np.zeros([1, self.N])
self.classifiers = []
def fit(self, x_source, x_target, y_source, y_target):
x_train = np.concatenate((x_source, x_target), axis=0)
y_train = np.concatenate((y_source, y_target), axis=0)
x_train = np.asarray(x_train, order='C')
y_train = np.asarray(y_train, order='C')
y_source = np.asarray(y_source, order='C')
y_target = np.asarray(y_target, order='C')
row_source = x_source.shape[0]
row_target = x_target.shape[0]
# 初始化權(quán)重
weight_source = np.ones([row_source, 1]) / row_source
weight_target = np.ones([row_target, 1]) / row_target
weights = np.concatenate((weight_source, weight_target), axis=0)
beta = 1 / (1 + np.sqrt(2 * np.log(row_source / self.N)))
result = np.ones([row_source + row_target, self.N])
for i in range(self.N):
weights = self._calculate_weight(weights)
self.base_classifier.fit(x_train, y_train, sample_weight=weights[:, 0])
self.classifiers.append(self.base_classifier)
result[:, i] = self.base_classifier.predict(x_train)
error_rate = self._calculate_error_rate(y_target,
result[row_source:, i],
weights[row_source:, :])
print("Error Rate in target data: ", error_rate, 'round:', i, 'all_round:', self.N)
if error_rate > 0.5:
error_rate = 0.5
if error_rate == 0:
self.N = i
print("Early stopping...")
break
self.beta_all[0, i] = error_rate / (1 - error_rate)
# 調(diào)整 target 樣本權(quán)重 錯(cuò)誤樣本權(quán)重變大
for t in range(row_target):
weights[row_source + t] = weights[row_source + t] * np.power(self.beta_all[0, i], -np.abs(result[row_source + t, i] - y_target[t]))
# 調(diào)整 source 樣本 錯(cuò)分樣本變小
for s in range(row_source):
weights[s] = weights[s] * np.power(beta, np.abs(result[s, i] - y_source[s]))
def predict(self, x_test):
result = np.ones([x_test.shape[0], self.N + 1])
predict = []
i = 0
for classifier in self.classifiers:
y_pred = classifier.predict(x_test)
result[:, i] = y_pred
i += 1
for i in range(x_test.shape[0]):
left = np.sum(result[i, int(np.ceil(self.N / 2)): self.N] *
np.log(1 / self.beta_all[0, int(np.ceil(self.N / 2)):self.N]))
right = 0.5 * np.sum(np.log(1 / self.beta_all[0, int(np.ceil(self.N / 2)): self.N]))
if left >= right:
predict.append(1)
else:
predict.append(0)
return predict
def predict_prob(self, x_test):
result = np.ones([x_test.shape[0], self.N + 1])
predict = []
i = 0
for classifier in self.classifiers:
y_pred = classifier.predict(x_test)
result[:, i] = y_pred
i += 1
for i in range(x_test.shape[0]):
left = np.sum(result[i, int(np.ceil(self.N / 2)): self.N] *
np.log(1 / self.beta_all[0, int(np.ceil(self.N / 2)):self.N]))
right = 0.5 * np.sum(np.log(1 / self.beta_all[0, int(np.ceil(self.N / 2)): self.N]))
predict.append([left, right])
return predict
def _calculate_weight(self, weights):
sum_weight = np.sum(weights)
return np.asarray(weights / sum_weight, order='C')
def _calculate_error_rate(self, y_target, y_predict, weight_target):
sum_weight = np.sum(weight_target)
return np.sum(weight_target[:, 0] / sum_weight * np.abs(y_target - y_predict))
2.2 基于特征的遷移
基于特征的遷移方法是指將源域和目標(biāo)域的數(shù)據(jù)特征變換到統(tǒng)一特征空間中,來(lái)減少源域和目標(biāo)域之間的差距。它基于這樣的假設(shè):“為了有效的遷移,良好的表征應(yīng)該是對(duì)主要學(xué)習(xí)任務(wù)的區(qū)別性,以及對(duì)源域和目標(biāo)域的不加區(qū)分。”
基于特征的遷移學(xué)習(xí)方法也是一大熱門(mén),大多是與神經(jīng)網(wǎng)絡(luò)的表示學(xué)習(xí)進(jìn)行結(jié)合,應(yīng)用于文本、圖像數(shù)據(jù)等任務(wù)。
對(duì)于結(jié)構(gòu)化數(shù)據(jù),個(gè)人理解基于特征的遷移通常是要結(jié)合樣本、模型層面的遷移。對(duì)應(yīng)著,可以通過(guò)特征加工轉(zhuǎn)換到同一分布減少些差異(如歸一化什么的),以及可以做下特征選擇,尋找既適用于源域又適用于目標(biāo)域的可遷移特征。比如可以通過(guò)特征重要性及穩(wěn)定性PSI進(jìn)行篩選,在原領(lǐng)域 與目標(biāo)領(lǐng)域的分布比較一致的特征。PSI表示的是實(shí)際分布與預(yù)期兩個(gè)分布間的差異,PSI=SUM( (實(shí)際占比 - 預(yù)期占比)* ln(實(shí)際占比 / 預(yù)期占比) )。特征選擇方法可以參考這篇《特征選擇》
特征處理完,再進(jìn)一步選擇合適的樣本或模型遷移方法。
2.3 基于模型的遷移
基于模型的方法是NLP、CV領(lǐng)域較為普遍的思路,通過(guò)大數(shù)據(jù)訓(xùn)一個(gè)預(yù)訓(xùn)練模型,然后下游任務(wù)只需要微調(diào)下就可以用了。
而對(duì)于結(jié)構(gòu)化數(shù)據(jù)任務(wù),這里我們可以采用熟悉的模型融合方式,簡(jiǎn)單高效地利用好原領(lǐng)域的這部分?jǐn)?shù)據(jù)信息,基于模型的方法很大程度也包含了特征層面的加工及遷移。
一個(gè)融合的思路是,我們可以先以原領(lǐng)域的數(shù)據(jù)訓(xùn)練模型A,將模型A的預(yù)測(cè)概率作為特征加入 以目標(biāo)領(lǐng)域訓(xùn)練的模型。這個(gè)思路還是和預(yù)訓(xùn)練微調(diào)很像的。
# 先從原領(lǐng)域?qū)W習(xí)一個(gè)模型
lgb_source=lightgbm.LGBMClassifier(n_estimators=100, num_leaves=6,class_weight= 'balanced',metric = 'AUC',lambda_l1=1,lambda_l2=1)
lgb_source.fit(train_source.loc[:,filter_feas], train_source.loc[:,'isDefault'])
# 原領(lǐng)域模型評(píng)分作為特征輸入目標(biāo)領(lǐng)域模型
train_bank['source_score'] = lgb_source.predict_proba(train_bank.loc[:,filter_feas])[:,1]
train_x, test_x, train_y, test_y = train_test_split(train_bank[filter_feas+['source_score']], train_bank.isDefault,test_size=0.3, random_state=0)
lgb_tar=lightgbm.LGBMClassifier(n_estimators=100, num_leaves=6,class_weight= 'balanced',metric = 'AUC',lambda_l1=1,lambda_l2=1)
lgb_tar.fit(train_x, train_y)
print(train_y.value_counts())
print('train ',model_metrics(lgb_tar,train_x, train_y))
print('test ',model_metrics(lgb_tar,test_x,test_y))
## 輸出特征重要性
from lightgbm import plot_importance
plot_importance(lgb_tar)