零基礎數據挖掘入門系列(五) - 模型建立與調參

思維導圖:零基礎入門數據挖掘的學習路徑

1. 寫在前面

零基礎入門數據挖掘是記錄自己在Datawhale舉辦的數據挖掘專題學習中的所學和所想, 該系列筆記使用理論結合實踐的方式,整理數據挖掘相關知識,提升在實際場景中的數據分析、數據清洗,特徵工程、建模調參和模型融合等技能。所以這個系列筆記共五篇重點內容, 也分別從上面五方面進行整理學習, 既是希望能對知識從實戰的角度串聯回憶,加強動手能力的鍛鍊,也希望這五篇筆記能夠幫助到更多喜歡數據挖掘的小夥伴,我們一起學習,一起交流吧。

既然是理論結合實踐的方式,那麼我們是從天池的一個二手車交易價格預測比賽出發進行學習,既可以學習到知識,又可以學習如何入門一個數據競賽, 下面我們開始吧。

今天是本系列的第五篇文章模型的建立與調參部分,這一塊也是很費時間且核心的一部分工作,畢竟特徵工程也好,數據清洗也罷,都是爲最終的模型來服務的,模型的建立和調參才決定了最終的結果,上限是一回事, 如何更好的去達到這個上限又是一回事, 前一篇我們已經做完了特徵工程,好像是有了一個上限,到底應該怎麼去達到這個上限呢? 就需要看我們的模型表現了。所以今天重點整理模型的評估和調參的相關技術。

首先,我們會先從簡單的線性模型開始,看看如何去建立一個模型以及建立完模型之後要去分析什麼東西, 然後我們會學習交叉驗證的思想和技術(這個在評估模型的效果是經常會用到)並且會構建一個線下測試集(當然這個針對本比賽), 上面的這些算是建立模型的基礎,然後我們會嘗試建立更多的模型去解決這個問題,並對比它們的效果, 當把模型選擇出來之後,我們還得掌握一些調參的技術發揮模型最大的性能, 模型選擇出來之後,也調完參數,但是模型真的就沒有問題了嗎? 我們其實不知道, 所以最後我們學習繪製學習率曲線看模型是否存在過擬合或者欠擬合的問題並給出相應的解決方法。

大綱如下:

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

PS: 本文默認學習者已經具備了機器學習和數據挖掘的基礎知識,已經知道了一些基本概念,比如過擬合,欠擬合,正則化,模型複雜度等。 所以關於這些基礎知識,本文不會過多的贅述,遇到了會提一下,如果沒有掌握這些基本概念,建議先去補一下這些基本的概念,可以參考後面的第一篇鏈接。並且關於模型的原理部分,這裏也不展開論述, 因爲關於這個比賽後面我嘗試用了五六種模型進行試驗,如果單純講這些模型的原理,篇幅會超級長,也不是這個系列需要整理的問題了, 這些東西我都放在了鏈接裏面。

Ok, let’s go!

2. 我們先從簡單的線性模型開始

這部分也算是一個熱身了,我們這個比賽是關於價格預測的,我們也知道了這是個迴歸的問題, 那麼對於迴歸的問題,我們肯定是要選擇一些迴歸模型來解決,線性模型就是一個比較簡單的迴歸模型了, 所以我們就從這個模型開始,看看針對這個模型,我們會得到什麼結果以及這些結果究竟是什麼含義。

那麼什麼是線性迴歸模型, 線性迴歸(Linear Regression)是利用稱爲線性迴歸方程的最小平方函數對一個或多個自變量和因變量之間關係進行建模的一種迴歸分析, 簡單的說,假設我們預測的二手車價格我們用YY來表示, 而我們構造的特徵我們用xix_i, 我們就可以建立這樣一個等式來表示它們的關係:
Y=w1x1+w2x2+....+wnxn+bY = w_1x_1+w_2x_2+....+w_nxn+b

訓練模型的過程其實就是根據訓練集的這些(X,Y)(X, Y)樣本來求出合適的權重ww, 然後對新的測試集XX預測相應的YtestY_{test}, 這個YtestY_{test}其實就是我們想要的答案。這就是這部分的邏輯,下面看實現。

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

