TowardsDataScience-2023-博客中文翻译-一百零七-

67 阅读1小时+

TowardsDataScience 2023 博客中文翻译(一百零七)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

DETR(用于目标检测的变换器)

原文:towardsdatascience.com/detr-transformers-for-object-detection-a8b3327b737a

对论文“基于变换器的端到端检测”的深入剖析和清晰解释

François PorcherTowards Data Science François Porcher

·发表于Towards Data Science ·阅读时长 8 分钟·2023 年 10 月 7 日

--

Aditya Vyas拍摄,来自Unsplash

注:本文深入探讨了计算机视觉的复杂世界,特别是变换器和注意力机制。建议了解论文“Attention is All You Need.”中的关键概念。

历史快照

DETR,即DEtection TRansformer,开创了一种新的目标检测方式,由Nicolas Carion 及其团队于 2020 年在 Facebook AI Research 提出

尽管目前尚未达到 SOTA(最先进技术)水平,DETR 对目标检测任务的创新重新定义显著影响了随后的模型,例如 CO-DETR,它是LVIS目标检测实例分割的当前最先进技术。

与传统的多对一问题场景不同,其中每个真实值对应多个锚点候选,DETR 通过将目标检测视为集合预测问题,实现了预测与真实值之间的一对一对应,从而消除了对某些后处理技术的需求。

目标检测概述

目标检测是计算机视觉的一个领域,专注于识别和定位图像或视频帧中的对象。除了仅仅对对象进行分类外,它还提供一个边界框,指示对象在图像中的位置,从而使系统能够理解各种识别对象的空间上下文和位置。

Yolo5 视频分割,来源

物体检测本身非常有用,例如在自动驾驶中,但它也是实例分割的前置任务,在实例分割中我们尝试寻找物体的更精确轮廓,同时能够区分不同实例(与语义分割不同)。

揭开非极大值抑制的神秘面纱(并摆脱它)

非极大值抑制,作者提供的图像

非极大值抑制 (NMS) 长期以来一直是物体检测算法中的基石,在后处理过程中发挥了不可或缺的作用,以精细化预测输出。在传统的物体检测框架中,模型提出了大量围绕潜在物体区域的边界框,其中一些不可避免地显示出显著的重叠(如上图所示)。

NMS 通过保留具有最大预测对象性分数的边界框,同时抑制那些表现出高重叠度的相邻框来解决这一问题,重叠度通过交并比 (IoU) 量化。具体来说,给定预设的 IoU 阈值,NMS 迭代选择具有最高置信度分数的边界框,并取消那些 IoU 超过该阈值的边界框,确保每个对象只有一个高置信度的预测

尽管 DETR(DEtection TRansformer)无处不在,但它大胆地避开了传统的 NMS,通过将物体检测重新定义为集合预测问题来重新发明物体检测。

通过利用变换器,DETR 直接预测固定大小的边界框,从而消除了传统 NMS 的必要性,显著简化了物体检测管道,同时保持甚至提升了模型性能

深入探讨 DETR 架构

在高层次图像中,DETR 是

  • 图像编码器(实际上是一个双重图像编码器,因为首先是一个CNN 骨干网络,然后是一个变换器 编码器,以增加更多的表达能力)

  • 变换器解码器 从图像编码中生成边界框。

DETR 架构,图片来自 文章

让我们详细了解每个部分:

  1. 骨干网络:

我们从一个具有 3 个颜色通道的初始图像开始:

这张图像被输入到一个骨干网络中,该骨干网络是一个卷积神经网络

我们使用的典型值是 C = 2048H = W = H0 = W0 = 32

2. 变换器编码器:

变换器编码器在理论上并非强制要求,但它为骨干网络增加了更多表达能力,消融研究表明性能有所提升。

首先,1x1 卷积将高层激活图 f 的通道维度从 C 减少到较小的维度 d

卷积之后

但正如你所知,Transformers 将输入向量序列映射到输出向量序列,所以我们需要压缩空间维度:

在压缩空间维度后

现在我们准备将其输入到 Transformer 编码器中。

重要的是要注意Transformer 编码器仅使用自注意力机制。

就是这样图像编码部分

解码器的更多细节,图像来自文章

3. Transformer 解码器:

这一部分是最难理解的部分,耐心点,如果你理解了这部分,你就理解了文章的大部分内容。

解码器使用自注意力和交叉注意力机制的组合。它接收N个对象查询,每个查询将被转换为一个输出框和类别。

框预测是什么样的?

它实际上由两个组件组成。

  • 一个边界框具有一些坐标(x1,y1,x2,y2)来识别边界框。

  • 一类(例如海鸥,但也可以为空)

重要的是要注意N是固定的。这意味着 DETR 始终预测N个边界框。但其中一些可能为空。我们只需确保N足够大,以覆盖图像中的足够对象。

然后交叉注意力机制可以关注编码部分(骨干网络 + Transformer 编码器)生成的图像特征。

如果你对机制不确定,这个方案应该能澄清问题:

详细的注意力机制,图像来自文章

在原始 Transformer 架构中,我们生成一个 token,然后使用自注意力和交叉注意力的组合来生成下一个 token 并重复。但在这里,我们不需要这种递归的公式,我们可以一次性生成所有输出,从而利用并行性。

主要创新:二分匹配损失

如前所述,DETR 产生N个输出(框 + 类)。但每个输出只对应一个真实框。如果你记得清楚,这就是关键点,我们不想应用任何后处理来过滤重叠的框。

二分匹配,由作者提供的图像

我们基本上想将每个预测与最近的真实框关联。因此,我们实际上是在寻找预测集与真实框集之间的一个双射,来最小化总损失。

那么我们如何定义这个损失呢?

1. 匹配损失(成对损失)

我们首先需要定义一个匹配损失,它对应于一个预测框和一个真实框之间的损失:

这个损失需要考虑两个组件:

  • 分类损失(预测框内的类别是否与真实类别相同)

  • 边界框损失(边界框是否接近真实框)

匹配损失

更准确地说,对于边界框组件,有两个子组件:

  • 交并比损失(IOU)

  • L1 损失(坐标之间的绝对差异)

2. 双射的总损失

计算总损失时,我们只是对N个实例求和:

所以基本上我们的问题是找到最小化总损失的双射:

问题的重新表述

性能洞察

DETR 与 Faster RCNN 的性能对比

  • DETR: 这指的是原始模型,使用了一个用于目标检测的 transformer 和ResNet-50 作为骨干网络。

  • DETR-R101: 这是 DETR 的一个变体,采用了ResNet-101 骨干网络而不是 ResNet-50。这里,“R101”指的是“ResNet-101”。

  • DETR-DC5: 这个版本的 DETR 使用了修改过的ResNet-50 骨干网络中的扩张 C5 阶段,由于特征分辨率的提高,提高了模型在小物体上的表现。

  • DETR-DC5-R101: 这个变体结合了这两种修改。它使用 ResNet-101 骨干网络,并包括扩张 C5 阶段,受益于更深的网络和更高的特征分辨率。

DETR 在大型物体上显著超越了基线,这很可能是由于 transformer 允许的非局部计算。然而,值得注意的是,DETR 在小物体上的表现较差。

为什么在这种情况下 Attention 机制如此强大?

对重叠实例的 Attention,图像来自文章

非常有趣的是,我们可以观察到在重叠实例的情况下,Attention 机制能够正确地分离出各个实例,如上图所示。

极端点的 Attention 机制

同样有趣的是,注意力机制集中在物体的极端点上以生成边界框,这正是我们所期望的。

总结

DETR 不仅仅是一个模型;它是一个范式转变,将目标检测从一个一对多的问题转变为一个集合预测问题,充分利用了 Transformer 架构的进展。

自其诞生以来,已经出现了许多改进,例如 DETR++和 CO-DETR,现在在LVIS数据集上引领了实例分割和目标检测的最前沿。

感谢阅读!在你离开之前:

