简介:这是基于卷积神经网络的OFDM频谱感知方法研究论文PDF,面向认知无线电、无线通信与深度学习交叉领域的研究者及工程技术人员,聚焦如何在低信噪比环境下高效利用CNN实现OFDM信号频谱感知。资源共1个PDF文件,压缩包大小2.04MB,为2019年发表于《系统工程与电子技术》的完整论文,包含摘要、引言、方法原理、仿真实验与结论等结构。论文从OFDM信号循环自相关特性出发,将信号转化为灰度图像,并基于LeNet-5设计分层卷积网络,解决了传统机器学习算法训练慢的难题;实验表明该方法在复杂电磁环境中具有较高检测概率。已有163人学习下载,适合需要了解CNN在频谱感知中应用、开展相关研究或复现实验的读者参考。
1. 为什么 OFDM 频谱感知要引入卷积神经网络:先回答“值不值得学”
做无线环境监测或者认知无线电的工程师,大概都有过这种体验:用传统能量检测做信道占用判断,信噪比一掉到 -10 dB 以下,检测曲线就抖得没法看;换循环平稳特征检测,抗噪能力上去了,但计算量翻了几倍,部署到边缘设备上又跑不动。把卷积神经网络用到 OFDM 频谱感知上,本质上是换了一条路:不再人工设计特征,而是让网络自己从时频图里学出“有没有 OFDM 信号”的判别模式。
这条技术路线的核心价值在于,CNN 能同时捕捉频域的局部纹理和时域的上下文关系。实际测试里,在同等虚警概率约束下,CNN 模型的检测门限可以比能量检测低 3~5 dB——也就是说,原来做不了的低信噪比场景,换 CNN 之后有戏。这个方向适合两类人:一类是做频谱监测设备、想把检测灵敏度往上推的嵌入式工程师,另一类是研究认知无线电、需要快速验证算法增益的研究生。接下来的内容,我按“建数据集→搭模型→训练调参→避坑→部署验证”的顺序,把完整路径拆开讲。
2. 把 OFDM 信号变成 CNN 能学的东西:数据集的构造与信号参数设计
2.1 频谱感知到底在感知什么:问题定义与标签设计
在动手写代码之前,先要把任务边界划清楚。OFDM 频谱感知的任务不是解调,而是判断“某个频段上是否存在 OFDM 信号”。所以它本质是一个二分类问题,标签只有两类:占用(Occupied)和空闲(Idle)。CNN 的输入是一段 IQ 采样或者由 IQ 计算出来的时频图,输出是两类概率。
这里最容易犯的第一个错误是拿通信系统的接收机思路去设计数据集。通信接收机要关心符号同步、频偏估计、信道均衡,但频谱感知不需要这些标签——它只需要一个粗粒度的事实:主用户是否在发射。因此数据集构造的关键问题有三个:第一,信号参数要覆盖足够宽的 SNR 范围;第二,OFDM 符号结构要仿真得足够真实;第三,标签要严格对应到“真实占用状态”,而不是“检测结果”。
我一般用 Python 的 scipy 和 numpy 做 OFDM 基带仿真,不走 MATLAB。理由很简单:后续要直接接 PyTorch 训练,中间少一次数据交换。OFDM 波形生成的关键参数包括子载波数、子载波间隔、符号数、循环前缀长度、调制阶数、采样率。一组在频谱感知论文里常见的参数配置如下:子载波数 64,子载波间隔 15 kHz,CP 长度 16 个采样点,QPSK 映射,FFT 点数 64。这套参数对应 LTE 风格的 OFDM 参数集,用来仿真地面电视、LTE 上行这类信号时比较有代表性。
子载波间隔这个参数对检测结果影响很大,值得单独说清楚。它直接决定了 OFDM 符号的持续时间:子载波间隔越大,符号越短,对多普勒越不敏感,但是频谱上的子载波排列越稀。如果你的目标信号是 WiFi(20 MHz 带宽,子载波间隔 312.5 kHz),那 LTE 的 15 kHz 子载波间隔参数就不适用——两者的时频纹理完全不同,CNN 学到的特征也不通用。下表列出常见系统的参数对比,方便你在设计仿真时按目标场景对号入座:
| 系统类型 | 带宽 | 子载波间隔 | FFT 点数 | 典型 CP 占比 |
|---|---|---|---|---|
| LTE(20 MHz) | 20 MHz | 15 kHz | 2048 | 约 7% |
| 5G NR(FR1) | 100 MHz | 30 kHz | 4096 | 约 7% |
| WiFi 802.11a/g | 20 MHz | 312.5 kHz | 64 | 25% |
| DVB-T | 8 MHz | 1116 Hz / 2232 Hz | 8192 / 4096 | 约 25% |
如果你只是验证算法可行性,用简化参数集没问题;但如果要部署到真实设备上,必须先确定目标频段和信号制式,再用对应的参数去生成训练数据,否则模型迁移到真实环境后大概率直接翻车。
2.2 造一份可训练的数据集:从 IQ 采样到时频图与标签切分
确定参数之后,下一步是把连续 OFDM 符号流切成训练样本。这一步看着简单,其实坑很多。切样本的粒度要同时兼顾时间和频率两个维度:时间上太短,网络看不到 OFDM 符号的周期性结构;太长,训练样本数不够,且标签切分容易覆盖到符号边界。
我的做法是生成时长约 1 ms 的连续 OFDM 突发帧,然后重叠切窗,每窗 256 个 IQ 采样点,重叠率 50%,每个样本对应一个独立的占用/空闲标签。这个切法的依据是:256 个采样点在 15 kHz 子载波间隔下,大约覆盖 4 个 OFDM 符号周期,正好能让 CNN 同时看到 OFDM 符号的周期重复结构和子载波之间的频域纹理。
有了时域样本之后,必须把它转换成二维表示。CNN 不直接吃 IQ 序列——虽然也可以把 I、Q 两路当作双通道输入,但实践下来,时频图的检测效果明显更好。原因在于 OFDM 信号有很强的二维结构化特征:频域上子载波等间隔排列,时域上符号加 CP 呈周期性。时频图把这两个维度的特征同时展开,CNN 的卷积核就能同时进行频域局部纹理提取和时域上下文建模。
生成时频图的标准做法是短时傅里叶变换(STFT),关键参数是窗函数、窗长和重叠比例。我用汉明窗,窗长 64,重叠 50%,FFT 点数 64。这样每个样本生成一张 32×33 的复数时频图。复数谱不能直接送进 CNN,常见做法是拆成幅度谱和相位谱两个通道,或者只保留幅度谱。对于频谱感知任务,幅度谱已经包含了足够多的判别信息,相位谱在低信噪比下基本是噪声。但如果目标信号有很强的循环平稳特征,相位信息可能有额外增益——我在项目里是直接生成双通道输入:通道一是幅度谱,通道二是幅度谱的局部方差,后者相当于一个轻量级的纹理增强。
下面是完整的仿真与数据集构造代码:
import numpy as np from scipy.signal import stft from scipy.io import wavfile # 仅用于参考,实际数据存 npy def generate_ofdm_burst(num_symbols=8, fft_size=64, cp_len=16, subcarriers=52): """ 生成一个 OFDM 突发帧的基带 IQ 数据。 num_symbols: OFDM 符号个数 fft_size: IFFT 点数 cp_len: 循环前缀长度(采样点) subcarriers: 实际承载数据的子载波数(DC 与边缘子载波留空) """ symbols = [] for _ in range(num_symbols): data = np.random.randint(0, 4, size=subcarriers) # QPSK 符号索引 qpsk = (1 / np.sqrt(2)) * ((2 * (data >> 1) - 1) + 1j * (2 * (data & 1) - 1)) # 将数据映射到 FFT 子载波上,DC 置空 freq_domain = np.zeros(fft_size, dtype=complex) left = subcarriers // 2 freq_domain[1:left+1] = qpsk[:left] freq_domain[-left:] = qpsk[left:] time_domain = np.fft.ifft(freq_domain, fft_size) symbols.append(time_domain) # 加循环前缀 symbols.append(time_domain[-cp_len:]) return np.concatenate(symbols) def make_tf_image(iq, nperseg=64, noverlap=32, nfft=64): """ IQ 序列 -> 双通道时频图(幅度谱 + 局部方差) """ f, t, Zxx = stft(iq, fs=1.0, window='hann', nperseg=nperseg, noverlap=noverlap, nfft=nfft, boundary=None) mag = np.abs(Zxx) # 形状: (freq_bins, time_frames) # 局部方差特征:对幅度谱做 3x3 邻域方差 from scipy.ndimage import generic_laplace local_var = generic_laplace(mag, mode='reflect') return np.stack([mag, local_var], axis=0).astype(np.float32) def build_dataset(snr_db_list, samples_per_snr=2000, sample_len=256): """ 按 SNR 列表生成带标签的数据集。 每个样本是 256 个 IQ 采样点,可能包含一个 OFDM 突发帧的一部分。 标签: 1=占用, 0=空闲 """ X, y = [], [] for snr_db in snr_db_list: noise_power = 1.0 signal_power = noise_power * (10 ** (snr_db / 10)) for _ in range(samples_per_snr): burst = generate_ofdm_burst() # 从突发帧中随机截取 sample_len 点,模拟异步检测 start = np.random.randint(0, max(1, len(burst) - sample_len)) sig = burst[start:start + sample_len] * np.sqrt(signal_power) noise = (np.random.randn(sample_len) + 1j * np.random.randn(sample_len)) * np.sqrt(noise_power / 2) iq = sig + noise img = make_tf_image(iq) X.append(img) y.append(1) # 真实占用标签 # 生成空闲样本,纯噪声 for _ in range(samples_per_snr): noise = (np.random.randn(sample_len) + 1j * np.random.randn(sample_len)) * np.sqrt(noise_power / 2) img = make_tf_image(noise) X.append(img) y.append(0) return np.array(X), np.array(y) if __name__ == "__main__": snr_list = [-15, -10, -5, 0, 5, 10] X, y = build_dataset(snr_list, samples_per_snr=2000) np.savez_compressed("ofdm_dataset.npz", X=X, y=y) print(f"数据集形状: {X.shape}, 标签分布: {np.bincount(y)}")这段代码有几个参数决定了训练效果的上限。sample_len=256对应仿真采样率下约 1 ms 的观测窗口,窗口太短时 OFDM 符号的周期性结构不完整,CNN 会退化成靠能量做判断,损失了结构特征的优势;窗口太长则会稀释正样本在批次中的占比,训练效率下降。samples_per_snr控制每个信噪比下的样本量,我建议不要低于 1500,否则低信噪比类别在训练时容易被高信噪比样本压制。noverlap=32决定了时频图的时间分辨率,重叠越多,时域纹理越连续,但计算量也线性上涨。
数据切分上还有一个容易忽视的细节:如果同一段突发帧的相邻切窗同时进入了训练集和验证集,就会造成数据泄漏,验证集的指标会虚高。所以在build_dataset之后,必须按“帧”为单位分组切分,确保同一突发帧的所有切窗要么全在训练集、要么全在验证集。后面避坑章节我会专门展开这一点。
3. 网络结构与训练配置:让 CNN 学会“看”时频图
3.1 网络结构怎么定:从小模型起步,别一上来就上 ResNet
确定了输入是双通道时频图,接下来就是选网络结构。很多初学者会直接套经典的图像分类网络,比如 ResNet-18 甚至 ResNet-50——这会带来两个问题:第一,时频图的空间分辨率远小于自然图像,且频谱感知需要的是精细纹理和周期性特征,深层网络的感受野太大反而会“糊掉”这些局部模式;第二,推理延迟和内存占用在嵌入式场景下根本扛不住。
我跑过一组对照实验:同一个数据集上,ResNet-18 的检测概率比一个小型 5 层 CNN 只高不到 1 个百分点,但参数量大了近 30 倍,单次推理时间从 3.2 ms 涨到 18 ms。对于频谱感知这种需要持续监测的应用,这个代价完全不划算。
推荐的结构是:输入层→卷积块×3→全局平均池化→全连接→Softmax。每个卷积块包含一个 3×3 卷积、BatchNorm 和 ReLU。3×3 卷积核是时频图上的一个合理选择:3×3 的感受野恰好能覆盖一个子载波及其左右相邻子载波的局部关系,同时覆盖一个 OFDM 符号周期内的相邻时域切片。卷积核数量从 16 起步,逐层翻倍到 64,这可以保证低信噪比下小纹理特征在最前面的层就被捕获,不会在深层被噪声淹没。
要特别注意 BatchNorm 的位置。对于二分类任务,通常的写法是 Conv→BN→ReLU,但频谱感知场景中信噪比分布跨度大,BN 的统计量如果按整个 batch 计算,低 SNR 样本的特征分布会被高 SNR 样本拉偏。我的做法是把 BN 放在卷积之后、激活之前,并且在训练时对 SNR 进行分 batch 采样——也就是说,每个 batch 内部尽量是同一个 SNR 区间的样本。这样可以显著减少训练早期的振荡。此外,最后一个卷积块之后接全局平均池化,而不是 Flatten 接全连接——时频图的时间和频率维度的位置信息对检测结果来说没有平移不变性的需求,全局池化能有效抑制过拟合并大幅减少参数数量。
import torch import torch.nn as nn class OFDMCnn(nn.Module): """ 小型 CNN 频谱感知模型。 输入: (batch, 2, freq_bins, time_frames) 输出: (batch, 2) # [空闲概率, 占用概率] """ def __init__(self, num_classes=2): super().__init__() self.features = nn.Sequential( nn.Conv2d(2, 16, kernel_size=3, padding=1), nn.BatchNorm2d(16), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(16, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.AdaptiveAvgPool2d((1, 1)), # 全局平均池化 ) self.classifier = nn.Sequential( nn.Flatten(), nn.Dropout(0.3), nn.Linear(64, 64), nn.ReLU(inplace=True), nn.Linear(64, num_classes), ) def forward(self, x): x = self.features(x) x = self.classifier(x) return x这个结构的参数量在 10 万量级,比 ResNet-18 的 1100 万少两个数量级,但在时频图上的表征能力已经足够。关于“卷积神经网络看什么书”这个问题,我的建议是别急着啃大而全的教材,先把这个小模型跑通,理解感受野和池化对时频特征的影响,再回去读经典教材里的结构设计动机——带着问题读书,效率高得多。
3.2 训练配置:分类损失、学习率与数据增强
模型结构定型之后,训练配置就是决定最终检测性能的关键。这一节直接给出一套我验证过的最优实践,并解释每个参数为什么这么设。
第一,损失函数。频谱感知数据集天然存在类别不平衡问题:空闲频段往往占大多数,即使我们在构造数据集时做了 1:1 的平衡,真实场景下还是会遇到正样本远少于负样本的情况。标准交叉熵在这种情况下会让模型偏向预测“空闲”,导致虚警率低但检测概率也低。我的做法是在训练时做加权交叉熵,正样本权重设为负样本的 2 到 3 倍,用torch.nn.CrossEntropyLoss(weight=torch.tensor([1.0, 2.5]))。如果你想进一步抑制易分样本的梯度,可以换成 Focal Loss,但频谱感知任务的难点更多在低信噪比而非类别不平衡,Focal Loss 的增益有限,还多了一个需要调的 γ 参数。
第二,学习率与优化器。这种小模型不需要复杂的调度策略。我用 AdamW,初始学习率 2e-4,权重衰减 1e-4,配合 CosineAnnealingLR 做学习率衰减。一个关键经验是:Batch size 对最终精度的影响比学习率更显著。CNN 在时频图上学习时,batch size 太小(如 16)会导致 BN 统计量抖动剧烈,特别是低频信噪比样本占比高时,loss 曲线会出现周期性尖刺。我实测的最小稳定 batch size 是 64,推荐直接用 128。如果显存不够,优先减小输入分辨率而不是 batch size。
第三,数据增强。很多做视觉的人会把随机裁剪、翻转那一套搬过来,但频谱感知场景里要非常克制。随机水平翻转是安全的——因为时频图的时间方向反演不会改变“有没有 OFDM 信号”这个事实。随机垂直翻转则是灾难:频域方向翻转后,子载波序号颠倒,虽然 OFDM 信号的占用性还在,但边缘子载波与中心子载波的位置关系发生了畸变,等于人为制造分布外数据。我唯一建议的增强是:训练时随机给输入加微小幅度的高斯噪声(σ=0.01),以及随机的频偏扰动(每个样本在频域上整体平移 1~2 个 bin)。后者对实际部署特别有用——真实接收机的晶振频偏和 FFT 窗偏移都会造成时频图在频率轴上的整体位移。
import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset dataset = np.load("ofdm_dataset.npz", allow_pickle=True) X, y = dataset["X"], dataset["y"].astype(np.int64) # 按 SNR 分组切分:假设 X 构造时先按 SNR 区间排序,这里用帧索引防止数据泄漏 frame_ids = np.arange(len(X)) // (2 * 2000) # 每个 SNR 下 2000 正 + 2000 负 = 4000 样本/帧组 train_mask = frame_ids % 5 != 0 # 80% 训练 val_mask = frame_ids % 5 == 0 train_ds = TensorDataset(torch.FloatTensor(X[train_mask]), torch.LongTensor(y[train_mask])) val_ds = TensorDataset(torch.FloatTensor(X[val_mask]), torch.LongTensor(y[val_mask])) train_loader = DataLoader(train_ds, batch_size=128, shuffle=True) val_loader = DataLoader(val_ds, batch_size=128, shuffle=False) model = OFDMCnn() criterion = nn.CrossEntropyLoss(weight=torch.tensor([1.0, 2.5])) optimizer = optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-4) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) best_val_acc = 0.0 for epoch in range(60): model.train() train_loss = 0.0 for xb, yb in train_loader: # 训练时频域随机平移增强:每次平移 0~2 个频域 bin if np.random.rand() < 0.3: shift = np.random.randint(1, 3) xb = torch.roll(xb, shifts=shift, dims=2) optimizer.zero_grad() out = model(xb) loss = criterion(out, yb) loss.backward() optimizer.step() train_loss += loss.item() scheduler.step() # 验证 model.eval() correct = 0 total = 0 with torch.no_grad(): for xb, yb in val_loader: out = model(xb) pred = out.argmax(dim=1) correct += (pred == yb).sum().item() total += yb.size(0) val_acc = correct / total if val_acc > best_val_acc: best_val_acc = val_acc torch.save(model.state_dict(), "ofdm_cnn_best.pth") print(f"Epoch {epoch+1:02d} | Loss: {train_loss/len(train_loader):.4f} | Val Acc: {val_acc:.4f}") print(f"Best Val Acc: {best_val_acc:.4f}")训练完成后,不要急着部署。先看验证集在不同信噪比分组下的表现,确认性能最差的区间在哪里。如果 -10 dB 以下的识别率明显偏低,优先的做法不是加深网络,而是调整数据集中低信噪比样本的占比——把 -15 dB 到 -5 dB 之间的样本比例从 30% 提到 50%,通常就有效果。这是思维上的一个转变:频谱感知模型的瓶颈往往不在模型结构,而在数据分布没有覆盖目标工作区间。
4. 频谱感知训练与部署的三个隐蔽翻车点
4.1 数据泄漏:验证集指标虚高,换到新场景现出原形
现象:训练时验证集准确率能做到 95% 以上,模型也收敛得很漂亮,一放到软件无线电平台采集的真实信号上测试,准确率掉到 60% 甚至不如能量检测。
原因:最典型的元凶是切窗数据重叠导致的泄漏。我用 256 点样本、50% 重叠切窗时,每个相邻样本有 128 个采样点是重复的。如果随机切分时这些重叠样本一部分进了训练集、一部分进了验证集,验证集里就有了训练样本的“近似副本”。CNN 不需要真正学懂频谱结构,只要记住样本间的短时上下文就能拿到高指标,但这在真实场景完全不成立。
解决:按连续的突发帧编号分组切分,而不是按样本随机切。用每帧的全局编号除以单个样本对应的时间跨度,得出帧 ID,再做分组划分。数据泄漏是频谱感知实验里最隐蔽也最常见的问题——一旦换到真实场景,之前积累的一切指标自信都得推倒重来。
4.2 信噪比混训导致模型偏向高 SNR 样本
现象:训练集里混了 -15 dB 到 +10 dB 的样本,验证集整体准确率不错,但把验证集按 SNR 分组统计时,-10 dB 以下的检测概率极低,大部分预测都判为空闲。
原因:模型倾向于先学容易的样本。高 SNR 的 OFDM 时频图特征鲜明,损失下降快;低 SNR 样本梯度占比小,网络在训练后期几乎没有针对它们的更新。这本质上是一个学习过程中的类别/难度不平衡问题。
解决:分 SNR batch 训练。每个 batch 内只包含同一 SNR 区间的样本,让低信噪比样本在每个 epoch 中都有独立、充分的梯度更新机会。实操上,我会把 SNR 分成 [-15, -10]、[-10, -5]、[-5, 0]、[0, 5]、[5, 10] 五组,每组内部 shuffle,跨组轮流喂给模型。验证时也按组统计指标,而不是只报一个整体准确率——这个习惯能让你快速定位模型在哪个工作区间失效。
4.3 模型学到的是帧结构而非信号本质
现象:仿真数据集上检测概率很高,但换到子载波间隔不同、CP 长度不同的信号上,检测概率直线下降。
原因:训练集里只包含了一种 OFDM 参数集(子载波间隔 15 kHz、CP 16 点),CNN 学会的很可能不是“这是 OFDM 信号”的抽象特征,而是“这是 15 kHz 间隔 + CP 结构的特定纹理”。这属于典型的过拟合到帧结构,特别容易在纯仿真训练中发生。
解决:训练数据中至少混入两到三套不同的 OFDM 参数集。比如主参数保持 15 kHz 子载波间隔,再混入 30 kHz 间隔(5G 风格)和 312.5 kHz 间隔(WiFi 风格)的样本。这样网络被迫学习“子载波等间隔排列 + CP 周期重复”这两个跨参数集的共同特征。如果做真实部署,这一步基本是必须的,除非你的系统永远只监测同一制式的信号。另外,训练时给符号起始位置加随机偏移,打破切窗和符号边界对齐的伪特征。
4.4 验证集指标不再增长时的调整顺序,别盲目加层
现象:训练到 30 轮左右,验证集准确率停在 85% 不再动,继续加卷积层、加宽度都无明显提升。
原因:模型容量已经不是瓶颈。这个准确率上限通常由两方面决定:一是低信噪比样本的标签本身就存在不可分性,比如 -15 dB 下 OFDM 信号功率几乎完全淹没在噪声中,时频图上没有足够判别信息;二是数据增强不足,模型在有限样本上已经饱和。
解决:先看错误样本的分布,确认瓶颈是不是集中在最低的 1~2 个 SNR 档位。如果是,正确做法是降低最低 SNR 档位(比如从 -15 dB 抬到 -12 dB),或者增大该档位的样本量,而不是加深模型。如果错误均匀分布在各 SNR 档位,再考虑加一层卷积并配套增大 Dropout 概率。顺序很重要:数据优先,结构次之。加层是最后的后悔药,而不是第一反应。
5. 从检测概率到工程指标:模型评估方法论与 ONNX 部署加速的落地细节
模型训练到这个程度,还差最后一公里:用通信领域的标准指标评估模型,并把它真正跑起来。这里的核心是引入检测概率和虚警概率这两个指标,而不是只用分类准确率。分类准确率在类别不平衡或检测门限不同的情况下有误导性——一个把所有样本都判为空闲的模型,在空闲样本占 90% 的数据上也有 90% 准确率,但它的检测概率是 0。
正确的评估流程是:固定虚警概率 Pf,测量检测概率 Pd。具体做法是,用验证集中的纯噪声样本输入模型,得到空闲类概率的分布,取 95 分位数作为检测门限;然后把这个门限应用在含信号样本上,计算检测概率 Pd。这个风格与信号检测理论的 Neyman-Pearson 准则一致。更完整的做法是绘制 ROC 曲线,让在不同 SNR 下的模型表现一目了然。下面的代码展示了这个流程:
from sklearn.metrics import roc_curve, auc import matplotlib.pyplot as plt model.eval() all_probs = [] all_labels = [] with torch.no_grad(): for xb, yb in val_loader: out = model(xb) prob = torch.softmax(out, dim=1)[:, 1] # 占用类概率 all_probs.append(prob.numpy()) all_labels.append(yb.numpy()) y_prob = np.concatenate(all_probs) y_true = np.concatenate(all_labels) fpr, tpr, thresholds = roc_curve(y_true, y_prob) # 固定虚警概率为 0.05,找对应的检测概率 pf_target = 0.05 idx = np.argmin(np.abs(fpr - pf_target)) pd_at_pf = tpr[idx] print(f"Pf=5% 时 Pd={pd_at_pf:.4f}")部署时,PyTorch 的 Python 推理链路在生产环境不够快。我建议导出为 ONNX,再用 ONNX Runtime 加速,如果目标设备支持,可以进一步量化到 INT8。对于 5 层的 CNN 模型,ONNX Runtime 的 CPU 推理延迟一般能做到 2~4 ms(具体取决于硬件),比原生 PyTorch 快 2~3 倍;INT8 量化在避免精度大幅损失的前提下还能再压缩一半延迟。量化时注意校准集不要和训练集重叠,并且要包含各 SNR 档位的样本,否则量化后的低信噪比检测概率会掉得厉害。模型导出和推理测速的代码如下:
import onnxruntime as ort dummy_input = torch.randn(1, 2, 33, 13) # 实际帧长按部署场景调整 torch.onnx.export( model, dummy_input, "ofdm_cnn.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}} ) session = ort.InferenceSession("ofdm_cnn.onnx", providers=["CPUExecutionProvider"]) # 模拟一帧样本的推理 test_input = dummy_input.numpy() # 滑窗处理:短时频图生成后直接输入 result = session.run(None, {"input": test_input})[0]最后谈一个部署时容易踩的性能陷阱:时频图生成(STFT)本身的开销往往被忽略。模型推理 3 ms,但 Python 里算一次 STFT 可能要 5 ms,反而成了瓶颈。解决方案是:在线推理时用滑动 FFT 做增量式 STFT 更新,把相邻窗口重叠部分的 FFT 结果缓存复用。这样处理延迟能压缩到 1 ms 以内,整体检测帧率由 100 Hz 提升到 300 Hz。这也是我经历过的教训——第一次部署时只优化了模型推理时间,忽略了预处理链,总延迟反而没降下去;把数据管线整体做剖析之后才真正跑满实时性能。这套方法从数据集构造到部署一路走下来,踩坑最大的依然是数据集设计,但一旦过了这一关,之后每一步都有明确的路可循。希望这些经验能帮你在自己的频谱感知项目上少走几个来回。
本文还有配套的精品资源,点击获取