梯度
上篇文章我们按变量分别计算了x0和x1的偏导数。现在,我们希望一起计算x0和x1的偏导数。比如,我们来考虑求x0 = 3, x1 = 4时(x0, x1)的偏导数(αf/αx0,αf/αx1) 。这样的由全部变量的偏导数汇总而成的向量称为梯度(gradient)。梯度可以像下面这样来实现。
import numpy as np
def function_2(x):
return x[0] ** 2 + x[1] ** 2
def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x) # 生成和x形状相同的数组
for idx in range(x.size): # 如X[3,4],idx=0,1
tmp_val = x[idx]
x[idx] = tmp_val + h # f(x+h)的计算
fxh1 = f(x)
x[idx] = tmp_val - h # f(x-h)的计算
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2 * h) # 梯度的计算
x[idx] = tmp_val # 还原X为[3,4]
return grad
print(numerical_gradient(function_2, np.array([3.0, 4.0]))) # [6. 8.]
为了更好地理解,我们把f(x0,x1)=x02+x12的梯度画在图上。不过,这里我们画的是元素值为负梯度的向量,负梯度方向是梯度法中变量的更新方向。
可以看出,梯度会指向各点处的函数值降低的方向。更严格地讲,梯度指示的方向是各点处的函数值减小最多的方向。
梯度法
机器学习的主要任务是在学习时寻找最优参数。同样地,神经网络也必须在学习时找到最优参数(权重和偏置)。这里所说的最优参数是指损失函数。取最小值时的参数。但是,一般而言,损失函数很复杂,参数空间庞大,我们不知道它在何处能取得最小值。而通过巧妙地使用梯度来寻找函数最小值(或者尽可能小的值)的方法就是梯度法。
在梯度法中,函数的取值从当前位置沿着梯度方向前进一定距离,然后在新的地方重新求梯度,再沿着新梯度方向前进,如此反复,不断地沿梯度方向前进。像这样,通过不断地沿梯度方向前进,逐渐减小函数值的过程就是梯度法(gradient method)。梯度法是解决机器学习中最优化问题的常用方法,特别是在神经网络的学习中经常被使用。根据目的是寻找最小值还是最大值,梯度法的叫法有所不同。严格地讲,寻找最小值的梯度法称为梯度下降法(gradient descent method),寻找最大值的梯度法称为梯度上升法(gradient ascent method)。但是通过反转损失函数的符号,求最小值的问题和求最大值的问题会变成相同的问题,因此“下降”还是“上升”的差异本质上并不重要。一般来说,神经网络(深度学习)中,梯度法主要是指梯度下降法。
梯度法的数学式如下:
上式中的η表示更新量,在神经网络的学习中,称为学习率(learning rate)。学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。该式是表示更新一次的式子,这个步骤会反复执行。也就是说,每
一步都按该式更新变量的值,通过反复执行此步骤,逐渐减小函数值。虽然这里只展示了有两个变量时的更新过程,但是即便增加变量的数量,也可以通过类似的式子(各个变量的偏导数)进行更新。
学习率需要事先确定为某个值,比如0.01或0.001。一般而言,这个值过大或过小,都无法抵达一个“好的位置”。在神经网络的学习中,一般会一边改变学习率的值,一边确认学习是否正确进行了。下面,我们用Python来实现梯度下降法,如下所示:
import numpy as np
def function_2(x):
return x[0] ** 2 + x[1] ** 2
def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x) # 生成和x形状相同的数组
for idx in range(x.size): # 如X[3,4],idx=0,1
tmp_val = x[idx]
x[idx] = tmp_val + h # f(x+h)的计算
fxh1 = f(x)
x[idx] = tmp_val - h # f(x-h)的计算
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2 * h) # 梯度的计算
x[idx] = tmp_val # 还原X为[3,4]
return grad
def gradient_descent(f, init_x, lr=0.01, step_num=200):
x = init_x
for i in range(step_num):
grad = numerical_gradient(f, x)
x -= lr * grad
return x
print(gradient_descent(function_2, np.array([-3.0, 4.0]))) # [-0.05276384 0.07035179]
print(gradient_descent(function_2, np.array([-3.0, 4.0]), lr=10)) # [-2.58983747e+13 -1.29524862e+12]
print(gradient_descent(function_2, np.array([-3.0, 4.0]), lr=1e-4)) # [-2.88235679 3.84314238]
可以看出,学习率过大的话,会发散成一个很大的值;反过来,学习率过小的话,基本上没怎么更新就结束了。也就是说,设定合适的学习率是一个很重要的问题。
像学习率这样的参数称为超参数。这是一种和神经网络的参数(权重和偏置)性质不同的参数。相对于神经网络的权重参数是通过训练数据和学习算法自动获得的,学习率这样的超参数则是人工设定的。一般来说,超参数需要尝试多个值,以便找到一种可以使学习顺利进行的设定。
神经网络的梯度实现
神经网络的学习也要求梯度。这里所说的梯度是指损失函数关于权重参数的梯度。比如,有一个只有一个形状为2 × 3的权重W的神经网络,损失函数用L表示。此时,梯度可以用αL/αW表示。用数学式表示如下:
下面,我们以一个简单的神经网络为例,来实现求梯度的代码。为此,我们要实现一个名为simpleNet的类:
class simpleNet:
def __init__(self):
self.W = np.random.randn(2, 3) # 用高斯分布进行初始化
def predict(self, x):
return np.dot(x, self.W)
def loss(self, x, t):
z = self.predict(x)
y = softmax(z)
loss = cross_entropy_error(y, t)
return loss
为什么要建立一个类呢?我们要求L和W的梯度,而我们的损失函数与x和t相关,t是正确解标签,是常数,所以问题在于怎样将w与x联系起来。所以建立一个类,求损失函数时,w改变x随之改变。程序完整实现如下:
import os
import sys
import numpy as np
sys.path.append(os.pardir)
def softmax(a):
c = np.max(a)
exp_a = np.exp(a - c) # 溢出对策
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
batch_size = y.shape[0]
return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x) # 生成和x形状相同的数组
for idx in range(x.size): # 如X[3,4],idx=0,1
tmp_val = x[idx]
x[idx] = tmp_val + h # f(x+h)的计算
fxh1 = f(x)
x[idx] = tmp_val - h # f(x-h)的计算
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2 * h) # 梯度的计算
x[idx] = tmp_val # 还原X为[3,4]
return grad
def numerical_gradient_2d(f, x):
if x.ndim == 1:
return numerical_gradient(f, x)
else:
grad = np.zeros_like(x)
for idx, x in enumerate(x):
grad[idx] = numerical_gradient(f, x)
return grad
class simpleNet:
def __init__(self):
self.W = np.random.randn(2, 3) # 用高斯分布进行初始化
def predict(self, x):
return np.dot(x, self.W)
def loss(self, x, t):
z = self.predict(x)
y = softmax(z)
loss = cross_entropy_error(y, t)
return loss
net = simpleNet()
x = np.array([0.6, 0.9])
t = np.array([0, 0, 1]) # 正确解标签
f = lambda w:net.loss(x, t)
dw = numerical_gradient_2d(f,net.W)
print(dw)