[## GitHub — FrancoisPorcher/awesome-ai-tutorials: 最好的 AI 教程合集让你成为…]

最好的 AI 教程合集,让你成为数据科学的专家!— GitHub …

github.com

你应该将我的文章直接发送到你的邮箱。 在这里订阅。

如果你想访问 Medium 上的优质文章,你只需每月支付 $5 会员费。如果你通过 我的链接注册,你将用你的一部分费用支持我,而无需额外支付。

如果你觉得这篇文章有见解且有帮助,请考虑关注我并点赞,以便获得更多深入的内容!你的支持帮助我继续制作对我们集体理解有帮助的内容。

参考文献

  • “End-to-End Object Detection with Transformers” 由 Nicolas Carion、Francisco Massa、Gabriel Synnaeve、Nicolas Usunier、Alexander Kirillov 和 Sergey Zagoruyko 编写。你可以在 arXiv 上阅读全文。

  • Zong, Z., Song, G., & Liu, Y.(出版年份)。DET 网上协作混合分配训练。 arxiv.org/pdf/2211.12860.pdf

  • COCO 数据集

在 Power BI 中开发和测试 RLS 规则

原文:towardsdatascience.com/develop-and-test-rls-rules-in-power-bi-9dc705945feb

通常,并非所有用户都应有权限访问报告中的所有数据。在这里,我将解释如何在 Power BI 中开发 RLS 规则以配置访问权限,并如何测试它们。

Salvatore CagliariTowards Data Science Salvatore Cagliari

·发表于 Towards Data Science ·阅读时长 11 分钟·2023 年 6 月 19 日

--

图片来源:FLY:DUnsplash

介绍

许多客户希望根据特定规则限制其报告中的数据访问。

数据访问被称为行级安全(简称 RLS)。

你可以在 Medium 上找到许多关于 Power BI 中 RLS 的文章。

我在下面的参考部分添加了其中的两个。

尽管所有文章都很好地解释了基础知识,但我总是缺少关于如何开发更复杂规则以及如何轻松测试它们的解释。

在本文中,我将一步步解释 RLS 的基础知识并逐步增加复杂性。

此外,我还将向你展示如何使用 DAX Studio 构建查询来测试 RLS 规则,然后再将它们添加到数据模型中。

所以,我们开始吧。

场景

我使用的场景是,用户根据公司内的门店或门店的地理位置访问零售销售数据,包括两者的组合。

在 Contoso 数据模型中,我使用以下表:

图 1 — 涉及的表(图由作者提供)

我创建了以下报告来测试我的结果:

图 2 — 起始报告(图由作者提供)

创建简单规则

要创建 RLS 规则,你需要打开安全角色编辑器:

图 3 — 打开安全角色编辑器(图由作者提供)

接下来,你可以创建一个新角色并设置该角色的名称:

图 4 — 创建一个角色并重命名它(图示由作者提供)

在我的情况下,我将名称设置为“StorePermissions”。

现在,我可以开始添加一个表达式来控制对 Store 表的访问:

图 5 — 将 DAX 表达式添加到新角色(图示由作者提供)

我们已经有了一个新的、更简单的 RLS 规则编辑器几个月了。

在我的情况下,我想添加一个 DAX 表达式。所以,我点击“切换到 DAX 编辑器”按钮。

起初,我添加了最简单的表达式:TRUE()

图 6 — 最简单的 RLS 规则(图示由作者提供)

要理解 RLS 规则,你必须知道访问是由 RLS 规则编辑器中表达式的输出控制的。

如果表达式的输出不是空的或 FALSE(),用户将获得访问权限。

原则上,RLS 规则编辑器中的任何表达式都会作为筛选器添加到任何查询中。

在我更详细地解释之前,让我们先看看这个第一个表达式的效果。

为了测试规则,我保存表达式并关闭编辑器。

现在我可以使用新规则查看报告:

图 7 — 测试 RLS 规则(图示由作者提供)

在报告页面顶部,你会看到一个黄色横幅,显示你正在使用 StorePermission 规则查看报告。

由于 StorePermission 规则不限制访问,你不会看到任何区别。

让我们尝试一些不同的东西。

现在我将 RLS 规则中的表达式更改为 FALSE()

当我测试规则时,我不会看到任何数据:

图 8 — 使用 FALSE() 测试规则(图示由作者提供)

这证明如果表达式不返回 FALSE(),数据是可以访问的。

测试查询

为了详细了解这种效果,让我展示一个 DAX 查询,以在没有任何限制的情况下获取结果:

EVALUATE
  SUMMARIZECOLUMNS(
          Store[Store]
          ,"Retail_Sales", 'All Measures'[Retail Sales]
          )
ORDER BY Store[Store]

当我添加一个带有 TRUE() 的 RLS 规则,如上所示,查询变成类似于以下的查询:

EVALUATE
  FILTER(
      SUMMARIZECOLUMNS(
            Store[Store]
            ,"Retail_Sales", 'All Measures'[Retail Sales]
            )
      ,TRUE()
      )
ORDER BY Store[Store]

我将查询封装在一个 FILTER() 函数中,并添加了 TRUE() 作为筛选表达式。

在接下来的示例中,我将使用 CALCULATETABLE(),因为编写代码更高效灵活。

稍后会详细介绍这一点。

使其更复杂

接下来,我想限制对所有包含“Contoso T”字符串的门店的访问。

为此,我将规则编辑器中的表达式更改为以下内容:

CONTAINSSTRING('Store'[Store], "Contoso T")

测试规则时,我得到了以下结果:

图 9 — 限制访问“Contoso T”门店的结果(图示由作者提供)

使用 DAX 查询测试规则

测试这种规则的结果将是很好的。

在这种情况下,我使用以下 DAX Studio 查询来检查结果:

EVALUATE
  CALCULATETABLE(
    SUMMARIZECOLUMNS(
          Store[Store]
          ,"Retail_Sales", 'All Measures'[Retail Sales]
          )
    CONTAINSSTRING('Store'[Store], "Contoso T") = TRUE()
    )
ORDER BY Store[Store]

内部部分,使用 SUMMARIZECOLUMNS(),生成输出表。

在这种情况下,我只对门店列表感兴趣。

然后,我用 CALCULATETABLE() 将 SUMMARIZECOLUMNS() 包装起来,以向查询添加过滤器。

在这种情况下,我添加了来自 RLS 规则的表达式,包括一个“= TRUE()”检查。

结果如下:

图 10 — 检查查询的结果(作者提供的图)

那么在后台发生了什么呢?

让我们看看存储引擎查询:

图 11 — 检查查询的结果(作者提供的图)

那么,当我将 RLS 规则应用到这个查询时会发生什么呢?

我可以通过 DAX Studio 轻松应用 RLS 规则:

图 12 — 激活 RLS 规则(作者提供的图)

存储引擎查询如下:

图 13 — 带有 RLS 规则的查询分析

第一个查询(第 2 行)检索所有商店的列表。

第二个查询在 WHERE 子句中包含 RLS 规则。

结果不是匹配的商店列表(根据过滤器),而是包含 RLS 规则的神秘行。

你可以看到存储引擎(SE)查询的结果仍然包含 309 行,如上所述,这些行的数量是所有商店 + 3 行。

我们有 3 行差异的提示在 SE 查询下方的文本中:估算大小:行数 = 309

实际返回的行数可能确实是 306。

但这个分析表明 RLS 规则是在存储引擎之后应用的,因为查询结果仅包含 21 行:所有以“Contoso T”开头的商店。

这很重要,因为计算最终结果的公式引擎(FE)在存储引擎之后是单线程的,只能使用一个 CPU 核心。

而 SE 是多线程的,可以使用多个 CPU 核心。

因此,我们必须避免在 RLS 规则中编写低效的代码。

组合规则

接下来,我想组合两个表达式:

  1. 仅包括以“Contoso T”开头的商店

  2. 仅包括欧洲的商店

为了实现这一点,我使用简单编辑器向地理表中添加第二个表达式:

图 14 — 向地理表添加表达式(作者提供的图)

当我切换到 DAX 编辑器时,我得到以下表达式:

图 15 — 来自简单编辑器的 DAX 表达式(作者提供的图)

注意使用了严格等于运算符。

更改为简单等于运算符可能是必要的。

测试规则时的结果是:

图 16 — 组合规则的结果(作者提供的图)

这个规则的 DAX 查询将如下所示:

图 17 — 转换为 DAX 查询及结果(作者提供的图)

现在,让我们给 RLS 规则增加另一层复杂性:

我想限制访问那些:

  • 商店的名称以“Contoso T”开头,并且位于欧洲

  • 商店的名称以“Contoso S”开头,并且位于北美

这次,我从 DAX 查询开始。这是开发和测试表达式的更简单方法。

我将第一个查询用过滤表达式括起来。

由于我需要过滤两个表(Store 和 Geography),我必须使用 FILTER()RELATED()

EVALUATE
  CALCULATETABLE(
    ADDCOLUMNS(
      SUMMARIZECOLUMNS(Store[Store], 'Geography'[Continent])
            ,"Retail_Sales", 'All Measures'[Retail Sales]
            )
    ,FILTER(Store
        ,OR(CONTAINSSTRING('Store'[Store], "Contoso T") && RELATED(Geography[Continent]) = "Europe"
          ,CONTAINSSTRING('Store'[Store], "Contoso S") && RELATED(Geography[Continent]) = "North America")
        )
    )
ORDER BY [Retail Sales] DESC, 'Geography'[Continent], Store[Store]

我需要 RELATED() 函数,因为我使用 FILTER() 遍历 Store 表,并且需要 Geography 表中的 Continent 列。

由于 Geography 表在关系的一侧,我可以使用 RELATED() 获取 Continent 列。

这是结果:

图 18 — 组合规则的查询(作者绘图)

接下来,我们必须将此过滤器转换为 RLS 规则。

对于 RLS 规则,我们可以移除 FILTER() 函数,因为 RLS 规则本身作为过滤器工作。

图 19 — 转换为一个 RLS 规则(作者绘图)

请注意,我从“Geography”表中移除了表达式。

当我在 Power BI 中测试此规则时,得到的结果与 DAX 查询的结果一致:

图 20 — 测试组合 RLS 规则(作者绘图)

为了测试 RLS 规则,例如,当你只想获取过滤后的商店列表时,你可以写一个简单的查询,仅使用 FILTER() 函数:

图 21 — 仅执行 FILTER()(作者绘图)

基于用户登录配置访问

到现在为止,我们查看了静态 RLS 规则。

但大多数情况下,我们需要基于用户登录的规则。

为了实现这一点,我们需要一个表来映射用户与用户需要访问的行。

例如,像这样的表:

图 22 — 分配地理位置的用户列表(作者绘图)

在将表添加到数据模型后,我们需要在新表和“Geography”表之间添加一个关系:

图 23 — 扩展的数据模型(作者绘图)

新的“Geography Access”表和“Geography”表之间的关系必须正确配置。

添加关系后,Power BI 将其配置为 1:n 关系,其中“Geography”表在一侧,过滤器从“Geography”表流向“Geography Access”。

但我们希望根据“Geography Access”上的 RLS 规则(过滤器)来过滤“Geography”表。

因此,我们必须将交叉过滤方向更改为双向:

图 24 — 关系设置(作者绘图)

此外,我们必须设置“在两个方向上应用安全筛选器”标志,因为 Power BI 在应用 RLS 规则时会忽略交叉筛选方向设置。

现在我们可以添加 RLS 规则:

图 25 — 配置 RLS 规则(作者提供的图)

记得在添加此规则之前,移除 Store 表上的任何筛选表达式。

测试 RLS 规则时,我得到了这个结果:

图 26 — 空结果(作者提供的图)

为了了解发生了什么,让我们回到 RLS 规则编辑器并将规则视图更改为 DAX:

图 27 — 错误的 RLS 规则(作者提供的图)

简单的 RLS 规则编辑器无法识别 DAX 函数,并将其作为文本添加到筛选器中。

我们必须将表达式更改为如下:

图 28 — 正确的 DAX 规则(作者提供的图)

现在结果如预期:

图 29 — 使用我的用户和正确的 RLS 表达式测试 RLS 规则(作者提供的图)

报告页面左上角的卡片包含一个带有 USERPRINCIPALNAME() 函数的度量,以确保在测试期间正确的用户处于活动状态。

我甚至可以使用另一个用户测试 RLS 规则:

图 30 — 使用另一个用户测试 RLS 规则(作者提供的图)

有趣的是,这个用户不需要实际存在。它只需要包含在“地理位置访问”列表中。

这是测试结果:

图 31 — 使用测试用户的测试结果(作者提供的图)

在顶部的黄色线中,你可以看到测试期间的活动用户。

结论

我向你展示了如何创建基础 RLS 规则以及如何测试它们。

然后我增加了更多的复杂性,并分析了 RLS 规则对底层存储引擎的影响。

我们已经看到公式引擎处理了部分 RLS 规则。因此,我们必须在 RLS 规则中编写高效的代码。

在将 RLS 规则实施到数据模型之前,了解如何测试它们非常重要。

通过理解规则如何应用于数据模型,可以更容易地理解错误结果。

最后,我向模型中添加了动态基于用户的 RLS 规则。

在 DAX 查询中测试这些规则更加困难,因为你必须知道每个用户可以访问哪些数据,以编写正确的测试查询来验证结果。

我希望我能给你一些关于简化使用 Power BI 中 RLS 功能的提示。

图片由 Andrew George 提供,来源于 Unsplash

参考文献

你可以在这篇文章中找到 Power BI 中的安全功能列表:

## Power BI 中的 4 + 2 安全功能

在我关于这个主题的第一篇文章发布一年后,这里是关于 Power BI 中新安全功能的更新

[towardsdatascience.com

你可以在 Power BI(现在是 Fabric)社区页面找到关于 Power BI 中行级安全的简单解释:Row-level security (RLS) with Power BI — Power BI | Microsoft Learn

我推荐这篇由 Nikola Ilic 撰写的文章,在其中你可以找到关于 RLS 的起点:

## Power BI 中行级和对象级安全的终极指南

“谁在报告中看到了什么?” 是 Power BI 中的关键安全问题之一。学习两种可能的实现方法…

[towardsdatascience.com

另一个关于 Power BI 中行级安全的良好入门文章由 Elias Nordlinder 撰写:

[## 如何在 Power BI 中实施行级安全(第 I 部分)

行级安全(Row-Level Security)是一种根据不同角色对数据进行不同筛选的方法。这可能是静态实现的…

elias-nordlinder.medium.com

访问我的故事列表以获取更多信息 关于 FILTER() 函数 以及如何 用 DAX Studio 分析 DAX 查询

我使用了 Contoso 示例数据集,像我之前的文章中一样。你可以从微软 这里 免费下载 ContosoRetailDW 数据集。

Contoso 数据可以根据 MIT 许可证自由使用,详细信息请见 这里

[## 订阅以便每次 Salvatore Cagliari 发布新文章时收到电子邮件。

每当Salvatore Cagliari发布内容时,你将收到一封电子邮件。通过注册,你将创建一个 Medium 账户,如果你还没有的话…

medium.com

🦜🔗 LangChain:开发由语言模型驱动的应用程序

原文:towardsdatascience.com/develop-applications-powered-by-language-models-with-langchain-d2f7a1d1ad1a

图片由 Choong Deng Xiang 提供,来源于 Unsplash

开始使用 LangChain 和 Python 利用 LLM

Marcello PolitiTowards Data Science Marcello Politi

·发布在 Towards Data Science ·阅读时间 12 分钟·2023 年 4 月 26 日

--

介绍

LangChain 是一个框架,使得利用大型语言模型(如 GPT-3)快速而轻松地开发应用程序成为可能

然而,这个框架引入了额外的可能性,例如,轻松使用外部数据源,如维基百科,以增强模型提供的能力。我相信你们可能都尝试过使用 Chat-GPT,并发现它无法回答某个日期之后发生的事件。在这种情况下,维基百科的搜索可以帮助 GPT 回答更多问题。

LangChain 结构

该框架分为六个模块,每个模块允许你管理与 LLM 交互的不同方面。让我们看看这些模块是什么。

  • 模型:允许你实例化和使用不同的模型。

  • 提示:提示是我们与模型互动以尝试获得输出的方式。现在知道如何编写有效的提示至关重要。这个框架模块允许我们更好地管理提示。例如,通过创建可以重用的模板。

  • 索引:最好的模型通常是那些与一些文本数据相结合的模型,以便为模型添加上下文或解释某些内容。这个模块帮助我们做到这一点。

  • :很多时候,单次调用 LLM API 是不够的。该模块允许整合其他工具。例如,一次调用可以是一个复合链,其目的是从维基百科获取信息,然后将这些信息作为输入提供给模型。这个模块允许将多个工具串联起来,以解决复杂的任务。

  • 记忆:该模块允许我们在模型调用之间创建持久状态。能够使用一个记住过去所说内容的模型,无疑会提高我们的应用程序的效果。

  • 代理:代理是一个 LLM,它做出决策、采取行动、对自己所做的事情做出观察,并以这种方式继续,直到完成任务。该模块提供了一组可以使用的代理。

现在让我们更详细地了解一下如何通过利用不同的模块来实现代码。

模型

模型 允许使用三种不同类型的语言模型,它们是:

  • 大型语言模型(LLMs): 这些是能够理解自然语言的基础机器学习模型。这些模型接受字符串作为输入,并生成字符串作为输出。

  • 聊天模型: 这些模型由 LLM 提供支持,但专门用于与用户聊天。你可以在这里阅读更多信息。

  • 文本嵌入模型: 这些模型用于将文本数据投影到几何空间中。这些模型将文本作为输入,并返回一个数字列表,即文本的嵌入。

Open AI API 密钥

让我们开始使用这个模块。首先,我们需要安装并导入库。要使用这个库,你需要一个来自 Open AI 网站的 API 密钥。

!pip install langchain >> null
!pip install openai >> null
from langchain.llms import OpenAI
#past you api key here

import os
os.environ['OPENAI_API_KEY'] = "yuor-openai-key"

现在我们准备实例化一个 LLM 模型。

llm = OpenAI(model_name="text-ada-001", n=2, best_of=2)
llm("tell me a story please.")

我的输出*:* 一个年轻的女人去了一个她从未听说过的狂欢派对。她是那里唯一一个在黑暗中带有光亮的人,也是唯一一个能够看到人们美好的一面的人。她喝酒、跳舞,做了所有可能的事情来让这一切发生。在几个小时的舞蹈和饮酒之后,她认识了派对上的一个人。他是一个有些吸引人的男人,留着胡须和山羊胡。她说:“嗨,我是那里唯一一个在黑暗中带有光亮的人。”他说:“嗨,我是那里唯一一个在黑暗中带有光亮的人。”

使用 generate() 方法,你还可以输入一个列表并接收多个答案,我们来看看怎么做。

llm_result = llm.generate(["Tell me a short story", "whath is your favourite colour?", "Is the earth flat?"])

llm.generate 输出

你还可以提取有关大型语言模型结果的一些额外信息。

llm_result.llm_output

我的输出: {‘token_usage’: {‘completion_tokens’: 527, ‘total_tokens’: 544, ‘prompt_tokens’: 17}, ‘model_name’: ‘text-ada-001’}

LLMs 无法理解过长的输入文本。特别是,包含太多标记的文本(如果你不知道标记是什么,可以考虑单词的音节)。在将字符串传递给模型之前,你可以使用简单的方法估计标记的数量。

为了做到这一点,你需要安装tiktoken库。

!pip install tiktoken >> None
import tiktoken
llm.get_num_tokens("How many old are you?")

#OUTPUT : 6

提示

提示是编程 NLP 模型的新方式。然而,创建一个好的提示并非易事。以不同的方式提问可能会导致不同的结果,这些结果可能更准确也可能不准确。提示也可以根据你所面对的使用案例而有所不同。让我们看看这个模块如何帮助我们创建一个好的提示。

提示模板

正如名称所示,提示模板允许我们创建可以重复使用的模板,以便向我们的模型提出问题。模板将包含变量,这些变量是用户会不时更改的唯一内容,以使提示适应其特定的使用案例。

现在让我们看看它们如何被使用。

from langchain import PromptTemplate

template = """
I want you to act as businessman.

You have few passions in your life which are:

- Money
- Data
- Basketball

I want to write a Medium Blog post about {product}.
What is a good for a title of such post?
"""

prompt = PromptTemplate(
    input_variables=["product"],
    template=template,
)

现在我们可以用我们想要的任何字符串替换提示中的变量‘product’。这样我们就可以根据自己的需要自定义提示。

prompt.format(product = "how to make a bunch of money")

我的输出: 我希望你充当商人。你生活中有几个热情所在:- 钱- 数据- 篮球。我想写一篇关于如何赚一大笔钱的 Medium 博客文章。这样的文章标题应该是什么?

已经有一些模板被编写好,你可以直接导入它们。要了解这些模板是什么,你可以查看文档。我们现在来尝试导入一个。

from langchain.prompts import load_prompt

prompt = load_prompt("lc://prompts/conversation/prompt.json")
prompt

我的输出: PromptTemplate(input_variables=[‘history’, ‘input’], output_parser=None, partial_variables={}, template=’以下是人类和 AI 之间的友好对话。AI 很健谈,并从其上下文中提供了许多具体细节。如果 AI 不知道问题的答案,它会如实地说不知道。\n\n 当前对话:\n{history}\n 人类:{input}\nAI:’, template_format=’f-string’, validate_template=True)

在这个模板中,我们有多个可以填写的变量。其中之一是history。我们需要历史记录来告诉模板一些先前发生的事情,以便它有更多的上下文。如果我们想从这个模板中请求模型的输出,这非常简单。

llm(prompt.format(history="", input="What is 3 - 3?"))

少量示例

有时候我们想向机器学习模型询问特别棘手的事情。此时,获得更准确回答的一种方法是向模型展示正确答案的类似示例,然后提出我们的问题。LangChain 提供了一种保存专门用于保存这些示例的模板的方法。让我们看看如何做到这一点。

首先,让我们创建一些示例。如果我想创建单词的最高级:tall -> tallest。

from langchain import PromptTemplate, FewShotPromptTemplate

# First, create the list of few shot examples.
examples = [
    {"word": "cool", "superlative": "coolest"},
    {"word": "tall", "superlative": "tallest"},
]

现在我们创建模板,如之前所做的那样,包含两个变量,一个用于基本词,一个用于最高级词。

# Next, we specify the template to format the examples we have provided.
# We use the `PromptTemplate` class for this.
example_formatter_template = """
Word: {word}
Superlative: {superlative}\n
"""
example_prompt = PromptTemplate(
    input_variables=["word", "superlative"],
    template=example_formatter_template,
)

现在我们将一切结合起来,使用 FewShotPromptTemplate 类,它接受示例、模板、前缀(通常是我们希望给模板的指令)和一个后缀(即模板输出的形式)作为输入。

# Finally, we create the `FewShotPromptTemplate` object.
few_shot_prompt = FewShotPromptTemplate(
    examples=examples,
    example_prompt=example_prompt,

    prefix="Give the superlative of every input",

    suffix="Word: {input}\nSuperlative:",

    input_variables=["input"],
    example_separator="\n\n",
)
print(few_shot_prompt.format(input="big"))

我的输出: 给出每个输入单词的最高级:cool 最高级:coolest 单词:tall 最高级:tallest 单词:big 最高级:

现在你可以输入模型并获得实际输出。

llm(few_shot_prompt.format(input="large"))

索引

这个模块允许我们与外部文档交互,我们想要将其提供给模型。这个模块基于 Retriever 的概念。实际上,我们最常做的就是去获取最能回答我们查询的文档。因此,这是一个信息检索系统。让我们看看 Retriever 接口的样子,以便更好地理解它。(对于那些还不知道的人,接口是一个不能实例化的类,如果你想了解更多,你可以阅读我关于设计模式的文章。)

from abc import ABC, abstractmethod
from typing import List
from langchain.schema import Document

class BaseRetriever(ABC):
    @abstractmethod
    def get_relevant_documents(self, query: str) -> List[Document]:
        """Get texts relevant for a query.

        Args:
            query: string to find relevant texts for

        Returns:
            List of relevant documents
        """

get_relevant_documents方法非常简单,你只需了解如何阅读英语即可理解它的作用。字符串可以根据你的喜好进行更改,所以如果你想修改或实现一个自定义的 Retriever,也没有什么复杂的。

现在我们来看一个实际的例子。我们想要创建一个关于特定文档的问答应用程序。也就是说,模型需要能够回答我关于特定文档的问题。

我们首先需要安装Chroma,它允许我们与Vectorstores一起工作,我们稍后会看到它的用途。

!pip install chromadb >> null

让我们导入一些我们需要的类。

from langchain.chains import RetrievalQA
from langchain.llms import OpenAI

现在让我们从网络上下载一个 txt 文档。

#download data
import requests

url = "https://raw.githubusercontent.com/hwchase17/langchain/master/docs/modules/state_of_the_union.txt"
response = requests.get(url)
data = response.text
with open("state_of_the_union.txt", "w") as text_file:
    text_file.write(data)

让我们使用 TextLoader 加载文档。

from langchain.document_loaders import TextLoader
loader = TextLoader('state_of_the_union.txt', encoding='utf8')

Retriever 总是依赖于所谓的Vectorstore检索器。因此,我们可以实例化一个Vectorstore retriever并将我们的文本加载器传递给它。

index = VectorstoreIndexCreator().from_loaders([loader])

现在我们终于有了索引,我们可以对数据提问了。

query = "What did Ohio Senator Sherrod Brown say?"
index.query(query)

我的输出: 俄亥俄州参议员谢罗德·布朗说,“是时候埋葬‘铁锈带’这个标签了。”

链允许我们创建更复杂的应用程序。仅仅使用 LLM 通常是不够的,我们想要做得更多。例如,我们首先创建一个模板,然后将编译好的模板作为输入提供给 LLM,这可以通过链简单实现。对我来说,可以将链想象成你在 scikit-learn 中使用的 Pipeline。

LLMChain 就是一个这样的链,它接受输入,将其格式化到模板中,然后将其作为输入传递给模板。

首先,让我们创建一个简单的模板。

from langchain.prompts import PromptTemplate
from langchain.llms import OpenAI

llm = OpenAI(temperature=0.9)
prompt = PromptTemplate(
    input_variables=["product"],
    template="What is a good name for a website that sells {product}?",
)

现在我们通过指定要使用的模板和模型来创建一个链。

from langchain.chains import LLMChain
chain = LLMChain(llm=llm, prompt=prompt)

#run the chain with the input needed for the prompt
print(chain.run("paints"))

你也可以创建一个自定义链,但我会写一篇更详细的文章来讲解。

内存

每次我们与模型互动时,它都会给我们一个答案,这个答案不会依赖于上下文,因为它不会记住过去的事件。这就像每次我们都是在与一个新的模型交谈一样。在应用程序中,我们通常希望模型具备某种记忆,以便它通过根据之前我们说的话不断改进来学习如何回复我们,特别是当我们在开发聊天机器人时。这就是这个模块的用途。

内存可以通过多种方式实现。例如,我们可以将之前的 N 条消息作为一个字符串或字符串序列提供给模型。现在让我们看看我们可以实现的最简单类型的内存,称为缓冲区。

我们可以使用一个 ChatMessageHistory 类,它允许我们轻松保存所有发送给模型的消息。

from langchain.memory import ChatMessageHistory
history = ChatMessageHistory()history.add_user_message("hello friend!")history.add_ai_message("how are you?")

现在我们可以轻松检索消息。

history.messages

现在我们理解了这个概念,我们可以使用我们刚刚使用的类的包装器,称为 ConversationBufferMemory,它允许我们实际使用消息历史记录。

from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory()
memory.chat_memory.add_user_message("hello friend!")
memory.chat_memory.add_ai_message("how are you?")
memory.load_memory_variables({})

最后,我们来看看如何在对话链中使用这个功能。

所以让我们开始与模型进行对话。

from langchain.llms import OpenAI
from langchain.chains import ConversationChain

llm = OpenAI(temperature=0)
conversation = ConversationChain(
    llm=llm, 
    verbose=True, 
    memory=ConversationBufferMemory()
)
conversation.predict(input="Hello friend!")

conversation.predict(input="I would like to discuss about the universe")

conversation.predict(input="Whats your favourite planet?")

你可以通过访问内存来检索旧消息。

conversation.memory

现在你可以保存你的消息,以便在你想从某个特定点开始对话时重新加载它们并提供给模型。

代理

我们看到的那种链条,遵循像流水线一样的预定步骤。但是我们通常不知道模型回答特定问题时需要采取哪些步骤,因为这也取决于用户不时给出的回答。

我们可以让模型使用各种工具来改进其回答。一个常见的例子是首先去维基百科上阅读一些信息,然后回答一个特定的问题。

from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.llms import OpenAI

我们将使用两个特定的工具,SERPAPI,它允许模型进行浏览器搜索并获取信息,以及 llm-math,以提高其数学技能。

要安装 SERPAPI,你必须在网站上注册并复制 API Token。

这是网站:serpapi.com/

完成后,我们安装库并将令牌设置为环境变量。

!pip install google-search-results >> null
os.environ['SERPAPI_API_KEY'] = "your token here"

现在我们已经准备好使用它所需的工具来实例化我们的代理了。

因此,我们需要 3 样东西:

  • LLM:我们想要使用的大型语言模型

  • 工具:我们希望用来改进基本 LLM 的工具

  • 代理:处理 LLM 与工具之间的互动

llm = OpenAI(model_name="text-ada-001")
tools = load_tools(["serpapi", "llm-math"], llm=llm)
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)

现在我们可以向模型提问,依靠它将使用各种可用的工具来回答我们。

agent.run("How old is Joe Biden? What is his current age raised to the 0.56 power?")

结论

在这篇文章中,我们介绍了 LangChain 及其各种模块。每个模块都对提高大型语言模型的能力有用,并且对于基于这些模型开发应用程序至关重要。请注意,因为我在本文中使用的模型不是最好的,所以回答可能不是最优的,而且你得到的回答可能与我的差异很大。不过,目的是为了熟悉这个库。我很期待看到未来基于新大型语言模型开发的所有应用程序。关注我,阅读我即将发布的关于这些话题的深入文章! 😉

结束

Marcello Politi

LinkedinTwitterWebsite

开发你的第一个 AI 代理:深度 Q 学习

原文:towardsdatascience.com/develop-your-first-ai-agent-deep-q-learning-375876ee2472?source=collection_archive---------0-----------------------#2023-12-15

深入人工智能世界——从零开始构建深度强化学习环境。

赫斯顿·沃恩Towards Data Science 赫斯顿·沃恩

·

关注 发布于 Towards Data Science ·61 分钟阅读·2023 年 12 月 15 日

--

构建你自己的深度强化学习环境——图片作者

目录

如果你已经掌握了强化学习和深度 Q 学习的概念,可以直接跳到逐步教程。在那里,你将获得所有构建深度强化学习环境所需的资源和代码,包括环境、代理和训练协议。

简介

为什么选择强化学习?

你将获得的内容

什么是强化学习?

深度 Q 学习

逐步教程

1. 初步设置

2. 大致概况

3. 环境:初步基础

4. 实现代理:神经架构和策略

5. 影响环境:完成

6. 从经验中学习:经验重放

7. 定义代理的学习过程:调整神经网络

8. 执行训练循环:将一切整合

9. 总结

10. 附录:优化状态表示

为什么选择强化学习?

最近,像 ChatGPT、Bard、Midjourney、Stable Diffusion 等先进 AI 系统的广泛采用,引发了对人工智能、机器学习和神经网络领域的兴趣,但由于实施这些系统的技术性特质,这种兴趣往往未能得到满足。

对于那些希望开始人工智能之旅(或继续当前进程)的人来说,使用深度 Q 学习构建一个强化学习 gym 是一个很好的起点,因为它不需要高级知识来实现,可以轻松扩展以解决复杂问题,并且可以立即直观地理解人工智能如何变得“智能”。

你将获得的知识

假设你对 Python 有基本了解,在这次深度强化学习的介绍结束时,不使用高级强化学习框架,你将开发自己的 gym,以训练代理解决一个简单问题——从起点移动到目标!

虽然不太光鲜,但你将亲身体验到构建环境、定义奖励结构和基本神经架构、调整环境参数以观察不同学习行为,以及在决策中找到探索与利用之间平衡等主题。

然后你将拥有所有必要的工具来实现自己更复杂的环境和系统,并为深入探讨神经网络和强化学习中的高级优化策略做好充分准备。

图片由作者使用Gymnasium的 LunarLander-v2 环境制作

你还将获得有效利用预构建工具如OpenAI Gym的信心和理解,因为系统的每个组件都是从头开始实现并解密的。这使得你能够将这些强大的资源无缝集成到自己的 AI 项目中。

什么是强化学习?

强化学习(RL)是机器学习(ML)的一个子领域,专注于代理(做出决策的实体)如何在环境中采取行动以完成目标。

其实现包括:

  • 游戏

  • 自动驾驶车辆

  • 机器人技术

  • 金融(算法交易)

  • 自然语言处理

  • 以及更多内容..

强化学习的理念基于行为心理学的基本原则,其中动物或人类从其行为的结果中学习。如果某个行动导致了良好的结果,则代理会获得奖励;如果没有,则会受到惩罚或不给予奖励。

在继续之前,了解一些常用术语非常重要:

  • 环境:这是世界——代理操作的地方。它设定了代理必须遵循的规则、边界和奖励。

  • 代理:环境中的决策者。代理根据对所处状态的理解来采取行动。

  • 状态:代理在环境中的当前情况的详细快照,包括用于决策的相关度量或感官信息。

  • 行动:代理与环境交互的具体措施,如移动、收集物品或发起互动。

  • 奖励:环境根据代理的行为给予的反馈,可以是正面的、负面的或中性的,引导学习过程。

  • 状态/行动空间:代理可能遇到的所有可能状态和它在环境中可以采取的所有行动的组合。这定义了代理必须学习导航的决策和情况的范围。

本质上,在程序的每一步(回合),代理从环境中接收一个状态,选择一个行动,获得奖励或惩罚,环境被更新或回合结束。每一步后收到的信息会被保存为“经验”以供后续训练使用。

举个更具体的例子,假设你在下棋。棋盘是环境,你是代理。每一步(或回合)你查看棋盘的状态,并从行动空间中选择,即所有可能的移动,然后挑选未来奖励最高的行动。完成移动后,你评估这个行动是否良好,并学习以便下次表现得更好。

这可能一开始看起来信息量很大,但随着你自己逐步建立,这些术语会变得非常自然。

深度 Q 学习

Q 学习是一种用于机器学习的算法,其中“Q”代表“质量”,即代理可以采取的行动的价值。它通过创建一个 Q 值表来工作,该表包含行动及其相关的质量,用于估算在给定状态下采取某个行动的预期未来奖励。

代理会获得环境的状态,检查表格以查看是否以前遇到过,然后选择奖励值最高的行动。

Q 学习的顺序流程:从状态评估到奖励和 Q 表更新。—— 作者提供的图像

然而,Q-Learning 有一些缺点。每个状态和动作对必须被探索才能获得良好的结果。如果状态和动作空间(所有可能状态和动作的集合)过大,那么将它们全部存储在表中是不现实的。

这就是深度 Q-Learning(DQL)的作用,它是 Q-Learning 的一种进化形式。DQL 利用深度神经网络(NN)来近似 Q 值函数,而不是将其保存到表中。这使得处理具有高维状态空间的环境成为可能,比如来自相机的图像输入,这对于传统的 Q-Learning 来说是不切实际的。

深度 Q-Learning 是 Q-Learning 和深度神经网络的交集 — 作者提供的图像

神经网络可以在类似的状态和动作上进行泛化,即使它没有在具体情况上进行过训练,也能选择出合适的动作,从而消除对大型表格的需求。

神经网络如何做到这一点超出了本教程的范围。幸运的是,实施深度 Q-Learning 并不需要深刻的理解。

构建强化学习 Gym

1. 初始设置

在开始编写我们的 AI 代理之前,建议您对 Python 中的面向对象编程(OOP)原则有扎实的理解。

如果您尚未安装 Python,以下是 Bhargav Bachina 提供的简单教程,可以帮助您入门。我将使用的版本是 3.11.6。

[## 如何安装和开始使用 Python

初学者指南,适合任何想要开始学习 Python 的人

medium.com](medium.com/bb-tutorial…)

您唯一需要的依赖是 TensorFlow,这是 Google 提供的开源机器学习库,我们将用来构建和训练我们的神经网络。可以通过终端中的 pip 安装。我的版本是 2.14.0。

pip install tensorflow

或者如果这样做不行:

pip3 install tensorflow

您还需要 NumPy 包,但这应该已经包含在 TensorFlow 中。如果遇到问题,可以使用 pip install numpy

还建议您为每个类创建一个新文件(例如,environment.py)。这样可以避免被信息量淹没,并简化故障排除。

供您参考,这里是包含完整代码的 GitHub 仓库:github.com/HestonCV/rl-gym-from-scratch。请随意克隆、浏览,并将其作为参考!

2. 全局视角

为了真正理解这些概念,而不仅仅是复制代码,了解我们将要构建的不同部分及其如何结合起来至关重要。这样,每个部分都能在更大的图景中找到位置。

以下是一个包含 5000 个回合的训练循环的代码。一个回合本质上是代理与环境之间的一个完整的互动过程,从开始到结束。

这一点在目前不需要实现或完全理解。当我们构建每一部分时,如果你想了解特定类或方法的使用方式,请回到这里。

from environment import Environment
from agent import Agent
from experience_replay import ExperienceReplay
import time

if __name__ == '__main__':

    grid_size = 5

    environment = Environment(grid_size=grid_size, render_on=True)
    agent = Agent(grid_size=grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01)
    # agent.load(f'models/model_{grid_size}.h5')

    experience_replay = ExperienceReplay(capacity=10000, batch_size=32)

    # Number of episodes to run before training stops
    episodes = 5000
    # Max number of steps in each episode
    max_steps = 200

    for episode in range(episodes):

        # Get the initial state of the environment and set done to False
        state = environment.reset()

        # Loop until the episode finishes
        for step in range(max_steps):
            print('Episode:', episode)
            print('Step:', step)
            print('Epsilon:', agent.epsilon)

            # Get the action choice from the agents policy
            action = agent.get_action(state)

            # Take a step in the environment and save the experience
            reward, next_state, done = environment.step(action)
            experience_replay.add_experience(state, action, reward, next_state, done)

            # If the experience replay has enough memory to provide a sample, train the agent
            if experience_replay.can_provide_sample():
                experiences = experience_replay.sample_batch()
                agent.learn(experiences)

            # Set the state to the next_state
            state = next_state

            if done:
                break
            # time.sleep(0.5)

        agent.save(f'models/model_{grid_size}.h5')

每个内循环被视为一步。

通过代理-环境互动进行的训练过程——图片由作者提供

在每一步:

  • 状态从环境中获取。

  • 代理根据这个状态选择一个动作。

  • 环境受到操作,返回奖励,采取动作后的结果状态,以及回合是否结束。

  • 初始的stateactionrewardnext_statedone随后被保存到experience_replay中,作为一种长期记忆(经验)。

  • 然后,代理在这些经验的随机样本上进行训练。

在每个回合结束时,或者按你的需要,模型权重会被保存到模型文件夹中。这些权重可以在后续加载,以避免每次都从头训练。然后,环境在下一个回合开始时被重置。

这个基本结构几乎足以创建一个智能代理来解决各种问题!

正如引言中所述,我们对代理的问题相当简单:从网格中的初始位置到达指定的目标位置。

3. 环境:初步基础

开发这个系统的最明显起点是环境。

要拥有一个功能齐全的 RL 训练环境,环境需要做几件事:

  • 维护世界的当前状态。

  • 跟踪目标和代理。

  • 允许代理对世界进行修改。

  • 返回模型可以理解的状态形式。

  • 以我们能够理解的方式进行渲染,以观察代理。

这里将是代理度过其整个生命周期的地方。我们将环境定义为一个简单的方阵/二维数组,或在 Python 中的列表列表。

该环境将具有离散的状态空间,这意味着代理可能遇到的状态是不同且可计数的。每个状态都是环境中的一个单独、特定的条件或场景,不同于连续状态空间,其中状态可以以无限、流动的方式变化——想象一下国际象棋与控制汽车。

DQL 专门设计用于离散动作空间(有限数量的动作)——这将是我们关注的重点。其他方法用于连续动作空间。

在网格中,空白区域将由 0 表示,智能体将由 1 表示,目标将由 -1 表示。环境的大小可以是您希望的任何大小,但随着环境的增大,所有可能状态的集合(状态空间)会呈指数增长。这可能会显著延长训练时间。

渲染后的网格将类似于以下内容:

[0, 1, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, -1, 0]
[0, 0, 0, 0, 0]

构造 **Environment** 类和 **reset** 方法 我们将首先实现 Environment 类以及初始化环境的方法。目前,它将接受一个整数grid_size,但我们很快会扩展这一点。

import numpy as np

class Environment:
    def __init__(self, grid_size):
        self.grid_size = grid_size
        self.grid = []

    def reset(self):
        # Initialize the empty grid as a 2d list of 0s
        self.grid = np.zeros((self.grid_size, self.grid_size))

当创建一个新实例时,Environment 会保存 grid_size 并初始化一个空网格。

reset 方法使用 np.zeros((self.grid_size, self.grid_size)) 填充网格,该方法接受一个形状的元组,并输出一个由零组成的二维 NumPy 数组。

NumPy 数组是一种类似网格的数据结构,行为类似于 Python 中的列表,但它使我们能够高效地存储和操作数值数据。它允许矢量化操作,这意味着操作会自动应用于数组中的所有元素,而无需显式循环。

这使得对大型数据集的计算比标准的 Python 列表要快得多且更高效。不仅如此,它还是我们的智能体神经网络架构所期望的数据结构!

为什么叫做 reset?嗯,这个方法将被调用以重置环境,并最终返回网格的初始状态。

添加智能体和目标

接下来,我们将构造将智能体和目标添加到网格中的方法。

import random

def add_agent(self):
    # Choose a random location
    location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))

    # Agent is represented by a 1
    self.grid[location[0]][location[1]] = 1

    return location

def add_goal(self):
    # Choose a random location
    location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))

    # Get a random location until it is not occupied
    while self.grid[location[0]][location[1]] == 1:
        location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))

    # Goal is represented by a -1
    self.grid[location[0]][location[1]] = -1

    return location

