【MATLAB+Python】基于深度学习增强 LS 信道估计的代码实现
·
- 项目简介
本项目是一套基于深度神经网络(DNN)的OFDM信道估计算法仿真代码。在传统的信道估计中,最小二乘(LS)算法计算复杂度低,但在低信噪比下易受噪声影响;最小均方误差(MMSE)算法估计精度高,但需要已知信道的二阶统计特性,且计算复杂度较高。
本项目参考相关文献,采用“先进行LS粗估计,再利用DNN进行误差修正”的方案。通过让神经网络学习多径衰落信道下的误差分布,可以在不依赖信道先验统计信息的情况下,有效提升信道估计的精度。
- 主要功能特点
- 联合仿真环境:采用MATLAB进行无线通信物理层仿真与原始数据生成,采用Python配合Keras框架进行深度学习网络的构建与训练。
- 多网络架构对比:代码内置了4种不同隐藏层和神经元数量的DNN架构,方便对比网络深度与宽度对信道估计性能的影响。
- 模型训练优化:在Python训练代码中引入了动态学习率衰减(ReduceLROnPlateau)和早停机制(Early Stopping),以改善模型在高信噪比场景下的收敛情况,降低归一化均方误差(NMSE)。
- 环境依赖
- MATLAB(用于生成物理层信号、计算传统算法基线及最终绘图)
- Python 3.x
- Python库:tensorflow (或 keras), numpy, scipy, scikit-learn
- 代码运行流程
本项目的完整仿真流程分为以下四个步骤:
-
- 物理层数据生成(MATLAB)
运行Main_Simulation.m。该脚本模拟了基于802.11p协议的OFDM系统,在瑞利时变(RTV)多径信道下生成不同信噪比下的原始数据,并计算传统的LS和MMSE估计结果作为性能参考。建议将样本量设置在10万级别以保证深度学习的训练效果。
-
- 数据格式预处理(MATLAB)
运行Process_Training_Data.m。由于标准神经网络无法直接处理复数,该脚本将生成的复数信道数据(LS估计值与真实信道)拆分为实部和虚部,并重组为Python可直接读取的.mat数据集格式。
-
- 模型训练与预测(Python)
运行Python训练和测试脚本。神经网络以LS估计值为输入特征,以真实信道为标签进行监督学习。训练完成后,对测试集进行预测,并将DNN修正后的信道数据导出为.mat文件。
-
- 性能对比与绘图(MATLAB)
运行最终的绘图脚本。代码会读取Python传回的预测结果,计算各算法的归一化均方误差(NMSE),并绘制随信噪比变化的对比曲线图。



