AI Studio 飛槳 零基礎入門深度學習筆記2-基於Python編寫完成房價預測任務的神經網路模型

2020-08-12 14:55:31

波士頓房價預測任務

上一節我們初步認識了神經網路的基本概念(如神經元、多層連線、前向計算、計算圖)和模型結構三要素(模型假設、評價函數和優化演算法)。本節將以「波士頓房價」任務爲例,向讀者介紹使用Python語言和Numpy庫來構建神經網路模型的思考過程和操作方法。

波士頓房價預測是一個經典的機器學習任務,類似於程式設計師世界的「Hello World」。和大家對房價的普遍認知相同,波士頓地區的房價是由諸多因素影響的。該數據集統計了13種可能影響房價的因素和該型別房屋的均價,期望構建一個基於13個因素進行房價預測的模型,如 圖1 所示。


圖1:波士頓房價影響因素示意圖


對於預測問題,可以根據預測輸出的型別是連續的實數值,還是離散的標籤,區分爲迴歸任務和分類任務。因爲房價是一個連續值,所以房價預測顯然是一個迴歸任務。下面 下麪我們嘗試用最簡單的線性迴歸模型解決這個問題,並用神經網路來實現這個模型。

線性迴歸模型

假設房價和各影響因素之間能夠用線性關係來描述:

y=j=1Mxjwj+by = {\sum_{j=1}^Mx_j w_j} + b

模型的求解即是通過數據擬合出每個wjw_jbb。其中,wjw_jbb分別表示該線性模型的權重和偏置。一維情況下,wjw_jbb 是直線的斜率和截距。

線性迴歸模型使用均方誤差作爲損失函數(Loss),用以衡量預測房價和真實房價的差異,公式如下:

MSE=1ni=1n(Yi^Yi)2MSE = \frac{1}{n} \sum_{i=1}^n(\hat{Y_i} - {Y_i})^{2}


思考:

爲什麼要以均方誤差作爲損失函數?即將模型在每個訓練樣本上的預測誤差加和,來衡量整體樣本的準確性。這是因爲損失函數的設計不僅僅要考慮「合理性」,同樣需要考慮「易解性」,這個問題在後面的內容中會詳細闡述。


線性迴歸模型的神經網路結構

神經網路的標準結構中每個神經元由加權和與非線性變換構成,然後將多個神經元分層的擺放並連線形成神經網路。線性迴歸模型可以認爲是神經網路模型的一種極簡特例,是一個只有加權和、沒有非線性變換的神經元(無需形成網路),如 圖2 所示。


圖2:線性迴歸模型的神經網路結構


構建波士頓房價預測任務的神經網路模型

深度學習不僅實現了模型的端到端學習,還推動了人工智慧進入工業大生產階段,產生了標準化、自動化和模組化的通用框架。不同場景的深度學習模型具備一定的通用性,五個步驟即可完成模型的構建和訓練,如 圖3 所示。


圖3:構建神經網路/深度學習模型的基本步驟


正是由於深度學習的建模和訓練的過程存在通用性,在構建不同的模型時,只有模型三要素不同,其它步驟基本一致,深度學習框架纔有用武之地。

1 數據處理

數據處理包含五個部分:數據匯入、數據形狀變換、數據集劃分、數據歸一化處理和封裝load data函數。數據預處理後,才能 纔能被模型呼叫。

1.1 讀入數據

我們首先來看一下數據的大致結構。間隔爲空格\t

在这里插入图片描述

# 匯入需要用到的package
import numpy as np
import json
# 讀入訓練數據
datafile = './work/housing.data'
data = np.fromfile(datafile, sep=' ')  # 以此讀取數據存入1維陣列中 注意這裏是(7084,)
data
np.shape(data)
(7084,)

1.2 數據形狀變換

由於讀入的原始數據是1維的,所有數據都連在一起。因此需要我們將數據的形狀進行變換,形成一個2維的矩陣,每行爲一個數據樣本(14個值),每個數據樣本包含13個X(影響房價的特徵)和一個Y(該型別房屋的均價)。

# 讀入之後的數據被轉化成1維array,其中array的第0-13項是第一條數據,第14-27項是第二條數據,以此類推.... 
# 這裏對原始數據做reshape,變成N x 14的形式
feature_names = [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE','DIS', 
                 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ]  # 給定特徵名稱 數量 定義
feature_num = len(feature_names)
data = data.reshape([-1, feature_num])  # 這裏採用numpy的功能,根據存在的維度計算另一個shape值--給定14,計算出506
print(data.shape)
data[:5,:]  # 檢視前五行數據
(506, 14)





array([[6.3200e-03, 1.8000e+01, 2.3100e+00, 0.0000e+00, 5.3800e-01,
        6.5750e+00, 6.5200e+01, 4.0900e+00, 1.0000e+00, 2.9600e+02,
        1.5300e+01, 3.9690e+02, 4.9800e+00, 2.4000e+01],
       [2.7310e-02, 0.0000e+00, 7.0700e+00, 0.0000e+00, 4.6900e-01,
        6.4210e+00, 7.8900e+01, 4.9671e+00, 2.0000e+00, 2.4200e+02,
        1.7800e+01, 3.9690e+02, 9.1400e+00, 2.1600e+01],
       [2.7290e-02, 0.0000e+00, 7.0700e+00, 0.0000e+00, 4.6900e-01,
        7.1850e+00, 6.1100e+01, 4.9671e+00, 2.0000e+00, 2.4200e+02,
        1.7800e+01, 3.9283e+02, 4.0300e+00, 3.4700e+01],
       [3.2370e-02, 0.0000e+00, 2.1800e+00, 0.0000e+00, 4.5800e-01,
        6.9980e+00, 4.5800e+01, 6.0622e+00, 3.0000e+00, 2.2200e+02,
        1.8700e+01, 3.9463e+02, 2.9400e+00, 3.3400e+01],
       [6.9050e-02, 0.0000e+00, 2.1800e+00, 0.0000e+00, 4.5800e-01,
        7.1470e+00, 5.4200e+01, 6.0622e+00, 3.0000e+00, 2.2200e+02,
        1.8700e+01, 3.9690e+02, 5.3300e+00, 3.6200e+01]])

1.3 數據集劃分

將數據集劃分成訓練集和測試集,其中訓練集用於確定模型的參數,測試集用於評判模型的效果。爲什麼要對數據集進行拆分,而不能直接應用於模型訓練呢?這與學生時代的授課和考試關係比較類似,如 圖4 所示。


圖4:訓練集和測試集拆分的意義


上學時總有一些自作聰明的同學,平時不認真學習,考試前臨陣抱佛腳,將習題死記硬背下來,但是成績往往並不好。因爲學校期望學生掌握的是知識,而不僅僅是習題本身。另出新的考題,才能 纔能鼓勵學生努力去掌握習題背後的原理。同樣我們期望模型學習的是任務的本質規律,而不是訓練數據本身,模型訓練未使用的數據,才能 纔能更真實的評估模型的效果。

在本案例中,我們將80%的數據用作訓練集,20%用作測試集,實現程式碼如下。通過列印訓練集的形狀,可以發現共有404個樣本,每個樣本含有13個特徵和1個預測值。

ratio = 0.8  # 定義數據比例
offset = int(data.shape[0] * ratio)
training_data = data[:offset]
training_data.shape
(404, 14)

1.4 數據歸一化處理

對每個特徵進行歸一化處理,使得每個特徵的取值縮放到0~1之間。這樣做有兩個好處:一是模型訓練更高效;二是特徵前的權重大小可以代表該變數對預測結果的貢獻度(因爲每個特徵值本身的範圍相同)。不理解沒關係,這個問題在結束後會再被提及,那時候會更加清楚。
歸一化只對訓練集進行
歸一化方法又稱爲離差標準化,使結果值對映到[0,1]之間,轉換函數如下:
在这里插入图片描述

# 計算train數據集的最大值,最小值,平均值
maximums, minimums, avgs = \
                     training_data.max(axis=0), \
                     training_data.min(axis=0), \
     training_data.sum(axis=0) / training_data.shape[0]  
# 換行時,除非(),[],{}不需新增。其餘需加上\ 表示換行標誌
# axis = 0 表示沿着從上到下的方向,分別計算各列的值。得到的是一個(14,)的列表
# 對數據進行歸一化處理
print(maximums)
for i in range(feature_num):
    data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i])
# 每次得到一列歸一化數據
[ 88.9762 100.      25.65     1.       0.871    8.78   100.      12.1265
  24.     666.      22.     396.9     37.97    50.    ]

1.5 封裝成load data函數

將上述幾個數據處理操作封裝成load data函數,以便下一步模型的呼叫,實現方法如下。

