UF_RL_训练与预测流程详解.md 31 KB

UF-RL 训练与预测流程详解

目录

  1. 训练阶段完整流程
  2. 预测阶段完整流程
  3. 从训练到部署的完整链路

训练阶段完整流程

概述:智能体如何学会决策

把训练过程想象成培养一个工程师

新手工程师(随机决策)
    ↓ 通过大量实践
    ↓ 记住成功/失败的经验
    ↓ 总结规律
    ↓
经验丰富的工程师(最优决策)

强化学习就是这个过程的数学化实现。


阶段0:准备工作(程序启动)

步骤0.1:固定随机种子

def set_global_seed(seed=2025):
    random.seed(2025)
    np.random.seed(2025)
    torch.manual_seed(2025)
    torch.cuda.manual_seed_all(2025)

作用:保证每次训练结果一致,便于调试和复现


步骤0.2:创建超滤系统参数

params = UFParams(
    q_UF=360.0,          # 进水流量:360 m³/h
    TMP0=0.03,           # 初始TMP:0.03 MPa
    TMP_max=0.06,        # TMP上限:0.06 MPa
    L_min_s=3800.0,      # 产水时长下限:3800秒
    L_max_s=6000.0,      # 产水时长上限:6000秒
    t_bw_min_s=40.0,     # 反洗时长下限:40秒
    t_bw_max_s=60.0,     # 反洗时长上限:60秒
    ...
)

这些参数定义了

  • 物理系统的运行范围
  • 决策空间的边界
  • 约束条件

步骤0.3:创建模拟环境

env = UFSuperCycleEnv(params)
env = Monitor(env)           # 包装:记录统计信息
env = DummyVecEnv([env])     # 包装:向量化接口

环境的作用

  • 模拟超滤系统的运行
  • 接收智能体的动作(产水时长、反洗时长)
  • 返回奖励和下一个状态

为什么需要包装?

  • Monitor:自动记录每个episode的奖励、长度等
  • DummyVecEnv:统一单环境/多环境的接口(虽然这里只有1个)

步骤0.4:创建DQN智能体

model = DQN(
    policy="MlpPolicy",        # 使用多层感知机策略网络
    env=env,
    learning_rate=1e-4,        # 学习率
    buffer_size=10000,         # 经验回放池大小
    learning_starts=200,       # 开始学习前的随机探索步数
    batch_size=32,             # 每次训练的样本数
    gamma=0.95,                # 折扣因子(重视长期奖励)
    train_freq=4,              # 每4步训练一次
    target_update_interval=1,  # 目标网络更新间隔
    tau=0.005,                 # 软更新系数
    exploration_initial_eps=1.0,   # 初始探索率100%
    exploration_fraction=0.3,      # 前30%训练时间探索衰减
    exploration_final_eps=0.02,    # 最终探索率2%
    verbose=1,
    tensorboard_log="./uf_dqn_tensorboard/"
)

DQN智能体包含

  1. Q网络(当前网络):估计每个动作的价值
  2. 目标网络:提供稳定的学习目标
  3. 经验回放池:存储历史经验
  4. 优化器:更新网络参数

Q网络结构

输入层:4维状态 → [TMP0, last_L, last_t_bw, max_TMP]
隐藏层1:4 → 64 (ReLU激活)
隐藏层2:64 → 64 (ReLU激活)
输出层:64 → 185 (每个动作的Q值)

步骤0.5:创建回调和记录器

recorder = UFEpisodeRecorder()          # 记录每个episode的数据
callback = UFTrainingCallback(recorder) # 训练回调

记录内容

  • 每一步的状态、动作、奖励
  • 每个episode的总奖励、回收率等
  • 用于训练后分析

阶段1:初始化(第1个Episode开始)

环境重置

state = env.reset()

# 环境内部执行:
self.TMP0 = np.random.uniform(0.01, 0.03)  # 随机初始TMP,例如0.025
self.current_step = 0
self.last_action = (3800, 40)  # 初始动作:最保守的选择
self.max_TMP = self.TMP0

