前面的笔记(上一篇笔记)中,我们介绍了神经网络的学习,并通过数值微分计算了神经网络的权重参数的梯度(严格来说,是损失函数关于权重参数的梯度)。数值微分虽然简单,也容易实现,但缺点是计算上比较费时间。本章我们将学习一个能够高效计算权重参数的梯度的方法——误差反向传播法。
计算图直观理解误差反向传播法
对于计算图,计算中途求得的导数的结果(中间传递的导数)可以被共享,从而可以高效地计算多个导数。能够通过正向传播和反向传播高效地计算各个变量的导数值。
理论:
求导的链式法则(数学基础)
代码验证
1.支持反向传播的variable类
class Variable:
def init (self, data):
self.data = data
self.grad = None
2.Function类
lass Function:
def __ call__ (self, input):
x = input .data
y = self. forward(x)
output = Variable(y)
self.input = input
# 保存输入的变量
return output
def forward(self , x):
raise NotlmplementedError()
def backward(self, gy):
raise NotlmplementedError()
下面只需要编写具体的函数,实现这个Function:
class Square(Function):
def forward(self , x):
y = x ** 2
return y
def backward(self, gy): #gy 反向传播时运算符号的上游变量
x = self.input.data #使用到了存储在函数的输入(前向传播时运算符号的上游变量)
gx = 2 * x * gy
return gx
3.正向传播代码
A = Square()
B = Exp()
C = Square ()
x = Variable(np.array(ß.5))
a = A(x)
b = B(a)
y = C(b)
4.反向传播代码
y.grad = np . array (1.0 )
b. grad = C. backward(y.grad)
a . grad = B. backward(b .grad)
x. grad = A. backward( a .grad)
print (x. grad )
5.反向传播自动化
类似链表结构,类中的某个成员存储与其他类的关系
接着,可以引出这样的一个问题:
反向传播,首先从y开始,已知y.grad,如何求出b.grad,需要:
1.获取函数:y.creator
2.获取输入:C.input
3.根据C.backward(C.input)计算出b.grad
通过上述步骤我们虽然只求出了b.grad但是,a.grad和x.grad的计算也是一样的。也是需要执行上面的步骤,即:1.已知反向传播的上游变量的梯度-->2.获取其creator-->3.获取creator的输入,利用creator的backward计算下游变量的梯度,因此为了自动完成
6.使反向传播更易用的3项改进
改进一:增加代码使得函数支持连续调用(把对象的实例化封装到函数里)
之前:
之后:
改进二:
改进三:
最后还需要注意(否则会报错--
E:\anaconda\python.exe E:\pycharm\pythonProject1\learn4.py
Traceback (most recent call last):
File "E:\pycharm\pythonProject1\learn4.py", line 78, in <module>
y = square(exp(square(x)))
^^^^^^^^^
File "E:\pycharm\pythonProject1\learn4.py", line 69, in square
return Square()(x)
^^^^^^^^^^^
File "E:\pycharm\pythonProject1\learn4.py", line 33, in __call__
output = Variable(y)
^^^^^^^^^^^
File "E:\pycharm\pythonProject1\learn4.py", line 8, in __init__
raise TypeError('{} is not supported'.format(type(data)))
TypeError: <class 'numpy.float64'> is not supported
):
- x = Variable(np.array(0.5)) 创建了一个值为 0.5 的 numpy 数组
- 但在 square 或 exp 的计算过程中,numpy 可能会返回标量值(numpy.float64)而不是数组
书中也有提到:
使用这个工具函数,确保函数的输出也是数组。
完整代码与运行结果
import numpy as np
class Variable:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError('{} is not supported'.format(type(data)))
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.data)
funcs = [self.creator]
while funcs:
f = funcs.pop()
x, y = f.input, f.output
x.grad = f.backward(y.grad)
if x.creator is not None:
funcs.append(x.creator)
class Function:
def __call__(self, input):
x = input.data
y = self.forward(x)
output = Variable(as_array(y)) # 使用 as_array 确保输出是数组
output.set_creator(self)
self.input = input
self.output = output
return output
def forward(self, x):
raise NotImplementedError()
def backward(self, gy):
raise NotImplementedError()
# 实现具体的函数类
class Square(Function):
def forward(self, x):
return x ** 2
def backward(self, gy):
x = self.input.data
gx = 2 * x * gy
return gx
class Exp(Function):
def forward(self, x):
return np.exp(x)
def backward(self, gy):
x = self.input.data
gx = np.exp(x) * gy
return gx
# 定义便捷函数
def square(x):
return Square()(x)
def exp(x):
return Exp()(x)
# 定义 as_array 函数
def as_array(x):
if np.isscalar(x):
return np.array(x)
return x
# 测试代码
x = Variable(np.array(0.5))
y = square(exp(square(x)))
y.backward()
print(x.grad)
说明:
对于输入 x = 0.5,这个计算过程是:
- 先计算 x²
- 然后计算 exp(x²)
- 最后计算 [exp(x²)]²
- 通过链式法则计算梯度
输出的梯度值是数值结果,表示函数在 x = 0.5 点的导数值
运行结果:
后面将继续学习深度学习代码的部分,以及优化学习的技巧与实践。
补充
什么是 __call__ 方法?
在 Python 中,__call__ 是一个特殊方法(也叫魔法方法或双下划线方法)。当你定义了这个方法后,类的实例可以像函数一样被“调用”。换句话说,定义了 __call__ 的对象可以通过加括号的方式执行某种操作,例如 obj()。
- 语法:def __call__(self, *args, **kwargs)
- 作用:当你对实例对象使用 () 调用时,Python 会自动执行 __call__ 方法。
- 用途:常用于需要让对象具有函数行为的情况,比如实现函数式编程、可调用对象,或者像你的代码中这样构建计算图。
-
与普通方法的区别:
- 普通方法:需要显式调用,比如 obj.forward(x)。
- __call__:让对象本身可以像函数一样直接调用 obj(x),更简洁、更符合函数式编程的风格。
class MyClass:
def __call__(self, x):
return x + 1
# 创建实例
obj = MyClass()
# 像函数一样调用实例
result = obj(5) # 这里会调用 __call__ 方法
print(result) # 输出: 6