跳转至

Method

这一页是长程知识,不随单个实验变。 概念用定义 + 公式 + 图固定;会变的取值集中在 §8 一张表。 当前方法 = EmoScale:音频到音频的情感强度编辑(M17–M20 线,2026-08-29 起)。 旧的 TTS 线方法(DFD,v4–M16)折叠在 §9,只作历史。

1 问题的形式化

输入一段情感录音 \(x_e\),输出同一说话人、同一句话、情感强度为 \(\alpha\) 的语音。 \(\alpha=0\) 中立化,\(\alpha=1\) 重建输入,\(\alpha>1\) 外推,\(\alpha[k]\) 逐帧可变。系统内没有文本、没有离散 token、没有 LLM。

训练用平行对 \((x_e, x_n)\):同人同句的情感录音与中立录音。\(x_n\) 只作训练目标,不进模型;推理时不需要它。

1.1 骨干:CosyVoice3 的流匹配解码器

\[\frac{dx_t}{dt}=v_\theta(x_t,t,c),\qquad x_0\sim\mathcal N(0,I),\qquad x_1=\text{mel}\in\mathbb R^{80\times K}\]

积分结果是 mel,波形由 HiFT 声码器生成。CosyVoice3 用直线路径:\(x_t=(1-(1-\sigma)t)\,z+t\,x_1\), 目标速度 \(u=x_1-(1-\sigma)z\) 与 \(t\) 无关。

直线路径决定了 α 能做什么、不能做什么

目标速度是端点的线性函数,所以速度的线性混合 ≡ 端点 mel 的线性混合(实测相关 0.9976,偏差 2.8%,E7)。 推论:\(v=v_B+\alpha v_\Delta\) 只能表达仿射于 α 的输出;对"位置型"量(谐波、共振峰的频率位置)线性混合会叠出两套梳齿——这就是中段 α 电音的来源, 修法只能在推理端(§4.3),不在训练端(归因页)。

1.2 双塔残差参数化

\[v_{\text{neu}}=v_B,\qquad v_{\text{emo}}=v_B+v_\Delta,\qquad v(\alpha)=v_B+\alpha\,v_\Delta\]

\(v_B\) 由基塔 DiT\(_B\) 产生,向中立目标学;\(v_\Delta\) 由残差塔 DiT\(_\Delta\) 产生,只通过和式向情感目标学(没有直接的残差监督)。 \(\alpha\) 不进入网络。

2 架构

flowchart LR
  XE["情感录音 x_e<br/>(唯一输入)"] --> W["Whisper-large-v3 encoder ❄<br/>最后层 · 50 Hz · 1280 维"]
  W --> AB["Adapter_B 🔥<br/>Conv1d 1280→512→80"] --> MUB["μ_B (80×K)"]
  W --> AD["Adapter_Δ 🔥<br/>同结构"] --> MUD["μ_Δ (80×K)"]
  XE --> C["CAM++ ❄"] --> LIN["Linear 🔥 192→80"] --> S["s (80)"]
  MUB --> DB["DiT_B 🔥<br/>init: CosyVoice3 flow.pt"]
  MUD --> DD["DiT_Δ 🔥<br/>init: 同上,输出投影置零"]
  S --> DB & DD
  YT["Y_t (80×K)<br/>训练:两端 mel 按 α 混合再加噪<br/>推理:ODE 当前状态,从纯噪声 z 出发"] --> DB & DD
  DB --> VB["v_B"] --> SUM(("⊕")) --> VE["v_emo = v_B + v_Δ"]
  DD --> VD["v_Δ"] --> SUM
  VB -. "L_B" .-> UN["u_n(中立目标)"]
  VE -. "L_Σ" .-> UE["u_e(情感目标)"]
  VD <-. "L_⊥" .-> VB
模块 输入 → 输出 参数量 训练状态 初始化
Whisper-large-v3 encoder 16 kHz 波形 → \(H\) (1280×K),最后一层 hidden,50 Hz;输入按 whisper 约定补到 30 s、只取有效帧 635M ❄ 冻结 官方权重
Adapter\(_B\) / Adapter\(_\Delta\) \(H\) → \(\mu_B\) / \(\mu_\Delta\) (80×K):Conv1d(1280→512,k3)+GELU+Conv1d(512→80,k3) 2.2M ×2 🔥 随机
CAM++ 波形 → x-vector (192) — ❄ CosyVoice3 自带
Linear 192 → 80 → \(s\) 15k 🔥 基座 spk_embed_affine_layer
DiT\(_B\) \((Y_t,\mu_B,t,s)\) → \(v_B\) 331M 🔥 基座 flow.pt 全量
DiT\(_\Delta\) \((Y_t,\mu_\Delta,t,s)\) → \(v_\Delta\) 331M 🔥 基座主体 + 输出投影置零(起点 \(v_\Delta\equiv0\))
HiFT mel → 24 kHz 波形(HnNSF 源-滤波) — ❄ 基座
prompt / cond 恒为 0:无参考前缀、无 in-context — — —

可训参数 ≈ 665M。训推完全同构:训练与推理的条件来源、张量形状、采样步骤一致,只是推理多一个 \(\alpha\)。

形状与单位:\(x,v,\mu,Y_t\in(1,80,K)\);\(K\) = mel 帧数 @ 50 Hz(hop 480 @ 24 kHz,1 帧 = 20 ms);whisper 特征 50 Hz 与 mel 帧对齐后线性插值到 \(K\)。

