核心想法其实始终未变。从我们在学校学习如何求导时, 就应该知道这一点了。如果我们能够追踪最终求出标量输出的计算, 并且我们知道如何对简单操作求导 (例如加法、乘法、幂、指数、对数等等), 我们就可以算出输出的梯度。
假设我们有一个线性的中间层f,由矩阵乘法表示(暂时不考虑偏置):
为了用梯度下降法调整w值,我们需要计算梯度∂l/∂w。这里我们可以观察到,改变y从而影响l是一个关键。
每一层都必须满足下面这个条件: 如果给出了损失函数相对于这一层输出的梯度, 就可以得到损失函数相对于这一层输入(即上一层的输出)的梯度。
现在应用两次链式法则得到损失函数相对于w的梯度:
相对于x的是:
因此, 我们既可以后向传递一个梯度, 使上一层得到更新并更新层间权重, 以优化损失, 这就行啦!
动手实践
先来看看代码, 或者直接试试Colab Notebook:
https://colab.research.google.com/github/eisenjulian/slides/blob/master/NN_from_scratch/notebook.ipynb
我们从封装了一个张量及其梯度的类(class)开始。
现在我们可以创建一个layer类,关键的想法是,在前向传播时,我们返回这一层的输出和可以接受输出梯度和输入梯度的函数,并在过程中更新权重梯度。
然后, 训练过程将有三个步骤, 计算前向传递, 然后后向传递, 最后更新权重。这里关键的一点是把更新权重放在最后, 因为权重可以在多个层中重用,我们更希望在需要的时候再更新它。
- class Layer:
- def __init__(self):
- self.parameters = []
-
- def forward(self, X):
- """
- Override me! A simple no-op layer, it passes forward the inputs
- """
- return X, lambda D: D
-
- def build_param(self, tensor):
- """
- Creates a parameter from a tensor, and saves a reference for the update step
- """
- param = Parameter(tensor)
- self.parameters.append(param)
- return param
-
- def update(self, optimizer):
- for param in self.parameters: optimizer.update(param)
标准的做法是将更新参数的工作交给优化器, 优化器在每一批(batch)后都会接收参数的实例。最简单和最广为人知的优化方法是mini-batch随机梯度下降。
- class SGDOptimizer():
- def __init__(self, lr=0.1):
- self.lr = lr
-
- def update(self, param):
- param.tensor -= self.lr * param.gradient
- param.gradient.fill(0)
在此框架下, 并使用前面计算的结果后, 线性层如下所示:
- class Linear(Layer):
- def __init__(self, inputs, outputs):
- super().__init__()
- tensor = np.random.randn(inputs, outputs) * np.sqrt(1 / inputs)
- selfself.weights = self.build_param(tensor)
- selfself.bias = self.build_param(np.zeros(outputs))
-
- def forward(self, X):
- def backward(D):
- self.weights.gradient += X.T @ D
- self.bias.gradient += D.sum(axis=0)
- return D @ self.weights.tensor.T
- return X @ self.weights.tensor + self.bias.tensor, backward
(编辑:ASP站长网)
|