数值微分和梯度法

小鸡
阅读712 喜欢4 算法 更新2019-5-20

梯度法使用梯度的信息决定前进的方向

数值微分

利用微小的差分求导数的过程称为数值微分。以计算函数f在(x + h)和(x − h)之间的差分。因为这种计算方法以x为中心,计算它左右两边的差分,所以也称为中心差分(而(x + h)和x之间的差分称为前向差分)。

  1. 用python代码简单表示数值微分,
def numerical_diff(f, x):
h = 1e-4 # 0.0001
return (f(x+h) - f(x-h)) / (2*h)
  1. 用python代码求偏导数
  • 求x0 = 3, x1 = 4时,关于x0的偏导数。
def function_tmp1(x0):
return x0*x0 + 4.0**2.0
numerical_diff(function_tmp1, 3.0)

输出 6.00000000000378

  • 求x0 = 3, x1 = 4时,关于x1的偏导数 。
def function_tmp2(x1):
return 3.0**2.0 + x1*x1

numerical_diff(function_tmp2, 4.0)

输出:7.999999999999119

梯度

(f(x0), f(x1))这样的由全部变量的偏导数汇总而成的向量称为梯度(gradient)。梯度可以像下面这样来实现。

# 求中心差分
def num_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x)

it = np.nditer(x, flags=[multi_index], op_flags=[readwrite])
while not it.finished:
idx = it.multi_index
tmp_val = x[idx]
x[idx] = float(tmp_val) + h
fxh1 = f(x) # f(x+h)

x[idx] = tmp_val - h
fxh2 = f(x) # f(x-h)
grad[idx] = (fxh1 - fxh2) / (2*h)

x[idx] = tmp_val # 还原值
it.iternext()

return grad

np.zeros_like(x)会生成一个形状和x相同、所有元素都为0的数组。参数f为函数。用一个for循环将所有参数的偏导数求出并放入grad数组中。这里用到np.nditer函数来对多维的输入变量进行遍历,求出变量数组中每个元素的差分

np.nditer用法:顺序遍历多维数组的元素并进行一些操作

梯度会指向各点处的函数值降低的方向。更严格地讲,梯度指示的方向是各点处的函数值减小最多的方向

梯度法

机器学习的主要任务是在学习时寻找最优参数。同样地,神经网络也必须在学习时找到最优参数(权重和偏置)。这里所说的最优参数是指损失函数取最小值时的参数。但是,一般而言,损失函数很复杂,参数空间庞大,无法确定何处能取得最小值。而通过梯度来寻找函数最小值的方法就是梯度法。

注意:梯度法找到的当前梯度最小值点不一定是函数的最小值点,可能是局部的最小值。在复杂的函数中,梯度指示的方向基本上都不是函数值最小处。

在梯度法中,函数的取值从当前位置沿着梯度方向前进一定距离,然后在新的地方重新求梯度,再沿着新梯度方向前进,如此反复,不断地沿梯度方向前进。直到梯度的下降小于某个设定的阙值,也就是停止条件。

严格地讲,寻找最小值的梯度法称为梯度下降法,寻找最大值的梯度法称为梯度上升法。但是通过反转损失函数的符号,求最小值的问题和求最大值的问题会变成相同的问题,因此“下降”还是“上升”的差异本质上并不重要。一般来说,神经网络(深度学习)中,梯度法主要是指梯度下降法

各函数参数梯度下降的数学表达式如下


η表示更新量,在深度学习中也叫学习率,学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。这个学习率要根据实际情况适中设定,不然太小或太大都会影响结果。在神经网络的学习中,一般会一边改变学习率的值,一边确认学习是否正确进行了

用python简单实现梯度下降法

import numpy as np

def gradient_descent(fun, init_x, lr=0.01, setp_num=100):
x =init_x
for i in range(setp_num):
grad = num_gradient(fun, x)
x -= lr * grad
return x

def function1(x):
return x[0]**2 + x[1]**2 - 5 * x[2]

init_x = np.array([-3.0, 4.0, 6.0])
grad1 = gradient_descent(function1, init_x, lr=0.01, setp_num=10000)
print(grad1)

输出:[-3.38147288e-11 3.15267812e-11 5.06000000e+02]

关于学习率的设置

学习率过大的话,会发散成一个很大的值;反过来,学
习率过小的话,基本上没怎么更新就结束了。也就是说,设定合适的学习率
是一个很重要的问题。