智能体和目标的位置将由元组 (x, y) 表示。这两个方法都会在网格边界内选择随机值并返回位置。主要区别在于,add_goal 确保不会选择已被智能体占据的位置。

我们将智能体和目标放置在随机起始位置,以在每个回合中引入变化,这有助于智能体从不同的起点学习如何在环境中导航,而不是记住一条路径。

最后,我们将添加一个方法来在控制台中渲染世界,以便我们能够看到智能体与环境之间的互动。

def render(self):
        # Convert to a list of ints to improve formatting
        grid = self.grid.astype(int).tolist()

        for row in grid:
            print(row)
        print('') # To add some space between renders for each step

render 做三件事:将 self.grid 的元素转换为整数类型,将其转换为 Python 列表,并打印每一行。

我们不直接打印 NumPy 数组的每一行的唯一原因就是这样做的效果不够美观。

把一切结合起来..

import numpy as np
import random

class Environment:
    def __init__(self, grid_size):
        self.grid_size = grid_size
        self.grid = []

    def reset(self):
        # Initialize the empty grid as a 2d array of 0s
        self.grid = np.zeros((self.grid_size, self.grid_size))

    def add_agent(self):
        # Choose a random location
        location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))

        # Agent is represented by a 1
        self.grid[location[0]][location[1]] = 1

        return location

    def add_goal(self):
        # Choose a random location
        location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))

        # Get a random location until it is not occupied
        while self.grid[location[0]][location[1]] == 1:
            location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))

        # Goal is represented by a -1
        self.grid[location[0]][location[1]] = -1

        return location

    def render(self):
        # Convert to a list of ints to improve formatting
        grid = self.grid.astype(int).tolist()

        for row in grid:
            print(row)
        print('') # To add some space between renders for each step

