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

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

用简单的,甚至非线性的时间序列模型获胜

原文:towardsdatascience.com/winning-with-simple-not-even-linear-time-series-models-6ece77be22bc

如果你的数据集较小,接下来的想法可能会很有用

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

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

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

照片由 Thomas Bormans 拍摄,来源于 Unsplash

免责声明: 标题深受 这个 精彩讲座的启发。

正如名字所示,今天我们想要考虑几乎简单到微不足道的模型。尽管目前的趋势是趋向复杂模型,即使是时间序列模型,我仍然非常相信简单性。特别是当你的数据集较小时,接下来的想法可能会很有用。

公平地说,这篇文章可能对刚开始进行时间序列分析的人最有价值。其他人应首先查看目录,自己决定是否继续阅读。

就个人而言,我对即使是最简单的时间序列模型能推到多远仍然感到非常好奇。接下来的段落展示了一些我在这个话题上逐渐积累的想法和见解。

纯 i.i.d.噪声的模型

我们从最简单的(概率性)方式开始建模(单变量)时间序列。也就是说,我们希望查看纯粹的i独立的,i同分布的,d分布随机性:

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

(图像作者提供)

这意味着我们的所有观测在任何时间点都遵循相同的分布(同分布)。更重要的是,我们假设观测之间完全没有相互关系(独立分布)。显然,这排除了任何自回归项。

你可能首先会问这样模型是否过于简单以至于无法用于现实问题。当然,大多数时间序列不太可能与自身过去没有统计关系。

尽管这些担忧完全正确,但我们仍然可以推导出以下内容:

任何比纯噪声模型更复杂的时间序列模型也应该能提供比纯噪声模型更好的预测。

简而言之,我们至少可以使用随机噪声作为基准模型。可以说,没有比这更简单的方法来创建基准基准了。即使平滑技术可能也需要更多的参数进行拟合。

除了这个相当明显的用例之外,i.i.d. 噪声还有另一个潜在的应用。由于其简单性,噪声模型对非常小的数据集也可能有用。考虑一下:如果大型复杂模型需要大量数据以防止过拟合,那么简单模型只需少量数据。

当然,什么数据集大小可以被视为“小型”是有争议的。

积分 i.i.d.噪声

现在,事情变得更有趣了。虽然原始 i.i.d.噪声无法解释观察值之间的自相关,但积分噪声可以。在我们进行演示之前,让我们介绍一下differencing operator

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

(image by author)

如果你还没听说过时间序列问题中的差分——太好了!如果你听说过,那么希望你仍然能学到一些新东西。

积分时间序列的定义

有了我们的差分算子,我们现在可以定义一个积分时间序列

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

(image by author)

这个定义中有几个我们应该进一步澄清的想法:

首先,你可能注意到差分算子的指数概念。你可以简单地把它看作是多次进行微分。对于平方差分算子,这看起来如下:

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

(image by author)

正如我们将看到的,多重差分算子使我们可以一次处理不同的时间序列模式。

第三,通常的约定是简单地写

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

(image by author)

我们会很乐意在这里采用这一约定。此外,我们称这样的时间序列为简单积分,而不参考其顺序或季节性。

显然,我们还需要将差分表示重新转换回其原始领域。在我们的记号中,这意味着我们反转差分变换,即

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

(image by author)

必须适用于任意差分变换。如果我们展开这个公式,我们得到

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

(image by author)

这些简化源于差分运算符是线性运算符的事实(我们在这里不会详细讨论)。从技术上讲,最后的方程仅仅表示下一个观测值是当前观测值加上一个增量。

在一个预测问题中,我们通常会有一个变化的预测。

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

(图片作者提供)

让我们将这个预测表示为

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

(图片作者提供)

强调这不是实际变化,而是预测的变化。因此,积分时间序列的预测为

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

(图片作者提供)

之后,我们将这种逻辑递归应用到预测应该延续的未来:

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

(图片作者提供)

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

白噪声时间序列(左)及其对应的积分时间序列(右)。这两个时间序列通过简单的差分运算符及其逆相关联。(图片作者提供)

对于看似复杂的模式,积分噪声

到现在,你可能已经能想象什么是积分噪声模型。实际上,我们可以通过将一些差分运算符与随机噪声链式组合来提出无数种积分噪声模型变体。

来自积分时间序列的线性趋势: 一种可能性是简单的积分时间序列,即

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

(图片作者提供)

使用普通标准正态分布模拟这种模型的数据是一个有趣的练习。

结果是,这个时间序列的样本似乎展示了具有潜在变化点的线性趋势。然而,这些趋势和变化点显然完全是随机的。

这意味着简单地将分段线性函数拟合用于预测这些趋势可能是一种危险的方法。毕竟,如果变化是随机发生的,那么所有线性趋势线只是随机数据生成过程的伪影。

作为一个重要的免责声明,虽然‘不可预测’意味着从时间序列本身不可预测,但一个外部特征可能仍然能够准确预测潜在的变化点。然而,在这里,我们假设时间序列是我们唯一可用的信息来源。

下面,你可以看到上述现象的一个示例。虽然在 t=50 左右似乎有一个趋势变化,但这个变化完全是随机的。t=50 之后的上升趋势在 t=60 左右也停滞了。想象一下,如果你在 t=60 之后对上升趋势进行外推,你的模型会如何表现。

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

通过对标准正态噪声进行积分生成具有变化线性趋势的时间序列。(图片作者提供)

当然,有句谚语说‘永远不要说永远’,即使在这些情况下也是如此。然而,如果你应用这样的模型,你真的应该知道自己在做什么。

季节性模式: 类似于简单集成产生趋势的方式,我们也可以创建季节性模式:

正式来说,我们现在需要季节性过程的第 s 阶差分是一个平稳过程,例如:

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

(图片作者提供)

逆操作——将 i.i.d.过程转换回季节集成——与之前的操作类似:

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

(图片作者提供)

你可以把季节差分的逆操作看作是在s个周期内的cumsum操作。由于我不知道相应的本地 Python 函数,我决定通过reshape->cumsum->reshape来获得所需的结果。以下是s=4的示例:

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

纯 i.i.d.过程的4阶季节性。(图片作者提供)

如你所见,生成的时间序列看起来相当真实。我们可以轻松地将其作为某产品的季度销售数字出售给一位不知情的数据科学家。

我们甚至可以结合这两种类型的集成,以生成具有趋势行为的季节性时间序列:

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

通过对白噪声进行集成,可以显现出季节性和趋势模式。(图片作者提供)

此时,你可能会意识到这篇文章的标题有些点击诱饵。事实上,集成时间序列完全是线性模型。然而,我相信大多数人不会认为一个参数更多或更少的模型是典型的线性模型。

集成中的记忆效应: 集成时间序列的另一个有趣特性是能够建模记忆效应。

当数据中出现较大冲击或异常值时,这种效果尤为明显。考虑下面的示例,它显示了从标准Cauchy 分布中进行 i.i.d.抽样时的季节集成,顺序为s=12

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

i.i.d.标准 Cauchy 序列(左)及其对应的季节性集成时间序列。(图片作者提供)

i.i.d. Cauchy 序列中大约 t=20 的第一次大冲击在右侧整个集成序列中得以维持。随着时间的推移,更多的冲击发生,这些冲击也得以维持。

这个记忆属性在实践中非常有用。例如,疫情带来的经济冲击导致了许多时间序列中的持续变化。

与 NBEATS 和 NHITS 的基准比较

现在让我们使用AirPassengers数据集,来自 Nixtla 的neuralforecast,对上述想法进行快速评估。如果你经常阅读我的文章,你可能还记得这篇文章中的一般程序。

首先,我们将数据分为训练期和测试期,后者包含 36 个月的数据:

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

航空乘客数据集——训练和测试拆分。(作者提供的图像)

为了获得一个平稳的 i.i.d.序列,我们执行以下变换:

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

(作者提供的图像)

首先,平方根稳定了不断增加的方差。然后,两次差分操作去除了季节性和趋势。有关相应的反变换,请查看下方代码。

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

平稳性变换后的时间序列(训练集)。(作者提供的图像)

我们还可以检查稳定时间序列的直方图和密度图:

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

平稳时间序列的核密度和直方图。(作者提供的图像)

我们的平稳序列看起来也有些接近正态分布,这总是一个很好的特性。

现在,让我们为测试期创建预测。假设我们不知道 i.i.d.序列的确切分布,我们仅仅通过训练数据从经验分布中抽样。因此,我们通过重新整合来自经验数据的随机样本来模拟未来值:

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

训练集(蓝色)、测试集(红色)、均值预测(绿色线)和 90%置信区间(绿色区域)。(作者提供的图像)

这看起来非常好——均值预测非常接近测试数据。此外,我们的模拟允许我们经验性地抽样整个预测分布。因此,我们还可以轻松地添加置信区间。

最后,让我们看看我们的方法与复杂时间序列模型的比较。为此,我使用了 Nixtla 的NBEATSNHITS的实现:

以下是测试集的均方根误差(RMSE):

  • 简单模型: 25.5021

  • NBEATS: 42.6277

  • NHITS: 62.6822

正如我们所见,我们几乎简单的模型已经比两个复杂的时间序列模型超出不少。当然,我们需要强调,这并不允许得出任何普遍性的结论。

相反,我期望神经模型在较大的数据集上超越我们简单的方法。然而,作为基准,这些简单模型总是值得考虑的。

结论——我们从中得到什么?

正如本文多次指出的:

看似复杂的时间序列仍然可能遵循相当简单的数据生成过程。

最终,你可能会花费数小时尝试拟合一个过于复杂的模型,即使基础问题几乎是微不足道的。某些时候,可能有人会出现,拟合一个简单的 ARIMA(1,0,0),并且仍然超越你的复杂神经模型。

为了避免上述最坏的情况,考虑以下想法:

在开始解决新的时间序列问题时,总是从最简单的模型开始,并将其作为所有其他模型的基准。

尽管这是数据科学领域的常识,但我觉得在这种情况下仍然值得特别强调。尤其是由于如今(在某种程度上是有道理的)围绕深度学习的炒作,直接从一些花哨的东西开始可能会很诱人。

对于许多问题,这可能正是正确的解决方案。今天没有人会在大规模语言模型嵌入几乎是免费的情况下考虑使用隐马尔可夫模型来进行自然语言处理。

然而,一旦你的时间序列变得庞大,现代机器学习可能会更好。特别是,梯度提升树 在这种大规模问题中非常受欢迎。

更具争议的方法是,你猜对了,时间序列的深度学习。虽然有些人认为这些模型在这里效果不好,但它们在像亚马逊这样的科技公司的流行程度可能说明了一切。

参考文献

[1] Hamilton, James Douglas. 时间序列分析. 普林斯顿大学出版社, 2020。

[2] Hyndman, Rob J., & Athanasopoulos, George. 预测:原理与实践. OTexts, 2018。

最初发布于 https://www.sarem-seitz.com 于 2023 年 5 月 10 日。

无需多言:自动化开发环境和构建

原文:towardsdatascience.com/without-further-ado-automate-dev-environments-and-build-f2f9bcaaae1e

通过环境和构建自动化使你的软件易于使用,给你的开发同事带来快乐。包括 Python 和 Hatch 中的代码示例。

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

·发表于 Towards Data Science ·11 分钟阅读·2023 年 2 月 24 日

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

图片来源:Carol JengUnsplash

大多数开发人员讨厌遗留软件,为什么?在我们的行业中,“遗留”意味着一个已经服务了多年的代码库,通常原始开发者不再在公司,而没有人能够真正维护它。

遗留软件配方中的一些重要成分是:缺乏文档、难以理解的代码、尝试修改的困难,以及构建软件以进行发布的严重困难。

然而,遗留软件之所以被称为遗留,是因为公司依赖它,这也是为什么尽管大多数开发人员不知道如何处理它,它仍然在使用。我们从过去的同事那里继承了它,因此我们有责任让它在未来继续像过去一样工作。

然而,有一些软件满足上述遗留软件的要求,但却没有服务多年的优点。部分代码也相当近期。这意味着,人们编写的代码已经根据其负面定义成为“遗留”代码。

不愉快的一天

今天是星期一,你被分配了一个新项目。在早上 9 点的会议上,你了解到你现在在Petty工作,这是一个用于搜索宠物图片的新公司产品。你的第一个任务是开发Petognizer,一个识别图片中宠物的组件。它目前在生产中,但只能识别猫和狗。现在管理层希望它也能识别仓鼠和兔子。

你得到了公司 GitLab 账户中的仓库链接,并准备使用它。

结果是你不能。

这个仓库没有 README,也没有叫做docs或类似的文件夹。看来你需要自己进行一些探索。

现在是 10:30,你开始挖掘仓库。经过 30 分钟你明白了在哪里可以找到训练代码、数据预处理脚本和推理代码。这看起来过于复杂,但现在不是进入细节的时候,你想对它有一个整体的了解。

你还发现了一个可能用于生产的 Dockerfile,但这个并没有明确写在任何地方,还有一个requirements.txt,这当然是一个 Python 项目,因为它涉及到机器学习。

现在是 11:30,你感觉准备好了要尝试一下。你知道新的数据集在哪里,所以现在你想开始数据准备流程。于是,你想安装包,却发现缺少setup.pypyproject.toml。你问你的同事是否这个包在公司私有的pypi服务器上,但得到的回答是否定的。毕竟,如果没有构建说明,它怎么可能被打包?

显然,你不得不走“脏活”的路。你下载了仓库,找到了数据准备的入口点,这不在__main__.py文件中,将项目的根文件夹添加到你的PYTHONPATH环境变量中。然后,你创建一个新的 python 环境,使用venv,运行pip install -r requirements.txt来安装所有必要的依赖,运行脚本,然后……瞧!

ImportError: module xxx not found

你以为requirements.txt会提供开发依赖,但显然不是。但现在你看了看手表,发现已经一点钟了,于是决定和同事一起去吃午饭。你会在之后搞清楚。

当你从午休回来时,你再次充满活力,准备再次攻克这个问题。

