神经网络 优化器

我们知道,神经网络的学习的目的就是寻找合适的参数,使得损失函数的值尽可能小。解决这个问题的过程为称为最优化。解决这个问题使用的算法叫做优化器

1. SGD

在前面我们实现的神经网络中所使用的优化方法是随机梯度下降法(Stachastic gradient desent 简称 SGD)。SGD 的想法就是沿着梯度的方向前进一定距离。用数学的语言来描述的话可以写成下式:

W \leftarrow W - \eta \frac{\partial L}{\partial W}

这里面,W 表示需要更新的权重,\frac{\partial L}{\partial W} 表示损失函数关于 W 的梯度(准确点来说这是一个 Jacobian 矩阵),\eta 表示学习率,\leftarrow 表示使用右边的值更新左边的值。下面我们先给出一个 python 实现:

class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr
        
    def update(self, params, grades):
        for key in params.keys():
            params[key] -= self.lr * grades[key]

将优化器实现为一个类是一个很好的做法, python 是动态语言的原因,我们在实现的时候只要类里面都有 update 方法,解释器就会正常执行。例如我们可以将之前的学习过程写成这样:重点在于加了 # 号的那几行。

# 导入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

# 全局变量
train_loss_list = []
train_acc_list = []
test_acc_list = []

# 超参数
iter_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
iter_per_epoch = max(train_size / batch_size, 1)

# 生成模型
net_work = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
optimizer = SGD(learning_rate)   ########

# 模型的训练
for i in range(iter_num):
    # 获取 mini_batch
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    # 计算梯度
    grads = net_work.gradient(x_batch, t_batch)

    # 更新参数
    params = net_work.params
    optimizer.update(params, grads)  ##########

    # 记录学习过程
    if i % iter_per_epoch == 0:
        loss = net_work.loss(x_train, t_train)
        train_loss_list.append(loss)
        train_acc = net_work.accuracy(x_train, t_train)
        train_acc_list.append(train_acc)
        test_acc = net_work.accuracy(x_test, t_test)
        test_acc_list.append(test_acc)
        print('运行中... 损失:{} 训练集准确度:{}  测试集准确度:{}'.format(loss, train_acc, test_acc))

SGD 的优点就是简单,容易实现。但是其缺点就是低效,因为有的时候梯度的方向并没有指向最小值的方向。低效的原因有两大方面:

    1. 函数呈延伸状,梯度指向了’谷底‘。(文章的最后有一个呈延伸状函数的图片)
      这使得损失函数值不停的在震荡。
    1. 梯度方向指向了极小值,或者鞍点方向,
      因为所有维度的梯度在这附近都接近于 0,这使得损失函数在这里变化的很慢。

下面的方法就全是针对这两大方面对 SGD 进行改进。

2. Momentum

这种方法主要是为了解决第一种情况,当函数呈延伸状的情况下,梯度指向了谷底,而不是直接指向了最低点,函数值在学习过程中会来回震荡,但是向最低点移动的却很小。

上面的叙述中提到了两个方向,一个是纵向的震动,一个是横向的向最低点移动,如下图,如果我们可以避免或者减少震荡,加快横向的向最低点移动,那么就加快了学习。

实际上震荡是不可避免的,所以我们只能考虑减轻震荡。

我们还是先看一下数学描述:

v_t \leftarrow \alpha v_{t - 1} - \eta \frac{\partial L}{\partial W} W \leftarrow W + v_t

Momentum 在 SGD 的基础上引入了一个变量 - 速度 v 和一个超参数 - 指数衰减平均 \alpha

我们分别来看一下他们的含义。为了便于理解,我们先将 \alpha 去掉,或者说是设为 1。首先将 v_0 初始化为零矩阵,然后进行第一次迭代,v_1 保存的就是上一次的梯度,如果方向没改变,再一次迭代的时候梯度会被累加,加快学习。如果方向改变了(符号发生了变化),那么就减少了这次的学习,这样其实就实现了我们的目的:使得梯度方向不变的维度上速度变快,梯度方向有所改变的维度上的更新速度变慢,这样就可以加快收敛并减小震荡

