天天看點

機器學習入門簡介

作者:Daniel205

機器學習是一種科學方法,從更正式和抽象的層面上,可以像Mitchell(1997)那樣定義:

如果一個計算機程式在T類任務中的性能(用P衡量)随着經驗E的增加而提高,則該程式可以從經驗E中學習關于T類任務和回測性能P的知識。

下面從資料準備,模型評估和驗證等幾個方面,粗略地介紹機器學習的藍圖。

1. 資料準備

樣本資料是基于歐元/美元匯率的真實金融時序建立的。首先,從遠端導入資料,然後将資料重新采樣為月度資料, 并存儲在Series對象中。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
np.random.seed(100)

plt.rcParams['savefig.dpi'] = 300
plt.rcParams['font.family'] = 'serif'
url = 'http://hilpisch.com/aiif_eikon_eod_data.csv'
raw = pd.read_csv(url, index_col=0, parse_dates=True)['EUR=']
# raw.head()
# 以月為周期對資料重新采樣
l = raw.resample('1M').last()
l.plot(figsize=(10, 6), title='EUR/USD monthly');           
機器學習入門簡介

圖1 歐元/美元匯率的時間序列(以月為機關)

為了使樣本隻有單一特征,下面建立一個合成特征向量,在圖表中進行簡單的可視化。當然,這一合成特征(自變量)對歐元 / 美元匯率(标簽資料,因變量)沒有任何解釋力。在接下來的内容中,它會把标簽資料是連續且瞬時的本質屬性抽象出來。

# 将标簽資料轉換為ndarray對象。
l = l.values
# 從資料元素中減去平均值
l -= l.mean()
# 建立一個合成特征作為ndarray對象
f = np.linspace(-2, 2, len(l))

plt.figure(figsize=(10, 6))
plt.plot(f, l, 'ro')
plt.title('Sample Data Set')
plt.xlabel('features')
plt.ylabel('labels');           
機器學習入門簡介

圖2 樣本資料集

2. 準确性(誤差)統計

MSE是預測問題常用的誤差度量。基于MSE,以标簽資料作為相關基準,并以算法在資料集(或部分資料集)上的預測值來判斷其是否成功。我們分别考慮兩種算法: OLS回歸和神經網絡。 首先是OLS回歸,這個應用非常簡單,如下面的 Python 代碼所示。如圖3所示,回歸結果是包含五階單項式的回歸。

def MSE(l, p):
    return np.mean((l - p) ** 2)

# OLS 回歸模型的拟合,包括五階單項式
reg = np.polyfit(f, l, deg=5)

# 通過 OLS 回歸模型的預測給出了最優參數
p = np.polyval(reg, f)

# 給定預測值的MSE值
mse = MSE(l, p)
print(f'MSE = {mse:.5f}')
# Out: 
# MSE = 0.00342

plt.figure(figsize=(10, 6))
plt.plot(f, l, 'ro', label='sample data')
plt.plot(f, p, '--', label='regression')
plt.legend();           
機器學習入門簡介

圖3 樣本資料與五階單項式回歸線

OLS回歸一般采用解析解,是以沒有疊代學習發生,然而可以通過逐漸将更多資料賦給算法來模拟學習過程。下面的代碼實作了OLS回歸和預測,開始時隻有幾個樣本, 然後逐漸增加樣本的數量,最終達到資料集的完整長度。回歸步驟是基于較小的子集實作的,而預測步驟是基于每一種情況下的全部特征資料實作的。一般來說,随着訓練資料集的增加,MSE會明顯下降。

for i in range(10, len(f) + 1, 20):
    # 基于資料子集的回歸步驟。
    reg = np.polyfit(f[:i], l[:i], deg=3)
    # 基于完整資料集的預測步驟
    p = np.polyval(reg, f)
    # 産生的MSE值
    mse = MSE(l, p)
    print(f'{i:3d} | MSE={mse}')
# Out:
#  10 | MSE=248628.10681594367
#  30 | MSE=731.9382249304159
#  50 | MSE=12.236088505004368
#  70 | MSE=0.741059061974328
#  90 | MSE=0.0057430617304093335
# 110 | MSE=0.006492800939555583           

神經網絡在樣本資料上的應用同樣非常簡單,圖4顯示了神經網絡是如何逼近樣本資料的。

import pandas as pd
import tensorflow as tf
tf.random.set_seed(100)
from keras.layers import Dense
from keras.models import Sequential

model = Sequential()

# 此神經網絡是一個隻有單個隐藏層的淺層網絡
model.add(Dense(256, activation='relu', input_dim=1))
model.add(Dense(1, activation='linear'))
model.compile(loss='mse', optimizer='rmsprop')

