摘要

随着 6G 无线通信向太赫兹(THz)频段拓展,信道估计成为制约系统性能的关键瓶颈。传统方法依赖 Kramers-Kronig关系对接收强度进行数值积分以恢复相位信息,计算复杂度高、延迟大,难以满足 6G 亚毫秒级实时性要求。本项目提出一种基于深度学习的端到端信道估计框架,直接从光电探测器输出的时域强度波形映射为完整的复频域信道传递函数 ,替代了传统的复杂计算。

1 背景意义

1.1 背景

随着 5G 大规模商用,6G 的研究已全面展开。6G 的核心目标之一是实现 Tbps 级别 的峰值速率,这需要在物理层获得远超现有频段的连续带宽。太赫兹频段(0.1 – 10 THz) 提供了高达数十 GHz 甚至上百 GHz 的可用频谱资源,是 6G 实现超高速率的关键使能技术。然而,THz 通信面临三大根本性挑战:

  • 极端路径损耗:THz 波在大气中传播时,受到水汽分子的强烈吸收,在约 0.56 THz、0.75 THz 和 0.988 THz 处形成尖锐的吸收峰,且吸收强度随环境湿度剧烈变化。

  • 频率选择性衰落:500 GHz 以上的超大带宽导致信道频率响应高度非平坦,传统窄带信道模型失效。

  • 硬件限制:在 THz 频段制造高精度、低成本的相干接收机极为困难,商用方案通常采用包络检波器(光电二极管),仅能测量光信号的强度 I(t),而无法直接获取相位信息——但相位对于信号解调至关重要。

因此,如何从强度测量中准确、快速地恢复完整的复信道信息,成为 THz 通信亟待解决的核心问题。

1.2 意义

a. 基于 导频辅助方法(LS / MMSE / OMP):在 OFDM 符号中插入已知导频信号,利用接收信号反推信道响应。

  • THz 频段频率选择性极深,需要密集导频才能捕捉尖锐的频谱凹陷,导频开销高达 10%~30%,严重挤占数据带宽。OMP 等压缩感知方法虽可减少导频数量,但迭代重构算法耗时严重(> 15ms),且对噪声敏感。

b. 基于 Kramers-Kronig关系的相位恢复: Kramers-Kronig 关系指出,对于满足因果律的物理系统,幅度谱与相位谱存在一一对应的数学映射(希尔伯特变换)。因此,理论上可以从测得的全频段强度谱积分恢复相位。

  • 缺陷:KK 积分是非因果的数值积分(柯西主值积分),计算复杂度高,实时通信中需数秒甚至更久才能完成一次信道估计,完全无法满足 6G 微秒级时隙要求

传统方法之所以“慢”,是因为在 “数学规则” 的层面做积分求解。既然神经网络是通用函数逼近器,为什么不直接用数据驱动的方式,让 神经网络直接去学习从强度波形 I(t) 到复数信道 H(f) 的端到端映射呢。

2 数据集生成

thz信道建模公式如下:

2.1 LOS链路

视距路径(LOS)同时受自由空间扩散与分子吸收的共同作用:
在这里插入图片描述
其中Hz 频段受水汽分子谐振影响,吸收系数建模为三个高斯峰的叠加:

在这里插入图片描述

2.2 NLOS链路

菲涅耳反射系数:
在这里插入图片描述
Beckmann粗糙度散射衰减:

在这里插入图片描述
其中,theta表示墙面均方根粗糙高度。最终第 l条 NLoS 路径的等效衰减为:
在这里插入图片描述

2.3 频域复信道传递函数 H(f)

将所有路径在频域进行相干叠加(LoS + 多条 NLoS 路径)
在这里插入图片描述
在这里插入图片描述

2.4 时域接收脉冲 r(t)

发射脉冲为高斯包络调制信号,令中心频率 0.75 THz:
在这里插入图片描述
接收端多径叠加信号为:

在这里插入图片描述

2.6 外差探测与平方律检波(接收机实际接收到的幅度I(t) )

接收机引入本地振荡器进行外差混频:
在这里插入图片描述
光电二极管输出的强度波形为平方律检波结果:
在这里插入图片描述

2.7 整体流程

在这里插入图片描述

3 神经网络算法设计

