Skip to content

如何训练问题分类 Encoder-only 模型 ​

基于糖豆TIM 社区云问题分类实战经验撰写 版本:v1.0(2026-10-14)


一、前言 ​

1.1 适用读者 ​

  • NLP 工程师 / 算法工程师
  • 后端 / 全栈想自己跑通分类模型的同学
  • 数据 / 标注团队,理解标注数据怎么变成模型

读完本文你能:独立从 0 训练一个短文本分类 BERT,并在 CPU 上 30 分钟内跑完全流程。

1.2 与 Decoder-only / Encoder-Decoder 选型对比 ​

Transformer 按注意力方向分三大类:

架构注意力方向训练方式擅长代表
Encoder-only双向判别式理解 / 分类 / NER / 检索BERT、RoBERTa
Decoder-only单向(因果)自回归生成GPT、LLaMA、Qwen
Encoder-Decoder双向 + 因果Seq2Seq翻译 / 摘要T5、BART

问题分类是判别式任务:给一段输入,输出一个离散标签。Encoder 双向注意力最擅长抽取语义特征。

1.3 本文产出物清单 ​

  • 完整训练脚本(src/prepare_data.py + src/train.py)
  • 评估脚本(src/evaluate.py)
  • 推理脚本(src/infer.py,单条 / 批量 / 交互三种)
  • 离线模型(models/chinese-roberta-wwm-ext/)
  • 标注规范(docs/labeling_guide.md)
  • 训练日志与混淆矩阵(output/)

二、任务定义与选型 ​

2.1 问题分类的任务本质 ​

把用户输入的短文本(一般 5–80 字)映射到一个预设标签集合。

特点:

  • 判别式:输出离散标签
  • 单标签:每条只对应一个类(vs 多标签需要多输出头)
  • 短文本:几乎不需要 sliding window
  • 类别少:通常 5–20 类,超过 50 类建议分层

2.2 为什么选 Encoder-only ​

  1. 双向注意力:同时看到上下文,对理解类任务最友好
  2. 单次前向 + [CLS] 分类:不需要自回归,推理快
  3. 模型小:BERT-base 110M、RoBERTa-base 125M,CPU 可跑
  4. 微调简单:在 [CLS] 上接 nn.Linear(hidden, num_labels) 即可
  5. 中文友好:chinese-roberta-wwm-ext 在中文 IM 任务上表现稳定

2.3 备选模型对比 ​

模型层数参数量中文优化适用场景
chinese-roberta-wwm-ext12102M哈工大 WWM 预训练首选通用
ernie-3.0-base-zh12118M百度知识增强知识型任务
chinese-macbert-base12102MMLM 纠错错别字多场景
electra-180g-small1212M判别式预训练极致小模型
xlm-roberta-base12270M多语种多语种场景

本项目选 chinese-roberta-wwm-ext:哈工大官方出品,社区使用广泛,IM 短文本表现稳定。


三、数据准备 ​

3.1 业务标签体系设计原则 ​

三条铁律:

  1. 互斥:每条样本只能归到一个类(多标签需另设)
  2. 完备:所有可能输入都能归到某类(不确定就归"其他")
  3. 业务粒度:标签对应"业务处理方",不是"语义相似度"

本项目 11 类(社区云场景):

ID标签业务处理方样本数
C01走访/统计数据组175
C02小区/物业/业委会/机构街镇办157
C03房态图/人房数据数据组140
C04标签/重点人群数据组74
C05换届选举民政组56
C06小事快办/社区动态街镇办33
C07账号/权限/登录运维28
C08系统故障/性能研发12
C09进度跟进/流程咨询客服11
C10功能优化/需求产品7
C11数据导出/下载数据组2

3.2 数据采集与清洗 ​

清洗规则(IM 场景标准):

处理规则示例
去除表情[表情]、emoji 字符[表情]你好 → 你好
去除媒体[图片]、[语音]、[文件][图片] →
匿名化 @@昵称 → @USER@张三 在吗 → @USER 在吗
替换链接URL → URL看 http://x.com → 看 URL
合并空白多空白 → 单空格你 好 → 你 好
截断长度> 128 字符保留前 128—

业务名词词典(社区云场景,必须维护):

房态图、人房数据、人房关系、线索发现、接待走访
综合采集、重点人群、一房一档、人口台账、换届选举
小事快办、议事协商、民主评议

这些词在通用中文语料里几乎不出现,模型必须从领域数据中学习。

3.3 标注规范要点 ​

3.3.1 优先级 + 流程图 ​

开始
 ↓
包含投诉/举报词?──是──→ L5
 ↓ 否
