强化学习算法实战:一个例子搞懂sarsa、dqn、ddqn、qac、a2c及其区别
简介
在学习强化学习算法:sarsa、dqn、ddqn、qac、a2c、trpo、ppo时,由于有大量数学公式的推导,觉得十分晦涩,且听过就忘记了。
但是当把算法应用于实战时,代码的实现要比数学推导直观很多。
接下来通过不同的算法实现gym中的CartPole-v1游戏。
游戏介绍
CartPole(推车倒立摆) 是强化学习中经典的基准测试任务,因为其直观可视、方便调试、状态和动作空间小等特性,常用于入门教学和算法验证。它的目标是训练一个智能体(agent)通过左右移动小车,使车顶的杆子尽可能长时间保持竖直不倒。
- 环境:小车(cart)可以在水平轨道上左右移动,顶部通过关节连接一根自由摆动的杆子(pole)。
- 目标:通过左右移动小车,使杆子的倾斜角度不超出阈值(±12°或±15°),同时小车不超出轨道范围(如轨道长度的±2.4单位)。简单理解为,就是杆子不会倒下里,小车不会飞出屏幕。
- 状态:状态空间包含4个连续变量,分别是小车位置(x),小车速度(v),杆子角度(θ),杆子角速度(ω)
- 动作:动作空间只有2个离线动作,分别是0(向左移动)或1(向右移动)
奖励机制:每成功保持杆子不倒+1分,目前是让奖励最大化,即杆子永远不倒
DQN&DDQN&Dueling DQN&Sarsa
首先我们通过DQN算法介绍完整代码,剩下的算法只需要在此基础上进行少量修改。
DQN
构建价值网络
DQN需要一个主网 络和目标网络,用来评估执行的动作得到的动作价值。这两个网络使用的是同一个网络结构,通常使用神经网络来实现。
输入的维度是状态空间的4个变量,分别是小车位置(x),小车速度(v),杆子角度(θ),杆子角速度(ω)。输出的维度是动作空间的维度,分别表示向左、向右移动的动作价值。
代码如下:
class QNetWork(nn.Module):def __init__(self, state_dim, action_dim):super().__init__()self.fc = nn.Sequential(nn.Linear(state_dim, 64),nn.ReLU(),nn.Linear(64, 64),nn.ReLU(),nn.Linear(64, action_dim))def forward(self,x):return self.fc(x)
构建DQN智能体
DQN智能体具备如下功能:
- 选择动作
- 存储经验
- 训练上面的神经网络:目的是返回给定状态下尽可能接近真实的动作价值
- 模型保存
- 评估价值网络
初始化参数
初始化q网络、目标网络、设置优化器、设置经验回放使用的缓存大小、训练的batch_size
dqn的折扣因子、探索率、更新目标网络的频率、智能体步数计数器初始化、记录最佳网络分数值参数初始化、评估轮数设置
def __init__(self,state_dim, action_dim):# 神经网络网络相关self.q_net = QNetWork(state_dim,action_dim)self.target_net = QNetWork(state_dim,action_dim)self.target_net.load_state_dict(self.q_net.state_dict())self.optimizer = optim.Adam(self.q_net.parameters(), lr=1e-3)self.replay_buffer = deque(maxlen=10000)self.batch_size = 64# DQN相关self.gamma = 0.99self.epsilon = 0.1self.update_target_freq = 100self.step_count=0self.best_avg_reward = 0self.eval_episodes=5
动作选择
在DQN算法中,会采用epsilon参数来增加智能体选择动作的探索性,因此,动作选择的代码逻辑为:
- 以epsilon的概率随机选择一个动作
- 以1-epsilon的概率来选择价值网络返回结果中动作价值更大的动作
def choose_action(self, state):if np.random.rand() < self.epsilon:return np.random.randint(0,2) //随机选择动作else:state_tensor = torch.FloatTensor(state)q_values = self.q_net(state_tensor) //调用价值网络选择动作return q_values.cpu().detach().numpy().argmax()
存储经验
DQN中,使用经验回放,那么我们需要预留缓冲区来存放历史的轨迹数据,方便后续取出用来训练网络
存储当前的状态、选择的动作、获得的奖励、下一个状态、游戏是否结束(即杆子是不是倒下)
def store_experience(self,state, action, reward, next_state, done):self.replay_buffer.append((state, action, reward, next_state, done))
训练神经网络⭐️
最终要的部分,不同的算法,基本上也就是这一部分存在差异。
代码流程如下:
- 缓冲区中采样一个batch的数据
- 计算当前q值和目标q值(不同的dqn算法,主要是这两步计算的方式不同)
- 计算损失,除了后面策略网络需要自己构造损失函数,其他的基本都是用MSELoss,也就是当前q值和目标q值的平方差,
- 梯度下降&更新网络,这两部都有现成的库来完成,基本上也是固定代码,
def train(self):# 判断是否有足够经验用例用来学习if len(self.replay_buffer) < self.batch_size:return# 从缓冲区随机采样batch = random.sample(self.replay_buffer, self.batch_size)states, actions, rewards, next_states, dones = zip(*batch)states_tensor = torch.FloatTensor(np.array(states))actions_tensor = torch.LongTensor(actions)rewards_tensor = torch.FloatTensor(rewards)next_states_tensor = torch.FloatTensor(np.array(next_states))dones_tensor = torch.FloatTensor(dones)# 计算当前q值current_q = self.q_net(states_tensor).gather(1, actions_tensor.unsqueeze(1)).squeeze()# 计算目标q值with torch.no_grad():# 使用目标网络计算下一状态的所有动作价值,选择动作价值最大的动作# 这里蕴含了两步:使用目标网络计算价值+动作选择next_q = self.target_net(next_states_tensor).max(1)[0]target_q = rewards_tensor + self.gamma * next_q * (1 - dones_tensor)# 构建损失函数,两者的平方差loss = nn.MSELoss()(current_q,target_q)# 梯度下降,网络更新,固定代码self.optimizer.zero_grad()loss.backward()self.optimizer.step()# 计算步数self.step_count += 1# 每隔一定步数更新目标网络,即复制主网络(q_net)的全部参数至目标网络中if self.step_count % self.update_target_freq == 0:self.target_net.load_state_dict({k: v.clone() for k, v in self.q_net.state_dict().items()})
保存模型
def save_model(self,path="./output/best_model_bak.pth"):torch.save(self.q_net.state_dict(),path)print(f"Model saved to {path}")
评估模型
这一步用来评估当前网络的好坏,我们评估得分更高的网络进行保存。
评估的逻辑如下:
- 从初始状态开始,使用我们训练的q网络选择动作控制杆子
- 累加轨迹下每个步骤获得的reward作为当前网络的得分
- 一直到杆子倒下(说明网络效果不够好)
- 或者分数足够高(说明网络可以控制杆子很长时间保持平衡,网络效果效果很好),再跳出循环
- 评估一定轮数后,取分数均值作为评估结果,分数越高越好
代码如下:
def evaluate(self, env):# 入参是游戏环境,需要一个新的环境进行评估# 由于模型评估不需要随机探索,因此记录当前的epsilon,并设置智能体epsilon=0origin_epsilon = self.epsilonself.epsilon = 0total_rewards = []# self.eval_episodes表示评估的轮数for _ in range(self.eval_episodes):state = env.reset()[0]episode_reward = 0while True:# 使用智能体选择动作action = self.choose_action(state)# 与环境交互next_state, reward, done, _, _ = env.step(action)# 得到reward和下一状态episode_reward += rewardstate = next_state# 判断杆子是否倒下、分数是否足够高if done or episode_reward>2e4:breaktotal_rewards.append(episode_reward)# 网络需要回到评估前的状态继续训练,因此恢复epsilon的值self.epsilon = origin_epsilon# 返回平均分数return np.mean(total_rewards)
主流程
- 初始化游戏环境:游戏环境用于选择动作后交互,并获取reward和下一状态,初始化后获得初始状态
- 初始化智能体:构建智能体对象,设置epsilon=1,因为初始q网络效果不好,所以设置很大的epsilon让智能体自由探索环境,设置训练的episode数目
- 对于每个episode:
- 对于episode中的每一步
- 使用智能体选择动作
- 与环境交互,并获得下一状态next_state,该动作的奖励值reward,杆子是否倒下done,存储本次经验
- 智能体训练
- 当一个episode结束后,更新一次epsilon参数,使epsilon慢慢衰减,这是因为随着训练,q网络效果变好,这时我们开始慢慢相信q网络给我们做出的选择
- 每10个episode我们对q网络做一次评估,存储最佳的q网络
- 对于episode中的每一步
代码如下:
if __name__ == '__main__':env = gym.make('CartPole-v1')state_dim = env.observation_space.shape[0]action_dim = env.action_space.nagent = DQNAgent(state_dim, action_dim)config = {"episode": 600,"epsilon_start": 1.0,"epsilon_end": 0.01,"epsilon_decay": 0.995,}agent.epsilon = config["epsilon_start"]for episode in range(config["episode"]):state = env.reset()[0]total_reward = 0while True:action = agent.choose_action(state)next_state, reward, done, _, _ = env.step(action)agent.store_experience(state, action, reward, next_state, done)agent.train()total_reward += rewardstate=next_stateif done or total_reward > 2e4:breakagent.epsilon = max(config["epsilon_end"],agent.epsilon*config["epsilon_decay"])if episode % 10 == 0:eval_env = gym.make('CartPole-v1')avg_reward = agent.evaluate(eval_env)eval_env.close()if avg_reward > agent.best_avg_reward:agent.best_avg_reward = avg_rewardagent.save_model(path=f"output/best_model.pth")print(f"new best model saved with average reward: {avg_reward}")print(f"Episode: {episode},Train Reward: {total_reward},Best Eval Avg Reward: {agent.best_avg_reward}")
DDQN
DDQN与DQN的区别是:
- DQN使用目标网络选择动作并计算q值
- DDQN使用主网络(q_net)选择动作,使用目标网络计算q值
因此DDQN和DQN的代码区别仅有一行代码:
# DQNcurrent_q = self.q_net(states_tensor).gather(1, actions_tensor.unsqueeze(1)).squeeze()with torch.no_grad():# 根据目标网络计算出来的所有Q值,选择了Q值最大的动作next_q = self.target_net(next_states_tensor).max(1)[0]target_q = rewards_tensor + self.gamma * next_q * (1 - dones_tensor)# DDQNcurrent_q = self.q_net(states_tensor).gather(1, actions_tensor.unsqueeze(1)).squeeze()with torch.no_grad():# 使用主网络选择下一状态要执行的动作next_actions = torch.LongTensor(torch.argmax(self.q_net(next_states_tensor),dim=1))# 使用目标网络计算执行这个动作后的q值next_q=self.target_net(next_states_tensor).gather(1,next_actions.unsqueeze(1)).squeeze()target_q = rewards_tensor + self.gamma * next_q * (1 - dones_tensor)
Dueling DQN
Dueling DQN与DQN的区别主要是神经网络结构不同
- DQN是简单的神经网络,输入状态,输出不同动作的价值
- Dueling DQN的网络内有两个分支
- 价值流:记录当前状态的状态价值,为标量
- 优势流:表示该动作的优势,与动作价值的作用相同,都是评估动作的好坏,是一个动作维度的向量
直接看代码,Dueling DQN的网络结构如下:
class QNetwork(nn.Module):def __init__(self, state_dim, action_dim):super(QNetwork, self).__init__()self.feature = nn.Sequential(nn.Linear(state_dim, 128),nn.ReLU())self.advantage = nn.Sequential(nn.Linear(128, 128),nn.ReLU(),nn.Linear(128, action_dim))self.value = nn.Sequential(nn.Linear(128, 128),nn.ReLU(),nn.Linear(128, 1))def forward(self, x):x = self.feature(x)# 优势流advantage = self.advantage(x)# 价值流value = self.value(x)# advantage - advantage.mean()消除优势函数基线影响return value + advantage - advantage.mean()
Sarsa
对比一下DQN和Sarsa:
- DQN在更新 Q 函数时使用了下一个状态 s ′ s' s′ 的最大动作价值: max a ′ Q t ( s ′ , a ′ ) \max_{a'} Q_t(s', a') maxa′Qt(s′,a′)。
- 这意味着 DQN并不关心智能体实际采取的下一个动作,而是假设智能体将在下一个状态采取具有最大动作价值的动作。
- 这使得 DQN能够学习到最优策略,而不受当前策略的影响。
- 但是它可能会过于乐观地估计动作价值
- Sarsa在更新q_net时,它使用了智能体实际采取的下一个动作 a ′ a' a′ 的动作价值: Q t ( s ′ , a ′ ) Q_t(s', a') Qt(s′,a′)。
- 这意味着 SARSA 在学习过程中会考虑智能体当前的策略。
- 因此,SARSA 学到的策略与智能体在训练过程中实际执行的策略密切相关。
由此,DQN和Sarsa的核心区别是:采取下一个动作的方式,在代码上的区别也是一行代码:
# 使用DQNcurrent_q = self.q_net(states_tensor).gather(1, actions_tensor.unsqueeze(1)).squeeze()with torch.no_grad():# 使用目标网络选择Q值最大的动作next_q = self.target_net(next_states_tensor).max(1)[0]target_q = rewards_tensor + self.gamma * next_q * (1 - dones_tensor)# 使用Sarsacurrent_q = self.q_net(states_tensor).gather(1, actions_tensor.unsqueeze(1)).squeeze()with torch.no_grad():# 使用当前策略选出下一个动作next_actions_tensor = self.choose_actions(next_states)# 根绝当前策略的下一个动作计算q值next_q=self.target_net(next_states_tensor).gather(1,next_actions_tensor.unsqueeze(1)).squeeze()target_q = rewards_tensor + self.gamma * next_q * (1 - dones_tensor)
再对比一下DDQN和Sarsa:
# 采用ddqn的方式current_q = self.q_net(states_tensor).gather(1, actions_tensor.unsqueeze(1)).squeeze()with torch.no_grad():next_actions = torch.LongTensor(torch.argmax(self.q_net(next_states_tensor),dim=1))next_q=self.target_net(next_states_tensor).gather(1,next_actions.unsqueeze(1)).squeeze()target_q = rewards_tensor + self.gamma * next_q * (1 - dones_tensor)# 使用Sarsacurrent_q = self.q_net(states_tensor).gather(1, actions_tensor.unsqueeze(1)).squeeze()with torch.no_grad():# 使用当前策略选出下一个动作next_actions = self.choose_actions(next_states)# 根绝当前策略的下一个动作计算q值next_q=self.target_net(next_states_tensor).gather(1,next_actions.unsqueeze(1)).squeeze()target_q = rewards_tensor + self.gamma * next_q * (1 - dones_tensor)
可以看到只有计算next_actions时不同,ddqn使用主网络来计算Q值,但是会选择Q值最大的动作;而Sarsa完全依赖当前策略做选择,意味着不一定选择了Q值最大的动作
QAC&A2C
对于actor-critic类算法,我们需要两个神经网络,分别代表运动员(PolicyNet)和裁判员(ValueNet)
运动员网络的作用是在游戏中根据策略做出动作
裁判员网络的作用是对运动员的动作进行打分
两个网络都需要不停地迭代,运动员的目标是得到更高的分数,裁判员的目标是更公正的评分。
actor-critic类算法与Q-Learning类算法相比,内部逻辑存在较大区别,不过我们仅需要修改智能体内部函数内的实现,整个智能体暴露出来的接口和DQN的接口结构仍然相同,因此在主函数里使用智能体的代码和DQN没有区别。
下面详细介绍actor-critic算法实现智能体的方式。
QAC
构建策略网络
输入的是当前的状态;输出一个动作维度的向量,代表执行不同动作的概率
class PolicyNet(nn.Module):def __init__(self, state_dim, action_dim):super(PolicyNet, self).__init__()self.fc1 = nn.Linear(state_dim, 128)self.fc2 = nn.Linear(128, action_dim)def forward(self, x):x = self.fc1(x)x = torch.relu(x)x = self.fc2(x)x = torch.softmax(x, dim=-1)return x
构建状态价值网络
输入的是当前的状态;输出一个标量,表示给定状态的状态价值
class ValueNet(nn.Module):def __init__(self, state_dim):super(ValueNet, self).__init__()self.fc1 = nn.Linear(state_dim, 128)self.fc2 = nn.Linear(128, 1)def forward(self, x):x = self.fc1(x)x = torch.relu(x)x = self.fc2(x)return x
智能体初始化参数
def __init__(self,state_dim,action_dim):# 两个网络self.critic = ValueNet(state_dim)self.actor = PolicyNet(state_dim,action_dim)# 优化相关self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=0.001) # 优化器self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=0.001)self.critic_criterion = nn.MSELoss() # 损失函数# 最优相关self.gamma = 0.99 # 折损率self.best_avg_reward=0 #最佳网络的奖励self.eval_episodes = 5 #评估轮数
动作选择
def choose_action(self,state):# 状态转为张量state_tensor = torch.FloatTensor(state)# 状态输入至神经网络得到每个动作的概率probs = self.actor(state_tensor)# 使用上面的概率分布构造抽样,抽样得到动作action = torch.multinomial(probs, 1).item()return action
这里的含义是,比如当前策略网络输出[0.7,0.3],表示向左的概率是0.7,向右的概率是0.3,那么我们使用这个策略网络选择动作10次,可能有7次向左,3次向右。
训练过程
训练过程中需要对两个网络都进行优化:
- critic更新
# critic更新# 获取给定状态的状态价值value = self.critic(state_tensor)# 获取下一状态的状态价值next_value_tensor = self.critic(next_state_tensor)# 计算targettarget = rewards_tensor + self.gamma * next_value_tensor * (1 - done_tensor)# 使用td_error=target-value的平方差作为损失函数critic_loss = self.critic_criterion(value,target)# 梯度下降优化网络self.critic_optimizer.zero_grad()critic_loss.backward()self.critic_optimizer.step()
- actor更新
# actor更新# td_error = target - value# 重新通过actor获取action概率action_probs = self.actor(state_tensor)action_prob = action_probs.gather(0, action_tensor)# 关键一行🌟,构造actor损失函数actor_loss = (-torch.log(action_prob) * value.detach()).mean()# 梯度下降优化网络self.actor_optimizer.zero_grad()actor_loss.backward()self.actor_optimizer.step()
actor更新中,最关键的就是损失函数的构建,它代表的数学公式是
L actor = − E [ log π θ ( a ∣ s ) ⋅ V ( s ) ] L_{\text{actor}} = -\mathbb{E}[\log \pi_\theta(a|s) \cdot V(s)] Lactor=−E[logπθ(a∣s)⋅V(s)]
实际上由于真实期望无法求得,采用了蒙特卡洛近似,也就是使用单次采样值:
L actor = − log π θ ( a ∣ s ) ⋅ V ( s ) L_{\text{actor}} = -\log \pi_\theta(a|s) \cdot V(s) Lactor=−logπθ(a∣s)⋅V(s)
其中:
- π θ ( a ∣ s ) \pi_\theta(a|s) πθ(a∣s) 是策略函数,表示在状态 s s s下选择动作 a a a的概率
- V ( s ) V(s) V(s) 是状态价值函数,代码中用
value
表示 - log π θ ( a ∣ s ) \log \pi_\theta(a|s) logπθ(a∣s) 是对策略概率取对数
直观理解:
- 这个损失函数的目标是增加那些价值高的状态-动作对的概率
- 也就是最大化 log π θ ( a ∣ s ) ⋅ V ( s ) \log \pi_\theta(a|s) \cdot V(s) logπθ(a∣s)⋅V(s),此时使用梯度上升优化即可
- 而通常的优化器是梯度下降算法,因此在前面增加负号
模型评估与模型保存
模型评估与DQN中并无区别
模型保存时需要保存两个网络:
def save_model(self):if not os.path.exists("output"):os.makedirs("output")torch.save(self.critic.state_dict(),'./output/critic_best_model.pth')torch.save(self.actor.state_dict(), './output/actor_best_model.pth')
A2C
搞懂QAC之后,A2C很简单,它是在QAC的基础上进行优化。
QAC的actor网络损失函数是直接使用的价值函数与动作概率构建:
L actor = − E [ log π θ ( a ∣ s ) ⋅ V ( s ) ] L_{\text{actor}} = -\mathbb{E}[\log \pi_\theta(a|s) \cdot V(s)] Lactor=−E[logπθ(a∣s)⋅V(s)]
A2C的actor网络损失函数是使用优势函数与动作概率构建:
L actor = − E [ log π θ ( a ∣ s ) ⋅ A ( s , a ) ] L_{\text{actor}} = -\mathbb{E}[\log \pi_\theta(a|s) \cdot A(s,a)] Lactor=−E[logπθ(a∣s)⋅A(s,a)]
其中, A ( s , a ) A(s,a) A(s,a)是优势函数,通常计算为:
A ( s , a ) = Q ( s , a ) − V ( s ) ≈ r + γ V ( s ′ ) − V ( s ) A(s,a) = Q(s,a) - V(s) \approx r + \gamma V(s') - V(s) A(s,a)=Q(s,a)−V(s)≈r+γV(s′)−V(s)
这里:
- π θ ( a ∣ s ) \pi_\theta(a|s) πθ(a∣s) 是策略网络,表示在状态 s s s下选择动作 a a a的概率
- r r r 是即时奖励
- γ \gamma γ 是折扣因子
- V ( s ) V(s) V(s) 是当前状态 s s s的价值估计
- V ( s ′ ) V(s') V(s′) 是下一状态 s ′ s' s′的价值估计
A2C使用优势函数 A ( s , a ) A(s,a) A(s,a),这能显著减少梯度估计的方差,提高训练稳定性和效率。
代码上的区别也仅仅两行:
# qac actor更新# td_error = target - value# 重新通过actor获取action概率action_probs = self.actor(state_tensor)action_prob = action_probs.gather(0, action_tensor)# 这里是用value就是q值,此时是actor-criticactor_loss = (-torch.log(action_prob) * value.detach()).mean()# 梯度下降优化网络self.actor_optimizer.zero_grad()actor_loss.backward()self.actor_optimizer.step()# a2c actor更新# 计算优势函数,也是td_error,参考上面优势函数的计算,两者等价td_error = target - value# 重新通过actor获取action概率,以便建立梯度图action_probs = self.actor(state_tensor)action_prob = action_probs.gather(0, action_tensor)# 关键区别⭐️,损失函数的构建,这里使用的是td_error,而qac使用的valueactor_loss = (-torch.log(action_prob) * td_error.detach()).mean()self.actor_optimizer.zero_grad()actor_loss.backward()self.actor_optimizer.step()
相关文章:
强化学习算法实战:一个例子搞懂sarsa、dqn、ddqn、qac、a2c及其区别
简介 在学习强化学习算法:sarsa、dqn、ddqn、qac、a2c、trpo、ppo时,由于有大量数学公式的推导,觉得十分晦涩,且听过就忘记了。 但是当把算法应用于实战时,代码的实现要比数学推导直观很多。 接下来通过不同的算法实现…...
文章记单词 | 第86篇(六级)
一,单词释义 pretty /ˈprɪti/- adj. 漂亮的;相当的 /adv. 相当地labour /ˈleɪbə(r)/- n. 劳动;劳工;分娩 /v. 劳动;努力(英式英语, labor)imaginary /ɪˈmdʒɪnəri/- adj. …...
firewall防火墙
一.Firewalld 防火墙概述 1.firewalld 简介 firewalld 的作用是为包过滤机制提供匹配规则(或称为策略),通过各种不同的规则告诉netfilter 对来自指定源、前往指定目的或具有某些协议特征的数据包采取何种处理方式为了更加方便地组织和管理防火墙,firewa11d 提供了…...
TII-2024《AGP-Net: Adaptive Graph Prior Network for Image Denoising》
推荐深蓝学院的《深度神经网络加速:cuDNN 与 TensorRT》,课程面向就业,细致讲解CUDA运算的理论支撑与实践,学完可以系统化掌握CUDA基础编程知识以及TensorRT实战,并且能够利用GPU开发高性能、高并发的软件系统…...
Pageassist安装(ollama+deepseek-r1)
page-assist网站:https://github.com/n4ze3m/page-assist 首先电脑配置node.js,管理员打开命令窗口输入下面命令下载bun npm install -g buncd 到你想要安装page-assist的地方(推荐桌面) 输入下列命令 git clone https://gith…...
Java—— 方法引用 : :
方法引用是什么 把已经存在的方法拿过来用,当做函数式接口中抽象方法的方法体 方法引用符 :: 方法引用的条件 1.需要有函数式接口 2.被引用方法必须已经存在 3.被引用方法的形参和返回值需要跟抽象方法保持一致 4.被引用方法的功能要满足当前…...
Linux基础开发工具大全
目录 软件包管理器 1>软件包 2>软件生态 3>yum操作 a.查看软件包 b.安装软件 c.卸载软件 4>知识点 vim编辑器 1>基本概念 2>基本操作 3>正常模式命令集 a.模式切换 b.移动光标 c.删除 d.复制 e.替换 f.撤销 g.更改 4>底行模式命令…...
C语言实现INI配置文件读取和写入
一.INI文件介绍 INI配置文件是一种简单的文本文件,用于存储配置信息,通常由一个或多个节(section)组成,每个节包含多个键值对(Key-Value)格式。INI文件易于阅读和编辑,广泛应用于多…...
volatile关键字详解
volatile关键字详解 1. 定义与核心作用 volatile 是Java中的关键字,用于修饰变量,主要解决多线程环境下的内存可见性和指令重排序问题。其核心作用: 保证可见性:确保所有线程读取到变量的最新值。禁止指令重排序:防止…...
二叉树子树判断:从递归到迭代的全方位解析
一、题目解析 题目描述 给定两棵二叉树root和subRoot,判断root中是否存在一棵子树,其结构和节点值与subRoot完全相同。 示例说明 示例1: root [3,4,5,1,2],subRoot [4,1,2] 返回true,因为root的左子树与subRoot完…...
【PhysUnits】4.1 类型级比特位实现解释(boolean.rs)
一、源码 该代码实现了一个类型级(type-level)的布尔系统,允许在编译时进行布尔运算。 //! 类型级比特位实现 //! //! 这些是基础的比特位类型,作为本库中其他数值类型的构建基础 //! //! 已实现的**类型运算符**: //! //! - 来自 core::op…...
(7)python开发经验
文章目录 1 找不到资源文件2 使用subprocess执行时有黑色弹窗3 找不到exec4 pyside6-project lupdate的bug5 找不到pyd模块6 pyd模块编码错误7 运行显示Qt platform plugin "windows" in "8 tr()包含的字符串无法被翻译 更多精彩内容👉内容导航 &…...
【Manim】使用manim画一个高斯分布的动画
1 Manim例子一 最近接触到manim,觉得挺有趣的,来玩一玩把。如下是一个使用manim画的高斯分布的动画。 from manim import * import numpy as npclass GaussianDistribution(Scene):def construct(self):# 创建坐标系axes Axes(x_range[-4, 4, 1],y_ra…...
Day11-苍穹外卖(数据统计篇)
前言: 今天写day11的内容,主要讲了四个统计接口的制作。看起来内容较多,其实代码逻辑都是相似的,这里我们过一遍。 今日所学: Apache ECharts营业额统计用户统计订单统计销量排行统计 1. Apache ECharts 1.1 介绍 A…...
论文阅读:Self-Collaboration Code Generation via ChatGPT
地址:Self-Collaboration Code Generation via ChatGPT 摘要 尽管大型语言模型(LLMs)在代码生成能力方面表现出色,但在处理复杂任务时仍存在挑战。在现实软件开发中,人类通常通过团队协作来应对复杂任务,…...
LocaleContextResolver实现多语言切换-笔记
1. LocaleContextResolver功能简介 org.springframework.web.servlet.LocaleContextResolver是 Spring MVC 中用于解析和管理用户 Locale(语言环境) 的核心接口。 //LocaleContextResolver 接口定义 public interface LocaleContextResolver extends L…...
Vue3中setup运行时机介绍
在 Vue3 中,直接写在 <script setup>...</script> 中的代码运行时机可以分为以下几个关键阶段: 一、执行顺序层级 #mermaid-svg-bF3p98MiNdLfcoSG {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#33…...
计算机视觉----感兴趣区域(ROI)、非极大值抑制
感兴趣区域(Region of Interest,ROI)是指在一幅图像或者数据集中,用户关注并希望进行重点分析、处理或者研究的特定区域。以下为你详细介绍它在不同领域的应用: 医学影像领域 在医学影像中,医生可以通过确…...
YOLO11解决方案之对象裁剪探索
概述 Ultralytics提供了一系列的解决方案,利用YOLO11解决现实世界的问题,包括物体计数、模糊处理、热力图、安防系统、速度估计、物体追踪等多个方面的应用。 对象裁剪是指从图像或视频中分离并提取特定的检测对象,YOLO11 模型功能可用于准…...
将单链表反转【数据结构练习题】
- 第 98 篇 - Date: 2025 - 05 - 16 Author: 郑龙浩/仟墨 反转单链表(出现频率非常的高) 文章目录 反转单链表(出现频率非常的高)题目:反转一个链表思路:代码实现(第3种思路): 题目:反转一个链表 将 1->2->3->4->5->NULL反转…...
多网卡管理实战指南:原理、问题分析与实用工具推荐
多网卡管理实战指南:原理、问题分析与实用工具推荐 在现代网络环境中,越来越多的用户面临一个实际问题:一台电脑连接了多个网络接口,如有线 无线、双有线、或实体网卡 虚拟VPN网卡。这种“多网卡”环境虽然提供了更多可能性&am…...
qt5.14.2 opencv调用摄像头显示在label
ui界面添加一个Qlabel名字是默认的label 还有一个button名字是pushButton mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include <opencv2/opencv.hpp> // 添加OpenCV头文件 #include <QTimer> // 添加定…...
使用Python实现简单的人工智能聊天机器人
最近研学过程中发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击链接跳转到网站人工智能及编程语言学习教程。读者们可以通过里面的文章详细了解一下人工智能及其编程等教程和学习方法。下面开始对正文内容的…...
Python爬虫(28)Python爬虫高阶:Selenium+Splash双引擎渲染实战与性能优化
目录 一、背景:动态渲染技术的演进与挑战二、核心技术对比与选型三、环境搭建与工具链配置1. Docker部署Splash集群2. Selenium环境配置 四、双引擎渲染核心实现1. 智能路由中间件2. Splash高级Lua脚本控制 五、性能优化实战方案1. 浏览器资源池化2. 异步渲染加速 六…...
uv python 卸载
又是查了半天 官网wiki没有 网上一堆傻子胡说 uv提示也不对 AI还在这尼玛胡编乱造 开始 我原来装了这几个环境 uv python list 现在python3.7.7不需要了,卸载,直接 uv python uninstall 3.7.7 去找你自己要卸载的版本号,不需要整个包名复制…...
如何备考GRE?
1.引言 GRE和雅思不太相同,首先GRE是美国人的考试,思维方式和很多细节和英系雅思不一样。所以底层逻辑上我觉得有点区别。 难度方面,我感觉GRE不容易考低分,但考高分较难。雅思就不一样了不仅上限难突破,下限还容易6…...
Crowdfund Insider聚焦:CertiK联创顾荣辉解析Web3.0创新与安全平衡之术
近日,权威金融科技媒体Crowdfund Insider发布报道,聚焦CertiK联合创始人兼CEO顾荣辉教授在Unchained Summit的主题演讲。报道指出,顾教授的观点揭示了Web3.0生态当前面临的挑战,以及合规与技术在推动行业可持续发展中的关键作用。…...
第六章 进阶10 实习生的焦虑
时间过得很快,实习的蕾蕾入职已经三个月了,到了离开的日子。 照例我和她约了1对1谈话,在开始和结束阶段的谈话格外有意义。 谈话的最后,我问蕾蕾有没有什么问题问我,她的问题让我格外惊讶: “自己有点焦…...
技术融资:概念与形式、步骤与案例、挑战与应对、发展趋势
一、技术融资概述 技术融资是指通过外部资金支持技术研发、产品开发或市场扩展的过程。它通常涉及风险投资、天使投资、私募股权、众筹等多种形式。技术融资的核心目标是为技术创新提供资金保障,推动技术从概念到市场的转化。 技术融资的主要形式包括以下几种&…...
duxapp 2025-03-29 更新 编译结束的复制逻辑等
CLI copy 文件夹内的内容支持全量复制优化小程序配置文件合并逻辑(更新后建议将 project.config.json 文件从git的追踪中移除)新增 copy.build.complete 文件夹的复制逻辑,会在程序编译结束之后将文件复制到指定位置 (模块和用户…...
【Linux】Shell脚本中向文件中写日志,以及日志文件大小、数量管理
1、写日志 shell脚本中使用echo命令,将字符串输入到文件中 覆盖写入:echo “Hello, World!” > laoer.log ,如果文件不存在,则会创建文件追加写入:echo “Hello, World!” >> laoer.log转移字符:echo -e “Name:\tlaoer\nAge:\t18” > laoer.log,\t制表符 …...
Qwen3技术报告
参考链接:https://zhuanlan.zhihu.com/p/1905945819108079268 介绍的很详细,先贴后续再整理...
python + flask 做一个图床
1. 起因, 目的: 对这个网站:https://img.vdoerig.com/ , 我也想实现这种效果。做一个简单的图床,后面,可以结合到其他项目中。 2. 先看效果 实际效果。 3. 过程: Grok 聊天: https://img.vdoerig.co…...
虚拟来电 4.3.0 |集虚拟来电与短信于一体,解锁VIP优雅脱身
虚拟来电是一款集虚拟来电与虚拟短信于一体的应用程序。它可以帮助用户在需要时模拟一个真实的来电或短信,以最顾及对方情面的方式逃离尴尬场合。无论是自定义来电联系人、时间、次数,还是设置自定义通话语音、来电震动和铃声,这款解锁了VIP功…...
日志与策略模式
什么是设计模式 IT⾏业 ,为了让 菜鸡们不太拖⼤佬的后腿, 于是⼤佬们针对⼀些经典的常⻅的场景, 给定了⼀些对应的解决⽅案, 这个就是 设计模式 日志认识 计算机中的⽇志是记录系统和软件运⾏中发⽣事件的⽂件,主要作⽤是监控运⾏状态、记录异常信 息ÿ…...
Linux下载与安装
一、YUM 1.1 什么是YUM 在CentOS系统中,软件管理方式通常有三种方式:rpm安装、yum安装以及编译(源码)安装。 编译安装,从过程上来讲比较麻烦,包需要用户自行下载,下载的是源码包,需…...
java18
1.API之时间类 Date类: SimpleDateFormat类: Calendar类:...
向量和矩阵范数
向量和矩阵范数 向量范数 定义 设 x T \boldsymbol{x}^\text{T} xT, y T \boldsymbol{y}^\text{T} yT ∈ K n \in \mathbb{K}^n ∈Kn,数量积定义为: y T x ( 或 y H x ) \boldsymbol{y} ^\text{T} \boldsymbol{x}\left(或\boldsymbol{y}^\text{H}\bo…...
使用 gcloud CLI 自动化管理 Google Cloud 虚拟机
被操作的服务器,一定要开启API完全访问权限,你的电脑安装gcloud CLI前一定要先安装Python3! 操作步骤 下载地址,安装大概需要十分钟:https://cloud.google.com/sdk/docs/install?hlzh-cn#windows 选择你需要的版本&a…...
驱动芯片走线、过孔指导,大电流、散热过孔
参考: 一份大厂PCB布局指南参考! 技巧 使用大面积铺铜 铜是一种极好的导热体。由于 PCB 的基板材料(FR-4 玻璃环氧树脂)是一种不良导热体。因此,从热管理的角度来看,PCB的铺铜区域越多则导热越理想。 走…...
数据结构进阶:AVL树与红黑树
目录 前言 AVL树 定义 结构 插入 AVL树插入的大致过程 更新平衡因子 旋转 右单旋 左单旋 左右双旋 右左双旋 实现 红黑树 定义 性质 结构 插入 实现 总结 前言 在学习了二叉搜索树之后,我们了解到其有个致命缺陷——当树的形状呈现出一边倒…...
AI人工智能在交通物流领域的应用
AI人工智能在交通物流领域的应用 AI人工智能在交通物流领域有着广泛而深入的应用,正推动着该领域的深刻变革,以下是详细介绍: 交通领域 智能驾驶 自动驾驶汽车:依靠深度学习算法、计算机视觉、激光雷达和传感器融合技术&#x…...
牛客网NC22222:超半的数
牛客网NC22222:超半的数 题目描述 输入输出格式 输入格式: 第一行包含一个整数 n (1 ≤ n ≤ 1000)第二行包含 n 个整数 a_i (1 ≤ a_i ≤ 10^9) 输出格式: 输出一个整数,表示出现次数超过一半的那个数 解题思路 这道题目有多种解法&a…...
在服务器上安装AlphaFold2遇到的问题(2)
如何删除已安装的cuDNN 1. 通过包管理器卸载(推荐) RHEL/CentOS (dnf/yum) #查看已安装的 cuDNN 包 sudo dnf list installed | grep cudnn #卸载 cuDNN 运行时和开发包 sudo dnf remove -y libcudnn* libcudnn8* libcudnn-devel* Ubuntu/Debian (ap…...
【2025年软考中级】第一章1.5 输入输出技术(外设)
文章目录 输入输出技术(外设)I/O设备总线结构输入输出控制程序控制方式中断方式直接内存存取(DMAC)方式IO通道方式和外围处理机(IOP)方式 数据传输方式生物特征认证技术 输入输出技术(外设&…...
2025 家用投影新标杆:雷克赛恩 CyberPro1 如何重新定义客厅观影体验
目录 一、家庭影音升级:从 “看得清” 到 “看得精” 的需求之变 (一)传统投影的痛点突围 (二)技术参数背后的用户价值 二、全天候观影无忧:亮度与环境光的博弈艺术 (一)真实亮…...
[基础] HPOP、SGP4与SDP4轨道传播模型深度解析与对比
HPOP、SGP4与SDP4轨道传播模型深度解析与对比 文章目录 HPOP、SGP4与SDP4轨道传播模型深度解析与对比第一章 引言第二章 模型基础理论2.1 历史演进脉络2.2 动力学方程统一框架 第三章 数学推导与摄动机制3.1 SGP4核心推导3.1.1 J₂摄动解析解3.1.2 大气阻力建模改进 3.2 SDP4深…...
12 web 自动化之基于关键字+数据驱动-反射自动化框架搭建
文章目录 一、如何实现一条用例,实现覆盖所有用例的测试1、结合数据驱动:编辑一条用例,外部导入数据实现循环测试2、用例体:实现不同用例的操作步骤对应的断言 二、实战1、项目路径总览2、common 文件夹下的代码文件3、keywords 文…...
学习状态不佳时的有效利用策略
当学习状态不佳时,可以尝试以下策略,将这段时间转化为有意义的活动,既不勉强自己又能为后续高效学习铺路: 1. 整理与规划:低精力高回报任务 整理学习环境:收拾书桌、归类资料、清理电脑文件,减…...
Spring Cloud深度实践:从服务发现到弹性智能API网关全景解析
引言 大家好!继初步搭建了微服务基础架构后,我们进一步深入到服务调用的优化、系统的弹性构建以及API网关的高级应用。本文将全面回顾这一进阶阶段的实践成果,通过更丰富的图解,力求清晰展现各核心组件的工作原理与协同方式。 项…...