深度學(xué)習(xí)第四篇--- Pytorch 線性回歸模型-波士頓房價預(yù)測

波士頓房價預(yù)測是一個經(jīng)典案例,類似于XX語言的Hello World。本文我們學(xué)習(xí)這個案例,體會深度學(xué)習(xí)的過程。波斯頓房價可能受影響的因素一共有下面13個,如下圖所示。

我們期望用這個13個因素構(gòu)建一個模型,實現(xiàn)對房價的預(yù)測。對于預(yù)測問題,根據(jù)預(yù)測值的輸出類型是否連續(xù),分為回歸任務(wù)和和分類任務(wù),因為房價的預(yù)測是一個連續(xù)值,所以房價的預(yù)測是一個回歸任務(wù),因此我們使用線性回歸模型來解決這個問題,則公式如下:


其中w是各個因素的權(quán)重,b是偏置,最終我們經(jīng)過多輪的訓(xùn)練,求解出每個w和每個b的值,能夠擬合所有數(shù)據(jù),那整個項目就成了。為了證明我們擬合的好不好,還需要定義一個損失函數(shù)。


訓(xùn)練集數(shù)據(jù):
https://archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.data

模型的構(gòu)建和訓(xùn)練分成5個部分
● 讀取數(shù)據(jù)集,進(jìn)行一些數(shù)據(jù)預(yù)處理
● 模型的設(shè)計
● 訓(xùn)練的配置,設(shè)定優(yōu)化算法,即梯度下降算法
● 循環(huán)調(diào)用模型的訓(xùn)練過程,主要有前向計算,定義損失函數(shù),反向傳播三個步驟
● 模型的保存、推理

1 數(shù)據(jù)集加載

1.1 讀取

def load_boston_house_data():
   print("加載數(shù)據(jù)")
   data = []
   ff = open("../data/boston_house_data").readlines()  # 將數(shù)據(jù)的每一列都讀取出來
   for item in ff:
       out = re.sub(r"\s{2,}", " ", item).strip()
       print(out)
       data.append(out.split(" "))
   data = np.array(data).astype(np.float64)  # 將數(shù)據(jù)進(jìn)行類型轉(zhuǎn)換為float
   print(data.shape)
   return data


if __name__ == '__main__':
   data = load_boston_house_data()

讀取出來是共計506行,每行14列,其中前13列代表了(表格中影響房價的13特征X,分別對應(yīng) [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE','DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ]),最后一列是房價Y


1.2 訓(xùn)練集和測試集劃分

通過打印訓(xùn)練集的形狀,可以發(fā)現(xiàn)共有506個樣本,每個樣本含有13個特征和1個預(yù)測值。在本案例中,我們將80%的數(shù)據(jù)用作訓(xùn)練集,20%用作測試集,實現(xiàn)代碼如下。506*0.8 = 405

def load_boston_house_data():
   print("加載數(shù)據(jù)")
   data = []
   ff = open("../data/boston_house_data").readlines()  # 將數(shù)據(jù)的每一列都讀取出來
   for item in ff:
       out = re.sub(r"\s{2,}", " ", item).strip()
       # print(out)
       data.append(out.split(" "))
   data = np.array(data).astype(np.float64)  # 將數(shù)據(jù)進(jìn)行類型轉(zhuǎn)換為float
   print('data.shape={},data[0]={},shape={}'.format(data.shape, data[0], data[0].shape))
   # 80%為訓(xùn)練集,20%為測試集
   ratio = 0.8
   offset = int(data.shape[0] * ratio)
   training_data = data[:offset]
   test_data = data[offset:]
   print('training_data.shape={},test_data.shape={}'.format(training_data.shape, test_data.shape))
   return training_data, test_data


if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   x = train_data[:, :-1]
   y = train_data[:, -1:]
   print('x.shape={},x[0]='.format(x.shape), x[0])
   print('y.shape={},y[0]='.format(y.shape), y[0])

將train_data數(shù)據(jù)劃分為特征數(shù)據(jù)x和目標(biāo)數(shù)據(jù)y。
● train_data[:, :-1]表示取train_data數(shù)組的所有行(維度為:),以及所有列除了最后一列(維度為:-1),即取除了最后一列外的所有列。這樣得到的就是特征數(shù)據(jù)x,它是一個二維數(shù)組。
● train_data[:, -1:]表示取train_data數(shù)組的所有行(維度為:),以及只取最后一列(維度為-1:),即只取最后一列的數(shù)據(jù)。這樣得到的就是目標(biāo)數(shù)據(jù)y,它是一個二維數(shù)組。

