NN中的学习技巧之(一)参数的最优化之 Momentum

前面的博文里说了SGD,最基础的一个梯度下降优化算法,在SGD之后还有很多改进版本的算法,比如动量法,下面我降动量法扥别作用于两个函数,第一个是完美凸函数,第二个则是非凸的香蕉函数

动量法的参数更新公式:

NN中的学习技巧之(一)参数的最优化之 Momentum_第1张图片
v就是动量,实际上是速度,可以认为是单位质量下的动量

第一个式子是说v是上一个v和刚计算出来的梯度的指数加权平均(但是这里我们不要求 α + η = 1 \alpha+\eta=1 α+η=1),上一个v的权重是 α \alpha α,即程序代码中的momentum参数, 而梯度的权重是 η \eta η,也就是学习率

通过v就引入了之前的梯度,包括大小和方向,所以不再像SGD那样,每次只考虑当前位置算出来的刚出炉的新鲜梯度,每次都独立的更新参数,而是把之前的更新记忆起来,指导现在的更新,新算出来的梯度的值反而占权重很小,对参数更新影响程度也小的多。

如果 α = 0.9 \alpha=0.9 α=0.9,则收敛比SGD近似快10倍
如果 α = 0.99 \alpha=0.99 α=0.99,则收敛比SGD近似快100倍

这个观点我在哪看到过,记不清了,以后弄清楚了过来记录


函数1:(Rosenbrock函数)

f ( x , y ) = ( a − x ) 2 + b ( y − x 2 ) 2 f(x,y)=(a-x)^2+b(y-x^2)^2 f(x,y)=(ax)2+b(yx2)2
取a=1,b=100
NN中的学习技巧之(一)参数的最优化之 Momentum_第2张图片
从下图可以看出,动量法确实比SGD快的多,我的另一篇讲SGD的博文有实验图和代码,学习率0.0035走10000步也还是离最小值点很远,而且后面由于梯度太小走的跟没走一样······而动量梯度下降(这里学习率0.001,动量0.9(即比普通梯度下降快10倍))只用不到1000步就到达最小值点了,但是动量法对于之字形下降缓解不大,只是加快收敛。下面代码中的参数可以改一改看看实验效果,学习率稍微再大一点比如0.0015,动量法都会学的太快导致跑出画布,毕竟跑得太远也不利于最终收敛,我们可以改小学习率,或者改大momentum参数(减小增速倍数)来控制学习速率。终于明白为啥别人都说深度学习就是调参了······如果使用完善的框架的话,不用自己搭网络,确实可以这么说。
NN中的学习技巧之(一)参数的最优化之 Momentum_第3张图片

# Momentum.py
# 动量梯度下降法应用于Rosenbrock函数
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D


class Momentum:
    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None

    def update(self, params, grads):
        if self.v is None:
            # 第一次调用
            self.v = {}
            for key, val in params.items():
                self.v[key] = np.zeros_like(val)

        for key in params.keys():
            self.v[key] = self.momentum * self.v[key] - self.lr * grads[int(key)]
            params[key] += self.v[key]

        return params


# Rosenbrock函数
def func(x):
    return (1 - x[0]) ** 2 + 100 * (x[1] - x[0]**2) ** 2


def gradient_descent(f, init_x, lr=0.1, step_num=100):
    x = init_x
    x_history = []

    for i in range(step_num):
        x_history.append(x.copy())
        # 这里必须用x.copy()
        # 否则最终x_history里所有数值都和最后一个数值相同

        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x, np.array(x_history)
    # 把x_history转换为numpy数组


def momentum_update(init_x, stepnum):
    x = init_x
    x_history = []

    for i in range(stepnum):
        x_history.append(np.array(list(x.copy().values())))
        grad = numerical_gradient(func, x)
        x = m.update(x, grad)

    return x, np.array(x_history)


def numerical_gradient(f, x):
    h = 1e-4
    x = np.array(list(init_x.values()))  # 转换为ndarray
    grad = np.zeros_like(x)

    for idx in range(x.size):
        temp = x[idx]
        x[idx] = temp + h
        fxh1 = f(x)

        x[idx] = temp - h
        fxh2 = f(x)

        grad[idx] = (fxh1 - fxh2) / (2 * h)
        x[idx] = temp

    return grad


init_x = {}  # 起始点
init_x['0'] = 0.7
init_x['1'] = -1.1
learning_rate = 0.001  # 学习率,再大就会导致一次参数更新跳到非常远的地方
m = Momentum(lr=learning_rate)
stepnum = 1000  # 沿着梯度走1000步,经过观察,rosenbrock函数走10000步也到不了最小点
# 前面梯度大,一次更新走很远,后面梯度小,几乎没怎么动
x, x_history = momentum_update(init_x=init_x, stepnum=stepnum)


