Instructions to use czyhust/finetune_spk-sortformer with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- NeMo
How to use czyhust/finetune_spk-sortformer with NeMo:
# tag did not correspond to a valid NeMo domain.
- Notebooks
- Google Colab
- Kaggle
Sortformer V2:双流 Speaker-Aware 说话人日志
一、整体思路
在原始 Sortformer 的 18 层 Transformer Encoder 基础上,引入一条与主特征流(H1)并行的 speaker 特征流(H2)。两条流在每一层都做 self-attention 和互相之间的 cross-attention,让说话人表征与 diarization 表征持续交互。spk 分支有自己的输出头,用 chunk 级别的 PIL loss 单独监督。
spk encoder 的表征只在第一层之前作为输入,此后 H2 由每层的 H2 分支自己演化,不再重新注入。
二、网络结构
原始音频 (16kHz, 最长300s)
│
├──────────────────────────────┐
│ │
▼ ▼
Mel Spectrogram CAM++ Speaker Encoder(冻结backbone)
(128维, 10ms) Fbank → 卷积backbone → 帧级特征
│ → Conv下采样 → 192维@80ms
▼ │
FastConformer (17层, 512维) │
8x下采样 → 512维@80ms │
│ │
▼ ▼
Linear 512→192 H2 初始表征 (B,T,192)
│ │
▼ ▼
┌──────────────────────────────────────────┐
│ DualStream Transformer (18层, 192维) │
│ │
│ 每层 Block: │
│ H1: self-attn → H1/H2 互attn → FFN │
│ H2: self-attn → H1/H2 互attn → FFN │
│ │
└──────────────────────────────────────────┘
│ │
▼ ▼
主 Head (预训练权重) spk Head (随机初始化)
│ │
▼ ▼
ATS+PIL loss chunk级PIL loss
每层 DualStreamTransformerBlock 细节
输入: H1 (B,T,192), H2 (B,T,192)
1. H1 self-attention (first_sub_layer, 预训练权重)
H1 = LN1(H1 + MHA(H1,H1,H1))
2. H2 self-attention (h2_self_attn, 随机初始化)
H2 = LN1(H2 + MHA(H2,H2,H2))
3. 互 attention(无 gate, 随机初始化)
H1 = crossLN1(H1 + MHA(H1→H2)) # H1 attend H2
H2 = crossLN2(H2 + MHA(H2→H1)) # H2 attend H1
4. FFN
H1 = LN2(H1 + FFN(H1)) (second_sub_layer, 预训练权重)
H2 = LN2(H2 + FFN(H2)) (h2_ffn, 随机初始化)
所有 attention 均为 8 头(head_dim = 192/8 = 24)。
关键设计:H1 的参数名与原始 TransformerEncoderBlock 完全一致(first_sub_layer、layer_norm_1、second_sub_layer、layer_norm_2),因此原始 Sortformer checkpoint 可以无缝 warm-start 到 H1 流,新增的 H2 流和互 attention 保持随机初始化。
权重来源总结
| 模块 | 权重来源 |
|---|---|
| FastConformer encoder | 预训练 |
| H1 self-attn + FFN + LN | 预训练(同名加载) |
| H2 self-attn + FFN + LN | 随机初始化 |
| H1↔H2 互 attention ×2 + LN | 随机初始化 |
| 主输出 head | 预训练 |
| spk 输出 head | 随机初始化 |
| CAM++ 卷积 backbone | 预训练,永远冻结 |
| CAM++ 下采样 conv (proj) | 随机初始化,可训练 |
三、Speaker Encoder(CAM++)
提取流程
原始音频 (最长300s)
│
▼
切分为无重叠窗口:每窗口 2 秒(chunk_dur_sec=2.0, chunk_stride_sec=2.0)
│
▼ 每窗口独立处理(encode_batch_size=16 批量并行):
Kaldi Fbank (80维, 10ms) → CAM++ 卷积backbone(冻结)
→ 帧级特征 (512维, 20ms),每帧编码周围大范围上下文
│
▼
Conv1d(512→192, kernel=12, stride=4, padding=4) ← 可训练下采样层
→ 每个输出帧融合 240ms(12帧×20ms)上下文
→ 192维 @80ms 帧率
│
▼
窗口按时间顺序拼接 → (B, T_diar, 192),与 sortformer 帧率严格对齐
设计说明
- 不做池化:CAM++ 原始的 utterance-level TSTP 全局池化不适用于逐帧场景。改为可学习卷积下采样,让 H2 流自己的 self-attention 做时间整合。
- kernel=12(240ms):每个 H2 帧融合 240ms 的 CAM++ 帧级特征,比输出帧率(80ms)宽 3 倍,提供更丰富的局部上下文。
- proj 可训练:下采样卷积是新参数(~98K),随训练学习如何把帧级特征映射到 H2 空间;CAM++ backbone 冻结。
- 2s 窗口:与 CAM++ 训练时长一致,窗口间无重叠,显存可控。
四、训练算法
双损失
主分支: loss1 = 0.5 × ATS_loss + 0.5 × PIL_loss (原始 Sortformer 损失)
spk分支: loss2 = chunk级 PIL loss
总损失: loss = loss1 + spk_pil_weight × loss2 (spk_pil_weight=1.0)
Chunk 级 PIL Loss(spk 分支)
主分支沿用原始做法:整段拼接后一次匈牙利排列匹配。spk 分支不同——按 chunk_len=188 帧(15.04秒)切块:
spk_preds / targets 按 188 帧切成 n 个 chunk
每个 chunk 内部独立做 PIL 排列匹配 → 各自算 BCE
最后所有 chunk 的 loss 取平均
每个 chunk 是独立的排列问题,模型被强制在短窗口内做说话人区分,与流式推理场景一致。
两种训练模式
| 模式 | 配置 | 可训练参数 | 说明 |
|---|---|---|---|
| 只训新增 | freeze_base_model: true |
~13.4M | H2流 + 互attn + spk head + proj |
| 全量训练 | freeze_base_model: false |
~131M | 所有参数(除 CAM++ backbone) |
两种模式下 CAM++ 卷积 backbone 均冻结(proj 下采样层可训练)。
学习率分组(全量模式)
| 参数组 | 学习率 |
|---|---|
| 原始 Sortformer 参数 | lr=2e-5 |
| 新增 H2 流 + 互attn + spk head + proj | speaker_lr=1e-4 |
| CAM++ backbone | 冻结 |
五、流式模式
- 流式推理时每 chunk 处理
[spkcache | fifo | chunk]拼接序列 spkcache_spk/fifo_spk同步缓存对应帧的 speaker embedding,压缩时用相同的topk_indicesgather,保证 H2 与 H1 每帧时刻严格对齐- spk 分支(H2 → spk head)只用于训练监督,推理时只用主分支输出
六、超参数
新增超参数
| 参数 | 值 | 说明 |
|---|---|---|
use_dual_stream |
true | 启用双流架构 |
spk_pil_weight |
1.0 | spk 分支 chunk PIL loss 权重 |
freeze_base_model |
false/true | 只训新增 / 全量训练 |
speaker_lr |
1e-4 | 新增参数学习率(全量模式) |
speaker_encoder.chunk_dur_sec |
2.0 | spk encoder 窗口长度(无重叠) |
speaker_encoder.chunk_stride_sec |
2.0 | 窗口步长(=窗口长,无重叠) |
speaker_encoder.encode_batch_size |
24 | 多窗口批量并行 |
speaker_encoder.downsample_kernel |
12 | 下采样卷积核(240ms 上下文) |
speaker_encoder.checkpoint_path |
null | 权重从 warm-start nemo 加载,不硬编码 |
沿用超参数
| 参数 | 值 |
|---|---|
| 采样率 | 16000 |
| 最大说话人数 | 8 |
| 最大音频时长 | 300s |
| batch size | 1 |
| 优化器 | AdamW (β=[0.9,0.98]) |
| 基础学习率 | 2e-5 |
| 权重衰减 | 2e-3 |
| 最大 epoch | 10 |
| chunk_len | 188 (15.04s) |
| spkcache_len | 376 |
| 因果注意力概率 | 0.5 (rc=7) |
| dropout (attn/ffn) | 0.5 |
7. 文件结构
fintune_sortformer_v2/
├── docs/ARCHITECTURE.md ← 本文档
├── checkpoints/
│ └── dual_stream_init_8spk.nemo ← 初始化nemo(含全部权重)
├── src/
│ ├── finetune_pipeline/
│ │ ├── conf/
│ │ │ └── streaming_sortformer_diarizer_8spk-v1-finetune.yaml
│ │ │ ← use_dual_stream, spk_pil_weight, freeze_base_model, speaker_lr
│ │ └── scripts/
│ │ ├── init_dual_stream_nemo.py ← 生成含spk encoder权重的nemo
│ │ └── ckpt2nemo.py ← ckpt转nemo(含全部新权重)
│ └── third_party/nemo/collections/asr/
│ ├── models/sortformer_diar_models.py ← 双流接入、spk head、chunk PIL loss、两种训练模式
│ └── modules/
│ ├── speaker_encoder.py ← 冻结CAM++ backbone + 可训练Conv下采样
│ ├── sortformer_modules.py ← spk cache 追踪(spkcache_spk/fifo_spk)
│ └── transformer/
│ ├── dual_stream_transformer.py ← 双流block + encoder
│ └── transformer_encoders.py ← 原始单流(use_dual_stream=false时用)
└── wespeaker/ ← CAM++ 模型
8. 初始化与训练流程
# 1. 生成初始化 nemo(H1=预训练, H2/互attn=随机, CAM++=冻结, proj=随机)
python src/finetune_pipeline/scripts/init_dual_stream_nemo.py \
--pretrained /path/to/original_sortformer.nemo \
--output /path/to/output_dual_stream.nemo
# 2. 训练时 warm-start 这个 nemo
bash run_train.sh --init_nemo_path /path/to/output_dual_stream.nemo
# 3. 训练后 ckpt 转 nemo(ckpt 已含全部权重)
python src/finetune_pipeline/scripts/ckpt2nemo.py \
/path/to/checkpoint.ckpt /path/to/output.nemo