python和numpy纯手写BP神经神经网络
二级目录
三级目录
神经网络的基本理解
神经网络分为三层,输入层,隐含层以及输出层。隐含层可以为多层神经网络。输入层为输入样本的数据维度(N*D),输出层为样本标签y,y可以为离散值(分类任务)也可以为连续值(回归任务)。
个人理解,神经网络的训练过程主要有四个步骤,包括:
(1)正向传播:通过多层线性网络结构(w_1,w_2,…w_n),将输入样本进行特征信息提取,降低原始数据的维度。
(2)激活函数:将每层网络线性求解的结果,进行非线性变换,实现特征信息的非线性转换。常见的激活函数:simgoid函数,relu函数,tanh函数等。
(3)误差求解:将模型求出的预测值,与真实值进行误差求解,得到模型预测值与真实值之间的差异,用于后续的反向传播。
(4)反向传播。根据误差值,调整模型参数w,使得模型的预测值接近于模型的真实值。
神经网络的主要原理就是通过不断调整模型参数,使得模型的预测值接近于真实值。
神经网络公式
神经网络结构如上图所示,下图是上图的公式理解,模型参数的设定,关键在于层层神经元的个数,也就是说上层的输入维度需与下一层的输入维度相同。
例:输入样本X为N个样本,3维特征,X-[N,3],第一层隐含层的w_1-[3,d],d为经过第一层网络后,对原始数据进行升维或者降维(特征提取),设w_1-[3,4],b_1-[1,4],进过第一层网络得到z_1,z_1-[N,4],通过激活函数,得到h_1,h_1便是第一层网络对原始数据特征提取后得到的结果。
前向传播
第一层网络公式如下(输入层→第一层隐含层):
f()为激活函数
第二层网络公式如下(第一层隐含层→第二层隐含层):
第三层网络公式如下(第二层隐含层→输出层):
最后输出的是h3,(N,10),N是样本个数,10是类别种类个数。
激活函数
前向传播中的f()就是激活函数
误差求解
计算模型输出值与真实值之间的误差。误差一般都是经过绝对值或者平方的数,以得的一个始终大于0得的数,以无限接近于0,作为预测精准度。本文以一个L2-LOSS为例:
反向传播
反向传播是神经网络中的关键一步,如何将误差层层方向传递到各个网络层中的w是关键。求解dw_out,dw_2.dw_1.
(1)求解dw_out;
通过链式法则,将复合函数进行分解进行逐步求导。
h2-(5,N)
注意:1. 这里用的激活函数是sigmoid函数,不同的激活函数,在该项中求得的结果是不一样的;2. h3矩阵与(1-h3)矩阵相乘,而不是点乘,这里是对矩阵中的每个元素进行求导,因此出来的结果是逐元素相乘。在求导过程中,有的是点乘,有的是相乘。
h2是输出层的输入值,(h3-y)(h3(1-h3))可以看做误差,所以dwout其实就是等于输入的转置点乘误差。(计算的时候,先计算误差,误差中的乘法运算是相乘。)
求解db_out;
db_out的输入只是单位矩阵I,其实就是将误差的所有行相加。
db就是误差的求和。
需要改变的梯度方向,其实就是输入的转置·误差
(2)求解dw_2;
将上一层的误差error_1通过上一层的参数w进行传递,并再乘以该层的激活函数的求导,便是这一层的误差。
这里w的转置,是为了与前面的维度相匹配,其实求导出来的是w还是w的转置,这个是根据分子布局还是分母分局来确定的,w和w的装置在这里其实是一样的,只是元素排列的方式不同。(个人理解)
(3)求解dw_1;
参数更新
求出dw,db只是说明在某点参数w,b下降最快的方向,并不能确定下降多少距离。因此引入一个学习率a,用于表示下降的距离。学习率a不能过大,因为过大的话,有可能下降距离过大,导致错过最小极值点。
w_out=w_out-adw_out
b_out=b_out-adb_out
w_2=w_2-adw_2
b_2=b_2-adb_2
w_1=w_1-adw_1
b_1=b_1-adb_1
dw只是个变化方向,通过a来赋予距离,在于原始的w进行向量的加减,便可以达到新w在某一方向变化的距离。
神经网络python代码
初始化模型参数w,b。利用np.random.randn随机生成0-1之间的数
def __init__(self,input_size,label_size):
np.random.seed(6)
self.w_1=np.random.randn(input_size,4)
self.b_1=np.random.randn(1,4)
self.w_2=np.random.randn(4,5)
self.b_2=np.random.randn(1,5)
self.w_out=np.random.randn(5,label_size)
self.b_out=np.random.randn(1,label_size)
定义前向传播
def feed_forward(self,x):
z_1=np.dot(x,self.w_1)+self.b_1
h_1=self.relu(z_1)
##print(h_1.shape)
z_2=np.dot(h_1,self.w_2)+self.b_2
h_2=self.sigmoid(z_2)
##print(h_2.shape)
z_3=np.dot(h_2,self.w_out)+self.b_out
h_3=self.sigmoid(z_3)
##print(h_3.shape)
return h_1,h_2,h_3
得到每一层神经网络的输出,并通过激活函数对输出进行非线性转换。
def back(self,x,y):
h_1,h_2,h_3=self.feed_forward(x)
error,loss=self.loss_l2(h_3,y)
error_tans_out=np.multiply(error,self.de_sigmoid(h_3))
dw_out=np.dot(h_2.T,error_tans_out)
db_out=np.sum(error_tans_out,axis=0,keepdims=True)
error_trans_2=np.multiply(np.dot(error_tans_out,w_out.T),self.de_sigmoid(h_2))
dw_2=np.dot(h_1.T,error_trans_2)
db_2=np.sum(error_trans_2,axis=0,keepdims=True)
error_trans_3=np.multiply(np.dot(error_trans_2,w_2.T),self.relu(h_1))
dw_1=np.dot(x.T,error_trans_3)
db_1=np.sum(error_trans_3,axis=0,keepdims=True)
return dw_out,db_out,dw_2,db_2,dw_1,db_1
在反向传播中,一定要搞清楚点乘和叉乘。
参数更新:
def update_weight(self,w_1,b_1,w_2,b_2,w_out,b_out,learn_rate):
self.w_1=self.w_1-learn_rate*dw_1
self.b_1=self.b_1-learn_rate*db_1
self.w_2=self.w_2-learn_rate*dw_2
self.b_2=self.b_2-learn_rate*db_2
self.w_out=self.w_out-learn_rate*dw_out
self.b_out=self.b_out-learn_rate*db_out
损失函数:
def loss_l2(self,h,y):
error=h-y
error_2=0.5*(error*error)
return error,np.sum(error_2)
激活函数与激活函数的求导
def relu(self,x):
z = np.maximum(x, 0)
return z
def tanh(self,x):
return np.tanh(x)
def sigmoid(self,x):
ex=np.exp(x)
return ex/(ex+1)
def de_relu(self,z,h):
z[z <= 0] = 0
z[z > 0] = 1.0
return z
def de_sigmoid(self,h):
return h*(1-h)
训练过程
def train(self,epoch_num,x,y,learning_rate):
losses=[]
for i in range(epoch_num):
h_1,h_2,h_3=self.feed_forward(x)
error,error_2=self.loss_l2(h_3,y)
dw_out,db_out,dw_2,db_2,dw_1,db_1=self.back(x,y)
self.update_weight(dw_1,db_1,dw_2,db_2,dw_out,db_out,learning_rate)
losses.append(error_2)
if(i%20==0):
print("iter:{},loss:{}".format(i,error_2))
return losses
所有代码:
class BPNN(object):
def __init__(self,input_size,label_size):
np.random.seed(6)
self.w_1=np.random.randn(input_size,10)
self.b_1=np.random.randn(1,10)
self.w_2=np.random.randn(10,5)
self.b_2=np.random.randn(1,5)
self.w_out=np.random.randn(5,label_size)
self.b_out=np.random.randn(1,label_size)
def feed_forward(self,x):
z_1=np.dot(x,self.w_1)+self.b_1
h_1=self.relu(z_1)
##print(h_1.shape)
z_2=np.dot(h_1,self.w_2)+self.b_2
h_2=self.sigmoid(z_2)
##print(h_2.shape)
z_3=np.dot(h_2,self.w_out)+self.b_out
h_3=self.sigmoid(z_3)
##print(h_3.shape)
return h_1,h_2,h_3
def back(self,x,y):
h_1,h_2,h_3=self.feed_forward(x)
error,loss=self.loss_l2(h_3,y)
error_tans_out=np.multiply(error,self.de_sigmoid(h_3))
dw_out=np.dot(h_2.T,error_tans_out)
db_out=np.sum(error_tans_out,axis=0,keepdims=True)
error_trans_2=np.multiply(np.dot(error_tans_out,w_out.T),self.de_sigmoid(h_2))
dw_2=np.dot(h_1.T,error_trans_2)
db_2=np.sum(error_trans_2,axis=0,keepdims=True)
error_trans_3=np.multiply(np.dot(error_trans_2,w_2.T),self.relu(h_1))
dw_1=np.dot(x.T,error_trans_3)
db_1=np.sum(error_trans_3,axis=0,keepdims=True)
return dw_out,db_out,dw_2,db_2,dw_1,db_1
def update_weight(self,w_1,b_1,w_2,b_2,w_out,b_out,learn_rate):
self.w_1=self.w_1-learn_rate*dw_1
self.b_1=self.b_1-learn_rate*db_1
self.w_2=self.w_2-learn_rate*dw_2
self.b_2=self.b_2-learn_rate*db_2
self.w_out=self.w_out-learn_rate*dw_out
self.b_out=self.b_out-learn_rate*db_out
def loss_l2(self,h,y):
error=h-y
error_2=0.5*(error*error)
return error,np.sum(error_2)
def relu(self,x):
z = np.maximum(x, 0)
return z
def tanh(self,x):
return np.tanh(x)
def sigmoid(self,x):
ex=np.exp(x)
return ex/(ex+1)
def de_relu(self,z,h):
z[z <= 0] = 0
z[z > 0] = 1.0
return z
def de_sigmoid(self,h):
return h*(1-h)
def train(self,epoch_num,x,y,learning_rate):
losses=[]
for i in range(epoch_num):
h_1,h_2,h_3=self.feed_forward(x)
error,error_2=self.loss_l2(h_3,y)
dw_out,db_out,dw_2,db_2,dw_1,db_1=self.back(x,y)
self.update_weight(dw_1,db_1,dw_2,db_2,dw_out,db_out,learning_rate)
losses.append(error_2)
if(i%20==0):
print("iter:{},loss:{}".format(i,error_2))
return losses
if __name__ == '__main__':
n_samples=10
n_feature=5
x=np.random.randn(n_samples,n_feature)
y=np.array([1,2,2,3,2,3,1,3,1,3])
y=y-1
out_size=len(np.unique(y))
y_1=np.zeros(shape=(n_samples,out_size))
for i in range(len(y)):
y_1[i][y[i]]=1
epoch_num = 10000
model=BPNN(5,3)
losses = model.train(epoch_num=epoch_num,x=x,y=y_1,learning_rate=0.01)
关键点:
参数梯度变化的方向(dw)=输入的转置·误差
误差就是将上一层的误差通过上一层的w进行传递,在叉乘该层激活函数的导数。