ARTICLE DETAIL

资讯详情

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

RML2016数据生成代码详解:从I/Q样本到调制识别训练集

RML2016数据生成代码详解:从I/Q样本到调制识别训练集 简介RML2016数据生成代码是面向无线通信与信号处理研究者的实用工具基于Gnuradio与Python实现RML2016a数据集的复现与自定义生成适合需要模拟Wi-Fi、LTE、蓝牙等调制信号及信道场景的算法验证工作。压缩包共12个文件大小约25.64MB以7个Python脚本为核心涵盖信号生成、信道建模、数据切片与统计分析等模块另含参考音频、文本素材及构建说明可直接用于搭建生成流程。已有8923人学习下载说明其在SDR与深度学习信号识别领域具有较高参考价值。通过运行与分析代码研究者可掌握从多调制类型生成、噪声干扰模拟到数据存储的完整链路并据此构建符合自身实验要求的RML2016格式数据集支撑调制识别、信号检测等后续研究。1. RML2016数据生成代码调制识别任务最常被忽略的一环很多人在做调制识别时第一件事就是下载RML2016.10A.pkl然后直接丢给卷积网络训练却很少问一句这批I/Q样本到底是怎么来的。所谓RML2016数据生成代码就是负责把随机比特变成基带复数波形、再按信噪比加上噪声、最终打包成2x128数组的那套程序。它能解决三件事第一复现原始数据集验证自己的训练流程和论文对齐第二扩充你想要的调制类型和信噪比范围不必受原始数据限制第三在仿真数据和实测数据之间搭建对照实验。适合正在做调制识别、信号分类或者需要构造合成I/Q样本的工程师。下面我按自己的实现习惯把生成链路、可运行代码和踩过的坑一次讲清楚。2. RML2016格式的生成链路从射频信号到I/Q样本的四步拆解2.1 RML2016.10A的样本结构与标签体系RML2016系列最常用的发布版本是RML2016.10A它是用pickle打包的字典key是(调制类型, SNR)value是若干样本堆叠出的数组。单个样本的形状是2 × 128第0行是I路实数序列第1行是Q路实数序列合在一起等效于128个复数采样点。这128个点对应的是一段极短的观测窗口在典型符号速率下只包含十几个符号周期所以调制识别模型必须在很短的时间里抓住调制特征。标签体系覆盖11种调制方式BPSK、QPSK、8PSK、CPFSK、PAM4、QAM16、QAM64、GFSK、WBFM、AM-DSB、AM-SSB。SNR从-20dB到30dB按2dB步进一共26个信噪比档位。数据生成代码需要严格保持这种键结构后续模型训练代码才不需要大改。最常见的错误是生成时把key写成了snr_mod这种字符串导致加载时和原版数据集完全对不上。2.2 生成链路上的四个关键环节调制、脉冲整形、信道与下变频要把发射机的比特流变成接收机看到的I/Q样本至少得经过四个环节。第一个环节是符号映射也就是把比特按照星座图映射成复数符号这一步决定了调制类型。第二个环节是脉冲整形通常用根升余弦滤波器限制信号带宽避免符号间干扰。第三个环节是信道叠加在信号上按指定信噪比注入高斯白噪声还可以追加多径衰落和载波频偏。第四个环节是接收端下变频把载波信号搬移到基带得到I路和Q路。RML2016原始数据是用GNU Radio画流图生成的发射机和接收机链路都在流图里完成。而在纯Python生成代码里下变频这一步往往被简化为直接输出基带复数序列不再单独建模载波。这样简化是可行的因为深度调制识别模型吃的就是基带复数载波频率本身不是分类依据。但要注意简化后仍然要保留相位、幅度和加性噪声的统计特性否则训练出来的模型换到真实接收机上会崩。2.3 用GNU Radio还是纯Python常见生成方案对比我两种方案都用过。GNU Radio的好处是物理链路完整可以直接调用星座映射器、RRC滤波器、信道模型甚至USRP硬件生成的数据贴近真实射频采集。坏处是批量生成效率低如果你想扫20个信噪比、每个类型每个信噪比几千条样本光开流图运行时间就很可观。纯Python用numpy向量化生成速度能快一个数量级适合做数据扩充和快速实验但多径、载波频偏、接收机非理想特性都需要自己补。回到RML2016数据生成代码.rar这个压缩包里面最常见的就是纯Python脚本因为它的目标是让用户快速复现RML2016的数据格式而不是重建一套完整射频仿真链路。我的建议是先用Python脚本跑通流程确认样本能训练出可用的分类器再根据实际项目要求决定是否要引入GNU Radio做高保真仿真。不要一开始就追求绝对真实否则你会在环境搭建上耗掉大量时间。3. 用数据生成代码跑通最小流程核心脚本与三个必调参数3.1 最小目录结构与依赖安装一份能跑的Python生成代码通常只需要三个文件config.py放调制类型和信噪比扫描参数generate_iq.py负责生成单条I/Q样本build_dataset.py负责批量保存。不需要GPUnumpy和scipy就够用。安装依赖只需要两条命令pip install numpy scipy如果你希望保存的格式和RML2016.10A.pkl一致直接使用Python自带的pickle即可不需要额外依赖。目录结构我习惯这样组织rml2016_gen/ ├── config.py ├── generate_iq.py ├── build_dataset.py └── output/output目录用来存放生成的pkl文件。这样组织的好处是每个模块职责清晰后续要增加新的调制类型只需要改config和映射表。3.2 第一步生成单条IQ样本的Python脚本下面这段代码是我常用的最小实现覆盖了BPSK、QPSK、8PSK、QAM16四种调制类型输出形状就是2 × 128。它包含了根升余弦脉冲整形、随机截取和加噪三个关键环节。import numpy as np from scipy.signal import upfirdn def rrc_filter(sps, beta0.35, num_taps16): # 根升余弦滤波器sps是每符号采样数 n np.arange(-num_taps, num_taps 1) t n / sps with np.errstate(divideignore, invalidignore): h np.sinc(t) * np.cos(np.pi * beta * t) / (1 - (2 * beta * t) ** 2 1e-8) h / np.sqrt(np.sum(h ** 2)) return h def bits_per_symbol(mod_type): return {BPSK: 1, QPSK: 2, 8PSK: 3, QAM16: 4}[mod_type] def map_bits_to_symbols(bits, mod_type): if mod_type BPSK: return (2 * bits.astype(float) - 1).astype(complex) elif mod_type QPSK: bits2 bits.reshape(-1, 2) idx bits2[:, 0] * 2 bits2[:, 1] const np.array([11j, -11j, -1-1j, 1-1j], dtypecomplex) / np.sqrt(2) return const[idx] elif mod_type 8PSK: bits3 bits.reshape(-1, 3) idx bits3[:, 0] * 4 bits3[:, 1] * 2 bits3[:, 2] angles 2 * np.pi * idx / 8 return np.exp(1j * angles) elif mod_type QAM16: bits4 bits.reshape(-1, 4) gray_map np.array([-3, -1, 3, 1]) re gray_map[bits4[:, 0] * 2 bits4[:, 1]] im gray_map[bits4[:, 2] * 2 bits4[:, 3]] return (re 1j * im).astype(complex) / np.sqrt(10) else: raise ValueError(funsupported mod: {mod_type}) def generate_iq(mod_type, snr_db, num_points128, sps4, num_syms64, seed0): rng np.random.default_rng(seed) bits rng.integers(0, 2, num_syms * bits_per_symbol(mod_type)) symbols map_bits_to_symbols(bits, mod_type) # 过采样每sps个采样点放一个符号其余补零 up np.zeros(num_syms * sps, dtypecomplex) up[::sps] symbols h rrc_filter(sps, beta0.35, num_taps16) shaped upfirdn(h, up, up1, down1) delay len(h) // 2 shaped shaped[delay:] if len(shaped) num_points: raise ValueError(num_points too large, increase num_syms) start rng.integers(0, len(shaped) - num_points 1) iq shaped[start:start num_points] # 功率归一化 iq iq / np.sqrt(np.mean(np.abs(iq) ** 2) 1e-12) # 按信噪比添加复高斯噪声 noise_pow 10 ** (-snr_db / 10) noise np.sqrt(noise_pow / 2) * (rng.normal(sizenum_points) 1j * rng.normal(sizenum_points)) iq iq noise return np.vstack([iq.real, iq.imag]).astype(np.float32)这段代码的逻辑很简单先按调制类型生成随机比特映射成复数符号然后插零过采样用根升余弦滤波器成型滤波后随机截取连续128个采样点最后做功率归一化并按信噪比加噪。sps是每符号采样数我习惯设为4太小会导致频谱混叠太大会让128个点里包含的符号数太少。num_syms是参与成型的总符号数至少要大到能保证截取时长度足够64个符号配合sps4可以生成256个采样点足够随机截取。num_points是输出样本长度保持和RML2016一致的128。3.3 第二步批量生成并保存为字典结构单条样本可以用循环生成但批量生成时要注意随机种子和进度输出。下面这段代码生成4种调制类型、从-2到20dB共12个信噪比、每个条件1000条样本import pickle from collections import defaultdict mods [BPSK, QPSK, 8PSK, QAM16] snrs list(range(-2, 21, 2)) # -2,0,2,...,20 samples_per_snr 1000 raw_data defaultdict(list) for idx_m, mod in enumerate(mods): for idx_s, snr in enumerate(snrs): for i in range(samples_per_snr): iq generate_iq(mod, snr, num_points128, sps4, num_syms64, seedi idx_m * 10000) raw_data[(mod, snr)].append(iq) print(f{mod} {snr}dB done) for key in raw_data: raw_data[key] np.array(raw_data[key]) with open(output/my_rml2016.pkl, wb) as f: pickle.dump(raw_data, f)这里seed用i idx_m * 10000区分是为了保证不同调制类型之间虽然共享同一个循环变量但生成的比特序列不同。如果你全部使用同一个seed每一类样本的星座图会完全一样模型很容易过拟合到随机噪声的特定模式上。samples_per_snr设1000是折中验证流程时500到1000足够想复现论文的完整曲线可以提高到6000。3.4 信噪比怎么扫从-2dB到20dB的常见设置RML2016.10A原始扫描范围是-20到30dB步进2dB。但在实际复现时我建议第一轮先扫-2到20dB。原因是低于-2dB时噪声完全淹没了调制特征绝大多数模型的准确率都接近随机猜测这段数据除了画出长尾曲线之外对模型收敛帮助不大。高信噪比端到20dB也够了20dB以上信号几乎无失真已经接近上限再往上主要是测试数值稳定性。信噪比参数直接传给generate_iq的snr_db即可不要在外部手工计算噪声功率。注意信噪比的定义是信号功率与噪声功率的比值单位dB。在代码里我把信号功率归一化到1所以噪声功率就是10 ** (-snr_db / 10)复高斯噪声的每个分量方差取噪声功率的一半。如果你看到星座图上的点发散程度不符合预期先检查信号归一化是否生效再检查噪声功率公式。4. 把生成的IQ数据整理成训练集文件组织、标签编码与归一化4.1 和RML2016.10A对齐的文件组织方式生成完的pkl字典键是(mod, snr)但PyTorch和TensorFlow的训练接口更习惯接收大数组加标签数组。我一般会把字典展平成两个npy文件samples.npy形状是(N, 2, 128)labels.npy形状是(N, 2)第0列是调制索引第1列是信噪比索引。展平代码可以这么写import numpy as np with open(output/my_rml2016.pkl, rb) as f: raw_data pickle.load(f) mod_list [BPSK, QPSK, 8PSK, QAM16] snr_list list(range(-2, 21, 2)) samples [] labels [] for idx_m, mod in enumerate(mod_list): for idx_s, snr in enumerate(snr_list): batch raw_data[(mod, snr)] # (N, 2, 128) samples.append(batch) labels.append(np.full((batch.shape[0], 2), [idx_m, idx_s], dtypenp.int64)) samples np.concatenate(samples, axis0).astype(np.float32) labels np.concatenate(labels, axis0) np.save(output/samples.npy, samples) np.save(output/labels.npy, labels)保持原始字典的同时导出npy等于留了一份后悔药。万一后续发现训练集和验证集划分有问题还能回到字典重新切分不用重新生成数据。文件组织上我建议把原始pkl和展平后的npy放在同一个目录用文件名区分版本比如my_rml2016_v1.pkl。4.2 标签编码modulation类型与SNR的两种存法调制分类的主任务只关心mod类型但验证时经常需要按SNR分桶统计准确率所以标签必须同时保留SNR。我习惯把标签存成两列训练时只取第0列labels[:, 0]作为分类目标测试时按第1列labels[:, 1]分组计算曲线。这种做法的好处是不需要在数据生成阶段重复做两套标签体系。值得注意的是mod_list和snr_list的顺序必须固定导出标签时不能改变。我遇到过项目后期在mod_list里新增了一个调制类型结果老样本的标签索引整体偏移训练精度直接掉到接近随机。解决方法是把mod_list和snr_list同时写入一个meta.json加载数据时读取它来重建索引对应关系{mod_list: [BPSK, QPSK, 8PSK, QAM16], snr_list: [-2, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]}4.3 归一化与数据增强的边界RML2016格式的I/Q样本已经接近零均值但不同调制方式的平均功率有差异。在训练前做样本级归一化可以避免模型学到功率差异而不是调制特征。我用的是每个样本内部求标准差再归一化def normalize_per_sample(x): # x: (..., 2, 128) std np.sqrt(np.mean(x ** 2, axis(-2, -1), keepdimsTrue) 1e-12) return x / std需要强调这里的归一化不能做在整个数据集上否则高功率的调制类型会被模型直接识别出来而不是通过相位和幅度结构识别这在真实场景中并不可靠。数据增强要克制随机翻转I/Q符号、复数旋转和加微小噪声是安全的因为它们不改变调制特征。不要做随机裁剪或缩放那会破坏脉冲成型和符号率信息。如果实验需要大量不同样本最可靠的方式是用不同的seed重新生成数据而不是在已有样本上叠加变换。5. 生成数据时的避坑清单信噪比偏差、样本重复与格式错位5.1 现象训练集精度很高一到真实信号就垮这个现象我在项目里遇到过不止一次。模型在自己生成的样本上能跑到90%以上接上软件无线电实测数据后准确率直接掉到50%。原因有两个生成代码只加了理想高斯白噪声没有建模载波频偏另外接收机的I/Q不平衡也没有体现在数据里。真实信号几乎一定有频偏尤其是低成本前端。解决方法是给生成代码增加一个可选频偏参数。在原有成型后的复数序列上乘一个相位旋转因子np.exp(1j * 2 * np.pi * f_off * t)频偏大小按采样率的千分之一量级设置。不要只生成一组无频偏训练集应该同时生成一组带频偏的验证集用来观察模型对失谐的容忍度。5.2 现象不同调制类型样本数量对不上生成时如果按比特数而不是按符号数生成不同调制类型对应的符号数会不同。比如BPSK每个符号只消耗1比特QAM16每符号消耗4比特相同比特数下QAM16产生的符号数只有BPSK的四分之一。这会导致随机截取时某些类型可用的起始位置更少统计上样本出现重复的概率更高。解决方法是统一按符号数生成。我前面代码里的num_syms64就是符号数然后根据调制类型反推需要的比特数。在批量生成脚本里每个条件使用相同的num_syms不许按调制类型分别指定不同比特数。如果你发现生成的样本里存在大量完全相同的I/Q序列检查一下是不是随机截取窗口太小而符号数太多导致不同seed生成了重叠区域。5.3 现象I/Q数据出现相位跳变星座图看着不对自己生成的数据用星座图展示时如果散点不是聚成一团而是绕原点呈圆弧分布说明每个样本的起始相位没有对齐。这其实不一定是错误因为RML2016原始数据也没有符号级同步。模型需要学会对任意起始相位保持鲁棒这反而是好事。但如果你是在验证调制映射是否正确可以临时不做随机截取直接把滤波器延迟补偿后的前num_points个采样点固定输出这样星座图就能看出映射关系。如果一个BPSK样本星座图出现了两个对称的簇而不是两个点说明混入了复数相位旋转检查一下iq.imag是不是几乎为零。5.4 现象保存的pkl用torch.load加载失败PyTorch的torch.load默认加载由torch.save保存的对象不能直接加载pickle.dump写出的文件。我在项目里见过队友把生成数据用pickle.dump保存又在训练脚本里用torch.load读取结果报UnpicklingError折腾了半天。解决方法是统一保存用pickle读取也用pickle.load训练时再手动把numpy数组转成Tensor。另外字典的key必须用简单tuple比如(QPSK, 10)不要用自定义对象。如果你的生成脚本把SNR存成了浮点10.0而训练脚本里用的是整数10看起来一样但key匹配不上遍历时会漏掉样本。建议所有SNR都用整数或统一浮点表示。5.5 现象频谱看着不对带外噪声太高如果你把生成样本做FFT发现信号带宽远超符号速率应有的带宽说明过采样率或脉冲整形滤波器的抽头数设置不合理。sps2时根升余弦滤波器的过渡带会明显变宽滤波器抽头数低于16时滚降不够频谱旁瓣会很高。解决方法是sps最低设为4num_taps设为16或更大。如果你用GNU Radio生成检查流图里的RRC Filter参数和Symbol rate、Sample rate比值。正确配置下带外能量应该被压制到很低水平噪声频谱在通带内才明显可见。6. 用生成代码反推模型可信度验证集构建与几条实用技巧生成数据不能只是为了训一个好看的模型更应该当作模型可信度的标尺。我常用的验证集分三类同分布验证集、带频偏验证集、留一调制验证集。同分布验证集就是生成时把seed往后偏移用同样的调制和信噪比参数生成一批新样本带频偏验证集在生成时加入千分之一的载波频偏留一调制验证集则是从训练集里扣掉一个调制类型看模型会不会把它错分到其他类型。还有一个技巧是始终保存生成参数。每次生成数据前把config.py里的所有参数、随机种子范围、生成时间一起存成一份JSON文件名对应数据集版本。这样哪个版本出了异常可以精确回溯。不要相信文件名里的v1、v2那根本不顶用。我遇到过生成脚本改了一行噪声功率公式生成的旧数据和新数据混在同一个训练集里训练曲线突然乱掉最后只能靠参数JSON定位差异。最后建议你为每个调制类型画一条SNR-准确率曲线而不是只报告平均准确率。调制识别模型的性能高度依赖信噪比一个在-2dB到20dB平均90%的模型可能在0dB以下只有30%。这条曲线能让你立刻看出生成数据的信噪比覆盖是否均衡也能让团队里的其他人快速理解模型边界。我现在每次交付生成代码都会附带一份这样的曲线脚本算是工程师习惯。希望帮到你。本文还有配套的精品资源点击获取
返回列表