# 導入之前處理好的數據
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']

可以看一下,我這邊處理的data數據的結果:
在這裏插入圖片描述
我這邊data會有38個特徵。

下面,我們建立線性模型,建立模型如果用sklearn的話還是非常簡單的,包括訓練和預測。

from sklearn.linear_model import LinearRegression

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

通過上面這兩行代碼,我們其實就建立了線性模型,並完成了訓練。 .fit方法就是訓練模型, 訓練的結果就是求出了上面的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前面的係數, intercept這個代表那個b, 我們上面說過了,有了係數,我們就可以對新的樣本進行預測。 預測也很簡單,只需要

y_pred = model.predict(x_test)

這樣的一句代碼就可以實現, 但是這裏想探索一點別的東西, 因爲後面模型對比中會發現線性模型的效果不好,因爲我們前面特徵選擇的時候也看到過了啊, 並不是所有的數值特徵都和price有相關關係啊, 還有一些非線性關係吧, 這些線性模型就捕捉不到了,並且一般線性模型喜歡那種歸一化或者標準化的數據, 導致和非線性模型的數據還得分開處理,所以這個比賽不會用到線性模型。

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

其次,我們還可以看一下線性迴歸的訓練效果,繪製一下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的分佈圖嗎?

print('It is clear to see the price shows a typical exponential distribution')
plt.figure(figsize=(15,5))
plt.subplot(1,2,1)
sns.distplot(train_y)
plt.subplot(1,2,2)
sns.distplot(train_y[train_y < np.quantile(train_y, 0.9)])

結果是長這個樣子:
在這裏插入圖片描述
通過作圖我們發現數據的標籤(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的散點圖, 會發現長這樣子了:
在這裏插入圖片描述
好了,線性迴歸模型我們就探索到這裏,引入這塊到底要表達個什麼意思呢?

  1. 線性模型是很簡單的一個模型,我們雖然後面不會用到,但是後面建立模型,訓練和預測模型的步驟和線性模型基本上是一致的,依然是.fit(X,Y), .predict(X_test)方法。所以在這裏先體會一下如何建立一個模型,並且對它訓練和預測。
  2. 線性模型這個操作中,有些方法還是可以用於其他模型的,比如模型訓練之後,我們可以通過某種方式去看哪些特徵對模型更加重要,這個在特徵篩選的時候非常非常有用(還記得嵌入式或者包裹式特徵選擇方法嗎),所以這裏算是一個簡單的溫習操作
  3. 通過查看模型的訓練效果,可能還會有意外的收穫,就比如這裏的price這個分佈,從模型的訓練效果也可以看出來這個分佈可能有問題。

上面的三個點你get到了嗎? 是不是這個熱身中也潛藏好多知識 😉, 雖然不用線性模型,但是有些思路和方法可以通用啊!

好了,熱身結束之後,下面我們就看一個比較重要的東西,叫做交叉驗證,這個常用來做算法的評估,評估就是估計算法在預測新數據的時候能達到什麼程度。

2.1 交叉驗證

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

因爲在實際的訓練中,訓練的結果對於訓練集的擬合程度通常還是挺好的(初始條件敏感),但是對於訓練集之外的數據的擬合程度通常就不那麼令人滿意了。因此我們通常並不會把所有的數據集都拿來訓練,而是分出一部分來(這一部分不參加訓練)對訓練集生成的參數進行測試,相對客觀的判斷這些參數對訓練集之外的數據的符合程度。這種思想就稱爲交叉驗證(Cross Validation)。 交叉驗證中,比較常用的就是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

看一下結果吧:
在這裏插入圖片描述
這就是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))

當然,這裏只是提一下,在這個比賽中,涉及的時間關係並不是那麼強,所以這裏完全可以使用k折交叉驗證的方式,並且關於時間序列的話,一般屬於自迴歸的問題,處理起來也不是單純的像上面這麼簡單。

2.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:]

好了,關於模型的基礎知識方面就整理這麼多, 主要是先做了個熱身, 然後瞭解了一下K折交叉驗證的思想,最後就是一個小的技巧吧。