def load_data():
    # 從檔案匯入數據
    datafile = './work/housing.data'
    data = np.fromfile(datafile, sep=' ')

    # 每條數據包括14項,其中前面13項是影響因素,第14項是相應的房屋價格中位數
    feature_names = [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', \
                      'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ]
    feature_num = len(feature_names)

    # 將原始數據進行Reshape,變成[N, 14]這樣的形狀
    data = data.reshape([data.shape[0] // feature_num, feature_num])

        # 將原數據集拆分成訓練集和測試集
    # 這裏使用80%的數據做訓練,20%的數據做測試
    # 測試集和訓練集必須是沒有交集的
    ratio = 0.8
    offset = int(data.shape[0] * ratio)
    training_data = data[:offset]
    # 計算訓練集的最大值,最小值,平均值
    maximums, minimums, avgs = training_data.max(axis=0), training_data.min(axis=0), \
                                 training_data.sum(axis=0) / training_data.shape[0]

    # 對數據進行歸一化處理
    for i in range(feature_num):
        data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i])
    

    training_data = data[:offset]
    test_data = data[offset:]
    return training_data, test_data
# 獲取數據
training_data, test_data = load_data()
x = training_data[:, :-1]
y = training_data[:, -1:]
# 檢視數據
print(x[0])
print(y[0])
[-0.02146321  0.03767327 -0.28552309 -0.08663366  0.01289726  0.04634817
  0.00795597 -0.00765794 -0.25172191 -0.11881188 -0.29002528  0.0519112
 -0.17590923]
[-0.00390539]

2 模型設計

模型設計是深度學習模型關鍵要素之一,也稱爲網路結構設計,相當於模型的假設空間,即實現模型「前向計算」(從輸入到輸出)的過程。

如果將輸入特徵和輸出預測值均以向量表示,輸入特徵xx有13個分量,yy有1個分量,那麼參數權重的形狀(shape)是13×113\times1。假設我們以如下任意數位賦值參數做初始化:
w=[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.1,0.2,0.3,0.4,0.0]w=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, -0.1, -0.2, -0.3, -0.4, 0.0]

w = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, -0.1, -0.2, -0.3, -0.4, 0.0]
w = np.array(w).reshape([13, 1])

取出第1條樣本數據,觀察樣本的特徵向量與參數向量相乘的結果。

x1=x[0]
t = np.dot(x1, w)
print(t)
print(y[0])
[0.03395597]
[-0.00390539]

完整的線性迴歸公式,還需要初始化偏移量bb,同樣隨意賦初值-0.2。那麼,線性迴歸模型的完整輸出是z=t+bz=t+b,這個從特徵和參數計算輸出值的過程稱爲「前向計算」。

b = -0.2
z = t + b
print(z)
[-0.16604403]

將上述計算預測輸出的過程以「類和物件」的方式來描述,類成員變數有參數wwbb。通過寫一個forward函數(代表「前向計算」)完成上述從特徵和參數到輸出預測值的計算過程,程式碼如下所示。
爲何b給定初值爲0,而w爲非零值。這裏從兩個角度解釋,實際情況下,每個房間特徵至少有一個是對房價有影響否則該問題研究失去意義;數學方面,模型假設是線性方程即 $y=wx + b $ w0w 不爲0

class Network():
    def __init__(self, num_of_weights):
        # 隨機產生w的初始值
        # 爲了保持程式每次執行結果的一致性,
        # 此處設定固定的亂數種子
        np.random.seed(0)
        self.w = np.random.randn(num_of_weights, 1)
        self.b = 0.
               
    def forward(self, x):
        z = np.dot(x, self.w) + self.b
        return z

基於Network類的定義,模型的計算過程如下所示。

net = Network(13)
x1 = x[0]
y1 = y[0]
z = net.forward(x1)
print(z)
print(y1)
[-0.63182506]
[-0.00390539]

3 訓練設定

模型設計完成後,需要通過訓練設定尋找模型的最優值,即通過損失函數來衡量模型的好壞。訓練設定也是深度學習模型關鍵要素之一。

通過模型計算x1x_1表示的影響因素所對應的房價應該是zz, 但實際數據告訴我們房價是yy。這時我們需要有某種指標來衡量預測值zz跟真實值yy之間的差距。對於迴歸問題,最常採用的衡量方法是使用均方誤差作爲評價模型好壞的指標,具體定義如下:

Loss=(yz)2Loss = (y - z)^2

上式中的LossLoss(簡記爲: LL)通常也被稱作損失函數,它是衡量模型好壞的指標。在迴歸問題中均方誤差是一種比較常見的形式,分類問題中通常會採用交叉熵作爲損失函數,在後續的章節中會更詳細的介紹。對一個樣本計算損失函數值的實現如下:

Loss = (y1 - z)*(y1 - z)
print(Loss)
[0.39428312]

因爲計算損失函數時需要把每個樣本的損失函數值都考慮到,所以我們需要對單個樣本的損失函數進行求和,併除以樣本總數NN
Loss=1Ni=1N(yizi)2Loss= \frac{1}{N}\sum_{i=1}^N{(y_i - z_i)^2}
在Network類下面 下麪新增損失函數的計算過程如下:

class Network():
    def __init__(self, num_of_weights):
        # 隨機產生w的初始值
        # 爲了保持程式每次執行結果的一致性,此處設定固定的亂數種子
        np.random.seed(0)
        self.w = np.random.randn(num_of_weights, 1)
        self.b = 0.
        
    def forward(self, x):
        z = np.dot(x, self.w) + self.b
        return z
    
    def loss(self, z, y):
        error = z - y
        cost = error * error
        cost = np.mean(cost)
        return cost

使用定義的Network類,可以方便的計算預測值和損失函數。需要注意的是,類中的變數xx, wwbb, zz, errorerror等均是向量。以變數xx爲例,共有兩個維度,一個代表特徵數量(值爲13),一個代表樣本數量,程式碼如下所示。

net = Network(13)
# 此處可以一次性計算多個樣本的預測值和損失函數
x1 = x[:3]
y1 = y[:3]
# 選取前三個樣本爲例
z = net.forward(x1)
# 樣本x喂入前向計算模型得到預測值
print('predict: ', z)
loss = net.loss(z, y1)
# 通過訓練設定得到損失函數
print('loss:', loss)
predict:  [[-0.63182506]
 [-0.55793096]
 [-1.00062009]]
loss: 0.7229825055441156

4 訓練過程

上述計算過程描述瞭如何構建神經網路,通過神經網路完成預測值和損失函數的計算。接下來介紹如何求解參數wwbb的數值,這個過程也稱爲模型訓練過程。訓練過程是深度學習模型的關鍵要素之一,其目標是讓定義的損失函數LossLoss儘可能的小,也就是說找到一個參數解wwbb使得損失函數取得極小值。

我們先做一個小測試:如 圖5 所示,基於微積分知識,求一條曲線在某個點的斜率等於函數該點的導數值。那麼大家思考下,當處於曲線的極值點時,該點的斜率是多少?


圖5:曲線斜率等於導數值


這個問題並不難回答,處於曲線極值點時的斜率爲0,即函數在極值點處的導數爲0。那麼,讓損失函數取極小值的wwbb應該是下述方程組的解:
Lw=0\frac{\partial{L}}{\partial{w}}=0
Lb=0\frac{\partial{L}}{\partial{b}}=0

將樣本數據(x,y)(x, y)帶入上面的方程組中即可求解出wwbb的值,但是這種方法只對線性迴歸這樣簡單的任務有效。如果模型中含有非線性變換,或者損失函數不是均方差這種簡單的形式,則很難通過上式求解。爲了解決這個問題,下面 下麪我們將引入更加普適的數值求解方法:梯度下降法。

4.1 梯度下降法

在現實中存在大量的函數正向求解容易,反向求解較難,被稱爲單向函數。這種函數在密碼學中有大量的應用,密碼鎖的特點是可以迅速判斷一個金鑰是否是正確的(已知xx,求yy很容易),但是即使獲取到密碼鎖系統,無法破解出正確的金鑰是什麼(已知yy,求xx很難)。

這種情況特別類似於一位想從山峯走到坡谷的盲人,他看不見坡谷在哪(無法逆向求解出LossLoss導數爲0時的參數值),但可以伸腳探索身邊的坡度(當前點的導數值,也稱爲梯度)。那麼,求解Loss函數最小值可以這樣實現:從當前的參數取值,一步步的按照下坡的方向下降,直到走到最低點。這種方法筆者稱它爲「盲人下坡法」。哦不,有個更正式的說法「梯度下降法」。

訓練的關鍵是找到一組(w,b)(w, b),使得損失函數LL取極小值。我們先看一下損失函數LL只隨兩個參數w5w_5w9w_9變化時的簡單情形,啓發下尋解的思路。
L=L(w5,w9)L=L(w_5, w_9)
這裏我們將w0,w1,...,w12w_0, w_1, ..., w_{12}中除w5,w9w_5, w_9之外的參數和bb都固定下來,可以用圖畫出L(w5,w9)L(w_5, w_9)的形式。

net = Network(13)
losses = []
# 只畫出參數w5和w9在區間[-160, 160]的曲線部分,以及包含損失函數的極值
w5 = np.arange(-160.0, 160.0, 1.0)
w9 = np.arange(-160.0, 160.0, 1.0)
losses = np.zeros([len(w5), len(w9)])