# 计算初始状态
state = [
    (0.025 - 0.01) / 0.04,     # TMP0归一化 = 0.375
    (3800 - 3800) / 2200,       # last_L归一化 = 0.0
    (40 - 40) / 20,             # last_t_bw归一化 = 0.0
    (0.025 - 0.01) / 0.04       # max_TMP归一化 = 0.375
]
# state = [0.375, 0.0, 0.0, 0.375]

状态解释

  • 当前是一个"全新的超滤膜",TMP=0.025
  • 还没有历史操作(last_L和last_t_bw都是初始值)
  • 周期最高TMP就是当前TMP

阶段2:交互与学习循环(50000步)

完整的一步流程图

┌─────────────────────────────────────────────────────────┐
│                    第 N 步                              │
└─────────────────────────────────────────────────────────┘

1. 当前状态
   state = [0.375, 0.491, 0.5, 0.429]
        ↓

2. 动作选择(ε-贪心)
   ┌────────────────────────────┐
   │ if random() < epsilon:     │
   │     action = random(0-184) │  ← 探索
   │ else:                      │
   │     Q值 = Q_network(state) │  ← 利用
   │     action = argmax(Q值)   │
   └────────────────────────────┘
        ↓
   假设选择 action = 92
        ↓

3. 动作解码
   L_idx = 92 // 5 = 18
   t_bw_idx = 92 % 5 = 2
   L_s = 3800 + 18×60 = 4880秒
   t_bw_s = 40 + 2×5 = 50秒
        ↓

4. 执行模拟
   simulate_one_supercycle(TMP0=0.025, L_s=4880, t_bw_s=50)
   ┌──────────────────────────────────┐
   │  计算小周期次数:k = 35         │
   │  For i in range(35):             │
   │    产水:TMP增长                 │
   │    反洗:TMP部分恢复             │
   │  CEB:TMP完全恢复                │
   │  计算指标:回收率、净供水率等   │
   └──────────────────────────────────┘
        ↓
   返回:feasible=True, info={recovery:0.97, net_rate:338, ...}
        ↓

5. 计算奖励
   reward = _score(info)
   = 0.8×0.97 + 0.2×0.94 - 0.2×0.00
   = 0.964(基础奖励)
   放大后 = (0.964-0.5)² × 5 = 1.076
        ↓

6. 观察新状态
   TMP0_new = 0.025(CEB后恢复)
   next_state = [0.375, 0.491, 0.5, 0.429]
        ↓

7. 存储经验
   buffer.add(
       state = [0.375, 0.0, 0.0, 0.375],
       action = 92,
       reward = 1.076,
       next_state = [0.375, 0.491, 0.5, 0.429],
       done = False
   )
        ↓

8. 训练网络(每4步,且步数>200)
   ┌──────────────────────────────────────┐
   │ if step % 4 == 0 and step > 200:    │
   │    batch = buffer.sample(32)         │
   │    训练Q网络(详见下方)             │
   └──────────────────────────────────────┘
        ↓

9. 更新epsilon
   epsilon = max(0.02, 1.0 - step/15000)
        ↓

10. 记录数据
    callback.record_step(state, action, reward, ...)
        ↓

11. 检查episode是否结束
    if done or step >= 20:
        state = env.reset()  # 开始新episode
    else:
        state = next_state   # 继续当前episode

深入理解:Q网络训练(第204步首次训练)

# ===== 第204步:终于可以开始学习了!=====

# 1. 从经验池随机采样32条经验
batch = buffer.sample(32)

# 采样结果示例:
batch = {
    'state': [
        [0.375, 0.0, 0.0, 0.375],      # 第1条经验的state
        [0.425, 0.2, 0.3, 0.450],      # 第2条经验的state
        ...                             # 共32条
    ],
    'action': [92, 105, 78, ...],      # 32个动作
    'reward': [1.076, 0.845, -20, ...],# 32个奖励
    'next_state': [...],                # 32个next_state
    'done': [False, False, True, ...]   # 32个done标志
}

# 2. 转换为PyTorch张量
state_tensor = torch.FloatTensor(batch['state'])       # [32, 4]
action_tensor = torch.LongTensor(batch['action'])      # [32]
reward_tensor = torch.FloatTensor(batch['reward'])     # [32]
next_state_tensor = torch.FloatTensor(batch['next_state'])  # [32, 4]
done_tensor = torch.FloatTensor(batch['done'])         # [32]

