1. 程式人生 > >機器學習(ML)七之模型選擇、欠擬合和過擬合

機器學習(ML)七之模型選擇、欠擬合和過擬合

訓練誤差和泛化誤差

需要區分訓練誤差(training error)和泛化誤差(generalization error)。前者指模型在訓練資料集上表現出的誤差,後者指模型在任意一個測試資料樣本上表現出的誤差的期望,並常常通過測試資料集上的誤差來近似。計算訓練誤差和泛化誤差可以使用之前介紹過的損失函式,例如線性迴歸用到的平方損失函式和softmax迴歸用到的交叉熵損失函式。

直觀地解釋訓練誤差和泛化誤差這兩個概念。訓練誤差可以認為是做往年高考試題(訓練題)時的錯誤率,泛化誤差則可以通過真正參加高考(測試題)時的答題錯誤率來近似。假設訓練題和測試題都隨機取樣於一個未知的依照相同考綱的巨大試題庫。如果讓一名未學習中學知識的小學生去答題,那麼測試題和訓練題的答題錯誤率可能很相近。但如果換成一名反覆練習訓練題的高三備考生答題,即使在訓練題上做到了錯誤率為0,也不代表真實的高考成績會如此。

機器學習裡,我們通常假設訓練資料集(訓練題)和測試資料集(測試題)裡的每一個樣本都是從同一個概率分佈中相互獨立地生成的。基於該獨立同分布假設,給定任意一個機器學習模型(含引數),它的訓練誤差的期望和泛化誤差都是一樣的。例如,如果我們將模型引數設成隨機值(小學生),那麼訓練誤差和泛化誤差會非常相近。

模型的引數是通過在訓練資料集上訓練模型而學習出的,引數的選擇依據了最小化訓練誤差(高三備考生)。所以,訓練誤差的期望小於或等於泛化誤差。也就是說,一般情況下,由訓練資料集學到的模型引數會使模型在訓練資料集上的表現優於或等於在測試資料集上的表現。由於無法從訓練誤差估計泛化誤差,一味地降低訓練誤差並不意味著泛化誤差一定會降低。

機器學習模型應關注降低泛化誤差。

模型選擇

在機器學習中,通常需要評估若干候選模型的表現並從中選擇模型。這一過程稱為模型選擇(model selection)。可供選擇的候選模型可以是有著不同超引數的同類模型。以多層感知機為例,我們可以選擇隱藏層的個數,以及每個隱藏層中隱藏單元個數和啟用函式。為了得到有效的模型,我們通常要在模型選擇上下一番功夫。下面,我們來描述模型選擇中經常使用的驗證資料集(validation data set)。

驗證資料集

測試集只能在所有超引數和模型引數選定後使用一次。不可以使用測試資料選擇模型,如調參。由於無法從訓練誤差估計泛化誤差,因此也不應只依賴訓練資料選擇模型。鑑於此,我們可以預留一部分在訓練資料集和測試資料集以外的資料來進行模型選擇。這部分資料被稱為驗證資料集,簡稱驗證集(validation set)。例如,我們可以從給定的訓練集中隨機選取一小部分作為驗證集,而將剩餘部分作為真正的訓練集。

然而在實際應用中,由於資料不容易獲取,測試資料極少只使用一次就丟棄。因此,實踐中驗證資料集和測試資料集的界限可能比較模糊。從嚴格意義上講,除非明確說明,否則中實驗所使用的測試集應為驗證集,實驗報告的測試結果(如測試準確率)應為驗證結果(如驗證準確率)。

K 折交叉驗證

由於驗證資料集不參與模型訓練,當訓練資料不夠用時,預留大量的驗證資料顯得太奢侈。一種改善的方法是K折交叉驗證(K-fold cross-validation)。K折交叉驗證中,我們把原始訓練資料集分割成K個不重合的子資料集,然後我們做K次模型訓練和驗證。每一次,我們使用一個子資料集驗證模型,並使用其他K-1個子資料集來訓練模型。在這K次訓練和驗證中,每次用來驗證模型的子資料集都不同。最後,我們對這K次訓練誤差和驗證誤差分別求平均。

