初学者指南:必知的 LightGBM 超参数
原文:
towardsdatascience.com/beginners-guide-to-the-must-know-lightgbm-hyperparameters-a0005a812702
最重要的 LightGBM 参数,它们的作用以及如何调整它们
·发布于 Towards Data Science ·5 分钟阅读·2023 年 3 月 7 日
–
调整 LightGBM 超参数的旋钮(图片来源:作者)
LightGBM 是一个流行的梯度提升框架。通常,你将开始指定以下 核心参数:
-
objective
和metric
用于问题设置 -
seed
用于可复现性 -
verbose
用于调试 -
num_iterations
、learning_rate
和early_stopping_round
用于训练
但接下来该怎么做呢?LightGBM 具有超过 100 个可以调整的参数 [2]。此外,每个参数还有一个或多个别名,这使得初学者很难清晰地了解重要参数。
因此,本文讨论了最重要和最常用的 LightGBM 超参数,列举如下:
-
树的形状 —
num_leaves
和max_depth
-
树的生长 —
min_data_in_leaf
和min_gain_to_split
-
数据采样 —
bagging_fraction
、bagging_freq
和feature_fraction
-
正则化 —
lambda_l1
和lambda_l2
在以下内容中,默认值取自 文档 [2],并且超参数调优的推荐范围参考了 文章 [5] 以及书籍 [1] 和 [4]。
树的形状
与 XGBoost 相比,LightGBM 以 叶子为单位 生长决策树,而不是按层生长。你可以使用 num_leaves
和 max_depth
来控制单棵树的大小。
使用 num_leaves 和 max_depth 指定 LightGBM 树形状(作者提供的图片)
参数 num_leaves
控制树中叶子的最大数量 [2]。
-
默认值:31
-
基线的良好起点:16
-
调整范围:(8,256),
num_leaves < 2^(max_depth)
[3]
参数 max_depth
控制树模型的最大深度 [2]。
-
默认值:-1(无限制)
-
基线的良好起点:默认
-
调整范围:(3,16)
树越小(num_leaves
和 max_depth
较小),训练速度越快——但这也可能降低准确性 [3]。
由于 num_leaves
对 LGBM 中的树生长影响大于 max_depth
[5],Morohashi [4] 不一定推荐调整该参数并偏离默认值。
树的生长
除了深度和叶子数量外,你还可以指定叶子分裂的条件。因此,你可以指定树的生长方式。
使用 min_data_in_leaf
和 min_gain_to_split
指定 LightGBM 树的生长(作者提供的图片)
参数 min_data_in_leaf
指定一个叶子中最少的数据点数 [2]。如果该参数过小,模型将过拟合训练数据 [2]。
-
默认值:20
-
基线的良好起点:默认
-
调整范围:(5,300),但取决于数据集的大小。对于大型数据集,数百个已经足够 [3]。经验法则:数据集越大,
min_data_in_leaf
越大。
参数 min_gain_to_split
指定叶子进行分裂所需的最小增益 [2]。
-
默认值:0
-
基线的良好起点:默认
-
调整范围:(0,15)
如果通过增加参数 min_gain_to_split
来限制树的生长,得到的较小树将导致训练时间更快——但这也可能降低准确性 [3]。
数据采样
数据采样是一种强制模型泛化的技术。总体思路是每次迭代时不给模型提供所有数据。相反,模型每次迭代只会看到训练数据的一部分。
Bagging
每隔 bagging_freq
次迭代,LGBM 将随机选择 bagging_fraction * 100 %
的数据用于下一个 bagging_freq
次迭代 [2]。例如,如果 bagging_fraction = 0.8
和 bagging_freq = 2
,LGBM 将在每个第二次迭代前抽样 80% 的训练数据。
该技术可用于加快训练速度 [2]。
-
默认值:
bagging_fraction = 1.0
和bagging_freq = 0
(禁用) -
基线的良好起点:
bagging_fraction = 0.9
和bagging_freq = 1
-
调整范围:
bagging_fraction
(0.5,1)
使用 bagging_fraction = 0.8 进行 LightGBM 的 bagging(作者提供的图片)
子特征采样
在每次迭代中,LGBM 将随机选择 feature_fraction * 100 %
的数据[2]。例如,如果feature_fraction = 0.8
,LGBM 将在训练每棵树之前抽样 80 % 的特征。
-
默认:1
-
基线的良好起点:0.9
-
调优范围:(0.5, 1)
LightGBM 中的子特征抽样,feature_fraction = 0.8(图源:作者)
虽然子特征抽样也可以像袋装方法[2]一样加速训练,但如果特征中存在多重共线性,它也能有所帮助[1]。
正则化
你可以将正则化技术应用于你的机器学习模型,以处理过拟合。正如参数名称所示,参数lambda_l1
用于 L1 正则化,lambda_l2
用于 L2 正则化。
-
L1 正则化惩罚权重的绝对值,因此对异常值具有鲁棒性
-
L2 正则化惩罚权重的平方和,因此对异常值敏感
你可以选择仅使用这两种正则化中的一种,或者如果你愿意,也可以将它们结合使用。
对于这两个参数,参数值的表现类似:
-
默认:0(禁用)
-
基线的良好起点:默认
-
调优范围:(0.01, 100)
总结
本文为你快速概述了最重要的LightGBM超参数调优。下面你可以找到它们及其推荐调优范围的概览。
最重要的LightGBM超参数及其调优范围概览(图源:作者)。
当然,LightGBM还有许多其他可以使用的超参数。
例如,参数min_sum_hessian_in_leaf
指定一个叶子中的最小 Hessian 和,并且还可以帮助缓解过拟合[2]。当你的数据集不平衡时,还有一个可以调优的参数scale_pos_weight
。或者你可以使用max_bin
指定特征将被分桶的最大数量。
享受这个故事了吗?
免费订阅 以便在我发布新故事时收到通知。
[## 每当 Leonie Monigatti 发布时获取电子邮件。
每当 Leonie Monigatti 发布时获取电子邮件。通过注册,如果你还没有 Medium 账户,你将创建一个…
medium.com](https://medium.com/@iamleonie/subscribe?source=post_page-----a0005a812702--------------------------------)
在 LinkedIn、Twitter和 Kaggle上找我!
参考文献
[1] K. Banachewicz, L. Massaron (2022). 《Kaggle 书》。Packt
[2] LightGBM (2023). 参数(访问于 2023 年 3 月 3 日)
[3] LightGBM (2023). 参数调优(访问于 2023 年 3 月 3 日)
[4] M. Morohashi (2022). Kaggle 中磨练机器学习实战能力。
[5] Bex T. (2021). 2021 年 Kaggler’s Guide to LightGBM 超参数调优与 Optuna(访问于 2023 年 3 月 3 日)
Behind the Millions: Estimating the Scale of Large Language Models
Discussing LLMs like ChatGPT, the underlying costs, and inference optimization approaches
·
Follow Published in Towards Data Science ·13 min read·Mar 31, 2023
–
Photo by Pixabay
感谢Regan Yue,你可以在mp.weixin.qq.com、juejin.cn、segmentfault.com和xie.infoq.cn阅读这篇文章的中文版本!
在近期过去,机器学习被视为一种复杂的、小众的技术,只有少数人能够理解。然而,随着机器学习应用变得越来越强大,公众的兴趣激增,围绕人工智能的内容也变得极其丰富。这一高潮发生在2022 年 11 月,当我们看到 ChatGPT,并在2023 年 3 月 GPT-4 发布时,即使是最怀疑的人也对现代神经网络的能力感到惊讶。
询问 ChatGPT 关于其能力的问题。图片由作者使用ChatGPT创建
尽管这些内容中有些无疑是有价值的,但其中大量内容传播了恐惧和错误信息,例如传播机器人将取代所有人类工作或发现神秘的方法通过神经网络赚取巨额财富。因此,澄清有关机器学习和大型语言模型的误解,并提供有用的信息以帮助人们更好地理解这些技术,变得越来越重要。
本文旨在讨论现代机器学习中一个常被忽视或误解的关键方面——训练大型语言模型的成本。同时,我们将简要了解什么是 LLM 以及一些可能的优化推理技术。通过提供详细的示例,我希望能让你相信这些技术并非凭空而来。通过了解数据的规模和底层计算,你将更好地理解这些强大的工具。
我主要依赖于最近的LLaMA 论文,由 Meta AI 发布,因为它清楚地说明了团队用于训练这些模型的数据量和计算资源。本文将分为以下几个部分:
-
首先,我们将简要了解现代大型语言模型是什么;
-
接着,我们讨论训练这些模型的成本;
-
最后,我们将简要考虑一些优化语言模型推理的流行技术。
请继续关注,我们将深入探讨大型语言模型的世界,你会发现一切既非常简单又非常复杂。
大型语言模型简介
在我们深入探讨训练大型语言模型(LLMs)相关的成本之前,让我们先简要定义一下什么是语言模型。
2018-2019 年发布的几种语言模型的参数计数。现代 LLM 通常有数十亿到数百亿的参数。图 1 来自DistilBERT 论文
简单来说,语言模型是一种旨在理解或生成自然语言的机器学习算法。最近,生成模型变得越来越受欢迎 —— 由 OpenAI 开发的GPT 模型系列:ChatGPT、GPT-4 等(代表生成预训练变换器,基于变换器架构)。
虽然不太流行,但仍然重要的例子包括GPT-3 (175B)、BLOOM (176B)、Gopher (280B)、Chinchilla (70B)和LLaMA (65B),其中B指十亿个参数,尽管这些模型中许多也有较小的版本。
关于 ChatGPT 特别是 GPT-4 的参数数量一无所知,但看起来这些数量大致相同。
一些流行的 LLM 架构。图像由作者提供
这些模型通过使用大量的文本数据进行“训练”,使它们能够学习自然语言的复杂模式和结构。然而,它们在训练过程中解决的任务非常简单:它们只是预测序列中的下一个词(或标记)。
你可能听说过这样一种模型叫做自回归,这意味着它使用过去的输出作为未来预测的输入,并一步一步生成输出。这可以在 ChatGPT 的示例中看到:
GhatGPT 生成了一个响应。Gif 由作者使用ChatGPT创建
你可以注意到模型生成答案是逐渐的和分块的,这些块有时少于一个词。这些块称为标记,它们在 NLP 中非常有用,尽管现在对我们来说不那么重要。
在每个时间步骤,模型将之前的输出连接到当前输入,并继续生成。它一直这样做,直到达到特殊的*序列结束(EOS)*标记。省略提示任务并将单词作为标记,为简便起见,过程可以如下所示。
为自回归模型生成文本的示意图。图像由作者提供
这个简单的机制加上大量的数据(比任何人几辈子能读的还多)使得模型能够生成连贯且上下文适宜的文本,模拟人类的写作。
注意,这里我们仅讨论生成模型。如果还有其他模型家族呢?
原因很简单——文本生成任务是最困难的任务之一,同时也是最令人印象深刻的任务之一。ChatGPT 在仅仅 5 天内获得了 100 万用户——比之前任何其他应用都要快,并且以相同的势头继续发展。
所谓的编码器(BERT模型家族)可能不那么令人兴奋,但它们也能以人类水平的表现解决各种问题,并帮助你完成诸如文本分类或命名实体识别(NER)等任务。
我不会提供 LLMs 可以做的具体例子——互联网已经充满了这些例子。最好的方法是亲自尝试 ChatGPT,但你也可以找到很多令人兴奋的资源,比如Awesome ChatGPT prompts repo。尽管其能力令人印象深刻,但当前的大型语言模型仍有一些限制。最流行且显著的包括:
-
偏见和静态性:由于 LLMs 是在来自各种来源的数据上训练的,它们不可避免地学习并复制了这些来源中的偏见。它们在某种意义上也具有静态性,即无法适应新数据或实时更新其知识,除非重新训练。
-
理解和虚假信息:尽管 LLMs 可以生成类似人类的文本,但它们可能并不总是完全理解输入的上下文。此外,生成输出文本的自回归方式并不禁止模型生成虚假或无意义的信息。
-
资源密集型:训练 LLMs 需要大量的计算资源,这转化为高成本和能源消耗。这个因素可能限制了 LLMs 对较小组织或个人研究人员的可及性。
这些及其他缺陷是研究社区的活跃话题。值得注意的是,该领域发展如此之快,以至于几个月内无法预测哪些限制会被克服——但毫无疑问,新的限制将会出现。
一个可能的例子是早期模型简单地增加了参数数量,但现在认为训练较小的模型更长时间,并提供更多的数据更为有效。这减少了模型的大小及其在推理过程中进一步使用的成本。
在了解了 LLM 的整体概况后,让我们进入本文的主要部分——估算训练大型语言模型的成本。
估算机器学习模型的一般成本及 LLM 的特殊成本
要估算训练大型语言模型的成本,必须考虑任何机器学习算法包含的三个关键因素:
-
数据,
-
计算资源,以及
-
架构(或算法本身)。
让我们深入探讨这些方面,以更好地理解它们对训练成本的影响。
数据
LLM 需要大量的数据来学习自然语言的模式和结构。估算数据的成本可能具有挑战性,因为公司通常会使用通过业务操作积累的数据以及开源数据集。
此外,数据需要被清洗、标注、组织和高效存储,考虑到 LLM 的规模。数据管理和处理成本可以迅速累积,特别是当考虑到这些任务所需的基础设施、工具和数据工程师时。
以一个具体例子为例,已知 LLaMA 使用了包含1.4 万亿个标记的训练数据集,总大小为4.6TB!
LLaMA 模型的训练数据集,来源于LLaMA 论文
较小的模型(7B 和 13B)在 1T 标记上进行训练,而较大的模型(33B 和 65B)则使用了 1.4T 标记的完整数据集。
LLaMA 模型的训练损失图,来源于LLaMA 论文
我认为现在你明白了,当人们称这些数据集为庞大时,并没有夸大其词,也明白了为什么在十年前技术上无法实现这些。但计算资源的情况更为有趣。
计算
实际的训练过程占据了 LLM 预算的一大部分。训练大型语言模型是资源密集型的,需要在强大的图形处理单元(GPU)上进行,因为 GPU 具有显著的并行处理能力。NVIDIA 每年发布新 GPU,其成本高达数十万美元。
训练这些模型的云计算服务成本可能非常高,达到数百万美元,尤其是考虑到需要通过各种配置进行迭代。
回到 LLaMA 论文,作者报告他们用两千个每个 80 GB RAM 的 GPU 训练最大的 65B 模型 21 天。
训练 LLaMA 模型所需的计算资源。图片来源于 LLaMA 论文
NVIDIA A100 GPU 是现代神经网络训练的热门选择。Google Cloud Platform 提供这种 GPU,每小时 $3.93。
NVIDIA A100 GPU 的价格。截图来源于 公共 GCP 定价页面
那么让我们做一些快速计算:
2048 GPUs x $3.93 GPU 每小时 x 24 小时 x 21 天 =
405 万美元
四百万美元是并非每个研究人员都能承担的预算,对吧?而且这是单次运行!再举一个例子,这篇文章估算了训练 GPT-3 的成本,作者得出了355 GPU 年和 460 万美元。
你可能听说过“神经网络在 GPU 上训练得非常快”,但没有人说明相对于什么。
考虑到巨大的计算量,它们的训练速度确实很快,没有这些 GPU,它们可能要训练几十年。因此,21 天对 LLM 来说确实很快。
架构(和基础设施)
最先进的 LLM 的开发还依赖于技术娴熟的研究人员和工程师来开发架构并正确配置训练过程。架构是模型的基础,决定了它如何学习和生成文本。
设计、实施和控制这些架构需要计算机科学各个领域的专业知识。负责发布和交付前沿成果的工程师和研究人员的薪资可以达到数十万美元。值得注意的是,LLM 开发所需的技能集可能与“经典”机器学习工程师的技能集差异很大。
机器学习系统基础设施。图 1 来源于 Hidden Technical Debt in Machine Learning Systems paper
我认为现在你不会怀疑训练 LLM 是一个非常困难和资源密集型的工程问题了。
现在让我们简要讨论一些使 LLM 推理过程更高效、成本更低的方法。
优化语言模型以进行推理
我们真的需要优化吗?
推理是指使用训练好的语言模型生成预测或回应的过程,通常作为 API 或 web 服务。鉴于 LLM 的资源密集型特性,优化它们以实现高效推理至关重要。
例如,GPT-3 模型有 1750 亿个参数,占700 GB的 float32 数字。大致相同量的内存也会被激活占用,请记住我们讨论的是 RAM。
为了进行预测而不使用任何优化技术,我们将需要 16 个每个有 80 GB 内存的 A100 GPU!
一些流行的技术可以帮助减少内存需求和模型延迟,包括模型并行、量化等。
模型并行
并行性是一种将单个模型的计算分布到多个 GPU 上的技术,可以在训练和推理过程中使用。
将模型的层或参数拆分到多个设备上可以显著提高整体推理速度,并且在实践中非常常见。
量化
量化涉及减少模型数值值(如权重)的精度。通过将浮点数转换为低精度整数,量化可以在不显著降低模型性能的情况下显著节省内存和加快计算速度。
产生的简单想法是使用float16数字代替float32,将内存减少一半。事实证明,模型权重甚至可以几乎无准确性损失地转换为int8,因为它们在数轴上相互接近。
其他技术
寻找优化 LLM 的方法是一个活跃的研究领域,其他技术包括:
-
知识蒸馏——训练一个较小的学生模型来模仿较大教师模型的行为。
-
参数剪枝——从模型中移除冗余或不重要的参数,以减少模型的大小和计算需求;
-
并使用像ORT (ONNX Runtime)这样的框架,通过算子融合和常量折叠等技术优化计算图。
总体而言,优化大型语言模型以进行推理是其部署的关键方面。通过应用各种优化技术,开发人员可以确保他们的 LLM 不仅强大和准确,而且具有成本效益和可扩展性。
为什么 OpenAI 开放 ChatGPT 访问?
考虑到上述所有因素,人们可能会想知道为什么 OpenAI 决定开放 ChatGPT 的访问权,考虑到训练和推理相关的高成本。虽然我们无法确定公司的确切动机,但我们可以分析这个决定背后的好处和潜在的战略原因。
首先,OpenAI 通过让最先进的 LLM 更广泛地为公众所用,从而获得了显著的知名度(参见AI 革命更多是用户体验革命)。通过展示大型语言模型的实际应用,该公司吸引了投资者、客户和科技界的广泛关注。此外,这使得 OpenAI 能够收集大量反馈和数据以改进他们的模型。
其次,OpenAI 的使命围绕着 AI 的创建和进步。通过开放 ChatGPT 的访问,公司可以说在更接近实现其使命和为不可避免的变化做准备。提供强大 AI 工具的访问促进了创新,推动了 AI 研究领域的发展。这一进展可能会导致更高效的模型、更广泛的应用和各种挑战的创新解决方案。值得注意的是,ChatGPT 和 GPT-4 的架构是封闭的,但这是另一个话题。
虽然训练和维护大型语言模型的成本无疑是巨大的,但对某些组织来说,开放这些工具的好处和战略优势可能会超过费用。以 OpenAI 为例,开放 ChatGPT 的访问不仅提高了他们的知名度,证明了他们在 AI 领域的领先地位,还使他们能够收集更多数据来训练更强大的模型。这一战略使他们能够推进使命,并在某种程度上对 AI 和 LLM 技术的发展做出贡献。
询问 ChatGPT 为何 OpenAI 提供免费访问 ChatGPT。图像由作者使用ChatGPT创建
结论
如我们所见,训练大型语言模型的成本受到各种因素的影响,包括不仅是昂贵的计算资源,还有大数据管理以及开发前沿架构所需的专业知识。
现代的 LLM 拥有数十亿个参数,训练数据量达到万亿个标记,且花费数百万美元。
我希望你现在能更好地理解训练和推理大型语言模型的规模,以及它们的局限性和陷阱。
自几年前以来,自然语言处理领域一直在经历其ImageNet 时刻,现在轮到生成模型了。生成语言模型的广泛应用和采纳有可能彻底改变各个行业和我们生活的各个方面。虽然很难预测这些变化会如何展开,但我们可以确定 LLM 将对世界产生一些影响。
就个人而言,我喜欢最近训练“更聪明”的模型,而不仅仅是“更大”的模型的趋势。通过探索更优雅的方式来开发和部署大型语言模型,我们可以推动人工智能和自然语言处理的边界,为该领域开辟创新解决方案和更加光明的未来。
资源
这里是我关于大型语言模型的其他文章,它们可能对你有用。我已经涵盖了:
-
提示工程最佳实践:如何应用提示工程技术与大型语言模型有效互动,以及如何使用 OpenAI API 和 Streamlit 构建本地大型语言模型应用程序;
-
使用 ChatGPT 进行调试:如何使用大型语言模型进行调试和代码生成。
如果你对大型语言模型感兴趣并想了解更多,这里有一些可以帮助你的资源:
-
插图化 Transformer 是对 Transformer 架构的极好介绍,该架构由 Jay Alammar 引发了自然语言处理领域的重大变革;
-
GPT-3 的工作原理——可视化和动画 是同一作者对自回归解码过程的可视化展示;
-
用 60 行 NumPy 实现 GPT 是 Jay Mody 撰写的一篇精彩文章,作者在其中构建了自己的简单 GPT;
-
让我们构建 GPT:从头开始,用代码详细说明 是著名的 Andrej Karpathy 制作的精彩视频,他曾在 特斯拉自动驾驶担任 AI 高级总监;
-
要深入了解该领域,请查看 Awesome-LLM GitHub 仓库 以获取更详细的资源列表。可以查看 Chinchilla 和 LLaMA 作为近年来最具影响力的论文之一。
感谢阅读!
-
希望这些资料对你有帮助。在 Medium 上关注我以获取更多类似的文章。
-
如果你有任何问题或评论,我很乐意收到任何反馈。可以在评论中问我,或通过 LinkedIn 或 Twitter 与我联系。
-
支持我作为作者并访问成千上万的其他 Medium 文章,请通过 我的推荐链接 获取 Medium 会员(对你没有额外费用)。
深度学习神经网络在图像分类中的幕后故事
这是魔法还是线性代数和微积分?
·
关注 发表在 Towards Data Science ·16 分钟阅读·2023 年 2 月 10 日
–
深度学习神经网络最近受到很多关注,这有充分的理由。它是语音识别、人脸检测、语音控制、自动驾驶汽车、脑肿瘤检测等技术的基础,这些技术在 20 年前还不曾进入我们的生活。尽管这些网络看起来很复杂,但它们的学习方式与人类一样:通过示例。网络通过大量数据集进行训练,并通过多个层次和多次迭代进行优化,以实现最佳结果。在过去 20 年中,计算能力和数据量的指数级增长为深度学习神经网络创造了完美的条件。即使我们在机器学习和人工智能等华丽术语面前感到困惑,但它们不过是线性代数和微积分与计算结合的产物。
像 Keras、PyTorch 和 TensorFlow 这样的框架简化了定制深度网络的构建、训练、验证和部署。这些框架在创建现实生活中的深度学习应用时是显而易见的首选。然而,有时候,退一步思考,真正理解框架背后的运行机制是至关重要的。在本文中,我们将通过仅使用 NumPy 来创建一个网络,并将其应用于图像分类问题。你可能会在计算过程中,尤其是在反向传播的微积分阶段感到迷茫,但不用担心。对过程的直观理解比计算本身更重要,因为框架会处理这些计算。
在本文中,我们将构建一个图像分类(猫或非猫)神经网络,该网络将使用来自两个数据集的 1,652 张图像进行训练:来自狗与猫图像数据集的 852 张猫图像和来自Unsplash 随机图像集合的 800 张随机图像。首先,图像需要被转换为数组,我们将通过将原始尺寸缩小到 128x128 像素来实现这一点,以加快计算速度,因为如果保持原始尺寸,训练模型将花费太长时间。所有这些 128x128 的图像都有三个颜色层(红色、绿色和蓝色),当混合在一起时,能还原图像的原始颜色。每张 128x128 图像上的每个像素都有从 0 到 255 的红色、绿色和蓝色值,这些就是我们图像向量中的值。因此,在我们的计算中,我们将处理 1,652 张图像的 128x128x3 向量。
要将这个向量传递通过网络,需要将其重新调整,将三层颜色堆叠成一个单一的数组,如下图所示。然后我们会得到一个 (49.152,1.652) 的向量,用 1.323 个图像向量来训练模型,使用 331 个图像向量来测试,通过预测图像分类(猫或非猫)来验证训练模型。通过将这些预测与图像的真实分类标签进行比较,就可以估算模型的准确性。
图 1 — 将图像转化为向量的过程。来源:作者。
最后,既然训练向量已经解释完毕,就该谈谈网络的架构,如图 2 所示。由于训练向量中有 49.152 个值,模型的输入层必须具有相同数量的节点(或神经元)。然后,有三个隐藏层,直到输出层,该层表示图片中猫的概率。在实际模型中,通常会有远超过 3 层隐藏层,因为网络需要更深以便在大数据背景下表现良好,但在这篇文章中,仅使用了三层隐藏层,因为它们对于简单的分类模型已经足够。然而,尽管这个架构只有 4 层(输出层不算在内),代码仍然可以应用于创建更深的神经网络,只需将层的维度作为训练函数的参数即可。
图 2 — 网络架构。来源:作者。
现在图像向量和网络架构已经解释完毕,优化算法在图 3 中进行了描述:梯度下降。如果你一开始没有完全理解也不用担心,因为每一步将在文章的编码部分中详细讲解。
图 3 — 训练过程。来源:作者。
首先,我们初始化网络的参数。这些参数是每个节点连接的权重 (w) 和偏置 (b),如图像 2 所示。在代码中,将更容易理解每个权重和偏置参数是如何工作的以及它们如何初始化。随后,初始化这些参数后,就可以运行前向传播模块,并在最后应用 sigmoid 函数以获得概率预测。在我们的案例中,它是猫出现在那张图片中的概率。然后,我们通过交叉熵成本来比较我们的预测与图像的真实标签(猫或非猫),交叉熵成本是优化分类模型的广泛使用的损失函数。最后,计算成本后,我们将其传递回反向传播模块,以计算相对于参数 w 和 b 的梯度。掌握了相对于 w 和 b 的损失函数梯度后,可以通过将相应的梯度相加来更新参数,因为它们指向最小化损失函数的 w 和 b 值。
由于目标是最小化损失函数,因此此循环应经过预定义的迭代次数,以小步向损失函数的最小值逼近。在某些时候,参数将停止变化,因为梯度将趋于零,最小值已经接近。
1. 加载数据
首先,需要加载库。除了 keras.preprocessing.image(用于将图像转换为向量)和 sklearn.model_selection(用于将图像向量拆分为训练和测试向量)之外,只需 Numpy、Pandas 和 OS。
数据必须从两个文件夹加载:cats 和 random images。这可以通过获取所有文件名并构建每个文件的路径来完成。然后,只需将所有文件路径汇总到数据框中,并创建一个条件列 “is_cat”,如果路径在猫文件夹中则值为 1,否则为 0。
拥有路径数据集后,接下来是通过将图像分成 80%用于训练和 20%用于测试来构建我们的训练和测试向量。Y 代表特征的真实标签,而 X 代表图像的 RGB 值,因此 X 被定义为数据框中包含图像文件路径的列,然后使用 load_img 函数加载图像,并将 target_size 设置为 128x128 像素以便加快计算速度。最后,图像使用 img_to_array 函数转换为数组。这些是 X_train 和 X_test 向量的形状:
图像 4 — X_train 和 X_test 的形状。来源:作者。
2. 初始化参数
由于线性函数为z = w*x + b
,网络有 4 层,需要初始化的参数向量包括 w1、w2、w3、w4、b1、b2、b3 和 b4。在代码中,通过遍历层维度列表的长度来完成这项工作,该列表将在后面定义,但它是一个硬编码的列表,包含网络中每一层的神经元数量。
参数w和b必须有不同的初始化:w必须初始化为随机小数矩阵,而b初始化为零矩阵。这是因为如果我们将权重初始化为零,权重对损失函数的导数将全部相同,因此在后续迭代中的值总是相同,隐藏层将全都对称,导致神经元只能学习相同的少量特征。因此,权重被初始化为随机数以打破这种对称性,并允许神经元学习不同的特征。需要注意的是,偏差可以初始化为零,因为权重已经打破了对称性,神经元中的值将全部不同。
最后,要理解参数向量初始化时定义的形状,需要知道权重参与矩阵乘法,而偏差则参与矩阵加法(记住z1 = w1*x + b1
?)。矩阵加法可以在不同大小的数组中进行,因为Python 广播的存在。另一方面,矩阵乘法仅在形状兼容时才可能发生,如(m,n) x (n,k) = (m,k)
,这意味着第一个数组的列数需要与第二个数组的行数匹配,最终矩阵将具有第一个数组的行数和第二个数组的列数。图 5 展示了神经网络中所有参数向量的形状。
图 5 — 参数向量的形状。来源:作者。
在第一层中,由于我们将w1参数向量与原始的 49.152 个输入值相乘,因此需要将w1的形状设置为(20,49.152)
,因为(20,49.152) * (49.152,1.323) = (20,1.323)
,这就是第 1 个隐藏层激活的形状。b1参数将矩阵乘法的结果相加(记住z1 = w1*x + b1
),因此我们可以将一个(20,1)
的数组添加到(20,1.323)
的乘法结果中,因为广播会处理形状不匹配的问题。这种逻辑适用于接下来的层,因此我们可以假设w(l)的形状公式是(层 l+1 的节点数, 层 l 的节点数)
,而b(l)的形状公式是(层 l+1 的节点数, 1)
。**
最后,对于权重向量初始化有一个重要的观察。我们应该将随机初始化的权重除以所在层的节点数的平方根。例如,输入层有 49.152 个节点,所以我们将随机初始化的参数除以√49.152,结果是 222,而第一个隐藏层有 20 个节点,所以我们将随机初始化的 w2 参数除以√20,结果是 4.5。初始化的值必须保持较小,因为这是随机梯度下降的要求。
3. 前向传播
现在参数向量已经初始化,我们可以进行前向传播,这是通过进行线性操作z = w*x + b
,然后进行一个 ReLU 激活,一直到最后一层,最后一层使用 sigmoid 激活替代 ReLU 激活,并获得一个概率作为最后的激活。线性操作的输出通常用字母“z”表示,并称为预激活参数。因此,预激活参数z将成为 ReLU 和 sigmoid 激活的输入。
在输入层之后,对给定层 L 上的线性操作将为z[L] = w[L] * A[L-1] + b[L]
,使用前一层的激活值而不是数据输入 x。线性操作和激活的参数都将存储在缓存列表中,以便作为后续反向传播块中计算梯度的输入。
现在首先定义线性前向函数:
现在必须定义 Sigmoid 和 ReLU 函数。图 6 展示了这两个函数的图表。Sigmoid 激活通常用于两类分类问题,以预测二进制变量的概率。这是因为 S 形曲线使大部分值接近 0 或 1。因此,我们只会在网络的最后一层使用 sigmoid 激活来预测图片中是否有猫的概率。
另一方面,如果 ReLU 函数的输出为正,则直接输出输入,否则输出为零。这是一个非常简单的操作,因为它没有任何指数运算,并且有助于加快内层的计算速度。此外,使用 ReLU 作为激活函数减少了梯度消失问题的可能性,与 tanh 和 sigmoid 函数不同。
ReLU 激活使得不是所有的节点同时被激活,因为负值在激活后会被变为零。网络中有一些 0 值很重要,因为它增加了神经网络所需的特性:稀疏性,意味着网络具有更好的预测能力和更少的过拟合。毕竟,神经元正在处理有意义的信息部分。例如,在我们的示例中,可能有一个特定的神经元可以识别猫的耳朵,如果图像是人类或风景,这个神经元显然应该被设置为 0。
图像 6 — Sigmoid 和 ReLU 函数。来源:作者。
现在可以实现完整的激活函数了。
最后,是时候根据计划的网络架构整合激活函数了。首先,创建缓存列表,将第一个激活设置为数据输入(训练向量),由于网络中有两个参数(w 和 b),层数可以定义为参数字典长度的一半。然后,函数循环遍历所有层,除了最后一层,应用线性前向函数,然后是 ReLU 激活,最后在网络的最后一层用最终的线性前向传播和 sigmoid 激活生成预测概率,即最后的激活。
4. 交叉熵损失
损失函数通过将预测概率(最后激活的结果)与图像的真实标签进行比较,量化模型在给定数据上的表现。如果网络在学习数据,成本(损失函数的结果)应该在每次迭代后下降。在分类问题中,交叉熵损失函数常用于优化,其公式见下图像 6:
图像 7 — 神经网络的成本。来源:作者。
使用 NumPy 定义交叉熵成本函数:
5. 反向传播
在反向传播模块中,我们应该从右到左遍历网络,计算相对于损失函数的参数梯度,然后进行更新。就像在前向传播模块一样,首先呈现线性反向传播,然后是 sigmoid 和 relu,最后一个函数将整合所有函数以适应网络架构。
对于给定的层 L,线性部分是 z[L] = w[L] * A[L-1] + b[L]
. 假设你已经计算了导数 dZ[L],即线性输出的成本导数。其公式将很快呈现,但首先让我们查看图像 8 中呈现的 dW[L]、dA[L-1] 和 db[L] 的导数公式,以便首先实现线性反向函数。
图 8 — 成本相对于权重、偏置和之前激活的导数。来源:作者。
这些公式是交叉熵成本函数相对于权重、偏置和之前激活 (a[L-1]) 的导数。本文不会详细介绍导数计算,但可以参考 这篇 Towards Data Science 文章。
定义线性反向传播函数时需要使用 dZ 作为输入,因为在反向传播中,线性部分位于 sigmoid 或 relu 反向传播之后。在下一个代码部分将计算 dZ,但为了遵循前向传播的相同函数实现逻辑,线性反向传播函数将首先出现。
在实现梯度计算之前,需要从之前的层加载参数 weight、bias 和 activation,这些参数在线性传播过程中都存储在缓存中。参数 m 最初来源于交叉熵成本公式,并且是之前激活向量的大小,可以通过 previous_activation.shape[1]
获得。然后可以使用 NumPy 实现梯度公式的向量化计算。在偏置梯度中,需要 keepdims=True
和 axis=1
参数,因为需要在向量的行中进行求和,并且必须保持向量的原始维度,即 dB 将与 dZ 具有相同的维度。
成本相对于线性输出的导数 (dZ) 公式如图 9 所示,其中 g’(Z[L]) 是激活函数的导数。
图 9— 成本相对于线性输出的导数。来源:作者。
因此,必须首先计算 sigmoid 和 ReLU 函数的导数。在 ReLU 中,当值为正时,导数为 1,否则为未定义,但为了计算目的,在 ReLU 反向传播中获取 dZ,可以直接复制 dactivation 向量(因为 dactivation * 1 = dactivation
),并在 z 为负时将 dZ 设置为 0。对于 sigmoid 函数 s,其导数为 s * (1-s)
, 乘以这个导数后,dactivation, 向量 dZ 就在 sigmoid 反向传播函数中实现了。
现在可以实现 linear_activation_backward
函数。
首先,需要从 cache
列表中检索线性和激活缓存。然后对每个激活,首先运行 activation_backward
函数,获取 dZ,然后将其作为输入,与 linear cache
结合,传递给 linear_backward
函数。最后,该函数返回 dW、dB 和 dprevious_activation 梯度。请记住,这与前向传播的顺序相反,我们在网络中从右到左进行。
现在是时候为整个网络实现反向传播函数了。这个函数将从最后一层 L 开始,向后遍历所有隐藏层。因此,代码需要计算dAL,即成本函数对最后一次激活的导数,以便将其作为linear_activation_backward
函数的输入,该函数用于 sigmoid 激活。dAL 的公式在下面的图 10 中展示。
图 10 — 成本函数对最后一次激活的导数。来源:作者。
现在一切准备就绪,可以实现反向传播函数了。
首先,创建梯度字典。网络的层数通过获取缓存字典的长度来定义,因为每层在前向传播块中都有其线性和激活缓存,因此缓存列表的长度与层数相同。之后,函数将遍历这些层的缓存,以检索线性激活反向传播函数的输入值。此外,真实标签向量 (Y_train) 被重塑为与最后一次激活的形状匹配的维度,因为这是在dAL计算中将一个除以另一个的要求,接下来的代码行。
创建并设置current_cache对象以检索最后一层的线性和激活缓存(记住 Python 索引从 0 开始,所以最后一层是 n_layers - 1)。然后,在linear_activation_backward
函数中,将激活缓存用于sigmoid_backward
函数,而线性缓存将作为linear_backward
函数的输入。最后,函数收集这些函数的返回值,并将其分配给梯度字典。在dA的情况下,由于梯度公式计算的是来自前一激活的,因此需要使用 n_layers-1 进行索引分配。在这个代码块之后,计算了网络最后一层的梯度。
按照网络的反向顺序,下一步是反向循环遍历 linear->relu 层并计算它们的梯度。然而,在反向循环过程中,linear_activation_backward
函数必须使用‘relu’参数而不是‘sigmaid’,因为relu_backward
函数需要被调用以处理其余层。最后,函数返回所有层计算得到的dA、dW和dB梯度,反向传播也就完成了。
6. 参数更新
在计算出梯度后,是时候通过使用梯度更新原始参数来结束梯度下降,向着成本函数的最小值移动。
该函数通过遍历层并将w和b参数赋予其原始值减去学习率输入乘以相应的梯度来实现。乘以学习率是控制每次模型权重更新时网络参数w和b变化多少的一种方式。
7. 向量的预处理
最终,所有必要的梯度下降优化函数都已实现,因此训练和测试向量可以预处理并准备好进行训练。
layers_dimensions,初始化函数的输入必须硬编码,通过创建一个包含每层神经元数量的列表来完成。随后,X_train 和 X_test 向量必须被展平以作为网络的输入,如图像 1 所示。这可以通过使用 NumPy 函数 reshape 来完成。此外,需要将 X_train 和 X_test 的值除以 255,因为它们是像素(范围从 0 到 255),而将值归一化到 0 到 1 的范围是一个良好的实践。这样,数字会更小,计算也会更快。最后,Y_train 和 Y_test 被转换为数组并且也展平。
这就是训练和测试向量的最终维度:
图片 11 — 训练和测试向量的维度。 来源:作者。
8. 训练
准备好所有函数后,只需将它们组织成一个循环来创建训练迭代。
但首先,创建一个空列表来存储来自cross_entropy_cost
函数的成本输出,并初始化参数,因为这必须在迭代之前完成一次,因为这些参数将通过梯度更新。
现在创建一个循环,遍历输入的迭代次数,按照正确的顺序调用实现的函数:l_layer_model_forward
、cross_entropy_cost
、l_layer_model_backward
和update_parameters
。最后,添加一个条件语句以每 50 次迭代或在最后一次迭代时打印成本。
调用函数进行 2500 次迭代:
成本从第一次迭代的 0.69 降到最后一次的 0.09。
图片 12 — 来源:作者。
这意味着在 NumPy 中开发的梯度下降函数已经在训练过程中优化了参数,从而导致更好的预测和更低的成本。训练完成后,我们可以检查训练好的模型如何预测测试图像标签。
9. 预测
通过使用训练好的参数,此函数运行X_test向量的前向传播以获得预测,然后将其与真实标签向量Y_test进行比较,以返回准确率。
图片 13 — 来源:作者。
该模型在测试图像中检测猫的准确率已达到近 77%。考虑到仅使用了 NumPy 构建网络,这个准确率相当不错。添加新图像到训练数据集中、增加网络复杂性,或使用数据增强技术将现有训练图像转换为新图像,都是提高准确率的可能方法。
不过,再次强调,准确率不是我们关注的重点,因为我们深入探讨了数学基础。这也是本文的价值所在。学习网络的基础知识为迷人的深度学习网络应用世界奠定了知识基础。希望你继续深入探索!
使用交叉验证和 Matplotlib 在 Python 中对机器学习模型进行基准测试
学习如何创建面向对象的方法,使用交叉验证和结果可视化来比较和评估机器学习模型的性能
·发表于数据科学的前沿 ·5 分钟阅读·2023 年 1 月 23 日
–
作者提供的图片。
在这篇文章中,我们将探讨如何使用 Python 来比较和评估机器学习模型的性能。
我们将使用 Sklearn 进行交叉验证以测试模型,并使用 Matplotlib 显示结果。
这样做的主要动机是清晰准确地理解模型性能,从而改进模型选择过程。
交叉验证是一种对训练数据以外的数据测试模型的稳健方法。它允许我们在折叠数据上评估模型性能,这些数据没有用于训练模型本身,从而为我们提供了对模型在真实数据上性能的更准确估计。
有关交叉验证的详细解释,请参阅这篇文章
了解交叉验证是什么——构建可泛化模型的基本技术
towardsdatascience.com
我们将使用面向对象的方法,以便可以轻松地将其重复用于其他机器学习项目,使得这种方法高度可复制。
基准测试类
首先,我们将创建一个名为Benchmark
的类,该类负责测试模型。该类将接受一个模型字典,其中键是模型名称,值是模板对象本身。
该类还将使用 scikit-learn 的make_classification
函数生成测试数据。
import numpy as np
from sklearn import model_selection
from sklearn import metrics
from sklearn import datasets
import matplotlib.pyplot as plt
class Benchmark:
"""
This class allows to compare and evaluate the
performance of machine learning models using cross-validation
Parameters
----------
models : dict
Dictionary of models,
where the key is the name of the model and
the value is the model object.
"""
def __init__(self, models):
self.models = models
def test_models(self, X=None, y=None, cv=5):
"""
Test the models using the provided data and cross-validation.
Parameters
----------
X : array-like or DataFrame, shape (n_samples, n_features)
Features for the test data.
y : array-like or Series, shape (n_samples,)
Target for the test data.
cv : int, cross-validation generator or an iterable, optional
Number of folds for the cross-validation.
Returns
-------
best_model : str
Name of the model with the highest score.
"""
if X is None or y is None:
X, y = datasets.make_classification(
n_samples=100,
n_features=10,
n_classes=2,
n_clusters_per_class=1,
random_state=0
)
self.results = {}
for name, model in self.models.items():
scores = model_selection.cross_val_score(model, X, y, cv=cv)
self.results[name] = scores.mean()
self.best_model = max(self.results, key=self.results.get)
return f"The best model is: {self.best_model} with a score of {self.results[self.best_model]:.3f}"
该类的主要功能将是test_models
函数,该函数将接受测试数据并使用交叉验证来测试模型。该函数将把结果存储在实例绑定的变量中,并通过交叉验证的各种迭代返回得分最高的模型。
为了显示结果,我们将向类中添加一个名为plot_cv_results
的函数。这个函数将使用 Matplotlib 创建一个条形图,显示每个模型的平均交叉验证得分。
def plot_cv_results(self):
"""
Create a bar chart to visualize the cross-validation results.
Returns
-------
None
"""
plt.figure(figsize=(15,5))
x = np.arange(len(self.results))
plt.bar(x, list(self.results.values()), align='center', color ='g')
plt.xticks(x, list(self.results.keys()))
plt.ylim([0, 1])
plt.ylabel('Cross-Validation Score')
plt.xlabel('Models')
plt.title('Model Comparison')
for index, value in enumerate(self.results.values()):
plt.text(index, value, str(round(value,2)))
plt.show()
最后,为了使用该类,我们将通过传递模型字典并调用test_models
函数与测试数据来实例化Benchmark
对象。接下来,我们将使用plot_cv_results
函数来显示结果。
from sklearn import linear_model, ensemble
models = {
'logistic': linear_model.LogisticRegression(),
'randomforest': ensemble.RandomForestClassifier(),
'extratrees': ensemble.ExtraTreesClassifier(),
'gbm': ensemble.GradientBoostingClassifier()
}
benchmark = Benchmark(models)
print(benchmark.test_models())
benchmark.plot_cv_results()
这是结果。
模型基准结果。图片由作者提供。
这样,我们可以轻松比较和评估模型的性能,然后选择最适合我们特定问题的模型。
在这个例子中,我们使用了make_classification
函数生成了玩具数据,但你当然可以使用任何你喜欢的数据集。
此外,Benchmark
类可以扩展以包括其他功能,例如将结果保存到文件的能力或在多个数据集上测试模型。
下一步是什么?
按照通常的机器学习流程,下一步将调优最佳模型的超参数(在这个例子中是ExtraTreesClassifier
)。如果我们的特征被认为是决定性的,这一步是必要的。
如果不是,另一种中间步骤是进行特征选择/工程,并在每次特征更改时重复基准测试步骤。
结论
我们创建的Benchmark
类只是如何在项目中实现这种技术的一个例子,但它可以轻松地适应和定制以满足你的项目具体需求。
使用这种方法的主要好处是它自动化了比较和评估模型的过程,这可以节省时间并减少人为错误。
推荐阅读
对感兴趣的人,我推荐了一些关于每个机器学习相关主题的书籍。这些书籍在我看来是必读的,并且对我的职业生涯有很大的影响。
-
机器学习简介: 自信的数据技能:掌握处理数据的基础知识并提升你的职业生涯 作者: Kirill Eremenko
-
Sklearn / TensorFlow: 带有 Scikit-Learn、Keras 和 TensorFlow 的实践机器学习 作者: Aurelien Géron
-
自然语言处理(NLP): 文本数据:机器学习和社会科学的新框架 作者: Justin Grimmer
-
Sklearn / PyTorch: 使用 PyTorch 和 Scikit-Learn 的机器学习:使用 Python 开发机器学习和深度学习模型 作者: Sebastian Raschka
-
数据可视化:用数据讲故事:商务专业人士的数据可视化指南 作者:Cole Knaflic
有用的链接(由我撰写)
-
学习如何在 Python 中执行顶级的探索性数据分析:在 Python 中进行探索性数据分析 — 逐步过程
-
学习 TensorFlow 的基础知识:开始使用 TensorFlow 2.0 — 深度学习简介
-
使用 TF-IDF 在 Python 中进行文本聚类:在 Python 中使用 TF-IDF 进行文本聚类
如果你想支持我的内容创作活动,可以通过下面的推荐链接加入 Medium 的会员计划。我将获得你投资的一部分,你将能够无缝访问 Medium 上大量的数据科学及其他文章。
[## 通过我的推荐链接加入 Medium - Andrea D’Agostino
阅读 Andrea D’Agostino 的每一个故事(以及 Medium 上其他成千上万作者的故事)。你的会员费直接…
medium.com](https://medium.com/@theDrewDag/membership?source=post_page-----4957a41149e--------------------------------)
代码模板
这是整个代码库
class Benchmark:
def __init__(self, models):
self.models = models
def test_models(self, X=None, y=None, cv=5):
if X is None or y is None:
X, y = datasets.make_classification(
n_samples=100,
n_features=10,
n_classes=2,
n_clusters_per_class=1,
random_state=0
)
self.results = {}
for name, model in self.models.items():
scores = model_selection.cross_val_score(model, X, y, cv=cv)
self.results[name] = scores.mean()
self.best_model = max(self.results, key=self.results.get)
return f"The best model is: {self.best_model} with a score of {self.results[self.best_model]:.3f}"
def plot_cv_results(self):
plt.figure(figsize=(15,5))
x = np.arange(len(self.results))
plt.bar(x, list(self.results.values()), align='center', color ='g')
plt.xticks(x, list(self.results.keys()))
plt.ylim([0, 1])
plt.ylabel('Cross-Validation Score')
plt.xlabel('Models')
plt.title('Model Comparison')
for index, value in enumerate(self.results.values()):
plt.text(index, value, str(round(value,2)))
plt.show()
from sklearn import linear_model, ensemble
models = {
'logistic': linear_model.LogisticRegression(),
'randomforest': ensemble.RandomForestClassifier(),
'extratrees': ensemble.ExtraTreesClassifier(),
'gbm': ensemble.GradientBoostingClassifier()
}
benchmark = Benchmark(models)
print(benchmark.test_models())
benchmark.plot_cv_results()
使用 Criterion 基准测试 Rust 编译器设置
使用脚本和环境变量控制 Criterion
·
关注 发表在 Towards Data Science ·6 分钟阅读·2023 年 12 月 15 日
–
蟹赛时间 — 来源:openai.com/dall-e-2/
。所有其他图像来自作者。
本文首先解释了如何使用流行的 criterion crate 进行基准测试。然后,提供了额外的信息,展示了如何在不同编译器设置下进行基准测试。虽然每种编译器设置的组合都需要重新编译和单独运行,但我们仍然可以汇总和分析结果。本文是 Towards Data Science 中的文章 Nine Rules for SIMD Acceleration of Your Rust Code 的配套文章。
我们将把这项技术应用到 [range-set-blaze](https://github.com/CarlKCarlK/range-set-blaze)
crate。我们的目标是测量不同 SIMD(单指令、多数据)设置的性能效果。我们还希望比较不同 CPU 之间的性能。这个方法也有助于理解不同优化级别的好处。
在 range-set-blaze
的背景下,我们评估:
-
3 种 SIMD 扩展级别 —
sse2
(128 位),avx2
(256 位),avx512f
(512 位) -
10 种元素类型 —
i8
,u8
,i16
,u16
,i32
,u32
,i64
,u64
,isize
,usize
-
5 个 lane 数量 — 4,8,16,32,64
-
2 种 CPU — AMD 7950X(带
avx512f
),Intel i5–8250U(带avx2
) -
5 种算法 — Regular,Splat0,Splat1,Splat2,Rotate
-
4 个输入长度 — 1024;10,240;102,400;1,024,000
在这些变量中,我们外部调整前四个变量(SIMD 扩展级别、元素类型、lane 数量、CPU)。我们通过在常规 Rust 基准测试代码内部的循环控制最后两个变量(算法和输入长度)。
使用 Criterion 入门
要将基准测试添加到你的项目中,添加这个开发依赖并创建一个子文件夹:
cargo add criterion --dev --features html_reports
mkdir benches
在 Cargo.toml
中添加:
[[bench]]
name = "bench"
harness = false
创建一个 benches/bench.rs
。这里是一个示例:
#![feature(portable_simd)]
#![feature(array_chunks)]
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use is_consecutive1::*;
// create a string from the SIMD extension used
const SIMD_SUFFIX: &str = if cfg!(target_feature = "avx512f") {
"avx512f,512"
} else if cfg!(target_feature = "avx2") {
"avx2,256"
} else if cfg!(target_feature = "sse2") {
"sse2,128"
} else {
"error"
};
type Integer = i32;
const LANES: usize = 64;
// compare against this
#[inline]
pub fn is_consecutive_regular(chunk: &[Integer; LANES]) -> bool {
for i in 1..LANES {
if chunk[i - 1].checked_add(1) != Some(chunk[i]) {
return false;
}
}
true
}
// define a benchmark called "simple"
fn simple(c: &mut Criterion) {
let mut group = c.benchmark_group("simple");
group.sample_size(1000);
// generate about 1 million aligned elements
let parameter: Integer = 1_024_000;
let v = (100..parameter + 100).collect::<Vec<_>>();
let (prefix, simd_chunks, reminder) = v.as_simd::<LANES>(); // keep aligned part
let v = &v[prefix.len()..v.len() - reminder.len()]; // keep aligned part
group.bench_function(format!("regular,{}", SIMD_SUFFIX), |b| {
b.iter(|| {
let _: usize = black_box(
v.array_chunks::<LANES>()
.map(|chunk| is_consecutive_regular(chunk) as usize)
.sum(),
);
});
});
group.bench_function(format!("splat1,{}", SIMD_SUFFIX), |b| {
b.iter(|| {
let _: usize = black_box(
simd_chunks
.iter()
.map(|chunk| IsConsecutive::is_consecutive(*chunk) as usize)
.sum(),
);
});
});
group.finish();
}
criterion_group!(benches, simple);
criterion_main!(benches);
如果你想运行这个示例,代码在 GitHub 上。
使用命令 cargo bench
运行基准测试。报告将出现在 target/criterion/simple/report/index.html
中,并包括像这样的图表,显示 Splat1 的运行速度比 Regular 快很多。
跳出 Criterion 的思维框架
我们面临一个问题。我们希望基准测试 sse2
与 avx2
与 avx512f
的性能,这需要(一般来说)多次编译和 criterion
运行。
这是我们的方法:
-
使用 Bash 脚本设置环境变量并调用基准测试。
例如,
bench.sh
:
#!/bin/bash
SIMD_INTEGER_VALUES=("i64" "i32" "i16" "i8" "isize" "u64" "u32" "u16" "u8" "usize")
SIMD_LANES_VALUES=(64 32 16 8 4)
RUSTFLAGS_VALUES=("-C target-feature=+avx512f" "-C target-feature=+avx2" "")
for simdLanes in "${SIMD_LANES_VALUES[@]}"; do
for simdInteger in "${SIMD_INTEGER_VALUES[@]}"; do
for rustFlags in "${RUSTFLAGS_VALUES[@]}"; do
echo "Running with SIMD_INTEGER=$simdInteger, SIMD_LANES=$simdLanes, RUSTFLAGS=$rustFlags"
SIMD_LANES=$simdLanes SIMD_INTEGER=$simdInteger RUSTFLAGS="$rustFlags" cargo bench
done
done
done
附注:如果你有 Git 和/或 VS Code,你可以 轻松地在 Windows 上使用 Bash。
- 使用
[build.rs](https://doc.rust-lang.org/cargo/reference/build-scripts.html)
将这些环境变量转化为 Rust 配置:
use std::env;
fn main() {
if let Ok(simd_lanes) = env::var("SIMD_LANES") {
println!("cargo:rustc-cfg=simd_lanes=\"{}\"", simd_lanes);
println!("cargo:rerun-if-env-changed=SIMD_LANES");
}
if let Ok(simd_integer) = env::var("SIMD_INTEGER") {
println!("cargo:rustc-cfg=simd_integer=\"{}\"", simd_integer);
println!("cargo:rerun-if-env-changed=SIMD_INTEGER");
}
}
- 在
[benches/build.rs](https://github.com/CarlKCarlK/range-set-blaze/blob/nov23/examples/simd/is_consecutive2/benches/bench.rs)
中将这些配置转换为 Rust 常量和类型:
const SIMD_SUFFIX: &str = if cfg!(target_feature = "avx512f") {
"avx512f,512"
} else if cfg!(target_feature = "avx2") {
"avx2,256"
} else if cfg!(target_feature = "sse2") {
"sse2,128"
} else {
"error"
};
#[cfg(simd_integer = "i8")]
type Integer = i8;
#[cfg(simd_integer = "i16")]
type Integer = i16;
#[cfg(simd_integer = "i32")]
type Integer = i32;
#[cfg(simd_integer = "i64")]
type Integer = i64;
#[cfg(simd_integer = "isize")]
type Integer = isize;
#[cfg(simd_integer = "u8")]
type Integer = u8;
#[cfg(simd_integer = "u16")]
type Integer = u16;
#[cfg(simd_integer = "u32")]
type Integer = u32;
#[cfg(simd_integer = "u64")]
type Integer = u64;
#[cfg(simd_integer = "usize")]
type Integer = usize;
#[cfg(not(any(
simd_integer = "i8",
simd_integer = "i16",
simd_integer = "i32",
simd_integer = "i64",
simd_integer = "isize",
simd_integer = "u8",
simd_integer = "u16",
simd_integer = "u32",
simd_integer = "u64",
simd_integer = "usize"
)))]
type Integer = i32;
const LANES: usize = if cfg!(simd_lanes = "2") {
2
} else if cfg!(simd_lanes = "4") {
4
} else if cfg!(simd_lanes = "8") {
8
} else if cfg!(simd_lanes = "16") {
16
} else if cfg!(simd_lanes = "32") {
32
} else {
64
};
- 在
benches.rs
中,创建一个基准 id,用来记录你正在测试的变量组合,变量之间用逗号分隔。这可以是一个字符串,也可以是一个 criterionBenchmarkId
。我使用了以下调用创建了一个BenchmarkId
:create_benchmark_id::<Integer>("regular", LANES, *parameter)
。
fn create_benchmark_id<T>(name: &str, lanes: usize, parameter: usize) -> BenchmarkId
where
T: SimdElement,
{
BenchmarkId::new(
format!(
"{},{},{},{},{}",
name,
SIMD_SUFFIX,
type_name::<T>(),
mem::size_of::<T>() * 8,
lanes,
),
parameter,
)
}
- 对于制表和分析,我喜欢将基准测试结果以逗号分隔值(CSVs)的形式保存。Criterion 已经转向
[*.csv](https://bheisler.github.io/criterion.rs/book/user_guide/csv_output.html)
文件,而转向*.json
文件。为了从*.json
中提取*.csv
,我创建了一个新的 cargo 命令供你使用:[criterion-means](https://github.com/CarlKCarlK/cargo-criterion-means)
。
安装:
cargo install cargo-criterion-means
运行:
cargo criterion-means > results.csv
输出示例:
Group,Id,Parameter,Mean(ns),StdErr(ns)
vector,regular,avx2,256,i16,16,16,1024,291.47,0.080141
vector,regular,avx2,256,i16,16,16,10240,2821.6,3.3949
vector,regular,avx2,256,i16,16,16,102400,28224,7.8341
vector,regular,avx2,256,i16,16,16,1024000,287220,67.067
# ...
分析
CSV 文件适合通过 电子表格数据透视表 或数据框工具,如 Polars 进行分析。
例如,这是我 5000 行长的 Excel 数据文件的顶部:
A 到 J 列来自基准测试。K 到 N 列由 Excel 计算得出。
这是基于数据的透视表(及图表)。它显示了 SIMD lanes 数量变化对吞吐量的影响。图表平均了元素类型和输入长度。图表表明,对于最佳算法,32 或 64 个 lanes 是最好的。
有了这次分析,我们现在可以选择我们的算法并决定如何设置 LANES 参数。
结论
感谢你与我一起踏上 Criterion 基准测试的旅程。
如果你之前没有使用过 Criterion,希望这能鼓励你尝试一下。如果你已经使用过 Criterion 但未能测量你关心的所有内容,希望这能为你提供一个前进的方向。以这种扩展的方式使用 Criterion 可以解锁对你 Rust 项目性能特征的更深刻洞察。
请 关注 Carl 的 Medium 博客。我在 Rust 和 Python 的科学编程、机器学习和统计学方面写作。我每个月写一篇文章。
机器学习中的伯克森悖论
理解数据分析中的隐性偏差
·
关注 发表于 Towards Data Science · 8 分钟阅读 · 2023 年 12 月 22 日
–
由 DALL-E 生成
有时,统计数据显示出令人惊讶的现象,让我们质疑日常看到的事物。伯克森悖论就是一个例子。这个悖论与抽样偏差问题密切相关,当我们误以为两件事物有关联时,因为我们没有看到全貌。作为一名机器学习专家,你应该对这个悖论有所了解,因为它可能通过导致对变量关系的错误假设,显著影响你预测模型的准确性。
让我们从一些例子开始
-
基于Berkson的原始例子,我们来设想一个在医院进行的回顾性研究。在这家医院,研究人员正在研究胆囊炎(胆囊疾病)的风险因素,其中一个风险可能是糖尿病。由于样本来源于住院人群而非一般人群,这存在抽样偏差,这可能导致错误地认为糖尿病对胆囊炎有保护作用。
-
另一个著名的例子来自Jordon Ellenberg。在这个例子中,Alex 创建了一个约会池。这个小组并不能很好地代表所有男人;我们有一个抽样偏差,因为她挑选的是非常友好、吸引人,或者两者兼具的人。在 Alex 的约会池中,发生了一些有趣的事情……在她约会的男人中,看起来他们越友好,越不吸引人,反之亦然。这种抽样偏差可能导致 Alex 错误地认为友好与吸引力之间存在负相关。
让我们尝试稍微形式化一下问题
假设我们有两个独立事件X和Y。由于这些事件是独立的:
这些随机事件可以是,例如,得胆囊炎或有糖尿病,如第一个例子,或者是友好或美丽,如第二个例子。当然,当我说这两个事件是独立的时,我指的是整个总体!
在之前的例子中,抽样偏差总是同一种类型:没有事件都未发生的情况。在医院样本中,没有患者既没有胆囊炎也没有糖尿病。在 Alex 的样本中,没有男人既不友好又丑。因此,我们被条件限制在至少发生一个事件:事件X发生了,或者事件Y发生了,或者两者都发生了。为此,我们可以定义一个新的事件Z,它是事件X和Y的并集。
现在,我们可以写下以下内容,以表明我们在抽样偏差假设下:
这是事件X发生的概率,已知事件X或Y(或两者)已经发生。从直觉上,我们可以感觉到这个概率比*P(X)*要高……但也可以通过正式证明来显示这一点。
为了做到这一点,我们知道:
通过假设这两个事件不可能同时发生(例如,有些人既丑又不友好),之前的陈述可以变成一个严格的不等式;因为集合*(X* ∪ *Y)*不是样本空间Ω:
现在,如果我们将这个严格不等式的两边同时除以P(X ∪ Y),然后乘以P(X),我们得到:
其中
因此,我们确实得到在抽样偏差下的概率P(X|Z)高于P(X),在整个总体中:
好的,明白了……但现在让我们回到伯克森悖论。我们有两个独立的随机变量 X 和 Y,我们想要展示它们在上述采样偏倚 Z 下变得相关。
为了做到这一点,让我们从 P(X | Y ∩ Z) 开始,这是在知道事件 Y 已经发生且我们处于偏倚采样 Z 下的情况下观察事件 X 的概率。请注意,P(X | Y ∩ Z) 也可以写作 P(X | Y, Z)。
由于 (Y ∩ Z) = (Y ∩ (X ∪ Y)) = Y,并且 X 和 Y 是独立变量,我们有:
然后……最终,知道 P(X) < P(X | Z),我们得到了我们寻找的结果:
这个方程式显示,在由 Z 定义的采样偏倚下,两个最初独立的事件 X 和 Y 变得相关(否则,我们会看到等号而不是">")。
回到亚历克斯的约会池的例子,如果
-
Z 是处于亚历克斯约会池中的事件
-
X 是选择一个友好的人的事件
-
Y 是选择一个有吸引力的人的事件
那么 (X | Z) 是亚历克斯遇到一个好人的事件,而 (X | Y ∩ Z) 是在给定他是帅哥的情况下亚历克斯遇到好人的事件。由于用于构建亚历克斯约会池的选择过程,以及伯克森悖论,亚历克斯会感觉当她遇到帅哥时,他们不会那么友好,而如果从整个群体中抽取,这可能是两个独立事件……
也许一个数值例子会帮助我们更具体地理解
为了说明伯克森悖论,我们使用两个骰子:
-
事件 X:第一个骰子显示 6。
-
事件 Y:第二个骰子显示 1 或 2。
这两个事件显然是独立的,其中 P(X)=1/6 和 P(Y)=1/3。
现在,让我们引入我们的条件 (Z),表示通过排除所有第一个骰子不是六而第二个骰子既不是 1 也不是 2 的结果来进行偏倚采样。
在我们的偏倚采样条件下,我们需要计算事件 X 发生的概率,前提是至少发生了事件 (X 或 Y),这用 P(X|Z) 表示。
首先,我们需要确定 Z = (X ∪ Y) 的概率……对不起,从现在开始我们需要做一点计算……我会为你做的…… 😃
接下来,我们计算在 Z 给定的情况下 X 的概率:
要查看在假设 Z 发生的情况下 X 和 Y 是否存在依赖关系,我们需要计算 P(X | Y ∩ Z)。
因为
我们有
为了演示伯克森悖论,我们将 P(X|Z) 与 P(X ∣ Y ∩ Z) 进行比较,我们得到:
-
P(X | Z) = 0.375
-
P(X |Y ∩ Z) ≈ 0.1666…
我们确实恢复了在伯克森悖论下,由于采样偏倚 Z,我们有 P(X | Z) > P(X ∣ Y ∩ Z) 的性质。
我个人感到很惊讶!我们有两个骰子……两个明显独立的随机事件……通过采样过程,我们可以获得骰子掷出的结果变得相关的印象。
为了进一步说服我们,让我们进行一点模拟
在下面的代码中,我将使用 Python 模拟骰子掷出。
以下代码模拟了百万次掷两个骰子的实验,对于每次实验,它检查第一个骰子是否掷出 6(事件 X),以及第二个骰子是否掷出 1 或 2(事件 Y)。然后,它将这些检查的结果(真或假)分别存储在列表 X 和 Y 中。
import random
#Get some observations for random variables X and Y
def sample_X_Y(nb_exp):
X = []
Y = []
for i in range(nb_exp):
dice1 = random.randint(1,6)
dice2 = random.randint(1,6)
X.append(dice1 == 6)
Y.append(dice2 in [1,2])
return X, Y
nb_exp=1_000_000
X, Y = sample_X_Y(nb_exp)
接下来,我们需要检查这两个事件是否确实独立。为此,以下代码计算了事件 X 的概率以及在事件 Y 给定的情况下事件 X 的条件概率。它通过将成功结果的数量除以每个概率的实验总数来完成这一过程。
# compute P(X=1) and P(X1=1|Y=1) to check if X and Y are independent
p_X = sum(X)/nb_exp
p_X_Y = sum([X[i] for i in range(nb_exp) if Y[i]])/sum(Y)
print("P(X=1) = ", round(p_X,5))
print("P(X=1|Y=1) = ", round(p_X_Y,5))
P(X=1) = 0.16693
P(X=1|Y=1) = 0.16681
如我们所见,这两个概率接近;因此(如预期 😉 )或两个骰子是独立的。
现在,让我们看看引入抽样偏差Z时会发生什么。以下代码过滤实验结果,仅保留 X = 1、Y = 1 或两者都为 1 的结果。它将这些过滤后的结果存储在列表 XZ 和 YZ 中。
# keep only the observations where X=1, Y=1 or both (remove when X=0 and Y=0)
XZ = []
YZ = []
for i in range(nb_exp):
if X[i] or Y[i]:
XZ.append(X[i])
YZ.append(Y[i])
nb_obs_Z = len(XZ)
现在,让我们检查这些新变量是否仍然独立。
# compute P(X=1|Z=1) and P(X1=1|Y=1,Z=1) to check if X|Z and Y|Z are independent
p_X_Z = sum(XZ)/nb_obs_Z
p_X_Y_Z = sum([XZ[i] for i in range(nb_obs_Z) if YZ[i]])/sum(YZ)
print("P(X=1|Z=1) = ", round(p_X_Z,5))
print("P(X=1|Y=1,Z=1) = ", round(p_X_Y_Z,5))
P(X=1|Z=1) = 0.37545
P(X=1|Y=1,Z=1) = 0.16681
我们有一个不等式(与前一节相同的值),这意味着如果 Z 为真,那么拥有 Y 的信息会改变 X 的概率;因此,它们不再是独立的。
这种悖论对机器学习专家的影响是什么?
我认为机器学习专家没有足够关注这种偏差。当我们谈论伯克森悖论时,我们是在深入探讨一个对从事机器学习的人员至关重要的主题。这个概念是关于理解我们如何被使用的数据所误导。伯克森悖论警告我们使用偏倚或片面数据的危险。
信用评分系统:在金融领域,基于高收入或高信用分数申请人的数据训练的模型,但这两者很少同时出现,可能会错误地推断出这两个因素之间存在负相关。这有可能导致不公平的贷款实践,偏向某些特定的人群。
社交媒体算法:在社交媒体算法中,当模型训练基于极端用户数据时,如具有高人气但低参与度的病毒内容和深度参与但低人气的利基内容,可能会出现伯克森悖论。这种偏倚的抽样通常导致错误结论,认为人气和参与深度之间是负相关的。因此,算法可能低估那些在中等人气和参与度之间平衡的内容,从而扭曲内容推荐系统。
求职者筛选工具:基于具有高学历或丰富经验的申请人的筛选模型可能错误地暗示这些属性之间存在反向关系,可能忽视了那些在这两个方面都平衡的候选人。
在每种情况下,忽视伯克森悖论可能导致模型偏倚,影响决策和公平性。机器学习专家必须通过多样化数据来源和持续验证模型以应对这一点。
结论
总之,伯克森悖论是对机器学习专业人士的重要提醒,提醒他们仔细审查数据来源并避免误导性的相关性。通过理解和考虑这一悖论,我们可以构建更准确、公平和实际的模型,真正反映现实世界的复杂性。请记住,强健的机器学习的关键在于复杂的算法以及周到、全面的数据收集和分析。
感谢阅读!
如果你希望及时了解我的最新发布并提高博客的可见性,请考虑关注我。
大型语言模型:BERT — Transformer 的双向编码器表示
理解 BERT 如何构建最先进的嵌入
·
关注 发表在 Towards Data Science · 11 分钟阅读 · 2023 年 8 月 30 日
–
介绍
2017 年是机器学习的历史性一年,当时Transformer模型首次亮相。它在许多基准测试中表现出色,适用于数据科学中的许多问题。由于其高效的架构,后来开发了许多其他基于 Transformer 的模型,这些模型在特定任务上有了更多的专业化。
其中一个这样的模型是 BERT。它主要以能够构建非常准确的文本嵌入而著称,这些嵌入可以表示文本信息并存储长文本序列的语义含义。因此,BERT 嵌入在机器学习中得到了广泛应用。理解 BERT 如何构建文本表示是至关重要的,因为这为处理自然语言处理中的大量任务打开了大门。
在本文中,我们将参考 原始 BERT 论文,查看 BERT 架构并理解其核心机制。在前几部分中,我们将给出 BERT 的高级概述。之后,我们将逐步深入其内部工作流程及信息在模型中的传递方式。最后,我们将了解如何对 BERT 进行微调,以解决 NLP 中的特定问题。
高级概述
Transformer 的架构由两个主要部分组成:编码器和解码器。堆叠编码器的目标是为输入构建有意义的嵌入,以保持其主要上下文。最后一个编码器的输出传递给所有解码器的输入,试图生成新的信息。
BERT 是 Transformer 的继承者,继承了其堆叠的双向编码器。BERT 中的大部分架构原则与原始 Transformer 相同。
Transformer 架构
BERT 版本
BERT 存在两个主要版本:base 和 large。它们的架构完全相同,只是参数数量不同。总体而言,BERT large 比 BERT base 多了 3.09 倍的参数进行调优。
BERT base 和 BERT large 的比较
双向表示
从 BERT 名称中的字母“B”来看,重要的是要记住 BERT 是一个 双向 模型,这意味着它可以更好地捕捉词汇之间的连接,因为信息是双向传递的(从左到右和从右到左)。显然,这与单向模型相比需要更多的训练资源,但同时也导致更好的预测准确性。
为了更好地理解,我们可以将 BERT 架构与其他流行的 NLP 模型进行比较。
从 原始论文 中比较 BERT、OpenAI GPT 和 ElMo 架构。由作者采用。
输入标记化
注:在官方论文中,作者使用“sentence”一词来表示传递给输入的文本。为了统一术语,本文系列中我们将使用“sequence”一词。这是为了避免混淆,因为“sentence” 通常指一个由句点分隔的单独短语,而许多其他 NLP 研究论文中“sequence”一词在类似情况下被使用。
在深入了解 BERT 的训练方法之前,有必要了解 BERT 接受数据的格式。对于输入,BERT 接受单个序列或一对序列。每个序列被拆分为标记。此外,两个特殊标记会被传递到输入中:
注意。官方论文使用了“句子”这个术语,它指的是传递给 BERT 的输入序列,该序列实际上可以由多个句子组成。为了简化,我们将遵循这个符号,并在本文中使用相同的术语。
-
[CLS] — 在第一个序列之前传递,表示其开始。同时,* [CLS] * 也用于训练中的分类目标(在下面的章节中讨论)。
-
[SEP] — 在序列之间传递,用以表示第一个序列的结束和第二个序列的开始。
传递两个序列使得 BERT 能够处理各种任务,其中输入包含一对序列(例如,问题和答案,假设和前提等)。
输入嵌入
在分词之后,为每个标记构建一个嵌入。为了使输入嵌入更具代表性,BERT 为每个标记构建了三种类型的嵌入:
-
标记嵌入 捕捉标记的语义意义。
-
段嵌入有两个可能的值,表示标记属于哪个序列。
-
位置嵌入 包含关于标记在序列中相对位置的信息。
输入处理
这些嵌入被加总,然后结果被传递给 BERT 模型的第一个编码器。
输出
每个编码器接受n个嵌入作为输入,然后输出相同数量、相同维度的处理后的嵌入。最终,整个 BERT 输出也包含n个嵌入,每个嵌入对应其初始的标记。
训练
BERT 训练分为两个阶段:
-
预训练。BERT 在未标记的序列对上进行训练,涉及两个预测任务:掩码语言建模(MLM) 和 自然语言推理(NLI)。对于每对序列,模型会对这两个任务进行预测,并根据损失值进行反向传播来更新权重。
-
微调。BERT 使用预训练的权重进行初始化,然后在标记数据上为特定问题进行优化。
预训练
与微调相比,预训练通常需要较长的时间,因为模型是在大量数据上训练的。因此,存在许多在线预训练模型的库,这些模型可以被相对快速地微调以解决特定任务。
我们将详细查看 BERT 在预训练期间解决的两个问题。
掩码语言建模
作者建议通过掩盖初始文本中的一定数量的标记来训练 BERT 并预测它们。这使得 BERT 能够构建出具有弹性的嵌入,可以利用周围的上下文来猜测某个词,从而为遗漏的词构建合适的嵌入。这个过程的工作方式如下:
-
在分词后,15% 的标记被随机选择进行掩盖。选择的标记将在迭代结束时进行预测。
-
选择的标记被以三种方式之一替换:
- 80% 的标记被替换为 [MASK] 标记。
示例*: 我买了一本书 → 我买了一个[MASK]*
- 10% 的标记被随机标记替代。
示例: 他在吃水果 → 他在画水果
- 10% 的标记保持不变。
示例: 一栋房子在我附近 → 一栋房子在我附近
-
所有标记被传递给 BERT 模型,模型输出每个接收到的输入标记的嵌入。
4. 对应于步骤 2 中处理的标记的输出嵌入被独立用于预测被掩盖的标记。每个预测的结果是词汇表中所有标记的概率分布。
5. 交叉熵损失通过将概率分布与真实掩盖标记进行比较来计算。
6. 模型权重通过反向传播进行更新。
自然语言推断
对于这个分类任务,BERT 尝试预测第二个序列是否跟随第一个序列。整个预测仅使用来自 [CLS] 标记的最终隐藏状态的嵌入,该标记应包含来自两个序列的聚合信息。
类似于 MLM,使用构建的概率分布(二进制的情况下)来计算模型的损失,并通过反向传播更新模型的权重。
对于自然语言推断(NLI),作者建议选择 50% 的序列对,这些序列在语料库中是紧接着的(正对),以及 50% 的序列对,其中序列是从语料库中随机选取的(负对)。
BERT 预训练
训练细节
根据论文,BERT 在 BooksCorpus(8 亿单词)和英文维基百科(25 亿单词)上进行预训练。为了提取较长的连续文本,作者仅从维基百科中提取阅读段落,忽略表格、标题和列表。
BERT 在大小为 256 的一百万批次上进行训练,这相当于在 33 亿个单词上进行 40 个周期。每个序列包含最多 128(90% 的时间)或 512(10% 的时间)个标记。
根据原始论文,训练参数如下:
-
优化器:Adam(学习率 l = 1e-4,权重衰减 L₂ = 0.01,β₁ = 0.9,β₂ = 0.999,ε = 1e-6)。
-
学习率预热在前 10,000 步内进行,然后线性降低。
-
在所有层上使用 Dropout(α = 0.1)层。
-
激活函数:GELU。
-
训练损失是平均 MLM 和平均下一个句子预测似然的总和。
微调
一旦预训练完成,BERT 可以字面上理解单词的语义,并构建几乎完全表示其意义的嵌入。微调的目标是逐渐调整 BERT 的权重,以解决特定的下游任务。
数据格式
由于自注意力机制的鲁棒性,BERT 可以轻松地为特定下游任务进行微调。BERT 的另一个优势是能够构建双向文本表示。这在处理对时提供了更高的发现两个序列之间正确关系的机会。以前的方法包括独立编码两个序列,然后对它们应用双向交叉注意力。BERT 统一了这两个阶段。
根据具体问题,BERT 接受几种输入格式。用 BERT 解决所有下游任务的框架是相同的:输入一个文本序列,BERT 输出一组标记嵌入,然后将这些嵌入送入模型。大多数时候,并不是所有的输出嵌入都会被使用。
让我们看看常见的问题以及通过微调 BERT 解决这些问题的方法。
句子对分类
句子对分类的目标是理解给定序列对之间的关系。常见的任务类型包括:
-
自然语言推理:确定第二个序列是否跟随第一个序列。
-
相似性分析:找到序列之间的相似程度。
句子对分类
对于微调,两个序列都传递给 BERT。一般来说,* [CLS] 标记的输出嵌入被用来进行分类任务。根据研究人员的说法, [CLS] *标记应该包含关于句子关系的主要信息。
当然,也可以使用其他输出嵌入,但在实际应用中通常会被省略。
问答任务
问答的目标是在文本段落中找到对应于特定问题的答案。大多数时候,答案以两个数字的形式给出:片段的开始和结束标记位置。
问答任务
对于输入,BERT 接收问题和段落,并输出一组对应的嵌入。由于答案包含在段落中,我们只对与段落标记对应的输出嵌入感兴趣。
为了找到段落中答案起始标记的位置,计算每个输出嵌入与一个特殊的可训练向量 Tₛₜₐᵣₓ的标量积。在大多数情况下,当模型和向量 Tₛₜₐᵣₓ经过相应训练时,标量积应该与相应标记实际上是起始答案标记的可能性成正比。为了规范化标量积,它们会传递到 softmax 函数,并可以看作是概率。对应于最高概率的标记嵌入被预测为起始答案标记。根据真实的概率分布,计算损失值并进行反向传播。预测结束标记时会使用向量 Tₑₙ𝒹进行类似的过程。
单句分类
与之前的下游任务相比,区别在于这里只传递单个句子给 BERT。此配置解决的典型问题如下:
-
情感分析:理解一个句子是否具有积极或消极的态度。
-
主题分类:根据句子的内容将句子分类到几个类别之一。
单句分类
预测工作流程与句子对分类的工作流程相同:*[CLS]
*标记的输出嵌入被用作分类模型的输入。
单句标注
*命名实体识别(NER)*是一个机器学习问题,旨在将序列中的每个标记映射到相应的实体之一。
单句标注
为了实现这个目标,通常会计算输入句子的词嵌入。然后,将每个嵌入(除了*[CLS]
和[SEP]
*)独立传递给一个模型,该模型将每个嵌入映射到给定的 NER 类别(如果不能映射,则不进行映射)。
特征提取
使用最后一个 BERT 层作为嵌入并不是从输入文本中提取特征的唯一方法。实际上,研究人员完成了几种不同方式的嵌入聚合实验,以解决 CoNLL-2003 数据集上的 NER 任务。为了进行实验,他们将提取的嵌入作为输入传递给一个随机初始化的两层 768 维 BiLSTM,然后应用分类层。
嵌入的提取方式(来自 BERT 基础模型)在下面的图中展示了。如图所示,最有效的方法是连接最后四个 BERT 隐藏层。
根据已完成的实验,重要的是要记住,隐藏层的聚合是一种可能的改进嵌入表示以在各种 NLP 任务中取得更好结果的方法。
左侧的图表展示了带有隐藏层的扩展 BERT 结构。右侧的表格则说明了嵌入的构建方式以及通过应用相应策略所取得的得分。
将 BERT 与其他特征结合
有时我们不仅处理文本,还处理数值特征。例如,自然希望构建能够融合文本和其他非文本特征信息的嵌入。以下是推荐应用的策略:
-
将文本与非文本特征进行串联。例如,如果我们处理的是以文本形式存在的人物简介,并且有其他独立的特征如姓名或年龄,那么可以得到新的文本描述,如:“我的名字是<name>。<profile description>。我<age>岁。”最后,这样的文本描述可以输入到 BERT 模型中。
-
将嵌入与特征进行串联。可以如上所述构建 BERT 嵌入,然后将其与其他特征进行串联。唯一改变的是配置中必须接受更高维度的输入向量用于下游任务的分类模型。
结论
在本文中,我们深入探讨了 BERT 的训练和微调过程。实际上,这些知识足以解决大多数自然语言处理任务,感谢 BERT 几乎完全将文本数据纳入嵌入中的能力。
最近,出现了其他类似 BERT 的模型(SBERT、RoBERTa 等)。甚至存在一个专门研究领域,称为“BERTology”,它深入分析 BERT 的能力,以开发新的高性能模型。这些事实进一步证明了 BERT 在机器学习领域引发了革命,并使自然语言处理得以显著进步。
资源
除非另有说明,否则所有图像均为作者提供
BERT 与 GPT:比较 NLP 巨头
原文:
towardsdatascience.com/bert-vs-gpt-comparing-the-nlp-giants-329d105e34ec
它们的结构有何不同,这些差异如何影响模型的能力?
·发布于 Towards Data Science ·阅读时长 7 分钟·2023 年 8 月 20 日
–
图片由作者使用 Stable Diffusion 生成。
在 2018 年,NLP 研究人员对 BERT 论文感到惊讶[1]。这个方法虽然简单,但结果却令人印象深刻:它为 11 个 NLP 任务设立了新的基准。
在短短一年多的时间里,BERT 已成为自然语言处理(NLP)实验中的一个普遍基准,超过 150 篇研究论文分析和改进了该模型。[2]
在 2022 年,ChatGPT [3] 以其生成类人响应的能力引爆了整个互联网。该模型可以理解广泛的话题,并能够自然地进行长时间对话,这使其与所有传统聊天机器人不同。
BERT 和 ChatGPT 是自然语言处理(NLP)领域的重大突破,但它们的方法不同。它们的结构有何不同?这些差异如何影响模型的能力?让我们深入探讨一下!
注意力
我们必须首先回顾常用的注意力机制,以便完全理解模型结构。注意力机制旨在捕捉和建模序列中令牌之间的关系,这也是它们在 NLP 任务中如此成功的原因之一。
一个直观的理解
-
想象一下你有 n 件商品存放在箱子 v1, v2,…,v_n. 这些被称为“值”。
-
我们有一个查询 q,它要求从每个箱子中取出一些适量的商品 w。我们称这些为 w_1, w_2,…,w_n(这就是“注意力权重”)。
-
如何确定 w_1, w_2,…, w_n?换句话说,如何知道在 v_1,v_2, …,v_n, 哪些应该比其他的多取?
-
记住,所有的值都存储在我们无法窥探的箱子里。因此,我们不能直接判断 v_i 应该取少还是取多。
-
幸运的是,我们在每个框上都有一个标签,k_1, k_2,…,k_n,这些被称为“keys”。“keys”代表容器内部的特征。
-
基于 q 和 k_i (qk_i)* 的“相似性”,我们可以决定 v_i 的重要性 (w_i) 以及我们应该取多少 v_i (w_iv_i*).
基础注意力机制(图片由作者提供)
当然,这是一种非常抽象的注意力解释,但它帮助我更好地记住“query”、“key”和“value”背后的含义。
接下来,让我们更深入地了解 Transformer 模型如何使用不同类型的注意力。
BERT:全球自注意力和双向编码器
全球自注意力对 query、key 和 value 的值是相同的。在一系列词元中,每个词元将“关注”所有其他词元,因此信息沿序列传播。而且更重要的是,以并行方式进行。
全球自注意力 [4]
与 RNN 和 CNN 相比,这一点非常重要。
-
对于 RNN,每个“状态”经过许多步骤,这可能导致信息的丢失。此外,RNN 按顺序传递每个词元,我们无法利用 GPU 并行处理。
-
对于 CNN,尽管它是并行运行的,但每个词元只能关注有限的领域,从而对词元的关系做出假设。
自注意力是编码器的关键组件,是 BERT 的构建块 [1]。BERT 论文的作者指出了从左到右的语言模型的局限性如下。
这些限制对于句子级任务是次优的,当应用基于微调的方法于如问答这样的词元级任务时,它可能非常有害,因为在这些任务中,结合来自两个方向的上下文至关重要。[1]
BERT 预训练 [1]
为了克服上述缺点,BERT 在“掩码语言模型”(MLM)和“下一个句子预测”(NSP)任务上进行了预训练。
-
对于 MLM 任务,15% 的词元位置被选中进行预测。因此,所选择的词元中将有 80% 被替换为 [MASK] 词元,10% 被随机词元替换,10% 不被替换。
-
对于 NSP 任务,给定 2 个句子,s1 和 s2,输入格式为“[CLS][SEP]”,模型预测 s1 是否接在 s2 之后。[CLS] 和 [SEP] 分别是特殊的分类和分隔符标记。
正如我们所见,模型可以在这两个任务中“窥视”每个词元的左右上下文。这使得模型能够利用双向词表示,并获得更深入的理解。
但双向编码有其代价。缺乏解码器的 BERT 可能不适合文本生成。因此,该模型需要添加额外的任务特定架构以适应生成任务。
GPT:因果自注意力和文本生成
与全局自注意力相比,因果自注意力允许每个标记仅关注其左侧上下文。这种架构不适合文本理解等任务,但使得模型在文本生成方面表现优秀。
因果自注意力 [4]
即,因果自注意力使模型能够学习一系列单词的概率,这是“语言模型” [8] 的核心。给定一个符号序列 x=(s1, s2, …, sn),模型可以预测该系列的概率如下。
一系列符号的联合概率 [6]
因果自注意力是 Transformer 解码器块的关键组成部分。第一个预训练的 Transformer 解码器之一是 OpenAI 的 GPT [5]。与 BERT 类似,该模型也旨在利用大量未标记的文本数据集来构建预训练语言模型。预训练于 Book Corpus[7] 上,该模型的目标是预测下一个标记。然后对预训练模型进行微调以适应下游任务。
GPT-2 [6] 采用了相同的构建通用词表示的方法,但更具雄心。它旨在成为一个“多任务学习者”,在不进行微调的情况下执行不同任务。GPT 只学习p(output|input) 的分布,这使得模型在“做什么任务”方面缺乏上下文***。*** 作者希望通过将预测条件化为输入和任务来将 GPT-2 适应多任务,p(output|input, task)。
之前的方法在架构层面上结合了“任务”信息,但 GPT-2 通过自然语言“表达”任务,使其更加灵活。例如,翻译任务的输入可以是“translate to French, ”。
从大量未标记的文本中提取明确的“任务”信息可能是具有挑战性的。然而,作者认为模型可以从自然语言中推断隐含的“任务”表达。因此,他们收集了一个庞大且多样化的数据集,可以在各种领域展示“任务”。即,模型在包含 4500 万个链接文本子集的 WebText 数据集[6] 上进行了训练。
尽管在一些基准测试中的表现不够出色,但 GPT-2 为许多后来的大型语言模型奠定了基础,如 GPT-3 [9] 和 ChatGPT。特别是,GPT-3 能够仅通过基于文本的交互来理解任务和示例。对于 SuperGLUE 基准测试 [10],一组语言理解任务,GPT-3 在没有基于梯度的更新的情况下,相较于微调后的 BERT 展现了令人印象深刻的表现。
GPT-3 和 BERT 在 SuperGLUE 上的表现 [9]
选择哪个模型?
根据模型的结构,我们可以得出结论,BERT 在理解语言和提取上下文信息方面表现出色,使其非常适合情感分析和文本分类等任务。相比之下,GPT 模型旨在生成类似人类的文本,使其成为聊天机器人和语言生成任务的首选。
另一个重要因素是我们的数据资源。我们可以仅用少量数据轻松定制最近的 GPT 模型以完成特定任务,使其适用于更广泛的应用。另一方面,BERT 微调可能需要更多的努力和数据。有关微调 LLM 技术,你可以查看我的文章。
大型语言模型 (LLM) 以其出色的文本生成能力,已经彻底改变了自然语言处理 (NLP) 领域…
medium.com](https://medium.com/mlearning-ai/a-simple-survey-of-fine-tuning-techniques-for-large-language-models-6c7945e6ee34?source=post_page-----329d105e34ec--------------------------------)
最后但同样重要的是,我们还需要考虑计算资源。尽管进行了许多优化努力,但相比于 BERT,微调、存储和服务 LLM 仍然需要大量资源。
或者你也可以通过将它们结合起来享受两者的最佳体验。我将在未来的文章中讨论这个话题。
目前,希望你享受阅读 😃
参考文献
[1] Devlin, Jacob 等. “Bert: 语言理解的深度双向变换器的预训练。” arXiv 预印本 arXiv:1810.04805 (2018).
[2] Rogers, Anna, Olga Kovaleva, 和 Anna Rumshisky. “BERT 学科概述:我们对 BERT 工作原理的了解。” 计算语言学协会会刊 8 (2021): 842–866.
[4] www.tensorflow.org/text/tutorials/transformer
[5] Radford, Alec 等. “通过生成预训练提高语言理解。” (2018).
[6] Radford, Alec 等. “语言模型是无监督的多任务学习者。” OpenAI 博客 1.8 (2019): 9.
[7] Zhu, Yukun 等. “对齐书籍和电影:通过观看电影和阅读书籍实现类似故事的视觉解释。” IEEE 国际计算机视觉会议论文集。2015 年。
[8] en.wikipedia.org/wiki/Language_model
[9] Brown, Tom 等. “语言模型是少量学习者。” 神经信息处理系统进展 33 (2020): 1877–1901.
[10] 王艾利克斯等. “Superglue: 一个更具挑战性的通用语言理解系统基准。” 神经信息处理系统进展 32 (2019).
BERTopic:v0.16 有什么特别之处?
原文:
towardsdatascience.com/bertopic-what-is-so-special-about-v0-16-64d5eb3783d9
探索零样本主题建模、模型合并和 LLM
·发布于Towards Data Science ·阅读时长 8 分钟·2023 年 12 月 13 日
–
我对BERTopic的愿景是通过提供显著的灵活性和模块化,使其成为一站式主题建模解决方案。
这已经是过去几年中的目标,并且随着v0.16 版本的发布,我相信我们离实现这一目标已经更进一步。
首先,让我们稍微回顾一下。什么是 BERTopic?
BERTopic 是一个主题建模框架,允许用户基本上创建自己的主题模型版本。由于实现了多种主题建模变体,目标是支持几乎任何用例。
BERTopic 的模块化特性允许你按照自己的方式构建主题模型。通过切换组件,BERTopic 可以随着语言人工智能的最新发展而不断成长。
在v0.16版本中,实施了几个功能,我相信这些功能将把 BERTopic 带到一个新的水平,即:
-
零样本主题建模
-
模型合并
-
更多的大型语言模型(LLM)支持
仅仅是 BERTopic 的一些功能。
在本教程中,我们将介绍这些特性以及它们可能对哪些用例有帮助。
首先,你可以按照以下步骤安装 BERTopic(包含 HF 数据集):
pip install bertopic datasets
你还可以跟随Google Colab Notebook来确保一切按预期工作。
更新:我上传了一个 YouTube 视频,更深入地讲解了如何使用这些新特性:
零样本主题建模:一种灵活的技术
零-shot 技术通常指的是没有用于训练数据的示例。尽管你知道目标是什么,但它并没有被分配给你的数据。
在 BERTopic 中,我们使用零-shot 主题建模来在大量文档中找到预定义的主题。
想象一下,你有关于机器学习的 ArXiv 摘要,并且你知道“大型语言模型”这个主题在其中。通过零-shot 主题建模,你可以让 BERTopic 找到所有与“大型语言模型”相关的文档。
本质上,它不过是语义搜索!但是……有一个很酷的技巧 😉
当你试图找到与“大型语言模型”相关的文档时,会有许多文档与这些主题无关。那么,你会如何处理这些主题?你可以使用 BERTopic 来找到所有剩下的主题!
结果是,你将有三种零-shot 主题建模的场景:
-
未检测到零-shot 主题。这意味着没有文档符合预定义的主题,因此将运行常规的 BERTopic。
-
仅检测到零-shot 主题。在这种情况下,我们不需要寻找额外的主题,因为所有原始文档都已被分配到预定义的主题之一。
-
检测到零-shot 主题和聚类主题。这意味着一些文档会符合预定义的主题,而其他文档则不符合。对于后者,发现了新的主题。
使用零-shot BERTopic 非常简单:
from datasets import load_dataset
from bertopic import BERTopic
from bertopic.representation import KeyBERTInspired
# We select a subsample of 5000 abstracts from ArXiv
dataset = load_dataset("CShorten/ML-ArXiv-Papers")["train"]
docs = dataset["abstract"][:5_000]
# We define a number of topics that we know are in the documents
zeroshot_topic_list = ["Clustering", "Topic Modeling", "Large Language Models"]
# We fit our model using the zero-shot topics
# and we define a minimum similarity. For each document,
# if the similarity does not exceed that value, it will be used
# for clustering instead.
topic_model = BERTopic(
embedding_model="thenlper/gte-small",
min_topic_size=15,
zeroshot_topic_list=zeroshot_topic_list,
zeroshot_min_similarity=.85,
representation_model=KeyBERTInspired()
)
topics, probs = topic_model.fit_transform(docs)
我们可以查看三个预定义的主题以及几个新发现的主题:
topic_model.get_topic_info()
请注意,尽管我们对主题有预定义的名称,但我们允许 BERTopic 进行额外的表示。
这为预定义的主题提供了令人兴奋的新见解!
那么……你什么时候使用零-shot 主题建模?
如果你已经知道数据中的一些主题,这是一个很好的解决方案来找到它们!因为它可以发现预定义的和新的主题,是一种非常灵活的技术。
模型合并:联邦学习和增量学习
这是一个有趣的新功能,模型合并!
模型合并指的是 BERTopic 将多个预训练的 BERTopic 模型合并为一个大型主题模型的能力。它探索哪些主题应该合并,哪些主题应该保持分开。
它的工作原理如下。当我们将一系列模型传递给这个新功能.merge_models
时,列表中的第一个模型被选择为基准。这个基准模型用来检查所有其他模型是否包含基于主题嵌入相似性的新的主题。
不同的主题被添加到基准模型中,而相似的主题则被分配到基准主题中。这意味着我们需要嵌入模型是相同的。
在合并 BERTopic 模型时,重复的主题将被合并,所有其他主题将保持不变。
合并预训练的 BERTopic 模型很简单,只需要几行代码:
from bertopic import BERTopic
# Merge 3 pre-trained BERTopic models
merged_model = BERTopic.merge_models(
[topic_model_1, topic_model_2, topic_model_3]
)
就这样!通过一个函数.merge_models
,你可以合并预训练的 BERTopic 模型。
合并预训练模型的好处在于,它允许多种创造性和有用的应用场景。例如,我们可以用它来:
-
增量学习 — 我们可以通过迭代合并模型来不断发现新主题。这可以用于问题票证,以快速发现紧迫的错误/问题。
-
批量学习 — 对于大型数据集,或者当你的硬件资源不足时,计算和内存问题可能会出现。通过将训练过程拆分为更小的模型,我们可以在减少所需计算的同时获得类似的性能。
-
联邦学习 — 合并模型允许将训练分布在不同的客户端之间,这些客户端不愿分享他们的数据。这增加了数据的隐私和安全,特别是如果使用基于非关键词的方法来生成表示,例如使用大型语言模型。
联邦学习相当简单,只需在你的中央服务器上运行.merge_models
。
另外两个,增量学习和批量学习,可能需要一些示例!
增量学习和批量学习
为了执行增量和批量学习,我们将模拟一个典型的.partial_fit
管道。在这里,我们将首先训练一个基础模型,然后迭代地添加一个新的小型训练模型。
在每次迭代中,我们可以检查是否有任何主题被添加到基础模型中:
from bertopic import BERTopic
from bertopic.representation import KeyBERTInspired
from datasets import load_dataset
# Prepare documents
all_docs = load_dataset("CShorten/ML-ArXiv-Papers")["train"]["abstract"][:20_000]
doc_chunks = [all_docs[i:i+5000] for i in range(0, len(all_docs), 5000)]
# Base Model
representation_model = KeyBERTInspired()
base_model = BERTopic(representation_model=representation_model, min_topic_size=15).fit(doc_chunks[0])
# Iteratively add small and newly trained models
for docs in doc_chunks[1:]:
new_model = BERTopic(representation_model=representation_model, min_topic_size=15).fit(docs)
updated_model = BERTopic.merge_models([base_model, new_model])
# Let's print the newly discover topics
nr_new_topics = len(set(updated_model.topics_)) - len(set(base_model.topics_))
new_topics = list(updated_model.topic_labels_.values())[-nr_new_topics:]
print("The following topics are newly found:")
print(f"{new_topics}\n")
# Update the base model
base_model = updated_model
举例来说,这将返回新发现的主题,例如:
以下是新发现的主题:
[
‘50_forecasting_predicting_prediction_stocks’,
‘51_activity_activities_accelerometer_accelerometers’,
‘57_rnns_deepcare_neural_imputation’
]
它保留了原始模型中的所有内容,包括
我们不仅通过将训练过程拆分为多个部分来减少计算量,还可以监控模型中新增的主题。
实际操作中,你可以使用适合你用例的频率来训练新模型。你可以每月、每周,甚至每天检查新主题,只要你有足够的数据。
更多大型语言模型支持
尽管我们现在可以在 BERTopic 中使用大型语言模型(LLMs),但 v0.16 版本发布了几个较小的功能,使得使用 LLMs 的体验更加愉快!
总结来说,以下内容被添加了:
-
llama-cpp-python:使用 llama.cpp 加载任何 GGUF 兼容的 LLM
-
截断文档:使用各种技术在传递给任何 LLM 时截断文档。
-
LangChain:支持@joshuasundance-swca的 LCEL Runnables
让我们探索前两个功能的简短示例,llama.cpp 和 文档截断。
当你将文档传递给任何 LLM 模块时,它们可能会超出其令牌限制。相反,我们可以通过定义tokenizer
和doc_length
来截断传递给 LLM 的文档。
截断文档时不同的分词方法。
doc_length
的定义取决于你使用的分词器。例如,100 的值可以指按令牌数或字符数进行截断。
在将文档添加到提示中之前,可以根据分词策略首先对其进行截断。
要将其与llama-cpp-python
一起使用,我们可以考虑以下示例。首先,我们安装必要的包,准备环境,并下载一个小而强大的模型(Zephyr-7B):
pip install llama-cpp-python
CMAKE_ARGS="-DLLAMA_CUBLAS=on" FORCE_CMAKE=1 pip install llama-cpp-python
wget https://huggingface.co/TheBloke/zephyr-7B-alpha-GGUF/resolve/main/zephyr-7b-alpha.Q4_K_M.gguf
在 BERTopic 中用llama-cpp-python
加载 GGUF 模型非常简单:
from bertopic import BERTopic
from bertopic.representation import LlamaCPP
# Use llama.cpp to load in a 4-bit quantized version of Zephyr 7B Alpha
# and truncate each document to 50 words
representation_model = LlamaCPP(
"zephyr-7b-alpha.Q4_K_M.gguf",
tokenizer="whitespace",
doc_length=50
)
# Create our BERTopic model
topic_model = BERTopic(representation_model=representation_model, verbose=True)
就这样!我们创建了一个可以截断输入文档并在不受令牌限制约束的情况下创建有趣主题表示的模型。
感谢阅读!
如果你和我一样,对人工智能和/或心理学充满热情,请随时在LinkedIn和Twitter上加我,或订阅我的新闻通讯。你还可以在我的个人网站上找到我的一些内容。
撰写关于人工智能、语言模型和心理学交集的内容。
maartengrootendorst.substack.com](https://maartengrootendorst.substack.com/?source=post_page-----64d5eb3783d9--------------------------------)
所有未注明来源的图像均由作者创建——这意味着所有图像都是如此,我喜欢自己创作图像;)
BERxiT: 适用于 BERT 的早期退出
介绍用于深度神经网络高效推理的“早期退出”方法,并回顾“BERxiT”论文
·
关注 发表在 Towards Data Science ·11 分钟阅读·2023 年 1 月 14 日
–
作者提供的图片(由 Midjourney 创建)
本文包含两个部分。在第一部分,我介绍了提高推理时间效率的动机,并引入了实现这一目标的“早期退出”概念。在第二部分,我回顾了有趣的论文“BERxiT: 通过更好的微调和回归扩展对 BERT 的早期退出”(Xin, Ji 等)[1],该论文于 2021 年发表,旨在改进早期退出方法。请注意,这篇论文集中于 NLP 领域(使用 BERT 模型),但这一思想可以很容易地应用于其他领域。
第一部分:介绍
推理时间效率的重要性
深度神经网络(DNN)在过去几年中规模显著增长,导致这些模型的训练和推理时间更长。虽然训练成本最初可能显得较高,但在许多情况下,推理成本实际上更高,因为这些模型通常只训练一次,但会被应用数百万次。
高效的推理也很重要,原因如下:
资源限制:在某些情况下,DNN 部署的设备可能资源有限,比如移动设备。在这些情况下,需要快速的推理时间以确保 DNN 能够高效且有效地运行。
用户体验:在许多应用中,DNN 用于提供对用户请求的实时响应。例如,在语音识别系统中,DNN 必须实时处理和分类用户的语音,以提供准确的转录。如果推理时间过慢,用户体验将会很差。
成本:在某些情况下,运行 DNN 的成本可能取决于推理所需的时间。例如,在云计算环境中,用户可能会根据 DNN 运行的时间来收费。
可持续性:关于 DNN 的能源消耗及其对环境潜在影响的讨论很多(例如,参见 [2]、[3] 和 [4]),而且快速的推理时间似乎更具能源效率。
早期退出方法
有不同的方法可以提高推理时间的效率 [5]。显而易见的方向是减少模型的大小,例如通过剪枝或知识蒸馏方法。然而,由于模型的复杂性通常会提高准确性,这可能会影响模型的性能,并且通常需要在常规训练阶段之外的额外步骤。
另一种方法是“早期退出”方法,这也被 RTJ3 [6]、DeeBERT [7] 和 FastBERT [8] 探索过。早期退出的想法源于观察到样本的难度不一 [6]。较长且结构复杂的句子可能需要更多的时间和精力进行分析。考虑以下句子用于情感分析任务:
(1) 餐厅很棒。
(2) 我不确定厨师是否真的有才华,还是食物只是微波加热的冷冻餐。
句子 1 易于分析,因为它短且包含直接的积极语言,表示积极情感。句子 2 更难分析,因为它包含积极和消极的词语,而总体情感是消极的。此外,评论员使用讽刺的语气表达对厨师才华的怀疑,这很难被检测到。
上述观察导致了以下想法:在网络中创建多个决策点,在推理时,让每个样本在网络对其预测有信心的最早点退出。因此,“简单”的样本可能会早早终止,只有“最难”的样本需要通过所有层。这样,网络可以避免执行不必要的计算,从而节省时间和资源。
在 BERT 模型中,这一思想通过在每个 Transformer 层的输出处附加一个小型分类器(除了已经有分类器的最后一层)来实际实现。我称这些分类器为“早期退出组件”。每个分类器的输出是一个概率向量;在这样的向量中,最大概率称为“置信度分数”。在样本的推理时,将每层的置信度分数与预定义阈值进行比较;如果某层的置信度分数大于阈值,则样本以当前预测退出,跳过未来的层。下图展示了这一思想。
左图: 这里第二层的置信度分数(0.95)大于预定义阈值(0.9),因此样本以“Positive”标签的预测从模型中退出。 右图: 这里所有层的置信度分数都小于预定义阈值(0.9),因此没有进行早期退出,预测结果是最终分类器的输出。来源: 链接
第二部分:BERexiT
BERxiT(BERT+exit)论文旨在解决之前工作的两个弱点:
-
微调策略 — 之前的微调策略对于具有早期退出组件的模型并不理想。
-
回归任务 — 之前的工作基于预测概率分布的置信度做出早期退出决策,因此仅限于分类任务。
BERxiT 架构。来源:链接
1. 微调策略
在“常规”神经网络架构中,优化的是单一损失函数。在我们的案例中,为每个 Transformer 层添加了一个早期退出组件,因此存在多个损失项。这对学习过程提出了挑战,因为 Transformer 层必须提供隐藏状态以满足两个竞争的目的:一个是相邻分类器的即时推断,另一个是未来分类器的渐进特征提取。因此,在分类器之间取得平衡至关重要,这也是微调策略的最终目标。
参数
在我介绍不同策略之前,让我们了解需要优化哪些参数。第一组参数是主干模型 Transformer 层的参数,用θ₁, …, θₙ表示。它们的工作是为任务学习良好的特征。第二组参数是 N 个分类器的参数。第 i 个分类器的参数用 wᵢ表示。因此,w₁, …, wₙ是前 n-1 个分类器(早期退出组件)的参数,而 wₙ是最后一个分类器的参数。它们的工作是将隐藏状态映射到一组类别的概率分布。
现在让我们深入了解三种微调策略:
-
联合
-
两阶段
-
交替
联合
在这种简单策略中,损失函数定义为所有 N 个分类器的损失函数之和,主干模型和所有分类器共同训练。
来源: 链接
缺点: 联合方法对所有分类器一视同仁,因此无法保持(原始)最终分类器的性能。这并不理想,因为最终分类器必须提供高度准确的输出;在它之后没有其他分类器来处理那些未被早期退出的样本。
两阶段
在这种策略中,训练阶段分为两个连续的独立阶段: 在第一阶段,仅训练最终分类器以及主干模型。在第二阶段,仅训练前 N-1 个分类器(而最终分类器和主干模型被冻结)。
来源: 链接
缺点: 这种策略产生了一个具有最佳质量的最终分类器,但以早期分类器为代价,因为主干模型参数(大多数参数)仅为最终分类器进行优化。
交替
本文提出了这种策略以克服之前策略的缺点。在该策略中,训练在奇数和偶数轮次之间交替进行不同的目标。在两者中,主干模型都会进行训练,但在奇数轮次中,还训练最终分类器,而在偶数轮次中,还训练前 N-1 个分类器。这样有可能在最终分类器的性能和早期退出组件的性能之间取得平衡。
来源:链接
2. 回归任务
“当模型对其预测有高置信度时停止推理”的方法不能应用于回归任务,因为回归任务输出的是实数而非概率。
为了将这个思路扩展到回归任务,作者建议使用一个对所有层共享的学习退出(LTE)组件。这个组件是一个单层全连接网络,它将某层的隐藏状态作为输入,并输出该层预测的置信得分。因此,在样本的推理时间,如果某层生成的置信得分高于阈值,隐藏状态也会被插入到相邻的回归器中以生成该样本的输出,推理过程则停止。
注意,LTE 是另一个需要训练的参数组件。该组件的损失函数是生成的置信得分 uᵢ与第 i 层的“真实值”置信得分 ũᵢ之间的简单 MSE:Jᵢ = ||uᵢ − ũᵢ||₂²。ũᵢ通过否定预测的绝对误差来估计:ũᵢ = 1- tanh( |gᵢ(hᵢ ;wᵢ) − y| ),其中 y 是真实值,gᵢ(hᵢ ;wᵢ)是第 i 个回归预测值。
LTE 组件通过将 Lᵢ替换为 Lᵢ+Jᵢ(i=1,…,n-1)与模型的其余部分一起训练。
实验
本文进行了几个实验。我将回顾其中的三个。
实验 1:微调策略比较
第一个实验比较了 3 种微调策略(涵盖 6 种不同的分类任务),通过展示它们的逐层得分曲线来进行比较:曲线中的每一点表示在某一退出层的输出得分,即所有样本必须在此层退出以进行评估。注意,这些得分已转换为相对于 BERTᵇᵃˢᵉ基准模型(值为 100%)的相对得分,这是一种不包含早期退出组件的模型。
比较了两阶段(2STG)、联合和交替(ALT)微调策略。 来源:链接
从图中得到的几项观察:
-
模型的准确性随着退出层的延迟而提高,这有意义,因为较深的层具有更高的复杂度。
-
两阶段策略在早期层次上表现不佳,这也有意义,因为该策略在现有层次的成本上重度优化了最后的分类器。
-
交替策略在后期层次上优于联合策略,而在早期层次上略显逊色。
结论是交替策略在早期退出组件上提供了良好的结果,同时保持了最终分类器的性能。
实验 2:质量–效率权衡
在本实验中使用了几种模型:
-
Raw — 没有早期退出组件的 BERTᵇᵃˢᵉ模型(基准)
-
ALT — BERTᵇᵃˢᵉ + 早期退出组件,采用交替微调策略
-
DB — DistilBERT,使用知识蒸馏方法将 BERTᵇᵃˢᵉ模型缩减为更小的模型
-
DB+ALT — DistilBERT + 早期退出组件,采用交替微调策略
这些模型通过两个指标进行了比较,以检查质量–效率权衡:
-
模型质量指标:Raw 的准确率得分和其他模型的相对得分(相对于 Raw 模型)。
-
模型效率指标:RAW 的层数和其他模型相对节省的层数(相对于 Raw 模型)。对于 ALT 和 DB+ALT 模型,节省的层数通过使用平均退出层计算得出。
实验目标是首先检查所提出模型(ALT)与基准模型(RAW)的质量-效率权衡。其次,检查所提出模型(ALT)是否优于另一种强效的方法(DB)。最后,检查是否通过在 DistilBert 模型(DB)上应用所提出模型(DB+ALT)可以改进 DistilBert 模型(DB)。
注意,与实验 1 相比,此处对早期退出组件(ALT 和 DB+ALT)的模型应用了“常规”推理阶段:测试集样本在某层的置信度分数高于阈值时可以自由退出。此外,ALT 中的三行不同是通过调整置信度阈值生成的。
质量–效率权衡。来源:链接
以结果为例:MRPC 数据集上的第一个 ALT 模型平均未使用 30%的层,但仍实现了 RAW 基准模型得分的 99%!降低置信度阈值导致模型效率提高(平均节省 56%和 74%),质量降级合理(分别为 97%和 94%)。
主要观察结果:
-
使用早期退出(配合交替微调)可以减少推理计算,同时仍能获得良好的得分,与没有早期退出组件的基准模型相比。
-
在大多数情况下,交替策略优于 DistilBERT,而 DistilBERT 需要在预训练中进行蒸馏,因此资源需求更高。
-
使用交替策略进一步提高了 DistilBERT 上的模型效率,表明早期退出与其他加速方法是累积的。
实验 3:回归任务
在此实验中,将所提出的模型(ALT-LTE)与先前的模型(PABEE)在预测两个句子相似度的任务(STS-B 数据集)上进行了比较。
比较 LTE 与 PABEE 在 STS-B 上的表现。来源:链接
如图所示,ALT-LTE 在推理时间上取得了相同的分数。
结论
-
快速推理时间对于部署在资源受限设备上的深度神经网络(DNN)至关重要,这不仅是为了向用户请求提供实时响应,还涉及到成本和可持续性问题。通过允许样本在网络中的不同深度退出,“早期退出”方法提高了推理时间,可能使许多“较容易”的样本提前退出,从而避免不必要的计算,同时保持准确性。
-
BERxiT 论文通过提出交替微调策略改进了这一方法,其目标是在最终分类器的性能与早期退出组件的性能之间取得平衡。此外,BERxiT 通过提出学习退出(LTE)组件,将早期退出方法扩展到回归任务,该组件学习输出置信度分数。
-
实验结果表明,交替策略在质量-效率权衡上表现更佳,LTE 组件在回归任务中确实有效,并且早期退出方法可以与其他加速方法结合使用。
参考文献
[1] Xin, J., Tang, R., Yu, Y., & Lin, J.J. (2021). BERxiT:BERT 的早期退出,改进微调及扩展至回归任务. 欧洲计算语言学协会会议。
[2] Strubell, E., Ganesh, A., & McCallum, A. (2019). 自然语言处理中的深度学习的能耗与政策考虑. ArXiv, abs/1906.02243。
[3] Desislavov, R., Mart’inez-Plumed, F., & Hern’andez-Orallo, J. (2021). 深度学习推理中的计算和能耗趋势. ArXiv, abs/2109.05472。
[4] Schwartz, R., Dodge, J., Smith, N., & Etzioni, O. (2019). 绿色人工智能. ACM 通讯,63, 54–63。
[5] Treviso, M.V., Ji, T., Lee, J., van Aken, B., Cao, Q., Ciosici, M.R., Hassid, M., Heafield, K., Hooker, S., Martins, P.H., Martins, A., Milder, P., Raffel, C., Simpson, E., Slonim, N., Balasubramanian, N., Derczynski, L., & Schwartz, R. (2022). 自然语言处理的高效方法:综述。ArXiv, abs/2209.00099。
[6] Schwartz, R., Stanovsky, G., Swayamdipta, S., Dodge, J., & Smith, N.A. (2020). 合适的工具:模型与实例复杂度的匹配。计算语言学协会年会。
[7] Xin, J., Tang, R., Lee, J., Yu, Y., & Lin, J.J. (2020). DeeBERT:加速 BERT 推理的动态早期退出。计算语言学协会年会。
[8] Liu, W., Zhou, P., Zhao, Z., Wang, Z., Deng, H., & Ju, Q. (2020). FastBERT:一种自我蒸馏的 BERT 与自适应推理时间。计算语言学协会年会。
PySpark 中最好的数据整理函数
原文:
towardsdatascience.com/best-data-wrangling-functions-in-pyspark-3e903727319e
学习在处理大数据时使用 PySpark 的最有用函数
·发布于 Towards Data Science ·阅读时长 7 分钟·2023 年 12 月 12 日
–
图片由 Oskar Yildiz 提供,来源于 Unsplash。
介绍
我每天在 Databricks 中使用 PySpark。作为一名数据科学家,我的工作需要处理许多不同表中的大量数据。这是一份充满挑战的工作。
尽管 提取、转换和加载 (ETL) 过程听起来很简单,但我可以说它并不总是如此。当我们处理大数据时,我们的思维需要因两个原因而改变:
-
这些数据的规模远远大于常规数据集。
-
在集群中进行并行计算时,我们必须考虑到数据会被分割到许多工作节点中,以执行部分工作,然后再合并为整体。并且这一过程,很多时候,如果查询过于复杂,可能会非常耗时。
知道这一点后,我们必须学习如何为大数据编写智能查询。在这篇文章中,我将展示一些我最喜欢的来自模块 pyspark.sql.functions
的函数,旨在帮助你在 PySpark 中进行数据整理。
最佳函数
现在,让我们继续讨论这篇文章的内容。
就像许多其他语言一样,PySpark 的模块也提供了许多现成的函数,适用于各种不同的目的。这里是我们将加载到会话中的模块:
from pyspark.sql import functions as F
如果你想查看pyspark.sql.functions
中函数的扩展列表,请访问这个网站,在那里有 API 参考。请注意,这适用于 3.5.0 版本。一些旧版本可能不包含我将在这篇文章中展示的所有函数。
数据集
用作示例的数据集是Diamonds,来自 ggplot2,依据MIT 许可证共享。
# Point file path
path = '/databricks-datasets/Rdatasets/data-001/csv/ggplot2/diamonds.csv'
# Load Data
df = spark.read.csv(path, header=True, inferSchema= True)
创建索引列
对于那些在 Python 中使用 Pandas 的人来说,刚开始处理没有索引的数据框时会觉得很奇怪。因此,如果我们想要添加一个索引列,可以使用函数monotonically_increasing_id()
。计数从 0 开始。因此,如果我们想从 1 开始,只需在函数后加上+1
。
# Add an increasing ID column starting in 1
display(
df
.limit(100)
.withColumn('ID', F.monotonically_increasing_id()+1 )
)
总和、均值、最大值、最小值
经典的数学函数肯定会出现在这个列表中。无论何种情况都很有用。
display(
df
.groupBy('cut')
.agg( F.sum('price').alias('total'),
F.mean('price').alias('avg_price'),
F.min('price').alias('min_price'),
F.max('price').alias('max_price') )
)
计数和计数不同
计数值和了解数据中有多少个不同值也是很重要的。
display(
df
.groupBy('cut')
.agg( F.count('cut').alias('n_count'), #count of obervations
F.countDistinct('price').alias('distinct') ) #distinct n prices
)
字面值
函数lit()
允许你为数据中的每一行写入一个字面值。
display(
df #dataset
.limit(10) #only 10 rows
.withColumn('literal', F.lit('my text or number')) #add column with literal value
)
lit(‘my text or number’). 图片由作者提供。
向下取整、向上取整和百分位数
一些在处理数据时非常有用的数学函数是floor
——向下取整的整数——和ceiling
——向上取整的整数。
display(
df
.limit(10)
.select('x')
.withColumn('floor', F.floor('x') )
.withColumn('ceiling', F.ceiling('x') )
)
向上取整和向下取整。图片由作者提供。
现在百分位数特别有用,尤其是用于计算中位数。直到不久前,我记得我在计算中位数时遇到过错误。现在我发现它在版本 3.5.0 中已经存在。最好的解决方法是使用percentile()
在 50%处计算。
display(
df
.groupBy('cut')
.agg( F.median('price').alias('median'),
F.percentile('price', 0.5).alias('50th pct'))
)
描述性统计
尽管describe()
不在sql.functions
模块中,但它也适用于那些熟悉 Pandas 的人。它提供了数据集的描述性统计信息。这里提供的数字有:计数、均值、标准差、最小值和最大值。
# Descriptive Stats
display(
df
.describe()
)
结果如下:
describe()函数的结果。图片由作者提供。
对数
作为数据科学家,我们常常使用对数函数。特别是在进行线性回归时,它是变量标准化的辅助工具。
# Calculating different Logs of 'price'
display(
df
.select( F.ln('price').alias('Ln'),
F.log1p('price').alias('Log1p'),
F.log10('price').alias('Log10'))
)
对数变量已计算。图片由作者提供。
数组聚合
array_agg
是一个很好的函数,用于获取一个组的值并将其列出在新列中。假设我们想按切割质量对钻石进行分组,并查看列出的价格。以下代码片段执行了这个操作。
# Get the aggregated values and list them in a new variable
display(
df.limit(50)
.groupBy('cut')
.agg( F.array_agg('price'))
)
按组列出值。图片由作者提供。
计数 IF
我敢打赌,如果你使用过 MS Excel,这听起来很熟悉,对吧?想法是一样的。如果我们想要计算在分组后价格超过 18,000 美元的钻石数量,可以使用这个函数。看看吧。
display(
df
.groupBy('cut')
.agg( F.count_if( col('price') > 18000))
)
我们有更多的理想和优质切工,价格昂贵,几乎没有公平。
计数操作。图片来自作者。
众数
变量的众数是最常见的值。现在我们想知道每种切工质量的最常见克拉数是什么。
# Most common value
display(
df
.groupBy('cut')
.agg( F.mode( 'carat' ).alias('mode') )
)
回归函数
这些函数非常有趣。我们可以快速计算线性回归指标,如 R 平方值、截距、斜率,使用这些函数:regr_r2
、regr_intercept
、regr_slope
、regr_avgx
。
在下一段代码中,我们将计算每组的回归 R 平方值和公式。
# Remember that the regression formula is y = a + b*x
(
df
.groupBy('cut')
.agg( F.regr_r2( 'price', 'carat').alias('regression_r2'),
F.lit('y ='),
F.regr_intercept( 'price', 'carat').alias('intercept'),
F.lit('+'),
F.regr_slope( 'price', 'carat').alias('reg_slope'),
F.lit('*'),
F.regr_avgx( 'price', 'carat').alias('avg_x') )
).show()
这非常酷!理想切工(最佳质量)具有最高的 R²,而公平切工(最低质量)具有最低的 R²。这是有道理的。
按组回归。图片来自作者。
正则表达式
是的,正则表达式无处不在。我看到很多人对那些漂亮的表达式翻白眼,但它是一个很棒的工具。想象一下,你可以使用这些表达式从文本中提取几乎任何东西。一开始可能会很困难且曲折,但一旦你了解得更多,你会开始喜欢它们。PySpark 的函数中也有这些。
在这里,我使用regexp()
与文字文本lit
结合,检查变量clarity
中是否有数字。下一行是函数locate
,用于定位在同一变量中字母‘S’首次出现的位置。
# Using Regular Expessions
display(
df
.select( 'clarity',
F.regexp('clarity', F.lit(r'(\d+)')),
F.locate('S', 'clarity', 1) )
)
文本解析函数。图片来自作者。
文本解析
关于文本解析,我们可以使用split()
将文本拆分成部分。在下一段代码中,我将carat
列转换为文本,并按.
拆分。所以像 0.23 这样的数字变成了[“0”, “23”]。然后,我只需使用切片表示法将结果放在不同的列中。
display( df
.select( col('carat').cast('string'))
.select( F.split('carat', '\.')[0],
F.split('carat', '\.')[1] )
)
拆分函数结果。图片来自作者。
另一种解析可能性是函数left
,类似于 MS Excel。你有一列文本,希望只获取其中的 N 个字符。只需使用left
与lit
组合。
display( df
.select('cut')
.withColumn('first3', F.left('cut', F.lit(3)))
)
左侧函数结果。图片来自作者。
在你离开之前
数据处理是一门艺术。不管是使用 PySpark、R 还是 Python,你总是需要最好的函数来实现你的转换。在这里,我列出了pyspark.sql.functions
模块中的一些函数。我建议你访问文档页面,创建你自己最好的函数列表。
我最好的数据转换建议是:
-
知道你的最终结果应该是什么样的。
-
从那里,你可以将过程拆分成更小的步骤,以实现目标。
我在职业生涯初期学习到这一点,当时我经常使用 Excel 表格。当我不知道如何写一个复杂的公式时,我会在不同的单元格中写出较小的部分,直到得到期望的结果。然后,只需将这些部分汇总在一起,就能作为一个公式使用。
同样的原则适用于编程。创建你的策略时要以最终目标为导向,并按步骤进行(如有需要)。就是这样。
如果你喜欢这内容,可以关注我的博客获取更多信息,并订阅我的通讯。
在 Medium 上阅读 Gustavo Santos 的文章。数据科学家。我从数据中提取洞察,以帮助人们和公司……
也可以在 LinkedIn 上找到我。
有兴趣深入了解 PySpark 吗?
我刚刚发布了我的新在线课程 在 Databrick 中掌握 PySpark 数据处理。这是一个很好的机会,让你提升技能,深入了解大数据处理!
我提供了 这场免费的网络研讨会,其中包含了我关于如何更快编写 PySpark 查询的顶级技巧。查看一下,你会发现里面有一个特别的首发优惠券!
参考资料
## Functions - PySpark 3.5.0 documentation