正向计算的实例

假设有一个函数:

z = x \cdot y \tag{1}

其中:

x = 2w + 3b \tag{2}

y = 2b + 1 \tag{3}

  1. def target_function(w,b):
  2. x = 2*w+3*b
  3. y = 2*b+1
  4. z = x*y
  5. return x,y,z

计算图如图2-4。

线性反向传播 - 图1

注意这里x, y, z不是变量,只是计算结果。w, b是才变量。因为在后面要学习的神经网络中,要最终求解的目标是w和b的值,所以在这里先预热一下。

当w = 3, b = 4时,会得到图2-5的结果。

线性反向传播 - 图2

最终的z值,受到了前面很多因素的影响:变量w,变量b,计算式x,计算式y。

反向传播求解w

求w的偏导

目前的z=162,如果想让z变小一些,比如目标是z=150,w应该如何变化呢?为了简化问题,先只考虑改变w的值,而令b值固定为4。

如果想解决这个问题,最笨的办法是可以在输入端一点一点的试,把w变成3.5试试,再变成3试试……直到满意为止。现在我们将要学习一个更好的解决办法:反向传播。

从z开始一层一层向回看,图中各节点关于变量w的偏导计算结果如下:

因为z = x \cdot y,其中x = 2w + 3b,y = 2b + 1

所以:

\frac{\partial{z}}{\partial{w}}=\frac{\partial{z}}{\partial{x}} \cdot \frac{\partial{x}}{\partial{w}}=y \cdot 2=18 \tag{4}

其中:

\frac{\partial{z}}{\partial{x}}=\frac{\partial{}}{\partial{x}}(x \cdot y)=y=9

\frac{\partial{x}}{\partial{w}}=\frac{\partial{}}{\partial{w}}(2w+3b)=2

线性反向传播 - 图3

图2-6其实就是链式法则的具体表现,z的误差通过中间的x传递到w。如果不是用链式法则,而是直接用z的表达式计算对w的偏导数,会是什么样呢?我们来试验一下。

根据公式1、2、3,我们有:

z=x \cdot y=(2w+3b)(2b+1)=4wb+2w+6b^2+3b \tag{5}

对上式求w的偏导:

{\partial z \over \partial w}=4b+2=4 \cdot 4 + 2=18 \tag{6}

公式4和公式6的结果完全一致!所以,请大家相信链式法则的科学性。

求w的具体变化值

公式4和公式6的含义是:当w变化一点点时,z会发生w的变化值的18倍的变化。记住我们的目标是让z=150,目前在初始状态时是162,所以,问题转化为:当需要z从162变到150时,w需要变化多少?

既然:

\Delta z = 18 \cdot \Delta w

则:

\Delta w = {\Delta z \over 18}={162-150 \over 18}= 0.6667

所以:

w = w - 0.6667=2.3333

x=2w+3b=16.6667

z=x \cdot y=16.6667 \times 9=150.0003

我们一下子就成功地让z值变成了150.0003,与150的目标非常地接近,这就是偏导数的威力所在。

  1. def back_propagation_for_w(w, b, t):
  2. '''
  3. 反向传播求解w
  4. :param w: 权重w
  5. :param b: 权重b
  6. :param t: 目标值t
  7. :return:
  8. '''
  9. print("\nback_propagation_for_w ----- \n")
  10. error = 1e-5
  11. count = 1
  12. while(True):
  13. x, y ,z = target_function(w, b)
  14. delta_z = z - t
  15. print("w=%f,b=%f,z=%f,delta_z=%f" % (w, b, z, delta_z))
  16. if abs(delta_z) < error:
  17. break
  18. # 偏z偏x=y
  19. # 偏x偏w=2
  20. partial_w = y * 2
  21. delta_w = delta_z / partial_w
  22. w = w - delta_w
  23. count = count + 1
  24. print("done!\ntotal iteration times = %d" % count)
  25. print("final w = %f" % w)

反向传播求解b

求b的偏导

这次我们令w的值固定为3,变化b的值,目标还是让z=150。同上一小节一样,先求b的偏导数。

注意,在上一小节中,求w的导数只经过了一条路:从z到x到w。但是求b的导数时要经过两条路,如图2-7所示:

  1. 从z到x到b
  2. 从z到y到b

线性反向传播 - 图4

从复合导数公式来看,这两者应该是相加的关系,所以有:

\frac{\partial{z}}{\partial{b}}=\frac{\partial{z}}{\partial{x}} \cdot \frac{\partial{x}}{\partial{b}}+\frac{\partial{z}}{\partial{y}}\cdot\frac{\partial{y}}{\partial{b}}=y \cdot 3+x \cdot 2=63 \tag{7}

