深度学习语音预测口型

作者:u010751535

代码演示LV3

前两节所描述的代码演示LV1代码演示LV2是都是为了分析方便而选择的浅层任务,深层学习对其并不具备什么优势,其他机器学习算法恐怕要比深层学习做的更好。

真正可以发挥深层学习的是具有特定结构的任务。

这一节有两个目的
1. 换一个高度非线性和复杂的任务来再次体会一下深层学习的功能
2. 熟悉深层学习训练的完整流程。

代码演示LV3用于构建网络的代码和代码演示LV2的完全相同相同,但参数等有所改变。全部代码在github上。所用到的数据输入输出数据分别是 X.npyY.npy

说明:上图中打条的内容表示本次网络所用到的(不做说明),带有数字的内容会被逐一讲解。
重要:看下面内容之前,请先回顾上一章梯度下降训练法中的基本流程图。

任务描述:

  • 目标:用声音来预测口腔移动。
  • 维度:两者都是实数域,输入是39维,输出是24维:inputR39labelR24
  • 任务类型:因为预测的数值全部都是连续的实数,所以是回归(regression)任务。
  • 样本数:39473
  • 输出层激活函数:linear(无)
  • 损失函数:预测数值和真实数值的差异可用均方差(mean square error:MSE)来表示。

    • 表达式L(f(xi),ai)=12424j=1(f(xi)jaij)2
      其中,j表示维度的角标,由于任务是24维,这里直接用24表示,i表示不同样本的角标。f(xi)表示经过神经网络计算后的预测值。
  • 初始预览:下面是未训练前真实值与预测值在四个不同维度的对比图。目的就是要给网络很多组(输入,输出)数据来更新网络权重,使网络输出尽可能符合真实值的预测。完美情况就是蓝线(真实值)和绿线(预测值)重合。下图中,横轴是时间,纵轴是数值。

  • 效果预览:下图是经过若干次训练后的预测值变化图。

1. 预处理——平缓变体

  • 目的:机器学习的最大困难之一就在于变体。神经网络解决该问题可通过增加隐藏层节点来增强模型拟合变体的能力,然而还有一种相对方式是减弱输入和输出空间的变体。预处理就是该种可以通过缩小输入输出数据的差异性,使得建模变得相对简单的方式之一。
  • 效果:下图是将每个样本都减去平均值、除以标准差后的变化(其中上图是变化后,下图是变化前)。
    :点击original或standardization会只显示一个轨迹线,可以看到两条轨迹线的形状相同,但值域不同(若显示有问题,请点下面选项栏的autoscale图标)
  • 代码

    def Standardize(seq):
    
    #subtract mean
    
    centerized=seq-np.mean(seq, axis = 0)
    
    #divide standard deviation
    
    normalized=centerized/np.std(centerized, axis = 0)
    return normalized
    • 解释:除此之外还有其他的预处理方式。比如之前的激活函数大多用tanh,需要将输出值y限制到[-1,1]的值域中。但该节注重的预处理是消除各个样本的差异性,其中去均值和模值的方法最简单有效。

    :该方式的仅仅是缩小了尺度,并不会把值域限制到固定的范围内。

2. 预处理——窗口化

  • 目的:如果用每个时刻的输入直接预测对应时刻的输出,由于判断依据较少,结果会不理想。就好比你画我猜,开始的几笔画出的信息在观察者的脑中会产生各种联想,这些联想所形成的集合是无比巨大的,观察者难以确定究竟哪一个才正确。但随着画者展开更多内容。联想范围会逐渐被缩减。用上下文窗(context window)就有这种功效,它提供了更多的判断依据。

  • 效果图:将相邻的时序向量并接在一起形成一个“大”向量作为输入,而输出一般选择中间那一个向量对应的label,为得是有上文和下文的共同限制。这里的每个向量也叫作帧(frame)。而没有上下文的部分可由0来填补(zero padding),填零的部分会失去该维度的信息,但并不会对其他部分的计算产生误差。下图中的上半部分是用上下文窗之前的序列。而下半部分是用size为3(3个frames)的window形成的新输入序列。t的维度是39维的话,下半部分的序列就是3*39维度,总个数不变。

  • 代码

    def Makewindows(indata,window_size=41):
    outdata=[]
    mid=int(window_size/2)
    indata=np.vstack((np.zeros((mid,indata.shape[1])),indata,np.zeros((mid,indata.shape[1]))))
    
    # 前后补零
    
    for i in range(indata.shape[0]-window_size+1):
    outdata.append(np.hstack(indata[i:i+window_size]))
    return np.array(outdata)
    • 解释:窗口化并不是一种非常好的处理时序信号的方式,在随后的递归神经网络中会提到它的弊端以及递归神经网络的解决方式。

