文章目录

  • 前言
  • SAC处理连续动作空间问题 (Pendulum-v1)
    • 核心代码实现
      • **工具函数与环境初始化**
      • **ReplayBuffer、网络结构与SAC算法**
      • **训练与结果**
  • SAC处理离散动作空间问题 (CartPole-v1)
    • 核心代码实现
      • **工具函数与环境初始化**
      • **ReplayBuffer、网络结构与SAC算法 (离散版)**
      • **训练与结果**
  • 总结


前言

在深度强化学习(DRL)的探索之旅中,我们不断寻求更高效、更稳定的算法来应对日益复杂的决策问题。传统的在线策略算法(On-policy)如A2C、PPO等,虽然在很多场景下表现优异,但其采样效率低下的问题也限制了它们在某些现实世界任务中的应用,尤其是在那些与环境交互成本高昂的场景中。

因此,离线策略(Off-policy)算法应运而生,它们能够利用历史数据(Replay Buffer)进行学习,极大地提高了数据利用率和学习效率。在众多离线策略算法中,Soft Actor-Critic(SAC)算法以其出色的稳定性和卓越的性能脱颖而出。

正如上图所述,与同为离线策略算法的DDPG相比,SAC在训练稳定性和收敛性方面表现更佳,对超参数的敏感度也更低。 SAC的前身是Soft Q-learning,它们都属于最大熵强化学习的范畴,即在最大化累积奖励的同时,也最大化策略的熵,从而鼓励智能体进行更充分的探索。 与Soft Q-learning不同,SAC引入了一个显式的策略函数(Actor),从而优雅地解决了在连续动作空间中求解困难的问题。 SAC学习的是一个随机策略,这使得它能够探索多模态的最优策略,并在复杂的环境中表现出更强的鲁棒性。

本篇博客将通过两个PyTorch实现的SAC代码示例,带您深入理解SAC算法的精髓。我们将分别探讨其在连续动作空间离散动作空间中的具体实现,并通过代码解析,让您对策略网络、价值网络、经验回放、软更新以及核心的熵正则化等概念有更直观的认识。

无论您是强化学习的初学者,还是希望深入了解SAC算法的实践者,相信通过本文的代码学习之旅,您都将有所收获。

完整代码:下载链接


SAC处理连续动作空间问题 (Pendulum-v1)

在连续控制任务中,SAC通过学习一个随机策略,输出动作的正态分布的均值和标准差,从而实现对连续动作的探索和决策。我们将以OpenAI Gym中的经典环境Pendulum-v1为例,这是一个典型的连续控制问题,智能体的目标是利用有限的力矩将摆杆竖立起来。

核心代码实现

以下是SAC算法在Pendulum-v1环境下的完整PyTorch实现。代码涵盖了工具函数、环境初始化、核心网络结构(ReplayBuffer、策略网络、Q值网络)、SAC算法主类以及训练和可视化的全过程。

工具函数与环境初始化

首先,我们定义一个moving_average函数用于平滑训练过程中的奖励曲线,以便更好地观察训练趋势。然后,我们初始化Pendulum-v1环境。