2 模型設(shè)計

2.1 單樣本理解

我們的公式是:


在不考慮求和的情況下,對于一個單樣本來說,y = wx + b。對于上面樣本x_train_data[0]、y_train_data[0] 如下:
x_train_data[0] [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]
y_train_data[0] 24.0
現(xiàn)在我們只要求出一個w 和 b 使得上面 w
x + b = 24.0就可以了。由于x可以看成1 * 13的矩陣,若滿足矩陣乘法,那么w需要有13行,矩陣的形狀是13*1,我們隨機(jī)生成一個 w如下:

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]) # 改變形狀

然后我們可以設(shè)置b =2,完整的代碼如下,使用公式 z = w *x +b ,計算出t的值

if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   x = train_data[:, :-1]
   y = train_data[:, -1:]
   print('x.shape={},x[0]='.format(x.shape), x[0])
   print('y.shape={},y[0]='.format(y.shape), y[0])

   x0 = x[0]
   print('x[0]={}'.format(x0))
   w0 = [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]
   w0 = np.array(w0).reshape([13, 1])
   print("w={}".format(w0))
   b = 2
   z = np.dot(x[0], w0) + b
   print("w * x0 + t = {}".format(z))

計算出z等于-163,因為我們的w和b是隨機(jī)設(shè)置的,所以和準(zhǔn)確值24相比,差距比較大。根據(jù)以上的代碼,我們定義一個網(wǎng)絡(luò)類,并且在main函數(shù)中調(diào)用。

class Network(object):
   def __init__(self, num_of_weights):
       # 隨機(jī)產(chǎn)生w的初始值 為了保持程序每次運行結(jié)果的一致性,此處設(shè)置固定的隨機(jī)數(shù)種子
       np.random.seed(0)
       self.w = np.random.randn(num_of_weights, 1)
       print("self.w={}".format(self.w))
       self.b = 2

   def forward(self, x):
       z = np.dot(x, self.w) + self.b
       return z
       
if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   x = train_data[:, :-1]
   y = train_data[:, -1:]
   x0 = x[0]
   net = Network(13)
   z = net.forward(x0)
   print("w * x0 + t = {}".format(z))

2.2 定義損失函數(shù)

可以看到我們上面隨機(jī)生成的w,離真實值差距都比較大,這節(jié)我們需要定一個損失函數(shù),來衡量模型的好壞,也就是衡量參數(shù)w和b的好壞。我們的損失函數(shù)如下:



現(xiàn)在先用這個公式計算損失,至于為什么是這樣的函數(shù),后面在說明,現(xiàn)在我們的Net類中添加loss函數(shù)

class Network(object):
   def __init__(self, num_of_weights):
       # 隨機(jī)產(chǎn)生w的初始值 為了保持程序每次運行結(jié)果的一致性,此處設(shè)置固定的隨機(jī)數(shù)種子
       np.random.seed(0)
       self.w = np.random.randn(num_of_weights, 1)
       print("self.w={}".format(self.w))
       self.b = 2

   def forward(self, x):
       z = np.dot(x, self.w) + self.b
       return z

   def loss(self, z, y):
       temp_l = (z - y) * (z - y)
       cost = np.mean(temp_l)
       return cost

if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   x = train_data[:, :-1]
   y = train_data[:, -1:]
   x0 = x[0]
   y0 = y[0]
   net = Network(13)
   z = net.forward(x0)
   loss = net.loss(z, y)
   print("w * x0 + t = {},loss = {}".format(z, loss))

3 模型訓(xùn)練

3.1 梯度下降算法含義

模型訓(xùn)練的過程,也就是求解w和b的過程,我們需要求到一個w和b,使用loss最小。對于上面定義的損失函數(shù),我們先看一個一元二次方程,它就一個開口向上的拋物線。我們要求P0處的斜率,就是求x值=x0的倒數(shù)。

我們要求函數(shù)值loss最小,什么時候曲線值最小呢,當(dāng)?shù)箶?shù)為0的時候,值最小。對于上面的損失函數(shù)來說:他們的偏導(dǎo)數(shù)為0即可:


但是想直接求解到w和b的值是比較困難的。在現(xiàn)實中存在大量的函數(shù)正向求解容易,但反向求解較難,被稱為單向函數(shù)。比如有了一個鑰匙X,打開鎖Y很容易,但是給你一把鎖Y,配出他的鑰匙X很難。
因此,有一種方法論被提出來:把拋物線想象成山坡,我們要從P點出發(fā)到山坡的底部。這種情況特別類似于一位想從山峰走到坡谷的盲人,他看不見坡谷在哪(無法逆向求解出Loss導(dǎo)數(shù)為0時的參數(shù)值),但可以伸腳探索身邊的坡度(當(dāng)前點的導(dǎo)數(shù)值,也稱為梯度)。那么,求解Loss函數(shù)最小值可以這樣實現(xiàn):從當(dāng)前的參數(shù)取值,一步步的按照下坡的方向下降,直到走到最低點。這種方法形象的稱它為“盲人下坡法”。更正式的說法叫“梯度下降法”。



我們可以先看了loss只隨著W1和W2兩個參數(shù)變化的過程,其他的W3....W13都被固定下來,用以下代碼畫出loss的變化曲線。

if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   x = train_data[:, :-1]
   y = train_data[:, -1:]
   x0 = x[0]
   y0 = y[0]

   net = Network(13)
   losses = []
   # 只畫出參數(shù)w1和w2在區(qū)間[-160, 160]的曲線部分,以及包含損失函數(shù)的極值
   w1 = np.arange(-160.0, 160.0, 1.0)
   w2 = np.arange(-160.0, 160.0, 1.0)
   losses = np.zeros([len(w1), len(w2)]) # 320*320的全0矩陣
   # 計算設(shè)定區(qū)域內(nèi)每個參數(shù)取值所對應(yīng)的Loss
   for i in range(len(w1)):
       for j in range(len(w2)):
           net.w[1] = w1[i]
           net.w[2] = w2[j]
           z = net.forward(x)
           loss = net.loss(z, y)
           losses[i, j] = loss

   fig = plt.figure()
   ax = Axes3D(fig)

   w1, w2 = np.meshgrid(w1, w2)

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

我們可以看到,在三維的空間中,很直觀的看到有山坡的低點。想著盲人從山坡感覺著地面的走勢,可以走到山底。

3.2 梯度下降算法推導(dǎo)

既然是求損失函數(shù)的偏導(dǎo)數(shù),那么可以定義出來:



考慮只有一個樣本i的情況下:


因為求偏到之后,會有一個2出來,所以這里乘以1/2,方便計算。那么Z真實 = w*x +b代入公式即為:


所以計算出L損失為:



計算偏導(dǎo)數(shù)為:



我們選擇一個樣本,比如w0的梯度來計算
if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   x = train_data[:, :-1]
   y = train_data[:, -1:]
   x0 = x[0]
   y0 = y[0]

   net = Network(13)
   z0 = net.forward(x0)

   gradient_w0 = (z0 - y0) * x0[0]
   print('x0 {}, shape {}'.format(x0, x0.shape))
   print('y0 {}, shape {}'.format(y0, y0.shape))
   print('z0 {}, shape {}'.format(z0, z0.shape))
   print('gradient_w0 {}'.format(gradient_w0))

   # 計算出第一個樣本所有w的梯度
   for index in range(13):
       gradient_w = (z0 - y0) * x0[index]
       print("gradient_w{}={}".format(index, gradient_w))

根據(jù)如上公式我們可以把w1......w13的梯度都計算出來。

3.3 使用NumPy進(jìn)行梯度計算

NumPy中的矩陣是可以直接做運算的,假設(shè)我們要計算第一個樣本所有的梯度。


if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   x = train_data[:, :-1]
   y = train_data[:, -1:]
   x0 = x[0]
   y0 = y[0]

   net = Network(13)
   z0 = net.forward(x0)

   gradient_w = (z0 - y0) * x0
   print('gradient_w {}'.format(gradient_w))

可以看到和上面一個個計算結(jié)果是一樣的,所以計算第一個和第二個樣本的梯度變的很簡單,如下:

if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   x = train_data[:, :-1]
   y = train_data[:, -1:]
   x0 = x[0]
   y0 = y[0]

   net = Network(13)
   z0 = net.forward(x0)

   gradient_w0 = (z0 - y0) * x0
   print('gradient_w0 {}'.format(gradient_w0))

   x1 = x[1]
   y1 = y[1]
   z1 = net.forward(x1)
   gradient_w1 = (z1 - y1) * x1
   print('gradient_w1 {}'.format(gradient_w1))

   x2 = x[2]
   y2 = y[2]
   z2 = net.forward(x2)
   gradient_w2 = (z2 - y2) * x2
   print('gradient_w2 {}'.format(gradient_w2))

上面三個樣本是分別求出梯度的,能不能一起求出呢,也是可以的。

if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   x = train_data[:, :-1]
   y = train_data[:, -1:]
   x0 = x[0]
   y0 = y[0]

   net = Network(13)
   # 注意這里是一次取出3個樣本的數(shù)據(jù),不是取出第3個樣本
   x3samples = x[0:3]
   y3samples = y[0:3]
   z3samples = net.forward(x3samples)
   gradient_w = (z3samples - y3samples) * x3samples
   print("w={}".format(gradient_w))

所以,我們根據(jù)上面的思路,可以求出所有的樣本的梯度:

def gradient(self, x, y):
   z = self.forward(x)
   y = y.reshape((len(y), 1))
   gradient_w = (z - y) * x
   gradient_w = np.mean(gradient_w, axis=0)
   gradient_w = gradient_w[:, np.newaxis]


z = net.forward(x_train)
loss = net.loss_fun(z, y_train)
gradient_w = net.gradient(x_train, y_train)
print('gradient_w {}, shape {}'.format(gradient_w, gradient_w.shape))

考慮到上面gradient_w的每一行代表了一個樣本對梯度的貢獻(xiàn)。根據(jù)梯度的計算公式,總梯度是對每個樣本對梯度貢獻(xiàn)的平均值,我們需要mean處理一下。


   def gradient(self, x, y):
       z = self.forward(x)
       gradient_w = (z - y) * x
       gradient_w = np.mean(gradient_w, axis=0)
       print('gradient_w={},gradient_w shape {}'.format(gradient_w, gradient_w.shape))
       gradient_w = gradient_w[:, np.newaxis]
       print('gradient_w={},gradient_w shape {}'.format(gradient_w, gradient_w.shape))
       return gradient_w

np.mean(gradient_w, axis=0)表示對梯度向量gradient_w按列進(jìn)行求平均值。這樣,將梯度向量從一維數(shù)組變?yōu)橐粋€標(biāo)量值,即得到了平均梯度。

[:, np.newaxis]用于改變數(shù)組的形狀。gradient_w[:, np.newaxis]表示將平均梯度gradient_w轉(zhuǎn)換為一個二維數(shù)組。其中[:, np.newaxis]的作用是在每個元素的維度上增加一個新的維度,從而將一維數(shù)組轉(zhuǎn)換為列向量。

將梯度向量進(jìn)行平均,可以減少梯度的方差,使參數(shù)更新更加穩(wěn)定。而將平均梯度轉(zhuǎn)換為列向量,則是為了與模型參數(shù)的形狀相匹配,從而實現(xiàn)參數(shù)的正確更新。

同時根據(jù)公式,可以把偏執(zhí)b寫出來

gradient_b = (z - y)
gradient_b = np.mean(gradient_b)

3.4 梯度反方向移動

if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   x = train_data[:, :-1]
   y = train_data[:, -1:]
   x0 = x[0]
   y0 = y[0]

   net = Network(13)
   # 設(shè)置[w1, w2] = [-100., -100.]
   net.w[1] = -100.0
   net.w[2] = -100.0

   z = net.forward(x)
   loss = net.loss(z, y)
   gradient_w, gradient_b = net.gradient(x, y)
   gradient_w1 = gradient_w[1][0]
   gradient_w2 = gradient_w[2][0]
   print('第一次 point {}, loss {}'.format([net.w[1][0], net.w[2][0]], loss))
   print('第一次 gradient {}'.format([gradient_w1, gradient_w2]))

   # 在[w1, w2]平面上,沿著梯度的反方向移動到下一個點P1,移動10次
   for index in range(10):
       # 定義移動步長 step_size
       step_size = 0.1
       # 更新參數(shù)w5和w9
       net.w[1] = net.w[1] - step_size * gradient_w1
       net.w[2] = net.w[2] - step_size * gradient_w2
       # 重新計算z
       z = net.forward(x)
       loss = net.loss(z, y)
       gradient_w, gradient_b = net.gradient(x, y)
       gradient_w1 = gradient_w[1][0]
       gradient_w2 = gradient_w[2][0]
       print('point {}, loss {}'.format([net.w[1][0], net.w[2][0]], loss))
       print('gradient {}'.format([gradient_w1, gradient_w2]))

