AI/ML 详解:音频与语音模型

Day 53 · 2026-07-10
面向:有编程经验的非 AI 方向工程师

语音识别 & CTC 对齐Speech-to-Text / CTC

机制对齐Whisper / wav2vec
一句话类比

你有两条采样率不匹配的流:高频音频帧(每 20 毫秒一帧,一句话几百帧)和低频字符序列(几十个字)。难点是它们长度不等、又没告诉你哪几帧对应哪个字——像两个时钟不同步的分布式流要做 join,却没对齐键。CTC 的解法很像流处理的去抖动 + 折叠去重:每帧各吐一个符号(可以吐"空"),最后把连续重复和空折叠掉得到干净文本。

它解决什么问题 + 工作机制

语音识别(STT / ASR,automatic speech recognition,把说话转成文字)最棘手的不是"听懂"而是对齐(alignment):训练数据只给"这段音频 = 这句话",没标哪几帧是"你"、哪几帧是"好",人手标对齐贵到不可行。CTC(Connectionist Temporal Classification,Graves 等 2006)的天才之处是:不需要对齐标注,把所有可能的对齐一次性"积分"掉

具体做法:在字符表里加一个特殊的 blank(空白符 ⊘)。网络对每一帧都输出一个字符分布(可以是 ⊘)。一条"帧→符号"的路径,通过折叠规则变成文本:先合并相邻重复、再删掉所有 ⊘。于是文本 "你好" 可由无数条帧级路径产生,比如 你你⊘好⊘你好好你⊘⊘好……CTC 的损失就是让所有能折叠成正确文本的路径的概率之和最大

P(文本 | 音频) = Σ_{所有能折叠成该文本的路径 π} P(π | 音频)

逐符号拆:π 是一条帧级路径;折叠把它压成文本;Σ 把千万条合法路径的概率加起来。为什么加?因为我们不在乎哪帧对哪字,只要最终文本对——把"对齐"这个不关心的隐变量边缘化(marginalize)掉。这个求和看似指数爆炸,但用动态规划(前向-后向算法)能高效算,跟 HMM 同源。blank 还顺手解决两个真问题:区分连续重复的真字符("嗯嗯"要用 ⊘ 隔开才不被折叠成一个"嗯")、以及沉默/停顿该输出什么。

CTC 是一派解法(帧独立、无自回归、快)。另一派是 Whisper(Radford 等 2022)走的 encoder-decoder:编码器把音频压成特征,解码器像翻译一样自回归逐字生成,对齐交给注意力隐式学习。还有 wav2vec 2.0(Baevski 等 2020)的第三条路——先自监督预训练(在海量无标注音频上做"完形填空",掩一段波形让模型对比预测),再用少量标注微调,让预训练表示先啃一半对齐难题。

代码示例
# 用官方 Whisper 做一次转写;再看 CTC 折叠规则的本质
import whisper                        # pip install openai-whisper
model = whisper.load_model("base")   # 本地模型,无需 API key
result = model.transcribe("audio.mp3", language="zh")
print(result["text"])              # encoder-decoder 自回归输出文本

# CTC 折叠规则本身只有几行——这才是它的核心
def ctc_collapse(frames, blank="⊘"):
    out, prev = [], None
    for ch in frames:          # frames: 每帧 argmax 出的符号
        if ch != prev and ch != blank:  # 跳过重复、删掉 blank
            out.append(ch)
        prev = ch
    return "".join(out)
print(ctc_collapse(list("你你⊘好好")))   # → 你好
常见误区 + 实践场景
误区:"CTC 里每帧独立预测,所以它'理解'了上下文。"——不。CTC 有一个硬假设:给定音频,各帧的输出条件独立。它不建模"字与字之间的语言规律"(比如"苹"后面大概率是"果")。这是它又快又能流式的代价,也是为什么实战里 CTC 常要外挂一个语言模型来纠错。Whisper 的自回归解码器天然带语言建模,所以更流畅,但也更容易在没听清时顺着语言惯性"脑补"(幻觉出并没说的词)。
📌 超级个体场景:本地跑 Whisper 把会议录音、语音备忘转成文字纪要,喂给 LLM 做摘要——一条零成本的"语音→知识"管道。理解 CTC vs 自回归的区别,你就知道:要低延迟实时字幕选 CTC 系,要离线高质量转写选 Whisper 系。
Takeaway + 思考题
💡 语音识别的核心不是"听懂"而是"对齐";CTC 用 blank + 折叠把无数对齐路径求和边缘化掉,无需帧级标注。
🤔 CTC 把"不关心的隐变量(对齐)"积分掉、只优化"关心的结果(文本)"。你在别处见过同样思想吗?(提示:EM、变分推断的边缘化)