# 計算設定區域內每個參數取值所對應的Loss
# 連續給定w5和w9各自321個值即321*321種組合值,並分別計算出各自的損失函數
for i in range(len(w5)):
    for j in range(len(w9)):
        net.w[5] = w5[i]
        net.w[9] = w9[j]
        z = net.forward(x)
        loss = net.loss(z, y)
        losses[i, j] = loss

#使用matplotlib將兩個變數和對應的Loss作3D圖
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = Axes3D(fig)

w5, w9 = np.meshgrid(w5, w9)

ax.plot_surface(w5, w9, losses, rstride=1, cstride=1, cmap='rainbow')
plt.show()

在这里插入图片描述

對於這種簡單情形,我們利用上面的程式,可以在三維空間中畫出損失函數隨參數變化的曲面圖。從圖中可以看出有些區域的函數值明顯比周圍的點小。

需要說明的是:爲什麼這裏我們選擇w5w_5w9w_9來畫圖?這是因爲選擇這兩個參數的時候,可比較直觀的從損失函數的曲面圖上發現極值點的存在。其他參數組合,從圖形上觀測損失函數的極值點不夠直觀。

觀察上述曲線呈現出「圓滑」的坡度,這正是我們選擇以均方誤差作爲損失函數的原因之一。
圖6 呈現了只有一個參數維度時,均方誤差和絕對值誤差(只將每個樣本的誤差累加,不做平方處理)的損失函數曲線圖。


圖6:均方誤差和絕對值誤差損失函數曲線圖


由此可見,均方誤差表現的「圓滑」的坡度有兩個好處:

  • 曲線的最低點是可導的。
  • 越接近最低點,曲線的坡度逐漸放緩,有助於通過當前的梯度來判斷接近最低點的程度(是否逐漸減少步長,以免錯過最低點)。

而這兩個特性絕對值誤差是不具備的,這也是損失函數的設計不僅僅要考慮「合理性」,還要追求「易解性」的原因。

現在我們要找出一組[w5,w9][w_5, w_9]的值,使得損失函數最小,實現梯度下降法的方案如下:

  • 步驟1:隨機的選一組初始值,例如:[w5,w9]=[100.0,100.0][w_5, w_9] = [-100.0, -100.0]
  • 步驟2:選取下一個點[w5,w9][w_5^{'} , w_9^{'}],使得L(w5,w9)<L(w5,w9)L(w_5^{'} , w_9^{'}) < L(w_5, w_9)
  • 步驟3:重複步驟2,直到損失函數幾乎不再下降。

如何選擇[w5,w9][w_5^{'} , w_9^{'}]是至關重要的,第一要保證LL是下降的,第二要使得下降的趨勢儘可能的快。微積分的基礎知識告訴我們,沿着梯度的反方向,是函數值下降最快的方向,如 圖7 所示。簡單理解,函數在某一個點的梯度方向是曲線斜率最大的方向,但梯度方向是向上的,所以下降最快的是梯度的反方向。


圖7:梯度下降方向示意圖


4.2 計算梯度

上面我們講過了損失函數的計算方法,這裏稍微加以改寫。爲了梯度計算更加簡潔,引入因子12\frac{1}{2},定義損失函數如下:

L=12Ni=1N(yizi)2L= \frac{1}{2N}\sum_{i=1}^N{(y_i - z_i)^2}

其中ziz_i是網路對第ii個樣本的預測值,jj表示的是一個樣本中的第jj個特徵:

zi=j=012xijwj+bz_i = \sum_{j=0}^{12}{x_i^{j}\cdot w_j} + b

梯度的定義:

gradient=(Lw0,Lw1,...,Lw12,Lb) gradient=\left( \frac{\partial L}{\partial w_0},\frac{\partial L}{\partial w_1},...,\frac{\partial L}{\partial w_{12}},\frac{\partial L}{\partial b} \right)

可以計算出LLwwbb的偏導數:

Lwj=1Ni=1N(ziyi)ziwj=1Ni=1N(ziyi)xij\frac{\partial{L}}{\partial{w_j}} = \frac{1}{N}\sum_{i=1}^N{(z_i - y_i)\frac{\partial{z_i}}{\partial{w_j}}} = \frac{1}{N}\sum_{i=1}^N{(z_i - y_i)x_i^{j}}

Lb=1Ni=1N(ziyi)zib=1Ni=1N(ziyi)\frac{\partial{L}}{\partial{b}} = \frac{1}{N}\sum_{i=1}^N{(z_i - y_i)\frac{\partial{z_i}}{\partial{b}}} = \frac{1}{N}\sum_{i=1}^N{(z_i - y_i)}

從導數的計算過程可以看出,因子12\frac{1}{2}被消掉了,這是因爲二次函數求導的時候會產生因子22,這也是我們將損失函數改寫的原因。

下面 下麪我們考慮只有一個樣本的情況下,計算梯度:

L=12(yizi)2L= \frac{1}{2}{(y_i - z_i)^2}

z1=x10w0+x11w1+...+x112w12+bz_1 = {x_1^{0}\cdot w_0} + {x_1^{1}\cdot w_1} + ... + {x_1^{12}\cdot w_{12}} + b

可以計算出:

L=12(x10w0+x11w1+...+x112w12+by1)2L= \frac{1}{2}{({x_1^{0}\cdot w_0} + {x_1^{1}\cdot w_1} + ... + {x_1^{12}\cdot w_{12}} + b - y_1)^2}

可以計算出LLwwbb的偏導數:

Lwj=(x10w0+x11w1+...+x112w12+by1)x1j=(z1y1)x1j\frac{\partial{L}}{\partial{w_j}} = ({x_1^{0}\cdot w_0} + {x_1^{1}\cdot w_1} + ... + {x_1^{12}\cdot w_12} + b - y_1)\cdot x_1^{j}=({z_1} - {y_1})\cdot x_1^{j}

Lb=(x10w0+x11w1+...+x112w12+by1)1=(z1y1)\frac{\partial{L}}{\partial{b}} = ({x_1^{0}\cdot w_0} + {x_1^{1}\cdot w_1} + ... + {x_1^{12}\cdot w_{12}} + b - y_1)\cdot 1 = ({z_1} - {y_1})

選取第一個樣本x[0] y[0] 計算梯度

x1 = x[0]
y1 = y[0]
z1 = net.forward(x1)
print('x1 {}, shape {}'.format(x1, x1.shape))
print('y1 {}, shape {}'.format(y1, y1.shape))
print('z1 {}, shape {}'.format(z1, z1.shape))
x1 [-0.02146321  0.03767327 -0.28552309 -0.08663366  0.01289726  0.04634817
  0.00795597 -0.00765794 -0.25172191 -0.11881188 -0.29002528  0.0519112
 -0.17590923], shape (13,)
y1 [-0.00390539], shape (1,)
z1 [-12.05947643], shape (1,)

按上面的公式,當只有一個樣本時,可以計算某個wjw_j,比如w0w_0的梯度。

gradient_w0 = (z1 - y1) * x1[0]
print('gradient_w0 {}'.format(gradient_w0))
gradient_w0 [0.25875126]

同樣我們可以計算w1w_1的梯度。

gradient_w1 = (z1 - y1) * x1[1]
print('gradient_w1 {}'.format(gradient_w1))
gradient_w1 [-0.45417275]

依次計算w2w_2的梯度。

gradient_w2= (z1 - y1) * x1[2]
print('gradient_w1 {}'.format(gradient_w2))
gradient_w1 [3.44214394]

寫一個for回圈即可計算從w0w_0w12w_{12}的所有權重的梯度,但是計算量很大。這裏採用numpy的並行計算。

4.3 使用Numpy進行梯度計算

基於Numpy廣播機制 機製(對向量和矩陣計算如同對1個單一變數計算一樣),可以更快速的實現梯度計算。計算梯度的程式碼中直接用(z1y1)x1(z_1 - y_1) * x_1,得到的是一個13維的向量,每個分量分別代表該維度的梯度。

