摘要

本项目针对5G超密集组网(UDN)场景下传统切换算法存在的乒乓效应、信令开销大、服务质量(QoS)保障不足等问题,提出了一种基于深度强化学习(DRL)的智能切换决策框架,将切换决策建模为马尔可夫决策过程,引导智能体学习长期最优策略,然后实现了基于深度Q网络(DQN)和近端策略优化(PPO)两种强化学习算法,并与传统的贪心SINR算法进行对比仿真。实验结果验证了DRL在抑制乒乓效应、保障紧急业务连续性方面的优势,为5G网络智能化运维提供了理论依据和实践参考,凸显了强化学习在无线资源管理领域的应用潜力。

关键词:5G超密集组网;深度强化学习;切换优化;乒乓效应;DQN;PPO

1 背景和意义

1.1 研究背景

5G超密集组网:从“广覆盖”到“深穿透”的范式转移:

  • 第五代移动通信技术(5G NR)不仅是一场速度革命,更是一次网络架构的根本性重构。为了实现千兆级吞吐量(eMBB)、毫秒级端到端延迟(URLLC)以及百万级物联连接(mMTC),5G 广泛采用高频段通信(FR1:<6GHz, FR2:毫米波)。然而,高频段的物理特性决定了其覆盖半径远小于传统4G宏站——城区微站覆盖半径通常缩小至 50 米至 300 米。
  • 这种超密集组网(UDN, Ultra-Dense Network)在提升空口容量的同时,也带来了一个尖锐的矛盾:网络拓扑的“碎片化”与用户移动的“连续性”之间的冲突。在传统4G时代,用户穿越一个基站覆盖区可能需要数分钟;而在5G密集城区,以 60 km/h 行驶的车辆可能每 10~15 秒就面临一次基站切换。对于时速 120 km/h 的紧急车辆(如救护车、消防车),这一时间窗口进一步压缩至 5~8 秒。这意味着,5G网络的切换决策已从“分钟级运维问题”演变为“秒级控制问题”。

现有切换机制的固有缺陷:

  • 当前商用的 5G 切换流程主要遵循 3GPP TS 38.331 定义的 A3 事件:当邻区信号(RSRP/RSRQ)超过服务小区一个预设的“偏置量(Offset)”并持续一段“触发时间(TTT, Time-to-Trigger)”后,终端上报测量报告,网络侧执行切换。
  • 在小区边缘,由于阴影衰落和快衰的影响,信号强度剧烈波动。A3 机制会因信号的微小抖动触发“服务小区→邻区→服务小区”的反复切换,即乒乓效应(Ping-Pong Effect)。每一次乒乓切换都伴随着信令风暴(RRC重配、随机接入)、业务中断(数十毫秒的切换时延)以及终端(UE)电池的额外消耗。在 UDN 场景下,传统算法的乒乓率可高达 15%~25%,严重挤占物理随机接入信道(PRACH)资源。
  • QoS(服务质量)的盲区:紧急车辆、远程手术机器人、自动驾驶汽车等业务对连接可靠性的要求远高于普通手机用户。3GPP 定义了明确的 5QI(5G QoS Identifier)优先级参数,但传统的基于 RSRP 的切换决策完全“看不见”这些 QoS 标签。紧急车辆在信号边缘区域往往会与普通用户竞争同一基站资源,导致关键任务(MCPTT,Mission Critical Push-to-Talk)在切换瞬间出现丢包甚至断连。
  • 为了抑制乒乓和提升切换成功率,业界传统上依赖人工参数调优,高度依赖专家经验,且只能针对特定区域静态生效,无法适应动态变化的城市交通流。

1.2 意义

