法律文本分类实战:从TF-IDF到BERT的完整NLP方案

法律文本分类实战:从TF-IDF到BERT的完整NLP方案 简介面向法律文本智能分类场景机器学习项目资源以Python为主体利用朴素贝叶斯、SVM、KNN、随机森林等算法对刑事案件判决书进行语料学习与类别建模适用于司法领域的罪名分类、案情要素识别及初步判决预测研究。资源包共34个文件压缩后为28.32MB其中15个pkl模型文件可直接加载使用5个py脚本覆盖特征构建、数据集切分、模型训练与测试流程4个txt文件提供停用词、罪名与法律条文词典2个json文件则为处理后的训练/测试数据集便于二次开发与复现。当前已有385人学习浏览适合自然语言处理初学者或司法文本挖掘入门者动手实践。包内还附带多种已训练好的分类器与完整数据预处理模块使用者在解除部分注释后可快速跑通全流程省去自行清洗与构建词典的时间能有效支撑课程设计或研究预研。 去年有朋友扔给我一个需求几千份历史法律文书需要按案件类型自动归档。项目本身听着不算难——法律文本分类机器学习里比较标准的文本多分类任务。但真正上手之后才发现这类文本跟新闻分类、商品评论分类完全不同它的表述方式、长尾分布、领域词汇和常规数据集差着十万八千里。这篇文章就把我当时踩过的坑和最终能跑通的方案完整记录下来。这篇内容适合三类人一是正在做 NLP 课程项目、想找真实场景练手的学生二是工作中接到“文书/合同/案件自动分类”需求的算法工程师三是想了解“文本分类上生产环境要做哪些事”的产品或项目经理。我会从数据处理、特征表示、模型选型到完整训练流程逐一拆解最后附上常见问题排查清单内容偏实操可以直接照着改。1. 处理法律文本前先把任务定义清楚1.1 分类目标你是要做“案由分类”还是“要素识别”很多人拿到法律文本就着急上模型结果建出来的模型上线一测准确率惨不忍睹。核心问题往往是任务没定义清楚。法律文本分类其实可以拆成至少三种目标按案由分类把文书归入“合同纠纷”“劳动争议”“知识产权”等预设类别属于粗粒度分类。按文书类型分类区分“判决书”“裁定书”“调解书”“起诉状”等文档类别结构特征更明显。按要素识别从文书中抽取“争议金额”“判决结果”“适用法律条款”等要素严格说是序列标注或文本抽取比分类更深一层。我这次的需求是“按案由归档”属于第一种最经典的多分类问题。但即便只是粗粒度分类也需要先和业务方确认标签体系。法律领域的标签体系通常不是平铺的而是有层级的比如“民事”下面有“合同纠纷”“合同纠纷”下面还有“借款合同纠纷”“租赁合同纠纷”等多个子类。一开始如果决定用二级甚至三级标签数据量不够时模型会很难收敛。我的建议是第一版先做一级或二级大类控制在 10 到 20 个类别把流程跑通再考虑细粒度拆分。1.2 数据从哪来标注数据是法律 NLP 最大的门槛比模型更重要的是数据。公开可用的法律文本数据集其实不少各大高校和司法公开平台都有过脱敏的数据集一般包含案由、裁判年份、法院层级、文本正文等字段。课程作业阶段这类公开数据集完全够用。但做真实项目时业务方给的往往是历年存量文书格式五花八门有 PDF、有 Word、甚至还有扫描件转出来的夹生文本清理成本远高于标注成本。另一个容易被忽略的问题是标签噪音。法律文书的“案由”字段在实际业务里经常被填错比如“民间借贷纠纷”被标成“借款合同纠纷”这种误标会对模型产生直接干扰。我处理时的做法是先做标签分布的占比分析把样本量极少比如小于 50 条的类别单独抽出来看能合并的合并是明显误标的就修正实在没法处理的类别干脆不进入第一版模型。提示无论数据来自哪里都要做脱敏处理。当事人姓名、身份证号、住址、银行卡号等敏感信息要么直接抹掉要么用占位符替换。这个环节不能省尤其是最终要部署到生产环境的项目。2. 文本预处理法律文本的“脏”和“怪”2.1 去噪、分段与法律术语处理法律文本的结构相当固定但也恰恰因为固定预处理环节有几个特殊点。先说常见的噪声扫描件转出来的文本会有大量换行符错乱、空格、乱码直接导出的电子文本则可能带上页眉页脚、表格残留。常规做法是先统一字符编码再用正则把连续空白符压缩、把无关的表格线删掉。真正麻烦的是法律术语。比如“原告诉称”“被告辩称”“本院认为”“综上”这些不仅仅是普通词汇它们往往标记着文书的语义结构。如果你直接用通用分词工具就像 jieba 默认词典可能会把“本院认为”切成“本院/认为”语义信息就被打散了。当时我的做法是导入一个自定义法律词典把高频术语作为整体保留import jieba legal_words [原告, 被告, 本院认为, 原告诉称, 被告辩称, 诉讼请求, 事实与理由, 合同纠纷, 劳动争议, 驳回诉讼请求] for w in legal_words: jieba.add_word(w)这个动作看着简单对后续特征质量的影响却不小。分词不对后面 TF-IDF 或词向量都会跟着错。2.2 如何切分长文本滑动窗口的取舍法律文书普遍很长动辄几千字甚至上万字。而深度学习文本模型的输入长度是有限的BERT 系模型一般 max_length 是 512 个 token长文本直接截断会把关键信息比如判决结果切掉。这里有两个常用方案只截取开头加结尾。因为法律文本通常是“当事人信息 → 诉称 → 辩称 → 本院认为 → 判决结果”的结构判决结果往往在尾部而案由信息通常在前部。所以按比例取头部和尾部拼接比从头一刀切更合理。用滑动窗口切段每段单独分类最后投票或取 max 概率。这个方案信息保留最完整但推理耗时成倍增加。我第一版图省事用了“头部尾部截断”效果其实已经不错F1 大约能到 0.86。后来为了进一步提升改成滑动窗口切段再概率平均整体提升了约两个点但推理时间也涨了近两倍。如果对延迟不敏感滑动窗口方案更值得用。3. 特征表示与方法选型3.1 传统机器学习方案TF-IDF 线性模型/浅层模型文本分类最经典的路子就是 TF-IDF 把文本变成稀疏向量然后喂给逻辑回归、SVM 或随机森林。TF-IDF 的核心思想是某个词在一篇文档中出现的次数越多越重要但在所有文档中出现的次数越少越有区分度。对应到法律文本里“原告”“法院”这类词几乎每篇都有TF-IDF 会把它们压得很低而“民间借贷”“返还欠款”这类词在部分类别里高频出现权重就会很高。在样本量不大、类别相对清晰的场景下SVM 线性核配合 TF-IDF 依然是性价比之王。它训练快、可解释性也还行而且对高维稀疏特征的表现通常优于随机森林。随机森林在文本这种高维稀疏数据上反而容易过拟合GBDT 类模型更不适合直接处理稀疏 TF-IDF 特征。我当时在同样的特征上对比过线性 SVM 的 F1 是 0.85随机森林只有 0.79差距很明显。逻辑回归也可以作为 baseline好处是输出概率可以直接用业务方要“置信度”的时候很方便。SVM 虽然也能做概率校准但训练时用 libsvm 工具箱的 Platt scaling 才能拿到概率多了两步操作。3.2 深度模型到底值不值得上传统模型的瓶颈在长文本语义理解。“本院认为”之后的论述和最终判决之间的因果关联传统 BoW 特征很难捕捉。这时候可以考虑FastText结构简单效果略好于 TF-IDF SVM训练极快适合做第二轮 baseline。TextRNN / TextCNN能够捕捉局部语义TextCNN 在短文本分类上效果稳定但法律文本偏长卷积窗口覆盖有限。BERT 及各类预训练模型这个方向上效果最好。用中文法律语料继续预训练过的模型比如一些法律领域 bert 模型会比通用 BERT 更贴合任务实践中通常能高出 3 到 5 个点的 F1。但深度模型也有代价训练需要 GPU推理速度慢模型体积动不动几百 MB。如果业务只是“每天跑批归档几千份文书”深度学习完全能接受如果要做在线实时分类需要权衡延迟和部署成本。我的建议是先用 TF-IDF SVM 跑通完整流程再逐步升级模型而不是一上来就上 BERT。4. 实操流程以裁判文书案由分类为例4.1 环境准备与数据划分说再多原理不如直接跑一遍。下面这个流程我实际验证过你用自己的语料替换路径就能复现。先准备环境。如果是本机跑建议 Python 3.8 以上加装以下库pip install jieba scikit-learn pandas numpy # 如果跑深度模型再加 pip install transformers torch数据我先按 8:1:1 划分成训练集、验证集和测试集。划分时要注意按类别分层抽样否则个别小类可能在训练集中一次都不出现。sklearn 里有个现成函数from sklearn.model_selection import train_test_split train_texts, temp_texts, train_labels, temp_labels train_test_split( texts, labels, test_size0.2, stratifylabels, random_state42 ) val_texts, test_texts, val_labels, test_labels train_test_split( temp_texts, temp_labels, test_size0.5, stratifytemp_labels, random_state42 )stratifylabels就是按原始标签分布抽样这一步是防止数据倾斜的关键。4.2 建立 baselineTF-IDF SVMTokenizer 和特征抽取的部分我直接用 scikit-learn 的 Pipeline 封装便于后面网格搜索from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.svm import SVC from sklearn.pipeline import Pipeline pipeline Pipeline([ (tfidf, TfidfVectorizer(analyzerword, token_patternr(?u)\b\w\b, ngram_range(1, 2), max_features200000)), (clf, SVC(C1.0, kernellinear, probabilityTrue, random_state42)) ]) pipeline.fit(train_texts, train_labels)几个参数解释一下因为分词后的文本已经是空格分隔的token_pattern用默认的\w配合jieba先对文本分词再传入。ngram_range(1, 2)同时考虑单词和相邻词组合。法律文本里“不构成违约”“主要过错”这类二元短语有明确语义比单纯单词强很多。max_features200000限制特征维度避免超大词表拖慢训练。核函数选线性核而不是 RBF 核。在高维稀疏 TF-IDF 特征上线性核已经能比较好地分割数据RBF 核不仅慢还容易把小类别噪声放大。训练完成后直接看整体效果和各类别明细from sklearn.metrics import classification_report preds pipeline.predict(test_texts) print(classification_report(test_labels, preds, digits4))如果只关心总体指标这个 report 出的 macro F1 和 weighted F1 是最值得盯的两个数。4.3 进阶方案预训练模型微调如果 baseline 效果不够再上预训练模型。我用的是 Transformers 库调用非常简单from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) model AutoModelForSequenceClassification.from_pretrained( bert-base-chinese, num_labelsnum_classes, id2labelid2label, label2idlabel2id ) def preprocess(examples): return tokenizer(examples[text], truncationTrue, max_length512, paddingmax_length)训练参数方面需要注意 batch size 和学习率的配合。法律长文本即便截断后单条样本的 token 数也偏大显存有限的场景下 batch size 通常取 8 或 16学习率从 2e-5 开始调。我第一轮训练时 learning rate 设成 5e-5loss 震荡得很厉害降回 2e-5 后才稳定下来。注意预训练模型的[CLS]token 和分类器之间的结构决定了模型对“句对关系”的处理方式但在纯分类任务上直接用AutoModelForSequenceClassification就够了不需要自己拼接[SEP]之类的特殊结构。4.4 类别不均衡与样本加权法律文本的类别分布极其不均衡。“民间借贷纠纷”可能占了三成“海事海商纠纷”只有几十条。如果直接训练模型会为了整体准确率把小类别全部无视。处理方法有两个调class_weight给少数类更高的惩罚权重。SVM 和逻辑回归在 sklearn 里都支持class_weightbalanced。训练时做重采样对少数类过采样复制样本或对多数类下采样随机去掉部分样本。文本分类里过采样更多见。如果你用 Trainer 训练 BERT可以在compute_metrics里算加权 F1或者用 weighted loss。但注意加权系数拉太高容易让模型在多数类上崩掉经验上class_weight系数不要超过原始频率反比的 3 倍。5. 常见问题与排查技巧实录5.1 F1 结果虚高检查你的数据切分方式这是最容易踩的坑也是我最后悔没早发现的问题。按整篇报告直接随机切分 train/test 会导致数据泄漏因为同一案件可能产生多个相关文书这些文书内容高度相似全被随机分进了训练集和测试集。模型等于开卷考试测试分数虚高上线后立刻原形毕露。正确做法是先按案件编号去重把同属一个案件的文书全部放到同一份数据集里再做切分。5.2 长文本跑不动怎么办原文本动不动一万多字BERT 强行截断 512 会丢掉很多信息。我最后采用的做法是def truncate_head_tail(text, max_len400): head_len int(max_len * 0.7) tail_len max_len - head_len chars list(text) if len(chars) max_len: return text return .join(chars[:head_len] [...] chars[-tail_len:])更彻底的办法是滑动窗口配合最后概率平均。具体做法把长文按 300 字一个窗口、步长 150 切段每个段都喂给模型输出概率加总后取平均。这个方案在离线跑批时很实用但如果线上延迟要求高还是得回到“头尾截断”方案。5.3 传统模型和深度模型效果接近说明什么有一种情况你辛苦调好的 BERT 比 TF-IDF SVM 只高不到两个点这说明类别的区分信息更多集中在表面词汇而不是深层语义上。法律文书里“借款合同纠纷”和“买卖合同纠纷”通常在文本里就有明确的关键词没必要非上大模型。这时候更好的杠杆是优化标签体系、清洗数据、扩充同义表达而不是继续堆算力。如果你的场景是“本院认为”之后的语义推理比较多比如区分“故意”和“过失”再考虑深度模型才有明显收益。5.4 模型上线后的标签漂移怎么处理业务方的文书格式会随着时间变化标签分布也会漂移。上线之后需要定期抽样做人工复核把那些模型置信度在 0.4 到 0.7 之间的边界样本挑出来重新查看。边界样本往往是标签体系定义不清的体现反馈给业务方后可以进一步修正标签。不要试图让模型自己去承担所有模糊地带人机协同才是常态。写在最后的一点经验法律文本分类这件事技术难度其实是逐级递增的最难的不在模型而在对文本结构的理解和标签体系的把控。如果你现在正准备拿这个题目做项目我的建议是先动手跑通 TF-IDF SVM 的完整链路哪怕效果一般也能帮你理解数据里的坑。之后再对照结果决定要不要上 BERT。我自己的体会是每个数据集都有它的“脾气”。公开数据集、法院导出的电子文本、扫描件转文字三种来源的预处理策略完全不同千万别拿一套流水线通吃。分类项目做完之后一定要保留一份完整的“数据血缘”文档记录每个字段从原始状态到最终特征的每一步改动。这个习惯在项目交接和线上问题回溯时能救你无数次。本文还有配套的精品资源点击获取