梯度下降

本文以线性回归为例,讲解了批量梯度下降、随机梯度下降、小批量梯度下降、冲量梯度下降等算法,由浅入深,并结合精心设计的例子,使读者最快掌握这种最常用的优化方法。每一种优化方法,笔者都基于R语言给出了相应的代码,供读者参考,

梯度下降

假如我们有以下身高和体重的数据,我们希望用身高来预测体重。如果你学过统计,那么很自然地就能想到建立一个线性回归模型:

\[y=a+bx \]

其中\(a\)是截距,\(b\)是斜率,\(y\)是体重,\(x\)是身高。

我们将身高与体重的关系在Excel里面用折线图表示,并且添加了线性的趋势线。蓝色的线条是真实数据,红色的实线是模型给出的预测值。蓝色线条与红色线条之间的距离绝对值是预测误差。所以,我们要找到最优的\(a\)\(b\)来拟合这条直线,使得我们模型的总误差最小。

\[Error = \frac{1}{2}(Actual\ weight - Predicted\ weight)^2=\frac{1}{2}(Y-Ypred)^2 \]

我们使用均方误差来表示模型的误差,由于\(Ypred = a + bx\),因此,模型的均方误差可以表示为

\[SSE = \sum \frac{1}{2}(Y-a-bx)^2 \]

也就是说,\(SSE\)是关于\(a\)\(b\)的函数,我们只需要不断调整\(a\)\(b\),使\(SSE\)降到最低就可以了。这个时候,我们就可以利用梯度下降算法,来求解\(a\)\(b\)的值。

梯度下降的计算过程如下:

step 1:随机初始化权重\(a\)\(b\),计算出误差\(SSE\)

step 2:计算梯度。 \(a\)\(b\)的轻微变化都会导致\(SSE\)的变化,因此,我们只需要找到能使\(SSE\)减小的\(a\)\(b\)的变化方向就可以了。这个方向,一般就是由梯度决定的。

step 3:调整权重值,使得\(SSE\)不断接近最小值。

step 4:使用新的权重去做预测,并且计算出新的\(SSE\)

step 5:重复step2-step3,直到权重不再显著变化为止。

我们在Excel中进行上述步骤。为了计算能够快一点,我们首先对数据进行Min-Max标准化。得到如下数据:

step1:随机选取一组权重(此处我们设置a=0,b=1),我们计算出预测值和误差:

step2:计算梯度

\[\frac{\partial SSE}{\partial a} = \sum-(Y-a-bx)=\sum-(Y-Ypred) \]

\[\frac{\partial SSE}{\partial b}=\sum-(Y-a-bx)x=\sum-(Y-Ypred)x \]

\(\frac{\partial SSE}{\partial a}\)\(\frac{\partial SSE}{\partial b}\)就是梯度,他们决定了\(a\)\(b\)的移动方向和距离。

step3: 调整权重值,使得\(SSE\)不断接近最小值。

调整规则为:

\[a_{new} = a_{old} - \eta \nabla a = a_{old} - \eta \cdot \partial SSE/\partial a \]

\[b_{new} = b_{old} - \eta \nabla b = b_{old} - \eta \cdot \partial SSE / \partial b \]

其中,\(\eta\)是一个被我们称之为学习率(learning rate)的东西,一般设置为0.01或者你希望的任何比较小的数值。

本文选择0.01作为学习率。

\[a_{new} = 0 - 0.01 \times 1.925 = -0.01925 \]

\[b_{new} = 1 - 0.01 \times 1.117 = 0.98883 \]

step4:使用新的权重去做预测,并且计算出新的\(SSE\)

可以看出,SSE从0.155降低到0.111,说明系数有改善。

step 5:重复step2-step3,直到权重不再显著变化为止。

我们知道,一元线性回归的系数可以用公式计算,我们用R的lm()函数来计算权重,结果为

lm(y~x,dat)

Call:
lm(formula = y ~ x, data = dat)

Coefficients:
(Intercept)            x  
-0.1167       0.9777 

然后,我在R里面写了一个梯度下降的函数,当精度调到0.0000001的时候,与lm的结果已经很接近了。

gradientDescent <- function(dat,start = c(0,0),learning_rate = 0.01,tol = 0.001)
{
	a = start[1]
	b = start[2]
	x = dat[,1]
	y = dat[,2]
	iters = 0
	while(TRUE)
	{
		Ypred = a + b * x
		old_a = a
		old_b = b
		a = a + learning_rate * sum(y - Ypred)
		b = b + learning_rate * sum((y-Ypred) * x)
		iters = iters + 1
		if(abs(a-old_a) <= tol & abs(b-old_b) <= 0.01)
			break;
	}
	list(weights = c(a,b),iters = iters)
}

