TowardsDataScience 2023 博客中文翻译(六十七)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

强化学习能否超越训练泛化?

原文:towardsdatascience.com/can-reinforcement-learning-generalize-beyond-its-training-3b9012d8e4cf?source=collection_archive---------20-----------------------#2023-01-24

模型泛化案例研究

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 John Morrow

·

关注 发表在 Towards Data Science ·6 min read·2023 年 1 月 24 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

János Szüdi拍摄于Unsplash

论文中详细描述的项目,强化学习:模型泛化案例研究,探讨了一个使用强化学习(RL)训练的模型在泛化能力上的表现,即当遇到训练中未接触的数据时,能够产生可接受的结果。本研究中的应用是一个具有多个控制的工业过程,这些控制决定了产品在过程中的过渡效果。在这种环境中确定最佳控制设置可能具有挑战性。例如,当控制之间存在相互作用时,调整一个设置可能需要重新调整其他设置。此外,控制与其效果之间的复杂关系使得找到最佳解决方案变得更加困难。这里展示的结果表明,经过 RL 过程训练的模型在这种环境下表现良好,并能够泛化到不同于训练条件的情况下。

论文描述了一种 RL 模型,该模型被训练以寻找用于将电子组件焊接到电路板上的回流焊炉的最佳控制设置(图 1)。烤箱的移动传送带将产品(即电路板)运输通过多个加热区域。此过程根据所需的温度-时间目标曲线加热产品,以生产可靠的焊接连接。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1:烤箱传送带上的电路板图像来自 Adobe,授权给 John Morrow

人工操作员通常采取以下步骤来确定成功焊接电路板所需的加热器设置:

• 运行一次产品通过烤箱的过程

• 观察传感器读数中的温度-时间曲线

• 调整加热器设置以改善曲线,接近目标曲线

• 等待烤箱温度稳定到新的设置

• 重复此过程,直到传感器读数中的曲线足够接近目标曲线

学习策略

一个 RL 系统用两阶段过程取代了操作员步骤。在第一阶段,代理学习烤箱的动态,并在各种烤箱条件下创建更新加热器设置的策略。

由于在改变加热器设置并将产品通过烤箱后,需要相当长的时间来稳定烤箱的温度,因此使用了烤箱模拟器以加速学习过程。模拟器在几秒钟内模拟了产品通过加热曲线的单次过程,而物理烤箱则需要许多分钟。(第二部分提供了模拟器的详细信息。)

在每一轮学习阶段中,代理从其当前状态采取行动,向模拟器发送八个加热器的新设置。模拟运行后,模拟器报告产品温度读数(每秒采集三百个读数)。

代理的奖励基于返回读数与目标温度-时间曲线之间的差异。如果当前运行的差异小于之前的差异,则奖励为正;否则,奖励为负。一部分读数决定系统的新状态。代理通过从新状态中采取行动来开始学习阶段的下一轮。

使用策略进行规划

在第二阶段,代理按照学习到的策略寻找最佳加热器设置。这些设置将使实际产品曲线与目标温度-时间曲线之间的匹配最接近。图 2 展示了代理遵循策略找到最佳设置的结果。蓝色轨迹是目标温度-时间曲线,红色轨迹是由最佳设置产生的实际曲线。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2:示例规划结果 蓝色轨迹:目标曲线。红色轨迹:实际产品曲线。

强化学习系统

如上所述,RL 系统包括一个代理在环境中采取行动,以学习一个策略来实现目标。环境对每个行动作出回应,提供奖励以指示行动是否有助于实现目标。环境还会返回代理在环境中的状态。代理由两个神经网络组成:模型网络和目标网络。代理的目标是找到加热器设置,使得生产的产品时间-温度曲线与目标曲线非常接近。环境是回流炉模拟器。图 3 显示了 RL 系统的组成部分,文中对每个部分进行了详细描述。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3:强化学习系统

泛化:状态和奖励定义

状态和奖励定义对于 RL 模型在目标曲线和产品参数与训练期间使用的参数不同的新环境中的泛化能力至关重要。具体来说,状态和奖励都是根据产品和目标曲线温度之间的相对差异定义的,并通过允许的加热器值的最大范围进行规范化。

状态参数在八个加热器区域的中心定义。每个状态参数被定义为产品中心温度与每个加热器区域中心温度的规范化差异。

当代理执行一个动作时,环境会返回一个奖励,表明该动作在实现代理目标方面的有效性。奖励是基于该动作是否减少了实际温度与目标曲线之间的总温差。状态和奖励函数在论文中有更详细的描述。

结果

以下是论文中在不同产品材料配置和温度-时间曲线下运行规划过程的两个测试结果。所有测试都使用了一个模型神经网络,该网络经过以下产品和曲线参数的训练:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

测试 1:基准测试

测试 1 作为测试模型性能的基准,使用与训练模型时相同的参数。以下是测试 1 的误差、最佳热区设置以及目标曲线与实际温度-时间图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

测试 1:蓝色曲线:目标曲线。红色曲线:实际产品曲线。

测试 6

测试 6 将产品从 FR4 更改为氧化铝(铝土矿 99%),更改了产品的尺寸,并且更改了曲线。烤箱参数值与测试 1 的基准相同,只是顶部和底部加热元件都处于活动状态。以下表格反映了该测试所用的曲线和产品参数(相对于基准训练参数的更改为粗体):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

以下是测试 6 的误差、最佳热区设置以及目标曲线与实际温度/时间图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

结论

本项目展示了强化学习系统如何为控制复杂的工业过程提供解决方案。具体来说,强化学习系统成功学习了用于将电子组件焊接到电路板上的回流焊炉的最佳控制设置。此外,一旦训练完成,系统可以泛化到在与训练时不同要求的环境中产生可接受的结果。

论文链接:强化学习:模型泛化的案例研究

除非另有说明,所有图片均为作者所用。

合成数据能提升机器学习性能吗?

原文:towardsdatascience.com/can-synthetic-data-boost-machine-learning-performance-6b4041e75dda

研究合成数据在不平衡数据集上提高模型性能的能力

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 John Adeojo

·发表于Towards Data Science ·阅读时间 7 分钟·2023 年 7 月 5 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供:由 Midjourney 生成

背景——不平衡数据集

在商业机器学习应用中,数据不平衡分类问题经常发生。你可能会在客户流失预测、欺诈检测、医疗诊断或垃圾邮件检测中遇到它们。在所有这些情况下,我们要检测的对象都属于少数类,而这些少数类在数据中可能严重不足。为提高模型在不平衡数据集上的表现,提出了几种方法:

  • 欠采样:通过随机欠采样多数类来实现更平衡的训练数据集。

  • 过采样:通过随机过采样少数类来获得平衡的训练数据集。

  • 加权损失:根据少数类为损失函数分配权重。

  • 合成数据:使用生成式 AI 创建高保真度的少数类合成数据样本。

在这篇文章中,我展示了如何通过在合成数据上训练模型来超越其他方法,从而提高分类器的性能。

数据集

数据来自Kaggle,包括 284,807 笔信用卡交易,其中 492 笔(0.172%)被标记为欺诈交易。数据可用于商业和非商业用途,采用开放数据公共许可证

对感兴趣的读者,Kaggle 提供了有关数据的更多详细信息和基本描述性统计

从这个 Kaggle 数据集中,我创建了两个子集:一个训练集和一个持出集。训练集包含总数据的 80%,以及在探索该方法时生成的合成样本。持出集则包含原始数据的 20%,不包括任何合成样本。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像:数据拆分过程

模型

我使用了Ludwig,这是一个开源的声明式框架,用于构建深度学习模型,因为它易于实现。通过在 yaml 文件中声明模型并通过 Ludwig 的 Python API 运行训练任务,可以轻松构建和训练模型。我之前写过一篇文章,详细介绍了 Ludwig,供感兴趣的读者参考。

对于每种方法,我使用相同的基线模型,仅根据需要调整特定参数。例如,Ludwig 原生支持权重和采样调整——这些可以简单地在 yaml 文件中进行调整。我提供了每种方法的模型配置 yaml 文件的链接,供您探索。

  • 基线模型 — 链接

  • 加权损失模型 — 链接

  • 欠采样模型 — 链接

  • 过采样模型 — 链接

  • 合成数据 — 使用与基线相同的模型,因为类别是平衡的。

生成合成数据

我使用了合成数据库(SDV),这是一个用于生成合成数据样本的开源库。使用 SDV,我生成了额外的 284k 合成欺诈样本,从而在训练数据集中实现了两个类别的均等表示。

合成样本是通过适用于表格数据的变分自编码器(TVAE)生成的。有关 TVAE 背后的理论,您可以在这篇论文中找到更多细节。

SDV提供了诊断统计数据,显示拟合质量的指示。您可以通过比较真实数据与生成数据中的变量分布,手动探索拟合质量,示例如下。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像:真实与合成的变量 v1 分布对比

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像:真实与合成的变量 v10 分布对比

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像:真实与合成的变量分布对比

使用精确度召回图表评估性能

我们通过绘制模型与持出数据集的精确度与召回率曲线来评估每个模型的性能。

精确度-召回率曲线

精确度-召回率曲线,即将精确度(在 y 轴上)与召回率(在 x 轴上)进行绘制的图,与 ROC 曲线类似。它作为一种强健的诊断工具,用于评估模型在显著类别不平衡场景中的性能,例如我们的信用卡欺诈检测用例,便是一个典型例子。

图表的右上角代表“理想”点 —— 假阳性率为零,真正阳性率为一。一个熟练的模型应该能够达到或接近这一点,这意味着曲线下面积(AUC-PR)较大的模型可能更优越。

无技能预测器

“无技能”预测器是一个简单的模型,其预测是随机的。对于不平衡数据集,无技能线是一个高度等于正类比例的水平线。这是因为如果模型随机预测正类,精确度将等于数据集中正实例的比例。

模型性能 — 基线

基线模型是没有样本调整、损失函数调整或增强训练数据的深度神经网络。每种方法与基线性能进行比较,基线性能作为性能基准。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像:基线模型的精确度-召回率曲线

模型性能 — 加权损失方法

加权损失根据欺诈交易与非欺诈交易的比例调整损失函数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像:加权损失方法的精确度-召回率曲线

模型性能 — 过采样方法

过采样随机地过度采样欺诈交易,直到训练数据集中各类别之间的表示均等。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像:过采样方法的精确度-召回率曲线

模型性能 — 欠采样方法

欠采样随机地欠采样非欺诈交易,直到训练数据集中各类别之间的表示均等。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像:欠采样方法的精确度-召回率曲线

模型性能 — 人工合成数据方法

利用 TVAEs 生成 284k 人工合成的欺诈样本,以在训练数据集中获得各类别的均等表示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像:人工合成数据方法的精确度-召回率曲线

自助法持出数据集

为了获得对保留集性能的稳健视角,我从原始数据中创建了五十个自举保留集。对每种方法关联的模型在所有集上运行,提供了性能分布。然后,我们可以使用 Kolmogorov-Smirnov 检验来确定每种方法是否与基线存在统计显著差异。

加权:加权方法在召回率和 AUC 方面相对于基线表现略逊。除此之外,各性能指标的方差相对于其他方法显得较高。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像:模型性能指标在 50 个自举保留样本上的表现。基线与加权损失,KS 统计 — AUC 0.420 p 值 < 0.000,精度 0.260 p 值 0.068,召回率 0.520 p 值 < 0.000

过采样:过采样方法提高了模型的召回率,但导致精度的急剧恶化。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像:模型性能指标在 50 个自举保留样本上的表现。基线与过采样,KS 统计 — AUC 0.160 p 值 0.549,精度 1.0 p 值 < 0.000,召回率 0.9 p 值 < 0.000

欠采样:该方法在所有指标上表现都不如基线。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像:模型性能指标在 50 个自举保留样本上的表现。基线与过采样,KS 统计 — AUC 0.880 p 值 < 0.000,精度 0.6 p 值 < 0.000,召回率 1.0 p 值 < 0.000

合成:合成方法提升了模型的召回率,尽管以牺牲精度为代价。尽管精度的影响仍然显著,但与过采样方法相比,合成方法提供了更具韧性的替代方案,能够在不显著影响精度的情况下提升模型召回率。合成方法的稳健性在 AUC-PR 的提升中得到了进一步证明。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像:模型性能指标在 50 个自举保留样本上的表现。基线与合成,KS 统计 — AUC 0.620,精度 0.560,召回率 0.360 所有 p 值 ≤ 0.003

结论

我们注意到,相对于基线,合成数据方法可以提升模型的召回率,但以牺牲精度为代价。过采样也能实现类似的结果,但模型精度相比之下急剧下降。

在我们特定的信用卡欺诈检测背景下,假阳性不像假阴性那样昂贵。因此,如果提高召回率能够显著提高,我们可以在模型精度上做出一定妥协。通过合成实例丰富我们的训练数据似乎是提高召回率同时减轻精度不良影响的有效策略。这种增强可能会显著影响盈利能力,特别是在将模型扩展到处理数百万笔交易时。最终,将假阳性和假阴性的确切成本进行归因,将使我们更清楚地理解最具商业可行性的方法,这一话题超出了本文的范围。

检查不同样本规模的合成数据的表现将非常有趣,也许可以与加权损失结合起来。类似地,尝试不同的过采样比例可能会产生与我们观察到的合成方法类似的效果。

这个项目的笔记本可以在我的 GitHub repo 中找到

LinkedIn 上关注我

订阅 Medium 以获取更多来自我的见解:

[## 使用我的推荐链接加入 Medium — John Adeojo

我分享数据科学项目、经验和专业知识,以帮助你在旅程中。你可以通过…

johnadeojo.medium.com](https://johnadeojo.medium.com/membership?source=post_page-----6b4041e75dda--------------------------------)

如果你有兴趣将 AI 或数据科学整合到你的业务操作中,我们邀请你预约与我们进行免费的初步咨询:

[## 在线预约 | 数据驱动解决方案

通过免费咨询发现我们在帮助企业实现雄心勃勃目标方面的专业知识。我们的数据科学家和…

www.data-centric-solutions.com](https://www.data-centric-solutions.com/book-online?source=post_page-----6b4041e75dda--------------------------------)

变换器能否学会制定策略?

原文:towardsdatascience.com/can-transformers-learn-to-strategize-862770c996ea

TicTacGPT 用于玩简单的棋盘游戏

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Charlie O’Neill

·发布于 Towards Data Science ·27 分钟阅读·2023 年 9 月 8 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Jon Tyson 提供,来源于 Unsplash

尽管大多数棋盘游戏倾向于使用卷积神经网络或其他几何灵感的架构,但我们实际能够将棋盘状态表示为字符串,这就引出了一个问题,即变换器是否可以自然地应用于游戏。在这里,我们将看看是否可以在简单的井字游戏的背景下回答这个问题。虽然这看起来可能不太实际(几乎每个人都知道这个游戏中存在一个简单的闭式纳什均衡策略),但它是我们问题的一个有用的测试平台。原因在于游戏足够简单,我们可以轻松训练一个变换器来玩它,但又足够复杂,不容易立刻看出最佳策略是什么。

实现游戏

我们将开始实现一个TicTacToe类。这相当简单。我们希望能够将棋盘表示为 9 个字符的字符串,每个字符代表一个方格。我们将使用X表示第一个玩家,O表示第二个玩家,-表示空方格。我们还会跟踪轮到谁进行下一步,游戏是否结束。如果有获胜者,我们也会记录下来。最后,我们将包含一个打印棋盘的方法,以便在调试时不必盯着字符串看。

class TicTacToe:
    def __init__(self):
        # Initialise an empty board
        self.board = ['-' for _ in range(9)]
        self.current_player = 'X'  # X will start

    def make_move(self, position):
        """Make a move on the board."""
        if self.board[position] == '-':
            self.board[position] = self.current_player
            self.switch_player()
            return True
        else: return False  # illegal move

    def switch_player(self):
        """Switch the current player."""
        self.current_player = 'O' if self.current_player == 'X' else 'X'

    def check_winner(self):
        """Check if there is a winner."""
        # Rows, columns, diagonals
        winning_positions = [
            [0, 1, 2], [3, 4, 5], [6, 7, 8],  # Rows
            [0, 3, 6], [1, 4, 7], [2, 5, 8],  # Columns
            [0, 4, 8], [2, 4, 6]  # Diagonals
        ]

        for positions in winning_positions:
            values = [self.board[pos] for pos in positions]
            if values[0] == values[1] == values[2] and values[0] != '-':
                return values[0]

        return None  # No winner yet

    def is_draw(self):
        """Check if the game is a draw."""
        return all(cell != '-' for cell in self.board)

    def get_board_string(self):
        """Get the current board state as a string."""
        return ''.join(self.board)

    def get_legal_moves(self):
        """Get the positions of all legal moves."""
        return [i for i, cell in enumerate(self.board) if cell == '-']

    def pretty_print_board(self):
        """Pretty-print the board."""
        for i in range(0, 9, 3):
            print(f"{self.board[i]} | {self.board[i+1]} | {self.board[i+2]}")
            if i < 6:
                print("- "*5)

# Test the pretty_print_board method
tic_tac_toe = TicTacToe()
print("Initial board:")
tic_tac_toe.pretty_print_board()

# Make some moves
tic_tac_toe.make_move(0)
tic_tac_toe.make_move(4)
tic_tac_toe.make_move(8)
print("\nBoard after some moves:")
tic_tac_toe.pretty_print_board()

Initial board:
- | - | -
- - - - - 
- | - | -
- - - - - 
- | - | -

Board after some moves:
X | - | -
- - - - - 
- | O | -
- - - - - 
- | - | X

创建我们的训练数据

我们希望我们的变换器能够输入一个给定的棋盘状态,并输出一个走法,该走法是一个从 0 到 8 的整数,表示它希望将棋子放在的位置。为此,我们将创建一个棋盘状态和走法的数据集。我们将通过模拟我们玩家的所有可能获胜位置,然后遍历所有可能使我们达到该位置的游戏组合来做到这一点。这意味着变换器将学习在任何给定的棋盘状态下什么是一个好的走法。

为了实现这一点,simulate_all_games 函数生成了训练和验证数据。具体来说,该函数模拟了所有可能的井字棋游戏,探索了两个玩家(‘X’ 和 ‘O’)的每一种走法排列。这种详尽的模拟确保了模型在一个全面的数据集上进行训练,涵盖了所有可能的游戏情景。在每个模拟的游戏中,函数记录了不仅是获胜或平局的结果,还记录了棋盘状态的序列以及导致这些结果的走法。这些棋盘状态和走法随后被转换为数字表示,适用于训练我们的变换器。这确保了模型不仅学习如何获胜,还能够从任何给定的棋盘状态中输出一个合适的获胜走法。你可以将其视为类似于常规语言变换器在给定任何长度的上下文时输出一个合适的标记,从一个标记(即我们的起始棋盘状态)到 EOS 标记(即我们的获胜走法生成最终棋盘状态)。

from copy import deepcopy
from itertools import product
import numpy as np
import torch

# Define character to integer mapping
chars = sorted(list(set('XO-')))
vocab_size = len(chars)
stoi = {ch: i for i, ch in enumerate(chars)}
itos = {0: '-', 1: 'X', 2: 'O'}
encode = lambda s: [stoi[c] for c in s]
decode = lambda l: ''.join([itos[i] for i in l])

input_sequences = []
output_sequences = []

# Function to simulate all possible games recursively
def simulate_all_games(game, x_moves, o_moves):
    global input_sequences, output_sequences

    # Check if the game has reached a terminal state
    winner = game.check_winner()
    if winner == 'X' or game.is_draw():
        # Add the sequence of board states and moves leading to this win for 'X' or draw
        board = ['-' for _ in range(9)]
        for i, x_move in enumerate(x_moves):
            input_sequences.append(encode(''.join(board)))
            output_sequences.append(x_move)
            board[x_move] = 'X'
            if i < len(o_moves):
                board[o_moves[i]] = 'O'
        return
    elif winner == 'O':
        return  # We don't add these to our training data

    # Otherwise, continue simulating the game
    legal_moves = game.get_legal_moves()
    for move in legal_moves:
        # Create a copy of the game to simulate the move
        new_game = deepcopy(game)
        was_legal = new_game.make_move(move)

        # If the move was legal, continue simulating
        if was_legal:
            if new_game.current_player == 'X':
                simulate_all_games(new_game, x_moves + [move], o_moves)
            else:
                simulate_all_games(new_game, x_moves, o_moves + [move])

# Create an initial empty game
initial_game = TicTacToe()

# Simulate all possible games starting with 'X'
simulate_all_games(initial_game, [], [])

# Convert to PyTorch tensors
input_tensor = torch.tensor(input_sequences, dtype=torch.long)
output_tensor = torch.tensor(output_sequences, dtype=torch.long)

# Show some sample input-output pairs
print(input_tensor[:10], output_tensor[:10])

print("Number of input-output pairs:", len(input_sequences))

tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 2, 0, 0, 0, 0, 0, 0, 0],
        [1, 2, 1, 2, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 2, 0, 0, 0, 0, 0, 0, 0],
        [1, 2, 1, 2, 0, 0, 0, 0, 0],
        [1, 2, 1, 2, 1, 2, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 2, 0, 0, 0, 0, 0, 0, 0],
        [1, 2, 1, 2, 0, 0, 0, 0, 0]]) 
tensor([1, 3, 5, 1, 3, 5, 6, 1, 3, 5])

Number of input-output pairs: 658224

这给我们大约 650,000 个张量用于训练。这些张量看起来大致正确,但没有可视化的棋盘很难判断。让我们重用我们的 print_board 函数来查看一些随机的棋盘状态,以及给定走法后的下一个棋盘状态是什么样的:

def pretty_print_board(board: str):
    """Pretty-print the board."""
    for i in range(0, 9, 3):
        print(f"{board[i]} | {board[i+1]} | {board[i+2]}")
        if i < 6:
            print("- "*5)

rand_idx = torch.randint(len(input_tensor), (1,))[0]
random_game = input_tensor[rand_idx].tolist()
print("Current game state:")
decoded_game = decode(random_game)
pretty_print_board(decoded_game)
print( )

move = output_tensor[rand_idx].item()
decoded_game = decoded_game[:move] + 'X' + decoded_game[move+1:]
print("New game state:")
pretty_print_board(decoded_game)

Current game state:
X | - | -
- - - - - 
- | X | O
- - - - - 
- | O | -

New game state:
X | - | X
- - - - - 
- | X | O
- - - - - 
- | O | -

这似乎是合理的,但我注意到有些游戏有一个可用的获胜走法,但模拟却做出了不同的走法(仍然以胜利告终)。在上面的示例中出现了这种情况。让我们将 simulate_all_games 函数更改为在找到至少一个潜在获胜走法时停止搜索。

input_sequences = []
output_sequences = []

def simulate_all_games(game, x_moves, o_moves):
    global input_sequences, output_sequences

    # Check if the game has reached a terminal state
    winner = game.check_winner()
    if winner == 'X' or game.is_draw():
        # Add the sequence of board states and moves leading to this win for 'X' or draw
        board = ['-' for _ in range(9)]
        for i, x_move in enumerate(x_moves):
            input_sequences.append(encode(''.join(board)))
            output_sequences.append(x_move)
            board[x_move] = 'X'
            if i < len(o_moves):
                board[o_moves[i]] = 'O'
        return
    elif winner == 'O':
        return  # We don't add these to our training data

    # Before simulating further moves, check if a winning move is available
    legal_moves = game.get_legal_moves()
    for move in legal_moves:
        test_game = deepcopy(game)
        test_game.make_move(move)
        if test_game.check_winner() == game.current_player:
            # This move is a winning move, so we make it and end further simulation
            if test_game.current_player == 'X':
                simulate_all_games(test_game, x_moves + [move], o_moves)
            else:
                simulate_all_games(test_game, x_moves, o_moves + [move])
            return  # End further exploration for this branch

    # If no immediate winning move is found, continue simulating the game
    for move in legal_moves:
        # Create a copy of the game to simulate the move
        new_game = deepcopy(game)
        was_legal = new_game.make_move(move)

        # If the move was legal, continue simulating
        if was_legal:
            if new_game.current_player == 'X':
                simulate_all_games(new_game, x_moves + [move], o_moves)
            else:
                simulate_all_games(new_game, x_moves, o_moves + [move])

# Create an initial empty game
initial_game = TicTacToe()

# Simulate all possible games starting with 'X'
simulate_all_games(initial_game, [], [])

# Convert to PyTorch tensors
input_tensor = torch.tensor(input_sequences, dtype=torch.long)
output_tensor = torch.tensor(output_sequences, dtype=torch.long)

最后,让我们看看我们需要训练多少步:

print("Number of input-output pairs:", len(input_sequences))

Number of input-output pairs: 147104

大约 150,000 个示例。这看起来是一个合理的开始。

多头注意力的变换器架构

注意力是一种机制,使模型能够在进行预测时专注于输入序列的某些部分。变压器架构使用多头自注意力,这意味着模型学习以不同的方式关注输入序列的不同部分。这很有用,因为它允许模型学习输入序列和输出序列之间的不同关系。例如,当预测输出序列中的第一个词时,它可能会学习关注输入序列中的第一个标记,但在预测输出序列中的第二个词时,则关注输入序列中的最后一个标记。这是一种强大的机制,可以使模型学习输入序列和输出序列之间的复杂关系。

但这到底是怎么工作的呢?从原始的 Attention is all you need 论文中,定义在查询矩阵 Q、键矩阵 K 和值矩阵 V 上的注意力定义为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其中我们除以 sqrt{d_k} 以确保 softmax 的方差适当。让我们分解一下实际发生了什么。假设我们有一个维度为 (B,T,C) 的输入,其中 B 是批次大小,T 是序列长度,C 是通道数。我们可以把它看作是一个包含 B 个长度为 T 的序列的批次,每个序列有 C 个通道:

import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(1337)
B,T,C = 4,8,32 # batch, time, channels
x = torch.randn(B,T,C)

然后,为了实现一个自注意力头,我们需要创建查询、键和值。实际上,这些是具有一定 head_size 的线性层,head_size 就是我们希望线性层的宽度。我们不包括偏置项,因为我们不想为注意力学习一个偏置项。

wei = q @ k.transpose(-2,-1) # (B, T, 16) @ (B, 16, T) -> (B, T, T)

但是,如果你拆解我们实际在做的事情,我们是在预测一个单词序列中的下一个词。由于我们不想作弊并使用我们尚未看到的序列部分(因为在生成过程中我们不能这样做),我们需要屏蔽掉尚未看到的序列部分。我们通过创建一个形状为 (T,T) 的掩码来实现,其中 T 是序列长度,然后将上三角的所有值设置为负无穷。这确保了 softmax 对所有掩码值为 0,因此模型不会关注这些值。

最后,我们将注意力权重与值矩阵相乘,以获得注意力层的输出。这是自注意力的单个头的输出。然后,我们可以根据需要重复此过程多次,然后将每个头的输出拼接在一起,以获得多头自注意力层的最终输出。

tril = torch.tril(torch.ones((T,T)))
wei = wei.masked_fill(tril==0, float("-inf"))
wei = F.softmax(wei, dim=-1)

v = value(x) # (B,T,16)
out = wei @ v
out.shape

这只是自注意力的一个头。为了创建多个头,我们只需多次重复这个过程,然后将每个头的输出连接起来以获得多头自注意力层的最终输出。我们还添加了残差连接,以提高我们优化这个相对深层模型的能力。对于类似代码的完整演示以及解码器仅变换器背后的机制,我强烈推荐 Andrej Karpathy 的nanoGPT 讲座

from tqdm import tqdm
import torch.nn as nn
import torch.nn.functional as F

# Hyperparameters
batch_size = 128  # How many independent sequences will we process in parallel?
block_size = 9  # The size of the tic-tac-toe board
max_iters = 10000
eval_interval = 500
learning_rate = 1e-3
device = 'mps' if torch.backends.mps.is_available() else 'cpu'
eval_iters = 100
n_embd = 32  # Reduced the embedding size
n_head = 2  # Reduced the number of heads
n_layer = 2  # Reduced the number of layers
dropout = 0.1

print(f'Training on {device}')

# Initialize random seed
torch.manual_seed(1337)

# Split into training and validation sets
n = int(0.90 * len(input_tensor))  # 90% for training
train_input = input_tensor[:n]
train_output = output_tensor[:n]
val_input = input_tensor[n:]
val_output = output_tensor[n:]

# Updated data loading function
def get_batch(split):
    input_data = train_input if split == 'train' else val_input
    output_data = train_output if split == 'train' else val_output
    # Choose index locs for batch_size sequences
    ix = torch.randint(len(input_data) - block_size + 1, (batch_size,))
    # Get the input and output sequences
    x = input_data[ix]
    y = output_data[ix]
    x, y = x.to(device), y.to(device)
    return x, y

@torch.no_grad()
def estimate_loss():
    out = {}
    model.eval()
    for split in ['train', 'val']:
        losses = torch.zeros(eval_iters)
        for k in range(eval_iters):
            X, Y = get_batch(split)
            logits, loss = model(X, Y)
            losses[k] = loss.item()
        out[split] = losses.mean()
    model.train()
    return out

class Head(nn.Module):
    """ one head of self-attention """

    def __init__(self, head_size):
        super().__init__()
        self.key = nn.Linear(n_embd, head_size, bias=False)
        self.query = nn.Linear(n_embd, head_size, bias=False)
        self.value = nn.Linear(n_embd, head_size, bias=False)
        self.register_buffer('tril', torch.tril(torch.ones(block_size, block_size)))

        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        # input of size (batch, time-step, channels)
        # output of size (batch, time-step, head size)
        B,T,C = x.shape
        k = self.key(x)   # (B,T,hs)
        q = self.query(x) # (B,T,hs)
        # compute attention scores ("affinities")
        wei = q @ k.transpose(-2,-1) * k.shape[-1]**-0.5 # (B, T, hs) @ (B, hs, T) -> (B, T, T)
        wei = wei.masked_fill(self.tril[:T, :T] == 0, float('-inf')) # (B, T, T)
        wei = F.softmax(wei, dim=-1) # (B, T, T)
        wei = self.dropout(wei)
        # perform the weighted aggregation of the values
        v = self.value(x) # (B,T,hs)
        out = wei @ v # (B, T, T) @ (B, T, hs) -> (B, T, hs)
        return out

class MultiHeadAttention(nn.Module):
    """ multiple heads of self-attention in parallel """

    def __init__(self, num_heads, head_size):
        super().__init__()
        self.heads = nn.ModuleList([Head(head_size) for _ in range(num_heads)])
        self.proj = nn.Linear(head_size * num_heads, n_embd)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        out = torch.cat([h(x) for h in self.heads], dim=-1)
        out = self.dropout(self.proj(out))
        return out

class FeedFoward(nn.Module):
    """ a simple linear layer followed by a non-linearity """

    def __init__(self, n_embd):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(n_embd, 4 * n_embd),
            nn.ReLU(),
            nn.Linear(4 * n_embd, n_embd),
            nn.Dropout(dropout),
        )

    def forward(self, x):
        return self.net(x)

class Block(nn.Module):
    """ Transformer block: communication followed by computation """

    def __init__(self, n_embd, n_head):
        # n_embd: embedding dimension, n_head: the number of heads we'd like
        super().__init__()
        head_size = n_embd // n_head
        self.sa = MultiHeadAttention(n_head, head_size)
        self.ffwd = FeedFoward(n_embd)
        self.ln1 = nn.LayerNorm(n_embd)
        self.ln2 = nn.LayerNorm(n_embd)

    def forward(self, x):
        x = x + self.sa(self.ln1(x))
        x = x + self.ffwd(self.ln2(x))
        return x