# 3. 计算当前Q值(Q_current)
q_values = Q_network(state_tensor)  # [32, 185]
# 对于第1条经验,Q网络预测所有185个动作的Q值
# 例如:[0.5, 0.6, ..., 1.2, ..., 0.8](185个值)

q_current = q_values.gather(1, action_tensor.unsqueeze(1))  # [32, 1]
# gather操作:取出实际执行的动作对应的Q值
# 对于第1条经验,action=92,取出q_values[92] = 1.2
# 结果:[1.2, 0.9, -5.0, ...](32个值)

# 4. 计算目标Q值(Q_target)
with torch.no_grad():  # 不计算梯度,加速
    # 用目标网络预测next_state的Q值
    next_q_values = Q_target(next_state_tensor)  # [32, 185]
    
    # 取每个next_state的最大Q值
    next_q_max, _ = next_q_values.max(dim=1)  # [32]
    # 例如:[1.5, 1.3, 0.0, ...]
    
    # 贝尔曼方程:Q_target = reward + gamma × max(Q_next) × (1-done)
    target = reward_tensor + 0.95 × next_q_max × (1 - done_tensor)
    # 对于第1条经验:
    # target = 1.076 + 0.95 × 1.5 × (1-0) = 2.501
    # 对于第3条经验(done=True):
    # target = -20 + 0.95 × 0.0 × (1-1) = -20

# 5. 计算TD误差(损失函数)
loss = F.mse_loss(q_current.squeeze(), target)
# MSE = mean((q_current - target)²)
# 例如:mean([(1.2-2.501)², (0.9-2.135)², ...])
# 假设 loss = 3.45

# 6. 反向传播更新Q网络
optimizer.zero_grad()   # 清空之前的梯度
loss.backward()         # 计算梯度
optimizer.step()        # 更新参数

# 更新后,Q_network的参数被调整:
# - 如果q_current < target,增大该动作的Q值
# - 如果q_current > target,减小该动作的Q值

# 7. 软更新目标网络
for param, target_param in zip(Q_network.parameters(), Q_target.parameters()):
    target_param.data.copy_(
        0.005 × param.data + 0.995 × target_param.data
    )
# 目标网络缓慢追踪Q网络,tau=0.005表示每次只更新0.5%

训练过程的关键时间点

时间线(50000步训练)

步数     | epsilon | 行为                    | 学习状态
---------|---------|------------------------|------------------
0-200    | 1.0     | 纯随机探索             | 填充经验池
204      | 1.0     | 首次训练               | Q值从随机初始化开始学习
500      | 0.98    | 探索为主               | Q值逐渐有意义
2000     | 0.87    | 探索与利用并存         | 策略初步成型
5000     | 0.67    | 开始偏向利用           | Q值趋于稳定
15000    | 0.02    | 探索衰减完成           | 基本使用最优策略
15000+   | 0.02    | 98%利用,2%探索        | 策略优化与稳定
50000    | 0.02    | 训练结束               | 保存最终模型

示例:第5000步时的决策过程

# 当前状态:TMP稍高
state = [0.625, 0.55, 0.6, 0.650]  # TMP0=0.035 MPa

# epsilon=0.67,仍有67%概率随机探索
if random() < 0.67:
    action = random.randint(0, 184)  # 假设随机到115
else:
    # 33%概率使用Q网络
    q_values = Q_network(state)
    # Q值示例(部分):
    # action 0 (L=3800, t_bw=40): Q=0.45  (保守,TMP低但产水少)
    # action 92 (L=4880, t_bw=50): Q=0.78 (平衡)
    # action 115 (L=5080, t_bw=55): Q=0.82 (激进,产水多但TMP升高)
    # action 184 (L=6000, t_bw=60): Q=-2.5 (太激进,违反约束)
    
    action = argmax(q_values) = 115

# 解码动作
L_s = 5080, t_bw_s = 55

# 执行模拟
feasible, info = simulate_one_supercycle(0.035, 5080, 55)
# 结果:feasible=True(刚好没违反约束)
#       recovery=0.965, net_rate=330

