随着深度学习的火热,越来越多的领域,越来越多的人,开始使用深度学习。当然,你可以使用现有的深度学习框架如tf和torch。然而,这些框架都是高度封装的,读者虽然用着好用,但是这对于想深入理解神经网络的读者不是最好的选择。因此,有一些想要深入理解神经网络的读者就想要使用源代码对神经网络进行复现。
很多读者在网上寻找手撕神经网络的源代码,却发现很多都是面向过程的:定义一堆函数,函数与函数之间相互交叉调用,然后使用一个非常冗长的函数把他们组合,然后放一堆代码进行训练。。。这对新手十分不友好。
如果你对后文中反向传播和计算图理解起来有困难,可以看我的这篇文章:https://blog.csdn.net/weixin_57005504/article/details/126159919?spm=1001.2014.3001.5501
我们知道,python是一门面向对象编程的语言,因此,我们希望能够利用这一点,将神经网络中的各个组件封装成一个类,然后将这些类拼装,就成了我们的神经网络,就像搭建积木一样。
什么是Affine层?所谓的Affine层就是,接受一个输入X,输出 X W + B XW+B XW+B的神经网络层。因此,该层需要具有属性w,b。
不要忘了,我们上面所讨论的仅仅是该层的正向传播的输出,该层在反向传播时,还需要能够输出对W和B的梯度,同时,我们在后面的梯度下降的过程中还希望能够获取这两个梯度,以更新权重,所以还要有属性dW,dB,因此,我们对该类的初始化代码:
class Affine:
def __init__(self, w, b):
self.w = w
self.b = b
self.x = None
self.dw = None
self.db = None
前向传播的代码相当简单,只需要完成 X W + B XW+B XW+B的计算即可:
def forward(self, x):
"""x是n行 一维行向量组成的批"""
self.x = x
out = np.dot(x, self.w) + self.b
return out
该方法接受一个后面一层反向传播过来的梯度dout,通过以下公式完成对dW,dB的梯度的计算以及反向传播:
∂ L o s s ∂ W = X T ⋅ d o u t \frac{\partial Loss}{\partial W}=X^T·dout ∂W∂Loss=XT⋅dout
∂ L o s s ∂ B = ∑ d o u t \frac{\partial Loss}{\partial B}=\sum dout ∂B∂Loss=∑dout
所以,代码如下:
def backward(self, dout):
dx = dout.dot(self.w.T)
self.dw = np.dot(self.x.T, dout)
self.db = np.sum(dout, axis=0)
return dx
首先,需要明确sigmoid函数:
s i g m o i d ( x ) = 1 1 + e − x sigmoid(x)=\frac{1}{1+e^{-x}} sigmoid(x)=1+e−x1
该函数有一个很好的性质:
设 y = s i g m o i d ( x ) y=sigmoid(x) y=sigmoid(x),则有 d y d x = y ( 1 − y ) \frac{dy}{dx}=y(1-y) dxdy=y(1−y)。
因此,我们发现,该层的正向输出 y y y在计算梯度中也可以巧妙地用到,所以,对于输出 y y y,我们可将其添加到该类的属性,以便前向传播的结果在反向传播中也能方便的用到,故其初始化的代码:
class Sigmoid:
def __init__(self):
self.out = None
直接带入 s i g m o i d sigmoid sigmoid函数计算即可:
def forward(self, x):
out = 1 / (1 + np.exp(-x))
self.out = out
return out
如前所述, d y d x = y ( 1 − y ) \frac{dy}{dx}=y(1-y) dxdy=y(1−y)。则有:
def backward(self, dout):
dx = dout * (1.0 - self.out) * self.out
return dx
该层和Sigjoid层类似,也是一个激活层,这里对于代码的讲解不再赘述:
class ReLU:
def __init__(self, ):
self.out = None
def forward(self, x):
mask = (x < 0)
self.mask = mask
out = x.copy()
out[mask] = 0
return out
def backward(self, dout):
dout[self.mask] = 0
return dout
对于回归问题,一个经典的 L o s s Loss Loss 函数是选用均方误差计算:
L o s s = 1 n ∑ i = 1 n ( y ^ − y l a b e l ) 2 Loss=\frac{1}{n}\sum_{i=1}^{n} (\hat y-y_{label})^2 Loss=n1i=1∑n(y^−ylabel)2
其中, y ^ \hat y y^是网络的预测值,而 y l a b e l y_{label} ylabel是标签。
求导可得:
∂ L o s s ∂ y ^ = 1 n ( y ^ − y l a b e l ) \frac{\partial Loss}{\partial \hat y}=\frac{1}{n}(\hat y -y_{label}) ∂y^∂Loss=n1(y^−ylabel)
你可能好奇,为什么是 1 n \frac{1}{n} n1,而不是 2 n \frac{2}{n} n2呢?这是因为在后面使用梯度下降法更新权重的时候,还需要乘以一个学习率 α \alpha α,所以这里分子是1和2没有区别,而我们再写代码时,我们习惯分子是1.
从上面的分析可知,我们的 y ^ , y l a b e l \hat y ,y_{label} y^,ylabel 在正向传播和反向传播中都出现了,所以为了方便传递,我们将此二者添加到类的属性中:
class MSELoss:
"""均方根误差层"""
def __init__(self):
self.pre = None
self.y_true = None
self.loss = None
def forward(self, pre, y_true):
self.pre = pre
m, n = self.pre.shape
self.y_true = y_true.reshape(m, n)
assert self.pre.shape == self.y_true.shape
return np.mean(np.power((pre - y_true), 2)) # loss 标量
def backward(self, dout, ):
return dout * (self.pre-self.y_true)
时间和经理有限,对于文章中有的细节,我后面会逐渐更新上去,所以欢迎收藏和关注我的文章!
——by 神采的二舅