# 訓練模型
h = model.fit(f, l, epochs=1500, verbose=False)

# Keras每個學習步驟的MSE值
res = pd.DataFrame(h.history)
res.iloc[100:].plot(figsize=(10, 6))
plt.ylabel('MSE')
plt.xlabel('epochs');           
機器學習入門簡介

圖4 MSE值與訓練輪數的關系

下面是模型的預測效果

# 在預測步驟中展平ndarray對象
p = model.predict(f).flatten()

# DNN 預測結果的MSE值
mse = MSE(l, p)
print(f'MSE = {mse:.5f}')
# Out: 
# MSE = 0.0011754115847228454

plt.figure(figsize=(10, 6))
plt.plot(f, l, 'ro', label='sample data')
plt.plot(f, p, '--', label='DNN approximation')
plt.legend();           
機器學習入門簡介

圖5 樣本資料和DNN近似

3. 模型的容量

通俗地講,模型的容量是指它拟合各種函數的能力,它基本上決定了模型或算法可以學習什麼類型的函數或關系。對于僅基于單項式的OLS回歸,最高階單項式的次數定義了模型的容量。如将該階數參數設定為deg=3,則 OLS 回歸模型可以學習常數型、線性型、二次型或三次型的函數關系。參數deg越大,OLS回歸模型的容量越大。下面從deg=1開始,以2為增量增大階數,MSE值随着階數參數的增大而單調遞減。圖6顯示了所有考慮階數的回歸線。

reg = {}
for d in range(1, 12, 2):
    reg[d] = np.polyfit(f, l, deg=d)
    p = np.polyval(reg[d], f)
    mse = MSE(l, p)
    print(f'{d:2d} | MSE={mse}')
              
plt.figure(figsize=(10, 6))
plt.plot(f, l, 'ro', label='sample data')
for d in reg:
    p = np.polyval(reg[d], f)
    plt.plot(f, p, '--', label=f'deg={d}')
plt.legend();           
機器學習入門簡介

圖6 不同最高階數的回歸線

神經網絡的容量取決于許多超參數,一般而言包括:

  • 隐藏層的數量
  • 每個隐藏層的隐藏單元數量。

這兩個超參數共同定義了神經網絡中可訓練參數(權重)的數量。前面的神經網絡模型的可訓練參數相對較少,隻要再增加一個相同大小的層,就可以顯著增加可訓練參數的數量。盡管可能需要增加訓練輪數,但是容量較大的神經網絡的MSE值會顯著降低,并且拟合在視覺上也會更好,如圖7所示。

def create_dnn_model(hl=1, hu=256):
    ''' 建立Keras DNN模型的函數
    參數
    ==========
    hl: int
        隐藏層數量
    hu: int
        每個隐藏層的單元數量
    '''
    model = Sequential()
    for _ in range(hl):
        model.add(Dense(hu, activation='relu', input_dim=1))
    model.add(Dense(1, activation='linear')) 
    model.compile(loss='mse', optimizer='rmsprop')
    return model

# 具有3個隐藏層,每層256個單元的DNN
model = create_dnn_model(3)

model.fit(f, l, epochs=2500, verbose=False)
p = model.predict(f).flatten()
mse = MSE(l, p)
print(f'MSE = {mse:.5f}')
# Out:
# MSE = 0.00028

plt.figure(figsize=(10, 6))
plt.plot(f, l, 'r', label='sample data')
plt.plot(f, p, '--', label='DNN approximation')
plt.legend();           
機器學習入門簡介

圖7 樣本資料和DNN近似(更大容量)

4. 模型評估

一般來說,當在同一資料集上對模型或算法進行訓練和評估時,模型或算法的容量會直接影響其性能。然而,更複雜的是,應使用經過訓練的模型或算法對其從未見過的資料進行泛化。例如,根據股票價格的曆史預測(估計)未來股票價格,或者根據現有債務人的資料将潛在債務人分為“有信用”或“無信用”。

通常,給定的資料集會被分為多個子集,每個子集有不同的用途

  • 訓練資料集:這是用于算法訓練的子集。
  • 驗證資料集:這是用于在訓練期間驗證算法性能的子集,該資料集與訓練資料集不同。
  • 測試資料集:這是訓練完成後僅在其上測試訓練算法的子集。

通過對驗證資料集應用一個(目前)經過訓練的算法獲得的結果,可能會反映在訓練本身上(比如,對模型超參數進行調整會影響訓練結果)。另外,在測試資料集上測試訓練算法的結果不應反映在訓練本身上或超參數中。