# utils"""
强化学习工具函数集
包含数据平滑处理功能
"""import torch
import numpy as np
def moving_average(data, window_size):"""计算移动平均值,用于平滑奖励曲线该函数通过滑动窗口的方式对时间序列数据进行平滑处理,可以有效减少数据中的噪声,使曲线更加平滑美观。常用于强化学习中对训练过程的奖励曲线进行可视化优化。参数:data (list): 原始数据序列,维度: [num_episodes]包含需要平滑处理的数值数据(如每轮训练的奖励值)window_size (int): 移动窗口大小,维度: 标量决定了平滑程度,窗口越大平滑效果越明显但也会导致更多的数据点丢失返回:list: 移动平均后的数据,维度: [len(data) - window_size + 1]返回的数据长度会比原数据少 window_size - 1 个元素这是因为需要足够的数据点来计算第一个移动平均值示例:>>> data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]  # 维度: [10]>>> smoothed = moving_average(data, 3)       # window_size = 3>>> print(smoothed)  # 输出: [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]  维度: [8]"""# 边界检查:如果数据长度小于窗口大小,直接返回原数据# 这种情况下无法计算移动平均值# data维度: [num_episodes], window_size维度: 标量if len(data) < window_size:return data# 初始化移动平均值列表# moving_avg维度: 最终为[len(data) - window_size + 1]moving_avg = []# 遍历数据,计算每个窗口的移动平均值# i的取值范围: 0 到 len(data) - window_size# 循环次数: len(data) - window_size + 1# 每次循环处理一个滑动窗口位置for i in range(len(data) - window_size + 1):# 提取当前窗口内的数据切片# window_data维度: [window_size]# 包含从索引i开始的连续window_size个元素# 例如:当i=0, window_size=3时,提取data[0:3]window_data = data[i:i + window_size]# 计算当前窗口内数据的算术平均值# np.mean(window_data)维度: 标量# 将平均值添加到结果列表中moving_avg.append(np.mean(window_data))# 返回移动平均后的数据列表# moving_avg维度: [len(data) - window_size + 1]return moving_avg
"""
强化学习环境初始化模块
用于创建和配置OpenAI Gym环境
"""import gym  # OpenAI Gym库,提供标准化的强化学习环境接口
import numpy as np  # 数值计算库,用于处理多维数组和数学运算# 定义环境名称
# env_name维度: 字符串标量
# 'Pendulum-v1'是一个连续控制任务,倒立摆环境
# 状态空间: 3维连续空间 (cos(theta), sin(theta), thetadot)
# 动作空间: 1维连续空间,范围[-2.0, 2.0]
env_name = 'Pendulum-v1'# 创建强化学习环境实例
# env维度: gym.Env对象
# 包含完整的环境状态、动作空间、奖励函数等信息
# 该环境支持reset()、step()、render()、close()等标准方法
env = gym.make(env_name)

ReplayBuffer、网络结构与SAC算法

这部分代码是SAC算法的核心。

  • ReplayBuffer: 经验回放池,用于存储和采样智能体的经验数据,打破数据相关性,提高学习效率。
  • PolicyNetContinuous: 策略网络(Actor),输入状态,输出动作分布的均值和标准差。这里使用了重参数化技巧(Reparameterization Trick),使得从策略分布中采样的过程可导,从而能够利用梯度进行端到端的训练。动作经过tanh函数激活并缩放到环境的动作边界内。
  • QValueNetContinuous: Q值网络(Critic),输入状态和动作,输出对应的Q值。SAC采用了双Q网络的技巧,即构建两个结构相同的Q网络,在计算目标Q值时取两者的较小值,以缓解Q值过高估计的问题。
  • SACContinuous: SAC算法的主类,整合了上述所有网络和组件。它实现了动作选择、目标Q值计算、网络参数的软更新以及策略和价值网络的更新逻辑。其中,温度参数α的学习和更新是SAC的核心之一,它通过最大化熵的目标自动调整,平衡探索与利用。
