首页 > 技术文章 > 梯度下降

always-fight 2018-04-25 13:13 原文

梯度~反向传播~实例~局部~凸函数

一、梯度下降(Gradient Descent

 1. 梯度

  在微积分里面,对多元函数的参数求∂偏导数,把求得的各个参数的偏导数以向量的形式写出来,就是梯度。

  比如函数\(f(x,y)\),分别对\(x,y\)求偏导数,求得的梯度向量就是(∂f/∂x, ∂f/∂y)T,简称grad f(x,y)或者▽f(x,y)。

  对于在点(x0,y0)的具体梯度向量就是(∂f/∂x0, ∂f/∂y0)T.或者▽f(x0,y0),如果是3个参数的向量梯度,就是(∂f/∂x, ∂f/∂y,∂f/∂z)T,以此类推。

  那么这个梯度向量求出来有什么意义呢?他的意义从几何意义上讲,就是函数变化增加最快的地方。具体来说,对于函数\(f(x,y)\)在点\((x_0,y_0)\),沿着梯度向量的方向就是(∂f/∂x0, ∂f/∂y0)T的方向是\(f(x,y)\)增加最快的地方。或者说,沿着梯度向量的方向,更加容易找到函数的最大值。反过来说,沿着梯度向量相反的方向,也就是 -(∂f/∂x0, ∂f/∂y0)T的方向,梯度减少最快,也就是更加容易找到函数的最小值。

 

 2.梯度下降和上升

  在机器学习算法中,在最小化损失函数时,可以通过梯度下降法来一步步的迭代求解,得到最小化的损失函数,和模型参数值。反过来,如果我们需要求解损失函数的最大值,这时就需要用梯度上升法来迭代了。

 

 3.梯度下降算法详解

  a)梯度下降的一个直观的解释

   比如我们在一座大山上的某处位置,由于我们不知道怎么下山,于是决定走一步算一步,也就是在每走到一个位置的时候,求解当前位置的梯度,沿着梯度的负方向,也就是当前最陡峭的位置向下走一步,然后继续求解当前位置梯度,向这一步所在位置沿着最陡峭最易下山的位置走一步。这样一步步的走下去,一直走到觉得我们已经到了山脚。当然这样走下去,有可能我们不能走到山脚,而是到了某一个局部的山峰低处。从上面的解释可以看出,梯度下降不一定能够找到全局的最优解,有可能是一个局部最优解。当然,如果损失函数是凸函数,梯度下降法得到的解就一定是全局最优解

 

 

  b)梯度下降相关概念

   1.步长(Learning rate):步长决定了在梯度下降迭代的过程中,每一步沿梯度负方向前进的长度。用上面下山的例子,步长就是在当前这一步所在位置沿着最陡峭最易下山的位置走的那一步的长度。

   2.特征(feature):指的是样本中输入部分,比如2个单特征的样本x(0) ,y(0) ,x(1) ,y(1) ,则第一个样本特征为x(0),第一个样本输出为y(0)

   3.假设函数(hypothesis function):在监督学习中,为了拟合输入样本,而使用的假设函数,记为hθ(x)。比如对于单个特征的m个样本x(i) ,y(i) (i=1,2,...m),可以采用拟合函数如下: hθ(x)=θ0 +θ1x

   4.损失函数(loss function):为了评估模型拟合的好坏,通常用损失函数来度量拟合的程度。损失函数极小化,意味着拟合程度最好,对应的模型参数即为最优参数。在线性回归中,损失函数通常为样本输出和假设函数的差取平方。比如对于m个样本xi ,yi(i=1,2,...m) ,采用线性回归,损失函数为:

 

 

   其中xi 表示第i个样本特征,yi表示第i个样本对应的输出,hθ(xi) 为假设函数。

   c)详细算法

 

  从这个例子可以看出当前点的梯度方向是由所有的样本决定的,加1/m是为了好理解。由于步长也为常数,他们的乘机也为常数,所以这里α*1/m α*1/m 可以用一个常数表示

  4.梯度下降的算法调优

   a) 算法的步长选择。在前面的算法描述中,我提到取步长为1,但是实际上取值取决于数据样本,可以多取一些值,从大到小,分别运行算法,看看迭代效果,如果损失函数在变小,说明取值有效,否则要增大步长。前面说了。步长太大,会导致迭代过快,甚至有可能错过最优解。步长太小,迭代速度太慢,很长时间算法都不能结束。所以算法的步长需要多次运行后才能得到一个较为优的值。

   b) 算法参数的初始值选择。初始值不同,获得的最小值也有可能不同,因此梯度下降求得的只是局部最小值;当然如果损失函数是凸函数则一定是最优解。由于有局部最优解的风险,需要多次用不同初始值运行算法,关键损失函数的最小值,选择损失函数最小化的初值。

   c) 归一化。由于样本不同特征的取值范围不一样,可能导致迭代很慢,为了减少特征取值的影响,可以对特征数据归一化,也就是对于每个特征x,求出它的期望和标准差std(x),然后转化为:(x - 期望)/std(x),这样特征的新期望为0,新方差为1,迭代次数可以大大加快。

 

