深度强化学习-DDPG代码阅读-AandC.py(2)

目录

1.编写AandC.py

1.1 导入包

1.2 定义权重和偏置初始化器

1.3 定义ActorNetwork类

1.3.1 代码总括

1.3.2 代码分解

1.4 定义 self.update_target_network_params

1.4.1 代码总括

1.4.2 代码分解

1.5 使用Adam优化

1.6 定义create_actor_network()函数

1.6.1 代码总括

1.6.2 代码分解

1.7 定义 Actor 函数

1.7.1 代码总括

1.7.2 代码分解

1.8 定义CriticNetwork 类

1.8.1 代码总括

1.8.2 代码分解

1.9 critic 目标网络

1.9.1 代码总括

1.9.2 代码分解

1.10 定义 create_critic_network()

1.10.1 代码总括

1.10.2 代码分解

1.11 定义Critic函数

1.11.1 代码总括

1.11.2 代码分解


1.编写AandC.py

在AandC.py中指定 ActorNetwork 类和 CriticNetwork 类,步骤如下。

1.1 导入包

import tensorflow as tf
import numpy as np
import gym
from gym import wrappers
import argparse
import pprint as pp
import sys

from replay_buffer import ReplayBuffer

这段代码导入了以下几个库,用于使用 DDPG 算法训练强化学习代理:

  • tensorflow(导入为tf):一个用于定义和训练神经网络的流行深度学习库。
  • numpy(导入为np):一个用于进行数值计算的Python库,常用于处理数组和矩阵。
  • gym:一个用于定义和训练强化学习环境(如游戏或机器人任务)的流行库。
  • wrappersgym库中的一个模块,提供了对环境进行包装的附加功能。
  • argparse:一个用于解析命令行参数的Python库,通常用于在训练过程中配置超参数。
  • pprint(导入为pp):一个用于将数据结构以更人类可读的方式打印的模块,常用于以更漂亮的格式打印复杂对象。
  • sys:Python的一个内建模块,提供了与系统交互的功能。

1.2 定义权重和偏置初始化器

winit = tf.contrib.layers.xavier_initializer()
binit = tf.constant_initializer(0.01)
rand_unif = tf.keras.initializers.RandomUniform(minval=-3e-3,maxval=3e-3)
regularizer = tf.contrib.layers.l2_regularizer(scale=0.0)

这段代码定义了一些 TensorFlow 的初始化器和正则化器:

  • tf.contrib.layers.xavier_initializer(): Xavier 初始化器,用于初始化权重矩阵,根据输入和输出的维度自动调整初始权重值,通常在深度神经网络中使用。

  • tf.constant_initializer(0.01): 常量初始化器,将所有权重初始化为固定的常量值 0.01,通常在偏置项中使用。

  • tf.keras.initializers.RandomUniform(minval=-3e-3,maxval=3e-3): 随机均匀分布初始化器,将权重初始化为在指定范围内的均匀分布随机值,这里范围是[-3e-3, 3e-3],通常在连续动作空间的神经网络中使用。

  • tf.contrib.layers.l2_regularizer(scale=0.0): L2 正则化器,用于对模型的权重进行 L2 正则化,其中 scale 参数控制正则化的强度,这里设置为0.0表示不使用正则化。

这些初始化器和正则化器可以在神经网络的层定义中使用,例如通过 tf.get_variabletf.layers 等方法来初始化权重和偏置项,并应用正则化。

1.3 定义ActorNetwork类

1.3.1 代码总括

class ActorNetwork(object):
    """
    DDPG算法中的Actor网络类,用于近似状态到动作的映射。
    """

    def __init__(self, sess, state_dim, action_dim, action_bound, learning_rate, tau, batch_size):
        """
        构造函数,初始化Actor网络。

        参数:
          - sess: TensorFlow会话对象
          - state_dim: 状态空间的维度
          - action_dim: 动作空间的维度
          - action_bound: 动作空间的边界,用于限制输出的动作值
          - learning_rate: 学习率,用于优化器的学习率
          - tau: 目标网络更新时的权重衰减系数
          - batch_size: 批量训练时的样本批次大小
        """

        self.sess = sess
        self.s_dim = state_dim
        self.a_dim = action_dim
        self.action_bound = action_bound
        self.learning_rate = learning_rate
        self.tau = tau
        self.batch_size = batch_size

        # 创建Actor网络
        self.state, self.out, self.scaled_out = self.create_actor_network(scope='actor')

        # 获取Actor网络的参数
        self.network_params = tf.trainable_variables()

        # 创建目标网络
        self.target_state, self.target_out, self.target_scaled_out = self.create_actor_network(scope='act_target')
        # 获取目标网络的参数
        self.target_network_params = tf.trainable_variables()[len(self.network_params):]

这段代码定义了一个 Actor 网络类,用于实现 DDPG(深度确定性策略梯度)算法中的 Actor 模型。

构造函数 __init__ 接受以下参数:

  • sess: TensorFlow 会话对象
  • state_dim: 状态空间的维度
  • action_dim: 动作空间的维度
  • action_bound: 动作空间的边界,用于限制输出的动作值
  • learning_rate: 学习率,用于优化器的学习率
  • tau: 目标网络更新时的权重衰减系数
  • batch_size: 批量训练时的样本批次大小

在构造函数中,进行了以下操作:

  • 创建了 Actor 网络的输入占位符 self.state输出层 self.out 和经过缩放的输出层 self.scaled_out,并通过调用 self.create_actor_network(scope='actor') 方法来定义了 Actor 网络的结构和参数,并将其保存到相应的实例变量中。
  • 创建了目标网络的输入占位符 self.target_state,输出层 self.target_out 和经过缩放的输出层 self.target_scaled_out,并通过调用 self.create_actor_network(scope='act_target') 方法来定义了目标网络的结构和参数,并将其保存到相应的实例变量中。

1.3.2 代码分解

(1)self.sess = sess

self.sess = sess

self.sess 是 Actor 网络对象的 TensorFlow 会话(Session)参数,用于执行 TensorFlow 计算图中的操作和评估张量值。在构造函数中通过 sess 参数进行初始化

在 DDPG 算法中,Actor 网络是用于生成动作值的神经网络模型,其需要在 TensorFlow 计算图中进行前向传播计算。self.sess 参数存储了一个 TensorFlow 会话(Session)对象,通过该会话可以执行计算图中的操作,包括对 Actor 网络的前向传播计算和训练过程中的参数更新等操作。

