深度强化学习课程文档

快速入门

Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始使用

入门:

要开始,请从这里下载项目(点击GDRL-IL-Project.zip旁边的下载图标)。zip文件包含“Starter”和“Complete”项目。

游戏代码已在 starter 项目中实现,并且节点已配置。我们将重点关注:

  • 实现 AIController 节点的代码,
  • 记录专家演示,
  • 训练代理并导出 .onnx 文件,我们可以在 Godot 中使用该文件进行推理。

在 Godot 中打开入门项目

解压 zip 文件,打开 Godot,点击“导入”,然后导航到解压后的存档中的Starter\Godot文件夹。

打开机器人场景

您可以在 FileSystem 搜索中搜索“robot”。

这个场景包含几个不同的节点,包括robot节点,它包含机器人的视觉形状,CameraXRotation节点用于在人工控制模式下使用鼠标旋转相机“上下”。AI 代理不控制此节点,因为它对于学习任务不是必需的。RaycastSensors节点包含两个 Raycast 传感器,可帮助代理“感知”游戏世界的某些部分,包括墙壁、地板等。

open robot scene

点击 AIController3D 旁边的卷轴以打开脚本进行编辑

您可能需要折叠“robot”分支以便更容易找到它,或者您可以在`Robot`节点上方的“Filter”框中输入`aicontroller`。

将 get_obs() 和 get_reward() 方法替换为以下实现:

func get_obs() -> Dictionary:
	var observations: Array[float] = []
	for raycast_sensor in raycast_sensors:
		observations.append_array(raycast_sensor.get_observation())

	var level_size = 16.0

	var chest_local = to_local(chest.global_position)
	var chest_direction = chest_local.normalized()
	var chest_distance = clampf(chest_local.length(), 0.0, level_size)
	
	var lever_local = to_local(lever.global_position)
	var lever_direction = lever_local.normalized()
	var lever_distance = clampf(lever_local.length(), 0.0, level_size)
		
	var key_local = to_local(key.global_position)
	var key_direction = key_local.normalized()
	var key_distance = clampf(key_local.length(), 0.0, level_size)
	
	var raft_local = to_local(raft.global_position)
	var raft_direction = raft_local.normalized()
	var raft_distance = clampf(raft_local.length(), 0.0, level_size)
	
	var player_speed = player.global_basis.inverse() * player.velocity.limit_length(5.0) / 5.0

	(
		observations
		.append_array(
			[
				chest_direction.x,
				chest_direction.y,
				chest_direction.z,
				chest_distance,
				lever_direction.x,
				lever_direction.y,
				lever_direction.z,
				lever_distance,
				key_direction.x,
				key_direction.y,
				key_direction.z,
				key_distance,
				raft_direction.x,
				raft_direction.y,
				raft_direction.z,
				raft_distance,
				raft.movement_direction_multiplier,
				float(player._is_lever_pulled),
				float(player._is_chest_opened),
				float(player._is_key_collected),
				float(player.is_on_floor()),
				player_speed.x,
				player_speed.y,
				player_speed.z,
			]
		)
	)
	return {"obs": observations}

func get_reward() -> float:
	return reward

get_obs()中,我们首先从检查器中添加到AIController3D节点的两个 Raycast 传感器获取观测值,并将它们添加到观测值中,然后获取到宝箱、杠杆、钥匙和木筏的相对位置向量,我们将其分为方向和距离,然后也将它们添加到观测值中。

我们还将其他游戏状态信息添加到观测值中

  • 杠杆是否已被拉动,
  • 钥匙是否已收集,
  • 宝箱是否已打开,
  • 玩家是否在地板上(也决定玩家是否可以跳跃),
  • 玩家的归一化局部速度。

我们将布尔值(如_is_lever_pulled)转换为浮点数(0 或 1)。

get_reward()中,我们只需返回当前奖励。

将 _physics_process() 和 reset() 方法替换为以下实现:

func _physics_process(delta: float) -> void:
	# Reset on timeout, this is implemented in parent class to set needs_reset to true,
	# we are re-implementing here to call player.game_over() that handles the game reset.
	n_steps += 1
	if n_steps > reset_after:
		player.game_over()

	# In training or onnx inference modes, this method will be called by sync node with actions provided,
	# For expert demo recording mode, it will be called without any actions (as we set the actions based on human input),
	# For human control mode the method will not be called, so we call it here without any actions provided.
	if control_mode == ControlModes.HUMAN:
		set_action()

	# Reset the game faster if the lever is not pulled.
	steps_without_lever_pulled += 1
	if steps_without_lever_pulled > 200 and (not player._is_lever_pulled):
		player.game_over()

func reset():
	super.reset()
	steps_without_lever_pulled = 0

将 get_action_space()、get_action() 和 set_action() 方法替换为以下实现:

# Defines the actions for the AI agent ("size": 2 means 2 floats for this action)
func get_action_space() -> Dictionary:
	return {
		"movement": {"size": 2, "action_type": "continuous"},
		"rotation": {"size": 1, "action_type": "continuous"},
		"jump": {"size": 1, "action_type": "continuous"},
		"use_action": {"size": 1, "action_type": "continuous"}
	}

# We return the action values in the same order as defined in get_action_space() (important), but all in one array
# For actions of size 1, we return 1 float in the array, for size 2, 2 floats in the array, etc.
# set_action is called just before get_action by the sync node, so we can read the newly set values
func get_action():
	return [
		# "movement" action values
		player.requested_movement.x,
		player.requested_movement.y,
		# "rotation" action value
		player.requested_rotation.x,
		# "jump" action value (-1 if not requested, 1 if requested)
		-1.0 + 2.0 * float(player.jump_requested),
		# "use_action" action value (-1 if not requested, 1 if requested)
		-1.0 + 2.0 * float(player.use_action_requested)
	]