"""
SAC (Soft Actor-Critic) 算法实现
用于连续动作空间的强化学习智能体
"""import torch  # PyTorch深度学习框架
import torch.nn as nn  # 神经网络模块
import torch.nn.functional as F  # 神经网络功能函数
import numpy as np  # 数值计算库
import random  # 随机数生成库
import collections  # 集合数据类型模块
from torch.distributions import Normal  # 正态分布类class ReplayBuffer:"""经验回放缓冲区类用于存储和采样智能体的经验数据"""def __init__(self, capacity):"""初始化经验回放缓冲区参数:capacity (int): 缓冲区容量,维度: 标量"""# 使用双端队列作为缓冲区存储结构# self.buffer维度: deque,最大长度为capacity# 存储格式: (state, action, reward, next_state, done)self.buffer = collections.deque(maxlen=capacity)def add(self, state, action, reward, next_state, done):"""向缓冲区添加一条经验参数:state (np.array): 当前状态,维度: [state_dim]action (float): 执行的动作,维度: 标量reward (float): 获得的奖励,维度: 标量next_state (np.array): 下一个状态,维度: [state_dim]done (bool): 是否结束,维度: 标量布尔值"""# 将经验元组添加到缓冲区# 元组维度: (state[state_dim], action[1], reward[1], next_state[state_dim], done[1])self.buffer.append((state, action, reward, next_state, done))def sample(self, batch_size):"""从缓冲区随机采样一批经验参数:batch_size (int): 批次大小,维度: 标量返回:tuple: 包含状态、动作、奖励、下一状态、完成标志的元组state (np.array): 状态批次,维度: [batch_size, state_dim]action (tuple): 动作批次,维度: [batch_size]reward (tuple): 奖励批次,维度: [batch_size]next_state (np.array): 下一状态批次,维度: [batch_size, state_dim]done (tuple): 完成标志批次,维度: [batch_size]"""# 随机采样batch_size个经验# transitions维度: list,长度为batch_sizetransitions = random.sample(self.buffer, batch_size)# 将经验元组解包并转置# 每个元素的维度: state[batch_size个state_dim], action[batch_size], 等等state, action, reward, next_state, done = zip(*transitions)# 将状态转换为numpy数组便于后续处理# state维度: [batch_size, state_dim]# next_state维度: [batch_size, state_dim]return np.array(state), action, reward, np.array(next_state), donedef size(self):"""返回缓冲区当前大小返回:int: 缓冲区大小,维度: 标量"""return len(self.buffer)class PolicyNetContinuous(torch.nn.Module):"""连续动作空间的策略网络输出动作的均值和标准差,用于生成随机策略"""def __init__(self, state_dim, hidden_dim, action_dim, action_bound):"""初始化策略网络参数:state_dim (int): 状态空间维度,维度: 标量hidden_dim (int): 隐藏层维度,维度: 标量action_dim (int): 动作空间维度,维度: 标量action_bound (float): 动作边界值,维度: 标量"""super(PolicyNetContinuous, self).__init__()# 第一个全连接层:状态到隐藏层# 输入维度: [batch_size, state_dim]# 输出维度: [batch_size, hidden_dim]self.fc1 = torch.nn.Linear(state_dim, hidden_dim)# 输出动作均值的全连接层# 输入维度: [batch_size, hidden_dim]# 输出维度: [batch_size, action_dim]self.fc_mu = torch.nn.Linear(hidden_dim, action_dim)# 输出动作标准差的全连接层# 输入维度: [batch_size, hidden_dim]# 输出维度: [batch_size, action_dim]self.fc_std = torch.nn.Linear(hidden_dim, action_dim)# 动作边界值,用于缩放tanh输出# action_bound维度: 标量self.action_bound = action_bounddef forward(self, x):"""前向传播函数参数:x (torch.Tensor): 输入状态,维度: [batch_size, state_dim]返回:tuple: 包含动作和对数概率的元组action (torch.Tensor): 输出动作,维度: [batch_size, action_dim]log_prob (torch.Tensor): 动作对数概率,维度: [batch_size, action_dim]"""# 第一层激活# x维度: [batch_size, state_dim] -> [batch_size, hidden_dim]x = F.relu(self.fc1(x))# 计算动作均值# mu维度: [batch_size, action_dim]mu = self.fc_mu(x)# 计算动作标准差,使用softplus确保为正值# std维度: [batch_size, action_dim]std = F.softplus(self.fc_std(x))# 创建正态分布对象# dist维度: Normal分布对象,参数维度均为[batch_size, action_dim]dist = Normal(mu, std)# 重参数化采样,确保梯度可以反向传播# normal_sample维度: [batch_size, action_dim]normal_sample = dist.rsample()  # rsample()是重参数化采样"""重参数化采样是一种用于训练神经网络生成模型(Generative Models)的技术,特别是在概率编码器-解码器框架中常见,例如变分自编码器(Variational Autoencoder,VAE)。这种技术的目的是将采样过程通过神经网络的可导操作,使得模型可以被端到端地训练。在普通的采样过程中,由于采样操作本身是不可导的,传统的梯度下降方法无法直接用于训练神经网络。为了解决这个问题,引入了重参数化技巧。`dist.rsample()` 是重参数化采样的一部分。这里的重参数化指的是将采样操作重新参数化为可导的操作,使得梯度能够通过网络反向传播。通过这种方式,可以有效地训练生成模型,尤其是概率生成模型。在正态分布的情况下,传统的采样操作是直接从标准正态分布中抽取样本,然后通过线性变换得到最终的样本。而重参数化采样则通过在标准正态分布上进行采样,并通过神经网络产生的均值和标准差进行变换,使得采样操作变为可导的。这有助于在训练过程中通过梯度下降来优化网络参数。"""# 计算采样点的对数概率密度# log_prob维度: [batch_size, action_dim]log_prob = dist.log_prob(normal_sample)# 使用tanh函数将动作限制在[-1, 1]范围内# action维度: [batch_size, action_dim]action = torch.tanh(normal_sample)# 计算tanh_normal分布的对数概率密度# 根据变换的雅可比行列式调整概率密度# 避免数值不稳定性,添加小常数1e-7# log_prob维度: [batch_size, action_dim]log_prob = log_prob - torch.log(1 - torch.tanh(action).pow(2) + 1e-7)# 将动作缩放到实际的动作边界范围# action维度: [batch_size, action_dim]action = action * self.action_boundreturn action, log_probclass QValueNetContinuous(torch.nn.Module):"""连续动作空间的Q值网络输入状态和动作,输出对应的Q值"""def __init__(self, state_dim, hidden_dim, action_dim):"""初始化Q值网络参数:state_dim (int): 状态空间维度,维度: 标量hidden_dim (int): 隐藏层维度,维度: 标量action_dim (int): 动作空间维度,维度: 标量"""super(QValueNetContinuous, self).__init__()# 第一个全连接层:拼接状态和动作后的输入层# 输入维度: [batch_size, state_dim + action_dim]# 输出维度: [batch_size, hidden_dim]self.fc1 = torch.nn.Linear(state_dim + action_dim, hidden_dim)# 第二个隐藏层# 输入维度: [batch_size, hidden_dim]# 输出维度: [batch_size, hidden_dim]self.fc2 = torch.nn.Linear(hidden_dim, hidden_dim)# 输出层:输出Q值# 输入维度: [batch_size, hidden_dim]# 输出维度: [batch_size, 1]self.fc_out = torch.nn.Linear(hidden_dim, 1)def forward(self, x, a):"""前向传播函数参数:x (torch.Tensor): 输入状态,维度: [batch_size, state_dim]a (torch.Tensor): 输入动作,维度: [batch_size, action_dim]返回:torch.Tensor: Q值,维度: [batch_size, 1]"""# 将状态和动作拼接作为网络输入# cat维度: [batch_size, state_dim + action_dim]cat = torch.cat([x, a], dim=1)# 第一层激活# x维度: [batch_size, state_dim + action_dim] -> [batch_size, hidden_dim]x = F.relu(self.fc1(cat))# 第二层激活# x维度: [batch_size, hidden_dim] -> [batch_size, hidden_dim]x = F.relu(self.fc2(x))# 输出Q值# 返回值维度: [batch_size, 1]return self.fc_out(x)class SACContinuous:"""SAC (Soft Actor-Critic) 算法实现类处理连续动作空间的强化学习问题SAC 使用两个 Critic 网络来使 Actor 的训练更稳定,而这两个 Critic 网络在训练时则各自需要一个目标价值网络。因此,SAC 算法一共用到 5 个网络,分别是一个策略网络、两个价值网络和两个目标价值网络。"""def __init__(self, state_dim, hidden_dim, action_dim, action_bound,actor_lr, critic_lr, alpha_lr, target_entropy, tau, gamma,device):"""初始化SAC算法参数:state_dim (int): 状态空间维度,维度: 标量hidden_dim (int): 隐藏层维度,维度: 标量action_dim (int): 动作空间维度,维度: 标量action_bound (float): 动作边界值,维度: 标量actor_lr (float): 策略网络学习率,维度: 标量critic_lr (float): 价值网络学习率,维度: 标量alpha_lr (float): 温度参数学习率,维度: 标量target_entropy (float): 目标熵值,维度: 标量tau (float): 软更新参数,维度: 标量gamma (float): 折扣因子,维度: 标量device (torch.device): 计算设备,维度: 设备对象"""# 策略网络:输出动作分布# self.actor维度: PolicyNetContinuous对象self.actor = PolicyNetContinuous(state_dim, hidden_dim, action_dim,action_bound).to(device)# 第一个Q网络:评估状态-动作价值# self.critic_1维度: QValueNetContinuous对象self.critic_1 = QValueNetContinuous(state_dim, hidden_dim,action_dim).to(device)# 第二个Q网络:评估状态-动作价值# self.critic_2维度: QValueNetContinuous对象self.critic_2 = QValueNetContinuous(state_dim, hidden_dim,action_dim).to(device)# 第一个目标Q网络:用于计算目标Q值# self.target_critic_1维度: QValueNetContinuous对象self.target_critic_1 = QValueNetContinuous(state_dim,hidden_dim, action_dim).to(device)# 第二个目标Q网络:用于计算目标Q值# self.target_critic_2维度: QValueNetContinuous对象self.target_critic_2 = QValueNetContinuous(state_dim,hidden_dim, action_dim).to(device)# 令目标Q网络的初始参数和Q网络一样self.target_critic_1.load_state_dict(self.critic_1.state_dict())self.target_critic_2.load_state_dict(self.critic_2.state_dict())# 策略网络优化器# self.actor_optimizer维度: torch.optim.Adam对象self.actor_optimizer = torch.optim.Adam(self.actor.parameters(),lr

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/915592.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/915592.shtml
英文地址,请注明出处:http://en.pswp.cn/news/915592.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

