简介:《基于卷积神经网络的OFDM频谱感知方法》选自《系统工程与电子技术》2019年第一期,面向认知无线电、频谱感知与深度学习交叉领域的研究者及工程技术人员。论文针对传统机器学习算法在频谱感知中训练速度慢、特征提取依赖人工设计的问题,提出将卷积神经网络图像识别优势引入OFDM信号感知:先对信号做循环自相关分析并归一化灰度处理,形成循环自相关灰度图,再以LeNet-5为基础设计分层卷积神经网络自动学习抽象特征,最后输入测试数据完成频谱占用判断。资源为单份PDF文件,大小约2.04MB,内容涵盖系统模型、网络结构、仿真实验与结果对比,结构紧凑,便于读者复现方法并迁移至其他调制信号的感知任务。资源已有163人浏览学习,适合通信工程、电子信息类专业学生及深度学习应用研究者下载参考。
1. 从一张频谱图看出信道是否被占用:CNN与OFDM频谱感知要解决的事
把《基于卷积神经网络的OFDM频谱感知方法》这个标题拆开看,其实是一条很务实的链路:OFDM信号在频域上有明显的子载波间隔特征,传统能量检测在低信噪比下分辨不了“噪声尖峰”和“真实占用”,而卷积神经网络(CNN)恰好擅长从二维矩阵里提取局部相关结构。做法很直接——把接收信号预处理成二维表示,比如循环谱、时频图或相关矩阵,再交给CNN做“占用/空闲”二分类,本质是把物理层检测问题转成图像分类问题。
这套方法的典型落地场景是认知无线电、频谱监测、无人机图传干扰排查:你不知道对面信号用什么调制、不知道导频位置,只想快速判断某个频段是不是被占了。对做通信感知一体化和频谱态势感知的工程人员来说,CNN管线比传统检测器更耐噪声,代价是要会造数据集、会调网络结构、会踩训练里的坑。这篇文章从输入表示、仿真数据、网络结构到避坑指南,把整条复现路径讲清楚。
2. 把OFDM信号变成CNN能吃的输入:循环谱、时频图与相关矩阵怎么选
2.1 先从频谱感知的本质说起:为什么要绕开能量检测
频谱感知在通信里是个二元假设检验问题:H0是信道空闲,H1是信道被占用。经典做法是能量检测,把接收信号在观察窗内求平均功率,再跟门限比。这套做法在噪声功率已知时很简单,但信噪比一旦低于0dB,噪声功率估计稍有偏差,误检率就会迅速恶化。匹配滤波检测需要知道发送端导频结构,在感知别人信号时通常拿不到;循环平稳特征检测灵敏度高,可是传统实现要估计谱相关函数,计算量不低。
CNN介入的位置,是用数据驱动的方式代替手工门限和手工特征。OFDM因为有多子载波、循环前缀(CP)、符号周期这样的周期结构,接收信号在统计特性上不是平坦的:子载波间隔会在频域形成等间距的纹波,CP和符号体之间也存在相关性。这些结构在二维表示里会表现为特定的纹理或对称性,CNN的卷积核天然适合提这类“局部规律”。换句话讲,CNN不靠某一根频线的能量做判断,而是同时看整片频谱图里的相关结构,所以低信噪比下的鲁棒性更强。
这里隐含一个关键设计决策:给CNN的输入到底是什么。输入不同,后续网络结构、数据量和训练策略全都不一样。
2.2 三类二维输入表示对比:循环谱、时频图、相关矩阵
我见过的主流做法有三类:循环谱(Spectral Correlation Function)、短时傅里叶变换得到的时频图、以及接收信号的样本相关矩阵。它们都能把一维采样序列变成二维矩阵,但物理含义和工程代价差很多。循环谱是最“正统”的循环平稳检测表示,在循环频率和频谱频率两个维度上展示周期性,OFDM的符号率和子载波间隔都会在特定循环频率处形成峰值;问题在于计算量大,要估计二维谱相关函数,一个样本可能就要做多次FFT和滑窗平均,数据生成速度会拖慢整个训练管线。
时频图最直观,用短时傅里叶变换把时间、频率、幅度画成一张图,工程上手最快;但低信噪比下信号分量会被噪声盖住,时频图看起来和纯噪声几乎没有差别,CNN能提取的判别特征很少。相关矩阵的处理思路不太一样,不画频谱而是算采样向量的协方差:OFDM信号因为有CP和子载波间相关性,相关矩阵里存在明显的非对角结构;噪声的相关矩阵则接近单位阵乘以噪声功率,二者差异天然适合CNN分类。相关矩阵不需要符号同步和载波同步,也不需要知道导频格式,这特别符合频谱感知“被动观察”的定位。
| 输入表示 | 物理含义 | 主要优点 | 主要风险 |
|---|---|---|---|
| 循环谱 | 信号在循环频率维度的相关能量分布 | 低信噪比下特征最强,抗噪声和抗干扰 | 计算量大,生成数据集慢 |
| 时频图 | 短时傅里叶变换的幅度谱 | 直观、实现简单、适合人眼判读 | 低SNR时特征淹没,CNN学不到稳定纹理 |
| 相关矩阵 | 接收样本向量的协方差结构 | 无需同步与先验,计算快,噪声结构差异明显 | 对频偏和多径敏感,需要归一化处理 |
从复现角度,我一般优先推荐相关矩阵:一个OFDM符号块只要做一次矩阵乘法就能得到输入样本,批量生成仿真数据时吞吐量远高于循环谱。如果你的场景是强干扰、极低信噪比,再考虑循环谱,但要为计算成本提前做预算。
2.3 输入维度与OFDM子载波间隔、符号参数的对齐关系
CNN输入矩阵的尺寸不是随便定的,它必须和OFDM物理参数对齐。以常见的15kHz子载波间隔为例,OFDM符号长度约66.7微秒,循环前缀长度通常取符号长度的1/4到1/8。假设系统带宽范围内有64个子载波,FFT点数为64,那么一个完整OFDM符号的基带采样点就是64个数据样本加上16个CP样本,共80点。
如果我们要构造相关矩阵,一个样本块取多个OFDM符号的采样点,按列排成矩阵,再计算协方差。典型做法是取32到64个符号,每个符号取64个数据样本,则输入矩阵是64×64。这个维度既能覆盖一个符号周期内的频率结构,又不会让CNN输入过大导致显存紧张。采样率变化时,子载波间隔变窄,符号时间变长,每个符号内的采样点数增加,相关矩阵的维度就要相应上调;反过来,如果矩阵太大,可以在符号维度上做平均池化或减少参与统计的符号数。
还要注意一点,相关矩阵的归一化必须在每个样本内部完成——用矩阵的最大特征值或Frobenius范数做缩放,而不是在整个数据集上算全局均值方差。OFDM信号有较高的峰均比,不同符号块的能量差异可能达到数倍,全局归一化会让低能量样本在训练时被高能量样本压制。这个细节在实验里经常成为训练不收敛的诱因,放到后面的坑里细说。
3. 自建OFDM仿真数据集:从发射机模型到带标签样本
3.1 为什么优先用仿真而不是现场采集
频谱感知的数据集不像图像分类那样有现成公开资源。真实采集的空中信号往往涉及特定频段授权、设备部署位置、观测时段,数据不可控也不易公开;更麻烦的是标注问题——想给样本打“占用/空闲”标签,需要同时有一台高精度接收机和一个已知信号源的开关日志,普通团队很难组织这种实验环境。仿真生成的标签是精确的:我们明确知道哪个样本加了信号、哪个样本只有噪声,不存在标注模糊。
仿真的另一个优势是能精确控制信噪比覆盖范围。能量检测在-5dB以下基本失效,CNN方法的目标工作区正好是低信噪比区间;通过仿真可以在-15dB到10dB之间按步进生成足够样本,观察模型性能随SNR的退化曲线。后面调阈值、做模型验收都需要这条曲线。
3.2 OFDM基带信号生成代码:子载波、QPSK映射与加CP
下面这段代码用NumPy实现一个最简OFDM发射机模型,特征包括QPSK映射、IFFT调制和循环前缀插入,足够生成频谱感知所需的信号源。
import numpy as np def generate_ofdm_baseband( n_symbols=64, # 一个样本块内的OFDM符号数 n_subcarriers=64, # 有效子载波数(含数据与导频占位) cp_len=16, # 循环前缀采样点数 mod_order=4, # QPSK映射 seed=42): rng = np.random.default_rng(seed) n_bits = n_symbols * n_subcarriers * int(np.log2(mod_order)) bits = rng.integers(0, 2, n_bits) # QPSK映射:每2 bit映射为一个QPSK符号 symbols = (2 * bits[0::2] - 1) / np.sqrt(2) + 1j * (2 * bits[1::2] - 1) / np.sqrt(2) symbols = symbols.reshape(n_symbols, n_subcarriers) # IFFT调制:频域符号转时域 time_domain = np.fft.ifft(symbols, axis=1) # 插入循环前缀:把每个符号尾部cp_len个样点复制到开头 tx_signal = np.concatenate( [time_domain[:, -cp_len:], time_domain], axis=1 ).flatten() return tx_signal signal = generate_ofdm_baseband() print(f"时域信号长度: {signal.shape[0]}, 类型: {signal.dtype}")逻辑说明:先按OFDM符号数和子载波数生成随机比特,QPSK映射把每两个比特变成一个复数符号;IFFT沿子载波维度做,把频域符号阵列变到时域;循环前缀把每个符号的尾部一段复制到开头,保证子载波正交性在多径环境下不被破坏。输出的一维复数组就是基带OFDM信号。
参数说明:n_symbols控制的是一个样本块的时间长度,取太短会让相关矩阵统计不稳定,取太长会增大矩阵计算量,64是常见折中;n_subcarriers与OFDM子载波间隔相乘就是系统带宽,64个子载波在15kHz间隔下对应960kHz带宽,接近常见窄带系统配置;cp_len取16对应1/4符号长度,抗多径能力和频谱效率之间比较均衡。
3.3 加噪声、加多径,按SNR滑窗生成训练集与验证集
有了基带信号,下一步是模拟信道。频谱感知训练集里必须同时包含两类样本:加了OFDM信号的样本和纯噪声样本。纯噪声样本不需要跑发射机,直接用复数高斯随机数生成即可;加信号样本则在OFDM基带基础上叠加信道效应。
def build_dataset( tx_signal, snr_db_list=(-15, -10, -5, 0, 5, 10), samples_per_snr=1000, n_subcarriers=64, cp_len=16, channel_type="awgn", seed=0): rng = np.random.default_rng(seed) X, y = [], [] symbol_len = n_subcarriers + cp_len n_symbols = len(tx_signal) // symbol_len for snr in snr_db_list: for _ in range(samples_per_snr): # 从整段基带信号里随机截取一个符号块 start = rng.integers(0, n_symbols - 32) block = tx_signal[start * symbol_len: (start + 32) * symbol_len] if channel_type == "multipath": # 两径信道:直达径加一条延迟1个采样点的反射径 h = np.array([1.0, 0.5j]) block = np.convolve(block, h, mode="same") # 加噪声:按SNR计算噪声功率 signal_power = np.mean(np.abs(block) ** 2) noise_power = signal_power / (10 ** (snr / 10)) noise = rng.normal(0, np.sqrt(noise_power / 2), block.shape) \ + 1j * rng.normal(0, np.sqrt(noise_power / 2), block.shape) rx_signal = block + noise # 构造相关矩阵:每64个采样点为一行,取32个符号 mat = rx_signal[:32 * (n_subcarriers + cp_len)] mat = mat.reshape(32, n_subcarriers + cp_len)[:, :n_subcarriers] cov = np.cov(mat) # 样本级归一化:除以矩阵Frobenius范数 cov = cov / (np.linalg.norm(cov) + 1e-12) X.append(cov[np.newaxis, :, :]) # 增加通道维,1x64x64 y.append(1) # 占用样本标签为1 # 纯噪声样本:不叠加信号,其他处理一致 for _ in range(samples_per_snr): noise_only = rng.normal(0, 1, (32, n_subcarriers)) \ + 1j * rng.normal(0, 1, (32, n_subcarriers)) cov = np.cov(noise_only) cov = cov / (np.linalg.norm(cov) + 1e-12) X.append(cov[np.newaxis, :, :]) y.append(0) return np.array(X, dtype=np.float32), np.array(y, dtype=np.int64)逻辑说明:这段代码把信噪比滑窗和样本构造封装在一个函数里。每个SNR下生成等量正负样本,避免类别不平衡;加噪声时按当前信号功率计算噪声方差,保证SNR定义准确;相关矩阵按32个符号×64个有效子载波计算,得到32×32矩阵——这里的实际维度由截取符号数和有效子载波数共同决定,不是硬编码64×64,便于后期调整参数。
参数说明:snr_db_list建议从-15dB起步,最低到0dB就够覆盖大多数感知场景,低于-15dB时OFDM结构本身已经淹在噪声里,CNN学到的是噪声统计而不是信号特征;samples_per_snr是每个SNR档位的样本数,1000个正样本加1000个负样本算一个档位,六个档位共12000个样本,足够训练一个三层卷积网络;channel_type参数留了扩展口子,先跑“awgn”验证链路,再切换“multipath”测试模型泛化。生成的数据用np.float32存储,一个样本是4KB,12000个样本约48MB,处理起来很轻松。
4. 设计CNN结构并训练:从三层卷积到带注意力的残差网络
4.1 迁移图像分类经验之前的三个误区
第一个误区是直接套ResNet或VGG这种为ImageNet设计的深网络。频谱感知输入是32×32或64×64的单通道相关矩阵,不是224×224的RGB图;深网络在这个尺度下感受野很快覆盖全图,网络越深越容易记住训练集的随机噪声。第二个误区是加载ImageNet预训练权重——相关矩阵的分布和自然图像差着十万八千里,第一层卷积核统计特性完全不匹配,预训练权重不仅不加速收敛,反而要花更多轮次把特征抹掉重学。第三个误区是忽略输入矩阵的“通道”语义:相关矩阵是复数协方差,实部和虚部对检测都有价值,规范做法是把实部、虚部、相位分别做成三个通道,或者只用实部做成单通道,而不是把复数直接塞进网络。
4.2 一个参数量合理的CNN基线结构
考虑到输入尺寸小、样本量在万级,我推荐用三层卷积加一个轻量注意力模块的结构。下面这段PyTorch代码是可运行的基线:
import torch.nn as nn class SpecSenseCNN(nn.Module): def __init__(self, in_channels=1, num_classes=1): super().__init__() # 第一层:捕捉子载波间隔带来的局部周期性纹理 self.conv1 = nn.Sequential( nn.Conv2d(in_channels, 16, kernel_size=3, padding=1), nn.BatchNorm2d(16), nn.ReLU(inplace=True), nn.MaxPool2d(2) ) # 第二层:扩大感受野,提取符号间相关结构 self.conv2 = nn.Sequential( nn.Conv2d(16, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2) ) # 第三层:通道注意力,让网络在低SNR时聚焦能量聚集的子载波位置 self.conv3 = nn.Sequential( nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True) ) self.attn = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(64, 64 // 4), nn.ReLU(inplace=True), nn.Linear(64 // 4, 64), nn.Sigmoid() ) self.global_pool = nn.AdaptiveAvgPool2d(1) self.fc = nn.Linear(64, num_classes) def forward(self, x): x = self.conv1(x) x = self.conv2(x) x = self.conv3(x) # 通道注意力:按全局均值给特征图加权 attn = self.attn(x) x = x * attn.unsqueeze(-1).unsqueeze(-1) x = self.global_pool(x).flatten(1) return self.fc(x) model = SpecSenseCNN(in_channels=1, num_classes=1) print(model)逻辑说明:前两层都是“3×3卷积+批归一化+ReLU+最大池化”的组合,3×3卷积核是在小尺寸输入上最稳妥的选择,两次池化把32×32的输入逐步降到8×8,特征图数量从16翻到64。第三层卷积后面接了通道注意力,每个特征图根据全局平均值算出一个权重,作用是让网络自动放大能量集中频段的特征、抑制纯噪声特征图——这个操作在低信噪比场景比单纯加深网络有效得多。
参数说明:num_classes=1对应二分类的logit输出,配合BCEWithLogitsLoss使用;BatchNorm放在卷积后能稳定低SNR样本的分布差异,但不能在训练和推理时混用;注意力模块里的缩放比例4是一个超参,数据量小的时候可以改成都2或者8,影响不大。如果你用64×64的输入,把第一层pooling去掉或者改用stride=2的卷积做下采样即可。
| 设计变量 | 本方案取值 | 调整方向 |
|---|---|---|
| 输入尺寸 | 32×32 或 64×64 单通道 | 增大则保留更多频域细节,但训练变慢 |
| 卷积核 | 3×3,padding=1 | 5×5适合提取更宽子载波间相关,慎用 |
| 池化方式 | MaxPool2d(stride=2) | 可换AvgPool,但MaxPool对锐利频谱峰值更友好 |
| 注意力 | SE模块在第三层 | 可移到第一层,但低层特征比较粗糙 |
| 输出 | 1维logit | 多类识别时改成K维输出 |
4.3 训练配置与阈值选择:SNR平衡采样、BCE损失与最优阈值
数据集里每个SNR档位的样本数是均匀的,但不同SNR样本的学习难度差异很大。-15dB的样本在CNN眼里几乎全是噪声,0dB的样本则很容易学。如果按原始顺序随机打乱训练,模型会倾向于“放弃”低SNR样本;规范做法是做一个按SNR分层的采样器,每轮迭代里各SNR档位均匀出现。
from torch.utils.data import Sampler import torch class SNRBatchSampler(Sampler): def __init__(self, snr_labels, batch_size): # snr_labels: 每个样本对应的SNR档位索引 self.snr_labels = np.array(snr_labels) self.batch_size = batch_size self.snr_groups = [np.where(self.snr_labels == i)[0] for i in np.unique(self.snr_labels)] def __iter__(self): indices = [] for g in self.snr_groups: rng = np.random.default_rng() perm = rng.permutation(g) indices.extend(perm) # 在每个batch内混入多个SNR档位的样本 for i in range(0, len(indices), self.batch_size): yield indices[i:i + self.batch_size] criterion = nn.BCEWithLogitsLoss() optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)逻辑说明:SNRBatchSampler先把同一SNR档位的样本各自打乱,再按batch_size顺序拼接,一个batch内部尽量涵盖多个SNR档位,避免模型被单一难度模式带偏。损失用BCEWithLogitsLoss,原因是它把sigmoid和交叉熵合并,数值稳定性比手动接nn.Sigmoid加nn.BCELoss更好。优化器选AdamW而不是SGD,小数据集上收敛更直接;CosineAnnealingLR让学习率在前50轮内从1e-3平滑降到接近0,对后面验证集阈值选择更友好。
阈值选择经常被忽略。模型输出的logit过了sigmoid是一个0到1的概率,但0.5不一定是最优判决门限。正确做法是在验证集上遍历多个阈值,画出ROC曲线,用“最大约登指数”或者“P_d优先”策略选定。如果业务更在意不漏检,阈值往低调;如果更在意误报,阈值往高调。下面这段代码输出各阈值下的检测概率和虚警概率。
from sklearn.metrics import roc_curve probs = torch.sigmoid(model(val_x)).detach().numpy() fpr, tpr, thresholds = roc_curve(val_y, probs) # 约登指数:敏感度 + 特异度 - 1 youden = tpr - fpr best_idx = np.argmax(youden) print(f"最优阈值: {thresholds[best_idx]:.3f}, P_d={tpr[best_idx]:.3f}, P_fa={fpr[best_idx]:.3f}")逻辑说明:roc_curve返回的是不同阈值下的虚警率和检测率,约登指数兼顾了H0和H1两种错误的代价。实际系统如果对虚警率有硬性要求(比如P_fa不能超过0.01),就改成在fpr<0.01的区间里挑tpr最大的阈值。
参数说明:训练轮次50是一个经验值,样本量小、网络浅,50轮足够收敛;batch_size建议32到64之间,太小会让SNR分层采样不稳定,太大在AdamW下收敛变慢。如果发现验证集P_d在15轮后不涨,优先检查学习率和数据归一化,不要急着加网络深度。
5. 频谱感知CNN避坑指南:5个高频翻车点的现象、原因与排查思路
5.1 数据与标注侧:类别不平衡、信道过拟合、归一化失效
坑1:训练loss持续下降,验证集检测概率P_d却很低。现象是模型过拟合到了噪声样本的统计细节,把噪声里的随机尖峰也当成信号特征。原因是正负样本比例失衡——如果空闲样本远多于占用样本,模型倾向于输出“空闲”来降低整体损失。解决方法是先统计训练集标签分布,把正负样本比例控制到1:1;如果业务场景确实空闲多,就在损失函数里给正样本加权重,或者改用Focal Loss压低易分样本的梯度贡献。
坑2:AWGN信道上训练效果不错,一换到多径信道就崩。现象是验证集P_d从90%掉到60%以下,且集中在低SNR档位。原因是仿真时信道条件太单一,模型学到了“白噪声背景下能量均匀分布”的错误先验。解决方法是训练数据里混合多种信道:两径、三径、含载波频偏、含采样钟偏移,至少保证每种信道在训练集里占20%以上;另外可以在每个epoch里随机替换信道类型,等效做信道增强。
坑3:输入相关矩阵不做样本级归一化,训练loss震荡。现象是loss曲线上下跳,验证集P_d不稳定,每次跑结果差异很大。原因是不同SNR下信号能量差异可达两个数量级,全局归一化后高SNR样本主导梯度。解决方法是回到数据生成代码里,在保存样本前逐样本用Frobenius范数归一化,并确认推理时对实时数据也执行同样的归一化步骤。这一步最容易在部署阶段漏掉,训练用归一化、推理用裸数据,模型输出概率会变成乱跳的标量。
5.2 模型与训练侧:过深网络、阈值漂移、显存失控
坑4:网络深度越加越深,训练准确率逼近100%,验证P_d反而下降。现象是模型把训练集样本的标签背下来了,典型表现是训练集loss趋近于0但验证集ROC的AUC不到0.8。原因是数据集总量只有1万出头,网络参数量超过500万后模型容量已经超出数据信息量。解决方法是先用参数表里的基线结构,把参数量压在100万以内;加入Dropout(建议0.3)和权重衰减,并在训练时做时间偏移增强——把符号块的起始位置随机偏移几个采样点,强制模型学习结构特征而非位置特征。
坑5:验证集上选出来的阈值,到另一个SNR档位上失效。现象是最优阈值在0dB档位是0.42,放到-10dB档位后虚警率飙升。原因是验证集各SNR样本均匀混合,选出的阈值是“平均SNR”下的结果,不代表低SNR子集。解决方法是按SNR分组计算ROC,再按目标工作SNR选择阈值;如果工作场景SNR范围很宽,可以训练一个SNR回归分支,让网络同时输出SNR估计和占用概率,用估计的SNR动态调整判决门限。这个做法在实测环境里非常实用,也是把仿真模型往工程落地的关键一步。
坑6:输入矩阵用64×64,网络一加深,batch_size只能设到16,训练慢得离谱。现象是GPU利用率上不去,显存倒是先爆了。原因是相关矩阵计算是密集存储,64×64的矩阵经过两层卷积后特征图尺寸虽小,但中间激活值不少;解决方法不是砍网络,而是把输入降到32×32——减少参与相关统计的符号数,或者对相关矩阵做双线性插值降采样,信息损失很小,训练速度能快三到四倍。
6. 验证闭环与进阶做法:用ROC和SNR-Pd曲线替模型“验收”
6.1 从仿真到实测信号台的验证路径
仿真里跑出漂亮指标只是第一步。把模型搬到实测环境,我一般按四步做验收:第一步,用软件无线电平台(常见的是USRP或RTL-SDR)在目标频段连续采集基带IQ数据,不需要解调,只需要记录对应的频谱占用日志作为弱标注;第二步,用与训练完全相同的参数切符号块、算相关矩阵、做样本级归一化,这一步代码可以直接复用,但要注意实测信号的采样率可能与仿真不一致,需要重采样到训练时用的采样率;第三步,用训练好的模型对实测样本做推理,输出概率再按验证集选出的阈值判决;第四步,画两条验收曲线——P_d随SNR的变化曲线,以及ROC曲线,与仿真的对应指标做对比。
实测数据里通常还有仿真没建出来的干扰,比如邻频泄漏、突发脉冲、带外杂散。模型在这时候的表现才是真实可用性。如果P_d比仿真低15个百分点以上,优先检查采样率对齐和归一化,这两个是高频问题;如果虚警偏高,说明模型学到了某种只在仿真里存在的统计特征,要回看数据生成代码里的噪声建模是否过于理想化。
6.2 一键绘制ROC与SNR-Pd曲线
验收离不开可视化。把验证代码整理成一个可复用的绘图函数,每次实验结束直接把PNG存下来,比盯终端打印的数字直观得多。
import matplotlib.pyplot as plt def plot_curves(snr_list, pd_list, pf_list, fpr, tpr): fig, axes = plt.subplots(1, 2, figsize=(12, 4)) # 左图:检测概率随SNR变化 axes[0].plot(snr_list, pd_list, marker="o") axes[0].set_xlabel("SNR (dB)") axes[0].set_ylabel("Probability of Detection") axes[0].grid(True) # 右图:ROC曲线 axes[1].plot(fpr, tpr, lw=2) axes[1].set_xlabel("False Alarm Probability") axes[1].set_ylabel("Detection Probability") axes[1].grid(True) plt.tight_layout() plt.savefig("spectrum_sense_validation.png", dpi=150)逻辑说明:左边曲线展示感知灵敏度随信噪比的退化趋势,是向项目干系人汇报时最有说服力的一张图;右边ROC曲线则用来选择工作点和评估整体判别能力。两个指标在不同SNR档位上的整体表现,才构成一次完整的模型验收结论。
说一个我自己的教训:刚开始做这个方向时,我只在0dB附近调阈值,拿到很高的P_d就以为任务完成,结果实测场景里主要干扰出现在低SNR区间,模型在真正需要它的时候反而失灵。后来改成每个SNR档位单独测、单独选阈值,心里才有底。频谱感知的CNN方法说到底是一个统计判决器,仿真是帮你理解它的工具,实测才是验证它是否可信的标准,希望这套避坑经验帮你在复现时少走几步弯路。
本文还有配套的精品资源,点击获取