神经网络的实现

183 阅读3分钟

「这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战」。

在前文中对正向与反向传播的具体过程中,我们发现了有一个很重要的现象,就是它的一致性。尽管先前我们初步的实现了它的过程。但是仍然不够简洁,比较繁琐。如果我们需要自行定义网络结构,这个过程过程会比较繁琐。

对先前我们推导的网络,可以发现的是,每一层之间相对来说是比较独立的,尽管在前向和反向传播的过程中需要前一层和后一层的数据。 这里考虑一种统一的方式来完成神经网络的层的设计。考虑如下的接口形式:

  • forward(x: Tensor)
  • backward(partial: Tensor)

对于每一层我们都可以使用这两个函数来完成传播的过程。考虑对线性层的实现,首先需要考虑的是特征的维度和处理之后的维度。这会涉及到数据的储存方式,此处姑且以每行为特征进行实现。那么权值矩阵是 (n×m)( n \times m ) 而偏置是 (1×m)(1 \times m) 。这表示我们这一个线性层有 m 个神经元。

还有一个要考虑的点在于输入数据的保存,外部和层内部都是可以的,个人倾向于保存在内部,这样内部的细节可以隐藏。

所以最终线性层实现如下

class Linear:

    def __init__(self, in_features: int, out_features: int, need_bias: bool = True):  
        # 全连接层的输出维度就是它的神经元个数
        self.weight: Tensor = np.random.randn(in_features, out_features)  # in_features x out_features
        if need_bias:
            self.bias: Tensor = np.random.randn(1, out_features)
        self.x = np.array([])  # store data input

    def forward(self, x: Tensor):
        self.x = x  # r x in_features
        return self.x @ self.weight + self.bias

    def backward(self, partial: Tensor):  # partial is r x out_features
        weight = self.weight.copy()
        self.weight = self.weight + (self.x.T @ partial) / partial.shape[0]
        self.bias = self.bias + partial.sum(0) / partial.shape[0]  # reduce on row
        return partial @ weight.T

这里把输入 x 保存在了自身,反向传播时,利用此处的 x 求出偏导。这里的除法是为了将偏导平均为单个样本的值。

现在来考虑激活函数的实现,它比线性层简单的多,因为无论是数据在它上面的正向传播还是反向传播,都不会具体的影响到维度。唯一要点在于如何实现它的偏导求解。对于最常用的两个激活函数,Sigmoid 和 Relu都可以比较轻松的求解。而且 Relu 函数的导数更加简单,只有 0 和 1 两个部分。 所以可以像如下方式实现

class Sigmoid:
    def __init__(self):
        self._y = np.array([])

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

    def backward(self, partial: Tensor):
        return self._y * (1 - self._y) * partial

class Relu:
    def __init__(self):
        self._x = np.array([])

    def forward(self, x: Tensor):
        self._x = x
        return np.maximum(0, x)

    def backward(self, partial: Tensor):
        grad = np.zeros_like(self._x)
        grad[self._x >= 0] = 1 
        return grad * partial

有了这两个层,我们已经可以搭建一个简单的神经网络了,为了更好的体验,我们将神经网络可以再做一个封装。

class NN:
    def __init__(self, *layer, loss=False, lr=0.001):
        self.layers = layer
        self.lr = lr

    def train_step(self, x: Tensor, labels: Tensor):
        # x /= x.shape[0]
        for i in self.layers:
            x = i.forward(x)
        partial_loss = -(x-labels) # 返回负的梯度
        loss = 1/2 * (y - y_hat) ** 2
        for i in reversed(self.layers):
            partial_loss = i.backward(0.01 * partial_loss)
        return loss.sum()

这里把每一层保存在了一个列表里,这样遍历一次列表即可完成数据的前向传播,然后是计算损失偏导并传播,这里默认是做回归了,所以使用了平方误差。有必要的话,也可以再封装或者修改,使得它更具有灵活性。 有了这个神经网络结构,我们使用神经网络就可以像如下的方式定义:

net = NN(Linear(2, 2), Sigmoid(), Linear(2, 2), Sigmoid(), Linear(2, 1), Sigmoid(), lr=0.1)

这样我们就可以既可以更清晰的感受神经网络的结构,也可以更专注于建模了。