# Balabit 预训练 + Fine-tune 重构设计

**日期**：2026-05-10
**作者**：Claude + 用户
**状态**：待用户评审

## 1. 背景与目标

### 1.1 问题
当前 `TrajectoryFlowModel` 仅用 605 条用户采集数据训练，生成质量差：
- **空间锯齿**：lateral 方向高频抖动（见 verify 页面截图）
- **时间模板化**：5 条生成轨迹的 Δt 曲线几乎完全重合（多样性丧失）
- 反检测能力弱

### 1.2 根本原因
1. **数据量不足**（605 条），模型欠拟合 + 记噪声
2. **后处理过度**：[generator.py](../../../ai_mouse/generator.py) 中的 `speed_profile`（确定性钟形曲线）和 `median±1.1` 硬 clip 把多样性压扁

### 1.3 目标
- 用 Balabit Mouse Dynamics Challenge 数据集（公开真实用户会话）做预训练
- 用现有 605 条做 fine-tune 适配本任务分布
- 重写后处理，让真人数据本身的速度模式自然显现
- 输出量化评估报表，便于迭代判断进步

### 1.4 不在范围（YAGNI）
- ❌ 滚轮模型 `ai_mouse/scroll/`（独立子系统，本次冻结不动）
- ❌ 模型架构变更（仍是 `TrajectoryFlowModel`，不换 Diffusion——CPU 推理约束）
- ❌ 对抗训练 / Discriminator（验证完基础方案再说）
- ❌ 浏览器插件、其他公开数据集
- ❌ 前端 UI 变更

## 2. 整体架构

```
Balabit raw sessions (CSV)
        │
        ▼   [新] ai_mouse/data_adapters/balabit.py
   click 锚定切分（Pressed 事件前 W ms 的 Move）
        │
        ▼
  data/pretrain_traces.jsonl   ← 与现有 traces.jsonl 格式 100% 兼容
        │
        ▼   [改] ai_mouse/trainer.py（streaming dataloader、resume_from）
  data/models_v2_pretrained/   ← 预训练 checkpoint（Balabit only）
        │
        ▼   [改] trainer.py 加 --resume-from 支持加载已有权重
  data/models_v2/   ← 605 条 fine-tune 后的最终权重（部署用）
        │
        ▼   [改] ai_mouse/generator.py（砍硬模板后处理）
  推理：CPU < 200ms

  评估：[新] ai_mouse/eval/
        ▼
  data/eval_reports/YYYY-MM-DD-<tag>.md   ← 量化指标 + 直方图 + FFT
```

**关键设计原则**：
- **数据格式不变**：Balabit 转换后产出和现有 `traces.jsonl` 格式 100% 一致。`load_and_prepare_data` / 旋转坐标系 / 6× 增强逻辑全部不动
- **训练流程加一个阶段**：原来"605 → train → models_v2"，现在变成"Balabit → pretrain → models_v2_pretrained → 605 fine-tune → models_v2"
- **滚轮子系统完全不动**

## 3. 文件变更清单

### 3.1 新增

| 路径 | 用途 |
|---|---|
| `ai_mouse/data_adapters/__init__.py` | 包初始化 |
| `ai_mouse/data_adapters/balabit.py` | Balabit CSV → traces.jsonl 适配器 + CLI |
| `ai_mouse/eval/__init__.py` | 包初始化 |
| `ai_mouse/eval/metrics.py` | 运动学指标计算（速度/加速度/jerk/FFT） |
| `ai_mouse/eval/report.py` | Markdown 报表生成 |
| `ai_mouse/eval/__main__.py` | CLI: `python -m ai_mouse.eval` |
| `ai_mouse/__main__.py` | CLI: `python -m ai_mouse train ...`（统一入口） |
| `tests/test_balabit_adapter.py` | 适配器单元测试 |
| `tests/test_eval_metrics.py` | 指标计算正确性测试 |

### 3.2 修改

| 路径 | 改动 |
|---|---|
| `ai_mouse/trainer.py` | 加 `resume_from` 参数；增强从一次性 numpy 改成 Dataset 内 on-the-fly；可选 AMP（有 GPU 时） |
| `ai_mouse/generator.py` | 砍 `speed_profile` 与 `median±1.1` clip；新增 5 点高斯 lateral 平滑 |
| `ai_mouse/config.py` | 新增 `PretrainConfig`、`FinetuneConfig`、`BalabitAdapterConfig`、`EvalConfig` |
| `tests/test_generator.py` | 适配新后处理 |
| `tests/test_trainer.py` | 加 resume_from 测试 |

