这是一个我曾经断断续续地钻研了很长一段时间的我的项目。在此我的项目之前我从未尝试过批改游戏,也从未胜利训练过“真正的”强化学习代理(智能体)。所以这个我的项目挑战是:解决钓鱼这个问题的“状态空间”是什么。当应用一些简略的 RL 框架进行编码时,框架自身能够为咱们提供代理、环境和处分,咱们不用思考问题的建模局部。然而在游戏中,必须思考模型将读取每一帧的状态以及模型将提供给游戏的输出,而后相应地收集适合的处分,此外还必须确保模型在游戏中具备正确的视角(它只能看到玩家看到的货色),否则它可能只是学会利用谬误或者基本不收敛。
我的指标是编写一个能读取钓鱼小游戏状态并完满玩游戏的代理。指标的后果是应用官网 Stardew Valley 的 modding API 用 C# 编写一个主动钓鱼的 mod。该模块加载了一个用 Python 训练的序列化 DQN 模型。所以首先要从游戏中收集数据,而后用这些数据用 Pytorch 训练一个简略的 DQN。通过一些迭代后,能够应用 ONNX 生成一个序列化模型,而后从 C# 端加载模型,并在每一帧中接管钓鱼小游戏的状态作为输出,并(心愿)在每一帧上输入正确的动作。
钓鱼迷你游戏
这个代理是在 SMAPI 的帮忙下编写的,SMAPI 是 Stardew Valley 官网的 mod API。API 容许我在运行时拜访游戏内存,并提供我所须要的所有去发明一个与游戏状态进行交互并实时向游戏提供输出的代理。
在钓鱼小游戏中,咱们必须通过点击鼠标左键让“鱼钩”(一个绿色条) 与挪动的鱼对齐。鱼在这条竖线上无规律地挪动,鱼钩条与鱼对齐时,绿色条就会填满一些,如果鱼胜利逃离绿色条就会开始变空。当你填满绿色的条形图时,你会钓到鱼,当它绿条没有时鱼就跑了。
强化学习问题定义
所以这里只须要每帧从游戏内存中读取这些特定属性并将它们保留为在第 t 帧的状态。通过 API 咱们能够查看并从游戏内存中读取特定属性的代码,对于主动钓鱼,须要在钓鱼小游戏期间跟踪的 4 个变量。“钩子”核心的地位、鱼的地位、钩子的速度和绿色条的填充量(这是处分!)。游戏外部应用的名称有点奇怪,以下是读取它们的代码。
/ Update State
// hook position
bobberBarPos = Helper.Reflection.GetField<float>(bar, "bobberBarPos").GetValue();
// fish position
bobberPosition = Helper.Reflection.GetField<float>(bar, "bobberPosition").GetValue();
// hook speed
bobberBarSpeed = Helper.Reflection.GetField<float>(bar, "bobberBarSpeed").GetValue();
// amount of green bar filled
distanceFromCatching = Helper.Reflection.GetField<float>(bar, "distanceFromCatching").GetValue();
前三个定义了咱们的状态:
这是模型能够在每一帧上能够获取的状态,要将其设置为强化学习问题还须要应用处分来领导训练。处分将是绿色条的填充量,这是里的变量名称为 distanceFromCatching。这个值的范畴从 0 到 1,正好非常适合作为处分。
Replay Memory
Replay Memory 是 Q-learning 中应用的一种技术,用于将训练与特定的“工夫”去关联。所以须要将状态转换存储在缓存中并通过缓存中随机抽取批次来训练模型而不是间接应用最新数据进行训练。为了训练模型,咱们须要 4 个数据,别离是以后状态、下一个状态、采取的口头和处分:
Q-learning 中关键问题是要获取已经处于哪个状态和采取了哪些口头、达到哪个新的状态,以及执行这个口头中失去的处分。有了这些数据,咱们能够应用像价值迭代 (Value Iteration 一种动静布局算法 ) 这样的简略算法将处分从最终状态(获胜状态)开始剖析,逐步往回推直至推至所有状态。因而对于每个可能的状态,模型都会晓得最大化其将来回报的方向。然而我不会应用价值迭代来训练模型,因为真正的问题往往有太多的状态并且动静布局须要很长时间。
下面的价值迭代只是为了阐明在 C# 中保留每个条目标形式。这里应用缓存从最初一帧获取状态和动作,并将所有这些与以后帧的状态和处分一起存储。
replayMemory[updateCounter,0] = OldState[0];
replayMemory[updateCounter,1] = OldState[1];
replayMemory[updateCounter,2] = OldState[2];
replayMemory[updateCounter,3] = NewState[0];
replayMemory[updateCounter,4] = NewState[1];
replayMemory[updateCounter,5] = NewState[2];
replayMemory[updateCounter,6] = reward;
replayMemory[updateCounter,7] = actionBuffer? 1 : 0;
所有这些数据都变成了一个微小的 csv 文件,这样能够通过 Python 加载并用于训练 DQN 模型。
DQN 模型
应用神经网络预计 Q-table 的 Q-Learning 称为 Deep Q-Learning。这个办法在很多个 Pytorch 教程中都有很好的解释,我从外面复制了很多代码并为咱们的问题对其进行了一些批改。次要思维是应用两个神经网络。一个将预计 Q(s,a) 的值(Policy Net),另一个将预计将来 Q-values 的值(Target Net)。而后咱们对这两个网络的差别进行反向流传。
这是 Q-Learning 算法的根本方程。咱们将应用一个网络来预计以后状态 Q(s,a) 的正确值,另一个将预计下一个状态的最大可能值。两个网络都应用随机值进行初始化,并且每隔几次迭代将 Policy Net 权重复制到 Target Net。Policy Net 则通过反向流传更新权重,通过反向流传这种,Policy Net 最终将学会预计这两个值。
α 是学习率,𝛾 是用于抉择为 Q 的将来值给出的重要值的折扣因子(discount factor)。强化学习是比拟难易了解的所以最初会整顿一堆链接,它们会做更好的细节解释。
训练
训练过程是“自我驱动的😂”,首先要本人玩游戏收集状态和处分数据,而后训练一个初始化的成果很差的模型让它主动玩游戏,并为咱们收集新的数据。而后应用这些数据在 Python 端训练新模型,生成一个新的 ONNX 格局模型,该模型将每 1000 帧左右从新加载一次,而后应用新模型持续玩游戏并生成数据来训练新模型。因为 C̶# 必须编译 mod 并将其打包到与游戏可执行文件兼容的 Windows DLL 中,我没有找到一个能够生成正确的 .NET 机器学习框架二进制文件(Stardew Valley 是在 .NET 5 中编译的),所以我放弃了,这里间接用 Python 编写了这部分。
另外一个重要决定是该模型不须要在线训练。Q-Learning 就是要找到函数 Q(s,a) 的良好近似值,即预计在特定状态 s 下执行特定动作 a 的值的函数。所以模型的目标是数据彻底摸索这个状态空间,无论是你(人肉)还是模型玩游戏都没有关系,当然如果可能全副自动化拿看起来必定更加的高大上。
从 C # 中读取 ONNX 模型
C# 端惟一真正的 ML 代码是 ONNX 进行推理(预测),它定义了张量类型和会话的对象,能够发送张量输出并从序列化的 ONNX 模型获取张量输入。上面的代码十分简单明了。更新函数在每一帧都运行,并以以后状态作为输出查问训练模型的动作,最初几行只是用于获取模型输入的 argMax 一些代码,这是与产生的动作对应的索引。序列化模型的分量只有 120kb 左右,所以运行起来十分笨重。
public int Update(double[] currentState)
{Tensor<double> input = new DenseTensor<double>(new[] {3});
input[0] = currentState[0];
input[1] = currentState[1];
input[2] = currentState[2];
// Setup inputs and outputs
var inputs = new List<NamedOnnxValue>()
{
// the model has only one input, the state tuple
NamedOnnxValue.CreateFromTensor<double>("0", input)
};
using (var results = session.Run(inputs))
{Tensor<double> outputs = results.First().AsTensor<double>();
var maxValue = outputs.Max();
var maxIndex = outputs.ToList().IndexOf(maxValue);
return maxIndex;
}
}
应用 Harmony 进行输出
SMAPI 短少的 API 是可能在游戏中提供输出,因为 99.999% 的 mod 不须要这样的货色。为了进行输出我找到了一个名为 Harmony 的 C# 库在能够在运行时更改游戏的外部函数,这样我就能够让游戏认为它收到了鼠标输出。这就是下面让 mode 本人玩游戏的办法。非常感谢 Drynwynn,Mod FishingAutomaton 的作者,我应用了很多代码来设置我的 mod。
[HarmonyPatch(typeof(Game1), "isOneOfTheseKeysDown")]
class IsButtonDownHack
{
// ...
// some important stuff
// ...
// change function return value to true
// makes the game think a mouse left button click ocurred
__result = true;
return;
}
最终后果
目前,该模型能够捕捉所有“简略”和“中级”的鱼。还不能训练它捕获传说中的鱼。
这个 gif 是未训练实现的演示
上面是咱们训练的后果,成果还不错
资源和援用
非常感谢 Stardew Valley 的 mod 社区帮忙并让我更好地了解游戏:)
C# mod 和 Python 训练的所有代码都能够在这里找到!
https://github.com/ThiagoLira…
上面是一些其余的 DQN 的相干资源,供参考:
https://www.overfit.cn/post/8f82fe918b644ce58a8e525243db87a4
作者:Thiago Lira