基于卷积神经网络与STFT时频图的OFDM频谱感知方法 📅 发布时间:2026/9/19 12:10:30 👁 浏览次数: 简介一份PDF学术论文主题是基于卷积神经网络的OFDM频谱感知方法面向认知无线电、深度学习与无线通信交叉领域的研究者和学习者。论文针对传统机器学习算法在频谱感知中训练速度慢、低信噪比条件下检测性能不足的问题提出利用OFDM信号循环自相关特征并归一化为灰度图像以LeNet-5为基础构建卷积神经网络实现分层特征提取与频谱占用状态识别并给出了仿真对比与结果分析。包体为单个PDF文件大小2.04MB共1个文件全文内容完整直接可读。已有163人学习。对于想了解深度学习在通信信号处理中落地方法、复现频谱感知实验或寻找研究思路的读者这篇论文清晰展示了从信号建模、网络设计到实验验证的完整流程具有直接参考价值。1. 频谱占用检测做到现场为什么反而要回头用卷积神经网络认知无线电里的频谱占用检测干到现场才发现最稳的不是门限仪器而是让卷积神经网络自己去看时频图。OFDM 把信号能量摊在几十上百个子载波上频谱轮廓本来就是一块「凸起不那么明显的平顶」能量检测在这种波形面前几乎每次都要跟噪声功率估计较劲循环平稳检测理论上能抓住 CP 结构可计算量在实时设备上又撑不住。把接收信号做一次 STFT 变成时频图再把「有没有 OFDM 信号」当成图像分类问题本质上是在让卷积神经网络从数据里直接学频谱纹理。这个方法适合两类人一类是做 5G 非授权频段共存、需要实时判断主用户是否在线的系统工程师另一类是刚接触软件无线电、手头有 USRP 或 RTL-SDR、想找一个能落地的深度学习信号处理实验的开发者。下面这套流程从 OFDM 信号仿真开始到网络训练、陷阱排查、部署校验为止用一台带 GPU 的普通笔记本就能完整跑通。2. 为什么把 OFDM 频谱感知建模成 CNN 图像分类任务2.1 能量检测和循环平稳检测在 OFDM 场景下的瓶颈OFDM 频谱感知的传统做法分两条技术路线。能量检测的思路是在接收端对一个窗口内的信号求平均功率再跟预设门限比较完成有无判决。实现非常简单复杂度和时延都低但它有两个天然弱点一是不利用任何波形结构OFDM 的循环前缀、导频排列、子载波间隔这些特征完全没有参与判决二是低信噪比区域里噪声功率估计本身的抖动就会让门限跟着漂移稍有失配虚警率就会快速上升。循环平稳检测走的是另一条路。OFDM 信号因为循环前缀的重复结构在循环频率维度上会出现非零的谱相关峰这种二阶统计特征比能量要稳即便信噪比很低也能被识别出来。问题在于谱相关密度估计需要很长的观测窗口计算复杂度也高要在实时感知设备上把循环频率扫描完整跑一遍多数时候吃不消。循环平稳检测对载波频偏和符号定时也比较敏感工程上的同步成本不低。卷积神经网络介入后的核心变化是把这两条路线收进了一个端到端框架。OFDM 信号在 STFT 时频图上的导频栅格会形成规则的亮点阵列CP 带来的重复结构会在时间轴方向留下纵向条纹这正好是卷积核擅长捕捉的周期纹理。实践中对比下来检测概率相当的条件下CNN 方案在 -10 dB 到 0 dB 区间的虚警控制比纯能量检测明显更稳推理时延又远低于全谱循环平稳扫描所以「STFT 时频图 CNN」成了目前软件无线电平台上最常见的一档选择。2.2 三种输入形式对比IQ 序列、功率谱与时频图同样的 OFDM 接收数据喂给网络的形态可以差很多。IQ 序列保留幅度和相位信息量最全但需要用足够长的观测窗才能让网络从一维序列里发现 CP 引起的周期性而且 I/Q 两路复数在卷积里的通道设计比较绕一维卷积核的感受野要覆盖几百个采样点才能看到结构。功率谱把相位全部丢弃只剩下幅度包络对 OFDM 这种多子载波叠加的波形来说频谱平坦度本身区分度不够低信噪比下几乎退化成能量检测。时频图是折中之后最常用的输入。它保留频率分辨率与时间分辨率两个维度导频、CP、突发时长都会在二维平面上形成可被卷积核捕捉的纹理同时它天然对相位不敏感省掉了同步环节的很多麻烦。三种输入的对比见表 2-1。输入形式维度保留信息模型复杂度适用场景IQ 序列2 × L幅度相位全保留低1D CNN同步较好、观测窗短的专用检测功率谱L幅度相位全丢低1D CNN粗检测无法区分同频段多波形STFT 时频图T × F时间/频率结构完整中2D CNN低信噪比检测、多径环境、波形识别选择时频图还有一个附带收益后续如果想从「检测有无」升级到「识别具体波形」比如区分 OFDM、OTFS 和 FMCW只需要在同一个骨干网络后面换分类头输入完全不用改。这一点的工程价值在倒数第二章展开。2.3 用 Python 合成 OFDM 信号并准备训练用的时频图没有公共数据集可以拿来就用常见做法是自己合成 OFDM 基带信号。下面的脚本生成 QPSK 调制的 OFDM 帧经过三径信道和加性高斯白噪声后做 STFT得到模型输入。import numpy as np from scipy import signal def ofdm_symbol(n_data192, n_carriers256, cp_len32): # QPSK 映射每 2 比特生成一个 ±1±1j 符号 bits np.random.randint(0, 2, n_data * 2) qpsk (bits[0::2] * 2 - 1) 1j * (bits[1::2] * 2 - 1) # 256 个子载波中192 个放数据其余补零作为保护带 freq np.zeros(n_carriers, dtypecomplex) freq[:n_data] qpsk # IFFT 到时域乘 sqrt 保持功率归一 body np.fft.ifft(freq) * np.sqrt(n_carriers) # 拼接循环前缀CP 长度取 32 return np.concatenate([body[-cp_len:], body]) def synth_ofdm_frame(snr_db, n_symbols14, n_carriers256): # 连续拼接 n_symbols 个 OFDM 符号 tx np.concatenate([ofdm_symbol(n_carriersn_carriers) for _ in range(n_symbols)]) # 2 倍上采样给频谱边缘留出观察余量 tx signal.resample_poly(tx, 2, 1) # 三径信道主径 两条 0.6/0.3 倍衰减的延迟径 rx signal.lfilter([1.0, 0.6, 0.3], 1.0, tx) # 按目标 SNR 生成复高斯噪声 noise np.random.randn(len(rx)) 1j * np.random.randn(len(rx)) signal_power np.mean(np.abs(rx) ** 2) noise_power signal_power / (10 ** (snr_db / 10)) rx rx noise * np.sqrt(noise_power / 2) # STFT 得到复数时频谱取模作为二维输入 f, t, Zxx signal.stft(rx, fs2, nperseg128, noverlap96) return np.abs(Zxx).astype(np.float32) def synth_dataset(n_frames2000, snr_db0): X, y [], [] for _ in range(n_frames): X.append(synth_ofdm_frame(snr_db)) y.append(1) # 有 OFDM 信号 X.append(synth_ofdm_frame(snr_db, n_symbols0)) # 纯噪声帧 y.append(0) # 无信号 return X, y代码里的几个点值得说明。OFDM 符号的 CP 长度 n_carriers//8子载波利用率是 192/25675%这是工程里常见的配置实际系统里换成 5G NR 的 15 kHz 子载波间隔、128 点 CP 也能照搬这个生成流程只要 STFT 参数对应调整。signal.lfilter模拟的是抽头延迟线信道三径系数里的 0.6 和 0.3 对应多径相对幅度实际部署时可以换成 Sionna 或 pyroraytrace 输出的真实信道冲击响应。噪声功率的计算是新手最容易错的地方。np.abs(rx)**2的均值已经包含实虚两路的能量所以复噪声的每一路都要乘以noise_power / 2否则实际注入的噪声功率会翻倍SNR 整体偏小 3 dB。合成负样本时把n_symbols设为 0输出的就是纯噪声的 STFT 幅值标签取 0。这样生成的正负样本各占一半二分类不需要再做样本均衡。提示STFT 的窗长和重叠率决定了时频图的分辨率。nperseg128、noverlap96 时频率分辨率大约是 2/128时间方向一帧约 32 个采样点足以分辨 14 个 OFDM 符号形成的纵向条纹如果要检测更短的突发需要把 nperseg 降到 64 并同时调低 noverlap。3. 搭建 CNN 模型并设定 OFDM 频谱感知的训练目标3.1 参考 Lenet5 卷积神经网络结构改造出轻量检测网络时频图的尺寸一般在 128×128 以内用不着把 ResNet 或 VGG 直接搬过来。参考 Lenet5 的双卷积-池化结构配合 BatchNorm 和 AdaptiveAvgPool可以在保持检测精度的同时把模型压到几 MB推理时延在 CPU 上也能控制在毫秒级。结构设计见表 3-1。层输出尺寸参数说明Conv2d(1, 16, 3, padding1)128×128×163×3 卷积提取局部频谱纹理BatchNorm2d ReLU MaxPool(2)64×64×16缩小特征图增强平移鲁棒性Conv2d(16, 32, 3, padding1)64×64×32第二层卷积开始组合纹理基元BatchNorm2d ReLU MaxPool(2)32×32×32感受野覆盖约 10 个子载波宽度Conv2d(32, 64, 3, padding1)32×32×64第三层对应更大范围的导频栅格BatchNorm2d ReLU AdaptiveAvgPool(8×8)8×8×64全局池化兼容不同输入尺寸Flatten Linear(4096, 256)256全连接层相当于特征聚合Dropout(0.5) Linear(256, 2)2输出 ON/OFF 两类 logitsAdaptiveAvgPool 是这类小型网络里值得保留的一个设计训练时输入固定为 128×128部署时如果 STFT 窗口改小成 96×96网络最后一层不需要改参数依然能输出同样形状的特征。如果还担心低信噪比下的时序抖动可以把池化层换成多尺度池化把 2×2 和 4×4 的池化结果拼一起代价是推理时延增加约 20%。3.2 用 PyTorch 实现 OFDM 频谱感知 CNN把表 3-1 的结构写成 PyTorch 代码就是下面的样子。import torch.nn as nn class OFDMSpecCNN(nn.Module): def __init__(self, n_class2): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 16, 3, padding1), nn.BatchNorm2d(16), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(16, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d((8, 8)) ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 8 * 8, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, n_class) ) def forward(self, x): return self.classifier(self.features(x))BatchNorm 在这类任务里不是辅助手段而是必需品。OFDM 时频图的幅值范围受功率控制、信道衰减和 STFT 窗函数影响批次之间的分布差异很大BatchNorm 把每层输入拉回零均值单位方差训练收敛速度能快一个量级。全连接前的 Dropout 取 0.5在样本量只有几千帧的合成数据集上能明显抑制过拟合。输入张量需要注意维度顺序。PyTorch 默认 Layout 是(batch, channel, height, width)所以单张时频图要 reshape 成(1, 1, 128, 128)再进网络。数据归一化用全局均值和标准差即可不要对每张图单独做 min-max否则会抹掉不同信噪比之间的幅度差异等于破坏了 SNR 信息。3.3 用 Focal Loss 压住低信噪比区域的漏检频谱感知的误判代价不对称漏检意味着主用户被干扰虚警则只是浪费一段空闲频谱。普通交叉熵对两类错误一视同仁而实际上 OFDM 检测的难点集中在低 SNR 帧这类帧的网络输出概率往往在 0.5 附近徘徊。Focal Loss 通过调制因子让模型把注意力放在难分类样本上import torch import torch.nn as nn class FocalLoss(nn.Module): def __init__(self, alpha0.3, gamma1.5): super().__init__() self.alpha alpha self.gamma gamma self.ce nn.CrossEntropyLoss(reductionnone) def forward(self, logits, target): ce_loss self.ce(logits, target) p torch.exp(-ce_loss) # 网络对 target 类的预测概率 alpha_t torch.where(target 1, torch.full_like(target, self.alpha, dtypetorch.float32), torch.full_like(target, 1 - self.alpha, dtypetorch.float32)) return (alpha_t * (1 - p) ** self.gamma * ce_loss).mean()参数alpha控制正负样本的权重取 0.3 意味着给「有信号」这一类更高的损失权重对应漏检代价更高的先验gamma是难易样本调节因子取 1.5 时概率为 0.7 的样本损失约被压到原来的 0.09 倍而概率为 0.5 的样本只压到 0.35 倍网络自然会偏向学习低信噪比区域的边界。如果验证集里 -10 dB 样本的检测概率始终上不去先把 gamma 降到 1.0 试调大 gamma 并不总是改善。4. 训练流程与 OFDM 场景特有的调参陷阱4.1 训练脚本主流程与跨 SNR 的采样方式训练流程本身不复杂复杂的是数据怎么喂。先统一输入尺寸STFT 输出大约是 129×91前面代码生成的每一帧尺寸略有浮动需要填充或裁剪到固定 128×128再按全局均值和标准差归一化。import numpy as np import torch from torch.utils.data import DataLoader, TensorDataset def to_model_input(X, size128): out np.zeros((len(X), 1, size, size), dtypenp.float32) for i, x in enumerate(X): f, t x.shape[0], x.shape[1] out[i, 0, :min(f, size), :min(t, size)] x[:min(f, size), :min(t, size)] return out X, y synth_dataset(n_frames1200, snr_db0) # 实际请按 8:2 拆 train/val X_norm to_model_input(X) mean, std X_norm.mean(), X_norm.std() X_norm (X_norm - mean) / (std 1e-8) dataset TensorDataset(torch.tensor(X_norm), torch.tensor(y, dtypetorch.long)) loader DataLoader(dataset, batch_size64, shuffleTrue)训练循环里需要做两件 OFDM 场景特有的事一是每个 epoch 重新用不同 SNR 合成数据而不是把某个固定 SNR 的数据集反复用二是每 5 个 epoch 按 SNR 分段评估一次避免「看起来收敛、换到弱信号就崩」。SNR 在 -10 dB 到 10 dB 之间均匀随机抽取每次合成的帧都不同等于做了无限数据增强。超参数取值说明优化器Adamlr1e-3weight_decay1e-4学习率1e-3 起步每 10 轮 ×0.5StepLR 对这类小数据集足够Batch size64显存允许可加到 128BN 更稳训练轮数25轻量网络在 20 轮左右即收敛数据增强时间轴随机左右平移 ±8 帧增强对定时偏差的鲁棒性数据增强里的时间轴平移很关键。STFT 起点只要偏移几十个采样点时频图在时间轴上就会整体平移但 OFDM 的 CP 和导频结构不变。随机平移等于告诉网络「条纹出现的位置不重要有没有条纹才重要」这比在频域做随机翻转更贴合物理实际。4.2 陷阱一SNR 池化不足导致部署性能崩塌只在一个 SNR 上训练部署到另一个 SNR 场景时性能会断崖式下滑这是频谱感知任务最常踩的坑。原因很直接网络会把「绝对幅度」当作特征而在 -5 dB 下学到的幅度范围跟 5 dB 下的完全不同。解决方法是训练时做 SNR 池化让每个 batch 内混合不同信噪比的样本。评估时不要把验证集混在一起只看总准确率而是按 SNR 分段统计检测概率def evaluate_by_snr(net, val_by_snr): net.eval() for snr, loader in val_by_snr.items(): tp fn 0 for xb, yb in loader: with torch.no_grad(): p torch.softmax(net(xb), dim1) pred (p[:, 1] 0.5).long() tp ((pred yb) (yb 1)).sum().item() fn ((pred 0) (yb 1)).sum().item() detection_prob tp / max(tp fn, 1) print(fSNR {snr:02d} dB 检测概率 {detection_prob:.3f})如果 -10 dB 段的检测概率远低于其他段说明 SNR 池化的采样区间还不够宽。我一般会把 SNR 采样范围放宽到 -15 dB 到 15 dB并保证每个 batch 里低 SNR 样本占比不低于 30%。注意评估代码里的阈值softmax 输出后默认用 0.5 判决但验证时目标虚警率不同阈值也要跟着调后面会提到。4.3 陷阱二STFT 窗跨过 ON/OFF 边界造成标签泄漏合成数据时如果只生成两种独立帧纯噪声帧标 0、OFDM 帧标 1训练集里就永远没有「从无信号到有信号过渡」的帧。实际部署时检测器每个 STFT 窗都会滑过边界边界帧的能量和纹理都介于两类之间模型在边界帧上的输出会来回抖动宏观表现就是检测滞后和突发虚警。更隐蔽的问题是标签泄漏如果直接把一段含 OFDM 的连续 IQ 流切成帧STFT 窗长 128 点、重叠 96 点时窗会同时盖住信号和噪声两个区域这一帧标 0 还是标 1 怎么定都有偏差。常见做法是在数据生成时显式加入过渡帧并把这些过渡帧从训练集剔除作为专门的边界校验集来用。# 生成一段前 1/4 纯噪声、后 3/4 OFDM 的连续流 def synth_transition_frame(snr_db, n_total_symbols20): n_noise n_total_symbols // 4 noise_part np.random.randn(128 * n_noise) 1j * np.random.randn(128 * n_noise) tx np.concatenate([ofdm_symbol() for _ in range(n_total_symbols - n_noise)]) tx signal.resample_poly(tx, 2, 1) # 与 synth_ofdm_frame 保持同采样率 rx np.concatenate([noise_part, tx]) noise np.random.randn(len(rx)) 1j * np.random.randn(len(rx)) signal_power np.mean(np.abs(rx) ** 2) noise_power signal_power / (10 ** (snr_db / 10)) rx rx noise * np.sqrt(noise_power / 2) f, t, Zxx signal.stft(rx, fs2, nperseg128, noverlap96) return np.abs(Zxx).astype(np.float32)这类边界帧的使用方式不是拿来训练而是留作部署前测试。一个合格的 OFDM 频谱感知模型在边界帧上的输出应该快速地从 0 翻到 1翻转时间不超过两个 STFT 窗即大约 64 个采样点如果输出出现多次往返抖动说明模型把时间结构当成了可忽略的噪声需要回到数据增强里加强时间平移。提示评估时把 0.5 当成唯一阈值会低估模型能力。正确做法是在验证集上扫一遍阈值画出 ROC 曲线按目标虚警率比如 1%取对应阈值。在软件无线电部署里虚警率指标往往由上级协议栈指定阈值校准这一步不能省。5. 把感知从「有无」推进到 OFDM / OTFS / FMCW 波形识别频谱感知做到能检测有无之后下一步自然是想知道「这个信号具体是什么」。OFDM、OTFS 和 FMCW 在时频图上的纹理差别足够明显OFDM 是平行等间隔子载波条纹OTFS 在时延-多普勒域有稀疏冲激但映射回时频图会呈现更细的近似随机散布FMCW 则是一根连续扫频的亮线。同一个 CNN 骨干换掉分类头就能从二分类变成三分类识别。新类别样本量不足时不要从头训练。加载前面训好的二分类模型权重冻结特征层只微调分类头net3 OFDMSpecCNN(n_class3) net3.features.load_state_dict(net.features.state_dict()) # 载入已有权重 for p in net3.features.parameters(): p.requires_grad False # 替换最后的线性层 net3.classifier[-1] nn.Linear(256, 3) optimizer torch.optim.Adam(net3.classifier.parameters(), lr1e-2) # 只优化分类头训练 10 个 epoch 即可每类波形只需几百个样本就能收敛因为底层的频谱纹理特征在两类任务之间完全复用。如果后续需要处理连续帧序列把单帧时频图按时间顺序堆成 3D 张量换上 3d 卷积神经网络去建模帧间状态转移也是同一条技术路线的自然延伸。部署前的最后一道验证是把模型导出成 ONNX 并跑一次前后端一致性校验。PyTorch 训练时的计算图和 ONNX Runtime 的算子实现可能存在微小差异不做校验直接量化很容易把浮点误差放大成误判。import onnxruntime as ort import torch x_dummy torch.randn(1, 1, 128, 128) torch.onnx.export(net3, x_dummy, ofdm_spec.onnx, input_names[input], output_names[logits], opset_version13, dynamic_axes{input: {0: batch}}) sess ort.InferenceSession(ofdm_spec.onnx, providers[CPUExecutionProvider]) with torch.no_grad(): ref net3(x_dummy).numpy() onnx_out sess.run(None, {input: x_dummy.numpy()})[0] diff np.max(np.abs(ref - onnx_out)) print(fmax diff: {diff:.2e})差值小于 1e-5 就说明导出链路没有问题。之后再对 ONNX 模型做 FP16 或 INT8 量化并用同一批边界帧样本复查一次检测概率与虚警率这是我在把模型交给嵌入式设备前固定要跑的 sanity check。本文还有配套的精品资源点击获取