尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

级联无监督-监督NLP流水线:公共采购指责性语言自动检测

级联无监督-监督NLP流水线:公共采购指责性语言自动检测 在公共采购的评标报告、质疑答复函和供应商评价文档里存在一种容易被忽略却值得警惕的文本类型指责性语言。它可能表现为对某供应商履约能力的强烈质疑也可能是对评标程序的隐性不满。这类文本一旦分散在大量日常文书里人工筛查的成本极高但漏掉又可能带来合规风险。针对这个场景本文提出并拆解一种级联无监督-监督 NLP 流水线用来在公共采购文本中自动检测指责性语言。先说结论在标注数据稀缺的公共采购领域直接训练一个有监督分类器往往不稳。更务实的做法是让无监督模型先做高召回候选筛选再由有监督模型进行精确分类两者级联组合既降低标注成本又提升检测精度。读完本文你会理解这套流水线的设计动机、各模块的职责边界、完整代码实现以及在实际项目中接入时最容易踩的坑。1. 这篇文章真正要解决的问题1.1 公共采购文本中的指责性语言到底是什么公共采购全流程会产生大量文本招标公告、投标文件、评标报告、质疑函、投诉答复、履约评价、合同变更记录等。指责性语言指的是其中带有否定、质疑、批评、控诉意味的表达比如“该供应商伪造资质文件”“评标委员会未按程序进行公示”“中标人在履约过程中多次拖延”等。这类语言在普通文档里只是表达态度但在公共采购语境中它可能指向围标串标线索、供应商失信行为、评标程序瑕疵或廉洁风险。从技术角度看它是强主观性、强上下文依赖的一类文本简单规则难以覆盖常规关键词匹配又容易误伤中性表达。1.2 为什么人工审查和规则筛选都撑不住公共采购项目的文本量很大一个中大型项目从发标到履约结束涉及的文书可达数百份而合规审查人员和审计人员的时间有限。人工逐份阅读的问题不只是慢还在于不同人对“指责性”的判断标准不一致导致漏检率不稳定。纯规则的方式同样有局限。你可以用“质疑”“投诉”“违规”这类词去匹配但很快会发现两个问题一是大量中性文本也会包含这些词比如“针对质疑事项现答复如下”二是真正的指责性表达有时非常委婉比如“希望未来能够在同等条件下给予外地供应商更公平的对待”这种句子不包含任何敏感词但情绪倾向明显。1.3 级联无监督-监督方案的核心思路本文讨论的方案用一个级联结构把两类方法串联起来第一级是无监督候选挖掘目标是尽可能多地找出“疑似指责性”文本宁可多召回不能漏掉第二级是有监督分类目标是在候选集里精确区分“真指责”和“中性表达”把精确率提上去。这个设计的核心假设是在公共采购领域用于训练的标注数据不会太多但未标注的原始文本很容易获得。先用无监督方法从原始文本中挖掘候选样本、生成伪标签再配合小规模人工标注训练监督模型比直接标注上千条数据再训练要省力得多也更适合实际项目的落地节奏。2. 核心概念与设计原理2.1 无监督、监督、级联分别指什么无监督学习不依赖标注数据算法从文本自身的结构中发现规律。在这套流水线里我们用它做两件事一是通过种子词和共现关系构建领域词典二是通过聚类分析找出语义相近的文本簇从而定位疑似指责性表达。有监督学习需要标注数据通过“文本-标签”对训练分类器。在流水线里它负责把无监督阶段筛出的候选文本做二次判断输出最终的“是否指责性”二分类结果。级联Cascaded的含义是多个模型按照先后顺序串联前一个模型的输出是后一个模型的输入。这种设计之所以在公共采购场景中有效是因为两个模型的优化目标不同前级追求召回率后级追求精确率组合成一个“粗筛精排”的结构。2.2 为什么不用纯监督或纯无监督方案纯监督方案的瓶颈在于标注成本。公共采购文本的专业性强标注人员需要理解招标流程、评标规则和行业术语普通人很难区分“程序性描述”和“指责性表达”。如果没有足够的高质量标注数据监督模型容易过拟合到少量样本上。纯无监督方案的瓶颈在于结果不可控。聚类和关键词扩展可以发现一些模式但无法给出明确的分类边界也难以满足业务对精确率的要求。比如“该供应商在同类项目中有丰富经验”和“该供应商在同类项目中弄虚作假被多次通报”在字面上相似度很高只有通过有监督判别才能稳定区分。2.3 级联结构与“粗筛精排”的类比可以把这套流水线类比成一个筛矿流程第一级用筛孔很大的筛子把大量不含矿的石头直接滤掉留下少量的候选矿石第二级用更精细的筛子从候选矿石里挑出真正的矿粒。在工程实现中这意味着第一级应该尽量简单、快速、覆盖广第二级可以稍微重一点因为它只处理第一级留下的少量文本。整体上系统拥有比单一模型更高的吞吐量和更可控的误判率。3. 适用场景与边界3.1 适合用这套方案的场景第一类是质疑投诉预审。采购机构收到质疑函后可以先通过流水线自动识别其中的指责性语言辅助判断是否需要进入正式受理流程。第二类是评标报告复核。评标报告中如果出现对某投标人的强烈负面评价系统可以提醒复核人员关注相关表述避免因措辞不当引发后续争议。第三类是供应商履约评价。合同履约阶段的评价文本往往措辞含蓄流水线可以帮助采购方自动提取其中的负面信号提前发现履约风险。3.2 不适合用这套方案的场景这套方案不适合需要精确法律定性的场景。比如“该供应商行为是否构成违法”这是法律判断需要结合证据链和法规条文文本分类只能提供线索不能给出结论。另外如果项目只有几十条文本完全不需要上模型。直接人工阅读反而更快、更准。流水线的价值是在数百条以上、且持续有新文本产生的场景中才体现出来。4. 环境准备与数据说明4.1 运行环境与依赖库本文示例使用 Python 编写建议使用 Python 3.9 或更高版本。核心依赖库包括pandas 用于数据处理scikit-learn 用于特征提取和分类建模jieba 用于中文分词。实际项目中的版本以你所在环境为准本文重点是流程实现。pip install pandas scikit-learn jieba4.2 数据格式说明输入数据使用 CSV 格式包含两列content 是采购文本内容label 是标注标签。其中 1 表示指责性文本0 表示中性文本。为了演示流程这里构造一个最小数据集。实际项目中label 列在第一阶段可能缺失由无监督模块负责生成伪标签。idcontentlabel1该供应商在过往项目中多次延误工期履约能力令人严重怀疑。12本项目已完成验收各项指标满足合同要求。03质疑函中指出代理机构存在程序违规但答复仅以模板敷衍了事。14经复核本次评标过程符合相关法律法规规定。05中标候选人提供的财务数据与公开年报差异巨大涉嫌弄虚作假。16供应商提交的资质文件齐全资格审查通过。05. 核心流程拆解5.1 阶段一无监督候选挖掘无监督阶段的目标是召回不是精准分类。做法分为两步第一步定义一组公开采购场景下的种子关键词如“质疑”“投诉”“弄虚作假”“违规”“不公”等第二步基于词共现关系扩展种子词表把经常与种子词一起出现的词纳入词典。同时我们用 TF-IDF 向量化所有文本再做 KMeans 聚类。聚类完成后统计每个簇中与扩展词典匹配的文本比例匹配比例高的簇被标记为“候选指责簇”簇内文本进入第二级。这个环节不要求簇的语义完全纯净只要能把大部分正例文本留在候选集里。5.2 阶段二有监督分类有监督阶段使用一个轻量级分类器本文选择逻辑回归。特征包括 TF-IDF 向量、扩展词典命中数、句子长度等。样本来源有两个一是无监督阶段标记的高置信候选样本二是少量人工标注的确定性负样本。训练时要注意样本不平衡问题。如果候选集中的正样本远多于负样本可以适当添加负样本或者在分类器中使用 class_weight 参数。这个阶段的输出是每个候选文本的指责性概率。5.3 阶段三级联推理推理阶段按顺序调用两个模块先经过无监督候选筛选如果文本没有命中任何候选信号直接判定为中性如果命中再交给有监督分类器计算概率根据业务需要设置一个概率阈值比如 0.5输出最终结果。这样做的好处很实际大部分中性文本在第一级就被滤掉有监督分类器的推理压力大幅降低系统整体耗时可控同时减少中性文本被误判为正例的概率。6. 完整示例代码实现6.1 文件结构与数据准备建议按下面的目录结构组织代码procurement_audit/ ├── data/ │ └── procurement_samples.csv ├── detectors/ │ ├── __init__.py │ ├── unsupervised.py │ ├── supervised.py │ └── cascade.py ├── train_eval.py └── requirements.txt先准备数据文件这里用 8 条构造样例做演示实际项目请替换为真实文本。# 文件路径data/prepare_demo_data.py import pandas as pd rows [ {id: 1, content: 该供应商在过往项目中多次延误工期履约能力令人严重怀疑。, label: 1}, {id: 2, content: 本项目已完成验收各项指标满足合同要求。, label: 0}, {id: 3, content: 质疑函中指出代理机构存在程序违规但答复仅以模板敷衍了事。, label: 1}, {id: 4, content: 经复核本次评标过程符合相关法律法规规定。, label: 0}, {id: 5, content: 中标候选人提供的财务数据与公开年报差异巨大涉嫌弄虚作假。, label: 1}, {id: 6, content: 供应商提交的资质文件齐全资格审查通过。, label: 0}, {id: 7, content: 在同等条件下评委对本地企业明显倾向对外地供应商不够公平。, label: 1}, {id: 8, content: 根据合同约定我方已按期支付工程进度款。, label: 0}, ] df pd.DataFrame(rows) df.to_csv(data/procurement_samples.csv, indexFalse, encodingutf-8-sig) print(df)6.2 无监督候选挖掘模块无监督模块包含两个核心函数一个是基于种子词表的关键词命中统计另一个是基于共现关系的词典扩展。种子词表需要根据真实业务文本调整这里演示的是公共采购场景的常见负面词汇。# 文件路径detectors/unsupervised.py import jieba from collections import Counter from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.cluster import KMeans SEED_WORDS [ 质疑, 投诉, 弄虚作假, 违规, 不公, 严重怀疑, 敷衍, 伪造, 虚假, 拖延, ] def tokenize(text): return [w for w in jieba.lcut(text) if w.strip()] def keyword_hit_count(text, lexicon): tokens set(tokenize(text)) return sum(1 for w in lexicon if w in tokens) def expand_lexicon(docs, seed_words, top_k20): co_occur Counter() for doc in docs: tokens set(tokenize(doc)) for seed in seed_words: if seed in tokens: for t in tokens: if t ! seed and len(t) 2: co_occur[t] 1 return list(dict(co_occur.most_common(top_k)).keys()) seed_words def find_candidate_clusters(texts, lexicon, n_clusters3): vec TfidfVectorizer(tokenizertokenize, max_features1000) X vec.fit_transform(texts) km KMeans(n_clustersn_clusters, random_state42, n_init10) labels km.fit_predict(X) cluster_hit_ratio {} for i in range(n_clusters): cluster_texts [texts[j] for j in range(len(texts)) if labels[j] i] hit_count sum(1 for t in cluster_texts if keyword_hit_count(t, lexicon) 0) ratio hit_count / max(len(cluster_texts), 1) cluster_hit_ratio[i] ratio return labels, cluster_hit_ratio这段代码的关键逻辑是先分词再统计文本对词典的命中数聚类之后计算每个簇的词典命中占比。命中占比高的簇会被视为候选簇。6.3 有监督分类模块有监督模块把无监督阶段的输出与文本特征拼接在一起训练逻辑回归分类器。这里为了让流程更稳定特征除了 TF-IDF 外还加入词典命中数作为额外特征。# 文件路径detectors/supervised.py import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from scipy.sparse import hstack class LexiconFeature: def __init__(self, lexicon): self.lexicon lexicon def transform(self, texts): return np.array([[keyword_hit_count(t, self.lexicon)] for t in texts]) def fit(self, texts, yNone): return self def train_supervised_classifier(df_train, lexicon): tfidf TfidfVectorizer(tokenizertokenize, max_features1000) lexicon_feat LexiconFeature(lexicon) clf LogisticRegression(max_iter1000, class_weightbalanced) X_tfidf tfidf.fit_transform(df_train[content]) X_lex lexicon_feat.transform(df_train[content]) X hstack([X_tfidf, X_lex]) clf.fit(X, df_train[label]) return tfidf, lexicon_feat, clf def predict_proba(texts, tfidf, lexicon_feat, clf): X_tfidf tfidf.transform(texts) X_lex lexicon_feat.transform(texts) X hstack([X_tfidf, X_lex]) return clf.predict_proba(X)[:, 1]这里使用 scipy.sparse.hstack 将 TF-IDF 稀疏矩阵和词典特征拼接避免内存膨胀。class_weightbalanced 可以在样本不均衡时让模型更关注少数类。6.4 级联流水线整合级联模块负责串联两个阶段。先调用无监督阶段的词典命中方法判断候选性再调用有监督分类器输出最终概率。# 文件路径detectors/cascade.py class CascadedProcurementDetector: def __init__(self, lexicon, tfidf, lexicon_feat, clf, candidate_threshold0, prob_threshold0.5): self.lexicon lexicon self.tfidf tfidf self.lexicon_feat lexicon_feat self.clf clf self.candidate_threshold candidate_threshold self.prob_threshold prob_threshold def is_candidate(self, text): return keyword_hit_count(text, self.lexicon) self.candidate_threshold def predict(self, text): if not self.is_candidate(text): return 0, 0.0 prob predict_proba([text], self.tfidf, self.lexicon_feat, self.clf)[0] return int(prob self.prob_threshold), prob这个类的设计思路是把两个阶段封装为统一接口后续切换无监督方法或替换有监督模型时只需要修改内部实现不影响上层调用。6.5 训练与评估脚本训练脚本中包含数据读取、无监督词典扩展、候选集划分、监督分类器训练以及最终的测试评估。# 文件路径train_eval.py import pandas as pd from sklearn.model_selection import train_test_split from detectors.unsupervised import expand_lexicon, SEED_WORDS, keyword_hit_count, find_candidate_clusters from detectors.supervised import train_supervised_classifier from detectors.cascade import CascadedProcurementDetector df pd.read_csv(data/procurement_samples.csv) texts df[content].tolist() y df[label].values lexicon expand_lexicon(texts, SEED_WORDS, top_k10) print(扩展后的词典, lexicon) # 无监督候选筛选 df[candidate] df[content].apply(lambda t: keyword_hit_count(t, lexicon) 0) print(候选集数量, df[candidate].sum()) # 用无监督结果构造训练样本 df_train df[df[candidate] | (df[label] 0)].copy() X_train, X_test, y_train, y_test train_test_split( df_train[content], df_train[label], test_size0.2, random_state42 ) train_data pd.DataFrame({content: X_train, label: y_train}) tfidf, lexicon_feat, clf train_supervised_classifier(train_data, lexicon) detector CascadedProcurementDetector( lexiconlexicon, tfidftfidf, lexicon_featlexicon_feat, clfclf, candidate_threshold0, prob_threshold0.5, ) for text, true_label in zip(X_test, y_test): pred, prob detector.predict(text) print(f文本{text[:30]}... 预测{pred} 真实{true_label} 概率{prob:.3f})7. 运行结果与效果验证7.1 预期输出示例在演示数据集上运行训练脚本你会看到类似下面的输出扩展后的词典 [代理机构, 质疑, 符合, 合同, 供应商, 项目, 履约, 材料, 规定, 资质, 质疑, ...] 候选集数量 6 文本在同等条件下评委对本地企业明显倾向... 预测1 真实1 概率0.873 文本供应商提交的资质文件齐全资格审查通过... 预测0 真实0 概率0.120注意不同随机种子和样本划分下数字会有浮动。关键是确认三个目标第一无监督词典扩展能把“质疑”“投诉”等种子词周边的高频词纳入第二候选集能够覆盖大部分 label 为 1 的文本第三级联预测的准确率达到可用水平。7.2 如何判断流水线是否有效判断流水线是否有效需要同时看召回率和精确率。在业务上第一级更关心召回率因为漏掉一个真正有风险的信息比多判断一个中性文本代价更高。第二级更关心精确率因为频繁误报会消耗审查人员的大量精力。建议在真实项目中记录三组指标第一级候选集对正例的召回率、第二级最终输出的精确率、以及整体 F1。如果第一级召回率低于 80%需要扩展种子词表或调整聚类参数如果第二级精确率过低需要增加高质量标注样本或调高概率阈值。7.3 运行失败时先看哪里启动报错时优先检查依赖库版本。jieba 分词版本差异可能导致分词结果不同scikit-learn 版本不同可能导致部分参数不兼容。其次检查 CSV 文件编码建议使用 UTF-8 或 UTF-8-sig避免中文乱码。8. 常见问题与排查思路问题现象可能原因排查方式解决方案聚类簇几乎没有正例种子词表与领域不匹配打印每个簇的高频词扩充种子词加入业务方提供的典型负面表达词典扩展出大量无关词共现窗口过大查看扩展词表设置共现窗口最小词频限制 top_k有监督模型预测全为 0负样本占比过高输出 class_weight 前后类别权重开启 class_weightbalanced级联后召回率下降明显第一级候选阈值设置过高统计 candidate 列分布将 candidate_threshold 调到 0提高召回处理真实长文本时速度慢TF-IDF 特征维度过大查看 vectorizer 输出维度限制 max_features或采用过滤式特征选择中文编码显示乱码CSV 编码不是 UTF-8用文本编辑器查看文件编码统一使用 utf-8-sig 写入和读取9. 最佳实践与工程建议9.1 让业务方参与种子词表构建种子词表是整条流水线的起点直接影响无监督阶段的召回效果。实际操作中不要只让算法工程师凭经验写种子词应该邀请采购业务人员或合规审查人员参与提供他们在实际工作中经常遇到的表达方式。这样能够显著降低种子词表的偏差。9.2 概率阈值要根据业务代价调整级联模型中最后的概率阈值不是固定值。如果系统做的是风险预警应该把阈值调低宁可多预警也要减少漏报如果系统做的是自动化归档对误报容忍度低可以把阈值调高。9.3 无监督部分也要定期重新训练很多团队把有监督模型纳入了定期训练流程却忽略了无监督部分的词典和聚类参数也需要更新。公共采购的文本风格会随着政策变化而改变比如新的监管要求会引入一批新的敏感词。建议设置月度或季度级别的重新训练节奏确保词典不过时。9.4 不能把模型输出直接当结论必须明确一个边界这套流水线的输出是风险线索不是违规结论。在实际业务系统中建议把模型预测结果标记为“建议人工复核”而不是直接生成处罚或投诉处理决定。这样可以避免模型误差引发业务风险也为后续人机协同留出空间。10. 总结与后续学习方向级联无监督-监督 NLP 流水线解决的核心问题是在标注数据有限的公共采购文本场景下用“先召回、后精确”的方式实现指责性语言检测。无监督阶段通过种子词、词典扩展和聚类分析锁定候选文本有监督阶段在少量标注样本的基础上做精确分类两级联动在降低标注成本的同时保持了可接受的准确率。这套设计并不局限于公共采购。同样的结构可以迁移到投诉工单分类、舆情评论风险识别、审计报告异常描述提取等场景只需替换种子词表和业务样本。下一步值得研究的方向包括引入领域预训练模型提升文本表示质量用主动学习策略减少标注样本量以及把流水线接入审计系统的实时告警链路。建议先基于这篇文章的示例代码跑通最小流程再替换为真实采购文本观察候选集覆盖率与分类精度的变化。代码本身适合作为项目原型在验证有效后再逐步完善工程化细节。
返回列表