在训练过程中,通过传入合适的会话对象 sess,可以在训练循环中执行神经网络的前向传播、反向传播和参数更新等操作,从而实现 DDPG 算法的训练过程。通过 self.sess 参数,Actor 网络可以与 TensorFlow 计算图进行交互,从而实现神经网络的计算和参数更新。

(2)self.s_dim = state_dim

self.s_dim = state_dim

self.s_dim 是 Actor 网络对象的状态空间维度属性,用于存储状态空间的维度大小。在构造函数中通过 state_dim 参数进行初始化,state_dim 是传入的状态空间维度参数。

其中,state_dim 是状态空间的维度,表示输入状态的特征数量。在 DDPG 算法中,Actor 网络接受状态作为输入,通过神经网络将状态映射为动作值。self.s_dim 将状态空间的维度大小存储在 Actor 网络对象中,供后续使用。

(3)self.a_dim = action_dim

self.a_dim = action_dim

self.a_dim 是 Actor 网络对象的动作维度属性,用于表示输出的动作值的维度。在构造函数中通过 action_dim 参数进行初始化。

在 DDPG 算法中,Actor 网络的输出是动作值,其维度由 self.a_dim 属性表示。具体而言,self.a_dim 表示 Actor 网络输出动作值的维度,通常与环境中的动作空间维度一致。例如,如果环境中的动作空间维度是2(如二维连续动作空间),那么 self.a_dim 的值应为2,表示 Actor 网络输出的动作值是一个2维的向量。

在训练过程中,Actor 网络通过神经网络模型生成动作值,并将其作为输出传递给 Critic 网络进行评估。self.a_dim 参数确定了 Actor 网络输出动作值的维度,从而影响了神经网络模型的构建和训练过程中的计算。

(4)self.action_bound = action_bound

self.action_bound = action_bound

self.action_bound 是 Actor 网络对象的动作边界参数,用于将网络输出的动作值限制在合理的范围内。在构造函数中通过 action_bound 参数进行初始化,action_bound 是传入的动作边界参数。

在 DDPG 算法中,Actor 网络负责生成连续动作值,但有时动作的取值范围可能需要受限制,例如在机器人控制等任务中,动作值可能需要限制在一定的范围内,以避免超出实际可行的动作范围。self.action_bound 存储了动作边界参数,用于对网络输出的动作值进行限制,确保生成的动作值在合理的范围内,从而保证算法的稳定性和安全性。在训练过程中,通过设置合适的 action_bound 参数,可以控制生成的动作值在合理的范围内,从而适应具体的任务需求。

(5)self.learning_rate = learning_rate

self.learning_rate = learning_rate

self.learning_rate 是 Actor 网络对象的学习率属性,用于表示网络的学习率。在构造函数中通过 learning_rate 参数进行初始化。

学习率是深度神经网络中的一个超参数,用于控制网络权重更新的步长较高的学习率可以导致权重在每次更新时更新较大的幅度,从而加快网络的收敛速度;而较低的学习率则可以使权重更新幅度较小,从而更加稳定地收敛到最优解

在 DDPG 算法中,Actor 网络通过梯度下降法来更新网络权重,学习率决定了每次权重更新的步长。self.learning_rate 参数确定了 Actor 网络的学习率,从而影响了网络在训练过程中的权重更新速度和稳定性。通常情况下,学习率需要经过调优来取得最佳的训练效果。

(6)self.tau = tau

self.tau = tau

self.tau 是 Actor 网络对象的目标网络更新参数,用于控制目标网络的更新速度。在构造函数中通过 tau 参数进行初始化,tau 是传入的目标网络更新参数。

在 DDPG 算法中,使用目标网络来辅助训练,通过更新目标网络的参数来平滑地更新 Actor 网络。self.tau 存储了目标网络更新参数,用于计算目标网络参数的更新量。较小的 tau 值将导致目标网络更新较慢,较大的 tau 值将导致目标网络更新较快。在训练过程中,通过控制 self.tau 的取值,可以控制目标网络的更新速度,从而影响算法的收敛性和性能。

(7)self.batch_size = batch_size

self.batch_size = batch_size

self.batch_size 是 Actor 网络对象的批量大小属性,用于表示每次训练时的样本批量大小。在构造函数中通过 batch_size 参数进行初始化。

批量大小是深度神经网络中的一个超参数,用于控制每次权重更新时使用的样本数量。较大的批量大小可以减少训练过程中的噪声,从而提高训练的稳定性;而较小的批量大小则可以加速训练过程,并减少内存消耗。

在 DDPG 算法中,Actor 网络通过梯度下降法来更新网络权重,每次权重更新时使用的样本数量即为 self.batch_size。较大的批量大小可以减少梯度估计的噪声,从而使网络的权重更新更加稳定,但同时也会增加计算和内存开销。通常情况下,批量大小需要根据具体问题和硬件资源进行调优,以取得最佳的训练效果。

(8)self.state, self.out, self.scaled_out = self.create_actor_network(scope='actor')

self.state, self.out, self.scaled_out = self.create_actor_network(scope='actor')

这一行代码定义了Actor网络的输入、输出和经过动作边界处理后的输出。其中,self.state表示输入状态,self.out表示未经缩放的输出,self.scaled_out表示经过缩放后的输出。它们的具体定义在create_actor_network函数中。

问:scope='actor'?

这里的scope是给create_actor_network函数用的,指定网络的变量作用域。在训练过程中,会有两个Actor网络,一个是实际使用的,另一个是用于计算target Q值的。这两个网络需要共享变量,因此需要在定义变量时通过不同的scope来指定。scope='actor'表示当前是定义Actor网络,对应的变量作用域是actor

(9)self.network_params = tf.trainable_variables()

self.network_params = tf.trainable_variables()

这一行代码会获取Actor网络中可训练的变量的列表,这些变量在网络训练中会被优化。这些变量包括Actor网络中的所有权重和偏置项。

(10)self.target_state, self.target_out, self.target_scaled_out = self.create_actor_network(scope='act_target')

self.target_state, self.target_out, self.target_scaled_out = self.create_actor_network(scope='act_target')

这行代码是创建了一个名为'act_target'的目标网络(Target Network),其中 self.target_state, self.target_out, self.target_scaled_out 是目标网络的输入、输出和经过缩放的输出。目标网络与普通的Actor Network是相同的,只是它们的参数是根据滑动平均的方式由普通的Actor Network更新得到的,这样可以提高算法的稳定性。