# Test Environment
env = Environment(5)
env.reset()
agent_location = env.add_agent()
goal_location = env.add_goal()
env.render()

print(f'Agent Location: {agent_location}')
print(f'Goal Location: {goal_location}')
>>>
[0, 0, 0, 0, 0]
[0, 0, -1, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 1, 0]
[0, 0, 0, 0, 0]

Agent Location: (3, 3)
Goal Location: (1, 2)

在查看位置时,可能会感觉有些错误,但它们应该从左上角到右下角读取为(行,列)。另外,记住坐标是从零开始索引的。

好的,那么环境已经定义好了。接下来是什么呢?

扩展 **reset**

让我们编辑reset方法以处理代理和目标的放置。顺便说一下,也让我们自动化渲染。

class Environment:
    def __init__(self, grid_size, render_on=False):
        self.grid_size = grid_size
        self.grid = []
        # Make sure to add the new attributes
        self.render_on = render_on
        self.agent_location = None
        self.goal_location = None

    def reset(self):
        # Initialize the empty grid as a 2d array of 0s
        self.grid = np.zeros((self.grid_size, self.grid_size))

        # Add the agent and the goal to the grid
        self.agent_location = self.add_agent()
        self.goal_location = self.add_goal()

        if self.render_on:
            self.render()

现在,当调用reset时,代理和目标会被添加到网格中,它们的初始位置会被保存,如果render_on设置为 true,它将渲染网格。

...

# Test Environment
env = Environment(5, render_on=True)
env.reset()

# Now to access agent and goal location you can use Environment's attributes
print(f'Agent Location: {env.agent_location}')
print(f'Goal Location: {env.goal_location}')
>>>
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, -1]
[1, 0, 0, 0, 0]

Agent Location: (4, 0)
Goal Location: (3, 4)

定义环境的状态

我们现在将实现的最后一个方法是get_state。乍一看,状态可能仅仅是网格本身,但这种方法的问题在于这并不是神经网络所期望的。

神经网络通常需要一维输入,而不是当前网格所表示的二维形状。我们可以通过使用 NumPy 的内置flatten方法将网格展平来解决这个问题。这将把每一行放入同一个数组中。

def get_state(self):
    # Flatten the grid from 2d to 1d
    state = self.grid.flatten()
    return state

这将转换为:

[0, 0, 0, 0, 0]
[0, 0, 0, 1, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, -1]
[0, 0, 0, 0, 0]

转换为:

[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0]

正如你所看到的,哪一个单元格是哪个并不是一目了然,但这对深度神经网络来说不会是问题。

现在我们可以更新reset以在grid填充之后返回状态。其他内容将保持不变。

def reset(self):
    ...

    # Return the initial state of the grid
    return self.get_state()

到目前为止的完整代码..

import random

class Environment:
    def __init__(self, grid_size, render_on=False):
        self.grid_size = grid_size
        self.grid = []
        self.render_on = render_on
        self.agent_location = None
        self.goal_location = None

    def reset(self):
        # Initialize the empty grid as a 2d array of 0s
        self.grid = np.zeros((self.grid_size, self.grid_size))

        # Add the agent and the goal to the grid
        self.agent_location = self.add_agent()
        self.goal_location = self.add_goal()

        if self.render_on:
            self.render()

        # Return the initial state of the grid
        return self.get_state()

    def add_agent(self):
        # Choose a random location
        location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))

        # Agent is represented by a 1
        self.grid[location[0]][location[1]] = 1

        return location

    def add_goal(self):
        # Choose a random location
        location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))

        # Get a random location until it is not occupied
        while self.grid[location[0]][location[1]] == 1:
            location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))

        # Goal is represented by a -1
        self.grid[location[0]][location[1]] = -1

        return location

    def render(self):
        # Convert to a list of ints to improve formatting
        grid = self.grid.astype(int).tolist()

        for row in grid:
            print(row)
        print('') # To add some space between renders for each step

    def get_state(self):
        # Flatten the grid from 2d to 1d
        state = self.grid.flatten()
        return state

你现在已经成功实现了环境的基础!虽然,如果你没有注意到,我们还不能与其互动。代理被卡在了原地。

我们将在Agent类编写完成后回到这个问题,以提供更好的上下文。

4. 实现代理神经网络架构和策略

如前所述,代理是接收其环境状态的实体,在这种情况下是世界网格的平面版本,并根据动作空间做出采取何种动作的决定。

需要重申的是,动作空间是所有可能动作的集合,在这种情况下,代理可以向上、向下、向左和向右移动,因此动作空间的大小为 4。

状态空间是所有可能状态的集合。根据环境和代理的视角,这可能是一个巨大的数字。在我们的例子中,如果世界是一个 5x5 的网格,则有 600 个可能的状态;但如果世界是一个 25x25 的网格,则有 390,000 个状态,这会大大增加训练时间。

为了让代理有效地学习完成目标,它需要一些条件:

  • 神经网络用于在 DQL 的情况下近似 Q 值(对一个动作的未来奖励的估计总量)。

  • 策略或策略是代理选择动作时遵循的规则。

  • 环境中的奖励信号告诉代理它的表现如何。

  • 能够基于过去的经验进行训练。

可以实现两种不同的策略:

  • 贪婪策略:选择当前状态下 Q 值最高的动作。

  • Epsilon-Greedy 策略:选择当前状态下 Q 值最高的动作,但有一个小的概率,即 epsilon(通常表示为ϵ),选择一个随机动作。如果 epsilon = 0.02,那么这个动作有 2%的概率是随机的。

我们将实现Epsilon-Greedy 策略

为什么随机动作有助于代理学习?探索。

当代理开始时,它可能学习到一条次优路径,并继续选择这条路径而不改变或学习新路径。

从一个较大的 epsilon 值开始,并逐渐减少它,可以让代理在更新 Q 值之前彻底 探索 环境,然后再 利用 学到的策略。我们随着时间减少 epsilon 的量称为 epsilon 衰减,稍后会更清楚。

就像我们对环境做的那样,我们将用一个类来表示代理。

现在,在实现策略之前,我们需要一种获取 Q 值的方法。这时我们代理的大脑——或神经网络——就派上用场了。

神经网络

在这里不扯太远,神经网络只是一个巨大的函数。值进入后,传递到每一层并进行转换,最后输出一些不同的值。仅此而已。真正的魔力在于训练开始时。

这个想法是给神经网络大量标记的数据,比如,“这是一个输入,应该输出什么”。它在每一步训练中慢慢调整神经元之间的值,试图尽可能接近给定的输出,发现数据中的模式,并希望帮助我们预测网络从未见过的输入。

状态通过神经网络转化为 Q 值 — 作者图片

代理类和定义神经网络结构 目前我们将使用 TensorFlow 定义神经网络结构,并专注于数据的“前向传播”。

from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Sequential

class Agent:
    def __init__(self, grid_size):
        self.grid_size = grid_size
        self.model = self.build_model()

    def build_model(self):
        # Create a sequential model with 3 layers
        model = Sequential([
            # Input layer expects a flattened grid, hence the input shape is grid_size squared
            Dense(128, activation='relu', input_shape=(self.grid_size**2,)),
            Dense(64, activation='relu'),
            # Output layer with 4 units for the possible actions (up, down, left, right)
            Dense(4, activation='linear')
        ])

        model.compile(optimizer='adam', loss='mse')

        return model

再说一次,如果你对神经网络不太熟悉,不要被这一部分困扰。虽然我们在模型中使用了 ‘relu’ 和 ‘linear’ 等激活函数,但对激活函数的详细探讨超出了本文的范围。

你需要知道的只是模型将状态作为输入,值在模型的每一层中被转换,四个对应于每个动作的 Q 值被输出。

在构建代理的神经网络时,我们从一个输入层开始,该层处理网格的状态,以 grid_size² 大小的一维数组表示。这是因为我们已经将网格展平以简化输入。该层本身就是我们的输入,因此在架构中无需定义,因为它不接受任何输入。

接下来,我们有两个隐藏层。这些是我们看不到的值,但随着模型的学习,它们对于更接近 Q 值函数的近似非常重要:

  1. 第一个隐藏层有 128 个神经元,Dense(128, activation='relu'),并以展平的网格作为输入。

  2. 第二个隐藏层包含 64 个神经元,Dense(64, activation='relu'),进一步处理信息。

最后,输出层 Dense(4, activation='linear') 包含 4 个神经元,对应于四种可能的动作(上、下、左、右)。该层输出 Q 值——每个动作未来奖励的估计。

通常,你需要解决的问题越复杂,你需要的隐藏层和神经元就越多。对于我们的简单用例,两个隐藏层应该足够了。

神经元和层可以并且应该进行实验,以找到速度和结果之间的平衡——每一层都增加了网络捕捉和学习数据细微差别的能力。像状态空间一样,神经网络越大,训练就越慢。

贪婪策略 使用这个神经网络,我们现在可以得到一个 Q 值预测,虽然还不是很理想,但已经可以做出决策了。

import numpy as np   

def get_action(self, state):
    # Add an extra dimension to the state to create a batch with one instance
    state = np.expand_dims(state, axis=0)

    # Use the model to predict the Q-values (action values) for the given state
    q_values = self.model.predict(state, verbose=0)

    # Select and return the action with the highest Q-value
    action = np.argmax(q_values[0]) # Take the action from the first (and only) entry

    return action

TensorFlow 神经网络架构要求输入状态为批量数据。这在你有大量输入并希望获得完整批次的输出时非常有用,但当你只有一个输入需要预测时可能会有些混淆。

state = np.expand_dims(state, axis=0)

我们可以通过使用 NumPy 的 expand_dims 方法并指定 axis=0 来解决这个问题。这会简单地将其转换为一个单一输入的批量。例如,一个 5x5 网格的状态:

[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0]

变为:

[[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0]]

在训练模型时,你通常会使用 32 或更多大小的批量。它看起来像这样:

[[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
 ...
 [0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

现在我们已经以正确的格式准备好了模型的输入,我们可以预测每个动作的 Q 值并选择最高的一个。

...

# Use the model to predict the Q-values (action values) for the given state
q_values = self.model.predict(state, verbose=0)

# Select and return the action with the highest Q-value
action = np.argmax(q_values[0]) # Take the action from the first (and only) entry

...

我们只需将状态传递给模型,它就会输出一批预测。记住,因为我们提供给网络的是一个批量的单一数据,它将返回一个批量的单一数据。此外,verbose=0 确保在每次调用 predict 函数时控制台不会出现常规调试消息。

最后,我们使用 np.argmax 在批量中的第一个且唯一的条目上选择并返回具有最高值的动作的索引。

在我们的例子中,索引 0、1、2 和 3 将分别映射到上、下、左和右。

贪婪策略总是选择根据当前 Q 值具有最高奖励的动作,但这可能不会总是导致最佳的长期结果。

Epsilon-贪婪策略 我们已经实现了贪婪策略,但我们想要的是 Epsilon-贪婪策略。这将随机性引入代理的选择中,以便 探索 状态空间。

重申一下,epsilon 是选择随机动作的概率。我们还希望有一种方法随着代理的学习逐渐降低这一概率,以便 利用 所学策略。如前所述,这称为 epsilon 衰减。

epsilon 衰减值应设置为小于 1 的十进制数,用于在代理每一步之后逐渐减少 epsilon 值。

通常,epsilon 会从 1 开始,而 epsilon 衰减值将接近 1,比如 0.998。在训练过程中的每一步,你将 epsilon 乘以 epsilon 衰减值。

为了说明这一点,下面是 epsilon 在训练过程中的变化情况。

Initialize Values:
epsilon = 1
epsilon_decay = 0.998

-----------------

Step 1:
epsilon = 1

epsilon = 1 * 0.998 = 0.998

-----------------

Step 2:
epsilon = 0.998

epsilon = 0.998 * 0.998 = 0.996

-----------------

Step 3:
epsilon = 0.996

epsilon = 0.996 * 0.998 = 0.994

-----------------

Step 4:
epsilon = 0.994

epsilon = 0.994 * 0.998 = 0.992

-----------------

...

-----------------

Step 1000:
epsilon = 1 * (0.998)¹⁰⁰⁰ = 0.135

-----------------

...and so on

正如你所看到的,epsilon 随着每一步慢慢接近零。到第 1000 步时,随机动作被选择的概率为 13.5%。epsilon 衰减是一个需要根据状态空间进行调整的值。状态空间较大时,可能需要更多探索或更高的 epsilon 衰减。

epsilon 在步骤中的衰减 — 图片由作者提供

即使代理已经训练得很好,保持一个较小的 epsilon 值也是有益的。我们应该定义一个停止点,在该点 epsilon 不再降低,即 epsilon 结束。根据用例和任务的复杂性,这可以是 0.1、0.01,甚至 0.001。

在上图中,你会注意到 epsilon 在 0.1 时停止减少,这是预定义的 epsilon 结束值。

让我们更新我们的 Agent 类以包含 epsilon。

import numpy as np

class Agent:
    def __init__(self, grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01):
        self.grid_size = grid_size
        self.epsilon = epsilon
        self.epsilon_decay = epsilon_decay
        self.epsilon_end = epsilon_end
        ...

    ...

    def get_action(self, state):

        # rand() returns a random value between 0 and 1
        if np.random.rand() <= self.epsilon:
            # Exploration: random action
            action = np.random.randint(0, 4)
        else:
            # Add an extra dimension to the state to create a batch with one instance
            state = np.expand_dims(state, axis=0)

            # Use the model to predict the Q-values (action values) for the given state
            q_values = self.model.predict(state, verbose=0)

            # Select and return the action with the highest Q-value
            action = np.argmax(q_values[0]) # Take the action from the first (and only) entry

        # Decay the epsilon value to reduce the exploration over time
        if self.epsilon > self.epsilon_end:
            self.epsilon *= self.epsilon_decay

        return action

我们将epsilonepsilon_decayepsilon_end的默认值分别设为 1、0.998 和 0.01。

记住 epsilon 及其相关值是超参数,用于控制学习过程。它们可以并且应该被实验以达到最佳结果。

方法get_action已更新以包含 epsilon。如果np.random.rand生成的随机值小于或等于 epsilon,则选择一个随机动作。否则,过程与之前相同。

最后,如果epsilon没有达到epsilon_end,我们通过将其乘以epsilon_decay来更新它,如self.epsilon *= self.epsilon_decay

**代理** 到目前为止:

from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Sequential
import numpy as np

class Agent:
    def __init__(self, grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01):
        self.grid_size = grid_size
        self.epsilon = epsilon
        self.epsilon_decay = epsilon_decay
        self.epsilon_end = epsilon_end
        self.model = self.build_model()

    def build_model(self):
        # Create a sequential model with 3 layers
        model = Sequential([
            # Input layer expects a flattened grid, hence the input shape is grid_size squared
            Dense(128, activation='relu', input_shape=(self.grid_size**2,)),
            Dense(64, activation='relu'),
            # Output layer with 4 units for the possible actions (up, down, left, right)
            Dense(4, activation='linear')
        ])

        model.compile(optimizer='adam', loss='mse')

        return model

    def get_action(self, state):

        # rand() returns a random value between 0 and 1
        if np.random.rand() <= self.epsilon:
            # Exploration: random action
            action = np.random.randint(0, 4)
        else:
            # Add an extra dimension to the state to create a batch with one instance
            state = np.expand_dims(state, axis=0)

            # Use the model to predict the Q-values (action values) for the given state
            q_values = self.model.predict(state, verbose=0)

            # Select and return the action with the highest Q-value
            action = np.argmax(q_values[0]) # Take the action from the first (and only) entry

        # Decay the epsilon value to reduce the exploration over time
        if self.epsilon > self.epsilon_end:
            self.epsilon *= self.epsilon_decay

        return action

我们已经有效地实现了 Epsilon-Greedy 策略,我们几乎准备好让代理开始学习了!

5. 影响环境:完成

环境目前有重置网格、添加代理和目标、提供当前状态以及将网格打印到控制台的方法。

为了使环境完整,我们需要不仅允许代理影响环境,还需要以奖励的形式提供反馈。

定义奖励结构:制定一个好的奖励结构是强化学习的主要挑战。你的问题可能完全在模型的能力范围内,但如果奖励结构设置不正确,模型可能永远无法学习。

奖励的目标是鼓励特定的行为。在我们的例子中,我们希望引导代理到达由-1 定义的目标单元。

类似于网络中的层和神经元,以及 epsilon 及其相关值,定义奖励结构也有许多正确(和错误)的方法。

奖励结构的两种主要类型:

  • 稀疏:当奖励仅在少数状态中给予时。

  • 密集:当奖励在状态空间中很常见时。

对于稀疏奖励,代理几乎没有反馈来指导它。这就像是每一步都给一个固定的惩罚,如果代理到达目标则提供一个大奖励。

代理确实可以学习达到目标,但根据状态空间的大小,这可能需要更长的时间,并且可能会陷入次优策略。

这与稠密奖励结构相对,稠密奖励结构允许代理更快地训练并表现得更可预测。

稠密奖励结构要么

  • 有多个目标。

  • 在整个过程中提供提示。

代理有更多的机会学习期望的行为。

例如,假设你在训练一个代理使用身体行走,而你给予的唯一奖励是它达到一个目标。代理可能会通过缓慢移动或在地面上滚动来学习如何到达那里,或者甚至根本没有学习到。

相反,如果你奖励代理朝目标前进、保持站立、迈出一步并保持直立,你将获得更自然和有趣的步态,同时改善学习效果。

允许代理对环境产生影响 为了获得奖励,你必须允许代理与其环境互动。让我们重新审视一下Environment类,以定义这种互动。

...

def move_agent(self, action):
    # Map agent action to the correct movement
    moves = {
        0: (-1, 0), # Up
        1: (1, 0),  # Down
        2: (0, -1), # Left
        3: (0, 1)   # Right
    }

    previous_location = self.agent_location

    # Determine the new location after applying the action
    move = moves[action]
    new_location = (previous_location[0] + move[0], previous_location[1] + move[1])

    # Check for a valid move
    if self.is_valid_location(new_location):
        # Remove agent from old location
        self.grid[previous_location[0]][previous_location[1]] = 0

        # Add agent to new location
        self.grid[new_location[0]][new_location[1]] = 1

        # Update agent's location
        self.agent_location = new_location

def is_valid_location(self, location):
    # Check if the location is within the boundaries of the grid
    if (0 <= location[0] < self.grid_size) and (0 <= location[1] < self.grid_size):
        return True
    else:
        return False

上述代码首先定义了与每个动作值相关的坐标变化。如果选择动作 0,则坐标变化为(-1, 0)。

记住,在这种情况下,坐标被解释为(行,列)。如果行减少 1,则代理上移一个单元格;如果列减少 1,则代理左移一个单元格。

然后根据移动计算新位置。如果新位置有效,则更新agent_location。否则,agent_location保持不变。

此外,is_valid_location 只是检查新位置是否在网格边界内。

这相当简单,但我们还缺少什么?反馈!

提供反馈 环境需要提供适当的奖励,并确定一集是否完成。

让我们首先加入done标志以指示一集是否结束。

...

def move_agent(self, action):
    ...
    done = False  # The episode is not done by default

    # Check for a valid move
    if self.is_valid_location(new_location):
        # Remove agent from old location
        self.grid[previous_location[0]][previous_location[1]] = 0

        # Add agent to new location
        self.grid[new_location[0]][new_location[1]] = 1

        # Update agent's location
        self.agent_location = new_location

        # Check if the new location is the reward location
        if self.agent_location == self.goal_location:
            # Episode is complete
            done = True

    return done

...

我们将done默认设置为 false。如果新的agent_locationgoal_location相同,则将done设置为 true。最后,我们返回这个值。

我们已经为奖励结构做好了准备。首先,我将展示稀疏奖励结构的实现。这对于大约 5x5 的网格是足够的,但我们将更新它以适应更大的环境。

稀疏奖励 实现稀疏奖励非常简单。我们主要需要在到达目标时给予奖励。

我们还可以为每一步未到达目标的情况给予小的负奖励,并为撞击边界的情况给予更大的奖励。这将鼓励我们的代理优先选择最短路径。

...

def move_agent(self, action):
    ...
    done = False # The episode is not done by default
    reward = 0   # Initialize reward

    # Check for a valid move
    if self.is_valid_location(new_location):
        # Remove agent from old location
        self.grid[previous_location[0]][previous_location[1]] = 0

        # Add agent to new location
        self.grid[new_location[0]][new_location[1]] = 1

        # Update agent's location
        self.agent_location = new_location

        # Check if the new location is the reward location
        if self.agent_location == self.goal_location:
            # Reward for getting the goal
            reward = 100

            # Episode is complete
            done = True
        else:
            # Small punishment for valid move that did not get the goal
            reward = -1
    else:
        # Slightly larger punishment for an invalid move
        reward = -3

    return reward, done

...

确保初始化reward以便在 if 块之后可以访问。此外,仔细检查每种情况:有效移动和达成目标、有效移动和未达成目标、以及无效移动。

稠密奖励 实施稠密奖励系统仍然相当简单,只是需要更频繁地提供反馈。

让代理逐步朝目标移动的好方法是什么?

第一个方法是返回曼哈顿距离的负值。曼哈顿距离是行方向的距离加上列方向的距离,而不是直线距离。以下是代码示例:

reward = -(np.abs(self.goal_location[0] - new_location[0]) + \
           np.abs(self.goal_location[1] - new_location[1]))

所以,行方向的步数加上列方向的步数,并取其负值。

另一种方法是根据代理移动的方向提供奖励:如果它远离目标,则提供负奖励;如果它朝目标移动,则提供正奖励。

我们可以通过将新的曼哈顿距离从之前的曼哈顿距离中减去来计算。这将是 1 或-1,因为代理每步只能移动一个单元格。

在我们的情况下,选择第二个选项最为合适。这应该提供更好的结果,因为它基于每一步提供即时反馈,而不是更一般的奖励。

这个选项的代码:

...

def move_agent(self, action):
    ...
        if self.agent_location == self.goal_location:
            ...
        else:
            # Calculate the distance before the move
            previous_distance = np.abs(self.goal_location[0] - previous_location[0]) + \
                                np.abs(self.goal_location[1] - previous_location[1])

            # Calculate the distance after the move
            new_distance = np.abs(self.goal_location[0] - new_location[0]) + \
                           np.abs(self.goal_location[1] - new_location[1])

            # If new_location is closer to the goal, reward = 1, if further, reward = -1
            reward = (previous_distance - new_distance)
    ...

如你所见,如果代理没有达到目标,我们计算previous_distancenew_distance,然后将reward定义为这两者的差值。

根据表现情况,可能需要对其进行缩放,或对系统中的任何奖励进行缩放。如果需要更高,可以通过简单地乘以一个数字(例如 0.01、2、100)来实现。它们的比例需要有效地引导代理到目标。例如,为接近目标提供 1 的奖励,为目标本身提供 0.1 的奖励是不太合理的。

奖励是成比例的。如果你以相同的因子缩放每个正奖励和负奖励,通常不会对训练产生影响,除非是非常大或非常小的值。

总结来说,如果代理离目标还有 10 步,而它移动到一个离目标 11 步的地方,则reward将是-1。

这是更新后的 **move_agent**

def move_agent(self, action):
    # Map agent action to the correct movement
    moves = {
        0: (-1, 0), # Up
        1: (1, 0),  # Down
        2: (0, -1), # Left
        3: (0, 1)   # Right
    }

    previous_location = self.agent_location

    # Determine the new location after applying the action
    move = moves[action]
    new_location = (previous_location[0] + move[0], previous_location[1] + move[1])

    done = False # The episode is not done by default
    reward = 0   # Initialize reward

    # Check for a valid move
    if self.is_valid_location(new_location):
        # Remove agent from old location
        self.grid[previous_location[0]][previous_location[1]] = 0

        # Add agent to new location
        self.grid[new_location[0]][new_location[1]] = 1

        # Update agent's location
        self.agent_location = new_location

        # Check if the new location is the reward location
        if self.agent_location == self.goal_location:
            # Reward for getting the goal
            reward = 100

            # Episode is complete
            done = True
        else:
            # Calculate the distance before the move
            previous_distance = np.abs(self.goal_location[0] - previous_location[0]) + \
                                np.abs(self.goal_location[1] - previous_location[1])

            # Calculate the distance after the move
            new_distance = np.abs(self.goal_location[0] - new_location[0]) + \
                           np.abs(self.goal_location[1] - new_location[1])

            # If new_location is closer to the goal, reward = 1, if further, reward = -1
            reward = (previous_distance - new_distance)
    else:
        # Slightly larger punishment for an invalid move
        reward = -3

    return reward, done

实现目标和尝试无效移动的奖励应保持一致。

步骤惩罚 还有一件事我们遗漏了。

代理当前没有因达到目标所需时间而受到惩罚。我们实现的奖励结构有许多净中性循环。它可能在两个位置之间来回移动而不积累任何惩罚。我们可以通过每步扣除一个小值来解决这个问题,使得远离目标的惩罚大于接近目标的奖励。这个说明应该会让情况更清楚。

奖励路径有和没有步骤惩罚 — 作者插图

想象代理从最左边的节点开始,并必须做出决策。如果没有步骤惩罚,它可以选择前进,然后返回任意次数,其总奖励将在最终移动到目标之前为 1。

所以从数学上讲,循环 1000 次然后再到达目标和直接到达目标是一样有效的。

试着想象在两种情况下循环,看惩罚是如何累积的(或者没有累积)。

让我们来实现它。

...

# If new_location is closer to the goal, reward = 0.9, if further, reward = -1.1
reward = (previous_distance - new_distance) - 0.1

...

就这样。代理现在应该受到激励去选择最短路径,防止循环行为。

好的,但重点是什么? 此时你可能会认为定义奖励系统并训练一个任务可以用更简单的算法完成是浪费时间。

你说得对。

我们这样做的原因是为了学习如何指导代理实现其目标。在这种情况下可能看起来很简单,但如果代理的环境中包含要拾取的物品、要战斗的敌人、要穿越的障碍物等等呢?

或者一个在现实世界中需要协调数十个传感器和电机以导航复杂和多变环境的机器人?

使用传统编程设计一个系统来完成这些任务将会非常困难,并且肯定不会像使用 RL 和良好的奖励结构那样自然或通用,以鼓励代理学习最佳策略。

强化学习在定义完成任务所需的精确步骤序列由于环境的复杂性和可变性而困难或不可能的应用中最为有用。你需要 RL 工作的唯一条件是能够定义什么是有用的行为,以及应该避免什么行为。

最终的环境方法——**step** 现在我们可以定义代理和环境之间交互的核心,因为Environment的每个组件都到位了。

幸运的是,这非常简单。

def step(self, action):
    # Apply the action to the environment, record the observations
    reward, done = self.move_agent(action)
    next_state = self.get_state()

    # Render the grid at each step
    if self.render_on:
        self.render()

    return reward, next_state, done

step首先在环境中移动代理并记录rewarddone。然后它获取此交互之后的状态,next_state。然后如果render_on设置为 true,则会渲染网格。

最后,step返回记录的值,rewardnext_statedone

这些将是构建我们代理将从中学习的经验的重要组成部分。

恭喜!你已经正式完成了你的 DRL 健身环境的构建。

下面是完成的**Environment**类。

import random
import numpy as np

class Environment:
    def __init__(self, grid_size, render_on=False):
        self.grid_size = grid_size
        self.render_on = render_on
        self.grid = []
        self.agent_location = None
        self.goal_location = None

    def reset(self):
        # Initialize the empty grid as a 2d array of 0s
        self.grid = np.zeros((self.grid_size, self.grid_size))

        # Add the agent and the goal to the grid
        self.agent_location = self.add_agent()
        self.goal_location = self.add_goal()

        # Render the initial grid
        if self.render_on:
            self.render()

        # Return the initial state
        return self.get_state()

    def add_agent(self):
        # Choose a random location
        location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))

        # Agent is represented by a 1
        self.grid[location[0]][location[1]] = 1
        return location

    def add_goal(self):
        # Choose a random location
        location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))

        # Get a random location until it is not occupied
        while self.grid[location[0]][location[1]] == 1:
            location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))

        # Goal is represented by a -1
        self.grid[location[0]][location[1]] = -1

        return location

    def move_agent(self, action):
        # Map agent action to the correct movement
        moves = {
            0: (-1, 0), # Up
            1: (1, 0),  # Down
            2: (0, -1), # Left
            3: (0, 1)   # Right
        }

        previous_location = self.agent_location

        # Determine the new location after applying the action
        move = moves[action]
        new_location = (previous_location[0] + move[0], previous_location[1] + move[1])

        done = False  # The episode is not done by default
        reward = 0   # Initialize reward

        # Check for a valid move
        if self.is_valid_location(new_location):
            # Remove agent from old location
            self.grid[previous_location[0]][previous_location[1]] = 0

            # Add agent to new location
            self.grid[new_location[0]][new_location[1]] = 1

            # Update agent's location
            self.agent_location = new_location

            # Check if the new location is the reward location
            if self.agent_location == self.goal_location:
                # Reward for getting the goal
                reward = 100

                # Episode is complete
                done = True
            else:
                # Calculate the distance before the move
                previous_distance = np.abs(self.goal_location[0] - previous_location[0]) + \
                                    np.abs(self.goal_location[1] - previous_location[1])

                # Calculate the distance after the move
                new_distance = np.abs(self.goal_location[0] - new_location[0]) + \
                               np.abs(self.goal_location[1] - new_location[1])

                # If new_location is closer to the goal, reward = 0.9, if further, reward = -1.1
                reward = (previous_distance - new_distance) - 0.1
        else:
            # Slightly larger punishment for an invalid move
            reward = -3

        return reward, done

    def is_valid_location(self, location):
        # Check if the location is within the boundaries of the grid
        if (0 <= location[0] < self.grid_size) and (0 <= location[1] < self.grid_size):
            return True
        else:
            return False

    def get_state(self):
        # Flatten the grid from 2d to 1d
        state = self.grid.flatten()
        return state

    def render(self):
        # Convert to a list of ints to improve formatting
        grid = self.grid.astype(int).tolist()
        for row in grid:
            print(row)
        print('') # To add some space between renders for each step

    def step(self, action):
        # Apply the action to the environment, record the observations
        reward, done = self.move_agent(action)
        next_state = self.get_state()

        # Render the grid at each step
        if self.render_on:
            self.render()

        return reward, next_state, done

