layazh-zh / README.md
NovaAI
docs: specific RLCD recipe (reward weights, group/sigma/tau, lr, unfrozen layers); temperature-clamp note
769ee58 verified
|
Raw History Blame Contribute Delete
20.9 kB
---
license: other
license_name: layazh-custom-license
license_link: https://huggingface.co/NovaAI6868/layazh-zh/blob/main/LICENSE
language:
- zh
library_name: transformers
pipeline_tag: text-classification
tags:
- laya
- system-one
- calibrated-decisions
- rlcd
- chinese
- classification
- routing
- scoring
- guardrails
- non-autoregressive
- commercial-license-required
base_model: hfl/chinese-roberta-wwm-ext-large
---
# layazh — Laya 中文版决策模型
**已发布:<https://huggingface.co/NovaAI6868/layazh-zh>**
## ⚖️ 许可协议(重要)
本模型采用**自定义协议**,**不是**开源许可:
> ### 个人使用免费 · 商业使用需付费授权
>
> - **个人使用(含研究、学习、非营利、以及年营收 < 1 万元人民币的个人开发者):免费**
> - **商业使用:需事先取得付费商业授权**
> - **商业授权联系:`novaweb6868@outlook.com`**
>
> 完整条款见本仓库的 [`LICENSE`](https://huggingface.co/NovaAI6868/layazh-zh/blob/main/LICENSE) 文件。
> 提交任何贡献即视为同意该文件第三节的贡献者许可协议(CLA)。
注意:本模型中**推理与训练代码**源自 Apache-2.0 许可的
[`convaiinnovations/laya`](https://huggingface.co/convaiinnovations/laya);上述自定义协议
主要约束**模型权重**。代码部分您仍可按 Apache-2.0 使用。
---
**非自回归的中文 System 1 决策模型。** 给它一个 **state**(中文文本 / 邮件 / 工单 / JSON)
和若干**带类型的问题**,它在**一次前向传播**里返回带校准概率的答案。它**不生成任何文本**,
所以没有东西需要解析,也没有东西可以幻觉。
架构与训练配方对齐 [`convaiinnovations/laya`](https://huggingface.co/convaiinnovations/laya),
编码器换为中文预训练 backbone 并在中文决策数据上微调。
## 与上游 Laya 的关系
| | 上游 `convaiinnovations/laya`(仓库根) | **layazh(本模型)** |
|---|---|---|
| 定位 | 英文 | **中文** |
| 编码器 | `answerdotai/ModernBERT-large`(28 层, 1024 hidden, 395M) | `hfl/chinese-roberta-wwm-ext-large`(24 层, 1024 hidden, 325M) |
| 决策头 | 2 层 TransformerEncoder + marker scorer + act head | **同构**(逐行对齐上游 `DecisionModel`) |
| 总参数 | 421M | **352M** |
| 上下文 | 512 tokens | 512 tokens |
| 选项预算 | `head_max_len = 192` | `head_max_len = 192` |
| 训练配方 | RLCD:log + 0.5·spherical(序数题另加 RPS),REINFORCE + group-mean baseline,多轮 TD(λ=1) | 同配方,实测采用:`rl_weight=8`、`kl_weight=1`、`group=4`、`σ=0.5`、`τ=1.5` |
**训练配方的具体做法(同构部分与实测取值)**
奖励函数两侧**逐字一致**(我方 `layazh.model.proper_reward` 与上游 `laya.common.proper_reward` 对拍
确认默认参数相同):
```
reward = log_score + 0.5 × spherical_score # 所有题型
− 1.0 × ranked_probability_score # 仅序数题 (score)
```
三者都是**严格恰当评分规则**,因此最大化期望奖励的唯一方式就是如实报告概率——
这是校准性的来源,而不是事后补丁。
具体训练设置(我方为实测值;上游的这几项未随 checkpoint 发布,故不臆测):
| 项 | 上游 | 本模型(layazh) |
|---|---|---|
| 策略梯度估计 | REINFORCE + group-mean baseline(GRPO 风格) | 同 |
| 探索噪声 | 对选项 logits 加零均值高斯噪声 | `σ = 0.5`,每组采样 `4` 次取组均值作基线 |
| 采样温度 | 未公开 | `τ = 1.5` |
| 目标函数 | 策略梯度 + 稠密 masked-KL 辅助项 | `8.0 × PG + 1.0 × KL` |
| 多轮对话 | TD(λ = 1.0) 于前缀切片 | 同(代码路径一致) |
| 决策头 lr | 未公开 | `3e-4`(AdamW) |
| 编码器 lr | 未公开 | `2e-5`(分组,`encoder_lr_scale=0.067`) |
| 可训练范围 | 全量微调编码器 | **顶部 8/24 层** + 决策头(12 GB 显存约束) |
| LR 调度 | 未公开 | 线性 warmup 6% + cosine 衰减 |
| 其他 | — | 选项顺序增强开、grad clip 1.0、gradient checkpointing 开、fp32 |
> **诚实说明**:上游只发布了**权重与推理配置**,没有发布 RLCD 的训练超参
> (`rl_agent_config.json` 里只有 `temperature` 与 `updates: 7313 / hours: 1.96` 这类元数据)。
> 因此"同配方"指的是**算法与奖励函数相同**,而上表中的具体数值是本项目自行确定并实测的,
> 不能声称与上游的隐藏设置一致。
序列模板与上游完全一致(已用上游 `laya` 包内的 `render_options` / `QTYPES` 做过程序化对齐校验):
```
[CLS] <qtype> 问题:<中文指令> [SEP] [MASK] 选项0 [MASK] 选项1 ... [SEP] <state> [SEP]
```
每个选项前一个 `[MASK]` marker,决策头读这些 marker 位置的隐状态并在该问题的选项上做 softmax。
`noul` 恒渲染为 `[false, true]`,故 `p[1]` = “该命题成立”的概率。
## 安装
`layazh` 包**已随本仓库提供 wheel**,无需克隆仓库、也无需 Hugging Face token。
### 第 1 步:安装 layazh 包
```bash
# 推荐:uv(也可直接用 pip,把 uv pip 换成 pip 即可)
uv venv --python 3.11 .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 从本仓库安装(固定到当前 commit,长期可复现;依赖会自动装好)
uv pip install https://huggingface.co/NovaAI6868/layazh-zh/resolve/d55f726a84aa2c17186c49d20fc35cf49c633f0e/layazh-0.1.0-py3-none-any.whl
```
这会把 `torch` / `transformers` / `numpy` / `safetensors` / `huggingface_hub` 一起装上,
之后 `from layazh import ZhAgent` 在任何目录都能直接用,**不需要改 `sys.path`**。
### 第 2 步(**老显卡必读**):换成匹配你 GPU 架构的 PyTorch
上一步装的是默认 PyTorch(面向较新的 CUDA)。如果你的卡比较老,需要替换:
| 你的 GPU | 命令 |
|---|---|
| **Maxwell / Pascal**(GTX 750–1080、Titan X 等,sm_50–sm_61) | `uv pip install torch==2.7.1 --index-url https://download.pytorch.org/whl/cu128` |
| Turing / Volta(RTX 20 系、T4) | `uv pip install torch --index-url https://download.pytorch.org/whl/cu121` |
| Ampere 及更新(RTX 30/40 系、A100) | 默认装的即可,无需替换 |
> **为什么 Maxwell 要锁 2.7.1**:`torch 2.7.1 + cu128` 是最后一个包含 `sm_50` cubin 的
> wheel(可在 sm_52 上执行)。更新的版本不再为这些架构编译内核。本模型就是在
> GTX Titan X(sm_52)上用这个组合训练和推理的。
>
> 另外该架构**不支持 bfloat16**,代码会自动把 dtype 收敛到 fp16/fp32,
> 不需要你手动处理。
### 第 3 步:验证安装
```python
import layazh
print(layazh.__version__) # -> 0.1.0
from layazh import ZhAgent
agent = ZhAgent.from_pretrained("NovaAI6868/layazh-zh") # 首次下载约 1.4 GB
print(agent.device) # -> cuda 或 cpu
```
没有 GPU 也能跑(CPU 推理,只是慢一些)。仅安装 CPU 版 PyTorch 可以用
`--index-url https://download.pytorch.org/whl/cpu`。
### 想从源码安装 / 复现训练
```bash
git clone https://huggingface.co/NovaAI6868/layazh-zh layazh-zh
cd layazh-zh
uv pip install -e . # 可编辑安装
uv pip install -e ".[train]" # 训练所需额外依赖(pandas/pyarrow/scipy)
```
## 用法
```python
from layazh import ZhAgent
# 从 Hub 加载(首次自动下载约 1.4 GB)
agent = ZhAgent.from_pretrained("NovaAI6868/layazh-zh")
# 也可从本地目录加载:agent = ZhAgent("checkpoints/layazh-zh")
res = agent.system_one(
"这家酒店位置很好,出门就是地铁站,前台小姐姐特别热情,还帮我们升级了房型。"
"房间干净整洁,早餐品种也很丰富,下次来一定还住这里。",
{
"sentiment": {
"type": "choice",
"instructions": "这条评论的情感倾向是什么?",
"criteria": {"positive": "正面评价,表达满意或推荐",
"negative": "负面评价,表达不满或抱怨"},
},
"rating": {
"type": "score",
"instructions": "这条评论给出的总体评分是多少?",
"criteria": ["非常差", "较差", "一般", "不错", "非常好"],
},
"recommend": {
"type": "noul",
"instructions": "这位用户是否推荐该酒店?",
},
},
)
print(res["answers"]["sentiment"]["choice"]) # -> positive
print(res["answers"]["sentiment"]["confidence"]) # -> 1.0
print(res["answers"]["rating"]["score"]) # -> 3.10 (0–4 的期望值)
print(res["answers"]["recommend"]["noul"]) # -> 0.986 P(推荐)
print(res["usage"]["output_tokens"]) # -> 0 不生成任何文本
```
> 上面这些输出是本模型在**实际运行**中产生的(非示意)。
一次调用里的**所有问题在同一次前向传播中批量作答**。
三种 primitive:
- **`choice`** — 给定 `criteria` 键值对,返回 argmax 选项 + 全部选项概率
+ `confidence`(1 − 归一化熵)。
- **`score`** — 有序选项列表,返回期望分值 `Σ i·pᵢ` 与完整分布。
**序数感知**:训练时额外用 ranked probability score 惩罚累积分布偏差。
- **`noul`** — 二元命题,返回 `P(成立)`。
## 架构
- **编码器**:`hfl/chinese-roberta-wwm-ext-large`,24 层 / hidden 1024 / 16 头 / 21128 词表,
中文全词掩码(whole word masking)预训练,325M 参数。使用 SDPA 注意力。
- **决策头**(从头训练,26.5M 参数):
- `type_emb(3)` — 题型嵌入(choice / score / noul)
- 2 层 `nn.TransformerEncoderLayer`(`norm_first=True`, `src_key_padding_mask`)
- `scorer`:`LayerNorm → Linear(d,d) → GELU → Linear(d,1)`,作用于每个 marker 位置
- `act_head`:`Linear(d+4, 256) → GELU → Linear(256, n_act)`,输入为 `[CLS]` 池化表示
加上答案分布的**已 detach** 摘要(top1、top1−top2、归一化熵、选项数/255)
- **单次前向**:一次 `system_one` 调用里所有问题在同一次前向传播中批量作答。
## 训练
### 数据(全部为**人工标注**中文语料,非模型自标注)
| 来源 | 许可 | 产出 | 记录数 |
|---|---|---|---|
| TNews (CLUE) | Apache-2.0 | `choice`(15 类新闻主题) | 18,756 |
| ChnSentiCorp | **research / 仅研究用途** | `choice` + `score` + `noul` | 10,160 |
| yf_dianping | **research / 仅研究用途** | `choice` + `score` + `noul` | 18,836 |
| **合计(train / eval)** | | **99,184 / 6,244 个问题** | 47,752 / 3,048 |
> ⚠️ **上游数据许可提示(请务必阅读)**
>
> 三个语料中,TNews 为 Apache-2.0,可自由使用;而 **ChnSentiCorp 与 yf_dianping 在其
> 原始发布页标注为研究用途(research-only)**,并未提供明确的商业使用授权。
>
> 本模型仓库采用"个人免费 / 商用付费"协议,但**该协议无法覆盖上游数据集的限制**。
> 因此:
>
> - 若您计划**商业使用**本模型,请自行确认 ChnSentiCorp / yf_dianping 的原始许可,
> 或联系我们获取**仅用 Apache-2.0 语料(TNews 等)重训**的版本;
> - 本项目已尽量披露数据来源以便下游合规评估,但不对下游使用场景的合规性作担保
> (见 `LICENSE` 第五节)。
一条文本扇出成多个 typed question(一条大众点评评论同时给出情感 `choice`、1–5 星序数
`score`、以及“是否推荐”的 `noul`),与 Laya 自己构造 typed-decisions benchmark 的方式一致。
大众点评原始文件按餐厅分组,因此用**蓄水池采样**从 2,979,616 条可用评论中均匀抽样,
而不是截取文件头部(那会严重偏向少数餐厅的客群)。
### 训练配置
决策头随机初始化,与编码器顶部 8 层联合微调:
| 项 | 值 |
|---|---|
| 可训练参数 | 128.3M / 352M(编码器顶部 8/24 层 101.8M + 决策头 26.5M) |
| 决策头 lr | 3e-4 |
| 编码器 lr | 2e-5 |
| LR 调度 | 线性 warmup 6% + cosine 衰减 |
| batch | 4096 padded tokens,≤64 序列 |
| epochs | 3(14,493 步) |
| 梯度裁剪 | 1.0 |
| 选项顺序增强 | 开启(choice/noul 的选项顺序每轮打乱,target 同步重排;`score` 不打乱,因其为序数) |
| 梯度检查点 | 开启(12 GB 显存约束) |
| 混合精度 | **fp32**(本机 sm_52 无原生 bf16) |
训练中每 100 步在一个固定的、跨来源均匀抽样的 400 条探针集上记录真实准确率,
并把探针最优的权重单独存为 `*-best` 快照——因为这比 running-mean loss 可靠得多:
batch 混合了 2 选项与 15 选项的题目,loss 水平主要由构成比例决定。
### 训练过程中测得的关键结论
**学习率与 warmup 是决定性的**(同一份 6000 条数据、同一探针集,唯一变量是 lr/warmup):
| 配置 | 探针准确率轨迹 |
|---|---|
| 编码器 lr 1e-4,**无 warmup** | 0.5113 → 0.3251 → **0.2551**(坍缩) |
| 编码器 lr 2e-5 + warmup 6% + cosine | 0.4199 → 0.5621 → 0.7032 → **0.7404**(持续上升) |
**RLCD 项当前贡献很小。** 日志中策略梯度项 `pg ≈ 0.002–0.024`,稠密 masked-KL 项
`kl ≈ 0.5–2.0`。即使 `rl_weight` 已从 1.0 提到 8.0,RLCD 仍只占损失的一小部分,
因此本版主要由有监督 KL 驱动;RLCD 的严格恰当评分规则主要作用于**概率校准**
(见下方温度标定结果),对 argmax 准确率贡献有限。
### 本机硬件与数值选择
训练在本机 **GTX Titan X(Maxwell, compute capability 5.2, 12 GB, CUDA 12.6)** 上完成。
该架构有三个硬性后果,已在代码中显式处理(均为实测,非推测):
- **不支持 bfloat16** → 上游配置里的 `"amp_dtype": "bf16"` 在本机不可用;
`layazh.config.resolve_amp_dtype` 会把 dtype 收敛到 fp16/fp32,
绝不在 sm_52 上使用 bf16。
- **无 FlashAttention** → 编码器使用 SDPA(math kernel)。
- **PyTorch 支持止于 2.7.1 + cu128** —— 该 wheel 内含可从 sm_52 执行的 `sm_50` SASS
(`torch.cuda.get_arch_list()` = `['sm_50','sm_60',...,'sm_120']`)。
实测数值特性(反直觉,故记录):fp16 matmul **3.80 TFLOP/s** 略快于 fp32 **2.94 TFLOP/s**,
因此推理用 fp16 权重是净收益;训练仍用 fp32 以保证稳定性。
## 评测
### 同台对比(1,200 道**完全相同**的中文题目、**修正后的** ground truth)
两个模型回答 byte-identical 的中文问题,走同一套代码路径与指标实现
(`scripts/benchmark_ab.py`):
| 指标 | **layazh-zh** | Laya `multilingual` | 变化 |
|---|---|---|---|
| **总体准确率** | **0.7150** | 0.4783 | **+0.237** |
| `choice` 准确率 | 0.6127 | 0.6039 | +0.009 |
| `score` 准确率 | **0.6854** | **0.1461** | **+0.539** |
| `noul` 准确率 | **0.9638** | 0.6486 | +0.315 |
| Brier(越低越好) | **0.3930** | 0.8040 | −0.411 |
| ECE(越低越好) | 0.2639 | 0.2626 | +0.001 |
| AURC(越低越好) | **0.1127** | 0.4239 | −0.311 |
| 单题延迟 | 67.0 ms | **35.5 ms** | 慢 1.9× |
> **关于 `choice` 的一项必要说明。** 上游在 `choice` 上看似打平(0.6039 vs 0.6127),
> 但这是**巧合**:上游对 2 选项题的点预测不随输入变化,其"准确率"等于多数类占比;
> 修正标签极性后恰好落在多数类一侧。证据是它的 `choice` Brier 高达 **0.6191**
> (本模型 0.5319),且 `choice` ECE 0.1752 是在"几乎恒定输出"下取得的。
> 逐来源看更能说明问题:在 15 类的 TNews 上上游接近随机水平。
>
> 其余三个 primitive 上本模型全面领先,其中 `score` 提升 **+0.539**。
### 全量评测(6,244 个问题,本模型)
| 分组 | n | 准确率 | ECE |
|---|---|---|---|
| **ALL** | 6,244 | **0.6951** | 0.3440 |
| `type:noul` | 1,392 | **0.9375** | 0.2131 |
| `type:score` | 1,804 | **0.6996** | 0.1645 |
| `type:choice` | 3,048 | 0.5817 | 0.5100 |
| `senticorp/choice` | 640 | 0.8688 | 0.8097 |
| `senticorp/score` | 640 | 0.9109 | 0.2334 |
| `senticorp/noul` | 640 | 0.9125 | 0.2588 |
| `dianping/noul` | 752 | 0.9588 | 0.1743 |
| `dianping/choice` | 1,164 | 0.7096 | 0.6000 |
| `dianping/score` | 1,164 | 0.5833 | 0.1266 |
| `tnews/choice`(15 类) | 1,244 | 0.3143 | 0.2715 |
**必须看地板线,否则准确率数字没有意义:**
| | 总体准确率 |
|---|---|
| 随机猜 | 0.2959 |
| 多数类(先验) | 0.4734 |
| Laya `multilingual`(上游实测) | 0.4783 |
| **layazh-zh** | **0.6951** |
### 温度标定
温度按 `(题型, 选项数)` 分桶拟合,**在同一半数据上拟合、在不相交的另一半上报告**。
三种方案在留出半集(3,122 题)上的 ECE 对比:
| 方案 | 留出集 ECE |
|---|---|
| 不标定(恒等) | 0.3512 |
| 每题型一个温度 | 0.2684 |
| **每 (题型, 选项数) 分桶**(最终采用) | **0.2547** |
最终写入本仓库 `rl_agent_config.json` 的温度:
```json
{"choice:11+": 0.466635, "choice:2": 0.253066, "choice:3-5": 0.545042,
"noul:2": 0.970277, "score:3-5": 1.034508}
```
> **与上游 SDK 的一处已知差异。** 上游 `laya` 包在加载 checkpoint 时会用
> `clamp_temperature` 把温度限制在 **[0.5, 5.0]**,而本模型的 `choice:2` 桶
> 拟合值为 **0.253**(低于下界)。我实测了加这个限幅的影响:
>
> | 温度限幅 | ECE(全部 6,244 题) |
> |---|---|
> | 不限幅(本模型默认) | **0.2498** |
> | `[0.5, 5.0]`(上游 SDK 行为) | 0.2799 |
>
> 限幅会让 ECE 变差,说明该桶确实需要更强的锐化(模型对 2 选项题过于保守)。
> 因此本模型**不做限幅**。若你通过上游 `laya` SDK 加载本 checkpoint,
> 温度会被自动限幅到 0.5,ECE 将为 0.2799 —— 这是可预期的差异,不是 bug。
> 分桶温度是**由留出数据挑选**出来的,不是默认采用上游做法。
> 这一点很重要:同一套分桶在各方案比较中只赢了 0.0137 ECE,
> 如果不做留出对比就无从判断它是否真的更好。
>
> 标定必须拟合在**未经温度缩放的原始 logits** 上,否则拟合会永远收敛到 ~1.0。
### 已知弱点
- **`choice` 是明确弱项(0.5817),15 类 TNews 仅 0.3143。** 15 个选项在 512 token
预算下每个只剩几个 token,正是上游模型卡承认的"选项数越多每项 token 越少导致准确率骤降"。
改进方向:调大 `head_max_len`,或改层级式 coarse-to-fine 选择。
- **`choice` 校准最差(ECE 0.5100)**,因为 `choice` 混合了 2/3/15 三种选项数。
- **上下文上限 512 token**(中文约 500 字),超长文档需先切分。
- **延迟慢于上游 1.9×**(67.0 ms vs 35.5 ms):中文 RoBERTa-large(24 层)比
上游 mmBERT-base 更大,这是精度换速度的取舍。
- **RLCD 贡献有限**:本版准确率主要由有监督 KL 驱动,RLCD 的严格恰当评分规则
主要体现于校准。
- **训练未跑满计划轮数**(见 `REPORT.md`),是一个接近平台期、而非训练到极限的版本。
## 诚实的边界
以下限制继承自上游 Laya 的 Honest Limits,中文版同样适用,另外加上中文特有的几条:
- **零样本能力弱。** 上游基础 checkpoint 在 typed-decisions benchmark 上仅 0.362
(随机 0.318、多数类 0.461)。Laya 是**快速专精的底座**,不是开箱即用的决策引擎;
价值在于在中文数据上定向微调后的表现。
- **`score` 序数题是最弱的 primitive**(上游 SST-5 仅 0.372)。
- **选项数 > 20 时准确率骤降**:选项共享固定的 `head_max_len` 预算,77 个选项时每项只剩
3–4 个 token。解法是调大 `head_max_len` 或改用层级式 coarse-to-fine 选择。
- **`noul` 可能跟着选项标签走而不是跟着 state 走**(上游 issue #156)。
若怀疑卡住,改用中性 key 的双选项 `choice`。
- **`action.act_probability` 暂无可用信号**(上游 issue #185)。请用 `confidence` 门控。
- **开箱过度自信**:务必在自己的数据上重新拟合温度。
- **中文上下文只有 512 token。** 中文 BERT 词表基本 1 字 1 token,因此有效正文约 500 字。
超长中文文档需要先切分或用支持更长上下文的编码器。
- **数据域偏移**:训练语料是新闻 / 酒店评论 / 餐厅点评,不等于你业务里的邮件或工单。
在你的领域上继续微调仍然必要。
## 引用
- Laya 模型卡与权重:<https://huggingface.co/convaiinnovations/laya>
- Laya 代码:<https://github.com/NandhaKishorM/laya>
- CLUE / TNews:<https://github.com/CLUEbenchmark/CLUE>
- 中文 RoBERTa:<https://github.com/ymcui/Chinese-BERT-wwm>