# 直接對1個樣本進行梯度計算,得到13個梯度值
gradient_w = (z1 - y1) * x1
print('gradient_w_by_sample1 {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))
gradient_w_by_sample1 [ 0.25875126 -0.45417275  3.44214394  1.04441828 -0.15548386 -0.55875363
 -0.09591377  0.09232085  3.03465138  1.43234507  3.49642036 -0.62581917
  2.12068622], gradient.shape (13,)

輸入數據中有多個樣本,每個樣本都對梯度有貢獻。如上程式碼計算了只有第1個樣本時的梯度值,同樣的計算方法也可以計算樣本2和樣本3對梯度的貢獻。

x2 = x[1]
y2 = y[1]
z2 = net.forward(x2)
gradient_w = (z2 - y2) * x2
print('gradient_w_by_sample2 {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))
gradient_w_by_sample2 [ 0.7329239   4.91417754  3.33394253  2.9912385   4.45673435 -0.58146277
 -5.14623287 -2.4894594   7.19011988  7.99471607  0.83100061 -1.79236081
  2.11028056], gradient.shape (13,)
x3 = x[2]
y3 = y[2]
z3 = net.forward(x3)
gradient_w = (z3 - y3) * x3
print('gradient_w_by_sample3 {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))
gradient_w_by_sample3 [ 0.25138584  1.68549775  1.14349809  1.02595515  1.5286008  -1.93302947
  0.4058236  -0.85385157  2.46611579  2.74208162  0.28502219 -0.46695229
  2.39363651], gradient.shape (13,)

可能有的讀者再次想到可以使用for回圈把每個樣本對梯度的貢獻都計算出來,然後再作平均。但是我們不需要這麼做,仍然可以使用Numpy的矩陣操作來簡化運算,如3個樣本的情況。

# 注意這裏是一次取出3個樣本的數據,不是取出第3個樣本
x3samples = x[0:3]
y3samples = y[0:3]
z3samples = net.forward(x3samples)

print('x {}, shape {}'.format(x3samples, x3samples.shape))
print('y {}, shape {}'.format(y3samples, y3samples.shape))
print('z {}, shape {}'.format(z3samples, z3samples.shape))
x [[-0.02146321  0.03767327 -0.28552309 -0.08663366  0.01289726  0.04634817
   0.00795597 -0.00765794 -0.25172191 -0.11881188 -0.29002528  0.0519112
  -0.17590923]
 [-0.02122729 -0.14232673 -0.09655922 -0.08663366 -0.12907805  0.0168406
   0.14904763  0.0721009  -0.20824365 -0.23154675 -0.02406783  0.0519112
  -0.06111894]
 [-0.02122751 -0.14232673 -0.09655922 -0.08663366 -0.12907805  0.1632288
  -0.03426854  0.0721009  -0.20824365 -0.23154675 -0.02406783  0.03943037
  -0.20212336]], shape (3, 13)
y [[-0.00390539]
 [-0.05723872]
 [ 0.23387239]], shape (3, 1)
z [[-12.05947643]
 [-34.58467747]
 [-11.60858134]], shape (3, 1)

上面的x3samples, y3samples, z3samples的第一維大小均爲3,表示有3個樣本。下面 下麪計算這3個樣本對梯度的貢獻。

# 直接計算出3*13個梯度值
gradient_w = (z3samples - y3samples) * x3samples
print('gradient_w {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))
gradient_w [[ 0.25875126 -0.45417275  3.44214394  1.04441828 -0.15548386 -0.55875363
  -0.09591377  0.09232085  3.03465138  1.43234507  3.49642036 -0.62581917
   2.12068622]
 [ 0.7329239   4.91417754  3.33394253  2.9912385   4.45673435 -0.58146277
  -5.14623287 -2.4894594   7.19011988  7.99471607  0.83100061 -1.79236081
   2.11028056]
 [ 0.25138584  1.68549775  1.14349809  1.02595515  1.5286008  -1.93302947
   0.4058236  -0.85385157  2.46611579  2.74208162  0.28502219 -0.46695229
   2.39363651]], gradient.shape (3, 13)

此處可見,計算梯度gradient_w的維度是3×133 \times 13,並且其第1行與上面第1個樣本計算的梯度gradient_w_by_sample1一致,第2行與上面第2個樣本計算的梯度gradient_w_by_sample1一致,第3行與上面第3個樣本計算的梯度gradient_w_by_sample1一致。這裏使用矩陣操作,可能更加方便的對3個樣本分別計算各自對梯度的貢獻。

那麼對於有N個樣本的情形,我們可以直接使用如下方式計算出所有樣本對梯度的貢獻,這就是使用Numpy庫廣播功能帶來的便捷。
小結一下這裏使用Numpy庫的廣播功能:

  • 一方面可以擴充套件參數的維度,代替for回圈來計算1個樣本對從w0 到w12 的所有參數的梯度。
  • 另一方面可以擴充套件樣本的維度,代替for回圈來計算樣本0到樣本403對參數的梯度。
z = net.forward(x)
gradient_w = (z - y) * x
print('gradient_w shape {}'.format(gradient_w.shape))
print(gradient_w)
gradient_w shape (404, 13)
[[  0.25875126  -0.45417275   3.44214394 ...   3.49642036  -0.62581917
    2.12068622]
 [  0.7329239    4.91417754   3.33394253 ...   0.83100061  -1.79236081
    2.11028056]
 [  0.25138584   1.68549775   1.14349809 ...   0.28502219  -0.46695229
    2.39363651]
 ...
 [ 14.70025543 -15.10890735  36.23258734 ...  24.54882966   5.51071122
   26.26098922]
 [  9.29832217 -15.33146159  36.76629344 ...  24.91043398  -1.27564923
   26.61808955]
 [ 19.55115919 -10.8177237   25.94192351 ...  17.5765494    3.94557661
   17.64891012]]

上面gradient_w的每一行代表了一個樣本對梯度的貢獻。根據梯度的計算公式,總梯度是對每個樣本對梯度貢獻的平均值。

Lwj=1Ni=1N(ziyi)ziwj=1Ni=1N(ziyi)xij\frac{\partial{L}}{\partial{w_j}} = \frac{1}{N}\sum_{i=1}^N{(z_i - y_i)\frac{\partial{z_i}}{\partial{w_j}}} = \frac{1}{N}\sum_{i=1}^N{(z_i - y_i)x_i^{j}}

我們也可以使用Numpy的均值函數來完成此過程:

# axis = 0 沿着從上到下的方向進行計算各值 這部分的理解可百度查一下
gradient_w = np.mean(gradient_w, axis=0)
print('gradient_w ', gradient_w.shape)
print('w ', net.w.shape)
print(gradient_w)
print(net.w)

gradient_w  (13,)
w  (13, 1)
[ 1.59697064 -0.92928123  4.72726926  1.65712204  4.96176389  1.18068454
  4.55846519 -3.37770889  9.57465893 10.29870662  1.3900257  -0.30152215
  1.09276043]
[[ 1.76405235e+00]
 [ 4.00157208e-01]
 [ 9.78737984e-01]
 [ 2.24089320e+00]
 [ 1.86755799e+00]
 [ 1.59000000e+02]
 [ 9.50088418e-01]
 [-1.51357208e-01]
 [-1.03218852e-01]
 [ 1.59000000e+02]
 [ 1.44043571e-01]
 [ 1.45427351e+00]
 [ 7.61037725e-01]]

我們使用Numpy的矩陣操作方便地完成了gradient的計算,但引入了一個問題,gradient_w的形狀是(13,),而w的維度是(13, 1)。導致該問題的原因是使用np.mean函數時消除了第0維。爲了加減乘除等計算方便,gradient_w和w必須保持一致的形狀。因此我們將gradient_w的維度也設定爲(13, 1),程式碼如下:

# newaxis 在相應位置增加一個維度 詳見 https://m.jb51.net/article/175474.htm
# 如果反覆 反復執行此程式碼,(13,1,1,1...)
gradient_w = gradient_w[:, np.newaxis]
print('gradient_w shape', gradient_w.shape)
gradient_w shape (13, 1)

綜合上面的討論,計算梯度的程式碼如下所示。

z = net.forward(x)
gradient_w = (z - y) * x
gradient_w = np.mean(gradient_w, axis=0)
gradient_w = gradient_w[:, np.newaxis]
gradient_w
array([[ 1.59697064],
       [-0.92928123],
       [ 4.72726926],
       [ 1.65712204],
       [ 4.96176389],
       [ 1.18068454],
       [ 4.55846519],
       [-3.37770889],
       [ 9.57465893],
       [10.29870662],
       [ 1.3900257 ],
       [-0.30152215],
       [ 1.09276043]])

上述程式碼非常簡潔地完成了ww的梯度計算。同樣,計算bb的梯度的程式碼也是類似的原理。

gradient_b = (z - y)
gradient_b = np.mean(gradient_b)
# 此處b是一個數值,所以可以直接用np.mean得到一個標量
gradient_b
-1.0918438870293816e-13

將上面計算wwbb的梯度的過程,寫成Network類的gradient函數,實現方法如下所示。

class Network():
    def __init__(self, num_of_weights):
        # 隨機產生w的初始值
        # 爲了保持程式每次執行結果的一致性,此處設定固定的亂數種子
        np.random.seed(0)
        self.w = np.random.randn(num_of_weights, 1)
        self.b = 0.
        
    def forward(self, x):
        z = np.dot(x, self.w) + self.b
        return z
    
    def loss(self, z, y):
        error = z - y
        num_samples = error.shape[0]
        cost = error * error
        cost = np.sum(cost) / num_samples
        return cost
    
    def gradient(self, x, y):
        z = self.forward(x)
        gradient_w = (z-y)*x
        gradient_w = np.mean(gradient_w, axis=0)
        gradient_w = gradient_w[:, np.newaxis]
        gradient_b = (z - y)
        gradient_b = np.mean(gradient_b)
        
        return gradient_w, gradient_b
# 呼叫上面定義的gradient函數,計算梯度
# 初始化網路
net = Network(13)
# 設定[w5, w9] = [-100., -100.]
net.w[5] = -100.0
net.w[9] = -100.0

z = net.forward(x)
loss = net.loss(z, y)
gradient_w, gradient_b = net.gradient(x, y)
gradient_w5 = gradient_w[5][0]
gradient_w9 = gradient_w[9][0]
print('point {}, loss {}'.format([net.w[5][0], net.w[9][0]], loss))
print('gradient {}'.format([gradient_w5, gradient_w9]))

point [-100.0, -100.0], loss 686.300500817916
gradient [-0.850073323995813, -6.138412364807848]

4.4 確定損失函數更小的點

下面 下麪我們開始研究更新梯度的方法。首先沿着梯度的反方向移動一小步,找到下一個點P1,觀察損失函數的變化。

# 在[w5, w9]平面上,沿着梯度的反方向移動到下一個點P1
# 定義移動步長 eta
eta = 0.1
# 更新參數w5和w9
net.w[5] = net.w[5] - eta * gradient_w5
net.w[9] = net.w[9] - eta * gradient_w9
# 重新計算z和loss
z = net.forward(x)
loss = net.loss(z, y)
gradient_w, gradient_b = net.gradient(x, y)
# 計算得到下一個梯度
gradient_w5 = gradient_w[5][0]
gradient_w9 = gradient_w[9][0]
print('point {}, loss {}'.format([net.w[5][0], net.w[9][0]], loss))
print('gradient {}'.format([gradient_w5, gradient_w9]))
point [-99.91499266760042, -99.38615876351922], loss 678.6472185028844
gradient [-0.855635617864529, -6.093226863406581]

執行上面的程式碼,可以發現沿着梯度反方向走一小步,下一個點的損失函數的確減少了。感興趣的話,大家可以嘗試不停的點選上面的程式碼塊,觀察損失函數是否一直在變小。

在上述程式碼中,每次更新參數使用的語句:
net.w[5] = net.w[5] - eta * gradient_w5

  • 相減:參數需要向梯度的反方向移動。
  • eta:控制每次參數值沿着梯度反方向變動的大小,即每次移動的步長,又稱爲學習率。

大家可以思考下,爲什麼之前我們要做輸入特徵的歸一化,保持尺度一致?這是爲了讓統一的步長更加合適。

圖8 所示,特徵輸入歸一化後,不同參數輸出的Loss是一個比較規整的曲線,學習率可以設定成統一的值 ;特徵輸入未歸一化時,不同特徵對應的參數所需的步長不一致,尺度較大的參數需要大步長,尺寸較小的參數需要小步長,導致無法設定統一的學習率。


圖8:未歸一化的特徵,會導致不同特徵維度的理想步長不同


4.5 程式碼封裝Train函數

將上面的回圈計算過程封裝在train和update函數中,實現方法如下所示。

class Network(object):
    def __init__(self, num_of_weights):
        # 隨機產生w的初始值
        # 爲了保持程式每次執行結果的一致性,此處設定固定的亂數種子
        np.random.seed(0)
        self.w = np.random.randn(num_of_weights,1)
        self.w[5] = -100.
        self.w[9] = -100.
        self.b = 0.
        
    def forward(self, x):
        z = np.dot(x, self.w) + self.b
        return z
    
    def loss(self, z, y):
        error = z - y
        num_samples = error.shape[0]
        cost = error * error
        cost = np.sum(cost) / num_samples
        return cost
    
    def gradient(self, x, y):
        z = self.forward(x)
        gradient_w = (z-y)*x
        gradient_w = np.mean(gradient_w, axis=0)
        gradient_w = gradient_w[:, np.newaxis]
        gradient_b = (z - y)
        gradient_b = np.mean(gradient_b)        
        return gradient_w, gradient_b
    
    def update(self, graident_w5, gradient_w9, eta=0.01):
        net.w[5] = net.w[5] - eta * gradient_w5
        net.w[9] = net.w[9] - eta * gradient_w9
        
    def train(self, x, y, iterations=100, eta=0.01):
        points = []
        losses = []
        for i in range(iterations):
            points.append([net.w[5][0], net.w[9][0]])
            z = self.forward(x)
            L = self.loss(z, y)
            gradient_w, gradient_b = self.gradient(x, y)
            gradient_w5 = gradient_w[5][0]
            gradient_w9 = gradient_w[9][0]
            self.update(gradient_w5, gradient_w9, eta)
            losses.append(L)
            if i % 50 == 0:
                print('iter {}, point {}, loss {}'.format(i, [net.w[5][0], net.w[9][0]], L))
        return points, losses

# 獲取數據
train_data, test_data = load_data()
x = train_data[:, :-1]
y = train_data[:, -1:]
# 建立網路
net = Network(13)
num_iterations=2000
# 啓動訓練
points, losses = net.train(x, y, iterations=num_iterations, eta=0.01)

# 畫出損失函數的變化趨勢
plot_x = np.arange(num_iterations)
plot_y = np.array(losses)
plt.plot(plot_x, plot_y)
plt.show()
iter 0, point [-99.99144364382136, -99.93861587635192], loss 686.300500817916
iter 50, point [-99.56362583488914, -96.92631128470325], loss 649.2213468309388
iter 100, point [-99.13580802595692, -94.02279509580971], loss 614.6970095624063
iter 150, point [-98.7079902170247, -91.22404911807594], loss 582.543755023494
iter 200, point [-98.28017240809248, -88.52620357520894], loss 552.5911329872217
iter 250, point [-97.85235459916026, -85.9255316243737], loss 524.6810152322887
iter 300, point [-97.42453679022805, -83.41844407682491], loss 498.6667034691001
iter 350, point [-96.99671898129583, -81.00148431353688], loss 474.4121018974464
iter 400, point [-96.56890117236361, -78.67132338862874], loss 451.7909497114133
iter 450, point [-96.14108336343139, -76.42475531364933], loss 430.6861092067028
iter 500, point [-95.71326555449917, -74.25869251604028], loss 410.988905460488
iter 550, point [-95.28544774556696, -72.17016146534513], loss 392.5985138460825
iter 600, point [-94.85762993663474, -70.15629846096763], loss 375.4213919156372
iter 650, point [-94.42981212770252, -68.21434557551346], loss 359.3707524354014
iter 700, point [-94.0019943187703, -66.34164674796719], loss 344.36607459115214
iter 750, point [-93.57417650983808, -64.53564402117185], loss 330.33265059761464
iter 800, point [-93.14635870090586, -62.793873918279786], loss 317.2011651461846
iter 850, point [-92.71854089197365, -61.11396395304264], loss 304.907305311265
iter 900, point [-92.29072308304143, -59.49362926899678], loss 293.3913987080144
iter 950, point [-91.86290527410921, -57.930669402782904], loss 282.5980778542974
iter 1000, point [-91.43508746517699, -56.4229651670156], loss 272.47596883802515
iter 1050, point [-91.00726965624477, -54.968475648286564], loss 262.9774025287022
iter 1100, point [-90.57945184731255, -53.56523531604897], loss 254.05814669965383
iter 1150, point [-90.15163403838034, -52.21135123828792], loss 245.6771575458149
iter 1200, point [-89.72381622944812, -50.90500040003218], loss 237.796349191773
iter 1250, point [-89.2959984205159, -49.6444271209092], loss 230.3803798866218
iter 1300, point [-88.86818061158368, -48.42794056808474], loss 223.39645367664923
iter 1350, point [-88.44036280265146, -47.2539123610643], loss 216.81413643451378
iter 1400, point [-88.01254499371925, -46.12077426496303], loss 210.60518520483126
iter 1450, point [-87.58472718478703, -45.027015968976976], loss 204.74338990147896
iter 1500, point [-87.15690937585481, -43.9711829469081], loss 199.20442646183585
iter 1550, point [-86.72909156692259, -42.95187439671279], loss 193.96572062803054
iter 1600, point [-86.30127375799037, -41.96774125615467], loss 189.00632158541163
iter 1650, point [-85.87345594905815, -41.017484291751295], loss 184.30678474424633
iter 1700, point [-85.44563814012594, -40.0998522583068], loss 179.84906300239203
iter 1750, point [-85.01782033119372, -39.21364012642417], loss 175.61640587468244
iter 1800, point [-84.5900025222615, -38.35768737548557], loss 171.59326591927962
iter 1850, point [-84.16218471332928, -37.530876349682856], loss 167.76521193253296
iter 1900, point [-83.73436690439706, -36.73213067476985], loss 164.11884842217904
iter 1950, point [-83.30654909546485, -35.96041373329276], loss 160.64174090423475

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SkYFP5hd-1597208354737)(output_67_1.png)]

4.6 訓練擴充套件到全部參數

爲了能給讀者直觀的感受,上面演示的梯度下降的過程僅包含w5w_5w9w_9兩個參數,但房價預測的完整模型,必須要對所有參數wwbb進行求解。這需要將Network中的update和train函數進行修改。由於不再限定參與計算的參數(所有參數均參與計算),修改之後的程式碼反而更加簡潔。實現邏輯:「前向計算輸出、根據輸出和真實值計算Loss、基於Loss和輸入計算梯度、根據梯度更新參數值」四個部分反覆 反復執行,直到到達參數最優點。具體程式碼如下所示。

class Network(object):
    def __init__(self, num_of_weights):
        # 隨機產生w的初始值
        # 爲了保持程式每次執行結果的一致性,此處設定固定的亂數種子
        np.random.seed(0)
        self.w = np.random.randn(num_of_weights, 1)
        self.b = 0.
        
    def forward(self, x):
        z = np.dot(x, self.w) + self.b
        return z
    
    def loss(self, z, y):
        error = z - y
        num_samples = error.shape[0]
        cost = error * error
        cost = np.sum(cost) / num_samples
        return cost
    
    def gradient(self, x, y):
        z = self.forward(x)
        gradient_w = (z-y)*x
        gradient_w = np.mean(gradient_w, axis=0)
        gradient_w = gradient_w[:, np.newaxis]
        gradient_b = (z - y)
        gradient_b = np.mean(gradient_b)        
        return gradient_w, gradient_b
    
    def update(self, gradient_w, gradient_b, eta = 0.01):
        self.w = self.w - eta * gradient_w
        self.b = self.b - eta * gradient_b
        
    def train(self, x, y, iterations=100, eta=0.01):
        losses = []
        for i in range(iterations):
            z = self.forward(x)
            L = self.loss(z, y)
            gradient_w, gradient_b = self.gradient(x, y)
            self.update(gradient_w, gradient_b, eta)
            losses.append(L)
            if (i+1) % 10 == 0:
                print('iter {}, loss {}'.format(i, L))
        return losses

# 獲取數據
train_data, test_data = load_data()
x = train_data[:, :-1]
y = train_data[:, -1:]
# 建立網路
net = Network(13)
num_iterations=1000
# 啓動訓練
losses = net.train(x,y, iterations=num_iterations, eta=0.01)

# 畫出損失函數的變化趨勢
plot_x = np.arange(num_iterations)
plot_y = np.array(losses)
plt.plot(plot_x, plot_y)
plt.show()
iter 9, loss 1.898494731457622
iter 19, loss 1.8031783384598723
iter 29, loss 1.7135517565541092
iter 39, loss 1.6292649416831266
iter 49, loss 1.5499895293373234
iter 59, loss 1.4754174896452612
iter 69, loss 1.4052598659324693
iter 79, loss 1.3392455915676866
iter 89, loss 1.2771203802372915
iter 99, loss 1.218645685090292
iter 109, loss 1.1635977224791534
iter 119, loss 1.111766556287068
iter 129, loss 1.0629552390811503
iter 139, loss 1.0169790065644477
iter 149, loss 0.9736645220185994
iter 159, loss 0.9328491676343147
iter 169, loss 0.8943803798194311
iter 179, loss 0.8581150257549611
iter 189, loss 0.8239188186389671
iter 199, loss 0.7916657692169988
iter 209, loss 0.761237671346902
iter 219, loss 0.7325236194855752
iter 229, loss 0.7054195561163928
iter 239, loss 0.6798278472589763
iter 249, loss 0.6556568843183528
iter 259, loss 0.6328207106387195
iter 269, loss 0.6112386712285091
iter 279, loss 0.59083508421862
iter 289, loss 0.5715389327049418
iter 299, loss 0.5532835757100347
iter 309, loss 0.5360064770773407
iter 319, loss 0.5196489511849665
iter 329, loss 0.5041559244351539
iter 339, loss 0.48947571154034963
iter 349, loss 0.47555980568755696
iter 359, loss 0.46236268171965056
iter 369, loss 0.44984161152579916
iter 379, loss 0.43795649088328303
iter 389, loss 0.42666967704002257
iter 399, loss 0.41594583637124666
iter 409, loss 0.4057518014851036
iter 419, loss 0.3960564371908221
iter 429, loss 0.38683051477942226
iter 439, loss 0.3780465941011246
iter 449, loss 0.3696789129556087
iter 459, loss 0.36170328334131785
iter 469, loss 0.3540969941381648
iter 479, loss 0.3468387198244131
iter 489, loss 0.3399084348532937
iter 499, loss 0.33328733333814486
iter 509, loss 0.32695775371667785
iter 519, loss 0.32090310808539985
iter 529, loss 0.31510781591441284
iter 539, loss 0.30955724187078903
iter 549, loss 0.3042376374955925
iter 559, loss 0.29913608649543905
iter 569, loss 0.29424045342432864
iter 579, loss 0.2895393355454012
iter 589, loss 0.28502201767532415
iter 599, loss 0.28067842982626157
iter 609, loss 0.27649910747186535
iter 619, loss 0.2724751542744919
iter 629, loss 0.2685982071209627
iter 639, loss 0.26486040332365085
iter 649, loss 0.2612543498525749
iter 659, loss 0.2577730944725093
iter 669, loss 0.2544100986669443
iter 679, loss 0.2511592122380609
iter 689, loss 0.2480146494787638
iter 699, loss 0.24497096681926714
iter 709, loss 0.2420230418567801
iter 719, loss 0.23916605368251415
iter 729, loss 0.23639546442555456
iter 739, loss 0.23370700193813698
iter 749, loss 0.23109664355154746
iter 759, loss 0.2285606008362593
iter 769, loss 0.22609530530403904
iter 779, loss 0.2236973949936189
iter 789, loss 0.22136370188515428
iter 799, loss 0.21909124009208833
iter 809, loss 0.21687719478222933
iter 819, loss 0.21471891178284028
iter 829, loss 0.21261388782734392
iter 839, loss 0.2105597614038757
iter 849, loss 0.20855430416838638
iter 859, loss 0.20659541288730932
iter 869, loss 0.20468110187697833
iter 879, loss 0.2028094959090178
iter 889, loss 0.20097882355283644
iter 899, loss 0.19918741092814593
iter 909, loss 0.1974336758421087
iter 919, loss 0.1957161222872899
iter 929, loss 0.19403333527807176
iter 939, loss 0.19238397600456975
iter 949, loss 0.19076677728439415
iter 959, loss 0.18918053929381623
iter 969, loss 0.18762412556104593
iter 979, loss 0.18609645920539716
iter 989, loss 0.18459651940712488
iter 999, loss 0.18312333809366155

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WPZna3DJ-1597208354738)(output_69_1.png)]