# 计算奖励
reward = 0.75(较好但不是最优,因为TMP有点高)

# 存储经验并训练
# Q网络学到:在TMP=0.035时,action=115是一个还不错的选择

示例:第25000步时的决策过程

# 同样的状态
state = [0.625, 0.55, 0.6, 0.650]  # TMP0=0.035 MPa

# epsilon=0.02,只有2%概率随机探索
if random() < 0.02:
    action = random(0-184)
else:
    # 98%概率使用Q网络(此时Q值已经很准确)
    q_values = Q_network(state)
    # 经过25000步学习,Q值更精准:
    # action 0: Q=0.52   (保守,稳定)
    # action 85: Q=0.88  (最优!)✓
    # action 92: Q=0.85  (次优)
    # action 115: Q=0.65 (之前试过,风险高)
    # action 184: Q=-15.0(确定违反约束)
    
    action = 85  # 选择最优动作

# 解码
L_s = 4720, t_bw_s = 45

# 执行
feasible, info = simulate(0.035, 4720, 45)
# 结果:feasible=True
#       recovery=0.972, net_rate=335
#       TMP贴边度低,安全

# 奖励
reward = 0.92(接近最优)

# 智能体学会了:
# - 在TMP较高时,要稍微保守一点
# - L_s=4720是在高TMP下的最佳平衡点

阶段3:训练结束与保存

# 训练完成
model.learn(total_timesteps=50000)  # 循环结束

# 保存模型
model.save("dqn_model.zip")

# 模型文件包含:
# 1. Q_network的所有参数(权重和偏置)
# 2. 优化器状态
# 3. 训练配置(学习率等)
# 不包含:经验回放池(太大且推理时不需要)

训练日志统计

stats = recorder.get_episode_stats()
print(f"""
训练完成统计:
- 总步数:50000
- 总episode数:约2500(平均每episode 20步)
- 最终平均奖励:0.85
- 约束违反率:5%(从初期80%大幅下降)
- 平均回收率:0.968
""")

预测阶段完整流程

概述:训练好的智能体如何工作

训练完成后,模型已经学会了:

给定状态(TMP, 历史操作) → 选择最优动作(L_s, t_bw_s)

预测阶段不需要:

  • ✗ 探索(epsilon=0,总是选择最优)
  • ✗ 训练(不更新网络参数)
  • ✗ 经验池(不存储新经验)

预测阶段只需要:

  • ✓ 加载训练好的Q网络
  • ✓ 输入当前状态
  • ✓ 输出最优动作

预测流程详解

场景:工厂实时决策

假设当前时间:2025-01-15 10:00,超滤系统运行中,需要决定下一个周期的参数。


步骤1:获取当前系统状态

# 从工厂SCADA系统读取实时数据
current_TMP0 = 0.032  # 当前TMP(MPa)
last_L_s = 4500       # 上一周期产水时长(秒)
last_t_bw_s = 50      # 上一周期反洗时长(秒)
max_TMP_last = 0.045  # 上一周期最高TMP(MPa)

# 也可以从数据库查询历史记录
# 或者如果是第一次运行,使用默认值

步骤2:初始化决策环境和模型

from DQN_decide import run_uf_DQN_decide
from DQN_env import UFParams

# 2.1 创建系统参数(与训练时一致)
uf_params = UFParams(
    q_UF=360.0,
    TMP_max=0.06,
    # ... 其他参数
)

# 2.2 加载训练好的模型(自动完成)
# 模型文件:dqn_model.zip
# 内部会执行:
# model = DQN.load("dqn_model.zip")

步骤3:执行决策

# 3.1 调用决策接口
result = run_uf_DQN_decide(
    uf_params=uf_params,
    TMP0_value=0.032  # 输入当前TMP
)

