### 第二部分:自动微分 #### 数值微分 有时候,我们无需知道一个函数具体的表达式,借助导数的定义,利用计算机可以求解出在某一点的导数值。这种方法称为数值微分。举个例子,对于任何一个$f(x)$,我们当然可以根据定义求出其在$x=x_0$处的导数,即 $$f'(x)|_{x=x_0} = \frac{f(x_0+\varepsilon)-f(x_0 - \varepsilon)}{2\varepsilon }$$ 其中$\varepsilon$是一个很小的正数。但是,如果$f(x)$的表达式非常复杂,那么我们可能无法直接求出导数。此时,我们可以借助数值微分来求解导数值。下面我们以$f(x)=x^2$为例,演示如何使用数值微分求解导数值。 ```python import numpy as np def f(x): return x**2 def numerical_diff(f, x): h = 1e-4 return (f(x+h) - f(x-h)) / (2*h) x = 5.0 ``` 当然,你现在需要用C++来完成这件事。 **[TASK 7]** 补全`operators/autodiff.h`中的`central_difference`函数,实现数值微分,求出$f(x_1, x_2, ..., x_n)$在第$arg$个参数处的导数值。 #### 高等数学中的导数 还记得$z = x + y$,对$x$和$y$分别求导的结果是什么吗?显然,根据多元函数的求导法则,有$\frac{\partial z}{\partial x}=1$,以及$\frac{\partial z}{\partial y}=1$。如果我们再考虑梯度,那么$z$的梯度就是$\nabla z = (1, 1)$。那么,对于更复杂的函数,比如$f(x, y) = x^2 + y^2$,其梯度$\nabla f$又是什么呢? **[TASK 8]** 补全`operators/autodiff.h`中的`Add`类,能够对表达式$z = x + y$求导。 提示:补全`forward`和`backward`函数,分别实现前向传播和反向传播。前向传播:得到`a + b`的值;反向传播,得到`a`和`b`的梯度(也就是`a`、`b`分别对于结果的导数再乘上梯度`d_input`)。 **[TASK 9]** 仿照`Add`类构造`operators/autodiff.h`中的`Mul`类,能够对表达式$z = x \cdot y$求导。 **[TASK 10]** 仿照`Add`类构造`operators/autodiff.h`中的`Log`类,能够对表达式$z = log(x)$求导。提示:使用``提供的`logf`函数。 **[TASK 11]** 仿照`Add`类构造`operators/autodiff.h`中的`Inv`类,能够对表达式$z = 1 / x$求导。 **[TASK 12]** 仿照`Add`类构造`operators/autodiff.h`中的`Sigmoid`类,能够对表达式$z = sigmoid(x)$求导。提示:使用``提供的`expf`函数。 #### 检查结果 做完了?很好,切换到`cc`,执行下面的语句来编译框架 ``` cmake -S . -B build cd build make ``` 如果你已经完成了01,那么环境变量应该是好的。否则,请回到01的实验手册,查看如何修改环境变量。 现在还有一个`frontend/framework/autodiff/test_task7.py`文件。切换到目录`frontend/framework/autodiff/`,直接运行相应的task文件,如果没有任何报错,说明你已经完成了这一关!🎉