2026年9月30日 · AI · 4 分钟阅读

把 Qwen3-VL 微调成「看图猜谐音梗」选手:数据合成流水线的踩坑记录

多模态谐音梗理解任务的微调实践。重点不是模型怎么训,而是数据怎么合成——以及合成数据里那些看起来很对、其实很脏的样本。

AI 摘要

为什么是谐音梗

为什么是谐音梗

谐音梗(pun)理解是视觉-语言理解里一个很特殊的任务:它要求模型同时具备

  1. 图像识别能力(看到的是什么东西);
  2. 中文语音学知识(「梅」和「没」同音);
  3. 幽默感与推理能力(知道这是哪里好笑)。

前两个可以靠预训练补齐,第三个几乎是纯「品味」问题。所以微调要解决的核心问题是:怎么用可验证的方式教会模型品味。

数据流水线

整体流程:

图片爬取 → 人工/自动打标 → 谐音梗模板生成 → VLM 校验 → 难例挖掘 → LoRA 微调

1. 图片来源

我们没有用模型生成图片,而是从公开数据集里捞。图源有两个:

  • 谐音梗相关:直接带有 pun 标注的数据集;
  • 通用图片 + LLM 造梗:让 VLM 看着图想一个谐音梗。

第二条链路产量大、噪声也大,后面处理。

2. 模板生成

用 LLM 把「梗的文本」套进若干模板,生成 grounding 描述:

{
  "image": "spicy-hotpot.jpg",
  "pun_text": "锅气",
  "answer": "辣锅",
  "options": ["辣锅", "辣个锅", "啦个锅", "腊个锅"],
  "type": "谐音替换",
  "explanation": "「锅气」的「锅」与「辣锅」的「锅」同音,「气」谐音替换为「辣」。"
}

关键在选项构造——这是脏数据的主要来源。

我犯的三个错误

错误一:判重只比 lower()

最初我用 option.lower() == other.lower() 判重。结果:

  • A Bell 和 a bell 被当成两个不同选项;
  • T-shirt 和 T-SHIRT 被当成两个不同选项。

结果我造出了 594 条「4 个选项里有 2 个是正确答案」的脏题。模型训出来之后在这批题上准确率反而下降,我一度以为是过拟合。

正确做法是归一化后再比:

import re

def norm(s: str) -> str:
    return re.sub(r'[^a-z0-9]+', '', s.lower())

错误二:切分分组数不够

我最初用 i % 10 < 8 做 8:1

切分,看起来很合理。但我的数据按 kind: task_type 分组,总共只有 7 组。结果前 7 组全部落进 train,val 和 test 是空的。

train 里没有 val 字段这件事,直到我打印 len(val) 得到 0 才发现。

现在改用稳定散列:

import hashlib

def bucket(item_id: str, n: int = 10) -> int:
    return int(hashlib.md5(item_id.encode()).hexdigest(), 16) % n

切分必须保证 组数 > 分桶数,否则一定失效。

错误三:循环内变量没初始化

for item in items:
    if item.has_image:
        image_path = item.path
    if item.has_answer:
        answer = item.answer
    records.append({
        "image": image_path,   # 上一个没有图的 item 的图!
        "answer": answer,
    })

当某个 item 既没有图也没有答案时,变量沿用上一轮的残值。结果 metadata 里 16 条记录错指别人的资源。

修法很简单——每轮开头重置:

for item in items:
    image_path = None
    answer = None
    ...

清洗流程

最终我的清洗脚本做这几件事:

检查项手段拦截量
选项归一化后重复norm() 去重594 条
正确答案个数 ≠ 1计数1,203 条
图片路径不存在os.path.exists431 条
解析不出谐音关系LLM 复核2,880 条
文本过短 / 模板味过重长度 + 去模板化检测1,077 条

清洗前后:

原始:  62,417 条
清洗后:56,232 条(保留 90.1%)

训练配置

base_model: Qwen3-VL-8B-Instruct
method: LoRA
lora_rank: 32
lora_alpha: 64
lora_dropout: 0.05
target_modules: [q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj]
lr: 1e-4
scheduler: cosine
warmup_ratio: 0.03
epochs: 3
batch_size: 4 (grad_accum 8)
max_pixels: 1280 * 28 * 28

几个经验:

  • 必须冻结视觉编码器。我一开始没冻,显存直接爆,而且过拟合更严重(模型开始「记住」训练图而不是学谐音逻辑)。
  • max_pixels 要压。默认分辨率下 8B 模型的显存吃紧,降到 1280 * 28 * 28 之后刚好能塞进单张 80G 卡。
  • LoRA rank 32 足够。试过 64,过拟合明显变差,验证集准确率反而掉 1.8 个点。

结果

指标BaseLoRA
谐音梗选择准确率41.2%67.8%
谐音位置定位 IoU0.310.58
通用 VQA(未微调任务)68.5%67.9%

通用能力基本不掉,说明 LoRA 没有把模型带偏。

下一步

  • 难例挖掘:用当前模型推理全量数据,把「置信度低但 VLM 校验通过」的样本挑出来做第二轮训练;
  • 三语泛化:中文谐音梗依赖音韵,迁移到日语和英语的难度更大,需要先确认「谜面 → 谐音替换」这个模式本身能不能跨语言成立。

一点碎碎念

谐音梗这个任务有个很微妙的地方:模型很容易学到「表层规律」而不是「语言规律」。比如我们发现,如果正确答案总是四个选项里唯一带某种声调的,模型就学会了「找带 X 调的选项」。

为了堵这个洞,我们专门构造了一批对抗样本:把正确答案的声调随机改变,同时重新生成干扰项。这类样本在全量数据里占 3%,但模型在它们上的准确率从 71% 掉到 48%,说明表层规律的漏洞比我们想象的大。

数据脏一点不要紧,假装数据干净才是真的要命。

© Yuki · 本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
看板娘
ESC

输入关键词开始搜索

提示:↑ ↓ 选择 · Enter 打开 · Esc 关闭