当前位置: 首页 > news >正文

【深度强化学习】(6) PPO 模型解析,附Pytorch完整代码

大家好,今天和各位分享一下深度强化学习中的近端策略优化算法(proximal policy optimization,PPO),并借助 OpenAI 的 gym 环境完成一个小案例,完整代码可以从我的 GitHub 中获得:

https://github.com/LiSir-HIT/Reinforcement-Learning/tree/main/Model


1. 算法原理

PPO 算法之所以被提出,根本原因在于 Policy Gradient 在处理连续动作空间时 Learning rate 取值抉择困难。Learning rate 取值过小,就会导致深度强化学习收敛性较差,陷入完不成训练的局面,取值过大则导致新旧策略迭代时数据不一致,造成学习波动较大或局部震荡。除此之外,Policy Gradient 因为在线学习的性质,进行迭代策略时原先的采样数据无法被重复利用,每次迭代都需要重新采样

同样地置信域策略梯度算法(Trust Region Policy Optimization,TRPO)虽然利用重要性采样(Important-sampling)、共轭梯度法求解提升了样本效率、训练速率等,但在处理函数的二阶近似时会面临计算量过大,以及实现过程复杂、兼容性差等缺陷。 

PPO 算法具备 Policy Gradient、TRPO 的部分优点采样数据和使用随机梯度上升方法优化代替目标函数之间交替进行,虽然标准的策略梯度方法对每个数据样本执行一次梯度更新,但 PPO 提出新目标函数,可以实现小批量更新。

鉴于上述问题,该算法在迭代更新时,观察当前策略在 t 时刻智能体处于状态 s 所采取的行为概率\pi (a_t |s_t),与之前策略所采取行为概率 \pi_{\theta old} (a_t | s_t)计算概率的比值来控制新策略更新幅度,比值 r_t 记作:

r_t(\theta) = \frac{\pi _{\theta}(a_t|s_t)}{\pi_{\theta old}(a_t|s_t)}

新旧策略差异明显且优势函数较大,则适当增加更新幅度;若 r_t 比值越接近 1,表明新旧策略差异越小。

优势函数代表,在状态 s 下,行为 a 相对于均值的偏差。在论文中,优势函数 \hat{A}_t 使用 GAE(generalized advantage estimation)来计算:

\hat{A}_t^{GAE(\gamma, \lambda)} = \sum_{l=0}^{\bowtie } (\gamma \lambda )^l \delta _{t+l} ^ V

\hat{A}_t^{GAE(\gamma, \lambda)} = \delta _t^V + (\gamma \lambda) \delta _{t+1}^V + (\gamma \lambda)^2 \delta _{t+2}^V + \cdots + (\gamma \lambda)^{T-t+1} \delta_{T+1}

\delta _t^V = r_t + \gamma V_\omega (s_{t+1}) - V_\omega (s_t)

PPO 算法可依据 Actor 网络的更新方式细化为含有自适应 KL-散度(KL Penalty)的 PPO-Penalty 含有 Clippped Surrogate Objective 函数的 PPO-Clip

(1)PPO-Penalty 基于KL 惩罚项优化目标函数,实验证明惩罚项系数 \beta 在迭代过程中并非固定值,需要动态调整惩罚权重,其目标函数 L 可以定义为:

L^{KLPEN} (\theta) = \hat{E} [\frac{\pi_{\theta}(a_t|s_t)}{\pi_{\theta old}(a_t|s_t)} \hat{A}_t - \beta KL [\pi_{\theta old}(\cdot |s_t), \pi_{\theta}(\cdot | s_t)]]

惩罚项 \beta 的初始值的选择对算法几乎无影响,原因是它能在每次迭代时依据新旧策略的 KL 散度做适宜调整,首先设置 KL 散度阈值 d_{targ},再通过下面的表达式计算 d

d = \hat{E} [KL[\pi_{\theta old}(\cdot |s_t), \pi_\theta (\cdot | s_t)]]