其中:

\frac{\partial{z}}{\partial{x}}=\frac{\partial{}}{\partial{x}}(x \cdot y)=y=9

\frac{\partial{z}}{\partial{y}}=\frac{\partial{}}{\partial{y}}(x \cdot y)=x=18

\frac{\partial{x}}{\partial{b}}=\frac{\partial{}}{\partial{b}}(2w+3b)=3

\frac{\partial{y}}{\partial{b}}=\frac{\partial{}}{\partial{b}}(2b+1)=2

我们不妨再验证一下链式求导的正确性。把公式5再拿过来:

z=x \cdot y=(2w+3b)(2b+1)=4wb+2w+6b^2+3b \tag{5}

对上式求b的偏导:

{\partial z \over \partial b}=4w+12b+3=12+48+3=63 \tag{8}

结果和公式7的链式法则一样。

求b的具体变化值

公式7和公式8的含义是:当b变化一点点时,z会发生b的变化值的63倍的变化。记住我们的目标是让z=150,目前在初始状态时是162,所以,问题转化为:当我们需要z从162变到150时,b需要变化多少?

既然:

\Delta z = 63 \cdot \Delta b

则:

\Delta b = {\Delta z \over 63}={162-150 \over 63}=0.1905

所以:

b=b-0.1905=3.8095

x=2w+3b=17.4285

y=2b+1=8.619

z=x \cdot y=17.4285 \times 8.619=150.2162

这个结果也是与150很接近了,但是精度还不够。再迭代几次,应该可以近似等于150了,直到误差不大于1e-4时,我们就可以结束迭代了,对于计算机来说,这些运算的执行速度很快。

这个问题用数学公式倒推求解一个二次方程,就能直接得到准确的b值吗?是的!但是我们是要说明机器学习的方法,机器并不会解二次方程,而且很多时候不是用二次方程就能解决实际问题的。而上例所示,是用机器所擅长的迭代计算的方法来不断逼近真实解,这就是机器学习的真谛!而且这种方法是普遍适用的。

  1. def back_propagation_for_b(w, b, t):
  2. '''
  3. 反向传播求解b
  4. :param w: 权重w
  5. :param b: 权重b
  6. :param t: 目标值t
  7. :return:
  8. '''
  9. print("\nback_propagation_for_b ----- \n")
  10. error = 1e-5
  11. count = 1
  12. while(True):
  13. x, y ,z = target_function(w, b)
  14. delta_z = z - t
  15. print("w=%f,b=%f,z=%f,delta_z=%f" % (w, b, z, delta_z))
  16. if abs(delta_z) < error:
  17. break
  18. # 偏z偏x=y
  19. # 偏x偏b=3
  20. # 偏z偏y=x
  21. # 偏y偏b=2
  22. partial_b = 2 * x + 3 * y
  23. delta_b = delta_z / partial_b
  24. b = b - delta_b
  25. count = count + 1
  26. print("done!\ntotal iteration times = %d" % count)
  27. print("final b = %f" % b)

同时求解w和b的变化值

这次我们要同时改变w和b,到达最终结果为z=150的目的。

已知\Delta z=12,我们不妨把这个误差的一半算在w账上,另外一半算在b的账上:

\Delta b=\frac{\Delta z / 2}{63} = \frac{12/2}{63}=0.095

\Delta w=\frac{\Delta z / 2}{18} = \frac{12/2}{18}=0.333

  • $$w = w-\Delta w=3-0.333=2.667$$
  • $$b = b - \Delta b=4-0.095=3.905$$
  • $$x=2w+3b=2 \times 2.667+3 \times 3.905=17.049$$
  • $$y=2b+1=2 \times 3.905+1=8.81$$
  • $$z=x \times y=17.049 \times 8.81=150.2$$
  1. def back_propagation_for_wb(w, b, t):
  2. '''
  3. 反向传播求解wb
  4. :param w: 权重w
  5. :param b: 权重b
  6. :param t: 目标值t
  7. :return:
  8. '''
  9. print("\nback_propagation_for_wb ----- \n")
  10. error = 1e-5
  11. count = 1
  12. while(True):
  13. x, y ,z = target_function(w, b)
  14. delta_z = z - t
  15. print("w=%f,b=%f,z=%f,delta_z=%f" % (w, b, z, delta_z))
  16. if abs(delta_z) < error:
  17. break
  18. # 偏z偏x=y
  19. # 偏x偏b=3
  20. # 偏z偏y=x
  21. # 偏y偏b=2
  22. # 偏x偏w=2
  23. partial_b = 2 * x + 3 * y
  24. partial_w = 2 * y
  25. # 同时求解w和b,将误差均分到w和b上
  26. delta_b = delta_z / partial_b / 2
  27. delta_w = delta_z / partial_w / 2
  28. b = b - delta_b
  29. w = w - delta_w
  30. count = count + 1
  31. print("done!\ntotal iteration times = %d" % count)
  32. print("final b = %f" % b)
  33. print("final w = %f" % w)

