hw1实验指南——实现一个自动微分框架

579 阅读4分钟

hw1的目标是建立一个基本的自动微分框架,然后用它来重新实现在HW0中用于MNIST数字分类问题的简单两层神经网络。

所有代码地址

文件目录

.
├── apps
│   └── simple_ml.py
├── data
│   ├── t10k-images-idx3-ubyte.gz
│   ├── t10k-labels-idx1-ubyte.gz
│   ├── train-images-idx3-ubyte.gz
│   └── train-labels-idx1-ubyte.gz
├── hw1.ipynb
├── python
│   └── needle
│       ├── autograd.py
│       ├── __init__.py
│       └── ops.py
└── tests
    ├── __pycache__
    │   └── test_autograd_hw.cpython-38-pytest-6.1.1.pyc
    └── test_autograd_hw.py

重要的是两个文件
autograd.py定义了计算图框架的基础知识,也将构成自动微分框架的基础
ops.py包含各种运算符的实现,将在整个作业和课程中使用实现。

框架结构

作业里面说不建议先浏览代码,确实是这样,整体结构不懂也能做作业,完成所有内容后,我回过头又看了下ops.py和autograd.py, 尝试把这部分记录在最前面,方便想要学习的同学。

autograd.py

autograd.py里面包含很多自定义类,有

  • Device
  • CPUDevice
  • Op
  • TensorOp
  • TensorTupleOp
  • Value
  • TensorTuple
  • Tensor

Op以及Value之间的关系

whiteboard_exported_image.png

ops.py

ops.py里只有各种操作符,继承自TensorOp,实现各种tensor操作,以及梯度计算,其中前向计算输入是NDArray,反向是输入是Tensor类型,tuple里面也是Tensor。

class EWiseAdd(TensorOp):
    def compute(self, a: NDArray, b: NDArray):
        return a + b
    def gradient(self, out_grad: Tensor, node: Tensor):
        return out_grad, out_grad
def add(a, b):
    return EWiseAdd()(a, b)

Question 1: Implementing forward computation

任务1要求实现以下基本计算,本质上还是numpy操作

  • PowerScalar: 整数幂
  • EWiseDiv: ndarry除法(两个矩阵)
  • DivScalar: 矩阵除标量
  • MatMul: 矩阵乘法
  • Summation: 在指定维度上求和
  • BroadcastTo: 广播一个数组到新维度
  • Reshape: 重塑数据
  • Negate: 求反
  • Transpose: 转置,只交换两个轴,默认交换最后两个

本地测试python3 -m pytest -v -k "forward"

Question 2: Implementing backward computation

前向传播很简单,调用公式就行,反向传播有点难理解,尤其是 SummationBroadcastTo

前向计算是numpy,反向传播梯度计算是tensor 举例

class EWiseAdd(TensorOp):
    def compute(self, a: NDArray, b: NDArray):
        return a + b

    def gradient(self, out_grad: Tensor, node: Tensor):
        return out_grad, out_grad

a+b,对a和b求导都为1,所以梯度还是原来梯度

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

a*b,求导分别为b和a,所以梯度为grad*b和grad*a

本地测试python3 -m pytest -l -v -k "backward"

Question 3: Topological sort

任务3要求实现拓扑排序,从而可以遍历(向前或向后)计算图
在一个DAG(有向无环图)中,我们将图中的顶点以线性方式进行排序,使得对于任何的顶点 u 到 v的有向边 (u,v), 都可以有 u 在 v 的前面。

这里要求后序深度遍历,变成u在v的后面,越后面的节点越靠前

# 获得一个list,其中节点按顺序出现
def find_topo_sort(node_list: List[Value]) -> List[Value]:
    visited = set()
    topo_order = []
    for node in node_list:
        if node not in visited:
            topo_sort_dfs(node, visited, topo_order)
    return topo_order

def topo_sort_dfs(node, visited, topo_order):
    """Post-order DFS"""
    if node in visited:
        return
    for next in node.inputs:
        topo_sort_dfs(next, visited, topo_order)
    visited.add(node)
    topo_order.append(node)

本地测试python3 -m pytest -k "topo_sort"

Question 4: Implementing reverse mode differentiation

实现反向自动微分

有点难理解,要结合计算图结构,和topo排序理解,还有一些奇怪的数据结构

def compute_gradient_of_variables(output_tensor, out_grad):
    node_to_output_grads_list: Dict[Tensor, List[Tensor]] = {}
    node_to_output_grads_list[output_tensor] = [out_grad]
    reverse_topo_order = list(reversed(find_topo_sort([output_tensor])))
    ### BEGIN YOUR SOLUTION
    for node in reverse_topo_order:
        v = sum_node_list(node_to_output_grads_list[node])
        node.grad = v
        for node1 in node.inputs:
            if node1 not in node_to_output_grads_list:
                node_to_output_grads_list[node1] = []
        if not node.is_leaf():
            # 
            gradient = node.op.gradient_as_tuple(v, node)
            for i, node1 in  enumerate(node.inputs):
                node_to_output_grads_list[node1].append(gradient[i])
    ### END YOUR SOLUTION

本地测试python3 -m pytest -k "compute_gradient"

Question 5: Softmax loss

y_one_hot也是矩阵,问题简单很多,按行求和即可

def softmax_loss(Z, y_one_hot):
   
    ### BEGIN YOUR SOLUTION
    a = ndl.ops.summation(Z*y_one_hot) # 全部累加
    b = ndl.ops.summation(ndl.ops.log(ndl.ops.summation(ndl.ops.exp(Z), axes=(1, ))))
    return (b-a)/Z.shape[0]
    ### END YOUR SOLUTION

本地测试python3 -m pytest -k "softmax_loss_ndl"

Question 6: SGD for a two-layer neural network

用实现的自动微分框架实现两层神经网络的训练
注意处理标签即可,将标签转为one_hot矩阵形式

def nn_epoch(X, y, W1, W2, lr = 0.1, batch=100):
    n = X.shape[0]
    step = n // batch
    Y_one_hot = np.zeros((n, W2.shape[1]))
    Y_one_hot[np.arange(n), y] = 1.0
    for i in range(step + 1):
        start = i * batch
        end = min(start + batch, n)
        if start == end:
           break
        x1 = X[start: end]
        x1 = ndl.Tensor(x1, requires_grad=False)
        # index = np.arange(x1.shape[0])
        y_one_hot = Y_one_hot[start: end]
        y_one_hot = ndl.Tensor(y_one_hot, requires_grad=False)
        
        Z1 = x1@W1
        A1 = ndl.ops.relu(Z1)
        Z2 = A1@W2

        loss = softmax_loss(Z2, y_one_hot)
        loss.backward()
        
        W1.data -= lr * ndl.Tensor(W1.grad.numpy().astype(np.float32)) 
        W2.data -= lr * ndl.Tensor(W2.grad.numpy().astype(np.float32)) 
    return W1, W2

总结

本章主要是运算符前向传播和反向传播实现,以及拓扑排序,实现自动微分

所有测试代码整理如下:

python3 -m pytest -v -k "forward"
python3 -m pytest -l -v -k "backward"
python3 -m pytest -k "topo_sort"
python3 -m pytest -k "compute_gradient"
python3 -m pytest -k "softmax_loss_ndl"
python3 -m pytest -l -k "nn_epoch_ndl"

参考文献

  1. www.zhihu.com/question/49…
  2. doraemonzzz.com/2022/10/17/…
  3. github.com/YuanchengFa…
  4. swapaxes和transpose的区别