如果 d<d_{targ} / 1.5 时,证明散度较小,需要弱化惩罚力度,\beta 调整为 \beta /2

如果 d>d_{targ} \times 1.5 时,证明散度较大,需要增强惩罚力度,\beta 调整为 \beta \times 2

(2)PPO-Clip 直接对新旧策略比例进行一定程度的 Clip 操作,以约束变化幅度。其目标函数的计算方式如下:

L^{CLIP} (\theta ) = \hat{E}_t [min(r_t(\theta)\hat{A}_t, clip(r_t(\theta), 1-\varepsilon ,1+\varepsilon )\hat{A}_t)]

其中,\varepsilon 代表截断超参数,一般设定值为 0.2clip() 表示截断函数负责限制比例 r_t 在[1-\varepsilon ,1+\varepsilon ] 区间之内,以保证收敛性;最终 L^{CLIP} 借助 min 函数选取未截断与截断目标之间的更小值,形成目标下限。L^{CLIP} 可以分为优势函数 A 为正数和负数两种情况,其变化趋势如下图所示:

如果优势函数为正数,需要增大新旧策略比值 r_t,然而当 r_t>1+\varepsilon 时,将不提供额外的激励;如果优势函数是负数,需要减少新旧策略比值 r_t,但在 r_t < 1+\varepsilon 时,不提供额外的激励,这使得新旧策略的差异被限制在合理范围内。PPO 本质上基于 Actor-Critic 框架,算法流程如下:

PPO 算法主要由 Actor 和 Critic 两部分构成,Critic 部分更新方式与其他Actor-Critic 类型相似,通常采用计算 TD  error(时序差分误差)形式。对于 Actor 的更新方式,PPO 可在KLPENL 、CLIPL 之间选择对于当前实验环境稳定性适用性更强的目标函数,经过 OpenAI 研究团队实验论证,PPO- Clip 比 PPO- Penalty有更好的数据效率和可行性 


2. 代码实现

下面我就采用 Clip 形式的 PPO。模型构建代码如下。下面的模型适用于 action 是离散的情况,连续情况的代码可以从我的 GitHub 中获取。