\(Y_t\) 是什么、从哪来。 \(Y_t\) 是两塔的状态输入(flow-matching 里的 \(x_t\)),不是从情感音频取的特征——情感音频只通过条件 \(\mu_B,\mu_\Delta,s\) 进入网络。

训练 推理
\(Y_t\) 的来源 两端目标 mel 按随机 α 混合 \(X_\alpha=(1-\alpha)X_n+\alpha X_e\),再与噪声 \(z\) 按随机 \(t\) 插值:\(Y_t=(1-(1-\sigma)t)\,z+t\,X_\alpha\) 从纯噪声 \(Y_0=z\) 出发,沿 \(v(\alpha)=v_B+\alpha v_\Delta\) 做 10 步 Euler 积分得到的当前状态
含有的信息 目标 mel 的部分信息(\(t\) 越大越多)+ 噪声 只有噪声与网络自己的历史输出
为什么要 α 混合 让两塔在"中立与情感之间"的状态上也见过样本,推理时任意 α 的路径都在训练分布内 —

输入音频统一 16 kHz,算 mel 前重采到 24 kHz。

为什么用基座初始化:同架构同数据下随机初始化的双塔 MOS 1.28(电音、25 h 数据学不出谱纹理),基座初始化 1.98(M17 vs M18)。基座的语音纹理先验是决定性的。

3 训练

3.1 数据与目标构造

项 值
语料 ESD,中文 0001–0010 + 英文 0011/0015–0020 共 17 位说话人,每人每情感 350 句
划分 按句子:文本编号 % 10 == 0 → test,== 1 → evaluation,其余 → train(build_esd_manifest.py --split_strategy by_text_idx_mod10);说话人不留出
训练对 train 划分、缓存齐全者 17,026 对(data/m20_cache)
\(X_e\) \(x_e\) 的 mel
\(X_n\) \(x_n\) 经 DTW 引导的 RubberBand R3 时变变速对齐到 \(x_e\) 的时间轴后的 mel,\(K_n=K_e\)

3.1.1 中立目标的时间对齐:DTW 锚点 + RubberBand R3

要解决的问题。 两塔看同一个 \(Y_t\),所以 \(X_n\) 必须与 \(X_e\) 逐帧对应、帧数相同。中立录音与情感录音的时长和节奏不同(情感录音通常更快或更慢、停顿位置不同), 直接均匀变速到同一长度时,中立目标与 whisper 条件的逐帧错位中位达 23–284 ms——这是 M18 以前 α=0 "像人话但说不对"(CER ≈ 1.0)的病根(M19)。 对齐后错位 ≤ 1 帧,α=0 的 CER 从 1.24 降到 0.19。

输入。 \(y_n\):中立录音波形(16 kHz);\(\text{mel}_n\in\mathbb R^{K_n\times80}\)、\(\text{mel}_e\in\mathbb R^{K_e\times80}\):两条录音重采到 24 kHz 后的 80 维 mel(n_fft 1920、hop 480,1 帧 = 20 ms = 16 kHz 下 320 采样点)。

第一步 · DTW 求逐帧对应(build_cache_m19.dtw_anchors)

  1. librosa.sequence.dtw(mel_n, mel_e, metric="euclidean") 得对齐路径 \(\{(i,j)\}\):中立第 \(i\) 帧 ↔ 情感第 \(j\) 帧
  2. 对情感侧每一帧 \(k\),取所有映到它的中立帧的均值 \(\varphi(k)\);再做累计最大值 \(\varphi(k)\leftarrow\max_{k'\le k}\varphi(k')\),保证单调不减(DTW 路径允许的"回退"在这里被抹平)
  3. 得到一条从情感时间轴到中立时间轴的单调映射 \(\varphi:[0,K_e)\to[0,K_n)\)

第二步 · 抽锚点并夹速率

每 250 ms(12 帧)取一个锚点,外加最后一帧;锚点写成采样点对 \((s,t)=(\lfloor\varphi(k)\rceil\cdot320,\ k\cdot320)\),即"中立波形第 \(s\) 点应落到目标第 \(t\) 点"。 相邻锚点之间的局部变速比 \(r=(t-t_{\text{prev}})/(s-s_{\text{prev}})\) 夹在 [0.4, 2.5]:超出则把 \(r\) 截断、按截断后的 \(r\) 反推 \(s\);不单调的锚点丢弃。 最后一个锚点强制 \(t_{\text{last}}=K_e\cdot320\)(目标长度 = 情感 mel 长度)、\(s_{\text{last}}\le|y_n|\)。

第三步 · R3 时变变速(r3warp.r3_warp,引擎 RubberBand R3 via pylibrb)

rate[n]   ← 分段常数:锚点段 i 内 rate = (T[i+1]−T[i]) / (S[i+1]−S[i])     # 逐采样点的速率曲线
stretcher ← RubberBandStretcher(16000, 1 ch,
              PROCESS_REALTIME | ENGINE_FINER | TRANSIENTS_CRISP | PHASE_LAMINAR | WINDOW_STANDARD | THREADING_NEVER,
              initial time_ratio = rate[0])
先推入 get_preferred_start_pad() 个零样点
for 每 256 采样点(16 ms)一块:
    stretcher.time_ratio ← median(rate[块内])       # realtime 模式允许中途改比率
    process(块, final=是否最后一块); 收集 retrieve_available()
排空; 丢掉开头 get_start_delay() 个输出点; 裁/补到 T_last

