1. 程式人生 > 實用技巧 >《動手學深度學習》mxnet版/第三章學習筆記

《動手學深度學習》mxnet版/第三章學習筆記

第三章


從單層神經網路延伸到多層神經網路,並通過多層感知機引入深度學習模型

  • 線性迴歸
  • 線性迴歸的從零開始實現
  • 線性迴歸的簡潔實現
  • softmax迴歸
  • softmax迴歸的簡潔實現
  • 多層感知機
  • 模型選擇、欠擬合和過擬合
  • 權重衰減
  • 丟棄法
  • 正向傳播、反向傳播和計算圖
  • 數值穩定性和模型初始化
  • 實戰Kaggle比賽:房價預測

線性迴歸


1.模型與模型訓練

線性迴歸假設輸出與各個輸入之間是線性關係
如: y = x1w1 + x2w2 + b;
基於輸入x1和x2來計算輸出y的表示式,其中w1和w2是權重(weight),b是偏差(bias),且均為標量。
它們是線性迴歸模型的引數(pa-rameter)

。模型輸出^ y是線性迴歸對真實價格y的預測或估計。
接下來我們需要通過資料來尋找特定的模型引數值,使模型在資料上的誤差儘可能小。這個過程 叫作模型訓練(model training)

2.損失函式

選取一個非負數作為誤差,且數值越小表示誤差越小。一個常用的選擇是平方函式。它在評估索引為i的樣本誤差的表示式為:
ℓ (i) (w1; w2; b) = 1/2 * ( Y (i)- y (i) )^2
誤差越小表 示預測價格與真實價格越相近,且當二者相等時誤差為0

3.向量計算表示式

from mxnet import nd 
from time import time 
#對兩個向量相加的兩種方法
a = nd.ones(shape=1000) 
b = nd.ones(shape=1000) 
#向量相加的一種方法是,將這兩個向量按元素逐一做標量加法
start = time() 
c = nd.zeros(shape=1000) 
for i in range(1000): 
    c[i] = a[i] + b[i] 
time() - start #計算運算時間 為0.15223002433776855 
#向量相加的另一種方法是,將這兩個向量直接做向量加法
start = time() 
d = a + b 
time() - start #0.00029015541076660156 應該儘可能採用向量計算,以提升計算效率

線性迴歸的從零開始實現

#首先匯入包
%matplotlib inline #用於繪圖 
from IPython import display 
from matplotlib import pyplot as plt 
from mxnet import autograd, nd 
import random

1.生成資料集

設訓練資料集樣本數為1000,輸入個數(特徵數)為2,我們使用線性迴歸模型真實權重w = [2,-3.4]^⊤ 和偏差b = 4.2,以及一個隨機噪聲項ε來生成標籤
y = X1w1+X2w2+b+ε

num_inputs = 2 
num_examples = 1000 
true_w = [2, -3.4] 
true_b = 4.2 
features = nd.random.normal(scale=1, shape=(num_examples, num_inputs)) 
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b 
labels += nd.random.normal(scale=0.01, shape=labels.shape) 
#features的每一行是一個⻓度為2的向量,而labels的每一行是一個⻓度為1的向量(標量)
def use_svg_display(): 
# 用向量圖顯示 
    display.set_matplotlib_formats('svg') 
def set_figsize(figsize=(3.5, 2.5)): #figsize大小為寬、長
    use_svg_display() 
    # 設定圖的尺寸 
    plt.rcParams['figure.figsize'] = figsize 
set_figsize() 
plt.scatter(features[:, 1].asnumpy(), labels.asnumpy(), 1); # 加分號只顯示圖 
#將上面的plt作圖函式以及use_svg_display函式和set_figsize函式定義在d2lzh包 裡。以後在作圖時,我們將直接呼叫d2lzh.plt。由於plt在d2lzh包中是一個全域性變數,我們 在作圖前只需要呼叫d2lzh.set_figsize()即可列印向量圖並設定圖的尺寸

2.讀取資料

