美文网首页Python羽升笈理科生的果壳
神经网络模型随机梯度下降法—简单实现与Torch应用

神经网络模型随机梯度下降法—简单实现与Torch应用

作者: 蛙声一爿 | 来源:发表于2016-02-26 21:15 被阅读709次

    备份自:http://blog.rainy.im/2016/02/26/sgd-with-python-and-torch/

    About

    本文以及后续关于 Torch 应用及机器学习相关的笔记文章,均基于牛津大学2015机器学习课程,课件和视频可从官网下载。本文主要关于神经网络模型中的随机梯度下降法,介绍其原理及推导过程,并比较 Python 简单实现和 Torch 的应用。对应课件为L2-Linear-prediction.ipynb

    梯度下降法(gradient descent)

    为了确定神经网络模型中参数(权值)的好坏,需要先指定一个衡量标准(训练误差,损失函数,目标函数),例如以均方差(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)。由此可得梯度下降算法如下:

    • GD(training_examples, alpha)
      • training_examples 是训练集合,$\lt \vec{inputs}, output \gt$
      • 初始化每个权值 $\theta_i$ 为随机值
        • 终止条件前,迭代:
          • 初始化权值的变化梯度 $\Delta\theta_i = 0$
          • 对每条训练数据:
            • 根据 $\vec{input}$ 计算模型输出为 o
            • 对每个权值梯度 $\Delta \theta_i$:
              • $\Delta \theta_i = \Delta \theta_i + \alpha (output - o) * x_i$ ==>(A
          • 对每个权值 $\theta_i$:
            • $\theta_i = \theta_i + \Delta \theta_i$ ==>(B

    根据算法描述可以简单实现(完整代码):

    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$ 时汇总了所有训练样本数据的误差,在实践过程中可能出现以下问题:

    1. 收敛过慢
    2. 可能停留在局部最小值

    需要注意的是,学习速率的选择很重要,$\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$ 获得较好的优化结果。

    Torch的应用

    清楚了 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

    关于 Torch 的 Neural Network Package 在 GitHub 上有更详细的文档介绍,这里暂时不作深入学习,根据后续课程进度再做补充。

    相关文章

      网友评论

        本文标题:神经网络模型随机梯度下降法—简单实现与Torch应用

        本文链接:https://www.haomeiwen.com/subject/cdswkttx.html