### 3.3 冻结不动

`ai_mouse/scroll/*`、`ai_mouse/coord.py`、`ai_mouse/models.py`、`ai_mouse/utils.py`、`ai_mouse/collector.py`、`ai_mouse/server/*`、`static/*`、`tests/test_scroll_*.py`、`tests/test_coord.py`、`tests/test_models.py`、`tests/test_server.py`

## 4. Balabit 适配器（`ai_mouse/data_adapters/balabit.py`）

### 4.1 输入格式
Balabit Mouse Dynamics Challenge 每个 session 是 CSV 文件，列：
```
record timestamp, client timestamp, button, state, x, y
```
- `state` ∈ {`Move`, `Pressed`, `Released`, `Drag`, `Scroll`}
- `button` ∈ {`NoButton`, `Left`, `Right`}

### 4.2 切分逻辑（click 锚定）
1. 扫描 session 所有事件
2. 找每个 `Pressed` 事件 P
3. 回溯前 W ms（默认 `W=1200`）内的所有 `Move` 事件，构成一段 trace
4. 段的 `start = 第一个 Move 的 (x,y)`，`end = P 的 (x,y)`
5. 时间戳归零（第一个 Move 的 t=0）

### 4.3 过滤规则
**丢弃整段**（不修复，直接丢弃这一条 trace），满足任一条件即丢：
- Move 事件数 < 5
- `dist(start, end) < 50` px
- 时间跨度 > 5000ms（避免长停顿）
- `start` 或 `end` 任一坐标 < 0 或 > 5000（避免跨屏瞬移异常值）
- 总弧长 < 50 px（避免抖动残留）
- 段内任意相邻 Move 之间时间差 > 200ms（采样断档，宁可整段丢也不要含断点的样本）

### 4.4 输出
追加到 `data/pretrain_traces.jsonl`，每行：
```json
{"meta":{"start":[x,y],"end":[x,y],"dist":int,"angle":float,"source":"balabit","session_id":"user7_session_42"},"events":[{"type":"move","x":int,"y":int,"t":int}, ...]}
```

**关于 click events 的兼容性**：Balabit 转换后的 trace **不包含** `down`/`up` 事件（预训练只学移动）。这与现有 `trainer.py:139-142` 的逻辑兼容——`load_and_prepare_data` 用 `if downs and ups` 检查，没有就跳过。后果：
- 预训练阶段产出的 `click_dist.json` 会基于零样本，**写入默认值**（mu=80, sigma=30）
- Fine-tune 阶段重新基于 605 条产出真正的 `click_dist.json`，覆盖默认值
- `flow_model.pt` 和 `train_config.json` 是预训练真正要保留的产物

### 4.5 CLI
```bash
uv run python -m ai_mouse.data_adapters.balabit \
  --input /path/to/balabit/sessions/ \
  --output data/pretrain_traces.jsonl \
  --window-ms 1200
```

## 5. 训练 pipeline 改造（`ai_mouse/trainer.py`）

### 5.1 新增参数
```python
def train(
    data_path: Path,
    output_dir: Path,
    epochs: int = 300,
    batch_size: int = 64,
    lr: float = 3e-4,
    seq_len: int = 64,
    progress_callback: Callable[[dict], None] | None = None,
    config: TrainConfig | None = None,
    resume_from: Path | None = None,   # 新增：加载已有权重
    use_amp: bool = False,              # 新增：mixed precision
) -> None:
```

### 5.2 Dataset 改造
当前 `_augment` 是**一次性把全部数据 6× 复制到内存**。Balabit 几万条样本 × 6 后内存占用大。改为：

```python
class TrajectoryDataset(torch.utils.data.Dataset):
    def __init__(self, seq, cond, augment: bool = True):
        self.seq = seq
        self.cond = cond
        self.augment = augment
        self._n_aug = 6 if augment else 1
    
    def __len__(self):
        return len(self.seq) * self._n_aug
    
    def __getitem__(self, idx):
        base_idx = idx // self._n_aug
        aug_id = idx % self._n_aug
        seq, cond = self.seq[base_idx], self.cond[base_idx]
        return self._apply_augment(seq, cond, aug_id)
```