下面就開始進入正題部分, 如何去選擇模型?

3. 評估算法模型的框架

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

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

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

由於這裏面會用到線性的一些模型,所以這裏會給出兩個框架吧算是,一個是基於原始的數據,一個是基於正態的數據(這裏利用了Pipeline自動處理流程), 這兩個框架都可以做遷移,用到別的選擇模型的任務中。

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的效果可能由於參數的原因表現不是那麼理想,這裏也作爲了我們調參備選。 畢竟現在還是xgb和lgb的天下嘛?

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

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, 下面再看一下baseline裏面的那個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, 這個效果是不是差別很大啊? 所以調參還是很重要的。但是在調參之前,還是先給出另一個正態了模板。

正態化數據?

  • 在這裏猜測也許因爲原始數據中心不同特徵屬性的度量單位不一樣, 導致有的算法不是很好。 接下來通過正態化,再次評估這些算法。
  • 在這裏對訓練數據進行轉換處理, 講所有的數據特徵值轉爲0位中位值, 標準差是1的數據
  • 對數據正態化時, 爲了防止數據泄露, 採用“Pipeline”來正態化數據和對模型進行評估
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())

當然,我這裏不用這個,因爲我試驗了一下,效果不如上面的好。

所以下面選定模型調參就選擇隨機森林, LGB, 然後xgb和gradientBoostingRegressor備選。

4. 模型的調參

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

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

模型的調參這裏,有三種方式:

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

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

在這裏插入圖片描述

下面的實驗,我是以LGB作爲實驗的,因爲這裏爲了整理調參方式,其他的模型也都是這個思路,所以爲了減少篇幅,只對LGB做的實驗,還有個原因就是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]

4. 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)

4.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_

上面這個我電腦沒跑出來,因爲我設置的分類器的個數有點多了。

4.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()

當然,這一個也沒跑出結果來,隨機森林的速度處理這種問題和xgb, lgb等還是不能比。 可以嘗試跑跑xgb調調吧。

5. 繪製學習曲線與驗證曲線

從上面的步驟中,我們通過算法模型的評估框架選擇出了合適的幾個模型, 又通過模型的調參步驟確定了模型的合適參數,這樣我們基本上就得到了一個我們認爲的比較好的模型了, 但是這個模型真的就是好的模型了嗎? 我們還不能確定是否存在過擬合或者欠擬合, 關於這兩個的概念這裏不多描述, 從字面裏面就是欠擬合就是模型沒有訓練好, 導致在訓練集和測試集效果都比較差,這個也叫做高偏差。 而過擬合就是模型訓練過了, 在訓練集上的效果非常好,而測試集上的效果很差, 泛化能力弱, 這個也叫做高方差。 可是,說是這麼說,我們在實際中究竟應該怎麼判斷呢? 學習曲線的繪製就是一個非常好的方式, 可以幫助我們看一下我們調試好的模型還有沒有過擬合或者欠擬合的問題, 好幫助我們進行下一步的工作。

關於學習曲線:

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

PS: 表現能力:也就是模型的預測準確率,使用均方誤差表示;學習率上體現了模型相對於訓練集和測試集兩類數據的均方誤差。

如何繪製學習曲線:sklearn.model_selection.learning_curve

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 error, 這個就是越低越好了。

那麼高偏差和高方差應該怎麼看呢?引用一個博客裏面的圖片:
在這裏插入圖片描述
什麼情況下欠擬合:模型在訓練集和驗證集上準確率相差不大,卻都很差,說明模型對已知數據和未知數據都不能準確預測,屬於高偏差。 看左上角那個圖。

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

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

5.2 驗證曲線

驗證曲線和學習曲線很相近,不同的是這裏畫出的是不同參數下模型的準確率而不是不同訓練集大小下的準確率。 這個用的不多, 所以這裏簡單的整理一下,可以做個對比:

這個是可以幫助我們可視化一些參數對於訓練結果的影響效果的, 比如LGB的學習率這個參數, 主要是看看和學習曲線的繪製區別。

from sklearn.model_selection import validation_curve

