1. DQN的核心机制 (经验回放、目标网络、损失函数)
DQN(Deep Q-Network)是将深度学习与Q-learning结合的经典强化学习算法。为了解决神经网络在强化学习中容易发散和不稳定的问题,DQN引入了几个关键机制。
- 经验回放 (Experience Replay)
- 概念: 智能体在与环境交互时,会将每一步的状态转移数据 (st,at,rt,st+1)(s_t, a_t, r_t, s_{t+1})(st,at,rt,st+1) 存储到一个固定容量的经验池(Replay Buffer)中。
- 作用: 神经网络训练通常假设数据是独立同分布的,但强化学习获取的序列数据具有强相关性。通过从经验池中随机采样小批量(Mini-batch)数据进行训练,可以打破数据之间的时序相关性,降低方差。此外,历史数据被多次重用,大大提高了样本的利用效率。
- 目标网络 (Target Network)
- 概念: DQN包含两个结构完全相同的神经网络:评估网络 (Evaluate Net) 和 目标网络 (Target Net) 。评估网络负责输出当前动作的 Q 值,其参数 θ\thetaθ 在每一步都会更新;而目标网络的参数 θ−\theta^-θ− 是固定的,只在经过一定步数(或以软更新的方式)后,才会把评估网络的参数复制过来。
- 作用: 在计算时序差分目标(TD Target)时需要用到下一状态的 Q 值:y=r+γmaxa′Q(s′,a′;θ−)y = r + \gamma \max_{a'} Q(s', a'; \theta^-)y=r+γmaxa′Q(s′,a′;θ−)。如果只用一个网络,目标值和当前预测值会同时发生变化,导致算法像"追逐自己尾巴的狗",容易产生训练震荡甚至发散。目标网络通过固定 θ−\theta^-θ− 一段时间,提供了稳定的更新目标。
- 损失函数 (Loss Function)
- 概念: 损失函数用于衡量评估网络的预测 Q 值与目标网络计算出的目标 Q 值之间的差距。
- 计算: 通常使用均方误差(MSE)或 Huber Loss。MSE 的计算公式为:
L(θ)=E[(r+γmaxa′Q(s′,a′;θ−)−Q(s,a;θ))2]L(\theta) = \mathbb{E} \left[ \left( r + \gamma \max_{a'} Q(s', a'; \theta^-) - Q(s, a; \theta) \right)^2 \right]L(θ)=E[(r+γa′maxQ(s′,a′;θ−)−Q(s,a;θ))2]
通过梯度下降算法最小化这个损失函数,从而更新评估网络的参数 θ\thetaθ。
2. Python:搭建DQN网络
在 Python 环境下(通常使用 PyTorch 或 TensorFlow 框架),搭建 DQN 需要实现以下几个核心模块:
- 神经网络模型 (Neural Network): 通常是一个多层感知机(MLP)。输入层维度对应环境的状态空间大小(例如车速、需求功率、当前 SOC 等),输出层维度对应离散动作空间的大小(例如发动机/电机的扭矩分配比例)。
- 智能体类 (Agent Class): 封装 DQN 的核心逻辑,主要包含:
choose_action(state):基于 ϵ\epsilonϵ-greedy 策略选择动作(以 ϵ\epsilonϵ 的概率随机探索,以 1−ϵ1-\epsilon1−ϵ 的概率利用当前网络选择最大 Q 值的动作)。store_transition():将与环境交互得到的数据存入经验池。learn():从经验池中采样,计算损失函数,执行反向传播并更新网络参数;同时负责定期更新目标网络。
- 环境交互循环 (Training Loop): 将 DQN Agent 与车辆仿真模型(环境)连接,不断执行"观察状态 -> 执行动作 -> 获得奖励 -> 更新状态"的循环。
3. 多目标优化:油耗 + SOC + 电池损耗
在混合动力汽车(HEV)的能量管理策略(EMS)中,通常需要平衡多个相互冲突的目标。这正是这篇论文的优化核心。
- 油耗 (Fuel Consumption): 这是最基础的优化目标。通过合理分配发动机和电机的功率输出(例如在低效区间让发动机停机或仅发电,由电机驱动),来使发动机尽可能工作在最佳燃油经济性区域(BSFC 最优线附近),从而最小化总油耗。
- SOC (State of Charge) 维持:
- 对于非插电式混动(HEV),电池主要起"削峰填谷"的作用。为了保证电池的持续充放电能力并防止过充/过放,需要将 SOC 维持在一个合理的区间(通常在 0.4 到 0.8 之间)。
- 电量维持 (Charge-Sustaining, CS): 仿真结束时的终端 SOC 必须尽可能等于初始 SOC,以保证油耗评价的公平性。在强化学习中,通常会在奖励函数中引入基于 SOC 偏差的惩罚项。
- 电池损耗 (Battery Degradation):
- 频繁的大电流充放电、极端的温度或极端的 SOC 状态都会加速锂电池的老化。
- 将电池损耗作为优化目标,意味着在能量分配时,不仅要考虑当前的燃油经济性,还要避免电机输出剧烈波动,从而延长电池的使用寿命。这通常通过内阻老化模型或安时吞吐量(Ah-throughput)模型来量化。
- 强化学习中的多目标融合:
在 DQN 中,这三个目标需要被整合到一个标量奖励函数 (Reward Function) 中。通常采用加权求和的方式:
R=−(w1⋅Costfuel+w2⋅PenaltySOC+w3⋅Costbattery)R = - (w_1 \cdot \text{Cost}{fuel} + w_2 \cdot \text{Penalty}{SOC} + w_3 \cdot \text{Cost}_{battery})R=−(w1⋅Costfuel+w2⋅PenaltySOC+w3⋅Costbattery)
权重的设计决定了系统在燃油经济性、SOC 稳定性和电池寿命之间的权衡倾向。
我们可以构建一个混合动力汽车(HEV)能量管理的具体场景,来看看 DQN 是如何结合 Python 代码来处理"油耗 + SOC + 电池损耗"这三个多目标优化问题的。
1. 强化学习建模 (定义 MDP)
首先,我们需要把 HEV 能量分配问题转化为 DQN 能够理解的数学模型:
-
状态空间 (State): 智能体需要知道车辆当前的工况。假设状态 st=[Preq,v,SOCt]s_t = [P_{req}, v, SOC_t]st=[Preq,v,SOCt],即当前车辆的需求功率、车速以及电池的剩余电量。
-
动作空间 (Action): DQN 只能处理离散动作。假设我们要控制的是发动机的输出功率 PengP_{eng}Peng,我们将其离散化为 5 个挡位:a∈[0kW,10kW,20kW,30kW,40kW]a \in [0\text{kW}, 10\text{kW}, 20\text{kW}, 30\text{kW}, 40\text{kW}]a∈[0kW,10kW,20kW,30kW,40kW]。(注意:由于 PreqP_{req}Preq 已知,确定了发动机功率,电机的输出/发电功率也就自然确定了。)
-
奖励函数 (Reward): 这是多目标优化的核心。我们需要在 Python 中设计一个函数,将三个目标量化为一个标量:
- 油耗成本: mfm_{f}mf (当前动作下的燃油消耗率)。
- SOC 惩罚: 假设参考 SOC 为 0.6,惩罚项为 α(SOCt−0.6)2\alpha (SOC_t - 0.6)^2α(SOCt−0.6)2,偏离越多惩罚越大。
- 电池损耗: 采用简化模型,使用电池电流的平方 βIbat2\beta I_{bat}^2βIbat2 来表征内部发热和老化。
最终的奖励函数为它们的加权负和(因为强化学习是追求 Reward 最大化,而我们要最小化成本):
rt=−(w1⋅mf+w2⋅α(SOCt−0.6)2+w3⋅βIbat2)r_t = - (w_1 \cdot m_{f} + w_2 \cdot \alpha (SOC_t - 0.6)^2 + w_3 \cdot \beta I_{bat}^2)rt=−(w1⋅mf+w2⋅α(SOCt−0.6)2+w3⋅βIbat2)
2. 仿真运行的一个具体 Step
假设在仿真的第 100 秒:
- 观察状态: 车辆正在以 60 km/h 巡航,需求功率 Preq=20kWP_{req} = 20\text{kW}Preq=20kW,当前 SOC=0.55SOC = 0.55SOC=0.55(略低于目标值 0.6)。
- 选择动作: Python 中的 DQN 评估网络接收到状态 [20,60,0.55][20, 60, 0.55][20,60,0.55]。通过前向传播,网络可能会发现如果选择动作
30kW(发动机输出 30kW),多出的 10kW 可以用来驱动电机发电给电池充电,从而拉高 SOC。因此,网络输出了最高 Q 值对应的动作:Peng=30kWP_{eng} = 30\text{kW}Peng=30kW。 - 执行与反馈: 环境接收到动作,计算出此刻的燃油消耗、电流大小和新的 SOC (比如升到了 0.552),并根据上面的公式计算出一个综合 Reward rt=−0.8r_t = -0.8rt=−0.8。
- 经验回放: Python 脚本会将这一步的数据组 ([20,60,0.55],30kW,−0.8,[21,62,0.552])([20, 60, 0.55], 30\text{kW}, -0.8, [21, 62, 0.552])([20,60,0.55],30kW,−0.8,[21,62,0.552]) 存入经验池中,留作后续采样训练。
3. Python 代码核心逻辑演示
在实际的 Python 仿真脚本中,这部分的交互和训练逻辑通常是这样搭建的:
python
# 假设已经定义好了环境 env 和智能体 agent
for episode in range(MAX_EPISODES):
state = env.reset() # 获取初始工况 [P_req, v, SOC]
for step in range(MAX_STEPS):
# 1. 动作选择 (epsilon-greedy 策略)
action = agent.choose_action(state)
# 2. 与环境交互,获取多目标反馈
# env.step 内部会计算 油耗、SOC变化 和 电池老化,并加权求和得出 reward
next_state, reward, done = env.step(action)
# 3. 存入经验池 (Experience Replay)
agent.store_transition(state, action, reward, next_state, done)
# 4. 从经验池采样,计算损失函数并更新网络
if agent.memory_counter > BATCH_SIZE:
agent.learn() # 内部包含计算 TD Target, MSE Loss 以及反向传播
state = next_state
if done:
break
在这个过程中,DQN 会在几百上千个 Episode 中不断试错,最终学习到在任何车速和 SOC 下,如何最优地分配发动机和电机的功率,以达到油耗、SOC 稳定和电池寿命的最佳平衡。
这是一份针对混合动力汽车(HEV)能量管理策略的完整、可落地的 Simulink 建模与 Python 深度强化学习(DQN)联合仿真开发指南。
我们将构建一个基于离散时间步的架构,使得 Python 能够像控制游戏一样,单步控制 Simulink 模型的运行。
第一部分:Simulink 被控对象详细建模过程
为了实现强化学习的交互,Simulink 模型必须是一个离散系统,并且能够接受外部随时打断和注入新状态。
1. 模型全局设置
新建一个 Simulink 模型,命名为 HEV_RL_Env.slx。
- 打开 Modeling -> Model Settings。
- Solver selection: Type 设为 Fixed-step ,Solver 设为 discrete (no continuous states)。
- Solver details: Fixed-step size 设为
1(代表每个仿真步长为 1 秒)。
2. 模块清单与参数配置
请从 Simulink Library Browser 中拖入以下模块,并严格按照参数进行配置:
A. 输入接口 (接收 Python 指令)
- 模块 1 & 2:
From Workspace(路径: Simulink / Sources)- 名称:
In_P_eng/In_v_req - 参数 Data: 分别填入
simin_P_eng和simin_v_req。 - 参数 Sample time:
1。
- 名称:
B. 需求功率计算 (车辆纵向动力学)
- 模块 3:
MATLAB Function(路径: Simulink / User-Defined Functions)-
名称:
Vehicle_Dynamics -
内部代码:
matlabfunction P_req = fcn(v) m = 1500; g = 9.81; f = 0.015; Cd = 0.3; A = 2.2; rho = 1.225; eff = 0.9; % 传动效率 % 计算并输出当前车速下的需求功率 (kW) P_req = (m*g*f*v + 0.5*rho*Cd*A*v^3) / 1000 / eff; end
-
C. 功率分配节点
- 模块 4:
Subtract(路径: Simulink / Math Operations)- 名称:
Power_Split - 参数 List of signs:
+-。
- 名称:
D. 发动机与油耗评估
- 模块 5:
1-D Lookup Table(路径: Simulink / Lookup Tables)- 名称:
Engine_Map - 参数 Breakpoints 1:
[0, 10, 20, 30, 40](发动机输出功率 kW)。 - 参数 Table data:
[0, 0.8, 1.4, 2.1, 2.9](对应的瞬时燃油消耗率 g/s)。
- 名称:
- 模块 6:
Discrete-Time Integrator(路径: Simulink / Discrete)- 名称:
Fuel_Integrator - 参数 Sample time:
1。
- 名称:
E. 电池与状态更新
-
模块 7:
MATLAB Function(路径: Simulink / User-Defined Functions)-
名称:
Battery_Dynamics -
内部代码:
matlabfunction [I_bat, dSOC] = fcn(P_mot) Voc = 300; % 假设恒定开路电压 V Rint = 0.15; % 假设恒定内阻 Ohm Qcap = 6.5 * 3600; % 电池容量 As % 解方程: Rint * I^2 - Voc * I + P_mot * 1000 = 0 delta = Voc^2 - 4 * Rint * P_mot * 1000; if delta < 0 delta = 0; % 避免虚数报错 end I_bat = (Voc - sqrt(delta)) / (2 * Rint); % 输出电流和 SOC 变化率 (负值代表放电) dSOC = -I_bat / Qcap; end
-
-
模块 8:
Discrete-Time Integrator(路径: Simulink / Discrete)- 名称:
SOC_Integrator - 参数 Initial condition source: 更改为
external(必须修改,用于接受 Python 初始化的 SOC)。 - 参数 Sample time:
1。
- 名称:
-
模块 9:
From Workspace(路径: Simulink / Sources)- 名称:
In_Init_SOC - 参数 Data:
simin_init_SOC。
- 名称:
F. 输出接口 (回传给 Python)
- 模块 10, 11, 12:
To Workspace(路径: Simulink / Sinks)- 名称:
Out_SOC/Out_Fuel/Out_I_bat - 参数 Variable name: 分别填
out_SOC,out_Fuel,out_I_bat。 - 参数 Save format: 必须选择
Array。
- 名称:
3. 严格的连线逻辑 (Routing)
- 将
In_v_req连接至Vehicle_Dynamics的输入端v。 - 将
Vehicle_Dynamics的输出端P_req连接至Power_Split的+输入口。 - 将
In_P_eng的输出线分支 (按住 Ctrl 键拖拽),分别连接至:Power_Split的-输入口。Engine_Map的输入端。
- 将
Power_Split的输出端连接至Battery_Dynamics的输入端P_mot。 - 将
Engine_Map的输出端连接至Fuel_Integrator。 - 将
Fuel_Integrator的输出端连接至Out_Fuel。 - 将
Battery_Dynamics的输出端I_bat连接至Out_I_bat。 - 将
Battery_Dynamics的输出端dSOC连接至SOC_Integrator的主输入口。 - 将
In_Init_SOC连接至SOC_Integrator多出来的初始状态输入口 (通常带有x0标识)。 - 将
SOC_Integrator的输出端连接至Out_SOC。
第二部分:Python 强化学习完整控制代码
这套代码使用 PyTorch 搭建 DQN 网络,并通过 matlab.engine 将其与上述 Simulink 模型打通。代码内置了归一化的多目标奖励函数。
python
import matlab.engine
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import random
from collections import deque
import math
# ==========================================
# 1. 定义 DQN 神经网络结构
# ==========================================
class DQN_Network(nn.Module):
def __init__(self, state_dim, action_dim):
super(DQN_Network, self).__init__()
# 使用两层隐藏层的多层感知机 (MLP)
self.net = nn.Sequential(
nn.Linear(state_dim, 128),
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, action_dim)
)
def forward(self, x):
return self.net(x)
# ==========================================
# 2. 定义强化学习智能体
# ==========================================
class DQNAgent:
def __init__(self, state_dim, action_dim):
self.action_dim = action_dim
self.memory = deque(maxlen=10000)
# 评估网络与目标网络
self.eval_net = DQN_Network(state_dim, action_dim)
self.target_net = DQN_Network(state_dim, action_dim)
self.target_net.load_state_dict(self.eval_net.state_dict())
self.optimizer = optim.Adam(self.eval_net.parameters(), lr=0.001)
self.loss_func = nn.SmoothL1Loss() # 使用 Huber Loss 提高稳定性
# 超参数
self.gamma = 0.99
self.epsilon = 1.0
self.epsilon_decay = 0.995
self.epsilon_min = 0.05
self.batch_size = 64
self.target_update_freq = 200
self.step_counter = 0
def choose_action(self, state):
# Epsilon-Greedy 探索策略
if np.random.uniform() < self.epsilon:
return random.randrange(self.action_dim)
state_tensor = torch.FloatTensor(state).unsqueeze(0)
with torch.no_grad():
q_values = self.eval_net(state_tensor)
return torch.argmax(q_values).item()
def store_transition(self, s, a, r, s_, done):
self.memory.append((s, a, r, s_, done))
def learn(self):
if len(self.memory) < self.batch_size:
return
self.step_counter += 1
if self.step_counter % self.target_update_freq == 0:
self.target_net.load_state_dict(self.eval_net.state_dict())
# 经验回放采样
batch = random.sample(self.memory, self.batch_size)
b_s, b_a, b_r, b_s_, b_d = zip(*batch)
b_s = torch.FloatTensor(np.array(b_s))
b_a = torch.LongTensor(b_a).unsqueeze(1)
b_r = torch.FloatTensor(b_r).unsqueeze(1)
b_s_ = torch.FloatTensor(np.array(b_s_))
b_d = torch.FloatTensor(b_d).unsqueeze(1)
# 计算 Q 值与目标值
q_eval = self.eval_net(b_s).gather(1, b_a)
q_next = self.target_net(b_s_).detach().max(1)[0].unsqueeze(1)
q_target = b_r + self.gamma * q_next * (1 - b_d)
# 反向传播更新网络
loss = self.loss_func(q_eval, q_target)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
# 衰减探索率
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
# ==========================================
# 3. 封装 Simulink 交互环境
# ==========================================
class SimulinkEnv:
def __init__(self, model_name='HEV_RL_Env'):
print("Initializing MATLAB Engine...")
self.eng = matlab.engine.start_matlab()
self.model_name = model_name
self.eng.eval(f"load_system('{self.model_name}')", nargout=0)
# 动作空间:5个离散功率档位 (kW)
self.action_space = [0.0, 10.0, 20.0, 30.0, 40.0]
self.dt = 1.0
self.current_time = 0.0
self.current_soc = 0.6
self.current_v = 10.0
self.max_time = 200.0 # 每个回合仿真时间
def reset(self):
self.current_time = 0.0
self.current_soc = 0.6
self.current_v = 15.0 # 初始车速 m/s
return np.array([self.current_v, self.current_soc])
def step(self, action_idx):
P_eng_action = self.action_space[action_idx]
# 1. 注入变量到 MATLAB 工作区
self.eng.workspace['simin_P_eng'] = float(P_eng_action)
self.eng.workspace['simin_v_req'] = float(self.current_v)
self.eng.workspace['simin_init_SOC'] = float(self.current_soc)
# 2. 控制 Simulink 仿真 1 个步长
sim_start = str(self.current_time)
sim_stop = str(self.current_time + self.dt)
self.eng.eval(f"sim('{self.model_name}', 'StartTime', '{sim_start}', 'StopTime', '{sim_stop}')", nargout=0)
# 3. 提取仿真结果 (获取 Array 的最后一个元素)
next_soc = np.array(self.eng.workspace['out_SOC'])[-1][0]
fuel_rate = np.array(self.eng.workspace['out_Fuel'])[-1][0]
bat_current = np.array(self.eng.workspace['out_I_bat'])[-1][0]
# 4. 计算归一化多目标 Reward
# 参数归一化基准
max_fuel = 3.0 # 假设最大油耗 3 g/s
soc_ref = 0.6 # 目标维持 SOC
soc_tol = 0.05 # 允许偏差
max_current = 150.0 # 假设最大电流 150A
norm_fuel = fuel_rate / max_fuel
norm_soc_penalty = ((next_soc - soc_ref) / soc_tol) ** 2
norm_deg_penalty = (bat_current / max_current) ** 2
# 设置权重参数
w1, w2, w3 = 0.5, 0.4, 0.1
reward = -(w1 * norm_fuel + w2 * norm_soc_penalty + w3 * norm_deg_penalty)
# 5. 更新状态与时间
self.current_soc = next_soc
self.current_time += self.dt
# 模拟随机车速变化 (代替真实 Drive Cycle)
self.current_v += random.uniform(-2.0, 2.0)
self.current_v = max(0.0, min(self.current_v, 33.0))
done = self.current_time >= self.max_time
next_state = np.array([self.current_v, self.current_soc])
return next_state, float(reward), done
# ==========================================
# 4. 执行训练逻辑
# ==========================================
if __name__ == "__main__":
env = SimulinkEnv()
state_dim = 2 # [v, soc]
action_dim = len(env.action_space)
agent = DQNAgent(state_dim, action_dim)
MAX_EPISODES = 300
for episode in range(MAX_EPISODES):
state = env.reset()
episode_reward = 0
while True:
# 智能体选择动作
action = agent.choose_action(state)
# 环境执行动作并返回反馈
next_state, reward, done = env.step(action)
# 存储数据并训练
agent.store_transition(state, action, reward, next_state, done)
agent.learn()
state = next_state
episode_reward += reward
if done:
print(f"Episode: {episode+1:03d} | Reward: {episode_reward:7.2f} | End SOC: {state[1]:.4f} | Epsilon: {agent.epsilon:.3f}")
break
env.eng.quit()
print("Training Completed.")
这个报错在 Python 与 Simulink 联合仿真中非常经典。
报错原因:
当 Simulink 加载(load_system)或准备运行模型时,它会进行一次编译自检 。此时,模型里的 From Workspace 模块会去 MATLAB 的基础工作区(Base Workspace)寻找名为 simin_P_eng、simin_v_req 和 simin_init_SOC 的变量。
但是,我们的 Python 代码是在 step() 函数里才第一次把这些变量塞进工作区。因此,Simulink 在初始化阶段"扑了个空",直接报错拒绝运行。
为了彻底解决这个问题,并且让后续的强化学习单步仿真(每次只传一个标量数值)更加稳定,我推荐你使用以下两种方案中的一种。强烈推荐方案一,这在做强化学习的标量动作输入时最不容易产生数据维度(Time-series)冲突。
方案一:将 From Workspace 替换为 Constant 模块(最推荐,最稳定)
在强化学习单步交互中,Python 每次传给 Simulink 的只是一个当前的标量数值 (比如推力 30、车速 15),而不是带时间戳的时间序列数组。Constant 模块能完美接收工作区中的标量变量,且不会报数据维度的错误。
详细修改步骤:
- 删除 原模型中的三个
From Workspace模块 (In_P_eng,In_v_req,In_Init_SOC)。 - 从 Simulink 库 (Sources 库) 中拖入三个
Constant(常数) 模块放置在原位。 - 双击这三个
Constant模块,将其 Constant value 参数分别修改为对应的变量名 (不要填数字,填变量名):- 第一个 Constant value 填入:
simin_P_eng - 第二个 Constant value 填入:
simin_v_req - 第三个 Constant value 填入:
simin_init_SOC
- 第一个 Constant value 填入:
- 保持它们与其他模块的连线与原来完全一致。
配合此方案的 Python 代码修改:
为了防止 Simulink 首次编译时找不到这三个变量,必须在 Python 启动 MATLAB 引擎后,立刻 给它们赋初始值。请修改 SimulinkEnv 类的 __init__ 函数:
python
class SimulinkEnv:
def __init__(self, model_name='HEV_RL_Env'): # 替换为你的实际模型名
print("Initializing MATLAB Engine...")
self.eng = matlab.engine.start_matlab()
self.model_name = model_name
# 【关键修复】:在加载 Simulink 模型之前,提前在工作区注入初始变量!
# 这样 Simulink 编译时就能找到它们,不会报错。
self.eng.workspace['simin_P_eng'] = 0.0
self.eng.workspace['simin_v_req'] = 0.0
self.eng.workspace['simin_init_SOC'] = 0.6
# 现在再加载模型,编译自检就能顺利通过
self.eng.eval(f"load_system('{self.model_name}')", nargout=0)
self.action_space = [0.0, 10.0, 20.0, 30.0, 40.0]
self.dt = 1.0
self.current_time = 0.0
self.current_soc = 0.6
self.current_v = 10.0
self.max_time = 200.0
方案二:利用 Simulink 的回调函数 (InitFcn) 自动初始化
如果你一定要坚持使用 From Workspace 模块,除了在 Python 中提前注入变量外,还可以直接让 Simulink 模型"自己照顾自己"。设置模型在初始化时,自动在 MATLAB 工作区创建这些变量。
详细修改步骤:
-
保持你的
From Workspace模块不变。 -
在 Simulink 模型界面的空白处点击右键 ,选择 Model Properties (模型属性)。
-
在弹出的窗口中,选择 Callbacks (回调) 选项卡。
-
在左侧列表中选中 InitFcn (初始化函数)。
-
在右侧的代码框中输入以下 MATLAB 代码:
matlab% 模型初始化时自动创建默认变量,防止报错 simin_P_eng = 0; simin_v_req = 0; simin_init_SOC = 0.6; -
点击 Apply 和 OK 保存。
(注:如果你保留 From Workspace,后续可能还会遇到一个报错:From Workspace 默认期望接收二维数组 [time, value] 而不是单单一个标量。如果出现那个报错,你需要在 Python 的 step 函数中构造时间序列 [[0, current_time], [value, value]] 传过去,非常繁琐。所以依然强烈建议你直接使用方案一的 Constant 模块。)
1. 致命细节:"单一仿真输出"导致 Python 读不到数据
观察截图: 你的三个输出模块显示的是 out.out_I_bat、out.out_Fuel、out.out_SOC。
问题所在: 前面的 out. 说明你的 Simulink 开启了较新版本默认的**"单一仿真输出 (Single simulation output)"**功能。这意味着仿真结束后,所有数据都被打包进了一个名为 out 的对象中,而不是单独的 out_SOC 变量。如果保持原样,Python 执行 eng.workspace['out_SOC'] 时会报 KeyError,因为工作区里根本没有这个独立变量。
修复方法:
- 在 Simulink 界面按键盘快捷键 Ctrl + E 打开 Model Settings (模型设置)。
- 在左侧列表中选择 Data Import/Export。
- 在右侧页面找到 Single simulation output (通常在最下面),取消勾选它。
- 点击 Apply 保存。这样模块内部就会变回纯粹的
out_SOC,Python 就能顺利抓取了。
2. 确认输入模块类型 (Constant 还是 From Workspace)
观察截图: 左侧 simin_P_eng 模块的下方依然带有蓝色的 From Workspace 字样。
建议操作: 如果这三个模块依然是 From Workspace,那么你必须 在 Python 端把单步动作(标量)包装成时间矩阵(也就是上一次回答中提到的"方法二")。
为了让强化学习的训练速度更快、代码更简洁,我再次强烈建议你:直接把这三个模块删掉,换成 Constant (常数) 模块,然后把 Constant value 改成对应的变量名 simin_v_req 等。这样 Python 直接传 float 标量就能跑,非常丝滑。
路线一:修改 Simulink 模型(强烈推荐,一劳永逸)
如果你有权限修改这个 .slx 模型文件,请务必按照以下步骤把它改掉,之后的联调会无比丝滑:
- 打开 MATLAB,双击打开你的
DQN_train.slx。 - 找到最左侧报错的那三个模块(名字带有
From Workspace、蓝色字体的模块)。 - **把它们三个全部按 Delet
- e 键删掉!**
- 在上方菜单栏打开 Library Browser (库浏览器),搜索
Constant(常数模块),拖三个出来放到刚刚删除的位置,并重新连好线。 - 双击这三个新的
Constant模块,在 Constant value 栏位里,分别填入:simin_P_engsimin_v_reqsimin_init_SOC
- 最重要的一步:按下
Ctrl + S保存你的 Simulink 模型。
只要你完成了这一步并保存,你刚才运行的那版"极简版" Python 代码就能瞬间跑通,再也不会有维度的报错。
路线二:保留 Simulink 不变,使用"矩阵包装版" Python 代码
如果你因为某些原因(比如导师要求、作业规定)绝对不能修改 Simulink 里的模块 ,必须保留 From Workspace,那你必须使用下面这段带有"时间边界放大矩阵"的 Python 代码。
请用以下代码完全覆盖 你的 DQN_train.py,再运行一次(这段代码会在每次下发动作时,自动把标量伪装成 Simulink 想要的二维时间矩阵):
python
import matlab
import matlab.engine
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import random
from collections import deque
class DQN_Network(nn.Module):
def __init__(self, state_dim, action_dim):
super(DQN_Network, self).__init__()
self.net = nn.Sequential(
nn.Linear(state_dim, 128), nn.ReLU(),
nn.Linear(128, 128), nn.ReLU(),
nn.Linear(128, action_dim)
)
def forward(self, x):
return self.net(x)
class DQNAgent:
def __init__(self, state_dim, action_dim):
self.action_dim = action_dim
self.memory = deque(maxlen=10000)
self.eval_net = DQN_Network(state_dim, action_dim)
self.target_net = DQN_Network(state_dim, action_dim)
self.target_net.load_state_dict(self.eval_net.state_dict())
self.optimizer = optim.Adam(self.eval_net.parameters(), lr=0.001)
self.loss_func = nn.SmoothL1Loss()
self.gamma = 0.99
self.epsilon = 1.0
self.epsilon_decay = 0.995
self.epsilon_min = 0.05
self.batch_size = 64
self.target_update_freq = 200
self.step_counter = 0
def choose_action(self, state):
if np.random.uniform() < self.epsilon:
return random.randrange(self.action_dim)
with torch.no_grad():
q_values = self.eval_net(torch.FloatTensor(state).unsqueeze(0))
return torch.argmax(q_values).item()
def store_transition(self, s, a, r, s_, done):
self.memory.append((s, a, r, s_, done))
def learn(self):
if len(self.memory) < self.batch_size: return
self.step_counter += 1
if self.step_counter % self.target_update_freq == 0:
self.target_net.load_state_dict(self.eval_net.state_dict())
batch = random.sample(self.memory, self.batch_size)
b_s, b_a, b_r, b_s_, b_d = zip(*batch)
b_s = torch.FloatTensor(np.array(b_s))
b_a = torch.LongTensor(b_a).unsqueeze(1)
b_r = torch.FloatTensor(b_r).unsqueeze(1)
b_s_ = torch.FloatTensor(np.array(b_s_))
b_d = torch.FloatTensor(b_d).unsqueeze(1)
q_eval = self.eval_net(b_s).gather(1, b_a)
q_next = self.target_net(b_s_).detach().max(1)[0].unsqueeze(1)
q_target = b_r + self.gamma * q_next * (1 - b_d)
loss = self.loss_func(q_eval, q_target)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
class SimulinkEnv:
def __init__(self, model_name='DQN_train'):
print("启动 MATLAB 后台引擎中,请稍候...")
self.eng = matlab.engine.start_matlab()
self.model_name = model_name
self.dt = 1.0
# 针对 From Workspace 的特殊处理:构造长达 10 倍步长的时间矩阵
t_start = 0.0
t_end = self.dt * 10.0
self.eng.workspace['simin_P_eng'] = matlab.double([[t_start, 0.0], [t_end, 0.0]])
self.eng.workspace['simin_v_req'] = matlab.double([[t_start, 0.0], [t_end, 0.0]])
self.eng.workspace['simin_init_SOC'] = matlab.double([[t_start, 0.6], [t_end, 0.6]])
print("加载 Simulink 模型...")
self.eng.eval(f"load_system('{self.model_name}')", nargout=0)
self.action_space = [0.0, 10.0, 20.0, 30.0, 40.0]
self.current_time = 0.0
self.current_soc = 0.6
self.current_v = 15.0
self.max_time = 100.0
def reset(self):
self.current_time = 0.0
self.current_soc = 0.6
self.current_v = 15.0
return np.array([self.current_v, self.current_soc])
def step(self, action_idx):
P_eng_action = self.action_space[action_idx]
# 针对 From Workspace 的特殊处理:每次 step 都更新时间矩阵
t1 = float(self.current_time)
t2 = float(self.current_time + self.dt * 10.0)
self.eng.workspace['simin_P_eng'] = matlab.double([[t1, float(P_eng_action)], [t2, float(P_eng_action)]])
self.eng.workspace['simin_v_req'] = matlab.double([[t1, float(self.current_v)], [t2, float(self.current_v)]])
self.eng.workspace['simin_init_SOC'] = matlab.double([[t1, float(self.current_soc)], [t2, float(self.current_soc)]])
sim_start = str(self.current_time)
sim_stop = str(self.current_time + self.dt)
self.eng.eval(f"sim('{self.model_name}', 'StartTime', '{sim_start}', 'StopTime', '{sim_stop}')", nargout=0)
next_soc = np.array(self.eng.workspace['out_SOC'])[-1][0]
fuel_rate = np.array(self.eng.workspace['out_Fuel'])[-1][0]
bat_current = np.array(self.eng.workspace['out_I_bat'])[-1][0]
w1, w2, w3 = 0.5, 0.4, 0.1
reward = -(w1 * (fuel_rate/3.0) + w2 * (((next_soc-0.6)/0.05)**2) + w3 * ((bat_current/150.0)**2))
self.current_soc = next_soc
self.current_time += self.dt
self.current_v += random.uniform(-2.0, 2.0)
self.current_v = max(0.0, min(self.current_v, 33.0))
done = self.current_time >= self.max_time
return np.array([self.current_v, self.current_soc]), float(reward), done
if __name__ == "__main__":
env = SimulinkEnv(model_name='DQN_train')
agent = DQNAgent(state_dim=2, action_dim=len(env.action_space))
for episode in range(50):
state = env.reset()
episode_reward = 0
while True:
action = agent.choose_action(state)
next_state, reward, done = env.step(action)
agent.store_transition(state, action, reward, next_state, done)
agent.learn()
state = next_state
episode_reward += reward
if done:
print(f"Episode: {episode+1:03d} | Total Reward: {episode_reward:7.2f} | Final SOC: {state[1]:.4f}")
break
env.eng.quit()
完整代码
python
import matlab.engine
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import random
from collections import deque
# ==========================================
# 1. DQN 神经网络模型
# ==========================================
class DQN_Network(nn.Module):
def __init__(self, state_dim, action_dim):
super(DQN_Network, self).__init__()
self.net = nn.Sequential(
nn.Linear(state_dim, 128),
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, action_dim)
)
def forward(self, x):
return self.net(x)
# ==========================================
# 2. DQN 智能体逻辑
# ==========================================
class DQNAgent:
def __init__(self, state_dim, action_dim):
self.action_dim = action_dim
self.memory = deque(maxlen=10000)
self.eval_net = DQN_Network(state_dim, action_dim)
self.target_net = DQN_Network(state_dim, action_dim)
self.target_net.load_state_dict(self.eval_net.state_dict())
self.optimizer = optim.Adam(self.eval_net.parameters(), lr=0.001)
self.loss_func = nn.SmoothL1Loss()
self.gamma = 0.99
self.epsilon = 1.0
self.epsilon_decay = 0.995
self.epsilon_min = 0.05
self.batch_size = 64
self.target_update_freq = 200
self.step_counter = 0
def choose_action(self, state):
if np.random.uniform() < self.epsilon:
return random.randrange(self.action_dim)
state_tensor = torch.FloatTensor(state).unsqueeze(0)
with torch.no_grad():
q_values = self.eval_net(state_tensor)
return torch.argmax(q_values).item()
def store_transition(self, s, a, r, s_, done):
self.memory.append((s, a, r, s_, done))
def learn(self):
if len(self.memory) < self.batch_size:
return
self.step_counter += 1
if self.step_counter % self.target_update_freq == 0:
self.target_net.load_state_dict(self.eval_net.state_dict())
batch = random.sample(self.memory, self.batch_size)
b_s, b_a, b_r, b_s_, b_d = zip(*batch)
b_s = torch.FloatTensor(np.array(b_s))
b_a = torch.LongTensor(b_a).unsqueeze(1)
b_r = torch.FloatTensor(b_r).unsqueeze(1)
b_s_ = torch.FloatTensor(np.array(b_s_))
b_d = torch.FloatTensor(b_d).unsqueeze(1)
q_eval = self.eval_net(b_s).gather(1, b_a)
q_next = self.target_net(b_s_).detach().max(1)[0].unsqueeze(1)
q_target = b_r + self.gamma * q_next * (1 - b_d)
loss = self.loss_func(q_eval, q_target)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
# ==========================================
# 3. 极度精简的 Simulink 交互环境
# ==========================================
class SimulinkEnv:
def __init__(self, model_name='DQN_train'):
print("启动 MATLAB 后台引擎中,请稍候...")
self.eng = matlab.engine.start_matlab()
self.model_name = model_name
self.dt = 1.0
# 【清爽注入】因为改成了 Constant 模块,直接给纯数字标量即可!
self.eng.workspace['simin_P_eng'] = 0.0
self.eng.workspace['simin_v_req'] = 15.0
self.eng.workspace['simin_init_SOC'] = 0.6
print("加载 Simulink 模型...")
self.eng.eval(f"load_system('{self.model_name}')", nargout=0)
#【新增的终极解法】:用底层指令直接把模型的单一输出给关了!
# ========================================================
self.eng.eval(f"set_param('{self.model_name}', 'ReturnWorkspaceOutputs', 'off')", nargout=0)
self.action_space = [0.0, 10.0, 20.0, 30.0, 40.0]
self.current_time = 0.0
self.current_soc = 0.6
self.current_v = 15.0
self.max_time = 100.0
def reset(self):
self.current_time = 0.0
self.current_soc = 0.6
self.current_v = 15.0
return np.array([self.current_v, self.current_soc])
def step(self, action_idx):
P_eng_action = self.action_space[action_idx]
# 将当前状态指令下发给 Simulink (恒定值)
self.eng.workspace['simin_P_eng'] = float(P_eng_action)
self.eng.workspace['simin_v_req'] = float(self.current_v)
self.eng.workspace['simin_init_SOC'] = float(self.current_soc)
# 设置仿真时间
sim_start = str(self.current_time)
sim_stop = str(self.current_time + self.dt)
# =====================================================================
# 【终极降维打击】:既然你喜欢打包,我就把你赋值给一个叫 simOut 的变量
# =====================================================================
cmd = f"simOut = sim('{self.model_name}', 'StartTime', '{sim_start}', 'StopTime', '{sim_stop}');"
self.eng.eval(cmd, nargout=0)
# 然后命令 MATLAB 自己把包裹里的数据拆出来,赋值给纯净的数组变量
self.eng.eval("val_SOC = simOut.out_SOC;", nargout=0)
self.eng.eval("val_Fuel = simOut.out_Fuel;", nargout=0)
self.eng.eval("val_I_bat = simOut.out_I_bat;", nargout=0)
# 现在,Python 再去拿这三个纯净的数组,绝对一拿一个准!
next_soc = np.array(self.eng.workspace['val_SOC'])[-1][0]
fuel_rate = np.array(self.eng.workspace['val_Fuel'])[-1][0]
bat_current = np.array(self.eng.workspace['val_I_bat'])[-1][0]
# =====================================================================
# 计算多目标奖励函数:油耗惩罚 + SOC维持惩罚 + 电池老化惩罚
w1, w2, w3 = 0.5, 0.4, 0.1
norm_fuel = fuel_rate / 3.0
norm_soc_penalty = ((next_soc - 0.6) / 0.05) ** 2
norm_deg_penalty = (bat_current / 150.0) ** 2
reward = -(w1 * norm_fuel + w2 * norm_soc_penalty + w3 * norm_deg_penalty)
# 状态更新
self.current_soc = next_soc
self.current_time += self.dt
# 模拟路况扰动 (后续可替换为真实的工况文件)
self.current_v += random.uniform(-2.0, 2.0)
self.current_v = max(0.0, min(self.current_v, 33.0))
done = self.current_time >= self.max_time
next_state = np.array([self.current_v, self.current_soc])
return next_state, float(reward), done
# ==========================================
# 4. 训练主循环
# ==========================================
if __name__ == "__main__":
env = SimulinkEnv(model_name='DQN_train')
state_dim = 2
action_dim = len(env.action_space)
agent = DQNAgent(state_dim, action_dim)
MAX_EPISODES = 50
for episode in range(MAX_EPISODES):
state = env.reset()
episode_reward = 0
while True:
action = agent.choose_action(state)
next_state, reward, done = env.step(action)
agent.store_transition(state, action, reward, next_state, done)
agent.learn()
state = next_state
episode_reward += reward
if done:
print(f"Episode: {episode+1:03d} | 总奖励(Total Reward): {episode_reward:7.2f} | 结束SOC: {state[1]:.4f} | 探索率: {agent.epsilon:.3f}")
break
env.eng.quit()
print("\n训练完成,MATLAB 引擎已安全关闭!")
bash
(hev) PS D:\software\opera_file\file\file\homework\train_project\python\hev_test01> & "D:/Program Files/anaconda/envs/hev/python.exe" d:/software/opera_file/file/file/homework/train_project/python/hev_test01/DQN_train.py
启动 MATLAB 后台引擎中,请稍候...
加载 Simulink 模型...
Episode: 001 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.831
Episode: 002 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.503
Episode: 003 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.305
Episode: 004 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.185
Episode: 005 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.112
Episode: 006 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.068
Episode: 007 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 008 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 009 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 010 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 011 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 012 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 013 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 014 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 015 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 016 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 017 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 018 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 019 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 020 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 021 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 022 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 023 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 024 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 025 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 026 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 027 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 028 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 029 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 030 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 031 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 032 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 033 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 034 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 035 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 036 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 037 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 038 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 039 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 040 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 041 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 042 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 043 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 044 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 045 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 046 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 047 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 048 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 049 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
Episode: 050 | 总奖励(Total Reward): 0.00 | 结束SOC: 0.6000 | 探索率: 0.050
训练完成,MATLAB 引擎已安全关闭!