到目前为止我们已经讨论了很多内容。返回到全局视图并使用你的新知识重新评估每部分的互动可能会很有益,然后再继续前进。

6. 从经验中学习:经验回放

代理的模型和策略,以及环境的奖励结构和采取步骤的机制都已经完成,但我们需要某种方式来记住过去,以便代理能够从中学习。

这可以通过保存经验来实现。

每个经验都包括几项内容:

  • 状态:在采取行动之前的状态。

  • 行动:在这个状态下采取了什么行动。

  • 奖励:代理根据其行动从环境中获得的正面或负面反馈。

  • 下一状态:紧跟动作之后的状态,使代理能够不仅仅基于当前状态的结果行动,而是基于多个状态的提前信息。

  • 完成:表示一个经验的结束,让代理知道任务是否已完成。它在每一步可以是 true 或 false。

这些术语你应该不陌生,但再看一遍也无妨!

每个经验都与代理的一个步骤相关联。这将提供训练所需的全部上下文。

ExperienceReplay

为了跟踪并在需要时提供这些经验,我们将定义最后一个类,ExperienceReplay

from collections import deque, namedtuple

class ExperienceReplay:
    def __init__(self, capacity, batch_size):
        # Memory stores the experiences in a deque, so if capacity is exceeded it removes
        # the oldest item efficiently
        self.memory = deque(maxlen=capacity)

        # Batch size specifices the amount of experiences that will be sampled at once
        self.batch_size = batch_size

        # Experience is a namedtuple that stores the relevant information for training
        self.Experience = namedtuple('Experience', ['state', 'action', 'reward', 'next_state', 'done'])

该类将接受 capacity,一个定义我们一次保存的最大经验数量的整数值,以及 batch_size,一个决定我们每次为训练采样多少经验的整数值。

批处理经验 如果你还记得,Agent 类中的神经网络接受输入批次。虽然我们只用一个大小为一的批次进行预测,但这对于训练来说效率极低。通常,批次大小为 32 或更大的情况更为常见。

批处理输入进行训练有两个作用:

  • 提高了效率,因为它允许并行处理多个数据点,减少计算开销,并更好地利用 GPU 或 CPU 资源。

  • 帮助模型更一致地学习,因为它一次学习来自多种示例的内容,这可以提高其处理新数据的能力。

内存 memory 将是一个双端队列(deque)。这允许我们将新经验添加到前面,并且当达到由 capacity 定义的最大长度时,双端队列将删除它们,而不需要像 Python 列表那样移动每个元素。这在 capacity 设置为 10,000 或更多时可以大大提高速度。

经验 每个经验将被定义为一个 namedtuple。虽然许多其他数据结构也可以,但这将提高可读性,因为我们在训练时按需提取每一部分。

**add_experience** **sample_batch** 实现 添加新经验和采样批次是相当直接的。

import random

def add_experience(self, state, action, reward, next_state, done):
    # Create a new experience and store it in memory
    experience = self.Experience(state, action, reward, next_state, done)
    self.memory.append(experience)

def sample_batch(self):
    # Batch will be a random sample of experiences from memory of size batch_size
    batch = random.sample(self.memory, self.batch_size)
    return batch

方法 add_experience 创建一个 namedtuple,包含经验的每一部分:stateactionrewardnext_statedone,并将其附加到 memory 中。

sample_batch 同样简单。它从 memory 中获取并返回一个大小为 batch_size 的随机样本。

经验回放用于存储代理的经验以便批量处理和学习 — 图像来源于作者

最后一个方法**can_provide_sample** 最终,能够检查 memory 是否包含足够的经验以提供完整的样本,将在尝试获取训练批次之前非常有用。

def can_provide_sample(self):
    # Determines if the length of memory has exceeded batch_size
    return len(self.memory) >= self.batch_size

完成 **ExperienceReplay** 类…

import random
from collections import deque, namedtuple

class ExperienceReplay:
    def __init__(self, capacity, batch_size):
        # Memory stores the experiences in a deque, so if capacity is exceeded it removes
        # the oldest item efficiently
        self.memory = deque(maxlen=capacity)

        # Batch size specifices the amount of experiences that will be sampled at once
        self.batch_size = batch_size

        # Experience is a namedtuple that stores the relevant information for training
        self.Experience = namedtuple('Experience', ['state', 'action', 'reward', 'next_state', 'done'])

    def add_experience(self, state, action, reward, next_state, done):
        # Create a new experience and store it in memory
        experience = self.Experience(state, action, reward, next_state, done)
        self.memory.append(experience)

    def sample_batch(self):
        # Batch will be a random sample of experiences from memory of size batch_size
        batch = random.sample(self.memory, self.batch_size)
        return batch

    def can_provide_sample(self):
        # Determines if the length of memory has exceeded batch_size
        return len(self.memory) >= self.batch_size

在保存每个经验和从中抽样的机制到位后,我们可以返回到Agent类,以最终启用学习。

7. 定义代理的学习过程:调整神经网络

训练神经网络的目标是使其产生的 Q 值准确地代表每个选择将提供的未来奖励。

本质上,我们希望网络学习预测每个决策的价值,不仅考虑即时奖励,还要考虑可能带来的未来奖励。

纳入未来奖励 为实现这一点,我们将后续状态的 Q 值纳入训练过程。

当代理采取行动并移动到新状态时,我们查看这个新状态中的 Q 值,以帮助确定先前行动的价值。换句话说,潜在的未来奖励会影响当前选择的感知价值。

**learn** 方法

import numpy as np

def learn(self, experiences):
    states = np.array([experience.state for experience in experiences])
    actions = np.array([experience.action for experience in experiences])
    rewards = np.array([experience.reward for experience in experiences])
    next_states = np.array([experience.next_state for experience in experiences])
    dones = np.array([experience.done for experience in experiences])

    # Predict the Q-values (action values) for the given state batch
    current_q_values = self.model.predict(states, verbose=0)

    # Predict the Q-values for the next_state batch
    next_q_values = self.model.predict(next_states, verbose=0)
    ...

使用提供的批量数据experiences,我们将通过列表推导和之前在ExperienceReplay中定义的namedtuple值提取每一部分。然后我们将每个部分转换为 NumPy 数组,以提高效率并与模型的预期一致,如前所述。

最后,我们使用模型预测在当前状态下采取行动的 Q 值以及紧接着的状态。

在继续learn方法之前,我需要解释一下折扣因子的概念。

折扣未来奖励——gamma 的作用 直观地说,我们知道在其他条件相同的情况下,立即奖励通常会被优先考虑。(你希望今天还是下周拿到工资?)

从数学上表示这一点可能显得不太直观。考虑到未来,我们不希望它与现在同等重要(加权)。折扣未来的程度,即每个决策的影响降低程度,由 gamma(通常用希腊字母γ表示)定义。

Gamma 可以进行调整,较高的值鼓励规划,较低的值则鼓励更短视的行为。我们将使用默认值 0.99。

折扣因子通常在 0 和 1 之间。大于 1 的折扣因子会优先考虑未来而非现在,这会引入不稳定的行为,实际应用很少。

实现 gamma 和定义目标 Q 值 记住,在训练神经网络的背景下,这一过程依赖于两个关键要素:我们提供的输入数据和我们希望网络学习预测的对应输出。

我们需要向网络提供一些目标 Q 值,这些 Q 值是基于环境在特定状态和行动下给予的奖励,以及下一个状态中最佳行动的折扣(由 gamma 折扣)预测奖励更新的。

我知道这可能很难理解,但通过实现和示例会更好地解释。

import numpy as np
...

class Agent:
    def __init__(self, grid_size, epsilon=1, epsilon_decay=0.995, epsilon_end=0.01, gamma=0.99):
        ...
        self.gamma = gamma
        ...
    ...

    def learn(self, experiences):
        ...

        # Initialize the target Q-values as the current Q-values
        target_q_values = current_q_values.copy()

        # Loop through each experience in the batch
        for i in range(len(experiences)):
            if dones[i]:
                # If the episode is done, there is no next Q-value
                # [i, actions[i]] is the numpy equivalent of [i][actions[i]]
                target_q_values[i, actions[i]] = rewards[i]
            else:
                # The updated Q-value is the reward plus the discounted max Q-value for the next state
                # [i, actions[i]] is the numpy equivalent of [i][actions[i]]
                target_q_values[i, actions[i]] = rewards[i] + self.gamma * np.max(next_q_values[i])
        ...

我们已经定义了类属性gamma,其默认值为 0.99。

然后,在获取我们上面实现的statenext_state的预测后,我们将target_q_values初始化为当前的 Q 值。这些将在以下循环中更新。

更新 **target_q_values** 我们遍历批次中的每个experience,有两种情况来更新这些值:

  • 如果回合已done,则该动作的target_q_value仅仅是给定的奖励,因为没有相关的next_q_value

  • 否则,回合尚未done,该动作的target_q_value变为给定的奖励,加上next_q_values中预测的下一个动作的折扣 Q 值。

如果done为真,则更新:

target_q_values[i, actions[i]] = rewards[i]

如果done为假,则更新:

target_q_values[i, actions[i]] = rewards[i] + self.gamma * np.max(next_q_values[i])

这里的语法target_q_values[i, actions[i]]可能看起来令人困惑,但它本质上是第 i 个经验的 Q 值,对于动作actions[i]

 Experience in batch   Reward from environment
                v                    v
target_q_values[i, actions[i]] = rewards[i]
                       ^
           Index of the action chosen

这相当于 NumPy 中的 *[i][actions[i]]* 在 Python 列表中。记住每个动作是一个索引(0 到 3)。

如何 **target_q_values** 被更新

为了更清楚地说明这一点,我将展示target_q_values如何更紧密地与实际奖励对齐,随着训练的进行。记住我们在处理一个批次。这将是一个简单的三个样本的批次。

另外,确保你理解experiences中的条目是独立的。这意味着这不是一个步骤序列,而是从一组独立经验中随机抽取的样本。

假设actionsrewardsdonescurrent_q_valuesnext_q_values的值如下。

gamma = 0.99
actions = [1, 2, 2]  # (down, left, left)
rewards = [1, -1, 100] # Rewards given by the environment for the action
dones = [False, False, True] # Indicating whether the episode is complete

current_q_values = [
    [2, 5, -2, -3],  # In this state, action 2 (index 1) is best so far
    [1, 3, 4, -1],   # Here, action 3 (index 2) is currently favored
    [-3, 2, 6, 1]    # Action 3 (index 2) has the highest Q-value in this state
]

next_q_values = [
    [1, 4, -1, -2],  # Future Q-values after taking each action from the first state
    [2, 2, 5, 0],    # Future Q-values from the second state
    [-2, 3, 7, 2]    # Future Q-values from the third state
]

然后我们将current_q_values复制到target_q_values中进行更新。

target_q_values = current_q_values

然后,对于批次中的每个经验,我们可以展示相关的值。

这不是代码,而只是每个阶段值的示例。如果你迷失了,确保回到初始值查看每个值的来源。

条目 1

i = 0 # This is the first entry in the batch (first loop)

