官方文档:
https://github.com/Unity-Technologies/ml-agents/blob/release_19/docs/Custom-SideChannels.md
目录
1.1.1 ChannelId = new Guid("621f0a70-4f87-11ea-a6bf-784f4387d1f7");
1.1.2 protected override void OnMessageReceived(IncomingMessage msg)
1.1.3 public void SendDebugStatementToPython(string logString, string stackTrace, LogType type)
1.1.4 var stringToSend = type.ToString() + ": " + logString + "\n" + stackTrace
1.2 RegisterStringLogSideChannel类
1.2.1 public class RegisterStringLogSideChannel : MonoBehaviour
1.2.2 StringLogSideChannel stringChannel
2.2 ML-Agents Python 与 Unity Editor 通信
2.2.1 env = UnityEnvironment(side_channels=[string_log])
2.2.2 group_name = list(env.behavior_specs.keys())[0]
2.2.3 group_spec = env.behavior_specs[group_name]
2.2.4 decision_steps, terminal_steps = env.get_steps(group_name)
using UnityEngine;
using Unity.MLAgents;
using Unity.MLAgents.SideChannels;
using System.Text;
using System;
public class StringLogSideChannel : SideChannel
{
// 创建一个唯一的 GUID 用于标识该 SideChannel
public StringLogSideChannel()
{
ChannelId = new Guid("621f0a70-4f87-11ea-a6bf-784f4387d1f7");
}
// 在接收到消息时调用该方法,这里只是简单地输出接收到的字符串消息
protected override void OnMessageReceived(IncomingMessage msg)
{
var receivedString = msg.ReadString();
Debug.Log("From Python : " + receivedString);
}
// 将日志信息发送到 Python 端
public void SendDebugStatementToPython(string logString, string stackTrace, LogType type)
{
if (type == LogType.Error)
{
// 将日志信息与堆栈跟踪合并为一个字符串
var stringToSend = type.ToString() + ": " + logString + "\n" + stackTrace;
// 创建一个 OutgoingMessage 对象,并将字符串信息写入其中
using (var msgOut = new OutgoingMessage())
{
msgOut.WriteString(stringToSend);
// 将消息加入发送队列,等待发送
QueueMessageToSend(msgOut);
}
}
}
}
这段代码实现了一个自定义的 SideChannel,用于向 Python 端发送调试信息。具体来说,它继承了 Unity.MLAgents.SideChannels.SideChannel 类,并覆盖了其中的 OnMessageReceived() 方法和自定义的 SendDebugStatementToPython() 方法。它还定义了一个 GUID 作为通道 ID。
在 OnMessageReceived() 方法中,它接收一个传入的消息,并将其作为字符串打印到 Unity 控制台中。
在 SendDebugStatementToPython() 方法中,它接收三个参数:一个字符串 logString,一个字符串 stackTrace 和一个 LogType 类型。如果传入的类型是 Error,它将将日志和堆栈跟踪信息连接在一起,并将结果作为字符串发送到 Python 端。
这个 SideChannel 的 ChannelId 是 "621f0a70-4f87-11ea-a6bf-784f4387d1f7",可以用于在 Python 端实例化对应的环境,并接收 Unity 控制台的调试信息。
public StringLogSideChannel()
{
ChannelId = new Guid("621f0a70-4f87-11ea-a6bf-784f4387d1f7");
}
这段代码定义了一个名为 StringLogSideChannel 的类,并在类的构造函数中为它的 ChannelId 属性设置了一个 Guid 值。Guid 是一种全局唯一标识符,可用于标识对象、类型、属性等。在这里,ChannelId 的 Guid 值用于标识这个 Side Channel 的唯一标识符,从而使它能够与其他 Side Channel 区别开来。
protected override void OnMessageReceived(IncomingMessage msg)
{
var receivedString = msg.ReadString();
Debug.Log("From Python : " + receivedString);
}
这个方法重载了父类 SideChannel 中的 OnMessageReceived 方法,用于处理接收到的消息。在这个具体的实现中,方法会从接收到的消息中读取字符串,并将其使用 Debug.Log 输出到 Unity 的控制台中,前面加上 "From Python : " 的前缀。这个方法的具体功能实现可以根据具体需求进行修改。
public void SendDebugStatementToPython(string logString, string stackTrace, LogType type)
{
if (type == LogType.Error)
{
// 将日志信息与堆栈跟踪合并为一个字符串
var stringToSend = type.ToString() + ": " + logString + "\n" + stackTrace;
// 创建一个 OutgoingMessage 对象,并将字符串信息写入其中
using (var msgOut = new OutgoingMessage())
{
msgOut.WriteString(stringToSend);
// 将消息加入发送队列,等待发送
QueueMessageToSend(msgOut);
}
}
}
这段代码定义了一个名为 SendDebugStatementToPython 的公共方法。这个方法用于将 Unity 引擎中的调试日志发送到 Python 程序中进行记录。
方法的第一个参数 logString 是一个字符串,表示调试日志信息。第二个参数 stackTrace 是一个字符串,表示调用堆栈信息。第三个参数 type 是一个 LogType 类型的枚举值,用于表示调试日志的类型(如普通信息、警告或错误)。
如果日志类型是错误(LogType.Error),则将日志信息和堆栈跟踪合并为一个字符串,并将其写入一个 OutgoingMessage 对象中。然后将该消息添加到消息发送队列中,等待发送到 Python 程序。
问:字符串信息写入其中?怎么写入的?
在这段代码中,创建了一个 OutgoingMessage
对象 msgOut
,然后调用 msgOut.WriteString(stringToSend)
将字符串信息 stringToSend
写入到这个消息对象中。最后调用 QueueMessageToSend(msgOut)
将这个消息对象加入到发送队列中,等待发送。这个消息对象中的字符串信息会随着消息一起发送到 TensorBoard 中。
问:发送到 TensorBoard ?
在 Unity ML-Agents 中,可以通过将训练期间的统计信息发送到 TensorBoard,进行实时的可视化和分析。在 ML-Agents 中,将这些统计信息发送到 TensorBoard 需要借助一个叫做 SideChannel 的机制。SideChannel 是一种用于在训练期间与外部系统交换信息的方式,可以将消息传递到 TensorBoard 或其他外部系统。
var stringToSend = type.ToString() + ": " + logString + "\n" + stackTrace;
这行代码将LogType、日志信息和堆栈跟踪信息合并成一个字符串,用于在 SendDebugStatementToPython() 方法中将其写入 OutgoingMessage 对象中,最终发送到 TensorBoard 中。具体来说,使用了 "+" 操作符将这三部分信息连接在一起,LogType、日志信息和堆栈跟踪信息之间用 "\n" 换行符分隔。
例如,如果 logString 是 "Failed to load model",stackTrace 是:
UnityEngine.Debug:LogError(Object) MLAgents.Inference.TensorFlowSharpUtils:LoadModel(String, Boolean) (at Assets/ML-Agents/Plugins/UnityTensorFlowPlugin/Scripts/Utilities/TensorFlowSharpUtils.cs:51) MLAgents.Inference.TensorFlowSharpModel:Reload() (at Assets/ML-Agents/Plugins/UnityTensorFlowPlugin/Scripts/Models/TensorFlowSharpModel.cs:63) MLAgents.Inference.BarracudaModel:Reload() (at Assets/ML-Agents/Plugins/Unity.Barracuda/Models/BarracudaModel.cs:54) MLAgents.Inference.ModelRunner:LoadModel() (at Assets/ML-Agents/Plugins/Unity.Barracuda/InferenceEngine/ModelRunner.cs:60) MLAgents.Inference.DecisionRequester:Initialize() (at Assets/ML-Agents/DecisionRequester.cs:26) MLAgents.Academy:InitializeEnvironment() (at Assets/ML-Agents/Components/Academy.cs:139) UnityEngine.Object:Instantiate(Object, Vector3, Quaternion, Transform)
则最终生成的字符串为 "Error: Failed to load model\nUnityEngine.Debug:LogError(Object)\nMLAgents.Inference.TensorFlowSharpUtils:LoadModel(String, Boolean) (at Assets/ML-Agents/Plugins/UnityTensorFlowPlugin/Scripts/Utilities/TensorFlowSharpUtils.cs:51)\nMLAgents.Inference.TensorFlowSharpModel:Reload() (at Assets/ML-Agents/Plugins/UnityTensorFlowPlugin/Scripts/Models/TensorFlowSharpModel.cs:63)\nMLAgents.Inference.BarracudaModel:Reload() (at Assets/ML-Agents/Plugins/Unity.Barracuda/Models/BarracudaModel.cs:54)\nMLAgents.Inference.ModelRunner:LoadModel() (at Assets/ML-Agents/Plugins/Unity.Barracuda/InferenceEngine/ModelRunner.cs:60)\nMLAgents.Inference.DecisionRequester:Initialize() (at Assets/ML-Agents/DecisionRequester.cs:26)\nMLAgents.Academy:InitializeEnvironment() (at Assets/ML-Agents/Components/Academy.cs:139)\nUnityEngine.Object:Instantiate(Object, Vector3, Quaternion, Transform)"。
using UnityEngine;
using Unity.MLAgents;
public class RegisterStringLogSideChannel : MonoBehaviour
{
StringLogSideChannel stringChannel;
public void Awake()
{
// 创建 Side Channel 对象
stringChannel = new StringLogSideChannel();
// 当创建一个 Debug.Log 消息时,将其发送到 stringChannel
Application.logMessageReceived += stringChannel.SendDebugStatementToPython;
// 注册该 Channel
SideChannelManager.RegisterSideChannel(stringChannel);
}
public void OnDestroy()
{
// 取消注册 Debug.Log 的回调
Application.logMessageReceived -= stringChannel.SendDebugStatementToPython;
if (Academy.IsInitialized){
// 取消注册该 Channel
SideChannelManager.UnregisterSideChannel(stringChannel);
}
}
public void Update()
{
// 可选: 如果按下空格键,触发一个错误
if (Input.GetKeyDown(KeyCode.Space))
{
Debug.LogError("This is a fake error. Space bar was pressed in Unity.");
}
}
}
这段代码用于注册一个自定义的 Side Channel,用于将 Unity 中的 Debug.Log 消息发送到 Python 程序中,以便在 TensorBoard 中进行可视化和分析。具体实现包括:
①创建 StringLogSideChannel 对象作为 Side Channel,并通过注册它来让 ML-Agents 知道这个 Side Channel 的存在。
②注册一个 Application.logMessageReceived 委托,以便在 Unity 中有新的 Debug.Log 消息时触发 StringLogSideChannel 的 SendDebugStatementToPython 方法,将消息发送到 Python 程序中。
③在 Update 方法中,可以通过按下空格键来产生一个错误消息,以测试消息是否能够被成功地发送到 Python 程序中。
总之,这段代码可以将 Unity 中的 Debug.Log 消息发送到 Python 程序中,方便在 TensorBoard 中进行分析和可视化。
public class RegisterStringLogSideChannel : MonoBehaviour
{
}
这行代码定义了一个名为 RegisterStringLogSideChannel 的类,这个类继承自 Unity 引擎的 MonoBehaviour 类,说明它是一个脚本组件。通常情况下,脚本组件可以被添加到游戏场景中的 GameObject 上,从而让脚本在游戏运行时被执行。
StringLogSideChannel stringChannel;
这行代码声明了一个名为 stringChannel
的 StringLogSideChannel
类型的变量。它将用于管理和发送日志消息。
public void Awake()
{
// 创建 Side Channel 对象
stringChannel = new StringLogSideChannel();
// 当创建一个 Debug.Log 消息时,将其发送到 stringChannel
Application.logMessageReceived += stringChannel.SendDebugStatementToPython;
// 注册该 Channel
SideChannelManager.RegisterSideChannel(stringChannel);
}
这段代码在 Awake 方法中实例化 StringLogSideChannel 对象,并且将它注册到 Unity 的 SideChannelManager 中。还通过 Application.logMessageReceived 事件将 SendDebugStatementToPython 方法绑定到 Debug.Log 消息的事件上,以便将消息发送到 Python 程序。这样,当 Unity 中使用 Debug.Log 输出日志时,这些日志信息将通过 StringLogSideChannel 被发送到 Python 程序,并被记录到 TensorBoard 中。
stringChannel = new StringLogSideChannel();
这行代码创建了一个名为stringChannel
的StringLogSideChannel
对象,并将其实例化。stringChannel
变量将在后面的代码中用于调用该对象的方法和属性。
Application.logMessageReceived += stringChannel.SendDebugStatementToPython;
这行代码实现了将 Unity 的 Debug.Log
消息发送到 Python 端。具体来说,Application.logMessageReceived
是一个事件,在每次 Unity 中打印一条日志信息时都会触发。通过将 stringChannel.SendDebugStatementToPython
添加为这个事件的监听器,可以将每条日志信息都发送到 Python 端。这里的 stringChannel
是 StringLogSideChannel
类的实例对象,而 SendDebugStatementToPython
方法则是这个实例对象中定义的用于发送日志信息的方法。
SideChannelManager.RegisterSideChannel(stringChannel);
SideChannelManager.RegisterSideChannel(stringChannel)
是将自定义的Side Channel注册到ML-Agents中的全局管理器中。通过此方法,可以确保所有的agents和训练进程都可以访问到该channel,并可以向其发送和接收消息。在这个例子中,我们注册了一个StringLogSideChannel
的实例,以便在Unity中生成的Debug.Log消息可以发送到Python端,从而能够在TensorBoard中查看。
public void OnDestroy()
{
// 取消注册 Debug.Log 的回调
Application.logMessageReceived -= stringChannel.SendDebugStatementToPython;
if (Academy.IsInitialized){
// 取消注册该 Channel
SideChannelManager.UnregisterSideChannel(stringChannel);
}
}
这段代码是在Unity中的一个MonoBehaviour对象销毁时执行的。当这个对象被销毁时,该方法被调用,取消注册之前注册的回调和SideChannel。如果Unity的Academy对象已经被初始化,也就是说我们正在训练一个ML-Agent,那么就取消注册该Channel。这是为了确保在训练期间我们不会向已经关闭的通道发送信息。
public void Update()
{
// 可选: 如果按下空格键,触发一个错误
if (Input.GetKeyDown(KeyCode.Space))
{
Debug.LogError("This is a fake error. Space bar was pressed in Unity.");
}
}
这段代码中的Update()函数,会在每一帧渲染时被Unity自动调用。在该函数中,如果检测到用户按下了键盘上的空格键,则会触发一个错误。这个错误将通过Debug.LogError()函数输出到控制台中,并通过Side Channel发送到Python程序中,在TensorBoard中记录下来。这个功能只是一个可选项,用于演示如何通过Side Channel从Unity向Python发送消息。
from mlagents_envs.environment import UnityEnvironment
from mlagents_envs.side_channel.side_channel import (
SideChannel,
IncomingMessage,
OutgoingMessage,
)
import numpy as np
import uuid
# 创建 StringLogChannel 类
class StringLogChannel(SideChannel):
def __init__(self) -> None:
# 调用 SideChannel 的构造函数,并传入通道 ID
super().__init__(uuid.UUID("621f0a70-4f87-11ea-a6bf-784f4387d1f7"))
def on_message_received(self, msg: IncomingMessage) -> None:
"""
Note: We must implement this method of the SideChannel interface to
receive messages from Unity
"""
# 读取字符串并输出到控制台
print(msg.read_string())
def send_string(self, data: str) -> None:
# 创建 OutgoingMessage 对象,并将字符串写入其中
msg = OutgoingMessage()
msg.write_string(data)
# 调用 SideChannel 的 queue_message_to_send 方法将消息加入发送队列
super().queue_message_to_send(msg)
这段代码定义了一个名为 StringLogChannel
的类,该类继承了 SideChannel
类。在 StringLogChannel
中,我们实现了 on_message_received
方法,该方法在接收到消息时被调用。在这个方法中,我们简单地从消息中读取一个字符串并打印它。
此外,我们还定义了一个名为 send_string
的方法,该方法用于将一个字符串发送到 Unity 环境中。在这个方法中,我们将字符串添加到一个 OutgoingMessage
中,并调用 queue_message_to_send
方法将消息加入发送队列。
整个 StringLogChannel
类的目的是实现一个自定义的 SideChannel
,用于在 Unity 环境和 Python 脚本之间传递字符串。通过这个自定义 SideChannel
,Python 脚本可以接收来自 Unity 环境的字符串日志信息,并且可以将字符串信息发送回 Unity 环境。
__init__
方法 def __init__(self) -> None:
# 调用 SideChannel 的构造函数,并传入通道 ID
super().__init__(uuid.UUID("621f0a70-4f87-11ea-a6bf-784f4387d1f7"))
这一行代码定义了 StringLogChannel
类的构造函数,构造函数的主要作用是在创建类的实例时对实例的属性进行初始化。在这个构造函数中,super().__init__(uuid.UUID("621f0a70-4f87-11ea-a6bf-784f4387d1f7"))
调用了父类 SideChannel
的构造函数,将一个唯一标识符作为参数传递给父类,用于识别该通道。
这段代码是定义了一个名为 StringLogChannel
的 Python 类,继承了 Unity ML-Agents 提供的 SideChannel
类,用于与 Unity 引擎进行通信。
在 __init__
方法中,该类调用了其父类 SideChannel
的构造函数,并传入了一个唯一标识该通道的 UUID。
通道 ID 的作用是让 Unity 引擎能够识别出这个通道,进而与之建立通信连接。在实际应用中,我们需要为每个自定义的 Side Channel 分配一个独有的 UUID。
def on_message_received(self, msg: IncomingMessage) -> None:
"""
Note: We must implement this method of the SideChannel interface to
receive messages from Unity
"""
# 读取字符串并输出到控制台
print(msg.read_string())
这行代码定义了一个名为on_message_received
的方法,它是一个回调函数,用于在收到来自Unity环境的消息时被调用。该方法需要接收一个IncomingMessage
类型的参数msg
,表示接收到的消息。返回值为None
,表示该方法不返回任何内容。
这是一个类方法(instance method),用于在接收到 Unity 端的消息时调用。它接受一个 IncomingMessage
类型的参数 msg
,表示从 Unity 端接收到的消息。该方法主要的作用是读取消息内容并进行相应的处理。
这个函数是实现了 SideChannel 接口中的一个方法 on_message_received()
,用于接收来自 Unity 的消息。其中的注释指出,必须实现这个方法才能从 Unity 接收消息。这个方法会读取一个字符串并输出到控制台。
2.1.3
def send_string(self, data: str) -> None:
# 创建 OutgoingMessage 对象,并将字符串写入其中
msg = OutgoingMessage()
msg.write_string(data)
# 调用 SideChannel 的 queue_message_to_send 方法将消息加入发送队列
super().queue_message_to_send(msg)
这一行代码定义了一个名为 send_string
的实例方法,它接收一个字符串类型的参数 data
,并且没有返回值。这个方法的作用是将 data
字符串发送给 Unity 环境,以便在 Unity 环境中打印这个字符串。方法的实现包括以下步骤:
OutgoingMessage
对象,该对象用于存储待发送的消息。data
字符串写入 OutgoingMessage
对象。SideChannel
的 queue_message_to_send
方法,将消息加入到发送队列中,等待 Unity 环境接收。# 创建 StringLogChannel 类的实例
string_log = StringLogChannel()
# 通过 side_channels 参数将该 SideChannel 实例传入 Unity 环境,与 Unity Editor 建立连接
env = UnityEnvironment(side_channels=[string_log])
# 重置 Unity 环境
env.reset()
# 发送一个字符串消息到 StringLogChannel
string_log.send_string("The environment was reset")
# 获取第一个 group 的名称和 group_spec
group_name = list(env.behavior_specs.keys())[0]
group_spec = env.behavior_specs[group_name]
# 进行 1000 次模拟
for i in range(1000):
# 获取当前步骤的决策和终止步骤
decision_steps, terminal_steps = env.get_steps(group_name)
# 发送一个字符串消息到 StringLogChannel,其中包含当前步骤的一些信息
string_log.send_string(
f"Step {i} occurred with {len(decision_steps)} deciding agents and "
f"{len(terminal_steps)} terminal agents"
)
# 进行一步模拟
env.step()
# 关闭 Unity 环境
env.close()
这段代码使用了 ML-Agents Python 包来与 Unity Editor 之间的通信,从而控制 Unity 的环境进行学习。
首先,创建了一个 StringLogChannel
对象 string_log
,它是一个自定义的 SideChannel
,用于发送调试信息。接着,通过 UnityEnvironment
创建了一个与 Unity Editor 进行通信的环境 env
,并将 string_log
作为输入的 side_channels
参数传入。然后,调用 env.reset()
重置环境,并向 string_log
发送一条调试信息。group_name
为第一个 group 的名称,group_spec
是该 group 的 BehaviorSpec
。随后,在一个循环中,通过 env.get_steps(group_name)
获取该 group 中的决策步骤和终止步骤。然后,向 string_log
发送包含有关智能体数量的调试信息,调用 env.step()
推进环境的状态,直到达到循环次数上限,最后调用 env.close()
关闭环境。
env = UnityEnvironment(side_channels=[string_log])
这行代码创建了一个Unity环境对象(UnityEnvironment),并将一个SideChannel(string_log)作为参数传递给构造函数。SideChannel被用来在Unity环境和Python之间传递信息。通过将string_log传递给构造函数,我们告诉Unity环境我们希望使用该SideChannel进行通信。
group_name = list(env.behavior_specs.keys())[0]
这行代码的含义是获取Unity环境中第一个Behavior的名称(Behavior是Agent的执行逻辑,例如移动、射击等),将其赋值给变量 group_name
。在ML-Agents库中,一个Unity环境可以有多个Behavior,每个Behavior都由一个Brain来控制。在这里,我们只选择了第一个Behavior进行操作。
group_spec = env.behavior_specs[group_name]
env.behavior_specs
是一个字典,它包含了 Unity 环境中每个行为的规格(specifications)。每个行为都有一个唯一的名称,称为 group_name
。通过 env.behavior_specs.keys()
可以获取所有行为名称组成的列表,而 [0]
则表示取第一个行为的名称。
通过 env.behavior_specs[group_name]
,可以获取指定名称的行为的规格,包括它的动作空间(action space)和观测空间(observation space)等信息。具体内容取决于你在 Unity 中创建环境时定义的行为规格。
decision_steps, terminal_steps = env.get_steps(group_name)
decision_steps
和 terminal_steps
是来自 Unity 环境的两个 AgentStep
对象列表,用于保存每个代理在给定时间步骤中的状态信息。 decision_steps
包含所有仍在决策的代理的信息,而 terminal_steps
包含已经终止的代理的信息。env.get_steps(group_name)
方法用于从环境中获取给定组名称 group_name
的所有 AgentStep
对象,然后将它们分别分配给 decision_steps
和 terminal_steps
。
string_log.send_string(
f"Step {i} occurred with {len(decision_steps)} deciding agents and "
f"{len(terminal_steps)} terminal agents"
这一行代码的作用是发送一个字符串给Unity,该字符串包含了当前步数(i),决策步骤的Agent数量(decision_steps),以及终止步骤的Agent数量(terminal_steps)。其中,字符串使用了f-string的格式化方法,可以动态地将变量嵌入到字符串中。