接下来我们再来看一下超参数 \alpha ,它是描述之前梯度对现在影响的参数。\alpha 越大表示之前梯度对现在的影响越大。\alpha 一般被设定为 0.9。

说了这么多理论,我们还是要实现一下这个优化器:

class Momentum:
    def __init__(self, lr=0.01, alpha=0.9):
        self.lr = lr
        self.alpha = alpha
        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.alpha*self.v[key] - self.lr*grads[key]
                params[key] += self.v[key]

如果想使用 Momentum ,只需要将上面代码中的 optimizer = SGD(learning_rate) 修改为 optimizer = Momentum(learning_rate) 即可。
保持其他参数不变,训练的结果是:

SGD: 损失:0.5450568343946544 训练集准确度:0.8639  测试集准确度:0.8672
Momentum: 损失:0.1830601846034292 训练集准确度:0.94755  测试集准确度:0.9444

所以总结一句,优点:使得梯度方向不变的维度上速度变快,梯度方向有所改变的维度上的更新速度变慢,可以加快收敛并减小震荡。
但是也有缺点:这种方法相当于小球从山上滚下来时是在盲目地沿着坡滚,如果它能具备一些先知,例如快要上坡时,就知道需要减速了的话,适应性会更好。根据这个改进的优化器叫做:NAG(Nesterov Accelerated Gradient)

3. AdaGrad

这种方法主要是为了解决 SGD 遇到鞍点或者极小值点后学习变慢的问题。我们知道超参数学习率是一个很重要的参数,不同的参数对学习结果的影响很大,如果设置的值较小,会导致学习花费较多的时间,学习率大了就会导致学习发散而不能正常的进行。所以我们可以考虑避免人为的介入,根据需要让程序自己动态地设置学习率。例如对于遇到鞍点的情况,参数变化很小,基本不会改变,那么这个方法就会设置一个较大的学习率,跨过鞍点。

在神经网络中有一种方法经常被使用:学习率衰减方法(learning rate decay),也就是说随着学习的进行,使学习率逐渐减少。AdaGrade 进一步发展了这个想法,它会为参数的每一个元素设当的调整学习率。

我们还是看一下数学的描述:

h \leftarrow h + \frac{\partial L}{\partial W} \odot \frac{\partial L}{\partial W} W \leftarrow W - \eta \frac{1}{\sqrt h}\frac{\partial L}{\partial W}

这里新出现了一个变量 h ,它保存了之前所有梯度的平方和,在更新参数的时候通过乘以 \frac{1}{\sqrt h} 就可以调整学习的尺度。
我们还是尝试实现它:

class AdaGrad:
    def __init__(self, lr=0.01):
        self.lr = lr
        self.h = None

    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)

        for key in params.keys():
            self.h[key] += grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

结果为:

AdaGrad: 损失:0.20811779864341037 训练集准确度:0.9411  测试集准确度:0.936

AdaGrad 的优点是可以动态的调整学习率,
缺点是 AdaGrad 会记录过去所有的梯度平方和,最后有可能不再更新,
针对这个问题有一个方法叫做 RMSProp 进行了优化。

4. Adam

Adam 直观的来讲就是融合了 Momentum 和 AdaGrad 方法,详细可以参考原版论文
详细回头再更,,

class Adam:
    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None
        
    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)
        
        self.iter += 1
        lr_t  = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)         
        
        for key in params.keys():
            #self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
            #self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])
            
            params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
            
            #unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
            #unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
            #params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)

最后看两个动态的优化过程,图片来自网络,侵删。
第一个是存在鞍点和局部极小值的情况。


第二个是损失函数呈延伸状的情况。

图片的绘制可以参考https://github.com/dream-catcher/learning_blogs

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容