【强化学习】深度确定性策略梯度算法(DDPG)详解(附代码)
📢本篇文章是博主强化学习(RL)领域学习时,用于个人学习、研究或者欣赏使用,并基于博主对相关等领域的一些理解而记录的学习摘录和笔记,若有不当和侵权之处,指出后将会立即改正,还望谅解。文章分类在👉强化学习专栏:
【强化学习】- 【单智能体强化学习】(10)---《深度确定性策略梯度算法(DDPG)详解》
深度确定性策略梯度算法(DDPG)详解
目录
DDPG算法详细介绍
算法特点
核心改进点
算法公式推导
1. Q值函数更新
2. 策略更新(Actor网络)
3. 目标网络更新
算法流程
[Python] DDPG算法实现
1. 导入必要库
2. 定义 Actor 网络
3. 定义 Critic 网络
4. 定义经验回放池
5. 定义 DDPG 智能体
6. 动作选择方法
7. 训练方法
8. 训练智能体
9.可视化学习曲线
完整代码
[Results] 运行结果
[Notice] 代码说明
优势
通俗类比
应用场景
DDPG算法详细介绍
深度确定性策略梯度(Deep Deterministic Policy Gradient、DDPG)算法是一种基于深度强化学习的算法,适用于解决连续动作空间的问题,比如机器人控制中的连续运动。它结合了确定性策略和深度神经网络,是一种模型无关的强化学习算法,属于Actor-Critic框架,并且同时利用了DQN和PG(Policy Gradient)的优点。
算法特点
适用于连续动作空间:
DDPG直接输出连续值动作,无需对动作进行离散化。
利用确定性策略:
与随机策略不同,DDPG输出的是每个状态下一个确定的最优动作。
结合目标网络:
使用延迟更新的目标网络,稳定了训练过程,避免了过大的参数波动。
经验回放机制:
通过经验回放缓解数据相关性,提升样本利用率。
高效学习:
使用Critic网络评估动作的质量,使得策略优化过程更加高效。
核心改进点
1.从DQN继承的目标网络:
避免Q值的估计震荡问题。
提高算法的训练稳定性。
2.从PG继承的策略梯度优化:
通过Actor网络直接优化策略,适应连续动作问题。
3.经验回放(Replay Buffer):
将交互环境中的经验(状态、动作、奖励、下一状态)存储起来,训练时从中随机采样,减少数据相关性和样本浪费。
4.双网络架构:
Actor网络负责生成动作;Critic网络评估动作的质量。
算法公式推导
1. Q值函数更新
DDPG使用Bellman方程更新Critic网络的目标Q值:
- 是目标Critic网络。
- 是目标Actor网络。
- 是折扣因子。
- 是下一状态和目标动作。
Critic网络的优化目标是最小化以下损失函数:
其中:
- 是Critic网络的参数。
- 是目标值。
2. 策略更新(Actor网络)
Actor网络通过最大化Critic网络的Q值来优化策略,其目标函数为:
使用梯度上升法更新Actor网络:
3. 目标网络更新
目标网络采用软更新方式,缓慢地向当前网络靠近:
其中 是软更新系数。
算法流程
-
初始化:
初始化Actor、Critic网络和它们对应的目标网络。
初始化经验回放池。 -
交互环境:
在状态 下,通过Actor网络生成动作。
执行动作 ,获取奖励和下一状态 。 -
存储经验:
将 存储到经验回放池。 -
采样训练:
从经验池中随机采样小批量数据。 -
更新Critic网络:
使用采样数据和目标Critic网络计算目标值 ,最小化Critic的损失函数。 -
更新Actor网络:
使用Critic网络的梯度来调整Actor网络的参数。 -
目标网络更新:
按照软更新公式更新目标网络的参数。 -
重复以上步骤,直到达到学习目标。
[Python] DDPG算法实现
下面给出了DDPG(深度确定性策略梯度)算法的完整Python实现。该实现包括Actor-Critic架构、缓冲区和目标网络等。
项目代码我已经放入GitCode里面,可以通过下面链接跳转:🔥
【强化学习】--- DDPG算法
后续相关单智能体强化学习算法也会不断在【强化学习】项目里更新,如果该项目对你有所帮助,请帮我点一个星星✨✨✨✨✨,鼓励分享,十分感谢!!!
若是下面代码复现困难或者有问题,也欢迎评论区留言。
1. 导入必要库
import gym # 导入 Gym 库,用于创建和管理强化学习环境
import numpy as np # 导入 NumPy,用于处理数组和数学运算
import torch # 导入 PyTorch,用于构建和训练神经网络
import torch.nn as nn # 导入 PyTorch 的神经网络模块
import torch.optim as optim # 导入 PyTorch 的优化器模块
from collections import deque # 导入双端队列,用于实现经验回放池
import random # 导入随机模块,用于从经验池中采样
- gym:用于创建和管理强化学习环境(例如
Pendulum-v1
)。 - numpy:处理数组和数值计算。
- torch:用于深度学习模型的构建和训练。
- deque:一个双端队列,适用于存储经验回放池。
- random:用于从经验池中随机抽样。
2. 定义 Actor 网络
# 定义 Actor 网络(策略网络)
class Actor(nn.Module):
def __init__(self, state_dim, action_dim, max_action):
super(Actor, self).__init__()
self.layer1 = nn.Linear(state_dim, 256) # 输入层到隐藏层1,大小为 256
self.layer2 = nn.Linear(256, 256) # 隐藏层1到隐藏层2,大小为 256
self.layer3 = nn.Linear(256, action_dim) # 隐藏层2到输出层,输出动作维度
self.max_action = max_action # 动作的最大值,用于限制输出范围
def forward(self, state):
x = torch.relu(self.layer1(state)) # 使用 ReLU 激活函数处理隐藏层1
x = torch.relu(self.layer2(x)) # 使用 ReLU 激活函数处理隐藏层2
x = torch.tanh(self.layer3(x)) * self.max_action # 使用 Tanh 激活函数,并放大到动作范围
return x # 返回输出动作
解析:
- Actor 网络的作用是生成给定状态下的最优动作。
- 输入:
state_dim
:环境状态的维度。action_dim
:动作空间的维度。max_action
:动作的最大值,用于约束输出动作的范围。
- 网络结构:
- 两层隐藏层(256个神经元,每层使用ReLU激活函数)。
- 输出层使用
tanh
激活函数(将动作限制在 ([-1, 1])),再乘以max_action
缩放到实际动作范围。
3. 定义 Critic 网络
# 定义 Critic 网络(价值网络)
class Critic(nn.Module):
def __init__(self, state_dim, action_dim):
super(Critic, self).__init__()
self.layer1 = nn.Linear(state_dim + action_dim, 256) # 将状态和动作拼接后输入到隐藏层1
self.layer2 = nn.Linear(256, 256) # 隐藏层1到隐藏层2,大小为 256
self.layer3 = nn.Linear(256, 1) # 隐藏层2到输出层,输出 Q 值
def forward(self, state, action):
x = torch.cat([state, action], dim=1) # 将状态和动作拼接为单个输入
x = torch.relu(self.layer1(x)) # 使用 ReLU 激活函数处理隐藏层1
x = torch.relu(self.layer2(x)) # 使用 ReLU 激活函数处理隐藏层2
x = self.layer3(x) # 输出 Q 值
return x # 返回 Q 值
解析:
- Critic 网络评估给定状态和动作的质量(即 Q 值)。
- 输入:
state
:环境的当前状态。action
:给定状态下的动作。
- 网络结构:
- 将状态和动作拼接作为输入。
- 两层隐藏层,使用ReLU激活。
- 输出层是一个标量 Q 值,表示该状态动作对的质量。
4. 定义经验回放池
# 定义经验回放池
class ReplayBuffer:
def __init__(self, max_size):
self.buffer = deque(maxlen=max_size) # 初始化一个双端队列,设置最大容量
def add(self, state, action, reward, next_state, done):
self.buffer.append((state, action, reward, next_state, done)) # 将经验存入队列
def sample(self, batch_size):
batch = random.sample(self.buffer, batch_size) # 随机采样一个小批量数据
states, actions, rewards, next_states, dones = zip(*batch) # 解压采样数据
return (np.array(states), np.array(actions), np.array(rewards),
np.array(next_states), np.array(dones)) # 返回 NumPy 数组格式的数据
def size(self):
return len(self.buffer) # 返回经验池中当前存储的样本数量
解析:
- 作用:存储智能体与环境交互的经验数据,打破样本间的时间相关性。
- 方法:
add
:将 ((state, action, reward, next_state, done)) 存入经验池。sample
:从经验池中随机采样,返回小批量的训练数据。size
:返回经验池中的样本数量。
5. 定义 DDPG 智能体
# 定义 DDPG 智能体
class DDPGAgent:
def __init__(self, state_dim, action_dim, max_action, gamma=0.99, tau=0.005, buffer_size=100000, batch_size=64):
self.actor = Actor(state_dim, action_dim, max_action) # 初始化 Actor 网络
self.actor_target = Actor(state_dim, action_dim, max_action) # 初始化目标 Actor 网络
self.actor_target.load_state_dict(self.actor.state_dict()) # 将 Actor 网络的权重复制到目标网络
self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=1e-4) # 定义 Actor 网络的优化器
self.critic = Critic(state_dim, action_dim) # 初始化 Critic 网络
self.critic_target = Critic(state_dim, action_dim) # 初始化目标 Critic 网络
self.critic_target.load_state_dict(self.critic.state_dict()) # 将 Critic 网络的权重复制到目标网络
self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=1e-3) # 定义 Critic 网络的优化器
self.max_action = max_action # 动作的最大值
self.gamma = gamma # 折扣因子
self.tau = tau # 目标网络软更新参数
self.replay_buffer = ReplayBuffer(buffer_size) # 初始化经验回放池
self.batch_size = batch_size # 批量大小
解析:
- 初始化网络:
Actor
和Critic
网络分别有目标网络(actor_target
和critic_target
),用于稳定训练。
- 优化器:
Adam
优化器分别优化 Actor 和 Critic 网络。
- 超参数:
gamma
:折扣因子。tau
:目标网络更新的系数。buffer_size
:经验池的容量。batch_size
:每次训练使用的样本数量。
6. 动作选择方法
# 根据状态选择动作
def select_action(self, state):
state = torch.FloatTensor(state.reshape(1, -1)) # 将状态转换为 PyTorch 张量
action = self.actor(state).detach().cpu().numpy().flatten() # 使用 Actor 网络生成动作,并转为 NumPy 数组
return action # 返回动作
- 作用:根据当前状态 (s),生成一个连续动作 (a)。
- 流程:
- 将输入状态转换为 Torch 张量。
- 用 Actor 网络预测动作。
- 动作转换为 NumPy 数组返回。
7. 训练方法
# 训练方法
def train(self):
# 如果回放池中样本数量不足,直接返回
if self.replay_buffer.size() < self.batch_size:
return
# 从回放池中采样一批数据
states, actions, rewards, next_states, dones = self.replay_buffer.sample(self.batch_size)
# 将采样的数据转换为张量
states = torch.FloatTensor(states)
actions = torch.FloatTensor(actions)
rewards = torch.FloatTensor(rewards).unsqueeze(1) # 添加一个维度以匹配Q值维度
next_states = torch.FloatTensor(next_states)
dones = torch.FloatTensor(dones).unsqueeze(1) # 添加一个维度以匹配Q值维度
# 计算critic的损失
with torch.no_grad(): # 关闭梯度计算
next_actions = self.actor_target(next_states) # 使用目标actor网络预测下一步动作
target_q = self.critic_target(next_states, next_actions) # 目标Q值
# 使用贝尔曼方程更新目标Q值
target_q = rewards + (1 - dones) * self.gamma * target_q
# 当前Q值
current_q = self.critic(states, actions)
# 均方误差损失
critic_loss = nn.MSELoss()(current_q, target_q)
# 优化critic网络
self.critic_optimizer.zero_grad()
critic_loss.backward()
self.critic_optimizer.step()
# 计算actor的损失
actor_loss = -self.critic(states, self.actor(states)).mean() # 策略梯度目标为最大化Q值
# 优化actor网络
self.actor_optimizer.zero_grad()
actor_loss.backward()
self.actor_optimizer.step()
# 更新目标网络参数(软更新)
for target_param, param in zip(self.critic_target.parameters(), self.critic.parameters()):
target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)
for target_param, param in zip(self.actor_target.parameters(), self.actor.parameters()):
target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)
# 将样本添加到回放池中
def add_to_replay_buffer(self, state, action, reward, next_state, done):
self.replay_buffer.add(state, action, reward, next_state, done)
解析:
-
Critic 网络更新:
计算目标 Q 值。
使用均方误差 (MSE) 更新 Critic 网络。 -
Actor 网络更新:
优化目标:最大化 Critic 网络的 Q 值。 -
目标网络更新:
使用软更新方式(通过 (\tau))平滑更新目标网络参数。
8. 训练智能体
def train_ddpg(env_name, episodes=1000, max_steps=200):
# 创建环境
env = gym.make(env_name)
state_dim = env.observation_space.shape[0] # 状态空间维度
action_dim = env.action_space.shape[0] # 动作空间维度
max_action = float(env.action_space.high[0]) # 动作最大值
# 初始化DDPG智能体
agent = DDPGAgent(state_dim, action_dim, max_action)
rewards = [] # 用于存储每个episode的奖励
for episode in range(episodes):
state, _ = env.reset() # 重置环境,获取初始状态
episode_reward = 0 # 初始化每轮奖励为0
for step in range(max_steps):
# 选择动作
action = agent.select_action(state)
# 执行动作,获取环境反馈
next_state, reward, done, _, _ = env.step(action)
# 将样本存入回放池
agent.add_to_replay_buffer(state, action, reward, next_state, done)
# 训练智能体
agent.train()
# 更新当前状态
state = next_state
# 累加奖励
episode_reward += reward
if done: # 如果完成(到达终止状态),结束本轮
break
# 记录每轮的累计奖励
rewards.append(episode_reward)
print(f"Episode: {episode + 1}, Reward: {episode_reward}")
解析:
- 训练流程:
- 初始化环境和智能体。
- 在每个 episode 中:
- 使用 Actor 网络生成动作与环境交互。
- 将经验存储到经验池。
- 更新 Actor 和 Critic 网络。
- 打印 episode 的累计奖励。
9.可视化学习曲线
# 绘制学习曲线的方法
import matplotlib.pyplot as plt
# 绘制学习曲线
plt.plot(rewards)
plt.title("Learning Curve")
plt.xlabel("Episodes")
plt.ylabel("Cumulative Reward")
plt.show()
完整代码
"""《DDPG算法的代码》
时间:2024.12
环境:gym
作者:不去幼儿园
"""
import gym # 导入 Gym 库,用于创建和管理强化学习环境
import numpy as np # 导入 NumPy,用于处理数组和数学运算
import torch # 导入 PyTorch,用于构建和训练神经网络
import torch.nn as nn # 导入 PyTorch 的神经网络模块
import torch.optim as optim # 导入 PyTorch 的优化器模块
from collections import deque # 导入双端队列,用于实现经验回放池
import random # 导入随机模块,用于从经验池中采样
# 定义 Actor 网络(策略网络)
class Actor(nn.Module):
def __init__(self, state_dim, action_dim, max_action):
super(Actor, self).__init__()
self.layer1 = nn.Linear(state_dim, 256) # 输入层到隐藏层1,大小为 256
self.layer2 = nn.Linear(256, 256) # 隐藏层1到隐藏层2,大小为 256
self.layer3 = nn.Linear(256, action_dim) # 隐藏层2到输出层,输出动作维度
self.max_action = max_action # 动作的最大值,用于限制输出范围
def forward(self, state):
x = torch.relu(self.layer1(state)) # 使用 ReLU 激活函数处理隐藏层1
x = torch.relu(self.layer2(x)) # 使用 ReLU 激活函数处理隐藏层2
x = torch.tanh(self.layer3(x)) * self.max_action # 使用 Tanh 激活函数,并放大到动作范围
return x # 返回输出动作
# 定义 Critic 网络(价值网络)
class Critic(nn.Module):
def __init__(self, state_dim, action_dim):
super(Critic, self).__init__()
self.layer1 = nn.Linear(state_dim + action_dim, 256) # 将状态和动作拼接后输入到隐藏层1
self.layer2 = nn.Linear(256, 256) # 隐藏层1到隐藏层2,大小为 256
self.layer3 = nn.Linear(256, 1) # 隐藏层2到输出层,输出 Q 值
def forward(self, state, action):
x = torch.cat([state, action], dim=1) # 将状态和动作拼接为单个输入
x = torch.relu(self.layer1(x)) # 使用 ReLU 激活函数处理隐藏层1
x = torch.relu(self.layer2(x)) # 使用 ReLU 激活函数处理隐藏层2
x = self.layer3(x) # 输出 Q 值
return x # 返回 Q 值
# 定义经验回放池
class ReplayBuffer:
def __init__(self, max_size):
self.buffer = deque(maxlen=max_size) # 初始化一个双端队列,设置最大容量
def add(self, state, action, reward, next_state, done):
self.buffer.append((state, action, reward, next_state, done)) # 将经验存入队列
def sample(self, batch_size):
batch = random.sample(self.buffer, batch_size) # 随机采样一个小批量数据
states, actions, rewards, next_states, dones = zip(*batch) # 解压采样数据
return (np.array(states), np.array(actions), np.array(rewards),
np.array(next_states), np.array(dones)) # 返回 NumPy 数组格式的数据
def size(self):
return len(self.buffer) # 返回经验池中当前存储的样本数量
# DDPG智能体类定义
class DDPGAgent:
# 初始化方法,设置智能体的参数和模型
def __init__(self, state_dim, action_dim, max_action, gamma=0.99, tau=0.005, buffer_size=100000, batch_size=64):
# 定义actor网络(策略网络)及其目标网络
self.actor = Actor(state_dim, action_dim, max_action)
self.actor_target = Actor(state_dim, action_dim, max_action)
# 将目标actor网络的参数初始化为与actor网络一致
self.actor_target.load_state_dict(self.actor.state_dict())
# 定义actor网络的优化器
self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=1e-4)
# 定义critic网络(值网络)及其目标网络
self.critic = Critic(state_dim, action_dim)
self.critic_target = Critic(state_dim, action_dim)
# 将目标critic网络的参数初始化为与critic网络一致
self.critic_target.load_state_dict(self.critic.state_dict())
# 定义critic网络的优化器
self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=1e-3)
# 保存动作的最大值,用于限制动作范围
self.max_action = max_action
# 折扣因子,用于奖励的时间折扣
self.gamma = gamma
# 软更新系数,用于目标网络的更新
self.tau = tau
# 初始化经验回放池
self.replay_buffer = ReplayBuffer(buffer_size)
# 每次训练的批量大小
self.batch_size = batch_size
# 选择动作的方法
def select_action(self, state):
# 将状态转换为张量
state = torch.FloatTensor(state.reshape(1, -1))
# 使用actor网络预测动作,并将结果转换为NumPy数组
action = self.actor(state).detach().cpu().numpy().flatten()
return action
# 训练方法
def train(self):
# 如果回放池中样本数量不足,直接返回
if self.replay_buffer.size() < self.batch_size:
return
# 从回放池中采样一批数据
states, actions, rewards, next_states, dones = self.replay_buffer.sample(self.batch_size)
# 将采样的数据转换为张量
states = torch.FloatTensor(states)
actions = torch.FloatTensor(actions)
rewards = torch.FloatTensor(rewards).unsqueeze(1) # 添加一个维度以匹配Q值维度
next_states = torch.FloatTensor(next_states)
dones = torch.FloatTensor(dones).unsqueeze(1) # 添加一个维度以匹配Q值维度
# 计算critic的损失
with torch.no_grad(): # 关闭梯度计算
next_actions = self.actor_target(next_states) # 使用目标actor网络预测下一步动作
target_q = self.critic_target(next_states, next_actions) # 目标Q值
# 使用贝尔曼方程更新目标Q值
target_q = rewards + (1 - dones) * self.gamma * target_q
# 当前Q值
current_q = self.critic(states, actions)
# 均方误差损失
critic_loss = nn.MSELoss()(current_q, target_q)
# 优化critic网络
self.critic_optimizer.zero_grad()
critic_loss.backward()
self.critic_optimizer.step()
# 计算actor的损失
actor_loss = -self.critic(states, self.actor(states)).mean() # 策略梯度目标为最大化Q值
# 优化actor网络
self.actor_optimizer.zero_grad()
actor_loss.backward()
self.actor_optimizer.step()
# 更新目标网络参数(软更新)
for target_param, param in zip(self.critic_target.parameters(), self.critic.parameters()):
target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)
for target_param, param in zip(self.actor_target.parameters(), self.actor.parameters()):
target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)
# 将样本添加到回放池中
def add_to_replay_buffer(self, state, action, reward, next_state, done):
self.replay_buffer.add(state, action, reward, next_state, done)
# 绘制学习曲线的方法
import matplotlib.pyplot as plt
def train_ddpg(env_name, episodes=1000, max_steps=200):
# 创建环境
env = gym.make(env_name)
state_dim = env.observation_space.shape[0] # 状态空间维度
action_dim = env.action_space.shape[0] # 动作空间维度
max_action = float(env.action_space.high[0]) # 动作最大值
# 初始化DDPG智能体
agent = DDPGAgent(state_dim, action_dim, max_action)
rewards = [] # 用于存储每个episode的奖励
for episode in range(episodes):
state, _ = env.reset() # 重置环境,获取初始状态
episode_reward = 0 # 初始化每轮奖励为0
for step in range(max_steps):
# 选择动作
action = agent.select_action(state)
# 执行动作,获取环境反馈
next_state, reward, done, _, _ = env.step(action)
# 将样本存入回放池
agent.add_to_replay_buffer(state, action, reward, next_state, done)
# 训练智能体
agent.train()
# 更新当前状态
state = next_state
# 累加奖励
episode_reward += reward
if done: # 如果完成(到达终止状态),结束本轮
break
# 记录每轮的累计奖励
rewards.append(episode_reward)
print(f"Episode: {episode + 1}, Reward: {episode_reward}")
# 绘制学习曲线
plt.plot(rewards)
plt.title("Learning Curve")
plt.xlabel("Episodes")
plt.ylabel("Cumulative Reward")
plt.show()
env.close() # 关闭环境
# 主函数运行
if __name__ == "__main__":
# 定义环境名称和训练轮数
env_name = "Pendulum-v1"
episodes = 500
# 开始训练
train_ddpg(env_name, episodes=episodes)
[Results] 运行结果
[Notice] 代码说明
演员和评论家网络:
演员网络预测给定当前状态的动作。
批评家网络评估状态-行为对的q值。
Replay Buffer:
存储过去的经验,并使有效的采样训练。
训练:
Critic使用Bellman方程更新评论家。
Actor被更新以最大化期望q值。
目标网络:
平滑更新以稳定训练。
环境:
代理在Pendulum-v1环境中进行训练作为演示。
# 环境配置
Python 3.11.5
torch 2.1.0
torchvision 0.16.0
gym 0.26.2
由于博文主要为了介绍相关算法的原理和应用的方法,缺乏对于实际效果的关注,算法可能在上述环境中的效果不佳或者无法运行,一是算法不适配上述环境,二是算法未调参和优化,三是没有呈现完整的代码,四是等等。上述代码用于了解和学习算法足够了,但若是想直接将上面代码应用于实际项目中,还需要进行修改。
优势
-
解决连续动作问题: 它可以直接输出一个连续值动作,而不像传统的离散强化学习算法需要动作离散化。
-
样本效率高: 使用了经验回放和目标网络,这减少了样本相关性问题,提高了学习效率和稳定性。
通俗类比
可以把DDPG算法想象成一个赛车手(Actor)和他的教练(Critic):
- **赛车手(Actor)**决定转弯的角度、加速的力度,直接控制赛车。
- **教练(Critic)**则通过观察赛车手的表现,告诉他哪些动作是好的,哪些是需要改进的。
- 经验回放池就是赛车手在训练中不断回看他之前的比赛录像,找到改进的地方。
- 目标网络则类似于赛车手的长期目标,比如平稳驾驶,而不是今天开得快、明天开得慢。
应用场景
- 机器人运动控制(机械臂、无人机)
- 自动驾驶中的连续控制任务
- 游戏中的复杂策略设计
更多强化学习文章,请前往:【强化学习(RL)】专栏
博客都是给自己看的笔记,如有误导深表抱歉。文章若有不当和不正确之处,还望理解与指出。由于部分文字、图片等来源于互联网,无法核实真实出处,如涉及相关争议,请联系博主删除。如有错误、疑问和侵权,欢迎评论留言联系作者,或者添加VX:Rainbook_2,联系作者。✨