下面的代碼任意選擇25%的樣本資料進行測試,在訓練(學習)完成之前,模型或算法不會“看到”這些資料。同樣,另外25%的樣本資料被保留以用于驗證,該資料用于在訓練步驟和許多學習疊代期間監控性能。剩餘的50%用于訓練(學習)本身。給定樣本資料集,打亂順序随機填充所有樣本資料子集是有意義的。

# 測試資料集樣本的數量
te = int(0.25 * len(f))
# 驗證資料集樣本的數量
va = int(0.25 * len(f))

np.random.seed(100)
# 完整資料集的随機索引
ind = np.arange(len(f))
np.random.shuffle(ind)

# 為資料子集生成的排序索引
ind_te = np.sort(ind[:te])
ind_va = np.sort(ind[te:te + va])
ind_tr = np.sort(ind[te + va:])

# 生成資料子集的特征
f_te = f[ind_te]
f_va = f[ind_va]
f_tr = f[ind_tr]

# 生成資料子集的标簽
l_te = l[ind_te]
l_va = l[ind_va]
l_tr = l[ind_tr]           

基于訓練資料子集和驗證資料子集,下面的代碼實作了不同deg參數值的回歸,并計算了兩個資料子集上的預測的MSE值。雖然訓練資料集上的MSE值單調減小,但驗證資料集上的MSE值往往在某一參數值達到最小值後又再次增大。這個現象就是所謂的過拟合。圖8顯示了不同deg值的回歸拟合情況,并比較了訓練資料集和驗證資料集的拟合情況。

reg = {}
mse = {}
for d in range(1, 22, 4):
    reg[d] = np.polyfit(f_tr, l_tr, deg=d)
    p = np.polyval(reg[d], f_tr)
    # 訓練資料集的MSE值
    mse_tr = MSE(l_tr, p)
    p = np.polyval(reg[d], f_va)
    # 驗證資料集的MSE值
    mse_va = MSE(l_va, p)
    mse[d] = (mse_tr, mse_va)
    print(f'{d:2d} | MSE_tr={mse_tr:7.5f} | MSE_va={mse_va:7.5f}')
# Out:
#  1 | MSE_tr=0.00574 | MSE_va=0.00492
#  5 | MSE_tr=0.00375 | MSE_va=0.00273
#  9 | MSE_tr=0.00132 | MSE_va=0.00243
# 13 | MSE_tr=0.00094 | MSE_va=0.00183
# 17 | MSE_tr=0.00060 | MSE_va=0.00153
# 21 | MSE_tr=0.00046 | MSE_va=0.00837

fig, ax = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
ax[0].plot(f_tr, l_tr, 'ro', label='training data')
ax[1].plot(f_va, l_va, 'go', label='validation data')

for d in reg:
    p = np.polyval(reg[d], f_tr)
    ax[0].plot(f_tr, p, '--', label=f'deg={d} (tr)')
    p = np.polyval(reg[d], f_va)
    plt.plot(f_va, p, '--', label=f'deg={d} (va)')
ax[0].legend()
ax[1].legend();           
機器學習入門簡介

圖8 包括回歸拟合的訓練資料集和驗證資料集

通過Keras和神經網絡模型,可以針對每個學習步驟監控驗證資料集的性能。當沒有觀察到進一步的改進時(比如,在訓練資料集上的性能方面),也可以使用回調函數提前停止模型訓練。下面的代碼使用了這樣的回調函數。圖9顯示了神經網絡對訓練資料 集和驗證資料集的預測。

from keras.callbacks import EarlyStopping

model = create_dnn_model(2, 256)


callbacks = [EarlyStopping(monitor='loss',            # 根據訓練資料集的MSE值停止學習
                           patience=100,              # 在一定的訓練輪數後沒有改善就停止學習
                           restore_best_weights=True) # 當學習停止時儲存最好的權重
            ]


h = model.fit(f_tr, l_tr, epochs=3000, verbose=False,
          validation_data=(f_va, l_va),               # 指定驗證資料子集
          callbacks=callbacks)                        # 将回調函數傳給fit()方法

fig, ax = plt.subplots(2, 1, sharex=True, figsize=(10, 8))
ax[0].plot(f_tr, l_tr, 'ro', label='training data')
p = model.predict(f_tr)
ax[0].plot(f_tr, p, '--', label=f'DNN (tr)')
ax[0].legend()
ax[1].plot(f_va, l_va, 'go', label='validation data')
p = model.predict(f_va)
ax[1].plot(f_va, p, '--', label=f'DNN (va)')
ax[1].legend();           
機器學習入門簡介

