• Unsupervised Pretraining Transfers well Across Languages


    利用非平行语料训练跨语种和多语种的语音识别(Automatic Speech Recognization,ASR),使用对比预测编码(Contrastive Predictive Coding,CPC)预训练语音识别系统,效果甚至超过监督学习。

    代码地址:CPC_audio

    简介

    已有较多的工作应用到无监督训练的语音识别系统中,该文主要集中研究对比预测编码在跨语种低数据资源的情况下,能否提升表征的质量。对比预测编码是在特征空间中建模的一种形式,其“预测”音频序列中相近的片段,而“对比”不同音频序列中的音频片段,或者同一音频序列相隔较远的片段。

    该文主要对原始方法引入一些修改以稳定训练,并且获得更好的音素表征。该文首先用英语数据集Librispeech训练修改之后的对比预测编码模型,然后将其迁移到Common Voice数据库中的其它语言。

    对比预测编码(Contrastive Predictive Coding,CPC)

    神经网络的无监督学习需要定义一个预训练任务,这个预训练任务引导神经网络生成有辨识度的特征。在对比预测编码中,这个预训练任务就是前向模型(forward modeling),也就是依据历史的序列,生成未来的序列。而对比预测编码的特殊性在于,其前向建模任务中的每一步都是为了获得未来的重建表征,而非未来的序列。在对比预测编码中,历史和未来表征由同一个模型生成,并且利用对比损失函数拉近时序相近的两个序列表征之间的距离,推远时序较远的两个序列表征之间的距离。

    具体地,对于语音识别系统而言,首先将一个音频序列切分为(T)个片段,之后利用一个编码器将每一个时刻(t)的输入信号编码到隐状态(phi_ heta(x_t)),最后利用一个序列模型将信号的隐状态映射为当前的音素表征(z_t)。也即:

    [z_t=psi_ ho(phi_ heta(x_1),phi_ heta(x_2),...,phi_ heta(x_t)) ]

    其中,(phi_ heta)表示编码器,(psi_ ho)表示序列模型,( ho, heta)均为参数。

    在原始的对比预测编码中,如图1中(g_{enc}),编码器由5个卷积层组成,卷积核大小分别为10、8、4、4、4,步长分别为5、4、2、2、2,序列模型为一层GRU。编码器的下采样率为160,也就是说,对于一个采样率16kHz的输入信号来说,每一个隐状态向量编码10ms的音频。

    给定音素嵌入向量(z_t),对比预测编码的预训练任务就是预测接下来的(K)个音频隐状态,也就是预测(phi_ heta(x_{t+k})),其中(kin {1,2,...,K})。对比预测编码同时推远随机选取的(N_t)个负样本隐状态,最终,在时刻(t)的损失函数为:

    [L_t=-frac{1}{K}sum_{k=1}^K mathop{log}[frac{mathop{exp}(phi_ heta(x_{t+k})^TA_kz_t)}{sum_{nin N_t}mathop{exp}(phi_ heta(n)^TA_kz_t)}], ag{1} ]

    其中,(A_k)是一个线性分类器,有很多的方法选取负样本,比如在一个说话人的语料中选取不同语句的音频,或者选取与当前时刻距离较远的音频片段,参数( heta, ho)(A_{1,...,K})通过随机梯度下降学习获得。

    修改对比预测编码(CPC)

    稳定训练

    该文作者观察到对比预测编码的训练较为不稳定,并且常常会收敛到一个较差的点,罪魁祸首是编码器神经网络层之间的批规范化层(batch normalization)。批规范化的可学习重构参数(gamma,eta)在整个批次的样本上进行计算,并且编码器在整个序列上是共享的,因此批规范化会泄漏序列未来的信息。结合公式1可知,需要将负样本尽可能推开,因此与负样本共享这部分统计信息就有可能导致训练的不稳定。解决方案很简单,就是将批规范化替换为同样可以约束内部表示的、沿通道方向的规范化,这种规范化不会在序列内共享参数。

    提升模型

    在上图1中的预测模型(Predictions),使用1层Transformer层代替线性分类器,使用所有的(z_1,z_2,...,z_t)去预测(phi(x_{t+k}));在回归模型(g_{ar})中,该文尝试使用LSTM,而非GRU可以带来性能的小幅提升。

    跨语种音素分类能力

    在该文中,为了衡量获得的无监督学习音素表征的质量,在预训练之后,冻结模型,仅针对目标语言单独训练一个线性分类器。特别地,对8个窗口的音素特征进行线性分类,然后利用CTC(Connectionist Temporal Classification)损失度量模型预测值和真实值。

    论文代码分析

    入口函数位于cpc/train.py,对比预测编码的编码器和自回归模型位于cpc/model.py,预测模型及损失函数位于cpc/criterion/criterion.py,模型默认参数位于cpc/cpc_default_config.py

    run()函数中开始训练和验证过程,其中,在trainStep()函数中训练,在valStep()函数中验证。在训练阶段,cpcModel()输入一批音频数据,输出音频经过编码且自回归模型之后的特征,音频编码后的数据,以及标签,标签是原样输入、原样输出;之后利用预测模型输出损失和准确率;最后反传。

    
    c_feature, encoded_data, label = cpcModel(batchData, label)
    allLosses, allAcc = cpcCriterion(c_feature, encoded_data, label)
    totLoss = allLosses.sum()
    
    totLoss.backward()
    
    # Show grads ?
    optimizer.step()
    optimizer.zero_grad()
    

    这里的标签是数据集中的说话人或者音素标签(该代码中,默认情况下的监督学习是说话人分类)。在cpc/dataset.py中,

    def __getitem__(self, idx):
    
        if idx < 0 or idx >= len(self.data) - self.sizeWindow - 1:
            print(idx)
    
        outData = self.data[idx:(self.sizeWindow + idx)].view(1, -1)
        label = torch.tensor(self.getSpeakerLabel(idx), dtype=torch.long)
        if self.phoneSize > 0:
            label_phone = torch.tensor(self.getPhonem(idx), dtype=torch.long)
            if not self.doubleLabels:
                label = label_phone
        else:
            label_phone = torch.zeros(1)
    
        if self.doubleLabels:
            return outData, label, label_phone
    
        return outData, label
    

    对比预测编码的模型看起来很简单,由编码器和自回归模型组成。在cpc/model.py中,

    class CPCModel(nn.Module):
    
        def __init__(self,
                     encoder,
                     AR):
    
            super(CPCModel, self).__init__()
            self.gEncoder = encoder
            self.gAR = AR
    
        def forward(self, batchData, label):
            encodedData = self.gEncoder(batchData).permute(0, 2, 1)
            cFeature = self.gAR(encodedData)
            return cFeature, encodedData, label
    

    其中,编码器和原始对比预测编码的实现一致,由5个卷积核大小分别为10、8、4、4、4的卷积层组成,只不过在该文中,为了提升模型训练的稳定性,批规范化被替换为通道规范化。提供的代码是实验代码,因此会存在多个规范化选项,默认设置和论文中的最优实验设置保持一致,使用通道规范化,下同。在cpc/model.py中,

    class CPCEncoder(nn.Module):
    
        def __init__(self,
                     sizeHidden=512,
                     normMode="layerNorm"):
    
            super(CPCEncoder, self).__init__()
    
            validModes = ["batchNorm", "instanceNorm", "ID", "layerNorm"]
            if normMode not in validModes:
                raise ValueError(f"Norm mode must be in {validModes}")
    
            if normMode == "instanceNorm":
                def normLayer(x): return nn.InstanceNorm1d(x, affine=True)
            elif normMode == "ID":
                normLayer = IDModule
            elif normMode == "layerNorm":
                normLayer = ChannelNorm
            else:
                normLayer = nn.BatchNorm1d
    
            self.dimEncoded = sizeHidden
            self.conv0 = nn.Conv1d(1, sizeHidden, 10, stride=5, padding=3)
            self.batchNorm0 = normLayer(sizeHidden)
            self.conv1 = nn.Conv1d(sizeHidden, sizeHidden, 8, stride=4, padding=2)
            self.batchNorm1 = normLayer(sizeHidden)
            self.conv2 = nn.Conv1d(sizeHidden, sizeHidden, 4,
                                   stride=2, padding=1)
            self.batchNorm2 = normLayer(sizeHidden)
            self.conv3 = nn.Conv1d(sizeHidden, sizeHidden, 4, stride=2, padding=1)
            self.batchNorm3 = normLayer(sizeHidden)
            self.conv4 = nn.Conv1d(sizeHidden, sizeHidden, 4, stride=2, padding=1)
            self.batchNorm4 = normLayer(sizeHidden)
            self.DOWNSAMPLING = 160
    
        def getDimOutput(self):
            return self.conv4.out_channels
    
        def forward(self, x):
            x = F.relu(self.batchNorm0(self.conv0(x)))
            x = F.relu(self.batchNorm1(self.conv1(x)))
            x = F.relu(self.batchNorm2(self.conv2(x)))
            x = F.relu(self.batchNorm3(self.conv3(x)))
            x = F.relu(self.batchNorm4(self.conv4(x)))
            return x
    

    具体地,在cpc/model.py中,传入通道规范化的张量大小为:([N,C_{out},L_{out}]),也就是说,对张量dim=1的维度上求平均和方差,然后训练self.weightself.bias恢复参数,具体实现为:

    class ChannelNorm(nn.Module):
    
        def __init__(self,
                     numFeatures,
                     epsilon=1e-05,
                     affine=True):
    
            super(ChannelNorm, self).__init__()
            if affine:
                self.weight = nn.parameter.Parameter(torch.Tensor(1,
                                                                  numFeatures, 1))
                self.bias = nn.parameter.Parameter(torch.Tensor(1, numFeatures, 1))
            else:
                self.weight = None
                self.bias = None
            self.epsilon = epsilon
            self.p = 0
            self.affine = affine
            self.reset_parameters()
    
        def reset_parameters(self):
            if self.affine:
                torch.nn.init.ones_(self.weight)
                torch.nn.init.zeros_(self.bias)
    
        def forward(self, x):
            cumMean = x.mean(dim=1, keepdim=True)
            cumVar = x.var(dim=1, keepdim=True)
            x = (x - cumMean)*torch.rsqrt(cumVar + self.epsilon)
    
            if self.weight is not None:
                x = x * self.weight + self.bias
            return x
    

    另一方面,相比原始的对比预测编码中使用GRU作为自回归模型,该文将自回归模型设置为LSTM。具体地,在cpc/model.py中,

    class CPCAR(nn.Module):
    
        def __init__(self,
                     dimEncoded,
                     dimOutput,
                     keepHidden,
                     nLevelsGRU,
                     mode="GRU",
                     reverse=False):
    
            super(CPCAR, self).__init__()
            self.RESIDUAL_STD = 0.1
    
            if mode == "LSTM":
                self.baseNet = nn.LSTM(dimEncoded, dimOutput,
                                       num_layers=nLevelsGRU, batch_first=True)
            elif mode == "RNN":
                self.baseNet = nn.RNN(dimEncoded, dimOutput,
                                      num_layers=nLevelsGRU, batch_first=True)
            else:
                self.baseNet = nn.GRU(dimEncoded, dimOutput,
                                      num_layers=nLevelsGRU, batch_first=True)
    
            self.hidden = None
            self.keepHidden = keepHidden
            self.reverse = reverse
    
        def getDimOutput(self):
            return self.baseNet.hidden_size
    
        def forward(self, x):
    
            if self.reverse:
                x = torch.flip(x, [1])
            try:
                self.baseNet.flatten_parameters()
            except RuntimeError:
                pass
            x, h = self.baseNet(x, self.hidden)
            if self.keepHidden:
                if isinstance(h, tuple):
                    self.hidden = tuple(x.detach() for x in h)
                else:
                    self.hidden = h.detach()
    
            # For better modularity, a sequence's order should be preserved
            # by each module
            if self.reverse:
                x = torch.flip(x, [1])
            return x
    

    在计算损失值时,使用cpc/criterion/criterion.py中的CPCUnsupervisedCriterion()

    class CPCUnsupersivedCriterion(BaseCriterion):
    
        def __init__(self,
                     nPredicts,             # Number of steps
                     dimOutputAR,           # Dimension of G_ar
                     dimOutputEncoder,      # Dimension of the convolutional net
                     negativeSamplingExt,   # Number of negative samples to draw
                     mode=None,
                     rnnMode=False,
                     dropout=False,
                     speakerEmbedding=0,
                     nSpeakers=0,
                     sizeInputSeq=128):
    
            super(CPCUnsupersivedCriterion, self).__init__()
    
            ...
    
            self.wPrediction = PredictionNetwork(
                nPredicts, dimOutputAR, dimOutputEncoder, rnnMode=rnnMode,
                dropout=dropout, sizeInputSeq=sizeInputSeq - nPredicts)
            self.nPredicts = nPredicts
            self.negativeSamplingExt = negativeSamplingExt
            self.lossCriterion = nn.CrossEntropyLoss()
    
            ...
    
        def sampleClean(self, encodedData, windowSize):
    
            ...
    
            for k in range(1, self.nPredicts + 1):
    
                # Positive samples
                if k < self.nPredicts:
                    posSeq = encodedData[:, k:-(self.nPredicts-k)]
                else:
                    posSeq = encodedData[:, k:]
    
                posSeq = posSeq.view(batchSize, 1, posSeq.size(1), dimEncoded)
                fullSeq = torch.cat((posSeq, negExt), dim=1)
                outputs.append(fullSeq)
    
            return outputs, labelLoss
    
        ...
    
        def forward(self, cFeature, encodedData, label):
            
            ...
    
            predictions = self.wPrediction(cFeature, sampledData)
    
            outLosses = [0 for x in range(self.nPredicts)]
            outAcc = [0 for x in range(self.nPredicts)]
    
            for k, locPreds in enumerate(predictions[:self.nPredicts]):
                locPreds = locPreds.permute(0, 2, 1)
                locPreds = locPreds.contiguous().view(-1, locPreds.size(2))
                lossK = self.lossCriterion(locPreds, labelLoss)
                outLosses[k] += lossK.view(1, -1)
                _, predsIndex = locPreds.max(1)
                outAcc[k] += torch.sum(predsIndex == labelLoss).float().view(1, -1)
    
            return torch.cat(outLosses, dim=1), 
                torch.cat(outAcc, dim=1) / (windowSize * batchSize)
    

    其中,代码中的PredictionNetwork()即为生成模型,相比于原始的对比预测编码,该文为了提升模型能力,将线性分类层修改为一层Transformer层。在cpc/criterion/criterion.py中,

    class PredictionNetwork(nn.Module):
    
        def __init__(self,
                     nPredicts,
                     dimOutputAR,
                     dimOutputEncoder,
                     rnnMode=None,
                     dropout=False,
                     sizeInputSeq=116):
    
            super(PredictionNetwork, self).__init__()
            self.predictors = nn.ModuleList()
            self.RESIDUAL_STD = 0.01
            self.dimOutputAR = dimOutputAR
    
            self.dropout = nn.Dropout(p=0.5) if dropout else None
            for i in range(nPredicts):
                if rnnMode == 'RNN':
                    self.predictors.append(
                        nn.RNN(dimOutputAR, dimOutputEncoder))
                    self.predictors[-1].flatten_parameters()
                elif rnnMode == 'LSTM':
                    self.predictors.append(
                        nn.LSTM(dimOutputAR, dimOutputEncoder, batch_first=True))
                    self.predictors[-1].flatten_parameters()
                elif rnnMode == 'ffd':
                    self.predictors.append(
                        FFNetwork(dimOutputAR, dimOutputEncoder,
                                  dimOutputEncoder, 0))
                elif rnnMode == 'conv4':
                    self.predictors.append(
                        ShiftedConv(dimOutputAR, dimOutputEncoder, 4))
                elif rnnMode == 'conv8':
                    self.predictors.append(
                        ShiftedConv(dimOutputAR, dimOutputEncoder, 8))
                elif rnnMode == 'conv12':
                    self.predictors.append(
                        ShiftedConv(dimOutputAR, dimOutputEncoder, 12))
                elif rnnMode == 'transformer':
                    from transformers import buildTransformerAR
                    self.predictors.append(
                        buildTransformerAR(dimOutputEncoder,
                                           1,
                                           sizeInputSeq,
                                           False))
                else:
                    self.predictors.append(
                        nn.Linear(dimOutputAR, dimOutputEncoder, bias=False))
                    if dimOutputEncoder > dimOutputAR:
                        residual = dimOutputEncoder - dimOutputAR
                        self.predictors[-1].weight.data.copy_(torch.cat([torch.randn(
                            dimOutputAR, dimOutputAR), self.RESIDUAL_STD * torch.randn(residual, dimOutputAR)], dim=0))
    
        def forward(self, c, candidates):
    
            ...
    
  • 相关阅读:
    DroidParts 中文系列教程(基于官方教程)
    IDEA添加其他项目为库文件的方法
    IDEA 部署项目的时候出错:Jar not loaded错误
    解决IDEA导入Myclipse项目的时候没有识别为Web项目的问题
    IDEA中安装及配置SVN
    VirtualBox下设置 XP虚拟机桥接模式
    主机上设置共享文件夹供虚拟机访问
    JS的splice()方法在for循环中使用可能会遇到的坑
    Eclipse优化
    State Design Pattern
  • 原文地址:https://www.cnblogs.com/mengnan/p/13770112.html
Copyright © 2020-2023  润新知