关键在 realtime 模式:R3 允许在流中逐块改 time_ratio,于是一条流走完整句——没有段界、没有交叉淡入, 既保留 R3(相位声码 + 瞬态保护)的音质,又逐锚点跟随 DTW 的局部速率。 选这条路的原因:pylibrb 的 keyframe-map 绑定有 bug(dict 转不进 std::map),无法直接给整条关键帧图;"按锚点切段各自 R3 再拼接"的方案则会在段界留痕。

第四步 · 生成目标:变速后的中立波形裁/补到 \(K_e\cdot320\) 点 → mel16 → 取前 \(K_e\) 帧 → \(X_n\);与 \(X_e\) 同帧数,存入 data/m20_cache。

成本与验证

项 值
速度 R3 0.11 s/条,WSOLA 2.7 s/条(27×);21,301 对用 48 个单线程进程 5 分钟建完(多线程互斥是瓶颈,8 进程多线程只有 185 条/分钟)
对齐精度 锚点变速后与情感帧错位 ≤ 1 帧(M19 mock 验收)
引擎代价 WSOLA 伪影占 GT 残差能量 24%、把 α=0 天花板砍掉 0.52 MOS;换 R3 后消除(探针页)
方向保真 构造出的目标位移与真实情感位移 cos 0.972(1276 条,E7)——对齐没有扭曲情感的声学方向
下游 M20(R3)vs M19(WSOLA):CER@α=0 0.15 vs 0.33,MOS@α=0 2.80 vs 2.05(M20)

已知代价与边界

  • 节奏被锁在输入时间轴:中立目标带着情感句子的节奏,模型学不到、也表达不了时长变化。悲伤 36% 的声学位移在节奏上,这是悲伤最难的架构原因(E7 §3.4);也是"节奏泄漏"已声明接受的部分
  • DTW 用 mel 欧氏距离,在静音/无声段可能局部误配;250 ms 锚点间距和 [0.4, 2.5] 夹紧是经验值,未消融
  • α=0 仍有 18% 样本乱说(E4 §3.5),说明对齐解决了大部分、不是全部

3.2 采样与损失

每条训练样本独立采样(全局 batch 8,DDP 8×1 每卡一条):

\[\alpha\sim\mathcal U(0,1),\qquad t\sim\mathcal U(0.05,\,0.95),\qquad z\sim\mathcal N(0,I)\in\mathbb R^{80\times K}\]

状态与目标(\(\sigma=10^{-6}\),CosyVoice 的 sigma_min;两端目标用同一个 \(z\)):

\[ \begin{aligned} X_\alpha &= (1-\alpha)\,X_n + \alpha\,X_e \\ Y_t &= \bigl(1-(1-\sigma)t\bigr)\,z + t\,X_\alpha \\ u_n &= X_n - (1-\sigma)\,z \\ u_e &= X_e - (1-\sigma)\,z \end{aligned} \]

两塔前向(同一个 \(Y_t\)、同一个 \(t\)、同一个 \(s\),条件各用各的):

\[v_B = \mathrm{DiT}_B(Y_t,\mu_B,t,s),\qquad v_\Delta = \mathrm{DiT}_\Delta(Y_t,\mu_\Delta,t,s)\]

损失(每项都是对该样本 \(80\times K\) 个元素取均值,只算有效帧,pad 帧不计):

\[ \begin{aligned} \mathcal L_B &= \frac{1}{80K}\sum_{c=1}^{80}\sum_{k=1}^{K}\bigl(v_B[c,k]-u_n[c,k]\bigr)^2 \\ \mathcal L_\Sigma &= \frac{1}{80K}\sum_{c,k}\bigl((v_B+v_\Delta)[c,k]-u_e[c,k]\bigr)^2 \\ \mathcal L_\perp &= \frac{1}{80^2}\sum_{i=1}^{80}\sum_{j=1}^{80} G_{ij}^{\,2},\qquad G_{ij}=\Bigl\langle \frac{v_\Delta[i,:]}{\|v_\Delta[i,:]\|+\epsilon},\;\frac{v_B[j,:]}{\|v_B[j,:]\|+\epsilon}\Bigr\rangle \\ \mathcal L &= \mathcal L_B+\mathcal L_\Sigma+\lambda\,\mathcal L_\perp,\qquad \lambda=1 \end{aligned} \]

三项分别在做什么:

项 谁被更新 学什么
\(\mathcal L_B\) DiT\(_B\)、Adapter\(_B\)、Linear 基塔向中立目标学(对齐后的中立 mel)
\(\mathcal L_\Sigma\) 两塔、两 adapter、Linear 和 \(v_B+v_\Delta\) 向情感目标学。残差塔没有自己的目标——它只学"基塔漏掉的那部分";输出投影零初始化使第 0 步时 \(v_\Delta\equiv0\)、\(\mathcal L_\Sigma=\mathcal L_B\) 的情感版
\(\mathcal L_\perp\) 两塔 让残差塔的每个 mel 通道的时间模式与基塔任一通道的时间模式不相关

\(\mathcal L_\perp\) 的确切算法(train_m17.py):

A = v_Δ[:, :K]          # (80, K)  残差塔输出,只取有效帧
B = v_B[:, :K]          # (80, K)
Â[i,:] = A[i,:] / (‖A[i,:]‖₂ + 1e-6)      # 每个 mel 通道沿时间轴归一成单位向量
B̂[j,:] = B[j,:] / (‖B[j,:]‖₂ + 1e-6)
G = Â · B̂ᵀ             # (80, 80),G[i,j] = 残差第 i 通道 与 基塔第 j 通道 的时间序列余弦
L_⊥ = mean(G ** 2)      # 80×80 全部元素(含对角)平方后取均值,∈ [0, 1]