基于第二节的数据集,令强度波形I(t)为神经网络的输入,频域信道H(f)为输出,包含实部+虚部。设计思路如下:

  • 用 CNN 从强度波形中提取局部特征(脉冲位置、宽度、衰减趋势)
  • 再用全连接层把这些特征组合成 实部+虚部的输出,直接预测出完整的复频域信道。
模块 层名称 输入形状 输出形状 参数量 说明
输入层 Input (B, 1, 1000) (B, 1, 1000) 0 B=批量大小,1=通道数,1000=时间采样点
特征提取器 Conv1d Block 1 (B, 1, 1000) (B, 32, 497) 256 卷积核大小7,步长2,填充3
BatchNorm1d (B, 32, 497) (B, 32, 497) 64 批归一化,加速收敛
ReLU (B, 32, 497) (B, 32, 497) 0 激活函数,引入非线性
MaxPool1d (B, 32, 497) (B, 32, 248) 0 池化核大小2,下采样
特征提取器 Conv1d Block 2 (B, 32, 248) (B, 64, 122) 10,304 卷积核大小5,步长2,填充2
BatchNorm1d (B, 64, 122) (B, 64, 122) 128 批归一化
ReLU (B, 64, 122) (B, 64, 122) 0 激活函数
MaxPool1d (B, 64, 122) (B, 64, 61) 0 池化核大小2,下采样
特征提取器 Conv1d Block 3 (B, 64, 61) (B, 128, 30) 24,704 卷积核大小3,步长2,填充1
BatchNorm1d (B, 128, 30) (B, 128, 30) 256 批归一化
ReLU (B, 128, 30) (B, 128, 30) 0 激活函数
MaxPool1d (B, 128, 30) (B, 128, 30) 0 池化核大小2,步长1(尺寸不变)
展平层 Flatten (B, 128, 30) (B, 3840) 0 将多维特征图展平为一维向量
回归头 Linear 1 (B, 3840) (B, 1024) 3,933,184 全连接层,降维
BatchNorm1d (B, 1024) (B, 1024) 2,048 批归一化
ReLU (B, 1024) (B, 1024) 0 激活函数
Dropout (B, 1024) (B, 1024) 0 随机丢弃30%神经元,防止过拟合
回归头 Linear 2 (B, 1024) (B, 512) 524,800 全连接层,进一步压缩
BatchNorm1d (B, 512) (B, 512) 1,024 批归一化
ReLU (B, 512) (B, 512) 0 激活函数
Dropout (B, 512) (B, 512) 0 随机丢弃20%神经元
回归头 Linear 3 (B, 512) (B, 800) 410,400 输出层,线性激活
层名称 计算公式 参数量
Conv1d Block 1 1 × 32 × 7 + 32 256
Conv1d Block 2 32 × 64 × 5 + 64 10,304
Conv1d Block 3 64 × 128 × 3 + 128 24,704
BatchNorm(各层合计) 2 × 通道数之和 3,520
Linear 1 3840 × 1024 + 1024 3,933,184
Linear 2 1024 × 512 + 512 524,800
Linear 3 512 × 800 + 800 410,400
总计 2,941,088

使用mse损失函数,最小化 MSE 等价于最小化预测信道与真实信道之间的欧氏距离,直接对应 NMSE 指标的优化。
在这里插入图片描述
经过上述设计,让网络学会从强度波形 I(t) 准确映射出复频域信道 H(f)。

4 仿真分析

对于测试集包含 N个样本、每个样本有 F个子载波的情况,线性 NMSE 指标为:
在这里插入图片描述
通信领域通常用 dB(分贝) 表示,因为动态范围大
在这里插入图片描述

  • 数值越小越好代表误差越小
  • -34.30 dB 意味着误差功率仅为信号功率的 0.037%

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看到:

  • 真实值和预测值在全频段上基本上重合,尤其是在水汽吸收峰(560 GHz、750 GHz、988 GHz)附近,模型仍能准确捕捉尖锐的衰减凹陷。
  • 在 64 万个测试频点(1600 样本 × 400 子载波)上,模型的预测误差高度集中在零误差附近,验证了模型的稳定性和可靠性。
  • 相比传统的 LS 提升约 10 dB,相比 OMP 提升约 9 dB,。且没有导频开销。

部分代码:

import os
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"

import sys
import torch
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT / "src"))

