深入理解深度Q网络DQN:基于python从零实现
DQN是什么玩意儿?
深度Q网络(DQN)是深度强化学习领域里一个超厉害的算法。它把Q学习和深度神经网络巧妙地结合在了一起,专门用来搞定那些状态空间维度特别高、特别复杂的难题。它展示了用函数近似来学习价值函数的超能力,因为传统的表格方法在面对状态空间特别大或者连续不断的状态空间时,就会因为太复杂而搞不定。
为啥要用深度学习来搞Q学习呢?
表格Q学习会把每个状态-动作对都对应一个估计的Q值,存到一个大表格里。这种方法碰到几个超级麻烦的问题:
- 维度灾难:状态变量的数量或者每个状态变量可能的值一多,Q表格的大小就会像滚雪球一样指数级增长,很快就会变得在计算上根本搞不定,存都存不下,更别提更新了(比如用像素表示的游戏状态、机器人的传感器读数)。
- 连续状态:表格方法根本没法直接处理连续的状态变量,还得先离散化,这不仅会丢失信息,而且离散化之后还是会碰到维度问题。
深度神经网络就像一个超级英雄,它能轻松解决这些问题,因为它是个函数近似器。它不用给每个特定的状态-动作对都存一个值,而是通过一个神经网络 (Q(s, a; \theta)) 来学习从状态(可能还有动作)到Q值的映射,这个映射是由网络的权重 (\theta) 参数化的。这就让网络能够在相似的状态之间泛化知识,特别适合处理大状态空间或者连续状态空间。通常情况下,网络会把状态 (s) 作为输入,然后输出这个状态下所有可能的离散动作对应的估计Q值。
DQN都在哪儿用,怎么用呢?
DQN是一个超级厉害的突破,它让强化学习能够解决以前根本搞不定的问题:
- 玩游戏:它最出名的就是用原始像素数据作为输入,在Atari 2600游戏里玩出了超越人类的水平。
- 机器人控制:从传感器数据里学习控制策略(虽然很多时候会针对连续动作进行调整)。
- 优化问题:在资源分配或者系统控制这种状态很复杂的地方也能用。
- 序列决策:在推荐系统或者对话管理这种状态维度很高的领域里也能大显身手。
DQN最适合解决以下这类问题:
- 状态空间很大、维度很高或者连续不断。
- 动作空间是离散的。
- 没有环境动态的模型(无模型)。
- 可以离线学习(在学习最优策略的时候,可以按照一个不同的探索策略来行动)。
DQN的数学基础
回忆一下Q学习
DQN的基础就是Q学习的更新规则,它旨在逐步改进对动作价值函数 (Q(s, a)) 的估计——从状态 (s) 开始,采取动作 (a),然后按照最优策略继续行动,预期能得到的总折扣未来回报。(Q^*(s, a)) 的贝尔曼最优性方程是:
Q ∗ ( s , a ) = E [ R t + 1 + γ max a ′ Q ∗ ( S t + 1 , a ′ ) ∣ S t = s , A t = a ] Q^*(s, a) = \mathbb{E}[R_{t+1} + \gamma \max_{a'} Q^*(S_{t+1}, a') | S_t=s, A_t=a] Q∗(s,a)=E[Rt+1+γa′maxQ∗(St+1,a′)∣St=s,At=a]
表格Q学习的更新规则是这样近似这个方程的:
Q ( s t , a t ) ← Q ( s t , a t ) + α [ r t + γ max a ′ Q ( s t + 1 , a ′ ) ⏟ TD目标 − Q ( s t , a t ) ] Q(s_t, a_t) \leftarrow Q(s_t, a_t) + \alpha \left[ \underbrace{r_t + \gamma \max_{a'} Q(s_{t+1}, a')}_{\text{TD目标}} - Q(s_t, a_t) \right] Q(st,at)←Q(st,at)+α TD目标 rt+γa′maxQ(st+1,a′)−Q(st,at)
函数近似
DQN把Q表格换成了一个神经网络 (Q(s, a; \theta))。这个网络通常把状态 (s) 作为输入,然后输出一个向量,包含所有动作对应的Q值,(Q(s, \cdot; \theta))。
DQN的损失函数
训练Q网络就是要最小化预测的Q值 (Q(s_t, a_t; \theta)) 和从贝尔曼方程里得到的目标值之间的差距。一个很直接的目标值就是 (r_t + \gamma \max_{a’} Q(s_{t+1}, a’; \theta))。但是,用同一个变化超快的网络来既做预测又计算目标值,会导致不稳定。DQN引入了两个超重要的技巧:
-
经验回放:把代理(agent)经历的转移 ((s_t, a_t, r_t, s_{t+1}, \text{done}_t)) 都存到一个回放缓存 (\mathcal{D}) 里。训练的时候,就从 (\mathcal{D}) 里随机抽样一小批转移。这样就能打破连续样本之间的相关性,让训练过程更加稳定、数据效率更高,有点像监督学习的感觉。
-
目标网络:用一个单独的网络 (Q(s, a; \theta^-)) 来计算目标Q值,这个网络的权重 (\theta^-) 是固定的,只会在一定时间间隔后更新(比如,每 (C) 步更新一次),把主Q网络的权重 (\theta) 复制过来 ((\theta^- \leftarrow \theta))。这就为主网络提供了一个更稳定的、可以学习的目标。
从回放缓存里抽样出来的转移 ((s_t, a_t, r_t, s_{t+1}, d_t)) 的目标值 (Y_t) 是:
Y t = { r t 如果 d t 是真(终止状态) r t + γ max a ′ Q ( s t + 1 , a ′ ; θ − ) 如果 d t 是假(非终止状态) Y_t = \begin{cases} r_t & \text{如果 } d_t \text{ 是真(终止状态)} \\ r_t + \gamma \max_{a'} Q(s_{t+1}, a'; \theta^-) & \text{如果 } d_t \text{ 是假(非终止状态)} \end{cases} Yt={rtrt+γmaxa′Q(st+1,a′;θ−)如果 dt 是真(终止状态)如果 dt 是假(非终止状态)
损失函数通常是均方误差(MSE)或者Huber损失(Smooth L1),在小批量样本上用梯度下降法来最小化这个损失函数:
L ( θ ) = E ( s , a , r , s ′ , d ) ∼ D [ ( Y j − Q ( s j , a j ; θ ) ) 2 ] L(\theta) = \mathbb{E}_{(s, a, r, s', d) \sim \mathcal{D}} \left[ (Y_j - Q(s_j, a_j; \theta))^2 \right] L(θ)=E(s,a,r,s′,d)∼D[(Yj−Q(sj,aj;θ))2]
DQN的逐步解释
- 初始化:回放缓存 (\mathcal{D})(容量 (N)),主Q网络 (Q(s, a; \theta))(随机权重 (\theta)),目标网络 (Q(s, a; \theta-))((\theta- = \theta)),探索参数 (\epsilon)。
- 对于每个剧集:
a. 重置环境,获取初始状态 (s_1)。如果需要的话,对状态进行预处理。
b. 对于每一步 (t):
i. 使用基于 (\epsilon)-贪婪的策略,根据 (Q(s_t, \cdot; \theta)) 来选择动作 (a_t)。
ii. 执行 (a_t),观察奖励 (r_t),下一个状态 (s_{t+1}),完成标志 (d_t)。对 (s_{t+1}) 进行预处理。
iii. 把 ((s_t, a_t, r_t, s_{t+1}, d_t)) 存到 (\mathcal{D}) 里。
iv. 抽样小批量:从 (\mathcal{D}) 里随机抽样。
v. 计算目标 (Y_j) 使用目标网络 (Q(s, a; \theta^-))。
vi. 训练主网络:对 (L(\theta) = (Y_j - Q(s_j, a_j; \theta))^2) 进行梯度下降步骤。
vii. 更新目标网络:每 (C) 步,设置 (\theta^- \leftarrow \theta)。
viii. (s_t \leftarrow s_{t+1})。
ix. 如果 (d_t),结束剧集。 - 重复:直到收敛或者达到最大剧集数。
DQN的关键组成部分
Q网络
- 核心函数近似器。学习把状态映射到动作价值。
- 架构取决于状态的表示方式(对于向量用多层感知机MLP,对于图像用卷积神经网络CNN)。
- 使用非线性激活函数(比如ReLU)。
- 输出层通常有和离散动作数量一样多的单元,输出原始的Q值(最后没有激活函数)。
经验回放
- 存储代理的经历。
- 打破相关性,允许重复使用数据,提高稳定性和样本效率。
- 使用数据结构比如
deque
来实现。
目标网络
- 主Q网络的一个副本,更新频率更低。
- 在计算TD误差的时候提供稳定的、可以学习的目标,防止“移动目标”问题。
- 对于稳定DQN训练来说至关重要。
探索与利用
- 通常使用 (\epsilon)-贪婪:以概率 (\epsilon) 随机行动,以概率 (1-\epsilon) 贪婪地(根据Q网络)行动。
- (\epsilon) 指数衰减:(\epsilon) 随着时间逐渐减小(比如线性或者指数衰减),从一个较高的初始值逐渐减小到一个较低的最终值。
损失函数(MSE/Huber)
- 测量网络预测和TD目标之间的差异。
- Huber损失(在PyTorch里叫Smooth L1损失)通常比MSE更受欢迎,因为它对异常值不那么敏感,而异常值在训练早期可能会因为大的TD误差而出现。
超参数
- DQN的性能对超参数特别敏感,比如学习率、缓冲区大小、批量大小、目标更新频率、折扣因子和 (\epsilon) 衰减计划。通常需要仔细调整。
实际例子:自定义网格世界
因为Gymnasium被禁止使用了,我们就自己创建一个简单的自定义网格世界环境。
环境描述:
- 网格大小:10x10。
- 状态:代理的
(row, col)
位置。为了网络输入,表示为归一化的向量[row/10, col/10]
。 - 动作:4个离散动作:0(上),1(下),2(左),3(右)。
- 起始状态:(0, 0)。
- 目标状态:(9, 9)。
- 奖励:
- 到达目标状态(9, 9)得 +10 分。
- 撞墙(试图移出网格)扣 -1 分。
- 其他步骤扣 -0.1 分(小成本鼓励效率)。
- 终止:当代理到达目标或者达到最大步数时,剧集结束。
设置环境
我们得先导入必要的库,设置好环境,所以咱们现在就动手吧。
# 导入用于数值计算、绘图和实用功能的必要库
import numpy as np
import matplotlib.pyplot as plt
import random
import math
from collections import namedtuple, deque
from itertools import count
from typing import List, Tuple, Dict, Optional# 导入 PyTorch 用于构建和训练神经网络
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F# 设置设备,如果可用就用 GPU,否则就用 CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备:{device}")# 设置随机种子,以便在每次运行时都能得到可重现的结果
seed = 42
random.seed(seed) # Python 随机模块的种子
np.random.seed(seed) # NumPy 的种子
torch.manual_seed(seed) # PyTorch(CPU)的种子
if torch.cuda.is_available():torch.cuda.manual_seed_all(seed) # PyTorch(GPU)的种子# 为 Jupyter Notebook 启用内联绘图
%matplotlib inline
使用设备:cpu
创建自定义环境
第一步就是创建一个自定义的环境,模拟一个简单的网格世界。这个环境有一个 10x10 的网格,代理可以在四个方向上移动:上、下、左、右。代理从左上角(0, 0)开始,目标是到达右下角(9, 9)。根据代理的动作和到达的状态,会给予相应的奖励。奖励定义如下:
- 到达目标(右下角)得 +10 分
- 撞墙(超出边界)扣 -1 分
- 每走一步扣 -0.1 分(鼓励走更短的路径)
# 自定义网格世界环境
class GridEnvironment:"""一个简单的 10x10 网格世界环境。状态:(row, col) 表示为归一化的向量 [row/10, col/10]。动作:0(上),1(下),2(左),3(右)。奖励:到达目标得 +10 分,撞墙扣 -1 分,每走一步扣 -0.1 分。"""def __init__(self, rows: int = 10, cols: int = 10) -> None:"""初始化网格世界环境。参数:- rows (int): 网格的行数。- cols (int): 网格的列数。"""self.rows: int = rowsself.cols: int = colsself.start_state: Tuple[int, int] = (0, 0) # 起始位置self.goal_state: Tuple[int, int] = (rows - 1, cols - 1) # 目标位置self.state: Tuple[int, int] = self.start_state # 当前状态self.state_dim: int = 2 # 状态由 2 个坐标(row, col)表示self.action_dim: int = 4 # 4 个离散动作:上、下、左、右# 动作映射:将动作索引映射到 (row_delta, col_delta)self.action_map: Dict[int, Tuple[int, int]] = {0: (-1, 0), # 上1: (1, 0), # 下2: (0, -1), # 左3: (0, 1) # 右}def reset(self) -> torch.Tensor:"""重置环境到起始状态。返回:torch.Tensor:起始状态作为归一化的张量。"""self.state = self.start_statereturn self._get_state_tensor(self.state)def _get_state_tensor(self, state_tuple: Tuple[int, int]) -> torch.Tensor:"""将 (row, col) 元组转换为归一化的张量,供网络使用。参数:- state_tuple (Tuple[int, int]): 状态表示为元组 (row, col)。返回:torch.Tensor:归一化的状态作为张量。"""# 将坐标归一化到 0 和 1 之间normalized_state: List[float] = [state_tuple[0] / (self.rows - 1),state_tuple[1] / (self.cols - 1)]return torch.tensor(normalized_state, dtype=torch.float32, device=device)def step(self, action: int) -> Tuple[torch.Tensor, float, bool]:"""根据给定的动作在环境中执行一步。参数:action (int): 要执行的动作(0:上,1:下,2:左,3:右)。返回:Tuple[torch.Tensor, float, bool]:- next_state_tensor (torch.Tensor): 下一个状态作为归一化的张量。- reward (float): 动作的奖励。- done (bool): 剧集是否结束。"""# 如果已经到达目标状态,就返回当前状态if self.state == self.goal_state:return self._get_state_tensor(self.state), 0.0, True# 获取动作对应的行和列的变化量dr, dc = self.action_map[action]current_row, current_col = self.statenext_row, next_col = current_row + dr, current_col + dc# 默认的步进成本reward: float = -0.1hit_wall: bool = False# 检查动作是否会导致撞墙(超出边界)if not (0 <= next_row < self.rows and 0 <= next_col < self.cols):# 保持在相同的状态,并受到惩罚next_row, next_col = current_row, current_colreward = -1.0hit_wall = True# 更新状态self.state = (next_row, next_col)next_state_tensor: torch.Tensor = self._get_state_tensor(self.state)# 检查是否到达目标状态done: bool = (self.state == self.goal_state)if done:reward = 10.0 # 到达目标的奖励return next_state_tensor, reward, donedef get_action_space_size(self) -> int:"""返回动作空间的大小。返回:int:可能的动作数量(4)。"""return self.action_dimdef get_state_dimension(self) -> int:"""返回状态表示的维度。返回:int:状态的维度(2)。"""return self.state_dim
现在我们已经实现了自定义的网格环境,接下来我们实例化它并验证它的属性和功能。
# 用 10x10 的网格实例化自定义网格环境
custom_env = GridEnvironment(rows=10, cols=10)# 获取动作空间的大小和状态维度
n_actions_custom = custom_env.get_action_space_size()
n_observations_custom = custom_env.get_state_dimension()# 打印环境的基本信息
print(f"自定义网格环境:")
print(f"大小:{custom_env.rows}x{custom_env.cols}") # 网格大小
print(f"状态维度:{n_observations_custom}") # 状态维度(2 表示行和列)
print(f"动作维度:{n_actions_custom}") # 可能的动作数量(4)
print(f"起始状态:{custom_env.start_state}") # 起始位置
print(f"目标状态:{custom_env.goal_state}") # 目标位置# 重置环境并打印起始状态的归一化状态张量
print(f"示例状态张量 (0,0):{custom_env.reset()}")# 执行一个示例动作:向右移动(动作=3)并打印结果
next_s, r, d = custom_env.step(3) # 动作 3 对应向右移动
print(f"动作结果 (向右):next_state={next_s.cpu().numpy()}, reward={r}, done={d}")# 再执行一个示例动作:向上移动(动作=0)并打印结果
# 由于代理已经在最上面一行,所以这个动作会导致撞墙
next_s, r, d = custom_env.step(0) # 动作 0 对应向上移动
print(f"动作结果 (向上):next_state={next_s.cpu().numpy()}, reward={r}, done={d}")
自定义网格环境:
大小:10x10
状态维度:2
动作维度:4
起始状态:(0, 0)
目标状态:(9, 9)
示例状态张量 (0,0):tensor([0., 0.])
动作结果 (向右):next_state=[0. 0.11111111], reward=-0.1, done=False
动作结果 (向上):next_state=[0. 0.11111111], reward=-1.0, done=False
你可以看到,代理在最上面一行,向上移动会导致撞墙,受到 -1.0 的惩罚。下一个状态保持不变。
实现DQN算法
现在,咱们来实现核心部分:Q网络、回放缓存、动作选择策略、优化步骤以及目标网络更新机制。
定义Q网络
我们用 PyTorch 的 nn.Module
定义一个简单的多层感知机(MLP)。
# 定义 Q 网络架构
class DQN(nn.Module):"""简单的多层感知机 Q 网络"""def __init__(self, n_observations: int, n_actions: int):"""初始化 Q 网络。参数:- n_observations (int):状态空间的维度。- n_actions (int):可能的动作数量。"""super(DQN, self).__init__()# 定义网络层# 简单的 MLP:输入 -> 隐藏层1 -> ReLU -> 隐藏层2 -> ReLU -> 输出self.layer1 = nn.Linear(n_observations, 128) # 输入层self.layer2 = nn.Linear(128, 128) # 隐藏层self.layer3 = nn.Linear(128, n_actions) # 输出层(每个动作的 Q 值)def forward(self, x: torch.Tensor) -> torch.Tensor:"""网络的前向传播。参数:- x (torch.Tensor):表示状态(或状态批次)的输入张量。返回:- torch.Tensor:表示每个动作 Q 值的输出张量。"""# 确保输入是浮点张量if not isinstance(x, torch.Tensor):x = torch.tensor(x, dtype=torch.float32, device=device)elif x.dtype != torch.float32:x = x.to(dtype=torch.float32)# 应用各层并使用 ReLU 激活函数x = F.relu(self.layer1(x))x = F.relu(self.layer2(x))return self.layer3(x) # 输出层没有激活函数(原始 Q 值)
定义回放缓存
我们用 collections.deque
实现高效的存储和 random.sample
进行批量抽样。一个 namedtuple
有助于组织转移数据。
# 定义存储转移的结构
Transition = namedtuple('Transition',('state', 'action', 'next_state', 'reward', 'done'))# 定义回放缓存类
class ReplayMemory(object):"""存储转移并允许抽样批次"""def __init__(self, capacity: int):"""初始化回放缓存。参数:- capacity (int):最多存储的转移数量。"""self.memory = deque([], maxlen=capacity)def push(self, *args):"""保存一个转移。参数:- *args:转移的各个元素(状态、动作、下一个状态、奖励、完成标志)。"""self.memory.append(Transition(*args))def sample(self, batch_size: int) -> List[Transition]:"""从内存中随机抽样一批转移。参数:- batch_size (int):要抽样的转移数量。返回:- List[Transition]:包含抽样转移的列表。"""return random.sample(self.memory, batch_size)def __len__(self) -> int:"""返回当前内存的大小"""return len(self.memory)
动作选择((\epsilon)-贪婪)
这个函数根据当前状态和 Q 网络,使用 (\epsilon)-贪婪策略来选择动作,用于探索。
# 动作选择(\(\epsilon\)-贪婪 - 修改为单状态张量输入)
def select_action_custom(state: torch.Tensor,policy_net: nn.Module,epsilon_start: float,epsilon_end: float,epsilon_decay: int,n_actions: int) -> Tuple[torch.Tensor, float]:"""使用 \(\epsilon\)-贪婪策略为单个状态张量选择动作。参数:- state (torch.Tensor):当前状态作为张量,形状为 [state_dim]。- policy_net (nn.Module):用于估计 Q 值的 Q 网络。- epsilon_start (float):初始探索率 \(\epsilon\)。- epsilon_end (float):衰减后 \(\epsilon\) 的最终值。- epsilon_decay (int):\(\epsilon\) 的衰减率(值越大衰减越慢)。- n_actions (int):可能的动作数量。返回:- Tuple[torch.Tensor, float]:- 选中的动作作为张量,形状为 [1, 1]。- 衰减后的当前 \(\epsilon\) 值。"""global steps_done_custom # 用于跟踪已执行的步数的全局计数器sample = random.random() # 生成一个随机数,用于 \(\epsilon\)-贪婪决策# 根据衰减公式计算当前的 \(\epsilon\) 值epsilon_threshold = epsilon_end + (epsilon_start - epsilon_end) * \math.exp(-1. * steps_done_custom / epsilon_decay)steps_done_custom += 1 # 增加步数计数器if sample > epsilon_threshold:# 利用:选择 Q 值最高的动作with torch.no_grad():# 为状态张量添加一个批次维度,使其变为 [1, state_dim]state_batch = state.unsqueeze(0)# 选择 Q 值最高的动作(输出形状:[1, n_actions])action = policy_net(state_batch).max(1)[1].view(1, 1) # 重塑为 [1, 1]else:# 探索:选择一个随机动作action = torch.tensor([[random.randrange(n_actions)]], device=device, dtype=torch.long)return action, epsilon_threshold
优化步骤和选择动作
优化步骤包括从回放缓存中抽样一个小批量,使用目标网络计算目标 Q 值,并通过反向传播更新主 Q 网络。动作选择使用 (\epsilon)-贪婪策略。
def select_action_custom(state: torch.Tensor,policy_net: nn.Module,epsilon_start: float,epsilon_end: float,epsilon_decay: int,n_actions: int
) -> Tuple[torch.Tensor, float]:"""使用 \(\epsilon\)-贪婪策略为单个状态张量选择动作。参数:- state (torch.Tensor):当前状态作为张量,形状为 [state_dim]。- policy_net (nn.Module):用于估计 Q 值的 Q 网络。- epsilon_start (float):初始探索率 \(\epsilon\)。- epsilon_end (float):衰减后 \(\epsilon\) 的最终值。- epsilon_decay (int):\(\epsilon\) 的衰减率(值越大衰减越慢)。- n_actions (int):可能的动作数量。返回:- Tuple[torch.Tensor, float]:- 选中的动作作为张量,形状为 [1, 1]。- 衰减后的当前 \(\epsilon\) 值。"""global steps_done_custom # 用于跟踪已执行的步数的全局计数器# 生成一个随机数,用于 \(\epsilon\)-贪婪决策sample: float = random.random()# 根据衰减公式计算当前的 \(\epsilon\) 值epsilon_threshold: float = epsilon_end + (epsilon_start - epsilon_end) * \math.exp(-1.0 * steps_done_custom / epsilon_decay)# 增加步数计数器steps_done_custom += 1if sample > epsilon_threshold:# 利用:选择 Q 值最高的动作with torch.no_grad():# 为状态张量添加一个批次维度,使其变为 [1, state_dim]state_batch: torch.Tensor = state.unsqueeze(0)# 选择 Q 值最高的动作(输出形状:[1, n_actions])action: torch.Tensor = policy_net(state_batch).max(1)[1].view(1, 1) # 重塑为 [1, 1]else:# 探索:选择一个随机动作action = torch.tensor([[random.randrange(n_actions)]], device=device, dtype=torch.long)return action, epsilon_threshold
接下来我们继续实现优化步骤,这是DQN算法的核心部分,它通过从回放缓存中抽样一个小批量数据,计算目标Q值,并通过反向传播更新主Q网络。
def optimize_model(memory: ReplayMemory,policy_net: nn.Module,target_net: nn.Module,optimizer: optim.Optimizer,batch_size: int,gamma: float,criterion: nn.Module = nn.SmoothL1Loss()) -> Optional[float]:"""对策略网络执行一步优化。参数:- memory (ReplayMemory):存储过去转移的回放缓存。- policy_net (nn.Module):正在优化的主Q网络。- target_net (nn.Module):用于稳定目标计算的目标Q网络。- optimizer (optim.Optimizer):用于更新策略网络的优化器。- batch_size (int):每次优化步骤要抽样的转移数量。- gamma (float):未来奖励的折扣因子。- criterion (nn.Module):使用的损失函数(默认:SmoothL1损失)。返回:- Optional[float]:优化步骤的损失值,如果没有足够的样本则返回None。"""# 确保回放缓存中有足够的样本以执行优化if len(memory) < batch_size:return None# 从回放缓存中抽样一批转移transitions = memory.sample(batch_size)batch = Transition(*zip(*transitions)) # 将转移解包为单独的组件# 标记非终止状态(不是终端状态的状态)non_final_mask = torch.tensor(tuple(map(lambda s: s is not None, batch.next_state)),device=device, dtype=torch.bool)# 将非终止的下一个状态堆叠成张量if any(non_final_mask): # 检查是否有任何非终止状态non_final_next_states = torch.stack([s for s in batch.next_state if s is not None])# 将当前状态、动作、奖励和完成标志堆叠成张量state_batch = torch.stack(batch.state)action_batch = torch.cat(batch.action)reward_batch = torch.cat(batch.reward)done_batch = torch.cat(batch.done)# 计算所采取动作的Q(s_t, a)state_action_values = policy_net(state_batch).gather(1, action_batch)# 使用目标网络计算下一个状态的V(s_{t+1})next_state_values = torch.zeros(batch_size, device=device)with torch.no_grad():if any(non_final_mask): # 只计算非终止状态next_state_values[non_final_mask] = target_net(non_final_next_states).max(1)[0]# 使用贝尔曼方程计算预期的Q值expected_state_action_values = (next_state_values * gamma) + reward_batch# 计算预测和预期Q值之间的损失loss = criterion(state_action_values, expected_state_action_values.unsqueeze(1))# 执行反向传播和优化optimizer.zero_grad() # 清除之前的梯度loss.backward() # 计算梯度torch.nn.utils.clip_grad_value_(policy_net.parameters(), 100) # 限制梯度以防止梯度爆炸optimizer.step() # 更新策略网络return loss.item() # 返回损失值以便记录
目标网络更新
这个函数将主策略网络的权重复制到目标网络。我们将使用“硬”更新,每 TARGET_UPDATE
步更新一次。
def update_target_net(policy_net: nn.Module, target_net: nn.Module) -> None:"""将策略网络的权重复制到目标网络。参数:- policy_net (nn.Module):主Q网络,其权重将被复制。- target_net (nn.Module):目标Q网络,将复制权重。返回:- None"""target_net.load_state_dict(policy_net.state_dict())
运行DQN算法
设置超参数,初始化网络、优化器和回放缓存,然后运行主训练循环。
超参数设置
我们需要为自定义网格世界设置环境和超参数。这些参数将定义学习率、折扣因子、批量大小以及其他重要的DQN算法设置。
# 自定义网格世界的超参数
BATCH_SIZE_CUSTOM = 128
GAMMA_CUSTOM = 0.99 # 折扣因子(鼓励向前看)
EPS_START_CUSTOM = 1.0 # 从完全探索开始
EPS_END_CUSTOM = 0.05 # 探索率最终值为 5%
EPS_DECAY_CUSTOM = 10000 # 较慢的衰减,以满足可能较大的状态空间探索需求
TAU_CUSTOM = 0.005 # 用于软更新的 Tau(备用,这里不使用)
LR_CUSTOM = 5e-4 # 学习率(可能需要调整)
MEMORY_CAPACITY_CUSTOM = 10000
TARGET_UPDATE_FREQ_CUSTOM = 20 # 目标网络更新频率
NUM_EPISODES_CUSTOM = 500 # 剧集数量
MAX_STEPS_PER_EPISODE_CUSTOM = 200 # 每个剧集的最大步数(与网格大小相关)
初始化
定义了环境和DQN之后,我们可以初始化策略和目标网络、优化器和回放缓存。
# 重新实例化自定义 GridEnvironment
custom_env: GridEnvironment = GridEnvironment(rows=10, cols=10)# 获取动作空间的大小和状态维度
n_actions_custom: int = custom_env.get_action_space_size() # 可能的动作数量(4)
n_observations_custom: int = custom_env.get_state_dimension() # 状态空间的维度(2)# 初始化策略网络(主Q网络)和目标网络
policy_net_custom: DQN = DQN(n_observations_custom, n_actions_custom).to(device) # 主Q网络
target_net_custom: DQN = DQN(n_observations_custom, n_actions_custom).to(device) # 目标Q网络# 将策略网络的权重复制到目标网络,并将其设置为评估模式
target_net_custom.load_state_dict(policy_net_custom.state_dict()) # 同步权重
target_net_custom.eval() # 将目标网络设置为评估模式# 初始化策略网络的优化器
optimizer_custom: optim.AdamW = optim.AdamW(policy_net_custom.parameters(), lr=LR_CUSTOM, amsgrad=True)# 初始化回放缓存,指定容量
memory_custom: ReplayMemory = ReplayMemory(MEMORY_CAPACITY_CUSTOM)# 用于绘图的列表
episode_rewards_custom = []
episode_lengths_custom = []
episode_epsilons_custom = []
episode_losses_custom = []
训练循环
现在我们已经编写好了所有代码,接下来让我们在自定义网格世界环境中训练DQN代理。
print("开始在自定义网格世界中训练DQN...")# 初始化全局计数器,用于 \(\epsilon\) 衰减
steps_done_custom = 0# 训练循环
for i_episode in range(NUM_EPISODES_CUSTOM):# 重置环境并获取初始状态张量state = custom_env.reset()total_reward = 0current_losses = []for t in range(MAX_STEPS_PER_EPISODE_CUSTOM):# 使用 \(\epsilon\)-贪婪策略选择动作action_tensor, current_epsilon = select_action_custom(state, policy_net_custom, EPS_START_CUSTOM, EPS_END_CUSTOM, EPS_DECAY_CUSTOM, n_actions_custom)action = action_tensor.item()# 在环境中执行动作next_state_tensor, reward, done = custom_env.step(action)total_reward += reward# 为存储在回放缓存中准备张量reward_tensor = torch.tensor([reward], device=device, dtype=torch.float32)action_tensor_mem = torch.tensor([[action]], device=device, dtype=torch.long)done_tensor = torch.tensor([done], device=device, dtype=torch.bool)# 将转移存储在回放缓存中memory_next_state = next_state_tensor if not done else Nonememory_custom.push(state, action_tensor_mem, memory_next_state, reward_tensor, done_tensor)# 转移到下一个状态state = next_state_tensor# 对策略网络执行一步优化loss = optimize_model(memory_custom, policy_net_custom, target_net_custom, optimizer_custom, BATCH_SIZE_CUSTOM, GAMMA_CUSTOM)if loss is not None:current_losses.append(loss)# 如果剧集结束,则跳出循环if done:break# 存储剧集统计信息episode_rewards_custom.append(total_reward)episode_lengths_custom.append(t + 1)episode_epsilons_custom.append(current_epsilon)episode_losses_custom.append(np.mean(current_losses) if current_losses else 0)# 定期更新目标网络if i_episode % TARGET_UPDATE_FREQ_CUSTOM == 0:update_target_net(policy_net_custom, target_net_custom)# 每 50 个剧集打印一次进度if (i_episode + 1) % 50 == 0:avg_reward = np.mean(episode_rewards_custom[-50:])avg_length = np.mean(episode_lengths_custom[-50:])avg_loss = np.mean([l for l in episode_losses_custom[-50:] if l > 0])print(f"剧集 {i_episode+1}/{NUM_EPISODES_CUSTOM} | "f"最近 50 个剧集的平均奖励:{avg_reward:.2f} | "f"平均长度:{avg_length:.2f} | "f"平均损失:{avg_loss:.4f} | "f"\(\epsilon\):{current_epsilon:.3f}")print("自定义网格世界训练完成。")
开始在自定义网格世界中训练DQN...
剧集 50/500 | 最近 50 个剧集的平均奖励:-13.14 | 平均长度:109.86 | 平均损失:0.0330 | \(\epsilon\):0.599
剧集 100/500 | 最近 50 个剧集的平均奖励:0.68 | 平均长度:60.18 | 平均损失:0.0290 | \(\epsilon\):0.456
剧集 150/500 | 最近 50 个剧集的平均奖励:0.20 | 平均长度:62.06 | 平均损失:0.0240 | \(\epsilon\):0.348
剧集 200/500 | 最近 50 个剧集的平均奖励:4.34 | 平均长度:40.32 | 平均损失:0.0115 | \(\epsilon\):0.293
剧集 250/500 | 最近 50 个剧集的平均奖励:5.62 | 平均长度:32.48 | 平均损失:0.0110 | \(\epsilon\):0.257
剧集 300/500 | 最近 50 个剧集的平均奖励:5.81 | 平均长度:30.50 | 平均损失:0.0068 | \(\epsilon\):0.228
剧集 350/500 | 最近 50 个剧集的平均奖励:6.96 | 平均长度:25.32 | 平均损失:0.0144 | \(\epsilon\):0.206
剧集 400/500 | 最近 50 个剧集的平均奖励:6.56 | 平均长度:25.90 | 平均损失:0.0134 | \(\epsilon\):0.187
剧集 450/500 | 最近 50 个剧集的平均奖励:6.57 | 平均长度:29.74 | 平均损失:0.0024 | \(\epsilon\):0.168
剧集 500/500 | 最近 50 个剧集的平均奖励:7.58 | 平均长度:20.84 | 平均损失:0.0010 | \(\epsilon\):0.157
自定义网格世界训练完成。
可视化学习过程
为自定义网格世界环境绘制结果图表。
# 为自定义网格世界绘制结果
plt.figure(figsize=(20, 3))# 奖励
plt.subplot(1, 3, 1)
plt.plot(episode_rewards_custom)
plt.title('DQN 自定义网格:剧集奖励')
plt.xlabel('剧集')
plt.ylabel('总奖励')
plt.grid(True)
rewards_ma_custom = np.convolve(episode_rewards_custom, np.ones(50)/50, mode='valid')
if len(rewards_ma_custom) > 0: # 避免绘制空的移动平均值plt.plot(np.arange(len(rewards_ma_custom)) + 49, rewards_ma_custom, label='50-剧集移动平均值', color='orange')
plt.legend()# 长度
plt.subplot(1, 3, 2)
plt.plot(episode_lengths_custom)
plt.title('DQN 自定义网格:剧集长度')
plt.xlabel('剧集')
plt.ylabel('步数')
plt.grid(True)
lengths_ma_custom = np.convolve(episode_lengths_custom, np.ones(50)/50, mode='valid')
if len(lengths_ma_custom) > 0:plt.plot(np.arange(len(lengths_ma_custom)) + 49, lengths_ma_custom, label='50-剧集移动平均值', color='orange')
plt.legend()# \(\epsilon\)
plt.subplot(1, 3, 3)
plt.plot(episode_epsilons_custom)
plt.title('DQN 自定义网格:\(\epsilon\) 衰减')
plt.xlabel('剧集')
plt.ylabel('\(\epsilon\)')
plt.grid(True)plt.tight_layout()
plt.show()
接下来我们继续分析DQN学习曲线,并且可视化学习到的策略。
分析DQN学习曲线(自定义网格世界)
- 奖励曲线:奖励应该随着时间的推移而增加,可能比CartPole更波动,因为有步进成本,而且可能会暂时被困住。移动平均值应该显示出向正值学习的趋势,因为代理更频繁地到达目标(+10),同时尽量减少步数(-0.1成本)和撞墙(-1)。
- 剧集长度曲线:最初很高,随着代理学会更直接地到达目标,剧集长度应该会减少。平台或尖峰可能表明探索导致路径变长或被困住。收敛到从起点到目标的最小可能步数表明学习良好。
- (\epsilon) 衰减:显示计划的探索率随剧集的减少。
这表明DQN在自定义、手动定义的环境中学习策略,只使用基本库以及PyTorch进行神经网络组件。
可视化学习到的策略(可选)
我们可以创建一个策略网格,类似于表格方法,但现在策略是从Q网络的输出中得出的。
def plot_dqn_policy_grid(policy_net: nn.Module, env: GridEnvironment, device: torch.device) -> None:"""绘制从DQN得出的贪婪策略。参数:- policy_net (nn.Module):用于得出策略的训练有素的Q网络。- env (GridEnvironment):自定义网格环境。- device (torch.device):用于处理张量的设备(CPU/GPU)。返回:- None:显示策略网格的图表。"""# 获取网格环境的维度rows: int = env.rowscols: int = env.cols# 初始化一个空网格来存储策略符号policy_grid: np.ndarray = np.empty((rows, cols), dtype=str)# 定义每个动作的符号action_symbols: Dict[int, str] = {0: '↑', 1: '↓', 2: '←', 3: '→'}# 创建图表fig, ax = plt.subplots(figsize=(cols * 0.6, rows * 0.6)) # 根据网格维度调整大小# 遍历网格中的每个单元格for r in range(rows):for c in range(cols):state_tuple: Tuple[int, int] = (r, c) # 当前状态作为元组# 如果当前单元格是目标状态,则标记为'G'if state_tuple == env.goal_state:policy_grid[r, c] = 'G'ax.text(c, r, 'G', ha='center', va='center', color='green', fontsize=12, weight='bold')else:# 将状态转换为张量表示state_tensor: torch.Tensor = env._get_state_tensor(state_tuple)# 使用策略网络确定最佳动作with torch.no_grad():# 为状态张量添加一个批次维度state_tensor = state_tensor.unsqueeze(0)# 获取当前状态的Q值q_values: torch.Tensor = policy_net(state_tensor)# 选择Q值最高的动作best_action: int = q_values.max(1)[1].item()# 在策略网格中存储动作符号policy_grid[r, c] = action_symbols[best_action]# 将动作符号添加到图表中ax.text(c, r, policy_grid[r, c], ha='center', va='center', color='black', fontsize=12)# 设置网格可视化ax.matshow(np.zeros((rows, cols)), cmap='Greys', alpha=0.1) # 背景网格ax.set_xticks(np.arange(-.5, cols, 1), minor=True)ax.set_yticks(np.arange(-.5, rows, 1), minor=True)ax.grid(which='minor', color='black', linestyle='-', linewidth=1) # 次要网格线ax.set_xticks([]) # 移除x轴刻度ax.set_yticks([]) # 移除y轴刻度ax.set_title("DQN 学习到的策略(自定义网格)") # 图表标题# 显示图表plt.show()# 绘制训练有素的网络学习到的策略
print("\n绘制从DQN得出的策略:")
plot_dqn_policy_grid(policy_net_custom, custom_env, device)
绘制从DQN得出的策略:
DQN中常见的挑战及解决方案
挑战:训练不稳定/发散
- 解决方案:
- 调整学习率:降低学习率 (
LR
)。 - 增加目标网络更新频率:减少目标网络的更新频率(增加
TARGET_UPDATE_FREQ
),或者使用软更新 (TAU
)。 - 梯度裁剪:防止梯度爆炸。
- 增大回放缓存:增加
MEMORY_CAPACITY
。 - 更换优化器/损失函数:尝试使用 RMSprop 或 Huber 损失。
- 调整学习率:降低学习率 (
挑战:学习速度慢
- 解决方案:
- 调整超参数:谨慎提高学习率,调整 (\epsilon) 衰减,优化批量大小。
- 调整网络架构:尝试不同的层数和神经元数量。
- 使用优先经验回放:更频繁地抽样回放缓存中“重要”的转移(更复杂的扩展)。
- 使用双DQN/双Q网络:这些扩展可以提高性能和稳定性。
挑战:Q值过高估计
- 解决方案:实现 双DQN,在目标计算中将动作选择和价值估计解耦。
挑战:回放缓存中的相关性
- 解决方案:确保回放缓存足够大,并且随机抽样。考虑使用优先回放。
总结
深度Q网络(DQN)算法成功地将Q学习扩展到能够处理高维状态空间的领域,通过使用深度神经网络作为函数近似器。经验回放和目标网络等关键创新对于在将深度学习与时间差分方法结合时稳定学习过程至关重要。
正如在CartPole环境中所展示的那样,DQN能够直接从经验中学习处理连续状态空间和离散动作的有效策略。尽管基本的DQN存在一些局限性(例如,过高估计、处理连续动作),但它为许多先进的深度强化学习算法奠定了基础,如双DQN、双Q网络、Rainbow以及演员-评论家方法,这些算法解决了这些挑战并能够处理更复杂的问题。理解DQN是掌握现代强化学习技术的重要一步。
相关文章:
深入理解深度Q网络DQN:基于python从零实现
DQN是什么玩意儿? 深度Q网络(DQN)是深度强化学习领域里一个超厉害的算法。它把Q学习和深度神经网络巧妙地结合在了一起,专门用来搞定那些状态空间维度特别高、特别复杂的难题。它展示了用函数近似来学习价值函数的超能力…...
使用lldb看看Rust的HashMap
目录 前言 正文 读取桶的状态 获取键值对 键值对的指针地址 此时,读取数据 读取索引4的键值对 多添加几个键值对 使用i32作为键,&str作为值 使用i32作为键,String作为值 前言 前面使用ldb看了看不同的类型,这篇再使用…...
Vue3简易版购物车的实现。
文章目录 一、话不多说,直接上代码? 一、话不多说,直接上代码? <template><div><input type"text" placeholder"请输入内容" v-model"keywords"><button click"addGood…...
比亚迪全栈自研生态的底层逻辑
比亚迪全栈自研生态的底层逻辑:汽车工程师必须理解的闭环技术革命 引言:当技术壁垒成为护城河 2023年比亚迪销量突破302万辆的震撼数据背后,隐藏着一个更值得工程师深思的事实:其全栈自研体系覆盖了新能源汽车83%的核心零部件。这…...
[Java实战]Spring Boot 快速配置 HTTPS 并实现 HTTP 自动跳转(八)
[Java实战]Spring Boot 快速配置 HTTPS 并实现 HTTP 自动跳转(八) 引言 在当今网络安全威胁日益严峻的背景下,为 Web 应用启用 HTTPS 已成为基本要求。Spring Boot 提供了简单高效的方式集成 HTTPS 支持,无论是开发环境测试还是生产环境部署࿰…...
5.1.1 WPF中Command使用介绍
WPF 的命令系统是一种强大的输入处理机制,它比传统的事件处理更加灵活和可重用,特别适合 MVVM (Model, View, ViewModel)模式开发。 一、命令系统核心概念 1.命令系统基本元素: 命令(Command): 即ICommand类,使用最多的是RoutedCommand,也可以自己继承ICommand使用自定…...
设计模式简述(十九)桥梁模式
桥梁模式 描述基本组件使用 描述 桥梁模式是一种相对简单的模式,通常以组合替代继承的方式实现。 从设计原则来讲,可以说是单一职责的一种体现。 将原本在一个类中的功能,按更细的粒度拆分到不同的类中,然后各自独立发展。 基本…...
常用设计模式
一、什么是设计模式 设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的代码设计经验总结,旨在解决面向对象设计中反复出现的问题,提升代码的可重用性、可理解性和可靠性。以下从多个维度详细讲解ÿ…...
20242817-李臻-课下作业:Qt和Sqlite
实验内容 阅读附件内容,编译运行附件中第一章,第三章的例子。 实验过程 第一章 t1实践 #include <QApplication> #include <QWidget> #include <QPushButton> #include <QVBoxLayout>int main(int argc, char *argv[]) {QA…...
嵌入式机器学习平台Edge Impulse图像分类 – 快速入门
陈拓 2025/05/08-2025/05/11 1. 简介 官方网址 https://edgeimpulse.com/ 适用于任何边缘设备的人工智能: Gateways - 网关 Sensors & Cameras - 传感器和摄像头 Docker Containers - Docker容器 MCUs, NPUs, CPUs, GPUs 构建数据集、训练模型并优化库以…...
JavaWeb, Spring, Spring Boot
出现时间 JavaWeb - Spring - Spring Boot 一、JavaWeb 的发展历程 Servlet 和 JSP: Servlet:1997 年首次发布,用于处理 HTTP 请求和响应。 JSP:1999 年首次发布,用于动态生成 HTML 页面。 特点:提供了基…...
upload-labs靶场通关详解:第五关
一、分析源代码 $is_upload false; $msg null; if (isset($_POST[submit])) {if (file_exists(UPLOAD_PATH)) {$deny_ext array(".php",".php5",".php4",".php3",".php2",".html",".htm",".ph…...
【问题】Watt加速github访问速度:好用[特殊字符]
前言 GitHub 是全球知名的代码托管平台,主要用于软件开发,提供 Git 仓库托管、协作工具等功能,经常要用到,但是国内用户常因网络问题难以稳定访问 。 Watt Toolkit(原名 Steam)是由江苏蒸汽凡星科技有限公…...
GitHub打开缓慢甚至失败的解决办法
在C:\Windows\System32\drivers\etc的hosts中增加如下内容: 20.205.243.166 github.com 199.59.149.236 github.global.ssl.fastly.net185.199.109.153 http://assets-cdn.github.com 185.199.108.153 http://assets-cdn.github.com 185.199.110.153 http://asset…...
【25软考网工】第六章(3)数字签名和数字证书
博客主页:christine-rr-CSDN博客 专栏主页:软考中级网络工程师笔记 大家好,我是christine-rr !目前《软考中级网络工程师》专栏已经更新二十多篇文章了,每篇笔记都包含详细的知识点,希望能帮助到你!…...
Android Native 函数 Hook 技术介绍
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/ 前言 Android Native 函数 Hook 技术是一种在应用运行时拦截或替换系统或自身函数行为的手段,常见实现包括 PLT Hook、Inline Hook。 PLT Hook 和…...
代码随想录算法训练营第60期第三十二天打卡
大家好,今天是我们贪心算法章节的第三阶段,前面我们讲过的几道题不知道大家理解的情况如何,还是那句话,贪心算法没有固定的套路与模板,一道题一个思路,我们要多思考这样慢慢地我就就可以水到渠成。今天我们…...
Problem C: 异常1
1.题目描述 检测年龄,其中若为负数或大于等于200岁皆为异常,请将下列代码补充完整。 // 你的代码将被嵌入这里 class Main{ public static void main(String[] args){ Person p1new Person("John",80); Person p2new Pers…...
Ollama部署使用以及模型微调和本地部署
ollama是一款开源的本地大语言模型管理工具,专注于简化大语言模型(LLM)的本地部署和使用。以下是关于 Ollama 应用的详细介绍: Ollama 的主要功能 本地化部署: Ollama 支持在本地运行模型,无需依赖外部云…...
汇编学习——iOS开发对arm64汇编的初步了解
汇编学习——iOS开发对arm64汇编的初步了解 文章目录 汇编学习——iOS开发对arm64汇编的初步了解前言栈 指令 寄存器寄存器指令运算指令寻址指令前变基 与 后变基 栈堆(Heap)内存机制三、栈(Stack)内存机制 3. 多级调用示例 例子A…...
前端代理问题
在前后端联调的时候,有一次因为前端项目代理配置有问题,导致请求接口对不上, transpileDependencies: true,devServer: {hot: true,port: 8081,proxy: {/api: {target: http://localhost:8080,changeOrigin: true,ws: true,pathRewrite: {^/a…...
E+H流量计通过Profibus DP主站转Modbus TCP网关与上位机轻松通讯
EH流量计通过Profibus DP主站转Modbus TCP网关与上位机轻松通讯 在现代工业自动化的广阔舞台上,Profibus DP与Modbus TCP这两种通信协议各领风骚,它们在不同的应用场景中发挥着举足轻重的作用。但工业生产的复杂性往往要求不同设备、系统之间能够顺畅沟…...
TCP/IP 模型每层的封装格式
TCP/IP 模型是一个四层网络架构,每一层在数据传输时都会对数据进行封装,添加相应的头部(和尾部)信息。以下是各层的封装格式及关键字段说明: 1. 应用层(Application Layer) 封装格式:…...
openjdk底层汇编指令调用(一)——汇编指令及指令编码基础
汇编指令 计算机在执行过程时只识别代表0或者1的电信号。因此为了让计算机能够执行则须向计算机输入一系列01构成的指令。 例如在x64平台下,0x53,二进制为01010011,表示将rbx寄存器中的值压栈。 但是,对于程序员而言,…...
5G-A来了!5G信号多个A带来哪些改变?
5G-A来了!5G信号多个A带来哪些改变? 随着科技不断进步,通信网络的迭代升级也在加速。自4G、5G的推出以来,我们见证了通信技术的飞跃式发展。最近,越来越多的用户发现自己手机屏幕右上角的5G标识已经变成了“5G-A”。那…...
探索虚拟化:云计算时代的资源优化之道
前言 如果您想知道云提供商如何在全球范围内运行无数应用程序,而每个应用程序都没有机架服务器,那么答案就在于虚拟化。 它是为云提供支持的核心技术之一,在幕后悄悄工作,使现代计算高效、可扩展且具有成本效益。 在本文中&#x…...
用户登录构件示例
目录 一、登录构件概述 二、构件内部结构 1. 构件组成元素(表格形式) 2. 组件连接件设计...
【软件测试】基于项目驱动的功能测试报告
目录 一、项目的介绍 1.1 项目背景 二、测试目标 2.1 用户服务模块 2.1.1 用户注册模块 2.1.1.1 测试点 2.1.1.2 边界值分析法(等价类+边界值) 2.1.1.2.1 有效等价类 2.1.1.2.2 无效等价类 2.1.1.2.3 边界值 2.1.1.2.4 测试用例设计 2.2 文章标签模块 2.3 文章模…...
【QT】UDP通讯本地调试
qt已经写好了udp通讯代码,现在要进行测试。 1、终端输入ipconfig查看本机网卡的ipv4地址 2、 用udpBind函数,绑定到此ip和自定义的端口号。 3、 打开网络调试助手,自动检测到本机的ip地址,输入任意一个和程序里不一样的端口号。 …...
web animation API 锋利的css动画控制器 (更新中)
什么是web animation api 以及为什么要使用web animation api? web animation API 是web页面中控制DOM元素动画效果的javascript原生API。 它能够逐个关键帧控制动画效果,具有Timeline 机制能通过javascript来实现动画的暂停,播放&#x…...
Nginx的增强与可视化!OpenResty Manager - 现代化UI+高性能反向代理+安全防护
以下是对OpenResty Manager的简要介绍: OpenResty Manager (Nginx 增强版),是一款容易使用、功能强大且美观的反向代理工具 ,可以作为OpenResty Edge 的开源替代品基于 OpenResty 开发,支持并继承 OpenRes…...
Spring Boot 中的重试机制
Retryable 注解简介 Retryable 注解是 Spring Retry 模块提供的,用于自动重试可能会失败的方法。在微服务架构和分布式系统中,服务之间的调用可能会因为网络问题、服务繁忙等原因失败。使用 Retryable 可以提高应用的稳定性和容错能力 1。 使用步骤 &…...
[Java实战]Spring Boot 整合 Freemarker (十一)
[Java实战]Spring Boot 整合 Freemarker (十一) 引言 Apache FreeMarker 作为一款高性能的模板引擎,凭借其简洁语法、卓越性能和灵活扩展性,在 Java Web 开发中占据重要地位。结合 Spring Boot 的自动化配置能力,开发者能快速构建动态页面、…...
现有预测式外呼系统如何接入AI系统,使用AI辅助,判断出意向客户再转人工
很多用户还在使用老旧的预测式外呼系统,只能外呼接通后播放一个提示音,播放完提示音后在转给人工坐席, 如果重新部署一套AI外呼系统,涉及到业务系统的迁移,非常不方便。 现在我就做一个如何让现有外呼系统,…...
实战项目3(04)
目录 任务场景一 【r1配置】 【sw1配置】 任务场景二 【r1配置】 【sw1配置】 【sw2配置】 任务场景一 某公司网络为了减少广播包对网络的影响,网络管理员对网络进行了VLAN划分,完成VLAN划分后,为了不影响VL…...
[Java实战]Spring Boot 静态资源配置(十三)
[Java实战]Spring Boot 静态资源配置(十三) 引言 静态资源(如 HTML、CSS、JavaScript、图片等)是 Web 应用的基石。Spring Boot 通过自动化配置简化了静态资源管理,但面对复杂场景(如多模块项目、CDN 集成…...
ESP-ADF wifi_service子模块wifi_ssid_manager凭证管理函数详解
目录 ESP-ADF wifi_service子模块wifi_ssid_manager凭证管理函数详解WiFi凭证管理函数分析wifi_ssid_manager_savewifi_ssid_manager_erase_all 内部实现机制存储策略分析内部数据流向关键辅助函数分析重要的内部辅助函数详解get_stored_id_by_ssidnvs_get_write_idnvs_set_cou…...
数据分析预备篇---NumPy数组
NumPy是数据分析时常用的库,全称为Numerical Python,是很多数据或科学相关Python包的基础,包括pandas,scipy等等,常常被用于科学及工程领域。NumPy最核心的数据结构是ND array,意思是N维数组。 #以下是一个普通列表的操作示例:arr = [5,17,3,26,31]#打印第一个元素 prin…...
解决VirtualBox中虚拟机(ubuntu)与主机(windows)之间互相复制粘贴(文本)
一.开始的设置 1.在VirtualBox中打开设置,常规中修改主机与虚拟机交互设置 2.虚拟机关闭状态下,存储中选中控制器SATA,勾选‘使用主机输入输出’ 3.选中操作系统对应的虚拟文件,.vdi文件,勾选右边的固态驱动器。 4.启…...
单细胞RNA测序数据分析与可视化:从基础原理到高级应用
引言 单细胞RNA测序(scRNA-seq)技术的出现彻底改变了我们研究复杂生物系统的方式,使科学家能够在前所未有的精细水平上解析细胞异质性。传统的bulk RNA测序只能捕获细胞群体的平均表达特征,而单细胞转录组测序允许我们检测每个细…...
嵌入式硬件篇---UART
文章目录 前言1. UART协议基础1.1 物理层特性两根信号线无时钟信号电平标准TTL UARTRS-232 1.2 数据帧格式1.3 波特率计算波特率 2. STM32F103RCT6的UART配置2.1 硬件连接2.2 CubeMX配置启用USART1引脚分配中断启用(可选) 3. HAL库代码实现3.1 UART初始化…...
C# 通过ConfigurationManager读写配置文件App.Config
目录 简述代码描述一、构建App.config二、调用代码1、代码步骤说明2、输出结果说明 简述 App.config 是 C#中最常用的配置文件类型。 通常位于项目的根目录中,以 XML 格式存储配置信息。App.config 文件可以包含多个配置节,如 appSettings、connectionS…...
Python3安装HTMLTestRunner
1.下载HTMLTestRunner地址:http://tungwaiyip.info/software/HTMLTestRunner.html 2.下载的HTMLTestRunner.py是针对python2写的,所以需要改成python3适合的内容: 问题1:No module named StringIO, 原因:py…...
图形化编程革命:iVX携手AI 原生开发范式
一、技术核心:图形化编程的底层架构解析 1. 图形化开发的效率优势:代码量减少 72% 的秘密 传统文本编程存在显著的信息密度瓶颈。以 "按钮点击→条件判断→调用接口→弹窗反馈" 流程为例,Python 实现需定义函数、处理缩进并编写 …...
GC垃圾回收
Gc是语言提供的自动的内存管理机制,自动释放不需要的内存对象,让出存储器资源。 Go语言变革: V1.5的三色并发标记法 V1.5的三色并发标记为什么需要STW V1.5的三色标记为什么需要屏障机制(“强-弱”,三色不变式、插入屏障、删除屏障…...
“多端多接口多向传导”空战数据链体系——从异构融合架构到抗毁弹性网络的系统性设计
“多端多接口多向传导”空战数据链体系——从异构融合架构到抗毁弹性网络的系统性设计 文章目录 “多端多接口多向传导”空战数据链体系——从异构融合架构到抗毁弹性网络的系统性设计第一章 引言:空战数据链体系的范式革新1.1 空战数据链的演进逻辑1.2 新架构的核心理论价值1…...
Unity3D仿星露谷物语开发42之粒子系统
1、目标 使用例子系统,实现割草后草掉落的特效。 通过PoolManager获取特效预制体,通过VFXManager来触发特效。 2、配置例子特效 在Hierarchy -> PersistentScene下创建新物体命名为Reaping。 给该物体添加Particle System组件。 配置例子系统参数…...
python打卡训练营打卡记录day22
复习日 仔细回顾一下之前21天的内容,没跟上进度的同学补一下进度。 作业: 自行学习参考如何使用kaggle平台,写下使用注意点,并对下述比赛提交代码 kaggle泰坦尼克号人员生还预测 导入数据 # 导入所需库 import pandas as pd impor…...
网络编程(一)网络编程入门
本节课学习TCP客户端和服务器端编程架构,其分为分为C/S(客户端/服务器模式)和B/S(浏览器/服务器架构模式)两种模式。接下来我们分别了解这两种模式 C/S模式 C/S模式:服务器首先先启动,并根据客…...
华为IP(6)
VLAN聚合 VLAN聚合产生的技术背景 在一般是三层交换机中,通常采用一个VLAN接口的方式实现广播域之间的互通,这在某些情况下导致了IP地址的浪费 因为一个VLAN对应的子网中,子网号、子网广播地址、子网网关地址不能用作VLAN内的主机IP地址&a…...