三点容易记错的地方: ① 归一化是沿时间轴(每个通道一条长度 \(K\) 的序列),不是逐帧、也不是整张 mel 拉平; ② \(G\) 是跨通道的全对(\(i\ne j\) 也算),不只是同通道; ③ 平方后取均值,所以正相关与负相关同样受罚——它推动的是"不相关",不是"反向"。 M22 显示不加这项时两塔自然呈负相关(cos −0.3 ~ −0.5),因为 \(v_\Delta\) 要抵消部分 \(v_B\);\(\mathcal L_\perp\) 与这个几何相抵触, λ 越大重建越差(M22)。

只记录、不参与梯度的诊断量(训练日志字段):

字段 定义 用途
L_delta_diag \(\frac{1}{80K}\sum\bigl(v_\Delta-(u_e-u_n)\bigr)^2\) 残差塔离"直接残差目标"多远(我们不用它训练)
A_energy_share \(\|v_\Delta\|^2/\|v_B+v_\Delta\|^2\) 残差占总速度能量的份额(M20 饱和在 ≈0.04)
cos_A_B \(\langle v_\Delta,v_B\rangle/(\|v_\Delta\|\|v_B\|)\)(整张拉平) 两塔输出的整体相关

批与并行:每卡一条样本、各自算三项后按卡平均;变长样本用 pad 到 batch 内最长再用 mask 截回有效帧;梯度裁剪 1.0。

3.3 优化

完整超参数表(数据、模型规模、优化器、四段续训、推理设置;论文 §4.1 Implementation Details)在 04 Experiments §4.1,这里只列骨架:

参数 值
优化器 / lr / clip AdamW(β 0.9/0.999,wd 0.01)/ 1e-5 常数、无 warm-up / 1.0;fp32;adapter 同 lr
全局 batch 8(DDP 8×1)
步数 420k(100k + 80k + 80k + 160k 四段续训,每段优化器重启;≈ 198 epoch,16,997 对)
seed 1234 / 4567 / 6789 / 7890(四段)
当前主模型 outputs/m20d/ckpts/step160000.pt(= 累计 420k)

训练量的教训:残差 A_share 在 100k 后饱和,delta 不再随步数涨;但 α=0 端的中立化到 180k 后才大幅兑现(+0.36 MOS/20k)。 三点以内的"平台"不足以判收敛(M20)。

4 推理

flowchart LR
  XE["情感录音 x_e"] --> COND["条件(同训练)<br/>μ_B, μ_Δ, s"]
  COND --> ODE["Euler 10 步 · 余弦调度<br/>z ~ N(0,I), K = 输入帧数<br/>只跑两端,α 不进 ODE"]
  ODE -->|"v = v_B"| X0["X̂_0 中立端"]
  ODE -->|"v = v_B + v_Δ"| X1["X̂_1 情感端"]
  X0 & X1 --> PA["音高对齐混合<br/>任意 α(标量或逐帧)<br/>同一条链路"]
  PA --> H["HiFT ❄"] --> OUT["波形"]

一条链路,对所有 α 相同。 网络只生成同一句话的两个端点 \(\hat X_0\)(\(v=v_B\))与 \(\hat X_1\)(\(v=v_B+v_\Delta\)); 强度 α 不进 ODE,全部由 §4.3 的音高对齐混合在两端之间施加。α∈[0,1] 是内插,α>1 是沿同一条公式外推; 代码里是同一个函数(m21_gen_lib.pa_mix / Gen.sample_pa),没有按 α 区段切换路径的分支。

4.1 条件与求解器

条件分支与训练完全相同、只用 \(x_e\)。\(K\) = 输入 mel 帧数;\(z\sim\mathcal N(0,I)\)(评测固定 seed 1234,两端共用同一个 \(z\)); Euler 10 步,余弦时间调度 \(t_j=1-\cos(\tfrac{\pi}{2}\tfrac{j}{10})\)。每步两塔各前向一次;正式链路只跑 α=0 与 α=1 两次。

4.2 直路(只用于生成两端;任意 α 代入 ODE 仅作对照臂)

\(v=v_B+\alpha\,v_\Delta\) 在正式链路里只取 α=0、1 两个值。把任意 α(标量或逐帧张量 \(\alpha[k]\))直接代入 ODE 的"直路 α" 只作为 E4 的对照臂保留:它在 α≥1 的 UTMOS 比对齐链路高(§4.3 末的代价表), 但中段谷(α=0.25–0.5 UTMOS 1.6–1.9,双梳齿电音)是它的固有问题,而且 α 区段之间要换路径,所以正式链路不用它。 α 的可用区段(E4,两臂共同结论;数值为直路臂、随机 160 条 + 全量复核):

α 区段 状态 依据
α < 0 ❌ 不开放 最好臂 CER ≥ 0.27;输出同时远离真实中立与真实情感,只是离开流形(E4 §3.7)
α = 0 ⚠️ 中立化 RA 与真实中立不可区分,但 CER 0.22、14% 样本乱说、强绑定说话人(E4 §3.5)
0.25 – 0.5 谷(直路) 直路 UTMOS 1.6–1.9;对齐链路 +0.27 / +0.46
0.75 – 1.5 ✅ 直路 UTMOS 2.17–2.66 / CER ≤ 0.05;1.25 几乎零代价多 25% 强度
α ≥ 2 ❌ UTMOS < 1.7,响度起落斜率与谱通量冲到 z=3–5,真实录音不存在(E7 §3.5)