x = np.arange(-5, 5, 0.05)
y = np.arange(-5, 10, 0.05)
X, Y = np.meshgrid(x, y)
z = np.array([X, Y])

# 画等高线
plt.figure()
plt.contour(x, y, func(z),np.arange(0,2500,100), zdir='z', cmap='binary')
# 画所有由梯度下降找到的点
plt.plot(x_history[:, 0], x_history[:, 1], '+', color='blue')

# 翻转坐标轴方向
ax = plt.gca()
ax.xaxis.set_ticks_position('top')
ax.invert_xaxis()
ax.yaxis.set_ticks_position('right')
ax.invert_yaxis()
# 画点间连线
for i in range(x_history.shape[0]-2):
    tmp = x_history[i:i+2]
    tmp = tmp.T
    plt.plot(tmp[0], tmp[1], color='blue')
# 标注最小值位置
plt.plot(1, 1, '+', color='r')
plt.xlim(-5, 5)
plt.ylim(-5, 10)
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
plt.title('Rosenbrock Zigzag ')
plt.show()

如果把学习率改为0.0005,就会是这样逼近最小值点,之字形的横向范围大大减小!!

NN中的学习技巧之(一)参数的最优化之 Momentum_第4张图片

函数2:
f ( x , y ) = x 2 + y 2 f(x,y)=x^2+y^2 f(x,y)=x2+y2
NN中的学习技巧之(一)参数的最优化之 Momentum_第5张图片
这种完美的凸函数,动量法则收敛超快,学习率0.004,30步就到了。
NN中的学习技巧之(一)参数的最优化之 Momentum_第6张图片
学习率再大容易错过全局最小,跑过了再反复震荡(下图是学习率0.005的情况),经过全局最小时却跑过了,回来又跑多了,来回震荡,所以随着训练时间逐渐调整学习率,快到最小值的时候减小学习率是有意义的
NN中的学习技巧之(一)参数的最优化之 Momentum_第7张图片

代码只是稍微改动了一点,参数和函数定义调用啥的:

# Momentum.py
# 动量梯度下降法
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D


class Momentum:
    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None

    def update(self, params, grads):
        if self.v is None:
            # 第一次调用
            self.v = {}
            for key, val in params.items():
                self.v[key] = np.zeros_like(val)

        for key in params.keys():
            self.v[key] = self.momentum * self.v[key] - self.lr * grads[int(key)]
            params[key] += self.v[key]

        return params


# Rosenbrock函数
def func(x):
    return (1 - x[0]) ** 2 + 100 * (x[1] - x[0]**2) ** 2


def func2(x):
    return x[0] ** 2 + x[1] ** 2

def gradient_descent(f, init_x, lr=0.1, step_num=100):
    x = init_x
    x_history = []

    for i in range(step_num):
        x_history.append(x.copy())
        # 这里必须用x.copy()
        # 否则最终x_history里所有数值都和最后一个数值相同

        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x, np.array(x_history)
    # 把x_history转换为numpy数组


def momentum_update(init_x, stepnum):
    x = init_x
    x_history = []

    for i in range(stepnum):
        x_history.append(np.array(list(x.copy().values())))
        grad = numerical_gradient(func2, x)
        x = m.update(x, grad)

    return x, np.array(x_history)


def numerical_gradient(f, x):
    h = 1e-4
    x = np.array(list(init_x.values()))  # 转换为ndarray
    grad = np.zeros_like(x)

    for idx in range(x.size):
        temp = x[idx]
        x[idx] = temp + h
        fxh1 = f(x)

        x[idx] = temp - h
        fxh2 = f(x)

        grad[idx] = (fxh1 - fxh2) / (2 * h)
        x[idx] = temp

    return grad


init_x = {}  # 起始点
init_x['0'] = -4.5
init_x['1'] = -4.5
learning_rate = 0.004  # 学习率,再大就会导致一次参数更新跳到非常远的地方
m = Momentum(lr=learning_rate)
stepnum = 30  # 沿着梯度走1000步,经过观察,rosenbrock函数走10000步也到不了最小点
# 前面梯度大,一次更新走很远,后面梯度小,几乎没怎么动
x, x_history = momentum_update(init_x=init_x, stepnum=stepnum)


x = np.arange(-5, 5, 0.05)
y = np.arange(-5, 5, 0.05)
X, Y = np.meshgrid(x, y)
z = np.array([X, Y])

# 画等高线
plt.figure()
plt.contour(x, y, func2(z),np.arange(0,50,5), zdir='z', cmap='binary')
# 画所有由梯度下降找到的点
plt.plot(x_history[:, 0], x_history[:, 1], '+', color='blue')