3. 权重初始化

  • 目的:权重初始化决定了网络从什么位置开始训练。但请不要认为“网络从任何位置开始都可以训练到相同结果,仅是耗时差别”。良好的起始位置不仅可以减少训练耗时,也可以使模型的训练更加稳定,并且可以避开很多训练上的问题
  • 效果图:这里展示一下初始化对于“dying ReLU”问题的缓解。由于ReLU的梯度简单,可以避开像sigmoid/tanh这类损失函数所造成的严重梯度消失(gradient vanishing),ReLU基本上成为深层学习中激活函数的标配。但在训练过程中,ReLU也会使很多节点再也无法被激活(“dying ReLU”)。下图展示了该问题的严重。

    :梯度消失简单说就是距离输出越远的层的梯度会越接近0,当用链式规则计算梯度时,就会使整体的梯度都拉向0,造成更新的权重并不是想要的正确数值。

如果用代码演示LV2中的随机权重初始化的话,是无法训练该任务的。因为随着训练,原本随机初始的预测值全部都变成了0,而后的任何数据都无法再对模型的权重更新起到作用。解决的方式之一就是合理的权重初始化
* 代码:只需要在LV2的权重初始化的基础上将所有值都除以输出维度的开方,使权重的初始值域落在[1/n,1/n]n论文)。

def weight_init(self,shape):
# shape : list [in_dim, out_dim]
# 在这里更改初始化方法
# 方式1:下面的权重初始化dropout率40%的网络,可以使用带有6个隐藏层的神经网络。
# 若过深,则使用dropout会难以拟合。
#initial = tf.truncated_normal(shape, stddev=0.1)/ np.sqrt(shape[1])
# 方式2:下面的权重初始化dropout率40%的网络,可以扩展到12个隐藏层以上(通常不会用那么多)
initial = tf.random_uniform(shape,minval=-np.sqrt(5)*np.sqrt(1.0/shape[0]), maxval=np.sqrt(5)*np.sqrt(1.0/shape[0]))
return tf.Variable(initial)
  • 解释:除此之外的解决“dying ReLU”的方式还有“Leaky ReLU”。

过拟合问题

  • 比喻:学习要达到的效果可以用高考时的做题来比喻:我们希望通过“做题、对照答案”来学会如何解该问题所有类型的题目,最终希望可在高考时解出问题。“练习题”就是机器学习中的数据(data),同时具有问题(observation)和答案(label)两种数据。不断“做题、对照答案”的过程就是学习(training)。而高考的题就是只有问题(observation),需要靠以前的学习的知识(function)来解出答案,从而考察(test)我们是否“真”的学会了该知识(function)。但是不同题有不同的思路(variation),一直做同类练习题会使自己过分纠结于该类题的细节规律(overfitting),再次遇到其他类型问题时却无法解出。
    机器学习也存在相同的现象:模型会过分学习训练数据(training set)中的规律,而在预测未见过的数据(testing set)时表现不佳。这种现象叫做过拟合(overfitting),而我们真正希望的是模型可以通过有限数据的学习涵盖所有变体,这种能力叫做普遍化(generalization)。深层学习相比浅层学习的优势就是在于其普遍化能力。然而普遍化是一个永远的课题,对人脑的学习来说同样如此。
  • 举例:为了感受普遍化是一件多么难的问题,请考虑这样一个例子。这里有一组数据。括号前一个数表示x,后一个数表示对应的y。(-1,1),(2,0),仅有两个数据时可以观察出什么规律?
    单靠观察,该规律可以是正数就输出0,负数就输出1。然而也可以是奇数就输出1,偶数就输出0。或者两者同时满足。事实上运用各种数学运算,可以有无数种function能够拟合上面两个数据,所有机器学习算法都可以做到,然而机器怎么知道我们想要哪种function?虽然可以通过更多的数据来确定想要的function。但是数据是无限的,如果我们能够获得所有情况的数据也就没有必要学习了。
  • 说明: 这里引用Bengio大神的一句话。
    If you don’t assume much about the world, it’s actually impossible to learn about it.
    如果将所有符合训练集规律的functions组成一个集合叫F,那么想要用更少的数据来学习具有普遍性的function,就需要加入先验知识来缩小F。不过,缩小F虽可排除掉一部分的functions,加快搜索,但代价也是无法学习被排除掉了的functions,这也就是No free lunch theorem所描述的内容。深层学习对于数据的assume是并行与迭代,进而排除掉了很多functions。而卷积神经网络、递归神经网络等变体就是优先搜索特定的functions,从而排除掉另一部分的functions。换句话说:
    神经网络的变体实际上是对函数优先搜索空间F的调整。
    防止过拟合也是对F的调整。但“过拟合”是对该问题的一个较为偷懒的描述。因为我们想要知道的是造成过拟合的原因,从而加以调整。

