资讯动态

从零生成RML2016无线信号数据集:调制识别与I/Q采样实战

发布时间:2026/10/14 12:55:35 来源:尧图企业网站定制
简介RML2016数据生成代码面向无线通信与信号处理方向的研究者、研究生及SDR爱好者用于复现RML2016a数据集解决通信信号样本获取困难、实验环境难以搭建的问题。资源以Gnuradio与Python为核心通过流图与脚本完成信号生成、信道建模、干扰噪声叠加及数据记录可支撑信号检测、调制识别与解码等算法验证。压缩包共12个文件约25.64MB以7个Python脚本为主体承担参数配置、数据集生成与统计分析等任务另含build_dataset构建脚本、txt说明及少量音频与文本素材便于理解代码组织与运行流程。目前已有8936人学习下载适合具备一定Python与SDR基础、希望深入理解无线通信系统设计的中高级读者参考实践。1. 从一份 RML2016 数据生成代码说起无线信号数据集到底怎么造如果你做过无线通信方向的深度学习大概率绕不开 RML2016 这个数据集。它几乎是调制识别任务的“MNIST”——16 类调制方式、多信噪比、I/Q 双通道采样很多论文的基线都跑在它上面。但真正让人头疼的不是用数据集而是造数据集当你想换一套调制样式、改采样率、加自己的信道模型时官方那套数据就不够用了。这时候一份能自己生成 RML2016 格式数据的代码价值就出来了。这份代码解决的核心问题很具体给定调制方式集合、信噪比范围、每类样本数输出和 RML2016 结构一致的 I/Q 采样数据通常是 pickle 或 HDF5 格式形状为(样本数, 2, 128)。它适合三类人想复现调制识别论文但需要自定义数据的研究者、做信号数据集扩增的工程师、以及想搞懂 I/Q 数据到底怎么来的新手。下面我按“先讲清数据长什么样再动手生成最后排坑”的顺序拆开讲。2. RML2016 的数据结构长什么样先看懂再动手2.1 一条样本的物理含义与张量形状RML2016 里每条样本是一个复数基带信号的离散采样。I 路是实部Q 路是虚部采样点默认 128 个。存成数组后形状是(2, 128)第一维 0 是 I1 是 Q。整个数据集是一个字典键是(调制方式, 信噪比)这样的元组值是该条件下的样本数组形状(样本数, 2, 128)。为什么是 128 个点这是采样率和符号率的折中。以 1 MS/s 采样、每符号 8 个采样点算128 点覆盖 16 个符号足够让卷积网络看到调制特征又不至于让输入太大。你自己生成时这个长度可以改但改了之后很多预训练模型的输入层就对不上了这是第一个要注意的边界。2.2 调制方式与信噪比的组合逻辑RML2016 的 16 类包括 8PSK、AM-DSB、AM-SSB、BPSK、CPFSK、GFSK、PAM4、QAM16、QAM64、QPSK、WBFM 等。信噪比从 -20 dB 到 18 dB步长 2 dB共 20 档。每个(调制, 信噪比)组合默认 1000 条样本所以总量是 16×20×1000 320000 条。生成代码要做的就是遍历这个笛卡尔积对每个组合调用调制器产生基带信号加高斯白噪声到目标信噪比再截取 128 点存起来。这里有个容易忽略的点信噪比是加在信号上的不是直接设定噪声功率。正确做法是先算信号功率再根据目标 SNR 反推噪声功率。import numpy as np def add_awgn(signal, snr_db): 按目标信噪比给复基带信号加高斯白噪声 # 信号平均功率 sig_power np.mean(np.abs(signal) ** 2) # 由 SNR(dB) 反推噪声功率SNR 10*log10(Ps/Pn) snr_linear 10 ** (snr_db / 10.0) noise_power sig_power / snr_linear # 复噪声实部虚部各占一半功率 noise np.sqrt(noise_power / 2) * ( np.random.randn(len(signal)) 1j * np.random.randn(len(signal)) ) return signal noise这段代码的关键在noise_power / 2。复高斯噪声的总功率是实部加虚部如果直接拿noise_power去乘标准正态实际噪声功率会翻倍SNR 就偏了 3 dB。这个坑我在第一次生成数据时踩过训练出来的模型在低 SNR 下表现异常好后来才发现是标签对应的噪声根本没加够。2.3 存储格式pickle 还是 HDF5RML2016 原版用的是 pickle字典直接序列化。好处是读取快、结构直观坏处是文件大、跨语言差、加载有安全限制。如果你只是自己用pickle 够了。要做长期数据集或多人共享建议 HDF5按(调制, snr)建组支持分块读取和压缩。格式优点缺点适用场景pickle读写简单、结构保留完整体积大、无压缩、跨语言差个人实验、快速验证HDF5支持压缩、分块、跨语言需要 h5py、结构稍复杂数据集发布、大规模训练NumPy .npz无需额外依赖全量加载内存、无分块小规模、临时交换选型上我的习惯是样本量低于 10 万用 pickle 省事超过就上 HDF5压缩后体积能降到三分之一左右。3. 动手生成从调制器到落盘的完整链路3.1 用 Python 搭一个最小可用的调制器集合生成数据的核心是调制器。不需要通信库用 NumPy 就能实现 BPSK、QPSK、QAM 这些常见调制。下面是一个最小集合覆盖相位调制和幅度相位联合调制两类。import numpy as np def modulate_bpsk(bits): BPSK0--1, 1-1 return 2 * bits - 1 def modulate_qpsk(bits): QPSK每 2 bit 映射一个星座点 bits bits.reshape(-1, 2) i 2 * bits[:, 0] - 1 q 2 * bits[:, 1] - 1 return (i 1j * q) / np.sqrt(2) # 归一化功率 def modulate_qam16(bits): 16QAM每 4 bit 映射格雷码近似用自然码 bits bits.reshape(-1, 4) # 每 2 bit 决定一个轴的 4 个电平 levels np.array([-3, -1, 1, 3]) / np.sqrt(10) # 归一化 i levels[bits[:, 0] * 2 bits[:, 1]] q levels[bits[:, 2] * 2 bits[:, 3]] return i 1j * q逻辑说明每个调制器输入是随机比特流输出是复基带符号。/ np.sqrt(2)和/ np.sqrt(10)是功率归一化保证不同调制方式的平均功率一致否则加同样 SNR 时实际信噪比会因调制方式而异。参数上比特流长度要保证能整除每符号比特数BPSK 是 1QPSK 是 216QAM 是 4。3.2 脉冲成型与采样为什么不能直接拿符号当样本符号速率和采样率不是一回事。RML2016 的 128 点是在采样率下取的中间要经过脉冲成型常见的是根升余弦滤波器。如果直接把符号当采样点频谱是矩形的带外泄漏严重和真实信号差距大模型学到的特征会偏。from scipy.signal import upfirdn def pulse_shape(symbols, sps8, span4, beta0.35): 根升余弦脉冲成型sps 为每符号采样点数 # 根升余弦滤波器系数 t np.arange(-span * sps, span * sps 1) / sps h np.sinc(t) * np.cos(np.pi * beta * t) / (1 - (2 * beta * t) ** 2 1e-8) h h / np.sqrt(np.sum(h ** 2)) # 上采样后卷积 upsampled np.zeros(len(symbols) * sps, dtypecomplex) upsampled[::sps] symbols return upfirdn(h, upsampled)参数说明sps8表示每符号 8 个采样点span4是滤波器每侧跨越的符号数beta0.35是滚降系数。这三个参数决定了信号占用的带宽和码间干扰程度。RML2016 的生成里 sps 和 span 没有公开细节但常见做法是 sps8、span4 到 6。改 sps 会直接改变 128 点覆盖的符号数进而影响模型能看到的调制特征周期。3.3 批量生成与落盘脚本把调制、加噪、截取、存储串起来就是一个完整的生成流程。下面这段是主循环按(调制, snr)遍历每类生成指定条数。import pickle def generate_dataset(mods, snrs, n_per_class1000, n_samples128): data {} for mod in mods: for snr in snrs: samples [] for _ in range(n_per_class): # 生成足够长的比特流 n_bits n_samples * 4 # 留余量给成型 bits np.random.randint(0, 2, n_bits) symbols MODULATORS[mod](bits) waveform pulse_shape(symbols) # 随机截取一段增加多样性 start np.random.randint(0, len(waveform) - n_samples) seg waveform[start:start n_samples] seg add_awgn(seg, snr) # 归一化后存 I/Q seg seg / np.sqrt(np.mean(np.abs(seg) ** 2)) samples.append(np.stack([seg.real, seg.imag])) data[(mod, snr)] np.array(samples, dtypenp.float32) return data MODULATORS { BPSK: modulate_bpsk, QPSK: modulate_qpsk, QAM16: modulate_qam16, } if __name__ __main__: mods list(MODULATORS.keys()) snrs list(range(-20, 20, 2)) ds generate_dataset(mods, snrs, n_per_class1000) with open(rml2016_custom.pkl, wb) as f: pickle.dump(ds, f, protocol4)逻辑说明n_bits n_samples * 4是给脉冲成型留余量因为成型后长度会扩展。随机截取起点是为了让每次取到的符号相位不同增加样本多样性。最后统一归一化到单位功率保证不同调制、不同 SNR 下输入动态范围一致。参数上n_per_class控制每类样本数n_samples控制每条样本长度这两个直接决定数据集大小和训练输入维度。3.4 生成后的自检三个必看的验证点生成完不能直接拿去训练先做三个检查。第一看每个(调制, snr)的样本数是否一致缺类会导致训练时类别不平衡。第二看实际 SNR 是否接近目标值用信号功率和噪声功率反算。第三看星座图是否合理QPSK 应该四个簇16QAM 应该十六个簇。def check_snr(clean, noisy): 反算实际信噪比 noise noisy - clean ps np.mean(np.abs(clean) ** 2) pn np.mean(np.abs(noise) ** 2) return 10 * np.log10(ps / pn)如果反算出来的 SNR 和目标差超过 1 dB多半是噪声功率计算或归一化环节出了问题。这个自检步骤花不了几分钟但能省掉后面几天的排查时间。4. 避坑与排查生成 RML2016 数据时最容易翻车的五件事4.1 现象训练准确率异常高低 SNR 下也不掉原因噪声功率算错实际加进去的噪声比目标少相当于标签对应的难度被削弱了。最常见的是复噪声功率没除以 2或者归一化在加噪之后又做了一次把噪声也归一化了。解决加噪前记录信号功率加噪后反算实际 SNR和目标对比。归一化只在加噪前做一次加噪后不要再动。4.2 现象不同调制方式之间模型混淆严重原因脉冲成型参数不一致或者功率归一化没做导致某些调制方式的信号幅度明显大于其他。模型可能靠幅度而不是调制特征来分类。解决所有调制方式共用同一套成型参数且在加噪前统一归一化到单位功率。检查每个调制方式的平均功率是否一致。4.3 现象生成的 pickle 文件加载时报内存错误原因样本数太多pickle 全量加载到内存。32 万条(2,128)的 float32 大约 320 MB如果每类加到 5000 条就是 1.6 GB普通机器吃不消。解决改用 HDF5 分块存储训练时用 DataLoader 按需读取。或者降低每类样本数用数据增强在训练时扩充。4.4 现象星座图看起来是散的没有明显簇原因截取位置不对截到了脉冲成型的过渡段或者符号定时偏移没处理。也可能是 SNR 太低-20 dB 时星座图本来就是散的。解决截取时避开成型滤波器的边缘从span * sps之后开始取。低 SNR 下星座图散是正常的看高 SNR 的样本验证。4.5 现象换一台机器生成结果对不上原因随机种子没固定。NumPy 的随机状态在不同版本、不同平台上可能有差异导致比特流和噪声不一致。解决在生成脚本开头固定np.random.seed(42)并且把种子写进数据集元信息。如果要完全可复现用np.random.default_rng(42)替代全局种子。5. 进阶技巧让生成的数据更接近真实场景5.1 加信道损伤从理想 AWGN 到多径与频偏RML2016 原版只有 AWGN但真实无线环境有多径、频偏、相位噪声。如果你想让模型更鲁棒可以在加噪前先过一遍信道模型。多径用几个抽头的 FIR 滤波器模拟频偏用复指数乘上去。def apply_multipath(signal, taps): 多径信道taps 为各径复增益 return np.convolve(signal, taps, modesame) def apply_cfo(signal, cfo_hz, fs): 载波频偏 t np.arange(len(signal)) / fs return signal * np.exp(1j * 2 * np.pi * cfo_hz * t)参数上taps可以设成[1, 0.30.2j, 0.1-0.1j]模拟三径cfo_hz设成采样率的千分之一到百分之一。加了这些之后模型在原始 RML2016 上的表现可能会降但在真实数据上会更稳。5.2 用生成代码做数据增强半监督与域适应生成代码的另一个用法是扩充少数类。比如你手头只有少量真实采集的某类调制信号可以用生成代码造大量同类样本做预训练再用真实数据微调。或者固定 SNR 生成训练一个 SNR 估计器反过来给真实数据打伪标签。我的习惯是先用生成数据训一个基线看混淆矩阵找出容易混的调制对然后针对这些对生成更多样本或者调整成型参数让它们更可分。这个迭代过程比一次性生成全量数据更有效。5.3 验证生成质量用分类器反测最后一步验证拿生成的数据训一个简单 CNN看准确率随 SNR 的曲线是否合理。正常情况是 -20 dB 接近随机0 dB 以上快速上升高 SNR 接近 100%。如果曲线形状不对说明生成环节有问题。这个反测比看星座图更能量化地反映数据质量。SNR 区间预期准确率异常表现与可能原因-20~-10 dB20%~40%过高噪声不足过低归一化错误-8~0 dB50%~80%平坦特征被破坏检查成型参数2~18 dB85%~100%上不去类别不平衡或标签错位我自己的教训是生成数据这件事看起来只是调库和循环但真正决定模型上限的是那几个不起眼的参数——噪声功率的除以 2、成型滤波器的滚降系数、截取起点的随机范围。这些地方错一个后面调模型结构、换损失函数都是白费。所以每次生成完先跑自检和反测确认数据本身没问题再动模型。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