本周讲解dlsyscourse 的第二次作业,也就是hw1的部分。上周的内容主要是个warm up,熟悉一下环境以及一些简单的代码,本周要开始实现一个基础的自动微分框架needle。同样本周作业也放到了 https://github.com/careywyr/dlsyscourse ,colab的题目也翻译成了中文方便阅读,代码里面也移除了关于mugrade的部分,建议先自己做一遍,这样会更有收获。
关于needle
框架的介绍在问题1的前面已经给出了描述,具体要注意的就是下面几个关键类:
Value
: 计算图中的一个值,可能是通过对其他Value
对象的操作计算得出,或者是常量(叶子)Value
对象。我们使用了一个通用类(后续版本中可以专门化为其他数据结构,例如张量Tensor),但目前你主要会通过其子类Tensor
进行交互(详见下文)。Op
: 计算图中的一个算子。算子需要在compute()
方法中定义其前向传播(即如何基于Value
对象的数据计算运算结果),并通过gradient()
方法定义其反向传播,用于处理传入的输出梯度。编写这些算子的详细信息将在下文中提供。Tensor
:Value
类的一个子类,表示计算图中的一个多维数组输出(张量)。在本次作业以及后续的作业中,你将主要使用Tensor
这个Value
的子类。我们提供了多个便捷函数(例如运算符重载),这些函数使你能够像正常使用Python中的操作符那样来操作张量,但在实现相应操作之前,这些便捷函数不会正常工作。TensorOp
:Op
类的一个子类,专门用于返回张量的运算符。你将在本次作业中实现的所有操作都是这种类型。
那么基于上面的基础类,来完成本次的作业。
问题1:实现前向计算(略)
这一题要在 ops_mathematic
这个文件里实现所有类的 compute()
方法,这个应该是这次作业最简单的一题了,没有太多需要介绍的,就是按照公式使用numpy的方法即可。
问题2:反向传播
为了实现计算图的自动微分,那就要计算反向传播,即将函数的导数和传入的反向的梯度相乘。在题目的介绍中给了逐元素加法EWiseAdd
和逐元素乘法EWiseMul
运算的反向传播的实现。
- 对于 它对于x或者y的偏导均是1,因此它的梯度就是1乘以反向传播的梯度,还是其本身。
- 对于 。它对于x或者y的偏导分别是另一半,即 y 和 x 。那么反向传播的结果就是 以及 。
那么这次作业其实就是完成剩下的那些方法的梯度计算。其实绝大部分都是简单的,只要掌握常见基本函数求导的基本公式即可,主要要说明的是下面几个方法:
- transpose
- reshape
- broadcast
- summation
- matmul
- ReLU
Transpose
对于转置,前向传播的时候是将输入按照axes进行翻转,默认是-1,-2轴,反向传播就是将其翻转回来,所以代码如下:
Reshape
Reshape和转置类似,就是将梯度形状还原:
Broadcast
广播就有点费劲了,广播前向计算的时候实际上会将张量的维度扩展,比如从(1,n)扩展到(m,n),那么反向传播的时候,需要将梯度压缩回(1,n)。具体来说,就是对第一个维度进行求和。那么就是:
- 对于那些被广播的维度,在反向传播时需要对这些维度进行求和。
- 对于那些没有被广播的维度,梯度会直接传递下去,不需要特殊处理。
Sum
对于sum操作的反向传播,首先要考虑sum前向计算的时候带来的后果是什么?它在某些维度上进行了累加,那就意味着这些维度消失了,反向传播的时候需要把这些维度分配回来。
MatMul
对于矩阵乘法,其实如果两者的维度是一样的,那么就跟求xy的偏导区别不大,顶多就是注意一下要进行转置,一个是$out_grad y^Tx^T out_grad$。 但要考虑一个问题,就是x和y的维度可能不一致,因为不一致并不代表不能进行矩阵乘法,只要x的最后一个维度和y的第一个维度一样即可。所以反向的时候正如上面的broadcast,这里也要sum一下。
ReLU
对于ReLU函数,前向传播就是max(0, x),其实就是个分段函数,那么对于两段的梯度分别是1和0。然后跟out_grad相乘即可。代码实现上可以创建一个新的Tensor方便进行计算:
问题3:拓扑排序(Topological Sort)
这一题就是图的排序,不过要确保执行的是后序深度优先搜索(Post-order Depth-First Search, DFS),它的意思是在访问某个节点之前,先递归地访问它的所有子节点或相邻节点,直到所有路径都被遍历完毕,再回溯并记录当前节点。 比如假设有这样一个有向无环图:
A
/ \
B C
/ \ \
D E F
通过DFS排序后,结果是DEBFCA。大致逻辑是:
- 选择一个未访问的起始节点作为起点。
- 将该节点标记为已访问。
- 对于该节点的每一个未访问相邻节点,递归执行后序深度优先搜索。
- 当所有相邻节点都访问完毕后,将当前节点加入遍历结果。
题目中给出了两个待实现的方法,find_topo_sort
和 topo_sort_dfs
,其中 topo_sort_dfs
是用来当做递归辅助的函数。
问题4:实现反向自动微分
这一题需要参考课件里面的关于反向微分计算的伪码:
要实现的方法实际上是要计算神经网络中每个节点(变量)的梯度,即最终输出张量对每个节点的梯度。那么按照图中的描述,大致如下几个步骤:
- 初始化字典来存储各节点的梯度(Dictionary that records a list of partial adjoints of each node)
- 反向遍历图中的节点(就是reverse上一题的list)
- 遍历节点并计算梯度
- 遍历节点
- 累加该节点的梯度 (Sum up partial adjoints)
- 计算梯度
- 遍历inputs将梯度放进去(Propagate gradients to input nodes)
问题5:计算Softmax Loss
对于要实现方法的输入输出如下:
Z
: 一个 2D Tensor,形状为(batch_size, num_classes)
,包含每个类的logit(未归一化的预测值)。y_one_hot
: 一个 2D Tensor,形状为(batch_size, num_classes)
,表示one-hot 编码的真实标签。- 返回的是一个标量 Tensor,表示平均的 softmax 损失。
对于损失的数学公式定义为:
那么我们可以将步骤分解成如下几步:
- 对每个样本的logits取指数求和
- 提取每个样本真实标签的logit值
- 求loss,记得除以batch
问题6:两层神经网络的SGD
这一题跟hw0中的nn_epoch类似,不同的是输入的W1和W2是 Tensors
,此外上次是手动推导两层网络的反向传播方差,这次实现了自动微分的方法,因此可以直接使用backward()
方法计算梯度。要注意的就是要使用needle里面的方法,要使得参数是Tensor
。
本次作业因为很多地方不再是使用NDArray,而是有时候是Tensor,写的时候经常分不清某个变量到底是个啥类型,所以其实我写的代码是有一些模糊的,虽然功能是实现了,但觉得并不是最优雅的写法,因此仅供参考,相信很多同学可以写出更好的方案。