基于卷积神经网络与STFT时频图的OFDM频谱感知方法
2026/9/19 12:10:26 网站建设 项目流程

简介:一份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_data=192, n_carriers=256, cp_len=32): # 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, dtype=complex) 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_symbols=14, n_carriers=256): # 连续拼接 n_symbols 个 OFDM 符号 tx = np.concatenate([ofdm_symbol(n_carriers=n_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, fs=2, nperseg=128, noverlap=96) return np.abs(Zxx).astype(np.float32) def synth_dataset(n_frames=2000, snr_db=0): 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_symbols=0)) # 纯噪声帧 y.append(0) # 无信号 return X, y

代码里的几个点值得说明。OFDM 符号的 CP 长度 n_carriers//8,子载波利用率是 192/256=75%,这是工程里常见的配置;实际系统里换成 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 的窗长和重叠率决定了时频图的分辨率。nperseg=128、noverlap=96 时频率分辨率大约是 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, padding=1)128×128×163×3 卷积提取局部频谱纹理
BatchNorm2d + ReLU + MaxPool(2)64×64×16缩小特征图,增强平移鲁棒性
Conv2d(16, 32, 3, padding=1)64×64×32第二层卷积开始组合纹理基元
BatchNorm2d + ReLU + MaxPool(2)32×32×32感受野覆盖约 10 个子载波宽度
Conv2d(32, 64, 3, padding=1)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 两类 logits

AdaptiveAvgPool 是这类小型网络里值得保留的一个设计:训练时输入固定为 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_class=2): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 16, 3, padding=1), nn.BatchNorm2d(16), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(16, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.AdaptiveAvgPool2d((8, 8)) ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64 * 8 * 8, 256), nn.ReLU(inplace=True), 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, alpha=0.3, gamma=1.5): super().__init__() self.alpha = alpha self.gamma = gamma self.ce = nn.CrossEntropyLoss(reduction='none') 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, dtype=torch.float32), torch.full_like(target, 1 - self.alpha, dtype=torch.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, size=128): out = np.zeros((len(X), 1, size, size), dtype=np.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_frames=1200, snr_db=0) # 实际请按 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, dtype=torch.long)) loader = DataLoader(dataset, batch_size=64, shuffle=True)

训练循环里需要做两件 OFDM 场景特有的事:一是每个 epoch 重新用不同 SNR 合成数据,而不是把某个固定 SNR 的数据集反复用;二是每 5 个 epoch 按 SNR 分段评估一次,避免「看起来收敛、换到弱信号就崩」。SNR 在 -10 dB 到 10 dB 之间均匀随机抽取,每次合成的帧都不同,等于做了无限数据增强。

超参数取值说明
优化器Adamlr=1e-3,weight_decay=1e-4
学习率1e-3 起步,每 10 轮 ×0.5StepLR 对这类小数据集足够
Batch size64显存允许可加到 128,BN 更稳
训练轮数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), dim=1) 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(f"SNR {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_symbols=20): 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, fs=2, nperseg=128, noverlap=96) 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_class=3) 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(), lr=1e-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_version=13, 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(f"max diff: {diff:.2e}")

差值小于 1e-5 就说明导出链路没有问题。之后再对 ONNX 模型做 FP16 或 INT8 量化,并用同一批边界帧样本复查一次检测概率与虚警率,这是我在把模型交给嵌入式设备前固定要跑的 sanity check。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询