gradientDescent(dat,tol=0.0000001)
$weights
[1] -0.1167315  0.9776839

$iters #迭代了975次
[1] 975

我们常说的梯度,其实是指向量,其方向与切线方向相同。

利用梯度下降法进行权重更新的公式为:

\[weight_{new} = weight_{old} - \eta \cdot \nabla \]

其中,那个倒三角形就是梯度的意思。我们在高中数学学过,切线方向是函数变化速度最快的方向,

Stochastic Gradient Descent

梯度下降算法,又可以称为Batch-Gradient-Gescent,即批量梯度下降算法。从上面的例子可以看出,批量梯度下降算法,每次更新系数都需要所有的样本参与计算,当样本规模达到一定数量以后,这个更新速度会非常慢。另外,还有可能导致内存溢出。

为了克服批量梯度下降的这个缺点,有人提出了随机梯度下降(Stochastic Gradient Descent)算法,即每次更新系数只需要一个样本参与计算,因此既可以减少迭代次数,节省计算时间,又可以防止内存溢出。

对于上述问题,随机梯度下降的算法过程如下:

for every \(Y_i\):

\(Ypred = a + bx\)

\(a = a + \eta (Y-Ypred)\)

\(b = b+\eta(Y-Ypred)\cdot x\)

随机梯度下降算法适用于大数量的计算,对于小数据量不一定准确。为了检验随机梯度下降算法,我们构造了一个有10000个样本的数据,同样是计算一元线性回归的系数。

随机梯度下降的函数如下:

stochasticGradientDescent <- function(dat,start = c(0,0),learning_rate = 0.01,tol = 0.000001,iteratons = 100)
{
	#start:初始参数
	#learning_rate:学习率
	#tol:精度
	#iterations:迭代次数
	
	dat = as.matrix(dat)
	a = start[1]
	b = start[2]
	x = dat[,1]
	y = dat[,2]
	iters = 0
	while(iters < iteratons)
	{
		#重排,即将样本的顺序打乱
		index = sample(length(x))
		old_a = a
		old_b = b
		for(i in index)
		{
			
			Ypred = a + b * x[i]
			a = a + learning_rate * (y[i] - Ypred)
			b = b + learning_rate * (y[i]-Ypred) * x[i]
		}
		if(abs(a-old_a) <= tol & abs(b-old_b) <= tol)
		 	break;
		learning_rate = learning_rate / (1 + 0.01 * iters) #自适应学习率
		iters = iters + 1
        if(iters > iterations)
            break;
	}
	list(weights = c(a,b),iters = iters)
}

然后我们构造一个相对大的样本用来检验算法:

set.seed(100)
x <- seq(1,10,length.out = 10000)
y <- 2 * x + rnorm(10000) * 10 + 2
bigdata <- data.frame(x ,y )
plot(x,y)
cor(x,y)

回归结果:

lm(y~x,data = bigdata )
Call:
lm(formula = y ~ x, data = bigdata)

Coefficients:
(Intercept)            x  
      2.004        2.006  

随机梯度下降的结果:

stochasticGradientDescent(bigdata,learning_rate = 0.001,tol=0.000000001)
$weights
2.01138749995603 2.00584502615877
$iters
69

批量梯度下降的结果:

batchGradientDescent(bigdata,learning_rate = 0.000001,tol = 0.000000001)
$weights
2.00385275101478 2.00634924457312
$iters
8345

可以看到,在同样的精度要求下,随机梯度下降进行59次迭代以后即收敛,而批量梯度下降则需要迭代8345次。

但是随机梯度下降也有一个缺点,即参数更新频率太快,有可能出现目标函数值在最优质附近的震荡现象,因为高频率的参数更新导致了高方差。 同时也可以看出,在相同精度要求下,随机梯度下降计算出来的系数与精确值离差较大,而批量随机下降则更接近精确值。

Mini-batch Gradient Descent

小批量梯度下降(Mini-batch Gradient Descent)是介于上述两种方法之间的优化方法,即在更新参数时,只使用一部分样本(一般256以下)来更新参数,这样既可以保证训练过程更稳定,又可以利用批量训练方法中的矩阵计算的优势。

具体更新哪些样本,通常是随机确定的,下面,我们定义一下小批量梯度下降的函数,用来求解上述bigdata的系数。