逐帧 α 的定位精确、无串扰:(0,1) 的前/后半 RA 0.08 / 0.89(E8,直路臂);分段本身有 0.04–0.35 UTMOS 代价,CER 无额外代价。

4.3 音高对齐混合(正式链路)

原理:中立端与情感端音高不同(实测 239 vs 499 Hz),mel 线性混合会把两套谐波梳齿叠在一起。 先把两端拉到同一音高,再混。WORLD 只用来对齐音高;情感的混合在 mel 域。

量(WORLD,16 kHz、帧移 20 ms) 算法 每帧 含义
F0 轮廓 Harvest 1 音高(无声帧 0)
谱包络 CheapTrick 513 平滑频谱 ≈ 声道 / 音色,无梳齿
非周期性 D4C 513 各频带噪声占比
① 两端 mel X̂_0, X̂_1 → HiFT → 波形 ŷ_neu, ŷ_emo(重采 16 kHz)
② WORLD 分析 ×2 → (F0, 谱包络, 非周期) 各一套
③ 共享 F0(对所有 α 同一条公式):
     log F0_α[k] = (1−α[k])·log F0_neu[k] + α[k]·log F0_emo[k]
     只在两端都有声(F0 > 50 Hz)的帧上算,单边有声取有声那边;标量 α 即 α[k]≡α
④ WORLD 重建 ×2:neu 的 (谱包络, 非周期) 配 F0_α;emo 的同理——谱包络各自原样保留
⑤ 各转 mel(重采 24 kHz、80 mel、hop 480 = 20 ms)→ M_α[k] = (1−α[k])·M_neu[k] + α[k]·M_emo[k]
⑥ HiFT → 输出

③ 的三条硬规则(代码 f0_interp 保证):

  1. α>1 不换公式。 α=1.5 就是 \(F_{0,\alpha}=F_{0,\text{neu}}\cdot(F_{0,\text{emo}}/F_{0,\text{neu}})^{1.5}\)——沿同一条对数直线外推,与 α∈[0,1] 是同一行代码。
  2. 不钳位。 不把 F0 的权重截到 [0,1](那是 E4 的消融臂 pa_mix(clamp_f0=True),只用于对照,正式链路禁用)。
  3. 不做线性 Hz 相加。 不用 \(F_{0,\text{neu}}+\alpha\,(F_{0,\text{emo}}-F_{0,\text{neu}})\);对数域插值使半音间隔随 α 线性变化,且 α>1 时不会把 F0 推成负数或零。

逐帧 α:mel 帧与 WORLD 帧都是 20 ms,\(\alpha[k]\) 直接逐帧用于 ③ 和 ⑤(长度不齐时线性插到 WORLD 帧数)。

代价(对齐链路 − 直路 α,同条配对差,n=1276 全档;E4 §3.2 + 补测 outputs/m21full/e4merged/analysis.json):HiFT 用两次、WORLD 往返有固定损失,在谷底由避谷收益抵消,两端没有谷可抵:

α 0 0.25 0.5 0.75 1.0 1.25 1.5
ΔUTMOS −0.24 +0.27 +0.46 +0.03 −0.38 −0.63 −0.73
ΔCER +0.01 +0.05 +0.08 +0.04 +0.02 +0.03 +0.05

正式链路的全量七档(outputs/m21full/e4merged,图 figs/m21_e4_curves_full2.png;将并入 S7 主结果):UTMOS 2.32 / 2.15 / 2.07 / 2.19 / 2.29 / 2.00 / 1.63,CER 0.264 / 0.222 / 0.161 / 0.085 / 0.064 / 0.074 / 0.100,RA 0.16 / 0.29 / 0.47 / 0.69 / 0.97 / 1.26 / 1.63。 强度刻度比直路更线性(档间差 0.13 → 0.37 单调),谷被填平成一条弧;可用区间严格只有 α=1.0,宽松 [0.75, 1.25]。 统一链路是设计决定(一条公式、一条路径、标量与逐帧同法),不是按这张表选的;两端的代价是已知边界,论文的外推区写到 1.25。

4.4 路由(已废止,只作 E4 记录)

二轮 E4 曾按预注册规则在 α 区段间切换路径(对齐臂须配对 ΔUTMOS ≥ +0.20、p < 0.01、ΔCER ≤ +0.07,命中 α=0.25 / 0.5;端到端七档 UTMOS 直路 2.27 → 路由 2.37)。 正式链路不再按 α 切换;该规则与数字保留在 E4 §3.3 作对照。

5 量测

四个有出处的指标,全部对单条音频打分,全部 16 kHz,全部配同批真实录音的锚点:

指标 量什么 方法与出处 参考(ESD-zh test 160 条)
UTMOS ↑ 音质 Saeki et al., Interspeech 2022,utmos22_strong 真实中立 3.71 · 真实情感 2.76(UTMOS 对情感语音系统性低 1 分,模型输出与后者比)
CER ↓ 内容 Paraformer-zh(Gao et al. 2022)/ whisper-large-v3(英文);去标点小写 真实录音 0.01–0.03
SECS ↑ 说话人 WavLM-base-plus-sv x-vector 余弦(Chen et al. 2022),参照 = 输入录音 同一人的中立录音 0.886(不是 1.0)
RA 情感强度 相对属性排序(Parikh & Grauman 2011;Zhou et al. TAFFC 2022),RankSVM on eGeMAPSv02,训练与打分流程见 E2 §2.1 0 = 真实中立均值,1 = 真实情感均值;真实情感 1.07
arousal(副) 强度的外部印证 audEERING wav2vec2-…-msp-dim(Wagner et al. 2022) α ≥ 0.5 后饱和,只作方向印证

