使用numpy实现反向传播算法示例

HW1:使用numpy实现反向传播算法示例

注:以往届 AI 基础作业为资料、采用任务驱动型方法进行学习

作业内容:

  • 阅读并理解代码
  • 修改 np_mnist_template.py,更改 loss 函数、网络结构、激活函数、完成训练 MLP (多层感知机,Multilayer Perception)网络以识别手写数字 MNIST(modified national institute of standards and technology)数据集

代码理解

numpy 数值调用库(相当于#include)

1
import numpy as np

使用np.array以支持矢量化运算如np.array[1,2]+np.array[3,4]

1
2
3
4
5
X=np.array([0,0,1],[0,1,1],[1,0,1],[1,1,1])
# 输入数据
y=np.array([0],[1],[1],[1])
# 标签数据
# 目的是为了让网络发现逻辑门(前两列->标签),第三列是一个+b 偏置但后面又有偏置向量因此此处似乎冗余

神经元模型

neuron

关于偏置

image-20260305162236456

定义激活函数及其导数

1
2
3
4
5
6
7
8
9
10
def sigmoid(x):
'''
sigmoid 函数
'''
return 1./(1.+np.exp(-x))
def sigmoid_prime(x):
'''
sigmoid 函数导数
'''
return sigmoid(x)*(1.-sigmoid(x))

初始化神经网络参数–填充随机数的矩阵

1
2
3
4
5
6
7
8
9
10
11
12
13
def getweights(shape=()):
'''
生成随机权重矩阵
'''
#shape矩阵大小:()默认标量,(a,b)为 a*b矩阵
np.random.seed(seed=0)
#设置随机数种子,在一次工程中保持一致
return np.random.normal(loc=0.0,scale=0.001,size=shape,)
#生成正态分布的随机数
#loc 均值
#scale 标准差(设值小确保初始状态不爆炸)
#size=shape 生成填满矩阵的满足正态分布的数字
#逗号:多行规整更美观

定义网络结构

1
class Network(object):

首先定义构造函数(初始化方法)

  • 初始化方法范式

  • 1
    2
    3
    4
    class ClassName
    def __init__(self,parameter1,parameter2,...):
    self.attribute1=parameter1
    self.attribute2=parameter2
  • 如何理解这个self:简单理解为相当于其定义的变量为c中该对象的成员变量(c中显示的this指针)

  • lr=learning rate学习率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def __init__(self,lr=0.01):
'''
初始化网络结构
'''
self.W1=get_weights((3,4))
#输入层到隐藏层的权重矩阵
#此处对应的x是行格式(3列)
self.b1=get_weights((4))
#输入层到隐藏层的偏置
self.W2=get_weights((4,1))
#隐藏层到输出层的权重矩阵
self.b2=get_weights(1)
#隐藏层到输出层的偏置
self.lr=lr
#学习率

接下来定义实现传播算法

1
def backward(self,X_batch,y_batch):
反向传播算法
1
# ...见第二模块专门详解

结束对象,最后主函数

1
2
3
4
5
if __name__=="__main__":
net= Network(lr=10) #覆盖默认值
# 在100 个 batch 上进行100次反向传播训练
for i in range(100):
net.backward(X,y)

此处提到batch即一次训练送进去的数据,此处为4也即batch size(batch:批,次)

重磅理解:反向传播算法

算法框架

1
def backward(self,X_batch,y_batch):
反向传播算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#初始化计数器
batch_size=0 #初始化实际处理的个数
batch_loss=0 #初始化累计本批次样本所产生的总误差
batch_acc=0 #记录预测正确的样本个数 acc:accuracy

#grads:gradients 梯度
#此处顺序开始相反:反向意味(从输出往输入)
self.grads_W2=np.zeros_like(self.W2)
self.grads_b2=np.zeros_like(self.b2)
self.grads.W1=np.zeros_like(self.W1)
self.grads_b1=np.zeros_like(self.b1)
#zeros_like:创建一个形状完全一样但是全0的矩阵

#...计算梯度,见下

#梯度平均
self.grads_W2/=batch_size
self.grads_b2/=batch_size
self.grads_W1/=batch_size
self.grads_b1/=batch_size
#loss 平均
batch_loss/=batch_size
#准确率平均
batch_acc/=batch_size

#输出训练状态
print("loss:{} batch_acc{} batch_size{} lr:{}".format(batch_loss.batch_acc,batch_size,self.lr))

#更新参数
self.W2-=self.lr*self.grads_W2
self.b2-=self.lr*self.grads_b2
self.W1-=self.lr*self.grads_W1
self.b1-=self.lr*self.grads_b1

其中字符串格式化语法:

语法:"模式字符串".format(变量 1,变量 2,……)

组件:

  • {}占位符
  • .format把变量名按顺序填入占位符

计算梯度

注:zip打包函数:**zip(X_batch,y_batch)把两个序列对应的元素逐一打包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
for x,y in zip(X_batch,y_batch):
#forward 前向传播计算结果
z1=np.matmul(x,self.W1)+self.b1 #z^{L-1} 此处-1 是指输出层反向走 1 层
a1=sigmoid(z1) #a^{L-1}
#np.exp 可以实现此处的sigmoid掉了向量的每个函数
z2=np.matmul(a1,self.W2)+self.b2 #z^{L} 此处就是代表输出层
a2=sigmoid(z2) #a^{L}

##loss函数(有多种)……详见后解
#loss= #表示于预测值和真实标签的差距(差距可定义)
#delta_l= #隐藏层的误差项
#delta_L= #输出层的误差项,对于标量结果就是a2-y

#梯度累加
self.grads_W2+=np.array([a1*delta_L]).T
# .T 表示转置,a1*delta_L 表示对应逐个相乘
self.grads_b2+=delta_L
#此处由于偏置项系数为1,直接加上
self.grads_w1+=np.matmul(np.array([x]).T,[delta_l])
#前层输出*后层误差变成外积
self.grads_b1+=delta_l

#size、loss、acc 累加
batch_size+=1
batch_loss+=loss
batch_acc+=1 if(y==(1 if a2>0.5 else 0)) else 0

loss函数专讲(数学演绎)