物联网安装调试-温湿度传感器

以下为温湿度传感器在物联网安装调试中的全流程技术指南,涵盖选型、安装、调试及故障排查,结合工业/农业/家居三大场景实操要点: 一、传感器选型核心参数表 参数 工业场景 农业大棚 智能家居 选型建议 精度 0.5℃/1.5%RH 1℃/3%RH 1℃/5%RH 工业级首选Sensirion SHT3x系列 防…

MySQL 核心知识点梳理(1)

目录 1.什么是数据库? 关系型数据库 非关系型数据库 2.Mysql出现性能差的原因? 3.MySQL的内联,左外联,右外连接的区别 4.为什么要有三大范式 建表需要考虑的问题? char和varchar的区别 blob和text的区别? DATETIME和TIMESTAMP的区别 in和exists的区别 null值陷 …

Word快速文本对齐程序开发经验:从需求分析到实现部署

在日常办公中&#xff0c;文档排版是一项常见但耗时的工作&#xff0c;尤其是当需要处理大量文本并保持格式一致时。Microsoft Word作为最流行的文档处理软件之一&#xff0c;虽然提供了丰富的排版功能&#xff0c;但在处理复杂的文本对齐需求时&#xff0c;往往需要重复执行多…

力扣面试150(34/150)

7.20 242. 有效的字母异位词 给定两个字符串 s 和 t &#xff0c;编写一个函数来判断 t 是否是 s 的 字母异位词 我的思路&#xff1a; 遍历s到一个sMap&#xff0c;字母次数的方式遍历t&#xff0c;判断t中的char是否在sMap当中&#xff0c;如果在的话次数-1&#xff0c;判…