欠擬合和過擬合

模型訓練中經常出現的兩類典型問題:

1、模型無法得到較低的訓練誤差,一現象稱作欠擬合(underfitting);

2、模型的訓練誤差遠小於它在測試資料集上的誤差,該現象為過擬合(overfitting)。

在實踐中,我們要儘可能同時應對欠擬合和過擬合。雖然有很多因素可能導致這兩種擬合問題,在這裡我們重點討論兩個因素:模型複雜度和訓練資料集大小。

模型複雜度

為了解釋模型複雜度,以多項式函式擬合為例。給定一個由標量資料特徵x和對應的標量標籤y組成的訓練資料集,多項式函式擬合的目標是找一個K階多項式函式

 

來近似y。在上式中,wk是模型的權重引數,b是偏差引數。與線性迴歸相同,多項式函式擬合也使用平方損失函式。特別地,一階多項式函式擬合又叫線性函式擬合。

因為高階多項式函式模型引數更多,模型函式的選擇空間更大,所以高階多項式函式比低階多項式函式的複雜度更高。因此,高階多項式函式比低階多項式函式更容易在相同的訓練資料集上得到更低的訓練誤差。給定訓練資料集,模型複雜度和誤差之間的關係通常如下圖所示。給定訓練資料集,如果模型的複雜度過低,很容易出現欠擬合;如果模型複雜度過高,很容易出現過擬合。應對欠擬合和過擬合的一個辦法是針對資料集選擇合適複雜度的模型。

 

訓練資料集大小

影響欠擬合和過擬合的另一個重要因素是訓練資料集的大小。一般來說,如果訓練資料集中樣本數過少,特別是比模型引數數量(按元素計)更少時,過擬合更容易發生。此外,泛化誤差不會隨訓練資料集裡樣本數量增加而增大。因此,在計算資源允許的範圍之內,我們通常希望訓練資料集大一些,特別是在模型複雜度較高時,例如層數較多的深度學習模型。

權重衰減

模型的訓練誤差遠小於它在測試集上的誤差。雖然增大訓練資料集可能會減輕過擬合,但是獲取額外的訓練資料往往代價高昂。應對過擬合問題的常用方法:權重衰減(weight decay)。

方法

權重衰減等價於L2範數正則化(regularization)。正則化通過為模型損失函式新增懲罰項使學出的模型引數值較小,是應對過擬合的常用手段。我們先描述L2範數正則化,再解釋它為何又稱權重衰減。

L2範數正則化在模型原損失函式基礎上新增L2範數懲罰項,從而得到訓練所需要最小化的函式。L2範數懲罰項指的是模型權重引數每個元素的平方和與一個正的常數的乘積。以“線性迴歸”中的線性迴歸損失函式

 

丟棄法

除了權重衰減以外,深度學習模型常常使用丟棄法(dropout)來應對過擬合問題。丟棄法有一些不同的變體。本節中提到的丟棄法特指倒置丟棄法(inverted dropout)。

方法

“多層感知機”描述了一個單隱藏層的多層感知機。其中輸入個數為4,隱藏單元個數為5,且隱藏單元hihi(i=1,…,5i=1,…,5)的計算表示式為