口径:单调性报组内 Spearman ρ,刻度一致性报按条中心化的 CCC——两个都要给,ρ 只管单调不管刻度。 RA 只在可懂音频(CER ≲ 0.1)上可解释,|RA| 超出 [−3, 5] 判退化剔除并计数。 评测集:ESD test 划分随机 160 条(四情感各 40,10 位说话人)+ 同批真实中立 / 情感录音;外部验收 CREMA-D(真人三档强度标注,16 位未见演员)。

已废弃:emotion2vec 分类概率

92% 的分数压在 0 或 1,相邻强度档 AUC 0.62——只会说"是不是",不会说"有多"。历史实验里的 delta 读数只作"情感是否翻转"用(E2 §1)。

6 方法论准则

从踩过的坑里提炼的可复用判断。事故经过留在实验页,这里只留原理。

6.1 分布跨在 1 两侧时,「统计量的函数」≠「函数的统计量」

截断率算错过两次(11% → 17% → 实测 12.4%)。错因是把中位比值代进截断公式, 而正确做法是逐对算截断再取中位。时长比中位 1.03 看着无害(情感有长有短、大致抵消), 但逐对截断中位有 12.4%、最坏 49.6%。

规则:比值类统计量,先逐样本算目标量,再聚合。

6.2 「跑起来了」不等于「跑对了」—— 要有落卡/落盘校验

--gpu 参数曾完全无效(deepspeed 在 import 期就初始化 CUDA),训练全落到物理卡 0, 挤到别人的任务上。症状是日志在跑但目标卡显存为 0。

规则:凡是「指定去哪」的参数,启动后必须验证它真的去了那里。 同类:评测报告 n_wavs=0 却退出码 0(读错了 manifest 文件)。

6.3 跨样本比较必须先解决对应关系

第一版跨句一致性测量是逐帧算余弦 —— 但第 \(k\) 帧在两句话里是不同音素, 即使 \(A\) 完全情感专一也测不出相关。改为子空间重叠(与时间对齐无关)才有意义。

规则:比较两个变长序列前,先问「第 k 个位置在两边是同一个东西吗」。

6.4 指标饱和时,低分读不出结论

一个真正成功的 50/50 混合本就不该在任一端点得高分。所以低分既可能意味着混合成功、 也可能意味着音频退化 —— 饱和的分类器无法区分这两者。

规则:用一个指标下结论前,先确认它在你关心的区间里有分辨率。

6.5 对照要能隔离单一变量,且要有「必然相同」的自检臂

六臂调色板里,纯情感配方在六臂逐条完全相同(单分支走恒等 warp,理应如此)。 这条「必然相同」的自检证明了所有差异确实来自混合配方,而非别的扰动。

规则:设计对照时留一个理论上必须相同的臂,它是整个对比有效性的证明。

6.6 n < 10 的数字只作线索,不写进结论

见 写作约定。定性结构通常稳,百分比数值在 n<30 时不可信。 本项目多数调色板实验 n=3 句,属弱证据。

6.7 观测到「没有结构」,不等于「不能施加结构」

M5 测出 \(A\) 的能量在前半/后半几乎无差异(比值 1.026), 我据此预测「前 K/2 vs 后 K/2」的分段 α 不会有效。M6 实测推翻了它 —— half 档定位良好(比值 3.3)。

错在混淆观测与干预:前者问「\(A\) 天然在哪里强」,后者问「人为在某段施加能否只改那段」。 天然没有结构,不妨碍施加人工结构。

规则:从"分布长什么样"推不出"控制能不能施加"。想知道干预的效果,就得做干预。

6.8 接口「看起来一致」只说明最内层一致

M9 里 CosyVoice2 与 CosyVoice3 的 estimator 签名完全相同,我据此以为混合代码能原样跑。实际差异分三层,逐层才暴露: flow 外壳(token→μ 的路径不同)、声码器(参数表不同)、LLM prompt 约定。

规则:跨模型复用代码时,用目标模型自己的推理路径当参照逐层核对, 不能凭签名相同就假定行为相同。

6.9 评测集必须随机抽样;试听集不能当评测集

首轮 M21 建在 P6 的 11 条上,那 11 条是为试听挑的。随机 160 条重测后,α=0 的 CER 从 0.10 变成 0.22、RA 从 0.45 变成 0.09—— 两个方向的偏差都有,因为那 11 条恰好落在中立化最好的三位说话人上。自助检验:从 160 条抽 11 条抽到那个均值的概率 2.6%。

规则:评测集从 test 划分随机抽、n ≥ 160、按情感/说话人分层;任何为听而挑的集合只能试听,不能出数。

6.10 指标要有出处,结果要给逐档绝对值 + 真值锚点

"不能随便想到一个 metric 就往上套"。相关系数只管单调性:ρ=0.93 的旋钮,α=0.25 只兑现了名义强度的 66%,这在 ρ 里完全看不到, 列出逐档分数并配同批真实录音的 0/1 锚点才暴露。