# 3.2 决策内部流程详解
def run_uf_DQN_decide(uf_params, TMP0_value):
    # Step A: 创建环境实例
    env = UFSuperCycleEnv(uf_params)
    
    # Step B: 设置当前TMP
    env.current_params.TMP0 = 0.032
    env.last_action = (4500, 50)      # 使用历史动作
    env.max_TMP_during_filtration = 0.045
    
    # Step C: 获取归一化状态
    obs = env._get_obs()
    # obs = [
    #     (0.032 - 0.01) / 0.04 = 0.55,   # TMP0
    #     (4500 - 3800) / 2200 = 0.318,   # last_L
    #     (50 - 40) / 20 = 0.5,           # last_t_bw
    #     (0.045 - 0.01) / 0.04 = 0.875   # max_TMP
    # ]
    # obs = [0.55, 0.318, 0.5, 0.875]
    
    # Step D: 模型预测(确定性,不探索)
    obs_reshaped = obs.reshape(1, -1)  # [1, 4]
    action, _states = model.predict(obs_reshaped, deterministic=True)
    
    # 模型内部执行:
    # q_values = Q_network(obs_reshaped)  # [1, 185]
    # 例如 q_values = [[0.45, 0.67, ..., 0.89, ..., -3.2]]
    # action = argmax(q_values) = 105
    # (选择Q值最大的动作)
    
    action = action[0]  # 105
    
    # Step E: 解码动作
    L_s, t_bw_s = env._get_action_values(105)
    # L_idx = 105 // 5 = 21
    # t_bw_idx = 105 % 5 = 0
    # L_s = 3800 + 21×60 = 5060秒
    # t_bw_s = 40 + 0×5 = 40秒
    
    # Step F: 模拟验证(可选,检查可行性)
    next_obs, reward, terminated, truncated, info = env.step(105)
    
    # Step G: 返回决策结果
    return {
        "action": 105,
        "L_s": 5060.0,
        "t_bw_s": 40.0,
        "next_obs": next_obs,
        "reward": reward,
        "terminated": terminated,
        "truncated": truncated,
        "info": info
    }

# 3.3 获取决策结果
print(f"""
模型决策结果:
- 建议产水时长:{result['L_s']} 秒 (约{result['L_s']/60:.1f}分钟)
- 建议反洗时长:{result['t_bw_s']} 秒
- 预期回收率:{result['info']['recovery']:.3f}
- 预期净供水率:{result['info']['net_delivery_rate_m3ph']:.1f} m³/h
- 预期周期最高TMP:{result['info']['max_TMP_during_filtration']:.4f} MPa
""")

# 输出示例:
# 模型决策结果:
# - 建议产水时长:5060 秒 (约84.3分钟)
# - 建议反洗时长:40 秒
# - 预期回收率:0.968
# - 预期净供水率:332.5 m³/h
# - 预期周期最高TMP:0.0485 MPa

步骤4:生成PLC指令(渐进式调整)

为了避免参数突变导致系统不稳定,使用渐进式调整策略:

from DQN_decide import generate_plc_instructions

# 4.1 准备输入
current_L_s = 4500      # 工厂当前设定值
current_t_bw_s = 50     # 工厂当前设定值
model_prev_L_s = 4800   # 模型上一轮建议值(如果有)
model_prev_t_bw_s = 45  # 模型上一轮建议值
model_L_s = 5060        # 模型本轮建议值
model_t_bw_s = 40       # 模型本轮建议值

# 4.2 生成渐进式指令
next_L_s, next_t_bw_s = generate_plc_instructions(
    current_L_s,
    current_t_bw_s,
    model_prev_L_s,
    model_prev_t_bw_s,
    model_L_s,
    model_t_bw_s
)

# 4.3 内部逻辑详解
def generate_plc_instructions(...):
    # Step 1: 选择基准值(上一轮模型值 vs 当前值)
    # 选择更接近本轮模型建议的那个
    if abs(current_L_s - model_L_s) <= abs(model_prev_L_s - model_L_s):
        effective_current_L = 4500  # 当前值更接近
    else:
        effective_current_L = 4800  # 上轮值更接近
    
    # 假设选择了4800
    
    # Step 2: 计算差异
    L_diff = model_L_s - effective_current_L
    # = 5060 - 4800 = 260秒
    
    # Step 3: 渐进调整(每次最多变化1个步长)
    L_step_s = 60  # 步长60秒
    threshold = 1.0
    
    if abs(L_diff) >= threshold * L_step_s:
        if L_diff > 0:
            L_adjustment = +60  # 向上调整
        else:
            L_adjustment = -60  # 向下调整
    else:
        L_adjustment = 0  # 差异小,不调整
    
    next_L_s = 4800 + 60 = 4860秒
    
    # 同样处理t_bw_s
    # t_bw_diff = 40 - 45 = -5秒
    # abs(-5) >= 1.0 × 5 → True
    # t_bw_adjustment = -5
    next_t_bw_s = 45 - 5 = 40秒
    
    return 4860, 40

