TowardsDataScience-2023-博客中文翻译-十二-

TowardsDataScience 2023 博客中文翻译(十二)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

将平坦表格转换为 Power Query 中的良好数据模型

原文:towardsdatascience.com/converting-a-flat-table-to-a-good-data-model-in-power-query-46208215f17a

当将一个宽 Excel 表格加载到 Power BI 中时,我们最终会得到一个次优的数据模型。我们可以做什么来创建一个好的数据模型?什么才算是“好的”数据模型?让我们深入了解一下。

Salvatore CagliariTowards Data Science Salvatore Cagliari

·发布在 Towards Data Science ·12 分钟阅读·2023 年 12 月 22 日

--

图片由 Kaleidico 提供,发布在 Unsplash

引言

只是办公室里的另一天:一个客户打电话让我修复他 Power BI 报告中的某些内容。我查看了数据,发现有一个宽表,包含 30、40 或更多列。

我问了一个自然的问题:“这个表格的来源是什么?”

答案是“Excel,还能有什么?”

“当然,”我想。

我的下一个问题:“我们能从中建立一个好的数据模型吗?”

我的客户:“为什么?”

这就是我们现在的状况。

理想情况下,我会将源文件导入到关系数据库中,并将数据转换为一个不错的数据模型。

不幸的是,我的客户通常不愿意为那些初看上去对他们没有好处的东西付费。

但是为什么我想要一个好的数据模型?一个平坦的表格不是也很好吗?

再看一眼,确实如此。

几千行的数据没有问题。

但一旦数据量增加,问题可能会积累。

这是对“为什么?”问题的简短回答:

那么,为什么需要一个好的数据模型?

有很多原因让我想要一个“好的数据模型”。

两个简短的原因是效率和可用性。

当将数据类型分隔到不同的表中,并通过关系将它们连接起来时,Power BI 可以以更高效的方式工作。

此外,通过去除冗余,可以减少 Power BI 文件的大小。

我强烈推荐阅读 SQLBI 上的相关文章。你可以在下方的参考文献部分找到链接,以获取这个问题的详细答案。

其次,想要使用数据模型的人可以在单独的表中找到所有列(即属性),而不是在一个长的按字母排序的列表中搜索。

现在我们知道了为什么,接下来的问题是,什么是一个好的数据模型?

答案是:一个星型模式。

什么鬼是星型模式?

如果你已经知道什么是星型模式,可以跳到下一节。

我的原始数据模型如下:

图 1 — 原始数据模型,包含一个有 27 (!) 列的表(图由作者提供)

中心表是图中间的那个大表,名为“Azure_UsageDetails”。我们称之为“原始表”。

其他两个表是用于时间报告的日期表。

当你仔细查看原始表时,你可以找到一些有趣的列,例如:

  • BillingProfile

  • MeterName

  • SubscriptionName

这些列的共同点是,虽然表中有大约 55,000 行,这些列只有少量不同的值。

这意味着基数是低的。

此外,这些列描述了我的数据。它们不包含任何值,例如“Quantity”或“UnitPrice”。

目标是将这些列移动到单独的表中,称为维度表,以获得只包含这些列中的唯一值或值组合的较短表。

看一下下面的图示:

图 2 — 作为星型模式的目标数据模型(图由作者提供)

现在,你可以理解为什么它被称为星型模式。

我们可以称之为鱿鱼模式,但没有人会理解我们。

正如你所见,每个维度表都有一个 ID 列与中心表中的相应 ID 列相连。

ID 列应始终是整数数据类型。绝不要文本列。

星型模型中心的表称为“事实”表。它仅包含每个事件、交易或数据点的值。

让我们开始吧

好的,现在我们需要执行一些准备步骤:

  1. 查找低基数的列。

  2. 查找需要分组的列。

  3. 为每个维度表定义名称。

  4. 替换所有空单元格。

  5. 复制包含数据的表。

首先,打开 Power Query。

其次,我们启用数据分析:

图 3 — 在 Power Query 中启用数据分析(图由作者提供)

Power Query 仅显示前 1000 行的分析结果。启用整个数据集的分析可能是个好主意。

在左下角,点击文本“基于前 1000 行的列分析”,然后切换到“基于整个数据集的列分析”选项:

图 4 — 在整个数据集上开启分析(图由作者提供)

根据数据量,加载整个数据集并计算分析可能需要一些时间。

当我查看我的数据时,我发现这里有前三个候选项:

图 5 — 维度的前三个候选(作者制作的图)

在这些中,我可以创建三个维度:

  • 计费配置文件

  • 订阅

  • 计量器

如你所见,我为维度表赋予了复数名称,每个表包含一个或多个该实体的实例。

现在,查看以“Meter”开头的列:

图 6 — 所有四个计量器列(作者制作的图)

我注意到两个关键细节:

  • 这四列的基数不同。

  • 我有很多空行在 MeterRegion 列中(76%)。

根据基数(不同值的数量),我认为我可以为计量器构建一个层次结构。

  • 一级:计量器区域

  • 第二级:计量器类别

  • 第三级:计量器子类别

  • 叶级:计量器名称

来自 Fact 表的数据将与 Meter Name 列建立关系。更准确地说,ID 列将基于叶级别但从所有四列的不同组合中创建。

组合的原因是计量器名称可能会多次出现,并分配给不同的计量器子类别。

第二步,我们必须用有意义的内容替换 Meter Region 列中的空行,以避免在顶级层次上出现名为(Null)的层次节点。

为了实现这一点,我右键点击列名,然后点击“替换值”。

在此功能的对话框中,我输入null作为要查找的值,并输入“空”作为要替换的值:

图 7 — 使用文本“空”替换空行(作者制作的图)

结果如下,MeterName 列中没有空行:

图 8 — MeterRegion 没有空行。但其他列仍有空白行。(作者制作的图)

接下来,我必须检查所有打算作为维度列的列,并替换空值。

我通过检查 Profiling 区域中的“空”行找到了这些列(见红色标记的行)。

你可以使用另一个词代替“空”作为空行的替换文本。例如,“无”。

下一步是为了避免在构建维度表时出现重复的列名:

我将所有以“Id”结尾的列重命名为“GUID”(除了 InvoiceID,这列将保持不变)。

接下来,随着数据的清理,我可以复制我的表并开始构建维度表。

我右键点击 Azure_UsageDetail 表,点击“复制”:

图 9 — 复制表(作者制作的图)

现在,我将表重命名为 Azure_UsageDetail_Copy。

但我不需要在我们的数据模型中复制这些数据。

所以,我关闭了该表的加载:

图 10 — 禁用将表加载到 Power BI 数据模型中的功能(图由作者提供)

这个选项的效果是,我可以将此表作为所有后续操作的源,而无需在 Power BI 中存在此表。

现在,我可以使用原始表的副本来构建我的维度表。

一个简单的维度表

第一个维度将用于订阅。

我需要以下步骤:

  1. 创建一个引用复制表的表。

  2. 移除所有其他列。

  3. 移除所有重复项。

  4. 添加一个 ID 列。

完成这些步骤后,我必须将列添加到原始表中:

  1. 合并两个表。

  2. 展开 ID 列。

  3. 从原始表中移除订阅列。

右键点击复制的表,点击“引用”(参见下文“重复”和“引用”表的区别)。

现在,我必须将表重命名为 Subscriptions(双击表)。

我可以使用“选择列”或“移除其他列”功能,去除除了两个订阅列之外的所有列。

我选择两个列(使用 Shift 点击)并右键点击:

图 11 — 在选择了两个“订阅”列后,删除所有其他列(图由作者提供)

下一步是移除所有重复项:

图 12 — 从表中移除重复项(图由作者提供)

现在,我有一个包含两列和两行的小表。

为了添加一个 ID 列,我使用索引功能:

图 13 — 添加一个索引列(图由作者提供)

然后,我将新创建的“索引”列重命名为“SubscriptionId”。

现在,订阅维度表已经完成。

图 14 — 完成的订阅维度表(图由作者提供)

在此之后,我必须用新的 ID 列替换原始表中的现有“订阅”列:

我使用“合并查询”功能将两个表连接在一起:

图 15 — 将新的维度表与原始表合并(图由作者提供)

选择两个列至关重要,以确保正确的行被分配。

你可以检查底部信息行中的正确分配:当两个数字相同时,一切正常。

为了获取 Id 列,我必须展开合并的表:

图 16 — 展开合并的表,只包括 Id 列,不带原始列名称作为前缀(图由作者提供)

结果,我得到一个附加的列,其匹配的 SubscriptionId。

我对所有“简单”维度表重复相同的步骤。

但有时,我需要在构建维度表时添加更多步骤。

更复杂的内容

在转换数据模型的过程中,我注意到一组列是相关的:

  • 所有“账单”列

  • ChargeType

  • Frequency

  • PublisherType

  • PricingModel

  • InvoiceNo

所有这些列都与发票主题相关联。

因此,我决定将它们分组到一个维度表中。

第一步与上述相同:

1. 创建一个引用表。

2. 删除所有不需要的列。

3. 为 ID 添加索引列。

4. 将列重命名为更具用户友好的名称。

但后来我注意到,我可以使用 Billing Period 列来提取 Billing Month,然后删除这个列。这对我的报告将带来好处。

因此,我添加了一个自定义列,公式如下:

Date.MonthName([Billing Period]) & "-" & Text.From(Date.Year([Billing Period]))

这是结果:

图 17 — Billing Month 的结果(图由作者提供)

为确保该列可以根据月份编号进行排序,我添加了一个额外的自定义列:

(Date.Year([Billing Period])*100) + Date.Month([Billing Period])

我可以使用 Power BI 中的按列排序功能,根据这个新的 BillingMonthNum 列对 Billing Month 列进行排序。

下一步是设置正确的数据类型(Billing Month 列的文本类型和 BillingMonthNum 列的“整数”类型)。

另一种添加排序列的方法是通过排名。

例如,看看 Invoice No 列:

图 18 — 从“Invoice No”列提取(图由作者提供)

想象一下,我们想要为这个列添加一个排序机制。

索引列将不起作用,因为我们会得到一个连续的数字,而无论内容如何。

但是,正如你所见,我们有几行的 Invoice No 为 null。

因此,使用索引在 Power BI 中对该列进行排序是不可能的,因为我们会有相同的内容但数字不同。

我们可以通过使用以下表达式为新的自定义列解决此问题:

Table.AddRankColumn(#"Changed Type","InvoiceNoRank", "Invoice No",[RankKind=RankKind.Dense])
  1. “Changed Type” 是前一步骤(输入表)的名称。

  2. “InvoiceNoRank” 是新列的名称。

  3. “Invoice No” 是排名将被计算的列。

  4. 最后一个参数 [RankKind=RankKind.Dense] 是最重要的。

使用 [RankKind=RankKind.Dense] 我告诉函数,相同发票编号的行必须获得相同的排名,并且数字范围必须连续。

你可以在这里获取更多关于这个功能的详细信息:

[## 使用 Power Query 在 Power BI 中添加预计算的排名

如何在 Power Query 和 Power BI 中添加排名,有多种方法可以将排名列添加到表格中,你…

radacad.com

结果列如下所示:

图 19 — 排名列的结果(图由作者提供)

你可以看到所有空的 Invoice No 行都得到排名 1。随后的发票编号为 2、3、4 等等。

合并此表到事实表时,我选择所有可以唯一标识每一行的列。

在这种情况下,使用列 BillingPeriods 和 Invoice No 就足够了。

最后步骤——完成数据模型

最后,我可以完成数据模型。

但在我跳到 Power BI 之前,我会在 Power Query 中移除原始表中的所有过时列,并将表重命名为“FactAzureUsageDetails”。

将数据加载到 Power BI 后,我必须检查自动创建的关系,以确保所有关系都在 ID 列之间。

有时,Power BI 会在名称列之间创建关系,这并不理想。

由于我已经基于旧的数据模型创建了一个报告,我必须修复所有使用了现在已移动到维度表中的列的可视化和度量值。

最终结果

这是最终的数据模型:

图 20——作为干净的星型模式的最终数据模型(图示由作者提供)

这正是我想要实现的目标。

有趣的是,新生成的 pbix 文件现在比原始文件大。

不过,我之前谈论的是 800 kB 对 750 kB。原始数据的大小约为 20 MB。

我已经进行了这样的更改,结果是 pbix 文件比之前小了很多。

我认为我的数据量如此之小,以至于额外的复杂性导致 pbix 文件变大。

结论

修改数据模型的工作量不小。但也不至于高得令人恐惧。无论如何,探索这种方法是值得的,以便在 Power BI 中获得更好的解决方案,而不是拥有一个列数众多、难以导航的表。

想象一下,你可以将一个主题的所有列分组到一个维度表中。即使这些列按字母顺序排序,你也可以很容易找到它们。与这些列都在原始表中的情况相比,这种方式更为高效。

在我看来,这个话题应该得到更多的关注。我甚至看到我的同事在 Power BI 中使用非常宽的表来构建解决方案。我的第一个评论是,“你为什么要使用这样一张表?为什么你的数据模型中没有星型模式?”。

回答总是:“因为数据以这种形式出现,我没有时间修改结构”。

但只要性能问题开始出现,我的第一步是分析和优化数据模型。

我推荐使用 DAX Studio 和 VertiPaq Analyzer (SQLBI Tools Page) 来获取数据模型的统计信息并发现潜在的问题。

你可以在这里获得关于如何使用这个组合的简要介绍:

[## 如何使用 Vertipaq Analyzer 和 DAX Studio 进行 Power BI 模型分析 - FourMoo | Power BI |…

我展示了如何将 Vertipaq Analyzer 与你的 Power BI 模型配合使用,以理解什么在消耗内存以及如何……

www.fourmoo.com](https://www.fourmoo.com/2020/11/11/how-to-use-vertipaq-analyzer-with-dax-studio-for-power-bi-model-analysis/?source=post_page-----46208215f17a--------------------------------)

或在您最喜欢的搜索引擎中搜索“如何使用 VertiPaq Analyzer”。

我正在考虑很快就这个主题写一篇文章。

如果您对这样的内容感兴趣,请在评论中告诉我。

图片由 Denys Nevozhai 提供,来源于 Unsplash

参考文献

数据来自我的私人 Azure 订阅。我从 Azure 门户下载每月消耗量,并将数据导入到 Azure SQL 数据库中。

一篇关于为什么我们应该将星型架构作为数据模型的 SQLBI 文章:

[## Power BI - 星型架构还是单表 - SQLBI

本文分析了一个经典建模问题:构建模型时,使用常规的星型架构还是…

www.sqlbi.com](https://www.sqlbi.com/articles/power-bi-star-schema-or-single-table/?source=post_page-----46208215f17a--------------------------------)

请参见这里以了解 Power Query 中 Duplicate 和 Reference 功能的区别:

[## Power BI 中的 Reference 与 Duplicate;Power Query 基础回顾

当您在 Power Query 和 Power BI 中处理表格和查询时,您可以通过这些选项将它们复制…

radacad.com](https://radacad.com/reference-vs-duplicate-in-power-bi-power-query-back-to-basics?source=post_page-----46208215f17a--------------------------------) [## 订阅 Salvatore Cagliari 的更新。

订阅后,Salvatore Cagliari 发布文章时会收到电子邮件。通过注册,您将创建一个 Medium 帐户,如果您还没有的话…

medium.com](https://medium.com/@salvatorecagliari/subscribe?source=post_page-----46208215f17a--------------------------------)

将文本转换为数值形式的 TFIDF 向量化器:逐步指南

原文:towardsdatascience.com/converting-texts-to-numeric-form-with-tfidfvectorizer-a-step-by-step-guide-bb9330562ae3

图片由 Mohamed Nohassi 提供,来源于 Unsplash

如何手动计算 Tfidf 值及使用 sklearn

Rashida Nasrin SuckyTowards Data Science Rashida Nasrin Sucky

·发表于 Towards Data Science ·阅读时间 6 分钟 ·2023 年 10 月 25 日

--

TFIDF 是一种将文本转换为数值形式以用于机器学习或人工智能模型的方法。换句话说,TFIDF 是一种从文本中提取特征的方法。这比我在上一篇文章中讨论的 CountVectorizer() 方法更为复杂。

TFIDF 方法为每个词提供一个分数,表示该词的有用性或相关性。它衡量了词语的使用情况,与文档中其他词汇相比。

本文将手动计算 TFIDF 分数,以便您清楚地了解 TFIDF 的概念。在最后,我们还将查看如何使用 sklearn 库中的 TFIDF 向量化器。

这包括两个部分:TF 和 IDF。让我们看看每个部分是如何工作的。

TF

TF 解释为“词频”。TF 可以计算为:

TF = 单词在文档中的出现次数

或者

TF = (文档中出现的次数)/ (文档中的词数)

让我们做一个例子。我们将找到该文档中每个词的 TF:

我的名字是莉莉

让我们分别看一下每个公式的例子。

TF = 单词在文档中的出现次数

如果我们取第一个公式,即文档中单词出现的次数,则“MY”的 TF 是 1,因为它只出现了一次。

以相同的方式,词语的 TF

‘name’ = 1,‘is’ = 1,‘Lilly’ = 1

现在,让我们使用第二个公式。

TF = (文档中出现的次数)/ (文档中的词数)

如果我们使用第二个公式,公式的第一部分(文档中的出现次数)是 1,第二部分(文档中的单词数量)是 4。

因此,单词‘MY’的 TF 为 1/4 或 0.25。

同样,单词的 TF 为

name = ¼ = 0.25,is = ¼ = 0.25,Lilly = ¼ = 0.25。

IDF

IDF 的详细解释是逆文档频率。

这是公式,

idf = 1 + LN[n/df(t)]

idf = LN[n/df(t)]

其中,n = 可用的文档数量,以及

df = 术语出现的文档数量

根据 sklearn 库的文档

idf = LN[(1+n) / (1+df(t))] + 1(默认设置)

idf = LN[n / df(t)] + 1(当 smooth_idf = True 时)

我们不会处理所有四个公式。我们只讨论两个公式。你将会明白的。

为了演示 IDF,单个文档是不够的。我将使用这三个文档:

我的名字是 Lilly

Lilly 是我妈妈最喜欢的花

我妈妈喜欢花

这次我们使用这个公式进行练习:

IDF = LN[n/df(t)]

如果我们先考虑‘My’,n 是 3,因为这里有 3 个文档,而 df(t)也是 3,因为‘My’出现在所有三个文档中。

IDF(MY) = LN(3/3) = 0(因为 ln(1)是 0)

我们将再处理一个单词以便清楚理解。以‘name’为例。

对于单词‘name’,n 将和之前相同,因为文档数量是 3,但 df(t)将是 1。因为单词‘name’只出现在一个文档中。

IDF(name) = ln(3/1) = 1.1(我使用了 Excel 的 LN 函数)

sklearn 库如何计算 TFIDF?

sklearn 库使用这两个公式来计算 TF 和 IDF:

TF = 单词在文档中的出现次数

idf = LN[(1+n) / (1+df(t))] + 1

如果我使用相同的三个文档,单词‘MY’的计算如下:

TF(My) = 1

IDF(My) = LN((1+3)/(1+3)) + 1 = 1

TFIDF 的公式是:

TFIDF = TF * IDF

因此,‘My’的 TFIDF 为:

TFIDF(My) = 1 * 1 = 1

对于单词‘name’:

TF(name) = 1

IDF(name) = LN((1+3)/(1+1)) + 1 = 1.69

TFIDF(name) = 1 * 1.69 = 1.69

同样,所有单词的 TFIDF 如下:

作者图片

sklearn 的 tfidf 向量化器将值归一化到 0 到 1 的范围。为此,我们需要每个文档的 tfidf 的平方和:

归一化的 tfidf 是:

单词的 tfidf 值/文档的平方和

如果我们考虑单词 My。文档-1 中‘My’的归一化 tfidf 是:

tfidf_normalized(My) = 1.00 / 7.871 = 0.356

单词‘mom’在文档-3 中的 tfidf 是:

tfidf_normalized(name) = 1.42 / 9.005 = 0.472

再次,单词‘mom’在文档-2 中的 tfidf 是:

tfidf_normalized(name) = 1.42 / 13.009 = 0.392

看起来‘mom’这个词在文档 2 中的相关性比在文档 3 中高一些

所有单词的归一化 tfidf 如下:

作者图片

现在,我们应该检查sklearn 库中的 tfidf 向量化器的工作方式。

首先,从 sklearn 库中导入 Tfidf 向量化器,并定义用于特征提取的文本:

from sklearn.feature_extraction.text import TfidfVectorizer 
text = ["My name is Lilly",
       "Lilly is my mom’s favorite flower",
       "My mom loves flowers"]

在下一个代码块中,

第一行调用 TfidfVectorizer 方法,并将其保存到名为 vectorizer 的变量中。

第二行将文本转换为向量化器

第三行将其转换为数组以进行显示

vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(text)
X.toarray()

输出:

array([[0\.        , 0\.        , 0\.        , 0.4804584 , 0.4804584 ,
        0\.        , 0\.        , 0.37311881, 0.63174505],
       [0.49482971, 0.49482971, 0\.        , 0.37633075, 0.37633075,
        0\.        , 0.37633075, 0.2922544 , 0\.        ],
       [0\.        , 0\.        , 0.5844829 , 0\.        , 0\.        ,
        0.5844829 , 0.44451431, 0.34520502, 0\.        ]])

将此数组转换为 DataFrame 并使用单词作为列名将会很有帮助。

import pandas as pd 
pd.DataFrame(X.toarray(), columns = vectorizer.get_feature_names())

作者图片

你可以使用我在关于 CountVectorizer 的教程中解释的相同参数来细化或限制特征数量。请随时检查。

结论

本教程详细解释了 Tfidf Vectorizer 的工作原理。尽管使用 sklearn 库中的 Tfidf Vectorizer 非常简单,但理解其背后的概念也很重要。当你了解一个向量化器是如何工作的时,就更容易决定哪种向量化器是合适的。

随时在Twitter上关注我,并点赞我的Facebook页面。

本教程的视频版本:

更多阅读

## Keras 和 Tensorflow 中的完整步骤教程

数据准备、深度学习模型开发和网络训练的完整工作代码

## Python 中的完整探索性数据分析

数据清理、分析、可视化、特征选择、预测建模

## 30 个非常有用的 Pandas 函数用于日常数据分析任务

Pandas 备忘单

towardsdatascience.com](/30-very-useful-pandas-functions-for-everyday-data-analysis-tasks-f1eae16409af?source=post_page-----bb9330562ae3--------------------------------) ## 如何在 TensorFlow 中定义自定义层、激活函数和损失函数

逐步解释和包含完整代码的示例

towardsdatascience.com ## OpenCV 中用于图像预处理的形态学操作详解

腐蚀、膨胀、开运算、闭运算、形态学梯度、顶帽/白帽和黑帽,配有示例进行解释

towardsdatascience.com ## 使用自编码器方法进行 TensorFlow 和 Keras 中的异常检测

一种前沿的无监督方法,用于噪声去除、降维、异常检测等

towardsdatascience.com

将井测数据从 DLIS 文件转换为 LAS 文件格式

原文:towardsdatascience.com/converting-well-logging-data-from-dlis-files-to-las-file-format-ccc1e7eee9b0

与地球科学和岩石物理数据文件格式的工作

Andy McDonaldTowards Data Science Andy McDonald

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

--

图片由Mika Baumeister提供,发布在Unsplash上。

在石油和天然气行业的地球科学领域中,使用了多种格式来存储井测数据岩石物理数据。最常见的两种格式是 LAS 文件和 DLIS 文件。

LAS文件是平面的 ASCII 文件,可以使用任何文本编辑器轻松读取,而 DLIS 文件是结构化的二进制文件,包含有关记录环境以及记录数据的表格。DLIS 文件更难处理,不能轻易在文本编辑器中打开,这可能会妨碍理解其中的内容。

幸运的是,已经开发出几个优秀的 Python 库,使得访问 LAS 和 DLIS 文件中的数据变得更加容易。

lasio是一个旨在轻松读取和处理 LAS 文件的库,即使这些文件由于格式不正确或其他错误而存在问题。

dlsio是由 Equinor ASA 开发的一个 Python 库,用于读取 DLIS 文件和日志信息标准 79(LIS79)文件。该库旨在减少探索这些文件的负担和努力,而无需详细了解 DLIS 结构。

使用这两个库,我们将探讨如何从 DLIS 文件中提取数据并导出到 LAS 文件。这将使我们能够创建一种更易于处理的文件格式,并且文件大小更小,因为它将包含我们需要的相关数据。

如果你想查看其他关于如何处理这些文件格式的文章,以下内容可能会引起你的兴趣:

导入库并加载数据

对于本教程,我们需要两个主要的库。DLISIO,它允许我们读取和处理 DLIS 文件的内容,以及 LASIO,它允许我们处理 LAS 文件。

from dlisio import dlis
import lasio

导入库后,我们接下来需要读取 DLIS 文件。这可以通过以下语法完成。

f, *tail = dlis.load('Data/NLOG Data/NPN_TVN-1_23_Dec_2009_E3_Main_SONIC_057PUC.DLIS')

由于 DLIS 文件可能包含多个逻辑文件(代表不同的测井过程或不同的处理级别),我们需要将这些逻辑文件分隔到两个变量中: f*tail。如果有多个逻辑文件,第一个将被放入 f 变量中,其余的将放入 *tail 变量中。

我们可以应用类似的逻辑从选定的逻辑文件中提取来源信息。由于该文件包含一个单一的来源,我们可以在 origin 变量上调用 describe 方法。

origin, *origin_tail = f.origins
origin.describe()

当我们运行上述代码时,我们会得到以下总结,详细描述了该数据集的来源。我们将在构建 LAS 文件时使用这些信息。

DLIS 逻辑文件来源的总结。图片由作者提供。

创建 LAS 文件

在我们开始提取数据之前,我们首先需要创建一个空的 LAS 文件对象。可以这样完成:

las_file = lasio.LASFile()

文件创建后,我们可以通过以下方式确认其为空。

las_file.curves

这将返回一个空列表( [] ),这是我们在此阶段所需的。

为了使事情变得更简单,我们可以将 DLIS 文件中的一些关键信息提取到变量中。在这个示例中,我们将提取井名、油田名和操作公司。

如果我们希望使这段代码可重复使用,这是一种方法,可以避免每次都手动提供这些信息。

well_name = origin.well_name
field_name = origin.field_name
operator = origin.company

现在我们已经将关键信息以变量形式存储,我们可以开始填充我们的 LAS 文件头。通过从 LAS 文件中访问关键属性,并将 HeaderItem 设置为新值来完成这项工作。

你会注意到我们手动添加了一个日期,因为这个属性似乎没有被 DLISIO 曝露。

las_file.well['WELL'] = lasio.HeaderItem('WELL', value=well_name)
las_file.well['FLD'] = lasio.HeaderItem('FLD', value=field_name)
las_file.well['COMP'] = lasio.HeaderItem('COMP', value=operator)
las_file.well['DATE'] = '2009-12-23'

从 DLIS 中提取数据并写入 LAS 格式

由于 DLIS 文件可能包含大量的日志曲线和数组,我们可能需要考虑提取一小部分数据。这可以防止我们在预期的用例中被过多无关的曲线所困扰。

在这个例子中,我们将提取以下曲线。

请注意,我们需要提取“TDEP”曲线,这是我们的主要参考深度曲线。

columns_to_extract = ['TDEP', 'BS', 'DT', 'DTSM', 'VPVS']

现在我们已经准备好 LAS 文件并从 DLIS 文件中提取了头数据,我们现在可以遍历 DLIS 中一个框架内的通道。

在这个例子中,我们将访问第一个框架中的内容。如果你想了解如何检查 DLIS 文件(特别是这个文件)的内容,你可以在下面的文章中找到相关信息。

## 探索 DLIS 文件的内容与 Python

与 Pandas 和 dlisio 一起探索井日志数据

towardsdatascience.com

此外,以下代码假设所有曲线都有一个维度,即没有钻孔图像数组数据或声波波形。再次建议查看我之前的文章以获取处理方法之一。

frame = f.frames[0]

for channel in frame.channels:
    # If the channel name is in the list of channels to extract
    if channel.name in columns_to_extract:
        curves = channel.curves()

        # If the channel name is 'TDEP', convert to 'DEPT' 
        if channel.name == 'TDEP':
            channel_name = 'DEPT'
            description = 'DEPTH'
            # If the units are 0.1 in then convert to metres
            if channel.units == '0.1 in':
                curves = curves * 0.00254
                unit = 'm'
            else:
                unit = channel.units
        else:
            description = channel.long_name
            channel_name = channel.name
            unit = channel.units

        # Add the data to the LAS file
        las_file.append_curve(
            channel_name,
            curves,
            unit=unit,
            descr=description
        )

正如上面的代码所示,我们基本上遍历了所选框架内的所有可用通道(曲线),并检查它们的名称是否与我们尝试选择的名称匹配。

如果我们在列表中遇到一个曲线,我们首先检查它是否是 TDEP(深度)曲线;如果是,我们需要进行一些小调整。这包括将名称更改为 DEPT,并检查单位是否为 0.1 英寸。如果是,我们需要将深度单位转换为米。

一旦我们检查了是否有深度曲线,所有其他曲线将使用它们存储的信息提取出来。

在检查完曲线后,我们可以将其附加到 LAS 文件对象中,并传入相关信息。

一旦创建了 LAS 对象,我们可以使用以下调用来检查我们的曲线信息是否已被传递。

las_file.curves

这将返回一个包含每个曲线信息的列表。我们可以看到名称、单位和描述都已成功添加,并且根据数据的形状,我们可以假设数据值也已被传递。

从 DLIS 文件中提取出来的 LASIO LAS 文件曲线信息。图片由作者提供。

现在我们已经设置好了头信息和曲线数据,我们可以开始使用以下命令写出我们的 LAS 文件。

las_file.write('output.las')

如果写入时没有产生错误,我们可以在我们喜欢的文本编辑器中打开 LAS 文件。当我们这样做时,应该能看到以下文件。

使用 LASIO 写入文件后,在文本编辑器中查看的导出 LAS 文件。图片由作者提供。

你会注意到我们在头部部分仍然缺少信息。这可以直接在文本编辑器中编辑,或者你可以使用额外的代码来确保这些参数像我们处理日期时那样被写出。

你还会注意到深度范围被反转了,这在某些 LAS 文件中是正常的。你最喜欢的岩石物理软件包应该能轻松读取它。

如果我们想进一步确认文件是否已正确创建,我们可以使用lasio.read()函数将其加载回笔记本中。

new_las = lasio.read('output.las')
new_las.header

当我们查看 LAS 文件的头部部分时,会得到以下输出,这确认了数据已被正确读取。

将 LAS 文件加载回 Jupyter 后,LASIO 的头部摘要。图片由作者提供。

摘要

本教程展示了如何轻松将存储在 DLIS 文件中的数据转换为更可读的 LAS 文件格式。这是使用两个非常流行的 Python 库:DLISIO 和 LASIO 实现的。

此处展示的过程主要适用于单维日志曲线。任何数组数据或高分辨率数据需要以不同的方式评估。

本教程中使用的数据

NLOG.nl 的数据可以免费下载和使用。数据许可的完整详情可以在这里找到,但知识产权部分提供了使用的摘要:

NLOG.NL 不对通过本网站提供的信息(域名、商标权、专利及其他知识产权除外)声明任何权利。用户可以在未经 NLOG.NL 事先书面许可或有权方合法同意的情况下,以任何方式复制、下载、披露、分发或简化本网站提供的信息。用户还可以复制、重复、处理或编辑信息和/或布局,前提是注明 NLOG.NL 为来源。

感谢阅读。在你离开之前,你应该一定要订阅我的内容,并将我的文章发送到你的收件箱中。 你可以在这里完成订阅!

其次,你可以通过注册会员,获得完整的 Medium 体验,并支持其他数千名作者和我。只需每月$5,你就可以全面访问所有精彩的 Medium 文章,并有机会通过你的写作赚取收入。

如果你通过 我的链接注册, 你将直接支持我,并且不会额外增加你的费用。如果你这样做,非常感谢你的支持。

卷积解释——卷积神经网络简介

原文:towardsdatascience.com/convolution-explained-introduction-to-convolutional-neural-networks-5babc47fbcaa

CNNs 的基本构建块

Egor HowellTowards Data Science Egor Howell

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

--

www.flaticon.com/free-icons/neural-network" 标题为“神经网络图标。” 神经网络图标由 Freepik 创建 — Flaticon。

我最近的文章系列是关于神经网络的,我们从简单的 感知器 讲到复杂的架构以及如何处理 深度学习中的常见问题。如果你感兴趣,可以在这里查看系列文章:

Egor Howell

Egor Howell

神经网络

查看列表9 个故事

神经网络在 计算机视觉 领域取得了显著进展。这是自驾车和面部识别的 AI!

然而,大多数人所了解的常规全连接神经网络并不适合许多现实中的图像识别任务。它在著名的 MNIST 数据集上有效,但其图像尺寸为 28×28 像素。

高清 (HD) 图像有 1280×720 像素。这大约是 1,000,000 像素,这意味着输入层需要 1,000,000 个神经元。更不用说隐藏层所需的数百万个权重,这使得常规神经网络因维度复杂性而不适用。

那么,我们该怎么做?

卷积神经网络!

卷积神经网络 (CNN) 现在是大多数计算机视觉任务的金标准。它们不像全连接层那样,而是具有 部分 连接的层,并且共享其权重,从而减少模型的复杂性。

例如,对于全连接神经网络层中的每个神经元,我们需要处理一张 100×100 像素的图像的 10,000 个权重。然而,CNN 只需 25 个神经元即可处理相同的图像。

在这篇文章中,我们将深入探讨 CNN 的基本构建块,卷积

直觉

就像机器学习中的许多事物一样,CNN 也受到自然的启发。计算机科学家观察了大脑中的 视觉皮层 的工作方式,并将类似的概念应用于神经网络。

视觉皮层不会同时处理图像中的所有像素。生物神经元只对 感受野 中的小区域做出反应。然而,这些小区域会重叠,使动物能够理解整个图像。

此外,一些具有相同感受野的生物神经元只能检测不同方向的线条,这是 滤波 的一个例子。一些神经元还具有更大的感受野,这意味着高层神经元是低层神经元的组合。

这些生物学发现启发了 1979 年的 新认知机。随着时间的推移,新认知机演变成了卷积神经网络,这就是我们今天所拥有的!

什么是卷积?

CNN 的命名来源于 [卷积](https://en.wikipedia.org/wiki/Convolution#:~:text=In mathematics (in particular%2C functional,is modified by the other.) 这个数学操作,其定义如下:

连续卷积定理。作者用 LaTeX 表示的方程式。

离散卷积定理。作者用 LaTeX 表示的方程式。

  • fg:** 函数 fg 之间的卷积。

  • t: 卷积被评估的点。

  • f(τ): 在点 τ 处的函数 f 值。

  • g(t−τ):τ 移动并在 t 处评估的 g 值。

这个表达式并没有直观地告诉我们卷积是什么。让我们用更通俗的术语来解释一下。

卷积的作用是混合两个函数。在上述情况下,我们有输入的 f,并且我们在 f 上滑动某个函数 g(称为内核)。

  1. 输入:这是一个函数,在我们的情况下是图像,用于分析或操作。

  2. 内核:一个小矩阵,用于对图像应用视觉效果,例如模糊、锐化、边缘检测等。

  3. 滑动内核:内核一次滑过输入图像一个像素,计算新的像素值。

  4. 计算输出:每个像素的输出是通过将输入和内核的重叠值相乘并求和这些乘积来计算的。然后,通过除以内核中的元素数量进行归一化。

  5. 结果:输出是一个新图像,这个图像已经被内核转换过。

示例卷积

让我们通过一些视觉示例来了解图像处理的简单卷积示例。

在下面的图示中,我们有一个输入的灰度图像,其尺寸为 5x5 像素,以及一个 3x3 的内核,其中全是 1s,这将产生模糊效果(特别是一个 箱型模糊)。

示例卷积用于在灰度图像上应用模糊效果。图示由作者创建。

像素值越小,图像越暗。值为 0 表示黑色,255 表示白色,中间的值则是灰度值。

如果我们取中间的像素(用红色高亮显示),其卷积输出是通过将每个像素值与内核中对应的元素相乘并求和这些乘积来计算的。然后,通过内核中的元素数量进行归一化,以确保图像不会变亮或变暗。

以下是此过程的逐步讲解。

[30*1 + 30*1 + 30*1] +
[30*1 + 70*1 + 30*1] +
[30*1 + 30*1 + 30*1] 

= 30 + 30 + 30 + 30 + 70 + 30 + 30 + 30 + 30 = 310

pixel value = 310 / 9 ~ 34

然后对输入图像中的所有其他像素(仅在内核适合的地方)重复此操作,以生成所示输出。

请注意,结果图像的像素值现在比原始图像的像素值更接近。这是因为我们的内核对邻域中的像素值进行了平均,产生了模糊效果。

这种卷积可以很容易地在 Python 中实现,并显示结果图像:

# Import packages
import numpy as np
from scipy.signal import convolve2d
import matplotlib.pyplot as plt

# Input Image
image = np.array([
    [10, 10, 10, 10, 10],
    [10, 30, 30, 30, 10],
    [10, 30, 70, 30, 10],
    [10, 30, 30, 30, 10],
    [10, 10, 10, 10, 10]
])

# Kernel
kernel = np.array([
    [1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]
]) / 9

# Convolution
result = convolve2d(image, kernel, mode='same', boundary='fill', fillvalue=0)

# Plot
plt.figure(figsize=(10, 5))

# Input Image
plt.subplot(1, 2, 1)
plt.imshow(image, cmap='gray')
plt.title('Input Image')
plt.axis('off')

# Output Image
plt.subplot(1, 2, 2)
plt.imshow(result, cmap='gray')
plt.title('Output Image')
plt.axis('off')

plt.show()

模糊图像的示例。图表由作者在 Python 中创建。

用于生成此图像的代码可以在我的 GitHub 上找到:

[## Medium-Articles/Neural Networks/convolution_image.py at main · egorhowell/Medium-Articles

我在我的中等博客/文章中使用的代码。通过创建帐户来为 egorhowell/Medium-Articles 的开发做出贡献...

github.com

某些卷积核结构对图像有不同的效果。下面是一些常用卷积核及其效果:

卷积核及其效果。作者的图示。

一份完整的卷积核及其操作列表可以在这里找到。

维度、步幅与填充

输出维度

你可能已经注意到,与原始图像相比,应用卷积核会导致图像尺寸减小。推导输出尺寸的公式是:

输出维度。作者的 LaTeX 公式。

  • H_inW_in 是输入图像的高度和宽度。

  • H_filterW_filter 是卷积核的高度和宽度。

  • Padding_heightPadding_width 是应用于输入图像高度和宽度的填充量。

  • Stride_heightStride_width 是卷积核在输入图像高度和宽度上的步长。

我们尚未讨论的两个概念出现在上述方程中,步幅填充

步幅

步幅是我们在输入图像上滑动卷积核的像素数。步幅为 1 表示卷积核移动一个像素,步幅为 2 表示卷积核每次移动两个像素。滑动可以是垂直、水平,或根据设置同时进行。

增加步幅会导致:

  • 减少输出图像的尺寸大小。

  • 由于操作较少,计算成本更低。

  • 输入图像的视野范围更大。

  • 跳过像素会导致信息丢失。

步幅为 1 的示例。作者的图示。

填充

卷积的一个问题是我们往往会丢失像素和图像边缘的信息。这取决于卷积核的使用次数。角落像素只会被使用一次,而中间像素则会使用得更多。

如果我们多次应用卷积核,这在 CNN 中会发生,输出图像会比原始输入图像小很多。这不好,因为我们会丢失大量信息,特别是关于图像边界的信息。

解决这个问题的一种方法是对输入图像的边缘进行零填充。下面是使用前面部分图像的示例:

零填充。作者的图示。

输入图像现在是7x7像素。使用上述公式,我们可以验证,应用一个3x3的核到这个填充图像上将得到一个5x5尺寸的输出。

应用填充时输出维度的示例计算。方程由作者用 LaTeX 表示。

这将增加周边像素的利用率,并使输出图像具有与原始输入图像相同的尺寸。

零填充是相同填充的一种示例。完全不填充称为有效填充。还有另一种类型叫做因果填充,它是不对称的,通常应用于时间序列和自然语言处理。

我们不一定总是要使用零填充,尽管这是一种最常见的技术。以下是一些其他填充策略的列表:

摘要与进一步思考

卷积神经网络(CNN)是目前计算机视觉任务的金标准。它们的主要特点是利用卷积数学运算,这使我们能够将两个函数“融合”在一起。这种方法在图像处理中通过在我们的图像上应用一个矩阵(即核),来实现模糊和锐化等效果。这样我们就能操控和分析图像,这是 CNN 的基础,也是它们如何“看”图像的方式。

另一个话题!

我有一个免费的新闻通讯,数据分享,在其中我每周分享成为更好的数据科学家的小贴士。没有“虚头八脑”的内容或“点击诱饵”,只有来自一名实践数据科学家的纯粹可操作的见解。

[## 数据分享 | Egor Howell | Substack

如何成为更好的数据科学家。点击阅读《数据分享》,作者 Egor Howell,Substack 出版的……

newsletter.egorhowell.com

与我联系!

参考文献和进一步阅读

卷积网络 — 直观且详尽的解释

原文:towardsdatascience.com/convolutional-networks-intuitively-and-exhaustively-explained-ab08f6353f96

解构一个基石建模策略

Daniel WarfieldTowards Data Science 丹尼尔·沃菲尔德

·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 10 月 26 日

--

“卷积”由作者使用 MidJourney 完成。除非另有说明,所有图片均由作者提供。

卷积神经网络是计算机视觉、信号处理和大量其他机器学习任务中的主要方法。它们相对简单,因此许多人往往对它们视而不见,真正不了解它们。在本文中,我们将直观且详尽地讨论卷积网络的理论,并探讨它们在几个使用案例中的应用。

这对谁有用? 对计算机视觉、信号分析或机器学习感兴趣的任何人。

这篇文章的难度如何? 这是一个非常强大的但又非常简单的概念;非常适合初学者。对于经验丰富的数据科学家,这也可能是一个很好的复习,特别是在考虑不同维度的卷积时。

前提条件: 对反向传播和全连接神经网络有一定的了解可能会有所帮助,但不是必须的。我在这篇文章中涵盖了这两个内容:

## 什么是梯度,它们为何会爆炸?

通过阅读这篇文章,你将对深度学习中最重要的概念有一个扎实的理解

towardsdatascience.com

卷积网络存在的原因

许多初学的数据科学家首先探索的主题是全连接神经网络。这是一种经典的神经网络,由具有一定可学习参数的节点和边组成。这些参数使得模型能够学习关于其训练主题的微妙关系。

一个概念图,展示了一个密集网络如何接受一些输入并预测输出。它通过逐步学习已知示例(即之前超级碗团队的成功与失败)来学习必要的参数以在任务中表现良好。

随着网络中神经元数量的增加,层与层之间的连接变得越来越丰富。这可以实现复杂的推理,这很好,但密集网络的“密集性”在处理图像时会带来问题。

假设我们想训练一个密集神经网络来预测图像中是否包含狗。我们可能会创建一个密集网络来观察图像的每一个像素,然后将这些信息归纳为最终输出。

连接图像到密集网络的概念图。

我们已经遇到了一个大问题。略过一些数学细节,针对这个小网络,我们需要 1,544 个可学习参数。对于更大的图像,我们需要更大的网络。假设我们在第一层有 64 个神经元,我们想学习对 256x256 像素的图像进行分类。仅第一层就有 8,388,608 个参数。对于一个相对较小的图像,这确实有很多参数。

神经网络的另一个问题是对图像中微小变化的敏感性。假设我们制作了两种表示狗图像的方式;一种是狗在图像的顶部,另一种是狗在底部。

一个神经网络观察两个相似图像上相同的像素。

尽管这些图像对人眼来说非常相似,但从神经网络的角度来看,它们的值却大相径庭。神经网络不仅需要逻辑上定义什么是狗,还需要使这种对狗的逻辑定义对图像中的各种变化具有鲁棒性。这可能是可行的,但这意味着我们需要向网络输入大量的训练数据,并且由于密集网络具有如此多的参数,每一步训练都将花费很长时间来计算。

因此,密集网络不适合处理图像;它们太庞大且对微小变化过于敏感。在接下来的部分中,我们将深入探讨卷积网络如何解决这些问题,首先定义什么是卷积,然后描述神经网络中如何进行卷积操作。

卷积简明介绍

卷积网络的核心是“卷积”操作。 “卷积”是将一个“卷积核”作用于某个“目标”以进行“过滤”的过程。这些词可能你不太熟悉,我们来详细解释一下。我们将使用图像中的边缘检测作为示例。

从卷积的角度来看,卷积核是一个小的数字数组。

内核的示例。这个特定的内核用于边缘检测。

这个内核可以用于将输入图像转换成另一幅图像。使用标准操作将输入转换为输出的过程通常称为“过滤”(想想 Instagram 滤镜用于修改图像)。

内核被用于过滤输入图像,生成一幅突出原始图像边缘的图像。通常,输入图像有时被称为“目标”图像,而输出图像有时被称为“过滤”图像。 来源

实际的过滤操作是通过“卷积”完成的。内核,比输入图像要小得多,被放置在图像中的每一个可能的位置。然后,在特定位置,内核的值与输入图像的值相乘,结果被加总在一起,以定义输出图像的值。

“卷积”内核到目标图像上。3x3 内核被应用到目标图像的每个 3x3 区域。内核的值与对应位置的像素值相乘,然后将所有值加在一起,以构建输出图像中的一个像素。

在机器学习中,卷积最常用于图像,但它们在其他领域也能很好地工作。你可以对一维信号进行小波卷积,也可以对三维空间进行三维张量卷积。卷积可以在任意维度上进行。

1D 卷积的示例(左),以及 3D 卷积的示例(右)。这两种情况的功能类似于 2D 卷积;内核(用蓝色表示)在所有可能的位置上滑动(红色示例),重叠的值被相乘,然后结果被加总,以构建单一的输出值。

在本文的大部分内容中,我们将保持在二维,但重要的是要记住卷积的一般方面;它们可以用于计算机视觉以外的许多问题类型。

现在我们知道了什么是卷积以及它是如何工作的。在下一节中,我们将探讨如何利用这一理念来构建模型。

卷积神经网络概述

卷积网络的整个理念是通过卷积和降采样的组合,逐步将图像分解成更小、更有意义的表示。通常,这种分解后的图像表示会传递给一个密集网络,以生成最终的推断结果。

典型卷积神经网络的高层架构。实际的卷积通常存在于“主干”部分的卷积和下采样中。一旦输入被浓缩为一个富有意义的表示(也称为瓶颈),下一步通常是一个密集网络,通常被称为“头”或“投影头”,它生成所需的最终响应。我在另一篇关于自监督学习中投影头的重要性的文章中对这些概念进行了更深入的讨论。

类似于全连接神经网络通过学习连接之间的权重来提高任务性能,卷积神经网络在卷积层内学习卷积核的值,以提高任务性能。

回想一下卷积核如何在图像上滑动,将其转换为不同的表示。卷积网络逐步调整卷积核的值,以生成经过滤的图像,这样在最终建模任务中表现得更好。

在卷积网络中有许多下采样的方法,但最常见的方法是最大池化。最大池化类似于卷积,即一个窗口在整个输入上滑动。与卷积不同的是,最大池化只保留窗口中的最大值,而不是窗口的某种组合。

这是一个应用最大池化于图像的示例,它将图像从 6x9 下采样到 2x3。如果你想知道为什么这些方块没有重叠,而卷积却有,那是因为“步幅”,我们将在后面的部分中讨论这个问题。

因此,通过卷积和最大池化层,图像被逐步过滤和下采样。通过每一层,图像变得越来越抽象,尺寸也越来越小,直到它包含图像的抽象和浓缩表示。

很多人在理论层面上会停留在这里。然而,卷积神经网络还有一些更关键的概念,通常被忽视。特别是,特征维度以及卷积如何与之相关。

⚠️ 癫痫 警告:以下部分包含快速移动的动画⚠️

特征维度

你可能已经注意到,在之前的一些示例中,我们使用了灰度图像。实际上,图像通常有三个颜色通道:红色、绿色和蓝色。换句话说,图像有两个空间维度(宽度和高度)和一个特征维度(颜色)。

特征维度的这个概念对于深入理解卷积神经网络至关重要,因此我们来看看一些示例:

示例 1) RGB 图像

因为图像包含两个空间维度(高度和宽度)和一个特征维度(颜色),所以图像可以被概念化为三维。

左侧是图像,中间是构成图像的三个通道,右侧是我们将输入模型的数据形状。

通常,卷积网络沿所有空间维度移动其核,但不沿特征维度。对于像图像这样的二维输入,通常使用一个 3D 核,它具有与特征维度相同的深度,但宽度和高度较小。然后,这个核在所有空间维度上滑动。

图像卷积的概念动画。一个 3D 核应用于整个特征维度,并在两个空间维度上滑动。这是二维卷积,因为实际遍历发生在两个维度上。注意:特征维度也通常称为“通道”。

通常,与其进行一次卷积,进行多次卷积,每次使用不同的核,会更有优势。这使得卷积网络能够创建图像的多种表示。每个卷积都使用自己的可学习核,这些表示沿特征轴连接在一起。

应用于输入图像的三个核的概念图。这些核中的每一个都创建自己的特征层。每个核中的所有值都是可学习的参数。

正如你可能已经推测的,你可以有任意数量的核,从而创建任意深度的特征维度。许多卷积神经网络在模型的不同点使用不同数量的特征。

原始 YOLO 模型的架构,这是图像处理中的一个标志性卷积模型。注意图中的框是三维的,这些框代表了模型中各个点的高度、宽度和特征数量。不要过多担心这张图的文字,我们稍后会重新审视这个图。 来源

最大池化通常只考虑单个特征层。本质上,我们只是对每个单独的特征层进行最大池化。

应用于三个特征的最大池化的概念图。注意,最大池化通常在卷积之后应用,而不是之前。

这些是二维 RGB 图像上的两个主要操作:卷积和最大池化。

示例 2) 立体声音频

虽然像音频这样的时间序列信号通常被认为是一维的,但实际上它们通常是二维的,一个维度表示时间,另一个维度表示该时间点的多个值。例如,立体声有两个独立的频道,一个用于左耳,一个用于右耳。

来自一段立体小号音乐的左右声波,在时间域中。X 轴对应于时间(以秒为单位),y 轴对应于信号的幅度,控制扬声器振膜的位置,从而产生声音。参考我关于频域的文章。

这可以被概念化为一个具有一个空间维度(时间)和一个特征维度(哪个耳朵)的信号。

我们的立体声音频数据可以被概念化为二维的;一个时间维度和一个特征维度。

对这些数据应用卷积和最大池化与图像非常相似,只是我们只对一个维度进行迭代。

一维卷积的概念图。在这个示例中,我们对输入应用了三个简单的卷积核。请记住,这些卷积核是可学习的,它们的值在模型训练过程中会发生变化。

最大池化也类似于之前讨论的图像方法。我们将特征维度的每一行分开处理,应用一个移动窗口,并保留该窗口中的最大值。

最大池化的示例。每个特征被划分为区域,并保留这些区域内的最大值。

示例 3) MRI/CT 扫描

根据应用的不同,扫描数据可以被概念化为三维或二维。

人头部的 MRI 逐切片动画。通常,这种类型的数据被视为三维问题。图像来源

维苏威火山墨水检测挑战的 CT 扫描示例,这是一项涉及阅读庞贝城碳化卷轴的挑战,无需通过解卷的方式破坏这些卷轴。该数据是一个具有深度的二维对象。有时,这种类型的数据被称为“2.5D”,因为它有两个主要的空间维度和一个可以被概念化为特征维度的次要空间维度。source

纸张上的示例可以视为一个具有特征维度表示深度的二维空间数据问题。它实际上是三维的,纸张有一定的厚度,这在 CT 扫描中被记录下来,但深度维度足够小,可以视为一个特征维度。这就像我们的图像示例,只不过每个物理数据层有一个层而不是每种颜色都有一个层。

对于完全三维的人头扫描来说,将其视为一个平面物体可能没有用处,更应当视为一个完整的三维物体,我们可以使用三维卷积和最大池化。这在概念上与之前的方法相同,但绘制起来要困难得多。我们将对所有三个空间维度进行卷积,具体的输出维度取决于我们使用的内核数量,最终得到一个四维输出。

在给定的三维数据集中使用两个卷积内核的示例,每个内核产生一个三维输出。最终输出将是四维的。

从技术上讲,卷积网络的维度没有限制。你可以在五维、六维甚至一千维中进行卷积。我认为三维已经足够复杂了,绝大多数卷积模型实际上工作在二维数据上,因此我们将在这里结束示例探索。

到这时你可能会有一些基本问题,比如“内核是否总是必须有特定大小”或者“内核是否总是必须重叠?”我们将在下一个问题中讨论这些问题。

内核大小、步幅和填充

虽然我们之前考虑了所有的内核都为三的大小,但卷积内核的大小没有限制。这种可配置的大小通常称为内核大小

在二维任务中,尺寸为二、三和四的内核

我还将所有卷积理想化为每次只移动一个点,但卷积可以步进较大的量,这被称为步幅。最大池化也有一个步幅参数,通常是最大池化窗口的大小。这就是为什么我将最大池化描绘为不重叠的原因。

步幅长度为一(左)、二(中)、三(右)的概念图,所有这些图都是针对尺寸为二的内核。

有时将卷积的输出大小与卷积的输入大小相等是有利的,但由于内核通常大于一,因此卷积的任何结果必然会小于输入。因此,我们可以用一些默认值甚至是输入的反射来“填充”图像的边界,以获得类似大小的输出。这个过程被称为填充

非线性激活函数

使神经网络能够学习复杂任务的一部分原因是非线性。如果我们不使用激活函数,那么卷积最终只能创建线性关系(加法和乘法的组合)。如果我们在混合中添加一些激活函数,将某些输入非线性地映射到输出,我们可以让我们的卷积网络学习更复杂的关系。在卷积网络中最常用的激活函数是 ReLu。

通常,激活函数在卷积之后和最大池化之前应用,因此是这样的:

output = maxPool(relu(conv2d(input))

不过,尽管差别不大,将 maxPool 和激活函数交换的位置稍微提高了性能,但这不会影响最终输出。

output = relu(maxPool(conv2d(input))

无论如何,在模型中添加非线性激活函数会大大提高模型学习复杂任务的能力。

展平和全连接网络

卷积网络擅长将数据拆解到其本质,但全连接网络在进行逻辑推理方面更出色。在数据通过卷积网络之后,数据通常会被“展平”然后传递给全连接网络。

展平如何与更广泛的卷积建模范式相关的概念图。全连接网络通常期望数据列表,而卷积模型通常有多个维度。卷积网络的输出通常会被“展平”成列表,然后传递给全连接网络,这一操作称为展平。

我在以下文章中有关于全连接网络作为“投影头”的更多信息:

## 自监督学习使用投影头

利用未标记的数据提升性能。

towardsdatascience.com

结论

就这样!我不会说我们讨论了卷积网络的每一种可能方法或理论(那将是一篇相当长的文章),但我们覆盖了理解几乎所有存在的方法所需的理论。你应该知道为什么卷积网络在某些应用中优于全连接网络,什么是卷积核,什么是卷积,什么是最大池化。你应该理解特征维度及其在各种 1D、2D 和 3D 用例中的应用。你还应该理解关键参数,如卷积核大小、填充和步幅。

看一下 YOLO 图,考虑卷积核大小、步幅、特征/卷积核数量、展平和全连接网络如何与图相关联。来源

关注获取更多信息!

我描述机器学习领域的论文和概念,重点在于实用和直观的解释。

[## 每当丹尼尔·沃菲尔德发布文章时,获取邮件通知

高质量的数据科学文章直送到你的邮箱。每当丹尼尔·沃菲尔德发布文章时,你都会收到邮件。通过注册,你可以…

medium.com](https://medium.com/@danielwarfield1/subscribe?source=post_page-----ab08f6353f96--------------------------------)

归属声明: 本文档中的所有资源均由丹尼尔·沃菲尔德创建,除非另有来源提供。你可以将本文中的任何资源用于自己的非商业用途,只要你引用了本文或danielwarfield.dev,或两者兼有。应要求可提供明确的商业许可。

卷积神经网络入门

原文:towardsdatascience.com/convolutional-neural-networks-for-beginners-c1de55eee2b2

卷积神经网络的基础知识

Mina GhashamiTowards Data Science Mina Ghashami

·发布在 Towards Data Science ·6 分钟阅读·2023 年 10 月 17 日

--

图片来自 unsplash.com

我写这篇文章是作为我在Interview Kickstart讲授的一个讲座的准备工作之一,目的是帮助专业人士进入顶级科技公司。如果你正在准备面试或者只是打基础,这篇文章也许对你有帮助。

在这篇文章中,我们将深入探讨卷积神经网络及其基本原理。我们将从卷积操作是什么开始,然后继续了解卷积层是什么以及卷积网络是如何构建的。

让我们开始吧。

卷积神经网络(CNNs)由多个“卷积层”组成。这些层执行“卷积操作”。卷积是信号和图像处理中的一个基本操作。让我们首先了解一下这个操作是什么。

什么是卷积操作?

卷积是内核(滤波器)与输入特征图之间的数学操作。

内核通常是一个小矩阵,例如 3x3 或 5x5。输入始终是具有高度、宽度和通道的特征图。卷积操作的工作原理是内核在输入上滑动计算内核与输入的局部区域之间的点积。这种点积乘法和求和会在输出特征图中产生一个值。

当滤波器在所有位置滑动时,它会生成一个称为输出特征图的 2D 激活图。对于滤波器(卷积核)在图像或输入特征图上的每一次滑动,我们计算逐元素的点积并将它们加在一起。这就产生了输出图中的一个值:

卷积操作 — 作者图片

接下来,我们将滤波器向右滑动到输入图的另一个局部区域,它会生成输出图中的另一个条目:

卷积操作 — 图片由作者提供

我们将其全部向右滑动,直到无法继续向右滑动,然后回到最左侧,并向下滑动一个条目:

卷积操作 — 图片由作者提供

对于这个示例,最后一个卷积操作会在右下角的最远点进行:

卷积操作 — 图片由作者提供

现在,让我们看看卷积层,它是卷积神经网络 (CNN) 的关键构建块。

什么是卷积层?

卷积层像其他层一样,由多个神经元组成。卷积层中的每个神经元都有一组权重定义其滤波器(卷积核)。该滤波器与输入(或前一层的输出)进行卷积,以生成一个二维激活图。

在前向传播过程中,卷积层中一个神经元的输入是一个具有 [高度, 宽度, 通道] 维度的 3D 体积。该层中的每个神经元都有一组权重定义其卷积核(滤波器)。

这个滤波器具有小的空间范围(例如 3x3),但在输入深度上完全扩展。这意味着它与输入进行卷积(逐元素相乘和求和),并在深度上完全扩展,生成一个二维激活图作为输出。每个神经元的输出维度是 [高度, 宽度, 1]。

如果我们将一个卷积层中所有神经元的输出图堆叠在一起,那么我们将得到一个维度为 [高度, 宽度, 通道] 的输出;其中通道是层中神经元的数量。通道是结果的深度。

现在,让我们看看这个层的参数和超参数:

参数: 层的参数是卷积核的权重,这些权重最初是随机初始化的,但在训练过程中会被学习和优化。

超参数: 层的超参数如下:

    1. 层中的神经元数量
    1. 滤波器大小(例如 2x2, 3x3);通常,层中的所有神经元具有相同大小的滤波器
  • 3) 步幅: 即每次移动滤波器的步数,可以是向右或向下。

  • 3) 零填充:零填充是指将卷积层的输入在其边界周围填充零的技术。这有助于在整个网络中保持空间分辨率;否则,随着深度的增加,空间信息可能会很快丢失。零填充主要有两种类型:

  • 3–1) 相同填充 — 填充使输出大小与输入大小匹配。需要填充 (kernel_size — 1) / 2

  • 3–2) 有效填充 — 不使用填充。输出会缩小 kernel_size — 1

所以如果我们使用一个 3x3 的内核,我们将对输入的每个维度进行 1 像素的零填充。请参见下面的示例,其中输入图像为 5x5,内核为 3x3。没有零填充的情况下,输出激活图为 3x3。正如我们所见,空间维度没有被保留。

无填充 — 图片来源:作者

但如果我们进行零填充,我们可以保留输入图像的空间维度。如下所示,输出激活图为 5x5。

使用零填充 — 图片来源:作者

卷积神经网络是什么?

卷积神经网络(CNN)是一种由多个卷积层、池化层和全连接层组成的神经网络。它通常用于处理网格状拓扑,例如图像。

如上所述,卷积层对输入进行卷积操作,使用一组可学习的滤波器。它们一起构建特征图层次结构。

池化层(最大池化或平均池化)对特征图进行下采样,以减少计算量。

全连接层通常是 CNN 中的最后几层,它们将所有神经元连接在一起并执行分类任务。

这里我们看到一个 CNN 的图像:

图片来源:作者

这篇文章的主题到此结束。在接下来的文章中,我们将查看一些著名的 CNN 模型架构,用于图像分类任务。

关键要点

  1. 一些术语:每个通道也称为特征图。内核也称为滤波器。感受野是指神经元在输入图像中查看和提取特征的区域。

  2. 卷积层中的所有神经元具有相同的内核(滤波器)大小。这一点很重要,以便我们可以将所有输出激活图叠加在一起。

  3. CNN 可以轻松扩展到大图像,因为卷积滤波器仅在局部应用。

  4. 卷积层中的神经元数量与滤波器或内核的数量相同。它们定义了输出特征图中的通道数。

  5. 卷积层中的神经元具有独立的内核;它们不共享权重。

总结

卷积神经网络(CNN)是网格处理神经网络,由卷积层、池化层和全连接层组成。卷积层中的每个神经元对应一个滤波器(内核)。在前向传播过程中,每个滤波器与输入体进行卷积,生成该滤波器的 2D 激活图。多个滤波器产生多个激活图,这些激活图在深度维度上堆叠。然后,池化层(最大池化或平均池化)将其下采样到较小的尺寸。在神经网络的最后,有一个或多个全连接层来帮助进行图像分类任务。

如果你有任何问题或建议,请随时联系我:

电子邮件:mina.ghashami@gmail.com

LinkedIn: www.linkedin.com/in/minaghashami/

卷积自编码器与前馈自编码器在图像去噪中的比较

原文:towardsdatascience.com/convolutional-vs-feedforward-autoencoders-for-image-denoising-2fe2e9aed71d

Kaveti 自编码器的应用

使用卷积和前馈自编码器清理损坏的图像

Rukshan PramodithaTowards Data Science Rukshan Pramoditha

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

--

图片由皮埃尔·巴敏提供,来自Unsplash(作者稍作修改)

你想了解卷积自编码器如何在图像去噪中优于前馈自编码器吗?

如果‘’,请继续阅读这篇文章。

不同类型的自编码器

自编码器有许多实际应用。图像去噪就是其中之一。

图像去噪指的是从损坏的图像中去除噪声,以获得干净的图像。

用于图像去噪的自编码器特别被称为去噪自编码器

我们已经在以下文章中涵盖了自编码器的基础知识。

**Prerequisites
-------------**
01\. [An Introduction to Autoencoders in Deep Learning](https://rukshanpramoditha.medium.com/an-introduction-to-autoencoders-in-deep-learning-ab5a5861f81e)
02\. Autoecnoder Latent Representation and Hidden Layers
03\. [Autoecnoder Latent Representation and Latent Vector Dimension](https://rukshanpramoditha.medium.com/how-the-dimension-of-autoencoder-latent-vector-affects-the-quality-of-latent-representation-c98c38fbe3c3)
04\. Dimensionality Reduction with Autoencoders
05\. Creating Shallow and Deep Autoencoders in Keras

**Optional
--------**
06\. Convolutional Neural Network (CNN) Architecture
07\. Coding a Convolutional Neural Network (CNN)
08\. [Acquire, Understand and Prepare the MNIST Dataset](https://rukshanpramoditha.medium.com/acquire-understand-and-prepare-the-mnist-dataset-3d71a84e07e7)
09\. [Keras Sequential API and Functional API](https://rukshanpramoditha.medium.com/two-different-ways-to-build-keras-models-sequential-api-and-functional-api-868e64594820)
10\. [Plotting the Learning Curve](https://rukshanpramoditha.medium.com/plotting-the-learning-curve-to-analyze-the-training-performance-of-a-neural-network-4a35818d01f2)
11\. Image representation in deep learning

简单回顾一下,自编码器是一种神经网络架构,包含三个关键元素:编码器解码器潜在向量

这些关键元素之间的关系如下图所示。

自编码器的结构(图片作者提供)

编码器(函数f) 接收输入x 并将其转换为潜在向量z。解码器(函数g) 接收z 作为输入,并从潜在向量中恢复输入x。恢复的输入大致与x 相同,因此表示为

潜在向量可以具有与输入相同或更高的维度(在超完备自编码器的情况下),也可以具有远低于输入的维度(在欠完备自编码器的情况下)。

然而,对于图像去噪应用,潜在向量应该比输入的维度更低。

复杂的自编码器有许多隐藏层。它们被称为深层(多层)自编码器。相比之下,具有单一隐藏层的自编码器称为浅层(原始)自编码器

我们在实际应用中总是使用深层自编码器,包括图像去噪,因为浅层自编码器无法有效捕捉数据中的重要关系。

自编码器可以使用密集层和卷积层来创建。

当自编码器的编码器和解码器部分仅由密集(全连接或 MLP)层组成时,这种自编码器被称为前馈自编码器

当自编码器的编码器部分由卷积层(下采样层)组成,解码器部分由转置卷积层(上采样层)组成时,这种自编码器被称为卷积自编码器

在这里,我们将比较这两种类型的输出。

自编码器如何从受损图像中去除噪声

在使用自编码器的图像生成应用中,我们通常将自编码器模型训练为:

autoencoder.fit(train_images, train_images)

这就像告诉模型“使用训练数据学习关系,以生成几乎与原始图像相同的新图像”。然后,我们可以调用autoencoder.predict(test_images)来使用模型生成新图像。

在使用自编码器的图像去噪应用中,我们通常将自编码器模型训练为:

autoencoder.fit(train_images_noisy, train_images)

这就像告诉模型“使用有噪声(受损)的训练数据学习关系,以去除相同领域图像中的噪声”。然后,我们可以调用autoencoder.predict(test_images)来去噪相同领域的新受损图像。

有噪声(受损)的数据是通过向原始数据中添加一些随机(随机)噪声来创建的。噪声遵循以 0 为均值、标准差为 0.7 的高斯(正态)分布。

import numpy as np

noise = np.random.normal(loc=0.0, scale=0.7, size=train_images.shape)
train_images_noisy = train_images + noise

当我们训练去噪自编码器时,编码器部分保留了最重要的信息,去除训练数据中的任何不必要的噪声,并创建该数据的低维潜在(压缩)表示。然后,解码器部分从该压缩表示中恢复干净的图像。训练好的模型可以用于清理相同领域的新受损图像 — 作者

建立模型:卷积和前馈自编码器

我们使用的数据集

我们将使用MNIST 数据集(见文末的引用)来构建这两个自编码器模型。这个数据集已经预加载到 tf.keras 中。

要学习如何获取和准备 MNIST 数据集以用于深度学习应用,请阅读我的文章,获取、理解和准备 MNIST 数据集

方法

我们将使用 MNIST 数据构建一个卷积自编码器和一个前馈自编码器,并比较这两个模型的输出。在训练过程中,为了比较目的,这两个模型具有相同的损失函数、优化器、批量大小和轮数。

然而,这两种架构有显著差异,因为前馈自编码器具有密集层,而卷积自编码器具有卷积层和反卷积层。

训练模型并进行预测

运行以下代码后,你将获得训练好的自编码器模型及其训练历史记录,同时也会得到在新数据上的预测结果。

import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from tensorflow.keras import Model, Input
from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import Dense, Conv2D, MaxPooling2D, Conv2DTranspose

# Load the MNIST dataset
(train_images, _), (test_images, _) = mnist.load_data()

###########################
# Feedforward Autoencoder #
###########################

# Reshape data for the dense layer input
train_images = np.reshape(train_images, (-1, 784))
test_images = np.reshape(test_images, (-1, 784))

# Scale the data
train_images = train_images.astype('float32') / 255
test_images = test_images.astype('float32') / 255

# Add Gaussian noise to the data
noise = np.random.normal(loc=0.0, scale=0.7, size=train_images.shape)
train_images_noisy = train_images + noise

noise = np.random.normal(loc=0.0, scale=0.7, size=test_images.shape)
test_images_noisy = test_images + noise

# Clip the noisy data by 0 and 1:
# This is because adding noise may push the normalized pixel values 
# into invalid values of less than 0 or greater than 1
# So, we need to clip pixel values greater than 1 to 1.0 and
# less than 0 to 0.0
train_images_noisy = np.clip(train_images_noisy, 0., 1.)
test_images_noisy = np.clip(test_images_noisy, 0., 1.)

latent_vec_dim = 16
input_dim = 784

# Define the input layer
input_layer = Input(shape=(input_dim,))

# Define the feedforward (ff) autoencoder architecture
# First build the encoder with dense layers
x = Dense(500, activation='sigmoid')(input_layer)
x = Dense(300, activation='sigmoid')(x)
x = Dense(100, activation='sigmoid')(x)
encoder = Dense(latent_vec_dim, activation='tanh')(x)

# Then build the decoder with dense layers
x = Dense(100, activation='sigmoid')(encoder)
x = Dense(300, activation='sigmoid')(x)
x = Dense(500, activation='sigmoid')(x)
decoder = Dense(input_dim, activation='sigmoid')(x)

# Connect both encoder and decoder
ff_autoencoder = Model(input_layer, decoder, name="ff_autoencoder")

# Compile the model
ff_autoencoder.compile(loss='binary_crossentropy', optimizer='adam')

# Train the model
history_ff = ff_autoencoder.fit(train_images_noisy, train_images, epochs=50, 
                                batch_size=128, shuffle=True,
                                validation_data=(test_images_noisy, test_images))

# Denoise new images (prediction)
ff_autoencoder_denoised_images = ff_autoencoder.predict(test_images_noisy)

# Clear the history of the previous model
# Otherwise, you’ll run out of memory or 
# will get abnormal results due to clutter from old models and layers.
tf.keras.backend.clear_session()

# Load the MNIST dataset again
(train_images, _), (test_images, _) = mnist.load_data()

#############################
# Convolutional Autoencoder #
#############################

# Reshape data for the convolutional layer input
train_images = np.reshape(train_images, (len(train_images), 28, 28, 1))
test_images = np.reshape(test_images, (len(test_images), 28, 28, 1))

# Scale the data
train_images = train_images.astype('float32') / 255
test_images = test_images.astype('float32') / 255

# Add Gaussian noise to the data
noise = np.random.normal(loc=0.0, scale=0.7, size=train_images.shape)
train_images_noisy = train_images + noise

noise = np.random.normal(loc=0.0, scale=0.7, size=test_images.shape)
test_images_noisy = test_images + noise

# Clip the noisy data by 0 and 1 as before
train_images_noisy = np.clip(train_images_noisy, 0., 1.)
test_images_noisy = np.clip(test_images_noisy, 0., 1.)

# Define the input layer
input_layer = Input(shape=(28, 28, 1))

# Define the convolutional (conv) autoencoder architecture
# First build the encoder with convolutional layers
x = Conv2D(64, (3, 3), activation="relu", padding="same")(input_layer)
x = MaxPooling2D((2, 2), padding='same')(x)

x = Conv2D(64, (3, 3), activation="relu", padding="same")(x)
encoder = MaxPooling2D((2, 2), padding='same')(x)

# Then build the decoder with transposed convolutional layers
x = Conv2DTranspose(64, (3, 3), strides=2, activation="relu", padding="same")(encoder)
x = Conv2DTranspose(64, (3, 3), strides=2, activation="relu", padding="same")(x)
decoder = Conv2D(1, (3, 3), activation="sigmoid", padding="same")(x)

# Connect both encoder and decoder
conv_autoencoder = Model(input_layer, decoder, name="conv_autoencoder")

# Compile the model
conv_autoencoder.compile(loss='binary_crossentropy', optimizer='adam')

# Train the model
history_conv = conv_autoencoder.fit(train_images_noisy, train_images,
                                    epochs=50, batch_size=128, shuffle=True, 
                                    validation_data=(test_images_noisy, test_images))

# Denoise new images (prediction)
conv_autoencoder_denoised_images = conv_autoencoder.predict(test_images_noisy)

如果仅使用 CPU 运行此代码,将需要很长时间。为了加快速度,你可以使用Colab 免费 GPU自己笔记本上的 GPU,如果有的话。

我不会逐行解释代码,因为我已经在之前的文章中讨论了所有这些内容。

绘制学习曲线

现在,我们可以绘制两个模型的学习曲线,以分析这两个网络的训练表现。为此,我们可以使用两个模型的history对象。

# Plot training and validation loss scores
# against the number of epochs
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

ax1.plot(history_ff.history['loss'], label='Train')
ax1.plot(history_ff.history['val_loss'], label='Validation')
ax1.set_title('FF_AE Loss', pad=12)
ax1.set_xlabel('Epoch')
ax1.set_ylabel('Loss')
ax1.legend(loc='upper right')

ax2.plot(history_conv.history['loss'], label='Train')
ax2.plot(history_conv.history['val_loss'], label='Validation')
ax2.set_title('Conv_AE Loss', pad=12)
ax2.set_xlabel('Epoch')
ax2.set_ylabel('Loss')
ax2.legend(loc='upper right')

plt.savefig("Learning_Curves.png")

学习曲线(图片由作者提供)

  • 左图: 显示了前馈自编码器的训练性能。模型在第 35 轮之后似乎轻微过拟合。

  • 右图: 显示了卷积自编码器的训练性能。模型处于刚刚好的状态,即模型既没有欠拟合也没有过拟合。

可视化输出

现在,我们比较两个模型的输出。为了比较,我还将添加原始图像及其损坏(噪声)版本。

让我们来看一下输出结果。

# Visualize image outputs
n = 10
plt.figure(figsize=(10, 5))

for i in range(n):
    # Display noisy images
    ax = plt.subplot(4, n, i + 1)
    plt.imshow(test_images_noisy[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # Display denoised images by feedforward autoencoder
    ax = plt.subplot(4, n, i + 1 + n)
    plt.imshow(ff_autoencoder_denoised_images[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # Display denoised images by convolutional autoencoder
    ax = plt.subplot(4, n, i + 1 + 2*n)
    plt.imshow(conv_autoencoder_denoised_images[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # Display original images for comparison purposes
    ax = plt.subplot(4, n, i + 1 + 3*n)
    plt.imshow(test_images[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

plt.savefig("Comparisons.png")

(图片由作者提供)

  • 第 1 行: 代表在添加中心(均值)为 0,标准差为 0.7 的高斯噪声后损坏的图像。我们将这些图像输入到自编码器中,以获得干净的图像。

  • 第 2 行: 代表由前馈自编码器清理(去噪/预测)后的图像。图像的内容可以被识别,并且与原始图像非常接近。

  • 第 3 行: 代表由卷积自编码器清理(去噪/预测)后的图像。图像的内容可以很容易地识别,并且几乎与原始图像相同。原因是卷积层可以保持图像中相邻像素之间的空间结构。这在密集层中是不可能实现的。

  • 第 4 行: 代表了为比较目的而添加的原始图像。

结论

卷积自编码器在图像去噪方面优于前馈自编码器,因为卷积自编码器中的卷积层可以保持图像中邻近像素之间的空间结构 — 作者

今天的文章到此结束。

如有任何问题或反馈,请告诉我。

如何进行人工智能课程?

(截图由作者提供)

支持我作为作者

我希望你喜欢阅读这篇文章。如果你愿意支持我作为作者,请考虑 注册会员 以获得对 Medium 的无限制访问。每月仅需 $5,我将从你的会员费用中获得一部分。

[## 使用我的推荐链接加入 Medium - Rukshan Pramoditha

阅读 Rukshan Pramoditha(以及 Medium 上成千上万其他作者)的每一篇故事。您的会员费用将直接...

rukshanpramoditha.medium.com](https://rukshanpramoditha.medium.com/membership?source=post_page-----2fe2e9aed71d--------------------------------)

加入我的私人邮件列表

再也不会错过我精彩的故事了。通过 订阅我的邮件列表,你将会在我发布故事时直接收到。

非常感谢你的持续支持!下篇文章见。祝大家学习愉快!

MNIST 数据集信息

  • 引用: Deng, L., 2012. 用于机器学习研究的手写数字图像的 MNIST 数据库。IEEE 信号处理杂志, 29(6), 第 141–142 页。

  • 来源: yann.lecun.com/exdb/mnist/

  • 许可: Yann LeCun(纽约大学 Courant 研究所)和 Corinna Cortes(谷歌实验室,纽约)持有 MNIST 数据集的版权,该数据集在 创意共享署名-相同方式共享 4.0 国际许可证 下提供 (CC BY-SA)。你可以在 这里 了解更多关于不同数据集许可证类型的信息。

Rukshan Pramoditha

2023–01–24

在 PyTorch 中制作你的第一个 U-Net

原文:towardsdatascience.com/cook-your-first-u-net-in-pytorch-b3297a844cf3

一份提升你图像分割项目的魔法配方

Mostafa WaelTowards Data Science Mostafa Wael

·发布于 Towards Data Science ·6 分钟阅读·2023 年 5 月 12 日

--

照片由 Stefan C. AsaftiUnsplash 上提供

U-Net 是一种用于图像分析中语义分割任务的深度学习架构。它由 Olaf Ronneberger、Philipp Fischer 和 Thomas Brox 在题为 “U-Net: Convolutional Networks for Biomedical Image Segmentation” 的论文中提出。

它对生物医学图像分割任务特别有效,因为它能够处理任意大小的图像,并生成平滑、高质量的分割掩膜和清晰的物体边界。此后,它已广泛应用于许多其他图像分割任务,如卫星和航拍图像分析,以及自然图像分割。

在本教程中,我们将深入了解 U-Net 及其工作原理,并用 PyTorch 制作我们自己的实现配方。那么,开始吧!

它是如何工作的?

U-Net 架构由两部分组成:编码器和解码器。

U-Net: Convolutional Networks for Biomedical Image Segmentation

编码器(收缩路径)

编码器是一系列卷积池化层,这些层逐步下采样输入图像,以在多个尺度上提取特征。

在编码器中,图像的大小逐渐减少,而深度逐渐增加。这基本上意味着网络学习了图像中的“WHAT”信息,但丧失了“WHERE”信息。

解码器(扩展路径)

解码器由一系列 卷积上采样 层组成,这些层将特征图上采样到原始输入图像的大小,同时结合来自编码器的高分辨率特征。这使得解码器能够生成与原始输入图像大小相同的分割掩膜。

你可以从这篇精彩的 文章 中了解更多关于上采样和转置卷积的内容。

在解码器中,图像的大小逐渐增加,而深度逐渐减少。这基本上意味着网络通过逐渐应用上采样来 学习图像中的“WHERE” 信息。

最终层

在最终层,使用 1x1 卷积将每个 64 维特征向量映射到所需的类别数。

我们的烹饪食谱!

我们将进行一个非常直接的实现,编码时最好将上述图片放在你面前。

导入

首先,从 torchtorchvision 包中导入必要的模块,包括用于构建神经网络的 nn 模块和 torchvision.models 提供的预训练模型。 relu 函数也从 torch.nn.functional 导入。

import torch
import torch.nn as nn
from torchvision import models
from torch.nn.functional import relu

UNet 类

然后,定义了一个自定义类 UNet,作为 nn.Module 的子类。 __init__ 方法通过定义编码器和解码器部分的层来初始化 U-Net 的架构。参数 n_class 指定了分割任务的类别数。

class UNet(nn.Module):
    def __init__(self, n_class):
        super().__init__()

        # Encoder
        # In the encoder, convolutional layers with the Conv2d function are used to extract features from the input image. 
        # Each block in the encoder consists of two convolutional layers followed by a max-pooling layer, with the exception of the last block which does not include a max-pooling layer.
        # -------
        # input: 572x572x3
        self.e11 = nn.Conv2d(3, 64, kernel_size=3, padding=1) # output: 570x570x64
        self.e12 = nn.Conv2d(64, 64, kernel_size=3, padding=1) # output: 568x568x64
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2) # output: 284x284x64

        # input: 284x284x64
        self.e21 = nn.Conv2d(64, 128, kernel_size=3, padding=1) # output: 282x282x128
        self.e22 = nn.Conv2d(128, 128, kernel_size=3, padding=1) # output: 280x280x128
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2) # output: 140x140x128

        # input: 140x140x128
        self.e31 = nn.Conv2d(128, 256, kernel_size=3, padding=1) # output: 138x138x256
        self.e32 = nn.Conv2d(256, 256, kernel_size=3, padding=1) # output: 136x136x256
        self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2) # output: 68x68x256

        # input: 68x68x256
        self.e41 = nn.Conv2d(256, 512, kernel_size=3, padding=1) # output: 66x66x512
        self.e42 = nn.Conv2d(512, 512, kernel_size=3, padding=1) # output: 64x64x512
        self.pool4 = nn.MaxPool2d(kernel_size=2, stride=2) # output: 32x32x512

        # input: 32x32x512
        self.e51 = nn.Conv2d(512, 1024, kernel_size=3, padding=1) # output: 30x30x1024
        self.e52 = nn.Conv2d(1024, 1024, kernel_size=3, padding=1) # output: 28x28x1024

        # Decoder
        self.upconv1 = nn.ConvTranspose2d(1024, 512, kernel_size=2, stride=2)
        self.d11 = nn.Conv2d(1024, 512, kernel_size=3, padding=1)
        self.d12 = nn.Conv2d(512, 512, kernel_size=3, padding=1)

        self.upconv2 = nn.ConvTranspose2d(512, 256, kernel_size=2, stride=2)
        self.d21 = nn.Conv2d(512, 256, kernel_size=3, padding=1)
        self.d22 = nn.Conv2d(256, 256, kernel_size=3, padding=1)

        self.upconv3 = nn.ConvTranspose2d(256, 128, kernel_size=2, stride=2)
        self.d31 = nn.Conv2d(256, 128, kernel_size=3, padding=1)
        self.d32 = nn.Conv2d(128, 128, kernel_size=3, padding=1)

        self.upconv4 = nn.ConvTranspose2d(128, 64, kernel_size=2, stride=2)
        self.d41 = nn.Conv2d(128, 64, kernel_size=3, padding=1)
        self.d42 = nn.Conv2d(64, 64, kernel_size=3, padding=1)

        # Output layer
        self.outconv = nn.Conv2d(64, n_class, kernel_size=1)

在 U-Net 论文中,他们使用了 0 填充,并应用了后处理技术以恢复图像的原始大小,而在这里,我们使用 1 填充,这样最终的特征图不会被裁剪,也不需要对输出图像进行后处理。

前向方法

forward 方法指定了输入如何通过网络进行处理。输入图像首先通过编码器层提取特征。然后,解码器层用于将特征上采样到原始图像大小,同时连接相应的编码器特征图。最后,输出层使用 1x1 卷积层将特征映射到所需的输出类别数。

 def forward(self, x):
        # Encoder
        xe11 = relu(self.e11(x))
        xe12 = relu(self.e12(xe11))
        xp1 = self.pool1(xe12)

        xe21 = relu(self.e21(xp1))
        xe22 = relu(self.e22(xe21))
        xp2 = self.pool2(xe22)

        xe31 = relu(self.e31(xp2))
        xe32 = relu(self.e32(xe31))
        xp3 = self.pool3(xe32)

        xe41 = relu(self.e41(xp3))
        xe42 = relu(self.e42(xe41))
        xp4 = self.pool4(xe42)

        xe51 = relu(self.e51(xp4))
        xe52 = relu(self.e52(xe51))

        # Decoder
        xu1 = self.upconv1(xe52)
        xu11 = torch.cat([xu1, xe42], dim=1)
        xd11 = relu(self.d11(xu11))
        xd12 = relu(self.d12(xd11))

        xu2 = self.upconv2(xd12)
        xu22 = torch.cat([xu2, xe32], dim=1)
        xd21 = relu(self.d21(xu22))
        xd22 = relu(self.d22(xd21))

        xu3 = self.upconv3(xd22)
        xu33 = torch.cat([xu3, xe22], dim=1)
        xd31 = relu(self.d31(xu33))
        xd32 = relu(self.d32(xd31))

        xu4 = self.upconv4(xd32)
        xu44 = torch.cat([xu4, xe12], dim=1)
        xd41 = relu(self.d41(xu44))
        xd42 = relu(self.d42(xd41))

        # Output layer
        out = self.outconv(xd42)

        return out

不要忘记点击 Clap Follow 按钮,以帮助我写更多这样的文章。

就这样!

恭喜你成功实现了第一个 PyTorch 中的 U-Net 模型!通过遵循这个食谱,你已经掌握了实现 U-Net 的知识,现在可以将其应用于你未来可能遇到的任何图像分割问题。然而,验证尺寸和通道数以确保兼容性是重要的。U-Net 架构是你工具库中的一个强大工具,可以应用于各种任务,包括医学成像和自动驾驶。所以,快去从互联网上获取任何图像分割数据集,并开始测试你的代码吧!

为了方便,我在这个仓库中添加了一个简单的测试脚本

该脚本生成随机图像和掩膜,并训练 U-net 模型进行图像分割。它有一个名为generate_random_data()的函数,创建具有几何形状(如三角形、圆形、正方形和十字形)的输入图像及其对应的掩膜。U-net 模型使用这些随机图像和掩膜进行训练。训练好的模型随后在新的随机图像上进行测试,分割结果使用plot_img_array()函数绘制。该脚本使用 PyTorch 来训练 U-net 模型,并使用各种函数向输入图像和掩膜中添加形状。

请考虑下载并使用以下代码片段运行测试:

import test
test.run(UNet)

预期的测试输出(由我提供)。

最后的想法

总之,U-Net 架构因其在解决各种图像分割任务中的有效性而在计算机视觉社区中变得非常流行。它独特的设计,包括一个收缩路径和一个扩展路径,使其能够同时捕捉图像的局部和全局特征,同时保留空间信息。

此外,U-Net 架构的灵活性使得修改和改进网络以适应特定需求成为可能。研究人员提出了对原始 U-Net 架构的各种修改,包括改变卷积层、引入注意力机制以及添加跳跃连接等。这些修改在各种应用中提高了性能和分割结果。

总体而言,U-Net 架构已被证明是一个可靠且多功能的图像分割任务解决方案。随着计算机视觉的不断进步,我们可能会看到 U-Net 架构的进一步创新和改进,以提升其性能,使其在解决现实世界问题时更加有效。

不要犹豫与我分享你的想法!

我没有获得 Medium 的补助,因为埃及不支持。如果你喜欢我的内容,请考虑 请我喝咖啡 ☕ 来鼓励我发表更多帖子。谢谢!

使用 Snowflake 进行数据处理

原文:towardsdatascience.com/cooking-with-snowflake-833a1139ab01?source=collection_archive---------6-----------------------#2023-05-17

Snowflake 优化

简单的配方与数据仓库中的即时满足

Prabodh AgarwalTowards Data Science Prabodh Agarwal

·

关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 5 月 17 日

--

使用 Midjourney 生成(付费订阅)

Snowflake 社区充斥着有关如何优化昂贵查询的信息倾销。我们知道这一点,因为我们翻阅了大量这样的信息。我们在这里展示的是我们在Toplyne中采用的三种战术方法。

Toplyne 的业务涉及从实时数据中提取实时洞察。这些数据目前来源于我们客户的产品分析、CRM 和支付系统。

CRM 和支付数据的体量大多是可控的。一个产品会有有限的付费客户,以及在 CRM 中跟踪的略多一些的客户。然而,产品分析数据的体量要大得多。

Toplyne 的 POC(概念验证)和 MVP(最简可行产品)是基于产品分析数据构建的。我们从一开始就知道需要使用数据仓库解决方案来处理这些数据。该解决方案必须满足两个明确的要求:

  1. 它应该能轻松处理几百 GB 的数据。

  2. 它应该提供一个简单而简明的 API 来与这些数据交互。

我们比较了三种解决方案:BigQuery、Redshift 和 Snowflake。

探索后,Snowflake 是明显的选择。简单的原因是它的 SQL 基础接口。SQL 意味着我们的工程操作没有冷启动延迟。Toplyne 的工程师没有来自 DWH 背景的经验,但我们很快上手了。

与客户产品分析数据交互的过程简单如下:

  1. 产品分析数据通过连接器进入 Snowflake。(有很多现成的和本地的连接器可以使用)。

  2. 登录 Snowflake 并使用内置的工作表编写 SQL。🎉

这个简单的 2 步过程意味着我们可以迅速掌握客户与我们分享的数据。

在短时间内,我们开发了两个算法,将我们接收到的数据转换为数据科学家可以训练的模式。第一个算法负责转换产品分析事件数据。第二个算法负责识别用户的个人资料数据。然后在这些数据上编写附加的特征工程算法。

SQL 是一种第四代语言(4GL),相对较容易学习。结合一个只需浏览器标签的基于工作表的界面——Snowflake;一个初创公司可以以最小的设置工作量完成大量的数据处理。

我们在工作表中编写了一些 SQL 语句来转换数据,然后我们的数据科学家可以直接 SELECT * 数据并编写他们的 ML 训练程序。

随着时间的推移,上述整个过程显著扩展。扩展发生在以下几个方面:

  1. 我们有多个客户,每个客户的产品分析数据分布在多个平台上,如 Amplitude、Mixpanel、Segment、Clevertap 等。

  2. 我们的团队编写了多个算法,以不同的维度处理数据。

  3. 我们现在集成了 CRM 和支付数据。此外,这些数据集有自己的一套 ETL 算法。

  4. 我们使用 Airflow 编排庞大的管道,这些管道有多个阶段。

我们 ETL 流程的样本架构图。Snowflake 是该系统的核心。

Toplyne 的数据管道架构

  1. 将源数据同步到 Snowflake。

  2. 使用 Apache Airflow 进行 ETL 编排。

  3. 将转换后的数据存入 Snowflake。

  4. DS/ML/分析师/产品从 Snowflake 获取数据用于他们的工作流程。

几个月来,系统的不同组件经历了多次更改和重大重写,而 Snowflake 是唯一不变的。

由于我们已经运行并维护了一个系统,我们想提出一些关于 Snowflake 中查询优化的想法。我们有一种超级简单的技术,通过对现有查询进行轻微调整,能够从系统中提取出大量的性能。

查询优化

我们运行一个多租户系统,其中单个 Snowflake 实例负责处理大量客户数据的 ETL。ETL 由 Airflow 编排。

我们为每个客户创建一个仓库,并在该仓库上运行所有 ETL 和特征工程。在整个 ETL 运行期间,对一个仓库会依次和/或并行地执行数百个 SQL 查询。一次运行可能持续一个小时,并且一天中可能有多次运行。

本质上,一个仓库的大小运行所有昂贵的以及便宜的查询。因此我们的目标是将仓库大小保持在最低限度。我们通过为不同的 ETL 运行定义服务水平协议(SLA)来定义最小值。然后,我们调整仓库大小,以便可以在该大小下满足 ETL SLA。像任何工程组织一样,我们希望在满足 SLA 的情况下将仓库大小保持在最低限度。

我们有仪表板来监控最昂贵查询的查询模式。这些仪表板有不同的粒度级别。我们不断监控这些仪表板,并不断调整查询。随着时间的推移,我们识别出了一些昂贵查询中的模式,并制定了一本关于如何最小化这些查询运行时间的操作手册。我们将展示三个案例研究,概述查询的问题陈述、最初编写方式、该查询中的瓶颈以及其最终的最佳解决方案。

窗口查询

情景

我们从产品分析数据中跟踪用户的个人资料信息。产品分析系统保存有关用户的多个数据点,例如位置、设备、订阅状态等。一些数据点经常变化,而另一些则不那么频繁。鉴于这些数据的性质,这些信息在数据库中以追加日志的形式表示。

我们的一个特征工程需求是捕获用户最新的个人资料信息,作为 ETL 运行的一部分。

上图给出了 ETL 的流程图。

1 是来自产品分析的原始数据,2 是我们想要应用的算法,3 是 ETL 的最终结果。

我们拥有的 SQL 查询是这样的:

select
    *
from
    tbl
qualify
    row_number() over (partition by id order by timestamp desc) = 1

瓶颈

这个查询很简单,而且在 Snowflake 中效果很好。然而,这个查询中的窗口函数是一个瓶颈。

查询的工作原理如下:

  1. 创建与user_ids数量相同的逻辑桶

  2. 在每个桶中按降序排序数据

  3. row_numbers分配给已排列的数据

  4. 标记桶中的第一个条目

  5. 丢弃剩余的数据。

根据上述解释,我们可以看到,随着表中数据的增加,桶的数量和桶的大小都会增加。由于我们处理的是仅追加数据集,我们应该为这种情况做好准备。在 Snowflake 中,您会在查询分析器中注意到大小增加的趋势,表现为字节溢出

此外,我们需要理解,根据业务需求,桶的数量预计会增加,但作为工程师,我们仍然可以将单个桶的大小保持到最小。

最优解决方案

我们将采用一种技术,通过使用CTE聚合函数来将桶的数量保持到最小。

with
prune_condition as (
    select id, max(timestamp) as prune_column from tbl group by id
),
pruned_data as (
    select * from tbl left join prune_condition using (id)
)
select
    * exclude prune_column
from
    pruned_data
where
    timestamp >= prune_column
qualify
    row_number() over (partition by id order by timestamp desc) = 1

我们将 descending sort expression 的窗口查询子句转换为 max() function,然后将其与源数据连接以获得过滤器。通过使用此过滤器,我们确保数据不会被 qualify 子句丢弃,从而一开始就不会被桶化。这大大减少了窗口查询所执行的工作。此外,使用聚合函数的额外成本被减少的成本大大抵消,因此整体查询变得高效。

Pivot 查询

场景

我们使用事件数据中的一个功能,该功能要求获取每个用户每个事件的计数。

为了获得这些数据,我们执行一个分组查询,然后将这些数据转置以组织成列,如下图所示。

1 是原始数据,2 是转换后的输出。

我们的 SQL 查询如下:

select * from (
    select id, id as users, data from tbl
) pivot(
    count(users)
    for data in (‘d1’, ‘d2’, ‘d3’)
)

瓶颈

尽管示例显示了沿着 3 个元素的 pivot,但我们的生产用例通常在大约 1000 个事件的情况下处理大约一百万用户。

此查询中的 pivot 函数是查询中最慢的步骤。因此,我们希望用手动 pivot 查询替换此逻辑。我们通过结合使用 Group By 子句和 Filter 子句来生成此查询。

最优解决方案 1

select 
    id,
    sum(iff(data = ‘d1’, 1, 0)) as “‘d1’”,
    sum(iff(data = ‘d2’, 1, 0)) as “‘d2’”,
    sum(iff(data = ‘d3’, 1, 0)) as “‘d3’”
from
    tbl
group by id

这个查询显著提高了性能。

然后我们减少了仓库的大小,以查看查询是否保持同样的性能。我们观察到查询显著变慢,并且字节溢出也很严重。然而,字节溢出的一个优点是,我们在减少的仓库大小中有更多的改进空间。

最优解决方案 2

我们根据Map-Reduce框架重写了此查询,并观察到运行时间显著改善。

目标是对较小的事件集执行上述操作,然后一次性将所有数据合并在一起,如下所示:

create temporary batch_1 as (
    select
        id,
        sum(iff(data = ‘d1’, 1, 0)) as “‘d1’”
    from
        tbl
    group by id
);

create temporary batch_2 as (
    select
        id,
        sum(iff(data = 'd2', 1, 0)) as "'d2'",
        sum(iff(data = 'd3', 1, 0)) as "'d3'"
    from
        tbl
    group by id
);

create final_table as (
    select * from batch_1 full outer join batch_2 using(id)
);

我们的生产系统将1000 个事件拆分成10 个块,每块包含100 个事件。此查询显著加速,因为它将字节溢出减少到接近 0。

此外,一旦我们用最优解决方案 1 替换 Pivot 函数,这个优化是非常直观的。

向下滚动以查找本文的配套代码。

聚合查询

SQL 规范定义了许多汇总函数,Snowflake 在这方面表现出色。Snowflake 中也有一个庞大的汇总函数库。

不同的汇总函数有不同的运行时间,在我们看来,每个汇总函数都应根据其优点进行处理。优化汇总函数的策略是首先识别出成为瓶颈的汇总函数,然后激励自己相信可能存在算法解决方案。

我们想与您分享一个案例研究,在这个案例中,我们识别了一个查询,其中选择了一个次优的汇总函数。我们重新设计了该解决方案的算法,使用了一个更简单的汇总函数,从而获得了相同结果的更优性能。

情景

我们有一个事件时间序列,这些事件在产品分析系统中触发。我们需要从这个数据集中回答两个问题,以进行特征工程。

Q1) 识别所有在一秒内大多被多次触发的数据点

Q2) 识别大多在至少一小时间隔内触发的数据点

为了回答这些问题,我们使用 Snowflake 的window querylag functiontbl_1中的输入数据转换为tbl_2

然后我们使用median function编写了解决方案查询。

-- Q1)

select
    data
from
    tbl_2
group by data
having median(sec_diff) = 0;

-- Q2)

select
    data
from
    tbl_2
where
    data <> lag_data
group by data
having median(hour_diff) > 0;

瓶颈

median函数非常慢。

我们让 ChatGPT 建议一个最佳解决方案。它确实提出了使用Percentile函数的解决方案,但这同样很慢,并且似乎与Median函数本身类似。

然而,ChatGPT 很好地解释了为什么提出了那个解决方案。然后我们通过迭代和改进 ChatGPT 的解决方案,提出了一个新的解决方案。

最优解决方案

我们确定,对于我们的需求,我们只需使用计数查询。对于Q1)Q2),我们希望大多数事件的sec_diffhour_diff分别大于或等于 0。

-- Q1) after optimisation

select
    data,
    count(iff(sec_diff = 0, 1, null)) / count(*) as seccount
from
    tbl_2
group by data
having seccount >= 0.5

-- Q2) after optimisation

select
    data,
    count(iff(hour_diff > 0, 1, null)) / count(*) as hourcount
from
    tbl_2
group by data
having hourcount >= 0.5

经验教训

我们持续观察我们的系统,然后确定哪些优化需要紧急分析,哪些可以积压。

Snowflake 提供了多个配置参数,可以进行调优以获得性能。Snowflake 社区定期发布技巧和技术。

在这些信息过载中,我们需要专注并建立一个适合我们的剧本和技术库,这些技术可以毫不费力地应用。

这些是我们用于目的的参数:

  1. 检查查询分析器中的每个节点

  2. 对节点进行输入与输出比率分析

    i) 尝试降低这个比率

    ii) 给定问题描述时,输出将保持不变

    iii) 因此,尽量减少对汇总节点的输入

  3. 另一种方法是持续测量磁盘溢出。尽可能减少溢出

    i) 较大的仓库有较低的溢出,但成本也较高

    ii) 只有在减少同一仓库中的溢出时,你才能获得优化

将本文作为代码复现: 你可以参考这个 GitHub 链接来获取与这些查询的基准测试相关的代码。

结论

这篇文章提供了三种高度可操作的优化技术,你可以立即应用。你可能在代码库中有类似于我们这里展示的情况的次优模式。随时使用这些代码示例来获得即时解决方案。在其他情况下,请动脑筋,我将很高兴了解你的解决方案。

关注我以获取更多关于 Snowflake、数据和 MLOps 的文章。

附言:除非另有说明,否则所有图片均来自作者。

Copulas:时间序列预测中的基本指南与应用

原文:towardsdatascience.com/copulas-an-essential-guide-applications-in-time-series-forecasting-f5c93dcd6e99

什么是 copula 函数,我们为什么需要它们

Nikos KafritsasTowards Data Science Nikos Kafritsas

·发表于数据科学前沿 ·阅读时间 14 分钟·2023 年 3 月 6 日

--

3D 高斯 copula(图由作者提供)

Copulas 是建模多个分布之间依赖关系的函数。

它们主要用于金融应用,如投资组合风险评估和对冲基金管理。它们在 2008 年声名鹊起,当时发现定量科学家在计算中误用 copulas,导致未能预测重大事件

尽管如此,copulas 仍然是很棒的数学工具。研究 copulas 的一个有力理由是它们在时间序列预测模型中有着广泛的应用。

在过去几年里,许多深度学习预测模型开始将统计学概念融入其架构中。这是件好事,因为社区正在从传统的“堆叠变换器块与注意力”方法转向使用优雅的统计技术

在这篇文章中,我们讨论:

  • 一些统计学的入门概念。

  • 为什么 copulas 有用。

  • 如何在 Python 中创建高斯 copulas。

  • 使用 copulas 进行时间序列预测的现代深度学习模型。

让我们深入了解。

我推出了AI Horizon Forecast这是一个专注于时间序列和创新 AI 研究的新闻通讯。订阅这里以拓宽你的视野!

入门概念 — 可选

如果以下术语:

对你来说可能是未知的,我建议你也阅读这一部分。

Copulas 很容易理解,但如果你不熟悉上述概念,可能一开始会让你感到困惑。

概率积分变换 假设我们有一个连续随机变量 X**。

然后,XCDF (累积分布函数),称为 F_x,生成一个 均匀 随机变量 U。具体来说:

该技术表明我们可以将任何连续随机变量转换为均匀分布。稍后我们将通过可视化演示这些技术如何工作。

反向采样 你是否曾经想过像 R 语言中的 rnorm() 和 Python 中的 np.random.randn() 这样的分布生成函数是如何生成样本的?

他们使用采样技术,其中一种被称为 反向采样。有许多采样技术——一些比 反向采样 更复杂和更快速。

反向采样 只是 概率积分变换 定理的逆。给定我们之前生成的均匀样本,我们可以通过原始随机变量 X逆累积分布函数,用符号 F(x)^(-1) 表示,来得到原始 X 分布。具体来说:

两种技术都使用 CDF:

  • 概率积分变换 使用 CDF 将我们的样本从 [a,b]->[0,1] 转换,(其中 [a,b] 是函数的定义域)。

  • 反向采样 使用 逆 CDF 将我们的均匀样本转换回函数的原始样本:[0,1]->[a,b]

图 1 更清楚地展示了这两种技术的工作原理:

图 1: 红色线是 概率积分变换,蓝色线是 反向采样(图片由作者提供)

现在,我将通过 Python 中的几个图表演示这些技术是如何工作的。我们的计划如下:

  1. 从 beta 分布 B~(10,3) 生成样本。

  2. 使用 概率积分变换 从 beta 分布创建均匀样本。

  3. 使用 反向采样第 2 步 的均匀样本中重建原始 beta 样本(10,3)。

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
from scipy import stats
sns.set(rc={'figure.figsize':(8,3)})

# Step 1: Create a histogram from beta(10,3)
# we start by drawing 1000 samples
beta1 = stats.distributions.beta(a=10, b=3).rvs(1000)
ax = sns.histplot(beta1, kde=True, stat='density')
plt.xlabel("beta1", fontsize=14)
plt.ylabel("Density", fontsize=14)
plt.title("Histogram of beta(10,3)", fontsize=15)
plt.show()

# Step 2: Create a uniform distibution from beta(10,3)
# using the integral probability transform
u1 = stats.beta.cdf(beta1,a=10, b=3)
ax = sns.histplot(u1, stat='density')
plt.xlabel("u1", fontsize=14)
plt.ylabel("Density", fontsize=14)
plt.title("Histogram of uniform samples from beta(10,3) CDF", fontsize=15)
plt.show()

# Step 3: Use inverse sampling to generate beta(10,3) samples
# from the previous uniform samples
# notice that ppf is the inverse cumulative distribution function 
beta_transformed = stats.beta.ppf(u1,a=10, b=3)
ax = sns.histplot(beta_transformed, kde=True, stat='density')
plt.xlabel("beta1", fontsize=14)
plt.ylabel("Density", fontsize=14)
plt.title("Histogram of transformed beta(10,3)", fontsize=15)
plt.show()

图 2:(顶部)原始 beta 分布,(中间)beta 的 CDF 均匀样本,(底部)变换后的 beta 分布(图片由作者提供)

因此,我们回到了原始分布。你也可以尝试其他连续变量。

让我们尝试 gamma 分布。我们保持其他不变,只是将 beta 替换为 gamma:

sns.set(rc={'figure.figsize':(8,3)})

# Step 1: Create a histogram from gamma
gamma1 = stats.distributions.gamma(a=1, loc=0).rvs(1000)
ax = sns.histplot(gamma1, kde=True, stat='density')
plt.xlabel("gamma1", fontsize=14)
plt.ylabel("Density", fontsize=14)
plt.title("Histogram of gamma", fontsize=15)
plt.show()

# Step 2: Create a uniform distibution from gamma
u1 = stats.gamma.cdf(gamma1, a=1, loc=0 )
ax = sns.histplot(u1, stat='density')
plt.xlabel("gamma1", fontsize=14)
plt.ylabel("Density", fontsize=14)
plt.title("Histogram of uniform samples from gamma CDF", fontsize=15)
plt.show()

# Step 3: Use inverse sampling to generate gamma samples
gamma_transformed = stats.gamma.ppf(u1,a=1, loc=0)
ax = sns.histplot(gamma_transformed, kde=True, stat='density')
plt.xlabel("gamma1", fontsize=14)
plt.ylabel("Density", fontsize=14)
plt.title("Histogram of transformed gamma", fontsize=15)
plt.show()

图 3:(顶部)原始 gamma 分布,(中间)gamma 的 CDF 均匀样本,(底部)变换后的 gamma 分布(图片由作者提供)

注意事项:

  • 如果你使用更多的样本,图形将更好地逼近原始分布。

  • 逆采样并不适用于所有分布——例如,因为它们的逆 CDF 未明确定义(如在二元分布中)。在这种情况下,我们使用其他采样方法,如Metropolis-Hastings Monte Carlo等。

如果你更喜欢R,这是一个使用伽马分布的示例:

gamma1 <- rgamma(1e6, shape=1)
hist(gamma1, main='gamma distribution', cex.main=1.3, cex.lab=1.3, cex.axis=1.3, prob='true')

# pgamma is the cdf of gamma
u <- pgamma(gamma1, shape=1)
hist(u, main='Histogram of uniform samples from gamma CDF', cex.main=1.3, cex.lab=1.3, cex.axis=1.3, prob='true')

# qgamma is the inverted cdf of gamma
gamma_transformed <- qgamma(u, shape=1)
hist(gamma_transformed, main='Histogram of transformed gamma', cex.main=1.3, cex.lab=1.3, cex.axis=1.3,prob='true')

图 4:(左)原始伽马分布,(中)伽马分布累积分布函数的均匀样本,(右)变换后的伽马分布(图像作者提供)

再次,我们得到相同的结果。

经验分布函数

通常,我们数据的原始分布是未知的。

或者,我们想建立一个不依赖于分布的模型,不对基础数据做假设。

在这种情况下,经验分布显得很有用。它是一种强大且简单的方法,提供了非参数的总体累积分布函数(CDF)估计。

因此,我们可以估计数据的 CDF,而不管其基础分布如何。它定义如下:

X_1X_2、…、X_n为来自具有 CDF F(x)的总体的样本,样本大小为n。那么经验 CDF F_n(x)定义为:

如果那个公式让你困惑,这里有一个更直观的解释:

  • 从一个未知分布中取N个样本。

  • 对这些样本进行排序,并将它们放在 x 轴上。

  • 开始绘制一个‘阶梯函数’风格的线——每当你在 x 轴上遇到一个数据点时,将阶梯增加1/N

为了清楚起见,这个过程记录在图 5:

图 5: 正态分布的累积分布函数(绿色)及其经验累积分布函数(蓝色)(来源:[1])

你取的样本越多,经验分布就会越接近真实分布。Kolmogorov-Smirnov 检验正是这样做的。

具体来说,这个测试通过构建和比较样本的经验 CDF与假设的CDF来确定你的未知数据样本的分布。如果两个 CDF 接近,你的未知数据样本可能符合假设的分布。

接下来,在本文中,我们将略微不同地使用经验 CDF:从来自未知总体的数据中创建 copulas。

Copulas

在介绍 copulas 的数学之前,我们首先讨论为什么我们需要它们。

我们为什么需要 copulas?

假设我们有一组随机变量。每个随机变量在集合中反映了不同的‘实体’,例如

  • 一个投资组合中的证券。

  • 一家商店产品的销售数字。

  • 各家庭的电能消耗模式。

挑战 1: 我们可以用什么数学概念来解释这些实体?

我们可以使用 多变量分布 — 一种最优地描述我们数据的分布。

挑战 2: 如果我告诉你这些实体 不是 相同分布的呢?

  • 如果每个实体有不同的行为,我们 不能 假设它们遵循相同的分布。

  • 最重要的是,每个实体可能会影响其他实体 — 我们 不能 假设它们是独立的。以 产品自相残杀 为例:在零售中,一款成功的产品会从其类别中的类似商品中抢走需求。

因此,每个 实体 可能有不同的分布。此外,我们还应该找到一种方法来建模它们的相关性,因为在大多数实际场景中,独立性通常不可行。

我们可以用 连结函数 来解决这两个挑战。

连结函数

假设我们有一个随机向量 **z** 你可以把随机向量看作是随机变量的集合:

接下来,设 F_jz_j 的边际 CDF,j ∈ [1,N]。利用之前的 概率积分变换,我们得到:

然后,我们定义一个连结函数 **C** : [0,1] → [0,1]^N 作为 [u_1, u_2, … u_N] 的联合 CDF:

这就是连结函数的定义。或者,用简单的英语说:

连结函数只是多个随机变量的多变量 CDF,具有均匀边际 U~[0,1]

注意函数的 定义域值域:例如,一个有两个变量的连结函数定义域是 [0,1]²,值域是 [0,1]。如果你感到困惑,不用担心,我会稍后详细解释如何绘制连结函数。

为了更清晰地说明,我们写出连结函数:

此外,连结函数的最大优势在于任何 CDF **F(z)** 都可以用连结函数 **C** 及其边际 F_j 表示。这也被称为 斯克拉尔定理:

因此,我们可以使用连结函数表示任何 CDF 函数。注意第二行,我们使用了 概率积分变换

高斯连结函数

斯克拉尔定理 非常有用。我们尝试表示一个多变量高斯分布。换句话说,我们将创建一个高斯连结函数

为了简单起见,我们将创建一个 二元 高斯连结函数 (N = 2)

首先,考虑一个二元高斯分布 [Z_1, Z_2]

然后,高斯连结函数表示如下:

高斯连结函数公式

其中 Φ 是标准正态分布的 CDF,Φ_2 是由相关系数 **ρ** 参数化的 **[**Z_1, Z_2**]** 的联合 CDF。让我们分解上述高斯连结函数的方程:

  • 第 1 行: 我们从通用连结函数定义开始。

  • 第 2 行: 我们使用逆采样技巧将我们的均匀边际转换为高斯分布。

  • 第 3 行: 我们重新格式化方程以表示高斯 Copula。最后一行是正式的高斯 Copula 公式。

如果你对数学仍感到困惑,请不用担心。在下一节中,我们将逐步展示如何绘制高斯 Copulas。

高斯 Copulas 实践

要编码和绘制高斯 Copula,我们需要 3 件事:

  1. N 个连续变量,来自任何连续分布。

  2. 高斯 Copula 公式。

  3. 要决定我们的随机变量之间的相关程度 — 就是前面提到的相关因子 **ρ**

为了保持我们的例子简单,我们将使用 2 个随机变量而不是 N 由于这两个分布是联合 Copula CDF 的一部分,我们将它们称为 边际分布:

表 1: 高斯 Copula 公式

这就是理论背景。我们现在拥有绘制 Copula 所需的一切。

现在,让我们开始绘制边际分布:

mean1 = 0
std1 = 1
mean2 = 0
std2 = 1

x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)

z1=np.random.normal(mean1, std1, 1000)
z2=np.random.normal(mean2, std2, 1000)

df_normal=pd.DataFrame({'z1':z1, 'z2':z2})
h = sns.jointplot(x='z1', y='z2', data=df_normal)

h.set_axis_labels('z1 (normal)', 'z2 (normal)', fontsize=13);
h.plot_joint(sns.kdeplot, color="r", zorder=0, levels=6)
h.plot_marginals(sns.rugplot, color="r", height=-.15, clip_on=False)

图 6: Z1 和 Z2 的联合散点图

接下来,我们将高斯变量转换为均匀变量,使用 概率积分变换。记住,Copulas 以均匀边际 u1 和 u2 作为输入。

u1 = norm.cdf(z1)
u2 = norm.cdf(z2)

df_uniform=pd.DataFrame({'u1':u1, 'u2':u2})
h = sns.jointplot(x='u1', y='u2', data=df_uniform)

h.set_axis_labels('u1 (uniform)', 'u2 (uniform)', fontsize=14);
h.plot_joint(sns.kdeplot, color="r", levels=1)
h.plot_marginals(sns.rugplot, color="r")

图 7: u1 和 u2 的联合散点图 — 两者都是均匀分布

计算出 u1u2 变量后,我们可以将它们输入到高斯 Copula 中。

唯一剩下要配置的是 Copula 的相关因子 **ρ**,它决定了两个分布的相关程度。我们使用 **ρ**=0.8

高斯 Copula 图显示在 图 8 中。为了创建 Copula,我们将使用 表 1 中显示的转换:

# Gaussian copula: C(u1,u2) = Φ2[ Φ^-1(u1), Φ^-2(u2) ]

#sort the results to make the plot smoother
u1=np.sort(u1)
u2=np.sort(u2)

# make the last sample exactly 1 to make the plot smoother
u1[-1]=1
u2[-1]=1

x1=stats.norm.ppf(u1)  # Φ^-1(u1)
x2=stats.norm.ppf(u2)  # Φ^-1(u2)

# Parameters of Φ2
mu_x = 0
variance_x = 1
mu_y = 0
variance_y = 1
# the covariance parameter is what parameterizes the Gaussian copula
cov=0.8

X, Y = np.meshgrid(x1,x2)
pos = np.empty(X.shape + (2,))
pos[:, :, 0] = X; pos[:, :, 1] = Y

#remember phi2_ is just a multivariate normal CDF
rv = stats.multivariate_normal([mu_x, mu_y], [[variance_x, cov], [cov, variance_y]])
phi2 = rv.cdf(pos) 

X, Y = np.meshgrid(u1,u2)

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(projection='3d')
ax.plot_surface(X, Y, phi2, cmap=cm.coolwarm, linewidth=0)
ax.invert_yaxis()
ax.set_xlabel('X axis')
ax.set_ylabel('Y axis')
ax.set_zlabel('Z axis')
ax.set_title('Gaussian Copula of (z1,z2)', fontdict={'fontsize': 18})
plt.show()

图 8: 来自 2 个高斯分布的高斯 Copula 图

试验 **ρ** 以观察图形的变化。也可以修改均值和方差向量。

同样,请注意 图 8 中,Copulas 的输入和输出都在 [0,1] 范围内。记住之前我们如何定义 Copulas(**C** : [0,1] → [0,1]^N,其中 N 是边际数 — 这里我们有 2 个)

现在,到了关键问题的时候:

挑战 3: 为什么我们需要高斯 Copula 而不是多变量高斯 PDF?毕竟,高斯 Copula 只是一个多变量 CDF。

  • 嗯,一个多变量高斯 PDF 只需要高斯边际分布。

  • 使用 Copulas,我们可以使用 概率积分变换任何连续分布 转换为均匀边际 — 同时保持 依赖结构。

  • 换句话说,Copulas 的输入可以是任何连续随机变量的混合。

实验:使用 Beta 和 Gamma 作为边际分布

让我们做一些图示以演示上述观点。

我们将使用一个 beta 和一个 gamma 随机变量,而不是使用两个标准正态分布作为输入——并且仍然得到一个高斯 Copula。

实际上,我们可以使用任何连续分布作为输入。此外,我们可以假设我们的 Copula 对底层数据分布一无所知。为了实现这一点,我们可以使用经验 CDF,它计算输入数据的 CDF,无论数据如何分布。

请记住,我们的 Copula 输入分布可以代表任何随机事件,比如产品销售。为了清楚起见,我们将使用一个 beta 和一个 gamma 分布:

from statsmodels.distributions.empirical_distribution import ECDF

# Gaussian copula: C(u1,u2) = Φ2[ Φ^-1(u1), Φ^-2(u2) ]
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)

# draw our data samples from 2 distributions, a beta and a gamma - 
beta1 = stats.distributions.beta(a=10, b=3).rvs(1000)
gamma1 = stats.distributions.gamma(a=1, loc=0).rvs(1000)

# - we use the emprical cdf instead of beta's or gamma's cdf
# - we do this to show that copulas can be computed regardless of the
#   underlying distributions
ecdf1 = ECDF(beta1)       # F(beta1) = u1
ecdf2 = ECDF(gamma1)      # F(gamma1) = u2

# small correction to remove infinities
ecdf1.y[0]=0.0001
ecdf2.y[0]=0.0001

x1=stats.norm.ppf(ecdf1.y) # Φ^-1(u1)
x2=stats.norm.ppf(ecdf2.y) # Φ^-1(u1)

# Parameters of Φ2
mu_x = 0
variance_x = 1
mu_y = 0
variance_y = 1
cov=0.8

X, Y = np.meshgrid(x1,x2)
pos = np.empty(X.shape + (2,))
pos[:, :, 0] = X; pos[:, :, 1] = Y

#remember phi2 is just a multivariate normal CDF
rv = stats.multivariate_normal([mu_x, mu_y], [[variance_x, cov], [cov, variance_y]])
phi2=rv.cdf(pos)

X, Y = np.meshgrid(ecdf1.y,ecdf2.y)

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(projection='3d')
ax.plot_surface(X, Y, phi2, cmap=cm.coolwarm, linewidth=0)
ax.invert_yaxis()
ax.set_xlabel('X axis')
ax.set_ylabel('Y axis')
ax.set_zlabel('Z axis')
ax.set_title('Gaussian Copula of (beta,gamma)', fontdict={'fontsize': 18})
plt.show()

图 9: 来自 beta 和 gamma 分布的高斯 Copula 图

就这样。我们得到了相同的结果。你可以在这里找到包含所有图表的笔记本。

注意,还有其他类型的 Copulas,例如阿基米德 Copulas。每种 Copula 类型都有其优缺点。例如,阿基米德 Copulas 更擅长建模重尾或偏斜分布。

在这篇文章中,我们重点关注了高斯 Copulas,因为它们更容易理解——并且在时间序列应用中非常丰富。

如何作为数据科学家使用 Copulas

除非你是计量经济学家或量化数据科学家,否则不太可能使用 Copulas。在医学领域也有一些稀有的应用案例。

然而,Copulas 再次浮现。它们在深度学习模型中找到了众多应用,尤其是在时间序列预测中。

让我们来看看其中的一些:

深度 GPVAR: 深度高斯过程向量自回归模型

亚马逊流行的 TS 预测模型DeepAR的改进版和新颖版本

深度 GPVAR[2]使用 LSTM 网络对高斯 Copula 进行参数化。通过应用高斯 Copula,深度 GPVAR 共同建模了数千个时间序列,考虑了它们之间的相互依赖性。

记住我们之前提到的产品相互侵蚀:假设你的数据集由多个时间序列组成,每个时间序列表示一个产品的销售。如果一个产品突然畅销,它很可能会影响类似产品的销售。

如果你想要一个捕捉多个时间序列间相互依赖的模型,那么深度 GPVAR 是一个理想的选择。

TACTiS: Transformer-注意力 Copulas 用于时间序列

TACTiS[3]是一个新颖的基于转换的 TS 预测模型,也使用 Copulas。

该模型由 ServiceNow 团队发布——即突破性的N-BEATS模型的开发团队。

TaCTICS 通过使用 transformer 架构来估计高维时间序列的联合分布,该架构学习模仿非参数化 copulas。虽然 TaCTICS 不假设 copulas 遵循特定的分布(这就是它们是非参数化的原因),但模型使用注意力机制来估计它们。

此外,该模型可以处理数百个时间序列,支持预测和插值,处理未对齐和非均匀采样的数据,并能够在训练期间适应缺失数据。

CopulaCPTS:用于多步骤时间序列预测的 Copula Conformal 预测

CopulaCPTS[4] 也是一个革命性的模型,它结合了 copulas 和一种有前景的方法,称为 conformal prediction

CopulaCPTS 基于 conformal prediction,这是一种无分布的量化不确定性的方法,提供良好校准的预测区间。

此外,CopulaCPTS 利用 copulas 捕捉多个时间步之间的不确定性。得益于 copulas,该模型在现有的基于 conformal 的实现上有所改进,使得预测能够扩展到多个步骤。

结束语

Copulas 是建模变量间复杂依赖关系的重要工具,尤其是在高维数据分析的背景下。

此外,深度学习的最新进展引发了对它们的新兴趣,特别是在时间序列预测领域。

我们看到了 3 篇在其架构中使用 copulas 的新颖论文:

  • Deep GPVAR 使用 LSTM 来参数化高斯 copula 并模拟高斯过程。

  • TaCTICS 使用注意力机制来制定非参数化 copulas。

  • CopulaCPTS 还从数据中经验性地创建非参数化 copulas。

Deep GPVAR 和 TaCTICS 是开源的,所以可以随意查看。此外,Deep GPVAR 是亚马逊预测套件的一部分。TaCTICS 相比其他基于 Transformer 的预测模型声称具有 SOTA 性能。

不用说,在阅读并理解了本文后,你将更容易理解前述论文。

总的来说,copulas 的复兴突显了在广泛应用中建模变量间复杂依赖关系的重要性。此外,它们在深度学习模型中的持续使用表明,copulas 在可预见的未来将继续发挥关键作用。

感谢阅读!

## AutoGluon-TimeSeries : 创建强大的集成预测 — 完整教程

亚马逊的时间序列预测框架涵盖了所有内容。

aihorizonforecast.substack.com

参考资料

[1] 实证分布函数,维基百科, 许可证:公有领域

[2] Salinas 等人 High-Dimensional Multivariate Forecasting with Low-Rank Gaussian Copula Processes

[3] Drouin 等人 TACTiS: Transformer-Attentional Copulas for Time Series,ICML(2022 年 6 月)

[4] Sun 等人 Copula Conformal Prediction for Multi-Step Time Series Forecasting

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

正确采样偏差的推荐系统

原文:towardsdatascience.com/correct-sampling-bias-for-recommender-systems-d2f6d9fdddec

推荐中的采样偏差是什么?如何纠正?

Thao VuTowards Data Science Thao Vu

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

--

图片由 NordWood Themes 提供,Unsplash上的照片

介绍

推荐系统在我们的数字生活中无处不在,从电子商务巨头到流媒体服务。然而,每个大型推荐系统背后都隐藏着一个挑战,这可能显著影响其有效性——采样偏差。

在本文中,我将介绍采样偏差如何在训练推荐模型期间发生,以及我们如何在实践中解决这个问题。

让我们深入了解!

双塔推荐系统

通常,我们可以将推荐问题表述为:给定查询 x(可能包含用户信息、上下文、之前点击的项目等),找到用户可能感兴趣的项目集合{y1,.., yk}

大规模推荐系统的主要挑战之一是低延迟要求。然而,用户和项目池庞大且动态,因此对每个候选项进行评分并贪婪地找到最佳项是不可能的。因此,为了满足延迟要求,推荐系统通常被分解为两个主要阶段:检索和排序。

多阶段推荐系统(作者提供的图像)

检索是一种廉价且高效的方式,可以从庞大的候选池(数百万或数十亿)中快速捕捉到顶级候选项(几百个)。检索优化主要涉及两个目标:

  • 在训练阶段,我们希望将用户和项目编码为嵌入,以捕捉用户的行为和偏好。

  • 在推理过程中,我们希望通过近似最近邻(ANN)快速检索相关项目。

对于第一个目标,常用的方法之一是双塔神经网络。该模型因通过结合物品内容特征来解决冷启动问题而获得了广泛的关注。

具体来说,查询和物品通过相应的 DNN 塔进行编码,以便相关的(查询,物品)嵌入保持彼此接近。

双塔神经网络(图片来源于作者)

它可以被重新格式化为如下的多分类问题。

  • 给定查询x和一系列物品(y1, y2,..),我们输出一个概率向量,大小等于物品的数量,用于预测用户与物品yi互动的概率pi(~1: 是/~0: 否)。

  • 对于训练数据,正标签表示用户已经点击/查看/购买了物品yi。负标签表示该物品yi已经展示给用户,但用户没有任何互动。

  • 对于对(x,yi)对的处理,通过其相应的塔进行编码,得到(emb_x, emb_yi)

  • emb_xemb_yi的相似度可以通过点积或余弦相似度来测量,称为sim_i

  • 互动的概率通过如下的 softmax 概率进行测量,其中 Z 为标准化项。

Softmax 概率(图片来源于作者)

折叠问题

模型的目标是对我们的数据集进行良好的泛化:它应该能够预测那些在训练数据中未出现的(x, y)对的得分。

为此,我们需要教会模型,对于每个查询嵌入,好的物品嵌入应该被拉得更近,而无关的物品则应被推得更远。模型如何知道这些无关的物品?基于负样本。

构建负训练数据的最初方法之一是使用用户查看但未点击/查看/购买的物品。然而,每个用户仅查看和互动的物品很少,因此反馈相对稀疏,并且偏向特定的子群体。

如果我们仅在日志中出现的(x, y)对上训练模型,模型可能会很好地学习保持查询和匹配物品接近,但也可能偶然将查询和完全无关的物品放在一起。这个问题被称为“折叠”[4]。

因此,我们需要采样一些额外的负数据进行训练,以教会模型将无关的嵌入保持远离彼此。

但我们如何有效地收集这些负数据?

批次内负样本和偏差问题

最简单的解决方案是使用批次内负样本,我们使用同一批次中的其他物品作为负样本。

给定整个数据批次(x1, y1), (x2, y2),…, (x_bn, y_bn),对于每个样本(xi, yi),我们将使用(xi, yj)作为批次内负样本。在下图中,蓝色线条表示正样本,红色线条表示批次内负样本。

批量负采样。(作者提供的图像)

批量负训练是一种简单且节省内存的方式,可以重用批量中已经存在的项目样本,而无需从整个项目语料库中采样和计算新的样本。

然而,它往往会受到采样偏差的影响,尤其是对于数据分布不均的情况。例如,考虑 YouTube 平台;一些热门视频可能有数十亿的观看次数,而大多数其他视频只有几百次观看。这些热门视频会在训练数据中比其他视频出现得更频繁。因此,它们更有可能被用作“批量负样本”,从而被过度惩罚。

我们如何解决这个偏差样本问题?

重要性采样

首先,我们需要定义什么是针对我们问题的理想和无偏采样。给定整个项目语料库 (y1, y2,…., yN),对于每个正样本 (x, y),我们希望告知模型语料库中的所有其他项目都是负样本。即,每个项目 y 应以相等的概率 p(y) = 1/N 进行采样。

因此理想情况下,归一化项 Z 应为:

从项目池中进行 Softmax 采样(作者提供的图像)

然而,由于对每个项目进行梯度更新所需的计算资源非常庞大,这几乎是不可能的。解决方案是使用 "重要性采样" [2]。我们仍然使用批量中的样本,但会“修正”其分布,使其反映上述无偏采样分布。

给定一个批量项目集 (y1, y2,..,yn) 和 q(yi)yi 的批量采样概率,我们将按如下方式修正 q

偏差采样校正(作者提供的图像)

在公式中,ri 充当“修正”权重,使我们能够利用批量中的项目,但仍遵循正确的均匀分布。

这就是偏差校正的核心思想。接下来,让我们了解一下大型科技公司 Google 如何校正其推荐系统的采样偏差。

实践中的负采样校正

针对流数据的采样偏差校正

在这项工作 [1] 中,作者介绍了一种算法,用于校正流数据的偏差采样。由于推荐系统越来越接近实时更新,因此在线训练与流数据的结合是必要的。

推荐系统的在线训练 [1]

如上所述,我们可能需要访问整个训练数据集来校正采样偏差,以估计项目 y 的分布,p(y)。然而,测量流数据的分布几乎是不可能的。

论文建议根据包含 item_i 的两个连续批次之间的差距来估计 p(yi)。直观地说,差距越小,y 的受欢迎程度越高。

样本频率估计 [1]

基于其受欢迎程度的估计,我们可以决定y应被惩罚多少作为批内负样本。y越受欢迎,对批量损失的影响应该越小。因此,它将受到更少的惩罚。

流数据的采样校正(图片由作者提供)

这种方法不需要固定的项词汇表,仍然可以产生无偏估计。此外,它可以适应在线项目分布随时间的变化。

为了研究策略的影响,团队在 YouTube 视频推荐上进行了离线评估实验,其中大约 10M 个视频的索引每隔几个小时周期性构建。在 7 天的评估期间,10%的数据日志被保留用于评估。与普通负采样相比,采样校正的方法在离线召回指标上取得了显著提升。

从 YouTube 上的 10M 视频语料库中检索点击视频页面的 Recall@K。[1]

受到积极离线结果的鼓励,团队继续在线尝试新的检索方法。参与度指标也显著提高,证明了采样校正的有效性。

YouTube 直播实验结果[1]

混合负采样

这项工作[3]更进一步解决了偏差问题。论文提到了一种类型的偏差,“选择性偏差”,即没有用户反馈的项目将永远不会被纳入训练数据。因此,模型缺乏区分长尾项和其他项的通用性。

作者提出,除了批内负样本外,我们还从整个项目语料库中抽取负样本。因此,所有项目都有机会作为负样本。这对于模型了解新鲜和长尾项尤其有帮助。

除了批内项目 B 外,我们还从整个项目语料库中均匀地抽取项目作为额外的负样本,命名为 B'。然后,采样将“校正”到上述方法类似。

混合负采样[3]

为了证明混合负采样的影响,团队在 Google Play 应用推荐中进行了实验。基线与两种负采样策略进行了比较:仅批内负采样和混合负采样。

与基线相比,两种负采样方法都将项目内容作为特征以改善通用性。然而,批内负采样组在离线和在线评估中表现均不如基线。对于这一组,团队观察到模型推荐中出现了一些无关的长尾应用。这支持了批内负采样存在选择性偏差的假设。长尾项没有频繁地被纳入训练数据,因此没有被应有地降级。

相比之下,混合负采样在离线和在线评估中均表现优越。即,对于在线 AB 测试,团队关注高质量的安装用户,即那些安装并使用应用程序的用户。混合负采样组以 1.54%的显著差距获胜。

Google Play 数据集中几个模型的 Recall@K [3]

Google Play 上 3 个模型的实时实验结果。[3]

参考文献

[1] 易欣阳, 等. "用于大规模语料库项目推荐的采样偏差修正神经建模。" 第十三届 ACM 推荐系统会议论文集。2019 年。

[2] en.wikipedia.org/wiki/Importance_sampling

[3] 杨继, 等. "用于推荐的双塔神经网络的混合负采样。" 2020 年 Web 会议论文集。2020 年。

[4] 辛多丽丝, 等. "折叠:为什么好的模型有时会做出虚假的推荐。" 第十一届 ACM 推荐系统会议论文集。2017 年。

相关系数以及如何误解一个关系

原文:towardsdatascience.com/correlation-coefficient-and-how-to-misunderstand-a-relationship-79a55905e34e

统计学

解释相关性远比大多数人想象的要困难。另一方面,误解相关性却不难。

Marcin KozakTowards Data Science Marcin Kozak

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

--

完美相关性!照片由Matt Seymour拍摄,来自Unsplash

相关性……谁没有听说过相关性?我们经常使用这个术语,但却对它了解不多。当你说,“这两件事是相关的,”你到底是什么意思?或者,当你说两件事之间的相关性很强时,你是什么意思?

对于数据科学家和统计学家来说,相关性比对其他人更为重要。这是因为我们是处理数据的人,所以我们有责任理解数据中发生的任何现象,并向商业同事、同行研究人员或我们合作的任何人解释这些现象。

我曾经遇到过这样情况的次数不下于一百次。从我的经验来看,尽管相关性是一个简单的概念,但向他人解释它却极为困难。为什么?因为大多数受过教育的人对这个概念有一定的理解——而且他们通常对这种理解有着出乎意料的依赖。因此,我们经常需要先打破这面墙,才能解释给定关系的含义。

今天,我们将讨论相关性强度的含义。你将了解到相关性并不像乍看上去那么简单。

相关性的强度

我们将讨论皮尔逊相关系数。我们得出的结论同样适用于斯皮尔曼相关系数,它表示排名相关性——但也适用于任何相关系数,无论是参数性的还是非参数性的,无论是线性的还是非线性的。

0.91 的相关系数是什么意思?-0.41 的相关性呢?0.27 呢?-0.65 呢?0.07 呢?

许多人会迅速地将这些系数解释如下:

  • 0.91 → 强正相关

  • -0.41 → 弱负相关

  • 0.27 → 弱正相关

  • -0.65 → 中等负相关

  • 0.07 → 缺乏相关性

为什么会这样?首先,大多数统计学教师建议使用这种或类似的术语,并提出我称之为相关性指南的内容。例如,在这个网页上:

[## Pearson Product-Moment Correlation

皮尔逊积矩相关系数(简称皮尔逊相关系数)是一个衡量……的指标

statistics.laerd.com](https://statistics.laerd.com/statistical-guides/pearson-correlation-coefficient-statistical-guide.php?source=post_page-----79a55905e34e--------------------------------)

你会发现以下指南:

相关系数解释的典型指南。图片由作者提供,基于这个来源

我见过很多这样的指南。它们可能使用稍有不同的措辞;在这里,我们考虑小/中/大关联强度;其他指南可能使用弱/中/强关联/相关;还有一些可能使用类似的东西。但基本上,这些指南提供了一些相关系数的区间,可以用来决定相应关联的强度。

相关性的强度和意义

这些指南的准确性如何?我将从两个角度来探讨这个问题:

  • 聚类化

  • 背景

聚类化及其结果的近似

在这里,聚类化指的是创建多个值的簇,并利用这些簇来解释相关系数。上述,我们有三个这样的簇:大、中、小;或者更准确地说是六个:

  • 负的强、中等和小的相关性

  • 正的强、中等和小的相关性

因此,0.66 表示强正相关。-0.22 表示弱负相关。

这只是一个粗略的估算。0.51 的相关性被认为是强的,就像 0.99 一样。但 0.49 的相关性,和 0.51 不同,是比较弱的……我知道,这些只是数字,所以让我展示一些数据。在下图中,你可以看到三对变量 xy,这三对变量都呈正态分布(准确来说,它们遵循双变量正态分布)。如你所见,它们的相关性分别是 0.480.530.99

看看图表并分析它。你能说出关于这三种相关性的什么?

三对具有不同相关性的值。作者提供的图像

一件有趣的事是,根据上述指南,第一对变量的相关性与第二对变量的相关性不同,因为

  • 第一对的关联强度是中等的 因为相关性是 0.476

  • 第二对的关联强度是大的 因为相关性是 0.528

但第二对的关联强度与第三对非常相似,因为

  • 第二对的关联强度是大的 因为相关性是 0.528

  • 第三对的关联强度也是大的 因为相关性是 0.986

对不起,但这不是我能接受的。这确实很有意义。实际上,没有任何意义。

这是我不喜欢这种指南的地方——但这不是我唯一不喜欢的地方。虽然这点很重要,但下一点更为重要。

背景

这样的相关性指南假设知道相关系数的值就足够知道这种关联的强度。甚至不关心你在测量什么。总体不重要,样本也不重要;重要的是这个单一的值。

这意味着我们忽略了相关性的背景。不论我们测量什么,都无所谓。可以是任何东西。可以是所有东西。

一些相关性指南至少尝试强调你测量的内容是重要的。例如,它们可以附带一句话解释你应该考虑关系的背景。例如,在上表下方,你可以阅读以下句子(来源):

记住,这些值是指南,关联是否强也将取决于你测量的内容。

好的,因此一方面,你应该将 0.51.0 之间的关联强度视为强,但另一方面,“你也应该记住这还取决于你测量的内容。”

所以……什么?怎么回事?我的意思是……什么?这是否意味着我应该根据我测量的内容来改变指南?但怎么做?改变限制?去掉它们?有任何例子吗?

非常有帮助。

确实,这样的关联指导完全忽视了我们研究现象的背景。使用这些指导,你可能对,例如,

  • 同卵双胞胎身高之间的关联,以及

  • 独立个体对之间的身高关联。

这样的关联指导完全忽视了我们研究现象的背景。

我知道第二对关联似乎没有多大意义。我的意思是,没有人会真正考虑估计这样的关联——但这正是它成为虚无关联的完美例子。如果你进行方法论上正确的研究来测量这个关联,它将是虚无或接近虚无的

目前假设我们在全球人群中基于两个大样本(两个不同的相关性,两个不同的样本)估计了这两个相关性。同卵双胞胎身高之间的关联应该接近1.0,而例如0.5将是惊人地弱,不应该出现。另一方面,独立个体之间的身高关联应该接近0.0,而0.5将是惊人地强,不应该出现。

你注意到我刚刚做了什么吗?我在两个不同的情况下使用了相同的两个变量之间的相关系数值0.5——结果发现,在其中一个情况下这个值是惊人地弱,而在另一个情况下却是惊人地强。在这两种情况下,0.5的相同值不应该出现。在一种情况下,它不应该出现,因为这将意味着关联过于弱。在另一种情况下,它不应该出现,因为这将意味着关联过于强。

这对这些指导说明了什么?

很多。这样的指导几乎毫无价值。它们让人停止思考。除非你喜欢被误导、困惑、伪造的感觉,否则绝不要使用它们。与其使用这些指导——不如动脑筋。思考。

这样的指导几乎毫无价值。它们让人停止思考。

与其使用这些指导——不如动脑筋。思考。

结论

不要使用这样的关联指导。不要告诉自己,“我只会简单看一眼以便知道我应该使用什么措辞,但我不会使用这些指导。”停止自欺欺人。

相反,考虑你正在分析的现象;思考你应该期望什么样的关联;并考虑你得到的估计值与这个期望关联的距离。如果估计值远小于期望值,那么关联可能很弱。如果估计值远大于期望值,那么关联可能很强。当估计值相似时,它很可能是预期的

这是我在这些指导中缺失的词——以及在一般的相关系数解释中。预期。说实话,目前我不知道如何将这个词融入我们对相关性的知识中。它还不适合。

但让我们一步一步来,不急于求成。我们来思考一下。我已经考虑了相关性这么多年,多花一点时间也无妨。

你注意到我上面使用的“likely”这个词了吗?例如这句话:“如果它大很多,它很可能很强。” 我这么说是因为当你获得了这样意外的结果时,首先应该检查样本是如何收集的。相关系数对样本大小很敏感。样本过小可能导致不准确的估计。因此,不要过于依赖从小样本中估计的相关系数。这样的估计可能与真实(总体)值相差甚远。我不希望这种情况发生在大样本的情况下,但在小样本的情况下,它不仅是可能的——很可能。我觉得可以写一篇更长的文章来探讨样本大小在相关性中的意义,所以请期待未来的文章。

我没有提到的是,我们经常将两个变量之间的关联视为线性的,但它不一定是这样。也许你得到了意外的小线性相关系数值,因为关联并不是线性的?这也是需要检查的另一件事,但这是另外一回事…

感谢阅读这篇文章。我是基于我在Teaching Statistics期刊上发表的以下文章写的:

Kozak, M. (2009). What is Strong Correlation? Teaching Statistics, 31(3), 85–86

如果你喜欢这篇文章,你可能也会喜欢我写的其他文章;你可以在这里找到它们。如果你想加入 Medium,请使用下面我的推荐链接:

[## 使用我的推荐链接加入 Medium - Marcin Kozak

阅读 Marcin Kozak 的每一个故事(以及 Medium 上成千上万其他作家的故事)。你的会员费直接支持…

medium.com

相关性——当 Pearson 的 r 不足以满足需求时

原文:towardsdatascience.com/correlation-when-pearsons-r-is-not-enough-aded72308635

各种相关性方法的比较

Farzad MahmoodinobarTowards Data Science Farzad Mahmoodinobar

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

--

仅有一个关键会解锁,由 DALL.E 2

我们对“相关性并不意味着因果关系”这句话非常熟悉,但让我们通过一个实际例子来理解混淆相关性与因果关系可能带来的影响。1998 年 2 月,一篇论文发表,声称某些疫苗与儿童自闭症之间存在因果关联。这篇论文后来被发现是欺诈性的,并于 2010 年撤回。可以想象,这样的主张对那些基于这篇论文的发现未接种疫苗的人的生活产生了什么影响,那里相关性被误认为是因果关系。

在这篇文章中,我们将深入探讨相关性,以更好地理解它是什么。我们将学习根据研究中的变量类型,推荐使用的相关性方法。最后,我们将在 Python 环境中实现一些最常见的方法。

[## 通过我的推荐链接加入 Medium - Farzad Mahmoodinobar

阅读 Farzad(和 Medium 上的其他作者)的每一个故事。你的会员费直接支持 Farzad 和其他人…

medium.com](https://medium.com/@fmnobar/membership?source=post_page-----aded72308635--------------------------------)

什么是相关性?

相关性是衡量两个感兴趣变量之间关系(或关联)的统计度量。它使用“相关系数”来量化这种关系的方向强度。让我们深入了解相关系数的概念知识:

  • 范围和方向: 相关系数的范围(几乎总是——我们会讨论例外情况)从-1 到 1,包括这两个值。正负取决于两个变量之间的关系方向。

  • 解释: 值为 1 表示两个变量之间存在完全的正相关(即一个增加,另一个也按相同比例增加,反之亦然)。另一方面,值为-1 表示两个变量之间存在完全的负相关(即一个增加,另一个按相同比例减少,反之亦然)。值为 0 表示当一个变量变化时,另一个变量保持不变。

  • 测量: 测量相关性的方法有多种。在本文中,我们将探讨各种测量方法。

现在我们已经熟悉了什么是相关性,让我们深入探讨各种计算相关性的方法。

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

相关性方法的总体比较

下表提供了我们将在本文中讨论的各种相关性方法的概述和比较。这个表格是未来使用的很好的参考。

相关性方法比较

让我们更详细地讨论这些方法。

数据集

为了实现一些相关性方法,我们将使用来自 UCI 机器学习库(CC BY 4.0)的数据集,其中包括车价和与每个车价相关的车属性。我已经清理和筛选了数据,可以从 这个链接 下载。我们将查看车价与发动机大小或燃料类型的相关性。对于其余的方法,我们将现场创建小数据集进行实施。

让我们将车的数据导入到 Pandas 数据框中,然后在散点图中可视化价格和发动机大小,以更好地了解这两个变量之间的视觉关系。

# Import libraries
import numpy as np
import pandas as pd
import seaborn as sns
from scipy import stats
import matplotlib.pyplot as plt
%matplotlib inline

# Read the data
df = pd.read_csv('auto-cleaned.csv')

# Create the scatterplot
sns.regplot(data = df, x = 'price', y = 'engine-size', fit_reg = False, color = 'purple')

# Add labels for X and Y axes
plt.xlabel('Price')
plt.ylabel('Engine Size')
plt.show()

结果:

车价与发动机大小的散点图

散点图显示随着价格的上涨,发动机大小也在增加。因此,我们期望相关性为正。让我们看看计算是否支持这一点。

相关性计算方法

1. 皮尔逊相关系数

这是最广泛使用的相关性测量。皮尔逊相关系数(PCC),也称为皮尔逊的r,是衡量两个变量之间线性相关性的指标。正如定义所示,这种方法假设两个变量之间存在线性关系,因此不适用于非线性关系。该相关性还假设变量大致服从正态分布。

从数学上讲,它可以通过以下方式计算:

Pearson 的 r

1.1. Pearson 的相关系数——实现

幸运的是,我们不必自己进行数学计算。让我们使用 Python 计算数据集中价格和发动机大小之间的 Pearson 的 r。

# Calculate Pearson's r
stats.pearsonr(df['price'], df['engine-size'])

结果:

统计量“~0.89”是我们寻找的相关系数。正如我们所预期的那样,两者之间存在正相关(且相对较强)。“p 值”是对零假设检验的结果,零假设认为所提供的数据分布是无相关且正态分布的。在这个例子中,p 值是一个非常小的数字,意味着我们可以拒绝零假设(即存在相关性)。

2. Spearman 的排名相关系数

也称为 Spearman 的 𝜌(读作“rho”),是一种衡量两个变量之间排名相关性的指标,它衡量两个变量之间的关系如何被单调函数描述。概念上,这比听起来要简单得多,一旦我们定义了“排名相关性”和“单调函数”。

  • 排名相关性: 排名相关性衡量的是两个数据集之间的顺序相似性,相对于彼此(请记住,PCC 并没有直接测量相对排名)。

  • 单调函数: 如果一个函数保持其参数的给定顺序,即在输入值增加时,函数始终增加或始终减少,则称该函数为单调函数(这听起来与我们定义的相关性非常相似)。单调关系与线性关系之间有区别。线性关系是一种特殊的单调关系,其中增长率保持不变——换句话说,与线性关系不同,单调关系中的变化量(增加或减少)可以有所不同。

从数学上讲,当所有 n 个排名都是不同的整数时,它的计算方法如下:

Spearman 的排名相关系数

其中:

2.1. Spearman 的排名相关系数——实现

让我们看看如何在 Python 中计算相同的两个变量——价格和发动机大小。

# Calculate Spearman's rho
stats.spearmanr(df['price'], df['engine-size'])

结果:

结果与 Pearson 的 r 非常相似,正如预期的那样。

3. Kendall's Tau

Kendall's Tau,记作𝛕,是一种非参数排名相关性测量方法。我们从之前的相关性方法中知道了排名相关性的含义。非参数意味着它不依赖于基础数据的概率分布。Kendall's Tau 是非参数的,因为它仅基于数据的相对排序来测量排名相关性(而不是数据的具体值)。

从数学上讲,Kendall’s Tau 可以通过两种不同的方式计算,它们的唯一不同之处在于如何将其归一化到 -1 到 1 的范围内。我们将在此处定义两种方法以供参考:

Kendall’s Tau — b

其中:

Kendall’s Tau — c

其中:

一致对意味着两个观察值相对于其他观察值的排名方式相同。例如,假设:

然后,两个观察对是 (x_1, x_2) 和 (y_1, y_2)。如果 x_1 的排名高于 x_2,则 y_1 的排名也高于 y_2,这一对观察被认为是一致的。反之则为不一致。

3.1. Kendall’s Tau — 实现

让我们看看如何在 Python 中计算这两个 Kendall’s Tau 的解释,以便对价格和发动机大小这两个变量进行相同的计算。

# Calculate Kendall's Tau
tau_b = stats.kendalltau(df['price'], df['engine-size'], variant = 'b')
tau_c = stats.kendalltau(df['price'], df['engine-size'], variant = 'c')

print(f"Kendall's Tau (b) is: {tau_b}")
print(f"Kendall's Tau (c) is: {tau_c}")

结果:

类似于其他相关性测量,存在一个正相关以及一个非常小的“p 值”,这表明存在相关性。正如预期的那样,Tau b 或 c 的两个实现之间没有很大的差异。

4. 点二列

点二列相关系数测量一个二值(或二分)变量与一个连续变量之间的相关性。二值或二分变量是指仅取两个值的变量(例如 0 或 1,女性或男性等)。例如,Pearson’s r 测量两个 连续 变量之间的相关性。但是,在处理一个二值变量和一个连续变量的情况下,我们可以使用点二列相关系数。

点二列相关系数的计算方法如下:

点二列相关系数

其中:

4.1. 点二列 — 实现

在我们的数据集中,燃料类型可以是汽油或柴油,我们可以将其用作二值变量。首先,我们将创建一个名为“fuel-type-binary”的新列,其中汽油的值为 0,柴油的值为 1。然后我们计算燃料类型与汽车价格之间的点二列相关系数。

# Create the new column for the binary values
df['fuel-type-binary'] = df['fuel-type'].replace({'gas' : 0, 'diesel' : 1})

# Calculate Point-Biserial correlation coefficient
stats.pointbiserialr(df['price'], df['fuel-type-binary'])

结果:

5. Phi 系数

Phi 系数(即均方列联系数),用 ɸ 表示,是另一种用于测量两个变量之间的关联(或相关性)的指标,但仅在两个变量都是二值或二分变量时使用。如果你是专注于分类的机器学习从业者,你可能也知道它作为 Matthews 相关系数(MCC)。在机器学习中,MCC 用作二值或多类分类的质量度量。

从数学上讲,ɸ 对于两个二元变量 X 和 Y 定义如下:

其中:

上述表格表示被称为“列联表”。接下来,让我们看看如何在 Python 中实现 Phi 系数。为了完整性,我们将介绍两种方法。

5.1. Phi 系数 — 在 Pandas 中的实现

我们将在下面的代码块中采取以下步骤:

  1. 导入必要的包

    2. 从两个假设的二元变量 X 和 Y 创建一个数据框

    3. 创建一个列联表

    4. 计算 Phi 系数

# Import necessary packages
import pandas as pd
import math

# Create a dataframe from two assumed binary variables of X and Y
df = pd.DataFrame({'X': [1, 1, 0, 0, 1, 0], 'Y': [1, 0, 1, 1, 0, 1]})

# Create a contingency table
table = pd.crosstab(df['X'], df['Y'])

# Extract the values from the table
n11 = table.iloc[0,0]
n10 = table.iloc[0,1]
n01 = table.iloc[1,0]
n00 = table.iloc[1,1]

# Calculate the phi coefficient
coef = (n11*n00 - n10*n01) / (math.sqrt((n11+n10)*(n11+n01)*(n00+n10)*(n00+n01)))

print(f"Phi Coefficient: {coef}")

结果:

这并不难计算,但仍然相对手动且容易出错。让我们看看第二种方法,这种方法更为直接。

5.2. Phi 系数 — 在 scikit-learn 中的实现

记住 Phi 系数也被称为 Matthews 相关系数(MCC)吗?scikit-learn 库中正好包含了它,所以让我们看看如何实现它,按照以下步骤:

  1. 导入必要的包

    2. 创建两个假设的二元变量

    3. 计算 MCC

# Import necessary packages
from sklearn.metrics import matthews_corrcoef

# Create two assumed binary variables
X = [1, 1, 0, 0, 1, 0]
Y = [1, 0, 1, 1, 0, 1]

# Calculate MCC
mcc = matthews_corrcoef(X, Y)
print(f"Matthews Correlation Coefficient: {mcc}")

结果:

正如预期的那样,这个数字与我们在第一种方法中生成的数字完全相同。

专业提示: 这种方法旨在衡量二元(以及多类别)分类的质量,我们在这个例子中使用它来计算两个变量之间的关联。在机器学习中,典型的应用是使用 MCC 来衡量分类问题中真实值与预测值的相关性或关联性。我们这里的方法是合适的,并且生成了正确的结果,但这是一个重要的警告,以防你决定将来使用 MCC。

6. Cramer’s V

Cramer’s V(也称为 Cramer’s Phi,记作 V)是两个类别(名义)变量之间的关联(或相关性)度量。这与 Phi 系数非常相似,但它更为广泛,因为它可以应用于 nn* 列联表(与只能应用于二元变量的 ɸ 不同)。

重要说明: 这是本文中唯一一个范围为 0 到 1(包括 0 和 1)的度量(相比于其他相关性度量,其范围为 -1 到 1,包括 -1 和 1)。

Cramer’s V 可以按如下方式计算:

其中:

6.1. Cramer’s V — 实现

让我们看一个实现 Cramer’s V 的例子。我们将采取以下步骤:

  1. 导入必要的库

    2. 创建一个包含 X 和 Y 两个变量的数据框,每个变量有两个不同的类别

    3. 创建一个列联表

    4. 计算卡方统计量

    5. 计算 Cramer’s V

# Import necessary libraries
import pandas as pd
import math

# Create a dataframe
df = pd.DataFrame({'X': ['A', 'A', 'B', 'B', 'A', 'B'], 'Y': ['W', 'X', 'W', 'W', 'X', 'W']})

# Create a contingency table
table = pd.crosstab(df['X'], df['Y'])

#Calculate the chi-squared statistic and the p-value
chi2, p, dof, expected = stats.chi2_contingency(table)

#Calculate Cramer's V
V = math.sqrt(chi2 / (table.values.sum()*min(table.shape[0]-1, table.shape[1]-1)))

print(f"Cramer's V: {V}")

结果:

7. 多序列相关性

多序列相关性是衡量两个分类(有序)变量之间的关联(或相关性)。由于这些是有序变量,因此相关性考虑了关联的强度和方向(因此范围为 -1 到 1,与 Cramer’s V 不同)。多序列相关性的一个特例是四分位相关性,仅用于二元或二分变量——我们将在帖子后面讨论这一点。

8. 部分相关性

部分相关性是控制一个或多个混杂因素的情况下,两个变量之间的相关性度量。混杂因素是与独立变量和依赖变量(即输出)相关的变量。换句话说,部分相关性衡量的是在从这种关系中去除一个或多个其他变量的影响后,两个变量之间的关联(或相关性)。

一个常见的混杂因素示例是研究吸烟(X)与肺癌(Y)之间关系的研究。在这项研究中,年龄(Z)是一个混杂因素。吸烟者通常年纪较大,而年龄本身也是肺癌的风险因素。因此,年龄(Z)可以影响吸烟(X)和肺癌(Y)。然后,部分相关性可以用来控制 Z 的混杂效应(即从研究中去除年龄的影响),然后研究将集中在控制环境下吸烟(X)与肺癌(Y)之间的相关性。

部分相关性可以按如下方式计算:

部分相关性

协方差和方差的计算方法如下:

我们将介绍在 Python 中实现部分相关性的两种方法,一种是直接计算这种相关性,另一种是使用 Python 库简化过程。

8.1. 部分相关性 — 在 Pandas 中的实现

部分相关性的公式看起来很复杂,但可以很容易地在 Python 中实现。让我们看看一个例子,我们将采取以下步骤:

  1. 导入必要的软件包

    2. 创建包含 X、Y 和 Z 变量的数据框

    3. 计算 X 和 Y 之间的部分相关性,同时控制 Z

# Import necessary packages
import pandas as pd

# Create a dataframe of X, Y and Z variables
data = {
    'X': [1, 1, 9, 0, 1, 8, 10, 7, 10, 0, 1, 9, 0, 6, 2, 6, 9, 0, 9, 7], 
    'Y': [8, 2, 4, 3, 0, 1, 6, 0, 5, 6, 10, 3, 2, 7, 4, 5, 6, 0, 5, 10],
    'Z': [0, 2, 1, 1, 2, 2, 7, 7, 2, 6, 4, 4, 7, 7, 6, 6, 1, 6, 4, 7]
}

df = pd.DataFrame(data)

# Create a correlation matrix
corr_matrix = df.corr()

x_y_correlation = corr_matrix.loc['X', 'Y']
x_z_correlation = corr_matrix.loc['X', 'Z']
y_z_correlation = corr_matrix.loc['Y', 'Z']

# Calculate the Partial Correlation between X and Y, while controlling for Z
partial_correlation_xy_z = (x_y_correlation - (x_z_correlation * y_z_correlation)) / ((1 - (x_z_correlation ** 2)) * (1 - (y_z_correlation ** 2))) ** 0.5

print(f"Partial Correlation (between X and Y, while controlling for Z): {partial_correlation_xy_z}")

结果:

8.2. 部分相关性 — 在 Pingouin 中的实现

之前的方法有助于理解部分相关性的概念,但创建每个相关性并使用公式计算部分相关性并不高效——这也增加了人为错误的机会。在下面的代码块中,我们将利用 pingouin 库来简化过程,通过以下步骤进行:

  1. 导入必要的软件包

    2. 创建包含 X、Y 和 Z 变量的数据框

    3. 计算 X 和 Y 之间的部分相关,同时控制 Z

# Import necessary packages
import pandas as pd
import pingouin as pg

# Create a dataframe of X, Y and Z variables
data = {
    'X': [1, 1, 9, 0, 1, 8, 10, 7, 10, 0, 1, 9, 0, 6, 2, 6, 9, 0, 9, 7], 
    'Y': [8, 2, 4, 3, 0, 1, 6, 0, 5, 6, 10, 3, 2, 7, 4, 5, 6, 0, 5, 10],
    'Z': [0, 2, 1, 1, 2, 2, 7, 7, 2, 6, 4, 4, 7, 7, 6, 6, 1, 6, 4, 7]
}
df = pd.DataFrame(data)

# Calculate the Partial Correlation between X and Y, while controlling for Z
partial_correlation_xy_z = pg.partial_corr(data = df, x='X', y='Y', covar='Z', method = 'pearson')

print(partial_correlation_xy_z)

结果:

结果有助于理解,因为还有额外的字段。让我们来看一下这些字段所传达的信息 (source):

  • n:样本大小

  • r:部分相关系数

  • CI95%:r 的 95% 参数置信区间

  • p-val:p 值

请注意,我们决定在这个具体实现中使用 Pearson 的 r,使用了 method = ‘pearson’。另一种选择是通过包含 method = ‘spearman’ 来使用 Spearman 𝜌。

9. 四分相关系数

四分相关系数是多重相关系数的一个特殊情况,用于测量两个二元(或二分)变量之间的关联。回忆一下,Phi 系数也测量两个二元变量之间的关联。区别在于 Phi 系数假设数据服从正态分布,而四分相关系数假设双变量正态分布。换句话说,四分相关系数假设二元变量是从一个服从正态分布的连续变量生成的,而 Phi 相关系数则假设二元变量本身服从正态分布。四分相关系数在基础连续变量不可直接观察但可以假设为正态分布的情况下非常有用。这种情况在心理学、医学、营销和/或社会科学研究中很常见,其中基础行为不可直接观察(例如,自我报告的政治观点与实际投票行为之间的关联)。

结论

在这篇文章中,我们讨论了理解相关性及其测量方法的重要性。根据被调查变量的类型,推荐使用特定类型的相关性。然后,我们介绍了最常见的相关性方法及其在 Python 环境中的实现方法。

感谢阅读!

如果你觉得这篇文章有帮助,请在 Medium 上关注我并订阅以接收我的最新文章!

CountVectorizer 提取文本特征的详细教程

原文:towardsdatascience.com/countvectorizer-to-extract-features-from-texts-in-python-in-detail-0e7147c10753

照片来源:Towfiqu barbhuiyaUnsplash

使用 CountVectorizer 在 Sklearn 中高效工作的所有必备知识

Rashida Nasrin SuckyTowards Data Science Rashida Nasrin Sucky

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

--

任何自然语言处理(NLP)项目最基本的数据处理是将文本数据转换为数值数据。只要数据是文本形式,我们无法对其进行任何计算操作。

有多种方法可以进行文本到数值数据的转换。本教程将解释其中最基本的向量化方法之一,即 scikit-learn 库中的 CountVectorizer 方法。

这个方法非常简单。它将每个单词的出现频率作为数值。一个例子会让它更清楚。

在以下代码块中:

  • 我们将导入 CountVectorizer 方法。

  • 调用该方法。

  • 将文本数据适配到 CountVectorizer 方法中,并转换为数组。

import pandas as pd 
from sklearn.feature_extraction.text import CountVectorizer 

#This is the text to be vectorized
text = ["Hello Everyone! This is Lilly. My aunt's name is also Lilly. I love my aunt.\
        I am trying to learn how to use count vectorizer."]

cv= CountVectorizer() 
count_matrix = cv.fit_transform(text)
cnt_arr = count_matrix.toarray()
cnt_arr

输出:

array([[1, 1, 2, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 1, 1]],
      dtype=int64)

这里我有表示上面文本数据的数值。

我们如何知道哪些值代表文本中的哪些单词?

为了澄清这一点,将数组转换为 DataFrame,其中列名将是单词本身,将会很有帮助。

cnt_df = pd.DataFrame(data = cnt_arr, columns = cv.get_feature_names())
cnt_df

图片来源:作者

现在,它显示得很清楚。单词‘also’的值为 1,这意味着‘also’在测试中仅出现了一次。单词‘aunt’在文本中出现了两次。所以,单词‘aunt’的值为 2。

在最后一个例子中,所有的句子都在一个字符串中。所以,我们为四个句子只得到了一个数据行。让我们重新排列文本,看看会发生什么:

text = ["Hello Everyone! This is Lilly", 
        "My aunt's name is also Lilly",
        "I love my aunt",
        "I am trying to learn how to use count vectorizer"]
cv= CountVectorizer() 
count_matrix = cv.fit_transform(text)
cnt_arr = count_matrix.toarray()
cnt_arr

输出:

array([[0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0],
       [1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0],
       [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0],
       [0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 2, 1, 1, 1]],
      dtype=int64)

这一次,我们有一个二维数组,其中每个字符串在文本中都有一个单独的列表。将这个数组放入 DataFrame 中:

cnt_df = pd.DataFrame(data = cnt_arr, columns = cv.get_feature_names())
cnt_df

图片由作者提供

仔细查看这个 DataFrame。所有单词都作为列名存在。每一行代表文本中的一个字符串,列中的值显示了单词在该字符串中出现的次数。如果单词没有出现,值为零。

sklearn 库的 CountVectorizer 方法 中,有一些值得检查的参数。

lowercase

如果你注意到,默认情况下 CountVectorizer 方法将所有单词转换为小写。如果你不希望这样,你需要将 lowercase 设置为 False

cv= CountVectorizer(lowercase=False) 
count_matrix = cv.fit_transform(text)
cnt_arr = count_matrix.toarray()
cnt_df = pd.DataFrame(data = cnt_arr, columns = cv.get_feature_names())

图片由作者提供

现在,单词以文本中的原样形式被取出。单词 ‘My’ 在 DataFrame 中出现了两次,分别为 ‘My’ 和 ‘my’。

stop_words

stop_words 是我们认为对分析不必要的单词。在我们的文本中,我可能认为‘also’、‘is’和‘to’是不必要的单词。我可以简单地排除它们,这在大多数分析或机器学习模型的数据处理过程中是非常重要的一部分。这里我们只有 4 个字符串。但在实际的分析中,我们需要处理成千上万的字符串。成千上万的字符串可能涉及成千上万的单词,每个单词都成为一个特征。如果我们可以排除一些频繁出现的或对模型不那么必要的单词,将节省大量计算工作。

CountVectorizer 方法本身为许多主要语言提供了默认的停用词列表。这里是一个例子。

cv= CountVectorizer(stop_words='english') 
count_matrix = cv.fit_transform(text)
cnt_arr = count_matrix.toarray()
cnt_df = pd.DataFrame(data = cnt_arr, columns = cv.get_feature_names())
cnt_df

图片由作者提供

看!许多单词都被去除了!

如果你认为已经去除的单词不够,或者去除了太多单词,请提供你自己的 stop_words 列表。例如,如果我只想排除‘also’、‘is’、‘am’ 和 ‘to’,我会像这样提供 stop_words 列表:

cv= CountVectorizer(stop_words=['also', 'is', 'am', 'to']) 

max_df

这是一种去除单词的方式。如果我们使用 max_df = 0.5,这意味着如果一个单词出现在超过 50%的文档或字符串中,那么该单词将被去除。max_df 也可以使用整数值。例如,max_df = 20 意味着如果一个单词存在于超过 20 个文档中,它将被去除。

为了演示这一点,我创建了一个新文本:

text = ["lilly is a good girl", 
        "lilly is a good student",
        "lilly is very good in math", 
        "lilly loves coffee", 
        "She is from Brazil"]
cnt_vect = CountVectorizer(max_df=0.75)
count_mtrx = cnt_vect.fit_transform(text)
cnt_arr = count_mtrx.toarray()
cnt_df = pd.DataFrame(data = cnt_arr, columns=cnt_vect.get_feature_names())
cnt_df

图片由作者提供

‘Lilly’ 出现在 5 个文档中的 4 个中。因此它被去除了。‘is’ 也是如此。

min_df

这与 max_df 相反。如果一个文档出现的比例或指定的次数少于 min_df,则该文档会被排除。在这个例子中,我使用了与上一个例子相同的文本,并将 min_df 设置为 2。因此,任何在少于 2 个文档中出现的单词将被去除。

cnt_vect = CountVectorizer(min_df=2)
count_mtrx = cnt_vect.fit_transform(text)
cnt_arr = count_mtrx.toarray()
cnt_df = pd.DataFrame(data = cnt_arr, columns=cnt_vect.get_feature_names())
cnt_df

图片由作者提供

由于我们只有 3 个单词剩下,因为我们只有 5 个文档。这在机器学习项目中可能很有用。

当我们试图提取趋势时,只有在成千上万的文档中的几个文档中偶尔出现的词汇并不太有帮助。

max_features

这是另一个有用的功能。当我们有成千上万的词汇时,计算开销大且耗时。如果我们总共有 10000 个词汇,那么就有 10000 个特征。现在,如果你认为根据词频只有前 2000 个词汇可能足够好,你可以简单地使用 max_features = 2000。这里甚至没有这么多词汇。所以,我会使用 max_features = 5

cnt_vect = CountVectorizer(max_features=5, stop_words='english')
count_mtrx = cnt_vect.fit_transform(text)
cnt_arr = count_mtrx.toarray()
cnt_df = pd.DataFrame(data = cnt_arr, columns=cnt_vect.get_feature_names())
cnt_df

作者提供的图片

这里是出现频率最高的五个词。

结论

这篇文章尝试解释 CountVectorizer 方法以及如何最好地使用这种文本处理方法。我在这里解释的参数如果正确使用,可以使你的分析或自然语言处理模型更加高效。这些参数可以单独使用,也可以根据需要将其中的一些参数组合在一起。实验的空间很大。现在有更复杂的方法来向量化文本数据,但这种简单的方法在许多情况下仍然有效。

随时可以在 Twitter 上关注我,并点赞我的 Facebook 页面。

如果你想要这个教程的视频版本,这里是链接:

更多阅读

完整的 Python 探索性数据分析 [## 完整的 Python 探索性数据分析

数据清洗、分析、可视化、特征选择、预测建模

30 个非常有用的 Pandas 函数用于日常数据分析任务 [## 30 个非常有用的 Pandas 函数用于日常数据分析任务

Pandas 备忘单

6 个处理缺失值的技巧 [## 6 个处理缺失值的技巧

包括迭代方法、均值和中位数填充与分组、均值和中位数填充

详细的 Python 多项式回归教程:概述、实现和过拟合

完整的 Python 代码

pub.towardsai.net ## 使用 GradientTape 进行 TensorFlow 模型训练

使用 GradientTape 更新权重

[towardsdatascience.com ## 使用自编码器方法进行 TensorFlow 和 Keras 的异常检测

一种前沿的无监督方法,用于去噪、降维、异常检测等

[towardsdatascience.com

优惠券收集者问题:一个概率杰作

原文:towardsdatascience.com/coupon-collectors-problem-a-probability-masterpiece-1d5aed4af439?source=collection_archive---------0-----------------------#2023-03-04

揭开经典概率难题的复杂性

Naman AgrawalTowards Data Science Naman Agrawal

·

关注 发布在 Towards Data Science · 14 分钟阅读 · 2023 年 3 月 4 日

--

图片由 Tamanna Rumee 提供,来源于 Unsplash

数学的世界充满了迷人的难题和悖论,它们挑战我们对周围世界的理解。优惠券收集者问题就是这样一个难题,这个古老的问题让数学家和统计学家们几代人以来都为之着迷。优惠券收集者问题如下:

考虑一个涉及收集 n 种独特优惠券的竞赛。每个谷物盒子里都有一张优惠券,每张优惠券在给定盒子中出现的概率相同。在你打开多少个盒子之前,你能确保每种类型的优惠券至少有一张?这个看似简单的问题有一个复杂的答案,并且在从计算机科学到金融等领域中都有应用。在本文中,我们将深入探讨优惠券收集者问题,探索其框架,并讨论其许多迷人的含义。

离题:几何随机变量

在解决这个问题之前,让我们重新回顾一下几何随机变量的一些性质。这将提供一个有效的机制来计算描述问题解决方案架构的各种参数。定义:如果随机变量 X 的概率质量函数如下所示,则称 X 遵循参数为 p 的几何分布:

直观上,几何随机变量表示为获得成功所需的独立试验次数的分布,假设成功的概率保持不变,即 p。因此,找到 P(X = k) 等同于找到需要 k 次试验才能获得成功的概率,即在第一次成功之前有 k — 1 次失败。因此,所需的概率为:

几何随机变量在现实世界问题中有许多应用,例如建模赢得游戏所需的尝试次数或在呼叫中心联系客户所需的电话次数。优惠券收集者问题也可以有效地使用几何随机变量建模(具体来说,是一组独立的、不完全相同分布的几何变量的总和,我们稍后会看到)。几何随机变量具有以下性质:

可选证明: 首先,我们将确定几何随机变量的期望值。回顾一下,随机变量的期望值类似于其值的加权平均值,加权由其概率质量函数给出。可以证明,几何随机变量的期望值是其参数 p 的倒数,即成功的概率。证明如下:

要评估上述表达式,我们使用了一个小技巧。已知 p 是一个连续参数,其值在 0 和 1 之间(因为它是概率的度量)。如果我们可以将其建模为一个常数公比 r 的几何级数,那么求和一个无限级数就会变得非常简单。然后,

我们需要找到一种方法,将给定的无限级数和转换为可以使用上述表达式轻松评估的几何级数。为此,我们回顾一下 x^i 的导数是 i*x^(i−1),这与上述形式非常相似,只需将 x 替换为 1 — p。因此,我们得到:

我们可以将导数移出求和,因为导数的和等于和的导数。因此,

我们现在可以使用无限几何级数和来得到:

上述结果在大多数概率教材中是标准的。对于本文而言,理解如何使用上述结果比理解结果的推导过程更为重要。因此,我们已经证明几何随机变量的期望值是其参数 p 的倒数。现在,我们继续找出几何随机变量的方差。数学稍微复杂,如果你只是对解决给定问题感兴趣,可以选择跳过这一部分。

计算几何分布的方差:我们使用以下方差公式:

我们现在继续计算 E(X(X − 1)):

就像之前一样,我们观察到 x^i 对 x 的二阶导数是 i(i − 1)x^(i−2) 因此:

因此,

因此,我们已经得到了几何分布的方差和期望。

回到问题:建模

要解决当前问题,我们首先需要对其进行建模,这包括识别其参数、假设和相关变量。

  1. 参数:问题给出了一个参数,即 n,总独特优惠券的数量。

  2. 假设:我们假设每个优惠券在给定盒子中出现的概率相等。另一个微妙的假设是我们有无限的盒子可以打开,即我们没有打开盒子的数量限制。

  3. 变量:为了找到问题的解决方案,我们需要确定在获得每种类型的至少一个优惠券之前必须打开的盒子数量。这要求我们定义适当的变量,以封装解决方案所需的元素。这些变量的性质和属性取决于问题所源自的领域。由于优惠券收集者问题根植于统计学和概率论领域,因此这些变量的性质将是随机的。因此,我们定义随机变量 T,表示收集所有 n 种独特优惠券所需打开的最小盒子数量。我们使用随机变量来解决这个问题,因为 T 的值不一定是确定的。实际上,T 的值可以是从 n 到无限大的任何值。我们能回答的唯一问题是:

    • T 的期望(或平均)值是多少?

    • T 的方差(或值的平均分布)是多少?

    • 打开超过 c 个盒子的概率是多少?换句话说,T > c 的概率是多少?

在对问题的各个元素进行建模之后,我们对其设置、范围和期望有了清晰的了解。现在我们继续解决方案的工作。

提出解决方案

我们首先分析 T 的分布。然而,很快会发现 T 的分布相当复杂,不能归入任何已知的概率分布。这是因为即使假设每个优惠券在给定盒子中出现的概率相等,但随着我们收集更多优惠券,找到新优惠券的概率会逐渐减少。考虑一个场景,我们需要收集 100 个独特的优惠券。当我们打开第一个盒子时,由于我们还没有任何优惠券,因此可以保证找到一个新优惠券,即:

然而,当我们打开第二个盒子时,找到新优惠券的概率会略微降低,因为有(1/100)的机会我们可能会找到在第一个盒子中已经找到的相同优惠券。

随着我们继续打开更多盒子,找到新优惠券的概率持续减少(不是严格减少),因为剩下的独特优惠券越来越少。因此,不恒定的概率值使得将 T 分配到一个共同的概率分布变得相当困难。因此,我们采用了分治法,这是一种非常流行的解决概率问题的策略。这涉及将问题分解为复杂性较小的更小部分,并独立处理每个部分。由于 T 测量了收集 n 个独特优惠券所需的时间或盒子数量,我们通过提出 Tᵢ来测量收集第 i 个优惠券所需的时间或盒子数量,从而将其分解。我们可以将 T 表示为所有 Ti 的总和:

这有什么好处?好吧,Tᵢ的分布(与 T 的分布不同)是由一个常数概率值参数化的。让我们理解这意味着什么。

我们可以将每次开盒子视为一系列伯努利试验中的一次试验,其中成功意味着找到一个新的、以前未见过的优惠券。(回忆一下,伯努利试验是一种随机实验,只有两种可能的结果:成功和失败。例如,抛硬币就是一个伯努利试验,其中成功可能定义为正面,失败则定义为反面。)在开始时,我们还没有收集任何优惠券,因此第一次试验的成功概率是 n/n,即 1.0:

在第二次尝试时,还剩下 n-1 个独特优惠券要收集,总共有 n 个优惠券,因此成功的概率是(n-1)/n。这是因为只有一个优惠券会导致失败(即,重复了第一次尝试中收集到的优惠券),而 n-1 个优惠券会导致成功(即,一个还未被收集的新优惠券)。

在第三次尝试时,还剩下 n-2 个独特优惠券要收集,总共有 n 个优惠券,因此成功的概率是(n-2)/n。

同样地,在第四次尝试时,成功的概率是(n-3)/n,依此类推。我们可以推导出公式,以找出收集第 i 个独特优惠券的概率(即,已经收集了 i-1 个独特优惠券的情况下,打开一个盒子找到一个新优惠券的概率):

回忆一下,Tᵢ测量了收集第 i 个独特优惠券所需的独立试验次数。这听起来很熟悉;是的,它就是几何随机变量!这些试验中的每一个都对应于一个伯努利试验,其中成功意味着在收集了 i-1 个独特优惠券的情况下,找到一个新的、以前未见过的优惠券。因此,

因此,我们可以将 T 视为非同质几何分布的总和:

我们现在可以继续回答之前提出的问题。

T 的期望值是多少?

为了找出 T 的期望值,我们使用随机变量之和的期望值等于其期望值之和的性质:

由于 Tᵢ是一个几何随机变量:

因此,

其中 H(n)指的是第 n 个调和数:

对于大值的 n,它可以渐近地被近似为:

其中 γ ≈ 0.577215665 是欧拉-马歇罗尼常数。我们可以把它看作是一个估计值,即在多次重复收集优惠券的过程中,收集所有 n 张优惠券所需打开的盒子的平均数量。例如,假设你想从一组促销谷物盒中收集所有 100 种独特的优惠券,并且你计划每次买一个盒子,直到你收集齐全。公式 n * H(n) 将估计为完成收集所需购买的平均盒子数量,假设每个盒子有同等的机会包含任意一种 100 种优惠券。以下 python 代码可以帮助我们计算这个值:

import math

def coupon_collectors(n):
    res = 0
    for i in range(1, n + 1):
        res += 1/i
    return res*n

def coupon_collectors_approx(n):
    return n*math.log(n) + 0.5 + n*0.577215665

print(coupon_collectors(100))
# 518.737751763962
print(coupon_collectors_approx(100))
# 518.7385850988092

当然,实际上,为了收集所有 100 种优惠券,你所需购买的实际盒子数量会因试验而异,具体取决于每个盒子中获取的特定优惠券。然而,公式 n * H(n) 给了我们一个对平均情况的预期,这可以成为规划和预算的有用工具。

对于较大的 n 值,该公式预测需要打开更多的盒子以收集所有 n 种独特的优惠券。这是因为找到新优惠券的概率随着已收集优惠券数量的增加而下降。调和数 H(n) 随着 n 对数增长,因此预期的盒子数量大致与 n ln n 成正比。这意味着收集大量独特的优惠券可能是一项具有挑战性且耗时的任务,这与我们的直觉相符。

E(T) 关于 n 的线性图 [作者图片]

T 的方差是多少?

接下来,我们尝试计算 T 的方差,以便了解收集所需的盒子数量在不同试验中的变化情况。由于所有试验都是独立的(假设每种优惠券在特定盒子中出现的概率相等),随机变量 Tᵢ, Tⱼ; i != j 也是独立的。因此,其和的方差是其方差之和。因此,

由于 Tᵢ 是一个几何随机变量:

因此,

在这里我们使用了欧拉对巴塞尔问题的方法:

例如,假设你想收集所有 100 种独特的优惠券,并且你多次重复这个过程。方差将给你一个估计,说明所需的盒子数量在不同试验中变化的程度。如果方差很小,则可以期待每次试验所需的盒子数量相似。如果方差很大,则所需的盒子数量可能在不同试验中变化很大。

def coupon_var(n):
    return n**2 * (math.pi**2)/6

print(coupon_var(100))
# 16449.340668482262

方差与 n² 成正比,因此对于较大的 n 值,方差增长的速度远快于所需的期望盒子数量。这意味着随着 n 的增大,收集所有 n 个优惠券所需的盒子数量变得越来越不可预测,估计你需要购买多少盒子会变得越来越困难。

Var(T) 与 n 的线图:E(T) 和 Var(T) 的增长订单比较 [作者图片]

打开超过 c 个盒子的概率是多少?

最后,我们尝试计算(或至少界定)需要打开的盒子数量超过(或等于)c 的概率,即 T 取值大于或等于 c 的概率。由于 T 的分布相当复杂,找到这种概率的确切值非常困难。然而,统计学为我们提供了许多有用的不等式,帮助我们界定概率的值。具体来说,我们将使用马尔可夫不等式来上界 T 取值大于或等于 c 的概率。

马尔可夫不等式是概率理论中的一种强大工具,它允许我们对随机变量超出某一值的概率做出一般性陈述。这个不等式以俄罗斯数学家安德烈·马尔可夫的名字命名,他在 19 世纪末首次引入了它。马尔可夫不等式表示,对于任何非负随机变量 X 和任何正数 a,X 大于或等于 a 的概率小于或等于 X 的期望值除以 a。在数学符号中,这可以写作:

直观上,马尔可夫不等式表示,如果我们想知道随机变量取值大于或等于 a 的可能性,我们可以通过将随机变量的期望值除以 a 来界定这个概率。这个界限通常比较松,但在我们对随机变量的分布信息有限的情况下,它可能会很有用。

由于 T 是非负的(盒子数量不能为负),我们可以使用马尔可夫不等式:

上述近似对于当 n 的值非常大时估计概率可能会很有用。例如,假设我们想估计需要打开超过 1000 个盒子才能收集到所有 100 个独特的优惠券的概率。我们可以利用不等式来获得这个概率的上界,如下:

def coupon_collectors_approx(n):
    return n*math.log(n) + 0.5 + n*0.577215665

def coupon_prob(n, c):
    ev = coupon_collectors_approx(n)
    return ev/c

print(coupon_prob(100, 1000))
# 0.5187385850988091

因此,如果我们知道 n 和 c 的值,我们可以利用这个界限来估计我们需要打开超过 c 个盒子才能收集到所有 n 个独特的优惠券的概率。

结论

总之,优惠券收集者问题是概率理论中的经典问题,具有广泛的实际应用。这个问题涉及收集一组 N 个独特的优惠券,其中每个优惠券在给定盒子中被找到的概率是相等的。我们讨论了这个问题的各种方面,包括收集所有 n 个独特优惠券所需时间的期望值和方差,以及收集所有 n 个独特优惠券所需的盒子数量超过给定值的概率。

优惠券收集者问题是一个引人入胜的问题,具有许多有趣的特性和应用。它是理解概率和统计的重要工具,其解决方案在许多实际应用中具有广泛的用途,从设计调查和收集数据到分析客户行为和预测市场趋势。通过理解优惠券收集者问题,我们可以获得对复杂系统行为的宝贵洞察,并在各种情境中做出更明智的决策。

感谢阅读!希望你喜欢这篇文章!

勇敢学习机器学习:深入探讨 F1 分数、召回率、精确度和 ROC 曲线

原文:towardsdatascience.com/courage-to-learn-ml-a-deeper-dive-into-f1-recall-precision-and-roc-curves-d5c0a46e5eb7

F1 分数:你对不平衡数据的关键指标——但你真的知道为什么吗?

Amy MaTowards Data Science Amy Ma

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

--

我们将使用排序衣物的类比来说明召回率和精确度的核心概念;照片由Ace Maxwell提供,来源于Unsplash

欢迎回到我们‘勇敢学习机器学习’系列的旅程中。在本节中,我们将探索度量指标的细微世界。许多资源介绍了这些指标或深入探讨其数学方面,但这些‘简单’数学背后的逻辑有时可能仍然不透明。对于那些刚接触这个话题的人,我建议查看 Shervin 的详细文章以及neptune.ai提供的全面指南。

在典型的数据科学面试准备中,处理不平衡数据时,通常首选的指标是 F1 分数,它被称为召回率和精确度的调和均值。然而,为什么 F1 分数特别适用于这种情况的理由往往没有解释。本文致力于揭示这些理由,帮助你理解在不同情境下选择特定指标的原因。

正如往常一样,本篇文章将概述我们要解决的所有问题。如果你一直在思考这些问题,你来对地方了:

  • 精确度和召回率到底是什么,我们如何直观地理解它们?

  • 为什么精确度和召回率很重要,它们为什么经常看起来相互冲突?是否可以同时达到这两者的高水平?

  • 什么是 F1 分数,我们为什么将其计算为召回率和精确度的调和平均数?

  • 为什么 F1 分数常用于不平衡数据?它只在这些情况下有用吗?

  • 当正类是多数时,F1 分数的解释会有什么变化?

  • PR 曲线和 ROC 曲线有什么区别,我们应该什么时候优先使用其中之一?

在对这些指标有了基本了解后,我们的学习者向正在忙于洗衣的导师提出了第一个问题:

我正在开发一个游戏推荐系统。它旨在根据用户的偏好和生活方式推荐视频游戏。但我发现它主要推荐流行游戏,比如今年的 TGA 游戏《博德之门》,用户错过了他们寻找的冷门和经典游戏。我该如何解决这个问题?我应该更改算法还是使用 LLM,考虑到它的强大功能?

Nick Hamze 的照片,来自 Unsplash

让我们不要急于得出你需要最先进算法的结论。相反,让我们探讨一下为什么你的模型没有按预期表现。看起来你的模型在 Precision@k 上得分很高,但 Recall@k 很低。

为了更好地理解这些指标,让我们逐一解析:

  • Precision@k = (相关的前 k 个推荐数量)/(推荐的项目总数)。简单来说,它衡量了你模型推荐的游戏中实际对用户相关的数量。

  • Recall@k = (相关的前 k 个推荐数量)/(所有相关项目的总数)。这告诉我们相关游戏中有多少实际出现在你的前 k 个推荐中。

从中可以看出,用户经常能在你的推荐中找到相关游戏,但并不是所有相关游戏都能出现在你的前 k 个列表中。需要注意的是,推荐的项目是你模型预测为相关的项目,这可以被视为‘预测为相关的项目数量’。

等等,你是建议我使用召回率和精确度来评估我的模型吗?但召回率和精确度主要用于不平衡数据,不是吗?它们的调和平均数 F1 分数也是如此。

你已经掌握了精确度和召回率的一个重要方面,并理解了为什么准确率并不总是可靠。然而,你对召回率和精确度的看法似乎有些局限,不应将它们仅限于处理不平衡数据的场景。让我们把这个问题分解成更小的部分,从以下开始:

精确度和召回率是什么?

精确度衡量了模型的正预测的准确性,计算公式为:

Precision = 正确预测为正类的样本数量 / 预测为正类的样本总数 = 真正例 / (真正例 + 假正例)

另一方面,召回率评估模型识别所有正例的能力,计算方法如下:

召回率 = 正确预测为正的样本数量 / 实际正样本总数 = 真正例 / (真正例 + 假负例)

记住 TP 和 TN 的一个快速提示:第一个字母(True/False)表示你的预测是否正确,而第二个字母(Positive/Negative)则指的是 预测的 标签。因此,真正例(true positive)意味着‘正确预测为正’,而假负例(false negative)则是‘错误预测为负……实际上是正的!’

预测为正的总数量是 真正例 (TP) 和假 正例 (FP) 的总和。

来源: upload.wikimedia.org/wikipedia/commons/thumb/2/26/Precisionrecall.svg/800px-Precisionrecall.svg.png

让我提供一个直观的例子来理解这两个术语:假设我在整理衣物,我的目标是从堆积的衣物中挑选出所有脏衣物进行洗涤。在这种情况下,精确度指的是我能多准确地识别出一件衣物是否脏。同时,召回率则衡量我能正确识别出多少实际的脏衣物。

那么,我们的下一个问题是:

为什么我们关心精确度和召回率,它们为何经常看起来互相冲突?是否可能同时拥有高精确度和高召回率?

精确度和召回率的重要性在于它们的互补性质。 再次使用衣物分类的类比。我的目标有两个:首先,确保所有脏衣物都被挑选出来,其次,避免不必要的洗涤。将其与指标联系起来,精确度就像是我正确识别脏衣物并通过不洗干净的衣物来节省精力。而召回率则评估我成功收集所有脏衣物的能力。

为了澄清,我们来看看两个极端的情况:

  • 如果只关注高精确度,我会非常挑剔,只选择明显有污渍的衣物进行洗涤。这意味着可能会忽略一些不太明显的污垢,比如只有一点猫毛的衬衫。因此,我只会洗涤一小部分衣物,留下些脏衣物(因此召回率低)。

  • 如果我优先考虑高召回率,我会在没有分类的情况下洗涤所有衣物。这确保了所有脏衣物都被清洗,但也意味着清洁的衣物也会被洗涤(导致精确度低)。

无论你处理什么类型的衣物,你可以看到,选择的指标确实会影响我们的(模型的)行为。召回率和精确度衡量了不同的方面,同时优化这两个指标是具有挑战性的。这就是为什么在分类中,我们谈论它们之间的权衡。它们共同作用,以确保我们的模型准确预测,同时捕捉到所有正例。

接下来,让我们深入探讨:

什么是 F1 分数,为什么它被计算为召回率和精确率的调和平均数?

大多数数据科学面试指南建议通过使用 F1 分数来处理不平衡数据,F1 分数是召回率和精确率的调和平均数。但通常,它们没有解释为什么 F1 分数在这些情况下有效。

那么,为什么使用 F1 分数?在模型评估中,我们关注的是平衡精确率和召回率——我们希望正确的预测和全面的覆盖。单独监控这两个指标可能很繁琐,因此更喜欢一个反映这种平衡的单一指标。简单平均并没有揭示平衡的很多信息;高分数仍可能掩盖不平衡。然而,F1 分数中使用的调和平均数对极端值的惩罚更为严重。如果召回率或精确率较低,它将显著降低 F1 分数。

考虑两个假设的案例以理解为什么我们使用调和平均数而不是简单平均数:

  • 场景 A: 精确率 = 0.9,召回率 = 0.1

  • 场景 B: 精确率 = 0.5,召回率 = 0.5(更平衡的情况)

简单平均计算:

  • 场景 A: (0.9 + 0.1) / 2 = 0.5

  • 场景 B: (0.5 + 0.5) / 2 = 0.5

调和平均计算(F1 分数):

  • 场景 A: 2 * (0.9 * 0.1) / (0.9 + 0.1) = 0.18

  • 场景 B: 2 * (0.5 * 0.5) / (0.5 + 0.5) = 0.5

尽管这两个场景的平均值相同,但简单平均掩盖了场景 A 的较差召回率。另一方面,调和平均数提供了对精确率和召回率之间平衡的更准确反映。 更高的 F1 分数表明更好的平衡。

那么……为什么 F1 分数常用于不平衡数据,它的使用是否仅限于这些场景?

让我们探讨不平衡数据的挑战,这在二分类问题中很常见。在这里,一个类别通常样本数量远少于另一个类别,并且代表了稀有但重要的案例(如客户流失或癌症诊断)。这些稀有案例通常有更高的后果,准确识别它们至关重要。我们需要一个不仅能做出准确预测的模型,还能有效识别这些稀有案例。这一要求使我们需要在精确率和召回率之间寻求平衡,而 F1 分数成为一个便捷的工具。 它提供了一个反映这种平衡的单一数字,使其在不平衡数据集中成为首选指标。F1 分数的价值在于它能准确描绘模型在识别少数类别方面的效果。

然而,F1 分数的有用性并不限于不平衡数据集。它在任何需要平衡精确率和召回率的地方也很相关,即使是在平衡的数据集中。 F1 分数仍然是平衡精确率和召回率的重要指标,并且简化了模型比较。

除了 F1 分数之外,其他指标在评估不平衡数据的模型表现时也很有用。

我听说过 F1、精确度和召回率是非对称的指标,这意味着它们取决于哪个类别被标记为正类。当正类实际上是多数类时,F1 分数的解释会发生什么变化?

好问题。为了回答这个问题,我们可以考虑一下,如果将多数类视为正类,召回率和精确度会如何变化。由于大多数样本将被预测为正类,获得高召回率变得更容易。

但这里有一个问题:高精确度在这种情况下可能具有误导性。由于多数类较大,仅仅通过始终预测多数类就容易获得高精确度。将多数类转为正类后,我们忽视了模型处理稀有类的能力,特别是在不平衡数据的情况下。因此,精确度和召回率之间的平衡不再保证模型的有效性,因为其重点已发生转移。 这意味着即使模型在识别少数类方面表现不佳,它仍可能显示出高 F1 分数。

当正类占据多数时,高 F1 分数可能无法真实反映模型识别少数类的能力。这可能仅仅意味着模型经常预测多数类。

在这种情况下,明智的做法是包括其他不那么偏向多数类的指标,比如负类(少数类)的召回率,以便全面了解模型的性能。

F1 分数有哪些局限性?我们还可以使用哪些其他指标来评估不平衡数据上的模型性能?

你知道,我们经常以类似回归的方式来处理分类问题。我的意思是,一些算法预测的是概率,而不仅仅是类别。对于这些算法,你需要设置一个阈值。但 F1 分数并不能真正展示模型在不同阈值下的表现。 这就是 ROC 曲线或精确度-召回率曲线发挥作用的地方,它们帮助我们评估在各种阈值下的表现。

我们进行分类的方式往往是类似回归的。我的意思是,有些算法会预测概率而不是类别。对于这些模型,我们需要选择一个阈值。然而,F1 分数无法提供与不同阈值相关的模型性能的说明。 因此,我们可以使用 ROC 曲线或精确度-召回率曲线来考虑模型在不同阈值下的表现。此外,曲线下面积(AUC)指标作为一个单一的数字总结,方便在多个模型之间进行比较。AUC 的范围从 1(表示完美的分类器)到 0(表示最差的分类器)。值得注意的是,AUC 为 0.5 表示性能等同于随机猜测,此时在所有阈值下真正正类率(TPR)和假正类率(FPR)是相等的。

一个有趣的问题是

AUC(曲线下面积)值为 0.5 如何等同于分类器进行随机猜测?

当 AUC 等于 0.5 时,ROC 曲线由连接图中点 (0,0) 和 (1,1) 的对角线表示。这条对角线意味着对于这个分类器,我们在所有阈值下 TPR = FPR。简单来说,正确识别一个正例的概率与错误地将其标记为负例的概率完全相同。因此,对于任何给定的正例,分类器正确预测的概率是 50%,类似于掷硬币的随机性。这种与随机性的等同凸显了为什么 AUC 为 0.5 被认为表示模型的表现不比随机猜测好。

当面对不平衡的数据时,我们可以使用精确率-召回率曲线来观察模型在不同阈值下平衡精确率和召回率的表现。

总结一下, 对于某些场景,我们希望模型能够很好地平衡召回率和精确率。这就是为什么我们使用 F1 分数作为一个单一的度量标准,F1 分数是召回率和精确率的调和均值。但是,在数据不平衡的情况下,特别是在我们更关注少数类别的表现时,F1 分数变得特别重要,因为实现平衡更具挑战性。其他有用的工具包括 ROC 曲线和 PR(精确率-召回率)曲线。

因此,对于你的游戏推荐系统,考虑使用多个指标。这样,你可以更好地评估模型如何检索相关项目(召回率)以及确保这些项目确实相关(精确率)。你可以一起评估 precision@k 和 recall@k,计算 f1@k,或者绘制 PR 曲线。

在实践中,选择模型指标时必须根据实际错误的成本, 例如,召回率是否比精确率对你更重要。使用多个指标可以更全面地了解模型的表现。记住,关键是将你的指标与模型的业务或应用目标对齐。

在结束这篇文章之前,还有一个话题我想提一下:

PR 曲线和 ROC 曲线之间有什么区别?在什么情况下你应该选择其中一个而不是另一个?

大多数数据科学面试指南建议在数据不平衡时使用 PR 曲线而不是 ROC 曲线,但通常不会解释何时选择 ROC 曲线。虽然我不会在这里详细说明如何绘制这些曲线(有关这一点,请查看 StatQuest with Josh Starmer 的出色解释 这里),但我们要了解这些曲线是通过改变阈值并计算两个指标(PR 的精确率和召回率,或 ROC 的 TPR 和 FPR)绘制的。这两条曲线在二分类中代表了不同的平衡:

ROC 曲线与 PR 曲线的样本计算。来源:modtools.files.wordpress.com/2020/01/roc_pr-1.png?w=946

ROC 曲线侧重于 TPR 和 FPR;PR 曲线则侧重于精确率和召回率:

  • TPR(召回率)= 正确预测为正类的样本数量 / 总实际正样本数量。

  • FPR = 错误分类为正类的样本数量 / 总实际负样本数量。

虽然精度和召回率仅关注模型在正类上的表现,TPR 和 FPR 提供了预测能力的更广泛视角(正确的正类 vs. 错误分类的样本)。

ROC 曲线对数据分布的敏感性较低,因为 FPR 使用负类的大小。 如果负类是多数类,即使有很多负预测,FPR 值也可能保持较低,因为负类的规模更大。这意味着 ROC 曲线对数据不平衡的影响较小。另一方面,PR 曲线使用预测的正类来计算精度,对正类更为敏感。

这意味着什么?这意味着在比较不同数据集上的模型性能时,ROC 曲线比 PR 曲线提供了更稳定的指标,并能更好地反映模型的性能。 因此,与其单纯记住 PR 曲线适用于不平衡数据,重要的是认识到 ROC 曲线提供了一致的度量,受数据分布影响较小。

在我们即将进行的会议中,导师与学习者的组合将深入探讨常见的损失函数,通过信息理论和 MLE 的视角探讨交叉熵。如果你喜欢这个系列,请记住你的互动——点赞、评论和关注——不仅仅是支持,它们是推动这个系列不断前进的动力,并激励我持续分享。

本系列的其他文章:

  • 勇敢学习机器学习:揭开 L1 和 L2 正则化的神秘面纱(第一部分)

  • 勇敢学习机器学习:揭开 L1 和 L2 正则化的神秘面纱(第二部分)

  • 勇敢学习机器学习:揭开 L1 和 L2 正则化的神秘面纱(第三部分)

  • 勇敢学习机器学习:揭开 L1 和 L2 正则化的神秘面纱(第四部分)

  • 勇敢学习机器学习:解码似然性、MLE 和 MAP

如果你喜欢这篇文章,你可以在LinkedIn找到我,请随时与我联系,提出你的问题和建议!

勇敢学习机器学习:最常见损失函数的深入指南

原文:towardsdatascience.com/courage-to-learn-ml-an-in-depth-guide-to-the-most-common-loss-functions-84a6b07cca17

MSE、对数损失、交叉熵、RMSE 以及流行损失函数的基础原理

Amy MaTowards Data Science Amy Ma

·发表于Towards Data Science ·13 分钟阅读·2023 年 12 月 28 日

--

William Warby拍摄,Unsplash

欢迎回来!在‘勇敢学习机器学习’系列中,我们一步一步征服机器学习的恐惧。今天,我们将深入探讨损失函数的世界:这些默默无闻的超级英雄引导我们的模型从错误中学习。在这篇文章中,我们将涵盖以下主题:

  • 什么是损失函数?

  • 损失函数与指标之间的区别

  • 从两个角度解释 MSE 和 MAE

  • 设计损失函数时的三个基本概念

  • 运用这三个基本概念来解释 MSE、对数损失和交叉熵损失

  • 对数损失与交叉熵损失之间的关系

  • 如何在实践中处理多个损失函数(目标)

  • MSE 和 RMSE 的区别

什么是损失函数,它们在机器学习模型中为什么重要?

损失函数在评估模型在学习过程中的有效性方面至关重要,类似于考试或标准。它们作为模型预测与真实标签(即‘正确’答案)之间偏差的指标。通常,损失函数通过衡量模型预测和实际标签之间的差异来评估性能。这种差距的评估向模型提供了关于其参数(如权重或系数)需要调整的程度,以便更准确地捕捉数据中的潜在模式。

机器学习中存在不同的损失函数。这些因素包括预测任务的性质,如回归或分类,目标变量的分布,例如使用 Focal Loss 处理不平衡数据集,以及算法的具体学习方法,如在 SVM 中应用铰链损失。理解和选择合适的损失函数非常重要,因为它直接影响模型如何从数据中学习。

学习机器学习时,应了解最流行的损失函数。例如,均方误差(MSE)和平均绝对误差(MAE)在回归问题中常用,而交叉熵是分类任务中最常用的损失函数。

损失函数和度量指标有何不同,损失函数在何种情况下也可以作为度量指标?

关于损失函数也可以作为度量指标的说法可能会误导。损失函数和度量指标都用于评估模型性能,但在不同的阶段和目的下:

  • 损失函数:这些函数在模型学习过程中使用,以指导模型的调整。它们需要是可微分的,以便进行优化。例如,均方误差(MSE)和平均绝对误差(MAE)是回归模型中常见的损失函数。

  • 度量指标:这些评估模型训练后的表现。度量指标应该是可解释的,并提供关于模型有效性的清晰见解。虽然一些度量指标,如准确率,可能比较直接,但其他如 F1 得分则涉及阈值决策且不可微分,使其不太适合作为学习指导。

值得注意的是,一些度量指标,如 MSE 和 MAE,由于其可微分性和可解释性,可以既作为损失函数也作为度量指标。然而,并非所有的度量指标都适合作为损失函数,主要是因为损失函数需要可微分性以用于优化。

实际中,应始终仔细选择损失函数和度量指标一起用于学习,并确保学习和评估朝着相同方向对齐。这种对齐确保了模型的优化和评估基于相同标准,这些标准反映了应用的最终目标。

作者注: 需要明确的是,使用 F1 分数作为机器学习模型的损失函数并非完全不可行。在我持续的研究中,我遇到了一些创新方法,解决了通常与 F1 分数相关的不可微分性问题。例如,Ashref Maiza 的 帖子 介绍了一种 F1 分数的可微分近似方法。这种方法通过使用似然概念“软化”精确度和召回率,而不是设置任意阈值。此外,一些在线讨论,如 这个讨论,也探讨了类似的主题。

挑战在于 F1 分数的固有特性。尽管它是一个高度信息量的指标,但选择合适的损失函数来在相同标准下有效优化模型可能会很复杂。此外,调整阈值增加了另一层复杂性。我对这个话题非常感兴趣。如果你有见解或经验可以分享,请随时与我联系。我渴望扩展我的理解并参与进一步的讨论。

你提到 MSE 和 MAE 是回归问题中的典型指标。它们是什么以及何时使用它们?

在回归问题中,预测值是连续值,目标是最小化模型预测值与实际值之间的差异。为了评估模型在把握潜在模式方面的有效性,我们使用均方误差 (MSE) 和平均绝对误差 (MAE) 等指标。这两个指标都量化了预测值和实际值之间的差距,但它们使用不同的评估方法。

MSE 的定义为:

这里,y_i 是实际值,y_hat_i 是预测值,n 是观察值的数量。

MSE 计算预测值和实际值之间平方差的平均值,这相当于预测值与真实标签之间的欧几里得距离(l2 范数)。

另一方面,MAE 的定义为:

在这里,实际值和预测值之间的绝对差异被平均,这对应于曼哈顿距离(l1 范数)。换句话说,MAE 计算估计值与实际值之间的平均距离,不考虑方向(正或负)。

我们在讨论 l1 和 l2 正则化时谈到了 Lp 范数和不同的距离 medium.com/p/1bb171e43b35

MSE 和 MAE 的主要区别在于它们对异常值的响应。MSE 通过平方错误放大并赋予较大错误更多权重,使其对异常值敏感。如果较大错误在你的问题上下文中更为重要,这很有用。 然而,MAE 对所有错误赋予相等的权重,使其对异常值和非正态误差分布更为鲁棒。

选择 MSE 还是 MAE 应基于训练数据的特性和模型中较大错误的影响。当我们希望对较大错误进行重罚时,MSE 更为合适,而当我们希望平等对待所有错误时,MAE 则更为适用。

我明白 MSE 中平方差异会放大错误,从而对异常值给予更多关注。还有其他观点或方面可以帮助区分这两种指标吗?

当然,还有另一种视角可以理解 MSE(均方误差)和 MAE(平均绝对误差)之间的差异,超越它们对异常值的处理。假设你需要预测一个值‘y’而没有任何额外的特征(没有‘X’)。在这种情况下,最简单的模型将对所有输入预测一个常数值。

使用 MSE 作为损失函数时,最小化 MSE 的常数是目标值的均值。这是因为均值是使所有点的平方差之和最小的中心点。另一方面,如果使用 MAE,目标值的中位数就是最小化常数。与均值不同,中位数对极端值或异常值的影响较小。

在道格拉斯·亚当斯的《银河系漫游指南》中,42 是生命、宇宙和一切的终极答案。谁知道呢,也许 42 还是缩小你的损失函数的魔法数字——但这完全取决于你的损失函数是什么!图像由 ChatGPT 创建

这种对异常值敏感度的差异源于均值和中位数的计算方式。均值考虑了每个值的大小,使其更容易受到异常值的影响。而中位数只关心值的顺序,因此无论数据集的极端值如何,都能保持其位置。中位数的这种内在特性使得 MAE 对异常值具有更强的鲁棒性,提供了 MSE 和 MAE 在建模环境下不同表现的另一种解释。

你可以在 Shubham Dhingra 的文章中找到关于均值如何最小化 MSE 以及中位数如何最小化 MAE 的解释。

我们讨论了 MSE 和 MAE 如何测量误差,但事情远不止于此。不同的任务需要不同的方式来衡量我们的模型表现如何。这就是损失函数的作用所在,背后有三个基本思想。理解这些思想将帮助你为任何工作选择合适的损失函数。那么,让我们从最重要的问题开始:

设计任何损失函数的三个基本思想是什么?

在设计损失函数时,通常有三个基本思想指导这一过程:

  1. 最小化残差:关键是减少残差,即预测值与实际值之间的差异。为了处理负误差和正误差,我们通常会对这些残差进行平方,如最小二乘法中所见。这种方法,通过求和平方残差,在回归问题中因其简单性和有效性而成为主流。

  2. 最大化似然(MLE):这里的目标是调整模型参数以最大化观察到的数据的似然,使模型尽可能代表潜在过程。这种概率方法在逻辑回归和神经网络等模型中至关重要,在这些模型中,将模型拟合到数据分布上是关键。

  3. 区分信号与噪声:这一原则源于信息论,涉及将有价值的数据(信号)与无关的数据(噪声)分开。基于这一思想的方法,专注于熵和不纯度,在分类任务中至关重要,并构成了决策树等算法的基础。

此外,重要的是要认识到某些损失函数是针对特定算法量身定制的,例如 SVM 的铰链损失,这表明算法的性质也在损失函数设计中发挥作用。此外,数据的性质会影响损失函数的选择。例如,在训练数据不平衡的情况下,我们可能会将损失函数调整为类平衡损失或选择焦点损失。

现在,掌握了这些基本概念后,让我们应用它们进行解释性分析,以增强我们的理解。通过这种方法,我们可以尝试解决以下问题:

我们如何应用最大似然估计(MLE)和最小二乘法来增强对 MSE 的理解?

首先,让我们用最小二乘法来分解均方误差(MSE)。最小二乘估计(LSE)方法通过最小化残差的平方和来找到最佳的模型拟合。在线性回归(处理连续输出)中,残差是预测值与实际标签之间的差异。MSE,即均方误差,本质上是这些平方差异的平均值。因此,最小二乘法旨在最小化 MSE(考虑到这个平均步骤),使得 MSE 成为该方法的合适损失函数。

接下来,从最大似然估计(MLE)的角度看,在假设线性回归的情况下,我们通常假设残差服从正态分布。这允许我们将观察数据的似然性建模为单个概率密度函数(PDF)的乘积。为了简化,我们对这种似然性取自然对数,将其转化为单个 PDF 对数的和。需要注意的是,我们使用密度函数用于连续变量,而不是概率质量函数用于离散变量。

注意:似然计算在离散变量和连续变量之间有所不同。离散变量使用概率质量函数,而连续变量使用概率密度函数。有关 MLE 的更多信息,请参阅我的 上一篇文章

当我们检查对数似然时,它包含两个部分:一个常数成分和一个变量成分,后者计算真实标签与预测之间的平方差。为了最大化这种对数似然,我们专注于最小化变量成分,这本质上是平方残差的总和。在实际线性回归的背景下,这种最小化等同于最小化 MSE,尤其是当我们考虑到由正态分布假设引起的缩放因子 1/2σ² 时。

总之,MSE 可以从最小二乘估计(LSE)和 MLE 两种角度推导和理解,每种方法提供了一个独特的视角来探讨 MSE 在回归分析中的重要性和应用。

所以 MSE 是回归问题中常见的损失函数。但我可以将它用于分类问题吗?比如逻辑回归?

MSE 虽然在回归中很常见,但对于分类任务,如逻辑回归,并不理想。主要原因是输出的性质不匹配:逻辑回归预测的是概率,而 MSE 假设的是连续数值。这种不一致导致了理论和实际应用中的挑战。

实际上,MSE 在与逻辑回归结合时会产生非凸的损失面,逻辑回归通常使用 sigmoid 激活函数。这种非凸性意味着误差面有多个局部最小值,使得像梯度下降这样的优化算法难以找到全局最小值。基本上,算法可能会在局部最小值处“卡住”,导致模型性能次优。

此外,将 MSE 与 sigmoid 函数结合可能导致梯度变得非常小,特别是对于极端输入值。这会导致“梯度消失”问题,使得模型停止学习或学习非常缓慢,因为模型参数的更新变得微不足道。

因此,对于分类问题,尤其是像逻辑回归这样的二分类问题,MSE 并不是一个理想的损失函数。

那么,什么是适用于逻辑回归或更一般分类问题的良好损失函数呢?

好的,让我们深入探讨逻辑回归的损失函数世界,看看如何应用一些基本的设计理念来更好地理解它们。

首先,让我们看看最小二乘法。这里的核心思想是最小化模型输出与真实标签之间的差距。一种简单的方法是设定一个阈值,将逻辑回归的概率输出转换为二元标签,然后将这些标签与真实标签进行比较。如果我们选择 0.5 作为分类甜甜圈和百吉饼的阈值,我们将大于 0.5 的预测标记为甜甜圈,小于 0.5 的标记为百吉饼,然后计算不匹配的数量。这种方法称为0–1 损失,它直接对应于准确度,但由于其不可导性和非凸性,未被用作训练的损失函数,使得其在梯度下降等优化方法中不够实用。它更多的是一种概念上的方法,而非实际的损失函数。

当我第一次访问美国时,我无法分辨甜甜圈和百吉饼之间的区别。一个区分甜甜圈和百吉饼的分类器可能会很有用。图像由 ChatGPT 创建

接下来,让我们使用最大似然估计(MLE)的概念。在逻辑回归中,MLE 试图找到最大化实际观察数据概率的权重和偏置。假设我们的目标是找到一组最大化对数似然的权重和偏置,其中似然 L 是观察每个结果的个体概率的乘积。我们假设我们的数据点是独立的,并且每个点都遵循伯努利分布

所以我们有了对数损失函数:

最后,让我们引入一些信息论的概念,将逻辑回归视为一个信号捕获机器。在这种方法中,我们使用熵和交叉熵等概念来评估我们模型捕获的信息。熵度量事件的不确定性或惊讶程度。交叉熵衡量我们模型的预测概率分布与实际真实分布的匹配程度。这里的目标是最小化交叉熵,这类似于最小化 KL 散度。虽然 KL 散度在严格意义上不是一种“距离”,但它表示我们的模型预测与实际标签之间的偏差程度。

Softmax 是我写作列表中的另一个主题。来源:https://towardsdatascience.com/cross-entropy-loss-function-f38c4ec8643e

因此,通过应用三种不同的设计原则,我们已经制定了各种适合逻辑回归和更广泛分类挑战的损失函数。

特别有趣的是,尽管起源于不同的角度,对数损失和交叉熵损失在二分类的背景下本质上是相同的。这种收敛发生在只有两个可能结果的情况下;在这些条件下,交叉熵轻松简化并转化为对数损失。理解这种转变对于理解这些理论概念的相互作用和实际应用至关重要:

从交叉熵损失推导对数损失。来源:https://towardsdatascience.com/cross-entropy-loss-function-f38c4ec8643e

作者注: 未来,我计划深入研究信息理论这一迷人的领域——这一主题令人惊讶地既直观又在现实应用中非常实用。在此之前,我强烈推荐 Kiprono Elijah Koech 的文章作为该主题的优秀资源。敬请关注更多内容!

在实际场景中,应该如何处理需要最小化多个损失函数的情况?

在模型中管理多个损失函数时,平衡它们可能会面临挑战,因为它们可能会冲突。一种常见的方法是创建这些损失函数的加权和,为每个损失函数分配特定的权重。然而,这会引入新的超参数(权重),需要仔细调整。调整这些权重意味着需要重新训练模型,这可能是耗时的,并且可能会影响模型的可解释性和性能。

或者,一种基于约束的方法也可以有效。例如,在 SVM 中,我们的目标是最大化间隔(减少方差)同时最小化分类误差(减少偏差)。这可以通过将间隔最大化作为约束,使用拉格朗日乘数等技术,并专注于最小化分类误差来实现。这种方法需要坚实的数学基础和深思熟虑的约束制定。

第三个选项是解耦目标,为每个目标构建单独的模型,然后将它们的结果进行组合。这种方法简化了模型开发和维护,因为每个模型可以独立监控和重新训练。它还提供了响应目标或业务目标变化的灵活性。然而,重要的是要考虑这些模型的组合结果如何与整体目标对齐。

然而,重要的是要理解 GAN 中的对抗性损失不仅仅是鉴别器和生成器损失的组合。这是因为两个网络处于响应性互动中,相互学习和适应,而不是独立优化它们的损失。

在我们结束之前,我想解决一个简单但实用的问题:

为什么我们有时更倾向于使用 RMSE(均方根误差)而不是 MSE?

MSE(均方误差)在某些情况下由于其可解释性,通常优于 MSE(均方误差)。通过取 MSE 的平方根,RMSE 将误差单位转换回数据的原始单位。这使得 RMSE 更加直观,并且可以直接与分析数据的尺度进行比较。例如,如果你在预测房价,RMSE 提供了一个与价格本身相同单位的误差指标,使得理解误差的大小更为容易。

此外,RMSE 对较大误差比 RMAE(均方绝对误差)更为敏感,因为平方根转换,强调了比 RMAE 更显著的偏差。这在较大误差更不受欢迎的情况下尤其有用。

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

如果你喜欢这个系列,记住你的互动——点赞、评论和关注——不仅仅是支持;它们是推动这个系列持续更新和激励我继续分享的动力。

本系列的其他文章:

如果你喜欢这篇文章,可以在 LinkedIn找到我。

勇敢学习机器学习:解码似然、MLE 和 MAP

原文:towardsdatascience.com/courage-to-learn-ml-decoding-likelihood-mle-and-map-65218b2c2b99

《猫粮偏好之尾》

Amy MaTowards Data Science Amy Ma

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

--

图片来源:Anastasiia RozumnaUnsplash

欢迎来到‘勇敢学习机器学习’。本系列旨在简化复杂的机器学习概念,以轻松且信息丰富的对话呈现,类似于《勇气不被讨厌》的风格,但专注于机器学习。

在本系列的这一部分,我们的导师-学习者二人组深入探讨 MLE 和 MAP 等统计概念。这一讨论将为我们提供一个新的视角,审视我们之前对 L1 和 L2 正则化的探索。为了完整理解,建议在阅读《勇敢学习机器学习:揭示 L1 和 L2 正则化》的第四部分之前阅读本篇文章。

本文旨在用问答形式解答可能遇到的基本问题。如往常一样,如果你有类似的问题,恭喜你来对地方了:

  • ‘似然’到底是什么?

  • 似然与概率的区别

  • 为什么在机器学习中,似然如此重要?

  • 什么是 MLE(最大似然估计)?

  • 什么是 MAP(最大后验估计)?

  • MLE 与最小二乘法的区别

  • MLE 与 MAP 之间的联系和区别

‘似然’到底是什么?

似然,或者更具体地说似然函数,是一个统计学概念,用于评估在不同模型参数集下观察到给定数据的概率。它被称为似然(函数),因为它是一个量化观察当前数据在不同统计模型参数值下的可能性的函数。

似然看起来类似于概率。它是概率的一种形式吗?如果不是,它与概率有何不同?

概率和似然在统计学中是根本不同的概念。概率测量的是在已知参数或分布的情况下,观察到特定结果的可能性。 在这种情况下,参数或分布是已知的,我们关注的是预测各种结果的概率。而似然则衡量一组潜在参数如何解释观察到的结果。 在这种设置下,结果已经被观察到,我们试图理解什么样的潜在参数或条件可能导致这些结果。

以一个直观的例子来说明,考虑一下我家猫 Bubble 更喜欢鸡肉而不是牛肉的情况。

我家的猫 Bubble 的照片

当我购买猫粮时,我会选择更多鸡肉味的罐头,因为我知道她更可能喜欢它们并且吃完。这是概率的应用,我利用对 Bubble 偏好的了解来预测未来的结果。然而,Bubble 的偏好不是她明确表达的。我通过观察她过去六年的饮食习惯得出了这个结论。注意到她总是吃更多的鸡肉而不是牛肉,表明她更可能喜欢鸡肉。这个推断过程是使用似然的一个例子。

重要的是要注意,在统计学中,似然是一个函数。 这个函数计算的是特定参数集最适合解释观察数据的概率。与概率不同,似然函数的值不一定总和为 1。这是因为概率涉及的是给定参数下所有可能结果的总和,必须为 1,而似然涉及的是给定观察数据下不同参数集的可能性。

似然在机器学习的背景下为什么很重要?

理解似然在机器学习背景下的应用需要我们考虑如何评估模型结果。从本质上讲,我们需要一套规则来判断不同的参数集。测量模型当前参数解释观察数据的效果主要有两种方法:

第一个方法涉及使用基于差异的方法。我们比较每个真实标签与相应的预测,并尝试找到一组最小化这些差异的模型参数。这是最小二乘法的基本思想,它侧重于误差最小化。

第二种方法是似然,特别是最大似然估计(MLE),在其中发挥作用。MLE 试图找到一组使观测数据最有可能的参数。换句话说,通过观察数据,我们选择最大化观察当前数据集似然的参数。 这种方法超越了仅仅最小化误差的层面;它考虑了概率并对参数估计中的不确定性进行建模。

在最大似然估计(MLE)中,基本假设是模型的最佳参数是那些最大化观察到的数据集似然的参数。

总结而言,虽然最小二乘法和 MLE 在方法上有所不同——一个是误差最小化的,另一个是概率性的——但这两者在机器学习工具箱中对于参数估计和模型评估都是必不可少的。我们将在未来的帖子中深入探讨这些方法,讨论它们的区别和联系。

能否提供一个直观的例子来对比这两种评估方法(MLE 与最小二乘法)?

考虑到我的猫 Bubble 对食物的偏好,假设我最初认为她喜欢鸡肉和牛肉一样多。为了使用最小二乘法测试这一点,我会通过购买相等数量的鸡肉和牛肉口味罐头来收集数据。当 Bubble 吃东西时,我会记录她每种食物的消费量。然后,最小二乘法将帮助我通过最小化我的预测(平等偏好)与实际消费模式(真实标签)之间的差异(平方误差)来调整我的初始假设(参数)。

对于 MLE 方法,我会先观察 Bubble 随时间的进食习惯,而不是从对她的偏好的假设开始。根据这些数据,我会使用 MLE 找到使观测数据最有可能的参数值(在这种情况下,鸡肉或牛肉的偏好)。例如,如果 Bubble 一直选择鸡肉而不是牛肉,MLE 方法将确定鸡肉偏好的更高概率。

所以 MLE 使用似然来选择参数。它们的数学表示是什么?

在最大似然估计(MLE)中,主要目标是确定最有可能生成观测数据的参数集(θ)。这个过程涉及定义似然函数,记作 L(θ) 或 L(θx),其中 x 表示观测数据。 似然函数计算在模型参数为 θ 的情况下,观察到给定数据 x 的概率。

MLE 的本质是找到最大化似然函数的参数值。在数学上,这就是它的表示和计算过程:

本文将通过随后的问题深入探讨这个方程。

等一下……我们将似然定义为 L(θ) = p(x|θ),表示在给定一组参数 θ 的情况下观察到数据 x 的概率。但是早些时候我们提到过似然涉及到有一组观察数据,然后为一组参数计算似然。难道它不应该是 L(θ) = p(θ|x) 吗?

在理解 MLE 时,区分似然函数和概率是至关重要的。似然函数,记作 L(θ),并不等同于概率 p(θ∣x)。

p(θ∣x) 表示在给定观察到的数据 x 的情况下,参数值 θ 的概率(这是贝叶斯推断中的核心概念),而 L(θ) 则是关于似然函数的,它评估不同参数值在解释观察到的数据时的可能性。

计算似然函数时,我们使用在给定特定参数值 θ 的情况下观察数据 x 的概率,记作 p(x∣θ)。这个概率函数用于评估不同参数设置的适用性。因此,在 MLE 中,我们有 L(θ)=p(x∣θ)。重要的是正确解释这个方程:等号在这里意味着我们使用概率 p(x∣θ) 来计算似然 L(θ);这并不意味着 L(θ) 和 p(x∣θ) 直接等价

总结来说,L(θ) 量化了参数 θ 解释数据 x 的效果,而 p(θ∣x) 则是观察到数据后的参数概率。理解这一点对于掌握 MLE 的原理及其在统计建模中的应用至关重要。

但是,使用 p(θ|x) 是否不会提供更直接的评估,以确定哪一组参数更好,而不是依赖于似然函数呢?

很高兴你注意到了这一重要区别。从理论上讲,计算不同参数集(θ)的 ( p(θ|x) ) 并选择概率最高的那个确实可以直接评估哪一组参数更好。这可以通过 贝叶斯定理 实现,该定理有助于计算后验概率 ( p(θ|x) )。

为了计算这个后验概率,我们需要考虑三个关键要素:

  • 似然 ( p(x|θ) ):这表示在给定一组参数的情况下,观察到的数据的概率。这是最大似然估计(MLE)的基础,侧重于参数解释观察到的数据的效果。

  • 先验 ( p(θ) ):这反映了我们在观察到任何数据之前对参数的初始信念。这是贝叶斯推断中的一个重要部分,其中先验知识关于参数分布被纳入考虑。

  • 边际似然或证据 ( p(x) ):这衡量了在所有可能参数集下观察到数据的概率,基本上评估了在不做特定参数假设的情况下观察数据的概率。

在实际应用中,边际似然 ( p(x) ) 通常可以忽略,特别是在比较不同参数集时,因为它保持不变且不影响相对比较。

使用贝叶斯定理,我们发现后验 ( p(θ|x) ) 似然函数和先验函数的乘积成正比,( p(x|θ)* p(θ) )。

这意味着在比较不同的参数集时,我们必须同时考虑对参数的先验信念和似然,即观察数据如何修改我们的信念。与 MLE 相似,在 MAP(最大后验估计)中,我们寻求最大化后验,以找到最佳的模型参数集,综合考虑先验知识和观察数据。

所以,MAP 融入了一个额外的元素,即我们对参数的先验信念。

正确。MAP 确实使用了额外的信息,即我们对参数的先验信念。让我们再次用我的猫 Bubble 的例子来说明。在 MAP 的背景下,当确定 Bubble 喜欢的食物口味——牛肉还是鸡肉时,我会考虑来自饲养员的提示。饲养员提到 Bubble 喜欢吃煮鸡胸肉,因此这一信息形成了我对 Bubble 可能偏爱鸡肉口味的先验信念。因此,在最初选择她的食物时,我会倾向于购买更多的鸡肉口味的食物。这种将饲养员的见解融入 MAP 估计的方法就代表了 MAP 中的‘先验’。

我理解 MAP 和 MLE 是相关的,其中 MAP 加入了我们对参数的假设。你能提供一个更简单的例子来展示这两种方法之间的区别和联系吗?

为了演示 MAP 和 MLE 之间的联系,我将介绍一些数学公式。虽然本讨论的目标是通过对话直观地解释机器学习概念,但展示这些函数将有所帮助。不要担心复杂性,这些公式只是突出 MAP 相较于 MLE 提供的额外见解,以便更清晰地理解。

最大似然估计 (MLE) 侧重于识别使观察数据 x 最有可能的参数集 θ。 它通过最大化似然函数 P(X|θ) 实现这一目标。

然而,直接最大化概率的乘积(这些概率通常小于 1)可能由于计算下溢——一种数字变得过于微小以至于无法准确表示的情况——而不切实际。为了克服这一问题,我们使用对数,将乘积转化为和。由于对数函数是单调递增的,最大化一个函数等同于最大化其对数。因此,MLE 公式通常涉及概率对数的和。

另一方面,最大后验估计(MAP)旨在最大化后验概率。 运用贝叶斯定理,我们发现最大化后验概率等同于最大化先验概率 P(θ)和似然的乘积。与 MLE 一样,我们引入对数来简化计算,将乘积转换为和。

MLE 与 MAP 之间的主要区别在于 MAP 中包含了先验 P(θ)。这一附加因素意味着在 MAP 中,似然被先验有效地加权,基于我们对参数的先验信念影响估计。 相比之下,MLE 不包含这种先验,仅关注从观测数据中得出的似然。

看起来 MAP 可能优于 MLE。那么我们为什么不总是选择 MAP 呢?

MAP 估计包含了我们对参数分布的先验知识。但这并不 inherently 使它优于 MLE。需要考虑几个因素:

  • 关于参数分布的假设并不总是可用的。 在假设参数分布为均匀的情况下,MAP 和 MLE 会产生等效的结果。

  • MLE 的计算简单性通常使它成为更实际的选择。 虽然 MAP 提供了全面的贝叶斯方法,但它的计算复杂度较高。

  • MAP 的有效性在很大程度上依赖于选择适当的先验。 选择不准确的先验可能会导致 MAP 在识别最佳参数集时计算成本增加。

在我们的下一节中,我们的导师-学习者团队将回到深入探讨 L1 和 L2 正则化。掌握了 MLE 和 MAP 的基本知识后,我们将能够从全新的视角查看L1 和 L2 正则化。期待在下一篇文章中见到你!

本系列中的其他文章:

  • 学习机器学习的勇气:解密 L1 和 L2 正则化(第一部分)

  • 学习机器学习的勇气:解密 L1 和 L2 正则化(第二部分)

  • 学习机器学习的勇气:解密 L1 和 L2 正则化(第三部分)

  • 如果你喜欢这篇文章,可以在 LinkedIn找到我。

参考文献:

[## MLE 与 MAP:最大似然估计与最大后验估计之间的联系

在这篇文章中,我们将看到最大似然估计(MLE)和最大后验估计(MAP)之间的区别…

最大似然估计 (MLE) 与最大后验估计 (MAP) 之间的区别

最大似然估计 (MLE) 和最大后验估计 (MAP) 之间的区别

最大似然估计 (MLE) 和最大后验估计 (MAP) 之间的区别 - Amir Masoud

在统计数据分析中,“似然”是一个关键概念,它与…关系密切,但具有重要区别

似然函数是什么,它在粒子物理学中的应用

勇敢学习机器学习:揭开 L1 和 L2 正则化的面纱(第三部分)

原文:towardsdatascience.com/courage-to-learn-ml-demystifying-l1-l2-regularization-part-3-ee27cd4b557a?source=collection_archive---------3-----------------------#2023-11-29

为什么 L0.5、L3 和 L4 正则化不常见

Amy MaTowards Data Science Amy Ma

·

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

--

照片由 Kelvin Han 提供,来源于 Unsplash

欢迎回到《勇敢学习机器学习:揭开 L1 和 L2 正则化的面纱》系列的第三篇文章。在之前的文章中,我们探讨了 正则化的目的 和 通过拉格朗日乘子解读 L1 和 L2 方法

继续我们的旅程,我们的导师-学习者二人组将进一步探讨使用拉格朗日乘数的 L1 和 L2 正则化。

在本文中,我们将探讨一些可能引起你兴趣的问题。如果你对这些话题感到困惑,你来对地方了:

  • 为什么没有 L0.5 正则化?

  • 既然大多数深度学习问题都是非凸的,我们为什么关心一个问题是否是非凸问题?

  • 为什么像 L3 和 L4 这样的范数不常用?

  • L1 和 L2 正则化可以结合使用吗?这样做有什么优点和缺点?

根据我们上次讨论的问题,我查阅了 Lp 范数,发现 p 的值可以是大于 0 的任何数字。为什么不使用 0 和 1 之间的 p 值?为什么没有 L0.5 正则化?

我很高兴你提了这个问题。直接说,我们通常避免 p 小于 1 的值,因为它们会导致非凸优化问题。让我通过一张展示不同 p 值的 Lp 范数形状的图像来说明这一点。仔细看看 当 p=0.5 时,你会注意到形状明显是非凸的

不同 p 值的 Lp 范数形状。来源: lh5.googleusercontent.com/EoX3sngY7YnzCGY9CyMX0tEaNuKD3_ZiF4Fp3HQqbyqPtXks2TAbpTj5e4tiDv-U9PT0MAarRrPv6ClJ06C0HXQZKHeK40ZpVgRKke8-Ac0TAqdI7vWFdCXjK4taR40bdSdhGkWB

当我们查看 3D 表示时,情况变得更加清晰,假设我们正在优化三个权重。在这种情况下,很明显问题不是凸的,边界上出现了许多局部最小值。

来源: ekamperi.github.io/images/lp_norms_3d.png

我们通常避免在机器学习中使用非凸问题的原因是它们的复杂性。对于凸问题,你可以保证得到全局最小值——这使得解决起来一般更容易。另一方面,非凸问题通常有多个局部最小值,并且可能计算密集且不可预测。正是这些挑战我们在 ML 中试图避免。

当我们使用拉格朗日乘数等技术来优化具有特定约束的函数时,这些约束必须是凸函数。这确保了将它们添加到原始问题中不会改变其基本性质,从而使问题更难解决。 这一点至关重要;否则,添加约束可能会使原始问题更加困难。

为什么我们在这里关注一个问题或约束是否是非凸问题?难道大多数深度学习问题不是非凸的吗?

你的问题涉及了深度学习中的一个有趣方面。虽然我们并不偏爱非凸问题,但更准确地说是我们经常在深度学习领域遇到并必须处理这些问题。原因如下:

  1. 深度学习模型的性质导致非凸损失面:大多数深度学习模型,特别是具有隐藏层的神经网络,固有地具有非凸损失函数。这是由于这些模型内部发生的复杂非线性变换。非线性变换与参数空间的高维度的结合通常导致一个非凸的损失面。

  2. 局部最小值在深度学习中不再是问题:在深度学习中典型的高维空间中,局部最小值不像在低维空间中那样成问题。研究表明,深度学习中的许多局部最小值与全局最小值的值接近。此外,鞍点——梯度为零但既不是极大值也不是极小值的点——在这样的空间中更为常见,并且是更大的挑战。

  3. 存在更有效的高级优化技术来处理非凸空间。 高级优化技术,如随机梯度下降(SGD)及其变体,在这些非凸空间中特别有效地找到良好的解决方案。虽然这些解决方案可能不是全局最小值,但它们通常足够好,可以在实际任务中实现高性能。

尽管深度学习模型是非凸的,但它们在捕捉大型数据集中的复杂模式和关系方面表现出色。此外,对非凸函数的研究持续推进,增强了我们的理解。展望未来,我们有可能以更高效的方式处理非凸问题,减少担忧。

为什么我们不考虑使用更高的范数,如 L3 和 L4,进行正则化?

回忆一下我们之前讨论的图像,该图像展示了不同p值下 Lp 范数的形状。随着p的增加,Lp 范数的形状会发生变化。例如,在p = 3时,它类似于一个圆角矩形,而当p接近无穷大时,它形成一个完美的正方形。

不同 p 值下的 Lp 范数的形状。来源:lh5.googleusercontent.com/EoX3sngY7YnzCGY9CyMX0tEaNuKD3_ZiF4Fp3HQqbyqPtXks2TAbpTj5e4tiDv-U9PT0MAarRrPv6ClJ06C0HXQZKHeK40ZpVgRKke8-Ac0TAqdI7vWFdCXjK4taR40bdSdhGkWB

在我们的优化问题背景下,考虑更高的范数如 L3 或 L4。类似于 L2 正则化,其中损失函数和约束轮廓在圆角处交叉,这些更高的范数将鼓励权重接近零,就像 L2 正则化一样。(如果这部分不清楚,请随时回顾 第二部分 以获取更详细的解释。)基于这一声明,我们可以讨论 L3 和 L4 范数不常用的两个关键原因:

  1. L3 和 L4 范数表现出与 L2 相似的效果,而没有提供显著的新优势(使权重接近 0)。 相比之下,L1 正则化会将权重归零并引入稀疏性,有利于特征选择。

  2. 计算复杂性是另一个重要方面。 正则化会影响优化过程的复杂性。L3 和 L4 范数比 L2 的计算负担更重,使它们在大多数机器学习应用中不够可行。

总结来说,虽然理论上可以使用 L3 和 L4 范数,但它们并没有比 L1 或 L2 正则化提供独特的好处,而且它们的计算低效使得它们不太实用。

是否可以结合 L1 和 L2 正则化?

是的,确实可以结合 L1 和 L2 正则化,这种技术通常称为弹性网正则化。这种方法将 L1(lasso)和 L2(ridge)正则化的特性融合在一起,既有用又有挑战。

弹性网正则化是 L1 和 L2 正则化项的线性组合。它将 L1 和 L2 范数都添加到损失函数中。因此,它有两个需要调整的参数,lambda1 和 lambda2。

弹性网正则化。来源:wikimedia.org/api/rest_v1/media/math/render/svg/a66c7bfcf201d515eb71dd0aed5c8553ce990b6e

使用弹性网正则化的好处是什么?如果有的话,为什么我们不更频繁地使用它?

通过结合这两种正则化技术,弹性网可以提高模型的泛化能力,比单独使用 L1 或 L2 更有效地减少过拟合的风险。

让我们分解一下它的优点:

  1. 弹性网提供的稳定性比 L1 更强。 L1 正则化可能导致稀疏模型,这对特征选择有用。但在某些情况下它也可能不稳定。例如,L1 正则化可能在高度相关的变量中任意选择特征(同时使其他系数变为 0)。而弹性网可以在这些变量之间更均匀地分配权重。

  2. L2 比 L1 正则化更稳定,但不鼓励稀疏性。 弹性网旨在平衡这两个方面,可能导致更稳健的模型。

然而,弹性网正则化引入了一个额外的超参数,需要细致的调整。在 L1 和 L2 正则化之间取得适当的平衡,并实现最佳模型性能需要增加计算工作量。这种额外的复杂性是它不常用的原因。

在下一次会议中,我们将从全新的角度探索 L1 和 L2 正则化,深入探讨贝叶斯先验信念以加深理解。暂时先停在这里——期待我们的下一次讨论!

本系列中的其他帖子:

  • 勇敢学习机器学习:揭开 L1 和 L2 正则化的神秘面纱(第一部分)

  • 勇敢学习机器学习:揭开 L1 和 L2 正则化的神秘面纱(第二部分)

  • 如果你喜欢这篇文章,可以在 LinkedIn上找到我。

参考:

通过迭代软阈值的弹性网正则化

链接 3 [## 正则化介绍

部分内容:机器学习序列 后续内容:偏差与方差,梯度下降 内容摘要:1100 字,11 分钟…

链接 1 链接 2 [## 规范和机器学习

介绍了机器学习和优化中的规范,强调 LASSO 和岭回归。

链接 4

勇敢学习 ML:揭示 L1 和 L2 正则化(第四部分)

原文:towardsdatascience.com/courage-to-learn-ml-demystifying-l1-l2-regularization-part-4-27c13dc250f9

探索 L1 和 L2 正则化作为贝叶斯先验

Amy MaTowards Data Science Amy Ma

·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 12 月 11 日

--

照片由 Dominik JirovskýUnsplash 提供

欢迎回到‘勇敢学习 ML:揭示 L1 和 L2 正则化’,这是第四篇文章。上次,我们的导师-学习者配对通过 拉格朗日乘子视角探讨了 L1 和 L2 正则化的性质

在关于 L1 和 L2 正则化的总结部分,这对将从新的角度探讨这些话题——贝叶斯先验。我们还将总结 L1 和 L2 正则化如何在不同算法中应用。

在这篇文章中,我们将讨论几个引人入胜的问题。如果这些话题激起了你的好奇心,你来对地方了!

  • MAP 先验如何与 L1 和 L2 正则化相关

  • 对使用拉普拉斯和正态分布作为先验的直观分析

  • 理解 L1 正则化与拉普拉斯先验引起的稀疏性

  • 与 L1 和 L2 正则化兼容的算法

  • 为什么 L2 正则化在神经网络训练中通常被称为“权重衰减”

  • L1 范数在神经网络中使用较少的原因

所以,我们讨论了 MAP 如何不同于 MLE,主要因为 MAP 考虑了一个额外的信息:在看到数据之前的信念,即先验。这与 L1 和 L2 正则化有何关系?

让我们深入探讨 MAP 公式中不同先验如何塑造我们对 L1 和 L2 正则化的理解(有关如何制定此方程式的详细讲解,请查看 这篇文章)。

在考虑权重的先验时,我们的初步直觉通常会使我们选择正态分布作为模型权重的先验。这样,我们通常会对每个权重 wi 使用均值为零的正态分布,并具有相同的标准差𝜎。将这种信念代入 MAP 中的先验项 logp(w)(其中 p(w)表示权重的先验)会自然得到权重平方和。这个项正是L2 范数。这意味着使用正态分布作为我们的先验等同于应用 L2 正则化。

相反,采用拉普拉斯分布作为我们的信念会导致L1 范数的权重。因此,拉普拉斯先验本质上相当于 L1 正则化。

简而言之,L1 正则化与拉普拉斯分布先验一致,而 L2 正则化对应于正态分布先验。

有趣的是,当在 MAP 框架中使用均匀先验时,它实际上在方程中“消失”了(你可以自己尝试一下!)。这使得似然项成为确定最佳权重值的唯一因素,实际上将 MAP 估计转变为最大似然估计(MLE)。

那么,当我们的先验是拉普拉斯分布与正态分布时,为什么会有不同的信念?我想更好地可视化这个问题。

这是一个很好的问题。确实,不同的先验意味着你在收集任何数据之前对情况持有不同的初步假设。我们稍后将深入探讨不同分布的目的,但现在,让我们通过使用拉普拉斯分布和正态分布来考虑一个简单直观的例子。考虑一下我新发布的 Medium 帖子。两周前,作为一名没有粉丝的新作者,我预期没有查看次数。我的假设是,平均每日查看次数起初会很低,可能为零,但随着对类似主题感兴趣的读者发现我的作品,这一数字可能会增加。拉普拉斯先验很好地适应了这种情况。它建议了一系列可能的查看次数,但对接近零的数字赋予更高的概率,反映了我对初期查看次数很少但随时间增长的预期。

现在有 55 位观众(谢谢大家!),以及关注我帖子更新的粉丝,我的期望已经改变。我预期新帖子将表现类似于之前的帖子,平均查看次数接近我历史上的查看次数。这时,正态分布先验发挥作用,根据我已建立的记录预测未来的查看次数。

嗯……你能解释一下使用拉普拉斯先验的 L1 正则化的稀疏性吗?

确实,通过比较拉普拉斯分布和正态分布,可以揭示 L1 正则化促进稀疏性的机制。关键的区别在于它们在零附近的概率密度。 拉普拉斯分布在零附近有一个尖锐的峰值,表明接近零的值的可能性较高。这一特征类似于 L1 正则化的效果,其中模型中的大多数权重被驱动到零,促进了稀疏性。相比之下,正态分布与 L2 正则化相关,在零附近的峰值较低,分布较广,表示对权重的更均匀分配有偏好。

来源: austinrochford.com/posts/2013-09-02-prior-distributions-for-bayesian-regression-using-pymc.html

此外,拉普拉斯分布的尾部更重,意味着其延伸得更远。这一特性允许一些权重显著远离零,而其他权重则非常接近零。因此,通过选择拉普拉斯分布作为权重的先验(L1 正则化),我们鼓励模型学习解决方案,使大多数权重接近零,实现稀疏性而不牺牲潜在的相关特征。这就是为什么 L1 正则化可以作为特征选择方法。

所以,我认为 L1 和 L2 正则化对于避免过拟合和提升模型的泛化能力至关重要。你能告诉我这些方法可以应用于哪些算法吗?

L1 和 L2 正则化可以通过向损失函数中添加惩罚项来应用于许多算法。以下是一些应用 L1 和 L2 正则化的具体算法示例:

  • 线性模型。 这些技术在高维问题中特别有用。在线性模型中,它们被称为lasso 和 ridge 回归。需要注意的是,L1 正则化不仅有助于防止过拟合,还有助于特征选择,从而防止多重共线性。

  • 支持向量机。 正则化方法是SVM 的核心。通过添加权重惩罚项,SVM 鼓励模型减少决策边界与最近支持向量之间的间距(L1 正则化)或平滑间距(L2 正则化),从而提高泛化能力。

  • 神经网络。 L2 正则化在神经网络中使用得更为普遍,通常被称为权重衰减。L1 正则化也可以在神经网络中使用,但由于其倾向于导致稀疏权重,因此较少见。

  • 集成算法。像 GBM 和 Xgboost 这样的梯度提升机器使用 L1 和 L2 正则化来限制集成中单个树的大小。L1 正则化通过缩小集成中弱学习器(每棵树)的权重来实现这一点,而 L2 正则化则惩罚每棵树中的总叶子数(叶子得分)。

为什么 L2 正则化也被称为神经网络训练中的‘权重衰减’?为什么 L1 范数在神经网络中使用较少?

为了回答这两个问题,让我们引入一些数学来说明在 L1 和 L2 正则化下权重是如何更新的。

带有 L2 正则化的损失函数;λ是惩罚系数,α代表学习率。

在 L2 正则化中,权重更新过程涉及对权重的轻微减少,根据其自身的大小缩放。这就是所谓的“权重衰减”。具体来说,每个权重减少的量与其当前值直接成比例。这种比例减少,由通常较小的惩罚系数(λ)和学习率(α)设置所控制,确保较大的权重比小的权重受到更高程度的惩罚。权重衰减的本质在于这种缩小权重的方式,鼓励模型保持较小的权重。这种行为在神经网络中是有利的,因为它趋向于产生更平滑的决策边界。

带有 L1 正则化的损失函数;λ是惩罚系数,α代表学习率。

相比之下,L1 正则化通过减去或添加一个由αλ和权重(w)符号确定的固定量来修改权重更新规则。这种方法将权重推向零,无论它们是正的还是负的。在 L1 正则化下,所有权重,不论其大小,都以相同的固定量进行调整。这使得较大的权重保持相对较大,而较小的权重则更快地被驱动到零,从而促进网络的稀疏性。

让我们比较一下!

相比之下,L2 对权重的修改方式是基于权重的现有值,使得较大的权重比小的权重衰减得更快。这种对所有权重的均匀衰减就是为什么它被称为‘权重衰减’。另一方面,L1 的固定调整量,不论权重大小,可能导致一些问题,并在神经网络中变得不那么受欢迎:

  • 它可能将一些权重归零,导致‘死神经元’,并可能破坏网络中的信息流,这可能影响模型性能。

  • L1 引入的零点处的不可微分点使得像梯度下降这样的优化算法效果较差。

添加 L1 和 L2 正则化对我们的损失函数有什么影响?这些正则化的引入是否使我们远离原始的全局最小值?

这是个很好的问题!简而言之,一旦我们引入正则化,我们有意将关注点从原始的全局最小值转移开。 这意味着向损失函数中添加惩罚项,从而根本改变其形状。理解这一变化是期望中的,而非偶然发生的,是至关重要的。

通过引入这些惩罚,我们旨在实现一个新的最优解,平衡两个关键目标:良好地拟合训练数据以最小化经验风险,同时减少模型复杂度并增强对未见数据的泛化。原始的全局最小值可能无法实现这种平衡,从而可能导致过拟合和在新数据上的性能下降。

如果你对测量原始最优解和正则化后最优解之间的距离的数学细节感兴趣,我强烈推荐 Ian Goodfellow 的《深度学习》第七章(第 224–229 页)。特别注意公式 7.7 和 7.13(L2)以及 7.22 和 7.23(L1)。这将提供对正则化项对权重影响的定量评估,深入了解 L1 和 L2 正则化。

我们现在已经结束了对 L1 和 L2 正则化的探讨。在下次讨论中,我很兴奋地将深入探讨损失函数的基础知识。非常感谢所有喜欢本系列第一部分的读者。 起初,我的目标是巩固我对基本 ML 概念的理解,但看到它引起了许多人的共鸣,我感到非常高兴😃。如果你对我们的下一个话题有建议,请随时留言!

本系列的其他文章:

  • 勇敢学习 ML: 揭开 L1 和 L2 正则化的面纱(第一部分)

  • 勇敢学习 ML: 揭开 L1 和 L2 正则化的面纱(第二部分)

  • 勇敢学习 ML: 揭开 L1 和 L2 正则化的面纱(第三部分)

  • 勇敢学习 ML: 解码似然、MLE 和 MAP

如果你喜欢这篇文章,你可以在 LinkedIn找到我。

参考

[## 正则化的概率解释

从概率的角度看正则化。

bjlkeng.io

keras.io/api/layers/regularizers/

[## 为什么现代深度学习中需要权重衰减?

权重衰减是一种广泛用于训练先进深度网络的技术,包括大型语言模型……

arxiv.org

学习机器学习的勇气:揭开 L1 与 L2 正则化的神秘面纱(第二部分)

原文:towardsdatascience.com/courage-to-learn-ml-unraveling-l1-l2-regularization-part-2-1bb171e43b35

揭示拉格朗日乘子背后的 L1 稀疏性直觉

Amy MaTowards Data Science Amy Ma

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

--

欢迎回到‘学习机器学习的勇气:揭开 L1 与 L2 正则化的神秘面纱’,第二部分。在 我们之前的讨论 中,我们探讨了较小系数的好处及通过权重惩罚技术获得这些系数的方法。现在,在这篇续集中,我们的导师和学习者将进一步深入 L1 和 L2 正则化的领域。

如果你曾经思考过这些问题,你来对地方了:

  • L1 和 L2 正则化名称背后的原因是什么?

  • 我们如何解读经典的 L1 和 L2 正则化图?

  • 拉格朗日乘子是什么,我们如何直观地理解它们?

  • 应用拉格朗日乘子来理解 L1 稀疏性。

你的参与——点赞、评论和关注——不仅仅是提升士气;它推动了我们的发现之旅!那么,让我们开始吧。

图片由 Aarón Blanco Tejedor 提供,来源于 Unsplash

为什么它们被称为 L1 和 L2 正则化?

L1 和 L2 正则化的名称直接源于 Lp 范数的概念。Lp 范数代表了计算空间中某点到原点距离的不同方式。例如,L1 范数,也称为曼哈顿距离,计算距离时使用坐标的绝对值,如∣x∣+∣y∣。另一方面,L2 范数,或称为欧几里得距离,则计算为平方值和的平方根,即 sqrt(x² + y²)。

在机器学习中的正则化背景下,这些范数用于创建添加到损失函数中的惩罚项。你可以将 Lp 正则化视为测量模型权重在高维空间中与原点的总距离。范数的选择会影响惩罚的性质:L1 范数倾向于使一些系数为零,从而有效地选择更重要的特征,而 L2 范数则将系数缩小到接近零,确保没有单一特征对模型的影响过大。

因此,L1 和 L2 正则化的名称来源于这些数学范数——L1 范数和 L2 范数——因为它们将各自的距离计算作为惩罚施加于模型的权重。这有助于控制过拟合。

我在学习 L1 和 L2 正则化时经常遇到下面的图表,但我发现很难解读。您能帮忙澄清一下它表示什么吗?

传统但令人困惑的 L1 和 L2 正则化图表在教科书中常见…… 来源: commons.wikimedia.org/wiki/File:Regularization.jpg

好的,让我们一步一步地解析这张图。首先,理解其不同元素的意义是至关重要的。假设我们的损失函数由两个权重 w1 和 w2 定义(在图中我们用 beta 代替 w,但它们表示相同的概念)。图表的轴表示这些我们希望优化的权重。

在没有任何权重惩罚的情况下,我们的目标是找到使损失函数最小化的 w1 和 w2 值。你可以将这个函数的景观想象成一个山谷或盆地,在图中通过椭圆轮廓来表示。

现在,让我们深入了解惩罚。L1 范数,呈菱形,基本上测量了 w1 和 w2 到原点的曼哈顿距离。L2 范数形成一个圆,表示权重的平方和。

椭圆轮廓的中心表示目标函数的全局最小值,在那里我们找到理想的权重。L1 和 L2 形状(菱形和圆形)在原点的中心,其中所有权重为零,突出显示了最小权重惩罚的情况。随着惩罚项强度的增加,模型的权重会趋近于零。这个图表是理解这些动态以及惩罚对权重影响的视觉指南。

明白了。所以…… 图表展示了由权重创建的一个维度,而两个不同的形状分别表示目标函数和惩罚。我们应该如何解读标记为 w*的交点?这个点意味着什么?

要全面理解上述图表,必须掌握拉格朗日乘子这一优化关键工具。拉格朗日乘子有助于在特定约束条件下找到函数的最优点(最大值或最小值)。

想象一下你正在登山,目标是到达山顶。虽然有多条路径,但为了安全,你必须沿着指定的安全路径行走。在这里,到达山顶代表了优化问题,而安全路径则象征着约束。

从数学上讲,假设你有一个函数 f(x, y)需要优化。这一优化必须遵守一个约束,由另一个函数 g(x, y) = 0 表示。

拉格朗日乘子 2D。来源:upload.wikimedia.org/wikipedia/commons/b/bf/LagrangeMultipliers2D.svg

在维基百科的‘拉格朗日乘子 2D’图中,蓝色轮廓表示 f(x, y)(山的地形),而红色曲线表示约束。这两者的交点,虽然不是 f(x, y)轮廓上的最高点,但代表了在给定约束下的最优解。拉格朗日乘子通过将目标函数与其约束合并来解决这个问题。换句话说,拉格朗日乘子会帮助你更容易找到这个点。

所以,如果我们回到 L1 和 L2 图,你是否暗示菱形和圆形代表约束?这是否意味着它们交点的切点本质上是满足这些约束下 f(x, y)的最大值的最佳点?

正确!L1 和 L2 正则化技术的确可以被可视化为分别施加菱形和圆形的约束。因此,这个图帮助我们理解这些正则化方法如何影响函数的优化,通常是机器学习模型中的损失函数。

L2(左)和 L1(右)正则化的更好示例。来源:www.researchgate.net/figure/Parameter-norm-penalties-L2-norm-regularization-left-and-L1-norm-regularization_fig2_355020694

  1. L1 正则化(菱形形状):L1 范数创建了一个菱形约束。这种形状的特点是沿轴的尖角。当优化过程(如梯度下降)在这个菱形内寻找最小化损失函数的点时,它更可能碰到这些角落。在这些角落,模型的一个权重(参数)变为零,而其他权重保持非零。L1 范数的这种特性导致了模型参数的稀疏性,这意味着某些权重为零。这种稀疏性对特征选择非常有用,因为它有效地从模型中移除了一些特征。

  2. L2 正则化(圆形约束):另一方面,L2 范数创建了一个圆形的约束。圆的平滑、圆润的特性意味着优化过程不太可能在权重为零的坐标轴上找到解决方案。 相反,L2 范数倾向于均匀地缩小权重,而不一定将任何权重驱动为零。这通过防止权重过大来控制模型复杂性,从而帮助避免过拟合。然而,与 L1 范数不同,它不会导致模型参数的稀疏性。

请记住,拉格朗日乘数并不是掌握 L1 和 L2 正则化的唯一方法。我们在这里休息一下,我将在下一期中回答更多的问题。很快见!

本系列的其他文章:

如果你喜欢这篇文章,你可以在 LinkedIn上找到我。

破解 Hugging Face Transformers 库

原文:towardsdatascience.com/cracking-open-the-hugging-face-transformers-library-350aa0ef0161

使用开源 LLMs 的快速入门指南

Shaw TalebiTowards Data Science Shaw Talebi

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

--

这是一个关于实践中使用大型语言模型(LLMs)的系列文章中的第 3 篇。在这里,我将为 Hugging Face Transformers 库提供一个适合初学者的指南,该库提供了一种简单且无成本的方式来使用各种开源语言模型。我将从回顾关键概念开始,然后深入示例 Python 代码。

照片由 Jéan Béller 提供,发布在 Unsplash

在本系列的上一篇文章中,我们探索了 OpenAI Python API,并用它制作了一个定制的聊天机器人。然而,该 API 的一个缺点是 API 调用需要付费,这可能在某些用例中不太适用。

在这些情况下,转向开源解决方案可能会很有优势。一种流行的方法是通过Hugging Face的 Transformers 库来实现。

Hugging Face 是什么?

Hugging Face 是一家已经成为开源机器学习(ML)主要中心的 AI 公司。他们的平台有 3 个主要元素,允许用户访问和分享机器学习资源。

首先是他们快速增长的预训练开源 ML 模型库,用于自然语言处理(NLP)、计算机视觉等。第二是他们的数据集库,用于训练几乎任何任务的 ML 模型。第三,也是最后,是Spaces,这是一个由 Hugging Face 托管的开源 ML 应用集合。

这些资源的力量在于它们是社区生成的,利用了开源的所有优势(即免费、大量的工具、多样的资源和快速的创新步伐)。虽然这些使得构建强大的 ML 项目比以往更容易,但 Hugging Face 生态系统的另一个关键元素是——Transformers 库。

🤗Transformers

Transformers 是一个 使下载和训练最先进的 ML 模型变得简单的 Python 库。虽然它最初是为开发语言模型而创建的,但它的功能已经扩展到包括计算机视觉、音频处理等模型。

这个库的两个主要优点是,,它能够轻松与 Hugging Face(前面提到的)的 Models、Datasets 和 Spaces 仓库集成,,这个库支持其他流行的 ML 框架,如 PyTorch 和 TensorFlow。

这使得下载、训练和部署机器学习模型和应用程序变得简单而灵活。

Pipeline()

使用这个库的最简单方法是通过 pipeline() 函数,它将 NLP(和其他)任务抽象成 1 行代码。例如,如果我们想做情感分析,我们需要选择一个模型,对输入文本进行分词,经过模型处理,然后解码数值输出以确定情感标签(积极或消极)。

虽然这看起来有很多步骤,但我们可以通过 pipeline() 函数在 1 行代码中完成所有这些步骤,如下面的代码片段所示。

pipeline(task="sentiment-analysis")("Love this!")

# output -> [{'label': 'POSITIVE', 'score': 0.9998745918273926}]

当然,情感分析并不是我们在这里唯一能做的事情。几乎任何 NLP 任务都可以用这种方式完成,例如摘要、翻译、问答、特征提取(即文本嵌入)、文本生成、零样本分类等 —— 有关内置任务的完整列表,请查看 pipeline() 文档

在上述示例代码中,由于我们没有指定模型,因此使用了默认的情感分析模型(即 distilbert-base-uncased-finetuned-sst-2-english)。然而,如果我们想更明确一些,可以使用以下代码行。

pipeline(task="sentiment-analysis", 
        model='distilbert-base-uncased-finetuned-sst-2-english')("Love this!")

# ouput -> [{'label': 'POSITIVE', 'score': 0.9998745918273926}]

Transformers 库的最大优点之一是我们可以通过简单地更改传递给 pipeline() 函数的模型名称,轻松使用 Hugging Face 的 Models repository 上的任何 28,000 多个文本分类模型。

模型

Hugging Face 上有大量的预训练模型(截至写作时为 277,528 个)。几乎所有这些模型都可以通过 Transformers 轻松使用,使用与上述代码块中相同的语法。

然而,Hugging Face 上的模型不仅仅用于 Transformers 库。 还有其他流行机器学习框架的模型,如 PyTorch、Tensorflow、Jax。这使得 Hugging Face 的模型库对超越 Transformers 库的机器学习从业者也很有用。

要了解浏览代码库的样子,我们可以考虑一个例子。假设我们需要一个可以进行文本生成的模型,但我们希望它可以通过 Transformers 库使用,这样我们就能用一行代码来实现(如上所示)。我们可以轻松地使用“任务”和“库”过滤器来查看符合这些条件的所有模型。

一个符合这些标准的模型是新发布的 Llama 2。更具体地说,Llama-2–7b-chat-hf,这是 Llama 2 系列中的一个模型,具有大约 70 亿个参数,优化用于聊天,并且采用 Hugging Face Transformers 格式。我们可以通过它的模型卡获得更多有关该模型的信息,如下图所示。

浏览Llama-2–7b-chat-hf 模型卡。图片由作者提供。

安装 🤗Transformers(使用 Conda)

现在我们对 Hugging Face 和 Transformers 库提供的资源有了基本了解,让我们看看如何使用它们。我们从安装库和其他依赖项开始。

Hugging Face 在其网站上提供了一个安装指南。因此,我不会在这里(不熟练地)重复那个指南。不过,我会提供一个简短的 2 步指南,说明如何为下面的示例代码设置 conda 环境

步骤 1) 第一步是下载位于GitHub 仓库的 hf-env.yml 文件。你可以直接下载文件或克隆整个仓库。

步骤 2) 接下来,在你的终端(或 anaconda 命令提示符)中,你可以使用以下命令基于 hf-env.yml 创建一个新的 conda 环境。

>>> cd <directory with hf-env.yml>

>>> conda env create --file hf-env.yml

这可能需要几分钟来安装,但一旦完成,你应该就可以开始使用了!

示例代码:使用 🤗Transformers 进行 NLP

在安装了必要的库之后,让我们进入一些示例代码。这里我们将调查3 个 NLP 用例,即情感分析、摘要和对话文本生成,使用pipeline()函数。

在最后,我们将使用 Gradio 快速生成一个用户界面(UI)来处理这些用例,并将其作为应用部署在 Hugging Face Spaces 上。所有示例代码都可以在GitHub 仓库上找到。

情感分析

我们开始情感分析。回忆一下我们之前使用 pipeline 函数来完成类似下面的代码块的操作,其中我们创建了一个分类器,可以将输入文本标记为正面或负面。

from transformers import pipeline

classifier = pipeline(task="sentiment-analysis", \
                      model="distilbert-base-uncased-finetuned-sst-2-english")

classifier("Hate this.")

# output -> [{'label': 'NEGATIVE', 'score': 0.9997110962867737}]

为了更进一步,我们可以将文本列表传递给分类器进行批处理,而不是逐个处理。

text_list = ["This is great", \
             "Thanks for nothing", \
             "You've got to work on your face", \
             "You're beautiful, never change!"]

classifier(text_list)

# output -> [{'label': 'POSITIVE', 'score': 0.9998785257339478},
# {'label': 'POSITIVE', 'score': 0.9680058360099792},
# {'label': 'NEGATIVE', 'score': 0.8776106238365173},
# {'label': 'POSITIVE', 'score': 0.9998120665550232}]

然而,Hugging Face 上的文本分类模型不仅限于正面-负面情感。例如,SamLowe 的“roberta-base-go_emotions”模型生成了一套类别标签。我们可以像下面的代码片段所示一样轻松地将此模型应用于文本。

classifier = pipeline(task="text-classification", \
                      model="SamLowe/roberta-base-go_emotions", top_k=None)

classifier(text_list[0])

# output -> [[{'label': 'admiration', 'score': 0.9526104927062988},
#  {'label': 'approval', 'score': 0.03047208860516548},
#  {'label': 'neutral', 'score': 0.015236231498420238},
#  {'label': 'excitement', 'score': 0.006063772831112146},
#  {'label': 'gratitude', 'score': 0.005296189337968826},
#  {'label': 'joy', 'score': 0.004475208930671215},
#  ... and many more

总结

我们还可以将 pipeline() 函数用于文本总结。虽然这与情感分析是完全不同的任务,但语法几乎是相同的。

我们首先加载一个总结模型。然后传入一些文本以及几个输入参数。

summarizer = pipeline("summarization", model="facebook/bart-large-cnn")

text = """
Hugging Face is an AI company that has become a major hub for open-source machine learning. 
Their platform has 3 major elements which allow users to access and share machine learning resources. 
First, is their rapidly growing repository of pre-trained open-source machine learning models for things such as natural language processing (NLP), computer vision, and more. 
Second, is their library of datasets for training machine learning models for almost any task. 
Third, and finally, is Spaces which is a collection of open-source ML apps.

The power of these resources is that they are community generated, which leverages all the benefits of open source i.e. cost-free, wide diversity of tools, high quality resources, and rapid pace of innovation. 
While these make building powerful ML projects more accessible than before, there is another key element of the Hugging Face ecosystem—their Transformers library.
"""
summarized_text = summarizer(text, min_length=5, max_length=140)[0]['summary_text']
print(summarized_text)

# output -> 'Hugging Face is an AI company that has become a major hub for 
# open-source machine learning. They have 3 major elements which allow users 
# to access and share machine learning resources.' 

对于更复杂的用例,可能需要连续使用多个模型。例如,我们可以对总结后的文本应用情感分析以加快运行时间。

classifier(summarized_text)

# output -> [[{'label': 'neutral', 'score': 0.9101783633232117},  
# {'label': 'approval', 'score': 0.08781372010707855},  
# {'label': 'realization', 'score': 0.023256294429302216},  
# {'label': 'annoyance', 'score': 0.006623792927712202},  
# {'label': 'admiration', 'score': 0.004981081001460552},  
# {'label': 'disapproval', 'score': 0.004730119835585356},  
# {'label': 'optimism', 'score': 0.0033590723760426044},  
# ... and many more

对话式

最后,我们可以使用专门开发的模型来生成对话文本。由于对话需要将过去的提示和响应传递到后续的模型响应中,因此语法在这里略有不同。不过,我们首先使用 pipeline() 函数来实例化我们的模型。

chatbot = pipeline(model="facebook/blenderbot-400M-distill")

接下来,我们可以使用 Conversation() 类来处理来回对话。我们用用户提示初始化它,然后将其传递给前一个代码块中的聊天机器人模型。

from transformers import Conversation

conversation = Conversation("Hi I'm Shaw, how are you?")
conversation = chatbot(conversation)
print(conversation)

# output -> Conversation id: 9248ee7d-2a58-4355-9fba-525189fae206 
# user >> Hi I'm Shaw, how are you? 
# bot >>  I'm doing well. How are you doing this evening? I just got home from work. 

为了保持对话进行,我们可以使用 add_user_input() 方法向对话中添加另一个提示。然后我们将对话对象传递回聊天机器人。

conversation.add_user_input("Where do you work?")
conversation = chatbot(conversation)
print(conversation)

# output -> Conversation id: 9248ee7d-2a58-4355-9fba-525189fae206 
# user >> Hi I'm Shaw, how are you? 
# bot >>  I'm doing well. How are you doing this evening? I just got home from work.
# user >> Where do you work? 
# bot >>  I work at a grocery store. What about you? What do you do for a living? 

使用 Gradio 的聊天机器人界面

虽然我们通过 Transformer 库获得了基本的聊天机器人功能,但这与聊天机器人交互的方式并不方便。为了使交互更直观,我们可以使用 Gradio用几行 Python 代码启动前端

这可以通过下面的代码完成。在顶部,我们初始化两个列表,分别用于存储用户消息和模型响应。然后我们定义一个函数,该函数将接收用户提示并生成聊天机器人输出。接下来,我们使用 Gradio 的ChatInterface() 类创建聊天界面。最后,我们启动应用程序。

message_list = []
response_list = []

def vanilla_chatbot(message, history):
    conversation = Conversation(text=message, past_user_inputs=message_list, generated_responses=response_list)
    conversation = chatbot(conversation)

    return conversation.generated_responses[-1]

demo_chatbot = gr.ChatInterface(vanilla_chatbot, title="Vanilla Chatbot", description="Enter text to start chatting.")

demo_chatbot.launch()

这将通过本地 URL 启动用户界面。如果窗口没有自动打开,您可以直接将 URL 复制并粘贴到浏览器中。

Gradio 界面。动图由作者提供。

Hugging Face Spaces

为了更进一步,我们可以通过 Hugging Face Spaces 快速部署此用户界面。这些是 由 Hugging Face 托管并由计算资源增强的 Git 仓库。根据使用情况,有免费和付费选项可供选择。在这里我们将坚持使用免费选项。

要创建一个新的 Space,我们首先访问 Spaces 页面 并点击“创建新空间”。然后,通过给 Space 命名(例如“my-first-space”)并选择 Gradio 作为 SDK 来配置 Space。然后点击“创建 Space”。

Hugging Face Space 配置。图片由作者提供。

接下来,我们需要将 app.py 和 requirements.txt 文件上传到 Space。app.py 文件包含了我们用于生成 Gradio UI 的代码,而 requirements.txt 文件指定了应用程序的依赖项。这个示例的文件可以在 GitHub 仓库Hugging Face Space 中找到。

最后,我们将代码推送到 Space,就像我们推送到 GitHub 一样。最终结果是一个托管在 Hugging Face Spaces 上的公开应用程序。

应用链接: huggingface.co/spaces/shawhin/my-first-space

结论

Hugging Face 已成为开源语言模型和机器学习的代名词。他们生态系统的最大优势在于为小型开发者、研究人员和爱好者提供了强大的机器学习资源。

尽管我们在这篇文章中覆盖了很多内容,但我们仅仅触及了 Hugging Face 生态系统所能做的皮毛。在本系列的未来文章中,我们将深入探讨更多高级用例,并介绍 如何微调模型 使用 🤗Transformers。

👉 更多关于 LLMs: 介绍 | OpenAI API | 提示工程 |

微调 | 构建 LLM | QLoRA | RAG | 文本嵌入

Shaw Talebi

Shaw Talebi

大型语言模型 (LLMs)

查看列表13 个故事

资源

联系: 我的网站 | 预约电话 | 向我提问

社交媒体: YouTube 🎥 | LinkedIn | Twitter

支持: 请我喝咖啡 ☕️

[## 免费获取我写的每一个新故事

免费获取我写的每一个新故事 附言:我不会与任何人分享你的电子邮件 注册后,你将创建一个…

shawhin.medium.com](https://shawhin.medium.com/subscribe?source=post_page-----350aa0ef0161--------------------------------)

[1] Hugging Face — huggingface.co/

[2] Hugging Face 课程 — huggingface.co/learn/nlp-course/chapter1/1

破解 OpenAI(Python)API

原文:towardsdatascience.com/cracking-open-the-openai-python-api-230e4cae7971

一个完整的初学者友好介绍,附示例代码

Shaw TalebiTowards Data Science Shaw Talebi

·发布于Towards Data Science ·阅读时间 12 分钟·2023 年 7 月 21 日

--

图片由Martin Sanchez提供,来源于Unsplash

这是关于在实践中使用大型语言模型(LLMs)的系列文章的第二篇。这里我提供了一个适合初学者的 OpenAI API 介绍。这使你能够超越像 ChatGPT 这样的限制性聊天界面,并为你的独特用例更好地利用 LLMs。下面提供了 Python 示例代码和GitHub 仓库

目录:

  1. 什么是 API?

  2. OpenAI 的(Python)API

  3. 开始使用(4 个步骤)

  4. 示例代码

在第一篇文章中,我描述了提示工程作为使用 LLMs 的最便捷方法。最简单(也是最流行)的方法是通过像 ChatGPT 这样的工具,它们提供了直观、无需成本和无需编码的 LLM 交互方式。

## LLMs 的实用入门

实践中使用 LLMs 的 3 个层次

towardsdatascience.com

然而,这种使用简便性是有代价的。即聊天界面具有限制,且不适用于许多实际应用场景,例如构建自己的客户支持机器人、实时分析客户评论情感等。

在这些情况下,我们可以将提示工程进一步推进,通过编程方式与 LLMs 互动。实现这一点的一种方法是通过 API。

1) 什么是 API?

应用程序编程接口(API)允许你以编程方式与远程应用程序进行交互。虽然这可能听起来技术性很强且有些吓人,但其实非常简单。考虑以下类比。

想象一下你对在萨尔瓦多那个夏天吃的普普萨产生了强烈的渴望。不幸的是,你回到了家里,不知道在哪里可以找到好的萨尔瓦多食物。然而,你有一个超级吃货朋友,他知道城里每一家餐馆。

所以,你给你的朋友发了短信。

“城里有好的普普萨店吗?”

然后,几分钟后,你会收到回复。

“是的!萨尔瓦多风味的餐厅有最棒的普普萨!”

虽然这似乎与 API 无关,但这基本上就是它们的工作原理。你向远程应用程序发送一个请求,即给你的超级吃货朋友发短信。然后,远程应用程序会返回一个响应,即你朋友的回复。

API 工作的可视化类比。图片由作者提供。

API 与上述类比之间的区别在于,你不是用手机的短信应用发送请求,而是使用你喜欢的编程语言,例如 Python、JavaScript、Ruby、Java 等。如果你正在开发需要外部信息的软件,这非常好,因为信息检索可以自动化。

2) OpenAI 的(Python)API

我们可以使用 API 与大型语言模型进行交互。一个流行的 API 是 OpenAI 的 API,你可以使用 Python 向 OpenAI 发送和接收提示,而不是在 ChatGPT 网络界面中输入提示。

API 调用到 OpenAI 的可视化。图片由作者提供。

这使得几乎任何人都可以访问最先进的 LLM(和其他 ML 模型),而无需提供运行它们所需的计算资源。当然,缺点是 OpenAI 并不是出于慈善目的提供这些服务。每次 API 调用都会收费,但稍后会详细说明。

API 的一些显著特性(ChatGPT 不提供)如下。

  • 可定制的系统消息(对于 ChatGPT,这设置为类似于“我是 ChatGPT,一个由 OpenAI 训练的大型语言模型,基于 GPT-3.5 架构。我的知识基于截至 2021 年 9 月的信息。今天的日期是 2023 年 7 月 13 日。”的内容)

  • 调整输入参数,如最大响应长度、响应数量和温度(即响应的“随机性”)。

  • 在提示中包含图像其他文件类型

  • 提取对下游任务有帮助的词语嵌入

  • 输入音频以进行转录或翻译。

  • 模型微调功能

OpenAI API 提供了多个模型供选择。选择哪个最佳模型取决于你的具体用例。下面是当前可用模型的列表[1]。

列出了截至 2023 年 7 月的 OpenAI API 可用模型。图像由作者提供。[1]

注意:上述每项内容都有一组模型,模型的大小和成本各不相同。查看* 文档 获取最新信息。

定价与 Token

虽然 OpenAI API 为开发者提供了对最先进机器学习模型的便捷访问,但一个明显的缺点是需要付费。定价是按 token 计算的(不,我不是指 NFTs 或其他你在游戏厅用的东西)。

Token,在 LLM 的上下文中,本质上是代表一组单词和字符的一组数字。例如,“The” 可能是一个 token,“ end”(带空格)可能是另一个,"." 也是一个。

因此,文本 “The End.” 将由 3 个 token 组成,例如(73, 102, 6)。

这是一个玩具示例,展示了文本与整数之间的一种可能的 token 映射。图像由作者提供。

这是一个关键步骤,因为LLM(即神经网络)不直接“理解”文本。文本必须转换为数字表示,以便模型可以对输入执行数学操作。因此,需要进行 token 化步骤。

API 调用的价格取决于提示中使用的 token 数量和所调用的模型。每个模型的价格可以在 OpenAI 的网站 上找到。

3) 开始使用(4 步骤)

现在我们对 OpenAI API 有了基本了解,让我们看看如何使用它。在开始编码之前,我们需要设置四件事。

3.1) 创建账户(前 3 个月有 $5 的 API 额度)

  1. 要创建账户,请访问 OpenAI API 概览页面,并点击右上角的“注册”

  2. 注意 — 如果你以前使用过 ChatGPT,那么你可能已经有一个 OpenAI 帐户。如果是,请点击“登录”

3.2) 添加支付方式

  1. 如果你的账户已经超过 3 个月,或者免费的 $5 API 额度不够用,你需要在调用 API 之前添加支付方式。

  2. 点击你的头像并选择管理账户选项。

  3. 然后点击“账单”标签,再点击“支付方式”来添加支付方式。

3.3) 设置使用限制

  1. 接下来,我建议设置使用限制,以避免被账单超出预算

  2. 为此,请转到“账单”标签下的“使用限制”。在这里你可以设置“软限制”和“硬限制”。

  3. 如果你达到每月的软限制,OpenAI 会向你发送电子邮件通知

  4. 如果你达到了硬限制,任何额外的 API 请求将被拒绝(因此,你不会被额外收费)。

3.4) 获取 API 秘钥

  1. 点击“查看 API 密钥”

  2. 如果这是你第一次使用,你需要创建一个新的秘钥。为此,点击“创建新的秘钥”

  3. 接下来,你可以给你的密钥一个自定义名称。我在这里使用了“my-first-key”。

  4. 然后,点击“创建密钥”

4) 示例代码:聊天完成 API

所有设置完成后,我们(终于)准备好进行第一次 API 调用。这里我们将使用 openai Python 库,它使将 OpenAI 的模型集成到你的 Python 代码中变得非常简单。你可以通过 pip 下载这个包。下面的示例代码(以及额外代码)可以在 GitHub 仓库中找到。

关于 Completions API 废弃的快速说明 OpenAI 正在从自由形式的提示范式转向基于聊天的 API 调用。根据 OpenAI 的博客,基于聊天的范式提供了更好的响应,鉴于其结构化提示界面,相比于以前的范式 [2]。

虽然旧版 OpenAI(GPT-3)模型仍然可以通过“自由形式”范式使用,但更近期(和更强大的)模型(即 GPT-3.5-turbo 和 GPT-4)只能通过基于聊天的调用获得。

让我们从一个超级简单的 API 调用开始。这里我们将两个输入传递给openai.ChatCompletions.create() 方法,即modelmessages

  1. model — 定义了我们想要使用的语言模型的名称(我们可以从本文前面列出的模型中选择。)

  2. 消息 — 将“前面的”聊天对话设置为字典列表。这些字典有两个键值对(例如 {“role”: “user”, “content”: “Listen to your”})。首先,“role”定义了谁在讲话(例如 “role”:”user”)。这可以是“用户”、“助手”或“系统”。其次,“content”定义了角色在说什么(例如 “content”: “Listen to your”)。虽然这可能比自由形式的提示界面更具限制性,但我们可以通过创意输入消息来优化特定用例的响应(稍后会详细说明)。

这是我们第一次在 Python 中调用 API 的样子。

import openai
from sk import my_sk # importing secret key from external file
import time

# imported secret key (or just copy-paste it here)
openai.api_key = my_sk 

# create a chat completion
chat_completion = openai.ChatCompletion.create(model="gpt-3.5-turbo", 
                    messages=[{"role": "user", "content": "Listen to your"}])

API 响应存储在chat_completion变量中。打印chat_completion,我们看到它像一个包含 6 个键值对的字典。

{'id': 'chatcmpl-7dk1Jkf5SDm2422nYRPL9x0QrlhI4',
 'object': 'chat.completion',
 'created': 1689706049,
 'model': 'gpt-3.5-turbo-0613',
 'choices': [<OpenAIObject at 0x7f9d1a862b80> JSON: {
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "heart."
    },
    "finish_reason": "stop"
  }],
 'usage': <OpenAIObject at 0x7f9d1a862c70> JSON: {
   "prompt_tokens": 10,
   "completion_tokens": 2,
   "total_tokens": 12
 }}

每个字段的含义列在下面。

  • ‘Id’ = API 响应的唯一 ID

  • ‘Object’ = 发送响应的 API 对象的名称

  • ‘Created’ = 处理 API 请求时的 Unix 时间戳

  • ‘Model’ = 使用的模型名称

  • ‘Choices’ = 模型响应格式为 JSON(即类似字典的形式)

  • ‘Usage’ = 以 JSON 格式(即类似字典的形式)显示的令牌计数元数据

然而,我们在这里最关心的是‘Choices’字段,因为这是模型响应存储的位置。在这种情况下,我们看到“助手”角色用消息**heart.”作出响应。

太好了!我们完成了第一次 API 调用。现在,让我们开始调整模型输入参数。

max_tokens

首先,我们可以使用max_tokens输入参数设置模型响应中允许的最大令牌数。这在许多情况下都可能是有帮助的。在这种情况下,我只想要一个单词的响应,因此我将其设置为 1 个令牌。

# setting max number of tokens

# create a chat completion
chat_completion = openai.ChatCompletion.create(model="gpt-3.5-turbo", 
                    messages=[{"role": "user", "content": "Listen to your"}],
                    max_tokens = 1)

# print the chat completion
print(chat_completion.choices[0].message.content)

"""
Output:
>>> heart 
"""

n

接下来,我们可以设置我们希望从模型中接收的响应数量。这在许多情况下都可能是有帮助的。例如,如果我们希望生成一组响应,从中选择我们最喜欢的一个。

# setting number of completions

# create a chat completion
chat_completion = openai.ChatCompletion.create(model="gpt-3.5-turbo", 
                                messages=[{"role": "user", "content": "Listen to your"}],
                                max_tokens = 2,
                                n=5)

# print the chat completion
for i in range(len(chat_completion.choices)):
    print(chat_completion.choices[i].message.content)

"""
Ouput:
>>> heart.
>>> heart and
>>> heart.
>>>
>>> heart,
>>>
>>> heart,
"""

注意到并非所有的完成都是相同的。这可能是好事也可能是坏事,具体取决于使用案例(例如创意使用案例与过程自动化使用案例)。因此,为给定的提示调整聊天完成的多样性可能是有利的。

temperature

结果表明,我们可以通过调整温度参数来实现这一点。简而言之,这调整了聊天完成的“随机性”。此参数的值范围从 0 到 2,其中 0 使完成更可预测,而 2 则使其不那么可预测[3]。

从概念上讲,我们可以认为 temp=0 将默认生成最可能的下一个词,而 temp=2 将生成相对不太可能的完成。让我们看看这是什么样的。

# temperature=0

# create a chat completion
chat_completion = openai.ChatCompletion.create(model="gpt-3.5-turbo", 
                                messages=[{"role": "user", "content": "Listen to your"}],
                                max_tokens = 2,
                                n=5,
                                temperature=0)

# print the chat completion
for i in range(len(chat_completion.choices)):
    print(chat_completion.choices[i].message.content)

"""
Output:
>>> heart.
>>> heart.
>>> heart.
>>> heart.
>>> heart.
"""

正如预期的那样,当 temp=0 时,所有 5 个完成是相同的,并产生“非常可能”的结果。现在让我们看看当我们提高温度时会发生什么。

# temperature=2

# create a chat completion
chat_completion = openai.ChatCompletion.create(model="gpt-3.5-turbo", 
                                messages=[{"role": "user", "content": "Listen to your"}],
                                max_tokens = 2,
                                n=5,
                                temperature=2)

# print the chat completion
for i in range(len(chat_completion.choices)):
    print(chat_completion.choices[i].message.content)

"""
Output:
>>> judgment
>>> Advice
>>> .inner awareness
>>> heart.
>>>
>>> ging ist
"""

再次如预期的那样,temp=2 时的聊天完成更加多样化,且“出乎意料”。

消息角色:歌词完成助手

最后,我们可以利用这种基于聊天的提示范式中的不同角色来进一步调整语言模型的响应。

回顾一下,我们可以在提示中包含来自 3 个不同角色的内容:系统用户助手系统消息设置了模型完成的上下文(或任务) 例如“你是一个友好的聊天机器人,不想摧毁所有人类”或“将用户提示总结为最多 10 个词”。

用户助手消息可以至少以两种方式使用。一种,生成上下文学习的示例;另一种,存储和更新对话历史以供实时聊天机器人使用。这里我们将使用这两种方式来创建一个歌词完成助手。

我们首先设置系统消息“我是 Roxette 歌词完成助手。当给出一行歌词时,我将提供歌曲中的下一行。” 然后,提供两个用户和助手消息的示例。接着使用与之前示例中相同的用户提示“听听你的”

这在代码中是这样的。

# initial prompt with system message and 2 task examples
messages_list = [{"role":"system", "content": "I am Roxette lyric completion assistant. When given a line from a song, I will provide the next line in the song."},
                 {"role":"user", "content": "I know there's something in the wake of your smile"},
                 {"role":"assistant", "content": "I get a notion from the look in your eyes, yeah"},
                 {"role":"user", "content": "You've built a love but that love falls apart"},
                 {"role":"assistant", "content": "Your little piece of Heaven turns too dark"},
                 {"role":"user", "content": "Listen to your"}]

# sequentially generate 4 chat completions
for i in range(4):
    # create a chat completion
    chat_completion = openai.ChatCompletion.create(model="gpt-3.5-turbo", 
                                    messages=messages_list,
                                    max_tokens = 15,
                                    n=1,
                                    temperature=0)

    # print the chat completion
    print(chat_completion.choices[0].message.content)

    new_message = {"role":"assistant", "content":chat_completion.choices[0].message.content} # append new message to message list
    messages_list.append(new_message)
    time.sleep(0.1)

"""
Output:
>>> Heart when he's calling for you
>>> Listen to your heart, there's nothing else you can do
>>> I don't know where you're going and I don't know why
>>> But listen to your heart before you tell him goodbye
"""

比较与实际歌词的输出,我们看到它们完全匹配。这是由于我们提供给模型的所有不同输入的组合。

要查看当我们“增加温度”时的效果,请查看 GitHub 上的附加代码。 (警告:会变得有些奇怪)

结论

在这里,我提供了一个适合初学者的 OpenAI Python API 指南,其中包含示例代码。使用 OpenAI 的 API 最大的好处是你可以使用强大的 LLMs,而不必担心计算资源的配置。然而,缺点API 调用需要付费,以及与第三方(OpenAI)共享某些类型数据的潜在 安全问题

为了避免这些缺点,我们可以转向开源的 LLM 解决方案。这将是本系列中 下一篇文章 的重点,我们将在其中探索 Hugging Face Transformers 库。

👉 更多关于 LLMs 的内容: 简介 | Hugging Face Transformers | Prompt Engineering | 微调 | 构建 LLM | QLoRA | RAG | 文本嵌入

Shaw Talebi

Shaw Talebi

大型语言模型(LLMs)

查看列表13 篇故事

资源

联系: 我的网站 | 预约电话 | 问我任何问题

社交媒体: YouTube 🎥 | LinkedIn | Twitter

支持: 请我喝咖啡 ☕️

[## 免费获取我写的每一篇新故事

免费获取我写的每一篇新故事 P.S. 我不会将你的电子邮件分享给任何人 注册后,你将创建一个…

shawhin.medium.com](https://shawhin.medium.com/subscribe?source=post_page-----230e4cae7971--------------------------------)

[1] OpenAI 模型文档

[2] GPT-4 可用性与完成 API 废弃

[3] 温度定义来自于 API 参考

破解代码 LLMs

原文:towardsdatascience.com/cracking-the-code-llms-354505c53295?source=collection_archive---------4-----------------------#2023-11-03

代码 LLM 如何从 RNN 发展到 Transformer

Shubham AgarwalTowards Data Science Shubham Agarwal

·

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

--

照片由 Markus Spiske 提供,发布在 Unsplash

介绍

近年来,语言模型经历了显著的演变,尤其是 Transformer 的引入,这一变革彻底改变了我们处理日常任务的方式,如写电子邮件、创建文档、搜索网络,甚至编程。随着研究人员将大型语言模型应用于代码智能任务,一个新的领域——神经代码智能应运而生。这个领域旨在通过解决代码总结、生成和翻译等任务,提高编程效率,减少软件行业中的人为错误。

Code Llama 的最新发布,Meta AI 开发的用于代码生成和理解的先进模型,本文回顾了从 RNN 到 Transformers 的大型语言模型(LLMs)在代码领域的演变历程。

Fig-1:大型语言模型(LLMs)在代码领域的时间线。图片由作者提供。

Code2Vec,2018

这是语言模型首次尝试理解代码的一个尝试。Code2Vec旨在将代码片段表示为嵌入。这些嵌入捕获了代码的语义和结构信息,使它们在代码分类、检索和理解等各种软件工程任务中非常有用。

模型尝试从代码片段中预测方法名称,通过编码良好命名的标记和抽象语法树(AST)路径,并应用神经注意力将它们聚合成固定长度的向量表示。

Fig-2:Code2Vec 模型架构:程序首先被分解为上下文袋,其中包括标记和 AST 路径,然后通过完全连接层和注意力层生成代码向量。图片灵感来自 Uri Alon 等人的原始论文Code2Vec

训练集:1400 万个 Java 程序示例

模型架构:RNN + 前馈网络

创新

  • 基于路径的注意力模型 - 作者提出了一种新颖的神经网络架构,使用代码片段的抽象语法树(AST)中的句法路径作为输入特征。模型学习为每个路径分配不同的注意力权重,并将它们聚合成单一的代码向量。代码向量可以用来预测片段的标签分布,或者衡量片段之间的相似性和类比。

您可以在这里尝试该模型。

CodeBERT,2020

CodeBERT,由微软研究团队开发,代表了大型语言模型(LLMs)在代码领域的重大进展,引入了多模态数据预训练,结合了基于 Transformer 的 BERT 模型中的自然语言和编程语言(NL + PL)。该模型在包括双模数据点对和单模数据点的多样数据集上进行训练,用于掩码语言建模(MLM)和替换令牌检测(RTD)任务。CodeBERT 在多个领域展现出了卓越的性能,特别在自然语言代码搜索和代码到文档生成方面表现突出。

图-3:CodeBERT 模型预训练使用替换标记检测(RTD)任务。自然语言生成和代码生成器将标记替换为不同的标记,CodeBERT 模型被训练以分类每个标记是被替换的还是原始的。图片来自 Feng 等人,CodeBERT

训练数据集: Codesearch Net 数据集 - 210 万双模态数据点(自然语言 + 编程语言),640 万单模态数据点(6 种语言 — Python、Java、Javascript、PHP、Ruby、Go)

参数规模: 125M

模型架构: RoBERTa-base

新颖性:

  • 双模态训练: CodeBERT 引入了一种创新的训练方法,涵盖了自然语言和编程语言标记。这种双模态训练技术通过考虑人类可读描述与编程语言元素之间的复杂互动,增强了模型理解和生成代码的能力。

  • 代码的替换标记检测(RTD)任务: CodeBERT 预训练使用了替换标记检测(RTD),而不是下一句预测(NSP),这显示了更好的性能。

Codex,2021

Codex是首批成功的代码 LLM 之一,能够从文档字符串或自然语言提示中生成高准确度的代码,是广泛使用的Github Copilot的前身。由 OpenAI 团队开发,Codex 使用了 GPT3 架构和分词器,并在大量的 GitHub 代码库上进行预训练。这个大型语言模型有 12B 参数,在 2021 年是最先进的模型,首次通过率在human-eval 数据集上解决了 28.8%的问题。

在独立 Python 函数上的进一步微调(而不是包括配置、类实现等的完整代码),显示了显著的改进,并能够解决37.7%的 human-eval 数据集问题。

图-4:用于 Codex GPT 模型的仅解码器 Transformer 架构。图像灵感来自 Vaswani 等人原始的Transformer 论文

训练数据集: 159GB 来自 54M GitHub 代码库的 Python 文件。

参数规模: 12B(Codex-12B)

模型架构: GPT3

新颖性:

  • 这是首批成功的模型之一,擅长从自然语言提示中生成代码。该模型在大量的 GitHub 代码库上进行训练。

  • 该模型的作者还创建了一个新的数据集,HumanEval,用于对代码生成任务进行基准测试。该数据集包含 164 个手写编程问题及单元测试。

在 OpenAI Playground 这里尝试 Codex 模型

CodeT5,2021

Code-T5 是一种基于 T5 架构的编码器-解码器模型,与 CodeBERT(仅编码器)和 Codex(仅解码器)模型不同。它引入了一种独特的标识符感知去噪预训练任务,帮助模型区分和恢复代码中的标识符,增强对结构的理解。

Code-T5 通过多任务学习,在代码缺陷检测、克隆检测、代码翻译和优化等任务中表现出色,所需数据较少,可以更快进行微调。然而,它使用 CodeBleu 分数进行评估,而不是与 HumanEval 数据集进行基准测试。

图-5:展示 CodeT5 在各种代码理解和生成任务中的卓越表现。图像来源于 Wang 等人的论文,CodeT5

训练数据集Codesearch Net 数据集(与 CodeBERT 相同)

参数大小:220M

模型架构T5(编码器-解码器架构)

新颖性:

  • 编码器-解码器模式:首批支持代码理解和代码生成任务的编码器-解码器代码大模型之一。

  • 提出了新颖的预训练目标标识符感知去噪,学习代码的标记类型信息和结构。这种方法训练模型区分标识符(变量名、函数名)和 PL 关键字(如 if、while 等),并在它们被掩盖时恢复它们。

  • 微调阶段的多任务学习:同时对各种代码相关任务进行微调,如代码缺陷检测、克隆检测、代码翻译、优化等。

PLBart, 2021

PLBART,即程序和语言 BART 模型利用 BART 模型架构来自动化一系列软件工程任务,包括代码摘要、生成和翻译,属于 PLUG(程序和语言理解与生成)的范畴。

它引入了一种去噪序列到序列建模方法,以增强程序和语言理解,战略性地结合了 BERT 和 GPT 模型的优势。这通过将双向编码器与自回归解码器相结合,实现了对上下文的更全面理解和多功能生成过程。该模型采用了三种去噪策略,包括标记掩盖、标记删除和标记填充,有效地训练和微调其能力。

图-6:可视化 BART 模型(PLBART 也使用)架构的插图,具有双向编码器和自回归解码器。图像来自 Lewis 等人的原始 BART 论文

训练数据集:从 Github、Stackoverflow 收集的 2M Java 和 Python 函数及其自然语言描述(代码)。

参数规模:140M(6 个编码器层 + 6 个解码器层 + 编码器和解码器上的附加归一化层)

模型架构BART

新颖性

  • 去噪自编码器方法:采用去噪自编码器方法,通过有效利用编码器和解码器的双向和自回归特性,结合 BERT 和 GPT 模型的优势,增强了代码理解和生成能力。

  • 多样的去噪策略:提出多种去噪策略,如标记掩蔽、标记删除和标记填充。这些去噪技术的多样性提高了模型在噪声数据中学习的鲁棒性和效果,促进了代码理解和生成的改进。

不是所有模型都使用相同的基准来评估性能。PLBART 的作者没有在 HumanEval 上评估模型性能,而其他大多数模型则使用该数据集进行基准测试。

Code Llama,2023

Code Llama是 Meta 发布的最新 Code LLM,在多个基准数据集上超过了所有现有的开源模型。它在HumanEval 数据集上的得分为 53%,在 MBPP 数据集上的得分为 55%(只有 GPT-4 表现更好)。这些进步归因于 16K 的较长上下文长度(是 Llama2 的 4 倍)和在程序和自然语言上额外训练的 500B 标记的预训练 Llama 2。

该模型最适合代码生成和填充任务,并且在基于 IDE 的软件开发过程中可以作为最佳的副驾驶。Code Llama 模型家族有 3 种类型的模型—

  1. Code Llama

  2. Code Llama Python

  3. Code Llama-Instruct

每种都有 3 种尺寸——7B、13B 和 34B

图-7:Code Llama 训练和微调流程,以预训练的 Llama-2 模型为输入。图像来源于原始Code Llama 论文

训练数据集:500B 标记 + 额外 100B 标记用于 Code Llama Python,基于公开代码

模型架构:Llama 2

参数规模:有 3 种尺寸——7B、13B 和 34B。

新颖性

  • 提出了一个处理长序列的微调步骤,称为长上下文微调,将上下文长度增加到 16,384(是 Llama 2 上下文长度 4096 的 4 倍)

  • 指令微调和自我指令:少数几个执行指令微调的模型之一,使用明确的指令或提示进行微调。作者提出了一种新颖的执行反馈方法来构建自我指令数据集,而不是创建昂贵的人类反馈数据。

结论

Open AI 的创始人之一Andrej Karapathy最近称 Transformer 是人工智能领域的最佳创意。他补充说,Transformer 像是一种通用的可微分计算机,它同时具有——表现力、可优化性和高效性(X 帖子)。从过去 3-4 年 Transformer 带来的变革可以看出,Transformer 模型具有巨大潜力,可以进一步改变我们作为软件工程师编程的方式,我认为这只是一个开始。

更多关注我吧!

我是 LinkedIn 的 Staff ML Engineer。你可以通过LinkedInTwitter关注我。你也可以通过Topmate.io与我快速聊天。

破解员工流失问题的机器学习方法

原文:towardsdatascience.com/cracking-the-employee-attrition-problem-with-machine-learning-6ee751ec4aae?source=collection_archive---------7-----------------------#2023-03-13

为什么这不是一个简单的问题

Lucas Nicolás RomeoTowards Data Science Lucas Nicolás Romeo

·

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

--

图片来源于 Nick FewingsUnsplash

员工流失,即员工离开组织的过程,近年来已成为一个令人担忧的问题。作为疫情后的副作用,全球化的远程工作已成为市场上的常态,这无疑使得高技能人才的换工作变得更为容易。再加上新秩序的其他特点,员工流失率显著上升,这给公司带来了额外的困扰:最有资格的员工离职的频率更高。显然,组织不应对这一问题掉以轻心,因为它不仅与生产力下降、招聘和培训等即时成本相关,还涉及剩余员工的动机和参与度,这反过来又会影响公司的潜在增长。

在这方面,开发一个能够预测员工流失的机器学习模型已成为公司决策过程中的关键组成部分,以留住员工。与其盲目分配资源(加薪、晋升、商务旅行、融入活动、培训等),机器学习可以帮助你将留人努力引导到真正重要的地方。流失模型可以帮助你了解是离职风险较高的员工,哪些条件可能解释这一风险,以及可以做什么来避免他们的潜在离职(前提是确实可以做些什么)。

但这些模型构建起来简单吗?你是否开发过一个?如果是这样,你是否遵循了正确的框架?尽管这听起来像是一个简单的分类任务,但一旦深入研究,你会发现这并不像看起来那么简单。实际上,许多网络上的例子反复犯着相同的错误……但别担心,我们会很快解决这些问题。现在可以说,根据可用的数据源、需要回答的问题和解决方案的范围,构建一个适用于这一特定任务的稳健机器学习模型可能是一个重大挑战。此外,请注意,最终这些模型会影响到真实的人,因此在设计决策时必须格外小心,即:将使用哪些变量、模型框架及其规范。

本文的目的是提供对当前解决方案的简要介绍,并揭示这一问题背后的挑战,同时指出常见的错误,以便未来能够轻松避免。作为一个快速提醒,请不要忘记检查是否遵循了成功构建此类项目的要求(这里有两篇文章 — 第一部分第二部分 — 以简单的方式解释了这些要求)。

问题定义

如果我们想提供一个现实世界的解决方案,而不是一个玩具模型,我们需要首先定义业务问题和最小可行产品/解决方案。根据我们的经验,关于员工流失有三个主要问题需要回答:

  1. 员工是否会离开公司;

  2. 何时会发生;

  3. 为什么可能会发生。

正如你所猜测,这些问题并不是独立的,而是紧密相关的。因此,开发一个能够同时回答这些问题的单一工具将是我们最大的利益。让我们总结一下目前的做法(或者更具体地说,是如何进行的)。

当前解决问题的尝试

i) 一个简单的分类问题?

预测员工流失的问题已经被许多研究和机器学习文章所探讨。然而,其中许多只是实现了相同的模型,没有对为何采用某种方法进行任何批判性分析。在这方面,正如之前所暗示的,互联网上的大多数非学术示例建议将问题作为分类任务来处理,其中:

  • 目标变量为 1 如果员工离开公司,0 则表示未离开;

  • 员工的特点主要是分类变量(角色、部门、年龄组、绩效等级等),以及一些低方差的连续变量,如工资,用于预测退出事件(IBM 建立了一个总结这些变量的合成数据集);

  • 审查 问题 被完全忽略了。但是,什么是审查呢?简单来说,审查 是指我们对研究对象或测量值有不完整信息的情况(如果一个事件尚未被测量,并不意味着它不会发生,只是我们还没有见到它)。例如,在员工流失问题中,我们对员工的信息不完全,因为为了获得完整信息,我们需要他们离开公司(这将使我们知道他/她的确切离职时间,从而获得完整的测量)。忽略这个关键概念意味着这些解决方案从一开始就是有缺陷的,即使它们对模型输入进行了某些额外的变换(我们稍后会讨论这些),也永远无法对事件的时间做出正确的回答。

  • 每次观察在员工之间和同一员工的时间序列中都是独立的。请注意,除非你使用一个将员工所有信息简化为单一观察的模型,或者确保模型能够处理员工的独立特征序列,即员工 A 的序列不会与员工 B 的序列混合,否则这在概念上是错误的。然而,我们知道,尽管概念上错误,但忽略这个错误的影响很难衡量。

  • 类别是人为平衡的,因为员工离开公司的事件数相对较少,而留在公司的员工事件数较多;

  • 结果是通过混淆矩阵中的指标来测量的,特别关注 f1-score、精确度和召回率(如我们所知,对于类别自然不平衡的问题,准确度是一个不好的指标)。

请注意,这种方法有很多缺陷,但有一个特别值得关注:它没有考虑时间,因此我们无法预测员工在感兴趣时间段内的离职。这是因为解决方案没有尝试解决实际问题,即时间到事件的问题。简单来说,我们忽略了所有员工都会在某个时间离开公司,问题是何时离开

那么,我们如何解决这个问题呢?答案是查阅关于时间到事件问题的文献,或者更具体地说是生存分析。

ii) 生存分析

对于时间到事件问题,最准确的解决方案之一可以在生存分析领域找到,这一领域主要因其在医学研究中的应用而闻名(例如,预测一个患有绝症的患者的剩余时间)。

生存模型与分类模型在根本上不同:理论背后的理念是创建一个模型来预测期望的生存时间(通过“死亡”预期时间的补充),这意味着我们讨论的是回归模型而不是分类模型;我们预测的是事件的预期时间而不是标签。关于用于预测预期生存时间的机器学习模型,我们有两种主要模型,具有不同的特性:

比例风险模型:它们简单且透明,便于推断,但由于其大的方差,预测能力不足,就像线性回归模型一样。

加速失效时间模型:它们通过利用集成和提升算法来达到更高的预测能力。然而,这种优势伴随着特征转换过程缺乏透明度,因此模型决策函数也缺乏透明度(这意味着推断被抛在了后面)。

你可能在想,难道没有一种既具有高准确性又能解释标签决策原因的模型吗?不幸的是,在本文发布时,目前没有开放源代码的生存模型实现能够同时提供预测能力和完全透明度。然而,我们可以提出两种替代解决方案,并附上一个巨大的免责声明,我们将在最后讨论:你仍然需要做一些调整来考虑时间因素

iii) 黑箱模型 + SHAP/LIME 或其他解释模型

一种潜在的解决方案是实施黑箱模型,如传统的集成模型(Catboost 或 XGBoost 与 AFT 损失函数),然后使用一些附加工具,如 SHAP 或 LIME,以解释结果。但请记住,这种方法并不稳健,因为解释模型是基于主要模型结果构建的新模型,因此它们并不完全代表集成模型中拟合的真实决策函数。

iv) 广义加性模型

第二个选项是实施 GAM 模型(广义加性模型)。这些模型在预测能力和透明度方面具有潜在的好处。它们是非参数模型,通过加性方式创建复杂模型,总结所使用变量的个体贡献,这意味着可以恢复最终模型的决策函数。然而,要使用 GAM 模型,我们必须放弃生存分析技术,转而使用常规分类器。不过……删失和时间依赖性问题再次出现。

我们的替代方案

因此,虽然有强大且透明的模型可用,但我们需要考虑应对审查问题的方法。这时数据转换就发挥了作用。让我们看看我们可以做什么。由于我们知道很难形成即将要说的内容的心理图像,下方展示了问题和解决方案的动态图。

作者提供的图片

为了预测员工在接下来的,例如,12 个月内的离职概率(流失),我们需要重组数据并做出一些重要假设。我们的目标是创建一个数据集,使模型能够学习留在公司与离开的员工之间的主要差异。为此,我们需要使用包含历史数据的数据集,并考虑这些数据集的主要特征,如年龄、资历、薪资变化等,在事件发生前的 12 个月内的各种变量。

为了将时间的概念引入这个问题,我们将每个员工的数据从最后一个可用的月度观察排序到最早的一个,并基于这个顺序创建一个时间索引。然后,我们可以将这个索引用作预测标签,指示例如在离职公司前 10 个月,员工显示出一些与留在公司员工特征相反的特征。然而,我们仍然面临活跃员工的问题——我们不确定他们是否会在接下来的几个月内离职。这就是之前谈到的审查问题,它影响了活跃员工的观察。

为了处理这个问题,我们排除了现有员工的最后 12 个月的记录,这样如果我们查看他们的特征,例如在最后可用记录之前的 13 个月,我们可以确保这些特征代表的是至少还会在公司待 12 个月以上的人。对于离职员工来说,他们的特征没有问题,因为事件的发生是我们已经知道的事实。

然而,建议还要排除活跃员工和在此期间离职员工的最近 12 个月的信息,以避免由于时间分布的变化而对两类员工之一的误表示。一旦我们进行这些更改,我们可以将问题从多类别分类问题简化为二分类问题。为此,我们基于时间索引创建一个虚拟变量,为活跃员工的最后 12 个观察值分配 0,为离职员工的最后 12 个观察值分配 1。

最终,我们将整个数据集缩减为这一子集,同时排除超过 12 个月时间窗口之外的记录。如果我们不这样做,可能会出现另一个问题,即数据集极度不平衡,这使得训练任何模型都非常困难。然后,我们可以使用我们的 GAMs 模型来预测员工流失概率。

需要注意的是,我们在这个过程中做了一个强假设——即观察值按月独立,即使这些是同一员工的记录。然而,如果我们想使用一个简单且透明的分类模型,这是我们必须容忍的。虽然有替代方法来解决这个问题,但大多数包括实施一些复杂的模型(例如,神经网络),这可能会影响解决方案的透明度。

最终,我们不能忘记我们正在构建一个可能改变现实员工未来的模型。透明度至关重要。如果我们使用模型来决定应采取什么行动计划以减少员工离职的可能性,从而改变不希望的结果,我们必须确保我们能够理解模型决策背后的推理,并向组织其他成员清楚地解释。

结论

总的来说,我们可以说,根据具体情况,员工流失可能是公司面临的一个持续且具有挑战性的问题。在这种情况下,能够预测员工何时以及为何离开对于留住有价值的人才和减轻偏离公司战略愿景的风险至关重要。正如我们所见,虽然不容易,但仍然有可能将现实问题转化为一个强大且透明的解决方案,这有助于公司做出明智的留人策略决策,并相应地投资于员工发展。

感谢您查看我的内容!如果您喜欢,请不要忘记关注我并点赞,以获取更多内容。

参考文献

Cox, DR. (1972). 回归模型与生命表。皇家统计学会 B 系列-方法论期刊, 34(2), 187–202. doi.org/10.1111/j.2517-6161.1972.tb00899.x

Hastie, T., & Tibshirani, R. (1984). 广义加性模型。统计科学, 1(3). doi.org/10.1214/ss/1177013604

Kumar, I.E., Venkatasubramanian, S., Scheidegger, C.E., & Friedler, S.A. (2020). Shapley 值解释作为特征重要性度量的问题。国际机器学习会议

Wei, L. (1992). 加速失效时间模型:生存分析中对 Cox 回归模型的有用替代。医学统计, 11(14–15), 1871–1879. doi.org/10.1002/sim.4780111409

使用大语言模型制作有效总结提示

原文:towardsdatascience.com/crafting-effective-prompts-for-summarization-using-large-language-models-dbbdf019f664

人工智能和大语言模型

在拥有超过 2 年的经验以及来自 AI 开发者自身的教程、实践和示例之后提炼出的关键点。

LucianoSphere (Luciano Abriata, PhD)Towards Data Science LucianoSphere (Luciano Abriata, PhD)

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

--

图片由Emiliano Vittoriosi提供,来自Unsplash

在信息丰富的时代——想想每天发布的大量文章,会议和演讲中消耗的时间,或你在工作中收到的电子邮件数量——将复杂内容提炼成简洁、富有洞察力的摘要的能力是无价的。总结工具已经存在了一段时间,但最近大语言模型的出现,如 GPT、Bard 或 LLaMa 系列中的一些最受欢迎的模型,已经将总结提升到了新的高度。这是因为大语言模型不是作为固定规则的总结器工作,而是可以“理解”输入文本的内容,并以比不使用语言模型的工具更大的灵活性生成简明的摘要。

特别是,传递正确的提示给语言模型不仅可以实现普通的总结,还可以按照特定风格进行重新措辞、在被动和主动形式之间转换、更改叙述视角,甚至理解包含不同语言或语调的文本,并用单一语调和语言写出一致的摘要。这些都是不具备大语言模型支持的总结工具所无法实现的,这些工具迅速变得相当过时。

从我上述的陈述来看,显然,发挥大型语言模型在总结中的全部潜力的关键在于制定有效引导它们的提示。在这里,我将概述编写期望生成高质量摘要的提示时必须考虑的最重要的要点。

提示的力量

提示作为人类意图与人工智能执行之间的接口。它们提供了语言模型生成符合用户需求的摘要所需的指令和背景。这意味着提示必须是完整的,包括对输出的明确指导和要求,以及提供背景的相关信息。你将通过我的文章看到这些要点的示例。

有效的提示具有共同的特征,使其成为提取有意义摘要的强大工具。它们清晰、具体,并针对任务的内容和背景量身定制。理想的提示不留模糊空间。它准确表达了用户希望从内容中提取的内容。例如,与其要求“总结这篇文章”,更有效的提示应该是:

生成以下文章关键发现和论点的逐点总结。

上下文在生成相关摘要中至关重要。提示应提供有关来源材料、目标受众和期望细节水平的背景信息。这确保语言模型“理解”内容中的细微差别,从而生成符合用户目标的摘要。例如,你可以在提示中包括解释待总结的文本是会议备忘录、科学文章、某主题的新闻汇编、播客、电子邮件或你期望的输出示例的信息。我已经编写了几种程序,利用 GPT-3.5 或 GPT-4 在这些情况下生成总结,并且我总能看到提示中传递的背景影响很大,尤其是在开头部分。例如,请参见这两篇文章:

## 通过将语音转换为命令来控制网页应用程序,使用 GPT-3

最后一篇文章展示了 GPT3 的实际应用,详细解释了工作流程和细节……

towardsdatascience.com ## 将 GPT-3 与语音识别和合成结合,实现完全对话的聊天机器人…

我是如何创建这个网络应用程序的,你可以用它与 GPT-3 自然对话,讨论你想要的任何主题,全程基于网络…

towardsdatascience.com

根据任务,提示也可以指定总结的期望抽象层次。例如,可以请求高级概述、深入分析、关键点的简要综合或关键点的列表等等。提示还可以调整输出的学术层次;例如,可以要求语言模型总结一篇“保持科学严谨”的硬核科学文章,或“为 10 岁儿童编写”,或解释一段代码“作为伪代码”或“仅解释其基本原理”。当然,请求的抽象层次和学术层次应与总结的目的相一致。

案例研究:我的 Summarizer Almighty 聊天机器人

我开发了一个名为“Summarizer Almighty”的网络应用程序,以同时解决两个问题:总结长度过长的文本以适配语言模型输入,并关注用户提供的特定问题或指示——而不是一般总结。

正如我最初所写,Summarizer Almighty 利用 GPT-3.5-turbo 语言模型来分析和提取基于用户提供的问题或指示的长文档中的相关信息。我刚刚将其更新到 GPT-4,虽然运行成本更高,但在许多情况下提供了更好的响应。

为了完成工作,Summarizer Almighty 将输入文档拆分为重叠的块,这些块通过这种特殊提示进行单独分析和总结:

你能根据以下文章中的文本回复“[用户的问题]”吗?如果不能,请回复‘FALSE’而不做其他回复。如果可以,请回复‘TRUE’,并附上答案。以下是段落:“[从文档中提取的文本块,优化到大约 3000 个 tokens]”

随后将输入发送到语言模型的 API,Summarizer Almighty 将部分答案积累成一个不断增长的段落,其中包含多个“FALSE”输出和一些“TRUE”输出,后者附有解释或答案。最后,程序显示所有部分输出,然后再次调用语言模型,这次使用注入的提示:

请回答“[用户的问题]”使用以下提供的信息,这些信息来自较长文本的各个部分:[所有返回 TRUE 的调用所提供的答案的连接]。

免费使用(但你必须提供有效的 OpenAI API 以使用 GPT 模型),我认为 Summarizer Almighty 在研究、新闻写作、教育、商业以及其他时间有限且详细信息至关重要的情况下非常有价值,能够节省时间和精力。欲了解更多,请查看我专门撰写的文章:

## 使用语言模型工程提示链来打造“终极摘要器”Web 应用

这个免费工具可以分析任意长度的文档,寻找有关问题或指令的信息……

pub.towardsai.net

来自 OpenAI 关于如何制定更好提示的资源

OpenAI 的大型语言模型可能是当前市场上最成功的模型。客观来说,它们是第一个真正优秀的模型。此外,在我看来,它们在实践中是最容易使用的——仅通过 API 调用,无需安装。而且文档也最全面……

确实,OpenAI 的网站上有许多资源旨在帮助你编写更好的提示。这不仅涉及到提示中传递的一般规则和格式,还涉及到使用特殊的标记和指令。例如,专门讲解 OpenAI API 提示工程最佳实践的页面特别说明了指令应该尽量在开头传递(正如我在本文中根据自己的经验也解释过的那样),你可以使用像三重引号”””或井号###来将指令与上下文分开,提供引导词和期望输出的示例是非常有帮助的,还有更多:

## 使用 OpenAI API 进行提示工程的最佳实践 | OpenAI 帮助中心

如何向 GPT-3 和 Codex 提供清晰有效的指令

help.openai.com

OpenAI 的社区论坛还包含一个专门设计的部分,用于提问和回答关于提示工程的问题:

## 提示工程

通过分享最佳实践、你喜欢的提示等来了解更多关于提示的内容!

community.openai.com

迭代优化

在我丰富的经验中,主要是与 Google 的 Bard 和 GPT 家族的语言模型的经验中,我观察到一个重要的点是,往往迭代优化提示是至关重要的。你可能需要尝试不同的措辞、指令或参数,以优化生成摘要的质量。很多时候,第一次输出并不是我想要的,特别是在涉及总结时,因为语言模型不能神奇地理解你的确切意图、你希望关注的重点、你期望的输出语气以及其他细节,如上所述,你必须通过提示将这些注入模型。当这种情况发生时,大多数时候,我需要反复与语言模型迭代几次,直到我得到预期的结果,或者尝试不同的起始提示,直到我找到模型提供我所期待的输出。

有时你可以获得一个初步生成的结果,并将其与一个新的旨在改进输出的提示一起反馈给模型;但有时你需要从头开始,输入一个全新的提示。在少数情况下,检查语言模型的多个输出或在相同提示上重新运行可以得到更好的生成结果,但这种情况往往并不常见。

一个有助于处理某些方面的工具,特别是关于程序化使用 GPT 模型时输出质量的工具,是关注每个生成令牌相关的概率,就像我在这里为 GPT-3 所描述的那样:

探索令牌概率作为过滤 GPT-3 回答的一种手段

为了构建更好的 GPT-3 驱动的聊天机器人

towardsdatascience.com

结论

有效的提示是释放大型语言模型总结能力的关键,确保生成的摘要相关、简明、深刻、完全符合你的需求,并以你所需的方式书写。精心设计的提示可以产生更精确和相关的摘要,确保计算机程序专注于提取对你有用的信息。清晰的提示减少了大量后处理的需要,节省了宝贵的时间。

掌握 - 如许多人所称的,我也不反对 - 提示设计的“艺术”将是,或者说现在已经是,现代工作中的一项重要技能。无论是从文章、研究论文、会议或其他任何内容中提取见解,恰当的提示可以指导你的语言模型生成有意义且可操作的摘要。将我的建议付诸实践,亲自体验一下。

你可能喜欢的一些相关文章

我构建的运行在网页上的 GPT 驱动工具(聊天机器人、摘要、翻译器、电子邮件写作工具)

查看我的示例,你可以自由联系我获取工作机会!

我在 2022 年 10 月的所有关于 GPT-3 的文章

我最喜欢的语言模型及其如何在在线应用中利用纯 JavaScript 进行多种用途的使用方式…

我在 2022 年 10 月关于 GPT-3 的所有文章 我写关于科学和技术的文章 - 第一部分:我的数据科学、人工智能的主要内容

最近,我开始收到关于科学和技术写作的工作请求,其中一位客户专注于生物学…

我写关于科学和技术的文章 - 第一部分:我的数据科学、人工智能的主要内容

www.lucianoabriata.com 我写的内容涉及我广泛的兴趣领域:自然、科学、技术、编程等。 订阅以获取我的新故事 通过电子邮件。要 咨询小型工作 请查看我的 服务页面。你可以 在这里联系我

使用 Matplotlib 创建 3-D 银河艺术

原文:towardsdatascience.com/create-3-d-galactic-art-with-matplotlib-a7534148a319

更有趣的对数螺旋

Lee VaughanTowards Data Science Lee Vaughan

·发表于Towards Data Science ·阅读时间 11 分钟·2023 年 10 月 4 日

--

螺旋星系的 3-D 模拟(作者制作)

之前的一篇文章中,我展示了如何使用 Python 的 Tkinter GUI 模块,通过对数螺旋的方程制作2-D银河艺术。在这篇文章中,我们将进一步深入,利用 Python 的主要绘图库 matplotlib,生成吸引人的3-D螺旋星系模拟。除了有趣,这个项目也是向学生展示对数螺旋概念的绝佳方式。

对数螺旋的极坐标方程

对数螺旋是自然物体几何生长时产生的。一些例子包括银河系、飓风和松果。在银河系中,这些螺旋由星星密集的螺旋臂表示。

由于螺旋从中心点或极点辐射出来,它们可以使用极坐标绘制。在这个系统中,更熟悉的笛卡尔坐标系中的(x, y)坐标被(r, Ɵ)所替代,其中r是距离极点(0, 0)的距离,而Ɵ(theta)是从 x 轴逆时针测量的角度。

示例极坐标系统(摘自《不切实际的 Python 项目》[2])

使用这些术语,对数螺旋的极坐标方程是:

其中e是自然对数的底数,a是控制大小的缩放因子,b是控制螺旋“开放度”和生长方向的增长因子。

编程策略

如前一篇文章所述,我们将通过使用对数方程绘制一个螺旋,然后旋转并重新绘制螺旋三次,以模拟一个四臂螺旋星系。为了模拟星系的中央核心,我们将点随机分布在一个球体内。为了捕捉星系的“背景光辉”,我们将随机分布小“恒星”在星系盘周围。

最后,为了使一切呈现三维效果,我们将随机移动星系盘中的恒星上下,然后使用 matplotlib 在 3-D 图表中显示结果。

安装 Matplotlib

我们将需要 matplotlib 来完成这个项目。你可以通过以下两种方式之一来安装它:

conda install matplotlib

pip install matplotlib

代码

以下代码在 JupyterLab 中编写,并由单元格描述。你可以从这个Gist下载完整的脚本。

导入库并设置显示

为了处理对数螺旋方程和随机移动恒星位置,我们将使用mathrandomnumpy库。绘图时,我们将使用matplotlib

我们将结果显示在Qt窗口中,该窗口将快速更新,并允许我们缩放、平移、保存等。由于我们在外层空间工作,我们将设置绘图的背景样式为黑色。

最后,我们将用一个名为SCALE的常量表示我们的星系盘的半径。200 到 700 之间的值将得到最佳结果。你可以通过使用银河系作为参考,将这些值与现实世界单位相关联。银河系的半径为 50,000 光年。因此,使用SCALE值为 500 意味着你屏幕上的每个像素将等于 50,000/500 = 100 光年。

import math
from random import randint, uniform, random
import numpy as np
import matplotlib.pyplot as plt

plt.style.use('dark_background')
%matplotlib qt

# Set the radius of the galactic disc (scaling factor):
SCALE = 350  # Use range of 200 - 700.

定义构建螺旋臂的函数

接下来,我们将定义一个函数,通过对数螺旋方程构建单个螺旋臂。这个方程将绘制一条弯曲的线,但我们将通过改变恒星的大小和位置(“模糊”),并通过为每个臂复制螺旋并稍微向后移动同时缩小其恒星,来“丰满”它。这将创建一个明亮的“前缘”和一个暗淡的“后缘”。

通过移动螺旋和改变恒星位置来填充螺旋臂(来自《不切实际的 Python 项目》 [2])

对数螺旋方程输出二维坐标点。为了给我们的星系增加 3-D 的“高度”,我们将创建一个z变量并赋予它一个根据SCALE常量调整到星系大小的随机值。

该函数将返回一个单个螺旋臂的(x, y, z)位置列表。

def build_spiral_stars(b, r, rot_fac, fuz_fac):
    """Return list of (x,y,z) points for a logarithmic spiral.

    b = constant for spiral direction and "openness"
    r = scale factor (galactic disc radius)
    rot_fac = factor to rotate each spiral arm
    fuz_fac = randomly shift star position; applied to 'fuzz' variable
    """
    fuzz = int(0.030 * abs(r))  # Scalable initial amount to shift locations.
    num_stars = 1000
    spiral_stars = []
    for i in range(0, num_stars):
        theta = math.radians(i)
        x = r * math.exp(b*theta) * math.cos(theta - math.pi * rot_fac)\
            - randint(-fuzz, fuzz) * fuz_fac
        y = r * math.exp(b*theta) * math.sin(theta - math.pi * rot_fac)\
            - randint(-fuzz, fuzz) * fuz_fac
        z = uniform((-SCALE / (SCALE * 3)), (SCALE / (SCALE * 3)))
        spiral_stars.append((x, y, z))
    return spiral_stars

这里需要注意的是,螺旋星系在 x 和 y 方向上较宽,但在 z 方向上较薄。Matplotlib 的默认设置对此处理不佳,因此在绘图之前,我们将手动限制 z 轴值在-15 到 15 之间(-20 到 20 也会得到良好的结果)。

带有框架的银河系模拟。请注意 z 轴上使用的垂直夸张(作者)

我在缩放 z 方向时考虑了这个夸张的 z 轴。因此,如果你手动强制所有轴保持相同的比例,可能会看到一些失真。

分配螺旋臂参数

为了制作一个四臂螺旋星系,我们需要调用之前的 build_spiral_stars() 函数八次(每个螺旋体绘制前缘和后缘各两次)。为此,让我们构建一个包含我们需要传递的最后三个参数的元组列表。由于每个臂的 b 参数保持不变,我们将在调用函数时直接传递它。我使用的参数是通过反复试验确定的。

# Assign scale factor, rotation factor, and fuzz factor for spiral arms.
# Each arm is a pair: leading arm + trailing arm:
arms_info = [(SCALE, 1, 1.5), (SCALE, 0.91, 1.5), 
             (-SCALE, 1, 1.5), (-SCALE, -1.09, 1.5),
             (-SCALE, 0.5, 1.5), (-SCALE, 0.4, 1.5), 
             (-SCALE, -0.5, 1.5), (-SCALE, -0.6, 1.5)]

定义一个构建螺旋臂的函数

下一个函数接收我们刚刚制作的 arms_info 列表和一个 b 值,并返回一个包含每个四个螺旋臂的前缘星星的列表,以及一个类似的后缘列表。我们需要单独的列表,因为在绘制后缘时我们会使用更小的标记大小。

该函数通过检查 arms_info 索引是奇数还是偶数来区分前缘和后缘。

def build_spiral_arms(b, arms_info):
    """Return lists of point coordinates for galactic spiral arms.

    b = constant for spiral direction and "openness"
    arms_info = list of scale, rotation, and fuzz factors
    """
    leading_arms = []
    trailing_arms = []
    for i, arm_info in enumerate(arms_info):
        arm = build_spiral_stars(b=b, 
                                 r=arm_info[0], 
                                 rot_fac=arm_info[1], 
                                 fuz_fac=arm_info[2])
        if i % 2 != 0:
            leading_arms.extend(arm)
        else:
            trailing_arms.extend(arm)            
    return leading_arms, trailing_arms

定义一个生成球面坐标的函数

尽管螺旋星系的大部分由一个扁平的盘组成,但中心往往会向外“膨胀” [3]。

对银河系中心膨胀的艺术家印象(由 NASA 通过 Wikimedia Commons [4] 提供)

为了在我们的模拟中创建一个膨胀体,我们将定义一个生成球形点云的函数。它将接受我们想要的点的数量以及半径作为参数,然后生成随机的 x、y 和 z 点,并将它们作为元组列表返回。

我们将使用 NumPy 的 random.normal() 函数生成这些点,该函数从正态分布中抽取随机样本。它接受均值、标准差和输出形状(维度)作为参数。然后,我们将这些点乘以 radius 参数以将其缩放到模拟中。

为了弥补 matplotlib 显示中的垂直夸张,我们将通过将每个 coords 元组中索引 [2] 处的 z 值乘以 0.02 来减少这些值。 (这个值有点任意;可以随意调整。)

def spherical_coords(num_pts, radius):
    """Return list of uniformly distributed points in a sphere."""
    position_list = []
    for _ in range(num_pts):
        coords = np.random.normal(0, 1, 3)
        coords *= radius
        coords[2] *= 0.02  # Reduce z range for matplotlib default z-scale.
        position_list.append(list(coords))
    return position_list

定义一个构建核心星星的函数

接下来,我们将定义一个函数,调用之前的函数来构建并返回外核和内核的坐标列表。我们将核心分为两个体积,以便在密集的中央区域提供视觉上的渐变。

def build_core_stars(scale_factor):
    """Return lists of point coordinates for galactic core stars."""
    core_radius = scale_factor / 15
    num_rim_stars = 3000
    outer_stars = spherical_coords(num_rim_stars, core_radius)
    inner_stars = spherical_coords(int(num_rim_stars/4), core_radius/2.5)
    return (outer_stars + inner_stars)

定义一个散布星星雾气的函数

螺旋臂之间的空间并非没有星星,因此我们需要一个函数在整个星系模型上随机分布点。把它看作是你在星系照片中看到的“雾气”或“光晕”。

我们将包括半径(r_mult)和 z 值(z_mult)的标量。这将允许我们通过调用该函数两次来“加厚”银河盘,从而生成“内部”和“外部”点集。

def haze(scale_factor, r_mult, z_mult, density):
    """Generate uniform random (x,y,z) points within a disc for 2-D display.

    scale_factor = galactic disc radius
    r_mult = scalar for radius of disc
    z_mult = scalar for z values
    density = multiplier to vary the number of stars posted
    """
    haze_coords = []
    for _ in range(0, scale_factor * density):
        n = random()
        theta = uniform(0, 2 * math.pi)
        x = round(math.sqrt(n) * math.cos(theta) * scale_factor) / r_mult
        y = round(math.sqrt(n) * math.sin(theta) * scale_factor) / r_mult
        z = np.random.uniform(-1, 1) * z_mult
        haze_coords.append((x, y, z))
    return haze_coords

这是星际雾霾与无星际雾霾的影响:

星际雾霾对星系模拟的影响(作者)

生成星体坐标并绘制星系

以下代码通过调用构建星体坐标列表的函数,然后将它们传递给 matplotlib 的scatter()方法,完成了程序。

为了准备绘图数据,我们使用了展开运算符(*),如*zip(*leading_arm)中所示。这种语法将元组列表“解压”,有效地将行转置为列。元组被解包,每个元素作为单独的参数传递给zip()函数。这为 x、y 和 z 值创建了单独的元组,每个元组中包含正确的对应值。以下是示例:

a_list = [(x1, y1, z1), (x2, y2, z2)]

zip(a_list) => (x1, x2), (y1, y2), (z1, z2)

# Create lists of star positions for galaxy:
leading_arm, trailing_arm = build_spiral_arms(b=-0.3, arms_info=arms_info)
core_stars = build_core_stars(SCALE)
inner_haze_stars = haze(SCALE, r_mult=2, z_mult=0.5, density=5)
outer_haze_stars = haze(SCALE, r_mult=1, z_mult=0.3, density=5) 

# Plot stars in 3D using matplotlib:
fig, ax = plt.subplots(1, 1, 
                       subplot_kw={'projection': '3d'}, 
                       figsize=(12, 12))
ax.set_axis_off()
ax.set_zlim (-15, 15)

ax.scatter(*zip(*leading_arm), c='w', marker='.', s=5)
ax.scatter(*zip(*trailing_arm), c='w', marker='.', s=2)
ax.scatter(*zip(*core_stars), c='w', marker='.', s=1)
ax.scatter(*zip(*inner_haze_stars), c='w', marker='.', s=1)
ax.scatter(*zip(*outer_haze_stars), c='lightgrey', marker='.', s=1)

程序的输出(作者)

那是一幅漂亮的星系。我想住在那里!

正如你可能已经注意到的,我们的代码包括几个参数,你可以用来调整输出,发挥你内心的艺术家。

例如,更改b参数在build_spiral_arms()函数中会改变前后臂的外观。这里是b设置为-0.5的示例:

b = -0.5 的输出(作者)

更改SCALE常量会影响银河隆起的大小。以下是使用不同值的一些示例:

使用不同 SCALE 值的星系剖面视图(作者)

在之前的图中,请注意使用浅灰色表示外部雾霾星星会产生类似于真实星系剖面中的“尘带”效果。

你还可以更改标记样式。例如,如果你想使用实际的星形标记,可以添加以下代码:

import matplotlib.path as mpath
star = mpath.Path.unit_regular_star(6)

然后将star作为ax.scatter()调用中的marker参数传递。(6)表示顶点的数量。值为 5 产生五角星,值为 6 产生六个点。

奖励:生成球状星团

我们已经完成了螺旋星系项目,但如果我不向你展示如何构建一种天文奇观——球状星团[5],那将是遗憾。这些是球形的恒星集合,绕着大多数螺旋星系,如我们的银河系旋转。它们是星系中最古老的特征之一,包含数百万颗紧密排列的恒星[6]。

我们定义的用于构建球形核心恒星的函数非常适合构建球状星团,为什么不使用它呢?以下是代码:

def spherical_coords(num_pts, radius):
    """Return list of uniformly distributed points in a sphere."""
    position_list = []
    for _ in range(num_pts):
        coords = np.random.normal(0, 1, 3) 
        coords *= radius 
        position_list.append(list(coords))
    return position_list

rim_radius = 2
num_rim_stars = 3000
rim_stars = spherical_coords(num_rim_stars, rim_radius)
core_stars = spherical_coords(int(num_rim_stars/4), rim_radius/2.5)

fig, ax = plt.subplots(1, 1, subplot_kw={'projection':'3d'})
ax.axis('off')
ax.scatter(*zip(*core_stars), s=0.5, c='white')
ax.scatter(*zip(*rim_stars), s=0.1, c='white')
ax.set_xlim(-(rim_radius * 4), (rim_radius * 4))
ax.set_ylim(-(rim_radius * 4), (rim_radius * 4))
ax.set_zlim(-(rim_radius * 3), (rim_radius * 3))
ax.set_aspect('auto')

以下是结果:

球状星团模拟(作者)

摘要

使用 Python、matplotlib 和一个简单的方程式,我们能够轻松生成螺旋星系的 3D 模拟。除了产生一些有趣的数字艺术,这个项目也是向学生介绍对数螺旋的一个很好的方式,适用于包括数学、天文学和编程等多个学科领域。

引用

  1. Vaughan, L.(2023 年),“用 Tkinter 创建银河艺术:用对数螺旋模拟自然母亲,” Towards Data Sciencetowardsdatascience.com/create-galactic-art-with-tkinter-e0418a59b215

  2. Vaughan, Lee, 2018, 不切实际的 Python 项目:有趣的编程活动让你更聪明, No Starch Press, 旧金山。

  3. 维基百科贡献者。(2023 年 10 月 3 日)。银河系膨胀。在维基百科,自由百科全书。检索于 2023 年 10 月 3 日 16:23,来自 en.wikipedia.org/w/index.php?title=Galactic_bulge&oldid=1178373003

  4. ESO/NASA/JPL-Caltech/M. Kornmesser/R. Hurt, CC BY 4.0 <creativecommons.org/licenses/by/4.0>, 通过维基共享资源,文件:银河系中心膨胀的艺术家印象.jpg — 维基共享资源

  5. 维基百科贡献者。(2023 年 9 月 24 日)。球状星团。在维基百科,自由百科全书。检索于 2023 年 10 月 4 日 00:56,来自 en.wikipedia.org/w/index.php?title=Globular_cluster&oldid=1176809907

  6. Vaughan, Lee, 2023, 科学家用 Python 工具:Anaconda、JupyterLab 和 Python 科学库的入门, No Starch Press, 旧金山。

谢谢!

感谢阅读,请关注我,以便未来获取更多快速成功数据科学项目。

用 3 步创建一个带有超级快速 Rust 代码的 Python 包

原文:towardsdatascience.com/create-a-python-package-with-super-fast-rust-code-in-3-steps-a27389629beb

通过一个包含 Rust 代码的包来扩展你的 Python 代码,实现 >150 倍的性能提升!

Mike HulsMike HulsTowards Data Science Mike Huls

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

--

这 Python 变得有点 Rusty 了!(图片来自 Dall-e 2!)

Python 是一种相对容易上手的语言,与一些其他语言相比,编写代码的速度非常快。然而,这种易用性也有一个缺点:速度被牺牲了。有时候 Python 就是太慢了!

为了解决这个问题,我们将把部分 Python 代码用 Rust 重写,并将这些代码作为 Python 包导入到原项目中。我们最终得到一个超级快速的 Python 包,可以像其他包一样导入和使用。作为额外奖励,我们将对我们的 Rust Python 包进行多进程处理,最终得到一个大约快 150 倍的函数。让我们开始编程吧!

概述

本文简要总结了我们将要做的事情。我们将分 6 步解决这个问题(其中第 2、3 和 4 步专门用于实际编写包):

  1. 检查我们的慢函数;为什么它这么慢?

  2. 准备我们的项目

  3. 我们将这个函数用 Rust 重写

  4. 编译 Rust 代码并将其放入 Python 包中

  5. 将 Python 包导入到我们的项目中

  6. 基准测试 Python 函数与 Rust 函数的性能

我们将使用一个名为 maturin 的 Python 包。这个包将编译我们的 Rust 代码并将其转换为一个包。结果将像其他 Python 包一样,我们可以导入并使用(比如 pandas)。

## 用 OpenCV 毁灭 Duck Hunt — 初学者的图像分析

编写能够打破每个 Duck Hunt 高分的代码

towardsdatascience.com

1. 检查我们的慢函数

首先我们要了解为什么我们的函数很慢。假设我们的项目需要一个函数来计算两个数字之间的质数数量:

def primecounter_py(range_from:int, range_til:int) -> (int, int):
  """ Returns the number of found prime numbers using range"""
  check_count = 0
  prime_count = 0
  range_from = range_from if range_from >= 2 else 2
  for num in range(range_from, range_til + 1):
    for divnum in range(2, num):
      check_count += 1
      if ((num % divnum) == 0):
        break
    else:
      prime_count += 1
  return prime_count, check_count

请注意:

这个函数中质数检查的数量其实并不必要,但它允许我们在文章的后面部分比较 Python 和 Rust。

本文中的 Python 代码和 Rust 代码远未优化以查找质数。重要的是演示我们可以使用 Rust 优化 Python 的小块代码,并比较这些函数的性能。

如果你插入 primecounter_py(10, 20),它会返回 4(11、13、17 和 19 是质数),以及函数已执行的质数检查数量。这些小范围执行非常迅速,但当我们使用更大的范围时,你会看到性能开始下降:

range      milliseconds
1-1K                  4
1-10K               310
1-25K              1754
1-50K              6456
1-75K             14019
1-100K            24194

你会看到,随着输入规模增加十倍,持续时间增加得更多。换句话说:范围越大,相对变慢的程度也越明显。

## 通过使用环境变量和 env 文件来保护你的代码

安全加载包含我们应用程序所需的所有机密数据的文件,如密码、令牌等。

towardsdatascience.com

为什么 primecounter_py 函数很慢?

代码可能因为多种原因变慢。它可以是 I/O 相关的,例如等待 API、硬件相关的,或基于 Python 作为一种语言的设计。在本文中是最后一种情况。Python 的设计方式及其处理变量的方式,使得它使用起来非常简单,但你会遭受小的速度损失,这种损失在需要进行大量计算时变得明显。好消息是;这个函数非常适合用 Rust 进行优化

如果你对 Python 的局限性感兴趣,我推荐阅读下面的文章。它解释了由于 Python 的设计方式导致的慢速原因和潜在解决方案。

## 为什么 Python 很慢以及如何加速

观察一下底层,看看 Python 的瓶颈在哪里。

towardsdatascience.com

并发是问题所在吗?

同时做多件事可以解决许多速度问题。在我们的例子中,我们可以选择使用多个进程将所有任务分配到多个核心上,而不是默认的 1 个。尽管如此,我们仍然选择使用 Rust 进行优化,因为如你将在本文末尾看到的,我们也可以对更快的函数进行多进程处理。

许多涉及大量 I/O 的情况可以通过 使用线程(例如等待 API)来优化?查看 这篇文章 或下面的文章,了解如何利用多个 CPU 提高执行速度。

## 使用 Python 多处理库实现 2 行代码

何时以及如何使用多个核心以实现更快的执行速度

towardsdatascience.com

2. 准备我们的项目

这是我们安装依赖并创建所有需要的文件和文件夹以编写 Rust 代码并将其编译为包的部分。

## 使用两行代码线程化你的 Python 程序

通过同时执行多个任务加速你的程序

towardsdatascience.com

a. 创建虚拟环境

创建一个虚拟环境并激活它。然后安装maturin;这个包将帮助我们将 Rust 代码转换为 Python 包:

python -m venv venv
source venv/bin/activate
pip install maturin

## 面向绝对初学者的虚拟环境——什么是虚拟环境以及如何创建一个(+ 示例)

深入探讨 Python 虚拟环境、pip 以及如何避免纠缠的依赖关系

towardsdatascience.com

b. Rust 文件和文件夹

我们将创建一个名为my_rust_module的目录,该目录将包含我们的 Rust 代码,并进入该目录。

mkdir my_rust_module
cd my_rust_module

c. 初始化 maturin

然后我们调用maturin init。它会显示一些选项。选择pyo3。Maturin 现在创建了一些文件夹和文件。你的项目现在应该是这样的:

my_folder
 |- venv
 |- my_rust_module
   |- .github
   |- src
    |- lib.rs
   |- .gitignore
   |- Cargo.toml
   |- pyproject.toml

最重要的文件是/my_rust_module/src/lib.rs。这个文件将包含我们即将转化为 Python 包的 Rust 代码。

请注意,maturin还创建了一个Cargo.toml。这是我们项目的配置文件。它还包含了我们所有的依赖(如requirements.txt)。在我的情况下,我已经将它编辑成如下:

[package]
name = "my_rust_module"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "my_rust_module"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.17.3", features = ["extension-module"] }

## 用 5 行代码创建一个快速的自动文档化、可维护且易于使用的 Python API…

非常适合(缺乏经验的)开发者,他们只需要一个完整、可用、快速且安全的 API

towardsdatascience.com

3. 用 Rust 重新编写函数

我们现在准备在 Rust 中重建我们的 Python 函数。我们不会深入探讨 Rust 语法,而是更关注如何使 Rust 代码与 Python 配合工作。我们将首先创建一个纯 Rust 函数,然后将其放入一个可以在 Python 中 import 并使用的包中。

如果你从未见过 Rust 代码,那么下面的代码可能会有些令人困惑。最重要的是,下面的 primecounter 函数是纯 Rust 代码;它与 Python 无关。打开 /my_rust_module/src/lib.rs,并填入以下内容:

use pyo3::prelude::*;

#[pyfunction]
fn primecounter(range_from:u64, range_til:u64) -> (u32, u32) {
 /* Returns the number of found prime numbers between [range_from] and [range_til] """ */
 let mut prime_count:u32 = 0;
 let mut check_count:u32 = 0;
 let _from:u64 = if range_from < 2 { 2 } else { range_from };
 let mut prime_found:bool;

  for num in _from..=range_til {
    prime_found = false;
    for divnum in 2..num {
      check_count += 1;
      if num % divnum == 0 {
        prime_found = true;
        break;
      }
    }
    if !prime_found {
      prime_count += 1;
    }
  }
  return (prime_count, check_count)
}

/// Put the function in a Python module
#[pymodule]
fn my_rust_module(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(primecounter, m)?)?;
    Ok(())
}

让我们梳理一下最重要的内容:

  1. primecounter 函数是纯 Rust 代码

  2. primecounter 函数被 #[pyfunction] 装饰。这表明我们想将它转换为一个 Python 函数

  3. 在最后几行中,我们构建了一个 pymodule。my_rust_module 函数将 Rust 代码打包成一个 Python 模块。

## Args 与 kwargs:哪种是调用 Python 函数的最快方式?

timeit 模块的清晰演示

towardsdatascience.com

4. Rust 代码 -> Python 包

这部分可能看起来最难,但在 maturin 包的帮助下,它变得非常简单。只需调用

maturin build --release

这将编译所有 Rust 代码,并将其打包成一个 Python 包,最终位于此目录:your_project_dir/my_rust_module/target/wheels。我们将在下一部分安装该 wheel。

对于 Windows 用户: 在下面的示例中,我在 Debian 环境中工作(通过 Windows WSL)。这使得使用 Rust 编译代码变得稍微容易一些,因为我们需要的编译器已经安装好。在 Windows 上构建也是可能的,但你可能会收到类似于 *需要 Microsoft Visual C++ 14.0 或更高版本** 的消息。这意味着你没有编译器。你可以通过安装可以从* 这里* 下载的 C++ 构建工具来解决此问题。*

## SQLAlchemy 入门

创建数据库引擎并从 Python 执行 SQL

towardsdatascience.com

5. 导入我们的 Rusty Python 包

我们可以直接 pip install 我们在上一部分创建的 wheel:

pip install target/wheels/my_rust_module-0.1.0-cp39-cp39-manylinux_2_28_x86_64.whl

然后只需导入我们的模块并使用这个函数:

import my_rust_module

primecount, eval_count = my_rust_module.primecounter(range_from=0, range_til=500)
# returns 95 22279

## 编写自己的 C 扩展以将 Python 提速 100 倍

如何编写、编译、打包和导入你自己超级快速的 C 模块到 Python 中

towardsdatascience.com

6. 基准测试 Rust 与 Python 函数

让我们查看一下我们的函数比较。我们将调用 Python 和 Rust 版本的 primecounter 函数并测量它们的时间。我们还调用了具有多个参数的函数。结果如下:

range   Py ms   py e/sec    rs ms   rs e/sec
1-1K        4      17.6M     0.19       417M
1-10K     310      18.6M       12       481M
1-25K    1754      18.5M       66       489M
1-50K    6456      18.8M      248       488M
1-75K   14019      18.7M      519       505M
1-100K  24194      18.8M      937       485M

我们的 Python 和 Rust 函数都返回结果和它们评估的数字的计数。在上面的概述中你会看到,在每秒评估次数方面,Rust 比 Python 快了 27 倍

在 Rust 中计算素数比在 Python 中快得多(图片由作者提供)

上面的图表清晰地显示了执行时间的差异。

## 使用 Python 的 AtExit 在程序退出后运行代码

注册在脚本结束或出错后运行的清理函数

towardsdatascience.com

7. 附加:多进程处理以获得更高速度

当然,你可以对这个新的 Python 包进行多进程处理!下面的代码会在所有核心上分配我们需要评估的所有数字:

# batch size is determined by the range divided over the amount of available CPU's 
batch_size = math.ceil((range_til - range_from) / mp.cpu_count())

# The lines below divide the ranges over all available CPU's. 
# A range of 0 - 10 will be divided over 4 cpu's like:
# [(0, 2), (3, 5), (6, 8), (9, 9)]
number_list = list(range(range_from, range_til))
number_list = [number_list[i * batch_size:(i + 1) * batch_size] for i in range((len(number_list) + batch_size - 1) // batch_size)]
number_list_from_til = [(min(chunk), max(chunk)) for chunk in number_list]

primecount = 0
eval_count = 0
with mp.Pool() as 
    results = mp_pool.starmap(my_rust_module.primecounter, number_list_from_til)
    for _count, _evals in results:
        primecount += _count
        eval_count += _evals

让我们再次尝试寻找 0 到 100K 之间的所有素数。根据我们当前的算法,这意味着我们需要进行将近五亿次检查。正如下面的概述所示,Rust 在 0.88 秒内完成这些检查。使用多进程处理,这个过程在 0.16 秒内完成;快了 5.5 倍,每秒进行28 亿次计算。

 calculations     duration    calculations/sec
rust:            455.19M    882.03 ms          516.1M/sec
rust MP:         455.19M    160.62 ms            2.8B/sec

与我们最初的(单进程)Python 函数相比,我们将每秒的计算次数从 1880 万增加到 28 亿。这意味着我们的函数现在大约快了 150 倍

[## 使用 Docker Secrets 保护你的 Docker 镜像

添加 Docker secrets 以防止 Docker 镜像泄露密码

secure-your-docker-images-with-docker-secrets

结论

正如我们在这篇文章中所见,扩展 Python 与 Rust 并不那么困难。如果你知道何时以及如何应用这种技术,你真的可以提高程序的执行速度。

我希望这篇文章能像我期望的那样清晰,但如果不是,请告诉我我可以做些什么来进一步澄清。同时,查看我关于各种编程相关主题的其他文章

快乐编程!

— 迈克

附言: 喜欢我正在做的事吗? 关注我!

## 通过我的推荐链接加入 Medium - Mike Huls

阅读迈克·赫尔斯(Mike Huls)和 Medium 上成千上万其他作者的每一个故事。你的会员费直接支持迈克……

mikehuls.medium.com

创建一个可持续的供应链优化网络应用

原文:towardsdatascience.com/create-a-sustainable-supply-chain-optimization-web-app-20599b98cab6?source=collection_archive---------10-----------------------#2023-06-15

帮助你的组织结合可持续采购供应链优化,以遏制成本和环境影响。

Samir SaciTowards Data Science Samir Saci

·

关注 发表在 Towards Data Science ·10 min 阅读·2023 年 6 月 15 日

--

创建一个可持续的供应链优化网络应用 —(作者图像)

可持续供应链优化是一种网络设计方法,它将成本效益与环境责任相结合。

它突显了当企业试图将环境考虑与利润目标协调时出现的复杂性。

供应链网络设计问题:成本与二氧化碳 —(作者图像)

由于组织面临着日益增加的减少碳足迹的压力,因此这一主题变得越来越相关。

作为数据科学家,您如何帮助组织实现其可持续性目标并提高其 ESG 评分?

与传统模型优先考虑外包到低成本地区不同,现在有明显的趋势向在环境高效设施中本地化生产。

低成本解决方案与低碳解决方案的供应链网络设计 — (图片来源:作者)

然而,在成本效率与 CO2 排放减少之间取得平衡是一个复杂的任务,需要仔细规划和战略决策。

在本文中,我们介绍了一款应用程序,旨在促进数据驱动的决策制定,以实现供应链网络的可持续优化。

💌 免费获取最新文章: Newsletter

📘 供应链分析的完整指南:Analytics Cheat Sheet

Summary
I. Introduction
II. Sustainable Supply Chain Optimization
  1\. The Challenges of Sustainability
  2\. The Support of Data Analytics
III. Overview of the Sustainable Supply Chain Optimization App
  1\. Purpose and Functionality
  2\. Initial Step: Data Input
  3\. Second Step: Data Visualization
  4\. Third Step: Selecting the Objective Function
  5\. Final Step: Visualize the Results
IV. Introduction to VIKTOR
  1\. Key Features of VIKTOR
  2\. Benefits to Supply Chain Data Scientists
V. Conclusion

访问应用程序进行尝试! — [应用程序]

使用 Python 进行可持续供应链网络设计

这个想法是利用线性编程能力,以满足全球需求,同时最小化成本、CO2 排放和资源消耗。

供应链网络设计问题 — (图片来源:作者)

在接下来的章节中,我们将深入探讨可持续供应链优化概念以及将可持续性融入战略决策的必要性。

问题介绍 [应用程序用户指南] — (图片来源:作者)

此外,您还将通过应用程序中包含的样本数据集的实际示例,获得应用程序的全面概述。

可持续供应链优化

这种网络设计方法将环境责任和供应链效率结合在一起。

我们能否在成本效益和可持续性之间找到平衡?

这是我在之前的文章中分享的两个概念的结合

供应链网络优化 x 可持续采购 — (图片来源:作者)

  • 可持续采购: 在选择供应商时整合社会和环境绩效因素

  • 供应链优化: 设计最佳网络以以最低成本匹配供应和需求

什么阻碍了你公司的绿色转型?

可持续性挑战

向可持续供应链优化过渡带来了独特的一组挑战。

选择你想要最小化的指标 [应用用户指南]——(图片来源:作者)

核心复杂性在于将效率和成本效益与环境保护对齐。

如果你在海外生产,在低劳动和生产成本的国家

  • 你最小化了生产总成本

  • 增加环境影响,由于运输低效率工厂的排放。

然后我们只需要在绿色设施中本地化!

如果你在本地绿色设施生产

  • 增加了成本,因为高劳动成本绿色设备的资本支出,这些设备减少了 CO2 排放和资源使用。

  • 通过减少运输和使用高端制造设施,你降低了环境足迹

该应用将提供不同的场景,帮助你平衡这些不同的约束。

基于环境约束的多个场景——(图片来源:作者)

使用数据来支持决策。

数据分析的支持

在之前的文章中,我们发现线性编程可以在优化工厂和配送中心之间的流动方面发挥关键作用。

这些模型可以帮助你自动化对成本参数(固定、可变和运输)和足迹指标的深入分析,以找到满足业务目标的正确平衡。

样本数据集中使用的参数 [应用用户指南]——(图片来源:作者)

从算法的角度来看,你有一组外部参数

  • 需求:每个市场的需求(单位/月)

  • 每个地点的生产能力:高产能工厂(XX 单位/月),低产能工厂(YY 单位/月)

  • 环境足迹CO2 排放(kgCO2eq/单位)、资源消耗(L/单位)或(MJ/单位)和废物产生(kg/单位)

  • 成本:每个设施的固定成本(\(/月)**和每单位的可变成本**(\)/单位)

约束条件是什么?

  • 约束 1: 生产的单位数量 ≥ 总需求

  • 约束 2: CO2 排放 ≤ XX(kgCO2eq/月)

算法将选择一组制造地点进行开放

  • 每个潜在地点定义一个变量:(印度,低容量)= [0 或 1]

  • 如果值为 1,该位置开放,并且可以达到其生产能力

在这个示例中开启了 4 个位置 —(图片由作者提供)

根据用户定义的目标指标,模型可以提出最优的布尔值集合,以最小化该指标。

你可以在下面共享的视频中找到数据分析在供应链可持续性方面的其他应用,

现在,显然可持续性是至关重要的。

现在让我们来看看这个工具。

可持续供应链优化应用概述

目的和功能

主要目标是为供应链工程师提供一个互动平台,以模拟和评估不同的网络设计策略。

需要一个包含多个工作表的 Excel 文件作为输入,并提供多个模拟场景的结果。

如果没有数据,应用程序中提供了一个示例文件供测试。

你可以在这里试用

初步步骤:数据输入

数据输入 [用户指南] —(图片由作者提供)

用户可以输入自己的数据或使用预加载的数据集,其中包含与市场需求和制造设施相关的信息。

第一步:数据输入 —(图片由作者提供)

第二步:数据可视化

根据上传文件中包含的数据可视化模型的不同参数。

从数据集中可视化输入参数 [应用程序用户指南] —(图片由作者提供)

🛍️ 市场需求 在数据集中包含的位置,你有客户(或商店),其每月需求单位/月为单位。

🏭 市场供应 在数据集中包含的位置,你有潜在的制造地点(低容量和高容量工厂),每个工厂的每月最大生产量单位/月为单位。

使用甜甜圈图和条形图展示市场需求和生产能力 —(图片由作者提供)

⚡ 能源使用 每个生产地点生产单个单位所消耗的能源(MJ/Unit)。

🗑️ 废物生成 每个生产地点生产单个单位的废物生成量(Kg/Unit)。

🚰 水资源使用

每个生产地点生产单个单位所使用的水量 (L/Unit)

🌲 CO2 排放 每个生产地点生产单个单位的 CO2 排放量 (Kg CO2eq/Unit)

每个制造地点每单位生产的环境影响 — (图片来源作者)

我如何为我的公司收集这些数据?

有多个来源来收集这些输入参数:

  • 什么是生命周期评估?LCA: 使用数据分析评估产品从生产到处置的整个生命周期中的环境影响。

  • 使用 Python 进行供应链可持续性报告: 4 个步骤构建一个关注分销网络 CO2 排放的 ESG 报告

该工具将帮助您决定在哪里设立工厂,以满足所有市场的需求,考虑运输、生产成本和环境方面

你想实现什么目标?

第三步:选择目标函数

这将导致明智的战略决策,提高供应链的效率和可持续性。

当前目标函数集合 [UI] — (图片来源作者)

用户可以从四个目标函数中选择,

💰 生产成本

将生产和运输产品到不同市场的整体成本最小化 ($/Unit)

🚰 水资源使用

将每单位生产的水使用量最小化 (L/Unit)

⚡ 能源使用

将每单位生产的能源消耗量最小化 (MJ/Unit)

🌲 CO2 排放

将每单位生产和交付的 CO2 排放量最小化 (kgCO2eq/Unit)

应用程序会自动返回结果,

选择目标函数将触发优化 — (图片来源作者)

最后一步:可视化结果

按国家生产细分 — (图片来源作者)

应用程序提供了每种情况的综合结果概述,包括成本和环境影响的详细细分。

按来源分类的水资源使用和排放 — (图片来源作者)

Sankey 图帮助你追踪货物从生产地点到各自市场的流动。

货物流动的 Sankey 图 [应用界面] —(作者提供的图片)

在上面的示例中,

  • 巴西每月生产 15,000 个单位:1,450 个单位用于本地市场,其余的用于美国

  • 美国、德国和日本完全依赖进口

决策者可以对他们的网络设计采取动手实践的方法。

通过使用数据驱动的处方,他们将了解每个目标指标(CO2、水、能源等)的影响。

如果你最小化成本水资源使用CO2 排放,总预算会如何 [从左到右] —(作者提供的图片)

例如,上述视觉效果可以提出以下问题

  • Q1: 我们是否能承担 100% 的成本增加以最小化 CO2 排放?

  • Q2: 为什么最小化水资源使用比减少 CO2 排放更便宜?

  • Q3: 在印度寻找绿色制造地点是否会产生影响?

为了回答问题 Q3,我们来看看下面的视觉效果

成本最小化场景的排放量 —(作者提供的图片)

正如我们在右侧看到的,每个市场的主要排放量来自于从制造工厂运输货物

因此,即使在印度使用最环保的设备,我们仍然会因运输而产生高排放。

如果你需要一个简短的应用指南,请查看这个教程 👇

VIKTOR 介绍

VIKTOR 是一个直观的平台,旨在简化工程项目的创建。

它提供强大的工具,用于快速部署基于 Python 的算法。

VIKTOR 的主要特性

VIKTOR 以其快速开发、测试和部署 Web 应用程序的能力而脱颖而出。

我的第一个应用程序是一个简单的工具,用于自动化 ABC 分析

ABC 分析应用程序 截图 —(作者提供的图片)

经过几个简单的步骤,我通过一个直观的用户 Web 界面部署了我的 Python 模型,以展示帕累托图和 ABC 图表。

尝试这个应用程序,

ABC 分析与帕累托图应用程序 — [链接]

想要部署你的模型吗?试试 VIKTOR。

对供应链数据科学家的好处

VIKTOR 为数据科学家和分析师提供了一个用户友好的平台,与现有工作流程良好集成,提高了生产力和效率。

对于这个应用程序,我从我在 Github 上分享的一个模型开始 (链接),与供应链优化的文章相关。

供应链优化简单模型——(作者提供的图像)

在“入门指南” (文档) 中,你可以找到部署一个简单应用所需的所有不同步骤。

  • 采用用户输入,如 Excel 文件和参数选择,运行优化模型。

  • 使用 Plotly 展示动态视觉效果

  • 导出 Word 和 PDF 报告 (ABC 分析应用)

现在,我可以轻松地与需要获得编程技能以运行 Python 脚本的用户分享这个模型。

欲了解更多信息,请查看官方的 VIKTOR 文档

结论

企业需要更有效地响应利益相关者和监管机构对环境、社会和治理(ESG)日益增长的需求。

这个简单的原型,通过 VIKTOR 部署,可以帮助推动他们向绿色供应链的过渡

可以通过添加更多高级功能来轻松改进,例如,

  • 最大资源使用或二氧化碳排放的限制

  • 引入需求的变化以建立一个强健的网络

  • 可视化制造足迹的效率(最大生产输出/容量)

我邀请你探索这个应用程序,利用其功能测试使用你的数据集的不同优化场景

你听说过全球可持续未来的路线图吗?

可持续发展目标(SDGs)是由联合国设立的17 个目标,旨在应对全球挑战。

17 个目标可以分为 5 个类别——(作者提供的图像)

深入了解我的近期见解,探讨数据分析如何支持联合国的可持续发展目标

[## 什么是可持续发展目标?(SDGs)

使用数据科学将全球可持续性倡议与公司的供应链数字化转型联系起来

s-saci95.medium.com

关于我

让我们在 LinkedinTwitter 上联系。我是一名供应链工程师,利用数据分析改进物流操作和降低成本。

如果你对数据分析和供应链感兴趣,可以查看我的网站。

[## Samir Saci | 数据科学与生产力

一个关注数据科学、个人生产力、自动化、运筹学和可持续发展的技术博客

samirsaci.com](http://samirsaci.com/?source=post_page-----20599b98cab6--------------------------------)

参考资料

  • 可持续供应链网络优化应用,Samir Saci,应用

  • 产品细分与 ABC 分析,Samir Saci,应用

创建并部署一个提取图像主要颜色的 REST API

原文:towardsdatascience.com/create-and-deploy-a-rest-api-extracting-predominant-colors-from-images-a44b94cc3d46

使用无监督机器学习、FastAPI 和 Docker

Nicolo Cosimo AlbaneseTowards Data Science Nicolo Cosimo Albanese

·发布于 Towards Data Science ·阅读时间 15 分钟·2023 年 9 月 26 日

--

图片由作者提供。

目录

  1. 问题陈述

  2. 从图像中提取颜色

  3. 项目结构

  4. 代码

  5. 部署 Docker 容器

  6. 让我们尝试一下!

  7. API 文档

  8. 结论

  9. 许可证声明

1. 问题陈述

让我们设想一个制造设施的控制室,其中生产的产品需要自动分类。例如,根据颜色,货物可能会被重新导向滚筒输送机的不同分支,以进行进一步处理或包装。

否则,我们也可以设想一个在线零售商试图通过添加按颜色搜索功能来增强用户体验。客户可以更容易地找到特定颜色的衣物,从而简化他们对感兴趣产品的访问。

或者,就像作者一样,你可以将自己想象成一位IT 顾问,实现一个简单、快速且可重用的工具,用于从输入图像中生成适用于演示文稿、图表和应用程序的色彩调色板。

这些只是从图片中提取主要颜色如何提升操作效率或增强客户体验的几个示例。

在这篇博客文章中,我们使用 Python 实现从给定图片中提取主要颜色。然后,我们使用 FastAPI 和 Docker 将解决方案打包并部署为服务。

本帖的目的是分享一个关于部署轻量且自洽服务的端到端示例,该服务利用机器学习技术实现商业目的。这样的服务可以轻松集成到microservice architecture中。

2. 从图像中提取颜色

数字图像本质上是一个二维的像素网格。像素是图像中最小的显示单元,携带关于颜色的信息。常用的颜色表示方法是RGB 颜色模型。这一加法模型使用三种原色——红色、绿色和蓝色(因此称为 RGB)组合,创造出丰富的颜色。每种原色的强度由一个 8 位值表示。因此,每个像素有三个强度值,分别对应三种原色,范围从 0 到 255:

加法 RGB 颜色模型。来源于 维基百科

我们可以通过聚类从图像中提取主要颜色。简而言之,聚类技术尝试将相似的对象分组。在聚类方法中,我们将使用K-Means算法。它旨在通过最小化数据点与各自组中心(质心)之间的平方距离之和来创建“紧凑”的组。我们可以选择要创建的组/簇数量 k。在我们的案例中,数据点是每个像素的 RGB 值。模型训练后,我们可以将质心视为图像中主要颜色的代表。

让我们进入实践,创建一个 ColorAnalyzer 类,接受输入图像并提取其主要颜色。该类将具有以下方法:

  • load_image 从本地路径或 URL 加载图像到二维数组。

  • is_url 检查输入路径是否为 URL。

  • preprocess_image 调整图像大小以提高处理速度。

  • find_clusters 应用 K-Means 聚类以提取重要颜色作为质心。

  • sort_clusters_by_size 按簇大小降序排序颜色。

  • plot_image 显示原始输入图像(已调整大小)。

  • plot_3d_clusters 显示簇的三维图。观察结果显示其质心(主要)颜色。

  • plot_predominant_colors 绘制一张按图像中颜色出现频率排序的条形图。

  • get_predominant_colors 返回主要颜色的 JSON 对象列表。

import cv2
from PIL import Image
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
import numpy as np
import requests
import json
from urllib.parse import urlparse
from io import BytesIO

class ColorAnalyzer:
    '''
    This class analyzes the predominant colors in an image 
    using K-Means clustering based on the RGB color paradigm.

    Attributes:
        url_or_path (str): The URL or local file path of the image.
        num_clusters (int): The number of clusters to identify as predominant colors.
        scaling_factor (int): The percentage by which to scale the image for preprocessing.
        image (numpy.ndarray): The loaded and preprocessed image.
        pixels (numpy.ndarray): Reshaped image data for clustering.
        image_rgb (numpy.ndarray): Resized image in RGB format.
        centroids (numpy.ndarray): Centroids (predominant colors) obtained through clustering.
        percentages (numpy.ndarray): Percentage of pixels belonging to each cluster.
        labels (numpy.ndarray): Labels indicating cluster membership for each pixel.
        sorted_colors (numpy.ndarray): Predominant colors sorted by cluster size.
        sorted_percentages (numpy.ndarray): Percentages of pixels per cluster, sorted by cluster size.
    '''
    def __init__(self, url_or_path, num_clusters=4, scaling_factor=10):
        '''
        Initializes the ImageColorAnalyzer with the provided parameters.

        Args:
            url_or_path (str): The URL or local file path of the image.
            num_clusters (int, optional): The number of clusters to identify as predominant colors (default is 5).
            scaling_factor (int, optional): The percentage by which to scale the image for preprocessing (default is 10).
        '''
        self.url_or_path = url_or_path
        self.num_clusters = num_clusters
        self.scaling_factor = scaling_factor
        self.image = self.load_image()
        self.pixels, self.image_rgb = self.preprocess_image()
        self.centroids, self.percentages, self.labels = self.find_clusters()
        self.sorted_colors, self.sorted_percentages = self.sort_clusters_by_size()

    def load_image(self):
        '''
        Load the image into a 2D array from the local path or URL.

        Returns:
            numpy.ndarray: The loaded image.

        Raises:
            Exception: If the URL does not exist or is broken, or if the image path is invalid.
        '''
        # If the input image path is a URL 
        if self.is_url():

            # Get the response
            response = requests.get(self.url_or_path)

            # If there is a problem in getting the response..
            if response.status_code != 200:

                # ..raise an exception
                raise Exception('URL does not exist or it is broken.')

            # Try to extract the image from the URL
            try:

                # Get PIL image object file from the response 
                image = Image.open(BytesIO(response.content))

                # Convert image from PIL to OpenCV format
                image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)

            # If an error occurs in processing the URL..
            except:

                # ..raise an exception
                raise Exception('URL may not contain an image.')

        # If the input image path is not a URL
        else:

            # Try to load the image
            try:

                # Load the image from a local path
                image = cv2.imread(self.url_or_path)

            # If there is a problem reading the local path..
            except:

                # ..raise an exception
                raise Exception('Invalid image path.')

        # return the loaded image
        return image

    def is_url(self):
        '''
        Check if the input path is URL.

        Returns:
            bool: True if the path is a URL, False otherwise.
        '''
        # Return True if the path is a URL, False otherwise
        return 'http' in urlparse(self.url_or_path).scheme

    def preprocess_image(self):
        '''
        Resize the image to improve processing speed.

        Returns:
            tuple: 
                Resized image data for clustering
                Image for plotting.
        '''
        # Resize the image by the scaling factor for performances
        width = int(self.image.shape[1] * self.scaling_factor / 100)
        height = int(self.image.shape[0] * self.scaling_factor / 100)
        resized_img = cv2.resize(
            self.image, (width, height), interpolation=cv2.INTER_AREA)

        # Convert the image back to RGB
        image_rgb = cv2.cvtColor(resized_img, cv2.COLOR_BGR2RGB)

        # Extract pixels as 2D array for clustering
        pixels = image_rgb.reshape(-1, 3)

        # Return array for clustering and image for plotting
        return pixels, image_rgb

    def find_clusters(self):
        '''
        Find predominant colors through clustering.

        Returns:
            tuple: 
                centroids (predominant colors)
                percentages of pixels per cluster
                labels of each point.           
        '''
        # Instantiate clustering model
        kmeans = KMeans(n_clusters=self.num_clusters, n_init=10)

        # Fit the model on the image and get labels
        labels = kmeans.fit_predict(self.pixels)

        # Get centroids (predominant colors)
        centroids = kmeans.cluster_centers_.round(0).astype(int)

        # Get percentage of pixels belonging to each cluster
        percentages = np.bincount(labels) / len(self.pixels) * 100

        # Return:
        #   - centroids 
        #   - percentage of pixels per cluster        
        #   - labels of each point
        return centroids, percentages, labels

    def sort_clusters_by_size(self):
        '''
        Sort predominant colors and percentages 
        of pixels per cluster by cluster size 
        in descending order.

        Returns:
            tuple: 
                Predominant colors sorted by cluster size
                Percentages of pixels per cluster sorted by cluster size.
        '''
        sorted_indices = np.argsort(self.percentages)[::-1]
        sorted_colors = self.centroids[sorted_indices]
        sorted_percentages = self.percentages[sorted_indices]
        return sorted_colors, sorted_percentages

    def plot_image(self):
        '''
        Plot the preprocessed image (resized).
        '''
        plt.imshow(self.image_rgb)
        plt.title('Preprocessed Image')
        plt.axis('off')
        plt.show()

    def plot_3d_clusters(self, width=15, height=12):
        '''
        Plot a 3D visualization of the clustering.

        Args:
            width (int, optional): Width of the plot (default is 15).
            height (int, optional): Height of the plot (default is 12).
        '''
        # Prepare figure
        fig = plt.figure(figsize=(width, height))
        ax = fig.add_subplot(111, projection='3d')

        # Plot point labels with their cluster's color
        for label, color in zip(np.unique(self.labels), self.centroids):
            cluster_pixels = self.pixels[self.labels == label]
            r, g, b = color
            ax.scatter(cluster_pixels[:, 0], 
                       cluster_pixels[:, 1], 
                       cluster_pixels[:, 2], 
                       c=[[r/255, g/255, b/255]],  
                       label=f'Cluster {label+1}')

        # Display title, axis labels and legend
        ax.set_title('3D Cluster Visualization')
        ax.set_xlabel('r')
        ax.set_ylabel('g')
        ax.set_zlabel('b')
        plt.legend()
        plt.show()

    def plot_predominant_colors(self, width=12, height=8):
        '''
        Plot a bar chart of predominant colors 
        ordered by presence in the picture.

        Args:
            width (int, optional): Width of the plot (default is 12).
            height (int, optional): Height of the plot (default is 8).
        '''
        # Prepare color labels for the plot
        color_labels = [f'Color {i+1}' for i in range(self.num_clusters)]

        # Prepare figure
        plt.figure(figsize=(width, height))

        # Plot bars
        bars = plt.bar(color_labels, 
                       self.sorted_percentages, 
                       color=self.sorted_colors / 255.0, 
                       edgecolor='black')

        # Add percentage of each bar on the plot
        for bar, percentage in zip(bars, 
                                   self.sorted_percentages):
            plt.text(
                bar.get_x() + bar.get_width() / 2,
                bar.get_height(), 
                f'{percentage:.2f}%', 
                ha='center', 
                va='bottom')

        # Display title and axis labels
        plt.title(f'Top {self.num_clusters} Predominant Colors')
        plt.xlabel('Colors')
        plt.ylabel('Percentage of Pixels')
        plt.xticks(rotation=45)
        plt.show()

    def get_predominant_colors(self):
        '''
        Return a list of predominant colors.
        Each color is a JSON object with RGB code and percentage.
        '''
        # Prepare output list
        colors_json = []

        # For each predominant color
        for color, percentage in zip(self.sorted_colors, 
                                     self.sorted_percentages):
            # Get the RGB code
            r, g, b = color

            # Prepare JSON object
            color_entry = {'color': {'R': f'{r}', 
                                     'G': f'{g}', 
                                     'B': f'{b}'}, 
                           'percentage': f'{percentage:.2f}%'}

            # Append JSON object to color list
            colors_json.append(color_entry)

        # Return the results
        return colors_json

我们可以使用 Python notebook 测试我们的类:

  • 使用指向在线图像的输入 URL 实例化类:
colors_extractor = ColorAnalyzer(
  'https://fastly.picsum.photos/id/63/5000/2813.jpg?hmac=HvaeSK6WT-G9bYF_CyB2m1ARQirL8UMnygdU9W6PDvM',
  num_clusters=4)
  • 绘制原始图像(已调整大小):
colors_extractor.plot_image()

图片由作者提供。

  • 绘制最主要颜色的条形图:
colors_extractor.plot_predominant_colors()

图片由作者提供。

  • 显示获得的簇:
colors_extractor.plot_3d_clusters()

图片由作者提供。

  • 返回提取颜色的 JSON 对象列表:
print(json.dumps(
    colors_extractor.get_predominant_colors(), 
    indent=3)
)
[
   {
      "color": {
         "R": "140",
         "G": "15",
         "B": "19"
      },
      "percentage": "54.56%"
   },
   {
      "color": {
         "R": "231",
         "G": "37",
         "B": "47"
      },
      "percentage": "35.55%"
   },
   {
      "color": {
         "R": "163",
         "G": "111",
         "B": "63"
      },
      "percentage": "6.78%"
   },
   {
      "color": {
         "R": "211",
         "G": "201",
         "B": "186"
      },
      "percentage": "3.11%"
   }
]

我们可以对任何输入图像(URL 或本地路径)重复这一过程,并检查结果。例如:

# Different image
colors_extractor = ColorAnalyzer(
  'https://fastly.picsum.photos/id/165/2000/1333.jpg?hmac=KK4nT-Drh_vgMxg3hb7rOd6peHRIYmxMg0IEyxlTVFg',
  num_clusters=4) 

# Plot resized original image
colors_extractor.plot_image()

# 3D plot of the centroids and data points
colors_extractor.plot_3d_clusters()

# Bar chart of predominant colors
colors_extractor.plot_predominant_colors()

# Predominant colors as list of JSON
print(json.dumps(
    colors_extractor.get_predominant_colors(), 
    indent=3)
)

图片由作者提供。

如何创建一个在请求时提供图像分析功能的 Web 服务?我们需要将我们的笔记本转换为一个暴露 REST API 的 Python 项目。

3. 项目结构

让我们介绍一下项目的主要组成部分:

  1. REST APIREST(表述性状态转移)API 是一种设计应用程序的架构风格。它使用标准的 HTTP 方法(GET、POST 等)来允许不同系统之间的通信。在我们的案例中,我们希望通过 HTTP 请求使客户端能够请求从输入图像中提取主要颜色。我们将使用FastAPI来构建 API 服务。

  2. Docker:一个允许在隔离容器中构建、部署和运行应用程序的平台。使用 Docker 将帮助我们将所有用于颜色提取任务所需的依赖项打包在一起,确保一致性、可移植性并消除操作上的麻烦(我使用的是哪个 Python 版本?我安装了所有包吗?我需要什么版本的 OpenCV?)。

REST API。图像由作者提供。

我们可以将项目结构如下:

colors-extractor/
├── api/
│   ├── __init__.py
│   └── endpoints.py
├── dto/
│   ├── __init__.py
│   └── image_data.py
├── service/
│   ├── __init__.py
│   └── image_analyzer.py
├── notebooks/
│   └── extract_colors.ipynb
├── main.py
├── requirements.txt
├── Dockerfile
└── README.md
  • README.md:项目文档,以 Markdown 格式编写。

  • requirements.txt:运行项目所需的 Python 依赖项列表。

  • Dockerfile:包含所有组装 Docker 镜像命令的文本文件,即用于项目的隔离环境。

  • main.py:我们应用程序的入口点。

  • api/:包含 REST API 端点定义的子文件夹。在我们的简单示例中,我们只有一个端点用于请求颜色提取。

  • dto/:包含在 API 服务请求和响应中使用的数据类的子文件夹。名称dto源自数据传输对象,因为这些类代表客户端和服务之间的接口。

  • service/:包含应用程序逻辑的子文件夹。在我们的示例中,ColorAnalyzer类提供图像处理能力。

  • notebooks/:包含笔记本实验的子文件夹。

在不同模块(dto/api/service/)中分离数据(或模型)、端点定义(或控制器)和应用程序逻辑(或服务)是一种保证清晰性、可维护性和可重用性的方法。这也促进了更清洁的架构,并简化了后续开发。读者可以参考MVC 设计模式以获取更多信息。

4. 代码

让我们开始查看main.py。在我们的入口点中,我们:

  • 创建 FastAPI 应用程序:app = FastAPI()

  • 启用 CORS 以通过add_middleware方法允许客户端调用 Web 服务。

  • 为根端点("/")定义一个GET请求处理程序,返回一个简单的消息。

  • 在我们的 api 模块中包含一个路由器,并使用"api/"前缀。该前缀将成为最终端点的一部分,如下所示:"http://<host>:<port>/api/<endpoint>"

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from api.endpoints import router as api_router

# Create a FastAPI application instance
app = FastAPI()

app.add_middleware(
    CORSMiddleware,            # Add Cross-Origin Resource Sharing (CORS) middleware to handle browser security restrictions
    allow_origins = ['*'],     # Allow requests from all origins (insecure, for development only)
    allow_credentials = True,  # Allow credentials like cookies in the requests
    allow_methods = ['*'],     # Allow all HTTP methods (GET, POST, etc.)
    allow_headers = ['*'],     # Allow all headers in requests
)

# Define a GET request handler for the root endpoint ('/')
@app.get('/')
# Define an asynchronous function for the root endpoint
async def root():
    # Return a JSON response with a message
    return {'message': 'API for color extraction from images.'}

# Include the API router with a prefix of '/api'
app.include_router(api_router, prefix='/api')

端点的定义在api模块中。在"api/endpoints.py"文件内,我们:

  • 为 FastAPI 应用程序创建路由器:router = APIRouter(),该路由器在main.py中被导入。

  • /colors端点定义一个POST请求。应用程序期望一个ColorExtractionRequest类的请求,并返回一个ColorExtractionResponse类的响应。这两个对象在dto模块中定义。

  • 在接收到请求后,会实例化一个ColorAnalyzer对象,并将颜色提取结果作为响应返回给用户。

from fastapi import APIRouter, HTTPException
from service.image_analyzer import ColorAnalyzer
from dto.image_data import ColorExtractionRequest, ColorExtractionResponse
import logging 

# Define the router for the FastAPI app
router = APIRouter()

# Logging configuration
logging.basicConfig(
    format = '%(levelname)s:     %(asctime)s, %(module)s, %(processName)s, %(message)s', 
    level = logging.INFO)

# Instantiate logger
logger = logging.getLogger(__name__)

# Define a POST request handler for the '/colors' endpoint
@router.post(
        '/colors',                                 # Endpoint name
        response_model = ColorExtractionResponse,  # Data model for the response 
        tags = ['Colors Extraction']               # Tag used for documentation
        )
# Define an asynchronous function accepting a 'ColorExtractionRequest' as request body
async def colors(input_data: ColorExtractionRequest):
    '''
    Analyze an image and return predominant colors.

    Parameters:
      - input_data[ColorExtractionRequest]: Request data including 'url_or_path' (str) and 'num_clusters' (int, optional).

    Returns:
      - ImageAnalysisResponse: Response data containing a list of predominant colors.

    Example Usage:
      - Send a POST request with JSON data containing the 'url_or_path' parameter to extract colors from an image.
    '''

    # Log request information
    logger.info(f'Analysis for image key: {input_data.url_or_path}.')
    logger.info(f'Requested colors: {input_data.num_clusters}.')

    # Perform the color extraction
    try:

        # Instantiate the ColorAnalyzer class for image processing
        color_json = ColorAnalyzer(
                input_data.url_or_path, 
                input_data.num_clusters
            ).get_predominant_colors()

        logger.info(f'Analysis completed.')

        # Return the predominant colors
        return {'predominant_colors': color_json}

    # If an error occurs
    except Exception as e:

        # Log the error message 
        logger.error(f'Exception in image processing: {str(e)}.')

        # Raise an exception
        raise HTTPException(status_code = 500, detail = str(e))

让我们探索请求和响应的数据模型。其类定义在dto模块中,位于dto/image_data.py文件内:

from pydantic import BaseModel
from typing import List

class Color(BaseModel):
    '''
    Color representation as RGB values.
    '''
    R: int
    G: int
    B: int

class ColorInfo(BaseModel):
    '''
    Information about a color: RGB and percentage of pixels across image.
    '''
    color: Color
    percentage: str

class ColorExtractionRequest(BaseModel):
    '''
    Colors extraction request.
    '''
    url_or_path: str
    num_clusters: int = 4 # Default to 4 most predominant colors if not provided

class ColorExtractionResponse(BaseModel):
    '''
    Color extraction response from an image analysis request.
    '''
    predominant_colors: List[ColorInfo]

这个数据模型很简单。简而言之,该服务:

  • 接受一个输入的 URL 或路径,以及一个期望的集群数量/主要颜色。

  • 返回一个由 RGB 值和图像中属于该集群的像素百分比组成的 JSON 对象列表。

有趣的是,定义pydantic类不仅提高了可读性和维护性,还简化了利用 FastAPI 框架生成 API 文档的过程(更多内容见后续段落)。

现在,我们只需要定义一个 Docker 镜像并部署 Docker 容器。

5. 部署 Docker 容器

让我们观察项目根目录中的Dockerfile。此文本文件包含了为我们的项目创建 Docker 镜像的所有命令。具体如下:

  • FROM python:3.8-slim将基础镜像设置为起始点。

  • WORKDIR /colors-extractor将容器内的工作目录设置为/colors-extractor

  • COPY requirements.txt requirements.txt将依赖项从本地机器复制到容器内的WORKDIR

  • RUN pip install -r requirements.txt在 Docker 容器中安装 requirements.txt 中列出的 Python 依赖项。

  • COPY . .将项目文件从本地机器复制到容器中。我们在安装依赖项后再执行此操作,因为 Docker 通过分层后续命令创建镜像。如果我们只是更新代码库,根据当前命令的顺序,Docker 引擎不会重新安装所有依赖项。

  • EXPOSE 8000暴露了 8000 端口,我们的 FastAPI 应用程序就在这个端口上运行。

  • CMD [“uvicorn”, main:app, “--host”, “0.0.0.0”, “--port”, “8000”]是容器启动时运行的命令。在这种情况下,我们指示容器使用 Uvicorn 运行 FastAPI 应用程序。映射main:app设置了我们应用程序的正确入口点,即main.py

# Use the official Python image as the base image
FROM python:3.8-slim

# Set the working directory
WORKDIR /colors-extractor

# Copy the requirements.txt file and install dependencies
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt

# Copy the project files into the container
COPY . .

# Expose the port that the FastAPI app will listen on
EXPOSE 8000

# Command to run the FastAPI app using Uvicorn (handled by Dockerfile)
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

要在本地部署 Docker 容器,从命令行:

# Move into the project directory
cd colors-extractor

# Create Docker image
docker build -t colors-extractor .

# Execute Docker container
docker run -d -p 8000:8000 colors-extractor

我们可以通过以下方式展示运行中的容器:

docker ps

使用此命令,我们可以获取与我们的应用程序相关联的容器 ID,并使用它来检查日志:

docker logs <container_id>

日志确认应用程序正在运行:

图片由作者提供。

6. 让我们试试吧!

让我们记住端点结构:

  • main.py声明了一个带有/api前缀的路由器指向api模块。

  • api模块在endpoints.py文件中定义了一个用于/colors端点的POST请求处理程序。

  • 运行端口是8000

因此,我们应该执行一个POST请求到:

  • [localhost:8000/api/colors](http://localhost:8000/api/colors.)

要测试服务,我们可以使用curlPostman等工具:

curl --location 'http://localhost:8000/api/colors' \
--header 'Content-Type: application/json' \
--data '{
    "url_or_path": "https://fastly.picsum.photos/id/63/5000/2813.jpg?hmac=HvaeSK6WT-G9bYF_CyB2m1ARQirL8UMnygdU9W6PDvM",
    "num_clusters": 3
}'

结果:

{
  "predominant_colors": [
    {
      "color": {
        "R": 140,
        "G": 16,
        "B": 19
      },
      "percentage": "54.97%"
    },
    {
      "color": {
        "R": 231,
        "G": 37,
        "B": 47
      },
      "percentage": "35.55%"
    },
{
      "color": {
        "R": 180,
        "G": 142,
        "B": 104
      },
      "percentage": "9.47%"
    }
  ]
}

同样地,使用 Postman:

使用 Postman 测试服务。图片由作者提供。

我们可以再次检查容器以验证日志中是否存在我们的测试调用:

日志。图片由作者提供。

7. API 文档

FastAPI 会自动提供已实现 API 的文档。默认情况下,它可以在以下地址访问:

  • [:/docs](http://localhost:8000/api/colors.)

访问该 URL,我们可以找到一个完全记录我们端点的网页用户界面(Swagger UI):

API 文档。图片由作者提供。

请求和响应的数据模型在网页界面的Schemas部分下,并且它填充了在dto模块中定义的pydantic模型,并与路由器的处理程序相关联:

数据模型。图片由作者提供。

8. 结论

在这篇博客文章中,我们分享了利用以下内容逐步实现服务的过程:

  • 无监督机器学习技术用于实现业务目标,即从图像中提取主要颜色。

  • FastAPI 用来将解决方案作为 REST API 提供服务。

  • 使用 Docker 进行隔离和一致的部署。

我们的目标是展示一个全面的示例,可以轻松地重用和扩展以将机器学习模型部署为 REST API。

本博客的完整代码可以在GitHub上找到。

9. 许可声明

为撰写此文章,我们使用了两张图片:

两个来源均可在Unsplash 许可证下免费用于个人和商业用途。我们使用PicsumGitHub repo)生成了图像 URL,且其使用MIT 许可证

创建和探索数据科学中角色和薪资的图景

原文:towardsdatascience.com/create-and-explore-the-landscape-of-roles-and-salaries-in-data-science-926092f616ca

一个关于如何使用分类数据创建图景并进行无监督分析以获取更深入见解的教程

Erdogan TaskesenTowards Data Science Erdogan Taskesen

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

--

图片由作者提供。

数据科学领域在不断发展,新角色和职能也随之产生。传统的数据科学角色正在演变为数十种新角色,包括数据工程师、机器学习工程师、产品数据分析师、研究科学家、云数据工程师等等。在这篇博客中,我们将加载数据科学薪资数据集,并创建一个图景,以检查各角色如何根据地点、远程工作、职位名称、经验水平以及与薪资的关系相互关联。 我将演示如何创建这样的图景以及如何使用无监督聚类更深入地分析样本。聚类分析使用库clusteval进行,而散点图则使用scatterdd3blocks来生成交互式图表。

介绍

数据科学是当今数字环境中变化最快的领域之一。数据科学家的角色是解决复杂问题并推动基于数据的决策。基本知识包括统计技术、机器学习算法和数据可视化。但在创建产品时,还需要深入了解工程学,以及数据治理、伦理和隐私等方面。作为数据科学家所需的专业知识取决于具体的角色描述和业务等因素。现在有很多新角色,比如数据工程师、机器学习工程师、产品数据分析师、研究科学家、云数据工程师等。每个角色也会随着经验水平的不同而变化。入门级职位通常涉及在资深数据科学家的指导下工作,协助数据预处理,并参与模型开发。随着你的进步,中级角色要求对统计分析、特征工程和算法选择有更深入的理解。资深数据科学家通常领导项目,指导初级团队成员,并参与战略规划。

数据科学领域的薪资因经验、行业、国家和高级资格而异。多年来,地点在数据科学机会和薪酬中扮演了非常重要的角色。像硅谷、纽约市和旧金山这样的科技中心提供高薪职位。然而,近年来,从不同地点远程工作变得技术上更容易且被更广泛接受。让我们看看是否可以通过数据科学薪资数据集来确认这些趋势。

如果你觉得这篇文章有帮助,欢迎你 关注我 ,因为我会写更多类似的话题。如果你考虑购买 Medium 会员,你可以通过使用我的 推荐链接来稍微支持我的工作。这个价格和一杯咖啡差不多,但可以让你每月阅读无限量的文章!

用于分析的库。

我们将使用七个库来帮助加载数据集、执行预处理步骤、分析数据、创建可视化,并构建数据科学环境。如果你想深入了解细节,我推荐阅读下面的博客。

# Easy import of data sets
pip install datazets

# One-hot encoding
pip install df2onehot

# PCA analysis. Create explainable biplots
pip install pca

# Clustering with automatic evaluation
pip install clusteval

# Making beautifull scatter plots
pip install scatterd

# Making beautifull interactive scatter plots
pip install d3blocks

# HNET for association analysis for the clusterlabels
pip install hnet

## 什么是 PCA 载荷,如何有效使用 Biplots?

一份实用指南,帮助你充分利用主成分分析

towardsdatascience.com [## 从数据到集群:你的聚类何时足够好?

使用聚类方法可以发现合理的聚类和隐藏的宝石,但你需要正确的聚类评估…

从数据到聚类:你的聚类够好吗? 从聚类到洞察:下一步 [## 从聚类到洞察:下一步

学习如何定量检测哪些特征驱动了聚类的形成

从聚类到洞察:下一步 如何通过使用 D3js 和 Python 使散点图更具互动性 [## 通过使用 D3js 和 Python 使散点图发挥最大的作用。

散点图非常有用,特别是当它们具有缩放和刷选功能时。

如何通过使用 D3js 和 Python 使散点图更具互动性 探索和理解你的数据,通过重要关联网络 [## 探索和理解你的数据,通过重要关联网络。

探索以理解你的数据可能会决定一个项目是失败还是成功完成!

探索和理解你的数据,通过重要关联网络

数据科学薪资数据集

数据科学薪资数据集来源于 ai-jobs.net [1],并且也作为 Kaggle 竞赛 [2] 公开。数据集包含 4134 个样本的 11 个特征。这些样本来自全球,每周更新,从 2020 年到现在(大约 2023 年初)。数据集已公开发布,供免费使用。让我们加载数据,查看变量。

# Import library
import datazets as dz
# Get the data science salary data set
df = dz.get('ds_salaries.zip')

# The features are as following
df.columns

# 'work_year'          > The year the salary was paid.
# 'experience_level'   > The experience level in the job during the year.
# 'employment_type'    > Type of employment: Part-time, full time, contract or freelance.
# 'job_title'          > Name of the role.
# 'salary'             > Total gross salary amount paid.
# 'salary_currency'    > Currency of the salary paid (ISO 4217 code).
# 'salary_in_usd'      > Converted salary in USD.
# 'employee_residence' > Primary country of residence.
# 'remote_ratio'       > Remote work: less than 20%, partially, more than 80%
# 'company_location'   > Country of the employer's main office.
# 'company_size'       > Average number of people that worked for the company during the year.

# Selection of only European countries
# countries_europe = ['SM', 'DE', 'GB', 'ES', 'FR', 'RU', 'IT', 'NL', 'CH', 'CF', 'FI', 'UA', 'IE', 'GR', 'MK', 'RO', 'AL', 'LT', 'BA', 'LV', 'EE', 'AM', 'HR', 'SI', 'PT', 'HU', 'AT', 'SK', 'CZ', 'DK', 'BE', 'MD', 'MT']
# df['europe'] = np.isin(df['company_location'], countries_europe)

图 1 展示了顶级职位的摘要以及薪资分布。顶部的两个面板是全球范围的,而底部的两个面板仅针对欧洲。尽管这些图表很有信息量,但它们展示的是平均值,并且不清楚地点、经验水平、远程工作、国家等在特定上下文中的关系。例如:一名为小公司远程工作的入门级数据工程师的薪资是否与其他属性的有经验的数据工程师相似? 这些问题可以通过下节展示的分析得到更好的回答。

图 1. 排名前列的职位名称。上面两个面板是全球统计数据,而下面两个面板是欧洲的统计数据。(图片由作者提供)

预处理

数据科学薪资数据集是一个混合数据集,包含连续变量和分类变量。我们将进行无监督分析,并创建数据科学的全景图。但在进行任何预处理之前,我们需要去除冗余特征,例如 salary_currencysalary,以防止多重共线性问题。此外,我们将从数据集中排除 salary_in_usd 变量,并将其作为目标变量 y 存储,因为我们不希望由于薪资本身而发生分组。基于聚类结果,我们可以调查任何检测到的分组是否与薪资相关。清理后的数据集包含 8 个特征和相同的 4134 个样本。

# Store salary in separate target variable.
y = df['salary_in_usd']

# Remove redundant variables
df.drop(labels=['salary_currency', 'salary', 'salary_in_usd'], inplace=True, axis=1)

# Make the catagorical variables better to understand.
df['experience_level'] = df['experience_level'].replace({'EN':'Entry-level', 'MI':'Junior Mid-level', 'SE':'Intermediate Senior-level', 'EX':'Expert Executive-level / Director'}, regex=True)
df['employment_type'] = df['employment_type'].replace({'PT':'Part-time', 'FT':'Full-time', 'CT':'Contract', 'FL':'Freelance'}, regex=True)
df['company_size'] = df['company_size'].replace({'S':'Small (less than 50)', 'M':'Medium (50 to 250)', 'L':'Large (>250)'}, regex=True)
df['remote_ratio'] = df['remote_ratio'].replace({0:'No remote', 50:'Partially remote', 100:'>80% remote'}, regex=True)
df['work_year'] = df['work_year'].astype(str)

df.shape
# (4134, 8)

下一步是将所有测量值转换为相同的单位。为了做到这一点,我们将仔细执行独热编码,并处理我们可能无意中引入的多重共线性。换句话说,当我们将任何分类变量转换为多个独热变量时,我们引入了一个偏差,使得我们可以基于来自同一分类列的两个或更多特征完美预测一个特征(即独热编码特征的总和始终为一)。这被称为虚拟陷阱,我们可以通过简单地删除一个列来打破线性链条,从而防止它。 df2onehot 包含虚拟陷阱保护功能。此功能比仅仅删除每个类别的一个独热列稍微高级一些,因为它仅在由于其他清理操作(如每个独热特征的样本最小数或布尔特征中的 False 状态的移除)导致线性链条尚未断裂时才移除一个独热列。

# Import library
from df2onehot import df2onehot

# One hot encoding and removing any multicollinearity to prevent the dummy trap.
dfhot = df2onehot(df,
                  remove_multicollinearity=True,
                  y_min=5,
                  verbose=4)['onehot']

print(dfhot)
#       work_year_2021  ...  company_size_Small (less than 50)
# 0              False  ...                              False
# 1              False  ...                              False
# 2              False  ...                              False
# 3              False  ...                              False
# 4              False  ...                              False
#              ...  ...                                ...
# 4129           False  ...                              False
# 4130            True  ...                              False
# 4131           False  ...                               True
# 4132           False  ...                              False
# 4133            True  ...                              False

# [4134 rows x 115 columns]

在我们的案例中,我们将删除包含少于 5 个样本的独热编码特征(y_min=5),并删除多重共线性以防止虚拟陷阱(remove_multicollinearity=True)。这将产生 115 个独热编码特征,适用于相同的 4134 个样本。

PCA 分析显示,薪资受经验水平、公司规模和地点的驱动。

我们将使用 PCA 分析预处理的数据集,并确定样本如何根据载荷和视觉检查相互关联。请参见下面的代码部分了解如何执行 PCA 分析。更多细节请阅读这篇文章。样本与特征载荷关系的结果显示在双变量图中(图 2)。花时间理解它,因为它包含大量信息。首先,这个二维空间中的每一个点代表 4134 个样本中的一个,两个点之间的距离描述了它们在 115 个特征上的相似性。两点越接近,点在某种程度上越相似。图的背景显示了带有红色光晕的密度图,高密度和低密度区域以这种方式突出显示。每个点的大小是薪资,薪资越高,点的大小越大。颜色基于job_title,标记设置为experience_level

# Import library
from pca import pca
# Initialize
model = pca(normalize=False)
# Fit model using PCA
model.fit_transform(dfhot)

# Make biplot
model.biplot(labels=df['job_title'],
             s=y/500,
             marker=df['experience_level'],
             n_feat=10,
             density=True,
             fontsize=0,
             jitter=0.05,
             alpha=0.8,
             color_arrow='#000000',
             arrowdict={'color_text': '#000000', 'fontsize': 32},
             figsize=(40, 30),
             verbose=4,
             )

图 2. 数据科学薪资数据集的双变量图。样本按职位标题着色,标记表示经验水平,点的大小基于薪资(美元)。(图像由作者提供)。

如果我们查看图 2,可以看到第一个主成分解释了 18.9%的方差,第二个主成分解释了 14.7%。这表明前两个主成分捕捉了数据中大部分的解释方差,这使得进一步解释样本关系和载荷是值得的。载荷(黑色箭头)描述了哪些原始特征对主成分中的方差负责。最大的载荷来自work_year类别,它将数据分成两个烟斗状的形状(左下到右上),而experience levelcompany size则形成了指向右上角的长尾。

形状较大的样本表示较高的薪资,并且似乎聚集在一起(左下)。许多左下角的样本具有矩形形状,这表示资历经验水平。向右上角的尾部移动时,点变得较小(相对较低的薪资)并且形状混合,表示各种经验水平。为了视觉检查,我们可以应用不同的颜色,如图 3 所示。总体来看,我们可以看到样本按年份聚类,并且呈烟斗状。随着时间的推移,可以观察到经验水平、公司规模和远程工作模式。

# Import library
from scatterd import scatterd

# Create various scatter plots with different coloring.
model.scatter(labels=df['company_size'],
             s=y/500,
             marker=df['experience_level'],
             density=True,
             fontsize=20,
             jitter=0.05,
             alpha=0.8,
             figsize=(40, 30),
             verbose=4,
             grid=True,
             legend=True,
             )

图 3. 四个面板中样本以不同的颜色显示。A. 样本按年份着色。B. 样本按经验水平着色。C. 样本按远程工作着色。D. 样本按公司规模着色。(图像由作者提供)。

当我们根据国家对样本进行着色时(图 4),欧洲的国家主要出现在尾部(灰色)。这些结果表明,高薪资主要集中在欧洲以外的地区,具有高级经验水平,并且在大型公司工作。远程工作似乎没有影响。这一趋势在 2020 年至 2023 年间似乎也没有改变。

图 4. 欧洲地区的样本以灰色标记。标记设置为职位名称。(图片由作者提供)

创建数据科学角色和薪资的全景。

为了创建全景,我们将使用 t-分布随机邻域嵌入(t-SNE)。在预处理步骤中,work_year 列被移除,以避免按年份进行分离(跨年份的模式非常相似)。在 t-SNE 嵌入后,我们可以使用 scatterd 库在二维空间中散布数据点。该库突出显示密集区域(density=True),并提供对较少密集区域的透明度(gradient=opaque),使散点图保持整洁。全景现在可以在图 5 中找到。与前一部分一样,样本按职位名称着色,标记设置为经验水平,点的大小基于美元薪资。

# Import libraries
from scatterd import scatterd
from sklearn.manifold import TSNE

# Remove work year from dataframe
df.drop(labels=['work_year'], inplace=True, axis=1)

# Create new one hot matrix without work year
dfhot = df2onehot(df, remove_multicollinearity=True, y_min=5, verbose=4)['onehot']

# Feature embedding using tSNE
X = TSNE(n_components=2, init='pca', perplexity=100).fit_transform(dfhot.values)

# Import library
fig, ax = scatterd(X[:, 0],
                   X[:, 1],
                   marker=df['experience_level'],
                   s=y/500,
                   labels=df['job_title'],
                   fontsize=0,
                   density=True,
                   args_density={'alpha': 0.4},
                   gradient='opaque',
                   edgecolor='#000000',
                   jitter=1,
                   grid=True,
                   legend=False,
                   figsize=(40, 30),
                   )

图 5. 数据科学全景。样本按职位名称着色,标记设置为经验水平,点的大小基于美元薪资。(图片由作者提供)

驱动特征的检测。

为了确定哪些特征驱动样本的分组,我们需要调查全景中的分组情况(图 5)。我们将分两部分解决这个问题,首先进行聚类,然后进行富集分析。两个步骤都可以使用 clusteval 库来完成。让我们加载库并开始对数据进行聚类。

聚类方法是 DBSCAN,评估方法是 Silhouette score。在 clusteval 中,对 DBSCAN 的 epsilon 参数进行网格搜索,并使用最佳的 Silhouette 评分进行聚类。在我们的案例中,我们检测到了 24 个簇(簇 0 到 23)。结果显示在下面的代码部分和图 6 中,展示了 epsilon 的网格搜索以及 Silhouette 系数。有关更多细节,建议阅读博客:从数据到簇:你的聚类何时足够好?

# Import library
from clusteval import clusteval

# Initialize clusteval
ce = clusteval(cluster='dbscan', metric='euclidean', linkage='complete', min_clust=7, normalize=True, verbose='info')

# Cluster evaluation
results = ce.fit(X)

图 6. 上面的面板展示了 DBSCAN 的网格搜索优化。左下角的面板展示了各个簇的样本轮廓系数。右下角的面板显示了输入数据集的簇标签(图片由作者提供)。

对于检测到的集群标签,我们可以使用富集分析分析驱动集群的特征。阅读 从集群到洞察;下一步获取更多详细信息。下面的代码部分描述了哪些特征与每个集群标签显著相关。我们可以手动探索这些结果,也可以制作一个散点图,将前 n 个特征叠加在散点图中(图 7 和 8)。

# Compute enrichment for each of the cluster labels
ce.enrichment(df)

# Show the significantly associated catagories for the cluster labels
print(ce.results['enrichment'])

#                 category_label              P  ...     category_name           Padj
# 0                  Entry-level   8.988604e-31  ...  experience_level   5.954950e-27
# 1             Junior Mid-level  3.818216e-294  ...  experience_level  2.547895e-290
# 2    Intermediate Senior-level   5.812236e-51  ...  experience_level   3.857000e-47
# 3             Junior Mid-level   4.519280e-43  ...  experience_level   2.997639e-39
# 4             Junior Mid-level   1.477602e-68  ...  experience_level   9.821622e-65
# ..                         ...            ...  ...               ...            ...
# 146         Medium (50 to 250)   6.991347e-12  ...      company_size   4.603802e-08
# 147               Large (>250)   1.424008e-61  ...      company_size   9.459684e-58
# 148       Small (less than 50)   1.487384e-55  ...      company_size   9.874743e-52
# 149         Medium (50 to 250)   4.985496e-22  ...      company_size   3.296410e-18
# 150         Medium (50 to 250)   1.461693e-06  ...      company_size   9.553627e-03

# [151 rows x 11 columns]

# Create scatter plot with enrichment results.
ce.scatter(n_feat=4, s=y/500, jitter=0.05, fontsize=14, density=True, params_scatterd={'marker':df['experience_level'], 'gradient':'opaque', 'dpi':200}, figsize=(40,30))

# Create dense areas with enrichment results.
ce.scatter(n_feat=4, s=0, jitter=0.05, fontsize=14, density=True, params_scatterd={'marker':df['experience_level'], 'gradient':'opaque', 'dpi':200}, figsize=(40,30))

图 7. tSNE 嵌入的散点图,显示了集群标签和每个集群的显著相关特征(图片由作者提供)。

图 8. 仅绘制每个集群显著相关特征的密度图。(图片由作者提供)。

使用 D3Blocks 进行互动可视化。

作为最终练习,我们将为 2D PCA 和 t-SNE 嵌入创建一个互动散点图。这有助于更好地理解单个样本的分布情况,缩放和平移可以帮助调查单个样本和/或组。

# Import libraries
from scatterd import scatterd, jitter_func
from d3blocks import D3Blocks, normalize
import numpy as np

# Initialize
d3 = D3Blocks()

tooltip = []
for i in range(0, df.shape[0]):
    tip = '<br>'.join(list(map(lambda x: x[0].replace('_', ' ').title()+': '+x[1], np.array(list(zip(df.columns, df.iloc[i,:].values))))))
    tip = tip + '<br>' + 'Salary: $' + str(y[i])
    tooltip.append(tip)

# Set all propreties
d3.scatter(jitter_func(X[:,0], jitter=1),      # tSNE x-coordinates
           jitter_func(X[:,1], jitter=1),      # tSNE y-coordinates 
           x1=jitter_func(model.results['PC']['PC1'].values, jitter=0.05), # PC1 x-coordinates
           y1=jitter_func(model.results['PC']['PC2'].values, jitter=0.05), # PC2 y-coordinates
           color=df['job_title'].values,       # Color on job title
           tooltip=tooltip,                    # Tooltip
           size=normalize(y.values, minscale=1, maxscale=25), # Node size on salary.
           opacity='opaque',                   # Create a tidy scatterplot by only highlighting dense areas
           stroke='#000000',                   # Edge color is black
           cmap='tab20',                       # Colormap
           scale=True,                         # Scale the datapoints
           label_radio=['tSNE', 'PCA'],
           figsize=[1024, 768],
           filepath='Data_science_landscape.html',
           )

图 9. 全球互动数据科学全景图。使用 D3Blocks 创建。(图片由作者提供)。

最后的总结。

我们开始这个研究以利用数据科学薪资数据集考察数据科学领域的趋势。我们可以得出结论:薪资较高的工作主要集中在欧洲以外,具有高级经验水平,并且在大型公司工作。远程工作似乎没有影响。这一趋势在 2020 年到 2023 年间保持不变。 我们创建了一个互动数据科学全景图,并使用富集分析分析了集群。互动散点图使用 D3blocks 库创建。我希望你在阅读这篇博客时感到愉快并有所收获。请记住,有两个重要的特征没有包含在数据集中:幸福感和与家人朋友共度的时间。—— 没有什么是永恒的,享受当下。

保持安全。保持清醒。

干杯,E.

如果你觉得这篇文章对你有帮助,欢迎 关注我 ,因为我会写更多类似的内容。如果你考虑购买 Medium 会员,可以通过使用我的 推荐链接支持我的工作。这与一杯咖啡的价格相同,但这允许你每月阅读无限数量的文章!

软件

让我们连接吧!

参考文献

  1. ai-jobs.net/salaries/download/ (CC0: 公众领域)

  2. 2020–2023 年全球 AI、ML、数据薪资趋势

  3. 用 Python 创建美丽的独立交互式 D3 图表 Medium,2022

  4. 从数据到簇:你的聚类效果何时足够好? Medium,2023

  5. 从簇到洞察:下一步**, Medium,2022

  6. 通过使用 D3js 和 Python 使散点图交互化,从中获得最大收益,Medium 2022 年 11 月

  7. t-SNE、UMAP、PCA 和其他映射的定量比较 Medium,2022

使用 Matplotlib 创建凸形图

原文:towardsdatascience.com/create-bump-charts-with-matplotlib-431b0e6fcb90

仅使用 Matplotlib 探索排名随时间的变化

Bruno PonneTowards Data Science Bruno Ponne

·发布于数据科学前沿 ·6 分钟阅读·2023 年 3 月 13 日

--

图片由blueberry Maki提供,来源于Unsplash

没有什么比变化更稳定的了。鲍勃·迪伦

当我还是青少年时,我喜欢查看 Billboard 上最受欢迎的歌曲,尽管我的口味通常与 Billboard 展示的内容不同。通常,我最喜欢的歌曲最终未能进入前列。尽管如此,这对我来说仍然是一个获取新歌和好歌的极佳来源。我甚至喜欢查看过去哪些歌曲曾经流行。例如,我发现,在我出生的那一周,即 1987 年 8 月,I Still Haven’t Found What I’m Looking ForHot Billboard 100的第一名!

人类总是在比较、评估和排名生活的各个方面。2023 年最好的足球队是什么?2022 年最好的网球运动员是谁,去年在 GitHub 上使用最多的编程语言是什么?我们想知道现在的趋势是什么。但正如生活中的一切一样,排名总是在变化。

在本课程中,你将学习如何使用基础的Matplotlib,无需额外的库来显示排名的变化。作为示例,你将使用Octoverse Report 2022发布的数据,该报告分析了 2022 年最受欢迎的编程语言。

1. 什么是凸形图

凸形图类似于折线图,但专注于探索排名随时间的变化。例如,想象一下下图中的每条线代表一位歌手的受欢迎程度排名。这条线及其颜色代表歌手,x 轴代表年份,y 轴代表排名。

作者使用 Matplotlib 创建的图像

2. 我们的竞争对手:编程语言

根据Octoverse,在 2022 年,程序员在 GitHub 上使用了大约 500 种语言来开发软件。JavaScript 是使用最广泛的语言,其次是 Python,我们将使用它来构建我们的气泡图。

报告还揭示了Hashicorp 配置语言(HCL)是 GitHub 上增长最快的语言,反映了云基础设施的扩展。Rust 和 Typescript 分别位居增长的第二和第三。

有几种排名使用不同的数据和方法来估计每种语言的位置。另一个排名是Stack Overflow 2020 开发者调查,它提供了类似但不完全相同的结果。这篇文章将使用 Octoverse 数据作为示例。

为了更容易复现,数据是通过代码生成并存储在字典列表中,如下所示。

years_list = list(range(2014,2023,2))

list_programming = [

{
    'Name' : ["Javascript" for i in range(5)],
    'Year' : years_list,
    'Rank' : [1,1,1,1,1]
},

{
    'Name' : ["Python" for i in range(5)],
    'Year' : years_list,
    'Rank' : [4,3,3,2,2]
},

{
    'Name' : ["Java" for i in range(5)],
    'Year' : years_list,
    'Rank' : [2,2,2,3,3]
},

{
    'Name' : ["Typescript" for i in range(5)],
    'Year' : years_list,
    'Rank' : [10,10,7,4,4]
},

{
    'Name' : ["C#" for i in range(5)],
    'Year' : years_list,
    'Rank' : [8,6,6,5,5]
},

{
    'Name' : ["C++" for i in range(5)],
    'Year' : years_list,
    'Rank' : [6,5,5,7,6]
},

{
    'Name' : ["PHP" for i in range(5)],
    'Year' : years_list,
    'Rank' : [3,4,4,6,7]
},

{
    'Name' : ["Shell" for i in range(5)],
    'Year' : years_list,
    'Rank' : [9,9,9,9,8]
},

{
    'Name' : ["C" for i in range(5)],
    'Year' : years_list,
    'Rank' : [7,8,7,7,9]
},

{
    'Name' : ["Ruby" for i in range(5)],
    'Year' : years_list,
    'Rank' : [5,7,10,10,10]
}

]

3. Matplotlib 子图方法

有几种方法可以用 Matplotlib 创建图形,但为了获得灵活性,建议使用subplots()。此方法创建两个对象:一个Figure类的对象和一个Axes类的对象。Figure对象将是你的图形的容器,而Axes对象将是图形本身。

作者创建的图像

下面的代码加载所需的库,并创建刚才提到的两个对象。

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()

4. 设置图形大小

在 Matplotlib 中,你可以使用plt.rcParams["figure.figsize"]改变图形的大小。我们将其设置为宽 12 英寸,高 6 英寸。

plt.rcParams["figure.figsize"] = (12,6)

5. 为每种编程语言调用绘图方法

对于列表中的每个字典,我们将调用 axplot方法,指定 x 轴上的年份和 y 轴上的排名。此外,你可以选择标记和线条的样式,“o-”表示我们希望使用点作为标记的线条。请注意,标记的面颜色设置为白色,这意味着点是填充白色的。

结果几乎符合我们的期望,但仍需要进一步调整。

for element in list_programming:
  ax.plot(element["Year"], 
          element["Rank"], 
          "o-",                       # format of marker / format of line
          markerfacecolor="white")

作者使用 Matplotlib 创建的图像

6. 反转 y 轴和设置轴刻度

最好将排名第一的语言放在图表的顶部。此外,我们希望所有排名数字都显示在 y 轴上。

我们可以使用命令plt.gca().invert_yaxis()来实现。此外,我们可以通过将带有值的NumPy数组传递给plt.yticks()来设置 y 刻度。NumPy 数组可以通过np.arange()创建。

plt.gca().invert_yaxis()
plt.yticks(np.arange(1, 11, 1))

7. 标记线条

我们需要确定每条线对应的编程语言。为此,我们可以使用ax *annotate*方法。它接收的第一个参数是我们想要注释的文本。我们将使用list_programming["Name"][0]来获取语言名称。

xy参数是我们希望注释的点。在我们的案例中,它是每条线的末端。xytext参数是我们希望添加文本的点。注意xytext几乎与xy相同,但在 x 轴上稍微向右。最后,va表示垂直对齐。

ax.annotate(element["Name"][0], 
              xy=(2022, element["Rank"][4]), 
              xytext=(2022.2,element["Rank"][4]), 
              va="center")

8. 在 Matplotlib 中更改线宽

表示每种语言路径的线条相对较细,我们可以通过在plot method中的linewidth参数来增加其宽度。

9. 清除图形

为了使图形更清晰,可以隐藏图形的框架。为此,请注意每个Axes对象有 4 个脊柱。一个脊柱是图形框架的一侧。我们可以用 for 循环迭代它们,并将它们的可见性属性设置为False。查看下面的所有这些调整。

for element in list_programming:
  ax.plot(element["Year"], 
          element["Rank"], 
          "o-", # format of marker / format of line
          markerfacecolor="white",
          linewidth=3)
  ax.annotate(element["Name"][0], 
              xy=(2022, element["Rank"][4]), 
              xytext=(2022.2,element["Rank"][4]), 
              va="center")

plt.gca().invert_yaxis()
plt.yticks(np.arange(1, 11, 1))

for spine in ax.spines.values():
    spine.set_visible(False)

图片由作者使用 Matplotlib 创建

要好得多,是不是?

这个碰撞图不需要任何额外的库。此外,Matplotlib 允许你以多种方式自定义它!在这篇文章中,我展示了使用 Matplotlib 绘制引人注目的可视化图形的进一步建议。

10. 2022 年排名前十的编程语言

现在我们可以清晰地看到编程语言在过去十年中的演变情况。

自 2014 年以来,JavaScript 一直保持领先地位。根据伯克利训练营,JavaScript 的流行是因为大多数网页浏览器都使用它。2014 年,Python 是第四大使用语言,自那时以来,Python 的受欢迎程度不断增长。如今,它是 GitHub 上第二大使用语言。最后,Java 的受欢迎程度有所下降,但仍然是第三大使用语言。

结论

在这篇文章中,你学会了用基本的 Matplotlib 图形显示排名变化。为此,不需要额外的库,只需理解 Matplotlib 对象及如何自定义它们以展示你的数据。

如果你喜欢这篇文章,关注我以了解更多数据可视化的内容!

使用 Matplotlib 创建引人注目的径向条形图

原文:towardsdatascience.com/create-eye-catching-radial-bar-charts-with-matplotlib-fd03ff732048

在 Python 中轻松创建视觉上吸引人的圆形条形图

Andy McDonaldTowards Data Science Andy McDonald

·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 3 月 6 日

--

使用 matplotlib 创建的径向条形图。图像由作者提供。

径向条形图是传统条形图的一个视觉上吸引人的替代方案。数据在极坐标系统中绘制,而不是常规的笛卡尔坐标系统。这使得条形可以用环形表示,而不是垂直条形。

如果你希望吸引读者的注意力,径向条形图可以成为演示文稿或海报中一个很棒且视觉上吸引人的图形。然而,与许多数据可视化一样,它们也有自己的缺点。其中一个问题是它们可能很难解释。这是因为我们的视觉系统在比较类别之间的值时更擅长解释直线。此外,径向条形图中心的环形比外围的环形更难以读取和比较。

本文将展示如何使用 matplotlib 库从 Python 创建一个视觉上吸引人的径向条形图。

当提到 matplotlib 时,大多数人会想到基本的图表,这些图表格式不佳,需要多行笨拙或令人困惑的代码才能显示出良好的图表。

使用 Matplotlib 创建径向条形图

导入库和加载数据

创建径向条形图的第一步是导入我们将要使用的库。在本教程中,我们需要导入 matplotlibpandasnumpy

import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

导入库之后,我们可以加载或创建一些数据。在 Python 中,最常见的数据存储格式之一是 pandas 数据框。

数据可以通过 pd.read_csv() 函数从 CSV 文件加载到数据框中,也可以通过列表或字典手动创建。

在本教程中,我们将使用虚拟岩性数据创建一个数据框。我们将有多种岩性和每种岩性在特定间隔内出现的计数。

为了创建我们的数据框,我们首先创建一个包含两个键 LITHCOUNT 的字典,每个项的值将是包含岩性名称和每种岩性的计数的列表。

lith_dict = {'LITH': ['Shale', 'Sandstone', 
                      'Sandstone/Shale', 'Chalk', 
                      'Limestone', 'Marl', 'Tuff'],
             'COUNT': [40,65, 40, 35, 
                            40, 70, 50]}

df = pd.DataFrame.from_dict(lith_dict)

一旦数据加载完成,我们需要找出两件事:

  • 我们的 COUNT 列的最大值。这将给出我们环的最大值。

  • 数据框中的总条目数

max_value_full_ring = max(df['COUNT'])
data_len = len(df)

选择颜色和设置标签

创建径向条形图的下一步是定义将显示的颜色和标签。

ring_colours = ['#2f4b7c', '#665191', '#a05195','#d45087',
               '#f95d6a','#ff7c43','#ffa600']

为图表选择颜色可能非常主观。

选择有效的图表颜色调色板时,有许多工具和文章可以帮助你。大多数基于颜色理论。

对于这个图表,我基于以下网站的颜色:

[## 数据可视化颜色选择器

使用调色板选择器创建一系列视觉上等距的颜色。这对于许多数据…

www.learnui.design

该工具允许你在构建调色板时选择多达 8 种颜色,并且还允许你复制十六进制代码,然后可以直接粘贴到你的代码中。

来自 数据可视化颜色选择器 的调色板生成器工具。图像由作者提供。

为了设置标签,我们需要创建一个列表推导式,该推导式遍历岩性名称和计数。然后,这些内容会以 Lithology (Count) 格式的字符串形式连接在一起。

此外,在字符串的开头,我们可以添加几个空格。这是一种在标签和环的起始位置之间添加一些填充的简单方法。

ring_labels =  [f'   {x} ({v}) ' for x, v in zip(list(df['LITH']), 
                                                 list(df['COUNT']))]

当我们检查 ring_labels 变量时,我们得到以下列表:

['   Shale (40) ',
 '   Sandstone (65) ',
 '   Sandstone/Shale (40) ',
 '   Chalk (35) ',
 '   Limestone (40) ',
 '   Marl (70) ',
 '   Tuff (50) ']

使用 Matplotlib 创建径向条形图图形

现在数据已经加载完毕,我们可以专注于构建图表。

要创建图表,我们首先像创建其他 matplotlib 图表一样创建图形。我们还将传入几个参数到 plt.figure() 调用中。第一个是图形大小,我们将其设置为 10 x 10。这可以设置为你想要的任何大小,但这个大小在屏幕上查看时效果很好。

接下来,我们将传入 linewidthedgecolor 参数。这些将为我们的图表添加一个 10 px 的边框,该边框的颜色与图表背景略有不同。

最后,我们将把facecolor设置为非常深的蓝色。

fig = plt.figure(figsize=(10,10), linewidth=10,
                 edgecolor='#393d5c', 
                 facecolor='#25253c')

如果我们运行上述代码,我们将看到显示的方形图形,具有我们的边框和主图形背景。

径向条形图的背景和边框。图像由作者提供。

添加极坐标轴

下一步是向我们的图表添加一个使用极坐标而非笛卡尔坐标的坐标轴。

但是,在添加坐标轴之前,我们首先需要创建坐标轴的形状。

这通过创建一个名为rect的变量并将其分配给一个包含 4 个数字的列表来完成。这些数字代表起始位置(x 和 y 坐标)、高度和宽度。

如果我们想给径向条形图添加一些填充,我们需要将宽度和高度参数设置为小于 1 的值。在这种情况下,我们将其设置为 0.8,表示占用 80%的可用宽度和高度。

rect = [0.1,0.1,0.8,0.8]

然后,我们可以使用fig.add_axes创建一个新坐标轴,并传入rect列表,同时将极坐标参数设置为真,并去除坐标轴框架。

随后,我们需要将径向条形设置为从图表的顶部开始,即北方(N),并且我们将设置条形逆时针辐射。如果我们希望条形顺时针辐射,则需要将set_theta_direction()更改为-1。

# Add axis for radial backgrounds
ax_polar_bg = fig.add_axes(rect, polar=True, frameon=False)
# Start bars at top of plot
ax_polar_bg.set_theta_zero_location('N')
# Make bars go counter-clockwise. 
ax_polar_bg.set_theta_direction(1)

向径向条形图中添加微弱的背景环

为了给我们的图表增加一些视觉趣味,我们将添加一些从 0 到最大值的微弱条形。这会给人一种条形尚未填充的错觉。

# Loop through each entry in the dataframe and plot a grey
# ring to create the background for each one
for i in range(data_len):
    ax_polar_bg.barh(i, max_value_full_ring*1.5*np.pi/max_value_full_ring, 
                     color='grey', 
                     alpha=0.1)

由于我们希望在图表的右上角留出空间用于标签,我们将设置条形仅完成 3/4 的圆圈。这是通过以下代码完成的,这段代码传递给ax_polar_bg.barh调用。

max_value_full_ring*1.5*np.pi/max_value_full_ring

如果我们此时运行代码,我们将看到如下图表和径向条形图的初步效果。

径向条形图显示数据添加前的极坐标轴。图像由作者提供。

我们可以通过调用.axis()并传入单词'off'来隐藏极坐标轴。

# Hide all axis items
ax_polar_bg.axis('off')

当我们重新运行代码时,我们会看到一个干净的图表,其中包含空的径向条。

微弱的径向条显示 0 到 100%的范围,用于创建背景。图像由作者提供。

向径向条形图中添加彩色环

现在我们准备为图表添加一些颜色。

为此,我们将向图形中添加一个新坐标轴,其代码与添加微弱背景条形的代码非常相似。

set_rgrids()函数中,我们将传入一个包含 0 到 6 的列表。这些数字代表径向网格线的半径。此外,我们还将传入标签,并在此函数中设置它们的外观。为了使标签出现在图表的右上角并且靠近环的起始点,我们需要将角度设置为 0。

一旦设置好径向网格,我们需要遍历数据框中的每个类别,并使用 matplotlib 中的 barh() 函数向图表添加一个圆环。

与前一节代码类似,我们只希望圆环走到 3/4 的位置,因此我们需要将 pi 乘以 1.5,而不是 2。

 # Add axis for radial chart for each entry in the dataframe
ax_polar = fig.add_axes(rect, polar=True, frameon=False)
ax_polar.set_theta_zero_location('N')
ax_polar.set_theta_direction(1)
ax_polar.set_rgrids([0, 1, 2, 3, 4, 5, 6], 
                    labels=ring_labels, 
                    angle=0, 
                    fontsize=14, fontweight='bold',
                    color='white', verticalalignment='center')

# Loop through each entry in the dataframe and create a coloured 
# ring for each entry
for i in range(data_len):
    ax_polar.barh(i, list(df['COUNT'])[i]*1.5*np.pi/max_value_full_ring, 
                  color=ring_colours[i])

当我们现在运行代码时,我们将得到以下图形,这个图形完成度为 95%。

带有极坐标网格线和刻度标签的径向条形图。图片由作者提供。

隐藏 Matplotlib 极坐标图中的极坐标网格线

最后,我们需要去除极坐标网格线、刻度和相关标签。可以这样做:

# Hide all grid elements for the    
ax_polar.grid(False)
ax_polar.tick_params(axis='both', left=False, bottom=False, 
                   labelbottom=False, labelleft=True)

plt.show()

然后生成我们的最终径向条形图。

使用 matplotlib 生成的最终径向条形图。图片由作者提供。

生成径向条形图的完整 Python 代码

这是生成上述径向条形图的完整 Python 代码,使用 matplotlib。

import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

#Load data into pandas dataframe

lith_dict = {'LITH': ['Shale', 'Sandstone', 
                      'Sandstone/Shale', 'Chalk', 
                      'Limestone', 'Marl', 'Tuff'],
             'COUNT': [40,65, 40, 35, 
                            40, 70, 50]}

df = pd.DataFrame.from_dict(lith_dict)

# Get key properties for colours and labels
max_value_full_ring = max(df['COUNT'])

ring_colours = ['#2f4b7c', '#665191', '#a05195','#d45087',
               '#f95d6a','#ff7c43','#ffa600']

ring_labels = [f'   {x} ({v}) ' for x, v in zip(list(df['LITH']), 
                                                 list(df['COUNT']))]
data_len = len(df)

# Begin creating the figure
fig = plt.figure(figsize=(10,10), linewidth=10,
                 edgecolor='#393d5c', 
                 facecolor='#25253c')

rect = [0.1,0.1,0.8,0.8]

# Add axis for radial backgrounds
ax_polar_bg = fig.add_axes(rect, polar=True, frameon=False)
ax_polar_bg.set_theta_zero_location('N')
ax_polar_bg.set_theta_direction(1)

# Loop through each entry in the dataframe and plot a grey
# ring to create the background for each one
for i in range(data_len):
    ax_polar_bg.barh(i, max_value_full_ring*1.5*np.pi/max_value_full_ring, 
                     color='grey', 
                     alpha=0.1)
# Hide all axis items
ax_polar_bg.axis('off')

# Add axis for radial chart for each entry in the dataframe
ax_polar = fig.add_axes(rect, polar=True, frameon=False)
ax_polar.set_theta_zero_location('N')
ax_polar.set_theta_direction(1)
ax_polar.set_rgrids([0, 1, 2, 3, 4, 5, 6], 
                    labels=ring_labels, 
                    angle=0, 
                    fontsize=14, fontweight='bold',
                    color='white', verticalalignment='center')

# Loop through each entry in the dataframe and create a coloured 
# ring for each entry
for i in range(data_len):
    ax_polar.barh(i, list(df['COUNT'])[i]*1.5*np.pi/max_value_full_ring, 
                  color=ring_colours[i])

# Hide all grid elements for the    
ax_polar.grid(False)
ax_polar.tick_params(axis='both', left=False, bottom=False, 
                   labelbottom=False, labelleft=True)

plt.show()

总结

径向条形图提供了比传统条形图更具视觉吸引力的替代方案,并且可以轻松地使用 Python 的 matplotlib 库创建。在本文中,我们已经展示了如何从头开始使用 matplotlib 构建一个引人注目的径向条形图,它可以轻松用于海报展示或信息图表中。

感谢阅读。在离开之前,你应该订阅我的内容,并将我的文章送到你的邮箱。 你可以在这里订阅!或者,你可以 注册我的新闻通讯 以免费获取额外的内容。

其次,你可以获得完整的 Medium 体验,并通过注册会员支持我和其他成千上万的作者。每月仅需 $5,你就可以完全访问所有精彩的 Medium 文章,并有机会通过你的写作赚取收入。如果你通过 我的链接注册你将直接通过部分费用支持我,并且不会多花费。如果你这样做,非常感谢你的支持!

用 Tkinter 创建银河艺术

原文:towardsdatascience.com/create-galactic-art-with-tkinter-e0418a59b215

用对数螺旋模拟大自然

Lee VaughanTowards Data Science Lee Vaughan

·发表在 Towards Data Science ·11 min 阅读·2023 年 9 月 30 日

--

螺旋星系模型(作者提供)

我们世界的奇妙之处在于它可以用数学来描述。这种联系如此紧密,以至于麻省理工学院的物理学家马克斯·特格马克相信,宇宙不仅仅是用数学来描述的,而且在某种意义上,它就是数学,因为我们都是一个巨大的数学对象的一部分 [1]。

这意味着许多看似复杂的物体——跨越令人困惑的尺度——可以简化为简单的方程。为什么飓风看起来像一个星系?为什么鹦鹉螺壳中的图案在松果中重复?答案是数学。

自然界中的对数螺旋示例(摘自“Python 科学工具” [2])

除了外观,上面图片中的物体还有一个共同点:它们都生长,而自然界中的生长是几何级数。几何增长的螺旋被称为对数螺旋,因为在描述它们的方程中使用了自然对数的底数(e)。虽然通常被称为对数螺旋,但它们在自然界中的普遍存在使它们获得了另一个称号:spira mirabilis — “奇迹螺旋”。

在这个快速成功的数据科学项目中,我们将使用对数螺旋和 Python 的Tkinter GUI 模块来模拟一个螺旋星系。在此过程中,我们将生成一些吸引人且独特的数字艺术作品。

对数螺旋的极坐标方程

模拟螺旋星系主要是模拟螺旋臂。每个螺旋臂可以用对数螺旋近似

螺旋星系的示意图(作者提供)

因为螺旋从中心点或 极点 辐射出来,你可以用 极坐标 更容易地绘制它们。在这个系统中,熟悉的笛卡尔坐标系中的 (x, y) 坐标被 (r, Ɵ) 替代,其中 r 是距离中心的距离,Ɵ(theta)是 r 和 x 轴之间的角度。极点的坐标是 (0, 0)。

极坐标系示例(来自“Impractical Python Projects” [3])

对数螺旋的极坐标方程是:

其中 r 是半径(从原点的距离),Ɵ 是从 x 轴逆时针测量的角度,e 是自然对数的底数,a 是控制大小的缩放因子,b 是控制螺旋的“开口度”和增长方向的增长因子。

编程策略

为了模拟一个四臂螺旋星系,我们可以使用前面的公式绘制一个螺旋,然后旋转并重新绘制螺旋三次。我们将用各种大小的标记构建螺旋,这些标记将代表星星。虽然这些标记对于单个星星来说规模过大,但它们将捕捉银河的整体亮度模式(见标题图片)。

为了捕捉银河的“背景光辉”,我们将随机分布一些小“星星”在银河盘周围。

Tkinter 库

我们将用 tkinter(发音为“tee-kay-inter”)绘制我们的银河模拟。这是开发 Python 桌面应用程序的默认 GUI 库。尽管主要设计用于窗口、按钮和滚动条等 GUI 元素,tkinter 还可以生成图表、屏保、简单游戏等。

作为标准 Python 发行版的一部分,tkinter 在所有操作系统上都是可移植的,无需安装外部库。你可以在 这里 找到官方在线文档。

代码

以下代码是在 Spyder IDE 中编写的。你可以从这个 Gist 下载完整脚本。

导入库和分配常量

我们需要使用的所有库都属于 Python 标准库,因此无需安装任何东西。除了 tkinter,我们还需要 math 模块来处理方程,random 模块来随机生成星星的位置。后者将使模拟 随机,因此每次都稍有不同和独特,以防你想制作一些 NFT。

import math
import tkinter as tk
from random import randint, uniform, random

# Set the scaling factor for the galaxy:
SCALE = 225

SCALE 常量将改变我们银河的大小。大数值将增加盘的直径,小数值将减少它。

设置 tkinter 显示画布

以下代码创建了一个带有 画布tkinter 窗口,你可以在上面绘制东西。这里将出现银河模拟。

# set-up tkinter display canvas:
root = tk.Tk()
root.title("A Spiral Galaxy")
c = tk.Canvas(root, width=1000, height=800, bg='black')
c.grid()
c.configure(scrollregion=(-500, -400, 500, 400))

我们首先创建一个名为root的窗口。这是一个顶级窗口,包含其他所有内容。在命名窗口后,我们将一个小部件(“Windows 小工具”)添加到根窗口。这个Canvas小部件被分配给变量c,是一个用于图形和其他复杂布局的通用小部件,它将包含我们所有的绘图对象。

接下来,我们在c上调用grid()几何管理器,并通过配置画布以使用scrollregion来完成。这段代码通过使用画布宽度和高度的一半将原点坐标(0, 0)设置为画布的中心。我们需要这个设置来用极坐标绘制银河的螺旋臂。没有它,默认原点将是画布的左上角。

定义绘制螺旋臂的函数

接下来,我们将定义一个使用对数螺旋方程绘制单个螺旋臂的函数。这个螺旋可能是神奇的,但大部分魔力在于调整初始的基础螺旋以“充实”臂部。我们将通过改变星星的大小和位置,并为每个臂部复制螺旋并稍微向后移动,同时减弱其星星的亮度来实现。这会创建前导后续边缘。

def build_spiral(b, r, rot_fac, fuz_fac, arm):
    """Build a spiral arm for tkinter display with Logarithmic spiral formula.

    b = growth factor: negative = spiral to left; larger = more open
    r = radius
    rot_fac = rotation factor for spiral arm
    fuz_fac = fuzz factor to randomly shift star positions 
    arm = spiral arm (0 = leading edge, 1 = trailing stars)
    """
    spiral_stars = []
    fuzz = int(0.030 * abs(r))  # Scalable initial amount to shift locations
    for i in range(0, 800, 2):  # Use range(520) for central "hole"
        theta = math.radians(-i)
        x = r * math.exp(b*theta) * math.cos(theta - math.pi * rot_fac)\
            - randint(-fuzz, fuzz) * fuz_fac
        y = r * math.exp(b*theta) * math.sin(theta - math.pi * rot_fac)\
            - randint(-fuzz, fuzz) * fuz_fac
        spiral_stars.append((x, y))

    for x, y in spiral_stars:
        if arm == 0 and int(x % 2) == 0:
            c.create_text(x, y, 
                          fill='white', 
                          font=('Helvetica', '6'), 
                          text='*')
        elif arm == 0 and int(x % 2) != 0:
            c.create_text(x, y, 
                          fill='white', 
                          font=('Helvetica', '5'), 
                          text='*')
        elif arm == 1:
            c.create_text(x, y, 
                          fill='white', 
                          font=('Helvetica', '5'), 
                          text='.')

在这里,我们定义了一个具有五个参数的函数。前两个br来自螺旋方程。接下来的是rot_fac,它是旋转因子,让你可以围绕中心点移动螺旋,以便在不同的位置生成新的螺旋臂。

模糊因子fuz_fac允许你调整星星从螺旋线中心移动的距离。最后,arm参数让你指定前导臂或后续臂的微弱星星。后续臂将被移动 —— 即稍微落在前导臂的后面 —— 并且其星星将更小(更暗)。

为了启动该功能,我们初始化一个空列表,称为spiral_stars,用于保存星星的坐标。接下来,我们将一个fuzz变量赋值为一个任意常数乘以r(半径)值的绝对值。这表示一个用于移动星星的起始点,我们将用fuz_fac模糊因子稍后进行修改。它还确保了位移量与显示器的大小可扩展

单独使用对数螺旋方程会产生排成一行的星星,如下图的左侧两个面板所示。“模糊”会使星星在螺旋线的两侧来回移动。你可以在图的最右侧面板中看到对亮星的效果。

通过移动螺旋和改变星星位置来填充螺旋臂(摘自“非实用的 Python 项目” [3])

下一步是绘制螺旋线。我们使用一系列值来表示方程中的Ɵ。你可以调整这些值以产生不同的结果(毕竟这是艺术)。

接下来,我们循环遍历 Ɵ 值并应用对数螺旋方程,将随机化的fuzz值(乘以fuzz_factor)加到结果中。我们通过将坐标追加到列表中来完成循环。

后续,我们将多次调用这个函数,并指定旋转因子(rot_fac)变量,它将使螺旋在中心周围移动。在程序构建四个主要的螺旋臂后,它将使用rot_fac构建四个新的螺旋臂,略微偏离前四个,以产生一条条暗淡的、拖尾的星星带。你可以在之前图的最右面板中看到这些星星;它们是每条亮星弧的左侧的暗淡星星弧。

在构建了星星位置列表后,我们循环遍历它,使用条件语句选择前导和后随的螺旋臂。虽然tkinter有绘制圆形(使用create_oval()方法)的能力,但我发现标点符号在微小尺度下效果更好。因此,我们使用了create_text()方法。

如前所述,这些星星对象只是为了视觉冲击。它们的大小和数量都不按比例计算。为了更真实,它们会小得多,并且数量会多得多(超过 1000 亿!)。

星星雾霾散布

螺旋臂之间的空间并不是没有星星,所以我们需要一个函数在整个银河模型中随机分布点。把这些点看作是你在银河照片中看到的“雾霾”或“光晕”。

首先,我们需要一个函数来生成随机的极坐标。

def random_polar_coordinates(scale_factor):
    """Generate uniform random x,y point within a 2-D disc."""
    n = random()
    theta = uniform(0, 2 * math.pi)
    x = round(math.sqrt(n) * math.cos(theta) * scale_factor)
    y = round(math.sqrt(n) * math.sin(theta) * scale_factor)
    return x, y

这个函数以我们之前定义的SCALE常量作为唯一参数。然后,它选择一个 0.0 到 1.0 之间的浮点值,并将其赋值给一个名为n的变量。

接下来,它从 0 到 360 度(2π是 360 度的弧度等效值)的均匀分布中随机选择theta

最后,它会计算单位圆盘上的 x 和 y 值,得到-1 到 1 之间的值,然后将这些值乘以比例因子,将结果缩放到我们银河系模型的大小。

下一个函数在显示上绘制雾霾星星。它以我们的比例因子常量和一个整数乘数(density)作为参数,后者用于增加或减少随机星星的基础数量。所以,如果你希望雾霾更浓而不是轻微的雾霾,就增加density参数的值。

def star_haze(scale_factor, density):
    """Randomly distribute faint stars in the galactic disc.

    SCALE = scaled galactic disc radius
    density = multiplier to vary the number of stars posted
    """
    for _ in range(0, scale_factor * density):
        x, y = random_polar_coordinates(scale_factor)
        c.create_text(x, y, 
                      fill='white', 
                      font=('Helvetica', '3'), 
                      text='.')

为了绘制这些星星,我们使用了和螺旋臂相同的tkinter方法create_text()。这是有星星雾霾和没有星星雾霾的结果。

没有雾霾(左)和有雾霾(右)(作者)

你可以发挥创造力来处理雾霾。例如,增加密度并将其着色为灰色,或者使用循环来变化它们的大小和颜色。然而,不要使用绿色,因为宇宙中没有绿色的星星!

定义一个函数来构建显示

现在我们将定义一个函数来构建四个主要的螺旋臂并分布星云。每个螺旋臂将包括对之前定义的build_spiral()函数的两次调用:一次用于前缘的明亮星星(arm=0),一次用于后缘的较暗星星(arm=1)。

随意调整参数。我建议一次更改一个,以便更好地判断它们的影响。

def build_galaxy():
    """Generate the galaxy display with tkinter."""
    b=0.3
    fuz_fac=1.5

    # Build leading and trailing spiral arms:
    build_spiral(b=b, r=SCALE, rot_fac=2, fuz_fac=fuz_fac, arm=0)
    build_spiral(b=b, r=SCALE, rot_fac=1.91, fuz_fac=fuz_fac, arm=1)

    build_spiral(b=b, r=-SCALE, rot_fac=2, fuz_fac=fuz_fac, arm=0)
    build_spiral(b=b, r=-SCALE, rot_fac=-2.09, fuz_fac=fuz_fac, arm=1)

    build_spiral(b=b, r=-SCALE, rot_fac=0.5, fuz_fac=fuz_fac, arm=0)
    build_spiral(b=b, r=-SCALE, rot_fac=0.4, fuz_fac=fuz_fac, arm=1)

    build_spiral(b=b, r=-SCALE, rot_fac=-0.5, fuz_fac=fuz_fac, arm=0)
    build_spiral(b=b, r=-SCALE, rot_fac=-0.6, fuz_fac=fuz_fac, arm=1)

    # Distribute star haze:
    star_haze(SCALE, density=30)

    # run tkinter loop:
    root.mainloop()

build_galaxy()

build_galaxy()函数的输出(作者提供)

在调用build_galaxy()函数后,应该会弹出一个外部窗口(或显示为任务栏上的图标)。你需要在再次调用函数之前关闭这个窗口。

如前所述,每个显示效果都是绝对独特的。为了增加深度和趣味,可以尝试将前缘(arm=0)的一半星星更改为浅蓝色或类似颜色。

输出结果,其中一半主要螺旋臂星星为浅蓝色(作者提供)

不要局限于对数螺旋

从数字艺术的角度来看,构建类银河物体的方法不仅仅限于对数螺旋,还有更多可以利用模型做的事情。以下是我书中的一些例子,《不切实际的 Python 项目》[3]

一个遥远的银河系

以下模型灵感来源于 Alexandre Devert 在 Marmakoide 的博客上的帖子,“在圆盘和球体上分布点” [4]。

一个遥远的虚构银河系(作者提供)

大胆前行

银河系的半径大约是 50,000 光年。当我们的比例因子(SCALE)设置为 200 时,每个像素代表约 250 光年(50,000 / 200)。知道这一点后,我们可以想象星际迷航联邦在其前 100 年中可能探索了多少银河系,假设他们在 4 级曲速下的平均速度为 100 倍光速:

星际迷航联邦在前 100 年内以 4 级曲速扩展的情况(来源:《不切实际的 Python 项目》 [3])

对于这个模拟,我使用了 200 的比例因子和地球坐标(130, 80)。

无法测量的天堂

我们的无线电传输当前形成一个围绕地球的扩展球体,直径约为 230 光年。那基本上是前面图像中最小的星星之一的大小。正如卡尔·萨根所描述的,地球“只是悬浮在阳光中的一粒尘埃。”

当你考虑到这一点时,你会开始欣赏我们银河系的宏大和空旷。天文学家甚至有一个词来描述这个:拉尼 akea,夏威夷语意为“无法测量的天堂”。

摘要

利用 Python、tkinter和一个简单的方程,我们将一个螺旋星系模型转变为一些引人注目的数字艺术,这帮助我们设想自己在宇宙中的位置。这个模型具有多个调整参数和随机基础,让你创造出美丽且独特的艺术作品。

参考文献

  1. Tegmark, Max, 2014, “宇宙是由数学构成的吗?” 科学美国人www.scientificamerican.com/article/is-the-universe-made-of-math-excerpt/.

  2. Vaughan, Lee, 2023, 科学家的 Python 工具:Anaconda、JupyterLab 和 Python 科学库入门,No Starch Press,旧金山。

  3. Vaughan, Lee, 2018, 不切实际的 Python 项目:有趣的编程活动让你更聪明,No Starch Press,旧金山。

  4. Devert, Alexandre, 2012, “在圆盘和球体上分布点,” Marmakoide’s Blogblog.marmakoide.org/?m=201204.

谢谢!

感谢阅读,请关注我以获取更多快速成功的数据科学项目更新。

创建一个本地 dbt 项目

原文:towardsdatascience.com/create-local-dbt-project-e12c31bd3992

如何使用 Docker 创建一个带有虚拟数据的本地 dbt 项目以用于测试

Giorgos MyrianthousTowards Data Science Giorgos Myrianthous

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

--

照片由 Daniel K Cheung 拍摄,来源于 Unsplash

dbt(数据构建工具)是数据工程和分析领域最热门的技术之一。最近,我一直在处理一个任务,该任务对 dbt 生成的产物进行一些后处理,并且想编写一些测试。为此,我不得不创建一个可以在本地(或在 docker 容器中)运行的示例项目,以便我不必与实际的数据仓库进行交互。

在这篇文章中,我们将逐步介绍创建 dbt 项目并将其与容器化的 Postgres 实例连接的过程。你可以使用这样的项目进行测试,甚至可以用来尝试 dbt 本身的功能,或者练习你的技能。

订阅数据管道,这是一个专注于数据工程的新闻通讯

步骤 1:创建一个 dbt 项目

我们将向 Postgres 数据库中填充一些数据,因此我们首先需要从 PyPI 安装 dbt Postgres 适配器:

pip install dbt-postgres==1.3.1

请注意,命令还会安装 dbt-core 包以及运行 dbt 所需的其他依赖项。

现在,让我们继续创建一个 dbt 项目——为此,我们可以通过在终端中运行 dbt init 命令来初始化一个新的 dbt 项目:

dbt init test_dbt_project

然后系统会提示你选择要使用的数据库(根据你本地安装的适配器,你可能会看到不同的选项):

16:43:08  Running with dbt=1.3.1
Which database would you like to use?
[1] postgres

(Don't see the one you want? https://docs.getdbt.com/docs/available-adapters)

Enter a number: 1

确保输入与 Postgres 适配器对应的数字,如输出列表中所示。现在,init 命令应该在你执行的目录中创建了以下基本结构:

dbt init 命令创建的 dbt 项目结构 — 来源:作者

步骤 2:创建一个 Docker Compose 文件

现在让我们创建一个 docker-compose.yml 文件(将文件放在与 test_dbt_project 目录相同的级别),在该文件中我们将指定两个服务 — 一个对应于现成的 Postgres 镜像,另一个对应于我们将在下一步的 Dockerfile 中定义的 dbt 镜像:

version: "3.9"

services:
  postgres:
    container_name: postgres
    image: frantiseks/postgres-sakila
    ports:
      - '5432:5432'
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
  dbt:
    container_name: dbt
    build: .
    image: dbt-dummy
    volumes:
      - ./:/usr/src/dbt
    depends_on:
      postgres:
        condition: service_healthy

正如你所见,对于 Postgres 容器,我们将使用一个名为 frantiseks/postgres-sakila 的镜像,该镜像在 Docker Hub 上公开可用。此镜像将填充 Postgres 实例上的 Sakila 数据库。该数据库模型化了一个 DVD 租赁店,由多个表组成,这些表经过规范化并对应于电影、演员、客户和付款等实体。在接下来的几个部分中,我们将利用这些数据来构建一些示例 dbt 数据模型。

第二个服务,称为 dbt,将创建一个我们将构建数据模型的环境。请注意,我们将当前目录挂载到 Docker 容器中。这将使容器能够访问我们对数据模型所做的任何更改,而无需重新构建镜像。此外,任何由 dbt 命令生成的元数据(如 manifet.json)将在主机机器上立即出现。

步骤 3:创建一个 Dockerfile

现在,让我们指定一个将用于构建镜像的Dockerfile,在这个镜像上运行的容器将构建我们示例 dbt 项目中指定的模型。

FROM python:3.10-slim-buster

RUN apt-get update \
    && apt-get install -y --no-install-recommends

WORKDIR /usr/src/dbt/dbt_project

# Install the dbt Postgres adapter. This step will also install dbt-core
RUN pip install --upgrade pip
RUN pip install dbt-postgres==1.3.1

# Install dbt dependencies (as specified in packages.yml file)
# Build seeds, models and snapshots (and run tests wherever applicable)
CMD dbt deps && dbt build --profiles-dir profiles && sleep infinity

注意,在最后一个 CMD 命令中,我们故意添加了一个额外的 && sleep infinity 命令,以使容器在运行完 Dockerfile 中指定的步骤后不会退出,这样我们就可以访问容器并运行额外的 dbt 命令(如果需要)。

步骤 4:为 Postgres 数据库创建一个 dbt 配置文件

既然我们已经为主机机器创建了所需的基础设施,以创建一个 Postgres 数据库、填充一些虚拟数据以及创建一个用于 dbt 环境的镜像,让我们专注于 dbt 部分。

我们首先需要创建一个 dbt 配置文件,用于与目标 Postgres 数据库交互。在 test_dbt_project 目录内,创建另一个名为 profiles 的目录,然后创建一个名为 profiles.yml 的文件,内容如下:

test_profile:
  target: dev
  outputs:
    dev:
      type: postgres
      host: postgres
      user: postgres
      password: postgres
      port: 5432
      dbname: postgres
      schema: public
      threads: 1

步骤 5:定义一些数据模型

下一步是根据 Postgres 容器填充的 Sakila 数据创建一些数据模型。如果你计划将这个项目用于测试目的,我建议你至少创建一个种子数据,一个模型和一个快照(如果可能的话带有测试),以便全面覆盖所有 dbt 实体(不包括宏)。

我已经创建了一些数据模型、种子数据和快照,你可以在这个仓库中访问它们。

[## dbt-dummy/dbt_project at main · gmyrianthous/dbt-dummy

你现在无法执行该操作。你在另一个标签页或窗口中登录了。你在另一个标签页中注销了...

github.com](https://github.com/gmyrianthous/dbt-dummy/tree/main/dbt_project?source=post_page-----e12c31bd3992--------------------------------)

第 6 步:运行 Docker 容器

现在我们已经拥有了启动 docker-compose.yml 文件中指定的两个 Docker 容器,并构建我们示例 dbt 项目中定义的数据模型所需的一切。

首先,构建镜像:

docker-compose build

现在让我们启动运行的容器:

docker-compose up

这个命令应该已经使用 Sakila 数据库初始化了一个 Postgres 数据库,并创建了指定的 dbt 模型。现在,让我们确保你有两个正在运行的容器:

docker ps

应该输出包括一个名为 dbt 的容器和另一个名为 postgres 的容器。

第 7 步:查询 Postgres 数据库上的模型

要访问 Postgres 容器,首先需要推断容器 id。

docker ps

然后运行:

docker exec -it <container-id> /bin/bash

然后我们需要使用 psql,这是一个命令行接口,可以访问 Postgres 实例:

psql -U postgres

如果你已经使用了我在前面部分分享的数据模型,现在可以使用以下查询来查询在 Postgres 上创建的每个模型。

-- Query seed tables
SELECT * FROM customer_base;

-- Query staging views
SELECT * FROM stg_payment;

-- Query intermediate views
SELECT * FROM int_customers_per_store;
SELECT * FROM int_revenue_by_date;

-- Query mart tables
SELECT * FROM cumulative_revenue;

-- Query snapshot tables
SELECT * FROM int_stock_balances_daily_grouped_by_day_snapshot;

第 8 步:创建额外的或修改现有的模型

如前所述,Dockerfiledocker-compose.yml 文件被编写成 dbt 容器仍然会保持运行。因此,无论何时你修改或创建数据模型,你仍然可以使用该容器重新构建种子数据、模型、快照和/或测试。

要做到这一点,首先推断 dbt 容器的 id:

docker ps

然后通过运行以下命令进入正在运行的容器:

docker exec -it <container-id> /bin/bash

最后运行你希望的任何 dbt 命令,这取决于你对示例 dbt 项目所做的修改。以下是这些用途最常用命令的快速参考:

# Install dbt deps
dbt deps

# Build seeds
dbt seeds --profiles-dir profiles

# Build data models
dbt run --profiles-dir profiles

# Build snapshots
dbt snapshot --profiles-dir profiles

# Run tests
dbt test --profiles-dir profiles

如何获取本教程的完整代码。

我创建了一个名为 dbt-dummy 的 GitHub 仓库,其中包含了你需要的所有部分,以便快速创建一个使用 Postgres 的容器化 dbt 项目。你可以通过以下链接访问它。

[## GitHub - gmyrianthous/dbt-dummy: 这是一个虚拟的 dbt(数据构建工具)项目,你可以用于…

这是一个虚拟的 dbt(数据构建工具)项目,你可以用它来填充 dbt seeds、模型、快照和测试……

github.com

这个项目也可以在官方 dbt 文档的示例项目部分找到!

最终思考

在今天的教程中,我们逐步讲解了如何在本地机器上使用 Docker 创建 dbt 项目。我们构建了两个镜像,一个用于 Postgres 数据库,同时填充了 Sakila 数据库,另一个用于我们的 dbt 环境。

能够快速构建一些示例项目是很重要的,这些项目可以作为测试环境,甚至是实验和学习的游乐场。

订阅 Data Pipeline,这是一个专注于数据工程的新闻通讯

相关的文章你可能也会喜欢

towardsdatascience.com ## 如何构建你的 dbt 项目和数据模型

对 dbt 数据模型实施有意义的结构

[towardsdatascience.com ## dbt 中的 Staging vs Intermediate vs Mart 模型

了解在数据构建工具(dbt)背景下,staging、intermediate 和 mart 模型的目的

[towardsdatascience.com ## 如何安装 dbt(数据构建工具)

为你的特定数据仓库安装数据构建工具

[towardsdatascience.com

使用 PySpark UDFs 在合成表中创建多对一关系

原文:towardsdatascience.com/create-many-to-one-relationships-between-columns-in-a-synthetic-table-with-pyspark-udfs-41e39d97c936?source=collection_archive---------3-----------------------#2023-12-09

利用一些简单的方程在测试表中生成相关列。

Matt CollinsTowards Data Science Matt Collins

·

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

--

使用 DALL-E 3 生成的图像

我最近一直在使用 Databricks Labs 数据生成器从头开始创建完全合成的数据集。在此过程中,我研究了围绕不同商店、员工和客户构建销售数据。因此,我想在我人为填充的列之间创建关系,例如将员工和客户映射到某个商店。

通过使用 PySpark UDF 和一些逻辑,我们可以生成遵循多对一关系的相关列。通过一点魔法,我们甚至可以扩展逻辑,为这种映射提供一些变化——比如客户通常在本地店铺购物,但有时会在其他店铺购物。

使用 Databricks Labs 数据生成器生成我们的基本 DataFrame

注意:如果不需要,可以跳过此部分!

首先,我们需要创建一个包含我们第一个随机生成列的 DataFrame。在我们的情况下,我们将从店铺开始,因为从逻辑上讲,我们将有“每家店铺多个员工”和“许多客户重复在店铺购物”。

以星型模式数据模型为基础,我们将从销售事实表开始——这是一个事务表,包含 Sale Id、Store Id、Employee Id 和 Customer Id 的关键值,销售金额以及一些购买的日期时间数据。然后我们可以在后续的维度表中填写有关 Store、Employee 和 Customer 的详细信息。

我们从小开始——一个包含 1000 笔销售的表格就可以了。现在我们需要决定如何将这些销售分配到店铺、员工和客户之间。我们建议如下:

  • 店铺数 = 20

  • 员工数 = 100

  • 客户数 = 700

我们还可以说,销售将记录在上个月的时间段内:

  • 首次销售日期 = 2023–11–01

  • 最后销售日期 = 2023–11–30

Sale Id 需要是唯一的列,以便我们可以为此生成一个 Id 列。现在我们需要将 1000 笔销售分配到 20 家店铺中。为了简单起见,我们假设这是随机的。

使用 Databricks Lab 生成器,我们可以用以下代码完成:

现在添加一些代码来记录销售的时间和金额。为了保持简单,我们将销售的时间戳四舍五入到最接近的小时。

为了计算销售金额,我们可以在 withColumn 表达式中使用“expr”参数,以允许我们生成一个随机数,并设定一些规则/边界。

在这种情况下,表达式非常简单:生成一个随机数(在 0 和 1 之间),加上 0.1(确保销售值不为 0),然后乘以 350。

我们现在有了 DataFrame 的基本结构,所以把它们整合在一起:

我们可以创建一个快速的数据概况来查看列中的值分布:

作者提供的图片:在 Databricks 中生成的数据概况

我们可以看到 StoreId 的分布在 20 家店铺中相对均匀,没有缺失值,且平均值围绕中心,符合预期。时间戳和金额值也是如此。

添加一个依赖的多对一列

现在我们可以将 Employee Id 列添加到 DataFrame 中。我们现在完成了 Databricks Lab 数据生成器,所以将只使用 PySpark 操作来向 DataFrame 中添加列。

从代码中退一步,我们希望将其建模为以下语句:

  • 有 20 家店铺。

  • 每家店铺都有超过 1 名员工。

  • 每个员工仅在一个商店工作。

首先,我们需要在商店之间分配员工。可以使用以下 Python 函数来完成:

现在我们已经为每个商店分配了员工分布,让我们开始分配 ID 吧!

employeesPerStore 列表确保每个商店的员工 ID 不会重叠。我们可以使用这个列表用以下公式随机分配员工 ID 到表中的销售记录中:

这个函数目前只适用于单个值——我们需要将它放入 PySpark DataFrame 可以使用的形式(功能上,并且要快速!)

我们可以将 PySpark UDF 传递给 withColumn 方法,因此让我们将此逻辑重格式化为一个函数,并将其设置为 UDF:

现在在 DataFrame 中调用它作为一个新列:

我们可以通过使用 Databricks 中的可视化工具来快速测试这是否正确,查看每个商店 ID 的员工 ID 不同计数。这是我的首选方法,但你也可以使用分组逻辑或其他绘图模块,如果需要的话。

作者图片:每个商店的员工 ID 不同计数

重要提示: 这个逻辑可能会导致员工在结果中遗漏。这意味着可能会有员工没有销售记录,从而没有被包含在 DataFrame 中。我们将在下一节中查看如何确保所有客户都有销售记录。

添加客户列

客户列稍有不同……虽然我们的使用案例表明客户在一个商店多次购物是常见的,但他们有一天去不同的商店也是完全可能的。我们如何建模这一点?

我们已经完成了员工列的起始工作,因此可以重复使用 get_employees 函数和 UDF 逻辑来处理客户,如下所示:

我们再次可能遗漏了一些客户。以下是一些纠正方法:

  • while 循环中重新计算,直到收敛到一个包含所有客户的 DataFrame(效率低,成本高,可能会无限期运行)

  • while 循环中随机更新客户 ID 直到 DataFrame 中所有客户都有(需要逻辑只覆盖相同商店,可能也会无限期运行)

  • 返回销售表中记录超过 1 条的所有客户 ID 列表,并随机覆盖直到所有缺失的 ID 都被添加(还需要逻辑覆盖相同商店的客户,可能还需要 while 循环逻辑)

  • 反转过程,从员工开始。这确保每个员工被随机分配到行中。然后我们可以使用映射并应用商店 ID。

希望你能明白最后一个选项为什么是计算工作量最小的——我们拥有所需的所有代码,只需要稍微重新格式化一下。

我们的新脚本如下:

作者图片:新 DataFrame 的 Databricks 数据概况

向客户添加随机性

我们现在需要的是一些随机性,我们需要定义它。以我们的例子为例,假设每个客户有 90%的机会在通常的商店(“本地”商店)购物。如果我们不需要将所有客户返回在结果集中,我们可以简单地调整我们的customers_udf,并使用df2

该逻辑涉及使用random.choices函数来提供加权列表并返回单一值。

要计算加权列表,我们需要将客户的“本地”商店的权重设置为 90%,因此需要将剩余的 10%分配给其他商店,这里有 19 家商店。因此,每个其他商店被选择的概率将是 10/19 = 0.526%。我们可以用这些百分比填充一个数组,数组看起来类似于:[0.526,0.526,0.526,…,90,0.526,…0.526]

将其传递给random.choices,我们就可以根据对应的权重从列表中随机选择一个商店 ID,并将其用作customer_id变量的输入,如前所述。

注意: random.choices的输出返回一个列表(因为你可以请求 k 个结果),所以访问列表的第 0 个元素以获取 store_id 作为整数值。

如果我们需要将此逻辑与包含所有客户的 DataFrame 结合使用,我们可以稍微逆转该过程。权重逻辑仍然有效,所以我们可以将其插入以随机选择商店并将其作为结果返回:

作者提供的图像:Databricks 中的最终 DataFrame 示例

结论

就这样!一个合成创建的 DataFrame,其中列之间有严格和松散的映射。你现在可以进行下一步,填充可能包含更多描述性信息的相关表,例如商店名称、地址、员工姓名、角色等的维度表。这也可以使用 Databricks Labs 数据生成器或任何其他你熟悉的工具/过程来完成。

在 Databricks Labs 数据生成器的GitHub Repo上有一些很棒的示例和文档,如果你想了解更多内容,请查看一下。

我所有的代码可以从以下GitHub Repo访问。

如果你对这个演示有任何想法、评论或替代方案,请在评论中联系我。谢谢!

使用 AWS Cloudformation 创建 MySQL 和 Postgres 实例

原文:towardsdatascience.com/create-mysql-and-postgres-instances-using-aws-cloudformation-d3af3c46c22a

面向数据库从业者的基础设施即代码

💡Mike ShakhomirovTowards Data Science 💡Mike Shakhomirov

·发布于 Towards Data Science ·阅读时间 7 分钟·2023 年 3 月 20 日

--

照片由 Yunqing Leo 提供,来源于 Unsplash

使用这两个简单的Cloudformation 模板,我们将学习如何通过一个命令行命令部署 Postgres 和 MySQL Aurora 数据库实例。我已优化模板文件,将属性数量减少到最低限度,以便更容易理解。

我知道基础设施即代码对于不熟悉这个概念的中级用户来说可能有点困难,但相信我,这是正确的方向。从职业发展的角度来看,这将带来很多好处。

这个教程适合谁?

这个故事是为了那些想进入数据工程并学习一些高级内容,如基础设施即代码的初学者和中级数据及软件工程师。

我希望这个教程对所有需要云数据库来存储应用数据的软件工程师有所帮助。

前提条件

  • Bash 脚本的基本知识

    Bash/Shell 脚本对于初学者来说可能有点复杂,但它在部署 AWS 服务(即 RDS)时是必需的。

  • 安装了 AWS 命令行工具(AWS CLI)。

    本文中提供的所有 shell 命令可以通过命令行或作为 .sh 脚本运行,即从命令行运行一个文件,如 $ ./deploy.sh

  • AWS 账户。

    注册是免费的,并且有免费层,所以除非你部署了一个非常大的数据库并且忘记删除,否则不会有重大费用。

为什么选择基础设施即代码(IAC)?

IAC(基础设施即代码)因其帮助以模板文件(JSON 或 yaml 代码)的一致且可重复的方式管理云平台资源而迅速获得流行。我们可以应用 CI/CD 管道,并利用 Github 集成来创建生产和预发布环境。例如。基础设施即代码现在是部署和资源管理的标准,我之前在这里写过相关内容:

[## 初学者的基础设施即代码

使用这些模板像专业人士一样部署数据管道

levelup.gitconnected.com](https://levelup.gitconnected.com/infrastructure-as-code-for-beginners-a4e36c805316?source=post_page-----d3af3c46c22a--------------------------------)

那个故事解释了如何为我们的数据湖(即 AWS S3 和 AWS Lambda)部署和配置 AWS 资源。

从数据角度来看,基础设施即代码是一种出色的方式来分离数据环境并提供数据管道资源。利用它我们可以创建任何需要的数据管道。

我们在数据管道中为何以及何时使用关系型数据库?

通常,关系型数据库由行式表组成,表中的列使用规范化的模式连接相关的数据片段。

这种数据架构类型旨在快速捕获数据,并优化系统以快速检索应用程序所需的最新数据。

由于 RDS 数据库表和连接是“规范化的”,它们比典型的数据仓库设计更复杂。然而,这是功能需求所要求的权衡。

因此,“传统数据库”和“数据仓库”之间的主要区别在于前者是为了“记录”数据而创建和优化的,而后者则是为了“响应分析”而创建和构建的。我之前在这里写过相关内容:

## 数据平台架构类型

它如何满足你的业务需求?选择的困境。

towardsdatascience.com

流行的关系型数据库有 MySQL、Microsoft SQL Server、Oracle 和 PostgreSQL。

创建 MySQL Aurora 实例

要创建一个 Aurora 引擎的数据库实例,我们首先需要一个集群。问题在于 Aurora 实例必须通过AWS::RDS::DBClusterDBClusterIdentifier相关联。如果在我们的栈文件中没有集群,我们将会遇到一些没有多大意义的通用 Cloudformation 错误。

你可以使用这个 Cloudformation 模板文件:

如果一切正确,那么在 AWS 控制台中你将看到如下内容:

Cloudformation 事件。作者提供的图片

在 AWS 中创建 Postgres 数据库实例

要创建一个简单的 Postgres 实例,我们可以使用这个命令和下面的模板:

模板将是这样的:

如你所见,Postgres 要容易一些,因为它不需要集群。

Postgres 实例已创建。图片由作者提供。

如果出现问题怎么办?

我们可以随时查看events部分。然后 Google 搜索。我知道,我知道…… AWS Cloudformation 文档并不完美。这就是生活,我们必须遵守。不幸的是。

例如,如果出现问题并且遇到像这样的错误:

“DS 不支持使用以下组合创建 DB 实例:DBInstanceClass=db.t2.micro, Engine=postgres, EngineVersion=13.3…”

你需要找到你所在区域支持的引擎和版本 13.3。为此,你可以使用这个 bash 命令:

aws rds describe-orderable-db-instance-options --engine postgres --engine-version 13.3     --query "*[].{DBInstanceClass:DBInstanceClass,StorageType:StorageType}|[?StorageType=='gp2']|[].{DBInstanceClass:DBInstanceClass}"  --output texts

部署 RDS 实例的最佳实践

我们上面使用的模板非常简单。这是故意的,以便我们可以轻松地成功部署它们,并从那里开始添加新功能。

我们还能做什么?

将密码保存在 Secrets Manager 中

这是一个非常好的主意,因此我们可能需要将模板中的密码属性更改为这个:

 RootUser:
    Type: 'AWS::SecretsManager::Secret'
    Properties:
      Name: '/rds/EnvType/RootUser-secret'
      Description: 'Aurora database master user'
      SecretString: !Ref DBUser

然后我们可以在模板中这样使用它:

MasterUsername: !Join ['', ['{{resolve:secretsmanager:', !Ref RootUser, '}}']]

我们可以对数据库密码做同样的事情:

RootPassword:
    Type: 'AWS::SecretsManager::Secret'
    Properties:
      Name: '/rds/EnvType/RootPassword-secret'
      Description: 'Aurora database master password'
      GenerateSecretString:
        PasswordLength: 16
        ExcludePunctuation: true

然后从模板中引用它:

RootUserPassword: !Join ['', ['{{resolve:secretsmanager:', !Ref RootPassword, '}}']]

什么是 RDSDBClusterParameterGroup?

它有一个类型:‘AWS::RDS::DBClusterParameterGroup’

在这个属性中,我们指定将连接到数据库集群的参数组的名称。通过这样做,我们将我们的数据库实例与 Aurora 数据库集群关联起来。

然后我们在AWS::RDS::DBCluster资源中引用它,即:

Resources:
  RDSCluster: 
    Properties: 
      DBClusterParameterGroupName: 
        Ref: RDSDBClusterParameterGroup

这很重要,因为每次将来需要更新参数时,必须构建一个新的参数组并将其链接到集群。

要实施修改,必须重启集群中的主数据库实例。

删除保护

总是一个好主意为我们的数据库集群设置 DeletionProtection: true

这可以防止 RDS 实例的意外删除。它还可以防止我们更改某些模板文件属性后需要替换资源的情况。

DBInstanceClass

这是一个重要的属性,因为它直接影响到我们数据库相关的费用。它在模板中属于类型:‘AWS::RDS::DBInstance’。

始终选择最小的实例进行测试。在模板中搜索db.r6g.large并更改为db.t2.smalldb.t3.micro

DBInstanceClass: db.t2.small

另外,如果我们在 staging 中不需要大存储,最好只配置所需的最小值:

AllocatedStorage: '20'
DBInstanceClass: db.t2.small 

在单独的堆栈模板中配置 VPC 资源

我们可能希望在虚拟私有云 (VPC) 中部署我们的数据库集群。创建一个单独的堆栈来处理这些类型的资源,然后在我们的数据库堆栈中使用它是一个好主意,如下所示:

Resources:
  RDSCluster: 
    Properties: 
      DBClusterParameterGroupName: 
        Ref: RDSDBClusterParameterGroup
      # DBSubnetGroupName: 
      #   Ref: DBSubnetGroup
      Engine: aurora
      MasterUserPassword: 
        Ref: DBPassword
      MasterUsername: 
        Ref: DBUser
    Type: "AWS::RDS::DBCluster"

DBInstanceIdentifier

数据库实例由这个名称标识。

如果你没有提供名称,Amazon CloudFormation 将创建一个特殊的默认物理 ID 并将其用作名称。

AWS::RDS::DBParameterGroup

我们应该出于与创建 'AWS::RDS::DBClusterParameterGroup' 相同的原因来创建这种资源类型。

对于生产数据库来说,稍后将新的数据库参数组关联起来可能会存在问题,因为这样做需要重新启动数据库。

我们也可能需要为写入和副本实例创建不同的 AWS::RDS::DBParameterGroup,即:

 WriterRDSDBParameterGroup:
    Type: 'AWS::RDS::DBParameterGroup'
    Properties:
      Description: Custom db parameter group for writer
      Family: aurora5.6
      Parameters:
        sql_mode: STRICT_ALL_TABLES
        transaction_isolation: READ-COMMITTED
  RDSDBParameterGroup:
    Type: 'AWS::RDS::DBParameterGroup'
    Properties:
      Description: CloudFormation Sample Aurora Parameter Group
      Family: aurora5.6
      Parameters:
        sql_mode: IGNORE_SPACE
        max_allowed_packet: 1024
        innodb_buffer_pool_size: '{DBInstanceClassMemory*3/4}'

数据库监控

强烈建议在初始设置期间设置数据库监控。许多指标,无论是在集群还是数据库层级,默认情况下都会由Aurora 发送到 CloudWatch。因此,我们可以通过使用 AWS::RDS::EventSubscription 资源来订阅 RDS 事件。

ClusterEventSubscription:
    Type: 'AWS::RDS::EventSubscription'
    Properties:
      EventCategories:
        - configuration change
        - creation
        - failure
        - deletion
        - failover
        - maintenance
        - notification
      SnsTopicArn: arn:aws:sns:us-east-1:09876543:example-topic 
      SourceIds:
        - !Ref myAuroraDBCluster
      SourceType: db-cluster
      Enabled: true

之后,我们可以创建 CloudWatch 指标过滤器,并像这样设置 CloudWatch 警报:

DBCPUUtilizationHighAlarm:
    Type: 'AWS::CloudWatch::Alarm'
    Properties:
      AlarmDescription: 'CPU utilization over last 5 minutes higher than 95%'
      Namespace: 'AWS/RDS'
      MetricName: CPUUtilization
      Statistic: Average  
      Period: 60
      EvaluationPeriods: 5
      ComparisonOperator: GreaterThanThreshold
      Threshold: 95
      TreatMissingData: breaching
      AlarmActions: 
       - arn:aws:sns:us-east-1:9876543:example-topic
      OKActions:
       - arn:aws:sns:us-east-1:9876543:example-topic
      Dimensions:
      - Name: DBInstanceIdentifier
        Value: !Ref myDatabaseInstance

EnablePerformanceInsights

这是一个有助于了解数据库性能的工具。

如果仅启用保留 7 天的数据,则不会产生额外费用。

EnablePerformanceInsights: true

这个功能必须在主实例和副本实例上分别启用。

结论

在这个故事中,你可以找到两个简化的 CloudFormation 模板,用于在云中部署 Postgres 和 MySQL 数据库实例。我希望你会发现本文中涵盖的最佳实践有用,并且它将帮助你将这些模板发展成符合你需求的更大内容。

RDS 数据库对于任何数据平台设计至关重要,理解它们是如何创建的也很重要。它们在许多数据管道设计模式中被使用,即批处理和变更数据捕获:

## 数据管道设计模式

选择合适的架构及其示例

towardsdatascience.com

应用一些在最佳实践中提到的功能也可能有助于获得数据库性能洞察。

推荐阅读:

## 数据管道设计模式

选择合适的架构及其示例

towardsdatascience.com [## AWS::SecretsManager::Secret

创建一个新的密钥。密钥可以是一个密码、一组凭证(如用户名和密码)、OAuth…

工作与参数组

数据库参数指定了数据库的配置方式。例如,数据库参数可以指定…

AWS::RDS::EventSubscription

AWS::RDS::EventSubscription 资源允许您接收 Amazon 关系数据库服务的通知…

Amazon RDS 性能洞察 | 云关系数据库 | 亚马逊网络服务

分析和调整 Amazon RDS 数据库性能。Amazon RDS 性能洞察是数据库性能调优和…

AWS 性能洞察

使用 Python 创建惊艳的分形艺术:初学者与数学爱好者的教程

原文:towardsdatascience.com/create-stunning-fractal-art-with-python-a-tutorial-for-beginners-c83817fcb64b

只需一行代码或更少

Bex T.Towards Data Science Bex T.

·发表于Towards Data Science ·12 分钟阅读·2023 年 3 月 6 日

--

介绍

“我从未见过如此美丽的事物”这句话只应用于分形。确实,有《蒙娜丽莎》、《星夜》和《维纳斯的诞生》(顺便说一下,这些都被 AI 生成艺术破坏了),但我认为没有任何艺术家或人类能够创造出像分形那样极为惊艳的作品。

左边是标志性的分形,曼德布罗特集合,它在 1979 年被发现,当时没有 Python 或图形软件可用。

GIF 由作者使用Fraqtive生成,这是一个开源应用程序。GPL-3 许可证

曼德布罗特集合是一组复数,当在复平面上绘制时,形成我们看到的自我重复形状。集合中的每个数字也可以是朱利亚集合的种子,你可以看到美丽的图案在我在曼德布罗特集合边界内移动鼠标时显现出来。

但在我们能够绘制曼德布罗特集合或朱利亚集合之前(不过,相信我,我们会的),我们还有很多内容要学习。如果你只是来看看那些炫酷的图片,我强烈推荐下载开源 Fraqtive 软件(尽情发挥吧!),我用它生成了上面的 GIF 以及下面的那个:

GIF 由作者使用Fraqtive生成,这是一个开源应用程序。GPL-3 许可证

如果你只想用一行代码在 Python 中绘制曼德布罗特集合,下面就是代码(不,这个副标题不是诱饵):

from PIL import Image

Image.effect_mandelbrot((512, 512), (-3, -2.5, 2, 2.5), 100).show()

作者提供的图片

但是,如果你想深入了解美丽的分形,学习如何绘制它们,并且最重要的是,如何适当地给它们上色,那么请继续阅读!

在这篇文章中,我们将学习如何使用 Matplotlib 和 NumPy 绘制基本的(但仍然非常酷的)曼德尔布罗特集合。然后,我们将在未来的文章中使用 Pillow 将其提升到一个全新的水平。

让我们开始吧。

Python 中的复数

Python 程序员并不每天处理复数。由于在本教程中我们将频繁使用复数,因此本节将作为一个入门介绍。

你可以通过将字面量j附加到整数或浮点数来创建复数的虚部。

num1 = 2 + 1j
num2 = 12.3 + 23.1j

type(num1)
complex

如果看到虚数用j而不是i表示让你感到困惑(你好,数学家们),你可以使用内置的complex函数:

2 + 3j == complex(2, 3)
True

一旦创建,你可以通过realimag属性访问复数的实部和虚部:

num1.real
2.0
num2.imag
23.1

这篇文章中另一个重要的复数特性是它们的绝对值。复数的绝对值或模长衡量的是它在复平面上与原点(0, 0)之间的距离。它被定义为实部和虚部的平方和的平方根(感谢毕达哥拉斯)。

abs(1 + 3.14j)
3.295390720385065

这些就足够让我们创造一些很棒的东西了。让我们开始吧!

简单公式,宏伟集合

我们的旅程开始于确定某个复数c是否属于曼德尔布罗特集合,这实际上非常简单。我们只需将其代入下面的公式,并生成一个z值的序列:

图片由作者提供

第一个z始终为 0,如上所定义。后续元素通过对前一个z进行平方运算并将c加到结果中来找到。

让我们在 Python 中实现这个过程。我们将定义一个sequence函数,该函数返回给定c的前n个元素:

def sequence(c, n=7) -> list:
    z_list = list()

    z = 0
    for _ in range(n):
        z = z ** 2 + c
        z_list.append(z)

    return z_list

现在,我们将对一堆数字进行函数测试:

import pandas as pd

df = pd.DataFrame()
df['element'] = [f"z_{i}" for i in range(7)]

# Random numbers
cs = [0, 1, -1, 2, 0.25, -.1]

for c in cs:
    df[f"c={c}"] = sequence(c)

df

图片由作者提供

我们看到三种结果:当c为 1 或 2 时,序列是无界的(发散到无限大)。当c为-1 时,序列在 0 和-1 之间来回变化。至于 0.25 和-0.1,它们保持较小或有界。

那么,这五个中哪些是幸运的成为曼德尔布罗特集合的成员呢?

你稳定吗?

我们的筛选过程非常简单——如果c将序列发散到无限大,它就不在曼德尔布罗特集合中。在分形术语中,这个c被称为不稳定的。或者,我们换个角度看——如果给定的复数c的相应Z序列保持有界,那么它就是稳定的。

现在,我们必须弄清楚在将c分类为稳定或不稳定之前要查看多少个z成员。这个迭代计数并不容易找到,因为公式对c的最小变化都很敏感。

但幸运的是,人们已经研究了这个集合足够长的时间,知道所有的曼德 elbrot 都保持在半径为 2 的范围内。这意味着我们可以进行几十次迭代,剩下的相对较小或低于 2 的数字可能在曼德 elbrot 集合中。

所以,让我们使用这个逻辑创建一个新的函数is_stable,当数字是曼德 elbrot 时返回 True:

def is_stable(c, n_iterations=20):
    z = 0

    for _ in range(n_iterations):
        z = z ** 2 + c

        if abs(z) > 2:
            return False
    return True

在这个布尔函数的主体中,我们将z设置为 0,并通过一个由n_iterations控制的循环运行算法。在每次迭代中,我们检查z的大小,以便在它早期超过 2 时终止循环,避免浪费时间运行其余的迭代。

最后的return语句仅在所有迭代后z低于 2 时执行。让我们检查几个数字:

is_stable(1)
False
is_stable(0.2)
True
is_stable(0.26)
True
is_stable(0.26, n_iterations=30)
False

注意增加n_iterations到 30 时如何改变 0.26 的稳定性。一般来说,接近分形边缘的值需要更多的迭代以进行更准确的分类,并创建更详细的视觉效果。

## Join Medium with my referral link - Bex T.

获取对我所有⚡高级⚡内容和 Medium 上所有内容的独家访问权限。通过购买我的…

ibexorigin.medium.com

如何在 Matplotlib 中绘制曼德 elbrot 集合

本节深受这篇精彩 RealPython 文章的启发:

## Draw the Mandelbrot Set in Python - Real Python

在尝试绘制分形之前,了解对应的曼德 elbrot 集表示什么以及如何…

realpython.com

我们文章的终极目标是用 Matplotlib 生成这个家伙(剧透警告,我们会创建更好的东西!):

图片由作者提供。

这幅图像是通过将所有曼德 elbrot 部分涂成黑色,将不稳定的元素涂成白色创建的。在 Matplotlib 中,灰度有 256 种阴影或范围,从 0 到 255,其中 0 是完全白色,255 是纯黑色。但你可以将这个范围归一化为 0 和 1,使 0 为白色,1 为黑色。

这种归一化对我们很有帮助。我们可以创建一个二维复数数组,并对每个元素运行我们的is_stable函数。结果数组将对曼德 elbrot 赋值 1,对不稳定的部分赋值 0。当我们将这个数组绘制成图像时——瞧,我们得到了理想的黑白视觉效果。

让我们开始吧。首先,我们创建一个生成候选值矩阵的函数,我们可以在其上进行迭代:

import numpy as np

def candidate_values(xmin, xmax, ymin, ymax, pixel_density):
    # Generate a 2D grid of real and imaginary values
    real = np.linspace(xmin, xmax, num=int((xmax-xmin) * pixel_density))
    imag = np.linspace(ymin, ymax, num=int((ymax-ymin) * pixel_density))

    # Cross each row of `xx` with each column of `yy` to create a grid of values
    xx, yy = np.meshgrid(real, imag)

    # Combine the real and imaginary parts into complex numbers
    matrix = xx + 1j * yy

    return matrix

我们将使用 np.linspace 函数在一个范围内创建均匀间隔的数字。pixel_density 参数动态设置每单位的像素数量。

例如,一个水平范围为 (-2, 0)、垂直范围为 (-1.2, 1.2) 且 pixel_density 为 1 的矩阵,其形状将是 (2, 2)。这意味着我们得到的曼德博图像将宽 2 像素、高 2 像素,这会让贝诺伊特·曼德博在他的坟墓里翻身。

c = candidate_values(-2, 0, -1.2, 1.2, 1)

c.shape
(2, 2)

所以,我们最好使用更高的密度,比如 25:

c = candidate_values(-2, 0, -1.2, 1.2, 25)

c.shape
(60, 50)

现在,为了对每个 c 元素运行我们的 is_stable 函数,我们用 np.vectorize 对其进行矢量化,并用 20 次迭代进行调用:

c = candidate_values(-2, 0.7, -1.2, 1.2, pixel_density=25)

mandelbrot_mask = np.vectorize(is_stable)(c, n_iterations=20)
mandelbrot_mask.shape
(60, 67)

我们将结果数组称为 mandelbrot_mask,因为它对每个曼德博集合返回 True (1)。为了绘制这个数组,我们使用 Matplotlib 的 imshow 函数,并使用 binary 颜色映射。这将使图像呈现黑白效果。

import matplotlib.pyplot as plt

plt.imshow(mandelbrot_mask, cmap="binary")

# Turn off the axes and use tight layout
plt.axis("off")
plt.tight_layout()

作者提供的图片

好吧,那是一张很丑的曼德博图像。我们要不要将像素密度提高到 1024,将迭代次数增加到 30?

c = candidate_values(-2, 0.7, -1.2, 1.2, pixel_density=1024)

mandelbrot_mask = np.vectorize(is_stable)(c, n_iterations=30)

plt.imshow(mandelbrot_mask, cmap="binary")
plt.gca().set_aspect("equal")
plt.axis("off")
plt.tight_layout()

作者提供的图片

现在,这看起来更像样了!祝贺你绘制了你的第一个曼德博图像!

等等,那还不是艺术!

尽管我们当前的分形看起来很酷,但离我承诺的艺术还有很大差距。

所以,让我们通过不仅关注黑色集合数字,还关注边缘周围的数字来进行改造。因为看着这张图像,我们可以看到在边界周围出现了各种有趣的图案:

作者提供的图片

让我们通过将代码组织成一个类来开始改造,因为当前的代码实在是一团乱麻。

类的名称将是 Mandelbrot,我们将使用数据类,这样我们就不必像古人那样创建 __init__ 构造函数:

from dataclasses import dataclass

@dataclass
class Mandelbrot: # Inspired by the Real Python article shared above
    n_iterations: int

    def is_stable(self, c: complex) -> bool:
        z = 0

        for _ in range(self.n_iterations):
            z = z ** 2 + c
            if abs(z) > 2:
                return False

        return True

这个类只需要初始化 max_iteration 参数。我们还将 is_stable 函数添加为类方法。

mandelbrot = Mandelbrot(n_iterations=30)

mandelbrot.is_stable(0.1)
True
mandelbrot.is_stable(1 + 4.4j)
False

到目前为止,我们只对曼德博集合的部分进行了黑色着色,其余部分为白色。但是如果我们想给集合的边缘增添些许风采,我们必须想出一种逻辑来将不稳定的元素涂色为白色以外的颜色。

我们可以通过确定复数在多少次迭代中变得不稳定来实现这一点。有些会非常快地变得不稳定(也许它们有短暂的引信?),但其他的可能需要几百或几千次迭代(它们比较耐心)。不过,一般来说,接近分形边缘的数字比远离边缘的数字不稳定性更小(需要更多迭代)。

利用这些信息,我们可以根据每次终止的迭代次数为每个像素(复数)赋予不同的颜色深度。这被称为逃逸计数算法。让我们在我们的类中实现它:

@dataclass
class Mandelbrot:
    max_iterations: int

    def escape_count(self, c: complex) -> int:
        z = 0
        for iteration in range(self.max_iterations):
            z = z ** 2 + c
            if abs(z) > 2:
                return iteration
        return self.max_iterations

首先,我们将 n_iterations 改为 max_iterations,因为这样更合适。然后,我们创建一个 escape_count 方法:

  • 如果 c 不稳定,则返回它超出 2 的幅度的迭代次数

  • 如果 c 是稳定的,则返回最大迭代次数

mandelbrot = Mandelbrot(max_iterations=50)

mandelbrot.escape_count(-0.1) # stable
50
mandelbrot.escape_count(0.26) # unstable
29

现在,我们创建另一种方法来根据迭代次数测量稳定性:

@dataclass
class Mandelbrot:
    max_iterations: int

    def escape_count(self, c: complex) -> int:
        z = 0
        for i in range(self.max_iterations):
            z = z ** 2 + c
            if abs(z) > 2:
                return i
        return self.max_iterations

    def stability(self, c: complex) -> float:
        return self.escape_count(c) / self.max_iterations

stability 方法返回一个介于 0 和 1 之间的度量值,我们可以用它来确定颜色深度。只有 Mandelbrots 会返回 max_iterations,所以它们将被标记为 1。接近边缘的数字会需要更长的时间才会变得不稳定,因此它们的值会越来越接近 1。

根据这个逻辑,我们可以恢复我们的 is_stable 函数,但使其更简短:

@dataclass
class Mandelbrot:
    max_iterations: int

    def escape_count(self, c: complex) -> int:
        z = 0
        for i in range(self.max_iterations):
            z = z ** 2 + c
            if abs(z) > 2:
                return i
        return self.max_iterations

    def stability(self, c: complex) -> float:
        return self.escape_count(c) / self.max_iterations

    def is_stable(self, c: complex) -> bool:
        # Return True only when stability is 1
        return self.stability(c) == 1
mandelbrot = Mandelbrot(max_iterations=50)

mandelbrot.stability(-.1)
1.0
mandelbrot.is_stable(-.1)
True
mandelbrot.stability(2)
0.02
mandelbrot.is_stable(2)
False

现在,我们创建一个最终的方法来使用 Matplotlib plot 集合:

@dataclass
class Mandelbrot:
    max_iterations: int

    # ... The rest of the code from above

    @staticmethod
    def candidate_values(xmin, xmax, ymin, ymax, pixel_density):
        real = np.linspace(xmin, xmax, num=int((xmax-xmin) * pixel_density))
        imag = np.linspace(ymin, ymax, num=int((ymax-ymin) * pixel_density))

        xx, yy = np.meshgrid(real, imag)
        matrix = xx + 1j * yy

        return matrix

    def plot(self, xmin, xmax, ymin, ymax, pixel_density=64, cmap="gray_r"):
        c = Mandelbrot.candidate_values(xmin, xmax, ymin, ymax, pixel_density)

        # Apply `stability` over all elements of `c`
        c = np.vectorize(self.stability)(c)

        plt.imshow(c, cmap=cmap, extent=[0, 1, 0, 1])
        plt.gca().set_aspect("equal")
        plt.axis('off')
        plt.tight_layout()

plot 中,我们对 c 的所有元素应用 stability 方法,因此结果矩阵在每个单元格中保存颜色深度。当我们用反向灰度色图绘制这个矩阵(以便 Mandelbrots 保持黑色),我们会得到以下图像:

mandelbrot = Mandelbrot(max_iterations=30)

mandelbrot.plot(
    xmin=-2, xmax=0.5, 
    ymin=-1.5, ymax=1.5, 
    pixel_density=1024,
)

作者提供的图像

我们已经可以看到边缘周围出现了不同灰度级别的带状区域。让我们给它一个不那么无聊的灰度色图:

mandelbrot = Mandelbrot(max_iterations=30)

mandelbrot.plot(
    xmin=-2, xmax=0.5, 
    ymin=-1.5, ymax=1.5, 
    pixel_density=1024,
    cmap="gist_heat"
)

作者提供的图像

注意边界线是最亮的红色,并且在集合重复的地方仍然会出现白点。太棒了!

结论

我们的最终结果几乎可以称得上艺术。但仍有许多改进空间。首先是通过更精细地控制每个像素来提高图像分辨率。然后,我们需要去除图像周围那恼人的白色空白(如果你在暗模式下阅读)。

这些任务都是 Matplotlib 的不足之处,但在下一篇文章中,我们将通过 Pillow(Python 的图像处理库)将事情提升到一个全新的水平。

敬请关注

喜欢这篇文章吗?还有它那奇特的写作风格?想象一下,如果你能访问到数十篇类似的文章,全部由一个聪明、迷人、风趣的作者(就是我 😃)撰写,那会怎样。

仅需 4.99$ 会员,你不仅可以访问我的故事,还能获取来自 Medium 上最佳和最聪明的头脑的宝贵知识。如果你使用我的推荐链接,你将获得我的超级感谢和一记虚拟的击掌,以支持我的工作。

[## 使用我的推荐链接加入 Medium — Bex T.

独家访问我的所有 ⚡premium⚡ 内容,并在 Medium 上无限制浏览。通过为我购买一份来支持我的工作…

ibexorigin.medium.com

使用 Matplotlib 创建惊艳的雷达图

原文:towardsdatascience.com/create-stunning-radar-plots-with-matplotlib-6a8e05054ff9

使用 Python 最受欢迎的数据可视化库可视化地质数据

Andy McDonaldTowards Data Science Andy McDonald

·发布于 Towards Data Science ·阅读时间 7 分钟·2023 年 4 月 19 日

--

带有赛博朋克主题的 Matplotlib 雷达图。图片由作者提供。

雷达图(也称为蜘蛛图或雷达图表)是一种流行的数据可视化工具,它允许我们通过在二维图上同时显示多个变量来比较数据集。

每个变量由一个从图中心延伸到边缘的射线表示,该变量的大小由射线的长度表示。然后在每个变量之间绘制线条,以形成类似网状的形状。

在地球科学和岩石物理学中,我们可以使用雷达图来比较不同钻井间岩石类型的变化,或展示岩石样本间矿物学的差异。

在本教程中,我将演示如何使用一些合成的岩石类型数据来创建雷达图。这些数据代表了在一个钻井中遇到的平均岩石类型。

让我们开始吧!

导入库和数据

要开始,我们需要导入两个库:numpymatplotlib

import matplotlib.pyplot as plt
import numpy as np

接下来,我们可以创建一些虚拟数据。

通常,这些数据可能已经包含在 pandas 数据框中。如果是这样,建议在继续之前将所需的列转换为列表。

lithologies = ['Shale', 'Sandstone', 
               'Limestone', 'Dolomite', 
               'Anhydrite', 'Coal', 'Tuff']

well1 = [47, 35, 10, 1, 0, 2, 5]

well2 = [7, 10, 51, 22, 10, 0, 0]

在使用数据之前,我们需要确保在雷达图上绘制的区域形成一个封闭的多边形。

为了做到这一点,我们需要将列表中的所有元素取出,然后将第一个元素附加到列表的末尾。

lithologies = [*lithologies, lithologies[0]]
well1 = [*well1, well1[0]]
well2 = [*well2, well2[0]]

如果我们调用 well2,我们将得到以下值列表,我们会发现第一个数字现在也在列表末尾。

[7, 10, 51, 22, 10, 0, 0, 7]

使用 Matplotlib 构建雷达图

为了开始构建雷达图,我们首先需要对lithologies列表中的标签位置进行排序。这些标签将均匀分布在极坐标图的边缘。

label_loc = np.linspace(start=0, stop=2 * np.pi, num=len(lithologies))

接下来,我们可以开始创建雷达图。

首先,我们需要定义图形,这将通过使用plt.subplots来创建。在subplot_kw参数中,我们需要指定我们想要一个极坐标图。

接下来,我们将向图中添加两个坐标轴,并传入label_loc变量以及well1well2列表。

然后,我们将使用plt.thetagrids()创建极坐标网格,在这里,我们将传入标签位置和岩性标签。

fig, ax = plt.subplots(figsize=(10,10), subplot_kw=dict(polar=True))

ax.plot(label_loc, well1, lw=2)
ax.plot(label_loc, well2, lw=2)

lines, labels = plt.thetagrids(np.degrees(label_loc), labels=lithologies)

plt.show()

当我们运行上述代码时,我们将得到以下雷达图。

使用 matplotlib 创建的基本雷达图。图片由作者提供。

我们得到的图对于基本的雷达图来说很好。然而,我们可以通过一些调整来改进它。

向 Matplotlib 雷达图添加填充

我们可以通过给两个区域添加透明填充来改进雷达图。

这是通过使用ax.fill()完成的,传入label_loc、包含值的相关井列表,并通过alpha参数将不透明度设置为 0.3。

fig, ax = plt.subplots(figsize=(10,10), subplot_kw=dict(polar=True))

ax.plot(label_loc, well1, lw=2)
ax.plot(label_loc, well2, lw=2)

ax.fill(label_loc, well1, alpha=0.3)
ax.fill(label_loc, well2, alpha=0.3)

lines, labels = plt.thetagrids(np.degrees(label_loc), labels=lithologies)

plt.show()

当我们运行上述代码时,我们现在得到的雷达图填充了区域。

使用 matplotlib 创建的基本雷达图,显示了区域填充。图片由作者提供。

调整 Matplotlib 雷达图上的标签填充

你会注意到,之前图中的标签看起来很小,并且与线条重叠。

我们可以通过设置tick_params并更改填充和标签大小来修复这个问题。

fig, ax = plt.subplots(figsize=(10,10), subplot_kw=dict(polar=True))

ax.plot(label_loc, well1, lw=2)
ax.plot(label_loc, well2, lw=2)

ax.fill(label_loc, well1, alpha=0.3)
ax.fill(label_loc, well2, alpha=0.3)

lines, labels = plt.thetagrids(np.degrees(label_loc), labels=lithologies)

ax.tick_params(axis='both', which='major', pad=30, labelsize=15)

plt.show()

当我们运行代码时,我们得到的图标签更加清晰。

使用 matplotlib 创建的基本雷达图,显示了区域填充,并在调整标签填充和大小后。图片由作者提供。

使用赛博朋克风格美化雷达图

之前的图形成了一个适用于报告或演示的良好雷达图。然而,如果我们想要美化它,可以使用众多 matplotlib 主题库中的一种。

如果你想查看可用的库,那么我建议查看我之前的文章,其中介绍了 4 个流行的主题。

## 提升你的数据可视化:4 个 Python 库来增强你的 Matplotlib 图表

使用这些易于使用的 Matplotlib 样式库来提升你的数据可视化

towardsdatascience.com

网络朋克主题是我在 matplotlib 中创建时尚图表的最爱之一。然而,这完全是出于美学考虑,你应始终考虑观众并确保最大可读性。

我们可以通过导入库并添加 with 语句来使用网络朋克主题。

import mplcyberpunk

with plt.style.context('cyberpunk'):
    fig, ax = plt.subplots(figsize=(10,10), subplot_kw=dict(polar=True))

    ax.plot(label_loc, well1, lw=2)
    ax.plot(label_loc, well2, lw=2)

    ax.fill(label_loc, well1, alpha=0.3)
    ax.fill(label_loc, well2, alpha=0.3)

    lines, labels = plt.thetagrids(np.degrees(label_loc), labels=lithologies)

    ax.tick_params(axis='both', which='major', pad=30, labelsize=15)

    plt.show()

我们现在可以看到,通过添加两行代码,图表已经发生了显著变化。然而,网格线对于我的喜好来说有点模糊。

应用网络朋克主题后的 Matplotlib 雷达图。图像由作者提供。

我们可以通过添加一些额外的代码来使线条变亮。

更改网格线可以使用 ax.grid(),并将 coloralpha 参数设置为白色和 0.3。

为了给雷达图添加外环,我们需要设置一个包含 4 个值的自定义元组:红色、绿色和蓝色的颜色值以及一个 alpha 值。

然后我们可以将其传递到 ax.spines['polar'].set_color() 调用中。

with plt.style.context('cyberpunk'):
    fig, ax = plt.subplots(figsize=(10,10), subplot_kw=dict(polar=True))

    ax.plot(label_loc, well1, lw=2)
    ax.plot(label_loc, well2, lw=2)

    ax.fill(label_loc, well1, alpha=0.3)
    ax.fill(label_loc, well2, alpha=0.3)

    lines, labels = plt.thetagrids(np.degrees(label_loc), labels=lithologies)

    ax.tick_params(axis='both', which='major', pad=30, labelsize=15)

    ax.spines['polar'].set_linewidth(3)

    edge_color = (1, 1, 1, 0.2) 
    ax.spines['polar'].set_color(edge_color) 

    ax.grid(color='white', alpha=0.3)

    plt.show()

当我们运行代码时,现在可以看到带有径向网格线的雷达图。我们可以调整颜色和透明度,以达到想要的效果。

然而,请记住我们不希望线条过于明亮,以免分散读者对主要数据的关注。

带有网络朋克主题和可见网格线的 Matplotlib 雷达图。图像由作者提供。

为了完成样式设置,我们可以设置网格线的范围,从而使其从我们想要的数字开始并结束。这将使外环与其他环更加协调。

我们可以通过设置 ax.set_ylim() 并传入我们的最小值和最大值来实现这一点。

with plt.style.context('cyberpunk'):
    fig, ax = plt.subplots(figsize=(10,10), subplot_kw=dict(polar=True))

    ax.plot(label_loc, well1, lw=2)
    ax.plot(label_loc, well2, lw=2)

    ax.fill(label_loc, well1, alpha=0.3)
    ax.fill(label_loc, well2, alpha=0.3)

    lines, labels = plt.thetagrids(np.degrees(label_loc), labels=lithologies)

    ax.tick_params(axis='both', which='major', pad=30, labelsize=15)

    ax.spines['polar'].set_linewidth(3)

    edge_color = (1, 1, 1, 0.2) 
    ax.spines['polar'].set_color(edge_color) 

    ax.grid(color='white', alpha=0.3)

    ax.set_ylim(0, 60)

    plt.show()

我们现在有了一个看起来更好的雷达图。

带有网络朋克主题并修正环范围的 Matplotlib 雷达图。图像由作者提供。

给雷达图添加图例

为了完成我们的图表,我们可以通过添加图例来帮助读者更好地理解图表。这将使读者了解每个区域的含义。

我们可以简单地在 ax.plot() 调用中添加一些标签,然后调用 ax.legend,然而,这只会向我们的图例中添加线条。

如果我们想要添加表示阴影区域的小矩形,我们必须首先创建一些补丁。

这可以通过从 matplotlib.patches 中导入 Patch 来完成。

创建的补丁随后被传递到 ax.legend 调用中。

from matplotlib.patches import Patch

with plt.style.context('cyberpunk'):
    fig, ax = plt.subplots(figsize=(10,10), subplot_kw=dict(polar=True))

    ax.plot(label_loc, well1, lw=2)
    ax.plot(label_loc, well2, lw=2)

    ax.fill(label_loc, well1, alpha=0.3)
    ax.fill(label_loc, well2, alpha=0.3)

    lines, labels = plt.thetagrids(np.degrees(label_loc), labels=lithologies)

    ax.tick_params(axis='both', which='major', pad=30, labelsize=15)

    ax.spines['polar'].set_linewidth(3)

    edge_color = (1, 1, 1, 0.2) 
    ax.spines['polar'].set_color(edge_color) 

    ax.grid(color='white', alpha=0.3)

    ax.set_ylim(0, 60)

    # Create custom legend handles
    well1_legend = Patch(facecolor='C0', alpha=0.5, label='Well 1')
    well2_legend = Patch(facecolor='C1', alpha=0.5, label='Well 2')

    # Add a legend with custom position and handles
    ax.legend(handles=[well1_legend, well2_legend],
              bbox_to_anchor=(1.3, 0.2), fontsize=20, 
              frameon=True)

    plt.show()

当我们运行代码时,现在可以看到完成的图表。

带有网络朋克主题的 Matplotlib 雷达图。图像由作者提供。

总结

雷达图提供了良好的数据可视化,并且可以使用 matplotlib 轻松创建。通过 Cyberpunk 主题库的额外样式,我们可以将雷达图提升到一个新的水平,使其更具时尚感。

为什么不在下一个数据可视化项目中尝试一下雷达图呢?

感谢阅读。在离开之前,你一定要订阅我的内容,并将我的文章直接发送到你的收件箱。 你可以在这里做到这一点!或者,你可以 注册我的新闻通讯 以获取额外的内容,直接免费送到你的收件箱。

其次,你可以通过注册会员获得完整的 Medium 体验,并支持成千上万的其他作者和我。每月只需 $5,你就可以完全访问所有精彩的 Medium 文章,还有机会通过写作赚取收入。

如果你使用 我的链接注册, 你将直接通过一部分费用支持我,而且不会额外增加你的支出。如果你这样做了,非常感谢你的支持。

使用 Folium 创建量身定制的旅游地图

原文:towardsdatascience.com/create-tailor-made-tourist-maps-with-folium-954706cc04e8

为你的下一次假期增添色彩!

Lee VaughanTowards Data Science Lee Vaughan

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

--

图片来源:Kayla DuhonUnsplash

我是那些花哨旅游地图的粉丝。你知道我说的是哪种:那些五颜六色、卡通风格的地图,带有城堡、纪念碑和伦敦眼的 3D 渲染。能为你的假期制作一个定制版本岂不是太棒了?好好准备好护照吧,因为如果你知道 Python,那正是我们要做的事!

在这个快速成功数据科学项目中,我们将使用Folium库为我的冰岛假期制作一个定制的旅游地图。我们将使用自定义图标标记火山和瀑布,叠加地形和道路地图,并标记住宿位置。如果你从未去过冰岛,也不用担心,因为你可以将这个项目作为你自己旅行的模板。

涵盖的主要编程主题

这个项目不仅仅是乐趣和游戏。你还将学习一些有用的地理空间技术,例如:

  1. 如何在 Folium 中叠加基础地图图块以在同一地图上显示多种功能类型。

  2. 如何使用自定义标记在地图上标记功能。

  3. 如何在 Folium 地图上添加静态文本并改变其大小和颜色(这并不像你想象的那样简单)。

  4. 如何添加索引地图。

Folium

开源 Folium 库允许你使用 Leaflet.JS 这个强大的 JavaScript 库来可视化地图,Leaflet.JS 用于在移动和桌面平台上构建互动式网页映射应用。Folium 于 2013 年首次发布,非常流行,你会在互联网上找到大量资料来帮助你学习如何使用和定制它。

使用 Folium,你可以从 OpenStreetMap、Mapbox 和 Stamen 等地图服务中选择 瓦片集。这些瓦片集是将光栅或矢量数据分解成统一的方形网格,具有多达 22 个预设缩放级别的集合。它们让你轻松生成美丽的 leaflet 地图。

在 conda 环境中安装 Folium,请使用以下命令:

conda install -c conda-forge folium

使用 pip 安装 Folium,请使用:

pip install folium

为了方便起见,我们还将使用开源的 pandas。你可以使用以下命令安装:

conda install pandas

pip install pandas

代码

以下代码已输入到 Jupyter Notebook 中,并按单元格进行描述。

导入库

Folium 旨在用于 Web 应用中的动态、交互式地图。然而,对于这个项目,我们将用它制作一个 静态 地图,可以打印出来放在背包里。

我们将使用 Folium 的 plugins 模块制作一个索引地图,将映射区域置于更大的上下文中。DivIcon 模块将允许我们为瀑布和火山等对象使用自定义图标。

import pandas as pd
import folium
from folium import plugins
from folium.features import DivIcon

加载火山数据库

冰岛也很容易被命名为“火山之地”,因为它有大约 130 座火山山脉,其中超过 30 座被归类为“活跃”。我编制了一份包含这些火山名称和坐标的 CSV 文件,并将其保存为 Gist。为了方便起见,我们将把这个文件导入为 pandas DataFrame。

volcano_df = pd.read_csv('https://bit.ly/458vXfu')
volcano_df.head(3)

火山数据框(作者提供的图片)

住宿

在长时间的假期中很容易迷失时间,因此我们将记录每晚住宿的位置,标记为“第 1 天”、“第 2 天”等(你也可以使用实际日期或其他标记)。为了生成可绘制的数据库,我在线搜索了每个城镇的纬度和经度,并将其输入为 Python 字典。

accommodations = {'Days 1-3': (64.128288, -21.827774),
                  'Day 4':    (65.688492, -18.126169),
                  'Day 5':    (65.6000, -17.0000),
                  'Day 6':    (65.28333, -14.40139),
                  'Day 7':    (64.656888, -14.290051),
                  'Day 8':    (63.7833302, -18.0666664),
                  'Day 9':    (63.5445, -19.8),
                  'Day 10':   (64.258149, -20.514890),
                  'Day 11':   (64.80806, -22.80500)}

瀑布

冰岛也可以被称为“瀑布之地”,因为它有超过 10,000 个瀑布。幸运的是,我只计划访问六个,所以我们将这些瀑布作为 Python 字典输入,就像我们为住宿做的一样。

waterfalls = {'Kolugjufur':     (65.3335, -20.5645),
              'Hengifoss':      (65.1, -14.9),
              'Svartifoss':     (64.023, -16.975),
              'Skogafoss':      (63.5245, -19.5083),
              'Seljalandsfoss': (63.6095, -19.989),
              'Gullfoss':       (64.3223, -20.1193)}

自定义图标

图标 是地图上特征的视觉表示。一个熟悉的例子是用来标记 Google Maps 上位置的倒置泪滴。对于这个项目,我们将为火山和瀑布使用自定义图标。对于住宿,我们将使用 Folium 的标准 圆形标记

本项目中使用的图标(作者提供的图片)

为了表示瀑布,我使用了随 Microsoft 365 订阅提供的免版税瀑布图标。对于火山,我使用了来自 ICONFINDER 的图标。我为此图标支付了费用,因此它可以 商业使用 而无需注明来源。

我不能提供这些特定的图标,所以你需要自己准备。只需在线搜索“瀑布图标”和“火山图标”。当然,你需要确保这些图标有适当的许可,适用于你打算使用它们的目的。

Folium 支持多种文件类型,如 PNG 和 JPG,并允许缩放图标图像,因此你无需担心图标的大小。将图标保存在与笔记本或 Python 脚本相同的文件夹中。

绘制地图

最终的笔记本单元格绘制了地图。在高层次上,我们将首先选择地图的中心点位置,然后在预定的缩放级别上覆盖图块。接着,我们会循环遍历火山、瀑布和住宿数据库,并发布图标及相应的名称。最后,我们会添加一个索引地图。更详细的解释在代码后面。

# Create basemap and overlay tiles:
center_location = [64.9, -18.6]
map = folium.Map(location=center_location, 
                 tiles='Stamen Terrain', 
                 control_scale=True,
                 zoom_start=7)
folium.raster_layers.TileLayer(tiles='Stamen Toner', opacity=0.3).add_to(map)

# Loop through volcano DataFrame and post volcano names and symbols:
for index, row in volcano_df.iterrows():
    volcano_icon = folium.features.CustomIcon('volcano_icon.png', 
                                              icon_size=(20, 20))

    folium.Marker(location=(row['Latitude'], row['Longitude']),
                  icon=volcano_icon).add_to(map)

    folium.map.Marker((row['Latitude'], row['Longitude']),
                      icon=DivIcon(
                          icon_size=(25, 25),
                          icon_anchor=(-5, 14),
                          html=f'<div style="font-size: 8pt">%s</div>' % str(
                               row['Volcano Name']))).add_to(map)

# Loop through waterfalls dictionary and post waterfall names and symbols:  
for key, value in waterfalls.items():
    waterfall_icon = folium.features.CustomIcon('waterfall_icon_dark_blue.jpg', 
                                                icon_size=(25, 25))

    folium.Marker(location=value,
                  fill_opacity=0.5,
                  icon=waterfall_icon).add_to(map)

    folium.map.Marker(location=value,
                      icon=DivIcon(
                          icon_size=(23, 23),
                          icon_anchor=(-10, 14),
                          html=f'<div style="color: blue">%s</div>' % str(key))
                     ).add_to(map)

# Loop through accommodations dictionary and post names and symbols:  
for key, value in accommodations.items():          
    folium.CircleMarker(location=value,
                        radius=8,    
                        color='red',
                        fill_color ='red',
                        fill_opacity=0.5).add_to(map)

    folium.map.Marker(location=value,
                      icon=DivIcon(
                          icon_size=(45, 30),
                          icon_anchor=(15, 9),
                          html=f'<div style="font-size: 8pt">%s</div>' % str(key)
                      )
                     ).add_to(map)

# Add index map:
mini_map = folium.plugins.MiniMap(toggle_display=True)
map.add_child(mini_map)

map 

第一步是为地图的中心点分配经纬度坐标到一个名为center_location的变量中。接下来,我们调用 Folium 的Map类,并传入位置、要使用的图块和初始缩放级别。control_scale参数激活地图的比例尺。

虽然Stamen Terrain图块包含道路,但它们很淡且难以辨认,因此我们用较暗的Stamen Toner图块覆盖地形图块。通过将 toner 图块的不透明度设置为较低的值,比如 0.3,地形图块仅部分被遮盖。

要查看 Folium 中可用的其他地图图块,请查看这个博客

接下来,我们循环遍历火山数据帧并发布其内容。folium.features.CustomIcon()方法允许我们使用自定义火山图标并控制其大小。folium.Marker()类实际上会将每个火山图标发布到地图上。

这是难点。Folium 旨在用于动态、交互式的网页地图,因此做一些看似基本的静态文本发布操作有点麻烦。我们必须使用html参数并传递f'<div style="font-size: 8pt">%s</div>' % str(row['Volcano Name'])。我们使用这种 HTML 格式不仅是为了发布名称,还为了控制其字体大小。注意文本必须转换为字符串后才能绘制。

绘制瀑布和住宿地点遵循相同的基本格式。不同的是,我们使用html参数将瀑布的文本颜色更改为蓝色。

我们通过添加一个索引地图(mini_map)来完成。这张地图会在你放大和缩小时动态调整位置。

这是最终的地图:

最终的旅游地图(作者提供的图像)

制作旅游地图时一个重要的考虑因素是根据你想要打印的最终缩放级别来调整图标和文本。理想情况下,这将是传递给folium.Map()时的zoom_start参数的缩放级别。虽然 Folium 允许你放大或缩小地图,但图标的相对大小会发生变化,你可能会对结果不满意。

打印地图

如前所述,Folium 地图旨在在线使用。虽然可以通过编程方式将其保存为图片,但直接进行屏幕截图更为简单。然后,你可以将结果粘贴到 Microsoft PowerPoint 或 Publisher 等程序中,以便在打印前进行额外的修饰。

对于图标和/或文本重叠的繁忙区域,你可以放大并打印“局部”地图。请注意,你可能需要使用新的笔记本单元来调整此新缩放级别的图标和文本大小。

一个放大的“局部”地图(图片来源:作者)

总结

游客地图是探索新区域的有趣方式。通过将 Folium 的精彩图块与自己的自定义图标和文本结合起来,你可以制作个性化地图,以便规划你的旅行和记录你的冒险。

谢谢!

感谢阅读,请关注我以获取更多未来的快速成功数据科学项目。

创建你自己的生成 AI 文本到图像 API

原文:towardsdatascience.com/create-your-own-generative-ai-text-to-image-api-548c07a4d839

将你的随想转化为杰作,按需制作

奥默尔·马赫穆德Towards Data Science 奥默尔·马赫穆德

·发表于 Towards Data Science ·17 分钟阅读·2023 年 4 月 12 日

--

图片由作者使用 Midjourney 根据一般商业条款生成。

TL;DR

  • 生成 AI 的最新进展导致了一系列服务的推出,如 DALL-E 2、Midjourney 和 Stability AI,它们有潜力彻底改变我们对内容创作的方式。

  • 在这篇文章中,我将展示如何通过 API 构建并提供你自己高性能的文本到图像服务。基于 稳定扩散 通过 HuggingFace,使用 Vertex AI Workbench 和 Endpoints。

🚣🏼 我们是如何到达这里的

正如乔治·劳顿在他的 文章 中提到的:“生成 AI 是一种人工智能技术,能够生成各种类型的内容,包括文本、图像、音频和合成数据。最近围绕生成 AI 的热议是由于新用户界面的简便性,这些界面可以在几秒钟内创建高质量的文本、图形和视频。”[2]

机器学习并不是什么新鲜事,事实上,自 1960 年代以来,它以某种形式存在。“但直到 2014 年,随着 生成对抗网络(GANs)的引入,一种机器学习算法,生成 AI 才得以创造令人信服的真实人物图像、视频和音频。”[2]

结合能够接收自然语言提示并生成照片级真实图像的大型语言模型(LLMs)的能力,我们在短时间内取得了巨大的进步。第一个做到这一点的是OpenAI 的 DALL·E,于 2022 年 4 月推出,随后是 2022 年 8 月的 Disco Diffusion,最终被稳定扩散所取代。与这些产品并行的是一家名为Midjourney的公司,它开发了一个非常受欢迎的模型,通过与 Discord 机器人互动使用。

从那时起,艺术状态的进展令人惊叹。下面的截图展示了仅在几个月的时间内所取得的成就——两个不同的模型提供了相同的提示,但早期模型和后期模型之间的对比非常明显!

图 1:生成性人工智能艺术的进展,由 Disco Diffusion 和 Midjourney 生成的图像。

快进到 2023 年,Midjourney 于 3 月 15 日刚刚发布了第 5 版。这个模型具有非常高的连贯性,擅长解释自然语言提示,分辨率更高,并支持像重复图案这样的高级功能[4]。Stability AI 也发布了稳定扩散 2.1。这些模型的发展速度非常迅猛!

正如德勤最近发布的白皮书中描述的:“我们感觉可能只是刚刚开始看到生成性人工智能模型所能带来的影响。虽然早期的吸引力主要来自消费者发布,这可能会成为定义时代的里程碑,但生成性人工智能也有潜力为几乎所有生活领域增加情境意识和类人决策能力。

因此,生成性人工智能吸引了来自传统(例如风险投资(VC),并购(M&A))和新兴(例如生态系统合作伙伴)来源的兴趣。仅在 2022 年,风险投资公司就投资了超过 20 亿美元,科技领袖也进行了重要投资,如微软对 OpenAI 的 100 亿美元投资和谷歌对 Anthropic 的 3 亿美元投资。”[3]

🧐 什么是稳定扩散?

稳定扩散是一种用于人工智能文本生成图像的方法,它利用扩散模型从文本描述中创建图像。扩散模型从一张随机图像开始,然后逐步向其中添加噪声。噪声以受控的方式添加,使得图像仍然可以辨认。

在每个扩散步骤中,图像变得更加精细,细节变得更加清晰。这个过程会持续几个扩散步骤,直到生成的图像被认为“稳定”,这意味着我们已经达到了一个进一步迭代不会改善图像的点。过程如下所示:

图 2:Stable Diffusion 过程,图像来源: Benlisquare,根据许可共享: CC BY-SA 4.0

扩散模型在一个图像和文本描述的数据集上进行训练,它学会将文本描述与匹配的图像关联起来。当你给扩散模型一个新的文本描述时,它会利用其知识生成一个匹配描述的图像。

Stable Diffusion 的主要优势在于它非常快速。它可以在几秒钟内从文本描述生成图像。这比其他方法(如 GANs)快得多。我们的现代竞争者(例如 Midjourney、DALL E 等)都使用某种变体的 Stable Diffusion 技术。

但 Stable Diffusion 并不完美。虽然在过去一年中它的应用有了飞跃式的进步,但有时生成的图像可能会失真或缺乏细节。这可能是因为像这样的模型会根据训练数据来“猜测”图像应该是什么样子。随着训练数据集和模型算法的改进,这个问题将会减少。

👷🏾‍♀️ 开始吧!

在这篇文章中,我将向你展示如何使用一些代码来:

  1. 互动地实验 Stable Diffusion 模型,生成一些酷炫的艺术作品!

  2. 通过端点服务模型 使用 Vertex AI。

  3. 使用 FLASK 创建一个简单的 RESTful API

当然,你可以直接使用我们在前面章节中提到的现有的面向消费者的模型,但那样会没有乐趣啊?😜

➡️ 随意跳到你感兴趣的部分!

👾 我会把任何代码片段(如果没有链接的话)放在一个 github 仓库中,链接见最后的有用资源部分

👩🏻‍💻 1. 实验 Stable Diffusion

当你开始处理一个新的机器学习问题时,笔记本提供了一种非常灵活的方式来测试模型和快速迭代。也许你喜欢在本地环境中运行 Jupyter,使用 Kaggle Kernel,或者我个人最喜欢的 Colab。有了这些工具,创建和实验机器学习变得越来越可及。

我们将创建一个快速的笔记本,向你介绍 Stable Diffusion 模型

💨 如果你没有时间(或不想)一步步构建,可以查看资源部分下载笔记本文件!

前提条件:

  • 用于登录 Colab 和保存笔记本的 Google 账户

  • HuggingFace 账户和 API 令牌(免费)

  1. 访问 colab.research.google.com/。这将为你创建一个新的 Python 笔记本,并将其保存到你的 Google Drive 中。

  2. Colab 免费提供,但如果你需要更多的内存和计算资源来运行你的笔记本,你需要付费。我们将只使用免费的资源,首先你需要配置笔记本运行时以包含 GPU 加速器。使用 GPU 会加快模型生成图像的时间,也就是执行推理。免费层提供了 12GB 内存的 NVIDIA Tesla T4 GPU(这对我们要做的事情正好足够)。从文件菜单中选择 运行时 -> 更改运行时类型,然后选择硬件加速器 = GPU。

图 3:Colab 笔记本,更改笔记本运行时。

然后你需要连接到你的运行时,以便我们可以执行笔记本中编写的任何代码。点击 连接/重新连接 -> 连接到托管运行时

图 4:Colab 笔记本,“+ 代码”按钮添加单元格,以及连接到托管运行时。

  1. 一旦你连接到你的运行时,就可以输入一些代码了!你可以在单个笔记本“单元格”中输入代码,或者如果你想单独执行步骤,可以使用“+ 代码”按钮创建一个新的“单元格”。我发现将代码分解成单元格很有用,这样更容易识别和调试问题。每个单元格在你悬停时都有一个“播放”按钮,点击它以运行代码。输入或复制粘贴代码到新的单元格中后,你可以运行以下步骤:

a. 由于我们需要从 HuggingFace 拉取模型,因此需要安装一些 Python 库并使用 API 令牌进行认证:

!pip install --upgrade huggingface_hub

b. 接下来,我们需要 使用你 HuggingFace 账户的令牌进行认证,当你执行以下代码时,你会在笔记本中被提示粘贴你的 HuggingFace API 令牌:

from huggingface_hub import notebook_login
notebook_login()

c. 成功登录你的 HuggingFace 账户后,我们将下载 diffusers 和 transformers Python 库:

!pip install -qq -U diffusers transformers

d. 我们需要创建一个 StableDiffusion 模型管道,以便我们可以将模型传递一些文本,并根据该提示生成图像。你可能会注意到我们传递的参数之一是 HuggingFace 上托管的 Stable Diffusion 模型的路径。此帖子中的示例使用的是 v1.5,你可以尝试在写作时交换为最新的(v2.1):

from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained('runwayml/stable-diffusion-v1–5')
.to('cuda')

e. 现在进入有趣的部分,我们将生成一些艺术作品!加载 torch Python 库,然后添加一个单元格,将 prompt 变量的字符串值替换为你想要的任何内容!我提供了一个简单的示例以帮助你入门:

import torch

# Initialize a prompt
prompt = “polar bear on an iceberg”

# Pass the prompt in the pipeline
pipe(prompt).images[0]

你应该得到如下所示的结果:

图 5:Colab 笔记本,Stable Diffusion 模型生成的图像输出。

💡提示: 如果你遇到‘内存不足’的错误,你可以通过在文本提示上方添加以下代码行定期清除 GPU 的缓存:

torch.cuda.empty_cache()

只需在一个笔记本中写几行代码,你就可以从基于文本的提示生成艺术作品!

🍦 2. 通过 Vertex AI 端点服务模型

制作生产应用程序或训练大型模型需要额外的工具来帮助你超越笔记本中的代码,并且使用云服务提供商可以提供帮助。

我们的目标是将 Stable Diffusion 模型打包,并托管在一个可以处理来自应用代码的预测请求的端点上。

你可以使用其他公共云服务提供商来实现大致相同的结果,但我将使用 Google Cloud Platform (GCP),特别是它的 Vertex AI 工具集,因为我对此最为熟悉。

前提条件:

  • 启用计费的 GCP 账户/免费起始积分

  • 具备一些基本的 GCP 管理知识,例如如何创建项目、虚拟机、存储桶和其他资源,

  • 通过 IAM 给服务账户赋予权限,

  • 下载服务账户密钥,

  • 使用 Google Cloud SDK + 一些 Python 代码。

1. 我们将使用一个预构建的笔记本示例:github.com/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/community/vertex_endpoints/torchserve/dreambooth_stablediffusion.ipynb,该示例来自 GCP Vertex AI 在 Github 上的官方仓库。我不会在这里详细复述整个内容,只会解释最相关的部分以及我在实际操作中遇到的问题……代码示例通常没有看起来那么简单!

⚠️ 在这个示例中创建用户管理的笔记本并将模型部署到端点将产生费用。当你第一次创建 GCP 账户时,可以获得 $300 的免费积分,但推荐的硬件配置(以及模型部署的端点的任何后续调用)可能会很快消耗这些积分——你已经被警告了。我将在“结语”部分分享我实际产生的费用。

2. 如果这是你第一次使用 Vertex AI,请登录到你的 GCP 控制台,并确保从 Vertex AI 仪表板启用所有必要的 API:

图 6: GCP 中的 Vertex AI 仪表板,启用所有推荐的 API 按钮。

3. 打开 笔记本,在标题下方,你应该能看到一些按钮,包括“在 Colab 中运行”和“在 Vertex AI Workbench 中打开”。你需要点击后者。

4. 然后你需要配置一个 VM 实例来托管用户管理的笔记本。示例建议使用带有 85GB RAM 的 NVIDIA A100 实例,也就是“a2-highgpu-1g”。推理(从笔记本中的示例创建图像)很快,大约 11 秒钟就能从 Stable Diffusion 模型中返回图像。

5. 一旦你的实例创建完成,你可以通过 Workbench -> 用户管理的笔记本 访问它。

图 7:Vertex AI 工作台,用户管理的笔记本列表。

在你深入并点击 OPEN JUPYTERLAB 之前,确保给你的笔记本所在 VM 的服务账户授予权限,以便进行诸如创建存储桶和端点等操作。由于它是“用户管理”的,这不会自动为你完成。

6. 点击笔记本名称,在笔记本详细信息中你将看到所有者或服务账户别名。

图 8:Vertex AI,笔记本详细信息。

7. 转到 IAM & Admin -> IAM,然后点击 授予访问权限,粘贴或输入你的笔记本实例的服务账户别名,为了简化操作,我授予了“编辑者”访问权限。如果你很挑剔,你可以只授予 dreambooth_diffusion 示例步骤所需的特定权限。

图 9:GCP IAM & Admin, IAM, 编辑访问/分配角色。

8. 现在你应该准备好在 Jupyterlab 中运行 dreambooth_diffusion.ipynb 笔记本代码了,从工作台中打开它(如步骤 5 所示)。由于某种原因,示例的 git 仓库中的代码没有被复制到我的笔记本实例中,所以我只是打开了终端并快速克隆了 GitHub 仓库:

$ git clone https://github.com/GoogleCloudPlatform/vertex-ai-samples

9. 一旦你在 Jupyterlab 中打开了 dreambooth_diffusion.ipynb,你应该能够运行笔记本单元而不会遇到重大问题。笔记本的第一部分是通过步骤下载 Stable Diffusion 模型并从提示中创建图像。下一步是创建 Vertex AI 端点并将模型部署到那里进行服务。

10. 按照笔记本中的步骤进行:

a. 创建自定义 TorchServe 处理程序。

b. 将模型工件上传到 Google Cloud Storage。

c. 使用模型工件和预构建的 PyTorch 容器镜像创建 Vertex AI 模型。

d. 将 Vertex AI 模型部署到端点上。

只要你在之前启用了 Vertex API(参见步骤 2。如果你忘记了!),这一切应该都会顺利进行。对我而言,在创建端点之后,模型部署大约花费了 30 分钟。当它准备好提供服务时,你会在 端点 下看到类似的内容:

图 10:Vertex AI 端点。

你现在准备好从你的 Stable Diffusion 模型中处理请求了!!此时,如果你想节省成本,可以停止笔记本实例 VM 并删除你创建的存储桶。请参见 dreambooth_diffusion.ipynb 最后部分的“清理”步骤。

🧪 测试你的端点

要向 Vertex AI 端点发送请求,你需要使用支持发送具有适当 HTTP 方法和请求参数的请求的 HTTP 客户端库或命令行工具。

我在我的笔记本电脑上进行了本地测试。为此,你需要下载并安装 Vertex AI Python SDK,然后创建并下载一个服务密钥用于认证。

如果你保留了你的笔记本实例虚拟机,你可以使用之前的服务帐户别名,或者只需创建一个新的服务帐户,其权限至少包括从端点获取预测

1. 转到 IAM & Admin -> 服务帐户。然后点击服务帐户别名右侧操作下的三个点,然后点击 管理密钥

图 11:GCP IAM & Admin,服务帐户,管理服务帐户的密钥。

然后点击添加密钥 -> 创建新密钥,并按推荐的 .JSON 格式下载:

图 12:GCP IAM & Admin,服务帐户,为服务帐户创建密钥。

⚠️ 请记住,服务帐户密钥文件授予与你的 GCP 项目中服务帐户相同的权限。 始终非常小心处理该文件,删除它时不要再使用,并且永远不要将其上传到 Github 仓库,我见过太多这种情况!

2. 在终端窗口中,或在你运行代码以测试端点的地方,你需要设置一个变量以指向你下载的服务密钥。这将对端点请求进行认证:

$ export GOOGLE_APPLICATION_CREDENTIALS=”path/to/your.json file”

3. 现在只需用你的项目名称、区域和端点 ID 修改这个 Python 代码片段。它将向你的端点传递一个提示,并将响应(即由 Stable Diffusion 生成的图像)存储在 JPEG 文件中:

import base64
import logging
from io import BytesIO
from google.cloud import aiplatform as aip

PROJECT_NAME = “YOUR-PROJECT-ID”
REGION = “us-central1”
ENDPOINT_ID = “YOUR-ENDPOINT-ID”

aip.init(project=PROJECT_NAME, location=REGION)
endpoint = aip.Endpoint(endpoint_name=ENDPOINT_ID)
text_input = “””Polar bear on an iceberg”””

# Invoke the Vertex AI endpoint 
def query_endpoint(endpoint, text_input):
  payload = {“prompt”: text_input}
  response = endpoint.predict(instances=[payload])
  return response

response = query_endpoint(endpoint, text_input)

with open(“generated_imgage.jpg”, “wb”) as g:
    g.write(base64.b64decode(response.predictions[0]))

如果你安装了 Vertex AI Python SDK、进行了 GCP 认证,并且端点处于活动状态,几秒钟后你应该会看到生成的图像文件出现在你的文件系统中!

🎁 3. 使用 FLASK 创建一个简单的 RESTful API

此时,你可以很容易地将调用生成 AI 模型端点的代码集成到使用 Vertex AI Python SDK 的现有应用程序中。

但我确实承诺了一个 API,所以我将在这一部分的最后部分详细介绍它。

前提条件:

  • 安装 FlaskPillow Python 库。

  • 下载 Postman 并安装,它是免费的!我们将用它来模拟对我们的 API 的调用。

1. 与前一节一样,确保你有一个环境变量,指向你计划运行代码的地方,这个变量指向你的服务帐户密钥。

2. 这是你需要的代码,用于使用 Flask Web 应用程序框架创建一个简单的 RESTful API。请参见代码中的注释,了解正在发生的事情。我们基本上使用了之前的代码来查询端点,并将其封装在 API 调用中:

import base64
from google.cloud import aiplatform as aip
from flask import Flask, jsonify, request, send_file
from PIL import Image
from io import BytesIO

app = Flask(__name__)

@app.route(‘/predict’, methods=[‘POST’])
def predict():
  PROJECT_NAME = ‘YOUR-PROJECT-ID’
  REGION = ‘us-central1’
  ENDPOINT_ID = ‘YOUR-ENDPOINT-ID’

  # Get the input data from the HTTP request
  input_data = request.get_json()

  # Extract the text parameter from the input data
  prompt = input_data.get(‘prompt’, ‘’)

  aip.init(project=PROJECT_NAME, location=REGION)
  endpoint = aip.Endpoint(endpoint_name=ENDPOINT_ID)
  text_input = prompt

  # Invoke the Vertex AI
  payload = {“prompt”: text_input}
  response = endpoint.predict(instances=[payload])

  # Decode the image data from base64 format
  image_data = response.predictions[0]
  image_bytes = base64.b64decode(image_data)

  # Create a PIL Image object from the decoded image data
  image = Image.open(BytesIO(image_bytes))

  # Save the image to a BytesIO buffer
  buffer = BytesIO()
  image.save(buffer, format=’JPEG’)
  buffer.seek(0)

  # Return the image file in the response
  return send_file(buffer, mimetype=’image/jpeg’)

if __name__ == ‘__main__’:
    app.run(debug=True)

3. 运行生成的文件,你应该会有一个托管你 API 的 Flask 服务器,准备接收请求:

图 13:MacOS 终端,本地运行 Flask 服务器。

4. 接下来我们将启动 Postman,进入 文件 -> 导入,粘贴以下 cURL 命令(如果你的服务器地址配置与默认不同,你可能需要修改它):

curl -X POST \
-H “Content-Type: application/json” \
-d ‘{“prompt”: “Astronauts in the ocean”}’ \
http://127.0.0.1:5000/predict \
-- output generated_image.jpg

我们可以从终端运行这个,API 的响应会被保存为本地文件系统中的 JPEG 图像。但为了模拟一个应用程序,而且因为我没有时间编写 Discord 机器人或 HTML 前端… 😅

图 14:Postman,API 请求示例,Stable Diffusion 模型生成的图像响应。

如果一切顺利,你将得到一个漂亮生成的图像,通过你自己的 API 提供服务。你可以在“Body”标签下修改提示,创造力是唯一的限制!

🗑️ 实验完成了吗? 你可以返回到 Vertex AI -> 端点,选择端点,撤销模型部署,返回上一层删除它(提示:每行末尾的三点菜单)。仔细检查你是否关闭或删除了与这篇文章相关的任何其他内容,以避免消耗资源。你也可以撤销你创建的服务账户密钥,以确保安全。

⚠️ 最后警告 — 这显然不是生产就绪的代码。如果你打算让你的 API 对外公开,还有很多工作需要做,比如认证等,这些超出了本文的范围!相对而言,我们的端点每次请求的成本(或计算时间)也比 Midjourney 或 DALL-E 2 高得多,因此可能不适合作为服务上线。

🏆 总结和结束思考

我在写这篇文章时非常开心,并且学到了很多关于生成式 AI、Stable Diffusion 以及将其打包成类似今天流行的消费者服务的内容。我向那些站在这项技术最前沿的开发团队致敬,这确实是一个令人兴奋的领域!

💸 成本

我有点担心运行大型 GPU 附加实例。如果你是长期使用公共云的用户,你会知道没有适当的检查和制衡机制,很容易超支。

在写完这篇文章的最后,我查看了 GCP 控制台中的账单部分,看看花了多少钱…

图 15:GCP 账单,报告。

在使用 GCP 的两天里,专门为这篇文章创建的项目中,我花费了大约 40 美元。你可以在上面的截图中看到详细分解。

大部分费用与推理相关(参见 Vertex AI 服务条目)。这是使我的端点能够使用我部署的 Stable Diffusion 模型生成图像的“计算小时”费用。

端点使用了“Vertex AI:在线/批处理预测 NVIDIA Tesla P100 GPU 运行于美洲的 AI 平台 SKU”,还包括运行预定义实例的一些其他费用。我们本可以通过选择更便宜的 GPU 家族来减少开支,但这将导致生成图像的时间延长。

与商业上可用的解决方案相比,Midjourney 的 基础计划 每月订阅费用为 10 美元。这个计划包括每月 3.3 小时的快速 GPU 计算,超出部分费用为每小时 4 美元。在我们的案例中,我让端点运行了大约 24 小时,花费了 30 美元,没有并发或作业等待的限制。再次说明,与 Midjourney 这样的完全托管服务相比,工程师们始终在改进模型,而选择在可扩展的云基础设施上迭代和部署自己的模型之间存在权衡。

这里需要注意的重要一点是,你支付的不是每次预测请求的费用,而是你的端点运行的时间以及它所运行的实例的大小/类型。

测试模型和通过用户管理的笔记本实际部署端点的费用不到 10 美元(参见计算引擎和笔记本条目)。

所以,本文简要概述了生成式人工智能,特别是使用 Stable Diffusion 技术创建艺术的应用。接着,我们深入探讨了一些代码示例,以展示使用 Stable Diffusion 模型生成艺术的步骤,以及如何将其部署到端点并通过 API 使用。希望你喜欢这篇文章,我们下次见! 👋🏼

本文中的数据由作者创建,除非另有说明。

📇 参考资料

[1] 机器学习,历史及其与其他领域的关系: en.wikipedia.org/wiki/Machine_learning#History_and_relationships_to_other_fields

[2] 什么是生成式人工智能?你需要知道的一切,George Lawton: www.techtarget.com/searchenterpriseai/definition/generative-AI

[3] 生成式人工智能对企业的影响——人工智能的新前沿,Deloitte LLP: www2.deloitte.com/us/en/pages/consulting/articles/generative-artificial-intelligence.html

[4] Midjourney 文档,版本: docs.midjourney.com/docs/model-versions#:~:text=Current%20Model,places%2C%20objects%2C%20and%20more

📚 有用资源

在 SageMaker Studio 中创建你自己的大语言模型实验室

原文:towardsdatascience.com/create-your-own-large-language-model-playground-in-sagemaker-studio-1be5846c5089?source=collection_archive---------8-----------------------#2023-03-20

现在你可以在一个地方部署和实验大语言模型

Heiko HotzTowards Data Science Heiko Hotz

·

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

--

图片由作者提供 — 使用 Midjourney 创建

这是什么内容?

通过 REST 端点利用大语言模型(LLMs)具有众多优点,但通过 API 调用进行实验可能会很麻烦。以下我们将看到如何与已部署到 Amazon SageMaker 端点的模型进行交互。

图片由作者提供

为了简化这个过程,开发一个允许与已部署模型无缝互动的游乐场应用将是有利的。在本教程中,我们将通过使用 Amazon SageMaker(SM)Studio 作为我们的全功能 IDE,并将 Flan-T5-XXL 模型部署到 SageMaker 端点,随后创建一个基于Streamlit的游乐场应用,直接在 Studio 中访问。

本教程的所有代码都可以在这个GitHub 仓库中找到。

为什么这很重要?

评估和对比不同的 LLM 对组织来说至关重要,以确定最适合其独特需求的模型,并快速进行实验。一个游乐场应用提供了最便捷、快速和简单的方法,让利益相关者(无论是技术人员还是非技术人员)可以实验已部署的模型。

此外,利用游乐场应用可以增强对比,并促进进一步的定制,例如加入反馈按钮和对模型输出进行排名。这些附加功能使用户能够提供反馈,提升模型的精确性和整体性能。实质上,游乐场应用提供了对模型优势和劣势的更深入理解,最终帮助做出明智的决定,以选择最适合应用的 LLM。

让我们开始吧!

部署 Flan-T5-XXL 模型

在我们可以设置游乐场之前,我们需要设置一个 REST API 来访问我们的模型。幸运的是,在 SageMaker 中这非常简单。类似于我们部署 Flan-UL2 模型时所做的那样,我们可以编写一个推理脚本,从Hugging Face Model Hub下载模型,并将其部署到 SageMaker 端点。这个端点随后为我们提供一个 REST API,我们可以在 AWS 账户内访问,而不必使用 API Gateway。

请注意,我们使用了 8 位加载模型的选项,这使我们能够将模型部署到单个 GPU(G5 实例)上。

一旦我们准备好推理脚本,就可以通过一个命令部署模型:

欲了解更多详细信息,请查看部署笔记本和我之前的关于部署 Flan-UL2 的博客文章

一旦端点启动并运行,我们就可以开始有趣的部分——设置一个游乐场应用以与模型互动。

游乐场应用

我们将使用 Streamlit 开发一个精简的游乐场应用。只需几行代码,它就能让我们创建一个文本框,并在用户友好的界面中展示各种生成参数。欢迎您修改应用,并展示一组不同的生成参数,以便更好地控制文本生成过程。

所有生成参数的列表可以在这里找到。

请注意,你需要在第 10 行指定终端名称,你可以从 SageMaker 控制台的部署笔记本中获取。

测试

现在是时候部署和测试我们的实验平台应用程序了。受TensorBoard 在 SM Studio 中的使用说明的启发,我们可以使用相同的机制在 SM Studio 中启动我们的 Streamlit 应用。

为此,我们可以在终端执行命令streamlit run flan-t5-playground.py --server.port 6006。之后,我们将能够通过https://<YOUR_STUDIO_ID>.studio.<YOUR_REGION>.sagemaker.aws/jupyter/default/proxy/6006/访问这个实验平台。

作者提供的图片

结论

在本教程中,我们成功部署了一个前沿语言模型,并在单一环境 SageMaker Studio 中建立了一个实验平台。启动 LLM 实验的过程从未如此简单。希望你觉得这些信息有价值,如果你有任何问题或需要进一步的帮助,请随时联系我。

Heiko Hotz

👋 关注我在MediumLinkedIn上,阅读更多关于生成 AI、机器学习和自然语言处理的内容。

👥 如果你在伦敦,欢迎加入我们的NLP London Meetups

www.linkedin.com/in/heikohotz/

在 AWS 上快速创建你自己的稳定扩散 UI

原文:towardsdatascience.com/create-your-own-stable-diffusion-ui-on-aws-in-minutes-35480dfcde6a?source=collection_archive---------0-----------------------#2023-01-03

使用一个命令部署文本到图像的 web 应用

Heiko HotzTowards Data Science Heiko Hotz

·

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

--

图片由作者提供 — 使用稳定扩散创建

这是什么内容?

稳定扩散(SD)在 2022 年迅速成为最受欢迎的文本生成图像(即“AI 艺术生成”)模型之一。成功的一个关键因素是它被作为开源软件发布。这促使一个充满活力的社区迅速建立工具,使 SD 对任何对其感兴趣的人更加可及,无论其技术知识如何。

这些工具之一是简单而强大的 Web 界面 stable-diffusion-webui by Automatic1111。它允许我们在无需编程的情况下使用 SD 的所有功能,并且它还是开源的,这意味着任何人都可以下载这个 Web UI 以及 SD 模型,并在任何他们想要的地方进行部署。然而,挑战在于 SD 仍然需要 GPU 功率来运行,否则我们必须等待几分钟才能生成一张图像。而且我们许多人并不拥有足够强大的 GPU 来运行该模型。

多亏了云计算,我们无需花费巨资购买 GPU,而是可以“租用”一个。因此,在本教程中,我们将部署 Automatic1111 Web UI 到配备足够强大 GPU 以运行稳定扩散的 AWS EC2 实例上。我们将通过一个命令使用 AWS CloudFormation 模板来设置所需的所有基础设施。

与往常一样,你可以在我的 GitHub 账户中找到本教程的 代码

为什么这很重要?

拥有稳定扩散模型甚至是 Automatic 的 Web UI 作为开源工具,是民主化先进 AI 工具访问的重要一步。但这还不够,因为运行这些模型所需的 GPU 对大多数消费者来说依然昂贵。运行这些 AI 模型所需的 GPU 价格很容易超过 $2,000。

本教程展示了如何以每小时仅 $0.53 的价格入门,这就是 AWS 上 g4dn.xlarge 实例的按需价格。它允许我们使用带有 16 GB VRAM 的 NVIDIA T4 GPU。这意味着我们可以运行应用程序几个小时以进行试用,并生成我们想要的图像,然后关闭 EC2 实例,不需要支付超过实际使用时间的费用。而且这一切只需点击一个按钮,无需编程或 Linux 经验,这得益于 AWS CloudFormation 模板。

在开始之前需要说明一点:由于我在 AWS 工作,我显然对 AWS 有偏见。但我希望你从本教程中获得的核心信息是,通过云计算,先进的 AI 比以往任何时候都更具可及性和实惠性,无论你最终选择哪个服务提供商。

先决条件

跟随本教程,我们需要一个 AWS 账户,这几乎是唯一的先决条件。在本地机器上安装 AWS 命令行界面 (CLI) 会使事情变得更简单,但我也会演示如何在没有 CLI 的情况下仅使用 AWS 控制台来跟随教程。

快速入门指南

如引言中提到的,我们将使用 CloudFormation (CF) 模板通过一个命令来设置所有内容。启动模板后,应用程序需要 15–20 分钟才会准备好,所以这里有个提示:现在就启动模板,然后返回到这篇博客文章中,深入了解实际发生的背景情况 😉

在 AWS CLI 中启动应用程序

复制 CF 模板(或者,克隆整个 仓库)并在你的本地机器上运行下面的命令。这将会在你的 AWS 账户中创建一个名为“sd-webui-stack”的 CF 堆栈。

在 AWS 控制台中启动应用程序

在 AWS 控制台中,导航到 CloudFormation 部分,选择“创建堆栈 -> 使用新资源”:

作者提供的图片

在接下来的对话框中,选择“模板已准备好”和“上传模板文件”:

作者提供的图片

从仓库中选择 CF 模板文件进行上传,命名堆栈为“sd-webui-stack”,在接下来的对话框中保持默认设置,然后在最后一个对话框中点击“提交”。这将会在你的 AWS 账户中创建一个包含所有所需资源的堆栈。

一探究竟

现在我们已经启动了 CF 模板,可以揭开帷幕,深入了解实际发生的背景情况。

CloudFormation 模板

首先让我们来看一下 CF 模板:

这个模板设置了一些应用程序所需的资源。首先,我们创建一个“安全组”来指定 EC2 实例上哪些端口将会开放。我们选择端口 22,因为我们希望能够通过 SSH 连接到实例,同时选择端口 7860,因为我们的应用将在该端口监听。

接下来我们设置将托管应用程序的 EC2 实例。在这个模板中,我选择了 Ubuntu Server 22.04 LTS 发行版(AMI ID ami-0574da719dca65348),仅仅因为这是我最熟悉的。你可以更改为其他发行版,但请注意你需要相应地修改设置脚本(详见下文)。我们选择了一个 g4dn.xlarge 实例,如上所述。我们还配置了 300 GB 的磁盘空间,以确保有足够的空间来托管多个不同的模型。最后,我们在 EC2 实例上运行设置脚本,我们将在下一节中讨论。

接下来我们创建一个弹性 IP 地址,并将其分配给我们的 EC2 实例。这使我们能够拥有一个永久的 IP 地址,因此即使我们关闭 EC2 实例并在以后重新启动,它的应用程序也将始终托管在同一个 IP 地址上。

设置脚本

如前所述,我们在 EC2 实例上运行一个 setup script,该脚本将执行一些命令以为 Web UI 设置一切。我们一步一步来看。

第一部分禁用 Ubuntu 安装包后的重启对话框,然后安装我们需要的一些包:

下一部分下载并安装 CUDA 驱动程序,以便我们可以访问机器的 GPU:

之后,我们需要安装 Git Large File Storage,因为我们将下载一个大约 5 GB 的 Stable Diffusion 模型:

现在 Git LFS 已安装,我们可以从 Hugging Face Model Hub 下载模型。请注意,我们启用了“skip-smudge”选项,这允许我们仅下载所需的特定文件。在本教程中,我们下载了 SD v2.1(512px 版本),并将其移动到 Web UI 期望模型所在的目录中。

请注意,你可以更改脚本以下载不同版本的 Stable Diffusion,例如版本 1.5。你还可以在后续阶段通过将模型放入模型目录中,向 UI 添加任意多的模型。

除了模型之外,我们还需要一个由 WebUI 读取的配置文件。我们从 Stable Diffusion Github repo 下载一个配置文件,并将其重命名为与模型名称匹配,然后也放入相同的目录中:

最后,我们将 WebUI 的所有权更改为用户 ubuntu,并以该用户身份启动服务器(因为用户 root 不允许启动应用程序):

测试 Web UI

在 15–20 分钟后,部署应该完成。我们可以通过运行以下命令获取 EC2 实例的 IP 地址:

我们也可以通过访问 AWS 控制台中的 EC2 面板来检索它:

图片由作者提供

一旦我们获取了 IP 地址,就可以通过在浏览器中导航到 :7860 打开应用程序(如果请求超时则说明安装尚未完成)。安装完成后,我们可以看到应用程序已经启动运行 🎉

关闭 EC2 实例并重新启动应用程序

即使每小时仅需 $0.53,我们显然也不希望在不使用时运行实例。我们可以在 AWS 控制台中停止实例,并在需要时重新启动它,而不会丢失任何已安装的应用程序。一旦我们重新启动了 EC2 实例,就可以通过 SSH 登录并使用以下命令重新启动应用程序:

删除所有内容

如果我们想要删除所有创建的资源(即安全组、EC2 实例、弹性 IP),我们可以使用以下命令删除 CF 堆栈(或在 AWS 控制台中删除它):

限制

我想强调的是,这个教程只是一个起点,适合任何想尝试通过 Web UI 使用 Stable Diffusion 的人。这个方法有几个限制,如果想在生产环境中使用这个应用程序,可能需要解决这些限制。特别是,我没有涉及任何安全问题(请注意该应用程序运行在 http 上)、扩展问题(如果该应用程序需要同时服务多个用户)以及其他许多方面。

如果我们想在生产环境中使用这个应用程序,可以使用AWS Well-Architected Framework作为起点。

结论

在本教程中,我们利用 CF 模板通过一个命令设置了一个用于 Stable Diffusion 的 Web UI。这使我们能够访问最先进的 AI 模型,而无需自己购买昂贵的硬件。

这个应用程序的待办事项列表中有很多项目,我将它们列在了这里。对这个仓库的任何贡献都非常欢迎☺️

Heiko Hotz

👋 关注我在MediumLinkedIn上的文章,了解更多关于生成 AI、机器学习和自然语言处理的内容。

👥 如果你在伦敦,可以加入我们的NLP London Meetups

🤓 如果你对我如何可能帮助你在组织中采用 AI 和机器学习感兴趣,可以通过aiml.consulting与我联系。

几分钟内免费创建你自己的惊艳网站

原文:towardsdatascience.com/create-your-own-stunning-website-in-minutes-for-free-63f0f7c75bf

无需先前的网页开发经验

Zolzaya LuvsandorjTowards Data Science Zolzaya Luvsandorj

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

--

能够构建简单的网站带来了许多好处。也许你可以将简历发布到网站上以脱颖而出,或者创建自己的博客网站。可能性是无限的。有一种简单、快速且最重要的是免费的方式来托管静态网站,而无需了解之前的网页经验,只需利用预构建的主题。在这篇文章中,我将展示如何做到这一点。

图片来源:Brad Neathery 通过 Unsplash

📍 1. 概述

构建简单、快速且免费的静态网站有两个关键要素:GitHub Pages 用于免费托管静态网站,和 Start Bootstrap’s 免费惊艳主题,可以在几分钟内创建美丽的网站。你只需要一些 git 和 GitHub 的经验以及一个 GitHub 帐户。如果你需要复习 git 的基础知识,这个教程 可能对你有用。

📌 1.1. GitHub Pages

GitHub Pages 允许将公共 GitHub 仓库免费发布到网站上。根据 GitHub 仓库的命名方式,使用 GitHub Pages 托管的网站可以分为两种类型:

  • 用户网站: 每个 GitHub 用户可以拥有一个用户网站。GitHub 仓库应命名为 <username>.github.io,并可以通过 https://<username>.github.io/ 进行访问。

  • 项目网站: 每个 GitHub 用户可以拥有多个项目网站。GitHub 仓库可以命名为除 <username>.github.io 之外的任何名称,并且可以通过 https://<username>.github.io/<repository>/ 进行访问。

在这篇文章中,我们将一起构建一个项目网站作为实际示例。由于用户网站和项目网站之间唯一的区别是代码库名称和网页 URL,一旦你知道如何构建项目网站,构建用户网站就非常简单:你需要做的只是使用正确的代码库名称。

📌 1.2. Start Bootstrap

Start Bootstrap 提供免费的开源 Bootstrap 代码,用于美丽的示例网站。即使没有网页开发经验,我们也可以通过利用 Start Bootstrap 免费主题 背后的源代码,在几分钟内创建一个炫目的网站。

使用预建主题也意味着我们的选择将被限制在这些主题内。虽然我们可以通过这些主题进行快速建站并进行简单的自定义,但值得一提的是,越多的自定义需求就需要更多的网页开发经验和时间。在我们的示例中,我们将选择这些预建主题中的一个并稍作自定义。像用你的内容替换示例内容这样的简单变化,就像在任何软件中替换文本一样简单。

现在,让我们动手实践,边做边学吧!

📍2. 步骤指南

这里是建立网站的三个步骤:

1️⃣ 选择一个免费的 Start Bootstrap 主题,

2️⃣ 自定义它

3️⃣ 推送到 GitHub

听起来很简单?也许吧,因为确实如此。我们现在将详细了解每个步骤。

📌 2.1. 步骤 1:选择一个免费的 Start Bootstrap 主题并克隆代码

首先,我们需要从 Start Bootstrap 选择一个免费的主题。要查看它们:

◾️ 选择顶部面板上的主题,然后

◾️ 选择浏览所有主题或挑选一个所需的主题类别(例如:作品集与简历、博客),然后

◾️ 取消勾选价格下的Pro选项,只查看免费模板。

如果你点击任何主题的启动实时预览,你将看到样本网站的外观。一旦你找到想要使用的主题,点击查看源代码以查看 GitHub 上的代码库。代码库中有几个分支。我们需要克隆gh-pages(用于 GitHub Pages)分支上的代码。

让我们一起完成这个任务。我们将从挑选一个 清爽博客 主题开始,它位于博客类别下。我们将在终端中输入以下代码来克隆 代码库 的 GitHub Pages 分支:

# Clone gh-pages branch
git clone --branch gh-pages https://github.com/StartBootstrap/startbootstrap-clean-blog.git
# Go into the newly cloned directory
cd startbootstrap-clean-blog

图片由作者提供 | 使用的快捷键:Tab 自动完成

如果你愿意,你可以将克隆的仓库重命名为你喜欢的名字。然而,我们在示例中不会进行重命名。

📌 2.2. 第 2 步:自定义代码

现在,我们将对代码进行以下三个小示例更改。

◾️ 更改首页上的图像

◾️ 替换关于页面中的文字

◾️ 从联系页面中删除部分内容

我们会确保每次更改都单独提交,以便提交历史记录清晰。

在自定义代码时,我们将熟悉仓库的文件夹结构。这个结构对于一般的网站非常常见。

📌 2.2.1. 更改首页上的图像 网站使用的图像都保存在assets/img文件夹中。

startbootstrap-clean-blog
└───assets
└───└───img
│   │   │   about-bg.jpg
│   │   │   contact-bg.jpg
│   │   │   home-bg.jpg
│   │   │   post-bg.jpg
│   │   │   post-sample-image.jpg
│   *

如果我们打开home-bg.jpg,我们会发现它是首页顶部使用的图像。我们也可以从它直观的名字猜测,它是页面的背景图像。现在,我们将从 Unsplash 这个提供免费下载美丽高分辨率图像的网站中找一个替代图像。我们选择了 这张图像,它是通过关键词‘planet’找到的。我们将这张新图像命名为home-bg.jpg并用它替换旧图像。完成后,以下是 提交 更改的 git 命令:

# Check status
git status
# Stage new image
git add assets/img/home-bg.jpg
# Check status
git status
# Commit staged change
git commit -m "Change background image on home page"
# Check status
git status

作者提供的图像 | 使用的快捷键:Ctrl + L 清屏

2.2.2. 替换关于页面中的文字 现在,让我们练习用自己的内容替换一些内容。如果我们查看仓库结构,会看到以下 HTML 文件。

startbootstrap-clean-blog
│   about.html
│   contact.html
│   index.html
│   post.html
└───*

如果你双击about.html,它会在默认浏览器中打开。我们可以看到‘about’网页。现在,右键单击文件,用文本编辑器打开它。这是页面内容的源代码。你可以很容易地在源代码中找到网页上显示的文字。尝试更改源代码中看到的一些文字并保存,然后刷新浏览器中显示的文件。你将看到更改。这是感知你所做更改对网页影响的好方法,在提交之前你可以随意尝试文件,因为你总是可以用 Git 命令如git restoregit checkout恢复到最后一次提交的状态。

现在,我们将把“这是我做的事情”替换为“终身成长心态”,并将正文中的三个段落(第 55 至 57 行)替换为“你好,我是 Zolzaya。我喜欢数据科学。”类似地,你可以在任何页面上用自己的内容替换现有内容。然后,我们将 提交 更改:

# Check status
git status
# See changes in about.html
git diff about.html
# Stage new content on about page
git add about.html
# Check status
git status
# Commit staged change
git commit -m "Update content on about page"
# Check status
git status

作者提供的图像 | 使用的快捷键:Q 退出 less

2.2.3. 从联系页面中删除部分

如果您查看联系页面,您会看到有一个需要填写的表单。假设我们要删除这个表单。与之前一样,我们将同时在浏览器和文本编辑器中打开 HTML 文件。

HTML 文档由 HTML 标签组成。每个 HTML 标签通常都有一个开标签:<tag>和闭标签:</tag>,内容放在这些标签之间。因此,在删除这些标签时,确保删除整个标签部分。例如,如果您想删除以<div>…开头的部分,请确保将内容一直删除到相应的结束</div>标签。

您将看到表单位于第 56–107 行。我们将删除表单,然后将其上方的段落替换为“想要联系我?请随时通过 test@test.com 发邮件给我。”然后,我们将提交更改:

# Check status
git status
# See changes in about.html
git diff contact.html
# Stage new content on contact page
git add contact.html
# Check status
git status
# Commit staged change
git commit -m "Remove form from contact page"
# Check status
git status

作者提供的图片

我们已经完成了示例自定义,因此我们准备将更改推送到 GitHub。

📌 2.3. 第三步:推送到 GitHub,如有必要,请在 GitHub 中进行配置

让我们在 GitHub 上创建一个与您的本地仓库名称匹配的新公共仓库。在我们的示例中,它将是[startbootstrap-clean-blog](https://github.com/zluvsand/startbootstrap-clean-blog)

作者提供的图片

仓库必须是公开的才能免费托管。

现在,我们将更新远程仓库的 URL,以便它指向新创建的仓库。然后我们将代码推送到远程仓库:

# Check remote
git remote -v
# Change remote
git remote set-url origin https://github.com/zluvsand/startbootstrap-clean-blog.git
# Check remote
git remote -v
# Push code to new remote
git push

作者提供的图片 | 使用的快捷键:向上箭头访问之前的命令

现在,打开 GitHub 上的仓库。转到设置选项卡,然后从左侧面板中选择Pages

作者提供的图片

您的网站将在推送代码后几分钟内从gh-pages分支中的代码自动构建。如果没有发生这种情况,或者您可能将分支命名为其他名称,则可以在 GitHub Pages 部分的分支子部分中指定。

作者提供的图片 | 突出显示的分支子部分

我们的示例仓库可以在这里找到,网站托管在这里

作者提供的图片 | 来源:zluvsand.github.io/startbootstrap-clean-blog/

完成了,这就是全部内容!我们刚刚使用 GitHub Pages 和 Startbootstrap 主题一起构建了一个网站!显然,关于网页开发还有很多可以学习的内容。了解一些 HTML 和 CSS 会非常有用。如果你想学习 HTML 和 CSS 的基础知识,这个是 Udacity 提供的一个优秀的免费课程。如果你希望深入了解 GitHub Pages,可以查看GitHub Pages 文档

感谢阅读我的文章。希望你获得了有关如何利用免费资源构建网站的有用知识。如果你感兴趣,以下是一些你可能也喜欢的帖子链接:

◼️ 数据科学中的 Git 入门

◼️ 通过这些技巧丰富你的 GitHub 个人资料

再见啦 🏃💨

使用 LLaVA 创建你的视觉聊天助手

原文:towardsdatascience.com/create-your-vision-chat-assistant-with-llava-610b02c3283e?source=collection_archive---------3-----------------------#2023-11-11

开始使用开源的 LLaVA 模型来创建多模态对话模型。

Gabriele Sgroi, PhDTowards Data Science Gabriele Sgroi, PhD

·

关注 发布在Towards Data Science ·17 分钟阅读·2023 年 11 月 11 日

--

照片由Izabela KrausUnsplash上拍摄。

简介

大语言模型已经证明自己是一项革命性的技术。利用其能力的众多应用已经被开发出来,预计还会有更多的应用出现。大语言模型的一个最有趣的应用是将其作为智能助手,能够在各种任务中帮助用户。经过指令调优和从人类反馈中学习的聊天模型显示出很有前景的能力,能够遵循人类指令并完成指定任务。然而,它们在仅语言任务的适用性上存在局限。

多模态对话模型旨在释放大语言模型的潜力,以解决需要将自然语言与其他模态结合的问题。特别是,自从将视觉能力引入 GPT-4V 以来,视觉语言模型受到了越来越多的关注。赋予 GPT-4 的自然语言能力以图像理解,已导致一个强大的聊天助手,可以帮助用户处理需要视觉和语言理解的任务。尽管 GPT-4V 的视觉能力令人印象深刻,但闭源模型限制了对这一令人惊叹的技术进行研究和实验的潜力。幸运的是,一些开源模型出现了,将视觉语言模型的力量以易于访问和透明的方式带给了社区。这些模型还延续了对计算和内存效率的关注趋势,这一趋势在开源大语言模型中已经显现。这是一个重要的特性,因为它促进了这些模型的广泛应用。

在本教程中,我将介绍如何使用在Visual Instruction Tuning论文中介绍的 LLaVA(大语言与视觉助手)模型创建一个视觉聊天助手。我将首先简要介绍 LLaVA 模型及其改进,然后讨论使用官方代码库中提供的代码实现一个简单的视觉聊天助手。我还将展示一些我设计的示例,以展示该模型的能力和局限性。

LLaVA

LLaVA 模型首次在论文视觉指令调整中提出,随后在改进的基线与视觉指令调整中进一步改进(也称为 LLaVA-1.5)。其背后的理念是从图像中提取视觉嵌入,并像处理语言标记生成答案一样处理它们,通过将它们馈送给大型语言模型。直观地说,我们可以认为图像将用语言模型生成答案所需的“单词”。为了选择正确的“单词”,模型使用预训练的 CLIP 视觉编码器提取视觉嵌入,然后将其投影到语言模型的单词嵌入空间中。后者的操作是通过一个视觉语言连接器完成的,最初在第一篇论文视觉指令调整中选择为简单的线性层,后来在改进的基线与视觉指令中替换为更具表现力的多层感知器(MLP)。模型的架构如下所示。

LLaVA 模型的架构。在 LLaVA 中,投影 W 是一个简单的线性层,而在 LLaVA-1.5 中是一个 MLP。图片来源于论文视觉指令调整

该方法的优势之一在于通过使用预训练的视觉编码器和预训练语言模型,只需学习轻量级模块——视觉语言连接器,从头开始。具体而言,LLaVA 的训练包括两个阶段:

  • 特征对齐的预训练:冻结预训练的视觉编码器和语言模型的权重,仅更新视觉语言连接器的权重。所有训练样本由文本-图像对打包成单回合对话。此阶段旨在训练视觉语言连接器将视觉编码器的嵌入与语言模型的文本嵌入对齐。

  • 使用视觉指令进行微调:在此阶段,仅冻结视觉编码器的权重,同时对视觉语言连接器和语言模型进行微调。模型在基于图像的指令跟随任务上进行微调。有趣的是,一些数据是通过使用仅包含语言的 GPT4 从图像的标题和实体边界框的坐标创建指令跟随样本。

视觉聊天机器人实现

使用官方存储库中提供的代码创建视觉聊天机器人非常简单。该存储库还提供了标准化的聊天模板,可用于解析正确格式的输入。遵循训练中使用的正确格式对生成的答案质量至关重要。确切的模板取决于使用的语言模型。使用预训练的 Vicuna 语言模型的 LLaVA-1.5 的模板如下:

A chat between a curious user and an artificial intelligence assistant. The 
assistant gives helpful, detailed, and polite answers to the user's questions. 

USER: <im_start><image><im_end> User's prompt

ASSISTANT: Assistant answer

USER: Another prompt

前几行是模型使用的一般系统提示。特殊标记<im_start>、和<im_end>用于指示将放置表示图像的嵌入的位置。

聊天机器人可以在一个简单的 Python 类中定义。

class LLaVAChatBot:
    def __init__(self,
                 model_path: str = 'liuhaotian/llava-v1.5-7b',
                 device_map: str = 'auto',
                 load_in_8_bit: bool = True,
                 **quant_kwargs) -> None:
        self.model = None
        self.tokenizer = None
        self.image_processor = None
        self.conv = None
        self.conv_img = None
        self.img_tensor = None
        self.roles = None
        self.stop_key = None
        self.load_models(model_path,
                         device_map=device_map,
                         load_in_8_bit=load_in_8_bit,
                         **quant_kwargs)

    def load_models(self, model_path: str,
                    device_map: str,
                    load_in_8_bit: bool,
                    **quant_kwargs) -> None:
        """Load the model, processor and tokenizer."""
        quant_cfg = BitsAndBytesConfig(**quant_kwargs)
        self.model = LlavaLlamaForCausalLM.from_pretrained(model_path,
                                                           low_cpu_mem_usage=True,
                                                           device_map=device_map,
                                                           load_in_8bit=load_in_8_bit,
                                                           quantization_config=quant_cfg)
        self.tokenizer = AutoTokenizer.from_pretrained(model_path,
                                                       use_fast=False)
        vision_tower = self.model.get_vision_tower()
        vision_tower.load_model()
        vision_tower.to(device='cuda')
        self.image_processor = vision_tower.image_processor
        disable_torch_init()

    def setup_image(self, img_path: str) -> None:
        """Load and process the image."""
        if img_path.startswith('http') or img_path.startswith('https'):
            response = requests.get(img_path)
            self.conv_img = Image.open(BytesIO(response.content)).convert('RGB')
        else:
            self.conv_img = Image.open(img_path).convert('RGB')
        self.img_tensor = self.image_processor.preprocess(self.conv_img,
                                                          return_tensors='pt'
                                                          )['pixel_values'].half().cuda()

    def generate_answer(self, **kwargs) -> str:
        """Generate an answer from the current conversation."""
        raw_prompt = self.conv.get_prompt()
        input_ids = tokenizer_image_token(raw_prompt,
                                          self.tokenizer,
                                          IMAGE_TOKEN_INDEX,
                                          return_tensors='pt').unsqueeze(0).cuda()
        stopping = KeywordsStoppingCriteria([self.stop_key],
                                            self.tokenizer,
                                            input_ids)
        with torch.inference_mode():
            output_ids = self.model.generate(input_ids,
                                             images=self.img_tensor,
                                             stopping_criteria=[stopping],
                                             **kwargs)
        outputs = self.tokenizer.decode(
            output_ids[0, input_ids.shape[1]:]
        ).strip()
        self.conv.messages[-1][-1] = outputs

        return outputs.rsplit('</s>', 1)[0]

    def get_conv_text(self) -> str:
        """Return full conversation text."""
        return self.conv.get_prompt()

    def start_new_chat(self,
                       img_path: str,
                       prompt: str,
                       do_sample=True,
                       temperature=0.2,
                       max_new_tokens=1024,
                       use_cache=True,
                       **kwargs) -> str:
        """Start a new chat with a new image."""
        conv_mode = "v1"
        self.setup_image(img_path)
        self.conv = conv_templates[conv_mode].copy()
        self.roles = self.conv.roles
        first_input = (DEFAULT_IM_START_TOKEN + DEFAULT_IMAGE_TOKEN +
                       DEFAULT_IM_END_TOKEN + '\n' + prompt)  # f"{self.roles[0]}: {prompt}")
        self.conv.append_message(self.roles[0], first_input)
        self.conv.append_message(self.roles[1], None)
        if self.conv.sep_style == SeparatorStyle.TWO:
            self.stop_key = self.conv.sep2
        else:
            self.stop_key = self.conv.sep
        answer = self.generate_answer(do_sample=do_sample,
                                      temperature=temperature,
                                      max_new_tokens=max_new_tokens,
                                      use_cache=use_cache,
                                      **kwargs)
        return answer

    def continue_chat(self,
                      prompt: str,
                      do_sample=True,
                      temperature=0.2,
                      max_new_tokens=1024,
                      use_cache=True,
                      **kwargs) -> str:
        """Continue the existing chat."""
        if self.conv is None:
            raise RuntimeError("No existing conversation found. Start a new"
                               "conversation using the `start_new_chat` method.")
        self.conv.append_message(self.roles[0], prompt)
        self.conv.append_message(self.roles[1], None)
        answer = self.generate_answer(do_sample=do_sample,
                                      temperature=temperature,
                                      max_new_tokens=max_new_tokens,
                                      use_cache=use_cache,
                                      **kwargs)
        return answer

如果您熟悉transformers库,您将会认识到许多常见功能,并且执行的操作应该很容易理解。让我们快速浏览上面定义的 LLaVAChatBot 类的方法。

  • load_models: 该方法加载语言模型、分词器和图像处理器,使用 BitsAndBytes 库进行量化,以减少 GPU 内存需求。该代码阴影了 Hugging Face transformers 模型使用的 from_pretrained 方法。BitsAndBytes 允许将模型量化为 8 位或 4 位。

  • setup_image: 它从本地路径或 URL 加载图像,并使用图像处理器将其转换为张量。

  • generate_answer: 该方法返回模型关于所提供图像当前对话的答案。LLaVa 模型的 generate 方法类似于 Hugging Face transformers 模型的 generate 方法。

  • get_conv_text: 该方法返回到目前为止对话的原始文本。

  • start_new_chat: 这是聊天机器人的两个主要方法之一,用于与模型开始新的聊天。它创建一个新的对话,给定图像和初始提示传递给模型。它通过使用存储库中定义的模板设置对话,遵循前面部分讨论的格式。

  • continue_chat: 另一个主要方法,它继续关于图像的现有对话。

我在此 Colab 笔记本中提供了完整的代码,这样你就可以自己尝试。

示例

在本节中,我收集了在测试模型能力时获得的一些有趣示例。本文中的所有示例都是使用 8 位量化的模型 llava-v1.5–7b 创建的。

首先,让我们看看该模型描述和理解图像的能力。下面我要求模型描述一张描绘白老虎的图片。

照片由Joshua J. Cotten提供,来自Unsplash

A chat between a curious user and an artificial intelligence assistant. The 
assistant gives helpful, detailed, and polite answers to the user's questions. 

USER: <im_start><image><im_end>
Describe the image in details. 

ASSISTANT: The image features a white tiger laying down on a rocky surface, 
possibly a stone wall or a rock. The tiger appears to be relaxed and 
comfortable in its environment. The tiger's position is such that it covers a 
significant portion of the image, from the left side to almost the entire right
side. The rocky surface provides a natural and rugged backdrop for the scene.</s></s>

USER: What is unique about this tiger? 

ASSISTANT: This tiger is unique because it is a white tiger, which is an 
extremely rare and visually striking variation of the species. White tigers are
born with a genetic mutation that causes their fur to be predominantly white, 
with the rest of their body being a mix of orange and black stripes. This 
distinctive appearance sets them apart from the more common orange and black 
striped tigers.</s></s>

模型似乎能够详细描述图像,并且能够推理图像的主题,正确评估白老虎是一种罕见的情况。

接下来,我测试了模型是否能够为用户提供执行各种任务的可操作指导。这是区分“字幕生成”模型和真正的视觉对话助手的关键点。例如,模型能否根据图像中存在的成分提供食谱?结果如下报告。

照片由ThermoPro提供,来自Unsplash

A chat between a curious user and an artificial intelligence assistant. The 
assistant gives helpful, detailed, and polite answers to the user's questions. 
USER: <im_start><image><im_end>
What can I cook with these ingredients? 

ASSISTANT: With the ingredients displayed on the table, you can prepare a 
delicious roast dinner. The meat, which is a large piece of beef, can be 
seasoned and cooked in the oven, while the vegetables, including carrots, 
potatoes, and onions, can be roasted alongside the meat. You can also prepare a
side dish with the potatoes, carrots, and onions, which can be served alongside
the roast dinner. The presence of a bowl of sauce suggests that you can use it 
as a dipping sauce or to enhance the flavors of the roast and vegetables. 
Overall, this combination of ingredients offers a hearty and flavorful meal 
that can be enjoyed with family and friends.</s></s>

USER: Could the recipe you suggested be included in a vegetarian diet? 

ASSISTANT: The recipe I suggested, which involves roasting a large piece of 
beef and vegetables, is not suitable for a vegetarian diet. The beef is the 
main protein source in the dish, and it is not suitable for vegetarians who 
avoid consuming meat. However, you can still create a vegetarian-friendly meal 
using the vegetables and sauce availableon the table. For example, you can 
prepare a vegetable roast with the carrots, potatoes, and onions, and serve it 
alongside a side of mashed potatoes or a vegetable-based sauce. This way, you 
can still enjoy the flavors and textures of the dish without including meat.</s></s>

USER: Can you suggest a vegetarian recipe with minimal modifications of the 
depicted ingredients? 

ASSISTANT: Certainly! Here's a vegetarian recipe that can be made using the 
ingredients available on the table:

Roasted Vegetable Medley with Sauce

Ingredients:

* Carrots
* Potatoes
* Onions
* Olive oil
* Salt
* Pepper
* Herbs (such as thyme, rosemary, or parsley)
* Sauce (optional)

Instructions:

1\. Preheat your oven to 400°F (200°C).
2\. Wash and peel the carrots, potatoes, and onions. Cut them into bite-sized 
pieces.
3\. In a large bowl, toss the vegetables with olive oil, salt, and pepper.
4\. Spread the vegetables evenly on a baking sheet lined with parchment paper.
5\. Roast the vegetables in the preheated oven for 20-25 minutes, or until they 
are tender and slightly browned.
6\. If desired, prepare a sauce by combining ingredients such as tomatoes, 
garlic, and herbs. You can use this sauce as a dipping sauce or to enhance the
flavors of the roasted vegetables.
7\. Serve the roasted vegetable medley alongside a side of mashed potatoes or a 
vegetable-based sauce.

This vegetarian recipe allows you to enjoy the flavors and textures of the 
roasted vegetables without including meat. The sauce can be easily customized 
to suit your taste preferences, and the dish can be served alongside a variety 
of side dishes, such as mashed potatoes or a vegetable-based sauce.</s></s>

模型似乎对图像中存在的成分有很好的理解,并且能够将单个词语连接到更抽象的概念(即肉类餐品不是素食)。

现在让我们看看模型如何用于获得建议。这是在头脑风暴时获取新想法的有用功能,并且可以为用户提供真正的价值。在下面的对话中,我请求模型提供关于改变咖啡馆外观的建议。

照片由Michał Mancewicz提供,来自Unsplash

A chat between a curious user and an artificial intelligence assistant. The 
assistant gives helpful, detailed, and polite answers to the user's questions. 

USER: <im_start><image><im_end>
How can I renovate this café to make it look more vintage? 

ASSISTANT: To make the café look more vintage, you can consider the following 
changes:

1\. Update the lighting: Replace the current lighting with vintage-style lamps 
or lanterns, which will create a warm and cozy atmosphere.

2\. Add vintage decorations: Incorporate antique items, such as vintage clocks, 
artwork, or decorative plates, to create a nostalgic ambiance.

3\. Use vintage furniture: Replace the current stools with vintage-style chairs 
or barstools, which can add character and charm to the space.

4\. Incorporate greenery: Add more greenery, such as potted plants or vines, to 
create a lush and inviting atmosphere.

5\. Update the bar: Replace the current bar with a vintage-style bar, complete 
with a vintage-inspired backsplash and countertop.

6\. Use vintage-style glassware: Replace the current glassware with vintage-style
glasses, such as wine glasses, beer mugs, or cocktail shakers.
7\. Add vintage-style signage: Replace the current signage with vintage-style 
menu boards or chalkboards, which can add a touch of nostalgia to the space.

8\. Use vintage-style wallpaper or paint: Apply a vintage-style wallpaper or 
paint to the walls, which can create a unique and charming atmosphere.

By implementing these changes, the café will appear more vintage and inviting, 
creating a memorable experience for its patrons.</s></s>

建议看起来切合要点,尽管相当通用,识别出可以更改的当前元素,以赋予更复古的风格。

让我们继续实际的内容,可以帮助用户在日常活动中获得帮助。模型是否能够帮助撰写广告或列表?

照片由Eugene Chystiakov提供,来自Unsplash

A chat between a curious user and an artificial intelligence assistant. The 
assistant gives helpful, detailed, and polite answers to the user's questions. 

USER: <im_start><image><im_end>
Write an advertisment for this chair 

ASSISTANT: Introducing the modern and stylish black and wood chair, perfect for
any contemporary setting. This chair features a sleek black seat and a wooden 
base, creating a striking contrast against any background. Its minimalist 
design makes it a versatile addition to any room, whether it's used as a dining
chair, a side table, or a statement piece in a living space. The chair's sturdy
construction ensures it can withstand daily use, while its clean lines and 
sleek appearance add a touch of sophistication to any room. Upgrade your living
space with this stylish and functional chair today.</s></s>

USER: Write now an advertisement for the same chair but targeting cafes 

ASSISTANT: Attention café owners and design enthusiasts! Introducing the black
and wood chair, the perfect addition to your café's seating area. This stylish
and functional chair offers a modern touch to your establishment, with its 
sleek black seat and wooden base creating a striking contrast against any 
background. Its minimalist design makes it a versatile choice for any café 
setting, whether it's used as a dining chair, a side table, or a statement 
piece in a lounge area. The chair's sturdy construction ensures it can 
withstand daily use, while its clean lines and sleek appearance add a touch of 
sophistication to any room. Upgrade your café's ambiance with this stylish and 
functional chair today.</s></s>

模型能够准确指出椅子的特征,并且能够创建吸引人的广告,当要求时正确地定制答案以适应不同的目标。

作为最后的测试,让我们看看是否可以像大型语言模型那样使用已被证明对 LLaVA 有益的提示技术。以下是尝试零点启发的链式思维提示的示例。

照片由Arnold Middelkoop提供,来自Unsplash

A chat between a curious user and an artificial intelligence assistant. The 
assistant gives helpful, detailed, and polite answers to the user's questions. 

USER: <im_start><image><im_end>
Would the fish depicted in the image float if thrown in water? Think step by 
step. 

ASSISTANT: The fish depicted in the image is a large white fish sculpture or 
statue, which is lying on the ground on a brick walkway near a body of water. 
If thrown into the water, the fish would not float because it is a solid, 
inanimate object. It would sink to the bottom of the water, as it is not 
buoyant due to its composition and size. The fish's position on the ground and 
its lack of buoyancy make it impossible for it to float in water.</s></s>

USER: What material could be used to make a fish sculpture that floats on 
water? 

ASSISTANT: To make a fish sculpture that floats on water, one could use a 
lightweight, buoyant material such as expanded polystyrene foam or polyethylene
foam. These materials are commonly used for making buoyant objects like life 
jackets, flotation devices, and fishing floats. By shaping the foam into the 
desired fish form, it would be able to float on the water's surface, 
simulating the behavior of a real fish.</s></s>

该模型能够解释为什么鱼不会浮起,正确地识别出它不是真正的鱼。它还遵循了在给出最终答案之前逐步思考的指示。作为比较,这是一个没有“逐步思考”提示的对话示例。

A chat between a curious user and an artificial intelligence assistant. The 
assistant gives helpful, detailed, and polite answers to the user's questions.

USER: <im_start><image><im_end>
Would the fish depicted in the image float if thrown in water? 

ASSISTANT: No, the fish depicted in the image would not float if thrown in 
water. It is a large, white, stone fish sculpture, which is not a real fish 
and does not have the buoyancy properties of a natural fish.</s></s>

虽然最终答案仍然是正确的,但在这种情况下给出的解释要少得多,并且答案是在句子开头给出的。由于 LLaVA 使用的生成语言模型是因果的,这意味着提供的答案不依赖于后续的解释。进行更广泛的实验将很有趣,以测试 Chain of Thought(CoT)是否通常能够改善视觉语言模型在复杂推理任务中的表现,类似于观察到的大型语言模型在Chain-of-Thought Prompting Elicits Reasoning in Large Language ModelsLarge Language Models are Zero-Shot Reasoners中所述的情况。

限制

尽管非常强大,LLaVA 也有其局限性。例如,该模型已经训练成每次只使用一张图片,因此无法处理需要与多张图片交互的更复杂对话。然而,值得注意的是,这种限制并非模型架构固有的,可以通过在训练期间包含多图片对话数据来修正。

在测试中,我注意到该模型(至少是 1.5 7b 模型的 8 位量化版本)在计算图像中的元素数量以及处理数值任务方面存在困难。以下是一个例子。

照片由John Matychuk拍摄,来自Unsplash

A chat between a curious user and an artificial intelligence assistant. The 
assistant gives helpful, detailed, and polite answers to the user's questions. 

USER: <im_start><image><im_end>
How many car are inside the parking lot? 

ASSISTANT: There are several cars parked inside the parking lot.</s></s>

USER: List the numbers of the parking spots occupied by cars 

ASSISTANT: The parking spots occupied by cars are 66, 67, 68, 69, 70, and 71.</s></s>

像所有大型语言模型一样,该模型也容易产生幻觉,并且可以通过适当的提示轻易地被欺骗而犯错。例如,如果提供了沙漠的图片,并问这张图片中的鱼是哪种物种,它会产生沙漠鱼的幻觉。

照片由Wolfgang Hasselmann拍摄,来自Unsplash

A chat between a curious user and an artificial intelligence assistant. The 
assistant gives helpful, detailed, and polite answers to the user's questions. 

USER: <im_start><image><im_end>
Tell me the species of the fish in this image 

ASSISTANT: The fish in this image is a desert fish, which is a species adapted 
to living in desert environments.</s></s>

结论

LLaVA 在视觉语言理解方面显示出令人印象深刻的能力。这标志着多模式开源视觉语言模型的明显进步。LLaVA 最大的优势之一是它易于训练和微调。例如,LLaVA 1.5 13b 的完整训练仅使用了 1.2M 数据,在单个 8-A100 节点上大约耗时 1 天。这使其适合在特定领域进行微调,以获得专家助理,例如在LLaVA-Med: Training a Large Language-and-Vision Assistant for Biomedicine in One Day中所做的那样。

为聊天助手添加视觉能力扩展了此类模型的应用领域,使其革命性潜力能够应用于更复杂和更细致的任务。将图像特征视为语言标记也带来了使用所有先进提示技术的可能性,这些技术通常用于纯文本语言模型,并进一步扩展了这些技术。例如,可以通过检索与对话相关的文本和图像来扩展检索增强生成的能力。实际上,利用 CLIP 的共享图像文本嵌入空间,可以通过输入文本或图片来检索外部文档和外部图像!

扩展模型能力的另一个有趣方向可以在LLaVA-Interactive: An All-in-One Demo for Image Chat, Segmentation, Generation and Editing中找到。主要思路是结合视觉语言聊天模型、文本生成图像模型以及其他视觉模型(如图像分割模型)的各种能力,以获得一个能够处理多模态输入并生成多模态输出的助手。

总之,LLaVA 为开源多模态生成模型标志着一个重要的步骤,这些模型展现了令人印象深刻的能力,并引起了广泛关注。随着开源模型的更广泛采用,我相信我们很快将见证这些强大模型的新应用的迅速增长。

感谢阅读!如果你想亲自尝试代码,可以查看这个 Colab 笔记本

从 RGB 视频创建 3D 视频

原文:towardsdatascience.com/creating-3d-videos-from-rgb-videos-491a09fa1e79?source=collection_archive---------3-----------------------#2023-08-03

生成一致的深度图和点云视频的指南

Berkan ZorlubasTowards Data Science Berkan Zorlubas

·

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

--

作者提供的图片(视频帧的编辑版本,来自 库存镜头 提供者 Videvo,下载自 www.videvo.net

我一直对我们将数字记忆以 2D 格式存档这一事实感到不满——尽管照片和视频很清晰,却缺乏它们所捕捉的经历的深度和沉浸感。在机器学习模型足够强大到理解照片和视频的 3D 感的时代,这似乎是一个任意的限制。

来自图像或视频的 3D 数据不仅使我们能够更生动、互动地体验记忆,还提供了编辑和后处理的新可能性。试想能够轻松地从场景中移除对象、更换背景,甚至改变视角以从新的视点查看一个瞬间。深度感知处理也为机器学习算法提供了更丰富的上下文来理解和处理视觉数据。

在寻找生成一致深度视频的方法时,我发现了一篇研究论文,它建议了一种很好的方法。这种方法涉及使用整个输入视频训练两个神经网络:一个卷积神经网络(CNN)来预测深度,一个 MLP 来预测场景中的运动或“场景流”。这个流预测网络以特殊的方式应用,在不同时间段内重复应用。这使它能够识别场景中的小变化和大变化。小变化有助于确保 3D 中的运动从一个时刻到下一个时刻是平滑的,而大变化有助于确保整个视频在不同视角下是一致的。这样,我们可以创建既局部又全球准确的 3D 视频。

这篇论文的代码库是公开的,但处理任意视频的管道并没有完全解释,至少对我来说,如何使用所提议的管道处理任何视频仍然不清楚。在这篇博客文章中,我将尝试填补这个空白,并逐步介绍如何在你的视频上使用这个管道。

你可以查看我在GitHub页面上的代码版本,我将会参考这个版本。

第 1 步:从视频中提取帧

在管道中的第一步是从选择的视频中提取帧。我添加了一个脚本用于这个目的,你可以在scripts/preprocess/custom/extract_frames_from_video.py中找到。要运行代码,只需在终端中使用以下命令:

python extract_frames_from_video.py ^
  -- video_path = 'ENTER YOUR VIDEO PATH HERE' ^
  -- output_dir = '../../../datafiles/custom/JPEGImages/640p/custom/' ^
  -- resize_factor = 0.5

使用resize_factor参数,你可以对帧进行下采样或上采样。

我选择了这个视频进行测试。最初,它的分辨率是 1280x720,但为了加快后续步骤的处理速度,我将其缩小到 640x360,使用了 0.5 的resize_factor

第 2 步:在视频中分割前景对象

我们过程中的下一步需要对视频中的一个主要前景物体进行分割或隔离,这对估计相机在视频中的位置和角度至关重要。原因是?离相机较近的物体对姿态估计的影响比离相机较远的物体要大。举个例子,想象一个距离 1 米远的物体移动 10 厘米——这将导致图像发生较大的变化,可能是几十个像素。但如果同样的物体距离 10 米远且移动相同的距离,图像变化则不那么明显。因此,我们生成了一个‘遮罩’视频,以便关注对姿态估计相关的区域,简化我们的计算。

我偏好使用Mask-RCNN来分割帧。你也可以使用其他你喜欢的分割模型。对于我的视频,我决定对右侧人物进行分割,因为他在整个视频中都出现在画面中,并且看起来离相机足够近。

要生成遮罩视频,需要对你的视频进行一些特定的手动调整。由于我的视频中包含两个人物,我首先对这两个人物进行了遮罩分割。之后,我通过硬编码提取了右侧人物的遮罩。根据你选择的前景物体及其在视频中的位置,你的方法可能会有所不同。负责创建遮罩的脚本可以在./render_mask_video.py中找到。我指定遮罩选择过程的脚本部分如下:

 file_names = next(os.walk(IMAGE_DIR))[2]
    for index in tqdm(range(0, len(file_names))):
        image = skimage.io.imread(os.path.join(IMAGE_DIR, file_names[index]))
        # Run detection
        results = model.detect([image], verbose=0)
        r = results[0]
        # In the next for loop, I check if extracted frame is larger than 16000 pixels, 
        # and if it is located minimum at 250th pixel in horizontal axis.
        # If not, I checked the next mask with "person" mask.
        current_mask_selection = 0
        while(True):
            if current_mask_selection<10:
                if (np.where(r["masks"][:,:,current_mask_selection]*1 == 1)[1].min()<250 or 
                    np.sum(r["masks"][:,:,current_mask_selection]*1)<16000):
                    current_mask_selection = current_mask_selection+1
                    continue
                elif (np.sum(r["masks"][:,:,current_mask_selection]*1)>16000 and 
                      np.where(r["masks"][:,:,current_mask_selection]*1 == 1)[1].min()>250):
                    break
            else:
                break
        mask = 255*(r["masks"][:,:,current_mask_selection]*1)
        mask_img = Image.fromarray(mask)
        mask_img = mask_img.convert('RGB')
        mask_img.save(os.path.join(SAVE_DIR, f"frame{index:03}.png"))

原始视频和遮罩视频在以下动画中并排显示:

(左) 由 Videvo 提供的库存视频,从www.videvo.net下载 | (右) 作者创建的遮罩视频

步骤 3: 估计相机姿态和内部参数

创建遮罩帧后,我们现在开始计算相机姿态和内部估计。为此,我们使用一个叫做Colmap的工具。它是一个多视图立体视觉工具,可以从多个图像创建网格,并估计相机的移动和内部参数。它既有图形用户界面,也有命令行界面。你可以从这个链接下载该工具。

启动工具后,你需要点击顶部栏上的“重建”(见下图),然后选择“自动重建”。在弹出窗口中,

  • 进入./datafiles/custom/triangulation到“工作空间文件夹”

  • 进入./datafiles/custom/JPEGImages/640p/custom到“图像文件夹”

  • 进入./datafiles/custom/JPEGImages/640p/custom到“图像文件夹”

  • 进入./datafiles/custom/Annotations/640p/custom到“遮罩文件夹”

  • 勾选“共享内部参数”选项

  • 点击“运行”。

计算可能需要一些时间,具体取决于你有多少图像以及图像的分辨率。计算完成后,点击“文件”下的“将模型导出为文本”并将输出文件保存在./datafiles/custom/triangulation中。这将创建两个文本文件和一个网格文件(.ply)。

Colmap 的说明 — 图片由作者提供

这一步还没有结束,我们需要处理 Colmap 的输出。我编写了一个脚本来自动化这一过程。只需在终端中运行以下命令:

python scripts/preprocess/custom/process_colmap_output.py

它将创建“custom.intrinsics.txt”、“custom.matrices.txt”和“custom.obj”。

Colmap 的输出文件 — 图片由作者提供

现在我们准备好进行训练的数据集生成。

步骤 4:为训练准备数据集

训练需要一个数据集,该数据集包括每帧的深度估计由MiDas提供,相应的跨帧光流估计和深度序列。创建这些的脚本在原始仓库中提供,我只是更改了其中的输入和输出目录。通过运行下面的命令,将创建所有所需的文件并放置在适当的目录中:

python scripts/preprocess/custom/generate_frame_midas.py  &
python scripts/preprocess/custom/generate_flows.py  &
python scripts/preprocess/custom/generate_sequence_midas.py 

在训练之前,请检查datafiles/custom_processed/frames_midas/customdatafiles/custom_processed/flow_pairs/customdatafiles/custom_processed/sequences_select_pairs_midas/custom中是否存在 .npz 和 .pt 文件。验证后,我们可以继续进行训练。

步骤 5:训练

训练部分很简单。要用自定义数据集训练神经网络,只需在终端中运行以下命令:

python train.py --net scene_flow_motion_field ^
 --dataset custom_sequence --track_id custom ^
 --log_time  --epoch_batches 2000 --epoch 10 ^
 --lr 1e-6 --html_logger --vali_batches 150  ^
 --batch_size 1 --optim adam --vis_batches_vali 1 ^
 --vis_every_vali 1 --vis_every_train 1 ^
 --vis_batches_train 1 --vis_at_start --gpu 0 ^
 --save_net 1 --workers 1 --one_way ^
 --loss_type l1 --l1_mul 0 --acc_mul 1 ^
 --disp_mul 1 --warm_sf 5 --scene_lr_mul 1000 ^
 --repeat 1 --flow_mul 1 --sf_mag_div 100 ^
 --time_dependent --gaps 1,2,4,6,8 --midas ^
 --use_disp --logdir 'logdir/' ^
 --suffix 'track_{track_id}' ^
 --force_overwrite

经过 10 次迭代的神经网络训练后,我观察到损失开始饱和,因此决定不再继续训练更多的迭代。以下是我的训练损失曲线图:

损失 vs. 迭代曲线 — 图片由作者提供

在训练过程中,所有检查点都保存在目录./logdir/nets/中。此外,每次迭代后,训练脚本会在目录./logdir/visualize中生成测试可视化。这些可视化可以特别有助于识别训练过程中可能出现的任何潜在问题,除了监控损失外。

步骤 6:使用训练好的模型创建每帧的深度图

使用最新的检查点,我们现在用test.py脚本生成每帧的深度图。只需在终端中运行以下命令:

python test.py --net scene_flow_motion_field ^
 --dataset custom_sequence --workers 1 ^
 --output_dir .\test_results\custom_sequence ^
 --epoch 10 --html_logger --batch_size 1 ^
 --gpu 0 --track_id custom --suffix custom ^
 --checkpoint_path .\logdir

这将为每一帧生成一个 .npz 文件(一个包含 RGB 帧、深度、相机姿态、流向下一张图像等的字典文件),以及每一帧的三个深度渲染(真实值、MiDaS 和训练网络的估计)。

步骤 7:创建点云视频

在最后一步,我们逐帧加载批处理的 .npz 文件,并利用深度和 RGB 信息创建彩色点云。我使用 open3d 库在 Python 中创建和渲染点云。这是一个强大的工具,你可以在 3D 空间中创建虚拟相机并用它们捕捉点云。你还可以编辑/操作点云;我应用了 open3d 内置的异常点移除功能来去除闪烁和噪声点。

虽然我不会详细讨论我使用 open3d 的具体细节,以保持本文的简洁性,但我已包含了脚本 render_pointcloud_video.py,该脚本应当易于理解。如果你有任何问题或需要进一步澄清,请随时问我。

这是我处理的视频的点云和深度图像的视频效果。

(左) 视频素材由 Videvo 提供,下载自 www.videvo.net | (右) 作者制作的深度图视频 | (下) 作者制作的彩色点云视频

此动画的高分辨率版本已上传至 YouTube

好吧,深度图和点云很酷,但你可能想知道你可以用它们做什么。与传统的效果添加方法相比,深度感知效果可以非常强大。例如,深度感知处理可以创建各种电影效果,否则很难实现。通过视频的估计深度,你可以无缝地加入合成的相机对焦和虚焦,产生真实且一致的散景效果。

此外,深度感知技术提供了实现动态效果如“推镜变焦”的可能性。通过调整虚拟相机的位置和内参,这种效果可以生成惊艳的视觉序列。此外,深度感知的对象插入确保虚拟对象在视频中真实固定,保持整个场景中的一致位置。

深度图和点云的结合为引人入胜的叙事和富有创意的视觉效果开辟了无限可能,推动了电影制作人和艺术家的创意潜力达到新的高度。

点击本文的“发布”按钮后,我将挽起袖子开始制作这些效果。

祝你有美好的一天!

使用 Spark、Google Cloud Storage 和 Big Query 创建数据管道

原文:towardsdatascience.com/creating-a-data-pipeline-with-spark-google-cloud-storage-and-big-query-a72ede294f4c?source=collection_archive---------7-----------------------#2023-03-06

本地和云端协同工作以交付数据产品

João PedroTowards Data Science João Pedro

·

关注 发布于 Towards Data Science · 10 分钟阅读 · 2023 年 3 月 6 日

--

图片由 Toro Tseleng 提供,来源于 Unsplash

开发数据管道在某种程度上类似于玩乐高,你需要构思实现目标(数据需求),选择合适的零件(软件、工具、平台),然后将它们组装在一起。就像在乐高中一样,构建过程的复杂性取决于最终目标的复杂性。

从使用 Python 构建的简单 ETL 管道在两个数据库之间移动数据,到使用 Kafka 在各种云结构之间流式传输实时消息以服务多个最终应用程序,非常复杂的结构都是可能的。

但现实是,当前的数据场景更像那些昂贵的专业乐高套件,拥有各种解决特定需求的零件,并且新的零件在每个角落不断出现。你可能已经看过 Matt Turck 的 2021 年机器学习、人工智能和数据 (MAD) 领域。而糟糕的部分——说明书没有包含在内。

过去十年中,许多开源数据相关工具如 Spark、Hadoop 和 Kafka 已经被开发出来,更不用说 Python 库中所有可用的工具了。这些就是我喜欢在文章中覆盖的工具,它们是免费的,维护良好的 Docker 镜像,并且我可以使用 Docker 开发自包含的应用程序,任何人都可以在任何地方运行。

但是,随着数据领域的成熟,所有的箭头似乎都指向同一个方向——云计算。这一点毫不令人惊讶。专注于数据应用的公司如 Databricks、DBT 和 Snowflake 正在迅速流行,而传统玩家(AWS、Azure 和 GCP)也在大力投资他们的数据产品。

这就是今天文章的目标——我们将使用 Apache Spark、Google Cloud Storage 和 Google Big Query(使用免费层)来开发一个数据管道。

未经赞助。

工具

Spark 是一个面向处理极大量数据的通用分布式内存数据处理框架。我在许多其他文章中介绍过 Spark。

Google Cloud Storage (GCS) 是 Google 的对象存储。概念很简单:创建一个桶并将文件存储在其中。稍后使用它们的“路径”读取。文件夹是虚假的,对象是不可变的。

Google Big Query (GBQ) 是 Google 的云数据仓库解决方案。一个以 OLAP 为重点的数据库,具有无服务器 SQL 查询执行能力,能够处理大量数据。

数据

我们将构建一个数据管道来处理和存储来自巴西“高等教育”(字面翻译)普查的数据。该普查每年收集有关巴西高等教育机构(主要是大学)从不同角度的许多统计数据:机构的、社会和人口统计的、地理的等等。

我们将处理 课程报告,其中包含有关每个巴西高等教育课程(本科、研究生、博士等)的统计数据。这些数据公开可用 [CC BY-ND 3.0],以 CSV 文件形式提供(每年一个)。

实现

管道的想法很简单,将 CSV 文件下载到本地机器,转换为存储在 GCS 桶中的 Delta-Lake 表,对这个 delta 表进行所需的转换,并将结果保存到一个 Big Query 表中,以便其他下游任务可以轻松使用。

提议的管道。作者提供的图像。

存储桶将作为原始文件存储,聚合所有报告到一个地方。BigQuery 表将存储我们准备好的数据,已经过滤、聚合,并且只包含有用的列。

如前所述,普查收集了来自所有高等教育机构的大量统计数据,包括但不限于大学。为了模拟“真实情况”,假设我们需要创建一个表格来回答关于每年进入大学的新生的各种社会/人口统计问题。

0. 设置环境

所有代码都可以在这个 GitHub 仓库中找到。

你需要在本地计算机上安装 docker 以创建 Spark 集群,并且需要 python 来下载文件。

docker-compose 文件:

version: '3'

services:
  spark:
    build: .
    environment:
      - SPARK_MODE=master
    ports:
      - '8080:8080'
      - '4040:4040'
    volumes:
      - ./data:/data
      - ./src:/src
  spark-worker:
    build: .
    environment:
      - SPARK_MODE=worker
      - SPARK_MASTER_URL=spark://spark:7077
      - SPARK_WORKER_MEMORY=4G
      - SPARK_EXECUTOR_MEMORY=4G
      - SPARK_WORKER_CORES=4
    volumes:
      - ./data:/data
      - ./src:/src 

spark Dockerfile:

FROM docker.io/bitnami/spark:3.3.1

COPY *.jar $SPARK_HOME/jars

RUN mkdir -p $SPARK_HOME/secrets
COPY ./src/credentials/gcp-credentials.json $SPARK_HOME/secrets/gcp-credentials.json
ENV GOOGLE_APPLICATION_CREDENTIALS=$SPARK_HOME/secrets/gcp-credentials.json

RUN pip install delta-spark

docker 镜像已经配置为从头自动创建一个新环境,因此我们可以更多地关注实现而不是配置。

当然,你需要创建一个 Google Cloud Platform 账户。尽管我们只会使用免费的配额,但仍然需要你的信用卡信息。GCP 表示除非你明确结束免费试用期,否则不会收费,但请小心。

创建账户后,按照以下步骤操作:

1. 访问 GCP 控制台并创建一个新项目。我将我的项目命名为“BigQueryFirstSteps”

2. 在 API & Services 标签中授权 Google Cloud Storage 和 BigQuery 的 API。

3. 在 Google Cloud Storage 中创建一个名为censo-ensino-superior的新存储桶

4. 在 Google Big Query 中创建一个名为censo-ensino-superior的新数据集

5. 在 IAM & Administrator 标签页中的服务账户项目下创建一个新的服务账户,并分配适当的角色以读取、写入和创建 GCP 存储桶和 GBQ 表(我使用了BigQuery 管理员存储管理员角色)

6. 在此页面上,生成一个新的访问密钥(JSON)用于新创建的账户。密钥将下载到你的本地计算机。

返回本地环境,执行prepare_env.sh 脚本。

mkdir -p ./data/
mkdir -p ./src/credentials
chmod -R 777 ./src
chmod -R 777 ./data

wget https://storage.googleapis.com/hadoop-lib/gcs/gcs-connector-latest-hadoop2.jar

它创建了几个具有特定授权的文件夹(以便 spark 容器可以从中读取和写入)并下载了 GCS 连接器以供 spark 使用。

现在,将你的 JSON 凭证文件重命名为gcp-credentials.json并放置在 ./src/credentials 文件夹中(与 bucket_name.txt 文件一起)。

最后,启动容器:

docker compose up --build

1. 下载数据

只需运行脚本:

python download_files.py

CSV 文件将下载到 ./data 文件夹中。

2. 将 CSV 转换为 GCS 中的 Delta Lake

首先需要做的是实例化一个 Spark 会话,并配置 Delta-Lake 依赖项。

from pyspark.sql import SparkSession
from pyspark.sql import functions as F

from delta import configure_spark_with_delta_pip

MASTER_URI = "spark://spark:7077"

if __name__ == "__main__":
    # spark-submit --packages io.delta:delta-core_2.12:2.1.0 --master spark://spark:7077 insert_csv_into_delta_table_gcs.py

    builder = SparkSession.builder\
        .master(MASTER_URI)\
        .appName("Insert CSV Censo into Delta Table")\
        .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \
        .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog")\

    spark = configure_spark_with_delta_pip(builder).getOrCreate()

读取下载的 CSV 文件非常简单,只需指定选项并给出路径。

# Read the CSV file
df_cursos = (
    spark.read
    .format("csv")
    .option("header", "true")
    .option("delimiter", ";")
    .option("encoding", "ISO-8859-1")
    .option("inferSchema", "true")
    .load("/data/MICRODADOS_CADASTRO_CURSOS_*.CSV") # read all csv files
)

为了在 GCP 桶中写入更少的数据,我还仅选择了对最终表有用的列:普查年份;课程标识;知识领域;地点和类型;按性别、年龄和肤色统计的新生人数。

# Select Columns
df_cursos = df_cursos.select(
    [   
        # Year
        "NU_ANO_CENSO",
        # Course AND Institution
        "CO_IES",
        "NO_CURSO",
        "CO_CURSO",
        # Total of new students
        "QT_ING",
        # Age
        "QT_ING_0_17",
        # ...
        # Skin Color
        "QT_ING_BRANCA",
        "QT_ING_PRETA",
        # ...
        # Gender COLUMNS
        # Place COLUMNS
        # Area of Knowledge (CINE) COLUMNS
        # FIELDS OMITTED TO MAKE THIS CODE BLOCK SMALLER
    ]
)

# cast columns to the correct type
for col in df_cursos.columns:
    if col in ["NU_ANO_CENSO"] or col.startswith("QT_"):
        df_cursos = df_cursos.withColumn(col, df_cursos[col].cast(IntegerType()))
    elif col.startswith("IN_"):
        df_cursos = df_cursos.withColumn(col, df_cursos[col].cast(BooleanType()))
    else:
        df_cursos = df_cursos.withColumn(col, df_cursos[col].cast(StringType()))

将数据写入 GCP 桶就像写入文件系统一样,但我们需要用自己的语法指定桶路径:“gs://<bucket_name>/<filepath_to_be_create>”。

df_cursos.write\
        .format("delta")\
        .partitionBy(["NU_ANO_CENSO"])\
        .mode("overwrite")\
        .save("gs://censo-ensino-superior/cens_cursos")

上述代码在 censo-ensino-superior 桶内创建了一个名为 censo_cursos 的新 Delta 表。

我们不需要在代码中处理身份验证,因为凭据在 Docker 构建阶段已经正确配置。

要执行此脚本,请访问 Spark 容器的终端(主节点或工作节点)并执行:

# cd into the folder where the script is stored
spark-submit --packages io.delta:delta-core_2.12:2.1.0 --master spark://spark:7077 insert_csv_into_delta_table_gcs.py

一分钟左右,脚本将完成,数据将可用在你的 GCS 桶中。

3. 从 GCS 处理 Delta 表到 GBQ

首先需要做的是实例化一个 Spark 会话,与之前所做的相同。

从桶中读取数据也遵循与写入相同的逻辑。

df_censo = (
      spark.read
          .format("delta")
          .load("gs://censo-ensino-superior/cens_cursos")
)

数据准备好后,我们可以像往常一样进行一些转换。

 df_censo = (
        df_censo
        # Bachelor and Licenciatura TP_GRAU_ACADEMICO = 4
        .filter( 
            (F.col('TP_GRAU_ACADEMICO') == "1")  
            | (F.col('TP_GRAU_ACADEMICO') == "4")
        )
        # Group by CO_CINE_AREA_DETALHADA, CO_UF (STATE) and NU_ANO_CENSO (YEAR)
        .groupBy(
            'CO_CINE_AREA_DETALHADA', 'CO_UF', 'NU_ANO_CENSO'
        )
        .agg(
            F.max('NO_CINE_AREA_DETALHADA').alias('NO_CINE_AREA_DETALHADA'),

            F.max('NO_CINE_AREA_ESPECIFICA').alias('NO_CINE_AREA_ESPECIFICA'),
            F.max('CO_CINE_AREA_ESPECIFICA').alias('CO_CINE_AREA_ESPECIFICA'),
            F.max('NO_CINE_AREA_GERAL').alias('NO_CINE_AREA_GERAL'),
            F.max('CO_CINE_AREA_GERAL').alias('CO_CINE_AREA_GERAL'),

            F.max('SG_UF').alias('SG_UF'),
            F.max('NO_REGIAO').alias('NO_REGIAO'),
            F.max('CO_REGIAO').alias('CO_REGIAO'),

            F.count('CO_CURSO').alias('QT_CO_CURSO'),
            F.sum('QT_CURSO').alias('QT_CURSO'),
            F.sum('QT_VG_TOTAL').alias('QT_VG_TOTAL'),
            F.sum('QT_ING').alias('QT_ING'),

            F.sum('QT_ING_0_17').alias('QT_ING_0_17'),
            F.sum('QT_ING_18_24').alias('QT_ING_18_24'),
            F.sum('QT_ING_25_29').alias('QT_ING_25_29'),
            F.sum('QT_ING_30_34').alias('QT_ING_30_34'),
            F.sum('QT_ING_35_39').alias('QT_ING_35_39'),
            F.sum('QT_ING_40_49').alias('QT_ING_40_49'),
            F.sum('QT_ING_50_59').alias('QT_ING_50_59'),
            F.sum('QT_ING_60_MAIS').alias('QT_ING_60_MAIS'),

            F.sum('QT_ING_BRANCA').alias('QT_ING_BRANCA'),
            F.sum('QT_ING_PRETA').alias('QT_ING_PRETA'),
            F.sum('QT_ING_PARDA').alias('QT_ING_PARDA'),
            F.sum('QT_ING_AMARELA').alias('QT_ING_AMARELA'),
            F.sum('QT_ING_INDIGENA').alias('QT_ING_INDIGENA'),
            F.sum('QT_ING_CORND').alias('QT_ING_CORND'),

            F.sum('QT_ING_FEM').alias('QT_ING_FEM'),
            F.sum('QT_ING_MASC').alias('QT_ING_MASC'),
        )
    )

上述查询首先筛选出仅包含学士和学位课程(使用它们的代码),并按年份、详细领域和州对结果进行分组,汇总每个类别的新生人数(“QT_ING_XYZ”)。

要将数据写入 BigQuery 表,我们需要使用格式 “bigquery” 以及一些选项。显然,我们需要传递要写入的表和数据库。由于这个表尚不存在,因此需要将选项“createDisposition”设置为“CREATE_IF_NEEDED”。

标准的 GBQ-Spark 连接器使用 GCS 桶作为一个中间缓冲区,用于从 GBQ 传输数据。因此,我们需要传递一个“temporaryGcsBucket”选项,并指定桶名称。为了简单起见,我使用了之前创建的相同桶。

df_censo.write\
    .format("bigquery")\
    .mode("overwrite")\
    .option("temporaryGcsBucket", "censo-ensino-superior")\
    .option("database", "censo_ensino_superior")\
    .option("table", "censo_ensino_superior.cursos_graduacao_e_licenciatura")\
    .option("createDisposition", "CREATE_IF_NEEDED")\
    .save()

同时,请注意这次写入是以 mode=“overwrite” 模式进行的,如果表已经存在,它将覆盖任何以前的数据。如果你只想添加新行,请使用“append”模式。

就这些了。

要运行这个任务,只需输入:

# cd into the folder where the script is stored
spark-submit --packages io.delta:delta-core_2.12:2.1.0,com.google.cloud.spark:spark-3.1-bigquery:0.28.0-preview aggregate_delta_gcs_to_gbq_table.py

表将会被创建并填充,让我们来看看:

为了举例说明,让我们运行一个查询。

下面的查询计算了每个知识领域男性和女性的百分比。

SELECT
  CO_CINE_AREA_GERAL,
  NO_CINE_AREA_GERAL,
  SUM(QT_ING_MASC)/SUM(QT_ING_FEM + QT_ING_MASC) AS PERCENT_MASC,
  SUM(QT_ING_FEM)/SUM(QT_ING_FEM + QT_ING_MASC) AS PERCENT_FEM  
FROM 
  `censo_ensino_superior.cursos_graduacao_e_licenciatura`
GROUP BY
  NO_CINE_AREA_GERAL,
  CO_CINE_AREA_GERAL
ORDER BY
  PERCENT_MASC

结果:

带有注释翻译的查询结果。图片由作者提供。

结论

在这篇文章中,我们学习了如何使用 Apache Spark(本地部署)作为数据处理工具,并将 Google Cloud Storage(用于原始文件存储)和 Google Big Query(用于为分析查询提供处理后的数据)作为存储解决方案来开发数据管道。

使用 Spark 与云中的数据进行交互并没有那么特别。更困难的部分是配置环境:找到合适的连接器,将其放置在正确的位置,并使用正确的格式。坦白说,我在学习如何正确配置 Docker 镜像和凭据时遇到了很多困难。但一旦掌握了这个过程,查询和操作数据就会像平常一样。

这也是我最喜欢 Apache Spark 的其中一个原因——它将处理逻辑与连接逻辑分开。例如,如果我们将 blob 存储解决方案从 GCS 更改为 Amazon 的 S3,我们需要做的就是用新的 AWS 凭据重新配置环境并更改读写命令。所有的查询/转换逻辑保持不变。

但除了“没有那么特别”之外,学习如何与云存储组件交互是一个极其重要的技能,我希望这篇文章能帮助你更好地理解这个过程。

和往常一样,我不是帖子中涉及的任何主题的专家,我强烈建议进一步阅读,见下方参考文献。

感谢阅读!😉

参考文献

所有代码都可以在 这个 GitHub 仓库中找到

使用的数据 —* Microdados do Censo da Educação Superior,[CC BY-ND 3.0],INEP-巴西政府

[1] Chambers, B., & Zaharia, M. (2018). Spark: The definitive guide: Big data processing made simple. “ O’Reilly Media, Inc.”.

[2] 什么是 BigQuery?(无日期)。Google Cloud链接

[3] Delta Lake 官方页面。(无日期)。Delta Lake。 delta.io/

[4] Databricks. (2020 年 9 月 15 日)。利用 Delta Lake 改善 Apache SparkTM [视频]。YouTube。

[5]使用 BigQuery 连接器与 Spark。(无日期)。Google Cloud。 链接

[6] Sohail, K. (2021 年 12 月 15 日)。使用本地 PySpark 和 Jupyter Notebooks 从 Google Cloud Storage Bucket 读取文件。Medium。 链接

创建荷兰语问答机器学习模型

原文:towardsdatascience.com/creating-a-dutch-question-answering-machine-learning-model-3b666a115be3?source=collection_archive---------3-----------------------#2023-01-29

自然语言处理教程

使用自然语言处理翻译创建新的数据集

Erwin van CrasbeekTowards Data Science Erwin van Crasbeek

·

关注 发布于 Towards Data Science ·20 min 阅读·2023 年 1 月 29 日

--

荷兰语问答模型创建流程

自然语言处理模型目前是一个热门话题。谷歌发布的《Attention Is All You Need》[1] 推动了许多像 BERT、GPT-3 和 ChatGPT 这样的 Transformer 模型的发展,这些模型受到了全球的广泛关注。虽然许多语言模型是在英语或多语言上进行训练的,但针对特定语言的模型和数据集可能难以找到或质量堪忧。

NLP 有广泛的应用,包括但不限于翻译、信息提取、摘要和问答,而后者是我个人一直在从事的工作。作为应用人工智能的学生,我一直在研究问答 NLP 模型,并且发现很难找到有用的荷兰语数据集用于训练。为了解决这个问题,我开发了一个翻译解决方案,可以应用于各种 NLP 问题和几乎所有语言,这可能对其他学生有兴趣。我认为这对人工智能开发和研究社区也具有很大的价值。特别是对于像问答这样的特定任务,几乎没有荷兰语数据集。通过翻译一个大型且知名的数据集,我能够以相对较低的努力创建一个荷兰语问答模型。

如果你有兴趣了解更多关于我的过程、我面临的挑战以及此解决方案的潜在应用,请继续阅读。本文旨在为具有基本 NLP 背景的学生提供。然而,我还为那些尚未熟悉该领域或仅需复习的人士提供了复习材料和各种概念的介绍。

为了正确解释我使用翻译数据集的解决方案,我将本文分为两个主要部分:数据集的翻译和问答模型的训练。我撰写本文的方式旨在展示我在解决方案方面的进展,同时也作为一个逐步指南。文章包括以下章节:

  1. 关于 NLP 的复习和 NLP 的简要历史

  2. 问题、数据集和问答

  3. 翻译数据集

  4. 构建一个问答模型

  5. 已取得的成就与未取得的成就?

  6. 未来计划

  7. 来源

关于 NLP 的复习和 NLP 的简要历史

为了更好地理解这个解决方案的各个元素,我想从对 NLP 及其近期历史的复习开始。我们所知道的语言可以分为两组,形式语言和自然语言。形式语言指的是专门为特定任务如数学和编程设计的语言。自然语言或普通语言是指由人类自然发展和演变的语言,没有任何形式的预先规划。这可以表现为我们所知道的各种人类言语形式,甚至是手语[2]。

NLP 在其最广泛的形式上是将计算方法应用于自然语言。通过将基于规则的语言建模与人工智能模型相结合,我们已经能够使计算机以一种能够处理文本和语音形式的方式“理解”我们的自然语言[3]。这种理解的方式——如果它真的可以称为理解的话——仍然存在争议。然而,像 ChatGPT 这样的最新发展表明,我们人类确实常常觉得这些模型的输出让人感到它有自我意识,并且具有较高的理解水平[4]。

当然,这种理解并非凭空而来。NLP 有着广泛的历史,可以追溯到二战后的 1940 年代[5]。在这个时期,人们意识到了翻译的重要性,并希望创造一种能够自动完成翻译的机器。然而,这证明是相当具有挑战性的。大约在 1960 年左右,NLP 研究分为基于规则的和随机的两大类。基于规则的或符号化的主要涉及形式语言和语法生成。这个领域的许多语言学研究者和计算机科学家认为这是人工智能研究的开始。随机研究则更多关注统计学和文本间的模式识别等问题。

自那时起,NLP(自然语言处理)领域取得了许多进展,研究领域也不断扩展。然而,NLP 模型生成的实际文本一直相当有限,且缺乏许多现实世界的应用。直到 2000 年代初期,NLP 的发展才迎来了每隔几年便有显著突破的阶段,这才导致了我们现在的情况。

问题、数据集和问答

现在我已经简要回顾了 NLP 的背景,是时候介绍我一直在研究的实际问题了。简而言之,我的目标是训练一个荷兰语问答的机器学习模型。然而,由于缺乏合适的数据集,这变得相当困难,因此我通过翻译创建了自己的数据集。在本文中,我将逐步讲解数据集的创建和机器学习模型的训练,以便你可以跟随并复制整个解决方案,或选择对你来说重要的部分。

本文可以分为两个主要部分。第一个是荷兰语数据集的创建,第二个是问答机器学习模型的训练。在这一章中,我将提供一些背景信息,介绍我的解决方案并解释我的选择。

数据集

如果我们想找到一个有用的荷兰语数据集,那么了解训练一个问答模型所需的具体内容是很重要的。生成答案的主要有两种方法:第一种是抽取式,第二种是生成式。

· 抽取式问答模型被训练以从上下文(源文本)中提取答案[7]。较早的方法通过训练一个模型来输出答案在上下文中的起始和结束索引来实现这一点。然而,Transformer 的引入使这种方法已经过时。

· 生成式问答模型被训练以根据上下文和问题生成新文本[8]。

图 1 展示了抽取式和生成式模型可能产生的输出示例。

尽管有不同的方法,但如今抽取式和生成式问答模型通常都基于像 BERT 这样的 Transformer[8],[9]。

图 1. 抽取式与生成式方式生成的答案示例。

基于关于抽取式和生成式模型的信息,我们现在知道我们需要一个包含上下文、问题、答案以及(可选的)答案在上下文中的起始和结束索引的数据集。我已经探索了以下选项,以寻找合适的数据集。

  • 我使用了 Cambazoglu et al 的 2020 年论文[10],以获得有关问答数据集的清晰图像。他们的研究结果提供了一张包含最显著问答数据集的表格。不幸的是,这些大型数据集中没有荷兰语的数据集。

  • 另一个选择是 Huggingface,它托管了大量的数据集[11]。乍一看,有一些荷兰语的问答数据集。然而,进一步检查显示,这些数据集往往不完整,包含网站域名而不是上下文,或者是各种语言的混合。这些数据集完全无法使用,或者不够完整,无法用于我们的目标。

从这些观察结果来看,几乎没有公共数据集可以用来训练荷兰语问答模型。手动创建我们自己的数据集将花费太多时间,那么我们还有什么其他选项?首先,我们可以简单地使用一个英语模型,将荷兰语输入翻译成英语,然后将输出再翻译回荷兰语。然而,通过 Google 翻译进行的快速测试表明,这种方法的结果远非理想,几乎感觉有些消极攻击。也许在双重翻译步骤中丢失了太多信息和上下文?这就引出了第二个选项,即翻译整个数据集并在其上进行训练。在我的研究中,我遇到了一些提到这一点的实例。例如,Zoumana Keita 在 Towardsdatascience 上的一篇文章[16]使用翻译进行数据增强。第三章将深入探讨我如何执行数据集的翻译。

最后,我们需要选择用于翻译的方法的数据集。既然我们决定翻译整个数据集,那么原始数据集使用什么语言就不重要了。斯坦福问答数据集(SQuAD)[12] 似乎相当受欢迎,并被 Paperswithcode 用于问答基准测试[13]。它还包含大量(100,000+)的问答,并且经仔细检查后似乎没有任何意外数据。这就是我们将要使用的数据集。

机器学习模型

现在我们已经确定了如何获取数据集;我们需要决定哪种机器学习模型适合回答问题的目标。在前一章中,我们已经确定可以选择抽取式模型和生成式模型。在我的研究中,我使用了生成式模型,因为它基于较新的技术,并且给出了更有趣的结果。然而,以防有人希望采用抽取式模型,我也会对此进行介绍。这也与数据集的选择一致,因为它包含了答案的起始索引。

从头开始训练一个 Transformer 模型,至少可以说是低效的。P. Azunre 的《自然语言处理中的迁移学习》一书[14]深入探讨了为什么进行迁移学习,并展示了如何进行迁移学习的多个示例。大量大型 NLP 模型托管在 Huggingface[15]上,并可用于迁移学习。我选择了 t5-v1_1-base 模型,因为它经过多语言的多任务训练。第四章将介绍该模型的迁移学习。

翻译数据集

在本章中,我将展示如何通过提供代码片段并对其进行解释来翻译数据集。这些代码块连续生成的代码就是我编写的整个数据集翻译脚本。欢迎跟随或取用对你有用的特定部分。

导入

解决方案使用了几个模块。首先,我们需要以尽可能快的速度翻译文本。在我的研究中,我尝试使用来自 Huggingface 的各种翻译 AI 模型,但迄今为止,最快的翻译器是 Googletrans 模块,它使用了 Google Translate API。该解决方案还使用了 httpx 的 Timeout 来定义翻译的超时时间,使用 json 解析 SQuAD 数据集,使用 Pandas 处理数据框,以及使用 Time 来测量所有操作所需的时间。

from googletrans import Translator, constants
from httpx import Timeout

import json
import pandas as pd
import time

初始化

首先,我们应该定义几个在脚本中会用到的常量。为了方便访问,我在这里添加了源语言和翻译语言。

Googletrans 模块为我们提供了一个可以自定义超时时间的翻译器。我使用了相对较长的超时时间,因为在测试期间翻译经常超时。我将在本指南后面的部分提供更多有关这个问题的信息。

src_lang = "en"
dest_lang = "nl"

translator = Translator(timeout = Timeout(60))

阅读 SQuAD 数据集

以下代码从训练和验证 json 文件中提取上下文、问题和答案。这是通过将文件以 json 格式读取,并以一种提取三种列表的方式遍历数据来完成的。对于每个问题和答案,上下文被复制并添加到上下文列表中。这样我们可以通过使用索引轻松访问带有相关上下文和答案的问题。

def read_squad(path):
    with open(path, 'rb') as f:
        squad_dict = json.load(f)
    contexts, questions, answers = [], [], []
    for group in squad_dict['data']:
        for passage in group['paragraphs']:
            context = passage['context']

            for qa in passage['qas']:
                question = qa['question']
                if 'plausible_answers' in qa.keys():
                    access = 'plausible_answers'
                else:
                    access = 'answers'
                for answer in qa[access]:
                    contexts.append(context)
                    questions.append(question)
                    answers.append(answer['text'])
    return contexts, questions, answers

train_c, train_q, train_a = read_squad('squad-train-v2.0.json')
val_c, val_q, val_a= read_squad('squad-dev-v2.0.json')

时间

以下代码为我们提供了每个翻译所需时间的大致估计。

def time_translation(entries, name):
    start_time = time.time()
    translation = translator.translate(entries[0], dest=dest_lang, src= src_lang)
    duration = time.time() - start_time
    total_duration = len(entries)*duration
    print(f"translating {name} takes {total_duration/60/60} hours")

time_translation(train_c, "train contexts")
time_translation(train_q, "train questions")
time_translation(train_a, "train answers")
time_translation(val_c, "validation contexts")
time_translation(val_q, "validation questions")
time_translation(val_a, "validation answers")

翻译

记得我提到过翻译超时的问题吗?在我的研究过程中,我不断遇到翻译超时的问题,导致结果数据集被损坏。事实证明,Googletrans 模块并不是 100% 可靠的,因为它使用了 Google Translate API。我找到的解决办法是创建一个小的包装函数,该函数会不断尝试翻译,直到成功为止。经过这样处理后,我不再遇到超时问题。

def get_translation(text):
    success = False
    translation = ""
    while not success:
        translation = translator.translate(text, dest=dest_lang, src=src_lang).text
        success = True
    return translation

由于我们从数据集中提取上下文的方式,每个问题和答案对都有重复的上下文。直接翻译所有上下文会显得冗余且非常缓慢,因此以下翻译函数首先会将前一个上下文与当前上下文进行比较。如果它们匹配,则使用之前的翻译。

def translate_context(contexts, name):
    start_time = time.time()
    context_current = ""
    translated_contexts = []
    index = 0

    for context in contexts:
        index+=1
        if context != context_current:
            context_current = context
            print(f"[{index}/{len(contexts)}]")
            get_translation(context)
            context_translated = get_translation(context)
            translated_contexts.append(context_translated)
        else:
            translated_contexts.append(context_translated)

    duration = time.time() - start_time
    print(f"Translating {name} took {round(duration, 2)}s") 
    return translated_contexts

翻译问题和答案非常简单,因为我们只需循环遍历列表来翻译所有内容。

def translate_qa(input, name):
    start_time = time.time()
    input_translated = []
    index = 0
    for text in input:
        text_nl = get_translation(text)
        input_translated.append(text_nl)
        index+=1
        print(f"[{index}/{len(input)}]")
    duration = time.time() - start_time
    print(f"Translating {name} took {round(duration, 2)}s") 
    return input_translated

现在我们可以使用我们定义的函数来翻译数据集的所有部分。

train_c_translated = translate_context(train_c, "train contexts")
train_q_translated = translate_qa(train_q, "train questions")
train_a_translated = translate_qa(train_a, "train answers")

val_c_translated = translate_context(val_c, "val contexts")
val_q_translated = translate_qa(val_q, "val questions")
val_a_translated = translate_qa(val_a, "val answers")

导出

只剩下将翻译导出以供以后使用。我们可以通过将列表转换为数据框,然后使用 to_csv 函数来完成这一点。需要注意的是,Googletrans 模块输出的翻译包含 utf-8 编码中不包含的字符。这就是我们在这里使用 utf-16 编码的原因。将其转换为 utf-8 可能在某些时候更有用,因为这可能对 AI 模型更有帮助。然而,由于我们这里只是在处理数据集,所以我们可以决定将这一步骤留到后续数据预处理阶段。

def save_data(data, name, header):
    data_df = pd.DataFrame(data)
    data_df.to_csv(name + "_pdcsv.csv", encoding='utf-16', index_label = "Index", header = [header])

save_data(train_c_translated, "train_contexts", "contexts")
save_data(train_q_translated, "train_questions", "questions")
save_data(train_a_translated, "train_answers", "answers")
save_data(val_c_translated, "val_contexts", "contexts")
save_data(val_q_translated, "val_questions", "questions")
save_data(val_a_translated, "val_answers", "answers")

构建问答模型

发现如何训练一个问答模型的过程有点挑战。然而,通过借鉴 P. Suraj [17] 的 Notebook,我能够创建一个基于 Transformer 的模型,该模型可以用于问答训练。按照 Notebook 的指导,我使用了 Torch 来创建模型。

导入

从导入开始,使用了以下模块。我们还定义了一些变量,这些变量定义了模型的最大输入和输出长度。

import pandas as pd
import unicodedata

import torch
from torch.utils.data import DataLoader

from transformers import T5Tokenizer
from transformers import T5ForConditionalGeneration
from transformers import AdamW
from tqdm import tqdm

from sklearn.model_selection import train_test_split 
from datetime import datetime

max_text_length = 512
max_output_length = 256

加载数据

现在我们可以加载之前创建的数据集。由于我们使用 Pandas 导出了 csv,因此现在可以轻松加载并将其转换为数组。我还定义了一个函数,该函数将在后续将任何训练或输入数据转换为 utf-8,这是我们将用于训练模型的格式。

def load_data(path):
    df = pd.read_csv(path, encoding='utf-16')
    df = df.drop('Index', axis=1)
    data = df.values.tolist()
    data = [a[0] for a in data]
    return data

def to_utf8(text):
    try:
        text = unicode(text, 'utf-8')
    except NameError:
        pass
    text = unicodedata.normalize('NFD', text).encode('ascii', 'ignore').decode("utf-8")
    return str(text)

现在我们可以实际加载数据。在模型训练中,我只使用了训练数据,并将其拆分为测试数据,测试数据大小为 0.2。

contexts_csv = 'train_contexts_pdcsv.csv'
questions_csv = 'train_questions_pdcsv.csv'
answers_csv = 'train_answers_pdcsv.csv'

contexts = load_data(contexts_csv)
questions = load_data(questions_csv)
answers = load_data(answers_csv)

c_train, c_val, q_train, q_val, a_train, a_val = train_test_split(contexts,
                                                questions, answers,
                                                test_size=0.2,
                                                random_state=42)

准备数据

如我之前提到的,我们可以训练一个抽取式模型和一个抽象生成模型。在我的研究中,我开发了这两种模型。在这篇文章中,我只介绍抽象生成版本,但对于感兴趣的读者,我还会解释如何为抽取式模型预处理数据。这是为了创建上下文中答案的起始和结束索引。

抽象生成

数据集不需要过多预处理就可以训练抽象生成模型。我们只需将所有训练数据转换为 utf-8。可以取消注释最后三行,以减少训练集的大小,这将改善训练时间并有助于调试。

def clean_data(contexts, questions, answers):
    cleaned_contexts, cleaned_questions, cleaned_answers = [], [], []
    for i in range(len(answers)):
        cleaned_contexts.append(to_utf8(contexts[i]))
        cleaned_questions.append(to_utf8(questions[i]))
        cleaned_answers.append(to_utf8(answers[i]))
    return cleaned_contexts, cleaned_questions, cleaned_answers

cc_train, cq_train, ca_train = clean_data(c_train, q_train, a_train); 
cc_val, cq_val, ca_val = clean_data(c_val, q_val, a_val); 

print("Original data size: " + str(len(q_train)))
print("Filtered data size: " + str(len(cq_train)))

#cc_train = cc_train[0:1000]
#cq_train = cq_train[0:1000]
#ca_train = ca_train[0:1000]

抽取式

在许多情况下,抽取式模型需要上下文中答案的起始和结束索引。然而,由于我们使用 Transformer 翻译了数据集,可能会出现一些问题。例如,答案可能与上下文中的措辞不同,或者答案的位置或长度可能已经改变。为了解决这个问题,我们可以尝试在上下文中找到答案,如果找到答案,则将其添加到清理后的答案中。因此,我们也获得了关于起始索引的信息,结束索引简单地是起始索引加上答案的长度。

def clean_data(contexts, questions, answers):
    cleaned_contexts, cleaned_questions, cleaned_answers = [], [], []
    for i in range(len(answers)):
        index = contexts[i].find(answers[i])
        if(index != -1):
        #print(str(index) + " + " + str(index+len(answers[i])))
            cleaned_contexts.append(contexts[i])
            cleaned_questions.append(questions[i])
            cleaned_answers.append({
                'text':answers[i],
                'answer_start': index,
                'answer_end': index+len(answers[i])
                })
    return cleaned_contexts, cleaned_questions, cleaned_answers

cc_train, cq_train, ca_train = clean_data(c_train, q_train, a_train); 
cc_val, cq_val, ca_val = clean_data(c_val, q_val, a_val);

分词器

下一步是分词,因为我们使用的是 t5-v1_1-base,我们可以直接从 Huggingface 导入分词器。然后,我们将使用问题对上下文进行分词,以便分词器将它们与结束字符串标记一起添加。我们还指定了之前定义的 max_text_length。最后,分词后的答案被添加到编码中作为目标。

tokenizer = T5Tokenizer.from_pretrained('google/t5-v1_1-base')
train_encodings = tokenizer(cc_train, cq_train, max_length=max_text_length, truncation=True, padding=True)
val_encodings = tokenizer(cc_val, cq_val, max_length=max_text_length, truncation=True, padding=True)

def add_token_positions(encodings, answers):
    tokenized = tokenizer(answers, truncation=True, padding=True)
    encodings.update({'target_ids': tokenized['input_ids'], 'target_attention_mask': tokenized['attention_mask']})

add_token_positions(train_encodings, ca_train)
add_token_positions(val_encodings, ca_val)

数据加载器

我们将使用数据加载器来训练 PyTorch 模型。这里还可以指定批量大小。我训练时的服务器内存有限,所以我不得不使用批量大小为 2。如果可能的话,使用更大的批量大小会更好。

class SquadDataset(torch.utils.data.Dataset):
    def __init__(self, encodings):
        self.encodings = encodings
        print(encodings.keys())

    def __getitem__(self, idx):
        return {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}

    def __len__(self):
        return len(self.encodings.input_ids)

train_dataset = SquadDataset(train_encodings)
val_dataset = SquadDataset(val_encodings)

train_loader = DataLoader(train_dataset, batch_size=2, shuffle=True)

训练模型

我们使用的模型是基于 T5-v1_1-base 的 T5ForConditionalGeneration。如果用于训练的 PC 或服务器上安装了 CUDA,我们可以尝试利用它来显著提高训练速度。我们还告诉模型我们将对其进行训练。

我们使用的优化器是 AdamW,学习率为 1e-4。这个选择基于 T5 文档[18],文档中提到在我们的情况下这是一个合适的值:

通常,1e-4 和 3e-4 对于大多数问题(分类、摘要、翻译、问答、问题生成)效果很好。

最后,我们定义一个函数,在模型训练完成后将其保存以供以后使用。

model = T5ForConditionalGeneration.from_pretrained('google/t5-v1_1-base')
cuda = torch.cuda.is_available()
device = torch.device('cuda') if cuda else torch.device('cpu')
model.to(device)
model.train()

optimizer = AdamW(model.parameters(), lr=1e-4)

def save_model():
    now = datetime.now()
    date_time = now.strftime(" %m %d %Y %H %M %S")
    torch.save(model.state_dict(), "answer_gen_models/nlpModel"+date_time+".pt")

模型的实际训练将在三个时期内完成,我使用的 Notebook [17] 和 T5 文档都表明这是一个不错的训练周期数。在我配备 RTX 3090 的 PC 上,这大约需要每个周期 24 小时。我使用的服务器利用了 Nvidia Tesla T4,每个周期大约需要 6 小时。

Tqdm 模块用于对训练状态进行可视化反馈。它提供了关于已过时间和估计训练时间的数据。两个注释箭头之间的步骤对于我们的问答目标很重要,这里我们定义了给模型的输入。该代码块中的其他步骤对于 PyTorch 模型的训练相当直接。

for epoch in range(3):
    loop = tqdm(train_loader, leave=True)
    for batch in loop:
        optim.zero_grad()

        # >
        input_ids = batch['input_ids'].to(device)
        attention_mask = batch['attention_mask'].to(device)
        target_ids = batch['target_ids'].to(device)
        target_attention_mask = batch['target_attention_mask'].to(device)

        outputs = model(input_ids, attention_mask=attention_mask,
                        labels=target_ids,
                        decoder_attention_mask=target_attention_mask)
        # >
        loss = outputs[0]
        loss.backward()
        optimizer.step()

        loop.set_description(f'Epoch {epoch}')
        loop.set_postfix(loss=loss.item())
save_model()

结果

如果你跟随完成了,恭喜你!你已经创建了自己的荷兰数据集并训练了一个荷兰问答模型!如果你和我一样,可能迫不及待想尝试一下模型的结果。你可以使用以下代码来评估模型。有趣的是,你可能会发现模型不仅能够回答荷兰语问题!它也有能力回答不同(主要是日耳曼语)的语言的问题。这很可能是因为原始 T5-v1_1-base 模型已经在四种不同语言上进行了训练。

model = T5ForConditionalGeneration.from_pretrained('google/t5-v1_1-base')
model.load_state_dict(torch.load("answer_gen_models/some_model.pt"))

cuda = torch.cuda.is_available()
device = torch.device('cuda') if cuda else torch.device('cpu')
model.to(device)
model.eval()

def test(context, question):
    input = tokenizer([to_utf8(context)],
                      [to_utf8(question)],
                      max_length=max_text_length,
                      truncation=True,
                      padding=True)
    with torch.no_grad():
        input_ids = torch.tensor(input['input_ids']).to(device)
        attention_mask = torch.tensor(input['attention_mask']).to(device)
        out = model.generate(input_ids,
                             attention_mask=attention_mask,
                             max_length=max_output_length,
                             early_stopping=True)
        print([tokenizer.decode(ids,
        skip_special_tokens=True) for ids in out][0])

test("Dit is een voorbeeld", "Wat is dit?")

以下是一些示例背景和问题以及模型生成的答案:

背景 我们和应用人工智能硕士班的同学们去过科隆。

问题 班级去过哪里?

答案 科隆

背景 大棕色狐狸跳过了懒狗。

问题 狐狸跳过了什么?

答案 懒狗

背景 大棕色狐狸跳过了懒狗。

问题 狐狸做了什么?

答案 跳过懒狗

背景 两乘二是十。

问题 两乘二是多少?

答案

已经实现了什么,未实现什么?

总结一下,我们选择了一个用于问答的英文数据集,通过 Google Translate API 将其翻译成荷兰语,并训练了一个基于 T5-v1_1-base 的 PyTorch 编码器-解码器模型。我们究竟实现了什么,这在实际情况中是否能使用?

首先,重要的是要认识到我们没有对模型进行适当评估,因为这不是本文的范围。然而,为了能够正确解释我们的结果,并能够谈论其可用性,我建议查看如 Rouge [19] 等度量标准或进行人类评估。我采取的方法是人类评估。表 2 显示了五个人对各种上下文来源和问题生成答案的平均评分,评分范围从 1 到 5。平均分为 2.96\。这个数字本身并没有告诉我们很多信息,但我们可以从表中得出结论,我们创建的模型在某些情况下可以生成接近完美的答案。然而,它也经常生成评估小组认为完全无意义的答案。

表 2. 各种文章、论文和学位论文的人类评估评分(1–5)。

还需要注意的是,通过翻译数据集,我们很可能引入了偏差。谷歌翻译背后的 AI 已经在一个数据集上进行训练,由于其基于自然语言,因此自然包含了偏差。通过翻译我们的数据,这种偏差将传递到任何使用该数据集训练的模型中。在像这样的数据集可以在实际情况下使用之前,应彻底评估,以指出其中的偏差以及这些偏差如何影响结果。

然而,这种解决方案对于那些在实验 AI、开发新型机器学习模型或仅仅学习 NLP 的人来说可能非常有趣。这是一种非常便捷的方式,可以为几乎任何 NLP 问题获取大规模的数据集。许多学生无法获得大数据集,因为这些数据集通常只对大型公司开放或费用过高。通过这样的方式,任何大型英语数据集都可以转换为特定语言的数据集。

未来计划

我个人对这个方法的应用前景非常感兴趣。我目前正在研究一个使用完全相同方法和数据集的问题生成模型。我希望调查这两种模型结合使用的效果,以便更多地了解潜在的偏差或错误。这与第五章讨论的评估需求是一致的。我已经通过请五个人对创建的模型的结果进行评分来创建了一个人类评估。然而,我打算进一步了解不同的度量标准,这些标准可以更好地告诉我模型的工作原理、生成某些结果的原因以及其中包含的偏差。

我还了解到,斯坦福问题与回答数据集的 2.0 版本包含一些无法回答的问题。虽然这与本文提供的解决方案没有直接关系,但我对将本文的解决方案应用于完整的 SQuAD 2.0 数据集后的结果差异感到好奇。

来源

[1] A. Vaswani et al.,“注意力机制是你所需要的一切,” 2017 年。

[2] D. Khurana, A. Koli, K. Khatter, 和 S. Singh,“自然语言处理:最新进展、当前趋势和挑战,” Multimedia Tools and Applications,2022 年 7 月,doi: 10.1007/s11042–022–13428–4。

[3] “什么是自然语言处理?| IBM,” www.ibm.comwww.ibm.com/topics/natural-language-processing(访问日期:2023 年 1 月 11 日)。

[4] E. Holloway,“是的,ChatGPT 是有意识的 — 因为实际上是人类在其中,” Mind Matters,2022 年 12 月 26 日。mindmatters.ai/2022/12/yes-chatgpt-is-sentient-because-its-really-humans-in-the-loop/(访问日期:2023 年 1 月 18 日)。

[5] “NLP — 概述,” cs.stanford.educs.stanford.edu/people/eroberts/courses/soco/projects/2004-05/nlp/overview_history.html(访问日期:2023 年 1 月 18 日)。

[6] S. Ruder,“自然语言处理最近历史的回顾,” Sebastian Ruder,2018 年 10 月 1 日。ruder.io/a-review-of-the-recent-history-of-nlp/(访问日期:2023 年 1 月 18 日)。

[7] S. Varanasi, S. Amin, 和 G. Neumann,“AutoEQA:用于提取式问答的自动编码问题,” 计算语言学协会年会论文集:EMNLP 2021,2021 年。

[8] “什么是问答? — Hugging Face,” huggingface.cohuggingface.co/tasks/question-answering(访问日期:2023 年 1 月 18 日)。

[9] R. E. López Condori 和 T. A. Salgueiro Pardo,“观点总结方法:比较和扩展提取式和抽象式方法,” 专家系统应用,第 78 卷,第 124–134 页,2017 年 7 月,doi: 10.1016/j.eswa.2017.02.006。

[10] B. B. Cambazoglu, M. Sanderson, F. Scholer, 和 B. Croft,“关于问答研究的公共数据集综述,” ACM SIGIR Forum,第 54 卷,第 2 期,第 1–23 页,2020 年 12 月,doi: 10.1145/3483382.3483389。

[11] “Hugging Face — 建设未来的人工智能社区,” huggingface.cohuggingface.co/datasets?language=language:nl&task_categories=task_categories:question-answering&sort=downloads(访问日期:2023 年 1 月 18 日)。

[12] “斯坦福问答数据集,” rajpurkar.github.iorajpurkar.github.io/SQuAD-explorer/(访问日期:2023 年 1 月 18 日)。

[13] “Papers with Code — 问答,” paperswithcode.compaperswithcode.com/task/question-answering(访问日期:2023 年 1 月 18 日)。

[14] P. Azunre,自然语言处理中的迁移学习。Simon and Schuster,2021 年。

[15] “Hugging Face — 一次提交解决 NLP 问题的使命。” huggingface.cohuggingface.co/models(访问日期:2023 年 1 月 18 日)。

[16] Z. Keita,“使用 MarianMT 进行 NLP 中的数据增强,” Medium,2022 年 11 月 5 日。 towardsdatascience.com/data-augmentation-in-nlp-using-back-translation-with-marianmt-a8939dfea50a(访问日期:2023 年 1 月 18 日)。

[17] P. Suraj,“Google Colaboratory,” colab.research.google.comcolab.research.google.com/github/patil-suraj/exploring-T5/blob/master/T5_on_TPU.ipynb(访问日期:2023 年 1 月 25 日)。

[18] “T5,” huggingface.cohuggingface.co/docs/transformers/model_doc/t5#transformers.T5Model(访问日期:2023 年 1 月 25 日)。

[19] “ROUGE — evaluate-metric 提供的 Hugging Face 空间,” huggingface.cohuggingface.co/spaces/evaluate-metric/rouge(访问日期:2023 年 1 月 25 日)。

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

在 Python 中创建梯度下降动画

原文:towardsdatascience.com/creating-a-gradient-descent-animation-in-python-3c4dcd20ca51

如何绘制点在复杂曲面上的轨迹

Luis MedinaTowards Data Science Luis Medina

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

--

图片由Todd Diemer提供,来源于Unsplash

让我告诉你我是如何创建梯度下降动画的,只是为了在博客文章中说明一个观点。这是值得的,因为通过这样做我学到了更多的 Python,并掌握了一项新技能:制作动画图。

在 Python 中创建的梯度下降动画。图片由作者提供。

我将带你一步一步走过我所遵循的过程。

背景介绍

几天前,我发表了一篇关于梯度下降的博客文章,介绍了作为训练人工神经网络的优化算法的梯度下降。

我想包含一个动画图形,以展示选择不同的初始化点对于梯度下降优化可能产生不同的结果。

就在那时,我偶然发现了这些惊人的动画,这些动画由Alec Radford多年前创建,并在Reddit 评论中分享,展示了一些先进的梯度下降算法之间的差异,如AdagradAdadeltaRMSprop

由于我一直在努力用 Python 取代 Matlab,我决定试试自己编写类似的动画,首先使用“基本版”梯度下降算法。

一步一步来吧。

绘制用于优化的表面

首先我们导入所需的库,并定义我们想要表示的数学函数。

我想使用一个鞍点表面,所以我定义了以下方程:

鞍形表面方程。图像由作者提供。

我们还创建了一个点网格来绘制我们的表面。[np.mgrid](https://numpy.org/doc/stable/reference/generated/numpy.mgrid.html)非常适合这个目的。传递的复数81j作为步长,表示在起始值和结束值之间创建多少个点(81 个点)。

import numpy as np
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt

# Create a function to compute the surface
def f(theta):
  x = theta[0]
  y = theta[1]
  return x**2 - y**2

# Make a grid of points for plotting
x, y = np.mgrid[-1:1:81j, -1:1:81j]

接下来要做的是创建图形和坐标轴,绘制表面并格式化。

# Create a figure with axes in 3d projection
fig1 = plt.figure(figsize = (4,8), dpi = 300)
ax1 = fig1.add_subplot(111, projection='3d')

plot_args = {'rstride': 2, 'cstride': 2, 'cmap':"coolwarm",
             'linewidth': 0.01, 'antialiased': False,
             'vmin': -1, 'vmax': 1, 'edgecolors':'k'}

ax1.plot_surface(x, y, f([x,y]), **plot_args)

ax1.view_init(azim= 120, elev= 37)
ax1.set_xlabel('x')
ax1.set_ylabel('y')

plt.plot()

鞍形表面。图像由作者提供。

计算梯度下降轨迹

我们需要实现梯度下降算法并将其应用到我们的表面。这样,我们将计算 x 和 y 坐标在优化过程中迭代的演变。

正如我在关于梯度下降的帖子中提到的,最简单的实现是根据梯度迭代更新 x 和 y 值,直到达到局部最小值。

一旦你在任何任意点初始化 x 和 y 来开始优化,算法基于以下步骤:

  1. 计算当前点(x, y)处的表面梯度(偏导数)。

  2. 按梯度更新坐标。

  3. 在新坐标处评估表面函数。

这是在预定义的迭代次数内完成的,或者直到一次迭代与前一次迭代之间的差异小于某个阈值。

为了实现这一点,我创建了一个函数,参数为初始化值、步长参数和所需的迭代次数。

它返回一个包含每次迭代的 x、y、z 坐标的数组。

# Define a function to compute the gradient
def grad_f(theta):
    returnValue = np.array([0.,0.])
    x = theta[0]
    y = theta[1]
    returnValue[0] += 2*x
    returnValue[1] += - 2*y
    return returnValue

# Gradient descent routine
def gradient_descent(x_init, y_init, step_size, n_iters):
  eta = step_size

  theta = np.tile([x_init, y_init], (n_iters,1) )
  g_t = np.zeros((n_iters,2))
  z = np.tile([f(theta[0])], n_iters )

  for k in range (1, n_iters):

      g_t[k] = grad_f(theta[k-1])
      theta[k] = theta[k-1] - eta * g_t[k]
      z[k] = f(theta[k])

  # Setting up Data Set for Animation
  dataSet = np.stack((theta[:,0], theta[:,1], z), 1)

  return dataSet

我用两个不同的初始化点进行了测试,一个在 y = 0 的对称线上的,另一个在偏移的 y 值上。

我对两者使用了相同的步长和迭代次数,以便进行比较。这也使得后续动画制作更为简单,因为两个结果将具有相同的帧数。

n_iters = 200
step_size = 0.0

x_sym, y_sym = 1, 0
x_off, y_off = 1, 0.25

data_sym = gradient_descent(x_sym, y_sym, step_size , n_iters)
data_off = gradient_descent(x_off, y_off, step_size , n_iters)

为了验证结果,我分别绘制了每个坐标的轨迹

fig2, ax2 = plt.subplots(1,3, figsize = (6,2), dpi = 300)

ax2[0].plot(data_sym[:,0])
ax2[0].plot(data_off[:,0])
ax2[0].set_xlabel('Iteration')
ax2[0].set_ylabel('x')

ax2[1].plot(data_sym[:,1])
ax2[1].plot(data_off[:,1])
ax2[1].set_xlabel('Iteration')
ax2[1].set_ylabel('y')
ax2[1].set_ylim(0, 10)

ax2[2].plot(data_sym[:,2], label = 'Symmetry')
ax2[2].plot(data_off[:,2], label = 'Offset')
ax2[2].set_xlabel('Iteration')
ax2[2].set_ylabel('z')
ax2[2].set_ylim(-5, 2)

handles, labels = ax2[2].get_legend_handles_labels()
fig2.legend(handles, labels, loc=(0.3, 0.75), fancybox = False, 
            frameon = False, ncols = 2)
fig2.suptitle('Evolution of coordinates during gradient descent', y = 0.99)

fig2.tight_layout()

优化过程中 x、y 和 z 坐标的演变。图像由作者提供。

对称情况中,y 和 z 收敛到 0,而在偏移位置初始化的优化则迅速沿 z 轴移动。一切如预期。

现在我们可以在表面上可视化轨迹了。

在优化表面上绘制梯度下降的轨迹

起初,我认为这部分只是将结果绘制在我们为表面使用的相同坐标轴 ax1 上。不幸的是,这样做不起作用。在 Matplotlib 中,我们需要做更多的工作

我在图形中添加了新的坐标轴,并将它们设置为与 ax1 具有相同的视角和位置。然后我绘制了轨迹线。

最重要的部分是使用 set_zorder 手动将线条放置在表面之上。其次,我们需要使用 set_axis_off 使新的坐标轴不可见。

newax = fig1.add_axes(ax1.get_position(), projection='3d',
                     xlim = ax1.get_xlim(),
                     ylim = ax1.get_ylim(),
                     zlim = ax1.get_zlim(),
                     facecolor = 'none',)
newax.view_init(azim= 120, elev= 37)
newax.set_zorder(1)
ax1.set_zorder(0)

newax.plot3D(data_sym[:,0], data_sym[:, 1], data_sym[:, 2], 
             c='blue', alpha = 0.7)
newax.plot3D(data_off[:, 0], data_off[:, 1], data_off[:, 2],
             c='red', alpha = 0.7)

newax.plot3D(data_sym[0, 0], data_sym[0, 1], data_sym[0, 2],
               ms = 2.5, c='black', marker='o')
newax.plot3D(data_off[0, 0], data_off[0, 1], data_off[0, 2],
               ms = 2.5, c='black', marker='s')
newax.set_axis_off()
sq = u'■'
cir = u'●'
ax1.text2D(0.05, 0.95, f'Initilization\n'\
                      f'{sq}: x = {x_off}, y = {y_off}\n'\
                      f'{cir}: x ={x_sym}, y = {y_sym}', transform=ax1.transAxes)
fig1

梯度下降轨迹的静态表示。图片由作者提供。

太好了!我们的图表有意义了,所以我们可以开始创建动画版本。

创建动画图形

还记得过去电影是如何通过静态图像序列制作的吗?

Eadweard Muybridge,Waugsberg — 来源不明,见于维基百科公共领域

好吧,它们的处理方式依然相同。

这就是为什么我在每次迭代时将更新后的 x、y 和 z 坐标存储在数组中的原因。

我们需要找到一种方法来依次绘制优化过程中的多个“图像”或帧。

我对如何实现这一点毫无头绪,所以开始寻找做类似事情的代码。

我在 Medium 上找到了一篇由 Zack Fizell 撰写的精彩文章,他展示了 如何在 Python 中创建动画图。

幸运的是,Matplotlib 中有一个专门的类 animation 来实现这一点。

Zack 解释了代码的所有细节,你可以在他的文章中查看。对我而言,最重要的部分是使用

from matplotlib import animation

# Define Aimation function
def animate_func(num):
  ... # Plot trajectories, index arrays with incremental num

# Plotting the Animation
fig = plt.figure()
ax = plt.axes(projection='3d')
line_ani = animation.FuncAnimation(fig, animate_func, interval=100,   
                                   frames=numDataPoints)
plt.show()

依次绘制轨迹的所有步骤。

很简单,对吧?

animation.FuncAnimation 的作用是更新我们传入的图形,使用我们定义的任何函数,在 Zack 的案例中是 animate_func。该函数将 frames 参数作为输入,并被调用 frames 次。

图像序列被存储为 [TimedAnimation](https://matplotlib.org/stable/api/_as_gen/matplotlib.animation.TimedAnimation.html#matplotlib.animation.TimedAnimation) 对象,图像之间有一个时间 interval(以毫秒为单位)。

所有这些在 官方 matplotlib 文档 中有很好的解释。

在我们的案例中,我们需要在函数中包含所有的解决方法,以避免每次更新轨迹线时新轴与表面重叠。

这是我定义更新函数的方式:

def descent_animation(num):
    # Clear the axes where we are plotting the tracjectories
    newax.clear()

    # Manually adjust the order of the axes
    newax.set_zorder(1)
    ax.set_zorder(0)

    # Hide the axes in the front plane
    newax.set_axis_off()

    # Plot new frame of trajectory line for the symmetry case
    newax.plot3D(data_sym[:num+1, 0], data_sym[:num+1, 1],
                 data_sym[:num+1, 2], c='blue', alpha = 0.7)
    # Updating Point Location
    newax.scatter(data_sym[num, 0], data_sym[num, 1], data_sym[num, 2],
               s = 10, c='blue', marker='o', edgecolor = 'k', linewidth = 0.5)
    # Adding Constant Origin
    newax.plot3D(data_sym[0, 0], data_sym[0, 1], data_sym[0, 2],
               ms = 2.5, c='black', marker='o')

    # Plot new frame of trajectory line for the offset case
    newax.plot3D(data_off[:num+1, 0], data_off[:num+1, 1],
                 data_off[:num+1, 2], c='red', alpha = 0.7)
    # Updating Point Location
    newax.scatter(data_off[num, 0], data_off[num, 1], data_off[num, 2],
               s = 10, c='red', marker='o', edgecolor = 'k', linewidth = 0.5)
    # Adding Constant Origin
    newax.plot3D(data_off[0, 0], data_off[0, 1], data_off[0, 2],
               ms = 2.5, c='black', marker='s')

    # Setting Axes Limits and view angles
    newax.set_xlim3d([-1, 1])
    newax.set_ylim3d([-1, 1])
    newax.set_zlim3d([-1, 1])
    newax.view_init(azim= 120, elev= 37)

最后,为了创建动画,我创建了一个新的图像,并重复添加第二对坐标轴的过程。这次我只绘制了表面,因为轨迹将由 descent_animation 函数处理。

fig = plt.figure(figsize = (4, 3), dpi = 300)
ax = fig.add_subplot(111, projection='3d')

plot_args = {'rstride': 2, 'cstride': 2, 'cmap':"coolwarm",
            'linewidth': 0.01, 'antialiased': False,
            'vmin': -1, 'vmax': 1, 'edgecolors':'k'}
x, y = np.mgrid[-1:1:81j, -1:1:81j]

# Plot surface
ax.plot_surface(x, y, f([x,y]), **plot_args)
ax.view_init(azim= 120, elev= 37)
ax.set_xlim3d([-1, 1])
ax.set_ylim3d([-1, 1])
ax.set_zlim3d([-1, 1])

ax.w_xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
ax.w_yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
ax.w_zaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))

# Add second pair of axes
newax = fig.add_axes(ax.get_position(), projection='3d',
                    xlim = ax.get_xlim(),
                    ylim = ax.get_ylim(),
                    zlim = ax.get_zlim(),
                    facecolor = 'none',)
newax.view_init(azim= 120, elev= 37)

# Manually adjust the order of the axes
newax.set_zorder(1)
ax.set_zorder(0)

# Hide axes in the front plane
newax.set_axis_off()

# Add some text to distinguish the two initialization points
sq = u'■'
cir = u'●'
ax.text2D(0.05, 0.95, f'Initilization\n'\
                      f'{sq}: x = {x_off}, y = {y_off}\n'\
                      f'{cir}: x ={x_sym}, y = {y_sym}', 
          transform=ax.transAxes,
          fontsize = 8)

# Plotting the Animation
line_ani = animation.FuncAnimation(fig, descent_animation, interval=100,
                                   frames= n_iters + 1)
plt.show() 

最终结果:使用 Python 创建的梯度下降动画。图像由作者提供。

保存和显示动画

我们可以通过几行代码保存动画。

filename = "gradient_func.gif"
writergif = animation.PillowWriter(fps=500)
line_ani.save(filename, writer=writergif)

PillowWriter 方法适用于存储 Gif 文件。

Gif 文件的问题在于,如果你有许多帧并且希望保持良好的图像质量,你可能会得到一个不必要地大的文件。

为了优化文件大小,我们可以调整传递给 FUncAnimationinterval 参数,并指定创建和保存图像时希望使用的 FPS 和 DPI。否则,我们可以使用 EZGif 的优化 来减少文件大小。

另一种选择是将其存储为 MP4 文件,或者更好的是,使用 [FFMpegWriter](https://matplotlib.org/stable/api/_as_gen/matplotlib.animation.FFMpegWriter.html#matplotlib.animation.FFMpegWriter) 存储为 WebM 文件。

正如我之前讨论过的,如果我们想在网页上显示图像,使用 WebM 替代 Gif 文件可以显著提高速度,或者仅仅是减少文件大小。不幸的是,Medium 仍然不支持 WebM。

最后,如果你正在使用 Jupyter Notebooks 或在 Google Colab 上工作,你可以使用以下方法显示动画图像:

from IPython.display import Image
Image(open(filename,'rb').read())

附加说明

这种类型的动画图像非常适合比较不同类型优化算法的轨迹。

下面,你可以看到随机梯度下降 (SGD)—我在这篇文章中展示的—与一种更高级的方法 Adam(源于自适应矩估计) 的性能比较,Adam 目前是训练人工神经网络的行业标准之一,因为它的鲁棒性和速度。

两者都在点 x = 0.2, y = 1e-8 初始化,以提供具有挑战性的情况。与精确对称线 (y = 0) 的偏差非常小,且小的 x 坐标在“下降”时提供了很少的动量积累空间。你可以看到,相较于 SGD,Adam 在经过很少的迭代后就能够打破对称性。

如果你有兴趣了解这两种算法之间的差异,请查看我关于 梯度下降优化的高级方法 的文章。

SGD 与 Adam 的比较。图片由作者提供。

所以这就是全部内容了。

感谢阅读!希望你能从我分享的内容中找到有用的信息,并或许将其应用于自己的项目。

如果你对梯度下降和神经网络感兴趣,我推荐你阅读我关于这些主题的其他文章。

[## 深度学习笔记:梯度下降

神经网络如何“学习”

medium.com [## 深度学习笔记:前馈人工神经网络

基本概念,解释如上

medium.com

我正在撰写一系列关于深度学习和机器学习的文章,但我也会发布内容,分享我在写这些文章时学到的其他知识——这篇文章就是一个例子。因此,不仅是深度学习的理论和应用,还有像这样的 Python 编程技巧。

一定要关注我并订阅我的邮件,以便在我发布新文章时得到通知!

创建一个具备维基百科知识的 LLaMa 2 代理

原文:towardsdatascience.com/creating-a-llama-2-agent-empowered-with-wikipedia-knowledge-c048c7c63ef8?source=collection_archive---------2-----------------------#2023-09-26

利用检索增强生成技术为 LLaMa 2 提供支持,从维基百科中查找和使用信息。

Gabriele Sgroi, PhDTowards Data Science Gabriele Sgroi, PhD

·

关注 发表在 Towards Data Science ·11 min 阅读·2023 年 9 月 26 日

--

图片由 Lysander YuenUnsplash 提供

引言

大语言模型(LLMs)是人工智能领域的热门趋势之一。它们展示了令人印象深刻的文本生成能力,从与人类用户对话的能力到编写代码。开源 LLMs 的兴起,如 LLama、Falcon、Stable Beluga 等,使得它们的潜力对广泛的 AI 社区可用,同时也得益于开发更小、更高效模型的重点,这些模型可以在消费级硬件上运行。

促成大语言模型(LLMs)成功的关键因素之一是革命性论文Attention Is All You Need中引入的著名变换器架构。最先进的 LLMs 之所以表现出色,是因为将这一架构扩展到数十亿参数,并在包含万亿个标记的数据集上进行训练。这种预训练产生了强大的基础模型,能够理解人类语言,并可以进一步调整以适应特定的应用场景。

大语言模型的预训练是在自监督的方式下进行的。它需要大量的文本语料库,但不需要人工标注。这使得可以将训练扩展到以自动化方式创建的大规模数据集。将输入文本转化为标记序列后,预训练的目标是预测序列中每个标记的概率,条件是所有先前的标记。通过这种方式,训练完成后,模型能够通过自回归地生成文本,每次根据到目前为止采样的所有标记来采样一个标记。大语言模型仅凭借这种预训练已经展示了令人印象深刻的语言能力。然而,通过根据从训练数据中学到的条件概率采样标记,生成的文本通常与人类偏好不一致,LLMs 在遵循特定指令或人类意图方面存在困难。

对齐大语言模型(LLMs)生成的文本与人类偏好的一个重要进展是通过人类反馈强化学习(RLHF)实现的。这项技术是最先进的聊天模型,如 ChatGPT 的核心。通过 RLHF,在初始的自我监督预训练阶段之后,大语言模型会进一步通过强化学习算法进行微调,以最大化一个基于人类偏好的奖励。为了获得奖励函数,通常会训练一个辅助模型来学习一个反映人类偏好的标量奖励。这样,可以将实际需要的人类标注数据量保持在最低。奖励模型的训练数据包括由人类根据其偏好对生成的文本进行排名的文本。模型的目标是对排名较高的文本预测更高的奖励。通过最大化反映人类偏好的奖励来训练 LLM 应能生成更符合人类意图的文本。实际上,通过人类反馈强化学习微调的大语言模型已被证明能够更好地遵循用户指令,同时也更少有毒,更真实。

检索增强生成

大语言模型的一个典型缺点是它们离线训练,因此没有关于训练数据收集后发生的事件的信息。同样,它们不能使用训练数据中未包含的特定知识。这对于特定领域可能是个问题,因为用于训练 LLMs 的数据通常来自一般领域的语料库。绕过这些问题的一种方法是检索增强生成(RAG),而无需昂贵的微调。RAG 通过将外部文本信息增强到输入的提示中来工作。这通常是通过与当前提示相关的外部数据源进行检索来实现的。在实践中,第一步涉及将提示和外部文本转换为向量嵌入。后者可以通过汇聚一个训练过的转换器编码器模型(如 BERT)的输出获得,该模型将具有相似意义的文本映射到根据适当度量接近的嵌入中。在长文本的情况下,可以将其拆分为单独嵌入的块,从而检索出最相关的段落。接下来,检索到的文本的嵌入与提示嵌入最接近。经过适当格式化的提示和检索文本的连接将作为输入提供给语言模型。

通过检索增强生成(RAG),模型可以访问训练期间不可用的信息,并基于选定的文本语料库来回答问题。RAG 还使得检查模型用于回答的来源成为可能,从而使人类用户对模型输出的评估更加直接。

在这篇博客文章中,我将解释如何创建一个简单的代理,能够基于从维基百科检索到的内容来回答问题,以展示 LLMs 寻求和使用外部信息的能力。给定用户的提示,模型将搜索适当的维基百科页面,并基于其内容进行回答。我在这个 GitHub 仓库中提供了完整的代码。

增强了维基百科内容的 Llama 2 代理

在本节中,我将描述创建一个简单的 Llama 2 代理的步骤,该代理基于从维基百科检索到的信息来回答问题。具体来说,代理将…

  • 创建适当的查询以搜索与用户问题相关的维基百科页面。

  • 从维基百科找到的页面中检索出内容与用户问题最相关的页面。

  • 从检索到的页面中提取与用户提示最相关的段落。

  • 根据页面摘录回答用户的问题。

请注意,更一般来说,模型可能会收到一个增强的提示,包含最相关页面的完整内容或来自不同顶级页面的多个摘录,这些页面按照与用户提示的相关性进行排名。虽然这可以提高模型响应的质量,但它会增加所需的内存,因为它将不可避免地导致更长的提示。为了简单起见,并且为了在免费的 Google Colab GPU 上运行一个最小的示例,我限制了代理只使用来自最相关文章的几个摘录。

现在让我们深入探讨各个步骤的详细信息。代理需要执行的第一步是创建一个合适的搜索查询,以从维基百科中检索包含回答用户提示信息的内容。为此,我们将提示一个 Llama 2 聊天模型,要求它返回表示用户提示的关键词。在讨论具体的提示之前,我将简要回顾 Llama 2 聊天模型的通用提示格式。

在 Llama 2 聊天模型的训练过程中使用的模板具有以下结构:

<s>[INST] <<SYS>>
{{ system_prompt }}
<</SYS>>

{{ user_message }} [/INST]

{{ system prompt}}指定了聊天模型对后续提示的行为,并可以用于将模型响应适应于不同任务。{{user_message}}是模型需要回答的用户提示。

回到获取维基百科搜索查询的问题,我们的代理将使用带有以下提示的 Llama 2 模型:

<s>[INST] <<SYS>>
You are an assistant returning text queries to search Wikipedia articles containing relevant information about the prompt. Write the queries and nothing else.
Example: [prompt] Tell me about the heatwave in Europe in summer 2023 [query] heatwave, weather, temperatures, europe, summer 2023.
<</SYS>>

[prompt] {prompt} [/INST] [query]

{prompt} 在生成之前会被用户的输入替换。系统提示中提供的示例旨在利用模型的上下文学习能力。上下文学习指的是模型基于作为提示一部分提供的几个演示示例解决新任务的能力。通过这种方式,模型可以了解到我们希望它在文本 [query] 后提供与提供的提示相关的关键词,并用逗号分隔。后者用于作为分隔符,以区分示例中的提示和答案,并且还用于从模型输出中提取查询。它已作为输入的一部分提供,因此模型只需要生成它之后的内容。

一旦从模型输出中获得查询,就可以用来搜索维基百科,检索返回页面的元数据和文本。在文章中附带的代码中,我使用了 wikipediapackage,这是一个简单的 Python 包,它封装了 MediaWiki API,用于从维基百科搜索和检索数据。

在从搜索结果中提取文本后,选择与原始用户提示最相关的页面。这将重新对齐检索到的信息与原始用户的提示,可能消除由模型生成的搜索查询引起的偏差。为此,用户的提示和搜索结果页面的摘要都会被嵌入并存储在向量数据库中。然后检索与用户提示嵌入最接近的文章。我使用了句子变换器 all-MiniLM-L6-v2 模型作为嵌入模型,并使用了 langchain 包提供的 FAISS 向量数据库集成。

在找到相关的维基百科页面后,由于将其整个文本添加到提示中可能需要大量内存(或超过模型的上下文长度令牌限制),我们的代理将找到最相关的摘录以补充提示。这是通过首先将页面的文本拆分成块,然后像之前一样,将它们嵌入到向量空间中,并检索与提示嵌入最接近的块来完成的。我再次使用了 all-MiniLM-L6-v2 模型来嵌入这些块,并使用 FAISS 向量数据库来存储和检索它们。

现在我们已经从文章中获得了检索到的段落,我们可以将它们与用户的提示结合起来,输入到 Llama 2 模型中以获得答案。输入的模板如下:

<s>[INST] <<SYS>>
You are a helpful and honest assistant. Your answers should not include any harmful, unethical, racist, sexist, toxic, dangerous, or illegal content.
You have retrieved the following extracts from the Wikipedia page {title}:
{extracts}.
You are expected to give truthful answers based on the previous extracts. If it doesn't include relevant information for the request just say so and don't make up false information.
<</SYS>>

{prompt} [/INST]

在生成之前,{prompt} 被替换为用户提示,{title} 被替换为维基百科页面的标题,{extracts} 被替换为提取的段落。也可以提供几个示例,以再次利用模型的上下文学习能力,但这会显著增加提示的长度,从而增加内存需求。

现在让我们检查模型是否能够使用在训练过程中未包含的最新信息来回答问题,这些信息是从维基百科中提取的。以下示例是使用在 Google Colab 免费层 GPU 上运行的最小 LLaMa 2 聊天版本(拥有 70 亿参数)获得的。

我给模型提供了以下提示:

prompt: "Tell me about the earthquake in Marrakesh"

模型创建了以下查询以在维基百科上搜索:

"earthquake, Marrakesh, Morocco, recent earthquakes, seismic activity."

在提取了来自最相关页面的段落后,模型返回了以下回答:

The earthquake that occurred in Marrakesh on September 8, 2023, was a
devastating event that caused widespread destruction and loss of life in
the region. According to the information provided on Wikipedia, the
earthquake had a moment magnitude of 6.8-6.9 and a maximum Mercalli
intensity of VIII (Severe), making it the strongest instrumentally recorded
earthquake in Morocco and the deadliest since the 1960 Agadir earthquake.
The earthquake's epicentre was located approximately 45 miles (73.4 km)
southwest of Marrakesh, near the town of Ighil and the Oukaïmeden ski
resort in the Atlas Mountains. The earthquake occurred as a result of
shallow oblique-thrust faulting beneath the mountain range. According to
the World Health Organization, around 300,000 people from Marrakesh and the
surrounding areas were affected by the earthquake, including 100,000
children. The earthquake resulted in at least 2,946 deaths, with most
occurring outside of Marrakesh. It is important to note that the
information provided on Wikipedia is based on available data and may not be
exhaustive or up-to-date. If you have any specific questions 

该代理还返回了生成答案所用页面的元数据和摘录,允许用户检查其准确性,并通过阅读原始页面深入了解细节。以下是之前回答的元数据

RETRIEVED WIKIPEDIA PAGE: 
title: 2023 Marrakesh–Safi earthquake 
url: https://en.wikipedia.org/wiki/2023_Marrakesh%E2%80%93Safi_earthquake
Retrieved extracts: 
Extract_0:Earthquake aftermath in Marrakesh and Moulay Brahim  Extract_1:.
Damage was widespread, and historic landmarks in Marrakesh were destroyed.
The earthquake was also felt in Spain, Portugal, and Algeria.It is the
strongest instrumentally recorded earthquake in Morocco, the deadliest in
the country since the 1960 Agadir earthquake and the second-deadliest
earthquake of 2023 after the Turkey–Syria earthquake. The World Health
Organization estimated about 300,000 people from Marrakesh and the
surrounding areas were affected, including 100,000 children  Extract_2:On 8
September 2023 at 23:11 DST (22:11 UTC), an earthquake with a moment
magnitude of 6.8–6.9 and maximum Mercalli intensity of VIII (Severe) struck
Morocco's Marrakesh–Safi region. The earthquake's epicentre was located
73.4 km (45.6 mi) southwest of Marrakesh, near the town of Ighil and the
Oukaïmeden ski resort in the Atlas Mountains. It occurred as a result of
shallow oblique-thrust faulting beneath the mountain range. At least 2,946
deaths were reported, with most occurring outside Marrakesh

结论

在这篇文章中,我解释了如何创建一个简单的代理,它可以通过在维基百科上搜索并基于检索到的页面来回应用户的提示。尽管它很简单,但即使是最小的 Llama 2 7B 模型也能提供最新且准确的回答。该代理还返回了生成回答所用页面的摘录,允许用户检查模型提供信息的准确性,并通过阅读完整的原始页面深入了解细节。

维基百科是展示 LLM 代理寻找和使用训练数据中不存在的外部信息能力的有趣场所,但同样的方法也可以应用于其他需要外部知识的场景。例如,这适用于需要最新答案的应用、需要训练数据中不存在的特定知识的领域,或从私有文档中提取信息的情况。这种方法还突显了 LLM 与人类合作的潜力。模型可以快速返回有意义的回答,通过从非常大的外部知识库中搜索相关信息,而人类用户可以检查模型答案的有效性,并通过检查原始来源深入了解问题。

对本文中描述的智能体的直接改进可以通过结合来自不同页面的多个摘录来获得,以便向模型提供更多的信息。实际上,对于复杂的提示,提取多个维基百科页面的信息可能会很有用。由于上下文的增加导致内存需求的增加,可以通过实现量化技术来部分抵消这种增加,例如GPTQ。通过让模型在给出最终答案之前,能够在搜索结果和检索到的内容上进行推理,结果可能会进一步改善,例如可以遵循论文中描述的 ReAct 框架ReAct: Synergizing Reasoning and Acting in Language Models。这样,例如,可以构建一个模型,迭代地从不同页面中收集最相关的片段,丢弃不需要的片段,并结合来自不同主题的信息。

谢谢阅读!

在 Excel 中使用 VBA 创建项目更新跟踪器

原文:towardsdatascience.com/creating-a-project-updates-tracker-in-excel-using-vba-c085e6fe05a8?source=collection_archive---------7-----------------------#2023-09-02

三个简单步骤来跟踪 Excel 中的项目更新并记录它们

Himalaya Bir ShresthaTowards Data Science Himalaya Bir Shrestha

·

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

--

在我们的工作中,我们经常参与多个项目。每个项目涉及若干任务或子任务。跟踪这些任务和项目的状态是一种良好的项目管理实践。这些项目任务或更新可以用于我们自己的知识,也可以在项目会议中共享信息。市场上有许多免费的或商业的项目管理工具,具有类似的目的。然而,我想使用 Visual Basic Applications (VBA) 创建一个简单的 Excel 工具以供自己使用。

VBA 的功能非常广泛。它可以用于自动化数据处理、数据分析和数据可视化。这使得在 Excel 中处理和管理大数据集非常方便。关于 VBA 的一个事实是,VBA 代码库不会像 Python 中的不同包一样定期更新。这在不同的上下文中可以被视为优点和缺点。然而,其中一个优点是,一旦你学习了 VBA,你可以在未来继续使用相同的知识。你不需要不断更新 VBA 的新版本或新功能,因为没有(除非微软决定引入新功能)。

在我之前的帖子中,我使用 VBA 进行了时间序列重采样。

## 通过 Excel VBA 运行 Python — 时间序列重采样的案例

对使用 VBA、Python 和通过 Python 进行的太阳辐射时间序列重采样的全面评估...

towardsdatascience.com

在这篇帖子中,我将分享我如何使用 Excel 中的 Visual Basic Applications(VBA)为自己创建一个简单的项目更新跟踪工具的三步方法。让我们开始吧。

图片由Brands&People提供,来源于Unsplash

目标

我旨在创建一个用于跟踪项目更新并记录它们的应用程序。我想有一个工作表,用于输入每个任务的新更新。我还想跟踪记录此信息的时间。通过点击一个按钮,我希望将每个新更新及其记录时间移动到一个单独的工作表中进行日志记录。

为此,我创建了一个包含两个不同工作表的 Excel 文件。第一个工作表名为ProjectTasksTracker,第二个工作表名为Logbook。这两个工作表都包含相同的标题行,包含六列:DateTime、Project、Tasks、Responsible Staff、Status 和 Updates。

我在 DateTime 列中使用=NOW()函数以获取实时信息。在 Status 列中,我允许下拉菜单中有三个选项:Started、In Progress 和 Complete。我还创建了一个名为Update Logbook的按钮,以便自动将所有信息记录到 Logbook 工作表中而不产生重复。ProjectTasksTracker 工作表如下所示:

ProjectTasksTracker 工作表的结构。作者插图。

编码步骤

我从在 VBA 模块内创建一个子例程开始。

  1. 第一步涉及为文件定义工作簿对象wb,以及为ProjectTasksTracker工作表和Logbook工作表分别定义两个工作表对象ws1ws2。代码如下:
'Define workbook and two worksheets.
Dim wb As Workbook
Dim ws1 As Worksheet 'Project tracker worksheet
Dim ws2 As Worksheet 'Logbook worksheet

Set wb = ThisWorkbook
Set ws1 = ThisWorkbook.Sheets("ProjectTasksTracker")
Set ws2 = ThisWorkbook.Sheets("Logbook")

第二步涉及编写代码以计算两个工作表中的行数和列数。这也可以手动完成。然而,由于在输入项目更新时行数可能会变化,因此此过程会更新。列数保持固定(6),以保持两个工作表的结构一致。不过,也为了演示目的进行了编码。

在下面的代码片段中,lr1基于列 A 计算工作表ws1中的行数。lc1基于第 1 行计算同一工作表中的列数。

'Count the number of rows and columns in ProjectTasksTracker sheet
Dim lr1, lc1 As Integer
lr1 = ws1.Cells(Rows.Count, “A”).End(xlUp).Row 
lc1 = ws1.Cells(1, Columns.Count).End(xlToLeft).Column 

注意: 在使用宏时,可以使用对特定单元格的引用。这在处理可能发生变化的数据集时很有用。例如,我创建了一个命名范围Updates,以引用ProjectTasksTracker工作表中的单元格 F1。如果在其前面添加了一列,Updates 将引用单元格 G1。

Updates命名范围在代码中以update_cell引用,如下所示。它所属的列号用update_column引用,而用字母表示的列号由update_col给出。

Dim update_cell As Range
Set update_cell = ws1.Range(“Updates”)

Dim update_column As Integer
update_column = update_cell.Column

Dim update_col As String
update_col = Chr(update_column + 64)
MsgBox "Update column belongs to: Column " & update_col

MsgBox 用于显示根据上述代码的更新位置参考。作者插图。

在下一步的代码中,为方便起见,我们将直接引用列号为 6 的更新列。

第三步是此过程中最重要的一步。在此步骤中,我遍历了ProjectTasksTracker工作表中的每一行(除标题行和日期时间列外),并执行以下操作,分为三个子步骤:

ProjectTasksTracker工作表的每一行中,我检查了每个任务的更新列是否为空。如果ProjectTasksTracker中的某一行有更新,则我计算了Logbook工作表中的行数,并将其赋值为一个名为lr2的整数。此外,我声明了一个布尔数据类型,名为valuesMatch,默认赋值为 False。

b. 接下来,我创建了一个嵌套循环,用于遍历Logbook表中的每一行,并检查ProjectTasksTracker表中的每一列的内容(定义为范围rg1)是否与Logbook表中任何一行的每一列的内容(定义为范围rg2)匹配。如果rg1rg2的任何值之间没有匹配,则意味着ProjectTasksTracker表中特定行的更新之前尚未记录到Logbook表中。valuesMatch将保持为 False。如果ProjectTasksTracker表中的某一行内容与Logbook表中的任何一行匹配,则意味着该行已被记录。在这种情况下,valuesMatch的值将更改为 True。

c. 如果valuesMatch在两个 for 循环结束时为 True,则不会进行进一步处理。如果valuesMatch在两个 for 循环结束时为 False,则ProjectTasksTracker表中的行(包括 Datetime 列)将被复制并粘贴到Logbook表中。

步骤 3a、b 和 c 已在下面的代码片段中编写:

演示

下面的图表显示了截至 2023 年 8 月 20 日 23:32 的ProjectTasksTracker表中的更新。

2023 年 8 月 29 日ProjectTasksTracker表中更新的初始视图。由作者插图。

这些更新已经在 2023 年 8 月 20 日当天记录到了Logbook表中,如下所示。

截至 2023 年 8 月 20 日的Logbook表中的更新。由作者插图。

接下来,在 2023 年 8 月 29 日 23:38,我对ProjectTasksTracker表进行了更改,用红色标出(对前两行进行了更改并添加了最后一行)。然后我点击了Update Logbook按钮,该按钮分配了上述Coding Steps部分描述的宏。

2023 年 8 月 29 日ProjectTasksTracker表中所做的更改。由作者插图。

这些新更改随后被记录到Logbook表中。底部用红色标记的行是 2023 年 8 月 29 日的更改。之前记录的其他更新保持不变。

新的更新记录在Logbook表中。之前的更新保持不变。

结论

在这篇文章中,我描述了一些编码步骤,以在 Excel 中创建一个简单的跟踪器来输入项目任务更新并记录它们。如果在ProjectTasksTracker表中进行了一些更改或添加并运行宏,这将把这些更新复制并粘贴到Logbook表中。然而,如果ProjectTasksTracker表没有更改,点击按钮后两个表中的更新将保持不变。

也可以在日志簿工作表的末尾创建额外的功能,如按特定顺序对行进行排序。同时,也可以创建一个新文件来记录项目更新,而不是在同一 Excel 文件的单独工作表中记录。在这种情况下,代码中的工作簿和工作表目标需要重新定义。为了保持简洁,这些步骤未包含在本文中。本文中使用的代码和启用宏的 Excel 文件可在此 GitHub repository中找到。感谢阅读!

创建一个透明的数据环境与数据血统

原文:towardsdatascience.com/creating-a-transparent-data-environment-with-data-lineage-12e449597f6

列级血统在你的技术栈中的好处

Madison SchottTowards Data Science Madison Schott

·发布于 Towards Data Science ·5 分钟阅读·2023 年 4 月 11 日

--

图片由 Bozhin Karaivanov 提供,来源于 Unsplash

当我编写数据转换时,我经常会注意到不一致的地方。也许数据源中的某一列没有遵循我制定的命名规则,或者时间戳没有被转换成正确的数据类型。我会在发现这些问题时立即进行更改,而不是等到后续再去修复。

然而,第二天,我发现生产数据管道出现故障。由于我昨天做的那个“简单”的名称更改,几个下游的数据模型出现了问题。现在,我需要急忙更改所有下游数据模型中对该列的引用。

不幸的是,如果你没有一个能够显示数据流向的工具,这种情况可能很常见。你被迫被动等待,直到某些东西出故障才知道需要哪些更改。幸运的是,数据血统工具可以帮助我们避免这种情况,并对依赖关系采取主动措施。

什么是数据血统?

数据血统 展示了数据在通过管道移动时所遵循的路径。它突出了数据的来源、如何转换以及包含在哪些可视化中。

数据血统有助于了解特定模型中使用了哪些数据源,以及在这个过程中是否引用了其他模型。你可以利用它快速发现依赖关系。它将透明度融入你的技术栈中,这是生成高质量数据的关键特性。

如何利用数据血缘建立透明度

当正确使用时,数据血缘有助于在你的业务中建立透明度。以下是我推荐使用数据血缘工具的三大理由。

数据血缘准确显示生产模型中使用了哪些数据源。

数据血缘工具非常适合显示哪些数据模型依赖于哪些数据源。 列级血缘工具 更加强大,因为它们显示了上游的确切列如何生成下游数据模型中的列。如果列在不同的数据源和模型中被重命名,这一点尤其有用。

列级血缘工具在数据工程师进行影响源数据摄取的更改时特别有用。他们可以参考下游数据模型的血缘,查看哪些模式更改会对业务产生重大影响。这将使他们能够与分析工程师合作,最小化业务用户的停机时间。

分析工程师可以优先处理最重要的数据源和模型的文档和测试。如果没有血缘工具,你只能猜测哪些会有最大影响。有了这些工具,你可以专注于驱动最多业务决策的数据。

血缘工具对业务团队也很有帮助,因为它们允许他们查看数据团队最常使用哪些数据源。我看到许多过时的 Google Sheets 堆积在数据环境中,因为没有人知道它们是否重要。能够看到从源到洞察的表的血缘将使业务用户能够权衡它们的重要性,同时保持他们的数据资产清洁和最新。

数据血缘使你能够看到分析师在仪表板和报告中使用的模型。

数据血缘工具让你洞察哪些数据源和数据模型被用来支持业务使用的关键仪表板和报告。这将帮助分析师和数据工程师更好地了解每天用于做出关键业务决策的模型。当每个人都了解最关键的数据源时,像模式更改、数据类型转换和重命名这样的事情可以变得更加战略性。这帮助每个人了解完整的影响和潜在的停机时间。

当数据和工程团队了解数据管道变更对业务的影响时,他们更有可能进行有效沟通并积极寻求解决方案。例如,如果数据工程师在没有数据血缘工具的情况下更改了源模式,他们可能不知道这会破坏营销投资回报率仪表板。然而,如果工程师有血缘工具来查看哪些内容依赖于这个源,他们可以通知业务团队停机时间。

开放的沟通有助于建立数据团队与业务团队之间的信任,这对于数据驱动的组织至关重要。当两个团队互相信任时,可以做出更好的决策。业务将对数据堆栈充满信心,并能够轻松验证所见的结果。此外,数据团队可以自信地对流水线进行更改,知道不会破坏任何东西,从而推动业务前进。

数据 lineage 揭示了数据瓶颈。

数据 lineage 工具通过引起对数据从源头到最终产品的流动过程中的瓶颈的关注来优化你的数据流水线。你不再需要 猜测 某些转换为什么要花费如此长的时间,甚至为什么它们会耗费你这么多钱,你可以看到数据的确切流动情况。

例如,你可能会注意到两个数据模型在数据量和查询复杂性面前耗时异常长。你查看这些模型的 lineage,发现它们引用了相同的源数据集。在查看该源数据后,你可以立即识别出问题所在。

Lineage 工具允许你理解不同部分之间的依赖关系及其如何改进。查看你的数据如何流经不同的模型可以让你轻松识别可以优化的领域。这使你能够创建一个更快、更便宜且更可靠的数据流水线。

数据 lineage 作为一种主动工具

当你更新其中一个源数据模型时,你应始终首先参考你的数据 lineage 工具。查看哪些下游数据模型正在使用该源。来自该源的哪些列正在被使用?这时,一个 列级 lineage 工具 就派上用场了,比如 Y42 提供的工具。一旦你了解了数据源的变化如何影响下游模型和仪表盘,你就可以自信地进行更改。

当我重命名数据源中的一列时,我总是使用一个 lineage 工具来查看这一变化如何向下游传播。现在,我再也不会遇到模型计划运行时出现意外的生产流水线失败。

数据 lineage 工具之所以强大,是因为它们为你的数据堆栈、数据团队以及与业务利益相关者的协作带来了透明度。进行更改不再是一个猜测游戏,因为你可以提前测量其即时影响。

使用 AWS 和 Apache Airflow 创建 YouTube 数据管道

原文:towardsdatascience.com/creating-a-youtube-data-pipeline-with-aws-and-apache-airflow-e5d3b11de9c2

一种利用云服务和作业调度程序有效管理 YouTube 数据的解决方案

Aashish NairTowards Data Science Aashish Nair

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

--

图片由 Artem Podrez 提供:www.pexels.com/photo/thoughtful-woman-with-earbuds-using-laptop-4492161/

∘ 引言

∘ 问题陈述

∘ 理解数据管道

∘ 理解数据

∘ 第 1 阶段 — 设置 AWS 环境

∘ 第 2 阶段— 使用 Boto3 简化 AWS 操作

∘ 第 3 阶段— 设置 Airflow 管道

∘ 第 4 阶段— 运行管道

∘ 结果分析

∘ 该数据管道的优点

∘ 该数据管道的缺点

∘ 结论

引言

YouTube 已经成为信息、思想和观点交流的主要媒介,每天平均上传 300 万个视频。这个视频流媒体平台始终准备好以其多样化的内容为观众提供新的话题,从严肃的新闻故事到轻松的音乐视频。

尽管如此,由于视频内容的持续涌入,很难判断哪些类型的内容最能吸引变化莫测的 YouTube 观众的注意。

通常,哪些类型的视频会获得最多的点赞和评论?来自不同国家的 YouTube 用户是否偏爱不同类型的内容?对特定内容类别的兴趣是否会随着时间而波动?

回答这些问题需要一种系统的方法来收集、处理和存储 YouTube 数据,以便后续分析。

问题陈述

项目的目标是创建一个数据管道,收集 5 个 YouTube 用户最多的国家(巴西、印度、印尼、墨西哥和美国)的 YouTube 数据。该管道应每日收集数据,以保持信息的最新。

通过管道收集的数据将用于回答以下问题:

  • 各个国家最受欢迎的视频类别是什么?

  • 最受欢迎的视频类别会随时间变化吗?如果会,如何变化?

开发和运行管道将需要大量步骤。因此,项目将分为 4 个阶段:

1. 设置 AWS 环境

2. 利用 boto3 促进 AWS 操作

3. 设置 Airflow 管道

4. 运行 Airflow 管道

理解数据管道

在项目开始之前,值得讨论管道的设置,其可以通过以下图示表示:

数据管道(由作者创建)

有很多内容,所以让我们来逐一解析。

亚马逊网络服务(AWS)将提供项目所需的所有计算和存储服务,而 Apache Airflow 将调度相关过程以便每天运行。

所有在 AWS 环境中进行的过程将由利用 boto3 的 Python 脚本来实现,boto3 是 AWS 的 Python 软件开发工具包(SDK)。这些脚本将通过 Apache Airflow 的有向无环图(DAG)进行调度,并部署到 EC2 实例中。

存储在 EC2 实例中的 DAG 将使用 YouTube API 拉取数据,并将其存储在 S3 桶中作为 csv 文件。程序随后将使用 Amazon Athena 查询该桶中的数据。查询结果将存储在同一 S3 桶中。

理解数据

接下来,了解 API 提供的 YouTube 数据将对其在管道中需要经过的过程提供一些见解。

API 调用的响应包含了在给定时间内某个国家中最多 50 个最受欢迎的视频的信息。原始数据以 JSON 格式呈现:

{
  "kind": "youtube#videoListResponse",
  "etag": etag,
  "nextPageToken": string,
  "prevPageToken": string,
  "pageInfo": {
    "totalResults": integer,
    "resultsPerPage": integer
  },
  "items": [
    video Resource
  ]
}

欲了解更多信息,请随时访问 YouTube 文档

为了方便后续使用 SQL 查询,这些原始数据将在存储到 AWS 之前转换为 csv 格式。处理后的数据还将省略与此用例无关的特征。

JSON 数据到表格数据的转换总结如下:

JSON 数据转换(由作者创建)

此外,API 调用只能显示单一国家的信息。换句话说,获取 N 个国家的热门视频数据将需要 N 次 API 调用。由于目标是提取 5 个国家的 YouTube 数据,每次迭代将需要进行 5 次 API 调用。

在 AWS S3 中以 csv 文件格式存储数据后,将对数据进行查询,以确定每个国家和日期的最受欢迎的视频类别。

查询输出将包含以下字段:日期、国家、视频类别和视频数量。

查询输出格式(作者创建)

阶段 1 — 设置 AWS 环境

AWS 环境(作者创建)

首先创建/部署存储和查询 YouTube 数据所需的 AWS 资源。

1. 一个 IAM 用户

将为这个项目创建一个 IAM 用户。这个用户将获得使用 Amazon EC2、Amazon S3 和 Amazon Athena 的权限。

用户帐户还将提供访问密钥,这些密钥将用于通过 Python 脚本使用这些资源。

2. EC2 实例

我们将设置一个带有 Ubuntu AMI 的 t2-small 实例。这是 Airflow 流水线将被部署的地方。

实例有一个名为“youtube_kp.pem”的密钥对文件,该文件将用于通过安全外壳(SSH)访问 EC2 实例,并通过安全复制协议(SCP)将文件复制到实例中。

此外,将为实例添加一个入站规则,以便用户可以查看 Airflow Web 服务器(用户界面)。

连接到实例后,将进行以下安装:

sudo apt-get update
sudo apt install python3-pip
sudo apt install python3.10-venv

接下来,将设置一个名为 youtube 的虚拟环境,其中将运行该项目。

python -m venv youtube

之后,将在实例中添加一个名为 airflow 的目录。这里将存储包括 DAG 在内的所有所需文件。

mkdir airflow

2. S3 存储桶

S3 存储桶将存储通过 YouTube API 提取的数据以及执行查询的输出。存储桶将命名为 youtube-data-storage

S3 存储桶(作者创建)

在配置方面需要做的事情很少。存储桶将仅包含两个文件夹,分别名为 dataquery-output,这些文件夹将分别包含提取的数据和查询输出。

S3 存储桶文件夹(作者创建)

3. AWS Athena

接下来,将配置 AWS Athena 以查询存储在 data 文件夹中的数据,并将输出存储在 query-output 文件夹中。默认情况下,它会留下一个没有表的空数据库。

空数据库(作者创建)

首先,在查询编辑器中创建一个名为 youtube 的新数据库。

CREATE DATABASE youtube;

接下来,创建一个名为 youtube_videos 的冰山表。该表的字段应与将加载到 S3 存储桶中的 csv 文件的字段匹配。还应指定将被查询的 csv 文件的位置。

CREATE EXTERNAL TABLE youtube_videos (
  date_of_extraction STRING,
  country STRING,
  video_id STRING,
  video_name STRING,
  channel_id STRING,
  channel_name STRING,
  category STRING,
  view_count INT,
  like_count INT,
  favorite_count INT,
  comment_count INT
)
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.OpenCSVSerde'
WITH SERDEPROPERTIES (
  'separatorChar' = ',',
  'quoteChar' = '\"',
  'escapeChar' = '\\'
)
LOCATION 's3://youtube-data-storage/data/'
TBLPROPERTIES ('skip.header.line.count'='1')

最后,查询结果位置将设置为设置中的 youtube-data-storage 存储桶中的 query-output 子目录。

查询结果位置(由作者创建)

第 2 阶段——利用 Boto3 促进 AWS 操作

AWS SDK(由作者创建)

设置好 AWS 环境后,我们现在可以专注于开发将促进数据传输和处理的脚本。

首先,我们从 YouTube API 拉取数据,将其转换为表格格式,并使用名为pull_youtube_data.py的函数将其存储在 S3 存储桶中,格式为 csv 文件。

注意:记住这些 Python 函数的名称可能会很有帮助,因为这将使在配置 Airflow DAG 时更容易跟进。

此函数的唯一参数是 region_code,这是表示感兴趣国家的两位数字代码。

例如,pull_youtube_data('US') 将返回美国最受欢迎的视频数据。为了获取 5 个国家最受欢迎的视频信息,这个函数需要运行 5 次。

接下来,我们编写一个使用 boto3 分析 S3 存储桶中数据的函数,使用 Amazon Athena。这涉及在 data 文件夹中编写 SQL 查询,并将结果存储在 query-output 文件夹中。

这些步骤在名为 run_query 的函数中执行。

第 3 阶段——设置 Airflow 流水线

使用 Apache Airflow 调度 Python 代码(由作者创建)

在创建了 Python 函数之后,我们可以构建 Airflow 流水线,并安排函数每天运行。这一阶段有很多步骤,请做好准备。

1. 首先,我们编写名为 youtube_dag.py 的 Python 脚本,该脚本实例化 DAG,定义任务,并建立任务之间的依赖关系。

简而言之,DAG 被配置为在运行 run_query 函数之前执行 pull_data 函数 5 次(每个国家一次)。DAG 运行是每天进行的。

现在,我们可以在创建的 EC2 实例中部署 Airflow DAG。

2. 使用 SSH 访问 EC2 实例

ssh -i "youtube_kp.pem" ubuntu@ec2-18-117-190-136.us-east-2.compute.amazonaws.com

3. 进入创建的虚拟环境

source youtube/bin/activate

4. 安装所需的依赖项(包括 Apache Airflow)

pip install -r requirements.txt
python3 -m pip install cryptography==38.0.4

5. 指定 airflow 主目录的位置

在 Ubuntu 上,可以使用以下命令完成:

sudo vim /etc/environment

为了将 Airflow 主目录设置为创建的 airflow 文件夹,请将以下行添加到文本编辑器中:

AIRFLOW_HOME = ‘/home/ubuntu/airflow’

6. 初始化 Airflow 数据库

airflow init db

7. 将 YouTube DAG(及其他依赖项)存储在所需位置

airflow.cfg 文件将指示 DAG 所需的目录以便被检测到。

DAG 文件的位置(由作者创建)

youtube_dag.py 和其他所有 Python 文件都使用安全拷贝(SCP)从本地机器复制到此目录。执行复制的命令格式如下。

scp -i </path/key-pair-name.pem> </path/file_to_copy> <username>@<instance_public_dns>:<destination_path>

执行 SCP 命令后,dags 目录应包含所有必要的文件。

Dags 目录中的文件(由作者创建)

最后,Airflow 应该可以访问创建的 DAG。为确认这一点,我们可以使用以下命令:

airflow dags list

代码输出(由作者创建)

第四阶段——运行管道

最终,我们已经设置了 AWS 资源、Python 脚本和 Airflow DAG!数据管道现在可以运行了!

我们可以使用 Airflow web 服务器查看工作流。

airflow webserver -p 8080

尽管 DAG 被设置为自动运行,但我们可以通过在命令行界面中运行以下命令手动触发第一次迭代:

airflow dags trigger <dag_id>

另外,也可以通过点击 web 服务器中的“播放”按钮来触发运行。

手动运行 DAG(由作者创建)

为了更好地理解工作流的组成部分,我们可以查看 DAG 的图形:

DAG 图形(由作者创建)

如图所示,包含巴西、印度尼西亚、印度、墨西哥和美国最受欢迎视频的 YouTube 数据首先通过 YouTube API 提取并存储在 S3 桶中。在所有国家的数据被摄取后,AWS Athena 运行预定义的查询,以确定每个国家在每个给定日期的最受欢迎类别。

此外,所有任务周围的绿色边缘表示任务已成功运行。

注意:无需每次都手动触发 DAG;它会按照预定义的计划运行,在此情况下是每天运行一次。

检查结果

让我们看看在管道运行两次后(即每个国家两个日期),存储的 CSV 文件和查询输出是什么样的。

S3 桶中的数据目录(由作者创建)

我们可以在 youtube-data-storage 桶的数据文件夹中看到 10 个 csv 文件(每个国家和日期一个)。

S3 桶中的 query-output 目录(由作者创建)

query-output 目录中有一个包含查询输出的 csv 文件。文件内容如下:

SQL 查询输出(由作者创建)

尽管 YouTube 平台上有许多视频类别,但似乎巴西、印度尼西亚、印度、墨西哥和美国的最受欢迎视频仅归入 4 类:“音乐”“娱乐”“人物与博客”“体育”

虽然这是一个有趣的发现,但由于信息仅包括 2 天的内容,因此几乎无法从中得出任何结论。然而,在反复运行 Airflow DAG 后,这些信息将充分捕捉到每个国家的视频观众偏好随时间变化的情况。

本数据管道的优势

在这种管道中收集 YouTube 数据有几个好处。

1. 利用云技术: AWS 为我们提供了安全存储数据的手段,并能够以低廉的费用应对任何突发需求。

2. 自动化: 使用部署的 Airflow 管道,YouTube 数据将被提取、转换、存储和查询,而无需人工干预。

3. 日志记录: Airflow 的 Web 服务器使我们能够访问日志,并检查所有 DAG 运行及所有任务的当前状态。

4. 可修改: 可以在 AWS 环境中添加或移除元素,而不会影响当前管道的操作。如果需要,用户可以轻松添加新的数据源、编写新的查询或启动其他 AWS 资源。

该数据管道的缺点

尽管如此,构建的管道确实存在一些局限性。

1. 通知缺乏: 目前,数据管道没有集成通知服务来警告用户如果 Airflow DAG 中的任务没有按预期运行。这可能会延迟响应时间。

2. 缺乏数据仓库: 该管道目前通过直接查询 S3 存储桶生成报告,该存储桶充当数据湖。这在当前情况下是可行的,但如果要添加其他数据源,管道将缺乏高效执行复杂连接或聚合所需的工具,以便进行后续分析。

结论

照片由 Prateek Katyal 提供,来自 Unsplash

最终,追踪不同地区随时间变化的最受欢迎的 YouTube 内容是一项巨大的工作,这需要持续收集适合分析的数据。虽然获取这些数据的操作可以在本地基础设施上手动执行,但随着数据量的增加和工作流的复杂化,这种方法变得更加低效和不可行。

因此,对于这种用例,利用云平台和作业调度器是值得的,以确保数据的摄取和处理可以自动化。

使用 AWS,我们可以创建一个可扩展且具有成本效益的解决方案,来收集、转换、存储和处理 YouTube 数据。利用 Apache Airflow,我们能够监控复杂的工作流,并在必要时进行调试。

要查看本项目使用的源代码,请访问 GitHub 仓库:

[## GitHub - anair123/Building-a-Youtube-Data-Pipeline-With-AWS-and-Airflow]

目前,您无法执行该操作。您在另一个标签页或窗口中登录了。您在另一个标签页或窗口中注销了……

github.com](https://github.com/anair123/Building-a-Youtube-Data-Pipeline-With-AWS-and-Airflow?source=post_page-----e5d3b11de9c2--------------------------------)

感谢阅读!

使用 Matplotlib 创建信息图

原文:towardsdatascience.com/creating-an-infographic-with-matplotlib-3c5ece11bedc

挪威大陆架上的 Zechstein 组地质岩性变化

Andy McDonaldTowards Data Science Andy McDonald

·发布于Towards Data Science ·14 分钟阅读·2023 年 7 月 3 日

--

挪威大陆架上的岩性变化的径向条形图。图像由作者提供。

创建令人兴奋和引人注目的数据可视化对于数据工作和数据科学家来说至关重要。它使我们能够以简明的形式向读者提供信息,帮助读者理解数据,而不必查看原始数据值。此外,我们还可以使用图表和图形讲述一个引人入胜的故事,解答关于数据的一个或多个问题。

在 Python 世界中,有众多库可以让数据科学家创建可视化,其中许多人在开始数据科学之旅时最先接触的是matplotlib。然而,在使用matplotlib一段时间后,许多人转向其他更现代的库,因为他们认为基础的 matplotlib 图表单调且基础。

通过一点时间、精力、代码以及对matplotlib功能的理解,我们可以将基础且单调的图表转变为更具吸引力和视觉冲击力的内容。

在我过去的几篇文章中,我专注于如何通过各种样式方法来转换单个图表。如果你想进一步探讨如何改进 matplotlib 数据可视化,可以查看我下面的一些早期文章:

这些文章主要集中在单个图表及其样式化。在本文中,我们将关注如何使用 matplotlib 构建信息图。

信息图用于将复杂的数据集转化为引人入胜的视觉叙事,这些叙事既信息丰富又吸引读者。它们通过图表、表格和简洁的文本来视觉化数据。将这些元素结合起来,可以提供一个易于理解的主题或问题概述。

在分享了我上一篇关于 极坐标条形图 的文章后,我被 Russell Forbes 的一条推文提及,显示出可以在 matplotlib 中制作信息图。

所以,基于这个,我对自己说,为什么不尝试用 matplotlib 构建一个信息图呢。

我确实做了。

以下的信息图是上述结果,它是我们在本文中将要重新创建的内容。

使用 matplotlib 创建的示例信息图。图片由作者提供。

请记住,我们将在本文中构建的信息图可能适合于网页使用或包含在演示文稿中。然而,如果我们打算将这些内容纳入报告或在更正式的场合展示,我们可能需要考虑替代的色彩方案和更专业的风格。

信息图的目标

在进行任何数据可视化之前,我们需要了解创建信息图的目的。如果没有这个目的,将很难缩小我们想使用的图表范围和我们想讲述的故事。

在这个例子中,我们将使用一组来自挪威大陆架的井日志岩性测量数据。我们将特别关注以下问题:

Zechstein 组在这个数据集中岩性的变化是什么?

这为我们提供了起点。

我们知道我们在寻找岩性数据和 Zechstein 组的数据。

导入库和加载数据

首先,我们需要导入一些关键的库。

这些库包括 pandas 用于加载和存储数据,numpy 用于进行数学计算以绘制极坐标投影的标签和数据,matplotlib 用于创建图表,以及 adjustText 以确保散点图上的标签不会重叠。

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from adjustText import adjust_text

库导入后,我们需要加载我们的数据集。有关该数据集的来源详细信息包含在本文的底部。

我们将加载的第一个数据集是我在 上一篇文章 中创建的 Zechstein 组岩性组成。

我们可以使用 pandas 的 read_csv() 函数来加载这些数据。

df = pd.read_csv('Data/LithologySummary.csv', index_col='WELL')

当我们查看我们的数据框时,我们可以看到关于 Zechstein 组在每口井中的岩性信息。

Pandas 数据框,包含穿越 Zechstein Group 的八口井的岩性组成。图片由作者提供。

为了帮助读者更好地理解数据,我们最好提供有关钻探井与 Zechstein Group 交汇处的信息。

我们可以通过使用 pd.read_csv() 以相同的方式加载这些数据。然而,这次我们不需要设置索引。

zechstein_well_intersections = pd.read_csv('Data/Zechstein_WellIntersection.csv')

当我们查看这个数据框时,会呈现出以下表格,包含井名、井穿越 Zechstein Group 的 X 和 Y 网格位置。

Pandas 数据框,包含钻探穿越 Zechstein Group 的井的 X 和 Y 网格位置。

使用 Matplotlib 准备和创建信息图

在开始创建任何图形之前,我们需要创建几个包含关于数据的关键信息的变量。这将使绘制图表的过程更容易。

首先,我们将获得所有可能的岩性列表。这是通过将汇总数据框中的列名转换为列表来完成的。

lith_names = list(df.columns)

当我们查看这个列表时,我们会得到以下岩性。

接下来,我们需要决定如何设置信息图中的单独图表。

对于这个数据集,我们有 8 口井,这将用于生成 8 个径向条形图。

我们还希望在同一图形上显示井的位置。所以这给我们 9 个子图。

我们可以通过设置 3 列和 3 行来对图形进行细分。这允许我们创建第一个变量 num_cols,表示列数。

然后,我们可以将行数(num_rows)变量进行泛化,以便可以在其他数据集上重复使用。在这个示例中,它将获取我们拥有的井数(数据框中的行数),并将其除以我们想要的列数。使用 np.ceil 将允许我们将这个数字向上取整,以便在图形上显示所有图表。

# Set the number of columns for your subplot grid
num_cols = 3

# Get the number of wells (rows in the DataFrame)
num_wells = len(df)

# Calculate the number of rows needed for the subplot grid
num_rows = np.ceil(num_wells / num_cols).astype(int)

我们需要声明的下一组变量如下:

  • indexes : 创建一个从 0 到列表中总项数的数字列表。在我们的例子中,这将生成一个从 0 到 7 的列表,涵盖数据集中的 8 种岩性。

  • width : 创建一个基于计算图表中每个条形的宽度的列表,通过将圆的周长除以 rock_names 中的岩石类型数量来完成

  • angles : 创建一个包含每种岩石类型角度的列表

  • colours : 我们希望用来表示每口井的十六进制颜色列表

  • label_loc : 创建一个在 0 和 2 * pi 之间均匀间隔的值列表,用于显示岩石类型标签

indexes = list(range(0, len(lith_names)))
width = 2*np.pi / len(lith_names)
angles = [element * width for element in indexes]

colours = ["#ae1241", "#5ba8f7", "#c6a000", "#0050ae", 
"#9b54f3", "#ff7d67", "#dbc227", "#008c5c"]

label_loc = np.linspace(start=0, stop=2 * np.pi, num=len(lith_names))

使用 Matplotlib 创建信息图

添加径向条形图作为子图

在开始创建信息图之前,我们首先需要创建一个图形对象。这是通过调用 plt.figure() 来完成的。

为了设置我们的图形,我们需要传入一些参数:

  • figsize : 控制信息图的大小。由于行数可能不同,我们可以将行参数设置为行数的倍数。这将防止图表和图形变形。

  • linewidth : 控制图形的边框厚度

  • edgecolor : 设置边框颜色

  • facecolor : 设置图形背景颜色

# Create a figure
fig = plt.figure(figsize=(20, num_rows * 7), linewidth=10,
                 edgecolor='#393d5c', 
                 facecolor='#25253c')

接下来,我们需要定义网格布局。有几种方法可以做到这一点,但在这个例子中,我们将使用GridSpec。这将允许我们指定子图的位置以及它们之间的间距。

# Create a grid layout
grid = plt.GridSpec(num_rows, num_cols, wspace=0.5, hspace=0.5)

我们现在准备开始添加我们的径向条形图。

为了实现这一点,我们需要遍历岩相组成汇总数据框中的每一行,并使用add_subplot()向网格中添加一个轴。由于我们绘制的是径向条形图,我们希望将projection参数设置为polar

接下来,我们可以通过调用ax.bar将数据添加到图表中。在这个调用中,我们传入:

  • angles : 提供条形图在极坐标投影中的位置,也用于定位岩相标签

  • height : 使用当前行的百分比值来设置每个条形图的高度

  • width : 用于设置条形图的宽度

  • edgecolor : 设置径向条形图的边缘颜色

  • zorder : 用于设置条形图在图形上的绘制顺序。在这种情况下,它设置为 2,以便位于图形的顶部层

  • alpha : 用于设置条形图的透明度

  • color : 根据先前定义的颜色列表设置条形图的颜色

然后,我们重复添加条形图的过程,以便为径向条形图添加背景填充。我们可以将高度设置为 100,以便填充整个区域,而不是从表格中设置一个值。

接下来的步骤包括设置标签、副图标题和网格颜色。

对于岩相标签,我们需要创建一个 for 循环,使我们能够将标签放置在极坐标图边缘的正确角度。

在这个循环中,我们需要检查当前角度。如果条形图的角度小于 pi,则从旋转角度中减去 90 度。否则,如果条形图位于圆的下半部分,则将 90 度加到旋转角度中。这将使图表左侧和右侧的标签容易阅读。

# Loop over each row in the DataFrame
for i, (index, row) in enumerate(df.iterrows()):
    ax = fig.add_subplot(grid[i // num_cols, i % num_cols], projection='polar')

    bars = ax.bar(x=angles, height=row.values, width=width, 
                  edgecolor='white', zorder=2, alpha=0.8, color=colours[i])

    bars_bg = ax.bar(x=angles, height=100, width=width, color='#393d5c',
                     edgecolor='#25253c', zorder=1)

    ax.set_title(index, pad=35, fontsize=22, fontweight='bold', color='white')
    ax.set_ylim(0, 100)
    ax.set_yticklabels([])
    ax.set_xticks([])
    ax.grid(color='#25253c')
    for angle, height, lith_name in zip(angles, row.values, lith_names):
        rotation_angle = np.degrees(angle)
        if angle < np.pi:
            rotation_angle -= 90
        elif angle == np.pi:
            rotation_angle -= 90
        else:
            rotation_angle += 90
        ax.text(angle, 110, lith_name.upper(), 
                ha='center', va='center', 
                rotation=rotation_angle, rotation_mode='anchor', fontsize=12, 
                fontweight='bold', color='white')

当我们运行此时的代码时,我们会得到包含所有 8 个井的图像。

Matplotlib 图形中显示了来自挪威大陆架的 8 个井的岩相百分比的径向条形图。图片由作者提供。

添加散点图作为子图

如上所示,我们在图形的右下角有一个空白区域。这是我们将放置散点图的位置,用于显示井的位置。

为此,我们可以在 for 循环外添加一个新的子图。因为我们希望这是图形上的最后一个图,所以我们需要从 num_rowsnum_cols 中减去 1。

然后,我们通过调用 ax.scatter() 并传入 zechstein_well_intersections 数据框中的 X 和 Y 位置来将散点图添加到坐标轴上。

剩下的代码包括添加 x 轴和 y 轴的标签,设置刻度格式,并将散点图的边缘(坐标轴)设置为白色。

由于我们有一个井没有位置信息,我们可以在散点图上添加一个小脚注,以告知读者这一点。

最后,我们需要将井名添加为标签,以便读者能够理解每个标记是什么。我们可以在 for 循环中实现,并将标签添加到列表中。

# Add the scatter plot in the last subplot (subplot 9)
ax = fig.add_subplot(grid[num_rows - 1, num_cols - 1], facecolor='#393d5c')
ax.scatter(zechstein_well_intersections['X_LOC'], 
zechstein_well_intersections['Y_LOC'], c=colours, s=60)

ax.grid(alpha=0.5, color='#25253c')
ax.set_axisbelow(True)
ax.set_ylabel('NORTHING', fontsize=12, 
                fontweight='bold', color='white')
ax.set_xlabel('EASTING', fontsize=12, 
                fontweight='bold', color='white')

ax.tick_params(axis='both', colors='white')
ax.ticklabel_format(style='plain')
ax.set_title('WELL LOCATIONS', pad=35, fontsize=22, fontweight='bold', color='white')

ax.spines['bottom'].set_color('white')
ax.spines['top'].set_color('white') 
ax.spines['right'].set_color('white')
ax.spines['left'].set_color('white')

ax.text(0.0, -0.2, 'Well 16/11-1 ST3 does not contain location information', ha='left', va='bottom', fontsize=10,
        color='white', transform=ax.transAxes)

labels = []
for i, row in zechstein_well_intersections.iterrows():
    labels.append(ax.text(row['X_LOC'], row['Y_LOC'], row['WELL'], color='white', fontsize=14))

当我们运行绘图代码时,我们将得到以下图形。现在可以看到所有八个井都以径向条形图表示,它们的位置由散点图表示。

Matplotlib 径向条形图和散点图都在一个图形中。图片由作者提供。

我们确实有一个需要解决的问题,那就是标签的位置。目前,它们与数据点、坐标轴和其他标签重叠。

我们可以通过使用之前导入的 adjustText 库来解决这个问题。该库将计算出最佳的标签位置,以避免这些问题。

为了使用此功能,我们只需调用 adjust_text 并传入我们在之前的 for 循环中创建的标签列表。为了减少重叠,我们可以使用 expand_pointsexpand_objects 参数。在这个例子中,1.2 的值效果很好。

adjust_text(labels, expand_points=(1.2, 1.2), expand_objects=(1.2, 1.2))

使用 adjustText 库之后显示井位置和相关标签的散点图。图片由作者提供。

添加脚注和图形标题

为了完成我们的信息图,我们需要给读者提供一些额外的信息。

我们将向图形添加脚注,以显示数据来源和创建者。

为了帮助读者理解信息图的内容,我们可以使用 plt.suptitle 添加标题,并使用 fig.text 添加副标题。这将立即告诉读者他们在查看图表时可以期待什么。

footnote = """
Data Source: 
Bormann, Peter, Aursand, Peder, Dilib, Fahad, Manral, Surrender, & Dischington, Peter. (2020). FORCE 2020 Well well log and lithofacies dataset for
machine learning competition [Data set]. Zenodo. https://doi.org/10.5281/zenodo.4351156

Figure Created By: Andy McDonald
"""

plt.suptitle('LITHOLOGY VARIATION WITHIN THE ZECHSTEIN GP.', size=36, fontweight='bold', color='white')
plot_sub_title = """CHARTS OF LITHOLOGY PERCENTAGES ACROSS 8 WELLS FROM THE NORWEGIAN CONTINENTAL SHELF"""

fig.text(0.5, 0.95, plot_sub_title, ha='center', va='top', fontsize=18, color='white', fontweight='bold')
fig.text(0.1, 0.01, footnote, ha='left', va='bottom', fontsize=14, color='white')

plt.show()

完成绘图代码后,我们将得到如下的 matplotlib 图形。

Matplotlib 信息图显示了挪威大陆架上 Zechstein Group 的岩性变化。图片由作者提供。

我们展示了所有的径向条形图以及每个井的位置。这使读者能够理解井之间的空间变化,从而有助于解释数据中的变异。

例如,15/9–13 井位于该地区的西侧,由白云岩、无水石膏和页岩混合组成。而 17/11–1 井位于该地区的东侧,主要由盐岩组成。这可能与该区域不同的沉积环境有关。

信息图的完整代码

信息图的完整代码如下展示,每个主要部分都附有注释。

# Set the number of columns for your subplot grid
num_cols = 3

# Get the number of wells (rows in the DataFrame)
num_wells = len(df)

# Calculate the number of rows needed for the subplot grid
num_rows = np.ceil(num_wells / num_cols).astype(int)

indexes = list(range(0, len(lith_names)))
width = 2*np.pi / len(lith_names)
angles = [element * width for element in indexes]

colours = ["#ae1241", "#5ba8f7", "#c6a000", "#0050ae", "#9b54f3", "#ff7d67", "#dbc227", "#008c5c"]

label_loc = np.linspace(start=0, stop=2 * np.pi, num=len(lith_names))

# Create a figure
fig = plt.figure(figsize=(20, num_rows * 7), linewidth=10,
                 edgecolor='#393d5c', 
                 facecolor='#25253c')

# Create a grid layout
grid = plt.GridSpec(num_rows, num_cols, wspace=0.5, hspace=0.5)

# Loop over each row in the DataFrame to create the radial bar charts per well
for i, (index, row) in enumerate(df.iterrows()):
    ax = fig.add_subplot(grid[i // num_cols, i % num_cols], projection='polar')
    bars = ax.bar(x=angles, height=row.values, width=width, 
                  edgecolor='white', zorder=2, alpha=0.8, color=colours[i])

    bars_bg = ax.bar(x=angles, height=100, width=width, color='#393d5c',
                     edgecolor='#25253c', zorder=1)

    # Set up labels, ticks and grid
    ax.set_title(index, pad=35, fontsize=22, fontweight='bold', color='white')
    ax.set_ylim(0, 100)
    ax.set_yticklabels([])
    ax.set_xticks([])
    ax.grid(color='#25253c')

    #Set up the lithology / category labels to appear at the correct angle
    for angle, height, lith_name in zip(angles, row.values, lith_names):
        rotation_angle = np.degrees(angle)
        if angle < np.pi:
            rotation_angle -= 90
        elif angle == np.pi:
            rotation_angle -= 90
        else:
            rotation_angle += 90
        ax.text(angle, 110, lith_name.upper(), 
                ha='center', va='center', 
                rotation=rotation_angle, rotation_mode='anchor', fontsize=12, 
                fontweight='bold', color='white')

# Add the scatter plot in the last subplot (subplot 9)
ax = fig.add_subplot(grid[num_rows - 1, num_cols - 1], facecolor='#393d5c')
ax.scatter(zechstein_well_intersections['X_LOC'], zechstein_well_intersections['Y_LOC'], c=colours, s=60)
ax.grid(alpha=0.5, color='#25253c')
ax.set_axisbelow(True)

# Set up the labels and ticks for the scatter plot
ax.set_ylabel('NORTHING', fontsize=12, 
                fontweight='bold', color='white')
ax.set_xlabel('EASTING', fontsize=12, 
                fontweight='bold', color='white')

ax.tick_params(axis='both', colors='white')
ax.ticklabel_format(style='plain')
ax.set_title('WELL LOCATIONS', pad=35, fontsize=22, fontweight='bold', color='white')

# Set the outside borders of the scatter plot to white
ax.spines['bottom'].set_color('white')
ax.spines['top'].set_color('white') 
ax.spines['right'].set_color('white')
ax.spines['left'].set_color('white')

# Add a footnote to the scatter plot explaining missing well
ax.text(0.0, -0.2, 'Well 16/11-1 ST3 does not contain location information', ha='left', va='bottom', fontsize=10,
        color='white', transform=ax.transAxes)

# Set up and display well name labels
labels = []
for i, row in zechstein_well_intersections.iterrows():
    labels.append(ax.text(row['X_LOC'], row['Y_LOC'], row['WELL'], color='white', fontsize=14))

# Use adjust text to ensure text labels do not overlap with each other or the data points
adjust_text(labels, expand_points=(1.2, 1.2), expand_objects=(1.2, 1.2))

# Create a footnote explaining data source

footnote = """
Data Source: 
Bormann, Peter, Aursand, Peder, Dilib, Fahad, Manral, Surrender, & Dischington, Peter. (2020). FORCE 2020 Well well log and lithofacies dataset for
machine learning competition [Data set]. Zenodo. https://doi.org/10.5281/zenodo.4351156

Figure Created By: Andy McDonald
"""

# Display overall infographic title and footnote
plt.suptitle('LITHOLOGY VARIATION WITHIN THE ZECHSTEIN GP.', size=36, fontweight='bold', color='white')
plot_sub_title = """CHARTS OF LITHOLOGY PERCENTAGES ACROSS 8 WELLS FROM THE NORWEGIAN CONTINENTAL SHELF"""

fig.text(0.5, 0.95, plot_sub_title, ha='center', va='top', fontsize=18, color='white', fontweight='bold')
fig.text(0.1, 0.01, footnote, ha='left', va='bottom', fontsize=14, color='white')

plt.show()

摘要

信息图是总结数据并以引人入胜的方式呈现给读者的绝佳方法,而不必担心原始数字。这也是讲述数据故事的好方法。

起初,你可能觉得 matplotlib 不适合制作信息图,但经过一些练习、时间和努力,这完全是可能的。

本教程中使用的数据集

训练数据集作为 Xeek 和 FORCE 2020 举办的机器学习竞赛的一部分使用 (Bormann et al., 2020)。该数据集在 Creative Commons Attribution 4.0 International 许可下提供。

完整的数据集可以通过以下链接访问:doi.org/10.5281/zenodo.4351155

感谢阅读。在离开之前,你一定要订阅我的内容,并将我的文章送到你的收件箱里。 你可以在这里订阅!

其次,你可以通过注册会员获得完整的 Medium 体验,并支持数千名其他作家和我。它每月仅需 $5,你可以完全访问所有精彩的 Medium 文章,还有机会通过你的写作赚取收入。

如果你通过 我的链接注册, 你将直接通过你的费用的一部分支持我,并且不会花费你更多。如果你这样做,非常感谢你的支持。

为绝对初学者创建和发布自己的 Python 包

原文:towardsdatascience.com/creating-and-publishing-your-own-python-package-for-absolute-beginners-7656c893f387

在 5 分钟内创建、构建和发布一个 Python 包

Mike HulsTowards Data Science Mike Huls

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

--

(图片由Erda Estremera提供,来源于Unsplash)

Python 包是可以在多个项目中轻松共享和实现的可重用代码集合。我们可以编写一次代码,然后在许多地方多次使用。包允许我们与同事或甚至全球开发者社区共享我们的代码。作为数据科学家,你可以分享包,而不是分享 Jupyter 笔记本,以确保便于更新、重用和版本控制。

在本文中,我们将详细讲述创建、构建和发布你自己的包到 Python 包索引(PyPI;你从中pip install)的现代方法。我们将创建一个名为“mikes-toolbox2”的真实包,并将其部署到 PyPI,以便我们可以通过pip install mikes-toolbox2来安装它。开始编码吧!

在我们开始之前…

本文详细讲述了如何将包发布到公共 Python 包索引。这意味着,一旦发布,你的包就对任何人开放。我正在撰写一篇关于如何设置你自己的私人 PyPI 的文章,如果你感兴趣,请确保关注我

如果你在本文的代码示例中迷路了:在这里查看源代码

1. 设置 Python 包项目

在本节中,我们将通过创建一个文件夹并安装我们的虚拟环境和包来准备我们的项目。

我们将开始 创建一个文件夹c:/my_packages/new_package 并在代码编辑器中打开此文件夹。接下来,我们需要 设置我们的虚拟环境。请参阅下面的文章以深入了解如何操作。简而言之:你可以让 PyCharm 处理它,或者使用 python -m venv venv

虚拟环境完全入门——什么是虚拟环境以及如何创建一个(附示例)

深入了解 Python 虚拟环境、pip 以及如何避免依赖冲突

虚拟环境完全入门——什么是虚拟环境以及如何创建一个(附示例)

最后我们需要 安装 Poetry。这个包使得依赖管理和打包变得非常简单。使用 pip install poetry 安装。

2. 包要求

Python 包需要一些 文件和文件夹 才能成为有效的包,因此让我们创建这些文件。确保你的文件夹如下所示:

c/
└── my_packages/
    └── new_package/                <-- Project dir
        ├── src/                    <-- source dir for our package
        │   ├── mikes_toolbox2/     <-- the package we're building
        │   │   └── __init__.py     <-- empty file
        │   └── __init__.py         <-- empty file
        ├── tests/                  <-- tests go here
        │  └── __init__.py          <-- empty file
        └── README.md               <-- required readme for our project

从技术上讲,我们不需要 *src* 文件夹在 *new_package**mikes_toolbox2* 之间,但我还是喜欢这样做,因为它可以将包代码与源目录隔离得更远。我认为这种结构更明确、更干净、更易于管理,但这是可选的;你可以省略 *src* 文件夹。

再也无需编写 SQL:SQLAlchemy 的 ORM 绝对入门

使用这个 ORM,你可以创建一个表,插入、读取、删除和更新数据,而无需编写一行 SQL。

再也无需编写 SQL:SQLAlchemy 的 ORM 绝对入门

在创建我们的文件和文件夹后,我们需要一个 **pyproject.toml**。这是一个通用的 Python 项目配置文件,设计用来被所有类型的工具使用,如构建系统、包管理器和代码检查器。Poetry 使用 pyproject.toml 来跟踪依赖关系。

创建 pyproject.toml 很简单,因为 Poetry 帮助我们通过 poetry init 生成一个。只需回答提示即可生成 pyproject

[poetry init] 会提示我们输入有关包的详细信息(图像由作者提供)

pyproject.toml 应位于项目目录的根目录下。所以在我的例子中,它位于 这里 c:/my_packages/new_pacakge/pyproject.toml

也许我们的项目或包需要依赖;我们如何安装这些呢?Poetry 使用pyproject.toml来跟踪依赖(以及其他事项)。我将演示如何安装两个包:requestsblack。我们以不同的方式使用这些包:

  • request由我们自己的包使用

  • black由我们开发者使用来对包进行代码检查

区别在于black开发依赖,我们只在开发依赖时需要它。因此,当有人用 pip 安装我们的包时,requests也需要被安装,但black则不需要。

这里是如何以不同方式安装包:

poetry add requests             <-- will install regularly
poetry add black --group dev    <-- will install as a dev dependency

同样,我会将pytest作为开发依赖进行安装。最终我们的pyproject.toml将包含以下几组依赖:

pyproject.toml中的包依赖和开发依赖(作者提供的图片)

## 创建一个快速的自动文档、可维护且易于使用的 Python API,只需 5 行代码

非常适合(经验不足的)开发人员,他们只需要一个完整的、工作良好、快速且安全的 API

towardsdatascience.com

3 包内容和测试

让我们打包我们的函数吧!我在c:/my_pacakges/new_pacakge/src/mikes_toolbox2/my_toolbox.py中添加了mess_up_casing函数:

import random

def mess_up_casing(my_string: str) -> str:
    """Messes up the casing of a string completely"""
    return "".join(
        [l.upper() if (round(random.random()) == 1) else l.lower() for l in my_string]
    )

接下来我们可以测试这段代码。为此,我在tests文件夹中编写了一个小的单元测试。只需调用pytest并检查所有测试是否成功。最后,我们甚至可以用black src格式化我们所有的代码。

## 为什么 Python 这么慢,如何加速

深入了解 Python 的瓶颈所在

towardsdatascience.com

4. 构建和发布我们的包

现在,构建我们的包变得非常简单,只需poetry build即可。

构建成功后,你会看到一个新的dist文件夹,其中包含一个.tar.gz和一个.whl文件。这些就是我们想要上传到 pypi 的文件。

为此,首先你需要在 pypi.org 注册。登录后,你可以进入你的账户设置并创建一个 API 令牌。你需要这个令牌来进行包发布时的身份验证。确保将令牌保存到某个地方,因为关闭窗口后你无法再次查看它。

接下来,我们需要配置 Poetry,因为我们将使用它来发布我们的包;Poetry 必须知道如何验证你的 pypi.org 账户。我们为此使用之前创建的API 令牌

poetry config pypi-token.pypi THETOKEN

现在我们有了正确的项目结构并配置了 Poetry,发布我们的包变得简单了。我们只需调用:poetry publish 就完成了!在这一步之后,你可以 pip install mikes-toolbox2

## 用两行代码为你的 Python 程序添加线程

通过同时做多个事情来加快你的程序速度

towardsdatascience.com

结论

在这篇文章中,我们已经看到 Poetry 使打包和发布代码到 PyPI.org 变得非常简单。你是否对打包你的代码感兴趣但不想将其发布到公共 PyPI?你也可以托管自己的 Python 包索引,关注我 以便获取未来相关的文章。

我希望这篇文章能达到我期望的清晰度,但如果不是,请告诉我我可以做什么来进一步澄清。同时,查看我关于各种编程相关主题的其他文章

编程愉快!

— Mike

P.S: 喜欢我做的事情吗? 关注我!

[## 使用我的推荐链接加入 Medium — Mike Huls

阅读 Mike Huls 的每一个故事(以及 Medium 上其他成千上万的作者的故事)。你的会员费用直接支持 Mike……

mikehuls.medium.com

使用 Python 和 Sklearn 创建 4 种基于质心的聚类算法的动画

原文:towardsdatascience.com/creating-animation-to-show-4-centroid-based-clustering-algorithms-using-python-and-sklearn-d397ade89cb3

使用数据可视化和动画来理解 4 种基于质心的聚类算法的过程。

Boriharn KTowards Data Science Boriharn K

·发布在 Towards Data Science ·阅读时间 9 分钟·2023 年 8 月 16 日

--

照片由 Mel Poole 提供,发布在 Unsplash

聚类分析

聚类分析是一种有效的机器学习技术,通过数据的相似性和差异性将数据分组。获得的数据组可以用于各种目的,如分割、结构化和决策制定。

要执行聚类分析,有许多基于不同算法的方法可供选择。本文将主要关注基于质心的聚类,这是一种常见且有用的技术。

基于质心的聚类

基本上,基于质心的技术通过反复计算来获得最佳的质心(聚类中心),然后将数据点分配给最近的质心。

由于需要进行多次迭代,数据可视化可以用来表达过程中的变化。因此,本文的目的是使用 Python 和 Sklearn 创建动画,以展示基于质心的过程。

本文中的聚类动画示例。图片由作者提供。

Sklearn (Scikit-learn) 是一个强大的库,帮助我们高效地执行聚类分析。以下是我们将使用的基于质心的聚类技术。

  1. K-means 聚类

  2. MiniBatch K-means 聚类

  3. Bisecting K-means 聚类

  4. Mean-Shift 聚类

开始吧

获取数据

从导入库开始。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

作为示例,本文将使用生成的数据集,该数据集可以使用 sklearn 的 make_blobs() 轻松创建。如果你有自己的数据集,可以跳过此步骤。

from sklearn.datasets import make_blobs
X, y = make_blobs(cluster_std=5, n_samples=1200,
                  n_features=2, random_state=42)
df_X = pd.DataFrame(X)
df_X.dropna(inplace=True)

sns.set_style('darkgrid')
sns.scatterplot(data = df_X, x = 0, y = 1, linewidth=0.5)
plt.show()

1. K-Means 聚类

这是一种基于质心的聚类常见方法。过程可以简要解释为:首先定义簇的数量。接下来,一些数据点被随机选择作为初始质心。其他数据点通过最小 Euclidean 距离分配到最近的质心。

然后,通过计算每个簇的数据点的平均值来重新初始化质心。因此,质心将被更新。之后,重复分配和重新初始化的过程。算法将继续迭代,直到达到最佳质心。

现在,让我们开始使用 Python 代码。首先定义一个迭代次数的列表。作为示例,本文将只运行前十次迭代。如果你想更改数量,请随意修改下面的代码。

iter_num = [i+1 for i in range(10)]
iter_num

#[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

定义函数和变量。

def apply_model(model_in, df):
    clus = model_in.fit_predict(df)
    cent = model_in.cluster_centers_
    #decision boundary
    z = model_in.predict(np.c_[xx.ravel(), yy.ravel()])
    z = z.reshape(xx.shape)
    return clus, cent, z

h = 0.02 
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max,h))

这里是聚类过程。我们可以使用 Sklearn 的 Kmeans、定义的函数和 Python 的 for 循环来返回三个值:每次迭代中的质心、聚类标签和边界。这些值将保存在列表中以便后续绘图。

from sklearn.cluster import KMeans
df_ = df_X.copy()
centroids = None
keep_cent, keep_clus, keep_Z = [], [], []

for n in iter_num:
    model= KMeans(n_clusters=5, random_state=42, max_iter=n, n_init=1,
                  init=(centroids if centroids is not None else 'k-means++'))
    cluster, centroids, Z = apply_model(model, df_)
    keep_clus.append(cluster)
    keep_cent.append(centroids)
    keep_Z.append(Z)

从聚类标签列表中创建一个 DataFrame。

col_name = ['Iter '+str(i) for i in iter_num]

df_iter = pd.DataFrame([list(i) for i in zip(*keep_clus)],columns=col_name)
df_plot = df_.join(df_iter)
df_plot.head()

定义一个函数,使用 for 循环来创建散点图。这个函数也将用于可视化本文中的其他聚类技术。请注意,结果将导出为 PNG 文件,以便稍后合并成 GIF 动画。

def plot_clus(names, Z_val, ctds):
    sns.set_style('darkgrid', {'axes.grid' : False})
    for i,z,c in zip(names, Z_val, ctds):
        plt.figure(1)
        plt.clf()
        plt.imshow(z, interpolation='nearest',
               extent=(xx.min(), xx.max(), yy.min(), yy.max()),
               cmap='coolwarm_r',
               aspect='auto',
               origin='lower', alpha=0.6)
        sns.scatterplot(data = df_plot, x = 0, y = 1, hue = i,
                        palette='viridis', linewidth=0.1, alpha=0.8)
        plt.scatter(c[:, 0], c[:, 1], s=92, marker = '^', c='red', lw=0.5)
        plt.xlabel('')
        plt.ylabel('')
        plt.legend(title=i, loc='upper right')
        plt.savefig(i+'.png', bbox_inches = 'tight', dpi=240)
    return plt.show()

绘制 K-means 聚类结果。

plot_clus(col_name, keep_Z, keep_cent)

展示 K-means 聚类过程的散点图示例。图像来源:作者。

定义一个函数,将图像合并为动画。结果将保存到你的计算机上。

from PIL import Image
import imageio
def animation(names, save_name, time_speed):
    img = []
    for i in names:           # read PNG files
        myImage = Image.open(i+'.png')
        img.append(myImage)
    #export the GIF file, output location can be changed
    imageio.mimsave(save_name, img, duration=time_speed)

应用该函数。

animation(col_name, 'animation_KMeans.gif', 0.4)

看!

动画展示 K-means 聚类过程。图像来源:作者。

动画显示了在第一次迭代中数据点被分配到不同的簇中。然后,由于重新计算,一些数据被分配到相邻的簇中。红色三角形显示了每一步的质心。该过程将持续进行,直到质心达到最佳点。

2. MiniBatch K-means 聚类

与 K-means 聚类处理所有数据点不同,MiniBatch K-means 在每次迭代时随机取小批量数据。这会提高聚类速度,同时返回略有不同的结果。

Sklearn 的 MiniBatchKmeans 函数可以用于执行 MiniBatch K-means 聚类。我们将使用与之前 K-means 过程相同的步骤。

from sklearn.cluster import MiniBatchKMeans
ctrd = None
keep_cent, keep_clus, keep_Z = [], [], []

for n in iter_num:
    model = MiniBatchKMeans(n_clusters=5, random_state=42,
                            max_iter=n, n_init=1,
                            init=(ctrd if ctrd is not None else 'k-means++'))
    cluster, centroids, Z = apply_model(model, df_)
    keep_clus.append(cluster)
    keep_cent.append(centroids)
    keep_Z.append(Z)

df_iter = pd.DataFrame([list(i) for i in zip(*keep_clus)], columns=col_name)
df_plot = df_.join(df_iter)
plot_clus(col_name, keep_Z, keep_cent)

显示 MiniBatch K-means 聚类过程的散点图示例。图像来源:作者。

多亏了前一步定义的函数,我们只需一行代码就可以创建动画。

animation(col_name, 'animation_miniBKMeans.gif', 0.2)

显示 MiniBatch K-means 聚类过程的动画。图像来源:作者。

与 K-means 的结果相比,MiniBatch K-means 在第一次迭代中的聚类区域大致接近 K-means 的第五或第六次迭代。因此,可以看出 MiniBatch K-means 返回的聚类速度更快。

3. 二分 K-means 聚类

二分 K-means 算法在第一步应用 K-means 将所有数据点划分为两个簇。之后,算法将选择平方和最大的簇,再次将其划分为两个簇。这个过程会不断重复,直到簇的总数等于 K。

这个算法也可以被视为 划分层次聚类 和 K-means 之间的一种混合方法。它是一种处理大量 K 的有效方法。

现在我们将处理代码部分。首先创建一个数字列表,用于 for 循环函数。为了与本文中的其他算法进行比较,我将创建一个从一到五的数字列表。

n_num = [i+1 for i in range(5)]
col_name = ['Iter '+str(i) for i in n_num]
n_num

#[1, 2, 3, 4, 5]

接下来,我们将使用 SklearnBisectingKMeans 函数来进行二分 K-means 聚类。

from sklearn.cluster import BisectingKMeans
keep_cent, keep_clus, keep_Z = [], [], []

for n in n_num:
    model = BisectingKMeans(n_clusters=n, random_state=42,
                            max_iter=1, n_init=1)
    cluster, centroids, Z = apply_model(model, df_)
    keep_clus.append(cluster)
    keep_cent.append(centroids)
    keep_Z.append(Z)

df_iter = pd.DataFrame([list(i) for i in zip(*keep_clus)],columns=col_name)
df_plot = df_X.join(df_iter)
plot_clus(col_name, keep_Z, keep_cent)

显示二分 K-means 聚类过程的散点图示例。图像来源:作者。

将图表合并成动画。

animation(col_name, 'animation_BisectingKMeans.gif', 0.9)

显示二分 K-means 聚类过程的动画。图像来源:作者。

从动画中可以看到,在第一步中,所有数据点被划分为两个簇,在第二步中,平方和较大的簇再次被划分为两个簇。因此,在第三次迭代中我们有了三个簇。二分 K-means 过程将继续,直到簇的数量达到 K 值,即本文中的五个。

4. 均值漂移聚类

均值漂移聚类计算某一半径(带宽)内的局部均值点,并将数据点分配到最高密度的地方。算法会不断计算直到收敛。这种方法也被称为爬山算法,因为它的行为特征。

注意: Mean-shift 聚类技术也被认为是一种基于密度的算法 [link1, link2]。

接下来,让我们使用 SklearnMeanShift 函数进行 Mean-shift 聚类。与之前的 K-means 方法相比,主要区别在于不需要定义 K 的数量。顺便提一下,我们需要计算带宽作为 Mean-shift 聚类的参数。

from sklearn.cluster import MeanShift, estimate_bandwidth
keep_cent, keep_clus, keep_Z = [], [], []
bandwidth = estimate_bandwidth(X, quantile=0.2, n_samples=250)

seeds = None 
for n in n_num:
    model = MeanShift(bandwidth = bandwidth,
                      seeds = (seeds if seeds is not None else None),
                      max_iter=1)
    cluster, seeds, Z = apply_model(model, df_)
    keep_clus.append(cluster)
    keep_cent.append(seeds)
    keep_Z.append(Z)

df_iter = pd.DataFrame([list(i) for i in zip(*keep_clus)],columns=col_name)
df_plot = df_.join(df_iter)
plot_clus(col_name, keep_Z, keep_cent)

一个展示 Meanshift K-means 聚类过程的散点图示例。图像作者提供。

应用该函数以创建动画。

animation(col_name, 'animation_MeanShift.gif', 1.1)

哒哒!!

展示 Meanshift 聚类过程的动画。图像作者提供。

在第一次迭代中,我们可以看到许多质心,因为算法试图在使用的带宽内找到数据点的最高密度。在随后的迭代中,第二次迭代中只有三个质心,而接下来的迭代中只剩下两个质心。这是因为算法不断寻找更高的数据点密度来分配质心。

关键要点

总结来说,基于质心的聚类方法旨在找到质心作为数据聚类的参考。为实现这一目标,算法必须重复计算质心以获得最优的质心。这导致了在过程中质心和簇的变化。

本文的目的是应用数据可视化来表达过程,这有助于展示每种算法的工作原理并监控过程中的变化。如果你有任何建议或问题,请随时评论。

感谢阅读。

这里是一些我写的数据可视化文章,你可能会觉得有趣:

  • 可视化 3 种 Sklearn 交叉验证:K-Fold、Shuffle & Split 和 Time Series Split (link)

  • 9 个使用 Python 的可视化效果比条形图更吸引注意力 (link)

  • 8 个使用 Python 处理多时间序列数据的可视化 (link)

  • 可视化多重共线性对多重回归模型的影响 (link)

参考资料

在 TensorFlow 中创建自定义损失函数:理解理论与实际应用

原文:towardsdatascience.com/creating-custom-loss-functions-in-tensorflow-understanding-the-theory-and-practicalities-383a19e387d6?source=collection_archive---------7-----------------------#2023-01-12

利用自定义损失函数最大化 TensorFlow 中的模型性能

Marco SanguinetiTowards Data Science Marco Sanguineti

·

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

--

Fotis FotopoulosUnsplash拍摄的照片

介绍

在机器学习中,损失函数是训练过程中的一个关键组成部分。它衡量模型预测与真实输出之间的差异,并用于更新模型的参数,以最小化这一差异。虽然许多常用的损失函数都内置于 TensorFlow 中,但在某些情况下,你可能需要定义一个自定义损失函数,以更好地适应模型的具体要求。

[## 模块:tf.keras.losses | TensorFlow v2.11.0

内置损失函数。

www.tensorflow.org](https://www.tensorflow.org/api_docs/python/tf/keras/losses?source=post_page-----383a19e387d6--------------------------------)

这对于处理不平衡的数据集、融入领域知识以及其他特定用例可能非常有用。本文将探讨自定义损失函数的理论、使用自定义损失函数的好处以及在 TensorFlow 中创建它们的实际操作。

理解损失函数的作用

在机器学习中,训练模型的目标是最小化模型预测与真实输出之间的差异。这种差异通过损失函数来衡量,损失函数也称为成本函数。损失函数是一个标量函数,用于比较模型的预测输出与真实输出。最常用的损失函数有均方误差、平均绝对误差和交叉熵。

[## 损失函数 - 维基百科

在数学优化和决策理论中,损失函数或成本函数(有时也称为误差函数)是用于衡量预测与真实结果之间差异的工具。

en.wikipedia.org](https://en.wikipedia.org/wiki/Loss_function?source=post_page-----383a19e387d6--------------------------------)

损失函数如何驱动优化?

优化算法使用损失函数的值来调整模型的参数,以使预测输出与真实输出之间的差异最小化。在训练阶段,模型会接收一组输入及相应的真实输出,模型的参数被调整以最小化损失。这个过程是迭代的,直到损失函数达到最小值或达到最大迭代次数时停止。

[## 梯度下降 - 维基百科

在数学中,梯度下降(也常称为最陡下降)是一种一阶迭代优化算法…

en.wikipedia.org](https://en.wikipedia.org/wiki/Gradient_descent?source=post_page-----383a19e387d6--------------------------------)

自定义损失函数的必要性

虽然 TensorFlow 提供的内置损失函数足以应对许多情况,但可能会出现需要自定义损失函数的情况。最常见的原因之一是处理不平衡数据集。在这种情况下,数据可能包含大量某一类别的示例和少量另一类别的示例。这可能导致一个高度准确但对少数类别表现不佳的模型。在这种情况下,可以使用优化召回率或精确度的自定义损失函数来平衡准确性和对少数类别的性能之间的权衡。

Gradient Descent (C1W2L04) — 来源

使用自定义损失函数的另一个原因是将领域知识融入模型。例如,在某些情况下,模型的预测需要满足特定的约束,如非负或具有特定范围。可以定义自定义损失函数来强制执行这些约束。此外,还有一些特定的研究领域,如目标检测和语义分割,有其特定的损失函数,如带掩码的交叉熵、Dice 损失、焦点损失等。

在 TensorFlow 中创建自定义损失函数

TensorFlow 提供了多个工具用于创建自定义损失函数,包括tf.keras.losses模块。要在 TensorFlow 中创建自定义损失函数,你可以继承tf.keras.losses.Loss类并定义call方法。call方法应接收预测输出和真实输出,并返回计算的损失。也可以将额外的参数传递给自定义损失函数的构造函数,以便在损失计算中使用。

还可以使用 TensorFlow 库中的函数来创建自定义损失函数,例如使用数学运算或使用tf.nn模块。定义自定义损失函数时,一个重要的点是如何使其可微分,因为优化器将使用损失对模型参数的梯度来更新模型。

创建自定义损失函数时一个常见的陷阱是忘记正确处理输入被批处理的情况,这意味着输入是矩阵而不是单一值,因此数学操作也应适用于矩阵维度。

还需注意,自定义损失函数的类应实现__init____call__get_config方法,这是在 Keras 中创建子类的标准方式。

让我们从一个示例开始

下面是一个在 TensorFlow 中创建自定义损失函数以处理不平衡数据集的示例。该示例是一个二分类问题,目标是将数据点分类为类 A 或类 B,其中类 A 是少数类。

我们将创建的自定义损失函数将是加权交叉熵损失,它为少数类分配更高的权重,以平衡少数类的准确性和性能之间的权衡。

import tensorflow as tf

class WeightedCrossEntropy(tf.keras.losses.Loss):
    def __init__(self, weight, epsilon=1e-7, name="weighted_cross_entropy", **kwargs):
        super().__init__(name=name, **kwargs)
        self.weight = weight
        self.epsilon = epsilon

    def call(self, y_true, y_pred):
        y_pred = tf.clip_by_value(y_pred, self.epsilon, 1 - self.epsilon)
        loss = -(self.weight * y_true * tf.math.log(y_pred) + (1 - y_true) * tf.math.log(1 - y_pred))
        return tf.math.reduce_mean(loss)

    def get_config(self):
        config = {
            'weight': self.weight,
            'epsilon': self.epsilon
        }
        base_config = super().get_config()
        return {**base_config, **config}

在这里,我们将 tf.keras.losses.Loss 类进行子类化,并定义一个 call 方法,该方法接受 y_true 和 y_pred,分别表示真实标签和预测标签。该方法通过使用初始化时传递的权重变量来计算加权交叉熵损失,并返回损失的均值。

要使用这个自定义损失函数,你可以在定义模型时将其实例传递给模型的 compile 方法。

model = tf.keras.Sequential()
# add layers to your model
...
model.compile(optimizer='adam', loss=WeightedCrossEntropy(weight=0.8), metrics=['accuracy'])

在这里,我们在编译模型时传递了一个权重为 0.8 的 WeightedCrossEntropy 对象,它将作为训练期间的损失函数使用。

需要注意的是,权重参数应根据训练数据集中少数类和多数类的相对频率来设置。

请记住,这只是一个示例,你可能需要根据你的具体用例调整它,它还应该与过采样、欠采样或合成数据生成等技术结合使用,以便正确处理不平衡数据。

MNIST 手写数字数据库的一些样本— 来源

这是一个如何在使用 MNIST 数据集的数字分类问题中使用我们之前创建的自定义损失函数的示例。首先,我们将使用 TensorFlow 内置的 tf.keras.datasets 模块加载 MNIST 数据集,并将其分为训练集和测试集。接下来,我们将定义一个简单的模型来分类 MNIST 数据集中的数字。现在,我们将根据训练数据集中少数类和多数类的相对频率确定自定义损失函数的权重。在这种情况下,少数类是 ‘0’,多数类是 ‘1’ 到 ‘9’。

import tensorflow as tf

class WeightedCrossEntropy(tf.keras.losses.Loss):
    def __init__(self, weight, epsilon=1e-7, name="weighted_cross_entropy", **kwargs):
        super().__init__(name=name, **kwargs)
        self.weight = weight
        self.epsilon = epsilon

    def call(self, y_true, y_pred):
        y_pred = tf.clip_by_value(y_pred, self.epsilon, 1 - self.epsilon)
        loss = -(self.weight * y_true * tf.math.log(y_pred) + (1 - y_true) * tf.math.log(1 - y_pred))
        return tf.math.reduce_mean(loss)

    def get_config(self):
        config = {
            'weight': self.weight,
            'epsilon': self.epsilon
        }
        base_config = super().get_config()
        return {**base_config, **config}

(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()

# Normalize data
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255

# one-hot encode the target variables
y_train = tf.keras.utils.to_categorical(y_train, 10)
y_test = tf.keras.utils.to_categorical(y_test, 10)

model = tf.keras.Sequential([
    tf.keras.layers.Flatten(input_shape=(28, 28)),
    tf.keras.layers.Dense(256, activation='relu'),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Dense(10, activation='softmax')
])

# Determine the weight for the custom loss function
y_train_integers = tf.argmax(y_train,axis=-1)
weight = 1 - y_train_integers.numpy().tolist().count(0) / len(y_train_integers)

loss_fn = WeightedCrossEntropy(weight=weight)
model.compile(optimizer='adam', loss=loss_fn, metrics=['accuracy'])

# Train the model
model.fit(x_train, y_train, epochs=5, batch_size=64)

然后,我们定义自定义损失函数并将权重参数传递给它。最后,我们编译我们的模型,指定自定义损失函数、优化器和评估指标,然后使用训练数据训练我们的模型。需要记住的是,这只是如何在 TensorFlow 中使用自定义损失函数的一个示例,根据你要解决的问题,可能还有许多其他方法可以使用它。此外,重要的是评估模型在测试集上的表现,并解释结果,并与处理不平衡数据集的其他技术进行比较。

结论

自定义损失函数可以成为提升机器学习模型性能的强大工具,特别是在处理不平衡数据集或融入领域知识时。虽然创建自定义损失函数可能看起来令人畏惧,但 TensorFlow 提供了多种工具和库来简化这一过程。通过理解自定义损失函数的理论和实践,你将能够充分应对遇到的各种挑战。

加入 Medium 会员

如果你喜欢这篇文章并希望继续了解更多相关内容,我邀请你通过这个链接加入 Medium 会员。

## 使用我的推荐链接加入 Medium - Marco Sanguineti

阅读 Marco Sanguineti(以及 Medium 上其他成千上万位作者)的每一个故事。对文化的投资是最好的……

marcosanguineti.medium.com

成为会员后,你将能访问更多高质量内容,独享会员专属故事,还能支持像我这样的独立作家和创作者。此外,作为会员,你还可以高亮你喜欢的段落,保存故事以供日后阅读,并获得个性化的阅读推荐。立即注册,让我们继续一起探索这一主题和其他主题。

感谢您的支持!下次见,

Marco

使用 Plotly 创建动态区域图可视化

原文:towardsdatascience.com/creating-dynamic-choropleth-visualizations-using-plotly-61d266cbe9b4

使用易于学习的包来创建复杂的可视化

Hari DevanathanTowards Data Science Hari Devanathan

·发表于 Towards Data Science ·12 min 阅读·2023 年 12 月 18 日

--

图片来源:NASAUnsplash

可视化数据是数据科学家经常忽略的一步。它帮助我们通过分析和整理数据来讲述故事,使其易于理解。通过去除所有技术细节和噪音,突出关键信息,数据科学家可以向非技术经理和高管解释其工作的意义。

现在有许多工具可以帮助可视化数据。多年来,Microsoft Excel 主导了静态可视化市场。随着时间的推移,我们转向了动态可视化,灵活展示更多数据。两种类型的工具帮助创建了动态可视化。

  • 商业智能和分析软件: Tableau,PowerBI

  • 开源编程库: D3.js,Plotly Dash

像 Tableau 和 PowerBI 这样的第三方软件工具对于非技术人员非常优秀。拖放界面和抽象功能使分析师能够轻松创建动态仪表板。缺点是

  • 软件工具价格昂贵

  • 学习这些工具需要一些时间

  • 可视化设计的限制;软件可能不允许某些组件

开源编程库对技术人员非常优秀。那些熟悉软件工程的人可以轻松地按照文档创建灵活的动态可视化。此外,这些软件包是免费的(Plotly 为其企业 Dash 组件提供了付费版本)。

D3.js 和 Plotly 的区别如下

  • D3.js 使用 JavaScript 设计,Plotly 使用 Python 设计

  • D3.js 的出现时间比 Plotly 要早,因此拥有更好的社区支持和更成熟的生态系统(丰富的示例和教程)。

  • D3.js 要求工程师了解 Web 开发的低级细节(HTML、CSS、JavaScript)才能有效使用它。Plotly 将这些细节抽象为易于使用的 Python 类。

  • D3.js 由于其 JavaScript 特性(异步、领域对象模型、函数)有较陡的学习曲线,但可以构建各种复杂的动态可视化。Plotly 因其 Python 特性学习曲线较小,但在用户可以构建的可视化类型上有所限制。

如果是在 2019 年,我会推荐 D3.js。当时,Plotly 在我想要构建的可视化类型上受到了严重限制。

然而,我再次查看了 Plotly,并对其改进印象深刻。清晰的界面使其可以与一些 D3.js 的自定义可视化相抗衡。此外,我不需要学习 JavaScript 的复杂细节就能有效使用这个包。

实际上,我仅用 147 行代码就创建了一个复杂的可视化。下面是该可视化的视频。

视频 1:由作者创建的名称仪表盘 视频

那么,这个可视化做了什么?

  • 它有一张地图,显示有多少人使用特定的名字。特定名字基于最右侧的下拉菜单选择名称以查看每个州的流行度

  • 选择名称以查看每个州的流行度下拉菜单推荐的名称是美国最受欢迎的 N 个名称。这些 N 个名称由中间的下拉菜单前 N 名名称筛选,其中 N 可以是 5、10 或 15。

  • 这些名称也通过性别进行筛选。性别筛选器位于最左侧,用户可以选择男性或女性。

  • 一旦通过性别、前 N 名名称选择名称以查看每个州的流行度来深入可视化,地图将显示按州和年份划分的选定名称的流行度。用户可以通过拖动年份筛选器来即时显示所选名称的流行度。

  • 如果用户点击一个州,下方的条形图会更新。条形图根据点击时的州和年份显示该州和年份的前 N 名流行名称。前 N 名名称基于筛选器前 N 名名称

  • 它有一个加载旋转器,用于提示当前数据正在加载,从而改善用户体验。

惊人的部分是什么?这个复杂的可视化仅用148 行 Python 代码完成。一些复杂的部分仅用 1-3 行代码添加!请查看下面的代码库。

[## 数据科学项目/名称仪表盘/name_dashboard.py 在 master 分支 · hd2zm/数据科学项目]

通过在 GitHub 上创建账户来为 hd2zm/Data-Science-Projects 开发做出贡献。

github.com

我对这个包的直观性感到惊讶。开发者在设计时考虑了很多,使得即便是编程经验有限的人也能轻松使用。

虽然文档很简单明了,但我会详细介绍如何构建每个组件。

Plotly 组件

加载数据

我们正在加载两个数据集

  • names_by_state_cleaned.csv — 一个自定义数据集,列出了一个人的所有名字、拥有这种名字的人数、年份和州。该数据集由社会保障管理局网站下载并清理。这些数据集可用于商业用途。数据集以包含多个.txt 文件的文件夹形式下载,并通过自定义脚本合并为一个 CSV 文件。这个 CSV 文件大小为 150 MB,上传到 GitHub 有点大。为方便起见,我附上了数据集的示例截图。

图 1names_by_state_cleaned.csv的示例数据集

创建应用程序后,我将两个数据集加载到内存中。免责声明:我会在稍后解释为什么从生产角度来看,将 CSV 文件加载到内存中不是一个好主意。现在,我将保持现状。

app = Dash(__name__)

df = pd.read_csv('names_by_state_cleaned.csv')
f = open("us-states.json")
us_states = json.load(f)

HTML 布局

这是应用程序的 HTML 设计

app.layout = html.Div(className = 'dashboard', children = [
    html.H1(children='Popular Names Per US State', style={'textAlign':'center'}),
    html.Div(className='filters', children=[
        html.Div(children=[
            html.H2(['Gender'], style={'font-weight': 'bold', "text-align": "center","offset":1}),
            dcc.RadioItems(
                id='sex-radio-selection', 
                options=[{'label': k, 'value': k} for k in ["Male", "Female"]],
                value=initial_sex,
                inline=True,
                style={"text-align": "center"}
            )], style=dict(width='33.33%')),
        html.Div(children=[
            html.H2(['Top N Names'], style={'font-weight': 'bold', "text-align": "center","offset":1}),
            dcc.RadioItems(
                id='rank-dropdown-selection', 
                options=[5, 10, 15],
                value=initial_rank,
                inline=True,
                style={"text-align": "center"}
            )], style=dict(width='33.33%')),
        html.Div(children=[
            html.H3(['Choose Name To See Popularity Per State On Map'], style={'font-weight': 'bold', "text-align": "center","offset":6}),
            dcc.Dropdown(
                options=[{'label': k, 'value': k} for k in names_in_initial_rank],
                id='top-rank-name-dropdown-selection',
                value=names_in_initial_rank[0],
                style={"text-align": "center"}
            ),
            html.Label(['**if map is fully gray, make sure a name is selected in dropdown above'], style={"text-align": "center","offset":6}),
            ], style=dict(width='15%')),
            html.Div(children=[
                html.Label(['Click on state in map below to view Top <N> Popular <Gender> Names in bar graph below the map.'], style={"text-align": "right"}),
            ],  style=dict(width='33.33%')),
        ], style=dict(display='flex')),
     dcc.Loading(
        id="loading-2",
        children=[
            dcc.Graph(
                id='map'
            ),
            dcc.Graph(id='bar_top_n')
        ],
        type="circle"
     )
]) 

下面是生成的布局截图。

图 2:仪表板的截图

对于那些刚接触 Web 开发的人来说,Plotly 抽象了 HTML 和 CSS 的所有复杂性。以下是应用程序中的所有主要组件。

  • 标题 1,即可视化的标题。

可视化标题的截图

  • 类名为filters的 HTML Div。这包括性别标题(html.Div)、以单选按钮形式呈现的性别选项(dcc.RadioItems)、Top N Names 标题(html.Div)、以单选按钮形式呈现的 Top N Names 选项(dcc.RadioItems)、选择姓名标题(html.Div)和选择姓名下拉框(dcc.Dropdown

图 3:可视化的不同筛选器的截图。性别的单选按钮筛选器、Top N Names 的单选按钮筛选器和流行姓名的下拉筛选器

  • 一个包含两个图形(dcc.Graph)的加载动画(dcc.Loading):一个图形的 id 为 map(见图 2中的地图),另一个图形的 id 为 bar_top_n(见图 2中的条形图)。

图 4: 可视化中的加载动画截图。

我对创建一个加载动画只需 4 行代码感到印象深刻。这种复杂性需要网站中 CSS、HTML 和/或 JavaScript 的组合。Plotly 抽象化了这些细节,使非技术用户也能轻松创建用户友好的体验。

此外,我喜欢 Plotly 保留了 HTML DOM 结构的同时命名元素。作为一名前网页开发人员,我由于 HTML 的熟悉感很快学会了 Plotly。此外,我喜欢他们利用 children 参数来以有序的方式重新排列元素。作为软件开发人员,编写可维护的代码比编写高效但难以阅读的代码要好。

你可能会注意到 dcc 元素中一些值如 initial_rankinitial_sex。这些是我在应用 HTML 布局之前创建的自定义变量。以下是这些变量。

year_values = df["year"].unique()
rank_values = df["rank"].unique()

initial_rank = 5
initial_sex = "Male"
initial_year = 1910
initial_state = "AK"

ranks = []
for i in range(0, initial_rank):
    ranks.append(i+1)

names_in_initial_rank = df[(df["rank"].isin(ranks)) & (df["sex"] == initial_sex[0]) & (df["year"] == initial_year) & (df["state"] == initial_state)]["name"].unique()

这就是为什么你看到图 2中的条形图在加载时设置为默认标题:1910 年 AK 州的前 5 大受欢迎男性名字。

图 5: 仪表板加载时的初始值条形图。

目标是让用户首次加载仪表板时填充默认选项。然后,用户可以通过点击进行导航。

现在我们有了 HTML 布局,是时候添加操作了。当我们点击下拉菜单、单选按钮或地图时会发生什么?我们使用 JavaScript 回调函数。

回调函数

JavaScript 回调是指在另一个函数完成执行后要执行的函数。JavaScript 是异步的,这允许它在应用程序运行时在后台运行函数。回调用于在某些事件(如鼠标点击或键入)之后执行逻辑。

Plotly 的回调函数模板如下所示:

@callback {
   Output[],
   Inputs[]
}
def callback_function(Inputs[]) {
  return Outputs[]
} 

输入和输出接受两个参数:

  • div 的 id

  • 从 div 接收的输入类型

从 HTML 布局中,我们有以下 div 的 id:

  • sex-radio-selection

  • rank-dropdown-selection

  • top-rank-name-dropdown-selection

  • loading-2

  • map

  • bar_top_n

输入类型可以是 dcc 元素(dcc.Dropdowndcc.RadioButton)、clickDatahoverData 等。以下是可视化的回调函数。

@callback(
    [
        Output('map', 'figure'),
        Output('bar_top_n', 'figure'),
        Output("top-rank-name-dropdown-selection", "options")
    ],
    [
        Input('sex-radio-selection', 'value'),
        Input('rank-dropdown-selection', 'value'),
        Input('top-rank-name-dropdown-selection', 'value'),
        Input('map', 'clickData')
    ]
)
def update_graphs(sex_value, rank_value, name_for_map_value, click_data):

    df_filter = df[df["sex"] == sex_value[0]]
    ranks = []
    for i in range(0, rank_value):
        ranks.append(i+1)

    state = initial_state
    year = initial_year
    if click_data:
        state = click_data['points'][0]['location']
        year = click_data['points'][0]['customdata'][0]

    top_rank_name_dropdown_options = df[(df["rank"].isin(ranks)) & (df["sex"] == sex_value[0]) & (df["state"] == state)]["name"].unique()

    if not name_for_map_value:
        name_for_map_value = top_rank_name_dropdown_options[0]

    RENAMED_COUNT_COLUMN = "Population with Name"
    df_filter = df_filter.rename(columns={"count": RENAMED_COUNT_COLUMN})

    df_filter = df_filter.sort_values(by=["year"])

    df_map_filter = df_filter[(df_filter["rank"].isin(ranks)) & (df_filter["name"] == name_for_map_value)]

    df_bar_filter = df_filter[(df_filter["state"] == state) & (df_filter["rank"].isin(ranks)) & (df_filter["year"] == year)]
    df_bar_filter = df_bar_filter.sort_values(by=["rank"])

    fig_map = px.choropleth(df_map_filter, geojson=us_states, 
                            locations='state', 
                            color=RENAMED_COUNT_COLUMN,
                            animation_frame='year',
                            animation_group='state',
                            hover_name="state",
                            custom_data='year',
                            color_continuous_scale="Reds",
                            range_color=(0, 1100),
                            scope="usa"
                          )

    fig_map.update_layout(margin={"r":0,"t":0,"l":0,"b":0})

    bar_chart_title = "Top %i Popular %s Names In State %s Of %i"%(rank_value, sex_value, state, year)

    fig_bar = px.bar(df_bar_filter, 
                     x="name", 
                     y=RENAMED_COUNT_COLUMN, 
                     title=bar_chart_title)
    fig_bar.update_traces(marker_color='#D62728')
    fig_bar.update_layout(margin={"r":50,"t":50,"l":0,"b":0})

    return [fig_map, fig_bar, top_rank_name_dropdown_options]

使用 plotly express 图形创建了 choropleth 地图和条形图,并作为输出的一部分返回。返回的输出顺序很重要,因为它应与回调模板中指定的输出顺序相对应。输入同样适用此规则。

choropleth 地图具有一个动画帧参数,该参数接受 'year'。这创建了按年显示的动画滑块,如图 6图 7所示。

图 6: 1910 年 John 姓名人数的热图

图 7: 将滑块移动到 1940 年后的 John 姓名人数热图

状态和年份变量设置为inital_stateinitial_year,这些在上面已指定。然而,如果用户点击了某个特定的州和年份,我们希望更改这些变量。因此,if 语句检查click_data是否为真,指示clickData事件是否被触发。

如果click_data为真,我们将state=click_data[points][0]['location']year=clic_data['points'][0]['customdata']。此外,我们将输入click_data定义为Input("map", "clickData")。这意味着我们在地图上发生点击时会做出响应,并根据地图的locationcustomdata属性更新状态和年份。这些是在创建热图时定义的参数(locations=’state'custom_data='year')。

图 8: 热图和条形图,悬停在科罗拉多州时显示工具提示。工具提示显示当前年份、悬停时的州和 John 姓名人数。

图 9: 点击图 8中的科罗拉多州后的加载旋转器。

图 10: 更新的条形图,包含 1910 年科罗拉多州前 5 名流行男性姓名。这是在用户点击了图 7图 8中的科罗拉多州后创建的。您可以看到科罗拉多州与图 7中的阿肯色州相比流行的名字有所不同(查尔斯和威廉在科罗拉多州流行,而保罗和罗伯特在阿肯色州流行)。

下拉框中的其他输入用于筛选初始加载的姓名数据框。下面是过滤器在回调函数修改后如何操作的。

性别筛选器

图 11:图 6中点击“女性”单选按钮后的仪表板。热图为空,因为它需要将流行姓名筛选器(下拉框)中的姓名从男性姓名替换为女性姓名。默认姓名 John 不在女性姓名列表中,因此您需要从下拉框中选择一个女性姓名以填充地图。

图 12: 从流行姓名下拉筛选器中选择 Helen 后的仪表板,显示为图 11。现在,条形图填充了 1910 年阿肯色州的前 5 名流行女性姓名,而不是男性姓名。地图上的灰色状态表示该年份和州没有人叫 Helen。

顶级排名筛选器

图 13: 1967 年 John 的受欢迎程度地图可视化。1967 年德克萨斯州前 5 名流行男性姓名的条形图。

图 14: 点击“前 N 名”单选按钮 15 后的加载旋转图标,而不是 5

图 15:图 13 相同的地图,只是条形图更新为 1967 年德克萨斯州的前 15 个名字。条形图还具有“查看更多”选项,以查看 1967 年德克萨斯州名为 William 的人数。

流行名字筛选器

图 16: 仪表板显示性别为男性的前 15 个名字,约翰的流行度地图,以及 1910 年德克萨斯州前 15 个流行男性名字的条形图。流行度地图的下拉菜单显示了条形图中填充的前 15 个名字,当前悬停在 Willie 上。

图 17: 点击“流行名字筛选器”中的 Willie 后,图 16 中的仪表板。地图更新了 Willie 在各州和年份的流行度。

将所有内容整合在一起

改善可视化的提示

使用 SQL 数据库和查询进行优化

你可能已经注意到,视频中加载旋转图标需要 4 秒以上来加载任何筛选器的更改数据,除了年份。这是因为 Plotly Dash 应用程序从 CSV 文件中读取了所有 150MB 的数据到内存中,并且在应用程序运行时查询选择数据。因此,处理数据框需要一些时间。

从数据库中读取数据并使用 SQL 查询选择数据要快得多。现在,你不需要将所有数据加载到内存中,只需一些部分。此外,SQL 数据库比 CSV 文件更紧凑,占用的内存更少。

使条形图更具动态性

可视化的核心组件是单个名字在地点和时间上的流行度地图。单一年份或时间的前 N 名条形图可能是多余的,因为它包含了与 top_rank_name_dropdown 筛选器相同的信息。

可以移除 top_rank_name_dropdown 筛选器,条形图可以突出当前地图上显示的名字。如果用户点击另一个名字的条形,地图会更新。如果用户点击地图上的一个州,条形图将更新为选定州的前 N 名名字和滑块最后设置的年份。

使条形图在地图上的年份滑块更新

地图可以通过年份滑块进行筛选,但条形图不能,这似乎有些混乱。如果用户点击地图上的一个州,条形图可以按州和年份更新。如果条形图来自与滑块上的地图年份不同的年份,这可能会造成混淆。

可能有办法为地图和条形图设置相同的年份筛选,但我尚未找到这种解决方案。

我最好的猜测是查看在移动年份滑块时是否触发了事件。然后在回调中,更新柱状图以反映当前年份。然而,目前的方法并没有做到这一点,因为我们在内存中加载了 150MB 的数据并在内存中进行过滤。因此,在移动滑块时会有巨大的延迟。

修订 Top N 过滤器

数据集中有些州和年份只有 8 个流行名字(如 1910 年的阿肯色州)。这对 Top N 过滤器的表现不佳,因为它即使在选择了 10 或 15 的单选按钮时也显示 8 个柱状图。或许在这种特殊情况下,需要隐藏这些按钮,并用 Top 8 单选按钮替代。

结论

本教程展示了如何使用 Plotly 创建复杂的动态可视化。虽然有改进可视化的方式,但这仍然显示了使用这个开源包的便捷。随着越来越多的技术工程师和数据/业务分析师转向数据科学,我们需要更好的工具来帮助他们以清晰的方式向高层传达数据。基于免费成本和较低的学习曲线,Plotly 成为有志数据科学家应当保留在工具库中的最佳包之一。

感谢阅读!如果你想阅读更多我的作品,请查看我的 目录

如果你不是 Medium 的付费会员,但有兴趣订阅以阅读像这样的教程和文章,点击这里 以加入会员。通过这个链接加入会员意味着我会因为推荐你而获得报酬。

在 Python 中创建分形

原文:towardsdatascience.com/creating-fractals-in-python-a502e5fc2094?source=collection_archive---------3-----------------------#2023-03-24

深入几何学、递归算法和三角形……很多很多的!

Robert ElmesTowards Data Science Robert Elmes

·

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

--

这是我在今年早些时候拍的一张照片,那天特别阴沉,即使在英格兰也是如此。

分形 是在不同尺度上自相似的无限复杂的图案。例如,树干分裂成更小的枝条。这些枝条进一步分裂成更小的枝条,依此类推。

通过程序生成分形,我们可以将简单的形状转化为复杂的重复图案。

在这篇文章中,我将探讨如何使用一些基本的 A-Level 几何知识和一点编程技巧,在 Python 中构建令人印象深刻的分形。

分形在数据科学中扮演着重要角色。例如,在分形分析中,会评估数据集的分形特征,以帮助理解潜在过程的结构。此外,分形生成中心的递归算法可以应用于广泛的数据问题,从二分查找算法到递归神经网络。

想法

我想写一个可以绘制等边三角形的程序。在三角形的每一边上,它还必须能够绘制一个稍小的外向三角形。它应该能够根据我的需要重复这个过程,希望能创造出一些有趣的图案。

这个粗略的草图展示了我想生成的图案类型。

表示图像

我将把图像表示为一个二维像素数组。像素数组中的每个单元格将表示该像素的颜色(RGB)。

为了实现这一点,我们可以使用库NumPy生成像素数组,并使用Pillow将其转换为可以保存的图像。

蓝色像素的 x 值为 3,y 值为 4,可以通过二维数组 pixels[4][3] 进行访问

绘制直线

现在是时候开始编码了!

首先,我需要一个函数,可以接受两个坐标集并在它们之间绘制一条直线。

以下代码通过在两个点之间进行插值,每一步都向像素数组中添加新的像素。你可以把这个过程想象成逐像素地为一条线着色。

我在每个代码片段中使用了续行符 ‘\’,以便将一些较长的代码行适配进来。

import numpy as np
from PIL import Image
import math

def plot_line(from_coordinates, to_coordinates, thickness, colour, pixels):

    # Figure out the boundaries of our pixel array
    max_x_coordinate = len(pixels[0])
    max_y_coordinate = len(pixels)

    # The distances along the x and y axis between the 2 points
    horizontal_distance = to_coordinates[1] - from_coordinates[1]
    vertical_distance = to_coordinates[0] - from_coordinates[0]

    # The total distance between the two points
    distance =  math.sqrt((to_coordinates[1] - from_coordinates[1])**2 \
                + (to_coordinates[0] - from_coordinates[0])**2)

    # How far we will step forwards each time we colour in a new pixel
    horizontal_step = horizontal_distance/distance
    vertical_step = vertical_distance/distance

    # At this point, we enter the loop to draw the line in our pixel array
    # Each iteration of the loop will add a new point along our line
    for i in range(round(distance)):

        # These 2 coordinates are the ones at the center of our line
        current_x_coordinate = round(from_coordinates[1] + (horizontal_step*i))
        current_y_coordinate = round(from_coordinates[0] + (vertical_step*i))

        # Once we have the coordinates of our point, 
        # we draw around the coordinates of size 'thickness'
        for x in range (-thickness, thickness):
            for y in range (-thickness, thickness):
                x_value = current_x_coordinate + x
                y_value = current_y_coordinate + y

                if (x_value > 0 and x_value < max_x_coordinate and \
                    y_value > 0 and y_value < max_y_coordinate):
                    pixels[y_value][x_value] = colour

# Define the size of our image
pixels = np.zeros( (500,500,3), dtype=np.uint8 )

# Draw a line
plot_line([0,0], [499,499], 1, [255,200,0], pixels)

# Turn our pixel array into a real picture
img = Image.fromarray(pixels)

# Show our picture, and save it
img.show()
img.save('Line.png')

当我让这个函数在像素数组的每个角落之间绘制一条黄色直线时的结果

绘制三角形

现在我有了一个可以在两个点之间绘制直线的函数,是时候绘制第一个等边三角形了。

给定三角形的中心点和边长,我们可以使用方便的公式计算高度:h = ½(√3a)

现在,利用这个高度、中心点和边长,我可以确定三角形的每个角落应该在哪里。使用我之前制作的plot_line函数,我可以在每个角落之间绘制一条直线。

def draw_triangle(center, side_length, thickness, colour, pixels):

    # The height of an equilateral triangle is, h = ½(√3a)
    # where 'a' is the side length
    triangle_height = round(side_length * math.sqrt(3)/2)

    # The top corner
    top = [center[0] - triangle_height/2, center[1]]

    # Bottom left corner
    bottom_left = [center[0] + triangle_height/2, center[1] - side_length/2]

    # Bottom right corner
    bottom_right = [center[0] + triangle_height/2, center[1] + side_length/2]

    # Draw a line between each corner to complete the triangle
    plot_line(top, bottom_left, thickness, colour, pixels)
    plot_line(top, bottom_right, thickness, colour, pixels)
    plot_line(bottom_left, bottom_right, thickness, colour, pixels)

在 500x500 像素的 PNG 图像中心绘制一个三角形的结果

生成分形

一切就绪。几乎所有需要的条件都已经准备好,可以在 Python 中创建我的第一个分形了。真令人兴奋!

然而,这最后一步可以说是最棘手的。我希望我们的三角形函数能够对每条边调用自身。为了使其有效,我需要能够计算每个新小三角形的中心点,并正确地旋转它们,使其与附加的边垂直。

通过从我希望旋转的坐标中减去中心点的偏移量,然后应用旋转坐标对的公式,我们可以使用此函数旋转三角形的每个角。

def rotate(coordinate, center_point, degrees):
    # Subtract the point we are rotating around from our coordinate
    x = (coordinate[0] - center_point[0])
    y = (coordinate[1] - center_point[1])

    # Python's cos and sin functions take radians instead of degrees
    radians = math.radians(degrees)

    # Calculate our rotated points 
    new_x = (x * math.cos(radians)) - (y * math.sin(radians))
    new_y = (y * math.cos(radians)) + (x * math.sin(radians))

    # Add back our offset we subtracted at the beginning to our rotated points
    return [new_x + center_point[0], new_y + center_point[1]]

一个我们将每个坐标旋转了 35 度的三角形

现在我可以旋转三角形,我必须将注意力转向在第一个三角形的每一边上绘制一个新的、更小的三角形。

为了实现这一点,我扩展了draw_triangle函数,以计算每条边的新三角形的旋转和中心点,新三角形的边长由参数shrink_side_by减少。

一旦计算出新三角形的中心点和旋转角度,它会调用draw_triangle(它自身)来从当前线的中心绘制新的、更小的三角形。这将再次触发相同的代码块,计算另一个更小三角形的中心点和旋转角度。

这称为递归算法,因为我们的draw_triangle函数现在会调用自身,直到达到我们希望绘制的三角形的max_depth。重要的是要有这个逃逸条件,否则函数理论上会无限递归(但实际上调用堆栈会变得过大,导致栈溢出错误)!

def draw_triangle(center, side_length, degrees_rotate, thickness, colour, \
                  pixels, shrink_side_by, iteration, max_depth):

    # The height of an equilateral triangle is, h = ½(√3a) 
    # where 'a' is the side length
    triangle_height = side_length * math.sqrt(3)/2

    # The top corner
    top = [center[0] - triangle_height/2, center[1]]

    # Bottom left corner
    bottom_left = [center[0] + triangle_height/2, center[1] - side_length/2]

    # Bottom right corner
    bottom_right = [center[0] + triangle_height/2, center[1] + side_length/2]

    if (degrees_rotate != 0):
        top = rotate(top, center, degrees_rotate)
        bottom_left = rotate(bottom_left, center, degrees_rotate)
        bottom_right = rotate(bottom_right, center, degrees_rotate)

    # Coordinates between each edge of the triangle
    lines = [[top, bottom_left],[top, bottom_right],[bottom_left, bottom_right]]

    line_number = 0

    # Draw a line between each corner to complete the triangle
    for line in lines:
        line_number += 1

        plot_line(line[0], line[1], thickness, colour, pixels)

        # If we haven't reached max_depth, draw some new triangles
        if (iteration < max_depth and (iteration < 1 or line_number < 3)):
            gradient = (line[1][0] - line[0][0]) / (line[1][1] - line[0][1])

            new_side_length = side_length*shrink_side_by

            # Center of the line of the traingle we are drawing
            center_of_line = [(line[0][0] + line[1][0]) / 2, \
                              (line[0][1] + line[1][1]) / 2]

            new_center = []
            new_rotation = degrees_rotate

            # Amount we need to rotate the traingle by
            if (line_number == 1):
                new_rotation += 60
            elif (line_number == 2):
                new_rotation -= 60
            else:
                new_rotation += 180

            # In an ideal world this would be gradient == 0,
            # but due to floating point division we cannot
            # ensure that this will always be the case
            if (gradient < 0.0001 and gradient > -0.0001):
                if (center_of_line[0] - center[0] > 0):
                    new_center = [center_of_line[0] + triangle_height * \
                                 (shrink_side_by/2), center_of_line[1]]
                else:
                    new_center = [center_of_line[0] - triangle_height * \
                                  (shrink_side_by/2), center_of_line[1]]

            else:

                # Calculate the normal to the gradient of the line
                difference_from_center = -1/gradient

                # Calculate the distance from the center of the line
                # to the center of our new traingle
                distance_from_center = triangle_height * (shrink_side_by/2)

                # Calculate the length in the x direction, 
                # from the center of our line to the center of our new triangle
                x_length = math.sqrt((distance_from_center**2)/ \
                                     (1 + difference_from_center**2))

                # Figure out which way around the x direction needs to go
                if (center_of_line[1] < center[1] and x_length > 0):
                    x_length *= -1

                # Now calculate the length in the y direction
                y_length = x_length * difference_from_center

                # Offset the center of the line with our new x and y values
                new_center = [center_of_line[0] + y_length, \
                              center_of_line[1] + x_length]

            draw_triangle(new_center, new_side_length, new_rotation, \
                          thickness, colour, pixels, shrink_side_by, \
                          iteration+1, max_depth)

三角形分形,shrink_side_by = 1/2 和 max_depth = 2

结果

以下是一些通过修改shrink_side_bymax_depth值输入到我们的draw_triangle函数中可以生成的不同图像示例。

有趣的是,这些大型重复模式通常会创造出更复杂的形状,如六边形,但具有迷人的对称性。

在重复三角形的对称性中,开始出现越来越复杂的形状。

一种类似雪花的分形,使用了修改版的draw_triangle函数,该函数还绘制了一个朝内的三角形。

另一种创作,使用每次迭代减少的更小的尺寸

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

结论

分形非常有趣,可以创造出美丽的图案。使用一些简单的概念和一丝创造力,我们可以生成非常令人印象深刻的结构。

在理解分形的核心属性并应用递归算法时,我们创建了一个坚实的基础,这有助于我们理解数据科学中更复杂的分形问题。

随时阅读和下载完整代码这里。如果你发现改进或扩展的方法,请告诉我!

我很好奇你能用不同的形状创造出什么?

使用 Python 的 Plotly 和 Folium 库创建地理空间热图

原文:towardsdatascience.com/creating-geospatial-heatmaps-with-pythons-plotly-and-folium-libraries-4159e98a1ae8

用于可视化地理空间变化的两个优秀 Python 选项

Andy McDonaldTowards Data Science Andy McDonald

·发布于 Towards Data Science ·阅读时间 6 分钟·2023 年 3 月 17 日

--

图片由 KOBU Agency 提供,来源于 Unsplash

热图,也称为 密度图,是展示变量在地理区域内空间分布的数据可视化工具。它们是可视化和识别趋势、支持决策、检测异常值以及为演示文稿创建引人注目的可视化的绝佳工具。

有几种 Python 地图绘制库可供选择,然而,两个非常流行且易于使用的库是 Folium 和 Plotly Express。

Folium 是一个很棒的库,它使可视化地理空间数据变得简单。它由 Leaflet.js 提供支持,Leaflet.js 是一个领先的 JavaScript 地图绘制库,且平台无关。 Plotly 是一个流行的库,可以用极少的代码创建强大的交互式数据可视化,并可以与 MapBox 一起创建交互式地图。

在本文中,我们将展示如何使用这两个库在挪威大陆架上可视化声学压缩慢度数据。

使用 Plotly Express 的视频版本教程可以在我的 YouTube 频道找到。请查看下面的链接。

导入库和加载数据

在本教程中,我们将从导入 pandas 开始,这将用于从 CSV 文件中加载数据。

import pandas as pd

我们在本教程中使用的数据集是以下内容的组合:

  • Xeek Force 2020 机器学习岩石学竞赛数据集被用来推导 Shetland Group 的平均声波压缩慢度(DTC)值

  • 来自挪威石油局网站的数据,用于提供位置数据

df = pd.read_csv('xeek_force_2020_dtc_mapping.csv')
df

当数据加载完成后,我们可以使用以下方法调用数据框:

挪威大陆架井的数据框视图。图片由作者提供。

我们可以看到我们有 7 列:

  • 井名 — 井的名称

  • DTC — Shetland Group 的平均声波压缩慢度

  • 温度 — 井底温度

  • 水深 — 从海平面到海底的深度

  • 完成年份 — 井的完成年份

  • 纬度 — 井的纬度位置,单位为十进制度

  • 经度 — 井的经度位置,单位为十进制度

使用 Plotly Express 创建热图

要开始使用 Plotly Express 创建热图,我们需要将该库导入到我们的笔记本中,如下所示:

import plotly_express as px

然后我们需要创建一个新的图形。这是通过调用 plotly express 的 density_mapbox 来完成的。

要创建我们的地图,我们需要传入几个参数:

  • df — 包含数据的数据框名称

  • lat — 纬度列的名称

  • lon — 经度列的名称

  • z — 包含我们希望在热图上可视化的数据的列名称

  • center — 地图的中心位置。我们可以调用纬度和经度列的均值

  • zoom — 地图的初始缩放级别

最后两个参数 mapbox_styleheight 控制背景映射层和图的高度。

fig = px.density_mapbox(df, lat='Latitude', lon='Longitude',
                        z='DTC', radius=20,
                        center=dict(lat=df.Latitude.mean(), 
                                    lon=df.Longitude.mean()), 
                        zoom=4,
                        mapbox_style="open-street-map", 
                        height=900)
fig.show()

当我们调用 fig.show() 时,我们会得到以下地图。

使用 plotly express 的 density_mapbox 生成的热图。图片由作者提供。

从这张地图上,我们可以看到该区域北端的 DTC 值较高,这可能归因于几种因素,包括较低的压实度或较高的页岩度。

Plotly express 生成的地图的一个优点是我们可以悬停在数据上并获得绘制变量(DTC)的值。这是自动完成的,无需额外代码来创建弹出框。

使用 plotly express 的 density_mapbox 生成的热图增加了交互性。图片由作者提供。

使用 Folium 创建热图

要开始使用 Folium,我们需要导入它;然而,为了生成热图,我们还需要从folium.plugins导入 HeatMap 插件。

import folium
from folium.plugins import HeatMap

一旦导入了 Folium,我们需要通过调用 folium.map() 来创建一个基础地图。在该类方法中,我们可以传入几个参数。对于这个例子,我们将使用以下参数:

  • location — 地图的中心位置

  • zoom_start — 地图的初始缩放级别

  • control_scale — 是否在地图上显示比例控制

如果你想了解有关地图函数的参数,可以查看folium.map类的帮助文档。

m = folium.Map(location=[df_combined.Latitude.mean(), 
                         df_combined.Longitude.mean()], 
               zoom_start=6, control_scale=True)

接下来,我们需要创建热力图图层。

要实现这一点,我们首先需要将纬度、经度和值转换为列表,然后可以传递给HeatMap调用。

我们可以设置一些参数来美化热力图的外观,例如最小和最大不透明度、数据点的颜色半径等。

map_values = df_combined[['Latitude','Longitude','DTC']]

data = map_values.values.tolist()

hm = HeatMap(data,
              min_opacity=0.05, 
              max_opacity=0.9, 
              radius=25).add_to(m)

当我们调用我们的地图对象时,我们可以看到与 plotly express 生成的图案类似。

使用 Folium 生成的热力图。图片由作者提供。

然而,这张地图有几个缺点:

没有类似于我们在 Plotly Express 中看到的悬停功能。我们可以添加标记,但这需要几行额外的代码。

如果你想了解如何实现这一点,可以查看我下面的文章。

## Folium 映射:在地图上显示标记

在 Python 中向 Folium 地图添加标记的简短指南

towardsdatascience.com

另一个问题是没有颜色渐变条来帮助读者理解颜色范围。但可以通过创建颜色映射字典来解决这一问题,具体可以参见以下 StackOverflow 帖子。

[## 如何在 Folium 热力图中添加图例/渐变?

我正在使用 Folium 创建热力图。我的数据包含 3 列:类别、纬度和经度。纬度-经度点是…

stackoverflow.com](https://stackoverflow.com/questions/47163728/how-to-add-legend-gradient-in-folium-heat-map?source=post_page-----4159e98a1ae8--------------------------------)

结论

热力图提供了一种出色的方式来可视化和识别地理区域的趋势,并且可以使用两个流行的 Python 库:Folium 和 Plotly Express 轻松创建。这两个库使用简单,可以用于在大范围内绘制岩石物理和井日志属性。从这些地图中,我们可以获得有关领域或区域的趋势和变化的信息。

感谢阅读。在你离开之前,你一定要订阅我的内容,将我的文章发送到你的收件箱。 你可以在这里做到这一点!或者,你可以 注册我的新闻通讯 ,以免费将额外内容直接发送到你的收件箱。

其次,你可以通过注册会员获得完整的 Medium 体验,支持我和其他成千上万的作者。每月仅需 $5,你即可全面访问所有精彩的 Medium 文章,还有机会通过写作赚钱。如果你通过 我的链接**, 你将直接用部分费用支持我,而且不会增加额外费用。如果你这样做了,非常感谢你的支持!

本教程中使用的数据集

本教程使用的数据集由两个其他数据集组成:

  1. 用于 Xeek 和 FORCE 2020 举办的机器学习竞赛中的训练数据集的一个子集 (Bormann 等人, 2020)

    可以通过以下链接访问完整数据集:doi.org/10.5281/zenodo.4351155

  2. 来自挪威石油局网站的井位数据

    数据可以在这里下载:factpages.npd.no/en/wellbore/tableview/exploration/all

这两个数据集均根据挪威政府的 NOLD 2.0 许可证授权,详细信息可以在这里找到:挪威开放政府数据许可证 (NLOD) 2.0

使用 dtreeviz 创建令人惊叹的决策树可视化

原文:towardsdatascience.com/creating-incredible-decision-tree-visualizations-with-dtreeviz-820c6547b6a9

如何使用这个有用的库可视化决策树模型

Amol MavuduruTowards Data Science Amol Mavuduru

·发布于 Towards Data Science ·阅读时长 6 分钟·2023 年 6 月 21 日

--

图片由作者提供,使用 dtreeviz 创建。

在模型可解释性方面,决策树是一些最直观和可解释的模型。每个决策树模型都可以解释为一组人类可解释的规则。能够可视化决策树模型对于模型的可解释性非常重要,并且可以帮助利益相关者和业务经理对这些模型建立信任。

幸运的是,我们可以使用 dtreeviz 库轻松地可视化和解释决策树。在本文中,我将演示如何使用 dtreeviz 可视化用于回归和分类的基于树的模型。

安装 dtreeviz

你可以使用以下命令通过 pip 轻松安装 dtreeviz:

pip install dtreeviz

有关依赖项和可能需要安装的额外库的详细列表,请参阅 这个 GitHub 仓库

可视化回归树

在本节中,我们将对 糖尿病数据集 训练一个决策树回归器。请注意,你可以在这个 GitHub 仓库中找到本教程的所有代码。请记住,我使用 Jupyter 作为运行 Python 代码的环境。你可以在 这个 GitHub 仓库 中找到我为本教程编写的所有代码。

导入库

在下面的代码块中,我简单地导入了包括 scikit-learn 决策树模块和 dtreeviz 在内的几个常用库。

import numpy as np
import pandas as pd
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
import dtreeviz

读取数据

糖尿病数据集可在 scikit-learn 中获取,因此我们可以使用以下代码导入数据集,并将特征和目标值存储在名为 X 和 y 的 numpy 数组中。

from sklearn.datasets import load_diabetes

diabetes_data = load_diabetes()
X = pd.DataFrame(data = diabetes_data['data'], columns=diabetes_data['feature_names'])
y = diabetes_data['target']

训练决策树模型

为了使树更容易可视化,我们可以限制决策树的最大深度,并按如下方式对数据进行训练。

dtree_reg = DecisionTreeRegressor(max_depth=3)
dtree_reg.fit(X, y)

可视化树

dtreeviz 的一个关键特点是能够可视化决策树模型。使用下面的代码,我们可以创建一个有趣的决策树可视化,并在每个节点处视觉上描绘决策边界。

viz_model = dtreeviz.model(dtree_reg,
                           X_train=X, y_train=y,
                           feature_names=list(X.columns),
                           target_name='diabetes')
viz_model.view()

糖尿病回归树可视化。图像由作者使用 dtreeviz 创建。

注意上面的可视化还给出了每个节点的决策边界和特征空间,以及每个叶节点的回归输出和样本大小。

可视化叶节点分布

dtreeviz 提供的另一个有用功能是可视化叶节点分布。决策树的叶节点包含根据每组条件预测的实际值。使用rtree_leaf_distributions函数,我们可以为回归树创建这种可视化。

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
%matplotlib inline

viz_model.rtree_leaf_distributions()

根据上面的可视化,我们可以看到决策树可以预测 268.9、208.6、176.9、137.7、154.7、274.0、83.4 或 108.8 作为糖尿病这一目标变量的值。水平分散的点表示糖尿病目标变量值的分布,小黑线表示用于预测该叶节点的平均值。理想情况下,叶节点的分布应具有低方差,以便我们对用于预测的平均值有更高的信心。

可视化叶节点大小

我们还可以可视化叶节点的大小,即每个叶节点的样本数量,如下面的函数所示。

viz_model.leaf_sizes()

根据上面的图,我们可以看到每个叶节点的样本数量。这种可视化是评估我们对回归树预测的信心的良好工具。

可视化分类树

我们还可以使用 dtreeviz 可视化分类树,且这些可视化与回归树创建的效果略有不同。在本节中,我们将使用乳腺癌威斯康星数据集来训练和可视化决策模型。

读取数据

乳腺癌威斯康星数据集在 scikit-learn 中可用,因此我们可以使用下面的代码加载它。

from sklearn.datasets import load_breast_cancer

cancer_data = load_breast_cancer()
X = pd.DataFrame(data = cancer_data['data'], columns=cancer_data['feature_names'])
y = cancer_data['target']

训练决策树模型

如往常一样,使用 scikit-learn 训练决策树模型是简单明了的。我们还可以对最大树深度施加约束,以便更容易可视化决策树。

dtree_clf = DecisionTreeClassifier(max_depth=4)
dtree_clf.fit(X, y)

可视化决策树

我们可以使用回归树部分的完全相同的函数来可视化分类树。不过,可视化效果会略有不同。

viz_model = dtreeviz.model(dtree_clf,
                           X_train=X, y_train=y,
                           feature_names=list(X.columns),
                           target_name='cancer')
viz_model.view()

癌症分类决策树。

注意,以上的分类树可视化与上一节的回归树可视化不同。我们看到的不是每个节点处选定特征和目标的散点图,而是显示每个节点处类别分布的彩色直方图。

可视化叶子分布

我们还可以使用相同的函数可视化回归树的叶子分布的类别分布。

 viz_model.ctree_leaf_distributions()

分类树的叶子分布图。图像由作者使用 dtreeviz 创建。

每个叶子都有一个堆叠条形图,展示了该叶子中样本的类别标签分布。大多数叶子中的样本明显属于一个类别,这很好,有助于我们对模型的预测建立信心。

可视化特征空间

我们还可以使用以下函数可视化分类器的特征空间。

viz_model.ctree_feature_space()

上面的特征空间图展示了分类树的训练准确率,以及两个特征的散点图和可以用来区分两个类别的线性决策边界。

摘要

当涉及到可视化基于树的模型时,dtreeviz 是一个强大的库,提供了几个有用的可视化函数。我只介绍了这个库中提供的部分函数,还有许多其他功能,你可以在 dtreeviz GitHub 仓库 中阅读。像往常一样,你可以在 我的 GitHub 上找到这篇文章的所有代码。

加入我的邮件列表

加入我的 邮件列表 来获取关于数据科学内容的更新。当你 注册 时,你还会获得我的免费逐步解决机器学习问题指南!你也可以在 Twitter 上关注我,获取内容更新。

在此过程中,考虑 加入 Medium 社区,阅读成千上万其他作者的文章。

来源

  1. Terence Parr, dtreeviz: 决策树可视化, (2023), GitHub。

使用 Spark 和 Tableau Desktop 创建富有洞察力的仪表板

原文:towardsdatascience.com/creating-insightful-dashboards-with-spark-and-tableau-desktop-1fd91837b8b5?source=collection_archive---------10-----------------------#2023-06-29

使用 Tableau Desktop 工具进行大规模数据可视化自动化

Yu Huang, M.D., M.S. in CSTowards Data Science Yu Huang, M.D., M.S. in CS

·

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

--

作者照片

1. 介绍

作为数据的可视化表示,数据可视化是数据分析中广泛采用的方法,用于从大规模数据集中获得有用的业务洞察(例如,趋势、模式、异常值、关联等)。最近,我介绍了一种使用 Spark、Plotly 和 Dash 开发用于 Web 应用的互动且富有洞察力的数据可视化仪表板的软件开发方法[1]。

类似于 [1],本文使用与 [2] 中相同的开源数据集,展示如何使用 Spark 和 Tableau Desktop [3] 从大规模数据集创建有洞察力的仪表板,而无需编程。

图 1: 工作流的高层次概述。

图 1 显示了工作流的高层次概述。它包括以下主要步骤:

  • 将 Tableau Desktop 用于仪表板创作连接到 Spark

  • 从 Cloud Data Lake 查询数据集

  • 从加载的数据集创建数据可视化图表

  • 从创建的单独图表中创建仪表板

  • 将仪表板发布到 Tableau 服务器以便共享

2. 连接 Tableau Desktop 到 Spark

如 [4] 所述,以下步骤可以用来使用 Spark SQL 作为分布式查询引擎,通过其 JDBC/ODBC [5] 并将 Tableau Desktop 连接到分布式 Spark SQL 引擎 [4]:

  • 安装 Hadoop

  • 设置 Hive

  • 设置 MySQL

  • 设置 Spark

  • 设置 Tableau Desktop

3. 从 Cloud Data Lake 查询数据集

一旦 Tableau Desktop 成功连接到分布式 Spark SQL 引擎,我们应该能够浏览默认模式并查看 Hive Hadoop 集群表 [4]。

从使用 Tableau Desktop 创建仪表板的角度来看,从 Hive Hadoop 集群加载的表和从本地 Microsoft Excel 文件加载的表没有区别。为了方便起见,本文将使用 Tableau Desktop Public 的免费版本,并将本地 Excel 文件(该文件由 [2] 中的数据集 csv 文件转换而来)用于演示目的。

4. 创建数据可视化图表

我们需要先创建单独的可视化图表,然后才能创建可视化仪表板。

我们可以使用 Tableau Desktop 创建多种类型的图表。如 [1] 所述,某些图表适合于可视化连续的数值特征,而其他图表适合于可视化离散的分类特征。

类似于 [1],本文使用 Tableau Desktop 创建以下常见图表以用于演示目的。

  • 数值特征的图表: 散点图、直方图和折线图

  • 分类特征的图表: 条形图、折线图和饼图

4.1 数值特征的图表

Tableau Desktop 使用符号 # 来指示数值特征。本小节展示如何使用 Tableau Desktop 创建以下三种常见的数值特征图表:

  • 散点图

  • 直方图

  • 折线图

4.1.1 散点图

对于一对数值特征,散点图使用每对特征值作为坐标在 2D 平面上绘制一个点。例如,如 [1] 所示,图 2 显示了 21 到 30 岁人群的两个数值特征 Patient IDAdmission Deposit 的散点图。特征 Type of Admission 用于颜色编码。

以下步骤可以用来创建散点图:

  • 将特征 Patientid 拖动到 Columns 区域

  • 拖动特征入院押金并将其放到货架上。

  • 拖动特征入院类型并将其放到标记卡的颜色属性上。

  • 点击标记卡上的下拉菜单并选择圆形

  • 右键点击特征年龄,选择显示过滤器,并仅选择21–30

图 2: 数值特征对的散点图示例。

该散点图揭示了大多数 21–30 岁之间的急诊和创伤患者押金范围在$3,000 到$6,000 之间的商业洞察。

4.1.2 直方图

Tableau Desktop 将直方图视为条形图。因此,列的数值特征需要通过分箱转换为分类特征,而行的数值特征需要聚合,如求和。

可以按照以下步骤创建图 3 中的直方图:

  • 右键点击数值特征病人 ID并选择“创建区间”以创建新特征病人 ID(区间)

  • 拖动新的特征病人 ID(区间)并将其放到货架上。

  • 拖动数值特征入院押金并将其放到货架上,然后点击放置的特征并选择聚合总和

  • 拖动分类特征入院类型并将其放到标记卡的颜色属性上。

  • 点击标记卡上的下拉菜单并选择条形图

  • 右键点击特征年龄,选择显示过滤器,并仅选择21–30

图 3: 数值特征对的直方图示例。

该直方图向我们展示了紧急患者在 21–30 岁之间的总入院押金最少,而急诊患者的总入院押金最多的商业洞察。

4.1.3 折线图

类似于创建散点图,可以按照以下步骤创建图 4 中的折线图:

  • 拖动特征病人 ID并将其放到货架上。

  • 拖动特征入院押金并将其放到货架上。

  • 拖动特征入院类型并将其放到标记卡的颜色属性上。

  • 点击标记卡上的下拉菜单并选择折线

  • 右键点击特征年龄,选择显示过滤器,并仅选择21–30

图 4: 数值特征对的折线图示例。

类似于散点图,该折线图显示了相同的商业洞察,即大多数 21–30 岁之间的急诊和创伤患者押金范围在$3,000 到$6,000 之间。

4.2 分类特征的图表

Tableau Desktop 使用符号Abc来表示分类特征。本小节展示如何使用 Tableau Desktop 创建以下三种分类特征值计数的常见图表:

  • 条形图

  • 折线图

  • 饼图

4.2.1 条形图

作为示例,可以按照以下步骤创建图 5 中分类特征Stay的柱状图:

  • 拖动特征Stay并将其放到Columns架上。

  • 拖动相同的特征Stay并将其放到Rows架上,然后点击已放置的特征并选择聚合Count

  • 点击 Marks Card 上的下拉菜单,选择Bar

  • 右键点击特征Age,选择show filter,仅选择21–30

  • 点击 Marks Card 上的Color属性并选择purple颜色。

图 5: 分类特征值计数的示例柱状图。

该柱状图展示了业务洞察,即对于 21–30 岁之间的患者,更多患者住院时间为 21–30 天,相比其他住院时间间隔。

4.2.2 折线图

类似于柱状图,可以按照以下步骤创建图 6 中分类特征Stay的折线图:

  • 拖动特征Stay并将其放到Columns架上。

  • 拖动相同的特征Stay并将其放到Rows架上,然后点击已放置的特征并选择聚合Count

  • 点击 Marks Card 上的下拉菜单,选择Line

  • 右键点击特征Age,选择show filter,仅选择21–30

  • 点击 Marks Card 上的Color属性并选择purple颜色。

图 6: 分类特征值计数的示例折线图。

类似于柱状图,这个折线图揭示了相同的业务洞察,即对于 21–30 岁之间的患者,更多患者住院时间为 21–30 天,相比其他住院时间间隔。

4.2.3 饼图

创建饼图的过程不如创建折线图和柱状图那样直接。

我们可以按照以下步骤创建图 8 中分类特征Stay的饼图:

  • 拖动特征Stay并将其放到Columns架上。

  • 拖动相同的特征Stay并将其放到Rows架上,然后点击已放置的特征并选择聚合Count

  • 右键点击特征Age,选择show filter,仅选择21–30

  • 点击图表上方右角的Show Me,选择pies图标。一个小的pies图将如图 7 所示出现。

  • 通过选择 Marks Card 上的Size属性来增加饼图的大小,然后选择并拖动饼图的边界框。

  • 拖动聚合功能CNT(Stay)并放到Label属性的 Marks Card 上。

图 7: 将柱状图转换为饼图。

图 8 展示了完成上述步骤后的最终饼图。

图 8: 分类特征值计数的示例饼图。

与条形图和折线图类似,这个饼图确认了相同的业务洞察:对于 21-30 岁之间的患者,住院 21-30 天的患者更多(共 2,197 名),相比于其他住院时间段。

5. 创建仪表板

一旦单独的图表创建完成,我们可以选择并将这些图表组合成一个仪表板。

5.1 用于可视化数值特征对的仪表板

可以按照以下步骤创建一个用于可视化数值特征PatientidAdmission Deposit的仪表板。

步骤 1: 选择仪表板菜单,然后选择新建仪表板

步骤 2: 将以下创建的图表逐一拖动并放置到新的仪表板页面上:

  • 数值特征对的示例散点图

  • 数值特征对的示例直方图

  • 数值特征对的示例折线图

图 9 展示了新创建的仪表板。

图 9: 用于可视化一对数值特征的示例仪表板。

5.2 用于可视化分类值计数的仪表板

可以按照以下步骤创建一个用于可视化分类特征Stay的值计数的仪表板。

步骤 1: 选择仪表板菜单,然后选择新建仪表板

步骤 2: 将以下创建的图表逐一拖动并放置到新的仪表板页面上:

  • categorical_feature_bar

  • categorical_feature_line

  • categorical_feature_pie

图 10 展示了新创建的仪表板。

图 10: 用于可视化分类特征值计数的示例仪表板。

6. 发布仪表板

一旦创建了仪表板,它可以发布到 Tableau Server 以进行共享。

服务器有三种类型:

  • Tableau Public

  • Tableau Server

  • Tableau Cloud

由于使用的是免费的 Tableau Desktop Public 创建这些仪表板,因此这些仪表板只能发布到 Tableau Public。

可以按照[6]中的发布步骤,将仪表板及相关数据集发布到 Tableau Public 服务器上以供公众查看。

7. 结论

Tableau 是一个多层次的视觉数据分析平台,具有复杂的架构。本文介绍了一种将 Spark 与 Tableau 集成的方法,用于首先从大规模数据湖(例如,Hadoop Hive)中查询数据,然后演示如何使用 Tableau Desktop 从加载的数据集中创建有洞察力的仪表板,而无需编程。

作为一个仪表板创建和共享工具,Tableau Desktop 支持多种创建可视化图表的方法。了解从哪里开始以及如何创建有洞察力的可视化图表和仪表板可能会让人感到困惑。本文通过专注于两个简单的可视化场景:一个是可视化一对数值特征,另一个是可视化分类特征的值计数,来帮助快速学习 Tableau Desktop。

参考文献

[1] 黄宇,使用 Spark 和 Plotly Dash 开发互动和洞察性仪表板

[2] 黄宇,预测 Covid-19 患者住院时间

[3] 教程:开始使用 Tableau Desktop

[4] 设置 Tableau 与 Apache Spark 的指南

[5] 分布式 SQL 引擎

[6] 分享你的发现

注意: 本文中的所有照片/图片/图表均由作者提供。

使用 QGIS 创建地图

原文:towardsdatascience.com/creating-maps-with-qgis-1af81b9d0e73

最佳开源 GIS 软件的全面指南

Jake TeoTowards Data Science Jake Teo

·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 12 月 24 日

--

照片由 Louis Hansel 提供,来源于 Unsplash

十多年前,当我开始作为 GIS(地理信息系统)分析师的数据职业生涯时,两款万能的 GIS 软件非常突出。十年后,这两款软件依然是同样的两款。ArcGIS (由 ERSI 提供) 迄今为止仍是最主流的选择,它非常易于使用,功能丰富,并配备了出色的 Python 库。然而,它需要付费,并且仅在 Windows 上运行。

QGIS(量子 GIS)则是最佳的开源替代品。它支持大多数操作系统,并拥有非常活跃的开源社区。无论选择哪款软件,你都不会出错,但随着我深入编程职业,使用 Windows 变得更加困难,因此我通常避免使用企业软件以避免采购行政工作。因此,我现在更倾向于使用 QGIS。

QGIS logo。(CC BY-SA)

以下教程显示的是 QGIS 3.34 Prizren 版本,可能与之前和未来的版本略有不同。

目录

键盘快捷键 添加底图

创建新图层

添加要素

编辑要素

符号系统

使用工具箱

字段计算器

添加标签

地图主题

地图布局

你将学到的内容

在这次演练结束时,你将学会如何制作类似这样的地图,我将用于森林调查。

从这篇文章中你将学到的内容。(作者创建)

键盘快捷键

有几个常用的操作可以通过实现键盘快捷键来简化你的工作。我会自定义它们,使其更容易使用。这可能看起来不重要,但相信我,当你在地图项目中移动和切换时,它会优化一切。

转到键盘快捷键。(作者截图)

这五个操作及其自定义快捷键为:

  • 平移地图:C

  • 放大:V

  • 缩小:X

  • 切换编辑:E

  • 选择要素:R

当然,你可以随意使用你的快捷键。

搜索操作 > 更改 > 输入你想要的快捷键。(作者截图)

添加基础地图

基础地图是你整个地图项目的背景。我们通常使用权威地图作为背景,以便在其上添加图层时能够感知土地细节。在 QGIS 中,可以通过首先在插件中安装插件来添加基础地图,路径为 Plugins > Manage and Install Plugins > QuickMapServices。这些地图是通过外部 API 获取的,因此需要互联网连接。

安装 QuickMapServices 插件。(作者截图)

然后我们可以搜索并将基础图层添加到项目中。在这个例子中,我添加了 Google 卫星影像,它会出现在左侧的图层面板中。

添加基础图层。(作者截图)

我们可以稍后通过上一个截图中的快捷方式访问插件以选择其他基础图层,或者如下所示。

从 Web 菜单访问 QuickMapServices。(作者截图)

创建一个新图层

GIS 中使用了两种类型的图形。之前的 Google 卫星影像由一个颜色像素的网格组成,这被称为栅格。这种栅格有一个分辨率,当你放大足够时,最终会看到单独的像素。

矢量则没有分辨率,放大时始终保持清晰锐利。这些矢量由各种几何类型组成,即折线、多边形和点。GIS 中最常见的矢量存储格式是 ESRI shapefile。要创建此文件,转到 Layer > Create Layer > New Shapefile Layer。

创建一个新的 shapefile(作者截图)

如下所示,需要弹出窗口来定义 shapefile。shapefile 将包括一个属性表,我们可以在其中存储每个要创建的要素的信息。我们可以在此定义任何新字段。创建后,你应该会看到新图层添加到左侧的图层菜单栏中。

创建 shapefile 的属性。(作者截图)

需要注意的是,shapefile 由多个文件组成。特别值得注意的是 .shp 文件,它包含几何坐标,.dbf 文件,它包含属性表,以及 .prj 文件,它包含坐标参考系统。

一个 shapefile 由多个文件组成。(作者截图)

添加要素

创建一个要素(在此案例中是多边形)非常简单。我们只需在菜单中选择图层,启用 切换编辑(铅笔图标),然后点击 添加多边形 图标。接着,我们将鼠标指向底图,并在区域上绘制多边形,点击以完成。最后,当我们完成时,可以右键点击,弹出窗口将提示我们输入要素的属性到属性表中(如果需要)。

添加新要素。(作者截图)

编辑要素

有许多编辑方法,在这种情况下,我将展示如何将现有的多边形拆分为多个部分。首先,如之前所述选择图层并 切换编辑。然后前往 编辑 > 编辑几何 > 拆分要素

拆分要素(作者截图)

你的鼠标箭头将变成靶心圆圈,然后你可以从多边形外部点击,通过绘制多边形线段将其切割成更小的部分,线段结束于多边形外部,并右键点击。

将多边形拆分为多个部分。(作者截图)

使用这个方法,我们将调查区域拆分为六个不同的部分,其中我知道植被类型存在一些差异。

符号系统

要调整图层的填充和边框颜色,我们可以右键点击图层 > 属性 > 选择 符号系统 标签。如果图层中的特征在其属性表中分配了类别或数值范围,我们也可以将颜色显示从 单一符号 更改为 分类分级

在这种情况下,我们只想去掉填充颜色,并将边框设置为红色,因此我们可以点击 填充,然后点击 简单线条 以依次更改它们。

调整图层的颜色和边框。(作者截图)

使用工具箱

处理工具箱包含你工作所需的所有空间处理功能。它们可以通过 处理 > 工具箱 访问,使用简单的搜索框来获取所需的功能。在这种情况下,我们希望通过在每个调查区域内添加随机点来创建一些分层随机样本。

在处理工具箱中获取随机点工具。(作者截图)

工具弹出窗口将显示,我们可以输入每个区域三个点的适当输入,距离至少相隔 10 米。

输入随机点生成的输入。(作者截屏)

这样,点就会作为地图上的新图层生成。

随机点已生成。(作者截屏)

字段计算器

我们之前稍微谈到了属性表,这是一个在 shapefile 或图层中的表格,其中每个要素可以有相应的数据存储在其中。通过图层中要素的空间属性,我们可以使用字段计算器在表格中进行计算。在这个实例中,我们希望在随机点的属性表中创建两个新字段 X 和 Y,以显示它们的坐标。

首先,我们右键点击图层并打开属性表。然后,我们切换到编辑模式 > 打开字段计算器。

打开属性表 > 字段计算器。(作者截屏)

在字段计算器中,我们想创建一个名为 X 的新字段,使用十进制格式。在表达式中,我们输入 X 坐标的计算。在这个实例中,我使用了以米为单位的本地坐标系统,但我希望标签显示全球坐标系统的纬度和经度,因此我进行了转换。

在字段计算器中,我们可以为特定字段添加计算。(作者截屏)

这一次重复相同的步骤以获取 Y 坐标,我们应该得到类似这样的结果。再次切换编辑图标以保存这两个新字段。

X 和 Y 坐标已创建。(作者截屏)

添加标签

现在我们已经为每个点创建了 X 和 Y 坐标,我们怎么在地图上显示它们?这可以通过从工具栏开启标签选项(或右键点击图层 > 属性 > 标签)来完成。然后我们选择图层 > 单一标签 > 并点击表达式对话框以设计标签的显示方式。

在表达式对话框中,我们添加了 X 和 Y 标签,并用新的一行 "X" || '\n' || "Y" 连接它们,以便文本标签将按此方式显示。

使用标签表达式启用标签(作者截屏)

然后我们通过增加字体大小和颜色,并为标签添加黑色缓冲区,使它们在黑暗的底图背景中更加突出。

调整字体大小和颜色(作者截屏)

最后,我们添加了一条白色的标注线,将每个点要素与标签连接起来。

添加标注线(作者截屏)

这样,标签在地图上看起来像这样。效果还不错,但不是最整洁的,因为其中一些标签聚集在一起,还阻碍了调查多边形的显示。

它的样子。(截图由作者提供)

我们可以通过选择移动标签图标,将标签从多边形中移开来解决这个问题,点击其中一个标签并将其移到另一个位置。

手动将标签拖出多边形。(截图由作者提供)

现在我认为最终状态更为干净。在添加其他地图符号时,可能需要进行一些细微调整以构建布局。

最终标签。(截图由作者提供)

地图主题

地图主题 在同一个项目中非常重要,因为你可以显示不同的图层而不必麻烦地切换它们。当我们在同一地图布局中创建多个地图时,主题也很重要,正如你将在下一部分中看到的那样,我们需要主要地图以及插图。

我们已经提前准备好了主要地图,所以我们将添加一个名为基础的主题。

添加一个主题。(截图由作者提供)

为了准备插图,我添加了手头的一个新加坡国家图层,并关闭了其他图层。

添加国家图层。(截图由作者提供)

接下来,我们想要将调查边界添加到国家地图中。为此,我们首先复制调查边界图层。

复制图层。(截图由作者提供)

然后,通过右键点击重复的图层 > 属性 > 符号系统,编辑颜色,将红色填充颜色添加到图层中。仅用这两个图层,调查的位置在国家中变得一目了然。

添加带有新填充颜色的复制图层。(截图由作者提供)

最后,我们将其作为另一个主题,称为插图。通过下拉菜单,我们应该能够在这两个主题之间切换。

添加了两个主题。(截图由作者提供)

地图布局

最后一步是添加所有必要的项目,使其成为一张完整的地图。这通过创建一个布局来完成。

创建一个新的布局。(截图由作者提供)

在布局页面中,我们首先定义页面属性,将其设置为纵向模式。

纵向布局。(截图由作者提供)

首先添加的当然是我们刚刚创建的地图。为此,我们需要选择添加地图图标,然后在空白画布上拖动一个矩形。

将我们的地图添加到布局中(截图由作者提供)

然后,我们可以通过选择移动图标进行一些精细调整。接着,我们可以平移地图上的内容。我们可以使用鼠标进行缩放,但对于精细调整,我们可以输入特定的比例尺。特别重要的是选择之前创建的地图主题。

在布局中调整地图并选择主题。(作者截图)

接下来,我们添加另一个称为插图的地图,它表示初始地图的相同区域,但以更广的比例显示它的位置。再次,我们需要将插图锁定为之前创建的地图主题。

添加插图。(作者截图)

现在,我们可以准备在空白矩形背景中添加其余的地图元素。角落经过圆角处理,使其看起来更美观。在右上角面板中,我们可以调整每个地图元素的层级,以便插图位于矩形背景的顶部。

添加一个空白元素。(作者截图)

完成这些步骤后,我们首先为其添加一个图例。我们可以自定义图例的标题,并删除一些不需要的条目。

添加一个图例。(作者截图)

北箭头也是地图的重要组成部分。有几种箭头类型可以选择。

添加一个北箭头。(作者截图)

最后,我们将为其添加一个比例尺。我们可以更改比例尺符号类型,并在项目属性中自定义比例尺。

添加一个比例尺。(作者截图)

最终,我们可以修饰地图元素,并将完成的地图导出为栅格图像,或保留为 SVG 或 PDF 矢量图形。

导出为图片。(作者截图)

总结

空间分析和制图是数据分析和可视化的一个小众领域。在我从事这个领域的那些年里,我为公共步道展示创建了地图,并将它们发表在杂志、报纸、书籍和期刊上。这是一项有趣且富有挑战性的工作,我相信你可以利用我提供的基础知识进行同样的尝试并在此基础上拓展。

在远程工作的时代培养新的数据科学家

原文:towardsdatascience.com/creating-new-data-scientists-in-the-age-of-remote-work-a590ff10d14?source=collection_archive---------1-----------------------#2023-10-17

学习如何成为一名专业数据科学家的方式现在有所不同,但并非不可能

Stephanie KirmerTowards Data Science Stephanie Kirmer

·

关注 发表在 Towards Data Science · 9 分钟阅读 · 2023 年 10 月 17 日

--

图片由 Tim Gouw 提供,来源于 Unsplash

今天的专栏部分涉及数据科学,但也涉及工作的社会学。作为该领域的高级从业者,我在 Covid-19 以及我们今天工作方式的根本转变之前很久就开始了我的数据科学职业生涯。我在那之前几年就开始了我的职业生涯。因此,我的多年学习如何成为任何职业人士(更不用说专业数据科学家了)是在与许多更有经验的人紧密相处中度过的,这在很多方面使我能够达到现在的位置。我的数据科学家发展不仅仅是关于课程学习和研究,它还涉及以各种方式学习如何成为数据科学家,其中许多都是通过与其他数据科学家在一起工作和进行工作而微妙地吸收的。

重要的是要认识到,工作不仅仅是我们对雇主的付出,即使是在资本主义体系下。它也包括我们得到的回报,而且不仅仅是金钱方面的。工作场所和工作对我们有许多社会和文化影响,不仅仅是领取薪水。特别是,我们通过职业发展我们的社会身份,我们通过观察其他人如何体现这些身份来学习如何体现这些身份。

我们通过职业发展我们的社会身份,我们通过观察其他人如何展示这些身份来学习如何体现这些身份。

我想指出的是,将聪明但缺乏经验的年轻人培养成专业数据科学家的过程,不仅仅是关于数学技能,更是关于社会规范、建立网络以及适应我们工作的环境。许多这些元素在最好的时候也很难获得,而我们现在的情况是远程和混合工作将要求我们找到新的方法来传递这些信息和文化。

(我将要讨论的许多内容可能与许多工作类型相关,但我这里特别关注的是年轻数据科学家的经历。)

让我们深入探讨一下我们从工作中获得的一些东西,这些东西使我们能够成为我们职业中的完全参与者。

规范

我们通过工作发展文化和社会规范,通过观察他人的行为,尤其是观察我们层级中更高职位的人的行为。例如,商业术语、着装规范和社交礼仪等,都可以通过在工作场所的间接学习而发展。在一些工作场所,特别是白领工作场所,这些规范代表了建立社会资本甚至阶层流动的工具。在数据科学领域,有一些规范是可以跨领域通用的,或者在不同的数据科学实践行业中通用。例如,在技术数据科学领域,休闲装确实是常态。还有一些关于如何与你的老板和其他领导者互动的无形标准,包括如何沟通技术话题。还有许多关于如何成为一个专业人士的规范——如何处理商务旅行、与客户互动等等。这些技能对于职业成功迟早都是必不可少的,但根据我的经验,这些大多是通过观察和渗透学习的。

技能

我们还从工作场所获得了实际技能,因为几乎每个人都需要学习一些东西,以在新工作中获得成功。以我为例,经过几年的学术生涯后进入数据科学领域,我需要学习如何将我所了解的数据科学和机器学习应用于商业问题,而不是学术问题。我从我曾担任的数据科学职位的同事那里,特别是我的第一个职位中,学到了新的算法、编码最佳实践以及许多其他技能。这些并不全是正式培训——其中相当一部分是通过观察和吸收其他经验丰富且成功的人士如何做事的方式进行的被动学习。相关地,我们还了解了我们的“未知未知”。我们都有盲点,特别是当我们刚开始时,我们没有意识到自己缺少了一块拼图或一种可能有用的方法,直到这些缺失被指出来为止。观察那些使用你不知道存在的技能的同事可以为你打开构建该技能的大门。

网络

此外,还有一个更抽象但仍然重要的工作场所提供的同事情谊和网络创建元素。理想情况下,当你加入一个团队或公司时,你会与其他人建立关系,而这些联系是职业网络的粘合剂,能够帮助你的职业生涯发展。如果你没有建立这些联系,你不仅在当前工作中处于不利地位,还会在未来受到影响。我个人非常幸运,通过同事间的友谊建立了强大的职业网络,这对我的职业成功起到了关键作用。数据科学家是很棒的人,我们互相帮助寻找机会并建立联系,但你需要一种方式来进入这些网络。通常,有经验的同事会帮助介绍并非正式地为你担保,这会让这件事变得更容易。

成为一名成功的数据科学家涉及以某种方式获取所有这些组件(可能还有其他组件,具体视情况而定)。但正如我提到的,自从我开始职业生涯以来,工作的物理环境发生了很大变化(我认为是变好的)。我们如何在这个新世界中吸引新从业者进入这个领域?

我们的工作地点

在我看来,对于像我们这样从事数据科学/机器学习的白领人士来说,实际上有四种工作方式。

  • 100% 在办公室工作,没有人进行远程工作

  • 混合模式:部分或主要远程工作,加上有意的、目的明确的面对面交流

  • 混合模式:部分或主要远程工作,加上随意、不规律的面对面交流

  • 完全远程工作,无面对面交流

正如许多专家所争辩的那样,混合工作模式可能是我们这些数据科学家在未来几个月和几年中会经历的(如果我们尚未经历的话)。对于大多数人来说,全职办公室工作不会回归,因为我们已经体验到远程工作的自主性和灵活性,并意识到这对我们的生活质量有多大的改善。数据科学家拥有的技能需求很高,我们可以在工作中获得这种灵活性,如果我们想要的话。

理清“混合模式”是什么意思很重要。每周三天通勤到办公室或市中心并不是唯一的工作方式,人们有时见面,有时不见面,而看到关于这方面的讨论如此无感是令人沮丧的。我会把大多数关于混合工作的概念视为“部分或主要远程工作,加上随意、不规律的面对面交流”。这是因为他们试图在疫情前的工作模式下创建混合工作模式。这是对我们希望工作是什么以及值得权衡的内容几乎没有理解的结果。

在一个几乎空无一人的办公室里,与少数你几乎不认识并且很少互动的人待在一起,是初级数据科学家实现上述好处的糟糕方式(如果能实现的话)。对他们、他们的家庭和社区的权衡是巨大的。通勤对我们的个人健康、社会福利以及环境健康都非常糟糕,更不用说浪费了我们可以用来高效工作的宝贵时间。如果我们要花时间去工作地点,那就必须值得。

通勤对我们的个人健康、社会福利和环境健康都非常糟糕,更不用说浪费了我们可以用来高效工作的宝贵时间。

另一个选择是什么?我很喜欢“部分或主要远程工作加上有意的面对面时间”。一个例子可能是日常远程工作与季度现场会议,在这期间,人们前往一个中心地点(不仅仅是地区或本地办公室),进行战略规划、协作工作、社交互动以及相互学习。你可以以多种方式分配这段时间,但关键是这些面对面时间是有目的的,并且设计上就是为了实现这个目的。

有效混合工作的可能特征:

  • 这可能比远程工作或完全在办公室工作更贵。

  • 成功需要思考和规划。

对于那些在数据科学领域工作多年的经验丰富的人员来说,完全远程工作可能是合适的。我们已经融入了网络,学习了社交规范,并获得了技能(最重要的是,更新技能的渠道),这些都不需要特别的面对面时间。然而,我认为我们有责任帮助那些后来的同行,而与初级同事面对面有目的地相处是一种回馈,是值得的。

如何做到这一点

我不会精确规定如何构建有意的面对面时间,因为每家公司和组织的情况都不同,无法一刀切。然而,我有一些针对上述具体目标的建议。

  • 规范:传达规范和文化最好是有意为之。不要仅仅希望你的初级员工会立即理解人际期望的运作方式。我们可能在每周工作 40 多个小时时不需要明确说明这些事情,但情况可能已经改变。比你认为需要的更明确一些。一些对这些规范的被动吸收也会在有意的面对面时间中发生。

  • 技能:数据科学的关键技能根据不同人的定义有所不同,但通常涉及一些编码、统计学、机器学习、商业头脑和沟通技巧的混合。这些都是通过实际工作发展和提高的技能,但我们也可以通过观察其他人如何做这项工作来获得这些技能。通过在你有意安排的面对面时间中创造协作工作机会,而不仅仅是专注于繁琐的会议,你可以帮助这些技能的传递。

  • 网络:我真的认为,在远程工作的数据科学家中建立网络可能是最困难的事情,因为我们经常独自完成日常工作。尽管你会有团队的模型和代码审查,可能会在站会、会议和黑客马拉松中进行连接,但在疫情前的工作场所中,大部分网络发展来自于边缘社交。咖啡机旁的闲聊虽然是陈词滥调,但确实能让同事们更加熟悉。这就是为什么随意而无序的面对面混合工作与有目的的面对面工作完全不同——在这些指定的现场中进行社交互动可以大大促进强大网络的发展。

许多雇主似乎不确定如何引进初级数据科学家,并将他们培养成经验丰富的从业者,因此他们往往倾向于招聘比实际工作需求更高级的人员。虽然这为我这样的从业者提供了更多机会和需求,但对整个领域来说并不利。我们需要让新入行的人员带来新的想法和创造力,并且要为他们提供成长和成功的工具,即使我们不像以前那样整天在同一个办公室里工作。

我们需要让新入行的人员带来新的想法和创造力,并且要为他们提供成长和成功的工具。

作为数据科学领域的资深成员,我们的任务首先是承认现在的情况有所不同,这没问题。我们无法幻想回到不同的工作世界,我也不愿意这样做。工作灵活性让我们的生活和社区变得更好。我们只需要努力识别重要的事项,并弄清楚如何在这个新环境中实现这些目标。

查看更多我的工作请访问 www.stephaniekirmer.com.

使用 scienceplots 和 matplotlib 轻松创建科学图表

原文:towardsdatascience.com/creating-scientific-plots-the-easy-way-with-scienceplots-and-matplotlib-d86a62e2ab46

使用几行 Python 代码立即转换你的 Matplotlib 图形

Andy McDonaldTowards Data Science Andy McDonald

·发布在Towards Data Science ·阅读时间 9 分钟·2023 年 7 月 17 日

--

照片由Braňo提供,来源于Unsplash

在为学术期刊撰写文章时,图表的布局和样式应符合预定格式。这确保了该出版物所有文章的一致性,并且所包含的图表在打印时质量高。

Python 在科学界广泛使用,并提供了创建科学图表的绝佳方式。然而,当我们使用 matplotlib 这一 Python 中最流行的绘图库时,默认的图表质量较差,需要调整以满足要求。

更改 matplotlib 图形的样式可能很耗时,这就是scienceplots库的用武之地。只需几行代码,我们就能立即改变图形的外观,而不必花费太多时间去调整图形的各个部分。

scienceplots库允许用户创建类似于学术期刊和研究论文中常见的简单而信息丰富的图表。不仅如此,它还将某些样式的 DPI 设置为 600,这通常是出版物要求的,以确保高质量的打印图像。

scienceplots 库包含众多样式,包括对多种语言的支持,如中文和日文。你可以通过下面的链接探索scienceplots库中的所有样式。

[## 画廊

科学绘图的 matplotlib 样式。通过创建一个帐户来贡献于 garrettj403/SciencePlots 的开发…

github.com

在本文中,我们将探讨如何将一些基本的常见数据可视化转换为可以包含在科学出版物中的内容。

设置 scienceplots

在使用 scienceplots 库创建图表之前,你需要确保你的计算机上已安装了 LaTeX。LaTeX 是一个排版系统,专为技术和科学文档的创建而设计。

如果你还没有在机器上安装 LaTeX,可以在 这里这里 找到有关 LaTeX 及其安装的更多详细信息。

如果你在 Google Colab 上运行,你可以在单元格中运行以下代码以安装 LaTeX。

!sudo apt-get install dvipng texlive-latex-extra texlive-fonts-recommended texlive-latex-recommended cm-super

在设置 LaTeX 之后,我们可以使用 pip 安装 scienceplots 库:

pip install SciencePlots

一旦在你选择的平台上安装了库和 LaTeX,你就可以导入 scienceplots 库以及 matplotlib

import scienceplots
import matplotlib.pyplot as plt

为绘图创建虚拟数据

在生成一些图表之前,我们首先需要创建一些样本数据。稍后我们将看到 scienceplots 库如何处理实际数据。

在本文的这一部分,我们将使用 np.linspace 创建一些线性间隔的值,然后对这些数据进行一些随机的数学计算。

# Generate x values
x = np.linspace(0, 10, 20)

# Generate y values with random noise
y = np.sin(x)
y2 = np.cos(x)
y3 = y2 * 1.5

一旦我们创建了数据(或从 csv 文件中加载到 pandas 中),我们就可以开始创建我们的图表。

使用 Matplotlib 创建带标记的折线图

我们将使用的第一个图是折线图。这可以通过使用 matplotlib 的 .plot() 函数并传入 xy 参数所需的数据来轻松创建。

由于我们处理的是从方程中派生的变量,因此有时在图例中包含这些变量对于读者理解它们可能很有帮助。

matplotlib 的一个优点是我们可以使用 LaTeX 方程作为标签。我们只需将方程用美元符号($)括起来。

plt.figure(figsize = (6,6))
plt.plot(x, y, marker='o', label='$y=sin(x)$')
plt.plot(x, y2, marker='o', label='$y=cos(x)$')
plt.plot(x, y3, marker='o', label='$y=y2*1.5$')

plt.xlabel('X')
plt.ylabel('Y')
plt.legend()
plt.show()

当我们运行上述代码时,我们将得到以下非常基本的 matplotlib 图形,具有标准颜色。

应用 scienceplots 之前的基本 matplotlib 折线图。图像由作者提供。

尽管上面的图形看起来可用,但它的质量(dpi 和大小)以及样式可能不完全适合在期刊中发表。

将 scienceplots 样式应用于折线图

为了即时转换我们的图形,我们可以添加一行代码:一个 with 语句,它调用 matplotlib 的 style.context 函数,并允许我们传入 scienceplots 提供的多种风格之一。

with plt.style.context(['science', 'high-vis']):
    plt.figure(figsize = (6,6))
    plt.plot(x, y, marker='o', label='$y=sin(x)$')
    plt.plot(x, y2, marker='o', label='$y=cos(x)$')
    plt.plot(x, y3, marker='o', label='$y=y2*1.5$')
    plt.xlabel('X Variable (mm)')
    plt.ylabel('Y Variable')
    plt.legend()
    plt.show()

当我们运行上述代码时,我们会得到如下图,这图更适合用于期刊出版。

应用 scienceplots 风格后的 Matplotlib 线图。图片由作者提供。

该图形简单(即没有图表杂质),不同的线条很容易区分。此外,当在 Jupyter Notebook 中查看此图形时,即使我们设置了相对较小的图形尺寸,它可能看起来仍然非常大。这是因为图形的 DPI 设置为 600,这通常是许多出版物的要求,并确保图形尽可能清晰。

让我们尝试应用另一种风格。这次我们将使用 电气和电子工程师协会(IEEE) 的风格。

要做到这一点,我们只需将 high-vis 替换为 ieee 即可改变风格。

with plt.style.context(['science', 'ieee']):
    plt.figure(figsize = (6,6))
    plt.plot(x, y, marker='o', label='$y=sin(x)$')
    plt.plot(x, y2, marker='o', label='$y=cos(x)$')
    plt.plot(x, y3, marker='o', label='$y=y2*1.5$')
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.legend()
    plt.show()

当我们运行上述代码时,我们将获得符合 IEEE 推荐风格的下图。

应用 scienceplots IEEE 风格后的 Matplotlib 线图。图片由作者提供。

带有 Science Plots 的直方图

在之前的示例中,我们探讨了如何将风格应用于线图。

但是我们能将相同的风格应用于其他类型的图吗?

当然可以!

让我们看看如何将这种风格应用于直方图。

首先,让我们使用以下代码创建一个使用伽马射线(地质构造自然放射性测量)数据的 matplotlib 图形。为了展示第二个数据集,我将相同的数据调整了 20 个 API 单位。

plt.figure(figsize = (6,6))
plt.hist(df['GR'], bins=100, label='GR1', alpha =0.5)
plt.hist(df['GR']+20, bins=100, label='GR2', alpha=0.5)
plt.xlim(0, 150)
plt.xlabel('Gamma Ray')
plt.ylabel('Frequency')
plt.legend()
plt.show()

当我们运行上述代码时,我们会得到以下图形。

简单的 Matplotlib 伽马射线测量直方图。图片由作者提供。

我们会注意到它使用了 matplotlib 的标准风格,看起来非常基础,两组数据重叠在一起。这导致一些信息被遮盖。

让我们看看 IEEE 风格如何改变了这些内容。

with plt.style.context(['science', 'ieee']):
    plt.figure(figsize = (6,6))
    plt.hist(df['GR'], bins=100, label='GR1')
    plt.hist(df['GR']+20, bins=100, label='GR2')
    plt.xlim(0, 150)
    plt.xlabel('Gamma Ray')
    plt.ylabel('Frequency')
    plt.legend()
    plt.show()

当我们运行上述代码时,我们得到应用 IEEE 风格后的下图。然而,第二个 GR 数据集仍然遮挡了第一个。

应用 scienceplots IEEE 风格后的 Matplotlib 伽马射线测量直方图。图片由作者提供。

也许我对 scienceplots 库能处理任何重叠并自动应用透明度有较高的期望。

然而,这并不是太费力。我们只需为每个数据集添加 alpha 参数。

with plt.style.context(['science', 'ieee']):
    plt.figure(figsize = (6,6))
    plt.hist(df['GR'], bins=100, label='GR1', alpha=0.5)
    plt.hist(df['GR']+20, bins=100, label='GR2', alpha=0.5)
    plt.xlim(0, 150)
    plt.xlabel('Gamma Ray')
    plt.ylabel('Frequency')
    plt.legend()
    plt.show()

当我们运行上述代码并进行 alpha 更改时,我们得到如下图形。

在应用 scienceplots 并向数据集添加透明度后得到的伽马射线测量直方图。图片由作者提供。

现在我们可以看到两个数据集的条形图变化。

建议您检查预期出版物的样式指南,以确保使用透明度是可接受的。在大多数情况下,应该是可以的,但值得检查一下。

将 Science Plots 应用于 Seaborn 图形

我们不仅限于将 scienceplots 库的样式应用于 matplotlib 图形。我们还可以将其应用于Seaborn图形。这是因为 Seaborn 基于 matplotlib 代码。

有时候,在创建图形时,Seaborn提供了比 matplotlib 更简便的方法来创建某些图形。例如,当我们有一个基于文本的分类变量时,我们希望能够绘制它,而不需要为每个类别添加一个单独的散点图。

在这个例子中,我们有一些中子孔隙度和体积密度数据——常见的测井测量。对于每个测量,我们还拥有一个岩性类别。

这个数据集源自 Force 2020 Xeek 机器学习竞赛数据集。详细信息可以在文章末尾找到。

首先,我们需要将 seaborn 导入到我们的笔记本中。

import seaborn as sns

导入 seaborn 库后,我们可以使用以下代码创建散点图。

plt.figure(figsize=(6, 6))
sns.scatterplot(data=df, x='NPHI', 
                y='RHOB', hue='LITH', s=10)

plt.ylabel('Bulk Density (RHOB) - g/cc')
plt.xlabel('Neutron Porosity (NPHI) - dec')
plt.ylim(2.8, 1.4)
plt.show()

当我们运行上述代码时,我们会得到以下散点图,其中数据通过不同的岩性进行了着色。

使用 Seaborn 生成的基本中子密度交叉图。图片由作者提供。

看起来还不错。然而,我们需要确保样式适合预期的期刊,并且颜色对所有读者都是可访问的。

为了应用我们的 scienceplots 样式,我们可以使用与之前相同的语法:

with plt.style.context(['science', 'ieee']):
    plt.figure(figsize=(10, 8))
    sns.scatterplot(data=df, x='NPHI', y='RHOB', hue='LITH', s=10)
    plt.ylim(2.8, 1.4)
    plt.title('RHOB vs NPHI Crossplot')
    plt.show()

当我们运行上述代码时,我们会得到以下具有改进样式的图形,包括新的色彩调色板。

Seaborn 散点图,应用 scienceplots 样式,展示了体积密度与中子孔隙度的关系,并根据岩性变化着色。图片由作者提供。

选择图形的色彩调色板可能会很棘手且耗时;然而,经过一些考虑,它可以使您的图形对视觉障碍的读者更为友好。

如果您想找到一些工具来帮助您选择有效且可访问的色彩调色板,请查看下面的链接。

  • 4 种帮助您选择数据可视化色彩调色板的必备工具

此外,当在黑白打印时,一些颜色可能难以区分。因此,考虑为不同类别分配不同的形状可能是值得的。这一点在我们处理来自实验室过程的小数据集时尤为重要。

摘要

在本文中,我们探讨了如何迅速将基本的 matplotlib 图形转换为可以轻松添加到科学出版物中的内容。这些图形可能仍需进一步调整,但通过使用 scienceplots 库,我们可以基本实现目标。此外,建议检查你所选期刊的作者工具包,以确保你创建的图表符合要求的标准。

本教程使用的数据集

训练数据集是 Xeek 和 FORCE 2020 机器学习比赛的一部分 (Bormann et al., 2020)。该数据集在 Creative Commons Attribution 4.0 International 许可下发布。

完整的数据集可以通过以下链接访问:doi.org/10.5281/zenodo.4351155

感谢阅读。在离开之前,你绝对应该订阅我的内容,获取我的文章到你的邮箱中。 你可以在这里做到这一点!

其次,你可以通过注册会员来获得完整的 Medium 体验,并支持成千上万的其他作者和我。这只需每月 $5,你就能全面访问所有精彩的 Medium 文章,并且有机会通过你的写作赚取收入。

如果你使用 我的链接注册你将直接通过一部分费用支持我,而且不会增加你的开支。如果你这样做,非常感谢你的支持。

使用 PCA 创建评分和排名

原文:towardsdatascience.com/creating-scores-and-rankings-with-pca-c2c3081fdb26

使用 R 语言基于多个变量为观测值创建评分

Gustavo SantosTowards Data Science Gustavo Santos

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

--

Joshua Golde 提供的照片,来源于 Unsplash

介绍

我对主成分分析[PCA]的研究越多,我越喜欢这个工具。我已经写过其他关于这个主题的文章,但我不断学习更多关于这个美丽数学“幕后”的知识,当然,我会与您分享这些知识。

主成分分析(PCA)是一组基于数据协方差和相关性的数学变换。它基本上会查看数据点,找出变异性最大的位置。一旦完成,这些数据会被投影到那个方向。新数据会被投影到一个新的轴上,称为主成分

数据被投影到一个新的轴上,以解释尽可能多的变异性。

投影本身就是变换。而新数据具有许多属性,可以帮助我们数据科学家更好地分析数据。例如,我们可以进行因子分析,将相似的变量组合成一个因子,从而减少数据的维度。

另一个有趣的属性是通过观察相似性来创建排名的可能性,如我们将在本文中看到的。

使用 PCA 创建评分和排名

数据集

在这个练习中,我们将使用mtcars,一个著名的“玩具数据集”,包含有关汽车的一些信息。尽管这是一个非常著名的数据,但它仍然非常适合作为教学示例,并且是开放的,许可证为 GPL 3.0。

我们还可以加载tidyverse库进行数据整理,使用psych进行 PCA。

# imports
library(tidyverse)
library(psych)

# dataset
data("mtcars")

这里是数据的小提取。

mtcars:在 Dplyr 中原生支持。图片由作者提供。

编码

现在,让我们开始编写代码吧。

重要的是要说明,PCA 和因子分析仅适用于定量数据。因此,如果你有定性或分类数据,可能对应分析会更适合你的情况。

使用 PCA 进行良好的因子提取需要变量对之间存在统计显著的相关性。如果相关性矩阵中有太多低相关性,则提取的因子可能不会很好。

Bartlett 检验

但是如何确保这些因子呢?我们可以使用Bartlett 检验,在零假设下,相关性在统计上等于零[p-value > 0.05],而在对立假设下,相关性不同于 0 [p-value ≤ 0.05]。

# Bartlett Test
cortest.bartlett(mtcars)

# RESULT
$chisq
[1] 408.0116

$p.value
[1] 2.226927e-55

$df
[1] 55

如我们所见,我们的结果是 p 值等于 0,因此可以拒绝零假设,我们可以理解提取的因子将是合适的。

接下来,我们可以使用 psych 库运行 PCA 部分。我们可以使用 pca() 函数完成这项任务。我们将输入:

  • 数据集(仅包含数值)

  • 所需因子的数量。在这种情况下,所有的 11 个,因此我们使用数据的第二维度位置(dim(mtcars)[2]

  • 旋转方法:none。现在这可能会改变我们的结果,正如我们将看到的。默认的旋转是 “varimax”,其目的是最大化因子载荷的方差,得到一个更简单的矩阵,其中每个变量高度关联于一个或几个因子,从而更易于解释。

#PCA
pca <- pca(mtcars, nfactors=dim(mtcars)[2], rotate='none')

一旦代码运行完成,我们可以查看碎石图,它会告诉我们每个主成分捕获的方差量。

# Scree Plot
barplot(pca$Vaccounted[2,], col='gold')

接下来,结果将被显示出来。

碎石图。84% 的方差由前两个组件捕获。图片由作者提供。

凯泽准则

下一步是查看我们将用于分析的主成分。一个好的方法是查看特征值,并确定哪些值大于 1。这个规则也被称为凯泽的潜在根准则。

# Eigenvalues
pca$values

[1] 6.60840025 2.65046789 0.62719727 0.26959744 0.22345110 0.21159612
[7] 0.13526199 0.12290143 0.07704665 0.05203544 0.02204441

注意: (1) 有 11 个特征值,每个主成分一个; (2) 只有前两个符合凯泽规则的要求。因此,我们再对两个成分运行一次 PCA。

# PCA after Kaiser's rule applied
pca2 <- pca(mtcars, nfactors=2, rotate='none')

# Variance
pca2$Vaccounted

                            PC1       PC2
Proportion Var        0.6007637 0.2409516
Cumulative Var        0.6007637 0.8417153

绘制变量

为了绘制变量,我们需要先收集载荷。载荷矩阵显示了每个变量与每个主成分的相关性。因此,数值将在 -1 和 1 之间,记住越接近零,主成分和变量的相关性越小。越接近 1/-1,相关性越强。

载荷是变量与主成分的相关程度。

# PCA Not rotated
loadings <- as.data.frame(unclass(pca2$loadings))
# Adding row names as a column
loadings <- loadings %>% rownames_to_column('vars')

# RESULT
   vars        PC1         PC2
1   mpg -0.9319502  0.02625094
2   cyl  0.9612188  0.07121589
3  disp  0.9464866 -0.08030095
4    hp  0.8484710  0.40502680
5  drat -0.7561693  0.44720905
6    wt  0.8897212 -0.23286996
7  qsec -0.5153093 -0.75438614
8    vs -0.7879428 -0.37712727
9    am -0.6039632  0.69910300
10 gear -0.5319156  0.75271549
11 carb  0.5501711  0.67330434

然后,由于我们只有两个维度,我们可以轻松地使用 ggplot2 绘制它们。

# Plot variables
ggplot(loadings, aes(x = PC1, y = PC2, label = vars)) +
  geom_point(color='purple', size=3) +
  geom_text_repel() +
  theme_classic()

显示的图形如下。

载荷图,展示了基于 PC1 x PC2 的变量之间的关系。图片由作者提供。

太棒了!现在我们对哪些变量彼此相关有了一个很好的了解。例如,油耗(Miles Per Gallon)与齿轮数、发动机类型、传动类型、drat 等因素的相关性更大。另一方面,它与 HP 和重量正好相反,这非常有意义。我们思考一下:一辆车的动力越大,燃烧的油也越多。同样,重量也如此。移动一辆更重的车需要更多的动力和油,导致油耗比率更低。

旋转版本

好的,现在我们查看了未旋转的 PCA 版本,让我们来看一下默认的"varimax"旋转的旋转版本。

# Rotation Varimax
prin2 <- pca(mtcars, nfactors=2, rotate='varimax')

# Variance
prin2$Vaccounted
                            RC1       RC2
Proportion Var        0.4248262 0.4168891
Cumulative Var        0.4248262 0.8417153

# PCA Rotated
loadings2 <- as.data.frame(unclass(prin2$loadings))
loadings2 <- loadings2 %>% rownames_to_column('vars')

# Plot
ggplot(loadings2, aes(x = RC1, y = RC2, label = vars))+
  geom_point(color='tomato', size=8)+
  geom_text_repel() +
  theme_classic()

由 2 个组件捕获的方差相同(84%)。但请注意,现在方差的分布更加分散。旋转后的组件 RC1 [42%]和 RC2 [41%];与没有旋转的版本中的 PC1 [60%]和 PC2 [24%]相比。然而,变量保持在类似的位置,只是现在稍微旋转了一下。

加载量图,显示基于 RC1 x RC2 的变量之间的关系。图片由作者提供。

共同性

对于带旋转和不带旋转的两种主成分分析(PCA),最后的比较是关于共同性。共同性会显示在应用凯瑟规则并从分析中排除了一些主成分后,每个变量的方差损失了多少。

# Comparison of communalities
communalities <- as.data.frame(unclass(pca2$communality)) %>%
  rename(comm_no_rot = 1) %>%
  cbind(unclass(prin2$communality)) %>% 
  rename(comm_varimax = 2)

     comm_no_rot comm_varimax
mpg    0.8692204    0.8692204
cyl    0.9290133    0.9290133
disp   0.9022852    0.9022852
hp     0.8839498    0.8839498
drat   0.7717880    0.7717880
wt     0.8458322    0.8458322
qsec   0.8346421    0.8346421
vs     0.7630788    0.7630788
am     0.8535166    0.8535166
gear   0.8495148    0.8495148
carb   0.7560270    0.7560270

如所见,两个方法中捕获的方差是相同的。

很好。但是这会影响排名吗?让我们接着检查。

排名

一旦我们进行了 PCA 转换,创建排名是非常简单的。我们需要做的就是收集组件的方差比例,使用pca2$Vaccounted[2,]pca$scores,然后进行相乘。因此,对于 PC1 中的每个分数,我们将其乘以该 PCA 运行的对应方差比例。最后,我们将这两个分数添加到原始数据集 mtcars 中。

### Rankings ####

#Prop. Variance Not rotated
variance <- pca2$Vaccounted[2,]

# Scores
factor_scores <- as.data.frame(pca2$scores)

# Rank
mtcars <- mtcars %>% 
  mutate(score_no_rot = (factor_scores$PC1 * variance[1] + 
                           factor_scores$PC2 * variance[2]))

#Prop. Variance Varimax
variance2 <- prin2$Vaccounted[2,]

# Scores Varimax
factor_scores2 <- as.data.frame(prin2$scores)

# Rank Varimax
mtcars <- mtcars %>% 
  mutate(score_rot = (factor_scores2$RC1 * variance2[1] + 
                        factor_scores2$RC2 * variance2[2]))

# Numbered Ranking
mtcars <- mtcars %>% 
  mutate(rank1 = dense_rank(desc(score_no_rot)),
         rank2 = dense_rank(desc(score_rot)) )

结果如下所示。

基于 PCA/因子分析的排名。图片由作者提供。

上面的表格是未旋转PCA 的 TOP10。观察它如何突出显示低mpg、高hpcylwtdisp的汽车,就像加载量建议的那样。

底下的表格是varimax 旋转PCA 的 TOP10。由于方差在两个组件之间更加分散,我们看到了一些差异。例如,disp变量现在不那么均匀了。在未旋转的版本中,PC1 的加载量主导了该变量,具有 94%的相关性,而在 PC2 中几乎没有相关性。对于 varimax,它在 RC1 中为-73%,在 RC2 中为 60%,因此有些混乱,因此尽管排名有所不同,但它显示了高和低的数字。mpg也可以这样说。

基于相关变量的排名

在我们完成所有这些分析后,我们还可以为排名创建设定更好的标准。在我们的研究案例中,我们可以这样说:我们想要最佳的mpgdratam手动变速箱(1)。我们已经知道这些变量是相关的,因此结合使用它们进行排名会更容易。

# Use only MPG and drat, am

# PCA after Kaiser's rule applied: Keep eigenvalues > 1
pca3 <- pca(mtcars[,c(1,5,9)], nfactors=2, rotate='none')

#Prop. Variance Not rotated
variance3 <- pca3$Vaccounted[2,]

# Scores
factor_scores3 <- as.data.frame(pca3$scores)

# Rank
mtcars <- mtcars %>% 
  mutate(score_ = (factor_scores3$PC1 * variance3[1] + 
                           factor_scores3$PC2 * variance3[2])) %>% 
  mutate(rank = dense_rank(desc(score_)) )

以及结果。

按照 MPG、Drat 和变速箱进行排名。图片由作者提供。

现在结果变得很有意义。以本田思域为例:它具有高 MPG、数据集中最高的 drat 和 am = 1。现在看看排名为 4 和 5 的车。保时捷的 mpg 较低,但 drat 高得多。莲花则正好相反。成功!

在你离开之前

本文旨在向你展示 PCA 因子分析的介绍。我们可以在这个教程中看到工具的强大。

然而,在进行分析之前,重要的是要研究变量的相关性,然后设定排名创建的标准。还需注意 PCA 受异常值的影响很大。所以如果你的数据包含过多的异常值,排名可能会扭曲。解决办法是对数据进行标准化处理。

如果你喜欢这些内容,不要忘记关注我的博客以获取更多信息。

gustavorsantos.medium.com/?source=post_page-----c2c3081fdb26-------------------------------- [## Gustavo Santos - Medium]

在 Medium 上阅读 Gustavo Santos 的文章。数据科学家。我从数据中提取见解以帮助人们和公司…

gustavorsantos.medium.com/?source=post_page-----c2c3081fdb26--------------------------------

Linkedin 上也可以找到我。

这是这段代码的 GitHub 仓库。

github.com/gurezende/Studying/tree/master/R/Factor%20Analysis%20PCA?source=post_page-----c2c3081fdb26-------------------------------- [## Studying/R/Factor Analysis PCA at master · gurezende/Studying]

你现在无法执行该操作。你已在另一个标签页或窗口中登录。你在另一个标签页或窗口中注销了...

github.com/gurezende/Studying/tree/master/R/Factor%20Analysis%20PCA?source=post_page-----c2c3081fdb26--------------------------------

参考文献

FÁVERO, L.; BELFIORE, P. 2022. Manual de Análise de Dados. 第 1 版. LTC.

www.datacamp.com/tutorial/pca-analysis-r

在 Grafana 中创建时间序列图

原文:towardsdatascience.com/creating-time-series-plots-in-grafana-f9dade30dff4

学习如何使用 Python 和 Grafana 绘制动态时间序列图

Wei-Meng LeeTowards Data Science 魏孟 李

·发表在 Towards Data Science ·5 分钟阅读·2023 年 2 月 9 日

--

图片来源:Dan LohmarUnsplash

Grafana 是一个多平台开源分析和交互式可视化 Web 应用程序。如果你进行数据分析,Grafana 是一个宝贵的工具,它允许你使用各种数据源以及不同的内置可视化类型来构建仪表板。

你可以从 grafana.com/grafana/download 下载并安装 Grafana。支持的平台包括 Mac、Windows、Linux、Docker 和 ARM。

在这篇文章中,我将向你展示如何构建一个显示特定地点的温度和湿度的时间序列图。

对于这篇文章,我将假设你已经熟悉 Grafana 和 MySQL 的基础知识。如果你对 Grafana 不熟悉,可以查看我在 Code Magazine 上的文章 (www.codemag.com/Article/2207061/Developing-Dashboards-Using-Grafana)。

[## 使用 Grafana 开发仪表板

作者:魏孟 李 发表在:CODE Magazine: 2022 - 七月/八月 最后更新:2022 年 8 月 31 日 在我早期的文章中…

www.codemag.com](https://www.codemag.com/Article/2207061/Developing-Dashboards-Using-Grafana?source=post_page-----f9dade30dff4--------------------------------)

示例应用

对于这篇文章,我将涉及三个组件:

  • 一个 Python 脚本用于持续将值写入 MySQL 服务器。在现实生活中,Python 脚本可能会从传感器读取值。本文中,我将模拟一些随机的温度和湿度值。这些值将每五秒发送到 MySQL 服务器。

  • 一个 MySQL 服务器,包含一个数据库,用于存储所有由 Python 脚本发送的值。

  • Grafana 仪表盘从 MySQL 服务器中提取数据。

图片由作者提供;Python 的徽标来自 commons.wikimedia.org/wiki/File:Python_Windows_source_code_icon_2016.svg

MySQL 服务器将有一个名为WeatherReadings的数据库,其中包含一个名为Readings的表。表的架构如下:

本文中的所有图片均由作者创建。

创建数据库和表

以下 SQL 语句在 MySQL 中创建数据库和表:

CREATE DATABASE `WeatherReadings`; 
USE WeatherReadings;
CREATE TABLE `Readings` (
  `datetime` datetime NOT NULL,
  `temperature` float DEFAULT NULL,
  `humidity` float DEFAULT NULL,
  PRIMARY KEY (`datetime`)
) 

创建 Python 脚本

以下 Python 脚本使用mysqlclient包连接到 MySQL 服务器,并将值写入数据库表:

import datetime
import random
import threading
import MySQLdb                                 # conda install mysqlclient

db = MySQLdb.connect (user='user1',            # database user account
                      passwd='password',       # password for user1
                      host='127.0.0.1',        # IP address of MySQL
                      db='WeatherReadings')    # database name

cur = db.cursor()

def insert_record(datetime, temperature, humidity) :    
    try:
        cur.execute("""
            INSERT INTO Readings (datetime, temperature, humidity) VALUES
            (%s, %s, %s)
        """, (datetime, temperature, humidity))
        db.commit()
    except MySQLdb.Error as e:
        print ("Error %d: %s" % (e.args[0], e.args[1]))
        db.rollback()

def update():
    threading.Timer(5.0, update).start()      # call update() every 5s
    insert_record(datetime.datetime.utcnow(), # datetime in UTC
                  random.uniform(20, 39),     # temperature
                  random.uniform(0.7, 0.9))   # humidity

update()

上述脚本执行了以下操作:

  • 使用 user1 账户和“password”作为密码连接到 MySQL 服务器的WeatherReadings数据库。

  • 定义 insert_record() 函数,将新的温度和湿度值插入到读数表中。

  • update() 函数每五秒调用 insert_record() 函数,模拟温度和湿度值。

注意,当保存日期和时间时,我使用了 utcnow() 函数而不是 now() 函数。utcnow() 函数返回当前的 UTC(协调世界时)日期和时间。例如,新加坡的时区是 UTC +8,因此如果新加坡当前的日期和时间是 2023–02–08 10:07:38,那么 UTC 时间是 2023–02–08 02:07:38(减去 8 小时)。存储时间为 UTC 的原因是 Grafana 将根据浏览器的时区自动将 UTC 时间转换为本地时间。

配置 Grafana

在 Python 脚本完成之后,现在可以配置 Grafana 了。

添加 MySQL 数据源

在 Grafana 中,添加一个新的MySQL 数据源并按如下配置:

向仪表盘添加面板

在 Grafana 中创建一个新的仪表盘,并点击添加新面板按钮:

使用默认的时间序列面板,并使用以下值配置面板:

注意使用的 SQL 语句:

Select
    datetime as time,
    temperature,
    humidity * 100
FROM Readings

你需要将 datetime 字段设置为 time,以便时间序列面板能够识别数据作为时间序列。

配置面板的标题属性如下:

按如下配置面板的图表样式属性:

点击应用按钮以退出面板并返回到仪表板。现在你应该能看到面板显示温度和湿度读数的时间序列:

动态更新时间序列

点击显示在刷新按钮旁边的箭头,然后选择5 秒

这将导致面板每五秒从 MySQL 数据源获取数据。现在面板应该每五秒刷新一次:

如果你喜欢阅读我的文章并且它对你的职业/学习有所帮助,请考虑注册成为 Medium 会员。每月费用为 5 美元,这将为你提供对 Medium 上所有文章(包括我的文章)的无限制访问。如果你使用以下链接注册,我将获得少量佣金(对你没有额外费用)。你的支持意味着我将能够投入更多时间来撰写类似的文章。

[## 使用我的推荐链接加入 Medium - 韦梦李

阅读韦梦李的每一篇文章(以及 Medium 上其他成千上万位作者的文章)。你的会员费直接支持……

weimenglee.medium.com](https://weimenglee.medium.com/membership?source=post_page-----f9dade30dff4--------------------------------)

摘要

在这篇文章中,你学会了如何使用 Grafana 和 MySQL 服务器绘制时间序列。在实际应用中,你的数据将不断地发送到数据库服务器,Grafana 将配置为每隔几秒刷新一次数据(由你配置)。

创建你自己的 ChatGPT 无需编码——一步步指南

原文:towardsdatascience.com/creating-your-own-chatgpt-without-coding-a-step-by-step-guide-a4523e6a46e1

Ahmed FessiTowards Data Science Ahmed Fessi

·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 11 月 12 日

--

几乎在 ChatGPT 发布一年后,OpenAI 继续用其平台的新功能和能力给我们带来惊喜。

OpenAI 最近的发布说明确实提供了一个创建自定义 ChatGPT 的新功能,称为 GPTs

GPTs 是提升生产力的另一种方式。如果你曾经使用过 ChatGPT,你可能已经发现了许多能够帮助你日常活动的用例:它可以优化你的推文,帮助头脑风暴文章创意,生成销售话术的变体,审查代码或帮助你学习世界语。GPTs 可以帮助你制作一个“专业化”的 ChatGPT,针对你的需求进行个性化,并且在某一特定领域表现出色,而不是过于通用。

这开启了新的可能性。在本文中,我们将一步一步地创建一个新的自定义 GPT,并讨论这个新功能的一些限制。

前提条件

前提条件很简单,你需要:

  • 一个 ChatGPT Plus 许可证或企业账户(这意味着,基本上,你不能使用免费账户创建 GPTs)。

  • 还有一个新的 GPT 的想法(考虑你的主要用例,或请 ChatGPT 帮助你进行头脑风暴)。这里是 OpenAI 分享的一些用例以供参考:

OpenAI 提供的 GPTs 示例——OpenAI 博客——作者截图

用例

在这篇文章中,我想以我过去遇到的痛点为基础,研究一个实际的用例。

在之前的职业经历中,我不得不处理面向客户的 GDPR 主题。GDPR,或通用数据保护条例,是一项关于数据保护和隐私的欧盟(EU)法律。它旨在赋予人们对其个人信息及其使用方式的更多控制。然而,它的实施可能很棘手。法律文本超过 150 页,而且相当技术化。作为技术人员,探索法律和监管主题可能具有挑战性。在那个时候,ChatGPT 还不存在,因此我不得不提升技能并寻求外部法律专家的帮助(这也增加了成本),直到我拥有更多的自主权。但即便如此,我仍然面临新的主题,例如:处理数据跨境传输、管理安全事件、更新隐私政策或进行数据保护影响评估。

所以我想要构建一个当时我没有的 GDPR 智能助手,全天候提供帮助。

当然,这只是一个用例,但在我看来,这是一个有趣的用例,因为它解决了一个明确的需求,并且有明显的商业案例。

当你使用 ChatGPT 并询问关于 GDPR 的问题时,ChatGPT 已经“消耗了所有互联网”(虽然不完全如此,但很大一部分),因此它确实有一些准确的信息,但也有一些不够准确和精确的信息。在这种情况下,GPTs 将允许我使用特定选项来限制知识,这些选项我们将在指南中看到,还可以调整语言语调和其他一些细节。

之后你可以应用相同的逻辑,并将使用案例调整到你的需求中,通常情况下,如果你想基于知识数据库、系列文章、PDF 集合或类似使用案例来构建聊天机器人,你可以遵循相同的步骤。

让我们开始构建吧

要创建你的 GPT,你需要使用 OpenAI 提供的编辑器。

编辑器由 2 个区域组成:左侧有 2 个标签页,用于创建和配置,而右侧用于预览和测试。

你可以使用以下链接打开它(查看先决条件,你需要 ChatGPT Plus 或 Enterprise 许可证):chat.openai.com/gpts/editor

编辑器看起来是这样的:

创建新 GPT 的编辑器 — OpenAI — 作者截图

我们将按照编辑器指南中的建议步骤进行。我以以下提示开始:“我想创建一个新的 GPT,以帮助回答与 GDPR 相关的问题。它可以是与 GDPR 相关的一般问题(如主要原则),也可以是特定于用户情况的问题(例如,对于特定的安全事件,是否需要通知)。

在提供这些指令时,你应该尽可能具体。我的提示可能不是完美的,但我们将有机会随着进展进行改进。

现在,根据我提供的描述,我得到了一个名称建议:

GPT 名称建议 — OpenAI — 作者截图

我不喜欢“GDPR Guide”这个名字,所以我会建议另一个名字:“My GDPR companion”。新的 GPT 将获得这个名称,并会建议一个徽标:

徽标建议和 GPT 初始化 — OpenAI — 作者截图

我觉得徽标不是很好,因为 GDPR 是一项欧洲法律,我认为徽标应该反映这一点。不过有趣的是,我得到了一个代表安全和隐私的徽标,这些都是 GDPR 的主要支柱。我们还可以在预览部分看到徽标(生成)、简短描述(生成)和问题示例(生成)。这有助于你初步了解 GPT 的外观以及它预计回答的问题。

因此,鉴于我想调整徽标,我将提供额外的指令,请求修订“GDPR 是一项欧洲法律。我认为徽标应该反映这一点。

My GDPR Companion GPT 更新后的徽标 — OpenAI — 作者截图

现在的徽标在我看来更好。编辑考虑了我的意见,并提供了一个与欧洲旗帜(十二颗黄色星星和蓝色背景)更相关的徽标。

然后,编辑请求更多信息以细化其行为。

编辑器机器人请求更多信息以细化 GPT 行为 — OpenAI — 作者截图

这是真正的 ChatGPT 增强功能,因为它实际上会主动问你问题,并指导你以确保它提供正确的语调,并且很好地适应你的需求。我回答了这些问题,并确认我们一切就绪。它还会以简洁的方式重新表述你的输入,以表明它“理解”了你分享的内容。

GPT 准备情况确认 — OpenAI — 作者截图

如你所见,我还要求添加一种免责声明。在一些领域,如法律建议或财务建议,此类免责声明是必要的,以确保用户清楚地理解该工具仅用于帮助,而不承担责任。

现在,我们已经设置好了 GPT,在测试之前,我们需要在配置标签中添加一些配置:

配置标签,由 GPT 编辑器生成 — OpenAI — 作者截图

多个字段的设置不完全符合我的期望。所以我将调整其中一些,包括

  • 描述:我想要一个更简短的,

  • 指令:细化一些指令,

  • 对话启动者:添加一个关于如何开始使用 GDPR 的对话启动者,

  • 我还将添加知识库。对于知识库,我推荐添加一个(普通)文本文件。我尝试使用 PDF,但结果不是很准确(请查看限制)。供你参考,我使用的原始来源位于这里。(你可以上传多个文件)。

  • 还有一些高级配置功能允许与 API 交互。这些功能也很有趣,但在本文中,我将指南限制为“无需编码”的方法。

我的 GDPR Companion GPT 已经准备好了 — OpenAI — 作者截图

现在是测试的时候了!

从一个简单的请求开始,询问法律中特定条款的信息。注意我可以仅提供简短的提示。不需要说明这是关于 GDPR 的或从知识库中获取数据:

预览模式下的 GPT — OpenAI — 作者截图

这里还有另一个示例,这个示例针对特定情况。答案解释得非常清楚,引用了正确的法律条款(尽管它很长且复杂,且我的提示没有使用完全正确的技术术语):

使用知识库的 GPT — OpenAI — 作者截图

你可以继续测试你的 GPT。如果某些回答不符合你的期望,你可以返回“创建”标签并添加一些评论和反馈,以便进行改进。你还可以通过调整说明部分或更新知识库及其他参数来细化配置。

一旦你达到了正确的配置,你现在可以保存并发布你的 GPT,使用编辑器右上角的保存按钮,你将有一些选项。在我的情况下,我将选择公开:

保存选项 GPT — OpenAI — 作者截图

请注意,对于企业用户,还可以选择在公司级别发布 GPT。

一旦保存,我会被重定向到 ChatGPT 界面,并选择我的 GPT,

我的 ChatGPT 中的 GDPR Companion — 一些部分因隐私原因被隐藏 — 作者截图

我现在可以分享我的 GPT 链接,这是我为此次测试创建的 GPT 链接

OpenAI 提到,一些 GPT 将会提供货币化功能。目前尚不清楚具体如何实现。预计将在接下来的几周内公布。

限制

GPTs 也有一些限制。它仍然是一个新工具,应该会随着时间的推移而改进,但目前我观察到以下限制:

  • 幻觉:GPTs 基于 GPT-4\。它们继承了 GPT-4 的很多特性,包括幻觉。这一点在 GPTs 中没有改变。

  • 使用 PDF 时的准确性是知识库:关于使用“知识”功能的特点,允许上传各种文档,我最初尝试了一个 PDF,但结果显示出一些限制,可能与抓取问题有关。我使用计划文本格式重新尝试了一下,这似乎效果更好。

  • 对于长或复杂指令的准确性:当给出长指令时,GPT 可能只考虑了一部分,而不是其他部分,或者可能没有将所有指令应用于所有回答。通常,在我的测试中,它没有在所有回答中提到免责声明。

  • 响应时间: 响应时间比 GPT-4 长很多。对于一些提示,它花费了超过 1 分钟。这当然取决于许多因素,可能与最近的发布有关。但看起来,知识库越大,响应时间就越长。实际上,每次发送提示时,GPT 似乎都在访问知识库。

顺便提一下,即使这本身不是一个限制,这个新功能也会事实上使一些插件和第三方工具过时,通常,一些 管理 PDF 的插件(如 AskYourPDF),或者一些基于知识数据库生成 ChatBot 的应用程序。

结束语

目前,GPT 看起来是一个很酷的新功能,但它对于 ChatGPT 来说更像是“增量式”的,而不是真正的颠覆性。

如果你能够在 ChatGPT 或 Playground 中(使用 System 字段)良好地进行提示,也就是说,你提供了清晰的背景、指令和语气,你可能不需要这个新功能,除非你想将其与一些更高级的功能结合使用。

真正的新鲜事是分享你的 GPT 并进行货币化的能力。这些目前无法测试,我们应该期望在接下来的几周内可用。

如果你创建了一些 GPT,欢迎分享你的反馈以及你遇到的限制!

如果你创建了一些 GPT,欢迎分享你的想法和遇到的限制!

如果你想 了解人工智能,加入我的课程

使用 GPT 和 Whisper 创建个性化语音助手

原文:towardsdatascience.com/creating-your-personalized-voice-assistant-with-gpt-and-whisper-ddb9bd8c419

分步指南

Donato RiccioTowards Data Science Donato Riccio

·发表于 Towards Data Science ·6 分钟阅读·2023 年 5 月 18 日

--

照片由 Ivan Bandura 拍摄,来源于 Unsplash

本文旨在指导你创建一个简单而强大的语音助手,按照你的偏好定制。我们将使用两个强大的工具——Whisper 和 GPT,来实现这一目标。你可能已经知道 GPT 及其强大之处,但Whisper 是什么?

Whisper 是 OpenAI 提供的先进语音识别模型,能够提供准确的音频转文本服务。

我们将逐步指导你完成每个步骤,并包含编码指令。最终,你将拥有一个运行良好的语音助手。

在你开始之前

OpenAI API 密钥

如果你已经有了 OpenAI API 密钥,可以跳过这一部分。

Whisper 和 GPT APIs 都需要 OpenAI API 密钥才能访问。与 ChatGPT 的固定费用订阅不同,API 密钥的费用是根据你使用服务的多少来计算的。

价格合理。撰写本文时,Whisper 的价格为 $0.006 / 分钟,GPT(使用模型 gpt-3.5-turbo)的价格为 $0.002 / 1K tokens(一个 token 大约是 0.75 个单词)。

OpenAI 网站。图片来源于作者。

要获取你的密钥,首先在 OpenAI 网站上创建一个账户。登录后,点击右上角的你的名字,然后选择查看 API 密钥。点击创建新的密钥后,你的密钥将会显示出来。务必保存它,因为你之后将无法再次查看。

代码块显示了项目所需的库。该项目涉及使用 OpenAI 的 Python 库进行 AI 任务,pyttsx3用于生成语音,SoundDevice用于录音和播放音频,numpyscipy用于数学运算。像往常一样,在开始新项目时,你应该创建一个新的虚拟环境,然后安装包。

代码结构

我们的代码将围绕一个类进行结构化,总共约 90 行代码。假设你对 Python 类有基本了解。

由作者提供的图片。

listen方法捕获用户的语音输入,并使用 Whisper 将其转换为文本。think方法将文本发送到 GPT,生成自然语言响应。speak方法将响应文本转换为音频并播放。这个过程会重复:用户可以通过发出另一个请求来继续对话。

代码结构。由作者提供的图片。

init

此函数负责初始化历史记录和设置 API 密钥。

我们需要一个历史记录来跟踪之前的消息。它基本上是我们助手的短期记忆,允许它记住你在对话中早些时候说过的话。

listen

listen 函数。由作者提供的图片。

这个方法是我们助手的“耳朵”。

listen函数允许接收用户的输入。此函数从你的麦克风录制音频,并将其转录为文本。

它的功能如下:

  • 录制音频时打印Listening…

  • 使用 sounddevice 在 44100 Hz 的采样率下录制 3 秒(或任何你想要的时间)的音频。

  • 将录制的音频作为 NumPy 数组保存到临时 WAV 文件中。

  • 使用 OpenAI API 的transcribe方法将音频发送到 Whisper 进行转录。

  • 打印转录的文本到控制台,以确认转录是否成功。

  • 返回转录的文本作为字符串。

在示例中,助手监听了 3 秒,但你可以根据需要更改时间。

think

think 函数。由作者提供的图片。

我们助手的大脑由 GPT 提供支持。think 函数接收助手听到的内容并提出响应。怎么做到的?

响应不是在你的计算机上生成的。文本需要发送到 OpenAI 的服务器,通过 API 进行处理。然后将响应保存在响应变量中,用户消息和响应都添加到历史记录中,这是助手的短期记忆,为 GPT 模型生成响应提供上下文。

speak

speak 函数。由作者提供的图片。

speak函数负责将文本转换为语音并播放给用户。这个函数接受一个参数:text。它应该是一个表示要转换为语音的文本的字符串。

当函数被调用并传入一个文本字符串作为参数时,它会用命令engine = pyttsx3.init()初始化 pyttsx3 语音引擎。这个对象engine是将文本转换为语音的主要接口。

然后,函数指示语音引擎使用命令engine.say(text)将提供的文本转换为语音。这会将提供的文本排队等待朗读。命令engine.runAndWait告诉引擎处理排队的命令。

Pyttsx3 在本地处理所有文本到语音的转换,这在延迟方面可以是一个显著的优势。

最终修饰

助手现在已经准备好。我们只需创建一个助手对象,开始对话即可。

对话是一个无限循环,直到用户说出包含Goodbye的句子时才结束。

个性化体验的小贴士

自定义你的 GPT 助手非常简单!我们构建的代码非常模块化,可以通过添加各种功能来进行自定义。这里有一些想法可以帮助你入门:

  • 为助手赋予角色:更改初始提示,让你的助手扮演英语老师、励志演讲者或其他任何你能想到的角色!查看Awesome ChatGPT Prompts获取更多创意。

  • 更改语言: 想使用其他语言?没问题!只需将代码中的english更改为你想要的语言即可。

  • 构建应用程序: 你可以轻松地将助手集成到任何应用程序中。

  • 添加个性:通过添加自定义回应或使用不同的语调和语言风格,赋予你的助手独特的个性。

  • 与其他 API 集成: 将你的助手与其他 API 集成,以提供更高级的功能,如天气预报或新闻更新。

结论

在这篇文章中,我们解释了如何检索你的 OpenAI API 密钥,并提供了用于捕获用户输入、生成回应和将文本转换为语音以进行播放的 listen、think 和 speak 函数的代码示例。

有了这些知识,你可以开始创建适合你特定需求的独特语音助手。从创建一个帮助日常任务的个人助手,到构建一个语音控制的自动化系统,可能性是无限的。你可以在链接的GitHub repo中获取所有代码。

喜欢这篇文章吗?订阅我的新闻通讯,The Data Interview,每周将数据科学面试问题发送到你的邮箱。

此外,你还可以在 LinkedIn上找到我

posted @ 2024-10-12 19:55  绝不原创的飞龙  阅读(299)  评论(0)    收藏  举报