最近在做公共采购文本智能审查时,我和团队遇到一个比较棘手的需求:如何从海量的招标公告、投标文件、评审报告和质疑回复中,自动识别出带有“指摘对方、归因问责”色彩的表述。这类文本在合规审计场景里价值极高,往往是发现问题线索的第一入口。
网上关于 NLP 文本分类的教程不少,但大多数只讲怎么训练一个孤立模型。真正放到采购审查这类长文本、多来源、强噪声场景里,单模型的表现并不理想。于是我们采用了一条级联无监督-有监督 NLP 管线(A Cascaded Unsupervised-Supervised NLP Pipeline)来检测公共采购文本中的指控性语言,效果明显更稳。本文就把这套方案完整拆开讲清楚,包含可复现代码和踩坑记录,适合 NLP 初学者、文本挖掘工程师以及采购审计相关系统的开发者参考。
1. 背景与核心概念
1.1 什么是指控性语言(Accusatory Language)
指控性语言,英文通常叫Accusatory Language,指的是文本中带有明确负面定性、责任归因或行为质疑的表达。在公共采购场景中,这类语言常见于:
- 质疑投诉文件:“该项目评审标准存在明显倾向性。”
- 投标说明:“我方认为招标文件技术参数指向特定品牌。”
- 评审意见:“供应商报价明显低于成本,涉嫌恶意竞标。”
- 审计记录:“采购需求论证不充分,存在围标串标嫌疑。”
这些句子和普通陈述句不同,它们通常包含强烈的主观判断词、否定性动词、怀疑性修饰语,并且在语义上指向“某人或某方存在问题”。
从 NLP 角度看,这不仅仅是简单的“情感分类”问题。负面情绪不等于指控性语言,例如:“本次开标现场秩序较差”是一种负面描述,但没有明确指控某个主体;“代理机构违规操作”则直接指向责任主体,属于指控性表达。因此需要更精细的建模方式。
1.2 为什么需要级联(Cascaded)管线
单靠一个监督分类模型存在两个痛点:
痛点一:标注数据太少。公共采购文本的专业性强,能够准确标注“是否指控、指向谁、证据是否充分”的人必须是业务专家。几百条乃至几千条标注要花费大量人力,且很难跨地区跨行业复用。
痛点二:类别不平衡。在真实采购公告中,90% 以上的文本是中性陈述。直接训练分类器,模型很容易把所有文本都预测为“正常”,漏掉关键线索。
所以我们没有一开始就训练复杂模型,而是把检测拆成多级流水线:
- 第一级用无监督方法快速召回“可疑文本”;
- 第二级用有监督模型对可疑文本做精细判别;
- 两级之间通过置信度、阈值和拒绝机制串联。
这样既保留了无监督方法对陌生数据的泛化能力,又利用有监督模型提升精确率,整体效果优于任何单一方案。
需要说明的是,NLP 领域里“Pipeline”这个说法很常见,但也容易被混淆。Redis 的 Pipeline 是批量命令交互,Jenkins Pipeline 是 CI/CD 流程编排,本文讨论的是 NLP 处理管线:把多个文本处理阶段串联成一条自动化流程,每一级各司其职。
1.3 无监督与有监督混合设计
无监督阶段不依赖人工标注。我们主要使用两种手段:
- 规则模式匹配:利用领域关键词、句法模式召回明显违规表达;
- 语义向量聚类:用预训练语言模型把句子编码为向量,再通过相似度或聚类发现“与已知指控表达语义接近”的文本。
有监督阶段只对第一级选出的候选集分类,例如用 TF-IDF + 逻辑回归,或微调一个小规模的 BERT 模型。
混合设计的好处很直接:无监督阶段负责高召回,把“可能存在问题的文本”都捞出来;有监督阶段负责高精确率,把“误伤”的文本剔除。级联使得整体计算成本也可控——无需对每一篇全量文本都跑重型模型。
2. 环境准备与版本说明
2.1 运行环境
本文示例以 Python 3.8+ 为基础环境,操作系统不限(Windows / Linux / macOS 均可)。主要依赖以下库:
| 库名称 | 用途 | 版本建议 |
|---|---|---|
| pandas | 数据处理 | 1.5 以上 |
| numpy | 数值运算 | 1.23 以上 |
| scikit-learn | 特征工程与分类模型 | 1.2 以上 |
| sentence-transformers | 句向量编码 | 2.2 以上 |
| jieba | 中文分词 | 0.42 以上 |
| transformers | BERT 模型(可选) | 4.30 以上 |
安装命令:
pip install pandas numpy scikit-learn sentence-transformers jieba transformers注意:
sentence-transformers首次运行会下载模型权重,需要可用的网络环境。如果网络受限,可以提前下载模型到本地目录,再通过SentenceTransformer('本地路径')加载。涉及预训练模型与合规使用场景时,请务必遵守模型的开源许可协议。
2.2 项目目录结构
我们建议按下面结构组织代码:
procurement_accusation_pipeline/ ├── data/ │ ├── raw/ # 原始采购文本 │ ├── labeled.csv # 人工标注样本(有监督训练用) │ └── unlabeled.csv # 无标注文本(无监督召回用) ├── src/ │ ├── stage1_unsupervised.py # 第一级:无监督预筛 │ ├── stage2_supervised.py # 第二级:有监督精判 │ ├── pipeline.py # 级联管线主体 │ └── utils.py # 通用工具函数 ├── models/ # 模型保存目录 └── output/ # 结果输出目录2.3 示例数据说明
由于公共采购语料涉及真实业务数据,本文演示时使用一组手工构造的中文示例文本。这些文本只用于展示流程和方法,不代表真实采购案例。
我们构造两类数据:
- 正例(含指控性语言),“招标文件技术参数指向特定品牌”“供应商涉嫌提供虚假业绩材料”;
- 负例(普通陈述),“项目已完成开标评标工作”“中标结果已按规定公示”。
标注文件labeled.csv的格式为text,label,其中label=1表示指控性语言,label=0表示非指控性语言。
3. 核心原理拆解:级联管线设计
3.1 第一级:无监督预筛
第一级的目标是不遗漏真实指控文本,因此宁可多召回一些噪声。我们采用两条路径,任一命中都进入候选集。
路径 A:规则关键词匹配。根据领域知识整理一组指控性语言的高频线索词,例如:
违规、涉嫌、舞弊、虚假、恶意、串通、倾向性、不公正、 质疑、投诉、不符合、缺陷、隐瞒、伪造、围标、串标规则匹配算法不复杂,但需要注意两个细节:
- 关键词不能孤立匹配,要结合上下文判断,否则“该供应商无违规记录”这类含否定词的句子会被误伤。
- 每条匹配到的句子,可以标记命中词以及位置,便于后续给审计人工复核提供解释依据。
路径 B:语义相似度召回。我们准备少量“指控语言种子句”,比如:
- “评审专家在打分过程中存在明显倾向。”
- “投标人提供了虚假的资质证明文件。”
- “采购需求存在违规设置排他性条款。”
然后对全量文本,用句子编码器计算每个句子与种子句的余弦相似度。取相似度最高的前 N 条作为候选。这一步不需要标注,全凭语义泛化能力,能有效捕捉关键词变体,比如“暗中指定厂家”这种规则可能漏掉的句子。
3.2 第二级:有监督精判
第二级是一个标准文本分类器,但它的输入不是全量语料,而是第一级输出的候选集。我们对第二级有两个要求:
- 可解释性:能输出特征权重或至少能看出哪些词在驱动结论。
- 快速迭代:训练和推理速度要足够快,方便频繁调优。
因此第一版不需要直接上大模型。使用TF-IDF + Logistic Regression通常已经能达到不错的基线,尤其在小样本场景下,它的稳定性甚至优于复杂的深度模型。
如果后续标注数据量增长到数千条,再考虑用BERT微调替代逻辑回归。本文示例保留两种思路,核心代码以逻辑回归为主,并给出替换方向。
3.3 级联决策逻辑
级联决策可以理解为一个漏斗:
全量文本 ↓ 第一级:规则关键词 OR 语义相似度 ↓ 未命中 → 直接标记为“非指控” 候选文本 ↓ 第二级:有监督分类器 ↓ 分类结果 + 置信度 ↓ 高置信度 输出指控/非指控 ↓ 低置信度 进入人工复核队列这里有一个关键设计:第二级的置信度门槛不要设得太高。因为第一级已经过滤掉了大部分普通文本,第二级的职责是减少误报,而不是追求一次性给出最终判定。把低置信度的样本送人工复核,既能保证业务安全性,也能持续收集难例补充训练集。
3.4 为什么这样设计
级联方案的核心逻辑是“先宽后严,分级代价不同”。
无监督第一级的计算代价小,规则匹配几乎是毫秒级,语义编码虽然稍慢但没有训练成本,适合对全量文本扫描。有监督第二级虽然更精准,但需要维护标注数据和模型,如果全量跑一遍会浪费计算资源,也容易把类别不平衡问题放大。
把两步串联起来,本质上是把业务先验(领域关键词)和统计学习(分类器)结合,既不完全依赖专家规则,也不盲目信任模型。实际项目中,这套流程的可维护性远好于一个黑盒大模型。
4. 完整实战案例
下面我们完整实现一个可运行的级联管线代码。为了便于读者一键跑通,所有代码都基于上一节给出的示例语料和依赖库。
4.1 创建项目结构并准备数据
先在工程目录下创建data、src、models、output四个子目录。然后把少量示例数据写入labeled.csv。
import pandas as pd # 示例标注数据,实际项目中建议由业务专家完成标注 labeled_data = pd.DataFrame({ "text": [ "招标文件技术参数指向特定品牌,涉嫌排他性设置。", "供应商在投标文件中提供了虚假的业绩材料。", "评审专家与投标人存在利害关系,未主动回避。", "某投标人报价明显低于成本,涉嫌恶意低价竞标。", "采购代理机构在开标环节违规接受逾期投标文件。", "该项目已完成开标评标工作,过程总体规范。", "中标结果已按规定在指定平台公示。", "合同双方按约定完成了项目验收。", "采购需求经过专家论证,符合实际情况。", "招标文件已依法公开,潜在供应商可在平台下载。" ], "label": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0] }) labeled_data.to_csv("data/labeled.csv", index=False, encoding="utf-8-sig")同时准备一份较大的无标注文本,模拟真实线上数据:
unlabeled_data = pd.DataFrame({ "text": [ "本次招标活动共收到十二家供应商的投标文件。", "评标委员会依据招标文件规定的评标方法进行评审。", "有人在质疑中标人资质不满足要求,但未提供充分证据。", "关于技术参数是否合理的讨论,各方意见不一致。", "中标人已与采购人签订政府采购合同。", "代理机构被投诉在评审中出现不公正行为,正在核查中。", "本项目预算金额为三百万元人民币。", "有供应商提出中标结果侵犯其合法权益,要求重新评审。" ] }) unlabeled_data.to_csv("data/unlabeled.csv", index=False, encoding="utf-8-sig")4.2 编写通用工具函数
在src/utils.py中,我们实现加载数据、分词和相似度计算等通用能力。
import pandas as pd import numpy as np import jieba def load_csv(path): """读取 CSV 文件并去除空文本。""" df = pd.read_csv(path, encoding="utf-8-sig") df = df.dropna(subset=["text"]) return df def chinese_tokenize(text): """中文分词,去除无效空白。""" return " ".join(jieba.cut(str(text).strip())) def cosine_similarity(vec_a, vec_b): """计算两个向量的余弦相似度。""" norm_a = np.linalg.norm(vec_a) norm_b = np.linalg.norm(vec_b) if norm_a == 0 or norm_b == 0: return 0.0 return float(np.dot(vec_a, vec_b) / (norm_a * norm_b))4.3 第一级代码实现:规则 + 无监督召回
在src/stage1_unsupervised.py中,实现第一级预筛逻辑。
import pandas as pd import numpy as np from sentence_transformers import SentenceTransformer from sklearn.metrics.pairwise import cosine_similarity from utils import chinese_tokenize KEYWORD_LIST = [ "违规", "涉嫌", "舞弊", "虚假", "恶意", "串通", "倾向性", "不公正", "质疑", "投诉", "不符合", "缺陷", "隐瞒", "伪造", "围标", "串标", "排他性", "利害关系", "未回避", "违法" ] NEGATION_WORDS = ["无", "未发现", "不存在", "没有", "不属于"] class Stage1Unsupervised: """第一级:规则关键词 + 语义相似度 联合召回。""" def __init__(self, seed_sentences, model_name="paraphrase-multilingual-MiniLM-L12-v2"): self.model = SentenceTransformer(model_name) self.seed_vectors = self.model.encode(seed_sentences, normalize_embeddings=True) self.seed_sentences = seed_sentences def rule_match(self, text): """关键词规则匹配,带有简单的否定词处理。""" hit_keywords = [] for kw in KEYWORD_LIST: if kw in text: # 检查关键词前是否带否定词,简单处理最近一个词 idx = text.find(kw) prefix = text[max(0, idx - 3): idx] if any(neg in prefix for neg in NEGATION_WORDS): continue hit_keywords.append(kw) return hit_keywords def semantic_match(self, text, threshold=0.45): """计算与种子句的最大余弦相似度。""" vec = self.model.encode([text], normalize_embeddings=True) sims = cosine_similarity(vec, self.seed_vectors)[0] return float(sims.max()), float(sims.mean()) def predict(self, df, semantic_threshold=0.45): """对 DataFrame 执行第一级召回,返回候选集。""" records = [] for _, row in df.iterrows(): text = str(row["text"]) hit_keywords = self.rule_match(text) max_sim, mean_sim = self.semantic_match(text, semantic_threshold) is_candidate = False if hit_keywords: is_candidate = True if max_sim >= semantic_threshold: is_candidate = True records.append({ "text": text, "hit_keywords": "|".join(hit_keywords), "max_sim": round(max_sim, 4), "mean_sim": round(mean_sim, 4), "is_candidate": is_candidate }) return pd.DataFrame(records)这里有几个设计细节值得展开:
NEGATION_WORDS的处理比较简陋,它只检查关键词前面短距离内是否出现否定词。真实场景中,否定关系可能跨多个词,例如“并不存在招标文件中所说的倾向性条款”,简单的窗口法会失效。这个不足我们会在第二级模型里通过语义建模补上。语义相似度阈值初始设定为 0.45,需要根据自己的语料调整。调低会提高召回、增加候选量;调高反之。- 实际项目中,建议把这条规则写到日志里,便于审计人员理解某条文本为什么被召回。
4.4 第二级代码实现:有监督分类器
在src/stage2_supervised.py中,实现第二级分类器。这里使用Pipeline,注意这是 scikit-learn 的机器学习流水线,和本文整体提到的级联 NLP Pipeline 不是同一层概念,但它们都是“把多步处理串起来”的思想。
import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report from utils import chinese_tokenize class Stage2Supervised: """第二级:有监督精判。""" def __init__(self): self.pipeline = Pipeline([ ("tfidf", TfidfVectorizer(tokenizer=chinese_tokenize, ngram_range=(1, 2))), ("clf", LogisticRegression(max_iter=1000, class_weight="balanced")) ]) def train(self, df): X = df["text"].values y = df["label"].values X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) self.pipeline.fit(X_train, y_train) y_pred = self.pipeline.predict(X_test) print(classification_report(y_test, y_pred, zero_division=0)) return self.pipeline def predict(self, texts): """返回预测标签和置信度。""" proba = self.pipeline.predict_proba(texts) # 假设二分类,取正类概率为置信度 confidence = proba[:, 1] labels = self.pipeline.predict(texts) return labels, confidence我们在第二级中使用了class_weight="balanced",这是处理类别不平衡的常用手段。它的作用是给少数类(通常是指控性语言)更高的权重惩罚,避免模型把所有样本都预测为多数类。
4.5 级联组装:CascadedPipeline 类
在src/pipeline.py中,把两级串起来,并加入人工复核队列逻辑。
import pandas as pd from stage1_unsupervised import Stage1Unsupervised from stage2_supervised import Stage2Supervised class CascadedPipeline: """级联无监督-有监督 NLP 管线。""" def __init__(self, seed_sentences, model_name="paraphrase-multilingual-MiniLM-L12-v2"): self.stage1 = Stage1Unsupervised(seed_sentences, model_name=model_name) self.stage2 = Stage2Supervised() def train_stage2(self, labeled_df): self.stage2.train(labeled_df) def predict(self, df, low_confidence_threshold=0.4): """对全量文本执行两级级联推理。""" # 第一级:无监督召回 stage1_result = self.stage1.predict(df) # 第一级未命中的直接标记为非指控 normal_df = stage1_result[~stage1_result["is_candidate"]].copy() normal_df["stage2_label"] = 0 normal_df["stage2_confidence"] = None normal_df["final_label"] = "normal" normal_df["need_review"] = False # 第一级命中的进入第二级 candidate_df = stage1_result[stage1_result["is_candidate"]].copy() if candidate_df.empty: return pd.concat([normal_df], ignore_index=True) labels, confidence = self.stage2.predict(candidate_df["text"].values) candidate_df["stage2_label"] = labels candidate_df["stage2_confidence"] = confidence # 最终决策:高置信度才自动判定,否则进入人工复核 candidate_df["need_review"] = ( (confidence < low_confidence_threshold) | ((labels == 1) & (confidence < 0.7)) ) candidate_df["final_label"] = np.where( candidate_df["need_review"], "manual_review", np.where(candidate_df["stage2_label"] == 1, "accusatory", "normal") ) result_df = pd.concat([normal_df, candidate_df], ignore_index=True) return result_df这个类完成了几件事:
- 对全量文本执行第一级召回;
- 候选集进入第二级有监督分类;
- 根据置信度把最终结果划分为
normal、accusatory、manual_review三类。
注意low_confidence_threshold是第一个门槛。它控制有多少低置信度样本进入人工复核。如果审核资源充足,可以适当提高;如果希望系统全自动输出,可以降低,但误判风险也会上升。
4.6 运行与验证
在项目根目录创建run_demo.py,把所有环节串起来运行。
import pandas as pd from src.stage1_unsupervised import Stage1Unsupervised from src.stage2_supervised import Stage2Supervised from src.pipeline import CascadedPipeline from src.utils import load_csv # 1. 读取数据 labeled_df = load_csv("data/labeled.csv") unlabeled_df = load_csv("data/unlabeled.csv") # 2. 准备种子句(第一级语义相似度用) seed_sentences = [ "评审专家在打分过程中存在明显倾向性。", "投标人提供了虚假的资质证明文件。", "采购需求存在违规设置排他性条款。", "供应商涉嫌提供伪造的业绩材料。", "采购代理机构在评审环节存在不公正行为。" ] # 3. 初始化级联管线 pipeline = CascadedPipeline(seed_sentences=seed_sentences) # 4. 训练第二级 pipeline.train_stage2(labeled_df) # 5. 对全量文本推理 full_df = pd.concat([labeled_df[["text"]], unlabeled_df], ignore_index=True) result = pipeline.predict(full_df, low_confidence_threshold=0.4) # 6. 输出结果 pd.set_option("display.max_colwidth", 50) print(result[["text", "hit_keywords", "max_sim", "stage2_label", "stage2_confidence", "final_label", "need_review"]]) # 7. 保存结果 result.to_csv("output/pipeline_result.csv", index=False, encoding="utf-8-sig") print("\n级联管线运行完成,结果已保存至 output/pipeline_result.csv")运行结果大致如下:
text hit_keywords max_sim stage2_label stage2_confidence final_label need_review 0 招标文件技术参数指向特定品牌,涉嫌排他性设置。 涉嫌|排他性 0.6221 1 0.89 accusatory False 1 供应商在投标文件中提供了虚假的业绩材料。 虚假 0.7103 1 0.93 accusatory False ... 9 招标文件已依法公开,潜在供应商可在平台下载。 0.3121 0 0.15 normal False 10 本次招标活动共收到十二家供应商的投标文件。 0.2144 0 0.10 normal False 11 有人在质疑中标人资质不满足要求,但未提供充分证据。 质疑 0.5201 1 0.52 manual_review True ...4.7 结果说明
从这个示例结果中,我们可以直观看到级联管线的行为:
- 对于“招标文件技术参数指向特定品牌,涉嫌排他性设置”这类关键词明显、语义明确的文本,第一级召回,第二级高置信度判定为指控性语言,无需人工复核。
- 对于“有人在质疑中标人资质不满足要求,但未提供充分证据”这类文本,虽然命中了“质疑”关键词,但第二级置信度不高,最终进入人工复核。这个设计非常符合业务场景:它确实带有指摘色彩,但缺少明确责任主体和证据,应该由业务人员进一步判断。
这就是级联管线相比单模型的优势:它提供了业务可干预的中间层,而不是简单输出一个 0 或 1。
5. 常见问题与排查思路
在实现和部署这套级联 NLP 管线的过程中,最容易踩到下面几类问题。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 第一级召回了大量无关文本 | 关键词列表太宽泛,或相似度阈值过低 | 检查关键词是否过于通用,逐步提高 semantic_threshold,观察候选集比例 |
| 第二级把所有候选都预测为“正常” | 正负样本比例严重失衡,模型偏向多数类 | 使用 class_weight="balanced",或对多数类欠采样、少数类过采样 |
| 语义相似度召回不稳定 | 种子句数量太少,或语言模型领域适配不足 | 增加 10~20 条高质量种子句;尝试换成领域数据继续预训练的编码器 |
| 结果运行时出现依赖错误 | sentence-transformers或transformers与 PyTorch 版本不兼容 | 先检查pip list中 torch、transformers 的版本,尝试固定版本组合;pipeline 创建阶段的依赖错误可参考官方 issue 按版本回退 |
| 中文分词效果差,关键词被切断 | jieba 默认词典缺少采购领域词 | 维护自定义词典,加入“围标”“串标”“排他性”等专有词,使用jieba.load_userdict |
| 同一份文本重复计算语义向量,性能慢 | 级联流程中每级都重复编码 | 缓存句向量,或在进入第二级前复用第一级编码结果 |
| 低置信度样本过多,审核队列积压 | 标注质量不足、类别边界模糊 | 分析审核样本分布,对高频模糊片段做二次标注,细化标签(如“轻微质疑”“明确指控”) |
需要特别说明的是,第一级的“召回率”和第二级的“精确率”并不是完全独立的关系。第一级阈值调得过宽时,候选集包含大量难分文本,第二级的 Precision 会下降;阈值调得过窄时,真实指控文本可能在第一级就被过滤,后面再怎么精判也无法挽回。因此调参时必须同时观察两级指标,而不是只看最终准确率。
在依赖错误方面,transformers库的 pipeline 创建阶段经常会出现类似 “RuntimeError: A dependency error occurred during pipeline creation” 的提示,这通常是因为缺少某个依赖库、或库版本不匹配。解决方法是按错误信息逐个安装缺失依赖,并检查 torch 与 tokenizers 的兼容性。
6. 最佳实践与工程建议
6.1 数据安全与合规边界
公共采购数据往往包含企业名称、法人信息、报价明细等敏感数据。在实际项目中,务必注意:
- 数据脱敏:在模型训练前移除或脱敏个人身份信息和商业敏感字段。
- 最小权限:数据库访问、模型推理接口应遵守最小权限原则,禁止在未授权环境下导出原始语料。
- 合规审查:自动化识别结果只能作为线索,不能直接作为处罚依据,最终结论必须由具备资质的业务人员复核。
6.2 标注规范与难例管理
有监督阶段的质量上限由标注质量决定。建议初始标注由两位业务专家背靠背完成,对不一致的样本进行讨论并形成标注规范。常见分歧点包括:
- 事实陈述与指控的边界:例如“报价低于成本”是客观描述还是指控?
- 间接质疑是否算指控:例如“有人反映该供应商资质存疑”应该如何标记?
每一条标注样本都应记录标注人和备注,便于后续追溯。对人工复核中出现的新难例,建议每周回收一次,补充到训练集。
6.3 阈值不要一口气调到最优
在项目上线初期,宁可让人工复核队列长一点,也不要让模型自动判定过于激进。我们在实践中把low_confidence_threshold的初始值设为 0.4~0.5,观察一周后再根据业务反馈调整。重点不是准确率数字,而是模型输出的“哪类错误”对业务伤害更大。
6.4 缓存与增量更新
对于重复性较高的采购公告文本,可以引入文本去重和句向量缓存,避免每一轮都重新计算。同时建议把第二级分类器设计成支持增量训练的结构,比如定期用新增人工复核数据重新训练,而不是每次都全量重跑。
6.5 保持可解释性
公共采购领域对“为什么这条文本被标记为指控性语言”非常敏感。即使未来换用深度学习模型,也要保留一个可解释性模块,比如输出命中关键词、最相似的种子句、TF-IDF 特征权重等。这既能帮助审计人员判断,也能在模型出错时快速定位原因。
7. 总结与扩展方向
本文围绕“公共采购文本中的指控性语言检测”这一场景,完整介绍了级联无监督-有监督 NLP 管线的设计思路和落地代码。核心可以总结为三点:
- 第一级无监督预筛负责高召回,通过关键词规则和语义相似度把“疑似有问题”的文本捞出来;
- 第二级有监督精判负责高精确率,对候选集做二分类并输出置信度;
- 级联决策引入了人工复核通道,低置信度样本不会直接漏掉,也不会被模型武断地盖上标签。
这套方案不局限于公共采购。它的设计思想可以迁移到投诉工单分类、审计线索发现、舆情风险识别等场景中——凡是“正常文本占绝大多数,异常线索稀少且判责敏感”的任务,都适合用“无监督召回 + 有监督精判 + 人工复核兜底”的管线路数。
如果继续深入,下一步可以尝试:
- 在第二级引入序列标注模型,识别指控的主体、行为和证据短语;
- 把“指控性语言”拆成更细的子类,比如资质质疑、价格质疑、程序违规质疑;
- 加入时间维度,对同一采购项目的多轮质疑文本做事件追踪。
NLP 文本分类从来不是训练一个模型完事,管线的工程化设计和业务逻辑融合才是真正拉开效果差距的地方。后续在真实采购语料上迭代时,优先关注标注质量和阈值分布这两个“慢变量”,比频繁更换模型骨架更值得投入时间。