TowardsDataScience 2023 博客中文翻译(一百五十五)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

街道名称中的隐藏模式:数据科学故事 [第一部分]

原文:towardsdatascience.com/hidden-patterns-in-street-names-a-data-science-story-part-1-82c8dd130693?source=collection_archive---------4-----------------------#2023-01-29

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

·

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

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

照片由 Alexandr Bormotin 拍摄,来源于 Unsplash

你好!

最近我花了一些时间编制数据集,并阅读了关于我祖国阿尔巴尼亚的研究,试图从数据驱动的角度理解它。除了我自己的经验,我还希望有一种系统的方法来分析该国的社会、政治和生活背景。最近引起我好奇的一个方面是街道名称及其模式。在网上查找时,我找不到一个现有的数据集来查看该国的道路和街道是如何命名的,所以我决定创建并分析一个数据集。

对于这个故事,我将使用 Open Street Map 数据来分析阿尔巴尼亚首都地拉那的街道名称中的性别分布,以及其他历史或地理空间模式。为此,我还会使用pandasseaborn和一个名为contextily的地图绘制库来制作更美观的地图。第一部分将重点关注性别和街道名称,但我会在接下来的几周内继续研究第二部分,关注活动年份以及贡献区域。

那么,开始吧!

数据标注与可视化

这是 Open Street Map 提供的数据的一个快照,指定了一组坐标以覆盖整个地拉那区域:

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

图片由作者提供

数据包含 26950 行,大多数行有几何形状和其他关联的道路类型属性。有趣的是,同一条街道在数据中出现了多次,可能是因为同一条街道的不同部分在不同时间被映射。由于数据中没有分类名字性别的变量,并且没有简单的方法通过代码将阿尔巴尼亚语的名字与特定性别匹配,我利用自己对语言的知识手动标记了大约一半的数据框的行。该列包含以下标签之一:

  • W = 女性名字

  • M = 男性名字

  • O = 其他类型的名字(例如历史事件、抽象概念或著名家族的姓氏)

这里是每个性别的计数情况:

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

图片由作者提供

如你所见,女性街道名称在地拉那的街道名称中占约 3.3%,而男性名称则占约 71%。这种情况在阿尔巴尼亚并非独特:巴黎有 2%的街道以女性命名,而在罗马这个比例是 3.5%。在这些主要城市中,部分原因是制定这些决定的市政委员会历史上往往是男性和白人的主导。事实上,全球地方政府成员的平均女性比例仅为36%,许多国家甚至远未达到这一值,例如只有约 25 个国家在地方政府中有 40%的女性代表。尽管如此,最近的地拉那市议会立法机构的男女比例为 50–50%,这是朝着正确方向迈出的一步。

街道类型和长度

OpenStreetMap 数据还包括描述每条街道类型的标签,以下是按性别比较这些类型的可视化图:

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

作者提供的图片

有趣的是,存在如此多的“住宅”街道。根据 OpenStreetMap 的定义,住宅标签是“用于提供访问或在住宅区内的道路,但通常不作为通行路线使用”。“生活”街道也非常常见,定义为“具有较低的速度限制,相较于使用住宅标签的街道,有特殊的交通和停车规则”。这些是地拉那街道中主导的两种类型,即使它们不是主要街道,而是较窄且交通较少的街道。因此,研究这些街道如何命名可能会很有趣。

让我们看看按性别分组的所有街道总长度。为此,我将线串几何体投影到使用米的投影坐标系统中,并按性别平均了它们的长度:

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

作者提供的图片

有趣的是,“其他”街道的平均长度最长,而以女性命名的街道略短于以男性命名的街道。还发现“其他”街道多位于远离地拉那城市核心的高速公路或外围街道上:

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

作者提供的图片

这可以通过高速公路或其他城市间街道的名称来解释,如“Tirane-Durres”,它标示了它们连接的两个城市中心,或者仅仅是“SH1”这样的高速公路代码。鉴于此,我将这些街道标记为“其他”。

这里是以男性和女性命名的街道地图:

以下是自定义地图的示例代码

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

以男性命名的街道地图(图像由作者提供)

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

以女性命名的街道地图(图像由作者提供)

职业和工作

每位女性的贡献领域是什么?我将职业分为几个类别,结果如下(有些人物在网上没有可用的信息):

我的分类大致为:

  • 艺术、教师/作家/研究员、政治、人道主义和宗教与战争(为提供一些背景,许多这些女性在二战中与男性并肩作战,她们代表了“战争”类别中大多数女性):

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

图像由作者提供

邻里

除了贡献领域,街道名称在不同邻里中的分布是否存在模式?地拉那有 14 个行政区域,将城市划分为不同的区域。使用来自 OpenStreetMaps 的 GeoJSON 文件,该文件显示了这些区域的多边形,我们可以将这个数据集与街道名称的数据集进行空间连接。这是 14 个区域的地图:

创建下图的代码,带有用户定义的图例

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

我对每个行政区域中街道名称中女性的比例感兴趣:

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

有一些有趣的发现(至少对我来说 😃 )。女性名字的比例存在一些差异,如第 4 区的街道中几乎有 10%以女性命名,而第 8 区仅有 0.08%。另一方面,两个区域(12、14)没有以女性命名的街道。再次说,探讨这些命名选择背后的决策过程将是有趣的。

结论

总结来说,这个故事探讨了地拉那(阿尔巴尼亚)的街道命名情况,重点关注了性别构成以及城市不同区域和贡献领域的模式。稍后的第二部分将会讨论街道名称中代表的历史人物的其他方面,但现在这里有 Jupyter Notebook数据集** 的链接。**

感谢阅读!

街道名称中的隐藏模式 [第二部分]

原文:towardsdatascience.com/hidden-patterns-in-street-names-part-2-4ae9af5fdee3?source=collection_archive---------20-----------------------#2023-03-06

使用数据科学分析阿尔巴尼亚地拉那的街道名称

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

·

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

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

图片来源:Mario BeqollariUnsplash

你好!

这是我关于地拉那街道名称的故事的第二部分(你可以在这里找到第一部分:街道名称中的隐藏模式:数据科学故事 [第一部分])。在第一部分中,我们探讨了整体性别分布以及它们如何根据社区和道路类型发生变化。在这篇后续文章中,我将重点关注职业以及这些人物生活的历史时期。让我们开始吧!

数据与工具

与第一篇博客一样,为了将人物与他们的出生和死亡日期及其职业匹配,我手动标记了从 OpenStreetMaps 获得的数据集的一部分(许可:CC BY-SA 2.0)。具体来说,我使用了维基百科中列出的特定历史人物的主要职业,并依赖于那里列出的出生/死亡日期将这些细节添加到数据中。总共,我标记了643条独特的街道,并将其分为数据中的多个段落,其中相当一部分没有在线信息(更多内容见文末)。

在这第二部分中,我将使用 pandas、seaborn 和一些地理空间 Python 库来分析结果数据集并创建各种可视化图表。

职业和历史时期

在标记数据时,我根据每个人的维基百科页面将其分类为几个广泛的类别。结果发现街道名称中有 62 种独特的职业!以下是前 20 名及其各自计数的条形图:

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

前 20 名职业(图片来源:作者)

请注意政治家、战士和作家的突出地位:这三个类别共同占据了数据中所有标记街道名称的约 40%。艺术家紧随其后,完成了前 10 名的其余部分。我选择将党派人物(占标记总数的 8%)与其他战士单独标记,因为他们与阿尔巴尼亚共产党政权时期(1946–1991)有直接关联,以及他们在此期间所参与的宣传。因此,我认为这在我们作为一个国家如何面对历史的更广泛讨论中值得单独考虑。

让我们更详细地了解一下前三大类别:政治家、作家和战士。具体来说,他们生活和工作的历史时期是什么?以下是出生和死亡年份的分布情况,以及它们的平均值:

政治家分布的代码片段

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

图片来源:作者

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

图片来源:作者

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

图片来源:作者

  • 平均出生年份:政治家(1882 年)、作家(1858 年)、战士(1853 年)

  • 平均死亡年份:政治家(1936 年)、作家(1925 年)、战士(1900 年)

在上述图表和平均统计数据中可以注意到一些有趣的模式:首先,似乎大多数这些人物活跃在1850 年代1930 年代。作家的情况则有所不同,他们也出现在 17 世纪和 18 世纪。此外,“战士”出生和死亡年份有一个高峰,这与奥斯曼帝国入侵期间斯坎德培领导的反抗时期相对应。斯坎德培(1405–1468)被认为是我们的民族英雄,至今在许多纪念碑、广场和机构中得到广泛尊敬。另一方面,19 世纪到 20 世纪则对应于阿尔巴尼亚民族觉醒时期,这是一个旨在建立独立阿尔巴尼亚国的文化和政治运动。

以女性命名的街道

上一次,我们查看了以女性命名的街道大约占总街道名称的 3%,但让我们也在历史背景和职业方面来看看这些街道。通过性别筛选,并查看相同的出生/死亡分布:

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

图片由作者提供

  • 中位出生年: 1912

  • 中位死亡年: 1949

这里也有一些有趣的模式:首先,我选择了中位数而不是均值,因为均值会受到 1400 年代小高峰的显著影响。这导致了女性活动时代的时间移位,相较于整体分布。在第一部分中,我们看到这些女性中的许多人从事艺术或参与过各种战争,这可以解释中位死亡年份的情况。

绘制地图,邻里与道路类型

让我们从地理学的角度来看看这些内容。这是前三个类别的三张地图:

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

图片由作者提供

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

图片由作者提供

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

图片由作者提供

我会说,仅凭数据我无法立即找出任何明显的模式。为了开始识别这些模式,这里还展示了蒂拉纳 14 个行政区的最常见职业(与街道名称合并的行政区的多边形数据也来自 OpenStreetMaps):

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

图片由作者提供

不出所料,政治家和作家依然占据重要位置:我认为值得更多思考的是,某些地区以宗教或党派人物作为最常见的职业。这可能指向这些邻里的特定人物作为一种地方代表,或者是其他有趣的模式值得探讨。

互联网上没有信息的名字

正如我在开始时提到的,这些街道名称中很大一部分在网上没有 readily available 的信息。根据标记的数据,它们约占以人名命名街道的 55%。我发现这是一个有趣的现象,因为这可能意味着这些名称是本地重要人物但不被广泛认知,或者(显而易见)互联网信息有限,需要查阅其他来源。这里是这些名称所在位置的可视化(请注意这些名称主要出现在城市的边缘,而不是城市的中心):

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

结论 + 代码

总体来说,这个故事从更具历史性的角度审视了地拉那的街道风貌。我们深入探讨了不同工作区域和历史时期的表现,并揭示了这两个方面的模式。此外,还有一些其他的方向可以推进这个项目:例如,如何随时间推移城市名称的变化,或是命名过程中的具体细节如何导致我们看到的分布。目前,这里是Jupyter Notebook数据集。希望你喜欢!

如果你喜欢这些城市规划主题的帖子,你可能会喜欢我的新闻通讯,在其中我会更深入地讨论这些话题:The Zoned Out Chronicles!

分层 Transformer — 第一部分

原文:towardsdatascience.com/hierarchical-transformers-54f6d59fa8fc

更高效的语言模型

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

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

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

图片来源于 unsplash.com

在本文中,我们将探讨分层 Transformer:它们是什么、如何工作、与标准 Transformer 的不同之处以及它们的好处。让我们开始吧。

什么是分层 Transformer

“分层 Transformer”指的是在输入序列的多个尺度或分辨率上运行的 Transformer 架构。

我们为什么需要分层 Transformer?

标准 Transformer 尽管非常出色,但在时间上非常消耗。Transformer 内部的注意力机制在处理 n 个标记的输入序列时需要 O(n²) 的时间。这意味着 Transformer 对于长序列不够实用。解决这种低效的一个方法是使用分层 Transformer。它是唯一的解决方案吗?不!另一个方法是提高注意力机制的效率。但这是另一个话题。

分层结构如何帮助 Transformer?

分层 Transformer 使模型能够在不同级别的输入上进行操作,例如单词、句子、段落等。这与人类处理文本的方式相匹配。这迫使注意力机制跨越不同的层次,以建模不同粒度的实体之间的关系。

分层 Transformer 有很多方法;在本文中,我们力图直观地解释其中一种方法。

Hourglass Transformer

Hourglass [1] 网络是 OpenAI、Google Research 和华沙大学的共同研究成果。它是一种分层自回归 Transformer,接收一个输入序列,并形成从全分辨率到越来越小尺度的序列层次;在每个尺度上,它处理该分辨率内的序列,最后将序列扩展回全尺寸。这使得模型更加高效,因为较短的序列处理成本较低。

请注意,在自回归 Transformer 中,第一层和最后一层至少必须在输入的全尺度上操作。这是因为第一层处理输入,所以必须在全尺度上操作,最后一层(由于模型是自回归的)生成输出,因此必须再次在全尺度上操作。

让我们看看这个架构。下图展示了沙漏:

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

“沙漏”架构——图片来自[1]

我们一步一步描述。我们从上面图片的左侧开始,其中输入令牌被表示为灰色框。

  1. 模型首先使用标准的 Transformer 层处理完整的输入序列。因此,如果输入序列(显示为标题为“输入令牌”的灰色框)有 L 个令牌,则它们都通过标准 Transformer 层(用蓝色表示,称为预处理层)。这些层输出每个令牌的 L 个嵌入向量。

预先处理的层是对缩短前的完整令牌级序列进行操作的 Transformer 层。

因此,如果任务是“文本语言建模”,则输入到预处理层的是一个表示文本的子词令牌序列。如果任务是“图像生成”,则输入将是展平的像素值序列。

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

第 1 步——沙漏架构——图片来自[1],由作者修改

2. 在第二步中,模型将序列从L 个令牌缩短为更少的令牌。这一步显示为橙色梯形,并且“缩短因子”表示为sf = k₁。注意sf 代表“缩短因子”,如果设置为k₁,则意味着每k₁ 个令牌合并为 1 个令牌。缩短通过使用某种池化操作来实现,例如平均池化线性池化注意力池化。我们很快会讨论这些。此步骤的输出为L/k₁ 个令牌。此步骤也称为下下采样步骤

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

第 2 步——沙漏架构——图片来自[1],由作者修改

3. 缩短的序列经过更多的 Transformer 层,称为缩短层。在图中,它们由黄色框表示。这些层输出更新后的令牌嵌入。

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

第 3 步——沙漏架构——图片来自[1],由作者修改

4. 如果还有更多的缩短需要完成,我们简单地重复这个过程。在下图中,在第二个橙色梯形中,我们将输入序列缩短一个sf = k₂的因子。这将把每个k₂ 个令牌合并为 1 个令牌,因此将输出L/(k₁.k₂) 个令牌。输出的令牌经过更多的缩短层,这些层用淡黄色表示。

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

第 4 步——沙漏结构——图像来自[1],由作者修改

到现在,我们正处于架构的中间…

从这里,上采样开始了!

  1. 上采样层用于将最短的序列扩展回原始的全分辨率。由于我们进行了两次下采样(一次从 L tokens 到L/k₁,第二次从L/k₁ tokens 到L/(k₁.k₂) tokens),我们将执行两次上采样,将 token 数量恢复到L tokens。第一次上采样如下:

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

第 5 步(第一次上采样)——沙漏结构——图像来自[1],由作者修改

第二次上采样如下:

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

第 5 步(第二次上采样)——沙漏结构——图像来自[1],由作者修改

每次上采样操作后,我们将 token 嵌入通过 Transformer 层。在图像中,它们被称为 缩短层后置 vanilla 层

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

第 5 步——上采样涉及到 Transformer 层,无论是作为缩短层还是作为后置 vanilla 层——图像来自[1],由作者修改

最后的上采样通过后置 vanilla 层传递嵌入,从而输出下一个预测 token 的嵌入。

下采样步骤

下采样步骤(也称为论文中的缩短步骤)将输入序列缩短为更少的 token。此步骤通过使用各种池化操作将 tokens 合并为组来完成,例如:1)平均池化,2)线性池化和 3)注意力池化。

1) 平均池化: 从高层次来看,平均池化通过取平均值将 k 个相邻的 token 嵌入合并为单一的嵌入。这种方法有两个超参数:“池化大小”和“步幅”。

“池化大小”是窗口的大小,“步幅”是窗口每次移动的步数。例如,在一个序列“ABCDEF”中,池化大小 = 步幅 = 2,前两个 token 组成第一个窗口,窗口每次移动 2 个 token。所以窗口将是:[AB],[CD],[EF]。

论文将“池化大小”和“步幅”设为相同的数字,并称之为“缩短因子 (sf)”。我们通过一个例子来看看:

如果输入序列是 [x1, x2, x3, x4, x5, x6, x7, x8, x9, x10],且超参数为 pool size=stride=3,则平均池化将序列划分为大小为 3 的块,即 [x1, x2, x3],[x4, x5, x6],[x7, x8, x9],[x10],并对每个窗口中的 token 嵌入取平均值,以获得单一嵌入,如下:

e1 = mean(x1, x2, x3)

e2 = mean(x4, x5, x6)

e3 = mean(x7, x8, x9)

e4 = x10

因此,缩短后的序列将是 [e1, e2, e3, e4]。注意缩短后的序列长度是输入长度/sf = 10/3 = 3。

2) 线性池化:

这种方法设置步幅 = k,并将长度为 L 的输入序列划分为 L/k 个窗口。每个窗口包含 k 个 token,每个 token 具有一个维度为 d 的嵌入向量。然后,该方法将每个窗口展平为 k*d 维的向量,并形成以下矩阵:

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

线性池化 — 第一部分 — 图片由作者提供

