- 注意:這里的代碼都是在Jupyter Notebook中運(yùn)行,原始的
.ipynb文件可以在我的GitHub主頁(yè)上下載 https://github.com/acphart/Hand_on_ML_Algorithm 其中的ANN_StochasticGradientDescent.ipynb就是這篇博客的文件,里面包括代碼、注釋以及交互式運(yùn)行結(jié)果,界面十分友好,讀者可以下載后直接在Jupyter Notebook中打開即可,在這里作者也強(qiáng)烈推薦使用Jupyter Notebook進(jìn)行學(xué)習(xí),這里面還有其他的機(jī)器學(xué)習(xí)算法實(shí)現(xiàn),這個(gè)代碼也在我的另外一個(gè)項(xiàng)目 GitHub:acphart/Deep_in_mnist 中,里面有更多的關(guān)于mnist手寫數(shù)字識(shí)別的代碼,喜歡可以順便給個(gè)star哦 ~~~
介紹
參考文獻(xiàn)
- Michael A. Nielsen, “Neural Networks and Deep Learning”, Determination Press, 2015.
項(xiàng)目介紹
- 這里實(shí)現(xiàn)了一個(gè)可以學(xué)習(xí)mnist手寫數(shù)字特征的人工神經(jīng)網(wǎng)絡(luò)類
ANN。 - 此版本的
ANN基于隨機(jī)梯度下降算法實(shí)現(xiàn),梯度由反向傳播算法計(jì)算,神經(jīng)元激活函數(shù)為sigmoid(),或稱logistic函數(shù)。 - 此項(xiàng)目主要為學(xué)習(xí)之用,在實(shí)現(xiàn)核心功能的前提下,盡量編寫簡(jiǎn)單、易于閱讀的代碼,暫時(shí)沒(méi)有考慮優(yōu)化。
反向傳播算法的公式證明
- 這里花了很多冤枉時(shí)間,原本證明單個(gè)權(quán)重或者偏置的梯度公式不怎么麻煩,但是我吃著沒(méi)事做,硬是啃了兩三天想實(shí)現(xiàn)同時(shí)向網(wǎng)絡(luò)裝入一批訓(xùn)練數(shù)據(jù)并計(jì)算平均梯度,這樣可以利用
numpy對(duì)運(yùn)算加速,結(jié)果最后在證公式、向量化運(yùn)算和debug中跪了 ~~ - 最后其實(shí)還是從上面列出來(lái)的參考書中看到,一次送一個(gè)訓(xùn)練數(shù)據(jù)進(jìn)
ann,只不過(guò)多了一層很小的循環(huán),整個(gè)過(guò)程就簡(jiǎn)潔多了 ~~ - 由于這里篇幅原因,其實(shí)主要是要碼的公式太多。。。,詳細(xì)證明過(guò)程等有空專門寫一篇博客,然后在這里更新鏈接。
下一步計(jì)劃
- 進(jìn)一步調(diào)參
- 加入正則化,嘗試
L1正則和L2正則 - 改變神經(jīng)元激活函數(shù),嘗試雙曲正切函數(shù)
tanh()和修正線性單元ReLU() - 搭建卷積神經(jīng)網(wǎng)絡(luò),使得網(wǎng)絡(luò)可以學(xué)習(xí)圖像結(jié)構(gòu)特征
步驟
1. 導(dǎo)入工具庫(kù)和準(zhǔn)備數(shù)據(jù)
- 這里主要需要借助numpy進(jìn)行向量運(yùn)算
import random
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.core.interactiveshell import InteractiveShell
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
import warnings
warnings.filterwarnings('ignore')
# InteractiveShell.ast_node_interactivity = 'all'
- 數(shù)據(jù)讀入之后轉(zhuǎn)換成np.ndarray對(duì)象
- 這里的all_mnist_data.csv是重新包裝后的所有原始mnist數(shù)據(jù),共70000個(gè)手寫數(shù)字,數(shù)據(jù)詳情及下載請(qǐng)閱讀我GitHub主頁(yè)上的介紹GitHub:acphart/Deep_in_mnist
data = pd.read_csv('../dataset/all_mnist_data.csv').values
2. 定義神經(jīng)網(wǎng)絡(luò)類ANN
ANN目前提供給外部使用的就是以下四個(gè)方法ANN其他方法為內(nèi)部方法調(diào)用所有方法的詳細(xì)文檔見方法定義處
ANN的使用方式如下:
- 構(gòu)建ANN實(shí)例并初始化:
ann = ANN(sizes); - 訓(xùn)練ANN:
ann.SGD(train_data, epochs, batch_size, alpha[, test_data]); - 評(píng)估ANN:
ann.evaluate(test_data); - 預(yù)測(cè):
ann.predict(x)。
class ANN(object):
'''
ANN:人工神經(jīng)網(wǎng)絡(luò)類:
構(gòu)建ANN實(shí)例:ann = ANN(sizes),sizes含義見self__init__();
訓(xùn)練ANN:ann.SGD(train_data, epochs, batch_size, alpha);
評(píng)估ANN:ann.evaluate(test_X, test_y);
預(yù)測(cè):ann.predict(x)。
'''
def __init__(self, sizes):
'''
sizes是ANN的規(guī)模;
num_layers是ANN的層數(shù);
biases是偏置矩陣列表,第l層的biases.shape設(shè)置為(nl, 1), nl為第l層的神經(jīng)元數(shù)目;
weights是權(quán)重矩陣列表,第l層的weights.shape設(shè)置為(nl, nl-1), nl和nl-1分別是第l和l+1層的神經(jīng)元數(shù)目;
如:sizes=[784, 30, 10]就是一個(gè)輸入層、隱藏層、輸出層分別是784,、30、10個(gè)神經(jīng)元的神經(jīng)網(wǎng)絡(luò);
而相應(yīng)的biases為[(30,1), (10,1)],第一層沒(méi)有偏置值;weights為[(30, 784), (10, 30)]
'''
'''
將biases.shape設(shè)置為(nl, 1),而不是(nl,)是為了能夠同時(shí)處理多個(gè)輸入;
使用標(biāo)準(zhǔn)正態(tài)分布對(duì)權(quán)重進(jìn)行初始化,可以給我們的梯度下降算法一個(gè)起點(diǎn);
但這不是最好的初始化方法,可以使用均值為0,標(biāo)準(zhǔn)差為1./sqrt(n)的正態(tài)分布進(jìn)行初始化。
'''
self.num_layers = len(sizes)
self.sizes = sizes
self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
self.weights = [np.random.randn(y, x)
for x, y in zip(sizes[:-1], sizes[1:])]
def feedforward(self, a):
'''
前向傳播,返回神經(jīng)網(wǎng)絡(luò)的輸出;
輸入的a.shape is (n, 1),n是特征維度;
返回的a.shape is (L, 1),L是輸出層神經(jīng)元個(gè)數(shù)。
'''
a = a.reshape(len(a), 1)
for b, w in zip(self.biases, self.weights):
a = sigmoid(np.dot(w, a) + b)
return a
def SGD(self, train_data, epochs, batch_size, alpha,
cv_data=None):
'''
隨機(jī)梯度下降:
train_data是data_wrapper()包裝之后的訓(xùn)練數(shù)據(jù),數(shù)據(jù)格式見data_wrapper()函數(shù)定義處;
epochs為迭代次數(shù);
batch_size為采樣時(shí)的批量數(shù)據(jù)的大小;
alpha是學(xué)習(xí)率;
cv_data為可選參數(shù),是data_wrapper()包裝之后的交叉驗(yàn)證數(shù)據(jù),數(shù)據(jù)格式見data_wrapper()函數(shù)定義處;
若給出了交叉驗(yàn)證數(shù)據(jù),則在每次訓(xùn)練后都會(huì)進(jìn)行性能評(píng)估,可用以跟蹤進(jìn)度,但會(huì)拖慢執(zhí)行速度。
'''
m = len(train_data)
for j in range(1, epochs+1):
'''
在每個(gè)迭代期,首先將數(shù)據(jù)打亂,然后將它分成多個(gè)小批量數(shù)據(jù)batches;
對(duì)于每一個(gè)小批量數(shù)據(jù)batch應(yīng)用一次梯度下降,通過(guò)調(diào)用self.grad_desent()完成。
'''
random.shuffle(train_data)
batches = [train_data[k: k+batch_size]
for k in range(0, m, batch_size)]
for batch in batches:
self.grad_desent(batch, alpha)
if cv_data:
'''若是給出了測(cè)試數(shù)據(jù),則進(jìn)行性能評(píng)估'''
m_cv = len(cv_data)
print("Epoch {0:<3d}: \t{1:>4d} / {2:<5d}".format(
j, self.evaluate(cv_data), m_cv))
else:
'''否則輸出此次迭代完成即可'''
print("Epoch {0} complete".format(j))
def grad_desent(self, batch, alpha):
'''
梯度下降,這里調(diào)用反向傳播函數(shù)self.backprop(),
對(duì)batch中的每一個(gè)樣本計(jì)算梯度,然后適當(dāng)?shù)馗聶?quán)重和偏置。
nabla_b 和nabla_w分別為偏置和權(quán)重的梯度;
delta_nabla_b 和delta_nabla_w分別是單個(gè)訓(xùn)練數(shù)據(jù)(x, y)求得的梯度;
'''
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
'''求得每一個(gè)訓(xùn)練數(shù)據(jù)(x, y)的梯度并累加到nabla_b和nabla_w中'''
for x, y in batch:
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
'''更新神經(jīng)網(wǎng)絡(luò)的權(quán)重和偏置'''
self.weights = [w - (alpha/len(batch))*nw
for w, nw in zip(self.weights, nabla_w)]
self.biases = [b - (alpha/len(batch))*nb
for b, nb in zip(self.biases, nabla_b)]
def backprop(self, x, y):
'''
x.shape is (n,), y.shape is (L,);
反向傳播:計(jì)算梯度。
nabla_b 和nabla_w分別為偏置和權(quán)重的梯度;
activations為神經(jīng)元的激活值向量列表[a1, a2,...an],其中ai代表第i層神經(jīng)元的激活值向量;
zs為神經(jīng)元的輸入權(quán)值向量列表[z2, z3,...zn],其中zi表示第i層神經(jīng)元的輸入權(quán)值向量;
'''
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
'''前向傳播,求得所有的激活值和輸入權(quán)重'''
activation = x
activations = [activation]
zs = []
for b, w in zip(self.biases, self.weights):
z = np.dot(w, activation) + b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
'''反向傳播過(guò)程'''
delta = (y - sigmoid(zs[-1])) * diff_sigm(zs[-1])
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].T)
for layer in range(2, self.num_layers):
z = zs[-layer]
delta = np.dot(self.weights[-layer+1].T, delta)
nabla_b[-layer] = delta
nabla_w[-layer] = np.dot(delta, activations[-layer-1].T)
return (nabla_b, nabla_w)
def evaluate(self, test_data):
'''
評(píng)價(jià)函數(shù),預(yù)測(cè)正確的個(gè)數(shù)。
np.argmax函數(shù)返回?cái)?shù)組的最大值的序號(hào),實(shí)現(xiàn)從one-hot到數(shù)字的轉(zhuǎn)換;
'''
test_results = [(np.argmax(self.feedforward(x)), y)
for (x, y) in test_data]
return sum(int(x == y) for (x, y) in test_results)
def predict(self, x):
'''預(yù)測(cè)函數(shù)'''
return np.argmax(self.feedforward(x))
3. 外部函數(shù)
ANN中會(huì)用到一些外部函數(shù),定義如下:sigmoid(z):神經(jīng)元激活函數(shù)
diff_sigm(z):sigmoid(z)的導(dǎo)數(shù)
vectorize(j):見函數(shù)注釋data_wrapper(data, s_size):數(shù)據(jù)包裝函數(shù)從Kaggle上下載的數(shù)據(jù)源文件格式對(duì)于這里構(gòu)建神經(jīng)網(wǎng)絡(luò)其實(shí)不是很方便,所以將數(shù)據(jù)重新包裝了,使得每一個(gè)數(shù)據(jù)都是
(x, y)元組,其中x為特征向量,y為期望結(jié)果,即真實(shí)值。其中訓(xùn)練數(shù)據(jù)train_data的y還需要進(jìn)行向量化轉(zhuǎn)換成one-hot向量,便于訓(xùn)練ANN。并且因?yàn)樽约簩?shí)現(xiàn)的ANN沒(méi)法使用電腦上的GPU,速度很慢,所以包裝函數(shù)可以指定一個(gè)
s_size參數(shù),選取數(shù)據(jù)的一個(gè)小子集進(jìn)行調(diào)參,同時(shí)根據(jù)子集大小的不同,其中劃分訓(xùn)練集、交叉驗(yàn)證集和測(cè)試集的大小也不同,子集很大時(shí),交叉驗(yàn)證集和測(cè)試集均固定為1000.
def sigmoid(z):
"""激活函數(shù)"""
return 1.0/(1.0+np.exp(-z))
def diff_sigm(z):
"""激活函數(shù)sigmoid()的導(dǎo)數(shù)"""
return -sigmoid(z)*(1-sigmoid(z))
def vectorize(j):
'''
將數(shù)字轉(zhuǎn)換成one-hot向量
如: 2 => [0,0,1,0,0,0,0,0,0,0]
'''
vector = np.zeros((10, 1))
vector[int(j)] = 1.0
return vector
def data_wrapper(data, s_size):
'''
對(duì)數(shù)據(jù)重新包裝:
data是原始的train.csv數(shù)據(jù)的np.ndarray格式;
s_size是抽取的樣本的大小。
返回的是train_data, cv_data, test_data :
train_data為訓(xùn)練集,格式為元組列表[tp0, tp1,...],
tpi = (xi, yi),xi為784維的特征向量,yi為對(duì)應(yīng)數(shù)字的one-hot向量;
cv_data為交叉驗(yàn)證集,格式同訓(xùn)練集,但yi是對(duì)應(yīng)的數(shù)字標(biāo)簽,非one-hot向量;
test_data為測(cè)試集,格式同cv_data。
'''
if s_size<=40000:
train_size = int(s_size*0.6)
cv_size = int(s_size*0.8)
else:
train_size = s_size - 20000
cv_size = s_size - 10000
s_data = data[:s_size, ]
s_X = s_data[:train_size, 1:]
s_y = s_data[:train_size, 0]
s_cv_X = s_data[train_size:cv_size, 1:]
s_cv_y = s_data[train_size:cv_size, 0]
s_test_X = s_data[cv_size: , 1:]
s_test_y = s_data[cv_size: , 0]
train_X = [np.reshape(x, (784, 1)) for x in s_X]
train_y = [vectorize(y) for y in s_y]
train_data = list(zip(train_X, train_y))
cv_X = [np.reshape(x, (784, 1)) for x in s_cv_X]
cv_data = list(zip(cv_X, s_cv_y))
test_X = [np.reshape(x, (784, 1)) for x in s_test_X]
test_data = list(zip(test_X, s_test_y))
return (train_data, cv_data, test_data)
3. 搭建ANN實(shí)例并訓(xùn)練網(wǎng)絡(luò)
在這里體會(huì)到不能用GPU的痛苦了,也體會(huì)到調(diào)參的痛苦了 ~~
先用大一點(diǎn)的學(xué)習(xí)率訓(xùn)練3代,再逐步減小學(xué)習(xí)率,同時(shí)查看測(cè)試結(jié)果
train_data, cv_data, test_data = data_wrapper(data, 70000)
ann = ANN([784, 100, 10])
'''SGD(self, train_data, epochs, batch_size, alpha, cv_data=None)'''
ann.SGD(train_data, 3, 30, 2.0, cv_data)
print('accuracy: ', ann.evaluate(test_data)/len(test_data))
Epoch 1 : 8880 / 10000
Epoch 2 : 9076 / 10000
Epoch 3 : 9124 / 10000
accuracy: 0.909
ann.SGD(train_data, 3, 30, 1.0, cv_data)
print('accuracy: ', ann.evaluate(test_data)/len(test_data))
Epoch 1 : 9212 / 10000
Epoch 2 : 9225 / 10000
Epoch 3 : 9242 / 10000
accuracy: 0.916
ann.SGD(train_data, 3, 30, 0.5, cv_data)
print('accuracy: ', ann.evaluate(test_data)/len(test_data))
Epoch 1 : 9245 / 10000
Epoch 2 : 9247 / 10000
Epoch 3 : 9243 / 10000
accuracy: 0.9175
ann.SGD(train_data, 3, 30, 0.2, cv_data)
print('accuracy: ', ann.evaluate(test_data)/len(test_data))
Epoch 1 : 9259 / 10000
Epoch 2 : 9269 / 10000
Epoch 3 : 9262 / 10000
accuracy: 0.9211
- 調(diào)了幾次參數(shù),發(fā)現(xiàn)這么弄效果還不錯(cuò),我們的準(zhǔn)確率就到0.9211了
查看預(yù)測(cè)效果
def show_pic(ax, image, y_, label=None):
'''
作圖函數(shù):
ax為Matplotlib.Axes對(duì)象;
image為單個(gè)的mnist手寫數(shù)字特征向量,image.shape is (784,);
y_為預(yù)測(cè)值;
label為image對(duì)應(yīng)的真實(shí)數(shù)字標(biāo)簽。
'''
img = image.reshape(28, 28)
ax.imshow(img, cmap='Greys')
ax.axis('off')
ax.text(28, 24, str(y_), color='r', fontsize=18)
if label != None:
ax.text(28, 12, str(label), color='black', fontsize=18)
- 這里把前n * n 個(gè)識(shí)別錯(cuò)誤的手寫數(shù)字顯示出來(lái)
- 每個(gè)手寫數(shù)字右上方黑色數(shù)字是真實(shí)值,右下方紅色數(shù)字是
ann預(yù)測(cè)值 - 結(jié)果顯示,這些錯(cuò)誤識(shí)別的數(shù)字有一部分的確不好辨認(rèn),但大部分我們?nèi)诉€是可以辨認(rèn)出來(lái)的。
- 所以網(wǎng)絡(luò)還有改進(jìn)的空間 ~~
n = 10
fig, ax = plt.subplots(n, n, sharex=True, sharey=True)
ax = ax.flatten()
fig.set_size_inches(15, 10)
ax_id = 0
i = 0
while ax_id < n*n :
x = test_data[i][0]
y = test_data[i][1]
y_ = ann.predict(x)
if y_ != y:
show_pic(ax[ax_id], x, y_, int(y))
ax_id = ax_id + 1
i = i + 1