二、代码实例

 1)梯度下降:参数的更新是使用了所有样本点

#!/usr/bin/env python


# http://www.cnblogs.com/louyihang-loves-baiyan/p/5136447.html
import random
import logging
import os
import sys


program = os.path.basename(sys.argv[0])
logger = logging.getLogger(program)
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(message)s')
logging.root.setLevel(level=logging.INFO)
logger.info("running %s\n" % ' '.join(sys.argv))


# This is a sample to simulate a function y = theta1*x1 + theta2*x2
input_x = [[1,4], [2,5], [5,1], [4,2]]
y = [19, 26, 19, 20]
theta = [1, 1]
loss = 10
step_size = 0.001
eps = 0.0001
max_iters = 10000
error = 0
iter_count = 0

# 梯度下降
while (loss > eps and iter_count < max_iters):
    loss = 0
    # 这里更新权重的时候所有的样本点都用上了,一轮所有样本循环,求出参数
    # 梯度下降:参数的更新是使用了所有样本点
    for i in range(4):
        pred_y = theta[0]*input_x[i][0] + theta[1]*input_x[i][1]
        theta[0] = theta[0] - step_size * (pred_y - y[i]) * input_x[i][0]
        theta[1] = theta[1] - step_size * (pred_y - y[i]) * input_x[i][1]
        logger.info('pred_y={}, theta[0]={}, theta[1]={}'.format(pred_y, theta[0], theta[1]))

    # 所有样本更新的权重,求出一次error,同时迭代次数加1
    for i in range(4):
        pred_y = theta[0] * input_x[i][0] + theta[1] * input_x[i][1]
        error = 0.5 * (pred_y - y[i]) ** 2
        loss += error

    iter_count += 1
    logger.info('iters_count={}, loss={}\n'.format(iter_count, loss))


logger.info('theta={}'.format(theta))
logger.info('final loss={}'.format(loss))
logger.info('gd: iters={}'.format(iter_count))

 

 

 2)随机梯度下降:参数的更新是使用了随机的一个样本点

#!/usr/bin/env python


# http://www.cnblogs.com/louyihang-loves-baiyan/p/5136447.html
import random
import logging
import os
import sys


program = os.path.basename(sys.argv[0])
logger = logging.getLogger(program)
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(message)s')
logging.root.setLevel(level=logging.INFO)
logger.info("running %s\n" % ' '.join(sys.argv))


# This is a sample to simulate a function y = theta1*x1 + theta2*x2
input_x = [[1,4], [2,5], [5,1], [4,2]]
y = [19, 26, 19, 20]
theta = [1, 1]
loss = 10
step_size = 0.001
eps = 0.0001
max_iters = 10000
error = 0
iter_count = 0

# 随机梯度下降
# 每次选取一个随机值,随机一个点更新参数
while (loss > eps and iter_count < max_iters):
    loss = 0

    # 每一次选取随机的一个点进行权重的更新
    i = random.randint(0,3)  # >= 0 and i <= 3
    pred_y = theta[0] * input_x[i][0] + theta[1]*input_x[i][1]
    theta[0] = theta[0] - step_size * (pred_y - y[i]) * input_x[i][0]
    theta[1] = theta[1] - step_size * (pred_y - y[i]) * input_x[i][1]
    logger.info('pred_y={}, theta[0]={}, theta[1]={}'.format(pred_y, theta[0], theta[1]))

    for i in range(4):
        pred_y = theta[0] * input_x[i][0] + theta[1]*input_x[i][1]
        error = 0.5 * (pred_y - y[i]) ** 2
        loss += error
    iter_count += 1

    logger.info('iters_count={}, loss={}\n'.format(iter_count, loss))