class Transformer(nn.Module):

    def __init__(self):
        super().__init__()
        # each token directly reads off the logits for the next token from a lookup table
        self.token_embedding_table = nn.Embedding(vocab_size, n_embd)
        self.position_embedding_table = nn.Embedding(block_size, n_embd)
        self.blocks = nn.Sequential(*[Block(n_embd, n_head=n_head) for _ in range(n_layer)])
        self.ln_f = nn.LayerNorm(n_embd) # final layer norm
        self.lm_head = nn.Linear(n_embd, 9)

        # better init, not covered in the original GPT video, but important, will cover in followup video
        self.apply(self._init_weights)

    def _init_weights(self, module):
        if isinstance(module, nn.Linear):
            torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
            if module.bias is not None:
                torch.nn.init.zeros_(module.bias)
        elif isinstance(module, nn.Embedding):
            torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)

    def forward(self, idx, targets=None):
        B, T = idx.shape

        # idx and targets are both (B,T) tensor of integers
        tok_emb = self.token_embedding_table(idx)  # (B,T,C)
        pos_emb = self.position_embedding_table(torch.arange(T, device=device))  # (T,C)
        x = tok_emb + pos_emb  # (B,T,C)
        x = self.blocks(x)  # (B,T,C)
        x = self.ln_f(x)  # (B,T,C)
        logits = self.lm_head(x)  # (B,T,vocab_size)
        # Take the logits corresponding to the last time step T
        logits = logits[:, -1, :]  # Now logits is (B, 9)

        if targets is None:
            loss = None
        else:
            loss = F.cross_entropy(logits, targets)

        return logits, loss

    def generate(self, idx, max_new_tokens):
        # idx is (B, T) array of indices in the current context
        for _ in range(max_new_tokens):
            # crop idx to the last block_size tokens
            idx_cond = idx[:, -block_size:]
            # get the predictions
            logits, loss = self(idx_cond)
            # focus only on the last time step
            logits = logits[:, -1, :] # becomes (B, C)
            # apply softmax to get probabilities
            probs = F.softmax(logits, dim=-1) # (B, C)
            # sample from the distribution
            idx_next = torch.multinomial(probs, num_samples=1) # (B, 1)
            # append sampled index to the running sequence
            idx = torch.cat((idx, idx_next), dim=1) # (B, T+1)
        return idx

为了确保我们的架构按预期工作,让我们尝试传递一个单一的批次。

xb, yb = get_batch('train')
print(xb.shape, yb.shape)
m = Transformer().to(device)
logits, loss = m(xb, yb)
print(logits.shape)
print(f"Loss: {loss.item():.3f}")

torch.Size([128, 9]) torch.Size([128])
torch.Size([128, 9])
Loss: 2.203

在进行这个初始前向传播时,一个好的步骤是测试损失是否大致等于我们对随机输入的期望。由于我们有 9 维 logits,并且我们使用的交叉熵损失等于正确类别的负对数似然,我们期望损失大致为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

训练模型

使用相当小的变换器(约 25,000 个参数),我们实现了以下损失(请注意,我使用了少量的权重衰减和丢弃):

model = Transformer()
model = model.to(device)

# Print the number of parameters in the model
print(sum(p.numel() for p in model.parameters()) / 1e6, 'M parameters')

optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate, weight_decay=1e-4)
train_loss_history = []
val_loss_history = []

# Training loop
for iter in tqdm(range(max_iters)):
    # Evaluate the loss on train and val sets occasionally
    if iter % eval_interval == 0 or iter == max_iters - 1:
        losses = estimate_loss()
        print(f"step {iter}: train loss {losses['train']:.4f}, val loss {losses['val']:.4f}")
        val_loss_history.append(losses['val'])

    # Sample a batch of data
    xb, yb = get_batch('train')

    # Evaluate the loss
    logits, loss = model(xb, yb)
    train_loss_history.append(loss.item())
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

0.025961 M parameters
  0%|          | 5/10000 [00:00<24:42,  6.74it/s]  
step 0: train loss 2.2033, val loss 2.2106
  5%|| 504/10000 [00:14<12:01, 13.15it/s]
step 500: train loss 1.9162, val loss 2.0215
 10%|| 1008/10000 [00:27<08:27, 17.73it/s]
step 1000: train loss 1.7846, val loss 1.8570
 15%|█▌        | 1505/10000 [00:40<10:34, 13.39it/s]
step 1500: train loss 1.7370, val loss 1.7648
 20%|██        | 2007/10000 [00:53<07:35, 17.55it/s]
step 2000: train loss 1.7188, val loss 1.7770
 25%|██▌       | 2506/10000 [01:05<07:11, 17.36it/s]
step 2500: train loss 1.6957, val loss 1.7456
 30%|███       | 3006/10000 [01:18<06:35, 17.69it/s]
step 3000: train loss 1.6965, val loss 1.7448
 35%|███▌      | 3506/10000 [01:31<06:12, 17.41it/s]
step 3500: train loss 1.6961, val loss 1.7809
 40%|████      | 4005/10000 [01:43<07:41, 12.98it/s]
step 4000: train loss 1.6819, val loss 1.7256
 45%|████▌     | 4506/10000 [01:56<05:18, 17.24it/s]
step 4500: train loss 1.6892, val loss 1.7066
 50%|█████     | 5005/10000 [02:09<05:14, 15.88it/s]
step 5000: train loss 1.6846, val loss 1.7141
 55%|█████▌    | 5508/10000 [02:23<04:37, 16.19it/s]
step 5500: train loss 1.6835, val loss 1.6998
 60%|██████    | 6004/10000 [02:36<05:19, 12.51it/s]
step 6000: train loss 1.6828, val loss 1.7095
 65%|██████▌   | 6506/10000 [02:49<03:23, 17.13it/s]
step 6500: train loss 1.6722, val loss 1.7151
 70%|███████   | 7008/10000 [03:02<03:05, 16.17it/s]
step 7000: train loss 1.6656, val loss 1.7158
 75%|███████▌  | 7505/10000 [03:15<02:30, 16.54it/s]
step 7500: train loss 1.6672, val loss 1.7078
 80%|████████  | 8007/10000 [03:28<02:01, 16.38it/s]
step 8000: train loss 1.6808, val loss 1.7120
 85%|████████▌ | 8505/10000 [03:41<01:47, 13.94it/s]
step 8500: train loss 1.6733, val loss 1.7144
 90%|█████████ | 9007/10000 [03:54<00:56, 17.54it/s]
step 9000: train loss 1.6714, val loss 1.7031
 95%|█████████▌| 9506/10000 [04:07<00:28, 17.39it/s]
step 9500: train loss 1.6707, val loss 1.7073
100%|██████████| 10000/10000 [04:20<00:00, 38.43it/s]
step 9999: train loss 1.6664, val loss 1.7506

这似乎不太好。让我们绘制图表看看发生了什么。

import matplotlib.pyplot as plt

def plot_transformer_loss(loss_history, val_loss_history):

    # Two horizontal figures side-by-side
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

    # First plot = training loss
    ax1.plot(loss_history, lw=0.5)
    # Plot moving average of loss
    window_size = 100
    ax1.plot(np.convolve(loss_history, np.ones(window_size) / window_size, mode='valid'), label='Moving average')
    ax1.set_xlabel('Iteration')
    ax1.set_ylabel('Cross-entropy Loss')
    ax1.set_title('Training Loss')
    ax1.legend()

    # Second plot = validation loss
    # Set marker style to be circles at each data point
    indices = np.arange(0, len(val_loss_history) * eval_interval, eval_interval)
    ax2.plot(indices, val_loss_history, marker='o')
    ax2.set_title('Validation Loss')
    ax2.set_xlabel('Iteration')

    plt.show()

plot_transformer_loss(train_loss_history, val_loss_history)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们的变换器在井字游戏数据上训练的初始损失(图片由作者提供)。

我们可以使用这个code来测试变换器的效果:

import random
import torch
from IPython.display import clear_output

def play_game(model, stoi, itos, device):
    game = TicTacToe()

    # Randomly decide who goes first
    game.current_player = random.choice(['X', 'O'])

    while game.check_winner() is None and not game.is_draw():
        #clear_output(wait=True)

        print(f"{game.current_player}'s turn.")
        game.pretty_print_board()

        current_board_str = game.get_board_string()

        if game.current_player == 'X':
            print("Model's turn...")
            current_board_encoded = torch.tensor([stoi[c] for c in current_board_str], dtype=torch.long).unsqueeze(0).to(device)
            logits, _ = model(current_board_encoded)

            # Move logits to cpu
            logits = logits.cpu()

            # Create a mask for legal moves and zero out logits for illegal moves
            legal_moves = game.get_legal_moves()
            mask = torch.zeros(9)
            mask[legal_moves] = 1
            masked_logits = logits * mask

            # Get the model's move
            predicted_move = masked_logits.argmax(dim=-1).item()

            # Make the model's move
            game.make_move(predicted_move)

        else:
            print("Your turn!")
            legal_moves = game.get_legal_moves()
            print("Legal moves:", legal_moves)

            user_move = int(input("Enter your move: "))
            if user_move in legal_moves:
                game.make_move(user_move)
            else:
                print("Illegal move. Try again.")
                continue

        winner = game.check_winner()
        if winner is not None:
            #clear_output(wait=True)
            print(f"{winner} wins!")
            game.pretty_print_board()
            break
        elif game.is_draw():
            #clear_output(wait=True)
            print("It's a draw!")
            game.pretty_print_board()
            break

O's turn.
- | - | -
- - - - - 
- | - | -
- - - - - 
- | - | -
Your turn!
Legal moves: [0, 1, 2, 3, 4, 5, 6, 7, 8]
X's turn.
- | - | -
- - - - - 
- | - | -
- - - - - 
- | - | O
Model's turn...
O's turn.
- | - | X
- - - - - 
- | - | -
- - - - - 
- | - | O
Your turn!
Legal moves: [0, 1, 3, 4, 5, 6, 7]
X's turn.
O | - | X
- - - - - 
- | - | -
- - - - - 
- | - | O
Model's turn...
O's turn.
O | - | X
- - - - - 
X | - | -
- - - - - 
- | - | O
Your turn!
Legal moves: [1, 4, 5, 6, 7]
O wins!
O | - | X
- - - - - 
X | O | -
- - - - - 
- | - | O

好吧,我轻松打败了模型。某些事情出错了。

改进变换器

所以目前,变换器甚至无法从任何给定位置可靠地学习简单的胜利动作。我能想到几个原因:

  • 变换器仅在胜利的动作上进行训练,因此当我使用一个良好的策略(即没有胜利的动作可用)时,可能无法学习如何游戏。理论上,为了应对这一点,我们应该允许它在游戏注定为平局时进行训练。

  • 变换器的参数过多。试图让几十万个神经元协调一个简单的策略可能需要很长时间来训练,并依赖于grokking和其他现象才能进入优化景观的可泛化部分。

  • 变换器的参数过少。也许它需要更多的神经元来学习一个好的策略。这样说来,我非常怀疑如果几百万个神经元都无济于事,那几十万个神经元能否解决问题。

状态空间分析

在继续之前,我想从理论上分析编码井字游戏中完整胜利策略所需的神经元数量,我们需要考虑游戏的状态空间和决策过程的复杂性。

在井字棋中,游戏棋盘是一个 3 x 3 的网格,每个单元可以处于三种状态之一:‘X’,‘O’,或空(‘-’)。因此,总的可能棋盘状态数量可以计算为 3⁹ = 19683。然而,并非所有这些状态在实际游戏中都是有效的;其中一些是不可达的或非法的(例如,所有单元都是‘X’的棋盘)。合法状态的数量实际上大约为 5478,但为了分析的目的,我们将考虑上限,即 3⁹。

每个状态都需要一个决策:在哪里放置下一个‘X’(因为我们考虑的是‘X’的获胜策略)。有 9 个可能的位置,但合法的移动数量通常少于 9,这取决于已经被占据的单元格数量。一个神经网络需要将每个可能的棋盘状态映射到一个正确的移动。实现这种映射的一种方法是通过一个完全连接的层,该层将棋盘状态作为输入,并输出 9 个可能移动的概率分布。输入层将有 3x3=9 个神经元(每个单元一个),输出层将有 9 个神经元(每个可能移动一个)。中间的隐藏层将执行学习获胜策略的复杂任务。

考虑到输入层有 9 个神经元和输出层有 9 个神经元,我们关注的是隐藏层中的神经元数量。从理论上讲,我们可以使用一个具有 3⁹个神经元的隐藏层将每个可能的状态映射到一个获胜的移动。这将是一个上限,可能远远超过实际需要的数量,因为存在不可达/非法状态以及井字棋的固有对称性会减少实际的唯一状态数量。因此,在这个上限情况下,总的神经元数量将是:9 + 19683 + 9 = 19701。这是一个理论上的上限,实际数量可能由于前述因素而低得多。让我们尝试实现一个简单的前馈普通神经网络,看看它在我们的任务中的表现。我们将使用三个隐藏层,而不是一个具有数千个神经元的层。

import torch.nn as nn
import torch.nn.functional as F

device = 'cpu'

class TicTacToeNN(nn.Module):
    def __init__(self):
        super(TicTacToeNN, self).__init__()
        self.fc1 = nn.Linear(9, 16)  # Input layer to hidden layer 1
        self.fc2 = nn.Linear(16, 32)  # Hidden layer 1 to hidden layer 2
        self.fc3 = nn.Linear(32, 16)  # Hidden layer 2 to hidden layer 
        self.fc4 = nn.Linear(16, 9)  # Hidden layer 3 to output layer

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = F.relu(self.fc3(x))
        x = self.fc4(x)
        return x

def get_batch(split):
    input_data = train_input if split == 'train' else val_input
    output_data = train_output if split == 'train' else val_output
    # Choose index locs for batch_size sequences
    ix = torch.randint(len(input_data) - block_size + 1, (batch_size,))
    # Get the input and output sequences
    x = input_data[ix].float()
    y = output_data[ix]
    x, y = x.to(device), y.to(device)
    return x, y

# Create an initial empty game
initial_game = TicTacToe()

# Simulate all possible games starting with 'X'
simulate_all_games(initial_game, [], [])

# Convert to PyTorch tensors
input_tensor = torch.tensor(input_sequences, dtype=torch.long)
output_tensor = torch.tensor(output_sequences, dtype=torch.long)

nn_model = TicTacToeNN()
nn_model.to(device)

# Print the number of parameters in the model
print(sum(p.numel() for p in nn_model.parameters()), 'parameters')

# Create a PyTorch optimizer
optimizer = torch.optim.AdamW(nn_model.parameters(), lr=learning_rate, weight_decay=1e-4)
train_loss_history = []
val_loss_history = []

# Training loop
max_iters = 1000000
for iter in tqdm(range(max_iters)):
    # Evaluate the loss on train and val sets occasionally

    # Sample a batch of data
    xb, yb = get_batch('train')
    # Evaluate the loss
    logits = nn_model(xb)
    # Calculate cross-entropy loss
    loss = F.cross_entropy(logits, yb)
    train_loss_history.append(loss.item())
    # Get the validation loss
    xb, yb = get_batch('val')
    logits = nn_model(xb)
    val_loss = F.cross_entropy(logits, yb)
    val_loss_history.append(val_loss.item())
    # Backpropagate and update the weights
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

1385 parameters
100%|██████████| 1000000/1000000 [08:08<00:00, 2048.42it/s]

让我们看看损失情况如何:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于普通前馈神经网络的训练损失(作者提供的图片)。

显然,我们正在饱和性能。任务和我们设置的方法有些问题,阻止了模型学习适当的策略。为了改变一下,我打算尝试给模型提供只包含最优策略的训练数据。

最优策略训练数据

纽厄尔和西蒙 1972 年的井字棋程序概述了完美策略(以赢得比赛或至少平局),如果我们从以下移动偏好中选择第一个可用的移动:

  1. 获胜:如果你有两个连成一行,玩第三个以完成三连。

  2. 阻挡:如果对手有两个连成一行,玩第三个以阻挡他们。

  3. 分叉:创造一个你可以通过两种方式获胜的机会。

  4. 阻止对手的叉子:我们可以创建两个连续的棋子以迫使对手防守(如果这样做不会给他们造成叉子),或者阻止他们的潜在叉子。

  5. 中心:占据中心位置。

  6. 对角角落:如果对手在一个角落里,选择对角的角落。

  7. 空角落:选择一个空的角落。

  8. 空侧:选择一个空的边侧。

让我们重写数据生成器,以根据这一策略获得所有可能的走法。我们还将模拟两种可能的先手玩家的所有游戏。

from copy import deepcopy

# Helper function to find if there's a winning move or a move that blocks the opponent from winning
def find_winning_or_blocking_move(board, player):
    winning_positions = [
        [0, 1, 2], [3, 4, 5], [6, 7, 8],  # Rows
        [0, 3, 6], [1, 4, 7], [2, 5, 8],  # Columns
        [0, 4, 8], [2, 4, 6]  # Diagonals
    ]
    for positions in winning_positions:
        values = [board[pos] for pos in positions]
        if values.count(player) == 2 and values.count('-') == 1:
            return positions[values.index('-')]
    return None