# First entries of associated values
actions[i] = 1
rewards[i] = 1
dones[i] = False
target_q_values[i] = [2, 5, -2, -3]
next_q_values[i] = [1, 4, -1, -2]

因为这个经验的dones[i]为假,我们需要考虑next_q_values并应用gamma(0.99)。

target_q_values[i, actions[i]] = rewards[i] + 0.99 * max(next_q_values[i])

为什么获取next_q_values[i]的最大值?因为那将是下一个选择的动作,我们需要估计的奖励(Q 值)。

然后我们在索引对应于actions[i]target_q_values中,将其更新为该状态/动作对的奖励加上下一个状态/动作对的折扣奖励。

这是该经验在更新后的目标值。

# Updated target_q_values[i]
target_q_values[i] = [2, 4.96, -2, -3]
                ^          ^
              i = 0    action[i] = 1

如你所见,对于当前状态,选择 1(向下)现在更具吸引力,因为值更高且这种行为已经被强化。

自己计算这些可能有助于真正弄清楚。

条目 2

i = 1 # This is the second entry in the batch

# Second entries of associated values
actions[i] = 2
rewards[i] = -1
dones[i] = False
target_q_values[i] = [1, 3, 4, -1]
next_q_values[i] = [2, 2, 5, 0]

dones[i]在这里也是假的,因此我们需要考虑next_q_values

target_q_values[i, actions[i]] = rewards[i] + 0.99 * max(next_q_values[i])

再次,在索引actions[i]处更新第 i 个经验的target_q_values

# Updated target_q_values[i]
target_q_values[i] = [1, 3, 3.95, -1]
                ^             ^
              i = 1      action[i] = 2

选择 2(向左)现在不再那么理想,因为 Q 值较低且这种行为被抑制。

条目 3

最后的条目在这一批中。

i = 2 # This is the third and final entry in the batch

# Second entries of associated values
actions[i] = 2
rewards[i] = 100
dones[i] = True
target_q_values[i] = [-3, 2, 6, 1]
next_q_values[i] = [-2, 3, 7, 2]

这个条目的dones[i]为真,表示这一轮已完成,不会再采取进一步的行动。这意味着我们在更新时不考虑next_q_values

target_q_values[i, actions[i]] = rewards[i]

注意我们只是将target_q_values[i, action[i]]设置为rewards[i]的值,因为不会再有更多的行动 — 没有未来需要考虑。

# Updated target_q_values[i]
target_q_values[i] = [-3, 2, 100, 1]
                ^             ^
              i = 2       action[i] = 2

在这种及类似状态中选择 2(左)现在会更具吸引力。

这是目标在智能体左侧的状态,因此当选择那个行动时,给予了全部奖励。

尽管它可能看起来相当令人困惑,但这个想法只是为了制作准确表示环境给予的奖励的更新 Q 值,以便提供给神经网络。这就是神经网络需要近似的内容。

尝试反向思考。由于到达目标的奖励相当可观,它将在状态中创建传播效应,最终到达智能体实现目标的状态。这就是 gamma 在考虑下一个状态及其在状态空间中奖励值向后传播的作用的力量。

奖励在状态空间中的波及效应 — 作者提供的图片

上面是 Q 值和折扣因子的简化版本,仅考虑目标的奖励,而不考虑增量奖励或惩罚。

选择网格中的任何一个单元格,并移动到质量最高的相邻单元格。你会发现它总是提供到达目标的最佳路径。

这一效果不是立竿见影的。它需要智能体探索状态和行动空间,逐渐学习和调整策略,建立对不同行动如何导致不同奖励的理解。

如果奖励结构经过精心设计,这将慢慢引导我们的智能体采取更有利的行动。

拟合神经网络 对于learn方法,最后需要做的是将智能体的神经网络与states及其相关的target_q_values配对。TensorFlow 将处理权重的更新,使其更准确地预测类似状态下的这些值。

...

def learn(self, experiences):
    states = np.array([experience.state for experience in experiences])
    actions = np.array([experience.action for experience in experiences])
    rewards = np.array([experience.reward for experience in experiences])
    next_states = np.array([experience.next_state for experience in experiences])
    dones = np.array([experience.done for experience in experiences])

    # Predict the Q-values (action values) for the given state batch
    current_q_values = self.model.predict(states, verbose=0)

    # Predict the Q-values for the next_state batch
    next_q_values = self.model.predict(next_states, verbose=0)

    # Initialize the target Q-values as the current Q-values
    target_q_values = current_q_values.copy()

    # Loop through each experience in the batch
    for i in range(len(experiences)):
        if dones[i]:
            # If the episode is done, there is no next Q-value
            target_q_values[i, actions[i]] = rewards[i]
        else:
            # The updated Q-value is the reward plus the discounted max Q-value for the next state
            # [i, actions[i]] is the numpy equivalent of [i][actions[i]]
            target_q_values[i, actions[i]] = rewards[i] + self.gamma * np.max(next_q_values[i])

    # Train the model
    self.model.fit(states, target_q_values, epochs=1, verbose=0)

唯一的新部分是self.model.fit(states, target_q_values, epochs=1, verbose=0)fit有两个主要参数:输入数据和我们想要的目标值。在这种情况下,我们的输入是一批states,目标值是每个状态的更新 Q 值。

epochs=1只是设置你希望网络尝试拟合数据的次数。一个就足够了,因为我们希望它能够很好地泛化,而不是拟合到这个特定的批次。verbose=0只是告诉 TensorFlow 不要打印类似进度条的调试信息。

Agent类现在具备了从经验中学习的能力,但它还需要两个简单的方法 — saveload

保存和加载训练好的模型 保存和加载模型可以防止我们每次需要时都进行完全的重训练。我们可以使用只需一个参数file_path的简单 TensorFlow 方法。

from tensorflow.keras.models import load_model

def load(self, file_path):
    self.model = load_model(file_path)

def save(self, file_path):
    self.model.save(file_path)

创建一个名为 models 的目录,或者其他你喜欢的名字,然后你可以在设定的间隔保存训练好的模型。这些文件以.h5 结尾。所以每当你想要保存模型时,只需调用agent.save('models/model_name.h5')。加载模型时也是如此。

完整 **Agent**

from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Sequential, load_model
import numpy as np

class Agent:
    def __init__(self, grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01, gamma=0.99):
        self.grid_size = grid_size
        self.epsilon = epsilon
        self.epsilon_decay = epsilon_decay
        self.epsilon_end = epsilon_end
        self.gamma = gamma

    def build_model(self):
        # Create a sequential model with 3 layers
        model = Sequential([
            # Input layer expects a flattened grid, hence the input shape is grid_size squared
            Dense(128, activation='relu', input_shape=(self.grid_size**2,)),
            Dense(64, activation='relu'),
            # Output layer with 4 units for the possible actions (up, down, left, right)
            Dense(4, activation='linear')
        ])

        model.compile(optimizer='adam', loss='mse')

        return model

    def get_action(self, state):

        # rand() returns a random value between 0 and 1
        if np.random.rand() <= self.epsilon:
            # Exploration: random action
            action = np.random.randint(0, 4)
        else:
            # Add an extra dimension to the state to create a batch with one instance
            state = np.expand_dims(state, axis=0)

            # Use the model to predict the Q-values (action values) for the given state
            q_values = self.model.predict(state, verbose=0)

            # Select and return the action with the highest Q-value
            action = np.argmax(q_values[0]) # Take the action from the first (and only) entry

        # Decay the epsilon value to reduce the exploration over time
        if self.epsilon > self.epsilon_end:
            self.epsilon *= self.epsilon_decay

        return action

    def learn(self, experiences):
        states = np.array([experience.state for experience in experiences])
        actions = np.array([experience.action for experience in experiences])
        rewards = np.array([experience.reward for experience in experiences])
        next_states = np.array([experience.next_state for experience in experiences])
        dones = np.array([experience.done for experience in experiences])

        # Predict the Q-values (action values) for the given state batch
        current_q_values = self.model.predict(states, verbose=0)

        # Predict the Q-values for the next_state batch
        next_q_values = self.model.predict(next_states, verbose=0)

        # Initialize the target Q-values as the current Q-values
        target_q_values = current_q_values.copy()

        # Loop through each experience in the batch
        for i in range(len(experiences)):
            if dones[i]:
                # If the episode is done, there is no next Q-value
                target_q_values[i, actions[i]] = rewards[i]
            else:
                # The updated Q-value is the reward plus the discounted max Q-value for the next state
                # [i, actions[i]] is the numpy equivalent of [i][actions[i]]
                target_q_values[i, actions[i]] = rewards[i] + self.gamma * np.max(next_q_values[i])

        # Train the model
        self.model.fit(states, target_q_values, epochs=1, verbose=0)

    def load(self, file_path):
        self.model = load_model(file_path)

    def save(self, file_path):
        self.model.save(file_path)

你的深度强化学习环境的每个类现在都完成了!你已经成功地编码了AgentEnvironmentExperienceReplay。剩下的唯一任务就是主训练循环。

8. 执行训练循环:将所有部分整合在一起

我们已进入项目的最后阶段!我们编码的每一部分,AgentEnvironmentExperienceReplay,都需要某种交互方式。

这将是主要程序,其中每个回合都会运行,并且我们定义像epsilon这样的超参数。

虽然它相当简单,但我会在编码时将每一部分拆开,以便更加清晰。

初始化每一部分 首先,我们设置grid_size并使用我们创建的类来初始化每个实例。

from environment import Environment
from agent import Agent
from experience_replay import ExperienceReplay

if __name__ == '__main__':

    grid_size = 5

    environment = Environment(grid_size=grid_size, render_on=True)
    agent = Agent(grid_size=grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01)
    experience_replay = ExperienceReplay(capacity=10000, batch_size=32)
    ...

现在我们已经准备好主训练循环所需的每一部分。

回合数和步骤限制 接下来,我们将定义训练中要运行的回合数和每个回合允许的最大步骤数。

限制步骤数有助于确保我们的代理不会陷入循环,并鼓励较短的路径。我们会相当慷慨地为 5x5 设置最大值为 200。对于较大的环境,这个值需要增加。

from environment import Environment
from agent import Agent
from experience_replay import ExperienceReplay

if __name__ == '__main__':

    grid_size = 5

    environment = Environment(grid_size=grid_size, render_on=True)
    agent = Agent(grid_size=grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01)
    experience_replay = ExperienceReplay(capacity=10000, batch_size=32)

    # Number of episodes to run before training stops
    episodes = 5000
    # Max number of steps in each episode
    max_steps = 200
    ...

回合循环 在每个回合中,我们将重置environment并保存初始state。然后,我们执行每一步,直到done为真或达到max_steps。最后,我们保存模型。每一步的逻辑尚未完全实现。

from environment import Environment
from agent import Agent
from experience_replay import ExperienceReplay

if __name__ == '__main__':

    grid_size = 5

    environment = Environment(grid_size=grid_size, render_on=True)
    agent = Agent(grid_size=grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01)
    experience_replay = ExperienceReplay(capacity=10000, batch_size=32)

    # Number of episodes to run before training stops
    episodes = 5000
    # Max number of steps in each episode
    max_steps = 200

    for episode in range(episodes):
        # Get the initial state of the environment and set done to False
        state = environment.reset()

        # Loop until the episode finishes
        for step in range(max_steps):
            # Logic for each step
            ...
            if done:
                break

        agent.save(f'models/model_{grid_size}.h5')

注意,我们使用grid_size来命名模型,因为神经网络架构会因每个输入大小而异。尝试将 5x5 的模型加载到 10x10 的架构中将会导致错误。

步骤逻辑 最终,在步骤循环内部,我们将按照之前讨论的方式安排各部分之间的交互。

from environment import Environment
from agent import Agent
from experience_replay import ExperienceReplay

if __name__ == '__main__':

    grid_size = 5

    environment = Environment(grid_size=grid_size, render_on=True)
    agent = Agent(grid_size=grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01)
    experience_replay = ExperienceReplay(capacity=10000, batch_size=32)

    # Number of episodes to run before training stops
    episodes = 5000
    # Max number of steps in each episode
    max_steps = 200

    for episode in range(episodes):
        # Get the initial state of the environment and set done to False
        state = environment.reset()

        # Loop until the episode finishes
        for step in range(max_steps):
            print('Episode:', episode)
            print('Step:', step)
            print('Epsilon:', agent.epsilon)

            # Get the action choice from the agents policy
            action = agent.get_action(state)

            # Take a step in the environment and save the experience
            reward, next_state, done = environment.step(action)
            experience_replay.add_experience(state, action, reward, next_state, done)

            # If the experience replay has enough memory to provide a sample, train the agent
            if experience_replay.can_provide_sample():
                experiences = experience_replay.sample_batch()
                agent.learn(experiences)

            # Set the state to the next_state
            state = next_state

            if done:
                break

        agent.save(f'models/model_{grid_size}.h5')

对于每个回合的每一步,我们首先打印回合数和步骤数,以便获得关于训练进度的信息。此外,你可以打印epsilon以查看代理动作的随机性百分比。这也有帮助,因为如果你想要停止,可以在相同的epsilon值下重新启动代理。

在打印信息后,我们使用agent策略从这个state中获取action,在environment中执行一步,并记录返回的值。

然后我们将stateactionrewardnext_statedone保存为经验。如果experience_replay有足够的内存,我们将对agent进行随机经验批次训练。

最后,我们将state设置为next_state,并检查这一回合是否done

一旦你运行了至少一个回合,你将会有一个保存的模型,可以加载并继续之前的操作或评估性能。