语音合成架构Text-to-Speech Architecture

两阶段声学模型 + 声码器
一句话类比

现代 TTS 像两阶段编译器声学模型是前端,把文字编译成一份中间表示(IR)——梅尔频谱(mel-spectrogram,一张"声音的乐谱",横轴时间、纵轴频率能量);声码器(vocoder)是后端,把 IR"代码生成"成能播放的波形。好处和编译器一样:关注点分离——前端管内容韵律、后端管音质音色,各自可独立替换升级。

它解决什么问题 + 工作机制

直接从文字一步生成 16kHz 波形,等于让模型逐个吐出每秒一万六千个高度相关的采样点——搜索空间大到几乎学不动。破局思路是降维中转:先生成信息密度低得多的梅尔频谱(每秒约几十帧,只留人耳敏感的频率能量、扔掉相位细节),再由声码器补回波形。

① 声学模型:文字 → 梅尔频谱。 Tacotron 2(Shen 等 2018)是经典:seq2seq + 注意力把字符序列映射成频谱帧。这里藏着 TTS 的核心难点——时长(duration):文本短、语音长,一个字摊几帧?哪里重读停顿?自回归靠注意力隐式解决,但易跳字/重复/念崩;后来的 FastSpeech 系改用显式时长预测器并行生成,更稳更快。

② 声码器:梅尔频谱 → 波形。 开山之作 WaveNet(van den Oord 等 2016)——自回归逐采样点生成,音质惊艳但慢到无法实用(生成 1 秒算 1.6 万步)。后续用 GAN(HiFi-GAN 等)和扩散声码器把它并行化、提速几个数量级而音质不降。

③ 新范式:codec 语言模型。 VALL-E(Wang 等 2023)把 TTS 当"语言建模"——先把语音用神经 codec 压成离散 token(下一节主角),再训 LLM 自回归预测音频 token,最后解码回波形。它模糊了声学模型/声码器的划分,还带来惊人能力:3 秒目标音频即可零样本克隆音色。

经典两阶段管道

文字──声学模型──▶梅尔频谱 (IR)──声码器──▶波形
      Tacotron2 / FastSpeech            WaveNet / HiFi-GAN / diffusion

新范式:codec 语言模型(VALL-E)
文字 + 3秒音色提示──▶自回归预测音频 token──▶codec 解码→波形
代码示例
# 用 HuggingFace 官方管道跑一次两阶段 TTS(Bark/VITS 类同理)
from transformers import pipeline
import soundfile as sf

tts = pipeline("text-to-speech", model="microsoft/speecht5_tts")
# 说话人向量:决定音色的"身份指纹",可换成克隆的目标音色
import torch
spk = torch.zeros((1, 512))          # 占位;实际用 speaker embedding
out = tts("你好,这是一次语音合成演示。",
          forward_params={"speaker_embeddings": spk})
sf.write("out.wav", out["audio"], out["sampling_rate"])
# 管道内部:文本→梅尔频谱(声学模型)→波形(声码器),两阶段被封装好了
常见误区 + 实践场景
误区:"TTS 已经完美,和真人无法区分。"——半对。Tacotron 2 在单句朗读的自然度评分(MOS)上确实逼近真人,但那是朗读腔。真正难的是长文本的韵律连贯、情感、语气重音——机器仍偏"平"。且自回归 TTS 有顽疾:注意力对齐一旦崩掉就会漏字、无限重复或戛然而止,这也是显式时长建模(FastSpeech)出现的原因。
📌 个人项目场景:把你的知识笔记、每日简报用 TTS 转成播客音频,通勤时"听"而非"看"。理解"声学模型 vs 声码器"两段可分,你就懂了为什么音色克隆(换说话人向量)和内容/韵律是解耦的——换音色不必重训整条链路。
Takeaway + 思考题
💡 TTS 用"文字→梅尔频谱→波形"两阶段降维破局:声学模型管内容韵律,声码器管音质;codec 语言模型(VALL-E)把它统一成"自回归预测音频 token"。
🤔 声学模型的核心难点是"时长":一个字该摊几帧、哪里停顿。这和你写代码时"估算一个任务要占多少时间片"的调度问题,本质是不是同一类?