现在你的工作是一个一个地安装缺失的依赖,每次安装一个库后你重新运行它,等待一会儿,接着下一个 ImportError 就会出现。你在 3 点钟前安装完所有东西,仍然在想requirements.txt到底是用来干什么的。

你已经在这个过程中几个小时了,你仍然不确定是否真的可以运行这个软件来满足你的需求。而你到目前为止的工作感觉完全浪费了。

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

图片来源:Anthony IntraversatoUnsplash

在另一个平行世界中,你简单地运行了

pip install petognizer

或者,也许,如果训练依赖作为额外的提供

pip install petognizer[train]

然后在 README 中找到了数据准备的命令,你在大约 10 分钟内准备好,一切都在午休前完成。

如果有一个pyproject.toml文件,将会大大简化包的安装(如果它在 pypi 服务器上会更容易)以及准备构建环境的过程。此外,一些适当的文档将使得了解如何实际使用代码进行不同任务变得更加简单。

每天都有如此多才华横溢的开发人员浪费时间,只因为某些代码维护者没有花时间设置文档和自动化,以提供良好开发体验的基础。

现在你负责这个代码库,你可以选择保持原样,把同样的挫败感留给下一个将要使用它的开发人员。或者,你可以决定为组织带来一些欢乐,使得代码库更容易使用。

虽然编写文档是一个需要长期投入的过程,需要对代码有深入理解(但一些正确的文档总比没有文档要好),自动化构建和开发环境的创建可以解放开发人员免于很多麻烦,使他们能更快地在代码库中提高生产力。

## 阅读和编写 ML 研究论文的技巧

通过多次的同行评审获得的经验教训

towardsdatascience.com

Hatch

Hatch是一个由 Python 打包机构(PyPA)支持的自动化工具,它能够以高度的简便性创建环境和构建软件。

它使用一个名为pyproject.toml的文件,这是“新”的统一 Python 项目设置文件。之所以称之为统一,是因为它被 Hatch 和其他类似工具(如Poetry)使用,并且它可以指定其他工具的配置,例如blackflake8等。

我们可以使用 Hatch 创建一个新项目,命令为

hatch new "My Project"

它将创建一个标准的项目结构,如

my-project
├── src
│   └── my_project
│       ├── __about__.py
│       └── __init__.py
├── tests
│   └── __init__.py
├── LICENSE.txt
├── README.md
└── pyproject.toml

它包含两个独立的包:一个用于源代码,另一个用于测试,还有一个包含一些默认文本的 README.md,一个 LICENSE.txt,一个名为__about__.txt的文件,里面包含软件版本,以及pyproject.toml

Hatch 也可以通过简单运行来用于现有项目

hatch new --init

在其根文件夹中。这将生成一个默认的pyproject.toml文件,需要进行修改。

可以通过pipx install hatch来安装 Hatch,使其作为用户命令可用,而不会“污染”现有的默认 Python 环境。

## 带有注册表的 Python 多态性 | Python 模式

学习一种模式来隔离包,同时扩展 Python 代码的功能。

towardsdatascience.com

pyproject.toml 的快速浏览

Hatch 文档非常详细,尽管有时清晰度可以提高,因此在这里我们将专注于与本文主题相关的部分和一些必要的背景信息。

pyproject.toml 带有一个初始部分,描述性很强。在这里我们可以找到项目的名称、描述、作者、支持的 Python 版本和版本。

以我为说明制作的这个玩具库为例 github.com/mattiadg/demo_it-analyze/blob/main/pyproject.toml (感谢我的朋友 Mario 帮助制作这个小应用的前端)

[project]
name = "demo-it-analyze"
description = ''
readme = "README.md"
requires-python = ">=3.7"
license = "MIT"
keywords = []
authors = [
  { name = "Mattia Di Gangi", email = "mattiadg@users.noreply.github.com" },
]
classifiers = [
  "Development Status :: 4 - Beta",
  "Programming Language :: Python",
  "Programming Language :: Python :: 3.7",
  "Programming Language :: Python :: 3.8",
  "Programming Language :: Python :: 3.9",
  "Programming Language :: Python :: 3.10",
  "Programming Language :: Python :: 3.11",
  "Programming Language :: Python :: Implementation :: CPython",
  "Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = [
  "flask",
  "pydantic",
  "spacy",
]
dynamic = ["version"]

[project.urls]
Documentation = "https://github.com/unknown/demo-it-analyze#readme"
Issues = "https://github.com/unknown/demo-it-analyze/issues"
Source = "https://github.com/unknown/demo-it-analyze"

应该很清楚,除了两个字段。dynamic = ["version"] 表示软件版本需要动态计算,稍后文件中提到它必须在 __about.py__ 文件中找到。另一方面,字段 dependencies 列出了的依赖关系。这些是与软件一起安装的包。当打包时,我这里只列出了名称,但也可以应用一些版本约束,如 ==<= 等… 它们与pip使用的相同。

请注意,这些依赖项是在 [project] 标签下指定的,但还有其他仅为某些环境指定的依赖项。然而,这些是包和所有环境共同的。代码库没有它们就无法工作。

工具配置

如前所述,pyproject.toml 可用于指定项目中使用的工具的配置,而 Hatch 是可以在这里配置的工具之一。

[tool.hatch.version]
path = "src/__about__.py"

[tool.hatch.envs.default.env-vars]
  FLASK_APP = "demo_it_analyze/app.py"

[tool.hatch.envs.default]
extra-dependencies = [
  "pytest",
  "pytest-cov",
  "mypy",
]

[tool.hatch.envs.default.scripts]
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=demo_it_analyze --cov=tests {args}"
no-cov = "cov --no-cov {args}"
download_ita = "python -m spacy download it_core_news_sm"
serve = "flask --app src.app run"

[tool.hatch.envs.train]
template = "default"
extra-dependencies = [
  "spacy[cuda-autodetect,transformers,lookups]"
]

[tool.hatch.envs.test]
template = "default"

[[tool.hatch.envs.test.matrix]]
python = ["37", "38", "39", "310", "311"]

请注意,这里的字段以 tool.hatch 开头,而不是 project

我们首先告诉 Hatch 到哪里去寻找软件版本。然后,我们为默认 Python 环境指定 FLASK_APP。Hatch 允许我们创建多个环境,默认环境是始终定义的,并且所有其他环境都从它派生。我们可以为环境定义依赖项,这些是开发依赖项,因为环境在包代码中不存在。

使用 Hatch,我们可以通过执行来创建默认环境

hatch env create 

它将创建具有所有依赖项的默认环境。我们还可以通过运行指定在 pyproject.toml 中的另一个环境(例如示例中也定义了 test)来创建另一个环境

hatch env create test

然而,Hatch 的一个好处是我们不需要显式创建环境。命令 hatch run 允许我们在环境中运行任何命令或脚本。在上面复制的文件中,我们有这个脚本定义:

[tool.hatch.envs.default.scripts]
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=demo_it_analyze --cov=tests {args}"
no-cov = "cov --no-cov {args}"
download_ita = "python -m spacy download it_core_news_sm"
serve = "flask --app src.app run"

因此,我们可以运行,例如

hatch run serve

hatch 会创建默认环境(如果不存在的话),同步其依赖pyproject.toml 中指定的依赖,并在环境中运行 flask --app src.app run,启动我们的 flask 服务器。我们还可以指定在特定环境中运行命令,例如 test

hatch run test:serve

注意,我们不需要安装任何东西。Hatch 会读取 pyproject.toml 并识别可以运行的脚本。

你看到我们在这里做了什么吗?项目维护者在一个文件中指定了一些配置选项,然后对任何使用该仓库的人来说,创建完全相同的环境变得非常简单。不再为重现开发环境而苦恼!

## 使用 inotifywait 进行数据处理自动化

如何在拥有一个生产就绪的 MLOps 平台之前进行自动化

[towardsdatascience.com

构建

Hatch 也是一个不错的构建工具。它有自己的构建后端,称为hatchling,而 pyproject.toml 附带了一些构建配置。根据示例:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.sdist]
exclude = [
  "/.github",
  "/docs",
]

[tool.hatch.build.targets.wheel]
packages = ["src"]

我们必须指定构建后端,因为 Hatch 可以作为所有功能的良好前端,然后用其他工具进行构建。然后我们看到可以为 sdistwheel 两种构建格式指定一些选项。然而,对于像这样的玩具项目,里面的内容不多。

然后,我们可以简单地运行 hatch build,它会构建两个目标。或者我们可以用 hatch build -t wheel 来指定其中一个目标,例如。

当你拥有 pypi 服务器的凭据时,无论是官方的、测试服务器的还是私有服务器的,你都可以通过简单地使用 hatch publish 从构建中发布你的包。

当然,发布包是一项需要谨慎处理的工作,在确保软件正常工作后进行。但随后,hatch 处理所有细节,我们只需要运行两个简单的命令。

如果你启用了类似Github Actions的 CI/CD 流水线,你也可以在那里指定这些命令,以实现更多的自动化!

我们没有覆盖的内容

使代码库易于使用可能是一个具有挑战性的问题,虽然构建自动化是朝着正确方向迈出的有用一步,但这远未满足所有需求。

我们在介绍故事中已经提到过文档,我们将不再详细讨论它。

另一个重要方面是彻底的测试套件,它有三个主要目的。

首先,它有助于验证代码是否正确。任何人都可以阅读测试并检查它们是否证明了正确的内容。如果测试通过,那么我们对代码的正确性有了一些保障。

其次,它们作为防止回归的安全网。当我们修改一个代码库时,特别是一个我们不太熟悉的代码库,全面的测试套件可以捕捉到修改是否破坏了预期的行为。

第三,如果文档不够详尽,测试可以展示我们难以理解的函数或类的使用方法,因为测试本质上就是代码示例。

另一个重要的遗漏部分是持续集成/持续交付管道(CI/CD),它在每次代码添加到远程仓库时运行一些检查,还可以构建和部署软件。CI/CD 管道展示了代码库中预期的代码质量,因为它可以运行诸如代码检查工具或静态类型检查器(针对像 Python 这样的动态语言)以及测试。管道中的构建和部署步骤还确保这些操作容易执行,并具有足够的自动化水平。

结论

Hatch 是一个出色的自动化工具,虽然需要一些维护和配置选项的指定,但它通过显著简化环境和构建创建,带来了极大的回报。

当开发者可以轻松地使用软件,而无需花费数小时去弄清楚如何设置时,他们更容易提高生产力,同时也能找出文档中遗漏的部分,因为他们可以运行代码。

感谢您阅读到这里,这篇文章较长,但我希望我能说服您,使用像 Hatch 这样的工具可以大大提升您和其他开发者的开发体验。

参考文献

一些书籍对这篇文章产生了深远的影响。最突出的书是独角兽项目,由著名的 DevOps 作者Gene Kim编写,并由 IT Revolution 编辑。一个高级开发者被流放到一个功能失调的部门,在那里开发者无法本地运行代码。在小说中,她和一群开明的同事尽力解救她的同事,摆脱成千上万的阻碍和官僚主义,去做他们真正被雇佣来做的事情。

深入理解持续交付由 Christie Wilson 编写,并由 Manning 编辑,是一本从零开始建立 CI/CD 管道并在过程中改进代码库的指南。

重构遗留系统由 Chris Birchall 编写,并由 Manning 编辑,全面探讨了遗留软件,包括如何处理我们被分配的遗留系统,如何改进它,以及如何确保我们不会编写遗留软件。

Medium 会员

您喜欢我的写作并考虑订阅 Medium 会员以无限制访问文章吗?

如果你通过此链接订阅,你将通过你的订阅支持我,而无需支付额外费用 medium.com/@mattiadigangi/membership

词嵌入的解释

原文:towardsdatascience.com/word-embeddings-explained-c07c5ea44d64

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

·发布于 Towards Data Science ·7 min read·2023 年 5 月 30 日

在自然语言处理领域,我们处理的是单词。然而,计算机无法直接理解单词,因此需要将其转换为数值表示。这些数值表示,称为向量或嵌入,由可以被人类解释或无法解释的数字组成。在这篇博客中,我们将深入探讨这些词汇表示的学习进展。

1 N-grams

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

图 1: 句子的 N-gram 向量表示(作者提供的图片)

让我们以 n-grams 为例,更好地理解这一过程。假设我们有一句话,希望计算机能够理解。为此,我们将句子转换为数值表示。这种表示包括各种词汇组合,如 unigrams(单词)、bigrams(词对)、trigrams(三词组)以及更高阶的 n-grams。结果是一个可以表示任何英语句子的向量。

在图 1 中,考虑对句子“This is a good day”进行编码。假设向量的第一个位置表示在原句中出现的 bigram “good day”的次数。由于它出现了一次,因此这个位置的数值表示为“1”。同样,我们可以用这个向量的不同位置来表示每一个 unigram、bigram 和 trigram。

这个模型的一个主要优点是可解释性。这个向量中的每个数字都有一定的含义,人类可以关联。当进行预测时,不难看出什么影响了结果。然而,这种数值表示有一个主要的缺点:维度灾难。这个 n-gram 向量很大。如果用于统计建模,需要从中挑选特定的部分。原因在于维度灾难。随着向量维度的增加,句子表示之间的距离也增加。这有利于表示更多信息,但如果过于稀疏,统计模型难以判断哪些句子在物理上(从而在意义上)更接近。此外,挑选是一个手动过程,开发人员可能会遗漏一些有用的 n-gram 表示。

2 神经网络

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

图 2:神经概率语言模型(Bengio 等,2003)

为了解决这一不足,神经概率语言模型在 2003 年被引入。语言模型预测序列中下一个出现的词。例如,一个训练好的语言模型会将“我想要一个法式”这个词序列,生成下一个词“吐司”。如图 2 所示的神经语言模型在很大程度上以相似的方式工作,即使用前 N 个词的上下文来预测下一个词。

对于每个词,我们学习一个密集表示,这是一个包含固定数量数字的向量来表示每个词。与 n-gram 不同,向量中的这些数字不易被人类直接解释。然而,它们捕捉了人类可能未察觉的各种细微差别和模式。

激动人心的部分是,由于这是一个神经网络,我们可以端到端地训练它以掌握语言建模的概念,并同时学习所有词向量表示。然而,训练这样的模型可能计算开销很大