不同于监督学习依赖海量标注样本,深度强化学习(Deep Reinforcement Learning, DRL)通过“智能体(Agent)—环境(Environment)”的闭环试错交互,自主学习最优序贯决策策略。将切换控制建模为马尔可夫决策过程(MDP)后,DRL 具备三大契合 5G 切换场景的核心优势:

  • 长时域回报(Long-term Horizon):DRL 不贪图“当下最大 SINR”,而是通过奖励函数(Reward)的设计,权衡“当前切换代价”与“未来信号增益”,从根本上抑制短视的乒乓振荡。
  • 高维上下文融合:DRL 的状态空间(State)天然可以融合物理层测量(SINR)、媒体接入层负载(Load)、用户运动速度(Velocity)以及核心网业务优先级(QCI/5QI)。这使得单一策略网络能够为不同用户群体提供差异化服务——例如,让紧急车辆享有更低的切换代价和更高的信噪比权重。
  • 模型无关性(Model-free):无线信道的衰落、干扰和用户轨迹极其复杂,难以建立精准的解析模型。模型无关的 DRL(如 DQN、PPO)无需预知信道统计特性,直接根据实时测量反馈优化策略,具备极强的环境自适应能力。
  • 本文设计了基于DQN和PPO的智能切换方案,将乒乓率从 5% 压缩至 0%。

2 理论基础

2.1 5g网络切换的定义

在蜂窝移动通信系统中,切换(Handover) 是指当用户设备(UE)从一个基站的覆盖区域移动到另一个基站时,网络将UE的无线连接从源基站(Source gNB)无缝转移到目标基站(Target gNB)的过程。切换是保障移动用户业务连续性的核心机制。

A3事件的数学条件为:当邻区信号质量(RSRP或RSRQ)超过服务小区信号质量一个预设偏置量(Offset)并持续一段触发时间(TTT, Time-to-Trigger)后,UE上报测量报告,网络侧做出切换决策。公式为:
在这里插入图片描述
加粗样式### 2.2 乒乓效应:密集组网下的“控制风暴”
乒乓效应(Ping-Pong Effect) 是指UE在两个(或多个)基站之间反复、快速切换的现象。典型场景发生在小区边缘:由于阴影衰落或多径效应,信号瞬时波动使A3条件反复满足又撤销,UE在BS1→BS2→BS1之间来回切换,如同乒乓球碰撞。
乒乓效应的危害是系统性的:

  1. 信令开销激增:每一次切换需要经历测量上报、RRC重配、随机接入(RACH)、路径转换等流程,产生数十条信令消息。乒乓切换可将空口信令负载提升3~5倍,在密集城区甚至导致物理随机接入信道(PRACH)拥塞。

  2. 业务质量劣化:每次硬切换(Break-before-Make)存在20~80ms的服务中断时间(Interruption Time)。对于VoNR(5G语音)或工业实时控制业务,多次乒乓造成的累积中断将直接违反5G URLLC的1ms~5ms时延预算。

  3. 终端能耗增加:切换过程中UE需进行额外的射频测量、上行同步和RRC处理,功耗骤增。研究表明,一次切换的能耗相当于数十次普通数据传输。

3 本文工作

3.1 系统建模

模型区域为一个 A×A 的二维平面区域,其中 。区域内部署 NBS=3 个5G gNB基站,位置假设固定分别为
在这里插入图片描述
定义用户 u 在时刻 t 的位置为二维向量:
在这里插入图片描述
基站i与用户 u之间的欧氏距离为:

3.2 信道模型

采用 3GPP TR 36.839 规范中定义的城区宏站(UMa)路径损耗模型。该模型适用于频率范围 2~6 GHz、基站天线高度 25m、用户高度 1.5m 的典型城区场景。
路径损耗(单位:dB)为发射端与接收端之间距离的函数:
在这里插入图片描述
其中:

  • 截距项 128.1 dB 表示在 1km 参考距离下的基本路径损耗(包含载波频率、天线增益等固定因素)
  • 斜率 37.6 反映信号随距离的衰减速率(介于自由空间 20 与典型城区 40 之间)。

3.3 接收端

户端接收功率为发射功率减去路径损耗:
在这里插入图片描述
信干噪比为
在这里插入图片描述

3.4 用户移动建模

采用离散时间二维随机行走(Random Walk)模型描述用户运动,包含状态变量为位置 xu(t)和航向角 θu(t),设仿真时间步长 Δt,单位为秒,用户 u 的速度恒定为 vu,单位为千米/时。则每个时间步的位移大小,单位为米
在这里插入图片描述
位置更新规则为:
在这里插入图片描述
本文考虑三种类型的用户,模拟实际场景,具体为:
在这里插入图片描述

航向角 θu(t)的更新通过随机扰动实现,在每一步,以概率p 对航向施加随机偏转
在这里插入图片描述

