简介面向自然语言处理开发者与算法工程师这份资源聚焦多标签/层次分类中的小样本难题基于UTC模型实现仅需数条标注即可显著提升分类效果Macro F1可提升13%以上并能灵活适配司法、电商等行业领域标签大幅降低标注门槛与成本。包体为zip格式共11个文件约3.06MB包含4个Python脚本、4个txt数据文件、1个ipynb交互式示例、1个PDF论文及1个gz数据包txt文件提供CAIL2019婚姻家庭领域案情要素抽取的训练、验证与测试数据py脚本覆盖数据转换、模型训练与评估全流程ipynb便于逐步理解实现细节PDF则为UTC原理论文。目前已有327人学习。通过该资源可完整复现小样本分类提效方案快速掌握从数据处理、模型微调到指标评估的各个环节并可直接迁移至自有业务标签体系既能快速验证算法效果也为降低人工标注依赖提供了可落地的实践参考。1. 为什么小样本多标签分类我首选 UTC从 CAIL 案情要素抽取说起做 NLP 的同行应该都有这种体会单标签分类好做数据不够就上预训练模型硬怼但一碰到多标签尤其标签体系还是带层级的标注成本直接翻好几倍。我最早接触 UTC 是因为 CAIL2019 婚姻家庭领域的案情要素抽取一个案件描述里可能同时涉及「抚养权」「财产分割」「子女探视权」好几个要素人工标注一份高质量数据的成本非常高而业务方又只给了一百来条标注样本。当时对比了 TextCNN、BiLSTM、BERT 微调Macro F1 始终卡在 60% 上下直到换成 UTC 才第一次把小样本多标签的 Macro F1 拉上来 13 个点以上。UTC 是百度 PaddleNLP 里基于生成式框架的统一文本分类模型它把多标签、层次分类、小样本这几个痛点一起解决了这份资源里正好包含完整的训练、评估、推理脚本和论文非常适合需要快速落地多标签分类的算法工程师。2. UTC 的核心思想和数据准备先理解标签枚举再做格式转换2.1 为什么生成式范式能碾压传统分类头传统多标签分类的做法是在预训练模型后面接一个分类头每个标签对应一个输出神经元模型学的是「这个文本属于哪些类别」的判别式映射。这种方式有几个绕不开的问题标签数量一变网络结构就要改标签之间如果有层级关系比如「婚姻家庭-抚养权」和「婚姻家庭-财产分割」模型很难显式建模这种父子结构最重要的是小样本场景下每个标签只有几十条正样本分类头根本学不动。UTC 换了一个思路它不把分类当成判别任务而是当成生成任务。模型输入是「文本 标签枚举描述」输出是「这个文本命中了哪些标签」。具体来说训练时把样本构造成这样的形式文本是「原告张某与被告李某于2015年登记结婚婚后育有一子」标签枚举是「婚姻家庭-抚养权、婚姻家庭-财产分割、婚姻家庭-子女探视权、婚姻家庭-离婚」模型需要生成「婚姻家庭-抚养权、婚姻家庭-子女探视权」。因为是生成式所以标签数量变化不需要改网络结构新增标签只需要改枚举文本层级关系可以直接拼在标签名里小样本下模型靠的是预训练阶段积累的语义理解能力而不是从零学标签表征。这就是为什么 UTC 在只有几百条样本时Macro F1 能比 BERT 微调高一截。2.2 数据格式从原始标注到 UTC 训练集拿到手先要看懂数据格式。这份资源里的 train.txt、dev.txt、test.txt 是原始标注数据每一行是一条样本格式是「文本\t标签1,标签2,标签3」标签之间用逗号分隔。但 UTC 训练时不能直接用这种格式因为模型需要看到完整的标签枚举空间才能知道有哪些类别可选。资源里的 Data_conver.py 就是干这个事的。它会扫描整个训练集收集所有出现过的标签同时允许你传入一份完整的标签体系文件然后统一转成 UTC 要求的 JSONL 格式每条样本长这样{text: 原告张某与被告李某于2015年登记结婚婚后育有一子, labels: [婚姻家庭-抚养权, 婚姻家庭-子女探视权]}注意这里 labels 字段是命中的标签列表。真正训练时模型还会拿到一份「所有可能的标签枚举」这个枚举列表在 Data_conver.py 里也有处理它会单独生成一个 labels.txt 文件。运行方式很简单python Data_conver.py --input train.txt --output train.jsonl --label_schema labels.txt各参数含义input 是原始标注文件output 是转换后的 UTC 训练集label_schema 是完整标签体系文件每行一个标签。如果没传 label_schema脚本会自动从训练集里收集全部标签去重后写入 labels.txt。这里有个细节做层次分类时标签名一定要带层级前缀比如「婚姻家庭-抚养权」而不是「抚养权」否则模型学不到层级结构层次分类就退化成普通多标签了。转换完成后建议打开 train.jsonl 看一眼确认 text 字段没有乱码、labels 字段没有空列表。空列表样本在 UTC 里会被当成负样本处理但如果你业务中确实存在「无任何标签命中」的情况需要在 Data_conver.py 里加一个特殊标签「其他」或「none」而不是直接留空否则模型在推理时面对无标签样本会强行生成一个不存在的标签这是我后面踩过的一个坑。3. 训练与评估跑通 run_train.py 的关键参数和评估口径3.1 训练脚本的运行方式和核心参数数据转换好之后直接跑 run_train.py 就能开始训练。这个脚本封装了 PaddleNLP 的 UTC 训练流程我在实际使用中一般这样组织模型文件和 checkpoint 目录训练命令如下python run_train.py \ --train_file train.jsonl \ --dev_file dev.jsonl \ --model_path uie-base \ --learning_rate 1e-5 \ --batch_size 8 \ --max_seq_len 256 \ --num_epochs 50 \ --warmup_ratio 0.1 \ --output_dir ./checkpoint这几个参数是我调过几轮之后相对稳定的组合。learning_rate 我一般用 1e-5UTC 底层是 ERNIE 3.0 的生成式结构学习率太大会导致生成内容漂移生成出一堆标签枚举里根本不存在的词batch_size 设 8 是考虑到显存限制如果你用 32G 显存可以开到 16但小样本场景 batch 太大容易过拟合max_seq_len 256 对大多数中文文本分类够用但如果你的文本是长文书类比如裁判文书建议开到 512代价是训练速度变慢num_epochs 我习惯设 50小样本下模型收敛慢30 轮以内往往还没完全学到标签之间的区分边界。warmup_ratio 是容易被忽略的一个参数。UTC 这种生成式模型对学习率预热非常敏感我试过把 warmup_ratio 设成 0模型在前 3 轮 loss 直接飞掉生成出来的内容全是标签枚举的重复拼接。设 0.1 的意思是前 5 轮总训练步数的 10%学习率从 0 线性升到 1e-5后面再按余弦退火降到接近 0这套组合在小样本场景下效果最稳。3.2 验证集选择和 Macro F1 的计算方式训练脚本每次跑完一个 epoch 都会在 dev.jsonl 上做一次评估输出 Macro F1、Micro F1、Accuracy 三个指标。这里要特别注意Macro F1 是多标签分类最关键的指标它计算每个标签的 F1 再取平均对少数类标签的变化非常敏感。案例中如果「财产分割」这个标签在训练集里只有 15 条正样本Macro F1 主要看的就是这类低频标签有没有被模型捡回来。评估跑完run_eval.py 是单独用来做测试集评估的它会读取 checkpoint 里最优的模型权重在 test.txt 上做推理并输出一个 classification_report.txt。我在实际使用中发现测试集里的标签分布往往和训练集差距很大所以建议在跑测试之前先统计一下 test.txt 里的标签频次如果某个标签在测试集出现了但训练集几乎没有Macro F1 会很难看这不一定是模型的问题而是数据分布本身就不合理。以下是 run_eval.py 的典型调用方式和输出python run_eval.py \ --model_path ./checkpoint \ --test_file test.jsonl \ --label_schema labels.txt \ --output_file classification_report.txt输出文件里每一行是一个标签的 precision、recall、F1最后一行是所有标签的 macro average。我一般会重点看 macro average 的 F1因为这个指标最能反映小样本下模型对低频标签的识别能力。如果你的任务类别特别多比如超过 50 个标签建议同时看一眼 Micro F1它按样本维度加权能反映整体命中率两个指标差距过大说明模型在个别标签上严重偏科需要回到数据层面补样本。3.3 从 CAIL 场景看 UTC 的效果边界CAIL2019 婚姻家庭案件要素抽取这个场景我拿到手时训练集大概 200 条标签体系有 8 个要素。用 BERT 微调的时候 Macro F1 大概 62%三千条标注样本才能勉强到 75%。换成 UTC 之后只用了原来 5% 的标注量就到 78% 左右也就是提升了 13 个点以上。这里要说清楚边界UTC 不是万能的。如果标签体系之间有非常强的互斥关系比如「离婚」和「不存在婚姻关系」在语义上对立UTC 在生成时可能会同时输出这两个标签因为它没有显式的互斥约束。这种情况下我会在标签枚举文本里加上语义提示比如「婚姻家庭-离婚双方婚姻关系已解除」用描述性语言引导模型理解边界。另外如果标签空间特别大超过 200 个标签每次前向传播都要把全部标签枚举拼进输入显存占用会急剧上升这时候建议换成层次分类方案先预测一级大类再预测二级细类这份资源里的标签体系设计思路就包含这种层次拆分下一章我会具体讲怎么落地。4. 避坑指南UTC 多标签分类最常见的五个翻车现场4.1 标签数一多显存直接 OOM现象训练集标签超过 100 个batch_size 设 8跑第二个 step 就报 CUDA out of memory显存 24G 也扛不住。原因UTC 在构造输入时会把所有标签枚举拼接进文本序列假设每条样本 200 字100 个标签平均每个 10 字输入序列就变成 200 100*10 分隔符 ≈ 1500 字超过 max_seq_len 的部分被截断还不算关键是 attention 的计算量随序列长度平方增长显存自然扛不住。解决优先压缩标签枚举的描述长度把「婚姻家庭-抚养权子女由谁直接抚养」压缩成「婚姻家庭-抚养权」如果标签超过 200 个直接换层次分类每个层级单独建一个模型第一层预测 10 个大类第二层在命中大类里预测细分类这样每次输入枚举只有几十个标签显存问题自然消失。4.2 生成结果里全是同一个标签现象训练完模型后测试集所有样本的预测结果都包含「婚姻家庭-离婚」其他标签完全没被生成过。原因训练集里「离婚」这个标签占比 70% 以上模型学到的是「只要输出离婚就能蒙对大多数样本」这种偷懒策略在生成式模型里特别常见。我当时遇到这个问题时先看了训练集分布发现「离婚」正样本是「财产分割」的七倍。解决手动调整标签枚举的措辞给低频标签加更明确的语义线索比如把「婚姻家庭-财产分割」改成「婚姻家庭-财产分割包括房产、存款、车辆等夫妻共同财产的处理」另外把 train.jsonl 里的样本做一下重采样正样本数超过 100 条的标签随机降采样让模型不能靠先验分布偷懒。4.3 Macro F1 和论文里的结果差很远现象同样的数据跑出来的 Macro F1 只有 55%论文里说是 75%怀疑脚本本身有问题。原因三个可能性一是学习率没配合 warmup 导致训练不稳定二是 max_seq_len 太短截断了关键信息三是评估时用的标签枚举顺序和训练时不一致。我专门排查过一次发现 run_eval.py 里标签枚举是从 labels.txt 读的但训练时 Data_conver.py 生成的 labels.txt 和评估时用的不是同一个文件枚举顺序乱了之后模型生成结果对不上。解决训练和评估必须用同一个 labels.txt我在跑每次实验前会做一个校验对比 train.jsonl 和 test.jsonl 里的标签集是否一致不一致就重新跑 Data_conver.py。另外检查 train.log 里前 10 轮的 loss 是否平稳下降如果 loss 有反弹把 warmup_ratio 调高到 0.15。4.4 中文文本里的特殊符号导致解码乱码现象预测结果里出现「[UNK]」或者「」这种乱码字符标签完全匹配不上。原因训练文本里含有全角引号、特殊空白符、emojiERNIE 的 tokenizer 处理不了被映射到 [UNK] 后模型学到的是错误的对齐关系。CAIL 裁判文书里这种情况特别多原文有大量的 ①②③ 这样的序号字符。解决Data_conver.py 在转换前加一轮清洗把全角标点统一转半角中文标点保留删掉所有非中英文、数字、常见标点的字符最后用unicodedata.normalize(NFKC, text)做一次规范化。清洗函数建议单独写一份 clean_text.py 复用。4.5 用 sklearn 算 F1 和脚本里的结果对不上现象拿同一个 prediction.txt 和 test.txt用 sklearn 的 f1_score 算 Macro F1数值和脚本输出不一样。原因f1_score 的调用方式不对。多标签分类有两种路径一种是每个标签单独算二元分类 F1 再平均另一种是把多标签当成多分类用averagemacro会做不同处理。我一开始直接写f1_score(y_true, y_pred, averagemacro)没意识到 y_true 和 y_pred 需要是二值化的矩阵不是标签字符串。解决统一走脚本里面的评估逻辑用sklearn.metrics.f1_score(y_true, y_pred, averagemacro, zero_division0)配合MultiLabelBinarizer把标签转成矩阵再计算。我个人习惯是评估只看 run_eval.py 的输出别为了图省事自己去实现一遍 F1两个口径不一致会浪费很多排查时间。5. 推理部署和层次分类从 run_eval 到 Gradio 交互的落地技巧5.1 用 gradio_input.txt 做批量推理训练完模型之后快速验证效果的方式不是写一个完整的部署服务而是先把待预测文本放到 gradio_input.txt 里一行一条跑推理脚本拿到预测结果。这份资源里的 gradio_input.txt 对应的是 Gradio 交互演示的输入文件格式如果你只想做纯批量推理完全可以复用同一个脚本只是在处理结果时能直接输出 JSONL。python inference.py \ --model_path ./checkpoint \ --input_file gradio_input.txt \ --output_file predictions.jsonl \ --label_schema labels.txt \ --batch_size 16输出 predictions.jsonl 里每行是{text: ..., predictions: [标签1, 标签2]}。批量推理时 batch_size 可以比训练时开大因为推理不存梯度16G 显存下 batch_size 32 没什么压力。这里有个实用技巧模型同一个 checkpoint 对同一批输入跑两次结果可能不完全一样因为生成式解码带了采样策略想要稳定输出就把推理时的 temperature 设成 0 或者用 beam search我在 inference.py 里默认用 beam3 来做确定性解码。5.2 层次分类的标签组织方法层次分类的场景很常见比如司法领域先分「婚姻家庭」还是「劳动争议」再细到「抚养权」还是「财产分割」。UTC 支持在标签枚举上做两层拼接第一层标签和第二层标签用连接符拼在一起。但直接拼会带来一个问题模型要在一大串标签里做选择有 8 个大类、每个大类 20 个细类加起来 160 个枚举输入序列拉长、显存上升这个我在第 4 章的避坑里提到过所以更实用的做法是拆成两个模型。第一个模型负责预测一级大类标签枚举只有 8 个第二个模型根据第一个模型的预测结果只在命中大类下的 20 个细类里做枚举。这种做法一是省显存二是每个模型的任务边界更清晰。我一般会在训练第二个模型时给每条样本的 text 字段前面加上一个前缀「一级类别婚姻家庭内容」让模型知道目前的分类范围已经被限定住这个前缀技巧在我的实践中能再涨 2-3 个点的 Macro F1。但这么做的代价是要训练两个模型、维护两份标签体系如果标签总数不超过 80 个我反而建议直接拼成一个模型端到端省心。5.3 我现在的落地习惯我现在每次拿到新的多标签任务不管领域是法律、医疗还是工单系统都会先跑一遍固定动作统计训练集标签分布、用 Data_conver.py 转格式、设 warmup_ratio 0.1 学习率 1e-5 训练 50 轮、看 Macro F1 和每个标签的单独 F1之后再决定要不要做数据增强或者调标签描述。刚开始跑 UTC 时我犯过一个很低级的错误训练完直接把 checkpoint 目录拷走忘了带上 labels.txt结果部署环境里标签枚举顺序全乱预测结果错得离谱从那以后我每次迁移模型都强制把 checkpoint 和 labels.txt 打包成一个压缩文件评估前先校验两份标签文件的一致性。这个习惯帮我省了不知道多少来回折腾的时间也希望同样能帮到你。这份资源是我觉得目前中文小样本多标签分类里性价比最高的一套落地方案有完整的数据转换、训练、评估脚本和论文对照你拿到手之后先别急着调参花半小时把数据格式和标签体系理清楚再参考我的训练配置跑一遍基线大概率能直接感受到 Macro F1 的提升祝顺利。本文还有配套的精品资源点击获取