【宇树机器人强化学习】(六):TensorBoard图表与手柄遥控go2测试

前言


0 前置知识 TensorBoard

0-1 介绍
  • TensorBoard 是由 TensorFlow 团队开发的可视化工具,但可以通过 torch.utils.tensorboard.SummaryWriterPyTorch 中使用。
  • 用途:监控训练过程中的各种指标、帮助调试和分析模型性能。
  • 它主要能做的事情:
    • 绘制训练损失曲线(Loss)
    • 绘制奖励曲线(Reward)
    • 绘制学习率变化(Learning rate)
    • 绘制自定义指标(如策略噪声、FPS 等)
    • 可视化模型图、参数直方图等

0-2 使用
  • 查看 TensorBoard:
bash 复制代码
tensorboard --logdir=runs
  • 其中--logdir=runs:告诉 TensorBoard 去 runs 目录读取日志
  • 然后在浏览器访问:http://localhost:6006

0-3 面板
  • 通常,TensorBoard 里有几个主要的面板(Tabs):
面板 显示的数据 要求 / 特点
Scalars 训练指标随迭代变化的曲线(Loss、Reward、Learning rate 等) 只要你用 add_scalar() 写入了数据,就能看到
Time Series 训练指标随真实时间(wall-clock time)变化的曲线 要求写入时使用 wall-clock timestampglobal_step 配合 summary_metadata.plugin_data 才能识别
  • 一般使用 Scalars 就够了,Time Series 很少用

1 TensorBoard图表与日志输出

1-1 日志输出代码一览
  • 我们曾经在第四期提到过运行训练文件train.py会有如下输出
bash 复制代码
################################################################################
                      Learning iteration 1499/1500                      

                       Computation: 36332 steps/s (collection: 0.584s, learning 0.077s)
               Value function loss: 0.0051
                    Surrogate loss: -0.0077
             Mean action noise std: 0.56
                       Mean reward: 19.65
               Mean episode length: 987.43
      Mean episode rew_action_rate: -0.1009
       Mean episode rew_ang_vel_xy: -0.0946
        Mean episode rew_collision: -0.0000
          Mean episode rew_dof_acc: -0.0900
   Mean episode rew_dof_pos_limits: -0.0045
        Mean episode rew_lin_vel_z: -0.0282
          Mean episode rew_torques: -0.0663
 Mean episode rew_tracking_ang_vel: 0.4226
 Mean episode rew_tracking_lin_vel: 0.9249
--------------------------------------------------------------------------------
                   Total timesteps: 36000000
                    Iteration time: 0.66s
                        Total time: 982.22s
                               ETA: 0.7s
  • 这个输出函数来自rsl_rl/runners/on_policy_runner.py中的log()
    • 当时在第三期讲解OnPolicyRunner跳过了这个函数,现在我们有了上一期各项奖励函数的解释和实现,我们现在可以很好的来看看这个平均奖励是如何实现的。

  • 下面是log函数的完整实现:
