目录标题
一、数值微分概念
损失函数的值越小,代表我们选取的参数越适合;想要求得损失函数的最小值,最基本的想法就是对函数求导,解出导数值为 0 的点,并判断它是否为极小值/最小值。
然而,实际的函数直接求导,不容易得到解析解。这时可以用数值微分的方式来求某点处的导数,这在工程上应用非常广泛。
二、数值微分代码
python
# 数值微分求导,传入x时一个标量
def numerical_diff(f, x):
h = 1e-4 # 微小值 0.0001
return (f(x+h) - f(x-h)) / (2 * h)
在这里,我们以 x 为中心,计算它两边各发生微小变化后的差分,可以避免只计算单向增大时的误差。这种方法称为 中心差分。
另外,取微小值 h 时不能太小,这会导致计算机浮点数表示的精度不够,出现舍入误差。
利用数值微分求导,测试代码
函数y = 0.01x^2 + 0.1x 在 x=5 处的导数
y ′ = 0.02 x + 0.1 y' = 0.02x + 0.1 y′=0.02x+0.1
(箭头指向 0.02 x 0.02x 0.02x 处)
x = 5 x = 5 x=5
python
import numpy as np
import matplotlib.pyplot as plt
from common.gradient import numerical_diff
# 原函数 y = 0.01x^2 + 0.1x
def f(x):
return 0.01 * x**2 + 0.1 * x
# 切线方程函数,返回切线函数 y = ax + b
def tangent_line(f, x):
y = f(x)
# 计算x处切线的斜率(利用数值微分计算x处的导数)
a = numerical_diff(f, x)
print("切线斜率为: ", a)
# 根据切线过(x, y)点,计算截距
b = y - a * x
return lambda x: a * x + b
# 定义画图范围
x = np.arange(0.0, 20.0, 0.1)
y = f(x)
# 计算x=5处的切线方程
f_line = tangent_line(f, x=5)
y_line = f_line(x)
plt.plot(x, y) # 原函数曲线
plt.plot(x, y_line) # 切线
# plt.show()
plt.savefig('tangent_line_result.png')

三、偏导数

可将其中一个元素 x x x 看作参数,此时 f f f 可看作关于另一元素 y y y 的函数。
f a ( y ) = x 2 + x y + y 2 f_a(y) = x^2 + xy + y^2 fa(y)=x2+xy+y2
在 x = a x = a x=a 固定的情况下,可计算 f x f_x fx 关于 y y y 的导数。
f a ′ ( y ) = a + 2 y f_a'(y) = a + 2y fa′(y)=a+2y
这种导数称为偏导数,一般记作:
∂ f ∂ y ( x , y ) = x + 2 y \frac{\partial f}{\partial y}(x, y) = x + 2y ∂y∂f(x,y)=x+2y
更一般地来说,一个多元函数 f ( x 1 , x 2 , ⋯ , x n ) f(x_1, x_2, \cdots, x_n) f(x1,x2,⋯,xn) 在点 ( a 1 , a 2 , ⋯ , a n ) (a_1, a_2, \cdots, a_n) (a1,a2,⋯,an) 处对 x i x_i xi 的偏导数定义为:
∂ f ∂ x i ( a 1 , a 2 , ⋯ , a n ) = lim Δ x i → 0 f ( a 1 , ⋯ , a i + Δ x i , ⋯ , a n ) − f ( a 1 , ⋯ , a i , ⋯ , a n ) Δ x i \frac{\partial f}{\partial x_i}(a_1, a_2, \cdots, a_n) = \lim_{\Delta x_i \to 0} \frac{f(a_1, \cdots, a_i + \Delta x_i, \cdots, a_n) - f(a_1, \cdots, a_i, \cdots, a_n)}{\Delta x_i} ∂xi∂f(a1,a2,⋯,an)=Δxi→0limΔxif(a1,⋯,ai+Δxi,⋯,an)−f(a1,⋯,ai,⋯,an)
偏导数同样也可以用数值微分的方式来求,即只改变一个自变量、其它不变,做差分考察函数值的变化率。
四、梯度(基础知识在机器学习就该知道了)
多元函数 f ( x 1 , ... , x n ) f(x_1, \dots, x_n) f(x1,...,xn) 关于每个变量 x i x_i xi 都有偏导数 ∂ f ∂ x i \frac{\partial f}{\partial x_i} ∂xi∂f,在点 a a a 处,这些偏导数定义了一个向量。
∇ f ( a ) = ∂ f ∂ x 1 ( a ) , ... , ∂ f ∂ x n ( a ) \nabla f(a) = \left \\frac{\\partial f}{\\partial x_1}(a), \\dots, \\frac{\\partial f}{\\partial x_n}(a) \\right ∇f(a)=∂x1∂f(a),...,∂xn∂f(a)
这个向量称为 f f f 在点 a a a 的梯度。
例如: f ( x , y ) = x 2 + x y + y 2 f(x, y) = x^2 + xy + y^2 f(x,y)=x2+xy+y2 在 ( 1 , 1 ) (1,1) (1,1) 处的梯度为 3 , 3 3, 3 3,3。

在函数的极小值、极大值和鞍点处,梯度为 0。
需要注意的是,梯度代表的其实是函数值增大最快的方向;在实际应用中,我们需要寻找损失函数的最小值,所以一般选择 负梯度 向量。同样地,负梯度代表的是函数值减小最快的方向,并不一定直接指向函数图像的最低点。
五、利用数值微分求梯度代码

python
import numpy as np
# 数值微分求梯度,传入x是一个向量
# 这个函数其实是在做多元函数的数值偏导,每次只针对一个维度做微小的改变,其他维度保持原样
# 比如传入 x = [3,4,6,77] , x.size = 4,循环四次, 以i=0为例
# * `tmp = x[0]`,此时 `tmp` 保存了 3(为了后面恢复用)。
# * `x[0] = tmp + h`,现在 `x` 变成了 `[3 + h, 4, 6, 77]`。
# * `fxh1 = f(x)`,算出了点 `x[0] + h` 处的函数值。
# * `x[0] = tmp - h`,现在 `x` 变成了 `[3 - h, 4, 6, 77]`。
# * `fxh2 = f(x)`,算出了点 `x[0] - h` 处的函数值。
# * `grad[0] = (fxh1 - fxh2) / (2 * h)`,用中心差分公式算出了对第一个变量 $x_1$ 的偏导数。
# * `x[0] = tmp`,把 `x` 恢复成 `[3, 4, 6, 77]`。
def _numerical_gradient(f, x):
h = 1e-4
grad = np.zeros_like(x)
# 遍历x中的特征xi(在求偏导时候,要改变当前xi,改完要还原不防止可能影响别的偏导)
for i in range(x.size):
tmp = x[i]
x[i] = tmp + h
fxh1 = f(x)
x[i] = tmp - h
fxh2 = f(x)
# 利用中心差分公式计算偏导数
grad[i] = (fxh1 - fxh2) / (2*h)
# 恢复x[i]的值
x[i] = tmp
return grad
# 传入X是一个矩阵
# 加入传入的是 W 是(2,4)矩阵, 调用两次_numerical_gradient,每次传入向量(4,)就是一维数组里面四个权重,参考_numerical_gradient的举例。
# 也就说一个W (2,4)矩阵算梯度要循环8次,每次执行两次前向传播
def numerical_gradient(f, X):
# 判断维度
if X.ndim == 1:
return _numerical_gradient(f, X)
else:
grad = np.zeros_like(X)
# 遍历X中的每一行数据,分别求梯度
for i, x in enumerate(X):
grad[i] = _numerical_gradient(f, x)
return grad