# 代码用于离散环境的模型
import numpy as np
import torch
from torch import nn
from torch.nn import functional as F# ----------------------------------- #
# 构建策略网络--actor
# ----------------------------------- #class PolicyNet(nn.Module):def __init__(self, n_states, n_hiddens, n_actions):super(PolicyNet, self).__init__()self.fc1 = nn.Linear(n_states, n_hiddens)self.fc2 = nn.Linear(n_hiddens, n_actions)def forward(self, x):x = self.fc1(x)  # [b,n_states]-->[b,n_hiddens]x = F.relu(x)x = self.fc2(x)  # [b, n_actions]x = F.softmax(x, dim=1)  # [b, n_actions]  计算每个动作的概率return x# ----------------------------------- #
# 构建价值网络--critic
# ----------------------------------- #class ValueNet(nn.Module):def __init__(self, n_states, n_hiddens):super(ValueNet, self).__init__()self.fc1 = nn.Linear(n_states, n_hiddens)self.fc2 = nn.Linear(n_hiddens, 1)def forward(self, x):x = self.fc1(x)  # [b,n_states]-->[b,n_hiddens]x = F.relu(x)x = self.fc2(x)  # [b,n_hiddens]-->[b,1]  评价当前的状态价值state_valuereturn x# ----------------------------------- #
# 构建模型
# ----------------------------------- #class PPO:def __init__(self, n_states, n_hiddens, n_actions,actor_lr, critic_lr, lmbda, epochs, eps, gamma, device):# 实例化策略网络self.actor = PolicyNet(n_states, n_hiddens, n_actions).to(device)# 实例化价值网络self.critic = ValueNet(n_states, n_hiddens).to(device)# 策略网络的优化器self.actor_optimizer = torch.optim.Adam(self.actor.parameters(), lr=actor_lr)# 价值网络的优化器self.critic_optimizer = torch.optim.Adam(self.critic.parameters(), lr = critic_lr)self.gamma = gamma  # 折扣因子self.lmbda = lmbda  # GAE优势函数的缩放系数self.epochs = epochs  # 一条序列的数据用来训练轮数self.eps = eps  # PPO中截断范围的参数self.device = device# 动作选择def take_action(self, state):# 维度变换 [n_state]-->tensor[1,n_states]state = torch.tensor(state[np.newaxis, :]).to(self.device)# 当前状态下,每个动作的概率分布 [1,n_states]probs = self.actor(state)# 创建以probs为标准的概率分布action_list = torch.distributions.Categorical(probs)# 依据其概率随机挑选一个动作action = action_list.sample().item()return action# 训练def learn(self, transition_dict):# 提取数据集states = torch.tensor(transition_dict['states'], dtype=torch.float).to(self.device)actions = torch.tensor(transition_dict['actions']).to(self.device).view(-1,1)rewards = torch.tensor(transition_dict['rewards'], dtype=torch.float).to(self.device).view(-1,1)next_states = torch.tensor(transition_dict['next_states'], dtype=torch.float).to(self.device)dones = torch.tensor(transition_dict['dones'], dtype=torch.float).to(self.device).view(-1,1)# 目标,下一个状态的state_value  [b,1]next_q_target = self.critic(next_states)# 目标,当前状态的state_value  [b,1]td_target = rewards + self.gamma * next_q_target * (1-dones)# 预测,当前状态的state_value  [b,1]td_value = self.critic(states)# 目标值和预测值state_value之差  [b,1]td_delta = td_target - td_value# 时序差分值 tensor-->numpy  [b,1]td_delta = td_delta.cpu().detach().numpy()advantage = 0  # 优势函数初始化advantage_list = []# 计算优势函数for delta in td_delta[::-1]:  # 逆序时序差分值 axis=1轴上倒着取 [], [], []# 优势函数GAE的公式advantage = self.gamma * self.lmbda * advantage + deltaadvantage_list.append(advantage)# 正序advantage_list.reverse()# numpy --> tensor [b,1]advantage = torch.tensor(advantage_list, dtype=torch.float).to(self.device)# 策略网络给出每个动作的概率,根据action得到当前时刻下该动作的概率old_log_probs = torch.log(self.actor(states).gather(1, actions)).detach()# 一组数据训练 epochs 轮for _ in range(self.epochs):# 每一轮更新一次策略网络预测的状态log_probs = torch.log(self.actor(states).gather(1, actions))# 新旧策略之间的比例ratio = torch.exp(log_probs - old_log_probs)# 近端策略优化裁剪目标函数公式的左侧项surr1 = ratio * advantage# 公式的右侧项,ratio小于1-eps就输出1-eps,大于1+eps就输出1+epssurr2 = torch.clamp(ratio, 1-self.eps, 1+self.eps) * advantage# 策略网络的损失函数actor_loss = torch.mean(-torch.min(surr1, surr2))# 价值网络的损失函数,当前时刻的state_value - 下一时刻的state_valuecritic_loss = torch.mean(F.mse_loss(self.critic(states), td_target.detach()))# 梯度清0self.actor_optimizer.zero_grad()self.critic_optimizer.zero_grad()# 反向传播actor_loss.backward()critic_loss.backward()# 梯度更新self.actor_optimizer.step()self.critic_optimizer.step()

3. 案例演示

基于 OpenAI 的 gym 环境完成一个推车游戏,一个离散的环境,目标是左右移动小车将黄色的杆子保持竖直。动作维度为2,属于离散值;状态维度为 4,分别是坐标、速度、角度、角速度。