3.5 系统动态与切换事件定义

3.5.1 系统动态

定义二元变量cu(t)表示表示用户在时刻 t是否连接至基站 i,每个用户在任何时刻恰好连接至一个基站,即
在这里插入图片描述
则用户 u的服务基站索引为:
在这里插入图片描述
每个基站 i 的负载定义为当前连接用户数与其最大容量的比值:
在这里插入图片描述

3.5.2 切换事件定义

切换事件发生在用户的服务基站发生变更时。定义时刻 t的切换指示变量
在这里插入图片描述
切换总数随时间的累计为:
在这里插入图片描述

3.5.3 乒乓事件检测

乒乓定义为同一用户在短时间窗口W_pp内发生两次以上切换且往返于相同基站对之间。设用户 u 在时刻 t之前最近一次切换的时间为 t_last(u),则乒乓指示为:

在这里插入图片描述
其中,

  1. 条件一表示:用户在时刻 t 确实发生了切换

  2. 条件二表示距离上次切换的时间在检测窗口内

  3. 条件三表示切换是原路返回

3.6 强化学习算法

3.6.1 马尔可夫决策建模

对于当前被决策的用户 u,智能体观测到由物理层测量和用户上下文构成的9维状态向量:
在这里插入图片描述
其中,

  1. 信噪比用来进行切换收益比较:若无邻区信号,智能体无法评估切换的价值;三个SINR值提供了清晰的候选目标排序。
  2. 速度用于预测信号变化率:高速用户SINR变化更快,智能体应学会采取更激进的切换策略以保持跟踪。
  3. 切换技术h用于乒乓风险感知:近期频繁切换的用户更需谨慎决策。
  4. 用户类型权重τ用于建立区别于普通用户的决策边界,配合奖励权重实现优先级保障。

动作空间设计为包含4个离散动作

在这里插入图片描述

其中,命令 0 1 2用来指定用户切换至对应的基站 ,3表示保持当前连接不变。

奖励函数是MDP设计的核心,它将多个竞争性KPI(信号质量、切换成本、乒乓率、时延、紧急保护、负载均衡)融合为单一的标量反馈信号。本系统的奖励函数完整表达式如下:
在这里插入图片描述
其中:

  1. 用户当前连接基站的SINR值/缩放因子用于数据压缩以确保奖励数值过大导致Q值不稳定。 w表示针对不同用户类型权重。
  2. 考虑到切换消耗信令资源并引入中断时延,因此施加惩罚以抑制不必要的切换。
  3. 乒乓惩罚项用于抑制乒乓效应。、
  4. 切换时延损失项用于模拟切换执行过程中的短暂服务中断,在此期间即使目标基站信号很好,用户也无法立即享用。该惩罚使智能体在切换收益不够显著时选择忍耐。
  5. 紧急断开惩罚定义为用户为紧急车辆且第一次进入信号弱的区域,施加大惩罚
  6. 负载均衡项惩罚随连接基站的负载线性增加,隐式激励智能体将用户分配至轻载基站,实现分布式负载均衡。

综上,本系统的最优切换策略π是以下优化问题的解:
在这里插入图片描述
约束条件为:

  1. 每个用户任意时刻最多连接一个基站
  2. 基站容量上限
    在此建模框架下,智能体通过与环境交互获得的累积奖励信号,自主发现能够平衡信号质量、切换成本、乒乓抑制、紧急保护和负载均衡的最优决策规律。这正是深度强化学习相较于传统基于规则的切换优化方法的根本优势所在。
3.6.2 DQN和PPO算法

基础理论书上有,这里不再赘述。列出关键公式:

DQN通过如下公式学习最优Q网络在这里插入图片描述
DQN采用ε-贪心(ε-greedy)策略平衡探索与利用:

在这里插入图片描述
探索率会随着训练过程衰减。

PPO的优化目标则为在这里插入图片描述

3.6.3 传统的基站切换的贪心算法

每个决策时刻,用户选择当前SINR最高的基站作为目标,即
在这里插入图片描述
该策略等价于传统切换算法中“始终连接最强信号基站”的极端版本,没有滞后余量、没有触发时间(TTT)、也没有对切换代价的考量。其性能代表了“纯信号驱动”方法的上界——它能够在任何时刻获得最优的瞬时SINR,但代价是完全忽视切换开销。

