简介这是一份面向深度学习初学者与进阶学习者的网络欺凌检测实战资源可作为毕业设计、课程设计、大作业或工程实训的参考方案。资源围绕网络暴力文本识别任务提供从数据到模型落地的完整链路帮助读者理解文本分类在社交内容治理中的具体应用。压缩包共8个文件约2.68MB包含2个ipynb交互式笔记、2个json数据与词表文件、1个py脚本、1个h5模型文件及md说明与license分别对应训练流程、推理示例、已训练模型和词表构建结果。已有101人学习关注。读者可直接加载预训练模型与词表在Jupyter中复现训练与预测过程也可基于纯Python脚本快速验证效果同时借助训练笔记梳理数据预处理、词表构建、模型搭建与评估的完整思路适合作为文本分类入门与网络欺凌检测方向的实践起点。1. 网络欺凌检测从一条恶意评论到可复现的深度学习流水线社交平台上一条带有人身攻击的评论从发布到被举报平均不超过几分钟但人工审核往往要几小时才能响应。基于深度学习的网络欺凌检测要解决的就是把这段延迟压到秒级让模型自动识别文本中的侮辱、威胁、歧视与群体攻击先拦截高风险内容再交给人工复核。它适合三类人想做一个能写进简历的深度学习项目案例的学生需要给社区、弹幕、评论区加一道自动过滤的开发者以及手上有标注数据、想验证模型到底能不能落地的算法工程师。这件事的门槛没有想象中高但坑比想象中密——数据脏、类别极不平衡、脏话变体满天飞模型在测试集上 F1 很高上线后却频繁翻车。下面按“数据怎么来、模型怎么选、参数怎么调、坑在哪”一路拆开讲。2. 数据准备网络欺凌语料怎么清洗、怎么切分、怎么防泄漏2.1 先搞清楚你要检测的是哪一类欺凌网络欺凌不是一个二分类标签就能概括的。常见做法是拆成四个维度侮辱谩骂、威胁恐吓、歧视仇恨、群体攻击。不同维度的语言特征差异很大——威胁类往往有明确的动作词和对象歧视类依赖身份词和贬损搭配侮辱类则高度依赖脏话变体和上下文反讽。如果一上来就做“欺凌/非欺凌”二分类模型很容易学到“只要出现某个脏话词就是欺凌”遇到反讽或引用就会翻车。我一般会先做一轮标签审计随机抽 200 条正样本人工看它们分别落在哪个维度如果某个维度占比超过 70%说明数据来源单一模型泛化能力会很差。这时候要么补充其他来源的数据要么在标签体系里明确只做这个维度不要硬撑四分类。2.2 清洗脚本去噪、去重、处理脏话变体原始语料通常来自公开数据集或平台导出里面混着大量噪声HTML 标签、重复刷屏、无意义符号、以及为了绕过审核而故意拼写的脏话变体比如用数字、拼音、谐音、间隔符号。下面这段 Python 脚本做三件事去重、去噪、把常见变体归一化。import re import hashlib from collections import Counter def normalize_text(text: str) - str: # 去掉 HTML 标签和 URL text re.sub(r[^], , text) text re.sub(rhttps?://\S|www\.\S, , text) # 全角转半角统一大小写 text text.encode(utf-8).decode(utf-8) text .join([chr(ord(c) - 0xFEE0) if 0xFF01 ord(c) 0xFF5E else c for c in text]) text text.lower() # 压缩连续重复字符草草草草 - 草草 text re.sub(r(.)\1{2,}, r\1\1, text) # 去掉多余空白 text re.sub(r\s, , text).strip() return text def dedup_by_hash(texts, labels): seen set() out_texts, out_labels [], [] for t, l in zip(texts, labels): h hashlib.md5(t.encode(utf-8)).hexdigest() if h not in seen: seen.add(h) out_texts.append(t) out_labels.append(l) return out_texts, out_labels # 脏话变体归一化表按自己语料补充 VARIANT_MAP { r傻\s*[逼比币]: 傻逼, r[艹草操]\s*[你尼]: 操你, rn\s*m\s*s\s*l: nmsl, } def normalize_variants(text: str) - str: for pattern, repl in VARIANT_MAP.items(): text re.sub(pattern, repl, text) return text逻辑说明normalize_text先做通用清洗dedup_by_hash用 MD5 去重normalize_variants处理变体。参数上重复字符压缩阈值设为 2 次以上才压缩是因为“哈哈哈”和“哈哈”语义接近但“草草草”和“草”情绪强度不同压到两个是折中。变体表不要一次写太大先统计语料里高频的变体按频次补否则容易误伤正常词。2.3 切分与防泄漏为什么随机切分会让指标虚高很多人直接train_test_split(random_state42)就开跑结果测试集 F1 0.95上线就崩。原因是同一段对话的连续评论、同一用户的重复发言、甚至同一模板的变体会被随机分到训练集和测试集造成信息泄漏。正确做法是按“对话线程”或“用户”分组切分保证同一线程/同一用户只出现在一个集合里。from sklearn.model_selection import GroupShuffleSplit # groups 可以是 thread_id 或 user_id gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(texts, labels, groupsthread_ids))参数说明test_size0.2是常见起点如果正样本很少可以调到 0.3 保证测试集里有足够正例。groups必须和文本一一对应不能有缺失值。切完之后要检查训练集和测试集的标签分布如果偏差超过 5 个百分点说明切分不稳定换随机种子或分层分组切分。3. 模型选型从 TextCNN 到 BERT 微调哪个更适合你的数据量3.1 小数据量优先 TextCNN别一上来就上大模型如果你的标注数据在 1 万条以下TextCNN 往往比 BERT 更稳。原因不是 TextCNN 更强而是小数据下大模型容易过拟合微调学习率稍微大一点就崩。TextCNN 结构简单嵌入层 多尺寸卷积核 最大池化 全连接。它对局部 n-gram 特征敏感正好匹配脏话、威胁短语这类模式。import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim128, num_classes2, kernel_sizes(2,3,4), num_filters128): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, k) for k in kernel_sizes ]) self.dropout nn.Dropout(0.5) self.fc nn.Linear(num_filters * len(kernel_sizes), num_classes) def forward(self, x): # x: [batch, seq_len] emb self.embedding(x) # [B, L, E] emb emb.permute(0, 2, 1) # [B, E, L] feats [] for conv in self.convs: c torch.relu(conv(emb)) # [B, F, L-k1] p torch.max_pool1d(c, c.size(2)).squeeze(2) # [B, F] feats.append(p) out torch.cat(feats, dim1) out self.dropout(out) return self.fc(out)逻辑说明kernel_sizes(2,3,4)覆盖二元到四元短语num_filters128是常见起点数据量小可以降到 64。dropout0.5是防过拟合的关键如果训练集准确率远高于验证集先加 dropout 再考虑减层。嵌入层可以加载预训练词向量也可以从头训数据少于 5000 条时建议加载。3.2 数据量过万再考虑 BERT 微调学习率是生死线当标注数据超过 1 万条且类别平衡尚可时BERT 类模型的优势才显现出来。微调时最重要的参数是学习率常见范围是 2e-5 到 5e-5超过 1e-4 很容易灾难性遗忘。批次大小在 16 到 32 之间序列长度截断到 128 通常够用长文本可以到 256但显存会翻倍。from transformers import BertTokenizer, BertForSequenceClassification, Trainer, TrainingArguments tokenizer BertTokenizer.from_pretrained(bert-base-chinese) model BertForSequenceClassification.from_pretrained(bert-base-chinese, num_labels2) def tokenize(batch): return tokenizer(batch[text], paddingmax_length, truncationTrue, max_length128) train_enc train_dataset.map(tokenize, batchedTrue) test_enc test_dataset.map(tokenize, batchedTrue) args TrainingArguments( output_dir./ckpt, learning_rate2e-5, per_device_train_batch_size16, num_train_epochs3, evaluation_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modelf1, )参数说明learning_rate2e-5是中文 BERT 微调的稳妥起点num_train_epochs3通常足够超过 5 轮几乎一定过拟合。metric_for_best_modelf1是因为类别不平衡时准确率没有参考价值。如果显存不够把max_length降到 64或者用梯度累积模拟大 batch。3.3 类别不平衡重采样、加权损失、阈值移动怎么选网络欺凌数据里正样本通常只占 5% 到 20%直接训练会让模型倾向于全预测为负。三种常见做法重采样过采样正例或欠采样负例、加权损失给正类更高权重、阈值移动调低正类判定阈值。我一般先用加权损失因为它不改数据分布实现最简单。# 加权损失正类权重 负类数量 / 正类数量 pos_weight torch.tensor([neg_count / pos_count]) criterion nn.BCEWithLogitsLoss(pos_weightpos_weight)如果加权损失后召回率还是上不去再考虑过采样但要注意过采样会放大噪声标注的影响。阈值移动放在最后在验证集上扫一遍阈值选 F1 最高的点但上线前要用另一批数据确认阈值没有过拟合。4. 训练与评估指标怎么选、阈值怎么定、错误怎么归因4.1 别只看准确率F1 和召回率才是上线依据在类别不平衡场景下准确率是最容易骗人的指标。一个把所有样本判为“非欺凌”的模型准确率可以到 85%但召回率为 0。实际落地时我更关注三个数正类召回率漏报多少、正类精确率误报多少、以及 F1。如果业务上漏报代价高就优先保召回精确率可以靠人工复核兜底。评估时还要看混淆矩阵的具体分布。如果假阳性集中在某些特定词上比如“打死你”在游戏语境里是玩笑在威胁语境里是欺凌说明模型没有学到上下文需要补充这类难例。4.2 阈值扫描脚本找到业务可接受的平衡点import numpy as np from sklearn.metrics import f1_score, precision_score, recall_score def scan_threshold(y_true, y_prob, thresholdsnp.arange(0.1, 0.9, 0.05)): results [] for th in thresholds: y_pred (y_prob th).astype(int) results.append({ threshold: round(th, 2), f1: f1_score(y_true, y_pred), precision: precision_score(y_true, y_pred), recall: recall_score(y_true, y_pred), }) return results逻辑说明y_prob是模型输出的正类概率thresholds从 0.1 扫到 0.85步长 0.05。拿到结果后不要直接选 F1 最高的阈值要看业务能接受多少误报。如果误报会导致大量正常用户被拦截就选精确率更高的阈值如果漏报会导致严重舆情就选召回率更高的阈值。选定后在独立测试集上再验证一次。4.3 错误归因把翻车样本捞出来看模型上线前我一定会做一轮错误归因把假阳性和假阴性各抽 50 条人工看它们为什么错。常见原因有四类反讽和引用被误判、脏话变体没覆盖、短文本信息不足、标注本身有错。前两类靠补充数据和变体表解决第三类可以考虑引入上下文或用户历史第四类要回去修标签。5. 避坑与排查网络欺凌检测落地时最容易翻车的 5 个点5.1 现象测试集 F1 0.95上线后误报率飙升原因随机切分导致信息泄漏同一线程或同一用户的样本同时出现在训练和测试集。解决改用 GroupShuffleSplit按 thread_id 或 user_id 分组切分切分后检查标签分布。5.2 现象模型把“打死你”在游戏语境里判成威胁原因模型只学到了词本身没有学到上下文。解决补充游戏语境下的负样本或者在特征里加入前后文窗口让模型看到“这局打死你”和“我打死你”的区别。5.3 现象脏话变体绕过检测比如用拼音首字母或间隔符号原因归一化表覆盖不全或者归一化在分词之后才做。解决归一化必须在分词之前做变体表按语料统计高频模式逐步补充不要一次写死。5.4 现象训练损失正常下降但验证集 F1 一直不涨原因学习率太大导致模型在最优解附近震荡或者类别权重设得过高导致模型偏向正类。解决把学习率降一个数量级检查 pos_weight 是否超过 10如果超过先做数据层面的重采样。5.5 现象模型对短文本少于 5 个字几乎全判错原因短文本信息量不足嵌入层学不到有效表示。解决对短文本单独设一个规则兜底或者把用户历史发言拼接进来但要注意隐私合规。6. 进阶技巧用对抗验证和持续学习把模型养住6.1 对抗验证提前发现训练集和线上分布的偏移对抗验证的做法是把训练集和线上采样数据混在一起训练一个分类器去区分“这条来自训练集还是线上”。如果分类器 AUC 明显高于 0.5说明两者分布有差异模型上线后大概率会掉点。这时候要找出差异最大的特征针对性补充数据。from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score # X_combined: 训练集和线上数据的特征拼接 # y_combined: 0 表示训练集1 表示线上 clf RandomForestClassifier(n_estimators100, random_state42) auc cross_val_score(clf, X_combined, y_combined, cv5, scoringroc_auc).mean() print(fAdversarial validation AUC: {auc:.4f})参数说明n_estimators100够用cv5看稳定性。AUC 超过 0.7 就要警惕超过 0.8 说明分布差异很大必须先解决再上线。6.2 持续学习用线上反馈数据做增量训练模型上线后人工复核的结果就是新的标注数据。我一般每周把复核过的数据加入训练集做一次增量微调学习率设为初始值的十分之一只训 1 到 2 轮。这样模型能跟上新的脏话变体和表达方式但不会因为新数据太少而遗忘旧知识。策略学习率训练轮数适用场景全量重训2e-53数据量翻倍或标签体系调整增量微调2e-61-2每周新增复核数据只调分类头1e-43-5嵌入层冻结数据量很少6.3 一个我踩过的坑别用准确率做早停早期我做早停时用了metric_for_best_modelaccuracy结果模型在验证集准确率 0.92 时保存但正类召回只有 0.3。后来改成 F1召回才上来。这个习惯我一直保持到现在只要类别不平衡早停指标必须用 F1 或召回绝不用准确率。希望帮到你。本文还有配套的精品资源点击获取