# Helper function for checking for fork opportunities
def find_fork_move(board, player):
    fork_move = None
    for i in range(9):
        if board[i] == '-':
            temp_board = board[:]
            temp_board[i] = player
            winning_moves = 0
            for j in range(9):
                if temp_board[j] == '-':
                    temp_board_2 = temp_board[:]
                    temp_board_2[j] = player
                    if find_winning_or_blocking_move(temp_board_2, player) is not None:
                        winning_moves += 1
            if winning_moves >= 2:
                fork_move = i
                break
    return fork_move

# Helper function to find the optimal move according to a defined strategy
def optimal_strategy(board, player):
    opponent = 'O' if player == 'X' else 'X'

    # 1\. Win: If you have two in a row, play the third to get three in a row.
    win_move = find_winning_or_blocking_move(board, player)
    if win_move is not None:
        return win_move

    # 2\. Block: If the opponent has two in a row, play the third to block them.
    block_move = find_winning_or_blocking_move(board, opponent)
    if block_move is not None:
        return block_move

    # 3\. Fork: Create an opportunity where you can win in two ways.
    fork_move = find_fork_move(board, player)
    if fork_move is not None:
        return fork_move

    # 4\. Block Opponent's Fork
    opponent_fork_move = find_fork_move(board, opponent)
    if opponent_fork_move is not None:
        return opponent_fork_move

    # 5\. Center: Play the center.
    if board[4] == '-':
        return 4

    # 6\. Opposite Corner: If the opponent is in the corner, play the opposite corner.
    corners = [(0, 8), (2, 6), (8, 0), (6, 2)]
    for corner1, corner2 in corners:
        if board[corner1] == opponent and board[corner2] == '-':
            return corner2

    # 7\. Empty Corner: Play an empty corner.
    for corner in [0, 2, 6, 8]:
        if board[corner] == '-':
            return corner

    # 8\. Empty Side: Play an empty side.
    for side in [1, 3, 5, 7]:
        if board[side] == '-':
            return side

# Function to simulate all games according to the optimal strategy
def simulate_all_games_optimal_v2(game, x_starts=True):
    global input_sequences, output_sequences

    # Check for terminal state
    winner = game.check_winner()
    if winner or game.is_draw():
        return

    # If it's X's turn, apply the optimal strategy and save the board state and move
    if game.current_player == 'X':
        move = optimal_strategy(game.board, 'X')
        if move is None:
            move = game.get_legal_moves()[0]  # fallback
        input_sequences.append(encode(''.join(game.board)))
        output_sequences.append(move)
        new_game = deepcopy(game)
        new_game.make_move(move)
        simulate_all_games_optimal_v2(new_game, x_starts)
    else:
        # If it's O's turn, explore all possible legal moves
        for move in game.get_legal_moves():
            new_game = deepcopy(game)
            new_game.make_move(move)
            simulate_all_games_optimal_v2(new_game, x_starts)

# Character to integer mapping
chars = sorted(list(set('XO-')))
vocab_size = len(chars)
stoi = {ch: i for i, ch in enumerate(chars)}
itos = {0: '-', 1: 'X', 2: 'O'}
encode = lambda s: [stoi[c] for c in s]
decode = lambda l: ''.join([itos[i] for i in l])

# Reset and re-simulate
input_sequences = []
output_sequences = []

# 'X' starts
initial_game = TicTacToe()
simulate_all_games_optimal_v2(initial_game, True)

# 'O' starts
initial_game = TicTacToe()
initial_game.current_player = 'O'
simulate_all_games_optimal_v2(initial_game, False)

# Convert to Pytorch tensors
input_tensor = torch.tensor(input_sequences, dtype=torch.long)
output_tensor = torch.tensor(output_sequences, dtype=torch.long)

print("Number of input-output pairs:", len(input_sequences))

Number of input-output pairs: 1017

让我们在新的训练数据上重新训练我们的模型。

# Hyperparameters
batch_size = 128  # How many independent sequences will we process in parallel?
block_size = 9  # The size of the tic-tac-toe board
max_iters = 10000
eval_interval = 500
learning_rate = 1e-3
device = 'mps' if torch.backends.mps.is_available() else 'cpu'
eval_iters = 100
n_embd = 32  # Reduced the embedding size
n_head = 2  # Reduced the number of heads
n_layer = 2  # Reduced the number of layers
dropout = 0.1

print(f'Training on {device}')

# Initialize random seed
torch.manual_seed(1337)

# Split into training and validation sets
n = int(0.90 * len(input_tensor))  # 90% for training
train_input = input_tensor[:n]
train_output = output_tensor[:n]
val_input = input_tensor[n:]
val_output = output_tensor[n:]

# Updated data loading function
def get_batch(split):
    input_data = train_input if split == 'train' else val_input
    output_data = train_output if split == 'train' else val_output
    # Choose index locs for batch_size sequences
    ix = torch.randint(len(input_data) - block_size + 1, (batch_size,))
    # Get the input and output sequences
    x = input_data[ix]
    y = output_data[ix]
    x, y = x.to(device), y.to(device)
    return x, y

# Initialize the model
model = Transformer()
model = model.to(device)

max_iters = 5000

# Print the number of parameters in the model
print(sum(p.numel() for p in model.parameters()) / 1e6, 'M parameters')

# Create a PyTorch optimizer
optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate, weight_decay=1e-4)
train_loss_history = []
val_loss_history = []

# Training loop
for iter in tqdm(range(max_iters)):
    # Evaluate the loss on train and val sets occasionally
    if iter % eval_interval == 0 or iter == max_iters - 1:
        losses = estimate_loss()
        print(f"step {iter}: train loss {losses['train']:.4f}, val loss {losses['val']:.4f}")
        val_loss_history.append(losses['val'])

    # Sample a batch of data
    xb, yb = get_batch('train')

    # Evaluate the loss
    logits, loss = model(xb, yb)
    train_loss_history.append(loss.item())
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

Training on mps
0.025961 M parameters
  0%|          | 6/5000 [00:00<10:15,  8.11it/s]  
step 0: train loss 2.2078, val loss 2.2166
 10%|| 505/5000 [00:13<05:23, 13.89it/s]
step 500: train loss 0.3063, val loss 0.6145
 20%|██        | 1005/5000 [00:26<04:40, 14.24it/s]
step 1000: train loss 0.0741, val loss 0.2259
 30%|███       | 1505/5000 [00:38<04:08, 14.05it/s]
step 1500: train loss 0.0368, val loss 0.1799
 40%|████      | 2005/5000 [00:51<03:36, 13.83it/s]
step 2000: train loss 0.0134, val loss 0.1589
 50%|█████     | 2504/5000 [01:04<02:57, 14.06it/s]
step 2500: train loss 0.0081, val loss 0.0884
 60%|██████    | 3008/5000 [01:17<01:56, 17.06it/s]
step 3000: train loss 0.0041, val loss 0.0521
 70%|███████   | 3505/5000 [01:29<01:46, 14.09it/s]
step 3500: train loss 0.0028, val loss 0.0855
 80%|████████  | 4005/5000 [01:42<01:10, 14.06it/s]
step 4000: train loss 0.0036, val loss 0.1125
 90%|█████████ | 4506/5000 [01:56<00:29, 16.68it/s]
step 4500: train loss 0.0014, val loss 0.0892
100%|██████████| 5000/5000 [02:08<00:00, 38.79it/s]
step 4999: train loss 0.0026, val loss 0.0721

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用最优训练数据的新损失(作者提供的图像)。

太棒了!我们不仅学会了策略,而且它在验证数据集上也能泛化(我们在训练和验证集上接近 0 损失)。我猜这是由于棋盘状态的固有对称性,变换器已经学会了一种对棋盘状态不变的模块算术形式。

让我们尝试新的变换器:

O's turn.
- | - | -
- - - - - 
- | - | -
- - - - - 
- | - | -
Your turn!
Legal moves: [0, 1, 2, 3, 4, 5, 6, 7, 8]
X's turn.
- | O | -
- - - - - 
- | - | -
- - - - - 
- | - | -
Model's turn...
O's turn.
X | O | -
- - - - - 
- | - | -
- - - - - 
- | - | -
Your turn!
Legal moves: [2, 3, 4, 5, 6, 7, 8]
X's turn.
X | O | -
- - - - - 
- | - | -
- - - - - 
- | O | -
Model's turn...
O's turn.
X | O | -
- - - - - 
- | X | -
- - - - - 
- | O | -
Your turn!
Legal moves: [2, 3, 5, 6, 8]
X's turn.
X | O | -
- - - - - 
- | X | -
- - - - - 
- | O | O
Model's turn...
O's turn.
X | O | -
- - - - - 
- | X | -
- - - - - 
X | O | O
Your turn!
Legal moves: [2, 3, 5]
X's turn.
X | O | O
- - - - - 
- | X | -
- - - - - 
X | O | O
Model's turn...
X wins!
X | O | O
- - - - - 
X | X | -
- - - - - 
X | O | O

它打败了我!使用了一个绝妙的叉子。看起来我们的变换器已经学会了最优策略。

结论

我认为这里的主要收获是变换器完全能够学习游戏的最优策略。虽然一个普通的神经网络可能也能学到相同的最优策略,但注意力机制的动态特性意味着它可能能够处理表示游戏随时间演变的更长序列。这些想法自然地促使我们在强化学习设置中应用变换器。例如,Janner 等(2021) 使用变换器来建模轨迹分布,并使用束搜索作为规划算法。

从这个项目中我学到的另一件事是,人工手动引导变换器通过最优策略的过程显然无法扩展,尤其是当游戏变得更加复杂时。例如,围棋并不是一个“已解决”的游戏,因此我们不能像上面那样提供最优策略进行训练。相反,我们必须使用类似自我对弈的方法来选择好的棋局序列,然后再用这些序列训练变换器。我希望未来能尝试这些想法。

最后,通过查看变换器中的预测和规划层级,仍有一个待开发的领域。正如Ba 等人 (2016) 所指出的,深度学习通常关注于在激活动态中保持临时状态的方法,而我们的大脑似乎是通过中期突触可塑性来调节临时状态信息。换句话说,应该有某种形式的工作记忆/预测,在下一个标记级别和例如 LSTM 中的长期状态之间操作。作为 GPT-3 支撑骨架的自回归解码器仅变换器架构是一个强大的模型,可以通过预测一个标记的未来生成逼真的文本。然而,如果我们将智能拟人化,我们知道快速的直观预测(仅预测一个标记)并不能使人变成天才。因此,在我看来,尝试为模型提供多个预测层级,让模型学会预测多个未来标记将会很有趣。这将类似于人类的提前规划能力,并且可能是变换器学习的一个有用技能。

预测未来意味着什么?这里有几个不同的探索方向:

  • 时间上的预测:变换器能否学会预测下一个序列中的标记,而不是预测两个、三个或更多步之后的标记?预测两个标记一次是否等同于先预测一个标记,再预测下一个标记,还是说一次预测两个标记有某种战略上的好处?这是否迫使变换器思考更长时间?

  • 空间上的预测:有证据表明,人类会产生粗略的任务语义表示,然后使用层级模块来“填补”这些粗略表示中的空白。你可以将其想象为写一篇文章:首先你创建一个要点的骨架,然后为每一段填入论点句子,最后完善细节。变换器是否可能学会做同样的事情?

无论这些问题是否有用,我希望这篇文章能为我们如何将问题重塑为适合变换器的问题提供一些清晰的见解。祝调优愉快!

参考文献

  1. Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., & Polosukhin, I. (2017). 注意力机制是你所需的一切。神经信息处理系统进展, 30

  2. Janner, M., Li, Q., & Levine, S. (2021). 离线强化学习作为一个大的序列建模问题。神经信息处理系统进展, 34, 1273–1286。

  3. Ba, J., Hinton, G. E., Mnih, V., Leibo, J. Z., & Ionescu, C. (2016). 使用快速权重关注最近的过去。神经信息处理系统进展, 29

  4. Andrej Karpathy. 让我们从头开始构建 GPT:从代码到拼写的全程讲解。 www.youtube.com/watch?v=kCc8FmEb1nY&t=5076s

我们能否阻止 LLMs 产生幻觉?

原文:towardsdatascience.com/can-we-stop-llms-from-hallucinating-17c4ebd652c6?source=collection_archive---------6-----------------------#2023-08-24

意见

普及 LLMs 的最大障碍之一可能是本质上难以解决的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Juras Juršėnas

·

关注 发表在 Towards Data Science · 阅读时间 5 分钟·2023 年 8 月 24 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Google DeepMind 提供,来源于 Unsplash

虽然大型语言模型(LLMs)已经引起了几乎所有人的关注,但由于它们存在一个相当恼人的方面——这些模型有时会“幻觉”,因此这种技术的大规模应用略显受限。简单来说,它们有时会编造信息,而且最糟的是,这些内容往往看起来非常令人信服。

幻觉,无论频繁与否,都带来了两个主要问题。它们无法直接应用于许多敏感或脆弱的领域,其中一个错误可能会非常昂贵。此外,它还会造成普遍的不信任,因为用户被期望验证 LLM 输出的所有内容,这在一定程度上违背了这项技术的初衷。

学术界似乎也认为幻觉是一个重大问题,因为 2023 年有很多研究论文讨论并试图解决这个问题。然而,我倾向于同意 Yann LeCun,Meta 的首席 AI 科学家,他认为幻觉根本无法解决。我们需要对技术进行彻底的重构以消除这个问题。

幻觉虚假陈述

我认为,有两个重要方面使得幻觉问题无法解决。首先是相当明显的技术基础,LLM 像其他机器学习模型一样,具有随机性。简单来说,它们做出预测。

虽然它们确实比“被吹捧的自动完成”要先进得多,但底层技术仍然使用关于令牌的统计预测。这既是 LLM 的优点也是缺点。

在强项方面,我们已经看到它们在预测输入后的内容方面是多么出色(假设没有故意破坏输出的尝试)。用户可能会犯几种类型的错误,比如留有错别字、误解单词的意思等,而 LLM 仍然可能给出正确的输出。

当初,首批基于文本的游戏被创造时,用户需要准确输入命令,不能有任何错误或解释的空间。比如,“move north”命令如果用户输入为“move morth”就会出错。然而,LLM 可能能够推测两者的意思。从这个角度来看,这项技术确实非常迷人。

然而,这也展示了一个弱点。任何输入都有一个广泛的令牌选择决策树。简单来说,模型生成输出的方式总是有很多种。在那种广泛的选择范围中,相对较小的部分是“正确”的决定。

尽管有许多优化选项可供选择,但问题本身是不可解决的。例如,如果我们增加提供某个特定答案的可能性,LLM 会变成一个查找表,所以我们希望保持平衡。底层技术仅基于随机预测,因此必须为更广泛的输出令牌提供一些空间。

但 LLMs 在当前状态下不能解决另一个问题。这涉及到更为抽象和虚幻的认识论问题,即研究知识本质的哲学领域。表面上看,这个问题很简单——我们如何知道哪些陈述是真实的,我们如何获得这种知识?毕竟,幻觉只是一些虚假的陈述事后产生的,因此如果我们能为模型创建一种方法来验证其是否做出了虚假的陈述并将其删除,这将解决问题。

分离幻觉和真实陈述

借鉴哲学的思路,我们可以区分两种可能的陈述——分析性和综合性。前者是通过定义而真实的陈述(最常见的例子之一是“单身汉是未婚男子”)。简单来说,我们可以通过分析语言本身找到真实的陈述,而无需外部经验。

综合性陈述是指通过某种形式的经验来判断真实的陈述,例如“桌子上有一个苹果”。在没有直接经验的情况下,无法知道这样的陈述是否真实。纯粹的语言分析对于判断其真假无济于事。

我应该指出,这些陈述之间的区别在几百年来一直备受争议,但这一讨论对于 LLMs(大型语言模型)来说基本上不相关。正如它们的名字所示,它们是高度先进的语言分析和预测机器。

根据这两种类型的区别,我们可以看到 LLMs 在分析性陈述方面几乎不会有问题(至少不会比人类有更多问题)。然而,它们无法获取经验或大范围的世界知识。它们无法知道某些陈述是否因为事件的存在而真实。