多項式函式擬合實驗程式碼

  1 #!/usr/bin/env python
  2 # coding: utf-8
  3 
  4 # In[1]:
  5 
  6 
  7 get_ipython().run_line_magic('matplotlib', 'inline')
  8 import d2lzh as d2l
  9 from mxnet import autograd, gluon, nd
 10 from mxnet.gluon import data as gdata, loss as gloss, nn
 11 
 12 
 13 #  生成資料集
 14 # 
 15 # 我們將生成一個人工資料集。在訓練資料集和測試資料集中,給定樣本特徵x,我們使用如下的三階多項式函式來生成該樣本的標籤:
 16 # $$y = 1.2x - 3.4x^2 + 5.6x^3 + 5 + \epsilon,$$
 17 # 其中噪聲項ϵ服從均值為0、標準差為0.1的正態分佈。訓練資料集和測試資料集的樣本數都設為100。
 18 
 19 # In[2]:
 20 
 21 
 22 n_train, n_test, true_w, true_b = 100, 100, [1.2, -3.4, 5.6], 5
 23 features = nd.random.normal(shape=(n_train + n_test, 1))
 24 poly_features = nd.concat(features, nd.power(features, 2),
 25                           nd.power(features, 3))
 26 labels = (true_w[0] * poly_features[:, 0] + true_w[1] * poly_features[:, 1]
 27           + true_w[2] * poly_features[:, 2] + true_b)
 28 labels += nd.random.normal(scale=0.1, shape=labels.shape)
 29 
 30 
 31 # In[3]:
 32 
 33 
 34 #檢視生成的資料集的前兩個樣本
 35 features[:2], poly_features[:2], labels[:2]
 36 
 37 
 38 # In[4]:
 39 
 40 
 41 # 定義作圖函式semilogy,其中 y 軸使用了對數尺度。
 42 # 本函式已儲存在d2lzh包中方便以後使用
 43 def semilogy(x_vals, y_vals, x_label, y_label, x2_vals=None, y2_vals=None,
 44              legend=None, figsize=(3.5, 2.5)):
 45     d2l.set_figsize(figsize)
 46     d2l.plt.xlabel(x_label)
 47     d2l.plt.ylabel(y_label)
 48     d2l.plt.semilogy(x_vals, y_vals)
 49     if x2_vals and y2_vals:
 50         d2l.plt.semilogy(x2_vals, y2_vals, linestyle=':')
 51         d2l.plt.legend(legend)
 52 
 53 
 54 # 和線性迴歸一樣,多項式函式擬合也使用平方損失函式。因為我們將嘗試使用不同複雜度的模型來擬合生成的資料集,所以我們把模型定義部分放在fit_and_plot函式中。多項式函式擬合的訓練和測試步驟與“softmax迴歸的從零開始實現”一節介紹的softmax迴歸中的相關步驟類似。
 55 
 56 # In[5]:
 57 
 58 
 59 num_epochs, loss = 100, gloss.L2Loss()
 60 
 61 def fit_and_plot(train_features, test_features, train_labels, test_labels):
 62     net = nn.Sequential()
 63     net.add(nn.Dense(1))
 64     net.initialize()
 65     batch_size = min(10, train_labels.shape[0])
 66     train_iter = gdata.DataLoader(gdata.ArrayDataset(
 67         train_features, train_labels), batch_size, shuffle=True)
 68     trainer = gluon.Trainer(net.collect_params(), 'sgd',
 69                             {'learning_rate': 0.01})
 70     train_ls, test_ls = [], []
 71     for _ in range(num_epochs):
 72         for X, y in train_iter:
 73             with autograd.record():
 74                 l = loss(net(X), y)
 75             l.backward()
 76             trainer.step(batch_size)
 77         train_ls.append(loss(net(train_features),
 78                              train_labels).mean().asscalar())
 79         test_ls.append(loss(net(test_features),
 80                             test_labels).mean().asscalar())
 81     print('final epoch: train loss', train_ls[-1], 'test loss', test_ls[-1])
 82     semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'loss',
 83              range(1, num_epochs + 1), test_ls, ['train', 'test'])
 84     print('weight:', net[0].weight.data().asnumpy(),
 85           '\nbias:', net[0].bias.data().asnumpy())
 86 
 87 
 88 # ### 三階多項式函式擬合(正常)
 89 # 我們先使用與資料生成函式同階的三階多項式函式擬合。實驗表明,這個模型的訓練誤差和在測試資料集的誤差都較低。訓練出的模型引數也接近真實值:$$w_1 = 1.2, w_2=-3.4, w_3=5.6, b = 5$$
 90 
 91 # In[6]:
 92 
 93 
 94 fit_and_plot(poly_features[:n_train, :], poly_features[n_train:, :],
 95              labels[:n_train], labels[n_train:])
 96 
 97 
 98 # ### 線性函式擬合(欠擬合)
 99 # 我們再試試線性函式擬合。很明顯,該模型的訓練誤差在迭代早期下降後便很難繼續降低。在完成最後一次迭代週期後,訓練誤差依舊很高。線性模型在非線性模型(如三階多項式函式)生成的資料集上容易欠擬合。
