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之间的关系
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
前向传播很简单,调用公式就行,反向传播有点难理解,尤其是
Summation和BroadcastTo
前向计算是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"