规则:每个指标写出处;每张结果表给逐档绝对值和同批真实录音的参考行;ρ 与中心化 CCC 一起报。

6.11 "某量随 α 变"之前,先问真值是不是按该量构造的

E7 首轮在按 F0 插值构造的阶梯上测出"F0 是强度的主载体"——是构造的回声。改成只比两端位移、α 不进计算后,载体排序变成响度 > 谱斜率 > MFCC > F0。

规则:凡是构造出来的真值,列出它用了哪些量;这些量上的相关不算证据。

6.12 分数只在可懂音频上可解释;退化值要有明示的剔除规则

崩坏音频的 eGeMAPS 大幅偏离中立分布,RA 把偏离读成"极强"(常向量臂读 1.6–2.4,α=5 读 9.4)。线性模型的权重也不可直读,须按 Haufe et al. 2014 转成 activation pattern。

规则:强度类指标先过可懂性门槛;退化判据跑前写死(|RA| ∉ [−3, 5]、eGeMAPS |z| 99.5 分位 > 8),剔除数量随结果一起报。

7 与 λ、训练量、泛化有关的已知边界

边界 事实 出处
节奏通道锁死 DTW 把中立目标对齐到情感时间轴;悲伤 36% 的位移在节奏上,故悲伤 d′ 最低、α=0 起点最高、α=1.5 回落 E7 §3.4
仿射-in-α 非线性的情感形变(谐波位置)表达不了,只能推理端修 §1.1
未见说话人 相对控制成立;绝对刻度、中立化深度、说话人身份漂移;低强度输入被抬向高强度(ESD 是高强度表演语音的先验) E3
α=0 乱说 14% 样本、强绑定说话人(0.04–0.44),未治愈 E4 §3.5
λ 残差参数化下正交正则有害(100k,n=1 种子) M22

8 参数与技巧

这一节会变。 上面是概念,这里是当前取值。

参数 当前值 备注
采样率 / hop 24 kHz / 480(1 帧 20 ms);输入与评测音频 16 kHz 模型固有
whisper 层 / 帧率 最后一层 / 50 Hz 未消融中层
Euler 步数 / 调度 10 / 余弦 —
推理 seed 1234 评测固定
lr / batch / clip 1e-5 / 8 / 1.0 全表见 04 §4.1
训练步数 420k(四段) 100k 后残差饱和,中立化 180k 后兑现
\(\lambda_\perp\) 1.0(主线);M22 显示 0 更优 是否切换待定
DTW 锚点 / 速率夹 250 ms / [0.4, 2.5] —
R3 块长 256 采样点 realtime 模式
α 网格(评测) 0 / .25 / .5 / .75 / 1 / 1.25 / 1.5(外推探针到 −1、2、3、5) —
推理链路 所有 α 走音高对齐混合:ODE 只生成两端,F0 对数域插值(α>1 同公式外推,不钳位、不线性相加),mel 域按 α 混合 §4.3
WORLD 16 kHz、帧移 20 ms、有声阈 50 Hz §4.3
评测集 ESD test 全量 zh 1276 / en 861 + 同批锚点(二轮 160 条为子集);CREMA-D 191 S7
主 ckpt outputs/m20d/ckpts/step160000.pt 累计 420k
GPU 卡 0–3 可用(2026-09-10 起;09-09 曾限卡 0) 训练时 8×1

9 旧版方法(TTS 线,v4–M16,2026-08 前)

DFD:在 TTS 管线上双前向构造情感方向(已由 M17 起的音频到音频架构取代)

以下为原方法页 §1–§3 原文,保留供对照。关键差异:旧方法有 LLM/token/参考前缀,\(A=v_{ ext{emo}}-v_{ ext{neu}}\) 在推理时构造、训练不监督; 新方法无文本,残差塔被训练,且训推同构。旧线的终点与失效原因见 M16 与 Graveyard。

1 问题的形式化

CosyVoice3 的 flow-matching 解码器把高斯噪声积分成梅尔谱:

\[\frac{dx_t}{dt} = v_\theta(x_t,\,t,\,c), \qquad x_0 \sim \mathcal{N}(0, I), \qquad x_1 = \text{mel}\]

其中 \(x \in \mathbb{R}^{1\times 80\times K}\)。积分结果是 mel 而非波形,波形还要过 HiFT 声码器 (相位由声码器生成,不在 \(x_1\) 里)。

DFD (Dual-Forward Decomposition) 在同一个 \(x_t\) 上跑两遍 DiT,一遍喂情感参考、一遍喂中性参考:

\[A := v_{\text{emo}} - v_{\text{neu}}, \qquad v_{\text{ctrl}} = v_{\text{neu}} + \alpha \cdot A\]

\(\alpha\) 即情感强度旋钮。

\(A\) 是构造出来的,不是学出来的

训练里没有任何损失直接监督 \(A\)(见 §3)。\(A\) 只在推理时被构造。 这一点决定了很多现象 —— 比如 \(L_{\text{ortho}}\) 为何完全无效(见 05 Findings)。

2 架构