miniBatchGradientDescent <- function(dat,start = c(0,0),learning_rate = 0.01,tol = 0.001,batchSize = 256,iterations = 10000)
{
    a = start[1]
    b = start[2]
    iters = 0
    len = length(y)
    while(TRUE)
    {
        mini_index = sample(len,batchSize,replace = FALSE)
        x = dat[mini_index,1]
        y = dat[mini_index,2]
        Ypred = a + b * x
        error = y - Ypred
        old_a = a
        old_b = b
        a = a + learning_rate * sum(error)
        b = b + learning_rate * sum((error) * x)
        start = rbind(start,c(a,b))
        iters = iters + 1
        if(abs(a-old_a) <= tol & abs(b-old_b) <= tol)
            break
        if(iters >= iterations)
            break
        
    }
    list(weights = c(a,b),iters = iters,coes = start)
}
miniBatchGradientDescent(bigdata,learning_rate = 0.0001,tol = 0.00001,batchSize = 100)
$weights
2.02646349019186 2.0439693915315
$iters
920064

小梯度批量梯度下降收敛时需要迭代92万次,这显然有点多了。一般来说,当数据量非常大时,小批量梯度下降比较有效,否则计算结果很有可能出现偏移。

先mark,偏移的原因待考究。

Momentum optimization

考虑这样一种情形,小球从山顶往下滚动,一开始很顺利,可是在接近最低点的时候,小球陷入了一段狭长的浅山谷。由于小球一开始并不是直接沿着山谷的方向滚下,因此小球会在这个浅浅的山谷中不断震荡——不断冲上墙壁,接着又从墙壁上滚下来。这种情况并不是我们想看到的,因为这增加了迭代时间。冲量(Momentnum)的引入,使得我们的目标更新的更快了,冲量的更新方式有以下两种,两种方式之间并无太大差异。

第一种:

\(Z^{k+1}=\beta Z_k + \nabla\)

\(w^{k+1} = w_k - \alpha Z^{k+1}\)

其中,\(Z\)是一个与\(w\)方向相同的向量,

第二种:

\(Z^{k+1}=\beta Z^k + \alpha \nabla\)

\(w^{k+1} = w^k - Z^{k+1}\)

两者的差别仅仅在于\(Z^{k+1}\)的系数不同。

通常,这里的学习率要比随机梯度下降小一点,因为随机梯度下降的梯度大一点。\(\beta\)的取值决定了前一次的梯度有多少被纳入了本次的更新。一般来说,稳定前将\(\beta\)设置为0.5,稳定后可以设置为0.9或更高。

#适用于求解一元或多元线性回归的回归系数,返回结果包括截距
momentumGradientDescent <- function(dat,beta = 0.9,z = 0,start = rep(0,dim(dat)[2]),alpha = 0.001,tol=0.0000001,iterations = 100)
{
    dataSet = cbind(1,dat)  #将第一列加上1
    cols = dim(dataSet)[2] #列数
    x = as.matrix(dataSet[,1:(cols - 1)])  #自变量矩阵
    y = as.matrix(dataSet[,cols],ncol = 1)  #因变量,矩阵
    w = as.matrix(start,ncol = 1)  #权重矩阵
    iters = 0
    while(TRUE)
    {
        old_w = w
        old_z = z
        Ypred = x %*% w
        error = y - Ypred
        grad = - t(x) %*% error
        z = beta * old_z + grad
        w = old_w - alpha * z
        if(sum(abs(w-old_w)) < tol)
            break;
        iters = iters + 1
        if(is.integer(iterations))
            if(iters >= iterations)
                break;
    }
    list(weights = as.vector(w),iters = iters)
}

利用冲量梯度下降求bigdata的系数:

momentumGradientDescent(bigdata,alpha=0.00001)
$weights
[1] 2.003851 2.006354

$iters
[1] 248

可以看到,迭代次数明显减少,并且系数与精确值更加接近了。

Nesterov Accelerated Gradient

然而,让一个小球盲目地沿着斜坡滚下山是不理想的。我们需要一个更聪明的球,它知道下一步要往哪里去,因此在斜坡有上升的时候,它能够自主调整方向。

Nesterov Accelerated Gradient 是基于冲量梯度下降算法进行改进的一种算法,也是梯度下降算法的变种。

在上一种算法中,我们使用了冲量\(\beta Z^k\)来调整我们的参数改变量,将上述第二种更新方法改写一下,得到如下式子:

\(Z^{k+1} = \beta Z^k + \alpha \nabla\)

$w^{k+1} = w^k - Z^{k+1} = w^k - \beta Z_k - \alpha \nabla $

$w^k - \beta Z_k $给出了下一个参数位置的近似,指明了参数该如何变化。现在,我们可以不用当前的参数,而是用未来的参数的近似位置来更新我们的参数,即

\(Z^{k+1} = \beta Z^k + \alpha \nabla _wJ(w - \beta Z^k)\)

