「这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战」。
在前文中对正向与反向传播的具体过程中,我们发现了有一个很重要的现象,就是它的一致性。尽管先前我们初步的实现了它的过程。但是仍然不够简洁,比较繁琐。如果我们需要自行定义网络结构,这个过程过程会比较繁琐。
对先前我们推导的网络,可以发现的是,每一层之间相对来说是比较独立的,尽管在前向和反向传播的过程中需要前一层和后一层的数据。 这里考虑一种统一的方式来完成神经网络的层的设计。考虑如下的接口形式:
forward(x: Tensor)backward(partial: Tensor)
对于每一层我们都可以使用这两个函数来完成传播的过程。考虑对线性层的实现,首先需要考虑的是特征的维度和处理之后的维度。这会涉及到数据的储存方式,此处姑且以每行为特征进行实现。那么权值矩阵是 而偏置是 。这表示我们这一个线性层有 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)
这样我们就可以既可以更清晰的感受神经网络的结构,也可以更专注于建模了。