ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于CNN的数字调制识别:从IQ采样到神经网络实现

基于CNN的数字调制识别:从IQ采样到神经网络实现 简介这是一份面向通信工程与信号处理学习者的数字调制识别MATLAB实现资源聚焦利用神经网络从时域/频域特征中识别ASK、FSK、PSK等调制类型适合课程设计、课题研究或通信系统解码等场景尤其适合希望快速上手神经网络信号识别的初学者。压缩包内仅含1个.m脚本文件整体约3KB代码精简便于阅读与修改该资源已有343人学习/下载。通过运行该脚本读者可以体验从信号预处理、特征提取到神经网络模型构建、训练优化与测试评估的完整流程直观理解卷积神经网络和循环神经网络在调制识别任务中的特征映射与分类过程。脚本基于MATLAB编写可作为数字信号自动识别方向的轻量级参考方便学习者结合自身数据调整网络结构与参数复现和对比不同调制方式的识别效果为后续深入研究提供基础代码与思路。1. 数字调制识别落到神经网络并不只是为了省事做调制识别的人多在频谱监测或认知无线电里遇到过同样的问题同样一段QPSK信噪比从15dB掉到3dB传统特征向量就开始分不清。我第一次用高阶累积量加SVM做盲识别时白噪声下还说得过去遇到一点载波频偏识别率立刻往下掉。换成神经网络直接吃IQ采样模型把频偏补偿、符号同步这些本该放在前面的环节隐式塞进了卷积层权重里这不是省事而是把信号识别从人工特征工程中解放出来。这里要讲的“数字调制识别”是给定一段基带IQ序列判断它是BPSK、QPSK、8PSK、16QAM还是64QAM。“神经信号”不是脑电而是数据在网络中流动时各层产生的中间激活值把它们当信号来观察就能看出模型在用哪些线索。新手可以按流程复现熟手可以直接跳到参数表和排错部分。2. 从IQ采样到调制识别把信号问题摆成神经网络能吃的形状数字调制识别的输入通常不是解调后的01比特而是一段离散复基带信号 (x(n)I(n)jQ(n))。无论你打算用CNN、RNN还是Transformer都要先把这段复信号变成网络能吃的张量。这个环节做不好后面换什么网络都白搭。2.1 数字调制识别的问题定义以及“神经信号”指的是什么把问题收窄一点给定一段长度为 (N) 的基带IQ采样采样点之间按符号速率排列模型输出它属于哪个调制类别。这里只做粗识别不做码元级解调也就是说模型不关心每个符号具体是0还是1只关心“这类信号用的是哪种幅度/相位/频率组合”。调制样式差异体现在IQ轨迹的形状上BPSK只有两个相位簇QPSK有四个16QAM则形成方阵。神经网络要学的就是不同调制样式在IQ平面上的几何分布和时序相关性。标题里“神经信号”这个词在中文技术社区里经常和神经网络识别并列出现。它并不是指生物电信号而是指神经网络内部多个卷积核输出的激活值张量。比如第一层卷积输出形状是[batch, channel, length]每个channel可以看作一路“神经信号”它们和输入IQ一样有时序结构可以被可视化、被统计分析。我会在第三章专门讲怎么把这层信号拉出来看因为很多CSDN上讨论调制识别模型的帖子最终都会回到“模型到底学到了什么”这个问题上而“神经信号”就是回答这个问题的最直接抓手。2.2 输入表示怎么选IQ序列、星座图还是频谱图常见做法有三种各有适用场景。下表是我的选型习惯输入表示数据形态保留的信息主要缺点推荐网络IQ原始序列[N, 2]或[2, N]相位连续性、时序关系对频偏和时间同步敏感1D CNN / LSTM星座图/密度图[H, W]灰度图幅度-相位分布丢失时间先后顺序2D CNN语谱图/功率谱[F, T]二维图频域能量分布相位信息被抹掉2D CNN我一般首选IQ原始序列原因是调制识别的关键特征大量集中在相位跳变上而IQ序列天然保存了相位连续变化的过程。星座图也没有错尤其当符号率未知、样本里包含多个符号周期的时星座图的统计分布反而比原始波形更稳定。下面这一段用Python做两件事先把IQ归一化再把IQ投影成星座密度图。import numpy as np def normalize_iq(iq): # iq: (N, 2)第0列是I路第1列是Q路 mu iq.mean(axis0, keepdimsTrue) sd iq.std(axis0, keepdimsTrue) 1e-6 return (iq - mu) / sd def iq_to_constellation(iq, size256, scope3.0): # 把IQ投影到二维直方图上得到密度图 iq_clip np.clip(iq, -scope, scope) hist, _, _ np.histogram2d( iq_clip[:, 0], iq_clip[:, 1], binssize, range[[-scope, scope], [-scope, scope]] ) # 用log1p压低高密度区域的数值差异 img np.log1p(hist) img img / img.max() * 255.0 return img.astype(np.uint8)这段代码里normalize_iq把I/Q分量各自减去均值再除以标准差目的是消除接收机增益差异和直流偏置。scope要按信号幅度经验值设置通常取3倍标准差太小会把星座簇截断太大图上的簇会缩在一起。histogram2d把大量IQ点压缩成固定尺寸的密度图比直接画散点图更适合作为卷积输入也避免了一个样本里点数不足导致图上稀疏的问题。2.3 数据集划分按信噪比分桶不要随机切窗口这里有一个隐蔽的坑如果一段连续IQ信号被切成多个窗口再把这些窗口随机分到train和validation那么训练集和验证集里会存在大量相邻窗口它们之间的相关性很强验证准确率会虚高。正确做法是先按“信号记录/文件”分组再按组划分。from sklearn.model_selection import train_test_split # file_ids 表示每个样本来自哪个原始记录文件 file_ids np.array([sample[file_id] for sample in samples]) train_files, test_files train_test_split( np.unique(file_ids), test_size0.2, random_state0 ) train_mask np.isin(file_ids, train_files) test_mask np.isin(file_ids, test_files)train_test_split作用在np.unique(file_ids)上保证同一个记录文件的全部窗口只进入训练集或只进入测试集。如果数据本身带有信噪比标签我还会按信噪比分层切分避免高信噪比样本几乎全在测试集里导致指标虚低或虚高。3. 用CNN识别调制方式的网络设计和损失函数神经网络识别调制类型核心是把时序信号压缩成一个类别分布。这里给出一个轻量级但能跑通的1D CNN以及对应的损失函数和中间特征观察方法。这三种代码组合起来就是一个可以直接套用的调制识别最小系统。3.1 1D CNN怎么接IQ输入给出可复现的PyTorch网络输入形状用[B, 2, L]其中第二个维度是I/Q两个通道L是采样点数。网络先用一个步长为1的7点卷积核提取局部波形模式再叠加5点卷积核做二次特征提取池化层把长度逐步压缩最后用全局平均池化得到固定维度的向量。import torch.nn as nn class IQCNN1D(nn.Module): def __init__(self, num_classes8): super().__init__() self.stem nn.Sequential( nn.Conv1d(2, 32, kernel_size7, padding3), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2) ) self.body nn.Sequential( nn.Conv1d(32, 64, kernel_size5, padding2), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2) ) self.head nn.Sequential( nn.AdaptiveAvgPool1d(1), nn.Flatten(), nn.Linear(64, num_classes) ) def forward(self, x): return self.head(self.body(self.stem(x)))第一层kernel_size取7而不是常见的3或5是因为数字调制信号在一个符号周期内通常有4到16个采样点7个采样点能在不跨越多个符号的前提下覆盖更多波形形态。padding使用padding3保持时序长度不变便于后续池化。AdaptiveAvgPool1d(1)把任意长度输入压缩成单一时间点使网络不要求固定输入长度但实际训练时还是建议统一长度否则batch内维度会冲突。3.2 网络里的“神经信号”用hook把中间层激活拉出来调模型时光看准确率不够还要看中间层有没有学出和星座结构相关的特征。PyTorch的forward hook可以不打乱网络结构就拿到某层输出这就是我调试“神经信号”的方式。feats {} def make_hook(name): def hook_fn(module, input, output): feats[name] output.detach() return hook_fn model.stem[0].register_forward_hook(make_hook(stem_conv)) # 推理 with torch.no_grad(): logits model(iq_batch) print(feats[stem_conv].shape)输出形状通常是[B, 32, L]可以把它看成32路并行的“神经信号”。如果某一channel的输出几乎全是0说明对应卷积核可能死掉了如果所有channel的幅度都集中在同一个尺度上说明BatchNorm之前可能存在饱和。经验上第一层卷积输出里应当能看到一部分channel对幅度和相位跳变特别敏感它们的激活峰值往往和符号切换点对齐。3.3 损失函数交叉熵配标签平滑不推荐直接MSE调制识别是离散类别分类最直接的是交叉熵。但当类别数多、类间相似度高时模型容易对训练集过于自信logits出现极端值。这种情况下我会开标签平滑。import torch def label_smooth_loss(logits, labels, smoothing0.1): n logits.size(1) with torch.no_grad(): target torch.zeros_like(logits).scatter_( 1, labels.unsqueeze(1), 1.0 ) target target * (1.0 - smoothing) smoothing / n log_probs torch.log_softmax(logits, dim1) return -(target * log_probs).sum(dim1).mean()smoothing0.1的含义是正确类别目标概率不再是硬编码的1而是0.9 0.1/n其余类别分到0.1/n。这么做等于告诉模型“不要过度相信训练标签里完全没有噪声”在低信噪比样本多的数据集上能减少过拟合。需要特别提醒标签平滑不是越大越好我一般从0.05试起超过0.2会让模型欠拟合。4. 训练配置、参数表和排错清单有了网络和损失函数下一步是把训练跑稳。调制识别任务的训练和图像分类没有本质区别但因为信号数据带SNR特性、I/Q通道顺序和归一化问题很容易出错所以排错清单比调参本身更重要。4.1 训练参数速查表下面这组参数可以当成起点先用小模型跑通再根据设备显存和数据量调整。参数项建议值说明输入长度1281024点每个样本至少包含16个符号周期优化器AdamW比SGD在低信噪比任务上收敛更稳初始学习率1e-3输入归一化做好的前提下适用学习率调度Cosine Annealing避免训练后期loss震荡batch size64256取决于单样本长度和显存训练轮数3060数据量小于2万时建议用小轮数weight decay1e-4抑制过拟合和标签平滑叠加使用信噪比范围-4dB18dB建议训练时覆盖实际部署的全部SNR这里最容易被忽略的是输入长度。长度太短比如只有32个采样点可能连一个完整符号都包不住模型只能看到噪声碎片长度太长比如超过4096点推理时延和显存压力都会变大。我会在数据集中统计符号周期对应的采样点数再让输入长度覆盖2到8个符号周期。4.2 用learning curve判断“假收敛”和过拟合训练调制识别模型时验证准确率会随SNR呈现阶梯状跳变高SNR类别先被学会低SNR类别最后才抬升。这本身正常但如果验证loss在训练后期不降反升而准确率还在微增就要看是不是过拟合到噪声样本上了。我的判断方法是同时保存每轮训练集loss和验证集loss如果两者间距超过0.3就把weight_decay加到5e-4或把标签平滑从0.1调整到0.15。另外要留意“假收敛”模型把所有样本都预测成数量最多的那一类准确率停在类别占比最高的数值附近。这种情况通常发生在高SNR和低SNR样本数量严重不平衡时。处理办法不是单纯加class_weight先把数据按SNR分桶在每个桶内做类别均衡采样效果更稳定。4.3 滑动窗推理与多数投票让识别结果更稳训练阶段用的是固定长度窗口但实际部署时接收到的IQ流可能很长输入长度不能直接适配网络。常见做法是从长信号里滑动截取多个窗口分别推理再取多数投票。这样不仅能解决长度问题还能通过窗口间的冗余降低瞬时突发干扰的影响。def window_vote(model, iq, window_len256, overlap0.5): model.eval() stride int(window_len * (1 - overlap)) preds [] with torch.no_grad(): for start in range(0, len(iq) - window_len 1, stride): window iq[start:start window_len] logits model(window.unsqueeze(0)) preds.append(logits.argmax(dim1).item()) counts np.bincount(preds) score counts.max() / len(preds) return int(counts.argmax()), float(score)window_len要和训练时一致否则模型会看到分布完全不同的输入。overlap取0.5时相邻窗口重叠一半投票结果更平滑但推理次数会翻倍。如果对时延敏感可以改成overlap0.8用更多窗口换稳定性也可以结合软输出做置信度加权而不是单纯硬投票。4.4 排错清单这五个问题我几乎每次都遇到按出现频率排序I/Q通道接反星座图左右镜像训练结果还在涨但验证集崩掉。检查方法对已知BPSK信号画星座图看两个簇是否落在同一直线上。幅度归一化在全数据集上计算mean/std导致验证集信息提前进入训练流程。修复只统计训练集的归一化参数并保存。多个信噪比样本混在一起模型只学会区分SNR高低而不是调制类型。修复每个batch内部强制按SNR类别采样。窗长和符号率不匹配一个窗口里只有半个符号周期。修复统计符号速率后调整窗口长度。标签平滑和Dropout同时开太大导致模型欠拟合。排查时先关掉标签平滑对比loss曲线。提示调试时不要只看验证准确率把每个SNR档位单独算一遍准确率才能暴露模型在小样本模上的真实表现。5. 用一个新技巧收尾温度缩放修正置信度让调制识别结果可以直接写进报告很多部署场景需要的不只是类别标签还需要一个“可信度”。比如认知无线电的后续决策模块会拿置信度去判断是否切换频点。新手做法是直接取softmax概率最大值当置信度这个值在模型校准良好时可以用但CNN训练出的softmax置信度常常偏高尤其在实际部署时遇到训练里没见过的带外干扰。这时我会先做一次温度缩放。温度缩放不改变模型参数只在softmax之前把logits除以一个温度系数 (T)def temperature_calibrate(logits, T): return torch.softmax(logits / T, dim1)如果 (T1)softmax分布变得更平滑最大值会下降如果 (T1)分布会更尖锐。调制识别里我见过很多模型需要 (T) 落在1.2到2.0之间这说明模型对低信噪比样本过度自信。用验证集来拟合 (T)而不是用测试集。下面是一段找最佳温度参数的代码import torch from torch.optim import Adam def fit_temperature(logits, labels, init_t1.0): T torch.tensor(init_t, requires_gradTrue) opt Adam([T], lr1e-2) for _ in range(500): opt.zero_grad() prob torch.softmax(logits / T, dim1) loss torch.nn.functional.nll_loss(torch.log(prob), labels) loss.backward() opt.step() return T.item()这里保存所有验证集样本的logits然后只训练一个标量参数速度非常快。拟合完成后把温度写进推理管线的配置文件里之后每个窗口得到的softmax值就可以当作校准后的置信度。最终报告指标时我还会额外计算每个SNR档位的混淆矩阵重点看相邻调制阶数之间是否被系统性地混淆如果16QAM和64QAM之间混淆率超过两成说明当前特征不足以区分高密度星座图这比单纯堆参数更值得优先处理。本文还有配套的精品资源点击获取
返回列表