学习率过大的例子:lr=10.0
>>> init_x = np.array([-3.0, 4.0])
>>> gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100)
array([ -2.58983747e+13, -1.29524862e+12])
学习率过小的例子:lr=1e-10
>>> init_x = np.array([-3.0, 4.0])
>>> gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100)
array([-2.99999994, 3.99999992])

像学习率这样的参数称为超参数。它和神经网络的参数(权重和偏置)性质不同。相对于神经网络的权重参数是通过训练数据和学习算法自动获得的,学习率这样的超参数则是人工设定的。一般来说,超参数需要尝试多个值,以便找到一种可以使学习顺利进行的设定。

神经网络的梯度

神经网络的梯度是指损失函数关于权重参数的梯度。比如,有一个只有一个形状为2 × 3的权重W的神经网络,损失函数用L表示。此时,梯度可以dL/dW用表示。用数学式表示的话,如下所示


下面用一个simpleNet的类来表示一个简单的一层神经网络。

import sys, os
import numpy as np

# 求中心差分
def num_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x)

it = np.nditer(x, flags=[multi_index], op_flags=[readwrite])
while not it.finished:
idx = it.multi_index
tmp_val = x[idx]
x[idx] = float(tmp_val) + h
fxh1 = f(x) # f(x+h)

x[idx] = tmp_val - h
fxh2 = f(x) # f(x-h)
grad[idx] = (fxh1 - fxh2) / (2*h)

x[idx] = tmp_val # 还原值
it.iternext()

return grad

# 批处理交叉熵误差
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)

# 监督数据是one-hot-vector的情况下,转换为正确解标签的索引
if t.size == y.size:
t = t.argmax(axis=1)

batch_size = y.shape[0]
return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

# softmax激活函数
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

# 一个简单的一层神经网络
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

if __name__ == "__main__":
net = simpleNet()
x =np.array([0.6, 0.9])
t = np.array([0, 0, 1])
def f(w):
return net.loss(x, t)

dw = num_gradient(f, net.W)
print(dw)

输出如下

[[ 0.49075392  0.06515262 -0.55590654]
[ 0.73613088 0.09772893 -0.83385981]]

这个输出的梯度矩阵的各个元素是loss函数对各个权值的求导,比如该矩阵的(1,3)和(2,3)元素值为负数,说明损失函数loss在这两个点正方向递降,权值w13w23应该往正方向更新(比如一元递减函数 y=-x,x越大y值越小)

相同的,权值w11w12w21w22就应该往负方向更新(权值减少)

学习算法的实现

前面的算法涉及函数颇多,现在整理一下整个学习算法的过程。

概念前提

神经网络的学习按照下面4个步骤进行。这个方法通过梯度下降法更新参数,不过因为这里使用的数据是随机选择的minibatch数据,所以又称为随机梯度下降法

神经网络存在合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为学习。神经网络的学习分成下面4个步骤。

步骤1(mini-batch)

从训练数据中随机选出一部分数据,这部分数据称为mini-batch。我们的目标是减小mini-batch的损失函数的值。

步骤2(计算梯度)

为了减小mini-batch的损失函数的值,需要求出各个权重参数的梯度。梯度表示损失函数的值减小最多的方向。

步骤3(更新参数)

将权重参数沿梯度方向进行微小更新。

步骤4(重复)

重复步骤1、步骤2、步骤3。

深度学习的很多框架中,随机梯度下降法一般由一个名为SGD的函数来实现。SGD来源于随机梯度下降法的英文名称的首字母。“随机”指的是“随机选择的”的意思,因此,随机梯度下降法是“对随机选择的数据进行的梯度下降法”。

2层神经网络的类

在此之前,先用一个思维导图理清要做什么


python代码如下

import sys, os
import numpy as np
from functions import *
from gradient_descent import num_gradient

class TwolayerNet:
def __init__(self, in_size, hide_size, out_size):
self.par = {}
self.par[w1] = np.random.randn(in_size, hide_size)
self.par[w2] = np.random.randn(hide_size,out_size)
self.par[b1] = np.zeros(hide_size)
self.par[b2] = np.zeros(out_size)
# 神经网络的推理
def predict(self, x):
z1 = sigmoid(np.dot(x, self.par[w1]) + self.par[b1])
z2 = softmax(np.dot(z1, self.par[w2]) + self.par[b2])
return z2
# 损失函数
def loss(self, x, t):
y = self.predict(x)
return cross_entropy_error(y, t)

def accuracy(self,x, t):
pass
# 计算梯度
def num_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}

grads[w1] = num_gradient(loss_W, self.par[w1])
grads[b1] = num_gradient(loss_W, self.par[b1])
grads[w2] = num_gradient(loss_W, self.par[w2])
grads[b2] = num_gradient(loss_W, self.par[b2])
return grads