讀取資料 在訓練模型的時候,我們需要遍歷資料集並不斷讀取小批量資料樣本。這裡我們定義一個函式: 它每次返回batch_size(批量大小)個隨機樣本的特徵和標籤

def data_iter(batch_size, features, labels): 
    num_examples = len(features) 
    indices = list(range(num_examples)) 
    random.shuffle(indices) # 樣本的讀取順序是隨機的 
    for i in range(0, num_examples, batch_size): 
        j = nd.array(indices[i: min(i + batch_size, num_examples)])
        yield features.take(j), labels.take(j) # take函式根據索引返回對應元素 
#每個批量的特徵形狀為(10, 2),分別對應批量大小和輸入個數;標籤形狀為批量大小
batch_size = 10 
for X, y in data_iter(batch_size, features, labels): 
    print(X, y) 
    break 

3.初始化以及定義模型

我們將權重初始化成均值為0、標準差為0.01的正態隨機數,偏差則初始化成0。

w = nd.random.normal(scale=0.01, shape=(num_inputs, 1)) 
b = nd.zeros(shape=(1,))
#之後的模型訓練中,需要對這些引數求梯度來迭代引數的值,因此我們需要建立它們的梯度。 
w.attach_grad() 
b.attach_grad() 

#線性迴歸的向量計算表示式的實現。我們使用dot函式做矩陣乘法
def linreg(X, w, b): 
    # 本函式已儲存在d2lzh包中方便以後使用 
    return nd.dot(X, w) + b 

4.定義損失函式

方損失來定義線性迴歸的損失函式。在實現中,我們需要把真實值y變形成預測值y_hat的形狀。
以下函式返回的結果也將和y_hat的形狀相同

def squared_loss(y_hat, y): 
    return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2 

5.優化損失函式

以下的sgd函式實現了上一節中介紹的小批量隨機梯度下降演算法。它通過不斷迭代模型引數來優 化損失函式。這裡自動求梯度模組計算得來的梯度是一個批量樣本的梯度和。我們將它除以批量 大小來得到平均值

def sgd(params, lr, batch_size): 
    for param in params: 
        param[:] = param - lr * param.grad / batch_size 

6.訓練模型

在每次迭代中,我們根據當前讀取的小批量資料樣本(特徵X和標籤y),通過呼叫反向函式backward計算小批量隨機梯度,並呼叫優化演算法sgd迭代模型引數.在一個迭代週期(epoch)中,我們將完整遍歷一遍data_iter函式,並對訓練資料集中所有 樣本都使用一次

lr = 0.03 #學習率
num_epochs = 3  #迭代週期個數 迭代週期數設得越大模型可能越有效,但是訓練時間可能過⻓
net = linreg # 線性迴歸 
loss = squared_loss 
for epoch in range(num_epochs): 
    # 訓練模型一共需要num_epochs個迭代週期 
    # 在每一個迭代週期中,會使用訓練資料集中所有樣本一次(假設樣本數能夠被批量大小整除)
    # 和y分別是小批量樣本的特徵和標籤 
    for X, y in data_iter(batch_size, features, labels): 
        with autograd.record(): 
            l = loss(net(X, w, b), y) # l是有關小批量X和y的損失 
        l.backward() # 小批量的損失對模型引數求梯度 
        sgd([w, b], lr, batch_size) # 使用小批量隨機梯度下降迭代模型引數 
        train_l = loss(net(features, w, b), labels) 
        print('epoch %d, loss %f' % (epoch + 1, train_l.mean().asnumpy())) 

線性迴歸的簡潔實現

介紹如何使用MXNet提供的Gluon介面更方便地實現線性迴歸的訓練

1.生成資料集

#features是訓練資料特徵,labels是標籤
from mxnet import autograd, nd 
num_inputs = 2 
num_examples = 1000 
true_w = [2, -3.4] 
true_b = 4.2 
features = nd.random.normal(scale=1, shape=(num_examples, num_inputs)) 
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b 
labels += nd.random.normal(scale=0.01, shape=labels.shape) 

2.讀取資料

