使用Python和Numpy構建神經網絡模型


本文以經典的波士頓房價預測任務爲例,介紹使用Python語言和Numpy庫來構建神經網絡模型的過程和代碼實現。
波士頓房價預測是一個經典的機器學習任務,波士頓地區的房價是由諸多因素影響的。該數據集統計了13種可能影響房價的因素和該類型房屋的均價,期望構建一個基於13個因素進行房價預測的模型。

波士頓放假影響因素示意圖:
在這裏插入圖片描述

預測問題分爲兩類,該問題屬於迴歸任務。

  • 迴歸任務(輸出連續的實數值) √
  • 分類任務(輸出離散的標籤)

線性迴歸模型

假設房價各影響因素之間能夠用線性關係來描述:
y=j=1Mxjwj+by=\sum_{j=1}^Mx_jw_j+b

模型的求解即是通過數據擬合出每個wjw_j(模型的權重)和b(模型的偏置),一維情況下,兩者分別是直線的斜率和截距。

均方差作爲損失函數(Loss),以衡量預測房價和真實房價的差異,公式如下:

MSE=1ni=1n(YˉiYj)2+b MSE=\frac{1}{n}\sum_{i=1}^n(\bar{Y}_i-Y_j)^2+b

採用均方差作爲損失函數,即將模型在每個訓練樣本上的預測誤差加和,以此來衡量整體樣本的準確性。

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

線性迴歸模型可以認爲是神經網絡模型的一種極簡特例,是一個只有加權和、沒有非線性變換的神經元(無需形成網絡)。
在這裏插入圖片描述

構建波士頓房價預測任務的神經網絡模型
在這裏插入圖片描述

一、數據處理

數據處理包含五個部分,①數據導入②數據形狀變換③數據集劃分④數據歸一化處理⑤封裝load data函數。只有數據預處理後,才能被模型調用。訓練數據housing.data附在文末。

數據導入
通過如下代碼讀入數據,瞭解下波士頓房價的數據集結構:

# 導入需要用到的package
import numpy as np
import json
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# 讀入訓練數據
datafile = 'housing.data'
data = np.fromfile(datafile, sep=' ')
print(data)
--------------------------------------------------------
輸出結果:
[6.320e-03 1.800e+01 2.310e+00 ... 3.969e+02 7.880e+00 1.190e+01]

數據形狀變換
由於讀入的原始數據是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([data.shape[0] // feature_num, feature_num])
# 查看數據
x = data[0]
print(x.shape)
print(x)
--------------------------------------------------------
輸出結果:
[6.320e-03 1.800e+01 2.310e+00 0.000e+00 5.380e-01 6.575e+00 6.520e+01
 4.090e+00 1.000e+00 2.960e+02 1.530e+01 3.969e+02 4.980e+00 2.400e+01]

數據集劃分
將數據集劃分成訓練集測試集,其中訓練集用於確定模型的參數,測試集用於評判模型的效果。在本案例中,將80%的數據用作訓練集,20%用作測試集,實現代碼如下。

ratio = 0.8
offset = int(data.shape[0] * ratio)
training_data = data[:offset]
print(training_data.shape)
--------------------------------------------------------
輸出結果:
(404, 14)
通過打印結果可知:共有404個樣本,每個樣本含有13個特徵和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]
# 對數據進行歸一化處理
for i in range(feature_num):
    #print(maximums[i], minimums[i], avgs[i])
    data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i])

注意:若訓練時做了歸一化,預測時也需要進行歸一化,需以訓練樣本的均值和極值計算。

封裝成load data函數

將上述幾個數據處理操作封裝成load data函數,以便下一步模型的調用,代碼如下。

# 導入需要用到的package
import numpy as np
import json

def load_data():
    # 從文件導入數據
    datafile = '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]

    # 計算train數據集的最大值,最小值,平均值
    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):
        #print(maximums[i], minimums[i], avgs[i])
        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]

二、模型設計

