JazerJu/qwen3-asr-ctc-aiinfra
Qwen3-ASR-CTC-aiinfra
一个 85.4M 参数的 Self-conditioned CTC 头,接在冻结的 Qwen3-ASR-1.7B 音频 编码器后面,做首遍转写和强制对齐。编码器不含在本仓库,运行时从 `Qwen/Qwen3-ASR-1.7B` 加载。
相对 `JazerJu/qwen3-asr-ctc`(v1, 48.3M,非 self-conditioned),本模型多做了两件事:
- 换成 Self-conditioned CTC(arXiv 2104.02724)+ Intermediate CTC 辅助损失;
- 在此基础上续训了 372 小时 AI 基础设施领域的中英混读语料,专门补 cann / Ascend C / 昇腾 / SGLang / HBM 这类术语。
领域续训没有以通用能力为代价。 FLEURS 11 个语种没有一个退步,15 个标准 测试集也全部持平或更好 —— 数字在下面。
和 v1 的架构差异:self_cond 必须跟着权重走
conditioning_layer(72468 → 512)参与推理前向,不是训练期的辅助结构: 它把第 2、4 个 block 之后的中间预测 softmax 后投影回主干、加到下一层输入上。
for i, block in enumerate(self.blocks):
x = block(x)
if self.self_cond and i in self.inter_layers: # (1, 3)
inter = self.ctc_lo(self.layer_norm(x))
x = x + self.conditioning_layer(F.softmax(inter.float(), -1).to(x.dtype))
return self.ctc_lo(self.layer_norm(x))照 v1 的结构建模型会静默出错:权重的键全都在,load_state_dict 少两个 unexpected key 就能过(把 strict 关掉的话),但少了这条通路,输出是错的。 config.json 里的 self_cond: true 和 inter_layers: [1, 3] 必须被读到。 本仓库的 modeling_ctc.py 已经按配置建图,直接用即可。
int4 量化后是 44.0 MB,不是 v1 的 25.1 MB —— 多出来的就是 conditioning_layer。
领域术语
自留的 955 段测试数据,47 个术语共出现 1,158 次(贪心解码,统计的是术语被 正确写出的次数):
单个术语(次数 ≥ 14):
zero 是唯一退步的:语料里 ZeRO(DeepSpeed 的显存优化)和数字 0 同音, 续训后模型更倾向写成 ZeRO。这是真实的退步,没有修。
通用能力:11 个语种一个不退
FLEURS 全量 7,876 句,基座与本模型同一次运行、同一套解码:
韩语那 −3.06 不是领域数据的功劳,是修标签修出来的:紧凑词表里有一类 「空格 + 半截 UTF-8 字节」的 token,训练时把它拆成独立空格 + 重新分词, 韩语含 U+FFFD 的句子从 280 句降到 38 句。
标准测试集(同环境):
长音频:可以直接按 30 秒切
同一段连续音频按固定窗口切开送进去的 CER:
中文语料几乎没有长句(AISHELL-1 超过 12 秒的占 0.01%、WenetSpeech 0.08%, 而 LibriSpeech 有 72.3%),只喂短段训出来的模型在 30 秒输入上会崩。 解决办法是把相邻字幕按 ≤0.6 秒的间隔合并成 8–20 秒的长段再训一遍 —— 不拼接音频,只把时间窗口拉长,中间的停顿原样保留。
训练
vocab_compact.json 是从 Qwen3 原生的 151,705 词表压出来的紧凑词表(72,468 类, 含 blank 与 unk),与 v1 同一份。
用法
pip install torch transformers qwen-asr safetensors soundfilefrom modeling_ctc import Qwen3CtcAsr
asr = Qwen3CtcAsr(".", device="cuda") # 会自动拉 Qwen/Qwen3-ASR-1.7B
print(asr.transcribe([waveform_16k_float32]))离线环境把本地编码器目录给 QWEN3_ASR_ENCODER。完整示例(含 CTC 强制对齐出 字级时间戳)见 example.py:
python example.py audio.wav三个会让你静默拿到错误结果的坑
前两个和 v1 一样,第三个是本模型新增的:
- 编码器输入是时间维拼接的 2D 张量
[128, ΣT_mel]加feature_lens, 不是[B, 128, T]。传 3D 会报split_with_sizes的错。
- 帧率是 13 fps(76.9 ms/帧),不是 Whisper/GLM 的 50 fps。 编码器用 3 层 stride-2 conv2d 做 8 倍降采样,但官方长度公式是「每 100 个 mel 帧出 13 帧」 (
qwen3_output_lengths())。沿用 50 fps 的算法会把可用帧数高估 4 倍 —— CTC 以为容量充足,实际 log_probs 只有 1/4 长,loss 看着能降但对齐全是错的。
- 必须打 attention mask 补丁。
qwen-asr0.0.6 里_prepare_attention_mask定义了却从来没被调用,cu_seq_lens_q/k只有 flashattention2 后端认。 不打补丁时同一条音频单条推理 vs 批推理的余弦相似度只有 0.81–0.88,打完 0.9998+。CUDA 上同样中招,不是昇腾特有的问题。patch_qwen3_attention_mask()是幂等的,Qwen3CtcAsr会自动调用。
- `self_cond` 必须建进图里,见上文。
已知局限
- `zero`(ZeRO)退步,基座 86% → 本模型 33%,见上表。
- 首遍写对的 `cann`,LLM 二遍会改回「看」。 在「CTC 首遍 + LLM 二遍」的 流水线里实测:首遍 105 次里写对 84 次(89%),二遍只剩约 21%。同一条链路上 ascend 95%、昇腾 84%、sglang 67% 都保得住,只有 cann 掉,因为它和高频字 「看」完全同音。要在最终文本里保住,得把热词匹配算出的改写直接应用到二遍 文本上,而不是只塞进 prompt。
- 贪心解码偶尔会吐出半个汉字(U+FFFD)。155 段自留测试集里有 1 段。加上 UTF-8 合法性约束的束搜索可以清零,同时 CER 从 0.1449 降到 0.1426。
- 训练语料里 TALCS / MAGICDATA / CS-Dialogue 的官方 test split 混进了训练集, 这三个 test 上的数字不能当泛化指标。上面列的测试集都实测确认干净。
- 中文时间戳没有真值可校,沿用 v1 的结论:CTC 尖峰式发射导致词起始系统性 偏晚约 100 ms、结束偏早约 80 ms,是常数,减掉即可。
ONNX
导出好的 ONNX(fp16 / q4 / q4f16)在 `JazerJu/glm-asr-ctc-bench` 的 qwen-ctc-aiinfra4/ 下,配套编码器在同仓库 qwen-ctc/。 导出与量化脚本:<https://github.com/JazerJu/Qwen3-ASR-CTC-GGUF>
许可
CTC 头权重按基础模型 Qwen3-ASR-1.7B 的 Apache-2.0 发布。训练语料各自的许可 归各自所有者,本仓库不含任何语料数据。