100 
101 # In[7]:
102 
103 
104 fit_and_plot(features[:n_train, :], features[n_train:, :], labels[:n_train],
105              labels[n_train:])
106 
107 
108 # ### 訓練樣本不足(過擬合)
109 # 事實上,即便使用與資料生成模型同階的三階多項式函式模型,如果訓練樣本不足,該模型依然容易過擬合。讓我們只使用兩個樣本來訓練模型。顯然,訓練樣本過少了,甚至少於模型引數的數量。這使模型顯得過於複雜,以至於容易被訓練資料中的噪聲影響。在迭代過程中,儘管訓練誤差較低,但是測試資料集上的誤差卻很高。這是典型的過擬合現象。
110 
111 # In[8]:
112 
113 
114 fit_and_plot(poly_features[0:2, :], poly_features[n_train:, :], labels[0:2],
115              labels[n_train:])
View Code

高維線性迴歸實驗

# ### 高維線性迴歸實驗
# 以高維線性迴歸為例來引入一個過擬合問題,並使用權重衰減來應對過擬合。設資料樣本特徵的維度為p。對於訓練資料集和測試資料集中特徵為$x_1, x_2, \ldots, x_p$的任一樣本,我們使用如下的線性函式來生成該樣本的標籤:
# $$y = 0.05 + \sum_{i = 1}^p 0.01x_i + \epsilon,$$
# 其中噪聲項$\epsilon$服從均值為0、標準差為0.01的正態分佈。為了較容易地觀察過擬合,我們考慮高維線性迴歸問題,如設維度p=200;同時,我們特意把訓練資料集的樣本數設低,如20。

# In[10]:


get_ipython().run_line_magic('matplotlib', 'inline')
import d2lzh as d2l
from mxnet import autograd, gluon, init, nd
from mxnet.gluon import data as gdata, loss as gloss, nn

n_train, n_test, num_inputs = 20, 100, 200
true_w, true_b = nd.ones((num_inputs, 1)) * 0.01, 0.05

features = nd.random.normal(shape=(n_train + n_test, num_inputs))
labels = nd.dot(features, true_w) + true_b
labels += nd.random.normal(scale=0.01, shape=labels.shape)
train_features, test_features = features[:n_train, :], features[n_train:, :]
train_labels, test_labels = labels[:n_train], labels[n_train:]


# In[11]:


def init_params():
    w = nd.random.normal(scale=1, shape=(num_inputs, 1))
    b = nd.zeros(shape=(1

DROPOUT程式碼實現

 1 # ### dropout程式碼實現
 2 
 3 # In[17]:
 4 
 5 
 6 import d2lzh as d2l
 7 from mxnet import autograd, gluon, init, nd
 8 from mxnet.gluon import loss as gloss, nn
 9 
10 def dropout(X, drop_prob):
11     assert 0 <= drop_prob <= 1
12     keep_prob = 1 - drop_prob
13     # 這種情況下把全部元素都丟棄
14     if keep_prob == 0:
15         return X.zeros_like()
16     mask = nd.random.uniform(0, 1, X.shape) < keep_prob
17     return mask * X / keep_prob
18 
19 
20 # In[18]:
21 
22 
23 
24 X = nd.arange(16).reshape((2, 8))
25 dropout(X, 0)
26 
27 
28 # In[19]:
29 
30 
31 dropout(X, 0.5)
32 
33 
34 # In[20]:
35 
36 
37 dropout(X, 1)
38 
39 
40 # In[22]:
View Code

&n