ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

wav2vec 2.0深度解析:语音自监督预训练的分水岭

wav2vec 2.0深度解析:语音自监督预训练的分水岭 如果要在语音自监督预训练这条技术路线里选一个真正的分水岭我会把票投给wav2vec 2.0。2020年这篇工作刚出现的时候整个语音社区被一个结果震到了只用10分钟标注语音做微调就能把识别词错误率做到传统方法需要几百小时标注才能达到的水平。前两篇我们聊了自监督预训练的基本设定也拆过wav2vec 1.0的连续表示预测思路这一篇把wav2vec 2.0从头到尾过一遍——三个核心组件怎么分工、量化器为什么不能省、对比损失和多样性损失在唱哪出戏以及完整数据流到底怎么流。想复现或者准备拿它解决低资源语音识别问题的朋友可以直接跳到最后工程细节那段但我还是建议从头看因为很多坑源头上就来自你对架构某个设计动机没理解透。1. 为什么wav2vec 2.0之前的语音自监督预训练都差点意思在聊wav2vec 2.0之前得先把背景补齐。语音和文本最大的不同在于文本本身有天然离散的token词、子词语音是一段连续波形没有谁告诉你哪里是音素的边界、这个音素应该编码成哪个符号。所以语音自监督预训练从第一天起就面临一个NLP不会遇到的问题模型连最基本的预测单元都没有。早期的wav2vec后面为了区分叫wav2vec 1.0走的是连续表示预测路线用一个卷积编码器把原始波形变成隐表示再用一个上下文网络去预测未来若干步的表示。思路和语言模型很像差别在于它预测的不是离散token而是连续的向量。问题也就出在这里连续向量回归任务太容易退化模型只要抓住低频成分、能量变化这些粗粒度信息就能把loss降下来根本不需要真正理解音素层面的结构。所以wav2vec 1.0能提供一定帮助但离“预训练大杀器”还差得远。之后vq-wav2vec补上了离散化这一步用vector quantization把连续表示变成离散码本token再在这些token上做类似语言模型的预测。这算是一个方向正确的尝试量化也确实给预训练目标加了一层信息瓶颈逼着模型做类别判断。但vq-wav2vec的问题是量化器通常不是和上下文网络联合训练的或者聚类、离散化目标和下游任务割裂整个流程有点拧巴。更重要的是它的上下文网络用的是卷积窗口有限长程依赖真的建模不了——语音里的句法、语义、说话风格这些信息往往需要跨很多帧才能感知到。与此同时NLP那边的BERT已经靠着masked language modeling和Transformer自注意力把预训练做到了一个新高度。这种做法搬到语音上的核心障碍只有一个语音没有tokenizer。如果你能自己造一个可学习的tokenizer让模型一边学习如何把语音切成离散单元一边学习根据上下文预测这些单元那不就等于把BERT的玩法完整迁移到语音了吗wav2vec 2.0做的正是这件事。它把量化器、CNN编码器、Transformer上下文网络放进同一个计算图端到端训练。所以你现在看到的wav2vec 2.0本质上干了两件事第一发明了一个可训练的语音tokenizer第二在token序列上做masked prediction。前者是工具后者是目标两者在同一个loss下联合优化。这一个改动把语音自监督预训练从“能用但提升有限”推到了“少量标注也能达到实用水平”。2. 架构逐块拆解CNN编码器、量化器和Transformer到底在干什么整体来看wav2vec 2.0的forward流程不复杂原始波形先进CNN编码器得到一帧一帧的隐表示zz复制成两份一份被mask后送进Transformer学习上下文表示c另一份送进量化器变成离散的token向量q最后在mask位置计算c和q的对比损失。下面把每一块单独拆开。2.1 CNN编码器把连续波形转成语音的像素级表示这个CNN编码器论文里叫feature encoder由7层因果卷积组成卷积核大小是[10, 8, 4, 4, 4, 4, 2]stride是[5, 4, 2, 2, 2, 2, 2]每层512个通道每层后面跟LayerNorm和GELU。把它看作一个“波形下采样器”就好输入一段16kHz的原始波形输出一串维度512的特征向量。算一下总步长5×4×2×2×2×2×2320对应16kHz下大约20ms一帧。也就是说10秒的音频会产生大约500个z向量。为什么用因果卷积因为因果卷积保证某个位置的特征只依赖它之前的波形不会看到未来信息。这在预训练任务里很重要——我们的目标本身就是“用上下文预测被mask的内容”如果编码器提前看到了未来任务就失去意义了。每一帧z相当于一个20ms窗口内声学信息的压缩你可以把它粗浅理解为“语音的像素级表示”它有足够的声学细节但还没有任何语义组织。真正把语义组织起来的工作交给后面的Transformer。这里有个容易忽略的点wav2vec 2.0完全不依赖MFCC或Fbank这类手工特征输入就是原始波形。好处是模型自己决定什么声学特征重要迁移到不同采样率、不同噪声环境时更灵活。坏处是训练更吃数据和算力前几层卷积如果没学好后面模型整体都会受影响。2.2 量化模块语音的tokenizer是怎么被训练出来的量化模块做的事情一句话就能说清把连续的z变成离散token的表示q。但具体实现有几个值得仔细看的点。论文用的是乘积量化product quantization不是单一码本。具体做法是把z拆成G组论文中G2每组分别从一个包含V个entry的码本里挑一个entry出来最后把两个entry拼接起来得到最终量化表示q。这个设计的精妙之处在于2组码本、每组320个entry组合起来的理论容量是320×320超过10万种可能但实际要存储的参数只有2×320×每组entry维度。更直观的理解是它用两套字符表拼出巨大的组合空间不需要单独开一个超大字典。这样既维持了足够大的词表又把参数量和一个entry被更新的机会控制住了。挑选entry的方式有两种Gumbel softmax和straight-through直通估计器。训练时通常用Gumbel softmax先算每个entry和输入z的logit加上Gumbel噪声再过一个带温度的softmax得到一个近似one-hot的软分布。温度初始设为2训练中逐渐退火到0.5——高温阶段选择更随机鼓励模型探索低温阶段逐渐逼近确定性选择。训练结束后真正使用时一般换成straight-through也就是argmax挑一个entry但在反向传播时把梯度直接直通回z。这么做是为了省掉Gumbel噪声带来的随机性让量化选择更稳定。为什么不能像NLP一样用现成的BPE或SentencePiece做tokenizer因为语音没有文本那样的标注信息你不知道什么长度的片段算一个音素单元。与其手工定义不如让模型在解决预测任务的过程中自己学会怎么切分、怎么离散化。量化器本质上就是一个被顺便训练出来的tokenizer它的好坏直接决定自监督目标的质量。2.3 Transformer上下文网络为什么非它不可拿到了z之后模型需要把被mask位置的真实内容猜出来这需要一个强大的上下文建模网络。wav2vec 2.0用的是Transformer编码器base配置12层、隐层维度768、8个注意力头large配置24层、1024维、16个头。最值得展开的是它没有使用绝对位置编码而是用了相对位置编码。原因有两层第一语音帧的绝对位置信息本身不是关键——你不关心“这是第367帧”更关心“它和前后的帧是什么关系”第二也是更致命的如果用了绝对位置编码模型在mask任务里很容易作弊它只要记住“位置p的embedding被换成了mask embedding”这个规律就能快速定位所有被mask的位置根本不需要真正理解上下文内容。相对位置编码只告诉模型“第i帧和第j帧之间的距离”不暴露绝对坐标逼着模型靠内容而不是靠位置线索去预测。这里多说一句mask的位置在输入给Transformer之前会被替换成一个可学习的mask embedding向量。Transformer自注意力能把序列里每个位置的表示和所有其他位置交互长程依赖能力比卷积强太多。这也是wav2vec 2.0相比1.0一个重要的升级点。三个组件的关系简单总结CNN提取局部声学特征量化器提供离散预测目标Transformer负责长程上下文建模。三者没有一个是optional的。CNN太浅z的粒度太粗或太细都不利于量化量化目标质量差Transformer再强也无从学起Transformer容量不够学了也白学。3. 损失函数为什么是对比多样性L2三件套训练目标这部分是整个wav2vec 2.0原理最容易被“看一眼公式就走”的地方。但如果不把每个loss项背后的动机搞懂后面调参、复现、迁移都会像盲人摸象。3.1 对比损失核心任务就是在这堆东西里找到正确那个对于每个被mask的位置tTransformer输出一个上下文表示c_t。我们需要c_t能够“认出”这个位置对应的真实量化表示q_t。注意不是让c_t直接回归到q_t的连续数值而是做对比把q_t当正样本同时从batch内随机抽K个其他位置的量化表示当负样本让c_t在K1个候选中选出正确正样本。默认K100。相似度用的是cosine经过温度τ0.1缩放后再算softmax然后取负对数。温度0.1让相似度分布变得很尖锐模型哪怕对正样本只高了一点点概率也会被放大反过来如果它分不清正负样本loss会非常大。这种“尖锐化”迫使模型把上下文表示学到足够有区分度。为什么用对比学习而不是把所有量化组合当成一个超大多分类去做交叉熵关键原因是码本组合空间太大前面说了10万种可能而且我们并没有一个静态的“标准答案列表”负样本是动态采样的量化表示也在训练中不断变化。对比学习天然适合这种动态目标它不关心“这个token具体叫什么”只关心“正样本和负样本能不能区分开”。还有一个工程上的好处负样本直接从batch内部采样不需要额外维护一个大矩阵训练效率高。3.2 多样性损失防止码本变成僵尸字典如果只有对比损失量化器会出现一个典型问题塌缩。模型很容易找到一个省事策略——把所有输入都映射到少数几个entry上。反正只要这几个entry能勉强区分正负样本对比loss就能降。但这样码本里绝大多数entry根本没被使用量化token的信息量极低自监督目标也失去意义。多样性损失就是针对这个问题的。它要求每个码本组里的entry被选用的概率分布尽量接近均匀分布。实现上通常是计算Gumbel softmax输出的概率在batch内平均后与均匀分布的KL散度或者直接最大化熵。这个约束能保证不同语音特征会倾向于落到不同entry上码本才能真正作为一个信息瓶颈——它逼着模型把“什么样的声学模式”和“什么样的离散单元”对应起来。这个loss的重要性我多说两句。我见过不少训练崩掉的case日志里对比loss一直在降但微调后WER完全没改善查到最后几乎都是多样性损失没起来码本利用率只有百分之几。所以训练中监控每个entry的使用频率是一项日常操作而不是可选的锦上添花。3.3 总损失怎么配比权重和温度是拍脑袋定的吗总损失由三部分组成损失项默认权重作用对比损失 Lm1让上下文表示能选中正确量化目标多样性损失 Ld0.1让码本entry被均匀使用L2正则 ||z||²0.1约束encoder输出向量的范数L2正则这一项经常被忽视但它其实很关键。encoder输出的z没有加约束的话量化器输入和Transformer输入的尺度可能变得很大导致Gumbel softmax的logit和对比相似度的数值不稳定。加一个弱的L2惩罚等于给z套了一个“绳套”让模型把信息放在方向上而不是向量长度上这也是LayerNorm替代不了的。Gumbel softmax的温度设置是另一个需要留意的点。论文说从2退火到0.5fairseq里的实现通常是通过每步乘一个接近1的衰减系数来实现。温度太高量化选择接近于均匀随机模型学不到精确的离散单元温度太低梯度退化量化器难以探索。我在实际训练里的体会是前10%的步数尽量维持相对较高的温度让码本先均匀“铺开”后面再开始集中否则容易过早收敛到局部最优。4. 从一段原始波形到loss值完整的前向数据流前面三章把组件和loss都讲了这一章把整个流程串一遍。理解这一章之后你在看fairseq源码时就能对得上每个变量到底在干什么。4.1 波形进入CNN序列是怎么变成z的batch里每条音频会先被随机裁剪成固定长度比如10-15秒。16kHz采样率下一条10秒音频就是160000个采样点。CNN encoder的7层卷积按stride逐层下采样输出约500个z向量每个向量512维base配置。这500个z就是模型用于后续操作的“token序列”。接下来是mask采样。论文里mask的目标是让大约一半的token被遮住遮法不是零散地遮单帧而是按span成簇地遮每个span连续10帧也就是约200ms的一段。为什么要mask一段而不是单个token因为单个token的信息太局部模型靠相邻帧几乎就能猜出来达不到语义理解层面mask一段则强迫模型从更远的上下文里找线索。这个span长度10是论文里调出来的平衡点太短任务太简单太长任务难到不收敛。mask操作发生在z这一层不是原始波形也不是Transformer输出的c。操作方式很简单把被选中的位置的z替换成一个可学习的mask embedding向量。注意这个mask embedding是全序列共享的一个向量不区分位置。真正的位置信息由相对位置编码提供。4.2 两条路径并行量化器生成答案Transformer写卷子处理完mask后z会分成两路这是理解整个模型最关键的地方。第一路是把原始z没被mask替换前的版本送进量化器。量化器先对z做LayerNorm然后分成两组每组在对应的320-entry码本里用Gumbel softmax挑一个entry最后把两个entry拼接起来得到q_t。这条路径产生的是“被遮位置的正确答案”。第二路是把已经mask过的z序列送进Transformer。注意只有被mask的位置的z换成了mask embedding其他位置还是原始z。Transformer通过相对位置编码、自注意力把所有位置的上下文信息融合起来在每一帧输出一个上下文表示c_t包括那些被mask的位置。这里我想强调一个容易误解的点量化器只看单帧的z不依赖上下文Transformer只看被mask后的序列不直接访问被mask位置的真实内容。两者各自独立但它们共享底层的CNN encoder。也就是说encoder参数的梯度来自两条路径的合力——它既要产出能稳定量化的特征又要产出Transformer能够预测的特征。两个任务放在一起才能让encoder学到“既能定位声学细节、又能服务语义理解”的中间表示。4.3 最后一步在mask位置上算对比loss现在我们有了一整个batch的c_t和q_t。为了计算loss先要把所有位置的q_t收集起来作为候选池。对于每个mask位置t正样本就是该位置自己的q_t负样本则是从候选池里随机抽取的K100个其他位置的q_t保证不等于正样本。然后算c_t与每个候选的cosine相似度除以温度0.1softmax取负对数就是该位置的对比loss最后对所有mask位置取平均。diversity loss和L2正则在这个阶段同时计算。diversity loss用的是量化器里Gumbel softmax的概率输出在batch内的平均分布和均匀分布做对比L2正则直接用encoder输出的z算。三项加权相加得到最终loss然后反向传播。到这里你可能会问为什么对比计算不放在Transformer输出层之后非要再单独拉一个量化分支这正是wav2vec 2.0和vq-wav2vec的本质区别量化器不是离线聚类出来的固定模块而是整张计算图的一部分。对比loss的梯度既能更新Transformer也能通过量化器传到CNN encoder整个系统是在同一个目标下联合生长的。离线聚类方案做不到这一点所以效果也差了一截。5. 解析实验结果标注数据砍掉两个数量级为什么还能打原理讲完看实验结果。wav2vec 2.0论文刷屏级的效果不是一个偶然的调参结果而是架构设计带来的结构性收益。5.1 10分钟/1小时/10小时标注数据WER大概是什么水平论文的标准实验是先用LibriSpeech-960h的无标注语音做预训练然后在不同大小的标注子集上微调。下面是我根据论文报告整理的典型水平base模型不接外部语言模型仅说明趋势具体数值会因随机种子和实现版本有小幅波动微调用标注数据量test-clean WERtest-other WER10分钟20%以内20%出头1小时接近或低于10%略高一些10小时6%左右7%左右100小时5%左右6%左右这个表的震撼之处在于10分钟标注是很多人连传统声学模型训练都跑不起来的量级。以前做低资源语音识别大家想的是怎么用数据增强、迁移学习、半监督学习把有限标注的每一秒榨干wav2vec 2.0直接把门槛降到了“标注10分钟就能出可用的demo”。当然这里说的“可用”是对任务相对简单、说话人相对标准的LibriSpeech而言真实场景噪声、口音都会让数字恶化但这个数量级的下降本身已经足够说明问题。和wav2vec 1.0对比更直观同样在10分钟或1小时标注下2.0相比1.0的WER下降幅度非常大而且标注数据越少优势越明显。这说明2.0的预训练表示质量确实上了一个台阶不是靠模型变大硬堆出来的。5.2 预训练数据规模和模型规模对效果的影响有多直接论文同时给了两组规模变量的实验。第一组是把预训练数据从960h扩到53k小时LibriVoxlarge模型的效果进一步提升100个小时的标注微调已经可以逼近直接用全部960h标注训练的传统模型。第二组是模型从base变large词错误率继续下降但训练成本也成倍增加。这背后的逻辑很朴素自监督预训练本质上是在无标注数据上“阅读”大量的声学模式和语音结构。数据越多模型见过的说话人、口音、语速变化越丰富学到的表示越稳定。这也是为什么wav2vec 2.0之后各家大厂都在拼命攒无标注语音做预训练——标注少没关系无标注音频对语音来说太容易获取了。但有一个我实际用下来很在意的点预训练数据域和下游数据域不匹配时收益会明显打折。用LibriSpeech这种朗读语音预训练拿到带噪会议语音上微调效果远不如用同领域的无标注语音预训练。如果条件允许尽量用跟目标场景接近的数据做预训练而不是迷信“数据越大越好”。5.3 为什么少量标注大量无标注在语音上可行直觉解释一下这个现象。语音识别的难点大头在声学建模把连续、充满变化的声学信号映射到离散的发音单元上。这个映射关系其实不需要文本标注也能学——因为语音本身就有很强的内在结构比如同一个音素在不同上下文里的声学表现是相关的音节、词之间存在可预测的分布规律。自监督预训练就是让模型在这种结构上先跑一遍。等你把encoder和Transformer都预训练好之后它们已经学会了“什么样的声学特征对应什么样的发音单元”这一大堆知识。微调阶段要做的只是在这套表示上加一个很薄的映射层把上下文表示映射到字符或子词。这个薄层的参数很少需要的数据自然就少。反之如果你从随机初始化开始训练模型必须在有限标注里同时学声学建模和语言建模两件事数据需求当然天差地别。顺着这个思路你也会理解为什么后来HuBERT、WavLM这些工作都在wav2vec 2.0的基础上改进。它们换的是“预测目标怎么生成”而不是推翻“预训练微调”这个范式。wav2vec 2.0最大的贡献就是把这个范式在语音上验证通了。6. 复现wav2vec 2.0的实操笔记与踩坑复盘原理讲完剩下的是真金白银的工程问题。我的复现主要基于fairseq和HuggingFace的transformers版本下面是整理出来的一些关键配置和血泪教训。6.1 fairseq里的模型配置和主要超参base模型在fairseq里的核心配置大概是参数baselarge卷积encoder层数77Z维度feature_dim512512Transformer层数1224模型维度7681024attention heads816总参数量约95M约317Mmask策略参数mask比例0.5mask span长度10负样本数100量化码本2组、每组320 entryGumbel温度从2退火到0.5。优化器一般用Adam或AdamWpeak learning rate约5e-4warmup占训练总步数的一小部分。预训练时batch size按音频总帧数控制通常会用到非常大的有效batch相当于数千秒音频所以单卡几乎跑不动需要多卡并行加梯度累积。微调阶段和预训练有几个明显不同mask、量化器全部去掉只保留CNN encoder和Transformer在最上面接一层随机初始化的线性层把输出映射到字符或子词表损失函数换成CTC。CTC对时间对齐要求低非常适合语音识别这种输入输出长度不一致的任务而且对少量标注数据更友好。6.2 显存、batch size、负采样数量这些工程细节显存base模型预训练8卡V10032G是起步配置再小的卡基本只能做微调和推理。如果预训练确实跑不动一个降级方案是直接从HuggingFace下载官方checkpoint做微调效果依然不错。batch size预训练时batch里所有音频的总时长直接决定显存占用。我习惯把音频裁到10-15秒然后用max_tokens按帧数控制batch比如base模型一开始设几千帧根据显存余量再调。负样本数量K100是论文默认。显存紧张时可以降到50效果会掉一些但不会崩。反过来K加到200基本没有提升徒增计算。梯度累积多卡训练时如果有效batch还不够大用梯度累积模拟大batch但注意BatchNorm之类的层在累积时等价性会有问题好在这里用的都是LayerNorm基本不受影响。推理显存预训练checkpoint里包含量化器的codebook和gumbel logits微调后部署时这些参数用不到记得裁剪掉能省一笔显存。6.3 我踩过的坑mask策略、学习率和量化温度第一个坑是学习率。warmup步数太短、峰值学习率太大量化器会在前几百步快速塌缩到少数entry后面再怎么训练都救不回来。我的做法是先把学习率拉低跑200步看码本利用率是否上升再逐步提到峰值。第二个坑是mask比例。论文的0.5/10是调好的平衡点但在小规模预训练数据上直接套用容易过拟合。数据只有一两百小时时我会把mask比例降到0.3-0.4同时加大dropout让任务难度和模型容量匹配。反之数据量很大时可以尝试mask比例0.6让模型做更难的任务但收益不一定明显需要试。第三个坑来自训练日志的误判。只看对比loss下降是不够的一定还要盯多样性损失和码本利用率。如果diversity loss一直高居不下或者某些entry使用频率远高于其他entry说明量化器没学好。我的经验是每训练一段就把Gumbel softmax输出的平均分布打印出来观察是否接近均匀出现明显偏斜时优先调低学习率而不是加大diversity loss的权重。第四个坑是微调阶段没有正确去掉mask。在fairseq里finetune任务如果忘了把mask相关配置关掉模型会在微调时继续mask输入导致训练目标变成“预测被遮内容CTC识别”的混合任务模型困惑度很高。这个问题在日志上表现得很隐蔽loss一直在降但验证集WER纹丝不动。以上这些坑踩过去之后你就会发现wav2vec 2.0原理上并不复杂真正难的是让整套系统的四个模块encoder、quantizer、transformer、loss在训练中保持平衡。任何一环突然失去约束效果都会像多米诺骨牌一样倒掉。这也是为什么我在调参时会同时盯三个指标对比loss、多样性loss、码本利用率而不仅仅是总loss。最后再分享一个我自己觉得很有用的小习惯训练完一个阶段性checkpoint后用固定的少量标注数据做一次快速微调比看多少loss曲线都有用。loss低不一定代表表示好但快速微调的WER能直接反映预训练表示的真实质量。整个wav2vec 2.0设计的最终目的就是让这条快速微调的路径尽量短、尽量稳而你已经理解了它内部每一段路是怎么修的。
返回列表