我們看到loss變化的太快了,而且不是呈現(xiàn)下降趨勢,后面的值有稍微變大,畫圖出來就是打勾了,也就是我們俗稱的不收斂,所以我們要把我們step值,即每次移動的步長(學(xué)習(xí)率)調(diào)整的小一些,從0.1調(diào)整成0.001,圖像是符合預(yù)期的


3.5 數(shù)據(jù)歸一化處理

對于上面其實還不行,我們將訓(xùn)練調(diào)整到100次,畫圖圖像。


可以看到 loss值到20輪之后,就下降不下去了,這樣的模型是不行的。我們在數(shù)據(jù)處理的時候,還忽視了重要的一個步驟,就是數(shù)據(jù)特征輸入歸一化后。

def load_boston_house_data():
   print("加載數(shù)據(jù)")
   data = []
   ff = open("../data/boston_house_data").readlines()  # 將數(shù)據(jù)的每一列都讀取出來
   for item in ff:
       out = re.sub(r"\s{2,}", " ", item).strip()
       # print(out)
       data.append(out.split(" "))
   data = np.array(data).astype(np.float64)  # 將數(shù)據(jù)進(jìn)行類型轉(zhuǎn)換為float
   print('data.shape={},data[0]={},shape={}'.format(data.shape, data[0], data[0].shape))
   # 80%為訓(xùn)練集,20%為測試集
   ratio = 0.8
   offset = int(data.shape[0] * ratio)
   training_data = data[:offset]
   # 針對training_data 做歸一化處理
   # 計算訓(xùn)練集的最大值,最小值
   maximums, minimums = training_data.max(axis=0), \
       training_data.min(axis=0)

   # 對數(shù)據(jù)進(jìn)行歸一化處理
   for i in range(13):
       data[:, i] = (data[:, i] - minimums[i]) / (maximums[i] - minimums[i])

   test_data = data[offset:]
   print('training_data.shape={},test_data.shape={}'.format(training_data.shape, test_data.shape))
   return training_data, test_data

歸一化處理常用于預(yù)處理階段,將數(shù)據(jù)縮放到一個固定的范圍內(nèi)。
maximums, minimums = training_data.max(axis=0), training_data.min(axis=0)計算訓(xùn)練集的每個特征列的最大值和最小值。這里使用max(axis=0)和min(axis=0)來分別計算每列的最大值和最小值,得到一個包含每個特征列最大和最小值的一維數(shù)組。

for i in range(13):遍歷數(shù)據(jù)的每個特征列。(data[:, i] - minimums[i]) / (maximums[i] - minimums[i])進(jìn)行歸一化處理。對于每個特征列,將該列中的每個數(shù)據(jù)減去最小值,然后除以最大值和最小值的差。這樣可以將每個數(shù)據(jù)映射到0和1之間的范圍。data[:, i]表示取數(shù)據(jù)矩陣data的第i列。

通過將數(shù)據(jù)進(jìn)行歸一化處理,可以避免特征之間的尺度差異對機(jī)器學(xué)習(xí)算法的影響。不同的特征往往具有不同的取值范圍,而歸一化可以將所有特征縮放到相同的尺度上,使得特征之間具有可比性,并且有助于算法更好地收斂和準(zhǔn)確性能。
我們在看下歸一化之后的圖像,loss值下降的還是挺明顯的。

3.6 將訓(xùn)練擴(kuò)到所有的樣本

