5 - Automatic Differentiation Implementation
这节课其实就是讲Needle的实现:
- 如何使用
- 自动微分如何实现
- 几个类如何发挥作用
可以看出这部分代码是出自CSE 599W 课程,但又扩展了不少,因此,课后参考主要以 CSE 599W 笔记为主。
Needle知识
只包含两个数据结构:
- Value:为 计算图中 每一个节点。属性包括了 该节点的运算操作op(OP实例)、该节点的输入 inputs(Value实例组成的列表)、该节点的计算数值 cached_data (NDarray类型)
- OP:包含各种基础的计算操作,如加减乘除、指数、reshape、transpose 等最最基础的操作。
可能会困惑的点:数据类型涉及到:NDarray 和 Tensor(继承自Value) 。Tensor是NDarray的上层封装,其属性包括了该节点的op, inputs 等,保证了计算的时候可以顺便构建出计算图;而NDarray仅仅表示了数据本身及数据类型,且在目前NDarray使用numpy.array。
- Tensor 在运算的时候会 同时构建计算图,具体表现在运算返回 一个新的Tensor,里面把输入运算操作都保存了起来;
- 而NDarray的时候只是单纯计算结果,返回一个NDarray。
注:只要是 Tensor 类型,在运算必定会创建计算图。如果不想创建计算图,可以 通过 .detch()来脱离。
OP包括了两个方法:
- compute() 参数类型为 NDarray。只是单纯的计算,创建节点部分事先已经完成了,只需要把计算结果返回给新创建节点的 cached_data。
- gradient() 参数类型为 Value。不只是计算,还包括创建计算图中新的节点。
执行计算的流程
当执行两个 Tensor 相加或者别的运算操作时,计算流程如下:
autograd.TensorOp.__call__
calls into
autograd.Tensor.make_from_op
calls into
autograd.Tensor.realize_cached_data
calls into
x3.op.compute
A few key points to note here:
make_from_op
constructs the computational graph node.
- The actual computation won't happen until
realize_cached_data
is called.
反向模式的自动微分
以此计算图中节点
为例:
该节点接收两个输入
,执行 按元素相乘操作,Needle 中对应代码如下:class EWiseMul(TensorOp): def compute(self, a: NDArray, b: NDArray): return a * b def gradient(self, out_grad: Tensor, node: Tensor): # 该节点左输入,右输入 lhs, rhs = node.inputs # 会创建出两个节点 return out_grad * rhs, out_grad * lhs
此处只研究 gradent 函数,当该节点执行 gradient 时,接收两个参数,一个是 从后面传过来的梯度 out_grad,一个是该节点 node ,可以通过 node 参数得到该节点的两个输入
。然后开始执行gradient,会返回两个 node,节点含义分别为 ,具体结果分别为 out_grad * rhs, out_grad * lhs
,与上图一致!回忆一下上一节课程中 计算方式
Python知识
@classmethod 类方法
可以理解为提供了多种构造方法。类方法只能访问到类的数据属性,不能获取实例的数据属性
类变量 与 实例变量
@staticmethod 静态方法
静态方法只是名义上归属类管理,但是不能使用类变量和实例变量,是类的工具包。该函数不传入self或者cls,所以不能访问类属性和实例属性
@property和@setter的用法
python 中的 @property 装饰器可以总结为两个作用:
- 让 class 内的方法可以像属性一样使用
- 对要读取的数据进行预处理。
python 中的 @*.setter 装饰器可以总结为两个作用:
- 对要存入的数据进行预处理
- 设置可读属性(不可修改)
注意:@*.setter 装饰器必须在 @property 装饰器的后面,且两个被修饰的函数的名称必须保持一致,* 即为函数名称。
综合示例:通过@*.setter和@property的组合使用我们就可以实现密码的密文存储和明文输出,具体步骤为:用户输入明文->转化为密文后存入->用户读取时先转化为明文再输出。
class User(): def __init__(self, name): self.name = name self._password = '' # 密文存储 @property def password(self): return decryption(self._password) # 解密 @password.setter def password(self,word): self._password = encryption(word) # 加密 user = User('xiao') user.password = '123' #明文输入 print(user.password) #明文输出
总结:为两个同名函数打上 @.setter 装饰器和 @property 装饰器后,当把函数作为变量赋值时会触发 @.setter 对应的函数,当把函数作为变量读取时会触发 @property 对应的函数,因此我们可以将其用于数据的预处理。