# 5.11 用你自己的“默认输出 → 改过的版本”给小模型训 LoRA

这份代码回答一个问题：模型默认写出来的东西，你每次都要改成自己的样子；把这些“默认输出 → 你改过的版本”攒成配对，拿来给一个小模型训 LoRA，它能不能学会你的改法，又该怎么判断它学会了没有。

整个配方分三步：

1. 准备配对数据：每一对是一段默认输出（`ai`）和你改过的版本（`original`）。
2. 训 LoRA：Mac 上用 MLX（`train_mlx.py`），NVIDIA 显卡上用 PEFT（`train_peft.py`）。底座模型不动，只训练一小组附加权重，存下来几十 MB。
3. 用训练时没见过的例子评估（`evaluate.py`）：和“不训练、把几组例子直接写进上下文”的基线比，看谁更接近你改过的版本；生成一份 A/B 顺序打乱的盲评表让你自己判断；可选地再请一个 API 模型盲评，并检查有没有改动原意。

## 示例数据

`data/` 里有一份现成的示例数据，用的是 5.11 文风 LoRA 那种“反向合成”做法：拿一位作者的原文，让大模型用它默认的写法把每段改写一遍，得到“AI 默认写法 → 作者原文”的配对。作者选了鲁迅（《朝花夕拾》和《呐喊》的 23 篇，已进入公有领域，原文取自维基文库并转成简体），默认写法由 AI Builder Space 上的 deepseek 生成。训练 455 对、验证 20 对、留出测试 20 对。生成过程见 `make_demo_data.py`，你不需要重跑。

## 需要什么设备

- Apple Silicon Mac：用 MLX，16 GB 内存可以训默认的 Qwen3-1.7B；8 GB 内存的 Mac 换 `--model Qwen/Qwen3-0.6B`。
- 一张 NVIDIA 显卡：用 PEFT，默认模型需要约 6 GB 显存（我们在 RTX 5090 上测到峰值 5.3 GB）。

`--judge` 盲评要用 AI Builder Space 的 key。

## 安装与运行

Mac：

```bash
cd 5_11_lora
uv venv && source .venv/bin/activate      # 没有 uv 就用 python3 -m venv .venv
uv pip install -r requirements-mac.txt
python train_mlx.py                        # 约 3 分钟（M3 Ultra），LoRA 存到 adapters/mlx/
python evaluate.py --backend mlx           # 生成留出例子的三种版本、相似度和盲评表
export AI_BUILDER_TOKEN=sk_你的key
python evaluate.py --backend mlx --judge gpt-5    # 另外请 gpt-5 盲评一遍
```

NVIDIA 显卡：

```bash
uv pip install -r requirements-cuda.txt
python train_peft.py                       # LoRA 存到 adapters/peft/
python evaluate.py --backend torch
```

为什么用 Qwen3 而不是模块 5 其他地方用的 Qwen3.5：Qwen3.5 的一部分层是线性注意力，目前在 MLX 上训练很慢（我们测到每秒 42–53 个 token，Qwen3-1.7B 约 600），还遇到过一次 macOS 报 GPU 卡死而中断。

## 该看到什么

我们的样例输出在 `sample_output/`（2026-10-23）：训练日志、评估结果、盲评表和答案。

Mac Studio（M3 Ultra，MLX）上：验证集损失从 2.46 降到最低 0.51（第 200 步），训完 450 步是 0.55。留出的 20 条上：

| | 与原文的逐字相似度 | 与默认输出的相似度（越低改得越多） | 比不改更接近原文的条数 |
|---|---|---|---|
| 默认输出本身（不改） | 0.895 | 1.000 | — |
| 基线：3 组例子写进上下文，不训练 | 0.785 | 0.875 | 0/20 |
| LoRA | 0.892 | 0.970 | 8/20 |

一张 RTX 5090 上用 PEFT 训同一份数据（2 遍，134 秒），验证集损失从 2.19 降到 0.50。留出的 20 条上，与原文相似度：默认输出 0.895，基线 0.797，LoRA 0.892；LoRA 与默认输出的相似度 0.976，比不改更接近原文的有 5/20 条，逐条比基线更接近原文 16/20。gpt-5 盲评判 LoRA 更接近原文写法 18/20，判为改动了原意 LoRA 6/20、基线 11/20。

PEFT 这一版改得比 MLX 那一版更保守（“伊”一次也没出现）。主要原因是 LoRA 的缩放系数不同：`train_peft.py` 用的是常见的 `lora_alpha = 2 × rank`，相当于缩放 2；mlx-lm 默认的缩放是 20。想让 PEFT 改得更大胆，可以在 `train_peft.py` 里把 `lora_alpha` 调大，或者多训几遍。

这张表要和下面两样一起看才说得清：

- **LoRA 学到的是小而具体的写法习惯。** 默认输出本来就和原文很像（相似度 0.895），LoRA 只改了很少的地方，但改的方向是对的。几个鲁迅常用、默认输出里几乎没有的写法，出现次数（默认输出 / 基线 / LoRA / 原文）：“伊” 0 / 0 / 3 / 4，“的说” 1 / 1 / 4 / 2，“罢” 1 / 1 / 4 / 3。把例子写进上下文的基线没学到这些，它要么几乎照抄，要么改写得离原文更远。
- **盲评。** gpt-5 在看得到原文的情况下，判 LoRA 那一版更接近原文写法的有 19/20 条。但它也判 LoRA 有 6/20 条改动了原意（基线是 13/20）。打开盲评表能看到具体的错，比如“我这才回忆到前几天的一件事”被改成“我才知道前几天的一件事”（回忆变成了得知），“便将饭篮在桌上一摔”漏掉了“在桌上”。这就是 5.11 说的事实漂移：微调改的是写法，它不保证意思不变，用之前要核对。

字面相似度在这份数据上不太敏感，因为默认输出本来就接近原文；真正的判断要靠盲评。换成你自己的数据时也一样：先自己做一遍盲评表，再看数字。

## 换成你自己的数据

把你的配对写成三个文件，放在一个目录里（例如 `my_data/`），每行一个 JSON：

```json
{"ai": "模型默认写出来的一段", "original": "你改过的版本"}
```

- `pairs_train.jsonl`：训练用，几百对比较合适，少于一百对效果很难看出来；
- `pairs_valid.jsonl`：训练时看损失用，十几二十对；
- `pairs_test.jsonl`：评估用，十几二十对，**不能和训练集重复**。

然后：

```bash
python train_mlx.py --data my_data          # 或 train_peft.py --data my_data
python evaluate.py --backend mlx --data my_data --markers "你常用的词1,你常用的词2"
```

配对从哪来：最直接的是把你平时改过的稿子攒下来，一边是模型给你的初稿，一边是你最后发出去的版本。攒不到那么多，可以学示例数据的做法：拿你自己写过的东西，让模型用它的默认写法改写一遍，配成“模型写法 → 你的原文”。提示格式（`common.py` 的 `INSTRUCTION`）在训练和推理时必须一致，要改就两边一起改。

盲评表在 `results/` 下的 `*_blind.md`，答案在同名的 `*_key.json`。先自己选完再看答案。
