简介:这是一套基于动态图神经网络的网络异常流量检测系统,面向计算机相关专业学生、毕业设计者及网络安全实践者,用于解决网络流量的动态建模与异常行为识别问题。资源不仅提供完整Python实现源码,还包含详尽注释、技术文档、预训练模型权重及多种配置与日志文件,可帮助读者理解动态图神经网络在安全检测中的实际落地流程,也可作为课程作业或学期项目的优质参考。压缩包共169个文件,以py源码和pyc编译文件为主,搭配pt模型、csv数据集、json配置、PDF文档等,整体大小约34.96MB,目录模块划分清晰。当前已有49人在线学习,参考价值与可复用性较强,适合需要快速搭建检测系统或借鉴优秀毕设代码结构的读者使用。
1. 动态图神经网络为什么能抓异常流量
网络流量检测的常见做法是把每个连接当成独立样本,用统计特征喂给随机森林或XGBoost。这类模型对已知攻击有效,一旦攻击者把流量伪装成正常会话,特征分布略微偏移,分类器就会失效。原因在于它忽略了流量之间的结构关系:一条攻击流量不是孤立的,它必然与某个源IP、目的端口、协议族产生关联,攻击行为会在图结构上留下痕迹。
动态图神经网络(Dynamic GNN)解决的是这个问题:把一段时间窗口内的流量构造成动态图,节点代表IP或端口,边代表通信行为,再用图卷积网络学习节点在时间轴上的演化规律。与静态GNN相比,动态GNN能捕捉连接频率、交互模式在时间上的突变,而这类突变恰好是扫描爆破、DDoS、蠕虫传播的典型特征。本项目采用RGCN(关系图卷积网络)配合One-Class SVM(OSVM)做异常判定,输出文件如rgcn-o-osvm.csv、rgcn-2018.csv、rgcn-sum-mean.csv分别对应不同聚合策略下的检测结果。这套实现适合计算机专业毕业设计、图神经网络课程项目,也适合想从"调包分类"转向"图建模"的工程师。
2. RGCN与One-Class SVM:动态图建模与异常判定原理
2.1 RGCN如何建模异构流量图
网络流量天然是异构图:源IP到目标IP的关系、IP到端口的关系、协议到流量的归属关系,这些边的语义不同,不能用同一套参数更新节点表示。RGCN在GCN基础上引入关系感知的聚合方式:
import torch import torch.nn as nn import torch.nn.functional as F class RGCNLayer(nn.Module): def __init__(self, in_dim, out_dim, num_relations, num_bases=-1): super().__init__() self.num_relations = num_relations if num_bases > 0: # 基分解:共享基矩阵,降低参数量 self.weight = nn.Parameter(torch.randn(num_bases, in_dim, out_dim)) self.coeff = nn.Parameter(torch.randn(num_relations, num_bases)) else: # 每个关系独立权重 self.weight = nn.Parameter(torch.randn(num_relations, in_dim, out_dim)) def forward(self, x, adj_list): # x: [num_nodes, in_dim] # adj_list: list of [num_nodes, num_nodes], 每个关系一个邻接矩阵 out = torch.zeros(x.size(0), self.weight.size(-1), device=x.device) for r, adj in enumerate(adj_list): w = self.weight[r] if self.weight.dim() == 3 else torch.einsum('b,boi->oi', self.coeff[:, r], self.weight) out = out + torch.spmm(adj, x @ w) return out这里num_relations对应流量图中边的类型数量。常见做法是把"源IP-目标IP""源IP-端口""IP-协议"作为三种关系,端口和协议节点与IP节点一起进图。基分解(num_bases)用于关系数多时防止过拟合,每个关系权重由少量基矩阵线性组合而成。
RGCN输出的是节点嵌入,但异常检测需要的是"图级"或"会话级"判定。项目里通过聚合函数把节点嵌入压成图级表示,rgcn-sum-mean.csv中的sum-mean指的就是先对节点嵌入按关系求和、再跨时间窗口取均值。聚合方式直接影响检测灵敏度:对扫描行为用sum保留强度信息,对缓慢探测用mean减少突发噪声干扰。
2.2 One-Class SVM在嵌入空间里圈定正常流量
OSVM是经典的单类分类器,核心思想是在特征空间中找一个超平面,把训练数据与原点尽可能分开,新样本落入超平面外侧即视为异常。它不像二分类SVM那样需要正负样本,适合异常流量这种"正常样本多、攻击样本少"的场景。
from sklearn.svm import OneClassSVM # X_train: 训练阶段提取的正常图嵌入向量 # X_test: 测试阶段提取的图嵌入向量 osvm = OneClassSVM(kernel='rbf', gamma='scale', nu=0.05) osvm.fit(X_train) y_pred = osvm.predict(X_test) # 1为正常,-1为异常参数nu控制训练数据中被判为异常的比例上限。在CIC2018数据集上,我建议先设0.02再逐步上调,因为该数据集的攻击流量占比通常小于5%,nu过大会把正常流量误伤。gamma='scale'会自动根据特征维度计算核带宽,比手动指定0.1或0.01更稳妥。
动态图模型和OSVM组合的合理性在于:RGCN学习到的嵌入空间把流量结构信息压缩为低维向量,OSVM在低维空间里做密度估计,比直接在原始特征上做距离判定的效果稳定得多。预训练模型正是用正常流量窗口的嵌入训练OSVM,后续推理时不再更新RGCN参数,只对新增窗口做前向传播。
3. Python实现:CIC2018与数据预处理完整流程
3.1 从CIC2018原始CSV到图结构的映射
项目根目录下的README.md列出了数据文件组织方式。CIC2018数据集每条记录包含80多个特征,包括流持续时间、包长度、标记等。直接把这些特征全塞进图会导致节点数量爆炸,所以需要做特征筛选与图抽取。
import pandas as pd import numpy as np def build_graph_from_csv(csv_path, time_window=60): df = pd.read_csv(csv_path) # 选择用于构图的核心特征 df = df[['Src IP', 'Dst IP', 'Src Port', 'Dst Port', 'Protocol', 'Timestamp', 'Label']] # 按时间窗口分组 df['Timestamp'] = pd.to_datetime(df['Timestamp']) df = df.sort_values('Timestamp') windows = df.groupby(pd.Grouper(key='Timestamp', freq=f'{time_window}s')) graphs = [] for ts, group in windows: if len(group) < 10: # 过滤过小窗口 continue # 节点ID映射:IP和端口作为独立节点类型 node_set = set(group['Src IP']).union(set(group['Dst IP'])) node_set = node_set.union(set(group['Src Port'])).union(set(group['Dst Port'])) node_ids = {n: i for i, n in enumerate(node_set)} edge_list = [] for _, row in group.iterrows(): src = node_ids[row['Src IP']] dst = node_ids[row['Dst IP']] rel_type = 0 # IP到IP edge_list.append((src, dst, rel_type)) # 源IP到端口关系 sp = node_ids[row['Src Port']] edge_list.append((src, sp, 1)) # 目标IP到端口关系 dp = node_ids[row['Dst Port']] edge_list.append((dst, dp, 2)) graphs.append((node_ids, edge_list)) return graphs时间窗口的长短直接决定图规模。CIC2018有数千万条记录,我用60秒窗口时每个窗口约5万条边,16GB内存可以跑;窗口缩到10秒,图数量增加但每个图变小,RGCN训练更快,但可能截断跨分钟的慢速攻击。毕业设计场景建议先跑60秒窗口,后续再对比30秒和120秒的检测指标。
uci.json存放的是UCI数据集相关的配置信息,用于做跨数据集的泛化验证。训练时用UR(UNSW-NB15)或CIC2018的良性流量构建OSVM模型,再在uci.json指定的测试集上评估,能说明动态图方法不止对单一数据集有效。
3.2 稀疏邻接矩阵与训练数据切分
RGCN在PyTorch里使用torch.sparse_coo_tensor存储邻接矩阵,避免大规模图的内存爆炸。上面的build_graph_from_csv返回的edge_list需要转换成scipy.sparse或PyTorch稀疏张量。
from scipy.sparse import coo_matrix def adjacency_to_sparse(edge_list, num_nodes): row = [e[0] for e in edge_list] col = [e[1] for e in edge_list] data = [1.0] * len(edge_list) adj = coo_matrix((data, (row, col)), shape=(num_nodes, num_nodes)) return adj注意这里只记录了有向边,但流量通信的双方是双向的,我会额外把反向边也加入矩阵,并对邻接矩阵做行归一化D^{-1}A。RGCN原论文中的正则化是D^{-1/2} A D^{-1/2},但动态流量图节点度数差异极大(热门端口被高频访问),行归一化更抑制热门节点的主导影响,推荐先试行归一化的版本。
训练/测试划分不能随机抽样,必须按时间切分。训练数据只能用早期时间段的正常流量,测试数据用后期时间段的完整流量(包含攻击)。cic2018train.log记录了训练曲线,包括每个epoch的loss、AUC和漏报率。如果训练日志中AUC在初期就超过0.98,先检查是不是数据泄漏——比如测试集的时间段和训练集重叠。
4. 模型训练与部署:从RGCN聚合到OSVM判定
4.1 RGCN训练流程与关键参数
RGCN在本项目里不是做节点分类,而是做图表示学习。训练目标可以是预测图中边的存在性,也可以直接优化下游OSVM的异常分离度。更稳妥的做法是用自监督的边预测任务预训练:
import torch.optim as optim def train_rgcn(model, graphs, epochs=50, lr=0.001): optimizer = optim.Adam(model.parameters(), lr=lr) for epoch in range(epochs): total_loss = 0.0 for graph in graphs: node_ids, edge_list = graph adj_list = build_relation_adjs(edge_list, len(node_ids)) embeddings = model(node_features, adj_list) # 负采样:随机破坏边的端点,与真实边对比 pos_score = (embeddings[src] * embeddings[dst]).sum(dim=1) neg_score = (embeddings[src] * embeddings[random_dst]).sum(dim=1) loss = F.margin_ranking_loss(pos_score, neg_score, target=torch.ones_like(pos_score), margin=0.5) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() print(f"Epoch {epoch}: loss={total_loss / len(graphs):.4f}")margin=0.5控制正负样本得分的最小间隔。图嵌入维度我采用64,太小表达不了三种关系的语义,太大在后续OSVM里容易过拟合。RGCN的层数不是越多越好,两层足以在IP、端口、协议间传播消息,三层以上会把不同攻击子图混在一起,反而降低区分度。
预训练完成后,用所有正常窗口的节点嵌入聚合出图级向量。项目提供三种聚合器的切换:sum对节点嵌入求和,mean取平均,attention用注意力权重加权。我测试下来的经验是:sum对高强度DDoS更敏感,mean对低速扫描更稳定,sum-mean(在窗口内求和、跨窗口求均值)的AUC通常最均衡,这也是rgcn-sum-mean.csv作为默认结果的原因。
4.2 One-Class SVM与评估指标
OSVM的输入是图级嵌入,但还有一个细节:动态图的时序性意味着第t个窗口的嵌入与第t-1个窗口相关。项目里把相邻窗口的差值h_t - h_{t-1}也拼进特征,即用"嵌入变化量"而非"嵌入绝对值"来做异常判定。
def train_osvm_with_temporal(embeddings): # embeddings: [num_windows, hidden_dim] delta = np.diff(embeddings, axis=0) features = np.hstack([embeddings[1:], delta]) clf = OneClassSVM(kernel='rbf', gamma='scale', nu=0.05) clf.fit(features) return clfrgcn-o-osvm.csv的列结构通常是timestamp,window_id,embedding_id,osvm_score,prediction。osvm_score是决策函数值,正数代表位于超平面内侧(正常),负数代表外侧(异常)。只看prediction(0/1)会丢失大量信息,实际部署时建议对osvm_score做滑动平均,平滑掉单窗口的抖动。
评估指标要留意:异常检测场景正样本极少,准确率没有参考价值。日志里记录的是AUC和F1,AUC关注排序能力,F1关注阈值选择。我一般先用AUC判断模型能力,再选择使得F1最大的阈值。开源代码里有cal_threshold脚本,它按OSVM得分从高到低扫描阈值,输出混淆矩阵中TPR与FPR之差最大的点。
4.3 模型序列化与推理接口
训练完成后需要把RGCN权重和OSVM对象打包。PyTorch和scikit-learn序列化方式不同,建议分开保存:
import joblib torch.save({ 'model_state_dict': rgcn.state_dict(), 'node_feature_dim': 64, 'hidden_dim': 64, }, 'rgcn_model.pth') joblib.dump(osvm_clf, 'osvm_clf.pkl')推理阶段不需要OSVM的反向传播,可以关闭梯度计算并把模型设为eval模式。launch.json是VS Code的调试配置,里面包含了"program": "detect.py"、"args": ["--input", "data/cic2018_test.csv", "--model_dir", "checkpoints/"]等启动参数。如果你在调试时发现断点进不去,检查launch.json里的justMyCode是否设为false,否则只会命中用户代码而跳过第三方库内部。
5. 部署细节与调优技巧
5.1 实时流式检测的实现方式
离线CSV检测不难,难的是把动态图更新做增量。项目里的detect.py支持两种模式:批处理和滑动窗口。滑动窗口模式下,新到达的流量不是全量重建图,而是维护一个deque,窗口时间到达即丢弃最老一批边。
from collections import deque class StreamingGraphUpdater: def __init__(self, window_size=60): self.window_size = window_size self.edge_deque = deque() self.node_counter = {} def add_batch(self, rows): current_ts = rows[-1]['Timestamp'] while self.edge_deque and (current_ts - self.edge_deque[0]['Timestamp']).seconds > self.window_size: old = self.edge_deque.popleft() self.remove_edge(old) self.edge_deque.extend(rows) # 重建稀疏邻接矩阵并做特征归一化 return self.build_adj()增量更新的好处是不需要重新运行build_graph_from_csv,CPU占用大幅降低。坏处是删除旧边时如果图结构变化剧烈,节点特征矩阵需要重新对齐。实际部署时我建议每5个窗口做一次全量重建,校验增量更新的嵌入与全量重建的误差,如果余弦相似度低于0.95,说明增量逻辑有bug或窗口内边数波动太大。
5.2 参数调优的三条经验
第一,nu参数不要固定。CIC2018训练集里良性流量占比约92%,nu=0.05对应的5%异常比例是合理的,但如果换到自建数据集,正常流量的行为模式更集中,nu可以降到0.01。观察训练后osvm_clf.score_samples()的分布,如果正常样本得分有大量集中在0附近,说明nu偏低。
第二,关系类型增加并非越多越好。试图把"TCP SYN包数量"作为第四种关系,反而会让多跳计算的特征被特定端口特征主导。我的做法是先跑三个基础关系,再用rgcn-2018.csv里记录的各关系权重占比判断是否需要细分。
第三,测试阶段的窗口大小与训练阶段必须一致。窗口60秒训练,部署时改成30秒,RGCN输出的节点嵌入统计分布会变,OSVM的超平面阈值全部失效。如果业务要求更快的响应,就重新训练OSVM,不要只调整推理端的窗口。
5.3 日志监控与失败排查
cic2018train.log是排查问题的第一入口。如果日志中loss在前10个epoch不下降,先检查node_features初始化是否全为零;如果AUC在0.5附近震荡,检查build_graph_from_csv中的Label列是否被错误包含进节点特征,导致信息泄漏。推理阶段出现CUDA内存不足,把detect.py中batch_size从1024降到256,因为动态图的邻接矩阵稀疏度随窗口内容变化,不能固定按峰值计算显存。
另外,项目根目录下的.gitignore已经忽略了*.pth、*.pkl和data/目录。如果你用Git做版本管理,训练好的模型文件不会入库,这是合理的。但要注意launch.json中如果使用了${workspaceFolder}变量,路径里带中文或空格时,VS Code的调试控制台会转义出错,最好把项目放在纯英文路径下运行。
最后分享一个验证部署是否正常的技巧:用curl模拟一条扫描流量,比如在1秒内对同一端口发起100次TCP连接,看检测接口是否返回异常标记。如果未触发,调低osvm_score的判定阈值再看,而不是重新训练模型,这样能快速区分是阈值得分问题还是图构建问题。
本文还有配套的精品资源,点击获取