4. 防过拟合——L2 regularization

  • 作用:L2对于神经网络有两个作用。其中一个作用在之前的章节中深层神经网络——方式3:加入隐藏层中已描述。具有强制让所有节点均摊任务的作用。而另一个作用举例来说:当有两个节点的网络需要输出3时,可以让其中一个是300,另一个是-297;也可以让其中一个是1.5,另一个是1.5,L2会鼓励网络选择后者。
    :我们是利用L2的特点对网络的权重在训练中进行调整,仅仅是缓解作用,并不能完全解决问题。其次附加额外loss的做法都会多少对最终结果产生负面影响。比如过高的L2强度会压制目标loss。

  • 一般做法:我们会选择稍微过拟合的网络,再增加防止过拟合的方法加以训练,其目的是:用超过学习训练集所需的节点来建模,从而涵盖测试集中的变体。

  • 效果图:下图是用playground训练的效果演示。上半部分是没用L2,下半部分用了L2。

    1.先观察每层的连线(权重)的深浅,越浅绝对值越低。用L2的模型的权重十分均匀且数值很浅。
    2.再看每个节点的方块,没用L2的网络就出现了300-297去获得3的方式,而用了L2的则倾向于用更简单的1.5+1.5去获得3。
    3.最后再看实际分类图的差别。

    下面这张图是训练声音预测口腔移动任务50个epoch时,loss值随训练的变化曲线。x轴是训练次数(iteration),y轴是loss值。红线是测试集loss,蓝线是训练集loss。可以明显看到两条变化线的差别。测试集的loss最终停在了0.387上,而训练集的loss近乎为0。
    注:一次epoch是指将所有样本数都训练完。一次iteration是指更新一次权重。

    而下面这张图则是使用了L2后的效果。这是测试集的loss在50个epoch后,停在了0.368上。(忽略图中的训练时间,因为用的是不同的GPU)

5. 防过拟合——dropout

  • 作用:拿语音为例,我们最终获得的语音信号并不纯粹(pure),而是有很多带有“额外规律”的杂讯(noise)掺杂其中,如录音设备的噪音,说话人的规律,环境噪音的规律。
    dropout论文对其效果的解释是:dropout实际上是多个模型预测的平均值。但为什么平均多个模型的预测值会提高表现?
    个人理解:dropout可阻碍网络学习仅存在于训练集中局部的“额外规律”。

  • 现象:目标规律(想学习的规律)基本在所有训练样本中都存在,但杂讯规律(由杂讯带来的额外规律)仅存在于部分的样本中,由于权重是随机初始,并且训练样本也是随机打乱顺序后逐个送入网络进行训练。当若干有相同杂讯规律的训练样本连续被送入网络训练后,网络就会记住该规律。

  • 影响:神经网络一大特点在于它可以拆分因素,包括杂讯,并将他们分开建模。但上面的现象会影响神经网络对于因素的拆分。假如目标信号是10,而杂讯是2,获得的实际语音数据是12,混合信号未必会被拆分成10+2的形式,有可能是9+3。本该被拆分成10的目标信号变成了9,以至于在测试集中表现不佳。

  • 缓解:但是这种现象有一个特点,就是杂讯规律相对目标规律而言较为薄弱,同时也并非在所有训练样本中都存在。dropout正是对这一特点展开的攻击。dropout会随机扔掉节点,让已经学到的规律被遗忘掉。这样即便是网络由于随机打乱顺序所训练的几个带有杂讯规律的样本,所形成的杂讯规律也会被遗忘掉。只有像目标规律稳固的规律才会得意保留。由于这种现象是随机性造成的,所以平均各个模型的预测值后就会抵消掉额外规律对目标规律的干扰(比如此次拆分成9+3,下次拆分成11+1的形式。取平均值后9+11=10)。有一种筛子的作用。我想人类的遗忘也有类似作用。

    dropout可阻碍网络学习仅存在于训练集中局部的“额外规律”。

  • 效果图:dropout的实现方法和特点在代码演示LV2——训练前-step 5中已描述。下图是用了dropout(随机扔掉30%节点)后的loss图。dropout的实现非常简单,但却异常强大。test的loss最终落在了0.344。
    从图中还可以看出来dropout确实是阻碍网络学习训练集中的规律,并且不仅仅是额外规律,连目标规律也被殃及(没用dropout前,50个epoch后的训练集的loss仅有0.01~0.02左右,用了以后连训练集loss都变成了0.172)。但其实随着训练,训练集的loss还会进一步下降一些。最终我们想要提升的是测试集的表现。
    浅蓝色的线是实际的loss。dropout会使其上下波动很大。可以感受到,若规律不是在所有样本中都存在,则很难在这种dropout的遗忘下存留。


    :还有一种阻碍网络学习仅存在于训练集中的“额外规律”的方法就是在训练集中加入其他杂讯,从而屏蔽掉额外规律。
    :如果你的代码是每一层之后都加入dropoout layer,那么随着层数的加深,阻碍网络学习规律的强度也会增加。