软件工程:可行性分析的任务及报告

简介 本博客围绕软件工程中的第一关——“可行性分析的任务及报告”展开&#xff0c;详细解析了可行性分析的基本概念、分析任务、四类可行性&#xff08;技术、经济、操作、社会&#xff09;以及可行性分析报告的结构与撰写要点。通过丰富的理论基础与图示支持&#xff0c;帮…

STM32与树莓派通信

STM32 与树莓派&#xff08;Raspberry Pi&#xff09;的通信常见方案及实现步骤&#xff1a;1. UART 串口通信&#xff08;最简单&#xff09;适用场景&#xff1a;短距离、低速数据交换&#xff08;如传感器数据、调试信息&#xff09;。 硬件连接&#xff1a;STM32引脚树莓派…

【NLP舆情分析】基于python微博舆情分析可视化系统(flask+pandas+echarts) 视频教程 - 数据持久化到Mysql

大家好&#xff0c;我是java1234_小锋老师&#xff0c;最近写了一套【NLP舆情分析】基于python微博舆情分析可视化系统(flaskpandasecharts)视频教程&#xff0c;持续更新中&#xff0c;计划月底更新完&#xff0c;感谢支持。今天讲解数据持久化到Mysql 视频在线地址&#xff…

【Java EE】多线程-初阶-Thread 类及常见方法

多线程-初阶2. Thread 类及常⻅⽅法2.1 Thread 的常⻅构造⽅法2.2 Thread 的⼏个常⻅属性2.3 启动⼀个线程 - start()2.4 中断⼀个线程2.5 等待⼀个线程 - join()2.6 获取当前线程引⽤2.7 休眠当前线程本节⽬标• 认识多线程• 掌握多线程程序的编写• 掌握多线程的状态• 掌握…

LVS技术知识详解(知识点+相关实验部署)

目录 1.1 LVS简介 1.2 LVS体系结构 1.3 LVS相关术语 1.4 LVS工作模式 1.5 LVS工作原理 1.6 LVS调度算法 2.LVS相关实验部署 2.1 lvs软件相关信息 2.1.1 ipsadm常见参数 2.1.2 试例 2.2 LVS部署NAT模式 2.2.1 实验环境 2.2.2 实验步骤 2.2.2.1 实验基础环境 2.2.…

