简介:本资源为基于机器学习的认知无线电频谱感知MATLAB仿真资料包,面向计算机、电子信息工程、数学等专业的大学生,适用于课程设计、期末大作业与毕业设计场景。包内共22个文件,以csv数据集、ipynb交互式代码、py脚本、pdf报告与md说明为主,涵盖训练与测试样本、信噪比标签及多种算法实现,压缩包约14.78MB,结构清晰便于按模块查阅。内容围绕能量检测、匹配滤波器检测与循环平稳特性检测等经典方法,并给出SVM、KNN、决策树等机器学习算法的对比实验,配套数据可直接运行,参数化编程便于调整。已有52人学习下载,适合希望快速理解频谱感知流程、复现算法对比并完成仿真报告的学习者参考。
1. 频谱感知为什么成了认知无线电里最容易翻车的一环
认知无线电要做的第一件事不是通信,而是"听"——听当前频段上有没有主用户(Primary User, PU)在占用。这个"听"的动作就是频谱感知。传统做法靠能量检测:设定一个门限,接收信号能量超过门限就判定频段被占用。听起来简单,但实际部署里,噪声功率随温度、器件漂移、环境变化而波动,固定门限在低信噪比下要么虚警率飙升,要么漏检严重。我在一次外场测试里见过能量检测在 SNR 低于 -8 dB 时几乎完全失效,判决结果跟抛硬币差不多。
机器学习切入这个问题的逻辑很直接:把频谱感知从"设一个门限"变成"学一个判决边界"。输入可以是接收信号的协方差矩阵特征值、循环谱特征、功率谱密度向量,输出是"占用/空闲"二分类。相比能量检测,基于机器学习的方法在低 SNR 区间能多出 3~8 dB 的检测增益,而且对噪声不确定性有更好的鲁棒性。这个方向适合做无线通信、信号处理、嵌入式感知的工程师,也适合想找一个"机器学习落地到物理层"练手项目的人。下面从特征工程讲到模型训练再到部署验证,把这条链路走通。
2. 从接收信号到特征向量:频谱感知的数据管线怎么搭
2.1 为什么不能直接把 IQ 采样丢给分类器
最直觉的做法是把接收到的 IQ 采样序列直接展平成一个长向量,喂给 SVM 或神经网络。我试过这条路,结论是:在样本量有限的情况下,直接展平 IQ 几乎必然过拟合。原因有两个。第一,IQ 采样点之间的时序相关性极强,展平后维度可能上千,但有效自由度远低于维度,分类器学到的判别方向不稳定。第二,频谱感知的核心判别信息不在单个采样点的幅度里,而在信号的统计结构里——比如循环平稳特征、协方差矩阵的特征值分布、功率谱的形状。
常见做法是先做特征提取,把高维 IQ 压成几十维的统计特征向量。我一般会用以下几类特征:
- 能量特征:接收信号的平均功率、方差。这是最基础的,但在低 SNR 下区分度有限。
- 协方差矩阵特征值:对接收信号做滑窗,计算协方差矩阵,取特征值。主用户存在时,最大特征值与最小特征值的比值(特征值比)会显著偏离 1。
- 循环谱特征:通信信号通常具有循环平稳性,在特定循环频率处有峰值。噪声不具备这个性质,所以循环谱特征在低 SNR 下区分度很好。
- 功率谱密度向量:对信号做 FFT 后取功率谱,降采样成固定长度的向量。
这些特征可以拼接成一个特征向量,维度控制在 20~50 之间。维度太低会丢信息,太高则样本需求急剧上升。
2.2 用 Python 搭一条可复现的特征提取管线
下面这段代码模拟了从接收信号到特征向量的完整流程。假设你已经有了接收信号的 IQ 采样,采样率 1 MHz,每次感知窗口 1024 个采样点。
import numpy as np from scipy.linalg import eigvalsh from scipy.signal import welch def extract_features(iq_samples, fs=1e6, window_len=1024): """ 从IQ采样中提取频谱感知特征向量。 iq_samples: 复数数组,长度 >= window_len fs: 采样率 window_len: 协方差估计的滑窗长度 返回: 特征向量 (n_features,) """ # 截取有效窗口 x = iq_samples[:window_len] # --- 特征1: 能量特征 --- power = np.mean(np.abs(x)**2) var = np.var(np.abs(x)) # --- 特征2: 协方差矩阵特征值比 --- # 构造滑窗协方差矩阵 (简化: 用延迟协方差) L = 8 # 平滑因子 R = np.zeros((L, L), dtype=complex) for i in range(L): for j in range(L): R[i, j] = np.mean(x[i:window_len-L+i] * np.conj(x[j:window_len-L+j])) eigenvalues = eigvalsh(R) eigenvalues = np.sort(eigenvalues)[::-1] # 降序 ev_ratio = eigenvalues[0] / (eigenvalues[-1] + 1e-12) ev_spread = eigenvalues[0] / (np.mean(eigenvalues) + 1e-12) # --- 特征3: 功率谱密度向量 (降采样到16维) --- f, psd = welch(x, fs=fs, nperseg=256) psd_ds = np.interp(np.linspace(0, len(psd)-1, 16), np.arange(len(psd)), psd) psd_ds = psd_ds / (np.sum(psd_ds) + 1e-12) # 归一化 # --- 特征4: 循环谱简化特征 --- # 用自相关函数的周期性作为代理 acf = np.correlate(x, x, mode='full') acf = acf[len(acf)//2:] acf = np.abs(acf[:64]) acf = acf / (acf[0] + 1e-12) # 取前几个峰值位置作为特征 peaks = [] for k in range(1, len(acf)-1): if acf[k] > acf[k-1] and acf[k] > acf[k+1] and acf[k] > 0.1: peaks.append(k) peak_feat = [len(peaks), peaks[0] if peaks else 0, peaks[1] if len(peaks) > 1 else 0] # 拼接 feat = np.concatenate([ [power, var, ev_ratio, ev_spread], psd_ds, peak_feat ]) return feat.astype(np.float32)这段代码的逻辑说明:能量特征和方差是最粗粒度的判别依据,计算量最小。协方差矩阵特征值比是频谱感知里的经典方法,主用户信号存在时,信号分量会让最大特征值明显高于噪声特征值。功率谱密度向量提供了频域形状信息,归一化后消除了绝对功率的影响,让分类器更关注谱形而非幅度。循环谱特征这里用自相关函数的峰值做了简化代理,实际项目中如果有条件,建议用专门的循环谱估计算法。
参数方面,window_len取 1024 是在 1 MHz 采样率下约 1 ms 的感知窗口,这个时长在多数认知无线电标准里是合理的。L=8是协方差矩阵的维度,太小则特征值分布不稳定,太大则计算量上升且需要更多采样点。psd_ds降到 16 维是经验值,再高会让特征向量维度膨胀。
2.3 数据集怎么造:仿真和实测的取舍
频谱感知的公开数据集不多,常见做法是自己造。仿真数据用 MATLAB 或 Python 生成:主用户信号可以用 QPSK、OFDM 或 chirp 信号,噪声用复高斯白噪声,通过调整发射功率来控制 SNR。每个 SNR 点生成几千个样本,标签是"占用/空闲"。
实测数据更真实但成本高。我一般会用一个 USRP 或类似软件无线电平台,一个天线收主用户信号,另一个收噪声底。实测数据的坑在于:标签对齐困难,主用户信号的出现和消失时刻需要精确同步;而且实测噪声不是白的,有色噪声会让仿真训练的模型性能下降。
提示:如果只是验证算法可行性,仿真数据足够了。但如果要写论文或做部署评估,至少需要一组实测数据来验证泛化性。
3. 分类器选型:SVM、随机森林还是轻量神经网络
3.1 各模型在频谱感知里的真实表现
频谱感知的分类任务有几个特点:样本量通常不大(几千到几万)、特征维度中等(20~50)、对推理延迟敏感(认知无线电要求实时判决)。基于这些约束,我把常见模型的适用性列一下:
| 模型 | 低SNR检测概率 | 训练样本需求 | 推理延迟 | 部署难度 |
|---|---|---|---|---|
| SVM (RBF核) | 中等 | 中等 | 低 | 低 |
| 随机森林 | 中等 | 中等 | 低 | 低 |
| KNN | 较差 | 高 | 高 | 低 |
| 轻量CNN (1D) | 较好 | 高 | 中等 | 中等 |
| LSTM | 较好 | 很高 | 高 | 高 |
SVM 在特征维度不高时表现稳定,RBF 核能处理非线性边界,而且支持向量机的决策函数只依赖支持向量,推理时计算量可控。随机森林的优势在于对特征尺度不敏感,不需要归一化,而且能输出特征重要性,方便做特征筛选。轻量 CNN 适合直接处理功率谱向量或协方差矩阵,但需要更多样本。
我一般会先用随机森林做基线,因为它的调参成本最低,而且特征重要性输出能告诉我哪些特征在起作用。如果随机森林的检测概率不够,再上 SVM 调核函数和惩罚系数。神经网络只在样本量足够(每个 SNR 点至少 5000 样本)时才考虑。
3.2 训练流程与关键参数设置
下面是一个用随机森林做频谱感知分类的完整训练脚本。假设你已经用上一节的extract_features生成了特征矩阵X和标签y。
import numpy as np from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split, StratifiedKFold from sklearn.metrics import roc_auc_score, confusion_matrix from sklearn.preprocessing import StandardScaler # 假设 X: (n_samples, n_features), y: (n_samples,) 0=空闲, 1=占用 # 按SNR分层划分,确保每个SNR区间都有训练和测试样本 # 这里假设你有一个snr_labels数组标记每个样本的SNR # snr_labels = ... # 标准化 (随机森林其实不需要,但SVM需要,统一处理) scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 分层K折交叉验证 skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) auc_scores = [] for train_idx, val_idx in skf.split(X_scaled, y): X_train, X_val = X_scaled[train_idx], X_scaled[val_idx] y_train, y_val = y[train_idx], y[val_idx] clf = RandomForestClassifier( n_estimators=200, # 树的数量,200在多数场景够用 max_depth=15, # 限制深度防止过拟合 min_samples_leaf=5, # 叶节点最少样本,控制平滑度 max_features='sqrt', # 每次分裂考虑sqrt(n_features)个特征 class_weight='balanced', # 类别不平衡时自动加权 random_state=42, n_jobs=-1 ) clf.fit(X_train, y_train) y_prob = clf.predict_proba(X_val)[:, 1] auc = roc_auc_score(y_val, y_prob) auc_scores.append(auc) print(f"5折AUC: {np.mean(auc_scores):.4f} +/- {np.std(auc_scores):.4f}") # 用全部数据训练最终模型 final_clf = RandomForestClassifier( n_estimators=200, max_depth=15, min_samples_leaf=5, max_features='sqrt', class_weight='balanced', random_state=42, n_jobs=-1 ) final_clf.fit(X_scaled, y) # 输出特征重要性 importances = final_clf.feature_importances_ top_k = np.argsort(importances)[::-1][:10] print("Top-10 重要特征索引:", top_k) print("对应重要性:", importances[top_k])逻辑说明:分层 K 折确保每个 SNR 区间的样本在训练和验证集中都有代表,避免模型只在某个 SNR 段表现好。class_weight='balanced'在频谱感知里很重要,因为实际场景中空闲频段的样本远多于占用频段,不加权的话分类器会偏向预测"空闲"。max_depth=15和min_samples_leaf=5是控制过拟合的关键参数,频谱感知的特征向量维度不高,树太深容易记住噪声。
参数调整建议:n_estimators从 100 开始试,到 300 以后收益递减。max_features用'sqrt'是分类任务的默认推荐,如果特征之间相关性很强,可以试0.3或0.5。min_samples_leaf在样本量小于 5000 时建议不低于 3,否则叶节点样本太少,概率估计不稳定。
3.3 检测概率与虚警率的权衡怎么调
频谱感知的评估指标不是准确率,而是检测概率(Pd)和虚警率(Pfa)。准确率在类别不平衡时会骗人——如果 90% 样本是空闲,全预测空闲就有 90% 准确率,但检测概率为 0。
调整 Pd 和 Pfa 的权衡,最直接的手段是改判决门限。分类器输出的是概率,你可以选一个阈值 t,概率大于 t 判为占用。t 降低,Pd 上升但 Pfa 也上升;t 升高则相反。实际操作中,先定一个可接受的 Pfa(比如 0.1),然后在这个约束下最大化 Pd。
from sklearn.metrics import roc_curve y_prob = final_clf.predict_proba(X_scaled)[:, 1] fpr, tpr, thresholds = roc_curve(y, y_prob) # 找Pfa<=0.1时最大的Pd target_pfa = 0.1 valid_idx = np.where(fpr <= target_pfa)[0] best_idx = valid_idx[np.argmax(tpr[valid_idx])] print(f"Pfa={fpr[best_idx]:.3f}, Pd={tpr[best_idx]:.3f}, 阈值={thresholds[best_idx]:.3f}")这段代码帮你找到在虚警率约束下的最优工作点。实际部署时,这个阈值需要根据现场噪声水平做在线校准,因为训练集的噪声分布和部署环境可能有偏移。
4. 避坑与排查:频谱感知落地时最容易踩的五个坑
4.1 坑一:训练集和测试集来自同一段信号,AUC虚高
现象:交叉验证 AUC 0.98,部署后检测概率不到 0.6。
原因:造数据时把一段长信号切成多个样本,相邻样本高度相关。随机划分后,训练集和测试集里有大量"近邻"样本,模型相当于在背答案。
解决:按时间段划分,前 70% 时间段的样本做训练,后 30% 做测试。或者按 SNR 分层,确保测试集的 SNR 分布和训练集不同。更严格的做法是留出一整段独立采集的数据做最终评估。
4.2 坑二:噪声功率估计偏差导致特征分布漂移
现象:仿真数据训练的模型,在实测数据上虚警率暴涨。
原因:仿真用复高斯白噪声,实测噪声有色且功率随时间波动。协方差矩阵特征值比和功率谱特征对噪声功率敏感,分布一偏,分类边界就失效。
解决:在特征提取前做噪声功率归一化。具体做法是用感知窗口前后的"静默期"估计噪声功率,然后对接收信号做归一化。如果无法保证静默期,可以用协方差矩阵的迹做归一化,消除绝对功率的影响。
4.3 坑三:类别不平衡导致检测概率被"淹没"
现象:整体准确率 92%,但占用频段的检测概率只有 0.4。
原因:实际频谱中空闲时段远多于占用时段,训练集里正负样本比例可能到 1:10 甚至更极端。不加权的分类器会倾向于预测多数类。
解决:用class_weight='balanced'或对少数类做过采样。但过采样要注意不能在时间上相邻的样本之间插值,否则会引入虚假样本。我一般优先用类别加权,简单且不改变数据分布。
4.4 坑四:特征维度太高但样本不够,SVM 训练不收敛
现象:SVM 训练时间极长,或者交叉验证结果方差很大。
原因:特征维度 50+,样本量只有几百,SVM 的 RBF 核在高维空间里找不到好的分隔面。
解决:先做特征选择。用随机森林的特征重要性排序,取前 15~20 个特征。或者用 PCA 降到 10~15 维。频谱感知里,协方差特征值和功率谱前几个分量通常贡献最大,后面的特征可以砍掉。
4.5 坑五:推理延迟超标,实时判决跟不上
现象:算法在离线数据上表现很好,但部署到嵌入式平台后,感知周期内完不成一次判决。
原因:特征提取里的协方差矩阵计算和循环谱估计计算量大,如果每毫秒都要做一次感知,CPU 扛不住。
解决:把特征提取拆成"快特征"和"慢特征"。快特征(能量、方差)每次感知都算,慢特征(协方差特征值、循环谱)每 N 次感知算一次,用滑动窗口更新。分类器也可以用轻量模型,比如决策树深度限制在 8 以内,推理时间能压到微秒级。
5. 进阶技巧:用集成投票和在线校准把检测概率再提一档
5.1 多模型软投票比单模型稳
单模型的检测概率在某个 SNR 区间可能突然掉下去,原因是那个区间的特征分布刚好落在分类边界的模糊地带。集成多个模型做软投票,能平滑这种波动。具体做法是训练三个模型:随机森林、SVM、梯度提升树,然后对它们的预测概率取平均。
from sklearn.ensemble import VotingClassifier, GradientBoostingClassifier from sklearn.svm import SVC ensemble = VotingClassifier( estimators=[ ('rf', RandomForestClassifier(n_estimators=200, max_depth=15, class_weight='balanced', random_state=42)), ('svm', SVC(kernel='rbf', C=10, gamma='scale', probability=True, class_weight='balanced', random_state=42)), ('gbdt', GradientBoostingClassifier(n_estimators=150, max_depth=5, learning_rate=0.1, random_state=42)) ], voting='soft' # 用概率平均而非硬投票 ) ensemble.fit(X_train, y_train)软投票的关键是每个模型都要输出校准过的概率。SVM 的probability=True会用 Platt 缩放做概率校准,这会增加训练时间但值得。梯度提升树本身输出概率,但可能偏保守,可以配合CalibratedClassifierCV做进一步校准。
5.2 在线校准:用滑动窗口更新判决门限
部署环境里的噪声水平会漂移,离线训练的门限过一段时间就不准了。我一般会在部署端加一个在线校准模块:维护一个长度为 200 的滑动窗口,记录最近 200 次感知的判决概率。如果窗口内判为"占用"的比例超过 80%,说明门限可能偏低了,把门限上调 0.02;如果低于 5%,说明门限偏高,下调 0.02。调整步长要小,避免震荡。
这个策略的前提是你对主用户的活动规律有大致了解——比如知道主用户不会连续占用超过 90% 的时间。如果主用户确实可能长时间占用,那这个校准逻辑要改,不能简单按比例调。
5.3 验证方法:用 ROC 曲线和 SNR-检测概率曲线说话
评估频谱感知模型,我习惯画两张图。第一张是 ROC 曲线,看整体分类能力。第二张是 SNR-检测概率曲线:横轴 SNR,纵轴 Pd(在固定 Pfa=0.1 下),每个 SNR 点画一个标记。这张图能告诉你模型在哪个 SNR 区间开始失效。
import matplotlib.pyplot as plt snr_bins = np.arange(-20, 5, 2) # -20dB 到 4dB,步长2 pd_at_pfa01 = [] for snr in snr_bins: mask = (snr_labels >= snr) & (snr_labels < snr + 2) if np.sum(mask) < 50: pd_at_pfa01.append(np.nan) continue y_true = y[mask] y_prob = ensemble.predict_proba(X_scaled[mask])[:, 1] fpr, tpr, _ = roc_curve(y_true, y_prob) valid = np.where(fpr <= 0.1)[0] pd_at_pfa01.append(tpr[valid[-1]] if len(valid) > 0 else np.nan) plt.plot(snr_bins, pd_at_pfa01, 'o-') plt.xlabel('SNR (dB)') plt.ylabel('Pd at Pfa=0.1') plt.grid(True) plt.axhline(y=0.9, color='r', linestyle='--', label='Pd=0.9') plt.legend() plt.show()这张图出来以后,你能清楚看到模型在 SNR 低于多少时 Pd 跌破 0.9。如果这个点比你要求的灵敏度还低 3 dB 以上,说明模型有裕量;如果刚好卡在边界,建议加特征或换模型。
我自己的习惯是:每次换特征集或换模型,都重新画这两张图,不看准确率。准确率在频谱感知里就是个玄学指标,Pd 和 Pfa 才是硬通货。另外,在线校准模块的滑动窗口长度和调整步长,我一般会在部署前用一周的实测数据做回放测试,确认不会因为校准逻辑本身引入震荡。这套流程走下来,从特征提取到模型部署,一个人两周能跑通,但要把检测概率在低 SNR 下做到稳定,通常需要反复迭代特征和校准参数。希望帮到你。
本文还有配套的精品资源,点击获取