本篇文章將會從簡單的線性模型開始,瞭解如何建立一個模型以及建立完模型之後要分析什麼東西,然後學習交叉驗證的思想和技術,並且會構建一個線下測試集,之後我們會嘗試建立更多的模型去解決這個問題,並對比它們的效果,當把模型選擇出來之後,我們還得掌握一些調參的技術發揮模型最大的性能,模型選擇出來之後,也調完參數,但是模型真的就沒有問題了嗎?我們還需要繪製學習率曲線看模型是否存在過擬合或者欠擬合的問題並給出相應的解決方法

大綱如下:

  • 從最簡單的模型開始(線性迴歸 & 交叉驗證 & 構建線下測試集)
  • 評估算法模型的框架(這裏會給出一個選擇模型的框架,適合遷移)
  • 模型的調參技術(貪心調參, GridSearchCV調參和貝葉斯調參)
  • 繪製訓練集曲線與驗證集曲線(從曲線分析過擬合欠擬合的問題,以及如果發生了這些問題,我們應該怎麼去嘗試解決)
  • 總結

1. 從簡單的線性模型開始

二手車交易價格預測 比賽是一個迴歸問題,所以需要選擇一些迴歸模型來解決,線性模型就是一個比較簡單的迴歸模型了,所以我們就從這個模型開始,看看針對這個模型,我們會得到什麼結果以及這些結果究竟是什麼含義

線性迴歸(Linear Regression)是利用最小平方損失函數對一個或多個自變量和因變量之間關係進行建模的一種迴歸分析。簡單的說,假設預測的二手車價格用$Y$來表示,而我們構造的特徵用$x_i$,之後就可以建立如下的等式來描述它們的關係

$$ Y=w_1x_1+w_2x_2+...+w_nx_n+b $$

訓練模型其實就是根據訓練集的$(x_1,x_2,...,x_n,Y)$樣本求出合適權重$(w_1,w_2,...,w_n)$的過程

首先導入特徵工程處理完畢後保存的數據

# 導入之前處理好的數據
data = pd.read_csv('./pre_data/pre_data.csv')
data.head()

# 然後訓練集和測試集分開
train = data[:train_data.shape[0]]
test = data[train_data.shape[0]:]    # 這個先不用

# 選擇那些數值型的數據特徵
continue_fea = ['power', 'kilometer', 'v_2', 'v_3', 'v_4', 'v_5', 'v_6', 'v_10', 'v_11', 'v_12', 'v_14',
                'v_std', 'fuelType_price_average', 'gearbox_std', 'bodyType_price_average', 'brand_price_average',
                'used_time', 'estivalue_price_average', 'estivalueprice_std', 'estivalue_price_min']
train_x = train[continue_fea]
train_y = train_data['price']

然後建立線性模型,直接使用sklearn庫,非常簡單

from sklearn.linear_model import LinearRegression

model = LinearRegression(normalize=True)
model.fit(train_x, train_y)

通過上面兩行代碼,其實就已經建立並且訓練完了一個線性模型,接下來可以查看一下模型的一些參數(w&b)

"""查看訓練的線性迴歸模型的截距(intercept)與權重(coef)"""
print('intercept: ' + str(model.intercept_))
sorted(dict(zip(continue_fea, model.coef_)).items(), key=lambda x: x[1], reverse=True)


## 結果:
intercept: -178881.74591832393
[('v_6', 482008.29891714785),
 ('v_std', 23713.66414841167),
 ('v_10', 7035.056136559963),
 ('v_14', 1418.4037751433352),
 ('used_time', 186.48306334062053),
 ('power', 12.19202369791551),
 ('estivalue_price_average', 0.4082359327905722),
 ('brand_price_average', 0.38196351334425965),
 ('gearbox_std', 0.1716754674248321),
 ('fuelType_price_average', 0.023785798378739224),
 ('estivalueprice_std', -0.016868767797045624),
 ('bodyType_price_average', -0.21364358471329278),
 ('kilometer', -155.11999534761347),
 ('estivalue_price_min', -574.6952072539285),
 ('v_11', -1164.0263997737668),
 ('v_12', -1953.0558048250668),
 ('v_4', -2198.03802357537),
 ('v_3', -3811.7514971187525),
 ('v_2', -5116.825271420712),
 ('v_5', -447495.6394686485)]