假设输入序列是 [x1, x2, x3, x4, x5, x6, x7, x8, x9, x10],且步幅=3,则我们有以下窗口: [x1, x2, x3] , [x4, x5, x6], [x7, x8, x9], [x10],如果每个 token 具有一个 100 维的嵌入向量,则上述矩阵变为:

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

线性池化 — 第二部分 — 图片由作者提供

注意,现在我们已经将序列的长度从 10 缩短到 4,但现在每个新 token 的维度是 300 而不是 100!为了恢复到原始维度,线性池化将它们通过学习到的线性变换投影到 100 维空间中。线性变换是一个 300*100 的矩阵,由数据学习得出。

3) 注意力池化:

这种方法与上述两种方法类似开始:输入序列被划分为大小为 k 的窗口,然后在每个窗口内应用注意力,这使得窗口中的 token 可以相互关注。最后,由注意力产生的每个窗口的嵌入被加在一起。经过这一步后,在块嵌入上应用前馈层。

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

注意力池化 — 图片由作者提供

上采样步骤

上采样步骤将缩短的序列扩展回原始的完整长度。有两种简单的上采样方法:

  1. 重复扩展: 重复扩展 只是简单地多次复制每个嵌入。这在计算上非常高效。

  2. 线性扩展线性扩展将其投影到更高的维度,然后进行扩展。例如,如果缩短后的序列是 [e1, e2, e3, e4] 并且 sf=k=3,那么每个嵌入会线性投影到一个大小为 k * d 的向量,其中 d 是原始嵌入维度。投影权重矩阵是可学习的,并且与完整模型一起端到端地训练。

为了保持对原始输入序列的保真度,残差连接(如红色虚线所示)将缩短之前的输入序列添加到上采样序列中。可以将其视为通过多次缩短-扩展周期来获取声学上下文的一种方式。

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

为了保持保真度添加的残差连接 — 图片来自 [1],由作者修改

还有一种更高级的上采样方法,称为 注意力上采样,其工作原理如下:

如果缩短的序列是[ e1, e2, e3, e4 ],且sf=k=3,则首先应用线性或重复上采样将其扩展到原始长度。这将得到[ u1, u2, …, u12 ]。

设缩短前的嵌入为[ x1, x2, …, x12 ];这些通过残差连接(红色虚线)添加到上采样的嵌入中,形成[ u1+x1, …, u12+x12 ]。现在,自注意力机制应用于这个序列,其中:

  • 查询(Q)来自求和的嵌入[ u1+x1, …, u12+x12 ]。

  • 键(K)和值(V)来自上采样的嵌入[ u1, u2, …, u12 ]。

这更新了求和的嵌入,这将是最终输出。对上采样序列的注意力有助于放大相关部分,并与预缩短的上下文进行结合。

实验

他们[1]在语言建模(使用 enwik8)和图像生成(使用 ImageNet-32/64)上评估了他们的模型。他们显示,相比于 Transformer-XL 基线,Enwik8 数据集上的困惑度提高了 10-15%;并且他们在 ImageNet-32 图像生成任务上实现了自回归变换器模型的新最先进水平。

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

实验参数 — 作者提供的图片

这部分结束了沙漏网络的讨论。在下一篇文章中,我们将深入探讨其他层次化的变换器模型。

总结

在这篇文章中,我们回顾了一种改进效率并减少处理长序列时内存使用的变换器层次化架构。这种架构称为 Hourglass [1],由两个主要组件组成:1)缩短或下采样,2)上采样。缩短是通过使用池化操作(如平均池化或线性池化)将令牌合并成组来完成的。网络中间层的序列长度由缩短因子 k 减少。上采样组件使用线性上采样或注意力上采样等方法将缩短的序列扩展回原始长度。Hourglass 模型相比于基线变换器如 Transformer-XL [1],改善了困惑度。实际上,它在 ImageNet32 图像生成任务上达到了变换器模型的新最先进水平。

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

邮箱: mina.ghashami@gmail.com

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

参考文献

  1. 层次化变换器是更高效的语言模型

  2. 探索层次化注意力变换器在长文档分类中的高效性

分层变换器 — 第二部分

原文:towardsdatascience.com/hierarchical-transformers-part-2-2616eecacb21

分层注意力更快

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

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

这篇文章要求你具备标准变换器及其工作原理的知识。如果你是初学者,想了解变换器,请查看 变换器入门 文章。

分层变换器 — 第一部分 中,我们定义了“分层变换器”的含义,并回顾了该领域的一项重要工作,即 Hourglass

在这篇文章中,我们将继续探讨另一项著名的工作,即 分层注意力变换器(HAT)。

让我们开始吧。

分层注意力变换器(HAT)

该方法最初是为分类长文档而提出的,通常长达数千个单词。一个应用案例是分类法律文档或生物医学文档,这些文档通常非常长。

分词和分段

HAT 方法通过获取输入文档,并使用字节对编码(BPE)分词器将其拆分成子词/标记来工作。这个分词器被许多著名的大型语言模型使用,如 BERT、RoBERTA 和 GPT 家族。

然后将分词后的文档拆分为 N 个大小相等的块;即如果 S 代表输入文档,则 S = [C1, …., CN]N 个大小相等的块。(在整篇文章中,我们有时将块称为段,但它们是相同的概念。)每个块是一个由 k 个标记组成的序列 Ci = [Wi[cls], Wi1…, Wik-1],其中第一个标记 Wi[cls]CLS 标记,代表该块。

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

图片由作者提供

如上图所示,每个块是一个由 k 个标记组成的序列,其中第一个标记是 CLS 标记。

模型架构

在对输入序列进行标记化和分段后,将其输入到HAT变换器模型中。HAT 模型是一个编码器变换器,由两个主要组件组成:

  1. 分段编码器(SWE):这是一个共享的编码器块,接收一个段(也称为块)的序列并处理该块。

  2. 交段编码器(CSE):这是另一个编码器块,它处理所有段(也称为块)的 CLS 标记,处理交段关系。

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

图像来源于[1]

正如上图所示,左侧的分段编码器接收一个段的所有 k 个标记,处理它们并输出更新后的标记表示。在左侧,我们看到交段编码器接收所有段的 CLS 标记嵌入,并输出它们的更新表示。

这两个组件可以用于几种不同的布局。例如,我们可以将它们放在“临时”布局中,其中一个堆叠的 L 层 SWE 编码器放在底部,然后在其上方放置两层 CSE 编码器。请注意,每层的 SWE 共享权重。层与层之间的箭头表示在层之间传递嵌入。

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

图像来源于[1]

另一种布局是“交错”层,如下所示:

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

图像来源于[1]

如上所示,交错布局是一个配对的分段编码器和交段编码器的堆叠,其中跨越模型的几层执行交段注意力。

[1]中的作者探讨了几种布局(见下图),并通过实验发现“交错”布局优于其他变体。

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

图像来源于[1]

模型的完整架构如下:

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

图像来源于[1]

架构由 N 层组成,每层如上所示。请注意,将它们堆叠在一起会带来“交错”架构。一个层中的所有分段编码器共享权重,它们独立且并行地处理输入段(块)。每个段都有自己的位置嵌入。

分段编码器的输出将是分段标记的更新嵌入。第一个标记,即 CLS 标记嵌入,被添加到分段位置嵌入中,并传递给交段编码器

交段编码器捕捉段之间的关系,并更新每个段的 CLS 嵌入并输出。该层的输出将是所有标记的更新嵌入。

位置嵌入。请注意,架构中有两个位置嵌入:

  1. 段的标记位置嵌入:这是为了指示标记在段中的位置。这仅在段级编码器中使用。

  2. 段的标记位置嵌入:这是为了指示段的顺序,仅在跨段编码器中使用。

模型训练

作者[1]将模型训练分为两个阶段:预训练和微调。

预训练:由于网络是一个编码器变换器,它使用掩码语言建模(MLM)目标进行训练,其中一部分(15%)的标记被掩码,语言模型应该预测这些标记。

微调:他们在几个标注数据集上使用文档分类任务来微调模型。

不同级别的嵌入

使用这个网络,我们可以在不同的尺度上获得嵌入:词、段落、文档。

  • 词嵌入或标记嵌入可以通过模型的最后一层直接获取,

  • 段落或段的嵌入可以通过段的 CLS 标记嵌入获得。

  • 文档嵌入可以通过对所有段 CLS 标记嵌入进行最大池化(或平均池化)来获得。在论文[1]中,作者选择了最大池化。

评估

我真正喜欢这篇论文的地方在于,他们在三个层次上进行了全面的评估:

1)上游评估任务:这些任务旨在以通用方式预训练编码器。对于这个任务,他们采用了 MLM(掩码语言建模)任务。

  1. 中游评估任务:这些任务旨在评估预训练模型学习到的表示的质量。为了这个评估,作者[1]考虑了几个任务,例如:
  • 段顺序预测:这是为了预测几个段的顺序。模型在此任务中的输入是来自文档[1]的打乱的段序列,目标是预测它们之间的正确顺序。由于这是一个回归问题,他们使用平均绝对误差(MAE)作为损失函数。直观上,这个任务评估 CLS 标记嵌入的质量。
  1. 下游评估任务:他们在现实应用中评估模型的性能。为了这个评估,他们考虑了文档分类,用于对美国医院的出院总结进行分类。为了获得文档嵌入,他们对文档中所有段的 CLS 标记嵌入进行了最大池化,并使用交叉熵作为分类的损失函数。

更多细节请参考论文 [1]。

实验结果

论文中有许多实验结果,但一个显著的结果是,他们将自己的模型与 Longformer [2] 和 BigBird [3] 模型进行比较。Longformer 和 BigBird 属于稀疏注意力方法,它们通过强制每个标记仅关注其邻域中的少数标记和少量全局标记来实现高效的注意力机制。这与标准注意力方法相对,后者每个标记都关注每个其他标记。在这个实验中,他们展示了 HAT 方法在性能上超越了 Longformer 和 BigBird。

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

图片来源 [1]

摘要

在这篇文章中,我们探讨了另一种分层的变换器架构,称为分层注意力变换器(HAT)。这是一种基于编码器的模型,将输入分割成相等长度的片段。每个片段以一个表示该片段的 CLS 标记开始。该模型架构包括两个主要组件:片段级编码器和跨片段编码器。第一个编码器学习单个片段的表示,而第二个编码器学习片段之间的跨关系。它们一起能够学习输入中各种层次的表示,如词表示、句子表示和文档表示。

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

邮箱:mina.ghashami@gmail.com

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

参考文献

  1. 对分层注意力变换器在长文档分类中的有效性的探索

  2. Longformer: The Long-Document Transformer

  3. Big Bird: Transformers for Longer Sequences

更高精度的浓缩咖啡篮成像

原文:towardsdatascience.com/higher-precision-imaging-of-espresso-982270300b80?source=collection_archive---------12-----------------------#2023-02-28

咖啡数据科学

为了更好地测量每个孔的顶部和底部之间的差异

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

·

关注 发布于 Towards Data Science ·4 min read·2023 年 2 月 28 日

多年来,摄像技术得到了极大的进步,使人们能够拍摄高质量的图像。虽然这对于计算机视觉应用很有用,但实验设计的简单改动往往可以大幅提高这些图像在特定应用中的质量。进入咖啡!

几年来,我一直在将我的图像处理技能应用于意式咖啡滤篮。两年前,我尝试对滤篮的顶部和底部进行成像以测量每个孔的形状。然而,当我遇到自动对齐图像的问题时,这项调查暂停了。最近,我又开始重新进行这个工作,不过我使用了手动对齐来改进这个过程。

在收集一些数据时,我意识到我的成像设置也可以更好,所以我们在这里讨论一下。

挑战

成像意式咖啡滤篮面临几个挑战:

  1. 金属滤篮和反射率

  2. 孔很小

  3. 相机镜头有曲线

我通过数据收集标准操作程序和后处理调整了许多这些问题。

数据收集

我使用了一些标准化工具:

  1. 使用平板屏幕照亮滤篮孔

  2. 在一个黑暗的房间中隔离其他光源

  3. 减少曝光以处理平板屏幕上的反射,使其从滤篮反射回相机。

后处理

我有一个半自动化的过程来简化处理:

  1. 使用蓝色圆圈标记滤篮

  2. 手动阈值处理图像

  3. 自动去除非孔区域

  4. 将任何椭圆形孔调整为圆形。

  5. 调整滤篮上的光照

如何改进这一过程?

首先,用于测量滤篮顶部的光量应与测量底部的光量匹配。

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

所有图像由作者提供

所以我使用一个纸杯制作了一个领圈,将滤篮固定在上面,使得滤篮顶部(篮子内部)与翻转过来的滤篮底部处于同一高度。

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

然后我还调整了光照。全屏幕是校准图像所需的(每毫米的像素数量),但任何不在滤篮下方的光线都会反射到相机(我的手机相机)上,再反射到滤篮上。

为了消除这个问题,我使用了全屏亮度进行校准,并拍摄了另一张中间有白色圆圈的图像。这消除了反射问题。

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

但接下来我需要校准!为了确保图像对齐,因为手机可能会移动(即使在支架上),我在应用程序 Procreate 中手动对齐图像。我以为这会更困难,但使用图层和 50%透明度,这一过程非常简单。我还发现了一些有趣的事情,比如 VST 滤篮的对称性。

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

我使用校准图像来对齐顶部和底部图像,以便一切都校准到相同的尺度。这涉及到线性缩放和物体旋转,直到孔的对齐最好或最对称。我必须镜像底部图像,以确保孔与顶部图像正确对齐。

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

然后我将这些图片通过我的算法处理。下面是 Wafo Classic 的假色图像的顶部和底部图像,以描绘孔径大小。图像的顶部(左侧)有一块蓝色斑点,这不是由于其他问题。这是多次拍摄中的一个持久特征。

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

左:从过滤器顶部,右:从过滤器底部(镜像)

这些分布可用于更好地理解过滤篮。

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

从根本上讲,这是一个帮助回答关于过滤篮和性能的问题的工具。将这些信息与实际的浓缩咖啡表现连接起来还存在其他挑战,但努力追求对性能关键属性的更深入理解仍然是令人愉快的。

如果你喜欢,可以在TwitterYouTube以及Instagram关注我,在这里我会发布不同机器的浓缩咖啡镜头视频和与浓缩咖啡有关的内容。你也可以在LinkedIn上找到我。也可以在Medium上关注我和订阅

我的其他阅读内容

我的书

我的链接

浓缩咖啡文章合集

工作和学校故事合集

KDD 2023 大型语言模型亮点

原文:towardsdatascience.com/highlights-on-large-language-models-at-kdd-2023-fc53440563c3?source=collection_archive---------3-----------------------#2023-09-11

你无法参加 KDD 吗?阅读我的总结,了解会议上的热门话题:LLMs

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

·

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

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

作者提供的图片

几周前,我第一次有机会参加了 ACM SIGKDD(简称 KDD)会议。KDD 2023 在加利福尼亚州长滩举行,是数据挖掘领域最古老且最重要的学术会议,开创了与数据科学和大数据相关的主题。

会议持续了 5 天,吸引了超过 2200 人参加,其中有大量来自业界的参与者。我对所涵盖的主题多样性感到印象深刻,但从我的角度来看,热点话题是大语言模型(LLMs)和图学习。同时也发现了很多关于推荐系统(RecSys)的内容,我对此特别关注。

在这篇文章中,我总结了我在参加的研讨会、教程和论文报告中对 LLMs 的亮点,并附上了额外信息的在线资源链接。

警告:接下来是一篇长文,包含大量资源链接!

LLM 革命主题演讲(Ed Chi - Google)

著名科学家及谷歌主任 Ed H. Chi 在备受期待的LLM 革命主题演讲中发表了讲话。他回顾了我们经历的技术革命,从互联网到移动设备,再到深度学习的兴起,现如今是 LLMs,这无疑令人瞩目。

他讨论了人类智能与 ML 的不同之处——(1)从少量示例中学习,(2)解释他们的预测/决策,(3)强大的分布外泛化能力——以及 LLM 如何开始填补这一差距。

随后,他讨论了使 LLM 能够进行一些推理的技术:(1)链式思维提示,(2)自我一致性,(3)最少到最多提示,以及(4)指令微调。更多内容请参见 Denny Zhou 在 LLM 日的演讲(下一部分)。

最后,他分享了他对 LLM 未来挑战的愿景:(1)责任与安全,(2)事实性、基础及归属,(3)人类<->AI 内容循环与生态系统,以及(4)个性化和用户记忆。

LLM 日

KDD 专门设立了一天用于 LLM,有 5 位不同的研究人员就微软、Google DeepMind、Meta、智谱 AI 和 OpenAI 如何推动 LLM 技术的发展、面临的挑战以及他们对该领域未来演变的预见进行了详细讲解。演示幻灯片已经发布,强烈推荐查看。

从文档到对话:LLMs 如何塑造未来工作(Jaime Teevan — Microsoft) (幻灯片)

这次演讲涵盖了关于 LLM 质量问题的不同研究和应用主题(例如,如何处理低资源语言)、云端高效训练、检索增强生成(RAG)作为利用私人知识库(KB)的可持续方式、微调中的差分隐私、提示工程和聊天记录分析的最佳实践。

教会语言模型推理(Denny Zhou - Google DeepMind)(幻灯片)

他关注了 ML 的终极目标——推理——作为仅通过少量示例进行学习的方式。总结了一些使 LLM 如此强大的核心技术:

  • 思维链(CoT)——逐步思考的提示技术,提供一些少量示例以概述推理过程

  • 最小到最大提示(规划 + 推理)——将复杂问题分解为一系列子问题,依次解决

  • 自洽(SC)解码——一种从采样的多样化推理路径生成不同答案的技术。最终答案不是贪婪选择的答案,而是这些不同答案的多数票。这种技术似乎对 LLMs 非常有效,让我想起了模型集成的力量!

  • 指令调优——对预训练 LLMs 进行微调以遵循指令的过程。这使得零样本提示新任务成为可能。这对启用如 Google Bard 或 Open.ai ChatGPT 这样的问答系统至关重要。