例如,如果我们用一个包含 100 个数字的向量来表示每个词,而我们需要将这些向量连接起来,这将涉及到成千上万的数字。考虑到词汇量可能达到数万个或更多,我们可能会面临数百万甚至数千万个参数需要计算。这在处理大型词汇表、大量示例或每个词表示的高维度时成为一个挑战。

理想情况下,较大的维度将使我们能够捕捉到语言的更复杂特性,考虑到语言本质上的复杂性。

在接下来的十年里,推出了各种架构来提升词嵌入的质量。一个这样的架构在论文中描述了,使用新颖的神经网络架构进行快速语义提取。它引入了为每个词汇加入位置性信息的概念,以改进嵌入。然而,这种方法也存在训练时计算开销大的缺点,仅用于学习词嵌入。

3 Word2Vec

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

图 3:向量空间中词表示的高效估计(Milikov 等,2013)

在 2013 年,生成词嵌入的一个重大突破是 Word2Vec 的引入。这篇论文提出了两种模型,即连续词袋模型(CBOW)和 Skip-gram 模型,旨在保持简单性同时理解词嵌入。在 CBOW 模型中,当前词是基于前两个和后两个词进行预测的。投影层表示该特定词的词嵌入。而 Skip-gram 模型则执行类似的任务,但方向相反,给定一个词预测其上下文周围的词。同样,投影层表示当前词的向量表示。在训练这些网络之后,会得到一个词及其对应嵌入的表格。这种架构更简单,参数更少,标志着预训练词嵌入时代和 word2vec 概念的到来。

然而,这种方法也有一些局限性。首先,它生成每次出现的词的相同向量表示,无论其上下文如何。例如,“drag queen”中的“queen”和“king and queen”中的“queen”将具有相同的词嵌入,尽管它们含义不同。此外,这些词嵌入的生成考虑了有限的上下文窗口,在训练阶段仅查看前两个词和后两个词。这一局限性影响了模型的上下文意识。

4 ELMo

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

图 4:深度上下文化的词表示(Peters 等,2018)

为了提高生成嵌入的质量,ELMo(来自语言模型的嵌入)在 2018 年被引入。ELMo 是一种双向 LSTM(长短期记忆)模型,在同一个训练过程中处理语言建模和创建密集的词嵌入。该模型通过利用 LSTM 单元,有效地捕捉上下文信息,尤其是在较长的句子中。然而,与 LSTM 模型类似,ELMo 也存在一些缺陷。训练这些模型可能较慢,并且它们采用了一种称为 BPTT(时间反向传播)的截断版本。此外,它们并非真正双向,因为它们分别学习前向和后向上下文,然后将其连接,这可能导致一些上下文信息的丢失。

5 Transformers

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

图 5:Attention is all you need(Vaswani 等,2017)

在 ELMo 引入之前不久,Attention Is All You Need 论文 提出了 Transformer 神经网络架构。Transformers 包括一个编码器和一个解码器,这两个部分都结合了位置编码以生成具有上下文意识的词向量。例如,当输入句子 “I am Ajay” 时,编码器生成三个密集的词嵌入表示,保留词义。Transformers 还解决了 LSTM 模型的缺点。由于数据可以并行处理,利用 GPU,训练速度更快。此外,Transformers 是深度双向的,因为它们采用了一个注意力机制,使得词语可以同时关注前后词语,从而实现有效的上下文理解。

Transformers 的主要问题是对于不同的语言任务,我们需要大量的数据。然而,如果人类对语言有一定的内在理解,那么他们不需要看到大量的例子就能理解如何回答问题或进行翻译。

6 BERT 和 GPT

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

图 6:BERT:深度双向 Transformers 的语言理解预训练(Devlin 等,2019)

为了克服 Transformer 模型在语言任务中的局限性,引入了两个强大的模型,BERT(双向编码器表示从 Transformers)和 GPT(生成预训练 Transformer)。这些模型利用了迁移学习,涉及两个训练阶段。

在第一阶段,称为预训练阶段,模型从大量数据中学习关于一般语言理解、上下文和语法的知识。在这个阶段,它们获得了坚实的知识基础。在第二阶段,称为微调阶段,模型通过提供特定任务的数据进行训练。这一微调过程使模型能够专注于执行期望的任务而无需大量的任务特定数据

BERT 在两个任务上进行了预训练:掩蔽语言建模和下一个句子预测。通过这种预训练,BERT 对每个单词的上下文和含义有了深刻的理解,从而改进了词嵌入。它可以在特定任务上进行微调,如问答或翻译,使用相对较少的任务特定数据。

同样,GPT 在语言建模上进行了预训练,这涉及预测句子中的下一个单词。这种预训练帮助 GPT 发展了对语言的全面理解。之后,它可以在特定任务上进行微调,以便像 BERT 一样利用其语言理解能力。

BERT 和 GPT 都具备 Transformer 架构和学习各种语言任务的能力,相比于早期方法,提供了更优越的词嵌入。这就是为什么 GPT 尤其作为许多现代语言模型的基础,例如 ChatGPT,使得先进的自然语言处理和生成成为可能。

7 结论

在这篇博客中,我们探讨了计算机如何通过被称为“嵌入”的表示来理解语言。我们见证了近年来的进展,特别是随着变换器的兴起,成为现代语言模型的基础。如果你对从头开始构建自己的变换器模型感兴趣,查看 这份视频播放列表,其中深入探讨了相关的代码和理论。祝学习愉快!

解释 Word2Vec、GloVe 和 FastText

原文:towardsdatascience.com/word2vec-glove-and-fasttext-explained-215a5cd4c06f

计算机如何理解单词

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

·发表于 Towards Data Science ·10 min 阅读·2023 年 6 月 20 日

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

GrowtikaUnsplash 上的照片

计算机不像我们那样理解单词。它们更喜欢处理数字。因此,为了帮助计算机理解单词及其含义,我们使用一种叫做嵌入的技术。这些嵌入以数学向量的形式数值化地表示单词。

这些嵌入的酷炫之处在于,如果我们正确学习它们,那么具有相似含义的单词将具有相似的数值。换句话说,它们的数值会更接近。这使得计算机能够基于数值表示把握不同单词之间的联系和相似性。

一种学习单词嵌入的显著方法是 Word2Vec。在这篇文章中,我们将深入探讨 Word2Vec 的复杂性,并探索其各种架构和变体。

Word2Vec

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

图 1: Word2Vec 架构 (来源)

在早期,句子通过 n-gram 向量进行表示。这些向量旨在通过考虑单词序列来捕捉句子的本质。然而,它们存在一些局限性。n-gram 向量通常非常大且稀疏,这使得它们在计算上很具挑战性。这造成了一个被称为维度诅咒的问题。本质上,这意味着在高维空间中,表示单词的向量距离过远,从而很难确定哪些单词真正相似。

然后,在 2003 年,随着神经概率语言模型的引入,出现了一个显著的突破。这个模型通过使用所谓的连续密集向量完全改变了我们对词语的表示方式。与离散且稀疏的 n-gram 向量不同,这些密集向量提供了连续的表示。即使这些向量发生小的变化,也会产生有意义的表示,尽管它们可能不直接对应特定的英语单词。

在这一激动人心的进展基础上,Word2Vec框架于 2013 年出现。它提供了一种将词义编码为连续密集向量的强大方法。在 Word2Vec 中,引入了两种主要架构:连续词袋模型(CBoW)和 Skip-gram。

这些架构为生成高质量的词嵌入的高效训练模型打开了大门。通过利用大量的文本数据,Word2Vec 使单词在数字世界中栩栩如生。这使计算机能够理解单词之间的上下文含义和关系,为自然语言处理提供了一种变革性的方法。

连续词袋模型(CBoW)

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

图 2:CBoW 训练示意图(作者提供的图片)

在本节及下一节中,让我们了解如何使用一个包含五个单词的小词汇表训练 CBoW 和 skip-gram 模型:biggest、ever、lie、told 和 the。我们有一个示例句子“The biggest lie ever told”。我们将如何将其传递给 CBoW 架构?这在图 2中显示,但我们也会描述这个过程。

假设我们将上下文窗口大小设置为 2。我们取“the”、“biggest”、“ever”和“told”这几个词,并将它们转换为 5x1 的独热向量。

然后将这些向量作为输入传递到模型,并映射到一个投影层。假设这个投影层的大小为 3。每个词的向量乘以一个 5x3 的权重矩阵(在输入间共享),得到四个 3x1 的向量。取这些向量的平均值,得到一个 3x1 的向量。然后使用另一个 3x5 的权重矩阵将这个向量投影回 5x1 的向量。

这个最终向量代表了中间单词“lie”。通过计算真实的独热向量和实际的输出向量,我们得到一个损失,用于通过反向传播更新网络的权重。

我们通过滑动上下文窗口并将其应用于成千上万的句子来重复这个过程。训练完成后,模型的第一层,尺寸为 5x3(词汇表大小 x 投影大小),包含了学习到的参数。这些参数用作查找表,将每个单词映射到其对应的向量表示。

Skip-gram

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

图 3:Skip-gram 训练示意图(作者提供的图片)

在 skip-gram 模型中,我们使用了类似于连续词袋(CBoW)模型的架构。然而,与其根据周围词预测目标词相反,我们将场景反转,如图 3所示。现在,词“lie”成为输入,我们的目标是预测其上下文词。名称“skip-gram”反映了这种方法,因为我们预测的上下文词可能会“跳过”几个词。

为了说明这一点,我们来看一些例子:

  • 输入词“lie”与输出词“the”配对。

  • 输入词“lie”与输出词“biggest”配对。

  • 输入词“lie”与输出词“ever”配对。

  • 输入词“lie”与输出词“told”配对。

我们对训练数据中的所有词汇重复这一过程。一旦训练完成,第一层的参数,其维度为词汇大小 x 投影大小,将捕捉输入词与其对应向量表示之间的关系。这些学习到的参数使我们能够在 skip-gram 模型中将输入词映射到其相应的向量表示。

优点

  1. 以简洁的方式克服维度灾难:Word2Vec 提供了一个直接而高效的解决方案来应对维度灾难。通过将词表示为密集向量,它减少了与传统方法(如 n-gram 向量)相关的稀疏性和计算复杂度。

  2. 生成向量,使得意义相近的词具有更接近的向量值:Word2Vec 的嵌入展现了一个有价值的特性,即具有相似意义的词由数值更接近的向量表示。这允许捕捉语义关系并执行诸如词语相似性和类比检测等任务。

  3. 预训练的嵌入用于各种 NLP 应用:Word2Vec 的预训练嵌入广泛可用,并可以用于各种自然语言处理(NLP)应用。这些嵌入经过大规模语料库的训练,为情感分析、命名实体识别、机器翻译等任务提供了宝贵的资源。

  4. 自监督框架用于数据增强和训练:Word2Vec 以自监督的方式运行,利用现有数据来学习词表示。这使得收集更多数据和训练模型变得容易,因为它不需要大量标记数据集。该框架可以应用于大量未标记的文本,从而增强训练过程。

缺点

  1. 对全局信息的有限保留:Word2Vec 的嵌入主要关注捕捉局部上下文信息,可能无法保留词之间的全局关系。这一限制可能会影响需要更广泛理解文本的任务,例如文档分类或文档级别的情感分析。

  2. 不太适合形态丰富的语言:形态丰富的语言特征是复杂的单词形式和词形变化,这可能对 Word2Vec 造成挑战。由于 Word2Vec 将每个单词视为原子单位,它可能难以捕捉此类语言中的丰富形态学和语义细微差别。

  3. 缺乏广泛的上下文意识:Word2Vec 模型在训练过程中只考虑了目标单词周围的局部上下文窗口。这种有限的上下文意识可能导致在某些语境中对单词意义的理解不完整。它可能难以捕捉某些语言现象中存在的长期依赖关系和复杂的语义关系。

在接下来的部分中,我们将看到一些能够解决这些缺点的词嵌入架构。

GloVe: 全球向量

Word2Vec 方法在一定程度上成功地捕捉了局部上下文,但没有充分利用语料库中可用的全局上下文。全局上下文指的是使用语料库中的多个句子来收集信息。这就是GloVe发挥作用的地方,因为它利用词-词共现来学习词嵌入。

词-词共现矩阵的概念是 GloVe 的关键。它是一个矩阵,捕捉语料库中每个单词在其他每个单词上下文中的出现情况。矩阵中的每个单元格表示一个单词在另一个单词上下文中的出现次数。

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

