zhihu-0.3b

一个从零预训练的 0.3B 中文问答语言模型,训练框架是打了补丁的 litgpt, 本目录把它转成了自包含的 Hugging Face 格式(trust_remote_code)。

它是什么

  • 参数规模:20 层 / 896 hidden / 14 个注意力头 / GQA 7 组 / 2560 FFN;词表 65024(SentencePiece 64789 + 10 个特殊 token,其余是 padding)。
  • 训练数据:知乎社区问答(问题标题 + 描述作为 instruction,回答作为 reply), 单轮 SFT 格式,不是多轮对话模型。
  • 训练目标:标准 causal LM;prompt 段的 label 被 mask 掉,只对回答计算 loss。

架构要点

  • Qwen3 风格 QK-Norm:Q/K 在 reshape 成多头之后、RoPE 之前,对每个头做 RMSNorm(权重长度 = head_dim = 64)。训练和推理的数值路径完全一致。
  • Qwen3-Next 风格注意力门控:注意力输出在 o_proj 之前乘上 sigmoid(g_proj(x)),其中 x 是该层注意力的(已归一化)输入。
  • GQA:14 个 query head 共享 7 组 K/V,推理时按 repeat_interleave(2) 展开。
  • RoPE:NeoX / rotate_half 布局(不是 interleaved),head_dim 全维度旋转, base = 10000,最大上下文 2048。
  • RMSNorm:全部在 float32 下计算,再乘权重、转回模型 dtype。
  • 输出 logits:在 float32 下计算(即使权重是 bf16),避免低精度 logits 在 top-2 很接近时翻转贪心结果;其余层仍按模型 dtype 计算。
  • 所有线性层和 norm 都没有 bias;lm_head 与 token embedding 共享权重。

Prompt 格式

训练时的模板(\n 是真实换行):

<|user|>
 {问题标题}

 {问题描述}<|assistant|>
 {回答正文}</s>

推理时只需要拼到 <|assistant|>\n。如果手动拼 id(sp 是 sentencepiece.SentencePieceProcessor(model_file="tokenizer.model")):

prompt_ids = [64795] + sp.encode("\n " + question) + [64796] + sp.encode("\n")

sp.encode("\n") 是 [30910, 13](▁ 在换行前);训练时回答也是以这个 ▁ 开头,所以推理前缀必须保留它。更推荐直接用下面的 chat_template。

特殊 token id

id token 用途
0 <unk> SentencePiece 原生
1 <s> SentencePiece 原生 BOS
2 </s> EOS(回答结束)
64789 [MASK] 保留,未用于本模型训练
64790 [gMASK] 保留,不要使用
64791 [sMASK] 保留,不要使用
64792 sop 保留,不要使用
64793 eop 保留,不要使用
64794 <pad> padding
64795 `< user
64796 `< assistant
64797 `< observation
64798 `< system

用法(trust_remote_code)

需要 transformers >= 5.0(在 5.14.1 上验证过;4.x 未测试)。模型和分词器都要 trust_remote_code=True。

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model_dir = "zhihu-0.3b-hf"

tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(model_dir, trust_remote_code=True)
model.eval()

messages = [{"role": "user", "content": "为什么天是蓝色的?"}]
inputs = tokenizer.apply_chat_template(
    messages, add_generation_prompt=True, return_tensors="pt"
)

with torch.no_grad():
    output = model.generate(
        **inputs,
        max_new_tokens=128,
        do_sample=True,
        temperature=0.8,
        top_k=50,
        top_p=0.95,
        repetition_penalty=1.2,
        eos_token_id=tokenizer.eos_token_id,
        pad_token_id=tokenizer.pad_token_id,
    )

print(tokenizer.decode(output[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True).strip())

apply_chat_template 渲染出的字符串是 "<|user|>\n {content}<|assistant|>\n"; 模型只训练过单轮问答,多轮消息虽然能拼出来,但效果没有保证。

推荐解码参数

参数 推荐值 说明
repetition_penalty ≥ 1.2(1.2–1.3) 必开。不加时很快开始复读,贪心解码也一样;低于 1.2 效果明显变差
temperature 0.8 0 表示贪心;贪心同样要配 repetition_penalty
top_p 0.95
top_k 50

一句话:**repetition_penalty 至少要开 1.2**,其余按上表默认值即可。

使用 chat_template

tokenizer_config.json 里内置的模板:

{% for message in messages %}<|{{ message['role'] }}|>
 {{ message['content'] }}{% endfor %}{% if add_generation_prompt %}<|assistant|>
{% endif %}

Jinja 渲染结果与训练模板逐 token 对齐;普通文本片段独立交给 SentencePiece 编码, <|assistant|>\n 后面会保留段首 ▁,得到 [64796, 30910, 13]。

限制

  • 单轮问答:训练数据是一个问题一条回答,没有多轮上下文、system prompt 或工具调用。
  • 上下文 2048:超过会被截断;生成长文本时注意给 prompt 留空间。
  • 词表边界:SentencePiece 本身只有 id 0..64788;64789 及以上是特殊 token / padding。sp.decode() 不能直接处理 ≥ 64789 的 id,解码前请用 tokenizer 或截断词表。
  • 重复:不加重复惩罚时贪心解码容易复读,repetition_penalty 至少要开 1.2(见上面的推荐解码参数)。
  • bf16 精度:除输出层外按 bf16 计算;输出 logits 虽然在 float32 下计算,隐藏状态 的低精度舍入仍可能让极接近的 top-2 翻转。需要严格逐 token 复现时用 from_pretrained(..., dtype=torch.float32) 加载,或使用 --dtype fp32 导出的目录。
  • 0.3B 规模,事实准确性有限,不适合当作知识库或高风险场景的唯一依据。

⚠️ 绝对不要使用 [gMASK]sop

本模型从零训练的是标准 causal LM,没有 GLM 的空白填充目标。ChatGLM3 原始 tokenizer 会在序列前自动加 [gMASK]sop,本模型的 tokenizer 不会,也不应 手动加。加上这两个 token 相当于给模型喂了训练时从未出现过的前缀,会显著降低 输出质量。同理,不要使用 Hugging Face 上原始 ChatGLMTokenizer 的 build_inputs_with_special_tokens。

Downloads last month
195
Safetensors
Model size
0.3B params
Tensor type
BF16
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Dataset used to train KrisTHL181/zhihu-0.3b