(11)self.target_network_params = tf.trainable_variables()[len(self.network_params):]

self.target_network_params = tf.trainable_variables()[len(self.network_params):]

这行代码将目标网络(target network)的可训练变量参数(trainable variables)分配给了 self.target_network_params。目标网络是一个与主网络(actor network)相似的网络,它的目的是用于训练过程中计算 TD 目标,以减少训练过程中的抖动(chattering)和不稳定性(instability)。 

在这里,tf.trainable_variables()返回一个列表,其中包含了所有当前计算图中的可训练变量。因为 self.network_params 中存储了 actor network 的可训练变量参数,所以通过从 tf.trainable_variables() 中获取 self.network_params 的长度来确定需要分配给 self.target_network_params 的变量范围,即从 actor network 的可训练变量参数列表后面开始,一直到列表的末尾。这些变量将会在训练过程中使用梯度下降算法进行优化。

这行代码的作用是获取目标网络(target network)的参数。在这个算法中,Actor-Critic算法会使用两个神经网络,一个是Actor网络,另一个是Critic网络。Actor网络用于决策,Critic网络用于估算价值函数,但Actor网络的决策也受到Critic网络的影响。为了解决过度估算的问题,Actor-Critic算法通常使用两个网络,分别是当前网络和目标网络。这样就可以分别使用当前网络和目标网络来计算动作和目标值,从而减少过度估算的问题。因此,这里通过tf.trainable_variables()方法获取目标网络的参数。在获取到所有的可训练变量后,根据当前网络和目标网络的参数个数区分两个网络的参数。

这行代码将用于创建actor target网络的参数与actor网络参数区分开来。tf.trainable_variables()返回所有可训练的变量列表,其中前一半是actor网络的参数,后一半是actor target网络的参数。因此,使用切片操作,选取后一半的参数列表,即actor target网络的参数列表

问:len(self.network_params): ?

len(self.network_params)表示当前Actor网络中可训练的变量的数量。

tf.trainable_variables()会返回所有可训练的变量列表,这里用len(self.network_params)获得了Actor网络中可训练的变量数量,然后用这个数量作为起点,获得了Target网络中可训练变量的列表,即self.target_network_params。这样做的目的是使得两个网络中的变量分别独立,不会互相影响。

1.4 定义 self.update_target_network_params

1.4.1 代码总括

# update target using tau and 1-tau as weights
self.update_target_network_params = \
    [self.target_network_params[i].assign(tf.multiply(self.network_params[i], self.tau) + tf.multiply(self.target_network_params[i], 1. - self.tau))
        for i in range(len(self.target_network_params))]

# 定义placeholder来接收critic计算出的梯度
self.action_gradient = tf.placeholder(tf.float32, [None, self.a_dim])

# 计算actor gradients
self.unnormalized_actor_gradients = tf.gradients(
    self.scaled_out, self.network_params, -self.action_gradient)
# 对梯度进行缩放,因为之前的梯度可能受到了batch_size的影响
self.actor_gradients = list(map(lambda x: tf.div(x, self.batch_size), self.unnormalized_actor_gradients))

第一部分是将目标网络(target network)的参数更新为当前网络(local network)的参数。这里采用了软更新的方法,即使用一个小的参数tau将当前网络的参数和目标网络的参数进行加权平均。这样做是为了让目标网络的更新更加稳定,避免目标网络的更新太快导致训练不稳定。

第二部分定义了一个placeholder,用于接收critic网络计算出的梯度这个梯度将会被用于计算actor网络的梯度。

第三部分是计算actor网络的梯度,使用了tensorflow自带的gradients函数来计算网络的梯度。这里计算的是scaled_out对network_params的梯度,也就是actor网络的梯度。这里将梯度取反是为了让actor网络朝着最大化Q值的方向更新,因为我们希望actor网络能够让Q值最大化。此外,对梯度进行缩放,是为了让之前梯度的影响得到平衡,避免在batch size较小的情况下,梯度对参数更新的影响过大,导致训练不稳定。

1.4.2 代码分解

(1)self.update_target_network_params = \ [self.target_network_params[i].assign(tf.multiply(self.network_params[i], self.tau) + tf.multiply(self.target_network_params[i], 1. - self.tau)) for i in range(len(self.target_network_params))]

self.update_target_network_params = \
            [self.target_network_params[i].assign(tf.multiply(self.network_params[i], self.tau) +
                            tf.multiply(self.target_network_params[i], 1. - self.tau))
                for i in range(len(self.target_network_params))]

这段代码实现了actor网络参数的soft更新,目的是让目标actor网络向当前actor网络靠近。

soft更新是指将目标网络的参数按一定比例更新为当前网络参数,具体更新方式如下: 对于actor网络中的每一个参数,在目标actor网络和当前actor网络的相应参数之间,按照一定的比例进行更新。其中,tau是一个超参数,用来控制目标网络的更新速度,一般取比较小的值,如0.001。

这段代码的具体实现是用tf.multiply来实现两个参数的加权更新,其中tf.multiply(self.network_params[i], self.tau)表示当前网络参数的权重,tf.multiply(self.target_network_params[i], 1. - self.tau)表示目标网络参数的权重。

最终更新后的目标网络参数可以通过self.target_network_params获取。

问:for i in range(len(self.target_network_params)) ?

这是一个循环语句,它的目的是遍历 self.target_network_params 列表中的所有元素。

在这里,它被用来对 self.target_network_params 列表中的每个元素执行一个赋值操作,用来更新目标 Actor 网络的参数。这个循环中的 i 变量是一个计数器,它从 0 开始计数,每次循环增加 1,直到遍历完整个 self.target_network_params 列表。

这个语句中,len(self.target_network_params) 返回 self.target_network_params 列表的长度,即神经网络中 target actor 的所有可训练变量的数量。for i in range(len(self.target_network_params)) 为这个长度值建立一个循环,使得在每次循环迭代时,i 都是一个从0到 len(self.target_network_params)-1 的整数,从而可以用它来索引列表中的元素。

问:tf.multiply(self.network_params[i], self.tau) ?