容易出现的问题:

  1. 在检查Δz时的值时,注意要用绝对值,因为有可能是个负数
  2. 在计算Δb和Δw时,第一次时,它们对z的贡献值分别是1/63和1/18,但是第二次时,由于b和w值的变化,对于z的贡献值也会有微小变化,所以要重新计算。具体解释如下:

\frac{\partial{z}}{\partial{b}}=\frac{\partial{z}}{\partial{x}} \cdot \frac{\partial{x}}{\partial{b}}+\frac{\partial{z}}{\partial{y}}\cdot\frac{\partial{y}}{\partial{b}}=y \cdot 3+x \cdot 2=3y+2x

\frac{\partial{z}}{\partial{w}}=\frac{\partial{z}}{\partial{x}} \cdot \frac{\partial{x}}{\partial{w}}+\frac{\partial{z}}{\partial{y}}\cdot\frac{\partial{y}}{\partial{w}}=y \cdot 2+x \cdot 0 = 2y

所以,在每次迭代中,要重新计算下面两个值

\Delta b=\frac{\Delta z}{3y+2x} \Delta w=\frac{\Delta z}{2y}

以下是程序的运行结果。

没有在迭代中重新计算Δb的贡献值:

  1. single variable: b -----
  2. w=3.000000,b=4.000000,z=162.000000,delta_z=12.000000
  3. delta_b=0.190476
  4. w=3.000000,b=3.809524,z=150.217687,delta_z=0.217687
  5. delta_b=0.003455
  6. w=3.000000,b=3.806068,z=150.007970,delta_z=0.007970
  7. delta_b=0.000127
  8. w=3.000000,b=3.805942,z=150.000294,delta_z=0.000294
  9. delta_b=0.000005
  10. w=3.000000,b=3.805937,z=150.000011,delta_z=0.000011
  11. delta_b=0.000000
  12. w=3.000000,b=3.805937,z=150.000000,delta_z=0.000000
  13. done!
  14. final b=3.805937

在每次迭代中都重新计算Δb的贡献值:

  1. single variable new: b -----
  2. w=3.000000,b=4.000000,z=162.000000,delta_z=12.000000
  3. factor_b=63.000000, delta_b=0.190476
  4. w=3.000000,b=3.809524,z=150.217687,delta_z=0.217687
  5. factor_b=60.714286, delta_b=0.003585
  6. w=3.000000,b=3.805938,z=150.000077,delta_z=0.000077
  7. factor_b=60.671261, delta_b=0.000001
  8. w=3.000000,b=3.805937,z=150.000000,delta_z=0.000000
  9. done!
  10. final b=3.805937

从以上两个结果对比中,可以看到三点:

  1. factor_b第一次是63,以后每次都会略微降低一些
  2. 第二个函数迭代了3次就结束了,而第一个函数迭代了5次,效率不一样
  3. 最后得到的结果是一样的,因为这个问题只有一个解

对于双变量的迭代,有同样的问题:

没有在迭代中重新计算Δb,Δw的贡献值(factor_b和factor_w每次都保持63和18):

  1. double variable: w, b -----
  2. w=3.000000,b=4.000000,z=162.000000,delta_z=12.000000
  3. delta_b=0.095238, delta_w=0.333333
  4. w=2.666667,b=3.904762,z=150.181406,delta_z=0.181406
  5. delta_b=0.001440, delta_w=0.005039
  6. w=2.661628,b=3.903322,z=150.005526,delta_z=0.005526
  7. delta_b=0.000044, delta_w=0.000154
  8. w=2.661474,b=3.903278,z=150.000170,delta_z=0.000170
  9. delta_b=0.000001, delta_w=0.000005
  10. w=2.661469,b=3.903277,z=150.000005,delta_z=0.000005
  11. done!
  12. final b=3.903277
  13. final w=2.661469

