深度学习(斋藤)学习笔记(四)-反向传播

前面的笔记(上一篇笔记)中,我们介绍了神经网络的学习,并通过数值微分计算了神经网络的权重参数的梯度(严格来说,是损失函数关于权重参数的梯度)。数值微分虽然简单,也容易实现,但缺点是计算上比较费时间。本章我们将学习一个能够高效计算权重参数的梯度的方法——误差反向传播法。

计算图直观理解误差反向传播法

对于计算图,计算中途求得的导数的结果(中间传递的导数)可以被共享,从而可以高效地计算多个导数。能够通过正向传播和反向传播高效地计算各个变量的导数值。

理论:

求导的链式法则(数学基础)

代码验证

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值