预测建模中的敏感性:用更少的流量购买付费客户的指南
通过定义和评估模型敏感性,发现一种经济高效的广告活动策略,并提供逐步指南和 Python 实现
·
关注 发表在 Towards Data Science · 7 分钟阅读 · 2023 年 2 月 20 日
–
图片由 Joey Kyber 提供,来源于 Unsplash
本博客文章概述了一种利用付费流量的广告策略。目标是以最小的流量获取付费客户,同时最大化效率。预测建模用于评估和提升模型在实现这一目标上的效果。通过定义和分析模型敏感性,公司可以在节省成本的同时实现预期结果。本文提供了 Python 实现和详细的逐步指南。
我们将涵盖以下内容:
· 介绍
· 理解业务中的混淆矩阵
· 与我谈 Python
· 这是完整代码
· 总结
介绍
以较少流量购买付费客户是使用付费流量进行广告的公司面临的常见挑战。目标是通过减少流量同时尽可能获得更多购买客户,从而提高效率。实现这一目标的一种方法是使用预测建模来评估和优化模型的性能。
预测建模涉及使用统计技术根据历史数据预测未来事件或结果。在此背景下,目标是预测哪些客户可能会购买,以便公司可以将广告投放目标对准这些客户。
评估预测模型性能时,可以使用混淆矩阵。混淆矩阵是一种表格,用于定义分类算法的性能,特别是在评估二分类模型时非常有用,如我们讨论的模型。矩阵比较模型的预测结果与实际结果。
评估二分类模型性能的常用指标之一是召回率。召回率是模型预测为购买客户的次数与实际购买客户的数量之比。换句话说,它衡量模型识别正例的能力,在我们的例子中即购买客户。
另一个重要的指标是阈值。阈值是将预测结果视为正例的点。提高阈值会增加假阳性的数量,从而降低精确度。而降低阈值则会增加假阴性的数量,从而降低召回率。
精确度和召回率之间的平衡被称为权衡。找到最佳阈值以最大化召回率同时最小化精确度非常重要,从而在减少流量的情况下获得尽可能多的付费客户。
在本博客文章中,我们将讨论一种以较少流量购买付费客户的策略。通过定义模型敏感性来评估模型,公司可以在实现预期结果的同时节省资金。
理解业务中的混淆矩阵
在商业预测建模中,拥有一个能够准确识别购买客户的模型至关重要,因为购买客户通常是稀有而有价值的群体。衡量分类算法准确性的一种方法是使用混淆矩阵。
混淆矩阵是一个表格,通过比较预测值和实际值,总结分类模型的性能。
二分类混淆矩阵中的四个类别是:
-
真正正例(TP):模型正确预测为正例的正例数量。
-
假正例(FP):模型错误地将负例预测为正例的数量。
-
真正负例(TN):模型正确预测为负例的负例数量。
-
假负例(FN):模型错误地将正例预测为负例的数量。
由作者创建,使用 excalidraw.com/
真正正例(TP)、假正例(FP)、假负例(FN)和真正负例(TN)。TP 表示模型预测为购买客户且预测准确的次数,而 FN 表示模型漏掉的购买客户次数。FP 表示模型预测为非购买客户但预测错误的次数,而 TN 表示模型预测为非购买客户且预测正确的次数。
由作者创建,使用 editor.codecogs.com/
召回率指标,也称为敏感性或真正正例率,衡量模型正确识别的实际购买客户的比例。计算公式为 TP/(TP+FN),表示模型预测为购买客户且预测正确的次数,除以实际购买客户的总数。
由作者创建,使用 editor.codecogs.com/
除了测量模型对购买客户的敏感性外,混淆矩阵还可以提供有关从特定阈值可以预期的流量和购买客户的洞察。通过计算 (FN + TP)/(TN + FP + FN + TP),可以确定在特定阈值下,模型将正确识别的购买客户占所有客户的百分比。
然而,需要注意的是,提高阈值会增加假正例,从而降低精确度。平衡模型的敏感性和精确度的一种方法是设置所需的支付客户百分比,并根据特定模型计算达到该百分比的阈值。
理解混淆矩阵及其指标可以为商业中预测模型的表现提供宝贵的见解,特别是在识别稀有且有价值的客户群体时。通过分析混淆矩阵,企业可以优化其模型,并做出数据驱动的决策,从而获得更好的结果。
Talk Python To Me
机器学习模型通过各种指标(如准确率、精确度和召回率)进行评估。在某些情况下,实现特定的召回水平比最大化准确率更为重要。在这篇文章中,我们将通过 Python 代码展示如何基于期望的召回水平评估模型。
问题:假设我们有一个二分类问题,我们希望预测用户是否会购买产品。数据集包含 200,000 条记录,其中 30,630 条为正例,169,070 条为负例。我们的目标是训练一个能够以高召回率预测用户是否会购买产品的模型。
解决方案:我们可以使用以下 Python 函数来评估模型在期望召回水平下的性能:
extract_threshold_given_recall(y_test, probabilities, given_recall)
该函数接受三个输入:
-
y_test:测试集的目标值
-
probabilities:测试集的预测概率
-
given_recall:期望的召回水平
该函数使用y_test
和概率计算精度-召回曲线,并返回给定召回值的阈值。
get_model_results_for_recall(model, X_test, y_test, X_train, y_train, given_recall, with_plots=True)
该函数接受六个输入:
-
model:训练好的机器学习模型
-
X_test:测试集的特征矩阵
-
y_test:测试集的目标值
-
X_train:训练集的特征矩阵
-
y_train:训练集的目标值
-
given_recall:期望的召回水平
函数首先使用模型计算测试集的预测概率。然后,它使用extract_threshold_given_recall
函数计算 ROC 曲线和期望召回值的最佳阈值。最后,它计算混淆矩阵、分类报告、FPR、AUC、准确度分数、最佳阈值和购买流量。可选地,函数还可以绘制 ROC 曲线。
输出将如下所示 👇
作者截屏
下面是完整代码:
总结
在本文中,我们看到了如何评估机器学习模型的性能,以达到所需的召回率水平。通过通过定义模型的敏感度来评估模型,公司可以在节省金钱的同时仍实现他们购买付费客户的期望结果,而不需要增加流量。我们提供了一个 Python 实现,可以帮助通过找到最大化召回率的最佳阈值来实现这一过程。最大化召回率可以减少购买未付费客户,因为召回率是衡量实际正例(即付费客户)被预测模型正确识别为正例的比例的指标。通过优化模型以最大化召回率,模型更擅长识别付费客户,这意味着公司可以避免购买不太可能产生付费客户的流量。这可以降低客户获取成本,并提高公司广告预算的效率。
句子变换器:伪装中的意义
原文:
towardsdatascience.com/sentence-transformers-meanings-in-disguise-323cf6ac1e52
NLP 语义搜索
现代语言模型如何捕捉意义
·发表于Towards Data Science ·阅读时间 12 分钟·2023 年 1 月 3 日
–
图片来源:Brian Suh于Unsplash。最初发布在Pinecone 的 NLP 语义搜索电子书中(作者在此工作)。
变换器完全重塑了自然语言处理(NLP)的格局。在变换器出现之前,得益于递归神经网络(RNNs),我们有了还不错的翻译和语言分类——它们的语言理解能力有限,导致许多小错误,大块文本的一致性几乎是不可能的。
自 2017 年论文*《Attention is all you need》* [1]首次引入变换器模型以来,NLP 从 RNNs 发展到 BERT 和 GPT 等模型。这些新模型可以回答问题、撰写文章(也许是 GPT-3 写的)、实现非常直观的语义搜索——还有更多。
有趣的是,对于许多任务,这些模型的后期部分与 RNN 中的部分相同——通常是几个前馈神经网络,用于输出模型预测结果。
改变的是这些层的输入。变换器模型创建的密集嵌入信息量要丰富得多,尽管使用相同的最终外层,我们仍然获得了巨大的性能提升。
这些越来越丰富的句子嵌入可以用来快速比较各种用例中的句子相似性。例如:
-
语义文本相似度 (STS) — 比较句子对。我们可能想要在数据集中识别模式,但这通常用于基准测试。
-
语义搜索——使用语义意义的信息检索(IR)。给定一组句子,我们可以使用*‘查询’*句子进行搜索,并识别最相似的记录。使搜索能够基于概念(而非特定词汇)进行。
-
聚类——我们可以对句子进行聚类,这对主题建模很有用。
在本文中,我们将探讨这些嵌入是如何被调整和应用于各种语义相似性应用的,通过使用一种叫做*‘句子变换器’*的新型变换器。
一些“上下文”
在我们深入探讨句子变换器之前,理解变换器嵌入为何如此丰富可能会有所帮助——以及普通变换器与句子变换器之间的差异所在。
变换器是之前 RNN 模型的间接后代。这些旧的递归模型通常由许多递归单元构成,如 LSTMs 或 GRUs。
在机器翻译中,我们会找到编码器-解码器网络。第一个模型用于编码原始语言到上下文向量,第二个模型用于将其解码成目标语言。
具有单一上下文向量共享的编码器-解码器架构在两个模型之间,这作为一个信息瓶颈,因为所有信息必须通过这一点传递。
这里的问题是我们在两个模型之间创建了一个信息瓶颈。我们在多个时间步骤中创建了大量信息,并试图通过一个连接将所有信息挤压过来。这限制了编码器-解码器的性能,因为编码器产生的许多信息在到达解码器之前就已经丢失。
注意机制为瓶颈问题提供了一个解决方案。它提供了信息传递的另一条途径。然而,它并没有让过程变得复杂,因为它仅仅关注最相关的信息。
通过将每个时间步的上下文向量传递到注意机制中(生成注释向量),去除了信息瓶颈,并在较长序列中有更好的信息保留。
带有注意机制的编码器-解码器。注意机制考虑了所有编码器输出激活和解码器中每个时间步的激活,这些都会修改解码器输出。
在解码过程中,模型一次解码一个单词/时间步。每一步都会计算单词与所有编码器注释之间的对齐度(例如,相似度)。
更高的对齐度导致了对解码器步骤输出的编码器注释的更大加权。这意味着机制计算了需要关注哪些编码器单词。
英法编码器和解码器之间的注意力,来源 [2]。
所有表现最好的 RNN 编码器-解码器都使用了这种注意力机制。
Attention is All You Need
在 2017 年,一篇题为Attention Is All You Need的论文发表了。这标志着 NLP 的一个转折点。作者们展示了我们可以去除 RNN 网络,并仅使用注意力机制——经过一些修改,就能获得更好的性能。
这个基于注意力的新模型被称为*‘transformer’*。从那时起,由于其极其优越的性能和出色的泛化能力,NLP 生态系统完全从 RNN 转向了 transformers。
第一个 transformer 通过使用三个关键组件去除了对 RNN 的需求:
-
位置编码
-
自注意力
-
多头注意力
位置编码取代了 RNN 在 NLP 中的关键优势——考虑序列顺序的能力(它们是递归的)。它通过根据位置向每个输入嵌入添加一组变化的正弦波激活来工作。
自注意力是指在一个词与其自身上下文(句子/段落)中的所有其他词之间应用注意力机制。这不同于普通的注意力,它专注于编码器和解码器之间的注意力。
多头注意力可以看作是几个并行的注意力机制共同工作。使用多个注意力头允许表示多个关系集(而不是单一的关系集)。
预训练模型
新的 transformer 模型的泛化能力远远超过了以前的 RNN,这些 RNN 通常是为每个用例特别构建的。
使用 transformer 模型,可以使用相同的*‘核心’模型,并仅替换最后几层以适应不同的用例(而无需重新训练核心*)。
这种新特性导致了NLP中预训练模型的兴起。预训练的 transformer 模型是在大量训练数据上训练的——通常由谷歌或 OpenAI 等公司高成本训练,然后免费提供给公众使用。
这些预训练模型中最广泛使用的之一是 BERT,或谷歌 AI 的Bidirectional Encoder Representations from Transformers。
BERT 产生了一系列进一步的模型和变种,如 distilBERT、RoBERTa 和 ALBERT,涵盖分类、问答、词性标注等任务。
BERT 用于句子相似度
到目前为止,一切都很好,但这些 transformer 模型在构建句子向量时存在一个问题:Transformers 使用的是词或token级别的嵌入,而不是句子级别的嵌入。
在句子 transformers 出现之前,使用 BERT 计算准确的句子相似度的方法是使用交叉编码器结构。这意味着我们将两个句子传递给 BERT,在 BERT 顶部添加一个分类头——并用它来输出相似度评分。
BERT 交叉编码器架构包括一个 BERT 模型,该模型处理句子 A 和 B。这两个句子在同一序列中处理,由 [SEP]
标记分隔。随后是一个前馈神经网络分类器,输出相似度评分。
交叉编码器网络确实生成非常准确的相似度评分(比 SBERT 更好),但它的可扩展性差。如果我们想在一个 100K 句子的数据库中进行相似度搜索,我们需要完成 100K 次交叉编码器推断计算。
要对句子进行聚类,我们需要比较我们 100K 数据集中的所有句子,结果将产生接近 5 亿次比较 —— 这显然是不现实的。
理想情况下,我们需要预先计算句子向量,这些向量可以被存储,然后在需要时使用。如果这些向量表示良好,我们只需计算每对句子之间的余弦相似度。
使用原始的 BERT(及其他变换器),我们可以通过对 BERT 输出的所有标记嵌入进行平均来构建句子嵌入(如果我们输入 512 个标记,则输出 512 个嵌入)。另一种方法是使用第一个 [CLS]
标记的输出(一个特定于 BERT 的标记,其输出嵌入用于分类任务)。
使用这两种方法中的一种可以得到我们的句子嵌入,这些嵌入可以被存储并且比较速度更快,将搜索时间从 65 小时缩短到大约 5 秒(见下文)。然而,准确性不佳,甚至比使用平均的 GloVe 嵌入(该方法开发于 2014 年)更差。
这个解决方案 旨在解决缺乏准确模型的合理延迟问题,由 Nils Reimers 和 Iryna Gurevych 在 2019 年设计,推出了 sentence-BERT(SBERT)和 sentence-transformers
库。
SBERT 在所有常见的语义文本相似性(STS)任务中表现优于之前的最先进(SOTA)模型 —— 更多关于这些任务的内容见下文 —— 唯一的例外是一个数据集(SICK-R)。
幸运的是,为了实现可扩展性,SBERT 生成句子嵌入 —— 因此我们不需要对每个句子对比较执行整个推断计算。
Reimers 和 Gurevych 在 2019 年展示了显著的速度提升。从 10K 个句子中找到最相似的句子对,使用 BERT 需要 65 小时。使用 SBERT,嵌入的创建时间约为 5 秒,与余弦相似度的比较时间约为 0.01 秒。
自 SBERT 论文发布以来,已经构建了许多使用类似概念的句子变换器模型,这些概念用于训练原始的 SBERT。它们都在许多相似和不相似的句子对上进行了训练。
使用诸如 softmax 损失、多负样本排序损失或 MSE 边际损失等损失函数,这些模型被优化以生成相似句子的相似嵌入和不相似句子的不同嵌入。
现在你已经了解了一些关于句子变换器的背景知识,包括它们的来源及其必要性。让我们深入探讨它们是如何工作的。
*[3] SBERT 论文涵盖了本节中的许多陈述、技术和数据。
句子转换器
我们解释了使用 BERT 的跨编码器架构来衡量句子相似性。SBERT 类似,但去掉了最终的分类头,并且一次处理一个句子。SBERT 然后在最终输出层上使用均值池化来生成句子嵌入。
与 BERT 不同,SBERT 在句子对上使用siamese架构进行微调。我们可以将其视为两个并行的完全相同的 BERT,分享完全相同的网络权重。
SBERT 模型应用于句子对句子 A和句子 B。注意,BERT 模型输出的是令牌嵌入(由 512 个 768 维向量组成)。我们随后使用池化函数将这些数据压缩成一个 768 维的句子向量。
实际上,我们使用的是单个 BERT 模型。然而,由于我们在训练期间将句子 A 和句子 B 作为对进行处理,因此更容易将其视为具有相同权重的两个模型。
Siamese BERT 预训练
训练句子转换器有不同的方法。我们将描述最初的 SBERT 过程,该过程主要优化softmax-loss。请注意,这是一个高层次的解释,我们将把深入讲解留到另一篇文章中。
softmax-loss 方法使用*‘siamese’*架构,在斯坦福自然语言推理(SNLI)和多领域 NLI(MNLI)语料库上进行微调。
SNLI 包含 570K 句子对,MNLI 包含 430K。这两个语料库中的句子对都包含一个前提
和一个假设
。每对句子被分配一个三种标签之一:
-
0 — 蕴含,例如,
前提
暗示了假设
。 -
1 — 中立,
前提
和假设
都可能为真,但它们不一定相关。 -
2 — 矛盾,
前提
和假设
互相矛盾。
给定这些数据,我们将句子 A(假设为前提
)输入到 siamese BERT A 中,将句子 B(假设
)输入到 siamese BERT B 中。
siamese BERT 输出我们的池化句子嵌入。SBERT 论文中有三种不同的池化方法结果。这些方法是均值、最大值和*[CLS]*-池化。均值池化方法在 NLI 和 STSb 数据集中表现最佳。
现在有两个句子嵌入。我们将嵌入 A 称为u
,嵌入 B 称为v
。下一步是拼接u
和v
。再次,测试了几种拼接方法,但表现最好的方法是(u, v, |u-v|)
操作:
我们将嵌入u、v和**|u — v|**进行拼接。
|u-v|
的计算结果给出两个向量的逐元素差异。除了原始的两个嵌入(u
和v
),这些都被输入到一个具有三个输出的前馈神经网络(FFNN)中。
这三个输出与我们的 NLI 相似性标签0、1和2对齐。我们需要从我们的 FFNN 计算 softmax,这在交叉熵损失函数中完成。softmax 和标签用于优化这个*‘softmax-loss’*。
这些操作是在训练过程中对两个句子嵌入u
和v
进行的。注意,softmax-loss 指的是交叉熵损失(默认情况下包含一个 softmax 函数)。
这导致我们对于相似句子(标签0)的池化句子嵌入变得更加相似,而对于不相似句子(标签2)的嵌入变得不那么相似。
请记住,我们使用的是siamese BERT,而不是dual BERT。这意味着我们不使用两个独立的 BERT 模型,而是使用一个处理句子 A 然后处理句子 B 的单个 BERT。
这意味着当我们优化模型权重时,它们会朝着一个方向推动,使模型在看到蕴含标签时输出更多相似的向量,而在看到矛盾标签时输出更多不相似的向量。
这种训练方法有效的事实并不是特别直观,实际上 Reimers 曾描述过它偶然产生了良好的句子嵌入[5]。
自原始论文以来,这个领域有了进一步的研究。已经建立了许多更多的模型,如最新的 MPNet 和 RoBERTa 模型(在超过 1B 样本上训练)(表现更佳)。我们将在未来的文章中探讨其中的一些模型及其使用的优越训练方法。
现在,让我们看看如何初始化和使用一些这些句子变换器模型。
开始使用句子变换器
开始使用句子变换器的最快和最简单的方法是通过 SBERT 创建的sentence-transformers
库。我们可以通过pip
安装它。
pip install sentence-transformers
我们将从原始的 SBERT 模型bert-base-nli-mean-tokens
开始。首先,我们下载并初始化模型。
我们可以看到的输出是SentenceTransformer
对象,它包含了三个组件:
-
变换器本身,我们可以看到最大序列长度为
128
个标记,并且是否对任何输入进行小写处理(在这种情况下,模型不进行小写处理)。我们还可以看到模型类BertModel
。 -
池化操作,在这里我们可以看到我们正在生成一个
768
维的句子嵌入。我们使用的是均值池化方法。
一旦我们有了模型,使用encode
方法可以迅速构建句子嵌入。
现在我们有了句子嵌入,可以用来快速比较句子相似性,用于文章开头介绍的用例:STS、语义搜索和聚类。
我们可以仅使用余弦相似度函数和 Numpy 快速编写一个 STS 示例。
热图显示了所有句子对之间的余弦相似度值。
在这里,我们计算了五个句子嵌入之间每种组合的余弦相似度。它们是:
我们可以看到右下角的最高相似度分数为 0.64
。正如我们所希望的,这一结果是针对描述使用建筑材料进行不良牙科实践的句子 4
和 3
的。
其他句子转换器
尽管我们从 SBERT 模型中得到了良好的结果,但自那以后已经构建了许多其他句子转换器模型。我们可以在 sentence-transformers
库中找到许多这样的模型。
这些更新的模型在性能上可以显著超过原始的 SBERT。事实上,SBERT 不再列为 SBERT.net 模型页面上的可用模型。
在句子转换器模型页面上的一些顶级表现模型。
我们将在未来的文章中更详细地介绍一些这些较新的模型。现在,让我们比较一下表现最好的模型,并运行我们的 STS 任务。
这里我们有 SentenceTransformer
模型 all-mpnet-base-v2
。这些组件与 bert-base-nli-mean-tokens
模型非常相似,只是有一些小差异:
-
max_seq_length
从128
增加到了384
。这意味着我们可以处理的序列长度是使用 SBERT 时的 三倍。 -
基础模型现在是
MPNetModel
[4] 而不是BertModel
。 -
对句子嵌入应用了额外的归一化层。
让我们比较一下 all-mpnet-base-v2
和 SBERT 的 STS 结果。
SBERT 和 MPNet 句子转换器的热图。
后期模型的语义表示非常明显。虽然 SBERT 正确地识别 4
和 3
为最相似的对,但它也对其他句子对赋予了相当高的相似度。
另一方面,MPNet 模型在相似对和不相似对之间做出了 非常 清晰的区分,大多数对的得分低于 0.1,而 4
-3
对的得分为 0.52。
通过增加不相似对和相似对之间的分离,我们正在:
-
使得自动识别相关对变得更加容易。
-
将预测结果推向训练期间使用的 0 和 1 目标分数,使 不相似 和 相似 对的分数更加接近。这是我们将在未来的文章中关于微调这些模型时看到的内容。
这就是本文介绍句子嵌入和当前 SOTA 句子转换器模型的全部内容,这些模型用于构建这些极其有用的嵌入。
句子嵌入,虽然最近才流行开来,但却是从一系列出色的创新中产生的。我们描述了一些应用于创建第一个句子转换器 SBERT 的机制。
我们还展示了尽管 SBERT 于 2019 年才刚刚推出,但其他句子变换器已经超越了该模型。幸运的是,通过sentence-transformers
库,我们可以轻松地将 SBERT 替换为这些更新的模型。
参考文献
[1] A. Vashwani 等,注意力机制全在于此(2017 年),NeurIPS
[2] D. Bahdanau 等,通过共同学习对齐和翻译的神经机器翻译(2015 年),ICLR
[3] N. Reimers, I. Gurevych,Sentence-BERT:使用 Siamese BERT 网络的句子嵌入(2019 年),ACL
[4] MPNet 模型,Hugging Face 文档
[5] N. Reimers,自然语言推断,GitHub 上的 sentence-transformers
所有图片均为作者提供,除非另有说明
情感分析与时间序列文本数据中的结构性断裂
原文:
towardsdatascience.com/sentiment-analysis-and-structural-breaks-in-time-series-text-data-8109c712ca2
Arabica 现在提供结构性断裂和情感分析模块,以丰富时间序列文本挖掘
·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 3 月 6 日
–
图片由 Adam Śmigielski 提供,来源于 Unsplash
介绍
文本数据包含大量定性信息,可以通过各种方法进行量化,包括情感分析技术。这些模型用于识别、提取和量化文本数据中的情感,并广泛应用于商业和学术研究。由于文本通常以时间序列的形式记录,因此文本数据集可能会显示出结构性断裂,因为定量信息可能因多种因素而变化。
作为业务分析师,衡量客户对特定品牌的感知变化可能是关键任务之一。在研究角色中,可能会关注弗拉基米尔·普京公开声明随时间的变化。Arabica 是一个专门设计用于处理类似问题的 Python 库。它包含以下时间序列文本数据集的探索性分析方法:
-
arabica_freq 用于描述性 n-gram 基础的探索性分析(EDA)
-
cappuccino 是一个可视化模块,包括 热力图、词云 和 折线图,用于 unigram、bigram 和 trigram 频率
-
coffee_break 实现情感和结构性断裂分析。
本文将介绍Coffee-break,情感与结构断点分析模块。阅读 文档 和这两个方法的教程:arabica_freq、cappuccino。
编辑 2023 年 7 月*:Arabica 已更新。请查看* 文档 以获取完整的参数列表。
2. Coffee_break: 算法和结构
coffee_break 模块具有简单的后端架构。以下是它的工作原理示意图:
图 1. Coffee_break 架构。来源:draw.io
原始文本使用 cleantext 进行清理,包括标点符号和数字的清除。停用词(语言中最常见但意义不大的词)在预处理步骤中不会被移除,因为它们不会对情感分析产生负面影响。然而,使用 skip
参数,我们可以移除一系列附加的停用词或不需要的字符串(词或词组),这些不会影响情感分析。
情感分析 实现了 VADER(Valence Aware Dictionary and Sentiment Reasoner),一种通用的预训练情感分类器 [1]。它是在 Twitter 的社交媒体数据上训练的,但也非常适用于其他类型的数据集。我之前的文章 提供了关于模型和 Python 编码的更详细介绍。
Coffee_break 使用 VADER 的复合指标进行情感评估。总体情感的计算方法如下:
其中 t 是聚合周期。总指标范围为 [-1: 1],正情感接近 1,负情感接近 -1。
聚合的情感创建了一个时间序列,显示出一定程度的时间变化。结构性断点 在时间序列中通过 Fisher-Jenks 算法 或 Jenks 优化方法 识别,最初由 George F. Jenks 提出 [2]。
这是一种基于聚类的方法,旨在找到将值最佳安排到不同类别(聚类)中的方法。jenks_breaks
函数通过 jenkspy
库实现,返回一个值列表,这些值对应于类别的边界。这些结构性断点在图中标记为垂直线,直观地指示了文本数据时间序列中的断点。
实施的库 包括 Matplotlib(可视化)、vaderSentiment(情感分析)和 jenkspy(结构性断裂)。Pandas 和 Numpy 进行处理操作。
3. 使用案例:Twitter 情感分析
让我们在辉瑞疫苗推文数据集上展示编码,该数据集使用 Twitter API 收集。数据包含 11,000 条关于辉瑞与 BioNTech 疫苗的推文,发布于 2006 年至 2021 年之间。该数据集根据CC0: 公共领域许可发布,符合Twitter 开发者政策。
数据包含大量标点符号和数字,在进行任何进一步的步骤之前需要清理:
图 2. 辉瑞疫苗推文数据集
coffee_break 方法的参数是:
def coffee_break(text: str, # Text column
time: str, # Time column
date_format: str, # Date format: 'eur' - European, 'us' - American
time_freq: int ='', # Aggregation period: 'Y'/'M'
preprocess: bool = False, # Clean data from numbers and punctuation
skip: [] , # Remove additional stop words
n_breaks: int ='' # Number of breaks: min. 2
)
3. 1. 随时间变化的情感分析
我们的数据涵盖了 15 年的时间跨度,包括 Covid-19 危机。关于疫苗接种的公众情绪变化、关于疫苗的虚假新闻以及许多其他因素预计会导致情感随时间发生显著变化。
编码
首先,导入coffee_break:
from arabica import coffee_break
Arabica 读取 美国风格(MM/DD/YYYY) 和 欧洲风格 (DD/MM/YYYY) 日期和时间格式。数据相当原始,涵盖了 15 年。因此,以月份显示情感并不是很有帮助。
让我们使用以下代码清理数据并按年份汇总情感:
coffee_break(text = data['text'],
time = data['date'],
date_format = 'eur', # Read dates in European format
time_freq = 'Y', # Yearly aggregation
preprocess = True, # Clean data - punctuation + numbers
skip = None , # No other stop words removed
n_breaks = None) # No structural break analysis
结果
Arabica 返回一张可以手动保存为 PNG 或 JPEG 的图片。
图 3. 情感分析 — 每年
与此同时,Arabica 返回一个包含相应数据的数据框。只需将函数分配给一个对象即可保存表格:
# generate a dataframe
df = coffee_break(text = data['text'],
time = data['date'],
date_format = 'eur',
time_freq = 'Y',
preprocess = True,
skip = None ,
n_breaks = None)
# save is as a csv
df.to_csv('sentiment_data.csv')
结果解读: 我们可以看到,情感在 2021 年辉瑞疫苗开始用于对抗 Covid 后显著下降(图 2)。原因可能是全球大流行以及这些年普遍的负面情绪。
3.2. 结构性断裂分析
接下来,让我们在统计上形式化情感中的结构性断裂。coffee_break 能够识别最少 2 个断点。以下代码返回一张带有 3 个断点的图,并附有相应时间序列的表格:
coffee_break(text = data['text'],
time = data['date'],
date_format = 'eur', # Read dates in European format
time_freq = 'Y', # Yearly aggregation
preprocess = True, # Clean data
skip = None, # No other stop words removed
n_breaks = 3) # 3 breaktpoints
图:
图 4. 结构性断裂分析 — 每年
将数据子集化到两个 Covid 年份(2020–2021),我们可以观察到公众情绪的月度变化,保持 n_breaks = 3
并设置 time_freq = 'M'
:
图 5. 结构性断裂分析 — 每月
该图表信息不够丰富。这个子集中有 1577 行数据和 24 次时间观测,清理原始数据后,时间序列非常波动。使用基于聚类的算法对如此有限的数据量做出结论并不是一个好主意。
结果解释: 年度频率的结构性断裂分析在统计上确认了我们从图 3 中的情感时间序列中看到的情况。Fisher-Jenks 算法识别出了三个结构性断裂点——分别在 2009 年、2017 年和 2021 年。我们只能猜测 2009 年以及 2016 年至 2018 年之间的下降原因。2021 年的下降可能是由于 Covid-19 危机造成的。
4. 结构性断裂分析的最佳实践
让我们总结一下coffee_break的最有效使用建议:
-
如果对应的时间序列中存在 NAN 值,请不要使用结构性断裂分析。
-
在较长时间序列中(至少 12 次观测),识别 3 个以上的断裂点是有意义的。
-
断裂点识别在高度波动的数据集中可能效果不好。剧烈变化的原因可能不是情感的变化,而是数据的质量。
-
分析的准确性仅与基础情感数据的质量有关。在实际使用之前,简要探索原始文本数据集,以检查 (1) 是否在每个时期的行数上不太不平衡,以及 (2) 是否包含足够的情感评估信息(文本不太短,且不包含大多数数字和特殊字符)。
作者提供的图片
结论
coffee_break 的一个缺点是目前它仅适用于英文文本。由于 Arabica 主要是一个基于 Pandas 的包(包括某些部分的 Numpy 向量化),coffee_break 在评估大型数据集时相对较慢。它在处理最多约 40 000 行的数据集时比较高效。
阅读这些教程,了解更多关于 n-gram 和情感分析以及时间序列文本数据可视化的信息:
-
客户满意度测量:N-gram 和情感分析
Coffee_break 是与布尔诺理工大学的 Jitka Poměnková教授合作开发的。这个教程中的完整代码在我的GitHub上。
你喜欢这篇文章吗?你可以邀请我 喝咖啡 来支持我的写作。你也可以订阅我的 电子邮件列表 以便接收到我新文章的通知。谢谢!
照片由Content Pixie提供,来源于Unsplash。
参考文献
[1] Hutto, C., Gilbert, E. (2014). VADER: 一种简约的基于规则的社交媒体文本情感分析模型。国际 AAAI 网络与社交媒体会议论文集,8(1),216–225。
[2] Jenks, G.F. (1977). 最优数据分类用于分层地图。堪萨斯大学地理气象系,临时论文第 2 号。
九月还是“Septemquake”?用 R 分析和可视化墨西哥的地震活动数据
如何使用 SSN(国家地震学服务)数据分析和可视化墨西哥的地震历史
·发布于 Towards Data Science ·13 分钟阅读·2023 年 9 月 21 日
–
当我在去年 9 月 30 日开始撰写这篇文章时,又一个月结束了,但这不是一个普通的月份。对许多墨西哥人来说,这个特别的月份常常让人忧虑,因为这个月份经常见证了我们国家因地震而动摇的情景,通常这些地震的强度都很大。本文旨在通过数据分析和可视化,为读者提供有关墨西哥地震历史的有价值的见解。虽然它不做预测或制定政策,但它提供了对地震趋势和模式的深入了解。通过获得这些知识,读者可以更好地为地震事件做好准备,并为建筑和灾难准备方面的知情决策做出贡献。
一个特别的日期突显出来,并激励了这篇文章:9 月 19 日。在 1985 年这一天,墨西哥经历了有记录以来最具破坏性的地震,震中在里氏 8.1 级。大约有 40,000 人遇难,近 4,000 人从由于构造运动而坍塌的无数建筑物和房屋的废墟中被救出。
图片来源 Mr. Dimentio 于 Wikimedia Commons
自那时起已经过去了 32 年,2017 年,墨西哥人再次在 9 月 19 日被 7.1 级里氏震级的地震所震惊,这让许多经历过 1985 年灾难的人再次感受到了旧伤。在 2017 年之前,我无法理解为什么我的父母在听到墨西哥城安装的地震警报声时会如此紧张,这种警报至少可以让我们提前几秒钟预知即将发生的事情。在 2017 年之前,我对警报声并不认真对待,但在经历了在九楼(我曾经工作的地方)的地震后,从那里你可以看到附近建筑物倒塌时的尘土云,听到恐惧的人们的统一尖叫,同时建筑物发出如同要断裂的声音,震动把你摔倒在地,这一切终身难忘。
## 2017 年 9 月 19 日在墨西哥城的地震 #地震 #earthquake #Sismo #temblor
编辑描述
当天在墨西哥录得近 400 人遇难,7000 多人受伤。在地震发生时,我的女友(现在是我们美丽小宝贝的母亲)和我紧紧相拥,忘记了那天早晨的争执。从那时起,每当听到地震警报声,都会让我浑身发抖,就像现在生活在墨西哥城的许多人一样。
不管怎样,我们来到了 2022 年,去年 9 月 19 日下午 1 点左右,我们再次被响遍全城的地震警报惊醒,预示着即将发生的是 7.7 级里氏震级的地震。尽管为了防范这样的事件建造了更坚固、更雄伟的房屋和建筑,但自然力量永远没有免疫保证。这次事件中,至少有两人报告遇难,3000 多处财产受到影响。这让许多人产生了疑问:这个日期有什么特别之处吗?为什么墨西哥在这个特定的日子会发生强震?这会不会是阿兹特克神灵要求牺牲的惩罚?
— 这是发生在九月。不是今年的九月,而是去年的。还是说是前年,梅利顿?
— 不,那是在去年。
— 是的,我现在记起来了。是在去年九月二十一号。嘿,梅利顿,九月二十一号不是正是地震那天吗?
— 那是稍早一点。我认为是在十八号。
— 你说得对。我那时在图斯卡库斯科(Tuzcacuexco)。我甚至看到房屋像太妃糖一样倒塌,扭曲并做出各种面部表情,然后整个墙壁轰然倒下。
… 摘自 1955 年墨西哥作家胡安·鲁尔福(Juan Rulfo)的故事《崩溃之日》(“El día del derrumbe”)。
本文旨在通过数据分析,为读者提供对墨西哥人特别是在这个月广泛讨论的问题的更全面的理解。需要指出的是,墨西哥的地震活动非常频繁,位于五个不同的构造板块交汇处,使其比其他国家更为脆弱。
我可以在哪里获得可靠的墨西哥地震活动数据?
有许多来源可以获得地震活动的历史数据;你使用的数据集已获得商业用途许可。幸运的是,我推荐一个非常可靠的来源,即国家地震服务中心(SSN)。SSN 维护了全国范围的地震监测网络,并提供适合商业用途的数据许可证。他们的数据集涵盖了从 1900 年至今的记录,并且几乎即时更新新的地震事件记录。
你可以在他们的网站上下载完整的历史记录 CSV 格式的数据,或者选择感兴趣的特定时间段:www2.ssn.unam.mx:8080/catalogo/
九月还是“震动九月”?使用 R 对墨西哥的地震活动数据进行分析和可视化 — SSN 网站截图(作者提供的图像)
数据加载和准备
在新的 R 脚本中,你将首先加载相关的库来进行分析。除了从国家地震服务中心网站提取的数据外,为了实用起见,你还需要从 INEGI 门户网站下载墨西哥共和国的政治分区地图,网址是 www.inegi.org.mx/app/mapas/
以 SHP 格式提供。众所周知,你可以在 R 中处理这种格式。最后,你还将获得一个州名和标识符的列表,以便在后续与其他内容结合使用。
# LOADING LIBRARIES
library(data.table)
library(magrittr)
library(sf)
library(tmap)
library(sp)
library(ggpubr)
library(ggplot2)
library(lattice)
# PALETTE & FUNCTION
myColors <- c('#d9ef8b','#91cf60','#fee08b','#fc8d59','#d73027','#1a9850')
decade <- function(date){
year <- data.table::year(date)
y <-year - year %% 10
return(y)
}
# LOAD DATA AND SHAPES
dataSSN <- fread("SSNMX_catalogo_19000101_20221126.csv", header = T, skip = 4, sep=",", fill=T)
mxMap <- st_read("dest20gw/dest20gw.shp")
statesNames <- fread("nombres_estados.csv", encoding = "UTF-8",header = T)
skipLast <- grep(pattern = "Fecha y hora local",as.character(unlist(dataSSN[,1])))
dataSSN <- dataSSN[-skipLast:-dim(dataSSN)[1],]
names(dataSSN)<- tolower(names(dataSSN))
names(dataSSN)<- gsub("[[:space:]]", ".", names(dataSSN))
你还将按震级对地震数据进行分类,并将 CSV 数据格式化以提高可读性,因为它包含了如震级、日期、震中和其他详细信息等精确数据。
# ADJUSTMENTS TO DATA
dataSSN <- dataSSN %>%
.[, state := gsub(".*,\\s*", "\\1", referencia.de.localizacion)] %>%
.[, state := gsub("[[:space:]]","", state)] %>%
.[, state := fifelse(state=="N", "NL", state)] %>%
.[, date := as.Date(fecha)] %>%
.[, intensity := suppressWarnings( as.numeric(magnitud))] %>%
.[, intensityRanges := fcase(
intensity>=0 & intensity<= 2 , "0-2", intensity>=2 & intensity<= 4 , "2-4",
intensity>=4 & intensity<= 6 , "4-6", intensity>=6 & intensity<= 8 , "6-8",
intensity>=8 & intensity<= 10 , "8-10", intensity>10, "10 +",
is.na(intensity), "Unmeasured magnitude")] %>%
.[, intensityRanges := factor(intensityRanges, levels = c("0-2", "2-4", "4-6",
"6-8", "8-10","10+",
"Unmeasured magnitude"))] %>%
.[, theDecade := decade(date)] %>%
.[, monthDate := as.Date(cut(date, "month"))] %>%
.[, weekDate := as.Date(cut(date, "week"))] %>%
.[, month := data.table::month(date)] %>%
.[, monthName := format(date, "%B")] %>%
.[, dayName := format(date, "%A")] %>%
.[, monthDay := format(as.Date(date), "%m-%d")] %>%
.[, year := data.table::year(date)] %>%
.[, day := data.table::mday(date)] %>%
.[date >= "1900-01-01"]
dataSSN <- merge(dataSSN, statesNames, by.x="state", by.y="id.estado")
自 1900 年至今记录的地震活动
使用你的数据,你可以大致查看自 1900 年以来记录到的地震数量是否有所增加或减少。
# SEISMIC ACTIVITY SINCE 1900
dataSSN %>%
.[, .(seismicCount = .N), by=date] %>%
ggplot(aes(x= date, y= seismicCount)) +
geom_line(color="darkcyan") +
xlab("Year") +
ylab("Earthquakes count") +
ggtitle("Earthquakes per day reported by the SSN", "Since 1900 to 2022")
请注意,虽然历史数据可以追溯到 1900 年,但直到 2010 年之后,国家才安装了更多的地震传感器。这清楚地显示了记录到的地震数量的增长,但这并不一定表明地震活动的增加。
九月还是“震动九月”?使用 R 对墨西哥的地震活动数据进行分析和可视化 — RStudio 绘图“自 1900 年至今记录的地震活动”(作者提供的图像)
哪种震级主导了墨西哥的地震活动?
正如你之前按震级对数据进行了分类,现在你可以可视化并更具体地了解记录到的地震震级随时间的变化。
# SEISMIC ACTIVITY SINCE 1900 BY INTENSITY
timeSeries1 <- dataSSN %>%
.[, .(seismicCount = .N), by=list(date, intensityRanges)] %>%
na.omit %>%
ggplot(aes(x= date, y= seismicCount, color=intensityRanges)) +
geom_line(size=1)+
scale_color_manual(values= myColors, name="Richter Scale") +
theme(legend.position="top")+
ggtitle("Earthquakes per day reported by the SSN", "Magnitude ranges since 1900 to 2022")+
xlab("Year") +
ylab("Earthquakes count")
timeSeries2 <- dataSSN %>%
.[, .(seismicCount = .N), by=list(date, intensityRanges)] %>%
na.omit %>%
ggplot(aes(x= date, y= seismicCount, color=intensityRanges)) +
geom_line(size=1)+
scale_color_manual(values= myColors) +
scale_x_date(breaks = "4 year")+
theme(legend.position="top", axis.text.x = element_text(angle=90))+
xlab("Year") +
ylab("Earthquakes count") +
facet_wrap(~intensityRanges, scales = "free_y")
ggarrange(timeSeries1, timeSeries2,
nrow = 2)
正如你所见,震中震级在里氏 2 到 4 级的地震占据主导地位。再提醒一下,在 2000 年之前,安装的传感器不多。
九月还是“九月震”?使用 R 进行的墨西哥地震活动数据分析和可视化——RStudio 图表“哪一月份地震活动最强?”(作者提供的图像)
墨西哥发生了多少次震级超过 6 的地震?
你还可以从总体上查看地震的发生情况,特别是震级超过 6 的地震(被视为高风险地震)。
# SEISMIC ACTIVITY SINCE 1900 BY INTENSITY (>6)
dataSSN %>%
.[intensity>6] %>%
.[, .(seismicCount = .N), by=list(date, intensityRanges)] %>%
ggplot(aes(x= (date), y= seismicCount)) +
geom_segment( aes(x=(date), xend=date, y=0, yend=seismicCount, color=intensityRanges)) +
geom_point(size=3, alpha=0.6, aes(color= intensityRanges))+
scale_color_manual(values= c("darkcyan", "darkred"), name="Range Magnitude Richter Scale") +
scale_x_date(breaks = "5 years")+
scale_y_continuous(breaks = seq(0,2,1))+
theme(legend.position="top", axis.text.x = element_text(angle=45))+
ggtitle("Earthquakes reported by the SSN", "Intensity > 6 (Since 1900 to 2022)")+
xlab("Year") +
ylab("Earthquakes count")
虽然在你获得的图像中可能不易察觉,但放大后会显示震级达到 8 的地震,这些地震确实给国家带来了震动。
九月还是“九月震”?使用 R 进行的墨西哥地震活动数据分析和可视化——RStudio 图表“墨西哥发生了多少次震级超过 6 的地震?”(作者提供的图像)
墨西哥哪个月份地震活动最强?
根据获得的数据,你可以回答这个问题,很多人在九月时曾关心过。值得注意的是,目前没有严格的科学依据解释为何特定月份的地震活动会增加。
# SEISMIC ACTIVITY REPORTED SINCE 1900 (PER MONTH)
dataSSN %>%
.[, .(seismicCount = .N), by=list(month, monthName)] %>%
.[order(month)] %>%
ggplot(aes(x= reorder(monthName, month), y= seismicCount)) +
geom_bar(stat = "identity") +
geom_col(aes(fill = seismicCount)) +
theme(axis.text.x = element_text(angle=45)) +
geom_label(aes(reorder(monthName, month), y= seismicCount,
label=seismicCount) ) +
xlab("Month") +
ylab("Earthquakes count")+
ggtitle("Earthquakes reported by the SSN", "Earthquakes per month (Since 1900 to 2022)")
然而,数据本身说明了问题,显示出九月确实记录了比其他月份更多的地震。
九月还是“九月震”?使用 R 进行的墨西哥地震活动数据分析和可视化——RStudio 图表“哪个月份地震活动最强?”(作者提供的图像)
按强度分组的墨西哥地震活动最高的月份?
假设你想更深入地了解先前获得的结果。你可以按强度分组,以确定除了九月是地震活动最强的月份外,九月是否也与较高的震级相关。
# SEISMIC ACTIVITY REPORTED SINCE 1900 (PER MONTH AND INTENSITY)
rangesMonth <- dataSSN %>%
.[, .(seismicCount = .N), by=list(monthName, month, intensityRanges)] %>%
ggplot(aes(x= reorder(monthName, month), y= seismicCount, fill= intensityRanges)) +
geom_bar(stat = "identity") +
scale_fill_manual(name = "Response", values = myColors) +
theme(axis.text.x = element_text(angle=45), legend.position="top") +
xlab("Month") +
ylab("Earthquakes count")+
ggtitle("Earthquakes reported by the SSN", "Earthquakes per month (Grouped by intensity since 1900 to 2022)")
rangesMonth
下面的图表可能不会显示震级 8 的地震,但它们确实存在,6 月份有一次,9 月份有两次。如果你愿意,可以使用 plotly 进行更明显的可视化。
九月还是“九月震”?使用 R 进行的墨西哥地震活动数据分析和可视化——RStudio 图表“按强度分组的墨西哥地震活动最强的月份?”(作者提供的图像)
墨西哥哪个月份的地震活动最强,震级超过 6 度?
考虑到震中达到 6 度或更高的地震通常更为显著,假设你想更深入地研究之前获得的图表,以筛选出那些明显超过 6 度的地震。
# SEISMIC ACTIVITY REPORTED SINCE 1900 (GROUPED BY INTENSITY >6)
dataSSN %>%
.[intensity>6] %>%
.[, .(seismicCount = .N), by=list(monthName, month, intensityRanges)] %>%
ggplot(aes(x= reorder(monthName, month), y= seismicCount, fill= intensityRanges)) +
geom_bar(stat = "identity")+
scale_y_continuous(breaks = seq(0,100,1))+
theme(axis.text.x = element_text(angle=45), legend.position="top")+
scale_fill_manual(values= c("darkcyan", "darkred"), name="Range Magnitude Richter Scale")+
xlab("Month") +
ylab("Earthquakes count")+
geom_label(aes(reorder(monthName, month), y= seismicCount, label=seismicCount) )+
ggtitle("Earthquakes reported by the SSN", "Earthquakes per month (Intensity > 6 since 1900 to 2022)")
这一次,情况变得更加清晰,尤其是震级超过 8 度的地震发生的月份。12 月似乎是震级超过 6 度的地震最多的月份。
9 月还是“九月地震”?使用 R 对墨西哥的地震活动数据进行分析和可视化 — RStudio 绘图“哪一个月份的地震活动最高,震级超过 6 度?”(图片由作者提供)
哪一天的“生日”地震数量最多,震级超过 6 度?
假设你还想了解在同一天和同一个月份但不同年份中发生了三次或更多次震级达到 6 度或以上的地震的日期。
# DAYS THAT AN EARTHQUAKE OF INTENSITY >6 HAS BEEN REPEATED AT LEAST 3 OR MORE TIMES
dataSSN %>%
.[intensity>=6] %>%
.[, .(seismicCount = .N), by=list(month, monthDay)] %>%
.[order(month)] %>%
.[seismicCount>=3] %>%
ggplot(aes(x= reorder(monthDay, month), y= seismicCount)) +
geom_bar(stat = "identity", fill="darkcyan")+
geom_col(aes(fill = seismicCount)) +
theme(axis.text.x = element_text(angle=45)) +
geom_label(aes(reorder(monthDay, month), y= seismicCount, label=seismicCount) ) +
xlab("Date (month-day)") +
ylab("Earthquakes count")+
ggtitle("Earthquakes reported by the SSN", "Dates that have occurred three or more earthquakes on the same day
and month but different year (Intensity >= 6 since 1900 to 2022)")
你会看到 6 月 7 日以七次符合条件的地震居于榜首。我们是否应该在这些日期佩戴头盔并避免高楼?
9 月还是“九月地震”?使用 R 对墨西哥的地震活动数据进行分析和可视化 — RStudio 绘图“哪一天的‘生日’地震数量最多,震级超过 6 度?”(图片由作者提供)
一周中的哪一天墨西哥的地震活动最高,震级超过 7 度?
这可能是另一个你可以通过建立震中等于或大于 7 度的地震发生情况来回答的问题。
# WEEKDAYS THAT AN EARTHQUAKE OF INTENSITY >7 HAS BEEN REPEATED MORE TIMES
dataSSN %>%
.[intensity>=7] %>%
.[order(dayName)] %>%
.[, .(seismicCount = .N), by=list(dayName)] %>%
ggplot(aes(x= dayName, y= seismicCount)) +
geom_bar(stat = "identity", fill="darkcyan")+
geom_col(aes(fill = seismicCount)) +
theme(axis.text.x = element_text(angle=45)) +
xlab("Mes") +
ylab("Conteo Sismos")+
ggtitle("Earthquakes reported by the SSN", "Weekdays with greater occurrences of earthquakes (Intensity >= 7 since 1900 to 2022)")
你将得到如下图表,显示出星期五记录的地震活动最高,而星期天的记录较少。
9 月还是“九月地震”?使用 R 对墨西哥的地震活动数据进行分析和可视化 — RStudio 绘图“一周中的哪一天墨西哥的地震活动最高,震级超过 7 度?”(图片由作者提供)
墨西哥地震活动图
在这样的练习中,拥有地理参考总是很有用。你可以创建一张地图,以概括地可视化该国的地震活动。
# MAP SEISMIC ACTIVITY BY STATE 1
dataSSN %>%
.[, .(seismicCount = .N), by=list(nombreEstado)] %>%
merge(., mxMap, by.x="nombreEstado", by.y="NOM_ENT") %>%
st_as_sf() %>%
ggplot()+
geom_sf(aes(fill = seismicCount)) +
ggtitle("Earthquakes reported by the SSN", "Map of seismic activity in Mexico (Since 1900 to 2022)")
结果会突出显示瓦哈卡,该地区的地震数量最高。
9 月还是“九月地震”?使用 R 对墨西哥的地震活动数据进行分析和可视化 — RStudio 绘图“墨西哥地震活动图”(图片由作者提供)
如果你愿意,可以为你的地图提供更具美感的格式。
# MAP SEISMIC ACTIVITY BY STATE 2
tmap_style("classic")
statesMap <- dataSSN %>%
.[, .(seismicCount = .N), by=list(nombreEstado)] %>%
merge(., mxMap, by.x="nombreEstado", by.y="NOM_ENT") %>%
st_as_sf() %>%
tm_shape() +
tm_polygons("seismicCount",
title = "Number of earthquakes range")+
tm_layout("Number of earthquakes
by State (Since 1900 to 2022)", title.position = c('right', 'top'))
statesMap
你可以试验**“tmap_style()”**函数的可用样式,以选择你喜欢的样式。
9 月还是“九月地震”?使用 R 对墨西哥的地震活动数据进行分析和可视化 — RStudio 绘图“墨西哥地震活动图”(图片由作者提供)
哪个州的地震活动最强?
尽管您已经获得了一个地图来回答这个问题,但制作另一个图表来用更多细节加强结果也不会有坏处。
# SEISMIC ACTIVITY GROUPED BY STATE
paretoData <- dataSSN %>%
.[, .(seismicCount = .N), by=list(nombreEstado)] %>%
.[, total := sum(seismicCount)] %>%
.[order(seismicCount, decreasing = T)] %>%
.[, accumulatedSum := cumsum(seismicCount)] %>%
.[, percentage := seismicCount/total] %>%
.[, accumulatedPercentage := accumulatedSum/total]
listPercentage <- 0:100
statesBars <- ggplot(data= data.frame(paretoData), aes(x=nombreEstado)) +
geom_bar(aes(x=reorder(nombreEstado, -seismicCount), y=seismicCount), fill='darkcyan', stat="identity") +
scale_y_continuous(limits = c(0, (max(paretoData$seismicCount))+15000 ))+
theme(axis.text.x = element_blank(), axis.title.x = element_blank())+
ylab("Earthquakes count")+
geom_text(aes(reorder(nombreEstado, -seismicCount), y= seismicCount, label=seismicCount), vjust=-1, angle=45, hjust=0)+
ggtitle("Earthquakes reported by the SSN", "Earthquakes grouped by State (Since 1900 to 2022)")
statesPercentage <- ggplot(data= data.frame(paretoData)) +
geom_bar(aes(x=reorder(nombreEstado, -seismicCount), y=percentage), fill='darkcyan', stat="identity") +
theme(axis.text.x = element_text(angle=45, hjust=1))+
scale_y_continuous(labels = scales::percent, breaks = seq(0,1,0.1), limits=c(0,0.7))+
xlab("State") +
ylab("Earthquakes count")+
geom_text(aes(x= reorder(nombreEstado, -seismicCount), y = percentage,
label=paste0(round(percentage*100, 3),"%"), vjust=0, angle=45, hjust=0))+
ggtitle(" ", "Percentage of earthquakes by State (Since 1900 to 2022)")
ggarrange(statesBars, statesPercentage, nrow = 2)
如果您在寻找一个可以摆脱地震担忧的地方,尤卡坦、克雷塔罗、阿瓜斯卡连特斯或杜兰戈可能是不错的选择。
九月还是“九月地震”?使用 R 对墨西哥地震活动数据进行分析和可视化 — RStudio 图表“墨西哥哪个州的地震活动最强?”(图片由作者提供)
里氏震级大于 7.5 的地震地图
讨论震中强度等于或大于 7.5 的地震意味着讨论可能导致灾难性损失的震动。您可以查看这些被记录为历史上最强烈的地震的起源地图。
# MAP TOP EARTHQUAKES >= 7.5
topEarthquakes <- dataSSN %>%
.[intensity>=7.5]
ggplot(data = mxMap) +
geom_sf()+
geom_point(data= topEarthquakes, aes(x= longitud, y= latitud, size= intensity), color="darkcyan")+
scale_color_manual( name="Magnitud escala de Richter")+
theme(axis.text = element_blank(), axis.title = element_blank(), legend.position="top")+
ggrepel::geom_text_repel(data= topEarthquakes, aes(x= longitud, y= latitud, label= intensity), color="darkred")+
ggtitle("Earthquakes reported by the SSN", "Map of strongest earthquakes locations (Intensity >= 7.5 since 1900 to 2022)")
结果将显示,太平洋沿岸的州是墨西哥记录到的最强烈地震发生地。
九月还是“九月地震”?使用 R 对墨西哥地震活动数据进行分析和可视化 — RStudio 图表“里氏震级大于 7.5 的地震地图”(图片由作者提供)
总结来说,本文通过数据分析和可视化揭示了墨西哥的地震历史。虽然它没有提供预测或政策建议,但它为读者提供了对地震趋势的宝贵理解。这些知识可以对个人安全、城市规划和建筑实践产生重大影响。通过了解地震活动最可能发生的时间和地点,个人和社区可以做出明智的决策以减少风险。无论您是墨西哥城的居民还是设计抗震结构的工程师,从这次分析中获得的见解都可以产生真正的影响。保持安全,保持知情,并记住,知识在地震准备中是一个强大的工具。
感谢您的细心阅读。如果您目前居住在地震活动频繁的地区,请务必注意自身安全。和我的其他文章一样,我在这里分享了完整的代码:github.com/cosmoduende/r-earthquakes
祝您在分析中获得愉快的体验,把一切付诸实践,并对结果感到惊讶和娱乐!
从你的电脑上提供大语言模型服务,通过文本生成推理
使用 Falcon-7B 的指令版本的示例
·发表在Towards Data Science ·阅读时间 6 分钟·2023 年 7 月 13 日
–
通过量化方法如 QLoRa 和GPTQ,现在可以在消费者硬件上本地运行非常大的语言模型(LLM)。
考虑到加载大语言模型的时间,我们可能还希望将 LLM 保持在内存中以便即时查询并获取结果。如果你使用标准的推理管道,你必须每次重新加载模型。如果模型非常大,你可能需要等待几分钟才能生成输出。
有各种框架可以在服务器(本地或远程)上托管大语言模型(LLMs)。在我的博客中,我已经介绍了NVIDIA 开发的非常优化的 Triton 推理服务器框架,它用于服务多个 LLMs,并在 GPU 之间平衡负载。但是,如果你只有一个 GPU,并且希望在你的电脑上托管模型,使用 Triton 推理可能会显得不太合适。
在这篇文章中,我介绍了一种替代方案,称为文本生成推理。这是一个更简单的框架,实现了运行和服务 LLMs 所需的所有基本功能,适用于消费者硬件。
阅读完本文后,你将在你的电脑上拥有一个本地部署并等待查询的聊天模型/LLM。
文本生成推理
文本生成推理(TGI)是一个用 Rust 和 Python 编写的框架,用于部署和服务 LLM。它由 Hugging Face 开发,并以 Apache 2.0 许可证 进行分发。Hugging Face 在生产中使用它来驱动他们的推理小部件。
尽管 TGI 已针对 A100 GPU 进行了优化,但由于对量化和 分页注意力的支持,我发现 TGI 非常适合自托管的 LLM,在如 RTX GPU 这样的消费级硬件上表现出色。然而,它需要特定的安装来支持 RTX GPU,这一点我将在本文后续部分详细说明。
最近,我还发现 Hugging Face 正在优化一些 LLM 架构,以便它们在 TGI 下运行更快。
这尤其适用于 Falcon 模型,这些模型在使用标准推理管道时运行较慢,但在使用 TGI 时运行更快。一位 Falcon 模型的作者在 Twitter 上告诉我,这是因为他们在多查询注意力的实现上匆忙,而 Hugging Face 则优化了它以便与 TGI 配合使用。
有几种 LLM 架构以这种方式进行了优化,以便在 TGI 下运行得更快:BLOOM、OPT、GPT-NeoX 等。完整列表可以在 TGI 的 GitHub 上找到,并定期更新。
设置文本生成推理
硬件和软件要求
我在 RTX 3060 12 GB 上进行了测试。它应该适用于所有 RTX 30x 和 40x,但请注意 TGI 特别优化了 A100 GPU。
要运行这些命令,你需要一个 UNIX 操作系统。我使用了通过 Windows WSL2 的 Ubuntu 20.04。
它在 Mac OS 上也应该可以正常工作。
TGI 需要 Python ≥ 3.9。
我将首先介绍如何从零开始安装 TGI,我认为这并不简单。如果你在安装过程中遇到问题,可能需要改用 Docker 镜像。我将讨论这两种情况。
设置
TGI 是用 Rust 编写的。你需要先安装它。如果你没有安装,运行以下命令:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
这应该花费不到 2 分钟。我建议重启你的 shell,例如,打开一个新的终端,以确保所有环境变量正确更新。
然后,我们创建一个专用的 conda 环境。此步骤是可选的,但我更喜欢为每个项目创建一个独立的环境。
conda create -n text-generation-inference python=3.9
conda activate text-generation-inference
我们还需要安装 Protoc。Hugging Face 目前推荐版本 21.12。你需要具有 sudo 权限。
PROTOC_ZIP=protoc-21.12-linux-x86_64.zip
curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v21.12/$PROTOC_ZIP
sudo unzip -o $PROTOC_ZIP -d /usr/local bin/protoc
sudo unzip -o $PROTOC_ZIP -d /usr/local 'include/*'
rm -f $PROTOC_ZIP
我们已经安装了所有要求。现在,我们可以安装 TGI。
首先,克隆 GitHub 仓库:
git clone https://github.com/huggingface/text-generation-inference.git
然后安装 TGI:
cd text-generation-inference/
BUILD_EXTENSIONS=False make install
注意:我将 BUILD_EXTENSIONS 设置为 False 以停用自定义 CUDA 内核,因为我没有 A100 GPU。
应该可以顺利安装……但在我的计算机上却没有。我不得不手动运行 server/Makefile 文件中的所有命令。我怀疑是由于“make”由于某种原因切换到不同的 shell,导致我的环境变量没有正确加载。你可能也需要这样做。
注意:如果安装失败,不用担心!Hugging Face 创建了一个 Docker 镜像,你可以启动它以启动服务器,我们将在下一部分中看到。
使用 TGI 启动模型
对于以下示例,我使用的是 Falcon-7B 模型 的 instruct 版本,它在 Apache 2.0 许可证下分发。如果你想了解更多关于 Falcon 模型的信息,我在上一篇文章中做了介绍:
## Open LLM Falcon-40B 介绍:性能、训练数据和架构
开始使用 Falcon-7B、Falcon-40B 及其 instruct 版本
不使用 Docker
安装创建了一个新的命令“text-generation-launcher”,它将启动 TGI 服务器。
text-generation-launcher --model-id tiiuae/falcon-7b-instruct --num-shard 1 --port 8080 --quantize bitsandbytes
-
model-id:模型名称在 Hugging Face Hub。
-
num-shard:设置为你拥有的 GPU 数量,以及你希望利用的数量。
-
port:你希望服务器监听的端口。
-
quantize:如果你使用的 GPU 内存少于 24 GB,你需要对模型进行量化,以避免内存不足。我选择了“bitsandbytes”进行即时量化。GPTQ(“gptq”)也可用,但我对这个算法不太熟悉。
使用 Docker(如果手动安装失败)
注意:如果 Docker 守护进程未运行,并且你通过 WSL 运行 Ubuntu,请在另一个终端中使用“sudo dockerd”启动守护进程。
volume=$PWD/data
sudo docker run --gpus all --shm-size 1g -p 8080:80 -v $volume:/data ghcr.io/huggingface/text-generation-inference:0.9 --model-id tiiuae/falcon-7b-instruct --num-shard 1 --quantize bitsandbytes
参数几乎与 text-generation-launcher 一样。如果你只有一个 GPU,你可以将“all”替换为“0”。
保持这个 Docker 镜像运行,只要你想使用服务器。
使用 TGI 查询模型
要用 Python 脚本查询 TGI 提供的模型,你需要安装以下库:
pip install text-generation
然后在 Python 脚本中,写类似这样的代码:
from text_generation import Client
client = Client("http://127.0.0.1:8080")
print(client.generate("Translate the following into French: 'What is Deep Learning?'", max_new_tokens=500).generated_text)
它应该打印:
Qu'est-ce que la profondeur de l'apprentissage ?
这是一种较差质量的翻译。这是对一个 70 亿参数模型的预期。它在编码任务上略好一些:
from text_generation import Client
client = Client("http://127.0.0.1:8080")
print(client.generate("Code in Javascript a function to remove all spaces in a string and then print the string twice.", max_new_tokens=500).generated_text)
它生成:
Here is an example code snippet in JavaScript to remove all spaces in a string and then print the string twice:
```javascript
function removeSpaces(str) {
return str.replace(/\s+/g, '');
}
console.log(removeSpaces('Hello World'));
console.log(removeSpaces('Hello World'));
```py
你也可以用 curl 进行查询,而不是 Python 脚本:
curl 127.0.0.1:8080/generate \
-X POST \
-d '{"inputs":"Code in Javascript a function to remove all spaces in a string and then print the string twice.","parameters":{"max_new_tokens":500}}' \
-H 'Content-Type: application/json'
TGI 确实很快。使用 Falcon-7B 并将最大 token 数设置为 500,仅需几秒钟,我的 RTX 3060 GPU 即可完成。
使用标准推理管道,几乎需要 40 秒,还不包括加载模型所需的时间。
结论
自托管聊天模型(即,指导 LLM)有很多优势。主要的是你不会将数据发送到互联网。另一个是你完全控制操作成本,这只反映在你的电费账单上。
然而,如果你使用消费级 GPU,你将无法运行最先进的 LLM。即使是较小的 LLM,我们也必须将其量化,以便在配备少于 24 GB VRAM 的 GPU 上运行。量化还会降低 LLM 的准确性。
尽管如此,即使是小型量化 LLM 也仍然适合简单任务:简单的编码问题、二元分类……
现在,你可以通过查询你的自托管 LLM 在计算机上完成所有这些任务。
如果你喜欢这篇文章并对阅读接下来的文章感兴趣,支持我工作的最佳方式是通过这个链接成为 Medium 会员:
[## 通过我的推荐链接加入 Medium - Benjamin Marie]
作为 Medium 会员,你的一部分会员费会给你阅读的作者,并且你可以全面访问每一个故事……
medium.com](https://medium.com/@bnjmn_marie/membership?source=post_page-----54f4dd8783a7--------------------------------)
如果你已经是会员并且想支持这项工作, 只需关注我在 Medium 上的账户。
使用 TorchServe 服务 ML 模型
一个完整的端到端图像分类任务 ML 模型服务示例
·
关注 发表在 Towards Data Science ·8 min read·2023 年 3 月 29 日
–
图片来源:作者
动机
本文将引导你通过使用 TorchServe 框架服务你的深度学习 Torch 模型的过程。
关于这个主题有很多文章。然而,通常这些文章要么专注于部署 TorchServe 本身,要么专注于编写自定义处理程序并获得最终结果。这是我写这篇文章的动机。本文涵盖了这两个部分,并提供了端到端的示例。
以图像分类挑战为例。最终你将能够部署 TorchServe 服务器,提供模型服务,发送任何随机的衣物图片,并最终获得预测的衣物类别标签。我相信这就是人们对作为 API 端点提供分类服务的 ML 模型的期望。
简介
比如,你的数据科学团队设计了一个很棒的深度学习模型。这无疑是一个伟大的成就。然而,为了让它发挥价值,模型需要以某种方式向外界开放(如果不是 Kaggle 竞赛的话)。这就是所谓的模型服务。在这篇文章中,我不会涉及批处理操作的服务模式以及基于流处理框架的流处理模式。我将专注于将模型作为 API 提供服务的一个选项(无论这个 API 是由流处理框架还是任何自定义服务调用)。更准确地说,这个选项是 TorchServe 框架。
因此,当你决定将模型作为 API 提供服务时,你至少有以下几种选择:
-
网络框架如 Flask、Django、FastAPI 等等
-
云服务如 AWS Sagemaker 端点
-
专用服务框架如 Tensorflow Serving、Nvidia Triton 和 TorchServe
每种方法都有其优缺点,选择可能并不总是简单明了的。让我们实际探索一下 TorchServe 选项。
结构
第一部分将简要描述模型是如何训练的。这对 TorchServe 来说并不重要,但我认为这有助于跟踪端到端的过程。接下来,将解释自定义处理器。
第二部分将重点讨论 TorchServe 框架的部署。
本文的源代码位于这里:git repo
准备一个深度学习模型
对于这个示例,我选择了基于 FashionMNIST 数据集的图像分类任务。如果你不熟悉这个数据集,它包含 70k 张 28x28 像素的灰度图像,展示了不同的衣物。共有 10 个衣物类别。因此,一个深度学习分类模型将返回 10 个 logit 值。为了简化,模型基于 TinyVGG 架构(如果你想用 CNN 解释器可视化它):仅有少量卷积层和最大池化层,带有 RELU 激活。仓库中的笔记本 model_creation_notebook 展示了训练和保存模型的全部过程。
简而言之,笔记本只是下载数据,定义模型架构,训练模型并用 torch 保存状态字典。有两个与 TorchServe 相关的文档:一个包含模型架构定义的类和保存的模型(.pth 文件)。
为 TorchServe 准备文档以提供模型服务
需要准备两个模块:模型文件和自定义处理器。
模型文件
根据文档,“*模型文件应包含模型架构。对于急切模式的模型,这个文件是必须的。
该文件应包含一个继承自 torch.nn.Module 的单个类。
所以,让我们从模型训练笔记本中复制类定义,并将其保存为 model.py(你可以选择任何名称):
处理程序
TorchServe 提供了一些默认处理程序(例如 image_classifier),但我怀疑它是否可以直接用于实际案例。因此,你很可能需要为你的任务创建一个自定义处理程序。处理程序实际上定义了如何从 http 请求中预处理数据,如何将其输入模型,如何后处理模型的输出以及在响应中返回最终结果。
有两个选项——模块级入口点和类级入口点。请查看官方文档这里。
我将实现类级选项。这基本上意味着我需要创建一个自定义 Python 类并定义两个强制函数:initialize 和 handle。
首先,为了简化操作,让我们继承 BaseHandler 类。initialize 函数定义了如何加载模型。由于我们这里没有任何具体要求,因此可以直接使用超类中的定义。
handle 函数基本上定义了如何处理数据。在最简单的情况下,流程是:预处理 >> 推理 >> 后处理。在实际应用中,你可能需要定义自定义的预处理和后处理函数。对于这个示例的推理函数,我将使用超类中的默认定义:
预处理函数
比如,你构建了一个图像分类应用。该应用将图像作为负载发送给 TorchServe。图像可能不会始终符合用于模型训练的图像格式。此外,你可能会在样本批次上训练模型,张量维度必须进行调整。因此,让我们创建一个简单的预处理函数:将图像调整为所需的形状,转换为灰度图像,转换为 Torch 张量并将其作为单样本批次。
后处理函数
多类别分类模型将返回一个 logit 或 softmax 概率列表。但在实际场景中,你更可能需要一个预测类别或带有概率值的预测类别,或者可能是前 N 个预测标签。当然,你可以在主应用程序/其他服务中完成这些操作,但这意味着将应用程序逻辑与 ML 训练过程绑定在一起。因此,让我们直接在响应中返回预测类别。
(为了简单起见,这里的标签列表是硬编码的。在 github 版本中,处理程序从配置中读取标签)
启动 Torch 服务器
好了,模型文件和处理程序都准备好了。现在让我们部署 TorchServe 服务器。上面的代码假设你已经安装了 pytorch。另一个前提条件是安装了 JDK 11(注意,仅 JRE 不够,你需要 JDK)。
对于 TorchServe,你需要安装两个包:torchserve 和 torch-model-archiver。
成功安装后,第一步是准备一个 .mar 文件——包含模型工件的档案。torch-model-archiver 的 CLI 接口旨在实现这一点。终端中输入:
torch-model-archiver --model-name fashion_mnist --version 1.0 --model-file path/model.py --serialized-file path/fashion_mnist_model.pth --handler path/handler.py
参数如下:
-模型名称:你想给模型起的名称
-版本:用于版本控制的语义版本
-模型文件:包含模型架构类定义的文件
-序列化文件:来自 torch.save() 的 .pth 文件
-处理器:包含处理器的 Python 模块
结果是一个名为模型名称的 .mar 文件(在这个例子中是 fashion_mnist.mar)将在执行 CLI 命令的目录中生成。因此,最好在调用命令之前 cd 到你的项目目录。
下一步是启动服务器。终端中输入:
torchserve --start --model-store path --models fmnist=/path/fashion_mnist.mar
参数:
-模型存储:存放 mar 文件的目录
-模型:模型名称和对应 mar 文件的路径。
注意,归档器中的模型名称定义了你的 .mar 文件将如何命名。torchserve 中的模型名称定义了调用模型的 API 端点名称。所以,这些名称可以相同也可以不同,取决于你。
执行这两个命令后,服务器应该启动并运行。默认情况下,TorchServe 使用三个端口:8080、8081 和 8082 分别用于推理、管理和指标。打开浏览器/curl/Postman 发送请求到
http://localhost:8080/ping
如果 TorchServe 正常工作,你应该看到 {‘status’: ‘Healthy’}
图片来源于作者
一些可能问题的提示:
1. 如果在 torchserve -start 命令后你在日志中看到提到“…no module named captum”的错误,那么请手动安装它。我在 torchserve 0.7.1 中遇到了这个错误
2. 可能会有某些端口已经被另一个进程占用。那你可能会看到 ‘Partially healthy’ 状态和日志中的一些错误。
要检查哪个进程使用了 Mac 上的端口(例如 8081),输入:
sudo lsof -i :8081
一种选择是终止进程以释放端口。但如果该进程很重要,这可能并不是一个好主意。
另外,可以在简单的配置文件中为 TorchServe 指定任何新的端口。假设你有一个已经在 8081 端口上运行的应用程序。通过创建包含一行的 torch_config 文件来更改默认的 TorchServe 管理 API 端口:
management_address=https://0.0.0.0:8443
(你可以选择任何空闲端口)
接下来我们需要让 TorchServe 知道配置。首先,通过以下命令停止不健康的服务器
torchserve --stop
然后重新启动它:
torchserve --start --model-store path --models fmnist=/path/fashion_mnist.mar --ts-config path/torch_config
请求模型的预测
在这一步假设服务器已正确启动。让我们将随机的衣物图像传递给推理 API 并获取预测标签。
推理的端点是
http://localhost:8080/predictions/model_name
在这个例子中是 http://localhost:8080/predictions/fmnist
让我们使用 curl 传递一个图像:
curl -X POST http://localhost:8080/predictions/fmnist -T /path_to_image/image_file
例如,使用 repo 中的示例图像:
curl -X POST http://localhost:8080/predictions/fmnist -T tshirt4.jpg
(X 标志用于指定方法 /POST/,-T 标志用于传输文件)
在响应中,我们应该看到预测的标签:
图片由作者提供
结论
好吧,通过跟随这篇博客文章,我们能够创建一个 REST API 端点,我们可以向其发送图像并获取图像的预测标签。通过在服务器上重复相同的过程,而不是本地机器,一个人可以利用它来为面向用户的应用程序、其他服务,或者例如流式机器学习应用程序创建一个端点(参见这篇有趣的论文了解为什么你可能不应该这样做:https://sites.bu.edu/casp/files/2022/05/Horchidan22Evaluating.pdf)
敬请关注,在下一部分中我将扩展示例:让我们为业务逻辑创建一个 Flask 应用程序的模拟,并通过 TorchServe 调用一个机器学习模型(并使用 Kubernetes 部署所有内容)。
一个简单的用例:面向用户的应用程序,具有大量业务逻辑和许多不同的功能。比如,一个功能是上传图像,将所需的样式应用于图像,使用样式迁移机器学习模型。机器学习模型可以通过 TorchServe 提供,因此机器学习部分将完全与主应用程序中的业务逻辑和其他功能解耦。
为 2024 年数据科学家的更高质量工作与生活平衡设定这些界限
这 5 条不可妥协的原则将帮助你让 2024 年成为你最平衡的一年
·
关注 发表在Towards Data Science ·6 分钟阅读·2023 年 11 月 4 日
–
图片由Leonardo Iheme拍摄,来源于Unsplash
工作与生活的平衡是每个人都渴望的,但只有少数人有勇气去实现。
在 Google 上“工作与生活平衡”有 29 亿个搜索结果,这很明显它是我们都在追求的东西。它不仅成为了我们搜索的重点,而且在过去三年里,它似乎逐渐进入了我们的日常对话中。
到 2020 年,数据科学被视为一种能够提供大家谈论的神秘工作与生活平衡的职业。然而,许多人似乎意识到,从事某种数据科学工作可能和其他任何工作一样,甚至因为我们更普遍的“灵活”工作安排(老板似乎觉得需要更加紧握我们的手,以缓解他们对微管理的不安)而更加消耗生活。
不幸的是,工作与生活平衡并不是总能得到的。有时候,它需要通过设定界限和非谈判项来争取。随着 2024 年仅剩两个月的时间,现在是准备如何恢复你的工作与生活平衡的完美时机,以使即将到来的一年成为你最平衡的一年。以下是你需要设定的五个界限,以获得 2024 年更高质量的工作与生活平衡。
1. 准备一个文档系统
不可避免地,项目里程碑将会被超越,预算会紧张,时间表会混乱。当这种情况发生时,你可能会成为承担你团队领导、客户,或者更糟糕的,你的老板全部怒火的“出气筒”。
然而,你也很有可能是唯一一个点了 i 的点和画了 t 的那个人。因此,为了你的工作与生活平衡,创建一个支持你在事情出错时能够证明你是在做自己工作的文档系统是很重要的。
我最喜欢记录这些互动方式之一是使用电子表格。在那里,你可以创建一个简单的文档,每一行都是一个独特的事件/邮件/对话(按需删除),列出如事件 ID(因为我们是数据科学家,不是吗?没有这个就会一片混乱)、日期、你互动的人的名字、问题、他们的回应、你的回应、是否跟进、解决方案是什么、是否需要升级措施等信息。我保持这种文档全天候打开,以确保我遇到的一切都被记录下来。
尽管这可能看起来繁琐或过度,但我从技术工作中学到的唯一一件事是:记录、记录、记录,这将始终避免你做更多不必要的工作。
2. 将项目时间表设定为所需时间的两倍
任何在技术行业工作超过两分钟的人都知道,项目总是会比预期花费更长的时间。因此,在你将重新获得工作与生活平衡的那一年,你需要给出项目所需时间的现实估计——换句话说,总是说项目会花费至少两倍的时间,因为它们总是会这样。
数据可能意外不可用,你的团队可能生病,你的代码可能会让软件开发人员难以将其制作成生产级的产品,你的客户可能会在你准备发布的前一周完全改变他们的要求,等等。
为了你的合同工时(顺便提一下,你绝对不应超时工作,见下文),你必须为项目设定适当的时间表,使你能够在经历所有脱轨情况的情况下,仍能生产高质量的工作,而不牺牲你的工作与生活平衡。没有什么比压力更能迅速降低项目质量,这就是为什么适当的数据清理、分析和可视化的工作最好在冷静的头脑和看似遥远的截止日期下进行。更好的是,如果你能提前完成项目,你就会低估承诺、高估交付,这应该真正成为你作为数据科学家的座右铭。
3. 其他人的糟糕计划不等于你的紧急情况
有时,不是你设定适当的项目截止日期,而是由其他人为你设定。这些截止日期不切实际,并且会影响你的工作与生活平衡。
解决方案?
告诉人们你不会满足他们的不切实际的截止日期,他们应该首先与你商量,以防止类似问题再次发生。
哎呀。我能理解这对刚入行的数据科学家来说听起来很可怕。然而,我也从经验中知道,如果你从一开始就不为自己站出来,后面将会困难得多,以至于最终可能更容易离开工作,而不是试图收紧你不断扩大的责任、项目和不切实际的截止日期。
数据分析最好不要急于求成,即使是看似微不足道的任务,如改变矩阵散点图上的颜色。虽然你会随着时间的推移在某些任务上变得更快,自动化也可能承担一些繁重的工作,但对依赖你分析结果的客户来说,提供半吊子的结果是毫无意义的。你呈现的结论和策略可能对客户(不是对个人,而是对公司)具有改变人生的影响,这意味着你需要对你的发现非常确定。因此,你不应该被催促。最终大家都会受益,你可能需要提醒他们这一点。
4. 永远不要为了虚假的截止日期加班
通过开始坚持对上述不合理计划的截止日期,你将已经在实现下一个不可谈判目标上取得了良好的进展。下一步是你永远不要为了虚假的截止日期加班。
人为的截止日期是一种让你突然在晚餐时间开始检查电子邮件、在周末推送代码,以及让团队聊天演变成另一个完全“快速”对接编程会话的快速简便方法。
2024 年是你拒绝为了人为截止日期而加班的时机,而是将这种有时必要的邪恶仅限于最极端和特殊的情况。但说实话,数据科学家何时真的需要加班呢?这种情况非常少见,虽然这些时候似乎出现的频率很高,尤其是当数据科学家不仅是数据科学家,还兼任系统分析师、他们工作区的本地 IT 帮助台,甚至可能还是让一切具备生产就绪状态的软件开发者。
关键在于,为了保持工作与生活的平衡,你需要对加班的紧急性诚实。因为只要你在公司内的工作情况健康,就没有真正必要的“数据紧急情况”加上人为截止日期。
5. 规定质量是你唯一的操作方式
客户希望得到项目三角形的所有三个方面:速度、成本和质量。这是生活的一个事实。
作为公司为数不多的数据科学家之一,某种程度上这自动成为了你的职责,即向客户阐明数据分析项目的运作方式,尤其是为什么速度从来不是解决问题的答案。虽然软件部门可以在几个小时内快速组装一个产品管理系统,但对于预测未来一年商业趋势的深入数据分析则需要一些技巧。
换句话说,当你的结果可能决定客户未来业务的走向时,质量永远不会因为速度而被舍弃。最终总是会以不好的结局告终。客户会从你多花几个小时来优化预测模型中受益,特别是当复杂变量起作用时,更详细的见解将从一个没有被为了几天速度而匆忙完成的分析中获得。
订阅以将我的故事直接发送到你的收件箱:Story Subscription
请成为会员,以通过我的推荐链接无限访问 Medium(这不会额外增加你的费用,我会获得少量佣金):Medium Membership
订阅我的通讯,以获得更多带有环保主义色彩的独家数据驱动内容:DataDrivenEnvironmentalist
通过捐赠支持我的写作,以资助创作更多类似的故事:Donate
为数据科学设置 Flask 应用
原文:
towardsdatascience.com/setting-up-a-flask-application-for-data-science-7522fc9f771e
构建一个 Flask 应用的基本结构,以支持模块化开发
·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 3 月 10 日
–
照片由 KOBU Agency 提供,来源于 Unsplash
数据科学工作流程通常涉及笔记本和 Python 脚本的使用。这些都是很棒的工具,但通常意味着你的输出可能会一直保留在这些文件中,无法展示。然而,改变这种情况的一个好方法是创建一个网站来展示和讨论你的发现,或者创建一个 API 将你的模型提供给全世界。Flask 是一个可以帮助实现这一点的框架。
Flask 允许你构建网站和 API,使你可以更广泛地分享你的成果。无论是通过一个展示你的工作和结果的界面,还是通过一个其他人可以调用以获取模型预测的 API,Flask 都能实现。Flask 是一个轻量级框架,易于学习和使用,非常适合希望专注于构建模型和分析数据的 Data Scientist,而不是学习复杂的 Web 开发框架。它也使用 Python,因此数据科学工作流程中的许多步骤可以很容易地转移到 Flask 框架中。
在本文中,我将向你展示如何设置一个 Flask 应用的基本框架,之后你可以在其基础上进行扩展。这将包括解释应用的基本结构以及你希望放入每个文件的内容。
Flask 项目结构
我们可以从基本的 Flask 结构概述开始。它的形式如下:
application/
|-static/
|--css/
|--images/
|--scipts/
|-templates/
|--includes/
|--index.html
|-__init__.py
|-routes.py
tests/
flask_env/
.env
.env.template
.gitignore
flask_config.py
app.py
requirements.txt
其中:
-
application/
目录包含主要的 Flask 应用代码 -
static/
目录包含网站中使用的静态文件,如 CSS 样式表、JavaScript 脚本和图片 -
templates/
目录包含 Flask 应用使用的 HTML 模板 -
__init__.py
初始化 Flask 应用程序并设置配置和数据库连接。 -
routes.py
定义了 Flask 应用程序的路由。 -
tests/
目录包含 Flask 应用程序的测试文件。 -
flask_env
包含 Flask 应用程序的虚拟环境。 -
.env
文件包含 Flask 应用程序使用的环境变量。 -
flask_config.py
包含 Flask 应用程序的配置设置。 -
app.py
是 Flask 应用程序的入口点,并运行 Flask 开发服务器。
那么,我们如何开始呢?
设置环境。
要开始构建这个 Flask 应用程序,你需要创建一个 Python 虚拟环境。这是一个好习惯,有助于将应用程序的依赖项与机器上的全局 Python 环境隔离开来。这使得管理依赖项变得更容易,并确保应用程序在不同机器上顺利运行。
为此,你需要首先打开一个终端窗口,并导航到你想要创建虚拟环境的目录。然后运行以下命令:
python -m venv <venv_name>
在这种情况下,我将其命名为 flask_application
,但也可以简单地命名为 env
。
然后你可以使用以下命令激活环境:
#on windows
<venv_name>\Scripts\activate
#on Max or Linux
source <venv_name>/bin/activate
并安装常用库。在这种情况下,我们将使用的基本库是 flask
(当然)、python-dotenv
和 pytest
。要安装这些库,你可以运行以下命令:
pip install flask python-dotenv pytest
这些库应该安装在你的虚拟环境中。请注意,有时这可能需要一些时间,所以不要太担心!
当对虚拟环境进行更改时,例如安装新库时,最好冻结要求,以便其他人知道你使用了哪些库及其版本。这可以通过以下命令完成:
pip freeze > requirements.txt
这将创建 requirements.txt
文件。这将使其他人更容易在自己的机器上复制你的环境。
基础文件。
一旦你设置好虚拟环境并创建了 requirements.txt
文件,你就可以开始构建你的应用程序。因此,我们可以从目录结构底部的文件开始:
.env
.env.template
.gitignore
flask_config.py
app.py
你需要首先从 .env
文件开始,该文件用于设置应用程序的环境变量。刚开始时,它看起来像这样:
# Flask server configurations
FLASK_APP=app
FLASK_ENV=development
FLASK_DEBUG=1
# Secret key
SECRET_KEY = b'nnz\x89\x0f\xde\x8a\xc4\x13\xe0\xf0\xca>=\xe0\xfe'
第一行告诉 Flask 应用程序应用程序文件叫做 app.py
(尚未创建)。使用 flask run
命令时,这将告诉应用程序运行 app.py
文件以启动应用程序。
我们还从开发环境开始,因为这只是目前在本地机器上。为此,我们设置FLASK_ENV = development
和FLASK_DEBUG=1
,这表明我们有一个开发环境,并在环境启动时运行 Flask 调试器。这允许热重载,因此当您对代码进行任何更改时,不必重启实例即可在浏览器中查看这些更改!
此文件中的最后一行是秘密密钥。这是一个加密密钥,用于加密会话数据和客户端与服务器之间传输的其他敏感数据,是重要的安全措施。在这种情况下,我们使用一个 16 位密钥,该密钥通过以下命令生成:
python3 -c "import os; print(os.urandom(16))"
如果您有其他信息或密钥需要应用程序使用,例如 API 密钥,可以将这些信息添加到此文件中。由于这些是环境变量,通常使用蛇形命名法(空格替换为_
),所有字母都大写。
当然,除非您与他人合作并希望安全地共享,否则您实际上不希望与任何人分享您的秘密或 API 密钥。这就是为什么您需要复制.env
文件来创建.env.template
文件,并删除您希望保密的任何信息。其他开发人员可以使用.env.template
文件创建自己的.env
文件,填入自己的密钥和信息,或在必要时安全地共享自己的密钥。
这意味着在您的.gitignore
文件中,您只需写入:
.env
这将告诉 git 忽略对.env
文件的任何更改,以确保您的秘密不会被共享。
下一阶段是创建flask_config.py
文件,用于为您的应用程序创建一个配置对象。在我们的案例中,这个对象的形式为:
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
def __init__(self):
"""Base configuration variables"""
self.SECRET_KEY = os.environ.get("SECRET_KEY")
if not self.SECRET_KEY:
raise("Secret key is missing. Something is wrong")
该密钥使用了在.env
文件中定义的SECRET_KEY
变量。如果没有找到秘密密钥,将会引发错误,并阻止应用程序运行。这是为了确保应用程序按预期运行,并且是安全的,您可以根据需要添加其他配置参数。
最后,我们有app.py
文件,它是 Flask 应用程序的入口点,用于运行 Flask 开发服务器。目前,这仅包含:
from application import app
if __name__ == "__main__":
app.run()
该结构导入application
文件夹中的app
并运行它。建立了基础结构后,我们可以继续创建应用程序结构。
应用程序结构
我们在仓库中创建一个application
文件夹,以结构化和模块化的方式组织应用程序代码和资源。具体而言,这种结构使得应用程序可以在需要时扩展为更全面的应用程序,通过创建多个路由、模型、视图模型和模板,从而提高代码的可维护性和可扩展性。然而,目前我们只需要一个简单的结构。
我们可以通过创建 __init__.py
文件开始,该文件用于初始化从基础文件夹中的 app.py
文件调用的应用程序。它包含:
from flask import Flask
from flask_config import Config
app = Flask(__name__)
app.config.from_object(Config)
from application import routes
它导入 flask
库和 Config
对象,初始化应用程序,然后导入路由。
重要的是,我们可以看到这个文件中没有定义任何路由或视图。为了保持模块化结构,这些决定应用程序行为的路由是在 routes.py
文件中定义的,与初始化分开。
因此,我们可以将 routes.py
文件定义如下:
from flask import render_template
from application import app
@app.route("/")
def index():
return render_template("index.html")
这里有两个主要要点需要注意:
-
奇怪的装饰器语法
@app.route("/")
。这个装饰器用于定义用户可以访问的路由,接受一个 URL 模式参数并将其与视图函数关联。在这个示例中,我们为根 URL (/
) 定义了一个路由,并将其与index()
函数关联。 -
index()
函数不是返回一个值,而是返回函数render_template("index.html")
。这个函数由 flask 提供,用于允许函数返回预定义的 HTML 模板。
那么这些 HTML 模板是什么呢?
好吧,在 index()
函数中我们返回了 index.html
模板。从文件结构来看,它位于 templates
文件夹中,并包含:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="{{url_for('static', filename='css/style.css')}}">
<title>{% block title %}Hello World{% endblock %}</title>
</head>
<body>
<div class = "container body-content">
<h1>Hello World!</h1>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>
这本质上是一个 HTML 文件,在我们的例子中简单地显示“Hello World”,可以在 container
类中的 <h1></h1>
标签之间看到。
我们还在头部和主体中添加了一些额外的内容,主要用于导入 bootstrap,一个使开发变得更容易的 CSS 框架,以及 JQuery,使应用程序的交互更容易。但如果你愿意,可以忽略这些,你可以简单地在主体中定义 <h1>Hello World!</h1>
!
其他文件和文件夹
我们已经覆盖了应用程序的基本结构及其内容,但还有一些其他文件和文件夹没有涉及:
-
application/templates
— 这个文件夹将包含应用程序使用的所有 HTML 模板。 -
application/templates/includes
— 这个文件夹可以包含你可能想要在布局或其他文件中包含的辅助 HTML 结构,例如导航栏、页脚或大屏幕。 -
tests
— 这个文件夹将包含所有对 flask 应用程序的测试,进一步分为functional
和unit
测试两个文件夹。 -
static
— 这个文件夹包含了应用程序中使用的所有静态文件,包括css
、images
和可能使用的scripts
。
运行应用程序
现在我们已经完成了所有结构设置和环境配置,只需运行命令 flask run
。你应该会看到类似如下的内容:
图片由作者提供
这告诉你应用程序正在运行,并且运行在 http://127.0.0.1:5000
。
这意味着你应该能够在浏览器中导航到这个 URL 或 localhost:5000
并查看输出:
图片由作者提供
这表明应用程序正在运行!恭喜你,成功运行了第一个 Flask 应用!
扩展功能
超越这里展示的基本结构,Flask 具有相当大的灵活性,允许各种不同的扩展:
-
Flask-wtf:一个可以以安全和可靠的方式处理用户输入的库
-
Flask-SQLAlchemy:一个允许你与 SQLAlchemy ORM 及数据库集成的库(还有其他库可以与如 MongoDB 等常见数据库进行交互)
-
Flask-Login:一个提供用户认证功能的库,如果你想根据用户角色管理对应用程序某些部分的访问权限。
在许多更多的库和用例中!这种结构通过允许你以模块化的方式构建应用程序,从而促进了这些扩展,提升了代码的可维护性、可扩展性和模块化。不论你是想通过路由服务你的机器学习算法的 API,还是想在网站上展示你的故事!
本文的代码可以在:github.com/PhilipDW183/flask_structure
获取
设置 Python 项目:第 V 部分
原文:
towardsdatascience.com/setting-up-python-projects-part-v-206df3c1e3d3
掌握 Python 项目设置的艺术:逐步指南
·发表于Towards Data Science ·阅读时间 20 分钟·2023 年 1 月 14 日
–
照片由Zoya Loonohod拍摄,来自Unsplash
无论你是经验丰富的开发者还是刚刚开始接触🐍 Python,了解如何构建稳健且易于维护的项目都很重要。本教程将指导你使用一些行业内最流行且有效的工具来设置 Python 项目。你将学习如何使用GitHub和GitHub Actions进行版本控制和持续集成,以及其他工具进行测试、文档编写、打包和分发。本教程的灵感来源于Hypermodern Python和新 Python 项目的最佳实践。然而,这并不是唯一的方法,你可能有不同的偏好或观点。教程旨在对初学者友好,同时涵盖一些高级主题。在每个部分,你将自动化一些任务,并为你的项目添加徽章,以展示你的进展和成就。
该系列的代码库可以在github.com/johschmidt42/python-project-johannes找到
这一部分的灵感来自于这篇博客文章:
[Python、Poetry 与 GitHub Actions 的语义化发布 🚀
由于我的同事们的兴趣,我计划向 Dr. Sven 添加一些功能。在此之前,我需要…](https://mestrak.com/blog/semantic-release-with-python-poetry-github-actions-20nn)
要求
-
操作系统: Linux、Unix、macOS、Windows(WSL2,例如 Ubuntu 20.04 LTS)
-
工具:python3.10, bash, git, tree
-
版本控制系统(VCS)主机: GitHub
-
持续集成(CI)工具: GitHub Actions
预计你对版本控制系统(VCS)git有所了解。如果不了解,以下是一个复习: Git 介绍
提交将基于 最佳 git 提交实践 和 传统提交。你可以使用 PyCharm 的传统提交插件 或 VSCode 扩展 来帮助你以这种格式编写提交。
概述
-
第二部分(格式化,Linting,CI)
-
第三部分(测试,CI)
-
第五部分(版本控制与发布,CI/CD)
结构
-
Git 分支策略 (GitHub 流程)
-
什么是发布? (zip, tar.gz)
-
语义版本控制 (v0.1.0)
-
手动创建发布 (git tag, GitHub)
-
自动创建发布 (传统提交,语义发布)
-
CI/CD (release.yml)
-
创建个人访问令牌(PAT)
-
GitHub Actions 流程 (编排工作流)
-
徽章 (发布)
-
奖励 (强制执行传统提交)
发布软件是软件开发过程中的重要步骤,因为它使新功能和修复程序可供用户使用。发布软件的一个关键方面是版本控制,它有助于跟踪和传达每个发布中的变化。语义版本控制是一种广泛使用的软件版本控制标准,它使用格式为 Major.Minor.Patch(例如 1.2.3)的版本号来指示发布中所做更改的级别。
传统提交是一种为提交消息添加人类和机器可读意义的规范。它是一种以一致的方式格式化提交消息的方法,这使得确定所做更改的类型变得简单。传统提交通常与语义版本控制结合使用,因为提交消息可以用来自动确定发布的版本号。语义版本控制和传统提交一起提供了一种清晰且一致的方法来跟踪和传达每个软件项目发布中的更改。
Git 分支策略
git 有许多不同的分支策略。很多人倾向于使用GitFlow(或变种)、Three Flow或Trunk based Flows。一些人使用这些策略中的混合策略,例如这个策略。我使用非常简单的GitHub flow分支策略,其中所有的 bug 修复和功能都有各自的独立分支,完成后,每个分支都会合并到主分支并进行部署。简单、好用且易于操作。
GitHub Flow 分支策略
无论你的策略是什么,最终你都会合并一个拉取请求,并(可能)创建一个版本发布。
什么是版本发布?
简而言之,发布就是将一个版本的代码打包(例如压缩文件),并推送到生产环境(这对你来说可能是任何东西)。
版本管理可能会很混乱。因此,需要有一个简明的方法(以及其他人也跟随的方法),定义什么是版本发布,以及一个版本与下一个版本之间的变化。如果你不跟踪版本之间的变化,你可能不会理解每个版本中发生了什么变化,也无法识别新代码中可能引入的任何问题。没有变更日志,很难理解软件如何随着时间的推移而发展。它也可能使回滚更改变得困难(如果必要的话)。
语义化版本控制
语义化版本控制只是一个编号方案和业界的标准实践。它指示了该版本与前一个版本之间的变更程度。一个语义版本号有三个部分,例如1.8.42,遵循以下模式:
- MAJOR.MINOR.PATCH
每个部分代表了不同程度的变化。PATCH 版本发布表示错误修复或微小更改(例如从 1.0.0 到 1.0.1)。MINOR 版本发布表示添加/删除功能或向后兼容的功能更改(例如从 1.0.0 到 1.1.0)。MAJOR 版本发布表示添加/删除功能以及可能的向后不兼容的更改,例如破坏性更改(例如从 1.0.0 到 2.0.0)。
我推荐迈克·迈尔斯的一个讲座,如果你想要一个关于语义版本发布的视觉介绍。它总结了什么是发布,以及如何利用git 标签来创建版本发布。
关于git 标签:git 中有轻量级标签和注释标签。一个轻量级标签只是指向特定提交的指针,而注释标签则是 git 中的一个完整对象。
手动创建版本发布
让我们先手动创建一个版本发布,然后再进行自动化处理。
如果你记得,我们的 example_app 的 __init__.py
文件包含了版本信息。
# src/example_app/__init__.py
__version__ = "0.1.0"
以及 pyproject.toml
文件
# pyproject.toml
[tool.poetry]
name = "example_app"
version = "0.1.0"
...
所以我们首先必须做的是创建一个注释的 git 标签 v0.1.0
并将其添加到主分支的最新提交中:
> git tag -a v0.1.0 -m "version v0.1.0"
请注意,如果在命令末尾没有指定提交哈希,则 git 会使用你当前所在的提交。
我们可以通过以下命令获取标签列表:
> git tag
v0.1.0
如果我们想要再次删除它:
> git tag -d v0.1.0
Deleted tag 'v0.1.0'
并通过以下命令获取有关该标签的更多信息:
> git show v0.1.0
tag v0.1.0
Tagger: Johannes Schmidt <johannes.schmidt.vik@gmail.com>
Date: Sat Jan 7 12:55:15 2023 +0100
version v0.1.0
commit efc9a445cd42ce2f7ddfbe75ffaed1a5bc8e0f11 (HEAD -> main, tag: v0.1.0, origin/main, origin/HEAD)
Author: Johannes Schmidt <74831750+johschmidt42@users.noreply.github.com>
Date: Mon Jan 2 11:20:25 2023 +0100
...
我们可以通过以下命令将新创建的标签推送到 origin:
> git push origin v0.1.0
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 171 bytes | 171.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:johschmidt42/python-project-johannes.git
* [new tag] v0.1.0 -> v0.1.0
使得这个 git 标签现在可以在 GitHub 上使用:
让我们手动在 GitHub 上创建一个新的版本发布,并使用这个 git 标签:
我们点击 Create a new release
,选择我们现有的标签(已经绑定到提交),然后通过点击 Generate release notes
按钮自动生成发布说明,最后用 Publish release
按钮发布该版本。
GitHub 将自动为源代码创建 tar
和 zip
(资产),但不会构建应用程序!结果将如下所示:
总结一下,发布的步骤是:
-
从你的默认分支创建一个新分支(例如功能或修复分支)
-
进行更改并增加版本(例如 pyproject.toml 和 init.py)
-
将功能/错误修复提交到默认分支(可能通过 Pull Request)
-
添加一个 注释的 git 标签(语义版本)到提交中
-
在 GitHub 上发布版本,并附加一些额外信息
自动创建发布
作为程序员,我们不喜欢重复自己。因此,有很多工具可以让这些步骤变得非常简单。在这里,我将介绍Semantic Releases,一个专门为 Python 项目设计的工具。
这是一个自动在你的仓库中设置版本号、用版本号标记代码并创建发布的工具!这一切都是基于 约定式提交 风格消息的内容完成的。
约定式提交
语义版本控制和 conventional-commits 之间有什么联系?
某些提交类型可以用于自动确定语义版本的提升!
-
一个
fix
提交是 PATCH。 -
一个
feat
提交是 MINOR。 -
一个带有
BREAKING CHANGE
或!
的提交是 MAJOR。
其他类型的提交,例如 build
、chore
、ci
、docs
、style
、refactor
、perf
、test
通常不会增加版本。
查看最后的附加部分,了解如何在你的项目中强制执行约定式提交!
自动语义版本发布(本地)
我们可以通过以下命令添加库:
> poetry add --group semver python-semantic-release
让我们深入配置设置,以便自动生成变更日志和发布。在 pyproject.toml
中,我们可以将 semantic_release 作为工具添加:
# pyproject.toml
...
[tool.semantic_release]
branch = "main"
version_variable = "src/example_app/__init__.py:__version__"
version_toml = "pyproject.toml:tool.poetry.version"
version_source = "tag"
commit_version_number = true # required for version_source = "tag"
tag_commit = true
upload_to_pypi = false
upload_to_release = false
hvcs = "github" # gitlab is also supported
-
branch
:指定发布应基于的分支,在这种情况下是 “main” 分支。 -
version_variable
:指定源代码中版本号的文件路径和变量名称。在这种情况下,版本号存储在文件src/example_app/__init__.py
中的__version__
变量中。 -
version_toml
:指定pyproject.toml
文件中版本号的文件路径和变量名称。在这种情况下,版本号存储在pyproject.toml
文件的tool.poetry.version
变量中。 -
version_source
:指定版本号的来源。在这种情况下,版本号来自标签(而不是提交)。 -
commit_version_number
:当version_source = "tag"
时,此参数是必需的。它指定是否将版本号提交到仓库。在这种情况下,它设置为 true,这意味着版本号将被提交。 -
tag_commit
:指定是否为发布提交创建新的标签。在这种情况下,它设置为 true,这意味着将创建一个新的标签。 -
upload_to_pypi
:指定是否将软件包上传到 PyPI 包仓库。在这种情况下,它设置为 false,这意味着软件包不会上传到 PyPI。 -
upload_to_release
:指定是否将软件包上传到 GitHub 发布页面。在这种情况下,它设置为 false,这意味着软件包不会上传到 GitHub 发布页面。 -
hvcs
:指定项目的托管版本控制系统。在这种情况下,它设置为 “github”,这意味着项目托管在 GitHub 上。“gitlab” 也是支持的。
我们可以更新定义项目/模块版本的文件。为此,我们使用变量version_variable
用于普通文件,version_toml
用于*.toml* 文件。version_source
定义了版本的真实性来源。由于这两个文件中的版本与 git 注释标签紧密耦合,例如我们每次发布时自动创建 git 标签(标志tag_commit
设置为 true),我们可以使用源tag
,而不是默认值commit
,后者在提交信息中查找最后一个版本。为了能够更新文件并提交更改,我们需要设置 [commit_version_number](https://github.com/relekang/python-semantic-release/issues/104)
标志为 true。因为我们不想将任何东西上传到 Python 索引 PyPi,所以标志upload_to_pypi
设置为 false。现在我们也不想将任何东西上传到我们的发布页面。hvcs
设置为github
(默认),其他值可以是:gitlab
。
我们可以通过运行几个命令在本地测试这一点,我将直接将这些命令添加到我们的 Makefile 中:
# Makefile
...
##@ Releases
current-version: ## returns the current version
@semantic-release print-version --current
next-version: ## returns the next version
@semantic-release print-version --next
current-changelog: ## returns the current changelog
@semantic-release changelog --released
next-changelog: ## returns the next changelog
@semantic-release changelog --unreleased
publish-noop: ## publish command (no-operation mode)
@semantic-release publish --noop
使用命令current-version我们可以从 git 树中的最后一个 git 标签获取版本:
> make current-version
0.1.0
如果我们以传统提交风格添加一些提交,例如 feat: new cool feature
或 fix: nasty bug
,那么命令 next-version 将计算版本号的增量:
> make next-version
0.2.0
目前,我们的项目中没有 CHANGELOG 文件,因此当我们运行:
> make current-changelog
输出将是空的。但根据提交记录,我们可以使用以下方法创建即将发布的变更日志:
> make next-changelog### Feature
* Add releases (#8)) (`5343f46`))
* Docstrings (#5)) (`fb2fa04`))
* Add application in app.py (`3f07683`))### Documentation
* Add search bar & github url (#6)) (`3df7c48`))
* Add badge pages.yml to README.py (`b76651c`))
* Add documentation to Makefile (#3)) (`2294ee1`))
如果我们推送新的提交(直接到主分支或通过 PR),我们现在可以发布一个新版本:
> semantic-release publish
发布命令将执行一系列操作:
-
更新或创建变更日志文件。
-
将更改推送到 git。
-
运行 build_command 并将分发文件上传到你的仓库。
-
运行 semantic-release changelog 并发布到你的 VCS 提供者。
-
将由 build_command 创建的文件附加到 GitHub 发布中。
每一步当然都可以配置或禁用!
CI/CD
让我们使用 GitHub Actions 构建一个 CI 流水线,每次提交到主分支时运行 semantic-release 的发布命令。
虽然整体结构与 lint.yml、test.yml 或 pages.yml 相同,但有一些变化需要说明。在步骤 Checkout repository
中,我们添加了一个新的 token,用于检出分支。这是因为默认值 GITHUB_TOKEN
没有操作受保护分支所需的权限。因此,我们必须使用一个包含 个人访问令牌 权限的秘密 (GH_TOKEN) 。稍后我会展示如何生成个人访问令牌。我们还定义了 fetch-depth: 0
以提取所有分支和标签的全部历史记录。
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.GH_TOKEN }}
fetch-depth: 0
我们仅安装 semantic-release 工具所需的依赖项:
- name: Install requirements
run: poetry install --only semver
在最后一步,我们更改一些 git 配置并运行 semantic-release 的发布命令:
- name: Python Semantic Release
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
set -o pipefail
# Set git details
git config --global user.name "github-actions"
git config --global user.email "github-actions@github.com"
# run semantic-release
poetry run semantic-release publish -v DEBUG -D commit_author="github-actions <action@github.com>"
通过更改 git 配置,提交的用户将会是“github-actions”。我们以 DEBUG 日志(stdout)运行发布命令,并显式将 commit_author
设置为“github-actions”。除了这个命令,我们还可以直接使用 semantic-release 的 GitHub action,但设置步骤 非常少,且该 action 每次都需要拉取 docker 容器。因此,我更倾向于采用简单的运行步骤。
因为发布命令会生成提交,您可能会担心我们会陷入触发工作流的无限循环。但请放心,生成的提交不会触发另一个 GitHub Actions 工作流运行。这是由于 GitHub 设定的限制。
创建个人访问令牌(PAT)
个人访问令牌是使用密码进行 GitHub Enterprise Server 身份验证的替代方案,当使用 GitHub API 或 命令行 时。个人访问令牌旨在代表您访问 GitHub 资源。要代表组织访问资源或用于长期集成,您应该使用 GitHub 应用。有关更多信息,请参见“关于应用”。
换句话说:我们可以创建一个 Personal Access Token,并让 GitHub Actions 存储并使用该 secret 代表我们执行某些操作。请记住,如果 PAT 被泄露,可能会被用于对您的 GitHub 仓库执行恶意操作。因此,建议在组织中使用 GitHub OAuth 应用和 GitHub 应用。为了本教程的目的,我们将使用 PAT 允许 GitHub Actions 流水线代表我们操作。
我们可以通过导航到 GitHub 用户的 Settings
部分并按照 创建个人访问令牌 中总结的说明来创建新的访问令牌。这将给我们一个看起来像这样的窗口:
具有推送访问权限的管理员帐户的个人访问令牌。
通过选择作用域,我们定义令牌将具有的权限。对于我们的用例,我们需要 push access 权限,因此新的 PAT GH_TOKEN
应该具有 repo
权限作用域。该作用域将授权对受保护分支的推送,前提是您没有在受保护分支的设置中启用 包括管理员。
回到代码库概览,在 设置 菜单中,我们可以在 密钥 部分添加环境设置或库设置:
仓库密钥特定于单个仓库(及其中使用的所有环境),而环境密钥特定于环境。GitHub 运行器可以配置为在特定环境中运行,这允许它访问该环境的密钥。这在考虑不同阶段(例如 DEV 与 PROD)时是有意义的,但对于本教程,我对仓库密钥感到满意。
GitHub Actions 流程
现在我们有了几个管道(linting、testing、releasing、documentation),我们应该考虑主分支提交的动作流程!有一些我们需要注意的事项,其中一些是特定于 GitHub 的。
理想情况下,我们希望主分支的提交创建一个推送事件,从而触发测试和 linting 工作流。如果这些工作流成功,我们将运行发布工作流,该工作流负责基于传统提交检测是否需要版本提升。如果是这样,发布工作流将直接推送到主分支,提升版本,添加 git 标签并创建发布。发布的版本应当例如通过运行文档工作流来更新文档。
预期的动作流程
问题与考虑
- 如果你仔细阅读上一段或查看上面的流程图,你可能会注意到有两个主分支的提交。一个是初始的(即来自 PR),另一个是用于发布的。由于我们的lint.yml和test.yml在主分支的推送事件下会触发,因此它们会运行两次!为了节省资源,我们应该避免两次运行。为此,我们可以在版本提交消息中添加
[skip ci]
字符串。可以在pyproject.toml文件中为工具semantic_release定义自定义提交消息。
# pyproject.toml
...
[tool.semantic_release]
...
commit_message = "{version} [skip ci]" # skip triggering ci pipelines for version commits
...
2. 工作流pages.yml当前在推送到主分支时运行。更新文档可能只是我们希望在有新版本发布时做的事情(我们可能会在文档中引用版本)。我们可以相应地更改pages.yml文件中的触发器:
# pages.yml
name: Documentation
on:
release:
types: [published]
现在,构建文档将需要已发布的版本。
3. 发布工作流应该依赖于 linting 和 testing 工作流的成功。目前,我们在工作流文件中没有定义依赖关系。我们可以让这些工作流依赖于特定分支上定义的工作流运行的完成,使用[workflow_run](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run)
事件。然而,如果我们为workflow_run
事件指定多个workflows
:
on:
workflow_run:
workflows: [Testing, Linting]
types:
- completed
branches:
- main
仅一个工作流需要完成!这不是我们所期望的。我们期望所有工作流都必须完成(并成功)。只有在这种情况下,发布工作流才应运行。这与在单个工作流中定义作业之间的依赖关系时得到的结果相反。更多关于这种不一致和不足的内容,请阅读这里。
作为替代方案,我们可以使用流水线的顺序执行:
这种想法的一个大缺点是它 a) 不允许并行执行和 b) 我们将无法在 GitHub 中查看依赖图。
解决方案
目前,我认为解决上述问题的唯一方法是将工作流在一个协调器工作流中进行协调。
让我们创建这个工作流文件:
当我们推送到 main
分支时,协调器被触发。
只有在两个工作流:Testing 和 Linting 都成功时,才会调用发布工作流。这在 needs
关键字中定义。如果我们希望对作业执行(工作流)有更细致的控制,也可以考虑使用 if
关键字。但要注意,如 这篇文章 所述的令人困惑行为。
为了使我们的工作流 lint.yml
、test.yml
和 release.yml
可以被另一个工作流调用,我们需要更新触发器:
# lint.yml
---
name: Linting
on:
pull_request:
branches:
- main
workflow_call:
jobs:
...
# test.yml
---
name: Testing
on:
pull_request:
branches:
- main
workflow_call:
jobs:
...
# release.yml
---
name: Release
on:
workflow_call:
jobs:
...
现在,新工作流(Release)应该仅在质量检查工作流成功的情况下运行,这里指的是 linting 和 testing。
徽章
要创建徽章,这次我将使用平台 shields.io。
这是一个为项目生成徽章的网站,徽章显示诸如版本、构建状态和代码覆盖率等信息。它提供了广泛的模板,并允许定制外观和创建自定义徽章。徽章会自动更新,提供项目的实时信息。
对于发布徽章,我选择了 GitHub release (latest SemVer)
:
徽章的 Markdown 可以复制并添加到 README.md 中:
我们的 GitHub 登录页面现在看起来是这样的 ❤(我稍微整理了一下,并提供了描述):
恭喜!你已经完成了本教程的主要部分!你已经学习了管理 软件发布 的基本步骤。我们首先手动创建了一个发布,然后利用 Conventional Commits 的力量,通过 CI pipeline 自动化我们的发布过程,处理版本控制。最后,我们在 README.md 文件中添加了 徽章,为用户提供了项目最新版本的清晰而简洁的显示。掌握了这些技巧,你将能够高效而有效地管理你的软件发布。
下一部分将是最后一部分,涵盖:容器化!
[## 通过我的推荐链接加入 Medium - Johannes Schmidt
阅读 Johannes Schmidt 的每一篇故事(以及 Medium 上其他数千名作者的故事)。您的会员费直接…
奖励
确保使用规范提交
我们已经看到,按照定义格式的提交可以帮助我们进行版本控制。在一个协作项目中,我们可能希望对所有提交到默认分支的提交强制执行这种格式。两个流行的工具可以帮助开发者遵循规范提交格式:
然而,一些开发者觉得这些工具有点限制性,因此避免使用它们*。所以仅仅希望总是有规范提交可能不是一个好主意。因此,在服务器端强制执行规则,如规范提交格式,才是明智的!
*同样适用于 pre-commit 钩子,这也是我在这一系列中排除它们的原因。
不幸的是,目前(2023 年 5 月)在 GitHub 上基于规则阻止提交仍然不可行,因为 该功能仍在开发中。但我们可以通过分支保护规则和CI 工作流尽可能接近这个目标。以下是我们在仓库中需要的策略:
-
对受保护的默认分支(例如 main)的提交应该限制为拉取请求(PR)提交。
-
只有 压缩提交 应该被允许*。
-
合并拉取请求时展示的默认提交信息应该是拉取请求 标题。
如果对受保护的默认分支(例如 main)的唯一提交方式是通过拉取请求(压缩提交 仅限),我们可以使用 GitHub Action,如 amannn/action-semantic-pull-request,确保拉取请求的标题符合 规范提交规范。这样,当我们 squash and merge
PR 分支(假设所有必需的流水线成功)时,建议的提交信息就是 PR 标题,该标题之前由 GitHub action 运行检查过。
*Squash and merge 策略是一种流行的代码合并方法,它将功能分支中的多个提交合并为一个提交。这种方法创建了一个线性的、一致的 git 历史记录,其中每个提交代表一个特定的更改。然而,这种方法也有其缺点,因为它丢弃了详细的提交历史记录,这对于理解开发过程是有价值的。虽然可以使用 rebase 合并来保留这些信息,但这可能会给工作流带来复杂性。从这个角度来看,squash and merge 策略因其简单性而受到青睐。
工作流
让我们为这个策略创建 GitHub Actions 工作流:
触发事件 pull_request_target
的解释见 这里。我使用了建议的类型 opened
、edited
、synchronize
。GITHUB_TOKEN
被作为 env
传递给 action。因此,每当 PR 的标题发生变化时,管道就会触发。只有当 PR 的标题符合约定的提交格式时,管道才会成功。
请注意
你需要在主分支中拥有此配置,以便 action 能够运行(例如,它不会在初次添加 action 的 PR 中运行)。此外,如果你在 PR 中更改配置,当前 PR 中的更改将不会被反映 —— 只有在更改被合并到主分支后,随后的 PR 才会反映这些更改。
所以我们必须首先在默认分支 main
中拥有这个工作流,只有这样我们才能看到它的实际效果。
分支保护规则
接下来,在 GitHub 仓库的 设置 部分,我们可以为 main 分支创建一个 分支保护规则:
主要分支的分支保护规则 — 作者提供的图片
现在一个提交需要一个通过状态检查(必需工作流)的 PR 才能合并*。
一个 必需的工作流 由拉取请求事件触发,并作为必需的状态检查出现,这会阻止合并拉取请求,直到必需的工作流成功。
所需工作流 — 作者提供的图片
组织所有者有能力在其组织内强制执行特定的工作流,例如要求对拉取请求进行状态检查。不幸的是,这个功能仅对组织可用,个人账户无法激活,因此无法阻止合并。
*请注意,规则不会在私有仓库中生效,直到它被 迁移到 GitHub Team 或 Enterprise 组织账户!
Squash & merge 策略
最后,我们可以配置 PR 选项,以便在选择 squash and merge 按钮时使用 PR 的标题作为默认提交消息:
默认的提交消息在“压缩和合并”时 — 图片由作者提供
这样,我们会看到一个类似这样的窗口:
在 PR 中的“压缩和合并”对话框 — 图片由作者提供
请注意,开发者可能会在合并过程中更改标题名称,这将绕过策略!
尽管我们还不能完全确保在 GitHub 上使用传统的提交方式,但我们应尽量做到尽可能接近。