上面的這些就是等式中每個$x_1$前面的係數$w_i$, intercept代表$b$

如果已經有了一系列$(x_1,x_2,...,x_n)$的樣本,要預測$y$,只需要下面一句話

y_pred = model.predict(x_test)

雖然線性模型非常簡單,但是關於線性模型還有些重要的東西我們得了解一下,比如從這些權重中如何看出哪個特徵對線性模型來說更加重要些?這個其實我們看的是權重的絕對值,因爲正相關和負相關都是相關,越大的說明那個特徵對線性模型影響就越大

其次,我們還可以看一下線性迴歸的訓練效果,繪製一下v_6這個特徵和標籤的散點圖:

subsample_index = np.random.randint(low=0, high=len(train_y), size=50)

plt.scatter(train_x['v_6'][subsample_index], train_y[subsample_index], color='black')
plt.scatter(train_x['v_6'][subsample_index], model.predict(train_x.loc[subsample_index]), color='blue')
plt.xlabel('v_6')
plt.ylabel('price')
plt.legend(['True Price','Predicted Price'],loc='upper right')
print('The predicted price is obvious different from true price')
plt.show()

結果如下:

從上圖中我們可以發現發現模型的預測結果(藍色點)與真實標籤(黑色點)的分佈差異較大,且部分預測值出現了小於0的情況,說明我們的模型存在一些問題。 這個還是需要會看的,從這裏我們也可以看出或許price這個需要處理一下

price的分佈圖如下:

通過這張圖我們發現price呈長尾分佈,不利於我們的建模預測。原因是很多模型都假設數據誤差項符合正態分佈,而長尾分佈的數據違背了這一假設。參考博客: https://blog.csdn.net/Noob_daniel/article/details/76087829

所以我們可以先嚐試取個對數

train_y_ln = np.log1p(train_y)
print('The transformed price seems like normal distribution')
plt.figure(figsize=(15,5))
plt.subplot(1,2,1)
sns.distplot(train_y_ln)
plt.subplot(1,2,2)
sns.distplot(train_y_ln[train_y_ln < np.quantile(train_y_ln, 0.9)])

這樣效果就好多了,然後重新訓練一下

model = model.fit(train_x, train_y_ln)

print('intercept:'+ str(model.intercept_))
sorted(dict(zip(continue_fea, model.coef_)).items(), key=lambda x:x[1], reverse=True)

這個權重結果就不顯示了,只畫出v_6和price的散點圖看一下:

到此,線性模型就結束了。雖然我們的主要模型並不是線性模型,而且線性模型很少用到,但是通過這個過程,我們可以學到一些東西:訓練和預測模型的步驟和線性模型基本上是一致的,依然是 .fit(X,Y).predict(X_test) 方法。所以在這裏先體會一下如何建立一個模型,並且對它進行訓練和預測

1.1 交叉驗證

在使用數據集對參數進行訓練的時候,經常會發現人們通常會將整個訓練集分爲三個部分:訓練集、驗證集和測試集。這其實是爲了保證訓練效果而特意設置的。測試集很好理解,就是完全不參與訓練的過程,僅僅用來觀測測試效果的數據。而訓練集和驗證集則牽涉到下面的知識

因爲在實際的訓練中,訓練的結果對於訓練集的擬合程度通常還是挺好的(初始條件敏感),但是對於訓練集之外的數據的擬合程度通常就不那麼令人滿意了。因此我們通常並不會把所有的數據集都拿來訓練,而是分出一部分來(這一部分不參加訓練)對訓練集生成的模型進行測試,相對客觀的判斷這個模型對訓練集之外的數據的符合程度。在驗證中,比較常用的就是K折交叉驗證了,它可以有效的避免過擬合,最後得到的結果也比較具有說服性

K折交叉驗證是將原始數據分成K組,將每個子集數據分別做一次驗證集,其餘的K-1組子集數據作爲訓練集,這樣會得到K個模型,用這K個模型最終的驗證集分類準確率的平均數,作爲此K折交叉驗證下分類器的性能指標。以下圖爲例:

關於K折交叉驗證詳細的原理這裏就不描述了,其實很好理解,就拿這個比賽來說,我們訓練集共150000個樣本,假設做5折交叉驗證,就是把這150000個樣本分成5份,每份30000個樣本,訓練模型的時候,選其中四份作爲訓練集訓練模型,然後在另一份上進行預測得到一個結果。這樣,這五份輪流着做一遍測試集正好就是循環了五輪,得到了五個分數,然後取平均即可。這樣的好處就是防止模型更加偏向某份數據,也能看出是否模型存在過擬合

交叉驗證,sklearn中提供了一個函數,叫做 cross_val_score ,我們就是用這個函數實現交叉驗證,函數具體的作用可以去查一下sklearn的官方文檔

from sklearn.model_selection import cross_val_score
from sklearn.metrics import mean_absolute_error, make_scorer

def log_transfer(func):
    def wrapper(y, yhat):
        result = func(np.log(y), np.nan_to_num(np.log(yhat)))   # 這個是爲了解決不合法的值的
        return result
    return wrapper

# 下面是交叉驗證
scores = cross_val_score(model, X=train_x, y=train_y, verbose=1, cv=5, scoring=make_scorer(log_transfer(mean_absolute_error)))

# 使用線性迴歸模型,對未處理標籤的特徵數據進行五折交叉驗證(Error 1.36)
print('AVG:', np.mean(scores))

# 對處理的標籤交叉驗證
scores = cross_val_score(model, X=train_x, y=train_y_ln, verbose=1, cv = 5, scoring=make_scorer(mean_absolute_error))
print('AVG:', np.mean(scores))

# 輸出五次的驗證結果:
scores = pd.DataFrame(scores.reshape(1,-1))
scores.columns = ['cv' + str(x) for x in range(1, 6)]
scores.index = ['MAE']
scores

得到的結果如下:

cv1 cv2 cv3 cv4 cv5
0.194979 0.195399 0.19679 0.19257 0.197563

最後多說一點,k折交叉驗證,並不適合處理時間序列數據,因爲時間序列是有先後關係的。就拿這次比賽來說,通過2018年的二手車價格預測2017年的二手車價格,顯然是不合理的,因此我們還可以採用時間順序對數據集進行分隔。在本例中,我們選用靠前時間的4/5樣本當作訓練集,靠後時間的1/5當作驗證集,最終結果與五折交叉驗證差距不大

split_point = len(train_x) // 5 * 4

# 訓練集
xtrain = train_x[:split_point]
ytrain = train_y[:split_point]

# 測試集
xval = train_x[split_point:]
yval = train_y[split_point:]
ytrain_ln = np.log1p(ytrain)
yval_ln = np.log1p(yval)

# 訓練
model.fit(xtrain, ytrain_ln)
mean_absolute_error(yval_ln, model.predict(xval))

1.2 構建一個線下測試集

這裏是簡單的介紹一個小技巧吧,當然這裏是針對這個比賽,因爲有時候我們發現在本地上訓練數據集得到的結果很好,但是放到線上進行測試的時候往往不是那麼理想,這就意味着我們線下的訓練有些過擬合了,而我們一般並不能發現這種情況,畢竟對於線上的測試,我們沒有真實的標籤對比不,所以我們可以先構建一個線下的測試集。這個實操起來也很簡單,就是我們有150000個樣本,可以用100000個樣本來做訓練集,後面的50000做測試集,因爲我們已經知道這50000個樣本的真實標籤,這樣訓練出來的模型我們就可以直接先測試一下泛化能力,對於後面的調參或者是模型的評估等感覺還是挺好用的

# 導入數據
data = pd.read_csv('./pre_data/pre_data.csv')

train = data[:train_data.shape[0]]
test = data[train_data.shape[0]:]    # 這個先不用

# 選數據
X = train[:100000]
Y= train_data['price'][:100000]
Y_ln = np.log1p(Y)

XTest = train[100000:]   # 模擬一個線下測試集, 看看模型的泛化能力
Ytrue = train_data['price'][100000:]

2. 評估模型的框架

模型選擇的時候,可以根據數據的特徵和優化目標先選出很多個模型作爲備選,因爲我們分析完數據不能立刻得出哪個算法對需要解決的問題更有效