Llama 2: 开放基础和微调聊天模型(Vedanuj Goswami - Meta FAIR)(幻灯片

他介绍了 Meta 在训练 Llama 基础模型和使用 SFT 数据(高质量的 27k 收集样本)进行指令微调的历程。他们的奖励模型是在 1M 收集样本上训练的。他还描述了他们的迭代微调与 RLHF,评估(人类,安全)。他在演讲结束时谈到了训练和部署 LLMs 面临的挑战,我在这里进行了记录:

  • 获取更多数据,多语言,多模态

  • 扩展到数千个 GPU,并提高 MFU(模型 FLOPs 利用率)

  • 设计高效的训练和推理架构,硬件-软件共同设计

  • 持续学习和更新知识

  • 改善事实准确性和引用来源

  • 减少幻觉和承认不确定性

  • 删除有害、攻击性或偏见内容

  • 适应超越训练数据的世界知识

从 GLM-130B 到 ChatGLM(Peng Zhang - Zhipu AI)(幻灯片

我了解了 Zhipu AI,这是一家在中文领域挑战 Open.ai 的公司。他们作为钻石赞助商在 KDD 上有强大存在,并在宴会庆典上发表了主题演讲。Zhipu 展示了他们在许多任务中表现最佳的中文 LLM 结果,甚至优于 GPT-4。他们描述了如何在其基础模型(GLM-130B)之上开发 ChatGLM 和 VisualGLRM。他们在HuggingFace 上开源了 ChatGLM-6B

大语言模型复兴:范式与挑战(Jason Wei — OpenAI)(幻灯片

关于规模规律的非常务实的演讲,讲述了如何达到当前 LLMs 的状态,以及当 LLMs 参数超过 100B 时可以观察到的新兴能力(包括推理)。还谈到了通过提示技术进行推理:Chain-of-Thought 和 Least-to-most 提示。

大规模 AI 模型的基础与应用预训练、微调和基于提示的学习研讨会

我认为LLM-AI 研讨会是会议上最具争议的一个。我早上实在无法参加,因为在 KDD 早晨的主旨发言后,小房间被人群完全挤满了。幸运的是,在咖啡休息之后,我找到了一个座位,能够参加几场会议。

LLMs 时代的 NLP 研究(Shafiq Joty - Salesforce)

他描述了 SalesForce 的 XGen LLM — 一个内部的 JaxFormer 库,继承了 LLaMA-7B,并通过 WizardLM 进行了指令微调,能够基于非结构化和结构化数据(例如 Spark 和 SQL 数据库)回答问题。还介绍了一些他们用于推理准备的技术,包括通过Chain-of-Thought来分解问题,并通过在自然句子、SPARQL 和 SQL 上训练模型进行自适应查询生成(LoRA)来选择最相关的知识库。该过程为每个推理步骤生成一个查询,并在知识源上执行。

模块化大型语言模型和最小化人工监督的原则驱动对齐(YiKang Shen — IBM)

这个演讲介绍了 IBM 的基础模型:(1)Sandstone — 适合针对特定任务微调的编码器-解码器架构,(2)Granite— 仅解码器,类似于 GPT 的生成任务,(3)Obsidian — 一种新的模块化架构,提供高效的推理能力和在各种任务中的表现水平

他还描述了他们在 LLM 方面面临的一些挑战:

  • 效率 — 如何训练和服务 Llama 65B 模型。

  • 可扩展性 — 如何用不断增长的训练语料库、不同语言和客户的私人数据来更新 LLM

  • 灵活性 — 能够在不同设备上使用不同复杂度的 LLM 模型,满足不同的延迟要求。

他们展示了他们的ModuleFormer,它通过稀疏专家混合(SMoE)来解决上述问题。它可以为每个输入激活其模块的子集,比密集型 LLMs 对灾难性遗忘的免疫力更强。对 ModuleFormer 进行微调可以使模块子集专门化,而与任务无关的模块可以被修剪,以实现轻量级部署。

教程

这些教程是同时进行的,所以我不得不分配时间去参加两个讲座。幸运的是,他们的优秀幻灯片被提供了,并且非常详细。

面向下一代智能助理,利用 LLM 技术 — Meta (幻灯片)

关于智能助理的非常全面的教程,这些智能助理是多模态的,并且能够利用用户的位置、用户可以听到和看到的内容(例如,使用 Google Glasses、Meta Quest 2)作为上下文。教程描述了不同模块之间的连接方式:ASR、CV、NLU、对话状态跟踪器、NLG、TTS、KB、个性化/推荐和隐私保护等。

预训练语言表示用于文本理解:一个弱监督视角 - 伊利诺伊大学香槟分校 (幻灯片)

介绍了语言模型预训练的进展,将其与传统的 NLU 任务进行了比较,并描述了 LLM 如何用于提取实体和层次关系、主题发现和文档理解。我从这个教程中获得的一个良好见解是使用一些 NLU 技术来评估生成的答案是否回答了问题。

研究论文

这是我喜欢的一些 NLP / LLM 论文的简短列表。

端到端查询术语加权(Google)(论文

这篇出色的论文结合了词汇和语义检索系统。他们在词汇检索器的基础上提出了一种术语加权 BERT(TW-BERT)模型来构建他们的解决方案。TW-BERT 学会预测单个 n-gram(例如,单字和双字)查询输入术语的权重。这些推断出的权重和术语可以直接被检索系统用来执行查询搜索。学习到的权重可以被标准词汇检索器(例如 BM25)以及其他检索技术(如查询扩展)轻松利用。

UnifieR:大型检索的统一检索器(Microsoft)(论文

另一个有趣的提议是将稠密向量和基于词汇的检索统一到一个具有双重表示能力的模型中。它通过两阶段自学习流程进行训练,并在最新的词汇和稠密检索模型上有所改进。

学会与对话搜索中的先前回合相关(论文

通常在多轮对话中,历史查询用于扩展当前查询。然而,并非所有的先前查询都与下一个问题相关或有用。该论文提出了一种方法,用于选择对当前查询有用的相关历史查询。他们使用伪标注机制来标注相关的历史查询,并与检索器训练一起训练选择模型。

GLM-Dialog:面向知识驱动对话生成的噪声容忍预训练(论文

描述了如何通过基于 RAG 的对话系统使用嘈杂的私人知识库。他们提出了一种新颖的评估方法,使人们可以同时与多个部署的机器人对话,并隐性地比较其表现,而不是使用多维度指标进行显式评分。

集群语言模型以改善电子商务检索和排名:利用查询相似性和微调以实现个性化结果(Home Depot)(论文

论文描述了 Home Depot 如何通过使用特定集群的语言模型来改善电子商务的语义搜索,而不是使用典型的双编码器架构。他们的方法首先使用 K-Means 将用户查询映射到集群,并使用所选的集群特定语言模型进行检索。

结论

这些是我在 KDD 2023 上对 LLM 的重点总结。希望你能从这个总结和我编纂的资源中找到一些有用的信息和灵感。

“抱歉,帖子写得太长。如果我有更多时间,我会写得更短一点” 😃

爬山算法优化:简单的初学者指南

原文:towardsdatascience.com/hill-climbing-optimization-algorithm-simply-explained-dbf1e1e3cf6c

最流行的优化算法之一的直觉

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

·发布于Towards Data Science ·阅读时间 5 分钟·2023 年 3 月 14 日

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

照片由Isaac Burke拍摄,发布在Unsplash

背景

许多工业和研究问题需要某种形式的优化来获得最佳解决方案或结果。其中一些问题属于组合优化类别,这意味着它们通常不能在合理的时间内通过暴力破解解决。因此,我们转向启发式元启发式算法,这些算法虽然不能保证找到最佳的全局解决方案,但通常能在合理的时间内计算出足够的解决方案。

其中一种元启发式算法是爬山算法***,***这是本文的主题。我们将深入探讨理论、优缺点,并通过实现该算法来解决著名的旅行商问题 (TSP)

爬山算法

概述

攀登算法是一种元启发式的 迭代 局部搜索 算法。它通过对当前解决方案进行小的 扰动 并继续这一过程,直到找不到更好的解决方案,旨在找到最佳解决方案。此外,它是一种 贪心算法,因为它只关注局部最优的移动,因此常常会陷入 局部最优(见下图)。

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

一个包含局部和全局最优的函数示例。图由作者提供。

算法

攀登算法的一般流程如下:

  • 生成初始解决方案,该解决方案现在是最佳解决方案。

  • 从最佳解决方案中选择一个邻域解决方案。

  • 如果邻域解决方案优于最佳解决方案,则将最佳解决方案设置为等于邻域解决方案。

  • 重复上述两个步骤,直到邻域解决方案不优于最佳解决方案,或满足其他终止条件,如迭代次数。

如果这仍然显得有些模糊,不用担心!稍后我们将在 Python 中应用上述算法到一个实际的例子中。

类型

爬山算法有多种类型和变体。以下是最常见的几种:

  • 简单爬山算法: 仅考虑最近的邻域。

  • 最陡爬升爬山算法: 考虑所有邻域并选择最佳方案。

  • 随机爬山算法: 随机选择一个邻域。

最陡爬升版本会带来更优的性能,但需要更多的计算资源。

优缺点

让我们简要列出爬山算法的主要优缺点:

优点:

  • 非常直观,易于向同事、利益相关者等解释。

  • 可以应用于 连续 离散 目标函数和问题。

  • 能够解决各种不同的问题。

缺点:

  • 可能会陷入 局部极小值/极大值 ,因此不能保证找到最佳的全局解决方案。

  • 可能出现平坦区域,其中所有邻域具有相同的目标分数。

还有更复杂的算法,如 模拟退火禁忌搜索 类似于爬山算法,但不会陷入局部极小值,并且能更广泛地探索搜索空间。要了解更多关于这些算法的信息,请查看我之前的帖子:

如何用模拟退火算法解决旅行推销员问题 ## 如何用模拟退火解决旅行推销员问题

使用模拟退火优化算法求解旅行推销员问题的最佳解决方案

[towardsdatascience.com ## Tabu Search 简单解释

对 Tabu Search 优化算法的直观解释以及如何将其应用于旅行推销员问题……

[towardsdatascience.com

Python 实现示例

旅行推销员问题

我们将编写爬山算法代码来解决旅行推销员问题(TSP)。不过,在此之前,让我们简要说明和解释一下我们在 TSP 中试图解决的问题。

TSP 是优化中的经典问题,并提出了以下问题:

“访问给定城市列表一次并返回到起点的最短路径是什么?”

这个问题看起来很简单,但由于组合爆炸的问题,随着城市数量的增加,暴力解决方法变得计算上不可行。例如,对于 10 个城市,存在~300,000 个可能的 路径

可行路径的数量作为城市数量n的函数是**(n-1)!/2.** 因此,它的计算复杂度是 O(n!)。

当城市数量约为 20 时,暴力解决方法变得不可行,因为计算所需的时间为~2,000!令人惊讶的是,对于 61 个城市,这一时间竟达到惊人的 10⁶⁷年!

爬山算法解决 TSP

让我们简要列出我们将用来实现爬山算法解决 TSP 的伪代码。我们将使用最陡上升版本:

  • 生成一个初始路线并将其设为最佳解决方案。

  • 通过交换当前最佳解决方案中的两个城市来生成邻域解列表。

  • 从这些邻域解中获取最佳邻域解(最短距离),并将其设为当前解决方案。

  • 将当前解决方案与最佳解决方案进行比较。如果当前解决方案更短,则将最佳解决方案设为当前解决方案。

  • 重复此过程,直到当前解决方案比最佳解决方案更差。

Python 代码

以下是我们刚刚讨论的算法的HillClimb类的示例代码:

现在让我们运行类并绘制 20 个合成生成城市的初始解决方案和最佳解决方案的结果:

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

图表由作者在 Python 中生成。

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

图表由作者在 Python 中生成。

正如我们所见,爬山算法找到的解决方案优于初始解决方案,但显然不是全局最优解。不过,它找到了一个足够的解决方案,而且没有花费 2000 年!

总结与进一步思考

在这篇文章中,我们讨论了元启发式局部搜索爬山算法。该算法对最佳解决方案进行小的增量扰动,直到达到一个变化不再带来更好解决方案的点。该算法在旅行推销员问题上表现良好,但却陷入了局部最小值,这是这种优化算法的主要缺点。

本文使用的完整代码可以在我的 GitHub 上找到:

[## Medium-Articles/Optimisation/hill-climbing at main · egorhowell/Medium-Articles

当前无法执行该操作。您已在另一个标签或窗口中登录。在另一个标签中注销了…

github.com

参考资料与进一步阅读

另一件事!

我有一个免费的通讯,Dishing the Data,我每周分享成为更好的数据科学家的小贴士。没有“虚华”或“点击诱饵”,只有来自实践数据科学家的纯粹可操作的见解。

[## Dishing The Data | Egor Howell | Substack

如何成为更好的数据科学家。点击阅读由 Egor Howell 编写的《Dishing The Data》,这是一个 Substack 出版物,包含…

newsletter.egorhowell.com

与我联系!

招聘杰出的数据科学家

原文:towardsdatascience.com/hiring-exceptional-data-scientists-c454110742f?source=collection_archive---------8-----------------------#2023-02-28

异常值:超越单纯的技术技能

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

·

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

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

布劳贝格斯特兰德(作者提供的图片)

随着对应用数据科学最初兴奋感的减退,回顾一些在热潮高峰期取得成功的举措的因素是很有必要的。值得考虑的是,在这段时期设定的那些大多不切实际的期望中,哪些因素对于超越这些期望至关重要。然而,当前的问题可能更适合用“谁?”而不是“什么?”来框定。在这篇文章中,我分享了我的观察和思考,谈论我认为使一名杰出的数据科学家脱颖而出的因素——那些通过全面的技能和无尽的热情不断带来惊艳表现和业务影响的人。

积极的自我学习者

一位出色的数据科学家会表现出强烈的好奇心和持续学习的动力。在我的经验中,大多数数据科学家面临需要新颖解决方案的问题,这些问题并非普通的监督学习或无监督学习方法可以解决。对于这些问题,解决方案需要在不总是直接相关的主题上进行积极的研究,探索在线资源,跟进开源社区的最新动态,阅读书籍,与同行交流,或参加数据科学聚会以外化思想。这类个人也可能通过开发工具或算法为该领域做出贡献(尽管不一定注册了研究生学位)。他们还会与学术界互动,以保持对研究的最新了解,并支持自己的学习,这在我看来是非常宝贵的资源。

自学的数据科学家能够批判性地评估新信息,并将其与他们正在进行的项目或同行的项目联系起来。此外,他们展现出开放的心态,保持对新思想和观点的接受态度,并愿意评估和挑战自己的假设,以便不断学习和扩展技能。这些人表现出高度的自我效能感,使他们对学习充满动力,并能坚持不懈。我曾与一些出色的数据科学家交谈过,他们还实践元认知。他们会反思自己的认知过程和学习方法,这对于调节和引导自己的思维和知识朝向特定目标非常有益。在我看来,这有助于提高他们的学习效率和解决问题的能力。

我如何识别这样的人呢?我会查看他们的公开代码库,看看他们是否实现了难度较大的理论概念或从科学论文中得出的算法。他们是如何处理手头的问题的?他们参考了哪些文献和资源?他们最终是如何开发出可行的解决方案的?这些见解可以提供有用的信息,帮助理解他们的思维过程和解决问题的方法。我还会关注那些采用“通过教学学习”方法的数据科学家,他们通过教导他人来巩固自己对相关主题的理解和知识。这种候选人对团队非常有益,因为他们愿意分享他们的学习成果!作为面试过程的一部分,我会问候选人他们希望学习(或更好地理解)的方法或算法,以及原因。他们的愿望清单上有什么?

高效的沟通者

杰出的数据科学家拥有能够清晰阐述商业和技术信息的词汇和沟通风格,同时考虑到听众的需求和视角*。* 他们可能会使用主动倾听技巧,例如复述或总结演讲者所说的内容,并提出后续问题。他们能够设身处地为听众着想,展现出同理心。这在数据科学家需要向非技术的业务利益相关者解释复杂概念时尤为必要。他们也对自己的能力充满信心,但能够自信地表达他们的不确定性。他们能够在演讲中保持听众的兴趣,使用讲故事的方式使信息令人难忘且具有影响力。他们是与产品负责人、业务利益相关者和同事的优秀合作者,并且意识到团队动态,这对于识别潜在障碍和成功机会至关重要。他们对他人充满信任,并善于建立稳固的工作关系,保持团队中的高度互依,并以积极和建设性的方式解决冲突。这也增强了团队的凝聚力,促进了成功的成果。

创造性问题解决者

杰出的数据科学家具备高度的流动智力,使他们能够解决新颖的问题、进行抽象思维并适应新情况。这有助于他们的创造力和对新概念的掌握,往往导致产生新的商业理念(如果你愿意,可以称之为创业思维),这些理念可以转化为新的实用产品或解决方案。一些人可能表现出发散性思维,能够在面对挑战或问题时产生多种想法,这涉及超越传统方法、建立新颖的联系和考虑多种视角。他们富有想象力、好奇心强,并且乐于接受新体验。

要识别这样的人,我会关注他们在提出不同问题解决方法时的认知灵活性。这里的重点并不一定是候选人提出正确的解决方案,而是评估他们通过连接看似无关的概念来生成有意义的联想的原创思维能力,以解决问题。

域专业人士

除了技术技能外,有些人可能在特定领域或行业中拥有相当的知识和专业技能,这对于您的招聘目标至关重要。杰出的数据科学家可能对业务(例如,商业战略、目标等)有较好的理解,这帮助他们识别并自主解决与业务相关的核心问题。其他领域(如工程、制造、金融等)也是如此。这些数据科学家在利用其领域知识理解他们所处理的数据的细微差别方面具有优势,并能够开发相关且可操作的模型或洞察。他们还会知道哪些模型假设最能代表领域的基本动态,这可以在面试中评估。招聘领域专业人士可以加速特定领域的数据科学计划。

技术熟练度

以下是一些杰出数据科学家非常重视的关键技术领域。

合理的模型假设: 能够数学地识别和解释基本逻辑、原则和假设,使用连贯的论据和证据来支持他们的观点。杰出的数据科学家能够解释他们的工具,并识别利用这些工具的理想场景。

系统导向推理: 他们会通过考虑解决方案嵌入的整个端到端系统来处理问题。他们仔细分析系统中各种数据和模型组件之间的相互关系,考虑这些组件如何被消费或互动。他们还会考虑系统中某部分的变化对整个系统的影响,并对导致系统行为的基本结构和反馈回路感兴趣。我还观察到,杰出的数据科学家会与系统的最终用户密切合作,以利用用户体验反馈。

指标驱动: 他们非常强调使用数据和定量措施(适当的统计数据)来推动决策和评估绩效。他们专注于跟踪和分析 KPI,并利用这些信息做出明智的决策,以改善模型或系统。例如,他们将准确知道在特定模型中使用哪些指标,如何解释它们,以及需要注意哪些认知偏差和统计偏差(如 Goodhart 定律、Simpson 悖论、Berkson 悖论等)。

数据导向: 他们非常注重数据的收集、分析和解释,以指导决策并获得对各种现象的洞察。个人在处理数据、数据分析和数据可视化方面非常熟练。他们在数据探索过程中注重细节,并会寻找异常发现的解释——绝不会将隐藏的知识掩盖在地毯下。

开源项目: 他们可能有自己的公开开源项目或代码库,在这些项目中,他们和其他贡献者经常进行贡献。这些个人积极地为一个共同目标贡献代码,他们的贡献被社区重视并接受。

面试可以通过不同的方式进行。就个人而言,我不喜欢给数据科学候选人分配时间限制的编码任务。这样做会削弱这篇文章中几乎所有的内容。相反,我会审查他们的公开代码库,以评估他们的编码风格、贡献和解决问题的方法。通过检查他们代码和分析中的评论,可以判断他们的思维过程和观察结果。在面试过程中与他们的公开代码相关的跟进问题可以帮助澄清差距。或者,让数据科学候选人谈谈他们做的一个有趣项目(保护机密信息),或提供给他们一个预定的案例研究以概念性地解决(而非代码)应该能为招聘团队提供充足的机会来识别这篇文章中提到的大部分特征。

附注:请确保你的职位要求是现实的,并能代表实际角色;否则,出色的候选人会对你的公司失去兴趣。了解你目前团队中哪些实践是成功的,你的公司/团队文化如何,你的团队需要什么额外技能,以及你将如何留住并培养那些出色的数据科学家!

以幽默的结尾——一个出色的数据科学家就像一个卡尔曼滤波器 😉

  • 适应性: 他们应该能够随着新信息或数据的出现,调整他们的模型和分析方法。

  • 稳健: 他们应该能够应对噪声和不确定的数据(或人)。他们应该能够利用不确定性,过滤掉噪声和其他无关信息,专注于重要内容。

  • 预测: 他们应该能够基于过去的数据(以及当前的观察)做出准确的预测,识别数据中的趋势和模式,并利用这些信息做出明智的预测/推荐。

  • 平衡准确性和复杂性: 他们应该找到最适合给定数据集的最佳模型,并从中提取最有用的信息,同时通过使用简单(通常是线性的)假设来保持计算时间合理。

直方图均衡化:逐步指南 (CV- 06)

原文:towardsdatascience.com/histogram-equalization-a-step-by-step-guideline-06-527dcb1a7504

图像直方图均衡化详细说明

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

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

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

原图由 Dan Fador 提供,Pixabay (左上图是主图,左下图是图像的灰度版本。右侧的图像是直方图均衡化的结果)

动机

直方图是通过条形图可视化频率分布的过程。在计算机视觉中,图像直方图是表示强度值频率的过程。通过图像直方图均衡化,我们可以轻松调整图像强度值的频率分布。通常,这个过程有助于提高图像的对比度和亮度。该过程简单易行。本文将讨论直方图均衡化的完整过程以及代码示例。

目录

  1. **图像直方图**

  2. **直方图均衡化的完整过程**

  3. **逐步实施**

图像直方图

图像直方图 是用条形图表示图像强度值的频率。在图-1 中,我展示了一幅样本图像及其在二维空间中的强度值。

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

图-1: 样本图像强度值(作者提供的图像)

值的范围从 0 到 7。让我们计算这些值的频率。

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

图-2: 图像强度值的频率(作者提供的图像)

图像直方图 是对频率强度值的简单表示,如图-3所示。

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

图-3: 图像的直方图(作者提供的图片)

直方图均衡化的完整过程

直方图均衡化是通过一些函数均匀分布图像强度值的频率的过程。主要的函数是概率函数 — PDF (概率密度函数) 和 CDF (累积分布函数)

  • PDF 是通过将强度值的频率除以总频率来计算的。

  • CDF 表示小于或等于特定值的概率分布的概率。例如,强度值的 PDF 为*0 → 0.12, 1 → 0.24, 2 → 0.12, 等等。所以,1 的 CDF 为 0.12+0.24 = 0.36,2 为 0.36+0.12=0.48,以此类推。*完整结果编译在图-4中。

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

图-4: 直方图均衡化计算(作者提供的图片)

最后,我们需要将CDF与一个整数相乘。

选择数字的过程 —

图像的最大强度值为 7,如图 — 1所示。我们必须用以下公式选择一个整数值。

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

这里,x 是表示最大强度值的最小位数。对于我们的情况,最大值为 7,*(2³ -1 = 7)*。如果我们选择 2 作为 x 的值,就无法表示最大强度值。因此,在我们的案例中,最佳乘法值是 7。

最终,我们将对乘法结果进行四舍五入以获得均衡直方图值。将强度值替换为均衡直方图值以找到最终输出。整个过程见图-4

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

图-5: 初始强度值和均衡直方图强度值(作者提供的图片)

为了应用直方图均衡化,我们需要用均衡直方图值替换初始强度值。

新的图像如下 —

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

图-6: 带有均衡直方图强度值的图像(作者提供的图片)

均衡直方图强度值的频率与之前的频率不同,如图-6所示。

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

图-6: 新的均衡频率(作者提供的图片)

图像强度频率的比较见图-7

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

图-7: 比较(作者提供的图片)

如果我们观察均衡直方图频率与初始频率的比较,会发现均衡直方图的强度值比初始值高,如图-7所示。

现在是时候用 Python 进行实际操作了……

分步实现

为了实现直方图均衡化,我们使用OpenCV库。

  • 让我们导入必要的库,并以灰度模式读取图像 —

  • 计算图像的频率并绘制直方图——

由于大多数图像部分较暗,低强度值(~0)的频率大于高强度值。

  • OpenCV 函数 ***cv2.equalizeHist()*** 有助于实现直方图均衡化——

如果我们仔细观察输出图像,可能会注意到图像的亮度高于原始图像。

  • 让我们设置均衡化直方图图像——

强度值的频率比原始图像更加均匀分布。

处理彩色图像

RGB/BGR 图像有 3 个通道—— 红色、绿色和蓝色。 要在图像中应用直方图均衡化,我们需要将 RGB/BGR 图像转换为 **HSV** *(色调、饱和度和值)* 图像。最后,我们对 HSV 图像的值参数应用直方图均衡化。

  • 应用直方图均衡化,并使用 **matplotlib** 将其转换为 RGB 图像进行可视化。

显然,图像的亮度和对比度显著提高了。

如果你愿意,可以绘制之前展示的直方图和均衡化直方图。

结论

我们生活在生成式 AI 的世界里。这些基本的计算机视觉技术看起来非常基础和过时,但它们是创建强大模型或创新的基础。所以,我总是强调知识的基础。

我已经开始撰写一系列计算机视觉文章。之前的文章如下嵌入——

## NumPy 和 OpenCV 在计算机视觉中的入门 (CV-01)

用 Python 开始你的计算机视觉编码

towardsdatascience.com ## 计算机视觉中色彩表示的综合指南 (CV-02)

色彩空间和颜色模型的详细解释

towardsdatascience.com ## 图像融合的最简单指南 (CV-03)

最简单的图像融合和粘贴指南

towardsdatascience.com [## 阈值化——使图像更可见的一种方法 (CV-04)

使用阈值化从图像中提取更多信息

阈值处理:使图像更清晰可见 形态学操作:去除图像失真 [## 计算机视觉中的形态学操作及其模拟 (CV-05)

图像处理中的形态学操作最简单解释

形态学操作:去除图像失真

达到时间预测:时间序列概率预测的另一种方式

原文:towardsdatascience.com/hitting-time-forecasting-the-other-way-for-time-series-probabilistic-forecasting-6c3b6496c353

需要多长时间才能达到一个特定值?

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

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

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

Mick Haupt拍摄,照片来源于Unsplash

准确预测的能力对每个时间序列预测应用都是基础。为此,数据科学家通常选择那些从点预测角度最小化误差的最佳模型。这虽然正确,但可能并不总是最有效的方法。

数据科学家还应考虑开发概率预测模型。这些模型不仅会生成点估计,还会提供上限和下限的可靠性区间,未来的观测值很可能会落在这些区间内。尽管概率预测似乎是统计学或深度学习解决方案的特权,任何模型都可以用来生成概率预测。这一概念在我之前的文章中解释了,我介绍了将符合性预测作为使用任何 scikit-learn 模型来估计预测区间的方法

点预测无疑更容易向非技术利益相关者传达。同时,能够生成关于我们预测可靠性的关键绩效指标(KPI)也是一种附加值。概率输出可能提供更多的信息来支持决策。告知未来几小时有 60%的降雨概率可能比报告降雨毫米数更具信息性。

在这篇文章中,我们提出了一种称为预测到达时间的技术,用于估计特定事件或条件发生的时间。它被证明是准确的,因为它基于合规预测,可解释的,因为它具有概率解释性,并且可重复的,与任何预测技术都适用。

介绍到达时间预测

预测到达时间是一个在各种领域常用的概念。它指的是预测或估计特定事件或条件发生所需的时间,通常是在达到特定阈值或水平的背景下。

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

模拟的季节性和趋势 [图片由作者提供]

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

模拟时间序列(季节性 + 趋势)示例 [图片由作者提供]

到达时间的最著名应用涉及可靠性分析和生存分析等领域。它包括估计系统或过程经历特定事件的时间,例如故障或达到特定状态。在金融领域,到达时间通常用于确定信号/指数朝向预期方向的概率。

总体而言,预测到达时间涉及对某一事件发生所需时间的预测,该事件遵循时间动态。

从点预测到概率预测

要正确估计到达时间,我们必须从点预测开始。作为第一步,我们选择所需的预测算法。对于本文,我们采用了来自tspiral的简单递归估计器,这种估计器在 scikit-learn 风格中易于获取。

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

预测值与实际数据点在测试集上的对比 [图片由作者提供]

model = ForecastingCascade(
    Ridge(),
    lags=range(1,24*7+1),
    use_exog=False,
)

我们的目标是为每个预测点生成预测分布,从中提取概率洞察。这通过三步法完成,并利用合规预测的理论:

  • 预测结果通过交叉验证在训练集上收集,然后进行平均。
CV = TemporalSplit(n_splits=10, test_size=y_test.shape[0])

pred_val_matrix = np.full(
    shape=(X_train.shape[0], CV.get_n_splits(X_train)),
    fill_value=np.nan,
    dtype=float,
)

for i, (id_train, id_val) in enumerate(CV.split(X_train)):

    pred_val = model.fit(
        X_train[id_train], 
        y_train[id_train]
    ).predict(X_train[id_val])

    pred_val_matrix[id_val, i] = np.array(
        pred_val, dtype=float
    )

pred_val = np.nanmean(pred_val_matrix, axis=1)
  • 合规性分数基于交叉验证预测和实际值的绝对残差在训练数据上计算。
conformity_scores  = np.abs(
    np.subtract(
        y_train[~np.isnan(pred_val)], 
        pred_val[~np.isnan(pred_val)]
    )
)
  • 未来的预测分布通过将合规性分数添加到测试预测中获得。
pred_test = model.fit(
    X_train, 
    y_train
).predict(X_test)

estimated_test_distributions = np.add(
    pred_test[:, None], conformity_scores
)

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

测试数据上的预测分布 [图片由作者提供]

按照上述程序,我们得到一组可能的轨迹,这些轨迹是未来值可能遵循的路径。我们拥有所有需要的东西来提供预测的概率表示。

从概率预测到到达时间预测

对于每个未来的时间点,记录了估计的测试分布中值超过预定义阈值(我们的击中目标水平)的次数。这个计数被转化为一个概率,方法是通过估计测试分布中的值的数量进行归一化。

最后,对概率数组应用了一种转换,以获得一系列单调递增的概率。

THRESHOLD = 40

prob_test = np.mean(estimated_test_distributions > THRESHOLD, axis=1)

prob_test = pd.Series(prob_test).expanding(1).max()

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

测试集上的预测数据点与实际数据点以及击中时间概率[作者提供的图片]

无论我们尝试预测的事件是什么,我们都可以仅从点预测开始生成概率曲线。解释仍然很直接,即对于每个预测的时间点,我们可以推导出目标序列达到预定义水平的概率。

总结

在这篇文章中,我们介绍了一种为预测模型提供概率结果的方法。这不需要应用奇怪且复杂的额外估计技术。从一个点预测问题出发,可以通过应用击中时间方法,添加任务的概率概述。

查看我的 GITHUB 仓库

保持联系:Linkedin

利用 SageMaker 多模型端点和 GPU 实例托管数百个 NLP 模型

原文:towardsdatascience.com/host-hundreds-of-nlp-models-utilizing-sagemaker-multi-model-endpoints-backed-by-gpu-instances-1ec215886248

将 Triton 推理服务器与 Amazon SageMaker 集成

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

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

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

图片来源于 Unsplash

过去,我们曾探索过**SageMaker 多模型端点 (MME)**作为在单一端点后面托管多个模型的经济有效的选项。虽然在 MME 上托管较小的模型可以使用基于 CPU 的实例,但随着这些模型变得越来越大和复杂,有时可能需要 GPU 计算。

MME 由 GPU 支持的实例是一项特定的 SageMaker 推理功能,我们将在本文中利用它展示如何在单个端点上高效托管数百个 NLP 模型。请注意,在本文发布时,SageMaker 上的 MME GPU 目前支持以下单 GPU 基于的实例系列:p2、p3、g4dn 和 g5。

MME GPU 目前还由两个模型服务堆栈提供支持:

  1. Nvidia Triton 推理服务器

  2. TorchServe

为了本文的目的,我们将利用 Triton 推理服务器和 PyTorch 后端,在我们的 GPU 实例上托管基于 BERT 的模型。如果你对 Triton 不太熟悉,我们会稍作介绍,但我建议参考我的入门文章 这里。

注意:本文假设你对 SageMaker 部署和实时推理有中级理解。我建议你参考这篇 文章 以深入了解部署/推理。我们也会概述多模型端点,但要进一步了解,请参考这份 文档

免责声明:我是 AWS 的机器学习架构师,我的观点仅代表我个人。

什么是 MME?解决方案概述

为什么使用多模型端点(MME),以及何时使用它们?MME 是一种成本和管理上有效的托管选项。传统的 SageMaker 端点设置将如下所示:

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

作者提供的图片

当你有数百或甚至数千个模型时,管理如此多的不同端点变得困难,而且你需要为每个持久端点背后的硬件付费。使用 MME,这变得简单,因为你只需要管理一个端点和一组硬件:

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

作者提供的图片

你可以将这些不同的模型打包成一个模型 tarball(model.tar.gz)。这个模型 tarball 实质上会包含所有模型元数据,格式符合模型服务解决方案的要求。在这个例子中,我们使用 Triton 作为我们的模型服务器,所以我们的 model.tar.gz 将如下所示:

- model.tar.gz/
  - linear_regression_model
    - 1
       - model.pt
       - model.py (optional, not included here)
    - config.pbtxt

在这个例子中,我们将制作 200 份我们的模型 tarball,以展示如何在单个端点上托管多个模型。对于实际应用,这些 tarballs 会根据你推送到端点后的模型有所不同。这些 tarballs 都被捕获在 SageMaker 能理解的一个通用 S3 路径中:

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

MME 分桶(作者提供的图片)

MME 背后的模型如何管理?SageMaker MME 将接收请求并动态加载和缓存你调用的特定模型。如果你期望你的端点会有大量流量,最好在端点后面配置多个初始实例或设置自动扩展。例如,如果单个模型接收了大量调用,这个模型将被加载到另一个实例上,以处理额外的流量。要进一步了解 SageMaker MME 的负载测试,请参考这个 指南。

本地设置与测试

在这个例子中,我们将在一个 SageMaker Classic Notebook 实例中工作,使用 conda_python3 内核和 g4dn.4xlarge 实例。我们使用基于 GPU 的实例在本地测试 Triton,然后再部署到 SageMaker 实时推理中。

在这个例子中,我们使用了流行的 BERT 模型。我们首先要创建本地模型文件,因此我们使用 PyTorch 进行跟踪,然后保存序列化的模型文件。

import torch
from transformers import BertModel, BertTokenizer
device = "cuda" if torch.cuda.is_available() else "cpu"

# Load bert model and tokenizer
model_name = 'bert-base-uncased'
model = BertModel.from_pretrained(model_name, torchscript = True)
tokenizer = BertTokenizer.from_pretrained(model_name)

# Sample Input
text = "I am super happy right now to be trying out BERT."

# Tokenize sample text
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)

# jit trace model
traced_model = torch.jit.trace(model, (inputs["input_ids"], inputs["attention_mask"]))

# Save traced model
torch.jit.save(traced_model, "model.pt")

我们可以通过加载保存的模型并用标记化的文本运行示例推理来确认模型推理是否正常。

# sample inference with loaded model
loaded_model = torch.jit.load("model.pt")
res = loaded_model(input_ids=inputs["input_ids"], attention_mask=inputs["attention_mask"])
res

我们现在可以专注于设置 Triton 来托管这个特定的模型。为什么在实现 SageMaker 之前需要 本地测试 Triton?我们希望在创建 SageMaker 端点之前捕获任何设置问题。创建 SageMaker 端点可能需要几分钟,直到你在日志中看到失败之前,你无法知道你的设置中出现了什么问题,即使是一个小的脚本错误或模型 tarball 的不当结构。通过首先本地测试 Triton,我们可以快速迭代我们的配置和模型文件,以捕获任何错误。

对于 Triton,我们首先需要一个 config.pbtxt 文件。这捕获了我们的输入和输出维度以及其他你希望调整的 Triton 服务器属性。在这种情况下,我们可以从描述 BERT 架构的 transformers 库中获取输入和输出形状。

from transformers import BertConfig
bert_config = BertConfig.from_pretrained(model_name)
max_sequence_length = bert_config.max_position_embeddings
output_shape = bert_config.hidden_size
print(f"Maximum Input Sequence Length: {max_sequence_length}")
print(f"Output Shape: {output_shape}")

我们可以使用这些值来创建我们的 config.pbtxt 文件。

name: "bert_model"
platform: "pytorch_libtorch"

input [
  {
    name: "input_ids"
    data_type: TYPE_INT32
    dims: [1, 512]
  },
  {
    name: "attention_mask"
    data_type: TYPE_INT32
    dims: [1, 512]
  }
]

output [
  {
    name: "OUTPUT"
    data_type: TYPE_FP32
    dims: [512, 768]
  }
]

然后我们用以下 Docker 命令启动 Triton 推理服务器,指向我们的模型库。

docker run --gpus all --rm -p 8000:8000 -p 8001:8001 -p 8002:8002 -v
/home/ec2-user/SageMaker:/models nvcr.io/nvidia/tritonserver:23.08-py3
tritonserver --model-repository=/models --exit-on-error=false --log-verbose=1

一旦容器启动,你可以进行示例请求以确保我们能够成功地使用现有模型文件进行推理。

import requests
import json

# Specify the model name and version
model_name = "bert_model" #specified in config.pbtxt
model_version = "1"

# Set the inference URL based on the Triton server's address
url = f"http://localhost:8000/v2/models/{model_name}/versions/{model_version}/infer"

# sample invoke
output = requests.post(url, data=json.dumps(payload))
res = output.json()

一旦这项工作成功进行,我们可以专注于 SageMaker MME 部署。

SageMaker MME GPU 部署

现在我们已经将模型文件转换为模型服务器所理解的格式,我们可以将其封装成 SageMaker 预期的 model.tar.gz 格式。

!tar -cvzf model.tar.gz bert_model/

我们还在一个公共 S3 路径中创建了 200 个模型副本,以支持我们的 MME。

%%time
# we make a 200 copies of the tarball, this will take about ~6 minutes to finish (can vary depending on model size)
for i in range(200):
    with open("model.tar.gz", "rb") as f:
        s3_client.upload_fileobj(f, bucket, "{}/model-{}.tar.gz".format(s3_model_prefix,i))

除了模型文件的位置,我们还需要指定我们用于 SageMaker 部署的管理 Triton 容器。

triton_image_uri = "{account_id}.dkr.ecr.{region}.{base}/sagemaker-tritonserver:23.07-py3".format(
    account_id=account_id_map[region], region=region, base=base
)

print(f"Triton Inference server DLC image: {triton_image_uri}")

接下来的几个步骤是常见的 SageMaker 端点创建流程:

在我们的 EndpointConfiguration 对象中,我们指定了基于 GPU 的实例:在本例中为 g4dn.4xlarge。

endpoint_config_response = client.create_endpoint_config(
    EndpointConfigName=endpoint_config_name,
    ProductionVariants=[
        {
            "VariantName": "tritontraffic",
            "ModelName": model_name,
            "InstanceType": "ml.g4dn.4xlarge",
            "InitialInstanceCount": 1,
            "InitialVariantWeight": 1
        },
    ],
)

endpoint_name = "triton-mme-gpu-ep" + time.strftime("%Y-%m-%d-%H-%M-%S", time.gmtime())

create_endpoint_response = client.create_endpoint(
    EndpointName=endpoint_name, EndpointConfigName=endpoint_config_name
)

print("Endpoint Arn: " + create_endpoint_response["EndpointArn"])

端点可能需要几分钟来创建,但创建完成后,你应该能够运行示例推理。在 TargetModel 头部,你可以指定 1 到 200 之间的任何模型,因为我们将这个范围设定为不同 model.tar.gz 文件的分隔符。

response = runtime_client.invoke_endpoint(
    EndpointName=endpoint_name, ContentType="application/octet-stream", 
    Body=json.dumps(payload), TargetModel='model-199.tar.gz'
)
print(json.loads(response["Body"].read().decode("utf8")))

在运行推理时,你也可以通过 CloudWatch 监控硬件和调用指标。特别是由于这是一个基于 GPU 的端点,你可以通过 API 或 SageMaker 控制台监控 GPU 使用率。

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

SageMaker 控制台的监控标签(作者截图)

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

硬件 GPU 指标(作者截图)

要了解所有其他 MME CloudWatch 指标,请参阅以下文档

附加资源与结论

[## SageMaker-Deployment/RealTime/Multi-Model-Endpoint/Triton-MME-GPU/mme-gpu-bert.ipynb at master ·…

对 SageMaker 推理选项及其他功能的示例汇编。…

github.com](https://github.com/RamVegiraju/SageMaker-Deployment/blob/master/RealTime/Multi-Model-Endpoint/Triton-MME-GPU/mme-gpu-bert.ipynb?source=post_page-----1ec215886248--------------------------------)

示例的完整代码可以在上述链接中找到。MME 已经是一个非常强大的功能,但当与基于 GPU 的硬件配合使用时,可以让我们在 NLP 和 CV 领域托管更大的模型。Triton 也是一个动态服务选项,它支持多种不同的框架和多样的硬件,从而大大增强了我们的 MME 应用程序。有关更多 SageMaker 推理示例,请参阅以下链接。如果你有兴趣更好地了解 Triton,请参阅我的 PyTorch 模型入门指南。

一如既往,感谢你的阅读,欢迎留下任何反馈。

如果你喜欢这篇文章,欢迎在 LinkedIn 上与我联系,并订阅我的 Medium Newsletter。如果你是 Medium 的新用户,可以使用我的 会员推荐链接注册。

在 Colab 上免费托管你的 Google Earth Engine RESTful API

原文:towardsdatascience.com/host-your-google-earth-engine-restful-apis-on-colab-for-free-3a95abc729d0

使用 FastAPI 和 ngrok

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

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

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

图片由NASA提供,Unsplash上的照片

地理空间数据的需求一直很高。它揭示了我们星球随时间变化的情况。当我们谈到地理空间时,我们会想到Google Earth Engine (GEE)。该服务有几个优势。它托管了跨度超过 37 年的大量数据集合。所有计算都在谷歌强大的云基础设施上运行。更重要的是,对于非营利项目,它是免费的。通过 GEE,我们可以免费研究土地利用和土地覆盖 (LULC)、植被、本地气候(这里这里),甚至美国的农作物生产

然而,GEE 确实有较高的门槛。首先,必须精通 JavaScript 或 Python。其次,我们需要熟悉许多地理空间概念,如图像集合、几何体和卫星波段。第三,它的异步请求-响应模式对于新手来说需要一些时间来适应。

这对许多数据科学家来说是一个小挑战。大多数情况下,他们只希望快速获得一组坐标的某些值,例如土壤 pH 值或平均地表温度。截止目前,他们需要进行相当多的编码,因为 GEE 没有提供 RESTful API。如果我们能自己填补这个空白岂不是很好(视频 1)?我们的 API 应该封装一些常见的 GEE 计算,并提供互联网 HTTP 访问。

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

视频 1. 使用 FastAPI 在 Google Colab 上创建的 Google Earth Engine API。视频由作者提供。

让我们在本文中完成它。我选择了FastAPI来完成这项工作。这个流行的库允许我们用 Python 构建高性能的 API,Python 是 GEE 中的两种服务语言之一。Google Colab 是首选的平台。Colab 不仅与 GEE 无缝集成,而且在ngrok的帮助下,还可以通过 HTTP 公开提供 API(视频 1)。在本文中,我将描述两个 API 的构建。其中一个返回土地温度,另一个计算给定坐标集的土壤 pH 值。之后,我将演示如何使用这些 API 从BacDive获取一些细菌的元数据。

这里定义了 API。

[## Google Colaboratory GEE APIs

colab.research.google.com](https://colab.research.google.com/drive/1BNsFLHA_ISC7sDSLF4-znBNY9YSIUbh1?usp=sharing&source=post_page-----3a95abc729d0--------------------------------)

这是一个 Python 应用程序,它通过我们的 GEE API 在 BacDive 中验证数据。

[## Google Colaboratory BacDive

colab.research.google.com](https://colab.research.google.com/drive/12NXlV6Q8Qrs6hLqEIeEmQ1cm_CElCkOB?usp=sharing&source=post_page-----3a95abc729d0--------------------------------)

1. Google Earth Engine API 与 FastAPI 和 ngrok 的结合

首先,你需要一个Google Earth Engine 账户和一个ngrok 账户。在你的账户页面找到 ngrok 的 Authtoken(图 1)。

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

图 1. 将 Authtoken 存储在你的 ngrok 账户页面。图片由作者提供。

1.1 初始化

创建一个 Colab 笔记本。在库导入后,让我们初始化 GEE 和 ngrok。首先,按照 Google 的说明进行身份验证和初始化 GEE(第 1 和第 2 行)。之后,input函数会提示你输入 ngrok 的 Authtoken(第 3 行)。将你的 Authtoken 粘贴到输入框中并确认。然后,代码将在第 4 行授权你的 ngrok 实例。

# Trigger the authentication flow.
ee.Authenticate()

# Initialize the library.
ee.Initialize()

ngrok_key = input("Your ngrok Authtoken")

!ngrok authtoken {ngrok_key}

1.2 GEE 封装函数

接下来,我们可以创建一些封装函数来封装 GEE 交互。

def generate_collection(geometry, dataset, startDate, endDate):
    collection = ee.ImageCollection(dataset).filterDate(startDate, endDate).filterBounds(geometry);
    return collection

def get_mean(image_collection, point, property, scale_factor):
    image = image_collection.select([property]).reduce(ee.Reducer.mean()).multiply(scale_factor)

    fcPoint = ee.FeatureCollection([ee.Feature(point)])

    geojson = image.sampleRegions(collection = fcPoint,  scale = 1000, geometries = True)

    return geojson.getInfo()["features"][0]["properties"]

def generate_image(dataset):
    return ee.Image(dataset)

def get_image_value(image, point, property, scale_factor):
    return image.reduceRegion(ee.Reducer.mean(), point, 100).get(property).getInfo() * scale_factor

一般来说,GEE 将测量结果存储在图像中。某些测量,例如海拔和土壤 pH,只做了一次,它们被存储在单个图像中。相比之下,其他测量,如土壤温度和降水量,是周期性进行的,因此它们被存储在图像集合中。两个函数generate_collection(第 1-3 行)和generate_image(第 14-15 行)分别返回图像集合和单个图像。接下来,我们想获取目标区域的平均测量值(第 5-12 行 & 第 17-18 行)。

1.3 FastAPI

现在是时候在我们的笔记本中设置 FastAPI 了。

app = FastAPI()

@app.get('/')
async def root():
    return {'hello': 'world'}

@app.get("/land-surface-temperature")
async def get_land_surface_temperature(lat: float, lon: float, start_date: datetime.date, end_date: datetime.date):
    dataset = "MODIS/061/MOD11A1"

    point = ee.Geometry.Point([lon, lat])

    image_collection = generate_collection(point, dataset, start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d"))

    result = get_mean(image_collection, point, "LST_Day_1km", 0.02)

    return {'result': result}

@app.get("/soil-ph")
async def get_soil_ph(lat: float, lon: float):
    dataset = "OpenLandMap/SOL/SOL_PH-H2O_USDA-4C1A2A_M/v02"

    point = ee.Geometry.Point([lon, lat])

    image = generate_image(dataset)

    scale_factor = 0.1

    ph = get_image_value(image, point, "b0", scale_factor)

    return {"result": {"pH": ph}}

在这个代码块中,我们首先初始化一个 FastAPI 应用。然后定义了三个路由:根路由,land-surface-temperaturesoil-ph。在后两个路由中,我们使用了第 1.2 节中的函数来请求 GEE 中的测量值。温度值来自于MOD11A1.061 Terra Land Surface Temperature and Emissivity Daily Global 1km数据集(MODIS 数据和产品在 LP DAAC 中获取,无后续使用、销售或再分发的限制),而 pH 值由OpenLandMap Soil pH in H2O数据集提供(CC-BY-SA-4.0)。

1.4 ngrok

现在让我们开始 API 服务。这里的代码借鉴自一个回答在 stackoverflow.com 上。

ngrok_tunnel = ngrok.connect(8000)
print('Public URL:', ngrok_tunnel.public_url)
nest_asyncio.apply()
uvicorn.run(app, port=8000)

代码将生成一个端点 URL 并继续运行(视频 1)。

1.5 测试

我们现在可以测试服务。点击端点 URL,你将看到根路由的“hello world”消息(视频 1)。

令人惊讶的是,FastAPI 会自动生成文档。你可以在/redoc路由下访问它(图 2)。

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

图 2. 文档由 FastAPI 准备。图片由作者提供。

让我们测试land-surface-temperature API。在 URL 中的“?”标记后,将所需参数编码为键值对。例如,以下查询字符串允许我们获取 2020 年 1 月 1 日到 2020 年 5 月 1 日(52.72389418745157, -92.03593750000002)的地表温度。

/land-surface-temperature?lat=52.72389418745157&lon=-92.03593750000002&start_date=2020-01-01&end_date=2020-05-01

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

图 3. 2020 年 1 月 1 日到 2020 年 5 月 1 日(52.72389418745157, -92.03593750000002)的地表温度。图片由作者提供。

请注意,返回的值是以开尔文为单位的(图 3)。

soil-ph API 的工作原理类似。但它不需要采样周期。因此,以下查询字符串可以单独请求相同坐标对的土壤 pH 值(视频 1)。

/soil-ph?lat=52.72389418745157&lon=-92.03593750000002

2. 比较 BacDive 的实验室结果与 GEE 的野外元数据

BacDive数据库由 DSMZ 创建,收集了关于细菌分离株的信息,包括其生长温度、pH 值和代谢特征。请注意,它几乎所有的信息都是在实验室中生成的。微生物在野外的行为很可能有所不同。

最近,BacDive已集成了Microbeatlas。嵌入的Microbeatlas地图显示了许多细菌 16S 序列的全球分布(图 4)。

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

图 4. BacDiveMicrobeatlas。作者提供的图片。

这个交叉验证非常棒。现在,研究人员不仅可以了解细菌在实验室中的行为(BacDive),还可以了解它们在全球范围内的分布(Microbeatlas)。但我们可以做得更多。我们可以从新推出的 GEE APIs 中收集野外的元数据,并将其与 BacDive 的实验室结果进行比较。这种比较可以向我们展示细菌在实验室和野外的生活是否有所不同。

以细菌Rhodopseudomonas palustris R1为例。它的 Microbeatlas 页面 显示该细菌可以在长长的样本列表中找到(图 5)。

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

图 5. 根据Microbeatlas,全球范围内的Rhodopseudomonas palustris R1分布。作者提供的图片。

截至 2023 年 1 月 5 日,Microbeatlas中的DOWNLOAD按钮无法使用。因此,我使用“soil”关键词过滤了这些样本。然后我检查了前几个样本,并选择了那些具有完整位置和时间数据的样本(表 1)。

表 1. 五个样本Rhodopseudomonas palustris R1丰富存在的土壤。

使用以下 Python 代码,我们可以获取这些样本的土地温度和 pH 值(也可以在我上面的 Colab 链接中找到)。

sheet_id = "YOUR GOOGLE SHEETS ID"
sheet_name = "YOUR GOOGLE SHEETS NAME"

url = f"https://docs.google.com/spreadsheets/d/{sheet_id}/gviz/tq?tqx=out:csv&sheet={sheet_name}"
df = pd.read_csv(url)

api_url = "YOUR ngrok ENDPOINT"

land_temperature_route = "/land-surface-temperature/?"
land_ph_route = "/soil-ph/?"

for index, row in df.iterrows():
    date = datetime.strptime(row["date"], "%Y-%m-%d")

    start_date = datetime.strptime(f"{date.year}-{date.month}", '%Y-%m').date()
    end_date =  datetime.strptime(f"{date.year}-{date.month + 1}", '%Y-%m').date()

    lat = row["lat"]

    lon = row["lon"]

    temp_url = f"{api_url}{land_temperature_route}lat={lat}&lon={lon}&start_date={start_date}&end_date={end_date}"
    ph_url = f"{api_url}{land_ph_route}lat={lat}&lon={lon}"

    temp = requests.get(temp_url).json()["result"]["LST_Day_1km_mean"] -273.15
    ph = requests.get(ph_url).json()["result"]["pH"]

    print (f'{row["sample"]}\t{temp}\t{ph}')

结果见表 2。

表 2. 根据 GEE 的五个样本的温度和 pH 值。

一方面,这些前五个样本的温度值范围为 11 至 19°C,远低于 BacDive 中显示的 28–30°C 的生长温度。另一方面,BacDive 没有记录该细菌的生长 pH 值。但我们的结果表明,Rhodopseudomonas palustris R1在酸性土壤中丰富存在,从而填补了 BacDive 中的信息空白。

结论

本文展示了使用 FastAPI 和 ngrok 在 Colab 上为 GEE 原型化自己的 RESTful API 是多么简单。只需几行代码,我们就可以在互联网上免费设置功能完善的 API。现在我们可以通过简单的 URL 请求 GEE 的地理空间结果。希望这些 API 也能吸引更多用户和研究人员使用 GEE。我在本文中仅展示了两个 API。我鼓励你为你的项目构建更多的 API。你也可以修改查询字符串的设计。但请注意,这种设置并不具备可扩展性。在生产环境中,我们最好将 API 部署到 Deta 或其他云基础设施上。

正如你在 BacDive 部分看到的,这些简单的 API 非常有用。我们使用它们填补了 BacDive 中的信息空白。但为何止步于此呢?例如,我们可以对全球微生物 DNA 进行测序,并将结果与 GEE 元数据结合。这些分析可能揭示有助于或限制某些微生物传播的环境因素。这些知识可以帮助我们对抗传染病和遏制疫情。

来自 BacDive 的数据采用 知识共享 4.0 国际许可证

## 通过我的推荐链接加入 Medium - 黄思行

作为 Medium 会员,你的会员费的一部分将用于支持你阅读的作者,你还可以完全访问每一个故事…

dgg32.medium.com

德国住房租赁市场:使用 Python 的探索性数据分析

原文:towardsdatascience.com/housing-rental-market-in-germany-exploratory-data-analysis-with-python-3975428d07d2

使用 Python、Pandas 和 Bokeh 获取统计见解

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

·发表于 Towards Data Science ·27 min 阅读·2023 年 4 月 4 日

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

Salzbrücke, Germany, 图片来源 en.wikipedia.org/wiki/German_Timber-Frame_Road

德国不仅是欧洲最大的经济体,而且还是一个拥有美丽风景和有趣文化的国家。不足为奇的是,德国是来自世界各地的游客和外籍人士的热门目的地。对德国住房租赁市场的探索性数据分析不仅对数据分析师有趣,也对即将移居和在此工作的人有意义。我将展示一些可以通过 Python、Pandas 和 Bokeh 发现的有趣趋势。

让我们开始吧。

数据收集

为了找到数据,我决定使用 ImmoScout24,它不仅是最大的(在撰写本文时列出了大约 72K 套公寓和房屋),也是最早的网站之一。根据 web.archive.org,第一个版本是在 1999 年制作的,已有 20 多年。ImmoScout24 还有一个 API 和开发者页面。我联系了公关部门,他们允许我使用该网站的数据进行出版,但无法提供 API 密钥。这个 API 可能仅供合作伙伴使用,以添加或编辑住房数据,而不是用于批量读取。不过,这不是问题;可以使用 Python 从网页中检索数据,这使得任务更加具有挑战性。

在以类似方式收集数据之前,请先征得所有者的许可,并且要“做一个好房客”:不要使用过多的线程以避免服务器过载,使用本地保存的 HTML 文件调试代码,在使用网页浏览器时尽可能禁用图片加载。

首先,我尝试使用 requests 获取页面数据:

import requests

url_berlin = "https://www...."
print(requests.get(url_berlin))

可惜,没能成功——页面对机器人有保护,在获得搜索结果之前,用户必须确认自己不是机器人。像更改“user-agent”这样简单的方法没有帮助。好吧,我们确实不是机器人,这没问题。 Selenium Python 库允许使用真实的 Chrome 浏览器来获取数据并自动化读取页面:

from selenium import webdriver
import time

def page_has_loaded(driver: webdriver.Chrome):
    """ Check if the page is ready """
    page_state = driver.execute_script('return document.readyState;')
    return page_state == 'complete'

def page_get(url: str, driver: webdriver.Chrome, delay_sec: int):
    """ Get the page content """
    driver.get(url)
    time.sleep(delay_sec)
    while not page_has_loaded(driver):
        time.sleep(1)
    return driver.page_source

options = webdriver.ChromeOptions()
driver = webdriver.Chrome(executable_path="./chromedriver", chrome_options=options)

# Get the first page
url_page1 = "https://www...."
html1 = page_get(url_page1, driver, delay_sec=30)

# Get next pages
url_page2 = "https://www..."
html2 = page_get(url_page1, driver, delay_sec=1)
...

当我们运行代码时,浏览器窗口将被打开。正如我们在代码中看到的,在处理第一页之前,我添加了一个 30 秒的延迟,这足以确认我不是机器人。在此期间,最好通过按右侧的三个点打开浏览器“设置”,并禁用图像加载;这会使处理速度更快。在请求下一页时,浏览器保持打开状态,进一步的数据可以在没有“机器人”检查的情况下进行处理。

在获取 HTML 内容后,数据提取或多或少是直接的。首先,我们必须通过使用 Web 浏览器中的“检查”按钮找到 HTML 元素的属性:

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

浏览器中的 HTML 输出,图片由作者提供

然后我们可以通过使用 Beautiful Soup 库在 Python 中获取这些元素。此代码从页面中提取所有公寓的 URL:

from bs4 import BeautifulSoup

soup = bs.BeautifulSoup(html1, "lxml")
li = soup.find(id="resultListItems")
links_all = []
children = li.find_all("li", {"class": "result-list__listing"})
for child in children:
    for link in child.find_all("a"):
        if 'data-go-to-expose-id' in link.attrs:
            links_all.append(base_url + link['href'])
            break
links_all.append(base_url + link['href'])

现在让我们找出可以获得什么数据。

数据字段

对于每个房地产对象,我们可以得到一个这样的页面(出于隐私原因,所有值和公司名称均被模糊处理):

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

页面示例,图片由作者提供

让我们看看可以获得什么数据:

  • 标题。在这张图片中,我们可以看到(当然是德文)“一个美丽的单间公寓在一个美丽的(地方)赫姆斯多夫”。我认为这段文字对于分析没有用处,不过只是为了好玩,稍后我们将从中构建一个词云。

  • 类型。在这个例子中,类型是“Etagenwohnung”(楼层上的公寓)。

  • 冷租是所谓的“冷价格”。这是一个不包含公用费用的租金价格,例如取暖费或电费。

  • 暖租,或称“暖价格”。这个名字可能有些误导,因为我们可以看到在图片中,“暖价格”不仅包含取暖费用(“heizkosten”),还包括其他额外费用(“nebenkosten”)。

  • 楼层。在这个页面上我们可以看到文本“0 of 3”——需要进行少量解析。在德国,第一层是第一个 升高 的楼层,所以我猜 0 意味着“底层”,或在德语中称为“Erdgeschoss”。从“0 到 3”的文本中,我们还可以提取出建筑物的总楼层数。

  • 押金。一个可以覆盖可能损坏的金额,并在租赁结束时退还给租户。在这里我们可以看到一个“3-冷租”的值。我们立即记住一些解析工作是必需的。

  • Flasche(面积)。顾名思义,这是房屋或公寓的面积。

  • Zimmer(房间)。在这个例子中,是 1。

其他数据字段也可以从页面中提取,比如车库的额外租金或允许养宠物的费用,但对于我们的任务,这些字段应该足够了。

HTML 解析过程通常与之前描述的相同。例如,要获取房产标题,可以使用以下代码:

soup = bs.BeautifulSoup(s_html, "lxml")

title = soup.find_all("h1", id="expose-title")
if len(title) > 0:
    str_title = title[0].get_text().strip()

其他字段也可以以相同的方式找到。在运行了所有页面的代码后,我得到了一个这样的数据集,并将其保存为 CVS 格式:

property_id;logging_date;property_area;num_rooms;floor;floors_in_building;price_cold_eur;price_warm_eur;deposit_eur;property_type;publisher;city;title;address;region;
13507XXX1;2023-03-20;7.0;1;None;None;110;110;None;Sonstige;Private;Berlin;Lagerraum / Kellerraum / Abstellraum zu vermieten;None;Moabit, 10551 Berlin;
13613XXX2;2023-03-20;29.0;1;None;None;189;320;None;None;XXXXXXXX Sverige AB;Berlin;Wohnungstausch: Luise-Zietz-Straße 119;Luise-Zietz-Straße 119;Marzahn, 12000 Berlin;
...
14010XXXn;2023-03-20;68.0;1;None;None;28000;28000;1000;None;HousingXXXXXXXXX B.V;Berlin;Wilhelminenhofstraße, Berlin;Wilhelminenhofstraße 0;Oberschöneweide, 12459 Berlin;

现在让我们看看可以获取哪些信息。

数据转换与加载

正如我们在上一段中看到的,住房数据肯定需要一些清理和转换。

我从德国不同地区的 6 个城市收集了数据:柏林、德累斯顿、法兰克福、汉堡、科隆和慕尼黑。作为例子,我们检查柏林;对于其他城市,方法是一样的。首先,让我们将 CSV 加载到 Pandas 数据框中:

import pandas as pd

df_berlin = pd.read_csv("Berlin.csv", sep=';', 
                        na_values=["None"], parse_dates=['logging_date'], 
                        dtype={"price_cold_eur": pd.Int32Dtype(), 
                               "price_warm_eur": pd.Int32Dtype(), 
                               "floor": pd.Int32Dtype(), 
                               "floors_in_building": pd.Int32Dtype()})
display(df_berlin)

过程很简单,但有一些有用的技巧。首先,解析是在 Python 中完成的,对于缺失值,“None” 被写入 CSV。我不希望有“None”作为文本字符串,因此我将其指定为“na_values”参数。我还指定了“;”作为分隔符,并为整数字段,如价格或楼层号,设置了“pd.Int32Dtype”类型。顺便提一下,我最初尝试使用 UInt32,因为价格无论如何不可能是负数,但结果发现计算差异时,有时会出现负值,这导致一些单元格得到类似 4,294,967,295 的值。实际上,保持 Int32 更简单;幸运的是,房价不会高于最大 Int32 值 😉

如果一切都做得正确,作为输出,我们应该得到类似这样的结果:

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

让我们检查一下维度和 NULL 值的数量:

display(df_berlin.shape)
display(df_berlin.isna().sum())

输出看起来是这样的:

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

我们可以看到,柏林有 3556 套房产待租,每套房产都有“冷”价和“热”价、一个面积和一个房间数量;这些字段可能是必需的。2467 套房产缺少“类型”字段,2200 套房产没有“楼层”值,等等。如前所述,某些字段,如“押金”值,将需要转换,例如,我们需要一种方法将类似“3 Nettokaltmieten”的文本字符串转换为数值。

基本分析

首先,让我们看看在不费太多劲地编写代码的情况下,使用 Pandas 能做些什么。作为热身,使用 Pandas 的“describe”方法获取数据集的描述性统计

display(df_berlin.drop(columns=['property_id']).describe().style.format(precision=0, thousands=","))

在这里,我只稍微调整了一下输出:我从结果中移除了“property_id”,并通过添加“千位”分隔符来调整输出样式。柏林的数据结果如下:

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

我们可以看到,柏林有 3,556 处房产上市。我们在前一步已经得到了这个值,并且有某种验证结果正确性的方法是好的。那些 3,556 处房产的中位数(第 50 百分位数)面积是 60 平方米,中位数价格是 1,645€。第 75 百分位数是 2,271€,这意味着 75%的租金价格低于这个值。有趣的是,平均房间数量是 2,这在直观上是正确的,但即便 11 间房的公寓也有(最高价格 28,000€暗示这些 11 间房的地方不便宜)。

下一步,让我们制作散点矩阵,用于一些字段:房产面积、房间数量和价格。可以通过 Pandas 的一个方法调用来完成:

pd.plotting.scatter_matrix(df_berlin[["property_area", "num_rooms", 
                                      "price_warm_eur", "price_cold_eur"]][(df_berlin['price_cold_eur'] > 0) & (df_berlin['price_cold_eur'] < 5000)], 
                           hist_kwds={'bins': 50, 'color': '#0C0786'}, 
                           figsize=(16, 16))

在这里,我还调整了可视化参数——我调整了直方图的箱数,将价格限制在 0-5000€范围内(否则图表因为一些离群值而过小),并且调整了颜色。结果对于 4 行代码来说还不错:

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

散点矩阵,作者图片

为了进一步可视化,我将使用Bokeh库,它适合制作美丽且互动的图表。让我们首先导入所需的文件:

from bokeh.io import show, output_notebook, export_png
from bokeh.plotting import figure, output_file
from bokeh.models import ColumnDataSource, LabelSet, Label, Whisker, FactorRange
from bokeh.transform import factor_cmap, factor_mark, cumsum
from bokeh.palettes import *
from bokeh.layouts import row, column
output_notebook()

我们准备好了;开始吧。

房产类型

第一个令我感兴趣的问题是德国有哪些类型的房产。如前所述,我从 6 个不同的德国城市收集了数据。让我们加载 CSV 文件并将它们合并成一个数据框:

df_berlin = pd.read_csv("Berlin.csv", sep=';', na_values=["None"], parse_dates=['logging_date'], dtype={"price_cold_eur": pd.Int32Dtype(), "price_warm_eur": pd.Int32Dtype(), "floor": pd.Int32Dtype(), "floors_in_building": pd.Int32Dtype()})
df_munchen = pd.read_csv("Munchen.csv", sep=';', na_values=["None"], parse_dates=['logging_date'], dtype={"price_cold_eur": pd.Int32Dtype(), "price_warm_eur": pd.Int32Dtype(), "floor": pd.Int32Dtype(), "floors_in_building": pd.Int32Dtype()})
df_hamburg = pd.read_csv("Hamburg.csv", sep=';', na_values=["None"], parse_dates=['logging_date'], dtype={"price_cold_eur": pd.Int32Dtype(), "price_warm_eur": pd.Int32Dtype(), "floor": pd.Int32Dtype(), "floors_in_building": pd.Int32Dtype()})
df_cologne = pd.read_csv("Cologne.csv", sep=';', na_values=["None"], parse_dates=['logging_date'], dtype={"price_cold_eur": pd.Int32Dtype(), "price_warm_eur": pd.Int32Dtype(), "floor": pd.Int32Dtype(), "floors_in_building": pd.Int32Dtype()})
df_frankfurt = pd.read_csv("Frankfurt.csv", sep=';', na_values=["None"], parse_dates=['logging_date'], dtype={"price_cold_eur": pd.Int32Dtype(), "price_warm_eur": pd.Int32Dtype(), "floor": pd.Int32Dtype(), "floors_in_building": pd.Int32Dtype()})
df_dresden = pd.read_csv("Dresden.csv", sep=';', na_values=["None"], parse_dates=['logging_date'], dtype={"price_cold_eur": pd.Int32Dtype(), "price_warm_eur": pd.Int32Dtype(), "floor": pd.Int32Dtype(), "floors_in_building": pd.Int32Dtype()}) 

df = pd.concat([df_berlin, df_munchen, df_hamburg, 
                df_cologne, df_frankfurt, df_dresden])

这段代码显然可以优化;例如,我可以使用glob.glob(‘.csv’)*来获取文件夹中所有 CSV 文件的列表,但对于仅 6 个文件来说,我实在是懒得这样做。

现在,让我们找出房产类型的分布:

 data_pr = df[['property_type']].fillna('Unbekannt').groupby(['property_type'], as_index=False).size().sort_values(by=["size"], ascending=True)

types = data_pr['property_type']
amount = data_pr['size']

palette = Viridis10 + Plasma10
p = figure(y_range=FactorRange(factors=types), width=1200, height=500, title="Apartment Types")
p.hbar(y=types, right=amount, height=0.8, color=palette[:len(types)])
p.xaxis.axis_label = 'Amount'
p.x_range.start = 0
show(p)

我使用了一些技巧来改进结果。我将“NA”值替换为“Unbekannt”,这是德语中的“未知”(其他类型都是德语的,所以名称应该统一)。然后,我按房产类型分组并按数量排序。作为最终调整,我指定了颜色调色板,以避免在 Matplotlib 风格中出现乏味的蓝色条形。最终输出如下:

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

房产类型分布,作者图片

结果很有趣。许多列表中的房产没有指定类型。在其他类型中,“Etagenwohnung”(楼层公寓)是最受欢迎的。第三和第四种类型是“dachgeshoss”(屋顶下的地方)和“erdgeschosswohnung”(底层公寓)。像“maisonette”(小房子)或“hochparterre”(提高的底层)这样的类型较少;读者可以自行查找更详细的描述。

让我们检查不同房产类型的价格。我猜“顶层公寓”应该比“标准”公寓更贵,而底层公寓应该比“标准”公寓便宜。让我们验证一下。我们可以通过按类型分组并在 Pandas 中聚合结果来找到价格分布:

def q0(x):
    return x.quantile(0.01)

def q1(x):
    return x.quantile(0.25)

def q3(x):
    return x.quantile(0.75)

def q4(x):
    return x.quantile(0.99)

agg_data = {'price_cold_eur': ['size', 'min', q0, q1, 'median', q3, q4, 'max']}

prices = df[df['price_cold_eur'].notna()][['property_type', 'price_cold_eur']].fillna('Unbekannt').groupby('property_type', as_index=False).agg(agg_data)
prices = prices.sort_values(by=('price_cold_eur', 'size'), ascending=True)
display(prices.style.hide(axis="index"))

我们可以在表格形式中查看结果:

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

使用箱线图,我们可以以可视化的形式呈现结果:

prices = prices.sort_values(by=('price_cold_eur', 'size'), ascending=True)

p_types = prices["property_type"]
q1 = prices["price_cold_eur"]["q1"].astype('int32')
q3 = prices["price_cold_eur"]["q3"].astype('int32')
v_min = prices["price_cold_eur"]["q0"].astype('int32')
v_max = prices["price_cold_eur"]["q4"].astype('int32')
median = prices["price_cold_eur"]["median"].astype('int32')

palette = Viridis10 + Plasma10
source = ColumnDataSource(data=dict(p_types=p_types, 
                                    lower=v_min, 
                                    bottom=q1, 
                                    median=median, 
                                    top=q3,                                     
                                    upper=v_max,
                                    color=palette[:p_types.shape[0]]))

p = figure(x_range=p_types, width=1400, height=500, title="Property types distribution") 
whisker = Whisker(base="p_types", upper="upper", lower="lower", source=source)
p.add_layout(whisker)
p.vbar(x='p_types', top='top', bottom='median', width=0.9, color='color', 
       line_color="black", source=source)
p.vbar(x='p_types', top='median', bottom='bottom', width=0.9, color='color', 
       line_color="black", source=source)

p.left[0].formatter.use_scientific = False
p.y_range.start = 0
p.y_range.end = 4000
p.yaxis.axis_label = 'Rent Price, EUR'
show(p)

我为每组使用了相同的大小排序,因此箱线图的调色板与之前的图表相同:

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

房产类型的须状图,作者提供的图像

我的猜测部分正确。顶层公寓确实是最贵的,但在标准公寓(“etagenwohnung”)、屋顶公寓(“dachgeshoss”)和底层公寓(“erdgeschosswohnung”)之间,没有显著差异。

房产价格

从整个数据集中获取分布是有用的,但让我们深入一些,比较不同地方的数据。正如之前所述,我从 6 个德国城市收集了数据:

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

城市位置地图,作者提供的图像

让我们看看从德国相对两端收集的数据之间的差异有多大。

每平方米价格

以一定金额租赁的房产大小是多少?使用散点图可以轻松获得结果;这种方法通常只需要两个 X 和 Y 的数组。但我们可以让它看起来更好。

首先,让我们创建一个按数量排序的可能房产类型列表,就像我们之前做的那样:

pr_types = df[['property_type']].fillna('Unbekannt').groupby(['property_type'], as_index=False).size().sort_values(by=["size"], ascending=True)['property_type']

然后我们可以为特定城市创建 3 个数组,数据将包括平方米面积、价格和类型:

price_limit, area_limit = 3000, 200  

df_city = df_berlin  
data_pr = df_city[df_city['price_cold_eur'].notna()][['property_area', 'price_cold_eur', 'property_type']].fillna('Unbekannt')
data_pr = data_pr[(data_pr['property_area'] > 0) & (data_pr['property_area'] < area_limit) & (data_pr['price_cold_eur'] > 0) & (data_pr['price_cold_eur'] < price_limit)] 
values_x = data_pr["property_area"].astype('float').values
values_y = data_pr["price_cold_eur"].astype('float').values
types = data_pr["property_type"]

在这里,我用“Unbekannt”替换了 NULL 房产类型,这在散点图本身中并不需要,但对图例有用。我还只使用了非 NaN 的价格值。选择了€3,000 和 200 m²的限制;我认为这是大多数读者感兴趣的合理范围。

作为一个可选步骤,我创建了一个线性回归模型并使用数据点进行了训练;这将允许绘制线性近似:

from sklearn.linear_model import LinearRegression

linear_model = LinearRegression().fit(values_x.reshape(-1, 1), values_y)

现在我们准备绘制结果:

palette = (Viridis10 + Plasma10)[:len(pr_types)]

name = "Berlin"
df_city = df_berlin

source = ColumnDataSource(dict(property_area=values_x, 
                               price_cold_eur=values_y, 
                               property_type=types))

title = f"Property prices: {name} ({df_city.shape[0]} items, {data_pr.shape[0]} displayed)"
p = figure(width=1200, height=550, title=title)

# Draw scatter
p.scatter("property_area", "price_cold_eur",
              source=source, fill_alpha=0.8, size=4,
              legend_group='property_type',
              color=factor_cmap('property_type', palette, pr_types)
             )
# Draw approximation with a linear model
linear_model = LinearRegression().fit(values_x.reshape(-1, 1), values_y)
p.line([0, 9999], linear_model.predict([[0], [9999]]), line_width=4, line_dash="2 2", alpha=0.5)

p.xaxis.axis_label = 'Area, m²'
p.yaxis.axis_label = 'Rent Price, EUR'
p.x_range.start = 0
p.x_range.end = area_limit
p.y_range.start = 0
p.y_range.end = price_limit
p.toolbar_location = None
p.legend.visible = True
p.legend.background_fill_alpha = 0.9

我决定将不同城市显示在一个图表上,因此我将所有这些代码放在一个单独的“get_figure_price_per_area”方法中。然后,我可以绘制几个 Bokeh 图形,将它们按行和列组合:

p1 = get_figure_price_per_area("Berlin", df, df_berlin)
p2 = get_figure_price_per_area("München", df, df_munchen)
p3 = get_figure_price_per_area("Hamburg", df, df_hamburg)
p4 = get_figure_price_per_area("Köln", df, df_koeln)
p5 = get_figure_price_per_area("Frankfurt", df, df_frankfurt)
p6 = get_figure_price_per_area("Dresden", df, df_dresden)
show(column(row(p1, p2), 
            row(p3, p4), 
            row(p5, p6)))

结果相当有趣:

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

价格与面积的散点图,作者提供的图像

首先,我们可以通过视觉上比较市场上可用物业的数量。柏林(图表左上角)是一个大城市,也是一个受欢迎的地方;那里的市场最大,价格差异也最大。例如,50 平方米的公寓可以在不同的价格类别中找到,从 400 欧元预算到 3000 欧元以上的高档。在比较中,德累斯顿(图表右下角)的房地产便宜得多,几乎没有昂贵的房产。也许德累斯顿的租赁需求低得多,可能与薪资和可用工作的数量有关。其次,在柏林的数据中,两个独立的类别明显可见。我不清楚解释,或许这与城市历史上被划分为西部(BRD)和东部(DDR)有关。

价格和面积的直方图

我们还可以通过使用直方图以更紧凑的形式查看价格。制作直方图很简单;NumPy 的“histogram”方法可以完成所有计算:

def get_figure_histogram_price(name: str, df_city: pd.DataFrame):
    price_limit = 10000
    prices = df_city['price_cold_eur'].dropna().values
    hist_e, edges_e = np.histogram(prices, density=False, bins=50, range=(0, price_limit))

    # Create figure
    palette = Viridis256[::3][0:len(hist_e)]  # Take every 3rd item from array
    p = figure(width=1400, height=500, 
               title=f"Property prices: {name} ({df_city.shape[0]} total)")
    p.quad(top=hist_e, bottom=0, left=edges_e[:-1], right=edges_e[1:], fill_color=palette)
    p.x_range.start = 0
    p.x_range.end = price_limit
    p.y_range.start = 0
    p.y_range.end = 360
    p.xaxis[0].ticker.desired_num_ticks = 20
    p.xaxis.axis_label = "Rent Price, EUR"
    p.yaxis.axis_label = "Amount"
    p.toolbar_location = None
    return p

我使用了相同的方法来绘制图表,将几个城市放在一起:

p1 = get_figure_histogram_price("Berlin", df_berlin)
p2 = get_figure_histogram_price("München", df_munchen)
p3 = get_figure_histogram_price("Hamburg", df_hamburg)
p4 = get_figure_histogram_price("Köln", df_koeln)
p5 = get_figure_histogram_price("Frankfurt", df_frankfurt)
p6 = get_figure_histogram_price("Dresden", df_dresden)
show(column(row(p1, p2), 
            row(p3, p4), 
            row(p5, p6)))

结果显然与之前看到的散点图有关:

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

可用价格的直方图,作者提供的图片

慕尼黑似乎是最昂贵的地方,分布的峰值约为 1500 欧元,并且正如之前提到的,柏林有两个峰值看起来很有趣。分布右偏,至少对于像柏林或慕尼黑这样的大城市,我们可以看到一个长尾,其中一些物业的价格甚至高于 10000 欧元/平方米。

至于平方米面积,我只展示柏林的结果;其他城市总体上看起来是一样的:

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

可用面积的直方图,作者提供的图片

大多数房屋和公寓的面积在 30 到 70 平方米之间,这看起来直观上是正确的,但正如我们所见,有些物业的面积甚至小于 10 平方米,有些则大于 250 平方米。

公用事业费用

接下来感兴趣的是公用事业费用是多少。如前所述,所有列出的公寓有两个价格:所谓的“暖”(包括供暖和电力等公用事业)和“冷”值。让我们计算差异并制作散点图:

name = "Berlin"
df_city = df_berlin

df_area = df_city[['property_area', 'price_warm_eur', 'price_cold_eur', 'property_type']].copy()
df_area['property_type'] = df_area['property_type'].fillna('Unbekannt')

pr_types = df_area[['property_type']].groupby(['property_type'], as_index=False).size().sort_values(by=["size"], ascending=True)['property_type']

df_area['price_diff'] = df_area['price_warm_eur'] - df_area['price_cold_eur']
df_area = df_area[df_area['price_diff'] > 0] 
values_x = df_area['property_area'].astype('float').values
values_y = df_area['price_diff'].astype('float').values

p = figure(width=1200, height=500, title=f"Utilities cost: {name}")

source = ColumnDataSource(dict(property_area=values_x, 
                               price_diff=values_y, 
                               property_type=df_area["property_type"]))

palette = (Viridis10 + Plasma10)[:len(pr_types)]

# Draw scatter
p.scatter("property_area", "price_diff", source=source,
          fill_alpha=0.8, size=4,
          legend_group='property_type',
          color=factor_cmap('property_type', palette, pr_types))
# Draw approximation with a linear model
model = LinearRegression().fit(values_x.reshape(-1, 1), values_y)
p.line([0, 9999], model.predict([[0], [9999]]), line_width=2, line_dash="2 2", alpha=0.5, color='red')

p.xaxis.axis_label = 'Area, m²'
p.yaxis.axis_label = '"Warm" - "Cold" price, EUR'
p.x_range.start = 0
p.x_range.end = 200
p.y_range.start = 0
p.y_range.end = 800
p.toolbar_location = None
p.legend.visible = True
p.legend.background_fill_alpha = 0.9
show(p)

结果很有趣:

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

公用事业费用与平方米面积的散点图,作者提供的图片

显然,结果存在很大差异:不同的房屋可能有不同类型的供暖、绝缘等。但一般来说,50 平方米的物业每月的公用事业费用约为 200 欧元,面积翻倍也会成比例地翻倍,我们的例子中,100 平方米的房子或公寓每月费用为 400 欧元。至于物业类型,图表上的点分布得较为均匀,我没有看到成本与不同类型之间的任何视觉关联。

押金

押金是租赁合同中的一个重要部分,因为其价值可能相当大。在德国,法律规定的最高押金为 3 个“冷”价格;让我们看看实际情况如何。

首先,让我们看看我们拥有的数据:

display(df_berlin[["title", "price_cold_eur", "deposit_eur"]])

结果如下:

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

我们可以看到这些值是不同的——一些房主使用数字形式的金额,如“585 €”,而其他人则使用文字描述,如“3 Nettokaltmieten”或“3 MM”。显示唯一值很简单:

print(df['deposit_eur'].unique().tolist())

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

在输出中,我们可以看到文本描述,如“Drei Nettokaltmieten”,“Zwei Monatsmiete”等。为了解析这些值,我创建了 2 种将文本字符串转换为数值的方法。也许这个检查没有涵盖所有可能性,但在大多数情况下,它能完成工作:

def value_from_str(price_str: str) -> Optional[float]:
    """ Convert string price like "7.935,60 EUR" to 7936 """
    try:
        # '4.800,00 EUR' => '4.800,00'
        if price_str.find(' ') > 0:
            price_str = price_str.split(" ")[0]
        s_filtered = ''.join(c for c in price_str if c in "0123456789,.")
        # "1000.0" => 1000.0
        if s_filtered.find(".") != -1 and s_filtered.find(",") == -1:
            return float(price_str)
        # 7.935,60 => 7935.60
        return float(s_filtered.replace(".", "").replace(",", "."))
    except ValueError as _:
        return None

def convert_price(s_deposit: str, cold_price: int) -> float:
    """ Convert text string and a price to a new value """
    try:
        if s_deposit is None:
            return None
        if isinstance(s_deposit, int) or isinstance(s_deposit, float):
            return s_deposit
        if '€' in s_deposit:
            # 585 € => 585
            return value_from_str(s_deposit)
        if 'zwei' in s_deposit.lower():
            return 2*cold_price
        if 'drei' in s_deposit.lower():
            return 3*cold_price
        if 'kalt' in s_deposit.lower() or 'km' in s_deposit.lower() or 'monat' in s_deposit.lower():
            # 2x Monatsnettokaltmieten => 2
            return cold_price*value_from_str(s_deposit)
        return value_from_str(s_deposit)
    except:
        return None

使用这些方法,我可以进行这样的转换:

convert_price("3 Nettokaltmieten", 659)
convert_price("1.274,00 EUR", 659)

最后,我们可以在数据集中创建一个押金与价格比率的列:

def get_deposit_ratio(s_deposit: str, cold_price: int) -> float:
    """ Calculate the ratio between deposit and price """
    try:
        deposit_price = convert_price(s_deposit, cold_price)
        if deposit_price is not None and cold_price != 0:
            return deposit_price/cold_price
    except:
        pass
    return None

df_berlin["deposit_price_ratio"] =  df_berlin.apply(lambda x: get_deposit_ratio(s_deposit=x['deposit_eur'], 
                                                                                cold_price=x['price_cold_eur']), 
                                                    axis=1)

使用这个新列,我们可以像以前一样轻松地制作直方图:

def get_deposit_histogram(name: str, df_city: pd.DataFrame):
    """ Get Bokeh figure from a dataframe """
    prices = df_city['deposit_price_ratio'].dropna().values
    hist_e, edges_e = np.histogram(prices, density=False, bins=60, range=(0, 5))

    # Draw
    palette = Viridis256[::2][0:len(hist_e)]  # Take every 2nd item from array
    p = figure(width=1400, height=400, 
               title=f"Property area: {name} ({df_city.shape[0]} total)")
    p.quad(top=hist_e, bottom=0, left=edges_e[:-1], right=edges_e[1:], fill_color=palette)
    p.x_range.start = 0
    p.x_range.end = 5
    p.y_range.start = 0
    p.y_range.end = 400
    p.xaxis[0].ticker.desired_num_ticks = 20
    p.xaxis.axis_label = 'Deposit amount, relative to the "cold" price'
    p.yaxis.axis_label = 'Number of objects available'
    p.toolbar_location = None
    return p

p1 = get_deposit_histogram("Berlin", df_berlin)
p2 = get_deposit_histogram("München", df_munchen)
p3 = get_deposit_histogram("Hamburg", df_hamburg)
p4 = get_deposit_histogram("Köln", df_koeln)
p5 = get_deposit_histogram("Frankfurt", df_frankfurt)
p6 = get_deposit_histogram("Dresden", df_dresden)
show(column(row(p1, p2), row(p3, p4), row(p5, p6)))

结果很有趣:

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

存款价值分布的直方图,作者提供的图片

许多房东要求最高可能的押金,这在德国法律限制为 3 个“冷”价格。尽管也可以找到要求 2 倍、1 倍甚至完全不需要押金的地方(德语中为“kautionsfrei”)。令人惊讶的是,有些房主要求的押金高于 3 倍的值。有时,当这个值为 3.05 时,可以通过计算错误来解释,但如果押金值约为 5 倍,那肯定不是这种情况。

物业发布者

一些房主喜欢自己出租他们的房产;其他人则与中介合作。这些金额有多大?让我们用饼图来展示分布。

def get_figure_publisher(name: str, df_city: pd.DataFrame) -> figure:
    publishers = df_city[['publisher']].groupby(['publisher'], as_index=False).size().sort_values(by=["size"], ascending=False)
    publishers = publishers[publishers['size'] > 5]

    # Put private first
    data_private = publishers[(publishers['publisher'] == "Private")]
    data_non_private = publishers[(publishers['publisher'] != "Private")]
    data = pd.concat([data_private, data_non_private])

    palette = RdGy3[:1] + Viridis11 + BrBG11 + Plasma11 + Cividis11 + RdYlBu11 + RdGy11 + PiYG11

    data['angle'] = data['size']/data['size'].sum()*2*np.pi
    data['percentage'] = data['size'] / data['size'].sum() * 100
    data['color'] = palette[:data.shape[0]]

    p = figure(width=1100, height=750, title=f"Property publishers: {name}", toolbar_location=None, x_range=(-0.5, 1.0))
    pie_chart = p.wedge(x=0, y=1, radius=0.4,
            start_angle=cumsum('angle', include_zero=True), end_angle=cumsum('angle'),
            line_color="white", 
            fill_color='color', 
            legend_field='publisher', 
            source=data)

    p.axis.axis_label = None
    p.axis.visible = False
    p.grid.grid_line_color = None
    return p

在这段代码中,我按发布者对数据框进行了分组,并按大小排序。唯一的技巧是将“私人”组放在首位,为了清晰起见,我还将该组标记为不同颜色。

2 个城市的结果,柏林和慕尼黑,看起来像这样:

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

显示每个发布者的房地产对象数量的饼图,作者提供的图片

有趣的是,只有 8.5%的柏林房地产由私人个人挂牌;在慕尼黑,这个比例是 27%。另一个有趣的点是,超过 50%的房产是由少数几家中介发布的。

楼层数量

正如我们之前看到的,公寓的位置,如底层或屋顶下的楼层,通常不会影响租金价格。但了解德国大多数建筑物的楼层数仍然很有趣。

最初,我没有预期到这个任务会有任何困难,但挑战在于进行自定义排序。许多公寓或房屋没有指定楼层号码,我想把“未知”值放在最左边。这可以通过在 Pandas 中实现自定义排序键来完成。这里的难点是,当进行 DataFrame 排序时,custom_key 是由 Pandas 应用于“pd.Series”对象而不是单个值。因此,我们需要第二种方法来更新系列中的值:

def to_digit(v):
    """ Convertion for string or digit """
    if v.isdigit():
        return f"{int(v):02d}" # "1" => "001"     
    return ' ' + str(v)

def custom_key(v):
    """ Custom key for the dataframe sort """
    # v: pd.Series, convert items for proper sort
    # ["1", "10", "2", "U"] => [" U", "001", "002", "010"]
    return v.apply(to_digit)

def get_figure_floors(city_name: str, df_city: pd.DataFrame) -> figure:
    """ Get Bokeh figure """
    floors = df_city[['floor']].astype(str).mask(df_city.isnull(), None).fillna("?").groupby(['floor'], as_index=False).size()
    floors = floors.sort_values(by=["floor"], key=lambda x: custom_key(x), ascending=True)

    # Draw
    palette = Viridis11 + Plasma11 + Cividis11
    values = floors['floor']
    amount = floors['size']
    p = figure(x_range=FactorRange(factors=values), width=1200, height=400, title=f"{city_name}: apartments floor")
    p.vbar(x=values, top=amount, width=0.8, color=palette[:len(values)])
    p.xaxis.axis_label = 'Floor №'
    p.yaxis.axis_label = "Amount"
    p.toolbar_location = None
    return p

p1 = get_figure_floors("Berlin", df_berlin)
p2 = get_figure_floors("München", df_munchen)
show(row(p1, p2))

前两个城市,柏林和慕尼黑的结果如下:

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

楼层分布直方图,作者提供的图片

正如我们所看到的,这两个城市的大多数公寓位于 1 到 5 层。但也有几套公寓位于 10 到 20 层,还有一套柏林的公寓位于 87 层(虽然我没有检查是否是打印错误,或该建筑是否真的存在)。

地理可视化

构建直方图或多或少是直接的;让我们进入有趣的部分:在地理地图上显示房产对象。在这里,我们面临两个挑战。首先,我们需要获取坐标,其次,我们需要绘制地图。

地理编码

让我们再检查一下数据:数据框中有“region”和“address”字段,我们可以用来进行地理编码请求:

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

获取坐标时,我将使用一个GeoPy库;它是免费的,不需要任何 API 密钥(例如 Google Maps API):

from geopy.geocoders import Nominatim
from functools import lru_cache

geolocator = Nominatim(user_agent="Python3.9")

@lru_cache(maxsize=None)
def get_coord_lat_lon(full_addr: str):
    """ Get coordinates for address """
    # Remove brackets: "Mitte (Ortsteil), 10117" => "Mitte, 10117"
    p1, p2 = full_addr.find('('), full_addr.find(')')
    if p1 != -1 and p2 != -1 and p2 > p1:
        full_addr = full_addr[:p1].strip() + full_addr[p2 + 1:]  
    # Make request
    pt = geolocator.geocode(full_addr)
    return (pt.latitude, pt.longitude) if pt else (None, None)

代码很简单;唯一的挑战是在地址中去掉“(”和“)”括号;结果发现,库在处理像“Nauen, Havelland (Kreis)”这样的地址时没有返回任何数据。我还使用了“lru_cache”以避免对相同地址进行多次请求(有些中介在同一栋楼里有几套待租公寓)。

使用这种方法,我可以轻松请求位置:

df_city = df_berli
df_addrs = df_city[["address", "region", "price_cold_eur"]].dropna().copy()
# Combine the address from 2 fields
df_addrs["address_full"] = df_addrs[["address", "region"]].apply(lambda x: x[0] + ", " + x[1], axis=1)

points = []
for index, row in df_addrs.iterrows():
    addr, price = row['address_full'], row['price_cold_eur']
    lat, lon = get_coord_lat_lon(addr)
    if (index % 50) == 0:
        print(f"{index} of {df_addrs.shape[0]}: {addr}, {price}, {lat}, {lon}")
    if lat and lon:
        points.append((addr, price, lat, lon)) 

print(f"Points added: {len(points)}")
return points

地图

为了绘制地图,我将使用一个免费的Folium库。作为使用该库的简单示例,可以通过几行代码显示带标记的地图:

import folium
from folium.plugins import HeatMap
from branca.element import Figure

fig = Figure(width=1200, height=1000)
m = folium.Map(location=(50.59, 10.38), tiles="openstreetmap", zoom_start=7)

folium.Marker(location=(52.59, 13.37), popup="Berlin").add_to(m)

fig.add_child(m)
display(fig)

这段代码将生成一个美观的互动地图,无需 API 密钥:

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

Folium 地图和标记示例,作者提供的图片

但我们的可视化会复杂一些。我将使用 Folium 的“Circle”对象来表示每个房产,并使用“FeatureGroup”来对不同价格进行分组:

import folium
from folium.plugins import HeatMap
from branca.element import Figure
import matplotlib
import matplotlib.cm as colormap

def value_to_color(value: int) -> str:
    """ Convert price value to the HTML color """
    if value >= 5000:
        # Mark high values in special colors
        return "#FF00FF"

    norm = matplotlib.colors.Normalize(vmin=0, vmax=3000, clip=True)
    mapper = colormap.ScalarMappable(norm=norm, cmap=colormap.inferno)
    r, g, b, _ = mapper.to_rgba(value, alpha=None, bytes=True)
    return "#" + f"{(r << 16) + (g << 8) + b:#08x}"[2:]

def add_to_map(fmap, lat, lon, price):
    """ Add point to map """
    color_str = value_to_color(price)
    folium.Circle(
        location=[lat, lon],
        radius=100,
        popup=addr + ": " + str(price),
        color=color_str,
        fill=True,
        fill_color=color_str
    ).add_to(fmap)

def get_html_text_label(text: str, value1: int, value2: int=999999):
    """ Prepare HTML label with a gradient text """
    color1 = value_to_color(value1)
    color2 = value_to_color(value2-1)
    return f'<span style="background: linear-gradient(to right, {color1}, {color2}); padding-left: 2%; padding-top: 3%; padding-bottom: 1%;">&nbsp;&nbsp;&nbsp;&nbsp;</span><span>&nbsp;{text}</span>'

def generate_map(points: list, location: Tuple, name: str):
    """ Draw a city map """
    fig = Figure(width=1200, height=600)
    m = folium.Map(location=location, zoom_start=12)

    heat_map = folium.FeatureGroup(name='Heat Map')
    price_less_500 = folium.FeatureGroup(name=get_html_text_label('< 500', 0, 500))
    price_less_1000 = folium.FeatureGroup(name=get_html_text_label('500..1000', 500, 1000))
    price_less_2000 = folium.FeatureGroup(name=get_html_text_label('1000..2000', 1000, 2000))
    price_less_5000 = folium.FeatureGroup(name=get_html_text_label('2000..5000', 2000, 5000))
    price_more_5000 = folium.FeatureGroup(name=get_html_text_label('> 5000', 5000))

    heat_data = []
    for addr, price, lat, lon in points:
        if price < 500:
            add_to_map(price_less_500, lat, lon, price)
        elif price <= 1000:
            add_to_map(price_less_1000, lat, lon, price)
        elif price <= 2000:
            add_to_map(price_less_2000, lat, lon, price)
        elif price <= 5000:
            add_to_map(price_less_5000, lat, lon, price)
        else:
            add_to_map(price_more_5000, lat, lon, price)
        heat_data.append((lat, lon))

    heat_map.add_child(HeatMap(heat_data, min_opacity=0.3, blur=50))
    m.add_child(heat_map)
    m.add_child(price_less_500)
    m.add_child(price_less_1000)
    m.add_child(price_less_2000)
    m.add_child(price_less_5000)
    m.add_child(price_more_5000)

    folium.map.LayerControl('topright', collapsed=False, style=("background-color: grey; color: white;")).add_to(m)

    fig.add_child(m)
    return fig

我还使用了热图作为背景,以使结果看起来更好。可视化还需要调整颜色和 CSS 样式以显示渐变;最终结果如下:

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

柏林的房产对象,图片由作者提供

结果或多或少是直观的。在柏林,中心周围的区域更贵,但没有特别的“高档”地方。例如,价格高于€5,000/m 的房产大致上是均匀分布的:

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

柏林价格≥€5,000/m 的房产,图片由作者提供

租金动态

这个问题有点挑战性。租赁过程的速度如何?房产对象可租赁的时间是多长?这个问题具有挑战性,因为网站上没有房产的发布日期。但我们可以通过比较不同日期获得的结果间接估计这些数据。

这个想法很简单。每个房产都有一个唯一的 ID。我保存了同一城市的数据两次,间隔 7 天。然后我显示了两个价格直方图:一个是所有房产的,另一个是那些在 7 天内被移除的房产(存在于第一个数据框中但在第二个数据框中不存在):

def get_two_histograms(name: str, df_all: pd.DataFrame, df_closed: pd.DataFrame):
    """ Draw two dataframe histograms on the same graph """
    price_limit = 10000
    prices = df_all['price_cold_eur'].values
    hist_e1, edges_e1 = np.histogram(prices, density=False, bins=40, range=(0, price_limit))
    prices = df_closed[(df_closed['price_cold_eur'] < price_limit)]['price_cold_eur'].values
    hist_e2, _ = np.histogram(prices, density=False, bins=edges_e1)

    # Draw
    palette1 = Viridis256[::3][0:len(hist_e1)]  # Take every 3rd item from array
    p = figure(width=1400, height=500, 
               title=f"Property prices: {name} ({df_city.shape[0]} total)")
    p.quad(top=hist_e1, bottom=0, left=edges_e1[:-1], right=edges_e1[1:], fill_color=palette1, fill_alpha=0.8, legend_label='All properties')
    p.quad(top=hist_e2, bottom=0, left=edges_e1[:-1]+20, right=edges_e1[1:]-20, fill_color=palette1, legend_label='Properties removed from listing within 7 days')
    # Add percentage labels
    for i in range(hist_e1.shape[0]):
        pos_x = (edges_e1[i] + edges_e1[i + 1])/2
        pos_y = hist_e1[i]
        value = 100*hist_e2[i]/hist_e1[i] if hist_e1[i] != 0 else 0        
        value_str = f'{value:.1f}%' if value > 0.5 else "0%"
        p.add_layout(Label(x=pos_x, y=pos_y + 2, text_align="center",
                 text=value_str, text_font_size='7pt',
                 background_fill_color='white', background_fill_alpha=0.0))

    p.x_range.start = 0
    p.x_range.end = price_limit
    p.y_range.start = 0
    p.y_range.end = 480
    p.xaxis[0].ticker.desired_num_ticks = 20
    p.left[0].formatter.use_scientific = False
    p.below[0].formatter.use_scientific = False
    p.xaxis.axis_label = "Rent Price, EUR"
    p.yaxis.axis_label = "Amount"
    return p

df_berlin0 = pd.read_csv("Berlin_00.csv", sep=';', na_values=["None"]).drop_duplicates(subset='property_id', keep="first")
ids0 = df_berlin0["property_id"].unique().tolist()
df_berlin6 = pd.read_csv("Berlin_06.csv", sep=';', na_values=["None"]).drop_duplicates(subset='property_id', keep="first")
ids6 = df_berlin6["property_id"].unique().tolist()

df_f = df_berlin0.copy()
df_f["deal_closed"] = df_f['property_id'].apply(lambda pr_id: pr_id in ids0 and pr_id not in ids6)
df_f_closed = df_f[df_f['deal_closed'] == True]

p1 = get_two_histograms("Berlin", df_berlin0, df_f_closed)
show(p1)

我还在条形图上添加了百分比标签,使条形图更易读。结果如下:

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

从列表中移除的房产直方图,图片由作者提供

显然,这个结果在统计上并不显著,因为实验仅进行了一次。至少,我可以说在进行这个测试时,柏林约 20%的€800–1,200 范围内的房产在一周内被从列表中移除。更贵的房产显然留存时间更长;在同一时期内,只有约 9%的€3,000 价格范围内的房产被移除。

异常检测

接下来的步骤,让我们来点乐趣,尝试找出“异常”情况,即一些不寻常和非标准的情况。为此,我将使用Isolation Forest算法,该算法在Scikit-Learn Python 库中实现。为了寻找异常,我将使用 3 个特征:面积、价格和房间数量:

from sklearn.ensemble import IsolationForest

df_city = df_berlin[["region", "address", "price_cold_eur", "num_rooms", "property_area"]].dropna().copy().reset_index(drop=True)
anomaly_inputs = ["price_cold_eur", "num_rooms", "property_area"]
for field in anomaly_inputs:
    df_city[field] = df_city[field].astype(float)

display(df_city[anomaly_inputs])

model_if = IsolationForest(contamination=0.01)
model_if.fit(df_city[anomaly_inputs])

# anomaly_scores: generated by calling model_IF.predict() and is used to identify if a point is an outlier (-1) or an inlier (1)
df_city['anomaly_scores'] = model_if.decision_function(df_city[anomaly_inputs])
# anomaly : generated by calling model_IF.predict() and is used to identify if a point is an outlier (-1) or an inlier (1)
df_city['anomaly'] = model_if.predict(df_city[anomaly_inputs])

正如我们在代码中看到的,算法只需要一个参数,即所谓的“污染”,它决定了数据集中异常值的比例。在这里我将其设置为 1%;显然,参数可以根据需要进行调整。

调用“fit”方法后,我们可以得到结果。“decision_function”方法返回异常分数,“predict”方法返回+1(如果特定对象被视为正常点)或-1(如果是异常点)。让我们只显示异常点:

display(df_city[df_city['anomaly'] == -1])

结果如下所示:

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

对于这些属性,有些参数不寻常,通过观察结果,我可以猜测房间数量、面积或价格较大。但是,“隔离森林”方法的一个优点是其可解释性。一个SHAP Python 包允许使用Shapley 值以图形化的方式解释结果:

import shap
shap.initjs()

explainer = shap.Explainer(model_if.predict, df_city[anomaly_inputs])
shap_values = explainer(df_city[anomaly_inputs])

分析本身大约需要一分钟。之后,我们可以得到列表中每一项的结果。例如,让我们检查编号为 3030 的房产:

shap.plots.waterfall(shap_values[3030])

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

Shapley 解释器结果,图像由作者提供

我们可以看到,价格是合适的,但 211 平方米的房产面积和 5 间房的数量被算法视为不寻常。

还可以通过显示散点图来查看算法的工作情况。例如,让我们看看“房间数量”和“价格”如何影响 Shapley 值:

display(shap.plots.scatter(shap_values[:,'num_rooms'], color=shap_values[:,"price_cold_eur"]))

结果如下所示:

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

SHAP 值的散点图,图像由作者提供

在这里,我们可以看到房间数量超过 4 的情况对得分的影响最大。

词云

我不会将最后一步视为真正的分析,但为了趣味,我们来构建词云,看看在房地产标题中哪些词最为流行。通过 Python WordCloud 库,我们只需几行代码即可做到这一点:

from wordcloud import WordCloud
import matplotlib.pyplot as plt

from nltk.corpus import stopwords
stop_words_de = stopwords.words('german')

# Generate
text = ""
for s in df.title:
    s_out = s.replace('/', ' ').replace(':', ' ').replace(',', ' ').replace('!', ' ').replace('-', ' ')
    text += s_out + " "

wordcloud = WordCloud(width=1600, height=1200, stopwords=set(stopwords_de), collocations=False, background_color="white").generate(text)

# Show
plt.figure(figsize=(16, 12), dpi=100)
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.show()

结果如下所示:

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

房地产对象标题的词云,图像由作者提供

词如“wohnung”(公寓)、“zimmer”(房间)和“balkon”(阳台)最为流行,我们还可以看到“helle”(明亮)、“moderne”(现代)、“schöne”(美丽)等词。

结论

正如英国国家统计局前研究员艾伦·史密斯在 2017 年的TED 演讲中所说,我们应该热爱统计学,因为它是关于我们的科学。探索数据集,如这个租房数据,并发现有趣的模式确实很有趣。我希望这对读者也很有趣,不仅作为使用 Pandas 或 Bokeh 的例子,而且作为对另一个国家的生活和文化的一个小见解。显然,我没有测试所有的数据;例如,了解多少房东允许租户在公寓里养宠物可能会很有趣。读者可以自行测试;本文中的代码片段应该足够了。

如果你喜欢这个故事,欢迎订阅Medium,你将会收到我新文章发布的通知,并且可以全面访问其他作者的数千篇故事。

感谢阅读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值