简介:本资源面向自然语言处理方向的开发者与算法工程师,聚焦小样本条件下的多标签与层次分类任务,基于UTC模型实现行业领域标签的快速适配。仅需少量标注样本即可显著提升分类效果,Macro F1提升13%以上,有效降低标注门槛与成本,适用于案情要素抽取、行业文本归类等复杂场景。压缩包共11个文件,约3.06MB,包含4个txt数据文件、4个py脚本、1个gz压缩数据、1个ipynb交互式笔记本及1份UTC论文PDF,覆盖数据处理、训练、评估与可视化演示的完整流程。目前已有327人学习下载。读者可获取可直接运行的训练与评估脚本、数据转换工具、Gradio交互界面示例以及论文参考,便于快速复现实验、迁移到自有标签体系,并理解小样本多标签分类的调参思路与工程落地方法。
1. 小样本多标签分类的破局点:为什么 UTC 能把 Macro F1 拉高 13%
做法律文书要素抽取的同行大概率遇到过这种局面:婚姻家庭领域的案情描述里,一条文本同时涉及"财产分割""子女抚养""感情破裂"多个标签,标注数据攒了半年也就几百条,训出来的模型 Macro F1 卡在 0.6 上下死活上不去。更麻烦的是换一个业务领域——比如从婚姻家庭切到劳动争议——标签体系全变,之前的标注基本白干。这个基于 UTC 的多标签/层次分类小样本文本应用,解决的正是这个场景:它把 UTC(Unified Text Classification,统一文本分类框架)拿来做小样本迁移,在 CAIL2019 婚姻家庭要素抽取这类任务上,Macro F1 相比常规微调方案提升 13% 以上,而且适配新领域标签时只需要几条样本。资源包里包含完整的训练、评估、数据转换脚本和 UTC 论文原文,适合做 NLP 分类落地、又不想在标注上烧钱的工程师直接拆用。
2. UTC 框架拆解:多标签分类为什么吃小样本这套
2.1 UTC 的核心思路与多标签适配逻辑
常规文本分类微调的做法是:预训练模型加一个线性分类头,用交叉熵损失在标注数据上训。数据量大的时候没问题,但小样本场景下分类头参数随机初始化,几百条样本根本喂不饱,模型直接过拟合到训练集的标签分布上。UTC 的思路不一样,它把分类任务统一成"文本匹配"的形式——每个标签对应一个标签描述文本,模型判断输入文本和标签描述之间的语义匹配程度,而不是直接输出类别概率。
这个设计在小样本下的优势很直接:标签描述文本本身携带了语义信息,相当于给每个类别提供了先验知识。比如"财产分割"这个标签,描述文本里包含"房产""存款""债务"这些词,模型即使只见过几条正样本,也能通过语义匹配泛化到没见过的表述上。多标签场景下,每个标签独立做一次匹配判断,标签之间不互斥,天然支持一条文本命中多个标签。
层次分类也是同理。UTC 支持把标签组织成树形结构,父节点和子节点分别做匹配,子节点的预测结果可以向上聚合。资源包里的UTC论文.pdf对这个机制有完整推导,建议先翻一遍再动代码,不然调参的时候容易凭感觉走。
2.2 资源包文件结构与各自职责
拿到压缩包先别急着跑run_train.py,花五分钟把文件认全,后面排错能省一半时间。资源包解压后的文件清单和职责如下:
| 文件 | 类型 | 职责 |
|---|---|---|
run_train.py | 训练入口 | 加载配置、构建模型、执行训练循环 |
run_eval.py | 评估入口 | 加载 checkpoint,在测试集上算 Macro F1 |
main.ipynb | 交互式脚本 | 数据探索、单条推理演示、结果可视化 |
Data_conver.py | 数据转换 | 把原始标注格式转成 UTC 需要的输入格式 |
utils.py | 工具函数 | 数据加载、指标计算、日志记录 |
train.txt/dev.txt/test.txt | 数据文件 | 训练/验证/测试集,制表符分隔 |
gradio_input.txt | 推理输入 | Gradio 演示用的样例输入 |
elements.tar.gz | 压缩包 | 标签体系定义和标签描述文本 |
UTC论文.pdf | 文档 | UTC 原论文,含完整方法推导 |
elements.tar.gz需要单独解压,里面是标签树和每个标签的描述文本。这个文件是 UTC 小样本能力的来源,标签描述写得好不好直接决定最终效果,后面会专门讲怎么改。
2.3 环境依赖与版本约束
UTC 依赖 PyTorch 和 HuggingFace Transformers,版本兼容性有几个硬约束。PyTorch 建议 1.10 以上,Transformers 用 4.20 到 4.30 之间的版本比较稳,太新的版本改了Trainer的接口,run_train.py里的回调可能报错。安装命令如下:
# 创建虚拟环境,Python 3.8 或 3.9 兼容性最好 conda create -n utc_cls python=3.9 -y conda activate utc_cls # 安装核心依赖,版本区间经过验证 pip install torch==1.13.1+cu117 -f https://download.pytorch.org/whl/torch_stable.html pip install transformers==4.28.1 pip install scikit-learn pandas numpy gradio tqdmtorch的 CUDA 版本按自己显卡驱动选,没有 GPU 就用 CPU 版,但训练时间会从十几分钟拉到一两个小时。scikit-learn是用来算 Macro F1 的,gradio只在跑main.ipynb里的演示时需要。装完之后用python -c "import torch; print(torch.cuda.is_available())"确认 GPU 可用,返回False的话先查驱动和 CUDA 版本匹配。
3. 从原始标注到 UTC 输入:数据转换与训练配置实操
3.1 Data_conver.py 的转换逻辑与输入格式
UTC 的输入格式和常规分类任务差别很大。常规做法是文本\t标签,UTC 需要的是文本\t标签描述的配对形式,每个标签展开成一条训练样本。Data_conver.py干的就是这件事,核心逻辑是把多标签样本拆成多条二分类样本。
# Data_conver.py 核心转换逻辑(简化版,实际代码以资源包为准) import pandas as pd def convert_to_utc_format(raw_df, label_descriptions): """ raw_df: 原始数据,包含 'text' 和 'labels' 两列 label_descriptions: dict,标签名 -> 标签描述文本 返回 UTC 格式的 DataFrame,每行是 (text, label_desc, label) """ records = [] for _, row in raw_df.iterrows(): text = row['text'] labels = row['labels'].split(',') # 多标签用逗号分隔 for label_name, label_desc in label_descriptions.items(): # 命中则为正样本,未命中为负样本 label = 1 if label_name in labels else 0 records.append({ 'text': text, 'label_desc': label_desc, 'label': label }) return pd.DataFrame(records)这段代码的关键在label_descriptions这个字典。标签描述不是随便写的,它决定了模型能不能在小样本下泛化。常见做法是:标签名 + 同义词 + 典型场景词,拼成一句话。比如"财产分割"可以写成"涉及房产、存款、债务等财产分配问题的描述"。描述文本控制在 20 到 40 个字,太短语义不够,太长会稀释匹配信号。
转换后的数据量会膨胀:假设原始 500 条样本、20 个标签,转换后就是 10000 条。这是 UTC 的正常开销,不用慌,实际训练时负样本会做下采样,run_train.py里有对应的参数控制。
3.2 run_train.py 关键参数逐项说明
run_train.py是训练入口,参数分三块:模型参数、训练参数、数据参数。直接跑默认配置能出结果,但要复现 13% 的提升,有几个参数必须按场景调。
# 基础训练命令 python run_train.py \ --model_name_or_path bert-base-chinese \ --train_file train.txt \ --dev_file dev.txt \ --label_file elements.tar.gz \ --max_seq_length 256 \ --per_device_train_batch_size 16 \ --learning_rate 2e-5 \ --num_train_epochs 10 \ --warmup_ratio 0.1 \ --negative_sample_ratio 3 \ --output_dir ./utc_output--model_name_or_path选中文预训练模型,bert-base-chinese是最稳的起点,有领域语料可以换成对应的继续预训练模型。--max_seq_length设 256 够用,法律文书案情描述一般不超过 200 字,设太大浪费显存。--learning_rate用 2e-5,小样本场景下学习率超过 5e-5 容易震荡。--negative_sample_ratio 3是负样本下采样比例,意思是每个正样本配 3 个负样本,这个值在 2 到 5 之间调,太小模型学不到区分边界,太大正负失衡。
--num_train_epochs设 10 是保守值,实际训练时看验证集 Macro F1 曲线,连续 3 个 epoch 不涨就可以停。--warmup_ratio 0.1让学习率在前 10% 的步数里线性上升,小样本训练不稳定,warmup 能明显减少早期震荡。
3.3 训练过程监控与日志解读
训练日志里重点看三个指标:loss、eval_macro_f1、eval_accuracy。小样本场景下accuracy会虚高——因为负样本占多数,模型全预测负样本也能有不错的 accuracy——所以只盯 Macro F1。
正常训练曲线是:前 2 个 epoch loss 快速下降,Macro F1 从 0.3 左右爬到 0.6;第 3 到第 6 个 epoch 缓慢上升,到 0.75 上下;之后开始波动,偶尔冲高但稳定不下来。如果第 1 个 epoch 结束 Macro F1 就超过 0.9,大概率是数据泄漏——检查train.txt和dev.txt有没有重复样本。如果 loss 一直不降,先查标签描述文本是不是写成了空字符串,再查学习率是不是设大了。
# utils.py 里 Macro F1 的计算逻辑,确认评估口径 from sklearn.metrics import f1_score def compute_metrics(preds, labels): # preds 是 sigmoid 后的概率,阈值 0.5 转成 0/1 preds_binary = (preds > 0.5).astype(int) # average='macro' 表示每个标签等权重,小样本下必须用 macro macro_f1 = f1_score(labels, preds_binary, average='macro') return {'macro_f1': macro_f1}评估口径必须是average='macro',如果用micro或者weighted,负样本多的标签会把指标拉高,看起来好看但实际没用。资源包里run_eval.py默认就是 macro,不用改。
4. 避坑与排查:小样本多标签分类的五个血泪教训
4.1 标签描述写得太随意,Macro F1 直接掉 20 个点
现象:训练 loss 正常下降,但验证集 Macro F1 卡在 0.5 上不去,换模型、调学习率都没用。
原因:elements.tar.gz里的标签描述文本是直接从标签名复制过来的,比如"财产分割"的描述就是"财产分割"四个字。UTC 靠文本匹配做分类,描述文本没有额外语义信息,模型退化成普通的线性分类头,小样本优势完全丢失。
解决:每个标签的描述文本至少包含标签名、2 到 3 个同义词、1 个典型场景短语。改完之后重新跑Data_conver.py生成训练数据,Macro F1 通常能回升 15 到 20 个点。这个坑我踩过两次,现在拿到新任务第一件事就是检查标签描述。
4.2 负样本下采样比例设错,模型全预测负类
现象:评估时eval_accuracy很高(0.85 以上),但 Macro F1 接近 0,混淆矩阵显示所有样本都被预测成负类。
原因:--negative_sample_ratio设得太大,比如设成 10,正负样本比例 1:10,模型发现全预测负类就能拿到 90% 的 accuracy,直接躺平。小样本场景下正样本本来就少,负样本再一多,梯度被负样本主导。
解决:把negative_sample_ratio降到 2 到 3 之间,同时在损失函数里给正样本加权。run_train.py里有--pos_weight参数,设成负正样本比例的倒数。比如比例 1:3,pos_weight设 3.0。改完重新训练,混淆矩阵里正类的召回率会明显上升。
4.3 标签体系有层次关系但没配父子节点,子类预测全乱
现象:层次分类任务里,子标签的 Macro F1 只有 0.4,但父标签能到 0.8,子类预测结果和父类对不上——比如父类预测"婚姻家庭",子类却预测"劳动争议"。
原因:elements.tar.gz里的标签树没有配父子关系,所有标签平铺做多标签分类。UTC 的层次分类能力依赖标签树结构,父节点的预测结果会约束子节点的候选范围,不配父子关系等于放弃了这个约束。
解决:在elements.tar.gz的标签定义文件里加上parent字段,每个子标签指向对应的父标签。run_train.py里有个--use_hierarchy开关,打开之后模型会先预测父类,再在父类范围内预测子类。配好之后子类 Macro F1 一般能到 0.65 以上。
4.4 训练集和验证集分布不一致,验证指标虚高
现象:验证集 Macro F1 到 0.85,但拿test.txt一跑只有 0.6,差距超过 20 个点。
原因:train.txt和dev.txt是从同一批数据里随机切的,但test.txt来自不同时间段或不同来源。小样本场景下模型对数据分布特别敏感,训练集里没出现过的标签组合,测试集里一出现就翻车。
解决:切分数据时按标签组合分层采样,保证每个标签在训练集和验证集里的出现频率接近。utils.py里有个stratified_split函数可以用,传label_column参数指定标签列。如果测试集分布确实不同,在训练集里补充一些测试集风格的样本,哪怕每个标签只补 5 条,效果也比不补强。
4.5 推理时阈值用默认 0.5,多标签漏检严重
现象:单条推理演示时,明明文本里涉及三个标签,模型只输出一个,另外两个概率在 0.3 到 0.4 之间被阈值卡掉了。
原因:多标签分类每个标签独立算概率,默认阈值 0.5 对稀有标签不友好。稀有标签的正样本少,模型学到的概率整体偏低,0.5 的阈值会把大量正确预测过滤掉。
解决:在验证集上给每个标签单独调阈值,稀有标签的阈值可以降到 0.2 到 0.3。run_eval.py里有个--per_label_threshold选项,打开之后会输出每个标签的最优阈值。调完阈值再跑测试集,Macro F1 通常还能再涨 3 到 5 个点。
5. 进阶技巧:用标签描述增强和阈值调优把 Macro F1 再推一截
标签描述增强是 UTC 小样本能力的天花板所在。常规做法是人工写描述,但人力有限,标签一多就写不过来。我一般会用一个半自动的流程:先用大模型对每个标签生成 5 条候选描述,再人工筛选合并。生成的时候给模型一个模板——"请用 30 个字描述[标签名]在[业务领域]中的含义,包含 3 个同义词和 1 个典型场景"——出来的描述质量比手写稳定。筛选标准就一条:把描述文本单独拿出来,能不能判断它属于哪个标签。判断不了就重写。
阈值调优有个容易忽略的细节:验证集上的最优阈值不一定适合测试集。如果测试集分布和验证集有偏移,阈值要重新调。稳妥的做法是在验证集上调完阈值后,在测试集上再微调一轮,步长 0.05,看 Macro F1 的变化。run_eval.py支持传入--threshold_search参数,会自动在 0.1 到 0.9 之间搜索最优阈值组合。
还有一个提分技巧是标签描述的多模板融合。同一个标签写 2 到 3 条不同角度的描述,推理时分别算概率再取平均。比如"财产分割"可以写"涉及房产存款债务分配"和"离婚时财产如何划分"两条描述,模型对两种表述的匹配结果平均之后,鲁棒性明显提升。代价是推理时间翻倍,但小样本场景下推理量不大,这个开销可以接受。
# 多模板融合推理示例 def predict_with_multi_desc(text, label_descs, model, tokenizer): """ label_descs: dict, 标签名 -> [描述1, 描述2, ...] 返回每个标签的平均概率 """ label_probs = {} for label_name, desc_list in label_descs.items(): probs = [] for desc in desc_list: inputs = tokenizer(text, desc, return_tensors='pt', truncation=True, max_length=256) with torch.no_grad(): logits = model(**inputs).logits probs.append(torch.sigmoid(logits).item()) label_probs[label_name] = sum(probs) / len(probs) return label_probs这段代码的核心是sum(probs) / len(probs)这一步,多条描述的预测概率取平均,抵消单条描述措辞偏差带来的波动。实际用的时候描述数量控制在 3 条以内,太多会平滑掉区分度。
从那以后我每次拿到新的分类任务,都强制走一遍"标签描述检查 → 负采样比例确认 → 阈值搜索"这三步,少一步 Macro F1 就可能掉一截。希望帮到你。
本文还有配套的精品资源,点击获取