4.7 隨機梯度下降法( Stochastic Gradient Descent)

在上述程式中,每次損失函數和梯度計算都是基於數據集中的全量數據。對於波士頓房價預測任務數據集而言,樣本數比較少,只有404個。但在實際問題中,數據集往往非常大,如果每次都使用全量數據進行計算,效率非常低,通俗地說就是「殺雞焉用牛刀」。由於參數每次只沿着梯度反方向更新一點點,因此方向並不需要那麼精確。一個合理的解決方案是每次從總的數據集中隨機抽取出小部分數據來代表整體,基於這部分數據計算梯度和損失來更新參數,這種方法被稱作隨機梯度下降法(Stochastic Gradient Descent,SGD),核心概念如下:

  • min-batch:每次迭代時抽取出來的一批數據被稱爲一個min-batch。
  • batch_size:一個mini-batch所包含的樣本數目稱爲batch_size。
  • epoch:當程式迭代的時候,按mini-batch逐漸抽取出樣本,當把整個數據集都遍歷到了的時候,則完成了一輪訓練,也叫一個epoch。啓動訓練時,可以將訓練的輪數num_epochs和batch_size作爲參數傳入。

下面 下麪結合程式介紹具體的實現過程,涉及到數據處理和訓練過程兩部分程式碼的修改。