# 4.4 结果
print(f"""
PLC指令:
- 下发产水时长:{next_L_s} 秒(从{effective_current_L}秒逐步调整)
- 下发反洗时长:{next_t_bw_s} 秒
- 调整方向:向模型建议值({model_L_s}秒, {model_t_bw_s}秒)靠拢
- 需要继续调整轮数:约{abs(model_L_s - next_L_s) // 60}轮
""")

# 输出:
# PLC指令:
# - 下发产水时长:4860 秒(从4800秒逐步调整)
# - 下发反洗时长:40 秒
# - 调整方向:向模型建议值(5060秒, 40秒)靠拢
# - 需要继续调整轮数:约3轮

渐进调整的好处

  • ✅ 避免参数突变导致TMP急剧波动
  • ✅ 给操作员时间观察和干预
  • ✅ 系统更平稳过渡

步骤5:计算预期性能指标

在实际下发指令前,先模拟计算性能:

from DQN_decide import calc_uf_cycle_metrics

# 5.1 计算指令对应的性能
TMP0 = 0.032
max_tmp = 0.048  # 如果工厂有实测数据
min_tmp = 0.025
L_s = 4860
t_bw_s = 40

metrics = calc_uf_cycle_metrics(
    uf_params,
    TMP0,
    max_tmp,
    min_tmp,
    L_s,
    t_bw_s
)

# 5.2 内部计算流程
def calc_uf_cycle_metrics(...):
    # 模拟一个完整超级周期
    feasible, info = simulate_one_supercycle(params, L_s, t_bw_s)
    
    # 提取关键指标
    k_bw_per_ceb = info["k_bw_per_ceb"]  # 小周期次数
    recovery = info["recovery"]            # 回收率
    net_rate = info["net_delivery_rate_m3ph"]  # 净供水率
    daily_prod_time = info["daily_prod_time_h"]  # 日均产水时间
    ton_water_energy = info["ton_water_energy_kWh_per_m3"]  # 吨水电耗
    
    # 计算渗透率
    if min_tmp is not None:
        max_permeability = 100 * q_UF / (膜面积) / min_tmp
    else:
        max_permeability = info中计算
    
    return {
        "k_bw_per_ceb": 36,
        "recovery": 0.968,
        "net_delivery_rate_m3ph": 332.8,
        "daily_prod_time_h": 18.5,
        "ton_water_energy_kWh_per_m3": 0.1019,
        "max_permeability": 58.3  # lmh/bar
    }

# 5.3 展示结果
print(f"""
预期性能指标:
- 小周期次数(48h内):{metrics['k_bw_per_ceb']}
- 回收率:{metrics['recovery']:.2%}
- 净供水率:{metrics['net_delivery_rate_m3ph']:.1f} m³/h
- 日均产水时间:{metrics['daily_prod_time_h']:.1f} 小时/天
- 吨水电耗:{metrics['ton_water_energy_kWh_per_m3']:.4f} kWh/m³
- 最高渗透率:{metrics['max_permeability']:.1f} lmh/bar
""")

# 输出:
# 预期性能指标:
# - 小周期次数(48h内):36
# - 回收率:96.80%
# - 净供水率:332.8 m³/h
# - 日均产水时间:18.5 小时/天
# - 吨水电耗:0.1019 kWh/m³
# - 最高渗透率:58.3 lmh/bar

步骤6:下发PLC指令

# 6.1 准备PLC通信数据包
plc_command = {
    "timestamp": "2025-01-15 10:00:00",
    "L_s": 4860,        # 产水时长(秒)
    "t_bw_s": 40,       # 反洗时长(秒)
    "source": "AI_DQN", # 指令来源
    "confidence": 0.95  # 模型置信度
}