import yaml
from dataset import get_dataloaders
from model import KKNet


def load_config(path: Path = PROJECT_ROOT / "configs" / "config.yaml") -> dict:
    with open(path, "r", encoding="utf-8") as f:
        return yaml.safe_load(f)


def calculate_nmse(Y_true: torch.Tensor, Y_pred: torch.Tensor) -> float:
    """
    计算归一化均方误差(NMSE),单位 dB。
    NMSE = E[ |H_pred - H_true|^2 / |H_true|^2 ]
    返回值:NMSE (dB),数值越低越好。
    """
    error_power = torch.abs(Y_pred - Y_true) ** 2
    true_power = torch.abs(Y_true) ** 2
    nmse = torch.mean(error_power / (true_power + 1e-10)).item()
    return 10 * np.log10(nmse)


def evaluate_model(config: dict | None = None):
    if config is None:
        config = load_config()

    cfg_paths = config["paths"]
    cfg_model = config["model"]
    cfg_ds = config["dataset"]

    data_path = PROJECT_ROOT / cfg_paths["data_dir"] / cfg_paths["dataset_file"]
    model_path = PROJECT_ROOT / cfg_paths["model_dir"] / cfg_paths["model_file"]
    results_dir = PROJECT_ROOT / cfg_paths["results_dir"]
    results_dir.mkdir(parents=True, exist_ok=True)

    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(f"{'='*60}")
    print(f"  KKNet 模型评估")
    print(f"{'='*60}")
    print(f"  设备 : {device}")

    # -- 加载数据 --------------------------------------------------------------
    _, _, test_loader, scalers = get_dataloaders(
        data_path=str(data_path),
        batch_size=256,
        val_split=cfg_ds["val_split"],
        test_split=cfg_ds["test_split"],
        num_workers=cfg_ds["num_workers"],
    )
    Y_mean = scalers["Y_mean"].to(device)
    Y_std = scalers["Y_std"].to(device)

    # -- 加载模型 --------------------------------------------------------------
    print(f"  正在加载模型: {model_path}...")
    model = KKNet(
        input_length=cfg_model["input_length"],
        output_size=cfg_model["output_size"],
        dropout_1=cfg_model["dropout_1"],
        dropout_2=cfg_model["dropout_2"],
    ).to(device)
    model.load_state_dict(torch.load(str(model_path), map_location=device, weights_only=True))
    model.eval()

    # -- 推理(测试集) ----------------------------------------------------------
    all_true, all_pred = [], []
    print("  正在测试集上推理...")

    with torch.no_grad():
        for X_batch, Y_batch in test_loader:
            X_batch = X_batch.to(device)
            Y_batch = Y_batch.to(device)

            predictions = model(X_batch)

            # 反归一化恢复到物理单位
            Y_true_phys = (Y_batch * Y_std) + Y_mean
            Y_pred_phys = (predictions * Y_std) + Y_mean

            # 从 [实部 | 虚部] 拼接格式重构复数信道
            n = cfg_model["output_size"] // 2  # = 400 个子载波
            Y_true_c = torch.complex(Y_true_phys[:, :n], Y_true_phys[:, n:])
            Y_pred_c = torch.complex(Y_pred_phys[:, :n], Y_pred_phys[:, n:])

            all_true.append(Y_true_c.cpu())
            all_pred.append(Y_pred_c.cpu())

    final_true = torch.cat(all_true, dim=0)
    final_pred = torch.cat(all_pred, dim=0)

    # -- 计算评估指标 ------------------------------------------------------------
    nmse_db = calculate_nmse(final_true, final_pred)
    print(f"\n  [OK] 测试集 NMSE : {nmse_db:.2f} dB")
    print(f"  [OK] 测试样本数  : {len(final_true)}")

    # 保存指标到文本文件
    results_txt = results_dir / "evaluation_results.txt"
    with open(results_txt, "w", encoding="utf-8") as f:
        f.write(f"KKNet -- THz 信道估计评估结果\n")
        f.write(f"{'='*50}\n")
        f.write(f"测试集 NMSE : {nmse_db:.4f} dB\n")
        f.write(f"测试样本数  : {len(final_true)}\n")
        f.write(f"模型路径    : {model_path}\n")
    print(f"  [OK] 结果已保存至 {results_txt}")

    # -- 绘图 ------------------------------------------------------------------
    _plot_channel_response(final_true, final_pred, results_dir, sample_indices=[800, 700, 100, 50])
    _plot_magnitude_error_distribution(final_true, final_pred, results_dir)

    print(f"\n{'='*60}\n")
    return nmse_db, final_true, final_pred