圖9 包括 DNN 預測的訓練資料集和驗證資料集

Keras允許對模型每一輪訓練的兩個資料集上MSE值的變化進行分析。從圖10中可以看出,MSE值随着訓練輪數的增加而減小,但隻是平均減小,而不是單調減小。

機器學習入門簡介

圖10 DNN模型在訓練資料集和驗證資料集上的MSE值

在OLS回歸的情況下,可能會為階數參數選擇一個高但不太高的值,比如deg=9。神經網絡模型的參數化會在訓練結束時自動給出最佳模型配置。圖11比較了兩個模型之間的預測以及與測試資料集的預測。鑒于樣本資料的性質,神經網絡的測試資料集性能更好一點兒并不令人驚訝。

p_ols = np.polyval(reg[5], f_te)
p_dnn = model.predict(f_te).flatten()
mse_ols = MSE(l_te, p_ols)
print(f'MSE of OLS: {mse_ols}')
# Out:
# MSE of OLS: 0.0038960346771028365
mse_dnn = MSE(l_te, p_dnn)
print(f'MSE of DNN: {mse_dnn}')
# Out:
# MSE of DNN: 0.0006873497338876613
plt.figure(figsize=(10, 6))
plt.plot(f_te, l_te, 'ro', label='test data')
plt.plot(f_te, p_ols, '--', label='OLS prediction')
plt.plot(f_te, p_dnn, '-.', label='DNN prediction');
plt.legend();           
機器學習入門簡介

圖11 OLS 回歸和DNN模型的測試資料集和預測

5. 偏差和方差

一般來說,機器學習中的一個主要問題是過拟合,特别是當機器學習算法應用于金融資料時。當驗證資料集和測試資料集的性能比訓練資料集的性能差時,就是模型過度拟合了其訓練資料,使用OLS回歸的示例可以在視覺上和數字上說明這個問題。 下面的代碼使用較小的子集進行訓練和驗證,并實作了線性回歸和高階回歸。如圖12所示,線性回歸拟合在訓練資料集上有較高的偏差,預測資料和标簽資料之間的絕對差異相對較大。高階拟合顯示出了較高的方差,它精确地命中所有訓練資料點,但為實作完美拟合,拟合本身變化會很大。

# 較少特征的資料子集
f_tr = f[:20:2]
l_tr = l[:20:2]

# 較少标簽的資料子集
f_va = f[1:20:2]
l_va = l[1:20:2]

plt.figure(figsize=(10, 6))

# 高偏差 OLS 回歸(線性)
reg_b = np.polyfit(f_tr, l_tr, deg=1)

# 高方差 OLS 回歸(高階)
reg_v = np.polyfit(f_tr, l_tr, deg=9, full=True)[0]

# 用于繪圖的放大的特征資料集
f_ = np.linspace(f_tr.min(), f_va.max(), 75)

plt.plot(f_tr, l_tr, 'ro', label='training data')
plt.plot(f_va, l_va, 'go', label='validation data')
plt.plot(f_, np.polyval(reg_b, f_), '--', label='high bias')
plt.plot(f_, np.polyval(reg_v, f_), '--', label='high variance')
plt.ylim(-0.2)
plt.legend(loc=2);           
機器學習入門簡介

圖12 高偏差和高方差的OLS回歸拟合

如圖12顯示,在本例中,高偏差拟合比高方差拟合在訓練資料集上的表現更差。但高方差拟合在很大程度上是過拟合,在驗證資料集上表現得更差。這可以通過比較所有情況下的回測性能來說明。以下的代碼不僅計算MSE 值,還計算 值。

from sklearn.metrics import r2_score


def evaluate(reg, f, l):
    p = np.polyval(reg, f)
    # 平均差異絕對值作為模型偏差
    bias = np.abs(l - p).mean()

    # 模型預測的方差作為模型方差
    var = p.var()
    msg = f'MSE={MSE(l, p):.4f} | R2={r2_score(l, p):9.4f} | '
    msg += f'bias={bias:.4f} | var={var:.4f}'
    print(msg)

# 高偏差模型在訓練資料集上的性能
evaluate(reg_b, f_tr, l_tr)
# Out:
# MSE=0.0026 | R2=   0.3484 | bias=0.0423 | var=0.0014

# 高偏差模型在驗證資料集上的性能
evaluate(reg_b, f_va, l_va)
# Out:
# MSE=0.0032 | R2=   0.4498 | bias=0.0460 | var=0.0014