主要问题在于,分析性陈述的数量远小于所有综合性陈述的集合。由于 LLMs 无法验证这些陈述是否真实,我们作为人类必须向它们提供这些信息。

因此,LLMs 面临一个挑战。所有可能输出的集合中总会包含一些综合性陈述,但对于模型来说,它们都是不具备真值的。简单来说,“尤利乌斯·凯撒的刺客是布鲁图斯”(虽然有很多,但在此案例中无关紧要)和“尤利乌斯·凯撒的刺客是亚伯拉罕·林肯”对于模型来说是等同的。

反驳意见可能是我们对这些事件也没有直接经验。我们只是从书籍中读到它们。但对陈述真实性的发现是基于对幸存记录的重建以及广泛的考古证据。

一个简单的(虽然相关性较低)例子是“今天在下雨。” 对于大型语言模型来说,这样的陈述是无法确定其真实性的,因为它需要在查询时接触到现实世界的经验。

从某种意义上说,认识论问题是自我解决的。我们的文学语料库会使“凯撒的刺客是布鲁图斯”这种输出变得显著更可能,因为它出现得更频繁。然而,再次强调的是,这种自我解决的方法依赖于在绝对所有可用文本信息上训练大型语言模型,这显然是不可能的。此外,这也会使其他不那么真实的输出仍然存在于所有可能输出的集合中。

因此,数据质量变得非常重要,但这种质量只能由人类观察者来判断。即使模型在大量数据上进行训练,仍然会有一定的选择过程,这意味着合成陈述的错误率无法消除。

结论

我认为,阻止模型产生幻觉的问题是无法解决的。一方面,技术本身基于随机过程,这不可避免地会在大量输出中导致错误的预测。

除了技术难题,还有一个问题是大型语言模型是否可以对陈述做出真实性判断,我再次认为这是不可能的,因为它们无法接触到现实世界。这个问题在很多大型语言模型现在提供的各种搜索引擎功能中稍微得到了缓解,这些功能可以验证某些陈述。

然而,可能会有一种方法是收集一个可以测试陈述的数据库,但这需要超出技术本身的东西,这将我们带回到最初的问题。

一个机器学习工程团队的碳排放

原文:towardsdatascience.com/carbon-emissions-of-an-ml-engineering-team-ce170bd4fae9

开发的隐性成本

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Jake Teo

·发表于Towards Data Science ·9 分钟阅读·2023 年 10 月 16 日

由于人为活动导致的全球变暖,大家都意识到了气候危机。为了防止其灾难性后果[1],世界需要大幅减少我们的温室气体排放,许多国家设定了到 2050 年实现净零排放的目标。

近年来 AI 技术的蓬勃发展也引发了对其环境成本的担忧。如果我们仅仅关注其直接贡献,这将通过电力使用来训练和驱动模型。例如,训练具有 1750 亿参数的 ChatGPT-3 产生了高达 502 吨的碳当量排放(tCO2e)[2]。新兴的 Llama2 在训练其四个模型时产生了类似的 539 吨 tCO2e[3]。作为对比,每一个模型的排放量相当于一名乘客从纽约到旧金山单程飞行 500 次的排放量。

我在一个机器学习工程团队工作,这个问题也时常困扰着我。我们通过电力消耗贡献了多少碳排放?是否有减少的方法?于是,我们开始首次尝试进行碳排放核算。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由Chris LeBoutillier提供,来自Unsplash

方法

没有单一直接的方法来测量我们的电力消耗以及随之而来的碳影响。这是因为我们使用的平台和服务多种多样。我不会深入探讨技术实现,但从高层次来看,方法包括三种。

  1. 提供:确切的碳排放数据已经为我们计算好了。这是我们的云服务提供商(CSP)提供的。

  2. 工具:我们使用了像 Powermetics、Nvidia-SMI 和 Turbostat 这样的几种软件工具来测量功率(瓦特),这些工具跟踪我们笔记本电脑和本地服务器的 CPU 和 GPU 计算。

  3. 自我计算:当上述方法不可行时,我们使用代理方法进行计算。这包括记录计算的持续时间,估计芯片的利用率百分比,以及查找每种芯片类型的热设计功率(TDP)来计算功耗。其余平台以这种方式计算。

对于后两种方法,功率会被转换为能量(千瓦时),如果有的话,会使用支持数据中心的电力使用效率(PUE)来获得更准确的能量消耗。最后,使用该国或地区的电网排放因子(kgCO2e/kWh)来计算温室气体排放。

结果与思考

结果显示在下面的饼图中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们使用的每个平台的碳排放量 extrapolated 为整个 2023 年。图像由作者提供。

在碳排放排名方面,平台的排名并不特别令人惊讶,但我对百分比感到惊讶。我没有想到我们的开发笔记本电脑和 CICD 服务在非常重的使用下只产生了微量的碳。与此同时,我也没想到我们的本地开发和模型训练服务器会消耗比我们的云服务多三倍的碳。

回顾过去,我们最近将笔记本电脑升级到了最新的 Apple Silicon M2 芯片,这一芯片以高效著称。我们的 CICD 平台虽然拥有数千分钟的流水线运行时间,但使用的是最低计算芯片,实际上是无服务器的,仅在必要时运行。

对于我们的本地服务器,我们发现空闲的 Nvidia GPU 芯片仍然消耗大量电力,导致电力消耗膨胀。我们需要调查是否存在任何配置错误,如果没有,是否有更好的管理方法。

绿色计算

现在我们对碳排放的认识有了更好的了解,我们如何才能真正改变开发团队,采用更多绿色解决方案呢?

绿色计算这个术语已经存在了一段时间,并且已被组织或分类成不同的形式,但我认为下面这六个广泛的主题将帮助我的团队更清晰地管理绿色转型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Ash from Modern Afflatus 提供的照片,来源于 Unsplash

1. 绿色 AI

这指的是寻找更高效地训练和推断模型以尽量减少质量损失的方法。它基本上意味着更快的训练和推断时间,以及更小的模型尺寸,使用更少的计算能力。使用更复杂的神经网络需要越来越大的数据集以及日益先进、昂贵且耗能巨大的 GPU 芯片。

幸运的是,这也是最新优化研究的热点。在过去几年中,我听说我的数据科学家同事们在各个领域使用更高效的架构、迁移学习、量化或知识蒸馏等压缩技术、ONNX、使用deepspeedPEFT等,以应对当今大语言模型时代的挑战。毫无疑问,我们需要跟上开源世界最新实施的步伐,因为它们的好处已被证明是显著的。

2. 绿色应用

模型在没有周围代码来处理数据、训练模型和最终提供服务的情况下是无用的。需要对时间和空间复杂性、实现的算法以及各种预构建函数有基本的理解。还应使用性能分析工具来查找延迟和内存中的瓶颈。

另一个构建绿色应用程序的重要软件工程技能是理解任务和进程的管理、执行和协调。这需要对并行性、并发性、异步性、多处理和线程、队列、I/O 和 CPU 限制任务等概念有扎实的掌握。

更进一步,编程语言的选择也很重要。由于其广泛的支持和易用性,Python 已成为数据科学和通用编程中使用的顶级语言之一。然而,作为一种解释性语言,与其编译语言如 Go 相比,在能耗和速度(约 x20)方面显著逊色[4]。因此,值得花费时间学习另一种编译语言,以应对需要大量处理的工作。

3. 绿色服务器

训练和服务机器学习应用需要计算能力。这由托管在本地或云端的服务器提供。如果可能的话,使用云服务是保持绿色的最佳方式,因为云服务提供商有动力高效运行其数据中心,而且你可以根据项目需求灵活切换资源。无论如何,我们应确保两个关键因素:选择正确的硬件来完成任务,以及仅在需要时使用计算资源

主要的 CSP 都提供了多样的服务器供选择。例如,AWS 有七个实例家族,每个家族包含不同的芯片、内存和其他规格,足以满足各种需求,如 GPU、CPU、内存密集型过程,甚至 ARM 或 x86 架构。我们应选择那些最佳匹配我们的用例的服务器,以便通过其硬件规格高效分配计算资源。

我们如何在需要时才计算?首先,对所有不使用的资源进行盘点并关闭。你会惊讶于遗留项目中还有多少闲置的服务。在架构设计方面,我们可以选择使用类似 AWS lambda 的无服务器计算,它只有在有流量时才使用资源,或者提供一个基本的长生命周期计算,具有水平扩展功能,可以自动响应负载增加。

4. 绿色存储

存储有多种形式,如对象存储、块存储和文件存储、容器注册表和数据库。我们可以使用两个一般性的指南来高效管理存储:减少存储大小选择合适的存储类型

数据的存储大小可以通过压缩来减少,一些常见的压缩工具包括 gzip 或用于归档的 tar.gz,它可以将大小减少一半。使用更高效的数据结构也可以是一个更好的替代方案。使用像 parquet 这样的列式格式不仅占用空间更少(>50%),而且由于其列式结构的特性,也使查询速度更快(提高 30 倍)。

以对象存储为例,有一些存储类别使用更少的能源。在 AWS S3 中,我们可以选择将不太重要的数据保留在一个区域,而不是在多个区域中复制。对于不经常访问的长期存储,我们可以将其放入“冷存储”(S3 glacier),那里使用的磁带驱动器相比 SSD 和 HDD 消耗更少的能源。还可以设置生命周期策略来自动在存储类别之间转换,甚至在项目结束时删除数据。

5. 绿色传输

数据需要在服务器、存储和其他设备之间来回传输。网络通信也需要能源,以支持复杂的网络设备、数据中心、传输基础设施和终端用户设备。对于我们这样的开发者,我们可以通过使用高效的传输协议以及减少传输的频率和距离来降低碳足迹。

在可能的情况下,应考虑使用 http/2 传输协议和 gRCP 框架,因为它可以以更紧凑的二进制格式传输,而不是传统的文本(JSON)有效载荷的 http/1。这样可以降低延迟和能源消耗。

将数据更靠近使用源,并安排它们的传输时间,也可以减少所需的能量。例如,运行自动化测试用例所需的依赖项可以缓存,并且仅在检测到新更改时才重新构建。镜像不需要每次都从 Dockerhub 拉取;我们可以将它们存储在我们的 CSP 注册表中,并在有新补丁时定期更新。

6. 绿色模板

这指的是高效代码、基础设施和流程的可重用性和可重复性。本质上,这是一种间接减少电力消耗的方式,因为实际实现来自前五个主题。然而,我认为这是最重要的一点,因为它是团队知识的总和。

这可以以文档或剧本的形式出现,设定团队职能和项目执行的标准,或为仓库、CICD 流水线、基础设施设置(例如 Terraform)和配置(例如 Ansible)提供现成模板。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

决策象限以优先考虑高影响力和易于实施的解决方案。图片来自作者。

在这六个主题中的每一个,我都给出了一些示例,但这仅仅是冰山一角。在每个主题中实施的建议众多且令人望而生畏。然而,通过将每个建议放在决策象限中,估算它们在现有工作流程中的实施难度,以及它们的影响是否显著和协同,可以实现渐进式过渡。这将提供一些关于优先考虑哪些建议的指导。

设计原则

这种转变既不直接也不容易。即便是像我们这样热衷于可持续发展的开发者,也必须优先考虑业务需求。我们可以通过不把可持续性和碳效率在机器学习开发中视为与其他需求对立或相互排斥的概念来应对这一挑战。这将确保你仍然与业务目标保持一致,同时也能更容易获得管理层的支持,他们总是面临着交付的压力。

我们可以将其可视化为一个维恩图,使用 AWS 的六个设计支柱来构建其架构良好框架,其中功能或业务需求与可持续性重叠。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

重新构想架构良好框架。图片来自作者。

事实上,如果你仔细思考,通常会发现协同影响。让我们来看一些例子:

  • 压缩数据存储可以减少 x2 大小,从而节省成本和带宽,同时也减少了存储和传输所需的能量。

  • 神经网络模型的量化在推理速度方面表现更好,从而消耗更少的能量。

  • 移除 Docker 镜像中的未使用依赖项将通过减少潜在的可利用表面积来提高安全性,增加由于镜像体积较小而导致的部署速度,并减少存储和传输到及从你的注册中心所需的能量。

结论

总的来说,这为我们工程团队减少碳足迹的旅程提供了一个良好的开端。接下来还会有大量的工作需要识别、量化、标准化和教育每个绿色计算主题下的推荐措施。

我希望听到你在测量和减少开发团队碳足迹方面的旅程。请在下面的评论中分享!

致谢:这次碳核算尝试是我与我的同行* 杨可文 钟耀威* 一起完成的个人项目。*

免责声明:这里表达的意见和建议仅代表作者个人观点。*

参考文献

碳足迹:为什么常见的说法可能不准确

原文:towardsdatascience.com/carbon-footprint-why-common-claims-may-not-be-accurate-6f7860a7f08b?source=collection_archive---------9-----------------------#2023-03-31

创建强健的 CO₂ 情景以推动数据驱动的气候行动

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Boris Ruf

·

关注 发表在 Towards Data Science ·7 min read·2023 年 3 月 31 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 David Aler 提供,来自 Unsplash / 插图及拼接由作者制作

气候变化的影响在全球变得越来越明显,从毁灭性的野火到创纪录的热浪和飓风。因此,越来越多的人在寻求减少碳足迹和帮助缓解气候变化影响的方法。然而,很难知道从何处开始或如何做出有意义的改变。我们介绍了一种使用开放和链接数据方案建模不同活动碳足迹的新方法。 相关研究文章 将于今年晚些时候在 Energy Reports 期刊上发表。

随着气候危机的持续展开,自然灾害发生频率增加,减少温室气体(GHGs)的紧迫性不断加大。其中一个关键挑战是理解我们日常活动对环境的影响。然而,识别个人减少潜力可能复杂且难以理解,因为隐性、嵌入式排放常常发生在其他地方且难以可视化。

近年来,你可能见过有关日常活动如接收电子邮件或观看电影的碳足迹的惊人插图。这些比较旨在让消费者对像碳排放这样的抽象概念有所了解。它们提高了意识,鼓励人们采取行动减少碳足迹。然而,为了做出明智的决策,验证这些说法的准确性至关重要。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Adriano BeckerUnsplash 上的照片

假新闻?

有一种说法认为,接收一年的电子邮件产生的碳排放量与驾驶一辆普通汽车行驶 200 英里相同。不幸的是,这一说法并没有经过严格的分析,并且存在重大舍入错误。作者后来与这一说法脱离了关系。

另一个流行的说法认为,观看仅 30 分钟的 Netflix 相当于驾驶近 4 英里。然而,这一说法也发现了重大错误,包括换算错误和未考虑终端设备的碳足迹。

显然,评估和讨论碳足迹情景需要可靠的信息,上述事件突显了透明和严格的方法论的必要性。

背景很重要

那么,为什么很难对看似基本的日常活动得到可靠的答案呢?考虑以下问题:我能开车行驶多远才能排放与长途飞行相同的碳量?在法国或德国从电网充电电动车——有什么区别?选择白肉而不是红肉,在碳足迹方面有什么好处?通过 VoIP 电话(如 WhatsApp)与普通手机通话相比,如何?

这些情境下的计算不仅受复杂技术规格的影响,还受到个体因素的影响,这些因素可能根据情境的不同而大相径庭。例如,与飞行相比,你的汽车燃油消耗扮演着重要角色。电动车充电时,本地电网的能源组合是一个关键因素。例如,2021 年法国电力部门的碳强度估计为每千瓦时 58 克 CO₂,而德国为 349 克。

因此,创建针对特定情境的强健排放情景至关重要。这对于进行有力比较和制定明确且可信的建议是基础。

数据模型

为应对这一挑战,我们提出了一种通用碳足迹情境数据模型,以提高碳足迹数据的可访问性和实用性。该模型旨在开放、链接和模块化,便于共享和重用。模型以 JSON 格式表示,包含几个实体,每个实体都有一个属性列表及其数据类型。实体包括ScenarioScopeComponentLinkSourceEmissionConsumerConsumption,每个实体都有自己独特的属性及与其他实体的关系。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

自指碳足迹情境的数据模型(可选属性为斜体)。图像由作者提供。