Gluon提供了data包來讀取資料。由於data常用作變數名,我們將匯入的data模組用添加了Gluon首字母的假名gdata代替。在每一次迭代中,我們將隨機讀取包含10個數據樣本的小批量

from mxnet.gluon import data as gdata 
batch_size = 10 
# 將訓練資料的特徵和標籤組合 
dataset = gdata.ArrayDataset(features, labels) 
# 隨機讀取小批量 
data_iter = gdata.DataLoader(dataset, batch_size, shuffle=True) 

3.定義模型且初始化模型引數

# 在Gluon中, Sequential例項可以看作是一個串聯各個層的容器。在構造模型時,我們在該容器中依次新增 層。當給定輸入資料時,容器中的每一層將依次計算並將輸出作為下一層的輸入。
from mxnet.gluon import nn net = nn.Sequential() 
# 作為一個單層神經網路,線性迴歸輸出層中的神經元和輸入層中各個輸入完全連線。因此,線性迴歸的輸出層叫全連線層。在Gluon中,全連線層是一個Dense例項。我們定義該層輸出個數為1
net.add(nn.Dense(1)) 
#值得一提的是,在Gluon中我們無須指定每一層輸入的形狀,例如線性迴歸的輸入個數。當模型 得到資料時,例如後面執行net(X)時,模型將自動推斷出每一層的輸入個數

#在使用net前,我們需要初始化模型引數,如線性迴歸模型中的權重和偏差。我們從MXNet匯入init模組。該模組提供了模型引數初始化的各種方法。這裡的init是initializer的縮寫形式。我們通過init.Normal(sigma=0.01)指定權重引數每個元素將在初始化時隨機取樣於均值為0、標準差為0.01的正態分佈。偏差引數預設會初始化為零
from mxnet import init 
net.initialize(init.Normal(sigma=0.01)) 

4.定義損失函式

在Gluon中,loss模組定義了各種損失函式。我們用假名gloss代替匯入的loss模組,並直接 使用它提供的平方損失作為模型的損失函式。

from mxnet.gluon import loss as gloss 
loss = gloss.L2Loss() # 平方損失又稱L2範數損失,L2範數是指向量各元素的平方和然後求平方根

5.定義優化演算法

我們也無須實現小批量隨機梯度下降。在匯入Gluon後,我們建立一個Trainer例項,並 指定學習率為0.03的小批量隨機梯度下降(sgd)為優化演算法。該優化演算法將用來迭代net例項所 有通過add函式巢狀的層所包含的全部引數。這些引數可以通過collect_params函式獲取。

from mxnet import gluon 
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.03}) 

6.訓練模型

在使用Gluon訓練模型時, 我們通過呼叫Trainer例項的step函式來迭代模型引數。上一節中我們提到,由於變數l是⻓度為batch_size的一維NDArray,執行l.backward()等價於執行l.sum().backward()。按照小批量隨機梯度下降的定義,我們在step函式中指明批量大小,從 而對批量中樣本梯度求平均

num_epochs = 3 
for epoch in range(1, num_epochs + 1): 
    for X, y in data_iter: 
        with autograd.record(): 
            l = loss(net(X), y) 
        l.backward() 
        trainer.step(batch_size) 
    l = loss(net(features), labels) 
    print('epoch %d, loss: %f' % (epoch, l.mean().asnumpy())) 

softmax迴歸

和線性迴歸不同,softmax迴歸的輸出單元從一個變成了多個,且引入了softmax運算使輸出更適合離散值的預測和訓練

1.softmax迴歸模型