使用mini-batch实现梯度法

就是从训练数据中随机选择一部分数据(称为mini-batch),再以这些mini-batch为对象,使用梯度法更新参数的过程。

# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 为了导入父目录的文件而进行的设定
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

iters_num = 5000 # 适当设定循环的次数
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1

train_loss_list = []

for i in range(iters_num):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

# 计算梯度
#grad = network.numerical_gradient(x_batch, t_batch)
grad = network.gradient(x_batch, t_batch)

# 更新参数
for key in (W1, b1, W2, b2):
network.params[key] -= learning_rate * grad[key]

loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)

y = train_loss_list
x = np.arange(0, len(y))
plt.plot(x, y)
plt.show()

损失函数下降图像如下


基于测试数据的评价

上面使用mini-batch方法对神经网络进行学习使损失函数的值逐渐减小。不过这个损失函数的值,严格地讲是“对训练数据的某个mini-batch的损失函数”的值。训练数据的损失函数值减小,虽说是神经网络的学习正常进行的一个信号,但光看这个结果还不能说明该神经络在其他数据集上也一定能有同等程度的表现。

神经网络的学习中,必须确认是否能够正确识别训练数据以外的其他数据,即确认是否会发生过拟合

神经网络学习的最初目标是掌握泛化能力,因此,要评价神经网络的泛化能力,就必须使用不包含在训练数据中的数据。下面的代码在进行学习的过程中,会定期地对训练数据和测试数据记录识别精度。这里,每经过一个epoch,都会记录下训练数据和测试数据的识别精度。

epoch是一个单位。一个epoch表示学习中所有训练数据均被使用过一次时的更新次数。比如,对于10000笔训练数据,用大小为100笔数据的mini-batch进行学习时,重复随机梯度下降法100次,所有的训练数据就都被“看过”了A。此时,100次就是一个 epoch

通过观察测试数据与训练数据的精确度来判断学习过程中是否存在过拟合,python代码如下(被省略的代码见上一段)。

# 
#
# 参数配置的代码
#
#

iters_num = 10000 # 适当设定循环的次数
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.05

train_loss_list = []
train_acc_list = []
test_acc_list = []

iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
#
#
# 学习过程的代码
#
#

if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))

# 绘制图形
markers = {train: o, test: s}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label=train acc)
plt.plot(x, test_acc_list, label=test acc, linestyle=--)
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc=lower right)
plt.show()

输出如下

train acc, test acc | 0.11236666666666667, 0.1135
train acc, test acc | 0.51685, 0.5168
train acc, test acc | 0.7925, 0.7995
train acc, test acc | 0.8524166666666667, 0.8574
train acc, test acc | 0.8783, 0.8831
train acc, test acc | 0.8917333333333334, 0.8956
train acc, test acc | 0.8984833333333333, 0.9003
train acc, test acc | 0.9038333333333334, 0.9067
train acc, test acc | 0.9076833333333333, 0.9109
train acc, test acc | 0.9114333333333333, 0.9136
train acc, test acc | 0.9134166666666667, 0.9175
train acc, test acc | 0.9171666666666667, 0.9184
train acc, test acc | 0.9185333333333333, 0.921
train acc, test acc | 0.9209666666666667, 0.9226
train acc, test acc | 0.9226666666666666, 0.924
train acc, test acc | 0.9254833333333333, 0.9274
train acc, test acc | 0.9274, 0.9278

可以看出,每次测试数据的精确度都很接近训练数据的精确度。

结果用图表示,可以看出这个学习过程没有产生过拟合


总结

神经网络的学习过程:首先,为了能顺利进行神经网络的学习,导入了损失函数这个指标。以这个损失函数为基准,找出使它的值达到最小的权重参数,就是神经网络学习的目标。为了找到尽可能小的损失函数值,引入了使用函数斜率的梯度法

  • 机器学习中使用的数据集分为训练数据测试数据
  • 神经网络用训练数据进行学习,并用测试数据评价学习到的模型的泛化能力
  • 神经网络的学习以损失函数为指标,更新权重参数,以使损失函数的值减小。
  • 利用某个给定的微小值的差分求导数的过程,称为数值微分。
  • 利用数值微分,可以计算权重参数的梯度
  • 数值微分虽然费时间,但是实现起来很简单。稍微复杂一些的误差反向传播法可以高速地计算梯度。

:本文为斋藤康毅的《深度学习入门:基于Python的理论与实现》片段摘抄与学习笔记