主要学习的是误差反向传播算法和实现,总的来说在算法推导上卡的好久,卡的最久的是数学hh,上学期没学线代,以及没学习高数下,导致许多数学证明看的有些吃力,基本情况就像是“我看懂了这里要求导,以及这个求导结果怎么用,但是怎么算这个的导数我不会算”,这很大程度地影响了我的阅读的连续性,让我看着很难受,不过在Gemini之神的帮助下还是逐渐理解了。

此处贴出ai对于Softmax with loss层的反向传播算法的数学推导,看到最后只觉得惊为天人,再次领略到了数学的简洁美

数学推导简述

如果你想知道这个 $y_k - t_k$ 是怎么来的,可以看这三个步骤:

第一步:计算 Softmax 的导数

  • 当 $i=j$ 时:$\frac{\partial y_i}{\partial a_i} = y_i(1-y_i)$
  • 当 $i \neq j$ 时:$\frac{\partial y_i}{\partial a_j} = -y_i y_j$

第二步:计算 Loss 对 $y$ 的导数

  • $\frac{\partial L}{\partial y_i} = -\frac{t_i}{y_i}$

第三步:链式法则合并
对第 $j$ 个输入 $a_j$ 求导:
$$\frac{\partial L}{\partial a_j} = \sum_{i} \frac{\partial L}{\partial y_i} \frac{\partial y_i}{\partial a_j}$$
代入上面的结果:
$$\frac{\partial L}{\partial a_j} = \left( -\frac{t_j}{y_j} \cdot y_j(1-y_j) \right) + \sum_{i \neq j} \left( -\frac{t_i}{y_i} \cdot (-y_i y_j) \right)$$
简化后:
$$\frac{\partial L}{\partial a_j} = -t_j + t_j y_j + \sum_{i \neq j} t_i y_j$$
$$\frac{\partial L}{\partial a_j} = y_j (\sum_{i} t_i) - t_j$$
因为 one-hot 标签之和 $\sum t_i = 1$,所以:
$$\frac{\partial L}{\partial a_j} = y_j - t_j$$


关于分层实现的神经网络