4 仿真结果分析

4.1 DQN 网络结构
层名称 输入维度 输出维度 参数量 激活函数
输入层(状态) 9
全连接层 1 9 64 640 ReLU
全连接层 2 64 64 4,160 ReLU
输出层(Q 值) 64 4 260 无(线性)
总参数量 5,060
4.2 PPO网络结构
层名称 输入维度 输出维度 参数量 激活函数
输入层(状态) 9
共享全连接层 1 9 64 640 ReLU
共享全连接层 2 64 64 4,160 ReLU
Actor 输出头 64 4 260 Softmax
Critic 输出头 64 1 65 无(线性)
总参数量 5,125

4.3 超参数对照表

超参数 DQN PPO
网络结构
隐藏层数 2 2
每层神经元数 64 64
激活函数 ReLU ReLU
共享特征提取层 是(Actor 与 Critic 共享)
优化器
优化器类型 Adam Adam
学习率 1 × 10⁻⁴ 3 × 10⁻⁴
批量大小 64 64
折扣与优势
折扣因子 γ 0.99 0.99
GAE 参数 λ 不适用 0.95
经验回放
回放缓冲区容量 100,000 不适用
学习起始步 1,000 不适用
探索策略
探索策略 ε-贪心 策略熵 + 随机初始化
初始 ε 1.0 不适用
最终 ε 0.05 不适用
ε 衰减比例 0.4(前 40% 步数) 不适用
轨迹与更新
每轮采样步数 1(单步更新) 2,048
每轮更新轮数 1 10
裁剪参数 ε_clip 不适用 0.2
目标网络更新频率 2,000 步 不适用
训练配置
总训练步数 400,000 400,000
回合最大步数 T 1,000 1,000
评估频率 10,000 步 10,000 步
评估回合数 10 10
4.4 仿真分析

在这里插入图片描述

在这里插入图片描述
可以看到:基于贪心算法取得的信噪比缺少高,但是切换次数和乒乓率都明显高于于强化学习算法,说明了“即时最优”不等于“长期最优”, 也说明“信号强度”与“信令开销”存在不可调和的矛盾。深度强化学习能够在少的SINR牺牲下,减少5G切换乒乓效应。
部分代码:

import os
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import pandas as pd
from stable_baselines3 import DQN, PPO
from environment import HandoverEnv

EVAL_SEEDS = [42, 123, 456, 789, 1337, 1111, 2222, 3333, 4444, 5555]
NUM_EPISODES = 10


def evaluate_agent(model, num_episodes=NUM_EPISODES):
    """Evaluate a trained RL agent over multiple seeded episodes."""
    rewards, handover_rates, avg_sinrs, ping_pong_rates, emergency_disc = [], [], [], [], []

    env = HandoverEnv()
    for ep in range(num_episodes):
        obs, _ = env.reset(seed=EVAL_SEEDS[ep])
        episode_reward = 0.0
        sinr_values = []
        done = False

        while not done:
            acting_user_idx = env.current_user_idx
            action, _ = model.predict(obs, deterministic=True)
            obs, reward, terminated, truncated, _ = env.step(action)
            episode_reward += reward

            user = env.users[acting_user_idx]
            if user.connected_bs is not None:
                sinr = env.base_stations[user.connected_bs].calculate_sinr(user.position)
                sinr_values.append(sinr)

            done = terminated or truncated

        rewards.append(episode_reward)
        handover_rates.append(env.total_handovers / max(env.time_step, 1))
        avg_sinrs.append(float(np.mean(sinr_values)) if sinr_values else 0.0)
        ping_pong_rates.append(env.ping_pong_count / max(env.total_handovers, 1))
        emergency_disc.append(env.emergency_disconnections)

    env.close()
    return {
        "rewards": rewards,
        "handover_rates": handover_rates,
        "avg_sinrs": avg_sinrs,
        "ping_pong_rates": ping_pong_rates,
        "emergency_disc": emergency_disc,
    }