图 4:词共现概率比率的示例(来源

与 Word2Vec 直接处理共现概率不同,GloVe 从共现概率的比率开始。在图 4的背景下,P(k | ice)表示单词 k 在“ice”一词上下文中的出现概率,而 P(k | steam)表示单词 k 在“steam”一词上下文中的出现概率。通过比较比率 P(k | ice) / P(k | steam),我们可以确定单词 k 与冰或蒸汽的关联。如果比率远大于 1,则表明与冰的关联更强。相反,如果接近 0,则表明与蒸汽的关联更强。比率接近 1 则表明与冰或蒸汽没有明确的关联。

例如,当 k = “solid”时,概率比率远大于 1,表明与冰的关联很强。另一方面,当 k = “gas”时,概率比率接近 0,表明与蒸汽的关联更强。至于“water”和“fashion”这两个词,它们与冰或蒸汽都没有明确的关联。

基于概率比率的词语关联正是我们希望实现的目标。而在学习 GloVe 的嵌入时,这一点得到了优化。

FastText

传统的 word2vec 架构除了缺乏全球信息的利用外,也无法有效处理形态丰富的语言。

那么,语言形态丰富意味着什么呢?在这种语言中,单词的形式可以根据其使用的上下文而变化。我们以一种名为“卡纳达语”的南印度语言为例。

在卡纳达语中,“house”的单词是写作 ಮನೆ (mane)。然而,当我们说“在房子里”时,它变成了 ಮನೆಯಲ್ಲಿ (maneyalli),而当我们说“从房子里”时,它则变为 ಮನೆಯಿಂದ (maneyinda)。如你所见,只有介词发生变化,但翻译的词具有不同的形式。在英语中,这些都是简单的“house”。因此,传统的 word2vec 架构会将所有这些变体映射到相同的向量。然而,如果我们为形态丰富的卡纳达语创建一个 word2vec 模型,那么这三种情况中的每一种将被分配不同的向量。此外,卡纳达语中的“house”可以有比这三个例子更多的形式。由于我们的语料库可能不包含所有这些变体,传统的 word2vec 训练可能无法捕捉所有不同的词表示。

为了解决这个问题,FastText 通过在生成词向量时考虑子词信息引入了一个解决方案。FastText 不将每个单词作为一个整体,而是将单词分解为从三元组到六元组的字符 n-grams。这些 n-grams 被映射到向量上,然后这些向量被聚合以代表整个单词。聚合后的向量接着被输入到 skip-gram 架构中。

这种方法能够识别语言中不同单词形式的共同特征。即使我们可能没有在语料库中见过单词的每一种形式,所学到的向量仍能捕捉这些形式之间的共性和相似性。形态丰富的语言,如阿拉伯语、土耳其语、芬兰语以及各种印度语言,可以受益于 FastText 能生成考虑不同形式和变体的词向量。

上下文意识

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

图 5:ELMo 和 BERT (来源)

尽管有其优点,前述的 word2vec 架构存在一个局限性:它们为给定的单词生成相同的向量表示,而不考虑其上下文。

为了说明这一点,我们考虑以下两个句子:

  1. “那个变装皇后真棒。”

  2. “她有一张王牌和一张皇后,手牌完美。”

在这些句子中,单词“queen”有不同的含义。然而,在 word2vec 架构中,这两种情况下“queen”的向量是相同的。这并不理想,因为我们希望词向量能够根据上下文捕捉和表示不同的含义。

为了解决这一问题,引入了更先进的架构,如LSTM单元。这些架构被设计为将上下文信息纳入词语表示中。随着时间的推移,基于 Transformer 的模型如BERTGPT的出现,推动了我们今天看到的大规模语言模型的发展。这些模型在考虑上下文和生成对周围词语和句子敏感的词语表示方面表现出色。

通过考虑上下文,这些先进的架构使得生成更加细致和有意义的词向量成为可能,确保相同的词在不同的具体上下文中可以有不同的向量表示。

结论

总之,本文提供了关于 word2vec 架构及其使用连续稠密向量表示词语的能力的见解。后续实现如 GloVe 利用了全局上下文,而 FastText 使得对形态丰富的语言如阿拉伯语、芬兰语和各种印度语言的向量学习变得高效。然而,这些方法的一个共同缺点是,在推理过程中,它们对一个词分配相同的向量,无论其上下文如何,这可能会阻碍对具有多重含义的词的准确表示。

为了解决这一局限性,后续在自然语言处理(NLP)领域的进展引入了 LSTM 单元和 Transformer 架构,这些技术在捕捉特定上下文方面表现出色,并成为现代大型语言模型的基础。这些模型能够理解并生成根据周围上下文变化的词语表示,适应了不同场景中词语的细微含义。

然而,重要的是要承认 word2vec 框架依然具有重要意义,因为它继续推动着自然语言处理领域中的众多应用。尽管词义的上下文变化带来了挑战,其简单性和生成有意义的词嵌入的能力仍然被证明是有价值的。

欲了解更多关于语言模型的信息,查看这个 YouTube 播放列表

快乐学习!

使用 Julia 进行 Wordle 单词长度和字母频率分析

原文:towardsdatascience.com/wordle-word-length-and-letter-frequency-analysis-using-julia-3fc5c63fedba?source=collection_archive---------12-----------------------#2023-02-10

探索英语单词数据集以提升我们的游戏

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

·

Follow 发布于 Towards Data Science ·19 分钟阅读·2023 年 2 月 10 日

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

Wordle 启发。图像由作者提供。

Wordle™ 是一款无需介绍的绝佳游戏。但保持连胜并非易事。为了更好地理解游戏的运作方式,我们将加载英语单词到 Julia 中,并对它们进行一点分析。我们将关注字母频率以及特别是 5 字母单词,以帮助在游戏中做出更有根据的猜测。同时,我们还将探讨为何选择五个字母作为游戏的单词长度。

虽然我不能保证它会减少你的猜测次数,但我可以保证使用 JuliaJulia Plots 进行数据分析会很有趣。

在我工作的 AAXIS 公司,我们实施了面向企业的数字商务解决方案以及面向消费者的数字商务解决方案。这包括将大量数据从现有的旧系统迁移到具有不同数据结构的新系统。我们使用各种数据工具来分析源数据,以确保一致性。我通常会使用本文中概述的技巧。熟悉这些技巧的好方法是经常使用它们,而 Wordle 单词列表听起来是一个有趣的练习方式。

让我们开始吧。

设置

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

图片由 Linus Mimietz 提供,来源于 Unsplash

如果你计划跟我一起学习 Julia,我强烈建议你阅读这一部分。如果不计划,你可以安全地跳过。

安装 Julia

请从 Julia Language 网站安装 Julia。该网站提供适用于 Windows、Mac 和 Linux 操作系统的安装程序。我还为 VSCode 安装了 Julia Language Support 扩展,并在 VSCode 中执行这些命令的 Julia REPL。这样,我的图形会显示在窗口中,使数据集分析变得极为方便。

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

Visual Studio Code 中的 Julia REPL。图片由作者提供。

现在,让我们加载图形和其他所需的库。安装完成后,你可以通过输入 CNTRL-SHIFT-P 并选择 Julia: Start REPL 来打开 Julia REPL 窗口。

在 Julia REPL 中输入这些命令来检查一切是否正常:

julia> println("Hello world")
Hello World

julia> f(x) =+ 3x + 2
f (generic function with 1 method)

julia> f(2)
12

在 VSCode 中,当代码被选中时,按 SHIFT-ENTER 可以执行它,并在下方的 Julia REPL 中输出结果。你可以直接从本文中剪切代码,粘贴到 VSCode 中,选择代码,然后按 SHIFT-ENTER 执行它。为了方便这种方法,我将跳过代码中的 julia> 提示。请注意,你可以在 REPL 中输入所有这些命令,即使是包含 for-loops 或函数的长命令。

接下来,让我们加载 Plots。你的用户界面可能会有所不同,因为 Julia 可能会下载和安装一些包。我们还需要 DelimitedFiles 和 Statistics 包:

import Pkg
Pkg.add("Plots")
using Plots
using DelimitedFiles
using Statistics

plot(f,-10,10)

最后一行应该会生成一个如上图所示的图形。

词汇数据集

在线有多个单词数据集,洁净程度不一。不幸的是,我没有权限在这篇文章中使用大多数数据集。我选择了 SCOWL(Spell Checker Oriented Word Lists)及 Friends,这是一个关于英语单词的数据库,适用于创建高质量的单词列表,适合大多数英语方言的拼写检查器,并可供我们使用。如果你决定探索,其他可供公众用于教育目的的单词列表如下:

  • Infochimp 的简单英语单词列表是 Infochimp 提供的英语单词列表。GitHub 网站提供了该列表,但声明版权归 Infochimp 所有。Infochimp 的链接指向一个不存在的页面。

  • Collins Official Scrabble™ Words列表得到WESPA的认可,用于全球锦标赛和俱乐部比赛,不包括美国和加拿大。该软件仅限私人、非商业用途。这是一个精心策划的英语单词列表。

  • The Wordle Guess List是游戏中允许的 13K 单词的完整列表,由 Josh Wardle 版权所有,现在可能由纽约时报版权所有。该列表与上述 Scrabble 列表中的 5 字母单词子集非常匹配。答案列表,即 Wordle 的 2.5K 可能答案,可以在这里找到。

现在,让我们继续获取 SCOWL 单词列表,并为分析做好准备。前往app.aspell.net/create下载单词列表。我选择的选项如图所示。我使用了 80 字母列表进行下一部分,并将文件命名为 scowl_american_words_80.txt。

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

生成单词列表所选择的选项的图像。截图由作者提供。

我还下载了 35 字母选项,并将其命名为 scowl_american_words_35.txt。我将用于稍后在文章中分析更常见的单词。

对于单词使用,我使用了在Miller Center¹提供的总统演讲。这些演讲属于公共领域,因此没有使用限制。只需选择总统的名字,点击演讲,然后点击抄本。我将文本复制并粘贴到文本编辑器(VSCode)中进行分析。

为什么选择 5 字母单词?

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

图片由Glen Carrie提供,来源于Unsplash

我的好朋友和邻居 Milan 提出了这个问题。他认为 5 字母词汇可能是英语中最常见的词汇。通过加载我们在设置中讨论的 SCOWL 词汇表数据集并计算词汇长度的频率,这确实值得检查。

rawwords = readdlm("data/scowl_american_words_80.txt", header=false,String)
343681×1 Matrix{String}:
 "A"
 "A'asia"
 "A's"
 "AA"
 "AA's"
 "AAA"
 "AAM""zymurgies"
 "zymurgy"
 "zymurgy's"
 "zythum"
 "zyzzyva"
 "zyzzyvas"
 "zzz"

totalWords = length(rawwords)
343681

aspell 词汇表包含了像zymurgy's这样的所有格词和像Albert.这样的专有名词。让我们把它们去掉。

dictwords = String[];
for word in rawwords
      # remove words with ', -' space and capital letter start
      if (length(word)>0 && !(occursin("\'",word) 
            || occursin("-",word) 
            || occursin(" ",word)
            || (word[1]<'a' || word[1]>'z')
            ))
          push!(dictwords,word)
      end
end

# get count
totalWords = length(dictwords)
244274

接下来,我们应该为词汇长度构建一个直方图。我知道有三种方法可以做到这一点:列表推导、map 函数和 for 循环。考虑到性能,在 Julia 中它们都应该差不多。让我们来看看:

# use list comprehension
wordlengths = zeros(Int64,0)
@time wordlengths = [length(x) for x in dictwords];
0.021251 seconds (45.30 k allocations: 4.246 MiB, 84.02% compilation time)

# use map
wordlengths = zeros(Int64,0);
@time wordlengths = map(word->length(word), dictwords);
0.024202 seconds (49.09 k allocations: 4.473 MiB, 85.97% compilation time)

# use for loop
wordlengths = zeros(Int64,0);
@time for word in dictwords
  push!(wordlengths, length(word))
end
0.027516 seconds (488.05 k allocations: 14.131 MiB) 

一般来说,虽然列表推导在性能上略好,但它们都非常接近,我会选择 for 循环,因为它使逻辑清晰易懂。现在,词汇长度数组应该包含每个词的长度。最小值和最大值是:

lrange = minimum(wordlengths),maximum(wordlengths)
(1, 45)

最小值为 1,最大值为 45,但为了直方图的目的,我决定将所有大于 20 的词汇放入“20”箱中(len>20 ? 20 : len)。现在,让我们来看看每个词汇长度的分布。Julia Plots 使这一过程变得简单。

julia>  histogram(wordlengths,bins=20)

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

英语词汇数据集中的词汇长度。图像作者提供

我实际上用了一些选项运行了上述命令,以使图表看起来更具信息性。

histogram(wordlengths,
    bins=20,
    xaxis=("WORD LENGTH"),
    yaxis=("COUNT"),
    xticks=([1:1:20;]),
    yticks=([0:5e3:4.5e4;],["$(x)k" for x=0:5:45]),
    label=("Word count"),
    xguidefontsize=8, yguidefontsize=8,
    margin=5mm, ylims = (0,4e4),
    framestyle = :box,
    fill = (0,0.5,:green),
    size=(800,420))

savefig("pics/2-english-word-lengths-v2.png")

令人惊讶的是,5 字母词汇甚至不在前五个最常见的词汇长度中!它们仅占我们词汇数据集的 4.6%。Ravi Parikh²和 Reginald Smith³也报告了类似的发现(5.2%)。差异可能是因为我选择了一个不包含俚语、连字符词等的词汇数据集。

首先,图表看起来像经典的钟形曲线分布。这种钟形曲线是自然界和心理学中常见的特征⁴,例如婴儿出生体重、男性身高、血压测量等。我不是语言学家,但在我看来,这表明英语语言似乎像是自然基础和随机演化的。

julia> mean(wordlengths)
9.250849455938823

julia> median(wordlengths)
9.0

最常见的词汇长度是 8 和 9,但我似乎很难想到任何一个。因此,让我们列出几个以确保:

julia> filter(x->(length(x)==9), dictwords)[1:5]
5-element Vector{String}:
 "aardvarks"
 "aasvogels"
 "abactinal"
 "abamperes"
 "abandoned"

julia> filter(x->(length(x)==9), dictwords)[27001:27005]
5-element Vector{Any}:
 "sassabies"
 "sassafras"
 "sassarara"
 "sassiness"
 "sassolite"

AASVOGELS?显然,这是南非的一种秃鹫。SASSAFRAS?难怪我在拼字游戏中不太行。

那么,有多少 5 字母词汇呢?

julia> length(filter(x->(length(x)==5), dictwords))
11210

相较于 36K 个 9 字母词汇,5 字母词汇只有 11K 个。嗯……

那么为什么 Wordle 使用 5 字母词汇?

也许我们使用它们更多。

我的编辑 Megan,那个帮助我写这些文章的好女士,认为普通人最适应 5 字母词汇。这意味着 5 字母词汇应该占据我们交流的大部分,并且出现频率较高。为了验证这个理论,我查看了总统演讲。

总统演讲通常是为不同背景和观点的广泛受众专业撰写的,因此写作必须具有包容性、文化敏感性和可及性。演讲稿撰写者还必须对当前事件和政治问题有深刻的理解,以有效传达总统的立场和目标。总统演讲对公众舆论和政治话语的形成有着重要影响,因此它们成为了那个时代语言的极佳代表文本。

这些演讲属于公有领域,且易于访问进行分析¹。

对于这项练习,我抓取了美国过去四位总统的演讲。我分析了单词长度并进行了图示。

为了简化图表绘制,我编写了一个函数来返回直方图数据。长度超过 18 个字母的单词,会被归入 18 字母以下的桶中。

function wordLengthHistoFromFile(filename, doUnique=true)
    set_tokenizer(poormans_tokenize)

    # split the words in the file into tokens
    words = collect(tokenize(read(filename, String)))

    if (doUnique) 
        words = unique(words)
    end
    totalWords = length(words)

    # this array holds the count. Index is the word length``
    local wordLengths = zeros(Int64,18)

    # Now iterate through the words
    for word in words
        l = length(word)

        # there should be very few words more than 18
        l = l<=18 ? l : 18;
        wordLengths[l] += 1
    end

    # Return % of total word lengths used
    return wordLengths./(totalWords/100.0)
end

我使用了穷人版的分词器。分词器本质上是将文本拆分为单词或子单词,以便我们可以逐个处理它们。穷人版分词器删除所有标点符号,并按空格拆分,虽然不够复杂,但足够满足我们的目的。

Julia 的分词器接口接受一个text作为输入,并返回一个单词数组。然后,我们简单地遍历数组中的所有单词,计算长度,并增加表示该长度的桶。

现在函数已经准备好,我们可以处理文本格式的演讲文件。首先,让我们看看这里的就职演讲。

hBushJrInaugural = wordLengthHistoFromFile("data/speech-bushjr-inaugural.txt")
hObamaInaugural = wordLengthHistoFromFile("data/speech-obama-inaugural.txt")
hTrumpInaugural = wordLengthHistoFromFile("data/speech-trump-inaugural.txt")
hBidenInaugural = wordLengthHistoFromFile("data/speech-biden-inaugural.txt")

因此,我们处理每篇就职演讲,并创建一个桶数组。我们将这些数组水平拼接(hcat),得到一个可以绘制的单一 18x5 数组。

plot(hcat(hBushJrInaugural,hObamaInaugural,hTrumpInaugural,hBidenInaugural ), 
        xticks=([1:18;]),
        yticks=([0:2:100;],["$(x)%" for x=0:2:100]),
        label=["Bush" "Obama" "Trump" "Biden"],
        yaxis=("% OF APPEARANCE"),
        xaxis=("WORD LENGTH"),
        xguidefontsize=8, yguidefontsize=8,
        margin=5mm,
        framestyle = :box,
        lw=2, size=(800,420), marker=(:circle),
        lc=[:red :blue :lightcoral :dodgerblue],
        mc=[:white :white :black :black]
)

savefig("pics/3-presidential-speeches.png")

上面的第一行绘制了图表,然后我将图表保存为图像文件。

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

总统演讲中的单词长度。图片来源:作者。

这里的图表显示了每篇演讲的分布。例如,奥巴马总统的就职演讲中不到 3%的独特单词由 2 个字母的单词组成。

5 个字母的单词似乎很受欢迎,同时 4 个和 6 个字母的单词也不少。注意,虽然 9 个字母的单词占字典的 15%,但在演讲中只占所有独特单词的不到 8%。

现在,让我们看一下更为即兴的环境。这应该能给我们一个日常用词的感觉,而不是经过深思熟虑的脚本化工作。我选择了过去四位总统中的每位的一个新闻发布会进行分析。

hBushJrPC = wordLengthHistoFromFile("data/pc_bushjr_final.txt")
hObamaPC = wordLengthHistoFromFile("data/pc_obama_final.txt")
hTrumpPC = wordLengthHistoFromFile("data/pc_trump_laborday.txt")
hBidenPC = wordLengthHistoFromFile("data/pc_biden_first.txt")

然后绘制结果:

plot(hcat(hBushJrPC,hObamaPC,hTrumpPC,hBidenPC ), 
        xticks=([1:18;]),
        yticks=([0:2:20;],["$(x)%" for x=0:2:20]),
         label=["Bush" "Obama" "Trump" "Biden"],
         xaxis=("WORD LENGTH"),yaxis="% OF APPEARANCE",
         lw=2, size=(800,420), marker=(:circle),
         xguidefontsize=8, yguidefontsize=8,
         framestyle = :box,
         margin=5mm, ylims = (0,20),
         lc=[:red :blue :lightcoral :dodgerblue],
         mc=[:white :white :black :black]
       )
savefig("pics/4-presidential-press-conf.png")

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

总统新闻发布会中的单词长度。图片来源:作者。

这里的模式更加清晰。除了拜登总统(他似乎偏爱 4 个字母的单词)之外,5 个字母的单词明显更受欢迎。

在文学和网络上,我们可以找到更多的证据。纽约时报文章的平均单词长度显然是 4.9⁵。根据WolframAlpha,英语中的平均单词长度是 5.1 个字符。该网站还声称,《大英百科全书在线》的平均单词长度为 5.3,而维基百科为 5.2。本文的单词平均长度为 4.75 个字母,计算这里。这似乎不算太差。

那么,为什么是 5 个字母呢?在英语中,我们的词汇似乎由 5 个字母的单词组成,尽管有更多更长的单词可供选择。也许 Josh Wardle 尝试了四个、五个和六个字母,决定五个字母最适合目标受众和解决时间。游戏应该有一点挑战性,这样你会觉得自己完成了一些事情,但又不会太难。或者,虽然可能性较小,他可能经过了这种分析并决定了五个字母。我们可能永远不会知道。

字母频率

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

图片由Isaac Smith提供,来源于Unsplash

既然我们已经把为什么是五个字母的问题搞清楚了,就让我们专注于改进我们的游戏。

玩 Wordle 的一种策略是最大化命中率(黄色或绿色),以减少总猜测次数。显而易见的方法是选择包含最常见字母的单词⁶。都柏林大学的模拟发现,根据一个好的首字母选择,平均猜测次数可以从 5 减少到 4⁷。这就是字母频率分析可以帮助的地方。

首先,让我们处理一下我们之前下载的整个单词数据集。这些将包括所有长度的单词。一旦我们弄清楚了所有英语单词中的频率,我们可以将其与 5 个字母单词中的字母频率进行比较,看看是否有显著差异。

我暂时想不到用列表推导的方式来做这个。不过,“map”和“for-loop”相当简单。由于 map 稍微快一些,我们先用它。

charfrequency = zeros(Int64,26)
@time map(word->(
map(
  char-> ((char>='a' && char<='z') ? charfrequency[char-'a'+1]+=1 : 1), collect(word))
), dictwords);

0.386160 seconds (5.85 M allocations: 142.942 MiB, 7.98% gc time, 26.68% compilation time)

# convert to percentage
charfrequency = charfrequency ./ (totalWords/100.0) 

我不知道你怎么样,但如果这不是我的代码,我可能会对它的作用感到有些迷惑。让我们看看 for 循环是否更好:

charfrequency = zeros(Int64,26)
@time for word in dictwords
  for char in collect(word)
     if (char>='a' && char<='z')
        charfrequency[char-'a'+1] += 1
     end
  end
end
0.437091 seconds (7.49 M allocations: 171.794 MiB, 6.45% gc time, 1.16% compilation time)

# convert to percentage
charfrequency = charfrequency ./ (totalWords/100.0)

这个我可以读懂:一个针对单词的 for 循环,然后是一个针对字符的 for 循环。之后,我只是简单地统计字母。性能也不差,增加可读性带来的百分之十的下降似乎是值得的。

让我们绘制这个数组:

bar(charfrequency,
         orientation=:h,
         yticks=(1:26, 'A':'Z'),
         xlabel=("% of APPREARANCE"),
         ylabel=("ALPHABET"),
         yflip=true,
         legend = :none, framestyle = :box,
         xticks=([0:10:110;],["$(x)%" for x=0:10:110]),
         xguidefontsize=8, yguidefontsize=8,
         margin=5mm, xlims = (0,110),
         ytickfont = font(6,"Arial"),
         fill = (0,0.5,:green),
         size=(800,420))

savefig("pics/5-dict-char-freq.png")

这就是结果。

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

字母在整个单词数据集中的出现百分比。图片由作者提供。

由于代码的工作方式,我们最终对 ‘SLEEP’ 中的‘E’进行了 2 次计数。这就是为什么我们在数据集中看到 E 的出现率超过 100%。我们应该只计数 1 次,即使字母在单词中出现多次。我们可以通过在计数之前简单地去除单词中的重复字符来解决这个问题。

charfrequencyu = zeros(Int64,26)
@time for word in dictwords
    for char in unique(word)
        if (char>='a' && char<='z')
        charfrequencyu[char-'a'+1] += 1
        end
    end
end

 0.491082 seconds (7.41 M allocations: 241.289 MiB, 7.55% gc time)

我将第二个 for 循环中的 collect 更改为 unique。现在,让我们绘制两个图表,看看是否有变化:

bar(hcat(charfrequency, charfrequencyu),
         orientation=:h,
         yticks=(1:26, 'A':'Z'),
         xlabel=("% of APPREARANCE"),
         ylabel=("ALPHABET"),
         yflip=true, framestyle = :box,
         xticks=([0:10:110;],["$(x)%" for x=0:10:110]),
         xguidefontsize=8, yguidefontsize=8,
         margin=5mm, xlims = (0,110),
         ytickfont = font(6,"Arial"),
         lc=[:black :black], mc=[:black :black],
         fill=[:green :blue], fillalpha=[0.5 0.5],
         label=["frequency" "unique"],
         size=(800,420))
savefig("pics/6-compare-vs-unique.png")

这是图表:

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

有无独特字母的出现次数比较。图片由作者提供。

我没有看到模式上有什么显著的不同。最常出现的字母也是出现次数最多的。从现在起,我们可以使用独特的字母来推动我们在 Wordle 中的词汇选择决策。

猜测词和答案词集合

上述分析适用于整个英语词汇数据集,不过,仅考虑 5 个字母的词汇会有所不同吗?让我们找出答案。使用 filter Julia 关键字,我们来制作一个只包含 5 个字母的词汇的新数组。

julia> guesswords = filter(x->(length(x)==5), dictwords)
julia> length(guesswords)

11210

官方 Wordle 猜测词汇表 包含略低于 13K 个词汇。所以,我们已经足够接近了。这些是你可以在游戏框中输入的词汇。Wordle 答案词汇表 是一个子集,包含 2.3K 个词汇,只包含可能作为隐藏答案出现的词汇。那么,为什么会有差异呢?

Josh Wardle 和他的伙伴 Palak Shah 创建了一个经过精心筛选的 答案词汇表,避免了晦涩和难解的词汇,以便让游戏对每个人都充满乐趣⁸。虽然你会发现 ZOPPA 是一个有效的猜测词,但不要指望它很快会出现作为答案。

为了查看两者之间的字母频率模式是否存在差异,让我们也生成那个列表。我使用了 size-35 的 aspell 词汇表作为起点,因为它应该包含更多常见的英语词汇。

rawwords35 = readdlm("data/scowl_american_words_35.txt",header=false,String)
length(rawwords35)
50043

dictwords35 = String[];
for word in rawwords35
    # println(word)
    if (length(word)>0 && !(occursin("\'",word) 
          || occursin("-",word) 
          || occursin(" ",word)
          || (word[1]<'a' || word[1]>'z')
          ))
        push!(dictwords35,word)
    end
end
totalWords = length(dictwords35)
39142

这个列表包含 39K 个词汇,而 85 大小的词汇表则包含 343K 个词汇。让我们只筛选出 5 个字母的词汇。

answerlist = filter(x->(length(x)==5), dictwords)
length(answerlist)
3467

Wardle 和 Shah 还从他们的答案词汇表中删除了所有的复数词汇。像 CROPS、ROCKS、DROPS 等复数形式的词汇已经从答案列表中删除,而它们仍然存在于猜测列表中。让我们大致看看有多少复数词汇。

answerlist = filter(x->(x[5]!='s' ||  x[4]=='s'), answerlist)

length(answerlist)
 2282

3467-2282
 1185

在猜测词汇表中,35% 或 1185 个词汇是复数!CROSS、CLASS 等是答案列表中的一些代表性词汇,它们没有 3 或 4 个字母的单数变体(这些词汇已经在 Wordle 中出现过,因此这里没有剧透)。

这解释了为什么最近我的好朋友 Sirisha 在那天的单词上表现得与平时不同,最终用 6 次尝试解决了它。她是我们中学小组的活跃成员,该小组每天解决 Wordle,并且通常表现很好。然而,她觉得选择太多,不知道答案列表中已经去除了 30% 的复数形式⁸。现在掌握了这些信息,她不太可能在下次 S 在第 5 位标记为绿色时被难住,我们也是如此。

在我们开始分析答案列表的模式之前,让我们看看目前拥有的三组字母频率;完整的 85 个单词列表、5 个字母的猜测单词列表和 5 个字母的答案单词列表。

让我们编写一个辅助函数来加载单词列表,

function letterFrequency(words)

    # count total words in the file
    totalWords = length(words);

    # initialize freq vector
    charFreq = zeros(Int64,26)
    for word in words
        for char in unique(word)
            if (char>='a' && char<='z')
                charFreq[char-'a'+1] += 1
            end
        end
    end

    # element wise devide by totalWords and return
    return charFreq ./ (totalWords/100.0) ;
end

函数准备好了,我们加载它吧。

cfDictwords = letterFrequency(dictwords)
cfGuesslist  = letterFrequency(guesslist)
cfAnswerlist = letterFrequency(answerlist)

现在是比较图,

plot((1:26, 'A':'Z'), hcat(cfDictwords, cfGuesslist, cfAnswerlist), 
    label=["Dict Words" "Guess list" "Answer list"],
    yticks=([0:5:70;],["$(x)%" for x=0:5:70]),
    framestyle = :box,
    xlabel="ALPHABET",ylabel="% OF APPEARANCE",
    xguidefontsize=8, yguidefontsize=8,
    margin=5mm, ylims = (0,75),
    xticks=(1:26, 'A':'Z'),
    size=(800,420)
)

savefig("pics/7-compare-guess-answer.png")

我将频率串联起来,然后在折线图上绘制它们。

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

来自整个数据集、猜测和答案列表的字母频率。图片由作者提供。

在上面的图表中,每个数据点表示包含特定字母的单词百分比。例如,对于 A,47% 的所有英语单词中至少有一个 A。相比之下,猜测和答案单词列表中的单词中只有 37% 包含 A。频率百分比之间的差异可能是由于英语单词列表中的长单词,因为像 A、E、I、R 等字母在长单词中出现的频率较高。我们已经知道,答案单词列表中的 S 频率低得多,这是因为去除了复数形式。

查看猜测列表可能会给出不正确的 S 模式。因此,让我们专注于答案列表,并更详细地分析其趋势。

字母频率

我们现在将按顺序绘制字母频率,以便在图表的顶部显示出现频率最高的字母。

listOrder = sortperm(cfAnswerlist, rev=true)

bar(cfAnswerlist[listOrder],
      orientation=:h,
      yticks=(1:26, ('A':'Z')[listOrder]),
      xlabel=("% of APPREARANCE"),
      ylabel=("ALPHABET"),
      yflip=true, framestyle = :box,
      xticks=([0:5:55;],["$(x)%" for x=0:5:50]),
      xguidefontsize=8, yguidefontsize=8,
      margin=5mm, xlims = (0,55),
      legend=:none,
      ytickfont = font(6,"Arial"),
      lc=(:black), mc=(:black),
      fill=(:orange), fillalpha=(0.5),
      size=(800,420))

savefig("pics/8-letterfrequency.png")

Julia 的 sortperm 返回一个排列向量 I,将 cfAnswerlist[I] 按排序顺序排列。条形图获取 cfAnswerlist[listOrder],这是一个频率值的排序列表。bar 函数的 yticks 参数被分配为 ('A':'Z')[listOrder],这将数组从 A 到 Z 按频率递减的顺序排序。

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

答案列表中字母的出现频率。图片由作者提供。

E 是最频繁的字母,至少出现在 52% 的答案单词中。A、R、O、I 和 T 是接下来的五个。ORATE 似乎是一个好的起始词。

V、X、Z、Q 和 J 是出现最少的字母,其中 J 出现在答案列表中的比例稍微超过 1%。我还没有验证这一点,但分析指出 XYLYL 是最差的起始词⁹ 看起来是对的。

如何使用分布

答案列表中字母的分布可以非常有用进行研究和记忆。假设你使用了第一个猜测单词 ‘ORATE’ 或者(我另一个喜欢的)‘AROSE’。对于 ORATE,你得到了 3 个黄色字母 R、A 和 E。下一个选择可以是 LASER、WAVER 等。我假设你是在使用“困难模式”进行游戏。上面的图表告诉我们,L 和 S 比 W 和 V 更可能出现在答案单词中。实际上,可能性大约是 W 和 V 的 3 倍。因此,我的猜测是 LASER。

作为另一个例子,如果你在 AROSE 上没有得到任何匹配,我通常会尝试 ‘UNWIT’,然后是 GLYPH。如果 ORATE 没有匹配,SULCI 可以是一个不错的第二个猜测,然后是 NYMPH。

作为一种策略,你可以记住频率的填充顺序;

EAROI TLSDN CUHYP GMBKW FVXZQ J

或者,记住这三个单词 ORATE、SULCI 和 NYMPH(以及 D)。当你在 ORATE 中得到一个匹配项,比如 E,你可以开始从 SULCI 和 NYMPH 中挑选字母作为你的下一个单词(这里可以想到 CLUES)。

其他研究者也推荐了类似的策略,即选择三个字母互不重叠的单词。Nisansa de Silva¹⁰ 推荐了 RAISE、CLOUT 和 NYMPH。我个人也曾使用过 AROSE、UNWIT 和 GLYPH。

结论

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

照片由 Sigmund 拍摄,发布于 Unsplash

我们已经从两个方面分析了游戏 Wordle,使用 Julia 来分析单词。首先,我试图证明游戏中选择五个字母的合理性。结果发现,我们的口语和书面表达中,五个字母的单词使用频率高于其他长度的单词。因此,选择五个字母的单词对于游戏是很有意义的。

其次,我们首先查看了整个英语单词列表,然后查看了五个字母猜测和浓缩答案列表中字母的频率。字母的分布图可以用来更有效地猜测单词。我注意到记住 ORATE、SULCI 和 NYMPH 可以帮助我们猜测更可能返回更多匹配的单词。使用这些字母作为指南将有助于提高你的游戏水平。

记住这些技巧,祝你明天的 Wordle 游戏一切顺利!

参考文献

  1. 弗吉尼亚大学公共事务米勒中心。“总统演讲:可下载数据。”访问于 2022 年 3 月 17 日。 millercenter.org/presidential-speeches-downloadable-data

  2. Ravi Parikh,《各种语言中的单词长度分布》,网站 www.ravi.io/language-word-lengths

  3. Reginald Smith,《不同单词长度频率:分布与符号熵》,arxiv.org/ftp/arxiv/papers/1207/1207.2334.pdf

  4. Liston Tellis,“正态分布——钟形曲线”,Medium,2020 年 7 月 5 日,medium.com/analytics-vidhya/normal-distribution-the-bell-curve-4f4a5fc2caaa

  5. Ann Wylie,“在线词语的最佳长度是什么?”,Ann Wylie 的写作技巧博客,www.wyliecomm.com/2021/11/whats-the-best-length-of-a-word-online/

  6. Ste Knight,“提高你得分的 12 个 Wordle 技巧和窍门”,Make Use Of,2022 年 9 月,www.makeuseof.com/wordle-tips-hints-tricks/

  7. Barry Smyth,“Wordle 中的大数据”,2022 年 6 月,Towards Data Science,towardsdatascience.com/big-data-in-little-wordle-306d5502c4d9

  8. Jonathan Lee,“纽约时报终于对 Wordle 做出改变”,华盛顿邮报,2022 年 11 月,www.washingtonpost.com/video-games/2022/11/07/wordle-new-answers-new-york-times-update/

  9. Brittany Alva,“根据科学,Wordle 中最糟糕的起始词”,SVG,2022 年 3 月,www.svg.com/751712/the-worst-starting-word-in-wordle-according-to-science/

  10. Nisansa de Silva,“使用字符统计选择 Wordle 种子词”,2022 年 2 月,arXiv:2202.03457arxiv.org/pdf/2202.03457.pdf

在邮政编码级别处理地理空间数据

原文:towardsdatascience.com/working-with-geospatial-data-at-the-postcode-level-3c9f79d866b3?source=collection_archive---------9-----------------------#2023-07-06

如何将“点”邮政编码与区域普查数据关联起来

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

·

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

在一些国家,邮政编码是点或路线,而非区域。例如,加拿大邮政编码的最后三位数字对应于本地投递单元,可能对应于街道一侧或乡村路线上的房屋。类似地,英国的邮政编码形式为“YO8 9UR”。这可以小到伦敦的一座建筑。在 5+4 的美国邮政编码中,最后四位数字决定了一个邮政投递路线(即一组地址),而不是一个区域。与普遍认为的相反,美国 5 位数的邮政编码也不是区域——它们只是 5+4 邮政路线的集合,通常由一个邮局提供服务。

法国作为公制系统的发源地,非常逻辑化。在法国,邮政编码对应于一个区域——最后两位数字对应于区,因此 75008 对应于巴黎的第八区,确实是一个区域。然而,邮件配送路线可能并不最佳。

由于人们和商店有地址,而这些地址有相关的邮政编码,大多数消费者数据都是以邮政编码级别报告的。为了进行区域覆盖、市场份额等计算,有必要确定邮政编码的区域范围。这在法国很容易,但在那些邮政编码是邮政路线而不是区域的国家中则会很困难。

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

英国邮政编码是皇家邮政的配送地址,而不是区域。加拿大和美国也是如此。照片由 Monty Allen 拍摄,发布在 Unsplash

由于他们的邮政编码是邮件配送地址,因此可以绘制无限多个多边形来将英国/加拿大/美国划分为有效的邮政编码“区域”。这就是为什么英国的人口统计数据由其国家统计局(ONS)按照行政区域(如县)发布,而不是邮政编码。美国人口普查以“邮政编码统计区”(ZCTA)级别发布数据,而美国投票数据则按县级发布。在处理英国/加拿大/美国的数据时,你通常会遇到地址(即点)和在区域内收集的空间数据的混合。你如何将这些数据关联起来?

为了说明这一点,我将在本文中将英国邮政编码数据和人口普查数据结合起来。

下载链接

如果你很急,可以从 github.com/lakshmanok/lakblogs/tree/main/uk_postcode 下载我分析的结果——那里有几个 CSV 文件,包含你可能需要的数据。

ukpopulation.csv.gz 具有以下列:

postcode,latitude,longitude,area_code,area_name,all_persons,females,males

ukpostcodes.csv.gz 有一列额外的内容——每个邮政编码的 WKT 格式的多边形:

postcode,latitude,longitude,area_code,area_name,all_persons,females,males,geometry_wkt 

请注意,数据或代码的使用风险由你自行承担——它是以“按原样”基础分发的,没有任何形式的明示或暗示的担保或条件。

在本文中,我将详细介绍我如何在那个 GitHub 仓库中创建数据集。你可以通过使用笔记本 uk_postcodes.ipynb 跟随我一起操作。

原始数据

我们从三个在 英国开放政府许可证 下发布的原始数据源开始:

  1. Free Map Tools 提供了一个可下载文件,其中包含每个邮政编码的中心点纬度和经度。这对于空间分析还不够,因为你不能仅用一个点做诸如 ST_INTERSECTS 的操作。但这是一个好的开始。

  2. ONS 已发布普查数据,覆盖如“达勒姆郡”这样的区域。这些不是邮政编码,通常是区、县或地区级别的数据。

  3. 英国统计局已经帮助识别了每个邮政编码相关的区域。每个邮政编码存在于不同目的和分辨率定义的区域中。这包括但不限于教区、区、县和所在地区。为完整起见,这里是所有其他可用的关联(根据你的空间数据集,你可能需要其他列):

pcd,pcd2,pcds,dointr,doterm,oscty,ced,oslaua,osward,parish,usertype,oseast1m,osnrth1m,osgrdind,oshlthau,nhser,ctry,rgn,streg,pcon,eer,teclec,ttwa,pct,itl,statsward,oa01,casward,park,lsoa01,msoa01,ur01ind,oac01,oa11,lsoa11,msoa11,wz11,sicbl,bua11,buasd11,ru11ind,oac11,lat,long,lep1,lep2,pfa,imd,calncv,icb,oa21,lsoa21,msoa21

我的笔记本使用 wget 下载数据文件:

mkdir -p indata
cd indata
if [ ! -f census2021.xlsx ]; then
    wget -O census2021.xlsx https://www.ons.gov.uk/file?uri=/peoplepopulationandcommunity/populationandmigration/populationestimates/datasets/populationandhouseholdestimatesenglandandwalescensus2021/census2021/census2021firstresultsenglandwales1.xlsx
fi

读取数据

直接将 CSV 读取到 Pandas 中很简单:

import pandas as pd
pd.read_csv(POSTCODES_CSV)

这为我提供了每个邮政编码的中心点纬度和经度:

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

每个邮政编码的中心点纬度和经度

有许多包允许你将 Excel 文件读取到 Pandas 中,但我决定使用 DuckDB,因为我会在笔记本中用 SQL 连接这三个数据集:

import duckdb
conn = duckdb.connect()

ukpop = conn.execute(f"""
install spatial;
load spatial;

SELECT 
  * 
FROM
st_read('{POPULATION_XLS}', layer='P01')
""").df()

这个 Excel 文件有 7 行标题信息,我可以删除这些信息。我还将列重命名为有意义的变量:

ukpop = ukpop.drop(range(0,7), axis=0).rename(columns={
    'Field1': 'area_code', 
    'Field2': 'area_name', 
    'Field3': 'all_persons', 
    'Field4': 'females', 
    'Field5': 'males'
})

那是名为 P01 的表格。请注意,P04 表格有一个人口密度信息,但它没有用,因为人口在区域编码上并不均匀分布。我们将推导出每个邮政编码的人口。

我将这些数据写入 CSV 文件,以便我可以从 DuckDB 中轻松读取它。

ukpop.to_csv("temp/ukpop.csv", index=False)

同样,我从英国统计局文件中提取所需的列并将其写入 CSV 文件:

onspd = pd.read_csv(ONSPD_CSV)
onspd = onspd[['pcd', 'oslaua', 'osward', 'parish']]
onspd.to_csv("temp/onspd.csv", index=False)

关联数据

现在,我们可以使用 DuckDB 将这三个准备好的数据集连接起来,以获取每个邮政编码的密度。为什么使用 DuckDB?即使我可以在 Pandas 中进行连接,但我发现 SQL 更加易读。此外,这也给了我一个使用新热门工具的理由。

我通过首先使用 read_csv_auto 将数据集读取到 DuckB 中来连接这些数据集。然后,我查找邮政编码所在的区、教区、县,并找到人口密度数据报告的区域(教区、区或县):

 /* pcd,oslaua,osward,parish */
WITH onspd AS (
    SELECT 
      * 
    FROM
    read_csv_auto('temp/onspd.csv', header=True)
),

/* area_code,area_name,all_persons,females,males */
ukpop AS (
    SELECT 
      * 
    FROM
    read_csv_auto('temp/ukpop.csv', header=True)
),

/* id,postcode,latitude,longitude */
postcodes AS (
    SELECT 
      * 
    FROM
    read_csv_auto('indata/ukpostcodes.csv', header=True)
),

/* postcode, area_code */
postcode_to_areacode AS (
  SELECT 
    pcd AS postcode,
    ANY_VALUE(area_code) as area_code
  FROM onspd
  JOIN ukpop 
  ON (area_code = oslaua OR area_code = osward OR area_code = parish)
  GROUP BY pcd
)

SELECT
  postcode, latitude, longitude, /* from postcodes */
  area_code, area_name, /* from ukpop */
  all_persons,females,males /* from ukpop, but has to be spatially corrected */
FROM postcode_to_areacode
JOIN postcodes USING (postcode)
JOIN ukpop USING (area_code) 

请注意,空间数量是对应于整个区域的标量,而不是邮政编码。它们必须在邮政编码之间进行分配。

在邮政编码之间分配区域数量

all_persons、females、males 对应于整个区域,而不是特定的邮政编码。我们可以根据邮政编码的面积按比例进行,但有无数种多边形可以适应邮政编码,正如我们稍后将看到的,靠近公园和湖泊的邮政编码的区域范围有点不确定。所以我们将做一些简单的处理,给我们一个唯一的答案——我们将把标量值均匀分配到该区域内的所有邮政编码中!这听起来并不奇怪——在高密度社区中,邮政编码更多,因此在邮政编码之间均等分配类似于按人口密度分配标量量。

npostcodes = ukpop.groupby('area_code')['postcode'].count()
for col in ['females', 'males', 'all_persons']:
    ukpop[col] = ukpop.apply(lambda row:  row[col]/npostcodes[row['area_code']], axis=1)

目前,我们拥有每个邮政编码的数量——这是我们需要的关联:

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

所以,写出来:

ukpop.to_csv("ukpopulation.csv", index=False)

邮政编码的区域范围

对于许多分析,我们希望邮政编码不是点,而是区域。尽管有无数种多边形可以将英国划分为每个多边形中仅有一个邮政编码质心,但确实存在一个“最佳”多边形的概念。那就是沃罗诺伊划分,它将区域划分为每个点都属于距离它最近的邮政编码:

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

20 个点的沃罗诺伊分析,来自 Wikipedia

为了计算这一点,我们可以使用 scipy:

import numpy as np
from scipy.spatial import Voronoi, voronoi_plot_2d

points = df[['latitude', 'longitude']].to_numpy()
vor = Voronoi(points)

我在这里假设区域足够小,以至于从纬度和经度计算的地理距离与欧几里得距离之间的差异不大。英国的邮政编码足够小,符合这一情况。

结果被组织成这样的结构:每个点都有一个由一组顶点组成的区域。我们可以为每个点创建一个 WKT 多边形字符串,方法如下:

def make_polygon(point_no):
    region_no = vor.point_region[point_no]
    region = vor.regions[region_no]
    if len(region) >= 3:
        # close the ring
        closed_region = region.copy()
        closed_region.append(closed_region[0])
        # create a WKT of the points
        polygon = "POLYGON ((" + ','.join([ f"{vor.vertices[v][1]} {vor.vertices[v][0]}" for v in closed_region]) + "))"
        return polygon
    else:
        return None

这是一个示例结果:

POLYGON ((-0.32491691953979235 51.7393550489536,-0.32527234008402217 51.73948967705648,-0.32515738641624575 51.73987124225542,-0.3241646650618929 51.74087626616231,-0.3215663358407994 51.742660660928614,-0.32145633473723817 51.742228570262824,-0.32491691953979235 51.7393550489536))

我们可以创建一个 GeoDataFrame 并绘制邮政编码的子集:

import geopandas as gpd
from shapely import wkt
df['geometry'] = gpd.GeoSeries.from_wkt(df['geometry_wkt'])
gdf = gpd.GeoDataFrame(df, geometry='geometry')

gdf[gdf['area_name'] == 'St Albans'].plot()

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

圣奥尔本斯的邮政编码空间范围。由作者生成的图像。

这是伯明翰:

gdf[gdf['area_name'] == 'Birmingham'].plot()

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

伯明翰的邮政编码空间范围。由作者生成的图像。

未开发区域

注意顶部的喇叭和中间的大块蓝色区域。发生了什么?让我们在 Google Maps 中查看伯明翰:

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

伯明翰,作者的 Google Maps 截图。

注意到公园区域了吗?皇家邮政不需要向那里任何人投递邮件。所以那里没有邮政编码。因此,附近的邮政编码被“扩展”到这些区域。这会导致空间计算中的问题,因为这些邮政编码看起来会比实际要大得多。

为了解决这个问题,我将采取一种相当启发式的方法。我将把英国划分为 0.01x0.01(大约 1 平方公里)分辨率的网格单元,并找到没有邮政编码的网格单元:

GRIDRES = 0.01
min_lat, max_lat = np.round(min(df['latitude']), 2) - GRIDRES, max(df['latitude']) + GRIDRES
min_lon, max_lon = np.round(min(df['longitude']), 2) - GRIDRES, max(df['longitude']) + GRIDRES
print(min_lat, max_lat, min_lon, max_lon)

npostcodes = np.zeros([ int(1+(max_lat-min_lat)/GRIDRES), int(1+(max_lon-min_lon)/GRIDRES) ])
for point in points:
    latno = int((point[0] - min_lat)/GRIDRES)
    lonno = int((point[1] - min_lon)/GRIDRES)
    npostcodes[latno, lonno] += 1

unpop = []
for latno in range(len(npostcodes)):
    for lonno in range(len(npostcodes[latno])):
        if npostcodes[latno][lonno] == 0:
            # no one lives here.
            # make up a postcode for this location
            # postcode latitude longitude area_code area_name persons_per_sqkm
            unpop.append({
                'postcode': f'UNPOP {latno}x{lonno}',
                'latitude': min_lat + latno * 0.01,
                'longitude': min_lon + lonno * 0.01,
                'all_persons': 0
            }) 

我们将在这些无人居住的网格单元的中心创建虚拟邮政编码,并将零人口密度分配给这些编码。将这些虚拟邮政编码添加到实际的邮政编码中,然后重复 Voronoi 分析:

df2 = pd.concat([df, pd.DataFrame.from_records(unpop)])
points = df2[['latitude', 'longitude']].to_numpy()
vor = Voronoi(points)
df2['geometry_wkt'] = [make_polygon(x) for x in range(len(vor.point_region))]
df2['geometry'] = gpd.GeoSeries.from_wkt(df2['geometry_wkt'])
gdf = gpd.GeoDataFrame(df2, geometry='geometry')

现在,当我们绘制伯明翰时,我们得到的效果要好得多:

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

伯明翰的邮政编码多边形。图片由作者生成

就是这个数据框,我将其保存为第二个 CSV 文件:

gdf.to_csv("ukpostcodes.csv", index=False)

[可选] 加载到 BigQuery

我们可以将 CSV 文件加载到 BigQuery 中并进行一些空间分析,但最好是让 BigQuery 首先将最后一个字符串列解析为几何图形,并根据邮政编码对数据进行聚类:

CREATE OR REPLACE TABLE uk_public_data.postcode_popgeo2
CLUSTER BY postcode
AS

SELECT 
  * EXCEPT(geometry_wkt),
  SAFE.ST_GEOGFROMTEXT(geometry_wkt, make_valid=>TRUE) AS geometry,
FROM uk_public_data.postcode_popgeo

现在,我们可以轻松地查询它。例如,我们可以对邮政编码使用 ST_AREA:

SELECT 
  COUNT(*) AS num_postcodes,
  SUM(ST_AREA(geometry))/1e6 AS total_area,
  SUM(all_persons) AS population,
  area_name
 FROM uk_public_data.postcode_popgeo2
 GROUP BY area_name
 ORDER BY population DESC

总结

空间分析通常需要面积扩展,而不仅仅是点位置。在邮政编码为点/路线的国家中,有无数种方法可以生成邮政编码的多边形空间扩展。一种合理的方法是使用 Voronoi 区域来创建包含这些邮政编码的多边形。然而,如果这样做,你会在湖泊或公园附近得到不自然的大多边形,因为邮局不在那里投递邮件。为了解决这个问题,还需要对国家进行网格化,并在无人居住的网格单元中创建虚拟邮政编码。在这篇文章中,我演示了如何在英国进行此操作。相关的笔记本可以适应其他地方。

下一步

  1. 查看完整的代码 github.com/lakshmanok/lakblogs/blob/main/uk_postcode/uk_postcodes.ipynb

  2. github.com/lakshmanok/lakblogs/tree/main/uk_postcode 下载数据

使用 Hugging Face 数据集

原文:towardsdatascience.com/working-with-hugging-face-datasets-bba14dd8da68

了解如何访问 Hugging Face Hub 上的数据集,以及如何使用 DuckDB 和 Datasets 库远程加载它们

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

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

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

照片由 Lars Kienle 提供,来源于 Unsplash

作为一个 AI 平台,Hugging Face 构建、训练并部署最先进的开源机器学习模型。除了托管所有这些训练好的模型外,Hugging Face 还托管数据集(huggingface.co/datasets),你可以将它们用于自己的项目。

在这篇文章中,我将展示如何访问 Hugging Face 中的数据集,并如何通过编程将它们下载到本地计算机上。具体来说,我将展示如何:

  • 使用 DuckDB 对 httpfs 的支持远程加载数据集

  • 使用 Hugging Face 的 Datasets 库流式传输数据集

Hugging Face 数据集服务器

Hugging Face 数据集服务器是一个轻量级的 Web API,用于可视化存储在 Hugging Face Hub 上的各种数据集。你可以使用提供的 REST API 查询存储在 Hugging Face Hub 上的数据集。接下来的部分提供了关于如何使用 API 的简短教程,访问地址为 [datasets-server.huggingface.co/](https://datasets-server.huggingface.co/)

获取 Hub 上托管的数据集列表

要获取可以从 Hugging Face 检索的数据集列表,请使用以下语句和valid端点:

$ curl -X GET "https://datasets-server.huggingface.co/valid"

你将看到如下的 JSON 结果:

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

可以正常工作的数据集列在结果中valid键的值中。上面的有效数据集示例是0-hero/OIG-small-chip2

验证数据集

要验证数据集,请使用以下语句,结合is-valid端点和dataset参数:

$ curl -X GET "https://datasets-server.huggingface.co/is-valid?dataset=0-hero/OIG-small-chip2"

如果数据集有效,你将看到以下结果:

{"valid":true}

获取数据集的配置和分割列表

一个数据集通常有splits(训练集、验证集和测试集)。它们也可能有配置——在更大数据集中的子数据集。

配置对于多语言语音数据集是常见的。有关分割的更多细节,请访问:huggingface.co/docs/datasets-server/splits

要获取数据集的分割,请使用以下语句,结合splits端点和dataset参数:

$ curl -X GET "https://datasets-server.huggingface.co/splits?dataset=0-hero/OIG-small-chip2"

以下结果将被返回:

{
  "splits": [
    {
      "dataset":"0-hero/OIG-small-chip2",
      "config":"0-hero--OIG-small-chip2",
      "split":"train"
    }
  ],
  "pending":[],
  "failed":[]
}

对于该数据集,只有一个train分割。

这是一个数据集(“duorc”)的示例,具有多个分割和配置:

{
  "splits": [
    {
      "dataset": "duorc",
      "config": "SelfRC",
      "split": "train",
      "num_bytes": 239852925,
      "num_examples": 60721
    },
    {
      "dataset": "duorc",
      "config": "SelfRC",
      "split": "validation",
      "num_bytes": 51662575,
      "num_examples": 12961
    },
    {
      "dataset": "duorc",
      "config": "SelfRC",
      "split": "test",
      "num_bytes": 49142766,
      "num_examples": 12559
    },
    {
      "dataset": "duorc",
      "config": "ParaphraseRC",
      "split": "train",
      "num_bytes": 496683105,
      "num_examples": 69524
    },
    {
      "dataset": "duorc",
      "config": "ParaphraseRC",
      "split": "validation",
      "num_bytes": 106510545,
      "num_examples": 15591
    },
    {
      "dataset": "duorc",
      "config": "ParaphraseRC",
      "split": "test",
      "num_bytes": 115215816,
      "num_examples": 15857
    }
  ]
}

获取前 100 行

通常,你可能希望检索数据集的子集进行检查,而不是下载所有数据。以下语句使用first-rows端点,结合datasetconfigsplit参数,从指定数据集中提取前 100 行:

$ curl -X GET "https://datasets-server.huggingface.co/first-rows?dataset=0-hero/OIG-small-chip2&config=0-hero--OIG-small-chip2&split=train"

JSON 结果将返回指定数据集的前 100 行:

{
 "dataset": "0-hero/OIG-small-chip2",
 "config": "0-hero--OIG-small-chip2",
 "split": "train",
 "features": [{
  "feature_idx": 0,
  "name": "user",
  "type": {
   "dtype": "string",
   "_type": "Value"
  }
 }, {
  "feature_idx": 1,
  "name": "chip2",
  "type": {
   "dtype": "string",
   "_type": "Value"
  }
 }],
 "rows": [{
  "row_idx": 0,
  "row": {
   "user": "I've heard that it's a good idea to have a will. What is a will?\n\n",
   "chip2": "A will is a legal document that specifies how your property should be distributed after you die. It can also specify who should care for any children or other dependents you may have. It's important to make sure that your will is valid and up-to-date, since the laws governing wills vary from state to state."
  },
  "truncated_cells": []
 }, {
  "row_idx": 1,
  "row": {
   "user": "How do I find a job?\n\n",
   "chip2": "The best way to find a job is to create a strong resume and cover letter that highlights your skills, experience, and qualifications. You can also search online for job openings in your area of expertise or contact local employers directly to inquire about available positions. Networking with people in the same field or industry can also be helpful in finding employment opportunities."
  },
  "truncated_cells": []
 }, {
  "row_idx": 2,
  "row": {
   "user": "Produce a detailed written description of a gloomy scene inside of a mansion with a domed ceiling, pillars, and a door.\n\n",
   "chip2": "Circular in shape, the floor is completely made from square-cut sections of black marble. Large, blackened marble pillars line the walls, ending with a domed glass ceiling high overhead. A narrow gap on the far side of the room leads to a sweeping stairwell with a mithglin-plated door beneath it."
  },
  "truncated_cells": []
 }, {

 ...

 }, {
  "row_idx": 98,
  "row": {
   "user": "I need to learn how to budget my money better. What advice can you give me?\n\n",
   "chip2": "One of the best ways to budget your money better is to make a budget and track your spending. Start by writing down all your sources of income and all your expenses. Go over each item closely to determine what you can cut back on. Prioritize the things that need to be paid first, such as rent and utilities, and try to make sure you’re setting aside some money for savings. Additionally, setting spending limits for yourself can help you stay on track. Finally, regularly review your budget to make sure you are staying within your budgeting goals."
  },
  "truncated_cells": []
 }, {
  "row_idx": 99,
  "row": {
   "user": "Produce a detailed written description of a serene scene outside a mountain road with monir trees and a boulder.\n\n",
   "chip2": "A large granite boulder, apparently fallen from the mountain's craggy cliffs in some distant age, lies beside the cobblestoned road. Surrounded by a thick stand of monir trees, the boulder provides a convenient resting place for travellers, and a brief respite from the rigors of the journey."
  },
  "truncated_cells": []
 }]
}

获取数据集的切片

你还可以获取数据集的切片。以下语句使用rows端点,结合datasetconfigsplitoffsetlength参数,从指定数据集的第三行开始提取三行:

$ curl -X GET "https://datasets-server.huggingface.co/rows?dataset=0-hero/OIG-small-chip2&config=0-hero--OIG-small-chip2&split=train&offset=2&length=3"

结果将如下所示:

{
 "features": [{
  "feature_idx": 0,
  "name": "user",
  "type": {
   "dtype": "string",
   "_type": "Value"
  }
 }, {
  "feature_idx": 1,
  "name": "chip2",
  "type": {
   "dtype": "string",
   "_type": "Value"
  }
 }],
 "rows": [{
  "row_idx": 2,
  "row": {
   "user": "Produce a detailed written description of a gloomy scene inside of a mansion with a domed ceiling, pillars, and a door.\n\n",
   "chip2": "Circular in shape, the floor is completely made from square-cut sections of black marble. Large, blackened marble pillars line the walls, ending with a domed glass ceiling high overhead. A narrow gap on the far side of the room leads to a sweeping stairwell with a mithglin-plated door beneath it."
  },
  "truncated_cells": []
 }, {
  "row_idx": 3,
  "row": {
   "user": "I have a sprained ankle and need to know what I can do to help it heal faster.\n\n",
   "chip2": "You may want to try some ice therapy, which will help reduce swelling and inflammation. Additionally, you could use an elastic bandage or compression wrap to provide additional support for your ankle. It's also important to stay off of it as much as possible, so that it can heal properly."
  },
  "truncated_cells": []
 }, {
  "row_idx": 4,
  "row": {
   "user": "How do you roast a turkey?\n\n",
   "chip2": "To roast a turkey, start by preheating your oven to 350 degrees F. Place the turkey in a roasting pan, and rub it with butter or olive oil. Sprinkle with salt and pepper, and any other herbs or seasonings you like. Place the turkey in the oven and cook for approximately 20 minutes per pound, or until the internal temperature registers 165 degrees. Once the turkey is cooked, remove it from the oven and let it sit for 10-15 minutes before carving."
  },
  "truncated_cells": []
 }]
}

获取数据集的 Parquet 文件

虽然 Hugging Face Hub 上的数据集可以以多种格式(CSV、JSONL 等)发布,但Datasets server会自动将所有公共数据集转换为Parquet格式。Parquet 格式提供了显著的性能提升,特别是对于大型数据集。稍后的章节将对此进行演示。

Apache Parquet 是一种文件格式,旨在支持复杂数据的快速处理。有关 Parquet 的更多信息,请阅读我之前的文章:

## 仍在以 CSV 格式保存数据?试试这些其他选项

了解如何以不同格式(CSV、压缩、Pickle 和 Parquet)保存数据,以节省存储空间并减少……

towardsdatascience.com

要以 Parquet 格式加载数据集,请使用以下语句,结合parquet端点和dataset参数:

$ curl -X GET "https://datasets-server.huggingface.co/parquet?dataset=0-hero/OIG-small-chip2" 

上述语句返回以下 JSON 结果:

{
 "parquet_files": [{
  "dataset": "0-hero/OIG-small-chip2",
  "config": "0-hero--OIG-small-chip2",
  "split": "train",
  "url": "https://huggingface.co/datasets/0-hero/OIG-small-chip2/resolve/refs%2Fconvert%2Fparquet/0-hero--OIG-small-chip2/parquet-train.parquet",
  "filename": "parquet-train.parquet",
  "size": 51736759
 }],
 "pending": [],
 "failed": []
}

特别是,url 键的值指定了你可以下载 Parquet 格式数据集的位置,在这个例子中是 [huggingface.co/datasets/0-hero/OIG-small-chip2/resolve/refs%2Fconvert%2Fparquet/0-hero–OIG-small-chip2/parquet-train.parquet](https://huggingface.co/datasets/0-hero/OIG-small-chip2/resolve/refs%2Fconvert%2Fparquet/0-hero--OIG-small-chip2/parquet-train.parquet)

程序化下载数据集

现在你已经了解了如何使用 Datasets server REST API,让我们看看如何程序化地下载数据集。

在 Python 中,最简单的方法是使用 requests 库:

import requests

r = requests.get("https://datasets-server.huggingface.co/parquet?dataset=0-hero/OIG-small-chip2")
j = r.json()

print(j)

json() 函数的结果是一个 Python 字典:

{
  'parquet_files': [
    {
      'dataset': '0-hero/OIG-small-chip2',
      'config': '0-hero--OIG-small-chip2', 
      'split': 'train', 
      'url': 'https://huggingface.co/datasets/0-hero/OIG-small-chip2/resolve/refs%2Fconvert%2Fparquet/0-hero--OIG-small-chip2/parquet-train.parquet', 
      'filename': 'parquet-train.parquet', 
      'size': 51736759
    }
  ], 
  'pending': [], 
  'failed': []
}

利用这个字典结果,你可以使用列表推导式来查找 Parquet 格式的数据集的 URL:

urls = [f['url'] for f in j['parquet_files'] if f['split'] == 'train']
urls

urls 变量是一个包含训练集下的数据集 URL 列表的列表:

['https://huggingface.co/datasets/0-hero/OIG-small-chip2/resolve/refs%2Fconvert%2Fparquet/0-hero--OIG-small-chip2/parquet-train.parquet']

使用 DuckDB 下载 Parquet 文件

如果你使用 DuckDB,你实际上可以使用 DuckDB 远程加载数据集。

如果你对 DuckDB 不太熟悉,你可以从这篇文章中了解基础知识:

[## 使用 DuckDB 进行数据分析

了解如何使用 SQL 进行数据分析

levelup.gitconnected.com](https://levelup.gitconnected.com/using-duckdb-for-data-analytics-bab3e3ff032c?source=post_page-----bba14dd8da68--------------------------------)

首先,确保你已安装 DuckDB(如果尚未安装):

!pip install duckdb 

然后,创建一个 DuckDB 实例并安装 httpfs

import duckdb

con = duckdb.connect()
con.execute("INSTALL httpfs;")
con.execute("LOAD httpfs;")

httpfs 扩展是一个可加载的扩展,实现了一个文件系统,允许读取远程/写入远程文件。

一旦 httpfs 安装并加载完成,你可以通过使用 SQL 查询从 Hugging Face Hub 加载 Parquet 数据集:

con.sql(f'''
    SELECT * from '{urls[0]}'
''').df()

上述 df() 函数将查询结果转换为 Pandas DataFrame:

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

图片由作者提供

Parquet 的一个很棒的特性是 Parquet 以列式格式存储文件。因此,如果你的查询仅请求单一列,只有该请求的列会被下载到你的计算机上:

con.sql(f'''
    SELECT "user" from '{urls[0]}'
''').df()

在上述查询中,只有“user”列被下载:

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

图片由作者提供

Parquet 特性对于大型数据集特别有用——想象一下,通过只下载你需要的列,你可以节省多少时间和空间。

在某些情况下,你甚至不需要下载数据。考虑以下查询:

con.sql(f'''
    SELECT count(*) from '{urls[0]}'
''').df()

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

图片由作者提供

不需要下载任何数据,因为这个请求可以通过读取数据集的元数据来满足。

这是另一个使用 DuckDB 下载另一个数据集(“mstz/heart_failure”)的示例:

import requests

r = requests.get("https://datasets-server.huggingface.co/parquet?dataset=mstz/heart_failure")
j = r.json()
urls = [f['url'] for f in j['parquet_files'] if f['split'] == 'train']

con.sql(f'''
    SELECT "user" from '{urls[0]}'
''').df()

这个数据集有 299 行和 13 列:

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

图片由作者提供

我们可以对age列进行一些聚合操作:

con.sql(f"""
    SELECT 
        SUM(IF(age<40,1,0)) AS 'Under 40',
        SUM(IF(age BETWEEN 40 and 49,1,0)) AS '40-49',
        SUM(IF(age BETWEEN 50 and 59,1,0)) AS '50-59',
        SUM(IF(age BETWEEN 60 and 69,1,0)) AS '60-69',
        SUM(IF(age BETWEEN 70 and 79,1,0)) AS '70-79',      
        SUM(IF(age BETWEEN 80 and 89,1,0)) AS '80-89',      
        SUM(IF(age>89,1,0)) AS 'Over 89',
    FROM '{urls[0]}'
"""
).df()

这是结果:

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

图片来源于作者

使用结果,我们还可以绘制柱状图:

con.sql(f"""
    SELECT 
        SUM(IF(age<40,1,0)) AS 'Under 40',
        SUM(IF(age BETWEEN 40 and 49,1,0)) AS '40-49',
        SUM(IF(age BETWEEN 50 and 59,1,0)) AS '50-59',
        SUM(IF(age BETWEEN 60 and 69,1,0)) AS '60-69',
        SUM(IF(age BETWEEN 70 and 79,1,0)) AS '70-79',      
        SUM(IF(age BETWEEN 80 and 89,1,0)) AS '80-89',      
        SUM(IF(age>89,1,0)) AS 'Over 89',
    FROM '{urls[0]}'
"""
).df().T.plot.bar(legend=False)

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

图片来源于作者

使用 Datasets 库

为了使与 Hugging Face 的数据工作变得简单而高效,Hugging Face 有自己的Datasets库(github.com/huggingface/datasets)。

要安装datasets库,使用pip命令:

!pip install datasets

load_dataset()函数加载指定的数据集:

from datasets import load_dataset

dataset = load_dataset('0-hero/OIG-small-chip2',
                       split='train')

当你第一次加载数据集时,整个数据集(以 Parquet 格式)会下载到你的计算机上:

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

图片来源于作者

返回的dataset的数据类型是datasets.arrow_dataset.Dataset。那你可以用它做什么呢?首先,你可以将其转换为 Pandas DataFrame:

dataset.to_pandas()

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

图片来源于作者

你也可以通过使用索引来获取数据集的第一行:

dataset[0]

这将返回数据的第一行:

{
  'user': "I've heard that it's a good idea to have a will. What is a will?\n\n",
  'chip2': "A will is a legal document that specifies how your property should be distributed after you die. It can also specify who should care for any children or other dependents you may have. It's important to make sure that your will is valid and up-to-date, since the laws governing wills vary from state to state."
}

对这个datasets.arrow_dataset.Dataset对象,你还可以做很多其他事情。我将留给你进一步探索。

流式传输数据集

同样,在处理大型数据集时,不可能在进行任何操作之前将整个数据集下载到计算机上。在前面的部分中,调用load_dataset()函数会将整个数据集下载到我的计算机上:

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

图片来源于作者

这个特定的数据集占用了 82.2MB 的磁盘空间。你可以想象更大的数据集所需的时间和磁盘空间。

幸运的是,Datasets库支持流式传输。数据集流式传输让你在不下载数据集的情况下进行操作——数据在你遍历数据集时被流式传输。要使用流式传输,在load_dataset()函数中将streaming参数设置为True

from datasets import load_dataset

dataset = load_dataset('0-hero/OIG-small-chip2',
                       split='train', 
                       streaming=True)

dataset的类型现在是datasets.iterable_dataset.IterableDataset,而不是datasets.arrow_dataset.Dataset。那么你该如何使用它呢?你可以对它使用iter()函数,这将返回一个iterator对象:

i = iter(dataset)

要获取一行,调用next()函数,它将返回迭代器中的下一个项目:

next(i)

你现在将看到第一行作为字典:

{
  'user': "I've heard that it's a good idea to have a will. What is a will?\n\n",
  'chip2': "A will is a legal document that specifies how your property should be distributed after you die. It can also specify who should care for any children or other dependents you may have. It's important to make sure that your will is valid and up-to-date, since the laws governing wills vary from state to state."
}

i调用next()函数将返回下一行:

{
  'user': 'How do I find a job?\n\n',
  'chip2': 'The best way to find a job is to create a strong resume and cover letter that highlights your skills, experience, and qualifications. You can also search online for job openings in your area of expertise or contact local employers directly to inquire about available positions. Networking with people in the same field or industry can also be helpful in finding employment opportunities.'
}

依此类推。

打乱数据集

你还可以通过对dataset变量使用shuffle()函数来打乱数据集,如下所示:

shuffled_dataset = dataset.shuffle(seed = 42, 
                                   buffer_size = 500)

在上述示例中,假设你的数据集有 10,000 行。shuffle()函数将从缓冲区的前 500 行中随机选择示例。

默认情况下,缓冲区大小为 1,000。

其他任务

你可以使用流式传输执行更多任务,例如:

  • 划分数据集

  • 交错数据集——通过在每个数据集之间交替行来组合两个数据集

  • 修改数据集的列

  • 过滤数据集

查看更多细节请访问 huggingface.co/docs/datasets/stream

如果你喜欢阅读我的文章,并且觉得它对你的职业/学习有帮助,请考虑注册成为 Medium 会员。每月 $5,即可无限访问 Medium 上的所有文章(包括我的)。如果你通过以下链接注册,我将获得少量佣金(对你没有额外费用)。你的支持意味着我可以投入更多时间撰写像这样的文章。

[## 通过我的推荐链接加入 Medium - 韦梦·李

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

weimenglee.medium.com/membership?source=post_page-----bba14dd8da68--------------------------------

摘要

在这篇文章中,我展示了如何访问存储在 Hugging Face Hub 上的数据集。由于数据集以 Parquet 格式存储,因此您可以远程访问这些数据集,而无需下载整个数据集。您可以使用 DuckDB 或 Hugging Face 提供的数据集库来访问这些数据集。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值