第 46 章

第 46 章:中文 Head 微调实验

第 46 章:中文 Head 微调实验

本文整理自 Datawhale 开源项目 datawhalechina/jev-cookbook(CC BY-NC-SA 4.0),源文件:main/10_本地模型/notebooks/zh_head_finetuning.ipynb。

本 Notebook 按照 self-llm 教程的逐步讲解形式组织:确认并下载权重、准备环境、检查数据、训练、查看指标、加载微调结果。内容针对 Laya 自己的模型结构和训练代码编写,可从仓库根目录或本目录逐格运行。

本实验复现仓库中的 112 条合成中文工单试跑。它适合学习和讨论流程;标签不是人工金标,不能据此推断生产效果。

训练方式: Laya 是多语言 encoder 加结构化决策 head 的模型。当前脚本冻结 encoder 和 act head,只用监督交叉熵更新决策 head。它不是 causal LM,也不使用 MiniCPM 的 LoRA 配置;这也不是 Laya 上游 RLCD 训练。

预计产物: GPU 微调 checkpoint、逐轮 CSV/JSONL 日志、实验元数据和离线 HTML 曲线。训练输出写到系统临时目录,不会覆盖下载的基座权重。

from pathlib import Path
import sys

ROOT = next(
    (parent for parent in (Path.cwd(), *Path.cwd().parents)
     if (parent / "laya" / "finetune_reviewed_jsonl.py").is_file()),
    None,
)
if ROOT is None:
    raise FileNotFoundError("请从 jev-cookbook 仓库打开本 Notebook")

DATA_PATH = ROOT / "laya" / "experiments" / "zh-pilot-112" / "train-dev.jsonl"
MODEL_DIR = ROOT / "laya" / "models" / "multilingual"
sys.path.insert(0, str(ROOT))
print("仓库根目录:", ROOT)
print("训练数据:", DATA_PATH)
print("多语言权重目录:", MODEL_DIR)

1. 先下载 Laya 多语言权重

中文数据使用 convaiinnovations/laya 的 multilingual checkpoint。只下载训练需要的多语言目录;首次下载需要网络,下载后 Notebook 会复用本地文件。完整说明见 laya/models/README.md。

import os
import shutil
import subprocess
import sysconfig

required_files = [
    MODEL_DIR / "model.safetensors",
    MODEL_DIR / "rl_agent_config.json",
    MODEL_DIR / "encoder",
    MODEL_DIR / "tokenizer",
]
missing = [str(path) for path in required_files if not path.exists()]
if missing:
    subprocess.run([sys.executable, "-m", "pip", "install", "modelscope-hub"], check=True)
    cli_name = "ms-hub.exe" if os.name == "nt" else "ms-hub"
    cli = Path(sysconfig.get_path("scripts")) / cli_name
    if not cli.is_file():
        found = shutil.which("ms-hub")
        if found:
            cli = Path(found)
        else:
            raise FileNotFoundError("ModelScope Hub CLI 未安装成功;请重启 kernel 后重新运行此格")
    subprocess.run(
        [str(cli), "download", "convaiinnovations/laya",
         "--local-dir", str(ROOT / "laya" / "models"),
         "--include", "multilingual/**"],
        check=True,
    )

missing = [str(path) for path in required_files if not path.exists()]
if missing:
    raise FileNotFoundError("模型文件仍不完整: " + ", ".join(missing))
print("多语言 checkpoint 已就绪:", MODEL_DIR)

2. 安装项目依赖并检查 CUDA

请在启用了 NVIDIA CUDA 的 Jupyter kernel 中运行。仓库的 requirements 文件包含 PyTorch、Transformers、Safetensors 和数据处理依赖;如果当前 Python 环境已经匹配,可跳过安装并直接检查 GPU。安装或升级 PyTorch 后需要重启 kernel,再从 GPU 检查开始继续运行。

import subprocess

subprocess.run(
    [sys.executable, "-m", "pip", "install", "-r", str(ROOT / "laya" / "requirements.txt")],
    check=True,
)
import torch
import transformers
import safetensors
import numpy as np