logger.info('theta={}'.format(theta))
logger.info('final loss={}'.format(loss))
logger.info('sgd: iters={}'.format(iter_count))

 

  

 

 

 3)批量梯度下降:从所有样本点中选择n个,这里从4个样本点中选择2个进行参数更新

#!/usr/bin/env python


# http://www.cnblogs.com/louyihang-loves-baiyan/p/5136447.html
import random
import logging
import os
import sys


program = os.path.basename(sys.argv[0])
logger = logging.getLogger(program)
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(message)s')
logging.root.setLevel(level=logging.INFO)
logger.info("running %s\n" % ' '.join(sys.argv))


# This is a sample to simulate a function y = theta1*x1 + theta2*x2
input_x = [[1,4], [2,5], [5,1], [4,2]]
y = [19, 26, 19, 20]
theta = [1, 1]
loss = 10
step_size = 0.001
eps = 0.0001
max_iters = 10000
error = 0
iter_count = 0


# 批量随机梯度下降,这里选用2个点来更新权重
while(loss > eps and iter_count < max_iters):
    loss = 0
    i = random.randint(0, 3)
    # 注意这里,我这里批量每次选取的是2个样本点做更新,另一个点是随机点+1的相邻点
    j = (i + 1) % 4
    pred_y = theta[0] * input_x[i][0] + theta[1] * input_x[i][1]
    theta[0] = theta[0] - step_size * (pred_y - y[i]) * input_x[i][0]
    theta[1] = theta[1] - step_size * (pred_y - y[i]) * input_x[i][1]

    # 随机点i相邻位置
    pred_y = theta[0] * input_x[j][0] + theta[1] * input_x[j][1]
    theta[0] = theta[0] - step_size * (pred_y - y[j]) * input_x[j][0]
    theta[1] = theta[1] - step_size * (pred_y - y[j]) * input_x[j][1]

    for i in range(4):
        pred_y = theta[0] * input_x[i][0] + theta[1]*input_x[i][1]
        error = 0.5*(pred_y - y[i]) ** 2
        loss += error

    iter_count += 1
    logger.info('iters_count={}, loss={}\n'.format(iter_count, loss))


logger.info('theta={}'.format(theta))
logger.info('final loss={}'.format(loss))
logger.info('bgd: iters={}'.format(iter_count))

 

 

 总结:

  对比一下结果,每个例子我都跑了几次,基本上都维持在哪个迭代次数

  可以看到梯度下降迭代的次数最少,因为我这里样本点少,所以这样快。数据多了的话,你想动则几万的样本计算一次的时间就够呛

  实际运用中一般使用批梯度下降,所谓的batch就是这么一回事

  你设一个比较恰当的batch值是可以帮助网络加速收敛的

 

三、实例

 神经网络的最终的目的就是要找到一个模型的一组最佳参数,使得模型在训练集上的损失最小,其中梯度下降算法主要用于优化单个参数的取值,而反向传播给出了一个高效的方式在所有参数上使用梯度下降

 假设我们的损失函数与参数的关系如下图:

 则梯度下降公式是:

 假设我们想通过梯度下降来优化参数x,来使得损失函数\(J(x^2)\)最小,我们需要一个x参数的初始值,假设为5,还要一个学习率,如0.3,我们的经验告诉自己损失函数最小时,x = 0,那么我们如何通过迭代求得x = 0

 从上面的结果看,经过5轮迭代,x已经为0.0512,接近0,梯度下降的优化过程就是类似这样一个过程,神经网络优化过程就是包含前向传播得到预测值,再通过反向传播更新每个参数

 如果我们的学习率设置过大,可能导致我们的参数在最优值两侧来回移动,如我们将学习率设置为1:

 

 

 需要注意:梯度下降并不一定保证被优化的函数达到全局最优,可能得到的是局部最优

 

 

 在上图小黑点处,函数偏导为0,参数不再更新,但如果损失函数初始值落在左侧区间,梯度下降就能得到全局最优

 可见训练神经网络时,参数的初始值会很大程度影响最后得到的结果,只有损失函数是凸函数时,梯度下降才能保证达到全局最优

 

 

推荐阅读