每次 `__getitem__` 即时增强，内存占用 = 原始数据 1×。

### 5.3 Resume from checkpoint
```python
if resume_from is not None:
    state = torch.load(resume_from / "flow_model.pt", weights_only=True)
    model.load_state_dict(state)
```

如果 fine-tune 阶段的 `cond_dim` 与预训练不一致，需要明确报错。

### 5.4 完整两阶段训练流程
```bash
# 阶段 1：预训练
uv run python -m ai_mouse train \
  --data data/pretrain_traces.jsonl \
  --output data/models_v2_pretrained \
  --epochs 200 --lr 3e-4 --batch-size 128

# 阶段 2：微调
uv run python -m ai_mouse train \
  --data data/traces.jsonl \
  --output data/models_v2 \
  --epochs 50 --lr 1e-5 --batch-size 64 \
  --resume-from data/models_v2_pretrained
```

### 5.5 服务端 API
`POST /api/train` 当前不支持 resume。**本次不改服务端 API**——CLI 是主要预训练入口，UI 上的"训练"按钮仍用于 605 条的 fine-tune（默认会自动从 `models_v2_pretrained` resume，如果存在）。

逻辑：
- `models_v2_pretrained/flow_model.pt` 存在 → fine-tune 模式（lr=1e-5, epochs=50）
- 不存在 → 走原逻辑（from scratch）

## 6. Generator 改造（`ai_mouse/generator.py`）

### 6.1 砍掉
- **lines 263–271** `max_allowed = median + 1.1`、`min_allowed = median - 1.1` 硬 clip 整段
- **lines 273–286** `speed_profile` 整段（acceleration/deceleration phase）

### 6.2 保留
- 端点 snap (lines 220–229)
- 起点 lateral 衰减 (lines 232–236)
- forward 单调性强制 (lines 239–246)
- log_dt 安全 clip [0, 5] (line 255)
- click duration 采样

### 6.3 新增：lateral 5 点高斯平滑
```python
# 在 lateral monotonic fix 之后、decode_trajectory 之前
def _gaussian_smooth(x: np.ndarray, sigma: float = 1.0) -> np.ndarray:
    """5-point gaussian smoothing, preserving endpoints."""
    kernel = np.exp(-0.5 * (np.arange(-2, 3) / sigma) ** 2)
    kernel /= kernel.sum()
    smoothed = np.convolve(x, kernel, mode="same")
    smoothed[0] = x[0]   # preserve endpoint
    smoothed[-1] = x[-1]
    return smoothed

lateral = _gaussian_smooth(lateral, sigma=1.0)
```

只对 lateral 平滑，**不对 forward 平滑**（forward 已经被单调性约束保护，再平滑会损害准确性）。

### 6.4 预期效果
- 解决高频锯齿（lateral 平滑）
- Δt 多样性恢复（不再被压扁到 median ± 1.1）
- 速度模式由模型自己决定（learned from Balabit real data）

## 7. 评估模块（`ai_mouse/eval/`）

### 7.1 指标（`metrics.py`）

每条轨迹计算：
- **速度** `v[i] = sqrt(dx² + dy²) / dt`（单位 px/ms）
- **加速度** `a[i] = (v[i+1] - v[i]) / dt`
- **Jerk** `j[i] = (a[i+1] - a[i]) / dt`
- **Δt 序列**

聚合到样本集：
- 速度/加速度/jerk 的均值、std、变异系数 CV、p25/p50/p75/p95
- Δt 分布的 CV
- **FFT 频谱**：每条轨迹 lateral 信号做 FFT，看 4–12Hz 频段是否有 peak（生理震颤）
- **多样性**：N 条样本之间的 PCA 方差贡献（衡量是否模板化）

对比"参考分布"（从 `pretrain_traces.jsonl` 随机抽 1000 条作为 holdout）：
- 速度/加速度分布的 KL 散度（直方图离散化估计）
- jerk 分布的 KL 散度

### 7.2 报表（`report.py`）

输出 Markdown 到 `data/eval_reports/YYYY-MM-DD-<tag>.md`，结构：