就拿這個比賽來說,我們直觀上認爲由於問題是預測價格,所以這是一個迴歸問題,肯定使用迴歸模型(Regressor系列),但是迴歸模型太多,但我們又知道部分數據呈線性分佈,線性迴歸和正則化的迴歸算法可能對解決問題比較有效。而由於數據的離散化,通過決策樹算法及相應的集成算法也一般會表現出色,所以我們可以鎖定幾個模型都嘗試一下

我一般習慣建立一個字典,把這些模型放到字典裏面,然後分別進行交叉驗證,可視化結果來判斷哪個模型針對當前問題表現比較好,這樣從這裏面選出3-4個進行下面的環節,也就是模型的調參工作。這裏給出一個我常用的一個評估算法模型的一個框架。首先採用10交叉驗證來分離數據,通過絕對值誤差來比較算法的準確度,誤差值越小,準確度越高

num_folds = 10
seed = 7

# 把所有模型寫到一個字典中
models = {}
models['LR'] = LinearRegression()
models['Ridge'] = Ridge()
models['LASSO'] = Lasso()
models['DecisionTree'] = DecisionTreeRegressor()
models['RandomForest'] = RandomForestRegressor()
models['GradientBoosting'] = GradientBoostingRegressor()
models['XGB'] = XGBRegressor(n_estimators = 100, objective='reg:squarederror')
models['LGB'] = LGBMRegressor(n_estimators=100)
#models['SVR'] = SVR()   # 支持向量機運行不出來

results = []
for key in models:
    kfold = KFold(n_splits=num_folds, random_state=seed)
    cv_result = cross_val_score(models[key], X, Y_ln, cv=kfold, scoring=make_scorer(mean_absolute_error))
    results.append(cv_result)
    print('%s: %f (%f)' % (key, cv_result.mean(), cv_result.std()))
    
# 評估算法 --- 箱線圖
fig1 = plt.figure(figsize=(15, 10))
fig1.suptitle('Algorithm Comparison')
ax = fig1.add_subplot(111)
plt.boxplot(results)
ax.set_xticklabels(models.keys())
plt.show()


## 結果:
LR: 0.192890 (0.001501)
Ridge: 0.196279 (0.001616)
LASSO: 0.515573 (0.003923)
DecisionTree: 0.190959 (0.002524)
RandomForest: 0.142333 (0.001489)
GradientBoosting: 0.178403 (0.001903)
XGB: 0.178492 (0.001441)
LGB: 0.147875 (0.001397)

看一下箱線圖的結果:

這樣,各個模型的效果就一目瞭然了,從上圖可以看出,隨機森林和LGB的效果還是好一些的,後面可以基於這兩個進行調參,當然xgboost的效果可能由於參數的原因表現不是那麼理想,這裏也作爲了我們調參備選

那麼調參究竟有沒有影響呢?我這裏做了一個實驗,可以先看一下:

model2 = LGBMRegressor(n_estimators=100)
model2.fit(X, Y_ln)
pred2 = model2.predict(XTest)
print("mae: ", mean_absolute_error(Ytrue, np.expm1(pred2)))

# 結果:
mae:  713.9408513079144

上面這個是沒有調參的LGB,下面再看一下調參的LGB:

def bulid_modl_lgb(x_train, y_train):
    estimator = LGBMRegressor(num_leaves=127, n_estimators=150)
    param_grid = {'learning_rage': [0.01, 0.05, 0.1, 0.2]}
    gbm = GridSearchCV(estimator, param_grid)
    gbm.fit(x_train, y_train)
    return gbm
 
model_lgb = bulid_modl_lgb(X, Y_ln)
val_lgb = model_lgb.predict(XTest)
MAE_lgb = mean_absolute_error(Ytrue, np.expm1(val_lgb))
print(MAE_lgb)


## 結果:
591.4221480289154

同樣的LGB,調參誤差能降到591,不調參713,所以調參還是很重要的。但是在調參之前,先給出一個正態化模板

from sklearn.pipeline import Pipeline

