
简介2020年本科毕业设计项目主题为基于机器学习的自组织网络鲁棒性研究面向人工智能、机器学习方向的本科生与入门研究者。项目针对无线传感器网络、Ad Hoc等自组织网络在故障与不确定环境下的稳定性问题结合监督学习和图神经网络方法建模并给出完整实验流程。资源共27个文件包含8个Python脚本模型定义、数据预处理、训练与对抗攻击分析、3个pkl模型权重、11张网络拓扑与攻击效果图、训练损失/精度记录及readme文档压缩包仅2.07MB结构清晰。已有121人学习下载。读者可获得可复现的毕业设计全流程从图自编码器GAE实现、随机与恶意攻击模拟到鲁棒性量化分析适合作为课程设计、论文复现或自组织网络智能优化课题的参考基线。1. 自组织网络的机器学习鲁棒性研究毕设做什么能解决什么问题自组织网络Self-Organizing NetworkSON是一类没有中心控制器的网络节点通过分布式协议自协调组网典型如移动自组网MANET、无线传感器网络和无人机蜂群。这类网络最大的痛点就是“不可控”节点随机移动、电量耗尽、链路被干扰每次节点失效都可能引起路由重建甚至网络分裂。传统鲁棒性设计依赖手工规则很难应对未知故障模式。把机器学习引入自组织网络的鲁棒性研究本质是让网络“学会”在异常发生前预判、发生时自救。这个毕设题目就是围绕“如何用机器学习预测和抵御自组织网络的各种扰动”展开。我当年做这个方向时把仿真平台搭起来、生成数据、训练模型、注入故障评估鲁棒性一共四步走。接下来我按这个顺序把完整路线和踩过的坑写清楚新手可以直接复现熟手也能在参数和边界上找到可讨论的地方。2. 构建仿真环境从NS-3到Python的数据生成闭环2.1 选型为什么优先用仿真而不是真机本科毕设做自组织网络最现实的问题是“没有硬件条件”。组建几十个节点的无线测试床设备成本和场地调试就能耗掉大半年。仿真就是标准解法。常用工具有NS-3、OMNeT以及Python生态里的networkx。我的建议是如果要精确模拟无线信道、802.11 MAC层协议、丢包和时延用NS-3如果只研究网络拓扑动态性、节点失效对连通性的影响用networkx生成拓扑、再用Python做机器学习特征完全够用而且后续衔接最顺。很多同学翻车就翻在“一上来就啃NS-3源码”结果数据没生成时间没了。我一般做法是“先小后大”先用networkx快速验证方法再迁移到NS-3做高保真复现这样两份结果互相印证论文更有说服力。如果决定用NS-3最小流程是# 安装NS-3以3.36为例源码编译 git clone https://gitlab.com/nsnam/ns-3-dev.git cd ns-3-dev ./ns3 configure --build-profiledebug --enable-examples --enable-tests ./ns3 build # 运行自带的wave消息示例验证安装 ./ns3 run wifi-80211n --visconfigure里的--enable-tests是为了跑单元测试确认环境没问题。真正做自组织网络仿真时要自己写scratch/目录下的C程序在里面配置节点数量、移动模型、信道参数。但NS-3的调试成本较高我建议第一版别用它。2.2 用networkx生成动态拓扑最小可运行代码下面是一段生成自组织网络动态拓扑的代码模拟了节点随机移动、链路按距离建立和断开的过程。你可以直接复制运行再根据课题改参数。import networkx as nx import numpy as np import pandas as pd np.random.seed(42) # 固定随机种子保证结果可复现 n_nodes 30 # 节点数 comm_range 50.0 # 通信半径米 time_steps 100 # 仿真步数 # 记录每个时刻的边集合和节点坐标 snapshots [] for t in range(time_steps): # 随机游走模型节点位置随时间小幅变化 if t 0: pos np.random.rand(n_nodes, 2) * 200 # 初始位置在200m×200m区域 else: pos pos np.random.normal(0, 2, size(n_nodes, 2)) # 每步移动2m左右 # 按欧氏距离判断是否可直接通信 G nx.Graph() G.add_nodes_from(range(n_nodes)) for i in range(n_nodes): for j in range(i1, n_nodes): dist np.linalg.norm(pos[i] - pos[j]) if dist comm_range: G.add_edge(i, j) snapshots.append((pos.copy(), G.copy()))这段代码的关键在于用“随机游走”模拟节点移动让拓扑每个时刻都不同且相邻时刻具有连续性。comm_range是核心参数半径越大网络越稠密鲁棒性表现自然越好研究鲁棒性时通常固定半径才能对比不同算法在相同拓扑下的表现。time_steps决定样本数量每步生成一个拓扑快照后续每个快照可以抽取多个特征样本。注意随机种子固定了这样别人复现实验时能得到完全相同的拓扑序列。提示pos在循环内被重新赋值Python 变量作用域没问题但如果你想保存每个时刻的位置必须用pos.copy()否则后面更新会覆盖历史记录。生成拓扑后最好先画出来看一眼确认不是所有节点堆在一起或过于分散。可以用下面这段代码输出每个时刻的平均度和最大连通子图比例这两个指标能告诉你网络是否处于“临界连通”区间。# 检查拓扑合理性 print(时刻, 平均度数, 最大连通子图占比) for t in range(0, time_steps, 10): pos_t, G_t snapshots[t] degrees [d for _, d in G_t.degree()] avg_deg np.mean(degrees) # 最大连通子图占总节点数比例 largest max(nx.connected_components(G_t), keylen) conn_ratio len(largest) / n_nodes print(f{t:3d}, {avg_deg:5.2f}, {conn_ratio:.2f})如果发现平均度数长期低于1说明网络处于碎片化状态模型很难学到有效规律反之高于5时网络过于稠密所有链路都稳定鲁棒性测试失去区分度。我一般会调整comm_range让初始网络平均度数在2~4之间这个区间正好对应自组织网络的典型稀疏场景。2.3 数据标注与特征提取把拓扑变成机器学习能用的样本有了拓扑快照下一步是提取特征并打上标签。自组织网络的鲁棒性研究通常有两种监督信号一种是指示“链路是否会在下一时刻断开”另一种是“节点是否在下一时刻成为孤立节点”。前者是链路预测问题后者是节点分类问题。我以链路质量预测为例特征包含链路两端节点的距离、相对移动速度、链路在上一时刻是否存活、两端节点的度数、以及节点剩余能量如果模拟能量的话。标签是下一时刻链路是否仍然连通。# 提取所有链路的特征和标签 feature_names [distance, speed, prev_alive, node_degree_u, node_degree_v] X [] y [] for t in range(time_steps - 1): pos_t, G_t snapshots[t] pos_t1, G_t1 snapshots[t1] # 考虑当前时刻的所有潜在链路实际存在或距离在2倍通信范围内 for u in range(n_nodes): for v in range(u1, n_nodes): dist np.linalg.norm(pos_t[u] - pos_t[v]) if dist comm_range * 2: continue # 特征当前距离 feat [dist] # 特征相对速度用两帧位置差计算 vel_u pos_t1[u] - pos_t[u] vel_v pos_t1[v] - pos_t[v] rel_speed np.linalg.norm(vel_u - vel_v) feat.append(rel_speed) # 特征当前链路是否存活 feat.append(1 if G_t.has_edge(u, v) else 0) # 特征两端节点度数 feat.append(G_t.degree(u)) feat.append(G_t.degree(v)) # 标签下一时刻是否连通 label 1 if G_t1.has_edge(u, v) else 0 X.append(feat) y.append(label) X np.array(X) y np.array(y) print(f样本数: {len(X)}, 正样本占比: {y.mean():.2f})特征不是越多越好但这几个都是自组织网络中链路存活最直接的影响因素。prev_alive尤其重要因为链路有短暂记忆性上时刻连着下时刻大概率还连着。如果你去掉这个特征模型很难达到高准确率。反过来如果把当前时刻的距离也当成预测依据那模型可能会偷看标签因为下一时刻距离小于半径就等价于连通。所以这里只用了当前时刻的特征标签来自下一时刻的实际连通状态。样本不平衡是常态网络较稀疏时不连通的链路占多数。打印正样本占比是为了提醒你后续处理。如果占比低于0.1就要考虑欠采样或改用AUC作为评估指标而不是盲目看准确率。最后把特征数据集保存到本地方便后续训练复用。推荐用np.savez或pandas.DataFrame.to_parquet。存成CSV也可以但10万级样本会很大读取慢。# 保存特征与标签 np.savez(son_dataset.npz, XX, yy, feature_namesfeature_names) # 读取时用 np.load(son_dataset.npz) 即可3. 训练机器学习模型从随机森林到LSTM如何选择和调参3.1 问题建模分类还是回归以及为什么自组织网络的鲁棒性研究机器学习可以扮演两种角色一种是“感知型”如预测链路状态、节点剩余寿命另一种是“决策型”根据网络状态输出路由权重、传输功率。感知型通常是二分类或回归问题决策型更接近强化学习。本科毕设更适合做感知型因为数据容易生成、评估指标直观并且能跟传统协议对比。如果你想挑战决策型注意需要构建闭环仿真环境训练成本大很多不确定性也多。我见过不少毕设题目写着深度强化学习最后只跑通了一个玩具环境连鲁棒性分析都没法做。所以我的建议是除非你已有丰富仿真经验否则第一版先做二分类让模型判断链路是否会在下一时刻断开再基于预测结果做拓扑重构。3.2 时间序列数据与特征泄漏数据划分的学问自组织网络的数据带时间顺序如果随机打乱后按普通交叉验证划分就会造成“模型从未来时间偷看过去”的泄漏。正确做法是按时间顺序划分前70%的时间步作训练后30%作测试。而且训练集和测试集之间要留一个隔离带比如隔5个时间步让相邻帧的强相关性不至于让测试集被“记忆”住。这一点很容易踩坑。下面代码展示了正确的时序划分。# X是按时间顺序堆叠的需要先恢复时间步索引 n_time time_steps - 1 # 每个时间步产生的潜在链路数不一定相同如果用前面代码则相同 # 因为遍历节点时范围固定。此处假设相同。 samples_per_time len(X) // n_time time_idx np.repeat(np.arange(n_time), samples_per_time) # 按时间顺序划分前70%训练后30%测试中间隔5个时间步 split_time int(n_time * 0.7) train_mask time_idx split_time test_mask time_idx split_time 5 X_train, y_train X[train_mask], y[train_mask] X_test, y_test X[test_mask], y[test_mask] print(f训练集时间步: 0~{split_time-1}, 测试集时间步: {split_time5}~{n_time-1})这里X的行必须保持与时间步的对应关系。如果你在前面顺手调用了np.random.shuffle顺序就乱了后面的时序划分全废。一旦用train_test_split默认随机分割评估结果会虚高没有学术价值。隔离带取5个时间步是经验值如果时间步总数是100可以接受如果生成的是连续长时间序列隔离带还可以加大到10。如果你觉得单次划分不够稳可以用“时序交叉验证”做多轮评估from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5, gap5) for fold, (train_idx, val_idx) in enumerate(tscv.split(X)): X_tr, X_val X[train_idx], X[val_idx] y_tr, y_val y[train_idx], y[val_idx] # 每一折的验证集都在训练集之后不会泄漏TimeSeriesSplit是sklearn里专门处理有序数据的划分器gap就是隔离带。注意它要求样本本身是按时间排序的所以不能再混入打乱操作。3.3 以随机森林为基底模型训练、调参与评估随机森林是机器学习入门最常用的算法之一对异常值和噪声容忍度高不需要大量调参适合作为基线模型。如果随机森林的结果已经能满足需求你就有了一个最低线后续换LSTM或Transformer才有对比意义。from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, roc_auc_score, precision_recall_fscore_support # 基础模型 rf RandomForestClassifier(n_estimators200, max_depth10, min_samples_leaf2, random_state42, n_jobs-1) rf.fit(X_train, y_train) # 预测 y_pred rf.predict(X_test) y_prob rf.predict_proba(X_test)[:, 1] # 指标 acc accuracy_score(y_test, y_pred) auc roc_auc_score(y_test, y_prob) precision, recall, f1, _ precision_recall_fscore_support(y_test, y_pred, averagebinary) print(fAccuracy{acc:.3f}, AUC{auc:.3f}, F1{f1:.3f})参数方面n_estimators从200起步太大在样本量几十万时训练慢max_depth限制单棵树深度防止极端情况下过拟合min_samples_leaf设为2~5能抑制噪声自组织网络数据噪声多建议别设成1。如果不平衡明显可以在初始化时加class_weightbalanced。调参优先级是先调max_depth再调min_samples_leaf最后看n_estimators。用网格搜索时不要一次性全空间穷举先粗后细。例如from sklearn.model_selection import GridSearchCV param_grid { max_depth: [6, 10, 15], min_samples_leaf: [2, 5, 10], } grid GridSearchCV(RandomForestClassifier(n_estimators200, random_state42, n_jobs-1), param_grid, cvtscv, scoringroc_auc) grid.fit(X, y) print(grid.best_params_)这里cvtscv继续用时间序列划分而不是普通K折保证每一折都不泄漏。scoringroc_auc对不平衡更友好。3.4 进阶模型用LSTM捕捉链路状态的时间连续性随机森林忽略了一个重要信息链路存活状态在时间上是连续演变的。如果你想捕捉这种时间依赖可以用LSTM把连续几个时刻的特征拼接成时间窗口再训练。以滑动窗口为例import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout # 把原始样本按时间窗口组织窗口长度5 def make_windows(X, y, time_idx, window_size5): X_w, y_w [], [] # X按时间顺序排列这里假设time_idx单调递增 for i in range(len(X)): t time_idx[i] # 找过去window_size个时间步的样本同一链路这里简化实际需要按链路ID配对 # 暂用相同时间窗口内所有样本的平均特征代替仅示意 if t window_size: mask (time_idx t - window_size 1) (time_idx t) window_feat X[mask].mean(axis0) X_w.append(window_feat) y_w.append(y[i]) return np.array(X_w), np.array(y_w) X_tr_w, y_tr_w make_windows(X_train, y_train, time_idx[train_mask], window_size5) X_te_w, y_te_w make_windows(X_test, y_test, time_idx[test_mask], window_size5) model Sequential([ LSTM(32, input_shape(5, X_train.shape[1]), return_sequencesFalse), Dropout(0.2), Dense(16, activationrelu), Dense(1, activationsigmoid) ]) model.compile(optimizeradam, lossbinary_crossentropy, metrics[AUC]) model.fit(X_tr_w, y_tr_w, epochs10, batch_size64, validation_data(X_te_w, y_te_w), verbose0)注意上面代码为了演示做了简化实际做窗口时需要把同一条链路在不同时刻的样本按链路ID组织成序列而不是对所有样本做平均。否则LSTM学到的是“全局平均状态”失去了链路维度信息。正确的做法是构造一个三维数组[链路ID, 时间步, 特征维度]这需要你记录每行样本属于哪条链路比如用端点ID拼接成链路ID。我在第一次做这个实验时偷懒用了平均法结果LSTM比随机森林还差后来才意识到是数据结构的问题。3.5 模型对比表什么时候用随机森林什么时候用LSTM模型优点缺点适用场景随机森林训练快、可解释性强、对不平衡鲁棒无法直接利用时间连续性节点规模大、特征维度高、快速出基线LSTM能捕捉链路状态的时变规律训练慢、需要足够样本、超参多时间步长数千以上、链路变化有明确前后依赖逻辑回归最简单、可解释无法处理非线性做传统基线对比如果样本量低于1万我不建议上LSTM。自组织网络的拓扑变化如果完全随机LSTM也学不到东西。你可以先用随机森林得到一个F1然后针对“预测错误”的样本分析看看那些样本是否都在节点高速移动的状态如果确实如此说明需要增加特征而非换模型。4. 鲁棒性评估与避坑指南故障注入、指标解读和常见问题4.1 鲁棒性指标连通度、丢包率和服务降级比例机器学习模型训练完之后要回答的第一个问题是它提升的到底是什么鲁棒性是网络系统层面的性质不能只看分类F1。我建议把评估拆成三个层面网络连通度、端到端丢包率、服务降级比例。连通度是指节点故障后最大连通子图包含的节点数占全网络节点的比例。丢包率是随机选择一批源-目的对按最短路径路由在故障注入后计算成功到达的比例。服务降级比例是故障后吞吐量或时延相对无故障时的退化倍数。这些指标能直观反映“网络抗不抗揍”。如果你只把模型F1做高但端到端指标没有显著改善那机器学习模型就没有落地价值。4.2 故障注入与蒙特卡洛模拟评估鲁棒性的标准做法评估鲁棒性不能只做一次实验因为自组织网络的故障是随机的。常见做法是蒙特卡洛模拟随机移除一定比例的节点或让节点按概率随机失效重复数百次统计指标的分布。下面代码展示了以“链路预测模型辅助拓扑重构”的鲁棒性评估流程。def evaluate_robustness_with_model(model, snapshots, comm_range, prob_threshold0.5, fail_ratio0.1, trials200): rng np.random.default_rng(2024) conn_ratios [] # 取最后一时刻的拓扑作为基准 pos, G snapshots[-1] nodes np.array(list(G.nodes())) for _ in range(trials): # 随机选择故障节点 n_fail int(len(nodes) * fail_ratio) failed set(rng.choice(nodes, sizen_fail, replaceFalse)) # 对剩余节点对用模型预测链路存活 G_pred nx.Graph() active_nodes [n for n in nodes if n not in failed] G_pred.add_nodes_from(active_nodes) for i_idx, u in enumerate(active_nodes): for v in active_nodes[i_idx1:]: dist np.linalg.norm(pos[u] - pos[v]) if dist comm_range * 2: continue # 构造特征与训练时一致速度和prev_alive需要计算 # 这里为演示直接使用距离特征和上一时刻实际存活需要历史信息 prev_alive 1 if G.has_edge(u, v) else 0 feat np.array([dist, 0.0, prev_alive, G.degree(u), G.degree(v)]).reshape(1, -1) prob model.predict_proba(feat)[0][1] if prob prob_threshold: G_pred.add_edge(u, v) if len(G_pred) 0: conn_ratios.append(0.0) else: largest max(nx.connected_components(G_pred), keylen) conn_ratios.append(len(largest) / len(nodes)) return np.mean(conn_ratios), np.std(conn_ratios) mean_conn, std_conn evaluate_robustness_with_model(rf, snapshots, comm_range, prob_threshold0.5, fail_ratio0.1) print(f10%节点故障后的平均连通度: {mean_conn:.3f} ± {std_conn:.3f})这段代码里我为了演示简化了特征构造实际做的时候要把相对速度、度数等全部按当前快照和前一帧计算否则特征分布与训练时不一致模型输出没有意义。建议把特征提取逻辑封装成函数extract_link_features(pos_now, pos_prev, G_now, G_prev, u, v)训练和评估都调它避免两处实现不一致。4.3 避坑自组织网络机器学习鲁棒性研究的五个常见问题第一特征泄漏导致性能虚高。现象是训练的模型在测试集上表现接近完美但真正仿真时不如阈值方法。原因是把下一时刻的标签信息混进了当前时刻特征比如把“未来距离”算进了当前特征。解决方法是严格按3.2的时间序列划分并保证每个特征最大只用到当前时刻以及之前的信息。我在做项目时习惯写一张“特征可用时间表”逐条核对。第二随机种子不一致导致结果无法复现。现象是同一个模型在不同机器上跑出来的指标差异很大。原因是所有随机源没有固定networkx的位置生成、numpy随机游动、sklearn模型初始化、蒙特卡洛采样。解决方法是把所有种子集中在配置区用np.random.default_rng生成独立随机数生成器传给每个模块。第三故障注入与模型预测的耦合错误。现象是评估时直接从真实拓扑删节点却没有重新计算模型预测的链路存活导致评估结果与模型无关。原因是对“预测”和“评估”两阶段的理解不清晰。解决方法是故障注入后对剩余节点间的每条潜在链路调用模型预测概率再按阈值重构网络。第四样本不平衡造成模型“全预测失败”。现象是F1低到让人怀疑人生或模型只预测多数类。原因是网络稀疏时正样本存活链路很少。解决方法是使用class_weightbalanced或RandomOverSampler但更推荐用AUC来评估排序能力因为AUC对不平衡不敏感。在论文里最好同时报告F1和AUC单独报Accuracy没有意义。第五忽略时间相关性导致训练集与测试集高度重叠。现象是测试集准确率比验证集还高一大截。原因是随机切分把同一链路不同时刻的样本分到两边模型直接“记忆”了链路ID。解决方法是按时间步切分并让测试集在时间上完全晚于训练集同时去掉隔离带。注意前两条是最伤元气的一旦出现整个毕设结论都得重做。建议每跑完一轮实验就把随机种子和特征边界记录下来否则后期返工成本极高。5. 把毕设做深与基线协议对比并用SHAP解释模型5.1 用传统协议作为鲁棒性对比基线自组织网络的经典路由协议有AODV和OLSR。你可以把“基于机器学习预测链路存活并重构拓扑”的路由策略与不使用预测的原始拓扑路由放在同一个故障场景下对比端到端丢包率。注意保持完全相同的拓扑序列、故障序列和流量模型。我常用做法是写一个统一实验函数接收策略参数并返回指标字典这样对比起来不会因为脚本差异产生额外偏差。如果模型预测能把连通度从0.8提到0.9同时丢包率下降30%那导师基本没话说。5.2 用SHAP解释模型让导师在答辩时点头只用准确率在本科答辩里是不够的你需要解释“为什么机器学习有效”。SHAP可以给出每个特征对预测结果的贡献方向。比如你可能会发现“上一时刻链路存活”的贡献远大于其他特征那就说明模型学到的只是短时记忆。为了证明模型确实捕获了距离、速度的影响可以分别计算去掉该特征前后的性能变化并画SHAP依赖图。import shap explainer shap.TreeExplainer(rf) shap_values explainer.shap_values(X_test[:500]) shap.summary_plot(shap_values, X_test[:500], feature_namesfeature_names)如果正样本不平衡严重建议在测试集里平衡抽样后再画SHAP否则图形没有区分度。SHAP依赖图在自组织网络场景里特别有价值你可以观察到距离在短距离时有正贡献超过某个阈值后贡献变负这正好对应无线信道的临界效应。把这张图放进论文能把“跑了个模型”升格为“用机器学习揭示了网络规律”。5.3 保存模型与重现场景给未来的自己留后悔药毕设最后一周最容易遇到的问题不是算法而是找不到当初的配置。我习惯把模型参数、特征名称、随机种子、训练集时间范围保存成JSON模型用joblib保存仿真拓扑快照用pickle保存放在同一目录。这样三个月后再打开项目也能用一句rf joblib.load(model.joblib)恢复。我当年因为没保存特征名称补实验时被迫重训模型教训很深。另一个建议是把整个流程封装成main.py支持命令行传参python main.py --fail-ratio 0.1 --seed 2024 --model random_forest调试时先跑小规模10个节点、50个时间步确认逻辑正确后再跑大规格。我踩过最惨的坑是直接跑1000个节点代码里有个距离计算写了三层循环一晚上没出结果。后来改用numpy广播和KDTree才把时间压下来。如果你能做到以上几点——数据生成可复现、时序划分合理、故障评估严谨、模型可解释、脚本可回放——这个毕设不仅合格还能作为你后续申请研究生或求职的独立项目。自组织网络和机器学习的结合方向至今依然活跃但本科阶段的重点不是发明新算法而是把整个研究闭环做扎实。这些血泪经验希望帮到你少走弯路。本文还有配套的精品资源点击获取