初始化agent后,只需使用它的加载方法,类似于我们保存时的操作 — agent.load(f’models/model_{grid_size}.h5')

你还可以在每一步中添加一个小的延迟,当你使用时间评估模型时 — time.sleep(0.5)。这会让每一步暂停半秒钟。确保包括import time

完成训练循环

from environment import Environment
from agent import Agent
from experience_replay import ExperienceReplay
import time

if __name__ == '__main__':

    grid_size = 5

    environment = Environment(grid_size=grid_size, render_on=True)
    agent = Agent(grid_size=grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01)
    # agent.load(f'models/model_{grid_size}.h5')

    experience_replay = ExperienceReplay(capacity=10000, batch_size=32)

    # Number of episodes to run before training stops
    episodes = 5000
    # Max number of steps in each episode
    max_steps = 200

    for episode in range(episodes):

        # Get the initial state of the environment and set done to False
        state = environment.reset()

        # Loop until the episode finishes
        for step in range(max_steps):
            print('Episode:', episode)
            print('Step:', step)
            print('Epsilon:', agent.epsilon)

            # Get the action choice from the agents policy
            action = agent.get_action(state)

            # Take a step in the environment and save the experience
            reward, next_state, done = environment.step(action)
            experience_replay.add_experience(state, action, reward, next_state, done)

            # If the experience replay has enough memory to provide a sample, train the agent
            if experience_replay.can_provide_sample():
                experiences = experience_replay.sample_batch()
                agent.learn(experiences)

            # Set the state to the next_state
            state = next_state

            if done:
                break

            # Optionally, pause for half a second to evaluate the model
            # time.sleep(0.5)

        agent.save(f'models/model_{grid_size}.h5')

当你需要time.sleepagent.load时,只需取消注释它们即可。

运行程序 试运行一下!你应该能够成功训练智能体完成一个大约 8x8 的网格环境。如果网格大小远大于此,训练会变得困难。

尝试看看你可以让环境变得多大。你可以做一些事情,比如向神经网络添加层和神经元、更改epsilon_decay,或给予更多的训练时间。这样做可以巩固你对每个部分的理解。

例如,你可能会注意到 *epsilon* 很快就达到了 *epsilon_end* 。如果你愿意,可以将 *epsilon_decay* 更改为 0.9998 或 0.99998。

随着网格大小的增加,网络接收到的状态会呈指数增长。

我在最后添加了一个简短的附加部分,修复了这个问题,并演示了有许多方法可以为智能体表示环境。

9. 总结

恭喜你完成了对强化学习和深度 Q 学习世界的全面探索!

尽管总有更多内容可以覆盖,你仍然可以获得重要的见解和技能。

在本指南中,你:

  • 介绍了强化学习的核心概念以及为什么它在人工智能中至关重要。

  • 构建了一个简单的环境,为智能体互动和学习奠定了基础。

  • 定义了用于深度 Q 学习的智能体神经网络架构,使你的智能体能够在比传统 Q 学习更复杂的环境中做出决策。

  • 理解了探索在利用学习策略之前的重要性,并实现了 Epsilon-Greedy 策略。

  • 实现了奖励系统以引导智能体达到目标,并学习了稀疏奖励和密集奖励之间的区别。

  • 设计了经验回放机制,让智能体能够从过去的经验中学习。

  • 获得了在拟合神经网络中的实际操作经验,这是一个关键过程,智能体根据环境反馈改进其性能。

  • 将所有这些部分结合在一个训练循环中,观察智能体的学习过程并进行调整,以获得最佳性能。

到现在为止,你应该对强化学习和深度 Q 学习有了信心。通过从头构建一个 DRL 环境,你不仅在理论上建立了坚实的基础,而且在实际应用中也得到了锻炼。

这些知识使你能够处理更复杂的 RL 问题,并为进一步探索这个激动人心的 AI 领域铺平了道路。

Agar.io 风格的游戏,其中代理被鼓励相互吞噬以获胜——作者制作的 GIF

上面是一个受 Agar.io 启发的网格游戏,其中代理被鼓励通过相互吞噬来增大体积。在每一步,环境都会使用 Python 库Matplotlib绘制在图上。围绕代理的框是它们的视野。这些作为环境中的状态以平铺网格的形式提供给它们,类似于我们在系统中所做的。

像这样的游戏以及其他许多应用,可以通过对你在这里制作的内容进行简单修改来实现。

但要记住,深度 Q 学习仅适用于离散的动作空间——即具有有限数量的不同动作的空间。对于连续的动作空间,如在基于物理的环境中,你需要探索 DRL 世界中的其他方法。

10. 附加:优化状态表示

不管你信不信,我们目前表示状态的方式并不是最优的。

实际上,这种方法非常低效。

对于 100x100 的网格,有 99,990,000 种可能的状态。考虑到输入的规模——10,000 个值,模型不仅需要非常大,还需要大量的训练数据。根据可用的计算资源,这可能需要几天或几周。

另一个缺点是灵活性。模型目前被固定在一个网格大小。如果你想使用不同大小的网格,你需要从头训练另一个模型。

我们需要一种表示状态的方法,这种方法能显著减少状态空间,并且适用于任何网格大小。

更好的方法 尽管有几种方法可以做到这一点,最简单且可能最有效的方法是使用相对于目标的距离。

而不是像这样表示 5x5 网格的状态:

[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0]

它可以用两个值来表示:

[-2, -1]

使用这种方法可以将 100x100 网格的状态空间从 99,990,000 减少到 39,601!

不仅如此,它的泛化能力也更强。它只需学会当第一个值为负时,向下移动是正确的选择,而当第二个值为负时,向右移动是合适的选择,正值的情况则相反。

这使得模型只能探索状态空间的一部分。

以目标为中心的 25x25 代理决策热图——作者制作的 GIF

上图展示了在 25x25 网格上训练模型的学习进程。它展示了智能体在每个格子上的选择,颜色编码表示,目标位于中央。

起初,在探索阶段,智能体的策略完全不对。你可以看到它在目标上方时选择向上移动,在目标下方时选择向下移动,等等。

但在不到 10 集的情况下,它学会了一种策略,使其能够在最少的步骤内从任何格子到达目标。

这同样适用于目标位于任何位置的情况。

模型在不同目标位置应用的四个 25x25 热图 — 图片由作者提供

最后,它的学习能力非常强。

201x201 热图展示了 25x25 模型的决策,显示了泛化能力 — 图片由作者提供

这个模型只见过 25x25 的网格,但它可以在一个更大的环境中使用其策略——201x201。如此大的环境中有 1,632,200,400 种智能体与目标的排列组合!

让我们用这种彻底的改进来更新我们的代码。

实现 幸好,我们需要做的事情并不多就能使其工作。

首先需要更新Environment中的get_state

def get_state(self):
    # Calculate row distance and column distance
    relative_distance = (self.agent_location[0] - self.goal_location[0],
                         self.agent_location[1] - self.goal_location[1])

    # Unpack tuple into numpy array
    state = np.array([*relative_distance])
    return state

与网格的展平版本不同,我们计算目标的距离,并将其作为 NumPy 数组返回。*运算符仅仅是将元组解包成单独的组件。它的效果等同于这样做——state = np.array([relative_distance[0], relative_distance[1])

同样,在move_agent中,我们可以将撞击边界的惩罚更新为与远离目标的惩罚相同。这样,当你更改网格大小时,智能体不会因移到原本训练区域之外而受到挫折。

def move_agent(self, action):
    ...
    else:
        # Same punishment for an invalid move
        reward = -1.1

    return reward, done

更新神经网络架构 目前我们的 TensorFlow 模型如下所示。为了简洁起见,我省略了其他所有内容。

class Agent:
    def __init__(self, grid_size, ...):
        self.grid_size = grid_size
        ...
        self.model = self.build_model()

    def build_model(self):
        # Create a sequential model with 3 layers
        model = Sequential([
            # Input layer expects a flattened grid, hence the input shape is grid_size squared
            Dense(128, activation='relu', input_shape=(self.grid_size**2,)),
            Dense(64, activation='relu'),
            # Output layer with 4 units for the possible actions (up, down, left, right)
            Dense(4, activation='linear')
        ])

        model.compile(optimizer='adam', loss='mse')

        return model
    ...

如果你还记得,我们的模型架构需要有一致的输入。在这种情况下,输入大小依赖于grid_size

使用我们更新的状态表示方式,无论grid_size是什么,每个状态只会有两个值。我们可以更新模型以适应这一点。同时,我们可以完全移除self.grid_size,因为Agent类不再依赖于它。

class Agent:
    def __init__(self, ...):
        ...
        self.model = self.build_model()

    def build_model(self):
        # Create a sequential model with 3 layers
        model = Sequential([
            # Input layer expects a flattened grid, hence the input shape is grid_size squared
            Dense(64, activation='relu', input_shape=(2,)),
            Dense(32, activation='relu'),
            # Output layer with 4 units for the possible actions (up, down, left, right)
            Dense(4, activation='linear')
        ])

        model.compile(optimizer='adam', loss='mse')

        return model
    ...

input_shape参数期望一个表示输入状态的元组。

(2,)表示一个具有两个值的一维数组。看起来像这样:

[-2, 0]

(2,1),例如,一个二维数组,表示两行一列。看起来像这样:

[[-2],
 [0]]

最后,我们将隐藏层中的神经元数量分别降低到 64 和 32。尽管这种简单的状态表示方式仍然可能有些过度,但运行速度应该足够快。

当你开始训练时,尝试看看模型有效学习所需的最少神经元数量。如果愿意,你甚至可以尝试移除第二层。

修复主要训练循环 训练循环需要很少的调整。让我们更新它以匹配我们的更改。

from environment import Environment
from agent import Agent
from experience_replay import ExperienceReplay
import time

if __name__ == '__main__':

    grid_size = 5

    environment = Environment(grid_size=grid_size, render_on=True)
    agent = Agent(epsilon=1, epsilon_decay=0.998, epsilon_end=0.01)
    # agent.load(f'models/model.h5')

    experience_replay = ExperienceReplay(capacity=10000, batch_size=32)

    # Number of episodes to run before training stops
    episodes = 5000
    # Max number of steps in each episode
    max_steps = 200

    for episode in range(episodes):

        # Get the initial state of the environment and set done to False
        state = environment.reset()

        # Loop until the episode finishes
        for step in range(max_steps):
            print('Episode:', episode)
            print('Step:', step)
            print('Epsilon:', agent.epsilon)

            # Get the action choice from the agents policy
            action = agent.get_action(state)

            # Take a step in the environment and save the experience
            reward, next_state, done = environment.step(action)
            experience_replay.add_experience(state, action, reward, next_state, done)

            # If the experience replay has enough memory to provide a sample, train the agent
            if experience_replay.can_provide_sample():
                experiences = experience_replay.sample_batch()
                agent.learn(experiences)

            # Set the state to the next_state
            state = next_state

            if done:
                break

            # Optionally, pause for half a second to evaluate the model
            # time.sleep(0.5)

        agent.save(f'models/model.h5')

因为agent不再需要grid_size,我们可以移除它以防止任何错误。

我们也不再需要为每个grid_size给模型不同的名称,因为一个模型现在适用于任何大小。

如果你对ExperienceReplay感兴趣,它将保持不变。

请注意,没有一种适合所有情况的状态表示。在某些情况下,像我们这样提供完整的网格,或者像我在第九部分中做的那样提供部分网格是有意义的。目标是找到简化状态空间和提供足够信息之间的平衡,以便代理能够学习。

超参数 即使像我们这样简单的环境也需要调整超参数。记住,这些是我们可以更改的值,影响训练过程。

我们讨论的每一个都包括:

  • epsilon, epsilon_decay, epsilon_end(探索/利用)

  • gamma(折扣因子)

  • 神经元数量和层数

  • batch_size, capacity(经验回放)

  • max_steps

还有很多其他的,但我们将讨论的还有一个对于学习至关重要。

学习率 学习率(LR)是神经网络模型的一个超参数。

它基本上告诉神经网络每次拟合数据时调整其权重——用于输入转换的值——的程度。

学习率的值通常范围从 1 到 0.0000001,其中最常见的值是 0.01、0.001 和 0.0001。

次优学习率可能永远无法收敛到最优策略——作者提供的图像

如果学习率过低,可能无法足够快地更新 Q 值以学习最优策略,这个过程称为收敛。如果你注意到学习似乎停滞不前,或者完全没有,这可能是学习率不够高的一个迹象。

虽然这些关于学习率的图示大大简化了,但它们应该传达了基本的概念。

次优学习率导致 Q 值持续指数增长——作者提供的图像

另一方面,学习率过高可能导致值“爆炸”或变得越来越大。模型的调整过大,导致它发散——或者随着时间推移变得更差。

什么是完美的学习率? 一根绳子有多长?

在许多情况下,你只需使用简单的试错法。确定学习率是否是问题的好方法是检查模型的输出。

这正是我在训练这个模型时遇到的问题。切换到简化的状态表示后,它拒绝学习。代理实际上在广泛测试每个超参数后继续移动到网格的右下角。

这让我感到不解,所以我决定查看Agent get_action方法中模型输出的 Q 值。

Step 10
[[ 0.29763165 0.28393078 -0.01633328 -0.45749056]]

Step 50
[[ 7.173178 6.3558702 -0.48632553 -3.1968129 ]]

Step 100
[[ 33.015953 32.89661 33.11674 -14.883122]]

Step 200
[[573.52844 590.95685 592.3647 531.27576]]

...

Step 5000
[[37862352\. 34156752\. 35527612\. 37821140.]]

这是一个值爆炸的示例。

在 TensorFlow 中,我们用来调整权重的优化器 Adam,其默认学习率为 0.001。对于这种特定情况,这个值显然太高了。

平衡学习率,最终收敛到最佳策略——作者提供的图像

在测试了各种值之后,最佳点似乎是 0.00001。

让我们来实现这个。

from tensorflow.keras.optimizers import Adam

def build_model(self):
    # Create a sequential model with 3 layers
    model = Sequential([
        # Input layer expects a flattened grid, hence the input shape is grid_size squared
        Dense(64, activation='relu', input_shape=(2,)),
        Dense(32, activation='relu'),
        # Output layer with 4 units for the possible actions (up, down, left, right)
        Dense(4, activation='linear')
    ])

    # Update learning rate
    optimizer = Adam(learning_rate=0.00001)

    # Compile the model with the custom optimizer
    model.compile(optimizer=optimizer, loss='mse')

    return model

随意调整这些设置,观察 Q 值的变化。同时,确保导入 Adam。

最后,你可以再次开始训练!

热图代码 如果你感兴趣,下面是绘制你自己热图的代码,正如之前所示。

import matplotlib.pyplot as plt
import numpy as np
from tensorflow.keras.models import load_model

def generate_heatmap(episode, grid_size, model_path):
    # Load the model
    model = load_model(model_path)

    goal_location = (grid_size // 2, grid_size // 2)  # Center of the grid

    # Initialize an array to store the color intensities
    heatmap_data = np.zeros((grid_size, grid_size, 3))

    # Define colors for each action
    colors = {
        0: np.array([0, 0, 1]),  # Blue for up
        1: np.array([1, 0, 0]),  # Red for down
        2: np.array([0, 1, 0]),  # Green for left
        3: np.array([1, 1, 0])   # Yellow for right
    }

    # Calculate Q-values for each state and determine the color intensity
    for x in range(grid_size):
        for y in range(grid_size):
            relative_distance = (x - goal_location[0], y - goal_location[1])
            state = np.array([*relative_distance]).reshape(1, -1)
            q_values = model.predict(state)
            best_action = np.argmax(q_values)
            if (x, y) == goal_location:
                heatmap_data[x, y] = np.array([1, 1, 1])
            else:
                heatmap_data[x, y] = colors[best_action]

    # Plotting the heatmap
    plt.imshow(heatmap_data, interpolation='nearest')
    plt.xlabel(f'Episode: {episode}')
    plt.axis('off')
    plt.tight_layout(pad=0)
    plt.savefig(f'./figures/heatmap_{grid_size}_{episode}', bbox_inches='tight')

只需将其导入到你的训练循环中,并根据需要运行。

下一步 一旦你有效地训练了你的模型并尝试了各种超参数,我鼓励你真正把它做成你自己的。

扩展系统的一些想法:

  • 在代理和目标之间添加障碍

  • 创建一个更为多样的环境,可能包括随机生成的房间和通道

  • 实现一个多代理合作/竞争系统——捉迷藏

  • 创建一个受乒乓球启发的游戏

  • 实现资源管理,如饥饿或能量系统,其中代理需要在前往目标的途中收集食物

这是一个超越我们简单网格系统的示例:

Flappy Bird 风格的游戏,代理必须避开管道才能生存——作者提供的 GIF

使用Pygame,一个流行的 Python 2D 游戏库,我构建了一个 Flappy Bird 克隆。然后,我在我们预构建的Environment类中定义了交互、约束和奖励结构。

我将状态表示为代理的当前速度和位置、与最近管道的距离,以及开口的位置。

对于Agent类,我只是将输入大小更新为(4,),增加了神经网络的层数,并更新了网络以仅输出两个值——跳跃或不跳跃。

你可以在 GitHub repoflappy_bird目录中找到并运行这些内容。确保pip install pygame

这表明你所构建的系统适用于各种环境。你甚至可以让代理探索三维环境或执行更抽象的任务,如股票交易。

在扩展你的系统时,不要害怕在环境、状态表示和奖励系统上进行创新。就像代理一样,我们也通过探索学习得最好!

我希望从零开始构建 DRL 健身房让你领悟到了 AI 的美妙,并激励你深入探索。

这篇文章的灵感来源于 《Python 从零开始的神经网络》 youtube 系列 由 Harrison Kinsley (sentdex) 和 Daniel Kukieł 主讲。对话式风格和从零开始的代码实现真正巩固了我对神经网络的理解。