import numpy as np
import matplotlib.pyplot as plt
import gym
import torch
from RL_brain import PPOdevice = torch.device('cuda') if torch.cuda.is_available() \else torch.device('cpu')# ----------------------------------------- #
# 参数设置
# ----------------------------------------- #num_episodes = 100  # 总迭代次数
gamma = 0.9  # 折扣因子
actor_lr = 1e-3  # 策略网络的学习率
critic_lr = 1e-2  # 价值网络的学习率
n_hiddens = 16  # 隐含层神经元个数
env_name = 'CartPole-v1'
return_list = []  # 保存每个回合的return# ----------------------------------------- #
# 环境加载
# ----------------------------------------- #env = gym.make(env_name, render_mode="human")
n_states = env.observation_space.shape[0]  # 状态数 4
n_actions = env.action_space.n  # 动作数 2# ----------------------------------------- #
# 模型构建
# ----------------------------------------- #agent = PPO(n_states=n_states,  # 状态数n_hiddens=n_hiddens,  # 隐含层数n_actions=n_actions,  # 动作数actor_lr=actor_lr,  # 策略网络学习率critic_lr=critic_lr,  # 价值网络学习率lmbda = 0.95,  # 优势函数的缩放因子epochs = 10,  # 一组序列训练的轮次eps = 0.2,  # PPO中截断范围的参数gamma=gamma,  # 折扣因子device = device)# ----------------------------------------- #
# 训练--回合更新 on_policy
# ----------------------------------------- #for i in range(num_episodes):state = env.reset()[0]  # 环境重置done = False  # 任务完成的标记episode_return = 0  # 累计每回合的reward# 构造数据集,保存每个回合的状态数据transition_dict = {'states': [],'actions': [],'next_states': [],'rewards': [],'dones': [],}while not done:action = agent.take_action(state)  # 动作选择next_state, reward, done, _, _  = env.step(action)  # 环境更新# 保存每个时刻的状态\动作\...transition_dict['states'].append(state)transition_dict['actions'].append(action)transition_dict['next_states'].append(next_state)transition_dict['rewards'].append(reward)transition_dict['dones'].append(done)# 更新状态state = next_state# 累计回合奖励episode_return += reward# 保存每个回合的returnreturn_list.append(episode_return)# 模型训练agent.learn(transition_dict)# 打印回合信息print(f'iter:{i}, return:{np.mean(return_list[-10:])}')# -------------------------------------- #
# 绘图
# -------------------------------------- #plt.plot(return_list)
plt.title('return')
plt.show()

训练100回合,绘制每回合的 return

相关文章:

【深度强化学习】(6) PPO 模型解析,附Pytorch完整代码

大家好&#xff0c;今天和各位分享一下深度强化学习中的近端策略优化算法&#xff08;proximal policy optimization&#xff0c;PPO&#xff09;&#xff0c;并借助 OpenAI 的 gym 环境完成一个小案例&#xff0c;完整代码可以从我的 GitHub 中获得&#xff1a; https://gith…...

【数据结构】第二站:顺序表

目录 一、线性表 二、顺序表 1.顺序表的概念以及结构 2.顺序表的接口实现 3.顺序表完整代码 三、顺序表的经典题目 1.移除元素 2.删除有序数组中的重复项 3.合并两个有序数组 一、线性表 在了解顺序表前&#xff0c;我们得先了解线性表的概念 线性表&#xff08;linear…...

嵌入式安防监控项目——实现真实数据的上传

目录 一、相关驱动开发 二、A9主框架 三、脚本及数据上传实验 https://www.yuque.com/uh1h8r/dqrma0/tx0fq08mw1ar1sor?singleDoc# 《常见问题》 上个笔记的相关问题 一、相关驱动开发 /* mpu6050六轴传感器 */ i2c138B0000 { /* #address-cells <1>…...

SAP 生成UUID

UUID含义是通用唯一识别码 (Universally Unique Identifier)&#xff0c;这 是一个软件建构的标准&#xff0c;也是被开源软件基金会 (Open Software Foundation, OSF) 的组织应用在分布式计算环境 (Distributed Computing Environment, DCE) 领域的一部分。 UUID-Universally…...

DevOPs介绍,这一篇就足够了

