1 介绍
本文内容主要包含神经网络(Neural Network)的原理以及代码实现。我看了很多神经网络的实现方法,但全部都是结构固定,扩展性差。本文将实现一种可以热拔插的代码来实现神经网络,无需修改代码,只需修改参数即可搭建不同结构的神经网络。
2 原理及代码
看了很多文章,博主觉得讲原理时配上代码,食用更佳。
误差反向传播是 NN 的难点所在,本文会以一种步骤更为清晰的求导方式,带你理解误差反向传播过程。读别人的代码总是很煎熬,所以我会尽可能在代码中加入详细注释,让渴望学习的你易于理解。
2.1 正向传播
正向传播很简单,不再详细介绍,正向传播的公式如下:
上式是三层结构的一个前向传播公式,相信大家都能看懂,
为了求导时容易理解,下面再定义每层神经元未激活时的输出为 z , 激活后为 a 。即:
在正向传播之前,需要先随机初始化权重 W 及偏置 b 。下面代码中的 layer_list 是要定义的各层神经元个数,比如 [32, 16, 8, 1] 表示1个输入层3个隐藏层的 NN,第一个数32为输入 X 的维度(特征个数),通过 layer_list 即可定义不同的NN的结构。
NN 各层权重可根据各层神经元的个数来定义对应的 shape。代码如下:
def __init__(self, layer_list=[], lr=0.1, epochs=100):
self.lr = lr #学习率
self.layer_list = layer_list #每层神经元个数
self.epochs = epochs #迭代次数
def weight_bias_init(self):
self.W = {} #权重字典,key是层号,value是对应权重矩阵
self.b = {} #偏置字典,key是层号,value是对应偏置矩阵
self.layer_num = len(self.layer_list)-1 #网络层数(权重矩阵的个数,输入层无权重)
for idx in range(self.layer_num): #为每层layer初始化W与b矩阵,每层 W 的shape为(前一层神经元个数,后一层神经元个数)
self.W[idx] = np.random.randn(self.layer_list[idx],
self.layer_list[idx+1]) * 0.01 #正态分布
self.b[idx] = np.random.randn(self.layer_list[idx+1])
有了矩阵及偏置后,对输入 X 进行累乘即可得到输出output,代码如下:
def forward(self, X, y):
self.X = X #将输入X保存为类的属性,可供其他函数使用
self.y = np.array(y).reshape(-1, 1) #更改y的shape,防止运算出错
#记录各层的z与a,反向传播时会用到
self.z = {} #字典,记录每层激活前的输出(z = W*X + b)
self.a = {} #字典,记录每层激活后的输出(a = sigmoid(z))
input = self.X
for idx in range(self.layer_num): #循环向前累乘
self.z[idx] = np.dot(input, self.W[idx]) + self.b[idx] #z = W*X + b
self.a[idx] = self.sigmoid(self.z[idx]) #a = sigmoid(z)
input = self.a[idx] #更新输入
self.output = self.a[self.layer_num-1] #记录最后一层输出
self.loss = -np.mean(self.y * np.log(self.output) +
(1-self.y) * np.log(1-self.output)) #对数损失
此时,就实现了对输入 X 的正向传播,并且记录了各层的输出 z 与 a ,很简单吧!
2.2 误差反传
对于二分类的对数损失函数: (此代码针对二分类任务设计)
需要求L对各个网络层的权重W及偏置b的导数,即:
链式求导之前,先梳理一下要求导的目标位置。
1> 要求导的目标 W 及 b 都包含在
2> 每层的权重比如
3> 前层的输出比如
4> 要对每层的 W 和 b 求导,只需求得每层输出 z 的导数 dz 即可,因为 z=W*X+b, 所以dW=Xdz, db=dz,有了每层的dz,dW与db就很好求了。
所以我们的链式求导, 先不考虑 W 与 b,避免式子复杂。我们只针对每层的未激活的输出 z 进行求导得到 dz,最后再根据每层的 dz 求 dW 与 db。
先对最后一层的输出
得到了
同理,得到迭代格式:
即前一层输出 z 的导数
Tips: 误差反向传播是不是也很简单,不要先想着对W与b进行求导,它们嵌套的太深,求导复杂。换一个角度,只对每层未激活时的输出z进行求导,再根据dz对W与b进行求导,这个问题就变得清晰了。
下面是误差反向传播的代码,通过迭代公式求每一层的梯度:
# sigmoid的一阶导数
def Dsigmoid(self, x):
return self.sigmoid(x) * (1 - self.sigmoid(x))
# 反向传播
def backward(self):
#跟权重保存方式一样,使用字典存储,key为对应的层号
self.dz = {} #对每层z的求导
self.dW = {} #对每层W的求导
self.db = {} #对每层b的求导
idx = self.layer_num - 1 #从后往前求导
while(idx>=0):
#********** 求dz *********#
if(idx==self.layer_num-1): #最后一层的求导比较特殊,套最后一层求导的公式dz3
self.dz[idx] = (self.output-self.y) * self.Dsigmoid(self.z[idx]) #元素乘
else: #前层都可根据最后一层的dz迭代得到,套迭代公式dzi
self.dz[idx] = np.dot(self.dz[idx+1], self.W[idx+1].T)
* self.Dsigmoid(self.z[idx])
#********** 求dW *********#
if(idx == 0): #idx为0时,即到达第一层时,前层输入a[idx-1]是X
self.dW[idx] = np.dot(self.X.T, self.dz[idx]) / len(self.X) #梯度需除上总样本数
else: #idx不为0时迭代计算即可
self.dW[idx] = np.dot(self.a[idx-1].T, self.dz[idx]) / len(self.X)
#********** 求db *********#
self.db[idx] = np.sum(self.dz[idx], axis=0) / len(self.X) #db=dz, 但是需要所有维度取平均
idx -= 1 #跳前一层
# 求完所有层的梯度后,更新即可
for idx in range(self.layer_num):
self.W[idx] -= self.lr * self.dW[idx]
self.b[idx] -= self.lr * self.db[idx]
到此,前向传播与反向传播的函数都已经实现了,最后用一个train函数对所有功能函数进行封装,即可实现完整的神经网络代码。
3 完整代码
此代码是我在学习了好朋友的文章之后,扩展的更灵活的版本,觉得有难度的话可以先看看他的文章。
numpy实现神经网络代码(mnist手写体识别)_stay_zezo的博客-CSDN博客_numpy实现mnist手写数字识别blog.csdn.net下面是我的完整版代码:
import numpy as np
class NN(object):
def __init__(self, layer_list=[], lr=0.1, epochs=100):
self.lr = lr #学习率
self.layer_list = layer_list #每层神经元个数
self.epochs = epochs #迭代次数
#权重与偏执初始化
def weight_bias_init(self):
self.W = {} #权重字典,key是层号,value是权重矩阵
self.b = {} #偏置字典,key是层号,value是怕偏置矩阵
self.layer_num = len(self.layer_list)-1 #网络层数
#为每层layer初始化W与b矩阵
for idx in range(self.layer_num):
self.W[idx] = np.random.randn(self.layer_list[idx],
self.layer_list[idx+1]) * 0.01 #正态分布
self.b[idx] = np.random.randn(self.layer_list[idx+1])
# sigmoid函数
def sigmoid(self, x):
return 1.0 / (1 + np.exp(-x))
# sigmoid的一阶导数
def Dsigmoid(self, x):
return self.sigmoid(x) * (1 - self.sigmoid(x))
# 前向传播
def forward(self, X, y):
self.X, self.y = X, np.array(y).reshape(-1, 1)
self.z = {} #记录每层激活前的输出(z = W*X + b)
self.a = {} #记录每层激活后的输出(a = sigmoid(z))
#循环向前累乘
input = self.X
for idx in range(self.layer_num):
self.z[idx] = np.dot(input, self.W[idx]) + self.b[idx]
self.a[idx] = self.sigmoid(self.z[idx])
input = self.a[idx] #更新输入
self.output = self.a[self.layer_num-1] #最后一层输出
self.loss = -np.mean(self.y * np.log(self.output) +
(1-self.y) * np.log(1-self.output)) #对数损失
#误差反向传播
def backward(self):
#跟权重保存方式一样,使用字典存储,key为对应的层号
self.dz = {} #对每层z的求导
self.dW = {} #对每层W的求导
self.db = {} #对每层b的求导
idx = self.layer_num - 1 #从后往前求导
while(idx>=0):
#********** 求dz *********#
if(idx==self.layer_num-1): #最后一层的求导比较特殊,套最后一层求导的公式dz3
self.dz[idx] = (self.output-self.y) * self.Dsigmoid(self.z[idx]) #元素乘
else: #前层都可根据最后一层的dz迭代得到,套迭代公式dzi
self.dz[idx] = np.dot(self.dz[idx+1], self.W[idx+1].T)
* self.Dsigmoid(self.z[idx])
#********** 求dW *********#
if(idx == 0): #idx为0时,即到达第一层时,前层输入a[idx-1]是X
self.dW[idx] = np.dot(self.X.T, self.dz[idx]) / len(self.X) #梯度需除上总样本数
else: #idx不为0时迭代计算即可
self.dW[idx] = np.dot(self.a[idx-1].T, self.dz[idx]) / len(self.X)
#********** 求db *********#
self.db[idx] = np.sum(self.dz[idx], axis=0) / len(self.X) #db=dz, 但是需要所有维度取平均
idx -= 1 #跳前一层
# 求完所有层的梯度后,更新即可
for idx in range(self.layer_num):
self.W[idx] -= self.lr * self.dW[idx]
self.b[idx] -= self.lr * self.db[idx]
#迭代训练
def train(self, X, y):
self.weight_bias_init()
for i in range(self.epochs):
self.forward(X, y)
self.backward()
#每10轮打印一次loss
if(i%10==0): print("Epoch {}: loss={}".format(i//10+1, self.loss))
#预测概率输出
def predict(self, X_test):
input = X_test
for idx in range(self.layer_num):
z = np.dot(input, self.W[idx]) + self.b[idx]
a = self.sigmoid(z)
input = a
return a
使用测试:
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
X, y = load_iris(return_X_y=True)
X, y = X[:100], y[:100]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4)
layer_list = [4, 1] #自定义神经网络结构
model = NN(layer_list, lr=0.1, epochs=100)
model.train(X_train, y_train)
pre = model.predict(X_test)
pre = [1 if x>=0.5 else 0 for x in pre]
print(accuracy_score(pre, y_test))
此代码写完之后,调试了很久。几束青丝又不经意间飘落,程序猿太难了~
码字不易,觉得有用就点个赞吧~万分感谢
写在最后
机器学习zhuanlan.zhihu.com如果你对机器学习感兴趣,欢迎关注我的机器学习专栏~