Skip to content
BaiRuic
Go back

动手写NumpyNN FNN

NumpyNN 系列导航
  1. 动手写NumpyNN FNN ← 当前
  2. 动手写NumpyNN CNN
  3. 动手写NumpyNN RNN

这是一个使用 NumPy 动手实现轻量神经网络库的系列,记录实现过程中学习到的神经网络基础知识。

碎碎念:习惯了用PyTorch写神经网络模型,每一次按下 nn. 都觉得不踏实。所以趁此假期用numpy来写一个轻巧的神经网络库,暂且命名为NumpyNN,托管于GitHub。一方面巩固一下神经网络基础知识,另一方面规范一下Python代码风格,还能顺便学一些Python的的高级语法糖。此文记录该过程中学到的一些琐碎知识点,不断更新…

因为多种因素,项目目前仍在开发中。

一个简单的全连接层即为对输入 XX 做一次仿射变换:

Y=X@W+bY = X@W+b

其中,W,bW,b 为该层参数,XX为该全连接层输入,YY为输出。

Numpy实现如下:

def forward(X):
    return X @ w + b

W和b为需要初始化的参数,且在训练过程中,通过反向传播来更新。那么反向传播如何更新参数,已经反向传播如何进行?

以下图为例,下图展示了一个典型两层的神经网络。其中黑线表示正向传播,红线表示反向传播。

NumpyNN - Untitled

正向传播:

初始输入 xx 经过第一层全连接层得到 z1z_1,再经过激活函数得到 a1a_1。a1a_1 作为第二层网络的输入,同样经过一个全连接层 和一个激活函数得到了 a2a_2,a2a_2 即为网络的输出,即 xx 的预测值,最后 预测值 a2a_2 和真实值 yy 经过损失函数得到 损失值 lossloss.

反向传播:

反向传播的本质是计算损失值 lossloss 对参数 W1,b1,W2,b2W_1,b_1,W_2, b_2 的偏导,因为参数是通过梯度下降的方式更新的,用到了偏导,例如参数 W2W2 的更新过程如下:

计算偏导可以通过偏微分的定义求得,但是当神经网络层数较多时,按定义的计算复杂度会很高,所以一般不会采用这种方式,而是通过基于链式法则的计算图来计算,即通常所说的反向传播。

在基于链式法则的计算图中,偏导计算方式如下:

∂  loss∂  W2=∂  loss∂  a2@∂  a2∂  z2@∂  z2∂  W2\begin{align*}&\frac{\partial\; \text{loss}}{\partial\; W_2} = \frac{\partial\;\text{loss}}{\partial\;a_2}@\frac{\partial\;a_2}{\partial\;z_2}@\frac{\partial\;z_2}{\partial\;W_2} \end{align*}

注:以上表达式是错误的,矩阵求导不同于标量求导,这里只是为了传达链式法则的精髓!

可见,在对 W2W_2 求偏导时,用到了 lossloss 对该层输出值 a2a_2 的偏导,即 lossa2\frac{loss}{a_2}.

因此,反向传播中,从后往前传递的是 lossloss 对 该层输出值偏导。然后在该层内部,可以利用传递回来的偏导计算得到该层参数的偏导,同时,还需要把 loss 对该层输入(即前一层输出)的偏导传递到前一层,供前一层计算。代码如下:

def backward(self, pre_grad):
    dw = X.T @ pre_grad
    db = np.mean(pre_grad, axis=0)
    return pre_grad @ W.T

其中per_grad 为 loss 对 该层输出的偏导。dw, db 为该层参数的偏导,需要先保存下完,等反向传播完成后,更新一次参数。return 值为loss对该层输入(即上一层输出)偏导,要传递给上一层,作为上一层神经网络的pre_grad参数。

到此,一个简单的全连接层即可搭建完成,完整代码如下:

class Linear:
    def __init__(self, in_features, out_features):
        self.w = None
        self.b = None
        self.dw = None
        self.b = None

        self.in_features = in_features
        self.out_features = out_features
        self._init_params()
    
    def _init_params(self):
        mu = 0
        sigma = np.sqrt(2 / (self.in_features + self.out_features))
        self.w = np.random.normal(mu, sigma, (self.in_features, self.out_features))
        self.b = np.zeros((1, self.out_features))
        
        
    def forward(self, input):
        self.input = input
        return input @ self.w + self.b

    def backward(self, pre_grad):
        self.dw = self.input.T @ pre_grad
        self.db = np.mean(pre_grad, axis=0)
        return pre_grad @ self.W.T
    
    def __call__(self, *args, **kwargs):
        return self.forward(*args, **kwargs)