1. 程式人生 > >機器學習——模型評估與模型選擇

機器學習——模型評估與模型選擇

評價一個機器學習模型的好壞需要特定的評估方法,並據此對模型進行選擇,從而得到一個更好的模型。本文主要是關於模型評估與模型選擇的筆記,以及利用 scikit-learn 對 Logistic 迴歸進行的結果進行交叉檢驗。

1. 訓練誤差,測試誤差與泛化誤差

學習器(模型)在訓練集上表現出來的誤差稱為 訓練誤差(training error) 或 經驗誤差(empirical error) ,這種誤差可以通過損失函式進行描述:

$$E_{training}(f) = \frac{1}{N}\sum_{i=1}^NL(y_i, f(x_i)$$

其中 $L(\cdot)$ 為損失函式,$f$ 為模型,$N$ 為訓練樣本容量。很多機器學習演算法的訓練過程就是試圖最小化這一訓練誤差。但是最小化訓練誤差並不一定就是一個好的模型,它有可能只是將訓練樣本中所有的特徵都非常好地挖掘出來進行學習,但這些訓練樣本的某些特徵有可能是具有特異性的,並不能推廣到所有樣本中,這就會導致模型的**過擬合(overfitting)**。模型在新資料集合上表現出來的誤差,稱為**泛化誤差(generalization error)**。通常會通過實驗測試來對模型的泛化誤差進行評估,這時需要引入一些新的測試資料對模型進行檢驗,在測試資料上表現出來的誤差稱為**測試誤差(testing error)**。測試誤差為:

$$e_{test} = \frac{1}{N'}\sum_{i=1}^N' I(y_i \neq f(x_i))$$

其中 $I(\cdot)$ 為指標函式(indicator function),當$\cdot$為真是返回$1$ ,否則返回 $0$;$N'$ 為測試樣本容量,測試準確率(或稱為精度(accuracy)):

$$acc_{test} = 1 - e_{test}$$

2. 過擬合與欠擬合

訓練誤差很小而泛化誤差很大時稱為過擬合,與之相對的是**欠擬合(underfitting)**。例如多項式擬合:

$$f_M(x, \omega) = \omega_0 + \omega_1x + \omega_2x^2+\dots+\omega_Mx^M = \sum_{j=0}^M\omega_jx^j$$

當選取 $M$ 個引數進行訓練時,可能出現下列情況:

當 $M = 0$ 和 $M = 1$ 時,模型為直線,擬合效果很差,即欠擬合;當 $M = 9$ 時,模型曲線經過了每一個訓練資料點,訓練誤差為 0,但是無法預測新的資料,因此泛化誤差很大,即過擬合。

3. 測試誤差的評估方法

  1. 留出法(hold-out)
  2. 交叉驗證法(cross validation)
  3. 自助法(bootstrapping)
  4. 調參(parameter tuning)

3.1 留出法

將資料集 $D$ 劃分為 $S, T$:

$$D = S \cap T, S \cup T = \emptyset$$

並採用**分層取樣(stratified sampling)**,通常選用 $2/3 - 4/5$ 用於訓練。

3.2 交叉驗證法

將 $D$ 劃分為 $k$ 個大小相似的互斥子集:

$$D = D_1 \cup D_2 \cup \dots \cup D_k, D_i \cap D_j = \emptyset (i \neq j)$$

每次用 $k-1$ 個子集作為訓練集,剩下一個作為測試集,稱為**k折交叉驗證(k-fold cross validation)**。$k$ 通常取 10,並隨機使用不同劃分重複 $p$ 次,最終取 $p$ 次結果均值,例如“10次10折交叉驗證”。

假設資料集 $D$ 容量為 $m$,若 $k = m$,則稱為**留一法(Leave-One-Out, LOO)**。留一法蘋果結果比較準確,但計算開銷也相應較大。

3.3 自助法

以**自助取樣法(bootstrap sampling)**為基礎,從 $D$ 中有放回地隨機抽取 $m$ 次,得到同樣包含 $m$ 個樣本的 $D'$,$D$ 中有一部分樣本會在 $D'$ 中出現多次,而另一部分則未出現,$m$ 次重取樣始終未被採到的概率是:

$$\lim_{m\rightarrow\infty}(1-\frac{1}{m})^m \rightarrow \frac{1}{e} \approx 0.368$$

即 $36.8\%$ 的樣本未出現在 $D'$。以 $D'$ 作為訓練集,$D - D'$ 作為測試集。自助法在資料集較小、難以劃分訓練/測試集時很有用。

4. 效能度量

除了精度($acc_{test}$)和錯誤率($e_{test}$),還需要反映任務需求的效能度量指標。

  1. 查準率、查全率與 $F_1$
  2. ROC & AUC
  3. 代價矩陣

4.1 查準率、查全率與 $F_1$

$$ TP + FP + TN + FN = m^++m^- = m$$

查準率(準確率,precision): $$P = \frac{TP}{TP + FP}$$

查全率(召回率,recall): $$R = \frac{TP}{TP + FN}$$

希望查全率高,意味著更看重決策的準確性,例如在商品推薦系統,儘量減少錯誤推薦;希望查全率高,意味著“寧可錯殺一千”,例如在罪犯檢測過程中。

$$F_1 = \frac{2PR}{P+R}$$ $$F_\beta = \frac{(1+\beta^2)PR}{(\beta^2+P)+R}$$

當 $\beta = 1$ 時,$F_\beta = F_1$;$\beta \gt 1$ 時,查全率影響更大;$\beta \lt 1$ 時,查準率影響更大。

4.2 訊號檢測論

$$TPR = \frac{TP}{TP + FN}$$ $$FPR = \frac{FP}{FP + TN}$$

在實驗心理學訊號檢測論中,TPR 是**擊中(Hit)**的概率,FPR 是 **虛驚(False alarm)**的概率。ROC(Receiver Operating Characteristic Curve)稱為接受者操作特性曲線(又稱感受性曲線)。曲線上各點反應相同的感受性,只是在不同的判定標準下所得的結果。以虛驚概率(FPR)為橫軸,擊中概率(TPR)為縱軸組成的座標圖和被試(學習模型)在相同刺激條件下采用不同判斷標準得出不同結果畫出的曲線。

曲線下區域的面積(Area Under ROC Curve, AUC)代表不同被試(模型)對刺激的辨別能力,AUC 越大,意味著辨別能力越強。

$$AUC = \frac{1}{2}\sum_{i=1}^{m-1}(x_{i+1}-x_i)(y_i+y_{i+1})$$

4.3 代價矩陣

暫略。

5. 統計檢驗

暫略。

6. 練習

《機器學習·周志華》習題 3.4:

選擇兩個 UCI 資料集,比較 10折交叉驗證法和留一法所估計出的對數迴歸錯誤率。

根據人口統計資料預測年收入是否超過 $50K(共14個屬性,有不完整資料,暫時將不完整資料清除)。

head -n 5 adult.data

# 39, State-gov, 77516, Bachelors, 13, Never-married, Adm-clerical, Not-in-family, White, Male, 2174, 0, 40, United-States, <=50K
# 50, Self-emp-not-inc, 83311, Bachelors, 13, Married-civ-spouse, Exec-managerial, Husband, White, Male, 0, 0, 13, United-States, <=50K
# 38, Private, 215646, HS-grad, 9, Divorced, Handlers-cleaners, Not-in-family, White, Male, 0, 0, 40, United-States, <=50K
# 53, Private, 234721, 11th, 7, Married-civ-spouse, Handlers-cleaners, Husband, Black, Male, 0, 0, 40, United-States, <=50K
# 28, Private, 338409, Bachelors, 13, Married-civ-spouse, Prof-specialty, Wife, Black, Female, 0, 0, 40, Cuba, <=50K

6.1 步驟

  1. 利用 pandas 讀取資料,將遺失資料(資料中以 ? 佔位)剔除;
  2. 將標字串格式的標籤轉化為數字格式的類別(category);
  3. 分別用 KFold 和 LOO 的方法對對數迴歸模型進行驗證。
  4. 完整程式碼及計算過程見: Exe3.4
# 處理資料元資訊
meta = """  
age: continuous.  
workclass: Private, Self-emp-not-inc, Self-emp-inc, Federal-gov, Local-gov, State-gov, Without-pay, Never-worked.  
fnlwgt: continuous.  
education: Bachelors, Some-college, 11th, HS-grad, Prof-school, Assoc-acdm, Assoc-voc, 9th, 7th-8th, 12th, Masters, 1st-4th, 10th, Doctorate, 5th-6th, Preschool.  
education-num: continuous.  
marital-status: Married-civ-spouse, Divorced, Never-married, Separated, Widowed, Married-spouse-absent, Married-AF-spouse.  
occupation: Tech-support, Craft-repair, Other-service, Sales, Exec-managerial, Prof-specialty, Handlers-cleaners, Machine-op-inspct, Adm-clerical, Farming-fishing, Transport-moving, Priv-house-serv, Protective-serv, Armed-Forces.  
relationship: Wife, Own-child, Husband, Not-in-family, Other-relative, Unmarried.  
race: White, Asian-Pac-Islander, Amer-Indian-Eskimo, Other, Black.  
sex: Female, Male.  
capital-gain: continuous.  
capital-loss: continuous.  
hours-per-week: continuous.  
native-country: United-States, Cambodia, England, Puerto-Rico, Canada, Germany, Outlying-US(Guam-USVI-etc), India, Japan, Greece, South, China, Cuba, Iran, Honduras, Philippines, Italy, Poland, Jamaica, Vietnam, Mexico, Portugal, Ireland, France, Dominican-Republic, Laos, Ecuador, Taiwan, Haiti, Columbia, Hungary, Guatemala, Nicaragua, Scotland, Thailand, Yugoslavia, El-Salvador, Trinadad&Tobago, Peru, Hong, Holand-Netherlands.  
salary: <=50K, >50K  
"""
#?? 有沒有更標準的方法可以把標籤資料轉化為數字?
names = []  
catMap = {}  
for line in meta.split("\n"):  
    line = line.strip()
    if len(line) == 0:
        continue
    name, cate = tuple(line.split(":"))
    names.append(name)
    if 'continuous' in cate: # 數字型別的跳過
        continue
    catMap[name] = {v.strip(): i for i, v in enumerate(cate.rstrip(".").split(","))}

import numpy as np  
import pandas as pd

# pandas 讀取 csv 格式到 DataFrame
datas = pd.read_csv('adult.data', header=None, names=names)  
print(datas.shape)    # (32561, 15)

# 遺漏資料在檔案中是 ?,替換成 NaN
datas = datas.replace(r'\?', np.nan, regex=True)

# 剔除任何含有 NaN 的資料
datas = datas.dropna(how="any")  
print(datas.shape)    # (30162, 15)

# 將 Object 型別轉化成 category 並賦予整數值
for c in datas.columns:  
    if datas[c].dtype == 'object':
        datas[c] = datas[c].apply(lambda x: catMap[c][x.strip()])
datas = datas.dropna(how="any")

# 分別將前 14 個屬性和目標屬性 salary 讀入
X, y = datas[datas.columns[:-1]], datas.salary  
X_arr = np.array(X)  
y_arr = np.array(y)

# 10折交叉檢驗
from sklearn.cross_validation import KFold  
kf = KFold(n=datas.shape[0], n_folds=10)  
for train_index, test_index in kf:  
    X_train, X_test = X_arr[train_index], X_arr[test_index]
    y_train, y_test = y_arr[train_index], y_arr[test_index]
    print("---- Shapes: ", X_train.shape, X_test.shape)
# ---- Shapes:  (27146, 14) (3016, 14)  X 10次

# 先取最後一折的資料進行測試:
from sklearn.linear_model import LogisticRegression  
from sklearn.preprocessing import StandardScaler

lr = LogisticRegression(penalty='l1', tol=0.01)  
lr.fit(X_train, y_train)  
lr.score(X_test, y_test)    # 0.83

# 或者直接用 cross_val_score
from sklearn.cross_validation import cross_val_score  
scores = cross_val_score(lr, X_arr, y_arr, cv=10)  
print("Mean: ", np.mean(scores)) # 0.81

# 留一法
from sklearn.cross_validation import LeaveOneOut  
## LOO 實際上等於 KFold(n, n_folds=n)
# scores = cross_val_score(lr, X_arr, y_arr, cv=X_arr.shape[0])
# -- 執行時間非常之長,在 Jupyter 裡面執行應該已經超時了,最後沒有返回結果
scores = cross_val_score(lr, X_arr, y_arr, cv=1000) # 意思一下…  
print("Mean: ", np.mean(scores))  # 0.82