python 复制代码
def log(self, locs, width=80, pad=35):
	self.tot_timesteps += self.num_steps_per_env * self.env.num_envs
	self.tot_time += locs['collection_time'] + locs['learn_time']
	iteration_time = locs['collection_time'] + locs['learn_time']

	ep_string = f''
	if locs['ep_infos']:
		for key in locs['ep_infos'][0]:
			infotensor = torch.tensor([], device=self.device)
			for ep_info in locs['ep_infos']:
				# handle scalar and zero dimensional tensor infos
				if not isinstance(ep_info[key], torch.Tensor):
					ep_info[key] = torch.Tensor([ep_info[key]])
				if len(ep_info[key].shape) == 0:
					ep_info[key] = ep_info[key].unsqueeze(0)
				infotensor = torch.cat((infotensor, ep_info[key].to(self.device)))
			value = torch.mean(infotensor)
			self.writer.add_scalar('Episode/' + key, value, locs['it'])
			ep_string += f"""{f'Mean episode {key}:':>{pad}} {value:.4f}\n"""
	mean_std = self.alg.actor_critic.std.mean()
	fps = int(self.num_steps_per_env * self.env.num_envs / (locs['collection_time'] + locs['learn_time']))

	self.writer.add_scalar('Loss/value_function', locs['mean_value_loss'], locs['it'])
	self.writer.add_scalar('Loss/surrogate', locs['mean_surrogate_loss'], locs['it'])
	self.writer.add_scalar('Loss/learning_rate', self.alg.learning_rate, locs['it'])
	self.writer.add_scalar('Policy/mean_noise_std', mean_std.item(), locs['it'])
	self.writer.add_scalar('Perf/total_fps', fps, locs['it'])
	self.writer.add_scalar('Perf/collection time', locs['collection_time'], locs['it'])
	self.writer.add_scalar('Perf/learning_time', locs['learn_time'], locs['it'])
	if len(locs['rewbuffer']) > 0:
		self.writer.add_scalar('Train/mean_reward', statistics.mean(locs['rewbuffer']), locs['it'])
		self.writer.add_scalar('Train/mean_episode_length', statistics.mean(locs['lenbuffer']), locs['it'])
		self.writer.add_scalar('Train/mean_reward/time', statistics.mean(locs['rewbuffer']), self.tot_time)
		self.writer.add_scalar('Train/mean_episode_length/time', statistics.mean(locs['lenbuffer']), self.tot_time)

	str = f" \033[1m Learning iteration {locs['it']}/{self.current_learning_iteration + locs['num_learning_iterations']} \033[0m "

	if len(locs['rewbuffer']) > 0:
		log_string = (f"""{'#' * width}\n"""
					  f"""{str.center(width, ' ')}\n\n"""
					  f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
						'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
					  f"""{'Value function loss:':>{pad}} {locs['mean_value_loss']:.4f}\n"""
					  f"""{'Surrogate loss:':>{pad}} {locs['mean_surrogate_loss']:.4f}\n"""
					  f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
					  f"""{'Mean reward:':>{pad}} {statistics.mean(locs['rewbuffer']):.2f}\n"""
					  f"""{'Mean episode length:':>{pad}} {statistics.mean(locs['lenbuffer']):.2f}\n""")
					#   f"""{'Mean reward/step:':>{pad}} {locs['mean_reward']:.2f}\n"""
					#   f"""{'Mean episode length/episode:':>{pad}} {locs['mean_trajectory_length']:.2f}\n""")
	else:
		log_string = (f"""{'#' * width}\n"""
					  f"""{str.center(width, ' ')}\n\n"""
					  f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
						'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
					  f"""{'Value function loss:':>{pad}} {locs['mean_value_loss']:.4f}\n"""
					  f"""{'Surrogate loss:':>{pad}} {locs['mean_surrogate_loss']:.4f}\n"""
					  f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n""")
					#   f"""{'Mean reward/step:':>{pad}} {locs['mean_reward']:.2f}\n"""
					#   f"""{'Mean episode length/episode:':>{pad}} {locs['mean_trajectory_length']:.2f}\n""")

	log_string += ep_string
	log_string += (f"""{'-' * width}\n"""
				   f"""{'Total timesteps:':>{pad}} {self.tot_timesteps}\n"""
				   f"""{'Iteration time:':>{pad}} {iteration_time:.2f}s\n"""
				   f"""{'Total time:':>{pad}} {self.tot_time:.2f}s\n"""
				   f"""{'ETA:':>{pad}} {self.tot_time / (locs['it'] + 1) * (
						   locs['num_learning_iterations'] - locs['it']):.1f}s\n""")
	print(log_string)

1-2 具体实现
  • 我们来一步步看这里头是如何计算平均奖励并输出的
1-2-1 函数定义
python 复制代码
def log(self, locs, width=80, pad=35):
  • locs:类型是dict,用来传递本次训练迭代的各种局部指标(local statistics),比如:

    • locs['ep_infos']:本轮 episode 信息列表(reward、长度等)
    • locs['collection_time']:收集数据时间
    • locs['learn_time']:策略更新时间
    • locs['mean_value_loss'] / locs['mean_surrogate_loss']:损失
    • locs['rewbuffer'] / locs['lenbuffer']:奖励和长度缓存
  • width:用于控制打印日志的总宽度,使输出整齐。

  • pad:用于控制标签和值之间的对齐长度。


1-2-2 累加总步数和总时间
python 复制代码
self.tot_timesteps += self.num_steps_per_env * self.env.num_envs  
self.tot_time += locs['collection_time'] + locs['learn_time']  
iteration_time = locs['collection_time'] + locs['learn_time']
  • self.tot_timesteps:训练到现在总的环境交互步数。
    • self.num_steps_per_env * self.env.num_envs = 每个环境本次训练循环步数 × 环境数量。
  • self.tot_time:累加训练时间,包括收集数据时间 + 学习更新时间。
  • iteration_time:本次迭代花费的时间。

1-2-3 处理 episode 信息,计算平均奖励并写入 TensorBoard(核心)
python 复制代码
ep_string = f''
if locs['ep_infos']:
    for key in locs['ep_infos'][0]:
        infotensor = torch.tensor([], device=self.device)
        for ep_info in locs['ep_infos']:
            # handle scalar and zero dimensional tensor infos
            if not isinstance(ep_info[key], torch.Tensor):
                ep_info[key] = torch.Tensor([ep_info[key]])
            if len(ep_info[key].shape) == 0:
                ep_info[key] = ep_info[key].unsqueeze(0)
            infotensor = torch.cat((infotensor, ep_info[key].to(self.device)))
        value = torch.mean(infotensor)
        self.writer.add_scalar('Episode/' + key, value, locs['it'])
        ep_string += f"""{f'Mean episode {key}:':>{pad}} {value:.4f}\n"""
  • locs['ep_infos']:是一个列表,每个元素包含本轮收集到的 episode 信息,比如 reward、episode length 等。
  • 循环每个指标 key(如 reward、length):
    1. 把所有 episode 的值堆到一个 tensor 中。
    2. 对 tensor 取平均值 → 得到本次迭代的平均值。torch.mean()
    3. 写入 TensorBoard:Episode/<指标>
    4. 拼接成打印字符串 ep_string

1-2-4 处理 episode 信息并写入 TensorBoard
python 复制代码
mean_std = self.alg.actor_critic.std.mean()
fps = int(self.num_steps_per_env * self.env.num_envs / (locs['collection_time'] + locs['learn_time']))
  • mean_std:当前策略的动作噪声平均标准差(衡量探索程度)。
  • fps:每秒处理步数(Frame Per Second),用于性能指标。

1-2-5 写入 Loss 和性能指标到 TensorBoard
python 复制代码
self.writer.add_scalar('Loss/value_function', locs['mean_value_loss'], locs['it'])
self.writer.add_scalar('Loss/surrogate', locs['mean_surrogate_loss'], locs['it'])
self.writer.add_scalar('Loss/learning_rate', self.alg.learning_rate, locs['it'])
self.writer.add_scalar('Policy/mean_noise_std', mean_std.item(), locs['it'])
self.writer.add_scalar('Perf/total_fps', fps, locs['it'])
self.writer.add_scalar('Perf/collection time', locs['collection_time'], locs['it'])
self.writer.add_scalar('Perf/learning_time', locs['learn_time'], locs['it'])
  • self.writerPyTorch TensorBoard 的 SummaryWriter 对象 ,它负责把训练过程中的标量、图像、模型参数等写入 日志文件 ,以便你在 TensorBoard 中可视化。
    • value_function:价值函数损失。
    • surrogate:策略优化损失。
    • learning_rate:当前学习率。
    • mean_noise_std:动作噪声 std。
    • total_fps:每秒处理步数。
    • collection_time / learning_time:时间指标。

1-2-6 写入训练奖励和长度指标
python 复制代码
if len(locs['rewbuffer']) > 0:
    self.writer.add_scalar('Train/mean_reward', statistics.mean(locs['rewbuffer']), locs['it'])
    self.writer.add_scalar('Train/mean_episode_length', statistics.mean(locs['lenbuffer']), locs['it'])
    self.writer.add_scalar('Train/mean_reward/time', statistics.mean(locs['rewbuffer']), self.tot_time)
    self.writer.add_scalar('Train/mean_episode_length/time', statistics.mean(locs['lenbuffer']), self.tot_time)
  • locs['rewbuffer'] / locs['lenbuffer']:存储最近收集的 reward 和 episode 长度。
  • 写入 TensorBoard,可以按迭代或总时间做横坐标。

1-2-7 打印信息
python 复制代码
str = f" \033[1m Learning iteration {locs['it']}/{self.current_learning_iteration + locs['num_learning_iterations']} \033[0m "
print(log_string)  

1-3 使用TensorBoard
  • 完成一次训练后,我们打开最新一次训练的日志文件夹
    • 例如:unitree_rl_gym/logs/rough_go2/Mar19_14-09-10_
  • 我们可以看到这样的文件结构
bash 复制代码
.
├── logs
│   └── rough_go2
│       └── Mar19_14-09-10_
│           ├── events.out.tfevents.1773900551.lzh.25216.0
│           ├── model_0.pt
│            ....
│           ├── model_1500.pt
│      
  • 其中:
    • events.out.tfevents.*TensorBoard 日志文件,记录训练指标(如 reward、loss、FPS 等)。可以用 TensorBoard 打开查看训练曲线。
    • model_*.pt保存的模型权重文件,数字表示训练迭代次数。可以用来恢复训练或做推理。
  • 打开终端,进入训练日志的上级目录:
bash 复制代码
cd unitree_rl_gym/logs/rough_go2
tensorboard --logdir=Mar19_14-09-10_

1-4 查看图表
  • 声明:这个模型还没优化!!!只是随便训练1500轮的普通模型,这里展示仅做示范!!!

  • 由于我们在log函数中是通过self.writer.add_scalar写入的数据,因此我们在打开TensorBoard后点击左上方的SCALARS

  • 然后我们就可以很直观的看到各项指标的图表


1-4-1 Policy/mean_noise_std
  • 这里我们随便找个表来看看

  • mean_std:所有动作维度的平均噪声,它本质上控制探索强度

    • std 越大 → 动作随机性越大 → 探索越多;
    • std 越小 → 动作越确定 → 利用当前策略更多
  • 可以看到图中的曲线正在慢慢下降,这说明训练过程在逐步收敛,策略正在从"探索"过渡到"利用",这就是 PPO 中策略分布逐渐收缩(variance reduction)的表现


1-5 日志输出间隔
  • 详细了解完self.log()函数,我们来看看self.log()函数的调用:
  • 同样位于rsl_rl/runners/on_policy_runner.py
python 复制代码
 def learn(self, num_learning_iterations, init_at_random_ep_len=False):
       # 省略其他代码

	tot_iter = self.current_learning_iteration + num_learning_iterations
	for it in range(self.current_learning_iteration, tot_iter):
		start = time.time()
		# Rollout
		with torch.inference_mode():
			for i in range(self.num_steps_per_env):
				# 省略其他代码

			stop = time.time()
			collection_time = stop - start

			# Learning step
			start = stop
			self.alg.compute_returns(critic_obs)
		
		mean_value_loss, mean_surrogate_loss = self.alg.update()
		stop = time.time()
		learn_time = stop - start
		if self.log_dir is not None:
			self.log(locals())
		if it % self.save_interval == 0:
			self.save(os.path.join(self.log_dir, 'model_{}.pt'.format(it)))
		ep_infos.clear()
		
	self.current_learning_iteration += num_learning_iterations
	self.save(os.path.join(self.log_dir, 'model_{}.pt'.format(self.current_learning_iteration)))
  • 可以看到,self.log(locals())在每个iterations中被调用,而每个iterations又包含多个num_steps_per_env
    • 补充:locals()是 Python 的一个内置函数,会返回当前作用域中的所有局部变量(local variables),以字典形式返回
  • 也就是说,self.log中计算的Mean episode是基于 本 iteration 收集到的所有 episode(结束的轨迹)

1-6 自定义log函数
  • 在明白log函数后,我们就可以实现自己的log函数
  • 通常在训练过程中,除了可以观察各项指标的曲线,或者log函数计算的各项指标,我们还可以添加自己想显示的指标。
  • 举个例子,我想统计俯仰角超过阈值的机器人数量,那么我们可以这样实现:
python 复制代码
def log_envs(self):

	pos = self.env.root_states[:, :3]               
	rpy_deg = self.env.rpy * (180.0 / 3.1415926)    

	z_mean = pos[:, 2].mean().item()
	rpy_mean = rpy_deg.mean(dim=0).cpu().numpy().round(3)
	# 输出所有机器人当前iter的平均z以及rpy的角度值
	print(f"Mean z: {z_mean:.3f}, Mean RPY (deg): {rpy_mean}")

	 # 统计俯仰角超过阈值的机器人数量
	pitch_threshold = 10.0  # 设置俯仰角阈值
	over_threshold_count = torch.sum(torch.abs(rpy_deg[:, 0]) > pitch_threshold).item()  # 统计超出阈值的数量

	print(f"Number of robots with pitch over {pitch_threshold}°: {over_threshold_count}")
  • 需要注意的是,log函数会在每个iterations被调用,因此我们拿到的所有数据是单次iterations中全部的steps所产生的数据:
    • self.env.root_states[[x,y,z],[x,y,z],[x,y,z]....]
    • self.env.rpy[[r,p,y],[r,p,y],[r,p,y]....]
  • 同时需要注意的是,这些数据不是 np 数组,而是 torch.Tensor(通常在 GPU 上) ,如果需要进行进一步处理或打印,需要进行类型转换:
    • .cpu():从 GPU 拷贝到 CPU
    • .numpy():转为 NumPy 数组
    • .item():转为 Python 标量

  • 然后我们可以在log函数调用我们自定义的函数,就可以实现信息的打印了
python 复制代码
  def log(self, locs, width=80, pad=35):
    # 省略其他代码...
	print(log_string)
	self.log_envs()
  • 通过输出,我们可以看到俯仰角超过阈值的机器人在不断减少,机器人也在减少以头抢地的次数

2 手柄遥控测试

2-1 commands
  • 在我们实现自己的遥控代码检验模型之前,我们需要搞清楚一件事,也就是模型是如何进行目标设定的。
  • 我们打开legged_gym/envs/base/legged_robot_config.py
python 复制代码
class LeggedRobotCfg(BaseConfig):
    class commands:
        curriculum = False
        max_curriculum = 1.
        num_commands = 4 # default: lin_vel_x, lin_vel_y, ang_vel_yaw, heading (in heading mode ang_vel_yaw is recomputed from heading error)
        resampling_time = 10. # time before command are changed[s]
        heading_command = True # if true: compute ang vel command from heading error
        class ranges:
            lin_vel_x = [-1.0, 1.0] # min max [m/s]
            lin_vel_y = [-1.0, 1.0]   # min max [m/s]
            ang_vel_yaw = [-1, 1]    # min max [rad/s]
            heading = [-3.14, 3.14]
  • 其中关键的内容就是ranges,这里制定了训练目标的范围
  • 每个环境(机器人)都有一个 commands 向量:
python 复制代码
[lin_vel_x, lin_vel_y, ang_vel_yaw, heading]
  • 也就是在每次训练中,模型每隔一段时间(resampling_time)都会随机在这个范围随机挑选一个值进行训练,同时根据奖励项系数去逼近目标值
python 复制代码
class LeggedRobotCfg(BaseConfig):
  class rewards:
        class scales:
            tracking_lin_vel = 1.0
            tracking_ang_vel = 0.5
            ang_vel_xy = -0.05
  • 而这个command会被采样进观测值进行训练:
python 复制代码
obs = [
    base_vel,
    joint_pos,
    joint_vel,
    gravity,
    commands  ← 关键
]
  • 当然在 play 阶段,我们直接手动覆盖 env.commands,因此不会再使用 ranges 中的随机采样逻辑

2-2 手柄控制
  • 那么我们要做的事情很简单了,在运行play.py进行模型验证的时候,手动映射手柄遥感和LeggedRobotCfg中的commandsranges即可
  • 手柄的绑定我们使用最基础的pygame
  • 这里直接贴上我修改后的legged_gym/scripts/play.py
python 复制代码
import sys
from legged_gym import LEGGED_GYM_ROOT_DIR
import os
import sys
from legged_gym import LEGGED_GYM_ROOT_DIR

import isaacgym
from legged_gym.envs import *
from legged_gym.utils import  get_args, export_policy_as_jit, task_registry, Logger

import numpy as np
import torch

import pygame

def play(args):
    env_cfg, train_cfg = task_registry.get_cfgs(name=args.task)
    # override some parameters for testing
    env_cfg.env.num_envs = min(env_cfg.env.num_envs, 100)
    env_cfg.terrain.num_rows = 5
    env_cfg.terrain.num_cols = 5
    env_cfg.terrain.curriculum = False
    env_cfg.noise.add_noise = False
    env_cfg.domain_rand.randomize_friction = False
    env_cfg.domain_rand.push_robots = False
   
    env_cfg.env.test = True


    # prepare environment
    env, _ = task_registry.make_env(name=args.task, args=args, env_cfg=env_cfg)
    obs = env.get_observations()
    # load policy
    train_cfg.runner.resume = True


    ppo_runner, train_cfg = task_registry.make_alg_runner(env=env, name=args.task, args=args, train_cfg=train_cfg)
    policy = ppo_runner.get_inference_policy(device=env.device)
    
    # export policy as a jit module (used to run it from C++)
    if EXPORT_POLICY:
        path = os.path.join(LEGGED_GYM_ROOT_DIR, 'logs', train_cfg.runner.experiment_name, 'exported', 'policies')
        export_policy_as_jit(ppo_runner.alg.actor_critic, path)
        print('Exported policy as jit script to: ', path)

    # 配置游戏手柄
    gamepad=init_gamepad()

    for i in range(100*int(env.max_episode_length)):
        
        # 手柄设置机器人指令
        setCommand(env,gamepad)
   
        actions = policy(obs.detach())
        obs, _, rews, dones, infos = env.step(actions.detach())
    
def apply_deadzone(value, deadzone=0.1):
    if abs(value) < deadzone:
        return 0.0
    return value
def setCommand(env, gamepad):
    # 刷新输入设备状态(手柄/键盘)
    pygame.event.pump()
    # 左遥感:gamepad.get_axis(0):左边-1,右边1
    # 左遥感:gamepad.get_axis(1):前推-1,后推1  
    raw_vx = -gamepad.get_axis(1)
    raw_vy = -gamepad.get_axis(0)

    # 加死区
    vx = apply_deadzone(raw_vx, 0.1)
    vy = apply_deadzone(raw_vy, 0.1)
    print(vx,vy)
    env.commands[:, 0] = vx
    env.commands[:, 1] = vy

    # 只有在有输入时才更新方向
    if vx != 0 or vy != 0:
        heading = np.arctan2(vy, vx)      # yaw
        env.commands[:, 3] = heading      # heading
def init_gamepad():
    pygame.init()
    pygame.joystick.init()
    joystick = pygame.joystick.Joystick(0)
    joystick.init()
    return joystick

if __name__ == '__main__':
    EXPORT_POLICY = True
    RECORD_FRAMES = False
    MOVE_CAMERA = False
    args = get_args()
    play(args)
  • 注意:当 heading_command=True 时,ang_vel_yaw 会根据 heading 自动计算, 因此我们只需要控制 heading,不需要手动设置 yaw 角速度

  • 在模型训练好的情况下,可以看到go2基本整齐划一的朝着手柄方向前进

  • 根据go2和手柄制定方向的跟踪效果,我们可以直观的测试模型的稳定性。

  • 当然还是根据TensorBoard的图表区分析修改比较合理。


小结

  • 本期我们从训练日志出发,深入解析了 OnPolicyRunnerlog 函数的实现,搞清楚了平均奖励、损失函数等指标的计算方式,并结合 TensorBoard 对训练过程进行了可视化分析。同时,我们实现了基于手柄输入的实时控制,将训练好的策略应用到实际控制场景中,从而直观验证模型的稳定性与泛化能力,为后续奖励函数优化和策略改进打下基础。
  • 下一期我们来谈谈如何修改奖励函数来进一步完善模型
  • 如有错误,欢迎指出!感谢观看

  • 一些训练的彩蛋:

相关推荐
szcsun52 小时前
关于在pycharm中新建项目创建虚拟化环境venv
ide·python·pycharm
码路飞2 小时前
体验完阿里「悟空」之后,我花 2 小时用 Python 撸了个 AI Agent 🔥
python·aigc
万里沧海寄云帆2 小时前
pytorch+cpu版本对Intel Ultra 9 275HX性能的影响
人工智能·pytorch·python
java资料站2 小时前
python爬虫入门
python
1941s2 小时前
Google Agent Development Kit (ADK) 指南 第二章:环境搭建与快速开始
人工智能·python·adk·google agent
抓个马尾女孩2 小时前
位置编码:绝对位置编码、相对位置编码、旋转位置编码
人工智能·深度学习·算法·transformer
天下无贼2 小时前
【Python】2026版——FastAPI 框架快速搭建后端服务
后端·python·aigc
小蚂蚁i2 小时前
LangChain 完全学习手册:看完就能上手
后端·python·ai编程
renhongxia12 小时前
多模态融合驱动下的具身学习机制研究
运维·学习·机器人·自动化·知识图谱