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