\(w^{k+1} = w^k - Z^{k+1}\)

这里,我们仍然设置\(\beta\)的值在0.9附近。如下图所示,冲量梯度下降先计算当前的梯度(短的蓝色向量),然后根据累积的冲量向前跨越一大步(长的蓝色向量)。NAG首先根据之前的累积梯度向前迈一大步(棕色向量),然后对梯度进行修正(红色向量)。这种利用近似未来参数来更新参数的方法,可以防止梯度更新太快,并且增加了响应能力。

假设我们的权重矩阵(系数矩阵)为\(w\),自变量\(x\),因变量\(Y\),则损失函数为:

\[J(w) = 1/2 \sum _{i=1} ^n (Y_i - w'x_i)^2 \]

\[\nabla J(w) = \frac{\partial J(w)}{\partial w} = - \sum_{i=1}^n(Y_i - w'x_i)x_i' \]

那么

\[\nabla J(w - \beta Z^{k}) = - \sum _{i=1}^n(Y_i - (w'-\beta Z^k)'x_i)x_i' \]

根据上述思想,编写的NAG代码如下 :

#适用于求解一元或多元线性回归的回归系数,返回结果包括截距
NAG <- function(dat,beta = 0.9,z = 0,start = rep(0,dim(dat)[2]),alpha = 0.001,tol=0.0000001,iterations = 100)
{
    dataSet = cbind(1,dat)  #将第一列加上1
    cols = dim(dataSet)[2] #列数
    x = as.matrix(dataSet[,1:(cols - 1)])  #自变量矩阵
    y = dataSet[,cols]  #因变量,矩阵
    w = start  #权重矩阵
    iters = 0
    while(TRUE)
    {
        old_w = w
        old_z = z
        #中间过程用mid1 mid2代替
        mid1 = apply((old_w - beta * old_z) * t(x),2,sum)
        mid2 = y - mid1
        grad = - apply(mid2 * x,2,sum)
        z = beta * old_z + alpha * grad
        w = old_w - z
        if(sum(abs(w-old_w)) < tol)
            break;
        iters = iters + 1
        if(is.integer(iterations))
            if(iters >= iterations)
                break;
    }
    list(weights = as.vector(w),iters = iters)
}

用NAG来求bigdata的系数:

NAG(bigdata,alpha=0.000001)
$weights
[1] 2.003849 2.006350

$iters
[1] 598

AdaGrad

尽管我们可以根据损失函数的梯度来加快更新参数,我们也希望能够根据参数的重要性来决定其更新的幅度。

AdaGrad是一种基于梯度算法的优化算法,它只做了一件事:根据参数来自适应调整学习率。对于不常出现的参数进行较大的更新,对于经常出现的参数进行较少的更新,因此,这种方法非常适合处理稀疏数据。

之前,我们对每一个参数更新所使用的学习率都是一样的,而AdaGrad在每一步都使用不同的学习率对不同的参数进行更新。我们先写出AdaGrad的单个参数的更新方法,然后将其向量化。长话短说,假设\(g_{t,i}\)表示损失函数对于参数\(\theta_i\)的梯度:

在步骤\(t\):

\[g_{t,i} = \nabla _\theta J(\theta_t,i) \]

那么,对于步骤\(t\),使用随机梯度下将对\(\theta_i\)进行更新的公式为:

\[\theta_{t+1,i} = \theta_{t,i} - \eta_i \cdot g_{t,i} \]

在上述更新过程中,AdaGrad在每一步都对参数\(\theta_i\)对应的学习率\(\eta_i\)进行调整,调整的方法基于过去的所有梯度:

\[\theta_{t+1,i} = \theta_{t,i} - \frac{\eta_i}{\sqrt{G_{t,ii} + \epsilon}} \cdot g_{t,i} \]

\(G_{t}\in R^{d\times d}\)是一个对角矩阵,第\(i\)个对角元素是历史上损失函数对\(\theta_i\)的所有梯度的平方和,\(\epsilon\)是一个平滑参数,防止分母为0,通常取\(10^{-8}\)。有趣的是,如果不加开方,算法表现极差。

因为\(G_t\)包含了过去所有参数梯度的平方和,因此我们可以将其向量化:

\[\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{G_t + \epsilon}}\odot g_t \]

AdaGrad的一个最大的好处是不用手动调整学习率的大小,通常设置默认值为0.01,然后顺其自然就好了。

AdaGrad的一个较大的缺点是,分母是不断增大的,当迭代次数不断增加时,分母会逐渐趋于无穷大,学习率进而趋于无穷小,此时,算法将变得不再有效。

我们延用上述符号来写出AdaGrad的函数。

#SGD为基础
AdaGrad <- function(dat,theta,learning_rate = 0.01,start = rep(0,dim(dat)[2]),tol = 0.000001,iterations = 1000)
{
    dataSet = cbind(1,dat) #将自变量进行增广,第一列全为1
    cols = dim(dataSet)[2] #列数
    rows = dim(dataSet)[1] #行数
    x = as.matrix(dataSet[,1:(cols - 1)])  #自变量矩阵
    y = dataSet[,cols]  #因变量,矩阵
    g = 0
    theta = start  #权重矩阵
    iters = 0
    while(TRUE)
    {
        old_theta = theta
        index = sample(rows)
        for(i in index)
        {
            grad = - (y[i] - sum(theta * x[i,])) * x[i,]
            g = g + grad * grad
            theta = theta - learning_rate / sqrt(g + 1e-8) * grad
        }
        iters = iters + 1
        if(is.integer(iterations))
            if(iters > iterations)
                break;
        if(sum(abs(old_theta - theta)) < tol)
            break;
    }
    list(weights = as.vector(theta),iters = iters)
}

AdaGrad的计算结果如下:

AdaGrad(bigdata)
$weights
[1] 2.008219 2.005682

$iters
[1] 6579

Adadelta

AdaDelta是在AdaGrad的基础上发展而来的,目的是解决AdaGrad算法中学习率的单调减少问题。AdaDelta不再采用累积梯度平方和的方法来调整学习率,而是根据一些固定的\(w\)的大小来限制过去累积梯度的窗口。

AdaDelta不再无效率地存储历史梯度的平方和,而将历史梯度平方和定义为衰减均值(decaying average)。第\(t\)步的移动平均值\(E[g^2]_t\)仅仅取决于过去的平均值和当前梯度(有点类似于momentum):

\[E[g^2]_t = \gamma E[g^2]_{t-1} + (1-\gamma)g_t^2 \]

同样的,我们把\(\gamma\)设置在0.9附近。为清楚起见,我们重新写下更新规则:

\[\Delta \theta _t = - \eta \cdot g_{t,i} \]

\[\theta _{t+1} = \theta_t + \Delta \theta _t \]

根据AdaGrad我们推出AdaDelta的参数更新公式:

\[\Delta \theta _t = - \frac{\eta}{\sqrt{G_t + \epsilon}} \odot g_t \]

我们只需把\(G_t\)替换\(E[g^2]_t\)就行了:

\[\theta_t = - \frac{\eta}{\sqrt{E[g^2]_t + \epsilon}} \]

因为分母刚好是梯度的均方根,我们将其简写为:

\[\Delta \theta_t = -\frac{\eta}{RMS[g]_t}g_t \]

作者们发现上述更新中的单位不一致(可以理解为量纲不一致),因此,他们定义了另外一个指数衰减均值,这次不用梯度的平方了,而是用参数的平方来进行更新:

\[E[\Delta \theta^2]_t = \gamma E[\Delta \theta^2]_{t-1} + (1-\gamma)\Delta\theta_t^2 \]

参数更新的均方误差即:

\[RMS[\Delta\theta]_t = \sqrt{E[\Delta\theta^2]_t + \epsilon} \]

因为\(RMS[\theta]_t\)是未知的,我们可以用之前所有的更新过的参数的RMS来代替。将之前的学习率\(\eta\)换成\(RMS[\Delta\theta]_{t-1}\),那么,我们得出AdaDelta的更新规则:

\[\Delta\theta_t = -\frac{RMS[\Delta\theta]_{t-1}}{RMS[g]_t}g_t \]

\[\theta_{t+1} = \theta_t + \Delta\theta_t \]

AdaDelta甚至不需要初始化学习率,因为在更新规则中已经不见它的身影了。

根据上述思路,我们写出AdaDelta的函数:

RMS <- function(x)
{
    sqrt(mean(x^2) - mean(x)^2)
}

AdaDelta <- function()

RMSprop

Adam

上述所有改进方法均可以运用于批量梯度下降、小批量梯度下降和随机梯度下降!

【参考文献】

[1]http://netedu.xauat.edu.cn/jpkc/netedu/jpkc/gdsx/homepage/5jxsd/51/513/5308/530807.htm

[2]https://www.kdnuggets.com/2017/04/simple-understand-gradient-descent-algorithm.html

[3]https://distill.pub/2017/momentum/

[4]http://ruder.io/optimizing-gradient-descent/index.html

posted @ 2018-07-01 19:21  小肥羊的博客  阅读(732)  评论(0编辑  收藏  举报