任何场景都由统一资源标识符(URI)标识。它具有一个标题,标题中可选地包括一个超链接形式的参考。一个场景涵盖 1 到 3 个排放范围,这些范围由温室气体议定书定义。该实体可能包含描述。此外,它包括 1 个或多个组件链接。后者简单地与另一个通过其 URI 标识的场景相关联。它还可以附有数量的指示。然而,组件必须包括数量和数量单位(例如,“km”,“kg”,“pcs”)。它还可以有一个定义消费者类型的类别(例如,“汽车”,“食品”,“电子产品”)。此外,该实体必须包括一个来源,它具有一个名称和类型(例如,“法国电网”和“电力”,或“优质汽油”和“汽油”)。它还可以包含描述(例如,“2022 年”)。

任何来源必须包括 1 个或多个排放量,这些排放量以键值对的形式实现。排放类型定义了键(“co2e”,“co2”,“ch4”,“n2o”,“hfcs”,“pfcs”,“sf6”,“nf33”)。值、单位(例如,“g”,“kg”,“t”)和基本单位(例如,“kWh”,“l”,“kg”,“km”)指定了排放细节。可以通过超链接公开此信息的来源。组件也可以有一个消费者,当有关能源效率的信息可用时(例如,食品的排放通常仅按生产 1 公斤来报告)。消费者有一个名称(例如,“波音 747”,“iPhone 14”),并可以有一个可选的描述。它还包括 1 个或多个消费量(某些消费者可能支持多种能源来源,例如,可以用不同类型的汽油加油的内燃机汽车;混合动力车使用汽油和电力),这些消费量以键值对的形式存在。能源类型定义了键(例如,“电力”,“汽油”)并与来源类型相对应。值、单位(例如,“kWh”,“l”)和基本单位(例如,“km”,“h”,“d”)指定了消费细节。这些信息可以通过参考进行补充。

这是一个简单的数据示例:

{
  "title": "Mobility",
  "scopes": [
    {
      "level": "Scope 1",
      "list": [
        {
          "type": "component",
          "consumer": {
            "name": "Volkswagen Golf (2014)",
            "description": "Engine ID 45, 4 cylinders, Manual 6-spd",
            "consumptions": {
              "diesel": {
                "value": "0.0735046875",
                "unit": "l",
                "base_unit": "km",
                "reference_url": "https://www.fueleconomy.gov/"
              }
            }
          },
          "quantity": "10000",
          "quantity_unit": "km",
          "source": {
            "name": "Gas/Diesel oil",
            "type": "diesel",
            "emissions": {
              "co2e": {
                "value": "3.25",
                "unit": "kg",
                "base_unit": "l",
                "reference_url": "https://bilansges.ademe.fr/index.htm?new_liquides.htm"
              }
            }
          }
        }
      ]
    }
  ]
}

我需要一个翻译员

为了使数据生动,我们开发了一个基于网络的查看器,可以解释和可视化这种数据格式。该应用程序按元素和范围聚合排放数据,进行单位转换,并基于不同类型排放数据的可用性找到共同点(例如,“CO2e”,“CO2”)。在用户界面中,用户可以立即调整数据,通过调整每个元素的数量并即时连接不同的数据源来替换消费者组件和能源来源。

自定义场景可以作为 JSON 文件下载并通过 URL 共享,这使得场景协作变得简单。最后,一个 基准视图 使用户能够通过标识符比较两个或多个场景。

该 Web 应用程序是使用 JavaScript 构建的,并作为 GitHub 页面部署,这使得部署和更新变得容易。我们数据模型的自我引用结构还允许以分布式方式托管嵌套场景。数据解释器能够递归地获取和处理这些场景,从而在分布式环境中访问和分析复杂的碳足迹模型成为可能。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基于网页的数据解释器在查看模式下。图片由作者提供。

关于如何使用和部署该应用程序的详细文档可以在我们的 GitHub 仓库 中找到。在这里,你还可以访问源代码,以及演示和多个示例场景:

我们的方法旨在提高碳足迹场景的透明度、可访问性和可探索性,使个人能够做出关于其行为环境影响的知情决策。

TL;TR

我们提出了一种新颖的数据模型,用于生成可以适应本地或个人情况的碳足迹场景。我们的查看器应用展示了用户如何增强对不同活动相关碳排放的理解,从而做出更明智的选择。用户可以实时操作数据,观察更换不同组件(例如使用替代能源或减少特定材料的数量)对整体碳足迹的影响。此外,该应用还便于对不同场景进行并排比较,使用户能够评估其差异。应用程序的另一个重要功能是能够轻松共享场景,促进合作与知识共享,从而减少碳排放。

附言:根据我的假设,从巴黎飞往新加坡的长途航班相当于驾驶我的汽车行驶 3,200 公里。

B. Ruf 和 M. Detyniecki (2022),《碳足迹场景的开放与联接数据模型》,第七届国际可再生能源与节能大会 (ICREC),法国巴黎。

卡洛斯·阿尔卡拉斯与三大巨头

原文:towardsdatascience.com/carlos-alcaraz-vs-the-big-3-138e50a8a429?source=collection_archive---------1-----------------------#2023-07-29

一张视觉数据对比图,展示了新兴网球明星与罗杰、纳达尔和德约科维奇的对比

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Abhi Sawhney

·

关注 发表在 Towards Data Science ·6 分钟阅读·2023 年 7 月 29 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由Filip Mroz拍摄,发布在Unsplash

背景

几十年来,男子网球与其主宰的三大巨头——罗杰·费德勒拉斐尔·纳达尔诺瓦克·德约科维奇几乎是同义词。这是一个独特的网球时代,如同在其他任何运动中一样,三位有史以来最成功的球员几乎都在同一代里竞技。在他们之间,这三大巨头(如下面的绿色区域所示)自 2003 年以来赢得了 82 场大满贯赛事中的 65 场。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图表 1

然而,随着 Roger 于 2022 年退役、Rafa 由于伤病目前缺席,以及 Novak 最近在一场史诗般的温网决赛中输给 Carlos,似乎传递接力棒的过程已经开始。

随着网球开始设想没有大三巨头的世界,球迷和专家们一直在想是否有球员能够再次拥有像 Roger、Rafa 或 Novak 那样辉煌的职业生涯,赢得超过 20 个大满贯。一两年前,甚至思考这样的问题都是愚蠢的,更不用说大声问出来了。

随着 20 岁的世界第一 Carlos Alcaraz 的惊人崛起,网球界不得不暂停并重新考虑。如今,考虑这个想法已不那么具争议性。作为网球迷,你可能会看到大量比较 Carlos 与大三巨头的评论和媒体报道。然而,很少有这些讨论将 Carlos 到目前为止的表现与大三巨头在职业生涯早期的表现进行比较。将 Carlos 在第十个大满贯正赛出场的水平与 Novak 在第七十一场的水平进行比较,虽然令人兴奋,但并未提供完整的画面。

数据来源与分析范围

作为一个网球迷和数据专业人士,我一直期待看到更多的同类比较,比较球员在职业生涯相似阶段的表现。幸运的是,得益于 Jeff Sackmann 在通过他的 tennis_atp GitHub 仓库提供巡回赛 ATP 比赛数据的伟大工作,这样的分析成为可能。这个仓库,更广泛地说,他的 GitHub 是任何对网球分析感兴趣的人的极好资源。另一方面,你可以在我的 GitHub 上找到本文中用于分析和视觉呈现的所有 Python 代码。

拥有这些数据并在 Pandas 和 Matplotlib 中进行一些分析,我们可以更细致地了解 Carlos 的早期职业生涯与大三巨头的比较。

我们将特别关注大满贯和大师赛 1000 赛事这两类最受追捧的赛事,并将比较范围扩大到包括 前十名球员,而不仅仅局限于 Carlos。这将有助于为表现提供更多背景和视角。

首先,这里是大三巨头与今天排名前十的球员(截至 2023 年 7 月 17 日)在大满贯和大师赛 1000 赛事中获胜数量的当前状态比较。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图表 2

我们可以明确说的是,目前大三巨头与前十名之间的差距相当大。了解 Roger、Rafa 和 Novak 到达现有地位的过程将是有帮助的。

比较历程

为了做到这一点,让我们对比一下上面显示的两大主要赛事类别中的球员历程。我们将从大师赛 1000 赛事开始,然后转到至关重要的大满贯赛事。

大师赛 1000

下图绘制了三巨头及七位其他前十名球员的历程,这些球员至少赢得过一个大师赛 1000 赛事。x 轴代表大师赛 1000 赛事的参与次数(从正赛开始),y 轴代表赢得的大师赛 1000 赛事的数量。在某一点上,球员趋势线越高,说明到那一点的表现越好。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图表 3

三巨头的长期性和持续成功显而易见,每位三巨头成员参加了超过 120 个大师赛 1000 赛事,并赢得了 20 多个冠军。然而,为了讨论方便,让我们聚焦于上面突出显示的左下角象限,比较两位杰出球员与三巨头的表现。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图表 4

达尼尔·梅德韦杰夫的六个大师赛 1000 冠军是当前前十名球员中最多的(不包括诺瓦克)。他起步较晚,在第 21 次出战时赢得了他的第一个大师赛 1000 冠军。以 42 次赛事赢得六个冠军,他目前略微领先于诺瓦克在此阶段的表现,并与罗杰持平。

然而,卡洛斯·阿尔卡拉斯是最引人注目的。卡洛斯在仅仅第 7 次出战时便获得了他的第一个大师赛 1000 冠军,他的起步比三巨头中的任何一位都要快。在参加了 16 个大师赛 1000 赛事后,他与拉法在四个冠军上并列领先,而在类似阶段,诺瓦克(2)和罗杰(0)则远远落后。

大满贯赛事

让我们看看这些趋势在最大舞台——大满贯赛事上是否也成立。卡洛斯和达尼尔是前十名中唯一在大满贯赛事中赢得过冠军的球员。让我们与三巨头对比他们的大满贯历程。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图表 5

达尼尔在 25 次出战中仅赢得一个大满贯冠军,目前稍微落后于诺瓦克(2),远远落后于罗杰(5)和拉法(8)。与此同时,卡洛斯则一飞冲天,在诺瓦克和罗杰获得首个大满贯之前就赢得了两个大满贯冠军。

由于每年只有四个大满贯赛事,因此对球员早期职业生涯的大满贯赛事数据集相对有限。为了进一步了解大满贯赛事的表现,我们将数据的粒度从赛事层面提高到比赛层面。

在下图中,x 轴代表大满贯比赛的数量(从正赛开始),y 轴代表赢得的大满贯比赛数量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图表 6

这个视图让我们感受到每位球员在淘汰赛中的深度。在 44 场大满贯比赛后,卡洛斯与拉法在 36 场胜利上并列第一,如果他在今年晚些时候的美国公开赛首轮比赛中获胜,他将独占榜首。

结论

从上述数据可以明显看出,卡洛斯·阿尔卡拉斯是一位绝对的天才。如果我们考虑年龄因素,他的表现更为引人注目。让我们看看这些球员在 25 岁之前赢得的各项大师 1000 赛和大满贯赛事的数量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图表 7

作为 19 岁时最年轻的男子世界第一,且在仅 20 岁时已经拥有两个大满贯和四个大师 1000 赛,卡洛斯前途广阔。

但正如大多数历代伟大球员一样,真正特别的是这三位伟大球员能够保持如此长时间的卓越。如果卡洛斯能保持健康,并继续像今天这样主导比赛,而这些图表中的 x 轴年复一年地延展,大三巨头可能需要腾出一些位置。

感谢阅读!

数据来源:Sackmann, Jeff. GitHub 仓库: github.com/JeffSackmann/tennis_atp

分析与视觉笔记本:github.com/asawhney27/Tennis-Analytics

案例研究:将数据科学过程模型应用于实际场景

原文:towardsdatascience.com/case-study-applying-a-data-science-process-model-to-a-real-world-scenario-93ae57b682bf?source=collection_archive---------1-----------------------#2023-03-11

供应链中材料规划的机器学习模型开发

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Jonas Dieckmann

·

关注 发表在 Towards Data Science ·16 分钟阅读·2023 年 3 月 11 日

在当今快速变化的环境中,公司面临的一个最关键的挑战是准确预测未来需求。这对于供应链团队尤其重要,准确的需求规划对于保持客户满意度和控制成本至关重要。

在这个案例研究中,我们将探讨数据科学过程模型如何通过利用统计预测方法,帮助公司实际解决这一挑战。虚拟公司的目标是开发一个更准确的需求规划过程,以减少缺货,提高库存周转率,并改善整体供应链表现。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由Unsplash提供

本项目是数据科学如何通过揭示新见解、提高效率和改善决策来改变业务的强大示例。我希望这个案例研究能帮助你考虑在组织中潜在的应用,并展示如何成功应用过程模型 DASC-PM。

请注意,整篇文章也已在以下出版物中发布,作者为 丹尼尔·巴杜拉 乔纳斯·迪克曼

**第三章:“供应链中材料规划的机器学习模型开发”,见:施尔茨等(2023): DASC-PM v1.1 案例研究。可从:www.researchgate.net/publication/368661660_DASC-PM_v11_Case_Studies获取

1. 领域和项目描述

SCHRAMME AG 是一家领先的敷料、创可贴和绷带供应商。管理层认为在材料规划及其产生的生产过程中存在定性优化潜力和节省机会。管理层指派了一名内部项目经理开发一个基于机器学习的模型,以规划供应链中的材料和需求。由于之前的数据科学项目中的负面经验,建议初期应使用过程模型来开发此项目。

选择DASC-PM以确保项目管理的结构化和科学化过程。为了获得项目任务的概述,项目经理最初制定了各种用例,然后检查其适用性和可行性。适用的用例将作为确定具体问题和设计项目的基础。随后,这一设计将再次检查其适用性和可行性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由Unsplash提供

起始点和用例开发

目前,公司手动规划并生产超过 2500 种不同的产品。在过去几个季度,公司在一些产品系列中越来越频繁地出现库存短缺,而个别产品的库存则超过了储存能力。尽管控制部门抱怨由于不精确的规划导致库存成本上升,需求规划师却感叹规划时间不足。供应链负责人已经批评规划完全依赖人工,未能充分利用数字化的机会。

项目目标

该项目的一个目标是开发一个机器学习模型,未来应根据各种影响因素自动规划大量产品需求。需求规划师应逐渐关注重要产品组和广告的规划。系统应考虑季节性、趋势和市场发展,达到 75%的规划准确度。这意味着每种产品的预测数量与实际需求的偏差不应超过 25%。订单历史、库存和客户销售数据,以及内部广告计划应作为潜在的数据来源。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

阶段 1: 项目订单(Schulz et al. 2022)

当前团队设置

除了供应链部门的参与外,还期望与销售和 IT 部门进行紧密合作。供应链部门的规划团队现在包括一个全球市场需求规划团队,负责根据市场发展、产品生命周期和战略重点进行长期规划(6-18 个月)。在个别市场中,还有地方客户需求规划团队,通过相应的销售渠道为零售实施短期物料和广告规划(0-6 个月)。

需要开发的数据科学模型应支持每月的规划周期,并量化短期和长期物料的需求。预测结果随后被加载到内部规划软件中,并应进行分析,如有必要,进行补充或修正。最终的规划数量将由工厂用于生产规划。为了考虑客户和产品的专业知识、季节性和过往经验,规划团队的个别成员应被纳入项目中,分配最多 20%的工作时间参与其中。

适用性检查

在用例选择过程中,一个重要的部分方面是适用性测试。项目经理试图检验项目是否从根本上可以被分类为可行,以及是否可以利用现有资源执行这些要求。专家访谈表明,该问题总体上非常适合数据科学的应用,并且类似的项目已经在外部进行并发布。数据科学团队确认有足够数量的潜在适用方法用于该项目,并且所需的数据源也可用。

最后,项目经理分析可行性。需要与 IT 部门协调,检查可用的基础设施和相关员工的专业知识。微软提供的云基础设施和数据科学团队对 Databricks 软件的经验使得项目看起来在基础上是可行的。由于计划者在实施阶段担任主要控制者,结果会被检查,因此项目风险总体上被分类为中等。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

数据科学过程模型 DASC-PM(Schulz 等,2022)

项目设计

根据问题和领域的具体方面,项目经理、供应链负责人以及数据科学家现在负责正式设计该项目。

项目的目标被认为是提高计划准确性和减少人工流程,并且与开发适当的项目模型的目标相关。根据初步估算,成本框架总计为 EUR 650,000。建议开发时间框架为六个月,额外计划六个月用于过程整合。

