ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

法律智能问答系统实战:神经网络分类+WMD相似度匹配

法律智能问答系统实战:神经网络分类+WMD相似度匹配 简介基于神经网络的法律智能问答系统项目包面向希望入门自然语言处理与法律智能应用的学习者适合作为毕业设计、课程作业或初期项目实践。资源围绕法律文本分类与相似问匹配整合了劳动法、劳动合同、工伤保险条例等领域的问答数据并附带中文停用词与关键词表便于复现与二次开发。压缩包共30个文件以Python源码、CSV语料数据、TXT文本、训练模型及pyc缓存为主整体大小37.48MB。其中6个py脚本涵盖GUI交互、文本预处理、相似度匹配与模型训练等流程11个CSV提供真实法律问答与关键词对照数据2个model文件保存已训练好的分类模型可直接加载使用。目前已有125人学习浏览资源目录结构清晰既适合按步骤训练调试也可直接运行观察效果对理解神经网络问答系统的完整搭建流程具有明确的参考价值。1. 法律问答系统的第一步用神经网络分类把检索范围砍掉八成用户问一句「公司不签劳动合同怎么办」传统搜索引擎会甩回几百个法条链接而法律智能问答系统要的是直接给出一段能用的维权话术。这份资源是一套完整的中文法律场景检索式问答实现先把用户问题送入神经网络分类模型判断它属于劳动法、劳动合同、工伤、辞退解雇中的哪一类再把检索范围缩小到对应领域的标准问答库里用相似度匹配召回最相近的问题返回预先整理好的答案。代码覆盖数据清洗、停用词过滤、模型训练、相似度匹配和 GUI 交互全流程适合做毕设、课程设计也适合刚接触 NLP 项目的人拿真实场景练手。2. 数据层的组织方式分类语料与标准问答库的分工整套系统能跑通的关键不在模型有多深而在数据拆得够不够清楚。解压LawSystem-dev.zip之后可以看到data目录下同时存在三类东西分类训练语料、标准问答对、停用词表。这三者职责完全不同很多项目翻车就翻在把它们混在一起用。2.1 数据文件清单与各自职责先按文件用途把整套数据梳理一遍后面操作时好对号入座。文件类型角色questions_train.csv分类训练语料用户问题文本 类别标签训练神经网络分类器classify.model产出的模型文件训练好的分类模型运行时直接加载关键词-劳动法.csv等领域关键词表每个类别对应的法律关键词辅助分类和检索劳动合同.csv、工伤事故.csv等标准问答对每个领域下的「标准问句 - 标准答案」对问题数据1.txt/回答数据1.txt原始问答文本未清洗的问答语料可能是标注前的中间产物qs_stopwords.txt/baidu_stopwords.txt停用词表用于分词后过滤无意义词未知.csv、write.txt中间产物调试或数据拼接时生成的文件不是核心输入questions_train.csv是分类模型的输入classify.model是训练产物而劳动合同.csv这类文件是最终检索时要用的答案来源。我一般会把训练语料和回答库分开管理这样后续新增领域时不需要动已经训练好的分类器只需要补数据和重训。2.2 标准问答对的格式与领域划分从文件名能直接看出领域边界劳动法、劳动合同、工伤保险、工伤事故、辞退解雇、员工权益、维权方式、劳动保险。每个文件对应一个二级类别而关键词-劳动法.csv这组文件又把这些类别跟具体法律关键词挂上了钩。标准问答对的常见格式是两列一列是标准问句一列是标准答案。实际项目里标准问句往往不止一条同一个法律问题会有多种问法「公司不签合同怎么办」「没签劳动合同怎么维权」「入职一年没签合同违法吗」这些都要写进 CSV 的同一行或相邻行答案共享一条。这样做的好处是匹配阶段只需要算相似度不需要做答案生成省掉了文本生成模型那些不可控的问题。值得注意的是问题数据1.txt和回答数据1.txt这种纯文本文件它们可能是收集来的原始语料问题与回答按行对应。如果 CSV 里的问答对不够用可以把两份文本按行合并成 CSV 再补充进去但前提是行数要对齐否则答案会串位。2.3 停用词表为什么要自己维护一套qs_stopwords.txt和baidu_stopwords.txt双表并存这本身就是一个信号法律文本的停用词处理不能直接抄通用方案。百度停用词表是通用中文场景的里面包含大量「的」「了」「吗」「呢」这类高频虚词这些在法律问答里确实该滤掉。但法律文本里还有一批词比如「单位」「劳动者」「用人单位」「应当」它们在通用场景下可能被当成普通名词但在法律问答里是判别的关键特征不能进停用词表。我的处理习惯是先把两份停用词表合并去重再把法律领域的高频词从停用表里强制移除。合并之后用分词工具跑一遍现有语料看哪些词被错误过滤了手动补回一个legal_keepwords.txt。这套词表是跟着语料走的语料更新时停用词表也要重跑一遍不能一个表用到底。3. 训练分类模型从questions_train.csv走到classify.model数据准备好之后下一步是训练分类器。这套系统的设计思路是「先分类、再检索」分类器质量直接决定后面的检索范围。如果分类错了后面匹配做得再好也白搭因为答案库根本找错了方向。3.1 分类任务定义多标签还是单标签先要明确一点这是一个单标签多分类问题。用户一条问题只归入一个领域比如「上下班途中摔伤算工伤吗」归入工伤事故「试用期被辞退有补偿吗」归入辞退解雇。虽然法律问题存在跨领域的情况但在这套架构里分类只需要选出最相关的一个类别召回阶段再处理模糊边界。questions_train.csv的结构我按常见做法还原一下两列一列是问题文本一列是类别标签。标签直接用中文类别名比如劳动法、劳动合同、工伤事故。训练之前先把标签转成数字 ID或者直接用LabelEncoder编码。3.2 train.py分词、向量化、MLP训练的关键逻辑训练脚本的核心逻辑是读取 CSV、分词、TF-IDF 向量化、送入神经网络分类器。这里不用 CNN 或 LSTM因为训练语料规模通常只有几千条深度模型容易过拟合MLP 配合 TF-IDF 在这种中等规模文本分类任务上性价比最高。# train.py 核心训练流程 import pandas as pd import jieba from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.neural_network import MLPClassifier from sklearn.preprocessing import LabelEncoder import joblib df pd.read_csv(data/questions_train.csv, encodingutf-8-sig) # 假设 CSV 是两列question, label questions df[question].astype(str).tolist() labels df[label].tolist() # 1. 分词 def cut_text(text): return .join(jieba.cut(text)) questions_cut [cut_text(q) for q in questions] # 2. TF-IDF 向量化注意 max_features 限制维度 vectorizer TfidfVectorizer(max_features8000, ngram_range(1, 2)) X vectorizer.fit_transform(questions_cut) # 3. 标签编码 le LabelEncoder() y le.fit_transform(labels) # 4. MLP 分类器 clf MLPClassifier( hidden_layer_sizes(256, 128), activationrelu, solveradam, max_iter300, early_stoppingTrue, random_state42 ) clf.fit(X, y) # 5. 保存模型和向量器 joblib.dump(clf, data/classify.model) joblib.dump(vectorizer, data/vectorizer.model) joblib.dump(le, data/label_encoder.model)这段代码里有几个关键点需要说明。max_features8000是特征维度上限TF-IDF 在中文法律语料上特征数很容易冲到几万但不是每个特征都有判别力限制到 8000 既能保证分类效果又能让训练速度维持在一个可接受的范围。ngram_range(1, 2)把单个词和相邻词组合都纳入特征这样「不签合同」和「不签劳动合同」这种表达能保留部分词组信息比单纯用单个词的效果好一些。hidden_layer_sizes(256, 128)是两层隐藏层第一层 256 个神经元第二层 128 个。这个配置是处理几千条文本分类语料的经验值层数再深容易过拟合神经元再宽训练时间翻倍但收益很小。early_stoppingTrue会在验证集 loss 连续多轮不下降时提前终止训练避免白白浪费训练时间。最后把分类器、向量器、标签编码器三个文件都保存下来。运行时只加载classify.model是不够的向量化要和训练时保持一致否则预测结果完全不可信。3.3 模型评估与类别权重训练完之后不要急着部署先看一下各类别的召回情况。法律问答场景最怕的是某个冷门类别被普遍漏判比如「劳动保险」的问题数量远少于「劳动法」分类器会倾向于把边界问题全判给样本量大的类。from sklearn.metrics import classification_report pred clf.predict(X_val_cut) print(classification_report(y_val, pred, target_namesle.classes_))classification_report会输出每个类别的精确率、召回率、F1 值。如果看到某个类别的召回率低于 0.6就要检查是不是语料太少或者标签边界不清晰。我一般会为少数类适当增加样本或者给 MLP 设置class_weightbalanced让模型在训练时自动给少数类更高的权重。train.py训练好的模型文件大小通常只有几十 KB 到几百 KB因为 TF-IDF 特征矩阵是稀疏的MLP 参数量也不大。如果发现模型文件有好几个 GB大概率是向量器保存时出了问题把原始文本特征也存进去了这在加载时会拖垮内存。4. 检索匹配的下半场WMD相似度与match.py的召回策略分类模型把范围缩小到某个领域之后剩下的问题是在这个领域的标准问答库里找到最接近用户问题的那一条。这里用的是 WMDWord Movers Distance词移距离也就是wmd_process.py这个文件负责的部分。4.1 为什么是WMD法律措辞改写带来的词面差异法律问答里同一个意思的表达差异非常大。「公司没跟我签劳动合同」和「用人单位未与劳动者订立书面劳动合同」词面重叠度很低但语义几乎一样。TF-IDF 的余弦相似度在这种场景下表现很差因为 TF-IDF 依赖词面重合。WMD 的核心思想是把两个句子里的词向量做最优匹配计算词向量之间的累积距离词向量相近的词即使字面不同也能被识别为语义相近。代价是 WMD 的计算复杂度很高尤其是词表大、句子长的时候。所以系统架构里必须先做分类把候选集从几千条降到几百条WMD 的计算压力才扛得住。如果一上来就对全量问答库算 WMD一条请求的响应时间会到秒级甚至分钟级基本没法用。4.2 wmd_process.py词向量加载与WMD封装wmd_process.py的功能是加载词向量模型并对原始文本做 WMD 距离计算。中文场景下常用的是腾讯中文词向量或 fastText 训练的词向量文件格式是每行一个词加一串向量数值。词向量文件很大加载时要只保留词表内相关的词否则内存占用过高。# wmd_process.py WMD距离计算封装 import jieba from gensim.models import KeyedVectors class WMDProcessor: def __init__(self, wv_path): # 只加载词向量不加载训练语料节省内存 self.wv KeyedVectors.load_word2vec_format(wv_path, binaryFalse) def sentence_to_words(self, sentence): # 分词后只保留词向量里存在的词避免 OOV 词干扰距离计算 return [w for w in jieba.cut(sentence) if w in self.wv.key_to_index] def wmd_distance(self, sent1, sent2): words1 self.sentence_to_words(sent1) words2 self.sentence_to_words(sent2) # 如果某一侧没有词落入词表直接返回一个大数表示不相似 if not words1 or not words2: return 999.0 return self.wv.wmdistance(words1, words2)wmdistance是 gensim 对 WMD 算法的内置实现底层用 EM 算法求解词与词之间的最优运输问题。这个方法的输入不需要带词频权重直接传词语列表即可。if not words1 or not words2这个兜底逻辑很关键。遇到分词结果全是停用词或者词表外新词的句子wmdistance会报错或者返回异常值必须提前拦截。返回一个大数 999.0 表示这两句完全不相似让后续排序逻辑能正确处理这种边界情况。加载词向量时建议加limit200000之类的参数只加载前几十万个高频词中文词向量的完整文件动辄几个 GB全量加载会让 GUI 程序启动时间变得特别长。4.3 match.py分类过滤WMD排序的完整匹配match.py负责把分类和 WMD 串起来这也是整套系统的核心流程。先加载训练好的分类模型和向量器对用户输入做分类预测然后只在该类别对应的标准问答库上计算 WMD最后取距离最小的问答对结果返回。# match.py 分类 WMD 匹配主流程 import joblib import pandas as pd from wmd_process import WMDProcessor def load_qa_pool(domain): # 示例劳动合同.csv 包含 question, answer 两列 df pd.read_csv(fdata/{domain}.csv, encodingutf-8-sig) return df.to_dict(records) class LegalMatcher: def __init__(self, wmd_processor): self.clf joblib.load(data/classify.model) self.vectorizer joblib.load(data/vectorizer.model) self.le joblib.load(data/label_encoder.model) self.wmd wmd_processor def predict_domain(self, question): # 与训练时保持相同的分词和向量化流程 text .join(jieba.cut(question)) vec self.vectorizer.transform([text]) label_id self.clf.predict(vec)[0] return self.le.inverse_transform([label_id])[0] def match(self, question, top_k1): domain self.predict_domain(question) qa_pool load_qa_pool(domain) scored [] for item in qa_pool: # 标准问句可能是多条用最短距离作为该答案的得分 dist min(self.wmd.wmd_distance(question, q) for q in item[question]) scored.append((dist, item[answer])) scored.sort(keylambda x: x[0]) return domain, scored[:top_k]predict_domain方法里最容易被忽略的是分词和向量化的一致性。训练时用了 jieba 分词和 8000 维的 TF-IDF 向量器预测时也必须走完全一样的流程任何一步不一致都会导致向量维度对不上或者特征分布漂移。实际项目里不少诡异问题都出在这里。min(self.wmd.wmd_distance(question, q) for q in item[question])这句用到了「同一答案多条标准问句取最小距离」的思路。一个答案对应的标准问句可能有三四条用户问题只要命中其中任何一条就算匹配成功这是检索式问答里常用的做法。取最小距离而不是平均距离是因为只要有一条标准问句足够接近用户问题答案就值得返回。match最后返回的是(domain, scored[:top_k])domain 可以用于调试时看分类是否正确。top_k 默认取 1也可以调成 3由 GUI 层决定展示几条候选答案。这一步是把分类结果和相似度结果同时暴露给上层方便定位问题到底出在分类还是匹配。5. 避坑指南法律问答实战中的五个典型翻车现场这套系统跑通不难但跑得可靠是另一回事。以下几条坑是我在实际操作中反复遇到的按「现象 → 原因 → 解决」整理出来每一条都值得在动手前先看一眼。5.1 现象一CSV 读进来中文全是乱码现象pandas.read_csv(data/劳动合同.csv)读出来的列名和正文全是锟斤拷或者一类乱码。原因Windows 下用 Excel 编辑保存的 CSV 默认是 GBK 编码而 Python 的read_csv默认按 UTF-8 解析。两种编码不对齐中文直接变成乱码。法律问答语料里大量中文文本这个问题一旦出现后面的分词和模型训练全部建立在错误数据上结果完全不可信。解决读取时统一指定encodingutf-8-sig这个编码能自动剥离 UTF-8 BOM 头同时兼容大部分从 Excel 导出的文件。如果读进来还是乱码再尝试encodinggbk。最省事的方式是写一个脚本把data目录下所有 CSV 统一转成 UTF-8 编码一劳永逸import glob import pandas as pd for path in glob.glob(data/*.csv): # 先按 GBK 尝试读取失败再按 UTF-8 try: df pd.read_csv(path, encodinggbk) except UnicodeDecodeError: df pd.read_csv(path, encodingutf-8-sig) df.to_csv(path, indexFalse, encodingutf-8-sig)从那以后我拿到任何带中文的 CSV 都会先跑一遍这个脚本省掉后面一整串乱码引发的连锁问题。5.2 现象二停用词把否定词滤掉答案方向反了现象用户问「企业没有给我缴社保怎么办」系统检索出来的答案却是「企业已依法缴纳社保」这类完全不搭边的内容。原因停用词表里包含了「没有」「不」「未」这类否定词分词后被过滤掉了。「没有缴社保」变成了「缴社保」语义完全翻转。通用停用词表是按「高频无实义」标准构建的不会考虑否定词对语义的影响。在法律文本里「未」「无」「不」恰恰是决定事实认定的核心词滤掉等于自废武功。解决把否定词和高频法律限定词从停用词表里移除。具体做法是在加载停用词表后追加一个强制保留集合# 强制保留的否定词和法律限定词 keep_words {未, 无, 不, 没有, 应当, 必须, 不得, 用人单位, 劳动者} stopwords set(stopwords) - keep_words处理完这一条我专门用「没有」「未」「不得」三个词开头的问句做了一组回归测试确认这些关键语义没有被第二次过滤掉。5.3 现象三分类器把八成问题都判成劳动法现象模型训练完用测试集评估准确率看着有 85%但实际输入一条「工伤骨折怎么赔偿」分类结果也落到劳动法检索返回的答案是劳动法领域的跟工伤完全不相关。原因训练语料类别分布严重不均衡。劳动法相关的问题可能有 600 条工伤只有 80 条模型学到的是「猜劳动法已经能拿很高准确率」的偷懒策略对真正的工伤特征没有建立有效判别。classification_report里能看到劳动法的召回率极高工伤的召回率很低但整体准确率被大类的数量拉上去了表面数字很有欺骗性。解决给 MLP 设置class_weightbalanced让模型按类别数量的反比调整权重。同时人工检查questions_train.csv里每个类别的样本条数少于 100 条的类别需要补充数据。我当时给工伤事故类补了 60 条问题做法是从裁判文书网的常见问法里改写成不同表达补完之后工伤类别的召回率从 0.4 提到了 0.7 以上。5.4 现象四WMD 计算慢到没法用现象输入一个问题GUI 界面转圈十几秒才出结果偶尔直接卡死。原因WMD 需要对候选问答库里的每一条标准问句做词向量最优运输计算这个过程本身就不快。更常见的问题是词向量文件全量加载内存占用高导致系统交换分区频繁再加上没有对候选集做剪枝几千条问答逐条算距离耗时自然爆炸。解决三层优化叠加。第一层是分类过滤这个系统本身已经做了确认predict_domain返回的类别是正确的。第二层是词向量加载时加limit200000只保留高频向量。第三层是提前对问答库做一次 TF-IDF 粗筛把距离明显很大的候选直接砍掉只留最相似的 50 条再做 WMD。粗筛用余弦相似度计算速度是 WMD 的几十倍虽然精度一般但作为前置过滤器足够了。5.5 现象五gensim 版本升级后 WMD 接口报错现象之前能跑的wmdistance代码换了 gensim 版本之后直接抛AttributeError: KeyedVectors object has no attribute wmdistance。原因gensim 4.x 大版本重构了 APIwmdistance被移动到了独立的wmd模块不再直接挂在KeyedVectors上。很多人跑别人的代码时环境版本对不上就会遇到这个问题。解决检查 gensim 版本然后用兼容写法处理from gensim.models import KeyedVectors # gensim 3.x 直接用实例方法4.x 需要从 wmd 模块导入 try: dist wv.wmdistance(words1, words2) except AttributeError: from wmd import WMD wmd_calculator WMD(wv) dist wmd_calculator.wmdistance(words1, words2)更省事的方案是锁版本pip install gensim3.8.3这个版本的 WMD 接口最稳定网上大部分法律问答开源项目的代码都是基于这个版本写的。搭配wmd_process.cpython-38.pyc这个文件来看原作者大概率用的就是 Python 3.8 对应旧版本 gensim。6. 把链路串起来GUI 启动、端到端验证与语料追加习惯模型训练好、匹配逻辑调通之后最后一步是验证整套链路是否真正可用。gui.py和myLawChat.py负责提供交互界面但直接启动 GUI 之前我习惯先用一批没进过训练集的问题做端到端测试确认分类、召回、答案返回三个环节都没问题。# 端到端验证脚本用未参与训练的问题走完整链路 from match import LegalMatcher from wmd_process import WMDProcessor # 加载词向量注意路径替换成实际词向量文件位置 wmd_proc WMDProcessor(data/word_vectors.vec) matcher LegalMatcher(wmd_proc) test_questions [ 公司拖欠我三个月工资怎么要回来, 上班路上骑电动车摔倒算工伤吗, 试用期没过被辞退有赔偿吗, 单位不给我买社保合法吗, 工伤鉴定结果不服可以申请重新鉴定吗 ] for q in test_questions: domain, results matcher.match(q, top_k1) print(f问题: {q}) print(f分类: {domain}) print(f答案: {results[0][1][:50]}...) print()这五条问题覆盖了劳动报酬、工伤认定、辞退补偿、社保缴纳、工伤鉴定五个不同子领域。如果每一条的分类结果都落在正确的领域且答案文本与问题明显相关说明从分类器到 WMD 匹配全链路是通的。如果某一条分类错了优先检查questions_train.csv里该领域的样本量和分布如果分类对了但答案不对问题多半出在标准问答库的覆盖度上。GUI 启动前还要确认myLawChat.py里的模型路径和词向量路径与实际文件位置一致。路径写错是最低级的错误但出现的频率最高。检查方式是在启动前打印一下路径对应的文件是否存在import os for p in [data/classify.model, data/vectorizer.model, data/label_encoder.model]: assert os.path.exists(p), f缺少模型文件: {p}这套系统上线运行之后最大的维护压力来自语料更新。法律条文会修订用户的问法也在变我养成的习惯是每次新增语料都强制走一遍固定流程先把新问题的分类跑一遍看classify.model是否判到了正确领域再跑一次 WMD 匹配看是否召回了预期答案最后把没召回的、分类错误的问题全部补进questions_train.csv和对应的标准问答 CSV重新执行train.py。这个流程走一次只要几分钟但能避免系统越用越偏。我有一次偷懒跳过了重训练直接把新数据加进了问答库结果分类器还是旧版本新数据里的问题全被分到了错误领域那一次让我长记性了。从那以后每次动语料都强制跑一遍全流程验证语料和模型必须同步更新这套习惯救了我好几回。希望帮到你。本文还有配套的精品资源点击获取
返回列表