简介:压缩包聚焦无线通信信道估计核心实验,面向通信专业学生、研究者和工程技术人员,围绕LS、MMSE等经典算法的原理实现与性能对比展开。包内共7个文件,包括4个MATLAB脚本与3张结果对比图,代码覆盖从信道估计到均衡处理的完整流程,图片直观呈现LS与MMSE差异、估计与实际信道匹配度及均衡前后效果,适合用于算法验证、课程设计或科研复现。压缩包整体仅147KB,轻量易用。已有199人学习。通过学习可掌握LS与MMSE信道估计算法的编程实现、插值提升估计精度的方法,以及基于估计结果设计信道均衡的思路,同时借助配套数据集进行算法性能评估与可视化分析,为OFDM系统优化和后续深度学习信道估计研究打下基础。
1. 信道估计内含数据集.zip:先搞清这个压缩包能给你什么
做物理层算法的人基本都经历过这样的场景:从公开页或同行手里拿到一个名为“信道估计内含数据集.zip”的压缩包,解压出来少则几十 MB,多则几个 GB,里面堆着 .mat、.h5、.npy 甚至二进制 .dat 文件,README 要么没有,要么只写一句“请引用论文”。这类包的核心用途是评估信道估计算法——典型如 OFDM 系统里的 LS 估计、LMMSE 估计,以及这两年大量出现的基于深度学习的信道估计网络。这篇笔记要解决的,就是把这个包从“能解开”讲到“能跑通、能复现、能判断值不值得继续投入”。
拆开这个压缩包,首要任务不是找训练脚本,而是建立一套自己的验证链路:读数据、识别信道标签与观测输入、用 LS/LMMSE 打出基线曲线、再决定是否沿这份数据继续做深度模型。下文按这条路径逐段展开。
2. 拆开压缩包先看数据:识读 CIR、CFR 与导频观测
信道估计数据集里最常见的是三类文件:时域信道冲激响应 CIR、频域信道频率响应 CFR、以及加了导频的接收信号 y_p。如果你的压缩包里有 h_channel.mat 和 rx_pilot.mat,大概率前者是理想信道标签,后者是带噪声的观测。这里有个容易忽略的点:很多包里的 CIR 是复数数组,实部是 I 路、虚部是 Q 路,如果你急着用 np.abs() 取模,相位信息就丢了,后边算 LMMSE 所需的信道自相关矩阵也会直接变成零矩阵。
2.1 三种数据形态:先分清谁是标签、谁是输入
先给一个通用判断方法:文件名里带 h、channel、ideal、true、groundtruth 的,是标签;带 rx、y、received、pilot 的,是观测输入;带 snr、sigma2 的,是信噪比或噪声功率标注。导频观测通常按子载波抽点,形状可能是 [n_frame, n_pilot, n_rx],也可能整段 OFDM 符号都保留成 [n_frame, n_subcarrier, 1],后者常见于给深度神经网络当输入的“伪导频”数据集。先分清谁是输入谁是标签,比急着写估计算法更重要——搞反了轻则曲线没法看,重则网络训练时直接维度爆炸。
2.2 用 Python 读三种封装:mat、h5、npy
信道估计数据最常见的封装是老版 .mat,但越来越多包开始面向 Python 用户提供 .npy 或 .h5。用 scipy.io.loadmat 读老版 .mat,用 h5py 读 v7.3 和 .h5。这里有个关键的格式分界:MATLAB R2014b 之后另存的 -v7.3 是 HDF5 容器,scipy 直接报 NotImplementedError,必须换 h5py。高光谱领域常见的 icvl 高光谱数据集也是类似的 h5 组织方式,读法完全通用。
import h5py import numpy as np from scipy.io import loadmat # 方案1:老版 .mat 文件,scipy 直接读 try: data = loadmat("h_channel.mat") h_cir = data["h_channel"] # 形态可能是 [n_frame, n_tap] 或 [n_frame, n_subcarrier] print("scipy 读取成功,shape:", h_cir.shape, "dtype:", h_cir.dtype) except NotImplementedError: print("v7.3 格式,改用 h5py") # 方案2:v7.3 或 .h5 文件,用 h5py 按 HDF5 读取 with h5py.File("h_channel.mat", "r") as f: for key in f.keys(): print("HDF5 键:", key, f[key].shape, f[key].dtype)这段代码的逻辑是先让 scipy 试读,失败再列 HDF5 里的数据集键名。h5py 读出来的数组默认是 C 行序,而 MATLAB 存的是 Fortran 列序,所以你看到的 shape 其实已经互换了。对二维数组用 .T 转置回来;对三维数组,先确认哪一维是 frame、哪一维是子载波,再用 np.moveaxis 调整,不要无脑用 .T。参数上,h_channel 的 dtype 如果是 complex128,而你的训练管线只需要 complex64,顺手转一次 dtype,能在后边省一半内存。
如果压缩包里直接是 .npy 文件,读取更简单,但建议养成分批读的习惯:
import numpy as np x = np.load("rx_pilot.npy", mmap_mode="r") print("形状:", x.shape, "数值范围:", np.nanmin(x), np.nanmax(x))mmap_mode="r" 是 numpy 的 memory-map 读取,数组文件多大都不会一次性占满内存,先看 shape 和数值范围,确认结构合理再决定是否全量载入。这一步很像拿到素材先看分辨率而不是直接开始渲染,成本几乎为零,却能挡掉大半“读出来形状不对”的返工。
2.3 字段命名与单位:先对齐这张表
| 字段名常见写法 | 含义 | 典型形状 | 是否复数 |
|---|---|---|---|
| h_channel / h_true / h_ideal | 理想信道频响或冲激响应 | [n_frame, n_subcarrier, n_rx, n_tx] | 是 |
| rx_pilot / y_pilot / y | 接收导频符号 | [n_frame, n_pilot, n_rx] | 是 |
| x_pilot / pilot_seq | 发送的导频序列 | [n_frame, n_pilot] | 是 |
| snr / SNR_dB | 信噪比标签 | [n_frame] | 否 |
| sigma2 / n0 | 噪声方差(复数域总功率) | [n_frame] | 否 |
这张表是判断基准,不是硬标准。我一般拿到数据先打印变量名,再看 README,顺序反了容易看漏信息。单位也要确认一下:snr 字段如果已经是 dB 值,后面换算噪声功率时,是 10^(SNR/10) 还是 10^(-SNR/10) 取决于你定义的是信号功率除以噪声功率还是相反。方向搞反,整条 NMSE 曲线会平移 20 dB,看起来就像完全不同的方案。
2.4 解压与文件体检:伪加密、路径穿越和缺失文件
先做一次完整解压,不要用右键直接解。命令行下顺序很重要:先看清单,再解压。
unzip -l 信道估计内含数据集.zip # 先看目录结构,不急着解压 unzip -O gbk 信道估计内含数据集.zip -d ./channel_data # 中文文件名乱码时指定编码unzip -l 能让你在解压前就看到目录层级、是否有绝对路径、是否有可疑的 ../../ 路径。zip 伪加密(文件头加密标志位置位但内容未加密)是个经典坑:7-Zip 弹窗要求输入密码,不输就解不出来。如果你确认数据是公开资源,先试空密码,或者用 7-Zip 打开后忽略错误直接导出,这比到处找“zip 密码移除”工具靠谱。另一个低频但值得警惕的是路径穿越压缩包,恶意构造的 ../../ 文件名在解压时会写到目标目录外面,务必用 -d 指定一个新建的空目录再解。
2.5 数据体检:样本量、信噪比覆盖和维度是否够用
跑算法之前,做一次快速数据体检。检查三件事:一是样本量,信道估计任务的数据集至少要几千帧才能支撑深度学习训练,如果只有一两百帧,只能跑通流程、没法谈收敛;二是信噪比覆盖,看 snr 字段是否覆盖 0~30 dB,覆盖不够时 NMSE 曲线只有半段,对比价值很低;三是维度一致性,把所有文件打印 shape 并标一下 frame 维是否对齐。这个函数可以复用:
import numpy as np def inspect_dataset(paths: dict): for name, arr in paths.items(): print(f"{name}: shape={arr.shape}, dtype={arr.dtype}, " f"nan={np.isnan(arr).any()}, inf={np.isinf(arr).any()}")这个函数检查三个点:NaN/Inf——仿真数据出现 NaN 几乎一定是生成流程没加噪声或除零;shape 与 README 是否一致;复数 dtype 是 complex64 还是 complex128。体检阶段发现维度对不上,比训练到一半才发现维度泄漏划算得多。
3. 用 LS 与 LMMSE 跑通最小验证:从数据集到第一条性能曲线
数据读进来之后,下一步是拿它跑一个最小验证。所谓最小验证,就是用最朴素的算法在整份数据集上算出 NMSE 对信噪比曲线。这样你能确认三件事:信道标签和观测信号是严格对应的、噪声功率换算没错、评估指标的口径和论文一致。这一步不做,后面训练更复杂的网络,出了问题也分不清是数据问题还是模型问题。
3.1 LS 信道估计:为什么它是最稳的基线
LS 信道估计在导频位置上的形式就是 H_ls = y_p / x_p(逐导频相除),不需要信道的任何先验统计信息,唯一的假设是导频序列已知且噪声与导频不相关。对 OFDM 系统,导频位置上的 LS 估计再通过插值扩展到全部数据子载波。LS 的优势是稳定、快、好实现,缺点是噪声没有被平均,高信噪比下误差平台仍然明显,这就是 LMMSE 出场的原因。做数据集基线时,LS 必须第一个跑通,因为它不依赖任何统计假设,如果 LS 都异常,问题一定出在数据读取或预处理。
3.2 LMMSE 估计:需要从数据集里学习二阶统计量
LMMSE 的公式形式是 H_lmmse = R_hp (R_pp + σ²I)^-1 y_p,其中 R_hp 是信道频响在导频位置上的自相关矩阵。这个矩阵在真实系统里通常不可知,但有了数据集就不一样——训练集里的 h_true 就是现成的统计样本,可以直接估计 R_hh。这里最常翻车的点是:R_hh 只能从训练集估计,不能包含测试集样本,否则叫标签泄漏,论文里属于一票否决的错误。估算 R_hh 的最小代码:
def estimate_R_hh(h_true_train, pilot_idx): """从训练集标签中估计导频位置自相关矩阵""" H_p = h_true_train[:, pilot_idx] # [n_train, n_pilot] n_train = H_p.shape[0] R_hh = (H_p.conj().T @ H_p) / n_train R_hh += 1e-6 * np.eye(H_p.shape[1]) # 正则项,防止奇异 return R_hh逻辑上分三步:取导频位置的信道标签、算外积均值、加对角正则。参数上,正则项系数 1e-6 不是固定值,如果后面 np.linalg.solve 报 singular matrix,把 1e-6 升到 1e-4 再试;数据量小的时候这个系数要适度调大,避免噪声方差为负这种不可能出现的事。
3.3 最小可运行代码:LS + 线性插值 + NMSE
先写一段只依赖 numpy/scipy 的最小实现,冒烟测试用。假设 rx_pilot、x_pilot、h_true 已经从数据集读入,shape 分别是 [n_frame, n_pilot]、[n_frame, n_pilot]、[n_frame, n_subcarrier]:
import numpy as np from scipy.interpolate import interp1d def ls_pilot(y_pilot, x_pilot): """导频位置 LS 估计,复数除法""" return y_pilot / np.maximum(np.abs(x_pilot), 1e-12) def nmse(h_est, h_true): """归一化均方误差,NMSE = ||e||² / ||h_true||²""" return np.linalg.norm(h_est - h_true) ** 2 / (np.linalg.norm(h_true) ** 2 + 1e-12) n_subcarrier = 512 pilot_interval = 4 pilot_idx = np.arange(0, n_subcarrier, pilot_interval) # [0, 4, 8, ...] whole_idx = np.arange(n_subcarrier) h_ls_p = ls_pilot(rx_pilot, x_pilot) # 导频位置估计值 h_ls = np.stack([ interp1d(pilot_idx, h_ls_p[n], kind="linear", fill_value="extrapolate")(whole_idx) for n in range(h_ls_p.shape[0]) ]) print("LS NMSE:", nmse(h_ls, h_true))代码逻辑分为三层:复数除法做 LS,线性插值把导频点扩展到全子载波,NMSE 计算评估误差。参数上两个点必须注意:interp1d 默认不允许外插,而导频不会刚好铺满数据块边缘,所以必须显式传 fill_value="extrapolate";pilot_interval 是导频间隔,OFDM 系统一般取 4 或 8,间隔越大插值误差越大,但这个参数也直接决定导频开销,不是越小越好。x_pilot 有可能是幅度归一化的复数序列,除之前取 abs 加一个极小值是为了避免导频序列里有 0 点导致除零。
3.4 参数怎么设:导频间隔、FFT 长度和插值方式
导频间隔由信道相干带宽决定,经验法则是导频间隔不要超过相干带宽的一半。对一个 512 点 OFDM、典型城市多径时延扩展 1~2 μs 的场景,导频间隔 4 是安全值,设到 8 就有明显性能损失。FFT 长度决定你看到的 CFR 频域分辨率,FFT 越大、子载波越密,LS 插值后的误差越小,但同等时延扩展下相邻子载波的相关性也越高,边际收益递减。插值方式上,线性插值足够当基线;DFT 插值在低信噪比下通常更好,因为它的本质是基扩展;LMMSE 则不需要插值这一步,它在导频位置直接求解,输出的 h_lmmse 天然覆盖全部子载波。
3.5 快速验证技巧:先取子集再全量跑
任何数据集我第一次跑通时都不会直接上全量。取前 200 帧、固定三个信噪比点(比如 0/10/20 dB)跑一遍 LS,看 NMSE 的绝对量级。NMSE 大于 1 基本可以断定数据没对齐——常见的三大原因:复数数组被取模导致相位丢失、导频索引偏了一位、训练集和测试集数据出现相互污染(常见于用 fit 过的归一化器去处理全集)。子集跑通再全量循环,时间增加不多,排查成本却会低很多。跑深度学习网络时这个习惯尤其有用,因为网络一轮 epoch 的时间比 LS 高出好几个数量级,不能靠反复试错来定位数据问题。
4. 把数据集用出价值:补齐样本、划分与对接深度网络
信道估计数据集和图像数据集有一个重要差别:图像数据的类别分布基本固定,而信道统计特性随场景变化很大。如果你拿到的压缩包只有几千帧,想在多径时延、多普勒频移和信噪比三个维度上都覆盖足够场景,大概率不够。所以“内含数据集”的价值往往不是直接拿去训,而是先验证工具链,再按同样的格式自己补一批数据。这章讲三个落地路径。
4.1 用标准信道模型生成补充样本:TDL 参数表与生成代码
最常见的扩数据做法是直接按标准信道模型生成信道冲激响应。ITU-R M.1225、3GPP TR 38.901 里的 TDL 模型给出了每条径的时延和平均功率,COST 207 则覆盖典型城市、乡村、山区场景。我常用的一组 TDL-A 参数如下:
| 径编号 | 时延 (ns) | 平均功率 (dB) |
|---|---|---|
| 1 | 0 | -13.4 |
| 2 | 25 | 0 |
| 3 | 85 | -2.2 |
| 4 | 150 | -4.0 |
| 5 | 255 | -6.0 |
| 6 | 350 | -8.2 |
| 7 | 440 | -13.4 |
生成时注意两点:每条径的相位要从均匀分布 [0, 2π) 随机抽,否则 CIR 里不会有幅度衰落;二是按帧做能量归一化,让单帧的期望能量为 1,这样后面加噪声时才好按 SNR 标定。能量归一化漏掉的话,整份数据集的 SNR 标签会整体偏移,这是最隐蔽的错误之一。
import numpy as np def gen_cfr_from_taps(n_frame, n_subcarrier, delays_ns, pow_db, fs_hz): """按 TDL 抽头参数生成频域 CFR,逐帧能量归一化""" delays = np.array(delays_ns) * 1e-9 amp = np.sqrt(10 ** (np.array(pow_db) / 10)) cfr = np.zeros((n_frame, n_subcarrier), dtype=complex) for f in range(n_frame): phase = np.exp(1j * 2 * np.pi * np.random.rand(len(delays))) for k in range(n_subcarrier): cfr[f, k] = np.sum(amp * phase * np.exp(-1j * 2 * np.pi * k * delays * fs_hz / n_subcarrier)) cfr[f] /= np.sqrt(np.mean(np.abs(cfr[f]) ** 2) + 1e-12) # 帧内能量归一化 return cfr逻辑说明:帧循环内先抽随机相位,子载波循环内按频域响应叠加各径,最后做帧内能量归一化。参数上,fs_hz 是 OFDM 符号的采样率,等于子载波间隔乘以子载波数;delays_ns 的每条径时延要换算成秒后再参与相移计算。生成之后直接拼接进原有数据集的观测文件中,前提是你把发送导频序列也按同样方式生成,保证 x_pilot 的格式一致。这段双循环优先保证可读性,n_frame 到几千、几万时性能会有点吃紧,可以先用它验证生成逻辑,再对子载波维做向量化重写。
4.2 用实测或仿真平台采集:SDR 流程的三个要点
如果你有条件用 USRP、软件无线电或 MATLAB LTE 工具箱采集,流程一般是:发送端发一段已知导频序列,接收端同步后做 FFT,取导频子载波上的解调结果作为观测。注意实测数据的“理想信道标签”本身也是估计出来的,所以它的误差不可能为零。一篇论文如果拿实测数据灌进 LMMSE 还能得到和仿真一样漂亮的曲线,通常是对数据做了过度清洗,只保留了高信噪比时段。对大多数入门场景,我建议先做到仿真数据自洽,再碰实测,否则排查问题的维度会多一倍。
采集时有三个要点:同步位置必须和导频起点对齐,偏差一个采样点会让相位以子载波频率线性旋转;收发端采样钟偏差必须在采集时先校正,不然数据集里混入一个系统性的频偏;记录环境参数(天线高度、移动速度、频段),因为信道模型的时延扩展和多普勒都依赖这些标签,缺了它们数据基本没有复用价值。
4.3 按场景划分数据:一份防泄漏的划分代码
数据集划分有一个容易被忽略的原则:按场景划分,而不是按帧随机划分。同一组信道参数生成的帧全部落在同一个子集里,不能打散到训练和测试两端。否则网络会通过记忆场景特征来“作弊”,跨场景泛化测试必然惨败。很多复现论文的人发现自己“跑出来比论文好很多”,不是模型更强,而是数据划分无意中做错了。划分代码:
def split_by_scene(unified_ids, train_ratio=0.6, seed=42): """按场景 ID 划分,同一场景的帧必须进同一个子集""" scenes = np.unique(unified_ids) rng = np.random.default_rng(seed) scenes = rng.permutation(scenes) n_train = int(len(scenes) * train_ratio) train_scenes, test_scenes = scenes[:n_train], scenes[n_train:] train_mask = np.isin(unified_ids, train_scenes) test_mask = np.isin(unified_ids, test_scenes) return train_mask, test_mask参数上,unified_ids 是每一帧所属场景的唯一编号,它来自生成配置或采集环境信息;train_ratio 是这个划分里最重要的超参数,0.6 起步,数据量大时可调到 0.8。seed 固定下来保证实验可复现,这点在做对比实验时尤其重要。
4.4 对接深度网络:复数输入怎么变成两通道实数
目前信道估计方向做深度学习的套路大致是:把接收到的导频观测和已知导频拼接成复数特征图,输入 CNN 或 transformer 结构,输出全子载波上的信道频响。这类网络通常吃不了复数张量,要拆成实部虚部两通道。转换代码只有一行,但维度顺序容易错:
def complex_to_two_channel(x): """把复数观测 [..., n_pilot] 变成 [..., 2, n_pilot] 的实数张量""" return np.stack([x.real, x.imag], axis=-2)参数说明:axis=-2 表示在倒数第二维插入通道维,得到 [batch, 2, n_pilot]。如果你不小心用了 axis=-1,得到的是 [batch, n_pilot, 2],卷积核对通道维的假设就全错了。数据包里如果只有 LS 估计值而没有原始观测 y,也能训练,但网络会退化成对 LS 结果做后处理,性能天花板被 LS 本身限制住。拿到包后优先确认有没有原始观测和导频序列,这决定了你能做的方法范围。
5. 避坑记录:从解压到训练的五个真实翻车现场
这章写的是这条数据路线上我实际踩过、以及帮人排查时看到的典型问题。每一条都按“现象 → 原因 → 解决”写,可以直接对照排查。
5.1 scipy 读 .mat 报 NotImplementedError:v7.3 不是老格式
现象:scipy.io.loadmat("h_channel.mat") 抛异常,提示 NotImplementedError: Please use HDF reader for matlab v7.3 files。
原因:文件是 MATLAB R2014b 以后用 -v7.3 保存的,底层是 HDF5 容器。scipy 不认这种格式,h5py 能开,但开出来的数据维度顺序和键名都与常规 MAT 不同。
解决:改用 h5py 读取,拿到 keys 和 shape;数据矩阵按 Fortran 到 C 的顺序做转置。建议封装一个 load_dataset_mat() 函数,内部走 h5py 并统一转置,后边所有代码只调这个函数,避免在工程里散落二十处临时转置。
5.2 h5py 读出来维度全反了
现象:README 写 h_true 是 [10000, 64],h5py 读出来是 [64, 10000]。你以为是对齐了,结果训练时 batch 维度张冠李戴,loss 乱跳。
原因:MATLAB 列主序,HDF5 里存的是转置后的排列;直接搬运过来没做维度调整。
解决:二维数组用 .T 转置。三维数组要分情况处理:如果是 [n_frame, n_subcarrier, n_rx] 存成 [n_rx, n_subcarrier, n_frame],用 np.moveaxis(arr, -1, 0),不能用 .T 一刀切。这种问题有时候表现得很像玄学,但只要在读取函数里固定加一句 shape 断言,就能把它挡在进入训练之前。
5.3 SNR 标签与数据对不上,NMSE 曲线整体平移
现象:画出的 LS 曲线在 20 dB 处 NMSE 还有 0.1,论文基线在 20 dB 处已经到 0.01。你怀疑数据集有问题,但重新读一遍发现文件没坏。
原因:大概率是噪声功率换算错了。加噪时如果按实数域的 SNR 定义来做信号功率除以噪声功率,而数据是复数,复数噪声的总方差要按每维功率定义重新换算。另一个常见原因是能量归一化与加噪顺序颠倒:标签 H 做了归一化,加噪用的却是归一化前的信号功率。
解决:先把标签和观测统一归一到同一基准,再做噪声注入。公式要写死:如果信号能量归一化为 1,复数域噪声总功率 = 10^(-SNR/10);如果按 I/Q 每维各占一半功率,则每维噪声方差 = 10^(-SNR/10)/2。两种口径哪个对,取决于数据集 README 里加噪时是用了总功率还是每维功率。把公式和口径写进注释,避免三个月后返工。
5.4 一次性 np.load 把内存耗尽
现象:几 GB 的 .npy 文件 load 到一半,进程被杀,机器开始疯狂换页,训练进度条完全不动。
原因:默认 np.load 会把整个数组读进内存,而信道数据动辄 [上万帧, 上千子载波, 多天线],轻松超过 8 GB。
解决:用 np.load(..., mmap_mode="r") 做内存映射,确认数据结构再分批读取;训练阶段用 DataLoader、tf.data 的流式接口,不要一次性把全集搬进显存。另一个低成本的优化是转一遍 dtype:complex128 降到 complex64,内存直接减半,物理层数据基本不需要双精度。
5.5 解压环节卡住:伪加密和中文乱码
现象:右键解压弹窗要密码,但数据来源明确说“公开数据、无密码”;或者解压之后文件名全是乱码,脚本按路径找文件全部失败。
原因:压缩包可能是 zip 伪加密(加密标志位置位但实际没有加密内容),也可能是文件名用了 GBK 编码,在 Linux/macOS 下被按 UTF-8 解码,显示成乱码。
解决:伪加密用 7-Zip 打开后忽略密码错误直接导出;乱码文件名在 Linux 下用 unzip -O gbk 指定编码解压。这个阶段不值得花太多时间找所谓“zip 密码移除”工具,先确认文件本体是否加密,多数情况下只是标志位问题。
6. 校验数据集的真实价值:三分钟复现曲线与一套可留用的验证脚本
跑通只是第一步,更要紧的是判断这份“内含数据集”值不值得投入。判断标准不是文件多不多、有没有 README,而是你能不能在三五分钟内复现出论文或说明文档里的基线曲线。做法是拿 LS 和 LMMSE 各跑一条 NMSE-SNR 曲线,和权威参考值对比。
6.1 一套最小复现脚本:LS、LMMSE 与理想上界
下面这套函数可以直接留在工程里当模板,换数据集时只改字段名:
import numpy as np def lmmse_pilot(y_p, R_hh, sigma2): """导频位置 LMMSE 估计""" n_p = R_hh.shape[0] R_pp = R_hh + sigma2 * np.eye(n_p) return R_hh @ np.linalg.solve(R_pp, y_p) def evaluate_baselines(data, snr_list): """按 SNR 分组输出 LS / LMMSE / 理想上界的 NMSE""" result = {} for snr in snr_list: mask = np.isclose(data["snr"], snr) h_s = data["h_true"][mask] y_s = data["rx_pilot"][mask] x_s = data["x_pilot"][mask] h_ls_p = y_s / np.maximum(np.abs(x_s), 1e-12) h_ls = interpolate_pilots(h_ls_p, 4, h_s.shape[1]) h_lm_p = lmmse_pilot(h_ls_p, R_hh, 1 / (10 ** (snr / 10))) h_lm = interpolate_pilots(h_lm_p, 4, h_s.shape[1]) result[snr] = { "ls_nmse": nmse(h_ls, h_s), "lmmse_nmse": nmse(h_lm, h_s), "ideal_nmse": 0.0, } return result逻辑上,LMMSE 的 sigma2 直接由 snr 线性换算,1/(10^(snr/10)) 对应复数总噪声功率为 1 的信号设定。如果你数据集的能量归一化基准不同,这条换算也要跟着调。interpolate_pilots 就是用 3.3 节的 interp1d 逻辑封装出来的辅助函数,签名是 interpolate_pilots(h_pilot, interval, n_total),内部用 np.arange(0, n_total, interval) 生成导频索引再插值。评估时有一件事要养成习惯:LMMSE 的 R_hh 只用训练集估计,测试集只负责计算 NMSE,这是整条链路上最不能妥协的边界。
6.2 把数据集当作跨场景泛化的试验场
比起多跑几条曲线,更有价值的投入是把这份数据集当作“预训练 + 微调”的语料。做法是:在 A 场景数据上训练一个轻量 CNN 信道估计器,在 B 场景数据上做少量微调,观察微调后 NMSE 的下降幅度。这个实验能检验数据集的场景多样性是否足够,也能衡量网络是否真的学到了信道统计结构而不是记住了坐标。我最近一次复现就是用这个办法,把公开包里的城区场景与高铁场景做跨域迁移,验证集 NMSE 比同域差了 2~3 dB,这个差距直接写进了报告,避免别人高估方案。
作为长期习惯,我建议每份数据集都留一个纯脚本目录,跑通后把数据读取、维度转换、基线评估和绘图四件事固化下来。下次换新数据集时,只需要改文件路径和字段名,半天内就能从裸包跑到第一版曲线。这个习惯帮我省掉了很多重复的“从零开始”,也希望帮到你。
本文还有配套的精品资源,点击获取