一、什么是DevOps&#xff1f; DevOps是一种将软件开发和IT运维进行整合的文化和运动。它的目标是通过加强软件开发、测试和运维之间的协作和沟通&#xff0c;使整个软件开发和交付过程更加高效、快速、安全和可靠。DevOps涵盖了从计划和设计到开发、测试、交付和部署的全生命…...

libcurl库简介

一、libcurl简介libcurl是一个跨平台的网络协议库&#xff0c;支持http, https, ftp, gopher, telnet, dict, file, 和ldap 协议。libcurl同样支持HTTPS证书授权&#xff0c;HTTP POST, HTTP PUT, FTP 上传, HTTP基本表单上传&#xff0c;代理&#xff0c;cookies,和用户认证。…...

Spark SQL支持DataFrame操作的数据源

DataFrame提供统一接口加载和保存数据源中的数据&#xff0c;包括&#xff1a;结构化数据、Parquet文件、JSON文件、Hive表&#xff0c;以及通过JDBC连接外部数据源。一个DataFrame可以作为普通的RDD操作&#xff0c;也可以通过&#xff08;registerTempTable&#xff09;注册成…...

Java【归并排序】算法, 大白话式图文解析(附代码)

文章目录前言一、排序相关概念1, 什么是排序2, 什么是排序的稳定性3, 七大排序分类二、归并排序1, 图文解析2, 代码实现三、性能分析四、七大排序算法总体分析前言 各位读者好, 我是小陈, 这是我的个人主页 小陈还在持续努力学习编程, 努力通过博客输出所学知识 如果本篇对你有…...

【springboot】数据库访问

1、SQL 1、数据源的自动配置-HikariDataSource 1、导入JDBC场景 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jdbc</artifactId></dependency>数据库驱动&#xff1f; 为什么导入JD…...

普通和hive兼容模式下sql的差异

–odps sql –– –author:宋文理 –create time:2023-03-08 15:23:52 –– – 差异分为三块 – 1.运算符的差异 – 2.类型转换的差异 – 3.内建函数的差异 – 以下是运算符的差异&#xff1a; – BITAND&#xff08;&&#xff09; – 当输入参数是BIGINT类型的时候&…...

github开源自己代码

接下来&#xff0c;我们需要先下载Git&#xff0c;的网址&#xff1a;https://git-scm.com/downloads&#xff0c;安装时如果没有特殊需求&#xff0c;一直下一步就可以了&#xff0c;安装完成之后&#xff0c;双击打开Git Bash 出现以下界面&#xff1a; 第一步&#xff1a;…...

数据库基础语法

sql&#xff08;Structured Query Language 结构化查询语言&#xff09; SQL语法 use DataTableName; 命令用于选择数据库。set names utf8; 命令用于设置使用的字符集。SELECT * FROM Websites; 读取数据表的信息。上面的表包含五条记录&#xff08;每一条对应一个网站信息&…...

【Java】期末复习知识点总结(4)

适合Java期末的复习~ &#xff08;Java期末复习知识点总结分为4篇&#xff0c;这里是最后一篇啦&#xff09;第一篇~https://blog.csdn.net/qq_53869058/article/details/129417537?spm1001.2014.3001.5501第二篇~https://blog.csdn.net/qq_53869058/article/details/1294751…...

IDEA好用插件:MybatisX快速生成接口实体类mapper.xml映射文件

目录 1、在Idea中找到下载插件&#xff0c;Install&#xff0c;重启Idea 2、一个测试java文件&#xff0c;里面有com包 3、在Idea中添加数据库 --------以Oracle数据库为例 4、快速生成entity-service-mapper方法 5、查看生成的代码 6、自动生成&#xff08;增删查改&#xff0…...

【JavaEE】初识线程

一、简述进程认识线程之前我们应该去学习一下“进程" 的概念&#xff0c;我们可以把一个运行起来的程序称之为进程&#xff0c;进程的调度&#xff0c;进程的管理是由我们的操作系统来管理的&#xff0c;创建一个进程&#xff0c;操作系统会为每一个进程创建一个 PCB&…...

