梯度检查

通过数值梯度(numerical gradient)和解析梯度(analytic gradient)的比较进行梯度检查,这个过程有助于得到更准确的网络

学习Gradient checks中提到的技巧和注意事项

中心差分

使用中心差分公式(the centered difference formula)能够更好的计算数值梯度

\[ \frac {df(x)}{dx} =\frac {f(x+h) - f(x-h)}{2h} \]

步长\(h\)是一个极小值,当前取值为\(1e^{-5}\)

相对误差比较

计算数值梯度\(f_{n}'\)和解析梯度\(f_{a}'\)的相对误差,同时除以其中最大值以消除量纲,最后得到的值能够有一个统一的评判标准

\[ \frac {|f_{n}' - f_{a}'|}{\max (|f_{n}'|, |f_{a}'|, 1e^{-8})} \]

使用\(max\)以及\(1e^{-8}\)是为了避免最大值为\(0\)以及两个值均为\(0\)的情况

评判标准

  • 相对误差大于\(1e^{-2}\)通常意味着梯度可能是错误的
  • 相对误差在\([1e^{-2}, 1e^{-4}]\)之间同样不太准确
  • 相对误差小于\(1e^{-4}\),如果目标存在扭结(kinks),比如tanh非线性或者softmax,那么这个结果是可以接受的;否则,\(1e^{-4}\)仍然太高
  • 相对误差小于\(1e^{-7}\)是最好的结果

深度网络的相对误差变得更大,所以对于10层网络而言,其相对误差小于\(1e^{-2}\)可能就很好了,因为误差在传递过程中累积;相反,对于单个可微函数而言,\(1e^{-2}\)表明梯度不正确

使用双精度

使用双精度(double precision)数据进行梯度检查能够得到更准确的相对误差

保持浮点数有效范围

参考:What Every Computer Scientist Should Know About Floating-Point Arithmetic

在梯度计算过程中,如果梯度的有效范围(大部分梯度的取值)太小,会造成更多的数值问题(numerical issues

可以通过打印原始数值/解析梯度的方式查看,也可以通过IDE调试工具进行查看。如果数值梯度过小,比如取值在\(1e^{-10}\)左右甚至更小,可以通过放大损失函数值进行调整,理想情况下在\(1.0\)的数量级,即浮点数的指数为\(0\)

目标中的扭结

相对误差过大的一个原因在于扭结(kink)的问题,扭结是指目标函数的不可微部分(non-differentiable parts),比如ReLU函数在\(x=0\)

假定输入数据\(x=-1e^{-6}\),因为x小于0,所以该点的解析梯度为0,然而如果\(h>1e^{-6}\),那么\(f(x+h)>0\),数值梯度大于0,会得到一个较大的相对误差

查找是否出现扭结需要追踪所有激活函数\(\max(x,y)\)winner的身份,比如在前向计算中\(x\)或者\(y\)的值更大,但是在\(f(x+h)\)\(f(x-h)\)的计算中winner身份发生了改变,那么表明出现了扭结现象

一种解决技巧是使用少量的数据点进行测试,降低发生扭结的可能性

谨慎设置步长h

参考:Numerical differentiation

步长\(h\)不一定是越小越好,因为过小的步长有可能会产生数值精度问题。当梯度无法检查时,可以尝试改变步长为\(1e^{-4}\)\(1e^{-6}\)

Gradcheck during a “characteristic” mode of operation

使用随机参数对网络进行梯度检查可能会引入病理边缘病例,并掩盖梯度的错误实现,也就是梯度看起来正确实现了,但实际上并没有

最好先训练一段时间,在损失值开始下降后再进行梯度检查,更能够保证网络的准确性

不要让正则化项影响数据

损失函数包括数据损失以及正则化损失,如果正则化损失比数据损失大,那么主要的梯度将来自于正则化项,这会掩盖数据丢失梯度的错误实现

所以梯度检查时可以先单独检查数据损失,再检查正则化损失。检查正则化损失的方式一方面可以去除数据损失相关代码,另一方面可以提高正则化强度,使正则化梯度无法被忽略

关闭随机失活/数据扩充

执行梯度检查的过程中,应该关闭网络中任何非确定性(non-deterministic)的影响,比如随机失活(dropout)、随机数据扩充(random data augmentation),因为在计算数值梯度时,这些参数会明显的带来巨大的误差

关闭操作会导致无法梯度检查这些参数,一种更好的解决方案是在计算\(f(x+h)\)\(f(x-h)\)过程中设置特定的随机数种子

检查少量维度

实际操作中网络存在上百万的参数,这种情况下只能假设大部分参数是正确的,仅检查少数参数,特别注意的是确保在不同参数的某些维度进行梯度检查

python实现

cs231n课程的课后作业assignment中已实现了数值梯度计算 - gradient_check.py,主要操作的是前两个函数:

def eval_numerical_gradient(f, x, verbose=True, h=0.00001): def eval_numerical_gradient_array(f, x, df, h=1e-5):

  • 参数\(f\)表示待检查的方法/类/网络
  • 参数\(x\)表示输入数据
  • 参数\(df\)表示反向传播中回传的梯度

两者均采用中心差分公式计算数值梯度,区别在于前一个函数的\(f\)输出值是单个数值,而后者\(f\)的输出值可以是数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# -*- coding: utf-8 -*-

# @Time : 19-8-1 下午7:40
# @Author : zj


import numpy as np


def eval_numerical_gradient(f, x, verbose=True, h=0.00001):
"""
a naive implementation of numerical gradient of f at x
- f should be a function that takes a single argument
- x is the point (numpy array) to evaluate the gradient at
"""

fx = f(x) # evaluate function value at original point
grad = np.zeros_like(x)
# iterate over all indexes in x
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:

# evaluate function at x+h
ix = it.multi_index
oldval = x[ix]
x[ix] = oldval + h # increment by h
fxph = f(x) # evalute f(x + h)
x[ix] = oldval - h
fxmh = f(x) # evaluate f(x - h)
x[ix] = oldval # restore

# compute the partial derivative with centered formula
grad[ix] = (fxph - fxmh) / (2 * h) # the slope
if verbose:
print(ix, grad[ix])
it.iternext() # step to next dimension

return grad


def eval_numerical_gradient_array(f, x, df, h=1e-5):
"""
Evaluate a numeric gradient for a function that accepts a numpy
array and returns a numpy array.
"""
grad = np.zeros_like(x)
it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
ix = it.multi_index

oldval = x[ix]
x[ix] = oldval + h
pos = f(x).copy()
x[ix] = oldval - h
neg = f(x).copy()
x[ix] = oldval

grad[ix] = np.sum((pos - neg) * df) / (2 * h)
it.iternext()
return grad


def compute_value(x):
return np.sum(x)


def compute_value_array(x):
return x + x


if __name__ == '__main__':
x = np.arange(9., dtype=np.double).reshape(3, 3)

grad = eval_numerical_gradient(compute_value, x, verbose=False)
print(grad)

grad = eval_numerical_gradient_array(compute_value_array, x, 1)
print(grad)

参考cs231n实现了全连接神经网络:nn_classifier.py,包含了全连接以及ReLU层的前后向运算,神经网络配置、训练及预测功能

同时实现了全连接神经网络的测试文件:test_nn_classifier.py,里面包含了对全连接操作、ReLU操作、softmax损失操作以及2层和3层网络的测试

相关阅读