本文以經典的
波士頓房價預測任務
爲例,介紹使用Python語言和Numpy庫來構建神經網絡模型的過程和代碼實現。
波士頓房價預測是一個經典的機器學習任務,波士頓地區的房價是由諸多因素影響的。該數據集統計了13種可能影響房價的因素和該類型房屋的均價,期望構建一個基於13個因素進行房價預測的模型。
波士頓放假影響因素示意圖:
預測問題分爲兩類,該問題屬於迴歸任務。
- 迴歸任務(輸出連續的實數值) √
- 分類任務(輸出離散的標籤)
線性迴歸模型
假設房價
和各影響因素
之間能夠用線性關係來描述:
模型的求解即是通過數據擬合出每個(模型的權重)和b(模型的偏置),一維情況下,兩者分別是直線的斜率和截距。
均方差
作爲損失函數(Loss),以衡量預測房價和真實房價的差異,公式如下:
採用均方差作爲損失函數,即將模型在每個訓練樣本上的預測誤差加和,以此來衡量整體樣本的準確性。
線性迴歸模型的神經網絡結構
線性迴歸模型可以認爲是神經網絡模型的一種極簡特例,是一個只有加權和、沒有非線性變換的神經元(無需形成網絡)。
構建波士頓房價預測任務的神經網絡模型
一、數據處理
數據處理包含五個部分,①數據導入②數據形狀變換③數據集劃分④數據歸一化處理⑤封裝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 = (y1 - z)*(y1 - z)
又因爲計算損失時需要把每個樣本的損失都考慮到,所以需要對單個樣本的損失函數進行求和,併除以樣本總數N。
在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是在不斷下降的。