```markdown
# Eval Report: <tag> (2026-05-10 18:30)

## 模型信息
- Checkpoint: data/models_v2/flow_model.pt
- 训练参数: ...

## 摘要
| 指标 | 生成 | 参考 | 评价 |
|---|---|---|---|
| 速度 KL | 0.12 | 0 | OK |
| FFT 4-12Hz peak | 8.3Hz @ 0.04 | 7.1Hz @ 0.05 | OK |
| Δt CV | 0.45 | 0.52 | 接近 |
| ...

## 直方图（PNG 嵌入）
![速度分布](plots/2026-05-10-baseline-speed.png)
...

## 5 条生成轨迹示例
![](plots/2026-05-10-baseline-paths.png)

## vs 上次报表
- 速度 KL 从 0.34 → 0.12（提升）
- ...
```

PNG 用 matplotlib 输出到 `data/eval_reports/plots/`。

### 7.3 CLI
```bash
uv run python -m ai_mouse.eval \
  --model-dir data/models_v2 \
  --reference data/pretrain_traces.jsonl \
  --n-samples 1000 \
  --output data/eval_reports/2026-05-10-baseline.md \
  --tag baseline
```

## 8. 配置变更（`ai_mouse/config.py`）

新增：

```python
@dataclass
class PretrainConfig:
    """Hyperparameters for Balabit pretraining."""
    epochs: int = 200
    batch_size: int = 128
    lr: float = 3e-4
    seq_len: int = 64

@dataclass
class FinetuneConfig:
    """Hyperparameters for fine-tuning on user data."""
    epochs: int = 50
    batch_size: int = 64
    lr: float = 1e-5  # 比预训练小一个数量级
    seq_len: int = 64

@dataclass
class BalabitAdapterConfig:
    """Settings for Balabit data conversion."""
    window_ms: int = 1200
    min_dist: int = 50
    min_events: int = 5
    max_span_ms: int = 5000
    max_gap_ms: int = 200

@dataclass
class EvalConfig:
    """Settings for evaluation report generation."""
    n_samples: int = 1000
    fft_freq_band: tuple[float, float] = (4.0, 12.0)
    kl_bins: int = 50
```

`TrainConfig` 保持不变（向后兼容现有训练脚本）。

## 9. 测试策略

### 9.1 新增测试
- `test_balabit_adapter.py`：用合成 CSV 测试切分逻辑、过滤规则、边界条件（空 session、无 click、坐标溢出）
- `test_eval_metrics.py`：固定输入下指标计算的正确性

### 9.2 更新测试
- `test_generator.py`：移除对 `speed_profile` 的断言；新增 lateral 平滑断言
- `test_trainer.py`：新增 `resume_from` 测试

### 9.3 保持通过
所有非 `test_generator.py`、非 `test_trainer.py` 的测试保持通过。`test_server.py` 不变（服务端 API 未改）。

## 10. 验收标准

最终评估报表（`data/eval_reports/<final>.md`）应显示：

1. **主观**：5 条生成轨迹的 Δt 曲线明显多样化（不再重合）
2. **主观**：lateral 无高频锯齿
3. **量化**：速度分布 KL（vs Balabit holdout）< 当前实现（pre-refactor baseline，第一次跑评估时记下）的 50%
4. **量化**：FFT 频谱在 4–12Hz 区间出现 peak
5. **回归**：所有非废弃测试保持通过

## 11. 工作量估计

| 阶段 | 时间 |
|---|---|
| Balabit 适配器（含测试） | 1–2 天 |
| Trainer 改造（streaming + resume） | 1 天 |
| Generator 后处理改造 | 0.5 天 |
| 评估模块 | 1–2 天 |
| 跑预训练 + fine-tune（CPU/GPU 视情况） | 0.5–2 天 |
| 调参迭代 + 报表对比 | 1–3 天 |

**最小可行版本**（搭起来跑通第一版）：3–5 天
**完整调到验收**：1–2 周

## 12. 风险与备选

| 风险 | 缓解 |
|---|---|
| Balabit 切分后样本数不足（< 5000） | 放宽 `min_dist` 到 30，或扩大窗口到 2000ms |
| 预训练后 fine-tune 出现灾难性遗忘 | lr 调更小（1e-6），epochs 减到 20 |
| 模型架构 cond_dim 在预训练/fine-tune 不一致 | 强制相同；不一致时直接 raise |
| 评估报表实现工作量过大 | 第一版只做基础指标（速度/Δt CV），FFT 和 PCA 后置 |
| Balabit 数据集合规问题 | 仅本地使用，不分发，不商用 |