神经音频 codec 与离散化Neural Codec & Discretization

量化RVQSoundStream / EnCodec
一句话类比

把连续音频量化成离散码,本质是数据库的字典编码(dictionary encoding):与其存一长串高精度浮点,不如学一本码本(codebook),把每小段声音映射成"第几号码字"的整数 ID。而 RVQ(残差向量量化)更像逐级 delta 编码:第一级粗略近似,第二级专编"第一级没搞定的残差(误差)",第三级再补残差……一层层逼近,精度越叠越高。

它解决什么问题 + 工作机制

这节是把音频接进 LLM 世界的关键枢纽。LLM 整套机器(Transformer、下一 token 预测)都建在离散 token上,可音频是连续的。神经 codec(neural audio codec)就是那个"音频分词器":编码器把波形压成低频特征,量化器把每个连续特征向量吸附到码本里最近的码字、输出整数 ID,解码器再从 ID 重建波形。整条链路端到端训练,目标是"重建音质尽量接近原始"。

难点:单个码本不够用。要覆盖丰富音色,码本得巨大(几十万条),训练不稳也浪费。RVQ(residual vector quantization)的解法是分而治之——用一串小码本级联:

RVQ:逐级残差逼近(像 delta 编码)

连续向量 x ──▶ 码本1 取最近码字 c₁,残差 r₁ = x − c₁
r₁ ──▶ 码本2 逼近 r₁ 得 c₂,残差 r₂ = r₁ − c₂
r₂ ──▶ 码本3 逼近 r₂ 得 c₃ …… 逐级细化
最终 x ≈ c₁+c₂+c₃+… ,音频每帧 = 一组码字 ID(几个整数)

这样 N 个各含 K 条的小码本,组合出 Kᴺ 种表达力,却只需存 N·K 条码字——指数级表达、线性级存储SoundStream(Zeghidour 等 2021)首创这套端到端 RVQ codec,EnCodec(Défossez 等 2022)做到更高保真。深远意义不止压缩:音频从此变成"离散 token 序列"——于是 VALL-E 能像写句子一样"写"语音,音乐音效也能被 LLM 生成。这正是多模态大模型能"听说"的底层地基。

代码示例
# 用官方 EnCodec 把波形编成离散 token,再解码回来
from transformers import EncodecModel, AutoProcessor
import torch

model = EncodecModel.from_pretrained("facebook/encodec_24khz")
proc  = AutoProcessor.from_pretrained("facebook/encodec_24khz")

wav = torch.randn(1, 24000)          # 1 秒假音频;实际读取真波形
inp = proc(raw_audio=wav, sampling_rate=24000, return_tensors="pt")
enc = model.encode(inp["input_values"], inp["padding_mask"])
codes = enc.audio_codes           # 离散码:形状 [层数, 帧数] 的整数 ID
print(codes.shape, codes.min().item(), codes.max().item())
# 这些整数 ID 就能当"音频 token"喂进语言模型
rec = model.decode(enc.audio_codes, enc.audio_scales,
                  inp["padding_mask"])[0]   # ID → 重建波形
常见误区 + 实践场景
误区:"codec 就是 MP3 那种压缩,只为省带宽。"——格局小了。传统 codec(MP3/Opus)是信号处理手工算法,产物是给耳朵听的比特流;神经 codec 的产物是语义化离散 token,能直接进 Transformer 做生成、编辑、理解。同一段"离散化",MP3 是终点,神经 codec 是起点。另注:量化必然有损,码本层数越少、压得越狠、失真越大。
📌 跨学科思考场景:离散化是"把连续世界塞进符号系统"的通用母题——它同时出现在语音 token、视觉 patch、甚至你对世界的语言化认知里。RVQ"逐级残差逼近"给了你一个思考"如何用有限符号逼近连续现实"的具体样板。
Takeaway + 思考题
💡 神经 codec 是"音频分词器":RVQ 用级联小码本逐级逼近残差,指数级表达、线性级存储,把连续波形变成离散 token——这是 LLM 能"听说"的地基。
🤔 文本 token 是人类造的天然离散单位,音频 token 是模型自己学的。学出来的会不会比人造的更"高效"?它对应"声音里的什么基本单位"?