softmax迴歸跟線性迴歸一樣將輸入特徵與權重做線性疊加。與線性迴歸的一個主要不同在於,softmax迴歸的輸出值個數等於標籤裡的類別數.
為一共有4種特徵和3種輸出動物類別,所以 權重包含12個標量(帶下標的w) 、偏差包含3個標量(帶下標的b), 且對每個輸入計算o1; o2; o3這3個輸出
o1 = x1w11 + x2w21 + x3w31 + x4w41 + b1;
o2 = x1w12 + x2w22 + x3w32 + x4w42 + b2;
o3 = x1w13 + x2w23 + x3w33 + x4w43 + b3:
softmax迴歸同線性迴歸一樣,也是一個單層神經網路。由於每個輸出o1; o2; o3的計算都要依賴於所有的輸入x1; x2; x3; x4,softmax迴歸的輸出層也是一個全連線層
分類問題需要得到離散的預測輸出,一個簡單的辦法是將輸出值oi當作預測類別是i的置信度,並將值最大的輸出所對應的類作為預測輸出,即輸出argmax。例如,如果o1; o2; o3分別 為0:1; 10; 0:1,由於o2最大,那麼預測類別為2,其代表貓
softmax運算子(softmax operator)解決了以上兩個問題。它通過下式將輸出值變換成值為正且 和為1的概率分佈:

影象分類資料集(Fashion-MNIST)

Fashion-MNIST是一個10類服飾分類資料集,用於體現演算法的效能

softmax迴歸的簡潔實現

使用Gluon來實現一個softmax迴歸模型

#首先匯入所需的包或模組。
%matplotlib inline 
import d2lzh as d2l 
from mxnet import gluon, init 
from mxnet.gluon import loss as gloss, nn 
batch_size = 256 
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) 
#softmax迴歸的輸出層是一個全連線層。因此,我們新增一個輸出個數為10的全連線層。我們使用均值為0、標準差為0.01的正態分佈隨機初始化模型的權重引數。
net = nn.Sequential() 
net.add(nn.Dense(10)) 
net.initialize(init.Normal(sigma=0.01)) 
#Gluon提供了一個包括softmax運算和交叉熵損失計算的函式。它的數值穩 定性更好
loss = gloss.SoftmaxCrossEntropyLoss() 
#使用學習率為0.1的小批量隨機梯度下降作為優化演算法
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.1}) 
#訓練模型

多層感知機

多層感知機在單層神經網路的基礎上引入了一到多個隱藏層(hidden layer)

1.隱藏層

位於輸入層和輸出層之間
H = XWh + bh;
O = HWo + bo;
雖然神經網路引入了隱藏層,卻依然等價於一個單層神經網路:其中 輸出層權重引數為Wh,Wo,偏差引數為bhWo + bo。不難發現,即便再新增更多的隱藏層,以上設計依然只能與僅含輸出層的單層神經網路等價

2.啟用函式

解決問題的一個方法是引入非線性變換,例如對隱藏變數使用按元素運算的非線性函式進行變換,然後再作為下一個全連線層的輸入。這個非線性函式被稱為啟用函式(activation function)
ReLU函式
ReLU(rectifiedlinear unit)函式提供了一個很簡單的非線性變換。給定元素x,該函式定義為
ReLU(x) = max(x;0)
可以看出,ReLU函式只保留正數元素,並將負數元素清零

3.多層感知機

多層感知機就是含有至少一個隱藏層的由全連線層組成的神經網路, 且每個隱藏層的輸出通過激 活函式進行變換。多層感知機的層數和各隱藏層中隱藏單元個數都是超引數。以單隱藏層為例並 沿用本節之前定義的符號,多層感知機按以下方式計算輸出
H = φ(XW h + bh);
O = HW o + bo;
其中φ表示啟用函式。在分類問題中,我們可以對輸出O做softmax運算,並使用softmax迴歸中的交叉熵損失函式。 在迴歸問題中,我們將輸出層的輸出個數設為1,並將輸出O直接提供給線性迴歸中使用的平方損失函式

4.多層感知機的簡潔實現

import d2lzh as d2l 
from mxnet import gluon, init 
from mxnet.gluon import loss as gloss, nn
#和softmax迴歸唯一的不同在於,我們多加了一個全連線層作為隱藏層。它的隱藏單元個數為256,並使用ReLU函式作為啟用函式
net = nn.Sequential() 
net.add(nn.Dense(256, activation='relu'), nn.Dense(10)) 
net.initialize(init.Normal(sigma=0.01))
batch_size = 256 
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) 
loss = gloss.SoftmaxCrossEntropyLoss() 
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.5}) 
num_epochs = 5 
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, trainer) 