pipelines = {}
pipelines['ScalerLR'] = Pipeline([('Scaler', StandardScaler()), ('LR', LinearRegression())])
pipelines['ScalerRidge'] = Pipeline([('Scaler', StandardScaler()), ('Ridge', Ridge())])
pipelines['ScalerLasso'] = Pipeline([('Scaler', StandardScaler()), ('Lasso', Lasso())])
pipelines['ScalerTree'] = Pipeline([('Scaler', StandardScaler()), ('Tree', DecisionTreeRegressor())])
pipelines['ScalerForest'] = Pipeline([('Scaler', StandardScaler()), ('Forest', RandomForestRegressor())])
pipelines['ScalerGBDT'] = Pipeline([('Scaler', StandardScaler()), ('GBDT', GradientBoostingRegressor())])
pipelines['ScalerXGB'] = Pipeline([('Scaler', StandardScaler()), ('XGB', XGBRegressor(n_estimators = 100, objective='reg:squarederror'))])
pipelines['ScalerLGB'] = Pipeline([('Scaler', StandardScaler()), ('LGB', LGBMRegressor(n_estimators=100))])

results = []
for key in pipelines:
    kfold = KFold(n_splits=num_folds, random_state=seed)
    cv_result = cross_val_score(pipelines[key], X, Y_ln, cv=kfold, scoring=make_scorer(mean_absolute_error))
    results.append(cv_result)
    print('%s: %f (%f)' % (key, cv_result.mean(), cv_result.std()))


# 評估算法 --- 箱線圖
fig2 = plt.figure(figsize=(15, 10))
fig2.suptitle('Algorithm Comparison')
ax = fig2.add_subplot(111)
plt.boxplot(results)
ax.set_xticklabels(models.keys())

我這裏不用正態化,因爲我試驗了一下,效果不如之前的好

3. 模型調參

同特徵工程一樣,模型參數調節也是一項非常繁瑣但又非常重要的工作

根據模型複雜程度的不同,需要調節的參數數量也不盡相同。簡單如邏輯迴歸,需要調節的通常只有正則項係數C;複雜如隨機森林,需要調節的變量會多出不少,最核心的如樹的數量n_estimators,樹的深度max_depth等等。參數越多,調參的難度自然也越來越大,因爲參數間排列組合的可能性越來越多。在訓練樣本比較少的情況下,sklearn的GridSearchCV是個不錯的選擇,可以幫助我們自動尋找指定範圍內的最佳參數組合。但實際情況是,GridSearch通常需要的運行時間過長,長到我們不太能夠忍受的程度。所以更多的時候需要我們自己手動先排除掉一部分數值,然後使用GridSearch自動調參

模型調參有三種方式:

  • 貪心調參
  • 網格搜索調參
  • 貝葉斯調參

這裏給出一個模型可調參數及範圍選取的參考:

下面我以LGB作爲實驗,因爲其他的模型也都是這個思路,所以爲了減少篇幅,只對LGB做實驗

objective = ['regression', 'regression_l1', 'mape', 'huber', 'fair']
num_leaves = [10, 55, 70, 100, 200]
max_depth = [ 10, 55, 70, 100, 200]
n_estimators = [200, 400, 800, 1000]
learning_rate =  [0.01, 0.05, 0.1, 0.2]

3.1 貪心調參

拿當前對模型影響最大的參數調優,直到最優化;再拿下一個影響最大的參數調優,如此下去,直到所有的參數調整完畢。這個方法的 缺點就是可能會調到局部最優而不是全局最優,但是省時間省力 ,巨大的優勢面前,可以一試

# 先建立一個參數字典
best_obj = dict()

# 調objective
for obj in objective:
    model = LGBMRegressor(objective=obj)
    score = np.mean(cross_val_score(model, X, Y_ln, verbose=0, cv=5, scoring=make_scorer(mean_absolute_error)))
    best_obj[obj] = score
 
# 上面調好之後,用上面的參數調num_leaves
best_leaves = dict()
for leaves in num_leaves:
    model = LGBMRegressor(objective=min(best_obj.items(), key=lambda x:x[1])[0], num_leaves=leaves)
    score = np.mean(cross_val_score(model, X, Y_ln, verbose=0, cv=5, scoring=make_scorer(mean_absolute_error)))
    best_leaves[leaves] = score