流式与实时机制Streaming & Real-Time

因果性流处理延迟-质量权衡
一句话类比

离线识别像批处理(batch):等整段音频到齐,前后文都能看,慢慢算最优解。流式识别像流处理(stream):一边流进来一边出结果,不能等未来。这逼出一个硬约束——因果性(causality):t 时刻的输出只能依赖 t 及之前的输入。代价和流式系统里一模一样:看不到未来上下文,结果只能次优,除非多缓冲一点(增加延迟)换质量。

它解决什么问题 + 工作机制

实时字幕、语音助手、同声传译都要求边说边出,不能等说完。但深度模型天生爱"看全局":标准注意力让每帧都能看到整段音频,双向卷积也读取未来帧。要流式化,必须给模型动因果手术,机制上有三招:

① 因果卷积/因果注意力(causal): 强制每帧只看过去、不看未来(掩码遮住未来帧),和自回归 LLM 的因果掩码同源。彻底流式、零延迟,但丢了未来上下文,准确率下降

② 分块处理(chunk-based): 把音频切成小块(如 320 毫秒),块内双向看全(拿到局部未来)、块间保持因果。这是延迟与质量的折中旋钮:块越大、上下文越足、质量越高,但要多等一个块时长、延迟越大。

③ 有限前瞻(look-ahead): 允许模型偷看未来一小段(如 200 毫秒)再出当前结果——用一点固定延迟,显著回收因果性损失的精度。

延迟 ←──────────────────────→ 质量

纯因果· 小前瞻· 分块 (小块)· 分块 (大块)· 离线全局
零延迟·准确率低                             高延迟·准确率高

结构上,CTC 这类无自回归模型天然适合流式(每帧独立、来一帧算一帧),Whisper 式自回归解码器要流式化则需额外设计——这也是低延迟场景常见 CTC/Transducer 系的原因。

代码示例
# 流式的本质:维护滑动缓冲,来一块算一块,只依赖过去
import numpy as np

class StreamingASR:
    def __init__(self, chunk=0.32, lookahead=0.2, sr=16000):
        self.buf = np.zeros(0, dtype=np.float32)   # 累积缓冲
        self.chunk = int(chunk * sr)               # 每块样本数
        self.la = int(lookahead * sr)             # 前瞻窗口(换质量)

    def feed(self, audio):          # 麦克风源源不断喂进来
        self.buf = np.concatenate([self.buf, audio])
        while len(self.buf) >= self.chunk + self.la:
            # 取"当前块 + 一点前瞻"送模型,但只输出当前块的结果
            window = self.buf[: self.chunk + self.la]
            yield self.decode_causal(window)   # 因果解码:不回看已定稿
            self.buf = self.buf[self.chunk:]      # 滑动,丢掉已处理
# chunk/lookahead 就是你手里的"延迟↔质量"两个旋钮
常见误区 + 实践场景
误区:"流式模型只是把离线模型切块喂进去。"——不。离线模型用了双向上下文,直接切块喂会严重掉点,因为它依赖的未来信息不存在了。真正的流式模型要在训练时就注入因果约束(因果掩码、分块训练),学会"只靠过去也能判断"。诚实结论:流式与离线是两种不同训练目标的模型,不是同一模型的两种用法。
📌 决策辅助场景:接语音功能时先问"能容忍多少延迟"。实时对话(<300ms)得纯因果/小前瞻、接受精度损失;事后转写不在乎延迟,直接上离线全局模型拿最高质量。这条"延迟换质量"谱系,和你在分布式系统里权衡"一致性 vs 响应时间"是同一种决策肌肉。
Takeaway + 思考题
💡 流式的核心约束是因果性(不能看未来);因果/分块/前瞻是在"延迟↔质量"谱系上取点的三种机制,且须在训练时就注入因果性,不是离线模型的切块用法。
🤔 同声传译也面临"不能等说完",译员靠"预测下一句"抢时间。这种"用预测补偿因果延迟"的策略,能不能用到流式语音模型里?