在每次迭代中都重新计算Δb,Δw的贡献值(factor_b和factor_w每次都变化):

  1. double variable new: w, b -----
  2. w=3.000000,b=4.000000,z=162.000000,delta_z=12.000000
  3. factor_b=63.000000, factor_w=18.000000, delta_b=0.095238, delta_w=0.333333
  4. w=2.666667,b=3.904762,z=150.181406,delta_z=0.181406
  5. factor_b=60.523810, factor_w=17.619048, delta_b=0.001499, delta_w=0.005148
  6. w=2.661519,b=3.903263,z=150.000044,delta_z=0.000044
  7. factor_b=60.485234, factor_w=17.613053, delta_b=0.000000, delta_w=0.000001
  8. w=2.661517,b=3.903263,z=150.000000,delta_z=0.000000
  9. done!
  10. final b=3.903263
  11. final w=2.661517

这个与第一个单变量迭代不同的地方是:这个问题可以有多个解,所以两种方式都可以得到各自的正确解,但是第二种方式效率高,而且满足梯度下降的概念。

参考资料

http://colah.github.io/posts/2015-08-Backprop/

完整代码

原代码位置:ch02, Level1

个人代码:

  1. def target_function(w, b):
  2. x = 2 * w + 3 * b
  3. y = 2 * b + 1
  4. z = x * y
  5. return x, y, z
  6. def back_propagation_for_w(w, b, t):
  7. '''
  8. 反向传播求解w
  9. :param w: 权重w
  10. :param b: 权重b
  11. :param t: 目标值t
  12. :return:
  13. '''
  14. print("\nback_propagation_for_w ----- \n")
  15. error = 1e-5
  16. count = 1
  17. while(True):
  18. x, y ,z = target_function(w, b)
  19. delta_z = z - t
  20. print("w=%f,b=%f,z=%f,delta_z=%f" % (w, b, z, delta_z))
  21. if abs(delta_z) < error:
  22. break
  23. # 偏z偏x=y
  24. # 偏x偏w=2
  25. partial_w = y * 2
  26. delta_w = delta_z / partial_w
  27. w = w - delta_w
  28. count = count + 1
  29. print("done!\ntotal iteration times = %d" % count)
  30. print("final w = %f" % w)
  31. def back_propagation_for_b(w, b, t):
  32. '''
  33. 反向传播求解b
  34. :param w: 权重w
  35. :param b: 权重b
  36. :param t: 目标值t
  37. :return:
  38. '''
  39. print("\nback_propagation_for_b ----- \n")
  40. error = 1e-5
  41. count = 1
  42. while(True):
  43. x, y ,z = target_function(w, b)
  44. delta_z = z - t
  45. print("w=%f,b=%f,z=%f,delta_z=%f" % (w, b, z, delta_z))
  46. if abs(delta_z) < error:
  47. break
  48. # 偏z偏x=y
  49. # 偏x偏b=3
  50. # 偏z偏y=x
  51. # 偏y偏b=2
  52. partial_b = 2 * x + 3 * y
  53. delta_b = delta_z / partial_b
  54. b = b - delta_b
  55. count = count + 1
  56. print("done!\ntotal iteration times = %d" % count)
  57. print("final b = %f" % b)
  58. def back_propagation_for_wb(w, b, t):
  59. '''
  60. 反向传播求解wb
  61. :param w: 权重w
  62. :param b: 权重b
  63. :param t: 目标值t
  64. :return:
  65. '''
  66. print("\nback_propagation_for_wb ----- \n")
  67. error = 1e-5
  68. count = 1
  69. while(True):
  70. x, y ,z = target_function(w, b)
  71. delta_z = z - t
  72. print("w=%f,b=%f,z=%f,delta_z=%f" % (w, b, z, delta_z))
  73. if abs(delta_z) < error:
  74. break
  75. # 偏z偏x=y
  76. # 偏x偏b=3
  77. # 偏z偏y=x
  78. # 偏y偏b=2
  79. # 偏z偏x=y
  80. # 偏x偏w=2
  81. partial_b = 2 * x + 3 * y
  82. partial_w = 2 * y
  83. # 同时求解w和b,将误差均分到w和b上
  84. delta_b = delta_z / partial_b / 2
  85. delta_w = delta_z / partial_w / 2
  86. b = b - delta_b
  87. w = w - delta_w
  88. count = count + 1
  89. print("done!\ntotal iteration times = %d" % count)
  90. print("final b = %f" % b)
  91. print("final w = %f" % w)
  92. if __name__ == '__main__':
  93. w = 3
  94. b = 4
  95. t = 150
  96. back_propagation_for_w(w, b, t)
  97. back_propagation_for_b(w, b, t)
  98. back_propagation_for_wb(w, b, t)