# 用上面兩個最優參數調max_depth
best_depth = dict()
for depth in max_depth:
    model = LGBMRegressor(objective=min(best_obj.items(), key=lambda x:x[1])[0],
                          num_leaves=min(best_leaves.items(), key=lambda x: x[1])[0],
                          max_depth=depth)
    score = np.mean(cross_val_score(model, X, Y_ln, verbose=0, cv=5, scoring=make_scorer(mean_absolute_error)))
    best_depth[depth] = score

# 調n_estimators
best_nstimators = dict()
for nstimator in n_estimators:
    model = LGBMRegressor(objective=min(best_obj.items(), key=lambda x:x[1])[0],
                          num_leaves=min(best_leaves.items(), key=lambda x: x[1])[0],
                          max_depth=min(best_depth.items(), key=lambda x:x[1])[0],
                          n_estimators=nstimator)
    
    score = np.mean(cross_val_score(model, X, Y_ln, verbose=0, cv=5, scoring=make_scorer(mean_absolute_error)))
    best_nstimators[nstimator] = score
 
# 調learning_rate
best_lr = dict()
for lr in learning_rate:
    model = LGBMRegressor(objective=min(best_obj.items(), key=lambda x:x[1])[0],
                          num_leaves=min(best_leaves.items(), key=lambda x: x[1])[0],
                          max_depth=min(best_depth.items(), key=lambda x:x[1])[0],
                          n_estimators=min(best_nstimators.items(), key=lambda x:x[1])[0],
                          learning_rate=lr)
    score = np.mean(cross_val_score(model, X, Y_ln, verbose=0, cv=5, scoring=make_scorer(mean_absolute_error)))
    best_lr[lr] = score

上面的過程建議放在不同的cell裏面運行,之後可視化這個過程的結果:

sns.lineplot(x=['0_initial','1_turning_obj','2_turning_leaves',
               '3_turning_depth','4_turning_estimators', '5_turning_lr'],
            y=[0.143 ,min(best_obj.values()), min(best_leaves.values()), min(best_depth.values()),
              min(best_nstimators.values()), min(best_lr.values())])

貪心的調參策略還是不錯的,我們還可以打印最後調參的結果:

print("best_obj:", min(best_obj.items(), key=lambda x: x[1]))
print("best_leaves:", min(best_leaves.items(), key=lambda x: x[1]) )
print('best_depth:', min(best_depth.items(), key=lambda x: x[1]))
print('best_nstimators: ', min(best_nstimators.items(), key=lambda x: x[1]))
print('best_lr:', min(best_lr.items(), key=lambda x: x[1]))


## 結果如下:
best_obj: ('regression_l1', 0.1457016215267976)
best_leaves: (100, 0.132929241004274)
best_depth: (20, 0.13275966837758682)
best_nstimators:  (1000, 0.11861541074643345)
best_lr: (0.05, 0.11728267187328578)

3.2 GridSearchCV調參

GridSearchCV,它存在的意義就是自動調參,只要把參數輸進去,就能給出最優化的結果和參數。但是這個方法適合於小數據集,一旦數據的量級上去了,很難得出結果。這個在這裏面優勢不大, 因爲數據集很大,不太能跑出結果,但是我也整理一下,有時候還是很好用的

from sklearn.model_selection import GridSearchCV

# 這個我這邊電腦運行時間太長,先不跑了
parameters = {'objective':objective, 'num_leaves':num_leaves, 'max_depth':max_depth,
             'n_estimators': n_estimators, 'learning_rate':learning_rate}

model = LGBMRegressor()
clf = GridSearchCV(model, parameters, cv=5)
clf = clf.fit(X, Y_ln)

# 輸出最優參數
clf.best_params_

3.3 貝葉斯調參

首先需要安裝包 pip install bayesian-optimization