芋道导入逻辑

一、代码 PostMapping("/import")Operation(summary "导入用户")Parameters({Parameter(name "file", description "Excel 文件", required true),Parameter(name "updateSupport", description "是否支持更新&a…

gradle7.6.1+springboot3.2.4创建微服务工程

目录 一、创建主工程cloud-demo并删除src目录 二、创建子工程user-service/order-service 三、更改父工程build.gradle文件 四、子工程使用mybatis框架 五、子工程使用mybatis-plus框架 六、相关数据库创建 七、最终目录结构 一、创建主工程cloud-demo并删除src目录 二、…

电脑windows系统深度维护指南

&#x1f5a5;️ 电脑系统全方位维护指南 预防故障 提升性能 延长寿命 &#x1f50d; 引言&#xff1a;为什么需要系统维护&#xff1f; 电脑如同汽车&#xff0c;定期保养可避免&#xff1a; ✅ 突发蓝屏死机 ✅ 系统卡顿崩溃 ✅ 硬件过早损坏 ✅ 数据丢失风险 本指南提供…

字节内部流传的数据分析手册

之前2领导整理内部分享的&#xff0c;所以很多内部业务的分析&#xff0c;比如工作中怎么落地、怎么推进。(数据都是脱敏的哈) **里面的内容都偏应用&#xff0c;比如产品迭代怎么做数据评估、用户增长靠什么指标拆解、AB实验怎么设计、运营活动怎么闭环。**数据分析都是很实际…

Nginx Proxy Manager + LB + Openappsec + Web UI 构建下一代WAF

Nginx Proxy Manager + LB + Openappsec + Web UI部署 一、环境介绍 二、系统参数优化 三、安装docker 四、创建docker网络 五、创建测试容器 六、部署NPM和openappsec 1、下载docker-compose文件 2、拉取相关镜像 3、web UI 获取token 4、修改compose文件并安装 七、登陆NPM配…

【React】npm install报错npm : 无法加载文件 D:\APP\nodejs\npm.ps1,因为在此系统上禁止运行脚本。

使用vsCode打开react项目安装依赖时报错&#xff0c;把terminal打开的powershell改成command prompt即可

深入解析C#装箱转换:值类型如何“变身”为引用类型?

当你将 int i 赋值给 object oi 时&#xff0c; 看似简单的操作背后&#xff0c;藏着一场精密的类型转换革命&#xff01;&#x1f511; 一、核心概念&#xff1a;什么是装箱&#xff1f; 装箱&#xff08;Boxing&#xff09; 是C#中的一种隐式转换机制&#xff0c;它将值类型&…

java list 与set 集合的迭代器在进行元素操作时出现数据混乱问题及原因

为什么 List 和 Set 迭代器删除结果不同&#xff1f;1. List 和 Set 的本质差异List&#xff08;如 ArrayList&#xff09;&#xff1a;有序集合&#xff0c;元素按插入顺序存储&#xff0c;允许重复元素。迭代器遍历时&#xff0c;元素按索引顺序返回。删除操作&#xff08;通…

大语言模型:人像摄影的“达芬奇转世”?——从算法解析到光影重塑的智能摄影革命

导言在摄影术诞生之初&#xff0c;达芬奇或许无法想象&#xff0c;他对于光影、比例和解剖的严谨研究&#xff0c;会在数百年后以另一种形式重生。今天&#xff0c;当摄影师面对复杂的光线环境或苦苦寻找最佳构图时&#xff0c;一位由代码构筑的“光影军师”正悄然降临——大语…

Java——MyBatis从入门到精通:一站式学习指南

MyBatis从入门到精通&#xff1a;一站式学习指南 作为一款优秀的半自动ORM框架&#xff0c;MyBatis以其灵活的SQL控制和简洁的配置方式&#xff0c;成为Java后端开发中持久层框架的首选。本文将从基础概念到高级特性&#xff0c;全面讲解MyBatis的使用方法&#xff0c;包含实用…

面试150 添加与搜索单词--数据结构设计

思路 通过哈希法去实现&#xff0c;这里主要描述search的思路&#xff1a;如果’.‘不在word中&#xff0c;我们只需要去查询word在不在set中。如果’.‘存在&#xff0c;我们对哈希中的字符串进行遍历w&#xff0c;如果当前字符串的长度不等于word跳过,对word进行遍历&#xf…