print("Python:", sys.version.split()[0])
print("PyTorch:", torch.__version__)
print("CUDA runtime:", torch.version.cuda)
print("Transformers:", transformers.__version__)
print("Safetensors:", safetensors.__version__)
print("NumPy:", np.__version__)
print("CUDA available:", torch.cuda.is_available())
if not torch.cuda.is_available():
    raise RuntimeError("当前 kernel 没有可用 CUDA;请安装与驱动匹配的 PyTorch CUDA 版本并重启 kernel")
print("GPU:", torch.cuda.get_device_name(0))

3. 读取 JSONL 并检查切分

每行是一条 state 记录,qs 中包含一道或多道 choice / noul / score 问题。训练器会检查审核状态、重复 ID,以及 source_group_id 是否跨 train/dev;这里先查看数据量和题型分布。

这份示例记录为 assistant_reviewed_pilot。它代表经过辅助抽查的合成试跑数据,仍不是人工 gold。

import hashlib
import json
from collections import Counter

records = [json.loads(line) for line in DATA_PATH.read_text(encoding="utf-8").splitlines() if line.strip()]
train_records = [record for record in records if record["split"] == "train"]
dev_records = [record for record in records if record["split"] == "dev"]
train_groups = {record["source_group_id"] for record in train_records}
dev_groups = {record["source_group_id"] for record in dev_records}
assert train_records and dev_records
assert not (train_groups & dev_groups), "source_group_id 泄漏到 train/dev 两侧"
assert all(record["metadata"]["review_status"] == "assistant_reviewed_pilot" for record in records)
reference_experiment = json.loads((DATA_PATH.parent / "experiment.json").read_text(encoding="utf-8"))
data_sha256 = hashlib.sha256(DATA_PATH.read_bytes()).hexdigest()
assert data_sha256 == reference_experiment["data_sha256"], "数据文件与已记录实验哈希不一致"

print("records:", len(records), "train:", len(train_records), "dev:", len(dev_records))
print("questions:", sum(len(r["qs"]) for r in train_records), "train /", sum(len(r["qs"]) for r in dev_records), "dev")
print("train/dev source groups overlap:", bool(train_groups & dev_groups))
print("question types:", dict(Counter(q["t"] for r in records for q in r["qs"])))
print("review states:", dict(Counter(r["metadata"]["review_status"] for r in records)))

4. 配置并启动 head-only 微调

默认参数与已提交的 GPU 试跑对齐:随机种子 42、head 学习率 1e-4、最多 8 轮、dev soft cross-entropy 连续 2 轮不改善则早停、token budget 4096。训练器按 dev soft cross-entropy 选最佳 checkpoint。

由于示例数据标记为 assistant_reviewed_pilot,命令显式启用 --allow-assistant-reviewed-pilot。对已由人工审核的正式数据,应去掉该开关并使用 approved 或 human_reviewed 状态。

import tempfile
import uuid

RUN_DIR = Path(tempfile.gettempdir()) / ("laya-zh-notebook-" + uuid.uuid4().hex[:8])
command = [
    sys.executable,
    str(ROOT / "laya" / "finetune_reviewed_jsonl.py"),
    "--data", str(DATA_PATH),
    "--model-dir", str(MODEL_DIR),
    "--output-dir", str(RUN_DIR),
    "--epochs", "8",
    "--patience", "2",
    "--head-lr", "1e-4",
    "--max-tokens", "4096",
    "--max-seqs", "8",
    "--seed", "42",
    "--allow-assistant-reviewed-pilot",
]
print("输出目录:", RUN_DIR)
subprocess.run(command, cwd=ROOT, check=True)

5. 查看训练曲线和评估结果

每轮训练和验证指标保存在 training_log.csv 与 training_log.jsonl;experiment.json 记录数据 / 权重 SHA256、实际软件版本、训练参数、显卡和验证结果。下面用同一个可视化脚本生成自包含 HTML。

subprocess.run(
    [sys.executable, str(ROOT / "laya" / "visualize_training.py"), "--run-dir", str(RUN_DIR)],
    cwd=ROOT,
    check=True,
)