貝葉斯優化用於機器學習調參,主要思想是,給定優化的目標函數(廣義的函數,只需指定輸入和輸出即可,無需知道內部結構以及數學性質),通過不斷地添加樣本點來更新目標函數的後驗分佈(高斯過程,直到後驗分佈基本貼合於真實分佈。簡單的說,就是考慮了上一次參數的信息,從而更好的調整當前的參數

它與常規的網格搜索或者隨機搜索的區別是:

  • 貝葉斯調參採用高斯過程,考慮之前的參數信息,不斷地更新先驗;網格搜索未考慮之前的參數信息
  • 貝葉斯調參迭代次數少,速度快;網格搜索速度慢,參數多時易導致維度爆炸
  • 貝葉斯調參針對非凸問題依然穩健;網格搜索針對非凸問題易得到局部最優

使用方法:

  • 定義優化函數(rf_cv,在裏面把優化的參數傳入,然後建立模型,返回要優化的分數指標)
  • 定義優化參數
  • 開始優化(最大化分數還是最小化分數等)
  • 得到優化結果
from  bayes_opt import BayesianOptimization

# 定義優化函數
def rf_cv(num_leaves, max_depth, subsample, min_child_samples):
    model = LGBMRegressor(objective='regression_l1', num_leaves=int(num_leaves),
                         max_depth=int(max_depth), subsample=subsample,
                         min_child_samples = int(min_child_samples))
    val = cross_val_score(model, X, Y_ln, verbose=0, cv=5, scoring=make_scorer(mean_absolute_error)).mean()
    
    return 1-val

# 定義優化參數
rf_bo = BayesianOptimization(
    rf_cv, 
    {
        'num_leaves':(2, 100),
        'max_depth':(2, 100),
        'subsample':(0.1, 1),
        'min_child_samples':(2, 100)
    }
)

#開始優化
num_iter = 25
init_points = 5
rf_bo.maximize(init_points=init_points,n_iter=num_iter)

#顯示優化結果
rf_bo.res["max"]


#附近搜索(已經有不錯的參數值的時候)
rf_bo.explore(
     {'n_estimators': [10, 100, 200],
      'min_samples_split': [2, 10, 20],
      'max_features': [0.1, 0.5, 0.9],
      'max_depth': [5, 10, 15]
     })

基於上面的思路,我們也可以對隨機森林進行調參:

對Random Forest來說,增加“子模型數”(n_estimators)可以明顯降低整體模型的方差,且不會對子模型的偏差和方差有任何影響。模型的準確度會隨着“子模型數”的增加而提高。由於減少的是整體模型方差公式的第二項,故準確度的提高有一個上限。在不同的場景下,“分裂條件”(criterion)對模型的準確度的影響也不一樣,該參數需要在實際運用時靈活調整。調整“最大葉節點數”(max_leaf_nodes)以及“最大樹深度”(max_depth)之一,可以粗粒度地調整樹的結構:葉節點越多或者樹越深,意味着子模型的偏差越低,方差越高;同時,調整“分裂所需最小樣本數”(min_samples_split)、“葉節點最小樣本數”(min_samples_leaf)及“葉節點最小權重總值”(min_weight_fraction_leaf),可以更細粒度地調整樹的結構:分裂所需樣本數越少或者葉節點所需樣本越少,也意味着子模型越複雜。一般來說,我們總採用bootstrap對樣本進行子採樣來降低子模型之間的關聯度,從而降低整體模型的方差。適當地減少“分裂時考慮的最大特徵數”(max_features),給子模型注入了另外的隨機性,同樣也達到了降低子模型之間關聯度的效果。詳細的可以參考:

# 定義優化函數
def rf_cv(n_estimators,  max_depth):
    model = RandomForestRegressor(n_estimators=int(n_estimators), 
                         max_depth=int(max_depth))
    val = cross_val_score(model, X, Y_ln, verbose=0, cv=5, scoring=make_scorer(mean_absolute_error)).mean()
    
    return 1-val

rf_bo = BayesianOptimization(
    rf_cv, 
    {
        'n_estimators':(100, 200),
        'max_depth':(2, 100)
    }
)

rf_bo.maximize()

4. 繪製訓練集曲線與驗證集曲線

從上面的步驟中,我們通過算法模型的評估框架選擇出了合適的幾個模型,又通過模型的調參步驟確定了模型的合適參數,這樣我們基本上就得到了一個我們認爲的比較好的模型了,但是這個模型真的就是好的模型了嗎? 我們還不能確定是否存在過擬合或者欠擬合問題,在實際中究竟應該怎麼判斷? 學習曲線的繪製就是一個非常好的方式,可以幫助我們看一下我們調試好的模型還有沒有過擬合或者欠擬合的問題

關於學習曲線:

  • 學習曲線是不同訓練集大小,模型在訓練集和驗證集上的得分變化曲線
  • 學習曲線圖的橫座標是x_train的數據量,縱座標是對應的train_score,test_score。隨着訓練樣本的逐漸增加,算法練出的模型的表現能力;

繪製學習曲線非常簡單

train_sizes,train_scores,test_score = learning_curve(estimator, X, y, groups=None, train_sizes=array([0.1, 0.33, 0.55, 0.78, 1. ]), cv=’warn’, scoring=None)

主要的參數說明如下:

通過cv設置交叉驗證,取幾次(組)數據,train_sizes設置每一次取值,在不同訓練集大小上計算得分

  • estimator:估計器,用什麼模型進行學習;
  • cv:交叉驗證生成器,確定交叉驗證拆分策略;

畫訓練集的曲線時,橫軸爲train_sizes, 縱軸爲train_scores_mean; train_scores爲二維數組,行代表train_sizes不同時的得分,列表示取cv組數據。

畫測試集的曲線時:橫軸爲train_sizes, 縱軸爲test_scores_mean; test_scores爲二維數組

learning_curve爲什麼運行時間那麼長:模型要進行train_sizes * cv次運行

那麼,我們就基於一個訓練好的模型,畫一下學習曲線,看看這個學習曲線究竟怎麼觀察:

from sklearn.model_selection import learning_curve, validation_curve

def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None, n_jobs=1, train_size=np.linspace(.1, 1.0, 5)):
    plt.figure()
    plt.title(title)
    if ylim is not None:
        plt.ylim(*ylim)
    plt.xlabel('Training example')  
    plt.ylabel('score')  
    train_sizes, train_scores, test_scores = learning_curve(estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_size, scoring = make_scorer(mean_absolute_error))  
    train_scores_mean = np.mean(train_scores, axis=1)  
    train_scores_std = np.std(train_scores, axis=1)  
    test_scores_mean = np.mean(test_scores, axis=1)  
    test_scores_std = np.std(test_scores, axis=1)  
    plt.grid()#區域  
    plt.fill_between(train_sizes, train_scores_mean - train_scores_std,  
                     train_scores_mean + train_scores_std, alpha=0.1,  
                     color="r")  
    plt.fill_between(train_sizes, test_scores_mean - test_scores_std,  
                     test_scores_mean + test_scores_std, alpha=0.1,  
                     color="g")  
    plt.plot(train_sizes, train_scores_mean, 'o-', color='r',  
             label="Training score")  
    plt.plot(train_sizes, test_scores_mean,'o-',color="g",  
             label="Cross-validation score")  
    plt.legend(loc="best")  
    return plt  