def evaluate_baseline(num_episodes=NUM_EPISODES):
    """Evaluate the greedy SINR baseline over multiple seeded episodes."""
    rewards, handover_rates, avg_sinrs, ping_pong_rates, emergency_disc = [], [], [], [], []

    env = HandoverEnv()
    for ep in range(num_episodes):
        obs, _ = env.reset(seed=EVAL_SEEDS[ep])
        episode_reward = 0.0
        sinr_values = []
        done = False

        while not done:
            acting_user_idx = env.current_user_idx
            user = env.users[acting_user_idx]
            sinrs = [bs.calculate_sinr(user.position) for bs in env.base_stations]
            action = int(np.argmax(sinrs))

            obs, reward, terminated, truncated, _ = env.step(action)
            episode_reward += reward
            sinr_values.append(max(sinrs))

            done = terminated or truncated

        rewards.append(episode_reward)
        handover_rates.append(env.total_handovers / max(env.time_step, 1))
        avg_sinrs.append(float(np.mean(sinr_values)) if sinr_values else 0.0)
        ping_pong_rates.append(env.ping_pong_count / max(env.total_handovers, 1))
        emergency_disc.append(env.emergency_disconnections)

    env.close()
    return {
        "rewards": rewards,
        "handover_rates": handover_rates,
        "avg_sinrs": avg_sinrs,
        "ping_pong_rates": ping_pong_rates,
        "emergency_disc": emergency_disc,
    }


def _mean_std(values):
    return float(np.mean(values)), float(np.std(values))


def _load_model(algo: str, use_best: bool):
    """Load final or best checkpoint for a given algorithm."""
    cls = DQN if algo == "dqn" else PPO
    if use_best:
        path = f"models/best_{algo}/best_model.zip"
        if not os.path.exists(path):
            print(f"  [warn] best model not found at '{path}', falling back to final.")
            path = f"models/{algo}_handover"
    else:
        path = f"models/{algo}_handover"
    return cls.load(path)


def compare_methods():
    """Compare baseline vs DQN vs PPO (final and best) with statistical reporting."""
    os.makedirs("figures", exist_ok=True)

    print("正在评估基线(贪心 SINR)...")
    baseline = evaluate_baseline()

    print("正在评估 DQN — 最终模型(200k 步)...")
    dqn_final = evaluate_agent(_load_model("dqn", use_best=False))

    print("正在评估 DQN — 最佳检查点...")
    dqn_best = evaluate_agent(_load_model("dqn", use_best=True))

    print("正在评估 PPO — 最终模型(200k 步)...")
    ppo_final = evaluate_agent(_load_model("ppo", use_best=False))

    print("正在评估 PPO — 最佳检查点...")
    ppo_best = evaluate_agent(_load_model("ppo", use_best=True))

    results = {
        "基线":  baseline,
        "DQN\n(最终)": dqn_final,
        "DQN\n(最佳)":  dqn_best,
        "PPO\n(最终)": ppo_final,
        "PPO\n(最佳)":  ppo_best,
    }

    # 打印对比表格(中文)
    print("\n" + "=" * 75)
    print(f"性能对比 —— 每种算法 {NUM_EPISODES} 个回合,固定随机种子")
    print("=" * 75)
    labels = {
        "基线":         "基线         ",
        "DQN\n(最终)":  "DQN (最终)  ",
        "DQN\n(最佳)":  "DQN (最佳)  ",
        "PPO\n(最终)":  "PPO (最终)  ",
        "PPO\n(最佳)":  "PPO (最佳)  ",
    }
    for name, r in results.items():
        rm,  rs  = _mean_std(r["rewards"])
        hrm, hrs = _mean_std(r["handover_rates"])
        sm,  ss  = _mean_std(r["avg_sinrs"])
        ppm, pps = _mean_std(r["ping_pong_rates"])
        em,  es  = _mean_std(r["emergency_disc"])
        print(f"\n{labels[name]}:")
        print(f"  平均奖励:        {rm:8.1f} ± {rs:.1f}")
        print(f"  切换率:          {hrm:8.2f} ± {hrs:.2f}  (次/时间步)")
        print(f"  平均信噪比:      {sm:8.2f} ± {ss:.2f}  dB")
        print(f"  乒乓率:          {ppm:8.3f} ± {pps:.3f}  (占切换比例)")
        print(f"  紧急断开次数:    {em:8.1f} ± {es:.1f}")

    plot_comparison(results)
    plot_training_curves()