數據處理程式碼修改

數據處理需要實現拆分數據批次和樣本亂序(爲了實現隨機抽樣的效果)兩個功能。

# 獲取數據
train_data, test_data = load_data()
train_data.shape
(404, 14)

train_data中一共包含404條數據,如果batch_size=10,即取前0-9號樣本作爲第一個mini-batch,命名train_data1。

train_data1 = train_data[0:10]
train_data1.shape
(10, 14)

使用train_data1的數據(0-9號樣本)計算梯度並更新網路參數。

net = Network(13)
x = train_data1[:, :-1]
y = train_data1[:, -1:]
loss = net.train(x, y, iterations=1, eta=0.01)
loss
[0.9001866101467376]

再取出10-19號樣本作爲第二個mini-batch,計算梯度並更新網路參數。

train_data2 = train_data[10:19]
x = train_data1[:, :-1]
y = train_data1[:, -1:]
loss = net.train(x, y, iterations=1, eta=0.01)
loss
[0.8903272433979659]

按此方法不斷的取出新的mini-batch,並逐漸更新網路參數。

接下來,將train_data分成大小爲batch_size的多個mini_batch,如下程式碼所示:將train_data分成 40410+1=41\frac{404}{10} + 1 = 41 個 mini_batch了,其中前40個mini_batch,每個均含有10個樣本,最後一個mini_batch只含有4個樣本。