tf.multiply 是 TensorFlow 中的一个函数,用于对两个数进行逐元素相乘。在这里,self.network_params[i] 表示 actor 网络参数中的第 i 个元素,self.tau 是一个超参数,这里表示指定的权重。因此,tf.multiply(self.network_params[i], self.tau) 的作用是将 actor 网络中的第 i 个参数与权重 tau 相乘。

示例:

假设 self.network_params[i] 是一个形状为 (4, 4) 的张量,其中每个元素的值如下:

[[ 0.2,  0.3,  0.4,  0.5],
 [ 0.1,  0.2,  0.3,  0.4],
 [ 0.5,  0.4,  0.3,  0.2],
 [-0.2, -0.3, -0.4, -0.5]]

假设 self.tau 是一个标量,比如说 0.1,那么 tf.multiply(self.network_params[i], self.tau) 将会将每个元素乘以 0.1,即得到如下张量:

[[ 0.02,  0.03,  0.04,  0.05],
 [ 0.01,  0.02,  0.03,  0.04],
 [ 0.05,  0.04,  0.03,  0.02],
 [-0.02, -0.03, -0.04, -0.05]]

这里实际上就是将 self.network_params[i] 中的每个元素乘以 self.tau

问:张量?

在 TensorFlow 中,张量(Tensor)是表示为多维数组的数据结构。可以将张量想象为一个 N 维的数组或列表。例如,一个标量可以表示为一个零维的张量,也可以称为常量。一个向量可以表示为一个一维的张量,一个矩阵可以表示为一个二维的张量,以此类推。

在 TensorFlow 中,所有的数据都表示为张量。可以对张量进行各种数学操作,例如加法、乘法等,也可以对张量进行变形,例如重塑、转置等。

问:assign() ?

assign() 是 TensorFlow 中的一个方法,用于给变量赋新的值。在这个代码中,self.target_network_params[i] 是目标策略网络中的变量,它会被赋值为一个由当前策略网络参数和目标策略网络参数线性插值得到的新值。新值的计算方法是:将当前策略网络参数乘以权重 tau,将目标策略网络参数乘以权重 1-tau,然后将它们相加。这个新值就成为了目标策略网络参数的新值。

(2)self.action_gradient = tf.placeholder(tf.float32, [None, self.a_dim])

self.action_gradient = tf.placeholder(tf.float32, [None, self.a_dim])

这段代码定义了一个 TensorFlow 占位符 (placeholder),用于在训练神经网络时传入动作梯度。动作梯度在这里是一个大小为 [None, self.a_dim] 的浮点型张量,其中 None 表示可以动态指定样本数量,self.a_dim 表示动作空间的维度。在训练过程中,这个占位符将被填充为对应的动作梯度张量,从而计算出相应的 actor gradients。

问:[None, self.a_dim] 和 action_gradient 是等价的吗?

[None, self.a_dim]action_gradient 是不等价的。[None, self.a_dim] 是一个占位符的形状,表示该占位符将被期望包含一个二维张量,第一维可以是任何大小(即它是一个不确定的维度),第二维的大小为 self.a_dim。而 action_gradient 是一个具体的张量,它将被传递到该占位符的值。可以将 action_gradient 视为占位符的值。

(3)self.unnormalized_actor_gradients = tf.gradients( self.scaled_out, self.network_params, -self.action_gradient)

这段代码计算了 actor 模型的梯度(即策略梯度),具体来说,它使用了 TensorFlow 的 tf.gradients 函数来计算 self.scaled_outself.network_params 的导数,并乘以 -self.action_gradient。其中,self.scaled_out 是经过 actor 模型计算出来的动作值,self.network_params 是 actor 模型的网络参数,self.action_gradient 是从 critic 模型传来的动作值梯度。该计算结果给出了 actor 模型中每个网络参数在当前状态下对策略的影响程度(即梯度值)。这里得到的是未归一化的梯度值,后面需要进行归一化处理。

tf.gradients 是 TensorFlow 中计算梯度的函数,它的返回值是一个列表列表的长度等于 self.network_params 中张量的数量每个张量对应列表中的一个元素。这里的 self.scaled_out 是 actor 模型的输出,self.network_params 是 actor 模型的参数,-self.action_gradient 是 critic 模型的动作值梯度取反。所以这个表达式的含义是求 self.scaled_outself.network_params 每个参数的梯度,且这个梯度要乘上 -self.action_gradient。得到的结果 self.unnormalized_actor_gradients 是一个列表,列表中的每个元素都是一个张量,它表示了 self.scaled_out 对某个参数的梯度,且这个梯度要乘上 -self.action_gradient

(4)self.actor_gradients = list(map(lambda x: tf.div(x, self.batch_size), self.unnormalized_actor_gradients))

self.actor_gradients = list(map(lambda x: tf.div(x, self.batch_size), self.unnormalized_actor_gradients))

这段代码将无正则化梯度转化为最终的 actor 梯度,通过将无正则化梯度除以批次大小得到。这可以理解为计算平均梯度,以避免使用不同批次大小的训练导致梯度大小的不稳定性。

具体来说,使用 list(map(lambda x: tf.div(x, self.batch_size), self.unnormalized_actor_gradients)) 将无正则化梯度 self.unnormalized_actor_gradients 转化为最终的 actor 梯度 self.actor_gradients,其中 tf.div(x, self.batch_size) 表示将输入的张量 x 除以批次大小 self.batch_size

这段代码是将self.unnormalized_actor_gradients中的所有元素分别除以self.batch_size,然后将结果存储在一个列表中。tf.div(x, self.batch_size)表示对张量x中的每个元素都进行除法运算,除数为self.batch_size。使用map()函数将这个操作应用于self.unnormalized_actor_gradients中的每个元素,返回一个迭代器对象,最后用list()函数将迭代器对象转换成一个列表,即为self.actor_gradients

1.5 使用Adam优化

使用Adam优化来应用策略梯度优化actor

        # adam optimization 
        self.optimize = tf.train.AdamOptimizer(self.learning_rate).apply_gradients(zip(self.actor_gradients, self.network_params))

        # num trainable vars
        self.num_trainable_vars = len(self.network_params) + len(self.target_network_params)

 (1)self.optimize=tf.train.AdamOptimizer(self.learning_rate).apply_gradients(zip(self.actor_gradients, self.network_params))

self.optimize = tf.train.AdamOptimizer(self.learning_rate).apply_gradients(zip(self.actor_gradients, self.network_params))