涉及钱/支付/会员?──是──→ L4
 ↓ 否
涉及账号/登录/封禁?──是──→ L3
 ↓ 否
询问怎么用/求助?──是──→ L2
 ↓ 否
表达不满/提建议?──是──→ L1
 ↓ 否
 → L0

3.3.2 置信度字段 ​

置信度含义处理
2 = 高明确属于该类直接入库
1 = 中大概率属于,但有歧义入库 + 交叉校验
0 = 低不确定不训练,入复审池

3.3.3 一致性指标 ​

Cohen's Kappa ≥ 0.75 视为合格。

公式:Kappa = (P_o - P_e) / (1 - P_e),扣掉"瞎猜能蒙对"的部分。

每 200 条插入 5 条金标准样本,每周汇报一次。

3.4 数据集划分 ​

python
from sklearn.model_selection import train_test_split

train_df, tmp_df = train_test_split(
    df, test_size=0.2, stratify=df["label_id"], random_state=42
)
val_df, test_df = train_test_split(
    tmp_df, test_size=0.5, stratify=tmp_df["label_id"], random_state=42
)

两个关键参数:

  • stratify:每份里各类比例一致(避免某类全在 train)
  • random_state=42:固定随机种子,便于复现和对比

本项目划分结果:train 556 / val 69 / test 70(共 695 条)。


四、基线训练全流程 ​

4.1 环境与依赖 ​

powershell
pip install transformers torch datasets accelerate scikit-learn pandas

CPU 即可,无需 GPU。BERT-base CPU 跑 5 epoch 大约 15 分钟。

4.2 模型下载与离线准备 ​

国内推荐 hf-mirror.com,无需登录。

必下 5 个文件(Pytorch 路线):

文件大小用途
config.json689 B模型结构配置
pytorch_model.bin412 MBPytorch 权重(核心)
tokenizer.json269 kB分词器词表+规则
tokenizer_config.json19 B分词器配置
vocab.txt110 kB词表

不用的文件:flax_model.msgpack、tf_model.h5、README.md 等。

放置到 models/chinese-roberta-wwm-ext/ 目录,from_pretrained() 即可离线加载。

4.3 数据加载与 Tokenize ​

python
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("models/chinese-roberta-wwm-ext")

def tokenize_fn(batch):
    return tokenizer(
        batch["text"],
        truncation=True,
        max_length=128,        # IM 问题 95% < 64 字
        padding="max_length"
    )

train_ds = Dataset.from_pandas(train_df).map(tokenize_fn, batched=True)

input_ids 怎么来的?

"怎么建群啊"
   ↓ BERT tokenizer 分词(wwm 整词保留)
["怎", "么", "建", "群", "啊"]
   ↓ 查 vocab 表
[2769, 6370, 3299, 6206, 1024]
   ↓ 加 [CLS] [SEP]
[101, 2769, 6370, 3299, 6206, 1024, 102]
   ↓ 补 0 到 128 位
[101, 2769, 6370, 3299, 6206, 1024, 102, 0, 0, ...]

attention_mask:1 表示真词,0 表示 padding。模型据此忽略 padding。

4.4 训练配置 ​

python
from transformers import TrainingArguments

args = TrainingArguments(
    output_dir="output/tdtim-classifier",
    num_train_epochs=5,
    per_device_train_batch_size=8,
    per_device_eval_batch_size=16,
    learning_rate=2e-5,            # BERT 微调黄金值
    weight_decay=0.01,
    warmup_steps=35,              # 约 10% 总步数
    eval_strategy="epoch",
    save_strategy="epoch",
    load_best_model_at_end=True,
    metric_for_best_model="f1_macro",
    logging_steps=20,
    report_to="none",
    fp16=torch.cuda.is_available(),  # 有 GPU 就开
)

关键超参解释:

参数推荐值原因
max_length=128128IM 问题 99% 覆盖
batch_size=88CPU 友好,显存够用可调大
lr=2e-52e-5太大易崩,太小难收敛
epochs=55数据 < 5000 时够用
warmup_ratio=0.10.1缓解早期梯度爆炸

类别不均衡处理:

python
from sklearn.utils.class_weight import compute_class_weight

class_weights = compute_class_weight(
    "balanced",
    classes=np.arange(len(LABELS)),
    y=train_df["label_id"]
)
class_weights = torch.tensor(class_weights, dtype=torch.float)

# 自定义 Trainer 注入加权 loss
class WeightedTrainer(Trainer):
    def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None):
        labels = inputs.pop("labels")
        outputs = model(**inputs)
        loss = torch.nn.CrossEntropyLoss(weight=class_weights.to(outputs.logits.device))(
            outputs.logits, labels
        )
        return (loss, outputs) if return_outputs else loss

