在内容平台的后台审核场景里,垃圾评论识别是一个持续存在的难题。如果只靠关键词过滤,很难解决批量注册、文本变体和抱团刷评的问题。用 Flask 搭建一个基于 GCN 的垃圾评论识别系统,可以把评论之间的关联关系纳入模型,从结构上发现异常。这篇文章会围绕系统设计、数据建图、模型训练、Flask 接口封装和常见问题展开。读完可以动手做出一个最小可运行版本,也能理解为什么图卷积网络适合这类任务。
这里要说明定位:示例代码用于说明思路,实际项目需要根据自己的包名、路径、数据格式和依赖版本调整。我会采用偏工程化的写法,而不是只给一个模型训练脚本。
1. 先弄明白垃圾评论识别为什么需要图卷积网络
1.1 关键词分类和文本分类的局限
很多初版审核系统都从关键词开始。广告词、涉黄涉赌词、引流词命中后直接拦截。这种方案实现简单,但问题也很明显:垃圾评论会变化,比如“VX”“薇❤”“加我扣”这类变体,正则写不完。后来大家普遍使用文本分类模型,把评论文本转换成向量,再用 LSTM、CNN 或 BERT 做二分类。这类模型能学到文本语义,但对“评论之间的关系”是盲区。
实际问题里,垃圾评论往往不是孤立出现的。同一个黑灰产团伙会注册大量账号,在短时间内对同一批内容发布相似评论;或者一批正常账号被盗后集中发 spam。这些评论单独看可能很隐蔽,但如果把“谁发的、什么时候发、文本是否高度相似”放在一起,异常模式就非常明显。传统文本分类只输入一条评论文本,没有使用这些结构化信息。
1.2 评论数据如何变成一张图
图神经网络的处理方式是把评论数据建模成图结构。图由节点和边组成:
- 节点:一条评论、一个用户,或一个内容。
- 边:评论之间的关联关系,比如同一用户发布、文本相似度超过阈值、发布时间相近、引用关系、IP 相同。
- 节点特征:评论文本的向量表示,比如 TF-IDF、词向量或句子向量。
- 节点标签:是否垃圾评论。
以评论为节点设计时,如果两条评论来自同一个 user_id,或者在向量空间里余弦相似度很高,就给它们建一条边。这样,垃圾评论会因为在图上形成密集的小团体而被 GCN 感知到。
除了评论节点,还可以把用户节点和内容节点混入同一张异构图。但这会明显增加建模复杂度。最小可行版本先使用“评论节点 + 关系边”的同构图,场景理解起来更直接。
1.3 GCN 在图结构中做了什么
GCN 全称是 Graph Convolutional Network,中文一般叫图卷积神经网络。它的核心思想是:每个节点的表示不只由自身特征决定,还会不断聚合邻居节点的特征。
一层图卷积可以简化为:
H^(l+1) = σ( D^(-1/2) A D^(-1/2) H^(l) W^(l) )其中:
- A 是邻接矩阵,表示节点之间是否有边。
- D 是度矩阵,D^(-1/2) 用于归一化,避免度数高的节点特征被过度放大。
- H^(l) 是第 l 层的节点特征矩阵。
- W^(l) 是这一层可学习的权重。
- σ 是激活函数,常用 ReLU。
一句话概括:GCN 让一条评论学习到“我和谁连接,我的邻居大概是什么类型”的上下文信息。堆叠两层时,第二层能聚合两跳邻居的信息,也就是“朋友的邻居”。对垃圾评论检测来说,这正好可以帮助识别“抱团刷评”结构。
容易误解的是,GCN 并不是把所有评论简单平均。每条评论仍然有独立向量表示,只是这个表示在传播过程中融入了局部结构信息。边定义得是否合理,比模型结构本身对效果影响更大。
2. 系统架构和技术选型
2.1 整体分层
整个系统可以分成五个部分:
- 数据层:保存评论内容、用户、时间、标签,最少需要 CSV 或 MySQL。
- 图构建层:从评论数据中提取节点特征和边关系,生成邻接矩阵。
- 模型层:训练 GCN 模型,输出每条评论是垃圾的概率。
- 服务层:使用 Flask 提供 HTTP 接口,接收新评论文本并返回识别结果。
- 调用层:后台管理端、审核界面或命令行工具。
训练和服务可以分离。训练时离线构建整张图,跑 GCN 模型;预测时加载训练好的模型,通过 Flask 对外暴露接口。
2.2 技术栈和依赖
Python 是最稳妥的选择,生态完善。依赖可以这样设计:
| 组件 | 用途 | 推荐方式 |
|---|---|---|
| Python | 运行环境 | 3.8 以上,建议 3.9 或 3.10 |
| Flask | Web 服务 | Flask 2.x |
| PyTorch | 深度学习框架 | CPU 版本可用于学习环境 |
| PyTorch Geometric | 图神经网络算子 | 可选,安装失败时可自实现 GCN |
| scikit-learn | TF-IDF、评估指标 | 稳定常用 |
| NetworkX | 图结构调试 | 小规模图可视化或检查连通性 |
| scipy | 稀疏矩阵运算 | 配合 PyTorch 稀疏张量 |
依赖版本需要以实际安装环境为准。尤其是 PyTorch 和 PyTorch Geometric,两者版本必须匹配,否则会出现导入错误。
2.3 学习环境与生产环境的差异
学习环境只需要能跑通小规模数据。可以全部在本地执行,用 CPU 训练几分钟甚至几秒,数据量控制在几百到几千条评论。
生产环境要求高很多:
- 评论数据量会持续增长,图不能每次请求都重建。
- 需要周期性定时建图、训练、评估和发布模型。
- Flask 服务需要加超时、鉴权、限流和日志。
- 模型预测可能被高并发调用,需要评估 QPS 和响应时间。
- 训练数据和线上数据的分布可能偏移,需要监控。
这篇文章的示例以学习环境为主,最后再给出生产化建议。
3. 数据准备与图构建
3.1 评论数据字段设计
先准备一份最小数据集。使用 CSV 或数据库表保存评论,建议包含以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| comment_id | string | 评论唯一 ID |
| user_id | string | 发布者 ID |
| content | string | 评论正文 |
| label | int | 0 正常,1 垃圾 |
| create_time | datetime | 发布时间 |
示例 CSV:
comment_id,user_id,content,label,create_time c001,u001,这篇内容写得很实用,0,2025-01-01 10:00:00 c002,u002,加我微信领资料,1,2025-01-01 10:01:00 c003,u002,加我微信领资料,1,2025-01-01 10:02:00 c004,u003,学到了谢谢分享,0,2025-01-01 10:03:00读取数据时要注意统一编码。中文环境推荐统一使用 UTF-8,否则会出现乱码或分词异常。
3.2 节点特征向量怎么来
GCN 的输入特征矩阵是一个N x D的矩阵,N 是评论节点数量,D 是特征维度。最简单稳定的特征来自 TF-IDF。
scikit-learn 提供了现成实现。先用全量评论文本拟合TfidfVectorizer,再转换成特征矩阵:
from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( max_features=512, ngram_range=(1, 2), stop_words=None ) X = vectorizer.fit_transform(df["content"]).toarray()这里把特征维度限制在 512 维,是为了让模型训练更快。ngram_range=(1, 2)能同时考虑单词和连续两个词。中文场景下可以结合 jieba 分词,但TfidfVectorizer默认按字符或空格切分,中文效果一般。实际项目中可以做自定义分词器。
如果数据量大且算力允许,也可以使用 sentence-transformers 生成句向量。这样向量能表达更多语义,但依赖模型下载和 GPU。最小版本用 TF-IDF 就够了。
3.3 边关系构建
边的定义决定了 GCN 能看到什么样的结构信息。给出三种常用规则:
- 相同 user_id:同一用户连续或重复发布多条评论时建边。
- 文本相似度超过阈值:两条评论内容高度相似时建边。
- 发布时间窗口内关联:同一内容下短时间内发布的多条评论建边。
可以用 NetworkX 先构建图,方便检查,再转换为稀疏邻接矩阵。
import networkx as nx G = nx.Graph() for _, row in df.iterrows(): G.add_node(row["comment_id"]) # 规则1:同一用户 user_groups = df.groupby("user_id")["comment_id"].apply(list).to_dict() for ids in user_groups.values(): for i in range(len(ids)): for j in range(i + 1, len(ids)): G.add_edge(ids[i], ids[j]) # 规则2:文本相似度 from sklearn.metrics.pairwise import cosine_similarity sim = cosine_similarity(X) for i in range(len(df)): for j in range(i + 1, len(df)): if sim[i][j] > 0.8: G.add_edge(df.iloc[i]["comment_id"], df.iloc[j]["comment_id"])这段代码用于说明思路。数据量变大后,双层循环计算相似度会非常慢。实际项目应该使用近似最近邻算法(如 faiss)或先按用户、IP 分组缩减候选对。
3.4 构建邻接矩阵与归一化
把 NetworkX 的边转换成 scipy 稀疏矩阵,再加上自环,最后做对称归一化。
import numpy as np from scipy import sparse nodes = list(G.nodes()) node_index = {node: i for i, node in enumerate(nodes)} n = len(nodes) adj = nx.to_scipy_sparse_array(G, nodelist=nodes, dtype=np.float32) # 加自环 adj = adj + sparse.eye(n, dtype=np.float32) # 对称归一化:D^-1/2 * A * D^-1/2 degree = np.array(adj.sum(axis=1)).flatten() degree_inv_sqrt = 1.0 / np.sqrt(degree + 1e-8) degree_mat_inv_sqrt = sparse.diags(degree_inv_sqrt) adj_norm = degree_mat_inv_sqrt @ adj @ degree_mat_inv_sqrt注意:加自环的目的是让节点在聚合时保留自身信息。nx.to_scipy_sparse_array返回的矩阵要保证nodelist与特征矩阵的行顺序一致。节点顺序一旦错位,训练时特征和标签就会对不上。
4. GCN 模型实现与训练
4.1 用 PyTorch 实现一个 GCN 模块
为了减小安装复杂度,可以不依赖 PyTorch Geometric,直接用 PyTorch 实现两层 GCN。核心类如下:
import torch import torch.nn as nn import torch.nn.functional as F class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.weight = nn.Parameter(torch.randn(in_dim, out_dim)) def forward(self, x, adj_norm): # x: [N, in_dim] # adj_norm: [N, N] 稀疏或稠密矩阵 out = torch.mm(adj_norm, x) out = torch.mm(out, self.weight) return out class GCN(nn.Module): def __init__(self, in_dim, hidden_dim, out_dim, dropout=0.5): super().__init__() self.layer1 = GCNLayer(in_dim, hidden_dim) self.layer2 = GCNLayer(hidden_dim, out_dim) self.dropout = nn.Dropout(dropout) def forward(self, x, adj_norm): x = self.layer1(x, adj_norm) x = F.relu(x) x = self.dropout(x) x = self.layer2(x, adj_norm) return x这里adj_norm可以预先计算好,训练时直接传入。如果节点数量不大,直接使用稠密矩阵也可以。节点多时要把adj_norm转成 PyTorch 稀疏张量,减少内存占用。
4.2 训练流程
训练时只需要准备特征矩阵X、归一化邻接矩阵adj_norm和标签y,然后划分训练集和验证集。由于图节点之间存在依赖,随机划分会带来信息泄漏,训练集和验证集的节点仍然可能通过边相连。简单做法是先按时间或用户划分,保证训练集中出现的用户不会出现在验证集。
最小训练代码如下:
import torch.optim as optim model = GCN( in_dim=X.shape[1], hidden_dim=32, out_dim=2 ) optimizer = optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4) loss_fn = nn.CrossEntropyLoss(weight=torch.tensor([1.0, 3.0])) X_tensor = torch.tensor(X, dtype=torch.float32) adj_tensor = torch.tensor(adj_norm.toarray(), dtype=torch.float32) y_tensor = torch.tensor(df["label"].values, dtype=torch.long) train_mask = torch.tensor(df["split"].values == "train", dtype=torch.bool) val_mask = torch.tensor(df["split"].values == "val", dtype=torch.bool) for epoch in range(100): model.train() optimizer.zero_grad() output = model(X_tensor, adj_tensor) loss = loss_fn(output[train_mask], y_tensor[train_mask]) loss.backward() optimizer.step() if epoch % 10 == 0: model.eval() with torch.no_grad(): pred = output.argmax(dim=1) val_acc = (pred[val_mask] == y_tensor[val_mask]).float().mean() print(f"epoch {epoch}, loss {loss.item():.4f}, val_acc {val_acc.item():.4f}")垃圾评论样本通常偏少,直接用标准交叉熵可能导致模型倾向预测正常类。可以在CrossEntropyLoss中给垃圾评论类别更高的权重,比如上面示例中的[1.0, 3.0]。具体权重需要根据训练集统计得到。
4.3 模型评估与保存
只看准确率不够。垃圾评论场景中,如果垃圾评论只占 5%,模型把所有评论都判断为正常,准确率也有 95%,但毫无实用性。需要重点看:
- 精确率:预测为垃圾的样本里,真正是垃圾的比例。
- 召回率:真实垃圾评论里,被模型找出来的比例。
- F1:精确率和召回率的调和平均。
from sklearn.metrics import classification_report, f1_score model.eval() with torch.no_grad(): output = model(X_tensor, adj_tensor) pred = output.argmax(dim=1) print(classification_report(y_tensor[val_mask], pred[val_mask], target_names=["正常", "垃圾"]))训练完成后保存模型参数,同时保存特征器。模型参数只包含网络权重,不包含 TF-IDF 词典,因此两者必须一起保存。
torch.save(model.state_dict(), "gcn_model.pt") import joblib joblib.dump(vectorizer, "tfidf_vectorizer.joblib")更完善的方案是把模型版本、特征版本、图构建参数都写进元信息,避免线上加载时出现模型和特征不匹配。
4.4 模型预测的限制
GCN 默认是一个直推式模型。训练时模型只对图中已有的节点学习表示。当系统收到一条全新评论时,它不在原来的邻接矩阵里,不能直接套用训练好的adj_norm。
学习环境里,一个简单做法是在服务启动时加载训练图,预测前把新评论加入图,计算它与已有节点的边,重新执行一次前向传播,取新节点的输出。这个方案只适合小数据量,生产环境需要改用支持归纳学习的模型(如 GraphSAGE),或者周期性离线重建图并批量预测。后面会讲具体处理方式。
5. Flask 服务封装与接口设计
5.1 项目目录结构
建议按以下结构组织代码:
comment_gcn/ ├── app.py ├── model.py ├── graph_builder.py ├── train.py ├── requirements.txt ├── models/ │ ├── gcn_model.pt │ └── tfidf_vectorizer.joblib ├── data/ │ └── comments.csv └── templates/ └── index.htmltrain.py负责离线训练,graph_builder.py负责建图,model.py定义网络结构,app.py是 Flask 入口。
5.2 加载模型和词向量工具
Flask 服务启动时加载模型和 TF-IDF 向量器,避免每次请求都重新加载:
import joblib import torch from model import GCN device = torch.device("cpu") model = GCN(in_dim=512, hidden_dim=32, out_dim=2) model.load_state_dict(torch.load("models/gcn_model.pt", map_location=device)) model.to(device) model.eval() vectorizer = joblib.load("models/tfidf_vectorizer.joblib")in_dim必须与训练时一致。如果训练时设定了max_features=512,那么加载时也要使用同一个维度。
5.3 单条预测接口
为了让模型处理新评论,需要把新评论转换成特征,并临时与训练集中的相似评论建立图关系。下面是一个简化实现,核心是先在训练集文本里找相似评论,再构造一个小图。
import numpy as np from scipy import sparse from sklearn.metrics.pairwise import cosine_similarity def build_predict_graph(text, train_texts, train_labels, top_k=5): new_vec = vectorizer.transform([text]).toarray() train_vec = vectorizer.transform(train_texts).toarray() sim = cosine_similarity(new_vec, train_vec)[0] top_indices = np.argsort(sim)[::-1][:top_k] n = top_k + 1 row, col = [], [] # 新节点 0 与 top_k 个训练节点相连 for idx in top_indices: # 这里使用节点的临时编号:0 表示新评论,idx+1 表示训练评论 row.append(0) col.append(idx + 1) row.append(idx + 1) col.append(0) # 只有新评论有标签,训练节点不需要参与预测,但可以作为邻居特征输入 x = np.vstack([new_vec, train_vec[top_indices]]) adj = sparse.coo_matrix((np.ones(len(row)), (row, col)), shape=(n, n), dtype=np.float32) adj = adj + sparse.eye(n, dtype=np.float32) degree = np.array(adj.sum(axis=1)).flatten() degree_inv_sqrt = 1.0 / np.sqrt(degree + 1e-8) adj_norm = sparse.diags(degree_inv_sqrt) @ adj @ sparse.diags(degree_inv_sqrt) return torch.tensor(x, dtype=torch.float32), torch.tensor(adj_norm.toarray(), dtype=torch.float32)Flask 接口:
from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/predict", methods=["POST"]) def predict(): data = request.get_json(force=True) text = data.get("text", "").strip() if not text: return jsonify({"error": "text is required"}), 400 x, adj_norm = build_predict_graph(text, train_texts, train_labels) with torch.no_grad(): logits = model(x, adj_norm) prob = torch.softmax(logits, dim=1)[0] label = int(torch.argmax(prob).item()) score = float(prob[label]) return jsonify({ "text": text, "label": label, "is_spam": bool(label == 1), "score": score })这段代码把训练集评论文本放在内存中,只适用于小规模演示。真实系统如果训练集有几百万条,每次请求都要计算余弦相似度,性能会非常差。生产环境更推荐的做法是:
- 定期离线重建图并训练模型。
- 服务端保存最近一段时间的高危评论索引,用向量检索召回相似节点。
- 或者接入支持在线推理的 GraphSAGE 模型。
5.4 批量预测接口
批量接口适合后台审核任务。一次接收多条文本,对每一条执行相同逻辑:
@app.route("/predict_batch", methods=["POST"]) def predict_batch(): data = request.get_json(force=True) texts = data.get("texts", []) if not isinstance(texts, list) or len(texts) == 0: return jsonify({"error": "texts must be a non-empty list"}), 400 results = [] for text in texts: if not text: results.append({"text": text, "error": "empty text"}) continue x, adj_norm = build_predict_graph(text, train_texts, train_labels) with torch.no_grad(): logits = model(x, adj_norm) prob = torch.softmax(logits, dim=1)[0] label = int(torch.argmax(prob).item()) score = float(prob[label]) results.append({ "text": text, "label": label, "is_spam": bool(label == 1), "score": score }) return jsonify({"results": results})批量接口要注意控制单次请求的文本数量。文本过多会导致内存和响应时间直线上升,建议一次不超过 100 条。
5.5 命令行与页面调用
接口写好后,可以用 curl 快速验证:
curl -X POST http://127.0.0.1:5000/predict \ -H "Content-Type: application/json" \ -d '{"text": "加微信免费领取资料"}'也可以在 Flask 里返回一个简单 HTML 页面,方便交互演示。这里不展开前端逻辑,只说明templates/index.html里放一个表单,点击提交后请求/predict即可。
6. 运行验证与结果分析
6.1 启动服务
安装依赖后,先运行训练脚本:
python train.py训练脚本会生成models/gcn_model.pt和models/tfidf_vectorizer.joblib。然后启动 Flask:
python app.py正常情况会看到 Flask 启动日志,监听在127.0.0.1:5000。
6.2 调用验证
使用正常评论测试:
curl -X POST http://127.0.0.1:5000/predict \ -H "Content-Type: application/json" \ -d '{"text": "这篇文章很实用,学到了"}'使用垃圾评论测试:
curl -X POST http://127.0.0.1:5000/predict \ -H "Content-Type: application/json" \ -d '{"text": "加我微信领红包"}'6.3 预期输出
如果模型和数据正常,预计输出类似:
{ "text": "加我微信领红包", "label": 1, "is_spam": true, "score": 0.93 }正常评论更可能得到:
{ "text": "这篇文章很实用,学到了", "label": 0, "is_spam": false, "score": 0.87 }这里需要提醒:判断是否垃圾取决于训练数据分布、边定义和特征质量。如果训练数据里垃圾评论比例很低,且没有指定类别权重,模型可能把所有评论都预测为正常。验证时不能只看一两条例子,要在验证集上算整体指标。
6.4 在真实部署中怎么更新图
在线评论不断增长,图结构必须持续更新。学习环境可以手工重建图,生产环境需要设计定时任务:
- 每天或每小时从数据库拉取新增评论。
- 重新构建图,更新节点特征和邻接矩阵。
- 增量或全量训练 GCN。
- 评估后发布新模型文件。
- Flask 服务平滑加载新模型。
如果是低延迟在线预测,最好使用支持归纳学习的模型,或者把预测改为“特征 + 相似子图召回”,而不是每次请求都重建大图。
7. 常见问题排查
7.1 训练阶段常见问题
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 训练 loss 不下降 | 边太稀疏或特征没归一化 | 检查邻接矩阵是否有边,查看特征范围 | 调整建边阈值,增加相似度阈值边 |
| 所有预测都为正常类 | 数据不平衡,权重未设置 | 打印训练集类别统计 | 使用加权交叉熵或过采样 |
| 验证集指标很高,线上效果很差 | 节点划分泄漏 | 检查训练集和验证集是否存在共同用户 | 按时间或用户 ID 划分数据 |
| 安装 PyTorch Geometric 失败 | Python 或 CUDA 版本不匹配 | 查看 pip 安装日志 | 改用自实现 GCN,或安装对应 CPU 版本 |
7.2 Flask 部署阶段常见问题
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 接口返回 500 | 模型路径错误或 input 维度不匹配 | 看 Flask 控制台日志 | 确认 model 文件存在,特征维度与训练一致 |
| 每次请求都很慢 | 每次重建大图或重新加载模型 | 打印函数耗时 | 使用缓存,限制候选集大小 |
| 中文乱码 | 请求或文件编码不一致 | 检查返回的 JSON 和控制台 | 统一 UTF-8,请求头设置application/json; charset=utf-8 |
| 报错 shape mismatch | 新评论构建的邻接矩阵尺寸与模型输入不一致 | 打印 x 和 adj 的 shape | 保证新节点编号从 0 开始,邻居节点编号连续 |
7.3 预测结果异常
如果正常评论被频繁判为垃圾,优先检查训练数据中是否存在大量误标注。简单试一下把训练数据里的垃圾评论按用户聚类,看这些用户是否集中在少数账号。垃圾评论的图特征应该表现为:
- 同一用户节点之间边密集。
- 文本相似度高的节点聚成小团体。
- 这些团体的节点标签以垃圾为主。
如果发现垃圾评论在图上是完全孤立的节点,说明边规则没有捕捉到有效结构,模型退化成普通文本分类器。
8. 最佳实践与扩展方向
8.1 模型上线前检查清单
上线前至少确认以下内容:
- 训练数据和将要预测的数据分布是否一致。
- 是否已经按时间或用户划分验证集。
- 是否记录了模型版本、特征版本和图构建参数。
- 是否评估过精确率、召回率和 F1,而不只是准确率。
- 预测接口是否有超时、异常捕获和请求日志。
- 是否有对新评论的处理逻辑,而不是只能预测训练集中的节点。
- 是否考虑过数据偏差和标注噪声。
这套清单也适用于一般的图神经网络服务化项目。
8.2 关键参数选型
| 参数 | 作用 | 建议 |
|---|---|---|
| 节点特征维度 | 影响模型参数量和训练速度 | TF-IDF 场景可以先取 256 到 512 维 |
| GCN 层数 | 控制邻居聚合范围 | 通常 2 层足够,层数过多容易过平滑 |
| 隐藏维度 | 模型表达能力 | 32 到 128,根据数据量调整 |
| dropout | 防止过拟合 | 0.5 是比较常见的初始化 |
| 建边相似度阈值 | 决定图结构密度 | 过高会损失关系,过低会导致图太密,建议从 0.8 调起 |
| 类别权重 | 缓解样本不平衡 | 按训练集反比设置,或使用Focal Loss |
8.3 可扩展方向
有了最小版本之后,可以从以下方向继续推进:
一是换更合适的图模型。GraphSAGE 和 GAT 都比基础 GCN 更适合在线预测,GraphSAGE 能引入采样邻居机制,GAT 能给不同邻居分配不同注意力权重。
二是引入更多异构图信息。把用户节点、内容节点、IP 节点加入图,形成异构图形;这样能识别同一 IP 下不同用户抱团评论的行为。
三是和 Web 管理端结合。Flask 服务可以只做后端接口,前端使用 Vue 或简单管理系统,数据存储使用 MySQL。这套结构适合课程设计,也适合小团队内部工具。
四是增加人工审核交接。模型输出概率落在 0.4 到 0.8 之间时,不直接拦截,而是进入人工审核队列。这比设置单一阈值更安全。
五是持续迭代。垃圾评论会随时间变化,只训练一次是不够的。可以把被误判的样本收集起来,标注后作为下一轮训练数据。
实际项目里,最值得花时间的不是模型结构,而是数据标注和图边定义。把“谁在什么时间、用什么账号、发布了什么内容”这些信息组织好,GCN 才能发挥结构建模的优势。如果只把文本向量丢进模型,图结构反而会成为噪声来源。先跑通最小系统,再逐步增加关系维度,是更稳妥的路线。