self.optimize 是用 Adam 优化器来更新 self.network_params 的梯度,其中梯度是由 self.actor_gradientsself.network_params 组成的元组对(即对应位置的元素组成的元组对)zip() 函数将它们打包成可迭代的对象apply_gradients() 函数对这些梯度进行计算并将它们应用到网络参数中,实现参数的更新。

(2)self.num_trainable_vars = len(self.network_params) + len(self.target_network_params)

self.num_trainable_vars = len(self.network_params) + len(self.target_network_params)

self.num_trainable_vars 表示可训练变量的数量,是指策略网络和目标网络中所有可训练参数的总数。在这段代码中,它的计算方式是将策略网络参数和目标网络参数的长度相加。

1.6 定义create_actor_network()函数

1.6.1 代码总括

def create_actor_network(self, scope):
    # 定义 Actor 网络结构,输入状态
    with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
        state = tf.placeholder(name='a_states', dtype=tf.float32, shape=[None, self.s_dim])
        
        # 第一层隐藏层,400 个神经元
        net = tf.layers.dense(inputs=state, units=400, activation=None, 
                              kernel_initializer=winit, bias_initializer=binit, name='anet1') 
        net = tf.nn.relu(net)

        # 第二层隐藏层,300 个神经元
        net = tf.layers.dense(inputs=net, units=300, activation=None, 
                              kernel_initializer=winit, bias_initializer=binit, name='anet2')
        net = tf.nn.relu(net)

        # 输出层,输出动作值
        out = tf.layers.dense(inputs=net, units=self.a_dim, activation=None, 
                              kernel_initializer=rand_unif, bias_initializer=binit, name='anet_out')     
        out = tf.nn.tanh(out)
        scaled_out = tf.multiply(out, self.action_bound)  # 输出动作值在动作空间内
        return state, out, scaled_out

这是一个创建 actor 网络的函数,它接受一个作用域 scope,并在该作用域下创建相应的神经网络。具体来说,这个函数使用 tf.variable_scope() 定义一个变量作用域。在该作用域下,函数创建一个占位符 state,表示输入的状态。然后通过 tf.layers.dense() 函数定义了两个隐藏层和一个输出层,其中隐藏层使用 ReLU 激活函数,输出层不使用激活函数。输出层的输出被压缩到 [-1, 1] 的范围内,乘以 action_bound 得到最终的动作值。函数返回 state、out 和 scaled_out 三个变量,分别表示状态输入、原始输出和压缩后的输出。

1.6.2 代码分解

(1)with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):

with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):

tf.variable_scope(scope, reuse=tf.AUTO_REUSE) 用于定义一个变量作用域,并指定变量重用策略。在 TensorFlow 中,变量作用域主要用于管理变量的命名和作用域。通过将变量放入作用域中,我们可以更好地管理和组织它们。同时,当需要重用某个作用域中的变量时,我们可以通过指定 reuse=True 来直接获取该作用域中的变量,避免了变量重复定义的问题。tf.AUTO_REUSE 则表示在需要重用该变量作用域时,如果该作用域已经被创建过,就直接重用;如果该作用域不存在,就创建一个新的作用域。

(2)state = tf.placeholder(name='a_states', dtype=tf.float32, shape=[None, self.s_dim])

state = tf.placeholder(name='a_states', dtype=tf.float32, shape=[None, self.s_dim])

这一行代码定义了一个占位符 state用于接收 Actor 模型的输入状态

其中:

  • name='a_states' 指定了该占位符的名称为 a_states
  • dtype=tf.float32 指定了该占位符的数据类型为 float32。
  • shape=[None, self.s_dim] 指定了该占位符的形状为 (None, self.s_dim),其中 None 表示输入状态的数量可以是任意值,self.s_dim 表示每个状态向量的维度。

(3)net = tf.layers.dense(inputs=state, units=400, activation=None, kernel_initializer=winit, bias_initializer=binit, name='anet1')

net = tf.layers.dense(inputs=state, units=400, activation=None, 
                              kernel_initializer=winit, bias_initializer=binit, name='anet1') 

这一段代码是使用 TensorFlow 构建 actor 网络的第一层,其中输入是 state,输出是 400 个神经元。 tf.layers.dense() 是一个高级函数,它为我们完成了变量的声明和权重和偏差的初始化,并在输入上应用一个密集层的变换activation 参数指定激活函数为无激活函数。kernel_initializerbias_initializer 参数指定权重和偏差的初始化方式,name 参数指定变量作用域下该层的名称。

(4)net = tf.nn.relu(net)

net = tf.nn.relu(net)

这行代码实现的是对 net 这个张量执行 ReLU 激活函数。ReLU 是一种非线性的激活函数,其将输入变换成其正值,负值变为0。常用于深度神经网络中,以提高其收敛速度和表达能力。

(5)net = tf.layers.dense(inputs=net, units=300, activation=None, kernel_initializer=winit, bias_initializer=binit, name='anet2')

 net = tf.layers.dense(inputs=net, units=300, activation=None, 
                              kernel_initializer=winit, bias_initializer=binit, name='anet2')

这行代码定义了一个具有300个神经元的全连接层,输入为net。这里使用了与之前一样的tf.layers.dense()函数,不同的是输出神经元的数量为300,层的名称为'anet2',激活函数为None,表示不使用激活函数。权重和偏置的初始化方法与之前相同。

(6)out = tf.layers.dense(inputs=net, units=self.a_dim, activation=None, kernel_initializer=rand_unif, bias_initializer=binit, name='anet_out')

out = tf.layers.dense(inputs=net, units=self.a_dim, activation=None, 
                              kernel_initializer=rand_unif, bias_initializer=binit, name='anet_out')

这一行代码定义了 actor 网络的输出层。使用了 tf.layers.dense 函数,其中:

  • inputs:输入,也就是前面通过两个全连接层得到的输出
  • units:输出神经元个数,这里为 self.a_dim,即动作维度数
  • activation:激活函数,这里为 None
  • kernel_initializer:权重初始化方法,这里为 rand_unif,即均匀分布
  • bias_initializer:偏置初始化方法,这里为 binit,即常数值
  • name:名称,这里为 'anet_out'

这一层输出的是动作的取值,但这些值可能会超出动作空间的范围,所以需要对其进行缩放,使其取值在动作空间内。因此,下一行代码进行了缩放操作。

(7)scaled_out = tf.multiply(out, self.action_bound)

scaled_out = tf.multiply(out, self.action_bound)

