ITPub博客

首页 > > > 10686 一次 CTC-RNN 调参经历

10686 一次 CTC-RNN 调参经历

作者:格伯纳 时间:2017-07-30 10:04:34 0 删除 编辑

  这个月花了将近一整个月的时间,调整了一个循环神经网络(RNN)的超参数,经历了各种挫折,最终取得了成功。在我最迷茫的时候,我曾经试图在网上搜索有针对性的调参经验,但一无所获。为了给后来人排雷,我就把我的调参经历记录在这里。

  要读懂这篇文章,你需要了解 RNN、LSTM、CTC,最好有过训练 CTC 网络的体验。

零、任务描述

  任务背景其实并不重要。如果你不想详细看,可以只看这一句:我用随机梯度下降法(SGD)训练了一个使用 CTC 输出层的 RNN,可调的超参数包括梯度裁剪阈值和初始学习率。

  我训练的 RNN 是一个语音识别任务的声学模型。它的输入是 40 维的滤波器组特征序列,输出是音素序列。网络并不负责把音素序列进一步转换成单词序列,所以它只是一个声学模型,并不是一个完整的语音识别器。

  训练使用的数据为 TEDLIUM v1,其内容为 TED 讲座。其中训练数据有 206 小时,开发数据有 1.7 小时,测试数据有 3.1 小时。由于种种原因,实际中我使用了 95% 的训练数据进行训练,5% 的训练数据进行验证,开发集和测试集没有用到。

  我一共训练了两个版本的网络。第一个版本与 EESEN 中的示例 相同。它含有 5 个双向 LSTM 隐藏层,每层每个方向有 320 个神经元;CTC 输出层含有 40 个神经元(39 个音素 + 1 个空白),放在一个 softmax 组中。训练时的目标函数为平均每帧的 negative log-likelihood。训练算法为随机梯度下降(SGD),每个 batch 含有 2 万帧,每个 epoch(即扫描完一趟所有训练数据)约为 2,000 个 batch。使用的超参数为:梯度裁剪至 10^{-4};初始学习率为 3,在前 12 个 epoch 保持恒定,之后每个 epoch 折半,直到 24 个 epoch;训练使用了 Nesterov momentum,系数为 0.9。解码时采用朴素的 best-path decoding 得到输出音素序列,网络性能用音素错误率(PER)衡量。这个网络的训练没打什么麻烦,1 个 epoch 过后,训练集上的错误率就降到了 28.8%,验证集为 30.2%;用 6 天时间跑完全部 24 个 epoch 后,最终的性能为:训练集 4.8%,验证集 15.4%。

  我真正想训练的是第二个版本 ——「弱监督」版本。在这个版本中,对于每一条训练语音,我不再告诉网络它对应的音素串是什么,而只告诉它每个音素出现了几次。网络的隐藏层结构都不变,但输出层现在只有 39 个神经元,均使用 sigmoid 激活函数。这可以看成是使用了 39 个平行的 CTC 输出层,每个输出层只负责识别一个音素;在计算目标函数时,每个 CTC 层的目标序列就是它所负责的音素重复指定的次数。训练方法、解码方法、性能指标均与「强监督」版本相同。为了控制超参数空间的规模,我只调整梯度裁剪阈值和初始学习率这两个超参数。我的预期是这个网络仍能进行音素识别,但学习过程可能会慢一些,性能也会比「强监督」版本差一些。

一、出师不利

  首先,我把「强监督」版本的超参数(梯度裁剪阈值 10^{-4}、初始学习率 3)直接应用到了「弱监督」网络上。结果,经过了好几个 epoch,网络都没有输出任何音素,两个数据集上的错误率都是 100%。对比「强监督」版本在 1 个 epoch 后就达到了 30% 左右的错误率,「弱监督」版本显然出问题了。

二、误入歧途

  我猜想,「弱监督」网络之所以学不到东西,是因为它的目标函数跟「强监督」版本不同,造成最优超参数发生了偏移。但我并不确定应该把超参数调大还是调小。于是,我对梯度裁剪阈值 limit、初始学习率 lr 两个参数进行了 grid search,limit 在每个数量级内取一个值,lr 在每个数量级内取两个值。由于训练比较费时间(1 个 epoch 需要 8 个小时),对每组超参数,我只训练了 50 个 batch(相当于 1/40 个 epoch),并观察此时在验证集中某 10 条语音上的错误率。如果训练正常,这个错误率应该小于 100%;如果训练异常,这个错误率可能等于 100%,也可能发生梯度爆炸导致程序崩溃。

  Grid search 的结果如下图所示:

(图 1:初次 grid search 的结果)

  这个图需要好好消化一下。图的纵轴是梯度裁剪阈值,横轴是初始学习率。首先注意到,最低的错误率(蓝色)分布在对角线上。这很容易理解:当梯度真的被裁剪到了的时候,裁剪阈值缩小一个数量级,就需要学习率放大一个数量级,才会让网络参数发生相同的变化。注意到上图正中的那个 89.3,它以及周围的一些数字向右下方向延伸时毫无变化,这说明梯度裁剪已经非常「狠」了,梯度中绝大多数的分量都被裁剪得只剩下符号。这并不是我想要的。而向左上方延伸时,数值稍有不同,这说明左上这片区域中,梯度裁剪的力度适中,梯度的有些分量被裁剪了,有些则没有。

  当超参数的组合偏离对角线时,错误率就开始增大了。在对角线左侧,学习率太小,所以学得慢;在对角线右侧,学习率太大,容易造成梯度爆炸(鲜红色)。左下角和右上角的两片区域我并没有尝试,因为这两片区域中显然不会出现更低的错误率。

  整张图中最低的错误率是左上角的 88.3 和 88.5,它们对应的 limit 分别为 10^{-3} 和 10^{-4},接近我最初尝试的值(10^{-4});而 lr 则是 0.01 和 0.03,比我最初尝试的值(3)小了两个数量级。于是我觉得,第一节中网络没有输出的原因是学习率太大了,网络可能陷入了一个「什么也不输出」的局部最优解,出不来了。

  我用刚刚找到的最优超参数以及它附近的几组超参数重新训练网络,结果发现,若在每个 epoch 后测量错误率,仍然全都是 100%。实际上,对于 (limit = 10^{-3},lr = 0.01) 这组超参数,「50 个 batch 之后」恰恰是错误率最低的时机。此后,错误率会逐渐回升,并在 1 个 epoch 以内回升到 100% 并保持。难道说用弱监督真的什么也学不到?我不甘心。

三、曲径通幽

  为了弄清楚网络到底在干什么,我决定画出在某一条语音上网络各层的输出,看看能否观察出端倪。

(图 2:弱监督网络训练 50 个 batch 后各层的输出)

  上图是「弱监督」网络使用最初的超参数(limit = 10^{-4},lr = 3)训练 50 个 batch 后,在验证集里一条语音上各层的输出。这条语音共有 85 帧。图中从左到右、从上到下依次是输入的滤波器组特征、5 个隐藏层的输出,以及最后预测的每个音素的概率。由于每个隐藏层有 640 个神经元,图中画不下,所以我只画了向前方向的 40 个神经元的输出。可以看出,各层的输出越来越「糊」,即在时间方向上的变化越来越不明显,到了第 3 个隐藏层,输出就基本是常数了。当然,在最开头的几帧,输出还是略有不同的。我觉得,这表示 RNN 陷入了某种「死循环」,除了开头的几帧,RNN 就陷入了「不管输入是什么,输出都不变」的死亡状态了。上面的图中画的是训练 50 个 batch 之后的情景,但哪怕训练到 100 个 batch,结果都是类似的。

  我试图分析网络参数(包括权重和偏置)的变化,以及 LSTM 各个门的开闭情况,都没能得出明确的结论。于是,我准备也画出「强监督」网络在同一条语音上各层的输出,与「弱监督」网络对比。

(图 3:强监督网络训练 10 个 batch 后各层的输出)

  上图是「强监督」网络在训练 10 个 batch 后各层的输出。我们同样看到,从第 4 个隐藏层开始,输出也「糊」掉了。但!是!随着训练的进行,这种「糊」掉的现象得以改善。比如,在训练 200 个 batch 之后,各层的输出是下面这样的。可以看到,所有隐藏层的输出都开始随着时间有变化,而且变化与输入特征中的突变基本同步。网络开始学到东西了!

(图 4:强监督网络训练 200 个 batch 后各层的输出)

   对「强监督」网络的观察推翻了我对于「弱监督」网络陷入「死循环」的猜想。原来,网络并不是「死了」、已经学不到东西了,而是还没有开始「活」。我想起了 CTC 网络在训练的初始阶段,会有一个「热身」(warm-up)的过程,此时「空白」符号的概率占绝对优势,网络什么也不会输出。而在「热身」阶段之前,其实还会有一个「预热身」阶段,在此阶段,「空白符号的概率占绝对优势」这一秩序尚未建立,网络可能会输出一些随机的符号(音素)。观察图 2 的最后一个子图,可以看到有 3 个音素具有较高的概率,网络会把它们输出出来。如果正好某一个音素也出现在标准答案中,PER 就会小于 100% 了。如果观察 PER 在 CTC 训练过程中的变化,会是这个样子:

(图 5:CTC 训练过程中各个阶段错误率的变化)

  原来,图 1 中所有小于 100% 的错误率,其实都对应着「预热身阶段」。它们在一段时间后回升到 100%,不代表着网络的死亡,而是代表进入了「热身阶段」。为了促使网络尽快脱离热身阶段、进入学习阶段(下文称为「启动」),不应该把学习率调低,反而应该调高。在第二节,我们调参的方向正好反了。

  当然,如果只调高学习率,不调整梯度裁剪阈值,就会导致梯度爆炸;所以需要相应地调低梯度裁剪阈值。如果放在图 1 中来看,就是说,最优参数很可能不位于图 1 的左上方,而是位于右下方。咦?等等,在图 1 的右下方,只要保持 limit * lr 恒定,错误率都一样呀?其实并不是这样 ——「错误率都一样」只是预热身阶段的现象。随着训练的进行,梯度的绝对值会越来越小,等到梯度裁剪不起作用的时候,就只剩下学习率决定训练进展的速度了。这说明我们应当调高学习率,同时调低梯度裁剪阈值。另外,最优参数不一定位于图 1 中错误率最低的对角线上,因为预热身阶段的错误率并不能说明学习阶段的表现。我们需要在图 1 右下方区域内重新进行 grid search,这次要观察的不是一定时间后的错误率,而是需要多久网络才能「启动」。

四、昂首阔步

  第二次 grid search 的结果如下图。我选择的超参数都位于图 1 中对角线偏右侧的区域,因为我观察到,只有这片区域内的超参数才能使网络在 100 个 batch 之内脱离预热身阶段,而我的目的正是寻找让网络训练进展更快的超参数。对于每组超参数,我训练了 10,000 个 batch,相当于 5 个 epoch,用时接近 2 天。每过 200 个 batch,我测量了验证集中 10 条语音上的错误率,图中记录的是网络「启动」的时间和发生梯度爆炸的时间。

(图 6:第二次 grid search 的结果)

  训练 10,000 个 batch 后,我们看到了与图 1 不一样的风景。原来,当我们「昂首阔步」地把学习率调高好几个数量级之后,网络还是可以在合理的时间内启动的。使网络启动最快的超参数出现在 limit = 10^{-8},lr = 10^4,它只需要 1,200 个 batch 就能启动。到 5 个 epoch 时,在随机选择的 10 条验证语音上的错误率已经达到了 55.6%,网络果然学到东西了!

五、宁静致远

  于是,我用 limit = 10^{-8},lr = 10^4 这组参数进行了正式的实验,每个 epoch 后在整个验证集上测量错误率。令我惊喜的是,在 5 个 epoch 后,整个验证集上的错误率实际上已经降到了 44.3%。但是,到第 8 个 epoch 时,发生了梯度爆炸,训练崩溃了。

  看来,上一节中使用的学习率还是「步子太大,扯到蛋了」。虽然启动很快,但当梯度小到裁剪不起作用的时候,学习率太大的坏处就体现出来了。这意味着学习率不应该调得这么大,走得慢一点,才能走得远。当然,还有一种解决方案是在第 8 个 epoch 之前开始降低学习率,不过如果去试验这一点,就意味着把学习率的 schedule(包括在什么时候开始衰减、衰减多快)也纳入待调整的超参数,超参数空间一下子又升了两维,这并不是我想要的。于是,我在图 6 中试验过的超参数的左侧又进行了第三次 grid search。这次 grid search 是正式的 —— 我跑完了全部 24 个 epoch,并在每个 epoch 后在整个验证集上测量错误率。下图展示了结果。绿色背景的单元格表示成功跑完 24 个 epoch 的超参数,其中的数值是最终验证集上的错误率;橙色背景的单元格表示发生梯度爆炸的超参数,其中记录了发生爆炸的 epoch 数,以及前一个 epoch 的错误率。如果 6 个 epoch 后网络仍未启动,我则没有让它们继续跑下去,相应的单元格涂成红色。

(图 7:第三次 grid search 的结果)

  最终得到的最优超参数为 limit = 10^{-6},lr = 100。在 24 个 epoch 之后,它在训练集和验证集上的错误率分别为 30.1% 和 34.5%,高于「强监督」网络的 4.8% 和 15.4%。这说明在「弱监督」下网络仍能学到东西,但不如在「强监督」下学得那么快、那么好,完美验证了我的预期。

六、经验总结

  1. CTC 在训练过程中会经历预热身阶段、热身阶段、学习阶段。在热身阶段,网络没有任何输出,错误率会是 100%;在预热身阶段,网络会有少量随机输出,错误率略低于 100%。如果只观察错误率,容易把预热身阶段误当成学习阶段。
  2. 如果观察到 RNN 中各隐藏层的输出不随时间变化,并不意味着网络已经陷入了「死循环」,往往只是网络还没有开始学习。此时应加大学习率,促使网络尽快进入学习阶段。
  3. 在对 CTC 网络的超参数进行 grid search 时,如果为了省时间而只跑较少的 batch 数,往往欲速则不达:你可能只观察到了预热身阶段;也有可能在学习阶段的初期学得很快的超参数会导致后期梯度爆炸。最终的最优超参数还是要跑完整个训练过程才能确定。

原文作者:;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论
管理员

注册时间:2018-03-30

  • 博文量
    2383
  • 访问量
    983853