智慧水务监控系统-智慧水务信息化平台建设

平台概述柳林智慧水务监控系统&#xff08;智慧水务信息化平台&#xff09;是以物联感知技术、大数据、智能控制、云计算、人工智能、数字孪生、AI算法、虚拟现实技术为核心&#xff0c;以监测仪表、通讯网络、数据库系统、数据中台、模型软件、前台展示、智慧运维等产品体系为…...

【Linux】进程优先级前后台理解

环境&#xff1a;centos7.6&#xff0c;腾讯云服务器Linux文章都放在了专栏&#xff1a;【Linux】欢迎支持订阅&#x1f339;相关文章推荐&#xff1a;【Linux】冯.诺依曼体系结构与操作系统【Linux】进程理解与学习&#xff08;Ⅰ&#xff09;浅谈Linux下的shell--BASH【Linux…...

时序预测 | MATLAB实现基于EMD-GRU时间序列预测(EMD分解结合GRU门控循环单元)

时序预测 | MATLAB实现基于EMD-GRU时间序列预测(EMD分解结合GRU门控循环单元) 目录 时序预测 | MATLAB实现基于EMD-GRU时间序列预测(EMD分解结合GRU门控循环单元)效果一览基本描述模型描述程序设计参考资料效果一览...

python 模拟鼠标,键盘点击

信息爆炸 消息轰炸模拟鼠标和键盘敲击import time from pynput.keyboard import Controller as key_col from pynput.mouse import Button,Controller def keyboard_input(insertword):keyboardkey_col()keyboard.type(insertword)def mouth():mouseController()mouse.press(…...

【CSS】盒子边框 ③ ( 设置表格细线边框 | 合并相邻边框 border-collapse: collapse; )

文章目录一、设置表格细线边框1、表格示例2、合并相邻边框3、完整代码示例一、设置表格细线边框 1、表格示例 给定一个 HTML 结构中的表格 , 默认样式如下 : <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8" />…...

云计算——弹性云计算器(ECS)

弹性云服务器&#xff1a;ECS 概述 云计算重构了ICT系统&#xff0c;云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台&#xff0c;包含如下主要概念。 ECS&#xff08;Elastic Cloud Server&#xff09;&#xff1a;即弹性云服务器&#xff0c;是云计算…...

shell脚本--常见案例

1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件&#xff1a; 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...

边缘计算医疗风险自查APP开发方案

核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...

【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器

——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的​​一体化测试平台​​&#xff0c;覆盖应用全生命周期测试需求&#xff0c;主要提供五大核心能力&#xff1a; ​​测试类型​​​​检测目标​​​​关键指标​​功能体验基…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容

基于 ​UniApp + WebSocket​实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配​微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...

【SpringBoot】100、SpringBoot中使用自定义注解+AOP实现参数自动解密

在实际项目中,用户注册、登录、修改密码等操作,都涉及到参数传输安全问题。所以我们需要在前端对账户、密码等敏感信息加密传输,在后端接收到数据后能自动解密。 1、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI

前一阵子在百度 AI 开发者大会上&#xff0c;看到基于小智 AI DIY 玩具的演示&#xff0c;感觉有点意思&#xff0c;想着自己也来试试。 如果只是想烧录现成的固件&#xff0c;乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外&#xff0c;还提供了基于网页版的 ESP LA…...

ElasticSearch搜索引擎之倒排索引及其底层算法

文章目录 一、搜索引擎1、什么是搜索引擎?2、搜索引擎的分类3、常用的搜索引擎4、搜索引擎的特点二、倒排索引1、简介2、为什么倒排索引不用B+树1.创建时间长,文件大。2.其次,树深,IO次数可怕。3.索引可能会失效。4.精准度差。三. 倒排索引四、算法1、Term Index的算法2、 …...

汇编常见指令

汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX&#xff08;不访问内存&#xff09;XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...

什么是Ansible Jinja2

理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具&#xff0c;可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板&#xff0c;允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板&#xff0c;并通…...