def plot_comparison(results: dict):
    """绘制 2×2 对比柱状图(含误差棒),所有文本为中文"""
    fig, axes = plt.subplots(2, 2, figsize=(12, 10))
    fig.suptitle("算法对比 — 基线 vs DQN vs PPO",
                 fontsize=14, fontweight="bold", y=1.01)

    methods = list(results.keys())
    colors = ["#6B7280", "#E41A1C", "#377EB8", "#4DAF4A", "#984EA3"]

    # 定义四个子图的指标信息(键名、中文标题、纵轴单位)
    metrics = [
        ("rewards",        "平均奖励",                  "奖励值"),
        ("handover_rates", "切换率 (次/时间步)",        "切换次数 / 步"),
        ("avg_sinrs",      "平均信噪比 (dB)",           "dB"),
        ("ping_pong_rates","乒乓率\n(占切换比例)",       "比例"),
    ]

    for idx, (key, title, ylabel) in enumerate(metrics):
        ax = axes[idx // 2, idx % 2]
        means = [np.mean(results[m][key]) for m in methods]
        stds  = [np.std(results[m][key])  for m in methods]
        bars = ax.bar(methods, means, yerr=stds, capsize=6,
                      color=colors, alpha=0.85, edgecolor="white", linewidth=1.5)
        ax.set_title(title, fontsize=12, fontweight="bold")
        ax.set_ylabel(ylabel, fontsize=10)
        ax.grid(axis="y", alpha=0.3)
        ax.spines["top"].set_visible(False)
        ax.spines["right"].set_visible(False)
        for bar, mean in zip(bars, means):
            ax.text(bar.get_x() + bar.get_width() / 2,
                    bar.get_height() * 1.01,
                    f"{mean:.2f}", ha="center", va="bottom", fontsize=9)

    plt.tight_layout()
    out = "figures/comparison_bar_charts.png"
    plt.savefig(out, dpi=300, bbox_inches="tight")
    print(f"\n对比图已保存至 '{out}'")
    plt.show()


def plot_training_curves():
    """绘制 DQN 和 PPO 的训练奖励曲线(含滚动平均),标题和标签为中文"""
    os.makedirs("figures", exist_ok=True)
    fig, axes = plt.subplots(1, 2, figsize=(14, 5))
    fig.suptitle("训练曲线 —— 回合奖励随时间变化",
                 fontsize=13, fontweight="bold")

    for ax, algo in zip(axes, ["dqn", "ppo"]):
        log_file = f"models/{algo}_monitor.monitor.csv"
        if not os.path.exists(log_file):
            ax.text(0.5, 0.5,
                    f"未找到训练日志\n({log_file})\n请先运行 train.py",
                    ha="center", va="center", transform=ax.transAxes,
                    color="gray", fontsize=11)
            ax.set_title(f"{algo.upper()} 训练曲线", fontsize=12, fontweight="bold")
            continue

        df = pd.read_csv(log_file, skiprows=1)
        rewards = df["r"].values
        rolling = pd.Series(rewards).rolling(5, min_periods=1).mean().values

        ax.plot(rewards, alpha=0.3, color="#6B7280", label="回合奖励")
        ax.plot(rolling, color="#3B82F6", linewidth=2, label="5回合滚动平均")
        ax.set_title(f"{algo.upper()} 训练曲线", fontsize=12, fontweight="bold")
        ax.set_xlabel("回合数")
        ax.set_ylabel("回合奖励")
        ax.legend(fontsize=10)
        ax.grid(alpha=0.3)
        ax.spines["top"].set_visible(False)
        ax.spines["right"].set_visible(False)

    plt.tight_layout()
    out = "figures/training_curves.png"
    plt.savefig(out, dpi=300, bbox_inches="tight")
    print(f"训练曲线已保存至 '{out}'")
    plt.show()


if __name__ == "__main__":
    compare_methods()

4 总结

在5G超密集组网中,传统切换算法(基于A3事件的贪心SINR策略)存在严重的乒乓效应——用户在基站间频繁往复切换,浪费信令资源、增加时延、消耗终端电量。本系统对比了贪心算法、DQN和PPO三种切换策略,给出了强化学习应用在无线通信领域的一种参考。

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

更多推荐