模型選擇、欠擬合和過擬合

1.訓練誤差和泛化誤差

訓練誤差和泛化誤差。通俗來講,前者指模型在訓練資料集上表現出的誤差,後者指模型在任意一個測試資料樣本上表現出的誤差的期望,並常常通過測試資料集上的誤差來近似.
當訓練資料不夠用時,預留大量的驗證資料顯得太奢侈。一種改善的方法是K折交叉驗證(K-fold cross-validation)。在K折交叉驗證中,我們把原始訓練資料集分割成K個不重合的子資料集,然後我們做K次模型訓練和驗證。每一次,我們使用一個子資料集驗證模型,並使用其他K-1個子資料集來訓練模型。在這K次訓練和驗證中,每次用來驗證模型的子資料集都不同。最後,我們對這K次訓練誤差和驗證誤差分別求平均.

2.欠擬合和過擬合

模型無法得到較低的訓練誤差, 我們將這一現象稱作欠擬合(underfitting)
模型的訓練誤差遠小於它在測試資料集上 的誤差,我們稱該現象為過擬合(overfitting)
模型的複雜度過低, 很容易出現欠擬合;如果模型複雜度過高,很容易出現過擬合。應對欠擬合和過擬合的一個辦法是針對資料集選擇合適複雜度的模型

權重衰減

權重衰減等價於L2範數正則化(regularization) 。正則化通過為模型損失函式新增懲罰項使學出 的模型引數值較小,是應對過擬合的常用手段
L2範數懲罰項指的是模型權重引數每個元素的平方和與一個正的常數的乘積,L2範數正則化令權重w1和w2先自乘小於1的數,再減去不含懲罰項的梯度

1.簡潔實現

def fit_and_plot_gluon(wd): 
    net = nn.Sequential() 
    net.add(nn.Dense(1)) 
    net.initialize(init.Normal(sigma=1)) # 對權重引數衰減。權重名稱一般是以weight結尾 
    trainer_w = gluon.Trainer(net.collect_params('.*weight'), 'sgd', {'learning_rate': lr, 'wd': wd}) # 不對偏差引數衰減。偏差名稱一般是以bias結尾 
    trainer_b = gluon.Trainer(net.collect_params('.*bias'), 'sgd', {'learning_rate': lr}) 
    train_ls, test_ls = [], [] 
    for _ in range(num_epochs): 
        for X, y in train_iter: 
            with autograd.record(): 
                l = loss(net(X), y) 
            l.backward()            
            #對兩個Trainer例項分別呼叫step函式,從而分別更新權重和偏差 
            trainer_w.step(batch_size)     
            trainer_b.step(batch_size) 
        train_ls.append(loss(net(train_features), train_labels).mean().asscalar()) 
        test_ls.append(loss(net(test_features), test_labels).mean().asscalar())
        d2l.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'loss', range(1, num_epochs + 1), test_ls, ['train', 'test']) #畫圖
        print('L2 norm of w:', net[0].weight.data().norm().asscalar()) 
#fit_and_plot_gluon(0) L2 norm of w: 13.311798 
#fit_and_plot_gluon(3) L2 norm of w: 0.03225094 
使用權重衰減可以在一定程度上緩解過擬合問題

丟棄法

深度學習模型常常使用丟棄法(dropout)來應對過擬合問題
當對多層感知機的隱藏層使用丟棄法時,該層的隱藏單元將有一定概率被丟棄掉。設丟棄概率為p,那麼有p的概率hi會被清零,有1-p的概率hi會除以1-p做拉伸

1.簡潔實現

在Gluon中,我們只需要在全連線層後新增Dropout層並指定丟棄概率。在訓練模型時,Dropout層將以指定的丟棄概率隨機丟棄上一層的輸出元素;在測試模型時,Dropout層並不發揮作用,丟棄法只在訓練模型時使用。

