简介:这是一项基于NLP的敏感文本识别与分类项目,提供Python源码与配套SQL数据库,主要面向信息安全、数据科学、人工智能等计算机相关专业的毕业设计、课程设计及入门实战。资源共6个文件,压缩包大小3.12MB,包含2个Python程序(非法信息收集、敏感不良文本分类)、2个文本说明文件(其中含停止词表)、1个SQL数据结构文件以及1个快捷方式,结构精简,便于快速定位代码与数据。目前已有133人学习/下载。项目代码可直接运行,通过自然语言处理实现对非法及不良文本的识别与分类,并配套数据库支持数据管理。既适合初学者理解分类流程与文本预处理,也适合在此基础上扩展功能,用于初期项目立项演示或进一步算法优化。
1. 敏感文本识别:为什么关键词过滤会被打穿,分类模型才是答案
社区评论区、客服对话、舆情监测面板,敏感内容总在以变体方式出现,谐音、拆字、拼音混排、英文缩写。关键词表加正则的经典方案,维护成本越滚越高,漏放和误杀两面挨骂。基于NLP的敏感文本识别分类项目,核心是把这件事从字符串匹配升级成文本分类:用预训练语言模型理解上下文语义,输出"正常 / 敏感"以及敏感所属类别,用数据库统一管理训练样本和预测结果。这个方向解决的是三个具体问题:误报率高、词表补丁打不完、样本散落各处难以迭代。适合内容安全方向的后端或算法工程师,也适合想把审核能力补进现有业务系统的团队。一句话:用 NLP 给审核流程加一道语义闸门,而不是继续给词表喂补丁。
2. 从词表到模型:敏感文本分类的原理、标签体系与模型选型
2.1 敏感词表的失效场景:变体、语境与维护成本
字符串匹配的实现并不复杂:维护一张敏感词表,加载后做子串包含判断,再用正则处理几种常见变体。我接过一个模拟项目X,初期词表只有两千多条,覆盖直白表达够用。但随着对抗手法升级,词表开始失控。变体常见形式有:词中间插空格或特殊符号、同音字替换、拆字加拼音混排、中英混合搭配。每发现一种新写法就补一条规则,规则之间还会互相冲突,例如某些词在正规医疗或教育场景出现时会被误杀。
比变体更麻烦的是语境歧义。同一个词,在朋友互怼和陌生人攻击之间,恶意程度完全不同;某类物品的正常价格讨论没问题,但出现在教学场景就要警惕。词表和正则匹配不到上下文,只能靠周围词做启发式判断,而启发式规则本身就是一叠补丁。最致命的是维护节奏永远落后:新变体出现到被发现、被补进词表,中间存在一个时间窗口,漏放就发生在这个窗口里。
词表方案的结构性缺陷是只认字面不认语义。文本分类模型则把输入序列映射到标签空间,从大量样本中学习"什么样的语境配合什么样的表达方式会被判为敏感"。这也是标题里"识别分类"的含义:先判别是否敏感,再区分属于哪一类。
2.2 标签体系怎么设计:二分类、多分类、多标签的选择
标签体系决定标注成本、模型复杂度和业务可解释性。我整理过三种设计的对比:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 二分类(敏感/正常) | 标注一致性好,模型简单,误报易调 | 答不了"哪类敏感" | 起步阶段、人力有限 |
| 多分类(正常/色情/暴恐/辱骂/广告等) | 类别可解释,运营能按类处置 | 边界模糊样本标注一致性差 | 有明确处置流程的业务 |
| 多标签(一条文本可命中多类) | 覆盖交叉情况,如辱骂含广告 | 标注成本高,训练和评估更复杂 | 样本充足、运营精细化 |
我的建议是从二分类起步,但样本入库时就按细类打标。这样前期用二分类模型守住业务底线,同时积累细类样本;等细类样本量足够,再平滑扩展成多分类。数据标注规范要写到具体:一条样本只给一个主标签,边界模糊的样本单独标记不进训练集,标注完成后抽检不一致率,超过百分之十就返工。
2.3 模型选型:为什么优先选中文预训练模型做微调
敏感判别依赖上下文理解,常规做法是用开源中文预训练模型做编码器,输出端接一个分类头微调。预训练模型提供的是语言底座,它已经理解了词与词、句与句之间的关系;微调阶段的任务是让模型学会"这种语境配这种表达等于敏感"。
选型要考虑数据规模。样本量在几百到几千时,全量微调容易过拟合,更稳的做法是冻结编码器只训练分类头。样本量到一万以上,再全量微调。显存有限时考虑蒸馏版模型或直接 CPU 推理,但训练速度要有心理准备。参数规模上,常见的中文预训练权重在 1 亿到 3 亿参数量之间,16G 显存可以微调 base 级别,7G 显存就要调小批次或者开梯度累积。
有一个认知要提前摆正:模型强不强,不取决于预训练阶段见过多少文本,而取决于微调数据覆盖了什么。把希望全押在模型底座上,忽略训练样本对线上真实变体和语境的覆盖,是这类项目最常见的翻车原因。
3. 样本与数据库先行:训练集清洗、SQLite 表设计与入库脚本
3.1 训练样本从哪来、怎么清洗
样本来源通常三条线:开源语料拿基础正负样本;线上举报和审核记录拿真实敏感样本;针对新变体人工构造对抗样本。三条线缺一条,覆盖面就出问题。只靠开源语料,模型对线上真实表达陌生;只靠线上举报,正常样本不足,误报率压不住。
清洗这一步决定模型上限。我一般做四件事:第一,全文去重,同一条文本在训练集出现多次,等于反复灌输给模型,容易过拟合;第二,去 HTML 标签和超链接,网页语料干净与否直接影响 tokenizer 的分词效果;第三,过滤过短文本,长度低于四个字的样本语义信息太少,多数场景下不适合做分类训练;第四,过滤纯数字串和纯英文广告串,这类样本不属于目标识别对象,混进训练集会干扰类别边界。
3.2 数据库表结构:样本表、类别表、预测结果表
源码包带数据库是有道理的,训练样本、类别定义、线上预测日志三块数据需要统一管起来。用 SQLite 起步最合适:单文件、可移植、零运维。三个核心表的结构如下:
CREATE TABLE categories ( id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT ); CREATE TABLE samples ( id INTEGER PRIMARY KEY, content TEXT NOT NULL, category_id INTEGER NOT NULL REFERENCES categories(id), source TEXT DEFAULT 'manual', is_augmented INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE predict_log ( id INTEGER PRIMARY KEY, content_hash TEXT NOT NULL, content_masked TEXT NOT NULL, label_id INTEGER, prob REAL, threshold REAL, review_status INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );samples 表存训练语料,category_id 关联类别表,is_augmented 标记增强样本,方便后续排查模型是否被增强模板带偏。predict_log 表存线上预测结果,content_hash 是文本的哈希值,用来去重和追踪误报;content_masked 是脱敏后的内容,避免原始文本在日志表里堆积引发合规风险。review_status 是人工复核状态:0 待复核,1 确认误报,2 确认有效,这个字段是后续迭代训练集的重要数据来源。
3.3 入库脚本与数据导出接口
清洗后的数据一般是 CSV 或 Excel,常见做法是写脚本读入 SQLite。给一个入库脚本架子:
import sqlite3 import hashlib import csv DB_PATH = "sensitive.db" def load_csv_to_samples(csv_path): conn = sqlite3.connect(DB_PATH) cur = conn.cursor() with open(csv_path, encoding="utf-8") as f: reader = csv.reader(f) next(reader) # 跳过表头 for row in reader: text, cat_name = row[0], row[1] cur.execute("SELECT id FROM categories WHERE name=?", (cat_name,)) cat_row = cur.fetchone() if not cat_row: cur.execute("INSERT INTO categories(name) VALUES(?)", (cat_name,)) cat_id = cur.lastrowid else: cat_id = cat_row[0] content_hash = hashlib.md5(text.encode("utf-8")).hexdigest() cur.execute("SELECT id FROM samples WHERE content_hash=?", (content_hash,)) if not cur.fetchone(): cur.execute( "INSERT INTO samples(content, category_id, source) VALUES(?,?,?)", (text, cat_id, "csv_import"), ) conn.commit() conn.close()逻辑说明:先查类别表,不存在就创建类别再取 id;写入 samples 前用 MD5 做内容去重,重复样本直接跳过。参数说明:DB_PATH 换成实际库路径;CSV 两列分别对应文本和类别名,列顺序不一致就调整 reader 里的索引。这个脚本只入不查,实际迭代时还要补一个导出函数,按类别和来源筛选样本,方便训练脚本直接拉数。
3.4 类别不平衡的缓解手段
线上环境正常文本占比极高,敏感样本只占很小比例。如果直接用原始分布训练,模型会倾向把所有样本判成正常,因为整体准确率已经很高。常见做法有三种:对多数类降采样,把正负比例压到 5:1 以内;对少数类做数据增强,如同义词替换、随机插入、回译生成变体;人工构造对抗样本补充到少数类。增强样本每类最多增加到原来的两到三倍,超过这个量模型会背下增强模板,对真实变体的泛化反而更差。
4. 训练与调参:最小可复现的训练脚本和三个关键参数
4.1 跑通最小训练脚本
基于 transformers 库微调开源中文预训练权重,最小脚本核心步骤是:加载 tokenizer 和模型、包装数据集、配置训练参数、训练保存。给一个能跑的骨架:
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments from datasets import Dataset import pandas as pd import sqlite3 DB_PATH = "sensitive.db" conn = sqlite3.connect(DB_PATH) df = pd.read_sql_query( """SELECT s.content, c.name AS label FROM samples s JOIN categories c ON s.category_id = c.id""", conn, ) conn.close() id2label = {i: name for i, name in enumerate(df["label"].unique())} label2id = {name: i for i, name in id2label.items()} df["label_id"] = df["label"].map(label2id) # 本地预训练权重目录,替换成你下载好的开源中文权重 model_name = "./pretrained/chinese_roberta_wwm" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained( model_name, num_labels=len(id2label) ) def tokenize_fn(batch): return tokenizer(batch["content"], truncation=True, max_length=128, padding="max_length") dataset = Dataset.from_pandas(df[["content", "label_id"]]).map(tokenize_fn, batched=True) dataset = dataset.train_test_split(test_size=0.1) args = TrainingArguments( output_dir="./checkpoints", num_train_epochs=3, per_device_train_batch_size=16, per_device_eval_batch_size=64, learning_rate=3e-5, evaluation_strategy="epoch", save_strategy="epoch", ) trainer = Trainer(model=model, args=args, train_dataset=dataset["train"], eval_dataset=dataset["test"]) trainer.train() trainer.save_model("./sensitive_model") tokenizer.save_pretrained("./sensitive_model")逻辑说明:先从数据库读取样本和标签,建立字符串标签到数字 id 的映射;tokenizer 将文本统一截断或填充到 128 长度;训练集与测试集按 9:1 划分。参数说明:num_train_epochs 从 3 起步,数据量少或类别不平衡时加到 5;learning_rate 在 2e-5 到 5e-5 之间选,预训练模型微调时这个区间最稳;per_device_train_batch_size 受显存限制,16 不够就降到 8,再配梯度累积。
跑通后再叠加早停,用 load_best_model_at_end=True 配合 evaluation_strategy="epoch",让模型自动保留验证集最优的那一版,避免最后一个 epoch 过拟合导致模型反而变差。
4.2 最容易影响结果的三个参数:长度、学习率、类别权重
文本长度是第一个参数。截断阈值定多少,取决于样本长度分布。我一般先跑一段统计,取长度的 95 分位数作为 max_length。短评语类和客服对话场景 128 足够;长文本和公告类场景要升到 256 或 512。文本被截断后,位于尾部的关键信号直接丢失,模型看不到就判不准,这类问题在长文本场景里尤其明显。
学习率是第二个参数。微调阶段如果把学习率设到 1e-4,预训练权重很快被冲掉,语言理解能力受损,效果反而下降。3e-5 起跑是常见做法,观察验证集 loss 震荡明显就降一半。warmup 比例也要设,一般取 0.1,让前几步用小学习率稳定方向。
类别权重是第三个参数。类别不平衡时,可以在损失函数中给少数类更高权重,也可以在数据层面做平衡。我偏好数据层面:让训练集里敏感样本占比不低于百分之二十,这个比例能让模型学到敏感类特征又不至于过拟合。损失函数的权重调整是锦上添花,数据比例才是雪中送炭。
4.3 阈值校准:宁可误杀也不漏放的策略落地
模型输出的是各类概率,直接取最大概率对应的类别只是默认策略。内容审核场景要求敏感内容宁可错拦不可放过,所以阈值必须单独调。验证集上先算出每一条样本的敏感类概率,再遍历阈值观察精确率和召回率的变化:
import numpy as np from sklearn.metrics import precision_recall_fscore_support probs = trainer.predict(dataset["test"]).predictions softmax_probs = np.exp(probs) / np.exp(probs).sum(axis=-1, keepdims=True) sensitive_idx = label2id.get("敏感") y_true = dataset["test"]["label_id"] y_sensitive = (y_true == sensitive_idx).astype(int) sensitive_prob = softmax_probs[:, sensitive_idx] best_f1 = 0 best_th = 0.5 for th in np.arange(0.3, 0.95, 0.05): y_pred = (sensitive_prob >= th).astype(int) p, r, f1, _ = precision_recall_fscore_support(y_sensitive, y_pred, average="binary") if f1 > best_f1: best_f1, best_th = f1, th print(f"best threshold: {best_th:.2f}, F1: {best_f1:.4f}, P: {p:.4f}, R: {r:.4f}")逻辑说明:把验证集预测结果转成敏感类概率,从 0.3 到 0.95 遍历候选阈值,用 F1 选出最优。业务上漏放代价更高时,在 F1 最优值基础上往低取一档,多拦一些;误报代价高则往高取。阈值定好后写进配置文件,推理时读取,避免每换一版模型都重新调。
5. 敏感识别落地避坑清单:五条常见踩坑记录
5.1 变体词补丁打不完,词表越改越长
现象:上线后总能收到漏报反馈,每次都是新变体,词表和规则加了一条又一条,还是追不上。 原因:对抗是动态过程,词表和正则属于一次性静态匹配,天然落后一个身位。 解决:把每个新变体样本作为对抗样本加入训练集,让模型在训练阶段见过多样写法。预测链路里保留关键词做预筛,但只作为兜底,不依赖规则做主判断。
5.2 多人标注结果不一致,模型学歪
现象:同一句话两个人标的类别不同,训练集标签分布混乱,模型在验证集上表现忽高忽低。 原因:标注规范没有覆盖边界样本的处理规则,标注人员按个人理解打了标签。 解决:先写标注规范,明确边界样本不留存;模糊样本单独标为"不确定",不进训练集;标注完成后抽样复核,不一致率超过百分之十就返工。
5.3 预测日志直接存原文,埋下数据隐患
现象:预测日志表里存了用户原始文本,复盘时发现日志里堆积了大量个人隐私信息。 原因:图方便,模型输入原样写入数据库,没有做脱敏。 解决:日志表只存内容哈希和脱敏片段;需要人工复核时再走单独流程调用原始文本,用完即弃。这个习惯越早养成越好,数据量大了再回头清理成本极高。
5.4 线上误报率比验证集高一大截
现象:验证集效果满意,上线后误报率翻倍,运营天天拿截图来找你。 原因:线上真实文本的写法风格与训练集不一致,验证集是从训练数据切出来的,分布同源,指标天然虚高。 解决:上线先灰度,只放 5% 流量,观察误报率稳定后再全量;同时定期抽线上样本回填训练集,落地持续迭代机制。模型上线不是终点,是数据闭环的起点。
5.5 长文本被截断,关键信息正好在尾巴上
现象:长文本样本被截到 128 字,模型把正常段落判成敏感,或者把敏感段落漏掉。 原因:max_length 与样本长度分布不匹配,统计分布时只看了均值,忽略了长尾。 解决:按 95 分位数定长度;对超过上限的文本先分段预测,再按命中阈值合并判定,而不是简单截断丢弃。分段时要保留句子完整边界,避免把一个词切成两半污染语义。
6. 上线前的最后一步:用灰度与兜底规则把模型钉在业务里
模型训好不等于能上线。我自己的习惯是:上线前拉一批与训练集无重叠的近期线上文本,覆盖正常、敏感、边缘三种类型,各几百条,逐类别算精确率和召回率,并打印混淆矩阵看一眼。只盯总准确率没有意义,正常样本占大头,指标很容易被稀释。分类别看指标才能发现模型到底在哪些类别上偏松、哪些类别上偏紧。
灰度阶段的排查重点不是总指标,而是误报集中在什么表达上。运营反馈的每条误报文本都值得拉出来看模型概率:如果误报样本的敏感概率集中在阈值附近,说明模型对边界把握不稳,优先调阈值;如果概率明显高于阈值仍被判错,说明训练样本与线上表达差异大,要回填样本重训。第二种情况靠调阈值解决不了,模型本身就没见过这类写法。
兜底规则是我的最后一道闸:低分段直接放行,高分段直接拦截,中间概率段走人工复核。三段式把模型压力集中在最难判定的那部分样本上,人工量可控,漏放也有拦截层接着。预测日志里复核过的样本要定期回填训练集,这是模型持续变好的燃料。
这套做法不是我第一次就顺手跑通的。早期我把全部信心押在模型输出上,阈值用默认 0.5,结果运营侧的误报反馈雪片一样飞来。从那以后我养成两个习惯:一是阈值必须按业务代价校准,模型只给概率,阈值是人定的;二是永远留一条人工复核通道,文本审核这件事不存在百分百完美的自动化。希望帮到你。
本文还有配套的精品资源,点击获取