4.5 训练过程监控 ​

训练中每个 epoch 会打印:

{'loss': 0.85, 'grad_norm': 1.2, 'learning_rate': 1.8e-05, 'epoch': 1.0}
{'eval_loss': 0.62, 'eval_accuracy': 0.7101, 'eval_f1_macro': 0.5501, 'eval_f1_weighted': 0.6637}

正常曲线:

EpochTrain LossVal F1行为
11.420.78学到基础语义
20.850.86学到标签特征
30.520.91收敛
40.310.92最佳(保存)
50.180.91过拟合迹象

load_best_model_at_end=True 会自动回到 Epoch 4 的参数。

4.6 评估指标 ​

4.6.1 三个核心指标 ​

指标公式适用
accuracy正确数 / 总数类别均衡时
f1_macro各类 F1 算术平均类别不均衡时主用
f1_weighted各类 F1 按样本数加权反映整体效果

类别不均衡时必须看 f1_macro,否则模型"全预测大类"也能拿 80% accuracy。

4.6.2 详细报告 ​

python
from sklearn.metrics import classification_report, confusion_matrix

print(classification_report(labels, preds, target_names=LABELS, digits=3))
print(confusion_matrix(labels, preds))

4.6.3 本项目基线结果 ​

test  accuracy = 0.8571
test  f1_macro = 0.6890
test  f1_weighted = 0.8580

大类(走访、小区、房态)F1 都在 0.87+,小类(系统故障、进度跟进)F1=0,完全没学好。


五、踩坑记录 ​

5.1 transformers 5.x 兼容性问题 ​

坑 1:warmup_ratio 被移除 ​

python
# ❌ transformers 5.x 报错
TrainingArguments(..., warmup_ratio=0.1)
# TypeError: __init__() got an unexpected keyword argument 'warmup_ratio'

# ✅ 改用绝对步数
total_steps = len(train_ds) // batch_size * epochs  # 556/8*5 = 348
warmup_steps = int(total_steps * 0.1)                # 35
TrainingArguments(..., warmup_steps=35)

坑 2:tokenizer= → processing_class= ​

python
# ❌ 旧版
trainer = Trainer(..., tokenizer=tokenizer)

# ✅ 新版
trainer = Trainer(..., processing_class=tokenizer)

坑 3:compute_loss 签名变化 ​

python
# ❌ 旧版签名
def compute_loss(self, model, inputs):
    ...

# ✅ 新版签名(5.x)
def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None):
    ...

坑 4:lr_scheduler_kwargs 参数名 ​

新版本 get_linear_schedule_with_warmup 用 num_warmup_steps,且 transformers 在调用前会注入这个参数,不能再在 lr_scheduler_kwargs 里传,否则会冲突。

5.2 类别不均衡导致小类 F1=0 ​

症状:C08(系统故障)12 条训练样本,模型从未预测该类,recall=0、F1=0。

根因:训练样本太少(< 20 条),模型"没见过"。

解法(按优先级):

  1. 数据增强:LLM 同义改写扩到 50+ 条
  2. 过采样:RandomOverSampler 复制小类样本
  3. class_weight:本项目已用,但样本太少时仍不够
  4. 合并小类:C08+C09+C10+C11 合并为 C99

5.3 训练中断后从 checkpoint 恢复 ​

python
# ✅ 加载上次的 checkpoint
model = AutoModelForSequenceClassification.from_pretrained("output/tdtim-classifier/checkpoint-140")
tokenizer = AutoTokenizer.from_pretrained("output/tdtim-classifier/checkpoint-140")

trainer = Trainer(
    model=model, args=args,
    train_dataset=train_ds, eval_dataset=val_ds,
)
trainer.train(resume_from_checkpoint="output/tdtim-classifier/checkpoint-140")

save_strategy="epoch" 默认只保留最近 2 个 checkpoint(save_total_limit=2),磁盘不会爆。

5.4 test 集未出现的类导致 classification_report 报错 ​

python
# ❌ 报错:labels are empty
print(classification_report(labels, preds, target_names=LABELS))

# ✅ 只保留出现过的类
present = sorted(set(labels) | set(preds))
present_names = [LABELS[i] for i in present]
print(classification_report(
    labels, preds,
    labels=present, target_names=present_names,
    digits=3, zero_division=0,
))

六、推理与部署 ​

6.1 单条 / 批量 / 交互三种用法 ​

单条:

powershell
python src\infer.py "走访记录导出乱码"

批量:

powershell
python src\infer.py --file data\new_messages.csv --out data\predicted.csv

交互:

powershell
python src\infer.py -i
>>> 提交申请后是街镇级审核么
  Top-1: 账号/权限/登录  score=0.65

6.2 批量预测 CSV 输入输出规范 ​

输入(仅需 1 列):

csv
text
怎么创建群聊
走访记录导出乱码

输出(自动追加 3 列):

csv
text,pred_label_id,pred_label,pred_score
怎么创建群聊,2,小区/物业/业委会/机构,0.7821
走访记录导出乱码,0,走访/统计,0.9622

若输入含 label_id 列,自动识别并打印准确率。

6.3 性能优化路线 ​

阶段方法预期效果
模型压缩ONNX Runtime + INT8 量化提速 2–3x,体积 -75%
批处理攒批推理 (dynamic batch)吞吐提升 3–5x
模型蒸馏Teacher=BERT-base → Student=4 层 MiniLM速度 +10x
缓存Redis 缓存 (text_hash → result)高频问题命中率 >40%
GPU单卡 T4 即可支撑 1k+ QPS—

ONNX 导出(推荐):

python
from optimum.onnxruntime import ORTModelForSequenceClassification
# 终端命令(推荐)
# optimum-cli export onnx --model output/tdtim-classifier/best onnx_model/

七、迭代优化方向 ​

7.1 数据增强(最优先) ​

小类样本不足是基线模型的主要瓶颈。

python
# 用 LLM 给 C08–C11 各扩 30+ 条同义样本
# Prompt 模板:
"""
你是社区云产品的用户,请用不同口吻重写下面这条问题,保留核心业务词:

原句:{original_text}
要求:
- 保持原意
- 长度相近(5–50 字)
- 风格多样(口语化、正式、带错别字)
- 输出 3 个版本
"""

扩充后预期 f1_macro 从 0.69 提升到 0.80+。

7.2 早停 + 学习率调度 ​

python
TrainingArguments(
    early_stopping_patience=2,    # 2 个 epoch 不涨就停
    lr_scheduler_type="cosine",  # 余弦退火
)

7.3 过采样 / 欠采样 ​

python
from imblearn.over_sampling import RandomOverSampler

ros = RandomOverSampler(sampling_strategy="auto", random_state=42)
X_res, y_res = ros.fit_resample(X_train, y_train)

7.4 冻结低层 + 微调顶层 ​

数据 < 1000 条时建议:

python
# 冻结 BERT 前 8 层
for param in model.bert.encoder.layer[:8].parameters():
    param.requires_grad = False

减少可训练参数,缓解过拟合。


八、附录 ​

8.1 完整项目结构 ​

d:\1sqyai\tangdou-bert\
├── data\
│   ├── train.csv (556 条)
│   ├── val.csv   (69 条)
│   ├── test.csv  (70 条)
│   ├── label_map.json
│   └── 讨论问题汇总-1.csv   (原始数据)
├── models\
│   └── chinese-roberta-wwm-ext\   (离线权重)
├── output\
│   ├── tdtim-classifier\
│   │   ├── checkpoint-140\   (最佳)
│   │   ├── checkpoint-350\   (最终)
│   │   └── best\             (推理用)
│   │       ├── model.safetensors (390 MB)
│   │       ├── config.json / tokenizer.json
│   │       └── test_report.json
│   └── logs\
│       ├── train_log.txt
│       └── eval_log.txt
├── src\
│   ├── prepare_data.py
│   ├── train.py
│   ├── evaluate.py
│   └── infer.py
└── docs\
    ├── labeling_guide.md
    ├── how-to-train-question-classifier.md   (本文)
    └── download_model.md

8.2 关键脚本入口 ​

脚本用途命令
prepare_data.py清洗 + 划分数据集python src/prepare_data.py
train.py微调训练python src/train.py
evaluate.py全量评估 + 混淆矩阵python src/evaluate.py
infer.py单条 / 批量 / 交互预测python src/infer.py "..."

8.3 术语表 ​

术语解释
MLMMasked Language Modeling,BERT 预训练任务
WWMWhole Word Masking,整词掩码(哈工大改进)
[CLS]分类标记,聚合整句语义
Cohen's Kappa标注一致性指标,>0.75 为高一致
Confusion Matrix混淆矩阵,看具体类间误判
f1_macro各类 F1 算术平均,看小类
f1_weighted各类 F1 按样本数加权,看整体
Stratify分层采样,保证分布一致
ONNX跨框架模型格式
INT88 位整数量化,体积 -75%

8.4 推荐阅读 ​

Released under the MIT License.