# 高方差模型在訓練資料集上的性能
evaluate(reg_v, f_tr, l_tr)
# Out:
# MSE=0.0000 | R2=   1.0000 | bias=0.0000 | var=0.0040

# 高方差模型在驗證資料集上的性能
evaluate(reg_v, f_va, l_va)
# Out:
# MSE=0.8753 | R2=-149.2835 | bias=0.3565 | var=0.7540           

結果表明,在訓練資料集和驗證資料集上,高偏差模型的性能大緻相當。相比之下,高方差模型在訓練資料集上的性能很好,在驗證資料集上的性能則相當差。

6. 交叉驗證

避免過拟合的标準方法是交叉驗證,在此過程中對多個訓練資料集和驗證資料集進行測試。scikit-learn包提供了以标準化方式實作交叉驗證的功能,函數cross_val_score可以用于任何scikit-learn模型對象。 下面的代碼使用scikit-learn的多項式 OLS 回歸模型在完整的樣本資料集上實作 OLS回歸方法,對最高多項式進行不同階數的五折交叉驗證。平均而言,回歸中最高階數越高,交叉驗證得分就越差。當使用樣本資料集中前20% 的資料(圖3中左側的資料)或最後20%的資料(圖3中右側的資料)進行驗證時,觀察到的結果尤為糟糕。但是,在 樣本資料集中間20%的資料處,能觀察到最佳驗證分數。

from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import make_pipeline

# 建立多項式回歸模型類。
def PolynomialRegression(degree=None, **kwargs):
    return make_pipeline(PolynomialFeatures(degree),
                         LinearRegression(**kwargs))

np.set_printoptions(suppress=True,
                    formatter={'float': lambda x: f'{x:12.2f}'}) # 調整 numpy 的預設列印設定

print('\nCross-validation scores')
print(74 * '=')
for deg in range(0, 10, 1):
    model = PolynomialRegression(deg)
    # 實施五折交叉驗證
    cvs = cross_val_score(model, f.reshape(-1, 1), l, cv=5)
    print(f'deg={deg} | ' + str(cvs.round(2)))
# Out:
# Cross-validation scores
# ==========================================================================
# deg=0 | [       -6.07        -7.34        -0.09        -6.32        -8.69]
# deg=1 | [       -0.28        -1.40         0.16        -1.66        -4.62]
# deg=2 | [       -3.48        -2.45         0.19        -1.57       -12.94]
# deg=3 | [       -0.00        -1.24         0.32        -0.48       -43.62]
# deg=4 | [     -222.81        -2.88         0.37        -0.32      -496.61]
# deg=5 | [     -143.67        -5.85         0.49         0.12     -1241.04]
# deg=6 | [    -4038.96       -14.71         0.49        -0.33      -317.32]
# deg=7 | [    -9937.83       -13.98         0.64         0.22    -18725.61]
# deg=8 | [    -3514.36       -11.22        -0.15        -6.29   -298744.18]
# deg=9 | [    -7454.15        -0.91         0.15        -0.41    -13580.75]           

Keras提供了包裝類來使用帶有scikit-learn接口的Keras模型對象,比如cross_val_ score函數。下面的例子使用KerasRegressor類來包裝神經網絡模型,并對其應用交叉驗證。與OLS回歸交叉驗證分數相比,測試的兩個網絡的交叉驗證分數始終更高。在這個例子中,神經網絡容量并沒有發揮太大作用。

np.random.seed(100)
tf.random.set_seed(100)
from keras.wrappers.scikit_learn import KerasRegressor
# from scikeras.wrappers import KerasRegressor

# 低容量神經網絡的包裝類
model = KerasRegressor(build_fn=create_dnn_model,
                       verbose=False, epochs=1000,
                       hl=1, hu=36)
# 低容量神經網絡的交叉驗證
cross_val_score(model, f, l, cv=5)
# Out:
# array([       -0.02,        -0.01,        -0.00,        -0.00,
#               -0.08])

# 高容量神經網絡的包裝類
model = KerasRegressor(build_fn=create_dnn_model,
                       verbose=False, epochs=1000,
                       hl=3, hu=256)
# 高容量神經網絡的交叉驗證
cross_val_score(model, f, l, cv=5)
# array([       -0.05,        -0.01,        -0.00,        -0.00,
#               -0.04])

            

一般而言,過拟合,即模型在訓練資料集上比在驗證資料集和測試資料集上表現得更好,在機器學習中是要避免的,尤其是在金融中。适當的評估程式和分析(比如交叉驗證)有助于防止過拟合并能找到足夠的模型容量。

繼續閱讀