# 6.2 发送到PLC(伪代码)
# plc_client.write_registers(
#     address=1000,
#     values=[4860, 40]
# )

# 6.3 记录操作日志
log_decision(
    timestamp="2025-01-15 10:00:00",
    TMP0=0.032,
    model_L_s=5060,
    model_t_bw_s=40,
    plc_L_s=4860,
    plc_t_bw_s=40,
    expected_recovery=0.968
)

步骤7:周期性重复预测

# 每个超级周期结束后(约48小时)重新预测

while True:
    # 等待当前周期结束
    wait_for_cycle_end()
    
    # 获取最新系统状态
    current_state = get_system_state()
    
    # 执行决策
    result = run_uf_DQN_decide(uf_params, current_state.TMP0)
    
    # 渐进调整
    next_L, next_t_bw = generate_plc_instructions(...)
    
    # 下发指令
    send_to_plc(next_L, next_t_bw)
    
    # 记录日志
    log_decision(...)
    
    # 等待下一周期
    sleep(48_hours)

从训练到部署的完整链路

完整流程图

┌─────────────────────────────────────────────────────────────┐
│                    阶段1:离线训练                           │
└─────────────────────────────────────────────────────────────┘

1. 数据准备
   ├─ 历史运行数据(可选,用于物理模型)
   └─ 系统参数配置(UFParams)

2. 模拟器开发
   ├─ 物理模型:TMP增长、反洗恢复
   └─ 约束检查:TMP上限、残余增量

3. 强化学习训练
   ├─ 环境:UFSuperCycleEnv
   ├─ 算法:DQN
   ├─ 训练:50000步,约数小时
   └─ 输出:dqn_model.zip

4. 模型验证
   ├─ 测试不同TMP条件
   ├─ 检查约束满足率
   └─ 评估性能指标

        ↓

┌─────────────────────────────────────────────────────────────┐
│                    阶段2:在线部署                           │
└─────────────────────────────────────────────────────────────┘

5. 部署准备
   ├─ 将dqn_model.zip部署到服务器
   ├─ 配置PLC通信接口
   └─ 搭建监控系统

6. 实时决策循环
   Every 48 hours:
   ├─ 从SCADA读取TMP0
   ├─ 调用run_uf_DQN_decide()
   ├─ 生成PLC指令(渐进式)
   ├─ 下发到PLC
   └─ 记录日志

7. 持续监控
   ├─ 实时性能追踪
   ├─ 异常告警
   └─ 人工干预接口

        ↓

┌─────────────────────────────────────────────────────────────┐
│                    阶段3:持续优化                           │
└─────────────────────────────────────────────────────────────┘

8. 数据收集
   ├─ 记录实际运行数据
   ├─ 标注异常事件
   └─ 构建真实数据集

9. 模型迭代
   ├─ 用真实数据训练物理模型
   ├─ 重新训练强化学习策略
   └─ A/B测试新旧模型

10. 上线新版本
    ├─ 灰度发布
    ├─ 性能对比
    └─ 全量替换

训练vs预测对比表

维度 训练阶段 预测阶段
目标 学习最优策略 应用最优策略
输入 随机初始状态 真实系统状态
动作选择 ε-贪心(探索+利用) 贪心(纯利用)
奖励 计算并用于学习 计算但不学习
网络更新 每4步更新参数 不更新参数
经验池 存储并采样 不使用
epsilon 1.0 → 0.02 0(不探索)
时间 数小时(50000步) 毫秒级(单次推理)
输出 dqn_model.zip (L_s, t_bw_s)

关键差异示例

训练时的动作选择(第1000步)

state = [0.55, 0.318, 0.5, 0.875]
epsilon = 0.93  # 仍在高探索期

if random() < 0.93:
    action = 127  # 93%概率:随机探索
else:
    q_values = Q_network(state)
    action = argmax(q_values)  # 7%概率:利用

# 即使知道最优动作,也故意选择次优动作来探索

预测时的动作选择

state = [0.55, 0.318, 0.5, 0.875]
epsilon = 0  # 预测时不探索

