• Dan版本的nnet2


    除了chain,nnet1, nnet2, nnet3训练时调整转移模型,chain模型使用类似与MMI的训练准则

    Dan's setup does not uses pre-training.

    Dan's setup uses a fixed number of epochs and averages the parameters over the last few epochs of training.

    首先是脚本

    首先可以从脚本中获得神经网络训练的顶层概述。 在egs/rm/s5egs/wsj/s5egs/swbd/s5b的标准示例脚本中,顶层脚本是run.sh. 此脚本调用(有时会注释掉)一个名为local/run_nnet2.sh的脚本。 这是Dan设置的顶层示例脚本。 在local/run_nnet2.sh中,有几个不同的示例对应不同的方案,我们尝试指出我们认为哪一个是任何时间点的"主要"方案。 而不是运行local/ run_nnet2.sh,这可能需要一些时间,我们建议您只需运行"主要"方案。 这通常是一个p-norm网络 (see this paper ).

    顶层训练脚本

    注意:以前的顶层训练脚本(以前是steps/nnet2/train_pnorm.sh)现在已弃用,您应该使用steps/ nnet2 / train_pnorm_fast.sh 这个脚本将通过多个节点并行化训练,我们将在下面解释一下。

    将特征输入到神经网络中

    神经网络的输入特征可以在一定程度上进行修改,但默认情况下,它们由相同的流程进行处理,通常的特征将经过适应(adapted),这些特征将输入到基于GMM的模型中:通常是MFCC(拼接)+ LDA + MLLT + fMLLR40维特征。神经网络实际输入的是这些特征的窗函数结果,将中央帧的左右两侧侧都加入4个帧(默认情况下)。因为神经网络很难从相关输入中学习,所以我们将这些特征乘以7,即变成40 * 7维,通过一个固定变换(lda,这一层不随着训练发生变化)来对特征进行解相关(去相关)。创建这个变换是训练脚本的第一件事情;它通过调用steps/ nnet2 / get_lda.sh来实现。这最初是基于我们在本文中的工作,但代码中计算得出的转换并不完全是LDA:在默认情况下,它更像是LDA变换的非降维形式(即维数不变的LDA),随后减少其中类间差异较小的输出特征的方差的维数尺寸。 (这是未发布的;请参阅代码)。脚本支持的其他类型的功能是未处理的功能,例如MFCC功能;这可以通过-feat-type选项激活,该选项必须传递给-egs-opts-lda-opts选项的get_egs.shget_lda.sh脚本。

       

    既然在脚本里寻找选项,最好的方式就是仅仅搜索用下划线代替破折号的选项名:在这种情况下,就变为feat_typeegs_opts,和 lda_opts。脚本utils/parse_options.sh自动解释那些设置对应变量的命令行参数。

    将训练数据dump到磁盘中

       

    假设最顶层的脚本(比如steps/nnet2/train_pnorm.sh)创建一个模型,在exp/nnet5d/。这个脚本做的第一件事情就是去调用steps/nnet2/get_egs.sh。这就放置了很多数据在exp/nnet5d/egs/。这个与输入的帧层随机化有关,这个对于随机梯度下降训练是需要的。我们仅仅做一次随机化,以至于在真正的训练中,我们可以顺序的访问这个数据。也就是说,每一轮迭代,我们有必要以相同的顺序访问数据;意味着对磁盘和网络是友好的。(事实上我们可以每次迭代时使用一个不同的种子和使用一小段内存来做随机化,但是这仅仅在本地上改变了顺序)

       

    如果你去看exp/nnet5d/egs/,你会看到很多文件名为egs.1.1.ark egs.1.2.ark等等。这些archives文件含有一个叫NnetTrainingExample的许多例子。对于一个单独的帧,这个类含有标签信息,和对这帧来说,有足够多的特征窗来做神经网络计算。除了在神经网络外部做帧切分,神经网络训练代码有一个时间的概念和需要知道上下文特征是多少(看函数RightContext()LeftContext())。在这个文件名中的2个整数索引分别是任务索引和迭代索引。任务索引对应我们有多少个并行的任务。例如如果我们使用CPUs运行,用16台机器并行(这里每个机器有很多不相关的线程),然后任务索引就是从116,或者你使用GPUs,使用8GPUs并行,然后任务索引就是从18。迭代索引的范围主要根据你有多少数据来决定的。默认情况下,每个archive200,000个样本数。迭代索引的数量是由你有多少数据和你有多少任务来决定的。一般很多轮(比如:20)跑一次训练,和每一轮我们需要做很多次迭代(对于像rm这样的小数据量就是1,对于大的数据集可以为10)

       

    目录exp/nnet5d/egs/也包含一些其他的文件:iters_per_epoch num_jobs_nnetsample_per_iter,这些文件里包含一些数;在一个rm数据集上的例子里,它们分别为11685493。它也包含valid_diagnostic.egs,这是用来诊断的,在held-out集上的一些小例子(e.g. exp/nnet5d/log/compute_prob_valid..log),和 train_diagnostic.egs,是除了held-out外的valid_diagnostic.egs;为了诊断,可以看exp/nnet5d/log/compute_prob_valid..log。文件combine.egs是在训练的结束用来计算神经网络的联合权重的一个稍微大点的训练数据集。

    神经网络初始化

       

    我们使用一个单隐含层来初始化这个神经网络;我们将在稍后的训练中增加隐含层的数量到一个设定的数字(通常是25这个范围)。这个脚本创建了一个叫exp/nnet4d/nnet.configconfig文件,这将传递给初始化模型的程序,名字叫nnet-am-init。对于rm数据集,对于p-norm部分的一些config文件的例子如下:

       

    SpliceComponent input-dim=40 left-context=4 right-context=4 const-component-dim=0

    FixedAffineComponent matrix=exp/nnet4d/lda.mat

    AffineComponentPreconditionedOnline input-dim=360 output-dim=1000 alpha=4.0 num-samples-history=2000 update-period=4 rank-in=20 rank-out=80 max-change-per-sample=0.075 learning-rate=0.02 param-stddev=0.0316227766016838 bias-stddev=0.5

    PnormComponent input-dim=1000 output-dim=200 p=2

    NormalizeComponent dim=200

    AffineComponentPreconditionedOnline input-dim=200 output-dim=1475 alpha=4.0 num-samples-history=2000 update-period=4 rank-in=20 rank-out=80 max-change-per-sample=0.075 learning-rate=0.02 param-stddev=0 bias-stddev=0

    SoftmaxComponent dim=1475

       

    FixedAffineComponent是我们之前提到的像LDA去相关变换。AffineComponentPreconditionedOnlineAffineComponent的改良。AffineComponent是由标准的神经网络(权重矩阵和偏移项)构成的,在标准的随机梯度下降法训练中使用。AffineComponentPreconditionedOnline就是AffineComponent,但是训练步骤使用的不仅仅是一个单独的全局学习率,而是一个矩阵值学习率来预处理梯度下降。我们接下来将介绍更多(dnn2_preconditioning)PnormComponent是非线性的;对于一个更传统的神经网络,将使用TanhComponent来代替。 insteadNormalizeComponent是我们用来稳定p-norm网络训练而添加的。它也是固定的,非训练的非线性,但是它的作用不是一个单独的激活函数,而是对于一个单独的帧的整个向量,来重新归一化他们的单位标准偏差。SoftmaxComponent是最终的非线性,它是在输出上产生归一化的概率。

       

    这个脚本也产生一个名叫hidden.configconfig文件,这对应到我们添加的和我们介绍的一个新的隐含层;这个例子如下:

       

    AffineComponentPreconditionedOnline input-dim=200 output-dim=1000 alpha=4.0 num-samples-history=2000 update-period=4 rank-in=20 rank-out=80 max-change-per-sample=0.075 learning-rate=0.02 param-stddev=0.0316227766016838 bias-stddev=0.5

    PnormComponent input-dim=1000 output-dim=200 p=2

    NormalizeComponent dim=200

       

    直到第一对训练后,它才被使用。

       

    脚本接下来做的就是调用nnet-train-transitions。它将计算在解码(似乎与神经网络本身无关)中使用的HMMs的转移概率,和计算这些目标(成千上万个上下文独立的状态)的先验概率。然后,当我们解码时,我们用神经网络得到的后验概率除以这些先验得到"pseudo-likelihoods";这个比原来的后验概率更加与HMM框架兼容。

    神经网络训练

       

    接下来我们将来到最主要的训练阶段。这是一个在迭代计算器x,范围从0num_iters - 1的一个循环。迭代的次数num_iters是我们训练的轮数乘以每一轮我们迭代的次数。我们训练的轮数是num_epochs(default: 15)加上 num_epochs_extra(default:5)的和。这个与学习率进程有关:默认的,我们从最开始的学习率initial_learning_rate(default: 0.04)下降到15轮后的final_learning_rate (default: 0.004)和在后面的5轮保持为常数。每一轮的迭代次数存储在一个文件叫egs/nnet5d/egs/iters_per_epoch中;它取决于我们的数据量的大小和我们并行运行时的训练任务数,和一般是从0到几十范围内。

       

    在每一次迭代中,我们做的第一件事情就是计算一些诊断:在训练和交叉集上的目标函数(对于第十次迭代,可以看egs/nnet5d/log/compute_prob_valid.10.logegs/nnet5d/log/compute_prob_train.10.log)。在文件egs/nnet5d/log/progress.10.log中你将看到每一层参数中变化的程度,和每一层的参数变化对训练集目标函数的变化的影响有多大。

       

    下面是在某个特定目录中一个诊断的例子

       

    grep LOG exp/nnet4d/log/compute_prob_*.10.log

    exp/nnet4d/log/compute_prob_train.10.log:LOG<snip> Saw 4000 examples, average probability is -0.894922 and accuracy is 0.729 with total weight 4000

    exp/nnet4d/log/compute_prob_valid.10.log:LOG<snip> Saw 4000 examples, average probability is -1.16311 and accuracy is 0.6585 with total weight 4000

       

    你可以看到在训练集的目标函数(-0.89)比交叉集上的(-1.16)要好。这是一个交叉熵,表示每一帧(属于测试集或验证集)分到正确的类的平均对数概率。在训练集和交叉集上的目标函数相差很大是正常的,因为神经网络有一个很强大的学习能力:对于一个用仅仅几个小时的数据来调参的系统上,它们可能相差两倍(但是当你含有很多训练数据的话差异会更小)。 如果你在训练目标函数上添加更多的参数将会改善目标函数,但是交叉集上的性能将下降。然而,对交叉集目标函数上调参不是一个好主意,因为它会导致系统含有很少的参数。不过,添加参数对提高WER(Word Error Rates)来说还是有帮助,即使它在一定程度上降低了交叉集上的性能。

       

    在文件exp/nnet4d/log/progress.10.log里,你将找到如下的诊断分析:

       

    LOG <snip> Total diff per component is [ 0.00133411 0.0020857 0.00218908 ]

    LOG <snip> Parameter differences per layer are [ 0.925833 1.03782 0.877185 ]

    LOG <snip> Relative parameter differences per layer are [ 0.016644 0.0175719 0.00496279 ]

       

    第一行"Total diff per component"通过由"不同层的影响(贡献)"来分解训练集目标函数的变换,其他行表示不同层的参数变化有多大。

       

    主要训练任务的log文件可以在exp/nnet5a/log/train...log被找到。第一个索引就是迭代的次数和第二个索引就是我们当前运行的并行任务数(总任务数为4或者16,这个数字是由–num-jobs-nnet参数传递到脚本的)。接下来是其中一个训练任务的例子:

       

    #> cat exp/nnet4d/log/train.10.1.log

    # Running on a11

    # Started at Sat Mar 15 16:32:08 EDT 2014

    # nnet-shuffle-egs --buffer-size=5000 --srand=10 ark:exp/nnet4d/egs/egs.1.0.ark ark:- |

    nnet-train-parallel --num-threads=16 --minibatch-size=128 --srand=10 exp/nnet4d/10.mdl

    <snip>

    LOG (nnet-shuffle-egs:main():nnet-shuffle-egs.cc:100) Shuffled order of 79100 neural-network

    training examples using a buffer (partial randomization)

    LOG (nnet-train-parallel:DoBackpropParallel():nnet-update-parallel.cc:256) Did backprop on

    79100 examples, average log-prob per frame is -1.4309

    LOG (nnet-train-parallel:main():nnet-train-parallel.cc:104) Finished training, processed

    79100 training examples (weighted). Wrote model to exp/nnet4d/11.1.mdl

    # Accounting: time=18 threads=16

    # Finished at Sat Mar 15 16:32:26 EDT 2014 with status 0

       

    这个特定的任务是没用使用GPU,而是使用16 CPU并行运行的,仅仅使用18秒就完成了。这里最主要的任务就是nnet-train-parallel,它是做随机梯度下降的,与Hogwild(例如:没有锁)的并行化有点相似, 每个线程使用的块(minibatch)大小为128。这个模型的输出是11.1.mdl。在exp/nnet4d/log/average.10.log中,你可以看到一个程序叫nnet-am-average的输出log文件,这个程序为这次迭代对所有的SGD训练的模型做平均化。 它通过我们的学习率进度来修改学习率,这个是指数级下降的(具体可以看论文"An Empirical study of learning rates in deep neural networks for speech recognition" by Andrew Senior et. al.,你会发现对于语音识别这个非常有效)。注意:在我们例子里的最后二层中,我们使用tanh这个函数,学习率需要减半;看脚本train_tanh.sh里的选项–final-learning-rate-factor

       

    对于几十万的样本来说,最基本的并行方法就是,在不同的任务中使用不同的数据来使用随机梯度下降法来训练,然后对这些模型进行平均化。在这个参数上目标函数是非凸的,你也许会惊讶这个是可行的,但是从经验上看,凸函数这个性质在这里似乎不是一个问题。注意:我们接下来描述的去做"preconditioned update",这个看起来更重要;我们通过大量的实验证明,这个对于我们并行化方法的成功至关重要。同时也要注意在最近的训练脚本(train_pnorm_fast.shtrain_tanh_fast.sh)中,我们没有做并行化和对迭代做平均,因为这里或者仅仅是初始化模型或者仅仅添加新的一层。这是因为在这些情况下,由于缺少凸性,做平均化有时候一点好处都没有(例如,在给定平均化模型的目标函数比单独目标函数的平均化要差的情况)

    最终模型的合并

       

    如果你继续往里面看,举例来说,exp/nnet4d/log/combine.log,你会看到一个最终的神经网络被创建,名字叫"final.mdl"。这是联合最后N次迭代的模型的参数得到的,这里的N对应脚本里的参数–num-iters-final(默认地:20)。最基本的想法就是通过对若干次迭代上做平均,这样可以减少估计的偏差。我们不能很容易的证明这个就比仅仅取最终的模型好(因为这是一个非凸问题),但是在实践中就是这样的。事实上,"combine.log"不仅仅是对这些参数取平均。这里使用训练数据样本(在这种情况下,从exp/nnet4d/egs/combine.egs得到)的一个子集来优化这些权重, 这个没有约束为正的。这里的目标函数是在这个数据集上的正常的目录函数(log-probability),和优化方法是L-BFGS,这里我们就不赘述这个特殊的预处理方法。对每个成分和每次迭代都有单独的权重,所以在这种情况下我们有学习(20*3=60)的权重。在这种方法的最初始版本,我们使用交叉集数据来估计这个参数,但是我们发现为了这个目的使用训练数据的一个随机子集,它可能表现的很好。

       

    #> cat exp/nnet4d/log/combine.log

    <snip>

    Scale parameters are [

    -0.109349 -0.365521 -0.760345

    0.124764 -0.142875 -1.02651

    0.117608 0.334453 -0.762045

    -0.186654 -0.286753 -0.522608

    -0.697463 0.0842729 -0.274787

    -0.0995975 -0.102453 -0.154562

    -0.141524 -0.445594 -0.134846

    -0.429088 -1.86144 -0.165885

    0.152729 0.380491 0.212379

    0.178501 -0.0663124 0.183646

    0.111049 0.223023 0.51741

    0.34404 0.437391 0.666507

    0.710299 0.737166 1.0455

    0.859282 1.9126 1.97164 ]

       

    LOG <snip> Combining nnets, objf per frame changed from -1.05681 to -0.989872

    LOG <snip> Finished combining neural nets, wrote model to exp/nnet4a2/final.mdl

       

    联合权重作为一个矩阵被打印出来,它的行索引对应迭代的次数,列索引对应层数。你将看到,联合权重在后面的迭代中是正的,在之前的迭代中是负的,我们可以解释为尝试对模型在这个方向上做更深的研究。我们使用训练数据集,而不是交叉数据集,因为我们发现训练数据集效果更好,尽管使用交叉数据集是一个更自然的想法;我们认为这个原因可能与在语音识别中的一个不准确的"dividing-by-the prior"归一化有关。

    Mixing-up

       

    如果使用程nnet-am-info来打印关于exp/nnet4d/final.mdl的信息,你将看到在输出层之前有一个大小为4000的层,这个输出层的大小是1483,因为决策树有1483个分支:

       

    #> nnet-am-info exp/nnet4d/final.mdl

    num-components 11

    num-updatable-components 3

    left-context 4

    right-context 4

    input-dim 40

    output-dim 1483

    parameter-dim 1366000

    component 0 : SpliceComponent, input-dim=40, output-dim=360, context=4/4

    component 1 : FixedAffineComponent, input-dim=360, output-dim=360, linear-params-stddev=0.0386901, bias-params-stddev=0.0315842

    component 2 : AffineComponentPreconditioned, input-dim=360, output-dim=1000, linear-params-stddev=0.988958, bias-params-stddev=2.98569, learning-rate=0.004, alpha=4, max-change=10

    component 3 : PnormComponent, input-dim = 1000, output-dim = 200, p = 2

    component 4 : NormalizeComponent, input-dim=200, output-dim=200

    component 5 : AffineComponentPreconditioned, input-dim=200, output-dim=1000, linear-params-stddev=0.998705, bias-params-stddev=1.23249, learning-rate=0.004, alpha=4, max-change=10

    component 6 : PnormComponent, input-dim = 1000, output-dim = 200, p = 2

    component 7 : NormalizeComponent, input-dim=200, output-dim=200

    component 8 : AffineComponentPreconditioned, input-dim=200, output-dim=4000, linear-params-stddev=0.719869, bias-params-stddev=1.69202, learning-rate=0.004, alpha=4, max-change=10

    component 9 : SoftmaxComponent, input-dim=4000, output-dim=4000

    component 10 : SumGroupComponent, input-dim=4000, output-dim=1483

    prior dimension: 1483, prior sum: 1, prior min: 7.96841e-05

    LOG (nnet-am-info:main():nnet-am-info.cc:60) Printed info about baseline/exp/nnet4d/final.mdl

       

    softmax层的维度是4000,然后由SumGroupComponent减少到1483.你使用命令nnet-am-copy把它转化为文本格式,然后你可以看到一些信息:

       

    #> nnet-am-copy --binary=false baseline/exp/nnet4d/final.mdl - | grep SumGroup

    nnet-am-copy --binary=false baseline/exp/nnet4d/final.mdl -

    <SumGroupComponent> <Sizes> [ 6 3 3 3 2 3 3 3 2 3 2 2 3 3 3 3 2 3 3 3 3

    3 3 4 2 1 2 3 3 3 2 2 2 3 2 2 3 3 3 3 2 4 2 3 2 3 3 3 4 2 2 3 3 2 4 3 3

    <snip>

    4 3 3 2 3 3 2 2 2 3 3 3 3 3 1 2 3 1 3 2 ]

       

    softmax成分产生比我们需要更多的后验概率(4000 instead of 1483),这些小组的后验概率求和产生输出的维度是1483,其中小组的大小范围在这里例子里是16。我们把"mixing up"和语音识别里的混合高斯模型训练中的mix up做类比,这里我们把高斯分成2份和影响均值。这种情况下,我们把最终权重矩阵的行分成2份和影响它们。这种额外的目标在训练过程中将增加一半。相关的log文件如下:

       

    cat exp/nnet4d/log/mix_up.31.log

    # Running on a11

    # Started at Sat Mar 15 15:00:23 EDT 2014

    # nnet-am-mixup --min-count=10 --num-mixtures=4000 exp/nnet4d/32.mdl exp/nnet4d/32.mdl

    nnet-am-mixup --min-count=10 --num-mixtures=4000 exp/nnet4d/32.mdl exp/nnet4d/32.mdl

    LOG (nnet-am-mixup:GiveNnetCorrectTopology():mixup-nnet.cc:46) Adding SumGroupComponent to neural net.

    LOG (nnet-am-mixup:MixUp():mixup-nnet.cc:214) Mixed up from dimension of 1483 to 4000 in the softmax layer.

    LOG (nnet-am-mixup:main():nnet-am-mixup.cc:77) Mixed up neural net from exp/nnet4d/32.mdl and wrote it to exp/nnet4d/32.mdl

    # Accounting: time=0 threads=1

    # Finished at Sat Mar 15 15:00:23 EDT 2014 with status 0

       

    Model "shrinking" and "fixing"(Optional)

       

    事实上没有在最原始例子里使用的p-norm网络上使用"Shrinking""fixing",但是在使用脚本steps/nnet2/train_tanh.sh训练的神经网络中需要使用"Shrinking""fixing",或者任何含有sigmoid激活函数的其他网络中也要使用。我们尝试解决的是发生在这些类型激活函数里的问题,这些神经元在过多的训练数据上将过饱和(也就是说,it gets outside the part of the activation that has a substantial slope)和训练将变的很慢。

       

    对于shrinking,我们来看其中的一个log文件,首先:

       

    #> cat exp/nnet4c/log/shrink.10.log

    # Running on a14

    # Started at Sat Mar 15 14:25:43 EDT 2014

    # nnet-subset-egs --n=2000 --randomize-order=true --srand=10 ark:exp/nnet4c/egs/train_diagnostic.egs ark:- |

    nnet-combine-fast --use-gpu=no --num-threads=16 --verbose=3 --minibatch-size=125 exp/nnet4c/11.mdl

    ark:- exp/nnet4c/11.mdl

    <snip>

    LOG <snip> Scale parameters are [

    0.976785 1.044 1.1043 ]

    LOG <snip> Combining nnets, objf per frame changed from -1.01129 to -1.00195

    LOG <snip> Finished combining neural nets, wrote model to exp/nnet4c/11.mdl

       

    当使用nnet-combine-fast时,但是仅仅把它作为神经网络的输入,所以我们可以优化的唯一一件事情就是神经网络在不同层数的参数的尺度。这次尺度都接近于1,和有些大于1,所以在这种情况下, shrinking也许是用词不当。那些"shrinking"是有用的情况里,但是可能在这种情况下也没有多大区别。

       

    接下来,我们看一下"fixing"log文件,当我们不做"shrinking"时,每次迭代都需要做'fixing'

       

    #> cat exp/nnet4c/log/fix.1.log

    nnet-am-fix exp/nnet4c/2.mdl exp/nnet4c/2.mdl

    LOG (nnet-am-fix:FixNnet():nnet-fix.cc:94) For layer 2, decreased parameters for 0 indexes,

    and increased them for 0 out of a total of 375

    LOG (nnet-am-fix:FixNnet():nnet-fix.cc:94) For layer 4, decreased parameters for 1 indexes,

    and increased them for 0 out of a total of 375

    LOG (nnet-am-fix:main():nnet-am-fix.cc:82) Copied neural net from exp/nnet4c/2.mdl to exp/nnet4c/2.mdl

       

    这里做的就是根据训练数据,计算tanh激活函数的导数的平均值。对于tanh来说,在任何数据点上这个导数都不能超过1.0。对于某个特定的神经元来说,如果它的平均值比这个值小很多(默认的我们设置门限值为0.1),也就是我们过饱和了和我们通过对这个输入的神经元降低2倍来补偿权重和偏移项。就像你在log文件里看到的那样,这个仅仅在这次迭代某个神经元上发生,意思也就是说对于这次特定的运行,这不是一个很大的问(如果你使用很大的学习率,它将经常发生)

    使用GPUsCPUs

       

    无论是GPU还是CPU,这个代码都可以保证平等透明的训练。注意如果你想使用GPU来运行,你必须使用GPU支持的编译,也就是说在src/下,你需要在一个含有 NVidia CUDA toolkit的机器上运行"configure""make"(也就是,在这台机器上,命令"nvcc"可以被执行)。如果kaldi是使用GPU支持的编译,神经网络训练代码是可以使用GPU来训练的。你可使用命令"ldd"来确定kaldi是否使用GPU编译的,和检查libcublas是否编译过,例如:

       

    src#> ldd nnet2bin/nnet-train-simple | grep cu

    libcublas.so.4 => /home/dpovey/libs/libcublas.so.4 (0x00007f1fa135e000)

    libcudart.so.4 => /home/dpovey/libs/libcudart.so.4 (0x00007f1fa1100000)

       

    当使用GPU来训练时,你将看到一些像 train...log的文件:

       

    LOG (nnet-train-simple:IsComputeExclusive():cu-device.cc:209) CUDA setup operating

    under Compute Exclusive Mode.

    LOG (nnet-train-simple:FinalizeActiveGpu():cu-device.cc:174) The active GPU is [0]:

    Tesla K10.G2.8GB free:3516M, used:66M, total:3583M, free/total:0.981389 version 3.0

       

    一些命令行程序有一个选项–use-gpu,这个选项可以取的值为"yes" "no"或者"optional",和意思就是你是否使用GPU(如果你设定为"optional",如果仅仅有一个GPU可用,那么将使用GPU)。 但是事实上我们在脚本里不使用这种机制,因为对于GPUCPU训练来说,我们有两种不同的代码。CPU版本的代码是nnet-train-parallel,和因为它支持多线程,所以它这么命名。当我们使用一个CPU时,我们将使用16个线程。这就是去做没有任何锁的多核随机梯度下降,有时候我们可以把它看成是Hogwild的一种形式。顺便,当使用多线程更新时,我们建议minibatch的大小不超过128,因为这个会导致不稳定性。 We consider that "effective minibatch size" as equal to the minibatch size times the number of threads, and if this gets too large the updates can diverge. Note that we have formulated the stochastic gradient descent so that the gradients get summed over the members of the minibatch, not averaged. Also note that the only reason why we can't just use nnet-train-parallel with one thread for GPU-based training is that nnet-train-parallel uses two threads even if configured with –num-threads=1 (because one thread is dedicated to I/O), and CUDA does not work easily with multi-threaded programs because the GPU context is tied to a single thread.

    GPUCPU之间切换

    当你借助像train_tanh.shtrain_pnorm.sh脚本来在使用CPUGPU之间切换时,有一些你不得不改的步骤(也许这不是理想的)。这些程序有一个选项–parallel-opts, 它是由传递到queue.pl(或者类似的脚本)里的额外的标示构成的。这里假设queue.pl是借助GridEngine和参数将传递到GridEngine –parallel-opts的默认值是使用一个16线程的CPU,和就是"-pe smp 16 -l ram_free=1G,mem_free=1G"。这仅仅影响我们从queue里获取哪些资源,和不影响真实运行的那些脚本;我们将不得不告诉脚本事实上使用的是16个线程,是通过选项–num-threads(默认的是16)。 选项"ram_free=1G" 也许全部的queue无关,为了内存使用,我们人工把它作为一个资源添加到我们的queue里;如果你的电脑没有这些资源,你可以删除它。默认的设置是使用16个线程的CPU;如果你想使用一个GPU,你就得借助脚本的选项像

       

    --num-threads 1 --parallel-opts "-l gpu=1"

       

    这里我们强调的是,选项"gpu=1"仅仅表示我们在一个特定的集群上借助GPU,和其他的集群将不同,因为一个GPU不能够认为GridEngine– queues将被使用者以不同的方式编译。 Basically the string needs to be whatever options you need to give to "qstat" so that it will request a GPU. 如果所有的都使用一个没有GridEngine的单机来运行,你仅仅使用 run.pl来运行任务,然后parallel-opts仅仅是一个空的字符串。如果你借助脚本并使用–num-threads=1,它会调用nnet-train-simple,当你使用GPUs编译时,默认地将使用一个GPU。如果 –num-threads超过1,它将调用nnet-train-parallel,而它不是使用一个GPU

    对任务数进行Tuning

       

    接下来我们描述如何在CPU训练和GPU训练中切换的一些重要点。你也许注意到一些样例脚本中(比如:对这一对脚本做对比local/nnet2/run_4c.shlocal/nnet2/run_4c_gpu.sh),选项–num-jobs-nnet的值在GPU脚本和CPU脚本中是不一样的,比如在CPU版本是8GPU版本是4。和–minibatch-size有时在这两个版本中也不同,举例来说, 在GPU中是512,在CPU中是128,学习率有时也不同。

       

    这里将解释这些不同的原因。首先,不考虑minibatch的大小。你应该知道我们的SGD中梯度的计算不是通过对minbatch平均相加的。 在我们看来,当minbatch大小改变时,就需要去最大限度地改变学习率。一般而言,矩阵相乘在大的minbatch,比如512时比较快(每个样本)。我们使用的预处理方法,就是接下来讨论的Preconditioned Stochastic GradientDescent,当使用大点的minbatch效果会好点。所以当minbatch大小是512或者1024时,训练会更快收敛。然而,这里有个限制,minbatch的大小与SGD更新的不稳定相关(和参数起伏不定和不可控)。如果minbatch太大,更新会不稳定。一旦这种不稳定性变大,它将受到选项–max-change的限制,对于每个minbatch,它会影响参数允许的改变范围,所以一般不会使得训练集的概率一直到负无穷大,但是它们也许会降得特别快。如果你在compute_prob_train.*.log里看到目标函数低于我们系统里叶子节点数目的负自然对数(一般大约为-7,你也可以在compute_prob_train.0.log看到这个值),意思就是神经网络比chance要差,这是因为不稳定性导致的一些设置。这个问题的解决方法一般是降低学习率或者minibatch大小。

       

    接下来我们讨论多线程更新时的不稳定的问题。当我们使用多线程更新时,为了稳定性这个目的,对minbatch的大小乘以线程数,所以我们让minbatch的大小低于设定的值。当我们使用CPU多线程训练时,一般minbatch的大小为128(我们应该注意到,考虑到多线程CPU更新,我们尝试做单线程训练和允许使用BLAS来实现使用多线程,但是我们发现在相同的参数上,它比单线程独立地做SGD要快)

       

    接下来,不考虑选项–num-jobs-nnet:相对于GPU的例子,CPU的例子一般使用更多的任务数(8或者16)。原因很简单,因为在运行样例时,我们不想有CPU那么多的GPUGPU训练一般比CPU20%-50%。我们感觉我们使用更少的任务来达到相同的训练时间。但是一般情况任务数是独立的,无论我们使用CPU或者GPU

       

    最后一个修改就是学习率(选项–initial-learning-rate –final-learning-rate),和这个与任务数有关(–num-jobs-nnet)。 一般而言,如果我们增加任务数,我们也应该以相同的因子来增加学习率。 因为这里的并行方法是对并行SGD跑出来的神经网络求平均得到的,我们把整个学习过程中的有效学习率等价的看成学习率除以任务数。所以当把任务数加倍时,如果我们也加倍学习率,但我们保持的有效学习率不变。但是这里有一些限制。如果学习率变高将导致不稳定,参数的更新将会发散。因此当最初的学习率变高,我们警惕它会增加很快。增加多少的依赖于我们的例子。

    对神经网络训练Tuning

    一般而言,当调整神经网络训练参数时,你应该从样例目录egs///local/nnet2/里的一些脚本开始,以某个方式来改变参数。这里假设你已经运行了train_tanh.sh或者train_pnorm.sh

    参数个数(隐层数和隐层结点大小)

    Number of parameters (hidden layers and hidden layer size)

       

    最重要的参数就是隐含层的层数(–num-hidden-layers)。 对于tanh网络来说,它一般在25之间(如果数据越多,它的值越大),和对于p-norm网络来说,它的值在23或者4之间。当修改隐含层的层数时,我们一般把隐含层节点数固定(512,或者1024,或者其他)

       

    对于tanh网络,你也可以改变隐含层的维度–hidden-layer-dim ;它是隐含层节点的数目。如果有更多的数据,这个值一般会越大,但是需要注意的是当节点数增多时,参数的数目将以平方倍增长,所以你添加更多的数据时,它的增长应该小于0.5次方的比例(比如:如果你的数据增加了10倍,你隐含层大小增加1倍将是有意义的)。我们不会用到2048或者更大的。对于一个很大的网络,我们一般使用1024个隐含层节点。

       

    对于p-norm网络来说,它没有–hidden-layer-dim参数,取代它的是其他两个参数,–pnorm-output-dim–pnorm-input-dim。它们各自的默认值为(3000, 300) output-dim必须是input-dim的一个整数倍;我们一般使用时5倍或者10倍。这个倍数会影响参数的数目;如果是大数据集,这个倍数需要变大。但是倍数的增长也要与tanh网络的隐含层大小增长一样,倍数的增大速度比数据量的增大速度要慢的多。

       

    与参数的数量相关的另一个选项是–mix-up(Optional)。它负责为每个叶子节点提供多个虚拟的目标,最后的softmax层的大小是随着决策树里的叶子节点的数目而变化(叶子节点的数目可以通过am-info函数,对神经网络训练里的输入目录里的final.mdl来得到);通常它的值为几千。参数 –mix-up一般是叶子节点的2倍左右,但是一般来说错误率对其不是很敏感。

    学习率

    另一个重要的参数就是学习率。这里有两个主要的参数:–initial-learning-rate–final-learning-rate。 它们默认的值分别为0.040.004。我们一般设定最终的学习率是最初学习率的五分之一或者十分之一。默认值0.040.004仅仅对小数据集是合适的,比如rm数据集,大约3个小时。如果数据量越大,你将训练更长时间,所以不需要这么大的学习率。 对于几百小时的数据集来说,这个学习率的十分之一也许是合适的。下面将介绍学习率和任务数之间的关系。

       

    如果不画出目标函数在时间轴上的曲线图,我们很难去判断学习率是太高还是太低。如果学习率很大,目标函数很快就收敛,但是得不到一个很好的目标函数值(就像被噪声梯度隐藏起来了)。但你可能使参数波动,将会得到一个非常差的目标函数值(如果minbatch的大小太大或者你使用多线程,这种情况将最可能发生)。如果学习率太低,目标函数将更新缓慢和将花费很长的时间到达最值。

       

    你可能不需要调整的一个学习率参数是在脚本train_tanh.sh里的一个配置值–final-learning-rate-factor,默认设置为0.5。在最后2层中,将使用给定学习率的一半(比如:softmax层和最后的隐含层的参数)。脚本train_pnorm.sh script支持一个相同的配置值 –soft-max-learning-rate-factor,它将影响最后的softmax层之前的参数,但是它的默认值为1.0

    Minibatch size

       

    另一个可调整的参数就是minibatch的大小。我们一般使用2的次方,典型的有128256或者512。一般一个大点的minbatch会更有效,因为它可以更好的与矩阵相乘代码里使用到的优化进行交互,尤其在使用GPU时,但是它如果太大(和如果学习率太大),在更新时会导致不稳定。对于基于CPU训练时,使用多线程的Hogwild!形式更新,如果minbatch的大小太大,更新将变不稳定。对于多线程的CPU训练时,minbatch的大小一般为128;对于基于GPU的训练,minbatch的大小为512。这就不需要再去调整了。我们需要注意的是,minibatch的大小跟接下来讨论的–max-change选项有关,如果minbatch越大,也就意味着–max-change越大。

    每个minibatch中模型参数的最大变化(Max-change

       

    在脚本train_tanh.shtrain_pnorm.sh里有个选项–max-change,这个值将传递给包含权重矩阵的那些成分的初始化(它们是类型AffineComponent或者AffineComponentPreconditioned)–max-change 选项限制了每个minibatch允许多少参数被修改,以l2范数来衡量,比如这个矩阵表示在任何给定的minibatch,任何给定层的参数的改变都不能超过这个值。为了做到这一点,我们使用一个临时矩阵来存储这些参数的变化。这很浪费空间,因此我们计算的变换量(actual-change)为一个minbatch中所有samplel2范数之和。如果在一个minibatch中,actual-change超过了max-change,我们就为这个minbatch的学习率乘以一个常数,以使得actual-change不超过max-change。如果指定了max-change选项,你将在train.*.log里看到如下的一些信息:

       

    LOG <snip> Limiting step size to 40 using scaling factor 0.189877, for component index 8

    LOG <snip> Limiting step size to 40 using scaling factor 0.188353, for component index 8

       

    (事实上,这个因子比正常的要小——打印出来的这些因子通常是接近这个。 也许对于这个特定的迭代,学习率太高了。–max-change是一个故障安全机制,如果学习率太高(或者minibatch大小太大),它将不会导致不稳定。–max-change可以减慢训练中学习过快的问题,特别是对最后的一层或者2层;稍后再训练过程中,这个约束将不再起作用,和在训练的结束中你不需要去看logs文件里的这些信息。这个参数将不再是重要的。如果minbatch的大小是512时,我们通常设置它为40(比如:当使用GPU),和如果minbatch的大小为128时,它为10(比如:当使用CPU)。 这个是有意义的,因为限制这个数量跟minbatch样本的数量是成正比的。

    Number of epochs, etc.

       

    训练的迭代次数这里有两个配置变量:–num-epochs (默认为15),和 –num-epochs-extra (默认为5)。 训练的迭代次数–num-epochs的准则就是学习率从–initial-learning-rate以几何级数降低到–final-learning-rate,然后保持–final-learning-rate不变迭代–num-epochs-extra次。一般情况下,改变迭代次数是没必要的,当训练集是小数据集时,我训练的迭代次数为20+5,而不是15+5。同时,如果数据量很大,并且计算环境不是很强大, 你也许通过减少训练迭代次数来节省时间。但是这样也许会轻微地降低最终性能。

       

    有时,这个也与参数–num-iters-final有关。这个决定了在训练结束时在哪个迭代之上做模型组合。我们坚信这不是一个非常重要的参数。

    Feature splicing width.

       

    这里有个选项–splice-width,默认的值为4,它将影响我们分配多少帧的特征给输入。这个会影响神经网络的初始化,和样例的生成。这个值等于4的意思是在中间帧的左右两边各加上4帧,总共9帧数据。 参数–splice-width事实上是一个相当重要的参数,但是对于正常的全处理特征来说(比如:从MFCC+splice+LDA+MLLT+fMLLR里得到的40维特征)4就是一个最佳值。注意LDA+MLLT特征就是根据中间帧的每一边都是3或者4帧,也就是表示整个有效的声学上下文的神经网络可以看到每一边都是7或者8帧。如果你使用"raw" MFCC或者log-filterbank-energy特征(看脚本get_egs.shget_lda.sh里的选项"--feat-type raw"), 你也许可以把–splice-width设置大一点,比如为5或者6

       

    许多人问我们,为什么使用超过4帧的上下文会不好?如果我们的目标是获得最好目标函数和去分每一个隔离的帧,或者你在对TIMIT数据库进行解码(没有使用语言模型),整个答案是肯定的。问题就是如果使用更多的上下文,会降低我们整个系统的性能。我们认为HMMs基于state-conditional帧独立的假设,系统很难与其交互好。总之,无论什么原因,它看起来都没有起作用。

    配置与LDA变换相关的参数

    在训练神经网络前,我们对特征使用了一个去相关变换。这个变换事实上称为神经网络的一部分—— 我们在之前固定的FixedAffineComponent类型,它是不需要训练的。我们称它为LDA变换,但是它跟传统的LDA是不一样的,因为这里我们对变换的行进行尺度拉伸。 这个部分将处理影响这个变换的配置值。这些通过选项–lda-opts ""传递到脚本get_lda.sh

       

    注意这里除了对数据进行去相关,我们还得使数据是零均值;这也许是因为输出是一个仿射变换(linear term plus bias),表示一个d*(d+1)的矩阵,而不是d*d(这里的d表示特征的维度,一般为40*9)。 默认地,这个变换是一个非降维形式的LDA,比如:我们保持整个维度。这也许听起来有点奇怪,因为LDA的作用就是降维。但是我们这里将它用来对数据去相关。

       

    在传统的LDA,大多数会这么去做,对数据进行归一化,以使得类内方差为单位矩阵,类间方差是对类与类之间的方差按从最大到最小排列的对角矩阵。所以在这个变换之后,整个方差(类内和类间)在第i个对角上的值为1.0 + b(i),这里的b(i)是与数据无关的,和随i降低的。我们修改的LDA,不是真正的LDA,采用这个变换和对每一行乘以,这里默认的 within-class-factor0.0001。这个方差的影响是这个因子的平方根,所以方差的第i个元素的影响因子默认地不是1.0 + b(i),而是 0.0001 + b(i)。基本上,我们缩减那些无信息的维度,因为我们的经验是添加无信息的数据到神经网络的输入会使性能变差,和简单地通过缩小它,可以使SGD训练在大多数情况下忽略它,这个是有用的。 我们怀疑如果对神经网络做一个简单的假设,比如它仅仅是一个逻辑斯特回归或者更简单的一个,这样的方案有时候是最佳的(也是使用0而不是0.0001)。不管怎么说,这个仅仅是一个技巧。

       

    这里的配置参数–lda-dim有时候是用来强制这个变换来降维,而不是使所有的维度都通过。当处理一个输入维度特别大,我们会常常使用它,但是也不一定是有用的。

       

    其他配置(Other miscellaneous configuration values

       

    对于脚本train_tanh.sh,有个选项–shrink-interval(默认为5),它决定我们多久对模型做一个收缩(Model "shrinking" and "fixing"),我们使用训练数据集的一小部分来优化不同层参数的拉伸。这个不是很重要的。

       

    选项–add-layers-period(默认为2)控制了在添加层之间我们需要等待的迭代次数,训练一开始我们就添加新的层。 这个可能是有差别的,但是我们一般不去调整它。

    Preconditioned Stochastic Gradient Descent

       

    这里我们不使用传统的Stochastic Gradient Descent (SGD),而使用一个特殊的预处理形式的SGD。这就意味着,不是使用一个通常的学习率,而是使用一个矩阵值的学习率。这个矩阵有一个特殊的结构,所以实际上是每个仿射成分的输入维度的一个矩阵和输出维度上的一个矩阵。如果你想把它看成一个单个大矩阵,它将是一个对角块结构的矩阵,其中每个块是两个矩阵的Kronecker (一个是输入维度和一个对应仿射成分的输出维度)。除此之外,每个minibatch中矩阵被估计。最基本的思想就是当衍生物有一个高的方差就降低学习率;这将趋去控制不稳定性和在任何一个方向上因为太快而停止参数更新。

       

    这里我们还没有足够的时间来做一个详细的总结,但是可以看源文件nnet2/nnet-precondition.h nnet2/nnet-precondition-online.h的说明,这里将描述的更详细。我们需要注意的是nnet2/nnet-precondition.h包含这个方法最原始的版本,对每一个minibatch来估计预处理矩阵。而 nnet2/nnet-precondition-online.h包含这个方法的最新版本,这里的预处理矩阵是一个特殊的低秩加单元矩阵的结构和通过在线估计;如果使用GPU来实现会更加有效,因为旧的方法依赖对称矩阵求逆,而且是在一个相当高的维度上做的(比如:512),在一个GPU上很难做到是有效的,然后就变成了一个瓶颈。

       

  • 相关阅读:
    程序员获取编程灵感的10 种方式
    修改Windows远程桌面3389端口
    修改Windows远程桌面3389端口
    JS 开发常用工具函数
    JS 开发常用工具函数
    IT公司老板落水,各部门员工怎么救
    IT公司老板落水,各部门员工怎么救
    如何优雅地给妹子优化电脑(Windows)?
    如何优雅地给妹子优化电脑(Windows)?
    程序员,你恐慌的到底是什么?
  • 原文地址:https://www.cnblogs.com/JarvanWang/p/7499583.html
Copyright © 2020-2023  润新知