本文以及后续关于 Torch 应用及机器学习相关的笔记文章,均基于 牛津大学2015机器学习课程 ,课件和视频可从 官网 下载。本文主要关于神经网络模型中的随机梯度下降法,介绍其原理及推导过程,并比较 Python 简单实现和 Torch 的应用。对应课件为 L2-Linear-prediction.ipynb 。
为了确定神经网络模型中参数(权值)的好坏,需要先指定一个衡量标准(训练误差,损失函数,目标函数),例如以均方差(Mean Square Error, MSE)公式作为损失函数:
$$J(/mathbf{/theta}) = MSE = /frac{1}{n} /sum_{i = 1}^n(/widehat{/mathbf{Y_i}} - /mathbf{Y_i})^2$$
其中,$/widehat{y_i} = /sum_{j = 1}^d x_{ij}/theta_j$,矩阵表示法为$/widehat{/mathbf{Y}} = /mathbf{X}/theta$,为线性模型(神经网络)拟合结果。
模型最优化实际上是最小化损失函数的过程,梯度下降法的原理是:
若函数 $F(x)$ 在点 $a$ 可微且有定义,则 $F(x)$ 在 $a$ 点沿着梯度相反方向 $-/nabla F(a)$ 下降最快。 梯度下降法 - 维基百科
损失函数 $J$ 对于权重向量 $/mathbf{/theta}$ 的梯度(gradient):
$$/nabla J(/mathbf{/theta}) = [/frac{/partial J}{/partial /theta_0}, /frac{/partial J}{/partial /theta_1}, ..., /frac{/partial J}{/partial /theta_n}]$$
则根据梯度下降法则,参数的变化应根据:
$$/Delta /theta_i = -/alpha/frac{/partial J}{/partial /theta_i}$$
其中 $/alpha$ 为学习速率(Learning Rate)。由此可得梯度下降算法如下:
根据算法描述可以简单实现( 完整代码 ):
def GD(training/_examples, alpha): # init thetas thetas = np.random.rand(NPAMATERS) for i in range(LOOPS): deltas = np.zeros(NPAMATERS) for record in training/_examples: inputs = [1] + list(record[1:]) output = record[0] o = NN(inputs, thetas) for j in range(NPAMATERS): # -- Step (A deltas[j] = deltas[j] + alpha /* (output - o) /* inputs[j] for j in range(NPAMATERS): # -- Step (B thetas[j] = thetas[j] + deltas[j] return thetas thetas = GD(training/_examples, 0.00001) test(thetas, training/_examples) """ #No Target Prediction 0 40 20.55 1 44 37.96 2 46 44.42 3 48 48.66 4 52 52.89 5 58 54.89 6 60 67.83 7 68 63.13 8 74 69.59 9 80 89.00 """
梯度下降法中计算 $/Delta /theta_i$ 时汇总了所有训练样本数据的误差,在实践过程中可能出现以下问题:
需要注意的是,学习速率的选择很重要,$/alpha$ 越小相当于沿梯度下降的步子越小。很显然,步子越小,到达最低点所需要迭代的次数就越多,或者说收敛越慢;但步子太大,又容易错过最低点,走向发散的高地。在我写的这一个简单实现的测试中,取 $/alpha = 1e-3$ 时导致无法收敛,而取 $/alpha = 1e-5$ 时可收敛,但下降速度肯定更慢。
常见的改进方案是随机梯度下降法(stochatic gradient descent procedure, SGD),SGD 的原理是根据每个单独的训练样本的误差对权值进行更新,针对上面的算法描述,删除 $(B$,将$(A$ 更新为:
$$/theta_i = /theta_i + /alpha (output - o) * x_i$$
代码如下:
def SGD(training/_examples, alpha): # init thetas thetas = np.random.rand(NPAMATERS) for i in range(LOOPS): for record in training/_examples: inputs = [1] + list(record[1:]) output = record[0] o = NN(inputs, thetas) for j in range(NPAMATERS): thetas[j] = thetas[j] + alpha /* (output - o) /* inputs[j] return thetas thetas = SGD(training/_examples, 0.001) test(thetas, training/_examples) """ #No Target Prediction 0 40 41.45 1 44 42.71 2 46 44.82 3 48 48.42 4 52 52.02 5 58 57.11 6 60 61.34 7 68 70.88 8 74 72.99 9 80 79.33 """
可以看出,SGD 可以用较大的 $/alpha$ 获得较好的优化结果。
清楚了 SGD 的原理后,再来应用 Torch 框架完成上上述过程,其中神经网络模型的框架由 torch/nn 提供。
require 'torch' require 'optim' require 'nn' model = nn.Sequential() -- 定义容器 ninputs = 2; noutputs = 1 model:add(nn.Linear(ninputs, noutputs)) -- 向容器中添加一个组块(层),本例中只有一个组块。 criterion = nn.MSECriterion() -- 获取初始化参数 x, dl_dx = model:getParameters() -- print(help(model.getParameters)) --[[ [flatParameters, flatGradParameters] getParameters() 返回两组参数,flatParameters 学习参数(flattened learnable parameters);flatGradParameters 梯度参数(gradients of the energy wrt) ]]-- feval = function(x_new) -- 用于SGD求值函数 -- 输入:设定权值 -- 输出:损失函数在该训练样本点上的损失 loss_x, -- 损失函数在该训练样本点上的梯度值 dl_dx if x ~= x_new then x:copy(x_new) end -- 每次调用 feval 都选择新的训练样本 _nidx_ = (_nidx_ or 0) + 1 if _nidx_ > (#data)[1] then _nidx_ = 1 end local sample = data[_nidx_] local target = sample[{ {1} }] local inputs = sample[{ {2, 3} }] dl_dx:zero() -- 每次训练新样本时都重置dl_dx为0 local loss_x = criterion:forward(model:forward(inputs), target)) -- print(help(model.forward)) --[[ [output] forward(input) 接收 input 作为参数,返回经该模型计算得到的 output,调用 forward() 方法后,模型的 output 状态更新。 ]]-- -- print(help(criterion.forward)) --[[ [output] forward(input, target) 给定 input 和(要拟合的)目标 target,根据损失函数公式求出损失值。 状态变量 self.output 会更新。 --]] model:backward(inputs, criterion:backward(model.output, target)) -- print(help(criterion.backward)) --[[ [gradInput] backward(input, target) 给定 input 和(要拟合的)目标 target,根据损失函数公式求出梯度值。 状态变量 self.gradInput 会更新。 --]] -- @ https://github.com/torch/nn/blob/948ac6a26cc6c2812e04718911bca9a4b641020e/doc/module.md#nn.Module.backward --[[ [gradInput] backward(input, gradOutput) 调用下面两个函数: 1. updateGradInput(input, gradOutput) 2. accGradParameters(input, gradOutput, scale) --]] return loss_x, dl_dx end -- 设置 SGD 算法所需参数 sgd_params = { learningRate = 1e-3, learningRateDecay = 1e-4, weightDecay = 0, momentum = 0 } for i = 1, 1e4 do for i = 1, (#data)[1] do -- optim.sgd@https://github.com/torch/optim/blob/master/sgd.lua _, fs = optim.sgd(feval, x, sgd_params) end end -- Test test = {40.32, 42.92, 45.33, 48.85, 52.37, 57, 61.82, 69.78, 72.19, 79.42} print('id/tapprox/ttext') for i = 1, (#data)[1] do local myPrediction = model:forward(data[i][{{2,3}}]) print(string.format("%2d/t%.2f/t%.2f", i, myPrediction[1], test[i])) end --[[ id approx text 1 40.10 40.32 2 42.77 42.92 3 45.22 45.33 4 48.78 48.85 5 52.34 52.37 6 57.02 57.00 7 61.92 61.82 8 69.95 69.78 9 72.40 72.19 10 79.74 79.42 --]]
关于 Torch 的 Neural Network Package 在 GitHub 上有更详细的 文档介绍 ,这里暂时不作深入学习,根据后续课程进度再做补充。
- END -