# 用一台 16GB 显卡，把「我喜欢什么样的视频」教给 4B 视觉模型

> 发布于：2026-08-29 | 分类：AI实践 | 原文：https://www.imbailey.cn/post/qwen3-vl-4b-sft-personal-video-preference

这篇是一份实验记录。目标不是做通用内容审核，也不是训练一个公开分类器，而是：在自己的片库里，用已经打过的 1–5 分，让一个小视觉语言模型学会**我的口味**——看完几张关键帧之后，能给出结构化观察，顺便再补一个分。

先交代清楚：机器是 RTX 4060 Ti 16GB，整次监督微调跑下来大约一个半小时。这个规模能出什么、出不了什么，下面全是真实数据和结论。

## 背景

我这边有一套本地片库流水线：抽关键帧、用冻结的视觉编码器做向量，再在上面接一个很小的回归头去拟合人工分数。这条路对「整体像不像我喜欢的东西」有用，但对**为什么喜欢 / 为什么不喜欢**几乎是黑盒。

后来想加一层更可读的判断：模型先把画面写成结构化观察（人物、构图、场景、时长线索、和口味卡的吻合/偏离），再给出 1–5 分。分数仍然以人工标注为准，观察文字只当作推理过程，而不是让模型自己发明标签。也就是让模型「开口说人话」，而不是直接甩一个黑盒分数。

数据规模不大：全库大约 3700 条已评分样本。5 分很少，1/2 分很多，直接全量训练会被低分短片淹没。所以这次先做 **SFT**，样本是刻意配过的。

## 为什么选 Qwen3-VL-4B-Instruct

候选其实不少：更小的 2B、更大的 8B/9B、Thinking 系列，以及各种「去拒绝」改版。最后锁的是官方 **Qwen3-VL-4B-Instruct**。理由不太玄学，都是踩点之前想清楚的：

- **4B 能在 16GB 上用 QLoRA 跑多图。** 8B 多图会很紧，2B 又容易把细的构图差写糊。
- **Instruct 而不是 Thinking / Base。** 这次要的是稳定的 JSON 观察，不是长篇思维链；Base 还要再做指令对齐。
- **原生多图，而不是拼贴。** 官方 VL 可以直接吃 6 张按时间排列的帧，并带上时间戳，不必把视频压成一张拼图。
- **不用第三方 Uncensored / abliterated 权重当底模。** 这类权重往往先伤视觉指令跟随。片库本身偏私人向，观察需要写得具体，但这是数据和提示词的事，不该靠来路不明的拒答消融来解决。官方 Instruct 加上写清楚的观察样本，足够让模型按格式输出。

训练加载用的是 Unsloth 的 4bit 路径；权重本身来自官方 Instruct。

## 数据怎么做

### 标签

分数是我自己打的 1–5，不另请模型当评委。分布大致是：低分很多、中高分少、5 分最少。这很像真实片库：随手划走的短片远多于反复看的长片。

一个很稳的信号是时长。低分里大量是几十秒的切片，高分中位往往是完整过程、业余机位、同一空间拍很久。模型如果只看文件名或封面，会学歪，所以训练时关键帧是主输入，文件名和专辑名会随机丢掉。

### 抽样

没有把 3700 条全送进 SFT。策略是：

- 4 分、5 分：**全要**（约 871 + 258）
- 1 / 2 / 3 分：各抽 **258** 条，和 5 分数量对齐，避免模型只会说「低分」
- 合计 **1903** 条观察样本

划分沿用已有的 train / val / test，缺划分的用固定种子补上。最终大约是 train 1363、val 271、test 269。test 全程不碰。

### 关键帧

每个视频最多取 **6** 帧，按时间均匀抽，带时间戳。短片可能只有 1–2 帧，这本身就是特征：信息不够、过程不完整。图像在训练里把长边收到 384，主要为了 16GB 显存。

不用拼贴图。提示词里会写「图1=12.3s，图2=…」，让模型和跨帧变化对齐。

### 观察文本

观察不是「好看 / 不好看」。每条样本是一份固定字段的 JSON，大致包括：人数、脸是否可见、体型与发型、服装、场景、镜头、动作、画质、跨帧发生了什么、匹配点、偏离点、倾向（high / mid / low）、一段 90–150 字的叙述，最后才是人工分数。

写观察时有几条硬规则：

- 只写画面里看得到的东西，文件名最多当弱提示
- 4 分不要写成 5 分：对得上口味但缺脸、偏短、棚感更重，倾向用 mid
- 低分要把「为什么不像我喜欢的」写具体，而不是一句「不符合」
- 文本侧做 dropout：大约一半样本不给文件名，三成左右不给专辑名，逼模型看图

这样 SFT 学的是「看帧 → 写观察 → 对齐我的分数」，而不是背标签词。

## 训练怎么做

环境在 WSL2 里，框架是 **Unsloth + TRL SFTTrainer**，模型接口是 `FastVisionModel`。视觉塔冻住，只在语言侧挂 LoRA。多图数据用 Unsloth 的 vision collator。