这一行代码是将输出层的输出进行缩放,以适应该环境中动作的范围。具体来说,out 是输出层的输出,self.action_bound 是一个常数,表示在该环境下动作的上下界。通过将 out 乘以 self.action_bound,可以将输出值缩放到正确的范围内,从而得到可用于执行动作的值。

1.7 定义 Actor 函数

1.7.1 代码总括

    def train(self, state, a_gradient):
        self.sess.run(self.optimize, feed_dict={self.state: state, self.action_gradient: a_gradient})

    def predict(self, state):
        return self.sess.run(self.scaled_out, feed_dict={
            self.state: state})

    def predict_target(self, state):
        return self.sess.run(self.target_scaled_out, feed_dict={
            self.target_state: state})

    def update_target_network(self):
        self.sess.run(self.update_target_network_params)

    def get_num_trainable_vars(self):
        return self.num_trainable_vars

1.7.2 代码分解

    def train(self, state, a_gradient):
        self.sess.run(self.optimize, feed_dict={self.state: state, self.action_gradient: a_gradient})

这是Actor网络的训练函数。在 输入状态(state) 和 动作梯度(a_gradient) 的情况下,通过feed_dict提供给优化器(self.optimize)来更新Actor网络中的参数。具体地说,将状态和动作梯度提供给优化器后,会根据指定的优化算法对网络中的参数进行更新,使得Actor网络能够更好地预测状态对应的动作。

    def predict(self, state):
        return self.sess.run(self.scaled_out, feed_dict={
            self.state: state})

该函数用于预测Actor网络的输出动作值。给定一个状态,它会返回Actor网络的输出动作值。

参数:state: 状态值,形状为 [batch_size, state_dim]

返回值:预测的动作值,形状为 [batch_size, action_dim]

这是一个类的方法,用于预测给定状态的动作。输入参数state是一个numpy数组,代表当前的状态,函数会将其喂入神经网络并返回对应的动作,这里使用了类成员变量self.scaled_out作为输出节点,也就是经过缩放后的动作值。函数内部会调用TensorFlow会话self.sessrun方法来运行神经网络计算。

在 TensorFlow 中,可以使用 feed_dict 参数 将 Python 对象的值传递给 TensorFlow 的占位符张量,这些占位符张量在 TensorFlow 计算图中表示为 tf.placeholder() 函数的调用。在这种情况下,feed_dict 是一个字典,将占位符张量映射到 Python 对象。在这里,feed_dict 将 self.state 占位符张量映射到输入的 state 数据。

    def predict_target(self, state):
        return self.sess.run(self.target_scaled_out, feed_dict={
            self.target_state: state})

predict_target 方法用于从 目标网络中预测状态 state 对应的动作输出。与 predict 方法类似,它也是通过调用 sess.run() 函数来执行 TensorFlow 计算图,其中 self.target_scaled_out 是目标网络的输出层张量,self.target_state 占位符,通过 feed_dict 参数将 state 传入 TensorFlow 计算图中。通过这种方式,可以获得目标网络在给定状态下的动作输出。

    def update_target_network(self):
        self.sess.run(self.update_target_network_params)

update_target_network方法是用来更新目标网络的参数的。在DDPG算法中,有两个神经网络,一个是Actor网络,一个是Critic网络。这里的目标网络就是Critic网络的另外一个拷贝。每隔一定的时间,我们会把当前Critic网络的参数拷贝到目标网络中,以使目标网络能够更好地反映Critic网络的学习进度,从而提高DDPG算法的性能。

    def get_num_trainable_vars(self):
        return self.num_trainable_vars

get_num_trainable_vars()是一个方法,用于获取该Actor网络中可训练的变量数量。在初始化Actor时,num_trainable_vars被计算为可训练变量的数量,并存储在对象属性中。该方法只是返回该属性的值。

1.8 定义CriticNetwork 类

1.8.1 代码总括

class CriticNetwork(object):
    def __init__(self, sess, state_dim, action_dim, learning_rate, tau, gamma, num_actor_vars):
        """        
        Args:
            sess: TensorFlow会话对象
            state_dim: 状态空间的维度
            action_dim: 动作空间的维度
            learning_rate: 模型的学习率
            tau: 软更新参数
            gamma: 折扣因子
            num_actor_vars: 指定在变量列表中Actor网络的参数数量,以便从变量列表中提取Critic网络的参数
            
        Returns:
            无返回值
        """
        self.sess = sess
        self.s_dim = state_dim
        self.a_dim = action_dim
        self.learning_rate = learning_rate
        self.tau = tau
        self.gamma = gamma

        # 创建Critic网络
        self.state, self.action, self.out = self.create_critic_network(scope='critic')

        # Critic网络的参数  
        self.network_params = tf.trainable_variables()[num_actor_vars:]

        # 创建Target Network
        self.target_state, self.target_action, self.target_out = self.create_critic_network(scope='crit_target')

        # Target Network的参数
        self.target_network_params = tf.trainable_variables()[(len(self.network_params) + num_actor_vars):]

1.8.2 代码分解

(1)self.state, self.action, self.out = self.create_critic_network(scope='critic')

self.state, self.action, self.out = self.create_critic_network(scope='critic')

这行代码创建了Critic网络,并返回网络的输入,输出和网络本身。其中,self.state表示Critic网络的输入状态,self.action表示Critic网络的输入动作,self.out表示Critic网络的输出值。self.create_critic_network(scope='critic')则是调用一个函数来创建Critic网络,scope='critic'指定网络的名称作为参数传递给函数。

(2)self.network_params = tf.trainable_variables()[num_actor_vars:]

 self.network_params = tf.trainable_variables()[num_actor_vars:]

这行代码的作用是获取critic网络中可训练的参数(trainable variables),并将它们存储在self.network_params中。具体来说,tf.trainable_variables()会返回当前计算图中所有可训练的变量,而num_actor_vars指的是actor网络中可训练参数的数量,因此self.network_params中存储的是critic网络的可训练参数。

[num_actor_vars:]表示从索引为num_actor_vars的可训练变量开始,到列表末尾的所有可训练变量组成的子列表。换句话说,这里提取了模型的critic部分所包含的所有可训练变量。

(3)self.target_state, self.target_action, self.target_out = self.create_critic_network(scope='crit_target')

self.target_state, self.target_action, self.target_out = self.create_critic_network(scope='crit_target')