模型設計也稱爲網絡結構設計,相當於模型的假設空間,即實現模型的前向計算(從輸入到輸出)的過程。如果將輸入特徵x輸出預測值z均以向量表示,輸入特徵有13個分量,輸出預測值有1個分量,那麼參數權重的形狀(shape)是13×1。

以類的方式來做前向計算,生成類的實例,調用其方法來完成前向計算的代碼如下:

1.使用時可以生成多個模型實例。
2.類成員變量有w和b,在類初始化函數中初始化變量。(w隨機初始化,b=0)
3.函數成員forward完成從輸入特徵x輸出z的計算過程。

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
   
net = Network(13)
x1 = x[0]
y1 = y[0]
z = net.forward(x1)
print('z的值爲:',z)
--------------------------------------------------------
輸出結果:
z的值爲: [-0.63182506]

三、訓練配置

模型設計完成後,需要通過訓練配置尋找模型的最優值,即通過損失函數來衡量模型的好壞。對於迴歸問題,最常採用的是使用均方誤差作爲評價模型好壞的指標,具體定義如下:
Loss=(yz)2Loss=(y-z)^2

代碼實現如下:

Loss = (y1 - z)*(y1 - z)

又因爲計算損失時需要把每個樣本的損失都考慮到,所以需要對單個樣本的損失函數進行求和,併除以樣本總數N。
L=1Ni(yizi)2L=\frac{1}{N}\sum_{i}(y_i-z_i)^2

在Network類下面添加損失函數的計算過程如下:

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
        cost = error * error
        cost = np.mean(cost)
        return cost

net = Network(13)
# 此處可以一次性計算多個樣本的預測值和損失函數
x1 = x[0:3]
y1 = y[0:3]
z = net.forward(x1)
print('predict: ', z)
loss = net.loss(z, y1)
print('loss:', loss)
--------------------------------------------------------
輸出結果:
predict:  [[-0.63182506]
 [-0.55793096]
 [-1.00062009]]
loss: 0.7229825055441156

四、訓練過程

前文介紹瞭如何構建神經網絡,通過神經網絡完成預測值和損失函數的計算。接下來介紹如何求解參數w和b的數值,這個過程也稱爲模型訓練過程。訓練過程的目標是:讓定義的損失函數Loss儘可能的小,也就是說找到一個參數解w和b使得損失函數取得極小值。

使用梯度下降法:
在這裏插入圖片描述
具體推導不再給出,直接將計算梯度下降與更新封裝進Network函數中。

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=50, batch_size=100, 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.2177
Epoch   0 / iter   1, loss = 0.2258
Epoch   0 / iter   2, loss = 0.2461
Epoch   0 / iter   3, loss = 0.2737
Epoch   0 / iter   4, loss = 0.0419
Epoch   1 / iter   0, loss = 0.2344
Epoch   1 / iter   1, loss = 0.1875
Epoch   1 / iter   2, loss = 0.2694
Epoch   1 / iter   3, loss = 0.2042
Epoch   1 / iter   4, loss = 0.4452
Epoch   2 / iter   0, loss = 0.1191
....
Epoch  47 / iter   4, loss = 0.0488
Epoch  48 / iter   0, loss = 0.0388
Epoch  48 / iter   1, loss = 0.0413
Epoch  48 / iter   2, loss = 0.0610
Epoch  48 / iter   3, loss = 0.0563
Epoch  48 / iter   4, loss = 0.0092
Epoch  49 / iter   0, loss = 0.0556
Epoch  49 / iter   1, loss = 0.0588
Epoch  49 / iter   2, loss = 0.0334
Epoch  49 / iter   3, loss = 0.0442
Epoch  49 / iter   4, loss = 0.0682

畫出的圖像如下:
在這裏插入圖片描述
觀察Loss的變化,隨機梯度下降加快了訓練過程,但由於每次僅基於少量樣本更新參數和計算損失,所以損失下降曲線會出現震盪。

由於房價預測的數據量過少,所以難以感受到隨機梯度下降帶來的性能提升。不過整體還是可以看到Loss是在不斷下降的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章