主要设置如下。

| 项 | 值 |
| --- | --- |
| 底模 | Qwen3-VL-4B-Instruct（4bit QLoRA） |
| 显卡 | RTX 4060 Ti 16GB |
| LoRA rank / alpha | 16 / 16 |
| 视觉层 | 冻住 |
| 可训参数 | 约 39.3M / 4.48B（0.88%） |
| 每设备 batch | 1 |
| 梯度累积 | 8（有效 batch = 8） |
| 学习率 | 1e-4，cosine |
| epoch | 2 |
| 最大序列长度 | 4096 |
| 图像长边 | 384 |
| 最多帧数 | 6 |
| 优化器 | adamw_8bit |
| 精度 | bf16 |

2 个 epoch 一共 342 step，墙钟大约 **1 小时 26 分**。

没有上全量微调，也没有一开始就做 DPO。先确认小模型能稳定吐出观察 JSON，再考虑偏好对。

## 结果

### 训练过程

这是一轮小样本 SFT，先看收敛：

- 训练 loss：约 **3.64 → 0.75**（全程均值约 1.02）
- 验证 loss：epoch 1 **0.805**，epoch 2 **0.764**
- 后半段训练 loss 在 0.74–0.78 附近晃，没有散掉

loss 能降，说明格式和「观察 + 分数」模型接住了。真正要看的是没进训练的 test。

### 测试集（269 条，训练时完全留出）

协议：用和训练相同的多图提示，按帧数分组做 batch 解码（默认 4 条一批），最长生成 256 token。墙上大约 **61 分钟**，合下来约 **16 秒/条**，比逐条 generate 的 40–80 秒快不少。

一个实现上的坑：观察 JSON 比较长，`score` 又在最后一个字段。256 token 经常在写完 `lean` 之后被截断，于是 **269 条里只有 136 条解析出了完整分数**（133 条 JSON 不完整）。`lean` 几乎都能读到（268/269），所以下面分数指标只在 136 条上算，倾向在全量上统计。

**打分（136 条完整 JSON）**

| 指标 | 数值 |
| --- | --- |
| MAE | 1.08 |
| RMSE | 1.42 |
| 完全一致 | 30.9% |
| 误差 ≤ 1 分 | 67.7% |
| 「喜欢」≥4 的精确率 / 召回 | 0.74 / 0.38 |

按真实分数拆开：

| 真实分 | 条数 | 完全一致 | MAE |
| --- | --- | --- | --- |
| 1 | 25 | 16% | 1.36 |
| 2 | 17 | 53% | 0.53 |
| 3 | 25 | 36% | 0.76 |
| 4 | 54 | 37% | 1.07 |
| 5 | 15 | 0% | 1.80 |

预测分布明显往中间靠：完整样本里预测 5 分只有 1 次，真实 5 分 15 条全部被打低——模型不敢轻易给满分配。混淆矩阵（行 = 真实 1–5，列 = 预测 1–5）：

```
      pred 1  2  3  4  5
true 1     4 12  5  4  0
     2     3  9  4  1  0
     3     2 10  9  3  1
     4     2 20 12 20  0
     5     1  1  7  6  0
```

**倾向 lean（268 条）**

模型更爱说 mid（166），low 70，high 32。真实 5 分里 high 只有 9 条，mid 20 条；真实 4 分也是 mid 占大多数。它能把「不太像」和「有点像」分开一点，但几乎不敢给满分配。

可以这么读：SFT 之后模型会按格式写观察，分数是**粗排序**（差 1 分大约七成），不是精确打分器。高分段偏保守，中间分（2–4）相对稳，5 分几乎没学会。截断也说明生成预算要加长，或者把 `score` 挪到 JSON 最前面，下次评测会完整得多。

另外做了一个很小的本地推理服务：加载 4bit 底模 + LoRA，提供 `/score` 和一份 OpenAI 风格的 `/v1/chat/completions`。第一次推理会慢一些（编译），后面会好一点。若要进 LM Studio 这类客户端，还需要把 LoRA 合并后再导出 GGUF，并配上官方的视觉投影文件；adapter 本身不能直接丢进去。

## 几点体会

1. **先配数据，再谈模型。** 低分短片如果按原比例灌进去，模型会变成「见短片就打低分」的机器。把 4/5 分保全、低分等量抽样，比换 8B 更重要。
2. **观察要比形容词有用。** 「匹配 / 偏离」写成可核对的画面事实，后面才好做 DPO 或人工抽查。
3. **16GB 够用，前提是冻视觉、限分辨率、限帧数。** 这次没有动视觉塔，多图仍然能训。
4. **别把去拒答改版当捷径。** 对 VL 来说，干净的官方 Instruct 加上写明白的样本，比来路不明的消融权重更可控。

下一步：加长生成或把分数放到 JSON 最前，把 269 条分数补全；抽一批观察做人工核对；再考虑很轻的 DPO。SFT 只是让模型学会按我的标准开口，真正贴口味，还得拿成对样本一点点打磨。