batch_size = 10
n = len(train_data)
mini_batches = [train_data[k:k+batch_size] for k in range(0, n, batch_size)]
print('total number of mini_batches is ', len(mini_batches))
print('first mini_batch shape ', mini_batches[0].shape)
print('last mini_batch shape ', mini_batches[-1].shape)
total number of mini_batches is  41
first mini_batch shape  (10, 14)
last mini_batch shape  (4, 14)

另外,我們這裏是按順序取出mini_batch的,而SGD裏面是隨機抽取一部分樣本代表總體。爲了實現隨機抽樣的效果,我們先將train_data裏面的樣本順序隨機打亂,然後再抽取mini_batch。隨機打亂樣本順序,需要用到np.random.shuffle函數,下面 下麪先介紹它的用法。


說明:

通過大量實驗發現,模型對最後出現的數據印象更加深刻。訓練數據匯入後,越接近模型訓練結束,最後幾個批次數據對模型參數的影響越大。爲了避免模型記憶影響訓練效果,需要進行樣本亂序操作。


# 新建一個array
a = np.array([1,2,3,4,5,6,7,8,9,10,11,12])
print('before shuffle', a)
np.random.shuffle(a)
print('after shuffle', a)
before shuffle [ 1  2  3  4  5  6  7  8  9 10 11 12]
after shuffle [ 7  2 11  3  8  6 12  1  4  5 10  9]

多次執行上面的程式碼,可以發現每次執行shuffle函數後的數位順序均不同。
上面舉的是一個1維陣列亂序的案例,我們再觀察下2維陣列亂序後的效果。

# 新建一個array
a = np.array([1,2,3,4,5,6,7,8,9,10,11,12])
a = a.reshape([6, 2])
print('before shuffle\n', a)
np.random.shuffle(a)
print('after shuffle\n', a)
before shuffle
 [[ 1  2]
 [ 3  4]
 [ 5  6]
 [ 7  8]
 [ 9 10]
 [11 12]]
after shuffle
 [[ 1  2]
 [ 3  4]
 [ 5  6]
 [ 9 10]
 [11 12]
 [ 7  8]]

觀察執行結果可發現,(6,2)陣列的元素在第0維被隨機打亂即6個數組順序打亂,但第1維的數位順序保持不變。例如數位2仍然緊挨在數位1的後面,數位8仍然緊挨在數位7的後面,而第二維的[3, 4]並不排在[1, 2]的後面。將這部分實現SGD演算法的程式碼整合到Network類中的train函數中,最終的完整程式碼如下。

# 獲取數據
train_data, test_data = load_data()

# 打亂樣本順序
np.random.shuffle(train_data)

# 將train_data分成多個mini_batch
batch_size = 10
n = len(train_data)
mini_batches = [train_data[k:k+batch_size] for k in range(0, n, batch_size)]

# 建立網路
net = Network(13)

# 依次使用每個mini_batch的數據
for mini_batch in mini_batches:
    x = mini_batch[:, :-1]
    y = mini_batch[:, -1:]
    loss = net.train(x, y, iterations=1)

訓練過程程式碼修改

將每個隨機抽取的mini-batch數據輸入到模型中用於參數訓練。訓練過程的核心是兩層回圈:

  1. 第一層回圈,代表樣本集合要被訓練遍歷幾次,稱爲「epoch」,程式碼如下:

for epoch_id in range(num_epoches):

  1. 第二層回圈,代表每次遍歷時,樣本集合被拆分成的多個批次,需要全部執行訓練,稱爲「iter (iteration)」,程式碼如下:

for iter_id,mini_batch in emumerate(mini_batches):

在兩層回圈的內部是經典的四步訓練流程:前向計算->計算損失->計算梯度->更新參數,這與大家之前所學是一致的,程式碼如下:

            x = mini_batch[:, :-1]
            y = mini_batch[:, -1:]
            a = self.forward(x)  #前向計算
            loss = self.loss(a, y)  #計算損失
            gradient_w, gradient_b = self.gradient(x, y)  #計算梯度
            self.update(gradient_w, gradient_b, eta)  #更新參數

將兩部分改寫的程式碼整合到Network類中的train函數中,最終的實現如下。

import numpy as np

class Network(object):
    def __init__(self, num_of_weights):
        # 隨機產生w的初始值
        # 爲了保持程式每次執行結果的一致性,此處設定固定的亂數種子
        #np.random.seed(0)
        self.w = np.random.randn(num_of_weights, 1)
        self.b = 0.
        
    def forward(self, x):
        z = np.dot(x, self.w) + self.b
        return z
    
    def loss(self, z, y):
        error = z - y
        num_samples = error.shape[0]
        cost = error * error
        cost = np.sum(cost) / num_samples
        return cost
    
    def gradient(self, x, y):
        z = self.forward(x)
        N = x.shape[0]
        gradient_w = 1. / N * np.sum((z-y) * x, axis=0)
        gradient_w = gradient_w[:, np.newaxis]
        gradient_b = 1. / N * np.sum(z-y)
        return gradient_w, gradient_b
    
    def update(self, gradient_w, gradient_b, eta = 0.01):
        self.w = self.w - eta * gradient_w
        self.b = self.b - eta * gradient_b
            
                
    def train(self, training_data, num_epoches, batch_size=10, eta=0.01):
        n = len(training_data)
        losses = []
        for epoch_id in range(num_epoches):
            # 在每輪迭代開始之前,將訓練數據的順序隨機打亂
            # 然後再按每次取batch_size條數據的方式取出
            np.random.shuffle(training_data)
            # 將訓練數據進行拆分,每個mini_batch包含batch_size條的數據
            mini_batches = [training_data[k:k+batch_size] for k in range(0, n, batch_size)]
            for iter_id, mini_batch in enumerate(mini_batches):
                #print(self.w.shape)
                #print(self.b)
                x = mini_batch[:, :-1]
                y = mini_batch[:, -1:]
                a = self.forward(x)
                loss = self.loss(a, y)
                gradient_w, gradient_b = self.gradient(x, y)
                self.update(gradient_w, gradient_b, eta)
                losses.append(loss)
                print('Epoch {:3d} / iter {:3d}, loss = {:.4f}'.
                                 format(epoch_id, iter_id, loss))
        
        return losses

# 獲取數據
train_data, test_data = load_data()

# 建立網路
net = Network(13)
# 啓動訓練
losses = net.train(train_data, num_epoches=5, batch_size=20, eta=0.1)