在 CriticNetwork 类中,这行代码创建了一个 target network,用于更新 critic network。其中,self.create_critic_network(scope='crit_target') 会创建一个与 critic network 结构相同但参数不同的 target network。self.target_stateself.target_actionself.target_out 分别是 target network 的输入 state、输入 action 和输出值。

(4)self.target_network_params = tf.trainable_variables()[(len(self.network_params) + num_actor_vars):]

self.target_network_params = tf.trainable_variables()[(len(self.network_params) + num_actor_vars):]

这行代码用于获取 target network 的可训练参数(trainable variables)。tf.trainable_variables() 返回所有可训练的变量,包括 critic network 和 target network 的参数。len(self.network_params) 表示 critic network 的参数数量,num_actor_vars 是 actor network 的参数数量。因此,(len(self.network_params) + num_actor_vars) 表示从 target network 参数列表中开始的位置,而 tf.trainable_variables()[(len(self.network_params) + num_actor_vars):] 则表示从该位置开始的所有参数,即为 target network 的参数列表。这些参数将在更新 target network 时使用。

这段代码是用来获取target network的参数的。其中self.network_params是critic network的参数,num_actor_vars是actor network的参数数目。因此,(len(self.network_params) + num_actor_vars)表示actor和critic网络的参数总数。然后通过tf.trainable_variables()函数获取所有可训练的参数,并从总参数数目开始切片获取target network的参数。

1.9 critic 目标网络

1.9.1 代码总括

# 更新target网络参数
self.update_target_network_params = \
    [self.target_network_params[i].assign(tf.multiply(self.network_params[i], self.tau) \
    + tf.multiply(self.target_network_params[i], 1. - self.tau))
        for i in range(len(self.target_network_params))]

# 网络目标 (y_i in the paper)
self.predicted_q_value = tf.placeholder(tf.float32, [None, 1])

# adam 优化器;最小化 L2 损失函数
self.loss = tf.reduce_mean(tf.square(self.predicted_q_value - self.out))
self.optimize = tf.train.AdamOptimizer(self.learning_rate).minimize(self.loss)

# Q值关于动作的梯度
self.action_grads = tf.gradients(self.out, self.action)

这段代码是critic网络的构建函数的一部分,它实现了以下功能:

  • 定义了更新目标网络参数的操作(update_target_network_params)
  • 定义了预测Q值的placeholder(predicted_q_value)
  • 定义了loss函数和优化器(Adam)(loss和optimize)
  • 计算Q值相对于动作的梯度(action_grads)

1.9.2 代码分解

(1)self.update_target_network_params = \ [self.target_network_params[i].assign(tf.multiply(self.network_params[i], self.tau) \ + tf.multiply(self.target_network_params[i], 1. - self.tau)) for i in range(len(self.target_network_params))]

self.update_target_network_params = \
    [self.target_network_params[i].assign(tf.multiply(self.network_params[i], self.tau) \
    + tf.multiply(self.target_network_params[i], 1. - self.tau))
        for i in range(len(self.target_network_params))]

这段代码是用于更新目标网络参数的操作,其功能如下:

  • self.target_network_params[i]: 获取目标网络参数列表中的第i个参数
  • self.network_params[i]: 获取当前网络参数列表中的第i个参数
  • tf.multiply(self.network_params[i], self.tau): 将当前网络参数乘以tau作为权重
  • tf.multiply(self.target_network_params[i], 1. - self.tau): 将目标网络参数乘以(1 - tau)作为权重
  • self.target_network_params[i].assign(...): 将目标网络参数更新为上述两个加权和的和

整体来说,这段代码的目的是通过将当前网络参数与目标网络参数按照一定的权重进行加权和的方式,来更新目标网络的参数。其中,tau是用于控制当前网络参数对目标网络参数的权重,其取值范围为0到1,通常设置为较小的值,以实现平滑的参数更新。

(2)self.predicted_q_value = tf.placeholder(tf.float32, [None, 1])

self.predicted_q_value = tf.placeholder(tf.float32, [None, 1])

这行代码创建了一个 TensorFlow 占位符,它将用于传递目标 Q 值(y_i)。它是一个形状为 [None, 1] 的二维张量,其中第一个维度是 batch 大小,而第二个维度是目标 Q 值的数量,这里只有一个值,因为是单一动作。

这段代码创建了一个 TensorFlow 占位符(placeholder),其数据类型是 float32,形状为 [None, 1]。其中 None 表示第一个维度可以为任意长度,1 表示第二个维度为长度 1。

这个占位符在后续的训练过程中,会被填充上训练数据,用来表示神经网络的输出值。

(3)self.loss = tf.reduce_mean(tf.square(self.predicted_q_value - self.out))

self.loss = tf.reduce_mean(tf.square(self.predicted_q_value - self.out))

这里定义了critic网络的loss函数,用于评估critic网络的输出和目标Q值的差异,即Q值误差。具体来说,self.predicted_q_value是一个占位符,表示目标Q值,self.out表示critic网络的输出,tf.square是平方函数,tf.reduce_mean是平均值函数。这一行代码的作用是计算当前的critic网络输出(self.out)与预测Q值(self.predicted_q_value)之间的均方误差(MSE)。具体来说,它首先计算两者差值的平方(使用 tf.square),然后将所有差值的平方求和并求取平均值(使用 tf.reduce_mean),这个平均值即为当前批次的MSE。这个MSE将被用于计算梯度和更新网络参数。

(4)self.optimize = tf.train.AdamOptimizer(self.learning_rate).minimize(self.loss)

self.optimize = tf.train.AdamOptimizer(self.learning_rate).minimize(self.loss)

这一行定义了一个Adam优化器,用来最小化损失函数self.loss,优化器的学习率为self.learning_rate。具体来说,它会根据当前的梯度值对网络参数进行更新。Adam优化器是梯度下降算法的一种变种,其主要优点是在处理大规模数据集时可以快速收敛,并且可以自适应调整学习率。

1.10 定义 create_critic_network()