def _plot_channel_response(
    Y_true: torch.Tensor,
    Y_pred: torch.Tensor,
    results_dir: Path,
    sample_indices: list[int] | None = None,
):
    """绘制4个测试样本的预测 vs 真实信道幅度响应,每个子图显示 NMSE。"""
    if sample_indices is None:
        sample_indices = [0, 5, 10, 20]
    # 确保至少4个样本
    if len(sample_indices) < 4:
        sample_indices = list(range(min(4, Y_true.shape[0])))
    sample_indices = sample_indices[:4]

    freqs = np.linspace(500, 1000, Y_true.shape[1])  # GHz

    fig, axes = plt.subplots(2, 2, figsize=(14, 8), sharex=True, sharey=True)
    axes = axes.flatten()

    for ax, idx in zip(axes, sample_indices):
        true_mag = 20 * np.log10(np.abs(Y_true[idx].cpu().numpy()) + 1e-15)
        pred_mag = 20 * np.log10(np.abs(Y_pred[idx].cpu().numpy()) + 1e-15)

        # 计算该样本的 NMSE(与 calculate_nmse 逻辑一致:先除后平均)
        error = Y_pred[idx] - Y_true[idx]
        nmse = 10 * np.log10(torch.mean((torch.abs(error) ** 2) / (torch.abs(Y_true[idx]) ** 2 + 1e-10)))

        ax.plot(freqs, true_mag, 'm-', linewidth=1, label='真实值')
        ax.plot(freqs, pred_mag, 'k--', linewidth=2, label='深度学习预测')
        ax.set_title(f'测试样本 #{idx}  |  NMSE = {nmse:.2f} dB', fontsize=11)
        ax.grid(True, alpha=0.3)
        if ax == axes[0] or ax == axes[2]:
            ax.set_ylabel('|H(f)| [dB]')
        if ax == axes[2] or ax == axes[3]:
            ax.set_xlabel('频率 [GHz]')
        ax.legend(fontsize=9)

    fig.suptitle('测试集样本预测与真实信道对比', fontsize=14, fontweight='bold')
    plt.tight_layout()
    out = results_dir / "channel_response_comparison.png"
    plt.savefig(out, dpi=200, bbox_inches='tight')
    print(f"  [OK] 信道响应对比图已保存至 {out}")
    plt.show()


def _plot_magnitude_error_distribution(
    Y_true: torch.Tensor,
    Y_pred: torch.Tensor,
    results_dir: Path,
):
    """绘制测试集所有子载波的幅度相对误差直方图(中文版)"""
    true_mag = torch.abs(Y_true)
    pred_mag = torch.abs(Y_pred)
    rel_err = ((pred_mag - true_mag) / (true_mag + 1e-10)).flatten().numpy()

    plt.figure(figsize=(8, 5))
    plt.hist(rel_err, bins=100, color="#4CAF50", edgecolor="white", alpha=0.85)
    plt.axvline(0, color="red", linestyle="--", linewidth=1.5, label="零误差")
    plt.xlabel("相对幅度误差  (预测值 - 真实值) / 真实值")
    plt.ylabel("频次")
    plt.title("测试集幅度误差分布", fontsize=13, fontweight="bold")
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    out = results_dir / "error_distribution.png"
    plt.savefig(out, dpi=150, bbox_inches="tight")
    print(f"  [OK] 误差分布图已保存至 {out}")
    plt.show()


if __name__ == "__main__":
    cfg = load_config()
    evaluate_model(cfg)

5 总结

本项目为 THz 通信提供了一种高效、低延迟、高精度的信道估计方法。意义如下:

  • 理论意义:验证了深度神经网络可以成功“学习”物理层中复杂的 因果约束关系,为“AI 替代物理逆问题求解”提供了典型案例。
  • 工程价值:为 THz 通信接收机提供了一种低硬件成本、低延迟的信道估计方案,推动 6G 从理论走向实际部署。

源代码 出图所见即所得,代码获取方式见VX公众号

更多推荐