ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于MLP的虚假新闻检测:文本预处理、模型训练与调参实践

基于MLP的虚假新闻检测:文本预处理、模型训练与调参实践 简介一份华中科技大学机器学习课程大作业基于多层感知器MLP的互联网虚假新闻检测器项目面向机器学习入门者及需要完成课程设计的学生可用于学习文本分类、深度学习基础与完整项目流程。项目数据预处理覆盖文本清洗、分词及TF-IDF/词嵌入特征提取模型构建包含输入层、隐藏层与输出层使用ReLU等激活函数采用SGD/Adam优化算法并通过反向传播更新参数且利用验证集早停防止过拟合代码逻辑清晰、可直接运行。压缩包共21个文件以Python源码、训练好的模型文件、CSV数据集、README及报告文档为主辅以XML等项目配置文件整体大小92.71MB。已有158人学习下载读者可获得完整可复现的实验方案、完整工程代码与结果说明尤其适合作为机器学习大作业参考或入门NLP实战项目。1. 机器学习大作业做成“检测器”MLP 怎么判断一条新闻真假互联网虚假新闻检测器是我见过机器学习入门项目里“性价比”最高的一种它不用造轮子模型也不需要海量算力用多层感知器MLP就能把“文本分类”这件事跑通。这份华中科技大学机器学习大作业把完整的训练、调参、预测链路都打包好了从preprocess.py的文本清洗到ML_fit.py的模型拟合再到optimize.py的学习率搜索最后用predict.py对未知新闻输出结果。对正在做机器学习课程设计、或者想拿一个端到端练手项目的人来说它的价值在于你能看到一份大作业里最容易被忽略的工程细节——数据怎么切、特征怎么对齐、训练怎么早停、预测结果怎么写回文本文件。这篇笔记就按我拆项目的习惯把它从数据处理到模型推理一层层剥开标出参数边界和实战中的坑。2. 数据预处理决定 MLP 上限的往往不是网络结构而是文本清洗2.1 拿到的原始新闻文本长什么样先别急着“喂”模型这个项目的输入是互联网新闻文本。实际拿到的原始数据可能非常脏有的带 HTML 标签有的夹杂表情符号、多余空白、URL还有的整条是转发拼接。直接把这些文本喂给 MLP 是行不通的因为 MLP 的输入必须是固定维度的数值向量你首先得把一条条新闻“翻译”成向量。第一步是写一个统一的清洗函数。我在preprocess.py里看到过这类处理逻辑通常包括这几件事去掉 HTML 标签、去掉 URL、去掉非文字字符、把连续空白压缩成一个空格。对中文数据还要额外做一件事把全角字符转半角否则“”和“”会被当成两个不同的词。import re def clean_text(raw: str) - str: # 去掉 HTML 标签例如 p、a href... text re.sub(r[^], , raw) # 去掉 URL text re.sub(rhttp\S, , text) # 只保留中文、英文、数字和常用标点其余全部去掉 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、\s], , text) # 把连续空白包括换行、tab压缩成单个空格 text re.sub(r\s, , text).strip() return text.lower()这段代码的清洗顺序是有讲究的先去 HTML 标签再去 URL。如果顺序反过来URL 里带的特殊字符会干扰正则匹配。只保留中英文和数字是因为 TF-IDF 这类统计特征对符号不敏感留太多符号反而会造成特征维度膨胀。最后那句lower()是给英文数据准备的中文不受影响。2.2 分词中文用 jieba英文用空格切分就够了清洗完之后下一步是分词。英文文本按空格切分基本够用但中文没有天然的词边界必须借助分词工具。在这个项目里常见的做法是引入 jieba 分词。import jieba def tokenize(text: str, use_jieba: bool True) - list: if use_jieba: # jieba.lcut 返回分词后的 list比 cut 更直观 tokens jieba.lcut(text) else: tokens text.split() # 过滤掉单个字符和无意义的词减小特征空间 tokens [t for t in tokens if len(t.strip()) 1] return tokens这里我一般会加一个“长度大于 1”的过滤单字在新闻文本里大部分是语气词或噪声保留它们只会让 TF-IDF 矩阵更稀疏。如果你的数据集里有很多专有名词比如“特朗普”“新冠疫苗”建议再加一个自定义词典jieba.load_userdict(userdict.txt)否则“特朗普”可能被切成“特朗”“普”两个词特征质量会明显下降。2.3 TF-IDF 特征化词袋模型的维度控制与词表对齐分词完成之后每条新闻就是一个词列表。接下来要把这些词列表转成数值向量。这个项目用的是 TF-IDF不是 Word2Vec原因是 TF-IDF 更简单、可解释性强而且对于几千条数据的小规模训练集TF-IDF 的效果往往不比词向量差。from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( tokenizertokenize, max_features5000, # 只保留词频最高的 5000 个词 ngram_range(1, 2), # 考虑单个词和相邻两个词 min_df2, # 至少在 2 篇文档中出现过 max_df0.8 # 出现在 80% 以上文档中的词视为停用词 ) # 注意fit_transform 只用在训练集上验证集和测试集必须用 transform train_tfidf vectorizer.fit_transform(train_texts) val_tfidf vectorizer.transform(val_texts)max_features5000是控制特征维度的关键。如果设成 20000MLP 的输入层就会膨胀训练时间成倍增加而准确率提升往往不到 1%。ngram_range(1, 2)会让“虚假新闻”这种双词组合作为一个特征出现对语义识别有帮助但代价是特征矩阵更稀疏。min_df2是为了去掉那些只在一条新闻里出现过的生僻词——它们对分类没有贡献只会增加噪声。max_df0.8是自动过滤“的”“了”“是”这类高频但无区分度的词。这里最容易翻车的是训练集和验证集的向量化不一致。fit_transform是在训练集上学习词表验证集只能调用transform它会把验证集里没见过的词直接忽略。如果你图省事对验证集也做fit_transform模型在验证集上的表现会虚高因为验证集的词表被单独拟合了这种数据泄漏在大作业答辩时会被直接问出来。2.4 数据集划分与标签平衡先看分布再决定要不要分层采样预处理最后一步是数据划分。常规做法是训练集、验证集、测试集按 8:1:1 或 6:2:2 划分。但虚假新闻数据集往往存在类别不平衡的问题——真实新闻多、虚假新闻少或者反过来。如果不处理模型会把所有样本都预测成多数类准确率看着很高实际一点用没有。from sklearn.model_selection import train_test_split # stratifyy 保证训练集和测试集中真假新闻的比例与原始数据一致 X_train, X_test, y_train, y_test train_test_split( texts, labels, test_size0.2, random_state42, stratifylabels ) X_train, X_val, y_train, y_val train_test_split( X_train, y_train, test_size0.25, random_state42, stratifyy_train )stratifylabels是必须写的参数它做的是分层采样。如果不写随机划分可能让训练集里 90% 全是真实新闻模型根本没见过足够多的虚假样本预测阶段会把虚假新闻也判成真的。这里的random_state42固定随机种子是另一个容易忽略的细节——不固定种子你每次跑出来的结果都不一样后面调参时完全没法判断是参数变了还是数据划分变了。3. MLP 模型构建与训练从输入层到反向传播的完整链路3.1 网络结构怎么定输入维度跟着特征走隐藏层靠实验说话预处理完成后每条新闻变成一个 5000 维的 TF-IDF 向量。MLP 的输入层维度就是 5000这个值是写死的由vectorizer.max_features决定。如果你改了词表大小输入层必须同步改否则代码直接报维度不匹配。隐藏层的设计是这个项目的核心。对于文本分类任务常见的经验值是“从输入维度往下递减”比如 5000 → 256 → 64 → 2。第一层隐藏层不宜太小否则特征压缩太快信息丢失严重。但也不是越大越好——512 个神经元的隐藏层参数量是5000 * 512 512约 256 万参数对于只有几千条数据的小数据集这么大参数量几乎必然过拟合。from sklearn.neural_network import MLPClassifier mlp MLPClassifier( hidden_layer_sizes(256, 64), # 两个隐藏层256 和 64 activationrelu, # 隐藏层激活函数 solveradam, # 优化器 alpha1e-4, # L2 正则化系数 max_iter200, # 最大迭代轮数 early_stoppingTrue, # 自动早停 validation_fraction0.1, # 从训练集再切 10% 做内部验证 n_iter_no_change10, # 连续 10 轮无提升则停止 random_state42 ) mlp.fit(X_train, y_train)early_stoppingTrue是这个项目里最值得用的参数sklearn 会自动从训练集里切出validation_fraction0.1作为内部验证集当验证集损失连续n_iter_no_change10轮不下降时训练自动终止。这比你自己手写早停逻辑省事得多。但注意如果你在外部已经切了验证集内部还要再切 10%实际训练数据会进一步减少。数据量充足时没问题数据量紧张时建议把early_stopping关掉自己手动按 epoch 保存最优模型。3.2 手写训练循环反向传播和梯度更新的最小实现用 sklearn 的MLPClassifier能快速出结果但大作业里如果完全靠封装库答辩时被问到“反向传播怎么算的”容易卡壳。这个项目的ML_fit.py和optimize.py其实暗示了另一条路自己用 PyTorch 或 NumPy 写训练循环把优化器、学习率调度、早停都拆开控制。我建议至少跑通一个手写版本因为在调参阶段手写循环能让你看到每个 epoch 的损失变化。import torch import torch.nn as nn class FakeNewsMLP(nn.Module): def __init__(self, input_dim5000, hidden_dims(256, 64), num_classes2): super().__init__() layers [] prev_dim input_dim for h_dim in hidden_dims: layers.append(nn.Linear(prev_dim, h_dim)) layers.append(nn.ReLU()) layers.append(nn.BatchNorm1d(h_dim)) # 缓解梯度消失 prev_dim h_dim layers.append(nn.Linear(prev_dim, num_classes)) self.net nn.Sequential(*layers) def forward(self, x): return self.net(x) model FakeNewsMLP(input_dim5000) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4)BatchNorm1d加在 ReLU 之后是我个人偏好它能稳定每层输入的分布让模型对学习率不那么敏感。但注意 BatchNorm 对 batch size 有要求——如果你的 batch size 只有 8 或 16BatchNorm 统计出来的均值和方差噪声很大反而坏事。数据量小的时候去掉 BatchNorm改用更高的weight_decay更稳妥。训练循环里的关键是把数据转成 Tensor并且保证维度是(batch_size, input_dim)。TF-IDF 矩阵是稀疏格式直接喂给 PyTorch 需要先toarray()转稠密但这样内存占用会剧增。一个可行的方案是直接训练时按 batch 切片再转稠密import numpy as np def train_one_epoch(model, X_tfidf, y, batch_size64, optimizerNone, criterionNone): model.train() total_loss, correct, total 0, 0, 0 n_batches int(np.ceil(X_tfidf.shape[0] / batch_size)) for i in range(n_batches): start, end i * batch_size, min((i 1) * batch_size, X_tfidf.shape[0]) X_batch torch.tensor(X_tfidf[start:end].toarray(), dtypetorch.float32) y_batch torch.tensor(y[start:end], dtypetorch.long) optimizer.zero_grad() outputs model(X_batch) loss criterion(outputs, y_batch) loss.backward() optimizer.step() total_loss loss.item() * (end - start) correct (outputs.argmax(dim1) y_batch).sum().item() total end - start return total_loss / total, correct / total这个训练函数的每一步都是标准的前向传播算输出交叉熵算损失backward()反传播算梯度optimizer.step()更新权重。注意这里必须在每个 batch 开头调用optimizer.zero_grad()否则梯度会跨 batch 累加权重更新方向会乱掉。训练完成后记得调用model.eval()再去做预测否则 BatchNorm 和 Dropout 在推理阶段的行为不一致预测结果会受影响。3.3 优化器与学习率调参optimize.py 里的搜索逻辑这份资源的optimize.py做的正是调参环节。MLP 对学习率极其敏感学习率太大损失函数在最优值附近震荡训练 loss 不降反升学习率太小训练速度慢到让人怀疑代码写错了。常见做法是用网格搜索或随机搜索扫几组学习率再结合学习率衰减策略。from sklearn.model_selection import GridSearchCV from sklearn.neural_network import MLPClassifier param_grid { hidden_layer_sizes: [(256, 64), (128, 128), (256, 128, 64)], activation: [relu, tanh], alpha: [1e-3, 1e-4, 1e-5], learning_rate_init: [1e-2, 1e-3, 1e-4] } grid GridSearchCV( MLPClassifier(max_iter200, early_stoppingTrue, random_state42), param_grid, cv3, # 3 折交叉验证 scoringf1_macro, # 类别不平衡时用 F1 而不是准确率 n_jobs-1 # 并行搜索 ) grid.fit(X_train, y_train) print(grid.best_params_)scoringf1_macro是第二个关键点。如果你用默认的accuracy在真假新闻比例 9:1 的数据集上全预测成多数类也能拿 90% 准确率但 F1 会很低。网格搜索的目的不是找一个训练集上最准的模型而是找一个在交叉验证中泛化能力最好的模型。cv3对几千条数据是合理的cv5会让每次训练的数据更少训练时间也成倍增加。4. 避坑指南这五个问题让我反复翻车提前帮你排掉4.1 验证集准确率虚高测试集一塌糊涂数据泄漏现象训练完在验证集上 F1 有 0.92一上测试集直接掉到 0.78。多次重跑结果类似。原因验证集和测试集的 TF-IDF 向量化用了fit_transform相当于验证集和测试集的词表被单独学习了一遍模型间接“见过”了验证集的文本分布。这是典型的数据泄漏。解决训练集fit_transform验证集和测试集一律只用transform。检查你的预处理代码确保vectorizer只实例化一次词表只来自训练集。4.2 训练 loss 变成 NaN模型权重爆炸现象训练到第 30 轮左右loss 突然变成nan之后再也无法恢复。原因学习率设置过大梯度爆炸导致权重值溢出或者输入数据里有 NaN 值清洗时没处理干净反向传播把 NaN 传播到了每一层。解决先把学习率降到1e-4再试。如果还是 NaN用np.isfinite(X_tfidf.data).all()检查输入矩阵是否有非法值。还可以在Linear层后面加一层梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)把梯度模长限制在 1.0 以内。4.3 模型只输出一类F1 为 0现象训练完成loss 一直在降但预测结果全部是“真实新闻”虚假新闻一条没识别出来。原因类别不平衡。数据集中真实新闻占 85% 以上模型发现把所有样本预测为多数类就能把 loss 压得很低于是放弃了少数类。解决两种方案。一是用class_weightbalancedsklearn 里直接传参或自定义权重向量给少数类更高的 loss 权重二是在训练集上做过采样对虚假新闻样本复制几份让正负样本比例接近 1:1。注意过采样要在划分训练集之后做不能把测试集也一起过采样。4.4 训练时间越来越长但准确率不再提升现象loss 在第 60 轮之后基本不变但max_iter还没跑完训练白白消耗时间。原因没有早停机制或者early_stopping的n_iter_no_change设得太大。模型已经收敛继续训练只会过拟合。解决把early_stopping打开n_iter_no_change设成 5~10。手写循环的话每个 epoch 后记录验证集 loss连续 5 轮不下降就break同时保存验证集 loss 最低时的模型权重这样你拿到的是全局最优而不是最后一次迭代的权重。4.5 特征维度 5000训练到一半内存爆了现象机器 8G 内存TF-IDF 矩阵转稠密时直接MemoryError。原因X_train.toarray()把稀疏矩阵转成了5000 * 5000的稠密矩阵光这一个矩阵就是 5000×5000×8 字节约 200MB如果 batch 处理不当容易爆。解决不要一次性转稠密。按上面训练循环的写法每个 batch 只转一批数据toarray()之前先切片。如果内存还是不够把max_features降到 2000对 MLP 来说 2000 维已经能拟合大部分文本分类任务了。5. 快速吃透这份代码资源的方法在把整份代码通读前先用README.md和doc/report.docx搭一个整体认知框架。然后将五个.py文件按逻辑顺序组织命令。中途要留近期作业项目资源清单cd recall_unpack/ ls -R # 按逻辑顺序请求文件 cat README.md python preprocess.py --input data/raw --output data/clean python ML_fit.py --mode train --hidden 256 64 python optimize.py --param-grid lr alpha python predict.py --input data/test_unlabeled.txtpreprocess.py通常需要指定原始文本路径和输出路径ML_fit.py里隐藏层参数可以通过命令行覆盖便于做多组对比实验optimize.py则自己跑一组网格搜索把best_params_打印出来最后用predict.py对未知新闻批量推理输出结果会写入mlp_pred.txt。在你实际项目里建议先跑一遍默认配置然后用验证集看 loss 曲线我从这份作业里学到的第一课是看到下滑的 loss 曲线不要高兴太早验证集 loss 曲线最早抬头的那个点就是模型真正停止学习的地方从那以后我每次训练都强制在 fit 循环里记下每个 epoch 的验证集 F1 和 loss并自动保存验证集最优权重这个习惯让我少走了很多弯路。希望帮到你。本文还有配套的精品资源点击获取
返回列表