class Network(object):
   def __init__(self, num_of_weights):
       # 隨機(jī)產(chǎn)生w的初始值 為了保持程序每次運行結(jié)果的一致性,此處設(shè)置固定的隨機(jī)數(shù)種子
       np.random.seed(0)
       self.w = np.random.randn(num_of_weights, 1)
       self.b = 2

   def forward(self, x):
       z = np.dot(x, self.w) + self.b
       return z

   def loss(self, z, y):
       temp_l = (z - y) * (z - y)
       cost = np.mean(temp_l)
       return cost

   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

   def gradient(self, x, y):
       z = self.forward(x)
       gradient_w = (z - y) * x
       gradient_w = np.mean(gradient_w, axis=0)
       # print('gradient_w={},gradient_w shape {}'.format(gradient_w, gradient_w.shape))
       gradient_w = gradient_w[:, np.newaxis]
       # print('gradient_w={},gradient_w shape {}'.format(gradient_w, gradient_w.shape))
       gradient_b = (z - y)
       gradient_b = np.mean(gradient_b)
       return gradient_w, gradient_b


if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   x = train_data[:, :-1]
   y = train_data[:, -1:]
   # 創(chuàng)建網(wǎng)絡(luò)
   net = Network(13)
   num_iterations = 1000
   # 啟動訓(xùn)練
   losses = net.train(x, y, iterations=num_iterations, eta=0.01)

   # 畫出損失函數(shù)的變化趨勢
   plot_x = np.arange(num_iterations)
   plot_y = np.array(losses)
   plt.plot(plot_x, plot_y)
   plt.show()

loss值變得更低了。

3.7 隨機(jī)梯度下降法

每次損失函數(shù)和梯度計算都是基于數(shù)據(jù)集中的全量數(shù)據(jù)。對于波士頓房價預(yù)測任務(wù)數(shù)據(jù)集而言,樣本數(shù)比較少,只有404個。但在實際問題中,數(shù)據(jù)集往往非常大,如果每次都使用全量數(shù)據(jù)進(jìn)行計算,效率非常低。

一個合理的解決方案是每次從總的數(shù)據(jù)集中隨機(jī)抽取出小部分?jǐn)?shù)據(jù)來代表整體,基于這部分?jǐn)?shù)據(jù)計算梯度和損失來更新參數(shù),這種方法被稱作隨機(jī)梯度下降法(Stochastic Gradient Descent,SGD),核心概念如下:
minibatch:每次迭代時抽取出來的一批數(shù)據(jù)被稱為一個minibatch。
batch size:每個minibatch所包含的樣本數(shù)目稱為batch size。
Epoch:當(dāng)程序迭代的時候,按minibatch逐漸抽取出樣本,當(dāng)把整個數(shù)據(jù)集都遍歷到了的時候,則完成了一輪訓(xùn)練,也叫一個Epoch(輪次)。啟動訓(xùn)練時,可以將訓(xùn)練的輪數(shù)num_epochs和batch_size作為參數(shù)傳入。


if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   print("train_data.shape=", train_data.shape)

   #暫定batch_size = 10
   # 第1個batch
   train_data1 = train_data[0:10]
   # 第2個batch
   train_data2 = train_data[10:20]
   # 第3個batch
   train_data3 = train_data[20:30]

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

按此方法不斷的取出新的minibatch,將train_data分成大小為batch size=10的多個minibatch

if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   print("train_data.shape=", train_data.shape)

   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)


404條數(shù)據(jù),共計41個batch,最后一個batch中數(shù)據(jù)有4條。
通過大量實驗發(fā)現(xiàn),模型對最后出現(xiàn)的數(shù)據(jù)印象更加深刻。訓(xùn)練數(shù)據(jù)導(dǎo)入后,越接近模型訓(xùn)練結(jié)束,最后幾個批次數(shù)據(jù)對模型參數(shù)的影響越大。為了避免模型記憶影響訓(xùn)練效果,需要進(jìn)行樣本亂序操作。

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

全部的訓(xùn)練代碼如下:

