有时候,我们无需知道一个函数具体的表达式,借助导数的定义,利用计算机可以求解出在某一点的导数值。这种方法称为数值微分。举个例子,对于任何一个$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$为例,演示如何使用数值微分求解导数值。
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)$求导。提示:使用<cmath>提供的logf函数。
[TASK 11] 仿照Add类构造operators/autodiff.h中的Inv类,能够对表达式$z = 1 / x$求导。
[TASK 12] 仿照Add类构造operators/autodiff.h中的Sigmoid类,能够对表达式$z = sigmoid(x)$求导。提示:使用<cmath>提供的expf函数。
做完了?很好,切换到cc,执行下面的语句来编译框架
cmake -S . -B build
cd build
make
如果你已经完成了01,那么环境变量应该是好的。否则,请回到01的实验手册,查看如何修改环境变量。
现在还有一个frontend/framework/autodiff/test_task7.py文件。切换到目录frontend/framework/autodiff/,直接运行相应的task文件,如果没有任何报错,说明你已经完成了这一关!🎉