由于与许多其他项目相比,在数据科学背景下通常无法进行全面规划和项目过程描述,项目经理仅为该过程准备了一个项目概要,其中包含前述部分已指明的基本要素。预算包括 1 名全职项目经理、2 名全职数据科学家和 0.5 名全职数据工程师的财政资源。如前所述,需求规划师应分配大约 20%的工作时间来分享他们的专业知识和经验。

整个项目应采用敏捷工作方法,并基于 Scrum 方法论的 DASC-PM 阶段进行处理。工作在数据获取、分析、利用和使用等领域以迭代方式进行,每个阶段都将前一阶段和后一阶段作为重点。如果在关键领域发现差距或问题,并且只能通过回到前一个阶段来解决,则回溯步骤尤为重要。项目概要以可视化的方式准备,并放置在 SCHRAMME AG 办公室中所有参与者都能看到的位置。然后,整个项目描述会再次检查其适用性和可行性,直到过程进入下一阶段。

2. 数据提供

数据准备

SCHRAMME AG 拥有多个可以纳入自动规划的数据源。除了 ERP 系统中的历史销售数据外,还可以选择来自 CRM 系统的订单历史和客户数据,以及库存和营销措施。Azure Data Factory 被用来准备一个基于云的数据管道,加载、转换和集成来自各种源系统的数据。自动预测的主要基础应该是订单历史:其余数据要么作为规划团队的背景信息使用,要么在需要时进行集群分析。在项目的初始阶段,各个数据源在质量和结构上仍存在较大差异。因此,与 IT 和技术部门一起进行调整,以便在后续阶段能够在坚实的基础上进行预测。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

ELT 数据准备过程用于分析。图像由作者提供

数据管理

数据管理过程由数据工程师自动化,并根据每日计划进行,以始终保持最新状态。为了保持复杂度在合理范围内,最有前景的数据源会首先进行处理,随后通过持续集成/持续部署(CI/CD)逐步扩展数据管道。部署后,处理过的数据会存储在 Azure Data Lake Storage 中,供未来使用 Azure Databricks 进行分析。DataLake 还存储准备好的数据和分析结果的备份,以及其他数据如协议、质量指标和凭证结构。写入和读取授权以及计划版本也确保只能处理最新的规划周期,使得过去的值不再发生变化。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

阶段 2:数据提供(Schulz 等,2022)

探索性数据分析

数据准备中的一个重要步骤是探索性数据分析(EDA),在这个步骤中会生成各种统计数据和可视化图表。结果展示了数据中的分布、离群值和相关性。EDA 的结果提供了下一阶段分析中需要考虑的特征的见解。在第二步中,使用特征选择和特征工程来选择相关特征或生成新特征。对于高维数据,应用主成分分析等降维方法。EDA 提供了关于 SCHRAMMEAG 现有需求历史的信息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

探索性数据分析的结果示例。图片由作者提供

3. 分析

识别合适的分析方法

项目开始时的可行性测试明确了该项目可以并应该使用数据科学方法解决。最初参与的两名数据科学员工提供了适合现有问题的现有方法概述。这个现有问题属于监督学习算法中的回归问题类别。从根本上讲,这是一种时间序列分析类型,可以通过额外因素或多重回归进行扩展。

在科学性关键领域的背景下,考察了对比问题的最新研究进展。这显示出 XGBoost、ARIMA、FacebookProphet 和 LightGBM 是该问题类别中经常提到的方法。一名数据科学家记录了每种方法的相应优缺点,并根据复杂性和计算强度对其进行排序。为了获得关于 SCHRAMME AG 产品模型能力的初步指示,项目组初步选择了更简单的模型,然后采用了经典的指数平滑和 ARIMA 模型系列。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第三阶段:分析(Schulz 等人 2022)

分析方法的应用

由于该项目涉及多个用户参与分析过程,团队最初依赖于 Databricks 中的合适笔记本开发环境。按照典型的机器学习工作流程,首先实现了导入和数据清洗的代码。为了确保有效性,最终通过交叉验证将基础数据集划分为训练、验证和测试数据。然后,将选定的方法应用于训练和验证数据集,以优化模型。在此过程中,还反复尝试优化处理参数,并在必要时合理减少可用维度。SCHRAMME AG 的数据科学家记录了各个运行的执行和验证结果。尽管 ARIMA 系列模型在相对指数平滑方面表现出更好的性能,但目前得到的 62.4%的目标准确性仍未达到 75%。RMSE 和 MAPE 指标也显示出优化的潜力。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

ARIMA 预测与实际需求的对比。图片作者提供

参数配置和选择最终模型的基础在第一次应用迭代后被记录并以技术上可理解的方式为项目经理和供应链负责人准备。特别观察到的是,一些产品组具有非常不寻常的季节性,某些产品总体上非常难以预测。即使 SCHRAMME AG 的产品组合由于冠状病毒大流行期间的临时关闭(封锁)受到的影响较小,但仍观察到调味品产品的需求略有下降。假设活动和运输减少,以及事故和伤害减少是导致这一下降的原因。

趋势可以在使用的分析方法中建模得相当好。为了提高目标准确性,在另一个实验中使用了技术上更复杂的方法,这些方法在识别合适方法的过程中证明是相关和适用的。在进行了一些参数优化和交叉验证迭代后,Prophet 和 XGBoost 方法分别展示了 73.4%和 65.8%的最高验证结果。

数据科学家认为 Prophet 是应用过程中最合适的方法,并根据测试时间序列确定规划准确性。即使准确性略低于目标值 73.4%,仍然取得了显著的规划准确性改进。MAPE 为 16.64%,RMSE 为 8,130,这表明与 XGBoost 方法中的 RMSE(10,134)相比,绝对偏差较小。然而,与第一次实验类似,仍然存在一些非常难以整体预测的产品组(37.2%),对累计准确性产生了负面影响。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

各种方法的性能比较。作者图像

评估

分析结果作为供应链负责人和分析师逻辑评估和分类的基础,由项目经理组织和主持。采用的评估指标是所有预先定义产品的累积计划准确性以及常用的 RMSE 和 MAPE 指标。部门需要一个现实、可追踪和可靠的基础来确定产品级别的需求。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

三个最佳模型的评估。作者图像

计划准确性的基准假设为过去两年中(手动计划的)中位准确率 58%。结果的评估显示,通过使用数据科学模型,许多产品组整体上可以以较高的准确性进行规划,并大大超过了基准。然而,也有一些产品组在手动规划方面表现出类似的准确性。尤其需要讨论的是排水领域,该领域使用模型的结果远差于手动规划,似乎不适合使用目前的方法进行统计需求计算。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最佳模型的评估,按产品组分布。作者图像

从技术角度来看,供应链负责人认为由于特定的季节性和趋势性特征,仅能实现有限的计划准确性,因此对这些产品组进行统计规划没有多大意义。她建议引入一个产品基础上的误差阈值,以确定哪些产品应通过模型进行预测,哪些产品组将从建模中剔除并仍由人工计划。略低于当前基准的范围似乎是一个合适的阈值,因为从部门的角度来看,较少的人工干预能带来几乎相同的准确性,这总是朝着实现项目目标的方向上的一种改进。项目负责人记录了评估结果以及所采取的决策和措施。

经过首次实际建模后,所有选择的产品在接下来的 18 个月所需的数量可以作为分析结果进行记录。现在可以将其利用并整合到团队的规划过程中。

4. 部署

团队现在进入 DASC-PM 的整合利用阶段。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第 4 阶段:部署(Schulz 等人 2022)

技术方法准备

可以依赖现有基础设施进行利用。预测数据被加载到规划软件 IBM Planning Analytics 中,在那里进行测试和重新处理。所谓的 TurboIntegrator 被用来自动化加载过程,这是 IBM Planning Analytics 的一个核心组件。Planning Analytics 的 OLAP 结构允许创建灵活的视图,用户可以选择他们的上下文(时间参考、产品组等)并实时调整计算。此外,报告软件 QlikSense 也被集成用于更深入的分析。在这里,一方面可以可视化时间序列的组件(趋势、季节性、噪声),另一方面可以显示诸如异常值和中位数等附加信息。处理后的最终计划会被加载到数据湖中,以便未来参考。

确保技术可行性

预测本身会在每月初自动重新生成。规划人员可以在月初的前四个工作日内进行修正,并实时查看规划系统中的结果。由于算法在云环境中运行,计算能力可以根据需要进行扩展。为了使所有过程自动运行,应尽量减少数据源的变化。如果需要调整,数据工程师将会被通知,并通过记录所有数据源和连接的信息来更新接口文档。规划和预测系统是云(Microsoft Azure)和本地系统(Planning Analytics)的混合体,规划人员仅对本地结构拥有主动访问权限。这里授予凭证,使本地规划人员仅能访问他们的区域,而全球规划人员可以查看所有主题。开发阶段结束后,支持服务主要由 IT 部门处理。在复杂问题的情况下,还会咨询数据科学家或数据工程师。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来自 Unsplash

确保适用性

解决方案的用户包括本地和全球规划团队。由于团队成员技术背景较少,举办培训课程帮助他们解读预测结果并评估其质量。用户界面也以清晰和易懂为设计重点。使用简单的折线图和条形图来展示过程和基准,同时表格内容精简到最重要的信息。用户从一开始就参与开发,以确保技术的正确性和相关性,并在开发阶段结束前熟悉解决方案。此外,还会编写完整的文档。文档的技术部分主要基于接口文档,展示数据结构和连接,而内容部分则与用户共同准备。

技术准备

为了确保新解决方案在几个月后不会失去相关性或质量,尽管投入的时间大幅减少,工作仍继续进行改进。持续改进中最重要的方面是不断自动调整预测模型以适应新数据。系统中在开始时仍需手动处理的其他部分也会随着时间的推移实现自动化。规划人员可以在 Planning Analytics 中调整诸如预测范围或预测准确度阈值等参数,模型保持灵活。发布首个版本后出现的问题通过 IT 票务系统记录,并分配给数据科学领域。定期检查模型是否仍满足公司的期望,是否需要进行更改。

5. (应用)使用和总结

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第 5 阶段:应用(Schulz 等,2022 年)

过渡到使用开发的模型意味着数据科学过程模型(DASC-PM)进入其最后阶段。SCHRAMME AG 通过使用结构化和整体的方法,在供应链领域实现了其设定的目标。现在可以从中衍生出额外或新的项目。规划过程大部分已实现自动化,并得到机器学习算法的支持。管理、财务和供应链中的相关利益相关者对结果感到非常满意。经过最初的怀疑,规划团队现在也对工作负担的减少和可能的优先排序感到信服。然而,也可以预见在使用过程中会出现弱点,并且在后续阶段可能需要更多的迭代。

整体案例研究表明,特别是非线性过程模型在数据科学领域具有优势。DASC-PM 是一种适用于转移到许多其他领域和问题的合适新型过程。

结论

总结来说,数据科学在解决复杂业务问题中扮演着不可或缺的角色,通过识别隐藏的模式并从数据中提取可操作的见解。通过这个案例研究,我们展示了如何利用数据科学技术开发预测模型,帮助企业做出明智的决策,例如在供应链中。

虽然这个案例研究侧重于需求规划,但该过程模型可以用于多种方式,例如在电子商务网站上构建个性化推荐、识别金融交易中的欺诈行为或预测电信或订阅型业务中的客户流失。

然而,必须注意的是,现实世界的数据科学项目面临多个挑战,如数据质量问题、缺乏领域专业知识和利益相关者之间的沟通不畅。相比之下,虚构的案例研究提供了一个理想化的环境,拥有干净、标记良好的数据和明确定义的问题陈述。因此,现实世界的项目需要一种务实的方法,考虑到业务目标、数据质量、计算资源和伦理问题。我相信你从自己的经验中知道这一点。不要低估现实!

总之,数据科学具有巨大的潜力来改变行业、社会,并为企业创造新的机会。DASC-DM(或任何)过程模型可以帮助合理地构建方法,以确保对业务利益相关者和项目团队本身的明确指导。

请告诉我你在数据科学项目中的经验。你如何构建这些项目?最大的挑战是什么?欢迎留言!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由Unsplash提供