flowchart TB
    subgraph LLM["LLM 通道 —— 情感的「源」"]
        RT["参考文本"] --> L
        TT["目标文本"] --> L
        RA["参考音频<br/>→ prompt speech token"] --> L
        L["LLM"] --> GT["gen_token<br/>长度 L_e"]
    end
    subgraph FLOW["Flow 通道 —— 情感的「闸门」"]
        GT --> MU["μ  (1,80,K)<br/>K = 2·L_e"]
        RA2["参考音频 mel<br/>→ 前缀位置"] --> COND["cond"]
        MU --> EST
        COND --> EST
        XT["共享状态 x_t"] --> EST["DiT estimator"]
        EST --> VE["v_emo"]
        EST --> VN["v_neu"]
        VE --> A["A = v_emo − v_neu"]
        VN --> A
        A --> VC["v_ctrl = v_neu + α·A"]
    end
    VC --> INT["∫ dt → mel"] --> HIFT["HiFT → 波形"]

2.1 ICL 的输入结构

零样本推理里参考占据前缀位置:

token = cat([prompt_token, gen_token])       prompt 来自参考音频
cond  = [参考的 mel | 生成段全零]              前缀有内容,生成段留空
spks  = 说话人嵌入,逐帧广播

三条容易记错的事实:

  1. prompt_text 是参考的文本,不是目标文本 —— 参考与目标文本解耦, 因而可以固定一组参考、任意替换目标文本。
  2. 输出要切掉前缀,只保留生成段。
  3. 各分支前缀可以不等长(各走各的,反正丢弃),但生成段必须等长才能逐帧混合。

2.2 张量形状与单位

量 形状 说明
\(x, v, \mu, \text{cond}\) \((1, 80, K)\) 80 = mel 频带
\(K\) — 帧数 @ 50 Hz (hop 480 @ 24 kHz),1 帧 = 20 ms
\(L\) — token 数 @ 25 Hz,1 token = 40 ms
换算 \(K = 2L\) token_mel_ratio = 2

2.3 共享状态 ≠ 输出平均

所有分支在同一个 \(x_t\) 上求速度(类似 CFG),混合速度后只积分一次。 不是各自积分完再平均波形 —— 后者会重影。这也是 \(\alpha>1\) 外推有意义的原因: 在同一点上做方向外推。

3 训练目标

\[\mathcal{L} = \mathcal{L}_{\text{FM}}(v_{\text{emo}}, u_{\text{emo}}) + \mathcal{L}_{\text{FM}}(v_{\text{neu}}, u_{\text{neu}}) + \lambda_{\text{ortho}} \mathcal{L}_{\text{ortho}}\]
  • 两个 FM 项各自完整、零截断,分别拟合情感样本与中性样本
  • 没有交叉项 —— 因此等价于普通的单分支 flow-matching 微调
  • \(\mathcal{L}_{\text{ortho}}\) 在 \(K_c = \min(K_{\text{emo}}, K_{\text{neu}})\) 上算 \(A\) 的跨通道余弦 —— 已证明完全无效(Findings F6)

\(\mathcal{L}_{\text{ortho}}\) 的确切形式

沿时间轴归一化(每个 mel 通道 → 一条时间序列的单位向量), 再取 \(80\times80\) 的通道两两余弦:

\[\hat{A}_i=\frac{A_i}{\|A_i\|+\epsilon},\qquad \mathcal{L}_{\text{ortho}}=\frac{1}{80^2}\sum_{i,j}\bigl(\hat{A}_i\cdot\hat{B}_j\bigr)^2\in[0,1]\]

既不是逐帧、也不是整体拉平 —— 三种口径的区别见 Glossary。

v3 曾用未归一化的 Gram \(\|AB^\top\|_F^2\),量级达 \(10^5\);在 \(\lambda{=}0.01\) 下 等效权重 \(10^3\),会劫持训练并搞崩 TTS。归一化正是为此。

3.1 训练时的实际采样与条件

以下均为 train_svd_flow_v4.py 的代码事实,与推理结构有本质差异,写论文时极易混淆:

项 训练实际 推理实际 代码位置
cond(参考 mel 占位) 全零 —— 训练时没有任何参考前缀 前缀塞参考 mel,生成段为零 build_flow_inputs L104: cond = torch.zeros_like(x_1)
token 来源 该录音自己的真实 speech token(预抽特征缓存);LM 一次都不跑 LM 生成一次,两分支共享同一串 gen_token,只有参考前缀不同 训练 dataset.py;推理 inference_svd_alpha_v4.py L176-182
时间 \(t\) \(t\sim\mathcal{U}[0.05,\ 0.95]\),两分支各自独立采样(--shared_t 默认关,v4 训练未开) Euler 网格覆盖 \([0,1]\),两分支同一 \(t\) cfm_perturb L110: t_min=0.05, t_max=0.95;L288-293
噪声 \(\epsilon\) 两分支各自独立 —— 且不可能共享:两条录音长度不同,同一张量无法匹配两个形状 生成段共享同一 \(z\) cfm_perturb 内 torch.randn_like(x_1)
说话人向量 逐录音提取(campplus 作用于该条音频),同说话人但两分支不同 各分支用各自参考的 build_flow_inputs L91
\(x_t\) 两分支来自不同录音的插值,必然不同 同一个 \(x_t\)(共享状态) —

推论:训练里的 A 和推理里的 A 不是同一个量

推理的 \(A=v_{\text{emo}}-v_{\text{neu}}\) 是「同一 \(x_t\)、同一 \(t\)、同一串 token,只换参考前缀」之差 —— 语义被抵消。训练的 \(A=v-B\) 是两次毫不相干的前向之差(不同录音/噪声/\(t\))。 训练日志里 cos_A_target 等诊断量测的是后者,不能当作推理量的性质引用。 另注意 \(t\in[0,0.05)\cup(0.95,1]\) 两段训练从未见过,而推理的 Euler 步会落在其中。

讨论