class Network(object):
   def __init__(self, num_of_weights):
       # 隨機(jī)產(chǎn)生w的初始值 為了保持程序每次運行結(jié)果的一致性,此處設(shè)置固定的隨機(jī)數(shù)種子
       np.random.seed(0)
       self.w = np.random.randn(num_of_weights, 1)
       self.b = 2

   def forward(self, x):
       z = np.dot(x, self.w) + self.b
       return z

   def loss(self, z, y):
       temp_l = (z - y) * (z - y)
       cost = np.mean(temp_l)
       return cost

   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_epochs, batch_size=10, eta=0.01):
       n = len(training_data)
       losses = []
       # 第一層循環(huán),確定訓(xùn)練多少個epoch
       for epoch_id in range(num_epochs):
           # 在每輪迭代開始之前,將訓(xùn)練數(shù)據(jù)的順序隨機(jī)打亂
           np.random.shuffle(training_data)
           # 將訓(xùn)練數(shù)據(jù)進(jìn)行拆分,每個mini_batch包含batch_size條的數(shù)據(jù)
           mini_batches = [training_data[k:k + batch_size] for k in range(0, n, batch_size)]
           # 第二層循環(huán),每個batch中的多條數(shù)據(jù)送給模型
           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

   def gradient(self, x, y):
       z = self.forward(x)
       gradient_w = (z - y) * x
       gradient_w = np.mean(gradient_w, axis=0)
       # print('gradient_w={},gradient_w shape {}'.format(gradient_w, gradient_w.shape))
       gradient_w = gradient_w[:, np.newaxis]
       # print('gradient_w={},gradient_w shape {}'.format(gradient_w, gradient_w.shape))
       gradient_b = (z - y)
       gradient_b = np.mean(gradient_b)
       return gradient_w, gradient_b


if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   print("train_data.shape=", train_data.shape)

   # 創(chuàng)建網(wǎng)絡(luò)
   net = Network(13)
   # 啟動訓(xùn)練
   losses = net.train(train_data, num_epochs=50, batch_size=100, eta=0.1)

   # 畫出損失函數(shù)的變化趨勢
   plot_x = np.arange(len(losses))
   plot_y = np.array(losses)
   plt.plot(plot_x, plot_y)
   plt.show()

第一層循環(huán),代表樣本要被訓(xùn)練epoch次。第二層的循環(huán),代表每個epoch中的樣本被拆分成多個批次送到網(wǎng)絡(luò)結(jié)構(gòu)中,成為迭代。最后觀察一下結(jié)果??梢钥吹接幸欢ǖ募獯坍a(chǎn)生,但是總體的趨勢是下降的。

使用pytorch訓(xùn)練模型

4.1 訓(xùn)練步驟

使用pytorch訓(xùn)練模型一般有如下關(guān)鍵的4步驟。


首先定義Net類,繼承torch.nn.Module,添加一個線性層

class Net(torch.nn.Module):
   def __init__(self, n_feature, n_output):
       super(Net, self).__init__()
       self.predict = torch.nn.Linear(n_feature, n_output)

   def forward(self, x):
       out = self.predict(x)
       return out

if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   print("train_data.shape=", train_data.shape)

   X_train = train_data[:, :-1]
   Y_train = train_data[:, -1:]
   net = Net(13, 1)
   # loss
   # 采用均方損失
   loss_func = torch.nn.MSELoss()

   # optimizer
   optimizer = torch.optim.SGD(net.parameters(), lr=0.0001)
   losses = []
   for i in range(4000):
       x_data = torch.tensor(X_train, dtype=torch.float32)
       y_data = torch.tensor(Y_train, dtype=torch.float32)
       pred = net.forward(x_data)
       pred = torch.squeeze(pred)
       loss = loss_func(pred, y_data)
       losses.append(loss)

       optimizer.zero_grad()
       loss.backward()
       optimizer.step()

       # 打印迭代次數(shù)和loss的變化
       print("ite:{}, loss_train:{}".format(i, loss))
       print(pred[0:10])
       print(y_data[0:10])
   # 畫出損失函數(shù)的變化趨勢
   plot_x = np.arange(len(losses))
   plot_y = np.array(losses)
   plt.plot(plot_x, plot_y)
   plt.show()

4.2 模型的保存與加載

   torch.save(net, "model.bin")
   net = torch.load("model.bin")

4.3 推理

if __name__ == '__main__':
   train_data, test_data = load_boston_house_data()
   print("train_data.shape=", train_data.shape)
   net = Net(13, 1)
   net.train(train_data)
   torch.save(net, "model.bin")

   # 推理
   X_test = test_data[:, :-1]
   Y_test = test_data[:, -1:]
   net = torch.load("model.bin")
   loss_func = torch.nn.MSELoss()
   # test
   x_data = torch.tensor(X_test, dtype=torch.float32)
   y_data = torch.tensor(Y_test, dtype=torch.float32)
   pred = net.forward(x_data)
   pred = torch.squeeze(pred)
   loss_test = loss_func(pred, y_data) * 0.001
   print("test loss_test:{}".format(loss_test))
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容