Hi,你好。我是茶桁。
咱们接着上节课内容继续讲,我们上节课已经了解了拓朴排序的原理,并且简单的模拟实现了。我们这节课就来开始将其中的内容变成具体的计算过程。
linear, sigmoid
和loss
这三个函数的值具体该如何计算呢?
我们现在似乎大脑已经有了一个起比较模糊的印象,可以通过它的输入来计算它的点。
让我们先把最初的父类 Node 改造一下:
1 2 3 4 5 6 7
| class Node(): def __init__(self, inputs=[], name=None): ... self.value = None ...
|
然后再复制出一个,和Placeholder
一样,我们需要继承
Node,并且改写这个方法自己独有的内容:
1 2 3 4 5 6 7 8 9
| class Linear(Node): def __init__(self, x, k, b, name=None): Node.__init__(self, inputs=[x, k, b], name=name)
def forward(self): x, k, b = self.inputs[0], self.inputs[1], self.inputs[2] self.value = k.value * x.value + b.value print('我是{}, 我没有人类爸爸,需要自己计算结果{}'.format(self.name, self.value)) ...
|
我们新定义的这个类叫Linear
, 它会接收 x, k, b。它继承了
Node。这个里面的 forward
该如何计算呢?我们需要每一个节点都需要一个值,一个变量,因为我们初始化的时候接收的
x,k,b 都赋值到了 inputs
里,这里我们将其取出来就行了,然后就是线性方程的公式k*x+b
,赋值到它自己的
value 上。
然后接着呢,就轮到 Sigmoid 了,一样的,我们定义一个子类来继承
Node:
1 2 3 4 5 6 7 8 9 10 11 12
| class Sigmoid(Node): def __init__(self, x, name=None): Node.__init__(self, inputs=[x], name=name) self.x = self.inputs[0]
def _sigmoid(self, x): return 1/(1+np.exp(-x))
def forward(self): self.value = self._sigmoid(self.x.value) print('我是{}, 我自己计算了结果{}'.format(self.name, self.value)) ...
|
Sigmoid 函数只接收一个参数,就是 x,其公式为
1/(1+e^{-x}),我们在这里定义一个新的方法来计算,然后在 forward
里把传入的 x
取出来,再将其送到这个方法里进行计算,最后将结果返回给它自己的
value。
那下面自然是 Loss 函数了,方式也是一模一样:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Loss(Node): def __init__(self, y, yhat, name=None): Node.__init__(self, inputs = [y, yhat], name=name) self.y = self.inputs[0] self.yhat = self.inputs[1]
def forward(self): y_v = np.array(self.y.value) yhat_v = np.array(self.y_hat.value) self.value = np.mean((y.value - yhat.value) ** 2) print('我是{}, 我自己计算了结果{}'.format(self.name, self.value))
...
|
那我们这里定义成 Loss
其实并不确切,因为我们虽然喊它是损失函数,但是其实损失函数的种类也非常多。而这里,我们用的
MSE。所以我们应该定义为MSE
,不过为了避免歧义,这里还是沿用
Loss 好了。
定义完类之后,我们参数调用的类名也就需要改一下了:
1 2 3 4
| ... node_linear = Linear(x=node_x, k=node_k, b=node_b, name='linear') node_sigmoid = Sigmoid(x=node_linear, name='sigmoid') node_loss = Loss(y=node_y, yhat=node_sigmoid, name='loss')
|
好,这个时候我们基本完成了,计算之前让我们先看一下sorted_node
:
1 2 3 4 5 6 7 8 9 10
| sorted_node
--- [Placeholder: y, Placeholder: k, Placeholder: x, Placeholder: b, Linear: Linear, Sigmoid: Sigmoid, MSE: Loss]
|
没有问题,我们现在可以模拟神经网络的计算过程了:
1 2 3 4 5 6 7 8 9 10 11
| for node in sorted_nodes: node.forward()
--- 我是 x, 我已经被人类爸爸赋值为 3 我是 b, 我已经被人类爸爸赋值为 0.3737660632429008 我是 k, 我已经被人类爸爸赋值为 0.35915077292816744 我是 y, 我已经被人类爸爸赋值为 0.6087876106387002 我是 Linear, 我没有人类爸爸,需要自己计算结果 1.4512183820274032 我是 Sigmoid, 我没有人类爸爸,需要自己计算结果 0.8101858733432837 我是 Loss, 我没有人类爸爸,需要自己计算结果 0.04056126022042443
|
咱们这个整个过程就像是数学老师推公式一样,因为这个比较复杂。你不了解这个过程就求解不出来。
这就是为什么我一直坚持要手写代码的原因。c+v
大法确实好,但是肯定是学的不够深刻。表面的东西懂了,但是更具体的为什么不清楚。
我们可以看到,我们现在已经将 Linear、Sigmoid 和 Loss
都将值计算出来了。那我们现在已经实现了从 x 到 loss 的前向传播
现在我们有了
loss,那就又要回到我们之前机器学习要做的事情了,就是将损失函数 loss
的值降低。
之前咱们讲过,要将 loss
的值减小,那我们就需要求它的偏导,我们前面课程的求导公式这个时候就需要拿过来了。
然后我们需要做的事情并不是完成求导就好了,而是要实现「链式求导」。
那从 Loss 开始反向传播的时候该做些什么?先让我们把“口号”喊出来:
1 2 3 4 5 6 7
| class Node: def __init__(...): ... ... def backward(self): for n in self.inputs: print('获取∂{} / ∂{}'.format(self.name, n.name))
|
这样修改一下
Node,然后在其中假如一个反向传播的方法,将口号喊出来。
然后我们来看一下口号喊的如何,用[::-1]
来实现反向获取:
1 2 3 4 5 6 7 8 9 10
| for node in sorted_nodes[::-1]: node.backward()
--- 获取∂Loss / ∂y 获取∂Loss / ∂Sigmoid 获取∂Sigmoid / ∂Linear 获取∂Linear / ∂x 获取∂Linear / ∂k 获取∂Linear / ∂b
|
这样看着似乎不是太直观,我们再将 node
的名称加上去来看就明白很多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| for node in sorted_nodes[::-1]: print(node.name) node.backward() --- Loss 获取∂Loss / ∂y 获取∂Loss / ∂Sigmoid Sigmoid 获取∂Sigmoid / ∂Linear Linear 获取∂Linear / ∂x 获取∂Linear / ∂k 获取∂Linear / ∂b ...
|
最后的k, y, x, b
我就用...代替了,主要是函数。
那我们就清楚的看到,Loss 获取了两个偏导,然后传到了 Sigmoid,Sigmoid
获取到一个,再传到
Linear,获取了三个。那现在其实我们只要把这些值能乘起来就可以了。我们要计算步骤都有了,只需要把它乘起来就行了。
我们先是需要一个变量,用于存储 Loss 对某个值的偏导
1 2 3 4 5
| class Node: def __init__(...): ... self.gradients = dict() ...
|
然后我们倒着来看,先来看 Loss:
1 2 3 4 5 6 7
| class Loss(Node): ... def backward(self): self.gradients[self.inputs[0]] = '∂{}/∂{}'.format(self.name, self.inputs[0].name) self.gradients[self.inputs[1]] = '∂{}/∂{}'.format(self.name, self.inputs[1].name) print('[0]: {}'.format(self.gradients[self.inputs[0]])) print('[1]: {}'.format(self.gradients[self.inputs[1]]))
|
眼尖的小伙伴应该看出来了,我现在依然还是现在里面进行「喊口号」的动作。主要是先来看一下过程。
刚才每个 node 都有一个 gradients,它代表的是对某个节点的偏导。
现在这个节点 self 就是 loss,然后我们self.inputs[0]
就是
y, self.inputs[1]
就是 yhat,
也就是node_sigmoid
。那么我们现在这个self.gradients[self.inputs[n]]
其实就分别是∂loss/∂y
和∂loss/∂yhat
,我们把对的值分别赋值给它们。
然后我们再来看 Sigmoid:
1 2 3 4 5 6
| class Sigmoid(Node): ...
def backward(self): self.gradients[self.inputs[0]] = '∂{}/∂{}'.format(self.name, self.inputs[0].name) print('[0]: {}'.format(self.gradients[self.inputs[0]]))
|
我们依次来看哈,这个时候的 self 就是 Sigmoid
了,这个时候的sigmoid.inputs[0]
应该是 Linear
对吧,然后我们整个self.gradients[self.inputs[0]]
自然就应该是∂sigmoid/∂linear
。
我们继续,这个时候self.outputs[0]
就是 loss,
loss.gradients[self]
那自然就应该是输出过来的∂loss/∂sigmoid
,然后呢,我们需要将这两个部分乘起来:
1 2 3
| def backward(self): self.gradients[self.inputs[0]] = '*'.join([self.outputs[0].gradients[self], '∂{}/∂{}'.format(self.name, self.inputs[0].name)]) print('[0]: {}'.format(self.gradients[self.inputs[0]]))
|
接着,我们就需要来看看 Linear 了:
1 2 3 4 5 6 7
| def backward(self): self.gradients[self.inputs[0]] = '*'.join([self.outputs[0].gradients[self], '∂{}/∂{}'.format(self.name, self.inputs[0].name)]) self.gradients[self.inputs[1]] = '*'.join([self.outputs[0].gradients[self], '∂{}/∂{}'.format(self.name, self.inputs[1].name)]) self.gradients[self.inputs[2]] = '*'.join([self.outputs[0].gradients[self], '∂{}/∂{}'.format(self.name, self.inputs[2].name)]) print('[0]: {}'.format(self.gradients[self.inputs[0]])) print('[1]: {}'.format(self.gradients[self.inputs[1]])) print('[2]: {}'.format(self.gradients[self.inputs[2]]))
|
和上面的分析一样,我们先来看三个inputs[n]
的部分,self
在这里是 linear
了,这里的self.inputs[n]
分别应该是x, k, b
对吧,那么它们就应该分别是linear.gradients[x]
,
linear.gradients[k]
和linear.gradients[b]
,也就是∂linear/∂x
,∂linear/∂k
,
∂linear/∂b
。
那反过来,outputs
就应该反向来找,那么self.outputs[0]
这会儿就应该是
sigmoid。sigmoid.gradients[self]
就是前一个输出过来的∂loss/∂sigmoid * ∂sigmoid/∂linear
,
那后面以此的[1]和[2]我们也就应该明白了。
然后后面分别是∂linear/∂x
,∂linear/∂k
,
∂linear/∂b
。一样,我们将它们用乘号连接起来。
公式就应该是:
\[
\begin{align*}
\frac{\partial loss}{\partial sigmoid} \cdot \frac{\partial
sigmoid}{\partial linear} \cdot \frac{\partial linear}{\partial x} \\
\frac{\partial loss}{\partial sigmoid} \cdot \frac{\partial
sigmoid}{\partial linear} \cdot \frac{\partial linear}{\partial k} \\
\frac{\partial loss}{\partial sigmoid} \cdot \frac{\partial
sigmoid}{\partial linear} \cdot \frac{\partial linear}{\partial b} \\
\end{align*}
\]
那同理,我们还需要写一下Placeholder
:
1 2 3 4 5
| def Placeholder(Node): ... def backward(self): print('我获取了我自己的 gradients: {}'.format(self.outputs[0].gradients[self])) ...
|
好,我们来看下我们模拟的情况如何,看看它们是否都如期喊口号了,结合我们之前的前向传播的结果,我们一起来看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| for node in sorted_nodes: node.forward() for node in sorted_nodes[::-1]: print('\n{}'.format(node.name)) node.backward()
--- Loss [0]: ∂Loss/∂y [1]: ∂Loss/∂Sigmoid
Sigmoid [0]: ∂Loss/∂Sigmoid*∂Sigmoid/∂Linear
Linear [0]: ∂Loss/∂Sigmoid*∂Sigmoid/∂Linear*∂Linear/∂x [1]: ∂Loss/∂Sigmoid*∂Sigmoid/∂Linear*∂Linear/∂k [2]: ∂Loss/∂Sigmoid*∂Sigmoid/∂Linear*∂Linear/∂b
k 我获取了我自己的 gradients: ∂Loss/∂Sigmoid*∂Sigmoid/∂Linear*∂Linear/∂k
b 我获取了我自己的 gradients: ∂Loss/∂Sigmoid*∂Sigmoid/∂Linear*∂Linear/∂b
x 我获取了我自己的 gradients: ∂Loss/∂Sigmoid*∂Sigmoid/∂Linear*∂Linear/∂x
y 我获取了我自己的 gradients: ∂Loss/∂y
|
好,观察下来没问题,那我们现在还剩下最后一步。就是将这些口号替换成真正的计算的值,其实很简单,就是将我们之前学习过并写过的函数替换进去就可以了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Linear(Node): ... def backward(self): x, k, b = self.inputs[0], self.inputs[1], self.inputs[2] self.gradients[self.inputs[0]] = self.outputs[0].gradients[self] * k.value self.gradients[self.inputs[1]] = self.outputs[0].gradients[self] * x.value self.gradients[self.inputs[2]] = self.outputs[0].gradients[self] * 1 ...
class Sigmoid(Node): ... def backward(self): self.value = self._sigmoid(self.x.value) self.gradients[self.inputs[0]] = self.outputs[0].gradients[self] * self.value * (1 - self.value) ...
class Loss(Node): ... def backward(self): y_v = self.y.value yhat_v = self.y_hat.value self.gradients[self.inputs[0]] = 2*np.mean(y_v - yhat_v) self.gradients[self.inputs[1]] = -2*np.mean(y_v - yhat_v)
|
那我们来看下真正计算的结果是怎样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| for node in sorted_nodes[::-1]: print('\n{}'.format(node.name)) node.backward()
--- Loss ∂Loss/∂y: -0.402796525409167 ∂Loss/∂Sigmoid: 0.402796525409167
Sigmoid ∂Sigmoid/∂Linear: 0.06194395247945269
Linear ∂Linear/∂x: 0.02224721841122111 ∂Linear/∂k: 0.18583185743835806 ∂Linear/∂b: 0.06194395247945269
y gradients: -0.402796525409167
k gradients: 0.18583185743835806
b gradients: 0.06194395247945269
x gradients: 0.02224721841122111
|
好,到这里,我们就实现了前向传播和反向传播,让程序自动计算出了它们的偏导值。
不过我们整个动作还没有结束,就是我们需要将 loss
降低到最小才可以。
那我们下节课,就来完成这一步。