# 假設已經調好了LGB的參數,我們可以繪製一下曲線看看這個模型有沒有什麼問題
model = LGBMRegressor(n_estimators=1000, leaves=200, learning_rate=0.05, objective='regression_l1')
model.fit(X, Y_ln)
pred2 = model.predict(XTest)
print("mae: ", mean_absolute_error(Ytrue, np.expm1(pred2)))

# 畫出學習曲線
plot_learning_curve(model, 'LGB', X[:10000], Y_ln[:10000], ylim=(0.0, 1), cv=5, n_jobs=1)

下面整理一下如何觀察學習曲線

learning_curve裏面有個scoring參數可以設置你想求的值,分類可以設置 accuracy ,迴歸問題可以設置 neg_mean_squared_error ,總體來說,值都是越大越好,但是注意如果模型設置的是 mae erro ,那就是越低越好

高偏差和高方差應該怎麼看呢?引用一個博客裏面的圖片

什麼情況欠擬合:模型在訓練集和驗證集上準確率相差不大,卻都很差,說明模型對已知數據和未知數據都不能準確預測,屬於高偏差。左上角那個圖

什麼情況過擬合:模型在訓練集和驗證集上的準確率差距很大,說明模型能夠很好的擬合已知數據,但是泛化能力很差,屬於高方差。右上角那個圖

右下角那個圖是比較合適的。所以上面lgb的那個模型效果還是不錯的

相關文章