You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

03-framework.md 8.2 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. ### 第三部分:进入人工智能的世界
  2. > 前两关是不是很简单?
  3. 相信你在前两部分中,已经积累了足够多的C++知识,也回忆起了足够多的高等数学知识。现在,我们要构造一个框架,这个框架可以接受一个矩阵作为输入,并且支持神经网络中的常见的网络层,例如
  4. - 线性层(Linear)
  5. - 激活层(Activation)
  6. - 损失层(Loss)
  7. #### 张量类
  8. 我们已经在`cc/tensor/tensor.h`中定义了张量类,这个类可以表示一个多维数组,并且支持常见的数学运算。我们可以在`cc/tensor/tensor.cc`中实现这些运算。当然,我们假定所有的张量都是二维的,这样你就不必考虑各种情况。
  9. **[TASK 13]** 补全`cc/tensor/tensor.cc`中关于`Tensor::transpose()`的函数实现。它能够将一个张量进行转置。
  10. **[TASK 14]** 补全`cc/tensor/tensor.cc`中关于`argmax(const std::shared_ptr<Tensor>& tensor, int axis)`的函数实现,它能够返回一个张量在指定维度上的最大值的索引。提示:你可以使用`std::numeric_limits<float>::infinity()`,可以通过LLM来查询它的含义。
  11. > 前面做了这么多次测试,你是不是该自己学会写测试了?...算了,还是我来帮你写吧...😂
  12. 测试文件:`frontend/framework/tensor/task13_14.py`
  13. **关于测试用例** 之后的内容的测试用例可以参考`frontend/uct/test`下的文件,或依据自己的需要编写。
  14. #### 线性层
  15. 线性层是神经网络中最为常见的网络层,它接受一个输入张量,并且输出一个张量。输入两个张量`feature: (batch_size x input_features)`和`weight: (input_features x output_features)`,输出张量`output: (batch_size x output_features)`,实际上就是将`feature`矩阵和`weight`矩阵相乘。
  16. 用公式表示就是$y = Wx + b$。
  17. **[TASK 15]** 补全`cc/operators/nn.h`中`Linear`类的构造函数和`forward`函数。
  18. - 构造函数:构造函数接受两个参数`a`和`b`,它们都是`std::shared_ptr<Node>`类型的智能指针,分别表示输入特征和权重。构造函数调用基类`FunctionNode`的构造函数,并将`a`和`b`传递给它。在构造函数中,调用`this->forward()`方法,并将结果赋值给`this->data`。
  19. - `forward`函数:参见有关线性层的介绍。
  20. **[TASK 16]** 补全`cc/operators/nn.cc`中`Linear`类的`backward`函数。
  21. - `backward()`函数实现反向传播,计算梯度并返回。它接受`std::shared_ptr<tensor::Tensor> gradient`作为输入,你需要计算`grad_features`和`grad_weights`,它们分别表示对`features`和`weights`的梯度。
  22. > 数学Tips:`grad_features`是通过将`gradient`与`weights`的转置相乘得到的。`grad_weights`是通过将`features`的转置与`gradient`相乘得到的。
  23. 完成了这两个任务后,你应该可以在`cc/`下执行
  24. ```
  25. cmake -S . -B build
  26. cmake --build build
  27. ```
  28. 就能够编译你的代码。然后,你应当可以运行`frontend/uct/perception.py`,它将使用你实现的线性层来训练一个感知机。
  29. #### 激活层
  30. 激活层是神经网络中常见的网络层,它接受一个输入张量,并且输出一个张量。输入一个张量`x`,输出一个张量`y`,实际上就是将`x`中的每个元素进行某种变换。
  31. 用公式表示就是$y = f(x)$。对于`ReLU`函数来说,$y = max(0, x)$。
  32. **[TASK 17]** 补全`cc/operators/nn.h`中`ReLU`类的构造函数和`forward`函数。
  33. - 构造函数:构造函数接受一个参数`a`,它是一个`std::shared_ptr<Node>`类型的智能指针,表示输入特征。构造函数调用基类`FunctionNode`的构造函数,并将`a`传递给它。在构造函数中,调用`this->forward()`方法,并将结果赋值给`this->data`。
  34. - `forward`函数:参见有关激活层的介绍。
  35. **[TASK 18]** 补全`cc/operators/nn.cc`中`ReLU`类的`backward`函数。
  36. - `backward()`函数实现反向传播,计算梯度并返回。它接受`std::shared_ptr<tensor::Tensor> gradient`作为输入,你需要计算`grads`,它表示对`features`的梯度。
  37. > 数学Tips:`grads`是通过将`gradient`与`x`中大于0的元素对应相乘得到的。
  38. #### 偏置
  39. 线性层中,我们没有实现偏置项`b`,它是一个向量,它的维度与输出特征的维度相同。偏置项的作用是使得线性层的输出能够更好地拟合数据。
  40. **[TASK 19]** 补全`cc/operators/nn.h`中`AddBias`类的构造函数和`forward`函数。
  41. - 构造函数:构造函数接受两个参数`a`和`b`,它们都是`std::shared_ptr<Node>`类型的智能指针,分别表示输入特征和偏置。构造函数调用基类`FunctionNode`的构造函数,并将`a`和`b`传递给它。在构造函数中,调用`this->forward()`方法,并将结果赋值给 `this->data`。
  42. - `forward`函数:`forward`方法实现前向传播,将偏置添加到输入特征上。`features`和`bias`分别从`this->objects`中获取,`features`的形状为`(batch_size x num_features)`,`bias`的形状为`(1 x num_features)`。在函数中,需要创建一个与`features`形状相同的输出张量`outNode`,使用嵌套循环将`features`的每个元素与`bias`的对应元素相加,结果存储在`outNode`中。最后,返回`outNode`。
  43. **[TASK 20]** 补全`cc/operators/nn.cc`中`AddBias`类的`backward`函数。
  44. - `backward()`函数实现反向传播,计算梯度并返回。它接受`std::shared_ptr<tensor::Tensor> gradient`作为输入,你需要计算`grad_features`和`grad_bias`,它们分别表示对`features`和`bias`的梯度。
  45. > 数学Tips:`grad_features`和`grad_bias`都是`gradient`的拷贝。但是考虑到我们有`batch_size`的存在,因此,在计算`bias`的梯度时,需要将`gradient`的每一列相加,得到`grad_bias`的对应元素。
  46. #### 损失层——均方误差损失函数
  47. 我们首先实现均方误差损失函数,它接受两个张量`y_pred`和`y_true`,它们分别表示预测值和真实值,输出一个标量,表示预测值与真实值之间的误差。
  48. 用公式表示就是$\displaystyle loss = \frac{1}{2} \sum_{i=1}^{n} (y_{pred} - y_{true})^2$。
  49. **[TASK 21]** 补全`cc/operators/nn.h`中`SquareLoss`类的构造函数和`forward`函数。
  50. - 构造函数:构造函数接受两个参数`a`和`b`,它们都是`std::shared_ptr<Node>`类型的智能指针,分别表示预测值和真实值。构造函数调用基类`FunctionNode`的构造函数,并将`a`和`b`传递给它。在构造函数中,调用`this->forward()`方法,并将结果赋值给`this->data`。
  51. - `forward`函数用于计算损失。
  52. **[TASK 22]** 补全`cc/operators/nn.cc`中`SquareLoss`类的`backward`函数。
  53. - `backward`函数计算损失函数相对于输入`a`和`b`的梯度。`gradient`是损失函数对输出的梯度(是一个形状为(1, 1)的张量,可以直接认为其是一个向量`g`)。`grad_a`和`grad_b`分别存储`a`和`b`的梯度。对于每个元素,梯度计算为`g * (a->data->data[i] - b->data->data[i]) / a->data->size`。最终返回 grad_a 和 grad_b 的向量。
  54. #### 损失层——SoftmaxLoss
  55. 接下来,我们实现Softmax损失函数,它接受两个张量`y_pred`和`y_true`,它们分别表示预测值和真实值,输出一个标量,表示预测值与真实值之间的误差。
  56. 用公式表示就是$\displaystyle loss = -\sum_{i=1}^{n} y_{true} \log(y_{pred})$。
  57. **[TASK 23]** 补全`cc/operators/nn.h`中`SoftmaxLoss`类的构造函数,`forward`函数和`backward`函数。
  58. 完成上述内容后,你可以编译和运行`frontend/uct/regression.py`,使用线性网络来拟合`sin`函数。
  59. ### 手写体识别
  60. 补全代码中的其他标注有`TODO`的内容,最后编译运行,你就将能够训练一个手写体识别模型。可以运行`frontend/uct/mnist.py`来试一下吧!
  61. > 是不是觉得运行得有点慢?考虑使用多线程来加速矩阵运算。(这已经超出了这门课的要求,对高性能计算/并行计算感兴趣的同学可以勇于尝试!)
  62. ### extra bonus
  63. 想打副本?
  64. ```
  65. nslookup -type=txt uc-cpp.shahe.org
  66. ```