使用numpy實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)
使用numpy實(shí)現(xiàn)MLP(多層感知機(jī))
手寫神經(jīng)網(wǎng)絡(luò)
手寫反向傳播
手寫梯度下降盲再、隨機(jī)梯度下降
代碼都在https://github.com/wushangbin/tripping/blob/master/Python/MLP_with_numpy.py
看了GitHub代碼氓英,別忘了star哦馅扣!
1 前向傳播
1.1 前向傳播理論
先看前向傳播過(guò)程,還是很簡(jiǎn)單的。
這里:
設(shè)這一層輸出的維度為, 這也是本層的結(jié)點(diǎn)個(gè)數(shù),而上一層輸出維度為
一次性輸入網(wǎng)絡(luò)的樣本個(gè)數(shù)為 (即batch_size的大小)
:上一層的輸出,如果上一層是輸入層孵坚,那就是x. 它的shape:(
,
)
: 權(quán)重矩陣,在訓(xùn)練中調(diào)整窥淆,它的shape:(
,
)
: 偏置卖宠,shape:(
, 1)
: 這一層的輸出(未經(jīng)激活函數(shù)),shape: (
,
)
最后這個(gè)輸出要經(jīng)過(guò)激活函數(shù)變?yōu)?img class="math-inline" src="https://math.jianshu.com/math?formula=o%5E%7B%5Bl%5D%7D" alt="o^{[l]}" mathimg="1">,即:
其中忧饭, 為激活函數(shù)扛伍,可以是sigmoid、tanh词裤、relu等等蜒秤。
1.2 前向傳播代碼
在前向傳播前汁咏,首先要把參數(shù)進(jìn)行隨機(jī)初始化:
import numpy as np
class MLPnet:
def __init__(self, x, y, lr=0.005):
"""
:param x: data
:param y: labels
:param lr: learning rate
yh: predicted labels
"""
self.X = x
self.Y = y
self.yh = np.zeros((1, self.Y.shape[1]))
self.lr = lr
self.dims = [12, 20, 1] # 不同層的結(jié)點(diǎn)個(gè)數(shù)
self.param = {} # 需要訓(xùn)練的參數(shù)
self.ch = {} # 將一些結(jié)果存在這里亚斋,以便在反向傳播時(shí)使用
self.loss = [] # 存放每個(gè)epoch的loss
self.batch_size = 64
def nInit(self):
"""對(duì)神經(jīng)網(wǎng)絡(luò)中的參數(shù)進(jìn)行隨機(jī)初始化"""
np.random.seed(1)
self.param['theta1'] = np.random.randn(self.dims[1], self.dims[0]) / np.sqrt(self.dims[0])
self.param['b1'] = np.zeros((self.dims[1], 1))
self.param['theta2'] = np.random.randn(self.dims[2], self.dims[1]) / np.sqrt(self.dims[1])
self.param['b2'] = np.zeros((self.dims[2], 1))
然后需要注意的是作媚,我們?cè)谇跋騻鞑ブ校枰獙?shí)現(xiàn)激活函數(shù)帅刊,這里纸泡,我們實(shí)現(xiàn)Relu和Tanh:
Relu:
Tanh:
這個(gè)實(shí)現(xiàn)起來(lái)還是很簡(jiǎn)單的:
def Relu(self, u):
return np.maximum(0, u)
def Tanh(self, u):
return (np.exp(u) - np.exp(-u)) / (np.exp(u) + np.exp(-u))
這里兩個(gè)函數(shù)的輸入u實(shí)際上可以是任意維度的
然后就是前向傳播的整體過(guò)程了,實(shí)際上就是把之前的乘法和加法用代碼實(shí)現(xiàn)就好:
def forward(self, x):
if self.param == {}:
self.nInit()
u1 = np.matmul(self.param['theta1'], x) + self.param['b1']
o1 = self.Tanh(u1)
u2 = np.matmul(self.param['theta2'], o1) + self.param['b2']
o2 = self.Relu(u2)
self.ch['X'] = x
self.ch['u1'], self.ch['o1'] = u1, o1
self.ch['u2'], self.ch['o2'] = u2, o2
return o2
要注意赖瞒,這里我們要把進(jìn)行保存女揭,是為了在反向傳播時(shí)使用
2 反向傳播
2.1 損失函數(shù)
在前向傳播之后,模型實(shí)際上就已經(jīng)輸出了一個(gè)預(yù)測(cè)結(jié)果栏饮,當(dāng)然了吧兔,這個(gè)預(yù)測(cè)結(jié)果可能并不好,我們需要根據(jù)預(yù)測(cè)結(jié)果調(diào)整模型中參數(shù)的權(quán)重袍嬉。
所以這里需要一個(gè)損失函數(shù)境蔼,來(lái)評(píng)價(jià)我們的預(yù)測(cè)結(jié)果,損失函數(shù)的值越小伺通,說(shuō)明我們的預(yù)測(cè)結(jié)果與真實(shí)值差距越小箍土,在這里,我們使用的損失函數(shù)是MSE(Mean Square Error)
其中
: 真實(shí)值
: 預(yù)測(cè)值
這里應(yīng)注意罐监,我們y和yh的形狀吴藻,都是2維的,所以在計(jì)算的時(shí)候弓柱,應(yīng)該y[0][i] - yh[0][i]
而且沟堡,因?yàn)槲覀儁的形狀是(1, n) 所以我們不能用len(y)來(lái)表示樣本數(shù)量,要把樣本數(shù)量顯示地提取出來(lái)
代碼:
def nloss(self,y, yh):
"""
:param y: 1*n, 列表
:param yh: 1*N, 列表
:return: 1*1 標(biāo)量
"""
n = y.shape[1]
error = []
squaredError = []
for i in range(n):
error.append(y[0][i] - yh[0][i])
for val in error:
squaredError.append(val * val)
result = sum(squaredError) / (2 * n)
return result
2.2 反向傳播理論
我們?cè)谟?jì)算出loss之后矢空,下一步就是根據(jù)loss調(diào)整模型中的參數(shù)
具體怎么調(diào)整呢航罗?
這個(gè)原理其實(shí)很簡(jiǎn)單:我們的輸入是固定的,像妇多,
這樣的參數(shù)是可訓(xùn)練的(可變的)伤哺,因此我們現(xiàn)在就可以得到一個(gè)這樣的函數(shù),函數(shù)的輸出值者祖,取決于參數(shù):
我們把整個(gè)網(wǎng)絡(luò)看作一個(gè)函數(shù), 其中的所有的參數(shù)表示為
立莉,那么接下來(lái),我們就是要找到這個(gè)函數(shù)的最小值(
的最小值)七问,以及Loss取最小值時(shí)的
的值蜓耻。
在線性回歸中,我們可以直接通過(guò)數(shù)學(xué)方法直接求出最優(yōu)解械巡,但是現(xiàn)在模型復(fù)雜刹淌,我們要采用梯度下降法饶氏。
在梯度下降法中,我們需要知道在取一個(gè)確定值時(shí)有勾,
對(duì)
的導(dǎo)數(shù)值疹启,這個(gè)導(dǎo)數(shù)值即為下降方向,也就是說(shuō)蔼卡,我們把w沿著這個(gè)方向調(diào)整喊崖,w就會(huì)離最小值的點(diǎn)更近。而學(xué)習(xí)率(learning rate)即為我們調(diào)整的幅度雇逞,如果調(diào)整幅度過(guò)大荤懂,可能會(huì)錯(cuò)過(guò)最小值的點(diǎn)。具體到我們模型中的4個(gè)參數(shù)上塘砸,參數(shù)更新公式分別是:
這其中节仿,參數(shù)的值是隨機(jī)初始化好的,是提前設(shè)定好的掉蔬,需要計(jì)算4個(gè)偏導(dǎo)數(shù)的值廊宪。
那么我們?nèi)绾吻筮@4個(gè)偏導(dǎo)數(shù)呢?直接求比較困難眉踱,因?yàn)槲覀儾⒉恢?img class="math-inline" src="https://math.jianshu.com/math?formula=Loss" alt="Loss" mathimg="1">與其中每個(gè)參數(shù)的函數(shù)關(guān)系式挤忙,但是我們知道每一層的函數(shù)(),所以要根據(jù)鏈?zhǔn)椒▌t:
不必被這一長(zhǎng)串公式嚇到谈喳,我們接下來(lái)求出其中等號(hào)右邊的每一項(xiàng)册烈,然后就可以算出等號(hào)左邊的偏導(dǎo)數(shù)了
那么對(duì)于等號(hào)右邊的每一項(xiàng),其實(shí)都是很好求的婿禽,我們舉個(gè)例子:
對(duì)于, 因?yàn)橛?br>
我們這里赏僧, =
,第二層的輸出過(guò)了激活函數(shù)即為模型的輸出結(jié)果扭倾,然后
是真實(shí)值淀零,注意這里沒(méi)有求和,因?yàn)槲疫@里的
和
都是矢量膛壹。那么
就很好求了驾中,它就是:
類似地,和
的關(guān)系模聋,就是一層激活函數(shù)的關(guān)系肩民,所以它們之間的偏導(dǎo)數(shù),就是對(duì)激活函數(shù)求偏導(dǎo)即可.
Tanh的偏導(dǎo):
Relu的偏導(dǎo):
因此链方,類似我們可得:
把這些式子帶入鏈?zhǔn)椒▌t持痰,可求出對(duì)每個(gè)參數(shù)的偏導(dǎo),然后再把偏導(dǎo)帶入參數(shù)更新公式祟蚀,就可以求出新的參數(shù)了工窍。
2.3 反向傳播代碼
我們先把激活函數(shù)的導(dǎo)數(shù)寫好
def dRelu(self, u):
"""
:param u: u of any dimension
:return: dRelu(u) """
u[u<=0] = 0
u[u>0] = 1
return u
def dTanh(self, u):
"""
:param u: u of any dimension
:return: dTanh(u)
"""
o = np.tanh(u)
return 1-o**2
在反向傳播代碼中割卖,有一個(gè)細(xì)節(jié),是需要注意的:
- 在
中患雏,其實(shí)除以n或者不除以n都沒(méi)有很大區(qū)別鹏溯,因?yàn)檫@里說(shuō)到底是影響了學(xué)習(xí)率,不影響梯度下降的方向纵苛,如果你沒(méi)有除以n剿涮,那么就需要在學(xué)習(xí)率上做出調(diào)整;
- 后面每一個(gè)操作其實(shí)有類似情況攻人,比如你矩陣相乘后的結(jié)果,融合了所有樣本的悬槽,那么是否需要除以n怀吻,再比如有對(duì)所有樣本loss求和的操作,求和后是否需要除以n等等初婆。我自己親身實(shí)驗(yàn)了下蓬坡,大部分影響不大,有公式的改動(dòng)影響很大磅叛。這里我統(tǒng)一采用:一旦有合并所有樣本的梯度的情況屑咳,就除以n
- 這里的yh-y不能搞反,不然梯度下降的方向就錯(cuò)了弊琴,會(huì)導(dǎo)致loss一致不下降
- 有的時(shí)候是點(diǎn)乘兆龙,有的時(shí)候是矩陣乘,這一點(diǎn)要注意敲董,時(shí)時(shí)小心紫皇,關(guān)注每一步的維度變化
def backward(self, y, yh):
n = y.shape[1]
dLoss_o2 = (yh - y) / n
dLoss_u2 = dLoss_o2 * self.dRelu(self.ch['o2']) # (1,379)
dLoss_theta2 = np.matmul(dLoss_u2, self.ch['o1'].T) / n
dLoss_b2 = np.sum(dLoss_u2) / n
dLoss_o1 = np.matmul(self.param["theta2"].T, dLoss_u2) # (20*1) mul (1*379)
dLoss_u1 = dLoss_o1 * self.dTanh(self.ch['u1']) # (20*379)
dLoss_theta1 = np.matmul(dLoss_u1, self.X.T) # (20*379) mul (379*13)
dLoss_b1 = np.sum(dLoss_u1, axis=1, keepdims=True) / n
# parameters update:
self.param["theta2"] = self.param["theta2"] - self.lr * dLoss_theta2
self.param["b2"] = self.param["b2"] - self.lr * dLoss_b2
self.param["theta1"] = self.param["theta1"] - self.lr * dLoss_theta1
self.param["b1"] = self.param["b1"] - self.lr * dLoss_b1
return dLoss_theta2, dLoss_b2, dLoss_theta1, dLoss_b1
3 梯度下降
3.1 梯度下降算法
梯度下降這一塊兒,比較簡(jiǎn)單腋寨,現(xiàn)在先說(shuō)最基礎(chǔ)的聪铺,我們把所有數(shù)據(jù)放入模型,跑出結(jié)果萄窜,然后根據(jù)結(jié)果調(diào)用backward函數(shù)铃剔,更新參數(shù)即可,這里我們每一次都跑全部的數(shù)據(jù)查刻,然后設(shè)定一個(gè)參數(shù)iter即為迭代的次數(shù)键兜。
先寫一個(gè)函數(shù),輸入數(shù)據(jù)赖阻,輸出預(yù)測(cè)結(jié)果(其實(shí)就是調(diào)用forward):
def predict(self, x):
yh = self.forward(x)
return yh
然后就是梯度下降:
def gradient_descent(self, x, y, iter=60000):
"""
每次跑全部數(shù)據(jù)蝶押,跑iter次,每2000次存儲(chǔ)一次loss
:param x: data
:param y: labels
:param iter: 迭代次數(shù)
"""
for i in range(iter):
pre_y = self.predict(x)
this_loss = self.nloss(y, pre_y)
self.loss.append(this_loss)
if i % 2000 == 0:
print("Loss after iteration", i, ":", this_loss)
self.backward(y, pre_y)
3.2 批量梯度下降
剛剛的方法火欧,每次都要放全部數(shù)據(jù)棋电,計(jì)算量太大了茎截,現(xiàn)在,我們每次放batch_size個(gè)數(shù)據(jù)赶盔。它的優(yōu)點(diǎn)在于企锌,下降速度更快,每次看少量數(shù)據(jù)就開(kāi)始修改參數(shù)于未,缺點(diǎn)是loss波動(dòng)性大撕攒,因?yàn)樗看螞](méi)有看全部的數(shù)據(jù),很可能這次看了一部分?jǐn)?shù)據(jù)烘浦,改了參數(shù)抖坪,第二次看了另一部分?jǐn)?shù)據(jù),又把參數(shù)改回去了闷叉。因此有一定的波動(dòng)性擦俐。
我們每次選取batch_size個(gè)數(shù)據(jù),進(jìn)行訓(xùn)練握侧,然后再往后選batch_size個(gè)數(shù)據(jù)蚯瞧,這樣就好。
def batch_gradient_descent(self, x, y, iter=60000):
"""
這里的迭代次數(shù)品擎,依然是backward的次數(shù)埋合,并非epoch(epoch是看全部數(shù)據(jù)的次數(shù))
"""
n = y.shape[1]
begin = 0
for k in range(iter):
index_list = [i % n for i in range(begin, begin + self.batch_size)]
x_batch = x[:, index_list]
y_batch = y[:, index_list]
pre_y = self.predict(x_batch)
self.X = x_batch
this_loss = self.nloss(y_batch, pre_y)
if k % 1000 == 0:
self.loss.append(this_loss)
print("Loss after iteration", k, ":", this_loss)
self.backward(y_batch, pre_y)
begin = begin + self.batch_size
4 實(shí)驗(yàn)結(jié)果
4.1 梯度下降
使用boston數(shù)據(jù)集
from sklearn.datasets import load_boston
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
if __name__ == '__main__':
dataset = load_boston() # load the dataset
x, y = dataset.data, dataset.target
y = y.reshape(-1, 1)
x = MinMaxScaler().fit_transform(x) # normalize data
y = MinMaxScaler().fit_transform(y)
x_train, x_test, y_train, y_test = train_test_split(x, y, random_state=1) # split data
x_train, x_test, y_train, y_test = x_train.T, x_test.T, y_train.reshape(1, -1), y_test
nn = MLPnet(x_train, y_train, lr=0.001)
nn.gradient_descent(x_train, y_train, iter=60000) # train
# create figure
fig = plt.plot(np.array(nn.loss).squeeze())
plt.title(f'Training: MLPnet')
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.show()
Loss after iteration 0 : 0.08998720790406557
Loss after iteration 2000 : 0.07518079881255157
Loss after iteration 4000 : 0.04587554948861338
Loss after iteration 6000 : 0.03592926084683294
Loss after iteration 8000 : 0.03037128839345559
Loss after iteration 10000 : 0.02678080951732856
...
Loss after iteration 48000 : 0.010800052957060184
Loss after iteration 50000 : 0.010534368656369569
Loss after iteration 52000 : 0.010286303319926585
Loss after iteration 54000 : 0.010054283077500703
Loss after iteration 56000 : 0.009836923785315307
Loss after iteration 58000 : 0.009633001471168611
Mean Squared Error (MSE) 0.02512249554701871
4.2 批量梯度下降:
把剛剛代碼中的gradient_descent改一下即可:
nn.batch_gradient_descent(x_train, y_train, iter = 60000) #train
實(shí)驗(yàn)結(jié)果:
Loss after iteration 0 : 0.08928835763543144
Loss after iteration 1000 : 0.06964017531640365
Loss after iteration 2000 : 0.08718731719795095
Loss after iteration 3000 : 0.07178114114739374
Loss after iteration 4000 : 0.03668939190963524
...
Loss after iteration 56000 : 0.008814925199710841
Loss after iteration 57000 : 0.006920864366920335
Loss after iteration 58000 : 0.005792226865048128
Loss after iteration 59000 : 0.012028778071099835
Mean Squared Error (MSE) 0.025001722467090818
4.3 實(shí)驗(yàn)結(jié)果分析
在訓(xùn)練中,可以明顯感覺(jué)到
- 隨機(jī)梯度下降更快萄传,耗時(shí)更少(我沒(méi)有計(jì)時(shí)甚颂,但是感覺(jué)很明顯)
- 隨機(jī)梯度下降地波動(dòng)性更大
另外要注意,隨機(jī)梯度下降盲再,我們這里的每個(gè)iter的數(shù)據(jù)量遠(yuǎn)遠(yuǎn)小于梯度下降的全量數(shù)據(jù)
代碼在https://github.com/wushangbin/tripping/blob/master/Python/MLP_with_numpy.py
看完代碼有幫助的話記得star哦西设!