[## Jonas Dieckmann - Medium

阅读 Jonas Dieckmann 在 Medium 上的文章。分析经理和产品负责人@Philips | 对…充满热情并撰写相关内容

medium.com](https://medium.com/@jonas_dieckmann?source=post_page-----93ae57b682bf--------------------------------)

希望你觉得这有用。告诉我你的想法!同时欢迎在LinkedIn上连接我,或在 Medium 上关注我。

另见我的其他文章:

## 人工智能中的伦理:偏见算法的潜在根源

理解数据偏见的另一种方法

[towardsdatascience.com ## DASC-PM:数据科学项目的新型过程模型

或:如何正确地构建下一个数据科学项目

[towardsdatascience.com

参考文献

整个案例研究已发布于:

[1] Schulz et al. (2023):DASC-PM v1.1 案例研究” 可从:www.researchgate.net/publication/368661660_DASC-PM_v11_Case_Studies 获取

过程图像取自:

[2] Schulz et al. (2022): DASC-PM v1.1 — 数据科学项目的过程模型”*(2022),出版商:NORDAKADEMIE gAG Hochschule der Wirtschaft,ISBN:978–3–00–064898–4,DOI:10.25673/32872.2

案例研究:使用彩虹方法进行实际标签编码

原文:towardsdatascience.com/case-study-practical-label-encoding-with-rainbow-method-d167c386e78?source=collection_archive---------13-----------------------#2023-02-24

在 MassMutual 生产模型上的真实世界测试

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 安娜·阿拉凯良

·

关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 2 月 24 日

德米特罗·卡拉巴什 共同编著

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由 杰森·波加尼克 提供,来源于 Unsplash

在我们之前的文章“隐藏的数据科学瑰宝:用于标签编码的彩虹方法”中,我们讨论了在开发基于树的模型时,使用标签编码而非独热编码的优势。我们介绍了彩虹方法,这有助于确定不同类型的分类变量的最合适的有序编码。

在本文中,我们将继续探讨 Rainbow 方法——这一次,从实际的角度,展示其在MassMutual数据科学团队开发的真实项目中的有效性,MassMutual 是一家知名的寿险公司,致力于推动数据科学家、工程师和技术专家来帮助做出明智的商业决策。

商业用例

目标是预测每个潜在客户的五个思维模式细分中的一个。实质上,这是一个多类别分类问题。

细分框架包括五个类别,反映了一个人的年龄、财务稳定性以及对金融决策的态度。MassMutual 营销团队随后在各种活动中使用预测的细分进行目标定位和定制。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1(由 Anna Arakelyan 绘制)

例如,展现出思维模式 A 的客户倾向于在决定是否购买人寿保险时优先考虑独立性和自主性,而思维模式 B 的客户则通常更愿意从专门的顾问那里获得指导和详细的金融产品解释。

数据代表了一小部分标记个体(17.5K 人),标签由设计了细分分配规则的 MassMutual 供应商提供。我们首先将主潜在客户数据库中的列添加到这些数据中。目标是使用这些目标标签和可用特征学习最佳模型,并预测所有其他(未标记)潜在客户的细分。

我们使用的消费者数据库涵盖了大约 300 列,代表了多种人口统计特征,如家庭组成、收入和净资产倾向、金融行为以及数字敏锐度。

在本文中,我们通过消费数据库和思维模式细分项目,将传统的一热编码与 Rainbow 编码进行比较。我们展示了一些标准指标——如宏观平均 F1 得分、宏观平均 AUC ROC、Cohen’s Kappa 和准确率——用于解释和比较这个 5 类分类问题。

分类变量

我们选择了消费数据库中的所有分类变量——包括区间变量、序数变量和名义变量——但排除了定量变量和二元变量。目的是展示相同分类因素下,一热编码和 Rainbow 编码在模型性能上的差异。

我们进行了目标分层的 4 折交叉验证拆分,并且从这一点开始的所有数据处理都在交叉验证循环内完成。这包括从每个折的训练集创建一热特征和 Rainbow 特征,然后将它们应用于每个折的验证集。

总共 111 个变量被转换为 121 个 Rainbow 特征,另外转换为 2260 个一热特征。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

表 1. 编码前后的变量列表

对区间和序数变量的 Rainbow 转换非常简单,结果是从 64 个区间特征得到了 64 个 Rainbows,从 14 个序数特征得到了 14 个 Rainbows。

名义变量的转换更为复杂,我们为剩下的 10 个变量创建了 23 个自然属性 Rainbows 和 20 个人工 Rainbow 特征。由于我们处理了五个类别,我们对随机类别应用了相关排序和目标百分比排序(见原文的自动化 Rainbow 选择部分)。例如,名义变量“Financial_Cluster”被转换为特征“Financial_Cluster_Mindset_B_correlation_rank”和“Financial_Cluster_Mindset_D_target_percent”。总体而言,33 个名义变量被转换为 43 个 Rainbows。

对于实际排序的选择——无论是自然属性 Rainbow 还是人工 Rainbow——高度依赖于项目和上下文,更多的是艺术而非科学。这需要在模型简洁性、性能和可解释性之间取得平衡。

与序数编码不同,One-hot 转换生成了超过两千个特征。

为什么我们在这里为区间和序数变量制作 One-hot 特征?因为我们希望在从完美顺序到模糊顺序,再到无顺序(或错误顺序)的完整连续体上,将 Rainbow 与 One-hot 进行比较。

此外,将变量分类为序数或名义有时是一种主观决定。一个明显的例子是颜色。正如我们在第一篇文章中讨论的,颜色被一些模型者认为是名义的,而另一些则认为是序数的。

起初,我们将所有类别变量进行汇总,但在文章后面,我们分别分析了区间、序数和名义变量。

我们训练了所有的XGBoost模型,涵盖了下面显示的超参数空间:

params = {
    'objective': 'multi:softprob',
    'eval_metric': 'mlogloss',
    'num_class': 5, 
    'subsample': 0.8, 
    'max_depth': [2, 3, 5], 
    'eta': [0.1, 0.3, 0.5],
    'n_estimators': [50, 100, 200],
}

我们避免将 max_depth 设置得高于 5,因为数据量相对较小,并且每个分支的末端需要至少 100 个样本。我们倾向于使用简单模型,这也有助于防止过拟合。

下面的所有结果表示交叉验证的平均指标。

综合结果

让我们从所有运行的总体平均值开始。显然,对于 Rainbow 编码,所有模型的平均指标更高。总体差异为几个百分点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2(作者生成)

超参数

下面的图示展示了在保持所有其他超参数不变的情况下,每个超参数的指标变化。这些图示也清楚地表明,Rainbow 的结果在每个超参数和指标上都超过了 One-hot 的结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3a(由作者生成)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3b(由作者生成)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3c(由作者生成)

运行时间

接下来,让我们比较每种方法的运行时间。

One-hot: 65.059 s
Rainbow:  5.491 s

运行一个“Rainbow”模型的平均时间几乎是运行一个“One-hot”模型的 12 倍!因此,除了显著提高模型性能指标外,我们还可以看到,“Rainbow”方法可以为数据科学家节省大量时间。

间隔型、序数型和名义型

接下来,我们分别运行了包含间隔型、序数型和名义型特征的模型。结果列在下面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4(由作者生成)

这些结果再次强调了“Rainbow”相比于“One-hot”的优势。正如预期的那样,“Rainbow”编码对间隔型和序数型特征的提升最大,而对名义型变量的提升则较小。

显然,类别顺序越明确,选择“Rainbow”而非“One-hot”的好处就越大。虽然“Rainbow”对名义型变量的表现与“One-hot”相似或略低,但它仍然能以显著较少的时间和空间达到相同的性能水平,生成的模型也显著更简单。

特征选择

最后,为了确保在维度方面的公平比较,我们从每个特征集(Rainbow 和 One-hot)中选择了前 10、50 和 100 个特征。我们利用了XGBoost模型的特征重要性属性,并聚合了四次交叉验证折中的特征重要性分数,以获得每种编码类型的最佳超参数集。结果如下所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5(由作者生成)

“Rainbow”编码轻松超越了“One-hot”编码,特别是在特征数量较少的情况下。“Rainbow”编码比“One-hot”编码更快地达到性能峰值,并且使用的特征更少。实际上,只有 10 个特征时,“Rainbow”编码已经接近其峰值,而“One-hot”编码则需要 50–100 个特征才能达到类似水平!

此外,“Rainbow”编码在 50 个特征上的结果甚至优于“One-hot”编码在 100 个特征上的结果。值得注意的是,当特征数量从 50 降至 10 时,“One-hot”编码的 Macro-F1 降低幅度是“Rainbow”方法的六倍(Kappa 和 Accuracy 降低幅度为三倍,Macro-AUC 降低幅度为两倍)。

结论

MassMutual 的心态分段模型的例子清楚地说明了 Rainbow 标签编码优于 One-hot 编码。不仅为建模人员节省了大量时间,还显著降低了维度,并提供了一个有机的特征选择框架。此外,如果所选的 Rainbow 顺序与数据生成过程一致,那么这种编码还可以显著提升模型性能指标。

CatBoost 回归:为我详细讲解一下

原文:towardsdatascience.com/catboost-regression-break-it-down-for-me-16ed8c6c1eca

CatBoost 内部工作原理的全面(并且有插图)解析

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Shreya Rao

·发表于 Towards Data Science ·阅读时间 14 分钟·2023 年 9 月 2 日

CatBoost,代表类别增强,是一种强大的机器学习算法,在处理类别特征和产生准确预测方面表现出色。传统上,处理类别数据是相当棘手的——需要使用独热编码、标签编码或其他一些可能扭曲数据固有结构的预处理技术。为了解决这个问题,CatBoost 使用了其内置的编码系统,称为 有序目标编码

让我们通过构建一个模型来预测某人如何给书籍 Murder, She Texted 打分,基于他们在 Goodreads 上的平均书籍评分和他们的最爱类别,来看看 CatBoost 在实践中是如何工作的。

我们让 6 个人对 Murder, She Texted 进行评分,并收集了关于他们的其他相关信息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这是我们当前的训练数据集,我们将用它来训练(显而易见)数据。

第 1 步:随机打乱数据集并使用 有序目标编码 对类别数据进行编码

我们处理类别数据的方式对 CatBoost 算法至关重要。在这种情况下,我们只有一个类别列 — Favorite Genre。这个列被编码(即转换为离散整数),具体的编码方式取决于这是回归问题还是分类问题。由于我们处理的是回归问题(因为我们想预测的变量 Murder, She Texted Rating 是连续的),我们按照以下步骤进行。

1 — 随机打乱数据集:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2 — 将连续目标变量分成离散的 :由于我们这里的数据非常少,我们将创建两个相同大小的桶来对目标进行分类。(了解更多关于如何创建桶的内容,请参见 这里)。

我们将Murder, She Texted Rating的 3 个最小值放入桶 0,其余的放入桶 1。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3 — 使用公式对分类列进行编码:Ordered Target Encoding 假设它一次接收一行数据,并使用此公式对最喜欢的类型进行编码:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • curCount = 我们之前见过的拥有相同最喜欢的类型且在评分桶1 中的人数

  • prior = 用户定义的常数值;在我们的例子中设置为 0.05

  • maxCount = 我们之前见过的拥有相同最喜欢的类型的人数

注意:如果我们有更多数据,我们将有更多的桶。我们使用不同的公式来编码分类数据。阅读更多这里

使用这个公式,让我们对第一行进行编码。由于这是第一行,我们假设之前没有数据,这一行是我们唯一的信息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里:

  • curCount = 我们之前见过的在评分桶1 中且最喜欢的类型为神秘的人的数量 = 0

  • maxCount = 我们见过的最喜欢的类型为神秘的人的数量 = 0

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因此,第一行中神秘的编码值是 0.05。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在对于第二行,我们假设唯一的数据是前两行。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • curCount = 我们之前见过的在评分桶1 中且最喜欢的类型为浪漫的人的数量 = 0

  • maxCount = 我们见过的最喜欢的类型为浪漫的人的数量 = 0

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

与第一行类似,第二行的编码值是 0.05。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于第三行:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • curCount = 我们之前见过的在评分桶 1 中且最喜欢的类型为神秘的人的数量 = 0

  • maxCount = 我们见过的最喜欢的类型为神秘的人的数量 = 1

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

类似地,如果我们对剩余的行进行此编码,我们得到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

就是这样,我们对分类变量进行编码。

现在我们可以忽略最喜欢的类型 仅考虑编码后的最喜欢的类型*。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

步骤 2:进行初步预测并计算残差

CatBoost 从对所有行进行初始的Murder, She Texted Rating预测 0 开始。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后我们使用这个公式计算称为残差的东西:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第 3 步:构建 CatBoost 树

现在我们有了残差,可以开始构建 CatBoost 树了。阅读我之前的文章决策树和 XGBoost 可能会对你理解决策树有所帮助。

查找根节点

我们通过比较使用最喜欢的类型(编码)与平均 Goodreads 评分作为根节点的效果,确定树根(第一次拆分)的最佳阈值。

首先,我们需要根据最喜欢的类型确定拆分树的候选节点。为此,我们必须将最喜欢的类型的值按升序排序:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后我们计算最喜欢的类型中相邻值的平均值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们的最喜欢的类型拆分候选值是这些平均值——0.0375、0.05、0.2875 和 0.525。

我们尝试的第一个候选是最喜欢的类型 < 0.0375:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

树的叶节点是绿色的。CatBoost 初始化了一个叫做输出的东西,将拆分的叶节点设置为 0:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果最喜欢的类型小于 0.0375,我们会落在左叶节点;否则,我们会落在右叶节点。当每行数据传递到树中时,其残差被放入叶节点。

所以将第一行数据传递到树中……

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

……我们将其残差放入右叶节点,因为最喜欢的类型为 0.05,大于 0.0375:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后我们跟踪该行的叶节点输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后我们将树中输出的值更新为叶节点中残差值的平均值。在这种情况下,由于叶节点中只有一个残差输出为 3.5。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在将第二行数据传递到树中:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们也将其残差放入右叶节点,因为 0.05 > 0.0375:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

残差最终落在右叶节点

我们存储叶节点输出值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后我们通过计算叶节点中两个残差的平均值来更新输出值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

正确输出:3.5 => 3

现在让我们传递第三行数据:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

残差最终落在左叶节点,因为最喜欢的类型 = 0.025 < 0.0375

跟踪叶节点输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

更新叶节点的输出值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

左侧输出:0 => 4

最后,让我们将最后三行运行在树上。我们得到这棵树…

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

…以及这张表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

存储了叶节点输出值的最终表

量化这个根节点的“好坏”

CatBoost 通过计算 叶节点输出 列和 残差 之间的 余弦相似度 来量化划分的好坏。余弦相似度的公式是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其中 A 和 B 只是我们试图比较的两列。

所以要计算 残差叶节点输出 列的余弦相似度…

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

…我们将相应的值代入公式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们发现余弦相似度为 0.786。因此,阈值 Favorite Genre < 0.0375 的余弦相似度为 0.786。

现在使用与上述相同的过程,我们构建一个使用第二个候选根阈值的树:Favorite Genre < 0.05。重复相同的过程,我们得到这棵树:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

…以及这张表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

…余弦相似度为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意:这与我们使用阈值 Favorite Genre < 0.0375 得到的值相同,因为残差落在相同的叶节点中。

让我们尝试下一个根阈值候选项:Favorite Genre < 0.2875。我们得到的树是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

…以及这张表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

…并且残差叶节点输出的余弦相似度为 0.84。在这里,由于余弦相似度大于其他两个阈值的相似度,我们得出结论:Favorite Genre < 0.2875 是比 Favorite Genre < 0.0375 和 Favorite Genre < 0.05 更好的根节点划分。

现在,我们来进行最后的划分:Favorite Genre < 0.525。我们得到余弦相似度为 0.84,这使我们得出结论:0.2875 和 0.525 的划分效果相似。

但请记住,我们只测试了 Favorite Genre 的候选项。 接下来,我们需要测试 Average Goodreads Rating 的根节点候选项。为此,我们需要通过将列按升序排列并计算相邻的平均值来确定 Average Goodreads Rating 的划分候选项。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于每一个平均值,我们构建一棵树并计算 叶节点输出残差 之间的余弦相似度:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

比较所有候选根节点的余弦相似度值,我们发现Average Goodreads Rating < 3.65 的余弦相似度最高,为 0.87。所以我们选择这个作为我们的根节点分割

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

获得根节点后,我们可以通过添加新分支来扩展树。为此,我们遵循与之前类似的过程,但不是选择根节点,而是选择从叶子处分裂出来的分支。选择具有最高余弦相似度值的分割。

CatBoost 树的一个注意事项是它们是对称的,意味着同一层上的每个分支使用相同的阈值。

注意:如果你对为什么要构建对称树感到好奇,你可以在这里阅读更多内容。

一个示例是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这种情况下,同一层的两个节点使用相同的分割。

由于我们数据很少,最好只构建深度为 1 的树。

注意:树的深度是我们可以调整的模型参数,主要用于避免过拟合。在大多数情况下,最佳深度范围是 4 到 10,建议使用 6 到 10 的值。

就这样,我们有了第一棵树!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

步骤 4:进行新的预测

现在我们使用旧的预测和这个公式进行新的预测:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

学习率是另一个我们可以调整以避免过拟合的参数。有关更多信息,请阅读这里。现在,让我们将其设置为 0.1。

让我们回到我们的表格。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用公式,我们可以计算新的预测。对于第一行,新预测将是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

同样,如果我们计算其余行的新预测值,我们得到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们可以看到我们的新预测不够准确,因为它们与Murder, She Texted的实际评分仍然有显著差异。然而,相比于之前全为零的预测,已有所改善。

我们的下一步是构建一棵新树。但在此之前,让我们先快速清理一下嘈杂的数据集,以便更容易处理。我们可以忽略旧的预测、残差列和叶子输出列…

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

…将“新预测”列重命名为“预测”(因为它不再是新的了)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

…并将编码的最喜欢的类型的值替换为我们原始的最喜欢的类型

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

步骤 5:使用步骤 1-4 构建新树

现在我们重复构建第一棵树时所做的相同步骤来构建第二棵树。

使用步骤 1,我们打乱数据集…

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

…并使用有序目标编码对类别数据(即最喜欢的类型)进行编码:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

按照步骤 2,因为我们已经有了预测,所以不需要进行初步预测。我们只需使用上述相同的公式计算残差

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们得到以下残差

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后我们使用步骤 3构建第二棵 CatBoost 树。假设在测试所有编码后的最喜欢类型平均 Goodreads 评分候选项后,我们找到的最佳根节点是编码后的最喜欢类型 < 0.288。由于树的深度为 1,如之前设置的,我们最终得到这样的树:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

…以及带有叶节点输出的更新表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最后,使用步骤 4,我们进行新的预测。使用这个公式…

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

…我们得到新的预测:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们可以看到,新预测稍微比旧的预测更好。如果我们继续这个过程并构建更多的树,我们的预测会越来越好。

注意:我们继续构建树,直到我们的预测足够好,或者直到达到我们可以设置的树的数量参数值。

使用我们的 CatBoost 树进行预测

假设我们用上面这两棵树完成了模型构建过程。(默认情况下,CatBoost 构建 1000 棵树)。我们现在有一个 CatBoost 模型(显然,由于我们只有 2 棵树,它不会接近一个好的模型),我们可以开始进行预测。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在使用我们的模型,我们想要预测这两个人会如何评分Murder, She Texted

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

首先,我们需要对类别数据进行编码——最喜欢的类型。编码新数据的过程类似于我们编码训练数据的方式;唯一的区别是我们使用整个训练数据集进行编码。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Murder, She Texted Rating分配到之前使用的相同评分桶中:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们使用上述相同的公式进行编码:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然而,我们使用整个训练数据集,而不是像训练过程中那样按顺序处理。例如,最喜欢的类型为神秘剧的编码将是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

同样,其他最喜欢的类型的编码是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们在新数据集中替换了编码值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在对于第一个人,我们回到我们的树,并将数据传递下去:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后我们使用这个公式来进行预测:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所以我们的预测是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当然,这是一个糟糕的预测。但请记住,这是一个相当糟糕的模型。我们拥有的树越多,我们的模型表现就会越好。

同样,对于第二个人:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

就这些了。这就是我们如何构建 CatBoost 树并利用它们对新数据进行预测!

除非另有说明,所有图片均由作者提供

你可以通过LinkedIn与我联系,或者通过shreya.statistics@gmail.com发送电子邮件给我,提出问题和建议,尤其是对任何你希望我讲解的其他算法!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值