深入资源Further Reading

深入思考Deep Questions

1. 本期四个概念(识别/合成/离散化/流式)看似分散,但有一条统一主线。那条主线是什么?
主线是"连续信号 ↔ 离散符号"这道鸿沟的反复跨越。声音是连续、无天然分词、长度不定的时序流;而整套智能机器(Transformer、token 预测、语言规律)都建在离散符号上。识别把连续波形折叠成离散文字(CTC 消掉时间轴上的对齐自由度);合成反向把离散文字展开成连续波形(难在"文本→时长"的一对多展开);离散化(codec)最纯粹——直接给音频造一套离散 token;流式在时间维度做同样的事,把连续输入切成可增量处理的单元。看懂这条线,你会发现多模态大模型动作惊人一致:凡想让 LLM 处理的模态,第一步都是给它造一套离散 token(视觉 patch、音频 codec code)。离散化是把万物接进符号智能的通用接口。
2. VALL-E 把语音变成 token 后,"说话"和"写字"在模型眼里就没本质区别了。这种统一会把语音模型带向何方?潜在风险是什么?
方向:音频一旦变 token,语音就能享受 LLM 全部红利——in-context learning(3 秒示例即克隆音色,正是 few-shot 的翻版)、scaling law多模态统一(文本/音频/视觉 token 混在同一序列,一个模型端到端"听-想-说",不再是识别→LLM→合成的拼装管道)。终点可能是原生语音对话模型:不转文字,直接在音频 token 空间理解回应,保留语气、情感、笑声。风险也源于这份统一:(1) 音色克隆滥用——3 秒样本即可伪造任何人声音,诈骗门槛塌了;(2) 幻觉迁移到语音,且语音幻觉比文本更难察觉;(3) 版权与身份——人的声音算不算须授权的生物特征?(4) 合成越像真、鉴伪越难。技术统一带来能力跃迁,也把文本时代的对齐/安全难题一并搬进了更难监管的音频域。
3. 语音识别中"CTC(帧独立、快、可流式)"与"自回归解码器(带语言模型、质量高、难流式)"是一对经典权衡。你在别的技术领域见过同构的权衡吗?
这是"独立并行 vs 顺序依赖"的经典对立。CTC 假设各帧输出条件独立——可并行、低延迟、可流式,但放弃了对输出间依赖的建模(不知道"苹"后接"果");自回归解码器让每个输出依赖之前全部——建模了依赖、质量高,但必须顺序生成、难流式。同构的权衡:分布式的无锁并行 vs 串行事务、数据库的最终一致 vs 强一致、LLM 推理里并行解码 vs 自回归解码,都是同一张牌桌。深层结构是:输出元素之间依赖有多强? 弱则并行(省时间、放弃一点全局最优),强则顺序(保质量、付延迟)。工程上的经典中间态——先并行出草稿、再顺序精修(CTC 候选 + 语言模型重排、投机解码的 draft+verify)——在语音、翻译、LLM 推理里是同一招式。识别出"这是不是依赖强度决定并行性的问题",就能把一个领域的直觉迁移到另一个。
4. 人类学母语时没人给"帧级对齐标注",却能从连续声流里自己切出词、学会说。这与本期机器路径有何异同?对"构建更像人的学习系统"有何启发?
相同:wav2vec 2.0 的自监督精神上很接近——婴儿也是在无标注声流里靠"预测/填补"先学出语音表示,远早于懂词义;CTC"不需对齐标注"也呼应"没人给孩子标第几毫秒是哪个音素"。不同:(1) 婴儿是多模态具身学习——声音伴随视觉、意图、反馈(指着苹果说"apple"),对齐信号来自跨模态关联而非纯音频;(2) 婴儿边听边说边被纠正,是闭环交互而非固定数据集;(3) 婴儿数据量远小于 Whisper 的 68 万小时却更通用,说明人有强归纳偏置。启发:更像人的系统或许该(a) 多模态联合而非单模态堆量;(b) 交互式闭环而非离线大数据;(c) 承认数据规模有极限,结构先验才是样本效率关键。这把语音学习和 Day 41 世界模型、具身智能连了起来——真正的语言理解,可能离不开一个"在世界里行动"的身体。