我的工作
之前在b站上看到小蛮大佬做的一期用纸笔训练神经网络的视频,关于正向传递和反向传播这一块受益匪浅,但是视频中也存在一些公式以及绘图错误的地方,所以尝试复现了一些代码来更清晰的展现整个过程,目前只提供matlab与python版本的代码(PS:该博客只对视频中提到的内容做一些浅显的梳理,便于初学者理解)。
视频链接:https://www.bilibili.com/video/BV1R64y187yt/
基本思路
神经网络正向传递和反向传播的过程可以看成下图所示
正向传递:左边3×4维的矩阵经过一个黑盒后会得到右边3×2维的矩阵
- 一开始的时候,我们经过黑盒得到的3×2维的矩阵不一定如我们所愿是实际的目标结果Y,我们需要借助已知的目标结果Y对黑盒进行调整
- 输入3×4维矩阵X,经过黑盒生成3×2维矩阵y(这里是y,不是目标矩阵Y),这样在不断的更新迭代后,黑盒表现的很好了
- 那么当我们再次输入3×4维矩阵X,输出的预测矩阵y就会和目标矩阵Y相差无几,那么对黑盒进行调整的过程,就是反向传播过程
反向传播:右边3×2维的矩阵对黑盒进行调整的过程
黑盒是什么
黑盒包括经过的各种神经元,通过一些列矩阵相乘、激活函数等操作,最终由softmax得到输出结果。
这里引用视频中的一张图,红色方框内可以看成黑盒的部分
其中的紫色线段、蓝色线段、黑色线段,分别对应三个权重w1、w2、w3(这三个权重初始时是随机生成的)。比如紫色线段(对应w1),左边是四个输入(x1、x2、x3、x4),右边是三个神经元输出(s1、s2、s3),所以w1的维度就是4×3,以此类推。
所以神经网络的正向传递可以看成是一系列的矩阵相乘的过程
前面提到,三个权重在一开始是随机生成的,那么反向传递的调整过程,就是对这三个权重进行调整,利用预测的y与实际的Y的差值,即Loss,通过Loss分别对w进行求导(视频里用的是链式法则来解决),得到w的调整量g(w1、w2、w3对应g1、g2、g3),反向传递完后,原来的权重w减去对应的g(当然这个g一般会乘上学习率以及转置)即可得到更新后的w。
总结来说就是反向传播就是更新w的过程。
g1、g2、g3用链式法则求导的公式我会在代码中给出,就用matlab的代码来说一下吧,需要注意 * 和 .* 的区别,*是矩阵乘法,需要前一个数组的列与后一个数组的行相等,而 .*是需要两个矩阵维度完全相等的,是矩阵对应位置相乘
MATLAB源码
建议用matlab来debug矩阵变化的各个过程,比较方便清晰
clc,clear,close all
%% 训练样本
X=[1,1,0,0;
0,0,1,1;
1,0,0,1];
Y=[1,0; %实际值
0,1;
1,0];
[Inx,Iny]=size(X);%输入矩阵的维数
[Outx,Outy]=size(Y);%输出矩阵的维数
Hid_wide=3;%隐藏层节点维度
D=100; %损失初始值
a=0.1;%学习率
times=1;
res=0.001;%容差
Loss=inf; %预测值-实际值
%% 循环训练权重
while D>res %大于容差则一直循环
% disp(times);
disp(D);
%第一层
if times==1 %第一次随机生成
W1=rand(Iny,Hid_wide);%第一次循环随机生成权重W1
end
S1=X*W1;
%S1节点经过激活函数sigmod
Z1=sigmoid(S1);
%第二层
if times==1
W2=rand(Hid_wide,Outx);%第一次循环随机生成权重W2
end
S2=Z1*W2;
%S2节点经过激活函数
Z2=sigmoid(S2);
%输出层
if times==1
W3=rand(Outx,Outy);
end
Q=Z2*W3;
%输出层节点经过激活函数
y=sigmoid(Q); %预测值
%计算损失值
sum=0;
for m=1:Outx
for n=1:Outy
sum=sum+(y(m,n)-Y(m,n))^2;
end
end
if D>sum
D=sum;
end
%% 反向传递过程
%loss对W3求导
g3=(y-Y)'*Z2;
%loss对W2求导
g2=(((y-Y)*W3').*(sigmoid(S2).*(1-sigmoid(S2))))'*Z1;
%loss对W1求导
g1=((((y-Y)*W3').*(sigmoid(S2).*(1-sigmoid(S2))))*W2'.*(sigmoid(S1).*(1-sigmoid(S1))))'*X;
%% 更新权重
W1=W1-a*g1';
W2=W2-a*g2';
W3=W3-a*g3';
Loss(times)=D; %记录每次的损失值
times=times+1;
end
plot(Loss);%打印损失降低过程
disp('调整后的W1');
disp(W1);
disp('调整后的W2');
disp(W2);
disp('调整后的W3');
disp(W3);
disp('真实Y值');
disp(Y);
disp('训练的Y值');
disp(y);
%% 激活函数sigmod
function result=sigmoid(A)
result=inf;
[A1,A2]=size(A);
for i=1:A1
for j=1:A2
result(i,j)=1/(1+exp(-A(i,j)));
end
end
end
Python源码
刚入门python,原谅我大量不熟练的操作,数组计算部分相比matlab确实太累了
"""
作者:猪脚三父
日期:2022年02月01日
"""
import numpy as np
import matplotlib.pyplot as plt # 画图用的包
def sigmoid(A): # 激活函数sigmod
A1 = np.size(A, 0)
A2 = np.size(A, 1)
result = [[0.0 for col in range(A2)] for row in range(A1)] # 初始化一个A1*A2维度的列表
result = np.array(result) # 转为数组
for i in range(A1):
for j in range(A2):
result[i, j] = 1 / (1 + np.exp(-1 * A[i, j]))
return result
if __name__ == '__main__':
X = [[1,1,0,0],
[0,0,1,1],
[1,0,0,1]]
Y=[[1,0],
[0,1],
[1,0]]
X = np.array(X) # 转成array格式
Y = np.array(Y) # 转成array格式
Inx = np.size(X, 0) # 输入矩阵的维数
Iny = np.size(X, 1) # 输入矩阵的维数
Outx = np.size(Y, 0) # 输出矩阵的维数
Outy = np.size(Y, 1) # 输出矩阵的维数
Hid_wide = 3 # 隐藏层节点维度
D = 100 # 损失初始值
a = 0.1 # 学习率
times = 1
res = 0.001 # 容差
Loss = []
while D > res: # 大于容差则一直循环
# 第一层
if times == 1: # 第一次随机生成
W1 = np.random.random((Iny, Hid_wide)) # 第一次循环随机生成权重W1
S1 = np.dot(X, W1)
Z1 = sigmoid(S1) # S1节点经过激活函数sigmod
# 第二层
if times == 1: # 第一次随机生成
W2 = np.random.random((Hid_wide, Outx)) # 第一次循环随机生成权重W2
S2 = np.dot(Z1, W2)
Z2 = sigmoid(S2) # S2节点经过激活函数sigmod
if times == 1: # 第一次随机生成
W3 = np.random.random((Outx, Outy)) # 第一次循环随机生成权重W3
Q = np.dot(Z2, W3)
# 输出层节点经过激活函数
y = sigmoid(Q) # 预测值
# 计算损失值
sum = 0
for m in range(Outx):
for n in range(Outy):
sum = sum + (y[m, n] - Y[m, n]) ** 2
if D > sum:
D = sum
# 反向传递过程
# loss对W3求导
g3 = np.dot(np.transpose(y - Y), Z2)
# loss对W2求导
tmp2 = np.dot(y - Y, np.transpose(W3))
tmp3 = np.multiply(sigmoid(S2), 1 - sigmoid(S2))
tmp1 = np.multiply(tmp2, tmp3)
g2 = np.dot(np.transpose(tmp1), Z1)
# loss对W1求导
tmp6 = np.multiply(sigmoid(S1), 1 - sigmoid(S1))
tmp5 = np.dot(tmp1, np.transpose(W2))
tmp4 = np.multiply(tmp5, tmp6)
g1 = np.dot(np.transpose(tmp4), X)
# 更新权重
W1 = W1 - a * np.transpose(g1)
W2 = W2 - a * np.transpose(g2)
W3 = W3 - a * np.transpose(g3)
Loss.append(D) # 记录每次的损失值
times = times + 1
# 打印数据 & 画图
print('调整后的W1')
print(W1)
print('调整后的W2')
print(W2)
print('调整后的W3')
print(W3)
print('真实Y值')
print(Y)
print('训练的Y值')
print(y)
t = range(len(Loss))
plt.figure(dpi=100, figsize=(12, 6)) # 指定图像分辨率和画板大小
plt.fill_between(t, Loss, color="skyblue", alpha=0.3)
plt.plot(t, Loss, color="blue") # 多勾勒一层蓝边
plt.xlabel('迭代次数') # x轴上的名字
plt.ylabel('Loss') # y轴上的名字
plt.rcParams['font.sans-serif'] = ['SimHei'] # 不加不能显示中文
plt.rcParams['axes.unicode_minus'] = False # 不加不能显示中文
plt.show() # 打印图像