2018 年 8 月
第 33 卷,第 8 期
测试运行 - 使用 C# 执行 Q-Learning 入门
强化学习 (RL) 是解决了问题的机器学习的分支,其中没有显式的定型数据已知正确输出值。问: 学习是一种算法,可用于解决某些类型的 RL 问题。在本文中,我解释 Q 学习的工作原理,并提供一个示例程序。
请参阅本文所述观点的最佳方法是看看在简单的迷宫图 1和中的关联的演示程序图 2。3 x 4 迷宫有 12 个单元格,从 0 到 11 编号。目标是从单元格中 8 在左下角中,转到单元格 11 在右下角中的最少的移动。您可以向左、 右、 向上或向下,但不是沿对角线方向。
图 1 简单迷宫难题
图 2 Q 学习演示程序
演示程序将设置在内存中迷宫的表示形式,然后使用 Q 学习算法查找 Q 矩阵。问: 质量,代表较大的值是更好。行索引是"发件人"单元格和列索引是"收件人"单元格。如果起始单元格是 8,然后扫描该行显示的最大的 Q 值是在向单元格 9 0.44。然后从单元格中 9,行中的最大值将为 1.08 到单元格中 5。继续此过程直至程序到达单元格 11 上的目标状态。
它很可能不需要编写代码解决了迷宫,但这是因为问题是容易掌握 Hello World Q 学习。我将解释如何 Q 学习可以通用化到更高版本在这篇文章中更真实问题。
本文假定您具有中级或更高的编程技能,但并不假定您完全了解 Q 学习。演示程序使用 C# 进行编码,但您应能够将代码重构为其他语言,如 Visual Basic 或 Python 太多麻烦。演示程序的代码显示无法在本文中,还可随附的文件下载。
代码展示
对我来说,至少 Q 学习是有些奇怪,因为我认为通过检查特定的演示代码而不是通过启动与一般原则,最好理解概念。图 3 显示了演示程序的整体结构(为节省空间进行了一些较小的修改)。
图 3 Q 学习演示程序结构
using System;
using System.Collections.Generic;
namespace QLearning
{
class QLearningProgram
{
static Random rnd = new Random(1);
static void Main(string[] args)
{
Console.WriteLine("Begin Q-learning maze demo");
Console.WriteLine("Setting up maze and rewards");
int ns = 12;
int[][] FT = CreateMaze(ns);
double[][] R = CreateReward(ns);
double[][] Q = CreateQuality(ns);
Console.WriteLine("Analyzing maze using Q-learning");
int goal = 11;
double gamma = 0.5;
double learnRate = 0.5;
int maxEpochs = 1000;
Train(FT, R, Q, goal, gamma, learnRate, maxEpochs);
Console.WriteLine("Done. Q matrix: ");
Print(Q);
Console.WriteLine("Using Q to walk from cell 8 to 11");
Walk(8, 11, Q);
Console.WriteLine("End demo");
Console.ReadLine();
}
static void Print(double[][] Q) { . . }
static int[][] CreateMaze(int ns) { . . }
static double[][] CreateReward(int ns) { . . }
static double[][] CreateQuality(int ns) { . . }
static List<int> GetPossNextStates(int s,
int[][] FT) { . . }
static int GetRandNextState(int s, int[][] FT) { . . }
static void Train(int[][] FT, double[][] R, double[][] Q,
int goal, double gamma, double lrnRate,
int maxEpochs) { . . }
static void Walk(int start, int goal, double[][] Q) { . . }
static int ArgMax(double[] vector) { . . }
} // Program
} // ns
为了创建演示程序,我启动了 Visual Studio 并创建了一个新 C# 控制台应用程序项目名为 QLearning。我使用 Visual Studio 2017,但演示程序并不重要的.NET 依赖,因此任何版本的 Visual Studio 可以正常工作。在模板代码加载到之后我删除了所有的编辑器不需要的 using 语句,只留下对引用 System 命名空间。然后我添加到 Collections.Generic 命名空间的引用,因为该演示使用列表 < int > 集合。
该演示程序类作用域的 Random 对象由于 Q 学习涉及一个随机选择组件,如稍后您将看到。变量 ns 代表的状态,数等同于在迷宫中的单元格的数目。对象 FT (可行转换) 是一个数组的数组样式矩阵。矩阵 R reward 矩阵,矩阵 Q 质量矩阵。
问: 学习算法需要参数 gamma (也称为折扣身份) 和 learnRate。我将介绍这些更高版本。Q 学习是迭代的因此此演示程序设置了 maxEpochs 变量来控制该算法可用于查找 Q 矩阵的多长时间。
设置在迷宫和奖励
创建迷宫方法 CreateMaze,定义如下:
static int[][] CreateMaze(int ns) {
int[][] FT = new int[ns][];
for (int i = 0; i < ns; ++i) FT[i] = new int[ns];
FT[0][1] = FT[0][4] = FT[1][0] = FT[1][5] = FT[2][3] = 1;
FT[2][6] = FT[3][2] = FT[3][7] = FT[4][0] = FT[4][8] = 1;
FT[5][1] = FT[5][6] = FT[5][9] = FT[6][2] = FT[6][5] = 1;
FT[6][7] = FT[7][3] = FT[7][6] = FT[7][11] = FT[8][4] = 1;
FT[8][9] = FT[9][5] = FT[9][8] = FT[9][10] = FT[10][9] = 1;
FT[11][11] = 1; // Goal
return FT;
}
该方法返回一个矩阵,用于定义允许移动。例如,您可以从移动单元格 4 到单元格 8,但您不能移动从单元格中 4 到单元格 5 因为没有墙的方式。前面曾提到,C# 将初始化为 0,整数数组以便 CreateMaze 需要指定仅允许移动。请注意,您不能移动从单元格到其自身,但目标单元格 11 除外。
由定义 reward 矩阵:
static double[][] CreateReward(int ns) {
double[][] R = new double[ns][];
for (int i = 0; i < ns; ++i) R[i] = new double[ns];
R[0][1] = R[0][4] = R[1][0] = R[1][5] = R[2][3] = -0.1;
R[2][6] = R[3][2] = R[3][7] = R[4][0] = R[4][8] = -0.1;
R[5][1] = R[5][6] = R[5][9] = R[6][2] = R[6][5] = -0.1;
R[6][7] = R[7][3] = R[7][6] = R[7][11] = R[8][4] = -0.1;
R[8][9] = R[9][5] = R[9][8] = R[9][10] = R[10][9] = -0.1;
R[7][11] = 10.0; // Goal
return R;
}
在此示例中,将迁移到目标单元格 11 时出现的 10.0,回报,但其他移动提供-0.1 负奖励。这些值是任意的。一般情况下,在使用 RL,奖励结构是完全取决于问题。在这里,小的负 reward 将遇到每次移动一点,其效果与目标通过较长的路径优先使用较短的路径。请注意,无需设置的不能使用,因为它们将永远不会发生移动奖励。
问: 学习的目标是查找 Q 矩阵的值。最初,所有的 Q 值设置为介于 0.0 和创建 Q 矩阵如下所示:
static double[][] CreateQuality(int ns) {
double[][] Q = new double[ns][];
for (int i = 0; i < ns; ++i)
Q[i] = new double[ns];
return Q;
}
定义可能的下一步状态
稍后将看到,Q 学习算法将需要知道哪些状态系统可以过渡到,根据当前状态。在此示例中,系统的状态是在迷宫中的位置相同因此仅有 12 的状态。定义方法 GetPossNextStates 如下所示:
static List<int> GetPossNextStates(int s, int[][] FT) {
List<int> result = new List<int>();
for (int j = 0; j < FT.Length; ++j)
if (FT[s][j] == 1) result.Add(j);
return result;
}
例如,如果当前状态 s,5 GetPossNextStates 返回列表 < int > 集合保存 (1、 6、 9)。问: 学习算法有时将进入从当前状态随机的下一个状态。该功能由 GetRandNextState 方法定义:
static int GetRandNextState(int s, int[][] FT) {
List<int> possNextStates = GetPossNextStates(s, FT);
int ct = possNextStates.Count;
int idx = rnd.Next(0, ct);
return possNextStates[idx];
}
因此,如果 s 的当前状态为 5,则 GetRandNextState 返回 1 或 6 或 9 的均匀分布 (0.33)。
问: 学习算法
问: 学习的密钥更新公式根据数学 Bellman 公式和底部的图 1 所示。方法 Train 中实现的算法。在高级伪代码中,Q 学习算法是:
loop maxEpochs times
set currState = a random state
while currState != goalState
pick a random next-state but don't move yet
find largest Q for all next-next-states
update Q[currState][nextState] using Bellman
move to nextState
end-while
end-loop
该算法根本不是显而易见的并对我来说,它最清楚地了解检查代码。定义开始:
static void Train(int[][] FT, double[][] R, double[][] Q,
int goal, double gamma, double lrnRate, int maxEpochs)
{
for (int epoch = 0; epoch < maxEpochs; ++epoch) {
int currState = rnd.Next(0, R.Length);
...
必须通过反复试验来确定培训时期数。备用设计是循环访问 Q 矩阵中的值未更改,直到或趋于稳定到少量更改每个迭代。内部循环迭代,直到当前的状态将变为目标状态,在演示迷宫的情况下的单元格 11:
while (true) {
int nextState = GetRandNextState(currState, FT);
List<int> possNextNextStates = GetPossNextStates(nextState, FT);
double maxQ = double.MinValue;
for (int j = 0; j < possNextNextStates.Count; ++j) {
int nns = possNextNextStates[j]; // short alias
double q = Q[nextState][nns];
if (q > maxQ) maxQ = q;
}
...
假设您在迷宫。您看到可以转到三个不同的房间,A、 B、 c。选取 B,但尚无移动。请求一个朋友的进入聊天室 B,友元告诉您,从房间 B 可以转到聊天室 X、 Y、 Z 和的这些聊天室 Y 具有最佳的 Q 值。换而言之,Y 是最佳的下一步的下一步状态。
执行到 Q 矩阵更新:
...
Q[currState][nextState] =
((1 - lrnRate) * Q[currState][nextState]) +
(lrnRate * (R[currState][nextState] + (gamma * maxQ)));
currState = nextState;
if (currState == goal) break;
} // while
} // for
} // Train
更新公式分为两部分。第一部分中,((1-lrnRate) * Q[currState][nextState]),称为攻击组件并将添加的旧值的一小部分。第二部分 (lrnRate * (R [currState] [nextState] + (gamma * maxQ))),称为资源管理器组件。LrnRate 增加当前奖励和未来奖励的影响较大的值 (浏览) 但要牺牲过去奖励 (攻击)。Gamma,折扣因子的值会影响将来的奖励的重要性。
使用问答矩阵
质量后矩阵计算所得,它可以用于查找到的目标状态的最佳路径从任意起始状态。方法遍历实现了此功能:
static void Walk(int start, int goal, double[][] Q) {
int curr = start; int next;
Console.Write(curr + "->");
while (curr != goal) {
next = ArgMax(Q[curr]);
Console.Write(next + "->");
curr = next;
}
Console.WriteLine("done");
}
请注意,该方法假定从起始状态,目标状态是可访问。该方法使用 helper ArgMax 来查找最佳的下一个状态:
static int ArgMax(double[] vector) {
double maxVal = vector[0]; int idx = 0;
for (int i = 0; i < vector.Length; ++i) {
if (vector[i] > maxVal) {
maxVal = vector[i]; idx = i;
}
}
return idx;
}
例如,如果一个向量,具有值 (0.5,0.3,0.7,0.2) ArgMax 返回 2。此演示程序定义了 Print 方法,以显示 Q 矩阵。可以在随附的文件下载中获取优质打印版本。简化的版本为:
static void Print(double[][] Q) {
int ns = Q.Length;
Console.WriteLine("[0] [1] . . [11]");
for (int i = 0; i < ns; ++i) {
for (int j = 0; j < ns; ++j) {
Console.Write(Q[i][j].ToString("F2") + " ");
}
Console.WriteLine();
}
}
总结
此处介绍的问题与解答学习示例应为您充分了解所涉及的主要原则。本文中介绍的问题方案是一个具有离散状态,但问题与解答学习可以过使用连续状态。常规的挑战就是最大程度提高长期的回报,这对于迷宫示例等同于进入中移动的最小数量的目标状态。您可以安全地进行训练具有许多试用版,如培训如何来执行某项任务的工业机器人的系统时,问题与解答学习非常有用。但问题与解答学习不适用于方案,如定型无人驾驶汽车如何导航交通。问: 学习和 RL 提醒我有些神经网络在 20 世纪 80 年代中 — 有相对较少的实际应用程序,但有密集的研究工作。我的同事的许多人都认为,在某些点 RL 将分解为实用性意想不到的方式。
Dr.James McCaffrey 供职于华盛顿地区雷蒙德市沃什湾的 Microsoft Research。他参与过多个 Microsoft 产品的工作,包括 Internet Explorer 和必应。Scripto可通过 jamccaff@microsoft.com 与 McCaffrey 取得联系。
衷心感谢以下 Microsoft 技术专家对本文的审阅:Asli Celikyilmaz、 Chris Lee、 Ricky Loynd、 Amr Sharaf、 Ken Tran