​ 上次的二层神经网络搭建是一个在搭建网络过程中实现的每层的计算与参数传递,而这次,通过对不同类型的层进行对应类的设计,利用多态实现了for循环进行计算,层自主存储参数,层可根据规划任意叠加等效果,整体上代码更解耦了,这是好的。就是鱼书先教层设计再教分层实现的反向传播,而不先教一遍不考虑层设计的反向传播,导致我读到最后才发现,反向传播实现其实就那一点,更多的设计都在为更好地保存和传递参数而做工作,虽然这是正确的思路,但是我提前不知道的,导致在看本章的时候一脸问号,看了好久都不知道这和上章是什么关系(

以下贴出分层神经网络的实现,我是在之前做的上面进行改动的,个人感觉自己注释还是挺详细的,当时写的时候就是为了理清思路写的注释,希望能对看博客的人有帮助

整体对层的调用

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import sys,os
sys.path.append(os.pardir)
import numpy as np
from common.functions import *
from common.gradient import numerical_gradient
from collections import OrderedDict
from common_layer import *
#相比于先前,此类要实现可叠加层,需要用到有序字典
"""
二层训练网络所需内容:
Params
1、每一层之间的权重与偏置的存放
Grads
2、每一次之间的权重与偏置的梯度的计算与存储

人为输入:每一层的大小,一个人为设定权重初始化的标准差以减小权重分布
问ai学到,不乘这个标准差,直接获得权重分布的方法叫 朴素随机初始化
如果乘了这个人为固定的标准差,则叫 缩放初始化
除此之外还有较为高级的初始化方法(对weight_init_std进行函数处理)
如针对sigmoid/tanh函数的Xavier/Glorot初始化(理论最优)
或者针对ReLU激活函数的He初始化

类的编写思路:
1、初始化出权重和偏置
2、写正常的预测网络
3、通过预测网络求交叉熵(损失函数)
4、将交叉熵函数传进梯度计算
5、返回梯度
6、额外可加个计算预测的准确率的(不过有损失函数在了也可反应,加上了给人看更好)
"""
class two_layer_net:
def __init__(self,input_size,hide_size,out_size,weight_init_std = 0.01):
self.params = {}
self.params["w1"] = np.random.randn(input_size,hide_size)*weight_init_std
self.params["b1"] = np.zeros(hide_size)
self.params["w2"] = weight_init_std * np.random.randn(hide_size,out_size)
self.params["b2"] = np.zeros(out_size)

#初始化层,按顺序正向添加
self.layers = OrderedDict() #创建有序字典
self.layers["Affine1"] = Affine(self.params["w1"],self.params["b1"])
self.layers["ReLu"] = ReLU_Layer()
self.layers["Affine2"] = Affine(self.params["w2"],self.params["b2"])
self.last_layer = Softmax_With_Loss()
#最后一层一般是固定的,则单独初始化

#预测网络只需要初始数据,权重,偏置即可,后二者类自带,则只需考虑初始数据,计算出预测值返回即可
def prediction(self,x):
xx = x #不去改变初始值
#由于初始化时给过权重与偏置了,又用了有序字典,则此处可用高贵的循环了hhh
for layer in self.layers.values(): #这一步就是通过有序字典获取所有层的字典集合(大概这个意思),并挨个取出每个layer
xx = layer.forward(xx)

return xx
#由于每种层的前向计算都对应做了实现,且都叫forward,则可直接调用
#哎呀这不是我们虚函数嘛(),多态太神奇了!

#计算损失函数,需要初始数据x和监测数据t,返回交叉熵误差
def loss(self,x,t):
y = self.prediction(x)
L = self.last_layer.forward(y,t)#这里把softmax和交叉熵一起计算了
return L

#正向传播梯度计算,需要交叉熵函数,x,t
def gradient_caculation(self,x,t):
#搞个临时函数来方便传入格式与函数要求相同
tmp_f = lambda w: self.loss(x,t)
#梯度计算
grads = {} #不能理解python都如此简化了,这里还需要初始化一下
grads["w1"] = numerical_gradient(tmp_f,self.params["w1"])
grads["w2"] = numerical_gradient(tmp_f,self.params["w2"])
grads["b1"] = numerical_gradient(tmp_f,self.params["b1"])
grads["b2"] = numerical_gradient(tmp_f,self.params["b2"])

return grads

#这里直接粘了教材示例,仅作理解,用到了argmax,取了第1维度,即按列搜索或者跨行搜索,返回每一行最大数的索引
def accuracy(self, x, t):
y = self.prediction(x)
y = np.argmax(y, axis=1) #取了预测值,每行最大值对应的索引即预测的值
if t.ndim != 1 : t = np.argmax(t, axis=1) #监测值,one-hot数据下应当正确的那个值为1,故一定是那一行最大
#上面代码对one-hot数据进行了取最大值索引的操作,当然如果直接给索引就不用操作了,故加if
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy


#进行误差反向传播梯度的计算
def gradient(self,x,t):
#前向先把每层的数据进行对应赋值,这在类内部实现
self.loss(x,t)

#反向传播
dout = 1 #人为定义dout,网络的终点如果不加花活一般都是1

#先计算soft_with_loss的反向
dout = self.last_layer.backward(dout)
#取出其余层的字典
layers = list(self.layers.values())
layers.reverse() #因为要逆序执行反向backward则反向一下,对于使用了有序字典的python来说是好的,也可记录数量来正序/逆序逐个执行,效果一样的
for layer in layers:#挨个查找layers字典,将每一层的backward执行了
dout = layer.backward(dout)

#传出计算出的梯度
grads = {}
grads['w1'] = self.layers['Affine1'].dw
grads['b1'] = self.layers['Affine1'].db
grads['w2'] = self.layers['Affine2'].dw
grads['b2'] = self.layers['Affine2'].db

return grads

各种层的实现

该说不说,目前虽然将加法,乘法,affine,soft_with_loss层都实现了,但实际上只使用了后三者。

这个的实现基本都是对教材的拙劣模仿,不过有些感觉不能直接修改传入参数的值,就copy了一下再进行计算

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import numpy as np
import sys,os
sys.path.append(os.pardir)
from common.functions import *
#参照示例学会了如何导入父目录下不同文件夹的东西,python真好用

class MulLayer: #得先用前向传播把x,y存储了才能用反向传播求导数
def __init__(self):
self.x = None
self.y = None

def forward(self,x,y):
self.x = x
self.y = y
return x*y
def backward(self,dout):
dx = dout*self.y
dy = dout*self.y
return dx,dy

class AddLayer:#反向传播为直接传出,没什么好说的
def __init__(self):
pass

def forward(self,x,y):
return x+y

def backward(self,dout):
dx = dout*1
dy = dout*1
return dx,dy

class ReLU_Layer:
def __init__(self):
self.mask = None #初始化一个数组,后续当布尔值数组使用,确实有“掩码”的感觉

def forward(self,x):
self.mask = (x<=0)
out = x.copy()
out[self.mask] = 0 #将self.mask中true值对应的索引在out中的位置的数据赋值为0,其余保持原样,符合ReLU,x or 0的特征
return out

def backward(self,dout): #对ReLU求导也是,正向传入的x对应大于0则导数反向原样传递,小于等于0则导数也为0
dx = dout.copy() #相比于示例采用了先copy再修改的方法,不会对传入值进行修改,个人认为更好
dx[self.mask] = 0
return dx

class sigmoid_layer:
#sigmoid层思路如下 x ---> sigmoid ---> y --->后续
#对应导数的传递(简化) (dL/dy 即dout)*y*(1-y) <--- dL/dy(dout)
def __init__(self):
self.out = None #便于保存sigmoid传出值y用于计算

def forward(self,x):
out = 1/(1+np.exp(-x))
self.out = out
return out

def backward(self,dout):
dx = dout*self.out*(1-self.out)
return dx

class Affine:
def __init__(self,w,b):
self.w = w
self.b = b
self.dw = None
self.db = None
self.x = None

def forward(self,x):
self.x = x
out = np.dot(x,self.w) + self.b
return out

def backward(self,dout):
dx = np.dot(dout,self.w.T)
self.dw = np.dot(self.x.T,dout)
self.db = np.sum(dout,axis=0) #由于偏置广播加给了每个Y数据,故将传入导数的每个1*M的数据求和视作db(直观理解,可更严谨地数学证明)
return dx

class Softmax_With_Loss:
def __init__(self):
self.loss = None
self.y = None #softmax函数的输出
self.t = None #监测数据,one-hot形式

def forward(self,x,t):
self.t = t
self.y = softmax(x)
self.loss = cross_entropy_error(self.y,self.t)
return self.loss

def backward(self, dout=1): #给出一个默认值
batch_size = self.t.shape[0]
dx = (self.y - self.t) / batch_size
return dx

训练的程序没有修改,就把计算梯度的方法换成了反向传播的方法,故不再贴出

训练效果

图的链接好像老是炸,后续有空可以改改博客,换个和国内链接好的平台部署可能好点?

视频更是别想传了,这里贴出图片,真炸了就炸了

训练过程中损失函数的变化