drop_prob1, drop_prob2 = 0.2, 0.5 
net = nn.Sequential() 
net.add(nn.Dense(256, activation="relu"), 
nn.Dropout(drop_prob1), # 在第一個全連線層後新增丟棄層 nn.Dense(256, activation="relu"), nn.Dropout(drop_prob2), # 在第二個全連線層後新增丟棄層 nn.Dense(10)) net.initialize(init.Normal(sigma=0.01)) 
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr}) 
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, trainer) 

正向傳播、反向傳播和計算圖

使用數學和計算圖兩個方式來描述正向傳播和反向傳播

正向傳播

正向傳播是指對神經網路沿著從輸入層到輸出層的順序,依次計算並存儲模型的中間變數(包括輸出)
假設輸入是一個特徵為x的樣本,且不考慮偏差項,那麼中間變數
z = W^(1) * x
其中W^(1)是隱藏層的權重引數。把中間變數z輸入按元素運算的啟用函式φ後,將得到向量⻓度為h的隱藏層變數
h = φ(z)
隱藏層變數h也是一箇中間變數。假設輸出層引數只有權重W^(2),可以得到向量⻓度為q的輸出層變數
o = W^(2) * h
假設損失函式為ℓ,且樣本標籤為y,可以計算出單個數據樣本的損失項
L = ℓ(o; y)
根據L2範數正則化的定義,給定超引數,正則化項即
s = λ/2 ( ∥W ^(1) ∥ ^2F+ ∥W (2) ∥ ^ 2)
模型在給定的資料樣 本上帶正則化的損失為
J = L + s
將J稱為有關給定資料樣本的目標函式,並在以下的討論中簡稱目標函式

反向傳播和計算圖

反向傳播指的是計算神經網路引數梯度的方法。總的來說,反向傳播依據微積分中的鏈式法則, 沿著從輸出層到輸入層的順序, 依次計算並存儲目標函式有關神經網路各層的中間變數以及引數的梯度
在訓練深度學習模型時,正向傳播和反向傳播相互依賴

數值穩定性和模型初始化

深度模型有關數值穩定性的典型問題是衰減(vanishing)和爆炸(explosion)

衰減和爆炸

當神經網路的層數較多時,模型的數值穩定性容易變差。
假設輸入和所有層的權重引數都是標量,如權重引數為0.2和5,多層感知機的第30層輸出為輸入X分別與
0.2^30 =1 * 10^21 (衰減)和530=9*1020(爆炸)的乘積。類似地,當層數較多時,梯度的計算也更容易出現衰減或爆炸

隨機初始化模型引數

在神經網路中,通常需要隨機初始化模型引數.
使用net. initialize(init.Normal(sigma=0.01))使模型net的權重引數採用正態分佈的隨機初始化方式。如果不指定初始化方法,如net.initialize(),MXNet將使用預設的隨機初始化方法:權重引數每個元素隨機取樣於-0.07到0.07之間的均勻分佈,偏差引數全部清零

有人說隨機初始化模型引數是為了“打破對稱性” 。這裡的“對稱”應如何理解
當我們把所有的引數都設成0的話,那麼上面的每一條邊上的權重就都是0,那麼神經網路就還是對稱的,對於同一層的每個神經元,它們就一模一樣了
這樣的後果是什麼呢?我們知道,不管是哪個神經元,它的前向傳播和反向傳播的演算法都是一樣的,如果初始值也一樣的話,不管訓練多久,它們最終都一樣,都無法打破對稱(fail to break the symmetry),那每一層就相當於只有一個神經元,最終L層神經網路就相當於一個線性的網路,如Logistic regression,線性分類器對我們上面的非線性資料集是“無力”的,所以最終訓練的結果就瞎猜一樣

實戰Kaggle比賽:房價預測

1.資料讀取加預處理

2.損失函式加訓練函式

3.k折交叉驗證

4.k折交叉驗證效果圖

5.利用資料集訓練加預測

6.kaggle提及結果