尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

级联式NLP Pipeline:政府采购文本指控性语言自动筛查实战

级联式NLP Pipeline:政府采购文本指控性语言自动筛查实战 政府采购文本量逐年增长投诉质疑内容的自动化筛查是一个很实际的需求。手工翻阅招标文件、质疑函、投诉书来定位“倾向性条款”“围标串标表述”“排斥潜在供应商”等指控性语言耗时且容易漏。本文围绕一个级联式 NLP Pipeline 方案展开先通过无监督方法做候选召回再用有监督模型做精细分类兼顾标注成本、模型精度与可解释性适合做文本审核、合规分析、招标文件筛查相关项目的开发者参考。1. 背景与核心概念1.1 为什么要在政府采购文本中识别指控性语言政府采购公开文本中包含大量非结构化信息例如招标公告、招标文件、评标报告、质疑函、投诉书、中标公告等。这些文本中经常出现对采购行为合法性的质疑比如招标文件参数指向特定品牌资格条件存在歧视性、倾向性投标人之间存在围标、串标嫌疑评标过程存在利益冲突或程序违规。这些语言在文本中往往不是直接声明而是以“疑似”“涉嫌”“可能”“不排除”等委婉表达出现。人工检测需要阅读大量段落并做语义判断不仅效率低还容易受到个人经验差异的影响。通过 NLP 技术自动识别指控性语言可以把待审文本按风险等级排序缓解人工审核压力。本文的 Pipeline 目标不是判断政府采购活动本身是否违规而是识别“文本中是否存在指控性语言”。这个边界需要首先明确否则后续模型设计容易走偏。1.2 无监督与有监督组合的动机纯有监督方案的问题在于标注样本难以获取。政府采购文本中的指控性语言属于长尾分布正样本占比低、表述形式多样人工标注成本很高。纯无监督方案虽然可以省去标注但规则和聚类的结果通常不够精细难以满足准确率要求。一个可行的思路是级联组合无监督阶段利用关键词规则、TF-IDF 向量化、KMeans 聚类把海量原始文本压缩成少量候选段落有监督阶段在相对较小的候选集上训练分类模型判断该段落是否包含指控性语言。这种设计既降低了对大规模标注数据的依赖又保留了有监督分类的精度。1.3 什么是级联 Pipeline级联 PipelineCascaded Pipeline指的是将多个处理模块按顺序串联前一个模块的输出作为后一个模块的输入。在 NLP 场景中常见的 Pipeline 形式包括文本清洗 → 分词 → 特征提取 → 建模召回 → 过滤 → 排序 → 输出无监督粗筛 → 有监督精排。本文的方案属于第三种。级联的好处是职责单一、便于替换、中间结果可审计。比如后续希望用预训练语言模型替换有监督分类器只需要修改第二阶段的代码不需要动第一阶段。2. 任务定义与数据准备2.1 任务边界本文要做的任务可以形式化描述为输入一段政府采购相关文本输出一个二分类标签label1表示文本中包含指控性语言0表示文本中不包含指控性语言。更细粒度可以把指控性语言进一步分为多个子类型例如“参数倾向性”“围标串标”“程序违规”“利益冲突”。本文先用二分类演示完整流程子类型可以在有监督阶段扩展为多分类任务。2.2 数据字段与示例实际项目中原始数据可能来自多个异构系统建议统一落成 CSV 格式至少包含以下字段字段名类型说明doc_idstr文档唯一标识source_typestr文本来源如质疑函、投诉书、招标文件textstr待分析的文本段落labelint是否有指控性语言0/1用于有监督训练下面是一个标注样本示例doc_id,source_type,text,label D001,质疑函,本项目招标文件参数明显指向特定品牌涉嫌排斥潜在供应商。,1 D002,中标公告,评标过程符合法定程序各投标人报价均在预算范围内。,0 D003,投诉书,两家投标人投标文件制作机器码一致疑似围标串标。,1 D004,招标文件,供应商须在合同签订后30日内完成交付逾期按合同约定处理。,02.3 数据标注难点标注指控性语言需要注意几个容易被忽略的问题委婉表达指控不一定使用激烈词更多是“疑似”“涉嫌”“不排除”这类词否定与反事实例如“不存在倾向性”也包含关键词“倾向性”但语义是否定上下文依赖一个段落单独看可能是中性的但结合前文会构成指控标注一致性不同标注员对“是否算指控”可能理解不同需要事先制定标注规范并计算标注一致性。3. Cascaded Pipeline 整体架构设计3.1 两阶段架构整个 Pipeline 可以分为两大阶段原始采购文本 │ ▼ ┌────────────────────┐ │ 阶段一无监督召回 │ │ 规则命中 聚类过滤 │ └────────────────────┘ │ 候选文本 ▼ ┌────────────────────┐ │ 阶段二有监督分类 │ │ TF-IDF LR/SVM │ └────────────────────┘ │ ▼ 指控性语言结果第一阶段的目标是“不漏”第二阶段的目标是“不错”。两个阶段的目标不同因此评估指标也应该分开看。3.2 每一阶段的职责第一阶段无监督文本清洗与中文分词基于指控性关键词表的规则召回召回结果做 TF-IDF 向量化使用 KMeans 聚类过滤明显与指控主题无关的文本。第二阶段有监督基于人工标注数据训练分类器对第一阶段召回的候选文本做最终判定输出结构化结果供审核人员复核。3.3 为什么先无监督后有监督如果只用有监督模型处理全部政府采购文本会遇到两个问题。第一正负样本极不平衡大量中性文本会淹没少数指控性表达模型容易偏向多数类。第二全量文本进入分类器会导致推理耗时和成本上升尤其在语料达到几十万段甚至上百万段时。级联设计让无监督阶段先做粗粒度筛选。规则命中保证了高召回漏斗聚类过滤剔除明显噪声而有监督模型只需要处理小规模候选集既减少了样本不平衡的干扰也提升了推理效率。4. 阶段一无监督线索挖掘4.1 文本预处理中文政府采购文本通常包含大量空格、特殊字符、英文缩写、数字单位。预处理阶段需要做去除多余空白字符保留中文、英文、数字和基础标点将文本转成干净字符串方便后续分词和关键词匹配。这里使用 jieba 做中文分词。需要注意 jieba 对领域词汇的分词效果依赖于词典建议额外准备采购领域的自定义词典例如“围标串标”“倾向性”“量身定制”等词条。4.2 关键词规则召回无监督阶段最直观的方法是构建指控性语言关键词表。示例词表如下围标 串标 倾向性 量身定制 指定品牌 排斥潜在供应商 利益冲突 关联关系 不公正 歧视性 围标串标 量身定做 疑似串通 操纵评标对每段文本统计命中关键词的数量命中数量达到阈值则进入候选集。阈值越低召回越高但也会引入更多噪声阈值越高过滤越强但可能漏掉部分委婉表达。这一步的代码逻辑是对每段文本做子串匹配计算命中次数。4.3 聚类过滤噪声关键词召回会带来大量“伪指控”文本比如“本项目不存在倾向性条款”这句话命中了“倾向性”但实际表达的是否定含义。聚类过滤的思路是根据文本语义将候选集分成若干簇如果某个簇整体的关键词命中率和语义特征与典型指控文本差异很大可以视为噪声簇。具体实现是使用 TF-IDF 向量化后做 KMeans 聚类然后计算每个簇的平均关键词命中次数过滤掉明显低于整体均值的簇。这个方法虽然简单但在实际项目中能明显减少有监督阶段的无效样本量。5. 阶段二有监督分类模型5.1 特征工程有监督阶段输入的是候选文本特征可以分成两类。第一类是基础文本特征最常用是 TF-IDF。为了保留短语信息可以加入ngram_range(1, 2)让“倾向性”“围标串标”这类双词组合成为独立特征。第二类是规则辅助特征比如关键词命中次数是否包含“疑似”“涉嫌”“不排除”“或许”等不确定性表达是否包含“招标文件”“评标委员会”“投标人”“中标”等主体名词文本长度。规则辅助特征可以帮助模型学习“委婉指控”模式而不仅仅依赖情感词或极端词。5.2 模型选型与训练有监督阶段不需要刻意追求复杂模型。政府采购文本分类通常是长文本、稀疏特征场景线性模型表现稳定且可解释性更强。这里以逻辑回归Logistic Regression为例实际项目中也可以对比 LinearSVC、朴素贝叶斯或随机森林。训练时需要注意两个问题类别不平衡设置class_weightbalanced让模型更关注少数类固定随机种子保证实验结果可复现也方便后续比较不同特征方案。5.3 评估指标对于这种任务不能只看准确率因为负样本占多数模型即使全部预测为 0 也能获得很高准确率。应重点关注精确率Precision模型判定为指控性语言的文本中确实是指控性语言的比例召回率Recall所有真正的指控性语言文本中模型正确找出的比例F1 值精确率和召回率的调和平均混淆矩阵观察误报和漏报的具体分布。在审核场景中通常倾向于优先保证召回率宁可多召回一些候选给人工复核也不要漏掉真正的风险文本。6. 完整代码实战6.1 项目结构为了便于演示本文使用以下项目结构。实际项目中可以按团队规范调整目录组织方式。procurement_nlp/ ├── config.py ├── preprocessing.py ├── stage1_unsupervised.py ├── stage2_supervised.py ├── pipeline.py ├── data/ │ ├── raw_docs.csv │ ├── labeled_samples.csv │ └── accusation_keywords.txt └── output/其中raw_docs.csv是待处理的原始文本labeled_samples.csv是人工标注的训练数据accusation_keywords.txt是指控性关键词表。下面逐一给出各文件的核心代码。6.2 配置模块 config.py# 文件路径procurement_nlp/config.py import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) # 数据路径 RAW_DATA_PATH os.path.join(BASE_DIR, data, raw_docs.csv) LABELED_DATA_PATH os.path.join(BASE_DIR, data, labeled_samples.csv) KEYWORD_FILE os.path.join(BASE_DIR, data, accusation_keywords.txt) # 输出目录 OUTPUT_DIR os.path.join(BASE_DIR, output) os.makedirs(OUTPUT_DIR, exist_okTrue) # 无监督召回参数 RECALL_THRESHOLD 1 # 关键词命中数量阈值 CLUSTER_NUM 8 # KMeans 聚类数量 CLUSTER_SAMPLE_RATIO 0.6 # 簇过滤比例 # 有监督模型参数 TEST_SIZE 0.2 RANDOM_STATE 42 CLASS_WEIGHT balanced TFIDF_MAX_FEATURES 10000把超参数集中放在config.py里后续调整阈值和模型参数时不需要修改主流程代码这是工程化的基本习惯。6.3 预处理模块 preprocessing.py# 文件路径procurement_nlp/preprocessing.py import re import pandas as pd import jieba STOP_WORDS set() # 建议加载停用词表示例中先保留空集合 # with open(data/stopwords.txt, r, encodingutf-8) as f: # STOP_WORDS set(line.strip() for line in f if line.strip()) def clean_text(text: str) - str: 清洗文本去除多余空白和特殊符号保留中英文、数字和基础标点。 if not isinstance(text, str): return text re.sub(r\s, , text) text re.sub(r[^\u4e00-\u9fa5A-Za-z0-9。、()%], , text) return text.strip() def tokenize(text: str) - list: 中文分词过滤停用词和单字。 words jieba.lcut(clean_text(text)) return [w for w in words if w.strip() and len(w) 1 and w not in STOP_WORDS] def load_raw_data(path: str) - pd.DataFrame: 加载原始文本并生成清洗列。 df pd.read_csv(path) df[clean_text] df[text].apply(clean_text) df[tokens] df[text].apply(tokenize) return df预处理阶段需要注意一个细节不要把“不排除”“不明显”中的“不”直接过滤掉因为这类否定词对判断指控性语言的置信度有重要作用。停用词表应该谨慎设计而不是直接套用通用中文停用词表。6.4 无监督召回 stage1_unsupervised.py# 文件路径procurement_nlp/stage1_unsupervised.py from typing import List import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.cluster import KMeans def load_keywords(path: str) - List[str]: 加载指控性语言关键词表。 with open(path, r, encodingutf-8) as f: return [line.strip() for line in f if line.strip()] def rule_recall(df: pd.DataFrame, keywords: List[str], threshold: int 1) - pd.DataFrame: 基于关键词命中次数做规则召回。 def hit_count(text: str) - int: return sum(1 for kw in keywords if kw in text) df[keyword_hits] df[clean_text].apply(hit_count) return df[df[keyword_hits] threshold].copy() def cluster_filter(df: pd.DataFrame, n_clusters: int 8, sample_ratio: float 0.6) - pd.DataFrame: 通过 TF-IDF KMeans 聚类过滤噪声文本。 vectorizer TfidfVectorizer(max_features5000, ngram_range(1, 2)) X vectorizer.fit_transform(df[clean_text]) km KMeans(n_clustersn_clusters, random_state42, n_init10) df[cluster] km.fit_predict(X) # 计算每个簇的平均关键词命中次数过滤低于阈值的簇 cluster_stats df.groupby(cluster)[keyword_hits].mean() global_mean df[keyword_hits].mean() keep_clusters cluster_stats[ cluster_stats global_mean * sample_ratio ].index.tolist() return df[df[cluster].isin(keep_clusters)].copy()cluster_filter是一个启发式过滤方法思路是如果某个簇整体关键词命中次数远低于全局平均水平说明该簇内文本很可能只是偶然命中关键词并不是真正的指控性语言。这个方法的核心价值在于过滤“伪命中”。6.5 有监督模型 stage2_supervised.py# 文件路径procurement_nlp/stage2_supervised.py import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, confusion_matrix def build_model(df: pd.DataFrame, random_state: int 42): 训练有监督分类模型。 输入 DataFrame 至少包含 clean_text 和 label 两列。 X_train, X_test, y_train, y_test train_test_split( df[clean_text], df[label], test_size0.2, random_staterandom_state, stratifydf[label], ) vectorizer TfidfVectorizer( max_features10000, ngram_range(1, 2), ) X_train_vec vectorizer.fit_transform(X_train) X_test_vec vectorizer.transform(X_test) model LogisticRegression( class_weightbalanced, max_iter1000, random_staterandom_state, ) model.fit(X_train_vec, y_train) y_pred model.predict(X_test_vec) print(classification_report(y_test, y_pred)) print(混淆矩阵) print(confusion_matrix(y_test, y_pred)) return vectorizer, model有监督部分采用了逻辑回归。如果标注数据规模较大可以尝试LinearSVC或RandomForestClassifier做对比。需要注意train_test_split中使用了stratifydf[label]保证训练集和测试集中正负样本比例一致避免因随机划分导致测试集没有正样本。6.6 串联运行 pipeline.py# 文件路径procurement_nlp/pipeline.py import pandas as pd from preprocessing import clean_text, load_raw_data from stage1_unsupervised import load_keywords, rule_recall, cluster_filter from stage2_supervised import build_model import config def run_inference(text: str, vectorizer, model, keywords, threshold1): 对单条文本执行完整推理流程。 clean clean_text(text) hits sum(1 for kw in keywords if kw in clean) if hits threshold: return {label: 0, stage: unsupervised_reject} vec vectorizer.transform([clean]) pred model.predict(vec)[0] return {label: int(pred), stage: supervised_predict} if __name__ __main__: # 1. 无监督召回 raw_df load_raw_data(config.RAW_DATA_PATH) keywords load_keywords(config.KEYWORD_FILE) candidates rule_recall(raw_df, keywords, thresholdconfig.RECALL_THRESHOLD) candidates cluster_filter( candidates, n_clustersconfig.CLUSTER_NUM, sample_ratioconfig.CLUSTER_SAMPLE_RATIO, ) # 2. 有监督模型训练 labeled_df pd.read_csv(config.LABELED_DATA_PATH) labeled_df[clean_text] labeled_df[text].apply(clean_text) vectorizer, model build_model(labeled_df, random_stateconfig.RANDOM_STATE) # 3. 对候选集预测 candidates[pred_label] model.predict( vectorizer.transform(candidates[clean_text]) ) result candidates[candidates[pred_label] 1] print(f候选数量{len(candidates)}指控性语言数量{len(result)}) output_path config.OUTPUT_DIR /predictions.csv result[[doc_id, source_type, text, pred_label]].to_csv( output_path, indexFalse, encodingutf-8-sig ) print(f结果已保存至{output_path})这里的主流程严格遵循先无监督、后有监督的顺序。无监督阶段先排除大量中性文本有监督阶段在候选集上做精细分类。单条推理函数run_inference展示了模型部署后如何对新文本进行预测。6.7 运行与预期输出安装依赖后在项目根目录执行pip install pandas scikit-learn jieba python pipeline.py如果标注样本充足终端会输出类似下面的分类报告precision recall f1-score support 0 0.88 0.84 0.86 50 1 0.72 0.78 0.75 30 accuracy 0.82 80 macro avg 0.80 0.81 0.80 80 weighted avg 0.82 0.82 0.82 80 混淆矩阵 [[42 8] [ 6 24]]需要强调的是上面只是演示预期效果实际指标完全取决于标注数据质量和语料分布。如果数据集中只有几十条正样本任何模型都很难达到理想效果此时应该优先扩充标注数据而不是更换模型。7. 常见问题与排查思路7.1 问题排查表问题现象常见原因解决思路jieba 分词结果把领域词切碎缺少采购领域自定义词典添加自定义词典例如“围标串标”“倾向性”关键词召回过量候选集太大关键词太宽泛阈值太低提高RECALL_THRESHOLD或增加聚类过滤强度有监督模型误报多正负样本不平衡或标注标准不一致检查标注规范使用class_weightbalanced明明有关键词但被判为负样本否定表达干扰在特征中加入否定词特征或用规则覆盖“不存在……”“未发现……”多次运行结果不一致未固定随机种子设置random_state并固定 KMeans 的n_init使用预训练模型后效果反而变差领域语料与预训练语料分布差异大先在采购文本上做领域自适应预训练或微调7.2 高发问题详细复盘在实践中“关键词命中但语义否定”是最容易踩坑的一类问题。比如下面这句话本项目招标文件不存在倾向性条款评标过程公平公正。“倾向性”命中关键词但语义是否定。规则召回阶段无法处理这种问题聚类过滤也只能从统计层面淡化最终还是要靠有监督模型学习“不存在”“未发现”“未体现”等否定表达与“倾向性”的共现模式。另一个常见问题是中文分词引起的召回偏差。默认 jieba 可能把一个完整的领域词切开导致关键词无法匹配。解决方案是在项目初始化时加载自定义词典import jieba jieba.load_userdict(data/procurement_dict.txt)自定义词典中每行一个词可以配置词频和词性例如围标串标 10 n 倾向性条款 10 n 排斥潜在供应商 5 n8. 最佳实践与工程建议8.1 数据与标注管理指控性语言识别本质上是文本分类任务数据质量决定了效果上限。建议项目启动时先制定标注规范明确以下几点什么是“指控性语言”什么不算委婉表达是否计入否定表达如何处理一段文本同时包含多个指控子类型时如何标记。标注阶段建议至少两人独立标注并计算 Cohen‘s Kappa 一致性指标。如果一致性低于 0.6需要重新讨论标注规范而不是急着训练模型。8.2 模型可解释性与审计政府采购文本审核涉及合规场景模型输出需要能被复核人员理解和信任。建议使用逻辑回归等线性模型时保存特征词权重便于解释预测依据对每条高风险预测输出命中关键词、Top 特征词以及置信度建立人工复核闭环把复核结果回填到训练集中持续迭代。如果后续替换为 BERT 等深层模型建议引入 SHAP 或 LIME 做局部解释。不要只输出一个 0/1 标签审核场景中“为什么判定为指控”比“判定结果”更重要。8.3 性能与生产部署级联 Pipeline 在生产环境中的性能可以从两个角度优化。第一无监督阶段的 TF-IDF 向量化和 KMeans 模型需要保存到本地建议使用joblib或pickle序列化避免每次推理重新训练import joblib joblib.dump(vectorizer, output/tfidf_vectorizer.pkl) joblib.dump(model, output/lr_model.pkl) joblib.dump(km, output/kmeans_model.pkl)第二大批量处理时可以采用批处理方式一次性对多篇文档做无监督召回再对有监督分类器做批量预测。单条循环调用会浪费大量时间在重复的数据加载与模型调度上。8.4 合规与安全边界政府采购文本通常涉及公开信息和敏感商业信息。处理时需要遵守法律规定和单位内部安全要求仅处理已授权的公开采购文本或本单位内部合规数据对数据做好脱敏处理去除个人身份信息模型训练和推理环境应遵循最小权限原则禁止将内部采购数据用于未经授权的场景或上传到不受管控的外部平台。涉及模型上线时建议先在测试环境验证并保留旧模型回滚能力避免因新模型误报率波动影响业务。9. 总结与学习路线本文围绕政府采购文本中的指控性语言识别拆解了一个级联式无监督-有监督 NLP Pipeline 的完整实现。核心思路是先通过关键词规则召回候选文本再用 TF-IDF 加聚类过滤噪声最后用逻辑回归模型在候选集上做精细分类。这个方案在工作量、标注成本和模型效果之间取得了一个相对平衡的点。接下来可以继续深入的方向包括引入预训练语言模型例如将第二阶段的逻辑回归替换为 BERT 分类模型对比效果差异将二分类扩展为多分类区分“参数倾向性”“围标串标”“程序违规”等指控子类型加入时间维度对同一采购项目的历史文本做交叉分析发现跨文档关联线索在人工审核闭环中引入主动学习让模型优先选择最值得标注的样本。实际项目中优先关注三个方面标注数据质量、类别不平衡处理、模型可解释性。先把这三件事做好再考虑更换复杂模型或增加更多特征否则容易陷入“调参改善零点几个点”的循环。本文的方案可以直接作为基础框架使用代码结构上把无监督和有监督阶段做了模块化拆分替换或扩展都相对容易。
返回列表