# 畫出損失函數的變化趨勢
plot_x = np.arange(len(losses))
plot_y = np.array(losses)
plt.plot(plot_x, plot_y)
plt.show()
Epoch   0 / iter   0, loss = 0.2521
Epoch   0 / iter   1, loss = 0.9664
Epoch   0 / iter   2, loss = 0.5541
Epoch   0 / iter   3, loss = 0.2619
Epoch   0 / iter   4, loss = 0.9227
Epoch   0 / iter   5, loss = 0.5416
Epoch   0 / iter   6, loss = 0.4918
Epoch   0 / iter   7, loss = 0.3090
Epoch   0 / iter   8, loss = 0.6816
Epoch   0 / iter   9, loss = 0.2077
Epoch   0 / iter  10, loss = 0.5822
Epoch   0 / iter  11, loss = 0.4233
Epoch   0 / iter  12, loss = 0.4172
Epoch   0 / iter  13, loss = 0.7366
Epoch   0 / iter  14, loss = 0.3486
Epoch   0 / iter  15, loss = 0.4710
Epoch   0 / iter  16, loss = 0.4661
Epoch   0 / iter  17, loss = 0.4091
Epoch   0 / iter  18, loss = 0.3968
Epoch   0 / iter  19, loss = 0.3975
Epoch   0 / iter  20, loss = 0.1120
Epoch   1 / iter   0, loss = 0.5768
Epoch   1 / iter   1, loss = 0.5322
Epoch   1 / iter   2, loss = 0.9350
Epoch   1 / iter   3, loss = 0.2656
Epoch   1 / iter   4, loss = 0.2300
Epoch   1 / iter   5, loss = 0.2850
Epoch   1 / iter   6, loss = 0.3642
Epoch   1 / iter   7, loss = 0.2121
Epoch   1 / iter   8, loss = 0.3077
Epoch   1 / iter   9, loss = 0.5499
Epoch   1 / iter  10, loss = 0.4342
Epoch   1 / iter  11, loss = 0.2141
Epoch   1 / iter  12, loss = 0.1607
Epoch   1 / iter  13, loss = 0.3346
Epoch   1 / iter  14, loss = 0.4893
Epoch   1 / iter  15, loss = 0.4344
Epoch   1 / iter  16, loss = 0.1179
Epoch   1 / iter  17, loss = 0.4322
Epoch   1 / iter  18, loss = 0.1098
Epoch   1 / iter  19, loss = 0.2725
Epoch   1 / iter  20, loss = 0.0806
Epoch   2 / iter   0, loss = 0.7431
Epoch   2 / iter   1, loss = 0.2790
Epoch   2 / iter   2, loss = 0.5033
Epoch   2 / iter   3, loss = 0.3427
Epoch   2 / iter   4, loss = 0.1945
Epoch   2 / iter   5, loss = 0.2371
Epoch   2 / iter   6, loss = 0.2124
Epoch   2 / iter   7, loss = 0.1284
Epoch   2 / iter   8, loss = 0.2467
Epoch   2 / iter   9, loss = 0.3809
Epoch   2 / iter  10, loss = 0.3818
Epoch   2 / iter  11, loss = 0.1488
Epoch   2 / iter  12, loss = 0.2534
Epoch   2 / iter  13, loss = 0.3322
Epoch   2 / iter  14, loss = 0.1377
Epoch   2 / iter  15, loss = 0.1063
Epoch   2 / iter  16, loss = 0.2039
Epoch   2 / iter  17, loss = 0.2299
Epoch   2 / iter  18, loss = 0.2825
Epoch   2 / iter  19, loss = 0.4077
Epoch   2 / iter  20, loss = 0.2074
Epoch   3 / iter   0, loss = 0.0783
Epoch   3 / iter   1, loss = 0.2755
Epoch   3 / iter   2, loss = 0.2339
Epoch   3 / iter   3, loss = 0.1456
Epoch   3 / iter   4, loss = 0.2915
Epoch   3 / iter   5, loss = 0.4859
Epoch   3 / iter   6, loss = 0.2171
Epoch   3 / iter   7, loss = 0.2782
Epoch   3 / iter   8, loss = 0.2043
Epoch   3 / iter   9, loss = 0.4662
Epoch   3 / iter  10, loss = 0.1965
Epoch   3 / iter  11, loss = 0.2081
Epoch   3 / iter  12, loss = 0.2149
Epoch   3 / iter  13, loss = 0.1411
Epoch   3 / iter  14, loss = 0.2372
Epoch   3 / iter  15, loss = 0.2769
Epoch   3 / iter  16, loss = 0.2567
Epoch   3 / iter  17, loss = 0.1392
Epoch   3 / iter  18, loss = 0.2381
Epoch   3 / iter  19, loss = 0.2093
Epoch   3 / iter  20, loss = 0.0887
Epoch   4 / iter   0, loss = 0.1108
Epoch   4 / iter   1, loss = 0.1123
Epoch   4 / iter   2, loss = 0.1902
Epoch   4 / iter   3, loss = 0.2440
Epoch   4 / iter   4, loss = 0.3204
Epoch   4 / iter   5, loss = 0.2762
Epoch   4 / iter   6, loss = 0.1308
Epoch   4 / iter   7, loss = 0.1404
Epoch   4 / iter   8, loss = 0.1268
Epoch   4 / iter   9, loss = 0.3178
Epoch   4 / iter  10, loss = 0.2440
Epoch   4 / iter  11, loss = 0.0672
Epoch   4 / iter  12, loss = 0.2202
Epoch   4 / iter  13, loss = 0.2206
Epoch   4 / iter  14, loss = 0.2242
Epoch   4 / iter  15, loss = 0.2936
Epoch   4 / iter  16, loss = 0.2797
Epoch   4 / iter  17, loss = 0.0963
Epoch   4 / iter  18, loss = 0.2270
Epoch   4 / iter  19, loss = 0.2134
Epoch   4 / iter  20, loss = 0.0458

在这里插入图片描述

觀察上述Loss的變化,隨機梯度下降加快了訓練過程,但由於每次僅基於少量樣本更新參數和計算損失,所以損失下降曲線會出現震盪。


說明:

由於房價預測的數據量過少,所以難以感受到隨機梯度下降帶來的效能提升。

總結

本節我們詳細介紹瞭如何使用Numpy實現梯度下降演算法,構建並訓練了一個簡單的線性模型實現波士頓房價預測,可以總結出,使用神經網路建模房價預測有三個要點:

  • 構建網路,初始化參數w和b,定義預測和損失函數的計算方法。
  • 隨機選擇初始點,建立梯度的計算方法和參數更新方式。
  • 從總的數據集中抽取部分數據作爲一個mini_batch,計算梯度並更新參數,不斷迭代直到損失函數幾乎不再下降。

作業1-1

  1. 樣本歸一化:預測時的樣本數據同樣也需要歸一化,但使用訓練樣本的均值和極值計算,這是爲什麼?

  2. 當部分參數的梯度計算爲0(接近0)時,可能是什麼情況?是否意味着完成訓練?

1.歸一化的作用

在機器學習領域中,不同評價指標(即特徵向量中的不同特徵就是所述的不同評價指標)往往具有不同的量綱和量綱單位,這樣的情況會影響到數據分析的結果,爲了消除指標之間的量綱影響,需要進行數據標準化處理,以解決數據指標之間的可比性。原始數據經過數據標準化處理後,各指標處於同一數量級,適合進行綜合對比評價。其中,最典型的就是數據的歸一化處理。

簡而言之,歸一化的目的就是使得預處理的數據被限定在一定的範圍內(比如[0,1]或者[-1,1]),從而消除奇異樣本數據導致的不良影響。

1)在統計學中,歸一化的具體作用是歸納統一樣本的統計分佈性。歸一化在[0,1]之間是統計的概率分佈,歸一化在[-1,+1]之間是統計的座標分佈。

2)奇異樣本數據是指相對於其他輸入樣本特別大或特別小的樣本向量(即特徵向量),譬如,下面 下麪爲具有兩個特徵的樣本數據x1、x2、x3、x4、x5、x6(特徵向量—>列向量),其中x6這個樣本的兩個特徵相對其他樣本而言相差比較大,因此,x6認爲是奇異樣本數據。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IOpHdCaZ-1597208354743)(attachment:c5b70a0bee41f65cf33c9f038f865aa.png)]

具體參考https://blog.csdn.net/zenghaitao0128/article/details/78361038

2.分佈參數的梯度爲0並不意味着訓練結束,只能說明其在對應的維度上,已達到最小

作業 1-2

  1. 隨機梯度下降的batchsize設定成多少合適?過小有什麼問題?過大有什麼問題?提示:過大以整個樣本集合爲例,過小以單個樣本爲例來思考。
  2. 一次訓練使用的設定:5個epoch,1000個樣本,batchsize=20,最內層回圈執行多少輪?

1.每次只訓練一個樣本,即 Batch_Size = 1。這就是線上學習(Online Learning)。線性神經元在均方誤差代價函數的剖面是一個拋物面,橫截面是橢圓。對於多層神經元、非線性網路,在區域性依然近似是拋物面。使用線上學習,每次修正方向以各自樣本的梯度方向修正,橫衝直撞各自爲政,難以達到收斂。[外連圖片轉存失敗,源站可能有防盜鏈機制 機製,建議將圖片儲存下來直接上傳(img-0278Knoj-1597208354744)(attachment:09efd4057ce83ea6a946e668654cd79.png)]

2.5個epoch即遍歷5次全部樣本。每個minibatch尺寸爲batchsize=20,則有1000/20 = 50個minibatch即遍歷1次樣本迭代50次,則內回圈50 * 5 = 250次

作業1-3

基本知識

1. 求導的鏈式法則

鏈式法則是微積分中的求導法則,用於求一個複合函數的導數,是在微積分的求導運算中一種常用的方法。複合函數的導數將是構成複合這有限個函數在相應點的導數的乘積,就像鎖鏈一樣一環套一環,故稱鏈式法則。如 圖9 所示,如果求最終輸出對內層輸入(第一層)的梯度,等於外層梯度(第二層)乘以本層函數的梯度。


圖9:求導的鏈式法則


2. 計算圖的概念

(1)爲何是反向計算梯度?即梯度是由網路後端向前端計算。當前層的梯度要依據處於網路中後一層的梯度來計算,所以只有先算後一層的梯度才能 纔能計算本層的梯度。

(2)案例:購買蘋果產生消費的計算圖。假設一家商店9折促銷蘋果,每個的單價100元。計算一個顧客總消費的結構如 圖10 所示。


圖10:購買蘋果所產生的消費計算圖


  • 前向計算過程:以黑色箭頭表示,顧客購買了2個蘋果,再加上九折的折扣,一共消費100*2*0.9=180元。
  • 後向傳播過程:以紅色箭頭表示,根據鏈式法則,本層的梯度計算 * 後一層傳遞過來的梯度,所以需從後向前計算。

最後一層的輸出對自身的求導爲1。導數第二層根據 圖11 所示的乘法求導的公式,分別爲0.9*1和200*1。同樣的,第三層爲100 * 0.9=90,2 * 0.9=1.8。


圖11:乘法求導的公式


作業題

  1. 根據 圖12 所示的乘法和加法的導數公式,完成 圖13 購買蘋果和橘子的梯度傳播的題目。

圖12:乘法和加法的導數公式



圖13:購買蘋果和橘子產生消費的計算圖


  1. 挑戰題:用程式碼實現兩層的神經網路的梯度傳播,中間層的尺寸爲13【房價預測案例】(教案當前的版本爲一層的神經網路),如 圖14 所示。

圖14:兩層的神經網路


在这里插入图片描述
2.下節給出