param_range =  [0.01, 0.05, 0.1, 0.2]
model = LGBMRegressor()
train_scores, test_scores = validation_curve(model, X, Y_ln, param_name='learning_rate', param_range=param_range, cv=10)
train_mean = np.mean(train_scores, axis=1)
train_std = np.std(train_scores, axis=1)
test_mean = np.mean(test_scores, axis=1)
test_std = np.std(test_scores, axis=1)
plt.plot(param_range, train_mean, color='blue', marker='o', markersize=5, label='training accuracy')
plt.fill_between(param_range, train_mean + train_std, train_mean - train_std, alpha=0.15, color='blue')
plt.plot(param_range, test_mean, color='green', linestyle='--', marker='s', markersize=5, label='validation accuracy')
plt.fill_between(param_range, test_mean + test_std, test_mean - test_std, alpha=0.15, color='green')
plt.grid()
plt.xscale('log')
plt.xlabel('Parameter C')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')
plt.ylim([0.8, 1.0])
plt.show()

這個結果如下:
在這裏插入圖片描述
從這裏可以看出, 學習率在0.2的時候效果比較不錯。

那麼如果模型真的出現了過擬合或者欠擬合,我們應該怎麼解決呢?

  • 對於欠擬合,也就是高偏差的問題,這時候可以嘗試增加更多的特徵, 好好做做特徵工程那塊,儘量挖掘更多的信息, 在模型上也可以下功夫,加大模型的複雜度, 減少正則化的係數等。boosting方式可以有效的減少模型的欠擬合。
  • 對於過擬合,也就是高方差的問題,我們可以嘗試增加數據樣本的數量,去掉一些噪聲數據, 或者減少特徵數量(這個其實不推薦),或者引入正則化的方式。 在模型上也可以下功夫,比如樹模型,我們可以減少樹的深度,減少模型的複雜度。 對於隨機森林可以有效的減少模型的過擬合。

低偏差,高方差,該如何權衡? 先保證低偏差,這樣得到模型在測試集上的最優得分,再調整高方差。

好了,今天的內容整理到這裏基本上就結束了。

6. 總結

這一次主要是整理了一些模型選擇和調參的一些技巧性的東西, 簡單的總結一下, 首先是從簡單的線性迴歸模型開始進行分析, 從裏面得到了一些思路和想法。 其次介紹了K折交叉驗證的思想和實現方式並且針對這個比賽,構造了一個線下的測試集。 在模型選擇這塊,通過分析確定了6個模型進行試驗,給出了兩個評估模型的框架,這兩個框架都可以遷移到其他的任務上去。 通過分析交叉驗證的結果確定了兩個模型進入到調參環節。 模型調參部分,介紹了三種調參方式並且針對LGB進行了演示。 調參完畢之後, 又通過繪製學習曲線來觀察模型是否出現了過擬合或者欠擬合的問題,並且給出了相應的解決方式。

總之吧, 模型調參這一塊的複雜程度不亞於特徵工程,是一個費時間的細活, 並且需要藉助於很多經驗,所以這一塊更加需要多試錯, 多思考,多積累經驗,一次兩次的是不會精通調參技術的, 入門也不一定能入門。 我這也是第一次調參, 我感受到了這一塊的費時性和複雜性,待把這個系列整理完後(還剩最後一個模型融合和項目部署), 那時候再好好的玩玩這一塊和特徵工程。 下面依然是以一個思維導圖把知識拎起來:
在這裏插入圖片描述
對了,如果把特徵工程部分和模型調參這部分好好做做的話, 誤差又會降低80多, 可以把誤差降到500左右了, 所以從開始的baseline的700多,到EDA, 數據清洗後的580, 到現在的500左右, 這樣一路走過來有沒有點升級打怪的感覺,哈哈,加油 😉,下一次學習模型的融合技巧,可以在這次的基礎上進一步提升模型效果, 關於本系列的所有代碼,會在整理完最後一篇之後整理一下放入GitHub, 最後一篇的時候在收尾的時候會找機會放上鍊接。

參考:

PS: 本次數據挖掘學習,專題知識將在天池分享,詳情可關注公衆號Datawhale。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章