# 总是选择Q值最大的动作
q_values = Q_network(state)
# q_values = [0.45, ..., 0.89, ..., 0.76, ...]
action = argmax(q_values)  # 一定选择最优

# 确定性决策,相同状态总是输出相同动作

常见问题解答

Q1:为什么训练需要50000步?

A

  • 185个动作 × 多种状态 = 需要大量经验
  • 前30%(15000步)主要是探索,发现好动作
  • 后70%(35000步)是优化,微调策略
  • 太少(如5000步)策略不稳定,太多(如500000步)训练时间长且收益递减

Q2:预测速度有多快?

A

import time

start = time.time()
result = run_uf_DQN_decide(uf_params, 0.032)
end = time.time()

print(f"预测耗时:{(end-start)*1000:.2f} 毫秒")
# 典型输出:预测耗时:15.32 毫秒

# 分解:
# - 状态归一化:<1ms
# - Q网络前向传播:5-10ms
# - 动作解码:<1ms
# - 模拟验证:5-10ms

Q3:模型何时需要重新训练?

A:以下情况需要重训:

  1. 系统参数变化:更换膜组、改变流量范围
  2. 物理模型更新:有了真实数据,改进模拟器
  3. 性能下降:实际回收率持续低于预期
  4. 新约束:增加了新的运行限制

通常3-6个月重训一次。

Q4:如何确保预测的动作安全?

A:多重保障:

# 1. 训练时的约束学习
# 模型在训练时已经学会避免违反约束

# 2. 预测后的模拟验证
feasible, info = simulate_one_supercycle(TMP0, L_s, t_bw_s)
if not feasible:
    # 回退到保守策略
    L_s, t_bw_s = safe_default_action()

# 3. 渐进式调整
# 每次只调整60秒,避免突变

# 4. 人工监督
# 操作员可以随时覆盖模型决策

Q5:训练时为什么要探索?

A

假设没有探索(epsilon=0):

初始Q值是随机的,假设:
- action 50 的Q值 = 0.8(最高)
- action 92 的Q值 = 0.3
- action 120 的Q值 = 0.5

智能体会一直选择action 50,永远不会尝试92和120。

但实际上:
- action 50 的真实价值 = 0.6(不够好)
- action 92 的真实价值 = 0.9(最优!)
- action 120 的真实价值 = 0.7

没有探索,永远发现不了action 92才是最优的。

有探索:
- 前期随机尝试各种动作
- 发现action 92 获得高奖励
- 更新Q值:Q(92) = 0.3 → 0.9
- 后期选择action 92

Q6:预测可以并行吗?

A:可以,但要注意:

# 单个预测(串行)
result = run_uf_DQN_decide(uf_params, TMP0)

# 批量预测(并行,需要修改代码)
TMP0_batch = [0.025, 0.030, 0.035, 0.040]
results = run_uf_DQN_decide_batch(uf_params, TMP0_batch)

# 内部并行:
obs_batch = np.array([
    [0.375, 0.0, 0.0, 0.375],
    [0.55, 0.2, 0.3, 0.6],
    ...
])  # [4, 4]

q_values = Q_network(obs_batch)  # [4, 185]
actions = q_values.argmax(dim=1)  # [4]

# GPU加速:
# PyTorch自动利用GPU并行计算
# 批量预测速度几乎和单次一样快

总结

训练流程核心

  1. 准备阶段:创建环境、初始化Q网络
  2. 探索阶段(0-15000步):随机尝试,填充经验池
  3. 学习阶段(200-50000步):从经验中学习,更新Q值
  4. 优化阶段(15000-50000步):利用为主,微调策略
  5. 保存模型:导出dqn_model.zip

预测流程核心

  1. 加载模型:读取训练好的Q网络
  2. 获取状态:从系统读取TMP等信息
  3. 模型推理:Q网络计算,选择最优动作
  4. 渐进调整:生成PLC指令,逐步靠近目标
  5. 下发执行:发送到PLC,控制超滤系统

关键洞察

  • 训练是学习过程:智能体从"一无所知"到"经验丰富"
  • 预测是应用过程:智能体"发挥所学"解决实际问题
  • 探索是必要代价:没有探索就没有发现
  • 渐进是安全保障:避免参数突变引发风险