6. 随机打乱

随机打乱(shuffle)训练数据送入网络更新的顺序十分重要。如dropout中所描述的现象。若更新网络的顺序始终不变,就会使网络受杂讯规律的干扰而拆分出错误的目标规律。若每个epoch对网络的作用也是几乎相同的,当该epoch的规律学习完毕后,网络几乎不再变化(如果看到自己训练的网络效果不好,一定要记得检查是否有shuffle)。正确的做法是:

每个epoch之后一定要将训练数据随机打乱顺序。

7. Batch size

梯度下降训练法中基本流程图-训练网络所说,Stochastic Gradient Descent (SGD) 有“跳出”局部最小值(鞍点)的作用。纯粹的SGD是每次只计算一个样本的梯度来更新权重。实际应用中,一般会用10-512个样本(batch)计算出他们的梯度平均值来更新权重。

大的Batch size

  • 优点:有节约训练时间、收敛方向稳定的优点。
  • 缺点:普遍化的程度降低、容易陷入鞍点、没有完全利用数据。

小的Batch size

  • 优点:普遍化的程度升高、利于逃脱鞍点、更全面的利用数据。
  • 缺点:训练时间提升、收敛方向波动。

8. 停训标准

很多情况下随着模型的训练,训练集的loss会越来越好,然而测试集的loss反而变得糟糕。早停(early stopping)的思路就是在变糟之前停止训练。比如当测试集的loss停止降低3次以后就可以让网络自动停止。

在正常分set的时候并不会单一的分成训练、测试集。而是分成train/test/validation 三部分。原因在于test往往是指从未见过的数据。而我们在调试网络时会从train set中另分一部分validation进行反馈和调试。

完整代码

完整代码可到github上查看,为节省篇幅,这里不再粘贴。网络模型的FNN类和代码演示LV2的完全一致,除了改变了权重初始化的值域。
值得说明的是数据的处理:

mfc=np.load('X.npy')
art=np.load('Y.npy')
x=[]
y=[]
for i in range(len(mfc)):
x.append(Makewindows(Standardize(mfc[i])))# 输入数据标准化并窗口化
y.append(Standardize(art[i]))# 输出数据标准化
vali_size=20 # 分多少百分比的数据作为验证集
totalsamples=len(np.vstack(x))
X_train=np.vstack(x)[int(totalsamples/vali_size):].astype("float32")#记得要改变类型。
Y_train=np.vstack(y)[int(totalsamples/vali_size):].astype("float32")

X_test=np.vstack(x)[:int(totalsamples/vali_size)].astype("float32")
Y_test=np.vstack(y)[:int(totalsamples/vali_size)].astype("float32")

print(X_train.shape,Y_train.shape,X_test.shape,Y_test.shape)#查看样本数和维度是否正确。
# 实际显示为:
((37500, 1599), (37500, 24), (1973, 1599), (1973, 24))

下面的函数是一个简单显示预测效果的plot图。仅此而已。

def plots(T,P,i, n=21,length=400):
m=0
plt.figure(figsize=(20,16))
plt.subplot(411)
plt.plot(T[m:m+length,7],'--')
plt.plot(P[m:m+length,7])

plt.subplot(412)
plt.plot(T[m:m+length,8],'--')
plt.plot(P[m:m+length,8])
plt.subplot(413)
plt.plot(T[m:m+length,15],'--')
plt.plot(P[m:m+length,15])
plt.subplot(414)
plt.plot(T[m:m+length,16],'--')
plt.plot(P[m:m+length,16])
plt.legend(['True','Predicted'])
plt.savefig('epoch'+str(i)+'.png')
plt.close()

实验参数

:上述参数不是最优参数。为了节省时间而随意设定的。

:上述代码也是为了让读者有体会而刻意编写的。如果熟悉流程,请放心用如Keras、tensorlayer这样的库包,可以帮助节省很多编写和省去你自己测试和找bug的时间。

  • 网络结构图

  • 训练/验证 loss:训练了200个epoch后的loss图,最后的验证loss达到了0.315左右。这次的图像并没有那么波动的原因是选择了256的batch size。不过feedforward做时序信号预测也只能到达这种程度了。随后会用相同任务和递归神经网络进行比较。
发表评论

0个评论

我要留言×

技术领域:

我要留言×

留言成功,我们将在审核后加至投票列表中!

提示x

机器翻译知识库已成功保存至我的图谱现在你可以用它来管理自己的知识内容了

删除图谱提示×

你保存在该图谱下的知识内容也会被删除,建议你先将内容移到其他图谱中。你确定要删除知识图谱及其内容吗?

删除节点提示×

无法删除该知识节点,因该节点下仍保存有相关知识内容!

删除节点提示×

你确定要删除该知识节点吗?