# Here we set human control and AI control actions to the robot
func set_action(action = null) -> void:
	# If there's no action provided, it means that AI is not controlling the robot (human control),
	if not action:
		# Only rotate if the mouse has moved since the last set_action call
		if previous_mouse_movement == mouse_movement:
			mouse_movement = Vector2.ZERO

		player.requested_movement = Input.get_vector(
			"move_left", "move_right", "move_forward", "move_back"
		)
		player.requested_rotation = mouse_movement

		var use_action = Input.is_action_pressed("requested_action")
		var jump = Input.is_action_pressed("requested_jump")

		player.use_action_requested = use_action
		player.jump_requested = jump

		previous_mouse_movement = mouse_movement	
	else:
		# If there is action provided, we set the actions received from the AI agent 
		player.requested_movement = Vector2(action.movement[0], action.movement[1])
		# The agent only rotates the robot along the Y axis, no need to rotate the camera along X axis
		player.requested_rotation = Vector2(action.rotation[0], 0.0)
		player.jump_requested = bool(action.jump[0] > 0)
		player.use_action_requested = bool(action.use_action[0] > 0)

对于get_action()(仅在使用演示记录模式时需要),我们需要提供当代理遇到相同状态时希望它发送的动作。重要的是,这些值必须在正确的范围内(-1.0 到 1.0),这就是为什么布尔状态使用-1 + 2 * variable,并且顺序正确,如get_action_space()中定义的那样。

在演示记录模式下,调用set_action()时不需要提供动作,因为我们需要根据人类输入设置动作值。在训练/推理模式下,调用此方法时会带有一个action参数,其中包含由强化学习模型提供的所有动作的值,因此我们有一个if/else来处理这两种情况。

更多信息包含在代码注释中。

将 _input 方法替换为以下实现:

# Record mouse movement for human and demo_record modes
# We don't directly rotate in input to allow for frame skipping (action_repeat setting) which
# will also be applied to the AI agent in training/inference modes.
func _input(event):
	if not (heuristic == "human" or heuristic == "demo_record"):
		return

	if event is InputEventMouseMotion:
		var movement_scale: float = 0.005
		mouse_movement.y = clampf(event.relative.y * movement_scale, -1.0, 1.0)
		mouse_movement.x = clampf(event.relative.x * movement_scale, -1.0, 1.0)

此代码部分在人工控制和演示记录模式下记录鼠标移动。

最后,保存脚本。我们已为下一步做好准备。

打开演示录制场景,然后点击 AIController3D 节点

您可以在文件系统搜索中搜索“demo”,并在场景的过滤框中搜索“aicontroller”。
open robot scene

您无需进行任何更改,因为所有内容都已预设好,但让我们回顾一下您需要在自己的环境中设置的内容

场景包含修改后的Level > Robot > AIController3D节点设置

  • Control Mode设置为Record Expert Demos
  • Expert Demo Save Path已填写
  • Action Repeat设置为与training_sceneonnx_inference_scene中的Sync节点相同的值。这意味着代理设置的每个动作都会重复 3 个物理帧。AIController中的设置将相同的动作重复添加到人类输入(这会引入一些延迟)以匹配相同的行为。这是一个相当低的值,不会引入太多延迟。如果您更改此值,请务必在所有 3 个地方进行更改。
  • Remove Last Episode键允许我们设置一个键,该键可用于在录制过程中删除失败的剧集,而无需重新启动整个会话。例如,如果机器人掉入水中并且游戏重置,我们可以使用此键在录制下一个剧集时删除之前录制的剧集。它设置为R,但您可以通过单击它,然后单击Configure按钮将其更改为任何键。

在挑战性环境中使情节录制更容易的另一种方法是在录制期间减慢环境速度。这可以通过单击场景中的Sync节点并调整Speed Up属性(默认为 1)轻松完成。

让我们录制一些演示:

请注意,只有当我们至少录制了一个完整的剧集并通过单击“X”或按下 ALT+F4 关闭游戏窗口时,演示才会保存。使用 Godot 编辑器中的停止按钮将不会保存演示。最好先尝试录制一个剧集,然后检查您是否在文件系统或 Godot 项目文件夹中看到“expert_demos.json”。

确保您仍处于demo_record_scene中,按 F6,演示录制将开始。

控制

  • 鼠标控制摄像机(如果您需要调整鼠标灵敏度,打开robot场景,点击Robot节点并调整Rotation Speed,在录制演示、训练和推理时保持相同的值),
  • WASD控制玩家移动,
  • SPACE跳跃,
  • E激活杠杆并打开宝箱

您可以先练习几次以熟悉环境。如果您希望跳过录制演示,您也可以在完成的项目中找到预先录制的演示,并使用其中的expert_demos.json文件。

录制的演示应至少包含 22-24 个完整的成功剧集。在训练阶段也可以使用多个演示文件,因此您不必一次性录制所有演示(您可以使用之前提到的Expert Demo Save Path属性更改文件名)。

录制 23 个剧集花费了我大约 10 分钟(因为钥匙有 2 个交替的生成位置,22 或 24 个剧集将提供演示中钥匙位置的均匀分布,但这已经相当接近了)。当接近杠杆或宝箱时,我按住E键的时间稍长一些,以确保在靠近这些物体时,该动作在多个步骤中被记录下来。我还通过在下一集期间按下R键删除了几个我未成功完成的剧集。

这是演示录制过程的加速视频

导出游戏进行训练:

您可以使用Project > Export从 Godot 导出游戏。

< > 在 GitHub 上更新