clc; % 清除命令行窗口的内容
clearvars; % 清除工作区中的所有变量
close all; % 关闭所有打开的图形窗口
ch_func = Channel_functions(); % 创建信道函数对象,调用 Channel_functions 类
est_func = Estimation_functions(); % 创建估计函数对象,调用 Estimation_functions 类
%% Simulation Parameters
% 仿真参数设置
ChType = 'RTV'; % 设置信道模型为 'RTV'(Rayleigh Time-Varying 信道)
fs = 64*156250; % 设置采样频率为 64*156250 Hz,基于 802.11p 协议,64 个子载波,每个子载波间距 156250 Hz
fc = 5.2e9; % 设置载波频率为 5.2 GHz
v = 0; % 设置用户移动速度为 0 km/h(静止)
c = 3e8; % 设置光速为 3e8 m/s
fD = (v/3.6)/c*fc; % 计算多普勒频率(Hz),公式:fD = (v/3.6)/c*fc,速度单位从 km/h 转换为 m/s
rchan = ch_func.GenFadingChannel(ChType, fD, fs); % 生成衰落信道,调用 GenFadingChannel 方法,输入信道类型、多普勒频率和采样频率
K = 64; % 设置子载波数量为 64
K_cp = K/4; % 设置循环前缀(Cyclic Prefix)的子载波数为 K/4,即 16
% Pre-defined preamble in frequency domain
% 定义频域中的前导序列
dp = [ 0 0 0 0 0 0 +1 +1 -1 -1 +1 +1 -1 +1 -1 +1 +1 +1 +1 +1 +1 -1 -1 +1 +1 -1 +1 -1 +1 +1 +1 +1 0 +1 -1 -1 +1 +1 -1 +1 -1 +1 -1 -1 -1 -1 -1 +1 +1 -1 -1 +1 -1 +1 -1 +1 +1 +1 +1 0 0 0 0 0];
% 定义频域前导序列 dp,长度为 64,包含 +1、-1 和 0
Kset = find(dp~=0); % 找到非零元素的位置,即激活的子载波索引
Kon = length(Kset); % 计算激活子载波的数量
Ep = 1; % 设置前导序列功率为 1
dp = sqrt(Ep)*dp.'; % 对前导序列进行归一化,乘以功率的平方根,并转置为列向量
xp = ifft(dp); % 通过逆傅里叶变换(IFFT)将频域前导序列转换为时域
xp_cp = [xp(end-K_cp+1:end); xp]; % 添加循环前缀,将时域信号末尾 K_cp 个样本添加到信号前面
SNR_p = (0:5:30)'; % 定义信噪比(SNR)范围,从 0 到 30 dB,步长为 5 dB,转置为列向量
N_SNR = length(SNR_p); % 计算 SNR 值的数量
N0 = Ep/K*10.^(-SNR_p/10); % 计算噪声功率,公式:N0 = Ep/(K*10^(SNR/10)),SNR 以 dB 为单位
N_CH = 10000; % 设置信道实现次数为 1000
Err_Ls = zeros(N_SNR,1); % 初始化 LS(最小二乘)估计的归一化均方误差(NMSE)向量
Err_MMSE = zeros(N_SNR,1); % 初始化 MMSE(最小均方误差)估计的 NMSE 向量
Phf = zeros(N_SNR,1); % 初始化平均信道功率向量 E(|hf|^2)
%% Rh estimation
% 信道自相关矩阵估计
release(rchan); % 释放信道对象,清除之前的设置
init_seed = 22; % 设置初始随机种子为 22,用于可重复的随机数生成
rchan.Seed = init_seed; % 将信道对象的种子设置为初始种子
Rh = est_func.Estimat_Rh(rchan, K_cp, K, Kset); % 估计信道自相关矩阵 Rh,输入信道对象、循环前缀长度、子载波数和激活子载波索引
release(rchan); % 再次释放信道对象
rchan.Seed = init_seed; % 重置信道对象的种子
%% Main Simulation
% 主仿真循环
for n_snr = 1:N_SNR % 遍历所有 SNR 值
disp(['Running Simulation, SNR = ', num2str(SNR_p(n_snr))]); % 显示当前仿真的 SNR 值
tic; % 启动计时器,记录仿真时间
Hfe_LS = zeros(Kon, N_CH); % 初始化 LS 估计的频域信道矩阵
Hfe_MMSE = zeros(Kon, N_CH); % 初始化 MMSE 估计的频域信道矩阵
Hf = zeros(Kon, N_CH); % 初始化真实频域信道矩阵
% MMSE 滤波器随 SNR 变化
W = est_func.MMSE_matrix(dp(Kset), Rh, Ep/N0(n_snr)/K); % 计算 MMSE 滤波矩阵,输入激活子载波的前导序列、信道自相关矩阵和噪声功率
for n_ch = 1:N_CH % 遍历所有信道实现
% 理想信道估计
[ h, y ] = ch_func.ApplyChannel(rchan, xp_cp, K_cp); % 应用信道,生成时域信道响应 h 和接收信号 y
release(rchan); % 释放信道对象
rchan.Seed = rchan.Seed+1; % 递增信道种子,生成不同的信道实现
yp = y((K_cp+1):end); % 移除接收信号的循环前缀,提取有效部分
h = h((K_cp+1):end); % 移除信道响应的循环前缀
yfp = fft(yp); % 对接收信号进行傅里叶变换,转换为频域
hf = fft(h); % 对信道响应进行傅里叶变换,转换为频域
Phf(n_snr) = Phf(n_snr) + norm(hf(Kset))^2; % 计算激活子载波的信道功率并累加
% 添加噪声
yfp_r = yfp + sqrt(K*N0(n_snr))*ch_func.GenRandomNoise([1,1], 1); % 在频域信号上添加高斯白噪声
% LS 估计
[hfe_ls, err_ls] = est_func.LS(yfp_r(Kset), dp(Kset), hf(Kset)); % 进行 LS 估计,计算估计信道和误差
Err_Ls(n_snr) = Err_Ls(n_snr) + err_ls; % 累加 LS 估计的误差
% MMSE 估计
[hfe_mmse, err_mmse] = est_func.MMSE(yfp_r(Kset), W, hf(Kset)); % 进行 MMSE 估计,计算估计信道和误差
Err_MMSE(n_snr) = Err_MMSE(n_snr) + err_mmse; % 累加 MMSE 估计的误差
% 保存信道数据供后续使用
Hf(:,n_ch) = hf(Kset); % 保存真实频域信道
Hfe_LS(:,n_ch) = hfe_ls; % 保存 LS 估计的频域信道
Hfe_MMSE(:,n_ch) = hfe_mmse; % 保存 MMSE 估计的频域信道
end
% 保存仿真数据
save(['./Dataset_' num2str(n_snr)], 'Hf', 'Hfe_LS'); % 保存真实信道和 LS 估计信道到文件
toc; % 停止计时器,显示仿真时间
end
%% Averaging over channel realizations
% 对信道实现取平均
Phf = Phf/N_CH; % 计算平均信道功率
Err_Ls = Err_Ls/N_CH; % 计算 LS 估计的平均 NMSE
Err_MMSE = Err_MMSE/N_CH; % 计算 MMSE 估计的平均 NMSE
%% Theorectical LS NMSE Calculation
% 理论 LS NMSE 计算
Err_Ls_th = Kon*K*N0/Ep; % 计算理论 LS NMSE,公式:Kon*K*N0/Ep
%% Theorectical MMSE NMSE Calculation
% 理论 MMSE NMSE 计算
release(rchan); % 释放信道对象
init_seed = 22; % 重置初始种子
rchan.Seed = init_seed; % 设置信道种子
Rh = est_func.Estimat_Rh(rchan, K_cp, K, Kset); % 重新估计信道自相关矩阵
release(rchan); % 释放信道对象
rchan.Seed = init_seed; % 重置信道种子
Sig = real(eig(Rh)); % 计算信道自相关矩阵的特征值(取实部)
Err_MMSE_th = zeros(N_SNR,1); % 初始化理论 MMSE NMSE 向量
for n_snr = 1:N_SNR % 遍历所有 SNR 值
Err_MMSE_th(n_snr) = Err_MMSE_th(n_snr) + sum(Sig./(Sig+K*N0(n_snr)./Ep)); % 计算理论 MMSE NMSE
end
Err_MMSE_th = K*N0/Ep .* Err_MMSE_th; % 归一化理论 MMSE NMSE
%% Normalization by Channel Power
% 按信道功率归一化
Err_LSth = Err_Ls_th./Phf; % 理论 LS NMSE 除以平均信道功率
Err_LSsim = Err_Ls./Phf; % 仿真 LS NMSE 除以平均信道功率
Err_MMSEth = Err_MMSE_th./Phf; % 理论 MMSE NMSE 除以平均信道功率
Err_MMSEsim = Err_MMSE./Phf; % 仿真 MMSE NMSE 除以平均信道功率
%% Plotting NMSE Results
% 绘制 NMSE 结果
figure, % 创建新的图形窗口
semilogy(SNR_p, Err_LSth, 'k--', 'LineWidth', 2);
hold on;
semilogy(SNR_p, Err_LSsim, 'k+', 'LineWidth', 2);
hold on;
semilogy(SNR_p, Err_MMSEth, 'k--', 'LineWidth', 2);
hold on;
semilogy(SNR_p, Err_MMSEsim, 'ko', 'LineWidth', 2);
hold on;
grid on;
legend('Analytical-Ls', 'sim-LS', 'Analytical-MMSE', 'sim-MMSE') % 添加图例
% xlabel('Preamble SNR')
% ylabel('Average Error per subcarrier')
xlabel('SNR')
ylabel('NMSE')
save('ErrSet.mat', 'Err_LSth', 'Err_LSsim', 'Err_MMSEth', 'Err_MMSEsim')
clc; % 清除命令行窗口的所有输出
clearvars; % 清除工作区中的所有变量,释放内存
close all; % 关闭所有打开的图形窗口
N_CH = 10000; % 定义样本数量(信道实现次数)。注意:这里是 1 万个样本
SNR_p = 0:5:30; % 定义信噪比 (SNR) 的范围:从 0dB 到 30dB,步长为 5dB
k = 52; % 定义激活的有效子载波数量 (与论文中的 Kon = 52 对应)
% 初始化深度学习的输入特征矩阵 X 和目标标签矩阵 Y
% 维度为 104 x N_CH (104 是因为实部 52 个 + 虚部 52 个)
X = zeros(k*2, N_CH);
Y = zeros(k*2, N_CH);
% 遍历所有的信噪比场景 (n_snr 会从 1 循环到 7)
for n_snr = 1: size(SNR_p,2)
% 加载主仿真脚本 (Main_Simulation.m) 生成的原始结果
% Hf: 真实的频域信道响应;Hfe_LS: LS 算法粗略估计的频域信道响应
load(['./Dataset_',num2str(n_snr),'.mat'], 'Hf', 'Hfe_LS');
% 构建深度学习的输入特征 X (LS 估计值)
X(1:k,:) = real(Hfe_LS); % 前 52 行存入 LS 估计值的实部
X(k+1:2*k,:) = imag(Hfe_LS); % 后 52 行存入 LS 估计值的虚部
% 构建深度学习的目标标签 Y (信道真实值)
Y(1:k,:) = real(Hf); % 前 52 行存入真实信道的实部
Y(k+1:2*k,:) = imag(Hf); % 后 52 行存入真实信道的虚部
% 将 X 和 Y 转置 (从 104 x N_CH 变为 N_CH x 104),以符合 Python Keras 模型的数据输入规范
% 并将它们打包进一个名为 Preamble_Error_Correction_Dataset 的结构体中
Preamble_Error_Correction_Dataset.('X') = X.';
Preamble_Error_Correction_Dataset.('Y') = Y.';
% 将打包好的结构体保存为新的 .mat 文件,专门供 Python 脚本读取训练
save(['./DNN_Dataset_' num2str(n_snr),'.mat'], 'Preamble_Error_Correction_Dataset');
end
from keras import Sequential
from keras.callbacks import ModelCheckpoint
from keras.initializers import TruncatedNormal # Corrected import
from keras.layers import Dense
from scipy.io import loadmat
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
snr = 7 # SNR Index 1: 0dB, 2: 5dB, 3: 10dB, 4: 20dB, etc...
# Load Matlab DataSets
mat = loadmat('./DNN_Dataset_{}.mat'.format(snr))
Dataset = mat['Preamble_Error_Correction_Dataset']
Dataset = Dataset[0, 0]
X = Dataset['X']
Y = Dataset['Y']
print('Loaded Dataset Inputs: ', X.shape)
print('Loaded Dataset Outputs: ', Y.shape)
# Normalizing Datasets
scalerx = StandardScaler()
scalerx.fit(X)
scalery = StandardScaler()
scalery.fit(Y)
XS = scalerx.transform(X)
YS = scalery.transform(Y)
# Split Data into train and test sets
seed = 7
train_X, test_X, train_Y, test_Y = train_test_split(XS, YS, test_size=0.2, random_state=seed)
print('Training samples: ', train_X.shape[0])
print('Testing samples: ', test_X.shape[0])
# Build the model.
init = TruncatedNormal(mean=0.0, stddev=0.05, seed=None)
model = Sequential([
Dense(units=104, activation='relu', input_dim=104,
kernel_initializer=init,
bias_initializer=init),
Dense(units=104, activation='relu',
kernel_initializer=init,
bias_initializer=init),
Dense(units=104, kernel_initializer=init,
bias_initializer=init)
])
# Compile the model.
model.compile(loss='mean_squared_error', optimizer='adam', metrics=['acc'])
print(model.summary())
model_path = './LS_DNN_{}.h5'.format(snr)
# This checkpoint saves the best DNN model with the highest validation accuracy
checkpoint = ModelCheckpoint(model_path, monitor='val_acc',
verbose=1, save_best_only=True,
mode='max')
callbacks_list = [checkpoint]
# Train the model.
epoch = 500
batch_size = 32
model.fit(train_X, train_Y, epochs=epoch, batch_size=batch_size, verbose=2, validation_split=0.25, callbacks=callbacks_list)
更多推荐
所有评论(0)