# 翻转坐标轴方向
ax = plt.gca()
ax.xaxis.set_ticks_position('top')
ax.invert_xaxis()
ax.yaxis.set_ticks_position('right')
ax.invert_yaxis()
# 画点间连线
for i in range(x_history.shape[0]-2):
    tmp = x_history[i:i+2]
    tmp = tmp.T
    plt.plot(tmp[0], tmp[1], color='blue')
# 标注最小值位置
plt.plot(0, 0, 'o', color='r')
plt.xlim(-5, 5)
plt.ylim(-5, 5)
plt.xlabel('x')
plt.ylabel('y')
plt.grid()
plt.title('x^2 + y^2 ')
plt.show()

函数3:
NN中的学习技巧之(一)参数的最优化之 Momentum_第8张图片
NN中的学习技巧之(一)参数的最优化之 Momentum_第9张图片
NN中的学习技巧之(一)参数的最优化之 Momentum_第10张图片

对比我另一个讲SGD的博客里的同一个函数使用SGD的收敛过程:(SGD有很多之字形,动量SGD有所缓解并且加快收敛)
NN中的学习技巧之(一)参数的最优化之 Momentum_第11张图片
调出bug的我眼泪掉下来·····

这次又把宝贵的时间浪费在小数点上了

init_x['0'] = -7.0
init_x['1'] = 2.0

我写成

init_x['0'] = -7
init_x['1'] = 2

于是梯度算出来都是好几万好几万的,一开始我没单步调,在那边疯狂改学习率和momentum参数,发现怎么调结果都不按照我想要的样子显示···最后pycharm崩了······重新打开,单步一看,mmp,第三次跳进同一个坑里

代码:

# Momentum.py
# 动量梯度下降法应用于Rosenbrock函数
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D


class Momentum:
    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None

    def update(self, params, grads):
        if self.v is None:
            # 第一次调用
            self.v = {}
            for key, val in params.items():
                self.v[key] = np.zeros_like(val)

        for key in params.keys():
            self.v[key] = self.momentum * self.v[key] - self.lr * grads[int(key)]
            params[key] += self.v[key]

        return params



def func2(x):
    return (x[0]**2) / 20 + x[1] ** 2

def gradient_descent(f, init_x, lr=0.1, step_num=100):
    x = init_x
    x_history = []

    for i in range(step_num):
        x_history.append(x.copy())
        # 这里必须用x.copy()
        # 否则最终x_history里所有数值都和最后一个数值相同

        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x, np.array(x_history)
    # 把x_history转换为numpy数组


def momentum_update(init_x, stepnum):
    x = init_x
    x_history = []

    for i in range(stepnum):
        x_history.append(np.array(list(x.copy().values())))
        grad = numerical_gradient(func2, x)
        x = m.update(x, grad)

    return x, np.array(x_history)


def numerical_gradient(f, x):
    h = 1e-4
    x = np.array(list(init_x.values()))  # 转换为ndarray
    grad = np.zeros_like(x)

    for idx in range(x.size):
        temp = x[idx]
        x[idx] = temp + h
        fxh1 = f(x)

        x[idx] = temp - h
        fxh2 = f(x)

        grad[idx] = (fxh1 - fxh2) / (2 * h)
        x[idx] = temp

    return grad


init_x = {}  # 起始点
init_x['0'] = -7.0
init_x['1'] = 2.0
learning_rate = 0.1  # 学习率,再大就会导致一次参数更新跳到非常远的地方
m = Momentum(lr=learning_rate)
stepnum = 25  # 沿着梯度走1000步,经过观察,rosenbrock函数走10000步也到不了最小点
# 前面梯度大,一次更新走很远,后面梯度小,几乎没怎么动
x, x_history = momentum_update(init_x=init_x, stepnum=stepnum)

axis_range = 10
x = np.arange(-axis_range, axis_range, 0.05)
y = np.arange(-axis_range, axis_range, 0.05)
X, Y = np.meshgrid(x, y)
z = np.array([X, Y])

# 画等高线
plt.figure()
plt.contour(x, y, func2(z),np.arange(0,10,2), zdir='z', cmap='binary')
# 画所有由梯度下降找到的点
plt.plot(x_history[:, 0], x_history[:, 1], '+', color='blue')

# 画点间连线
for i in range(x_history.shape[0]-2):
    tmp = x_history[i:i+2]
    tmp = tmp.T
    plt.plot(tmp[0], tmp[1], color='blue')
# 标注最小值位置
plt.plot(0, 0, 'o', color='r')
#plt.xlim(-axis_range, axis_range)
#plt.ylim(-axis_range, axis_range)
plt.xlabel('x')
plt.ylabel('y')
plt.title('0.05x^2 + y^2 ')
plt.show()

实验说明动量法还是比SGD好很多的,SGD连最小值的身都近不了···当然这么说很笼统,没有具体分析,我对动量法的理论部分也还没完全搞懂,以后再补充。

你可能感兴趣的:(机器学习,Python)