experiment = json.loads((RUN_DIR / "experiment.json").read_text(encoding="utf-8"))
summary = {
    "gpu": experiment["gpu"],
    "best_epoch": experiment["best_epoch"],
    "dev_before": experiment["dev_before"],
    "dev_after": experiment["dev_after"],
    "peak_vram_gib": experiment["peak_vram_gib"],
    "training_seconds": experiment["training_seconds"],
    "artifacts": ["training_report.html", "training_log.csv", "training_log.jsonl", "experiment.json"],
}
summary
from IPython.display import HTML, display

display(HTML(filename=str(RUN_DIR / "training_report.html")))

6. 加载基座与微调模型做一次推理对照

这一步检查保存的 checkpoint 可否被项目客户端加载,并观察相同 dev state 上的输出。单条样本是加载 smoke check,不是准确率或泛化评估;量化指标以上一格的完整 dev 指标为准。

import gc
from laya.client import LayaClient

sample = dev_records[0]
questions = {
    question["id"]: {
        "type": question["t"],
        "instructions": question["ins"],
        "criteria": question["crit"],
    }
    for question in sample["qs"]
}

base_client = LayaClient(MODEL_DIR, device="cuda")
base_answers = base_client.system_one(sample["state"], questions)["answers"]
del base_client
gc.collect()
torch.cuda.empty_cache()

tuned_client = LayaClient(RUN_DIR, device="cuda")
tuned_answers = tuned_client.system_one(sample["state"], questions)["answers"]
print("标签索引:", {q["id"]: q["y"] for q in sample["qs"]})
print("基座输出:", json.dumps(base_answers, ensure_ascii=False, indent=2))
print("微调输出:", json.dumps(tuned_answers, ensure_ascii=False, indent=2))

7. 复现结果说明

本次提交的数据、日志、实验摘要和 HTML 报告位于 laya/experiments/zh-pilot-112/。相同软件、驱动和随机种子可获得接近的结果;CUDA kernel 与库版本差异可能造成数值偏差。

样本量只有 112 条,且 dev 来自同一任务规范。请把本 Notebook 当作训练流程教程,讨论模型效果前应补充真实人工审核数据、独立 test / OOD split 和概率校准评估。完整操作说明见 laya/FINETUNING.md。

📑 Jev Cookbook:System One 判断模型实战教程

1 第 1 章:认识 Jev:模型、上手与场景 2 第 2 章:核心概念总览 3 第 3 章:System One:判断的核心心智模型 4 第 4 章:状态:让判断连续可追溯 5 第 5 章:原语:Choice、Score 与 Noul 6 第 6 章:置信度:让概率可信 7 第 7 章:应用构建:从原语到完整系统 8 第 8 章:架构模式 9 第 9 章:实战指南总览 10 第 10 章:自一致性 · Noul 11 第 11 章:自一致性 · Choice 12 第 12 章:并行提问 13 第 13 章:重排序 14 第 14 章:逐行语义搜索 15 第 15 章:结构恢复 16 第 16 章:函数调用 17 第 17 章:技能推荐 18 第 18 章:实体对齐 19 第 19 章:RAG 段落分类 20 第 20 章:引用核查 21 第 21 章:LLM 防护栏 22 第 22 章:SDE 级联 23 第 23 章:日期抽取 24 第 24 章:预解析值抽取 25 第 25 章:层级分类 26 第 26 章:自动研究特征发现 27 第 27 章:基于置信度的分类 28 第 28 章:智能家居实验 29 第 29 章:模型评测总览 30 第 30 章:模型评测实验 31 第 31 章:Laya vs Jev 对比基准 32 第 32 章:JevBench:LLM 评测体系 33 第 33 章:智能家居应用实战 34 第 34 章:Jev-Mem 研究总览 35 第 35 章:Jev-Mem 缩放实验 36 第 36 章:Jev-Mem 完整走查 37 第 37 章:Agent 集成总览 38 第 38 章:Pi 集成实验 39 第 39 章:DSH 决策协作 40 第 40 章:本地模型总览 41 第 41 章:本地模型介绍与对比 42 第 42 章:中文数据集构建方案 43 第 43 章:微调指南 44 第 44 章:RLCD 原理与实验优化 45 第 45 章:中文数据集构建实验 46 第 46 章:中文 Head 微调实验 47 第 47 章:全量 v2 微调实验 48 第 48 章:知识库
← 返回本书大纲