1.10.1 代码总括

    def create_critic_network(self, scope):
        # 构建一个评论家网络,接收状态和动作作为输入,输出当前状态下选择某个动作的Q值

        # 定义状态和动作的placeholder
        with tf.variable_scope(scope, reuse=tf.AUTO_REUSE):
            state = tf.placeholder(name='c_states', dtype=tf.float32, shape=[None, self.s_dim])  
            action = tf.placeholder(name='c_action', dtype=tf.float32, shape=[None, self.a_dim]) 

            # 把状态和动作连接起来作为输入
            net = tf.concat([state, action], 1) 

            # 第一层全连接层,输出大小为400,使用ReLU激活函数
            net = tf.layers.dense(inputs=net, units=400, activation=None, 
                                  kernel_initializer=winit, bias_initializer=binit, name='cnet1') 
            net = tf.nn.relu(net)

            # 第二层全连接层,输出大小为300,使用ReLU激活函数
            net = tf.layers.dense(inputs=net, units=300, activation=None, 
                                  kernel_initializer=winit, bias_initializer=binit, name='cnet2') 
            net = tf.nn.relu(net)

            # 输出层,大小为1,不使用激活函数
            out = tf.layers.dense(inputs=net, units=1, activation=None, 
                                  kernel_initializer=rand_unif, bias_initializer=binit, name='cnet_out')     
            return state, action, out

这段代码实现了一个Critic神经网络模型。模型的输入包含一个状态state和一个动作action,其维度分别是self.s_dim和self.a_dim。首先将state和action拼接在一起得到net,并通过两个隐藏层(分别包含400个和300个神经元)进行计算,中间使用ReLU激活函数。最后通过一个神经元输出层输出一个值out,表示Q-value的估计。该函数返回state, action, out三个张量,分别代表输入状态、输入动作和输出值。

1.10.2 代码分解

(1)net = tf.concat([state, action],1)

 net = tf.concat([state, action],1) 

这行代码将状态和动作按照列的维度(axis=1)拼接在一起,形成一个新的张量net。其中,state是critic网络的输入状态,action是actor网络输出的动作。这里的拼接是为了在critic网络中将状态和动作进行联合处理。

示例:

当把两个形状为 [None, n][None, m] 的张量沿着第二个维度连接时,它们会变成一个形状为 [None, n+m] 的张量。

下面是一个例子:

import tensorflow as tf

# 创建两个形状为 [None, 2] 的张量
x = tf.placeholder(tf.float32, shape=[None, 2])
y = tf.placeholder(tf.float32, shape=[None, 2])

# 把两个张量按第二个维度连接起来
z = tf.concat([x, y], axis=1)

# 输出连接后的张量的形状
print(z.get_shape())

输出为:

(?, 4)

其中 ? 表示这个维度的大小可以是任意的,这取决于在运行时提供给张量的实际数据。

(2)out = tf.layers.dense(inputs=net, units=1, activation=None, kernel_initializer=rand_unif, bias_initializer=binit, name='cnet_out')

out = tf.layers.dense(inputs=net, units=1, activation=None, 
                                  kernel_initializer=rand_unif, bias_initializer=binit, name='cnet_out')

这行代码定义了一个全连接层,输入是net,输出是大小为1的张量,即每个样本对应一个Q值。具体解释如下:

  • inputs=net:输入为net,即上一层的输出。
  • units=1:输出张量的大小是1,即每个样本对应一个Q值。
  • activation=None:不使用激活函数,即直接输出线性变换的结果。
  • kernel_initializer=rand_unif:权重矩阵的初始化方法使用随机均匀分布,rand_unif是一种自定义的初始化函数。
  • bias_initializer=binit:偏置的初始化方法使用常数初始化,具体数值由binit定义。
  • name='cnet_out':这一层的名称是cnet_out。

在深度强化学习中,Critic网络被用来估计状态-动作值函数Q(s, a),其中Q(s, a)表示在状态s下执行动作a所得到的累积奖励的期望值。因此,Critic网络的输出是对Q值的估计。

1.11 定义Critic函数

1.11.1 代码总括

    def train(self, state, action, predicted_q_value):
        return self.sess.run([self.out, self.optimize], feed_dict={self.state: state, self.action: action, self.predicted_q_value: predicted_q_value})

    def predict(self, state, action):
        return self.sess.run(self.out, feed_dict={self.state: state, self.action: action})

    def predict_target(self, state, action):
        return self.sess.run(self.target_out, feed_dict={self.target_state: state, self.target_action: action})

    def action_gradients(self, state, actions):
        return self.sess.run(self.action_grads, feed_dict={self.state: state, self.action: actions})

    def update_target_network(self):
        self.sess.run(self.update_target_network_params)

这段代码定义了 Critic 类的许多方法,包括训练(train)、预测(predict)、预测目标(predict_target)、动作梯度(action_gradients)和更新目标网络(update_target_network)。

  • train 方法执行一次优化步骤,用于更新神经网络的参数,输入包括当前状态 state、动作 action 和预测的 Q 值 predicted_q_value。其中,优化器的选择是 Adam 优化器,通过 self.optimize 引用。函数返回值为训练后的 Q 值以及执行的优化步骤。
  • predict 方法根据输入的当前状态 state 和动作 action,返回当前策略下的 Q 值。
  • predict_target 方法根据输入的当前状态 state 和动作 action,返回目标策略下的 Q 值。
  • action_gradients 方法根据输入的当前状态 state 和动作 actions,返回当前策略下动作的梯度。
  • update_target_network 方法用于更新目标网络的参数。此处使用的更新方法是软更新(soft update),在更新目标网络的参数时,先按照当前网络的参数以一定的比例更新目标网络的参数,再以另一定比例保留之前的目标网络参数。

1.11.2 代码分解

(1)def action_gradients(self, state, actions): return self.sess.run(self.action_grads, feed_dict={self.state: state, self.action: actions})

def action_gradients(self, state, actions):
        return self.sess.run(self.action_grads, feed_dict={self.state: state, self.action: actions})

这个函数用于计算动作的梯度。在DQN算法中,用来更新策略的是状态的梯度,而在DDPG算法中,用来更新策略的是动作的梯度。因此,在DDPG算法中,需要计算动作的梯度,以便更新策略。这个函数的输入是状态和动作,输出是动作的梯度。

问:feed_dict={self.state: state, self.action: actions} ?

feed_dict参数是一个字典,它将placeholder(占位符)张量映射到实际的数据值。在这个函数中,self.stateself.action是两个占位符张量,用于接收神经网络输入的状态和动作,stateactions是实际的输入数据。在这里,feed_dict指定了占位符张量和对应的输入数据,这样神经网络就可以利用这些输入数据进行推理和训练。

猜你喜欢

转载自blog.csdn.net/aaaccc444/article/details/130212435