手搓神经网络——BP反向传播
前言本打算围绕《Learning representations by back-propagating errors》一文进行解读的
奈何没有中文版的文档(笔者懒得翻译 English)
所以文章内容只能根据笔者自身对 BP 反向传播算法的理解来编写咯~??
在论文的标题里写道back-propagating errors,即反向传播误差
先通过前向传播计算得到输出结果(预测值)进而计算输出结果与正确值之间的误差将误差反向传播,并更新参数,得以实现“学习”的目的至于…误差如何反向传播,参数如何得以更新,也是本文在探讨的importrandom
importnumpyasnp
importtensorflowastf
frommatplotlibimportpyplotasplt
定义激活函数、损失函数这里只采用 ReLU、Sigmoid 作为神经网络的激活函数,MSE 作为损失函数
下面列了这仨的计算公式与导数的计算公式。除了定义计算公式,也定义了其求导公式,因为链式法则,懂吧??? 就是为了方便后面的求导(也就是误差反向传播)
ReLU
Sigmoid
MSE
#ReLU激活函数
classReLU:
def__call__(self,x):
returnnp.maximum(0,x)
#对ReLU求导
defdiff(self,x):
x_temp=x.copy()
x_temp[x_temp0]=1
returnx_temp
#Sigmoid激活函数
classSigmoid:
def__call__(self,x):
return1/(1+np.exp(-x))
#对Sigmoid求导
defdiff(self,x):
returnx*(1-x)
#MSE损失函数
classMSE:
def__call__(self,true,pred):
returnnp.mean(np.power(pred-true,2),keepdims=True)
#对MSE求导
defdiff(self,true,pred):
returnpred-true
relu=ReLU()
sigmoid=Sigmoid()
mse=MSE()
简单的 BP 反向传播这里从最简单的开始,隐藏层只设置一个神经元,用sigmoid作为激活函数
一切随缘??,对于x、w、b、true全都随机生成。而这一个神经元的任务就是不断的卷(学习),让输出结果无限接进true
整个神经网络的模型图如下,将就着看吧,笔者懒得弄图??????
|————————————输入层
||————————隐藏层
|||————输出层
〇——〇——〇
前向计算过程:x - w·x+b - sigmoid(w·x+b) - mse(true, sigmoid(w·x+b))
反向计算过程:
天地!反向计算得过程好复杂哇??。但一句概括得话就是误差对参数求导,这里就是在用链式法则对w与b求导,仅此而已
而更新得计算方式就是,参数自身减去lr乘?误差对参数的求导结果
约法 6 章:
x:输入的值w:权重b:偏置true:我们要的正确值lr:学习率epochs:学习次数x=random.random()
w=random.random()
b=random.random()
true=random.random()
print(f'x={x}true={true}')
lr=0.3
epochs=520
#用于记录loss
loss_hisory=[]
forepochinrange(epochs):
#获取预测值
pred=sigmoid(w*x+b)
#计算损失
loss=mse(true,pred)
#更新参数
w-=lr*x*sigmoid.diff(pred)*mse.diff(true,pred)
b-=lr*sigmoid.diff(pred)*mse.diff(true,pred)
ifepoch%100==0:
print(f'epoch{epoch},loss={loss},pred={pred}')
loss_hisory.append(loss)
print(f'epoch{epoch+1},loss={loss},pred={pred}')
#绘制loss曲线图
plt.plot(loss_hisory)
plt.show()
==============================
输出:
x=0.11313136923799194true=0.8484027350076178
epoch0,loss=0.017935159490587653,pred=0.7144805206793456
epoch100,loss=0.0025295950301247104,pred=0.7981076554259657
epoch200,loss=0.000615626922003235,pred=0.8235909047243993
epoch300,loss=0.00018266148479303084,pred=0.8348875034227343
epoch400,loss=5.94620257403187e-05,pred=0.8406915725958721
epoch500,loss=2.0323334362411495e-05,pred=0.8438945941089311
epoch520,loss=1.6631758505966922e-05,pred=0.8443245297030791
e3c0cd82-7117-47cf-b2b8-50a549864227.pngloss 图中的曲线也是呈下降的趋势,输出的预测值pred是在不断的接近true的,说明确确实实起到了“学习”的效果。下面来瞅瞅为啥要求导吧
#假设有一批预测值pred
pred=np.arange(-20,21)
true=1
#MSE的曲线图
plt.plot((pred-true)**2,label='MSE')
plt.scatter(21,1,color='red',label='true')
plt.scatter(30,81,color='orange',label='pred1')
plt.scatter(12,81,color='green',label='pred2')
plt.plot(np.arange(25,40),2*9*(np.arange(15))-10,label='line1(k=18)')
plt.plot(np.arange(5,20),2*-9*(np.arange(15))+205,label='line2(k=-18)')
plt.legend()
plt.show()
86cc6f64-fcd9-4744-9108-0792ee27ec45.png上面是一张MSE的曲线图
红点??是true,是我们想要的预期值,在这个点的时候误差最小橙点??是pred,也是神经网络的输出值橙色的线条是在于MSE曲线上橙点??处的切线(这条线的斜率为18)绿色的线条是在于MSE曲线上绿点??处的切线(这条线的斜率为-18)那么...有趣的来了,对于橙点??,误差要向左减小。对于绿点??,误差要向右减小。观察两条切线的斜率,这能发现这与斜率的方向(正负)有关系
至此,笔者认为:计算误差对参数(权重w and 偏置b)的导数,根据其导数的正负即可得出参数更新的方向(是加? or 是减???)
从而得到参数更新的计算方式,参数自身减去lr乘?误差对参数的求导结果
那学习率lr还有什么用,lr能控制学习的速度。但太小会导致学的慢,太大会导致难以收敛(用上面的MSE曲线图来解释的话,就是原本在橙点??处,结果学习率太大,一学学过头跳到绿点??所在的地方了)
高级的 BP 反向传播前面只是开胃菜????????????,这里难度加加加!使用矩阵的方式,再实现一次
与前面神经网络不同的是,这次的隐藏层含有3个神经元。输入x仍然是随缘,只不过我们要的正确值true在这里指定为[0.1]
有亿点长,但笔者认为不复杂
前向计算过程:x - x@w1+b1 - sigmoid(x@w1+b1) - sigmoid(x@w1+b1)@w2+b2 - sigmoid(sigmoid(x@w1+b1)@w2+b2) - mse(true, sigmoid(sigmoid(x@w1+b1)@w2+b2))
@是矩阵点乘,注意哦噢??,因为是已经是矩阵计算了,变成了x@w+b而非之前的w·x+b
整个神经网络的模型图如下
|————————————输入层
|〇———————隐藏层
|/\|————输出层
〇——〇——〇
\/
〇
x=np.random.rand(1,1)
#生成3个神经元的权重
w1=np.random.rand(1,3)
#生成3个神经元的偏置
b1=np.random.rand(1,3)
#这是输出层的
w2=np.random.rand(3,1)
b2=np.random.rand(1,1)
#我们期望得到的正确值
true=np.array([[0.1]])
lr=0.1
epochs=520
loss_hisory=[]
forepochinrange(epochs):
#注意了!??y是隐藏层的输出pred是输出层的输出
y=sigmoid(x@w1+b1)
pred=sigmoid(y@w2+b2)
#计算损失
loss=mse(true,pred)
#时代变了,大人!这里要反着来(先输出层后隐藏层),要不然你以为为什么叫误差反向传播呢?
#更新输出层参数
w2-=lr*x.T@sigmoid.diff(pred)*mse.diff(true,pred)
b2-=lr*sigmoid.diff(pred)*mse.diff(true,pred)
#更新隐藏层参数
w1-=lr*x.T@(sigmoid.diff(y)*((sigmoid.diff(pred)*mse.diff(true,pred))@w2.T))
b1-=lr*(sigmoid.diff(y)*((sigmoid.diff(pred)*mse.diff(true,pred))@w2.T))
ifepoch%100==0:
print(f'epoch{epoch},loss={loss},pred={pred}')
loss_hisory.append(loss[0])
print(f'epoch{epoch+1},loss={mse(true,pred)},pred={pred}')
#绘制loss曲线图
plt.plot(loss_hisory)
plt.show()
==============================
输出:
epoch0,loss=[[0.65487189]],pred=[[0.90924155]]
epoch100,loss=[[0.08011342]],pred=[[0.38304315]]
epoch200,loss=[[0.00990116]],pred=[[0.19950457]]
epoch300,loss=[[0.00304457]],pred=[[0.1551776]]
epoch400,loss=[[0.00126247]],pred=[[0.13553131]]
epoch500,loss=[[0.00060302]],pred=[[0.12455654]]
epoch520,loss=[[0.00052945]],pred=[[0.12300987]]
a994dc86-ea93-4216-b8c2-369ca2492f08.png观察输出的pred,非常的nice??,最后一次的[[0.90332459 0.89611935 0.11057356]]已经足够接近[[1, 1, 0]]了,下面来谈谈重点
这是简单的BP反向传播中的参数更新算法
w-=lr*x*sigmoid.diff(pred)*mse.diff(true,pred)
b-=lr*sigmoid.diff(pred)*mse.diff(true,pred)
这是高级的BP反向传播中的参数更新算法
w-=lr*x.T@sigmoid.diff(pred)*mse.diff(true,pred)
b-=lr*sigmoid.diff(pred)*mse.diff(true,pred)
观察输出的pred,非常的nice??,最后一次的[[0.90332459 0.89611935 0.11057356]]已经足够接近[[1, 1, 0]]了,下面来谈谈重点
这是简单的BP反向传播中的参数更新算法
w2-=lr*x.T@sigmoid.diff(pred)*mse.diff(true,pred)
b2-=lr*sigmoid.diff(pred)*mse.diff(true,pred)
这是高级的BP反向传播中的参数更新算法
w-=lr*x.T@sigmoid.diff(pred)*mse.diff(true,pred)
b-=lr*sigmoid.diff(pred)*mse.diff(true,pred)
都是用链式法则的原理进行损失对参数的求导,从而更新参数。来对比一下,不同之处就在于权重w的更新上了,一个是x * sigmoid.diff(pred)一个则是x.T @ sigmoid.diff(pred)。无非是从x *改成了x.T @(吐槽??:娘希匹,就改这么一丢丢,可要笔者老命咯~,要不然这篇文章在3年前就该写出的)
哇趣??????,对不住了各位。在矩阵求导里为什么用x.T @,笔者也无法解释,只能说这非常重要特别是那个.T和@(其实就是笔者菜??????)。至于为啥解释不了,请看VCR???。y1与y2的区别就是有无激活函数,笔者没搞懂为什么这里的有无对最后的求导计算影响蛮大的
x=tf.constant([[0.5]],dtype='float32')
w=tf.constant([[1,2,3]],dtype='float32')
b=tf.constant([[4,5,6]],dtype='float32')
withtf.GradientTape()astape_1,tf.GradientTape()astape_2:
tape_1.watch(w)
tape_2.watch(w)
y1=x@w+b
y2=tf.nn.sigmoid(x@w+b)
#使用tensorflow求导
print('使用tensorflow求导')
diff_1=tape_1.gradient(y1,w)
diff_2=tape_2.gradient(y2,w)
print('d(y1)_d(w):',diff_1.numpy())
print('d(y2)_d(w):',diff_2.numpy())
#使用numpy手搓求导
print('\n使用numpy手搓求导')
print('d(y1)_d(w):',x.numpy().T)
print('d(y2)_d(w):',x.numpy().T@sigmoid.diff(y2.numpy()))
==============================
输出:
使用tensorflow求导
d(y1)_d(w):[[0.50.50.5]]
d(y2)_d(w):[[0.005433110.001233260.00027623]]
使用numpy手搓求导
d(y1)_d(w):[[0.5]]
d(y2)_d(w):[[0.005433110.001233260.00027623]]
对比使用 tensorflow 求导与使用 numpy 手搓求导的d(y1)_d(w),数值上是对了,但形状不一样。一旦加上了激活函数两者却又一样了,等大佬解释??????
手搓神经网络至此,重头戏来咯~??????,要实现矩阵求导+自动求导,搓出个神经网络 ??
绷不住啦!笔者必须要吐槽下??????????(下面内容与机器学习无关,纯纯笔者吐槽编编写文章时所遇问题,可跳过)
在代码中,NetWork中的self.layers是为了便于记录神经网络层而存在的
ifselfnotinparent.layers:
parent.layers.append(self)
这段的作用是将Linear层加入到self.layers中,方便后期的反向传播计算。但如果没有if self not in parent.layers:这句,整个神经网络的输入与输出的形状(shape)就必须一样,否做就会造成矩阵计算错误
Why?在NetWork的fit方法中有pred = self(x),这是用于前向计算神经网络的输出的,如果没有前面的if语句,就会导致每次调用pred = self(x)时,都会向self.layers中添加Linear层(明明设置了2层的神经网络,第一次调用会添加2层,这是对的,但第二次调用会继续添加2层,导致后期反向传播时造成矩阵计算错误(错误位置new_grad = activation_diff_grad @ self.weight.T))
关于这一点,因为在之前x与true的形状(shape)一直是设置成一样的,所以笔者也没发现
目前读者所读的文章,已经是第 n 个版本了(一直在努力详解内容,与修改勘误中??????)
#定义层
classLinear:
def__init__(self,inputs,outputs,activation):
'''
inputs:输入神经元个数
outpus:输出神经元个数
activation:激活函数
'''
#初始化weight
self.weight=np.random.rand(inputs,outputs)/10
#此行是为了防止后期梯度消失而存在的,最简单的方法也可以是self.weight/10,下同
self.weight=self.weight/self.weight.sum()
#初始化bias
#这里只写outputs与批大小的计算有关
self.bias=np.random.rand(outputs)/10
self.bias=self.bias/self.bias.sum()
#激活函数
self.activation=activation
#这里用作后期误差反向传播用
self.x_temp=None
self.t_temp=None
#层前向计算
def__call__(self,x,parent):
self.x_temp=x
self.t_temp=self.activation(x@self.weight+self.bias)
#将此层加入到layers当中,便于后期的反向传播操作
ifselfnotinparent.layers:
parent.layers.append(self)
returnself.t_temp
#更新weight、bias
defupdate(self,grad):
activation_diff_grad=self.activation.diff(self.t_temp)*grad
#这个变量肩负重任,将后面的梯度不断向前传播
new_grad=activation_diff_grad@self.weight.T
#参数的更新
self.weight-=lr*self.x_temp.T@activation_diff_grad
#这里的mean(axis=0)与批大小的计算有关
self.bias-=lr*activation_diff_grad.mean(axis=0)
#这里将误差继续往前传
returnnew_grad
#定义网络
classNetWork:
def__init__(self):
#储存各层,便于后期的反向传播操作,类似于tf.keras.Sequential
self.layers=[]
#构造神经网络
self.linear_1=Linear(4,16,activation=relu)
self.linear_2=Linear(16,8,activation=relu)
self.linear_3=Linear(8,3,activation=sigmoid)
#模型计算
def__call__(self,x):
x=self.linear_1(x,self)
x=self.linear_2(x,self)
x=self.linear_3(x,self)
returnx
#模型训练
deffit(self,x,y,epochs,step=100):
forepochinrange(epochs):
pred=self(x)
self.backward(y,pred)
ifepoch%step==0:
print(f'epoch{epoch},loss={mse(y,pred)},pred={pred}')
print(f'epoch{epoch+1},loss={mse(y,pred)},pred={pred}')
#反向传播
defbackward(self,true,pred):
#对误差求导
grad=mse.diff(true,pred)
#反向更新层参数,反向!!!所以是reversed,反着更新层
forlayerinreversed(self.layers):
grad=layer.update(grad)
network=NetWork()
x=np.array([[1,2,3,4]])
true=np.array([[0.1,0.1,0.6]])
#训练启动!!!
network.fit(x,true,520,100)
==============================
输出:
epoch0,loss=[[0.16011657]],pred=[[0.637361440.537695950.60382743]]
epoch100,loss=[[0.01028116]],pred=[[0.227849940.218970940.61854131]]
epoch200,loss=[[5.52173933e-05]],pred=[[0.106588260.110966410.60140883]]
epoch300,loss=[[1.66720973e-06]],pred=[[0.099780160.102224520.60006921]]
epoch400,loss=[[2.65396934e-07]],pred=[[0.099532480.100759980.60000692]]
epoch500,loss=[[6.09600151e-08]],pred=[[0.099733310.10033430.60000093]]
epoch520,loss=[[4.62963427e-08]],pred=[[0.099765070.10028930.60000066]]
反正输出显示,pred有在逼近[[0.1, 0.1, 0.6]],说明模型确确实实是在“学习”的...
TensorFlow 验证是骡子 ?? 是马 ?? 拉出来溜溜不就晓得了
验证方式
两者皆使用想用的神经网络构造与参数,x与true也相同用 TensorFlow 计算一次用 手搓的神经网络 计算一次对比loss对层1中w1参数的导数是否一致神经网络的模型图,如下
x=tf.random.uniform((1,2))
#层1的参数
w1=tf.random.uniform((2,4))
b1=tf.random.uniform((4,))
#层2的参数
w2=tf.random.uniform((4,8))
b2=tf.random.uniform((8,))
#层3的参数
w3=tf.random.uniform((8,2))
b3=tf.random.uniform((2,))
true=tf.constant([[0.5,0.2]])
withtf.GradientTape()astape_1:
tape_1.watch(w1)
#用tensoflow的激活函数前向计算
y=tf.nn.relu(x@w1+b1)
y=tf.nn.sigmoid(y@w2+b2)
y=tf.nn.sigmoid(y@w3+b3)
#用tensoflow的损失函数计算loss
loss=tf.keras.losses.mse(true,y)
print('mse-loss:',loss.numpy())
dLoss_dX=tape_1.gradient(loss,w1)
print('loss对w1的导数:\n',dLoss_dX.numpy())
==============================
输出:
mse-loss:[0.4319956]
loss对w1的导数:
[[0.001113730.00122630.000950620.00114191][0.000207710.00022870.000177290.00021296]]
因为只是对比计算的结果,这里手搓的神经网络就简化一下子了
重点??!!! 标记处是和获loss对w1的导数有关的注意了??! 标记处是细节,为了与tensorflow的结果做对比与上面的手搓神经网络有区别的地方classLinear:
def__init__(self,weight,bias,activation):
#这里权重、参数不随机生成了,直接用上面tensorflow的
self.weight=weight
self.bias=bias
self.activation=activation
self.x_temp=None
self.t_temp=None
#重点??!!!为了方便计算linear_1里loss对w1的导数,这个变量用来记录梯度
self.activation_diff_grad=None
def__call__(self,x,parent):
self.x_temp=x
self.t_temp=self.activation(x@self.weight+self.bias)
ifselfnotinparent.layers:
parent.layers.append(self)
returnself.t_temp
defupdate(self,grad):
self.activation_diff_grad=self.activation.diff(self.t_temp)*grad
new_grad=self.activation_diff_grad@self.weight.T
#重点??!!!self.x_temp.T@activation_diff_grad便是loss对weight的导数
self.weight-=lr*self.x_temp.T@self.activation_diff_grad
self.bias-=lr*self.activation_diff_grad
returnnew_grad
classNetWork:
def__init__(self):
self.layers=[]
#这里使用前边tensorflow的权重、偏置
#注意了??!这里要把参数转为numpy类型
self.linear_1=Linear(w1.numpy(),b1.numpy(),activation=relu)
self.linear_2=Linear(w2.numpy(),b2.numpy(),activation=sigmoid)
self.linear_3=Linear(w3.numpy(),b3.numpy(),activation=sigmoid)
def__call__(self,x):
x=self.linear_1(x,self)
x=self.linear_2(x,self)
x=self.linear_3(x,self)
returnx
deffit(self,x,y,epochs):
forepochinrange(epochs):
pred=self(x)
self.backward(y,pred)
defbackward(self,true,pred):
print('mse-loss:',mse(true,pred))
grad=mse.diff(true,pred)
forlayerinreversed(self.layers):
grad=layer.update(grad)
#重点??!!!这里只输出最后一次的计算结果
print('loss对w1的导数:\n',self.linear_1.x_temp.T@self.linear_1.activation_diff_grad)
network=NetWork()
#迎接你们的亡!!!??
#注意了??!这里要把参数转为numpy类型(因为这里的数据都是上面tensorflow的,转成numpy格式才行)
network.fit(x.numpy(),true.numpy(),1)
==============================
输出:
mse-loss:[[0.43199557]]
loss对w1的导数:
[[0.001113740.00122630.000950620.00114191][0.000207710.00022870.000177290.00021296]]
快快快!你看 ?????? 两者的结果是一样的,说明手搓出来的是对的
。k 文章水完啦 ??????
哇趣...写的真是累喂~ (#`O′)
没用的冷知识:整篇文章由ipynb改的,若要运行验证,每段直接copy至ipynb文件
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线