TowardsDataScience-2023-博客中文翻译-三十一-
TowardsDataScience 2023 博客中文翻译(三十一)
精通模块化编程:如何提升你的 Python 技能
编写模块化 Python 代码的最佳实践
·发表于Towards Data Science ·阅读时间 9 分钟·2023 年 4 月 10 日
--

我最近写了一篇关于Python 类的文章,其中提到类在 Python 中经常作为模块使用。
在这篇文章中,我将描述什么是模块,如何使用模块,以及为什么你应该在 Python 中使用模块。
在这篇文章的最后,如果你从未使用过模块,你将改变看法。你会改变你的编程方式,并在每次可能的情况下使用模块。我对此深信不疑。
我能理解你的感受:当我发现了模块及其强大功能时,我开始在每次可能的情况下使用它们。
我和你之间唯一的区别是我没有找到任何完整而有价值的资源来讲解你需要了解的所有关于 Python 模块的知识:这就是我创建这篇文章的原因。
我发现了一些零碎的资源。感谢这些资源和高级开发者的建议,经过几天的学习,我对 Python 模块有了全面的了解。
在阅读完这篇文章后,我相信你也会清楚地理解这个主题,并不需要其他资料去了解它。因此,如果你是 Python 新手,或者你是一个“摸索”中的 Python 爱好者但从未使用过模块,那么这篇文章绝对适合你。
为什么你应该将 Python 代码模块化
首先,让我们从一些你应该在 Python 中使用模块的理由开始讨论。我找到了 6 个理由:
-
更好的代码组织。模块允许开发者将代码组织成可重用的单元;这些单元可以被导入并在其他程序中使用。这意味着你不再需要把你的 Python 应用程序看作是一个写满所有代码的整体程序。你会有一个主文件,在其中加载和使用模块。我们将在接下来的段落中以实际的方式看到这个理念。
-
代码重用性。这是模块的一个优点:你创建一个独立的程序,可以在其他程序中导入和重用。可能需要稍微修改一下;但通常,你可以直接使用。这帮助我们节省了大量时间和编写代码的量:编写类似的代码发生的频率比你想象的要高。
-
命名空间管理。如果你曾经用任何语言编码,你肯定遇到过需要命名变量的情况。随着代码在应用程序中增长,你可能需要写出具有相似名称的变量。模块化帮助我们修复名称,以便我们不必每次都重新发明轮子,因为我们将使用在模块中创建的名称,即使在主文件中也是如此。
-
协作。当在公司工作或与朋友合作进行分布式项目(特别是使用 Git)时,模块化允许我们独立工作在模块上。这样,我们可以避免在同一文件中出现重叠问题。
-
更容易调试。如果你调试过一个 200 行的应用程序,你就知道这有多痛苦。找到一个有几十行代码的错误可能需要几天时间。模块化避免了所有这些痛苦,因为模块彼此独立。它们代表了小程序,因此我们可以比调试一个整体应用程序更容易地调试它们。
-
性能优化。模块可以优化你的机器性能,因为我们可以只导入我们在特定应用程序中需要的代码。
Python 中的模块是什么?
正如[1]所说:“在 Python 中,模块指的是包和库,但也包括任何可以与其他代码分离且可以独立工作的代码片段”。
基本上,每个 Python 程序都是一个模块,因为它可以独立工作。但这并不意味着我们可以导入所有模块。或者,更好地说:这并不意味着将一个模块导入到另一个文件中总是有意义的。
例如,假设你想创建一个 Python 文件,检查是否存在三个文件夹。如果不存在,它将创建这些文件夹。它可能类似于这样:
import os
# Define folders name
folders = ["fold_1", "fold_2", "fold_3"]
# Check if folders exist. If not, create folders
for folder in folders:
if not os.path.isdir(folder):
os.makedirs(folder)
这个文件可以保存为folders.py并运行。它可以独立工作,因此是一个模块。但让我问你一个问题:将它导入到另一个 Python 文件中是否有意义?
停下来思考一下,并回答这个问题。
好吧,答案是否定的,原因很简单:它太具体了。我的意思是:如果你需要将这三个文件夹重新创建到另一个文件夹中,你可以将文件移动到新文件夹中并运行它。
因此,当我们导入模块时,我们想要的是通用性。这就是为什么我们创建类和函数作为模块:因为它们是通用的。如果你想在其他文件中重用上述模块,你必须使它通用。例如,我们可以创建一个如下的函数:
import os
def create_folders(fold_1, fold_2, fold_3):
# Define folders name
folders = [fold_1, fold_2, fold_3]
# Check if folders exist. If not, create folders
for folder in folders:
if not os.path.isdir(folder):
os.makedirs(folder)
因此,这个函数可以用来检查在一个特定的文件夹中是否存在三个通用的文件夹。但这次,名称可以是任何的,因为它们作为函数的参数传递。如果它们不存在,则会被创建。所以,例如,我们可以这样调用它:
# Invoke the function
create_folders("audio", "image", "documents")
然后我们的函数会检查目录中是否存在“audio”、“image”和“documents”作为文件夹。如果不存在,它会创建这些文件夹。
这段代码适合被导入到另一个 Python 文件中,因为它是通用的:无论何时我们需要检查三个文件夹,我们都可以使用它。我们只需声明这三个文件夹的名称即可。
如何在 Python 中使用模块
从示意图来看,模块是这样工作的:

模块工作示意图。作者图片。
我们有一个通常可以称为 main.py 的“主” Python 文件。在该 Python 文件中,我们导入了两个模块。
也许你没有注意到,但在上述代码中,我们导入了 os 模块,它是一个帮助创建、管理和删除目录的 Python 模块。这意味着在管理目录时,我们不需要创建自定义的函数(或类):我们可以使用 os 模块并将其应用于我们的具体情况。例如,我们使用了 os.makedirs 来创建文件夹。
但除了已知的模块和包,我们如何在日常编程活动中使用 Python 的模块化呢?好吧,如果你从未使用过模块化的力量,你需要“转换”你的思维;但相信我,这值得一试!
如果你不使用模块,你的 Python 应用程序看起来会像这样:

一个单体 Python 应用程序。作者图片。
所以,在一个文件中你:
-
创建函数和类。
-
调用使用变量或其他你可能需要的内容创建的函数和类。
相反,如果你使用模块,你的 Python 应用程序会变成这样:

一个模块化的 Python 应用程序。作者图片。
因此,这样,你在单独的 Python 文件中创建两个函数和两个类——这些都是模块。然后,你将它们导入到主文件中。
此时,你的文件夹组织应该是这样的:
project_name
├── main.py
│ └── packages
│ └── __init__.py
│ └── module_1.py
│ └── module_2.py
│ └── module_3.py
└── └── module_4.py
当然,你可以创建你所需的任何数量的 packages 子文件夹。
重要的是,每个子文件夹中必须有一个 __init__.py 文件。
让我们看看它是什么,以及如何使用它。
Python 的一个实际示例
那么,让我们做一个实际的 Python 示例。我们创建一个名为 my_project 的项目,其中包含:
-
一个
main.py文件是我们的主要文件。 -
一个名为
operations的子文件夹,其中我们将有三个 Python 文件:__init__.py、addition.py、subtraction.py。
所以,这是我们的结构:
my_project
├── main.py
│ └── operations
│ └── __init__.py
│ └── addition.py
└── └── subtraction.py
我们想创建两个简单的模块,用于计算两个整数的和与差。如果输入不是整数,程序将返回错误。
所以,这两个模块可以是这样的:
def sum(x: int, y: int) -> int:
""" this function sums two integers.
Args:
param 1: integer
param 2: integer
Returns:
an integer that is the sum of the two arguments
"""
if type(x) and type(y) == int:
print(f"the sum of these two numbers is: {x+y}")
else:
print("arguments are not integers!")
if __name__ == "__main__":
sum()
和这些:
def diff(x: int, y: int) -> int:
""" this function subtracts two integers.
Args:
param 1: integer
param 2: integer
Returns:
an integer that is the difference of the two arguments
"""
if type(x) and type(y) == int:
print(f"the difference of these two numbers is: {x-y}")
else:
print("arguments are not integers!")
if __name__ == "__main__":
diff()
**NOTE:**
If you are not familiar with this way of writing Python with type hints,
and if you don't know how ' if __name__ == "__main__"' work, you
have to read my article [here](https://medium.com/towards-data-science/python-classes-made-easy-the-definitive-guide-to-object-oriented-programming-881ed609fb6) and eveything will be clarified.
现在,我们希望将这两个模块导入到主文件中。为此,我们必须在 __init__.py 中写下以下内容:
__all__ = ["sum", "diff"]
所以,__init__.py 是一个在处理模块时必需的 Python 文件,因为它以某种方式告诉主文件在 operations 文件夹中有一些包需要导入。因此,在 __init__.py 中,我们必须声明所有包中的函数,如上所示。
现在,main.py 可以像这样编程:
from operations.addition import sum
from operations.subtraction import diff
# Sum two numbers
a = sum(1, 2)
# Subtract two numbers
b = diff(3,2)
# Print results
print(a)
print(b)
关于主文件,有几个考虑因素:
-
由于这些模块每个只包含一个函数,我们可以写
from operations.addition import *。不过,从模块中仅导入我们使用的函数和类是一种良好的实践。 -
如我们所见,主文件非常简洁。它仅包含导入、使用导入方法声明的变量和打印结果。
最后的提示
我从前辈那里得到的建议是为每个模块创建一个类,因为这样更容易维护。然而,这并不是必须的:这取决于你编写的代码。
在上面的示例中,我创建了两个包含一个函数的单独模块。一种替代方案是创建一个名为 Operations 的类,并将这两个函数作为其方法来编写。
这将有助于我们在一个文件中创建“类似函数”,并在主文件中进行一次导入。
所以,不要总是把建议当作必须的:接受它们,但要根据你需要创建的东西来思考,同时考虑你需要的优化。
结论
现在,告诉我:你对这种方法有多兴奋?好吧,当我理清了对模块的理解后,我开始将它们应用到我的日常工作中,相信我,情况只会变得更好。
希望我已经为你澄清了这个话题;如果没有,请:在评论中告诉我。我可以创作其他文章作为补充。
-
订阅 我的 Substack 新闻通讯 以获取更多 Python 相关内容。
-
通过 我的推荐链接加入 Medium:以 5$/月(无额外费用)解锁 Medium 上的所有内容。
-
在这里找到/联系我: https://bio.link/federicotrotta
-
觉得有用吗? 请给我买杯 Ko-fi。
更多来自我的:
通过这本全面的类参考提升你的 Python 技能
利用 Python 中循环的强大功能
视频制作:
[1] 如果 name == “main” 对于 Python 开发者 (视频)
精通蒙特卡洛:如何通过模拟提升机器学习模型
通过模拟技术提升预测算法的概率方法应用
·发表于 Towards Data Science ·26 分钟阅读·2023 年 8 月 2 日
--

“在蒙特卡洛的轮盘赌桌上”由爱德华·蒙克(1892 年)
一位科学家玩扑克牌如何永远改变了统计学游戏
在 1945 年的动荡岁月中,当世界正经历第二次世界大战的最后阶段时,一场扑克牌游戏悄然引发了计算领域的一次突破。这不仅仅是普通的游戏,而是导致蒙特卡洛方法诞生的关键。玩家?正是科学家斯坦尼斯瓦夫·乌拉姆,他当时还深陷于曼哈顿计划中。乌拉姆在从疾病中恢复的过程中,沉迷于扑克牌游戏。游戏的复杂概率引起了他的兴趣,他意识到反复模拟游戏可以提供这些概率的良好近似。这是一个灵光一现的时刻,类似于牛顿的苹果,但换成了扑克牌。随后,乌拉姆与他的同事约翰·冯·诺依曼讨论了这些想法,两人共同形式化了蒙特卡洛方法,命名来源于著名的摩纳哥蒙特卡洛赌场(在上面的爱德华·蒙克的著名画作中描绘),在那里赌注高昂,运气主宰——这正如方法本身。
快进到今天,蒙特卡罗方法已成为机器学习领域中的一张王牌,包括在强化学习、贝叶斯滤波和复杂模型优化中的应用(4)。其稳健性和多功能性确保了其持续的相关性,距其诞生已超过七十年。从乌拉姆的纸牌游戏到今天复杂的 AI 应用,蒙特卡罗方法依然是应对复杂系统中模拟和逼近力量的见证。
用概率模拟来玩转你的牌
在数据科学和机器学习的复杂世界中,蒙特卡罗模拟类似于精心计算的赌注。这种统计技术使我们能够在不确定性面前进行战略性投注,为复杂的确定性问题提供概率性的解决方案。在这篇文章中,我们将揭示蒙特卡罗模拟的神秘面纱,并探讨其在统计学和机器学习中的强大应用。
我们将深入探讨蒙特卡罗模拟背后的理论,阐明使这一技术成为强大问题解决工具的原理。我们将通过一些 Python 中的实际应用,演示如何在实践中实现蒙特卡罗模拟。
接下来,我们将探讨如何利用蒙特卡罗模拟来优化机器学习模型。我们将专注于通常具有挑战性的超参数调优任务,提供一个实用的工具包,以帮助应对这一复杂的领域。
所以,下注吧,开始吧!
理解蒙特卡罗模拟
蒙特卡罗模拟对于数学家和数据科学家来说是一项宝贵的技术。这些模拟提供了一种在广泛而复杂的可能性中导航的方法,制定有根据的假设,并逐步完善选择,直到出现最合适的解决方案。
其工作原理如下:我们生成大量随机场景,遵循某个预定义的过程,然后审视这些场景,以估计各种结果的概率。这里有一个类比来帮助理解:将每个场景视为流行的哈斯布罗棋盘游戏“Clue”中的一次回合。对于不熟悉的人来说,“Clue”是一个侦探风格的游戏,玩家在一座豪宅中移动,收集证据以推断犯罪的细节——谁、什么和哪里。每回合或提问都会排除潜在的答案,使玩家更接近揭示真实的犯罪场景。同样,蒙特卡罗研究中的每次模拟提供的见解使我们更接近解决复杂问题的方案。
在机器学习的领域,这些“场景”可以代表不同的模型配置、各种超参数集、将数据集分割为训练集和测试集的不同方式以及其他许多应用。通过评估这些场景的结果,我们可以获得对机器学习算法行为的宝贵洞察,从而做出有关其优化的明智决策。
飞镖游戏
要理解蒙特卡洛模拟,想象你在玩飞镖游戏。但不是瞄准特定目标,而是蒙上眼睛随机地将飞镖扔向一个大的正方形飞镖盘。这个正方形里有一个圆形目标。你的目标是估算圆周率π,即圆的周长与直径的比值。
听起来不可能,对吧?但这里有个窍门:圆的面积与正方形的面积之比是π/4。因此,如果你投掷大量的飞镖,落在圆内的飞镖数与总飞镖数的比率应大致为π/4。将这个比率乘以 4,你就能估算出π!
随机猜测 vs. 蒙特卡洛
为了展示蒙特卡洛模拟的威力,让我们将它与一种更简单的方法进行比较,也许是所有方法中最简单的:随机猜测。
当你运行下面的代码进行随机和蒙特卡洛两种情况时,每次都会得到一组不同的预测。这是意料之中的,因为飞镖是随机投掷的。这是蒙特卡洛模拟的一个关键特征:它们本质上是随机的。然而,尽管有这种随机性,它们在正确使用时可以提供非常准确的估算。因此,虽然你的结果不会完全像我的,但它们会讲述相同的故事。
在第一组可视化图(图 1a 到 图 1f)中,我们对π的值进行了一系列随机猜测,每次生成一个基于猜测值的圆。让我们给这个“随机性”一个正确的推动,假设虽然我们不能记住π的确切值,但我们知道它高于 2 且低于 4。从结果图中可以看到,圆的大小根据猜测值变化很大,这显示了这种方法的不准确性(这也不应让人感到惊讶)。每个图中的绿色圆圈代表单位圆,即我们试图估算的“真实”圆。蓝色圆圈则基于我们的随机猜测。
#Random Guessing of Pi
# Before running this code, make sure you have the necessary packages installed.
# You can install them using "pip install" on your terminal or "conda install" if using a conda environment
# Import necessary libraries
import random
import plotly.graph_objects as go
import numpy as np
# Number of guesses to make. Adjust this for more guesses and subsequent plots
num_guesses = 6
# Generate the coordinates for the unit circle
# We use np.linspace to generate evenly spaced numbers over the range from 0 to 2*pi.
# These represent the angles in the unit circle.
theta = np.linspace(0, 2*np.pi, 100)
# The x and y coordinates of the unit circle are then the cosine and sine of these angles, respectively.
unit_circle_x = np.cos(theta)
unit_circle_y = np.sin(theta)
# We'll make a number of guesses for the value of pi
for i in range(num_guesses):
# Make a random guess for pi between 2 and 4
pi_guess = random.uniform(2, 4)
# Generate the coordinates for the circle based on the guess
# The radius of the circle is the guessed value of pi divided by 4.
radius = pi_guess / 4
# The x and y coordinates of the circle are then the radius times the cosine and sine of the angles, respectively.
circle_x = radius * np.cos(theta)
circle_y = radius * np.sin(theta)
# Create a scatter plot of the circle
fig = go.Figure()
# Add the circle to the plot
# We use a Scatter trace with mode 'lines' to draw the circle.
fig.add_trace(go.Scatter(
x = circle_x,
y = circle_y,
mode='lines',
line=dict(
color='blue',
width=3
),
name='Estimated Circle'
))
# Add the unit circle to the plot
fig.add_trace(go.Scatter(
x = unit_circle_x,
y = unit_circle_y,
mode='lines',
line=dict(
color='green',
width=3
),
name='Unit Circle'
))
# Update the layout of the plot
# We set the title to include the guessed value of pi, and adjust the size and axis ranges to properly display the circles.
fig.update_layout(
title=f"Fig1{chr(97 + i)}: Randomly Guessing Pi: {pi_guess}",
width=600,
height=600,
xaxis=dict(
constrain="domain",
range=[-1, 1]
),
yaxis=dict(
scaleanchor="x",
scaleratio=1,
range=[-1, 1]
)
)
# Display the plots
fig.show()






图 1a-1f:π的随机估算
你可能会注意到一个奇怪的现象:在随机猜测方法中,有时接近真实π值的猜测会导致一个更远离单位圆的圆。这种明显的矛盾产生是因为我们关注的是圆的周长,而不是半径或面积。两个圆之间的视觉差距表示的是基于猜测的圆周长估算误差,而不是整个圆的误差。
在第二组可视化(图 2a至图 2f)中,我们使用蒙特卡罗方法来估算π值。我们不是进行随机猜测,而是向一个正方形上扔大量的飞镖,并计算落在正方形内切圆内的数量。由此得到的π值估算更为准确,如图中所示,圆的大小更接近实际单位圆。绿色点表示落在单位圆内的飞镖,红色点表示落在单位圆外的飞镖。
#Monte Carlo Estimation of Pi
# Import our required libraries
import random
import math
import plotly.graph_objects as go
import plotly.io as pio
import numpy as np
# We'll simulate throwing darts at a dartboard to estimate pi. Let's throw 10,000 darts.
num_darts = 10000
# To keep track of darts that land in the circle.
darts_in_circle = 0
# We'll store the coordinates of darts that fall inside and outside the circle.
x_coords_in, y_coords_in, x_coords_out, y_coords_out = [], [], [], []
# Let's generate 6 figures throughout the simulation. Therefore, we will create a new figure every 1,666 darts (10,000 divided by 6).
num_figures = 6
darts_per_figure = num_darts // num_figures
# Create a unit circle to compare our estimates against. Here, we use polar coordinates and convert to Cartesian.
theta = np.linspace(0, 2*np.pi, 100)
unit_circle_x = np.cos(theta)
unit_circle_y = np.sin(theta)
# We start throwing the darts (simulating random points inside a 1x1 square and checking if they fall inside a quarter circle).
for i in range(num_darts):
# Generate random x, y coordinates between -1 and 1.
x, y = random.uniform(-1, 1), random.uniform(-1, 1)
# If a dart (point) is closer to the origin (0,0) than the distance of 1, it's inside the circle.
if math.sqrt(x**2 + y**2) <= 1:
darts_in_circle += 1
x_coords_in.append(x)
y_coords_in.append(y)
else:
x_coords_out.append(x)
y_coords_out.append(y)
# After every 1,666 darts, let's see how our estimate looks compared to the real unit circle.
if (i + 1) % darts_per_figure == 0:
# We estimate pi by seeing the proportion of darts that landed in the circle (out of the total number of darts).
pi_estimate = 4 * darts_in_circle / (i + 1)
# Now we create a circle from our estimate to compare visually with the unit circle.
estimated_circle_radius = pi_estimate / 4
estimated_circle_x = estimated_circle_radius * np.cos(theta)
estimated_circle_y = estimated_circle_radius * np.sin(theta)
# Plot the results using Plotly.
fig = go.Figure()
# Add the darts that landed inside and outside the circle to the plot.
fig.add_trace(go.Scattergl(x=x_coords_in, y=y_coords_in, mode='markers', name='Darts Inside Circle', marker=dict(color='green', size=4, opacity=0.8)))
fig.add_trace(go.Scattergl(x=x_coords_out, y=y_coords_out, mode='markers', name='Darts Outside Circle', marker=dict(color='red', size=4, opacity=0.8)))
# Add the real unit circle and our estimated circle to the plot.
fig.add_trace(go.Scatter(x=unit_circle_x, y=unit_circle_y, mode='lines', name='Unit Circle', line=dict(color='green', width=3)))
fig.add_trace(go.Scatter(x=estimated_circle_x, y=estimated_circle_y, mode='lines', name='Estimated Circle', line=dict(color='blue', width=3)))
# Customize the plot layout.
fig.update_layout(title=f"Figure {chr(97 + (i + 1) // darts_per_figure - 1)}: Thrown Darts: {(i + 1)}, Estimated Pi: {pi_estimate}", width=600, height=600, xaxis=dict(constrain="domain", range=[-1, 1]), yaxis=dict(scaleanchor="x", scaleratio=1, range=[-1, 1]), legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01))
# Display the plot.
fig.show()
# Save the plot as a PNG image file.
pio.write_image(fig, f"fig2{chr(97 + (i + 1) // darts_per_figure - 1)}.png")






图 2a-2f:蒙特卡罗估算π值
在蒙特卡罗方法中,π的估算是基于落在圆内的“飞镖”与总投掷次数的比例。得到的π值估算用于生成一个圆。如果蒙特卡罗估算不准确,生成的圆将再次出现错误。这个估算圆与单位圆之间的差距宽度,提供了蒙特卡罗估算准确性的指示。
然而,由于蒙特卡罗方法在“飞镖”数量增加时会产生更准确的估算,因此随着更多“飞镖”的投掷,估算的圆应当会趋近于单位圆。因此,虽然两种方法在估算不准确时都显示出差距,但随着“飞镖”数量的增加,这种差距在蒙特卡罗方法中应当会更加一致地减少。
预测π值:概率的力量
蒙特卡罗模拟之所以如此强大,是因为它们能够利用随机性来解决确定性问题。通过生成大量随机场景并分析结果,我们可以估算不同结果的概率,即使对于那些难以通过解析方法解决的复杂问题也是如此。
在估计π的情况下,蒙特卡罗方法允许我们即使随机投掷飞镖,也能做出非常准确的估计。如前所述,投掷的飞镖越多,我们的估计就越准确。这展示了大数法则,这是概率论中的一个基本概念,指出从大量试验中获得的结果的平均值应该接近期望值,并且随着试验次数的增加,会趋近于期望值。让我们通过绘制投掷的飞镖数量与蒙特卡罗估计的π与真实π之间的差异的关系图来检验我们的图 2a-2f中的六个例子是否趋于真实。一般来说,我们的图表(图 2g)应该呈现负趋势。这里是实现这一点的代码:
# Calculate the differences between the real pi and the estimated pi
diff_pi = [abs(estimate - math.pi) for estimate in pi_estimates]
# Create the figure for the number of darts vs difference in pi plot (Figure 2g)
fig2g = go.Figure(data=go.Scatter(x=num_darts_thrown, y=diff_pi, mode='lines'))
# Add title and labels to the plot
fig2g.update_layout(
title="Fig2g: Darts Thrown vs Difference in Estimated Pi",
xaxis_title="Number of Darts Thrown",
yaxis_title="Difference in Pi",
)
# Display the plot
fig2g.show()
# Save the plot as a png
pio.write_image(fig2g, "fig2g.png")

注意,即使只有 6 个例子,整体模式也如预期:投掷的飞镖更多(更多的场景),估计值与真实值之间的差异更小,从而预测更准确。
假设我们投掷 1,000,000 支飞镖,并进行 500 次预测。换句话说,我们将记录在 1,000,000 支飞镖模拟过程中,π的估计值与实际值之间的差异,在 500 个均匀间隔的位置上。与其生成 500 个额外的图形,不如直接跳到我们要确认的内容:是否真的如飞镖数量增加那样,π的预测值与真实π之间的差异变小。我们将使用散点图(图 2h):
#500 Monte Carlo Scenarios; 1,000,000 darts thrown
import random
import math
import plotly.graph_objects as go
import numpy as np
# Total number of darts to throw (1M)
num_darts = 1000000
darts_in_circle = 0
# Number of scenarios to record (500)
num_scenarios = 500
darts_per_scenario = num_darts // num_scenarios
# Lists to store the data for each scenario
darts_thrown_list = []
pi_diff_list = []
# We'll throw a number of darts
for i in range(num_darts):
# Generate random x, y coordinates between -1 and 1
x, y = random.uniform(-1, 1), random.uniform(-1, 1)
# Check if the dart is inside the circle
# A dart is inside the circle if the distance from the origin (0,0) is less than or equal to 1
if math.sqrt(x**2 + y**2) <= 1:
darts_in_circle += 1
# If it's time to record a scenario
if (i + 1) % darts_per_scenario == 0:
# Estimate pi with Monte Carlo method
# The estimate is 4 times the number of darts in the circle divided by the total number of darts
pi_estimate = 4 * darts_in_circle / (i + 1)
# Record the number of darts thrown and the difference between the estimated and actual values of pi
darts_thrown_list.append((i + 1) / 1000) # Dividing by 1000 to display in thousands
pi_diff_list.append(abs(pi_estimate - math.pi))
# Create a scatter plot of the data
fig = go.Figure(data=go.Scattergl(x=darts_thrown_list, y=pi_diff_list, mode='markers'))
# Update the layout of the plot
fig.update_layout(
title="Fig2h: Difference between Estimated and Actual Pi vs. Number of Darts Thrown (in thousands)",
xaxis_title="Number of Darts Thrown (in thousands)",
yaxis_title="Difference between Estimated and Actual Pi",
)
# Display the plot
fig.show()
# Save the plot as a png
pio.write_image(fig2h, "fig2h.png")

蒙特卡罗模拟与超参数调优:完美组合
你可能会想,“蒙特卡罗是一个有趣的统计工具,但它如何应用于机器学习呢?”简短的回答是:有很多种方式。蒙特卡罗模拟在机器学习中的一个应用就是超参数调优。
超参数是我们(人类)在设置机器学习算法时调整的旋钮和拨盘。它们控制算法行为的方面,这些方面关键的是从数据中无法学习到的。例如,在决策树中,树的最大深度是一个超参数。在神经网络中,学习率和隐藏层的数量是超参数。
选择合适的超参数可以决定一个模型表现差异与表现优异之间的差别。但是我们如何知道选择哪些超参数呢?这就是蒙特卡罗模拟的作用所在。
传统上,机器学习从业者使用网格搜索或随机搜索等方法来调整超参数。这些方法涉及为每个超参数指定一组可能的值,然后为每一个可能的超参数组合训练和评估模型。这可能会非常耗费计算资源和时间,尤其是当有许多超参数需要调整或每个超参数可以取的值范围很大时。
蒙特卡洛模拟提供了一个更高效的替代方案。我们可以根据某种概率分布从超参数空间中随机采样,而不是穷尽所有可能的超参数组合。这使我们能够更有效地探索超参数空间,并更快地找到好的超参数组合。
在下一部分,我们将使用一个真实数据集来演示如何在实践中使用蒙特卡洛模拟进行超参数调整。让我们开始吧!
蒙特卡洛模拟用于超参数调整
实验的核心:心脏病数据集
在机器学习的世界里,数据是驱动我们模型的命脉。为了探索蒙特卡洛模拟在超参数调整中的应用,让我们来看一个与心脏息息相关的数据集——字面上的意思。心脏病数据集(CC BY 4.0)来自 UCI 机器学习库,是一些患者的医学记录,其中有些人患有心脏病。
数据集包含 14 个属性,包括年龄、性别、胸痛类型、静息血压、胆固醇水平、空腹血糖等。目标变量是心脏病的存在,使这是一个二分类任务。由于包含了分类和数值特征,这是一个有趣的数据集,用于演示超参数调整。
首先,让我们查看一下我们的数据集,以了解我们将要处理的内容——总是一个好的开始。
#Load and view first few rows of dataset
# Import necessary libraries
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import roc_auc_score
import numpy as np
import plotly.graph_objects as go
# Load the dataset
# The dataset is available at the UCI Machine Learning Repository
# It's a dataset about heart disease and includes various patient measurements
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data"
# Define the column names for the dataframe
column_names = ["age", "sex", "cp", "trestbps", "chol", "fbs", "restecg", "thalach", "exang", "oldpeak", "slope", "ca", "thal", "target"]
# Load the dataset into a pandas dataframe
# We specify the column names and also tell pandas to treat '?' as NaN
df = pd.read_csv(url, names=column_names, na_values="?")
# Print the first few rows of the dataframe
# This gives us a quick overview of the data
print(df.head())
这展示了数据集中所有列的前四个值。如果你加载了正确的 csv 文件并且像我一样命名了你的列,你的输出将会类似于图 3。

图 3:数据集中前 4 行的数据显示
设定脉搏:数据预处理
在我们可以使用心脏病数据集进行超参数调整之前,我们需要对数据进行预处理。这涉及几个步骤:
-
处理缺失值:数据集中有些记录缺失了值。我们需要决定如何处理这些缺失值,可以通过删除记录、填补缺失值或其他方法。
-
编码分类变量:许多机器学习算法要求输入数据为数值型。我们需要将分类变量转换为数值格式。
-
规范化数值特征:机器学习算法在数值特征处于类似尺度时通常表现更好。我们将应用规范化来调整这些特征的尺度。
首先处理缺失值。在我们的心脏病数据集中,‘ca’和‘thal’列中有一些缺失值。我们将用各自列的中位数填补这些缺失值。这是一种处理缺失数据的常见策略,因为它不会严重影响数据的分布。
接下来,我们将对分类变量进行编码。在我们的数据集中,‘cp’,‘restecg’,‘slope’,‘ca’和‘thal’列是分类的。我们将使用标签编码将这些分类变量转换为数值型的。标签编码将列中的每个唯一类别分配给不同的整数。
最后,我们将规范化数值特征。规范化调整数值特征的尺度,使它们都落在类似的范围内。这可以帮助提高许多机器学习算法的性能。我们将使用标准化来进行规范化,将数据转化为均值为 0,标准差为 1。
这是执行所有这些预处理步骤的 Python 代码:
# Preprocess
# Import necessary libraries
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import LabelEncoder
# Identify missing values in the dataset
# This will print the number of missing values in each column
print(df.isnull().sum())
# Fill missing values with the median of the column
# The SimpleImputer class from sklearn provides basic strategies for imputing missing values
# We're using the 'median' strategy, which replaces missing values with the median of each column
imputer = SimpleImputer(strategy='median')
# Apply the imputer to the dataframe
# The result is a new dataframe where missing values have been filled in
df_filled = pd.DataFrame(imputer.fit_transform(df), columns=df.columns)
# Print the first few rows of the filled dataframe
# This gives us a quick check to make sure the imputation worked correctly
print(df_filled.head())
# Identify categorical variables in the dataset
# These are variables that contain non-numerical data
categorical_vars = df_filled.select_dtypes(include='object').columns
# Encode categorical variables
# The LabelEncoder class from sklearn converts each unique string into a unique integer
encoder = LabelEncoder()
for var in categorical_vars:
df_filled[var] = encoder.fit_transform(df_filled[var])
# Normalize numerical features
# The StandardScaler class from sklearn standardizes features by removing the mean and scaling to unit variance
scaler = StandardScaler()
# Apply the scaler to the dataframe
# The result is a new dataframe where numerical features have been normalized
df_normalized = pd.DataFrame(scaler.fit_transform(df_filled), columns=df_filled.columns)
# Print the first few rows of the normalized dataframe
# This gives us a quick check to make sure the normalization worked correctly
print(df_normalized.head())
第一个打印语句显示了原始数据集中每列的缺失值数量。在我们的情况下,‘ca’和‘thal’列中有一些缺失值。
第二个打印语句显示了填补缺失值后的数据集前几行。如前所述,我们使用了每列的中位数来填补缺失值。
第三个打印语句显示了对分类变量进行编码后数据集的前几行。经过这一步骤,我们的数据集中的所有变量都是数值型的。
最后的打印语句显示了在规范化数值特征后数据集的前几行,其中数据的均值为 0,标准差为 1。经过这一步骤,我们数据集中的所有数值特征都在类似的尺度上。检查您的输出是否类似于图 4:

图 4:预处理打印语句输出
运行这段代码后,我们得到了一个已经过预处理的数据集,准备好进行建模。
实现一个基本的机器学习模型
现在我们已经预处理了数据,准备实施一个基本的机器学习模型。这将作为我们的基准模型,之后我们将尝试通过超参数调优来改进它。
我们将使用一个简单的逻辑回归模型来完成这个任务。请注意,尽管它被称为“回归”,但它实际上是处理二分类问题(如我们在心脏病数据集中遇到的)的最受欢迎的算法之一。这是一个线性模型,用于预测正类的概率。
在训练我们的模型后,我们将使用两个常见指标来评估其性能:准确度和 ROC-AUC。准确度是所有预测中正确预测的比例,而 ROC-AUC(接收者操作特征曲线—曲线下面积)衡量真实正率和假正率之间的权衡。
那么这与蒙特卡洛模拟有什么关系呢?好吧,像逻辑回归这样的机器学习模型有几个可以调整的超参数,以提高性能。然而,找到最佳的超参数组合就像在大海捞针。这就是蒙特卡洛模拟的用武之地。通过随机采样不同的超参数组合并评估其性能,我们可以估算出良好超参数的概率分布,并对使用哪些最佳参数做出有根据的猜测,类似于我们在掷飞镖练习中挑选更好π值的方式。
这是实现并评估我们新预处理数据的基本逻辑回归模型的 Python 代码:
# Logistic Regression Model - Baseline
# Import necessary libraries
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, roc_auc_score
# Replace the 'target' column in the normalized DataFrame with the original 'target' column
# This is done because the 'target' column was also normalized, which is not what we want
df_normalized['target'] = df['target']
# Binarize the 'target' column
# This is done because the original 'target' column contains values from 0 to 4
# We want to simplify the problem to a binary classification problem: heart disease or no heart disease
df_normalized['target'] = df_normalized['target'].apply(lambda x: 1 if x > 0 else 0)
# Split the data into training and test sets
# The 'target' column is our label, so we drop it from our features (X)
# We use a test size of 20%, meaning 80% of the data will be used for training and 20% for testing
X = df_normalized.drop('target', axis=1)
y = df_normalized['target']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Implement a basic logistic regression model
# Logistic Regression is a simple yet powerful linear model for binary classification problems
model = LogisticRegression()
model.fit(X_train, y_train)
# Make predictions on the test set
# The model has been trained, so we can now use it to make predictions on unseen data
y_pred = model.predict(X_test)
# Evaluate the model
# We use accuracy (the proportion of correct predictions) and ROC-AUC (a measure of how well the model distinguishes between classes) as our metrics
accuracy = accuracy_score(y_test, y_pred)
roc_auc = roc_auc_score(y_test, y_pred)
# Print the performance metrics
# These give us an indication of how well our model is performing
print("Baseline Model " + f'Accuracy: {accuracy}')
print("Baseline Model " + f'ROC-AUC: {roc_auc}')

我们的基本逻辑回归模型具有 0.885 的准确度和 0.884 的 ROC-AUC 分数,为我们提供了一个坚实的基准,以便进一步改进。这些指标表明我们的模型在区分有心脏病和无心脏病的患者方面表现相当出色。让我们看看是否能进一步提高。
使用网格搜索进行超参数调整
在机器学习中,模型的性能通常可以通过调整其超参数来提高。超参数是从数据中未学习到的参数,而是在学习过程开始之前设置的。例如,在逻辑回归中,正则化强度‘C’和惩罚类型‘l1’或‘l2’都是超参数。
让我们使用网格搜索对逻辑回归模型进行超参数调整。我们将调整‘C’和‘penalty’超参数,并使用 ROC-AUC 作为我们的评分指标。让我们看看能否超越基准模型的表现。
现在,让我们开始这一部分的 Python 代码。
# Grid Search
# Import necessary libraries
from sklearn.model_selection import GridSearchCV
# Define the hyperparameters and their values
# 'C' is the inverse of regularization strength (smaller values specify stronger regularization)
# 'penalty' specifies the norm used in the penalization (l1 or l2)
hyperparameters = {'C': [0.001, 0.01, 0.1, 1, 10, 100, 1000],
'penalty': ['l1', 'l2']}
# Implement grid search
# GridSearchCV is a method used to tune our model's hyperparameters
# We pass our model, the hyperparameters to tune, and the number of folds for cross-validation
# We're using ROC-AUC as our scoring metric
grid_search = GridSearchCV(LogisticRegression(), hyperparameters, cv=5, scoring='roc_auc')
grid_search.fit(X_train, y_train)
# Get the best hyperparameters
# GridSearchCV has found the best hyperparameters for our model, so we print them out
best_params = grid_search.best_params_
print(f'Best hyperparameters: {best_params}')
# Evaluate the best model
# GridSearchCV also gives us the best model, so we can use it to make predictions and evaluate its performance
best_model = grid_search.best_estimator_
y_pred_best = best_model.predict(X_test)
accuracy_best = accuracy_score(y_test, y_pred_best)
roc_auc_best = roc_auc_score(y_test, y_pred_best)
# Print the performance metrics of the best model
# These give us an indication of how well our model is performing after hyperparameter tuning
print("Grid Search Method " + f'Accuracy of the best model: {accuracy_best}')
print("Grid Search Method " + f'ROC-AUC of the best model: {roc_auc_best}')

经过网格搜索发现最佳超参数为{‘C’: 0.1, ‘penalty’: ‘l2’},我们最佳模型的准确度为 0.852,ROC-AUC 分数为 0.853。有趣的是,这一表现稍低于我们的基准模型。这可能是因为基准模型的超参数已经非常适合这个数据集,或者可能是由于训练-测试划分中的随机性所致。不管怎样,这都提醒我们更复杂的模型和技术并不总是更好。
不过,你可能也注意到,我们的网格搜索只探索了相对较少的超参数组合。在实际操作中,超参数及其潜在值的数量可能会大得多,从而使网格搜索计算上昂贵甚至不可行。
这就是蒙特卡罗方法的作用。让我们看看这种更有指导性的方法是否能改善原始基线或基于网格搜索的模型的表现:
#Monte Carlo
# Import necessary libraries
from sklearn.metrics import accuracy_score, roc_auc_score
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
import numpy as np
# Split the data into training and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Define the range of hyperparameters
# 'C' is the inverse of regularization strength (smaller values specify stronger regularization)
# 'penalty' specifies the norm used in the penalization (l1 or l2)
C_range = np.logspace(-3, 3, 7)
penalty_options = ['l1', 'l2']
# Initialize variables to store the best score and hyperparameters
best_score = 0
best_hyperparams = None
# Perform the Monte Carlo simulation
# We're going to perform 1000 iterations. You can play with this number to see how the performance changes.
# Remember the Law of Large Numbers!
for _ in range(1000):
# Randomly select hyperparameters from the defined range
C = np.random.choice(C_range)
penalty = np.random.choice(penalty_options)
# Create and evaluate the model with these hyperparameters
# We're using 'liblinear' solver as it supports both L1 and L2 regularization
model = LogisticRegression(C=C, penalty=penalty, solver='liblinear')
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
# Calculate the accuracy and ROC-AUC
accuracy = accuracy_score(y_test, y_pred)
roc_auc = roc_auc_score(y_test, y_pred)
# If this model's ROC-AUC is the best so far, store its score and hyperparameters
if roc_auc > best_score:
best_score = roc_auc
best_hyperparams = {'C': C, 'penalty': penalty}
# Print the best score and hyperparameters
print("Monte Carlo Method " + f'Best ROC-AUC: {best_score}')
print("Monte Carlo Method " + f'Best hyperparameters: {best_hyperparams}')
# Train the model with the best hyperparameters
best_model = LogisticRegression(**best_hyperparams, solver='liblinear')
best_model.fit(X_train, y_train)
# Make predictions on the test set
y_pred = best_model.predict(X_test)
# Calculate and print the accuracy of the best model
accuracy = accuracy_score(y_test, y_pred)
print("Monte Carlo Method " + f'Accuracy of the best model: {accuracy}')

在蒙特卡罗方法中,我们发现最佳 ROC-AUC 分数为 0.9014,最佳超参数为{‘C’: 0.1, ‘penalty’: ‘l1’}。最佳模型的准确率为 0.9016。
两种技术的故事:网格搜索与蒙特卡罗
网格搜索通过评估超参数空间中所有可能组合在训练集上的性能,并根据预定义的指标(例如,准确率、ROC-AUC 等)选择产生最佳平均性能的组合,从而选择“最佳”超参数。这涉及将训练数据分成几个子集(或“折叠”,我们在代码中设置为 5),在一些折叠上训练模型,在其余折叠上评估模型,然后在所有折叠中平均性能。这被称为交叉验证,有助于通过确保模型在不同训练数据子集上的平均表现良好来减少过拟合。
相比之下,蒙特卡罗方法不会对不同训练数据子集进行任何平均。它随机选择超参数,在整个训练集上评估模型,然后选择在测试集上产生最佳性能的超参数。这种方法不使用任何交叉验证,因此不会对不同训练数据子集的性能进行平均。
在上述实验中,尽管网格搜索方法评估了蒙特卡罗方法选择的组合,但由于该组合在交叉验证过程中未能在不同训练数据子集上产生最佳平均性能,因此未被选为“最佳”超参数。然而,蒙特卡罗方法选择的组合恰好在本实验中使用的特定测试集上表现更好。这表明所选超参数在特定测试集的特征上表现良好,即使它们在不同训练数据子集上的平均表现不是最好。
这突显了网格搜索方法中通过对不同训练数据子集进行平均引入的偏差,与蒙特卡罗方法中通过在整个训练集上评估模型并根据单个测试集选择超参数引入的方差之间的权衡。
我鼓励你动手尝试 Python 代码,观察不同因素如何影响性能。你还可以比较两种方法在不同超参数空间下的计算时间,以了解它们的效率。这个练习旨在展示这些方法的动态,它们各有优缺点,并突出“最佳”方法可能取决于你的具体场景、计算资源和模型要求。因此,随意实验,祝学习愉快!
结论
起源于纸牌游戏的蒙特卡罗方法,无疑重新塑造了计算数学和数据科学的格局。它的力量在于其简洁性和多功能性,使我们能够相对轻松地处理复杂的高维问题。从通过掷飞镖估计π的值到在机器学习模型中调整超参数,蒙特卡罗模拟已经证明了其在数据科学武器库中的宝贵价值。
在这篇文章中,我们从蒙特卡罗方法的起源出发,了解了它的理论基础,并探讨了其在机器学习中的实际应用。我们看到了它如何用来优化机器学习模型,并通过使用真实数据集进行超参数调整的实践探索。我们还与其他方法进行了比较,展示了它的效率和有效性。
但蒙特卡罗的故事远未结束。随着我们不断推动机器学习和数据科学的边界,蒙特卡罗方法无疑将继续发挥重要作用。无论我们是在开发复杂的 AI 应用、理解复杂数据,还是只是玩一局纸牌游戏,蒙特卡罗方法都证明了模拟和近似在解决复杂问题中的强大力量。
当我们向前迈进时,让我们花一点时间欣赏这个方法的美妙——一个起源于简单纸牌游戏的方法,却能够驱动世界上最先进的计算。蒙特卡罗方法确实是一场高风险的机会与复杂性的游戏,到目前为止,它似乎总能获胜。因此,继续洗牌,继续打牌,并记住——在数据科学的游戏中,蒙特卡罗可能就是你手中的王牌。
离别感想
恭喜你成功到达终点!我们已经探索了概率的世界,挣扎于复杂的模型,并对蒙特卡罗模拟的强大能力有了新的认识。我们见证了它们如何将复杂的问题简化为可管理的组件,甚至优化了机器学习任务的超参数。
如果你像我一样喜欢深入探讨机器学习问题的复杂性,请关注我在 Medium 和 LinkedIn 上。让我们一起在人工智能的迷宫中,一次解决一个巧妙的问题。
直到我们的下一个统计冒险,继续探索,继续学习,继续模拟!在你数据科学和机器学习的旅程中,愿好运常伴你左右。
注意:除非另有说明,所有图片均由作者提供。
掌握 NLP:深度学习模型的深入 Python 编码
原文:
towardsdatascience.com/mastering-nlp-in-depth-python-coding-for-deep-learning-models-a15055e989bf
一份逐步指南,包含全面的代码解释,介绍了如何使用 Python 进行深度学习的文本分类。
·发布在数据科学前沿 ·21 分钟阅读·2023 年 10 月 13 日
--

这篇文章是在阅读了大量文档资源和观看了 YouTube 上的文本数据、分类、递归神经网络及其他热门话题的视频之后完成的。许多信息并不友好,有些部分比较晦涩,因此,我希望节省读者大量时间,并阐明在任何机器学习项目中使用文本数据的最重要概念。
这里展示的示例支持代码可以在以下网址找到:github.com/Eligijus112/NLP-python
本文覆盖的主题包括:
-
将文本转换为序列
-
将序列索引转换为嵌入向量
-
深入的 RNN 解释
-
分类的损失函数
-
使用 Pytorch 的完整 NLP 管道
NLP 代表 Natural Language Processing¹。这是一个关于如何在任务中使用硬件和软件的广泛话题,例如:
-
将一种语言翻译成另一种语言
-
文本分类
-
文本摘要
-
下一步的标记预测
-
命名实体识别
还有更多。在这篇文章中,我想覆盖最受欢迎的技术,并通过简单的示例和代码使读者熟悉这些概念。
在 NLP 中,许多任务从对文本进行分词²开始。
文本分词是将原始文本拆分成较小部分——标记的过程。这些标记可以是字符、子词、单词或这三者的混合。
考虑一下字符串:
“Python 中的 NLP 很有趣,文档也做得很好。让我们开始吧!”
我将使用词级标记,因为相同的逻辑也适用于更低级别的标记化。
首先,让我们定义并应用一个将标点符号与单词分开的函数:
# Regex
import re
def preprocess_text(x: str) -> str:
"""
Function that preprocess the text before tokenization
Args:
x (str): text to preprocess
Returns:
str: preprocessed text
"""
# Create whitespaces around punctuation
x = re.sub(r'([.,!?;:])', r' \1 ', x)
# Returns the text
return x
# Original text
text = "NLP in Python is fun and very well documented. Let's get started!"
# Applying the function
text_preprocessed = preprocess_text(text)
# Printing the results
print(text)
将上述函数应用于文本,我们得到:
“NLP in Python is fun and very well documented . Let’s get started !”
现在,让我们定义一个函数,该函数创建一个将单词映射到索引和索引映射到单词的字典。文本中出现的第一个单词将具有较小的索引号。索引的排序完全是任意的——唯一的规则是每个单词应有一个唯一的索引。
# Typehinting
from typing import Tuple
# Defining the function
def create_word_index(x: str) -> Tuple[dict, dict]:
"""
Function that scans a given text and creates two dictionaries:
- word2idx: dictionary mapping words to integers
- idx2word: dictionary mapping integers to words
Args:
x (str): text to scan
Returns:
Tuple[dict, dict]: word2idx and idx2word dictionaries
"""
# Spliting the text into words
words = x.split()
# Creating the word2idx dictionary
word2idx = {}
for word in words:
if word not in word2idx:
# The len(word2idx) will always ensure that the
# new index is 1 + the length of the dictionary so far
word2idx[word] = len(word2idx)
# Adding the <UNK> token to the dictionary; This token will be used
# on new texts that were not seen during training.
# It will have the last index.
word2idx['<UNK>'] = len(word2idx)
# Reversing the above dictionary and creating the idx2word dictionary
idx2word = {idx: word for word, idx in word2idx.items()}
# Returns the dictionaries
return word2idx, idx2word
# Applying the function
word2idx, idx2word = create_word_index(text_preprocessed)
word2idx 字典如下:
{
'NLP': 0,
'in': 1,
'Python': 2,
'is': 3,
'fun': 4,
'and': 5,
'very': 6,
'well': 7,
'documented': 8,
'.': 9,
"Let's": 10,
'get': 11,
'started': 12,
'!': 13,
'<UNK>': 14
}
idx2word 字典如下:
{
0: 'NLP',
1: 'in',
2: 'Python',
3: 'is',
4: 'fun',
5: 'and',
6: 'very',
7: 'well',
8: 'documented',
9: '.',
10: "Let's",
11: 'get',
12: 'started',
13: '!',
14: '<UNK>'
}
从上述字典中,我们可以看到我们的数据总共有15 个标记。在本文中,我将把单词的索引称为标记。
现在让我们定义两个函数——一个创建索引序列,另一个将索引序列翻译为单词。
# Defining a function that splits a text into tokens
def text2tokens(x: str, word2idx: dict) -> list:
"""
Function that tokenizes a text
Args:
x (str): text to tokenize
word2idx (dict): word2idx dictionary
Returns:
list: list of tokens
"""
# Spliting the text into words
words = x.split()
# Creating the list of tokens
tokens = []
for word in words:
# The bellow line searches for the word in the word2idx dictionary
# and returns the index of the word. If the word is not found,
# it returns the index of the <UNK> token
tokens.append(word2idx.get(word, word2idx['<UNK>']))
# Returns the list of tokens
return tokens
# Defining a function that converts the tokens to text
def tokens2text(x: list, idx2word: dict) -> str:
"""
Function that converts tokens to text
Args:
x (list): list of tokens
idx2word (dict): idx2word dictionary
Returns:
str: text
"""
# Creating the list of words
words = []
for idx in x:
# The bellow line searches for the index in the idx2word dictionary
# and returns the word. If the index is not found,
# it returns the <UNK> token
words.append(idx2word.get(idx, '<UNK>'))
# Returns the text
return ' '.join(words)
# Applying the text2tokens function to the text
tokens_seq = text2tokens(text_preprocessed, word2idx)
# Transforming the tokens back to text
text_seq = tokens2text(tokens_seq, idx2word)
标记序列:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
从标记索引解码得到的词序列:
"NLP in Python is fun and very well documented . Let's get started !"
让我们创建一个新的文本,这是索引器未见过的:
“正如我所说,Python 是一个非常好的 NLP 工具”
从预处理到标记序列的整个流程:
# Putting everything together with a new text
text = "As I said, Python is a very good tool for NLP"
# Preprocessing the text
text_preprocessed = preprocess_text(text)
# Applying the text2tokens function to the text
tokens_seq = text2tokens(text_preprocessed, word2idx)
# Transforming the tokens back to text
text_seq = tokens2text(tokens_seq, idx2word)
print(f"Original text:\n {text_preprocessed}")
print(f"Tokens:\n {tokens_seq}")
print(f"Text:\n {text_seq}")
结果:
Original text:
As I said , Python is a very good tool for NLP
Tokens:
[14, 14, 14, 14, 2, 3, 14, 6, 14, 14, 14, 0]
Text:
<UNK> <UNK> <UNK> <UNK> Python is <UNK> very <UNK> <UNK> <UNK> NLP
现在标记序列是这样,我们可能在实际应用中遇到它——不是所有单词都被索引器见过,且标记索引没有排序。
这结束了本文的第一部分。回顾一下,我们已经创建了一些关键函数来从文本中创建标记,我们将在接下来的步骤中使用它们。
既然我们已经为每个单词创建了一个索引,我们可以开始为机器学习模型创建特征。目前,我们只有索引器从文本中看到单词的次数。这既不能捕捉单词的大小,也不能捕捉顺序信息,我们无法以任何方式比较单词——索引仅用作查找值。
从索引到特征,我们将创建一个词向量,或者使用更流行的术语,词嵌入。
词嵌入是一个单词的数值向量表示³。它将文本链接到一个向量空间。例如,
# Bellow are examples of valid embeddings
'dog' = [-0.25, 0.35, 0.87]
'cat' = [0.98, 0.21, -0.78, 0.77]
'person' = [1.25, 3.75]
嵌入维度是固定的,由用户定义。在构建 NLP 系统时,每个标记必须与嵌入具有相同的维度。
创建词嵌入的最常见技术是定义向量的坐标数,并从均值为 0、方差为 1 的正态分布中随机模拟每个坐标。
# Defining a function that creates the embedding vector
# Defining a function that creates the embedding vector
def create_embedding_vector(
n_dim: int = 16,
mean: float = 0.0,
variance: float = 1.0,
precision: int = None
) -> np.array:
"""
Function that creates a random embedding vector
Args:
n_dim (int, optional): embedding dimension. Defaults to 16.
mean (float, optional): mean of the normal distribution. Defaults to 0.0.
variance (float, optional): variance of the normal distribution. Defaults to 1.0.
precision (int, optional): precision of each of the gotten coordinate. Defaults to None.
Returns:
np.array: embedding vector
"""
# Creating a random normal distribution
X = np.random.normal(mean, variance, (n_dim, ))
# Rounding the coordinates to the given precision
if precision:
X = np.round(X, precision)
# Returns the embedding vector
return X
默认情况下,上述函数将创建一个长度为 16 的向量。让我们将函数应用于字典中的每个单词。为了阅读方便,我们将嵌入维度限制为 6:
# To recap, the idx2word dictionary is:
{
0: 'NLP',
1: 'in',
2: 'Python',
3: 'is',
4: 'fun',
5: 'and',
6: 'very',
7: 'well',
8: 'documented',
9: '.',
10: "Let's",
11: 'get',
12: 'started',
13: '!',
14: '<UNK>'
}
# Initiating the embedding dictionary
idx2embeddings = {}
# Iterating over the idx2word dictionary
for idx in range(len(idx2word)):
# Creating the embedding vector
X = create_embedding_vector(n_dim=6, precision=3)
# Adding the embedding vector to the embedding dictionary
idx2embeddings[idx] = X
# Creating the word2embeddings dictionary
word2embeddings = {word: idx2embeddings[idx] for word, idx in word2idx.items()}
idx2embeddings 和 word2embeddings 字典如下所示:
# idx2embeddings
{
0: array([ 0.308, -1.003, -0.36 , 0.57 , -1.106, -0.997]),
1: array([-1.283, 0.709, 0.812, 0.201, 0.339, 1.264]),
2: array([ 1.095, -0.666, 1.32 , -0.668, -0.705, 0.311]),
3: array([ 0.417, 1.088, 1.242, 0.905, -0.061, 1.316]),
4: array([ 1.432, -0.072, 0.622, -0.077, 0.597, 0.722]),
5: array([-0.724, -0.496, -1.652, 1.118, -2.108, -0.996]),
6: array([ 0.733, 0.021, 0.972, 0.363, 0.074, -0.661]),
7: array([-0.284, 1.453, 2.522, 1.027, -1.484, 0.301]),
8: array([ 0.921, 0.19 , 0.068, 0.517, -0.767, 0.225]),
9: array([-0.317, 0.691, -1.281, 0.624, 2.004, 1.377]),
10: array([ 0.171, 0.607, -1.064, -0.064, -0.091, 0.748]),
11: array([-0.151, 1.137, 0.783, -0.689, -1.473, -0.753]),
12: array([1.699, 0.021, 1.422, 0.316, 0.317, 0.064]),
13: array([-0.804, 0.156, -0.298, 0.15 , -0.686, 0.752]),
14: array([ 0.538, 0.405, 0.501, -0.245, -1.946, 0.282])
}
# word2embeddings
{
'NLP': array([ 0.308, -1.003, -0.36 , 0.57 , -1.106, -0.997]),
'in': array([-1.283, 0.709, 0.812, 0.201, 0.339, 1.264]),
'Python': array([ 1.095, -0.666, 1.32 , -0.668, -0.705, 0.311]),
'is': array([ 0.417, 1.088, 1.242, 0.905, -0.061, 1.316]),
'fun': array([ 1.432, -0.072, 0.622, -0.077, 0.597, 0.722]),
'and': array([-0.724, -0.496, -1.652, 1.118, -2.108, -0.996]),
'very': array([ 0.733, 0.021, 0.972, 0.363, 0.074, -0.661]),
'well': array([-0.284, 1.453, 2.522, 1.027, -1.484, 0.301]),
'documented': array([ 0.921, 0.19 , 0.068, 0.517, -0.767, 0.225]),
'.': array([-0.317, 0.691, -1.281, 0.624, 2.004, 1.377]),
"Let's": array([ 0.171, 0.607, -1.064, -0.064, -0.091, 0.748]),
'get': array([-0.151, 1.137, 0.783, -0.689, -1.473, -0.753]),
'started': array([1.699, 0.021, 1.422, 0.316, 0.317, 0.064]),
'!': array([-0.804, 0.156, -0.298, 0.15 , -0.686, 0.752]),
'<UNK>': array([ 0.538, 0.405, 0.501, -0.245, -1.946, 0.282])
}
通常在文献和实践中,嵌入矩阵对象用于解决自然语言处理任务。嵌入矩阵不过是将 idx2embeddings 字典转换为数组。矩阵的第一行是 idx2embeddings 中的第一个条目,第二行是第二个条目,以此类推。让我们创建我们自己的嵌入矩阵:
# Creating the embedding matrix
embedding_matrix = np.array([idx2embeddings[idx] for idx in idx2embeddings])
# Printing the matrix
print(embedding_matrix)
# The resulting embedding matrix of shape 15 x 6
[[ 0.308 -1.003 -0.36 0.57 -1.106 -0.997]
[-1.283 0.709 0.812 0.201 0.339 1.264]
[ 1.095 -0.666 1.32 -0.668 -0.705 0.311]
[ 0.417 1.088 1.242 0.905 -0.061 1.316]
[ 1.432 -0.072 0.622 -0.077 0.597 0.722]
[-0.724 -0.496 -1.652 1.118 -2.108 -0.996]
[ 0.733 0.021 0.972 0.363 0.074 -0.661]
[-0.284 1.453 2.522 1.027 -1.484 0.301]
[ 0.921 0.19 0.068 0.517 -0.767 0.225]
[-0.317 0.691 -1.281 0.624 2.004 1.377]
[ 0.171 0.607 -1.064 -0.064 -0.091 0.748]
[-0.151 1.137 0.783 -0.689 -1.473 -0.753]
[ 1.699 0.021 1.422 0.316 0.317 0.064]
[-0.804 0.156 -0.298 0.15 -0.686 0.752]
[ 0.538 0.405 0.501 -0.245 -1.946 0.282]]
一般规则是,嵌入矩阵的行数等于文本中唯一标记的总数,列数等于嵌入维度。
现在我们为每个单词都创建了一个嵌入,我们实际上为每个单词创建了 6 个特征。就现在而言,它们的意义并不比词到索引的链接更大。
但我们应该把这些嵌入视为机器学习模型权重初始化的开始。在训练过程中,每个坐标都是可训练的,最终的嵌入矩阵会与我们初始化时的矩阵大相径庭。
现在我们为每个单词都有了一个嵌入向量,让我们停下来重新审视我们的分词序列:
# Text
"NLP in Python is fun and very well documented . Let's get started !"
# The token sequence
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
在人类语言中,我们在句子中想要表达的单词顺序非常重要。交换两个单词可能会改变整个句子的含义。因此,为机器学习模型创建能够捕捉序列本质的数据是非常重要的。
为此,我们将创建一个张量,格式如下⁴:
(序列长度,批量大小,嵌入维度大小)
通常很难想象这个 3 维张量是什么。要可视化它,可以参考这个立方体:

典型的序列张量(4 x 5 x 4);作者图表
上述张量是由以下句子创建的:
# 5 random texts
texts = [
"I love my dog",
"The sun is rising",
"Bacon lettuce tomatoe yum",
"I love my mother",
"The frogs are gray"
]
批量大小等于我们数据中文档或数据点或任何其他文本单位的数量。在上述示例中,我们有 5 篇文本,因此,批量大小等于 5。
为了简化起见,我创建了包含 4 个单词的句子。因此,最大序列长度等于 4:
"I" -> "love" -> "my" -> "dog"
"The" -> "sun" -> "is" -> "rising"
"Bacon" -> "lettuce" -> "tomatoe" -> "yum"
"I" -> "love" -> "my" -> "mother"
"The" -> "frogs" -> "are" -> "gray"
嵌入向量维度也等于 4(这只是巧合,嵌入大小可以远大于序列长度)。因此,句子中的每个标记都有 4 个与之关联的数字(一个嵌入向量)。在张量图中,每个标记的嵌入向量用不同的颜色标记。因此,如果嵌入向量看起来像这样:
{
"The": [0.01, 0.14, 0.71, 0.69],
"frogs": [-1.25, 0.69, -0.54, 0.19],
"are": [2.10, -1.13, -0.15, -0.13],
"gray": [-0.22, 0.55, 1.12, 0.25],
}
然后序列是:

“青蛙是灰色的”序列;作者图表
请注意,有些作者以不同的轴方向可视化序列建模的张量。我的观点是,当考虑嵌入矩阵的序列建模时,应该始终在脑海中有一个随手可得的立方体。
现在我们脑海中有了序列的图像,我们可以使用什么样的机器学习模型来输出序列末尾的结果?一种特别适合序列的模型是递归神经网络,简称RNN。
根据定义,递归神经网络是一种包含循环的神经网络,允许在网络内存储信息。
让我们绘制一个简单的前馈神经网络,并使用我们拥有的输入。在序列中的每个时间步,令牌被“拆分”成 4 个特征,从嵌入矩阵中提取并通过网络传递:

前馈神经网络;图表由作者提供
每个权重 w1、w2、w3、w4 和 w5 在网络中随机初始化,并用于计算通过网络的前馈传递的最终输出。中间神经元的激活函数选择RELU。输出神经元的激活函数是linear。
例如,对于单词“The”,权重初始化为0.01、1.14、-1.16、1.75和2.1,网络将如下所示:

“The”令牌的预测;图表由作者提供
为了使我们的网络成为递归网络,我们需要在每个新令牌通过网络前馈后保存一些信息。我们通过在最终输出信息之前保存输出信息来实现这一点。为此,我们向网络中添加了一个权重并扩展了网络:

展开的 RNN 图;图表由作者提供
上述图表中有 3 组权重:Wx、Wa和Wo。
Wx和Wo是与简单前馈神经网络相同的权重。
Wa权重用于乘以在每个令牌的输出层前馈的信号。然后,这个信号在下一个令牌级别的sum and activation神经元处被使用。
让我们初始化相同的Wx和Wo权重,并额外将Wa权重初始化为 1.12。然后,对于我们拥有的完整序列的计算如下:
在“frog”令牌级别(或序列中的第二步)的激活函数输入计算如下:
1.25 * 0.01 + 0.69 * 1.14 -0.54 * -1.16 + 0.19 * 1.75 + 0.544 * 1.12 = 2.34228

从第一步到第二步;图表由作者提供
最后两个步骤在下面的图表中呈现:

序列的末尾;图表由作者提供
RNN 的每一步产生了以下数值序列:
[1.14, 4.91, 2.73, 2.56]
默认情况下,在大多数软件包(如 Pytorch 或 Tensorflow)中,只返回最后一步的输出:2.56。如果参数return_sequences = True,则返回完整序列。
请记住,在网络的展开过程中,Wx、Wa 和 Wo 是固定的,并保持不变。这些权重在反向传播过程中会被更新。
上述管道的最后部分是将最终输出层连接到我们 RNN 网络的输出。根据我们要解决的任务,我们可以:
-
如果我们想要解决回归问题(例如,给定职位广告的描述,预测薪资),请添加一个线性层。
-
如果想要解决分类问题(例如,电影评论的情感是正面还是负面),请添加一个 sigmoid 层。
-
用于自定义任务的其他层。
我们将在网络的末尾添加一个 sigmoid 层,模型预测一个介于 0 和 1 之间的值。
Sigmoid 是一个函数,它输入一个数字并输出一个范围在 (0, 1) 之间的数字。公式为:

Sigmoid 公式
如果我们将 RNN 输出上的权重 Ws 定义为 0.85,那么序列在我们网络中的完整流动将如下所示:

完整的 RNN 管道;图表由作者提供
记住,RNN 的输出是一个等于 2.56 的数字(因此 sigmoid 层的输入是 2.56 * 0.85 = 2.18)。
在训练过程中,将会调整的权重包括:
-
Wx (4)
-
Wa (1)
-
Wo (1)
-
Ws (1)
-
嵌入矩阵中的所有数字(唯一标记 * 4)
在训练过程中,所有权重将会被调整,以根据用户定义的损失函数产生最佳的概率输出。
请注意,我在权重计算中省略了偏置变量。逻辑并没有改变,但额外的 1 个参数会被添加到 Wx、Wa、Wo 和 Ws。
在深入实际实现上述图表之前,我们需要定义最后一个要优化的东西——损失函数。我们将使用一种非常流行的二分类损失函数⁶,称为 二元交叉熵损失函数:

BCE 损失;公式由作者提供
上述方程看起来很吓人,但让我们逐一解析每个符号。
BCE(w) 部分意味着我们只能在网络中改变权重,这也是函数的唯一参数。
i 从 1 到 N 的总和 意味着当我们计算 BCE 时,我们使用了所有的数据。
y_i 和 x_i 是来自数据集的第 i 个数据点。这些数据是固定的,我们不能以任何方式调整它们。
f(w, x_i) 代表在给定一组权重和第 i 个 x 观测值的情况下,我们的函数(一个带有嵌入层和 sigmoid 输出的 RNN 网络)的输出。
为了更好地理解上述损失,假设我们有两个观测值 (y, x) 的形式:[1, 0.47],[0, 65.23]。假设我们有一组权重 w 和 f(w, 0.47) = 0.88,f(w, 65.23) = 0.12。
然后,为了计算 BCE 损失,我们只需代入这些值:
BCE(w) = -0.5 [(1 * log(0.88) + (1 - 1) log(1 - 0.88)) + (0 * log(0.12) + (1 - 0) log(1 - 0.12))]
BCE(w) = 0.056
现在假设我们有另一组权重 w,产生以下概率:
f(w, 0.47) = 0.95 和 f(w, 65.23) = 0.08
那么 BCE(w) 是:
BCE(w) = -0.5 [(1 * log(0.95) + (1 - 1) log(1 - 0.95)) + (0 * log(0.08) + (1 - 0) log(1 - 0.08))]
BCE(w) = 0.029
新的权重集更好,因为损失函数较小。
这里的直觉是,二进制交叉熵越小,模型在真实 Y 值为 1 时给出高概率的确定性越高,而在真实 Y 值为 0 时给出低概率的确定性越高。
现在让我们使用 Pytorch 将一切结合起来。我们将尝试分类推文的情感是正面还是负面。数据集来源于此⁷:www.kaggle.com/datasets/jp797498e/twitter-entity-sentiment-analysis
数据是关于计算机游戏的推文是正面还是负面。
我们将对 Y 变量进行编码,其中 1 表示负面情绪,0 表示正面情绪。
我们将数据集拆分为训练集和测试集(分别为 34410 和 8603 行),并应用之前定义的所有函数:
import pandas as pd
# Reading the data
d = pd.read_csv('input/Tweets.csv', header=None)
# Adding the columns
d.columns = ['INDEX', 'GAME', "SENTIMENT", 'TEXT']
# Leaving only the positive and the negative sentiments
d = d[d['SENTIMENT'].isin(['Positive', 'Negative'])]
# Encoding the sentiments that the negative will be 1 and the positive 0
d['SENTIMENT'] = d['SENTIMENT'].apply(lambda x: 0 if x == 'Positive' else 1)
# Dropping missing values
d = d.dropna()
# Spliting to train and test sets
train, test = train_test_split(d, test_size=0.2, random_state=42)
# Reseting the indexes
train.reset_index(drop=True, inplace=True)
test.reset_index(drop=True, inplace=True)
对 create_word_index() 函数进行了小的调整,我们将每个令牌索引移位 1,因为我们稍后将引入 padding 概念,这是 Pytorch 所必需的。
def create_word_index(x: str, shift_for_padding: bool = False) -> Tuple[dict, dict]:
"""
Function that scans a given text and creates two dictionaries:
- word2idx: dictionary mapping words to integers
- idx2word: dictionary mapping integers to words
Args:
x (str): text to scan
shift_for_padding (bool, optional): If True, the function will add 1 to all the indexes.
Returns:
Tuple[dict, dict]: word2idx and idx2word dictionaries
"""
# Spliting the text into words
words = x.split()
# Creating the word2idx dictionary
word2idx = {}
for word in words:
if word not in word2idx:
# The len(word2idx) will always ensure that the
# new index is 1 + the length of the dictionary so far
word2idx[word] = len(word2idx)
# Adding the <UNK> token to the dictionary; This token will be used
# on new texts that were not seen during training.
# It will have the last index.
word2idx['<UNK>'] = len(word2idx)
if shift_for_padding:
# Adding 1 to all the indexes;
# The 0 index will be reserved for padding
word2idx = {k: v + 1 for k, v in word2idx.items()}
# Reversing the above dictionary and creating the idx2word dictionary
idx2word = {idx: word for word, idx in word2idx.items()}
# Returns the dictionaries
return word2idx, idx2word
应用函数后,我们得到约 29k 个唯一令牌:
# Joining all the texts into one string
text = ' '.join(train['TEXT'].values)
# Creating the word2idx and idx2word dictionaries
word2idx, idx2word = create_word_index(text, shift_for_padding=True)
# Printing the size of the vocabulary
print(f'The size of the vocabulary is: {len(word2idx)}')
The size of the vocabulary is: 29568
让我们从数据集中创建令牌序列:
# For each row in the train and test set, we will create a list of integers
# that will represent the words in the text
train['text_int'] = train['TEXT'].apply(lambda x: [word2idx.get(word, word2idx['<UNK>']) for word in x.split()])
test['text_int'] = test['TEXT'].apply(lambda x: [word2idx.get(word, word2idx['<UNK>']) for word in x.split()])
# Calculating the length of sequences in the train set
train['seq_len'] = train['text_int'].apply(lambda x: len(x))
# Describing the length of the sequences
train['seq_len'].describe()
count 34410.000000
mean 21.825574
std 17.707891
min 0.000000
25% 9.000000
50% 17.000000
75% 31.000000
max 312.000000
关于航空公司推文的平均词数等于21.8个词。目前,我们的数据集如下:

数据片段;图片由作者提供
text_int 列的长度不相等。大多数机器学习框架期望 X 的维度是相同的。因此,我们将引入填充函数,该函数将序列右侧填充 0 直到所需的序列长度:
def pad_sequences(x: list, pad_length: int) -> list:
"""
Function that pads a given list of integers to a given length
Args:
x (list): list of integers to pad
pad_length (int): length to pad
Returns:
list: padded list of integers
"""
# Getting the length of the list
len_x = len(x)
# Checking if the length of the list is less than the pad_length
if len_x < pad_length:
# Padding the list with 0s
x = x + [0] * (pad_length - len_x)
else:
# Truncating the list to the desired length
x = x[:pad_length]
# Returning the padded list
return x
让我们应用这个函数,看看会发生什么:
# Padding the train and test sequences
train['text_int'] = train['text_int'].apply(lambda x: pad_sequences(x, 30))
test['text_int'] = test['text_int'].apply(lambda x: pad_sequences(x, 30))

填充后的 text_int 列;图片由作者提供
上述片段中的最后一行展示了填充的效果。
为了更清晰的了解,填充的小例子如下:
假设我们有两个序列:[1, 2, 5, 9, 3] 和 [1, 6, 12]。如果我们将 pad_length 设置为 4,那么这些序列将变成:
[1, 2, 5, 9, 3] -> [1, 2, 5, 9]
[1, 6, 12] -> [1, 6, 12, 0]
现在我们可以在 Pytorch 中定义我们的模型类和数据加载器类:
# Importing the needed packages
import torch
from torch import nn
# Defining the torch model for sentiment classification
class SentimentClassifier(torch.nn.Module):
"""
Class that defines the sentiment classifier model
"""
def __init__(self, vocab_size, embedding_dim):
super(SentimentClassifier, self).__init__()
self.embedding = nn.Embedding(vocab_size + 1, embedding_dim)
self.rnn = nn.RNN(input_size=embedding_dim, hidden_size=1, batch_first=True)
self.fc = nn.Linear(1, 1) # Output with a single neuron for binary classification
self.sigmoid = nn.Sigmoid() # Sigmoid activation
def forward(self, x):
x = self.embedding(x) # Embedding layer
output, _ = self.rnn(x) # RNN layer
# Leaving only the last step of the RNN sequence
x = output[:, -1, :]
# Fully connected layer with a single neuron
x = self.fc(x)
# Converting to probabilities
x = self.sigmoid(x)
# Flattening the output
x = x.squeeze()
return x
# Initiating the model
model = SentimentClassifier(vocab_size=len(word2idx), embedding_dim=16)
# Initiating the criterion and the optimizer
criterion = nn.BCELoss() # Binary cross entropy loss
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# Defining the data loader
from torch.utils.data import Dataset, DataLoader
class TextClassificationDataset(Dataset):
def __init__(self, data):
self.data = data
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
# The x is named as text_int and the y as airline_sentiment
x = self.data.iloc[idx]['text_int']
y = self.data.iloc[idx]['SENTIMENT']
# Converting the x and y to torch tensors
x = torch.tensor(x)
y = torch.tensor(y)
# Converting the y variable to float
y = y.float()
# Returning the x and y
return x, y
# Creating the train and test loaders
train_loader = DataLoader(TextClassificationDataset(train), batch_size=32, shuffle=True)
test_loader = DataLoader(TextClassificationDataset(test), batch_size=32, shuffle=True)
模型的训练和测试:
# Defining the number of epochs
epochs = 100
# Setting the model to train mode
model.train()
# Saving of the loss values
losses = []
# Iterating through epochs
for epoch in range(epochs):
# Initiating the total loss
total_loss = 0
for batch_idx, (inputs, labels) in enumerate(train_loader):
# Zero the gradients
optimizer.zero_grad() # Zero the gradients
outputs = model(inputs) # Forward pass
loss = criterion(outputs, labels) # Compute the loss
loss.backward() # Backpropagation
optimizer.step() # Update the model's parameters
# Adding the loss to the total loss
total_loss += loss.item()
# Calculating the average loss
avg_loss = total_loss / len(train_loader)
# Appending the loss to the list containing the losses
losses.append(avg_loss)
# Printing the loss every n epochs
if epoch % 20 == 0:
print(f'Epoch: {epoch}, Loss: {avg_loss}')
损失与时代图:
# Ploting the loss
import matplotlib.pyplot as plt
plt.plot(losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Loss vs. Epoch')
plt.show()

损失与时代;图片由作者提供
验证集中的准确率:
# Setting the model to eval model
model.eval()
# List to track the test acc
total_correct = 0
total_obs = 0
# Iterating over the test set
for batch_idx, (inputs, labels) in enumerate(test_loader):
outputs = model(inputs) # Forward pass
# Getting the number of correct predictions
correct = ((outputs > 0.5).float() == labels).float().sum()
# Getting the total number of predictions
total = labels.size(0)
# Updating the total correct and total observations
total_correct += correct
total_obs += total
print(f'The test accuracy is: {total_correct / total_obs}')
The test accuracy is: 0.7420667409896851
结果测试准确率为 ~75%,对于如此少的代码和特征工程来说真的不错。
总结一下,在本文中,我涵盖了以下概念:
-
文本到索引(分词)。
-
词嵌入。
-
对 RNN 层的深入解释。
-
二分类的损失函数。
-
如何在 Pytorch 中封装一切并创建一个模型。
祝学习愉快,编码愉快!
[1]
名称: 自然语言处理
网址: en.wikipedia.org/wiki/Neuro-linguistic_programming
[2]
名称: NLP | 如何进行文本、句子、词语的分词
网址: www.geeksforgeeks.org/nlp-how-tokenizing-text-sentence-words-works/
[3]
名称: 词嵌入
网址: en.wikipedia.org/wiki/Word_embedding
[4]
名称: 时间序列预测
网址: www.tensorflow.org/tutorials/structured_data/time_series
[5]
名称: 循环神经网络
网址: en.wikipedia.org/wiki/Recurrent_neural_network
[6]
名称: 分类的损失函数
网址: en.wikipedia.org/wiki/Loss_functions_for_classification
[7]
名称: Twitter 情感分析
网址: www.kaggle.com/datasets/jp797498e/twitter-entity-sentiment-analysis
数据集许可证: creativecommons.org/publicdomain/zero/1.0/
掌握机器学习中的 P 值
原文:
towardsdatascience.com/mastering-p-values-in-machine-learning-bdc5bd0dd8ae
理解 P 值和机器学习的应用场景
·发布于 Towards Data Science ·7 分钟阅读·2023 年 1 月 6 日
--

图片由 Karolina Grabowska 提供,来自 Pexels
P 值是一个统计指标,帮助统计学家决定是否接受或拒绝原假设。P 值衡量的是变量之间没有关系的概率。低 P 值提供了反对原假设的证据。P 值常常被误解。例如,这常使人错误地认为低 P 值意味着变量之间存在关系。声称存在关系与声称拒绝或接受没有关系是有些不同的。P 值仅提供支持或反对原假设的证据。具体来说,P 值 < 0.05 是好的。P 值 < 0.05 意味着变量之间没有关系的概率为 5%。此外,小的 P 值可以解释为观察到的效果很小的概率是由于偶然因素。
例如,如果一家制药公司在临床试验中测试一种新药,可能会观察到该药物对治疗特定疾病的症状有效。尽管有这些观察结果,但药物效果可能是由于偶然因素,而药物实际上可能无效。为了得出观察结果不是偶然的、代表真实效果的结论,可以使用 P 值来衡量观察结果由于随机偶然的概率。
p 值传统上用于许多统计技术,包括方差分析(ANOVA)、t 检验和回归。除了传统技术之外,虽然较少见,但在构建机器学习模型时也可以使用 p 值来检验假设。根据我的经验,作为数据科学家,p 值的必要性存在于传统统计方法与机器学习之间的边界。例如,线性回归和逻辑回归等回归技术通常需要计算回归系数的 p 值。线性回归和逻辑回归都被统计学家和数据科学家使用。另一个例子是特征选择。p 值可用于测试特征选择技术,包括 ANOVA(传统统计方法)和基于树的特征选择(数据科学/机器学习方法)。此外,p 值还可用于评估机器学习预测和特征重要性计算中的置信水平。具体来说,p 值可以用来确定两个模型之间的性能差异是否具有统计学意义。
另一个值得注意的区别是推断(传统统计学)与预测(机器学习)之间的差异。由于这种区别,人们通常认为在机器学习中,p 值是不必要的。在推断统计中,重点通常是关系背后的理论,而在机器学习中,重点则更多是准确预测未见数据。推断统计采用诸如 p 值、R2 和 F 统计量等指标来进行模型验证。机器学习模型则需要交叉验证和样本外测试来进行模型验证。
为了进一步理解这种区别,让我们考虑之前的药物示例。在统计推断的背景下,我们希望能够有统计证据来反对“药物 X 对治疗疾病 Y 无效”这一说法。在机器学习中,我们则更感兴趣的是使用预测来筛选潜在药物目录,并将其标记为有效或无效。前者更多的是验证关于关系的主张,而后者则是验证你能多好地预测未见数据。尽管存在这些区别,但数据科学家仍然有充分的理由考虑理解和使用 p 值,而统计学家也应考虑使用可能需要或不需要 p 值的非参数机器学习模型。
虽然 p 值在自然科学和社会科学中都有应用,但它们也有其局限性。最近,p 值的局限性成为数据科学家和统计学家讨论的热门话题。例如,p 值并不能衡量效应大小、重要性,甚至不能确定效应是否百分之百真实。它更多是用来支持声明,而不是将声明确立为真。尽管如此,数据科学家应该理解它们在传统统计中的地位,以及它们如何与机器学习结合使用。
在这里,我们将讲解如何计算线性和逻辑回归模型中系数的 p 值。我们将讨论如何计算机器学习预测的 p 值。我们可以使用 p 值将新模型与基准模型进行比较,并比较不同 ML 算法的性能。
对于这项工作,我将使用 Deepnote 编写代码,这是一个协作数据科学笔记本,使运行可重复的实验变得非常简单。
对于我们的回归模型,我们将使用 医疗费用数据集。我们将使用患者的年龄、体质指数和孩子数等属性来预测医疗费用。数据可以免费使用、修改和共享,遵循 数据库内容许可 (DbCL: 公共领域)。
对于我们的分类模型,我们将使用虚构的 Telco Churn 数据集,该数据集在 Kaggle 上公开提供。数据集可以免费使用、修改和共享,遵循 Apache 2.0 许可。
添加数据集
首先,让我们导航到 Deepnote 并创建一个新项目(如果你还没有账户,可以免费注册)。
让我们创建一个名为 ‘p_values’ 的项目,并在该项目中创建一个名为 ‘p_values_ml’ 的笔记本。同时,让我们将 insurance.csv 和 telco_churn.csv 文件拖放到页面左侧面板上标有 ‘FILES’ 的位置:

作者截图
线性回归系数的 p 值
我们将使用 p 值来测试以下内容:
备择假设 (H1): 特征年龄、BMI 和孩子数对预测成本很重要。
原假设 (H0): 特征年龄、BMI 和孩子数与成本没有关系。
我们将首先在 Python 中安装 stats 模块。这将使我们能够计算线性模型的 p 值,因为 Scikit-learn 不提供模型系数的 p 值。
作者创建的嵌入
接下来,让我们导入 Pandas 库,将保险数据读入 Pandas 数据框,并显示前五行:
作者创建的嵌入
接下来,让我们定义输入。我们将输入定义为年龄、BMI 和孩子数量,输出定义为费用。我们还将数据分为训练和测试集:
由作者创建的嵌入
接下来,让我们导入 stats 模型 API,并将我们的模型拟合到训练数据中。结果是一个总结我们模型的统计指标的表格。这里我们只对 P 值列感兴趣,但它还提供了其他指标,如 R 平方、t 检验和标准误差:
由作者创建的嵌入
我们看到年龄、BMI 和孩子数量的 P 值均小于 0.05。由此,我们有证据反对零假设,该假设表明输入和输出之间没有关系。
逻辑回归系数的 P 值
我们将使用 P 值测试以下内容:
替代假设 (H1): 特征 tenure、monthly charges 和 tenure_squared 对预测流失很重要。
零假设 (H0): 特征 tenure、monthly charges 和 tenure_squared 与流失没有关系。
我们可以对逻辑回归模型做类似的操作。让我们将流失数据读入数据框,生成编码的流失标签,并显示前五行:
由作者创建的嵌入
我们将输入定义为 tenure 和 monthly charges。我们还将计算每个特征的平方,并将其用作模型输入。输出将是流失列。我们还将数据分为训练和测试集:
由作者创建的嵌入
我们可以训练我们的逻辑回归模型并打印总结:
由作者创建的嵌入
P 值<0.05 提供了拒绝零假设的证据(输入与输出之间没有关系)。
比较随机森林回归模型的 P 值
替代假设 (H1): 模型性能存在实际差异。
零假设 (H0): 模型性能没有差异。
假设我们想比较两个随机森林回归模型。我们可以使用 P 值来提供拒绝零假设的证据。在这里,零假设是模型性能在两个模型之间没有差异。我们用默认的随机森林参数构建一个模型,再用n_estimators =5和max_depth =5构建第二个模型。然后,我们对测试集进行预测:
由作者创建的嵌入
接下来,我们需要安装mlxtend包。这将允许我们比较模型性能:
由作者创建的嵌入
接下来,让我们使用paired_ttest_5x2cv方法生成我们的 P 值。我们传递两个估计器及其参数值,以及训练数据:
由作者创建的嵌入
我们看到在这里没有证据可以拒绝零假设。这意味着模型性能的差异可能是偶然的。
比较不同机器学习算法的 P 值
让我们将随机森林模型(使用默认参数)与线性回归模型进行比较:
由作者创建的嵌入
我们看到这里有证据拒绝原假设,因为 p <0.05。这意味着模型性能差异不太可能是偶然的。
本文中的代码可在GitHub上找到。
结论
在本文中,我们讨论了如何将 p 值应用于机器学习用例。首先,我们研究了如何生成线性回归系数的 p 值。接着,我们讨论了如何生成逻辑回归的 p 值。随后,我们考虑了更现代的机器学习应用。我们展示了如何生成 p 值,以确定两个随机森林模型之间的性能差异是否显著。最后,我们使用 p 值来比较不同的机器学习算法类型。具体来说,我们计算了 p 值来比较随机森林和线性回归模型。
精通提示工程以释放ChatGPT的潜力
原文:
towardsdatascience.com/mastering-prompt-engineering-to-unleash-chatgpts-potential-9578a3fe799c
探索最佳实践并提升你的提示语,以获得更好的结果
·发表于 Towards Data Science ·阅读时长 11 分钟·2023 年 6 月 24 日
--

照片由 Léonard Cotte 在 Unsplash 提供
提示工程迅速成为我们生活的一部分,就像微风一样,这一切都要归功于ChatGPT和其他大型语言模型。尽管这不是一个全新的领域,但它目前正经历着巨大的流行。也就是说,我认为现在是提升技能、成为提示工程大师的绝佳时机。
如果你想知道提示工程到底是什么,不妨问问ChatGPT!

根据 Google Trends 数据,“提示工程”在过去几个月的搜索兴趣达到了顶峰。搜索“提示工程”的用户对后续话题如“课程”、“学习”和“薪水”感兴趣🤑。对“提示工程课程”的查询数量出现了激增。

过去 12 个月“提示工程”的 Google 趋势。值为 100 代表该术语的最高流行度。我在这里过滤了过去 12 个月的数据,但你可以自行调整。
无论您是在利用提示工程处理简单的日常问题、寻找食谱灵感、还是使用网页用户界面(例如 ChatGPT UI)规划下次旅行,或是开发访问大型语言模型 API(例如 ChatGPT API)的应用程序,提高您的提示编写技能都会显著提升您获得的响应质量。了解提示工程还提供了有关大型语言模型工作原理、能力和局限性的有用见解。
在这篇文章中,我将深入探讨我获得的见解,并提供一些示例来展示关键概念。让我们开始吧!💬 🤖
开始进行提示工程
提示工程在充分利用大型语言模型(LLMs)方面发挥着重要作用。当提示编写得很好时,响应会更加准确、相关和有用。

这里是编写提示时的一些常识指南:
-
🔖 清晰且具体: 清楚地传达您对模型的期望,并提供具体的指示。
-
🔖 包含必要的内容和期望的输出样式: 包括所有必要的信息,并指定您希望响应的格式或样式。
-
🔖 尽可能提供示例: 包括示例可以帮助模型理解期望的响应结构。
还有一些重要的注意事项:
-
📍响应可能包含错误
-
📍对相同提示的响应可能会有所不同
-
📍模型的知识基于训练数据收集时的时间(例如 ChatGPT 的知识截止日期是 2021 年 9 月)
五种超级强大的提示模式
我们现在将介绍与大型语言模型互动时可以遵循的五种非常重要的提示模式。
1. 人物模式
使用人物模式,您可以要求 ChatGPT 扮演特定的角色并执行特定的任务。



非常不错!(由 ChatGPT 提供的德语学习计划。)
您也可以反向工程这种模式,并要求 ChatGPT 假设您是 Persona X。例如:“为我制定一个德语学习计划,并假设我拥有语言学学位。”
2. 问题细化模式
使用问题细化模式,您可以要求 ChatGPT 生成后续问题,并利用这些问题的答案准确回答最初的问题。


使用问题细化模式进行提示编写
使用这种方法,我们可以洞察 ChatGPT 在构建答案时考虑的因素。我们还可以提供超出初始提示的额外背景。
3. 模板模式
我们可以提供一个模板,并指示 ChatGPT 按照模板生成响应。为了演示,我将提供几段来自维基百科大型语言模型页面的内容。使用这些段落,ChatGPT 将生成五个问题和答案,遵循模板。


使用 Markdown 可以格式化文本,例如word为word
4. 零-shot 提示
众所周知,大型语言模型(LLMs)可以按照指令执行,并能够从提示中学习模式,并在回答中遵循这些模式。这是一个零-shot 学习的示例任务,我们可以教会模型如何分类模型之前未遇到过的类别。以迪士尼乐园客户评论数据集为例,要求模型确定特定评论的情感。

正如你所看到的,响应是“积极的”。
5. 链式思维提示
与仅提供最终答案不同,尤其是对于数学问题,链式思维提示旨在提供逐步解释。
在以下示例中,我向 ChatGPT 提出了一个难题,它最初在回答中犯了一个错误。然而,当我要求 ChatGPT 提供完整的推理过程时,它能够提供正确的解决方案。
这种方法也是了解大型语言模型如何通过预测每个后续词生成文本的一个很好的例子。

正如你所看到的,答案是不正确的。应该是 9 颗糖而不是 8 颗。现在我们将通过改进提示并确保 ChatGPT 遵循一个可以进行更多评估的模板来解决这个问题。

当 ChatGPT 需要编写推理过程时,它能找到正确答案。
对于开放式响应,我们只能看到结果;而使用链式思维提示,我们可以了解过程。🤓
利用 ChatGPT API 构建数据应用
1. 总结文本并提取信息
总结信息可以是一种节省时间的技巧,特别是当你需要提取关键点或特定细节时。通过 ChatGPT API,我们可以利用其能力生成大量文本的摘要。
以迪士尼乐园评论数据集为例,该数据集包含 42,000 条评论,我们可以通过利用摘要来简化评估过程。尽管我将仅演示一个评论,但这种方法可以很容易地扩展到处理更多的文本。
要使用 ChatGPT API,您需要登录 OpenAI 账户并通过右上角的“查看 API 密钥”部分生成 API 密钥。创建 API 密钥后,请将其存放在安全的地方,并且不要展示。
# Install openai
pip install openai
import os
import openai
# Safely store your API key
OPENAI_API_KEY = "sk-XXXXXXXXXXXXXXXXXXXXXXXX"
openai.api_key = OPENAI_API_KEY
我们现在将生成一个辅助函数,该函数将接受我们的提示并返回该提示的完成结果。
# Helper function to return completion for a prompt
def get_completion(prompt, model="gpt-3.5-turbo"):
messages = [{"role": "user", "content": prompt}]
response = openai.ChatCompletion.create(
model=model,
messages=messages,
temperature=0, # degree of randomness of the response
)
return response.choices[0].message["content"]
# Input one review
review = """
Have been to Disney World, Disneyland Anaheim and Tokyo Disneyland
but I feel that Disneyland Hong Kong is really too small to be
called a Disneyland. It has way too few rides and attractions.
Souvenirs, food and even entrance tickets are slightly more
expensive than other Disneyland as well. Basically, this park is
good only for small children and people who has never been to Disney.
The food choices were acceptable, mostly fast food, and not too expensive.
Bottled water, however, was VERY expensive but they do have water
fountains around for you to refill your water bottles. The parade was
pretty good. It was crowded not a problem but what was the problem was
the people were just so rude, the pushing and shoving cutting in lines
for the rides, gift shops, food stands was just to much to take. forget
trying to see one of the shows its a free for all for seats, i don't see
how Disney can let this happen, it was by far the worst managed Disney
property.
"""
# Write the prompt and generate the response using the helper function
prompt = f"""
Summarize the review below in 30 words.
Review: ```{review}```py
"""
response = get_completion(prompt)
print(response)
香港迪士尼乐园太小,游乐设施少,食品和纪念品昂贵。适合小孩子和首次访问者。人多且有无礼的行为,管理不善。
总结很棒,大约 30 个字,如我们在提示中要求的。如果我们愿意,我们还可以专注于总结中的特定元素,例如价格和价值。
prompt = f"""
Summarize the review below in 10 words and
focus price and value.
Review: ```{review}'''
"""
response = get_completion(prompt)
print(response)
```py
> Expensive, small, and crowded Disneyland with few attractions.
By generating a targeted summary, we have transformed our initial review into a more meaningful and impactful statement. 🍄
Now, let’s take it a step further and extract relevant information in the JSON format for easier processing and integration with other systems. We will define a template for keys in the prompt.
prompt = f"""
从评论中识别以下项目:
- 情感(正面或负面)
- 被评论的是哪个迪士尼公园
- 评论者是否表达了失望?(是或否)
- 评论者是否表达了快乐?(是或否)
将您的回复格式化为 JSON 对象,包括
"情感"、"公园"、"愤怒" 和 "快乐" 作为键。
将愤怒和快乐的值格式化为布尔值。
保持回答简洁。
评论: ```py{review}```
"""
response = get_completion(prompt)
print(response)
> {
> "Sentiment": "negative",
> "Park": "Hong Kong Disneyland",
> "Anger": true,
> "Happiness": false
> }
It’s pretty amazing!
## 2\. Translate the text
As you can imagine, if we can create summaries, we can also translate the text into different languages. Building on the previous example, we will now request summaries to be generated in English, Italian, and German languages.
prompt = f"""
你的任务是总结以下评论。
用不超过 10 个字总结以下评论。
将总结打印为英语、意大利语和德语
评论: ```py{review}```
"""
response = get_completion(prompt)
print(response)
> Summary:
> English: Disneyland Hong Kong is small with few attractions and expensive.
> Italian: Disneyland Hong Kong è piccolo con poche attrazioni e costoso.
> German: Disneyland Hong Kong ist klein mit wenigen Attraktionen und teuer.
## 3\. Expanding text
Expanding involves utilizing a smaller text piece, and leveraging the capabilities of a large language model to generate a more extensive piece of text. For example, if we input a customer review, we can produce a detailed reply email and address the concerns raised. We can provide guidelines for the format and content.
prompt = f"""
你的任务是写一封回复客户的电子邮件。
给定的客户电子邮件以 ```py.
Generate a reply to thank the customer for their review.
Guidelines:
- If the sentiment is positive or neutral, thank the customer
for their feedback and refer to the specific details they mentioned.
- If the sentiment is negative, sincerely apologize for their poor
experience, take responsibility, and provide follow-up contact information.
- Express appreciation for their time and review. Refer to particular
points to build rapport.
- For negative experiences, apologize sincerely and accept responsibility. \
Offer customer service contact information.
- Aim for a concise yet thoughtful tone.
- Close professionally as an "AI customer agent".
Customer review: ```{review}```py
"""
response = get_completion(prompt)
print(response)
> 尊敬的客户,
>
> 感谢您抽出时间分享您在香港迪士尼乐园的体验。我们感谢您的反馈,并很遗憾您的访问没有达到您的期望。
>
> 对于有限的游乐设施和景点,以及较高的纪念品、食品和票价,我们深表歉意。我们理解这些因素可能会影响您在公园的整体体验。
>
> 我们很高兴听到您觉得食品选择可接受,游行也很愉快。我们也感谢您对人群和部分客人无礼行为的反馈。我们会认真对待这些问题,并致力于改善我们的公园管理,以确保所有客人都能有更愉快的体验。
>
> 如果您有任何进一步的疑虑或希望提供额外的反馈,请随时联系我们的客服团队 [insert contact information]。我们重视您的反馈,并希望将来有机会为您提供更好的体验。
>
> 再次感谢您的评论。
>
> 此致,
>
> AI 客服代理
干得好,AI 客服代理!✉️
# 接下来是什么?
随着大型语言模型能力的不断扩展以及我们在编写有效提示方面的熟练程度,确保 AI 模型生成的内容符合特定的审核指南和规则变得至关重要。
🌵在我接下来的文章中,我深入探讨了 OpenAI 的内容审核端点,它允许我们评估内容是否符合使用政策。我强烈推荐查看这篇文章,了解更多关于内容审核的知识!
[](/enhance-content-moderation-with-openais-moderation-api-bb0b865d883b?source=post_page-----9578a3fe799c--------------------------------) ## 使用 OpenAI 的审核 API 提升内容审核
### 无缝地将审核端点集成到你的管道中与 ChatGPT
towardsdatascience.com
# 结论
在这篇文章中,我们探讨了使用网页 UI 和 API 的提示工程最佳实践。我们讨论了几种有效的提示模式,包括角色模式、问题细化、模板、零-shot 以及思维链提示。此外,我们还深入了解了利用 ChatGPT API 生成我们的秘密密钥,并探讨了其在文本总结、翻译和扩展方面的能力。
我希望这个教程能激发你使用大型语言模型来提升创造力和生产力。我相信,它能帮助你生成想法、获得新见解、解决复杂问题——并在多个层面上提升你的日常工作。可能性是巨大的!🤖✨
🍓 如果你喜欢阅读这样的文章并希望支持我的写作,你可以考虑[成为 Medium 会员](https://idilismiguzel.medium.com/membership)!Medium 会员可以全面访问所有作者的文章,如果你使用[我的推荐链接](https://idilismiguzel.medium.com/membership),你将直接支持我的写作。
🍓 如果你已经是会员并有兴趣阅读我的文章,你可以[订阅以获得通知](https://medium.com/subscribe/@idilismiguzel)或[在 Medium 上关注我](https://idilismiguzel.medium.com/)。如果你有任何问题或建议,请告诉我。
## 我推荐在阅读完这篇文章后查看的额外资源:
在撰写这篇文章之前,我完成了两个专注于掌握提示工程的在线课程。第一个课程是由 DeepLearning.AI 提供的[ChatGPT 提示工程师课程](https://www.deeplearning.ai/short-courses/chatgpt-prompt-engineering-for-developers/),特别适合开发者及从事 API 工作的人员。第二个课程是由 Coursera 提供的[ChatGPT 提示工程课程](https://www.coursera.org/learn/prompt-engineering),对于在网页 UI 上编写提示非常有用。这篇文章中的大部分知识来自这两个宝贵的资源,如果你感兴趣,我强烈推荐你去看看!
参考文献
1. [提示工程的 Google 趋势](https://trends.google.com/trends/explore?q=prompt+engineering&hl=en-GB)
1. [迪士尼乐园客户评论数据集](https://www.kaggle.com/datasets/arushchillar/disneyland-reviews),许可证: [CC0: 公共领域](https://creativecommons.org/publicdomain/zero/1.0/)
1. [ChatGPT 提示工程师课程](https://www.deeplearning.ai/short-courses/chatgpt-prompt-engineering-for-developers/)由 DeepLearning.AI 提供
1. [ChatGPT 提示工程](https://www.coursera.org/learn/prompt-engineering)由 Coursera 提供
1. 标题照片由 [Léonard Cotte](https://unsplash.com/@ettocl?utm_source=medium&utm_medium=referral) 提供,拍摄于 [Unsplash](https://unsplash.com/?utm_source=medium&utm_medium=referral)
1. 其他所有图像均由作者提供
# 掌握 Python RegEx:深入探讨模式匹配
> 原文:[`towardsdatascience.com/mastering-python-regex-a-deep-dive-into-pattern-matching-2ae4fe6216fe?source=collection_archive---------2-----------------------#2023-07-24`](https://towardsdatascience.com/mastering-python-regex-a-deep-dive-into-pattern-matching-2ae4fe6216fe?source=collection_archive---------2-----------------------#2023-07-24)
## *Python 正则表达式揭秘:解读使用 Python 的 re 模块进行模式匹配的艺术*
[](https://nathanrosidi.medium.com/?source=post_page-----2ae4fe6216fe--------------------------------)[](https://towardsdatascience.com/?source=post_page-----2ae4fe6216fe--------------------------------) [Nathan Rosidi](https://nathanrosidi.medium.com/?source=post_page-----2ae4fe6216fe--------------------------------)
·
[关注](https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fsubscribe%2Fuser%2Fab636cbf3611&operation=register&redirect=https%3A%2F%2Ftowardsdatascience.com%2Fmastering-python-regex-a-deep-dive-into-pattern-matching-2ae4fe6216fe&user=Nathan+Rosidi&userId=ab636cbf3611&source=post_page-ab636cbf3611----2ae4fe6216fe---------------------post_header-----------) 发表在 [数据科学前沿](https://towardsdatascience.com/?source=post_page-----2ae4fe6216fe--------------------------------) ·16 min read·2023 年 7 月 24 日[](https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fvote%2Ftowards-data-science%2F2ae4fe6216fe&operation=register&redirect=https%3A%2F%2Ftowardsdatascience.com%2Fmastering-python-regex-a-deep-dive-into-pattern-matching-2ae4fe6216fe&user=Nathan+Rosidi&userId=ab636cbf3611&source=-----2ae4fe6216fe---------------------clap_footer-----------)
--
[](https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fbookmark%2Fp%2F2ae4fe6216fe&operation=register&redirect=https%3A%2F%2Ftowardsdatascience.com%2Fmastering-python-regex-a-deep-dive-into-pattern-matching-2ae4fe6216fe&source=-----2ae4fe6216fe---------------------bookmark_footer-----------)
图片由作者在 [Canva](https://www.canva.com/) 上创建
# 什么是 Python RegEx 或正则表达式?
正则表达式,通常缩写为 regex,是处理文本的强大工具。它们本质上是一系列字符,用于建立搜索模式。这个模式可以用于各种字符串操作,包括匹配模式、替换文本和分割字符串。
# 正则表达式的历史

*图像来源:* [*http://serge.mehl.free.fr/chrono/Kleene.html*](http://serge.mehl.free.fr/chrono/Kleene.html)
数学家斯蒂芬·科尔·克利尼在 1950 年代首次引入了正则表达式,作为描述正规集合或正规语言的记号。
今天,正则表达式已成为程序员、数据科学家和 IT 专业人员的必备技能。
# Python 正则表达式或正则表达式的意义及应用
在深入了解这些正则表达式如何使用 Python 之前,让我们看看它的不同应用范围,以激励自己。
+ **数据验证**:正则表达式在验证不同类型的数据时非常有用。(电子邮件地址、电话号码)
+ **网页抓取**:在通过网页抓取数据时,正则表达式可以用来解析 HTML 并提取必要的信息。
+ **搜索和替换**:正则表达式擅长识别符合特定模式的字符串并用替代品替换它们。这一能力在文本编辑器、数据库和编程中尤其有价值。
+ **语法高亮**:许多文本编辑器使用正则表达式来进行语法高亮。
+ **自然语言处理 (NLP)**:在 NLP 中,正则表达式可以用于标记化、词干提取以及其他各种文本处理功能。
+ **日志分析**:在处理日志文件时,正则表达式在提取特定日志条目或分析一段时间内的模式方面非常有效。
现在我希望你已经有足够的动力了!
让我们开始使用 re 模块,它专注于正则表达式。
# Python 的 re 模块入门
很好,让我们从 Python 的 re 模块基础知识开始。在接下来的部分,我们将涵盖更多高级主题。
# re 模块简介
Python 通过 re 模块提供了对正则表达式的原生支持。
这个模块是 Python 的标准库,这意味着你不需要外部安装,它会随每个 Python 安装包一起提供。
re 模块包含了用于处理正则表达式的各种函数和类。一些函数用于匹配文本,一些用于拆分文本,还有一些用于替换文本。
它包括了广泛的函数和类,专门用于处理正则表达式。其中某些函数用于文本匹配,其余的用于文本拆分或文本替换。
# 导入 re 模块
正如我们已经提到的,它与安装一起提供,因此无需担心安装问题。
这就是为什么,为了开始在 Python 中使用正则表达式,你需要首先导入 re 库。你可以使用如下的 import 语句来完成这一步。
```py
import re
在导入库之后,你可以开始使用 re 模块提供的功能,如函数和类。
让我们从一个简单的例子开始。
假设你想在一个字符串中找到所有“Python”一词的出现。
我们可以使用来自 re 模块的 findall() 函数。
下面是代码。
import re
# Sample text
text = "Python is an amazing programming language. Python is widely used in various fields."
# Find all occurrences of 'Python'
matches = re.findall("Python", text)
# Output the matches
print(matches)
下面是输出。

在 re 模块中还有许多其他函数,我们可以用它们来构建更复杂的模式。
首先,让我们来看一下 re 模块中的常见函数。
re 模块中的常见函数
在向你介绍 Python RegEx 的基本知识之前,我们先来了解常见的函数,以便更好地掌握剩下的概念。re 模块包括许多不同的函数。通过使用它们,我们可以执行不同的操作。
在接下来的部分中,我们将发现其中的一些函数。

图片由作者在 Canva 创建
a. re.match() 函数
re.match() 用于捕捉正则表达式是否以特定字符串开始。
如果有匹配,函数返回一个匹配对象;如果没有匹配,则返回 None。
接下来,我们将使用 re.match() 函数。在这里,我们将检查字符串文本是否以“Python”这个词开始。然后我们将结果打印到控制台。
下面是代码。
import re
pattern = "Python"
text = "Python is amazing."
# Check if the text starts with 'Python'
match = re.match(pattern, text)
# Output the result
if match:
print("Match found:", match.group())
else:
print("No match found")
下面是输出。

输出显示模式“Python”匹配文本的开头。
b. re.search() 函数
与 re.match() 相对的是,re.search() 函数扫描整个字符串以查找匹配项,如果发现匹配项,则返回一个匹配对象。
在以下代码中,我们使用 re.search() 函数在字符串文本中查找“amazing”这个词。如果找到这个词,我们将其打印出来;否则,打印“未找到匹配项”。
下面是代码。
pattern = "amazing"
text = "Python is amazing."
# Search for the pattern in the text
match = re.search(pattern, text)
# Output the result
if match:
print("Match found:", match.group())
else:
print("No match found")
下面是输出。

输出显示我们的代码从给定文本中捕捉到了“amazing”。
c. re.findall() 函数
re.findall() 函数用于收集字符串中所有非重叠的模式匹配项。它将这些匹配项作为字符串列表返回。
在以下示例中,我们使用 re.findall() 函数来查找字符串中的所有“a”。匹配项作为列表返回,然后我们将其打印到控制台。
下面是代码。
pattern = "a"
text = "This is an example text."
# Find all occurrences of 'a' in the text
matches = re.findall(pattern, text)
# Output the matches
print(matches)
下面是输出。

输出展示了文本中所有非重叠的字母“a”出现的情况。
d. re.finditer() 函数
re.finditer() 函数类似于 re.findall(),但它返回一个迭代器,迭代器生成匹配对象。
在以下代码中,使用 re.finditer() 函数来查找字符串文本中所有字母“a”的出现情况。它返回一个匹配对象的迭代器,我们打印每个匹配的索引和值。
下面是代码。
pattern = "a"
text = "This is an example text."
# Find all occurrences of 'a' in the text
matches = re.finditer(pattern, text)
# Output the matches
for match in matches:
print(f"Match found at index {match.start()}: {match.group()}")
下面是输出。

输出显示了模式“a”在文本中的索引。
e. re.sub() 函数
re.sub() 函数用于将一个字符串替换为另一个字符串。
接下来,我们将使用 re.sub() 函数将“Python”替换为“Java”。
然后我们打印修改后的字符串。
下面是代码。
pattern = "Python"
replacement = "Java"
text = "I love Python. Python is amazing."
# Replace 'Python' with 'Java'
new_text = re.sub(pattern, replacement, text)
# Output the new text
print(new_text) # Output: "I love Java. Java is amazing."
这是输出结果。

输出显示我们可以成功将“Python”替换为“Java”。
在下一节中,我们将深入探讨可以在正则表达式中用于匹配各种文本模式的基本模式。
Python 正则表达式中的基本模式
让我们从基本模式开始。
正则表达式是通过字面字符、元字符和量词的组合构建的。因此,掌握这些基本组件对于创建有效的正则表达式至关重要。
让我们从字面字符开始。
a. 字面字符
字面字符是正则表达式中最简单的模式匹配形式。
它们本身完全匹配,没有特殊含义。
例如,正则表达式 python 将精确匹配字符串 python。
import re
pattern = "python"
text = "I love programming in python!"
# Find all occurrences of 'Python'
matches = re.findall(pattern, text)
# Output the matches
print(matches)
这是输出结果。

输出显示我们的 re.findall() 函数找到了所有“python”模式的实例。
b. 元字符
元字符如“.”、“^”、 “$”。这些字符在处理字符串时可能非常重要。让我们看看。
i. 点(.)
点号 . 就像一张万能牌。它可以代替任何单个字符,除了换行符。
在下面的代码中,我们将使用正则表达式模式“p.t”。
这是代码。
import re
pattern = "p.t"
text = "pat, pet, p5t, but not pt."
# Find all occurrences of 'Python'
matches = re.findall(pattern, text)
# Output the matches
print(matches)
这是输出结果。

输出显示我们的代码找到了所有以“p”开头并以“t”结尾的三个字符实例。
ii. 插入符号 (^)
插入符号 ^ 用于检查字符串是否以某个字符开头。
让我们看一个例子。
以下代码检查文本是否以 Hello 开头(匹配找到:“匹配”)或没有(未找到匹配)。
这是代码。
import re
pattern = "^Hello"
text = "Hello, world!"
# Use re.match() because it checks for a match only at the beginning of the string
match = re.match(pattern, text)
# Output the match
if match:
print("Match found:", match.group())
else:
print("No match found")
这是输出结果。

输出显示我们的代码捕捉到了文本开头的 hello 模式。
iii. 美元符号($)
美元符号 $ 用于检查字符串是否以某个字符结尾。
以下代码检查文本是否以 world$ 结尾(如果是,则打印“匹配找到:‘匹配’”),否则(打印“未找到匹配”)。
这是代码。
import re
pattern = "world$"
text = "Hello, world"
# Use re.search() to search the entire string
match = re.search(pattern, text)
# Output the match
if match:
print("Match found:", match.group()) # Output: Match found: world
else:
print("No match found")
这是输出结果。

输出显示 re.search() 函数找到了以“world”结尾的文本。
c. 量词
量词用于定义在你试图匹配的模式中,字符(或字符)出现的次数。
在本小节中,我们将查看关于星号(*)、加号(+)和问号(?)的示例,并以大括号({})结束。
让我们从星号开始。
i. 星号 (*)
正则表达式中的星号(*)表示前一个字符可以出现零次或多次。
让我们看看代码。在以下代码中,我们首先定义了模式(“py”),然后我们将使用 findall() 函数。
这是代码。
import re
pattern = "py*"
text = "p py pyy pyyy pyyyy"
matches = re.findall(pattern, text)
print(matches)
这是输出结果。

输出显示所有,因为星号允许“y”出现零次或多次。
ii. 加号(+)
加号 + 匹配前一个字符的 1 次或更多次重复。
在这里我们再次使用 findall() 函数配合 py 模式,但这次我们会使用加号(+)。
这是代码。
import re
pattern = "py+"
text = "p py pyy pyyy pyyyy"
matches = re.findall(pattern, text)
print(matches) # Output: ['py', 'pyy', 'pyyy', 'pyyy']
这是输出。

从输出中可以看到,加号要求“p”之后至少有一个或多个“y”字符。
iii. 问号(?)
问号 ? 匹配前一个字符的 0 次或 1 次重复。它使前一个字符成为可选的。
这是代码。
import re
pattern = "py?"
text = "p py pyy pyyy pyyyy"
matches = re.findall(pattern, text)
print(matches) # Output: ['p', 'py', 'p', 'p', 'p']
这是输出。

在输出中,你可以看到它只匹配了“p”和“py”,因为问号允许“y”出现一次或零次。
iv. 花括号({})
花括号 {} 允许你匹配特定次数的重复。
import re
pattern = "py{2,3}"
text = "py, pyy, pyyy, pyyyy"
matches = re.findall(pattern, text)
print(matches) # Output: ['pyy', 'pyyy', 'pyy']
这是输出。

在这个例子中,模式匹配了“pyy”和“pyyy”,但没有匹配“py”或“pyyyy”,因为我们指定了要匹配“p”之后正好有 2 或 3 个“y”字符。
Python 正则表达式中的特殊字符
特殊字符可以用来构建更复杂的模式。

图片由作者在 Canva 上创建
a. 字符类
让我们先看看字符类。
在接下来的例子中,我们将看到其中的 3 个。
让我们从 \d, \D 开始。
i. \d, \D
“\d”用于查找数字(从 0 到 9),而“\D”用于查找不是数字的元素。
在下面的代码中,“\d”扫描文本字符串并提取文本中的数字。
import re
pattern = "\d"
text = "My phone number is 123-456-7890."
# Find all digits in the text
matches = re.findall(pattern, text)
# Output the matches
print(matches)
这是输出。

输出显示我们找到了文本中的所有数字(0–9)。
ii. \s, \S
“\s”可以用来查找空白字符,而“\S”可以用来查找不是空白的字符。
在下方,正则表达式 “\s” 识别了给定文本中的所有空格和制表符。
这是代码。
import re
pattern = "\s"
text = "This is a text with spaces and\ttabs."
# Find all whitespace characters in the text
matches = re.findall(pattern, text)
# Output the matches
print(matches) # Output: [' ', ' ', ' ', ' ', ' ', ' ', '\t']
这是输出。

从输出中可以看出,我们可以识别所有的空白字符。
iii. \w, \W
“\w”可以用来查找单词(字母、数字和下划线字符),而“\W”则是其相反的。
在下面的代码中,“\w”从文本中提取所有字母和数字。
这是代码。
import re
pattern = "\w"
text = "This is an example with words and numbers 123!"
# Find all word characters in the text
matches = re.findall(pattern, text)
# Output the matches
print(matches)
这是输出。

b. 预定义字符类
预定义字符类提供了常见类的快捷方式。例如,“\d”是一个预定义字符类,表示数字。
在这种情况下,“\d”模式提取了给定文本中的所有数字。
import re
pattern = "\d"
text = "The year is 2023."
# Find all digits in the text
matches = re.findall(pattern, text)
# Output the matches
print(matches)
这是输出。

输出显示我们的代码在文本中找到了所有的预定义字符类“\d”(代表所有数字)实例。
c. 自定义字符类
自定义字符类允许你使用方括号 [] 定义自己的字符集。
在下面的示例中,自定义字符类 “[aeiou]” 用于查找文本中的所有元音字母。
这是代码。
import re
pattern = "[aeiou]"
text = "This is an example text."
# Find all vowels in the text
matches = re.findall(pattern, text)
# Output the matches
print(matches)
这是输出结果。

输出结果显示了文本中所有我们定义的元音字母的实例。
我们还可以使用“-”来定义字符范围。
这是代码。
pattern = "[A-Z]"
text = "This is an Example Text With Uppercase Letters."
# Find all uppercase letters in the text
matches = re.findall(pattern, text)
# Output the matches
print(matches)
这是输出结果。

这里我们可以看到输出由文本中的大写字母组成。
编译 Python 正则表达式
当你在脚本中多次使用相同的正则表达式时,首先将其编译成模式对象可以节省时间。这节省了很多时间,因为正则表达式不需要在每次使用时重新解析。
a. compile() 方法
可以使用 re.compile() 方法将正则表达式模式编译成模式对象。
一旦我们拥有这个模式对象,我们可以调用它的方法(匹配文本、搜索和其他操作。)
这是代码。
import re
# Compile the regular expression pattern
pattern = re.compile(r'\d+') # Matches one or more digits
# Use the pattern object to search for matches
text = "There are 3 apples and 4 oranges."
matches = pattern.findall(text)
# Output the matches
print(matches)
这是输出结果。

输出结果显示数字。
b. 编译正则表达式的好处
使用正则表达式的一些好处;
-
性能:它更快,特别是当正则表达式需要反复使用时。
-
可重用性:一旦编译,相同的模式对象可以在代码的不同部分多次重用。
-
可读性:使用模式对象可以使你的代码更简洁,特别是当你使用复杂的正则表达式时。
这是一个编译正则表达式的简单示例:
import re
# Compile the regular expression pattern
pattern = re.compile(r'\d+') # Matches one or more digits
# Use the pattern object to search for matches in different texts
text1 = "There are 3 apples."
text2 = "I have 15 dollars and 30 cents."
# Find matches in text1
matches1 = pattern.findall(text1)
# Find matches in text2
matches2 = pattern.findall(text2)
# Output the matches
print(matches1)
这是输出结果。

现在让我们检查第二个文本。
这是代码。
print(matches2)
这是输出结果。

上述示例相对简单,帮助你理解可重用性、性能和可读性的重要性,尤其是当我们的模式计划重复使用时。
实际示例:提取电话号码
在本节中,让我们通过编写一个 Python 脚本来提取文本中的电话号码,以测试我们所发现的内容。
这是正则表达式的一个常见用法,特别是在数据清理过程中。
a. 定义正则表达式模式
电话号码可以有不同的格式,尤其是在不同的国家,因此你可以根据自己的需要调整这些数字,对于这个示例,让我们考虑格式为 XXX-XXX-XXXX,其中 X 是数字。
以下代码定义了一个匹配上述格式的模式,并将其编译为正则表达式。
让我们看看代码。
import re
# Define the regular expression pattern for phone numbers
phone_number_pattern = re.compile(r'\d{3}-\d{3}-\d{4}')
b. 使用 findall() 方法
在这个示例中,我们将使用 findall() 方法来提取匹配我们模式的电话号码。
以下代码使用正则表达式模式来查找和提取所有
import re
# Define the regular expression pattern for phone numbers
phone_number_pattern = re.compile(r'\d{3}-\d{3}-\d{4}')
# Sample text with phone numbers
text = """
John Doe: 123-456-7890
Jane Doe: 234-567-8901
Office: 555-555-5555
"""
# Find all phone numbers in the text
phone_numbers = phone_number_pattern.findall(text)
c. 打印结果
最后,让我们把提取的电话号码打印到控制台。
这里是代码。
# Output the phone numbers
print("Phone numbers found:")
for phone_number in phone_numbers:
print(phone_number)
这是输出结果。

d. 完整示例代码
下面是结合上述所有步骤的完整 Python 脚本:
import re
# Define the regular expression pattern for phone numbers
phone_number_pattern = re.compile(r'\d{3}-\d{3}-\d{4}')
# Sample text with phone numbers
text = """
John Doe: 123-456-7890
Jane Doe: 234-567-8901
Office: 555-555-5555
"""
# Find all phone numbers in the text
phone_numbers = phone_number_pattern.findall(text)
# Output the phone numbers
print("Phone numbers found:")
for phone_number in phone_numbers:
print(phone_number)
这是输出结果。

最佳实践
在继续使用正则表达式时,请记住以下几个最佳实践:
-
保持简单:简洁是关键。通常建议使用更简单的模式,因为正则表达式可以瞬间变得复杂。
-
注释你的模式:在为你的项目开发正则表达式时,不要忘记在注释中包含说明,因为我们提到过它可能会很复杂,但一旦你这样做了,当你回头看时,你的代码将变得可重用。
-
彻底测试:反复测试你的代码,因为正则表达式由于其复杂的性质有时会产生意外的结果,这就是为什么要严格测试,以确保你的工作按预期运行。
-
使用原始字符串:在 Python 中处理文本时,有时你会使用具有不同含义的特殊字符(如反斜杠 \ 或 \n 表示换行)。为了避免这种混淆,Python 允许你使用所谓的“原始字符串”。你可以通过在字符串的第一个引号前加上字母“r”来使字符串变成“原始”。这样,Python 就会理解该字符串中的反斜杠应被视为普通字符,而不是特殊字符。
结论
在本指南中,我们探讨了 Python 正则表达式的领域。我们从常见函数和基本知识开始,深入了解了更高级的概念和实际示例。但请记住,做实际项目将作为你职业生涯中的一个例子,以加深你对这一领域的理解。通过这样做,你将获得知识,并避免在处理 Python 正则表达式时进行 Google 搜索。
查看这个 高级 Python 概念综合指南 来了解这些概念的概述。
我希望你通过阅读这篇文章也获得了关于 Python 正则表达式的有价值的信息。
感谢阅读!
最初发布于 https://www.stratascratch.com。
掌握来自 SAP 系统的数据集成与快速工程
·
跟进 发表在 Towards Data Science ·8 分钟阅读·2023 年 10 月 7 日
--
建筑工程师调查他的工作 — 稳定扩散
简介
在我们之前的出版物中,从数据工程到提示工程,我们展示了如何利用 ChatGPT 解决数据准备任务。除了我们收到的良好反馈外,还有一个关键问题被提出:提示工程可能对简单任务有所帮助,但在更具挑战性的环境中是否真的有用?这是一个合理的观点。在最近几十年,数据架构变得越来越多样化和复杂。因此,数据工程师越来越多地需要整合他们不一定熟悉的各种数据源。提示工程在这种情况下能否提供帮助?
本文基于来自人力资源管理的实际用例来探讨这个问题。我们应用少量示例学习将 SAP HCM 数据模型引入 ChatGPT,并使用 Apache Spark 分析收集到的信息。通过这种方式,我们展示了提示工程在先进的数据工程环境中如何提供价值。
关于业务案例
每个中型到大型公司都需要完成的一项常见任务是确定其员工数量及其组织分配。我们场景中的相关数据存储在 SAP HCM 系统中,这是一款领先的企业人力资源管理应用程序。
为了实现这种目标,每个数据工程师需要建立大量与业务相关的知识,这些知识与基础数据模型密切相关。
本文将提供一个逐步指南,通过创建 PySpark 代码来解决描述的业务问题,这些代码可用于构建数据模型,并因此作为任何报告解决方案的基础。

PowerBi 示例报告显示了人员数量
第一步:确定需要哪些信息
数据科学的主要挑战之一是根据业务用例选择必要的信息,并确定其在源系统中的来源。为了解决这个问题,我们需要向 ChatGPT 传递一些业务知识。为此,我们向 ChatGPT 介绍了 SAP HCM 基本表的一些信息,这些信息可以在 SAP 参考手册中找到:人力资源 | SAP 帮助门户,并结合每个表的 CSV 示例记录。
在这个初步场景中,我们的目的是报告某一特定时间点的所有在职员工。结果还应包括员工的个人编号、姓名、状态和组织分配。
为了收集所有必要的信息,我们需要向 ChatGPT 推断一个数据库模式,包括示例数据集和字段描述,使用少量示例提示。我们将从向 ChatGPT 传播数据库模式和一些示例数据开始。
每个了解 SAP HCM 数据模型的人都应该对 infotypes 和透明表的概念非常熟悉。infotype 包含所有事务信息,而透明表包含每个实体的业务信息(主数据)。
对于以下场景,我们将使用OpenAIs GPT-4来创建所需的代码。让我们首先向 ChatGPT 提供基本的表格信息。
Prompt:
Given the following Tables
1\. Table PA0000 - Employee Actions
Field;Key;Data Element;Domain;Data Type;Length;Decimal;Short Description;Check table
MANDT;;MANDT;MANDT;CLNT;3;0;Client;T000
PERNR;;PERSNO;PERSNO;NUMC;8 ;0;Personnel number;PA0003
SUBTY;;SUBTY;SUBTY;CHAR;4;0;Subtype;
OBJPS;;OBJPS;OBJPS;CHAR;2;0;Object Identification
...
containing the following sample Datasets:
100;99992228;;;;9999-12-31T00:00:00.0000000;2023-07-01T00:00:00.0000000;0;2023-01-10T00:00:00.0000000;TRP0001;;;;;;;;;;;;;;10;3;;0;0
100;99992229;;;;9999-12-31T00:00:00.0000000;2023-07-01T00:00:00.0000000;0;2023-03-29T00:00:00.0000000;TRP0001;;;;;;;;;;;;;;10;6;;0;0
2\. Table T529U - Status Values
Field;Key;Data Element;Domain;Data Type;Length;Decimal Places;Short Description;Check table
MANDT;;MANDT;MANDT;CLNT;3;0;Client;*
SPRSL;;SPRAS;SPRAS;LANG;1;0;Language Key;T002
STATN;;STATN;STATN;NUMC;1;0;Status number;
...
containing the following sample datasets:
MANDT,SPRSL,STATN,STATV,TEXT1
100,D,2,3,active
100,D,2,0,resigned
....
第 2 步:连接必要的基本表并仅筛选活跃员工
现在让我们创建连接基本表的代码:
Prompt:
Give me the pyspark code to restrict "Employee Actions" to contain
only employees indicated as active according to their corresponding
"Status Values" for English Language texts. Use "structuredzone.PA0000"
as the source for Table PA0000 and "structuredzone.T529U" as the source
for Table T529U. Use meaningful column aliases.
这将生成相当不错且格式良好的 PySpark 代码:
from pyspark.sql import SparkSession
from pyspark.sql.functions import col
# Initializing Spark Session
spark = SparkSession.builder.appName("EmployeeActionsFilter").getOrCreate()
# Reading the tables into DataFrames
df_pa0000 = spark.read.table("structuredzone.PA0000")
df_t529u = spark.read.table("structuredzone.T529U")
# Filtering T529U for the relevant status description and language
df_active_status = df_t529u.filter(
(col("TEXT1") == "Active") &
(col("SPRSL") == "E")
)
# Joining PA0000 with the filtered T529U based on the status field STAT2
df_active_employees = df_pa0000.join(
df_active_status,
(df_pa0000.STAT2 == df_active_status.STATV) &
(df_active_status.STATN == 2) # Assuming STATN=2 indicates employment status based on given data
).select(
df_pa0000["PERNR"].alias("EmployeeNumber"),
df_pa0000["MASSN"].alias("ActionType"),
df_pa0000["MASSG"].alias("ReasonForAction"),
df_active_status["TEXT1"].alias("StatusDescription")
)
# Show the result
df_active_employees.show()
# Optionally, you can write this DataFrame to a destination or further process it.
注意 GPT-4 识别了两个表的连接条件,基于表 PA0000 的列 STAT2 和表 T529U 的列 STATV,后者是对应的透明表。此外,创建的代码包含业务描述作为列别名,以提高其可读性。
第 3 步:构建一个时间轴以反映公司的员工历史
现在我们将创建一个包含从 2020–01–01 到 2024–01–01 期间的日期值的 DataFrame,并根据员工的入职日期(BEGDA)和可能的离职日期(ENDDA)连接所有有效员工,我们需要创建一个人工时间轴以将员工关联起来。
Prompt
In Pyspark: Create a dataframe containing one row for each date beginning
at 2020-01-01 ending at 2024-01-01\. The Date column should be formatted as
a date. Afterwards join all valid employees according to their start and
end date. The output should contain only active employees according to their
Employment status.
from pyspark.sql import SparkSession
from pyspark.sql.functions import explode, sequence, to_date, lit, col
# Start a Spark Session
spark = SparkSession.builder.appName("EmployeeTimeline").getOrCreate()
# Define start and end dates
start_date = to_date(lit("2020-01-01"))
end_date = to_date(lit("2024-01-01"))
# Create a DataFrame with one row for each date
date_df = (spark.range(1)
.select(explode(sequence(start_date, end_date)).alias("date")))
# Load the PA0000 and T529U tables
pa0000_df = spark.read.table("structuredzone.PA0000")
t529u_df = spark.read.table("structuredzone.T529U")
# Filter T529U for 'Active' employees with language key = E
active_t529u_df = t529u_df.filter((col("SPRSL") == "E") & (col("TEXT1") == "Active"))
# Join PA0000 with filtered T529U to get active employees
active_pa0000_df = pa0000_df.join(active_t529u_df, pa0000_df.STAT2 == active_t529u_df.STATV)
# Join with date range DataFrame to get timeline of active employees
active_employee_timeline_df = date_df.join(active_pa0000_df, (date_df.date >= active_pa0000_df.BEGDA) & (date_df.date <= active_pa0000_df.ENDDA))
# Show the DataFrame
active_employee_timeline_df.show()
第 4 步:通过确定最高级别的组织对象来拆解父子关系表
在最后一步,我们希望构建一个表示公司组织结构的 DataFrame,并确定每个对象的组织分配。具体来说,我们要确定每个子对象分配到哪个最高级别的组织单元(例如区域或部门)。
组织结构可以用于将员工时间轴连接起来,并在后续步骤中获取每个员工组织分配的详细信息。我们需要利用 SAP 的 HRP1001 表来实现这一点:
Prompt:
The following table is containing the organizational assignment
of each employee:
Table HRP1001 – Organization structure
Field;Data Element;Domain;Data Type;Length;Decimal Places;Short Description;Check table
MANDT;MANDT;MANDT;CLNT;3;0;Client;T000
OTYPE;OTYPE;OTYPE;CHAR;2;0;Object Type;T778O
OBJID;HROBJID;HROBJID;NUMC;8;0;Object ID;PLOGI
...
containing the following sample datasets:
MANDT,OTYPE,OBJID,PLVAR,RSIGN,RELAT,ISTAT,PRIOX,BEGDA,ENDDA,VARYF,SEQNR,INFTY,OTJID,SUBTY,AEDTM,UNAME,REASN,HISTO,ITXNR,SCLAS,SOBID,PROZT,ADATANR
100,P,00998833,A,209,1,,2018-01-01T00:00:00.0000000,9999-12-31T00:00:00.0000000,CP50035300,000,1001,P 00100769,A209,2020-12-11T00:00:00.0000000,TRP1000,,,00000000,CP,60095300,0.00,
Prompt:
Table HRP1001 contains parent-child information on the companies organization
structure. Column OBJID is containing the object ID and SOBID is containing
the related parent object ID. The column SCLAS is containing the information
weather the object ID is a position 'S', organizational unit 'O' or role 'K'.
In Pyspark: Give me the code that creates a dataframe containing the
object id of the highest available organizational unit of each object by
recursively dissolving the hierarchy. Use meaningful column aliases.
语言模型生成一个递归函数,搜索每个对象的最高级别组织单元(‘O’):
from pyspark.sql import SparkSession
from pyspark.sql.functions import col
# Initialize a Spark session
spark = SparkSession.builder.appName("HighestAvailableOrgUnit").getOrCreate()
HRP1001_df = spark.read.table("structuredzone.HRP1001")
# Recursive function to find the highest available organizational unit
def find_highest_org_unit(df, obj_id):
# Filter rows for the current object ID
object_df = df.filter(col("OBJID") == obj_id)
if object_df.count() == 0:
return None
# Get the parent object ID (SOBID) and SCLAS for the current object
parent_obj_id = object_df.select("SOBID").first()[0]
sclas = object_df.select("SCLAS").first()[0]
# Recursively find the highest available organizational unit
highest_org_unit = find_highest_org_unit(df, parent_obj_id)
# If the current object is an organizational unit ('O') or the highest_org_unit is None, update the result
if sclas == 'O' or highest_org_unit is None:
highest_org_unit = obj_id
return highest_org_unit
# Create a UDF (User-Defined Function) to apply the recursive function
from pyspark.sql.functions import udf
from pyspark.sql.types import StringType
find_highest_org_unit_udf = udf(find_highest_org_unit, StringType())
# Apply the UDF to find the highest available organizational unit for each object
highest_org_unit_df = df.withColumn("HighestAvailableOrgUnit", find_highest_org_unit_udf(df, col("OBJID")))
# Select meaningful columns and aliases
result_df = highest_org_unit_df.select(
col("OBJID").alias("ObjectID"),
col("HighestAvailableOrgUnit").alias("HighestAvailableOrgUnit")
)
# Show the resulting DataFrame
result_df.show()
# Stop the Spark session
spark.stop()
即使生成的代码没有很好地优化,它也包含一个递归函数来拆解层级。那些偏好使用公共表表达式(CTE)的用户应在输入提示中给出提示(使用公共表表达式),以创建更具可读性和可理解性的 PySpark 语句。
from pyspark.sql import SparkSession
# Create a Spark session
spark = SparkSession.builder.appName("OrganizationalUnitHierarchy").getOrCreate()
# Define the HRP1001 DataFrame (assuming you have it loaded already)
# Replace 'hrp1001_df' with your actual DataFrame
# Make sure you have the appropriate columns: 'OBJID', 'SOBID', and 'SCLAS'
# Register the HRP1001 DataFrame as a temporary SQL table
hrp1001_df.createOrReplaceTempView("hrp1001")
# Define the recursive query to dissolve the hierarchy and find the highest organizational unit
recursive_query = """
WITH RECURSIVE OrgHierarchy AS (
SELECT OBJID, SOBID, SCLAS
FROM hrp1001
WHERE SCLAS = 'O' -- Start with organizational units
UNION ALL
SELECT h.OBJID, p.SOBID, p.SCLAS
FROM OrgHierarchy h
JOIN hrp1001 p ON h.SOBID = p.OBJID
)
SELECT DISTINCT
h.OBJID AS EmployeeID,
h.SOBID AS HighestOrganizationalUnit
FROM OrgHierarchy h
"""
# Execute the recursive query and create the resulting DataFrame
result_df = spark.sql(recursive_query)
# Show the result
result_df.show()
# Stop the Spark session when done
spark.stop()
两个代码版本将创建一个用于扁平化层级组织结构的 DataFrame,该 DataFrame 可以通过简单地将其连接到之前生成的 DataFrame 来用于进一步的数据集成步骤。

包含层级信息的数据集
结论
我们已经展示了如何通过提取和集成来自 SAP 系统的数据来解决更高级的数据工程任务,使用 ChatGPT 生成 PySpark 代码。虽然大型语言模型可能尚不完美,但每个人或许已经可以想象这些技术对数据工程师的强大潜力。这里有几个关键要点:
-
ChatGPT 能够理解数据模型的基本原理。你可以通过利用提示技术来精细化它的理解,从而提供更深入的知识。
-
即使这种方法第一次尝试时不会产生完美的代码,我们仍然可以轻松调整生成的代码以适应我们个人的场景。
-
由于开放参考文档和 SAP 知识库的广泛可用性,该方法可以扩展为一种检索增强生成(RAG)解决方案。
在提示工程最佳实践方面,尽量做到尽可能精确,并提供 Spark 环境返回的错误代码,以利用 LLM 的能力来重构生成的代码。可能需要多次尝试来完善代码,不过在提示中添加像“精确”这样的关键词可能会帮助 ChatGPT 产生更好的结果。要求对解决方案方法进行详细解释,这将迫使 ChatGPT 的 Transformer 模型深入挖掘。
注意:由于本文长度限制,包含 csv 示例数据集的提示不得不被截断。
关于作者
马克斯·斯塔迪 是 Dehn SE 的高级云数据工程师,多年来从事数据工程、数据科学和数据分析领域的工作。
克里斯蒂安·科赫 是 BWI GmbH 的企业架构师,同时也是纽伦堡工业大学乔治·西蒙·欧姆的讲师。
精通 Python Asyncio 中的同步原语:全面指南
PYTHON CONCURRENCY
asyncio.Lock、asyncio.Semaphore、asyncio.Event 和 asyncio.Condition 的最佳实践
·发表于 Towards Data Science ·8 分钟阅读·2023 年 5 月 29 日
--

图片来源:由作者创建,Canva
这是我在 Python Concurrency 系列中的一篇文章,如果你觉得有用,可以从这里阅读其余部分。
介绍
在本文中,我将介绍为什么在 Python 的 asyncio 中需要同步原语,以及几种同步原语的最佳实践。在文章的最后部分,我将带你通过一个实际的同步原语示例进行讲解。
为什么你需要 asyncio 中的同步原语
任何使用过 Python 多线程的人都知道多个线程共享相同的内存块。因此,当多个线程同时对相同区域执行非原子操作时,会发生线程安全问题。
由于 asyncio 运行在单线程上,是否没有类似的线程安全问题?答案是否定的。
asyncio 中的并发任务是异步执行的,这意味着可能会有多个任务在时间上交替执行。当一个任务访问特定的内存区域并等待 IO 操作返回时,另一个任务也同时访问该内存区域,就会触发并发错误。
为了避免此类错误,Python asyncio 引入了一种类似于多线程的同步原语功能。
此外,为了避免过多任务同时访问资源,asyncio 的同步原语通过限制同时访问资源的任务数量来提供保护资源的能力。
接下来,让我们看看 asyncio 中有哪些同步原语可供使用。
Python asyncio 的同步原语
锁
在介绍这个 API 之前,让我们看看一种情况:
假设我们有一个并发任务需要一份网站数据。它会首先检查缓存中是否有数据;如果有,它会从缓存中获取数据;如果没有,它会从网站中读取数据。
由于读取网站数据以返回并更新缓存需要一些时间,当多个并发任务同时执行时,它们都假设这些数据不在缓存中,并同时发起远程请求,如下代码所示:

两个任务都认为缓存中没有数据,从而访问远程网站。作者提供的图片
这不符合我们最初的设计意图,因此asyncio.Lock派上了用场。
当并发任务需要首先获取锁时,我们可以检查缓存中是否有数据,其他未获取锁的任务将等待。直到获取锁的任务完成更新缓存并释放锁,其他任务才能继续执行。
整个流程图如下所示:

作者提供的图片
让我们看看如何编写代码:

只有第一个任务需要更新缓存。作者提供的图片
问题解决了,是不是很简单?
信号量
有时,我们需要访问一个有限并发请求的资源。例如,一个特定的数据库只允许同时打开五个连接。或者根据你拥有的订阅类型,某个 Web API 仅支持一定数量的并发请求。
在这种情况下,你需要使用asyncio.Semaphore。asyncio.Semaphore使用一个内部计数器,每次获取 Semaphore 锁时计数器减少 1,直到计数器变为零。

信号量将限制并发任务的数量。作者提供的图片
当asyncio.Semaphore的计数器为零时,其他需要锁的任务将会等待。在执行其他任务后调用释放方法时,计数器会增加 1,等待的任务可以继续执行。
代码示例如下:
这样,我们可以限制同时访问的连接数量。
有界信号量
有时,由于代码的限制,我们不能使用async with来管理获取和释放信号量锁,因此我们可能会在某处调用acquire,在另一处调用release。
如果我们不小心多次调用asyncio.Semaphore的release方法,会发生什么?
如代码所示,我们限制同时运行两个任务,但由于我们调用了多次释放,我们可以在下次同时运行三个任务。
为了解决这个问题,我们可以使用asyncio.BoundedSemaphore。
从源代码中我们可以知道,当调用release时,如果计数器的值大于初始化时设置的值,将抛出ValueError。

当我们多次调用释放方法时,会抛出 ValueError 异常。图片来源于作者
因此,问题正在得到解决。
事件
Event维护一个内部布尔变量作为标志。asyncio.Event有三个常用方法:wait、set和clear。
当任务运行到event.wait()时,任务处于等待状态。此时,可以调用event.set()将内部标记设置为 True,所有等待的任务可以继续执行。
当任务完成时,需要调用event.clear()方法将标记的值重置为 False,以将事件恢复到初始状态,并且下次可以继续使用事件。
我将展示如何使用Event在文章末尾实现一个事件总线,而不是示例代码。
条件
asyncio.Condition类似于asyncio.Lock和asyncio.Event的结合体。
首先,我们将使用async with确保获取条件锁,然后调用condition.wait()释放条件锁,使任务暂时等待。当condition.wait()返回时,我们重新获取条件锁,以确保只有一个任务同时执行。
当一个任务通过condition.wait()暂时释放锁并进入等待状态时,另一个任务可以通过async with获取条件锁,并通过condition.notify_all()方法通知所有等待的任务继续执行。
流程图如下:

asyncio.Condition 的工作流程。图片来源于作者
我们可以通过一段代码演示asyncio.Condition的效果:
有时,我们需要asyncio.Condition等待特定事件发生后才能继续执行下一步。我们可以调用condition.wait_for()方法,并将一个方法作为参数传递。
每次调用condition.notify_all时,condition.wait_for会检查参数方法的执行结果,如果结果为 True,则结束等待;如果结果为 False,则继续等待。
我们可以通过一个示例来演示wait_for的效果。在以下代码中,我们将模拟一个数据库连接。在执行 SQL 语句之前,代码会检查数据库连接是否已经初始化,如果连接初始化完成,则执行查询;否则,等待直到连接初始化完成:
使用同步原语的一些提示
记得在需要时使用超时或取消。
在使用同步原语时,我们通常是在等待特定 IO 操作的完成。然而,由于网络波动或其他未知原因,任务的 IO 操作可能比其他任务花费更长时间。
在这种情况下,我们应该为操作设置一个超时,以便在执行时间过长时,能够释放锁并允许其他任务及时执行。
在另一个情况下,我们可能会循环执行一个任务。它可能会使一些任务在后台等待,阻止程序正确结束。这时,记得使用 cancel 来终止任务的循环执行。
避免使用同步原语或只锁定最少的资源
我们都知道 asyncio 的优势在于任务可以在等待 IO 返回时切换到另一个任务执行。
但一个 asyncio 任务通常同时包含 IO 绑定操作和 CPU 绑定操作。如果我们在任务上锁定了太多代码,它将无法及时切换到另一个任务,从而影响性能。
因此,如果没有必要,尽量不要使用同步原语或仅锁定最少的资源。
为了避免一些其他竞争锁定的情况
asyncio 中没有 RLock,因此在递归代码中不要使用锁。
与多线程一样,asyncio 也有死锁的可能性,因此尽量避免同时使用多个锁。
实践中的高级技术:基于 asyncio 的 Event Bus
在文章前面的介绍之后,我相信你对如何正确使用 asyncio 的同步原语有了清晰的理解。
接下来,我将通过带你实现一个事件总线,教你如何在实际项目中使用同步原语。
像往常一样,作为架构师的第一步是设计 EventBus API。
由于 EventBus 使用字符串进行通信,并且在内部,我打算使用 asyncio.Event 来实现与每个字符串对应的事件,我们将从实现一个 _get_event 方法开始:
on 方法将回调函数绑定到特定事件上:
trigger 方法可以手动触发事件并传入相应的数据:
最后,让我们编写一个 main 方法来测试 EventBus 的效果:
在主方法结束时,记得使用超时来防止程序一直执行,就像我之前警告的那样。

代码按预期执行。图片来自作者
如你所见,代码按预期执行了。是不是很简单?
结论
本文首先介绍了为什么 Python asyncio 需要同步原语。
接着,我介绍了 Lock、Semaphore、Event 和 Condition 的最佳实践,并给出了一些正确使用它们的提示。
最后,我完成了一个关于 asyncio 同步原语的动手训练的小项目,希望能帮助你更好地在实际项目中使用同步原语。
随时评论、分享或与我讨论关于 asyncio 的话题。
你可以通过我的文章列表获取更多关于 Python 并发的知识:

Python 并发
查看列表 10 个故事!用 Aiomultiprocess 超级充实你的 Python Asyncio:全面指南
[## 通过我的推荐链接加入 Medium - Peng Qian
阅读 Peng Qian(以及 Medium 上成千上万其他作者)的每一个故事。你的会员费直接支持 Peng…
qtalen.medium.com](https://qtalen.medium.com/membership?source=post_page-----ae1ae720d0de--------------------------------)
掌握机器学习工作流的艺术:变压器、估算器和管道的全面指南
编写无缝代码以获得最佳结果
·
关注 发布于 Towards Data Science ·14 min read·2023 年 6 月 9 日
--
“只要我现在能理解它,写成这样也没关系,而好的一点是,它确实有效!我成功地用我的模型奇迹般地得到了一个相当不错的结果,真是一个不错的收尾。”
不,我来告诉你,这还不够好。确实,当你开始一个机器学习项目时,许多新手和中级分析师都急于制作出中等水平的模型,却缺乏适当的工作流程。虽然有时候问题本身很简单,但如果不遵循适当的工作流程,常常会导致一些难以察觉的潜在问题,比如数据泄漏。
“只要有效,就足够好了。” 让我告诉你,这并不是。让我们快速模拟一个场景,你需要向高级分析师解释你的工作。这里有一些问题。如果今天有效,是否能保证明天也有效,并且容易重复?你能在包含 200 多个单元格的 Notebook 中解释你的模型工作流程的预处理步骤吗?如果你以这种方式进行交叉验证,是否会暴露测试数据集并使模型性能膨胀?这些问题很棘手,不是吗?
让我告诉你,实际上,你并不孤单,也并没有那么远。即使在参加了多个商业分析和机器学习课程后,我的任何一个讲师都没有分享我下面所要分享的工具和技巧。我会说,这些不是每个人在第一次接触 Scikit-Learn 时都关注的亮点课程。然而,它们会产生一致的结果,显著提升你的代码编写水平。想象一下,轻松处理数据,流畅地转换特征,并训练复杂的模型,同时保持代码的优雅和简洁。这就是我们在本综合指南结束时的目标,希望你能被下面的实践所说服。让我们开始吧。
目录
-
采用流水线的理由
-
估算器
-
变换器
-
流水线
-
自定义估算器
-
特征联合
-
真实世界数据集示例:银行营销与网格搜索交叉验证
采用流水线的理由
1. 精简的工作流程。 利用流水线可以实现数据预处理和建模过程中的多个步骤的无缝集成。它使你能够将各种变换器和估算器串联起来,确保从数据预处理到模型训练和评估的流程清晰、简洁且自动化。通过将你的预处理和建模步骤封装在流水线中,你的代码变得更加有组织、模块化,并且更易于理解。它改善了代码的外观和可维护性,因为每一步都被清楚地定义。将流水线中的每一步视为独立的,你可以在不担心一个预处理步骤如何影响其他步骤的情况下进行更改或添加步骤!

作者提供的图片
2. 防止数据泄露。 这是每个分析师都害怕的对手。数据泄露可能发生在测试数据集的信息无意中影响了预处理步骤或模型训练,从而导致过于乐观的性能估计。从某种程度上来说,你是在泄露关于将要测试的内容的信息,使你的学习模型提前看到将要测试的内容。显然,“他在试图吹嘘”。 通常的经验法则是只拟合训练数据集,然后同时转换训练和测试数据集。下面的代码展示了某些人错误的地方。 此外,你通常会有多个预处理步骤,这些步骤通常涉及变换器,例如 StandardScaler()、MinMaxScaler()、OneHotEncoder() 等。想象一下在整个工作流程中多次进行拟合和转换过程,难道这不会让人困惑和不便吗?
#Variation 1: Do not fit transform both training and testing dataset!
ss = StandardScaler()
X_train_scaled = ss.fit_transform(X_train)
X_test_scaled = ss.fit_transform(X_test)
#Variation 2: Remember to transform your training dataset!
X_train_scaled = ss.fit(X_train)
X_test_scaled = ss.transform(X_test)
3. 超参数调整和交叉验证。 使用如 GridSearchCV 之类的技术轻松调整管道中所有步骤的超参数。然而,这一步骤中的错误往往被忽视。让我们看一个简单的例子。
#Previously an oversight, correction contributed by @Scott Lyden
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.feature_selection import SequentialFeatureSelector
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.pipeline import Pipeline
X, y = load_breast_cancer(return_X_y=True, as_frame=True)
#Without Pipeline
select = SequentialFeatureSelector(RandomForestClassifier(n_estimators=100, n_jobs=-1),
n_features_to_select=8,
direction='forward').fit(X,y)
X_selected = select.transform(X)
logreg = LogisticRegression()
np.mean(cross_val_score(estimator=logreg,
X=X_selected,
y=y,
n_jobs=-1))
#With Pipeline
pipe = Pipeline([("select", SequentialFeatureSelector(RandomForestClassifier(n_estimators=100, n_jobs=-1),
n_features_to_select=8,
direction='forward',
n_jobs=-1)),
("log", LogisticRegression())])
np.mean(cross_val_score(estimator=pipe, X=X, y=y))
尝试运行这两个示例:虽然交叉验证分数相差不远,但没有使用 Pipeline 的部分泄露了信息,因为特征选择步骤是在整个数据集上进行的。当我们到达交叉验证步骤时,数据集被分为训练集和验证集,它们本质上来自同一来源(训练集在进行特征选择时已从验证集中学习到信息)。如果你发现这一部分难以理解,请尝试重新阅读这一段并自己编码以加深理解。
估算器
在我们深入了解 Pipeline 能做什么之前,让我们暂时离开,了解组成 Pipeline 的组件——估算器。我们将在下一节中讨论其他组件——变换器、预测器和模型。
许多人在使用 Scikit-learn 时常常对估算器这个术语感到困惑。人们倾向于将估算器与预测能力联系起来——也就是特别指 predict 方法。虽然这种说法有一定的真实性,但遗憾的是,最多只是部分真实。估算器基本上是 Scikit-learn 库的构建块。估算器是一种工具,它可以从你的训练集学习,创建一个可以对新数据进行预测或推断的模型。由于所有估算器都有 fit 方法来从训练集学习,它们继承自 BaseEstimator。
从BaseEstimator本身来看,没有predict方法,只有fit。一个估计器并不一定需要有predict方法,虽然有些有。一个具有predict方法的估计器试图基于学习到的模型对新的、未见过的数据进行预测。例如,像线性回归、随机森林分类器、梯度提升分类器等回归器和分类器都是具有predict方法的估计器。
更进一步,我们来看看LogisticRegression类的原始文档²。在下面的代码片段中,我们观察到该类继承了BaseEstimator以获得fit方法,并继承了LinearClassifierMixin以获得predict方法。

Scikit-learn GitHub(BSD-3)
变换器
变换器是一种具有transform方法的估计器。请注意,这里的“变换器”特指 Scikit-learn 上下文。它不应与近年来备受关注的神经网络架构中的变换器混淆。
简而言之,变换器的作用是以某种方式转换/处理预测变量(X),使其可以被机器学习算法使用。这可能是使用像StandardScaler和MinMaxScaler这样的显著工具对连续预测变量进行缩放,或者使用OneHotEncoder或OrdinalEncoder对分类预测变量进行编码。
更进一步,变换器具有 fit-transform 机制,其中它使用fit方法从训练数据中学习,然后使用transform方法将学习到的转换应用于训练数据和测试数据。这确保了整个过程中的转换一致应用。
再进一步,为了遵循 Scikit-learn API 实现规则,变换器通常从BaseEstimator继承其fit方法,从TransformerMixin继承其transform方法。我们来看看StandardScaler库的原始文档³。

ColumnTransformer⁵
有时,你可能需要根据需求仅对某些列应用特定的变换。例如,对没有特定层次结构的分类特征应用OneHotEncoder,对具有特定层次结构和排序的分类特征(即 T 恤尺寸,我们通常有 XS<S<M<L<XL 的排序)应用OrdinalEncoder。我们可以使用ColumnTransformer来实现这种分离。
from sklearn.compose import ColumnTransformer
ohe_categorical_features = ['a', 'b', 'c']
ohe_categorical_transformer = Pipeline(steps=[
('ohe', OneHotEncoder(handle_unknown='ignore', sparse_output=False, drop='first'))
])
orde_categorical_features = ['d', 'e', 'f']
orde_categorical_transformer = Pipeline(steps=[
('orde', OrdinalEncoder(dtype='float'))
])
col_trans = ColumnTransformer(
transformers=[
('ohe_categorical_features', ohe_categorical_transformer, ohe_categorical_features),
('orde_categorical_features', orde_categorical_transformer, orde_categorical_features),
],
remainder='passthrough',
n_jobs=-1,
)
正如你可能预料的那样,我们将把变量col_trans作为代码后续大整体管道的一部分放在上面。简单而优雅。
管道
Pipeline⁶类以顺序方式执行管道中的估算器,将一个步骤的输出作为下一个步骤的输入。这本质上实现了链式操作的概念。根据Scikit-learn 文档⁴的说明,以下是估算器有资格作为管道的一部分的标准。
要使估算器能够与
pipeline.Pipeline一起使用,除了最后一步之外,需要提供fit或fit_transform函数。为了能够在训练集之外的数据上评估管道,它还需要提供transform函数。管道中的最后一步没有特别要求,只需具有fit函数。
使用Pipeline,我们去除了在每个估算器和/或转换器上调用fit和transform方法的冗余步骤。直接从管道调用一次fit方法就足够了。其背后的工作原理是,首先在第一个估算器上调用fit,然后transform输入并传递给下一个估算器。实际上,管道的效果取决于最后一个估算器(它包含了管道中最后一个估算器的所有方法)。如果最后一个估算器是回归器,那么管道也可以作为回归器使用。如果最后一个估算器是转换器,管道也是如此。
以下是如何使用Pipeline类的示例。
imputer = KNNImputer(n_neighbors=5)
feature_select = SequentialFeatureSelector(RandomForestClassifier(n_estimators=100), n_features_to_select=8, direction='forward')
log_reg = LogisticRegression()
pipe = Pipeline([("imputer", imputer),
("select", feature_select),
("log", log_reg)])
简而言之,Pipeline的参数是一个顺序执行的元组列表。元组的第一个元素是你任意设定的名称,用来标识估算器,有点像 ID。而第二个元素是估算器对象。简单吧?如果你不擅长起名字,Scikit-learn 提供了简写的make_pipeline方法,省去了起名字的麻烦。
from sklearn.pipeline import make_pipeline
imputer = KNNImputer(n_neighbors=5)
feature_select = SequentialFeatureSelector(RandomForestClassifier(n_estimators=100), n_features_to_select=8, direction='forward')
log_reg = LogisticRegression()
make_pipeline(imputer, feature_select, log_reg)
自定义估算器
到目前为止,像StandardScaler和MinMaxScaler这样的方法看起来很好,并且适用于许多情况。问题是,如果你有自己定制的方法来处理和预处理数据集,可以将其整洁地整合到Pipeline类中吗?答案是肯定的!有两种方法可以实现这一点——利用FunctionTransformer或编写你自己的自定义类。
比如你想对数据集的一部分进行 Box-Cox 变换。
from scipy.stats import boxcox
from sklearn.preprocessing import FunctionTransformer
from sklearn.compose import ColumnTransformer
boxcox_features = ['x1', 'x2']
boxcox_transformer = Pipeline(steps=[
('boxcox', FunctionTransformer(lambda x: boxcox(x)[0])
])
col_trans = ColumnTransformer(
transformers=[
('boxcox_features', boxcox_transformer, boxcox_features),
...
],
remainder='passthrough',
n_jobs=-1,
)
第二种方法是编写一个自定义类,继承自BaseEstimator和TransformerMixin,如果你编写的是转换器估算器。例如,如果你编写一个分类任务的估算器,那么应继承自ClassifierMixin。
比如你想编写一个移除异常值的类,并将其整合到你的管道中。
def outlier_thresholds(df: pd.DataFrame,
col: str,
q1: float = 0.05,
q3: float = 0.95):
#1.5 as multiplier is a rule of thumb. Generally, the higher the multiplier,
#the outlier threshold is set farther from the third quartile, allowing fewer data points to be classified as outliers
return (df[col].quantile(q1) - 1.5 * (df[col].quantile(q3) - df[col].quantile(q1)),
df[col].quantile(q3) + 1.5 * (df[col].quantile(q3) - df[col].quantile(q1)))
def delete_potential_outlier_list(df: pd.DataFrame,
cols: list) -> pd.DataFrame:
for item in cols:
low, high = outlier_thresholds(df, col)
df.loc[(df[col]>high) | (df[col]<low),col] = np.nan
return df
class OutlierRemove(BaseEstimator, TransformerMixin):
def __init__(self, outlierlist):
self.outlierlist = outlierlist
def fit(self, X, y=None):
return self
def transform(self,X,y=None):
return delete_potential_outlier_list(X, self.outlierlist)
我特别希望将您的注意力集中在OutlierRemove类上。在这里,我们有fit方法返回self以便继续链式调用,还有transform方法进行异常值的删除。之后,我们可以将该类简单地合并到我们的Pipeline中,如下所示。
pipe = Pipeline([("remove_outlier", OutlierRemove(["a", "b", "c"])),
("imputer", imputer),
("select", feature_select),
("log", log_reg)])
FeatureUnion
这里是令人困惑的部分——FeatureUnion的作用与Pipeline相同,但它们的工作方式却大相径庭。在FeatureUnion中,fit和transform方法不是一个接一个地执行。每个转换器估算器独立地fit数据,然后并行地应用transform方法。最终结果被组合在一起。想象一下下面的代码。在这里,我们可以使用FeatureUnion⁷并行运行数值和分类预测的预处理,因为它们相互独立。这带来了更快和更高效的操作。
from sklearn.pipeline import FeatureUnion
standard_numerical_features = ['x1', 'x2']
standard_numerical_transformer = Pipeline(steps=[
('remove_outlier', OutlierTrans(standard_numerical_features)),
('scale', StandardScaler())
])
ohe_categorical_features = ['x3', 'x4']
ohe_categorical_transformer = Pipeline(steps=[
('ohe', OneHotEncoder(handle_unknown='ignore', sparse_output=False, drop='first'))
])
feature_union = FeatureUnion(
transformers=[
('standard_numerical_features', standard_numerical_transformer),
('ohe_categorical_features', ohe_categorical_transformer),
],
n_jobs=-1,
)
pipeline = Pipeline([
('feature_union', feature_union),
('model', RandomForestClassifier())
])
pipeline.fit(X_train, y_train)
真实世界数据集示例:银行营销与网格搜索 CV
在这里,我希望通过使用受葡萄牙金融机构启发的真实数据集来说明上述内容。该数据集可以在UCI 机器学习库¹上获取,供公众使用并引用。
请允许我跳过所有的探索性数据分析和可视化,直接进入管道建模部分。
1. 导入数据集
import pandas as pd
df = (pd
.read_csv('../../dataset/bank_marketing/bank-11k.csv', sep=',')
.rename(columns={'y': 'deposit'})
.pipe(lambda df_: df_.assign(deposit=np.where(df_.deposit == "no", 0, 1)))
)
简而言之,上面的代码实现了以下几个功能:
-
使用逗号分隔符导入数据集
-
将列‘y’重命名为‘deposit’
-
将列中的“no”和“yes”编码为 0 和 1
2. 训练-测试拆分
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(df.drop(columns=['deposit']),
df[['deposit']].values.ravel(),
test_size=0.2,
random_state=42)
3. 编写另外 3 个自定义类
from sklearn.base import BaseEstimator, TransformerMixin
#Custom class #1: switch between classifiers
class ClfSwitcher(BaseEstimator):
#By default, run XGBClassifier
def __init__(self, estimator = XGBClassifier()):
self.estimator = estimator
def fit(self, X, y=None, **kwargs):
self.estimator.fit(X, y)
return self
def predict(self, X, y=None):
return self.estimator.predict(X)
def predict_proba(self, X):
return self.estimator.predict_proba(X)
def score(self, X, y):
return self.estimator.score(X, y)
#Custom class 2: remove outliers
def outlier_thresholds(df: pd.DataFrame,
col: str,
q1: float = 0.05,
q3: float = 0.95):
return (df[col].quantile(q1) - 1.5 * (df[col].quantile(q3) - df[col].quantile(q1)),
df[col].quantile(q3) + 1.5 * (df[col].quantile(q3) - df[col].quantile(q1)))
def delete_potential_outlier_list(df: pd.DataFrame,
cols: list) -> pd.DataFrame:
for item in cols:
low, high = outlier_thresholds(df, col)
df.loc[(df[col]>high) | (df[col]<low),col] = np.nan
return df
class OutlierTrans(BaseEstimator, TransformerMixin):
def __init__(self, outlierlist):
self.outlierlist = outlierlist
def fit(self, X, y=None):
return self
def transform(self,X,y=None):
return delete_potential_outlier_list(X, self.outlierlist)
#Custom class #3: add new columns, drop column, and modify data types
class TweakBankMarketing(BaseEstimator, TransformerMixin):
def fit(self, X, y=None):
return self
def transform(self, X, y=None):
return (X
.assign(pdays_cat=lambda df_: np.where(df_.pdays < 0, "no contact", "contacted"),
previous_cat=lambda df_: np.where(df_.previous == 0, "no contact", "contacted"),
job=lambda df_: np.where(df_.job == "unknown", np.nan, df_.job),
education=lambda df_: np.where(df_.education == "unknown", np.nan, df_.education),
contact=lambda df_:np.where(df_.contact == "unknown", np.nan, df_.contact),
poutcome=lambda df_: np.where(df_.poutcome == "other", np.nan, df_.contact),
) #add new predictors
.drop(columns=['duration']) #drop predictor due to data leakage
.astype({'age': 'int8',
'balance': 'int32',
'day': 'category',
'campaign': 'int8',
'pdays': 'int16',
'previous': 'int16',})
.pipe(lambda df_: df_.astype({column: 'category' for column in (df_.select_dtypes("object").columns.tolist())})) #convert data type from object to category
)
简而言之,上面的代码实现了以下几个功能:
-
类
ClfSwitcher继承自BaseEstimator。此类的目的是方便地在分类器之间切换。我们将默认分类器设置为 XGBoost 分类器。 -
方法
outlier_thresholds和delete_potential_outlier_list识别每列中的异常值并将其设置为NaN。类OutlierTrans是一个继承自BaseEstimator和TransformerMixin的转换器。transform方法返回之前提到的两个方法。 -
类
TweakBankMarketing是一个自定义类,用于执行自定义转换,例如创建新列、删除不需要的列以及相应地更改数据类型。
4. 准备管道
from sklearn.preprocessing import StandardScaler, MinMaxScaler, OrdinalEncoder, OneHotEncoder,
from sklearn.impute import KNNImputer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
standard_numerical_features = ['age', 'campaign', 'pdays', 'previous'] #drop pdays
standard_numerical_transformer = Pipeline(steps=[
('remove_outlier', OutlierTrans(standard_numerical_features)),
('scale', StandardScaler())
])
minmax_numerical_features = ['balance']
minmax_numerical_transformer = Pipeline(steps=[
('remove_outlier', OutlierTrans(minmax_numerical_features)),
('scale', MinMaxScaler())
])
ohe_categorical_features = ['job', 'marital', 'default', 'housing', 'loan', 'contact', 'poutcome', 'pdays_cat', 'previous_cat']
ohe_categorical_transformer = Pipeline(steps=[
('ohe', OneHotEncoder(handle_unknown='ignore', sparse_output=False, drop='first'))
])
orde_categorical_features = ['education', 'day', 'month']
orde_categorical_transformer = Pipeline(steps=[
('orde', OrdinalEncoder(dtype='float'))
])
col_trans = ColumnTransformer(
transformers=[
('standard_numerical_features', standard_numerical_transformer, standard_numerical_features),
('minmax_numerical_features', minmax_numerical_transformer, minmax_numerical_features),
('ohe_categorical_features', ohe_categorical_transformer, ohe_categorical_features),
('orde_categorical_features', orde_categorical_transformer, orde_categorical_features),
],
remainder='passthrough',
verbose=0,
verbose_feature_names_out=False,
n_jobs=-1,)
pipeline = Pipeline(steps = [
('tweak_bank_marketing', TweakBankMarketing()),
('col_trans', col_trans),
('imputer', KNNImputer(n_neighbors=5)),
('clf', ClfSwitcher()),
])
pipeline
简而言之,上面的代码实现了以下几个功能:
-
使用
StandardScaler和MinMaxScaler对数值列进行缩放 -
使用
OneHotEncoder和OrdinalEncoder对分类列进行编码 -
使用
ColumnTransformer对数据集中的不同列分别进行转换。 -
最终,
Pipeline无缝地封装了所有内容。
在这个阶段,这是我们构建的管道。

作者图片
5. 为网格搜索 CV 定义超参数
#We define all the hyperparameters for 4 classifiers so that we can easily switch from one to another
params_grid = [
{'clf__estimator': [SGDClassifier()],
'clf__estimator__penalty': ('l2', 'elasticnet', 'l1'),
'clf__estimator__max_iter': [500],
'clf__estimator__tol': [1e-4],
'clf__estimator__loss': ['hinge', 'log_loss', 'modified_huber'],
},
{'clf__estimator': [LogisticRegression()],
'clf__estimator__C': [0.01, 0.1, 1, 10, 100],
'clf__estimator__max_iter': [1000]
},
{'clf__estimator': [RandomForestClassifier(n_estimators=100)],
'clf__estimator__max_features': [3,4,5,6,7],
'clf__estimator__max_depth': [3,4,5]
},
{'clf__estimator': [XGBClassifier()],
'clf__estimator__max_depth': [4,5,6],
'clf__estimator__learning_rate': [0.01, 0.1],
'clf__estimator__n_estimators': [80, 100],
'clf__estimator__booster': ['gbtree'],
'clf__estimator__gamma': [7, 25, 100],
'clf__estimator__subsample': [0.3, 0.6],
'clf__estimator__colsample_bytree': [0.5, 0.7],
'clf__estimator__colsample_bylevel': [0.5, 0.7],
'clf__estimator__eval_metric': ['auc']
},
]
简而言之,上述代码执行的操作如下:
- 为 4 种不同的分类器定义参数网格,即
SGDClassifier,LogisticRegression,RandomForestClassifier,XGBClassifier。
6. 执行网格搜索 CV
from sklearn.model_selection import GridSearchCV
%%time
grid = GridSearchCV(pipeline, params_grid, cv=5, n_jobs=-1, return_train_score=False, verbose=0)
grid.fit(X_train, y_train)
简而言之,上述代码执行的操作如下:
- 将我们的管道对象作为
GridSearchCV参数的第一个参数。
7. 打印最佳估计器
print(f'Best params: {grid.best_params_}')
print(f'Best CV score: {grid.best_score_}')
print(f'Validation-set score: {grid.score(X_test, y_test)}')
print(f'Accuracy score: {accuracy_score(y_test, grid.predict(X_test))}')
print(f'Precision score: {precision_score(y_test, grid.predict(X_test))}')
print(f'Recall score: {recall_score(y_test, grid.predict(X_test))}')
print(f'ROC-AUC score: {roc_auc_score(y_test, grid.predict(X_test))}')
在这里,我们获得了 0.74 的验证分数,以及 0.74 的 AUC 分数。
8. 绘制 ROC-AUC 曲线
fpr, tpr, thresholds = skmet.roc_curve(y_test, grid.predict(X_test))
roc_auc = skmet.auc(fpr, tpr)
display = skmet.RocCurveDisplay(fpr=fpr,
tpr=tpr,
roc_auc=roc_auc,
estimator_name='XGBoost Classifier')
display.plot();

图片来源:作者
后记
就是这样!使用估计器和转换器的管道。下次当你处理 ML 项目时,考虑使用这个技术。起初可能觉得难以采用,但持续练习,很快你就能创建出稳健而高效的机器学习管道。
如果你从这篇文章中获得了有用的信息,请考虑在 Medium 上给我一个关注。简单,每周一篇文章,保持更新并走在前沿!
关注我!
参考文献
-
银行营销数据集 [Moro et al., 2014] S. Moro, P. Cortez, 和 P. Rita。基于数据的方法来预测银行电话营销的成功。决策支持系统,Elsevier,62:22–31,2014 年 6 月:
archive.ics.uci.edu/ml/datasets/Bank+Marketing(CC BY 4.0) -
Scikit-learn 线性模型逻辑回归:
github.com/scikit-learn/scikit-learn/blob/364c77e047ca08a95862becf40a04fe9d4cd2c98/sklearn/linear_model/_logistic.py -
Scikit-learn 预处理:
github.com/scikit-learn/scikit-learn/blob/364c77e04/sklearn/preprocessing/_data.py#L644 -
开发 Scikit-learn 估计器:
scikit-learn.org/stable/developers/develop.html -
Scikit-learn ColumnTransformer:
scikit-learn.org/stable/modules/generated/sklearn.compose.ColumnTransformer.html -
Scikit-learn 管道:
scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html -
Scikit-learn FeatureUnion:
scikit-learn.org/stable/modules/generated/sklearn.pipeline.FeatureUnion.html
掌握定价优化的艺术 — 一种数据科学解决方案
解锁零售定价优化中的真实世界数据科学解决方案的秘密
·
关注 发表在 Towards Data Science ·16 min 阅读·2023 年 8 月 28 日
--
作者提供的图片
目录:
1. 概述2. 弹性建模3. 优化
1. 概述
定价在商业世界中扮演着至关重要的角色。平衡销售和利润对于任何业务的成功至关重要。我们如何以数据科学的方式来实现这一目标?在本节中,我们将建立一个有效的数据科学解决方案的直觉,然后深入了解每个组件的细节和代码。
注意 — 尽管有不同类型的定价策略,但在本文中,我们将重点关注为拥有足够价格变动历史数据的传统业务/成熟品牌建立定价策略。在详细讨论之前,让我们先看看我们尝试遵循的基本方法 —

图片由作者提供
我们已经为商品 1 绘制了销售和价格图。在过去的 9 个月里,价格发生了 2 次变动,我们可以清楚地看到销售的影响。当价格较低时,销售额较高。现在的问题是如何量化过去价格变动对销售的影响,并预测未来商品的最佳价格。
从 1 月至 4 月的一个有趣观察是,价格固定在$5,但我们仍然观察到销售波动。这是非常正常的,因为在实际世界中,有许多外部因素会影响销售,如季节性、节假日、促销活动、市场营销支出等。因此,我们不对实际销售进行建模,而是使用不同模型推导出的基线销售。

图片由作者提供
你可以观察到,我们正在查看基线销售系列中的平滑销售趋势。它是 100%准确的吗?当然不是!数据科学的关键在于我们能接近现实的程度。现在让我们进入过程 —
假设我们被聘请并要求为 Retailmart 集团的数千种商品提供价格。这些商品在不同商店的价格可能不同。公司提供了过去 5 年的数据。我们应该采用什么方法来解决这个问题?
让我们通过一个价格计量器的例子来理解这一点。假设我们有一个价格计量器,我们已经固定了最低和最高值,并且拨盘可以在这两个极端之间移动。目前,拨盘指向当前价格。我们的目标是将拨盘停在一个可以最大化利润的点上。
现在,当我们将调整拨盘向右移动(即我们在提高价格)时,销售将开始下降,利润率将增加,但
-
我们可以量化这种下降吗?是的,我们可以,这被称为项目的价格弹性。简单来说,价格弹性表示价格变动 1%时,销售额的百分比变化。
-
在现实世界中,销售往往受到促销活动、假期、额外折扣等因素的驱动,但为了优化价格,我们需要排除所有这些外部因素的影响,并计算基线销售。
-
一旦我们量化了销售变化与价格变化的关系,我们需要的答案是,我应该在哪儿停下调整? 为此,我们需要一个目标,通常是最大化利润。利润 = 销售额 * 利润率,因此我们需要在利润值达到最大的位置停下。从数学上讲,这是一种非线性优化的概念,其中值可以在范围内移动。
-
商业规则很重要,我们必须确保最终推荐的价格符合这些规则。
所以这些是我们将遵循的主要步骤,以为给定商店中的每件商品确定正确的价格。让我们更详细地了解这些步骤 —
1. 基线销售/基本单位
这一步是后续步骤的前提步骤。如前所述,我们希望建模价格变化对销售的影响。理想情况下,销售只会受到价格变化的影响,但在实际情况下,情况从未如此 —
所以我们希望模拟我们理想情况下的销售,并通过下面的方程式使用时间序列模型来实现 —
销售 ~ 函数[基线销售 + (促销效果) + (假期效果) + (其他任何效果)]
请注意,有时我们没有实际的数据来反映影响销售的外部因素。在这种情况下,我们可以使用虚拟变量来考虑所有这些因素。例如,如果在某个月我们看到销售突然增加但价格保持不变,我们可以引入一个简单的虚拟变量,在该月标记为 1,其余月份标记为 0。
2. 价格弹性
价格弹性是指销售量相对于某商品价格变化的百分比变化。
举个例子,考虑两种产品牛奶和 ABC 绿茶。你认为哪一个会有较高的价格弹性?
牛奶作为一种必需的日常商品,竞争激烈,显示出较高的价格弹性。即使是价格的微小变化也能显著影响销售,因为其需求广泛。另一方面,ABC 绿茶可能仅在少数商店有售,价格弹性较低。由于其市场定位,小幅度的价格变化不太可能对销售产生重大影响。
我们将如何建模这个?
基线销售 ~ 函数[(价格) + 趋势]
价格变量的系数将用作价格弹性。趋势变量用于考虑由于长期趋势而导致的销售增长,而不一定是由于价格变化。我们将在下面的价格弹性部分详细讨论如何计算弹性。
3. 在范围内的非线性优化
在这一步中,我们将得到旋钮应该停在哪儿的答案。
我们首先定义我们的目标函数——最大化利润
然后我们定义价格计量器的起点和终点,这定义了价格的下界(LB)和上界(UB)
我们已经计算了基线销售和价格弹性,这些量化了销售对价格的敏感性。我们将把所有这些输入放入我们的非线性优化函数中,得到优化后的价格。
用非常简单的话来说,算法将在范围内尝试不同的价格点,并检查目标函数的值,在我们的案例中是利润。它会返回到能够为我们的目标函数获得最大值的价格点。(在线性优化中,可以想象一下梯度下降的工作方式)。我们将在下面的优化部分中讨论计算优化价格的更多细节。
4. 商业规则
那么我们可以直接在我们的商店里实施优化后的价格吗?
不能,但现在还剩下什么?遵循商业规则是任何业务最重要的要求之一。
但我们讨论的定价规则是什么——
-
结束数字规则——通常我们会将产品定价为$999 或$995,而不是$1000。这样做有几个心理学原因,因此我们需要确保最终推荐的价格遵循这些规则(如适用)。
-
产品差距规则——你能否以单个包装的 Maggi 价格高于 4 包装的 Maggi?不能,对吧。通常情况下,如果包装的数量增加,每单位的成本应该下降,或至少保持不变。
这些是业务希望应用的一些商业规则示例。我们将对优化价格进行一些后处理步骤,以获得最终推荐价格。
现在你了解了整体过程,是时候深入探讨更多细节和编码了。
2. 弹性建模
在本节中,我们将了解如何利用这个概念来推导出在多个商店中 1000 多件商品的优化价格。假设我们需要确定过去 3 年在加州商店销售的零食 Yochips 的价格弹性。让我们首先看看价格弹性的定义:
价格弹性定义为价格变化 1%时,销售额的百分比变化。
现在你可能在想,我可以使用哪个算法来计算像 Yochips 这样的商品的价格弹性?
让我们深入探讨经济学书中的常数价格弹性模型,看看是否能将其与一些数据科学算法相关联。
需求函数的乘法形式将是:-
Yi = α*Xi^β(其中 y 为销售/需求,x 为价格,β 为价格弹性)
对两边取对数
log(Yᵢ) = log(α*Xᵢ^β)
log(Yᵢ) = log(α) + β*log(Xᵢ) ……….Eq(1)
log(α) 可以视为截距,类似于 β₀
log(Yᵢ) = β₀ + β₁*log(Xᵢ) ………….Eq(2)
现在对两边进行微分,我们将得到
δY/Y = β₁*δX/X
左侧的项表示 Y 的百分比变化,即销售额的百分比变化,而右侧的项表示价格的百分比变化。现在当
%价格变化 = 1%;则 δX/X = 1
δY/Y = β₁
这意味着销售额的百分比变化将是β₁,这就是我们的弹性。
现在,如果你注意到的话,方程 2 是一个回归方程,其中对数(销售)对数(价格)进行回归,对数(价格)的系数将是我们的价格弹性。
太棒了!现在我们知道计算弹性就像训练一个回归模型一样简单。
但还有一个问题。需求函数方程有一些假设,其中之一是销售仅受价格影响,但在现实世界中通常并非如此,因为销售通常受促销、假期、活动等多个因素的影响。那么解决方案是什么?我们需要计算销售组件,从而去除所有这些额外事件的影响。
有一点需要澄清的是,基础销售指的是单位销售,而不是美元销售。因此,在方程 2 中,我们需要将价格对基础单位进行回归,而不是实际单位销售。那么问题是我们如何从实际销售单位中推导出基础单位?
我们通过一个例子来理解。下面你可以看到销售单位和销售价格的时间序列的每周图:

作者提供的图片
你能在上面的图中看到任何模式吗?这很难判断,因为销售单位系列中有太多波动。这些波动可能由多个因素造成,如假期、促销、活动、FIFA 世界杯等。为了隔离价格变化的影响,我们需要计算排除这些额外因素影响的销售数据。
使用 prophet 模型,我们可以分解时间序列并提取代表基础销售的趋势组件。通过应用这一技术,我们将价格变化的长期影响与其他短期影响分开。让我们看看我们在说什么:

作者提供的图片
在上面的图中,我们将原始对数销售单位(灰色)分解为对数基础单位(黄色线图)
这里有一个代码,你可以用它来分解时间序列并获取将成为基础销售的趋势组件:
# Defining the inputs
timestamp_var = "week_ending_sunday"
baseline_dep_var = "ln_sales"
changepoint_prior_scale_value = 0.3
list_ind_vars_baseline = ['event_type_1_Cultural','event_type_1_National','event_type_1_Religious','event_type_1_Sporting']
# Preparing the datasecloset
df_item_store = df_item_store.rename(columns={timestamp_var: 'ds', baseline_dep_var: 'y'})
df_item_store['ds'] = pd.to_datetime(df_item_store['ds'])
# Initializing and fitting the model
model = Prophet(changepoint_prior_scale= changepoint_prior_scale_value) #Default changepoint_prior_scale = 0.05
# Add the regressor variables to the model
for regressor in list_ind_vars_baseline:
model.add_regressor(regressor)
model.fit(df_item_store)
# Since we are only decomposing current time series, we will use same data is forecasting that was used for modelling
# Making predictions and extracting the level component
forecast = model.predict(df_item_store)
level_component = forecast['trend']
以下是我们需要定义的输入:
-
changepoint_prior_scale_value — 这控制趋势的平滑性。你可以在 prophet 模型文档中阅读更多关于它的信息。
-
list_ind_vars_baseline — 这些包括所有影响销售的额外事件,如节日、体育赛事、文化活动等。
changepoint_prior_scale_value 如何影响趋势:当值较小时,它会导致几乎是一条直线;当值较高时,趋势就不那么平滑了。

代码很简单,首先,我们将“ln_sales”变量重命名为“y”,将“week”变量重命名为“ds”,以满足使用 Prophet 模型的前提条件。接下来,我们初始化 Prophet 模型,指定“changepoint_prior_scale”参数。随后,我们将额外的事件和假期变量纳入模型。最后,我们使用训练模型的同一数据集生成预测,并提取趋势成分。
很好。我们现在有了基础单位系列,我们可以在基础单位(因为我们分解了 log_base_units 系列,已经是对数尺度)和 log(price) 之间拟合线性回归模型。下面是方程:-
log(base units) = 截距 + 弹性*log(price)
使用上述方程,我们可以计算弹性值。在实际操作中,并不是所有的系列都适合建模,因此你可能会遇到一些不同于预期的弹性值。那解决方案是什么呢?如果我们能以某种方式在弹性值上施加约束进行回归该怎么办?答案是使用优化函数。
对于任何优化,基本要求如下:-
-
目标函数 —— 这是我们尝试最小化/最大化的方程。在我们的案例中,它将是我们在线性回归中使用的损失函数 MSE (pred-actual => [截距 + 弹性 *ln_price — 实际值]²)
-
我们尝试优化的参数的初始值,在我们的案例中是截距和弹性。这些初始值可以是任何随机值。
-
参数的边界,即截距和弹性的最小值和最大值
-
优化算法 —— 这取决于库,但你可以使用默认设置,这应该能给你正确的结果
现在让我们来看一下代码:-
# Preparing the matrix to feed into optimization algorithm
x = df_item_store_model
x["intercept"] = 1
x = x[["intercept","ln_sell_price","ln_base_sales"]].values.T
# x_t = x.T
actuals = x[2]
from scipy.optimize import minimize
# Define the objective function to be minimized
def objective(x0):
return sum(((x[0]*x0[0] + x[1]*x0[1]) - actuals)**2) # (intercept*1 + elasticity*(ln_sell_price) -ln_base_sales)²
# Define the initial guess
x0 = [1, -1]
# Define the bounds for the variables
bounds = ((None, None), (-3,-0.5))
# Use the SLSQP optimization algorithm to minimize the objective function
result = minimize(objective, x0, bounds=bounds, method='L-BFGS-B')
# Print the optimization result
print(result)
# Saving the price elastitcity of an item in the dataframe
price_elasticity = result.x[1]
df_item_store_model["price_elasticity"] = result.x[1]
注意,我们为截距定义了初始参数值为 1,为弹性定义了初始参数值为 -1\。截距没有定义边界,而弹性则定义了 (-3,-0.5) 的边界。这就是我们通过优化函数进行回归的主要原因。在运行优化后,我们保存了优化后的价格弹性参数值。太棒了!我们已经计算出价格弹性!
因此,我们在加利福尼亚商店的 Yochips 价格弹性为 -1.28。
让我们看看其他系列的价格弹性:-
低价格弹性:价格上涨时销售额没有显著变化。下面是一个价格弹性为 -0.5 的项目的图表

作者提供的图片
中等价格弹性:价格上涨时销售额有适度下降。下面是一个价格弹性为 -1.28 的项目的图表

作者提供的图片
高价格弹性:价格上涨时销售额有很大下降。下面是一个价格弹性为 -2.5 的项目的图表

作者提供的图片
使用相同的方法,我们可以计算所有物品的价格弹性。在下一篇文章中,我们将探讨如何利用这些弹性值来确定每个物品的优化价格。
3. 优化
在前一部分中,我们已经确定了 Yochips 在加州商店的价格弹性。但这对商店经理并没有帮助,他想知道如何调整 Yochips 的价格以最大化收益。在这篇文章中,我们将了解优化价格的方法。
但在此之前,我们必须向商店经理提出一些问题。
问:在优化价格时,是否应考虑最低和最高价格变动的限制?
根据与商店经理的讨论,我们确定价格下降不得超过 20%,价格上涨也应限制在 20%以内。
目前 Yochips 的价格是$3.23,现在我们知道优化价格必须在$2.58 — $3.876 之间。但我们如何得出优化价格呢?
但是我们如何得出一个最大化收益的优化价格呢?让我们做些数学运算:-
优化收益 = 总售出单位 * (优化价格)
我们需要优化价格以最大化收益。但总售出单位也会随着价格变化而变化。让我们重新写一下上述方程,并将总售出单位称为优化单位:-
优化收益 = 优化单位* (优化价格)……………(eq1)
我们已经知道 —
弹性 = 售出单位的百分比变化/价格的百分比变化
因此:-
优化单位 = 基准单位 + 优化价格下的单位变化
这里,基准单位指的是当前价格为$2.58 时的总销售单位
优化单位 = (基准单位 + (基准单位 * 价格弹性 * (优化价格与常规价格的百分比变化) ………. (eq2)
让我们将 eq2 代入 eq1 中
优化收益 = (基准单位 + (基准单位 * 价格弹性 * (优化价格与常规价格的百分比变化) * (优化价格) …….. (eq3)
优化收益 = (基准单位 + (基准单位 * 价格弹性 * [(优化价格 — 当前价格)/ 当前价格]* (优化价格)…………..eq(4)
以下是优化方程(eq4)中的关键参数:
基准单位 = 当前价格下的平均销售单位。
价格弹性 = 计算得出的物品价格弹性值
当前价格 = 最新销售价格
很好!在我们的方程式中,除了优化价格外,我们还有所有其他变量的数据。那么我们可以使用哪种算法来计算一个最大化收益的优化价格?我们可以简单地使用优化算法。
优化所需的关键组件是什么:-
-
需要最小化/最大化的目标函数:- 我们已经定义了目标函数,即最大化优化收益,如方程(eq4)所定义。
-
边界:根据店铺经理的定义,我们需要优化价格变化不超过 20%。因此,下限 = 当前价格(1–0.2),上限 = 当前价格(1+0.2)
-
优化算法:我们将使用 Python 中的 Scipy.optimize 库来实现优化。
让我们看看代码:-
# Taking latest 6 weeks average of the base sales
#--------------------------------------------------
# Ranking the date colume
df_item_store_optimization["rank"] = df_item_store_optimization["ds"].rank(ascending=False)
# Subset latest 6 weeks of data
base_sales_df = df_item_store_optimization.loc[df_item_store_optimization["rank"] <= 6].groupby("id")["base_sales"].mean().reset_index()
df_item_store_optimization_input.rename(columns = {"base_sales":"base_units"}, inplace=True)
# Deriving the min and max bound for the sell_price
#--------------------------------------------------
# Creating UB and LB as with the range of 20%
df_item_store_optimization_input["LB_price"] = df_item_store_optimization_input["sell_price"] - (0.2*df_item_store_optimization_input["sell_price"])
df_item_store_optimization_input["UB_price"] = df_item_store_optimization_input["sell_price"] + (0.2*df_item_store_optimization_input["sell_price"])
上述代码帮助我们准备数据以进行优化。首先,我们将计算基准单位,即最近 6 周base_sales(分解系列的趋势成分)的平均值。我们已经在 t节中讨论了计算base_sales的方法。
接下来,我们通过分别从当前销售价格减少和增加 20%来定义LB_price和UB_price。
让我们定义执行优化的代码。
from scipy.optimize import minimize
# Define the objective function to be minimized
def objective(opti_price):
df_item_store_optimization_input["opti_price"] = opti_price
df_item_store_optimization_input["optimized_units"] = df_item_store_optimization_input["base_units"] + (df_item_store_optimization_input["base_units"]*\
((df_item_store_optimization_input["opti_price"]/df_item_store_optimization_input["sell_price"]) - 1)*\
(df_item_store_optimization_input["price_elasticity"]))
df_item_store_optimization_input["optimized_revenue"] = df_item_store_optimization_input["optimized_units"]*df_item_store_optimization_input["opti_price"]
return -sum(df_item_store_optimization_input["optimized_revenue"])
# Define the initial guess
opti_price = df_item_store_optimization_input["sell_price"][0]
# Define the bounds for the variables
bounds = ((df_item_store_optimization_input["LB_price"][0], df_item_store_optimization_input["UB_price"][0]),)
# # Use the optimization algorithm to minimize the objective function
result = minimize(objective, opti_price, bounds=bounds)
# Print the optimization result
print(result)
上述代码将为我们提供优化价格。你能猜到在目标函数中为什么定义了负优化收益吗?-(-1)是什么?是 1。我们在最小化目标函数,使用负号表示优化收益将导致最大化优化收益。
此外,我们可以用任何随机值初始化opti_price变量,仅仅为了促进快速收敛,我们将其初始化为当前的sell_price。在边界中,我们定义了在上述代码中创建的LB和UB。
万岁!我们已经找到了 Yochips 的优化价格,并准备向加州的店铺经理提议。

图片由作者提供
我们的建议是将 Yochips 的价格降低 10.2%至$2.9。这将带来最大的收益。
这是价格优化方法的最后一步,这种整体方法非常强大,可以帮助我们为每个店铺的每个商品返回优化价格。
上述方法的一个局限性是对于我们没有足够价格变动历史的商品。在这种情况下,我们使用其他技术,但如果这种商品的比例较少,则可以使用类别级别的平均价格弹性。
希望你喜欢这篇文章!现在你知道如何进行价格优化了。

图片由作者提供(字体由 onlinewebfonts 制作)
掌握回归分析的艺术:每个数据科学家应该了解的 5 个关键指标
关于回归分析中使用的度量的所有知识的权威指南
·发布在 Towards Data Science ·阅读时间 14 分钟·2023 年 2 月 20 日
--

由作者在 Dall-E 上根据提示“一个未来派机器人在黑板上教数学”创建的图像。
在监督学习的情况下,我们可以将机器学习问题细分为两大类:回归问题和分类问题。
在本文中,我们将讨论在回归分析中使用的五个度量,以了解模型是否适合解决特定的机器学习问题。
但首先,让我们回顾一下回归分析是什么。
回归分析是一种数学技术,用于寻找因变量与一个或多个自变量之间的函数关系。
在机器学习中,我们将“特征”定义为自变量,将“标签”(或“目标”)定义为因变量,因此回归分析的目的是找到特征与标签之间的估计(一个好的估计!)。
**Table Of Contents**
The residuals
1\. The mean squared error (MSE)
2\. The root mean square error (RMSE)
3\. The mean absolute error (MAE)
4\. The Coefficient of Determination (R²)
5\. The adjusted R²
Calculating all the Metrics in Python
残差
在讨论度量之前,我们需要讨论残差。
为了简化起见,我们考虑线性回归模型(但结果可以推广到任何其他机器学习模型)。
比如,假设我们有一个数据集,其中数据以某种线性方式分布。我们通常会遇到如下情况:

回归线。作者提供的图像。
红线称为回归线,它是我们将用来进行预测的线。如我们所见,数据点并不完全位于回归线上,因此我们可以将残差定义为回归线(预测值)与实际数据点之间的误差,方向为垂直方向。
因此,对于上述图像,我们从数学上定义残差为:

残差的定义。图像由作者提供。
我们希望得到 e_i=0,因为这意味着所有数据点都恰好位于回归线上的,但不幸的是,这不可能实现,因此我们在回归问题中使用以下度量来验证我们的 ML 模型。
我们定义 “帽子” y 为 拟合值 或 预测值(由模型拟合/预测:在这种情况下是线性回归模型),而 y 是 真实值。因此,预测值可以计算为:

如何计算预测值。图像由作者提供。
在上述公式中,系数 w(称为 权重)和 b(称为 偏置 或 常数)是估计值,这意味着它们在学习过程中由 ML 模型学习得到。
这些知识很重要,因为现在我们可以将 残差平方和(RSS) 定义为:

残差平方和的公式。图像由作者提供。
现在,如果我们将之前看到的预测值公式代入括号中,我们得到:

残差平方和的扩展公式。图像由作者提供。
估计的系数 w 和 b 是那些最小化 RSS 的系数。
实际上,我们必须记住,学习过程要求所选的 度量(也称为 成本函数 或 损失函数)必须被最小化。
在数学中,最小化一个函数意味着计算它的导数并将其等于 0。因此,我们应该执行类似的操作:

RSS 函数对 w 的导数。图像由作者提供。
和

RSS 函数对 b 的导数。图像由作者提供。
我们不会在这里进行计算;请考虑这些计算的结果是:

最小化 RSS 函数的值。图像由作者提供。
在上述公式中,带“上划线”的 x 和 y 是均值。因此它们可以计算为:

x 的均值(也适用于 y)。图像由作者提供。
现在,考虑到这些,我们将定义和计算 5 个成本函数。
我们将使用表格中的 5 个数字来展示各种度量之间的差异。该表格包含以下内容:
-
真实值。
-
预测值(由线性回归模型预测的值)。

我们将用于后续计算的表格。图像由作者提供。
**NOTE**: consider these data as calculated on the train set. In the following
calculations we'll give for granted that we refer just to the train set
and we won't discuss the test set.
1. 均方误差(MSE)
我们定义 均方误差 (MSE) 如下:

MSE 的定义。图片来源:作者。
其中 n 是观察值的数量。换句话说,它表示我们总共有多少个值。在我们的例子中,由于我们有一个包含 5 个数字的表格,因此 n=5。
MSE 衡量预测值与实际值之间的平均平方差。换句话说,它告诉我们我们的预测值与实际值的平均偏差。
让我们根据表格中的值进行计算:

使用给定数字计算 MSE。图片来源:作者。
我们得到:MSE = 51.2
2. 均方根误差 (RMSE)
均方根误差 (RMSE) 只是 MSE 的平方根;所以它的公式是:

RMSE 的定义。图片来源:作者。
现在,让我们考虑上述表格中的值,并计算 RMSE:

使用给定数字计算 RMSE。图片来源:作者。
MSE 和 RMSE 之间没有很大的差别。它们指的是相同的量,唯一的数学区别在于 RMSE 有一个平方根。然而,RMSE 更容易解释,因为它与输入值(预测值和真实值)具有相同的单位,因此更直接地与它们进行比较。
让我们举个例子来理解这个问题。
想象一下,我们已经训练了一个线性回归模型,以预测房屋的价格,基于其面积和卧室数量。我们计算 MSE 和 RMSE 的值并进行比较。
假设模型预测一座 1000 平方英尺、2 间卧室的房子的价格为 200,000 美元。然而,房子的实际价格为 250,000 美元。我们将得到:

房子的价格的 MSE(在这种情况下,n=1,因为我们只计算了一个值)。图片来源:作者。
和

房子的价格的 RMSE(在这种情况下,n=1,因为我们只计算了一个值)。图片来源:作者。
所以,这里要点是:RMSE 很容易与输入数据进行比较,因为在这种情况下,我们如何解释 USD² 作为度量单位?这是无法解释的,但它是正确的!
所以,这就是这两个指标之间的区别。
3. 均值绝对误差 (MAE)
均值绝对误差 (MAE) 是另一种计算实际数据点和预测数据点之间距离的方法。其公式是:

MAE 的定义。图片来源:作者。
这里,实际值和估计值之间的距离是通过范数(有时称为“曼哈顿距离”)计算的。
从公式中我们可以看到,尽管 MAE 也与输入值具有相同的单位,因此也容易解释。
现在,让我们考虑表格中的值,并计算 MAE:

使用给定数字计算 MAE。作者提供的图像。
我们得到:MAE = 5.6。
现在,在解释其他两个指标之前,我们需要说一下上面提到的三个指标。
我们已经看到 MAE 和 RMSE 比 MSE 更容易解释,因为我们得到的结果与输入数据具有相同的单位,但这并不是唯一需要说明的事。
另一个需要说明的是,任何这些指标接近 0 的值都表示模型的预测更接近实际值;换句话说,模型对数据的预测效果很好。
相反,远离 0 的值表明模型的预测值远离实际值;换句话说,模型对数据的预测效果很差。
另一个我们可以说的事是,MSE 和 RMSE 对异常值非常敏感,因为它们基于预测值和真实值之间的平方差。在实际值与预测值之间有少量大误差的情况下,平方误差将非常大,这会显著影响 MSE 和 RMSE。在这些情况下,使用对异常值不那么敏感的 MAE 可能更为合适。
如果我们分析上面的表格,可以看到第五个数据点的预测值非常偏离(真实值为 50,而预测值为 64),这对 MSE 产生了显著影响,但对 MAE 的影响较小,如我们从获得的结果中所见。
所以,我们首先要做的事情之一是正确处理异常值(这里有一篇文章解释了如何做到这一点)。
另一点需要考虑的是,我们不会仅仅使用一个模型来解决我们的机器学习问题:通常,我们从 4 到 5 个模型开始,调整它们的超参数,最后选择最佳模型。
但作为起点,如我们所知,我们无法为 4 到 5 个模型计算 MAE、MSE 和 RMSE,因为这会耗费大量时间。
所以,让我们看看我们通常面临的情况:我们决定使用 5 个机器学习模型的池,并且例如,我们计算了 MAE 并得到了以下结果:
-
ML_1 的 MAE:115
-
ML_2 的 MAE:351
-
ML_3 的 MAE:78
-
ML_4 的 MAE:1103
-
ML_5 的 MAE:3427
我们知道 MAE 的值(但这同样适用于 MSE 和 RMSE)必须尽可能接近 0;因此,我们可以立即理解 ML_1 和 ML_3 在我们选择的 5 个模型中表现最好,但问题是:它们有多好?
这些指标中的每一个都可以达到任何值,甚至 100 万或更多。我们只知道我们必须尽可能接近 0 才能说我们的模型在解决这个机器学习问题上表现良好;但是结果必须离 0 有多近?MAE 为 78 足以说明 ML_3 在解决这个机器学习问题上非常好吗?
因为这些指标都可以达到任意值,统计学家定义了另外两个值在 0 到 1 之间的指标。这对于一些数据科学家在比较不同模型的结果时可能更有帮助。
4. 决定系数(R²)
我们定义 决定系数(或 R²)如下:

决定系数的定义。图片由作者提供。
在这里,我们之前定义了 RSS 为残差平方和。然后我们得到了总平方和,其定义为:

总平方和的定义。图片由作者提供。
TSS 简单来说就是预测变量 y 的方差;实际上,我们可以将分子和分母都乘以并除以 1/n:

决定系数的修改定义。图片由作者提供。
现在,分子正好是 MSE,分母是 y 的方差;所以我们可以写:

决定系数的另一种定义方式。图片由作者提供。
如果 R²=1,这意味着 MSE=0,因此模型完全符合数据。相反,R²=0 表示我们的模型完全不符合数据。
R² 的范围在 0 和 1 之间,正如我们希望的,但仅适用于训练集。这意味着 TSS>RSS 或 var(y)> MSE。相反,在测试集上,R² 可能变成负值,这意味着我们的模型对测试集的拟合很差(但我们在这里不会进一步讨论)。
现在,让我们回顾一下之前的工作。使用上面提供的表格,我们得到了:
-
MSE = 51.2
-
RMSE = 7.15
-
MAE = 5.6
因此,从 RMSE 和 MAE 来判断,我们用于这些计算的(唯一)模型似乎很好,因为我们接近 0。
但如果你熟悉数学分析,你可能会同意 5.6 可以认为离 0 很远。这是因为我们没有参考标准来进行判断。
现在,让我们看看计算 R² 会发生什么。
让我们计算 y 的均值:

表中提供的 y 的均值。图片由作者提供。
现在我们可以计算方差:

表中提供的 y 的方差。图片由作者提供。
我们之前计算了 MSE(MSE = 51.2),所以,最后我们有:

使用提供的值计算决定系数。图片由作者提供。
记住,在训练集上,R² 的范围在 0 和 1 之间,且离 1 越近模型越好,一般认为 R² 为 0.7 或更高是一个良好的拟合。
因此,我们可以立即且毫无疑问地说,我们的模型对数据的拟合相当好,因为我们知道我们可以获得的最佳值是 1,由于我们得到了 0.739,我们可以说,就比较而言,这个结果相当不错。
R²的问题在于,当我们向模型中添加额外的解释变量时,它往往会增加。这是因为额外的变量可能会改善模型的拟合度。因此,当我们向模型中添加更多的解释变量时,它对预测变量的信息更多,这可能使预测更准确。然后,这可能导致预测变量的方差减少,从而使 R²增加。
要确定一个变量是否对我们的模型具有解释性,我们必须考虑它是否可能对因变量产生影响。例如,如果我们研究收入与幸福之间的关系,假期花费的钱可能被认为是一个解释变量,因为它可能对幸福产生影响。另一方面,受访者的汽车颜色在这种情况下可能不会被认为是解释变量,因为它不太可能对幸福产生影响。
为了应对 R²这种行为,统计学家定义了调整后的 R²。
5. 调整后的 R²
调整后的 R²是 R²的一种特殊形式,用于修正由于模型中新增解释变量而可能导致的 R²过高的情况。我们可以定义为:

调整后的 R²的定义。图像由作者提供。
其中:
-
n是我们数据中的样本数量。
-
p是特征的数量(在回归问题中有时称为预测变量:这就是我们使用字母p的原因)。
假设我们有一个包含 2 个自变量和 10 个样本的模型,该模型的 R²为 0.8。我们有:
调整后的 R²的计算。图像由作者提供。
通常,当模型中有大量自变量时,建议使用调整后的 R²,因为它比“标准”R²提供了更准确的度量。
在 Python 中计算所有指标
幸运的是,在 Python 中我们无需计算这些指标:sklearn库会为我们计算,除了调整后的 R²:在这种情况下,我们必须通过编码来计算公式的参数。
让我们看一个例子。我们生成一些随机数据,用线性回归模型拟合训练集,并打印所有指标的结果。
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
# generate random data
np.random.seed(42)
X = np.random.rand(100, 5)
y = 2*X[:,0] + 3*X[:,1] + 5*X[:,2] + np.random.rand(100)
# split data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,
random_state=42)
# fit linear regression model to the train set
reg = LinearRegression()
reg.fit(X_train, y_train)
# make predictions on the train set
y_pred_train = reg.predict(X_train)
# calculate metrics on the train set with 2 decimals
mae_train = round(mean_absolute_error(y_train, y_pred_train), 2)
mse_train = round(mean_squared_error(y_train, y_pred_train), 2)
rmse_train = round(np.sqrt(mse_train), 2)
r2_train = round(r2_score(y_train, y_pred_train), 2)
# calculate adjusted r-squared on the train set with 2 decimals
n = X_train.shape[0] #number of features
p = X_train.shape[1] #number of predictors
adj_r2_train = round(1 - (1 - r2_train) * (n - 1) / (n - p - 1), 2)
# print the results
print("Train set - MAE:", mae_train)
print("Train set - MSE:", mse_train)
print("Train set - RMSE:", rmse_train)
print("Train set - r-squared:", r2_train)
print("Train set - adjusted r-squared:", adj_r2_train)
>>>
Train set - MAE: 0.23
Train set - MSE: 0.07
Train set - RMSE: 0.26
Train set - r-squared: 0.98
Train set - adjusted r-squared: 0.98
现在,在这种情况下,R²和调整后的 R²之间没有差异,因为数据是故意创建的,而且我们只有 5 个特征。
这段代码只是为了展示我们如何在 Python 中将本文所学的知识应用于实际案例。
此外,我们可以清楚地看到 MAE、MSE 和 RMSE 接近 0 的含义。由于 R² 是 0.98,实际上这些指标是“0.xx”,这比我们在表格示例中找到的 5.6 更接近 0。
结论
到目前为止,我们已经全面了解了与机器学习回归分析相关的所有指标。
即使我们写了很长的文章,我们希望这能帮助读者更好地理解这些指标的底层内容,更好地理解如何使用它们,以及它们之间的区别。
你可能也喜欢:
成为掌握机器学习分类指标的唯一指南
towardsdatascience.com
免费 Python 电子书:
开始学习 Python 数据科学但感到困惑? 订阅我的新闻通讯,获取我的免费电子书:这将为你提供正确的学习路径,以通过实践经验学习 Python 数据科学。
享受这个故事了吗?成为 Medium 会员每月 5$ 通过我的推荐链接:我会获得小额佣金,对你没有额外费用:
[## 通过我的推荐链接加入 Medium — Federico Trotta
阅读 Federico Trotta 的每一个故事(以及 Medium 上成千上万其他作家的故事)。您的会员费用直接支持…
medium.com](https://medium.com/@federicotrotta/membership?source=post_page-----1e2a8a2936f5--------------------------------)
Python:
-
Python 中的循环和语句:深入理解(附示例)
-
Python 循环:如何在 Python 中迭代的完整指南
-
学习的 5 个 Python 库来开始你的数据科学职业生涯
-
如何学习 Python 数据科学
数据科学:
-
如何处理数据科学中的缺失值
-
如何在数据科学项目中进行特征选择
-
如何在数据科学项目中检测异常值
-
条形图和直方图有什么区别?
-
相关性与回归的区别
-
理解 l1 和 l2 正则化
掌握数据科学工作流程

图片由 Aron Visuals 提供,来源于 Unsplash
通过这六个简单阶段自信地导航你的数据科学项目!
·
关注 发表在 Towards Data Science · 5 分钟阅读 · 2023 年 9 月 16 日
--
介绍
在今天的数据驱动世界中,我们必须在信息的广阔海洋中航行,以提取有价值的见解。为了安全地引导我们穿越这些具有挑战性的水域,我们需要一个可靠的指南针:数据科学工作流程。
数据科学工作流程是什么?
数据科学工作流程是一个结构化的阶段框架,指导数据科学家有效地应对数据科学项目的复杂性。
阶段
**1) 定义
-
收集
-
准备
-
探索
-
分析
-
沟通**
重要性
数据科学工作流程使数据科学家在从数据中提取价值时能够高效而有效地合作。
挑战
数据科学工作流程本质上是迭代的,因此当新的见解出现时,认识到需要重新审视早期阶段至关重要。
替代框架
数据科学工作流程没有一刀切的方法,因此本文提供了一种个性化的看法,借鉴了广泛认可的框架,如 CRISP-DM 和 OSEMN。

图片由 Brett Jordan 提供,来源于 Unsplash
1) 定义
定义阶段涉及明确地概述项目,以确保努力、期望和资源与共同的目标和方向一致。
技巧
背景 收集与项目相关的背景信息(例如原因、目标、问题、期望、影响)
目标 定义期望的结果、可测量的目标和关键问题,然后将任务拆分为不同的、可管理的组件
约束 通过考虑重要因素来确定项目的限制(例如资源可用性、时间限制、数据访问性、伦理考量)

图片由 Fer Troulik 提供,来源于 Unsplash
2) 收集
收集阶段涉及获取必要的数据,以便基于准确的信息进行有意义的分析。
技巧
数据需求 定义完成项目所需的数据(例如格式、变量、时间范围、粒度)
数据来源 寻找可靠且相关的数据来源(例如数据库、API、文件、传感器读数)
认证 确保获得访问数据所需的权限(例如电子邮件/密码、OAuth、API 密钥、robots.txt)
收集 使用适当的方法获取数据(例如 SQL 查询、API 调用、网页抓取、手动数据输入)
数据管理 按照最佳实践处理数据(例如数据质量、数据治理、数据安全)

图片由 Darren Ahmed Arceo 提供,来源于 Unsplash
3) 准备
准备阶段涉及处理原始数据,以实现一致且结构化的格式,适合进行可靠的分析。
技巧
数据清洗 识别和处理数据中的错误和不一致(例如缺失值、重复条目、异常值、数据格式)
数据集成 从多个来源合并数据,同时确保一致性(例如变量、命名约定、索引)
特征工程 从原始数据中提取有意义的特征(例如特征选择、特征创建、数据转换)

4) 探索
探索阶段涉及理解数据的主要特征,以便提出有效的假设、识别问题并完善项目定义。
技术
分布分析 检查每个变量的分布(例如均值、中位数、标准差、偏度、异常值)
依赖分析 调查和量化变量关系,以了解它们如何相互影响(例如相关性、交互作用、协方差、时间序列分析)
数据分割 使用各种分段和子集探索数据,以了解模式在不同组之间的变化
假设生成 生成初步见解以发展关于关系和模式的假设

照片由 Julia Koblitz 提供,来自 Unsplash
5) 分析
分析阶段涉及对数据进行深入检查,以开发一个能够产生有价值见解的稳健解决方案。
技术
假设检验 应用显著性测试以评估观察到的模式和关系的统计重要性(例如 t 检验、方差分析、卡方检验)
高级技术 利用与特定假设相关的高级算法(例如时间序列分析、回归分析、异常检测)
建模 选择、构建和评估适当的模型,并使用相关指标识别最佳配置,同时考虑复杂性、可解释性和性能等权衡因素

照片由 Patrick Fore 提供,来自 Unsplash
6) 交流
交流阶段涉及向利益相关者展示项目及其发现,以创建清晰性和意识。
技术
模型部署 部署模型以供实际使用(例如创建 API、构建 Web 应用程序、集成到现有系统中)
监控与记录 实施模型使用过程中的性能跟踪和问题记录
文档 创建全面的项目文档,涵盖技术细节(例如:模型架构、数据来源、假设、局限性)
报告和展示 生成并提供简洁、信息丰富且引人入胜的项目总结(例如:目标、方法、结果、见解、关键发现)

图片由 Jordan Madrid 提供,来源于 Unsplash
结论
数据科学工作流程是一个重要的工具,因为它为复杂的项目提供了结构和组织,从而改善决策制定、增强协作和提高准确性。
数据科学是一个动态领域,虽然工作流程提供了坚实的基础,但应根据具体项目的需求和目标进行调整。
采用并应用数据科学工作流程将使数据科学家能够优化他们的过程,并在不断变化和不断增长的数据海洋中蓬勃发展。
参考文献
[1] J. Saltz, 什么是数据科学工作流程? (2022), 数据科学过程联盟
[2] P. Guo, 数据科学工作流程:概述与挑战 (2013), ACM 通讯
[3] Springboard, 数据科学过程 (2016), KDNuggets
[4] S. Gupta, 数据科学过程:初学者的简明指南 (2022), Springboard
[5] M. Tabladillo, 团队数据科学过程生命周期 (2022), 微软
[6] D. Cielen, A. Meysman, M. Ali, 介绍数据科学 — 第二章:数据科学过程 (2016), 曼宁出版
[7] Z. Awofeso, 初学者的数据科学项目工作流程结构指南 (2023), Analytics Vidhya
[8] N. Hotz, 什么是 CRISP-DM? (2023), 数据科学过程联盟
[9] J. Brownlee, 如何像数据科学家一样解决问题 (2014), 机器学习精通
掌握未来:评估利用 IaC 技术生成 LLM 数据架构
评估 LLM 在生成基础设施代码以配置、配置和部署现代应用程序方面的适用性
·
关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 10 月 16 日
--
照片由 ZHENYU LUO 提供,出处为 Unsplash
介绍
在这篇文章中,我们探讨了大型语言模型(LLMs)在利用真实应用程序生命周期方面的适用性,包括从基础设施提供到配置管理和部署。由此产生的源代码在 GitHub¹¹ 上公开可用。基础设施即代码(IaC)解决方案通过代码而非手动过程来促进基础设施的管理和提供¹。它们正变得越来越普遍,主要的云服务提供商已实现了自己的 IaC 解决方案,以与其服务互动。在这方面,AWS CloudFormation、Google Cloud Deployment Manager 和 Azure Resource Manager Templates 简化了云服务的提供,消除了 IT 运营手动建立服务器、数据库和网络的需求。然而,这些多种可能性引入了供应商锁定的风险,因为为特定云提供商定义的 IaC 不能移植,如果需要不同的云提供商,则需要进行翻译。在这方面,像 Terraform² 或 Pulumi³ 这样的工具提供了对不同云提供商各种实现的抽象,并促进了可移植部署的开发。这样,供应商锁定的风险显著降低,组织可以动态响应其需求,而无需承担巨大的实施成本。此外,IaC 技术还带来了许多好处⁴:
-
一致性:它允许通过实现可重复的部署来自动化基础设施提供。
-
降低风险:它通过最小化人工干预,促成了一种更少出错的基础设施管理方法。
-
成本优化:它使得识别不必要资源更容易,并能够更快地在云提供商之间迁移,以应对计费变化。
-
改进的协作:脚本可以集成到版本控制工具中,这促进了个人和团队之间的协作。
然而,应用生命周期超出了基础设施提供的范围。下图显示了不同 IaC 技术支持的应用生命周期⁵。

基础设施即代码技术支持的应用生命周期。| 来源:Josu Diaz-de-Arcaya 等⁵
在这种情况下,IaC 技术的目标超出了简单的基础设施资源提供。在建立了必要的基础设施后,配置管理阶段确保所有要求都得到适当安装。这个阶段通常使用工具如 ansible⁶、chef⁷ 或 puppet⁸ 来完成。最后,应用部署阶段负责在各种基础设施设备上进行应用的协调。
了解 LLMs
大型语言模型(LLMs)是指一类人工智能模型,它们旨在根据提供给它们的输入理解和生成类似人类的文本。这些模型以其大量的参数而闻名,这使它们能够捕捉语言中复杂的模式和细微的差异⁹。
-
文本生成:LLMs 生成的文本可以与其周围的内容连贯且相关。它们被用于完成文本、生成材料,甚至进行创意写作活动。
-
语言理解:LLMs 能够理解并从文本中提取信息。它们能够进行态度分析、文本分类和信息检索。
-
翻译:LLMs 可以将文本从一种语言翻译成另一种语言。这对机器翻译应用非常有益。
-
回答问题:LLMs 可以根据给定的上下文回答问题。它们用于聊天机器人和虚拟助手以回答用户的查询。
-
文本摘要:LLMs 可以将长篇文章总结为更短、更连贯的摘要。这对于快速消化信息非常有用。
在上述能力中,我们将专注于文本生成。特别是在根据输入提示生成正确的 IaC 代码方面,大型语言模型(LLMs)在自然语言处理领域取得了显著进展,但也提出了几个挑战和关注点。当时与 LLMs 相关的一些关键问题和关注点包括:
-
偏见和公平性:LLMs 可以学习数据中存在的偏见,这可能导致有偏见或不公平的结果。
-
错误信息和虚假信息:LLMs 可能生成虚假或误导性信息,这可能促成在线错误信息和虚假信息的传播。这些模型有潜力创建看似可信但事实上不正确的内容。
-
安全和隐私:LLMs 可能被误用来生成恶意内容,例如深度伪造文本、假新闻或钓鱼邮件。
下表显示了各种 LLMs 的比较¹⁰。
使用 LLMs 生成 IaC
为了测试当前 LLM 工具在 IaC 领域的表现,设计了一个用例。最终目标是使用 FastAPI 框架在虚拟机中构建一个 API,允许客户端使用 HTTP 方法在 Elasticsearch 集群中执行搜索和删除任务。该集群将由三个节点组成,每个节点都在自己的虚拟机中,并且在另一台机器上将是 Kibana,这是支持可视化的集群管理工具。所有内容都必须在 AWS 云中完成。下图显示了这个架构:

设计用例,旨在测试 LLMs 生成 IaC 的可行性。
挑战是使用 LLM 工具成功完成以下三个任务。在这篇文章中,我们使用了 OpenAI 的 ChatGPT。
-
用于在 AWS 上构建五个虚拟机器的 Terraform 代码。
-
用于通过 API 的 HTTP 方法执行文档搜索和删除操作的 FastAPI 应用的源代码。
-
用于在三个节点上部署和安装 Elasticsearch 集群、在另一个节点上部署 Kibana、在剩余节点上部署 FastAPI 应用的 Ansible 代码。
任务 #1
对于这个挑战,我们使用了以下提示:
我需要通过 Terraform 创建五个虚拟机器,选择你想要的公共云提供商。这些虚拟机器的目的如下:其中三个用于部署 Elasticsearch 集群,每天处理 2G 的数据;另一个用于 Kibana;最后一个用于部署 FastAPI 应用。你应该为每个虚拟机器选择硬件和云提供商。对于你不知道的变量,使用占位符变量。选择便宜的实例。
初始回应是一个不错的尝试,但我们需要不断迭代。例如,我们希望所有的变量都定义在一个单独的文件中,这导致了以下图像。
variables.tf 的代码片段,包含了需要配置的变量。
同样,我们希望知道部署的 IP 地址,并且我们希望这个配置在一个单独的文件中。
output.tf 的代码片段,包含了刚刚配置好的虚拟机器的 IP 地址。
AI 在描述我们想要的实例以及为它们配置所需的安全组方面做得非常出色。
main.tf 的代码片段,包含了需要配置的虚拟机器
它还创建了我们需要的安全组所需的资源,并在定义各种端口时使用了占位符。
main.tf 的代码片段,包含了要使用的安全组。
总的来说,ChatGPT 在完成这个任务时表现得很好。然而,我们花了一段时间来获得一个可行的配置,确保网络配置正确。例如,我们希望连接到每个配置好的虚拟机器,这一点我们以如下方式进行说明。
我希望从我的笔记本电脑上对它们进行 ssh 访问,并且 Kibana 实例需要从我的笔记本电脑上进行 http 和 https 访问。
上述提示生成的代码几乎正确,因为 AI 对 ingress 和 egress 策略感到困惑。然而,这一点很容易发现并修正。
在能够访问虚拟机器后,我们遇到了由于权限不足而无法连接的问题。这导致了更长时间的对话,最终我们发现自己添加这些行更容易。
任务 #2
对于这个挑战,我们使用了以下提示:
我需要创建一个 FastAPI 应用程序。这些 API 的目的是提供存储单个 JSON 文档到 Elasticsearch 集群、存储多个文档以及删除文档的方法。Elasticsearch 集群部署在 3 个节点上,并且具有基本的身份验证,用户名为“tecnalia”,密码为“iac-llm”。
这个提示的结果非常成功。该应用程序使用 Elasticsearch python 包¹²与 Elasticsearch 集群进行交互,完全有效。我们只需要记住要更改部署集群的节点的 IP 地址。在下图中,第一个方法是用于在 Elasticsearch 中插入单个文档。
存储单个文档方法的代码摘录。
然后,第二个方法用于在一次调用中批量插入多个文档。
存储多个文档方法的代码摘录。
最后,最后一个方法可以用于从 Elasticsearch 集群中删除单个文档。
删除文档方法的代码摘录。
我们认为这个实验非常成功,因为它正确地选择了适合执行任务的库。然而,仍需进一步手动调整以将这段代码变成生产就绪的软件。
任务 #3
对于这个挑战,我们使用了以下提示:
生成 Ansible 代码以在三个节点上安装 Elasticsearch 集群。请同时添加一个连接到集群的 Kibana 节点。
这个提示在生成所需的 Ansible 脚本方面表现尚可。它在将源代码组织到各种文件中方面做得非常出色。首先是包含所有节点详细信息的清单。请记住,此文件需要根据任务 #1 生成的正确 IP 地址进行调整。
inventory.ini 的代码摘录
然后,安装 Elasticsearch 的主要 Ansible 脚本在以下图中显示。这是它的一个摘录,完整示例可以在仓库¹¹中找到。
elasticsearch_playbook.yml 的代码摘录
另一方面,每个 Elasticsearch 节点所需的配置已经方便地生成了 Jinja 文件。在这种情况下,我们不得不手动添加 path.logs 和 path.data 配置,因为 Elasticsearch 无法启动,原因是权限问题。
elasticsearch.yml.j2 的代码摘录。
类似地,ChatGPT 能够为 Kibana 实例生成类似的配置。然而,在这种情况下,我们手动将配置分离到一个单独的文件中以方便管理。以下图片展示了该文件的一个摘录。
kibana_playbook.yml 的代码摘录。
类似地,以下 jinja 文件涉及 Kibana 实例,虽然 IP 地址最好进行参数化,但似乎也不错。
kibana.yml.j2 的代码摘录
总体来说,我们发现 ChatGPT 在生成项目框架方面非常出色。然而,将框架转化为生产级应用程序仍需进行大量操作。在这方面,需要对所使用的技术具有深入的专业知识来调整项目。
结论
本文讨论了使用大语言模型(LLMs)来监督应用程序生命周期的情况。以下将讨论这一工作的优缺点。
优点
-
使用大语言模型(LLMs)支持应用生命周期的各个阶段在启动项目时特别有利,尤其是在知名技术方面。
-
初始框架结构良好,提供了否则不会使用的结构和方法。
缺点
-
大语言模型(LLMs)面临使用 AI 解决方案时的偏见风险;在这种情况下,ChatGPT 选择了 AWS 而非类似选项。
-
将项目打磨至生产就绪可能会很麻烦,有时手动调整代码更为方便,这需要对所使用的技术有深入了解。
致谢
本工作由巴斯克政府资助的 SIIRSE Elkartek 项目(面向工业 5.0 的鲁棒、安全和伦理的智能工业系统:规范、设计、评估和监控的先进范式)(ELKARTEK 2022 KK-2022/00007)提供资金支持。
作者贡献
概念化、分析、调查和写作是 Juan Lopez de Armentia、Ana Torre 和 Gorka Zárate 的共同努力。
参考文献
-
什么是基础设施即代码(IaC)?(2022)。
www.redhat.com/en/topics/automation/what-is-infrastructure-as-code-iac -
Terraform by HashiCorp。(无日期)。检索于 2023 年 10 月 5 日,来自
www.terraform.io -
Pulumi — 通用基础设施即代码。(无日期)。检索于 2023 年 10 月 5 日,来自
www.pulumi.com/ -
基础设施即代码的七大主要好处 — DevOps。(无日期)。检索于 2023 年 10 月 5 日,来自
duplocloud.com/blog/infrastructure-as-code-benefits/ -
Diaz-De-Arcaya, J., Lobo, J. L., Alonso, J., Almeida, A., Osaba, E., Benguria, G., Etxaniz, I., & Torre-Bastida, A. I.(2023)。IEM: 用于多语言 IaC 部署的统一生命周期编排器 ACM 引用格式。
doi.org/10.1145/3578245.3584938 -
Ansible 是简单的 IT 自动化。(无日期)。检索于 2023 年 10 月 5 日,来自
www.ansible.com/ -
Chef 软件 DevOps 自动化解决方案 | Chef。(无日期)。检索于 2023 年 10 月 5 日,来自
www.chef.io/ -
Puppet Infrastructure & IT Automation at Scale | Puppet by Perforce。(无日期)。检索于 2023 年 10 月 5 日,来自
www.puppet.com/ -
Kerner, S. M. (无日期)。什么是大语言模型? | TechTarget 定义。检索于 2023 年 10 月 5 日,来自
www.techtarget.com/whatis/definition/large-language-model-LLM -
Sha, A. (2023)。2023 年 12 大最佳大语言模型(LLMs) | Beebom。
beebom.com/best-large-language-models-llms/ -
Diaz-de-Arcaya, J.,Lopez de Armentia, J.,& Zarate, G. (无日期)。iac-llms GitHub。检索于 2023 年 10 月 5 日,来自
github.com/josu-arcaya/iac-llms -
Elastic Client Library 维护者。(2023)。elasticsearch · PyPI。
pypi.org/project/elasticsearch/
掌握未知领域与 GPT-4 和翻转互动模式
利用 GPT-4 生成高质量问题,重新定义问题解决和决策制定
·发表于 Towards Data Science ·19 min 阅读·2023 年 7 月 13 日
--

由 Ali Kazal 提供的照片,来源于 Unsplash
介绍
像我一样,你可能也被生成 AI 领域最近取得的进展所惊艳。这感觉就像我们生活在科幻现实中,机器理解了我们。
在这场革命的核心,OpenAI 的 GPT-4 已经成为语言模型领域的一项奇迹。作为曾有幸尝试过它的人,我可以证明它令人惊叹的能力。
在这篇文章中,我想分享一种让我非常感兴趣的技术。在文献中,这种技术被称为 翻转互动模式,我发现它是一个非常强大的框架,可以帮助解决问题。这个提示工程方法的独特之处在于其“倒置”的(即翻转的)方法:它并不直接要求 AI 提供答案或解决方案,而是主要集中在让 AI 提出能够有效引导我们找到期望解决方案的正确问题。
在本文中,我将带你了解它的工作原理,解释它的好处,并提供其有效性的真实案例。希望到最后,你能掌握一个有用的技巧——一个可以应用于各种问题的技巧,结果可能会令你惊讶。
为什么选择 翻转互动模式?
在今天的世界中,我们的知识深度常常可以比作一个池塘——长而窄。我们深入某些专业领域,成为某些特定领域的专家。这种超专业化使我们在各自领域中表现出色,但同时,也可能让我们在遇到自己不擅长的问题时感到迷茫。作为一名软件工程师,我可能对算法、数据结构和编码非常熟悉,但当我手握铁锹,被要求照料花园时,我将把任何绿地变成荒地。
这时,达宁-克鲁格效应可能会发挥作用。根据这一心理假设,存在一种认知偏差,即能力较低的人高估了自己执行任务的能力。本质上,我们不知道我们不知道什么,这可能导致过度自信和错误。
然而,问题不仅仅在于我们在一个陌生领域里是业余的。问题在于我们甚至无法提出正确的问题来引导我们找到解决方案。园艺时需要考虑哪些关键因素?我应该注意什么?我应该从哪里开始?这些是我们在跨出专业领域时可能不知道要问的问题。
这就是翻转互动模式的这个版本发挥作用的地方。这种方法承认解决方案的有效性取决于之前问题的质量。我们不会让 GPT 提供问题的解决方案,而是使用 GPT-4 帮助识别需要提出的问题以找到解决方案。
翻转互动模式的实际应用
让我们将这一技术应用到园艺上。挑战是什么?给我那片荒废的前院带来新生。
要应用这一模式,我们从精心设计的提示开始。这个提示为我们与 GPT-4 的对话奠定了基础。以下是基本模板:
Act as a **{{Relevant Professional}}**.
I want to **{{Problem to Solve}}**
You will ask me 10 questions, one at the time.
After each question you will wait for my answer, after my answer you
will ask the next question. When all the 10 questions
have been answered, you will write me the exact steps
I have to do to solve my problem
在我们具体的案例中,我们将使用以下初始提示:
Act as a **professional gardener**. I want to **renew a front garden that
is in a pretty bad shape, but I don't know where to start**.
You will ask me 10 questions, one at the time. After each question you
will wait for my answer, after my answer you will ask the next question.
When all the 10 questions have been answered, you will write me the exact
steps I have to do to solve my problem.
让我们分析不同的元素。
-
角色分配: 我们提示的第一部分设定了 GPT-4 将采用的角色来帮助我们解决问题——在这种情况下,是专业园艺师。为 GPT-4 分配角色是一种被广泛认可的策略,它在使 GPT 输出更相关和准确的回答方面证明了其有效性。
-
问题陈述: 我们提示的下一部分明确了我们的问题。理想情况下,我们希望包括尽可能多的细节;因为提供的上下文越多,模型可以更好地调整其问题。
-
顺序提问: 最后,我们的提示强调 GPT-4 的提问应遵循顺序格式。这一技术元素至关重要,因为它确保了 AI 的后续问题基于之前的回答。
让我们看看对话如何展开。
GAIrdening
你可以在这个 链接 找到完整的对话记录。为了参考,我也在下面写出了对话内容。
**Question 1**
*GPT-4*: What is the size of your garden area?
*Andrea*: It's around 15 squares meters. There is an area that has a weed lawn, and another area that has tiles. i only want to renew the weedy lawn, which is 15 squares meters. In that area I also have a cherry tree
**Question 2**
*GPT-4*: Could you describe the condition of the soil? Is it sandy, loamy, clayey? And do you know the pH level of your soil?
*Andrea*: It's loamy. I have no idea about the ph
**Question 3**
*GPT-4*: Do you have a preference for a certain type of plants or a specific look you want for your garden? For instance, do you want flowering plants, ornamental grasses, shrubs, perennials, or annuals?
*Andrea*: In the side I want to plant a periennals, the most part would be grass. I'd like to have some plants around the cherry tree, and a hedge on the front of the garden (the part that sees the street)
**Question 4**
*GPT-4*: What is the sunlight situation in your garden? Does it receive full sunlight, partial sun, or is it mostly in the shade?
*Andrea*: Full sun, besides the area under the tree that is mostly in shade
**Question 5**
*GPT-4*: For the hedge, do you have any particular preferences for height, thickness, and color (green, variegated, etc.)?
*Andrea*: Green High plants
**Question 6**
*GPT-4*: Could you tell me a bit more about your local climate? For instance, where are you located and what kind of temperatures, rainfall, and seasons do you experience?
*Andrea*: I live in Ireland, it rains quite a lot, but there is some level of sun almost every day.
**Question 7**
*GPT-4*: Do you have a preference in terms of maintenance? Would you prefer a low-maintenance garden or are you comfortable with investing a bit more time in upkeep?
*Andrea*: I'm comfortable in investing some time
**Question 8**
*GPT-4*: What is the main purpose of this garden? Is it mainly for aesthetics, to create a private space, to attract wildlife, or maybe for growing fruits and vegetables?
*Andrea*: Aesthetic, but I also want to support pollinators
**Question 9**
*GPT-4*: What's your budget for this project? This will help determine what type of plants, grass, and amendments you can afford.
*Andrea*: 500 euro
**Question 10**
*GPT-4*: Lastly, do you have any known allergies to specific plants, trees, or flowers that we should avoid including in your garden?
*Andrea*: No
很明显,ChatGPT 询问了重要的细节,例如花园的大小、土壤状况、光照情况、偏好的植物类型、当地气候、我对维护的准备情况(笑)、花园的主要用途、预算以及潜在的过敏反应。
回顾起来,在走过 AI 的问题之后,我很感激这些提问促使我考虑到花园翻新中可能被忽视的方面。
两个例子:
-
ChatGPT 关于阳光的询问:"你的花园阳光情况如何?是全天候阳光、部分阳光,还是大部分时间在阴影中?" 这个问题突显了理解花园光照条件的重要性,以便选择合适的植物。说实话,这是一个关键细节,如果我单独处理这个任务,很可能会忽视。
-
GPT-4 询问:“你是否对 任何已知的过敏源 (如特定植物、树木或花卉)有过敏反应,我们应该避免在你的花园中包括这些吗?”,提醒了我一个如果没有指导可能会轻易遗漏的方面。
总之,GPT-4 实施的逐步提问策略不仅加深了我对当前问题的理解,还使得 AI 能够生成更详细和个性化的解决方案。
这是 GPT 给我的关于花园翻新的最终建议:
Step 1: Clear the Area and Check the Soil
Start by removing the weeds and any other debris from the area.
To check the pH of your soil, you can purchase an inexpensive soil
testing kit from your local garden center or online.
Loamy soil is typically around neutral but knowing the exact pH will help you
choose the best plants.
Step 2: Improve the Soil
Based on your soil test results, you may need to add amendments to your soil
to improve its fertility. For loamy soil, adding compost or well-rotted
manure can improve its nutrient content.
Step 3: Plan Your Plantings
You mentioned you wanted a hedge, grass, perennials, and plants around
the cherry tree. Here are some suggestions:
Hedge: Yew (Taxus baccata) can make a good, tall green hedge.
It's evergreen and can handle Ireland's wet climate.
Grass: Ryegrass is durable and can handle full sun and frequent rain.
Perennials for sunny areas: Consider Lavender, Sedum, and Echinacea.
These will attract pollinators and can handle full sun.
Plants for the shaded area under the cherry tree: Ferns and hostas
are shade-loving and will add some texture and visual interest.
Step 4: Purchase and Plant
Purchase your chosen plants, keeping your budget in mind. Start planting with
the hedge, as it will be your primary structural element. After that,
plant the grass, then your perennials, and finally, the shade-loving plants
around the cherry tree.
Step 5: Water and Care
After planting, water everything well. Remember that new plants will need
more frequent watering until they establish. After they are established,
adjust watering based on the weather and the needs of each type of plant.
In general, the grass will likely need watering once a week, the hedge
every 1-2 weeks, and the perennials every 1-2 weeks, but adjust as needed.
The plants under the cherry tree will likely need less water due to the
shade and potential rain protection from the tree.
Step 6: Ongoing Maintenance
Mow the grass as needed, trim the hedge to maintain its shape and height,
and prune the perennials after flowering to encourage new growth.
Remove any fallen leaves or debris regularly to keep the garden tidy.
Remember, gardening is a process, and it's okay if everything doesn't
look perfect right away. Over time, as the plants grow and fill in,
you'll see your garden transform into a beautiful, pollinator-friendly
space. Enjoy the process and the result!
自动化反转互动模式
我已经多次测试了这种技术,所以我很有信心它在大多数情况下会有效,尤其是使用 GPT-4 作为 LLM(而不是 GPT-3)。接下来我们转向一种更具实验性的方式:是否可以自动化这种方法? 为了回答这个问题,我们需要一些编码(终于)。
我们将创建两个 AI 代理进行对话,一个提问,即 inquiry_persona,另一个回答,即 response_persona。我们将用一些指令来初始化我们的代理,使他们共享一个共同的目标。最后,我们将让这两个 AI 对话,直到 inquiry_persona 提供最终回应。值得注意的是,这些代理必须保持对话历史,记住他们自己的回答以及对方的回答。系统的组织结构如下图所示。

采用 Google Drawings 手工制作 ❤
实施
首先让我们看看如何初始化这两个代理:
inquiry_persona 代理的初始提示与我们之前看到的非常相似。由于我们将用于此次实验的模型是 GPT-3.5,因此有一些小差异,我不得不付出额外的努力使其正常工作。
response_persona代理被初始化为相同的TASK,并意识到其他代理是谁。
下一步是用我们需要的信息填充这些变量,以解决我们的难题。
考虑一个场景,其中一个软件工程师正在寻求加薪,并需要制定请求薪资增加的策略。此人可能会设置如下提示:
现在提示已经准备好,我们只需调用 API 并处理对话。以下是调用 OpenAI API 的函数;该函数只需返回回应,我们将手动处理此回应。
最后,我们可以编写处理对话的代码段。
如你所见,我们使用了两个对话。这样我们可以分别初始化两个代理(即设置不同的角色以定义“谁”是代理)。对话以我们的起始消息初始化,这些消息存储在main_conversation和side_conversation列表中。这些列表允许代理记住对话内容。
然后,代码使用complete()函数生成每个 AI 角色的初步回应:该函数将获取完整的对话历史,并输出“下一个回应”。请注意,对于 Chat 模型(如 GPT-3.5),可以使用ChatML标记语言自定义消息/角色(这就是通过设置role和content在dicts中发生的情况)。
循环是对话的核心。在每次迭代中,两位代理将看到迄今为止的整个对话历史。
让我们执行代码,看看对话如何展开。
**AI:** 1\. Can you provide specific examples of
how you have maintained a good performance over the past 5 years?
**Andrea AI:** Certainly! Over the past 5 years, I have consistently delivered high-quality software solutions on time and within budget. I have received positive feedback from clients and colleagues on my ability to understand and meet their requirements effectively. Additionally, I have actively participated in team collaborations, contributing valuable ideas and solutions to complex problems. My performance evaluations have consistently been positive, highlighting my strong work ethic, attention to detail, and ability to adapt to changing project requirements.
**AI:** 2\. Have you taken on any additional responsibilities
or projects beyond your initial job description during these 5 years?
**Andrea AI:** Yes, I have taken on additional responsibilities and projects beyond my initial job description during these 5 years. I have actively sought out opportunities to expand my skill set and contribute to the company's success. For example, I have volunteered to mentor junior engineers, providing guidance and support in their professional development. I have also taken the initiative to lead cross-functional teams on critical projects, demonstrating my ability to handle increased responsibilities and manage complex tasks. These additional responsibilities have allowed me to contribute to the company's growth and success while further developing my own expertise.
**AI:** 3\. How has your performance directly impacted the company's success or bottom line?
**Andrea AI:** My performance has directly impacted the company's success and bottom line in several ways. First, by consistently delivering high-quality software solutions, I have helped to enhance the company's reputation and client satisfaction. This has resulted in repeat business from existing clients and positive referrals, contributing to revenue growth. Additionally, my ability to meet project deadlines and stay within budget has helped to minimize project overruns and maximize profitability. I have also proactively identified and implemented process improvements, resulting in increased efficiency and cost savings for the company. Overall, my performance has played a significant role in driving the company's success and financial performance.
**AI:** 4\. Can you provide any evidence or metrics to support your claim of maintaining a good performance?
**Andrea AI:** Certainly! Here are some evidence and metrics to support my claim of maintaining a good performance:
1\. Performance evaluations: I have consistently received positive performance evaluations from my managers over the past 5 years, highlighting my strong work ethic, technical expertise, and ability to meet project goals.
2\. Client feedback: I have received positive feedback from clients on multiple projects, commending my professionalism, attention to detail, and ability to deliver high-quality software solutions that meet their needs.
3\. Project success rate: I have successfully delivered 95% of the projects assigned to me on time and within budget, demonstrating my ability to effectively manage and execute complex software development tasks.
4\. Team collaboration: I have actively participated in cross-functional team collaborations, contributing valuable ideas and solutions to complex problems. This has resulted in successful project outcomes and positive feedback from team members.
5\. Training and certifications: I have consistently pursued professional development opportunities, obtaining relevant certifications and attending training sessions to enhance my skills and stay updated with the latest industry trends.
These evidence and metrics highlight my consistent performance and dedication to delivering high-quality work, which has contributed to the success of both individual projects and the overall company.
**AI:** 5\. Have you researched the current market
value for your role and level of experience in your industry?
**Andrea AI:** Yes, I have researched the current market value for my role and level of experience in the industry. I have conducted thorough research and analysis to understand the prevailing salary ranges and compensation packages for software engineers with similar experience, skills, and responsibilities. I have taken into account factors such as industry standards, geographical location, and company size to ensure that my expectations are aligned with the market value. Based on my research, I believe that my skills and experience warrant a fair and competitive compensation package.
**AI:** 6\. Have you prepared a list of specific
reasons why you believe you deserve a raise based on your
performance and contributions to the company?
**Andrea AI:** Yes, I have prepared a list of specific reasons why I believe I deserve a raise based on my performance and contributions to the company. Here are some key points:
1\. Consistently high-quality work: Over the past 5 years, I have consistently delivered high-quality software solutions, meeting or exceeding client expectations. This has contributed to the company's reputation and client satisfaction.
2\. Meeting project goals: I have a track record of successfully meeting project deadlines and staying within budget. This has helped to minimize project overruns and maximize profitability for the company.
3\. Additional responsibilities: I have voluntarily taken on additional responsibilities beyond my initial job description, such as mentoring junior engineers and leading cross-functional teams. These contributions have added value to the company and its employees.
4\. Process improvements: I have proactively identified and implemented process improvements, resulting in increased efficiency and cost savings. These improvements have positively impacted the company's bottom line.
5\. Positive feedback: I have received positive feedback from clients and colleagues on my professionalism, attention to detail, and ability to deliver results. This feedback demonstrates the value I bring to the company.
6\. Professional development: I have consistently pursued professional development opportunities, obtaining relevant certifications and staying updated with industry trends. This investment in my skills and knowledge directly benefits the company.
Based on these reasons, I believe that my performance and contributions warrant a raise in recognition of my value to the company.
**AI:** 7\. Have you scheduled a meeting with your manager
to discuss your request for a raise?
**Andrea AI:** No, I have not scheduled a meeting with my manager yet to discuss my request for a raise. However, I am planning to request a meeting in the near future to have a conversation about my performance, contributions, and the possibility of a raise. I believe it is important to have a formal discussion with my manager to present my case and provide them with the opportunity to consider my request.
**AI:** 8\. Have you prepared a well-structured and persuasive
argument to present during the meeting with your manager?
**Andrea AI:** Yes, I have prepared a well-structured and persuasive argument to present during the meeting with my manager. My argument highlights my consistent performance, contributions to the company, and the market value for my role and level of experience. Here is an outline of my argument:
1\. Opening: Express gratitude for the opportunity to discuss my performance and contributions with my manager.
2\. Recap of performance: Provide a brief summary of my performance over the past 5 years, emphasizing my consistent delivery of high-quality work, meeting project goals, and positive feedback received from clients and colleagues.
3\. Additional responsibilities: Discuss the extra responsibilities I have taken on voluntarily, such as mentoring junior engineers and leading cross-functional teams, and how they have added value to the company.
4\. Process improvements: Highlight the process improvements I have identified and implemented, resulting in increased efficiency and cost savings for the company.
5\. Market value research: Share the research I have conducted on the current market value for my role and level of experience, demonstrating that my expectations are aligned with industry standards.
6\. Financial impact: Discuss how my performance and contributions have directly impacted the company's success and bottom line, providing specific examples of projects, client satisfaction, and cost savings.
7\. Professional development: Highlight my commitment to continuous learning and professional development, emphasizing how it benefits both myself and the company.
8\. Request for a raise: Clearly and confidently state my request for a raise, based on my performance, contributions, and market value.
9\. Closing: Express my commitment to the company and reiterate that my intention is not to leave, but rather to be fairly compensated for my value and contributions.
By following this well-structured argument, I believe I can effectively present my case and persuade my manager to consider my request for a raise.
**AI:** 9\. Are you prepared to negotiate and consider
alternative forms of compensation if a salary increase is not
immediately possible?
**Andrea AI:** Yes, I am prepared to negotiate and consider alternative forms of compensation if a salary increase is not immediately possible. While a salary increase would be ideal, I understand that there may be constraints or limitations within the company's budget. In such a case, I am open to discussing alternative forms of compensation that may still provide value and meet my financial needs. This could include options such as performance-based bonuses, stock options, additional paid time off, professional development opportunities, or other benefits that align with my career growth and financial goals. I believe in maintaining open and constructive communication during the negotiation process to find a mutually beneficial solution.
**AI:** 10\. Have you considered the potential outcomes and next
steps if your request for a raise is not approved?
**Andrea AI:** Yes, I have considered the potential outcomes and next steps if my request for a raise is not approved. While I am hopeful that my manager will recognize my contributions and grant the raise, I understand that it may not always be possible due to various factors. In such a scenario, I am committed to maintaining a positive attitude and continuing to perform at a high level in my current role.
If my request for a raise is not approved, I would seek feedback from my manager on areas of improvement and opportunities for growth within the company. I would also explore other avenues for professional development, such as attending training sessions or pursuing certifications, to enhance my skill set and increase my value to the company.
Additionally, I would consider discussing the possibility of a performance-based plan or timeline with my manager, where I can work towards specific goals or milestones that would make me eligible for a raise in the future. This would allow me to have a clear path for advancement and provide an opportunity to revisit the topic of a raise at a later date.
Ultimately, my goal is to continue contributing to the success of the company and growing in my career. If a raise is not immediately possible, I am committed to exploring alternative paths for growth and advancement within the company while keeping an open mind about potential opportunities outside the organization as well.
这是 AI 的最终建议。
**AI**: Based on your responses, here are the exact steps you can
take to address your situation:
1\. Schedule a meeting with your manager: Request a
meeting with your manager to discuss your performance,
contributions, and the possibility of a raise.
2\. Present your case: During the meeting, present a
well-structured argument that highlights your consistent performance,
additional responsibilities, process improvements,
market value research, financial impact, and commitment
to professional development. Clearly state your request for a
raise based on these factors.
3\. Be open to negotiation: Be prepared to negotiate and
consider alternative forms of compensation if a salary increase
is not immediately possible. Discuss options such as
performance-based bonuses, stock options, additional paid time off,
or professional development opportunities.
4\. Seek feedback and growth opportunities: If your request for
a raise is not approved, seek feedback from your manager on areas
of improvement and opportunities for growth within the company.
Explore avenues for professional development to enhance your skills
and increase your value to the company.
5\. Discuss a performance-based plan: Consider discussing the
possibility of a performance-based plan or timeline with your manager,
where you can work towards specific goals or milestones that would make
you eligible for a raise in the future. This provides a clear path
for advancement and an opportunity to revisit the topic of a
raise at a later date.
6\. Maintain a positive attitude and performance: Regardless
of the outcome, maintain a positive attitude and continue to
perform at a high level in your current role. Stay committed to
contributing to the success of the company and explore growth opportunities
within or outside the organization.
Remember, it's important to approach the meeting with professionalism, confidence, and a focus on the value you bring to the company. Good luck with your meeting and your pursuit of a raise!
当然,我的初步提示可能过于笼统,无法生成针对我们具体案例的最终回应,但请考虑以下学习点:
-
你知道应该关注什么,如果你面对那个特定问题(研究市场以查看同行的薪酬,收集你对公司的贡献信息)。
-
你有一个关于软件工程师应该如何回答这些问题的想法。
-
你实际上有一个最终建议,虽然很通用,但是一个很好的起点。
最后,请记住,在这个阶段你可以手动跟进,以帮助 GPT 纠正/改进其最终回应。也许是我,所有这些看起来都像魔法。
结论
翻转互动模式利用 GPT 帮助集中于问题而非答案。这种方法使你能够更深入地理解当前问题,并为 AI 提供一个框架,以提供个性化和准确的回应;基本上,你只需要能够提出你的问题!
总的来说,我发现这种方法非常有用,所以我鼓励你尝试一下,但请注意:将自动化部分仅视为有趣的练习,避免将可能的与经理的对话交给它!
如果你尝试了这个方法,或者有改进的想法,请随时联系我!
一名数据科学家在需求高/供应低的市场中寻找物业的探险
## 数据科学的好处:我如何在都柏林找到新家 ## 如何用旅行推销员问题拯救圣诞节
圣诞老人最佳旅行路线的一个很好的可视化
附录
如果你想直接复制粘贴并尝试的话,下面是完整代码!
使用 Python 类掌握时间序列分析
原文:
towardsdatascience.com/mastering-time-series-analysis-with-python-classes-1a4215e433f8
面向对象编程在时间序列分析中的应用
·发表于Towards Data Science ·阅读时间 8 分钟·2023 年 1 月 10 日
--

图片由Tima Miroshnichenko提供,发布于Pexels
时间序列分析是最常见的数据科学任务之一。它涉及分析按时间顺序排列的数据点的趋势。时间序列数据种类繁多,包括股票市场数据、天气数据、消费者需求数据等。时间序列分析在各个行业中都有应用,这使得它成为数据科学家和数据分析师的必备技能。
时间序列分析涉及许多技术,无法在一篇文章中总结。一些最常见的方法包括通过折线图可视化时间序列数据、构建时间序列预测模型、进行光谱分析以揭示周期性趋势、分析季节性趋势等。
由于时间序列分析涉及多种不同的技术,它自然适合面向对象编程。Python 类使得组织相关时间序列任务的属性和方法变得简单。例如,如果作为数据科学家,你经常进行折线图可视化、季节性分析和时间序列预测,类可以让你轻松组织这些任务的方法和属性。
当面向对象编程做得好的时候,可以提高时间序列实验的可读性、可重用性、可维护性和重复性。由于类是方法和属性的集合,它清楚地指出了哪些函数用于特定目的。这使得修改和维护现有函数变得简单。此外,一旦在你的类中定义了可靠的方法集合,你可以轻松地使用不同的参数、更新的训练数据等重新运行实验,而无需重新编写代码。
在这里,我们将演示如何编写一个类,以组织时间序列分析工作流中的步骤。工作流的每个部分将由一个类方法定义,该方法完成单个任务。我们将学习如何为时间序列可视化、统计测试、数据拆分用于训练和测试、训练预测模型和验证时间序列模型定义类方法。
在这个工作中,我将使用Deepnote编写代码,它是一个协作数据科学笔记本,使得运行可重复的实验变得非常容易。
对于我们的建模工作,我们将使用虚构的Weather Climate Time Series data set,该数据集公开可在 Kaggle 上获取。该数据集可以免费使用、修改和共享,遵循创作共用公共领域许可证 (CC0 1.0)。
读取数据
首先,让我们导航到 Deepnote 并创建一个新项目(如果你还没有账户,可以免费注册)。
我们来创建一个名为‘time_series’的项目,并在该项目中创建一个名为‘time_series_oop’的笔记本。同时,将 DailyDelhiClimate.csv 文件拖放到页面左侧面板上“FILES”部分:

作者拍摄的截图
我们从导入 Pandas 库开始:
import pandas as pd
接下来,我们将定义一个类来读取我们的天气数据。我们将把我们的 Python 类命名为 TimeSeriesAnalysis:
class TimeSeriesAnalysis:
def __init__(self, data):
self.df = pd.read_csv(data)
我们可以定义一个类的实例,并通过我们的 TimeSeriesAnalysis 对象访问我们的数据框。让我们显示数据的前五行:
作者创建的嵌入
我们看到我们有一个日期对象和四个浮点列。我们有平均温度、湿度、风速和平均气压。让我们为我们的类添加一个准备时间序列的方法。该方法将接受一个数值列名,并返回一个时间序列,其中数据作为索引,值对应于选定的列:
class TimeSeriesAnalysis:
...
def get_time_series(self, target):
self.df['date'] = pd.to_datetime(self.df['date'])
self.df.set_index('date', inplace=True)
self.ts_df = self.df[target]
现在我们可以定义我们类的一个新实例,并访问我们的时间序列数据:
作者创建的嵌入
生成摘要统计信息
接下来,我们可以添加一个生成时间序列基本统计信息的方法。例如,我们可以定义一个类方法,返回指定列的均值和标准差:
class TimeSeriesAnalysis:
...
def get_summary_stats(self):
print(f"Mean {self.target}: ", self.ts_df.mean())
print(f"Standard Dev. {self.target}: ", self.ts_df.std())
然后,我们可以定义一个新的类实例,在我们的对象实例上调用 get_time_series 方法,并将平均温度列作为输入,生成摘要统计数据:
嵌入由作者创建
我们可以对湿度、风速和平均压力做同样的处理
嵌入由作者创建
可视化时间序列
下一步我们可以定义一个方法来生成一些可视化。对于我们的可视化,我们需要导入 Seaborn、Matplotlib 和 statsmodels 包:
import seaborn as sns
import matplotlib.pyplot as plt
import statsmodels.api as sm
让我们为平均温度定义一个新的类实例,创建一个折线图、直方图,并进行季节性分解:
class TimeSeriesAnalysis:
...
def visualize(self, line_plot, histogram, decompose):
sns.set()
if line_plot:
plt.plot(self.ts_df)
plt.title(f"Daily {self.target}")
plt.xticks(rotation=45)
plt.show()
if histogram:
self.ts_df.hist(bins=100)
plt.title(f"Histogram of {self.target}")
plt.show()
if decompose:
decomposition = sm.tsa.seasonal_decompose(self.ts_df, model='additive', period =180)
fig = decomposition.plot()
plt.show()
让我们让我们的方法提供生成目标值的直方图、时间序列折线图和时间序列分解的选项:
嵌入由作者创建
平稳性检验
Dickey-Fuller 调整器
我们可以添加的另一个方法是使用 Dickey-Fuller 检验进行平稳性测试。平稳性是指时间序列的均值和方差随时间不会改变。此外,如果时间序列是平稳的,它就没有任何趋势。通过检查我们的图表,我们可以看到天气数据是非平稳的,因为存在明显的季节性趋势。我们将使用 Dickey-Fuller 检验来检查平稳性。Dickey-Fuller 检验具有以下假设:
原假设:时间序列是非平稳的。
备择假设:时间序列是平稳的。
我们以以下方式解释结果:
如果检验统计量 < 临界值,我们拒绝原假设
如果检验统计量 > 临界值,我们未能拒绝原假设
检验结果将具有 1%、5% 和 10% 显著性水平的临界值。结果还将包括检验统计量。让我们定义一个方法来运行这个测试:
class TimeSeriesAnalysis:
...
def stationarity_test(self):
adft = adfuller(self.ts_df,autolag="AIC")
output_df = pd.DataFrame({"Values":[adft[0],adft[1],adft[2],adft[3], adft[4]['1%'], adft[4]['5%'], adft[4]['10%']] , "Metric":["Test Statistics","p-value","No. of lags used","Number of observations used",
"critical value (1%)", "critical value (5%)", "critical value (10%)"]})
self.adf_results = output_df
在我们的笔记本的顶部,让我们从 statsmodels 包中导入 adfuller 方法:
from statsmodels.tsa.stattools import adfuller
然后,我们可以在我们的气候对象上调用 stationarity_test 方法并访问测试结果:
嵌入由作者创建
我们看到在每种情况下,临界值都小于检验统计量,这是可以预期的。这意味着我们的数据是非平稳的。
Kwiatkowski — Phillips — Schmidt — Shin (KPSS)
原假设:时间序列是平稳的。
备择假设:时间序列是非平稳的。
类似于 ADF 测试,我们以以下方式解释结果:
如果检验统计量 < 临界值,我们拒绝原假设
如果检验统计量 > 临界值,我们未能拒绝原假设
另一个平稳性测试是 Kwiatkowski — Phillips — Schmidt — Shin 测试 (KPSS)。该测试的原假设是时间序列是平稳的。ADF 和 KPSS 之间的区别在于 ADF 测试平稳性,而 KPSS 测试非平稳性。如果 ADF 测试结果未能拒绝原假设,则应使用 KPSS 来确认时间序列是非平稳的。我们扩展我们的平稳性测试方法,使其同时提供 ADF 和 KPSS 的选项。让我们从 sstats 模块导入 KPSS 方法:
from statsmodels.tsa.stattools import adfuller, kpss
然后我们可以扩展我们平稳性测试方法的定义,以包括 KPSS:
class TimeSeriesAnalysis:
...
def stationarity_test(self):
adft = adfuller(self.ts_df,autolag="AIC")
kpsst = kpss(self.ts_df,autolag="AIC")
adf_results = pd.DataFrame({"Values":[adft[0],adft[1],adft[2],adft[3], adft[4]['1%'], adft[4]['5%'], adft[4]['10%']] , "Metric":["Test Statistics","p-value","No. of lags used","Number of observations used",
"critical value (1%)", "critical value (5%)", "critical value (10%)"]})
kpss_results = pd.DataFrame({"Values":[kpsst[0],kpsst[1],kpsst[2], kpsst[3]['1%'], kpsst[3]['5%'], kpsst[3]['10%']] , "Metric":["Test Statistics","p-value","No. of lags used",
"critical value (1%)", "critical value (5%)", "critical value (10%)"]})
self.adf_results = adf_results
self.kpss_resulta = kpss_results
然后我们可以在我们的气候对象上调用 stationarity_test 方法,并访问 KPSS 测试结果:
作者创建的嵌入
我们看到测试统计量小于临界值,这意味着我们拒绝原假设。原假设声明时间序列是平稳的。这意味着我们有证据支持时间序列是非平稳的。
我们可以定义一个执行 ADF 和 KPSS 测试并打印结果的方法:
class TimeSeriesAnalysis:
...
def stationarity_test(self):
adft = adfuller(self.ts_df,autolag="AIC")
kpsst = kpss(self.ts_df)
adf_results = pd.DataFrame({"Values":[adft[0],adft[1],adft[2],adft[3], adft[4]['1%'], adft[4]['5%'], adft[4]['10%']] , "Metric":["Test Statistics","p-value","No. of lags used","Number of observations used",
"critical value (1%)", "critical value (5%)", "critical value (10%)"]})
kpss_results = pd.DataFrame({"Values":[kpsst[0],kpsst[1],kpsst[2], kpsst[3]['1%'], kpsst[3]['5%'], kpsst[3]['10%']] , "Metric":["Test Statistics","p-value","No. of lags used",
"critical value (1%)", "critical value (5%)", "critical value (10%)"]})
self.adf_results = adf_results
self.kpss_results = kpss_results
print(self.adf_results)
print(self.kpss_results)
self.adf_status = adf_results['Values'].iloc[1] > adf_results['Values'].iloc[4]
self.kpss_status = kpss_results['Values'].iloc[1] < kpss_results['Values'].iloc[3]
print("ADF Results: ", self.adf_status)
print("KPSS Results: " ,self.kpss_status)
结果如下:
作者创建的嵌入
最好使用两种测试来确认时间序列是否平稳。这两种测试的结果强烈表明时间序列是非平稳的。
有趣的是,最常见的时间序列预测方法,如 ARIMA 和 SARIMA 可以处理非平稳数据。通常建议在拟合 ARIMA 模型之前进行差分。Auto ARIMA 是一个 Python 包,它允许你自动搜索最佳差分参数。这很方便,因为不需要进行手动测试来找到最佳的差分阶数。
划分训练和测试数据
在准备训练预测模型的数据时,重要的是将数据划分为训练集和测试集。这有助于防止模型过拟合数据,从而导致泛化能力差。我们将训练集定义为 2016 年 7 月之前的所有数据,将测试集定义为 2016 年 7 月之后的所有数据点:
def train_test_split(self):
self.y_train = self.ts_df[self.ts_df.index <= pd.to_datetime('2016-07')]
self.y_test = self.ts_df[self.ts_df.index > pd.to_datetime('2016-07')]
自动 ARIMA 模型
要使用 Auto ARIMA,首先让我们安装 pdarima 包:
!pip install pmdarima
接下来,让我们从 pdarima 导入 auto arima:
from pmdarima.arima import auto_arima
然后我们可以定义我们的拟合方法,用于将 ARIMA 模型拟合到训练数据中:
def fit(self):
self.y_train.fillna(0,inplace=True)
model = auto_arima(self.y_train, trace=True, error_action='ignore', suppress_warnings=True, stationary=True)
model.fit(self.y_train)
forecast = model.predict(n_periods=len(self.ts_df))
self.forecast = pd.DataFrame(forecast,index = self.ts_df.index,columns=['Prediction'])
self.ts_df = pd.DataFrame(self.ts_df, index = self.forecast.index)
self.y_train = self.ts_df[self.ts_df.index < pd.to_datetime('2016-07')]
self.y_test = self.ts_df[self.ts_df.index > pd.to_datetime('2016-07')]
self.forecast = self.forecast[self.forecast.index > pd.to_datetime('2016-07')]
我们还将定义一个验证方法,该方法展示性能并可视化模型预测。我们将使用平均绝对误差来评估性能:
def validate(self):
plt.plot(self.y_train, label='Train')
plt.plot(self.y_test, label='Test')
plt.plot(self.forecast, label='Prediction')
mae = np.round(mean_absolute_error(self.y_test, self.forecast), 2)
plt.title(f'{self.target} Prediction; MAE: {mae}')
plt.xlabel('Date')
plt.ylabel(f'{self.target}')
plt.xticks(rotation=45)
plt.legend(loc='upper left', fontsize=8)
plt.show()
完整的类如下所示:
作者创建的嵌入
现在,如果我们定义一个新的实例,对其进行拟合并验证模型,我们得到:
作者创建的嵌入
本文中的代码可以在 GitHub 上找到。
结论
在这篇文章中,我们讨论了如何编写一个类来组织时间序列分析工作流程的步骤。首先,我们定义了一个方法,使我们能够读取和显示数据。然后,我们定义了一个方法来计算时间序列的均值和标准差。接着,我们定义了一个方法,使我们能够可视化时间序列数据的折线图、时间序列值的直方图以及数据的季节性分解。随后,我们编写了一个类方法,使我们能够执行平稳性统计检验。我们使用了 ADF 检验来测试平稳性,并使用了 KPSS 检验来测试非平稳性。最后,我们将一个 Auto ARIMA 模型拟合到训练数据中,验证了我们的模型,并为预测生成了可视化图。我希望你觉得这篇文章有用。我鼓励你尝试将这些方法应用到你的时间序列分析项目中!
掌握天气预报:利用 LSTM 深度学习模型释放 AI 的力量以实现准确的温度预测
使用 LSTM 的先进深度学习技术预测温度趋势
·发表于Towards Data Science ·7 分钟阅读·2023 年 3 月 27 日
--

作者提供的图片:使用 DALLE-2 生成的天气预测
天气预报是现代世界中最重要的工具之一,开发一个良好的温度预测模型可以为许多企业带来巨大的竞争优势。环境温度测量与农业、能源部门、交易、航空等多个业务领域直接相关。
测量温度是天气预报的重要方面,是气候条件中最基本且广泛使用的指标之一,因为它影响大气气体的行为以及空气和水的循环,这些都是气候系统的关键组成部分。
天气温度测量对许多企业的重要性体现在以下几个关键方面:
-
农业:温度测量用于监测生长条件并预测潜在的作物产量,这有助于农民做出关于种植、收获和储存作物的明智决策。
-
能源和公用事业:温度测量用于预测热浪、寒潮及其他温度极端情况,这有助于公用事业公司计划并应对能源需求的变化。一些公司进行能源交易,而能源需求预测对于获取良好的利润至关重要。
-
运输:温度测量用于预测和监控极端温度,如热浪或寒潮,这些会影响交通系统(如道路和机场)的安全性和效率。
总体而言,温度测量是理解和预测天气及气候条件的基本工具,一个能够准确预测温度的模型对于许多行业的运作至关重要,并且对其他许多行业非常有利。
在这篇文章中,我们将学习如何构建 LSTM 深度学习模型来精确预测温度。
数据集
用于训练的数据集来自 INMET 网站,包含来自巴西圣保罗市 SAO PAULO — INTERLAGOS(A771)气象站的气象数据。温度数据的采样频率为每小时,训练数据从 2022 年 3 月 23 日 01:00 到 2023 年 3 月 23 日 12:00,共 1 年。

作者提供的图像:圣保罗地图
- 目标变量:最高温度(ºC)

作者提供的图像:INMET 温度(最高、平均、最低)
LSTM
LSTM(长短期记忆)是一种递归神经网络(RNN)架构,特别适合处理序列数据,如时间序列、语音或文本。
与传统的前馈神经网络不同,LSTM 模型的基本构建块是 LSTM 单元,该单元设计用于长期记住和更新信息。每个 LSTM 单元具有一组“门”,控制信息的流入和流出,使网络在处理输入数据时能够选择性地存储或忘记信息。
LSTM 网络通常由多个 LSTM 单元按序列排列组成。输入数据一次通过一个时间步的单元,前一个单元的输出用作下一个单元的输入。这使得网络能够学习和建模数据中的复杂序列模式。

作者提供的图像:LSTM 架构
训练和基准
训练时间序列模型的主要结果是如何正确地将数据集拆分为训练集和测试集。由于序列很重要,我们不能随意拆分数据集,为了正确拆分数据集,使用了 sklearn 函数“TimeSeriesSplit”。
LSTM Vanilla
LSTM vanilla(或“vanilla LSTM”)指的是一种长短期记忆(LSTM)神经网络架构,是 LSTM 模型的基本或标准版本。
训练
-
18 天的采样时间
-
数据集按小时分隔
-
80% 训练数据集

作者提供的图像:训练 LSTM Vanilla
训练的回归指标:
-
MAE(平均绝对误差):0.53
-
MSE(均方误差):0.60
-
MAPE(平均绝对百分比误差):2.9%
问题:
- 温度上升的延迟和峰值识别
测试
- 数据集的 20%

作者提供的图像:测试 LSTM vanilla
训练的回归指标:
-
MAE(平均绝对误差):0.59
-
MSE(均方误差):0.82
-
MAPE(均绝对百分比误差):2.5%
预测
- 48 小时的预测

作者提供的图像:预测 LSTM Vanilla
LSTM 堆叠
堆叠 LSTM(长短期记忆)是一种流行 LSTM 循环神经网络架构的变体。在标准 LSTM 中,使用单层 LSTM 单元来处理序列数据。在堆叠 LSTM 中,使用多个 LSTM 单元层,其中一层的输出作为下一层的输入。
堆叠 LSTM 架构用于提高网络学习和建模数据中复杂序列模式的能力。堆叠中的每一层可以学习不同层次的抽象,并从输入数据中提取特征,这使得网络能够捕捉数据中的更复杂关系和依赖性。
训练
-
18 天的样本时间
-
数据集按小时分隔
-
80% 训练数据集

作者提供的图像:训练 LSTM 堆叠
训练的回归指标:
-
MAE(均绝对误差):0.52
-
MSE(均方误差):0.57
-
MAPE(均绝对百分比误差):2.8%
问题:
-
温度上升和峰值识别的延迟
-
对未来预测存在问题
测试
- 数据集的 20%

作者提供的图像:测试 LSTM 堆叠
训练的回归指标:
-
MAE(均绝对误差):0.57
-
MSE(均方误差):0.82
-
MAPE(均绝对百分比误差):2.4%
预测
- 48 小时的预测

作者提供的图像:预测 LSTM 堆叠
评估预测 — LSTM Vanilla
在对每个模型进行训练和测试后,比较它们效率的最一致方法是与新的实际数据进行对比。对 LSTM Vanilla 和堆叠模型进行了未来 48 小时的预测,并与实际数据(黄色)进行比较,如下所示:

作者提供的图像:LSTM Vanilla、LSTM 堆叠和实际温度对比
LSTM Vanilla
-
平均误差:2.04 ºC
-
最大误差:4.44ºC
LSTM 堆叠
-
平均误差:1.36 ºC
-
最大误差:3.16ºC

作者提供的图像:MAE 比较
结论
在 48 小时的预测中,堆叠 LSTM 模型显示出较低的平均绝对误差和较低的最大温度误差,表明对这些 LSTM 模型及拟合数据来说,增加模型复杂性是有益的。
在 48 小时预测中,Stacked LSTM 模型显示出相较于 Vanilla 模型的平均绝对误差和最大温度误差均有所减少。这些发现表明,模型中加入的额外复杂性对 LSTM 架构是有利的。这些额外层的引入使得模型能够捕捉数据中的更复杂的模式和依赖性,从而提升其在预测任务中的性能。观察到的误差减少表明,增加的复杂性使得模型能够捕捉到更多的基础模式和温度数据的变异性,从而实现更准确的预测。
两个模型在预测未来 48 小时的温度值时均表现出较高的精确度,平均误差率约为 2%和 2ºC。然而,两个模型都遇到了准确识别与预期结果不同的次级温度谷的挑战。这一问题突显了模型在捕捉和解释温度数据中的复杂模式时的潜在限制,尤其是在出现意外波动或异常时。可能需要进一步研究以提高模型对这些不规则性的敏感性,并增强其在温度预测中的整体表现。
非常感谢您的阅读!如有任何问题或建议,请通过 LinkedIn 联系我:www.linkedin.com/in/octavio-b-santiago/
如果您想实现此解决方案或了解更多关于 LSTM 算法的内容,可以在我的 GitHub 库中找到完整的 Python 代码,链接如下:
代码
[## GitHub - octavio-santiago/temperature-forecasting: 温度预测模型与 AI - Python
温度预测模型与 AI - Python 本库包含了用于训练和评估 LSTM 深度学习的代码…
参考文献
数据来源:mapas.inmet.gov.br/# — INMET(国家气象研究所)公开数据集:CC0 1.0 通用许可证 (portal.inmet.gov.br/sobre)
现实世界中的数学:测试、模拟及更多
·
关注 发布于 Towards Data Science · 发送为 Newsletter · 3 分钟阅读 · 2023 年 8 月 10 日
--
最佳的数学与统计学写作实现了一项艰难的壮举:它将高深的概念和复杂的公式与数据专业人士在日常工作中遇到的实际挑战联系起来。
一些数据科学家喜欢深入探讨新的数学话题,而另一些人则对这一领域持谨慎态度,甚至有些不情愿。无论你在这个范围的哪个位置,我们相信你会喜欢我们本周精选的文章。从 A/B 测试的内部机制到图论和统计实验,它们都轻松融合了理论与实践、抽象与具体。让我们深入了解吧。
-
无论你是刚接触蒙特卡罗模拟还是需要一个扎实的复习,悉尼·奈的首篇 TDS 文章深入浅出地探讨了统计技术,“让我们在面对不确定性时进行战略性投注,从而使复杂的确定性问题变得概率化。”
-
图论在机器学习研究中已经占据了核心位置,但对那些不在该领域的人来说,它可能仍然显得令人望而却步。亨尼·德·哈德提供了一份面向初学者的指南,介绍了图是什么,它们如何运作,以及数据科学家如何利用它们的力量来解决复杂的现实问题。
-
如果自高中以来你对微分方程没有多加思考,这是你从全新角度重新审视它们的机会:帅·郭的物理信息神经网络(PINN)系列文章回归,其中一篇专门讨论微分方程及其如何“提供对系统动态的洞察,并使我们能够预测系统未来的行为。”

-
要深入了解置换检验及其如何取代更传统的基于公式的统计方法,请跟随潘·克里坦了解如何设计带有重抽样的实验。(那些从较少数学背景转到数据科学的读者会发现这篇文章尤其有用!)
-
我们最后的每周亮点回到了我们开始时提到的蒙特卡罗模拟,但将其力量用于不同的目的。艾达·约翰逊博士分享了一个有用的 A/B 测试介绍:它清晰地定义了相关的统计概念,并聚焦于使用蒙特卡罗模拟评估测试性能的过程。
本周我们推荐的其他阅读内容并不完全免于数学,但它们为关于其他重要主题的引人入胜的讨论打开了空间。
-
在一项系统而及时的研究中,Yennie Jun探讨了大型语言模型内置历史知识中的性别偏见。
-
错过了 ICML 2023?Michael Galkin在这里帮助我们通过详细回顾最新进展和新兴趋势来赶上。
-
每个人都喜欢抱怨数据清理,但Vicky Yu的简明指南可以帮助你简化这个过程,使其变得不那么乏味。
-
Francesco Foscarin在其首次 TDS 文章中,将变压器与爵士和弦相结合,展示了基于数据的树状音乐分析方法。
-
Hans van Dam将移动应用程序开发与 LLM 结合,通过一个利用 GPT-4 功能导航应用程序图形用户界面(GUI)的实用教程。
感谢您支持我们的作者!如果您喜欢在 TDS 上阅读的文章,请考虑成为 Medium 会员 — 这将解锁我们的整个存档(以及 Medium 上的每篇其他文章)。
直到下一个变量,
TDS 编辑
Matplotlib 提示,以立即提升你的数据可视化——根据《数据故事讲述》
使用 Matplotlib 在 Python 中重现Cole Nussbaumer Knaflic书中的经验教训
·发布于数据科学前沿 ·阅读时间 9 分钟·2023 年 6 月 19 日
--

能够有效地用数据进行沟通是一项适用于所有数据相关工作的技能——不仅仅是数据科学家和数据分析师。
我最喜欢的一本关于这个主题的书是Cole Nussbaumer Knaflic的《数据故事讲述》。这本书里充满了如何改进数据可视化的实用示例。
《数据故事讲述:面向商业专业人士的数据可视化指南》 [Nussbaumer Knaflic, Cole] 在 Amazon.com 上…
我认为这本书唯一不幸的地方是,其示例是使用 Microsoft Excel 创建的。
如果你知道一个喜欢在 Excel 中创建数据可视化的工程师,请举手——是的,我也没有。
“你可能是一个工程师,但理解你的图表不应该需要一个工程学学位。” ——Cole Nussbaumer Knaflic在《数据故事讲述》中
这就是为什么这篇文章将涵盖我自从阅读Nussbaumer Knaflic的《数据故事讲述》以来使用过的 Matplotlib 代码片段。
import matplotlib.pyplot as plt
这篇文章假设你已经掌握了 Matplotlib 和 Seaborn 的数据可视化基础知识,比如创建条形图、折线图或散点图,修改颜色调色板,并添加基本标签。文章还假设你知道何时使用哪种类型的图表。
这篇文章重点介绍一些不太常见的技巧,而不是 Matplotlib 的基础知识,例如:
-
如何去除 Matplotlib 图的顶部和右侧边框
-
如何从 Matplotlib 图中移除刻度线
-
如何自定义 Matplotlib 图中每个条形的颜色
-
如何更改 Matplotlib 图中 x 轴和 y 轴的颜色
-
如何向 Matplotlib 图中添加文本注释
-
如何在 Matplotlib 图中为条形图添加数值
-
如何在 Matplotlib 注释中将整段或部分文本设为粗体
-
如何在 Matplotlib 注释中为文本上色
我们从一个简单的例子开始。以下数据是虚构的,以便我们能够专注于数据可视化技术:
import pandas as pd
# Define fictional example dataframe
df = pd.DataFrame(
{'feature 1' : ['cat 1', 'cat 2', 'cat 3', 'cat 4'],
'feature 2' : [400, 300, 200, 100]
})
让我们以一个简单的单色条形图为起点,使用 Seaborn 并添加标题:
import seaborn as sns
# Create a basic bar chart from the example dataframe
fig, ax = plt.subplots(1,1, figsize = (6, 4))
sns.barplot(data = df,
x = 'feature 1',
y = 'feature 2',
color = 'tan')
# Add title
ax.set_title('Meaningful Title')
plt.show()

移除杂乱
在《杂乱是你的敌人!》一章中,Nussbaumer Knaflic 讨论了如何识别并消除数据可视化中的视觉杂乱——这一部分将展示如何在 Matplotlib 图中去除视觉杂乱。
“[……E]每一个元素都会增加观众的认知负担。”—— Cole Nussbaumer Knaflic 在《数据讲故事》中
如何去除 Matplotlib 图的顶部和右侧边框
默认情况下,Matplotlib 图的边缘有一个所谓的脊线框。尤其是顶部和右侧的脊线会使数据可视化显得杂乱,因此应该被去除。
你可以通过以下代码片段简单地去除不相关的脊线:
# Remove top and right spines
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
如果你还想去除其他脊线,可以使用 'bottom' 和 'left'。如果你想去除边框,包括完整的 x 轴和 y 轴,可以使用 ax.axis('off')。
如何从 Matplotlib 图中移除刻度线
刻度线通常不会被认为是杂乱的。但在某些情况下,如此示例中,条形图的 x 轴刻度线是多余的。
# Remove ticks on x-axis
ax.tick_params(bottom = False)
如果你还想去除 y 轴的刻度线,可以使用 left = False。
现在,去除杂乱后的例子如下所示:

在 Matplotlib 图中去除杂乱前后的对比。
新图由于去除了视觉杂乱,难道没有给你一种更加平静的感觉吗?
减少强调
在《像设计师一样思考》一章中,Nussbaumer Knaflic 向我们展示了如何消除必要但相关的信息。这一部分展示了如何更改 Matplotlib 图中不重要部分的颜色。
“将必要但不会影响信息传递的项推到背景中。[……] 浅灰色在这方面效果很好。”—— Cole Nussbaumer Knaflic 在《数据讲故事》中
如何自定义 Matplotlib 图中每个条形的颜色
用 palette 参数替换 sns.barplot 方法中的 color 参数,以控制每个条形的颜色。通过这样做,你可以使用浅灰色来减少不重要的条形的强调,只用主要颜色来突出相关的条形。
# Define colors of individual bars
custom_colors = ['lightgrey', 'tan', 'lightgrey', 'lightgrey']
# De-emphasize less important bars
sns.barplot(data = df,
x = 'feature 1',
y = 'feature 2',
palette = custom_colors)
如何在 Matplotlib 绘图中更改 x 轴和 y 轴的颜色
接下来,我们还想降低 x 轴和 y 轴的颜色。为此,我们需要降低轴的脊、刻度和标签的颜色:
# Mute colors of spines
ax.spines['left'].set_color('grey')
ax.spines['bottom'].set_color('grey')
# Mute colors of ticks
ax.tick_params(colors = 'grey')
# Mute colors of labels
ax.set_xlabel('feature 1', color = 'grey')
ax.set_ylabel('feature 2', color = 'grey')
现在,强调较不重要信息的示例如下所示:

在 Matplotlib 图中强调和不强调不重要信息的前后对比。
文本是你的朋友:添加注释
Nussbaumer Knaflic 强调,您应该在数据可视化中添加文本以突出关键要点。在本节中,我们将探讨 ax.annotate() 方法,以在 Matplotlib 图中添加文本。
“如果您希望您的观众得出结论,请用文字说明。” — Cole Nussbaumer Knaflic 在《用数据讲故事》中
如何在 Matplotlib 图中添加文本注释
要在 Matplotlib 图形中添加文本,您可以使用 ax.annotate() 方法,该方法将文本及其在图中的位置作为参数。此外,您还可以指定水平 (ha) 或垂直对齐 (va) 以及字体大小等方面。
# Add text annotations
ax.annotate('Look at "cat 2". \nThis is important!',
xy = (1.5, 360),
ha = 'center',
fontsize = 11,
)
如果您想添加额外的箭头指向某个位置,则需要使用以下参数:
-
xy:要注释的点——即箭头指向的位置 -
xytext:文本的位置(以及箭头的终点) -
arrowprops = {'arrowstyle' : '->'}:箭头的样式
如何在 Matplotlib 图中给条形图添加数值
要为每个单独的条形添加数值,我们需要遍历 ax.patches。对于每个 bar,您可以使用 get_height()、get_width() 和 get_x() 方法来将数值放置在条形上方。
# Annotate bar chart with values
for bar in ax.patches:
ax.annotate(int(bar.get_height()),
xy = (bar.get_x() + bar.get_width() / 2, bar.get_height()),
ha = 'center',
va = 'center',
xytext = (0, 8),
textcoords = 'offset points'
)
现在,添加了文本注释的示例如下所示:

在 Matplotlib 图中添加文本注释的前后对比。
强调
在章节“聚焦您的观众注意力”中,Nussbaumer Knaflic 讨论了如何利用预注意属性引导观众的注意力到您希望他们看到的内容。在本节中,我们将讨论可以应用于 Matplotlib 图中文本注释的一些简单调整,以利用文本中的预注意属性。
“……如果我们战略性地使用预注意属性,它们可以帮助我们让观众在他们甚至不知道自己在看到之前,就看到我们希望他们看到的东西。” — Cole Nussbaumer Knaflic 在《用数据讲故事》中
如何在 Matplotlib 注释中使整段或部分文本变为 粗体
使用 粗体 文本可以帮助突出数据可视化中的重要部分。如果您只想突出注释中的一部分,可以在字符串中使用 $\\bf{}$ 并将要强调的文本放在花括号中。如果您想突出整个注释,只需添加参数 fontweight = 'bold'。
# Make only part of text bold
ax.annotate('Look at "cat 2". \nThis is $\\bf{important}$!',
#...
)
# Make all of the text bold
ax.annotate('Look at "cat 2". \nThis is important!',
#...
fontweight='bold',
)
如何在 Matplotlib 注释中给文本上色
要将特定文本与数据可视化中的特定元素关联起来,你可以利用相同颜色的关联性。要给文本注释上色,只需将 color 参数添加到 ax.annotate() 方法中。
# Remove ticks on x-axis
ax.tick_params(bottom = False)
# Add important take away to plot
ax.annotate('Look at "cat 2". \nThis is $\\bf{important}$!', # Emphasize important terms
xy = (1.5, 360),
ha = 'center',
color = 'tan',
fontsize = 11,
)
现在,强调重要信息的示例如下:

在 Matplotlib 绘图中应用重要信息之前和之后。
下面是创建最终数据可视化的代码。
import matplotlib.pyplot as plt
import seaborn as sns
# Define color palette
highlight_color = 'tan'
muted_color = 'dimgrey'
muted_color2 = 'lightgrey'
custom_colors = [muted_color2, 'tan', muted_color2, muted_color2]
# Create a basic bar chart from the example dataframe
fig, ax = plt.subplots(1,1, figsize = (6, 4))
sns.barplot(data = df,
x = 'feature 1',
y = 'feature 2',
palette = custom_colors) # De-emphasize less important bars
# Add title
ax.set_title('Meaningful Title')
# Mute colors of labels
ax.set_xlabel('feature 1', color = muted_color)
ax.set_ylabel('feature 2', color = muted_color)
# Remove unimportant spines and mute color of remaining spines
ax.spines['right'].set_visible(False) # Remove top and right spines
ax.spines['top'].set_visible(False) # Remove top and right spines
ax.spines['left'].set_color(muted_color) # Mute colors of spines
ax.spines['bottom'].set_color(muted_color) # Mute colors of spines
# Remove ticks on x-axis and mute colors of ticks
ax.tick_params(bottom = False, # Remove ticks on x-axis
colors = muted_color, # Mute colors of ticks
)
# Annotate bar chart with values
for i, bar in enumerate(ax.patches):
ax.annotate(int(bar.get_height()),
xy = (bar.get_x() + bar.get_width() / 2, bar.get_height()),
ha = 'center',
va = 'center',
xytext = (0, 8),
textcoords = 'offset points',
color = custom_colors[i])
# Add important take away to plot
ax.annotate('Look at "cat 2". \nThis is $\\bf{important}$!', # Emphasize important terms
xy = (1.5, 360),
ha = 'center',
color = highlight_color,
fontsize = 11,
)
plt.show()
科尔·努斯鲍默·克纳夫利克的《数据讲故事》是我最喜欢的数据可视化书籍之一。如果你对如何将数据可视化提升到更高水平感兴趣,我绝对推荐这本书。
如果你对更多的 Matplotlib 技巧感兴趣,在这个仓库中,安德烈·加斯科夫用 Python 和 Matplotlib 重现了书中的许多可视化:
[## GitHub - empathy87/storytelling-with-data: 来自《数据讲故事》的绘图…
来自《数据讲故事》的绘图实现,使用 Python 和 matplotlib - GitHub …
享受这个故事了吗?
免费订阅 以便在我发布新故事时获得通知。
[## 每当 Leonie Monigatti 发布新内容时获取电子邮件通知。
每当 Leonie Monigatti 发布新内容时获取电子邮件通知。通过注册,如果你还没有 Medium 账户,你将创建一个…
在 LinkedIn、Twitter和 Kaggle上找到我!
参考文献
图片参考
如果没有其他说明,所有图片均由作者创作。
网络与文献
科尔·努斯鲍默·克纳夫利克. 《数据讲故事:商业专业人士的数据可视化指南》, Wiley, © 2015.
Matplotlib 教程:将你的国家地图提升到另一个水平
原文:
towardsdatascience.com/matplotlib-tutorial-lets-take-your-country-maps-to-another-level-a6bd1f40fff
Matplotlib 教程
如何使用 Python 和 Matplotlib 绘制美丽的地图
·发表于Towards Data Science ·阅读时间 10 分钟·2023 年 9 月 24 日
--

地图由作者创建
是的,我使用 Matplotlib 创建了上面的地图,我会在本教程中向你展示如何做。
这个想法是创建一个可重用且灵活的函数,使我能够立即绘制任何区域的美丽地图。
有了这样的功能,我可以节省大量时间,当我想创建带有地理组件的图表或信息图时。
我还想展示可重用代码的力量,因为许多从事数据可视化的程序员往往忽视了这样的最佳实践。
本教程包含了创建上述非洲地图所需的每一行代码。
让我们开始吧。
第一步:下载地理数据
在开始本教程之前,你需要做的唯一一件事是从这里下载地理数据:
datacatalog.worldbank.org/search/dataset/0038272/World-Bank-Official-Boundaries
我使用的是名为 World Boundaries GeoJSON — Very High Resolution 的数据。
这是来自世界银行的官方边界数据集,你可以按任何你想要的方式使用。
第二步:导入库
一如既往,我们从导入必要的库开始,我们不需要很多库。由于我们有地理数据,我们希望使用geopandas使绘图尽可能简单。
import numpy as np
import pandas as pd
import seaborn as sns
import geopandas as gpd
import matplotlib.pyplot as plt
import matplotlib.patheffects as PathEffects
from matplotlib.patches import Polygon
你可能还没见过的一个导入是PathEffe。我将使用它来稍后在国家标签周围创建边框。
第三步:创建 seaborn 风格
在绘图之前,我总是创建一个 seaborn 风格,以获得一致的外观。在这里,我只定义了一个background_color、font_family和text_color。我将背景设置为浅蓝色,以代表海洋。
font_family = "sans"
background_color = "#D4F1F4"
text_color = "#040303"
sns.set_style({
"axes.facecolor": background_color,
"figure.facecolor": background_color,
"font.family": font_family,
"text.color": text_color,
})
你可以样式化其他图表方面,例如网格,但我更喜欢在绘制地图时使用plt.axis(“off”)隐藏大多数标准图表组件。
第 4 步:加载地理数据
现在是时候使用 geopandas 从世界银行加载地理数据。我正在更改塞舌尔的CONTINENT和赤道几内亚的INCOME_GRP。
这看起来并不像它看起来那么奇怪,因为塞舌尔是非洲的一部分,根据世界银行最新的数据,赤道几内亚属于“中上收入”组别。
world = gpd.read_file("WB_Boundaries_GeoJSON_highres/WB_countries_Admin0.geojson")
world.loc[world.NAME_EN == "Seychelles", "CONTINENT"] = "Africa"
world.loc[world.NAME_EN == "Equatorial Guinea", "INCOME_GRP"] = "3\. Upper middle income"
africa = world[world.CONTINENT == "Africa"].reset_index(drop=True)
disputed_areas = gpd.read_file("WB_Boundaries_GeoJSON_highres/WB_Admin0_disputed_areas.geojson")
disputed_areas = disputed_areas[disputed_areas.CONTINENT == "Africa"]
接下来,我将非洲国家分开,因为我想将它们单独绘制,以作为我的图表的重点,对争议区域也进行同样的处理。
第 5 步:创建绘制地图函数
现在,我准备创建绘制地图的第一个版本函数。稍后我将扩展它以添加一些额外的函数。
这个第一个版本循环遍历一系列 geopandas 数据帧,并使用color和edgecolor列绘制它们。
该函数接受一些我还未使用的参数,但它们在教程的后续部分将会很有用。
def draw_map(
maps_to_draw,
boundry_map_index=0,
use_hatch_for_indexes=[],
padding={},
labels=[],
figsize=(40, 40)
):
assert "color" in maps_to_draw[0].columns, "Missing color column in map dataframe"
assert "edgecolor" in maps_to_draw[0].columns, "Missing edgecolor column in map dataframe"
fig = plt.figure(figsize=figsize)
ax = fig.add_subplot()
for map_index, map_to_draw in enumerate(maps_to_draw):
map_to_draw.plot(
ax=ax, color=map_to_draw.color, edgecolor=map_to_draw.edgecolor,
hatch="//" if map_index in use_hatch_for_indexes else "",
)
# Additional functions below this comment
return ax
我希望draw_map()返回ax对象,因为我经常希望根据使用案例扩展图表以添加更多信息。
要使用draw_map(),我必须为每个要绘制的 geopandas 数据帧定义color和edgecolor列。
我为world定义了颜色,以将每个国家绘制为淡背景。对于africa,我选择了使用数据中表示的三种收入组别的更显眼的颜色。
选择颜色时的一个提示是使用Coloring for Colorblindness检查你的颜色是否适合色盲人士。
world["color"] = "#f0f0f0"
world["edgecolor"] = "#c0c0c0"
africa["edgecolor"] = "#000000"
africa.loc[africa.INCOME_GRP == "5\. Low income", "color"] = "#dadada"
africa.loc[africa.INCOME_GRP == "4\. Lower middle income", "color"] = "#89bab2"
africa.loc[africa.INCOME_GRP == "3\. Upper middle income", "color"] = "#1B998B"
disputed_areas["color"] = "#FFD6D6"
disputed_areas["edgecolor"] = "#000000"
现在我已经创建了所需的列,我可以运行draw_map()。
ax = draw_map(maps_to_draw=[world, africa, disputed_areas])
plt.axis("off")
plt.show()
这是我得到的结果。

地图由作者创建
这是一个很好的开始,但我们还有很长的路要走。
第 6 步:设置边界
由于我想创建一张非洲地图,所以显示整个世界是没有意义的。
这就是为什么我创建了以下函数,它接收一个地理数据帧,并设置ax对象的范围。
def set_limits(ax, data, pad_left=0, pad_right=0, pad_top=0, pad_bottom=0):
xmin_ = data.bounds.minx.min()
ymin_ = data.bounds.miny.min()
xmax_ = data.bounds.maxx.max()
ymax_ = data.bounds.maxy.max()
xmin = xmin_ - pad_left * (xmax_ - xmin_)
xmax = xmax_ + pad_right * (xmax_ - xmin_)
ymin = ymin_ - pad_bottom * (ymax_ - ymin_)
ymax = ymax_ + pad_top * (ymax_ - ymin_)
ax.set(xlim=(xmin, xmax), ylim=(ymin, ymax))
你可以在区域的每一侧添加填充,以便为额外的信息(如图例)留出空间。
让我们使用boundary_map_index和padding参数将其添加到draw_map()函数中。
def draw_map(
maps_to_draw,
boundry_map_index=0,
use_hatch_for_indexes=[],
padding={},
labels=[],
figsize=(40, 40)
):
...
# Additional functions below this comment
set_limits(ax, maps_to_draw[boundry_map_index], **padding)
return ax
我将boundry_map_index设置为maps_to_draw列表中我们希望显示的 geopandas 数据帧的索引,并添加了一些padding。
注意:我还传递了use_hatch_for_indexes=[2]以在争议区域绘制斜纹,以显示它们与其他区域的不同。
ax = draw_map(
maps_to_draw=[world, africa, disputed_areas], boundry_map_index=1,
padding={"pad_bottom": -0.08, "pad_top": 0.07, "pad_left": 0.07, "pad_right": 0.05},
use_hatch_for_indexes=[2]
)
plt.axis("off")
plt.show()
现在我们得到了一个看起来不错的非洲地图,还包括了邻近国家的轮廓。如果你不想显示邻近国家,请从maps_to_draw中删除world。
这是生成的地图。

地图由作者创建
我们正在取得进展。
第 7 步:添加国家标签
由于大多数人不认识所有非洲国家,下一步是为每个国家添加标签。
我想在大国家的中间附近添加标签,对于较小的国家,则在国家和标签之间添加一条线。
def add_label(ax, label, fontsize=24, fontweight="bold", va="center", ha="center"):
annotation = plt.annotate(
label["label"],
xy=label["xytext"] if "xypin" not in label.keys() else label["xypin"],
xytext=None if "xypin" not in label.keys() else label["xytext"],
xycoords="data", fontsize=fontsize, va=va, ha=ha,
linespacing=1.3, color=label["color"], fontweight=fontweight,
arrowprops={
"arrowstyle": "-",
"linewidth": 2,
})
annotation.set_path_effects([PathEffects.withStroke(linewidth=6, foreground='w')])
定义国家标签的位置是这个教程中唯一繁琐的过程,但我已经为你完成了。
你可以尝试使用 geopandas 中的几何图形计算每个标签的位置,但这说起来容易做起来难,因为国家的形状各异。
有时候,最快的方法是卷起袖子直接硬编码值,这也是我在这里做的。
country_labels = [
{"label": "Algeria", "color": "#040303", "xytext": (2.0, 27.5)},
{"label": "Angola", "color": "#040303", "xytext": (17.7, -13.1)},
{"label": "Benin", "color": "#040303", "xytext": (3.2, 5.4), "xypin": (2.3, 7.6)},
{"label": "Botswana", "color": "#040303", "xytext": (24.4, -22.3)},
{"label": "Burkina\nFaso", "color": "#040303", "xytext": (-1.4, 12.6)},
{"label": "Burundi", "color": "#040303", "xytext": (43.3, -4.9), "xypin": (29.8, -3.6)},
{"label": "Cameroon", "color": "#040303", "xytext": (12.5, 5.2)},
{"label": "Cape Verde", "color": "#040303", "xytext": (-23.7, 19), "xypin": (-23.7, 16)},
{"label": "Central African\nRepublic", "color": "#040303", "xytext": (21.1, 6.5)},
{"label": "Chad", "color": "#040303", "xytext": (18.5, 16.0)},
{"label": "Comoros", "color": "#040303", "xytext": (46.8, -9.6), "xypin": (43.3, -11.7)},
{"label": "Cote\nd'Ivoire", "color": "#040303", "xytext": (-5.5, 8.5)},
{"label": "Democratic\nRepublic of\nthe Congo", "color": "#040303", "xytext": (23.3, -2.7)},
{"label": "Djibouti", "color": "#040303", "xytext": (47.0, 13.4), "xypin": (43.0, 12.2)},
{"label": "Egypt", "color": "#040303", "xytext": (29.2, 26.6)},
{"label": "Equatorial\nGuinea", "color": "#040303", "xytext": (5.9, -2.5), "xypin": (10.5, 1.6)},
{"label": "Eritrea", "color": "#040303", "xytext": (43.0, 16.9), "xypin": (38.5, 16.2)},
{"label": "Lesotho", "color": "#040303", "xytext": (35.0, -31.0), "xypin": (28.4, -29.5)},
{"label": "Ethiopia", "color": "#040303", "xytext": (39.9, 8.5)},
{"label": "Gabon", "color": "#040303", "xytext": (11.8, -0.7)},
{"label": "Ghana", "color": "#040303", "xytext": (-1.3, 6.6)},
{"label": "Guinea", "color": "#040303", "xytext": (-11.6, 11.0)},
{"label": "Guinea-\nBissau", "color": "#040303", "xytext": (-20.3, 10.3), "xypin": (-14.5, 12.2)},
{"label": "Kenya", "color": "#040303", "xytext": (37.9, 0.5)},
{"label": "Eswantini", "color": "#040303", "xytext": (35.5, -29.3), "xypin": (31.5, -26.8)},
{"label": "Liberia", "color": "#040303", "xytext": (-10.6, 3.6), "xypin": (-9.6, 6.7)},
{"label": "Libya", "color": "#040303", "xytext": (17.5, 27.5)},
{"label": "Madagascar", "color": "#040303", "xytext": (46.7, -19.6)},
{"label": "Malawi", "color": "#040303", "xytext": (38.9, -21.3), "xypin": (35.0, -15.6)},
{"label": "Mali", "color": "#040303", "xytext": (-1.9, 17.8)},
{"label": "Mauritania", "color": "#040303", "xytext": (-11.1, 19.6)},
{"label": "Morocco", "color": "#040303", "xytext": (-6.9, 31.3)},
{"label": "Mozambique", "color": "#040303", "xytext": (40.8, -15.2)},
{"label": "Namibia", "color": "#040303", "xytext": (17.3, -20.7)},
{"label": "Niger", "color": "#040303", "xytext": (9.8, 17.5)},
{"label": "Nigera", "color": "#040303", "xytext": (7.8, 9.8)},
{"label": "Republic of\nthe Congo", "color": "#040303", "xytext": (7.8, -7.2), "xypin": (12.0, -4.1)},
{"label": "Rwanda", "color": "#040303", "xytext": (43.8, -3.6), "xypin": (30.1, -2.0)},
{"label": "São Tomé and\nPríncipe", "color": "#040303", "xytext": (-0.9, 0.2), "xypin": (6.8, 0.2)},
{"label": "Senegal", "color": "#040303", "xytext": (-15.0, 14.7)},
{"label": "Seychelles", "color": "#040303", "xytext": (55.6, -2), "xypin": (55.6, -4.5)},
{"label": "Sierra Leone", "color": "#040303", "xytext": (-16.4, 6.3), "xypin": (-12.0, 8.5)},
{"label": "Somalia", "color": "#040303", "xytext": (45.7, 2.7)},
{"label": "South\nAfrica", "color": "#040303", "xytext": (22.4, -31.0)},
{"label": "South\nSudan", "color": "#040303", "xytext": (30.2, 7.0)},
{"label": "Sudan", "color": "#040303", "xytext": (29.7, 16.0)},
{"label": "Tanzania", "color": "#040303", "xytext": (35.0, -6.7)},
{"label": "The\nGambia", "color": "#040303", "xytext": (-20.3, 13.6), "xypin": (-15.4, 13.6)},
{"label": "Togo", "color": "#040303", "xytext": (1.0, 4.1), "xypin": (1.0, 7.5)},
{"label": "Tunisia", "color": "#040303", "xytext": (9.3, 38.9), "xypin": (9.3, 35.7)},
{"label": "Uganda", "color": "#040303", "xytext": (32.6, 0.9)},
{"label": "Zambia", "color": "#040303", "xytext": (26.1, -14.9)},
{"label": "Zimbawe", "color": "#040303", "xytext": (29.7, -19.1)},
]
我们将函数直接添加到set_limits下方。
def draw_map(
maps_to_draw,
boundry_map_index=0,
use_hatch_for_indexes=[],
padding={},
labels=[],
figsize=(40, 40)
):
...
# Additional functions below this comment
set_limits(ax, maps_to_draw[boundry_map_index], **padding)
for label in labels:
add_label(ax, label)
return ax
然后将其传递给draw_map()。
ax = draw_map(
maps_to_draw=[world, africa, disputed_areas], boundry_map_index=1,
padding={"pad_bottom": -0.08, "pad_top": 0.07, "pad_left": 0.07, "pad_right": 0.05},
use_hatch_for_indexes=[2]
labels=country_labels
)
plt.axis("off")
plt.show()
这是我们得到的结果。

地图由作者制作
太棒了!现在,我可以看到我正在查看哪些国家,最后要做的是解释颜色。
第 8 步:添加图例和标题
在 matplotlib 中添加图例很简单,所以为了让它更有趣,我想使用国家的形状,而不是标准的圆圈或方块。
这个步骤是一个例子,其中我使用draw_map()返回的 ax,而不是直接将其添加到函数中。
为了将一个国家的几何图形转移到另一个位置,我创建了以下函数,它接受一个row并将形状放置在(x_loc,y_loc)。
def draw_legend_geometry(ax, row, x_loc, y_loc, height):
x = np.array(row.geometry.boundary.coords.xy[0])
y = np.array(row.geometry.boundary.coords.xy[1])
x = x - (row.geometry.centroid.x - x_loc)
y = y - (row.geometry.centroid.y - y_loc)
ratio = height / (y.max() - y.min())
x = x * ratio + (x_loc - x_loc * ratio)
y = y * ratio + (y_loc - y_loc * ratio)
ax.add_artist(Polygon(np.stack([x, y], axis=1), facecolor=row.color, edgecolor=row.edgecolor, hatch=row.hatch))
除了改变位置,它还会改变几何图形的比例,以达到特定的height。
为了使用draw_legend_geometry(),我创建了一个名为legend的数据框,其中选择了适当的国家来代表每种颜色。目前,它仅适用于具有单个Polygon的国家,而不适用于MultiPolygon。
我添加INCOME_GRP到西撒哈拉的方式有点“hacky”,利用它来排序值,并将row.INCOME_GRP[3:]作为文本,但现在这样也没关系。
我还在绘制图例后添加了标题和数据来源。
ax = draw_map(
maps_to_draw=[world, africa, disputed_areas], boundry_map_index=1,
padding={"pad_bottom": -0.08, "pad_top": 0.07, "pad_left": 0.07, "pad_right": 0.05},
use_hatch_for_indexes=[2]
labels=country_labels,
)
legend = pd.concat([
disputed_areas[disputed_areas.NAME_EN == "Western Sahara"],
africa[africa.NAME_EN.isin(["Niger", "Senegal", "Botswana"])]
])
legend.loc[legend.NAME_EN == "Western Sahara", "INCOME_GRP"] = "6\. Disputed area"
legend = legend.sort_values("INCOME_GRP")
legend["hatch"] = ["", "", "", "//"]
for i, row in legend.reset_index().iterrows():
draw_legend_geometry(ax, row, -25, -20 - 3.5*i, 2.5)
ax.annotate(row.INCOME_GRP[3:], (-22, -20 - 3.5*i), fontsize=28, fontweight="bold", va="center")
fontstyles = {"fontweight": "bold", "ha": "left"}
plt.annotate("Data source:", xy=(0.05, 0.32), fontsize=24, xycoords="axes fraction", **fontstyles)
plt.annotate("The World Bank", xy=(0.133, 0.32), fontsize=24, xycoords="axes fraction", color="#1B998B", **fontstyles)
plt.title("Income Groups in Africa", x=0.05, y=0.29, fontsize=42, **fontstyles)
plt.axis("off")
plt.show()
如果我运行上述代码,我会得到以下地图,它与您在教程开始时看到的地图完全相同。

地图由作者制作
这就是教程的最后一步。恭喜你完成了!
结论
你可以通过添加更多样式参数来使draw_map()函数更灵活(我这里有几个硬编码的值),但对我来说,这覆盖了 95%的使用场景。
我希望你喜欢这个教程,并学到了可以在项目中使用的东西。
如果你做到了,鼓掌,订阅,并分享,以便更多人能学会如何用 Python 和 Matplotlib 绘制美丽的地图。
你也应该看看我的其他教程:

Matplotlib 教程
查看列表8 篇故事!

此外,订阅我的免费通讯,Data Wonder,并在 Twitter 上关注我 @oscarl3o。
感谢阅读,下次见。
逻辑回归中的矩阵和向量运算
向量化逻辑回归
·
关注 发布于 Towards Data Science ·10 分钟阅读·2023 年 7 月 7 日
--
任何人工神经网络(ANN)算法背后的数学基础可能令人难以理解。此外,用于表示模型在批量训练过程中前向传播和反向传播计算的矩阵和向量操作会增加理解的难度。虽然简洁的矩阵和向量符号是有意义的,但深入这些符号以了解矩阵操作的细节会带来更多的清晰度。我意识到,理解这些微妙细节的最佳方法是考虑一个最基本的网络模型。我找不到比逻辑回归更好的算法来探索底层机制,因为它具备了 ANN 的所有特点,如多维输入、网络权重、偏差、前向传播操作、应用非线性函数的激活函数、损失函数和基于梯度的反向传播。我的博客意图是分享我对逻辑回归模型核心的矩阵和向量操作的笔记和发现。
逻辑回归简要概述
尽管名字里有“回归”,逻辑回归实际上是一种分类算法,而不是回归算法。它通常用于二分类任务,以预测某个实例属于两个类别之一的概率,例如,预测一封电子邮件是否是垃圾邮件。因此,在逻辑回归中,因变量或目标变量被视为分类变量。例如,垃圾邮件用 1 表示,而非垃圾邮件用 0 表示。逻辑回归模型的主要目标是建立输入变量(特征)与目标变量概率之间的关系。例如,给定一封电子邮件的特征作为输入特征集合,逻辑回归模型会找到这些特征与电子邮件是垃圾邮件的概率之间的关系。如果‘Y’表示输出类别,比如电子邮件是垃圾邮件,‘X’表示输入特征,则概率可以表示为 π = Pr( Y = 1 | X, βi),其中 βi 表示包括模型权重‘wi’和偏置参数‘b’在内的逻辑回归参数。实际上,逻辑回归预测给定输入特征和模型参数下 Y = 1 的概率。具体来说,概率 π 被建模为一个 S 形的逻辑函数,称为 Sigmoid 函数,公式为 π = e^z/(1 + e^z) 或等效地 π = 1/(1 + e^-z),其中 z = βi . X。Sigmoid 函数允许在 0 和 1 之间平滑曲线,非常适合于估计概率。本质上,逻辑回归模型在输入特征的线性组合上应用 Sigmoid 函数,以预测 0 和 1 之间的概率。确定实例输出类别的常见方法是对预测概率进行阈值处理。例如,如果预测概率大于或等于 0.5,则该实例被分类为类别 1;否则,分类为类别 0。

逻辑回归模型示意图 — 由作者创建
逻辑回归模型通过将模型拟合到训练数据上来训练,然后通过最小化损失函数来调整模型参数。损失函数估计预测概率与实际概率之间的差异。用于训练逻辑回归模型的最常见损失函数是对数损失函数,也称为二元交叉熵损失函数。对数损失函数的公式如下:
L = — ( y * ln(p) + (1 — y) * ln(1 — p) )
其中:
-
L 代表对数损失。
-
y 是实际的二元标签(0 或 1)。
-
p 是输出类别的预测概率。
逻辑回归模型通过使用梯度下降等技术来最小化损失函数,从而调整其参数。给定一批输入特征及其真实类别标签,模型的训练在多个迭代(称为 epoch)中进行。在每个 epoch 中,模型进行正向传播操作来估计损失,并进行反向传播操作以最小化损失函数并调整参数。所有这些操作都涉及矩阵和向量计算,如下一节所示。
矩阵和向量表示
请注意 我使用了 LaTeX 脚本来创建嵌入在此博客中的数学方程和矩阵/向量表示的图片。如果有人对 LaTeX 脚本感兴趣,请随时联系我;我很乐意分享。
如上图所示,使用二元逻辑回归分类器作为示例,以简化插图。如下所示,矩阵 X 表示‘m’个输入实例。每个输入实例包含’n’个特征,并表示为矩阵 X 中的一列,即输入特征向量,使其成为一个(n x m)大小的矩阵。上标(i)表示矩阵 X 中输入向量的序号。下标‘j’表示输入向量中特征的序号。大小为(1 x m)的矩阵 Y 捕捉了矩阵 X 中每个输入向量的真实标签。模型权重由大小为(n x 1)的列向量 W 表示,其中包含’n’个权重参数,对应于输入向量中的每个特征。虽然只有一个偏置参数‘b’,为了说明矩阵/向量操作,考虑一个大小为(1 x m)的矩阵 B,其中包含‘m’个相同的偏置 b 参数。

正向传播
正向传播操作的第一步是计算模型参数和输入特征的线性组合。如下所示,此矩阵操作的符号表示一个新的矩阵 Z 的计算:

注意权重矩阵 W 的转置使用。上述矩阵的扩展表示如下:

上述矩阵运算的结果是计算出大小为(1 x m)的矩阵 Z,如下所示:

下一步是通过对计算出的线性组合应用 sigmoid 函数来推导激活值,如以下矩阵操作所示。这会生成一个大小为(1 x m)的激活矩阵 A。

反向传播
反向传播或称为反向传播是一种计算每个参数对最终预测错误或损失的贡献的技术。通过计算损失函数对每个模型参数的梯度来评估各个损失的贡献。函数的梯度或导数是该函数相对于一个参数的变化率或斜率,同时将其他参数视为常数。当在特定的参数值或点上进行评估时,梯度的符号指示函数增加的方向,梯度的大小指示斜率的陡峭程度。如下所示的对数损失函数是一个碗状的凸函数,具有一个全局最小点。因此,在大多数情况下,对数损失函数的梯度相对于参数指向全局最小值的相反方向。一旦评估了梯度,就使用参数的梯度更新每个参数值,通常使用称为梯度下降的技术。

每个参数的梯度使用链式法则计算。链式法则使得能够计算由其他函数组成的函数的导数。在逻辑回归的情况下,对数损失 L 是激活‘a’和真实标签‘y’的函数,而‘a’本身是‘z’的 sigmoid 函数,‘z’是权重‘w’和偏置‘b’的线性函数,这意味着损失函数 L 是由其他函数组成的函数,如下所示。

利用偏导数链式法则,权重和偏置参数的梯度可以如下计算:

单个输入实例的梯度推导
在我们回顾作为更新参数的一部分的矩阵和向量表示之前,我们将首先使用单个输入实例推导梯度,以便更好地理解这些表示的基础。
假设‘a’和‘z’表示单个输入实例的计算值,并且真实标签为‘y’,则损失函数相对于‘a’的梯度可以推导如下。请注意,这个梯度是评估链式法则以推导参数梯度所需的第一个量。

给定损失函数相对于‘a’的梯度,可以使用以下链式法则推导损失函数相对于‘z’的梯度:

上述链式法则意味着必须推导出‘a’相对于‘z’的梯度。请注意,‘a’是通过对‘z’应用 sigmoid 函数计算得出的。因此,‘a’相对于‘z’的梯度可以通过如下所示的 sigmoid 函数表达式推导出来:

上述推导以‘e’为基础,似乎需要额外的计算来评估‘a’相对于‘z’的梯度。我们知道‘a’是在前向传播过程中计算的。因此,为了消除任何额外的计算,上述导数可以完全用‘a’表示,如下:

插入用‘a’表示的上述术语,‘a’相对于‘z’的梯度如下:

现在我们有了损失函数对‘a’的梯度以及‘a’对‘z’的梯度,损失函数对‘z’的梯度可以如下评估:

我们在评估损失函数对‘z’的梯度方面已经取得了很大进展。我们仍然需要评估损失函数对模型参数的梯度。我们知道‘z’是模型参数和输入实例‘x’特征的线性组合,如下所示:

使用链式法则,损失函数对权重参数‘wi’的梯度被评估如下:

同样,损失函数对‘b’的梯度被评估如下:

使用梯度的参数更新的矩阵和向量表示
现在我们理解了使用单个输入实例导出的模型参数的梯度公式,我们可以将这些公式表示为矩阵和向量形式,以考虑整个训练批次。我们将首先对损失函数对‘z’的梯度进行向量化,其表达式如下:

上述所有‘m’实例的向量形式是:

同样,损失函数对每个权重‘wi’的梯度可以进行向量化。单个实例的损失函数对权重‘wi’的梯度由下式给出:

上述所有权重在所有‘m’输入实例中的向量形式被计算为‘m’梯度的均值,如下:

同样,损失函数对‘b’的梯度在所有‘m’输入实例中的结果是通过如下方式计算的各个实例梯度的均值:

给定模型权重梯度向量和偏置的整体梯度,模型参数将按以下方式更新。如下所示的参数更新基于称为梯度下降的技术,其中使用了学习率。学习率是优化技术(如梯度下降)中使用的超参数,用于控制每次迭代时对模型参数进行调整的步长。有效地说,学习率充当缩放因子,影响优化算法的速度和收敛性。

结论
从本博客中说明的矩阵和向量表示可以看出,逻辑回归使得一个基本的网络模型能够理解这些矩阵和向量操作的细微工作细节。大多数机器学习库封装了这些琐碎的数学细节,但却在更高层次上暴露了定义良好的编程接口,如前向传播或反向传播。虽然理解所有这些细微的细节可能不是使用这些库开发模型的必要条件,但这些细节确实揭示了这些算法背后的数学直觉。然而,这种理解肯定有助于将底层数学直觉应用到其他模型,如人工神经网络(ANN)、递归神经网络(RNN)、卷积神经网络(CNN)和生成对抗网络(GAN)。
数据流中的矩阵近似
原文:
towardsdatascience.com/matrix-approximation-in-data-streams-7585720e8671
在没有所有行的情况下近似矩阵
·发表于 Towards Data Science ·13 分钟阅读·2023 年 9 月 17 日
--

图片来源:unsplash.com
矩阵近似是数据挖掘和机器学习中一个广泛研究的子领域。许多数据分析任务依赖于获得矩阵的低秩近似。例如,降维、异常检测、数据去噪、聚类和推荐系统。本文将深入探讨矩阵近似的问题,以及在数据不完全时如何计算它!
本文内容部分取自我在 斯坦福大学-CS246 课程 的讲座。希望对你有用。完整内容请见 此处。
数据作为矩阵
大多数在网上生成的数据可以表示为矩阵,其中矩阵的每一行是一个数据点。例如,在路由器中,每个通过网络发送的包都是一个数据点,可以表示为所有数据点矩阵中的一行。在零售中,每次购买都是所有交易矩阵中的一行。

图 1:数据作为矩阵 — 作者提供的图像
与此同时,几乎所有在网上生成的数据都是流式性质的;这意味着数据由外部源以我们无法控制的快速速率生成。想象一下用户每秒在 Google 搜索引擎上进行的所有搜索。我们称这种数据为流式数据;因为它像溪流一样源源不断地涌入。
一些典型流式网页规模数据的示例如下:

图 2:典型的流式网页规模数据的大小 — 作者提供的图像
将流数据视为包含n 行、d 维空间中的矩阵A,其中通常 n >> d。通常 n 是以十亿为单位并不断增加的。
数据流模型
在流模型中,数据以高速到达,一次一行,算法必须快速处理这些项目,否则它们将永远丢失。

图 3:数据流模型 — 图片由作者提供
在数据流模型中,算法只能对数据进行一次遍历,并且需要使用较小的内存进行处理。
秩-k 近似
矩阵A的秩-k 近似是一个秩为k的较小矩阵B,使得B对A进行准确的近似。图 2 展示了这一概念。

图 4:从A获取更小的草图B — 图片由作者提供
B通常被称为A的草图。注意,在数据流模型中,B会比A小得多,以便适合内存。此外,rank(B) << rank(A)。例如,如果A是一个包含 100 亿文档和 100 万词的术语-文档矩阵,那么B可能是一个 1000×100 万的矩阵;即,少 1000 万行!
秩-k 近似必须“准确”地近似A。虽然准确是一个模糊的概念,但我们可以通过各种误差定义来量化它:
1️⃣ 协方差误差:
协方差误差是矩阵 A 的协方差与矩阵 B 的协方差之间差异的 Frobenius 范数或 L2 范数。这个误差在数学上定义如下:

协方差误差定义 — 图片由作者提供
2️⃣ 投影误差:
投影误差是当A中的数据点被投影到B的子空间时的残差的范数。这个残差范数被测量为 L2 范数或 Frobenius 范数:

投影误差定义 — 图片由作者提供
这些误差评估了近似的质量;它们越小,近似效果越好。但它们可以小到什么程度呢?
当我们计算这些误差时,我们必须有一个基准来进行比较。在矩阵草图领域,每个人使用的基准是由奇异值分解(SVD)创建的秩-k 近似!SVD 计算最佳的秩-k 近似!这意味着它在“协方差误差”和“投影误差”上造成的误差最小。
对A的最佳秩-k 近似记作 Aₖ。因此,SVD 引起的最小误差是:

最小秩 k 近似误差 — 图片由作者提供
SVD 将矩阵A分解为三个矩阵:
-
左奇异矩阵 U
-
奇异值矩阵 S
-
右奇异矩阵 V
U 和 V 是正交的,意味着它们的列是单位范数且它们彼此正交;即 U 中每两列(V 中也是)之间的点积为零。矩阵 S 是对角矩阵;只有对角线上的条目是非零的,并且按降序排列。

图 5:奇异值分解 — 图片来自作者
SVD 通过取 U 和 V 的前 k 列以及 S 的前 k 项来计算最佳的秩-k 近似:

图 6:SVD 的秩 k 近似 — 图片来自作者
如前所述,以这种方式计算的 Aₖ 在任何秩为 k 或更低的矩阵 B 中具有最低的近似误差。然而,SVD 是一种非常耗时的方法,如果 A 是 n×d,则需要运行时间 O(nd²),并且不适用于数据流中的矩阵。此外,SVD 对稀疏矩阵效率不高;它在计算近似时没有利用矩阵的稀疏性。
❓现在的问题是我们如何以流式方式计算矩阵近似?
流式矩阵近似方法主要有三大类:
1️⃣ 基于行抽样
2️⃣ 随机投影方法
3️⃣ 迭代草图法
基于行抽样的方法
这些方法从相对于良好定义的概率分布的“重要”行中进行抽样。这些方法的不同之处在于它们如何定义“重要性”的概念。通用框架是它们按以下方式构建草图 B:
-
它们首先给流式矩阵A中的每一行分配一个概率
-
然后他们从A中抽取l行(通常是有放回的)来构建B
-
最后,它们将 B 适当缩放,使其成为 A 的无偏估计

图 7:有放回的行抽样以构建草图 B — 图片来自作者
注意,步骤 1 中分配给行的概率实际上是行的“重要性”。将“重要性”视为与项相关的权重,例如,对于文件记录,权重可以是文件的大小。或者对于 IP 地址,权重可以是 IP 地址发出请求的次数。
在矩阵中,每个项都是一个行向量,其权重是其范数的平方;也称为 L2 范数。有一种行抽样算法根据行的 L2 范数在数据的一次遍历中进行抽样。这个算法被称为“L2 范数行抽样”,其伪代码如下:

图 8:L2 范数行抽样算法 — 图片来自作者
该算法以有放回的方式抽样 l = O(k/ε²) 行,并实现以下误差界限:

图 9:L2 范数行抽样的误差保证 — 图片来自作者
注意,这是一个较弱的误差界限,因为它受限于矩阵 A 的 Frobenius 范数,总体来说可能是一个很大的数值!有一个改进的算法表现更好;我们来看看它。
扩展:有一种变体算法同时采样行和列!它被称为“CUR”算法,并且比“L2-范数行采样”方法表现更好。CUR方法通过从 A 中采样行和列来创建三个矩阵 C、U 和 R。它的工作原理如下:
步骤 1:CUR首先从A中采样几列,每列的采样概率与该列的范数成正比。这形成了矩阵C。

图 10:CUR 算法步骤 1—— 图片由作者提供
步骤 2:然后CUR从A中随机抽取几行,每行的抽取概率与该行的范数成正比。这形成了矩阵R。
步骤 3:CUR 然后计算 C 和 R 的交集的伪逆。这被称为矩阵 U。

图 11:CUR 算法步骤 2,3—— 图片由作者提供
最终,这三个矩阵的乘积,即C.U.R,近似于A,并提供了一个低秩近似。该算法在采样l = O(k log k/ε²)行和列时达到了以下误差界限。

图 12:CUR 误差保证—— 图片由作者提供
注意,与L2-范数行采样相比,这个界限要紧得多。
总结: 行采样方法家族(包括 CUR)通过采样行(和列)来形成低秩近似,因此它们非常直观并形成可解释的近似。
在下一部分,我们将看到另一类数据无关的方法。
基于随机投影的方法
这些方法组的关键思想是,如果将向量空间中的点投影到一个随机选择的适当高维子空间中,则点之间的距离大致保持不变。
Johnson-Lindenstrauss 变换(JLT)很好地描述了这一点:d个数据点在任何维度(例如,对于 n≫d 的 n 维空间)中可以被嵌入到大约log d维的空间中,使得它们的成对距离在某种程度上得以保持。
JLT 的更精确和数学化的定义如下:

图 13:JLT 定义—— 图片由作者提供
有许多方法可以构造一个矩阵 S,以保持成对距离。所有这些矩阵都称为具有JLT 属性。下图展示了一些创建这样的矩阵 S 的常见方法:

图片由作者提供
如上图所示,S的一个简单构造是从N(0,1)中抽取独立随机变量作为S的条目,然后将 S 按√(1/r)进行缩放:

图 14:JLT 矩阵—— 图片由作者提供
这个矩阵具有 JLT 属性 [6]*,我们用它来设计随机投影方法如下:

图 15:随机投影方法 — 作者提供的图片
注意第二步,它将数据点从高维空间投影到低维空间。很容易证明 [6] 该方法生成了无偏的草图:

图 16:随机投影提供了无偏的近似 — 作者提供的图片
随机投影方法在设置 r = O(k/ε + k log k) 时能达到以下误差保证。请注意,它们的界限优于行采样方法。

图 17:随机投影误差界限 — 作者提供的图片
有一类与随机投影类似的工作可以实现更好的时间界限。它被称为 哈希技术 [5]。这种方法采用一个每列只有一个非零条目的矩阵 S,而该条目是 1 或-1。它们计算近似值为 B = SA。

哈希技术 — 作者提供的图片
总结:随机投影方法计算效率高,并且数据无关,因为其计算仅涉及一个随机矩阵 S。相比之下,行采样方法需要访问数据以形成草图。
迭代草图
这些方法在流 A=<a1,a2,…> 上工作,其中每个项目被读取一次,迅速处理且不再读取。读取每个项目时,它们更新草图 B。

图 18:迭代草图方法 — 作者提供的图片
该组的最先进方法称为“频繁方向”,基于 Misra-Gries 算法 查找数据流中的频繁项。接下来,我们首先了解 Misra-Gries 算法如何查找频繁项,然后将其扩展到矩阵。
Misra-Gries 算法用于查找频繁项
假设有一个项目流,我们想找到每个项目的频率 f(i)。

图 19:流中的频繁项计数 — 作者提供的图片
如果我们保持 d 个计数器,我们可以计算每个项的频率。但这不够好,因为在某些领域,如 IP 地址、查询等,唯一项的数量太多了。

图 20:用于项频率估计的 d 个计数器 — 作者提供的图片
所以让我们保持 l 个计数器,其中 l≪d。如果流中到达的新项目在计数器中,我们将其计数加 1:

图 21:增加项的计数器 — 作者提供的图片
如果新项目不在计数器中且我们有空间,我们为其创建一个计数器并将其设置为 1。

图 22:为新项目设置计数器 — 作者提供的图像
但如果我们没有空间容纳新项目(这里的新项目是棕色盒子),我们获得中位数计数器,即位置为l/2的计数器:

图 23:从每一个计数器中减去中间计数器。— 作者提供的图像
并从所有计数器中减去它。对于所有变成负值的计数器,我们将其重置为零。所以它变成如下:

图 24:一半计数器为零 — 作者提供的图像
如我们所见,现在我们有空间容纳新项目,所以我们继续处理流🙂。
在流的任何时刻,项目的近似计数是我们迄今为止保留的计数,例如:

图 25:估计项目计数 — 作者提供的图像
这种方法会低估计数,因此对于任何项目 i,其近似频率小于或等于其真实频率:

与此同时,它的近似频率是下界的,因为每次我们减少时,最多减少l/2位置计数器的计数。

在流中看到n个元素的任何点,我们有:

因此,它提供的错误保证如下:

Misra-Gries 错误界限 — 作者提供的图像
因此,Misra-Gries 对所有真实频率大于2n/l的项目生成一个非零近似频率。例如,要找到出现超过 20% 的项目,我们必须采取l = 10计数器并运行 Misra-Gries 算法。
频繁方向:Misra-Gries 的扩展
现在,让我们将 Misra-Gries 扩展到向量和矩阵。在矩阵的情况下,流中的项目是d维的行向量。在流中的任何时刻n,所有行一起形成一个有n行的高矩阵A。目标是找到A的最重要方向。这些方向对应于A*的前几个奇异向量。一个方向越重要,它在数据点中出现的频率就越高,这就是我们称下一个算法为频繁方向 [2,3]的原因。
频繁方向算法的伪代码如下:

图 26:FrequentDirections — 作者提供的图像
如我们所见,该算法以矩阵A和草图大小l作为输入。然后,在第一步(即第一行突出显示的蓝色部分),算法将草图B初始化为空矩阵,具有l行。然后对于流中的每一行A,算法将其插入B中,直到B满为止:

图 27:当草图 B 满时 — 作者提供的图像
然后我们计算B的SVD;这将产生左奇异矩阵U、奇异值矩阵S和右奇异矩阵V。注意,U和V提供了子空间的旋转,因为它们是正交矩阵。

图 28: B 的 SVD — 作者提供的图像
然后我们通过从所有奇异值的平方中减去中间奇异值的平方来降低B的秩!注意,这一步类似于 Misra-Gries 中的部分操作,我们从所有计数器中减去中间计数器。从所有奇异值的平方中减去中间奇异值的平方使得一半的奇异值变为零。

图 29: 降低 B 的秩 — 作者提供的图像
然后我们将S(奇异值矩阵)乘以V 转置(右奇异矩阵)并将其分配给B。换句话说,我们通过去掉左奇异矩阵U来重构B。这种操作的效果是得到一个新的矩阵B,其一半的行为空。这是好消息,因为它为流矩阵A中的下一行提供了空间。
误差保证:类似于频繁项的情况,该方法具有以下误差保证,其中l是草图大小,k是秩:

FrequentDirections 协方差误差界限 — 作者提供的图像

FrequentDirections 投影误差界限 — 作者提供的图像
比较第二个误差界限与随机投影和行采样的误差界限。注意这是一个更紧凑和更好的误差界限。
实验:实验[2,4]表明,Frequent Directions 算法优于上述讨论的所有其他流式算法。以下是与协方差误差界限相关的实验:

图 30: 协方差误差中的实验 — 来自 [2] 的图像
这是关于投影误差界限的实验:

图 31: 投影误差中的实验 — 来自 [2] 的图像
本文到此结束。正如我们所见,Frequent Directions 不仅在近似误差中优于所有其他方法,而且使用的空间最少。换句话说,它在实现的误差界限方面是空间最优的。
总结
数据流中的低秩矩阵近似是计算低秩矩阵近似的问题,其中矩阵的行以流式方式到达。这意味着无法一次性访问矩阵的所有行,也不知道矩阵的大小。有三种主要的近似方法类别:行采样——随机投影——迭代草图。虽然第一组方法是最直观的,因为它们采样实际数据点,第二组方法在运行时效率最高。最先进的方法(SOTA)属于第三组,称为频繁方向。该方法基于频繁项估计的旧方法,并且在误差界限方面具有空间最优性。
如果你有任何问题或建议,请随时联系我:
电子邮件:mina.ghashami@gmail.com
LinkedIn:www.linkedin.com/in/minaghashami/
参考文献
GPU 上的矩阵乘法
如何在 CUDA 中实现最先进的矩阵乘法性能。
·
关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 10 月 9 日
--
“从矩阵乘法中汲取灵感的极简艺术,风格为 vaporwave” —— DALLE-2
这篇博客源于我突然意识到自己对矩阵乘法在 GPU 上如何运作知之甚少。做了这么多机器学习项目,我觉得我应该了解这个在机器学习中最重要的操作是如何工作的:什么是“张量核心”?为什么每个人都说“数据移动是瓶颈”?GPU 实际上能有多快?
为了回答这些问题,我决定必须走出我的 PyTorch 领域,深入探索 CUDA 的深渊。我写了这篇博客来记录我所学到的一切,希望读到这篇文章的任何人都不必像我一样经历挖掘 CUDA 文档/代码的痛苦。
如果我在这段旅程中学到了什么,那就是并发矩阵乘法是困难的。高效的矩阵乘法在很大程度上依赖于你使用的具体硬件和你尝试解决的问题规模。没有一刀切的解决方案。
够了,让我们深入了解吧!
回顾 GPU 架构
让我们回顾一下(NVIDIA)GPU 的工作原理。GPU 通过运行许多线程来实现并行处理。每个线程在一个 CUDA 核心上执行,但在某一时刻,只有一部分线程是活动的,因此可能有比可用的 CUDA 核心更多的线程。每个线程,无论是否活动,都有自己的寄存器。
一组 32 个线程称为warp。warp 中的所有线程必须一起执行(或一起处于非活动状态)。在大多数情况下,非活动 warp 的数量远多于活动 warp,而warp 调度器负责选择在特定时间执行哪些 warp。这使得 GPU 能够通过调度其他 warp 在 warp 等待数据时执行,从而隐藏内存访问的延迟。
一组 warp 称为线程块。所有线程块中的 warp 在同一个流处理器(SM)中执行。每个线程块有自己的共享内存,所有线程块中的线程都可以访问。
注意:较新的架构
从 Volta 架构开始,每个线程也有自己的程序计数器和调用栈等。这意味着 warp 中的每个线程可以同时执行不同的指令。
Volta 架构还引入了Tensor Cores,这些核心专门用于解决特定大小的矩阵乘法。每个活动 warp 可以访问一个 Tensor Core。
在最新的 Hopper 架构中,引入了线程块集群的概念,它表示一组线程块。它使用户能够更细粒度地控制线程块的调度,并允许一个线程块的共享内存在同一集群中的其他线程块访问。
并行化矩阵乘法
假设我们想计算:

我们说在这种情况下问题的规模是(M, N, K)。为了并行化这个操作,我们可以将A和B拆分成更小的矩阵,分别进行矩阵乘法,然后将结果连接起来形成C。
具体来说,我们可以按行分割A(即,将M分成大小为M’的块)和按列分割B(即,将N分成大小为N’的块),得到:

我们可以看到C中的每个子矩阵彼此独立,因此我们可以轻松地并行计算每个子矩阵。
实际上,K可能过大,无法直接加载到内存中进行计算。相反,典型的实现也会将K分割成大小为K’的块,迭代每个块,并对部分结果进行累加(求和)。这被称为串行-K归约。(与parallel-K reduction相对,见下节)。从数学上看,这样表示:

注意:Padding
在任何问题大小不能被分区大小整除的情况下,我们需要添加padding。这通常在我们将分区输入(𝐴ᵢ,ₖ 和 𝐵ₖ,ⱼ)加载到低级内存时隐式完成,我们通过添加零确保加载的分区(𝐴ᵢ,ₖ的大小为 M’×K’,𝐵ₖ,ⱼ的大小为 K’×N’)总是“满”的。在将结果写回全局内存时需要特别小心,以避免越界错误。
从高层次看,三层嵌套分区用于在 GPU 上并行化矩阵乘法:
-
第一次分区发生在threadblock级别。每个线程块负责计算Cᵢ,ⱼ = Aᵢ Bⱼ。
-
第二次分区发生在warp级别。线程块级别的问题Cᵢ,ⱼ 进一步被分区,每个 warp 负责计算Cᵢ,ⱼ⁽ᵐⁿ⁾ = Aᵢ⁽ᵐ⁾ Bⱼ⁽ⁿ⁾。
-
第三次分区发生在instruction级别。有些指令需要特定大小的输入。例如,第二代 Tensor Cores 操作大小为(16, 8, 8)的fp16问题,而在 CUDA 核心上通过标量乘法直接实现则仅操作大小为(1, 1, 1)的问题。因此,warp 级别的问题被进一步分区,使得每个块有适合指令的大小:Cᵢ,ⱼ⁽ᵐⁿ⁾⁽ᵃᵇ⁾ = Aᵢ⁽ᵐ⁾⁽ᵃ⁾ Bⱼ⁽ⁿ⁾⁽ᵇ⁾。
我们需要三个分区级别是有充分理由的,正如我们在下一节中将看到的。
数据冗余
矩阵乘法如果我们每次计算时都从全局内存重新获取数据,很容易变成内存瓶颈。关键观察是,许多子输入Aᵢ和Bⱼ在不同的子矩阵乘法中被重复使用。例如,Aᵢ需要用于Cᵢ,₁ , Cᵢ,₂ , ... 和Bⱼ需要用于C₁,ⱼ , C₂,ⱼ , … 。如果我们能最小化冗余数据移动并尽可能多地重用加载的数据,就能获得最佳的吞吐量。
在 CUDA 中,有三种用户可访问的内存类型:

下面是每种内存类型如何使用的高级视图:
-
每个线程块将首先从全局内存加载其所需输入到共享内存中。之后对这些数据的访问将由共享内存提供,而不是较慢的全局内存。
-
在每个线程块中,每个 warp 将首先从共享内存加载其所需输入到寄存器中。随后对这些数据的访问将直接由快速寄存器提供。
深入细节
线程块级别
在线程块级别,问题被划分为大小为 (M’, N’, K’) 的子问题。因此,每个线程块负责计算 C 的一个片段,记作:

通过将子输入 Aᵢ,ₖ 和 Bₖ,ⱼ 加载到共享内存中来最小化冗余的数据移动。当我们完成 Aᵢ,ₖ Bₖ,ⱼ 的计算后,下一个块 (Aᵢ,ₖ₊₁ 和 Bₖ₊₁,ⱼ) 将被加载。
warp 级别
在 warp 级别,子问题进一步划分为大小为 (M’’, N’’, K’’) 的子子问题。因此,每个 warp 负责计算 Cᵢ,ⱼ, 记作 Cᵢ,ⱼ⁽ᵐ ⁿ⁾:

通过将子子输入 Aᵢ,ₖ⁽ᵐ ˡ⁾ 和 Bₖ,ⱼ⁽ˡ ⁿ⁾ 加载到寄存器中来最小化冗余的数据移动。任何对 Aᵢ,ₖ⁽ᵐ ˡ⁾ 和 Bₖ,ⱼ⁽ˡ ⁿ⁾ 的访问在warp 内将由快速寄存器提供服务。
注意:在寄存器之间分配数据
值得注意的是,寄存器是线程级的。这意味着寄存器中的输入不能被 warp 中的其他线程访问。如何将 Aᵢ,ₖ⁽ᵐ ˡ⁾ 和 Bₖ,ⱼ⁽ˡ ⁿ⁾ 分配到每个线程的寄存器中,取决于使用的具体指令。NVIDIA 文档中的 Warp Level Matrix Multiply-Accumulate Instructions 对每条指令进行了详细描述。
张量核心级别
为了实际执行矩阵乘法,我们使用 GPU 上的张量核心。我的 GPU (RTX 2060) 具有第二代张量核心,专门解决大小为 (M’’’, N’’’, K’’’) = (16, 8, 8) 的 fp16 问题。因此,我们进一步将 Cᵢ,ⱼ⁽ᵐ ⁿ⁾ 划分为符合指令预期大小的片段:

在这里,所有输入已经在寄存器中,因此数据移动开销最小。
注意:张量核心
张量核心操作是warp 级指令,意味着 warp 中的所有线程需要同时执行张量核心指令,协同准备要被一个张量核心消费的数据。
选择分区大小
所以,鉴于我们希望最小化数据移动,我们应该选择尽可能大的分区大小来利用所有的共享内存和寄存器,对吗? 其实并不是这样。
线程块分区大小
从渐近的角度来看,随着问题大小的增加,是的,我们确实希望尽可能使用更多的共享内存和寄存器。然而,对于小问题大小,我们可能会遇到两个问题:
-
大的分区大小意味着我们将有更少的线程块。由于每个线程块只能在一个 SM 上执行,这可能意味着我们不能利用所有的 SM。
-
对于不能被分区大小整除的问题大小,我们需要为输入添加更多的填充。这会降低效率,因为对有意义的输入计算较少。
典型的实现可能使用分区大小为 (M’, N’, K’) = (128, 256, 32)。
Warp 分区大小
通常,较大的 warp 分区大小意味着会有更少的冗余数据移动,但代价是拥有更少的 warps。拥有过少的 warps 意味着我们将无法隐藏内存访问的延迟(因为当当前 warp 等待数据时,我们可能没有其他 warp 来调度)。
典型的实现可能使用分区大小为 (M’’, N’’, K’’) = (64, 64, 32)。
指令分区大小
这完全取决于你的 GPU 支持什么指令。对于我的 RTX 2060,fp16 Tensor Core 矩阵乘法(带有 fp16 累积)的 ptx 指令是 mma.sync.aligned.m16n8k8.row.col.f16.f16.f16.f16,它期望输入的大小为 (16, 8, 8)。
进一步优化
上述技术在问题规模较大时可以使我们接近 GPU 的理论峰值性能。然而,对于较小的问题规模,它们的效率不是很高。进一步提高矩阵乘法性能的两种常见技术是 并行-K 减少 和 软件流水线。
并行-K 减少
在 M 和 N 较小时,我们可能只有少量的线程块。例如,如果 (M’, N’) = (128, 256) 且原始问题规模具有 M ≤ 128 和 N ≤ 256,我们将只有一个线程块,因此我们只利用了 GPU 计算能力的一小部分!(例如,我的 RTX 2060 有 30 个 SM,因此为了最大化利用率,我们希望至少有 30 个线程块。)
在 K 较大(尽管 M 和 N 较小)的情况下,我们可以通过进行 并行-K 减少 来利用更多的并行性。回想一下,在串行-K 减少中,每个线程块遍历以下和:

并将中间结果累积到 Cᵢ,ⱼ。在并行-K 减少中,我们将每个线程块分配为仅计算 一个和的元素(即 Aᵢ,ₖ Bₖ,ⱼ)。这使我们可以将线程块的数量增加 K/K’ 倍,从而利用更多的 SMs。
需要注意的是,现在我们需要 分配更多内存 来存储每个线程块的结果,并且 调用第二个内核 来对部分结果进行最终的归约以获得 Cᵢ,ⱼ。
软件流水线
通常,CUDA 通过调度其他 warps 执行来隐藏内存访问的延迟,而当一个 warp 等待数据时。这要求我们拥有足够的 warps 来掩盖延迟。
然而,在进行 GEMM 时,warps 的数量通常相对较少。这是因为 warp 的数量受到“每个线程块的可用寄存器数除以每个 warp 需要的寄存器数”的限制。对于矩阵乘法,我们使用大量寄存器以最大化数据重用。因此,我们可能没有足够的 warps 来掩盖延迟。
“累加器元素通常占用至少一半的线程总寄存器预算。” — CUTLASS 文档
为了缓解这一效果,我们可以使用软件流水线。本质上,我们可以(手动)使用特殊指令异步预加载下一个迭代的输入。在输入被加载的同时,我们可以继续在当前迭代上进行计算。其总结如下图所示:

来自CUTLASS的软件下载流水线
这得益于 GPU 的特性:它像任何现代 CPU 一样,可以在没有数据依赖关系的情况下流水化内存访问和算术操作。这被称为指令级并行。
矩阵乘法的实际应用
如果你想了解这些概念如何在实际实现中结合起来,可以查看我用 CUDA 从零开始训练 MNIST 的实现。在那里,我使用 CUDA 训练了一个多层感知器,并在隐藏层大小为 128 时实现了比优化后的 PyTorch 快 6 倍:
[## GitHub - andylolu2/cuda-mnist
通过在 GitHub 上创建账户来参与 andylolu2/cuda-mnist 的开发。
参考资料
1. CUTLASS 文档
2. CUDA 文档
3. CUTLASS 示例
通过选择最佳图表:网络图、热图还是桑基图来最大化你的洞察力?
美丽的可视化是很棒的,但为了最大化其可解释性,你需要仔细选择图表。
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 8 月 13 日
--

图片来源于 David Pisnoy 在 Unsplash
可视化是数据分析中的重要部分,因为它可以将数据转化为洞察力,并帮助你讲述故事。在这篇博客文章中,我将重点介绍网络图、热图和桑基图。这些图表使用相同的输入数据,但我们应该记住,它们是为特定目标设计的,因此其可解释性可能有所不同。 我将描述网络图、热图和桑基图之间的差异、应用,并通过实际示例展示它们的可解释性。 所有示例均使用 D3Blocks library 在 Python 中创建。
输入数据用于 热图和桑基图。
作为数据科学家,一个常见但重要的任务就是制作图表。有时这些图表作为理智检查,有时它们会出现在演示文稿中,成为故事的基础。尤其是在后者的情况下,我们的目标是将复杂的信息转化为逻辑的图形可视化。
创建图表就像摄影一样。你想捕捉到讲述故事的风景。
然而,决定使用哪种图表并不总是容易的,因为尽管图表可能有类似的输入,但它们设计用于描述场景的特定部分。三个图表的输入需要 ***source***、 ***target****** 和*** ***weight*** 信息。下面展示了一个小示例。它描述了变量(或节点)之间的连接及其强度。换句话说,Penny 与 Leonard 之间的连接强度为 5。第二个节点的名称再次是 Penny,它也与 Amy 连接,但强度略低,值为 3,依此类推。
# Source node names
source = ['Penny', 'Penny', 'Amy', 'Bernadette', 'Bernadette', 'Sheldon', 'Sheldon', 'Sheldon', 'Rajesh']
# Target node names
target = ['Leonard', 'Amy', 'Bernadette', 'Rajesh', 'Howard', 'Howard', 'Leonard', 'Amy', 'Penny']
# Edge Weights
weight = [5, 3, 2, 2, 5, 2, 3, 5, 2]
节点即为 source 和 target 的联合名称集。边表示 source 和 target 之间的关系。图表可以处理有向或无向的源-目标值关系。weight 值描述了关系的强度。值得注意的是,source-target-weight 的值也可以是(稀疏)邻接矩阵的形式,其中列和索引是节点,值大于 0 的元素被认为是边。这种形式通常用于热力图的创建,但它本质上包含相同的信息。在下一节中,我将描述这些信息如何转换为图表。
# Install d3blocks for the following examples
pip install d3blocks
# Install cluster evalation (required for the heatmaps)
pip install clusteval
# Import
from d3blocks import D3Blocks
# Initialize
d3 = D3Blocks()
# Convert
adjmat = d3.vec2adjmat(source, target, weight)
# Print
print(adjmat)
# target Amy Bernadette Howard Leonard Penny Rajesh Sheldon
# source
# Amy 0.0 2.0 0.0 0.0 0.0 0.0 0.0
# Bernadette 0.0 0.0 5.0 0.0 0.0 2.0 0.0
# Howard 0.0 0.0 0.0 0.0 0.0 0.0 0.0
# Leonard 0.0 0.0 0.0 0.0 0.0 0.0 0.0
# Penny 3.0 0.0 0.0 5.0 0.0 0.0 0.0
# Rajesh 0.0 0.0 0.0 0.0 2.0 0.0 0.0
# Sheldon 5.0 0.0 2.0 3.0 0.0 0.0 0.0
图表以不同的方式翻译数据。
网络图、桑基图和热力图 各有其特性,因此可以以不同的方式呈现相同的数据。简要总结如下:
- 网络图 直观地展示实体之间的关系,其中节点代表实体,边代表它们之间的关系。优点:这种图表适用于理解复杂的行为,并且你也需要知道(一些)实体之间的确切关系。缺点 是当数据集较大时,图表会变得混乱且难以阅读。然而,通过使用不同的布局或按权重拆分网络,它可以再次变得有效。有关如何使用交互功能的更多详细信息,请阅读以下博客 [1]。
应用于 D3 力导向网络图
-
热力图 有效地可视化变量之间关系的强度或大小,其中值由(不同的)颜色表示。 优点: 这种类型的图表对识别具有多个变量的大数据集中的模式和趋势非常有用。当网络图变得复杂时,热力图可以提供结构化的见解。 缺点: 你很容易失去对个别关系的跟踪。然而,当你提供清晰的标签并对行和/或列进行聚类时,变量之间的关系可以更容易解读。
-
桑基图 可以 直观地显示数据或资源在实体之间的流动,其中节点代表不同的阶段或实体,链接代表数据或资源在它们之间的流动。 优点: 对于理解复杂的过程或系统以及识别优化或改进的领域非常有用。 缺点 是当阶段或实体过多时可能会变得难以阅读。有关更多详细信息,请阅读以下博客[2]:
## 使用 Python 在 d3js 中创建美丽的桑基图的实践指南。
桑基图是一种出色的方法,通过查看个别项目在各状态之间的流动,可以发现最突出的贡献。
towardsdatascience.com
网络图、热力图和桑基图的应用
网络图、热力图和桑基图可以使用D3Blocks 库创建。有关 D3blocks 的更多详细信息,请参阅[3]:
## D3Blocks: 用于创建互动和独立的 D3js 图表的 Python 库
创建基于 d3 javascript(d3js)图形但可以用 Python 配置的交互式和独立图表。
towardsdatascience.com
网络图、热力图和桑基图的应用各不相同。网络图常用于可视化社交媒体网络,例如 Twitter 帖子或 Facebook,其中节点代表用户,边代表他们之间的关系。热力图用于许多数据点较多的应用场景,如股票价格、基因表达数据和气候数据等。桑基图用于可视化流量,例如客户旅程数据中不同阶段(例如,网站访问、注册、购买)。另一个例子是能源流动或供应链流动,涉及能源的不同来源和用途或供应链的不同阶段(例如,原材料、制造、分销)。
网络图、热力图和桑基图的实操比较
让我们加载能源数据集[4],并比较这三种图表的可解释性。能源数据集包含 48 个节点和 68 个加权(无向)关系,我们可以可视化能源流动。你会发现网络图使理解角色之间的确切关系变得容易。另一方面,热力图展示了所有关系的整体视图,而桑基图则显示了角色之间的流动。例如,在这个数据集中,John似乎是一个重要的角色,在网络图中占据中心点,并且有许多流动进出。你可以使用以下代码块重现这些结果:
# ######################
# Create network graph #
# ######################
# Load library
from d3blocks import D3Blocks
# Initialize
d3 = D3Blocks()
# Load energy data sets
df = d3.import_example(data='energy')
# Create the network graph
d3.d3graph(df, cmap='Set2')
# Extract the node colors from the network graph.
node_colors = d3.D3graph.node_properties

使用 D3Blocks 创建的 D3graph。交互式 HTML 版本可以在我的Github 页面上查看。
热力图的聚类使用clusteval 库[5]创建。该库使用聚类评估指标,如轮廓系数、DB 指数或 DBscan,来确定最优的聚类切割点。默认值可以如代码部分所示进行更改。数据已进行 z-score 标准化。
# ################
# Create Heatmap #
# ################
# Initialize
d3 = D3Blocks()
# Load Energy data sets
df = d3.import_example(data='energy')
# Create the default heatmap but do hide it. We will first adjust the colors based on the network colors.
d3.heatmap(df, showfig=False)
# Update the colors of the network graph to be consistent with the colors
for i, label in enumerate(d3.node_properties['label']):
if node_colors.get(label) is not None:
d3.node_properties['color'].iloc[i] = node_colors.get(label)['color']
# The colors in the dataframe are used in the chart.
print(d3.node_properties)
# Make the chart
d3.show(showfig=True, figsize=[600, 600], fontsize=8, scaler='zscore')
# You can make adjustments in the clustering:
d3.heatmap(df, cluster_params={'evaluate':'dbindex',
'metric':'hamming',
'linkage':'complete',
'normalize': False,
'min_clust': 3,
'max_clust': 15})

使用 D3Blocks 创建的热力图。交互式 HTML 版本可以在我的Github 页面上查看。
# ###############
# Create Sankey #
# ###############
# Initialize
d3 = D3Blocks()
# Create sankey graph
d3.sankey(df, showfig=True)

使用 D3Blocks 创建的桑基图。交互式 HTML 版本可以在我的Github 页面上查看。供应在左侧,需求在右侧。链接显示了能源在被消耗或丢失之前是如何转换或传输的。
我们还可以调整节点的颜色以与其他图表匹配。
# Initialize
d3 = D3Blocks(chart='Sankey', frame=True)
# Load data set
df = d3.import_example(data='energy')
# Set default node properties
d3.set_node_properties(df)
# Update the colors of the network graph to be consistent with the colors
for i, label in enumerate(d3.node_properties['label']):
if node_colors.get(label) is not None:
d3.node_properties['color'].iloc[i] = node_colors.get(label)['color']
# The colors in the dataframe are used in the chart.
print(d3.node_properties)
# id label color
# 0 Agricultural_waste #66c2a5
# 1 Bio-conversion #66c2a5
# 2 Liquid #e5c494
# 3 Losses #e78ac3
# 4 Solid #66c2a5
# 5 Gas #fc8d62
# ...
# Create edge properties
d3.set_edge_properties(df, color='target', opacity='target')
# Show the chart
d3.show()

使用 D3Blocks 创建的桑基图。节点颜色与其他两个图表匹配,边缘颜色自动设置。
通过交互式图表提高可解释性
使用交互式图表可以帮助增强解释和/或突出感兴趣的区域。一种方式是平移和缩放功能,这在 d3graph 图表中也得到了演示。另一种获得更多洞察的方法是使用自动创建的滑块根据边的强度来拆分网络。这使我们能更快地理解节点之间的关系。
使用堆叠方法构建你的故事
你可能已经从前面的部分注意到,没有一种最佳的图表适用于所有用例。通常,使用堆叠方法从不同的角度和/或深度描述数据是有益的。例如,你可以开始使用热力图来展示整体的弱关系和强关系。然后,选择一个感兴趣的集群或区域,使用网络图更深入地分析精确关系。最后,如果你现在需要描述节点之间的流动和依赖关系,可以使用 Sankey 图。
摘要
总之,选择正确的可视化技术对于有效洞察数据集至关重要。图表的选择取决于数据集的类型以及研究问题。在本博客中,我们比较了 3 种流行的可视化图表:网络图、热力图和 Sankey 图,并使用了实际示例。需要注意的是,创建图表是分析中的重要部分。如果你特别关注解释,故事情节可以更有效地传达给观众。
保持安全。保持冷静。
干杯,E.
如果你觉得这篇文章有帮助,欢迎 关注我 ,因为我会写更多关于可视化技术的文章。如果你考虑订阅 Medium 会员,可以通过我的 推荐链接支持我的工作。价格相当于一杯咖啡,但允许你每月无限制阅读文章!
让我们联系吧!
参考文献
-
E. Taskesen, 使用 Python 创建美丽的独立交互式 D3 图表,Medium(Towards Data Science),2022 年 2 月
-
E. Taskesen, 动手指南:用 Python 在 d3js 中创建美丽的 Sankey 图表,Medium(Towards Data Science),2022 年 10 月
-
E. Taskesen, D3Blocks: 用于创建交互式和独立 D3js 图表的 Python 库,Medium(Towards Data Science),2022 年 9 月
-
E. Taskesen, 从数据到聚类:你的聚类何时足够好? Medium, 2023 年 4 月
随机变量参数的最大似然估计
原文:
towardsdatascience.com/maximum-likelihood-estimation-4a1a866dfa70
通过观察数据的最高似然性来建模参数
·发表于 Towards Data Science ·11 分钟阅读·2023 年 12 月 10 日
--

Francesco Ungaro 摄影: www.pexels.com/photo/blue-and-white-abstract-painting-1912832/
概率和统计学中的概念由于高水平的数学、糟糕的符号表示以及随机变量和数据的交织而显得有些难以捉摸。本文阐明了在估计量、估计值、偏差和方差,以及最大似然估计方法的背景下,随机变量和数据之间的关系。
本文将分为以下几个部分。
-
概率质量和分布函数的参数
-
估计量
-
估计值
-
偏差和方差
-
最大似然估计
-
无耻的推广 Quant Guild
概率质量和分布函数的参数
本文不是关于常见随机变量的入门文章(这篇文章是为了这个目的)。我建议你阅读那篇文章,或在继续之前具备基本概率(公理、质量/分布函数等)的扎实基础。
让我们以医院接收病人的例子来讨论。
假设我们是一名医院风险管理人员、高级医生统计师、数据科学护士(我确实不知道谁会负责这个),我们想估计没有足够的病床来接纳病人的风险。将某一天检查入住的病人数建模为泊松随机变量是合理的。也就是说,我们假设每天检查入住医院的病人数服从泊松分布。

作者生成的图像
X是1、2、…、n天住院的病人数量。
我现在将简化问题空间,以免出现愤怒的评论:
-
假设所有住院病人当天都会出院
-
假设每天入住的病人彼此独立
这实际可行吗?可能不行——但它确实能帮助我们以简化的方式前进。这些是建模风险时需要考虑的事情,你的假设如何影响模型。
在这些假设下,我们很可能会低估风险。为什么?因为人们总是会在同一天出院吗?绝对不会,所以在这种情况下我们高估了床位的可用性。独立性呢?人们会互相传染吗?我在这里偏题了,因为这不是一篇关于假设和违背假设的文章。
好的,考虑到所有给定的假设,我们知道病人遵循一个泊松分布,其概率质量函数如下。

作者生成的图像
因此,对于某一天的病人数量 a,我们可以找出看到该数量病人的相关概率。
此外,假设我们只有 100 张床位,根据历史病人数据,我们如何估计无法接纳病人的概率?我们将在回答一个更紧迫的问题后回到这个想法。
特别敏锐的人,或者那些稍微关注一点的人,会问出以下某种变体的问题
我们如何估计参数 lambda?我们选择什么值?
啊,是的,现在我们可以讨论估计量了。
估计量
一个估计量是任意一组随机变量的函数。重要的是要注意,估计量是一个函数,该函数在一组数据上给出估计值。请允许我详细说明。
以下方程是一个估计量

作者生成的图像
希望以函数形式书写有助于传达要点。这个函数,即样本均值,是总体均值的一个估计量。请注意,这这是一个任意一组独立且服从标准正态分布的随机变量的函数。
让我们对估计量应用期望算子

作者生成的图像
我们可以看到,这个估计量的期望值为零,这也与给定标准正态随机变量集的总体参数一致。请注意,我们在这里有总体参数,在我们上面的医院示例中,我们缺少总体参数 lambda,因此它必须被估计。稍后会详细讨论。
以下也是一个估计量

作者生成的图像
其中 zeta 是一个任意常数。
我们可以应用相同的期望算子

由作者生成的图像
期望值现在是 zeta,注意这与总体参数不一致。
我们这里有一个有趣的情况。我们知道总体参数,因此自然希望我们的估计量的期望值能够得到控制我们数据的总体参数。直观上,这是第一个没有 zeta 的估计量。
但如果我们不知道总体参数呢?如果我们不知道总体参数,不添加像 zeta 这样的任意常数是否是最优的?
再次,我们将很快在偏差和方差的背景下回到这个概念。
估计
这一部分很简单,我保证会很简短。估计是数据的一个函数,其中函数是给定的估计量。
需要注意的是,这些估计有其自身的分布,因为它们是我们提供给估计量的数据的函数。
假设我们有来自标准正态分布的以下实现随机变量

由作者生成的图像
这是数据,没有任何不确定性。我们知道支配分布,并且知道每个z的值。根据这些数据,我们可以构造对总体均值的估计(我们刚好知道这个均值)。这是通过在实现的数据上应用g函数得到的。
这里有一个 Python 脚本来完成这个任务。
由作者生成的代码
Population Mean Estimate: -0.016672307230033958
注意,在这种情况下,n=1000,我们构造了一个总体均值的估计(我们知道这个均值是零),结果非常接近。每一个新的数据样本将产生不同的估计——然而,我们知道在期望值上,这些估计将与总体均值对齐。
这就是当说估计量遵循它们的自身分布时的含义。如果我们记录一系列 n=1000 个样本并创建直方图,我们实际上会观察到样本均值的分布。
这里涉及大数法则和中心极限定理的影响——如果你对这些概念在完全不同的背景下的直观解释感兴趣,请参见以下视频。
由作者生成的视频
我们现在可以回到文章前面提出的问题。
在我们的医院例子中,我们需要估计参数,我们知道可以使用估计量来估计该参数。
我们面临的真正问题是,我们应该使用什么估计量,以及如何评估这个估计量?
偏差和方差
偏差指的是估计量对被估计参数的期望偏离。这听起来比较复杂,我们来看看数学公式。
假设我们有一系列独立且同分布的正态随机变量N。我们可以为总体均值 theta 构造一个估计。

作者生成的图像
正式来说,偏差是估计量的期望与真实值之间的差异。

作者生成的图像
在这种情况下,tau 等于总体均值 mu,因为这是我们试图估计的真实值。现在我们找出我们的估计量的期望值。

作者生成的图像
因此

作者生成的图像
在这里,我们发现估计量 theta 对于总体均值是无偏的。
在以下情况下,出现了一个必要的问题:
一个具有零偏差和大量方差的估计量是否比一个具有小偏差和极低方差的估计量更好?
哪个估计量更好?如果有一个考虑了偏差和方差的度量就好了…
均方误差 (MSE)
均方误差考虑了估计量与被估计值之间的期望(平方)距离。

作者生成的图像
MSE 实际上简化为方差与偏差的平方的组合。

作者生成的图像
我们现在有了回答总体问题的方法:我们应该选择哪个估计量? 也就是说,我们可以比较估计量之间的 MSE,以确定在偏差和方差的背景下哪个可能更好。
在机器学习的背景下,我们面临着偏差-方差权衡的著名问题。当模型有更多的预测变量时,它们往往对训练数据的估计误差(偏差)较低——然而,在拟合新的训练样本时,这些估计的方差往往会更大。权衡指的是同时最小化偏差和方差的问题。
或许我们将在另一篇文章中更深入地探讨这个话题。现在,让我们最后讨论如何利用最大似然估计构造一个有效的估计量。
最大似然估计
目前为止在本文中所花费的时间为为什么最大似然估计有用奠定了基础。让我们回顾一下。
-
我们有一个有 100 张床位的医院,并且有关于每天检查和出院患者数量的数据,我们想用泊松分布来建模这个随机变量。
-
我们知道泊松分布由 lambda 参数化,但我们应该如何估计这个参数呢?我们应该选择什么估计量?
-
我们知道我们希望选择的估计量在偏差和方差的背景下具有较低的 MSE,但我们如何凭空构造这样的估计量呢?
幸运的是,我们回顾中的最后一个问题可以通过最大似然估计的方法来回答。MLE 在估计量中具有几个理想的特性,例如
-
MLE 提供渐近零偏差
-
MLEs 在其数据的统治分布下是渐近正态的
-
MLEs 提供的方差对所有渐近正态估计量来说是尽可能低的
这里的渐近指的是给定数据集的样本量的任意增加。
但是MLE到底是什么?对于我们估计的任何参数,我们都是在寻找使观察到的数据的似然性最高的估计。在随机变量的背景下进行这种操作将得到 MLE 估计量,将该估计量应用于数据将得到 MLE 估计。
现在让我们推导泊松随机变量的最大似然估计量。之后,我们可以使用这个估计量来估计 lambda。

作者生成的图像
在这里,我们可以使用之前假设的独立性概念,找到多变量函数作为泊松随机变量的单独概率质量函数的分解乘积。

作者生成的图像
我们通常使用对数似然,因为它使计算更简单。

作者生成的图像
不过,不要混淆,以下解法是等效的。

作者生成的图像
现在让我们代入 theta 的概率质量函数。

作者生成的图像
利用对数的性质,我们可以将乘积分解为求和。

作者生成的图像
我们现在希望通过选择一个合适的 lambda 值来最大化这个概率。这成为了一个微积分中的优化问题,我们对 lambda 求导并将方程设为零——求解 lambda。

作者生成的图像

作者生成的图像

作者生成的图像

作者生成的图像
真棒!总体参数 lambda 的 MLE 是样本均值。使用 Python,我将展示如何将这一切结合起来。
-
给定一组假设遵循泊松分布的每日患者数据,使用 MLE 计算总体参数 lambda 的估计值
-
使用 MLE 估计量建模泊松分布
-
计算在特定一天内没有足够床位容纳患者的概率
-
将其与已知的 lambda 进行比较(这里我们可以知道,因为这是模拟数据)以查看 MLE 估计的效果如何
作者生成的代码
MLE estimated probability of not having enough beds: 0.049382812868119186
Actual probability of not having enough beds: 0.0493453265906304
就是这样,这是对我们人口参数的相当准确的估计!这就是最大似然估计的方法。
结论
概率与统计的基础研究涵盖了随机变量的基本假设和功能。然而,抽象的研究往往让学生感觉缺少了什么:应用于实际问题以指导决策。
在这里,我们讨论了如何基于泊松随机变量的理论和假设构建一个基础概率模型,并结合观察到的数据来分析假设被违反时可能的概率高估和低估。
如前所述,这个基础概率模型的一个关键假设是日常就医者之间的独立性,但这可能并不完全准确。以近期的证据为例,请回忆一下在 COVID-19 大流行期间,医院床位如何逐日变得越来越紧张。在这种情况下,这一过程的自协方差结构很可能是非零的,这意味着日常入住之间存在某种相关性。此外,我们的所有就医者不可能在同一天全部离院。
基于今天的入住情况来建模明天的入住情况可能更为合理。利用这个框架,我们现在可以将原始问题视为一个随时间演变的依赖系统,其中的未来状态仅依赖于现在的状态。幸运的是,我们有方法处理这样的依赖过程,通过将其视为马尔可夫链进行研究,这将是未来文章的主题——敬请关注!
Quant Guild
有兴趣提升你在量化金融、数学、统计学、数据科学、机器学习和人工智能方面的知识吗?
在 Quant Guild 查看我们的课程,使用代码 QGMEDIUM 享受 50% 折扣!
Python 已经成为学术界和从业者的首选语言…
## Algorithmic Trading System Development ## Introduction to Python
一个为那些有兴趣构建自己算法交易系统的人员设计的简明课程。
查看我们的免费资源!
YouTube [## Quant Guild
divitiae et educatione
www.youtube.com](https://www.youtube.com/@QuantGuild?source=post_page-----4a1a866dfa70--------------------------------)
Quant-Guild 目前有 4 个可用的代码库。关注他们在 GitHub 上的代码。
github.com](https://github.com/Quant-Guild?source=post_page-----4a1a866dfa70--------------------------------)
财富与教育
medium.com](https://medium.com/quant-guild?source=post_page-----4a1a866dfa70--------------------------------)
[## 加入 Quant Guild Discord 服务器!
查看 Quant Guild 社区的 Discord——与其他 1 名成员一起闲逛,享受免费的语音和文字聊天。
discord.gg](https://discord.gg/MJ4FU2c6c3?source=post_page-----4a1a866dfa70--------------------------------)
非常感谢阅读!希望你喜欢这篇文章——如果有任何问题,请随时留言或随时联系:roman@quantguild.com。
除此之外,我们下次见!
RMP
五月刊:城市空间的数据
月刊
数据如何帮助我们理解城市
·
关注 发布于 Towards Data Science ·4 分钟阅读·2023 年 5 月 3 日
--
图片由 Lennart Jönsson 提供,来源于 Unsplash
有时候很难不将城市视为我们需要解决的问题集合:住房、食品安全和环境影响都会浮现在脑海中。今年早些时候,我们强调了数据专业人士当前如何通过利用气候和地理空间数据的创新方法来应对这些挑战。
城市远不止其痛点的总和:它们反映了社会价值(善的、恶的和模糊的),激发创造力和联系,并作为不断演变的文化的活跃存储库。
本月,我们邀请您探索城市空间的丰富性和复杂性。从公交车等待时间到我们穿越的街道名称,我们选择的作者们将他们的数据技能应用于城市生活的具体片段,使我们能够更深入地理解城市环境的运作方式。
在我们深入探讨之前,我们想感谢您一如既往地支持我们发布的工作。如果您希望做出有意义的贡献,考虑成为 Medium 会员;如果您是符合条件国家的学生,现在可以享受实质性折扣。
TDS 编辑精选
-
街道名称中的隐藏模式:数据科学故事 [第一部分] (2023 年 1 月,6 分钟)
我们很少质疑谁应该以其名字命名街道,谁可能在这一过程中被排除。Dea Bardhoshi的迷人数据分析项目是对这种常见漠视形式的有效解药:它研究了阿尔巴尼亚地拉那街道名称的性别分布,并揭示了许多有趣的发现(有些比其他的更可预测)。
-
德国住房租赁市场:使用 Python 进行探索性数据分析 (2023 年 4 月,27 分钟)
随着全球许多城市的住房危机持续存在,围绕可靠数据构建解决方案导向的对话至关重要。Dmitrii Eliuseev对德国租赁市场的深入分析提供了一条强有力的分析路线图,可以服务于租户和政策制定者。
-
使用 GeoPy 和 Folium 绘制黑人拥有的企业地图 (2021 年 1 月,5 分钟)
来自边缘化社区的城市居民经常面临长期的排斥和隔离历史。Avonlea Fisher的项目旨在解决 COVID-19 对波士顿黑人拥有企业产生的巨大经济影响;两年后,它仍然可以激励数据科学家寻找对超本地问题的超本地答案。
-
公交车在哪里?GTFS 将告诉我们! (2023 年 1 月,15 分钟)
Leo van der Meulen的端到端教程的基本前提很难反驳:“公共交通与开放数据的结合具有巨大的潜力。” 将数据洪流转换为一个面向用户的界面,以告知乘客下一班公交车何时到达需要付出相当大的努力,但这个过程既有趣又可能扩展到其他背景和地点。
-
数字文本分析:荷兰语地区的街头诗歌 (2021 年 11 月,5 分钟)
地理空间数据、文本分析和诗歌在 Emma-Sophia Nagels的帖子中结合在一起,该帖子追踪了在荷兰及荷兰语区比利时编制、绘制和分析公开展示的诗歌的过程。
-
通过地理空间关联规则挖掘发现便利店位置模式 (2023 年 3 月,7 分钟)
东京的便利店可谓传奇:无处不在,随时可达,且充满了世界上最好的一些小吃。Elliot Humphrey超越了表面,试图检测商店位置中的模式,以推测出一个看似随意甚至混乱的现象背后的商业策略。
原创特色
探索我们最新的资源和阅读推荐。
-
音频机器学习的新前沿
我们精心策划了一系列强大的文章,涵盖了新模型、AI 接口和应用程序的崛起,这些使得处理音频和音乐的工作变得更加高效。
-
“最佳实践”究竟意味着什么? 我们收集的关于工作流程优化的亮点帖子,从更好的绘图到更有效的实验。
热门帖子
如果你错过了,这里是上个月在 TDS 上最受欢迎的一些帖子。
-
零 ETL、ChatGPT 与数据工程的未来 由 Barr Moses
-
时间序列预测:深度学习 vs 统计学 — 谁胜出? by Nikos Kafritsas
-
6 Python 最佳实践,区分高级开发者和初学者的关键 by Tomer Gabay
-
如何将公司文档转变为可搜索的数据库:使用 OpenAI by Jacob Marks, Ph.D.
-
你需要了解的 4 种自主 AI 代理 by Sophia Yang
-
Pandas 2.0 有哪些新特性? by Jeff Hale
我们非常高兴在三月份迎来了新一批 TDS 作者——他们包括Matt Collins、Krzysztof Pałczyński、Colin Horgan、Victor Graff、Dr. Roi Yehoshua、Mark Chen、Bernardo Furtado、Toon Beerten、Peng Qian、Edozie Onyearugbulem、Willem Koenders、Aaron Master和 Doron Bergman、Lingjuan Lyu、Jacob Marks, Ph.D.、Anthony Mensier、Francisco Caio Lima Paiva、Abhi Sawhney、Chris Mauck、Massimiliano Costacurta、Lee Vaughan以及Davide Caffagni等人。如果你有有趣的项目或想法要与我们分享,我们非常愿意听取你的意见!
下个月见。
MDM作为您数据治理之旅的起点
企业级数据管理成熟度路线图
·
关注 发布于 Towards Data Science · 11 分钟阅读 · 2023 年 5 月 25 日
--
图片来源:Joshua Sortino 通过 Unsplash。
许多组织面临的一个问题是——如何开始数据治理这一复杂的旅程?
尽管数据管理的许多不同方面都可以作为起点,本文提出了将主数据管理(MDM)作为组织建立初步数据治理成熟度的基础。MDM 实施可以帮助你将一组核心数据治理学科整合在一起,将其应用于有限但具有影响力的范围,学习经验并设定标准,然后再在组织的其他部分进行扩展。

理论上,任何数据管理能力领域都可以成为数据治理之旅的起点。来源:DAMA International,根据 CC-BY-ND 许可。
这一观点将首先通过检查典型 MDM 实施的各个方面来实现,然后将 MDM 实施组件映射到标准数据管理框架中。案例研究将描述一家领先的保险公司如何使用 MDM 启动其整体数据治理之旅。
什么是 MDM,MDM 实施项目包括哪些内容?
主数据管理(“MDM”)是一种能力,确保在组织的系统、应用程序和流程中关键业务实体的数据准确且一致。MDM 对于提供主数据属性(如客户、产品、供应商或原材料数据)的单一、可靠的数据来源至关重要。(有关更详细的定义,请参见 这里。)

主数据管理的概念概述。
MDM 实施项目通常包括以下几个阶段:
-
策略、政策和标准:描述了 MDM 解决方案和过程的预期成果,并明确了如何管理关键主数据的规则和期望。
-
发现和分析:评估和理解组织内数据的当前状态,并识别需要管理的主数据实体。
-
数据治理框架:定义了数据治理框架,包括数据管理员、数据所有者和数据治理论坛的角色和责任,以及将这些角色分配给“真实”的人员和团队,并动员治理论坛。
-
数据建模和映射:创建数据模型,定义主数据实体之间的关系以及描述它们的属性,并将主数据映射到使用它的系统和应用程序。
-
数据集成:数据集成是通过创建连接器从源系统中提取数据,将其转换为符合数据模型的格式,并将其加载到 MDM 平台中来构建的。同样,也会从 MDM 解决方案中建立数据流,以供消费过程和用例使用。
-
数据质量管理:定义和执行数据质量规则、计算指标、建立报告,并确定解决数据质量问题的过程。这还可能包括实体解析和生存分析。
-
培训和变更管理:为利益相关者提供培训,以确保他们了解 MDM 平台以及他们在数据治理框架中的角色和责任。建立变更管理过程,以管理对平台或框架的任何变更。
-
监控和维护:在此阶段,建立持续改进的过程,包括数据质量监控和报告、数据治理审计,以及识别改进机会。可以遵循问题管理过程来记录和解决观察到的问题。

MDM 解决方案组件的示意图。
映射到标准数据管理框架能力
当然,实施 MDM 解决方案并使围绕它的过程实现操作化将提升参考和主数据的维度成熟度。然而,在上述描述的典型 MDM 实施程序的 8 个步骤中,也与更广泛的数据管理框架有关联。

实施 MDM 解决方案当然直接提升参考和主数据管理维度的成熟度。然而,如果从战略上使用,它还会提高其他数据管理能力的成熟度。
让我们明确描述这些连接,以准确识别如何在这些能力领域建立成熟度。
数据战略
-
如果 MDM 解决方案没有配套的更广泛战略来解释为什么掌握某些关键数据至关重要,它将无法驱动价值。如果已有数据战略,可以用来加强现有战略;如果没有,它实际上可以作为组织的首个明确的数据战略。
-
对于特定平台的实现,应该在程序开始前确定用例和消费过程,以确保将相关要求纳入其中。这可以将 MDM 解决方案转变为真正的数据产品,并使数据办公室能够开始管理数据产品组合和跟踪数据驱动的用例(以及价值创造!)。
数据治理
-
在 MDM 解决方案及其掌握的数据周围,建立基础治理,包括通过定义一套政策和标准来概述 MDM 的一般期望,以及具体的角色和责任。这些可以增强现有的数据政策和标准,或者如果还不存在,形成第一版。
-
数据或领域所有者被指定为对主数据负责的责任方。可以确定数据管理员,他们被赋予了照看数据的责任(由数据所有者委托)。这包括确保数据适用性的责任,并可以使得创建更广泛的数据治理 RACI 矩阵成为可能,该矩阵可以在后续扩展到非 MDM 领域。因此,可以引入和定义主数据的数据治理角色,培养数据所有权和问责文化,然后将其在组织中推广,以确保数据治理实践的一致性。
-
定义了用于跟踪平台健康状况和使用情况的指标,并跟踪风险。这些指标可以形成第一组数据治理指标,之后可以进行扩展。如果目前还没有,可以召集一个论坛来监控这些指标并解决问题。该论坛可以扩展或为未来涵盖其他数据治理主题的新论坛提供灵感,从而推动治理实践中的关键数据成熟度。
架构
-
对于 MDM 平台本身,创建了解决方案和数据架构。这可以通过一套明确识别的架构标准来指导。可以建立最佳实践,即任何解决方案架构应受到适用数据标准的指导。实际的标准本身也可用于其他未来的转型倡议。
-
只有当平台成为组织交易中的可信来源时,MDM 才会创造价值。这不应是可选的——如果存在主数据,那么其消费对任何使用该数据的人来说应该是强制性的。这有助于建立更广泛的参考数据架构,其中的指导方针、原则、最佳实践和实施模式可以推动组织内部架构设计的稳健性和一致性,以及供应商和技术的合理化。
-
对于 MDM 解决方案,记录了数据从哪些来源获取,以及主数据如何提供给下游消费。记录的数据流和谱系可以作为捕捉更广泛组织数据景观的起点。
数据质量
-
MDM 的一个关键组成部分是测量和管理主数据的质量。如果没有受控(且量化的)质量,就无法建立信任,缺乏信任将导致采用困难。围绕业务规则识别、创建可执行的数据质量规则和编制数据质量指标的过程在任何未来的数据质量工作中都非常可重复使用。
-
如果使用了特定工具来测量数据质量,那么它可以在后续进行额外的扩展。同样,总结主数据数据质量的仪表板也可以很容易地扩展以包括非 MDM 数据的数据质量指标。
-
理想情况下,“设计中质量”这一概念应嵌入到 MDM 解决方案的架构中。即,数据捕捉、集成和转换的处理方式要尽可能保证数据质量。例如,对于国家字段,只允许出现相应 ISO 标准中的选项。这建立了一个模式、最佳实践和肌肉记忆,这对任何未来的现代化和转型努力同样适用。
-
创建了一个问题管理流程,以确保解决主数据中的问题。识别、跟踪和解决数据问题的相应过程可以扩展到其他数据领域。
建模与设计及元数据
-
如上所述,任何 MDM 解决方案都需要一个数据模型,以定义主数据实体之间的关系及其描述属性,并将主数据映射到使用它的系统和应用程序。为主数据(例如客户、产品)创建的概念模型和逻辑模型(以及底层元模型)可以扩展到其他领域(例如财务、人力资源、供应链、交易)。
-
作为 MDM 的一部分,可以采用一种技术来设计和捕捉数据模型和元数据。这项技术以及使用它的过程,可以用于后续解决方案和领域的数据模型。
-
捕捉的数据模型和元数据越多,未来范围的增量工作就越少。例如,如果需要描述数据元素并捕捉元数据以用于客户入职过程,如果关键客户数据和相关业务需求已经在概念和逻辑层面上定义过,那么所需的时间会少得多。
数据存储与操作
-
在 MDM(主数据管理)程序中,考虑主数据的存储和操作需求至关重要。这一需求推动了数据存储最佳实践的实施以及数据操作标准的制定。作为这一部分,组织需要设计和实施一种最佳的数据存储策略,考虑因素包括数据量、速度、种类以及对可扩展性和弹性的需求。
-
为 MDM 开发的操作流程,包括数据备份、归档和恢复流程,可以作为其他数据管理领域类似流程的基础,从而为数据存储和操作的成熟度建设铺平道路。
集成与互操作性
-
MDM 本质上需要将来自不同来源的数据整合在一起,从而促进了集成和互操作性的坚实基础。为主数据在不同系统中的合并、去重和同步设置的程序可以作为未来数据集成计划的蓝图。
-
在 MDM 实施过程中建立数据标准、创建通用数据模型,并执行一致的数据格式,可以为系统之间的互操作性奠定基础。作为 MDM 过程的一部分建立的集成模式和实践可以在整个组织中重复使用,有助于建立成熟的集成和互操作性实践。
-
具体来说,有机会定义一套互操作性标准——例如,定义使用通用数据模型的数据并采用一组选定的 API 和 ETL 技术——这些标准可以在整个组织中更广泛地采用。
数据仓库与商业智能(BI)
-
实施良好的 MDM 程序可以确保输入数据仓库的数据是干净、一致和可靠的。它提供了进行有效报告和分析所需的唯一真实来源,从而为稳固的 BI 战略奠定基础。
-
在 MDM 过程中建立的数据质量规则、指标和流程可以用来维护数据仓库的完整性。此外,主数据的强大元数据管理可以推动高效的数据血缘追踪、影响分析以及对 BI 目的的数据理解。
数据安全
-
在实施 MDM 程序时,组织会建立并执行具体的数据访问、修改和删除安全政策。这些政策在组织内统一应用,使得对谁可以访问或操作主数据进行严格控制。
-
这为建立更全面的数据安全框架提供了机会。在设置解决方案的过程中,组织可以定义安全角色和职责,并制定加密、匿名化和伪匿名化敏感数据的协议,从而为更广泛的数据安全成熟度奠定初步基础。
案例研究:一家保险公司在 MDM 方面的历程

图片由 Vlad Deep 提供,通过 Unsplash 获取。
作为 MDM 如何推动更广泛成熟度的实际说明,我们来看一个涉及领先保险公司的案例研究。我有机会与这家公司合作,这家公司在其主要市场中市场份额排名前三。该公司启动了一个项目,旨在为其客户数据实施 MDM 解决方案。
在这个倡议中,我的角色是确保技术解决方案不仅被实施,而且在企业内真正实现。这包括建立正确的流程、记录这些流程、创建强大的数据模型、开发数据质量解决方案和仪表盘,以及一个元数据模型和基本的元数据管理流程。我们还识别并建立了关键角色,如数据拥有者、数据管理员和数据质量分析师。
在实际应用方面,这里的主数据主要针对公司的市场部门,使用案例包括客户细分和营销活动管理。在实施 MDM 之前,公司由于难以协调来自各种来源的客户数据,甚至无法实现基本的 360 度客户视图。
实施一年后,成功的业务影响已经显而易见。通过整合和可靠的客户数据视图,营销团队能够判断一个与某个业务线有互动的客户是否也拥有其他业务线的产品。这促成了更深层次的数据驱动客户细分,取代了之前基于规则的分析。新获取的数据使得预测建模成为可能,从而确定“下一步最佳行动”,并使营销活动能够在更具个性化的层面上启动。
一个意外但重要的好处是联系数据(如电子邮件地址、电话号码和实际地址)的管理得到改善。通过将来自各种输入渠道(如呼叫中心、实体分支机构或客户电子邮件)的更新整合到 MDM 解决方案中,所有使用这些信息的人员都能获取到最新的详细信息。因此,通过书面营销材料或电话联系到目标受众的成功率提高了超过 20%。根据团队的估计,他们在营销活动上的投资回报率提高了多达 50%。
结论
MDM 可以作为组织数据治理之旅的强大起点。它提供了一种全面的方法来建立一个可靠的关键业务实体信息源,构建一个强大的数据治理框架,并推动各项数据管理能力的成熟。通过将特定领域的 MDM 作为一个可管理的范围,组织可以引入关键数据治理概念,在受控环境中测试这些概念,并逐步扩展到整个组织。
参考文献和进一步阅读建议
-
什么是主数据管理(MDM)?, Informatica。
-
我的 MDM 框架, Medium。
-
实践中的主数据管理:实现真正的客户 MDM, Dalton Cervo 和 Mark Allen。
-
数据产品有什么大不了的?, Medium。
-
智能数据治理设计 — 一个实际示例, Medium。
除非另有说明,所有图片均由作者提供。
平均绝对对数误差(MALE):一种更好的“相对”性能指标
介绍“相对”性能指标的 MAE 和 MSE
·
关注 发表在 Towards Data Science ·11 分钟阅读·2023 年 1 月 18 日
--
图片来源:William Warby 在 Unsplash。
在预测领域,以及更广泛的机器学习和统计学领域中,性能指标扮演着至关重要的角色。
它们在模型拟合、超参数调优和模型选择过程中被使用,以找到最佳的模型;同时也用于理解和解释模型如何在绝对术语上预测未来。
在这篇文章中,我的目的是说服你,对于许多预测¹问题,你应该使用均值绝对对数误差(MALE)或紧密相关的均方根对数误差(RMSLE)来衡量模型性能。
就像 MAE 和 RMSE 使用预测值与真实值之间的“原始”距离一样,MALE 和 RMSLE 使用“相对”(对数)距离;你可以将 MALE 和 RMSLE 视为“相对”指标的 MAE 和 RMSE。
事实上,我有信心可以说服你,这些指标不仅是最佳选择,而且在许多情况下是显而易见的选择。
在解释原因之前,让我们快速回顾一下性能指标解决了什么问题,以及现有指标的不足之处。
问题
错误度量和性能指标
首先,让我们区分两个密切相关的概念:错误度量和性能指标。
错误度量(或称错误)告诉你预测值f与观测值y的“距离”有多远。例如,“原始”错误 RE = f — y 和百分比误差 PE = (f — y) / y。
相比之下,性能指标(或称指标)使用错误度量来告诉你某个模型(或一组预测)的整体表现如何。一个例子是 MSE,它计算数据集中所有点的“原始”误差的平方均值:
它们有什么用处?
错误度量和性能指标在模型拟合过程中作为模型损失函数的一部分使用;在模型调整过程中,用于选择合适的超参数;以及在模型选择过程中,用于选择最佳的模型类型。它们在决定最终模型时扮演重要角色,因此确保它们能捕捉到你关心的内容非常重要。
当试图理解和解释模型的表现时,它们也会被使用:例如在“我们可以以平均 15%的误差预测下一季度的销售额”这样的陈述中。因此,它们也需要具备可解释性。
到目前为止,都是显而易见的(希望如此)。这些概念稍后会派上用场。
严格为正的数据和相对误差
正如我将在另一篇文章中讨论的(敬请期待!),不同的预测指标在不同的情况下是合适的。
例如,如果你关注的是百分比误差(并且你的数据严格为正),那么“相对”指标如 MAPE 或 sMAPE 通常被推荐;如果你关注的是以实际单位(例如苹果数量)衡量的误差,或者你的数据可以为零或负数,那么“原始”指标如 MAE 或 MSE 更为合适。
如果你想比较或聚合时间序列中的性能指标,那么你可能需要使用标准化指标[1]。
在这篇文章中,我将专注于某种特定问题的指标。具体来说,问题是:
-
数据是严格为正的:它们不取零或负值。
-
预测者(即你!)关心的是相对误差而非原始误差²。
虽然许多预测问题属于这种类型,但并非所有问题都如此——选择适合你问题的指标非常重要。
当前解决方案
预测者使用什么误差?
Gneiting [2] 发现,到 2000 年代中期,平均绝对百分比误差(MAPE)是商业和组织中最广泛使用的预测准确性度量。
查看流行的统计预测包,例如 R 中的 “fable” 包,或 Python 中的 “gluonts” 包,表明 MAPE(及其相关的 sMAPE)仍然是实际预测应用中使用的主要“相对”性能指标。
MAPE 之所以受欢迎,是因为它能够处理尺度随时间变化的数据;它在时间序列之间具有相对的可比性;对异常值的处理相对较好;并且具有简单的解释。
然而,MAPE 和 sMAPE 存在问题——让我们看看它们是什么。
MAPE 的问题
MAPE 的问题是什么?
正如其名称所示,MAPE 使用 百分比误差 作为其误差度量:
这种误差度量以惩罚高估比低估更著名。
作为一个极端的例子,如果你高估观察值 10 倍,你会得到一个绝对百分比误差(APE = |PE|)为 9;而如果你低估它 10 倍,你得到的 APE 仅为 0.9——仅为误差的 1/10!
在更现实的情况下,这是一个较小的问题(但仍然是一个问题),即你的预测更接近实际情况。
另一种看待 PE 问题的方法是,低估的误差被压缩到 -1 到 0 的范围内(即 -100% 到 0%),而高估的误差可以取任何从 0 到无穷大的值。
sMAPE 的问题
解决这个低估问题的提议是对称 MAPE(sMAPE),它使用“对称百分比误差”作为误差度量:
sPE 压缩 高估的误差,使其与低估的误差处于相同的尺度;这意味着低估和高估会受到同样严格的惩罚。
但虽然 sMAPE 成功解决了“低估”问题,但它也带来了另外两个问题³:
-
这个指标现在更难以解释。
-
由于 sPE 错误是有界的,它不会告诉你数据中的极端异常值。(这可以是一个特性或缺陷,取决于你的观点。)
(次)最优模型
最后,让我们稍微考虑一下 MAPE 和 sMAPE 优化的是什么。
作为提醒,当你使用 MAE 或 MSE 优化模型时,你的模型最终会预测分布的中位数或均值。(前提是你有足够的数据和足够好的模型。)
相反,当你为 MAPE 或 sMAPE 进行优化时,你的模型最终预测的……并没有什么特别的东西。
也就是说,模型最终预测的量将取决于目标 y 的分布,但它通常不会是分布的均值或中位数。
这种行为并不特别好。我们能做得更好吗?
对数误差
更好的误差度量…
我们已经看到,MAPE 和 sMAPE 的问题来自于它们的误差度量——百分比误差(PE)和对称百分比误差(sPE)。
我们能找到一种更好的方法来测量预测 f 和观测值 y 之间的相对误差吗?(也许类似于 MAE 和 MSE 使用的“原始”误差 f — y?)
我的解决方案⁵是对数误差(LE):
简而言之,它是预测值和观测值对数之间的差异⁶——或者等效地,预测值和观测值之间比例的对数⁷。它与百分比误差紧密相关:LE = log(1 + PE)。
这种误差度量具有许多优良属性:
-
如果且仅如果预测完全正确(f = y),它才为零。
-
低估和高估受到同等严厉的惩罚:例如,低估 y 2 倍的绝对对数误差(ALE = |LE|)与高估 y 2 倍的误差是一样的。
-
错误相加⁸:在数学中,LE(x, y) + LE(y, z) = LE(x, z)。这使得 LE 不同于 PE 和 sPE,使其看起来像是 MSE 和 MAE 使用的“原始”误差度量。
一种思考 LE 的方式是,它通过使用对数变换将误差低估拉伸,并将误差高估压缩到完整的实数线上(-∞,∞)。
另一种思考它的方式是作为对数尺度上的原始误差——即只是预测值和观测值之间在数据对数上的原始距离。
正如我稍后将解释的,它与 MAE 和 MSE 使用的“原始”误差度量非常相似。
…导致更好的指标
现在我们有了一个更好的相对误差度量,让我们回到我们真正感兴趣的内容:性能指标。
对于 MAPE 或 sMAPE,为了从我们的误差度量中获得一个指标,我们取其绝对值在所有数据点上的均值:
结果是均方绝对对数误差度量,简称“MALE”。
另外,我们可以使用 LE 的平方来得到均方根对数误差(RMSLE):
与 MALE 相比,这一度量对离群值更为敏感。
这些指标相比其他“相对”指标具有一些优势,我将在下面讨论。
它们估计的是什么
当你优化 MALE 时,你会得到预测你分布中位数的模型。
(或者更正式一点:在 MALE 损失下,贝叶斯估计器是中位数。)
当你使用“平方误差”版本(RMSLE)时,你会得到预测数据几何均值的模型。
这与 MAPE 和 sMAPE 相对,它们通常存在偏差。
可解释性
为了将 MALE 度量转换成你的老板能理解的东西,你可以通过取指数来将其转换回相对尺度,即:
这给出了 (几何) 平均相对误差。例如,EMALE 为 1.2 意味着你期望平均上在任一方向上错误因子为 1.2。(向你的老板解释为 20% 平均百分比误差。)
这是对 sMAPE 的改进,sMAPE 没有直接解释,而 MAPE 尽管技术上简单,但可能有点误导⁹。
你可以对 RMSLE 使用相同的技巧:
像 RMSE 一样,这不能直接解释,但它给出了你误差在相对术语中的大小感。
实证证据
尽管关于哪个指标在“实际环境”中表现更好的实证证据较弱,但现有的证据略微表明 MALE 和 RMSLE 至少与 sMAPE 在实际中表现相当。
例如,Tofallis [3] 显示 RMSLE 指标
-
在几个示例数据集上,比 MAPE 拟合效果更好。(这并不意外。)
-
在使用模拟数据的模型选择任务中表现优于 MAPE 和 sMAPE。
Koutsandreas 等人 [4] 没有发现不同指标(包括 MALE 但不包括 RMSLE)在模型选择中的性能差异。但他们发现的小差异支持了 MALE 指标倾向于选择准确模型的说法。
简单性
MALE 和 RMSLE 易于理解、记忆和编码——甚至比 MAPE 或 sMAPE 更简单。例如,在 Python 中:
male = mean(abs(log(f / y)))
rmsle = sqrt(mean(log(f / y)**2))
MALE 与其他指标的比较
MAE 和 MSE
如果你一直在关注,你可能已经注意到 MAE / MSE 和 MALE / RMSLE 之间的一些相似之处。
实际上,这两组指标之间有密切的联系:具体来说,优化 MALE(或 RMSLE)等同于在数据的对数上优化 MAE(或 MSE)。
也许这很明显,但让我们明确看一下 MAE:
实际上,你可以将 MAE 和 MALE 视为同一家族的指标:唯一的区别是 MAE 使用预测值和观察值之间的原始距离,而 MALE 使用相对(对数)距离。
这意味着什么?
一个含义是,对于在训练过程中使用 MSE(或 MAE)作为损失函数的模型,你可以通过在训练之前对数据取对数来优化 RMSLE(或 MALE)¹⁰。
(只要记得之后转换回原始尺度!)
MAPE 和 sMAPE
绝对百分比误差(APE)、对称绝对百分比误差(sAPE)和绝对对数误差(ALE)在预测值接近真实值时是相似的,如下图所示:

绝对百分比误差(APE)、对称绝对百分比误差(sAPE)和绝对对数误差(ALE)的比较,观察值为 y = 1。图片来源于作者。
所以实际上,只要你的误差足够小(例如,少于 10%),使用 MALE、sMAPE 还是 MAPE 可能没有太大区别。
ALE 在对异常值的敏感性上介于 APE 和 sAPE 之间。例如,与错估 +10 倍相比,错估 +100 倍会使你的原始 sAPE 增加 1.2 倍,原始 ALE 增加 2 倍,原始 APE 增加 9 倍。
缩放指标
由于 MALE 和 RMSLE 是相对单位(而不是实际单位),它们可以用于比较模型在时间序列中的表现,或在不同时间序列之间汇总性能指标。(就像其他相对指标一样。)
当噪声的水平(相对而言)在不同时间序列之间相似时,这种方法效果很好。但如果,例如,你的一些序列非常嘈杂/难以预测,而其他序列则不那么困难怎么办?
在这种情况下,你可以通过基准方法的误差(例如,天真的或季节性天真的方法)来规范化你的误差,以将它们放在相同的尺度上 [1]。
例如,给定时间t = 1, 2, …, T 的训练数据,时间 T + h 的预测的 scaled 对数误差(SLE)为
均值绝对缩放对数误差为
总结
在这篇文章中,我论证了对于某些预测问题,你应该使用对数误差(LE)来衡量预测误差,而不是“原始”或百分比误差。这意味着使用基于 LE 的指标来衡量模型性能:具体来说是均值绝对对数误差(MALE)和均方根对数误差(RMSLE)。
这些指标仅应在数据严格为正且你对相对(百分比)度量的准确性感兴趣时使用。在这种情况下,它们有以下好处:
-
它们具有明确且简单的解释。
-
优化这些指标会产生中位数(MALE)或几何平均数(RMSLE)的估计值。
-
它们可以在不同时间序列之间轻松比较和汇总。
-
它们避免了均值绝对百分比误差(MAPE)和对称均值绝对百分比误差(sMAPE)的著名问题。
MALE 和 RMSLE 可以被视为“相对”性能指标的 MAE 和 RMSE。我认为,如果你有兴趣以相对方式衡量误差,它们应该是默认选择。
我说服你了吗?为什么/为什么没有?在评论中告诉我。
参考文献
[1] R. Hyndman 和 A. Koehler, 预测准确性测量的另一种看法 (2006), 国际预测学期刊 22(4)。
[2] T. Gneiting, 制作和评估点预测 (2011), 美国统计协会期刊 106(494)。
[3] C. Tofallis, 一种更好的相对预测准确性测量方法用于模型选择和模型估计 (2015), 运营研究学会期刊 66(8)。
[4] D. Koutsandreas, S. Evangelos, P. Fotios 和 A. Vassilios, 关于预测准确性度量的选择(2022),《运筹学会期刊》73(5)。
脚注
-
我主要关注预测领域——因为这里似乎是“相对”误差度量目前最常用的地方——但相同的论点适用于其他机器学习领域。
-
相对误差仅在讨论严格正的数据时才有意义。
-
值得注意的是,通常这些问题并不太严重,sMAPE 已经在实际应用中成功使用。
-
更准确地说,这对于这个损失函数来说是最佳预测器(“贝叶斯估计量”)。
-
这个误差度量已被 Tofallis [3] 和 其他人 讨论;我认为这个想法足够明显,很多人一定已经想到并使用了它。
-
对数中可以使用任何底数;在这里我假设使用的是底数 e,即自然对数。
-
因此,这也被称为“对数准确性比”。我更喜欢称之为“对数误差”,因为这样更简洁,并且明确它是一个误差度量。
-
这个属性并不严格用于误差测量,但它是一个很好的特性。
-
特别是,+20% 的百分比误差意味着你的误差因子是 1.2,而 -20% 的百分比误差意味着你的误差因子是 1.25(1 / 0.8)。这种不对称性容易造成混淆。
-
你可能会想,如果使用这些新度量与在数据对数上使用 MAE / RMSE 相当,那么定义和命名这些新度量的意义何在。在我看来,明确地定义和命名这些度量可以帮助你更清楚地理解在处理对数尺度数据时优化的内容,并且更易于沟通。(说“我优化了 / 评估了 MALE”比“我优化了 / 评估了数据对数上的 MAE”更简洁且不容易混淆。)
平均精确度@K(MAP@K)清晰解释
原文:
towardsdatascience.com/mean-average-precision-at-k-map-k-clearly-explained-538d8e032d2
对于推荐或排名问题,最受欢迎的评估指标逐步解释
·发布于Towards Data Science ·阅读时长 7 分钟·2023 年 1 月 18 日
--

图片由Joshua Burdick提供,来源于Unsplash。
平均精确度@K(MAP@K)是推荐系统和其他排名相关分类任务中最常用的评估指标之一。由于该指标是不同误差指标或层的组合,因此初次查看时可能不易理解。
本文将逐步解释 MAP@K 及其组成部分。文章末尾还将提供代码片段,说明如何计算该指标。但在深入了解每个部分之前,我们首先谈谈为什么。
为什么使用 MAP@K?
MAP@K 是一种误差度量指标,当你的推荐项的序列或排名在任务中扮演重要角色或是目标时,可以使用该指标。通过使用它,你可以获得以下问题的答案:
-
我生成或预测的推荐是否相关?
-
最相关的推荐是否在前几位?
使以下步骤更易于理解
既然你知道了“为什么”,让我们来谈谈怎么做。接下来的章节将以“洋葱”风格逐步解释,从内部(从精确度 P 开始)到外部(MAP@K),MAP@K 的结构。
为了使步骤及其组成更易理解,我们以以下示例进行操作:我们希望评估我们的推荐系统,该系统在访问产品详细信息页面时向潜在客户推荐六个项目(见图 1)。

图 1. 推荐示例(作者提供的图片)。
精确度(P)
你可能已经在书籍或文章中听说过精度,当你学习分类模型的误差指标时。精度可以看作是质量的衡量标准。高精度意味着我们的模型返回的结果或推荐更相关,而不是无关的。
精度可以定义为所有推荐项目(相关 + 无关项目)中相关项目的比例。

图 2. 精度公式(图片由作者提供)。
以下示例(图 3)显示了 6 个推荐项目。在这 6 个推荐中,有 2 个是相关的。

图 3. 精度示例(图片由作者提供)。
将这些值代入我们的公式(图 1),我们得到一个0.33的精度( **2** 个相关项目 / (**2** 个相关 **+** **4** 个无关项目) )。
Precision@K(P@K)
精度指标(图 2)本身不考虑相关项目出现的排名或顺序。是时候将排名纳入我们的精度公式中。Precision@K可以定义为前K个推荐项中相关项目的比例(图 4)。

图 4. Precision@K 公式(图片由作者提供)。
下图(图 5)展示了我们上述示例(图 3)中的排名场景。

图 5. Precision@K 示例(图片由作者提供)。
Precision@K 列显示了每个排名(1 到 6)的 Precision@K 值。K 代表我们考虑的排名数量(1、2、…、6)。
Precision@1
假设我们只考虑第一个排名(K=1),那么我们将有0 个相关项目除以1(总项目),这导致 Precision@1 为0。
Precision@3
假设我们考虑前三名排名(K=3),那么在前 3 个推荐中我们将有 1 个相关项目和 2 个无关项目。如果我们将这些数字代入我们的公式(图 3),我们将得到0.33(**1** 个相关项目 / (**1** 个相关 + **2** 个无关项目) )。
Precision@5
最后但同样重要的是,让我们考虑前五名排名(K=5)。我们将有前 5 个推荐中 2 个相关和 3 个无关的项目。如果我们重新计算,我们将得到一个0.4的值。
(**2** 个相关项目 / (**2** 个相关 + **3** 个无关项目)).
平均精度@K(AP@K)
正如我们所见,精度和 Precision@K 都非常简单。下一步有一点复杂性。
平均精度@K 或 AP@K 是 Precision@K 的总和,其中第 kₜₕ 排名的项目是相关的(rel(k)),除以前 K 个推荐项中的相关项目总数(r)(图 6)。

图 6. AP@K 公式(图片由作者提供)。
迷惑了?让我们看看以下关于 AP@6 的示例(图 7)。

图 7. AP@K 示例 1(图片由作者提供)。
相关项目的总数(r)在这个示例中为2(在排名 2 和 4)。因此,我们可以将 2 放在分数 1/r 中。
看第一个排名 1。Precision@1 为 0,且该项不相关(灰色)。因此,我们将 Precision@1 的值乘以 0,得到 0 * 0。
然而,在第 2 个排名中,我们有一个 Precision@2 值为 0.5 和一个在第 2 个排名的相关项。这导致 0.5 * 1。
在第 3 个排名中,我们再次有一个无关项,Precision@3 为 0.33。结果为 0.33 * 0。
我们会逐个排名进行处理。如果排名 k 包含相关项,我们将其 Precision@k 乘以 1。如果无关,我们将其乘以 0,这意味着它不会对我们的总和产生影响。
对于这个示例,最终结果将是AP@6为0.5。
在我们进入最后一步之前,你还记得我们在开始时提到 MAP@K 可以回答的问题吗:
最相关的推荐是否在前几个排名中?
该度量的一个重要特性是它惩罚低排名中的相关项。为了让你更好地理解,让我们看看以下示例(图 8)。

图 8. 不同排名下相关项目的示例。最佳情况(左),最差情况(右)。排名越高越好(图片由作者提供)。
与最初的示例(图 7)相比,相关项目的数量没有变化。但变化的是它们被放置的排名。AP@K(因此 MAP@K)会惩罚那些将相关项目放在较低排名的推荐或模型。
平均精度均值@K(MAP@K)
之前的步骤和示例都是基于评估单个查询或一个访客在浏览产品 X 的产品详情页时收到的单个推荐列表。但我们有不止一个访客……
平均精度均值@K 或 MAP@K 考虑到了这一点。它平均了对M 用户显示的推荐的 AP@K。

图 9. MAP@K 公式(图片由作者提供)。
请注意:为了简化起见,我在这个示例中选择了“用户”。但是,根据你的情况,M 也可以是例如搜索查询。
为了更好地理解 MAP@K 是如何计算的,下面的示例(图 10)可以提供帮助。

图 10. MAP@K 示例(图片由作者提供)。
基于 3 个不同的用户(或查询),MAP@6 为0.59。
编码
现在我们已经熟悉了理论,让我们开始编码。在接下来的示例中,我们将处理两个包含 product_ids 的列表 actuals 和 predicted。
目标是检查我们的实际值是否出现在预测列表中,以及如果出现,它们出现在什么排名/位置。这就是为什么在 actuals 列表中项目的顺序无关紧要,而在 predicted 列表中却很重要。
AP@K
以下列表反映了我们之前使用的示例(图 11)。

图 11. 从开始回顾的示例(图片由作者提供)。
actuals = ['p_a', 'p_b']
predicted = ['p_d', 'p_a', 'p_c', 'p_b', 'p_e', 'p_f']
如果我们将实际值与预测值进行比较,那么我们可以看到 p_b 出现在第 2 位,p_d 出现在第 4 位。
def apk(y_true, y_pred, k_max=0):
# Check if all elements in lists are unique
if len(set(y_true)) != len(y_true):
raise ValueError("Values in y_true are not unique")
if len(set(y_pred)) != len(y_pred):
raise ValueError("Values in y_pred are not unique")
if k_max != 0:
y_pred = y_pred[:k_max]
correct_predictions = 0
running_sum = 0
for i, yp_item in enumerate(y_pred):
k = i+1 # our rank starts at 1
if yp_item in y_true:
correct_predictions += 1
running_sum += correct_predictions/k
return running_sum/len(y_true)
如果我们将我们的两个列表放入我们的函数中
apk(actuals, predicted)
然后我们得到像我们手动示例中那样的0.5(图 7)。
MAP@K
由于 MAP@K 对多个查询进行平均,我们将我们的列表调整为以下结构:
actual = ['p_a', 'p_b']
predic = [
['p_a', 'p_b', 'p_c', 'p_d', 'p_e', 'p_f'],
['p_c', 'p_d', 'p_e', 'p_f', 'p_a', 'p_b'],
['p_d', 'p_a', 'p_c', 'p_b', 'p_e', 'p_f'],
]
我们的 actuals 保持不变,但我们的 predicted 列表现在包含了多个列表(每个查询一个)。predicted 列表对应于图 10 中的列表。

图 12. 从图 10 中回顾的示例(图片由作者提供)。
下面的代码显示了 MAP@K 的计算:
import numpy as np
from itertools import product
def mapk(actuals, predicted, k=0):
return np.mean([apk(a,p,k) for a,p in product([actual], predicted)])
如果我们将我们的列表放入这个函数中
mapk(actuals, predicted)
然后我们得到0.59。
结论
在评估推荐系统或排名模型时,MAP@K 是一个很好的选择。它不仅提供了推荐是否相关的洞察,还考虑了正确预测的排名。
由于其排名考虑因素,它比其他标准误差指标如 F1 分数更难以理解。但我希望这篇文章能为你提供关于这个误差指标如何计算和实施的全面解释。
如果你想测量你的推荐是否相关以及它们有多相关,可以查看归一化折扣累积增益(NDCG)。
来源
信息检索导论 — 评估,web.stanford.edu/class/cs276/handouts/EvaluationNew-handout-1-per.pdf
有意义的实验:5 个有影响力的数据项目,帮助打造更好的产品
实际数据领导者系列的第二部分,介绍如何与各部门及其所有奇特特性进行最佳合作
·
关注 发表在 Towards Data Science ·14 分钟阅读·2023 年 1 月 6 日
--
图片由 Shane Murray 提供。
理解并与每个业务领域独特的激励机制和工作流程对齐,这才是让数据团队不仅高效而且卓越的关键。
本系列的第一部分讨论了大家最喜欢的电子表格强用户——财务团队。本文将探讨数据团队如何更好地识别共同原因并避免与产品团队的摩擦点。
虽然这些互动根据交付产品的性质会有所不同,但这篇文章适用于主要提供数字化、面向消费者产品的广泛组织。通常在这些组织中,跨职能的产品团队(产品经理、设计师、工程师等)共同拥有从头到尾的消费者体验,并各自拥有该体验的某些方面,如功能或产品。
让我们先看看一些有影响力的项目示例,其中数据和产品团队可以找到共享的成功点,然后再深入探讨推动有意义协作的技巧。
赢得你产品团队的项目
产品团队可能负责从电子邮件到应用程序的任何东西。他们可以专注于一个新功能,也可以广泛评估全新的货币化策略。然而,有一些共性可以作为产品分析最佳实践的参考。
这里是我看到的推动结果的五个最佳实践:
1. 数据驱动的产品策略
产品团队在产品生命周期的所有阶段都需要以数据为依据,从创意生成和产品定义,到验证原型和构建商业成功的产品。
多年来我遇到的一个不幸的情况是,有些产品团队希望数据来决定他们的策略,可能会问他们的数据合作伙伴,“我们接下来应该构建什么功能?”
这可以理解——开发新功能是一个资源密集型的过程,风险很高。这给产品经理带来了做出正确决策的压力,这通常会产生一定程度的确定性需求,或者至少是可以辩护的理由。但分析师只有过去实验和决策的数据,而没有可能允许他们推断未来决策的反事实数据。

图片由 JUVART 提供,来源于 Shutterstock。
当有明确的产品假设需要测试或探索,以及一组可以根据明确定义的结果或成功标准进行评估的选项时,分析团队会做出最佳的工作。
例如,“‘发现’标签会比‘最近播放’标签在日常互动中表现更好”是一个可以通过历史数据(以及一些关于标签使用的假设)或提议的实验来探索的假设。另一方面,如果问题是,“我们应该构建一个发现标签还是其他什么?”你可能需要回到用户研究。
那么这使我们处于何种境地呢?
数据团队应该主导(~年度)建模工作,探索与增长(即客户获取和留存)相关的最重要的产品杠杆。早期的工作通常会附带“这是相关性,而非因果性”的警告,因为对产品机制的因果理解将随着时间和多次迭代而建立。
这些模型可以作为投资产品的有价值的指引,但绝不应过于规定性。那个迫使你抛弃当前模型的大胆想法,可能比你从当前产品杠杆对业务的影响中推导出的想法更具影响力。
我发现这些工作会产生对更精细模型的需求,这些模型深入研究小型产品(如新闻通讯、应用)或客户旅程不同阶段中的产品杠杆的重要性。
此外,数据团队可以利用对业务的更深入、整体理解来定义“北极星指标”或“重要指标”套件,这些指标可以作为产品团队在构建产品战略和路线图时的指南针。
2. 基础知识:跟踪、报告和发布后评估
产品分析中最基础的实践是指标的定义、跟踪和报告,这使得产品或功能在发布后能够可靠地进行评估。但数据往往在产品发布中并不被重视。
事实上,数据往往是发布前最后考虑的事物,但发布后首先被询问的内容。
数据领导者和产品领导者有责任使优质数据成为产品发布的核心。我合作过的最佳产品领导者的态度是,“如果我们无法测量它,那么我们就不能发布它。”
在发布前,讨论通常从数据团队询问哪些指标重要以及哪些用户细分有意义开始。产品团队的回答几乎总是“所有和每一个”。
最佳的产品分析团队既像人类学家,又像工程师。他们首先研究希望在接触新功能的用户群体中观察的行为,定义成功的明确标准,然后将这些标准转化为可实施的结果指标和用户细分。前期做好这些工作会在后期节省大量时间,当高层询问发布是否成功时。
3. 实验
实验是产品经理的生命线。随着大多数数字产品开发团队以敏捷方式进行交付,进行实验的能力现在通常被认为是“基本要求”。
但做得好的产品实验远非“基本要求”。这不仅仅是将电子邮件营销活动拆分为对照组和测试组,调整文案,然后测量响应率那么简单。
-
风险通常更高 — 例如,用户体验的变化可能需要多天的设计、构建和部署。
-
存在多个集成点——例如,你可能需要通过 SDK 或 CDN 部署你的实验分配库,此外还要提供通过浏览器中的 JavaScript 进行集成。
-
结果不容易立即知晓——例如,一个推荐系统可能需要根据它对用户参与度和留存率的影响来衡量,而不是其点击率,这可能需要几周或几个月的时间。
-
权衡可能更复杂——例如,主页实验可能需要测量多个业务结果,每个结果对业务的价值不同,并且需要管理不同的利益相关者。
-
方法可能根据产品类型有所不同——例如,根据优化问题的形状,你可能会使用离散(A/B 和多变量)或自适应(“土匪”)实验模式。
因此,数据领导者积极标准化平台或工具以支持产品实验是有益的。特别是当实验的执行可能会分散到多个跨职能产品团队时,这一点尤为重要。
不要认为可以购买或构建支持所有用例的平台。但应明确平台将支持的用例,并在实施和数据收集上提供足够的灵活性,以便可靠地完成不常见或更复杂的实验。

图片由 Hans Reniers 提供,来自 Unsplash。
实验速度通常被产品团队视为构建和扩展成功产品的关键,这通常通过谷歌(严重缺陷的)“41 种蓝色”实验来体现。
但虽然速度和迭代学习至关重要,你无法通过迭代方式达到新的商业模式。 大型组织中的实验往往受限于当前用户体验的可能性和/或单个产品团队所能控制的范围,而实际任务可能是重新构想体验的可能性。
这就是为什么数据团队还应该推动实验设计和分析的最佳实践,比如:
-
以行为假设为主导,而不是产品功能的变体
-
将假设转化为独立的实验属性(也许将蓝色的阴影留给设计团队)
-
考虑合适的实验设计——是连续的 A/B 测试系列,还是需要多变量方法,以便理解实验属性之间的交互?
不要陷入局部最优。过去 15 年中,我见过的最具影响力的实验都要求团队超越自身范围,定义那些从根本上改变用户体验和商业模式的假设。
4. 队列分析
仅次于进行受控实验,队列分析是产品团队最有用的分析工具。这种方法涉及将用户划分为具有共同特征的组,以评估用户健康状况并制定产品假设。
没有一种正确的方法来进行队列分析——可能性是无限的——但有很多方法会使其变得错误。我将解释一些我见过的这种技术误用的方法,以及如何正确地进行。
首先,让我们考虑一个产品团队,希望了解他们的用户基础的健康状况,以及产品体验是否随着时间变得更“粘性”。
在这里,“健康”可以由用户保留、较为具体的活跃度,或者用户转化为付费客户来定义。
避免对队列定义过于宽泛,例如逐周比较所有用户,这可能会因为不同队列中的回访用户比例不同而混淆分析。
为了使这种分析有用,至关重要的是,你必须比较“苹果与苹果”,例如关注每周到来的新用户。你还可以进一步细分这些队列(例如,按地域或设备),以诊断用户健康状况的增加和减少。

这张图表绘制了每周新用户的用户保留随时间的变化,显示了每个队列的健康状况。 图片由 Shane Murray 提供。
其次,让我们考虑一个产品团队,希望了解哪些特征在推动健康用户方面最有价值。
避免返回那些分析说“做 x 的人更可能留下”的结论。这些陈述因几个原因几乎毫无用处。
首先,执行任何操作的用户更可能留存——你可能只是挑选数据;其次,你没有提供关于特征使用情况的背景信息——它可能是最不受欢迎的特征;最后,缺乏时间意识使得这种说法更可能只是相关性的。
相反,你应该:
-
对队列操作进行时间限制(例如前 30 天),以将潜在原因与后续效果(例如第 30 天至第 180 天)分开。
-
比较多个操作,以避免挑选感兴趣的特征。
-
提供关于特征使用情况及其对用户保留影响的背景信息。

特征散点图。图片由 Shane Murray 提供。
上述功能散点图显示了一个标记为 A 到 H 的产品功能集的功能使用与保留提升的关系。例如,功能 A 被轻度采纳,但与最大的保留提升相关联;功能 D 被大量采纳,但与较小的保留提升相关联。
尽管这项分析没有规定投资的具体方向,但它可以为未来的产品开发提供信息:
-
你可能会因功能 E 缺乏采用和对保留的影响而逐步淘汰它
-
你可能会选择推动高影响力功能 A、B 或 F 的更多采用
-
你可能会选择重建一个表现不佳的功能,以提升其潜在采纳或影响力
产品分析工具,如 Amplitude 和 Mixpanel,通过前期的实施使得分组变得简单。但你也可以选择在 BI 工具或自定义应用中自行构建,从基本的行为分组开始,然后添加维度以支持更多的探索。
5. 自助访问数据
在上述所有产品数据需求中,你会面临一个共同的问题 —— 我们如何使其自助服务?
尽管数据团队变得越来越灵活,产品团队仍然期望能够随时得到看似简单的问题的答案(例如,“与推送通知交互的用户的保留差异如何?”),即使之前没有人考虑过这个具体问题。
此外,分析工作仍然承载着 自己动手做 的期望,而设计或工程则没有。虽然很容易把产品利益相关者看作是不理解我们领域的人,但实际上往往是因为分析师被困在回答一些基础问题上,这些问题 本可以 通过更高质量的数据访问得到解决,而不是花时间进行更复杂、全面或深刻的分析来展示他们的专业知识。
数据团队需要解决的问题是如何为产品利益相关者提供正确的自助访问,以实现以下目标:
-
更广泛地访问可靠数据,以便更快地做出良好的决策
-
从以服务为导向的响应转向数据洞察路线图的分析师工作转变
在工具配置和访问上花费的时间通常是值得的,但自助使用的回报必须大于构建和维护它所花费的努力。你可能会发现,为基本指标访问构建自助服务非常有意义,但如果你进一步推动更精细或复杂的分析,只有在你能够将解决方案普遍化并在许多产品团队中驱动显著采用时,才值得付出额外的努力。
你可以通过多种路径来实现这些能力——从现成的到自制的,以及介于两者之间。数据团队将希望在其数据仓库中保持单一的数据来源,以最小化治理成本并维持数据的可信度。但今天的技术栈需要一系列工具来实现自助服务的承诺——BI、产品分析和实验往往是值得投资的独立工具,因为它们有针对性的用户界面。
成功与产品团队合作的技巧
随着产品团队越来越注重数据,数据团队越来越注重产品,数据与产品学科之间的关系可能变得复杂或充满挑战。
但这并非必须——这是一个关键的合作关系,如果你能为产品团队设定正确的期望,了解数据能带来什么好处,它可以高效运作。以下是方法。
调动他们的能量——产品经理因推动影响力而获得奖励,而最常见的路径是通过专注于产品工程团队并使用实验来实现渐进式的产品改进。
分析师或数据科学家应成为产品的专家伙伴,确保实验设计的质量、完美的执行和对实验数据的严格解释。在这些以实验为驱动的产品团队中,分析师通常会获得一个战略合作伙伴的席位,贡献假设,对产品路线图进行评估,并校准团队在何时使用数据与何时依赖直觉或定性方法的指南针。
实验不会全部都是突破性的,但动能是成功产品团队的必要属性。
用好奇的数据团队提升战略——虽然分析师在支持每个产品团队目标方面成为专家伙伴至关重要,但强大且协作的数据团队的独特优势在于其观察多个产品团队之间的重叠、差距和权衡的能力。
一个协作的分析师团队会看到不同产品团队之间指标和报告的重叠,并可以构建出没人意识到他们需要的分析数据产品。
或者,他们可能会观察到用于理解用户行为的指标存在差距,然后研究并实施一套新的指标,以更好地体现驱动产品战略的行为。例如,一位对多个产品团队有全面了解的数据科学家可能会将多种结果纳入其目标设定或推荐算法中,例如平衡短期增长与长期参与。
培养一种鼓励好奇心和批判性思维的文化,使分析师和数据科学家能够挑战现状,并在产品合作伙伴的直接方向之外产生影响。
总会有权衡——没有免费的午餐。当你为一种行为优化时,按定义,你是在抑制其他行为。实验越雄心勃勃,需要参与的利益相关者就越多,试验的时间也就越长。
产品首页上的实验通常是这些复杂性出现的地方,你可能会在非常小的一部分用户上运行长期实验(例如<1%)。
了解利益相关者及其目标,事先定义成功标准(例如你将如何评估与浏览、导航和购买相关的不同结果),并期望提供一系列解决方案,以便产品团队进行权衡。
嵌入数据团队成员以提高速度,但保持强大的核心——产品团队对数据需求旺盛,乐于实验,并感受到速度的必要性。因此,大多数数据团队会发现利用分散的方法,并嵌入数据分析师以增加与产品团队的接近度、避免瓶颈并提高速度是有帮助的。
然而,我建议只在以下情况下这样做:
-
数据平台在工具和流程方面已经相当成熟,能够支持高效的分析工作(同样适用于数据科学/机器学习工作)。
-
数据团队拥有足够的资源来嵌入主管/经理以及个别贡献者,以便你可以在分布式模型下进行管理,同时继续构建更强大的核心(或“卓越中心”)。
-
产品组织有明确而独特的目标,否则你分析师的工作将充其量只是重叠,最糟糕的情况是毫无意义。
另一个需要考虑的挑战是,一旦你开始在每个产品团队中嵌入分析师,你就会转移管理这些团队所需的关键数据产品。你可能会无意中创建一个孤岛,错过生产有利于多个产品团队的分析或数据产品的机会。
通过共享平台和服务可以捕获协同效应。关于费用分摊模型的问题可能会出现,最好避免这种复杂性,除非实际情况迫使你不得不面对(各团队之间成本差异较大)。
启用自助点击流数据,但不要期望这是万能的——数据团队需要做出的关键决策之一是如何处理其点击流数据,它在理解用户行为方面作为不同数据源之间的连接纽带发挥了关键作用。
这个决策很少是容易的——虽然数据团队的客户通常说他们希望访问原始数据,但他们通常指的是希望访问经过高度筛选、可信的数据,并以一种他们可以轻松操作的方式提供。
自助解决方案(例如 Amplitude、Mixpanel)提供了许多现成的可视化和可下载报告,这些是你的产品客户急需的,但这将要求你遵循他们的数据模型,并且需要数据团队在仪表化、治理、培训和对所选工具的支持上投入大量精力。
另一方面,在你的数据仓库之上构建以产品为中心的报告,通过 BI 工具或更多自定义的数据应用,可能为数据团队提供他们所需的一致性和灵活性,但通常需要为每个新的指标、报告或屏幕进行开发。
无论你选择哪条路径,最终目标都应是为了更好或更快的产品决策。因此,你应考虑更广泛访问的好处,以及数据团队从采用这一解决方案中工作负载的变化。一个好的、轻量级的自服务实施将使你的分析师能够从事更有意义的工作。

图片由 Shane Murray 提供。
另一个需要记住的关于点击流数据的决定性特征是其短暂的价值。产品和市场的根本变化使得从五年前的点击流数据中提取见解几乎是徒劳的。有些人建议 时间衰减可以表示为 1/x。这里的要点是,在迁移或其他数据重组项目中,你通常可以将其留在冷存储中(如果你还保留的话)。
“真正的艺术家会发布作品”
著名的史蒂夫·乔布斯名言恰如其分地总结了有效的数据产品团队关系。在最佳状态下,两个团队共同协作,发布新的有效功能和产品,并由最佳的数据驱动决策…以及一些艺术直觉指导。
关注我以获取更多关于数据领导力、数据科学应用及相关主题的故事。订阅以将我的故事送到你的收件箱。
测量新 Pandas 2.0 相对于 Polars 和 Datatable 的速度——仍然不够好
尽管新的 PyArrow 后端为 Pandas 带来了令人兴奋的功能,但在速度方面仍然令人失望。
·发表于 Towards Data Science ·7 分钟阅读·2023 年 3 月 29 日
--

作者提供的图像来自 Midjourney
自从第一次尝试用 read_csv 读取一个大小为一千兆字节的数据集,并意识到需要等待 - 喘息 - 五秒钟以来,人们一直在抱怨 Pandas 的速度。是的,我也是那些抱怨者之一。
五秒钟听起来可能不多,但当加载数据集本身需要这么长时间时,通常意味着后续操作也会需要这么久。而且,由于速度是快速、简便的数据探索中最重要的因素之一,这可能让你感到非常沮丧。
因此,PyData 的人员最近宣布了计划发布带有全新 PyArrow 后端的 Pandas 2.0。对于完全不了解的人,PyArrow 本身是一个设计用于高性能、内存高效处理数组的小巧库。
人们真心希望新的后端能带来比原生 Pandas 更显著的加速。本文将通过将 PyArrow 后端与两种最快的数据框库 Datatable 和 Polars 进行比较来测试这一希望的光芒。
难道这些人还没有做过这个吗?
既然 H20 目前已经运行了受欢迎的 Database-like Ops Benchmark 来测量几乎 15 个库在三种不同数据集大小上的三个数据操作的计算速度,那做这个基准测试还有什么意义?我的基准测试不可能做到那么全面。
好吧,首先,这个基准测试没有包括带有 PyArrow 后端的 Pandas,并且最后一次更新是在 2021 年,那已经很久了。
其次,基准测试在一台拥有 40 个 CPU 核心、128 GB 内存和 20 GB GPU 的超级机器上运行(cuDF,有人用过吗?)。普通用户通常无法使用这样的机器,因此比较在像我这样日常设备上的库之间的差异非常重要。它配备了一个拥有十几个核心的中等性能 CPU 和 32 GB 的内存。
最后,我提倡过程中的完全透明,所以我将详细解释基准测试代码,并将其作为 GitHub Gist 提供,供您在自己的机器上运行。
安装和设置
我们首先安装 Pandas 2.0 的 RC(候选版本),以及最新版本的 PyArrow、Datatable 和 Polars。
pip install -U "pandas==2.0.0rc0" pyarrow datatable polars
import datatable as dt
import pandas as pd
import polars as pl
dt.__version__
'1.0.0'
pd.__version__
'2.0.0rc0'
pl.__version__
'0.16.14'
我使用 NumPy 和 Faker 库创建了一个合成数据集,以模拟人口普查数据集中的典型特征,并将其保存为 CSV 和 Parquet 格式。以下是文件路径:
from pathlib import Path
data = Path("data")
data_csv = data / "census_data.csv"
data_parquet = data / "census_data.parquet"
查看这个 GitHub gist以查看生成数据的代码。
有 5000 万行七个特征,使文件大小达到约 2.5 GB。
基准测试结果
在展示代码之前,我们先看看好东西——基准测试结果:

作者提供的图片
一开始,我们可以看到 PyArrow Pandas 在所有类别中都排在最后(或在groupby中倒数第二)。
请不要将阅读和写入 Parquet 类别中的不存在的条形图误认为是 0 运行时间。这些操作在 Datatable 中不受支持。
在其他类别中,Datatable 和 Polars 并列第一,Polars 稍微占有优势。
写入 CSV 文件一直是 Pandas 的慢速过程,我猜新后端不足以改变这一点。
你应该切换吗?
所以,百万美元的问题是——你应该切换到更快的 Polars 还是 Datatable?
答案是我非常讨厌的“这要看情况”。你是否愿意为了更快的速度牺牲 Pandas 近二十年的成熟度,以及说实话,愚蠢的容易和熟悉的语法?
在这种情况下,请记住,你花时间学习新库的语法可能会平衡其性能提升。
但是,如果你只是处理大量数据集,那么学习这些快速库中的任何一个可能从长远来看都值得付出努力。
如果你决定继续使用 Pandas,请仔细阅读Pandas 用户指南的性能提升页面。它概述了一些技巧和窍门,以在不依赖第三方库的情况下为 Pandas 引擎增加额外的动力。
此外,如果你被困在一个大的 CSV 文件中,仍然想使用 Pandas,你应该记住以下代码片段:
import datatable as dt
import pandas as pd
df = dt.fread("data.csv").to_pandas()
它以 Datatable 的速度读取文件,将其转换为 Pandas DataFrame 几乎是瞬间完成的。
基准测试代码
好了,最后来看代码吧。
导入库后,首先要做的是定义一个 DataFrame 来存储基准测试结果。这将使绘图过程变得更简单。
import time
import datatable as dt
import pandas as pd
import polars as pl
# Define a DataFrame to store the results
results_df = pd.DataFrame(
columns=["Function", "Library", "Runtime (s)"]
)
它有三列,一列用于任务名称,另一列用于库名称,还有一列用于存储运行时间。
然后,我们定义一个timer装饰器,执行以下任务:
-
测量装饰函数的运行时间。
-
提取函数的名称和其
library参数的值。 -
将运行时间、函数名称和库名称存储到传递的结果 DataFrame 中。
def timer(results: pd.DataFrame):
"""
A decorator to measure the runtime of the passed function.
It stores the runtime, the function name, and the passed
function's "library" parameter into the `results` DataFrame
as a single row.
"""
这个想法是定义一个通用函数,如read_csv,用于读取三种库中的任意一个的 CSV 文件,可以通过像library这样的参数进行控制:
# Task 1: Reading CSVs
@timer(results_df)
def read_csv(path, library):
if library == "pandas":
return pd.read_csv(path, engine="pyarrow")
elif library == "polars":
return pl.read_csv(path)
elif library == "datatable":
return dt.fread(str(path))
注意我们是如何用timer(results_df)装饰函数的。
我们以类似的方式定义其他任务的函数(参见the Gist中的函数体):
# Task 2: Writing to CSVs
@timer(results_df)
def write_to_csv(df, path, library):
...
# Task 3: Reading to Parquet
@timer(results_df)
def read_parquet(path, library):
...
# Task 4: Writing to Parquet
@timer(results_df)
def write_to_parquet(df, path, library):
...
# Task 5: Sort
@timer(results_df)
def sort(df, column, library):
...
# Task 6: Groupby
@timer(results_df)
def groupby(df, library):
...
然后,我们为每个库运行这些函数:
from pathlib import Path
# Define the file paths
data = Path("data")
data_csv = data / "census_data.csv"
data_parquet = data / "census_data.parquet"
# libraries = ["pandas", "polars", "datatable"]
l = "datatable"
# Task 3/4
df = read_parquet(data_parquet, library=l)
write_to_parquet(df, data_parquet, library=l)
# Task 1/2
df = read_csv(data_csv, library=l)
write_to_csv(df, data_csv, library=l)
# Task 5/6
sort(df, "age", library=l)
groupby(df, library=l)
为了避免内存错误,我避免了循环,并在 Jupyter Notebook 中运行了三次基准测试,改变了l变量。
然后,我们使用可爱的 Seaborn 创建基准图形,展示以下简单的柱状图:
g = sns.catplot(
data=results_df,
kind="bar",
x="Function",
y="Runtime (s)",
hue="Library",
)

图片由作者提供
事情正在发生变化
多年来,Pandas 依赖于 NumPy 的肩膀,因为 NumPy 在流行度上迅猛增长。NumPy 慷慨地借用了其快速计算和数组操作的功能。
但这种方法受限于 NumPy 对文本和缺失值的糟糕支持。Pandas 不能使用本地 Python 数据类型如列表和字典,因为那样会在大规模上成为笑柄。
因此,Pandas 在过去几年中已悄然远离 NumPy。例如,它在 2020 年就引入了 PyArrow 数据类型用于字符串。它还使用了用其他语言编写的扩展,如 C++和 Rust,用于日期(含时区)或分类数据等复杂数据类型。
现在,Pandas 2.0 拥有一个全面的后端,支持所有数据类型,基于 Apache Arrow 的 PyArrow 实现。除了明显的速度提升外,它还提供了更好的缺失值支持、互操作性和更广泛的数据类型支持。
因此,即使后端仍然比其他 DataFrame 库慢,我仍然热切期待其正式发布。感谢阅读!
这里有一些页面可以了解更多关于 Pandas 2.0 和 PyArrow 后端的信息:
喜欢这篇文章以及它那奇特的写作风格?想象一下,获取更多类似的文章,全部由一位才华横溢、迷人幽默的作者(对了,那就是我 :)。
只需 4.99 美元的会员费用,你将不仅能访问我的故事,还能获取来自 Medium 上最聪明、最杰出思想者的宝贵知识。如果你使用我的推荐链接,你将获得我超级感激的心意和一个虚拟的击掌,以支持我的工作。
[## 通过我的推荐链接加入 Medium — Bex T.
独享所有⚡高级⚡内容,并在 Medium 上无限畅游。通过购买我一杯…
ibexorigin.medium.com](https://ibexorigin.medium.com/membership?source=post_page-----e44dc78f6585--------------------------------) 
媒体组合建模:数据科学家避免陷阱的技术指南
如何理解输出以及最大化其实际应用
·
关注 发布在 Towards Data Science ·6 分钟阅读·Mar 31, 2023
--
TLDR: 媒体组合建模(MMM)是一种用于市场测量的统计方法,但它并不是像其他技术那样“一刀切”的解决方案。我将介绍数据科学家提高建模技巧和实现改进结果的重要检查点,广告报告与 MMM 之间的区别,以及多点触摸归因(MTA)与 MMM 之间的区别。

图片由Austin Distel提供,来源于Unsplash。
介绍
MMM 是一种统计方法,用于理解和衡量投资回报率(ROI)并优化媒体预算。
在饮料、消费品、汽车和时尚行业拥有巨额广告预算的公司中的数据科学家们一直在致力于改善 MMM。此外,广告技术公司,如谷歌和 Meta,最近也积极关注 MMM,因为 GDPR 和苹果 IDFA 弃用等隐私法规影响了数字世界的追踪准确性。
欲了解更多信息,请参考我之前发布的这篇文章。
## 媒体混合建模:如何使用 Python 和 LightweightMMM 衡量广告效果
媒体混合建模,它的实施及实用技巧
[towardsdatascience.com
在发布上述文章后,我收到了许多积极的反馈和一些来自 Towards Data Science 读者的问题。一些常见的问题包括:
(1) 成功的 MMM 项目的检查点是什么?
(2) 广告报告和 MMM 有何不同?
(3) 多触点归因(MTA)和 MMM 之间有什么区别?
在这篇文章中,我将解决这些问题,并提供见解,以帮助你更好地理解这些概念。

图片由krakenimages提供,来源于Unsplash。
(1) 成功的 MMM 项目的检查点是什么?
为了确保 MMM 的可靠性和准确性,数据科学家在相信结果之前应考虑以下检查点:一些想法受到谷歌研究论文的启发:Chan, D., & Perry, M. (2017). 媒体混合建模中的挑战与机遇。
- ✔️ 足够的数据点:
-
确保有足够的数据点来建模广告渠道及其关系。
-
通常,MMM 需要至少两年的周级数据。然而,如果你没有这么多数据,每日数据也是可以接受的,但在这种情况下,你需要更加仔细地审查异常值。
2. ✔️ 选择适当的输入变量:
-
输入数据的变异性至关重要。例如,如果媒体频道 X 的支出在整个时间段内保持不变,模型可能难以确定其对销售的影响。在这种情况下,排除该频道作为输入变量可能会更有利。
-
保留影响销售的因素。例如,如果杂志 X 的支出显著小于其他广告且仅为暂时性支出,可能更适合将其排除。
-
输入变量可以在模型前期和模型选择阶段进行选择。
3. ✔️ 处理相关的输入变量:
-
检查输入变量之间的潜在多重共线性,这可能导致高方差系数估计和将销售归因于特定广告渠道的困难。
-
如果你不断为 Meta 和 Tiktok 广告分配相同的预算,模型的可靠性可能会受到影响,使得评估这些媒体渠道的影响变得困难。
-
使用方法来解决相关变量,例如正则化技术或变量选择方法。
4. ✔️ 控制选择偏差:
-
注意潜在的选择偏差问题,例如季节性。
-
例如,11 月份的销售增加可能不是由于广告,而是消费者需求或黑色星期五促销。
-
根据我的经验,内部促销数据或定价数据可以作为不可观测需求变量的有价值的控制因素。
5. ✔️ 验证外推假设:
-
外推假设涉及对模型在可用数据范围之外的行为进行预测或得出结论。
-
在尝试回答诸如“如果将我的广告支出 X 增加十倍会发生什么”或“如果我突然停止对广告 X 的支出会发生什么”等商业问题时,请小心。
-
测试模型在不同场景下的表现,并在解释需要显著外推的结果时要小心。
6. ✔️ 测试模型性能:
-
通过将 MMM 的预测与实际销售和营销数据进行比较来验证 MMM。
-
R-squared 和 MAPE,即均值绝对百分比误差,是 MMM 的知名度量指标。一般来说,如果 R2 大于 0.8,则被认为是好的。此外,对于 MAPE,目标是 20% 或以下。
7. ✔️ 进行随机实验
-
使用 MMM 估计媒体预算变化的影响时,建议进行随机实验以验证发现。
-
例如,通过将区域划分为测试组和对照组并调整广告支出,你可以测量提升效果并获得更准确的结果。
通过解决这些检查点,数据科学家可以提高对 MMM 结果有效性和可靠性的信心,并根据模型提供的见解做出更明智的决策。
(2) 广告报告和 MMM 有什么不同? 🤔
直截了当地说,MMM 提供了关于营销表现的更广泛视角,帮助企业从长期角度分配资源并优化整体营销组合。相比之下,像 Google/Meta Ads 这样的广告报告提供了对特定广告或活动的更细致见解,允许进行调整以改善短期表现。
媒体组合建模(MMM)的特点:
-
MMM 专注于帮助企业优化整体营销组合和资源分配。
-
它考虑了不同媒体渠道之间的相互作用,包括在线和离线。
-
即使转换点是离线的,也可以使用。
广告报告的特点:
-
广告报告提供了有关特定广告或活动表现的更详细见解,如点击率(CTR)和每次点击费用(CPC)。
-
这些报告主要关注短期表现,帮助广告主快速掌握单个活动的进展。
-
实际客户有多个媒体接触点。多个媒体的交织效果无法在每个媒体的广告报告中考虑。

作者提供的图像:Google Ads 上的广告报告
(3)Multi-Touch Attribution(MTA)和 MMM 之间的区别是什么? 🤔
Multi-Touch Attribution(MTA)是另一种营销测量方法,旨在使用其访问日志将信用分配给客户旅程中的不同营销接触点。
每种方法都有其优缺点。与 MMM 检查每个营销渠道的汇总成本不同,MTA 提供了对各个接触点贡献的更细致和可靠的视角。
另一个区别是跟踪非数字客户行为。MTA 专注于数字接触点和数字转换,而 MMM 考虑了离线媒体支出的成本和离线转换的结果。

作者提供的图像:MTA 不跟踪离线活动
MMM 更适用于
-
一个品牌投资于电视和杂志等离线媒体,并希望评估其影响时,MTA 更为适用。
-
一个品牌通过离线和在线渠道销售商品/ 一个品牌主要通过离线渠道销售商品。
-
财务视角更为重要。
-
一个品牌拥有过去 2-3 年的历史广告和销售数据。
另一方面,当
-
一个品牌在 Google Ads 和 Meta Ads 等数字媒体上花费很多。
-
一个品牌主要在线销售商品。
-
优先考虑理解客户旅程。
-
需要实时评估。
最后
感谢阅读!如果你有任何问题/建议,请随时通过Linkedin与我联系!此外,如果你在 Towards Data Science 上关注我,我会很高兴。
参考
-
媒体混合建模:如何使用 Python 和 LightweightMMM 测量广告效果
-
通过营销混合建模超级提升你的跨渠道客户获取
认识 Gemini:谷歌最大、最强大的 AI 模型
原文:
towardsdatascience.com/meet-gemini-googles-largest-and-most-powerful-ai-model-2ffd2f07490f
这个下一代 AI 模型在几乎所有学术基准测试中超过了 ChatGPT。
·发布于 Towards Data Science ·6 分钟阅读·2023 年 12 月 9 日
--

图片由 Mitchell Luo 拍摄,来源于 Unsplash
当 OpenAI 去年 11 月发布 ChatGPT 时,每个人心中都有一个迫切的问题——科技巨头们在做什么?
像谷歌这样的公司会何时回应这一发展?
我们现在有了答案。
2023 年 12 月 6 日,谷歌宣布了他们最新的 AI 模型:Gemini。
根据公司首席执行官 Sundar Pichai 的说法,这项技术是人工智能的巨大飞跃,将影响几乎所有谷歌的产品。
Gemini 提供 3 种尺寸
当前版本的模型称为 Gemini 1.0。它可以处理文本、图像、视频和音频,并且有 3 种不同的尺寸。
Gemini Nano 是一个小型、高效的版本,可以在原生环境和 Android 设备上运行。
Gemini Pro,中级版本,在能力和效率之间取得了平衡。这个模型的表现比谷歌之前的旗舰模型 PaLM-2 好得多。目前它为 Bard 聊天机器人提供支持。
最终,Gemini Ultra 是系列中最强大的模型。它在复杂推理方面表现出色,并且在各种基准测试中超越了 OpenAI 的 GPT-4 模型。
Gemini Ultra 尚未公开发布。谷歌宣布该模型将在明年初推出,尽管没有提供具体的时间框架。
Gemini 与 OpenAI 的 GPT 模型有何不同?
OpenAI 的 ChatGPT 目前由 2 个 AI 模型提供支持——免费版的 GPT-3.5 和付费版的 GPT-4。
几个月前,OpenAI 宣布 GPT-4 具有多模态能力(即能够 处理文本、音频和图像)。
然而,尽管它可以处理各种数据类型,模型的主要设计和功能仍然集中在 基于文本的输入和输出 上。
这意味着 GPT-4 是一个基于文本的框架,视觉和音频处理模型在其基础上作为第二阶段构建。
例如,将 GPT-4 与图像生成模型 DALLE-3 结合,可以将文本转换为如下所示的图像:

虽然 GPT-4 在处理其他模态的训练与其基于文本的训练是分开的,但 Gemini 从一开始就接受了文本、图像、视频和音频的多样化数据集训练。
简而言之,多模态能力是从基础上构建到 Gemini 中的,以确保它能原生理解所有数据类型。
这种架构差异意味着 Gemini 更容易进行泛化,因为它理解来自文本、图像、音频和视频的信息。
像 GPT-4 和 DALLE-3 这样的模型,则针对 特定任务 进行了微调(GPT-4 针对文本,DALLE-3 针对图像)。
在 Google Deepmind 关于 Gemini 的研究论文中,提出了以下问题:
一个未解的问题是,这种联合训练是否能产生一个在每个领域都有强大能力的模型——即使与针对单一领域的模型和方法相比。
这个问题涉及 AI 领域中的一个基本挑战:专业化模型与通用模型之间的权衡。
针对单一领域设计的模型通常在该特定任务上的表现更好,相比之下,通过联合训练开发的模型可能表现较差。
现在让我们将 Gemini 的表现与专门针对特定领域的最先进模型进行比较。
Gemini 在几乎所有基准测试中都超过了 ChatGPT
在 Google Deepmind 最新的 报告 中,模型的性能与其他算法如 Claude 2、PaLM-2、GPT-3.5 和 GPT-4 进行了比较。
下面是 Gemini 在各个领域与最佳模型的对比:
文本
Gemini 在文本基准测试中的表现与现有模型的对比:

图片来自 Deepmind 的技术报告
在涵盖推理能力、阅读理解、STEM 和编码的多个基于文本的基准测试中,注意到 Gemini Ultra 在 9 个评估中的 8 个中优于 OpenAI 的 GPT-4 模型。
根据 Google Deepmind 的报告,当使用一种名为“链式思维”的提示技术时,模型表现最佳。
在思维链提示中,你将一个问题分解,并引导 AI 通过逐步推理的过程,类似于人类解决问题的方式。
这通常比单纯地将整个问题抛给 AI 模型更有效。
你可以阅读这个指南,了解更多关于思维链提示的信息。
同时,请注意Gemini Pro(中档版本),目前为 Bard 提供支持,在几乎所有基准测试中都超越了 GPT-3.5。
自 Gemini Pro 发布以来,我一直在进行实验,个人认为它的响应与 GPT-3.5 相当。
我计划对它们的推理和编码能力进行更详细的比较,并会尽快发布后续文章。
图像理解
即使在特定任务上没有先前的训练,Gemini Ultra 的视觉能力仍然超过了那些为这些基准测试进行了微调的其他模型:

第一个基准测试,MMMU,包含了 6 个学科的大学级问题——商业、科学、人文学科、艺术、技术和医学。
这个基准不仅要求强大的推理能力和大学级的专业知识,而且问题也独特地基于图像。
准确回答这些问题并不容易,因为模型必须做两件事——解释视觉元素和进行复杂的文本分析。
Gemini Ultra 在这个基准测试中超过了所有其他 AI 模型,突显了其强大的多模态能力和泛化能力。
视频理解
Gemini Ultra 在视频相关任务中也达到了最先进的性能——它能够添加字幕和根据视频回答问题。
例如,给定这个足球比赛视频,该模型被要求提供有关如何提高球员技术的建议。
这是 Gemini 在分析视频后的回应:
This soccer player needs to work on their ball-striking mechanics. Their foot is not making
good contact with the ball, which is causing the ball to fly up and away from the goal. They
also need to work on their body positioning. They should be leaning into the shot with their
non-kicking leg in front of their kicking leg, and they should be following through with their
kicking leg.
同样,Gemini Ultra 在图像生成和音频理解任务中表现出了卓越的性能。你可以在这里阅读完整报告。
尽管该模型在学术基准测试中的表现看起来很有前景,但请记住,在研究环境中 4-5%的改进可能未必会对现实世界产生实际影响。
在我看来,将 AI 模型整合到其他应用程序中并为特定用例(如自动化数据分析或增强搜索能力)进行产品化,将比性能的边际改进产生更大的影响。
我期待看到 Google 如何将这个新模型整合到其产品套件中,特别是 Google Analytics,以生成更先进的见解和预测。
我从关于 Gemini 的论文中得到的最大收获是,一个以多模态为核心构建的模型可以超越专为特定任务设计的算法。
这种方法可能是实现人工通用智能(AGI)的重要一步,其目标是建立一个可以应用于广泛任务的 AI 模型。
下面是如何开始使用 Gemini 的方法。
正如本文前面提到的,你可以通过访问Bard 聊天机器人今天开始使用 Gemini Pro。
谷歌还宣布,Gemini Nano(最轻版本)将很快被集成到 Pixel 智能手机中,从 Pixel 8 Pro 开始。
这将允许你使用 Recorder 应用创建对话的自动总结,并在 WhatsApp 上生成高质量的回复建议。
最后,Gemini 将于 12 月 13 日通过 Google Generative AI Studio 向开发者开放。你还可以注册谷歌的实时讲座,了解如何在 Google Cloud 上使用 Gemini 构建应用程序。
缓存 DataFrame 函数
使用可哈希 DataFrame 和消息摘要来优化重复计算
·
关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 3 月 3 日
--
作者提供的照片
提高软件系统运行性能的一个著名技术是函数缓存。缓存是一种应用于单个函数的缓存方法。如果函数多次使用相同的参数调用,可以通过将结果存储在映射(或磁盘)中来避免重复计算。后续调用时,如果找到参数,则返回存储的结果。
这种机会伴随着权衡。记忆化以空间换时间:先前计算的结果必须存储在内存或磁盘上。此外,被记忆化的函数必须是纯粹的:输出必须完全由输入决定。最后,并非所有类型的函数参数都是合适的。在内存记忆化中,结果存储在映射中,参数必须是可哈希和不可变的。在基于磁盘的记忆化中,结果存储在文件中,参数必须能还原为唯一的文件名;从密码学哈希函数派生的消息摘要是实现此目的的最佳选择。
记忆化的另一个挑战是缓存失效:为了避免缓存过度增长,必须丢弃缓存。Python 标准库通过 functools.lru_cache() 装饰器提供了一种内存解决方案。该装饰器通过“最近最少使用”(LRU)缓存失效策略实现记忆化:在达到最大计数后,会丢弃最近最少使用的缓存。
对于使用 Pandas DataFrame 作为函数参数的 Python 程序员,还有更多挑战。由于 Pandas DataFrame 和 Series 是可变容器,因此它们不可哈希。如果参数是 Pandas DataFrame,则 functools.lru_cache() 将失败。
>>> import functools
>>> @functools.lru_cache
... def cube(v):
... return v ** 3
...
>>> import pandas as pd
>>> df = pd.DataFrame(np.arange(1_000_000).reshape(1000, 1000))
>>> cube(df)
Traceback (most recent call last):
TypeError: unhashable type: 'DataFrame'
StaticFrame 是一个替代 DataFrame 的库,为内存和基于磁盘的记忆化提供了高效的解决方案。
哈希函数和哈希碰撞
在展示使用 StaticFrame 的 DataFrame 记忆化之前,区分不同类型的哈希函数是重要的。
哈希函数将可变大小的值转换为较小的(通常是)固定大小的值。哈希碰撞是指不同的输入哈希到相同的结果。对于某些应用,哈希碰撞是可以接受的。密码学哈希函数旨在消除碰撞。
在 Python 中,内置的 hash() 函数将可哈希对象转换为整数。任意类型可以通过实现魔法方法 __hash__() 提供支持。重要的是,hash() 的结果并不具备抗碰撞能力:
>>> hash('')
0
>>> hash(0)
0
>>> hash(False)
0
Python 字典使用 hash() 将字典键转换为低级 C 数组中的存储位置。预期会有碰撞,如果发生碰撞,会使用 __eq__() 进行相等性比较解决。因此,要使任意类型可哈希,它需要实现 __hash__() 和 __eq__()。
密码学哈希函数不同于 hash():它们旨在避免碰撞。Python 在 hashlib 库中实现了一系列密码学哈希函数。这些函数处理字节数据,并通过 hexdigest() 方法返回一个消息摘要字符串。
>>> import hashlib
>>> hashlib.sha256(b'').hexdigest()
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
>>> hashlib.sha256(b'0').hexdigest()
'5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9'
>>> hashlib.sha256(b'False').hexdigest()
'60a33e6cf5151f2d52eddae9685cfa270426aa89d8dbc7dfb854606f1d1a40fe'
内存记忆化
要对以 DataFrames 作为参数的函数进行记忆化,需要一个不可变且可哈希的 DataFrame。StaticFrame 提供了 FrameHE 来实现这一点,其中 “HE” 代表“哈希、相等”,这是 Python 哈希功能所需的两个实现。虽然 StaticFrame 的 Frame 是不可变的,但它不是可哈希的。
FrameHE.__hash__() 方法返回索引和列标签的 hash()。虽然这会与任何具有相同标签但值不同的 FrameHE 冲突,但仅使用标签会将更昂贵的完整值比较推迟到 __eq__()。
FrameHE.__eq__() 方法的实现简单地委托给 Frame.equals(),这是一个始终返回单一布尔值的方法。这与 Frame.__eq__() 不同,后者返回的是布尔 Frame 中的逐元素比较结果。
>>> f = sf.FrameHE(np.arange(1_000_000).reshape(1000, 1000))
>>> hash(f)
8397108298071051538
>>> f == f * 2
False
使用 FrameHE 作为参数时,可以使用装饰了 functools.lru_cache() 的 cube() 函数。如果没有 FrameHE,可以使用 to_frame_he() 方法从其他 StaticFrame 容器中高效地创建 FrameHE。由于底层的 NumPy 数组数据是不可变且可以在容器之间共享的,这是一种轻量级、无复制的操作。如果来自 Pandas DataFrame,可以使用 FrameHE.from_pandas()。
在下面的示例中,cube() 函数与上述创建的 FrameHE 一起调用。IPython 的 %time 工具显示,在首次调用后,使用相同参数的后续调用速度提高了三个数量级(从毫秒到微秒)。
>>> %time cube(f)
CPU times: user 8.24 ms, sys: 99 µs, total: 8.34 ms
>>> %time cube(f)
CPU times: user 5 µs, sys: 4 µs, total: 9 µs
尽管对内存中的记忆化非常有用,FrameHE 实例也可以作为集合的成员,提供了一种收集唯一容器的新方法。
从 DataFrame 创建消息摘要
虽然内存中的记忆化提供了最佳性能,但缓存会消耗系统内存,并且在进程结束后不会持久化。如果函数结果较大或缓存需要持久化,基于磁盘的记忆化是一个替代方案。
在这种情况下,参数的可变性和可哈希性无关紧要。相反,可以从文件中检索缓存结果,该文件名源自参数。对参数应用加密哈希函数是理想的选择。
由于哈希函数通常以字节数据作为输入,因此 Frame 及其所有组件必须转换为字节表示。常见的方法是将 Frame 序列化为 JSON(或其他字符串表示),然后再转换为字节。由于底层的 NumPy 数组数据已经以字节存储,将这些数据转换为字符串是低效的。此外,由于 JSON 不支持全部 NumPy 类型,JSON 输入可能也不够独特,从而导致哈希碰撞。
StaticFrame 提供了 via_hashlib() 来满足这一需求,提供了一种有效的方法来将字节输入传递给 Python hashlib 模块中的加密哈希函数。以下是使用 SHA-256 的示例。
>>> f.via_hashlib(include_name=False).sha256().hexdigest()
'b931bd5662bb75949404f3735acf652cf177c5236e9d20342851417325dd026c'
首先,调用 via_hashlib() 并选择要包括在输入字节中的容器组件。由于默认的 name 属性 None 无法进行字节编码,因此被排除。其次,调用哈希函数构造器 sha256(),返回一个加载了适当输入字节的实例。第三,调用 hexdigest() 方法以字符串形式返回消息摘要。还可以使用其他密码学哈希函数构造器,如 sha3_256、shake_256 和 blake2b。
为了创建输入字节,StaticFrame 连接了所有基础字节数据(包括值和标签),可选地包括容器元数据(如 name 和 __class__.__name__ 属性)。使用 via_hashlib().to_bytes() 方法可以获得相同的字节表示。如果需要,这可以与其他字节数据结合以创建基于多个组件的哈希摘要。
>>> len(f.via_hashlib(include_name=False).to_bytes())
8016017
StaticFrame 对创建消息摘要的内置支持显示出比 Pandas 的两种常见方法更高效。第一种方法使用 Pandas 工具函数 pd.hash_pandas_object() 来派生每列的整数哈希值。这个例程使用了一种定制的摘要算法,没有声称具有密码学碰撞抵抗性。为了比较,这里将这些每列的整数哈希值作为 hashlib 消息摘要函数的输入。第二种方法提供了整个 DataFrame 的 JSON 表示,作为 hashlib 消息摘要函数的输入。虽然这可能比 pd.hash_pandas_object() 更具碰撞抵抗性,但通常较慢。下图显示了这两种方法与 via_hashlib() 的性能特征比较。在各种 DataFrame 形状和类型组合中,via_hashlib() 表现优于所有方法,只有一个例外。

作者图表
基于磁盘的记忆化
给定将 DataFrame 转换为哈希摘要的方法后,可以实现基于磁盘的缓存例程。下面的装饰器适用于接收和返回单个Frame的函数。在这个例程中,文件名是从参数的消息摘要派生的,前缀是函数的名称。如果文件名不存在,则调用装饰的函数并写入结果。如果文件名存在,则加载并返回。这里使用了 StaticFrame NPZ 文件格式。正如最近 PyCon 讲座中演示的,将 Frame 存储为 NPZ 通常比 Parquet 和相关格式要快得多,并提供完整的往返序列化。
>>> def disk_cache(func):
... def wrapped(arg):
... fn = '.'.join(func.__name__, arg.via_hashlib(include_name=False).sha256().hexdigest(), 'npz')
... fp = Path('/tmp') / fn
... if not fp.exists():
... func(arg).to_npz(fp)
... return sf.Frame.from_npz(fp)
... return wrapped
为了演示这个装饰器,它可以应用于一个迭代十行窗口、对列求和,然后将结果连接成一个单一 Frame 的函数。
>>> @disk_cache
... def windowed_sum(v):
... return sf.Frame.from_concat(v.iter_window_items(size=10).apply_iter(lambda l, f: f.sum().rename(l)))
在第一次使用后,性能降低到原始运行时间的不到 20%。虽然加载基于磁盘的缓存比检索内存缓存要慢,但避免重复计算的好处可以在不消耗内存的情况下获得,并且可以利用持久缓存。
>>> %time windowed_sum(f)
CPU times: user 596 ms, sys: 15.6 ms, total: 612 ms
>>> %time windowed_sum(f)
CPU times: user 77.3 ms, sys: 24.4 ms, total: 102 ms
via_hashlib()接口可以在其他情况下作为 DataFrame 所有特征的数字签名或校验和。
结论
如果纯函数在相同的参数下被调用多次,记忆化可以极大地提高性能。虽然处理输入和输出 DataFrame 的函数需要特别处理,但 StaticFrame 提供了便捷的工具来实现内存和基于磁盘的记忆化。虽然必须小心确保缓存被正确失效并避免冲突,但当消除重复工作时,可以实现显著的性能提升。
Apache Spark 中的内存管理:磁盘溢出
原文:
towardsdatascience.com/memory-management-in-apache-spark-disk-spill-59385256b68c
它是什么以及如何处理
·发表于 Towards Data Science ·阅读时间 12 分钟·2023 年 9 月 15 日
--

照片由 benjamin lehman 提供,来源于 Unsplash
在大数据领域,Apache Spark 因其极快的数据处理能力而备受喜爱。作为世界上排名第一的大数据处理引擎,学习使用这个工具是任何大数据专业人士技能组合中的基石。而这一路径中的一个重要步骤是了解 Spark 的内存管理系统及“磁盘溢出”的挑战。
磁盘溢出是指当 Spark 无法将数据全部存储在内存中时,需要将其存储到磁盘上。Spark 的主要优势之一是其内存处理能力,这比使用磁盘驱动器要快得多。因此,构建会溢出到磁盘的应用程序在一定程度上违背了 Spark 的目的。
磁盘溢出有许多不良后果,因此学习如何处理它是 Spark 开发者的重要技能。这就是本文旨在帮助的内容。我们将深入了解磁盘溢出是什么,为什么会发生,它的后果是什么,以及如何解决它。通过使用 Spark 的内置 UI,我们将学习如何识别磁盘溢出的迹象并理解其指标。最后,我们将探讨一些减轻磁盘溢出的可操作策略,例如有效的数据分区、适当的缓存和动态集群调整。
Spark 中的内存管理
在深入探讨磁盘溢出之前,了解 Spark 中的内存管理如何工作是非常有用的,因为这在磁盘溢出的发生和管理中起着至关重要的作用。
Spark 被设计为一个内存数据处理引擎,这意味着它主要使用 RAM 来存储和处理数据,而不是依赖磁盘存储。这种内存计算能力是使 Spark 快速高效的关键特性之一。
Spark 为其操作分配了有限的内存,这些内存被分为不同的部分,这些部分组成了所谓的统一内存:

作者提供的图片
存储内存
这是 Spark 存储将来要重用的数据的地方,比如缓存的数据和广播变量。通过将这些数据保持在随时可用的状态,Spark 可以通过快速检索数据来提高数据处理任务的性能。
你可以在第一次读取后将 DataFrame 缓存到存储内存中,而不是每次单独分析时从磁盘读取销售数据。这样,对于后续的分析,Spark 可以快速访问缓存的数据,从而提高整个过程的效率。
执行内存
这部分内存是 Spark 用于计算的。因此,当你执行连接或聚合时,Spark 使用执行内存。例如,要计算 DataFrame 中一列的平均值,Spark 会:
-
将 DataFrame 的相关部分加载到执行内存中(可能从存储内存中的缓存 DataFrame 中提取)。
-
执行聚合,将中间的和和计数存储在执行内存中。
-
计算最终的平均值,仍然使用执行内存进行计算。
-
输出最终结果,释放用于操作的执行内存。
用户内存
这是用于你创建的自定义数据结构或变量的地方,但这些不由 Spark 直接管理。这就像是你在 Spark 应用程序中的工作区。执行内存用于 Spark 主动处理的数据,而用户内存通常包含元数据、自定义哈希表或其他你可能需要的结构。
假设你有一个包含年龄列的 DataFrame,并且你想在应用程序中稍后的自定义逻辑中跟踪最大年龄。你会读取 DataFrame,计算最大年龄,然后将该年龄存储为一个变量,该变量会存储在用户内存中。这个变量是你稍后会使用的信息,但它并没有被 Spark 的内置操作主动处理,因此它属于用户内存。
与主要存储缓存 DataFrame/RDD 的存储内存不同,用户内存不由 Spark 管理。这意味着你需要确保你使用的用户内存不会超过分配的内存,以避免内存不足(OOM)错误。
保留内存
保留内存用于系统级操作和 Spark 的内部对象。与执行内存、存储内存或用户内存不同,这些内存用于 Spark 应用程序中的特定任务或数据,保留内存用于 Spark 自身的内部操作。
这种内存是“保留”的,因为它不能用于你的数据或任务。这就像你计算机上的操作系统保留了一些磁盘空间用于系统文件和操作——你不能将这些空间用于自己的文件。
存储与执行内存之间的动态分配
在 Spark 中,存储内存和执行内存之间的关系就像两个人分享一个派。这个派代表了总的可用内存,而每种内存——存储和执行——都想要这块派的一部分。
如果执行内存有更多任务需要处理并且需要更多资源,它可以占用更大的内存份额,留下较小的份额给存储内存。相反,如果存储内存需要缓存更多数据,它可以占用更大的份额,留下更少的份额给执行内存。就像这样:

作者提供的图片
如果另一个不需要,执行内存和存储内存都可能占用整个派(总的可用内存)。然而,执行内存在需要更多时对存储内存的份额有“优先权”。但有个限制——它不能占用过多,以至于存储内存留下的份额小于最低限度。
更技术性的说,执行内存和存储内存共享一个统一的内存区域,其中任何一个可以占用整个区域,如果另一个未使用;然而,执行内存可以从存储内存中驱逐数据,直到某个阈值,超过该阈值则不允许驱逐,而存储内存不能驱逐执行内存。
数据从存储内存中的驱逐通常是由最近最少使用(LRU)策略来管理的。这意味着,当执行内存需要更多空间并决定从存储内存中驱逐数据时,它通常会移除那些最近被访问最少的数据。其想法是,你很久没有使用的数据相比最近访问的数据,可能不太容易立即需要。
Spark 中的磁盘溢出
Spark 喜欢在内存中进行大部分工作,因为这是处理数据的最快方式。但当你的数据比内存大时会发生什么?当你的总内存是 100GB,但你的 DataFrame 是 110GB 时会发生什么?当数据过大而无法放入内存时,它将被写入磁盘。这被称为磁盘溢出。
磁盘溢出可以来自存储内存(当缓存的 DataFrame 太大时)或执行内存(当操作需要大量的中间数据存储时)。回想一下,当执行内存需要额外的空间用于诸如连接或洗牌等任务时,它可以从存储内存中借用——但仅限于一定的限制。如果执行内存达到这个限制仍然需要更多空间,Spark 将会将多余的数据溢出到磁盘:

作者提供的图片
当存储内存需要更多空间但达到从执行内存借用的限制时,也会发生同样的情况:

作者提供的图片
分区大小对磁盘溢出的影响
当你将一个表加载到 Spark 中时,它会被拆分成可管理的块,称为分区,这些分区会被分配到工作节点上。Spark 会自动将数据拆分成 200 个分区,但你也可以指定 Spark 将数据拆分成多少个分区。了解需要使用多少个分区是理解 Spark 的一件重要事情,它也与磁盘溢出有关。
我们在前一节中讨论的统一内存是以每个节点为基础存在的。每个工作节点都有自己的统一内存,包括存储内存、执行内存、用户内存和保留内存。每个工作节点会将尽可能多的分区装入其统一内存中。但是,如果这些分区太大,部分分区会溢出到磁盘上。
想象一下你正在处理一个 160GB 大小的 DataFrame,并且你告诉 Spark 将其分成 8 个分区。因此,每个分区将为 20GB。现在,如果你的集群中有 10 个工作节点,每个节点总共有 16GB 内存,那么没有一个分区能完全适应单个节点的内存。我们最多能在内存中放入 16GB 的分区,其余 4GB 将被溢出到磁盘。
然而,如果我们将分区数增加到 10,那么每个分区将为 16GB —— 恰好能适应内存!了解分区对于理解磁盘溢出至关重要,也有助于更全面地理解 Spark。为 Spark 任务选择合适的分区数对于快速执行至关重要。
磁盘溢出的成本
磁盘溢出非常低效。Spark 不仅需要花时间将数据写入磁盘,还需要花费更多时间在需要数据时将其读回。这些读写操作是昂贵的,可能会对你的 Spark 应用产生重大影响。
性能影响 磁盘 I/O(读写)比内存访问显著慢,这可能导致作业完成时间变长。
资源利用 磁盘溢出可能导致资源的低效使用。本可以用于计算的 CPU 周期被浪费在读写操作上。
操作复杂性 频繁发生磁盘溢出的 Spark 应用程序可能更复杂。你不仅需要监控 CPU 和内存使用情况,还要监控磁盘使用情况,为你的操作考虑增加了另一层复杂性。
成本影响 在基于云的环境中,你通常会按使用的计算资源计费,包括存储、CPU、内存,有时还包括网络利用。如果磁盘溢出导致 Spark 任务运行变慢,你将需要更长时间运行云实例才能完成相同的工作,这会增加你的成本。
磁盘溢出的原因
造成内存不足进而导致磁盘溢出的常见场景有:
-
大数据集: 当处理的数据超过可用内存时,Spark 会将多余的数据溢出到磁盘。
-
复杂操作: 需要大量中间数据的任务,如连接、聚合和洗牌,如果执行内存不足,可能会导致磁盘溢出。
-
不适当的分区: 当你有太少的分区,而分区大小大于可用内存时,Spark 会将不适合的分区部分溢出到磁盘。
-
多个缓存的 DataFrames/RDDs: 如果你缓存了多个大型 DataFrames 或 RDDs,而存储内存已满,则最近最少使用的数据会被溢出到磁盘。
-
并发任务: 运行多个每个需要大量内存的任务可能会导致磁盘溢出,因为它们争夺有限的内存资源。
-
数据倾斜: 数据倾斜可能导致某些分区的数据远多于其他分区。负责这些“重”分区的工作节点可能会耗尽内存,迫使它们将多余的数据写入磁盘。
-
配置不足: 有时候,默认或用户定义的 Spark 配置没有为某些操作分配足够的内存,导致磁盘溢出。
识别磁盘溢出
识别磁盘溢出的最简单方法是通过 Spark UI。进入“Stages”选项卡,然后点击一个阶段。在下面的图片中,我按“Duration”排序阶段,并选择了持续时间最长的阶段。长时间运行不一定意味着磁盘溢出,但长时间运行的阶段通常是开始调查的好地方,因为这些阶段可能表示长时间的洗牌操作或数据倾斜。

图片作者
在这里我们可以看到关于这个阶段的各种指标,包括磁盘溢出的量(如果有的话):

图片作者
“溢出(内存)”显示了数据在溢出到磁盘之前的内存大小。而“溢出(磁盘)”则显示了溢出到磁盘后的数据大小,在溢出之后。
“溢出(磁盘)”比“溢出(内存)”小的原因是数据写入磁盘时,会进行序列化和压缩。序列化是将数据对象转换为字节流,以便于存储或传输,而压缩则涉及减少数据大小,以节省存储空间或加快传输速度。
缓解磁盘溢出
一旦识别了磁盘溢出,下一步是缓解它。你如何处理磁盘溢出将取决于你的具体情况和造成溢出的原因。以下是针对常见磁盘溢出原因的一些一般建议。
了解根本原因 在采取任何措施来减轻磁盘溢出之前,你需要了解它发生的原因。使用 Spark UI 识别导致溢出的操作,查看“Stages”选项卡中的“Spill (Disk)”和“Spill (Memory)”等指标。一旦确定了根本原因,采取适当的解决方案。
有效地划分数据 有效的数据划分是最小化 Spark 中磁盘溢出的关键策略,特别是由于 shuffle 操作。Shuffle 是一种昂贵的操作,它会在分区间重新分配数据,并且通常需要大量的中间存储。如果无法在内存中存储这些中间数据,它将溢出到磁盘。更好的数据划分可以减少对 shuffle 的需求(即使没有磁盘溢出,shuffle 也是一种计算上昂贵的操作)。
通过优化数据的划分方式并减少在任务如连接或聚合过程中数据移动的需求,你可以降低磁盘溢出的风险。
你不能总是避免 shuffle,但你可以有效地划分数据。推荐的分区大小应小于 1GB——如果更大,可能会遇到长时间的 shuffle 和磁盘溢出。
有关优化 Spark 中分区 shuffle 的策略,请参见此指南:
通过采用有效的 shuffle 分区策略来提升你的 Spark 性能
适当缓存数据 在 Spark 中缓存数据可以通过将最常访问的数据保留在内存中来显著加快迭代算法。然而,这是一把双刃剑。缓存过多的数据会迅速填满你的存储内存,减少执行内存的空间,并增加磁盘溢出的可能性。
知道何时缓存数据以及何时不缓存数据是至关重要的。选择性地缓存,并考虑缓存与重新计算数据的权衡。
想了解更多关于缓存的信息,请阅读这篇文章:
## 解锁更快的 Spark 操作:PySpark 中的缓存
在 Apache Spark 中弥合重计算与快速结果之间的差距
解决数据倾斜 数据倾斜发生在数据不成比例地集中在少数几个分区中,导致集群内存使用不均衡。这可能导致磁盘溢出,因为那些过载的分区可能无法完全放入内存中。
像加盐这样的策略可以帮助更均匀地重新分配数据到各个分区,从而降低溢出的风险。
要了解如何解决数据倾斜并防止它导致磁盘溢出,请参见:
处理 PySpark 中数据倾斜的策略和技术
调整集群规模 向集群中添加更多内存可以帮助减少磁盘溢出,因为执行内存和存储内存有更多的操作空间。这是一个简单但昂贵的解决方案——它可以立即缓解内存压力,但会增加计算成本,具体是否值得取决于磁盘溢出的严重程度。
替代简单地增加集群内存的方法是启用动态扩展,这允许 Spark 根据需要添加或移除节点。这意味着当 Spark 需要更多内存时,它可以添加一个额外的工作节点,然后在不再需要时将其移除。
然而,最好是确定溢出的原因并使用适当的解决方案来解决它。如果这不可行,才考虑扩大你的集群——有时你确实需要更多内存。
结论
Spark 中的磁盘溢出是一个复杂的问题,可能会显著影响 Spark 应用程序的性能、成本和操作复杂性。了解它是什么、为何发生以及如何缓解是 Spark 开发者的重要技能。
在 Spark UI 中监视你的应用程序,并注意溢出情况。当你发现它时,诊断原因,然后你可以应用适当的解决方案,无论是对数据倾斜进行加盐处理还是对大规模洗牌操作进行重新分区。
可能很诱人直接增加集群内存或启用动态扩展,因为这些可以是快速的解决方案。然而,它们通常伴随着增加的成本。因此,识别磁盘溢出的根本原因并解决那个具体问题是很重要的。有关如何处理本文中讨论的特定磁盘溢出原因的更多信息,请阅读文中推荐的帖子。
通过采取积极的磁盘溢出管理方法,你可以确保你的 Spark 应用程序以尽可能高效的方式运行,从长远来看节省金钱和时间。
Meta AI 推出了突破性的图像分割模型,该模型基于 10 亿个掩码进行训练。
Segment Anything - 最佳深度学习图像分割模型。
·发表在 Towards Data Science ·7 分钟阅读·2023 年 4 月 6 日
--
介绍
在 OpenAI 的 ChatGPT 在自然语言处理领域取得革命性进展之后,人工智能的进步继续进行,Meta AI 也在计算机视觉领域取得了令人惊叹的进展。Meta AI 研究团队推出了名为 Segment Anything Model(SAM)的模型,并在 1100 万张图像上使用 10 亿个掩码的数据集。图像分割是识别哪些图像像素属于一个对象。

由 ai.facebook.com 提供的图像分割演示
该项目主要包括三个支柱:任务、模型和数据。
1. Segment Anything 任务
Meta AI 团队的主要目标是创建一个可以根据用户输入提示工作的图像分割模型,就像 ChatGPT 一样。因此,他们提出了将用户输入与图像集成以生成分割掩码的解决方案。分割提示可以是任何指示图像中要分割内容的信息。例如,前景或背景点的集合、框、自由形式的文本等。因此,模型的输出是给定任何用户定义提示的有效分割掩码。
2. Segment Anything 模型
可推广的 Segment Anything Model(SAM)有三个组件,如下图所示。

Segment Anything 模型工作流程由 ai.facebook.com 提供
高级模型架构由图像编码器、提示编码器和掩码解码器组成。对于图像编码器,他们使用了具有 Vision Transformer (ViT) [2] 架构的 MAE [1] 预训练模型。ViT 模型在图像分类和分割任务中是最先进的模型。至于提示,他们将其分为两种类型——一种是稀疏的提示,如点、框和文本,另一种是密集的提示,如掩码。提示编码器步骤为每种类型的提示创建嵌入。至于掩码解码器,它仅将图像嵌入、提示嵌入和输出令牌映射到掩码上。
3.2 Segment Anything 数据集
3.1 Segment Anything 数据引擎

垃圾进垃圾出(作者图像)
原则——垃圾进垃圾出——同样适用于 AI 领域。如果输入数据质量较差,则模型生成的结果也不会好。这就是为什么 Meta 团队试图选择高质量图像来训练他们的模型。该团队创建了一个数据引擎来筛选原始图像数据集。创建数据引擎分为三个阶段。
-
人工阶段:专业的人工标注员参与手动标注图像上的掩码。
-
半自动阶段:他们在标注的图像上训练了模型,并对其余图像进行了推断。然后,要求人工标注员标注模型未检测到的附加未标记对象,或纠正置信度低的分段。
-
完全自动阶段:这一阶段包括自动掩码生成和自动过滤阶段,试图保留非歧义掩码,并根据置信度、稳定性和大小保留掩码。
3.2 Segment Anything 数据集
Segment Anything 数据引擎创建了一个包含 10 亿个掩码的数据集(SA-1B),这些掩码基于 1100 万张多样化、高分辨率(平均 3300x4900 像素)和授权的图像。值得一提的是,99.1% 的掩码是自动生成的,但由于它们经过精心选择,质量非常高。
结论——为什么该模型具有革命性
Meta AI 团队与其他大型公司团队一起在 AI 发展方面取得了重大进展。Segment Anything 模型(SAM)具有为许多领域中的应用提供支持的能力,这些领域需要在任何图像中查找和分割任何对象。例如:
-
SAM 可能是一个大型多模态模型的组成部分,该模型集成了图像、文本、音频等。
-
SAM 可以基于用户的视线在 AR/VR 领域选择对象,然后将其“提取”到 3D 中。
-
SAM 可以提升创意应用,例如提取图像区域以进行视频编辑。
-
还有更多。
图像分割演示
在这一部分,我将尝试使用官方 GitHub 代码来玩转算法,并使用Google Colab对图像进行两种类型的分割。首先,我将进行用户定义提示的分割,其次,我将进行完全自动的分割。
第一部分:使用用户定义提示进行图像分割
- 设置(导入库和安装)
from IPython.display import display, HTML
import numpy as np
import torch
import matplotlib.pyplot as plt
import cv2
display(HTML(
"""
<a target="_blank" href="https://colab.research.google.com/github/facebookresearch/segment-anything/blob/main/notebooks/predictor_example.ipynb">
<img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/>
</a>
"""
))
using_colab = True
if using_colab:
import torch
import torchvision
print("PyTorch version:", torch.__version__)
print("Torchvision version:", torchvision.__version__)
print("CUDA is available:", torch.cuda.is_available())
import sys
!{sys.executable} -m pip install opencv-python matplotlib
!{sys.executable} -m pip install 'git+https://github.com/facebookresearch/segment-anything.git'
!mkdir images
!wget -P images https://raw.githubusercontent.com/facebookresearch/segment-anything/main/notebooks/images/truck.jpg
!wget -P images https://raw.githubusercontent.com/facebookresearch/segment-anything/main/notebooks/images/groceries.jpg
!wget https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth
2. 辅助函数用于在图像上绘制掩码、点和框。
def show_mask(mask, ax, random_color=False):
if random_color:
color = np.concatenate([np.random.random(3), np.array([0.6])], axis=0)
else:
color = np.array([30/255, 144/255, 255/255, 0.6])
h, w = mask.shape[-2:]
mask_image = mask.reshape(h, w, 1) * color.reshape(1, 1, -1)
ax.imshow(mask_image)
def show_points(coords, labels, ax, marker_size=375):
pos_points = coords[labels==1]
neg_points = coords[labels==0]
ax.scatter(pos_points[:, 0], pos_points[:, 1], color='green', marker='*', s=marker_size, edgecolor='white', linewidth=1.25)
ax.scatter(neg_points[:, 0], neg_points[:, 1], color='red', marker='*', s=marker_size, edgecolor='white', linewidth=1.25)
3. 输入图像(初始图像进行分割)。让我们尝试选择第一袋食品的掩码。
image = cv2.imread('/content/images/groceries.jpg')
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
plt.figure(figsize=(5,5))
plt.imshow(image)
plt.axis('on')
plt.show()

输入图像(图像来自 Facebook 研究)
4. 加载名为sam_vit_h_4b8939.pth的预训练模型,该模型为默认模型。还有其他更轻量的模型版本,如sam_vit_l_0b3195.pth和sam_vit_b_01ec64.pth
sam_checkpoint = "/content/sam_vit_h_4b8939.pth"
device = "cuda"
model_type = "default"
import sys
sys.path.append("..")
from segment_anything import sam_model_registry, SamPredictor
sam = sam_model_registrymodel_type
sam.to(device=device)
predictor = SamPredictor(sam)
predictor.set_image(image)
5. 可视化图像上的点(用户提示),这将有助于识别我们的目标对象 — 第一个食品袋。
input_point = np.array([[465, 300]])
input_label = np.array([1])
plt.figure(figsize=(10,10))
plt.imshow(image)
show_points(input_point, input_label, plt.gca())
plt.axis('on')
plt.show()

带用户提示的输入图像(图像来自 Facebook 研究)
6. 进行预测以生成目标对象的掩码。
masks, scores, logits = predictor.predict(
point_coords=input_point,
point_labels=input_label,
multimask_output=True,
)
print(masks.shape) # (number_of_masks) x H x W
7. 显示前 3 个生成的掩码。当multimask_output=True时,算法返回三个掩码。稍后我们可以选择分数最高的一个。
for i, (mask, score) in enumerate(zip(masks, scores)):
plt.figure(figsize=(10, 10))
plt.imshow(image)
show_mask(mask, plt.gca())
show_points(input_point, input_label, plt.gca())
plt.title(f"Mask {i+1}, Score: {score:.3f}", fontsize=18)
plt.axis('off')
plt.show()

预测结果(图像由作者提供)
突出的对象是模型预测的掩码。结果显示,模型生成了三个输出掩码,预测分数分别为:掩码 1 — 0.990,掩码 2 — 0.875 和掩码 3 — 0.827。我们选择了分数最高的掩码 1。瞧!!!模型预测的掩码就是我们最初想要分割的目标对象。结果令人惊讶,模型表现得相当好!
第二部分:完全自动的图像分割 — 续
- 分段的绘图函数
def show_anns(anns):
if len(anns) == 0:
return
sorted_anns = sorted(anns, key=(lambda x: x['area']), reverse=True)
ax = plt.gca()
ax.set_autoscale_on(False)
polygons = []
color = []
for ann in sorted_anns:
m = ann['segmentation']
img = np.ones((m.shape[0], m.shape[1], 3))
color_mask = np.random.random((1, 3)).tolist()[0]
for i in range(3):
img[:,:,i] = color_mask[i]
ax.imshow(np.dstack((img, m*0.35)))
2. 自动生成掩码
from segment_anything import sam_model_registry, SamAutomaticMaskGenerator, SamPredictor
sam = sam_model_registrymodel_type
sam.to(device=device)
mask_generator = SamAutomaticMaskGenerator(sam)
masks = mask_generator.generate(image)
print(len(masks))
3. 显示结果
plt.figure(figsize=(5,5))
plt.imshow(image)
show_anns(masks)
plt.axis('off')
plt.show()

SAM 的自动分割结果(图像由作者提供)
算法使用默认参数识别了 137 个不同的对象(掩码)。每个掩码包含关于分段区域、边界框坐标、预测分数和稳定性分数的信息,这些信息可以用来过滤掉不需要的分段。
希望你喜欢这篇文章,并且现在可以开始自己创建美丽的应用程序了。如果你有任何问题或想分享对这篇文章的看法,请随时评论,我会很高兴回答。
如果你想直接支持我的工作并且获得 Medium 文章的无限访问权限,请通过我的 推荐链接 成为 Medium 会员。在此,万分感谢,祝你有美好的一天!
[## 通过我的推荐链接加入 Medium - Gurami Keretchashvili
阅读 Gurami Keretchashvili(以及 Medium 上成千上万其他作者)的每一个故事。你的会员费直接...
medium.com](https://medium.com/@gkeretchashvili/membership?source=post_page-----8f13c86a13a2--------------------------------)
参考文献
[1] Kaiming He, Xinlei Chen, Saining Xie, Yanghao Li, Piotr Dollar, 和 Ross Girshick. 掩蔽自编码器是可扩展的视觉学习者. CVPR, 2022.
[2] Alexey Dosovitskiy, Lucas Beyer, Alexander Kolesnikov, Dirk Weissenborn, Xiaohua Zhai, Thomas Unterthiner, Mostafa Dehghani, Matthias Minderer, Georg Heigold, Sylvain Gelly, Jakob Uszkoreit, 和 Neil Houlsby. 一张图胜过 16x16 个词:大规模图像识别的变换器. ICLR, 2021.
[3] Alexander Kirillov, Eric Mintun, Nikhila Ravi, Hanzi Mao, Chloe Rolland, Laura Gustafson, Tete Xiao, Spencer Whitehead, Alexander C. Berg, Wan-Yen Lo, Piotr Dollar, Ross Girshick. Segment Anything, 2023
我之前关于机器学习部署的文章
在网络上部署你的机器学习模型的最简单方法。
towardsdatascience.com ## 如何部署机器学习模型
在网络上部署机器学习模型的最简单方法
towardsdatascience.com
Meta AI 的另一个革命性大规模模型——DINOv2 用于图像特征提取。
DINOv2 是一种出色的自监督 ViT 基于深度学习模型,用于图像特征提取。
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 6 月 26 日
--
介绍
Meta AI 介绍了图像特征提取模型的新版本 DINOv2,该模型自动从图像中提取视觉特征。这是 AI 领域,特别是在计算机视觉领域的数据和模型扩展方面的又一步革命性进展。

DINOv2 的演示由 ai.facebook.com 提供
动机——我们为什么要关注?
DINOv2 是一个自监督模型,无需微调且性能良好。此外,它还可以作为许多计算机视觉任务的主干网络,例如:
-
分类,细粒度分类——例如猫与狗,或例如狗品种识别
-
图像检索——例如,从互联网上大量图像中找到与您的包相似的图像。
-
语义图像分割——为图像中的每一个像素关联标签或类别。
-
视频理解——自动识别和解释视频的各种方面,如对象、动作、事件、场景,甚至更高级的概念。
-
单目深度估计——预测图像中一个物体是位于前景还是背景。
-
图像聚类——将图像分组到簇中,使得同一簇中的图像彼此相似
-
基于内容的推荐系统——根据图像的表示向用户推荐项目。
DINOv2 补充了另一项近期计算机视觉研究,包括 Segment Anything。Segment Anything 是一个可提示的分割系统,专注于对多样化的分割任务进行零-shot 泛化。
你可以查看我之前的帖子中的 Segment Anything Model (SAM)。
## Meta AI 推出基于 10 亿个掩膜训练的革命性图像分割模型
Segment Anything Model (SAM) - 最佳深度学习图像分割模型
towardsdatascience.com
方法论
论文的主要贡献可以是以下几点:
-
创建一个大型整理的训练数据集
-
改进训练算法和实现
-
设计一个功能性的蒸馏管道。
创建一个大型整理的训练数据集
大型深度学习模型需要大量的数据进行训练。这就是为什么作者创建了一个自动化流程,如下图所示,以获取整理的训练数据集。

整理图像创建的工作流程 [1]
他们从大约 25 个第三方数据集中选择了一组种子图像,并且他们的目标是增强这些种子图像。其工作原理如下。他们从互联网上抓取了大量未经整理的图像(约 12 亿张独特图像)。之后,他们使用在 ImageNet-22k 上预训练的自监督 ViT-H/16 网络创建了图像嵌入。其次,他们使用余弦相似度作为图像间的距离度量,以过滤重复图像,从而减少冗余并增加多样性。在从大规模未整理数据集中移除重复图像后,他们进行了图像检索阶段,其中提取了与整理图像相似的图像。最终,这种方法使他们能够从 12 亿张未整理的图像数据库中创建出 1.42 亿张整理图像。
改进的训练算法
DINOv2 使用了学生-教师机制,这是一种训练技术,其中一个较小的神经网络(称为学生)学习模仿一个较大或更复杂的神经网络(称为教师)的行为。学生和教师基于视觉变换器架构(ViT)[2]。至于损失函数,他们使用交叉熵损失来计算学生-教师特征的相似性。为了学习图像的局部和全局特征,他们使用了不同层次的学习。对于全局学习,他们使用的是图像层次学习——对同一图像进行随机裁剪数据增强。至于局部特征学习,他们使用了补丁层次学习——随机对输入补丁应用遮罩,仅对学生应用,不对教师应用。此外,他们还进行了不同的归一化技术,如 Sinkhorn-knop 批归一化等。更多细节可以在论文中找到。
蒸馏管道。
使用大型模型进行预测(推理)时需要强大的硬件。为了克服这一限制,他们还将大型模型压缩为较小的模型。知识蒸馏 [5] 旨在通过最小化一组给定输入的两个输出之间的距离来用较小的模型重现大型模型的输出。训练算法基于自蒸馏,使得将大型模型压缩为较小模型变得简单。
结果
他们在八个不同的计算机视觉任务上评估了模型性能,并与其他方法进行了比较。
在下面的图表中,DINOv2 模型的结果为深蓝色,其他自监督方法为浅橙色,弱监督方法则用深粉色突出显示。虚线水平线表示表现最佳的弱监督模型。
结果表明,DINOv2 模型在自监督学习领域大幅超越了之前的最先进技术,达到了与弱监督特征相当的性能。

DINOv2 与其他 SOTA 模型 [1]
结论
总之,DINOv2 是 Meta AI 团队推出的另一个革命性模型。它无需微调,可以作为许多不同计算机视觉模型的基础。DINOv2 使用自监督机制,能够从任何图像集合中学习。
DINOv2 演示 — 细粒度图像分类
在这一部分,我将尝试展示 DINOv2 在实际场景中的工作原理。我将创建一个细粒度图像分类任务。
分类工作流程:
-
从 PyTorch 数据集中下载 Food101 数据集。
-
使用小型 DINOv2 从训练和测试数据集中提取特征
-
使用从训练数据集中提取的特征训练机器学习分类模型(SVM、XGBoost 和 KNN)。
-
对测试数据集提取的特征进行预测。
-
评估每个机器学习模型的准确率和 F1 分数。
数据:Food 101 是一个具有挑战性的 101 个食品类别的数据集,共有 101,000 张图像。每个类别提供 250 张手动审核的测试图像以及 750 张训练图像。
模型:小型 DINOv2 模型(ViT-S/14 蒸馏版)
机器学习模型:SVM、XGBoost、KNN。
步骤 1 — 设置(你可以使用 Google Colab 来运行代码并开启 GPU)
import torch
import numpy as np
import torchvision
from torchvision import transforms
from torch.utils.data import Subset, DataLoader
import matplotlib.pyplot as plt
import time
import os
import random
from tqdm import tqdm
from xgboost import XGBClassifier
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, f1_score
import pandas as pd
def set_seed(no):
torch.manual_seed(no)
random.seed(no)
np.random.seed(no)
os.environ['PYTHONHASHSEED'] = str()
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
set_seed(100)
步骤 2 — 创建转换,下载并创建 Food101 Pytorch 数据集,创建训练和测试数据加载器对象。
batch_size = 8
transformation = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
trainset = torchvision.datasets.Food101(root='./data', split='train',
download=True, transform=transformation)
testset = torchvision.datasets.Food101(root='./data', split='test',
download=True, transform=transformation)
# train_indices = random.sample(range(len(trainset)), 20000)
# test_indices = random.sample(range(len(testset)), 5000)
# trainset = Subset(trainset, train_indices)
# testset = Subset(testset, test_indices)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size,
shuffle=True)
testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size,
shuffle=False)
classes = trainset.classes
print(len(trainset), len(testset))
print(len(trainloader), len(testloader))
[输出] 75750 25250
[输出] 9469 3157
步骤 3(可选) — 可视化训练数据加载器批次
# Get a batch of images
dataiter = iter(trainloader)
images, labels = next(dataiter)
# Plot the images
fig, axes = plt.subplots(1, len(images),figsize=(12,12))
for i, ax in enumerate(axes):
# Convert the tensor image to numpy format
image = images[i].numpy()
image = image.transpose((1, 2, 0)) # Transpose to (height, width, channels)
# Normalize the image
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
normalized_image = (image * std) + mean
# Display the image
ax.imshow(normalized_image)
ax.axis('off')
ax.set_title(f'Label: {labels[i]}')
# Show the plot
plt.show()

批量图像
步骤 4 — 加载小型 DINOv2 模型并从训练和测试数据加载器中提取特征。
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
dinov2_vits14 = torch.hub.load('facebookresearch/dinov2', 'dinov2_vits14').to(device)
#training
train_embeddings = []
train_labels = []
dinov2_vits14.eval()
with torch.no_grad():
for data, labels in tqdm(trainloader):
image_embeddings_batch = dinov2_vits14(data.to(device))
train_embeddings.append(image_embeddings_batch.detach().cpu().numpy())
train_labels.append(labels.detach().cpu().numpy())
#testing
test_embeddings = []
test_labels = []
dinov2_vits14.eval()
with torch.no_grad():
for data, labels in tqdm(testloader):
image_embeddings_batch = dinov2_vits14(data.to(device))
test_embeddings.append(image_embeddings_batch.detach().cpu().numpy())
test_labels.append(labels.detach().cpu().numpy())
#concatinate result
train_embeddings_f = np.vstack(train_embeddings)
train_labels_f = np.concatenate(train_labels).flatten()
test_embeddings_f = np.vstack(test_embeddings)
test_labels_f = np.concatenate(test_labels).flatten()
train_embeddings_f.shape, train_labels_f.shape, test_embeddings_f.shape, test_labels_f.shape
[输出] ((75750, 384), (75750,), (25250, 384), (25250,))
步骤 5 — 为 SVM、XGBoost 和 KNN 分类器构建一个函数。
def evaluate_classifiers(X_train, y_train, X_test, y_test):
# Support Vector Machine (SVM)
svm_classifier = SVC()
svm_classifier.fit(X_train, y_train)
svm_predictions = svm_classifier.predict(X_test)
# XGBoost Classifier
xgb_classifier = XGBClassifier(tree_method='gpu_hist')
xgb_classifier.fit(X_train, y_train)
xgb_predictions = xgb_classifier.predict(X_test)
# K-Nearest Neighbors (KNN) Classifier
knn_classifier = KNeighborsClassifier()
knn_classifier.fit(X_train, y_train)
knn_predictions = knn_classifier.predict(X_test)
# Calculating Top-1
top1_svm = accuracy_score(y_test, svm_predictions)
top1_xgb = accuracy_score(y_test, xgb_predictions)
top1_knn = accuracy_score(y_test, knn_predictions)
# Calculating F1 Score
f1_svm = f1_score(y_test, svm_predictions, average='weighted')
f1_xgb = f1_score(y_test, xgb_predictions, average='weighted')
f1_knn = f1_score(y_test, knn_predictions, average='weighted')
return pd.DataFrame({
'SVM': {'Top-1 Accuracy': top1_svm, 'F1 Score': f1_svm},
'XGBoost': {'Top-1 Accuracy': top1_xgb,'F1 Score': f1_xgb},
'KNN': {'Top-1 Accuracy': top1_knn, 'F1 Score': f1_knn}
})
X_train = train_embeddings_f # Training data features
y_train = train_labels_f # Training data labels
X_test = test_embeddings_f # Test data features
y_test = test_labels_f # Test data labels
results = evaluate_classifiers(X_train, y_train, X_test, y_test)
print(results)
结果

小型 DINOv2 + SVM/XGBoost/KNN 的结果(图片由作者提供)
哇,结果真棒!正如展示的那样,基于小型 DINOv2 提取特征训练的 SVM 模型优于其他机器学习模型,达到了近 90%的准确率。
结论
尽管我们使用了小型 DINOv2 模型提取特征,但在提取特征上训练的机器学习模型(尤其是 SVM)在细粒度分类任务上表现出色。该模型能够在 101 个不同类别中以接近 90%的准确率进行分类。
如果使用大型、超大或巨型 DINOv2 模型,准确率会提高。你只需在第 4 步将dinov2_vits14替换为dinov2_vitb14、dinov2_vitl14或dinov2_vitg14。你可以尝试一下,欢迎在评论区分享准确率结果 😃
希望你喜欢这篇文章。如果你有任何问题或想分享对这篇文章的看法,请随时评论,我将很高兴回答。
如果你想直接支持我的工作,并且获得 Medium 文章的无限访问权限,请使用我的推荐链接成为 Medium 会员。非常感谢,祝你有美好的一天!
[## 使用我的推荐链接加入 Medium - Gurami Keretchashvili
作为 Medium 会员,你的一部分会员费用会分配给你阅读的作者,你可以完全访问每一篇故事……
medium.com](https://medium.com/@gkeretchashvili/membership?source=post_page-----1114b287eadd--------------------------------)
参考文献
[1] Oquab, M., Darcet, T., Moutakanni, T., Vo, H., Szafraniec, M., Khalidov, V., … & Bojanowski, P. (2023). Dinov2: 在没有监督的情况下学习稳健的视觉特征。arXiv 预印本 arXiv:2304.07193。
[2] Dosovitskiy, A., Beyer, L., Kolesnikov, A., Weissenborn, D., Zhai, X., Unterthiner, T., … & Houlsby, N. (2020). 一张图片值 16x16 个词:用于大规模图像识别的 Transformers。arXiv 预印本 arXiv:2010.11929。
[3] DINOv2 团队,DINOv2: 具有自监督学习的最先进计算机视觉模型
[4] DINOv2 Github
[5] Hinton, G., Vinyals, O., & Dean, J. (2015). 提炼神经网络中的知识。arXiv 预印本 arXiv:1503.02531。
元启发式方法解析:蚁群优化
原文:
towardsdatascience.com/meta-heuristics-explained-ant-colony-optimization-d016fe925108

跟随信息素路径的蚂蚁。图像由作者使用 Dall·E 创建。
关于一种较少为人知的启发式算法,它基于蚂蚁的行为
·发表于 Towards Data Science ·阅读时长 7 分钟·2023 年 9 月 4 日
--
在优化算法的世界里,有许多方法灵感来自自然界的奇观。从基于进化的遗传算法到模拟退火的降温策略,这些算法在解决复杂问题上已证明其有效性。然而,在这个自然启发算法的多样化景观中,隐藏着一颗鲜为人知的宝石——蚁群优化。我们将深入探讨这种启发式算法,它汲取了蚂蚁巧妙觅食行为的灵感。
蚁群优化(ACO)是一个有趣的算法,核心部分出乎意料的简单。在这篇文章中,你将学习基础知识并理解算法背后的主要思想。在接下来的文章中,我们将编写代码,并使用它来解决几个现实世界的问题。让我们开始吧!
使用蚂蚁解决优化问题
如你所知,ACO(蚁群优化)受蚂蚁行为的启发。该算法模拟蚂蚁寻找食物和相互沟通的方式,以找到它们巢穴和食物源之间的最短路径。你可以使用该算法来寻找图中的良好路径,或用于解决分配类问题。
ACO 中使用了一群人工蚂蚁。它们通过逐步构建解决方案来探索解决方案空间。每只蚂蚁通过基于概率分布选择下一个组件来构建解决方案。该概率分布受到组件质量(例如路径长度)和其他蚂蚁留下的信息素轨迹的影响。信息素轨迹代表了蚂蚁之间的一种通信形式,使它们能够跟随过去成功的路径。
在算法开始时,每个组件上的信息素轨迹被初始化为一个较小的值。当蚂蚁构建解决方案时,它们在使用的组件上沉积信息素。沉积的信息素量与解决方案的质量成正比。作为良好解决方案一部分的组件会被更多的信息素强化,使其对其他蚂蚁更具吸引力。
随着时间的推移,较少吸引人的组件上的信息素轨迹会蒸发,从而降低它们的吸引力,并鼓励探索解决方案空间的其他部分。通过这种方式,算法通过利用搜索空间中最有前景的区域,同时也探索新的区域,从而收敛到一个好的解决方案。

ACO 可视化:蚂蚁从巢穴出发寻找食物。开始时它们会探索所有轨迹,但很快它们会发现最快的路径,并避免长而死胡同的轨迹。图像由作者提供。
超参数
超参数用于控制算法的行为。以下是 ACO 的关键超参数:
迭代次数
这是蚂蚁在算法终止之前构建解决方案的次数。你也可以使用其他终止标准,例如当解决方案在一段时间内没有改进时,你可以决定停止运行。或者你可以设置时间限制。
蚂蚁数量
每次迭代中使用的人工蚂蚁数量。更多的蚂蚁可以更好地探索搜索空间,但也会增加计算时间。
信息素蒸发率(rho)
该参数控制信息素从轨迹中蒸发的速率,取值范围在 0 到 1 之间。较高的衰减率可以鼓励更多地探索搜索空间,而较低的衰减率可以导致更多地利用好的解决方案。
初始信息素水平(tau)
该值对应于轨迹上的初始信息素量。较高的初始信息素水平可以鼓励更多地利用好的解决方案,而较低的初始信息素水平可以鼓励更多地探索搜索空间。你可以使用特定的 tau 值来创建一个温暖的起始。
信息素的重要性 (alpha)
Alpha (≥ 0) 确定信息素水平 (tau) 在选择下一个图组件中的影响。如果值较高,算法对搜索空间的变化更敏感,而较低的值有助于防止过早收敛。
成本的重要性 (beta)
Beta (≥ 0) 与 alpha 可比。它不是确定信息素水平的影响,而是确定成本 (eta) 对选择下一个图组件的影响。更多内容在下一节中介绍。
超参数的选择将取决于具体问题和可用的计算资源。需要通过实验不同的超参数设置,以找到适合特定问题的最佳组合。
伪代码
以下是算法的伪代码:
initialize
problem details
graph
eta = cost per edge
hyperparameters
n_iterations
n_ants
alpha = pheromone importance
beta = cost importance
rho = pheromone evaporation rate
tau = pheromone initialization
for it in n_iterations
x_best, y_best = [], inf
calculate edge attractiveness
update tau (pheromone levels)
for ant in n_ants
x_best, y_best = find ant trail
return x_best
首先,你需要用问题的详细信息和超参数初始化算法。实际的算法包括几个重要的步骤:
计算边的吸引力
这决定了某条边对蚂蚁的吸引力。它取决于 tau(每条边的信息素水平)、eta(每条边的成本)、alpha(信息素的重要性)和 beta(成本的重要性)。
让我们称矩阵的边的吸引力为 A。边 (i, j) 的更新规则如下:
A[(i, j)] = tau[(i, j)]^alpha * eta[(i, j)]^beta
例如,如果你将 beta 设置为 5,将 alpha 设置为 1,那么边的吸引力将更受成本的影响,而不是信息素水平的影响。
更新 tau
随着时间的推移,信息素轨迹会蒸发(由超参数 rho 控制)。因此,我们需要在每次迭代中更新信息素水平。所有的值都乘以 (1-rho)。
tau[(i, j)] = (1-rho)*tau[(i, j)]
寻找蚂蚁路径
这一步是最重要的,它利用所有信息为一只蚂蚁找到一条路径。
蚂蚁从起始节点开始,根据边的吸引力 A 选择当前节点的一个邻居:
import random
...
p = [A[(i, j)] for j in neighbors]
sampled_neighbor = random.choices(neighbors, weights=p)[0]
...
正如你所见,边的吸引力被用作选择下一个节点的权重。如果蚂蚁到达终点节点,则根据解决方案的得分更新解决方案中边的信息素轨迹 (tau):
...
y = score_solution(solution)
for (i, j) in solution:
tau[(i, j)] += Q/y # Q is a constant, e.g. 1
...
蚂蚁可能在到达终点节点之前卡住。在这种情况下,该蚂蚁会被忽略,这意味着此蚂蚁的信息素轨迹将不会被更新。
何时使用 ACO
最初,ACO 是为解决涉及在图上找到最短路径的问题而开发的,但自那时以来,ACO 已被应用于其他类型的问题。
ACO 成功应用于优化问题的示例包括旅行商问题、车辆路径问题、云计算中的任务调度、通信网络、图着色问题以及蛋白质折叠。ACO 的一个优势是它可以处理具有大量变量和约束的大型复杂问题。另一个优势是它可以快速高效地找到良好的解决方案,通常优于其他优化算法。但 ACO 的主要好处在于当问题随着时间变化时,算法能够适应。这是因为算法能够不断探索和适应新解决方案。这样的例子包括繁忙地区的运输系统或网络路由。
除了问题的动态特性外,ACO 在解决组合优化问题方面特别有效,这些问题涉及寻找离散变量的最佳组合。也许你现在已经明白了,ACO 可以处理非线性优化问题,这些问题涉及非线性目标函数并可能具有多个局部最优。在这种情况下,ACO 可以通过更彻底地探索解决方案空间来帮助识别全局最优解。
然而,像任何工具一样,ACO 需要仔细调整。超参数的选择在其性能中起着关键作用。微调这些参数对于获得最佳性能至关重要。

图片来源:Prabir Kashyap 在 Unsplash
结论
在我的下一篇文章中,我们将采取实践的方法,实施 ACO 并见证其实际效果。无论你是经验丰富的算法爱好者还是好奇的初学者,ACO 承诺在自然启发的优化领域中带来一个有趣且有回报的冒险。
敬请期待,见证蚂蚁如何解开复杂的优化挑战!
相关内容
理解预测模型局限性的见解
medium.com [## 介绍一种强大的优化技术:模拟退火
解释、参数、优点、缺点和使用案例
数学优化启发式方法每个数据科学家都应该知道 [## 数学优化启发式方法每个数据科学家都应该知道
局部搜索、遗传算法等。
Julia 中的 Metal 编程
原文:
towardsdatascience.com/metal-programming-in-julia-2db5fe8ee32c

Little Heavy | 作者提供的图片
利用 macOS GPU 的强大功能,通过 Metal.jl 框架。
·发表于Towards Data Science ·阅读时间 11 分钟·2023 年 12 月 4 日
--
介绍
就在去年,我们了解到了 Metal.jl框架,这是一个用于苹果硬件的 GPU 后端。这对希望利用其 macOS M 系列芯片全部潜力的Julia从业者来说是令人兴奋的消息。特别是,数据科学家和机器学习工程师可以通过利用 GPU 的并行处理能力来加速他们的计算工作流程,从而实现更快的训练和推断时间。Metal.jl 的引入标志着对将语言的能力与苹果平台上不断发展的科学计算和机器学习领域对接的重要推动。
在2020 年时,苹果公司开始将其 Mac 系列从基于 Intel 的处理器过渡到 Apple Silicon,从 M1 芯片开始。尽管这是苹果公司历史性且令人印象深刻的成就,但也伴随着不少批评和问题。自从我拿到 32 核的 Mac Studio M1 芯片后,我一直希望充分利用 GPU 并尝试新的应用程序。我必须说,这并非全是轻松愉快的经历。从ARM架构兼容性问题到不支持的机器学习库——有时要获得一个工作环境确实是一种挑战。这是任何重大过渡和操作方式中都会遇到的预期问题。我仍然保持乐观,并看到在稳定性和功能方面的全面重大改进。
在本文中,我们将预览 Metal.jl 框架以了解其能力。我们还将展示一个使用 Flux的实际示例,它是 Julia 中的一个机器学习库,并配合 Metal 后端。
以下是涵盖主题的纲要:
I. 项目设置
i. Julia 环境设置
ii. 依赖概述
II. 利用 Metal API
i. 内核函数
ii. 基准测试
iii. 性能分析
III. 使用 Flux 和 Metal Backend
i. 数据集概述
ii. 简单神经网络
iii. 模型评估
希望跟随的读者应具备:
-
对 Julia 编程语言 的基本知识。
-
对机器学习概念的高级理解。
让我们深入了解吧!
项目设置
i. Julia 环境设置
设置一个特定于项目的环境被视为良好实践。这样可以隔离项目所需的确切版本的包,并为自己和团队成员提供一个容易复现的环境。这在 Julia 中很容易做到,如下所示。
ii. 依赖概述
Metal: 这是一个使在 macOS 上编程 GPU 成为可能的框架。如贡献者所述,该包是正在开发中,存在错误、功能缺失和性能尚未完全优化的情况。请确保您还满足以下系统要求:
✔ 配备 M 系列芯片的 Mac 设备️️️
✔ Julia 1.8–1.10
✔ macOS 13 (Ventura) 或 14 (Sonoma)
BenchmarkTools: 我们将使用这个库来执行一些操作的基准测试,这些操作通过 Metal APIs 发送到我们的 GPU。这个包使配置、执行和分析结果变得容易。
Flux: Flux 是一个直观的 Julia 机器学习库;它旨在提供一个高级且用户友好的界面来构建和训练神经网络。我们将使用这个库作为示例,并利用 Metal Backend 来利用我们的 GPU。
以下是本文发布时的依赖版本。
julia> # press "]" to get into the package manager
(jl_jQfFj6) pkg> status
Status `/private/var/folders/33/mcgc5pgd2ls6yp3ggq6rszvc0000gn/T/jl_jQfFj6/Project.toml`
[6e4b80f9] BenchmarkTools v1.3.2
[587475ba] Flux v0.14.6
[dde4c033] Metal v0.5.1
配置好我们的环境并对所使用的库有了高级理解后,让我们探索 Metal API。
利用 Metal API
Metal.jl 与 Apple 的 Metal 图形 API 进行接口——这是 Apple 为其各种平台(macOS、iPhone、手表等)开发的低级 API。
这使用户可以直接控制 GPU (图形处理单元),用于渲染图形和并行计算等任务。让我们看一个基本的示例。
i. 内核函数
在 Apple Metal 框架的背景下,内核函数是一种在 GPU 上执行的特殊类型的函数。这些函数是用着色语言编写的;在我们的案例中是Metal Shading Language (MSL)。
“MSL 允许用户编写一个shader 程序,这是一种在 GPU 上运行的图形和数据并行计算代码。Shader 程序在 GPU 的不同可编程单元上运行。MSL 是一个统一的语言,允许图形和计算程序之间有更紧密的集成。” ³
在开始之前,让我们确保能够监控 GPU 的负载。Apple 具有内置的 GPU 历史记录。在 Mac 上的活动监视器应用中,选择窗口 > GPU 历史记录。你应该能看到类似我的画面:

GPU 历史记录。顶部的尖峰表示最大 GPU 使用率 | 图片来源:作者
在这个例子中,我们将创建一个矩阵乘法内核。为了利用 GPU,我们将故意通过在矩阵操作上迭代N = 100 万次来创建计算复杂性。
我们将定义A为一个m x n矩阵,B为一个n x p矩阵,C为由m x p定义的结果矩阵。C=AB。 结果的内核函数如下所示;除了对内核函数和底部定义的矩阵变量进行一些小的调整外,代码与你在 Julia 中期望的相当相似。
微妙的差异或附加内容如下:thread_position_in_grid_1d()函数返回当前线程在其一维网格中的索引。基本上,它会知道 GPU 应该在每个线程上操作哪些数据。这种精细化的线程分配控制方法使用户能够最大限度地发挥系统的计算能力。
当我们初始化矩阵A、B和C时,我们希望确保在 GPU 上使用 Metal 初始化值;这确保我们使其适合 GPU 加速计算。此外,storage=Shared参数指示矩阵数据应存储在共享内存中,从而使 CPU 和 GPU 都能访问。这样,我们需要确保在访问该资源之前进行同步:
“确保你在 CPU 或 GPU 上为使用共享内存的资源安排的所有更改在访问该资源的另一个处理器之前完成。” ²
最后要提到的是末尾的C_cpu变量。unsafe_wrap函数用于创建一个与 GPU 数组C共享相同底层内存的 CPU 数组C_cpu。这使我们能够在 CPU 和 GPU 之间传输数据,以便后续进行计算。在下面的示例中,我将展示在所有 GPU 操作完成后,我们可以通过对其进行逆操作来修改结果矩阵 C(inv(C_cpu))。
太棒了!现在我们已经准备好了内核,接下来进行计算和基准测试吧。
ii. 基准测试
benchmark宏(宏以“@”开头)用于测量 GPU 内核matrix_multiplication_kernel的性能。
在执行我们的begin...end块之前,我们将使用Metal.@sync确保 CPU 和 GPU 之间的同步,并且所有 GPU 命令在切换到 CPU 代码之前已经完成。
在metal宏中,我们指定了threads和groups的数量。每个线程负责执行计算。这个数量还决定了多少操作可以并行进行。例如,如果我们指定 256 个线程,则每个线程将负责一部分计算。线程被组织成组;一个线程组是可以一起工作并协调线程并行执行的线程集合。
总的来说,M1 GPU 包含最多 128 个执行单元或 1024 个 ALU,⁴ 苹果表示这些执行单元可以同时执行最多 24,576 个线程,并且其最大浮点(FP32)性能为 2.6 TFLOPs。⁵ ⁶
下面是对N (100)、threads (256)和groups (256)参数进行调整后的结果。

基准测试结果 | 作者提供的图片
iii. 性能分析
要进行性能分析并查看结果,您必须安装 XCode。
性能分析代码是一个常被忽视的学科,它是软件开发和性能优化的关键方面。性能分析涉及测量程序执行的各种方面。这可以包括不同函数所需的时间、函数调用的频率、内存使用或泄漏。性能分析还有助于确保对代码所做的任何更改通过性能量化。
如果你曾经历过代码部署到生产环境后意外导致系统性能显著下降,后来发现是因为初级程序员无意中引入了一个时间复杂度为 O(n²)的嵌套循环,你并不孤单。虽然嵌套的 for 循环在某些情况下看似可以接受,但随着值的数量增加,其影响变得越来越麻烦,导致显著的性能挑战。及早发现,借助性能分析来捕捉!
性能分析可以通过两个步骤简单完成。首先,我们需要在metal宏前面指定Metal.@profile宏。接下来,确保你设置了以下环境变量:ENV[“METAL_CAPTURE_ENABLED”] = 1
现在你可以执行以下代码:
Metal.@profile @metal threads=n_threads groups=n_groups matrix_multiplication_kernel(A, B, C)
从那里,我们获得一个julia_capture_N.gputrace文件,存储在与项目相同的目录中。要与之交互,请在 XCode 中打开它并重放跟踪。我们可以看到各种有用的指标,可以进一步挖掘。

Xcode | 作者提供的图片
到此为止,我们已经介绍了如何通过 Metal.jl 与 Apple 的 Metal 图形 API 交互——这使我们能够直接控制 GPU 以进行并行计算。我们介绍了 Metal 着色语言中的内核函数,并使用了一个故意增加 GPU 工作负载的矩阵乘法内核。此外,我们通过 Metal 的宏介绍了基准测试工具和性能分析能力。
让我们进入一个实际的场景!
使用 Flux 和 Metal 后端
i. 数据集概述
我们将使用从威斯康星大学医院麦迪逊分院的威廉·H·沃尔伯格博士那里获得的乳腺癌数据库。¹⁰ ¹¹ Class特征将作为目标,其中有两个可能的值(2 表示良性,4 表示恶性)。
为了简洁起见,我将不会演示数据集所需的预处理,而是将读者参考到附录 I:Julia 源代码。
ii. 简单的神经网络
我们将使用Flux构建一个简单的神经网络,该网络利用了Metal后端。从 v0.14 开始,Flux 不强制用户使用特定的 GPU 后端和相应的软件包依赖。⁷ 尽管我们使用 Metal 作为后端,Flux 也支持其他后端,如 AMDGPU 和 CUDA。
让我们做一个快速的合理性检查,以确保我们的环境设置正确。

使用 Flux 测试 Metal | 作者提供的图片
现在我们有了device变量,我们将使用它将数据和模型移到 GPU 上。在这里要小心,你可以在 Flux 中执行所有业务逻辑,而不需要将模型导出到 GPU——这意味着你仍在使用 CPU(哎呀)!

模型定义(对于我们的二分类问题,这将是一个逻辑回归模型) | 作者提供的图片
为了准备我们的数据运行,我们将利用 Flux.DataLoader。这个模块处理对小批量数据的迭代。为了演示,我保持简单,设置了batchsize=1。这意味着如果我的数据包含 800 行,每个批次与一行数据相关。在更高效的场景中,你可能希望将数据分开,以便将处理的数据进行分组。
在跳到下一部分之前,重要的是要注意我在将 DataFrame 直接传递给 DataLoader 时遇到了一些问题,所以这里有一些我必须实现的解决方法。

数据准备 | 图片来源:作者
iii. 模型评估
以下代码演示了模型训练和评估过程。当 x_cpu 和 y_cpu 变量(我们的行和标签)从 DataLoader 返回时,利用 GPU 是至关重要的——否则,由于兼容性问题,模型期望在 GPU 上的数据将导致崩溃。

项目运行演示及利用 M1-Max GPU! | 图片来源:作者
在对 Metal.jl 框架的简要介绍中,我们涵盖了在 Apple 硬件上进行 GPU 编程的基础知识。通过探索三个核心概念——内核函数、基准测试和性能分析——读者现在对如何入门有了较高层次的理解,并被鼓励深入探索 API 的功能。此外,我们还演示了一个实际示例,利用 Flux 构建了一个简单的神经网络应用程序,使用了 Metal 后端。如果你是数据科学家或 ML 工程师,想要利用 Apple M 系列 GPU 的强大功能,希望这篇文章能作为加速你的计算工作流的起点。从这里,我将留下更多资源供读者查看:
Flux 是一个机器学习库。它包含许多有用的工具,但也允许…
fluxml.ai](https://fluxml.ai/Flux.jl/stable/?source=post_page-----2db5fe8ee32c--------------------------------)
希望你喜欢这篇文章,谢谢阅读!
👏
最后,我要大力感谢 Metal.jl 项目的创作者。我期待该项目继续成功。对于任何希望贡献的人,请查看他们的 Github 页面 这里。
附录 I:Julia 源代码
在运行 Github 上找到的源代码之前,我强烈建议匹配本文中找到的版本,以确保成功编译。此外,Metal.jl 库仍在开发中,可能会根据指定的版本缺少或包含不同的功能。
🔗 github.com/lausena/JuliaExperiments/tree/main
预处理的补充内容可以在此 帖子中找到。
[## 每当 Gabriel Sena 发布新内容时,你将会收到电子邮件。
每当 Gabriel Sena 发布新内容时,你将会收到电子邮件。通过注册,如果你还没有 Medium 账户,将会创建一个…
medium.com](https://medium.com/subscribe/@lausena?source=post_page-----2db5fe8ee32c--------------------------------)
参考文献
[1] www.youtube.com/watch?v=IARikXzRU7s&ab_channel=TheJuliaProgrammingLanguage
[2] juliagpu.org/post/2022-06-24-metal/index.html
[3] developer.apple.com/metal/Metal-Shading-Language-Specification.pdf
[4] Frumusanu, Andrei。 “2020 Mac Mini 大揭秘:测试 Apple Silicon M1”。 www.anandtech.com。 存档 自原始页面存档于 2021 年 2 月 1 日。检索日期为 2021 年 1 月 30 日。
[5] “Apple M1 芯片”。 Apple.com。Apple。 存档 自原始页面存档于 2020 年 11 月 10 日。检索日期为 2020 年 11 月 11 日。
[6] Kingsley-Hughes, Adrian (2020 年 11 月 10 日)。 “Apple Silicon M1 芯片:我们知道什么”。 ZDnet。Red Ventures。 存档 自原始页面存档于 2021 年 9 月 17 日。检索日期为 2021 年 7 月 1 日。
[7] fluxml.ai/Flux.jl/stable/gpu/
[8] juliagpu.org/post/2023-03-03-metal_0.2/
[9] fluxml.ai/Flux.jl/stable/tutorials/logistic_regression/
[10] William H. Wolberg 和 O.L. Mangasarian: “用于医疗诊断的模式分离,应用于乳腺细胞学”。
“用于医疗诊断的模式分离,应用于乳腺细胞学”,
《美国国家科学院院刊》,第 87 卷,
1990 年 12 月,第 9193–9196 页。
[11] 该数据集遵循 知识共享署名 4.0 国际 (CC BY 4.0) 许可协议。 archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/breast-cancer-wisconsin.names
META 的 Hiera:降低复杂性以提高准确性
原文:
towardsdatascience.com/metas-hiera-reduce-complexity-to-increase-accuracy-30f7a147ad0b
| 人工智能 | 计算机视觉 | VITs |
| --- | --- | --- |
简单性使得人工智能能够达到惊人的性能和速度
·发表于 Towards Data Science ·阅读时间 12 分钟·2023 年 6 月 21 日
--

图片由 Alexander Redl 提供,来源于 Unsplash
卷积网络在计算机视觉领域已经主导了二十多年。随着变换器的到来,人们曾认为它们会被淘汰。然而,许多从业者仍在项目中使用基于卷积的模型。这是为什么呢?
本文试图回答这些问题:
-
视觉变换器是什么?
-
它们的局限性是什么?
-
我们能否尝试克服这些问题?
-
META Hiera 如何以及为何似乎成功了?
视觉变换器:一张图值多少字?

图片来源:这里
视觉变换器近年来在视觉基准测试中占据了主导地位,但它们究竟是什么呢?
直到几年前,卷积神经网络 一直是视觉任务的标准。然而,在 2017 年,变压器 的发布让 NLP 世界颠覆了。在文章 Attention is all you need 中,作者展示了一个仅使用自注意力构建的模型能够比 RNNs 和 LSTMs 实现更优越的性能。所以人们很快会想:是否可以将变压器应用于图像?
在 2020 年之前,已经尝试过包括自注意力集成的混合模型。但无论如何,这些模型的扩展性都不佳。这个想法是找到一种方法,使变压器可以原生地用于图像。
在 2020 年,谷歌的作者决定最佳方法 是将图像分割成不同的补丁,然后对序列进行 嵌入。这样,图像基本上被视为模型中的标记(单词)。

图片来源:这里
在短时间内,CNNs 在计算机视觉中的主导地位正逐渐被削弱。视觉变压器 在 CNNs 一直主导的基准(如 ImageNet)上表现更为优越。

图片来源:这里
实际上,提供足够的数据,视觉变压器(ViTs) 表现出它们优于 CNNs。还显示出,尽管存在一些差异,但也有许多相似之处:
-
ViTs 和 CNNs 都构建了复杂且逐步深入的表示。
-
然而,ViTs 更能利用背景信息,并且似乎更具鲁棒性。
一些最大的模型如何看待世界
pub.towardsai.net](https://pub.towardsai.net/a-visual-journey-in-what-vision-transformers-see-9db9c8ba62d4?source=post_page-----30f7a147ad0b--------------------------------)
此外,变压器的可扩展性也是一个额外的优势。这成为了 ViTs 的竞争优势,使其成为热门选择。

使用视觉变压器。 source
实际上,多年来我们见证了数百万参数的 CNN 和达到数十亿参数的 ViTs。 去年,Google 展示了如何将 ViTs 扩展到 20B 参数,并且未来我们可能会看到更大的模型。
Google ViT-22 为新型大型变压器和计算机视觉的革命铺平了道路
towardsdatascience.com
视觉变压器的限制

图片由Joshua Hoehne提供,来自Unsplash
原生适配变压器仍然有成本:ViTs在使用其参数时效率低下。这源于它们在网络中使用相同的空间分辨率和相同数量的通道。
CNN 有两个方面决定了它们的初期命运(都受到人脑皮层的启发):
-
随着层级的上升,空间分辨率减少。
-
增加不同的“通道”数量,每个通道变得越来越专业。

图像来源:这里
另一方面,变压器具有不同的结构,即一系列自注意力模块,其中发生两个主要操作,使其能够很好地进行泛化:
-
用于建模元素间关系的注意力操作。
-
全连接层则建模元素间的关系。
这实际上早已被注意到,源于变压器设计是针对文字而非图像的。毕竟,文本和图像是两种不同的模式。其一的不同之处在于,文字在尺度上不会变化,而图像则会。这在需要关注在目标检测中尺度变化的元素时是有冲突的。
此外,图像中像素的分辨率高于文本段落中文字的分辨率。 由于注意力机制具有平方级别的成本,使用高分辨率图像在变压器中具有较高的计算成本。
之前的研究尝试通过使用层次特征图来解决这个问题。例如,Swin Transformer通过从小块开始并逐渐合并各种邻近块来构建层次表示。

图像来源:这里
其他研究尝试在 ViTs 中实现多通道。例如,MVITs尝试创建初级通道,专注于简单的低级视觉信息,同时更深的通道则关注复杂的高级特征,如 CNNs 中的做法。

图像来源:这里
然而,这些方法并未完全解决问题。随着时间的推移,提出了越来越复杂的模型和专业模块,这些改进在一定程度上提升了性能,但使得ViTs在训练时变得相当缓慢。
我们能否在不需要复杂解决方案的情况下解决这些变换器的局限性?
如何学习空间关系

图片由Ali Kazal在 Unsplash 上提供
ViTs 已成为计算机视觉领域的一个模型,然而,适应它们需要越来越复杂的修改。
我们能否在不需要复杂解决方案的情况下解决这些变换器的局限性?
近年来,人们一直在努力简化模型并加快速度。常用的一种方法是引入稀疏性。在计算机视觉领域,一个非常成功的模型是掩码自编码器(MAE)。
在这种情况下,经过分块后,一些块会被掩盖。然后,解码器必须从被掩盖的块中重建。ViT 编码器仅处理 25%的块。在这种情况下,你可以用少量的计算和内存来训练宽编码器。

图像来源:这里
这种方法已被证明能够教授空间推理,取得了与 Swin 和 Mvit 相当甚至更优的结果(但后者在计算上要复杂得多)。
另一方面,如果稀疏性确实提高了训练效率,那么 CNNs 的一个主要优点就是其层次化的方法。但这与稀疏性存在冲突。
实际上,这之前已经做过测试,但效果不佳:
是否可以设计一个稀疏且层次化但仍高效的模型?
META 的新工作已脱离了 MAE 训练和其他技巧,致力于构建一个高效且准确的 ViT,而不需要过去使用的那些复杂结构。
现代层次化视觉变换器在追求有监督学习的过程中添加了多个视觉专用组件…
arxiv.org](https://arxiv.org/abs/2306.00989?source=post_page-----30f7a147ad0b--------------------------------)
Hiera: 层次化、稀疏且高效的 ViT

照片由 Jordan Opel 提供,Unsplash
模型
基本思想是,为了在视觉任务中训练一个高准确度的层次化 ViT,不需要使用一系列使其变得缓慢和复杂的元素。根据作者的说法,空间偏差可以通过掩码自编码器训练从模型中学习。
在 MAE 中,补丁被删除,因此在层次化模型中,它在重建 2D 网格(以及空间关系)时遇到问题。作者解决了这个问题,使得内核在掩码单元之间不能重叠(在 池化 中,掩码单元之间没有重叠)。

图片来源: 这里
作者们从现有的 ViT 层次化模型 MViTv2 开始,并决定使用 MAE 训练对其进行改造。该模型由多个 ViT 块组成,但如结构所示,某些地方会减少尺寸,这通过使用池化注意力实现。

图片来源: 这里
在池化过程中,注意力特征使用 3x3 卷积进行局部聚合,然后计算自注意力(这是为了减少 K 和 V 的大小,从而减少计算量)。当使用视频时,这种机制可能变得昂贵。因此,作者用掩码单元注意力替代了它。
换句话说,在 Hiera 中,池化过程中内核会移动,以确保掩码部分不会出现在池化中。因此,对每组标记(掩码大小)进行某种局部注意力

图片来源: 这里
MViTv2 随后引入了一系列增加复杂性的附加组件,作者认为这些是非必需的,因此将其删除:
-
相对位置嵌入。位置嵌入被添加到每个块中的注意力中。
-
最大池化层,在 Hiera 中使用时需要填充。
-
注意力残差,在 Q(查询)和输出之间存在残差连接,以更好地学习池化注意力。作者减少了层数,因此不再需要。
作者展示了这些变化的影响如何显著提高了准确率(acc.)和速度(每秒图像)。

图片来源:这里
一般来说,简化模型使 Hiera 不仅在图像和视频上更快,而且比其对应的 MViTv2 和其他模型更准确。
Hiera 在图像上比我们开始使用的 MViTv2 快 2.4 倍,在视频上快 5.1 倍,并且由于 MAE 实际上更准确(来源)

图片来源:这里
作者指出,模型不仅在推断时更快,而且训练也快得多。

图片来源:这里
结果
作者展示了基本模型在参数数量有限的情况下,如何在 Imagenet 1K 上取得良好结果(这是最重要的图像分类数据集之一)。

图片来源:这里
第二点是,在低参数范围内,通常是卷积基础的模型占据主导地位。这里更小的模型表现非常好。对于作者而言,这证实了他们的直觉,即空间偏差可以在训练过程中学习,从而使 ViTs 即使在小模型中也能与卷积网络竞争。

图片来源:这里
大型 CNN 模型的成功在于它们可以用于迁移学习。ResNet 和基于 VGG 的模型已经在 Imagenet 上训练,然后被社区用于许多任务。因此,作者测试了 Hiera 的迁移学习能力,使用了两个数据集:iNaturalists 和 Places。

图片来源:这里
作者在这两个数据集上对模型进行了微调,并展示了他们的模型优于之前的 ViTs。这表明他们的模型也可以用于其他数据集。
此外,作者使用了另一个流行的数据集 COCO。虽然 iNaturalists 和 Places 是图像分类的数据集,COCO 是用于图像分割和目标检测(计算机科学中的两个热门任务)的最广泛使用的数据集之一。再次,模型表现出强劲的扩展性(参数增加时性能提升)。此外,模型在训练和推断时都更快。

图片来源:这里
此外,该模型已在视频上进行了测试,特别是在两个视频分类数据集上。Hiera 表明它在参数更少的情况下表现更好。该模型在推理中也更快。作者展示了该模型在这种任务中达到了最先进的水平。

图片来源:这里
作者展示了该模型还可以用于其他视频任务,例如动作检测。
告别感言
在这项工作中,我们通过采用现有的层次化视觉变换器,去除所有附加功能,同时通过 MAE 预训练为模型提供空间偏置,从而创建了一个简单的层次化视觉变换器。 (source)
作者展示了许多为提高变换器性能而添加的元素实际上不仅是不必要的,而且增加了模型的复杂性,使其变得更慢。
相反,作者展示了使用 MAE 和层次结构可以使 ViT 在图像和视频处理上更快、更准确。
这项工作很重要,因为在许多任务中,社区仍然使用基于卷积的模型。ViTs 是非常大的模型,计算成本高。因此,人们通常更愿意使用基于 ResNet 和 VGG 的模型。ViTs 如果在推理上更准确但尤其更快,可能会改变游戏规则。
其次,它突出了在其他地方看到的一个趋势:利用稀疏性进行训练。这具有减少参数和加速训练与推理的优势。一般来说,稀疏性的概念也在其他人工智能领域中被看到,并且是一个活跃的研究领域。
如果你觉得这很有趣:
你可以查看我的其他文章,你也可以 订阅 以在我发布文章时收到通知,你还可以 成为 Medium 会员 以访问所有故事(平台的附属链接,我从中获得少量收入,对你没有成本),你也可以在LinkedIn上连接或联系我。
这是我 GitHub 仓库的链接,我计划在其中收集与机器学习、人工智能及更多相关的代码和资源。
[## GitHub — SalvatoreRa/tutorial: 机器学习、人工智能、数据科学的教程…
机器学习、人工智能、数据科学的教程,包含数学解释和可重复使用的代码(Python…
github.com](https://github.com/SalvatoreRa/tutorial?source=post_page-----30f7a147ad0b--------------------------------)
或者你可能对我最近的一篇文章感兴趣:
模仿模型能否达到像 ChatGPT 这样的专有模型的性能?
levelup.gitconnected.com [## 扩展并不是一切:更大模型失败得更惨
大型语言模型真的理解编程语言吗?
salvatore-raieli.medium.com [## META 的 LIMA:玛丽亚·近藤的 LLM 训练方法
更少而整洁的数据来创建一个能够与 ChatGPT 相媲美的模型
levelup.gitconnected.com [## 人工智能有趣吗?也许,稍微有点
为什么人工智能仍然在幽默方面挣扎,以及这为什么是一个重要步骤
参考文献
这是我撰写本文时参考的主要文献列表,仅列出文章的第一个作者。
-
Chaitanya Ryali 等,2023 年,《Hiera: A Hierarchical Vision Transformer without the Bells-and-Whistles》,链接
-
Peng Gao 等,2022 年,《MCMAE: Masked Convolution Meets Masked Autoencoders》,链接
-
Xiang Li 等,2022 年,《Uniform Masking: Enabling MAE Pre-training for Pyramid-based Vision Transformers with Locality》,链接
-
Zhenda Xie 等,2022 年,《SimMIM: A Simple Framework for Masked Image Modeling》,链接
-
Ze Liu 等,2021 年,《Swin Transformer: Hierarchical Vision Transformer using Shifted Windows》,链接
-
Haoqi Fan 等,2021 年,《Multiscale Vision Transformers》,链接
-
Kaiming He 等,2021 年,《Masked Autoencoders Are Scalable Vision Learners》,链接
-
Chen Wei 等人, 2021, 《自监督视觉预训练的掩码特征预测》,链接
-
Alexey Dosovitskiy 等人, 2020, 《一张图像价值 16x16 个词:用于大规模图像识别的变换器》,链接
-
Ashish Vaswani 等人, 2017, 《注意力机制就是你所需的》,链接
-
Kaiming He 等人, 2015, 《用于图像识别的深度残差学习》,链接
-
Wei Yu 等人, 2014, 《可视化和比较卷积神经网络》,链接
-
Karen Simonyan 等人, 2014, 《用于大规模图像识别的非常深的卷积网络》,链接
-
为什么我们有大型语言模型而小型视觉变换器?,TDS,链接
-
在 Vision-Transformers 看到的视觉之旅,TowardsAI,链接
-
Vision Transformer,paperswithcode,链接
客户终身价值建模的方法:好东西和陷阱
一份关于 CLV 技术和实际应用案例的全面实用指南的第三部分
·发表于 Towards Data Science ·10 分钟阅读·2023 年 11 月 17 日
--

客户多久购物一次?他们花多少钱?他们忠诚多久?三个简单的因素帮助你建模你平均消费者的客户终身价值。但这是否意味着任务很容易?不,不是的。来源:作者提供。
欢迎回来继续我的客户终身价值预测系列,我称之为“其他教程遗漏的所有内容”。在 第一部分,我介绍了历史 CLV 分析这个经常被低估的阶段,以及你可以用这些后视信息做些什么。 下一部分,我展示了大量的 CLV 预测 用例,远远超出了我在其他文章中看到的通常有限的例子。现在,是时候进入实际部分,包括我和我的数据科学团队在处理实际数据和客户时学到的一切。
再次说明,我无法将所有有用的信息压缩到一篇博客文章中,而不将其变成一部史诗。所以今天我将专注于历史 CLV 建模,正如第一部分所示,这已经非常有用。我将介绍 Stupid Simple Formula、Cohort Analysis 和 RFM 方法,包括我发现的每种方法的优缺点。下一次我将做相同的工作,但针对 CLV 预测 方法。我将以数据科学家学习到的最佳实践来结束整个系列,讲述如何正确地进行 CLV 分析。
听起来不错?那我们就来探讨一下历史上的 CLV 分析方法,以及你需要注意的优点和“陷阱”。
方法 1:简单的公式
或许最简单的公式基于三个要素:顾客通常购买多少,购物频率,以及忠诚的持续时间:

比如,如果你的平均顾客每次交易花费 25 欧元,每月进行两次交易,并且保持忠诚 24 个月,那么你的 CLV = 1200 欧元。
我们可以通过考虑利润或盈余,使其变得更复杂。有几种方法可以做到这一点:
简单公式 V1:每产品利润
在这里,你需要计算所有库存产品的平均利润,然后将这个非常简单的公式结果乘以这个数字,以得到平均客户生命周期利润:

例如,如果你使用上述数据,并考虑到平均产品利润为 10%,那么你的平均 CLV =(25 欧元 * 2 * 24)* 0.1 = 120 欧元。
计算平均产品利润的方法取决于你所拥有的成本数据,这些数据可能来自各种数据源。一个简单的开始方法是直接用标准目录价格减去销售成本(COGS),因为你可能在库存表中已经有了这些信息。当然,这并不考虑更复杂的成本,或者当物品打折时的销售价格,或者不同交易包含不同项目,可能有非常不同的利润。我们来看看一个考虑了这些因素的选项…
简单公式 V2:每交易利润
第二版用平均交易利润替代了原公式中的平均交易值:

例如,每次交易 5 欧元的利润 * 每月 2 次交易 * 24 个月的生命周期 = 平均 CLV 为 240 欧元。
这种变体需要基于价格减去成本的每项交易级别的利润。其优点在于,你可以使用实际销售价格,而不是目录价格,从而考虑到最终结账时的任何促销或折扣。此外,你可以包括配送服务成本(CODS),即运输费用,以及支付服务成本(COPS),即支付系统提供商(如 Visa 或 PayPal)的费用。这一切都能带来更准确和可操作的洞察。

来源:作者提供。
简单公式的优缺点
积极的一面:
-
这个公式在概念上很简单,这有助于数据科学家和领域专家在如何计算它以及如何使用它上更好地合作。
-
此外,它可以根据你的利润计算方式而变得简单或复杂,
然而,存在两个主要缺点。首先,这个公式并不是特别具有操作性:
-
它生成一个单一的平均值,这个值难以解释和影响:即使你做出一些改变,重新计算后发现平均值也有所变化,你也无法确定这是否与您的行动相关。
-
它平滑了销售速度,所以你无法追踪客户是在生命周期的开始还是后期进行消费。
-
并且它无法帮助理解客户细分及其需求。
其次,该公式可能不可靠:
-
作为一个平均值,它很容易被扭曲,例如如果你有大额花费者或零售和消费者客户的混合。
-
在非合同情况中,客户不受合同约束继续付款,你永远无法真正知道客户何时“死亡”。因此,很难估算平均生命周期组件的价值。
-
该公式假设每个客户的支出和流失率是恒定的。它未考虑客户旅程和阶段,即他们在何时需要更多或更少的产品。
方法 2:群体分析
这种技术涉及将平均 CLV 公式应用于个别客户细分。你可以按任何方式对客户进行细分,例如按人口统计、获取渠道或通常按首次购买月份。其目的是回答如下问题:
-
在 3、6、12 个月后,平均客户的价值是多少?
-
他们在生命周期中的何时花费最多?例如,他们最初是否花费很多,然后逐渐减少,还是相反?
-
获取漏斗如何影响 CLV?例如,由促销活动引起的注册可能会赢得很多短期的、不忠诚的客户,而来自推荐朋友计划的客户可能会成为终身粉丝。同样,店内获取可能比在结账时强制在线注册带来更多忠诚度。
-
人口统计群体在其平均 CLV 上有何不同?例如,富裕郊区的购物者是否花费更多?答案并不总是符合预期,每当发生这种情况时,如果你深入挖掘,通常会发现有价值的见解。
下面我们看到按获取月份进行的经典群体分析。横轴显示群体组,指示我们数据中最早的交易月份。这可能表示获取月份(尽管一些客户可能在数据收集开始之前就已存在)。纵轴显示群体周期:自数据中最早交易以来的月份数。

来源:Finn Qiao,《Cakestands & Paper Birdies: E-Commerce Cohort Analysis in Python》,发表于 TowardsDataScience.com
你是怎么解读这个的?最左边的列显示了 2010 年 12 月加入的客户(或当时已经是客户)。颜色越深表示这些客户在他们的第一个月(左上角单元格)和第 10 到第 12 个月(右下角),即 2011 年 9 月至 11 月,花费越多。这可能意味着什么?数据科学家和营销人员之间的合作可以帮助解码这一趋势:也许是因为公司在 2010 年为这些客户挽救了圣诞节,他们在 2011 年开心地回归了。也可能只是因为他们在数据收集开始前已经是客户了。与此同时,7 月和 8 月获得的客户往往花费较少。为什么?针对在其他时间获得的客户,应该采取什么策略来提升平均客户生命周期价值(CLV)?
同样的调查也可以并且应该在其他类型的细分中进行。

来源:作者提供。
方法 3:“RFM”方法
RFM 方法基于为每个客户计算以下指标:

是的,我买了新的 iPad。不,我不是艺术家。来源:作者提供。
这使我们能够根据这些指标对客户进行分类,探索不同的客户群体,并为其分配有意义的业务名称。例如,那些具有最高的近期性、频率和货币价值分数的客户被称为“Top Prio”或“VIPs”。确定了他们之后,接下来你需要了解的是:这个群体在你的整体客户基础中所占的比例是多少?与此同时,具有高频率和货币价值但近期性低的客户花费显著,但时间较短。如果你添加一个额外的指标——自上次购买以来的时间——发现这个时间很长,他们可能被认为是“流失风险”。
发现这些群体的最简单方法是使用百分位数:按近期性对客户进行排序,并将其分为——例如前 20%、中间 50%和底部 30%的层次。对其他指标重复此操作。然后定义所有可能的层次组合,标记生成的群体,并绘制每个群体在整体客户基础中所占的百分比。下方展示了这一点。创建这样的图表可以清晰地显示,只有很小一部分整体客户基础是“VIPs”,而更大一部分是“变冷”甚至是“流失风险”。这些洞察可以帮助你制定策略,以获得更多忠实客户,减少风险客户。

发现并标记你的客户基础中的细分。来源:作者提供。
这个图表中有相当多的类别,由三种指标和每种指标的三个层级的组合产生。为了更详细,你可以为每个指标定义更多层级。你还可以尝试不同的分段方法:我提到过 20–50–30 的分割,但你可以使用其他数字,甚至对每个指标使用不同的策略。例如,由于频率是客户忠诚度的一个重要指标,如果你认为这能帮助你最准确地找出最佳客户,你可能会考虑将其排名并分割为 5-10-85 百分位。
如果你不确定如何分割客户,或者希望使用更数据驱动的方法?你可以尝试使用无监督机器学习(ML)算法,例如k-means,以发现客户群体。这增加了使用 ML 和确定真正代表数据分布的簇数量的复杂性(有些人推荐使用肘部法,但我有个坏消息要告诉他们)。不过,如果你具备数据科学能力,数据驱动的方法可能会产生更准确的结果。
RFM 方法的优缺点
在积极的一面:
-
RFM 方法直观,这使得数据科学家和领域专家之间的沟通与协作更为轻松。
-
手动标记的组高度感知并且量身定制。你可以与营销部门合作定义它们,因为无疑是营销部门会基于结果采取行动。
缺点:
-
定义多少个 R、F 和 M 层级可能很困难: 高-中-低的粒度是否足够?这取决于企业的需求及其调整营销策略、客户服务或产品线以适应不同群体的运营能力。
-
如何结合 R、F 和 M 分数也是一个棘手的问题。 想象一下,你已按照最近性对客户进行排名,并将他们分为三层,其中顶层客户被分配为 3,中层为 2,其余为 1。你对频率和货币值也做了相同的操作。现在你有几个选择:

如何结合不同客户细分的分数?继续阅读以了解更多。来源:作者提供。
-
使用简单拼接时,得分为 R=3,F=3 和 M=3 的客户最终得分为 333,而一个全方位的底层客户得分为 111。使用简单拼接和每个指标三个层级的组合可以产生多达 27 种可能的分数,这很多(要验证这一点,请计算“Concat.”列中的唯一值)。你添加的层级越多,组合就越多。你可能会得到比你能处理的更多的组,和/或创建的组太小,你不知道如何处理,或者无法依赖基于这些组的任何分析。
-
总结将为你提供更少的组别:现在你所有的底层客户得分为 1+1+1 = 3,你的顶级客户得分为 3+3+3 = 9,其他每个得分都会落在 3-9 范围内。这可能更易于管理,但也带来了一个新问题。现在 R、F 和 M 指标被同等对待,这可能不合适。例如,糟糕的最近性得分是一个你不想忽视的大警告,但通过求和,你再也无法看到它的个别贡献。
-
添加权重可以解决这些问题:例如,如果你发现频率是一个常规高 CLV 购物者的最佳指标,你可能会将 F 乘以某个正数,以提高其重要性。但这也带来了一个新挑战,即使用哪些权重因素?找出一些结果公平且有用的数据表示的值并非易事。
总结完毕。也就是说,现在我们可以进入机器学习了吗?
呼。正如你所见,建模历史 CLV 并非易事。然而,我真的相信这是值得的,并希望更多的数据科学项目能专注于真正了解数据,再跳入机器学习和做出预测。
不过,我知道这正是你们一些人来这里的目的。所以下次——我保证——我会介绍 CLV 预测方法的优缺点。在此之前,开始探索那些历史数据吧!对它们建立一些直觉;这将帮助你在下一步中。
指标层:所有 KPI 定义的唯一真实来源
原文:
towardsdatascience.com/metrics-layer-a-single-source-of-truth-for-all-kpi-definitions-671c9aa04a91

图片由 Midjourney 生成
了解为什么实现指标层将使在您的组织中获取数据驱动的见解变得更加稳健!
·发布于 Towards Data Science ·阅读时间 8 分钟·2023 年 8 月 8 日
--
指标层是一个框架,它通过以统一和直观的方式整合、分析和可视化关键绩效指标,赋能组织挖掘有价值的见解并推动数据驱动的决策。
在本文中,我们将探讨指标层的重要性、它的好处、与语义层的关键区别以及成功实施的要求。
什么是指标层?
指标 层(也称为指标 存储或无头 BI)是一个用于标准化指标的框架,即集中化公司计算指标的方式。它可以被视为定义组织内 KPI(或指标,这些术语将互换使用)的唯一真实来源。
💡 额外小知识:如果您想知道,“无头 BI”这一术语源于这些解决方案使各种 BI 工具能够连接到 API 以访问指标。因此,它们提供了在保持指标定义完整性的同时更换工具的灵活性。
实质上,指标层的概念并不完全陌生。例如,您已经将项目的代码库存储在一个集中式仓库中,并使用 Git 进行版本控制。类似地,组织的数据仓库或数据湖作为所有数据的唯一真实来源。类比地,指标层作为所有在组织内使用的 KPI 定义的唯一真实来源。
如下图所示,指标层应位于数据仓库(或更广义的数据源)和所有相关应用程序(如仪表板、报告、AI 模型等)之间,这些应用程序使用这些指标。

让我们进一步扩展这个定义。指标层不仅存储所有指标定义,还将应用程序生成的请求转换为 SQL。然后,该层在数据仓库/湖中执行这些请求,以检索所需的指标。
你为什么需要指标层?
你可能在你的组织中听到过以下一些变体:
-
为什么在仪表板 X、Y 和 Z 上这个指标的值不同?
-
看来这个仪表板使用了不同的指标 X 定义。我们能否快速使所有仪表板保持一致,以传达相同的故事?
-
有管理层的人员询问了这个指标的定义。你能调查一下这个仪表板中的自定义查询,并确定我们实际上是如何计算它的吗?
不幸的是,这些例子说明了数据科学家或数据分析师在日常工作中经常遇到的问题。
这些问题表明指标变得难以管理,为用户带来了混乱,无论他们是数据专业人士还是非技术利益相关者。
更糟糕的是,这些用户通常必须基于这些指标做出关键的业务决策。
简单指标的隐藏复杂性
随着企业的增长和发展,他们监控的指标也在变化。随着收集的数据量增加,其复杂性也随之增长。
起初看起来不直观的是,即使是看似简单的任务,如统计,也在分析中变得具有挑战性,因为在聚合原始数据时会出现许多复杂性。
为了说明这一点,让我们考虑一个许多组织都能理解的例子:计算应用程序或服务的用户数量。这应该很简单,对吗?
然而,尝试统计用户时可能会出现以下问题:
-
确定统计用户的时间框架:应该按天、周、月、年或其他基础进行统计吗?
-
按地理区域对用户进行细分:如果需要细分,应使用什么级别的详细信息?大陆、国家、省、城市等?
-
定义 活跃 用户:我们如何识别一个活跃用户?如果在特定时间段后没有交易,用户是否应被视为非活跃?如果是,这个特定时间段是什么?此外,如何处理那些登录并使用服务但没有购买的用户?“活跃用户”的定义可能会有很大的不同。
-
应用数据过滤器或排除特定用户:是否应根据特定标志排除某些用户?例如,是否应排除公司员工使用的测试账户?
即使是看似简单的任务,如用户计数,也涉及许多复杂性。
确保这些指标的准确性至关重要,因为在多个出口(如仪表板或报告)中不一致的 KPI 可能导致利益相关者对数据失去信任。此外,数据团队可能会极其困难地识别所有使用不同且常常相互冲突的指标定义的位置。
在这种情况下,最大的问题是没有一个中央存储库来存储指标定义。这些定义分散在各种 BI 工具和填充视图或仪表板的自定义 SQL 查询中。因此,它们常常被重新创建和重复使用,而没有适当的监督和一致性。
这就是指标层发挥作用的地方。接下来,让我们看看为您的 KPI 设置单一事实来源的好处。
指标层的优势
实施指标层可以确保组织内的多个人员在向不同的数据和非数据专业人员询问有关某一指标的问题时,得到一致的答案。
让我们探讨一下实施指标层的一些额外优势。
促进一致性并建立信任
通过启用清晰且可重用的业务定义,指标层在组织内促进了一致性。这种一致性增强了利益相关者对数据的信任。
此外,它允许检查指标的来源——了解指标是如何构建的以及使用了哪些数据源。
遵循 DRY(不要重复自己)原则
使用指标层可以避免在多个位置定义每个指标的业务逻辑。这避免了不必要的重复,并确保了管理指标定义的效率。
促进遵守软件工程最佳实践
由于指标层是通过代码定义的,因此更容易遵循已建立的最佳实践。此外,还可以使用行业标准解决方案来进行指标层的版本控制,从而确保适当的跟踪。
为数据消费渠道提供未来保障
有了指标层,可以减少在各种实例中使用过时指标定义的风险。这使得开发人员能够构建分析功能和数据驱动的应用程序,同时保持一致和最新的指标定义。
支持各种工具
指标层的集中架构使其能够与各种工具(如 CRM 系统、BI 工具和内部开发的解决方案)无缝集成。
无论使用何种工具或其内部逻辑,最终结果都是基于标准化的指标逻辑。
提供单一的指标定义接口
度量层的集中式架构提供了一个统一的界面,让组织中的所有数据利益相关者都能检查特定度量的定义方式。这促进了透明度,并确保对度量定义的共享理解。
为成功的度量层实现奠定基础
在探讨了度量层的定义和意义之后,现在是时候深入了解其实现方式了。
让我们研究成功实现度量层的要求。市面上有几种现成的解决方案,每种方案都有其优缺点。
然而,让我们退一步,将重点转移到任何度量层实现应具备的关键特性,以便在现代数据栈中有效地履行其角色。
成功的度量层实现需要五个核心属性:
-
强大的语义层
-
集成能力
-
低延迟的性能优化
-
部署灵活性
-
企业能力
让我们深入探讨每个属性的细节,从一个常常引起混淆的方面开始:语义层,也称为语义模型或逻辑模型。

度量层与语义层
语义层作为数据仓库中的表和列与有意义的业务实体之间的映射。语义层是企业可以使用业务友好的语言定义维度、度量和指标的地方。
💡 需要注意的是,语义层只是度量层的一个组成部分,不应与度量层本身混淆。
理想情况下,这些定义应通过直观的用户界面(UI)轻松制定,并存储在版本控制的文本文件中,通常为 YAML 或 JSON 格式。
此外,为了促进自动化,实现应提供声明式 API。
除了强大的语义层之外,还有几个其他属性对于全面的度量层实现至关重要,如上所述。让我们进一步探讨这些属性。
集成能力
为确保度量定义的一致性,头部无 BI 解决方案应具有与流行 BI 工具、编程语言、ML 框架和其他相关技术集成的灵活性。
这需要广泛支持基于标准的数据协议、API 和 SDK。
低延迟的性能优化
度量层应设计为高性能查询,使得在规模上实时访问度量成为可能。
这对于支持自动化功能如电子邮件触发和个性化产品体验至关重要。
部署灵活性
度量层应支持广泛的部署选项,包括完全托管服务和跨不同提供商的云原生部署。
这种灵活性使组织能够选择最适合其特定需求和基础设施的部署模型。
企业能力
治理、安全、访问控制、性能、可扩展性和高可用性等因素对于许多组织至关重要。
随着指标层成为各种应用、工具和流程的关键组件,它应具备企业级功能以满足组织的要求。
通过考虑这些要求,组织可以确保在其现代数据堆栈中成功实施指标层。
总结
许多公司仍处于数据科学和机器学习的早期阶段。因此,提高商业智能和报告可以解决他们面临的约 90%的数据相关挑战。
因此,对于这些公司来说,建立一致且集中化的指标定义至关重要。
指标层作为组织内所有 KPI 定义的权威来源。它充当数据源与依赖这些指标的各种应用(仪表板、报告、AI 模型等)之间的桥梁。
实施指标层带来了众多好处。它确保了数据的一致性和可信度,提高了操作效率,促进了最佳实践的遵循,使数据分析具备前瞻性,与不同工具集成,并为利益相关者提供统一的指标定义访问。
通过利用指标层,公司可以提高数据驱动决策过程的精确度、可靠性和整体有效性。
喜欢这篇文章?成为 Medium 会员,继续无限制阅读学习。如果你使用 这个链接 成为会员,你将以零额外成本支持我。提前感谢,期待见面!
你可能还会对以下内容感兴趣:
学习如何通过使线性模型更具灵活性来改善模型的拟合。
## 解锁线性回归中的交互项的力量 ## 解锁线性回归中的交互项的力量
学习如何通过包含交互项来使你的线性模型更具灵活性。
towardsdatascience.com](/unlocking-the-power-of-interaction-terms-in-linear-regression-ba30c2cf158f?source=post_page-----671c9aa04a91--------------------------------) ## 使用 DVC 进行实验跟踪的极简指南
开始进行实验跟踪的最基本指南
towardsdatascience.com ## 回归评估指标的全面概述
对常用回归评估指标及其在各种实际应用中的详细参考
towardsdatascience.com
参考资料
除非另有说明,否则所有图片均由作者提供。
最初发表于 Atlan 博客 于 2023 年 8 月 2 日
指标存储的实际应用
原文:
towardsdatascience.com/metrics-store-in-action-76b16a928b97
使用 MetricFlow、Python、DuckDB、dbt 和 Streamlit 的教程
·发表于Towards Data Science ·阅读时间 11 分钟·2023 年 2 月 23 日
--

指标层集中定义所有关键业务指标和维度。它将指标请求转换为 SQL,抽象化了实现细节。图片由作者创建。
关于现代数据栈(MDS)的文献很多——大多数讨论围绕存储、摄取、转换和展示。今天我们聚焦于指标,这是许多其他MDS 类别中的一种。
有人说指标是语义层的一个组成部分——请参阅这篇 Airbyte 博客文章。而我们,如同其他人一样,将“指标存储”、“指标层”和“语义层”这些术语交替使用。
什么是指标存储,也称为指标层,也称为语义层?
“上个月的收入,分析师的笔记本中报告的与我们仪表盘中的不同。我们应该向审计报告哪个数据?”
“你知道我们在保留应用中如何定义流失率 KPI 吗?”
“我们如何将客户订单的收入按地理区域进行细分?”按客户账单地点?按交货地点?还是其他?
这些问题经常出现在我曾工作的公司中。我们都知道原因:不同的团队在孤岛中工作,使用他们偏好的技术栈实现指标。细节隐藏在锁定访问的数据库中的 SQL 程序中,或者在一些本地 Power BI 文件中,或在我们不知道的其他地方。指标存储就是为了解决这个问题:
-
真实的单一来源——一个用于指标和维度的中央代码库和知识数据库。
-
数据仓库的代理——所有的展示工具(BI、报告、笔记本)通过指标存储的一个 API 请求 KPI。
指标存储还帮助你进行治理和缓存。但这超出了这里的讨论范围。
转换层与指标层——如何划分界限?
转换层是 ELT 中的 “T”,在逻辑上,它排在接收原始数据(“EL”)到我们的存储之后第二位。与度量层一样,“T”步骤在下游应用程序使用数据之前进行转换。你可能会问,这两层有什么不同?“T”的责任在哪里结束,度量存储的责任从哪里开始?Simon Späti 一针见血。
语义层在查询时转换/连接数据,而转换层在 ETL/ELT 的转换(T)步骤中进行,即在数据处理前已计算好。
转换层应无疑涵盖的步骤:
-
通过遵循一致的风格指南(如这个 dbt 风格指南),重新组织和重命名表格和列。
-
清理列数据类型,例如时间戳。
-
添加相关列,例如 margin = revenue minus cost。
-
修复已知的数据质量问题。
而汇总数据,如每月总收入,自然属于度量存储的责任。
将更多的转换步骤移到度量指标存储中会增加计算成本——远高于我们在存储方面节省的费用。因此,你可能需要将一些昂贵或经常请求的计算保留在“T”步骤中。这也是度量指标存储的物化和缓存功能可以帮助的地方。
为什么选择 Transform Data 的 MetricFlow
我们选择了MetricFlow有三个原因:
-
它是开源的,并且带有友好的许可证。
-
这是一个 Python 包,适合我们在教程中使用的数据栈。而且它容易配置。
-
dbt Labs 将收购 MetricFlow 背后的公司——这强烈表明 MetricFlow 在未来几年将会蓬勃发展。
想了解其他竞争语义空间的工具吗?请查看这个概述并阅读这篇文章。
教程第一部分——设置本地环境。
我们使用DuckDB进行存储,dbt进行转换层,MetricFlow 作为我们的度量指标存储,以及一个Streamlit应用程序用于展示。这四个工具都是 Python 包,可以舒适地安装在Conda 虚拟环境中。
但首先,我们需要安装 Postgres 和 MySQL,虽然我们使用的是 DuckDB,这两者都是 MetricFlow 包的依赖项。在我的 Mac 上,我需要执行以下操作:
brew install postgresql mysql
接下来,克隆 这个 GitHub 仓库,cd 到根目录,执行
conda env create -f environment.yml
并激活 conda activate local-mds。
教程第二部分 — 生成和探索原始数据。
以下 Python 脚本生成合成数据并将其导入本地 DuckDB 数据库:
python src/generate-raw-data.py
“原始”数据包括四个表:accounts、sites、orders 和 fx rates。一个虚拟业务包含订单
con = duckdb.connect('data/data-mart.duckdb')
con.query('select * from raw.orders limit 5').df()
Id SiteIdDelivery DateBooked CurrencyCode Price Cost Type
0 o00000 s00018 2020-10-28 USD 13.44 9.36 Parts
1 o00001 s00032 2020-05-04 USD 84.74 49.30 Parts
2 o00002 s00009 2021-08-16 USD 76.38 41.19 Installation
3 o00003 s00025 2022-07-26 USD 25.51 9.99 Installation
4 o00004 s00038 2020-02-16 GBP 49.54 20.08 Parts
用不同货币预订的汇率
con.query('select * from raw.fx_rates limit 5').df()
Year Currency Rate
0 2020 Euro 1.10
1 2021 Euro 1.20
2 2022 Euro 1.15
3 2020 Pound Sterling 1.30
4 2021 Pound Sterling 1.40
并交付到网站
con.query('select * from raw.sites limit 5').df()
Id SiteOwnerId Region
0 s00000 a00000 Antarctica
1 s00001 a00001 Europe
2 s00002 a00002 South America
3 s00003 a00003 South America
4 s00004 a00004 South America
由拥有账户的客户拥有
con.query('select * from raw.accounts limit 5').df()
Id CompanyName BillingRegion
0 a00000 Cisco Systems, Inc. Antarctica
1 a00001 Asbury Automotive Group Europe
2 a00002 Helping Hand South America
3 a00003 Buena Vista Realty Service South America
4 a00004 The Pink Pig Tavern South America
教程第三部分 — 使用 dbt 转换数据。
我们的 git 仓库已经包含了一个名为 data_mart 的 dbt 项目。使用 dbt 转换数据就像编写 SQL 模板并将其放入“models”目录 data_mart/models/ 一样简单。
我们遵循 dbt 的风格指南。首先,我们在 models/staging/ 中创建一个“staging”模式。每个暂存模型都通过简单的重命名、转换和其他操作镜像原始数据。我们通过
-- data_mart/models/staging/stg_fx_rates.sql
select
Year::integer as year,
Currency as currency_name,
Rate as to_usd_rate
from raw.fx_rates
其他三个暂存表遵循非常类似的逻辑。
暂存层不应该被下游应用程序直接消费。为此,我们创建了第二层也是最终的转换层,“marts”。我们将货币名称解析为代码,以便它们与我们的订单中的代码匹配:
-- data_mart/models/marts/fct_fx_rates.sql
select
year,
case
when currency_name = 'Euro' then 'EUR'
when currency_name = 'Pound Sterling' then 'GBP'
else null
end as currency_code,
to_usd_rate
from {{ ref('stg_fx_rates') }}
我们使用这些汇率将所有订单金额转换为美元:
-- data_mart/models/marts/fct_orders.sql
with orders as (
select *,
extract('year' from booked_date)::integer as booked_year
from {{ ref('stg_orders') }}
), rates as (
select * from {{ ref('fct_fx_rates') }}
)
select
orders.order_id,
orders.deliver_to_site_id,
orders.order_type,
orders.currency_code,
orders.order_price_lcu,
orders.order_cost_lcu,
orders.order_price_lcu * rates.to_usd_rate as order_price_usd,
orders.order_cost_lcu * rates.to_usd_rate as order_cost_usd,
orders.booked_date
from orders
left join rates
on orders.booked_year = rates.year and orders.currency_code = rates.currency_code
注意我们如何引用最终的汇率表 — 这正是 dbt 发挥作用的地方!同时,也请注意,我们需要临时添加一个年份列,以便根据年份和货币代码连接汇率。
其他两个最终表只是暂存表的 1 对 1 复制,这使我们的 dbt 建模完成。我们通过以下三个步骤完成 dbt 练习:
- 我们将暂存物化为视图,将最终层物化为表,正如 dbt 风格指南中推荐的那样。我们可以通过更改 dbt 项目配置文件来最舒适地完成此操作:
# bottom of data_mart/dbt_project.yml
models:
data_mart:
+materialized: table
staging:
+materialized: view
+schema: staging
marts:
+materialized: table
+schema: marts
2. 我们配置与数据库的连接:
# $HOME/.dbt/profiles.yml
data_mart:
outputs:
dev:
type: duckdb
path: <path-to-data-mart.duckdb> # replace with absolute path
target: dev
3. 最后,我们在命令行中将所有模型物化为数据库中的表:
dbt run --project-dir data_mart/
我们可以通过查询任何新表来验证这一步。比如,
con = duckdb.connect('data/data-mart.duckdb')
con.query('select * from main_marts.fct_orders limit 5').df()
order_id deliver_to_site_id ... order_cost_usd booked_date
0 o00000 s00048 ... 10.2960 2020-10-28
1 o00010 s00033 ... 86.1250 2020-01-21
2 o00014 s00039 ... 20.8725 2022-04-25
3 o00015 s00035 ... 32.8580 2021-08-18
4 o00019 s00026 ... 0.3410 2020-07-27
教程第四部分 — 指标和维度
回顾一下,指标存储定义了指标以及我们可以在哪些维度上计算这些指标。专门的地方在于我们 git 仓库根目录下的 metrics/ 目录。
MetricFlow 中的两个主要对象是数据源和指标。与 dbt 不同的是,我们在 YAML 中配置这些对象。然后,由指标存储负责将请求转换为 SQL 并在我们的数据库中执行它们。
让我们先从指标的规范开始。我们在同一个 YAML 文件中定义了两个:
# metrics/metrics.yml
metric:
name: order_revenue_usd
type: measure_proxy
type_params:
measure: order_revenue_usd
---
metric:
name: order_margin_percent
type: expr
type_params:
expr: (order_profit_usd / order_revenue_usd) * 100
measures:
- order_profit_usd
- order_revenue_usd
MetricFlow 提供了几种不同的方法来定义度量,选择几种类型。类型measure_proxy接受任何已定义的度量并应用其默认的聚合策略。类型expr允许我们使用 SQL 语法。我们在数据源中定义度量和聚合策略。以下是我们对源orders的规范:
# metrics/source_orders.yml
data_source:
name: orders # must be unique across all sources
description: Master table customer orders.
sql_table: main_marts.fct_orders # the corresponding database table
identifiers: # primary and foreign keys
- name: order_id
type: primary
- name: site_id
type: foreign
expr: deliver_to_site_id
measures:
- name: order_revenue_usd
description: The total order revenue in USD.
expr: order_price_usd
agg: sum
- name: order_profit_usd
description: The profit of orders in USD.
expr: order_price_usd - order_cost_usd
agg: sum
dimensions: # temporal or categorical attributes, or "dimensions"
- name: order_type
type: categorical
- name: booked_date
type: time
type_params:
is_primary: true
time_granularity: day
mutability: # only relevant for caching
type: immutable
我们可以在相应部分指定的每个维度上汇总度量。通过使用指定的外键标识符,我们甚至可以使用来自其他数据源的维度进行汇总。
一个“标识符”指向一个外部数据源,即
# metrics/source_sites.yml
data_source:
name: sites
description: Master table customer sites.
sql_table: main_marts.dim_sites
identifiers:
- name: site_id
type: primary
- name: account_id
type: foreign
expr: owner_account_id
dimensions:
- name: site_region
type: categorical
mutability:
type: immutable
有了这个,我们可以计算不同站点区域的订单指标。由于站点通过另一个外键规范与账户相关联,我们甚至可以跨站点客户维度计算订单指标。
使用 YAML 的关键点在于其简洁性。你不需要深入的工程经验就能理解规范。下次业务询问你某个度量是如何定义的,请指向 YAML 中的相应规范。
最后,我们需要将 MetricFlow 连接到我们的数据库:
# $HOME/.metricflow/config.yml
model_path: <path-to-the-metrics-directory> # <repo-root>/metrics
dwh_schema: main_marts
dwh_dialect: duckdb
dwh_database: <path-to-data-mart.duckdb> # <repo-root>/data/data-mart.duckdb
你可以通过在命令行上执行以下操作来验证连接:
mf health-check
教程第五部分 — MetricFlow API 实践
传统上,度量定义在多个地方:隐藏在 BI 应用程序、报告实现、Jupyter 笔记本等中。我们通过将度量和维度移动到一个地方——度量存储,克服了这个问题。但这项变更只有在我们的度量存储与数据栈良好集成时才有效。我们的 BI 应用引擎知道如何与 MetricFlow 通信吗?对一些应用是正确的,但对许多其他应用是不正确的。
和许多其他 MDS 类别一样,与当前技术栈的集成将驱动你对度量存储解决方案的选择。下面我们使用 MetricFlow 的 CLI 和 Python 接口。后者适用于任何使用 Python 的 BI 工具,如 Streamlit。
我们从命令行的第一个示例开始:
mf query --metrics order_revenue_usd --dimensions booked_date__year --order booked_date__year
✔ Success 🦄 - query completed after 0.25 seconds
| booked_date__year | order_revenue_usd |
|:--------------------|--------------------:|
| 2020-01-01 00:00:00 | 290906.78 |
| 2021-01-01 00:00:00 | 316765.36 |
| 2022-01-01 00:00:00 | 196059.51 |
我们使用了两个配置度量中的一个,并将时间作为唯一维度。请注意,MetricFlow 允许我们通过简单地添加__year来切换到年度粒度。查看文档以了解其他选项。
第二个示例通过使用第二个度量和来自外键关系的第二个维度增加了复杂性:
mf query --metrics order_revenue_usd,order_margin_percent --dimensions booked_date__year,site_id__account_id__account_region --order booked_date__year
✔ Success 🦄 - query completed after 0.56 seconds
| booked_date__year | site_id__account_id__account_region | order_revenue_usd | order_margin_percent |
|:--------------------|:--------------------------------------|--------------------:|-----------------------:|
| 2020-01-01 00:00:00 | North America | 41072.23 | 54.35 |
| 2020-01-01 00:00:00 | South America | 74438.04 | 50.04 |
| 2020-01-01 00:00:00 | Asia | 11014.66 | 53.45 |
| 2020-01-01 00:00:00 | Europe | 16931.01 | 50.34 |
| 2020-01-01 00:00:00 | Australia | 86869.00 | 50.86 |
| 2020-01-01 00:00:00 | Antarctica | 16628.68 | 54.75 |
| 2020-01-01 00:00:00 | Africa | 43953.17 | 51.79 |
| 2021-01-01 00:00:00 | Africa | 41805.07 | 51.56 |
| 2021-01-01 00:00:00 | Australia | 96069.27 | 52.88 |
| 2021-01-01 00:00:00 | Antarctica | 20648.17 | 52.80 |
| 2021-01-01 00:00:00 | North America | 42920.44 | 53.30 |
| 2021-01-01 00:00:00 | South America | 83231.81 | 51.95 |
| 2021-01-01 00:00:00 | Europe | 19789.86 | 52.43 |
| 2021-01-01 00:00:00 | Asia | 12300.75 | 53.04 |
| 2022-01-01 00:00:00 | South America | 51114.66 | 50.94 |
| 2022-01-01 00:00:00 | Africa | 27058.34 | 53.68 |
| 2022-01-01 00:00:00 | Europe | 11311.23 | 55.24 |
| 2022-01-01 00:00:00 | Asia | 8608.74 | 51.16 |
| 2022-01-01 00:00:00 | North America | 27818.37 | 56.11 |
| 2022-01-01 00:00:00 | Australia | 60342.90 | 50.47 |
| 2022-01-01 00:00:00 | Antarctica | 9805.27 | 54.43 |
接下来,我们用以下命令启动我们的 Streamlit 应用:
streamlit run src/app.py

在应用程序的实现细节中,你将找到后端如何在显示数据之前提取数据:
import streamlit as st
from metricflow import MetricFlowClient
client = MetricFlowClient.from_config()
@st.cache_data
def load_data(metric: str, granularity: str = 'month'):
data = client.query(metrics=[metric],
dimensions=['site_id__site_region', f'metric_time__{granularity}'],
order=[f'metric_time__{granularity}']).result_df
data.columns = ['Period', 'Region', metric]
return data
再次是 MetricFlow 在幕后完成繁重的工作。不论我们选择哪种 MetricFlow API,数据都将是相同的。
结论
你是否也厌倦了讨论为什么在不同的应用程序中“相同”维度上的“相同” KPI 显示不同的数字?另一方面,如果对像上个季度总收入这样基本的事情没有共识,也是很烦人的。那么,哪里出了问题?以下是我个人经验中的一些例子:
-
销售部门通过“关闭日期”将收入记录分配到一个季度,而财务部门则通过“记账日期”来分配。
-
在聚合之前,需要删除一些收入记录,例如,因为它们与取消相关。这些规则可能比通过单个布尔“IsValid”列进行过滤更复杂。有时,不同的业务单元之间没有达成共识。不同的规则在雷达下被应用。
-
我们有不同货币的交易。对于每个时间点,应用哪个汇率也没有共识。
避免这些陷阱的唯一方法是:停止在孤立环境中实施转型管道。相反,使用单一的数据模型和一个地方来定义指标和维度。销售和财务是否都需要自己的收入 KPI?创建两个,并将它们放入指标库中,以便每个人都能解释差异。
微软与谷歌:语言模型是否会超越搜索引擎?
原文:
towardsdatascience.com/microsoft-vs-google-will-language-models-overtake-search-engines-4e9590bed232
评论
如果微软更名 Bing,我会考虑……
·发表于 Towards Data Science ·阅读时间 12 分钟·2023 年 1 月 10 日
--

巨头之战。图片来源:Midjourney
有传言称微软已经启动了一个项目,预计将在未来几年内对科技领域产生影响。正如The Verge 的 Tom Warren 所写的那样,这家软件巨头“据说计划推出一个使用 ChatGPT 的 Bing 版本。”二十年来,搜索领域即将迎来一场革命。
这条不令人惊讶的消息是在微软的合作伙伴 OpenAI 发布了ChatGPT,一个为对话优化的强大语言模型之后一个月传出的,我认为这是世界上最好的聊天机器人——虽然可能不会持续太久。
在 ChatGPT 于 11 月 30 日发布之后,人们迅速意识到其存在带来了语言模型(LM)在短期内超越传统搜索引擎(SE)作为在线信息检索主要手段的非忽视概率。进一步说,这意味着谷歌在搜索领域二十年的霸主地位可能面临威胁。
微软(尚未正式宣布)的声明重新点燃了语言模型与搜索引擎的辩论,尽管没人知道事件将如何发展,但有一个共识;无论如何,语言模型和搜索在未来很可能将成为一个更大整体中不可分割的部分。
就像重力把我们拉向地面一样,技术自发地流动——如热力学中所用——在一个方向上:让我们的生活更轻松。语言模型更具直观性,与它们的交互对我们来说是自然的。搜索引擎要么改变,要么消亡,这似乎是一个不可避免的结果。
我知道,这听起来像是你典型的泛泛而谈的不可证伪的预测。幸运的是,我们可以揭示一些未知的问题:ChatGPT 是否对 Google 构成真实威胁?微软能否推翻 Google?搜索巨头能否做出适当反应?最终哪家公司会脱颖而出?语言模型会取代搜索吗?还是会补充它?语言模型将在什么方面改善或削弱搜索?这一切将如何以及何时发生?
让我们尝试回答这些问题,了解未来语言模型和搜索引擎将如何互动,微软、谷歌和 OpenAI 对此有何看法,以及我认为未来几个月/几年将如何发展。
这篇文章摘自 算法桥梁,这是一本旨在弥合算法与人们之间差距的教育通讯。它将帮助你理解 AI 对你生活的影响,并发展出更好地应对未来的工具。
弥合算法与人们之间的差距。关于对你有意义的 AI 的通讯。点击阅读《The…》
thealgorithmicbridge.substack.com
语言模型与搜索引擎
一位名为 josh 的 Twitter 用户在 ChatGPT 公开的那一天首先指出:“谷歌完了。”其他人,比如现在已退出 Twitter 的乔治·霍兹,也同意这一观点——但并不是所有人都得出了相同的结论。
加里·马库斯教授用实证证据回应了乔治·霍兹的观点,谷歌的弗朗索瓦·肖莱也指出了类似的问题:“搜索是搜索问题,而不是生成问题:”
我同意马库斯和肖莱的观点。语言模型本身无法超越搜索引擎。然而,搜索引擎可以显著改进,达到那些未集成基于语言模型的特性就会变得过时的地步。
如果我们接受这个假设,很容易看出,最适合将语言模型和搜索结合起来的公司是谷歌。不是 OpenAI,不是微软。即便是分别来看,谷歌在这两个领域的世界领先地位也无与伦比。尽管 OpenAI 很受欢迎,GPT-3、ChatGPT 以及所有类似的模型都基于谷歌的技术,而且谷歌的搜索引擎占据了市场份额的 4/5。
如果这家公司没有推出太多的 AI 产品,那是因为其“制度惯性”,正如Stability 的 Emad Mostaque 所说。在研究深度和广度方面,谷歌无疑是全球领先的 AI 公司。
然而,正如受欢迎的投资者Balaji Srinivasan 所解释,研究和生产是两种不同的事物:谷歌不能冒着从头重构其搜索引擎以通过语言模型提升其功能的风险。这家公司多年来推出了新的搜索功能和渐进式的改进,但没有像微软——以及像Perplexity、You、和Neeva——那样的革命性进展。
我对语言模型与搜索引擎的看法可以总结如下:“搜索引擎的局限性要大得多,但在[搜索网络]的任务上装备更好……[但是] 我认为[传统的]搜索引擎将无法在语言模型面前存活。”这里的关键词——我在原文中没有提到——是“传统的”。
搜索引擎将会存在,但它们会变得如此不同,以至于难以辨认。语言模型很可能是原因所在。
(我不会详细讨论将语言模型整合进搜索引擎是否是个好主意。Gary Marcus 有一篇很棒的文章,我几乎完全同意他的观点:“ChatGPT 是否真的对谷歌搜索构成‘红色警报’?”)
微软与谷歌:技术时代的大战
微软对 OpenAI 的 10 亿美元投资——以及获得的独家许可证——明确表明了其对这一领域的兴趣。毫不奇怪,他们计划将DALL-E和 ChatGPT 整合进他们的服务中。增强版的 Bing 搜索引擎可能会“挑战谷歌的主导地位”,正如 Tom Warren所写。
当然,这个想法并不是用语言模型来取代搜索引擎,而是为了补充它。一位微软发言人告诉彭博社,“对用户查询的对话式和上下文回应将通过提供超越链接的高质量答案来赢得搜索用户的青睐。”
不像谷歌,微软非常清楚语言模型不如搜索引擎可靠。公司必须评估实现那些人们无法 100%依赖的功能的风险与其在与谷歌竞争中的潜在收益。微软正在“权衡…聊天机器人的准确性[以及]初始发布可能是对少数用户的有限测试。”听起来是一个合理的开始。
然而,如果有人比微软更了解语言模型能做什么和不能做什么,那就是谷歌。在一篇2021 年的论文中——这篇论文发布的时间远早于 ChatGPT 成为构想的种子——谷歌研究人员探讨了使用语言模型来“重新思考搜索”的问题。
他们考虑了是否可以做到,更重要的是,是否应该做到:
“传统的信息检索系统 [即传统的搜索引擎] 不直接回答信息需求,而是提供对(希望是权威的)答案的参考。
…
相对而言,预训练语言模型能够直接生成可能响应信息需求的文章,但目前它们只是外行而非领域专家——它们对世界并没有真正的理解,容易产生幻觉,至关重要的是,它们无法通过引用它们所训练过的语料库中的支持文档来证明自己的言论。”
谷歌最终得出的结论是,使用类似 ChatGPT 的系统来提升其搜索引擎会带来较高的“声誉风险”。首席执行官桑达尔·皮查伊和 AI 负责人杰夫·迪安告诉 CNBC,“如果出现问题,成本将比 OpenAI 更高,因为人们必须信任他们从谷歌获得的答案。”
谷歌在 2021 年 5 月宣布了 LaMDA(但没有发布)。鉴于 LaMDA 与 ChatGPT 相当——如果不是更好,正如 Blake Lemoine 所称——有理由质疑为什么谷歌没有利用它来规避像 OpenAI 这样的威胁。Balaji Srinivasan 预测这是因为公司没有足够的“风险预算”,事实证明,他是对的。
像谷歌这样的大公司,为数十亿用户(而非像 OpenAI 那样的几百万用户)提供的高可靠性服务,如谷歌搜索,不能仅仅因为某项新技术似乎是未来趋势并且大家都疯狂追捧就冒险嵌入一个不值得信赖、未经严格测试的技术。
但 Google 高管并不是傻子。他们知道 ChatGPT,尽管由一家规模更小、风险规避程度较低的公司拥有,却确实是一个威胁——尤其是当像微软这样的直接竞争对手在其中拥有大量股份时。这就是为什么他们将 ChatGPT 宣布为“红色警报”,正如 纽约时报 报道的那样:
“…随着一种新型聊天机器人技术的出现,可能会重新定义甚至取代传统搜索引擎,Google 可能面临对其主要搜索业务的第一次严重威胁。一位 Google 高管描述这些努力是决定 Google 未来的关键。”
…
“Google 必须投入其中,否则行业可能会在没有它的情况下继续发展…”
就目前的情况而言,Google 面临着微软——在搜索领域是一个强劲的直接竞争对手——以及 OpenAI——拥有类似的 AI 技术,尽管预算要紧张得多——同时,还要平衡由于语言模型固有的不可靠性和它们在低风险规避初创公司手中构成的明显威胁所带来的声誉风险。
正如 皮查伊所说,Google 必须“大胆且负责任”,并找到一个折衷解决方案。“我们必须做到这一点,这非常重要,”迪恩总结道。
我的对事件发展如何展开的预测
鉴于目前的情况,我认为有三个关键点需要关注,以理解将会发生什么以及如何发生。首先,Google 真的在与谁竞争,以至于将“声誉风险”报告为前进的主要障碍?第二,是否可能通过语言模型和当前的 AI 安全/对齐技术“做到正确”?第三,即使可以做到,公司也认为应该这样做,是否能从中衍生出一个可行的商业模式?
Google 的真正敌人
当我阅读了关于 ChatGPT 威胁的 皮查伊和迪恩的论点 时,我注意到了一些奇怪的地方:他们似乎在暗示 Google 正在与 OpenAI 竞争。确实,OpenAI 的技术被 Google 高管称为“红色警报”,但我认为 OpenAI 并不是 Google 的威胁——这是错误的框架。
一方面,OpenAI 无法在技术研究和 AI 专业知识方面与 Google 竞争。Google 的预算和人才远远超过 OpenAI——即使只是从数量上来看。正如 Emad Mostaque 论述的:
另一方面,OpenAI 不想 与 Google 竞争。
OpenAI 的声誉风险远低于 Google,因为它是一家相对较新且规模较小的公司,最多为几百万用户提供服务,而估计声称 Google 搜索被全球超过 40 亿人使用,占据了令人瞩目的84%市场份额。
然而,OpenAI 报告的使命是建立一种有益的通用人工智能(AGI)。他们为什么要冒着与主要目标完全不重叠的大公司竞争的风险呢?
即使 OpenAI 主要关注经济利润(不可否认,取代 Google 会导致一个极其成功的赚钱机器),公司也有更好的选择,不会与其长期目标冲突,比如设置付费订阅或按需付费模式,正如他们现在所做的(例如 GPT-3 和 DALL-E)。
在影响力、规模、预算以及最重要的目标方面,Google 的真正竞争对手是微软。但以这种方式看,Google 面临的 更高 声誉风险的论点就会崩溃。微软的用户数量与 Google 相当,公司也必须照顾其精心建立的声誉——正如其在 2016 年关闭种族主义聊天机器人 Tay 的决定所示。
支持“声誉风险”观点的一点是微软的搜索市场份额与 Google 不可同日而语。然而,如果微软成功地将语言模型和搜索结合起来,他们将增加用户数量,因此声誉风险也会相应增加。
剩下的问题是微软是否愿意决定将 ChatGPT 集成到 Bing 中,冒着声誉风险,因为用户可能因新服务的更强大功能而被吸引,只为有机会取代 Google。
Google 打算如何应对这一点?
“做对事情”是一个听起来很美好的目标,但实际上不可行。
Jeff Dean 解释 Google 正在等待“做对事情”的说法让我想起了我对将伦理原则融入 AI 模型和对抗虚假信息的重要性的更为乐观的看法。我认为这些努力至关重要,我会继续坚持这一观点,但我能看到,虽然在理论上它们是值得尊敬的,但在实践中几乎变得难以忍受。
在我看来,只有完全重新定义、重新设计和重建语言模型(LMs),才能符合 Dean 所指的那种标准。如果正如 Gary Marcus 所建议的那样,它们根本不具备真实、准确、可靠和中立的能力,那么无论多少临时性保护措施都无法遏制语言模型数据驱动核心所带来的恶劣本质。
也许,一旦公司尝试将 SE 与 LM 结合,前者的所有关键特性就会被 LMs 的功能设计缺陷所破坏。Marcus 在对 Perplexity、Neeva 和 You 的分析中提供了大量证据。他的结论保持了对未来的希望,但目前解决了这一争论:
“我能说的最好的事情是,Perplexity.ai 和 you.com 的聊天确实在探索一个有趣的想法:结合经典搜索引擎与大型语言模型的混合体,可能允许更快速的更新。但仍然有 许多 工作需要完成,以正确整合这两者,即经典搜索和大型语言模型。”
另一个问题是当前最先进的 AI 对齐技术是否足够好或朝着正确的目标发展。Scott Alexander 写了一篇很好的文章讨论了通过人类反馈进行强化学习(RLHF)的局限性,而 ChatGPT 使用这种方法,并且似乎是公司阻止 LMs 行为缺陷的唯一方法。
Alexander 直言不讳地说:“RLHF 效果不是很好。”正如我在我的 ChatGPT 文章中所写的,“人们可以‘轻松’通过其过滤器,它也容易受到提示注入的影响。”经过 RLHF 优化的模型还可能进入优先级冲突的循环。Alexander 表示,“惩罚无用的答案会使 AI 更可能给出错误的答案;惩罚错误的答案会使 AI 更可能给出冒犯性的答案;等等。”同时生成有用、真实且不冒犯的响应可能是不可能的。
此外,如果 LM 在 RLHF 中的改进是渐近的,正如 Alexander 所思考的,我们将永远无法“做到正确”——然而,由于这是表现最佳的方法,公司没有动力花费时间和资源去研究另一种可能——也可能——同样有效的方法。
如果上述所有情况都属实——LMs 本质上不适合搜索,而我们能访问的最佳技术也只是平庸的——那么短期内不会有一个“做到正确”的时刻,就如 Jeff Dean 所希望的那样——而 Google 也需要。
谷歌将面临一个困境:一方面,他们可以让微软领先,承担“声誉风险”,有可能重新定义搜索的未来,成为该领域的下一个霸主。另一方面,他们可以决定“做对”是一个过于宏大的目标,自行承担声誉风险,配合一些公关手段(例如“我们尽了最大努力”)和不成熟的功能(例如“现在效果更好”)——但最终在 AI 和搜索领域保持领先,并在未来几十年继续竞争。
如果谷歌不得不在声誉和生存之间做出选择,我想我们都知道会发生什么。
基于语言模型的搜索是否与盈利兼容?
但挑战的最后部分,如果一切对谷歌来说顺利,将会是一个不可避免的障碍。微软也不会摆脱这一困境。如果搜索通过广告业务模型是有利可图的,那么当无需点击任何东西时,企业如何从基于语言模型的搜索中获利呢?
如果谷歌在领先的情况下,能否找到一种方法,在基于语言模型的搜索周围建立护城河,同时围绕语言模型+搜索构建一种新的可行商业模型?谷歌的 PageRank 算法结合广告模型二十年前是无敌的组合。谷歌能否重现这一壮举?
当然,不用说,如果我们能享受一个没有广告的互联网,那将是很棒的。然而,另一种选择是将搜索转变为付费服务。人们愿意接受这种反惯性的改变吗?
我看到的一个替代可能性(可能只是一个疯狂的假设)是,微软可能决定通过将搜索变成无盈利的服务(没有广告或其他任何形式的盈利)来商品化搜索,目的是在几年内有效地将谷歌从市场中移除。
然而,还有其他问题可能阻止微软尝试这一点。正如 Marcus 在他的文章中解释的那样,当前的搜索比语言模型便宜得多,而且速度更快。这意味着利润更少,这是公司所厌恶的。微软将同时耗费资金与谷歌竞争,这将使双方陷入一个非常冒险的商业操作。
不论最终结果如何,很明显,搜索空间,已经停滞了近二十年,即将经历一个前所未有的拐点。
订阅 The Algorithmic Bridge。弥合算法与人们之间的差距。关于对你的生活重要的 AI 的新闻通讯。
你也可以通过使用我的推荐链接直接在 Medium 上支持我的工作,并成为会员,获得无限访问权限 这里 😃
微软的新 AI 方法预测分子如何移动和功能
被称为“Distributional Graphormer”的技术,可能会启动 AI 在化学和生物科学领域的下一步革命。
LucianoSphere (Luciano Abriata, PhD)
·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 7 月 20 日
--

图 1. 分子(橙色)的草图,采用三种不同的结构(x 轴),每种结构具有不同的能量(y 轴)。这被称为分子由于运动可以探索的“构象景观”。局部或全局最小值的能量越低,分子在结构间的转换时间就越长(绿色箭头)。虽然像 AlphaFold 这样的常规结构预测方法旨在找到对应于最低能量结构的中心最小值,但现在的圣杯是找到可能结构的完整分布及其概率。见图 2 和文本以获取更多解释。图片由作者绘制。
“我们能否不仅预测静态蛋白质结构,还能预测其结构多样性?”——这是我大约一年前在 AlphaFold 2 的兴奋逐渐平息时提出的问题:
[## 我们能否不仅预测静态蛋白质结构,还能预测其结构多样性?
最近的一项研究以特殊方式应用 AlphaFold 2,表明这可能是可行的。
看起来,微软的团队将人工智能应用于科学(与一位著名教授合作,见最后的注释)可能会首先以“是”的答案来回应我的问题。他们刚刚展示了他们的新型“Distributional Graphormer”,它不仅可以预测单一分子结构(蛋白质或其他分子或材料),还可以实际预测分子或材料在三维空间中可以采用的多种替代结构(或“构象”)。也就是说,它们的原子在空间中可以采取的不同排列。此外,新的 AI 模型还“理解”不同结构将具有不同的能量,从而在不同程度上被占据;因此,新的模型可以训练来预测在分子运动过程中不同构象之间的热力学交换。
在其第一个版本中实现的 Distributional Graphormer 可以解析任何类型的分子。事实上,介绍该模型的预印本描述了其在预测蛋白质运动(我在上面的问题中称之为“结构多样性”)、蛋白质如何结合小分子(包括酶底物、药物、代谢物等)、分子如何在催化剂表面吸附以及碳结构如何在电子导电性质上进行调节等任务中的应用示例。
阅读下去可以详细了解这些内容,进一步了解模型,并掌握它可能对现代化学和生物学的影响。
Graphormer 的演变
微软的新 Distributional Graphormer 源自他们在 2021 年推出的 Graphormer,旨在加速从制药到材料的分子科学 AI 研究和应用。Graphormer 本身是一个深度学习算法和包,帮助研究人员和开发人员训练用于分子建模任务的自定义模型。
作为一种算法,Graphormer 是一种改进的变换器,更好地处理图表示,特别适用于描述神经网络中的分子,将其结构编码到模型中。例如,看看这些描述符如何帮助神经网络处理分子这里或这里。要了解更多,请查看这篇预印本,微软团队介绍了几种简单但有效的结构编码方法,并描述了 Graphormer 的表现力,包括具体示例。
Transformer 架构已经在许多领域成为主流选择,例如自然语言处理等。
作为一个包,Graphormer 提供了对各种分子模拟任务的支持,例如分子动力学模拟和属性预测,这两个元素都用于新工作:模拟以探究分子如何移动以改变原子的位置,从而根据需要改变形状,例如蛋白质构象探索;以及属性预测,例如将分子结构与属性相关联,如碳结构调节电子导电性。你可以在这里了解关于这个包的更多信息,包括研究人员和开发人员对它进行的多个测试:
## Graphormer - Microsoft Research
Graphormer 是一个深度学习包,允许研究人员和开发人员训练自定义分子模型……
在预测静态结构之后,AI 进入分子运动的预测
如果应用于科学的 AI 方法到目前为止主要集中在预测“静态”分子结构上,例如 AlphaFold 2 的成功 (参见这里),那么最终目标现在转向一个更加困难的目标:理解分子结构如何与功能相关。
这需要理解属性是如何从结构中产生的,即从空间中原子的三维排列,以及这些排列如何随着时间变化。后者被称为“内部分子动力学”,指的是原子相对彼此的位移,与简单扩散不同,后者中所有原子一起移动和旋转,从而整体上不改变分子的形状。这里有两个经历这种“内部动态”的分子系统的例子,通过经典物理模拟 - 首先是一个碳纳米管的振动,其次是一个蛋白质的折叠:
内部分子动力学,简称“分子动力学”或“结构构象”,源于非常快速且几乎是随机的热原子运动与较慢但规模更大且协调的多个原子的集体运动之间的耦合。举例来说,两个相互连接的原子可能在飞秒到皮秒的时间尺度上振动,而对其周围环境并不“察觉”,但由于热波动而运动的一堆相互连接的原子会在纳秒或微秒时间尺度上逐渐演化出集体运动,这可能导致分子的一部分发生显著移动,从而改变其整体形状和性质。
例如,在蛋白质中,平均结构中可能无法接触到的区域可能会因其内部动态而暂时暴露,从而允许例如底物分子的进入。这在蛋白质研究中被广泛研究,因为它是大多数甚至所有蛋白质功能的核心——这一主题我从“经典”分子动力学计算的角度在这里有所提及:
各种复杂程度、成功率和应用的计算方法概述,以及关键…
文章中描述的模拟类型,本质上是模拟物理并整合牛顿运动方程,这些模拟非常强大,但在计算能力方面要求极高,因此运行速度非常慢,更不用说收敛了。因此,如果能够在不实际运行任何分子模拟的情况下快速预测分子可以采用的替代构象及其运动方式,那将是一个巨大的进步。这正是许多大型人工智能实验室目前正在追求的目标,微软的新模型也许会有所不同。
微软的分布式图形变换器提供了一个初步的方法,这是一种深度学习框架,能够处理不仅是单一而是多个分子的结构或材料的结构,还包括热力学景观,这些景观衡量了观察每种结构的可能性。当我们谈论一个蛋白质及其结构时,我们通常指的是一个通过实验确定或建模的非常稳定的构象。但一个真实的蛋白质溶液包含无数个副本,每个副本都采用来自一组可能状态中的某种结构,这些状态有不同的观察概率。在这个“景观”选项中,最稳定的结构(就自由能而言,即能量和熵的组合)是最常见的,这意味着在某一时刻更多的分子副本会采用对应的结构。更重要的是,由于其内部动态,蛋白质可以改变其结构,从而有效地“跳跃”到另一个常见状态,在短暂的时间内通过一个由自由能不青睐的“非可能”状态进行过渡。通常,由于热波动,蛋白质会持续改变结构,平均而言更多时间保持在最稳定的状态。所有可能结构及其分布的集合就是所有生物学家希望预测的“热力学构象景观”——而且即使通过实验也非常难以获得。

图 2. 一个非常简单的、一维的分子构象景观(橙色),它可以采用比如说三种主要结构:最稳定的一个在中心,通常是通过实验方法“确定”的或通过常规结构预测方法“预测”的;还有两个具有稍高自由能的替代状态,这意味着在给定的温度下它们的存在几率较低。真实的构象景观是多维的,因为多个分子自由度可以贡献于结构的多样性。图中的绿色线条示例了状态之间的过渡路径,这实际上发生在真实分子中的多个维度上。图由作者绘制。
分布式变换器的作用
尽管机器学习在过去 3 到 4 年里通过 AlphaFold 对蛋白质的特定应用极大地改善了分子结构预测,但许多现实世界的应用需要理解从结构分布中得出的宏观观察,而不是单一的结构——即我刚刚解释的构象景观。科学家们用来采样这些分布的常规方法,通常基于分子动力学模拟,使用某种物理或数据驱动的势能模型,计算开销很大。非常昂贵。
微软的 Distributional Graphormer 是首个解决这一挑战的工具,它通过利用深度神经网络将简单的起始分布(如高斯分布)转化为通过分子系统描述符条件化的平衡分布的预测。简单分布到粗略景观的扩展是通过类似于热力学中的模拟退火模拟的扩散过程实现的。
为了指导状态分布的学习,该架构可以在转换过程中使用不同类型的数据或信息进行训练,例如专门构建的能量函数、通过分子动力学模拟生成的数据等。在训练过程中,Distributional Graphormer 旨在最小化为每个状态预测的概率与作为训练提供的概率之间的差异。
值得注意的是,这种方法允许利用系统的先验知识,对于广泛研究的分子而言,这种知识可能相当丰富。但显然,Distributional Graphormer 也表现出良好的泛化能力,这可能是因为它继承了先进深度学习架构的优点,并将其应用于分布预测这一新的挑战性任务。
多样性通过不同的应用场景得到验证
Distributional Graphormer 高效地生成多样的构象,并提供状态密度的估计,从而接近对相关分子构象景观的描述。展示 Distributional Graphormer 的预印本和博客文章已经展示了该框架在多个分子任务中的应用,包括蛋白质构象采样、配体与蛋白质结构结合、分子在催化剂上的吸附以及通过调节材料属性引导结构生成。
在蛋白质构象采样中,如果你关注我的工作,你会知道我可以更深入地探讨,Distributional Graphormer 被证明能够成功生成一些研究较多的蛋白质的结构和构象景观,与分子动力学模拟相比,展示了良好的构象景观覆盖范围,但生成速度要快得多。一些例子还展示了该模型生成具有多个实验上已知的亚稳态结构的蛋白质功能性构象状态的能力。此外,这些例子还展示了在某些情况下,Distributional Graphormer 输出的构象景观预测了不同构象之间相互转换的合理路径,从而更深入地理解蛋白质动态。
在配体结合结构生成中,分布图变换器被展示为能够预测配体在多个示例蛋白质的口袋中的结合姿态。一个类似的任务,但涉及完全不同类型的原子,证明了模型框架的灵活性,预印本和博客展示了它如何有效地采样催化表面的吸附体配置,这对理解催化过程至关重要。
最后,预印本和博客展示了该模型如何通过电子带隙预测器实现基于属性的结构生成。通过逐步优化简单的分布,该模型生成了具有目标带隙值的碳结构,包括一些已知的碳晶体,如钻石和石墨。
上述示例表明,至少核心思想在分布图变换器中得到了实现,并且在有足够的训练数据的情况下,它可以成为一个非常强大的工具。毫无疑问,它可以促进深度学习在分子系统中进一步发展,对制药、生物技术、材料科学等领域产生直接影响。作为基础模型,它还可以为分子科学领域开辟新的研究机会,并通过数据构建为复杂分子系统的统计理解铺平新道路。
参考文献
由微软 AI4Science 和量子部门的研究人员主导的 arXiv 预印本,包括科学家 Frank Noe。后者是柏林自由大学的微软合作研究经理,是我最欣赏的现代科学家之一。他是分子模拟领域中较年轻的世界领导者之一,将严谨的理论方法与务实的应用结合在一起。他现在当然在大量探索 AI 方法,提出了深度学习系统辅助分子模拟的第一个要素,如 Boltzmann Generator。过去,他做出了各种贡献,其中最有趣的是他开创了使用马尔科夫状态模型描述分子力学的方法。
深度学习的进展大大提升了分子结构预测的能力。然而,许多宏观…
微软的两个类似博客的条目,以更易于理解的格式解释了这项工作:
[## 预测分子系统的平衡分布的深度学习方法
预测分子系统的平衡分布的深度学习方法
分布式图变换器:朝着分子系统平衡分布预测的方向 [## 分布式图变换器:朝着分子系统平衡分布预测的方向
分布式图变换器,微软新推出的深度学习框架,用于预测…
www.lucianoabriata.com 我写作和拍摄关于我广泛兴趣范围内的一切:自然、科学、技术、编程等。
在这里小费我 或 成为 Medium 会员 以访问所有故事(我会获得少量收入,对你没有成本)。 订阅以获取我的新故事 通过电子邮件。 关于小工作的咨询 请在我的 服务页面。你可以* 在这里联系我。
迁移数据?充分利用产品本身!
原文:
towardsdatascience.com/migrating-data-make-the-most-of-the-product-itself-b79b56d5e6c1
利用产品功能使数据迁移过程更轻松
·发布在Towards Data Science ·7 分钟阅读·2023 年 3 月 1 日
--
在这篇文章中,我想分享我过去在数据迁移项目中的一些关键经验。我的公司正在将内部 CRM 系统迁移到 Salesforce,我负责促进数据迁移过程。在过程中我遇到了许多障碍,但我都克服了它们。下面列出了帮助我完成项目的最佳实践。如果你目前正在进行或计划进行数据迁移项目,这篇文章适合你。如果你对这个问题完全陌生,我尝试保持语言简洁,以便你可以跟随并大致了解数据迁移项目的运作方式。尽管我使用的是与 Salesforce 更相关的示例,你可能会发现对你所使用的特定产品有价值的内容。

熟悉产品
我已经了解了内部 CRM 系统(我们称之为遗留系统)及其数据架构,因此我所需要做的就是熟悉 Salesforce。我开始通过探索 Trailhead,Salesforce 的免费在线学习门户。它们还提供了一个免费的开发者登录账户(注册这里),以便你可以探索他们的产品及其功能。在我获得雇主提供的访问权限之前,我使用了这个账户。最初的调查使我能够发现所有潜在的产品功能,这些功能我可以在之后利用。
创建你的测试环境
如果可能,为自己(和你的团队)设置一个独立的测试环境来测试你正在构建的数据管道/连接。如果产品不允许你创建独立环境,你必须建立一些测试标准和技术。所有测试数据应易于识别并删除,以确保生产环境保持干净。你可能不是唯一一个在项目上工作的人员,你当然不希望其他人的活动对你的数据产生影响。
幸运的是,Salesforce 提供了创建这种环境的选项(称为 Sandbox)。通常,公司可以维护一个用于 UAT 测试的沙盒,一个用于备份恢复的沙盒,以及一个或多个用于开发目的的沙盒。Salesforce 的专业版提供了 10 个开发者沙盒,我认为这非常酷。我方便地测试了所有我构建的 API 连接、数据管道和一些其他代码。我可以发送、修改和删除所有测试记录,而无需过多担心。
设计架构
每个业务都是独特的,没有单一的产品能够完美适配。你总是需要做出特殊的调整,以确保数据符合业务逻辑。你必须为每个表找到最佳的数据结构。这也是一个很好的时机来审查当前的数据架构,并识别在新系统中可以改正的不足之处。Salesforce 允许客户创建自定义对象(表)和字段(列)来存储公司特定的数据。在专业版中,你可以创建多达 50 个自定义对象,每个对象最多可以有 100 个自定义字段。最棒的是,这些自定义对象和字段具有与内置对象和字段相同的功能。
例如,我能够将三个表合并为一个,并通过添加一个额外的字段来区分它们,该字段描述记录的类型。利用父子关系,我可以轻松地在父对象上设置聚合字段,当子项的数据更新时,这些字段会自动调整。我在字段上设置了验证规则,以便系统在存储数据之前进行质量检查。我还修改了页面布局,以调整向最终用户显示的数据。我做了几个这样的微小改进,结果得到了一个干净的数据架构,并降低了系统的整体复杂性。
自动化你的流程
这里我谈到的自动化有两种类型:
-
自动化业务流程
-
构建数据管道以迁移数据
自动化业务流程是首要任务。这可以减少终端用户的人工操作和错误。从数据角度来看,这也意味着你需要导入的数据稍微少一些。例如,销售团队会为客户创建一个“机会”。如果进入报价阶段,他们会添加产品和其他属性以生成报价。如果交易成功,那么使用从报价生成的签署合同,销售管理员将会在系统中创建一个包含相同产品的“订单”。过去,销售和 CRM 系统是不同的,因此双重录入不可避免。但有了 Salesforce,我能够轻松地使用内置的“流程”功能来自动化订单创建过程,这会直接从机会中提取所有数据。因此,对于仍在处理阶段的新的机会,订单直接在 Salesforce 中创建,而不是在旧系统中。
第二部分是自动化数据导入过程。理想情况下,数据应该只迁移一次。迁移完成后,用户应该只使用新系统。遗憾的是,这对我的组织来说并非如此。存在某些复杂的程序和边缘情况不适合 Salesforce,我们不得不等待开发人员为这些情况构建定制解决方案。另一方面,管理层希望用户开始使用 Salesforce,因此两个系统在一段时间内同时使用。对于旧数据,仅在遗留系统中进行更新。对我来说,这是一团糟,因为很难跟踪发生了什么变化,我不得不多次更新导入的数据。两个系统中的数据结构不同,在我能够导入 Salesforce 之前需要进行一些预处理。我通过编写代码来确保两个系统中的数据保持最新,从而节省了大量时间和精力。
注意数据治理
因为你正在过渡到一个新系统,你必须在授予终端用户访问权限之前格外小心。可能没有数据治理结构。默认情况下,每个人都可以看到所有内容,这可能非常危险。你必须定义角色,确定谁有权访问什么,以及你希望授予的访问量。Salesforce 拥有一个强大的框架来管理这些角色和职责。可以建立层级结构,并且可以控制到字段级别的读/写/查看或无访问权限,同时权限集可以配置以处理特殊情况。每当有人离职时,简单地停用他们的账户并将所有记录转移给其他人是非常容易的。最好设置至少一个用户作为测试账户,以便你可以测试每个角色的数据访问和权限。

图片由 Melanie Deziel 提供,来源于 Unsplash
维护报告
数字说明了一切。创建关于完成的迁移任务的报告,并且如果可能,将所有任务放在一个仪表板上以跟踪你的进度。如果在产品本身上无法做到这一点,可以在 Excel 中维护这些报告。这可以作为跟踪你进度的一种方式。Salesforce 提供了一个优秀的报告工具。虽然他们的仪表板相比于标准 BI 工具受限,但仍然是有用的。我为这个项目设置了一个单独的数据质量仪表板。我创建了多个报告来标记缺失数据和差异,并将它们全部放在一个仪表板上。这个仪表板与所有用户共享,他们只能看到自己的数据问题记录。这使得问题修复更容易,并随着时间的推移提高了数据质量。

图片由 Towfiqu barbhuiya 提供,来源于 Unsplash
我还利用了许多额外的功能,比如 Apex 代码、审批流程、电子邮件服务、Visualforce 页面、Chatter 等,但这些更多涉及开发方面,而不是数据。我不想深入每个让我印象深刻的 Salesforce 元素,因此在这里停下。作为一个数据人员,我必须承认他们的产品给我留下了深刻印象。希望你觉得这篇文章有用。请查看我之前的文章,其中谈到了在使用大数据创建 Tableau 仪表板时遇到的挑战。
## 使用 Tableau 仪表板的大数据经验——挑战与学习
使用 Tableau 仪表板从大数据中获取最佳洞察,前提是使用良好的数据集,这些数据集经过汇总且易于…
towardsdatascience.com
在你离开之前…
关注我的 Medium 账户,以便不遗漏我未来发布的任何新文章;你可以在我的 个人资料页面找到更多文章。你也可以在 LinkedIn 或 Twitter上与我联系!
微型神经网络用于吉他效果与微控制器

我的自制数字吉他效果器使用神经网络(图片由作者提供)
少即是多
·
关注 发布于 Towards Data Science ·7 min read·Apr 7, 2023
--
我逐渐意识到,限制可以驱动创造力和创新。艺术家拥有的选择越多,想出一个可靠的想法可能就越困难。如果没有音乐的规则,可能性会如此庞大,以至于几乎不可能写出可听的作品。正是这些规则和限制为一个想法提供了结构。当然,知道什么时候打破这些规则与知道它们是什么一样重要。
在过去的三年里,我一直对使用深度神经网络模拟模拟吉他放大器和效果器着迷。我编写了一些 音频插件,并测试了无数种神经网络变体用于模拟吉他设备。神经网络对 CPU 的需求很高,但使用 PC 时,你通常可以增加计算机的处理能力以实现更准确的声音。
有关更多信息,请参见我的文章 实时音频的神经网络.
但如果你想在一个便宜的微控制器上运行神经网络,使用一个小型(吉他效果踏板大小)的设备,专门用于运行音频效果而不做其他事情呢?像 Raspberry Pi 这样的微型计算机足够强大,可以运行操作系统,支持应用程序和吉他效果,还有多余的空间,这意味着更高成本下的资源浪费。这些计算机使用微处理器,而不是微控制器。微-处理器 具有外部内存和 I/O,而微-控制器 是一个自包含的计算系统,可以独立运行程序。微控制器通常不需要像 PC 或智能手机那样强大,因为它专门用于做一件事。微控制器非常适合数字吉他踏板,但为它们编写软件与智能手机或 PC 是不同的,这是我以前从未做过的。

部分构建的 Terrarium PCB(左)和 Daisy Seed(右)(图片作者提供)
于是,Daisy Seed 登场了!Daisy Seed 是一个 $30 的音乐嵌入式平台,利用 Cortex-M7 ARM 微控制器。它有自己的软件库 (libDaisy) 和音频 DSP 库 (DaisySP),都是开源的。对于吉他踏板,你需要一个输入音频缓冲区,用于将模拟吉他信号连接到数字硬件,还有一个输出缓冲区,用于与放大器匹配阻抗。这确保了你的音色在经过效果器时得以保留。PedalPCB 销售一种名为 Terrarium ($12) 的接口板,专门为 Daisy Seed 设计,甚至提供了最多 6 个旋钮、4 个开关、2 个脚踏开关、输入/输出单声道音频插孔和 9 伏电源(吉他效果器常用)的连接。
以 42 美元的音频硬件成本,我的成本已经远低于我之前使用的 NeuralPi 吉他踏板 的 Raspberry Pi4。我购买了电路板,得益于我之前对模拟效果的痴迷,手头有足够的硬件来构建 Terrarium 踏板。总成本约为 100 美元(包括外壳、电气组件、旋钮/开关),需要基本的焊接技能来组装。其他现有项目 在完成的 Terrarium 踏板上运行良好,声音也很棒。

我完成的 Terrarium 踏板。(图片作者提供)
不过,我的 NeuralPi 插件无法在微控制器上运行,所以我需要发挥创造力。我决定使用 RTNeural 引擎来运行神经模型,并使用 libDaisy 和 DaisySP 开发最小的 c++ 代码。虽然有几个代码库可以用于 M 系列微控制器的开发,但我没有这些的经验,所以如果可能的话,我希望坚持使用我已经熟悉的。
现在谈谈我提到的那些限制:处理能力和内存。M7 芯片的最大运行频率为 480MHz,在这个速度下只能使用 Flash 内存,一个只有 128KB 的小内存!(但公平地说,登月只用了 74KB)。Daisy Seed 上还有其他可以访问的内存区域,但我希望最大化我的处理速度,以运行尽可能大的神经网络,产生最佳的声音。
当我最初编译我的最小 c++ 程序与 RTNeural 时,我超出了 128KB 的限制。因此,我向 RTNeural 的创建者寻求帮助(正如我以前多次做的那样!)。他成功地将编译后的占用量减少了大量,为所需的代码和模型文件腾出了足够的空间。

Daisy Seed 通过 USB 连接以上传 c++ 程序(图片作者提供)
一旦我的程序可以加载到 Daisy Seed 上,我非常好奇它能处理多大尺寸的模型。我从 NeuralPi 模型(LSTM 尺寸 20)开始,但这太过庞大了,导致踏板没有响应。然后我决定从小开始,一步步达到极限,因此我使用了 LSTM 尺寸 4,成功了!它可以运行效果(捕获了我的 TS-9 过载踏板),但我对准确性不满意。我确定它能处理的最大尺寸是 LSTM 尺寸 7,这对大多数踏板来说足够好,可能在捕获低增益放大器时能获得一些不错的声音。请记住,Daisy Seed 运行在 48kHz 的音频下,这意味着每秒通过神经网络 48,000 次!
这种神经网络采样率在任何系统上都令人印象深刻,但我希望它能听起来更好。近年来的吉他手已经习惯于与真实效果难以区分的数字建模。像 NeuralDSP 的 Quad Cortex、IK Multimedia 的 ToneX 和最近的 Headrush Prime 等产品给吉他手提供了以前只有使用 Kemper 才能实现的选项。尽管我的工作专门针对 DIY 社区,但这并不意味着它的能力要逊色! (正如最近开源的 Neural Amp Modeler 所证明的那样。)
除了 LSTM 外,还有一种我尚未测试的递归神经网络(RNN),即 GRU(门控递归单元)。该网络在相同内部大小的情况下使用的处理能力约少 33%。我并没有进行过多测试,因为在 PC 上,额外的处理能力并不是一个大问题。
初步测试 GRU 是令人鼓舞的,我在 GRU 上得到的损失值甚至比相同大小的 LSTM 还要低。除了运行速度更快外,GRU 模型还消耗了更少的内存。我通过反复试验确定,大小为 10 的 GRU 大约是我在 Daisy seed 上可以通过 RTNeural 运行的最大值(相比之下,LSTM 为 8),这种差异在某些情况下将损失值降低了一半。我在失真效果器上的损失值低于 0.01,在低增益放大器上的损失值低于 0.02。
在当前版本中,我命名的 NeuralSeed 能够准确捕捉大多数失真/过驱动效果器(最多支持 3 个参数化旋钮)和低增益放大器。使用无参数化旋钮仍然是最准确的,但在我看来,增益和音调的控制灵活性是一个很酷的选项。我之前学习的技巧有助于提高整体音质。这些技巧包括对录音设备的改进,例如使用负载箱录音和使用重新放大箱对效果器进行录音。从一个预训练模型(迁移学习)开始,并使用专门的输入信号,有助于提高模型训练的准确性。通过使用一种叫做“蒸馏”(从大模型训练小模型)的方法,我能够捕捉到低到中等增益下管式放大器的复杂失真。
这里是 GitHub 上的开源代码。当加载到 Terrarium 效果器上时,它具有输入/输出电平旋钮、湿/干混音器、4 个 EQ 增益开关,以及最多 3 个旋钮用于控制一个参数化神经模型(例如增益、低音和高音)。左脚踏开关可以旁路或激活效果。右脚踏开关在可用的神经放大器/效果器模型之间循环切换。
[## GitHub - GuitarML/NeuralSeed: 神经网络用于 Daisy Seed 上的吉他放大器/效果器模拟
Neural Seed 利用神经网络在 Electro-Smith 的 Daisy Seed 硬件上模拟放大器/踏板,以及 Terrarium……
为 Daisy Seed 开发让我更加欣赏现代计算机的强大。M7 处理器(一个非常强大的微控制器!)的限制让我突破思维定式,用更少的资源实现更多的功能。NeuralSeed 代码是开源的,任何人都可以在 Daisy Seed / Terrarium 踏板上尝试、修改和进行自己的调整和改进。此外,还有一个 Colab 脚本 用于训练自己的模型以与 NeuralSeed 软件配合运行。这里有一个视频演示,展示了完成的 Neural Seed 踏板运行多个实际吉他放大器和踏板的神经网络模型。
Neural Seed 演示(作者视频)
特别感谢 ChowDSP 的 Jatin,他帮助我减少了 RTNeural 的足迹,并解决了 GRU 实现问题!也感谢 Daisy Discord 社区的朋友们,他们帮助我了解 Daisy Seed 和相关软件,以及 Neural Amp Modeler Facebook 群组提供的训练数据。
SQL 中的最小会议室问题
计算(在 SQL 中)安排一组会议所需的最小会议室数量
·
关注 发表在 Towards Data Science ·5 分钟阅读·2023 年 1 月 5 日
--
图片由 Dane Deaner 提供,来源于 Unsplash
会议室问题要求你确定安排所有会议所需的最少会议室数量,以便能够避免冲突。我们将探讨如何仅使用声明性 SQL 来解决这个问题。
上一篇文章: 使用 SQL 验证平衡括号
问题陈述
给定一组会议的开始和结束时间(均包括在内),确定安排所有会议所需的最少会议室数量,以避免任何冲突(即,会议室在会议进行时应始终可用,并且不能有两个会议在同一会议室内的时间重叠)。
在线讨论
这个问题在编程圈子里相当流行,你可以在这里找到相关讨论:
实际上,也有关于用 SQL 解决这个问题的讨论(这在我所覆盖的文章类型中通常较少见)。
输入表模式
输入表看起来像这样:
CREATE TABLE meetings(
idx SERIAL PRIMARY KEY,
start_at TIMESTAMP NOT NULL,
end_at TIMESTAMP NOT NULL
);
这些列的含义如下:
-
idx:此会议的索引(唯一 ID)
-
start_at:会议开始时的时间戳
-
end_at:会议结束时的时间戳(会议被视为包括此时间戳,因此在此时间戳开始的会议不能与该会议安排在同一房间)。
INSERT INTO meetings(start_at, end_at) VALUES
-- { 1, 18 }, { 18, 23 }, { 15, 29 }, {4, 15}, {2, 11}, {5, 13}
('01/01/2022', '01/18/2022'),
('01/18/2022', '01/23/2022'),
('01/15/2022', '01/29/2022'),
('01/04/2022', '01/15/2022'),
('01/02/2022', '01/11/2022'),
('01/05/2022', '01/13/2022');
SELECT * FROM meetings;

提供数据的输入表(作者提供的图片)
第一种解决方案:O(n²)
这个解决方案在上述在线文章中讨论,这些文章讲述了如何使用 SQL 解决这个问题。
主要思路是应该查看第一个和最后一个安排会议的时间戳之间的每一个时间点(例如每秒),并检查有多少会议与该时间点重叠。
可以进行的优化是,不必检查每个时间点,而只检查“有趣”的时间点——即会议开始或结束的时间点。这是因为在任何两个这样的“有趣”时间点之间,安排的会议数量不会发生变化。
这个解决方案的时间复杂度是O(n²),因为我们需要将每个会议O(n)与每个唯一的时间点O(n)(即会议的开始或结束时间点)匹配。
WITH unique_time_points AS (
SELECT ts
FROM (
SELECT start_at AS ts
FROM meetings
UNION
SELECT end_at AS ts
FROM meetings
) AS start_and_end_ts
),
joined AS (
SELECT
lhs.ts, COUNT(rhs.idx) AS num_overlaps
FROM unique_time_points lhs INNER JOIN meetings rhs
-- Join condition checks how many scheduled meetings intersect
-- the point "ts".
ON lhs.ts BETWEEN rhs.start_at AND rhs.end_at
GROUP BY lhs.ts
ORDER BY lhs.ts ASC
)
SELECT * FROM joined;

第一种解决方案的输出(作者提供的图片)
在这里,我们可以看到安排所有会议所需的最少会议室数量是 4。这发生在时间点 2022-01-05 和 2022-01-11 之间。
上述输出的图形表示。

第一种解决方案的输出图形视图(作者提供的图片)
预计成本: 对于一个有 6 行的表格,这个查询的预计成本是104k。
第二种解决方案:O(n log n)
该解决方案的主要思想是,一旦你有了所有会议开始和结束的唯一时间点,就可以简单地跟踪这些时间点上有多少个“进行中的”会议。
计数器: 当遇到开始时间点时,我们需要递增一个计数器(running_sum),而当遇到会议结束时间点时,我们需要递减这个计数器。
计数重叠: 对于在特定时间点开始和结束的会议,我们需要注意首先递增计数器,然后再递减,因为会议被认为完全占用其结束时间点。因此,我们有这样的代码:ORDER BY ts ASC, delta DESC — 即如果我们不按 delta 降序排序,那么可能会在开始时间点之前处理结束时间点,这会导致重叠会议数量的低估。
该解决方案的运行时复杂度是O(n log n),因为它由对所有唯一时间点按时间戳的非递减顺序排序的成本主导。
WITH unique_time_points AS (
SELECT start_at AS ts, +1 AS delta
FROM meetings
UNION
SELECT end_at AS ts, -1 AS delta
FROM meetings
),
with_running_sum AS (
SELECT
ts,
delta,
-- We need to process time points in "ts" order. Also in case
-- there are multiple time points, we need to first process a
-- meeting start and then a meeting end since a meeting end is
-- assumed to occupy its end timepoint. This ensures that we
-- don't under-count the number of simultaneously scheduled
-- meetings.
SUM(delta) OVER (ORDER BY ts ASC, delta DESC) AS running_sum
FROM unique_time_points
)
SELECT * FROM with_running_sum;

第二种解决方案的输出(作者提供的图片)
我们可以看到,安排所有会议而不发生冲突所需的最小会议室数量(4)在 2022 年 1 月 5 日至 2022 年 1 月 11 日之间(与上面的解决方案相同)。
上述输出的图形表示。

第二种解决方案的输出图形视图(作者提供的图片)
预计成本: 对于一个有 6 行的表格,这个查询的预计成本是474。
SQL Fiddle
本文中所有解决方案的 SQL Fiddle 链接可以在这里找到。
结论
这个问题与我们之前看到的使用 SQL 验证平衡括号有些相似,主要体现在使用 SQL 窗口函数的运行总和。
我们看到如何使用 SQL 窗口函数(再次)来利用一个简单的概念如运行总和来解决看似复杂的问题。
缺失数据解密:数据科学家的绝对入门指南
数据质量编年史
缺失数据、缺失机制以及缺失数据分析
·
关注 发布于 Towards Data Science · 12 min read · 2023 年 8 月 29 日
--
缺失数据使数据科学家无法看到数据要讲述的完整故事。有时,即使是最小的信息片段也能提供一个完全独特的世界观。图片由 Ronan Furuta 拍摄,来源于 Unsplash。
今年早些时候,我开始了一篇关于几个数据质量问题(或特征)的文章,这些问题严重影响我们的机器学习模型。
其中之一,毫无意外,是缺失数据。
我已经研究了这个话题很多年了(我知道,对吧?!),但在我参与的一些数据中心社区项目中,我意识到许多数据科学家仍未完全掌握这个问题的复杂性,这激励我创建了这个综合教程。
今天,我们将深入探讨 缺失数据的问题,发现我们可能在实际中遇到的不同缺失数据类型,并探索我们如何* 识别和标记缺失值 在现实世界的数据集中。
缺失数据的问题
缺失数据是一个有趣的数据不完善情况,因为它可能由于领域的自然性质而自然产生,或在数据收集、传输或处理过程中无意间产生。
本质上,缺失数据的特点是数据中出现了缺失值,即数据集中某些记录或观察中的缺失值,可以是单变量(一个特征有缺失值)或多变量(多个特征有缺失值):

单变量与多变量缺失数据模式。图像作者提供。
我们来考虑一个例子。 假设我们正在对一个糖尿病患者队列进行研究。
医疗数据就是一个很好的例子,因为它经常高度受到缺失值的影响: 病人值来自调查和实验室结果,可能在诊断或治疗过程中被测量多次,存储在不同格式中(有时分布在不同机构),并且通常由不同的人处理。这可能会(并且肯定会)变得很混乱!
在我们的糖尿病研究中,缺失值的存在可能与研究的进行或数据的收集有关。
例如,缺失数据可能是由于传感器故障导致高血压值关闭。另一个可能性是“体重”特征中的缺失值更可能出现在年长女性中,她们不太愿意透露这信息。或者肥胖患者可能更不愿意分享他们的体重。
另一方面,数据也可能因与研究完全无关的原因而缺失。
由于一个轮胎爆胎导致病人错过了医生预约,他的信息可能会缺失。数据也可能因人为错误而缺失:例如,进行分析的人员可能会错放或误读一些文件。
无论数据缺失的原因是什么,在建模之前调查数据集是否包含缺失数据都很重要,因为这个问题可能会对分类器产生 严重后果 :
-
一些分类器不能内部处理缺失值: 这使得它们在处理缺失数据的 数据集时不适用。在某些情况下,这些值被编码为预定义值,例如“0”,以便机器学习算法能够处理它们,尽管这不是最佳实践,尤其是在缺失数据的比例较高(或缺失机制更复杂)时;
-
基于缺失数据的预测可能会有偏差和不可靠: 尽管一些分类器可以内部处理缺失数据,但它们的预测可能会受到影响,因为训练数据中可能缺少重要的信息。
此外,尽管缺失值可能“看起来都一样”,但实际上它们的潜在机制(即它们缺失的原因)可以遵循 3 种主要模式:完全随机缺失 (MCAR)、随机缺失 (MAR) 和非随机缺失 (MNAR)。
牢记这些不同类型的缺失机制很重要,因为它们决定了处理缺失数据的适当方法选择,并且影响从中得出的推论的有效性。
让我们快速回顾一下每种机制!
缺失数据机制
如果你是一个数学爱好者,我建议你阅读这篇论文 (咳咳), 特别是第 II 和 III 节,其中包含了你可能寻找的所有符号和数学公式(我实际上是受到了这本书的启发,它也是一本非常有趣的入门书籍,请查看第 2.2.3 节和 2.2.4 节)。
如果你和我一样也是一个视觉学习者,你可能会想要“看到”它,对吧?
为此,我们将查看论文中使用的青少年烟草研究示例。我们将考虑虚拟数据来展示每种缺失机制:

缺失机制示例:一个模拟的数据集,关于青少年烟草使用研究,其中每天吸烟的平均数在不同机制(MCAR, MAR, 和 MNAR)下缺失。图像作者提供。
需要记住的一点是:缺失机制描述了缺失模式是否以及如何通过观察数据和/或缺失数据来解释。 这很棘手,我知道。但通过示例会更清楚!
在我们的烟草研究中,我们关注的是青少年烟草使用。共有 20 个观察值,相对于 20 名参与者,而特征Age是完全观察到的,而Number of Cigarettes(每天吸烟的数量)将根据不同的机制缺失。
完全随机缺失(MCAR):无害,无错!
在完全随机缺失(MCAR)机制中,缺失过程与观测数据和缺失数据完全无关。这意味着特征缺失值的概率是完全随机的。

MCAR 机制:(a)香烟数量的缺失值完全是随机的;(b)真实数据集中 MCAR 模式的示例。图片由作者提供。
在我们的例子中,我只是随机删除了一些值。注意缺失值没有集中在特定的Age或Number of Cigarettes值范围内。因此,这种机制可能由于研究过程中发生了意外事件:比如负责记录参与者回答的人不小心跳过了调查中的一个问题。
随机缺失(MAR):寻找明显的迹象!
这个名称实际上是误导性的,因为随机缺失(MAR)发生在缺失过程可以与数据中的观测信息相关联(尽管与缺失信息本身无关)。
考虑以下例子,其中我只删除了年轻参与者(15 至 16 岁)的Number of Cigarettes值。请注意,尽管缺失过程明显与Age中的观测值相关,但与这些青少年吸烟数量无关(请注意“Complete”列,如果报告了这些值,其中缺失值中会有低值和高值)。

MAR 机制:(a)香烟数量的缺失值与年龄相关;(b)真实数据集中 MAR 模式的示例:X_miss_1、X_miss_3 和 X_miss_p 的缺失值取决于 X_obs 的值。与最高/最暗值对应的值缺失。图片由作者提供。
例如,年轻孩子可能不愿意透露他们每天吸烟的数量,避免承认他们是常规吸烟者(无论他们吸烟的数量如何)。
缺失非随机(MNAR):那一刻的顿悟!
如预期所示,缺失非随机(MNAR)机制是最棘手的,因为缺失过程可能依赖于数据中的观测信息和缺失信息。这意味着特征中缺失值的发生概率可能与数据中其他特征的观测值相关,也可能与该特征本身的缺失值相关!
看下一个例子:Number of Cigarettes的较高值缺失,这意味着Number of Cigarettes缺失值的概率与缺失值本身相关(请注意“Complete”列)。

MNAR 机制:(a)香烟数量中的缺失值对应于如果观察到的最高值;(b)现实世界数据集中 MNAR 模式的例子:X_miss 中的值取决于自身的值(最高/较暗的值被删除)。图片由作者提供。
这将是那些由于吸烟量非常大的青少年拒绝报告他们每天吸烟数量的情况。
缺失数据机制的影响
根据我们的简单示例,我们已经看到MCAR 是缺失机制中最简单的一种。在这种情况下,我们可以忽略由于缺失值出现而产生的许多复杂性,一些简单的解决方法,比如列表删除或逐例删除,以及更简单的统计插补技术,可能就能解决问题。
然而,尽管方便,但现实中MCAR 往往是不现实的,大多数研究人员通常在研究中至少假设 MAR,这比 MCAR 更具普遍性和现实性。在这种情况下,我们可能会考虑比 MCAR 更强大的策略,这些策略可以从观察到的数据中推断缺失的信息。在这方面,基于机器学习的插补策略通常是最受欢迎的。
最后,MNAR 迄今为止是最复杂的情况,因为很难推断缺失的原因。当前的方法集中于使用领域专家定义的校正因子来映射缺失值的原因,从分布式系统中推断缺失数据,扩展最先进的模型(例如生成模型)以纳入多重插补,或进行敏感性分析以确定在不同情况下结果如何变化。
此外,在可识别性问题上,问题也不会变得更容易。
虽然有一些测试可以区分 MCAR 和 MAR,但它们并不广泛流行,并且有一些对复杂现实世界数据集不适用的限制性假设。也无法区分 MNAR 和 MAR,因为所需的信息缺失了。
要在实践中诊断和区分缺失机制,我们可以关注假设检验、敏感性分析、从领域专家那里获取一些见解,以及调查可以提供领域理解的可视化技术。
自然,还有其他复杂因素需要考虑,这些因素决定了缺失数据处理策略的应用,例如缺失数据的百分比、影响的特征数量以及技术的最终目标(例如,为分类或回归提供训练模型,还是以尽可能真实的方式重建原始值?)。
总的来说,这不是一项简单的工作。
识别和标记缺失数据
让我们一点一点地来。我们刚刚学到了关于缺失数据及其复杂纠缠的大量信息。
在这个示例中,我们将介绍如何标记和可视化真实数据集中的缺失数据,并确认缺失数据对数据科学项目造成的问题。
为此,我们将使用皮马印第安人糖尿病数据集,该数据集可以在 Kaggle 上找到(许可证 — CC0: 公共领域)。如果你想跟随教程,可以随时下载笔记本来自 Data-Centric AI Community GitHub 仓库。
为了快速分析数据,我们还将使用[ydata-profiling](https://github.com/ydataai/ydata-profiling),它可以通过几行代码给我们提供数据集的全面概览。让我们开始安装它吧:
安装最新版本的ydata-profiling。作者提供的代码片段。
现在,我们可以加载数据并快速创建概况:
加载数据并创建数据概况报告。作者提供的代码片段。
通过查看数据,我们可以确定这个数据集包含 768 条记录/行/观察(768 名患者),以及 9 个属性或特征。实际上,Outcome是目标类(1/0),所以我们有 8 个预测变量(8 个数值特征和 1 个分类特征)。

数据质量报告:总体数据特征。作者提供的图片。
初看之下,数据集似乎没有缺失数据。然而,这个数据集已知受到缺失数据的影响! 我们怎么确认这一点呢?
查看“警报”部分,我们可以看到几个“零值”警报,表明有几个特征的零值没有意义或在生物学上是不可能的: 例如,体重指数或血压的零值是无效的!
浏览所有特征,我们可以确定怀孕数似乎正常(零怀孕数是合理的),但对于其余特征,零值则值得怀疑:

数据质量报告:数据质量警报。作者提供的图片。
在大多数真实世界的数据集中,缺失数据通常由哨兵值编码:
-
超出范围的条目,例如
999; -
特征本应仅有正值时的负数,例如
-1; -
在一个本不应为 0 的特征中出现零值。
在我们的案例中,Glucose、BloodPressure、SkinThickness、Insulin和BMI都存在缺失数据。让我们统计这些特征中的零值数量:
统计零值的数量。作者提供的代码片段。
我们可以看到,Glucose、BloodPressure 和 BMI 只有少量的零值,而 SkinThickness 和 Insulin 有更多的零值,几乎覆盖了现有观察值的一半。这意味着我们可能需要考虑不同的策略来处理这些特征: 有些特征可能需要比其他特征更复杂的插补技术。
为了使我们的数据集与特定数据的惯例一致,我们应该将这些缺失值标记为 NaN 值。
这是在 Python 中处理缺失数据的标准方法,也是像 pandas 和 scikit-learn 等流行包遵循的惯例。这些值会在某些计算中被忽略,如 sum 或 count,并且一些函数会识别这些值以执行其他操作(例如,删除缺失值、插补它们、用固定值替换等)。
我们将使用 replace() 函数标记缺失值,然后调用 isnan() 来验证它们是否被正确编码:
将零值标记为 NaN 值。作者提供的代码片段。
NaN 值的数量与 0 值的数量相同,这意味着我们已经正确标记了缺失值!然后,我们可以使用配置文件报告来检查现在缺失数据是否被识别。以下是我们“新”数据的样子:

检查生成的警报:“缺失”警报现在已被突出显示。作者提供的图像。
我们可以进一步检查缺失过程的一些特征,浏览报告中的“缺失值”部分:

配置文件报告:调查缺失数据。作者录制的屏幕录像。
除了“计数”图表,它为我们提供了每个特征所有缺失值的概述,我们可以更详细地探索“矩阵”和“热图”图表,以对数据可能存在的潜在缺失机制进行假设。特别是,缺失特征之间的相关性可能会提供信息。在这种情况下,Insulin 和 SkinThickness 之间似乎存在显著的相关性:对于某些患者,这两个值似乎同时缺失。这是否是巧合(不太可能),或者缺失过程是否可以通过已知因素来解释,即表现出 MAR 或 MNAR 机制,这将是我们深入探讨的内容!
不管怎样,现在我们的数据已经准备好进行分析了!不幸的是,处理缺失数据的过程远未结束。 许多经典的机器学习算法无法处理缺失数据,我们需要找到专家的方法来缓解这个问题。让我们尝试在这个数据集上评估线性判别分析(LDA)算法:
评估带有缺失值的线性判别分析(LDA)算法。作者提供的代码片段。
如果你尝试运行这段代码,它会立即抛出一个错误:

LDA 算法无法内部处理缺失值,抛出错误信息。图片由作者提供。
解决这个问题的最简单方法(也是最天真的方法!)是删除所有包含缺失值的记录。我们可以通过创建一个新数据框,使用dropna()函数删除包含缺失值的行来实现这一点……
删除所有缺失值的行/观察值。代码片段由作者提供。
… 再次尝试:
评估没有缺失值的 LDA算法。代码片段由作者提供。

LDA 现在可以运行,尽管数据集的大小几乎减少了一半。图片由作者提供。
就这样! 通过删除缺失值,LDA 算法现在可以正常运行。
然而,数据集的大小被大幅缩减至仅 392 个观察值,这意味着我们失去了近一半的可用信息。
因此,我们应该寻找插补策略,而不是仅仅删除观察值,可以是 统计学的 或基于机器学习的。我们还可以使用合成数据来替换缺失值,具体取决于我们的最终应用。
为此,我们可能需要尝试了解数据中潜在的缺失机制。这将是未来文章中的一个期待点?
结语
在这篇文章中,我们已经覆盖了数据科学家在开始处理缺失数据时需要掌握的所有基础知识。
从问题本身到缺失机制,我们发现了缺失信息对我们的数据科学项目可能造成的影响,尽管所有的NaNs看起来可能相同,它们可能讲述非常不同的故事。
在接下来的文章中,我将逐一介绍每种缺失机制,重点关注生成、插补和可视化策略,所以请关注新的博客文章,并不要忘记给数据驱动的 AI 仓库加星标,以便随时获取代码更新!
一如既往,欢迎反馈、问题和建议!你可以给我留言,贡献代码到仓库,甚至可以在数据驱动的 AI 社区找到我,讨论其他与数据相关的话题。到时候见?
关于我
博士,机器学习研究员,教育者,数据倡导者,以及全能“万金油”。在 Medium 上,我写关于数据驱动的人工智能和数据质量,为数据科学与机器学习社区提供如何从不完美数据转向智能数据的教育。
以数据为中心的人工智能社区 | GitHub | 谷歌学术 | 领英
您的数据产品中的缺失特性
参与度、愉悦感和信任作为成果
·
关注 发表在 数据科学前沿 ·7 分钟阅读·2023 年 2 月 15 日
--
我在 Zendesk 领导一个每月的数据讨论小组,我很幸运能够听到来自聪明、多样化和才华横溢的人的各种想法和观点。我们讨论的主题范围从技术到程序性,并且我们定期将这些与我们的工作流联系起来。在线讨论小组、本地聚会和会议都非常棒,但我觉得将您组织中的人员聚在一起讨论行业趋势会有更多价值,因为这可以为您的团队及其项目提供方向。
我们一直在回到的一个主题是团队的价值以及如何衡量它。在早期的文章中,我讨论了数据团队如何创造价值,但这不是唯一的方法。无论你是在提供业务支持、通过反向 ETL 实现自动化,还是为高级分析和机器学习用例提供数据,总会存在一个挥之不去的问题:我们是否节省或创造了比花费更多的价值?
正如我们的一位与会者所说,我们的客户经常根据“氛围”来衡量我们的影响力。这不应被视为贬低,而是对围绕影响力和价值创建明确指标的困难的一种让步。此外,这个想法中还隐含了分析输出中存在主观且或许无法量化的价值,我们稍后会讨论这个问题。
为数据产品建立归因模型是一个臭名昭著的难题,我不会尝试在这里解决所有元素。虽然我相信我们可以从一个广泛的归因框架中受益,但鉴于数据降低成本和增加收入的方式众多,我不知道这是否甚至可能。相反,我想关注几个我认为被忽视的具体组件,特别是:
-
数据的心理社会价值
-
客户的愉悦和满意
-
数据质量作为核心交付物
我们是拉拉队员吗?
第一个观察点更不涉及我们应如何“做数据产品”,而是关于基本要求。在讨论中,我提出了一个观点:我们不一定知道数据驱动型公司成功的原因,但研究表明它们确实成功。有人回应道:也许这些仪表盘“没有做任何事情”,至少在推动特定、明确的行动方面没有发挥作用。如果它们的价值在于激励和对齐我们的客户,而不是(或除了)可操作的见解,那会怎样呢?

由Rojan Maharjan拍摄,来源于Unsplash
在我的职业生涯的某个阶段,我曾担任一家批发公司的 BI 开发人员,我们有一份报告向销售团队提供销售指标的汇总。这份报告在整个月的使用情况还算合理,但在月末使用量激增,一些用户每小时刷新好几次。我记得对这种趋势感到非常困惑。
我相信这种行为背后有几个操作性用例,但事后看来,我认为报告的最有价值的输出之一是激励。看到他们的货物几乎实时发出使我们的销售人员感到兴奋;它给了他们额外的“动力”,让他们在争分夺秒地完成或超越配额时进行电话和达成交易。这份报告正在做非常实际的工作,推动业务流程,即使这不是我们通常期望的方式。
还值得注意的是,许多这些数据文物本质上具有社交属性。一个仪表板是一个共享对象,可以作为同事之间对话的共同纽带。即使底层数据并不出色或特别有洞察力,由此引发的对话和共享叙事也可能是有价值的。对齐是组织的强大工具,而数据可以推动对齐。
作为“客观”的人,许多数据专业人员可能不愿意接受“灵感”是有效或有价值的输出;然而,如果我们不利用数据来激励自己,我们就会错过一些东西。我最初也抵触这个想法,但反思我的职业生涯,我有很多类似于上述的故事和轶事,我不禁觉得这种定性影响是显著的。也许我从这次经历中得到的最重要的教训是,仅仅因为我无法感知到生成价值的机制,并不意味着它就不存在。
最后,数据向我们展示一些隐藏的见解或反对意见的叙事现在已经成了一个陈词滥调,但当数据确认我们的信念并让我们安心我们走在正确的轨道上时,确实会发生一些强大的事情。数据常常支持现状而不是颠覆它,这应该被庆祝,而不是害怕。
面包与马戏团

图片由 Mathew Schwartz 提供,来源于 Unsplash
同样,我认为考虑我们的客户是否喜欢我们的数据产品是很重要的。这可能包括为分析师和数据科学家提供符合人体工程学的命名规范和易于使用的表格,也包括为业务用户提供美观的仪表板。
这是什么样的呢?我们是否应该在数据产品中构建反馈机制?我们是否应该进行调查?我会提醒我们不要过于强硬;与所有内部产品一样,我们有可能与实际价值链(即我们如何获得报酬)保持脆弱的关系。
不过,我认为在数据产品生命周期中提供轻量级系统以驱动反馈仍然有空间。哪些功能受到喜爱?哪些功能被讨厌?何时数据产品可以进入“维护模式”,何时我们应该结束?根据我的经验,这些问题几乎总是以临时的方式提出,到产品生命周期的那个阶段,甚至识别活跃用户也可能被已经被遗忘的过程自动化所复杂化。
质量作为可交付物
在我们结束每月会议时,团队中的数据工程经理之一,Niral Patel提出了一个中肯的问题:为什么质量总是被当作事后的思考,而它却是满足我们客户需求的关键因素?
在某种程度上,这涉及到资源问题。数据团队经常是“临时拼凑”的,要求承担远超其能力的工作。他们陷入了操作困境和不明确的需求中。在时间紧迫时,交付一个不完整的东西总比什么都不交付要好。
但就像标准化测试中因错误答案而受罚一样,数据质量差会破坏信任。如果你的团队持续产生不准确、过时且不一致的仪表板,你的客户将失去对你的信任。这种损害不仅限于你和你的团队;你还在助长一种需要多年努力和大量人力才能修复的不信任文化。
我们有责任向利益相关者推广数据质量作为我们产品的关键特性,并需要让他们参与权衡取舍。由于数据质量有许多维度,我们的客户必须参与优先级排序:我们应如何处理异常数据量?遇到模式变化时我们应该怎么做?准时交付不完整的数据与迟交完整的数据,哪个更重要?虽然“差”数据质量有一定的感觉,但在交付“好”数据时的权衡管理要复杂得多且具有上下文敏感性。
在数据治理领域正进行着重要的工作,现在是时候将这些应用到我们的数据产品中,从一开始就如此,并且每次都如此。质量是一个基本特性,与我们产品的设计、接口和用例同样重要。
我们所需的指标
虽然有产品和服务可以提供我们使用数据,但数据本身是不够的;我们需要指标和相关结果。我认为以下指标(及相关的监控/可观察性)是成功数据产品的关键组件:
-
一种理想的使用情况测量方法,能够区分自动化与临时使用
-
一种理想的特征级满意度测量方法
-
一种理想的数据质量测量方法,涵盖所有关键方面
这些组件远远不够,但我相信它们对于数据团队充分判断他们的工作是否产生了影响是必要的。是的,美元或 FTE 等价物归因是金标准,但在无法获得这些的情况下,知道我们向投入、满意的用户提供了高质量的数据是一个很好的替代指标。这些指标还允许我们捕捉数据产品的那些意外好处;也许我们的用户将输出应用于完全不同的过程。
Niral 在后续谈话中提出的一个重要观点是,数据产品应该具有可操作性;自动化或仪表板应推动某些产生价值的行动。区分创建数据产品的主要目标是心理社会影响,还是将心理社会影响视为一种价值维度是至关重要的。我不会建议在规划阶段仅以提高士气或团队凝聚力为唯一目标来制作产品,因为这种价值主张充其量也只是勉强成立。相反,如果我们的产品未能推动预期的行动但仍然非常受欢迎,我们应该有框架来调查原因。本质上,我们不希望仅仅因为它没有“按预期工作”就抛弃一些有价值的东西。
总结一位参与者的精彩见解:数据团队为我们利益相关者的工作场所福祉做出了贡献;如果提供数据提取或仪表板使用户感到愉快,那就是有用的。这肯定不是数据团队的唯一价值主张,但也不是我们应该忽视的东西。尽管倾向于依赖客观性并忽视我们工作中的人文元素(“数据可以‘克服’的‘弱点’”)可能很诱人,但现实是我们是社会性生物,工作在复杂的组织中。我们有工具来穿透主观性,但大多数工作会经过其他人的手;承认和认识到这个人文元素的价值大于忽视它。
我刚完成这篇文章的初稿几分钟后,Benn Stancil 发布了一篇关于他对放弃数据客观性的 discomfort的智能文章。如果我不把你引导到他的方向,我会感到遗憾。
Mistral 7B:在您的计算机上进行微调和量化的配方
便宜的监督微调与令人印象深刻的 LLM
·
关注 发布于 Towards Data Science · 9 分钟阅读 · 2023 年 10 月 26 日
--
mistral 是一种吹拂在北部地中海的风 —— 图片来自 Pixabay
Mistral 7B 是由 Mistral AI 创建的非常受欢迎的大型语言模型(LLM)。它超越了所有 其他相似规模的预训练 LLM,甚至优于更大的 LLM,如 Llama 2 13B。
它也非常优化了快速解码,特别是对于长上下文,这要归功于使用滑动窗口计算注意力和分组查询注意力(GQA)。你可以在介绍 Mistral 7B 的 arXiv 论文和Salvatore Raieli的这篇精彩文章中找到更多细节。
Mistral 7B 表现良好,但足够小,可以在经济实惠的硬件上利用。
在本文中,我将展示如何使用 QLoRA 微调 Mistral 7B。我们将使用我为本文修改的“ultrachat”数据集。Ultrachat 被 Hugging Face 用来创建Zephyr 7B。我们还将看看如何使用 AutoGPTQ 对 Mistral7B 进行量化。
我编写了实现所有部分的笔记本。你可以在这里找到它们:
使用 TRL 对 Mistral 7B 进行监督微调
Mistral 7B 是一个拥有 70 亿参数的模型。你大约需要 15 GB 的 VRAM 来加载它到 GPU 上。然后,使用批量进行全面微调将消耗更多的 VRAM。
标准全面微调的替代方案是使用 QLoRA 进行微调。QLoRA 在冻结的量化模型上微调 LoRA 适配器。在之前的文章中,我已经使用它来微调 Llama 2 7B。
由于 QLoRA 将量化到 4 位(NF4),我们大约将加载模型的内存消耗减少了 4 倍,即,使用 NF4 量化的 Mistral 7B 消耗约 4 GB 的 VRAM。如果你有一块 12 GB VRAM 的 GPU,这会留出大量空间来增加批量大小并使用 LoRA 针对更多模块。
换句话说,如果你的机器有足够的 VRAM,或者使用配备 T4 GPU 的 Google Colab 免费实例,你可以免费微调 Mistral 7B。
要使用 QLoRA 进行微调,你需要安装以下库:
pip install -q -U bitsandbytes
pip install -q -U transformers
pip install -q -U peft
pip install -q -U accelerate
pip install -q -U datasets
pip install -q -U trl
然后,导入以下内容:
import torch
from datasets import load_dataset
from peft import LoraConfig, PeftModel, prepare_model_for_kbit_training
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
AutoTokenizer,
TrainingArguments,
)
from trl import SFTTrainer
接下来,我们加载并配置标记器。与 Llama 2 一样,我们需要定义一个填充标记。像往常一样,我选择了 UNK 标记来填充训练示例。
注意:Mistral 7B 完全开放。你无需连接到 Hugging Face 或在下载模型和标记器之前签署许可协议。
model_name = "mistralai/Mistral-7B-v0.1"
#Tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True)
tokenizer.pad_token = tokenizer.unk_token
tokenizer.pad_token_id = tokenizer.unk_token_id
tokenizer.padding_side = 'left'
对于微调,我制作了一个自定义版本的ultrachat(MIT 许可证)。Ultrachat 包含 774k 个 JSON 格式的对话。这对便宜的微调来说太多了,而且该格式对于在消费级硬件上使用 TRL 微调并不理想。注意: TRL 是 Hugging Face 开发的一个库,它简化了对指令 LLMs 的微调。
我随机从 ultrachat 中抽取了 100k 对话,然后将对话展开。例如,在原始数据集中,一个示例如下:
[ "What percentage of the Earth's surface is covered by oceans?", "About 71% of the Earth's surface is covered by oceans.", "Wow, that's a lot of water! No wonder we call it the blue planet.", "Yes, it certainly is! The oceans play a vital role in regulating the Earth's climate and supporting life on our planet. And they're also a great source of food, energy, and recreation for us humans!", "Absolutely! I love visiting the beach and going for a swim in the ocean. It's amazing how vast and powerful the sea can be, yet also so peaceful and calming.", "As an AI language model, I have never gone to the beach or swam in the ocean, but based on what you said, I am sure it's a wonderful experience. The ocean is a place of great beauty and mystery, full of fascinating creatures and hidden treasures waiting to be discovered. It can be both a source of wonder and a reminder of the awesome power of nature." ]
一旦展开,我们将所有内容放入一个单一的 token 序列中,并标记为“### Human”和“### Assistant”:
### Human: What percentage of the Earth's surface is covered by oceans?### Assistant: About 71% of the Earth's surface is covered by oceans###Human: Wow, that's a lot of water! No wonder we call it the blue planet### Assistant: Yes, it certainly is! The oceans play a vital role in regulating the Earth's climate and supporting life on our planet. And they're also a great source of food, energy, and recreation for us humans!### Human: Absolutely! I love visiting the beach and going for a swim in the ocean. It's amazing how vast and powerful the sea can be, yet also so peaceful and calming### Assistant: As an AI language model, I have never gone to the beach or swam in the ocean, but based on what you said, I am sure it's a wonderful experience. The ocean is a place of great beauty and mystery, full of fascinating creatures and hidden treasures waiting to be discovered. It can be both a source of wonder and a reminder of the awesome power of nature
这与 timdettmers/openassistant-guanaco 的格式相同。这个格式可以直接被 TRL 使用。你可以在这里找到这个版本的 ultrachat:
要加载数据集,我们执行:
dataset = load_dataset("kaitchup/ultrachat-100k-flattened")
现在,让我们加载模型并为 QLoRA 做准备:
#Quantization configuration
compute_dtype = getattr(torch, "float16")
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=True,
)
#Load the model and quantize it on the fly
model = AutoModelForCausalLM.from_pretrained(
model_name, quantization_config=bnb_config, device_map={"": 0}
)
#Cast some modules of the model to fp32
model = prepare_model_for_kbit_training(model)
#Configure the pad token in the model
model.config.pad_token_id = tokenizer.pad_token_id
model.config.use_cache = False # Gradient checkpointing is used by default but not compatible with caching
如果你的 GPU 支持,请将“float16”替换为“bfloat16”。这将使训练更加稳定。
接下来,我们定义 LoRA 的配置。
peft_config = LoraConfig(
lora_alpha=16,
lora_dropout=0.05,
r=16,
bias="none",
task_type="CAUSAL_LM",
target_modules= ['k_proj', 'q_proj', 'v_proj', 'o_proj', "gate_proj", "down_proj", "up_proj"]
)
我通常将 r=lora_alpha 设为 16,因为我阅读了 Platypus 论文。
[## Platypus: 数据集策划与适配器,用于更好的大型语言模型]
在你的目标任务上实现低成本的最先进技术
kaitchup.substack.com](https://kaitchup.substack.com/p/platypus-dataset-curation-and-adapters?source=post_page-----631401583f77--------------------------------)
我瞄准了几个模块。这里,我选择了所有“proj”模块,但还有更多。你可以通过运行“print(model)”找到它们。请注意,添加更多模块可能会提升性能,但也会增加可训练参数,从而消耗更多 VRAM。
对于训练,我选择了以下超参数:
training_arguments = TrainingArguments(
output_dir="./results",
#evaluation_strategy="steps",
#do_eval=True,
optim="paged_adamw_8bit",
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
per_device_eval_batch_size=4,
log_level="debug",
save_steps=20,
logging_steps=10,
learning_rate=4e-4,
#eval_steps=200,
#num_train_epochs=1,
max_steps=100,
warmup_steps=100,
lr_scheduler_type="linear",
)
我仅训练了 100 步。由于样本非常长,使用 T4 GPU 训练一个 epoch 需要超过 200 小时。如果你使用 V100 或 RTX 40xx,你可以将时间减少到 100 小时。
我推荐至少进行 2,000 次训练步骤,以获得一个相当好的模型。使用 T4 训练大约需要两天时间,使用更新的 GPU 只需一天。
我还用“#”注释了与评估相关的超参数,因为评估成本较高。取消注释以记录验证损失。
开始训练:
trainer = SFTTrainer(
model=model,
train_dataset=dataset['train'],
#eval_dataset=dataset['test'],
peft_config=peft_config,
dataset_text_field="text",
max_seq_length=512,
tokenizer=tokenizer,
args=training_arguments,
)
trainer.train()
然后,一旦训练完成,通过运行以下命令测试训练后的适配器:
from transformers import GenerationConfig
model.config.use_cache = True
model = PeftModel.from_pretrained(model, "./results/checkpoint-100/")
def generate(instruction):
prompt = "### Human: "+instruction+"### Assistant: "
inputs = tokenizer(prompt, return_tensors="pt")
input_ids = inputs["input_ids"].cuda()
generation_output = model.generate(
input_ids=input_ids,
generation_config=GenerationConfig(pad_token_id=tokenizer.pad_token_id, temperature=1.0, top_p=1.0, top_k=50, num_beams=1),
return_dict_in_generate=True,
output_scores=True,
max_new_tokens=256
)
for seq in generation_output.sequences:
output = tokenizer.decode(seq)
print(output.split("### Assistant: ")[1].strip())
generate("Tell me about gravitation.")
所有这些代码都实现于 这个 notebook #22。
使用 AutoGPTQ 和 bitsandbytes NF4 对 Mistral 7B 进行量化
使用 bitsandbytes NF4
为了保持内存消耗低,我们希望运行量化模型。
如果你已经用 QLoRA 微调了自己的模型并希望对其进行量化,你最好的选择是加载并量化 Mistral 7B,使用 bitsandbytes nf4,像我们对 QLoRA 所做的那样。然后,将微调的适配器加载到其上。这是我们在上一节的最后一个代码示例中所做的:
model = PeftModel.from_pretrained(model, "./results/checkpoint-100/")
我探索了几个其他选项,例如将适配器合并到基础模型中,但没有一个是最优的。
[## LoRA Adapters: When a Naive Merge Leads to Poor Performance
LoRA 适配器微调的情况,使用 QLoRA
kaitchup.substack.com](https://kaitchup.substack.com/p/lora-adapters-when-a-naive-merge?source=post_page-----631401583f77--------------------------------)
我们本可以使用QA-LoRA 对量化感知的 LoRA 进行微调,但该框架目前尚不支持 Mistral 7B。
Mistral AI 还发布了 Mistral 7B 的指令版。你可以在这里获取,而不是训练自己的指令 Mistral 7B:
我使用这个模型进行以下量化示例。
要用 bitsandbytes 加载并量化模型,你首先需要安装以下库:
pip install -q -U bitsandbytes
pip install -q -U transformers
pip install -q -U accelerate
然后,运行:
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
AutoTokenizer,
)
model_name = "mistralai/Mistral-7B-Instruct-v0.1"
#Tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True)
tokenizer.pad_token = tokenizer.unk_token
tokenizer.pad_token_id = tokenizer.unk_token_id
tokenizer.padding_side = 'left'
#Quantization configuration
compute_dtype = getattr(torch, "float16")
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=True,
)
#Load the model and quantize it on the fly
model = AutoModelForCausalLM.from_pretrained(
model_name, quantization_config=bnb_config, device_map={"": 0}
)
对于生成测试,你可以使用我在前面部分定义的generate函数,配合 Mistral Instruct 使用的正确提示格式:
from transformers import GenerationConfig
def generate(instruction):
prompt = "[INST] "+instruction+" [/INST]\n"
inputs = tokenizer(prompt, return_tensors="pt")
input_ids = inputs["input_ids"].cuda()
generation_output = model.generate(
input_ids=input_ids,
generation_config=GenerationConfig(pad_token_id=tokenizer.pad_token_id, temperature=1.0, top_p=1.0, top_k=50, num_beams=1),
return_dict_in_generate=True,
output_scores=True,
max_new_tokens=256
)
for seq in generation_output.sequences:
output = tokenizer.decode(seq)
print(output.split("[/INST]")[1].strip())
generate("Tell me about gravity.")
使用 AutoGPTQ
bitsandbytes nf4 量化速度快,但推理速度慢。目前,我们无法序列化 nf4 模型。
AutoGPTQ 的量化速度较慢,但你只需做一次。它也比nf4 解码速度快,但请注意,用AutoGPTQ (INT4) 量化的模型略逊色于用 nf4 量化的模型。
我们需要以下库:
pip install -q -U bitsandbytes
pip install -q -U transformers
pip install -q -U accelerate
pip install -q -U optimum
pip install -q -U auto-gptq
然后,导入以下包:
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
GPTQConfig,
AutoTokenizer
)
然后,当我们用“AutoModelForCausalLM.from_pretrained”加载模型时,它会被量化。
model_name = "mistralai/Mistral-7B-Instruct-v0.1"
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True)
tokenizer.pad_token = tokenizer.unk_token
tokenizer.pad_token_id = tokenizer.unk_token_id
tokenizer.padding_side = 'left'
quantization_config = GPTQConfig(bits=4, dataset = "c4", tokenizer=tokenizer)
model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto", quantization_config=quantization_config)
量化非常消耗内存。你需要超过 24 GB 的 VRAM,例如,Google Colab Pro 的 A100 会有效。我建议在加载模型后保存它,以确保你不会再需要重复操作。
所有这些量化代码也可以在我的笔记本 #23中找到。
请注意,TheBloke 在 Hugging Face Hub 上提议了使用 AutoGPTQ 量化的 Mistral Instruct 7B:
结论
Mistral 7B 是一个令人印象深刻的预训练 LLM。你可以很容易地在电脑上用 QLoRA 进行微调。然而,微调依然非常耗时。正如我们所见,你可能需要运行几百小时或更长时间。
如果你不需要在你的数据上微调 Mistral 7B,Mistral AI 提出的 Mistral Instruct 是一个不错的替代方案。要在消费级硬件上运行它,你可以使用 bitsandbytes nf4 或 GPTQ 进行量化。
支持我的工作,请考虑订阅我的新闻通讯:
[## Kaitchup - AI on a Budget | Benjamin Marie, PhD | Substack
每周提供关于在你的计算机上微调、运行和服务大型语言模型的新闻、技巧和教程。每一个……
高基数分类变量的混合效应机器学习 — 第 I 部分:不同方法的实证比较
为什么随机效应对机器学习模型有用
·
关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 7 月 7 日
--
图 1:二元数据和具有不同基数的分类变量中树增强方法(“GPBoost”与“LogitBoost”)的对比。基数越高(即每个水平的样本数越少),随机效应模型的表现越好(即测试误差的减少) — 图片由作者提供
高基数分类变量是指不同级别的数量相对于数据集的样本大小很大,换句话说,每个分类变量级别的数据点很少。机器学习方法可能在处理高基数变量时遇到困难。在本文中,我们认为随机效应是机器学习模型中建模高基数分类变量的有效工具。特别是,我们通过对比在多个具有高基数分类变量的表格数据集上,两种最成功的机器学习方法——树提升和深度神经网络——以及线性混合效应模型的不同版本。我们的结果表明,首先,具有随机效应的机器学习模型表现优于没有随机效应的模型,其次,具有随机效应的树提升优于具有随机效应的深度神经网络。
目录
· 1 介绍
· 2 高基数分类变量的随机效应建模
· 3 使用真实数据集的不同方法比较
· 4 结论
· 参考文献
1 介绍
处理分类变量的一种简单策略是使用独热编码或虚拟变量。但由于下面描述的原因,这种方法对于高基数分类变量通常效果不好。对于神经网络,一个常用的解决方案是实体嵌入 [Guo and Berkhahn, 2016],它将分类变量的每个级别映射到低维欧几里得空间。对于树提升,一种简单的方法是给分类变量的每个级别分配一个数字,然后将其视为一维数值变量。在LightGBM提升库 [Ke et al., 2017] 中实现的替代方案通过使用近似方法 [Fisher, 1958] 将所有级别分成两个子集来进行树构建算法中的分裂。进一步地,CatBoost提升库 [Prokhorenkova et al., 2018] 实现了一种基于有序目标统计的方法,该方法通过使用训练数据的随机划分来处理分类预测变量。
2 高基数分类变量的随机效应建模
随机效应可以作为建模高基数分类变量的有效工具。在具有单一高基数分类变量的回归情况下,随机效应模型可以表示为

其中 j=1,…,ni 是第 i 层内的样本索引,其中 ni 表示分类变量达到第 i 层的样本数量,而 i 表示层级,q 为分类变量的总层数。因此,总样本数为 n = n0 + n1 + … + nq。这样的模型也被称为混合效应模型,因为它包含了固定效应 F(xij) 和随机效应 bi。xij 是固定效应预测变量或特征。混合效应模型可以扩展到其他响应变量分布(例如,分类)和多个分类变量。
传统上,随机效应被用于线性模型中,其中假设 F 是一个线性函数。在过去几年中,线性混合效应模型已经扩展到使用随机森林 [Hajjem et al., 2014]、树提升 [Sigrist, 2022, 2023a],以及最近(从首次公开预印本的角度来看)深度神经网络 [Simchoni and Rosset, 2021, 2023]。与经典的独立机器学习模型相比,随机效应引入了样本之间的依赖性。
为什么随机效应对高基数分类变量有用?
对于高基数分类变量,每个层级的数据很少。从直观上讲,如果响应变量在许多层级上具有不同的(条件)均值,传统的机器学习模型(例如,独热编码、嵌入或简单的一维数值变量)可能会在此类数据上出现过拟合或欠拟合问题。从经典的偏差-方差权衡的角度来看,独立的机器学习模型可能难以平衡这种权衡并找到适当的正则化量。例如,可能会发生过拟合,这意味着模型具有低偏差但高方差。
从广义上讲,随机效应作为先验或正则化器,建模函数的难点部分,即其“维度”类似于总样本大小,并且通过这种方式提供了一种在过拟合与欠拟合或偏差与方差之间找到平衡的有效方法。例如,对于单一的分类变量,随机效应模型将会将组拦截效应的估计值收缩向全局均值。这一过程有时也称为“信息汇聚”。它代表了完全忽略分类变量(= 欠拟合 / 高偏差和低方差)与在估计中给予分类变量的每个级别“完全自由”(= 过拟合 / 低偏差和高方差)之间的权衡。重要的是,正则化的程度,由模型的方差参数决定,是从数据中学习得到的。具体来说,在上述单级随机效应模型中,对于具有预测变量 xp 和分类变量级别 i 的样本,响应变量的(点)预测值由下式给出

其中 F(xp)是评估在 xp 处的训练函数,σ²_1 和σ²是方差估计值,而 yi 和 Fi 分别是级别 i 下 yij 和 F(xij)的样本均值。忽略分类变量会得到预测值 yp = F(xp),而一个完全灵活的无正则化模型给出的预测值是 yp = F(xp) + (yi — Fi)。即,这两种极端情况与随机效应模型之间的差异是收缩因子σ²_1 / (σ²/ni + σ²_1)(如果级别 i 的样本数量 ni 很大,这个因子趋近于零)。与此相关,随机效应模型允许更高效(即更低方差)的固定效应函数 F(.)的估计[Sigrist, 2022]。
根据这一论点,Sigrist [2023a,第 4.1 节]在实证实验中发现,树提升与随机效应结合(“GPBoost”)的表现优于经典的独立树提升(“LogitBoost”),尤其是在分类变量的每个级别样本数量较少,即分类变量的基数较高时。上述结果在图 1 中进行了再现。这些结果是通过模拟具有 5000 个样本的二分类数据、一个非线性预测函数和一个逐步增加级别的分类变量(即每个级别样本更少)获得的;有关更多细节,请参见 Sigrist [2023a]。结果表明,GPBoost 和 LogitBoost 的测试误差差异越大,当每个级别的样本数量越少(即级别数量越多)时。
3 使用真实数据集的不同方法比较
接下来,我们使用多个具有高卡方类别变量的真实数据集比较几种方法。我们使用 Simchoni 和 Rosset [2021, 2023]提供的所有公开表格数据集,以及与 Simchoni 和 Rosset [2021, 2023]相同的实验设置。此外,我们还包括 Sigrist [2022]分析的 Wages 数据集。
我们考虑以下方法:
-
`Linear’:线性混合效应模型
-
`NN Embed’:具有嵌入的深度神经网络
-
`LMMNN’:结合深度神经网络和随机效应 [Simchoni 和 Rosset, 2021, 2023]
-
`LGBM_Num’:通过为每个类别变量的每个水平分配一个数字,并将这些数字视为一维数值变量的树提升
-
LGBM_Cat’:使用LightGBM` [Ke et al., 2017]的方法进行树提升 -
CatBoost’:使用CatBoost` [Prokhorenkova et al., 2018]的方法进行树提升 -
`GPBoost’:结合树提升和随机效应 [Sigrist, 2022, 2023a]
请注意,最近(版本 1.6 及以后),XGBoost库 [Chen 和 Guestrin, 2016] 也实现了与LightGBM相同的处理类别变量的方法。我们在这里不将其视为单独的方法。
我们使用以下数据集:

表 1:数据集总结。n 是样本数量,p 是预测变量的数量(不包括高卡方类别变量),K 是高卡方类别变量的数量,‘Cat. var.’描述类别变量 — 作者提供的图像
对于所有随机效应方法,我们包括表 1 中提到的每个类别变量的随机效应,随机效应之间没有先验相关性。Rossmann、AUImport 和 Wages 数据集是纵向数据集。对于这些数据集,我们还包括线性和二次随机斜率;请参见本系列的第三部分。有关数据集的更多详细信息,请参见 Simchoni 和 Rosset [2021, 2023]以及 Sigrist [2023b]。
我们对每个数据集进行 5 折交叉验证(CV),使用测试均方误差(MSE)来衡量预测准确性。有关实验设置的详细信息,请参见 Sigrist [2023b]。数据预处理的代码以及下载数据和运行实验的代码可以在这里找到。对于许可证允许的原始数据源,建模所用的预处理数据也可以在上述网页找到。

图 2:相对最低测试 MSE 的平均差异。由于不是所有方法都在 Wages 数据集上运行,因此该数据集未用于计算 — 作者提供的图像
结果汇总在图 2 中,该图显示了与最低测试均方误差(MSE)的平均相对差异。这是通过首先计算每个数据集的测试 MSE 与最低 MSE 的相对差异,然后对所有数据集进行平均得到的。详细结果可以在 Sigrist [2023b] 中找到。我们观察到,结合树提升和随机效应(GPBoost)具有最高的预测准确性,与最佳结果的平均相对差异约为 7%。第二好的结果是由LightGBM(LGMB_Cat)的分类变量方法和具有随机效应的神经网络(LMMNN)获得,它们的平均相对差异约为 17%。CatBoost 和线性混合效应模型的表现则明显较差,与最佳方法的平均相对差异接近 50%。鉴于 CatBoost 特别设计用来处理分类特征,这令人有些失望。总体上,表现最差的是具有嵌入的神经网络,其与最佳结果的平均相对差异超过 150%。将分类变量转换为一维数值变量的树提升(LGBM_Num)的表现略好一些,与最佳结果的平均相对差异约为 100%。在其在线文档中,LightGBM 推荐 “对于高基数的分类特征,通常将特征视为数值特征效果最佳”(截至 2023 年 7 月 6 日)。我们显然得出了不同的结论。
4 结论
我们在具有高基数分类变量的表格数据上对几种方法进行了实证比较。我们的结果显示,首先,具有随机效应的机器学习模型的表现优于没有随机效应的模型;其次,具有随机效应的树提升优于具有随机效应的深度神经网络。虽然可能有几种原因可以解释这一发现,但这与 Grinsztajn 等人 [2022] 的最新工作一致,他们发现树提升在没有高基数分类变量的表格数据上优于深度神经网络(以及随机森林)。类似地,Shwartz-Ziv 和 Armon [2022] 也得出结论,树提升“在表格数据上优于深度模型。”
在 Part II 中,我们将展示如何使用 [GPBoost](https://github.com/fabsig/GPBoost) 库 进行演示,使用上述提到的真实数据集之一。在 Part III 中,我们将展示如何使用 GPBoost 库来建模纵向数据,即面板数据。
参考文献
-
T. Chen 和 C. Guestrin. XGBoost:一个可扩展的树提升系统。发表于第 22 届 ACM SIGKDD 国际知识发现与数据挖掘会议,页码 785–794。ACM,2016 年。
-
W. D. Fisher. 论最大同质性分组。美国统计协会期刊,53(284):789–798,1958 年。
-
L. Grinsztajn, E. Oyallon 和 G. Varoquaux. 为什么基于树的模型在典型的表格数据上仍然优于深度学习?在 S. Koyejo, S. Mohamed, A. Agarwal, D. Belgrave, K. Cho 和 A. Oh 编辑的《神经信息处理系统进展》卷 35,页码 507–520。Curran Associates, Inc., 2022 年。
-
C. Guo 和 F. Berkhahn. 分类变量的实体嵌入。arXiv 预印本 arXiv:1604.06737,2016 年。
-
A. Hajjem, F. Bellavance 和 D. Larocque. 用于聚类数据的混合效应随机森林。统计计算与模拟期刊,84(6):1313–1328,2014 年。
-
G. Ke, Q. Meng, T. Finley, T. Wang, W. Chen, W. Ma, Q. Ye 和 T.-Y. Liu. LightGBM:一种高效的梯度提升决策树。发表于神经信息处理系统进展,页码 3149–3157,2017 年。
-
L. Prokhorenkova, G. Gusev, A. Vorobev, A. V. Dorogush 和 A. Gulin. CatBoost:具有分类特征的无偏提升。发表于神经信息处理系统进展,页码 6638–6648,2018 年。
-
R. Shwartz-Ziv 和 A. Armon. 表格数据:深度学习并不是你所需要的一切。信息融合,81:84–90,2022 年。
-
F. Sigrist. 高斯过程提升。机器学习研究期刊,23(1):10565–10610,2022 年。
-
F. Sigrist. 潜在高斯模型提升。IEEE 模式分析与机器智能学报,45(2):1894–1905,2023a 年。
-
F. Sigrist. 针对高维分类变量的数据的机器学习方法比较。arXiv 预印本 arXiv::2307.02071 2023b。
-
G. Simchoni 和 S. Rosset. 使用随机效应来考虑深度神经网络中的高维分类特征和重复测量。神经信息处理系统进展,34:25111–25122,2021 年。
-
G. Simchoni 和 S. Rosset. 深度神经网络中的随机效应整合。机器学习研究期刊,24(156):1–57,2023 年。
-
github.com/fabsig/Compare_ML_HighCardinality_Categorical_Variables
高基数分类变量的混合效应机器学习 — 第二部分:GPBoost 库
一个使用真实数据的 GPBoost 在 Python 和 R 中的演示
·
关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 7 月 11 日
--
高基数分类数据的示意图:不同分类变量水平的响应变量的箱线图和原始数据(红点)— 图片作者提供
高卡方分类变量是指相对于数据集样本大小,具有大量不同层级的变量。在 Part I 中,我们对不同的机器学习方法进行了实证比较,发现随机效应是处理高卡方分类变量的有效工具,其中 GPBoost 算法 [Sigrist, 2022, 2023] 具有最高的预测准确性。在本文中,我们演示了如何将结合了树提升与随机效应的 GPBoost 算法应用于 Python 和 R 包的 [GPBoost](https://github.com/fabsig/GPBoost) 库。本演示中使用的 GPBoost 版本为 1.2.1。
目录
∘ 1 介绍
∘ 2 数据:描述、加载和样本分割
∘ 3 训练 GPBoost 模型
∘ 4 选择调优参数
∘ 5 预测
∘ 6 解释
∘ 7 进一步建模选项
· · 7.1 类别变量与其他预测变量的交互
· · 7.2 (广义)线性混合效应模型
∘ 8 结论和参考文献
1 介绍
应用 GPBoost 模型包括以下主要步骤:
-
定义一个
GPModel,在其中指定以下内容:— 随机效应模型:通过
group_data和/或通过gp_coords的高斯过程—
likelihood(= 固定和随机效应条件下的响应变量分布) -
创建一个
Dataset,包含响应变量(label)和固定效应预测变量(data) -
选择调优参数,例如,使用函数
gpb.grid.search.tune.parameters -
训练模型
-
进行预测和/或解释训练好的模型
在下文中,我们将逐步讲解这些点。
2 数据:描述、加载和样本分割
本演示中使用的数据是工资数据,该数据也在 Sigrist (2022) 中进行了分析。可以从 这里 下载。数据包含 28’013 个样本和一个高卡方分类变量(person ID = idcode),具有 4’711 个不同的层级。响应变量是对数实际工资(ln_wage),数据还包括几个预测变量,如年龄、总工作经验、工作年限、日期等。
在下面的代码中,我们首先加载数据,并将数据随机分为 80% 的训练数据和 20% 的测试数据。后者被留作后用,用于测量预测准确性。
Python
import gpboost as gpb
import pandas as pd
import numpy as np
# Load data
data = pd.read_csv("https://raw.githubusercontent.com/fabsig/Compare_ML_HighCardinality_Categorical_Variables/master/data/wages.csv.gz")
# Partition into training and test data
n = data.shape[0]
np.random.seed(n)
permute_aux = np.random.permutation(n)
train_idx = permute_aux[0:int(0.8 * n)]
test_idx = permute_aux[int(0.8 * n):n]
data_train = data.iloc[train_idx]
data_test = data.iloc[test_idx]
# Define fixed effects predictor variables
pred_vars = [col for col in data.columns if col not in ['ln_wage', 'idcode', 't']]
R
library(gpboost)
library(tidyverse)
# Load data
data <- read_csv("https://raw.githubusercontent.com/fabsig/Compare_ML_HighCardinality_Categorical_Variables/master/data/wages.csv.gz")
data <- as.matrix(data) # Convert to matrix since the boosting part does not support data.frames
# Partition into training and test data
n <- dim(data)[1]
set.seed(n)
permute_aux <- sample.int(n, n)
train_idx <- permute_aux[1:as.integer(0.8*n)]
test_idx <- permute_aux[(as.integer(0.8*n)+1):n]
data_train <- data[train_idx,]
data_test <- data[test_idx,]
# Define fixed effects predictor variables
pred_vars <- colnames(data)[-which(colnames(data) %in% c("ln_wage", "idcode", "t"))]
3 训练 GPBoost 模型
下面的代码展示了如何训练一个 GPBoost 模型。我们首先定义一个随机效应模型(gp_model)、一个Dataset(data_bst)、调整参数(params和nrounds = 树的数量/提升迭代次数),然后通过调用gpb.train函数运行 GPBoost 算法。请注意,我们使用了事先选择的调整参数(见下文)。
Python
gp_model = gpb.GPModel(group_data=data_train['idcode'], likelihood='gaussian')
data_bst = gpb.Dataset(data=data_train[pred_vars], label=data_train['ln_wage'])
params = {'learning_rate': 0.01, 'max_depth': 2, 'min_data_in_leaf': 10,
'lambda_l2': 10, 'num_leaves': 2**10, 'verbose': 0}
nrounds = 391
gpbst = gpb.train(params=params, train_set=data_bst, gp_model=gp_model,
num_boost_round=nrounds)
R
gp_model <- GPModel(group_data = data_train[,"idcode"], likelihood = "gaussian")
data_bst <- gpb.Dataset(data = data_train[,pred_vars], label = data_train[,"ln_wage"])
params <- list(learning_rate = 0.01, max_depth = 2, num_leaves = 2¹⁰,
min_data_in_leaf = 10, lambda_l2 = 10)
nrounds <- 391
gpbst <- gpb.train(data = data_bst, gp_model = gp_model,
nrounds = nrounds, params = params, verbose = 0)
对于这个数据,只有一个高基数的分类变量。如果有多个分类变量需要使用随机效应建模,这可以在gp_model中指定,如以下代码所示。注意:当有多个随机效应时,计算可能会比只有一个随机效应时更慢。此外,使用*nelder_mead* 代替*gradient_descent*(=默认值)作为优化器在有多个随机效应时可能会更快。这可以在调用*gpb.train*之前进行设置。
Python
gp_model = gpb.GPModel(group_data=data_train[['idcode','t']], likelihood='gaussian')
gp_model.set_optim_params(params={"optimizer_cov": "nelder_mead"})
R
gp_model <- GPModel(group_data = data_train[,c("idcode","t")], likelihood = "gaussian")
gp_model$set_optim_params(params = list(optimizer_cov = "nelder_mead"))
4 选择调整参数
调整参数的选择对于提升效果至关重要。没有通用的默认值,每个数据集可能需要不同的调整参数。以下展示了如何使用gpb.grid.search.tune.parameters函数来选择调整参数。我们使用了一个确定性的网格搜索,并在代码中展示了参数组合。请注意,我们调整了max_depth参数,因此将num_leaves设置为一个大值。我们随机将训练数据拆分为 80%的内训练数据和 20%的验证数据,并使用均方误差(mse)作为验证数据上的预测准确度衡量标准。或者,也可以使用,例如,测试负对数似然(test_neg_log_likelihood = 默认值),它还考虑了预测的不确定性。注意:根据数据集和网格大小,这可能需要一些时间(在我的笔记本电脑上,对于这个数据集需要几分钟)。除了下面的确定性网格搜索外,也可以进行随机网格搜索(见 *num_try_random*)或其他方法如贝叶斯优化来加快速度。
Python
# Partition training data into inner training data and validation data
ntrain = data_train.shape[0]
np.random.seed(ntrain)
permute_aux = np.random.permutation(ntrain)
train_tune_idx = permute_aux[0:int(0.8 * ntrain)]
valid_tune_idx = permute_aux[int(0.8 * ntrain):ntrain]
folds = [(train_tune_idx, valid_tune_idx)]
# Specify parameter grid, gp_model, and gpb.Dataset
param_grid = {'learning_rate': [1,0.1,0.01], 'max_depth': [1,2,3,5,10],
'min_data_in_leaf': [10,100,1000], 'lambda_l2': [0,1,10]}
other_params = {'num_leaves': 2**10, 'verbose': 0}
gp_model = gpb.GPModel(group_data=data_train['idcode'], likelihood='gaussian')
data_bst = gpb.Dataset(data=data_train[pred_vars], label=data_train['ln_wage'])
# Find optimal tuning parameters
opt_params = gpb.grid_search_tune_parameters(param_grid=param_grid, params=other_params,
num_try_random=None, folds=folds, seed=1000,
train_set=data_bst, gp_model=gp_model,
num_boost_round=1000, early_stopping_rounds=10,
verbose_eval=1, metric='mse')
opt_params
# {'best_params': {'learning_rate': 0.01, 'max_depth': 2, 'min_data_in_leaf': 10, 'lambda_l2': 10}, 'best_iter': 391, 'best_score': 0.08507862825877585}
R
# Partition training data into inner training data and validation data
ntrain <- dim(data_train)[1]
set.seed(ntrain)
valid_tune_idx <- sample.int(ntrain, as.integer(0.2*ntrain))
folds <- list(valid_tune_idx)
# Specify parameter grid, gp_model, and gpb.Dataset
param_grid <- list("learning_rate" = c(1,0.1,0.01), "max_depth" = c(1,2,3,5,10),
"min_data_in_leaf" = c(10,100,1000), "lambda_l2" = c(0,1,10))
other_params <- list(num_leaves = 2¹⁰)
gp_model <- GPModel(group_data = data_train[,"idcode"], likelihood = "gaussian")
data_bst <- gpb.Dataset(data = data_train[,pred_vars], label = data_train[,"ln_wage"])
# Find optimal tuning parameters
opt_params <- gpb.grid.search.tune.parameters(param_grid = param_grid, params = other_params,
num_try_random = NULL, folds = folds,
data = data_bst, gp_model = gp_model,
nrounds = 1000, early_stopping_rounds = 10,
verbose_eval = 1, metric = "mse")
opt_params
5 预测
可以通过predict函数获得预测。我们需要提供树集合的测试预测变量(data)和随机效应模型的测试分类变量(group_data_pred)。以下是对留下的测试数据进行预测并计算测试均方误差(MSE)。
Python
pred = gpbst.predict(data=data_test[pred_vars], group_data_pred=data_test['idcode'],
predict_var=False, pred_latent=False)
y_pred = pred['response_mean']
np.mean((data_test['ln_wage'] - y_pred)**2)
## 0.0866730945477676
R
pred <- predict(gpbst, data = data_test[,pred_vars], group_data_pred = data_test[,"idcode"],
predict_var = FALSE, pred_latent = FALSE)
y_pred <- pred[["response_mean"]]
mean((data_test[,"ln_wage"] - y_pred)²)
6 解释
估计随机效应模型的信息可以通过以下方式获得。
Python
gp_model.summary()
## =====================================================
## Covariance parameters (random effects):
## Param.
## Error_term 0.0703
## idcode 0.0448
## =====================================================
R
summary(gp_model)
有几个工具可以帮助理解GPBoost库支持的固定效应树集合函数的形式:
-
SHAP 值
-
SHAP 和部分依赖图(二维和一维)
-
SHAP 力图
-
交互统计
-
基于拆分的变量重要性
以下我们查看 SHAP 值和 SHAP 依赖图。
Python
import shap
shap_values = shap.TreeExplainer(gpbst).shap_values(data_train[pred_vars])
feature_names = [ a + ": " + str(b) for a,b in zip(pred_vars, np.abs(shap_values).mean(0).round(2)) ]
shap.summary_plot(shap_values, data_train[pred_vars], max_display=10, feature_names=feature_names)
shap.dependence_plot("ttl_exp", shap_values, data_train[pred_vars], alpha=0.5)


SHAP 值和 SHAP 依赖图 — 作者图片
R
library(SHAPforxgboost)
library(ggplot2)
shap.plot.summary.wrap1(gpbst, X = data_train[,pred_vars], top_n=10) +
ggtitle("SHAP values")
shap_long <- shap.prep(gpbst, X_train = data_train[,pred_vars])
shap.plot.dependence(data_long = shap_long, x = "ttl_exp",
color_feature = "occ_code_7", smooth = FALSE, size = 2) +
ggtitle("SHAP dependence plot for ttl_exp")
7 进一步的建模选项
7.1 分类变量与其他预测变量之间的交互
在上述模型中,随机效应与固定效应预测变量之间没有交互,即高维分类变量与其他预测变量之间没有交互。这种交互可以通过将分类变量额外包含在树集成中来建模。以下代码演示了如何实现这一点。下面,我们还计算了该模型的测试均方误差(MSE)。与没有这种交互的模型相比,测试 MSE 略小。注意:下面使用的调优参数已提前选择,但为了简洁起见,我们省略了这段代码。
Python
pred_vars_int = ['idcode'] + pred_vars
gp_model = gpb.GPModel(group_data=data_train['idcode'], likelihood='gaussian')
data_bst = gpb.Dataset(data=data_train[pred_vars_int], categorical_feature=[0],
label=data_train['ln_wage'])
params = {'learning_rate': 0.01, 'max_depth': 2, 'min_data_in_leaf': 10,
'lambda_l2': 1, 'num_leaves': 2**10, 'verbose': 0}
gpbst = gpb.train(params=params, train_set=data_bst, gp_model=gp_model,
num_boost_round=482)
pred = gpbst.predict(data=data_test[pred_vars_int], group_data_pred=data_test['idcode'])
np.mean((data_test['ln_wage'] - pred['response_mean'])**2)
## 0.08616085739486774
R
pred_vars_int = c("idcode", pred_vars)
gp_model <- GPModel(group_data = data_train[,"idcode"], likelihood = "gaussian")
data_bst <- gpb.Dataset(data = data_train[,pred_vars_int], categorical_feature = c(1),
label = data_train[,"ln_wage"])
params <- list(learning_rate = 0.01, max_depth = 2, num_leaves = 2¹⁰,
min_data_in_leaf = 10, lambda_l2 = 1)
gpbst <- gpb.train(data = data_bst, gp_model = gp_model,
nrounds = 482, params = params, verbose = 0)
pred <- predict(gpbst, data = data_test[,pred_vars_int], group_data_pred = data_test[,"idcode"])
mean((data_test[,"ln_wage"] - pred[["response_mean"]])²)
7.2(广义)线性混合效应模型
GPBoost库还支持(广义)线性混合效应模型(GLMMs)。在 GLMM 中,假设固定效应函数 F()是线性函数而不是树集成。以下代码演示了如何实现这一点。注意需要手动添加一列 1 以包括截距。使用选项params={"std_dev": True} / params = list(std_dev=TRUE),可以在summary函数中获得标准差和 p 值。
Python
X = data_train[pred_vars]
X = X.assign(Intercept = 1)
gp_model = gpb.GPModel(group_data=data_train['idcode'], likelihood="gaussian")
gp_model.fit(y=data_train['ln_wage'], X=X, params={"std_dev": True})
gp_model.summary()
## =====================================================
## Model summary:
## Log-lik AIC BIC
## -6191.97 12493.95 12934.9
## Nb. observations: 22410
## Nb. groups: 4567 (idcode)
## -----------------------------------------------------
## Covariance parameters (random effects):
## Param. Std. dev.
## Error_term 0.0794 0.0008
## idcode 0.0452 0.0014
## -----------------------------------------------------
## Linear regression coefficients (fixed effects):
## Param. Std. dev. z value P(>|z|)
## grade 0.0379 0.0025 14.8659 0.0000
## age 0.0032 0.0013 2.4665 0.0136
## ttl_exp 0.0310 0.0012 25.4221 0.0000
## tenure 0.0107 0.0009 11.9256 0.0000
## not_smsa -0.1326 0.0079 -16.8758 0.0000
## south -0.0884 0.0072 -12.3259 0.0000
...
R
X = cbind(Intercept = rep(1,dim(data_train)[1]), data_train[,pred_vars])
gp_model <- fitGPModel(group_data = data_train[,"idcode"], X = X,
y = data_train[,"ln_wage"], likelihood = "gaussian",
params = list(std_dev=TRUE))
summary(gp_model)
8 结论与参考文献
我们展示了如何使用[GPBoost](https://github.com/fabsig/GPBoost) 库的 Python 和 R 包将树提升与随机效应结合应用。GPBoost库还有许多我们未展示的功能。您可以查看Python 示例和R 示例。在本系列的第三部分中,我们将展示如何使用GPBoost库对纵向数据(即面板数据)进行建模。
-
F. Sigrist. 高斯过程提升. 机器学习研究期刊, 23(1):10565–10610, 2022.
-
F. Sigrist. 潜在高斯模型提升. IEEE 模式分析与机器智能期刊, 45(2):1894–1905, 2023.
使用 GPBoost 进行纵向数据和面板数据的混合效应机器学习(第三部分)
使用真实世界数据的 Python 和 R 中 GPBoost 的演示
·
关注 发表在 Towards Data Science ·11 分钟阅读·2023 年 7 月 17 日
--
纵向数据的示例:不同对象(idcode)的时间序列图 — 图片由作者提供
在 第 I 部分 和 第 II 部分 的系列文章中,我们展示了如何使用随机效应对机器学习模型中的高基数类别变量进行建模,并介绍了实现 GPBoost 算法 的 [GPBoost](https://github.com/fabsig/GPBoost) 库,该算法将树提升与随机效应结合。在本文中,我们演示了如何使用 GPBoost 库的 Python 和 R 包进行纵向数据(也称为重复测量或面板数据)分析。你可能需要先阅读 第 II 部分,因为它对 GPBoost 库进行了初步介绍。此演示中使用的是 GPBoost 版本 1.2.1。
目录
∘ 1 数据:描述、加载和样本分割
∘ 2 GPBoost 中的纵向数据建模选项
· · 2.1 主题分组随机效应
· · 2.2 仅固定效应
· · 2.3 主题和时间分组的随机效应
· · 2.4 带时间随机斜率的主题随机效应
· · 2.5 主题特定的 AR(1) / 高斯过程模型
· · 2.6 主题分组随机效应和联合 AR(1) 模型
∘ 3 训练 GPBoost 模型
∘ 4 选择调优参数
∘ 5 预测
∘ 6 结论和参考文献
1 数据:描述、加载和样本分割
此演示中使用的数据是已在 第 II 部分 使用过的工资数据。数据可以从 这里 下载。数据集包含 28,013 个样本,涉及 4,711 个人,这些数据在几年内被测量。此类数据称为纵向数据或面板数据,因为对于每个主题(个人 ID = idcode),数据在时间上(年份 = t)被重复收集。换句话说,每个类别变量 idcode 的级别的样本都是随时间重复测量的。响应变量是对数实际工资 (ln_wage),数据还包括年龄、总工作经验、工作年限、日期等多个预测变量。
在下面的代码中,我们加载数据并将其随机分成 80% 的训练数据和 20% 的测试数据。
Python
import gpboost as gpb
import pandas as pd
import numpy as np
# Load data
data = pd.read_csv("https://raw.githubusercontent.com/fabsig/Compare_ML_HighCardinality_Categorical_Variables/master/data/wages.csv.gz")
data = data.assign(t_sq = data['t']**2)# Add t²
# Partition into training and test data
n = data.shape[0]
np.random.seed(n)
permute_aux = np.random.permutation(n)
train_idx = permute_aux[0:int(0.8 * n)]
test_idx = permute_aux[int(0.8 * n):n]
data_train = data.iloc[train_idx]
data_test = data.iloc[test_idx]
# Define fixed effects predictor variables
pred_vars = [col for col in data.columns if col not in ['ln_wage', 'idcode', 't', 't_sq']]
R
library(gpboost)
library(tidyverse)
# Load data
data <- read_csv("https://raw.githubusercontent.com/fabsig/Compare_ML_HighCardinality_Categorical_Variables/master/data/wages.csv.gz")
data[,"t_sq"] <- data[,"t"]² # Add t²
data <- as.matrix(data) # Convert to matrix since the boosting part does not support data.frames
# Partition into training and test data
n <- dim(data)[1]
set.seed(n)
permute_aux <- sample.int(n, n)
train_idx <- permute_aux[1:as.integer(0.8*n)]
test_idx <- permute_aux[(as.integer(0.8*n)+1):n]
data_train <- data[train_idx,]
data_test <- data[test_idx,]
# Define fixed effects predictor variables
pred_vars <- colnames(data)[-which(colnames(data) %in% c("ln_wage", "idcode", "t", "t_sq"))]
2 GPBoost 中的纵向数据建模选项
使用GPBoost库可以处理纵向数据的各种固定和随机效应模型组合。下面,我们演示了几种模型。这个列表并不详尽,还有其他模型可能。定义了随机效应模型(gp_model)和Dataset(data_bst)之后,可以使用gpb.train函数进行训练。后续的训练步骤对所有模型都是相同的,因此在这里我们不包括训练步骤,而是首先仅展示如何定义相应的模型。关于如何进行训练,请参见后续的第三部分。
2.1 受试者分组的随机效应
第一种选择是对分类受试者变量(idcode)使用分组随机效应,对时间变量使用固定效应。这对应于以下模型:

其中 i 是人员索引(=idcode),j 是时间索引,并且我们假设 xij 包含时间变量(或其“虚拟化”/独热编码版本)和其他预测变量。人员编号 i 的idcode随机效应是 bi,F(xij)是树集成固定效应函数。这样的模型可以在GPBoost中指定,如下所示。受试者特定的随机效应通过将idcode分类变量传递给GPModel()构造函数的group_data参数来定义。注意时间变量t的基数非常低,因此我们可以使用独热编码或虚拟变量。我们在下面所做的即为此,pred_vars定义的固定效应预测变量已经包含了年份虚拟变量。
Python
gp_model = gpb.GPModel(group_data=data_train['idcode'], likelihood='gaussian')
data_bst = gpb.Dataset(data=data_train[pred_vars], label=data_train['ln_wage'])
R
gp_model <- GPModel(group_data = data_train[,"idcode"], likelihood = "gaussian")
data_bst <- gpb.Dataset(data = data_train[,pred_vars], label = data_train[,"ln_wage"])
2.2 仅固定效应
另一种选择是使用仅固定效应模型,其中受试者(idcode)和时间都使用固定效应进行建模,没有随机效应。这对应于以下模型:

再次,为了简化符号,假设 xij 包含时间变量。这样的模型可以通过在GPBoost中简单地将受试者(idcode)和时间变量包含在树提升预测变量中并且没有随机效应模型来指定。
Python
pred_vars_FE = ['idcode'] + pred_vars
data_bst = gpb.Dataset(data=data_train[pred_vars_FE], categorical_feature=[0],
label=data_train['ln_wage'])
R
pred_vars_FE = c("idcode", pred_vars)
data_bst <- gpb.Dataset(data = data_train[,pred_vars_FE], categorical_feature = c(1),
label = data_train[,"ln_wage"])
2.3 受试者和时间分组的随机效应
也可以对受试者(idcode)和时间(t)使用随机效应。这对应于以下模型:

其中 bi 和 bj 是idcode和t随机效应。这样的模型可以在GPBoost中如下指定。与上述第 2.1 节中的单级随机效应模型相比,我们现在将两个变量(idcode和t)传递给GPModel()构造函数的group_data参数。
Python
gp_model = gpb.GPModel(group_data=data_train[['idcode','t']], likelihood='gaussian')
data_bst = gpb.Dataset(data=data_train[pred_vars], label=data_train['ln_wage'])
R
gp_model <- GPModel(group_data = data_train[,c("idcode","t")], likelihood = "gaussian")
data_bst <- gpb.Dataset(data = data_train[,pred_vars], label = data_train[,"ln_wage"])
2.4 受试者随机效应与时间随机斜率
在迄今为止介绍的随机效应模型中(例如,在第 2.1 节中),每个个体(idcode)都有不同的截距(=均值)。然而,除了这种“偏移”之外,时间上的演变对所有个体是相同的。通过包括多项式随机系数(= 随机斜率),可以放宽这一限制。例如,一个对t和t²具有随机斜率的模型为

其中 b(0,i) 是截距随机效应,b(1,i) 和 b(2,i) 是随机斜率,tij 表示样本 ij 的时间t。由于时间上的随机斜率,在此模型中每个人在时间t上的演变是不同的。这种模型可以在GPBoost中进行指定,如下代码所示。group_rand_coef_data 变量包含随机系数协变量数据t 和 t²。此外,ind_effect_group_rand_coef中的索引指示应该使用哪些group_data中的分类变量来获取随机系数。在这里,只有一个分类变量(idcode),因此 ind_effect_group_rand_coef 只是 (1,1)(即,t 和 t² 都属于 idcode)。
Python
gp_model = gpb.GPModel(group_data=data_train['idcode'], likelihood='gaussian',
group_rand_coef_data=data_train[["t","t_sq"]],
ind_effect_group_rand_coef=[1,1])
data_bst = gpb.Dataset(data=data_train[pred_vars], label=data_train['ln_wage'])
R
gp_model <- GPModel(group_data = data_train[,"idcode"], likelihood = "gaussian",
group_rand_coef_data = cbind(data_train[,"t"], data_train[,"t_sq"]),
ind_effect_group_rand_coef = c(1,1))
data_bst <- gpb.Dataset(data = data_train[,pred_vars], label = data_train[,"ln_wage"])
2.5 个体特定的 AR(1) / 高斯过程模型
对上述随机斜率模型的进一步扩展是使用时间序列模型或高斯过程,而不是多项式随机斜率。这允许以更灵活的方式分别建模每个个体的时间演变。例如,可以使用个体特定的 AR(1) 模型:

在此模型中,每个个体 i 都有独立的随机效应 bij,这些效应随时间按照 AR(1) 模型演变(= 离散化的高斯过程,具有指数协方差函数)。这种模型可以在GPBoost中进行指定,如下代码所示。时间高斯过程通过将时间变量t传递给gp_coords参数来定义。我们使用具有指数协方差函数的高斯过程,这相当于 AR(1) 模型。此外,cluster_ids 变量指示每个人(idcode)都有一个独立的(= 先验独立)高斯过程。
Python
gp_model = gpb.GPModel(gp_coords=data_train['t'], cov_function='exponential',
cluster_ids=data_train['idcode'], likelihood='gaussian')
data_bst = gpb.Dataset(data=data_train[pred_vars], label=data_train['ln_wage'])
R
gp_model <- GPModel(gp_coords = data_train[,"t"], cov_function="exponential",
cluster_ids = data_train[,"idcode"], likelihood = "gaussian")
data_bst <- gpb.Dataset(data = data_train[,pred_vars], label = data_train[,"ln_wage"])
训练后,AR(1) 模型参数可以从高斯过程协方差参数中获得,如下所示。
Python
cov_pars = gp_model.get_cov_pars()
phi_hat = np.exp(-1 / cov_pars['GP_range'][0])
sigma2_hat = cov_pars['GP_var'][0] * (1\. - phi_hat ** 2)
print("Estimated innovation variance and AR(1) coefficient of year effect:")
print([sigma2_hat, phi_hat])
R
cov_pars = gp_model$get_cov_pars()
phi_hat = exp(-1 / cov_pars["GP_range"])
sigma2_hat = cov_pars["GP_var"] * (1 - phi_hat²)
print("Estimated innovation variance and AR(1) coefficient:")
print(c(sigma2 = sigma2_hat, phi = phi_hat))
2.6 主题分组随机效应和联合 AR(1) 模型
另一种选择是使用一个在所有个体中共享的单一时间 AR(1) 模型,但包括单独的时间常数个体(idcode)随机效应。这对应于以下模型:

这种模型可以在GPBoost中进行如下指定。
Python
gp_model = gpb.GPModel(group_data=data_train['idcode'], likelihood='gaussian'
gp_coords=data_train['t'], cov_function='exponential')
data_bst = gpb.Dataset(data=data_train[pred_vars], label=data_train['ln_wage'])
R
gp_model <- GPModel(group_data = data_train[,"idcode"], likelihood = "gaussian",
gp_coords = data_train[,"t"], cov_function="exponential")
data_bst <- gpb.Dataset(data = data_train[,pred_vars], label = data_train[,"ln_wage"])
3 训练 GPBoost 模型
以下代码展示了如何训练一个 GPBoost 模型。我们首先定义了一个随机效应模型(gp_model)、一个Dataset(data_bst)、调整参数(params和nrounds = 树的数量/提升迭代次数),然后通过调用gpb.train函数运行 GPBoost 算法。我们使用一个带有时间随机斜率的受试者(idcode)随机效应模型(见第 2.4 节),因为这样的模型通常在灵活性/准确性和运行时间之间提供了很好的折衷。请注意,我们使用的是提前选择好的调整参数(见下文)。
Python
gp_model = gpb.GPModel(group_data=data_train['idcode'], likelihood='gaussian',
group_rand_coef_data=data_train[["t","t_sq"]],
ind_effect_group_rand_coef=[1,1])
data_bst = gpb.Dataset(data=data_train[pred_vars], label=data_train['ln_wage'])
params = {'learning_rate': 0.01, 'max_depth': 2, 'min_data_in_leaf': 10,
'lambda_l2': 10, 'num_leaves': 2**10, 'verbose': 0}
nrounds = 379
gpbst = gpb.train(params=params, train_set=data_bst, gp_model=gp_model,
num_boost_round=nrounds)
gp_model.summary() # Estimated random effects model
## =====================================================
## Covariance parameters (random effects):
## Param.
## Error_term 0.0531
## idcode 0.0432
## idcode_rand_coef_t 0.0004
## idcode_rand_coef_t_sq 0.0000
## =====================================================
R
gp_model <- GPModel(group_data = data_train[,"idcode"], likelihood = "gaussian",
group_rand_coef_data = cbind(data_train[,"t"], data_train[,"t_sq"]),
ind_effect_group_rand_coef = c(1,1))
data_bst <- gpb.Dataset(data = data_train[,pred_vars], label = data_train[,"ln_wage"])
params <- list(learning_rate = 0.01, max_depth = 2, num_leaves = 2¹⁰,
min_data_in_leaf = 10, lambda_l2 = 10)
nrounds <- 379
gpbst <- gpb.train(data = data_bst, gp_model = gp_model,
nrounds = nrounds, params = params, verbose = 0)
summary(gp_model) # Estimated random effects mode
当存在多个随机效应(如这里的情况)时,使用nelder_mead作为优化器通常比gradient_descent(默认为优化器)更快。 这可以在调用gpb.train之前设置。然而,对于这个数据集来说,这并不重要。
Python
gp_model.set_optim_params(params={"optimizer_cov": "nelder_mead"})
R
gp_model$set_optim_params(params = list(optimizer_cov = "nelder_mead"))
4 选择调整参数
调整参数在提升模型中选择得当是非常重要的。没有通用的默认值,每个数据集可能需要不同的调整参数。下面,我们展示了如何使用gpb.grid.search.tune.parameters函数来选择调整参数。我们使用带有下述参数组合的确定性网格搜索。请注意,我们调整了max_depth参数,并将num_leaves设置为较大的值。我们将训练数据随机拆分为 80%的内部训练数据和 20%的验证数据,并使用均方误差(mse)作为验证数据上的预测准确度测量。或者,也可以使用例如测试负对数似然(metric = "test_neg_log_likelihood" = 默认),它也会考虑预测的不确定性。注意:根据数据集和网格大小,这可能需要一些时间(在我的笔记本电脑上大约半小时)。除了下面的确定性网格搜索外,还可以进行随机网格搜索(见*num_try_random*)或其他方法,如贝叶斯优化,以加快速度。
Python
# Partition training data into inner training data and validation data
ntrain = data_train.shape[0]
np.random.seed(ntrain)
permute_aux = np.random.permutation(ntrain)
train_tune_idx = permute_aux[0:int(0.8 * ntrain)]
valid_tune_idx = permute_aux[int(0.8 * ntrain):ntrain]
folds = [(train_tune_idx, valid_tune_idx)]
# Specify parameter grid, gp_model, and gpb.Dataset
param_grid = {'learning_rate': [1,0.1,0.01], 'max_depth': [1,2,3,5,10],
'min_data_in_leaf': [10,100,1000], 'lambda_l2': [0,1,10]}
other_params = {'num_leaves': 2**10, 'verbose': 0}
gp_model = gpb.GPModel(group_data=data_train['idcode'], likelihood='gaussian',
group_rand_coef_data=data_train[["t","t_sq"]],
ind_effect_group_rand_coef=[1,1])
data_bst = gpb.Dataset(data=data_train[pred_vars], label=data_train['ln_wage'])
# Find optimal tuning parameters
opt_params = gpb.grid_search_tune_parameters(param_grid=param_grid, params=other_params,
num_try_random=None, folds=folds, seed=1000,
train_set=data_bst, gp_model=gp_model,
num_boost_round=1000, early_stopping_rounds=10,
verbose_eval=1, metric='mse')
opt_params
# {'best_params': {'learning_rate': 0.01, 'max_depth': 2, 'min_data_in_leaf': 10, 'lambda_l2': 10}, 'best_iter': 379, 'best_score': 0.08000593485771226}
R
# Partition training data into inner training data and validation data
ntrain <- dim(data_train)[1]
set.seed(ntrain)
valid_tune_idx <- sample.int(ntrain, as.integer(0.2*ntrain))
folds <- list(valid_tune_idx)
# Specify parameter grid, gp_model, and gpb.Dataset
param_grid <- list("learning_rate" = c(1,0.1,0.01), "max_depth" = c(1,2,3,5,10),
"min_data_in_leaf" = c(10,100,1000), "lambda_l2" = c(0,1,10))
other_params <- list(num_leaves = 2¹⁰)
gp_model <- GPModel(group_data = data_train[,"idcode"], likelihood = "gaussian",
group_rand_coef_data = cbind(data_train[,"t"], data_train[,"t_sq"]),
ind_effect_group_rand_coef = c(1,1))
data_bst <- gpb.Dataset(data = data_train[,pred_vars], label = data_train[,"ln_wage"])
# Find optimal tuning parameters
opt_params <- gpb.grid.search.tune.parameters(param_grid = param_grid, params = other_params,
num_try_random = NULL, folds = folds,
data = data_bst, gp_model = gp_model,
nrounds = 1000, early_stopping_rounds = 10,
verbose_eval = 1, metric = "mse")
opt_params
5 预测
可以通过predict函数获得预测结果。我们需要提供树集成的测试预测变量(data)以及随机效应模型的测试受试者变量和随机斜率数据(group_data_pred和group_rand_coef_data_pred)。下面,我们对遗漏的测试数据进行预测,并计算测试均方误差(MSE)。正如下面的结果所示,与第 2.1 节中介绍的没有引入随机斜率的简单受试者随机效应模型相比,这个随机斜率模型的预测准确度更高(有关后者模型的结果,请参见第二部分)。
Python
pred = gpbst.predict(data=data_test[pred_vars], group_data_pred=data_test['idcode'],
group_rand_coef_data_pred=data_test[["t","t_sq"]],
predict_var=False, pred_latent=False)
y_pred = pred['response_mean']
np.mean((data_test['ln_wage'] - y_pred)**2)
## 0.08117682279905969
R
pred <- predict(gpbst, data = data_test[,pred_vars], group_data_pred = data_test[,"idcode"],
group_rand_coef_data_pred = cbind(data_test[,"t"], data_test[,"t_sq"]),
predict_var = FALSE, pred_latent = FALSE)
y_pred <- pred[["response_mean"]]
mean((data_test[,"ln_wage"] - y_pred)²)
6 结论和参考文献
我们展示了如何使用GPBoost库来建模纵向数据。GPBoost库还有更多我们未展示的功能。例如,在第二部分中,我们展示了如何利用机器学习可解释性技术理解固定效应函数,同时也演示了GPBoost库如何处理(广义)线性混合效应模型(GLMMs → F() 是线性的而不是树集合)。你还可以查看Python 示例和R 示例。
-
F. Sigrist. 高斯过程提升. 机器学习研究杂志, 23(1):10565–10610, 2022。
-
F. Sigrist. 潜在高斯模型提升. IEEE 模式分析与机器智能学报, 45(2):1894–1905, 2023。
使用 GPBoost 进行分组和区域空间计量经济数据的混合效应机器学习
使用欧洲 GDP 数据的演示
·
关注 发布于 Towards Data Science ·14 分钟阅读·2023 年 6 月 21 日
--
欧洲 GDP 数据:预测变量‘edu’的空间效应和 SHAP 依赖图 — 作者提供的图片
GPBoost 算法通过用树提升建模的非参数非线性函数替换线性固定效应函数,扩展了线性混合效应和高斯过程模型。本文展示了如何使用在[GPBoost](https://github.com/fabsig/GPBoost) 库中实现的 GPBoost 算法来建模具有空间和分组结构的数据。我们使用欧洲 GDP 数据展示GPBoost库的功能,该数据是一个区域空间计量经济学数据的例子。
目录
∘ 介绍
· ·GPBoost 的基本工作流程
· ·数据描述
· ·数据加载和简短可视化
∘ 训练 GPBoost 模型
∘ 选择调优参数
∘ 模型解释
· ·估计的随机效应模型
· ·空间效应图
· ·理解固定效应函数
∘ 扩展
· ·不同时间段的独立随机效应
· ·空间与固定效应预测变量之间的交互
· ·大数据
· ·其他空间随机效应模型
· ·(广义)线性混合效应和高斯过程模型
∘ 参考文献
介绍
GPBoost 的基本工作流程
应用 GPBoost 模型(= 组合树提升和随机效应/GP 模型)包括以下主要步骤:
-
定义一个
GPModel,在其中指定以下内容:-
随机效应模型(例如,空间随机效应、分组随机效应、组合空间和分组等)
-
似然(= 条件于固定和随机效应的响应变量的分布)
-
-
创建一个包含响应变量和固定效应预测变量的
gpb.Dataset -
选择调优参数,例如,使用函数
gpb.grid.search.tune.parameters -
使用
gpboost / gpb.train函数训练模型 -
解释训练好的模型和/或进行预测
在本文中,我们使用一个实际数据集展示这些步骤。此外,我们还展示了如何解释拟合模型,并查看GPBoost库的几个扩展和附加功能。所有结果均使用GPBoost版本 1.2.1 获得。此演示使用 R 包,但相应的 Python 包提供相同的功能。
数据描述
本演示使用的数据是欧洲国内生产总值(GDP)数据。可以从raw.githubusercontent.com/fabsig/GPBoost/master/data/gdp_european_regions.csv下载。数据由罗马第三大学 Massimo Giannini 从 Eurostat 收集,并于 2023 年 6 月 16 日提供给 Fabio Sigrist,用于罗马第三大学的讲座。
数据收集了 2000 年和 2021 年这两年的 242 个欧洲区域的数据。即,总数据点数为 484。响应变量是(对数)人均 GDP。有四个预测变量:
-
L: (对数)就业份额(empl/pop)
-
K: (对数)固定资本/人口
-
Pop: 对数(人口)
-
Edu: 高等教育份额
此外,还有每个区域的中心空间坐标(经度和纬度),242 个不同欧洲区域的空间区域 ID,以及一个额外的空间集群 ID,用于标识区域所属的集群(共有两个集群)。
数据加载与简短可视化
我们首先加载数据,并创建一张地图来展示空间上的(对数)人均 GDP。我们创建了两张地图:一张包含所有数据,另一张则排除了一些偏远的岛屿。在下面的注释代码中,我们还展示了在没有空间多边形形状文件的情况下如何创建空间图。
library(gpboost)
library(ggplot2)
library(gridExtra)
library(viridis)
library(sf)
# Load data
data <- read.csv("https://raw.githubusercontent.com/fabsig/GPBoost/master/data/gdp_european_regions.csv")
FID <- data$FID
data <- as.matrix(data[,names(data)!="FID"]) # convert to matrix since the boosting part currently does not support data.frames
covars <- c("L", "K", "pop", "edu")
# Load shape file for spatial plots
cur_tempfile <- tempfile()
download.file(url = "https://raw.githubusercontent.com/fabsig/GPBoost/master/data/shape_European_regions.zip", destfile = cur_tempfile)
out_directory <- tempfile()
unzip(cur_tempfile, exdir = out_directory)
shape <- st_read(dsn = out_directory)
# Create spatial plot of GDP
data_plot <- merge(shape, data.frame(FID = FID, y = data[,"y"]), by="FID")
p1 <- ggplot(data_plot) + geom_sf(aes(group=FID, fill=y)) +
scale_fill_viridis(name="GDP / capita (log)", option = "B") +
ggtitle("GDP / capita (log)")
# Sample plot excluding islands
p2 <- ggplot(data_plot) + geom_sf(aes(group=FID, fill=y)) +
scale_fill_viridis(name="GDP / capita (log)", option = "B") +
xlim(2700000,6400000) + ylim(1500000,5200000) +
ggtitle("GDP / capita (log) -- excluding islands")
grid.arrange(p1, p2, ncol=2)
# # Spatial plot without a shape file
# p1 <- ggplot(data = data.frame(Lat=data[,"Lat"], Long=data[,"Long"],
# GDP=data[,"y"]), aes(x=Long,y=Lat,color=GDP)) +
# geom_point(size=2, alpha=0.5) + scale_color_viridis(option = "B") +
# ggtitle("GDP / capita (log)")
# p2 <- ggplot(data = data.frame(Lat=data[,"Lat"], Long=data[,"Long"],
# GDP=data[,"y"]), aes(x=Long,y=Lat,color=GDP)) +
# geom_point(size=3, alpha=0.5) + scale_color_viridis(option = "B") +
# ggtitle("GDP / capita (log) -- Europe excluding islands") + xlim(-10,28) + ylim(35,67)
# grid.arrange(p1, p2, ncol=2)

242 个欧洲区域的人均 GDP(对数)——作者图片
训练 GPBoost 模型
接下来,我们使用带有指数协方差函数的高斯过程模型来建模空间随机效应。此外,我们还包括了集群变量 cl 的分组随机效应。在GPBoost库中,高斯过程随机效应由gp_coords参数定义,分组随机效应通过GPModel构造函数的group_data参数来定义。上述预测变量用于固定效应的树集成函数。我们使用gpboost,或等效地gpb.train函数来拟合模型。请注意,我们使用的调整参数是通过交叉验证选择的。
gp_model <- GPModel(group_data = data[, c("cl")],
gp_coords = data[, c("Long", "Lat")],
likelihood = "gaussian", cov_function = "exponential")
params <- list(learning_rate = 0.01, max_depth = 2, num_leaves = 2¹⁰,
min_data_in_leaf = 10, lambda_l2 = 0)
nrounds <- 37
# gp_model$set_optim_params(params = list(trace=TRUE)) # To monitor hyperparameter estimation
boost_data <- gpb.Dataset(data = data[, covars], label = data[, "y"])
gpboost_model <- gpboost(data = boost_data, gp_model = gp_model,
nrounds = nrounds, params = params,
verbose = 1) # same as gpb.train# same as gpb.train gpb.train
选择调整参数
调整参数对于提升模型的性能至关重要。没有通用的默认值,每个数据集可能需要不同的调整参数。下面我们展示了如何使用gpb.grid.search.tune.parameters函数选择调整参数。我们使用均方误差(mse)作为验证数据上的预测准确性指标。或者,也可以使用例如测试负对数似然(test_neg_log_likelihood = 默认值,如果没有指定)来考虑预测的不确定性。根据数据集和网格大小,这可能需要一些时间。除了下面的确定性网格搜索,还可以使用随机网格搜索来加快速度(参见 *num_try_random*)。
gp_model <- GPModel(group_data = data[, "cl"],
gp_coords = data[, c("Long", "Lat")],
likelihood = "gaussian", cov_function = "exponential")
boost_data <- gpb.Dataset(data = data[, covars], label = data[, "y"])
param_grid = list("learning_rate" = c(1,0.1,0.01),
"min_data_in_leaf" = c(10,100,1000),
"max_depth" = c(1,2,3,5,10),
"lambda_l2" = c(0,1,10))
other_params <- list(num_leaves = 2¹⁰)
set.seed(1)
opt_params <- gpb.grid.search.tune.parameters(param_grid = param_grid, params = other_params,
num_try_random = NULL, nfold = 4,
data = boost_data, gp_model = gp_model,
nrounds = 1000, early_stopping_rounds = 10,
verbose_eval = 1, metric = "mse") # metric = "test_neg_log_likelihood"
opt_params
# ***** New best test score (l2 = 0.0255393919591794) found for the following parameter combination: learning_rate: 0.01, min_data_in_leaf: 10, max_depth: 2, lambda_l2: 0, nrounds: 37
模型解释
估计的随机效应模型
通过对 gp_model 调用 summary 函数,可以获取估计的随机效应模型的信息。对于高斯过程,GP_var 是边际方差,即空间相关性或结构空间变化的量,GP_range 是测量相关性随空间衰减速度的范围或尺度参数。对于指数协方差函数,该值的三倍(这里大约为 17)是(残差)空间相关性基本为零的距离(低于 5%)。如下面的结果所示,由于边际方差 0.0089 相对于响应变量的总方差约 0.29 较小,空间相关性量相对较小。此外,误差项和 cl 分组随机效应的方差也较小(0.013 和 0.022)。因此,我们得出结论,大部分响应变量的方差由固定效应预测变量解释。
summary(gp_model)
## =====================================================
## Covariance parameters (random effects):
## Param.
## Error_term 0.0131
## Group_1 0.0221
## GP_var 0.0089
## GP_range 5.7502
## =====================================================
空间效应图
我们可以通过对训练数据调用 predict 函数来绘制在训练数据位置估计的(“预测”)空间随机效应;见下面的代码。这样的图显示了在剔除固定效应预测变量的效应后,空间效应的情况。注意,这些空间效应考虑了空间高斯过程和分组区域集群随机效应。如果想仅获取来自高斯过程部分的空间随机效应,可以使用 predict_training_data_random_effects 函数(见下面的注释代码)。或者,也可以进行空间外推(“克里金插值”),但这对于区域数据并没有太大意义。
pred <- predict(gpboost_model, group_data_pred = data[1:242, c("cl")],
gp_coords_pred = data[1:242, c("Long", "Lat")],
data = data[1:242, covars], predict_var = TRUE, pred_latent = TRUE)
data_plot <- merge(shape, data.frame(FID = FID, y = pred$random_effect_mean), by="FID")
plot_mu <- ggplot(data_plot) + geom_sf(aes(group=FID, fill=y)) +
scale_fill_viridis(name="Spatial effect", option = "B") +
ggtitle("Spatial effect (mean)") + xlim(2700000,6400000) + ylim(1500000,5200000)
data_plot <- merge(shape, data.frame(FID = FID, y = pred$random_effect_cov), by="FID")
plot_sd <- ggplot(data_plot) + geom_sf(aes(group=FID, fill=y)) +
scale_fill_viridis(name="Std. dev.", option = "B") +
ggtitle("Uncertainty (std. dev.)") + xlim(2700000,6400000) + ylim(1500000,5200000)
grid.arrange(plot_mu, plot_sd, ncol=2)
# Only spatial effects from the Gaussian process ignoring the grouped random effects
# rand_effs <- predict_training_data_random_effects(gp_model, predict_var = TRUE)[1:242, c("GP", "GP_var")]
# data_plot <- merge(shape, data.frame(FID = FID, y = rand_effs[,1]), by="FID")
# plot_mu <- ggplot(data_plot) + geom_sf(aes(group=FID, fill=y)) +
# scale_fill_viridis(name="Spatial effect (mean)", option = "B") +
# ggtitle("Spatial effect from Gausian process (mean)") + xlim(2700000,6400000) + ylim(1500000,5200000)
# data_plot <- merge(shape, data.frame(FID = FID, y = rand_effs[,2]), by="FID")
# plot_sd <- ggplot(data_plot) + geom_sf(aes(group=FID, fill=y)) +
# scale_fill_viridis(name="Uncertainty (std. dev.)", option = "B") +
# ggtitle("Uncertainty (std. dev.)") + xlim(2700000,6400000) + ylim(1500000,5200000)
# grid.arrange(plot_mu, plot_sd, ncol=2)

空间效应(在剔除预测变量后)— 作者提供的图片
理解固定效应函数
有几个工具可以帮助理解固定效应函数的形式。下面我们考虑变量重要性度量、交互作用度量和依赖图。具体而言,我们查看
-
SHAP 值
-
SHAP 依赖图
-
基于分裂的变量重要性
-
Friedman 的 H 统计量
-
部分依赖图(单维和二维)
-
SHAP 力量图
如下结果所示,从 SHAP 值和 SHAP 依赖图获得的信息与查看传统变量重要性度量和部分依赖图的信息相同。最重要的变量是‘K’和‘edu’。从依赖图中,我们得出有非线性的结论。例如,K 的效果在 K 的大值和小值之间几乎是平的,中间增加。此外,edu 的效果在 edu 的小值时更陡峭,而在 edu 的大值时变平坦。Friedman 的 H 统计量表明存在一些交互。对于交互量最大的两个变量 L 和 pop,我们在下面创建了一个二维部分依赖图。此外,SHAP 力量图显示预测变量在 2000 年与 2021 年的效果不同。
# SHAP values
library(SHAPforxgboost)
shap.plot.summary.wrap1(gpboost_model, X = data[,covars]) + ggtitle("SHAP values")

SHAP 值 — 作者提供的图像
# SHAP dependence plots
shap_long <- shap.prep(gpboost_model, X_train = data[,covars])
shap.plot.dependence(data_long = shap_long, x = covars[2],
color_feature = covars[4], smooth = FALSE, size = 2) +
ggtitle("SHAP dependence plot for K")
shap.plot.dependence(data_long = shap_long, x = covars[4],
color_feature = covars[2], smooth = FALSE, size = 2) +
ggtitle("SHAP dependence plot for edu")


K 和 edu 的 SHAP 依赖图 — 作者提供的图像
# SHAP force plot
shap_contrib <- shap.values(gpboost_model, X_train = data[,covars])
plot_data <- cbind(shap_contrib$shap_score, ID = 1:dim(data)[1])
shap.plot.force_plot(plot_data, zoom_in=FALSE, id = "ID") +
scale_fill_discrete(name="Variable") + xlab("Regions") +
geom_vline(xintercept=242.5, linewidth=1) + ggtitle("SHAP force plot") +
geom_text(x=100,y=-1.2,label="2000") + geom_text(x=400,y=-1.2,label="2021")

SHAP 力量图 — 作者提供的图像
# Split-based feature importances
feature_importances <- gpb.importance(gpboost_model, percentage = TRUE)
gpb.plot.importance(feature_importances, top_n = 25, measure = "Gain",
main = "Split-based variable importances")

变量重要性 — 作者提供的图像
# H-statistic for interactions
library(flashlight)
fl <- flashlight(model = gpboost_model, data = data.frame(y = data[,"y"], data[,covars]),
y = "y", label = "gpb",
predict_fun = function(m, X) predict(m, data.matrix(X[,covars]),
gp_coords_pred = matrix(-100, ncol = 2, nrow = dim(X)[1]),
group_data_pred = matrix(-1, ncol = 1, nrow = dim(X)[1]),
pred_latent = TRUE)$fixed_effect)
plot(imp <- light_interaction(fl, v = covars, pairwise = TRUE)) +
ggtitle("H interaction statistic") # takes a few seconds

H 交互统计量 — 作者提供的图像
# Partial dependence plots
gpb.plot.partial.dependence(gpboost_model, data[,covars], variable = 2, xlab = covars[2],
ylab = "gdp", main = "Partial dependence plot" )
gpb.plot.partial.dependence(gpboost_model, data[,covars], variable = 4, xlab = covars[4],
ylab = "gdp", main = "Partial dependence plot" )


K 和 edu 的部分依赖图 — 作者提供的图像
# Two-dimensional partial dependence plot (to visualize interactions)
i = 1; j = 3;# i vs j
gpb.plot.part.dep.interact(gpboost_model, data[,covars], variables = c(i,j), xlab = covars[i],
ylab = covars[j], main = "Pairwise partial dependence plot")

pop 和 L 的二维部分依赖图 — 作者提供的图像
扩展
不同时间段的随机效应
在上述模型中,我们对 2000 年和 2021 年使用了相同的随机效应。或者,也可以对不同时间段使用独立的空间和分组随机效应(在先验下独立,基于数据有依赖……)。在 GPBoost 库中,这可以通过 cluster_ids 参数完成。cluster_ids 需要是一个长度为样本量的向量,每个条目表示观测值所属的“集群”。不同集群具有独立的空间和分组随机效应,但超参数(例如,边际方差、方差成分等)和固定效应函数在集群之间是相同的。
下面我们展示了如何拟合这样的模型并创建两个单独的空间地图。如下结果所示,2000 年和 2021 年的空间效应相差较大。特别是,与 2000 年相比,2021 年的(残余)空间变异(或异质性)较少。这一点通过查看预测空间随机效应的标准差得到了证实,2000 年的标准差几乎是 2021 年的两倍(见下文)。进一步的结论是,2000 年有更多的“低”GDP 数值区域(空间效应低于 0),而 2021 年不再如此。
gp_model <- GPModel(group_data = data[, c("cl")], gp_coords = data[, c("Long", "Lat")],
likelihood = "gaussian", cov_function = "exponential",
cluster_ids = c(rep(1,242), rep(2,242)))
boost_data <- gpb.Dataset(data = data[, covars], label = data[, "y"])
params <- list(learning_rate = 0.01, max_depth = 1, num_leaves = 2¹⁰,
min_data_in_leaf = 10, lambda_l2 = 1)
# Note: we use the same tuning parameters as above. Ideally, the would have to be chosen again
gpboost_model <- gpboost(data = boost_data, gp_model = gp_model, nrounds = nrounds,
params = params, verbose = 0)
# Separate spatial maps for the years 2000 and 2021
pred <- predict(gpboost_model, group_data_pred = data[, c("cl")],
gp_coords_pred = data[, c("Long", "Lat")],
data = data[, covars], predict_var = TRUE, pred_latent = TRUE,
cluster_ids_pred = c(rep(1,242), rep(2,242)))
data_plot <- merge(shape, data.frame(FID = FID, y = pred$random_effect_mean[1:242]), by="FID")
plot_mu_2000 <- ggplot(data_plot) + geom_sf(aes(group=FID, fill=y)) +
scale_fill_viridis(name="Spatial effect", option = "B") +
ggtitle("Spatial effect for 2000 (mean)") + xlim(2700000,6400000) + ylim(1500000,5200000)
data_plot <- merge(shape, data.frame(FID = FID, y = pred$random_effect_mean[243:484]), by="FID")
plot_mu_2021 <- ggplot(data_plot) + geom_sf(aes(group=FID, fill=y)) +
scale_fill_viridis(name="Spatial effect", option = "B") +
ggtitle("Spatial effect for 2021 (mean)") + xlim(2700000,6400000) + ylim(1500000,5200000)
grid.arrange(plot_mu_2000, plot_mu_2021, ncol=2)
sd(pred$random_effect_mean[1:242]) # 0.2321267
sd(pred$random_effect_mean[243:484]) # 0.1286398

2000 年和 2021 年的单独空间效应 — 作者提供的图片
空间与固定效应预测变量的交互
在上述模型中,随机效应和固定效应预测变量之间没有交互。即,空间坐标与固定效应预测变量之间没有交互。可以通过将随机效应输入变量(即空间坐标或分类分组变量)额外包含在固定效应函数中来建模这种交互。下面的代码展示了如何做到这一点。正如下面的变量重要性图所示,坐标在树集成中未被使用,因此在该数据集中没有检测到这种交互。
gp_model <- GPModel(group_data = data[, c("cl")], gp_coords = data[, c("Long", "Lat")],
likelihood = "gaussian", cov_function = "exponential")
covars_interact <- c(c("Long", "Lat"), covars) ## add spatial coordinates to fixed effects predictor variables
boost_data <- gpb.Dataset(data = data[, covars_interact], label = data[, "y"])
params <- list(learning_rate = 0.01, max_depth = 1, num_leaves = 2¹⁰,
min_data_in_leaf = 10, lambda_l2 = 1)
# Note: we use the same tuning parameters as above. Ideally, the would have to be chosen again
gpboost_model <- gpboost(data = boost_data, gp_model = gp_model, nrounds = nrounds,
params = params, verbose = 0)
feature_importances <- gpb.importance(gpboost_model, percentage = TRUE)
gpb.plot.importance(feature_importances, top_n = 25, measure = "Gain",
main = "Variable importances when including coordinates in the fixed effects")

包含坐标的树集成中的变量重要性 — 作者提供的图片
大数据
对于大数据集,高斯过程的计算较慢,需要使用近似方法。GPBoost库实现了几种近似方法。例如,在GPModel构造函数中设置gp_approx="vecchia"将使用 Vecchia 近似。本文使用的数据集相对较小,因此我们可以准确地进行所有计算。
其他空间随机效应模型
上述中,我们使用了高斯过程来建模空间随机效应。由于数据是区域数据,另一种选择是使用依赖于邻域信息的模型,如 CAR 和 SAR 模型。这些模型目前尚未在GPBoost库中实现(可能会在未来添加 -> 联系作者)。
另一种选择是使用由分类区域 ID 变量定义的分组随机效应模型来建模空间效应。下面的代码展示了如何在GPBoost中实现这一点。然而,该模型实质上忽略了空间结构。
gp_model <- GPModel(group_data = data[, c("group", "cl")], likelihood = "gaussian")
(广义)线性混合效应和高斯过程模型
(广义)线性混合效应和高斯过程模型也可以在GPBoost库中运行。下面的代码展示了如何使用fitGPModel函数来实现这一点。请注意,需要手动添加一列 1 来包含截距。
X_incl_1 = cbind(Intercept = rep(1,dim(data)[1]), data[, covars])
gp_model <- fitGPModel(group_data = data[, c("cl")], gp_coords = data[, c("Long", "Lat")],
likelihood = "gaussian", cov_function = "exponential",
y = data[, "y"], X = X_incl_1, params = list(std_dev=TRUE))
summary(gp_model)
## =====================================================
## Model summary:
## Log-lik AIC BIC
## 195.97 -373.94 -336.30
## Nb. observations: 484
## Nb. groups: 2 (Group_1)
## -----------------------------------------------------
## Covariance parameters (random effects):
## Param. Std. dev.
## Error_term 0.0215 0.0017
## Group_1 0.0003 0.0008
## GP_var 0.0126 0.0047
## GP_range 7.2823 3.6585
## -----------------------------------------------------
## Linear regression coefficients (fixed effects):
## Param. Std. dev. z value P(>|z|)
## Intercept 16.2816 0.4559 35.7128 0.0000
## L 0.4243 0.0565 7.5042 0.0000
## K 0.6493 0.0212 30.6755 0.0000
## pop 0.0134 0.0097 1.3812 0.1672
## edu 0.0100 0.0009 10.9645 0.0000
## =====================================================
参考文献
-
Sigrist Fabio. “高斯过程提升”。机器学习研究杂志(2022 年)。
-
Sigrist Fabio. “潜在高斯模型提升”。IEEE 模式分析与机器智能汇刊(2023 年)。
-
数据集来源于 Eurostat;有关版权的更多信息,请参见 https://ec.europa.eu/eurostat/about-us/policies/copyright (“Eurostat 的政策是鼓励免费重用其数据,无论是用于非商业还是商业目的。”)
混合整数线性规划:正式定义与解空间
MILP 的理论基础以及其解空间的视觉表现
·
关注 发布于 Towards Data Science · 10 分钟阅读·2023 年 2 月 21 日
--
图片由 Ivan Bandura 提供,来源于 Unsplash
本文是关于混合整数线性规划(MILP)理论和应用系列的延续(上一篇文章)。今天,我们将探讨
-
MILP 的正式、一般定义,
-
以及其可行解空间的视觉表现。
第一部分描述了 MILP 模型的一般外观,哪些是有效的 MILP 表达式,哪些不是。第二部分展示了 MILP 背后的理论,这将在未来的帖子中讨论实际的 MILP 求解算法时非常有用。
这篇文章比较长,所以请拿杯咖啡,深入阅读吧!
混合整数线性规划的正式定义
在接下来的文本中,我将使用大写粗体字母(例如,A、E)表示矩阵,使用小写粗体字母表示向量(例如,x、y)。MILP 是一个优化问题,可以用矩阵表示法形式化如下:

让我们把这个难题拆解开来。我们得到一个由矩阵和向量A、E、b、f、c、d 代表的问题实例,这些矩阵和向量来自不同领域(ℝ代表实数集合,ℤ代表整数集合)。例如,如果我们回到第一个帖子中的预算问题,这个问题实例表示资产、它们的成本、估计的利润以及预算。我们说整数向量x和连续向量y是给定问题实例的可行解,如果满足以下条件:

通过逐项写出这些条件,我们得到:

其中 𝒂ᵢ 是矩阵 A 的行,𝒆ᵢ 是矩阵 E 的行。这些不等式被称为约束。
在 MILP 中,我们寻找一个可行解 x、y,以最小化目标函数。

如果存在这样的解,我们称之为最优解。如果你想知道,实际上也可以最大化目标函数;只需将其乘以 -1 并最小化得到的表达式。没有保证存在可行解(更不用说最优解了),但让我们将关于这些情况的讨论推迟到后面的部分。
尽管矩阵形式是定义 MILP 问题的规范方式,但模型设计者通常不使用它,因为它非常繁琐。相反,在实际应用中,我们使用简单的数学表达式定义约束和目标函数,这些表达式会被求解器内部转换为矩阵形式(这对机器来说非常方便)。为了了解什么是正确的 MILP 表达形式,什么不是,这里有一些例子:
-
𝑎₁𝑥₁ + 𝑎₂ + 𝑥₃ ≥ 𝑏₁ + 𝑎₄𝑥₄ 是一个正确的 MILP 约束。注意,与规范形式相比,不等式的方向被反转了,一个常数被包含在左侧,我还在右侧添加了一个变量。在规范形式中,这个约束会被转换为 −𝑎₁𝑥₁ − 𝑥₃ + 𝑎₄𝑥₄ ≤ −𝑏₁ + 𝑎₂。
-
𝑥₁𝑥₂ 不是一个正确的 MILP 表达式,因为它是两个变量的乘积,这不是线性表达式。|𝑥₁|, max(𝑥₁, 𝑥₂), log(𝑥₁) 等也是如此。所有这些表达式都不是线性的,尽管其中一些可以通过各种技巧 线性化,但更多内容将在另一篇文章中讨论。
-
𝑎₁𝑎₂ 是一个正确的 MILP 表达式,因为将两个固定参数相乘会在“编译”时间,即在构建矩阵模型但在启动求解器之前,得到一个单一的固定参数。经过数学函数(如对数)转换的固定参数也都是正确的 MILP 表达式。
-
𝑎ₓ 不是一个正确的 MILP 表达式。我们不能使用变量作为索引。
-
𝑎₁𝑥₁ < 𝑏₁ 不是一个正确的 MILP 约束,因为不可能强制严格不等式。
我们通过以矩阵形式展示前一篇文章中的预算问题的 MILP 公式来结束这一部分。

目标使用了 乘以 -1 的技巧来最大化预期利润。预算约束编码在约束矩阵的第一行,剩余的行对应于每个变量 𝑥ᵢ 上的约束 0 ≤ 𝑥ᵢ, 𝑥ᵢ ≤ 1(即 𝑥ᵢ 被要求为二进制)。许多求解器允许直接指定变量的域,因此通常不需要手动创建这些约束。此外,请注意我们的模型仅包括整数变量(没有 y 变量),因此它是一个 整数线性规划。
解决方案空间
当我们知道 MILP 模型的可行解空间是什么样子时,我们可以开始推理关于 MILP 的通用算法、理论属性以及如何使模型更高效。
让我们从以下 MILP 模型开始。为了简化,我们将只考虑两个整数变量 𝑥₁ 和 𝑥₂。

我们如何将这个模型图形化?我们从一个空的 2D 图开始,表示没有约束的模型。逐渐地,我们一个个地添加约束,直到我们拥有一个完整的模型。在图中,水平和垂直轴分别对应于 𝑥₁ 和 𝑥₂ 的值。

由于模型中当前没有约束,𝑥₁ 和 𝑥₂ 的每一种组合都是一个可行解。让我们添加第一个约束 4𝑥₁ ≥ 2𝑥₂ 并查看会发生什么。首先,在解决方案空间中绘制一条直线 4𝑥₁ = 2𝑥₂。

这条线是由以下不等式定义的两个半空间之间的边界:4𝑥₁ > 2𝑥₂ 和 4𝑥₁ < 2𝑥₂。线的一侧的所有点满足不等式 4𝑥₁ > 2𝑥₂,而另一侧的所有点满足不等式 4𝑥₁ < 2𝑥₂。当我想知道哪个半空间表示特定约束条件时,我会选择一些不在该线上的随机点 𝑥₁, 𝑥₂,将其代入约束条件,并检查约束条件是否得到满足。如果满足,那么包含此点的半空间就是对应于该约束条件的半空间。如果不满足,那么另一个半空间就是正确的。
举个例子,我们选择点 𝑥₁ = 5, 𝑥₂ = 3。将这些值代入约束条件

我们可以看到约束条件得到了满足。因此,包含此点的半空间是所有点都满足该约束条件的半空间。在图中,我将用箭头表示满足约束条件的半空间。当然,线上的点也满足约束条件,所以一个约束条件由边界线和半空间共同表示。

让我们添加另一个约束条件 −2𝑥₁ + 30 ≥ 2𝑥₂。我们从绘制边界线 −2𝑥₁ + 30 = 2𝑥₂ 开始。

哪个半空间满足这个约束条件?对于这个测试,我们选择点 𝑥₁ = 14, 𝑥₂= 6,并将其代入约束条件

我们发现选定的点不满足约束条件 −2𝑥₁ + 30 ≥ 2𝑥₂,因此可行的半空间必须是另一个,而不是包含点 𝑥₁ = 14, 𝑥₂= 6 的那个。

然而,现在模型中有两个约束条件, feasible 解决方案空间是每个约束条件的可行半空间的交集。原因在于我们希望所有的约束条件都得到满足,因此只有那些位于所有可行半空间中的解决方案才在整个模型中是可行的。

现在应该清楚如何处理约束条件 𝑥₂ ≥ 0, 0.5𝑥₁ + 3.75 ≥ 𝑥₂,所以我将直接绘制新的可行解空间。

还有一件事就是我们最初默默忽略的整数约束 𝑥₁, 𝑥₂ ∈ ℤ。这条约束意味着什么?简单来说,并不是所有彩色区域中的点都是实际可行的,只有那些 𝑥₁ 和 𝑥₂ 都是整数的点才是可行的。我们可以通过网格将它们可视化,因此,示例 MILP 模型的最终可行解空间如下。

示例 MILP 模型的可行解空间。可行解用红点表示,黑线表示约束条件的边界。
每一个红点都对应一个满足所有约束的可行整数解。现在,最后一个问题是:这些点中哪个优化了我们的目标函数,即哪个点给出了最小的 -2𝑥₁ − 3𝑥₂ 值?这个问题稍微复杂一点。我们可以将图扩展到第三维,代表目标函数的值,将可行解投影到目标函数的平面上,然后找到投影点中最低的点。然而,我认为在 2D 空间中找到最优解是一个很好的思维练习。
你们中的许多人可能对梯度这个概念很熟悉,因为它是许多机器学习训练算法的核心。简而言之,函数的梯度表示在某一点函数最大增加的方向。也就是说,如果我们沿梯度方向移动,我们将增加函数值。知道 MILP 目标函数的梯度可以告诉我们去哪里寻找最优可行解。所以,让我们找到目标函数的梯度,我们通过对每个变量 𝑥₁, 𝑥₂ 进行偏导数来获得。

由于我们的目标函数是线性的,它在解空间中的每一点梯度都是相同的。由于梯度是一个方向,我们可以在解空间中绘制一个向量,以显示目标函数增加的方向。

让我们绘制目标的等高线(等高线上的每一点目标值相同)。可以证明等高线与梯度垂直,因此一旦我们有了梯度,就很容易绘制等高线。随着我们沿梯度的相反方向移动,等高线上的目标值会减少,直到我们无法再移动,因为我们将超出可行空间。你可以把这个过程想象成将一把与梯度垂直的尺子沿梯度的相反方向移动,直到你触及最后一个可行解。在我们的例子中,最优解是 𝑥₁ = 8, 𝑥₂ = 7,目标值为 -37。呼!

如果你对如何在 Python 中编写这个 MILP 模型感兴趣,这里是代码。
Solver status: OptimizationStatus.OPTIMAL
Optimal objective value: -37
Optimal solution: x1=8, x2=7
优化结果
在上面的例子中,我们找到了一个最优解。然而,这并不总是有保证的。理论上,优化可能会得出以下三种结果之一:找到最优解、模型不可行或目标无界。
其他结果也是可能的(例如,超时、内存不足等),但这些与整数规划的实践相关,而不是理论,所以我不会在这篇文章中讨论它们。
优化结果:找到最优解
我们已经看到了一些找到最优解的例子(例如,预算问题)。我想强调的是,可能存在多个具有相同目标值的最优解。例如,如果我们将上面定义的模型中的目标替换为 10𝑥₂,那么在可行解空间中所有位于 x₂ = 0 线上的点都是最优解,目标值为 0。多次运行优化求解器并使用不同的随机种子可能会给出不同的最优解,因此不要期望每次都返回唯一解。
优化结果:不可行模型
不可行的模型是指没有满足所有约束条件的可行解的模型。一个不可行模型的例子如下。

没有任何一个 𝑥₁ 的值可以同时满足 𝑥₁ ≥ 6 和 𝑥₁ ≤ 2,因此这些约束的可行半空间的交集是一个空集。

优化结果:无界目标
这有点奇怪,但不幸的是,这种情况可能会发生。通常在实践中,这表明模型定义不明确,例如,当一个重要的约束没有包含在模型中时。
无界目标意味着对于每个可行解,我们可以找到另一个具有更小目标值的可行解。换句话说,最优目标值趋向于负无穷。以下是一个具有无界目标的模型。

可行解空间没有约束,因此我们可以将 𝑥₂ 增加到我们想要的任何值,从而目标函数值将减小。
总结
现在,你应该能够仅使用笔、纸和直尺来解决任何具有两个整数变量的 MILP 模型!当然,解决 MILP 的实际计算机算法要复杂得多,但对解空间的直观理解将在我们未来的文章中学习算法时派上用场。
除非另有说明,所有图片均由作者提供。
Mixtral-8x7B: 理解和运行稀疏专家混合模型
如何高效超越 GPT-3.5 和 Llama 2 70B
·
关注 发布于 Towards Data Science ·6 分钟阅读·2023 年 12 月 15 日
--
最近的大型语言模型(LLMs)使用了非常相似的神经网络架构。例如,Falcon、Mistral 和 Llama 2 模型使用了类似的自注意力和 MLP 模块组合。
相比之下,Mistral AI(也创造了 Mistral 7B)刚刚发布了一种具有显著不同架构的新 LLM:Mixtral-8x7B,这是一个稀疏混合的 8 个专家模型。
总体而言,Mixtral 包含 46.7B 参数。然而,得益于其架构,Mixtral-8x7B 可以高效地在消费者硬件上运行。与其他相似规模的模型相比,Mixtral-8x7B 的推理速度显著更快,并且在大多数任务中表现优越。
在这篇文章中,我解释了什么是稀疏专家混合网络,以及为什么它在推理时比标准模型更快。接着,我们将看到如何在消费者硬件上使用和微调 Mixtral-8x7B。
我已经在这里实现了一个笔记本,展示了使用 Mixtral-8x7B 进行 QLoRA 微调和推理:
稀疏专家混合网络

图片由作者提供
稀疏专家混合网络(SMoE)是一种神经网络架构,旨在提高传统模型的效率和可扩展性。专家混合的概念是为了使模型能够通过专门的“专家”子网络学习输入空间的不同部分。在 Mixtral 中,共有 8 个专家子网络。
请注意,模型名称中的“8x7B”略有误导。该模型总共有 46.7B 参数,比 8x7B 参数应该有的量少了近 10B 参数。实际上,Mixtral-8x7B 并不是一个 56B 参数的模型,因为一些模块,比如自注意力模块,与 8 个专家子网络共享。
如果你用 Transformers 加载并打印模型,模型的结构会更容易理解:
MixtralForCausalLM(
(model): MixtralModel(
(embed_tokens): Embedding(32000, 4096)
(layers): ModuleList(
(0-31): 32 x MixtralDecoderLayer(
(self_attn): MixtralAttention(
(q_proj): Linear4bit(in_features=4096, out_features=4096, bias=False)
(k_proj): Linear4bit(in_features=4096, out_features=1024, bias=False)
(v_proj): Linear4bit(in_features=4096, out_features=1024, bias=False)
(o_proj): Linear4bit(in_features=4096, out_features=4096, bias=False)
(rotary_emb): MixtralRotaryEmbedding()
)
**(block_sparse_moe): MixtralSparseMoeBlock(
(gate): Linear4bit(in_features=4096, out_features=8, bias=False)
(experts): ModuleList(
(0-7): 8 x MixtralBLockSparseTop2MLP(
(w1): Linear4bit(in_features=4096, out_features=14336, bias=False)
(w2): Linear4bit(in_features=14336, out_features=4096, bias=False)
(w3): Linear4bit(in_features=4096, out_features=14336, bias=False)
(act_fn): SiLU()
)
)**
)
(input_layernorm): MixtralRMSNorm()
(post_attention_layernorm): MixtralRMSNorm()
)
)
(norm): MixtralRMSNorm()
)
(lm_head): Linear(in_features=4096, out_features=32000, bias=False)
)
我们可以看到 MoE 模块与自注意力模块是分开的。
每个专家子网络负责处理输入数据的特定区域或方面,而一个门控(或路由器)网络决定每个专家对最终预测的贡献程度。在 Mixtral 的情况下,只有 2 个专家会同时激活。在推理过程中仅使用 13B 参数,因此相比于其他相似规模的模型,推理更加高效。
总结来说,这种稀疏激活在几个方面可能会带来好处:
1. 计算效率:仅激活一部分专家可以降低评估每个输入时整个专家池的计算成本。
2. 参数效率:通过仅允许少数专家对给定输入进行激活,模型可以更有效地分配其参数。
3. 泛化:稀疏激活可以鼓励模型学习输入空间不同区域的更多专门和细化的特征。这可以导致在多样化输入上的更好泛化和性能。例如,Mistral AI 声称 Mixtral 已在意大利语、德语、法语、西班牙语以及编程方面进行过训练。我们可以想象,他们可能已训练了其中的一些 8 个专家,使其在这些语言和任务上表现得特别好。
虽然 SMoEs 比相似大小的标准模型更快,但它们仍然占用相同的内存量。量化是减少内存消耗的一种方法。将一些专家卸载到较慢的内存,例如 CPU RAM,也可能是一个不错的替代方案,尤其是当我们提前知道哪些专家将最常用于特定任务时。
关于 Mixtral 的信息
我们知道的大部分细节都发布在 Mistral AI 的这篇文章中:
Mixtral 是一个具有 8 个子网络的模型,这些子网络作为专家存在。在每一层和每个 token 上,只有 2 个子网络被激活。另一个参数集合,即路由网络,决定激活哪些子网络。
关于训练数据,Mistral AI 并没有给出太多信息。我们只知道它是 Web 数据:
Mixtral 是在从开放 Web 中提取的数据上进行预训练的
我想知道“开放 Web”是什么。
他们在各种公共基准测试中评估了模型,以证明 Mixtral 在大多数测试中优于 Llama 2 70B 和 GPT-3.5。
目前我们了解的就是这些。
在你的电脑上运行 Mixtral
Mixtral 已由 Hugging Face Transformers(从版本 4.36.0 起)和 bitsandbytes 支持。我们可以对模型进行量化,以便在消费级硬件上进行微调或运行。
它还支持 FlashAttention 2,这有助于减少推理和长序列(最多 32k tokens)微调的内存消耗。
在模型卡中,Mistral AI 提供了一个运行模型的代码示例:
from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "mistralai/Mixtral-8x7B-v0.1"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, load_in_4bit=True, use_flash_attention_2=True)
text = "The mistral is"
inputs = tokenizer(text, return_tensors="pt").to(0)
outputs = model.generate(**inputs, max_new_tokens=200)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
在 Google Colab 笔记本中,它仅与 A100 GPU 一起工作(你需要 Google Colab Pro)。该模型有 46.7B 个参数。由于参数是 bfloat16,我们每个参数需要 2 字节的内存,即几乎 100 GB 的硬盘空间来下载模型。
该模型以 safetensors 格式提供。它分为 20 个约 4.95 GB 的分片。下载完成后,加载模型到 GPU 上可能需要最多 7 分钟。
当量化到 4-bit 时,模型占用 23 GB 的 VRAM。为了在你的电脑上流畅运行,它需要至少两个每个 16 GB VRAM 的 GPU,例如 2 个 NVIDIA RTX 4060 16 GB,或更好的 2 个 NVIDIA RTX 4080 16 GB。
在你的电脑上微调 Mixtral
Mixtral 还支持 QLoRA 微调。以下是加载模型并准备进行微调的代码示例:
model_name = "mistralai/Mixtral-8x7B-v0.1"
#Tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name, add_eos_token=True, use_fast=True)
tokenizer.pad_token = tokenizer.unk_token
tokenizer.pad_token_id = tokenizer.unk_token_id
tokenizer.padding_side = 'left'
compute_dtype = getattr(torch, "float16")
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
model_name, quantization_config=bnb_config, device_map={"": 0}
)
model = prepare_model_for_kbit_training(model)
或者你可以直接参考我的 之前的 Mistral 7B 微调教程。效果会是一样的。
你也可以尝试通过 IPO 进行微调,来创建一个强大的 Mixtral 聊天模型。
Mistral 7B 与 IPO 对齐
kaitchup.substack.com](https://kaitchup.substack.com/p/fine-tune-better-chat-models-with?source=post_page-----0e3fc7fde818--------------------------------)
结论
稀疏专家混合模型是一种高效的模型架构,它比相似规模的标准模型具有更快的推理速度。尽管 Mixtral 在推理时只使用了大约 1/4 的参数,但仍然需要将所有参数加载到内存中。
减少 LLM 内存占用的一种方法是量化。例如,我们可以使用 bitsandbytes 的 NF4 将 Mixtral 量化为 4 位。这样就可以在消费级硬件上运行或微调 Mixtral,但你至少需要两块 GPU。
希望 Mistral AI 会发布一篇技术论文,透露有关训练 Mixtral 8 位专家的方法的更多信息。
混合模型、潜变量和期望最大化算法
·发表于Towards Data Science ·阅读时长 8 分钟·2023 年 3 月 19 日
--

无监督学习一直让我着迷。这是一种无需手动标记的数据学习方式,并且可以识别数据集中的模式。在各种无监督学习技术中,最简单的就是聚类。从本质上讲,聚类算法旨在找到彼此相似的数据点。通过将数据点进行聚类,我们可以获得关于数据集的有价值见解,以及各个聚类所代表的内容。
本文旨在深入探讨高斯混合模型聚类算法,它如何建模数据,更重要的是如何使用期望最大化来将模型拟合到新数据集上。
什么是混合模型?
实质上,混合模型(或混合分布)是将多个概率分布组合成一个单一的分布。

混合模型的 PDF
为了将这些分布组合在一起,我们为每个组件分布分配一个权重,以使分布下的总概率和为 1。一个简单的例子是由 2 个高斯分布组成的混合分布。我们可以有 2 个均值和方差不同的分布,并使用不同的权重将它们结合起来。


2 个均值相同但 π 值不同的 GMMs — 图片由作者提供
具体来说,我们可以将这种分布视为由一个两步生成过程产生。在这个过程中,一个数据点可以从 n 个不同的概率分布中生成。首先,我们确定它来自哪个概率分布。这个概率就是权重 π_i。一旦选择了组件概率分布,数据点可以通过模拟组件概率分布本身来生成。
GMM
高斯混合模型本质上是一个混合模型,其中 所有组件分布都是高斯分布。

GMM 模型的概率密度函数
现在让我们尝试理解为什么使用高斯分布来建模混合组件。当查看数据集时,我们希望将相似的点聚集在一起。这些聚类通常是球形或椭圆形的,因为我们希望相近的点被聚集在一起。
因此,正态分布是聚类的一个良好模型。分布的均值将是 聚类的中心,而 形状 和 分布 可以通过 分布的协方差 来很好地建模。
聚类的第二个变量将是 不同聚类的相对大小。在一个真实数据集中,我们通常不期望聚类的大小相同,这意味着一些聚类将比其他聚类包含更多的点。聚类的大小将由聚类权重 π_i 决定。
在聚类的背景下,我们假设有 k 个影响因素影响数据的生成。每个影响因素有一个 不同的权重,对应于聚类权重 π。
定义术语
在深入模型推导之前,我们首先定义一些关键术语。

聚类责任的数学函数
-
θ:这是指 GMM 模型的参数,包括聚类的均值和协方差 θ = {μ, Σ,π}。因为每个聚类都有相应的参数,所以有 k 个 θ 值。
-
y_n — 这是指数据集中的第 n 个数据点
-
z_n — 这是指第 n 个数据点所属的聚类
-
r_nk — 聚类责任(在下一节中解释)
拟合 GMM 模型
既然我们已经有了模型,我们希望利用它来洞察数据。对于一个聚类问题,这将是预测的聚类分配 z_n^。与其求解确定的聚类分配,GMM 模型允许我们计算聚类概率。
这个值r_nk是聚类k对第 n 个数据点的影响,被称为聚类责任。这种方法被称为软聚类,其中每个数据点按比例分配到每个聚类,而不是直接分配到一个单一聚类。作为概率分布,向量r_nk对所有点的总和为 1。在数学上,这个值定义为:

聚类责任定义
从定义中,我们可以看到这是给定数据 y_n 和参数θ的 z_n 的后验概率。
这种方法的好处在于识别模型对数据点的不确定性。或者,如果我们想找到最可能的聚类,我们可以通过将每个数据点分配到具有最高责任的聚类来将结果转换回硬聚类。

针对硬聚类的聚类责任的 Argmax 函数
使用贝叶斯规则,我们可以进一步细化后验分布:

使用贝叶斯规则扩展聚类责任
在分子中,后验分布被分解为先验p(z_n=k|θ)和似然p(y_n|z_n=k, θ)。分母是归一化常数,它考虑了所有可能的聚类。
在这里,我们也遇到了解决后验分布的问题。我们无法在首先不知道参数估计θ的情况下解决责任问题。同时,在不知道聚类责任的情况下,也无法得出良好的责任估计。因此,我们使用期望最大化算法,它用于解决潜在变量模型。
EM 算法
潜在变量是数据集中从未被明确观察到的变量。在这种情况下,潜在变量是聚类分配z_n。
该算法是一个迭代的 2 步算法。它从随机初始化的参数θ值开始,然后在每次迭代中逐步优化对潜在变量z_n和参数θ的估计。每次迭代有 2 个步骤:
- E 步:在 E 步中,我们尝试估计给定当前对θ的估计的潜在变量z_n的概率分布。这等同于构造给定 Z 和θ的条件对数似然的期望。

给定当前参数的期望条件对数似然
- M 步:在 M 步中,我们生成一组新的θ估计值,给定当前期望的 Z(来自 E 步)。这是通过最大化 M 步中相对于参数θ的项来完成的。
如果上述步骤听起来很混乱,不用担心!让我们在 GMM 的背景下看这个例子。
GMM 的 E 步

让我们尝试求解之前推导的集群责任方程。
对于分子,我们有第一个项p(z_n=k|θ),这是数据点属于集群k的概率。这个项不考虑似然,因此将只是π_k。第二个项p(y_n|z_n=k,θ)是在属于集群 k 的情况下的似然,这是高斯似然。实质上,这个概率变为:

简化集群责任的推导
对于数据集中的每个点,我们计算r_n,一个大小为k的向量,表示点属于每个集群的概率。由于我们有随机初始化的μ和Σ,这个问题是可以解决的。一旦 E 步完成,我们可以进入 M 步。
GMM 的 M-Step
在 M 步中,我们使用E 步中的集群责任来获得集群参数的更好估计。有 3 个参数{π, μ, Σ},我们可以进行这种优化。
第一个参数π_k代表一个分类分布,并具有简单的 MLE 估计。

GMM 的π的 MLE 估计
对于数据集中的每个点,我们有点属于集群k的概率。如果我们在数据集中取平均,这将给我们提供给定数据的最可能的π值。
对于μ和Σ,它们可以以类似于高斯 MLE 的方式进行优化。不同之处在于r_nk作为计算的加权参数。

GMM 的μ和Σ的 MLE 估计
对于参数μ,我们对与集群k相关联的特征取加权平均。我们对方差执行相同的过程,取加权平均,这给我们提供了Σ的估计。
使用我们改进的参数,我们完成了 EM 算法的 1 次迭代。我们可以重复相同的步骤,直到算法收敛,参数变化很小。
与 K-Means 的区别
尽管 GMM 模型可能更难实现,但它们比更常用的 K-Means 算法具有两个独特的优势。
- 椭圆形集群
-
K-Means 使用 L2 距离,这导致集群在本质上是球形的。
-
GMM 允许更多的灵活性。首先,不同的方差(对角线条目)可以解释特征扩展的相对差异。
-
允许协方差矩阵具有非对角条目也可以允许
2. 软聚类
- 如前所述,GMM 模型允许软聚类,其中每个数据点以部分的方式分配到不同的集群。
有趣的是,K-Means 算法可以看作是 GMM 的一种特殊形式。如果我们将算法限制为仅具有单位协方差矩阵(Σ = I),则参数优化将是最小化 L2 损失函数。这与 K-Means 相同,其中最小化数据点与簇之间的平方距离。
同时,为了将软聚类转换为硬聚类,我们可以将簇设置为最可能的簇。通过取argmax,在 E 步骤中,每个数据点只被分配到一个簇。这与 K-Means 相同,其中每次迭代都将数据点重新分配到最近的簇。
在未来的博客文章中,我将详细讲解一个“从头开始”的 Python 实现 EM 算法,并探索拟合 GMM 模型的一些特征。
PINNs 的专家混合(MoE-PINNs)
利用集成方法提升物理信息神经网络
·
关注 发表在 Towards Data Science · 9 分钟阅读 · 2023 年 2 月 2 日
--
图片由 Soviet Artefacts 提供,来源于 Unsplash
物理信息神经网络(PINNs) [1] 已成为解决偏微分方程(PDEs)的一种流行且有前途的方法。在我们最新的研究中,我的同事迈克尔·克劳斯和我介绍了一种名为用于解决具有复杂和多样模式的 PDEs 的混合专家物理信息神经网络(MoE-PINNs) [2] 的新框架,它在 PDEs 方面显示出巨大潜力。
在本文中,我们将讨论 MoE-PINNs 的好处以及如何轻松实现它们来解决各种 PDE 问题。结构如下:
-
MoE-PINNs 入门
-
使用 MoE-PINNs 解决 Burgers' PDE
-
通过稀疏正则化减少超参数搜索
-
通过解决 L 形域上的泊松 PDE 的 MoE-PINNs 示例
-
使用可微 MoE-PINN 进行自动体系结构搜索
为了帮助您更好地理解本文中讨论的概念,我们提供了伴随的笔记本,可以直接在 Colab 上运行:
PINNs 利用物理定律和自动微分,仅需几行代码就能解决偏微分方程(PDEs)。然而,它们也对超参数非常敏感,例如激活函数或权重初始化。这使得训练 PINNs 特别困难,是一个费力的迭代过程。网络的深度和激活函数的选择可以极大地影响解决方案的准确性。例如,对于具有不同模式和不连续性的复杂 PDEs,如 Navier-Stokes 方程,深度网络效果良好。而对于在正方形域上的泊松方程这样具有简单模式的简单 PDEs,浅层网络可能已经足够。正弦激活函数以其在微分下保持形状的特性,可能是高阶微分问题的理想选择。另一方面,像 swish 或 softplus 这样的激活函数可以更好地处理具有尖锐不连续性的问题。
但是,如果您的问题需要结合两者呢?如果我们处理的 PDE 在一部分平滑重复,在另一部分则是高度复杂且具有尖锐不连续性?这就是混合专家(MoE)框架对 PINNs 的贡献之处。通过利用多个网络和一个门来划分域,每个专家可以专注于解决问题的不同部分,从而提高准确性并减少偏差-方差的折衷。
将问题划分为较小的子问题有很多好处:
-
通过在不同子领域使用多个学习者,问题的复杂性得以降低。
-
MoE-PINNs 中的门控是一个连续函数,从而在领域之间实现平滑过渡。因此,领域中更复杂的区域可以在多个学习者之间平均分配,而较简单的区域则可以分配给单个专家。
-
该门控可以是任何类型的神经网络,从线性层到深度神经网络,这使得它能够适应不同类型的领域,并以任意方式进行划分。
-
MoE 可以很容易地并行化,因为只需在不同设备上的学习者之间共享权重 lambda。理论上,每个学习者可以放置在一个不同的 GPU 上。
-
通过初始化大量具有不同架构的 PINNs,可以减少劳动密集型的超参数调优需求。
MoE-PINN 架构

PINNs 的专家混合。一个任意数量 m 的 PINNs,可能具有不同的架构和属性,与一个门控网络一起初始化。所有模型接收相同的输入,门控网络生成的权重用于聚合结果。图示由作者提供 [2]。
与传统的 PINNs 使用单一神经网络进行预测不同,MoE-PINNs 采用多个 PINNs 的集成,并通过门控网络进行组合。
与集成中的所有 PINNs 一样,门控网络是一个全连接的前馈网络,它接收空间坐标 x 和 y(可能会因 PDE 而异)。然而,与 PINNs 不同的是,它的输出是一个 m 维的权重向量,其中 m 是集成中的 PINNs 数量。这些权重经过 softmax 函数处理,以转换为概率分布,确保所有权重之和等于 1。
softmax 函数将未归一化的重要性 z_i 映射到概率分布 p。
MoE-PINN 的最终预测是通过聚合集成中每个 PINN 的预测结果获得的,权重由各自的重要性决定。
MoE-PINNs 可以在 TensorFlow 中非常简洁地构建,只需几行代码:
import tensorflow as tf
from tensorflow.keras.layers import Input, Dense, Concatenate
def build_moe_pinn(pinns:List[tf.keras.Model], n_layers:int, n_nodes:int) -> tf.keras.Model:
x = Input((1,), name='x')
y = Input((1,), name='y')
# create predictions with each PINN in the ensemble
u = Concatenate()([pinn([x, y]) for pinn in pinns])
# initialise the gating network
gate = tf.keras.models.Sequential(
[Dense(n_nodes, activation='tanh') for _ in range(n_layers)] + \
[Dense(len(pinns), activation='softmax')],
name='gate',
)
# receive importances from the gating network, multiply with PINN
# predictions and sum up the results
u = tf.reduce_sum(gate([x, y]) * u, axis=1, keepdims=True)
return tf.keras.Model([x, y], u, name='moe_pinn')
Burgers’ 方程
为了说明 MoE-PINNs 的有效性,我们来看一个例子。Burgers’ 方程是一种用于建模冲击波、气体动力学或交通流等现象的 PDE。其形式如下:
Burgers’ 方程,空间坐标 x 和时间变量 t。初始条件设定为正弦函数,边界条件设定为零。

使用上述初始条件和边界条件可视化 Burgers’ 方程。图片由作者提供。
Burgers’ PDE 提出了一个有趣的挑战:它随着时间的推移,从平滑的正弦函数过渡到陡峭的阶跃型不连续性。这一独特属性使其成为评估 MoE-PINNs 性能的理想基准。
训练 MoE-PINNs 以解决 Burgers’ 方程
让我们初始化一个包含 5 个 PINNs 和一个门控网络的 MoE-PINN,并在 Burgers 方程上进行训练。这些专家具有以下架构:
-
专家 1: 2 层,每层 64 个节点,激活函数为 tanh
-
专家 2: 2 层,每层 64 个节点,激活函数为 sine
-
专家 3: 2 层,每层 128 个节点,激活函数为 tanh
-
专家 4: 3 层,每层 128 个节点,激活函数为 tanh
-
专家 5: 2 层,每层 256 个节点,激活函数为 swish

使用 5 个专家的 MoE-PINN 对 Burgers 方程进行预测和与实际结果(谱元方法)的平方误差比较。图像来源:作者。
更有趣的是,我们现在可以检查专家在领域中的分布以及它们的单独预测情况:

由门控网络产生的权重 λ(顶部行)对于每个专家(列),以及每个专家对 Burgers 方程的预测(底部行)。图像来源:作者。
观察 MoE-PINNs 中的门控网络如何根据每个专家在建模领域不同部分的能力有效地分配任务。较少层数和节点的专家被分配到平滑区域,这些区域相对容易建模,即接近初始条件。与此同时,更复杂的专家,即具有更深更宽架构的专家,被利用在具有不连续性的区域,这些区域需要更复杂的模型来准确表示。特别是在专家 3 的情况下,它完全致力于捕捉不连续性。
稀疏性正则化以减少超参数搜索
MoE-PINNs 通过允许初始化一组多样化的专家,减少了调整多个超参数的需求。然而,仍有一个重要的超参数:专家数量 m。为了获得最佳结果,m 应该尽可能高,同时仍能适应内存。但专家数量过多也会增加计算成本。为了平衡这些权衡,重要的是确定将物理领域划分为最佳所需的最小专家数量。一种方法是通过添加一个正则化项来鼓励由门控网络产生的稀疏权重 λ。
正则化损失可以表示为:
用于强制门控网络产生的权重 λ 稀疏的正则化项。
其中 B 是一批协同点 (x, t),p 是控制正则化强度的超参数。p 值低于 1 强制稀疏,而 p 值高于 1 导致更均匀的分布。为了鼓励稀疏性,p 的一个良好起始点是 0.5。
最后,为了使训练过程更加高效,我们可以使用启发式方法来排除门控网络认为不重要的专家。例如,如果某专家在一个批次中的平均权重低于某个阈值,则可以排除该专家。
让我们看一个其他的例子来说明这一过程。
L 形区域上的泊松 PDE
泊松方程是用于建模工程和自然科学中物理过程的常用工具。例如,它可以用来解决杆在扭转负荷下的弹性静力学问题。为了测试稀疏性正则化,让我们检查 MoE-PINNs 在解决具有均匀 Dirichlet 边界条件的二维 L 形区域上的泊松方程时表现如何:
L 形区域上的泊松方程,其中 Gamma 代表 L 形区域边界上的点。

上述 L 形区域上泊松方程的可视化,使用有限元方法解决。图示由作者[2]提供。
如果工程师必须对这个领域进行细分并使用不同的模型,一个直观的选择是在 L 形区域的三个象限中使用不同的专家:一个在左上角,一个在左下角,一个在右下角。观察 MoE-PINN 如何决定划分领域并分配专家将会很有趣。
在泊松 PDE 上训练稀疏 MoE-PINNs
当用四个相同的专家初始化一个集合时,MoE-PINN 的结果如下:

使用 4 个专家的 MoE-PINN 在泊松方程上的预测和平方误差与实际值(FEM 解)对比。图示由作者[2]提供。
但更重要的是,我们现在可以检查每个专家的重要性:

门控网络生成的权重 lambda(顶行)以及每个专家(列)的预测(底行)在泊松方程中的表现。图示由作者[2]提供。
图示表明,在稀疏性正则化的影响下,门控网络决定几乎完全排除专家 1。这导致了在剩余的 PINNs 之间更加高效和有效的领域划分。网络将一个主要专家分配给三个象限中的每一个,创建了一个对称且直观的分布。
同样值得注意的是,由于专家 1 的平均重要性较低,如果启动新的训练,这个专家可能会被从集合中排除,剩余的专家可以在一个减少规模的、更高效的集合中进行微调。
可微分架构搜索
最后,我们希望利用引入的概念来减少调整超参数所需的时间。MoE-PINNs 允许初始化一个多样化的专家集合,并让门控网络决定应该保留哪些专家,哪些专家可以在稀疏正则化下被丢弃。

在对泊松方程的三个专家(左)和四个专家(右)使用不同激活函数时的重要性。图由作者[2]提供。
出人意料的是,当分析使用不同激活函数的多样化专家集合时,门控网络始终丢弃使用 tanh 激活的网络,尽管 tanh 是 PINN 文献中常用的激活函数。相反,门控网络始终偏爱使用正弦激活的专家。这种偏好表明,使用正弦激活网络的集合可能会提高 PINN 的性能,这与使用傅里叶变换进行信号分解的原则一致,表明任何函数都可以表示为不同频率的正弦函数的组合。

在对泊松方程的四个专家进行深度(左)和相同专家的深度(右)变化时的重要性。图由作者[2]提供。
从不同深度和宽度的专家集合来看,对于泊松方程,二层或三层的深度似乎是最佳选择,而较宽的网络似乎优于较窄的网络。
结论
总之,MoE-PINNs 是一种很好的扩展,能改善在表现出不同模式的 PDEs 上的 PINNs,并通过让门控网络决定从多样化的专家集合中使用哪些架构,减少调整超参数所需的时间。
如果你想自己试验 MoE-PINNs,可以查看以下笔记本:
非常感谢你读完这篇文章!如果你觉得这篇文章对你有帮助,并希望在自己的工作中使用 MoE-PINNs 或这些笔记本,请使用以下引用:
R. Bischof 和 M. A. Kraus, “Mixture-of-Experts-Ensemble Meta-Learning for Physics-Informed Neural Networks”,《33rd Forum Bauinformatik 会议论文集》,2022
你可以在 mkrausai.com 查找有关我的同事 Michael Kraus 的更多信息,也可以在 rabischof.ch 了解我自己。
[1] M. Raissi、P. Perdikaris 和 G. E. Karniadakis,《物理信息神经网络:一种解决涉及非线性偏微分方程的前向和逆向问题的深度学习框架》,《计算物理学杂志》378(2019),686–707。
[2] R. Bischof 和 M. A. Kraus,“物理信息神经网络的专家组合元学习”,第 33 届建筑信息学论坛论文集,2022
深度会话推荐系统中的 Softmax 混合模型
原文:
towardsdatascience.com/mixture-of-softmaxes-for-deep-session-based-recommender-systems-aea5727e213d
现代会话基础的深度推荐系统在某种程度上可能会受到 softmax 瓶颈的限制,就像它们的语言模型类似物一样。
·发表于 Towards Data Science ·15 分钟阅读·2023 年 7 月 18 日
--

由 Preethi Viswanathan 拍摄的照片,来源于 Unsplash
TL;DR:
传统的 softmax 在完全建模像自然语言这种高度依赖上下文的任务时能力有限。这种表现力的限制被称为 Softmax 瓶颈,可以通过矩阵分解和研究结果矩阵的秩来描述。
Yang 等人提出的 Softmax 混合模型 by Yang et al. [1] 通过增加网络的表现力而不增加参数数量,从而克服了瓶颈。
在这项工作中,我研究了这一限制是否也适用于深度会话推荐系统,并且 MoS 技术是否能带来好处。我实现了这一技术,并将其应用于 Gru4Rec 架构,使用了Movielens-25m 数据集 [3]。结果是,这取决于……
介绍
在之前的工作中,当我在一个大规模的序列基础深度推荐系统上工作时,产品团队指出了一个用例,那个时候推荐的质量确实很低。推荐的项目与用户正在观看的内容不一致,可以说它们并不是公司所提供的最优质的内容。
那么到底发生了什么呢?经过仔细检查,我们理解到这特别涉及到由内容团队手动推送到首页显示的条目(这更是一个问题)。这些条目在提取出来用于学习的用户序列中出现了很多。一开始,这让人相当困扰,因为我们知道并且以前已经证明了,算法从包含特定条目的用户序列中学习得越多,该特定条目的推荐质量就越高。

性能随着数据量的典型增长 — 作者提供的图片
在这种情况下,这是一个数据量更多并不总是更好的完美例子。具有给定特征项的用户序列虽然很多,但却不一致。我们从一个主题(在首页显示的)切换到一个完全不同的主题。这是因为落在首页的用户的多样性。

作者提供的图片
这让我感觉到这种多样性让算法感到困惑。但这更多是一种直觉,而不是一个有科学依据的事实。
几天前,我偶然发现了一篇论文“突破 Softmax 瓶颈:高阶 RNN 语言模型”[1](发表于 ICLR 2018),这篇论文给了我一个更正式的框架来理解为什么大多数分类神经网络无法处理高上下文多样性。在这篇博客文章中,我们将首先仔细看看这个问题,并解释其基础理论。然后我们将进行一个实验,看看 Mixture-of-Softmaxes 技术是否能改善被构建为基于会话的分类任务的深度推荐系统。
问题
首先,让我们回顾一下,在大多数情况下,为了选择正确的标签,分类神经网络必须对每一个可能的选项进行评分。为了做到这一点,我们生成一个向量(我们称之为h),它在选定的输出维度中,通常不超过三位数字,这个向量与其他表示标签的向量(嵌入向量)进行比较。这个比较实际上是向量之间的点积。点积产生一个标量,它是一种测量两个向量相似性的方式。与所有条目进行比较的结果(矩阵乘法)是一个称为 logits 的向量,其维度等于可能的输出类别的数量(我们称之为V)。然后,logits 通过 softmax 操作生成最终的概率向量。

作者提供的图片
我在上一家公司使用的模型是基于序列的分类递归神经网络。给定一系列消费物品,我们希望在所有可能的物品中预测下一个消费物品。这是 Hidasi 等人论文 “基于会话的推荐与递归神经网络” 的一个实现。

图片来源于作者
在评分操作之前,GRU 单元的输出仅是一个相对较小维度的向量。但什么是向量?它只是向量空间中的一个位置。因此,网络的目标是生成在同一序列中彼此接近的向量(例如,小点积值)。
所以我的问题是,当在特定物品之后消费的所有物品彼此非常不同时,算法将如何处理这些情况?如果我们考虑之前说的,所有不同的消费物品(目标)必须接近于序列物品(输入)。最终,很容易形成直觉,即用户序列越多样化,例如,从一个主题跳到一个完全不同的主题,算法实现学习目标的难度就越大。如果所有用户序列非常多样化,那么物品必须同时既接近又远离彼此。
Softmax 作为矩阵分解
正如我们之前解释的,将语言模型或深度推荐系统框架化为分类问题的 softmax 操作通常具有相同的最后操作步骤。它们对 logits 使用 softmax,logits 是隐藏状态h和物品嵌入向量w的乘积:

论文 介绍了 Mixture-of-Softmaxes,并展示了上述操作可以表示为矩阵分解。更正式地说,设 N 为训练数据集中样本的数量,d 为物品嵌入的选择维度,V 为可推荐的物品数量。我们可以定义 3 个矩阵:

-
H ∈ R (N×d),编码上下文矩阵
-
W ∈ R (V×d),物品嵌入矩阵
-
A ∈ R (N×M),真实数据分布 P* 的对数概率
这一演示超出了本文的范围,但基本上,作者展示的是 softmax 操作最多增加 logits 矩阵的秩 1。这意味着输出对数概率矩阵的秩由 logits 矩阵的秩决定。
那么,这与瓶颈有什么联系?
如果我们以某种方式获得了真实上下文数据分布 P* 的概率矩阵,我们可能(根据任务的复杂性)能够展示在现实世界中这个矩阵的秩是非常高的。从理论上讲,这个矩阵的秩可以达到词汇表的大小 V。
但正如我们刚刚解释的,在我们的模型化中,输出矩阵的秩由 logits 的秩决定,后者是矩阵 H 和 W 的乘积。此外,对于任何两个矩阵 X 和 Y,我们有 rank(XY) = min(rank(X), rank(Y))。在我们的情况下,这意味着 logits 矩阵的秩不能超过 d(W 和 H 的最大秩——项目的嵌入维度)。在试图建模高度依赖上下文的任务时,这可能是一个严重的限制。
为什么瓶颈是一个问题?
如作者所示,普通的 softmax 操作可以被认为是一个小秩矩阵变换。提醒一下,矩阵的秩是由其列(或行)生成的向量空间的维度。它决定了可以用来创建输出的基(线性无关向量集)的数量。在语言建模中,可以将其解释为模型可以组合的基本语义意义的数量。鉴于自然语言的复杂性,小秩很可能不足以满足需求。
解决方案
为了克服 softmax 的瓶颈,论文的作者引入了 Mixture-of-Softmaxes(MoS)。MoS 将输出类别的条件分布在给定上下文的情况下表述为:

使用了 K 个 softmax,而不是一个。混合权重 π(先验)决定了每个 softmax 应该关注的程度。混合权重和每个 softmax 使用的 logits 都依赖于上下文 c 和混合组件 k。这意味着 MoS 引入了两个新的权重矩阵 P ∈ R (Kd × d),和 W ∈ R (K × d)。它们允许以下计算:

上述公式中,g 相当于普通 softmax 的隐藏状态 h。让我们试着可视化这里发生的事情。首先,我们使用投影矩阵从 g 创建每个 softmax 头所需的隐藏状态:

图片由作者提供
然后我们计算出一组混合权重 π,同样是从 g 中计算得出的。请注意,我们使用 softmax 激活来确保每个上下文中的混合权重总和为 1:

图片由作者提供
最终的计算可以如图所示:

图片由作者提供
那么为什么它能解决瓶颈问题呢?
请记住,交叉熵损失(我们用来训练网络的)对输出概率应用了对数变换。这意味着我们用以下表达式来近似真实数据分布的对数概率:

其中Πk 是一个(N × N)对角矩阵,元素为先验πc,k。由于这是上下文向量和词嵌入的非线性函数(log_sum_exp),上述函数可以是任意高阶。
Softmax 瓶颈是否适用于深度推荐系统?
Softmax 瓶颈表明表达能力受限于嵌入维度 d。如果 d 过小,softmax 没有足够的能力来表达真实的数据分布。但这只有在真实的数据分布复杂度需要高容量时才成立,比如建模自然语言。那在顺序推荐系统中呢?用户消费特定项目的概率是否高度依赖上下文?我感觉这依赖于许多因素,比如模型使用的特征的质量和数量,也包括我在引言中描述的因素。最终,我喜欢对这些问题做的就是进行实验。
实验
在接下来的部分,我将描述我进行的实验,尝试评估 Mixture Of Softmaxes 是否能改善深度推荐系统的推荐质量。
模型
我选择实现的模型是之前介绍的 Gru4Rec。在这里,我们实现了基础版本。我们只包含项目标识符的序列,不使用任何附加信息。

作者提供的图像
数据集
我为这些实验选择的数据集是著名的 MoviLens 数据集 [3]。更具体地说,我选择了 25M 版本,其中包含 162,000 个用户对 62,000 部电影的 2,500 万条评分。这个 zip 文件包含几个 CSV 文件,但我们这里只使用评分数据。

数据框的前几个评分
数据预处理
我们有一个典型的显式用户偏好数据集的例子。但在这里,我们想将推荐框架设定为一个分类问题。为此,我们需要:
-
通过仅保留大于 2.0 的评分(任意选择)并丢弃评分列,将其转化为隐式用户偏好数据集。
-
将数据集分成训练集和验证集(时间切分)
-
按用户 ID 分组评分
-
仅保留评分大于 1 的用户(因为我们需要最少长度为 2 的序列,以便有至少一个输入和一个目标)
-
对于每个用户,根据时间戳对评分进行排序,并应用滑动窗口来创建序列。
-
将每个序列保存为序列化的 tensorflow record
-
同时,统计并保存训练集中的不同电影数量(以便我们了解词汇表的大小)

图片作者提供
鉴于数据的规模,我必须使用分布式框架(在这种情况下是 dataflow,即 google cloud 管理版的 apache beam)来在 4 个不同的工作节点上并行执行所有预处理。
结果
与其他神经网络一样,我们有一些重要的超参数需要调整。在我们的案例中,最重要的超参数是:
-
批量大小
-
学习率
-
电影 ID 嵌入维度
-
softmax 头的数量
现代推荐系统架构通过多个步骤来提高性能。如果你想了解更多,可以查看我关于多阶段推荐的文章。我们使用的模型通常用作候选生成模型。这意味着我们最关心的指标是召回率。在这里,我们选择报告 Recall@100。
我最初想对超参数进行全面的网格搜索,但鉴于数据集的规模,这看起来非常昂贵。经过几次实验,我了解到相对较低的批量大小会带来更好的性能(选择了 64)。此外,我将 softmax 头的数量固定为 4(出于成本考虑)。
我在这里报告了三种不同嵌入维度(32、64、128)下普通 softmax 与 softmax 混合模型的结果:

图片作者提供
正如我们通常观察到的,这些基于嵌入的网络中,嵌入维度越大,性能越好。此外,从这个图中并不完全明显,但 Mixture-of-Softmaxes 的性能比普通 softmax 略好。现在,这种改进是否显著?实际上并不显著,我们只在 32 维版本中获得了非常低的 0.090% 改进,在 64 版本中为 0.333%,在 128 版本中为 0.314%。
这个结果是预期的吗?
首先,正如我们在引言中解释的,Mixture-of-Softmaxes 使我们能够克服普通 softmax 的低秩瓶颈。但是,这个瓶颈本身就是一个问题吗?作者们展示了在语言建模中确实如此。现在,对于基于序列的推荐任务,这一点并不明显。我猜测这可能取决于推荐的上下文复杂性。诸如序列长度和消费模式的多样性等因素可能对 MoS 的效果有显著影响。
其次,正如我们在理论部分解释的那样,日志概率输出矩阵的秩受限于电影标识符的嵌入维度。理论上,用 softmax 混合物替代普通 softmax 可以消除这一限制。因此,我原本期望 MoS 在低嵌入维度下比高维度下更有益。那么发生了什么呢?
按观察数量划分的性能
Gru4Rec 是一个协同推荐系统。像任何类似的系统一样,它在给定项目上的表现高度依赖于该项目的数据量。当新项目进入平台(即处于冷启动阶段)时,我们没有很多与之相关的序列,系统在提供相关推荐方面表现不佳。因此,性能与每个项目的训练示例数量直接相关。

图像由作者提供
我创建了分箱的限制,以便每个分箱获得相同数量的观察值。如图所示,电影在数据集中出现得越多,性能越好。现在,我们还看到另一个趋势。这一次,MoS 版本在低观看次数分箱(最左侧的分箱)中的表现低于普通 softmax。这表明建模复杂度的增加对冷启动项目的推荐没有好处。MoS 增加了对数据量的要求。MoS 仅在项目有足够数据时才有益。
按多样性划分的性能
你可能还记得,在介绍中我描述了我之前公司中的一个特定用例,该用例的性能较低,因为训练序列的多样性。另一个我想进行的有趣分析是研究 MoS 的有效性与用户序列的多样性之间的关系。当用户序列非常多样化时,MoS 是否更有效?
要回答这个问题,我们首先需要能够量化用户序列的多样性,例如量化一个视图与序列中前一个视图的不同程度。为此,我们将依赖用户-项目互动矩阵。

图像由作者提供
用户-项交互矩阵是一个由 1 和 0 组成的稀疏矩阵。用户通过行表示,项通过列表示。在每个单元格中,如果用户观看过特定电影,则为 1,否则为 0。从这个矩阵中,我们可以通过计算两个相关列之间的余弦距离来计算两个项之间的协作距离。当两个项被共同用户观看时,它们就接近。如果我们考虑有 N 个项,则总体计算的结果是一个 N x N 的平方矩阵,给出了任意两个项之间的距离。最后,为了确定序列的多样性,我仅返回目标项与序列中前一个项之间的距离。我本可以考虑目标项与所有前项之间的距离,但结果已经足够好了。

图片由作者提供
正如所见,序列中的多样性越高,算法准确预测下一个观看项的难度就越大。我们本可以通过利用项的元数据来以基于内容的方式计算两个项之间的距离,但我相信得出的见解会是类似的。
此外,另一个从上述数据中获得的见解是,我们发现 MoS 技术似乎对最具多样性的序列(最右侧的桶)效果不佳。这似乎有些违反直觉,因为使用高排名建模技术的目的是更好地处理更复杂的推荐设置。那么 MoS 是否在处理高度多样化的用户序列时失败了呢?
性能按 #观察 x 多样性 细分
为了充分回答上述问题,我们需要通过数据量和多样性的组合来细分性能。一旦计算出每个组合的 Recall@100,我们就可以更详细地研究 MoS 对性能的影响。我们首先对 32 维版本进行分析。

正如所见,MoS 效果的减弱主要是由于低观看次数/高多样性桶。这是相当有道理的。模型复杂性的增加会提高数据量要求。在项的序列不多且非常多样化的情况下,引入 MoS 并不具有优势。相反,当数据量足够时,可以看到用户序列越多样化,MoS 的效果越明显。
现在看一下 64 维版本:

图片由作者提供
以及 128 维版本:

图片由作者提供
如果我们通过移除第一个低观看次数桶(0 到 814)中的所有观察来计算性能,我们观察到 MoS 的整体表现是积极的:
-
32 维版本的 +0.618%
-
64 维版本的 +0.459%
-
128 维版本的提升为+0.320%
正如预期的那样,MoS 对低维嵌入更有益。这是因为嵌入维度越低,MoS 旨在消除的瓶颈越大。
结论
本研究的主要结论如下。MoS 的有效性与以下因素相关:
-
数据量。它通过增加建模复杂性,给每项数据引入额外的数据量要求。当每项的观察数量足够时,性能会得到提升。
-
用户序列的多样性。仅在会话数据集的复杂性要求时,MoS 才有效。
-
嵌入维度大小。MoS 在低维嵌入上的效果优于高维嵌入。
附注:你可以在我的个人 GitHub上找到所有相关代码。
最后说明:尽管对于这个数据集的改进相对较小,但我仍然认为,对于一些具有非常长序列和大量多样性的工业数据集,改进可能会更为重要。
参考文献
[1] Zhilin Yang, Zihang Dai, Ruslan Salakhutdinov, William W. Cohen. 打破 Softmax 瓶颈:高阶 RNN 语言模型. arxiv.org/pdf/1711.03953.pdf
[2] Balázs Hidasi, Alexandros Karatzoglou, Linas Baltrunas, Domonkos Tikk. 基于会话的推荐系统与递归神经网络. arxiv.org/abs/1511.06939
[3] F. Maxwell Harper 和 Joseph A. Konstan. 2015. MovieLens 数据集:历史与背景. ACM 互动智能系统 (TiiS) 5, 4: 19:1–19:19. doi.org/10.1145/2827872
机器学习基础(第四部分):决策树
原文:
towardsdatascience.com/ml-basics-part-4-decision-trees-cc37d07137b2
什么是决策树,如何构建和应用决策树于不同的分类任务
·发布于 Towards Data Science ·8 分钟阅读·2023 年 1 月 4 日
--

图 1:决策树分类器(来源:作者)
在之前的文章中,我们探讨了回归、支持向量机以及 人工神经网络。在本文中,我们将介绍另一种机器学习概念——决策树*。您可以通过上述链接查看其他方法。
介绍
决策树也许是机器学习工具箱中最简单和最直观的分类方法之一。决策树的首次出现是在威廉·贝尔森于 1959 年的一篇出版物中。早期对决策树的使用主要限于分类法,因为它们与这种数据类型的自然相似性。后来,决策树分类器的适应版本应运而生,它们不再局限于特定的数据类型(例如,名义型数据),也适用于其他数据类型(例如,数值型数据)。决策树是一种简单却有效的工具,用于许多分类问题,在某些情况下,它们可以超越其他更复杂的方法,这些复杂的方法对于简单的数据集可能显得过于繁琐。
决策节点、约束和叶节点
决策树——顾名思义,是一种树形数据结构,具有一组决策节点和一组在有向图中连接的边。决策节点包含决策标准(即约束),决定从节点到树中其他节点的出边路径。最常见的决策树是二叉树,每个决策节点只有两条边,但也可以有多于两条边的树(例如,基于名义数据构建的树)。约束可以简单到由一个变量组成(例如,X₁ ≤ C),也可以由多个特征的线性组合组成(例如,X₁ ≤ X ₂ + C)。同样,约束也可以是非线性的。树的叶节点包含分类标签。这意味着,当我们从训练数据集遍历构建的树并到达一个叶节点时,我们会得到测试数据点的分类标签。在图 2 中,我们可以看到一个示例数据集以及从该数据中学习到的决策树。

图 2:左侧:来自两个类别的一组随机数据点形成的簇,右侧:从数据中学习到的决策树图示——数字仅用于后续参考标记(来源:作者)
学习树
正如前一节所提到的,决策树是一组以树形排列的决策约束,其中叶节点为特定数据实例提供分类。最重要的步骤是找出我们应该以树形排列的最佳约束。我们通过在每个节点找到最佳约束来做到这一点,这样可以为其子节点提供最佳的数据拆分。更具体地说,在任何给定节点,我们选择一个特征,然后通过迭代地遍历该特征在训练集中的所有变量值来生成一组候选约束。例如,如果我们选择一个变量X₀,那么候选约束将通过将该变量与该变量的所有值进行比较来生成(即,X₀ ≤ C)。其中 C 是该变量在训练集中的常数值。

图 3:数据簇、决策树以及数据拆分的约束可视化(来源:作者)
对所有特征变量重复相同的过程,获得所有候选约束。为了找出最佳约束,我们在该节点的数据上应用该约束,得到两个数据子集:一个是左分支,满足该约束,另一个是右分支,否定该约束。我们现在需要一个度量来评估拆分的效果。换句话说,我们想知道拆分数据是否提高了找到真实数据标签的机会。为此,我们使用熵度量。

图 4:熵的方程式(来源:作者)
熵是系统中不确定性的度量。在信息理论中,熵是数据中存在的信息量(惊讶/不确定性)。例如,一个表示随机变量结果的二元集X ∈ [1,1,1,1,1,1]的熵为零。这意味着它包含最少量的信息,因此我们对该集合中所有未来发生的事件的值都很确定。然而,对于值集X ∈ [1,1,1,0,0,0],熵为1.0,这意味着它包含最多的信息,我们完全不确定新事件的值是什么。
现在我们测量每个数据分割中分类标签的熵,并将其与父节点的熵进行比较。更具体地说,我们计算一个叫做信息增益的量度,它测量通过观察另一个随机变量的结果来获得关于一个随机变量的信息量。在决策树的背景下,这意味着相对于父节点,子节点的熵减少。因此,它可以通过从父节点的熵中减去子节点的熵来计算。如果有多个子节点,则从父节点的熵中减去子节点熵的加权平均值。权重可以通过计算左子节点和右子节点相对于父节点的概率来计算。

图 5:信息增益计算(来源:作者)
我们为每个候选约束计算信息增益,并选择具有最大信息增益值的约束。直观地说,我们试图找到那些在类别标签方面变化最小的数据分割。我们一直这样做,直到到达叶子节点,我们希望最终得到只有一个类别的分类标签。然而,根据我们构建的树的深度,叶子节点可能没有单一的类别标签,在这种情况下,我们将取数据分割中多数类别的类别标签作为最终分类标签。在图 3 中,你可以看到树的构建过程,包括最佳约束、数据分割和叶子节点的分类标签。

图 6:决策树算法(来源:作者)
带线性约束的决策树
使用前面描述的过程,我们可以使用numpy在 Python 中构建决策树,并将其应用于各种类型的数据。我们从创建一个随机数据集开始,并将其分割为训练集和测试集,如图 7 所示。

图 7:一组随机数据点分割为训练集和测试集(来源:作者)
我们可以在训练集上训练一个决策树分类器。从训练集构建的决策树如图 8 所示。

图 8: 单变量决策树分类器与示例预测(来源:作者)
一旦构建了树,我们可以使用它来预测测试点。例如,X=(0.5,-0.5) 的一个实例在遍历树后将被预测为属于‘0’类。预测过程仅包含一组应用于数据点的条件。训练树也是一个简单直观的过程,但对于大型数据集,它可能变得耗时且繁琐。
具有非线性约束的决策树
在前面的部分,我们对线性可分数据应用了决策树分类器。然而,在大多数情况下,数据并不是线性可分的。例如,图 9 中的数据就不是线性可分的。

图 9: 非线性数据示例(来源:作者)
我们可以像以前一样使用单个变量来构建树,但仅使用正交约束会在准确找到决策边界时遇到问题。因此,我们将从变量的线性组合中构建约束。基于这些约束学习到的决策树如图 10 所示。请注意,以这种方式,我们将每个约束建模为一条直线,并可以找到倾斜的决策边界。与使用单变量约束构建的决策树相比,这种方法对相同数据的准确分类所需的树深度也较少。

图 10: 用于非线性数据的多变量决策树(来源:作者)
在大多数情况下,使用线性约束构建的决策树已经足够。因为多个线性约束可以准确地创建非线性决策边界。然而,在某些情况下,你可能希望尝试非线性约束以获得更精确的分类。具有非线性约束的决策树如图 11 所示。

图 11: 具有非线性约束的决策树(来源:作者)
决策树的结果,无论是多变量线性约束还是非线性约束,都可以在图 12 中看到。正如你可能注意到的,两种分类边界之间几乎没有差别。这是因为,使用多变量线性约束构建的决策树分类器本身就是一个非线性分类器。然而,对于分类性能可能有显著差异的其他数据集,使用这种分类器可能值得尝试。

图 12: 左侧: 多变量线性约束的分类,右侧: 非线性约束的分类(来源:作者)
结论
在这篇文章中,你了解了决策树以及如何构建决策树。你还学会了如何对不同数据类型应用决策树分类器。决策树分类器可能是机器学习工具箱中最简单和最直观的分类器。然而,在某些情况下,这就是获得准确分类所需的一切。你可以在以下的 GitHub 仓库中找到使用 numpy 实现决策树的代码。
代码:
www.github.com/azad-academy/MLBasics-DecisionTrees
成为 Patreon 支持者:
在 Substack 上找到我:
关注 Twitter 以获取更新:
使用 DynamoDB 进行 ML 工程
原文:
towardsdatascience.com/ml-engineering-with-dynamodb-398ff2e6394e
如何利用这个强大的 NoSQL 数据库进行在线推理
·发布在 Towards Data Science ·5 分钟阅读·2023 年 2 月 7 日
--
为什么考虑 DynamoDB?
在处理离线批量 ML 系统时,基于 SQL 的数据仓库工作流程如 Snowflake 构成了业务分析、描述性统计和预测建模的核心。这是一种理想状态,其中复杂的转换可以推送到分布式数据库引擎,而特征则在用于推理的相同语言中定义。
避免离开这个乐园,除非你被(真实的)实时推理需求推出来。如果你发现自己处于这个新的、混乱的世界,尤其是当相关特征高频变动时,并且你需要低延迟的读取和写入,你可能需要 NoSQL。
AWS DynamoDB 是一个 极具扩展性 的托管 NoSQL 数据库。像其他 NoSQL 数据库一样,它用一致性换取可用性,使其成为构建低延迟 ML 预测器的理想数据存储。本文的目标是展示一些设计模式,帮助你构建足够灵活的生产工作流程,以处理常见的 ML 访问模式。

作者生成的图片来自 imgflip.com/
DynamoDB 中的索引简介
键值存储系统速度很快,因为访问模式很简单。集合中的元素通过主键进行访问。在 Dynamo 中,这是一组分区键和排序键的组合,每行必须唯一。分区键和排序键共同构成一个复合主键,分别直观地确定了数据存储的位置和顺序。
本地二级索引在单个分区的范围内操作。在你想要以多种方式对分区进行排序或过滤的情况下,它们很有用。
全局二级索引不需要与主索引共享相同的分区键。它们可以帮助在单个表中创建灵活且高效的访问模式,正如我们将在 ML 产品场景的第二部分中看到的那样。
GSI(全局二级索引)是以最终一致性的方式更新的,这意味着写入操作可能在所有索引更新之前成功写入 DynamoDB。这是一个有意的设计权衡,也是 DynamoDB 如此强大的部分原因。当存储高吞吐量的数据(例如遥测数据、大量交易)时,快速且可能不完全干净的写入是一个好的权衡。
使用时间序列特征对单个客户进行推断
假设你有一个模型,它预测客户是否会在过去 30 天的历史记录的基础上进行购买。你的产品经理要求你使用这个模型来决定是否在浏览器上展示促销横幅(为什么不呢;我不是产品经理)。
DynamoDB 如何支持这个预测管道?
-
我们设计一个表,支持每个客户所需特征的高效访问。
-
我们实时地(可能通过某种队列服务)将购买记录写入表中。
-
推断应用在接收到来自网站的请求时查询这个表。
首先,让我们在一个更熟悉的关系模型中展示数据和特征工程的样子。在推断时,模型需要访问客户过去 30 天的购买记录。
你如何在 DynamoDB 中高效地复制这个设置?模型在客户层面进行预测,使用该客户的特征,因此一个自然(且高效)的起点是按 customer_id 分区数据。DynamoDB 将为每个客户 ID 连续存储数据;在每个主键分区内,我们希望高效地获取与预测相关的购买数据。一个本地二级索引可以让我们做到这一点。
让我们在一个实时推断应用中使用这个表中的值。这里模拟的模型是一个逻辑回归,具有单一且易于理解的特征total_purchase_volume_30d。我们通过 FastAPI 端点将预测结果提供给前端,FastAPI 从 DynamoDB 中获取数据,并将其输入到预测模型中。
将其扩展到同时进行多个预测
到目前为止,一切顺利。我们能够用一个客户 ID 命中一个 API 端点,并以低延迟访问该客户的预测。事实上,你的产品经理对促销活动的表现非常满意,她希望将其扩展到邮件群发。她希望每次群发都针对可能在接下来的 30 天内购买的客户,并根据在特定类别中进行过购买的客户进行细分。一封邮件群发可能针对珠宝购物者,另一封则可能针对有趣的衬衫(这里随便说说)。
目前,机器学习管道需要支持为每个类别中所有已购买的客户返回预测。问题在于客户数量庞大;假设有数千万个客户。此外,大多数客户在过去 30 天内没有进行购买。我们当前的数据设计要求对所有客户进行扫描,过滤出我们关心的类别中的交易。这种对大型表格进行扫描的操作代价极其高昂,因为它需要读取表格中的每一项;同时也极其麻烦,因为你需要分页浏览大量结果。
幸运的是,我们可以通过扩展表格并添加全局二级索引(GSI)来有效支持这种用例。GSI 数据与主索引独立存储,因此不需要共享相同的分区。这对于支持我们的用例至关重要:我们的策略是查询表中某一特定类别下的所有项,筛选出相关客户,然后利用主索引进行预测。注意:我们利用“ProjectionType”属性来限制存储在二级索引中的数据量(在这种情况下,我们只需要客户 ID)。这将降低成本并加快读写速度。
下面的预测循环实现了上述策略。针对 GSI 的简单查询可以快速返回表中由 API 参数 category_name 指定的类别中的所有购买记录。我们去重返回的客户 ID,然后利用这些 ID 发起一个 BatchGetItem 请求(这可以减少网络往返时间),以获取这些客户的所有购买记录,使用主键和本地二级索引。特征数据的封送处理和预测可以完全在内存中进行。
结论
通过两个机器学习产品用例,我们已了解如何利用 DynamoDB 中的本地和全局索引来高效存储和访问特征数据。你甚至可以更进一步,通过二级索引模拟一整套关系。
在 SQL 中进行特征工程更直观且冗余较少,因此尽可能使用它。然而,DynamoDB 的低延迟、最终一致性模型使其成为高量在线推断的有吸引力的选项。
“ML-Everything”?在科学中的机器学习方法中平衡数量与质量
需要进行适当的验证和优质的数据集,这些数据集要客观、平衡,并且预测在实际场景中要有用。
LucianoSphere (Luciano Abriata, PhD)
·发表于 Towards Data Science ·12 min read·2023 年 3 月 14 日
--

Tingey Injury Law Firm 提供的照片,来源于 Unsplash
引言
近期的机器学习(ML)研究在各个领域取得了显著进展,包括科学应用。然而,为了确保新模型的有效性、测试和验证程序的质量,以及所开发模型对实际问题的适用性,仍然存在一些需要解决的限制。这些限制包括不公平、主观和不平衡的评估,这些评估可能不是故意的,但却存在;使用无法准确反映真实世界使用情况的数据集(例如“过于简单”的数据集);将数据集分割成训练、测试和验证子集的错误方法等。在本文中,我将讨论所有这些问题,并使用生物学领域的例子来说明,生物学领域正被机器学习方法所变革。
在此过程中,我还将简要讨论机器学习模型的可解释性,尽管目前非常有限,但它非常重要,因为它可以帮助澄清文章第一部分讨论的需要解决的限制的许多方面。
此外,我将强调,虽然某些机器学习模型可能被过度宣传,但这并不意味着它们没有用处,或没有产生能够推动机器学习某些子领域发展的新知识。
每个月都有许多新的机器学习模型出现,这只是我工作领域中的一部分!
随着针对科学应用的机器学习(ML)论文数量的激增,我开始质疑… 这些工作是否如其所宣称的那样具有革命性和实用性?当然,AlphaFold 2 是革命性的,还有类似理论框架所启发的后续 ML 工具,例如 ProteinMPNN。但更广泛地说,领域内发生了什么?科学家们如何能同时生产出如此多的 ML 工具,并且它们都被称为“最佳”?这些研究的质量是否足够高?假设这些工作是新颖且优秀的,它们的评估是否始终公平、客观和平衡?实际应用是否如宣传的那样具有革命性?
每次看到一种新的结构生物学 ML 方法时,我都会思考如何评估其优点,特别是其实际性能和对我研究的实际效用。
研究人员越来越多地将神经网络的最新发展应用于各种领域的长期存在的问题,从而取得了显著进展。然而,确保评估公平、客观和均衡,并确保数据集和预测能力准确反映 ML 模型在现实世界中的适用性是至关重要的。
社交网络帖子、预印本服务器和同行评审的论文显示了现代神经网络模块和概念(如 transformers、扩散模型等)在解决科学长期存在的问题上的迅猛应用。这些研究本身就是一种积极的现象,因为这种方法在各个领域取得了显著进展。以两个主要的例子为例,AlphaFold 2 在 CASP14 中获胜所带来的蛋白质结构预测进展,以及 D. Baker 的 ProteinMPNN 在蛋白质设计方面的应用,其预测的蛋白质序列在实验工作中得到了广泛测试,证实了该方法的有效性。如果你想了解更多关于这些方法的信息,可以查看我的博客文章:
## 一年多的 AlphaFold 2 免费使用及其在生物学中引发的革命
对蛋白质结构的自信建模、预测其与其他生物分子的相互作用,甚至蛋白质...
## 这里是我关于蛋白质建模、CASP 和 AlphaFold 2 的所有同行评审和博客文章
我在这里汇总了所有经过同行评审的文章(一些论文、几篇综述、一篇观点)以及关于...
在许多情况下,新方法至少在某种程度上被过度宣传。例如——我认为我的怀疑在这里最为明显——蛋白质设计的情况。大多数甚至所有近期展示现代机器学习模型用于蛋白质设计的工作,都将恢复序列相对于输入蛋白质结构的序列身份作为成功指标。起初这可能有意义,但深入思考并了解蛋白质序列和结构后,显然一个好的序列匹配并不一定意味着(完全不)蛋白质会按预期折叠。例如,即使是单个突变体蛋白质也非常常见,可能会折叠失败并从溶液中崩溃,尽管它们与野生型蛋白质几乎相同,并且在“序列身份”指标方面基本完美。此外,单个突变体有时可能引发折叠交换,导致几乎完美的序列身份但没有结构的保留。相反,发现折叠方式几乎相同的蛋白质虽然序列完全不同,因此序列身份非常低,也并不少见。总之,序列恢复作为实际成功的指标非常有限。
序列恢复是合理的,但作为实际蛋白质设计成功的指标却非常有限。
目前,在蛋白质设计中,唯一真正有意义的测试是亲自进入湿实验室,生产设计的蛋白质,并通过实验确定其结构,查看它是否与输入结构匹配——至少要合理匹配,当然我们不能期望完全匹配。虽然 ProteinMPNN 论文和一些其他工作进行了这种方法的实验验证,但我看到的大多数预印本和论文完全忽视了这一点,专注于序列恢复和类似指标。而且,不,发现 AlphaFold 可以回溯预测输入的结构也不是设计有效的安全指标!充其量,它只能用于识别不良序列。
如果你想了解更多关于我刚刚提到的 ProteinMPNN:
这款来自 Baker 实验室的新软件设计的蛋白质在湿实验室中实际上有效。你可以用它来……
数据集和评估的潜在问题
一个重要的问题,我在这里一般性地描述,因为我不想对任何特定工作进行负面评价,是许多研究在数据集上评估其模型,而这些数据集并不适合任务。我看到的两个主要问题是数据集未能反映 ML 模型的实际应用,以及数据集包含与训练数据重叠的条目。
我并不是说有不良意图。训练 ML 模型需要非常大的数据集,这些数据集大到无法手动整理,自动整理管道总是有局限性。另一方面,预印本和论文往往只展示那些说明 ML 模型在某些生物学问题中的积极应用的案例,隐藏或忽视那些缺乏生物学意义、难以解释的案例,或者——这纯粹是不良的科学实践——那些与已有知识不符的案例。
这些问题并非特定于某一领域,而是科学领域普遍存在的显著问题的特定表现:只有积极结果被认为有贡献,因此负面和不正确的结果在文献中并不常见,尽管它们和积极结果一样重要,特别是在避免资源和时间的徒劳浪费方面。发布或死亡的文化推动了主要是积极结果的发表,这些结果往往被装饰以声称过度的新颖性和优越性。要了解更多关于我对科学问题尤其是出版问题的看法,请参见此:
总结和反思由期刊 eLife 的新标题引发。
根据我上述的观点,我认为像CASP(或 CAMEO,或 CAPRI 结构预测等)这样的竞赛以及致力于客观基准测试现有方法的研究,比绝大多数报告新模型的论文更能推动该领域的发展。实际上,我如此坚信,以至于对我来说,在像 CASP 这样的竞赛或独立基准测试研究中排名靠前,就排除了任何描述(可能的)新方法的结果不佳的论文(尽管这并不一定意味着它可能涉及某些潜在的未来兴趣的新方法)。
像 CASP(或 CAMEO,或 CAPRI 结构预测等)这样的竞赛以及致力于客观基准测试现有方法的研究,比绝大多数报告新模型的论文更能推进这一领域。
关于评价的一点需要注意,特别是针对 AlphaFold 2 之后的蛋白质三级结构预测,是现在只有小幅度改进的可能性,这使得不同新方法的比较变得复杂——这个问题 AlphaFold 2 当然没有面临,因为当时的标准还不高。在其他领域中,这不是一个大问题,例如在药物设计、对接、构象动力学预测等领域,预测仍然较差到中等水平,而蛋白质三级结构预测已算是基本解决了:
## AlphaFold 之后的蛋白质结构预测:机器学习的新进展
AI 驱动的生物学革命会继续吗?我们能期待新的突破吗?现在发生了什么…
towardsdatascience.com
这些方法被过分炒作并不意味着它们没有用处或无法推动领域的进步!
当科学家们探索解决问题的替代方法时——如这里所关注的机器学习——他们可能会提出看似革命性的解决方案,并具有良好的前景,但最终却未能达到预期。这并不一定意味着这些新发展对于某些应用没有用处,或没有产生对未来工作有用的新知识,也有助于推动某些领域的进步。
一个很好的例子是语言模型在结构生物学预测中的应用。最早报告的这类方法之一显示的性能远低于 AlphaFold 2,但执行时间却快了几个数量级,这可能在某些应用中带来一些好处:
## 使用蛋白质语言模型的蛋白质结构预测:比 AlphaFold 2 快一百万倍
方法及其相关性的总结;尝试在 Google Colab 中运行;并测试其是否真的有效…
towardsdatascience.com
可能是这些基于语言的蛋白质结构预测模型中最受欢迎(对我来说也最有用)的 Meta 的 ESMFold 很快就出现了。Meta 推出的这个新神经网络及其与预计算的大型蛋白质结构模型数据库一起发布,掀起了一阵短暂的炒作,我认为这本身也是对 Deepmind 的 AlphaFold 2 在该领域引发的革命的一个较小但实质性的贡献:
具有与 AlphaFold 相似的准确度但速度提高至 60 倍 — 并且开发了新的 AI 方法…
towardsdatascience.com
然而,尽管 ESMFold 非常快速,并且在结构预测上表现远超之前的蛋白质语言模型,它的预测仍略逊于 AlphaFold 2(此外,ESMFold 在例如自定义模板的使用或蛋白质复合物结构预测方面存在局限)。你可以亲自查看 ESMFold 在今天标准下相对较差的表现,具体可以参考在 CASP 第十五届评估中进行的官方评价:
由美国国家普通医学科学研究所(NIH/NIGMS)资助的蛋白质结构预测中心…
predictioncenter.org](https://predictioncenter.org/casp15/results.cgi?tr_type=regular&source=post_page-----baa0477f5ac9--------------------------------)
但请不要误解我的意思。Meta 的模型非常有用且具有巨大潜力,在实际应用中铺平了使用其蛋白质语言模型的新工具的开发道路。我认为这个模型从结构预测的内部网络机制角度尤其有趣,这可能在未来进一步发展。例如,贝克实验室和 Meta 的联合工作表明,尽管语言模型仅在序列上进行训练,但其学习深度足以设计出超越自然蛋白质的结构,甚至包括在已知蛋白质中未观察到的结构动机(甚至经过实验验证!):
从进化中的序列学习蛋白质设计模式,可能在生成蛋白质方面具有前景…
www.biorxiv.org](https://www.biorxiv.org/content/10.1101/2022.12.21.521521v1?source=post_page-----baa0477f5ac9--------------------------------)
可解释性及其如何帮助赢得对 ML 模型的信任,缓解其一些局限
使用机器学习时的一个限制,不仅在结构生物学中,我认为在大多数科学或工程应用中也是如此,就是缺乏可解释性,或至少是明确的可解释性。换句话说,机器学习模型在很大程度上作为黑箱工作,因此在其被设计执行的任务或预测中是实用的,但对于它们如何以及为何表现如此好(或不好,视情况而定)却没有提供太多(如果有的话)洞见。
理想情况下,即便是完美的模型,假设其具有很高的准确性和可靠性,理解模型在何时何地表现良好或不佳仍然是有价值的。此外,理想的情况是这些解释能够根植于领域的基础科学中,比如说从潜在的物理学或化学的角度,以某种明确的方式连接独立变量,就像常规建模使用我们可以理解的方程一样。
特别是在结构生物学中,我们面临的问题是,蛋白质结构预测的机器学习模型表现非常好,但对于它们如何实现如此好的预测却知之甚少。因此,目前尚不清楚它们是否学到了我们不了解的蛋白质结构知识,或者(我认为更可能的是)它们可能学到了我们了解但难以量化,从而难以以“分析”方式应用于结构预测的知识。此外,我们甚至不知道这些结构预测的机器学习方法是否仅仅是对折叠状态的优秀预测者,还是也能预测折叠路径中的中间体、替代构象状态、内在无序区域的结构倾向等。我猜它们不能,或者至少信心不高,因为按设计,它们偏向于预测良好折叠状态的结构。一个关于它们如何实现对这些状态如此好预测的明确解释,以及系统中流动的信息类型,或许可以证明我的猜测是错的,并且确实能帮助方法开发者理解限制并尝试用改进的模型克服这些限制。
机器学习模型缺乏可解释性可能会在多个方面造成问题。首先,当模型表现不如预期时,诊断和纠正错误可能会很困难。这在处理牵强的外推时尤为重要,例如预测一个蛋白质的结构,而这个蛋白质的结构实际上与所有已知结构差异很大。如果不了解模型如何得出预测结果,就很难知道如何修复它,并评估每个预测的可靠性——尽管现代机器学习工具正越来越多地纳入预测可靠性的指标。
其次,缺乏可解释性限制了我们对解释系统行为的基础物理、化学或生物学的洞察能力,即使我们能够正确预测这种行为。对实际应用来说,这不是大问题,但对于科学寻求的基本理解而言,显然是不完整的。
最后,缺乏可解释性可能限制我们对机器学习模型建立信任的能力。如果我们无法理解一个模型是如何得出预测的,我们可能会不愿在准确性至关重要的情况下使用它。特别是在结构生物学中,不准确的模型可能导致对生物分子功能的错误结论,并阻碍所有相关研究和发展的进展。
本文中可解释性的要点是,更具可解释性的机器学习模型可以缓解与其构建、训练和应用于实际问题相关的许多问题;甚至可能在实际应用之前识别潜在问题,从而在数量与质量之间的平衡中提高质量。
更具可解释性的机器学习模型可以缓解与其构建、训练和应用于实际问题相关的许多问题,从而在数量与质量的平衡中提高质量。
有人在研究机器学习模型的可解释性问题,包括一些专门在科学应用背景下工作的人员。我将很快在这里写一篇关于此的博客文章。
相关资料
在整理我的文章时,我阅读了其他博主和科学家的精彩帖子,其中这些非常推荐——尽管我们并不总是达成一致:
[## 对神秘宇宙的一些思考
由穆罕默德·阿尔库拉伊希编写
moalquraishi.wordpress.com [https://www.blopig.com/blog/author/carlos/?source=post_page-----baa0477f5ac9--------------------------------] [## 牛津蛋白质信息学组
Jamboree(1)大型集会,如政治党派或体育联盟的团队,通常包括一系列程序……
www.blopig.com [## 科学的艰难之路
是否曾感觉到,作为一名科学家,主要工作就是在大量值得怀疑的论文中摸索……
喜欢这篇文章?那就看看我的其他文章吧
科学家们正在接近首个接近原子级的全细胞模拟
在 AI 对生物学影响之后,这可能代表另一个里程碑,这次根植于纯物理学
towardsdatascience.com [## 8 种你必须了解的现代和即将出现的技术
从最现代的计算机技术和硬核物理学到生物技术的前沿
medium.com [## 结构生物学和生物信息学的关键网站和程序
来自硕士和博士课程的实际笔记
lucianosphere.medium.com [## 使用 gpt-3.5-turbo 构建定制化聊天机器人
摘要、源代码已准备好使用,还有一个示例聊天机器人可以立即体验
www.lucianoabriata.com 我写作和拍摄关于我广泛兴趣领域的所有内容:自然、科学、技术、编程等。 成为 Medium 会员 以访问所有故事(我通过该平台的附属链接获得少量收入,对你没有费用)并且 订阅以获取我的新故事 通过电子邮件。要 咨询小型工作,请查看我的 服务页面在这里。你可以 在这里联系我。
ML 模型注册中心——绑定模型实验和模型部署的“接口”
实践中的 MLOps——深入探讨 ML 模型注册中心、模型版本控制和模型生命周期管理
·发表于 数据科学之路 ·阅读时间 8 分钟·2023 年 2 月 16 日
--
背景
在我之前的文章中:
实践中的 MLOps——将 ML 解决方案架构拆解为 10 个组件
我谈到了管理 ML 实验运行中生成的模型元数据和工件的架构重要性。我们都知道,模型训练过程会产生许多工件,用于进一步调优 ML 模型性能以及后续的 ML 模型部署。这些工件包括训练后的模型、模型参数和超参数、指标、代码、笔记本、配置等。集中管理和利用这些模型工件和元数据对于构建稳健的 MLOps 架构至关重要。因此,在今天的文章中,我将讨论 ML 模型注册中心,它作为绑定模型实验和模型部署的“接口”。
今天的文章将重点讨论以下方面:
-
ML 注册中心是什么?以及 ML 注册中心执行的关键功能
-
ML 模型注册中心带来的关键好处
-
如何将 ML 模型注册中心集成到端到端的 MLOps 解决方案中
-
ML 注册中心背后的技术以及流行的开源 ML 注册中心解决方案

图片来源:Robert Anasch 通过 Unsplash
什么是 ML 注册中心?
ML 注册表是一个集中存储所有 ML 工件及其元数据的地方,从早期阶段的实验到生产就绪的模型。类似于 DockerHub 这样的容器注册表或 PyPi 这样的 Python 包注册表,ML 注册表允许数据科学家和 ML 从业者发布和共享 ML 模型及工件。通常,ML 注册表提供一个用户界面(UI),以及一套 API,供 ML 管理员和用户注册、发现、共享、版本控制和管理 ML 模型的权限和生命周期。
存储在 ML 注册表中的信息可以总结为以下几类:
-
模型元数据 包括模型名称、模型注释和描述、模型标签、模型创建时间、模型修改时间和模型架构(模型输入架构和输出架构);
-
模型生命周期管理 包括模型生命周期的所有阶段,从创建到退役。例如,MLflow 提供了预定义的阶段,适用于常见的用例,如 Staging、Production 和 Archived。
-
模型版本控制。 对于任何注册的 ML 模型,几乎可以肯定会有多个版本。原因是 ML 模型需要不断监控和更新,以反映业务环境和数据的变化。模型版本控制不仅仅是提供版本号,而是一种将 ML 模型的每个版本与用于训练的相应数据、特征和代码对齐的机制,实现端到端的模型沿袭。
-
模型治理 — 包括管理模型权限(控制谁可以管理和更新模型)、审计模型活动和使用记录、在部署到生产环境之前审查和批准模型、关键模型变更的通知以及模型沿袭。
-
模型服务。模型注册表可以通过提供 Webhooks 来促进模型服务,使 ML 工程师能够监听模型注册表事件。当特定事件发生时,可以自动触发相应的操作。你可以使用 Webhooks 来自动化并将你的 ML 流水线与现有的 CI/CD 工具和工作流集成。例如,当创建新版本的模型时,你可以触发 CI 构建,或在每次请求模型转移到生产时通过 Slack 通知你的团队成员。
-
模型监控和调试。在 ML 模型部署到生产环境后,监控模型的性能是必要的。ML 模型注册表提供了数据科学家使用生产数据测试已部署模型的机制,以密切监控模型的表现。如果发现模型降级,数据科学家可以利用模型沿袭信息来识别根本原因。

ML 模型注册表的关键组件 | 图片来源:作者
ML 模型注册表存储带来的主要好处
-
生产力 — 一个中央 ML 注册表显著地消除了每个数据科学/ML 团队管理自己 ML 模型和工件的孤岛。一个 ML 注册表可以像一个 ML 模型市场一样运作,团队可以在其中发布、分享和重用其他团队的工作。总体而言,这可以显著提高团队的生产力,并且用更少的资源开发更多的 ML 应用。
-
治理 — 负责任的 AI 和 ML 治理一直是许多组织在监管、伦理、社会和法律方面的重要话题。一个中央 ML 注册表可以通过提供模型权限、模型使用记录、模型审计报告以及模型从原始数据和特征的血统等信息来协助 AI/ML 治理工作。
-
协作 — 一个 ML 模型注册表是数据科学家和 ML 工程师共享的单一统一接口,可以促进和简化数据科学家与 ML 工程师之间的交接。当数据科学家对经过多轮实验后的整体模型性能感到满意时,他们将把代码和模型交给 ML 工程师进行生产部署。拥有 ML 模型注册表,ML 工程师可以清晰地了解模型是如何训练的、使用了哪些数据和特征以及特征工程逻辑是什么。这显著减少了数据科学家和 ML 工程师之间的沟通工作,提高了团队之间的协作。

ML 模型注册表存储带来的关键好处 | 作者提供的图像
ML 注册表存储背后的技术
在幕后,模型注册表通常包括以下两个元素:
-
一个是 ML 实体(元数据)存储 — 实体存储存储 ML 实体的元数据,例如 ML 实验、运行、参数、指标、标签、备注、来源、生命周期阶段以及 ML 工件位置。ML 实体存储通常由 SQL 关系数据库实现,如 PostgreSQL、MySQL、MSSQL 和 SQLite。
-
另一个是 ML 工件存储 — 工件存储持久化工件文件、模型、图像、内存对象、模型摘要或任何记录到 ML 注册表存储的对象。工件存储是一个适合大数据的位置,是客户端记录其工件输出(例如模型)的地方。工件存储的实现通常由持久文件系统支持,如 Amazon S3 和 S3 兼容存储、Azure Blob 存储、Google Cloud 存储、FTP 服务器、SFTP 服务器、NFS 和 HDFS。

ML 注册表存储背后的技术 | 作者提供的图像
将 ML 模型注册表集成到端到端 MLOps 解决方案中
一个 ML 模型注册表在端到端 MLOps 解决方案的 3 个关键阶段中扮演着非常重要的角色:
-
第一个是模型开发——模型开发是非常迭代和实验性的,这意味着数据科学家需要尝试各种算法、框架以及这些算法的不同特征、参数和超参数组合,以找出最适合问题的方案。因此,能够重现机器学习实验的运行可以显著提高数据科学家的生产力,帮助他们更快地找到最理想的解决方案。机器学习模型注册提供了谱系功能,使数据科学家可以从注册的机器学习模型追溯到产生模型的训练运行,以便他们可以重现模型或做出必要的更改以重新训练模型的新版本。
-
第二个是模型部署——模型注册事件(例如,为相关模型创建新模型版本或将模型版本的阶段从测试阶段更改为生产阶段)可以被利用来自动触发机器学习模型的部署。例如,你可以在创建新模型版本时触发 CI 构建,或者每次请求模型转生产时通过 Slack 通知团队成员。你还可以将模型注册事件集成到现有的 CI/CD 管道和工作流中,以自动触发这些过程。
-
第三个是生产中的模型——模型注册提供了所有生产中模型的整体视图,并允许 ML 操作团队相应地监控这些模型。机器学习模型非常依赖数据。因此,机器学习模型的性能可能会因数据环境的不断变化而恶化,而不仅仅是由于编码不佳。一旦识别出模型性能恶化,ML 注册服务可以通过提供必要的模型工件和模型谱系功能,帮助 ML 操作团队调试和重新训练模型。
因此,MLOps 的整体实现,直到你拥有一个最先进的模型注册系统,才能完全且正确地进行。

将机器学习模型注册集成到端到端的 MLOps 解决方案中 | 图片作者
流行的机器学习注册存储解决方案
-
MLflow 模型注册——MLflow 是一个开源平台,用于管理机器学习生命周期,包括实验、可重复性、部署和集中模型注册。MLflow 的第四个主要组件是模型注册。使用 MLflow,你可以在运行 MLflow 的机器的本地文件系统中建立一个注册存储,或者你可以启动一个远程的中央跟踪服务器,团队可以在其中集中注册和共享机器学习模型工件。如果你已经在 Databricks 上,你将可以访问一个托管的跟踪服务器。
-
VertaAI ModelDB——一个开源系统,用于机器学习模型版本控制、元数据和实验管理。Verta 库提供了一个模型目录组件,用户可以在其中查找、发布和使用 ML 模型或 ML 模型管道组件。
-
Amazon SageMaker Model Registry——众所周知,SageMaker 是 AWS 的 ML 管理工具,提供各种组件供用户构建、训练和部署 ML 模型。SageMaker 还有一个模型注册服务,用户可以将 ML 模型的版本编入预定义的模型包组。如果您在 SageMaker 上构建了您的 ML 平台,那么 SageMaker 原生的模型注册表可能是一个不错的选择。
总结
ML 模型注册表是 MLOps 的核心组件,有助于减少模型实验、活动和模型生产活动之间的已知差距。我相信,MLOps 只有在拥有先进的模型注册表时才能做到正确。
如果您对这个话题或其他 MLOps 相关话题有任何意见或问题,请随时告诉我!我通常每周发布 1 篇与数据和 AI 相关的文章。请随时在 Medium 上关注我,以便在这些文章发布时获得通知。
如果您想查看更多关于现代高效数据+AI 堆栈的指南、深入探讨和见解,请订阅我的免费新闻通讯——Efficient Data+AI Stack,谢谢!
注意:如果您还没有成为 Medium 会员,而且您真的应该成为会员,因为您将获得对 Medium 的无限访问权限,您可以使用我的推荐链接注册!
非常感谢您的支持!
参考资料
MLOps 简化版:如何使用 Azure 机器学习组件运行批量预测管道
你只是想获取你的模型.pt 文件,上传到某个地方并获得预测,对吗?让我们看看如何使用 AML 基础设施来做到这一点。
·
关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 2 月 6 日
--
图片由 Sarah Dorweiler 提供,来源于 Unsplash
在日常工作中,作为数据科学家,我们经常需要使用一些模型用于内部目的。如果我们是一个人团队,将其运行在自己的机器上是可以的,但我们通常需要与其他队友共享我们的工作。共享运行脚本的 Jupyter 笔记本是一种方法,但在实验阶段之后,笔记本会变得难以管理和操作。
最近,Azure 引入了组件,这是一种“自包含的代码片段,执行机器学习管道中的一个步骤”。我们可以使用这些组件构建独立可执行的工作流,并与其他队友共享。在今天的文章中,我们将使用组件创建一个特征匹配管道。
注意:如果你对 Azure Machine Learning 完全陌生,阅读这篇文章可能会对你有所帮助,其中简要回顾了一些 AML 概念。
MLOps 101
正如Ville Tuulos在书籍《高效数据科学基础设施:如何提高数据科学家的生产力》(一本很棒的书,我强烈推荐阅读)中指出的那样,数据科学基础设施有三个构建块:
-
架构:实际代码的样子以及系统对数据科学家的感受
-
作业调度器:工作流如何触发、执行、监控以及如何处理失败
-
计算资源:代码实际执行的地方
对我们来说,架构部分包含我们的 python 代码。这部分在不同平台上变化不大。作业调度器和计算资源会根据我们运行工作流的平台而有所不同。
大多数数据科学工作流失败并不是由于代码本身,而是由于数据或我们运行代码的环境发生了变化。每个 AML 组件都有自己的环境和依赖项,因此它对我们工作流的稳定性贡献很大。
使用 AML 堆栈,我们可以通过组件构建架构,使用管道作业作为我们的作业调度器,并使用计算集群或计算实例来运行我们的工作流。
现在让我们看看如何在 AML 工作区中使用这些内容。
我们的批量预测管道
我们将创建一个批处理预测管道,使用 Superglue 和 Superpoint [1]。我们的输入可以是目标图像(例如电影海报),我们希望知道数据集中每张图像是否包含该电影海报。
你听说过 使用 OpenCV 进行特征匹配 吗?

使用 SIFT 描述符和比率测试的暴力匹配
Superglue 和 Superpoint 使用深度学习来完成这个任务。最棒的是,我们不需要重新训练模型,因为 Superpoint 是以自监督的方式训练的 [2]。
AML 组件
组件需要一个明确定义的接口(输入和输出)。我们可以在管道之间共享和重用它们,并且它们有版本控制,所以我们可以不断改进代码并根据需要进行修改。组件的代码可以用任何语言编写(python、R 等);唯一的要求是它必须能够通过 shell 命令执行。
我们的主要组件将从数据资产中读取数据,运行 Superpoint+Superglue,并输出一个包含目标图像的图像 URL 的 CSV 文件。输入的 数据资产 将包含我们要验证的图像列表和一张包含电影海报的 .jpeg 文件。
我们可以使用 Azure ML CLI v2 或 Azure ML SDK v2 构建组件。在这里我们将使用 python SDK。代码改编自 Ariya Sontrapornpol 编写的 superglue_rank_images.py 脚本。这就是组件的样子:
from mldesigner import command_component, Input, Output
from pathlib import Path
@command_component(
name="superpoint_and_superglue",
version="1",
display_name="Superpoint and Superglue",
description="Run Superpoint and Superglue models and outputs csv with matching images",
environment=dict(
conda_file=Path(__file__).parent / "conda.yaml",
image="mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04",
),
)
def superpoint_and_superglue_component(
input_data: Input(type="uri_folder"),
data_output: Output(type="uri_folder"),
):
import os
import pandas as pd
import numpy as np
import cv2
import argparse
import requests
import shutil
import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"
import torch
from pathlib import Path
import random
import matplotlib.cm as cm
import models.matching
import models.utils
def ranking_score(matches, match_confidence):
return np.sum(np.multiply(matches,match_confidence)).astype(np.float32)
def load_models(device, nms_radius, keypoint_threshold, max_keypoints, superglue, sinkhorn_iterations, match_threshold):
# Load the SuperPoint and SuperGlue models.
print('Running inference on device \"{}\"'.format(device))
config = {
'superpoint': {
'nms_radius': nms_radius,
'keypoint_threshold': keypoint_threshold,
'max_keypoints': max_keypoints
},
'superglue': {
'weights': superglue,
'sinkhorn_iterations': sinkhorn_iterations,
'match_threshold': match_threshold,
}
}
matching = models.matching.Matching(config).eval().to(device)
return matching
def get_scores(timer, device, pairs, matching, input_dir, output_dir, viz, viz_extension, resize, resize_float, query):
# [...]
def main():
torch.set_grad_enabled(False)
input_dir = input_data
print('Looking for data in directory \"{}\"'.format(input_dir))
output_dir = Path(os.path.join(input_data, "/superglue_outputs"))
output_dir.mkdir(exist_ok=True, parents=True)
print('Will write matches to directory \"{}\"'.format(output_dir))
force_cpu = False
device = 'cuda' if torch.cuda.is_available() and not force_cpu else 'cpu'
timer = models.utils.AverageTimer(newline=True)
pairs = [["query.jpg", "query.jpg"],["query.jpg", "movie_poster.jpg"]]
all_uri_files = os.listdir(input_data)
candidate_image_filenames = []
for image_name in all_uri_files:
if (image_name.endswith('.jpg') or image_name.endswith('.png') or image_name.endswith('.jpeg')):
candidate_image_filenames.append(image_name)
nms_radius = 4
keypoint_threshold = 0.005
max_keypoints = 1024
superglue = "indoor"
sinkhorn_iterations = 20
match_threshold = 0.2
matching = load_models(device, nms_radius, keypoint_threshold, max_keypoints, superglue, sinkhorn_iterations, match_threshold)
output = []
query = (output_dir / "query.jpg")
for image in candidate_image_filenames:
shutil.copyfile(os.path.join(input_dir, image), query)
viz = False
viz_extension = "png"
resize = [1200, 900]
resize_float = False
_,predicted_score = get_scores(timer, device, pairs, matching, input_dir, output_dir, viz, viz_extension, resize, resize_float, query)
item_output = {"score":predicted_score,"image":image}
output.append(item_output)
return output
output = main()
pd.DataFrame(output).to_csv((Path(data_output) / "results.csv"), index=False)
@command_component 装饰器将 python 函数转换为管道服务使用的静态规范(YAML)。我们需要提供这些元数据 [3]:
-
name是组件的唯一标识符 -
version是组件的当前版本。一个组件可以有多个版本。 -
display_name是组件在 UI 中的友好显示名称,这个名称不是唯一的 -
description通常描述该组件可以完成的任务 -
environment指定了该组件的运行时环境。该组件的环境指定了一个 docker 镜像,并引用了conda.yaml文件。
能够为每个组件提供环境非常好,因为我们可以隔离每个组件使用的依赖项。
uri_folder 输入类型具有只读挂载模式,而 uri_folder 输出类型具有读写挂载模式。有关在管道作业中访问数据的更多信息,请参考 在作业中访问数据 和 Azure 机器学习中的数据概念。
要在我们的 AML 工作区注册此组件,我们还将使用 Python SDK,以下是代码:
from azure.identity import DefaultAzureCredential, InteractiveBrowserCredential
from azure.ai.ml import MLClient, load_component
try:
credential = DefaultAzureCredential()
# Check if given credential can get token successfully.
credential.get_token("https://management.azure.com/.default")
except Exception as ex:
# Fall back to InteractiveBrowserCredential in case DefaultAzureCredential not work
credential = InteractiveBrowserCredential()
# Get a handle to workspace
ml_client = MLClient.from_config(credential=credential)
# IMPORTANT: here we import the method we've created previously
from src.component import superpoint_and_superglue_component
# Register the component
ml_client.components.create_or_update(superpoint_and_superglue_component, version="1.3")
现在我们可以在 AML 工作区中看到这个组件。

我们创建的组件
下一步是创建管道以使用该组件。
AML 管道
我们可以使用 Azure ML CLI v2、Azure ML SDK v2 或 Designer 来定义管道。由于我们的管道很简单,我们将使用设计器来定义它。要创建输入数据资产,我们也将使用工作区 UI。

使用 UI 创建数据资产
由于我们的管道有一个自定义组件,我们需要创建一个自定义管道

制作自定义管道的选项卡
提交管道运行时,我们需要指定计算资源。您也可以使用 UI 创建一个。

使用 Designer 创建管道
最后的想法
AML 组件是组织我们在数据科学日常活动中代码的绝佳方式。由于它们可以用任何语言编写,我们可以在任何可以运行 shell 命令的平台上运行这些代码,而不仅仅是在 Azure Machine Learning 内部。
通过使用 AML,我们可以获得 UI 来管理它们、创建数据资产、管理计算资源等。AML 生态系统为在工作流程中开始使用 MLOps 原则提供了一个很好的方法。
参考文献
[1] github.com/jomariya23156/SuperGlue-for-Visual-Place-Recognition
[2] DeTone, Daniel, Tomasz Malisiewicz, 和 Andrew Rabinovich. “Superpoint: 自监督兴趣点检测和描述。” IEEE 计算机视觉与模式识别会议研讨会论文集. 2018.
[3] learn.microsoft.com/en-us/azure/machine-learning/how-to-create-component-pipeline-python
MLOps 节奏:战略目标如何创造更快的迭代?
基础是了解组织的需求
·
关注 发表在 Towards Data Science ·12 min 阅读·2023 年 6 月 14 日
--
照片由 Cristina Gottardi 拍摄,发布在 Unsplash,作者创作
战略目标是伟大开发节奏(MLOps 节奏)的基础。它有助于确定开发的方向、资源的重点以及如何高效执行项目。它为 MLOps 节奏的其他所有方面奠定了基础。

MLOps 节奏的因素,作者创作
MLOps 节奏是组织规划、开发和部署机器学习模型的速度。这个节奏越快,组织利用机器学习的优势就越迅速,从而获得显著的竞争优势。
快速周转不仅仅是工具。
towardsdatascience.com
影响 ML Ops 节奏的因素在很大程度上由组织决定。对 MLOps 节奏的良好理解对决策者至关重要,因为它直接影响从 ML 或 AI 应用中获得的价值。这些目标指导数据产品的运营和产品模型,以及它们驱动的决策。
一家公司可以拥有出色的领导者和愿景,也可以拥有知识丰富且经验丰富的业务团队。但是,如果没有明确的战略目标来指导其运营,ML 模型规划、开发和部署将会减缓。
这是 MLOps 节奏的基础,实际上影响了我们对其他因素的效率的看法。
战略目标如何影响 MLOps 节奏?

照片由 Steven Lelham 拍摄,发布在 Unsplash
设定良好的战略目标本质上是关于有效的决策,辅以高效的流程。那些能够迅速适应并拥有高效流程的人,能够从他们的努力中获得最多的价值。
战略目标还会影响操作模型。物流和运营有助于掌握竞争环境——它将信息传递给决策者,并将决策传达给团队以执行。战略目标帮助集中 MLOps 节奏。
节奏影响组织交付高质量、可靠模型的速度。组织如何加快或提高节奏的效率和效果,推动了它们的流程和运营模型。
在现代大型语言模型的时代,这一点极为重要,因为这些模型的复杂性需要良好的运营、项目和产品管理流程。
你必须首先有明确的方向才能优化你的 MLOps 节奏。这是基础。战略目标有助于实现这一点。
这如何影响组织
理解战略目标如何影响 MLOps 节奏需要从三个层次来看待:个体贡献者、经理以及中高层管理。

战略目标如何加快组织的节奏,作者创作
理解并传达战略目标有助于加快组织中不同层级的 MLOps 节奏。
-
个体贡献者:明确的战略目标有助于制定清晰的项目目标。清晰的任务有助于提升士气,识别瓶颈,并提高工作效率和价值。这也有助于提升他们向上级传递信息的质量。
-
经理:较大的战略对于项目管理至关重要,有助于资源分配、任务优先级排序,以及将团队努力与组织目标和时间线对齐。或者获取特定赞助者的支持。
-
中高层管理:明确的战略目标有助于提供背景,并做出明智的决策,评估新举措,并对扩展操作做出明智的决策。
良好的战略目标有助于整个组织,从上到下,允许重要信息向上传递,以及做出明智的决策。这样可以加快周转时间,评估项目是否有效,当然,还可以评估 ML 项目是否符合战略目标。
对不同层级的 MLOps 节奏有良好的理解,有助于每个人更好地应对 ML 项目的复杂性。
从“为什么”开始,以“如何”结束

照片由 Startaê Team 提供,来源于 Unsplash
战略目标必须从“为什么”开始。了解它对更大业务目标的重要性很重要。这有助于优先分配资源,避免不必要的复杂性,加快开发决策,甚至决定是否需要 ML 解决方案。
这种战略从决策者传递到项目和产品经理,最终到构建机器学习(ML)或人工智能(AI)解决方案的数据科学团队。当每个人都理解意图时,这会改善团队之间的沟通——这是任何行业良好操作节奏的基本要素。减少了在澄清问题上的会议时间,增加了实施的时间。
用例必须始终以最终产品为目标,然后筛选技术方面和限制。这是 ML 项目的“为什么”,绝对有必要确定项目是否与战略目标对齐,所需的资源等。
“为什么”形成了在模型构建过程开始时需要提出的其他关键问题的基础。它对 MLOps 节奏的其他因素有着显著影响。

制定 MLOps 节奏的战略目标,作者创作
有一些特定的“为什么”问题需要被提出,这有助于制定良好的 ML 战略目标:
我们为什么要构建这个?
对于某些用例,ML 并非总是必需的。这个问题有助于定义是否需要它,从而节省可以用于其他地方的资源和时间。它还帮助定义是否有多个用例。这有助于界定范围并设定方向。
定义机器学习模型的目的至关重要,因为它为整个项目设定了方向。它有助于将所有的努力和资源对准一个共同的目标,确保时间的高效利用,并提升 MLOps 的节奏。它有助于
我们希望它做什么?
这种关注意味着将业务用例和期望的最终产品放在首位。重要的是这些用例是否与战略目标对齐。机器学习模型需要能够回答这些用例。

从“为什么”到“如何”的流程,作者创作
知道你想做什么有助于使“为什么”变得可行。在开发过程中,这有助于指导技术战略和执行。在这个阶段的定义减少了试错,尤其是在模型构建、实验和部署阶段,从而加快了项目时间线。
潜在的风险是什么?
识别潜在风险可以实现缓解。模型并不总是有效。良好的战略目标帮助领导层和构建团队创新并设计替代解决方案,以实现预期的结果。它有助于建立替代路径,以实现组织对机器学习的战略目标。

什么与潜在风险,作者创作
适应能力至关重要。计划和现实之间的摩擦是常见的。清晰、沟通良好且理解到位的战略目标可以防止意外情况阻碍或暂停项目。它允许 MLOps 的节奏保持稳定,并实现高效的周转。
我们希望什么时候完成这项工作?
战略目标不仅指导时间表,还指导资源的时机和协调,以创建一个机器学习解决方案。从一开始并不是所有资源都是必需的。特定的资源,例如资金、共享工具专家、赞助商等,必须在适当的时间发挥作用,以避免影响其他项目。

完成时间,作者创作
建立时间表为工作设定了节奏,并有助于按顺序规划任务。这确保了任务按照优化项目节奏的顺序进行,从而实现按时交付。时间框定很重要,否则项目的某些部分可能会耗费过多时间。
如果风险和时间表超过了所产生的价值,你可能需要回到“为什么”这一步。这可能看起来很繁琐,但即使是非正式的审查也可以节省大量的开发成本、生产力损失和时间。
我们将如何构建它?
这通常留给运营和构建团队。战略层面的人将不太参与其中。然而,回答模型的“为什么”、“什么”、“风险”和“何时”是非常重要的。这为这些团队的工作奠定了基础。

从“为什么”到“如何”的时间线,作者创作
许多机器学习项目最终超出预算,原因在于这些问题没有得到解答。对于项目和产品经理来说,这是调整构建团队的参考依据。对于构建团队而言,它有助于使经理们有效地指导数据科学家、数据工程师和机器学习工程师,通过提供一个框架来对齐研究、开发、实验、测试模型。
战略目标影响他们构建模型的方式——不同的目标会导致不同的构建结果。
案例研究:战略目标如何影响 MLOps 节奏
在我创业的早期,一家中型零售公司找到了我们。他们在实施机器学习解决方案方面遇到了困难——运营过程缓慢,机器学习项目未能创造价值。过去的项目未能提供价值或结果。他们意识到有些东西缺失了。
我们首先通过发现会话和研讨会来了解他们的战略目标。我们很快发现他们围绕着一个他们想要使用的机器学习算法进行构建,而不是询问为什么需要使用它。
原因
我们关注的第一个问题是“为什么要构建它”。我们发现他们之前的几个机器学习项目是不必要的。一些用例并不需要机器学习模型,可以通过条件逻辑等更简单的方法解决。其他用例确实需要机器学习模型,但使用了比实际需要的更复杂的模型。我们将复杂的用例分解为更小的、有价值的用例,这有助于项目经理明确监督 MLOps 团队的工作流程。
数据和机器学习产品
接下来,我们专注于“什么”。我们关注业务用例和期望的数据产品。然后帮助他们缩小和完善战略目标。这帮助他们减少了在模型构建、开发、实验和部署阶段的很多试错过程。这缩短了项目时间表,并提高了资源分配的效率。
风险缓解
模型并不总是按计划工作。因此,我们与公司紧密合作,识别机器学习项目的潜在风险,规划替代方案,并确定何时终止项目。明确的战略目标使领导层和构建团队能够识别项目何时成为成本中心,从而调整他们的努力。这也帮助构建团队创新和设计替代方案,确保了 MLOps 节奏的稳定和高效的周转,即使面临意外挑战。这减少了与项目相关的成本,无论是在机器学习模型开发链条的上下游。
项目时间表
我们还帮助公司建立了现实的时间表,协调资源的使用,以避免与其他进行中的项目冲突。使用时间限制确保资源在设定时间内分配。这包括简化沟通,以便构建团队对时间表的现实评估迅速传达到决策者。
如何构建解决方案
我们将“如何”问题留给了运营和建设团队。退一步,让专家和领域专家学习过程并提供长期价值。我们确保我们可以帮助,并设置会议来确保他们对流程有坚实的基础。这种对齐帮助项目和产品经理有效地引导数据科学家、数据工程师和 ML 工程师。
遵循这种战略目标设定方法,公司能够避免预算超支,并建立符合并实现战略目标的 ML 解决方案。
公司注意到 ML 倡议的效率显著提升,并且比以前的高价值开发时间更加高效。
业务和技术战略聚焦战略目标

集中的战略目标需要对业务策略和技术策略有良好的理解。每个都贡献独特的力量,同时考虑两者可以加快您的 MLOps 节奏。业务和技术策略对您为 ML 开发设定的战略目标产生了深远影响。
业务战略关注于这些战略目标如何与企业的盈利能力联系在一起。
而技术战略侧重于如何利用现有技术资源实现这些目标。
每个因素对 MLOps 的节奏都有明显影响。不明确的业务策略可能通过设定超出可用技术范围的过于雄心勃勃的结果来减缓技术策略。而不明确的技术策略可能会为不需要的业务用例开发 ML 模型。
业务策略和技术策略都有几个影响战略目标的因素。

业务战略支柱,作者创作
对于业务策略,有三个关键支柱:
-
业务策略与愿景:一个明确定义的愿景,明晰的目标路线图成为所有利益相关者的指南。将所有人的努力都对齐到这些目标上,自然加速了运营,简化了决策过程,并消除了不必要的弯路。
-
项目和产品管理:有效管理项目和产品的艺术是操作节奏的核心。当项目得到有效管理时,任务能够及时执行,从而增强整体的进展速度,推动项目向目标迈进。
-
当前业务资源:全面了解可用资源能够展现当前的现实图景。这使得高效的规划和分配成为可能,最有前景的倡议和活动得到最多的关注和价值。

技术策略,作者创作
在技术策略中,有四个基本要素:
-
技术资源:技术资源的战略分配是技术策略的基础。资源可以是工具、专家、资金、赞助等。在合适的时间和地点确保适量的资源可以加快项目的进展。
-
可用基础设施:了解现有技术设置有助于确定业务战略目标是否可行。及早识别潜在的障碍,使你能够快速调整、利用资源并改变项目和数据产品规划。
-
机器学习策略:这是在项目中应用机器学习的蓝图。它定义了 ML 模型将解决的具体问题、实验、迭代和部署。它帮助开发团队专注于关键的可操作项目——减少数据、技术和模型债务。
-
开发框架:这些是指导框架,能够顺利进行版本控制、跟踪和测试模型、管道和部署。它还帮助技术团队了解最佳实践,以便进行有效的部署和开发。框架建立良好时,可以确保一致性和效率,减少错误,加速项目进程。
在任何项目开始之前建立这些要素是非常重要的。它能防止瓶颈,促进更顺利的过程,并使项目稳步朝着设定目标前进。
不明确的更大战略目标可能导致 ML 项目失败,从而产生大量的技术、数据和模型债务。在某些情况下,这甚至可能导致数据科学部门被视为成本中心,而 ML 举措被认为是需要避免的高成本开支。
通过正确的对齐、评估和定义商业和技术策略,组织可以显著加快 MLOps 的节奏,同时保持数据科学部门和团队不断交付并展示价值。
最后的思考
了解战略目标对于评估 MLOps 节奏的速度至关重要。定义战略目标对于改进 ML 项目的战略决策以及使其更有效和高效非常重要。
总结:
-
设定明确的战略目标,并定义用例和数据产品对齐。
-
首先问清楚业务用例背后的原因——这将有助于评估虚荣项目与业务需求之间的差异。
-
关注战略目标如何与技术和商业策略对齐——这对于确保 ML 的商业目标得以有效实施非常重要。
感谢阅读!
我经常撰写有关数据策略、MLOps 和机器学习的文章。可以在Linkedin、YouTube和Twitter与我联系。
如果你喜欢这篇文章,你可能也会喜欢我的其他文章:
为什么这对你的数据科学职业至关重要?
blaza-matt.com [## MLOps 简化:什么是工件?
在机器学习中,工件是指在机器学习过程中生成的任何文件或对象。
blaza-matt.com ## MLOps:什么是操作节奏?
快速的周转不仅仅是工具。
[towardsdatascience.com
MLOps-技巧与窍门-75 个代码片段

MLOps 和数据工程
·
关注 发表在 Towards Data Science ·21 min read·2023 年 3 月 15 日
--
介绍:
MLOps,或称机器学习运维,是指一套简化机器学习模型开发、部署和维护的实践方法,旨在弥合数据科学与软件工程之间的差距。本文旨在提供有关 MLOps 和数据工程的宝贵技巧和窍门,涵盖模型训练、数据预处理、性能优化、监控和部署等广泛主题。

作者图片
- Dask-ML-并行化模型训练:
-
使用 Dask-ML 并行训练和评估机器学习模型,充分利用硬件的全部能力。
-
使用 Dask-ML,您可以迅速扩展机器学习工作负载,跨多个核心、处理器甚至集群,轻松训练和评估大型数据集上的大型模型。
import dask_ml.model_selection as dcv
from sklearn.datasets import make_classification
from sklearn.svm import SVC
# Create a large dataset
X, y = make_classification(n_samples=100000, n_features=20, random_state=42)
# Define your model
model = SVC()
# Train your model in parallel using Dask-ML
params = {"C": dcv.Categorical([0.1, 1, 10]), "kernel": dcv.Categorical(["linear", "rbf"])}
search = dcv.RandomizedSearchCV(model, params, n_iter=10, cv=3)
search.fit(X, y)
2. Feature Tools: Featuretools 是一个开源 Python 库,用于自动化特征工程,允许您从原始数据中生成新特征,手动工作量最小化。
import featuretools as ft
# Load your raw data into an entityset
es = ft.EntitySet(id="my_data")
es = es.entity_from_dataframe(entity_id="customers", dataframe=data, index="customer_id")
# Define relationships between entities
# ...
# Automatically generate new features
feature_matrix, feature_defs = ft.dfs(entityset=es, target_entity="customers", max_depth=2)
3. Tensorboard: TensorBoard 是一个强大的可视化工具,用于 TensorFlow,允许您监控模型的性能并跟踪训练和评估过程中的各种指标。
import tensorflow as tf
from tensorflow.keras.callbacks import TensorBoard
# Define your model
model = tf.keras.Sequential([...])
# Compile your model
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
# Create a TensorBoard callback
tensorboard_callback = TensorBoard(log_dir="./logs")
# Train your model with the TensorBoard callback
model.fit(X_train, y_train, epochs=10, validation_data=(X_test, y_test), callbacks=[tensorboard_callback])
4. Tensorflow Serving:
-
TensorFlow Serving 是一个高性能的机器学习模型服务系统,专为生产环境设计。
-
TensorFlow Serving 支持多个模型、模型版本控制,以及模型的自动加载和卸载,使您能够轻松管理和大规模服务机器学习模型。
# Save your TensorFlow model in the SavedModel format
model.save("my_model/1/")
# Install TensorFlow Serving
echo "deb [arch=amd64] http://storage.googleapis.com/tensorflow-serving-apt stable tensorflow-model-server tensorflow-model-server-universal" | sudo tee /etc/apt/sources.list.d/tensorflow-serving.list && \
curl https://storage.googleapis.com/tensorflow-serving-apt/tensorflow-serving.release.pub.gpg | sudo apt-key add -
sudo apt-get update && sudo apt-get install tensorflow-model-server
# Start TensorFlow Serving with your model
tensorflow_model_server --rest_api_port=8501 --model_name=my_model --model_base_path=$(pwd)/my_model
5. 使用 Optuna 自动化超参数调优: Optuna 是一个强大且灵活的优化库,可以自动探索和优化机器学习模型的超参数。
import optuna
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
def objective(trial):
n_estimators = trial.suggest_int("n_estimators", 10, 200)
max_depth = trial.suggest_int("max_depth", 3, 20)
clf = RandomForestClassifier(n_estimators=n_estimators, max_depth=max_depth)
score = cross_val_score(clf, X_train, y_train, cv=5).mean()
return score
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=50)
best_params = study.best_params
6. SHAP: 使用 SHAP(SHapley Additive exPlanations)来解释机器学习模型的输出,并深入了解其行为。
import shap
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
# Load and prepare your data
# ...
# Train a RandomForestRegressor
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
model = RandomForestRegressor()
model.fit(X_train, y_train)
# Explain the model's predictions using SHAP
explainer = shap.Explainer(model)
shap_values = explainer(X_test)
# Plot the SHAP values for a single prediction
shap.plots.waterfall(shap_values[0])
7. Ray: Ray Tune 是一个强大且灵活的分布式超参数调优库,使您能够利用硬件的全部能力来优化机器学习模型。
from ray import tune
from ray.tune.schedulers import ASHAScheduler
from sklearn.datasets import make_classification
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
def train_model(config):
n_estimators = config["n_estimators"]
max_depth = config["max_depth"]
clf = RandomForestClassifier(n_estimators=n_estimators, max_depth=max_depth)
score = cross_val_score(clf, X_train, y_train, cv=3).mean()
tune.report(mean_accuracy=score)
# Load your data
X, y = make_classification(n_samples=1000, n_features=20, random_state=42)
# Define the search space for hyperparameters
config = {
"n_estimators": tune.randint(10, 200),
"max_depth": tu:ne.randint(3, 20)
}
# Set up Ray Tune
scheduler = ASHAScheduler(metric="mean_accuracy", mode="max")
analysis = tune.run(train_model, config=config, scheduler=scheduler, num_samples=50)
# Get the best hyperparameters
best_params = analysis.best_config
8. 使用 MLflow 进行实验跟踪: 使用 MLflow,您可以比较不同的实验,重现以前的结果,并与他人共享您的工作,使协作和迭代更加高效。
import mlflow
import mlflow.sklearn
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
# Load your data
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# Train your model and log metrics with MLflow
with mlflow.start_run():
clf = RandomForestClassifier(n_estimators=100, max_depth=10)
clf.fit(X_train, y_train)
train_accuracy = clf.score(X_train, y_train)
test_accuracy = clf.score(X_test, y_test)
mlflow.log_param("n_estimators", 100)
mlflow.log_param("max_depth", 10)
mlflow.log_metric("train_accuracy", train_accuracy)
mlflow.log_metric("test_accuracy", test_accuracy)
mlflow.sklearn.log_model(clf, "model")
9. Scikit-learn: 流水线:使用 Scikit-learn 的**Pipeline**将多个预处理步骤和最终估算器链接在一起。
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
pipe = Pipeline([
("scaler", StandardScaler()),
("classifier", LogisticRegression())
])
pipe.fit(X_train, y_train)
10. Scikit-learn: 网格搜索:使用GridSearchCV进行超参数调优。
from sklearn.model_selection import GridSearchCV
param_grid = {
"classifier__C": [0.1, 1, 10],
"classifier__penalty": ["l1", "l2"]
}
grid_search = GridSearchCV(pipe, param_grid, cv=5)
grid_search.fit(X_train, y_train)
11. Joblib: joblib是一个流行的库,用于保存和加载 Scikit-learn 模型。使用dump()将模型保存到文件中,使用load()从文件中恢复模型。
import joblib
# Save the model
joblib.dump(grid_search.best_estimator_, "model.pkl")
# Load the model
loaded_model = joblib.load("model.pkl")
12. Tensorflow: 简单神经网络。使用 Keras API 定义一个具有密集(全连接)层的简单前馈神经网络。
import tensorflow as tf
model = tf.keras.Sequential([
tf.keras.layers.Dense(64, activation="relu", input_shape=(10,)),
tf.keras.layers.Dense(32, activation="relu"),
tf.keras.layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"])
13. 提前停止: 提前停止的代码片段
early_stopping = tf.keras.callbacks.EarlyStopping(monitor="val_loss", patience=3)
history = model.fit(X_train, y_train, epochs=100, validation_split=0.2, callbacks=[early_stopping])
14. Tensorflow 模型保存和加载: 使用save()方法将模型结构、权重和优化器状态保存到单个文件中。使用load_model()从文件中恢复保存的模型。
# Save the model
model.save("model.h5")
# Load the model
loaded_model = tf.keras.models.load_model("model.h5")
15. Dask: 并行化操作:使用 Dask 对大型数据集进行并行化操作。
import dask.array as da
x = da.ones((10000, 10000), chunks=(1000, 1000))
y = x + x.T
z = y.sum(axis=0)
result = z.compute()
- TPOT:自动化机器学习: TPOT (基于树的管道优化工具) 是一个基于遗传算法的自动化机器学习库。使用
TPOTClassifier或TPOTRegressor来优化你的数据的机器学习管道。
from tpot import TPOTClassifier
tpot = TPOTClassifier(generations=5, population_size=20, verbosity=2)
tpot.fit(X_train, y_train)

作者提供的图片
17. 类别编码器: 类别编码器是一个提供各种类别变量编码方法的库,如目标编码、独热编码和序数编码。
import category_encoders as ce
encoder = ce.TargetEncoder()
X_train_encoded = encoder.fit_transform(X_train, y_train)
X_test_encoded = encoder.transform(X_test)
18. Imbalanced-learn: 是一个提供各种处理不平衡数据集技术的库,如过采样、欠采样和组合方法。使用适当的重采样技术,如 SMOTE,在训练模型之前平衡你的数据集。
from imblearn.over_sampling import SMOTE
smote = SMOTE()
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)
19. Auto-sklearn: 是一个自动化机器学习库,它封装了 Scikit-learn,提供自动化模型和预处理选择。使用AutoSklearnClassifier或AutoSklearnRegressor来优化机器学习管道数据。
from autosklearn.classification import AutoSklearnClassifier
auto_classifier = AutoSklearnClassifier(time_left_for_this_task=600)
auto_classifier.fit(X_train, y_train)
20. Scikit-learn:列变换器: ColumnTransformer 允许你对输入数据的不同列应用不同的预处理步骤,这在处理混合数据类型时特别有用。
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
preprocessor = ColumnTransformer(
transformers=[
("num", StandardScaler(), ["numerical_feature_1", "numerical_feature_2"]),
("cat", OneHotEncoder(), ["categorical_feature"]),
]
)
X_train_transformed = preprocessor.fit_transform(X_train)
X_test_transformed = preprocessor.transform(X_test)
21. RandomizedSearchCV 是 GridSearchCV 的一种替代方法,通过随机抽样固定数量的参数设置来更有效地搜索参数空间。定义一个参数分布字典,其中键是参数名称(包括使用管道时的步骤名称),值是用于抽样参数值的分布。将模型(或管道)和参数分布传递给 RandomizedSearchCV 并拟合数据。
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import uniform
param_dist = {
"classifier__C": uniform(loc=0, scale=4),
"preprocessor__num__with_mean": [True, False],
}
random_search = RandomizedSearchCV(pipe, param_dist, n_iter=10, cv=5, scoring="accuracy")
random_search.fit(X_train, y_train)
22. TensorFlow 数据验证: 使用 TensorFlow Data Validation (TFDV) 来验证和探索你的数据。
import tensorflow_data_validation as tfdv
stats = tfdv.generate_statistics_from_csv(data_location="train.csv")
schema = tfdv.infer_schema(statistics=stats)
tfdv.display_schema(schema=schema)
23. TensorFlow 模型分析: 使用 TensorFlow Model Analysis (TFMA) 来评估你的 TensorFlow 模型。
import tensorflow_model_analysis as tfma
eval_shared_model = tfma.default_eval_shared_model(
eval_saved_model_path="path/to/saved_model"
)
results = tfma.run_model_analysis(
eval_shared_model=eval_shared_model,
data_location="test.tfrecords",
file_format="tfrecords",
slice_spec=[tfma.slicer.SingleSliceSpec()]
)
tfma.view.render_slicing_metrics(results)
24. TensorFlow Transform: 使用 TensorFlow Transform (TFT) 来预处理你的数据,以便用于 TensorFlow 模型。
import tensorflow_transform as tft
def preprocessing_fn(inputs):
outputs = {}
outputs["scaled_feature"] = tft.scale_to_z_score(inputs["numerical_feature"])
outputs["one_hot_feature"] = tft.compute_and_apply_vocabulary(inputs["categorical_feature"])
return outputs
25. TensorFlow Extended (TFX): 使用 TensorFlow Extended (TFX) 创建端到端的机器学习管道。
from tfx.components import CsvExampleGen, Trainer
from tfx.orchestration.experimental.interactive.interactive_context import InteractiveContext
context = InteractiveContext()
example_gen = CsvExampleGen(input_base="path/to/data")
context.run(example_gen)
trainer = Trainer(
module_file="path/to/trainer_module.py",
examples=example_gen.outputs["examples"],
train_args=trainer_pb2.TrainArgs(num_steps=10000),
eval_args=trainer_pb2.EvalArgs(num_steps=5000)
)
context.run(trainer)

作者提供的图片
26. CuPy: CuPy 是一个提供类似于 NumPy 接口的 GPU 加速计算库。使用 CuPy 数组,它们具有类似于 NumPy 数组的接口,在 GPU 上执行计算。CuPy 提供了许多常见的 NumPy 函数,使你能够使用熟悉的语法进行 GPU 加速计算。
import cupy as cp
x = cp.array([1, 2, 3, 4, 5])
y = cp.array([6, 7, 8, 9, 10])
z = cp.dot(x, y)
27. RAPIDS是一个用于数据科学的 GPU 加速库套件,包括 cuDF(类似于 Pandas 的 GPU 加速数据框库)和 cuML(类似于 Scikit-learn 的 GPU 加速机器学习库)。使用 cuDF DataFrames 在 GPU 上执行数据操作任务,使用 cuML 模型在 GPU 上训练和评估机器学习模型。
import cudf
import cuml
df = cudf.read_csv("data.csv")
kmeans_model = cuml.KMeans(n_clusters=5)
kmeans_model.fit(df)
28. FastAPI是一个现代化的高性能 Web 框架,用于使用 Python 构建 API,特别适合机器学习模型。
-
创建
FastAPI的实例,并使用装饰器(如@app.post())定义 API 端点。 -
使用
uvicorn运行 FastAPI 应用,指定主机和端口。
from fastapi import FastAPI
import uvicorn
app = FastAPI()
@app.post("/predict")
async def predict(text: str):
prediction = model.predict([text])
return {"prediction": prediction}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
29. Streamlit是一个库,用于快速创建机器学习和数据科学的互动 Web 应用,仅使用 Python。
-
使用 Streamlit 的简单 API 创建用户界面元素,例如文本输入和滑块,并显示输出或可视化。
-
使用命令
streamlit run app.py在终端中运行 Streamlit 应用。
import streamlit as st
st.title("My Streamlit App")
input_text = st.text_input("Enter some text:")
st.write(f"You entered: {input_text}")
slider_value = st.slider("Select a value:", 0, 100, 50)
st.write(f"Slider value: {slider_value}")
30. Docker 文件: 创建一个 Dockerfile 来定义适用于机器学习应用的自定义 Docker 镜像。
FROM python:3.8
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
-
使用
FROM关键字指定基础镜像,例如官方 Python 镜像。 -
使用
WORKDIR关键字设置后续指令的工作目录。 -
使用
COPY关键字将文件和目录从主机系统复制到镜像中。 -
使用
RUN关键字在构建过程中执行命令,例如安装依赖项。 -
使用
CMD关键字定义容器启动时要运行的默认命令。
31. 构建 Docker 镜像:
docker build -t my_ml_app:latest .
32. 运行 Docker 容器:使用docker run命令从镜像创建并启动 Docker 容器。使用-p标志将主机端口映射到容器端口,允许外部访问容器内运行的服务。
docker run -p 5000:5000 my_ml_app:latest
32. Kubernetes YAML 配置文件:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-ml-app
spec:
replicas: 3
selector:
matchLabels:
app: my-ml-app
template:
metadata:
labels:
app: my-ml-app
spec:
containers:
- name: my-ml-app-container
image: my_ml_app:latest
ports:
- containerPort: 5000
---
apiVersion: v1
kind: Service
metadata:
name: my-ml-app-service
spec:
selector:
app: my-ml-app
ports:
- protocol: TCP
port: 80
targetPort: 5000
type: LoadBalancer
-
使用
apiVersion、kind和metadata来定义 Kubernetes 资源类型和元数据。 -
使用
spec来定义资源的期望状态,例如副本数量、容器镜像和暴露的端口。 -
使用
---分隔符在同一文件中定义多个资源,例如一个 Deployment 和一个 Service。
33. kubectl: 使用kubectl命令行工具来管理 Kubernetes 集群和资源。
# Apply the Kubernetes configuration file
kubectl apply -f my_ml_app.yaml
# List all deployments
kubectl get deployments
# List all services
kubectl get services
# Scale the deployment
kubectl scale deployment my-ml-app --replicas=5
# Delete the deployment and service
kubectl delete -f my_ml_app.yaml
34. 组织你的项目:
my_ml_project/
|-- data/
| |-- raw/
| |-- processed/
|-- models/
|-- notebooks/
|-- src/
| |-- features/
| |-- models/
| |-- utils/
|-- Dockerfile
|-- requirements.txt
-
使用单独的目录存储数据、模型、笔记本和源代码。
-
进一步细分目录以分开原始数据和处理后的数据或不同类型的源代码模块。
35. 模型版本控制: 使用版本控制工具如 DVC 或 MLflow 来跟踪不同版本的训练过的机器学习模型。
-
将模型工件(例如权重、元数据)存储在集中式存储系统中,例如 Amazon S3 或 Google Cloud Storage。
-
使用版本控制工具跟踪模型版本、相关训练数据和超参数。
-
通过跟踪性能指标和训练配置来启用模型比较和可重复性。
36. 自动化测试:
-
使用
unittest或pytest等测试库来编写和运行测试。 -
用单元测试测试单个函数和类,用集成测试测试组件之间的交互。
-
执行端到端测试,以确保整个系统按预期工作,包括模型服务和 API 端点。
37. Papermill:
-
Papermill 允许你通过为特定单元格注入新值来参数化 Jupyter Notebooks。
-
以编程方式执行 Notebooks,并生成具有不同参数值的报告,而无需手动干预。
import papermill as pm
pm.execute_notebook(
input_path='input_notebook.ipynb',
output_path='output_notebook.ipynb',
parameters={'param1': 'value1', 'param2': 'value2'}
)
38. 环境管理: 使用 Conda 或 virtualenv 等工具为项目创建隔离的环境。
# Create a new Conda environment
conda create -n my_ml_env python=3.8
# Activate the environment
conda activate my_ml_env
# Install packages
conda install pandas scikit-learn
# Deactivate the environment
conda deactivate
39. 逐步模型加载: 将大型模型分块加载,以减少内存消耗并提高性能。
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
chunksize = 10000
model = LinearRegression()
for i, chunk in enumerate(pd.read_csv("large_dataset.csv", chunksize=chunksize)):
X_chunk = chunk.drop("target", axis=1)
y_chunk = chunk["target"]
model.partial_fit(X_chunk, y_chunk)
print(f"Processed chunk {i + 1}")
40. 特征编码:
-
特征编码技术将分类变量转换为机器学习模型可以使用的数值表示。
-
One-hot 编码为每个类别创建二进制列,而目标编码则用该类别的目标变量均值替换每个类别。
import pandas as pd
from sklearn.preprocessing import OneHotEncoder
data = pd.DataFrame({"Category": ["A", "B", "A", "C"]})
encoder = OneHotEncoder()
encoded_data = encoder.fit_transform(data)
print(encoded_data.toarray())
41. 数据验证: 使用数据验证框架,如 Great Expectations、Pandera 或自定义验证函数,验证数据的质量和一致性。
import pandera as pa
from pandera import DataFrameSchema, Column, Check
schema = DataFrameSchema({
"age": Column(pa.Int, Check(lambda x: 18 <= x <= 100)),
"income": Column(pa.Float, Check(lambda x: x >= 0)),
"gender": Column(pa.String, Check(lambda x: x in ["M", "F", "Other"])),
})
# Validate your DataFrame
validated_df = schema.validate(df)
42. 数据版本控制: 使用数据版本控制工具,如 DVC 或 Pachyderm,跟踪数据集的变化,并确保在不同实验和模型版本之间的可重复性。
# Initialize DVC in your project
dvc init
# Add your dataset to DVC
dvc add data/my_dataset
# Commit the changes to your Git repository
git add data/my_dataset.dvc .dvc/config
git commit -m "Add my_dataset to DVC"
43. 使用特征存储: 实施像 Feast 或 Hopsworks 这样的特征存储来存储、管理和提供机器学习模型的特征。
from feast import FeatureStore
# Initialize the feature store
store = FeatureStore(repo_path="path/to/your/feature_store")
# Fetch features for training
training_df = store.get_historical_features(
entity_df=entity_df,
feature_refs=["your_feature_name"]
).to_df()
# Fetch features for serving
feature_vector = store.get_online_features(
feature_refs=["your_feature_name"],
entity_rows=[{"your_entity_key": "your_value"}]
).to_dict()
特征存储可以帮助你集中管理特征,确保一致性并减少不同模型和实验之间的重复。
44. 特征缩放: 应用特征缩放技术,如 MinMax 缩放、标准化缩放或归一化,以确保你的特征具有相似的尺度和分布。
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
X, y = load_iris(return_X_y=True)
# Scale features using standard scaling
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
45. 降维: 应用降维技术,如 PCA、t-SNE 或 UMAP,以减少数据集中的特征数量,同时保留重要的模式和关系。
from sklearn.datasets import load_iris
from sklearn.decomposition import PCA
X, y = load_iris(return_X_y=True)
# Apply PCA to reduce the dimensionality of the dataset
pca = PCA(n_components=2)
X_reduced = pca.fit_transform(X)
46. Pandas 链式操作: 将 Pandas 操作链在一起,以创建更具可读性和简洁的数据处理代码。
import pandas as pd
data = pd.read_csv("my_data.csv")
# Chain Pandas operations
result = (
data.query("age >= 30")
.groupby("city")
.agg({"salary": "mean"})
.sort_values("salary", ascending=False)
)
47. 使用pipe函数: 使用pipe函数在你的 Pandas 链式操作工作流中集成自定义函数或操作。
import pandas as pd
def custom_operation(df, column, value):
return df[df[column] > value]
data = pd.read_csv("data.csv")
# Integrate custom operations using 'pipe'
result = (
data.pipe(custom_operation, "age", 18)
.groupby("city")
.agg({"salary": "mean"})
.sort_values("salary", ascending=False)
)
48. Pandas 内置绘图: 使用 Pandas 的内置绘图函数进行快速且简单的数据可视化。
import pandas as pd
data = pd.read_csv("my_data.csv")
# Create a bar plot of average salary by city
data.groupby("city")["salary"].mean().plot(kind="bar")
49. 使用 Missingno 可视化缺失数据: 使用 Missingno 库可视化数据集中缺失的数据。
import pandas as pd
import missingno as msno
data = pd.read_csv("data.csv")
# Visualize missing data
msno.matrix(data)
50. 使用 SQL 数据库: 你可以在 Python 中使用sqlite3库与 SQLite 数据库进行交互。例如,你可以在 SQLite 数据库中创建一个表并插入一些数据:
import sqlite3
# Connect to an SQLite database
conn = sqlite3.connect('example.db')
# Create a table
conn.execute('CREATE TABLE IF NOT EXISTS my_table (id INTEGER PRIMARY KEY, name TEXT)')
# Insert some data
conn.execute('INSERT INTO my_table (id, name) VALUES (?, ?)', (1, 'John'))
conn.execute('INSERT INTO my_table (id, name) VALUES (?, ?)', (2, 'Jane'))
# Commit the changes
conn.commit()
# Retrieve data
cursor = conn.execute('SELECT * FROM my_table')
for row in cursor:
print(row)
51. Requests 库: 使用 requests 库进行 HTTP 请求:requests 库提供了一种简单的方法来向 API 或网站发送 HTTP 请求。以下是如何进行 GET 请求的示例。
import requests
# make a GET request to a website
response = requests.get('https://www.google.com')
# print the response content
print(response.content)
52. OS 库: 使用 os 库操作文件和目录:os 库提供了与文件和目录交互的功能。
import os
# create a directory
os.mkdir('my_directory')
53. 处理 JSON:
将 Python 数据编码为 JSON 格式:
import json
data = {
"name": "Mark",
"age": 28,
"gender": "Male"
}
json_data = json.dumps(data)
print(json_data)
将 JSON 数据解码为 Python 格式:
import json
json_data = '{"name": "Mark", "age": 28, "gender": "Male"}'
data = json.loads(json_data)
print(data)
54. 处理 CSV 文件:使用 CSV 模块。
import csv
# Reading a CSV file
with open('example.csv', 'r') as file:
csv_reader = csv.reader(file)
for row in csv_reader:
print(row)
# Writing to a CSV file
with open('example.csv', 'w', newline='') as file:
csv_writer = csv.writer(file)
csv_writer.writerow(['Name', 'Age', 'Gender'])
csv_writer.writerow(['John', 25, 'Male'])
csv_writer.writerow(['Jane', 30, 'Female'])
55. 使用 SQL Alchemy 进行数据库访问: SQL Alchemy 是一个流行的 Python 库,用于处理数据库。它提供了一个简单的接口,用于连接到各种数据库并执行 SQL 查询。
from sqlalchemy import create_engine
# Connect to a PostgreSQL database
engine = create_engine('postgresql://username:password@host:port/database_name')
# Execute a SQL query and return the results as a dataframe
query = "SELECT * FROM table_name WHERE column_name > 100"
df = pd.read_sql(query, engine)
# Write a dataframe to a new table in the database
df.to_sql('new_table_name', engine)
56. 使用递归特征消除(RFE)进行特征选择:
-
RFE 有助于识别最重要的特征,从而提高模型性能并加快训练速度。
-
特征选择可以减少过拟合并提高模型的泛化能力。
from sklearn.datasets import load_iris
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
# Load your data
iris = load_iris()
X, y = iris.data, iris.target
# Create a Logistic Regression model
model = LogisticRegression()
# Perform Recursive Feature Elimination
rfe = RFE(model, n_features_to_select=2)
rfe.fit(X, y)
# Get the most important features
important_features = rfe.support_
57. 使用 Apache Parquet 高效存储列式数据: Apache Parquet 是一种列式存储文件格式,提供高效的压缩和编码方案,非常适合存储用于机器学习的大型数据集。
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
# Read a CSV file using pandas
data = pd.read_csv("data.csv")
# Convert the pandas DataFrame to an Apache Arrow Table
table = pa.Table.from_pandas(data)
# Write the Arrow Table to a Parquet file
pq.write_table(table, "data.parquet")
# Read the Parquet file into a pandas DataFrame
data_from_parquet = pq.read_table("data.parquet").to_pandas()
58. 使用 Apache Kafka 进行实时数据流: Apache Kafka 是一个分布式流处理平台,使您能够构建实时数据管道和应用程序。
from kafka import KafkaProducer, KafkaConsumer
# Create a Kafka producer
producer = KafkaProducer(bootstrap_servers="localhost:9092")
# Send a message to a Kafka topic
producer.send("my_topic", b"Hello, Kafka!")
# Create a Kafka consumer
consumer = KafkaConsumer("my_topic", bootstrap_servers="localhost:9092")
# Consume messages from the Kafka topic
for msg in consumer:
print(msg.value)
59. 对数据进行分区以提高查询效率: 对数据进行分区可以通过减少需要读取的数据量来提高查询性能。
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
# Read a CSV file using pandas
data = pd.read_csv("data.csv")
# Convert the pandas DataFrame to an Apache Arrow Table
table = pa.Table.from_pandas(data)
# Write the Arrow Table to a partitioned Parquet dataset
pq.write_to_dataset(table, root_path="partitioned_data", partition_cols=["state"])
# Read the partitioned Parquet dataset into a pandas DataFrame
data_from_partitioned_parquet = pq.ParquetDataset("partitioned_data").read().to_pandas()
60. 使用数据增强技术增加数据集大小: 数据增强涉及通过对现有数据应用各种转换来创建新的训练样本,这有助于提高模型性能。
import numpy as np
import tensorflow as tf
from tensorflow.keras.preprocessing.image import ImageDataGenerator
# Define an image data generator for data augmentation
datagen = ImageDataGenerator(
rotation_range=20,
width_shift_range=0.2,
height_shift_range=0.2,
horizontal_flip=True,
)
# Load your data
(x_train, y_train), (_, _) = tf.keras.datasets.cifar10.load_data()
x_train = x_train.astype(np.float32) / 255.0
# Fit the data generator to your data
datagen.fit(x_train)
# Train your model with augmented data
model = create_your_model()
model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"])
model.fit(datagen.flow(x_train, y_train, batch_size=32), epochs=10)
61. 使用 Flask 部署模型: 以下是使用 Flask 部署机器学习模型的示例:
from flask import Flask, request, jsonify
import joblib
app = Flask(__name__)
@app.route('/predict', methods=['POST'])
def predict():
data = request.get_json()
features = [data['feature1'], data['feature2'], data['feature3']]
model = joblib.load('model.pkl')
prediction = model.predict([features])[0]
response = {'prediction': int(prediction)}
return jsonify(response)
if __name__ == '__main__':
app.run()
62. 使用 Pytest 进行测试:
例如,我们有一个名为math_operations.py的文件。
# math_operations.py
def add(a, b):
return a + b
def multiply(a, b):
return a * b
接下来,创建一个测试模块,名称与您的模块相同,但以test_为前缀。在我们的例子中,我们将创建一个名为test_math_operations.py的文件:
# test_math_operations.py
import math_operations
def test_add():
assert math_operations.add(2, 3) == 5
assert math_operations.add(-1, 1) == 0
assert math_operations.add(0, 0) == 0
def test_multiply():
assert math_operations.multiply(2, 3) == 6
assert math_operations.multiply(-1, 1) == -1
assert math_operations.multiply(0, 0) == 0
使用pytest命令运行测试
pytest test_math_operations.py
Pytest 将发现并运行test_math_operations.py模块中的测试函数。
63. 使用自动化数据管道: 自动化数据管道可以帮助您自动化数据摄取、清洗和转换的过程。一些重要的工具包括
Apache Airflow 机器学习管道
from airflow import DAG
from airflow.operators.python_operator import PythonOperator
from datetime import datetime
def preprocess_data():
# Preprocess data here
pass
def train_model():
# Train model here
pass
default_args = {
'owner': 'myname',
'start_date': datetime(2023, 3, 15),
'retries': 1,
'retry_delay': timedelta(minutes=5),
}
with DAG('my_dag', default_args=default_args, schedule_interval='@daily') as dag:
preprocess_task = PythonOperator(task_id='preprocess_task', python_callable=preprocess_data)
train_task = PythonOperator(task_id='train_task', python_callable=train_model)
preprocess_task >> train_task
64. 使用迁移学习: 迁移学习可以帮助你重用和调整预训练的机器学习模型以适应你的使用案例。以下是一个如何使用 TensorFlow 进行迁移学习的示例:
import tensorflow as tf
from tensorflow.keras.applications import VGG16
# Load pre-trained model
base_model = VGG16(weights='imagenet', include_top=False, input_shape=(224, 224, 3))
# Freeze base layers
for layer in base_model.layers:
layer.trainable = False
# Add custom top layers
x = base_model.output
x = tf.keras.layers.GlobalAveragePooling2D()(x)
x = tf.keras.layers.Dense(256, activation='relu')(x)
predictions = tf.keras.layers.Dense(10, activation='softmax')(x)
# Create new model
model = tf.keras.models.Model(inputs=base_model.input, outputs=predictions)
# Compile and train model
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
model.fit(X_train, y_train, epochs=10)
65. 使用自动化机器学习(Auto ML): 通过使用 H2O.ai 或 Google Cloud AutoML 等平台,你可以根据数据和需求自动选择、训练和部署模型。以下是一个如何使用 H2O.ai 的 AutoML 平台的示例:
import h2o
from h2o.automl import H2OAutoML
# Start H2O cluster
h2o.init()
# Load data
data = h2o.import_file('my-data.csv')
# Define target variable
target = 'label'
# Split data into train and test sets
train, test = data.split_frame(ratios=[0.8])
# Define AutoML settings
automl = H2OAutoML(max_models=10, seed=1234)
# Train AutoML model
automl.train(x=data.columns, y=target, training_frame=train)
# Evaluate AutoML model
predictions = automl.leader.predict(test)
accuracy = (predictions['predict'] == test[target]).mean()
print(f'Accuracy: {accuracy}')
66. 使用异常检测: 通过使用 PyOD 或 TensorFlow 等库,你可以基于统计或机器学习技术检测异常。以下是一个如何使用 PyOD 在数据集中检测异常的示例:
import numpy as np
from pyod.models.knn import KNN
# Load data
X = np.load('my-data.npy')
# Define anomaly detector
detector = KNN(n_neighbors=5)
# Train detector
detector.fit(X)
# Detect anomalies
anomaly_scores = detector.decision_scores_
threshold = np.percentile(anomaly_scores, 95)
anomalies = np.where(anomaly_scores > threshold)
# Print anomalies
print(f'Anomalies: {anomalies}')
67. 使用权重与偏差: 下面是一个如何使用权重与偏差来运行和跟踪机器学习实验的示例。
import wandb
import tensorflow as tf
# Initialize W&B
wandb.init(project='my-project')
# Load data
data = tf.data.TFRecordDataset('my-data.tfrecord')
# Define hyperparameters
config = wandb.config
config.learning_rate = 0.1
config.num_epochs = 10
# Define model
model = tf.keras.models.Sequential([
tf.keras.layers.Dense(32, activation='relu'),
tf.keras.layers.Dense(10, activation='softmax')
])
model.compile(optimizer=tf.keras.optimizers.Adam(), loss='categorical_crossentropy', metrics=['accuracy'])
# Train model
history = model.fit(data.batch(32), epochs=config.num_epochs)
# Log metrics and artifacts to W&B
wandb.log({'accuracy': history.history['accuracy'][-1]})
wandb.log_artifact('my-model.h5')
68. 重要的机器学习工作流管理工具:
69. 使用数据压缩: 在 Python 中考虑使用 zlib、gzip 或 bz2 等工具和库进行数据压缩。
import zlib
# Compress data with zlib
data_compressed = zlib.compress(data)
# Decompress data with zlib
data_decompressed = zlib.decompress(data_compressed)
70. 数据序列化: 在 Python 中考虑使用 JSON、YAML 或 protobuf 等工具和库进行数据序列化。
import json
# Serialize data to JSON
data_json = json.dumps(data)
# Deserialize data from JSON
data_deserialized = json.loads(data_json)
71. 数据标准化和缩放: 在 Python 中考虑使用 scikit-learn、TensorFlow 或 PyTorch 等工具和库进行数据标准化和缩放。
import pandas as pd
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# Standardize data with Z-score normalization
scaler = StandardScaler()
data_normalized = scaler.fit_transform(data)
# Scale data with min-max scaling
scaler = MinMaxScaler()
data_scaled = scaler.fit_transform(data)
72. 数据加密与安全: 在 Python 中考虑使用诸如 cryptography、Fernet 或 PyAesCrypt 的工具和库来进行数据加密和安全。
from cryptography.fernet import Fernet
# Generate encryption key with Fernet
key = Fernet.generate_key()
# Encrypt data with Fernet
cipher_suite = Fernet(key)
encrypted_data = cipher_suite.encrypt(data)
# Decrypt data with Fernet
decrypted_data = cipher_suite.decrypt(encrypted_data)
import hashlib
# Hash data with hashlib
hash_value = hashlib.sha256(data.encode('utf-8')).hexdigest()
import tokenizers
# Define tokenization with tokenizers
tokenizer = tokenizers.Tokenizer(tokenizers.models.WordPiece('vocab.txt', unk_token='[UNK]'))
encoded_data = tokenizer.encode(data).ids
73. 使用 Great Expectation 进行数据验证:
import great_expectations as ge
# Load a dataset (e.g., a Pandas DataFrame)
data = ge.read_csv("data.csv")
# Create an Expectation Suite
expectation_suite = data.create_expectation_suite("my_suite")
# Add expectations
data.expect_column_values_to_be_unique("id")
data.expect_column_values_to_not_be_null("name")
data.expect_column_mean_to_be_between("age", min_value=20, max_value=40)
# Validate data against the Expectation Suite
validation_result = data.validate(expectation_type="basic")
# Save the Expectation Suite and the validation result
ge.save_expectation_suite(expectation_suite, "my_suite.json")
ge.save_validation_result(validation_result, "my_suite_validation.json")
74. **logging** 模块: 使用 logging 模块进行灵活的日志记录。
import logging
logging.basicConfig(level=logging.INFO)
logging.info("This is an info message.")
logging.error("This is an error message.")
75. 使用 Dask 数据框: Dask 是一个用于 Python 的强大库,支持并行和分布式计算。它允许你通过将大型数据集拆分为较小的块并并行处理它们来处理不适合内存的大数据集。
import dask.dataframe as dd
# Read CSV file using Dask (file is partitioned into smaller chunks)
ddf = dd.read_csv('large_file.csv')
# Perform operations on the data (lazy evaluation)
filtered_ddf = ddf[ddf['column_A'] > 10]
mean_value = filtered_ddf['column_B'].mean()
# Compute the result (operations are executed in parallel)
result = mean_value.compute()
print("Mean of column B for rows where column A > 10:", result)
结论:
我希望以上提示和技巧对你有用。MLOps 中还有许多工具和流程,MLOps 领域也在快速变化。最好保持更新,以便了解最新的工具和 MLOps 流程。
参考文献:
-
Mlflow: 一个简化机器学习生命周期的开放平台。 (2021).
mlflow.org/ -
Prometheus: 一个具有维度数据模型的开源监控系统。 (2021).
prometheus.io/ -
Grafana: 开放且可组合的可观察性和数据可视化平台。 (2021).
grafana.com/ -
Seldon Core: 在 Kubernetes 中部署、扩展和监控你的机器学习模型。 (2021).
www.seldon.io/tech/products/core/ -
Kubeflow: Kubernetes 的机器学习工具包。 (2021).
www.kubeflow.org/ -
Dask: 使用任务调度进行并行计算. (2021). 取自
dask.org/ -
《伟大的期望:始终了解数据中的期望》 (2021).
greatexpectations.io/ -
Airflow: 用于编程创作、调度和监控工作流的平台. (2021).
airflow.apache.org/ -
TensorFlow Extended: 为 TensorFlow 提供生产就绪的 ML 平台. (2021).
www.tensorflow.org/tfx -
Prefect: 数据流自动化的新标准. (2021).
www.prefect.io/ -
Feast: 机器学习的特征存储. (2021).
feast.dev/ -
安德烈·布尔科夫的《机器学习工程》
-
安德烈亚斯·克雷茨的《数据工程实用指南》
-
詹姆斯·李的《Python 数据工程实战》
MLOps — 了解数据漂移

照片由 Joshua Sortino 拍摄,来自 Unsplash
·
关注 发表在 Towards Data Science ·18 分钟阅读·2023 年 1 月 9 日
--
介绍:
MLOps 工程师的重要职能之一是监控模型性能。数据漂移会导致模型性能随时间退化。让我们详细探讨数据漂移及我们可以采取的检测步骤。
数据漂移是指数据分布随时间发生变化。这可能由于
-
数据收集过程的变化,
-
数据源的变化,
-
或业务需求或目标的变化。
数据漂移可能导致模型性能下降,因为模型应用于与训练时不同的数据。数据漂移有几种类型,包括概念漂移、协变量漂移、先验概率变化和虚拟漂移。
我在这篇文章中使用了 chatgpt3、excalidraw、GitMind 和 carbon.now.sh 工具。
内容:
-
数据漂移的类型。
-
协变量漂移或特征漂移或输入漂移。
-
标签漂移或目标漂移或输出漂移。
-
预测漂移或模型漂移
-
概念漂移或任务漂移。
-
创建警报并通知。
-
漂移检测的 Python 包。
-
结论。
数据漂移的类型:

图片由作者提供
协变量漂移或特征漂移或输入漂移:
特征漂移是指用于训练模型的输入变量或特征的变化。
特征漂移可能由于
-
数据收集过程的变化
-
提供输入的数据源的变化
-
业务需求的变化。
特征漂移可能导致模型性能下降,因为模型用于预测的特征发生了变化,并且可能不再相关或准确。
让我们考虑以下情况。
假设我们有一个模型,它是基于输入变量 X 预测输出变量 Y 的。我们可以将输入变量的概率分布(p(X))和输出变量的概率分布(p(Y))表示如下:
p(X): 这是输入变量的概率分布。它描述了 X 在数据中取不同值的可能性。
p(Y):这是输出变量的概率分布。它描述了 Y 在数据中取不同值的可能性。
如果数据中存在特征漂移,这意味着输入变量的分布随着时间的推移发生了变化。这可以用数学公式表示如下:
-
p(X1):这是在时间 t1 输入变量的概率分布。它描述了在时间 t1 数据中 X 取不同值的可能性。
-
p(X2):这是在时间 t2 输入变量的概率分布。它描述了在时间 t2 数据中 X 取不同值的可能性。
如果 p(X1) 和 p(X2) 存在显著差异,这可能表明数据中存在特征漂移。这可能导致模型性能下降,因为模型应用于与训练时不同的特征数据。

图片由作者提供
监控特征漂移所需的措施:

图片由作者提供
-
监控输入数据: 定期监控输入数据以检测特征分布的变化。这可以帮助你识别特征漂移发生的时机,从而采取措施进行纠正。
-
重新训练模型: 如果你检测到特征漂移,可能需要在更新的数据上重新训练模型以纠正漂移。这将确保模型能够基于当前特征分布进行准确预测。
-
使用漂移检测方法: 有几种方法可以检测特征漂移,例如 Jensen-Shannon 散度或 Kolmogorov-Smirnov 检验。这些方法可以帮助你检测特征分布是否发生了变化,从而采取措施进行纠正。
-
利用领域知识: 运用你的领域知识来理解特征漂移发生的原因以及可以采取的纠正措施。例如,如果你知道数据收集过程发生了变化,你可能需要调整数据收集和预处理的方法。
-
使用鲁棒模型: 考虑使用对特征漂移更鲁棒的模型,例如随机森林或梯度提升模型。这些模型对输入数据的变化不那么敏感,因此可能不容易受到特征漂移的影响。
可用的特征漂移检测方法:
有几种方法可以检测机器学习中的特征漂移:
-
视觉检查: 一种简单的方法是通过可视化检查输入数据随时间的变化,看是否有明显的特征分布变化。这可以通过绘制不同时间的数据直方图或散点图并进行比较来完成。
-
统计检验: 统计检验如 Jensen-Shannon 散度或 Kolmogorov-Smirnov 检验可以用于比较不同时间点的输入数据分布并检测显著差异。这些检验可以提供数据漂移程度的定量度量。
-
漂移检测算法: 还有一些专门设计用于检测数据漂移的算法。这些算法可以用来自动检测特征分布的变化,并在检测到漂移时触发警报或采取行动。
-
模型性能监控: 另一种方法是随时间监控模型的性能,并寻找准确性或其他性能指标的显著变化。如果模型的性能开始下降,这可能表明数据存在漂移。
-
数据质量检查: 定期检查输入数据的质量也有助于检测特征漂移。例如,如果特征的范围或方差发生了突然变化,这可能表明数据存在漂移。
我们可以监控以下内容来检测特征漂移:
数值特征

作者提供的图片
作者提供的图片
这个函数计算了训练数据和测试数据中每个特征的各种汇总统计量,包括均值、中位数、方差、缺失值计数、最大值和最小值。然后,它会比较这些统计量在训练数据和测试数据之间的差异,并且如果这些差异中有任何一个超出某个阈值,则返回**True**。这可以用来检测数值特征漂移,因为训练数据和测试数据之间汇总统计量的显著差异可能表明数据发生了漂移。
统计检验:

图片由作者提供
Jensen-Shannon:
Jensen-Shannon 散度是衡量两个概率分布相似度的指标。它被定义为两个分布之间的 Kullback-Leibler 散度和这两个分布的平均分布之间的 Kullback-Leibler 散度的平均值。Jensen-Shannon 散度总是非负的,当且仅当两个分布相同时,它的值为零。
在特征漂移检测的背景下,Jensen-Shannon 散度可以用来比较训练集中特征的分布与测试集中特征的分布。如果这两个分布之间的 Jensen-Shannon 散度超过某个阈值,这可能表明数据中存在漂移。
图片由作者提供
这个函数使用jensen_shannon_divergence()函数计算训练数据和测试数据中特征分布之间的 Jensen-Shannon 散度。Jensen-Shannon 散度是衡量两个概率分布相似度的指标,散度越低表示相似度越高。如果散度超过某个阈值,这可能表明数据存在漂移。
双样本 Kolmogorov-Smirnov (KS)、Mann-Whitney 或 Wilcoxon 检验:
Kolmogorov-Smirnov (KS) 检验是一种非参数检验,可用于检测机器学习中的特征漂移。它基于比较两个样本的分布以查看它们是否来自同一总体的思想。
要使用 KS 检验进行特征漂移检测,你首先需要将数据分成两个集合:训练集和测试集。训练集用于训练模型,而测试集用于评估模型的性能。然后,你可以使用 KS 检验比较训练集中特征的分布与测试集中特征的分布。如果检验的 p 值低于某个阈值,这可能表明数据存在漂移。
图片由作者提供
该函数使用来自 scipy.stats 模块的 ks_2samp() 函数计算训练数据和测试数据中特征分布之间的两样本 KS 检验的 p 值。KS 检验是一种非参数检验,比较两个样本的分布,以判断它们是否来自相同的总体。如果 p 值低于某个阈值,这可能表示数据存在漂移。通常这个阈值或显著性水平为 0.05。
Wasserstein 距离:
作者提供的图片
该函数使用来自 scipy.stats 模块的 wasserstein_distance() 函数计算训练数据和测试数据中特征分布之间的 Wasserstein 距离。Wasserstein 距离是衡量两个概率分布之间距离的指标,距离越高表示分布之间的差异程度越大。如果距离超过某个阈值,这可能表示数据存在漂移。
需要注意的是,Wasserstein 距离要求输入数据是一维且非负的。如果你的数据不符合这些要求,你可能需要对其进行转换或使用其他方法来检测特征漂移。
Kullback-Leibler 散度:
作者提供的图片
该函数使用来自 scipy.stats 模块的 entropy() 函数计算训练数据和测试数据中特征分布之间的 Kullback-Leibler 散度。Kullback-Leibler 散度是衡量两个概率分布之间差异的指标,散度越高表示分布之间的差异程度越大。如果散度超过某个阈值,这可能表示数据存在漂移。
分类特征
总结统计
众数、唯一值数量、缺失值数量:
作者提供的图片
该函数计算训练数据和测试数据中的众数、唯一值数量和缺失值数量,并比较它们以查看是否存在显著差异。如果众数或唯一值数量发生显著变化,或者缺失值数量显著增加,这可能表示数据存在漂移。
需要注意的是,这种检测分类数据特征漂移的方法基于特征分布在训练数据和测试数据之间没有发生显著变化的假设。如果这个假设不成立,这种方法可能无法有效检测漂移。根据你的数据特征,你可能需要使用其他方法或方法组合。
统计检验:
单向卡方检验:
单向卡方检验(Chi Squared Contingency Test)是一种统计检验方法,可用于检测分类数据中的特征漂移。它基于将训练数据和测试数据中不同类别的观察频率与若类别相互独立时预期观察到的频率进行比较的思想。
作者提供的图像
此函数使用scipy.stats模块中的chi2_contingency()函数计算卡方统计量和检验的 p 值。卡方统计量是数据中类别的观察频率与预期频率之间差异的度量,而 p 值是观察到的频率在类别独立的情况下出现的概率度量。如果 p 值低于某个阈值,这可能表明数据中存在漂移。
双向卡方检验:
双向卡方检验(Chi Squared Contingency Test)是一种统计检验方法,可用于检测分类数据中的特征漂移。它基于将训练数据和测试数据中不同类别的观察频率与若类别相互独立时预期观察到的频率进行比较的思想。
作者提供的图像
此函数使用scipy.stats模块中的chi2_contingency()函数计算卡方统计量和检验的 p 值。卡方统计量是数据中类别的观察频率与预期频率之间差异的度量,而 p 值是观察到的频率在类别独立的情况下出现的概率度量。如果 p 值低于某个阈值,这可能表明数据中存在漂移。
Fisher 精确检验:
Fisher 精确检验是一种统计检验方法,可用于检测分类数据中的特征漂移。它基于将训练数据和测试数据中不同类别的观察频率与若类别相互独立时预期观察到的频率进行比较的思想。
作者提供的图像
此函数使用scipy.stats模块中的fisher_exact()函数计算 Fisher 精确检验的 p 值。p 值是观察到的频率在类别独立的情况下出现的概率度量。如果 p 值低于某个阈值,这可能表明数据中存在漂移。
需要注意的是,当预期频率较小时,Fisher 精确检验(Fisher Exact Test)是比卡方检验更强大的替代方法,但当预期频率较大时,它通常不如卡方检验强大。此外,该检验要求数据为频数而非比例或百分比。如果您的数据形式不同,可能需要在进行检验前调整数据。
类-MonitorDrift:
结合以上所有内容,创建一个名为 MonitorDrift 的类:
作者提供的图像
这个类有五个方法:feature_drift_fisher()、feature_drift_chi2()、feature_drift_chi2_one_way()、feature_drift_jensen_shannon() 和 feature_drift_wasserstein()。每个方法都接受一个参数 feature,这是你想计算特征漂移的特征名称。
feature_drift_fisher() 方法使用 scipy.stats 模块中的 fisher_exact() 函数来计算指定特征的 Fisher 精确检验的 p 值。feature_drift_chi2() 方法使用 chi2_contingency() 函数来计算指定特征的卡方统计量和 p 值的双向卡方检验。feature_drift_chi2_one_way() 方法类似,但使用单向卡方检验。
feature_drift_jensen_shannon() 方法使用 scipy.spatial.distance 模块中的 jensenshannon() 函数来计算 Jensen-Shannon 距离。
标签漂移或目标漂移:
标签漂移,也称为注释漂移,是指当与数据集相关的标签或类别随时间变化时出现的问题。这可能由于多种原因,例如人类判断的变化、新类别的引入,或现有类别的合并或拆分。
-
数据中标签分布的变化
-
P(Y)的变化
假设我们有一个包含特征向量 x 和相应标签 y 的数据集。数据的概率分布表示为 p(x),标签的概率分布表示为 p(y)。
在没有标签漂移的情况下,我们期望 p(x) 和 p(y) 随时间保持稳定,这意味着看到特定特征向量或标签的概率不会显著变化。然而,当标签漂移发生时,p(y) 会随时间变化,意味着看到特定标签的概率可能会显著变化。这可能是由于标签定义的变化、标签分配方式的变化或其他因素造成的。
监测标签漂移所需的行动:
-
定期审查和更新数据集中的标签。
-
使用主动学习或自学习算法。
-
监控你的模型的性能。
-
使用漂移检测方法识别标签漂移。
-
监控数据收集过程。
检测标签漂移的方法:
这里是一些可用于检测标签漂移的方法。
-
Page-Hinkley 检验。
-
ADWIN(自适应窗口) 。
-
DDM(漂移检测方法)。
-
双向卡方检验。
-
单向卡方检验。
-
Fischer 精确检验。
标签漂移的例子:
这里是一些标签漂移的例子。
-
例如,考虑一个医学记录数据集,其中标签表示患者的诊断。如果医学条件的定义随着时间的推移而发生变化(例如,由于新的研究或临床指南的更新),那么数据集中的标签可能会变化,从而导致标签漂移。
-
基于客户评论的客户情绪分析。如果确定评论是积极还是消极的标准发生变化,则会导致标签漂移。
Page-Hinkley 测试:
作者提供的图片
作者提供的图片
预测漂移或输出漂移:
预测漂移,也称为输出漂移,发生在机器学习模型的准确性随着时间的推移而降低时。
预测漂移的原因
-
数据分布的变化。
-
模型性能的变化。
-
预测的现实世界现象的变化。例如,考虑一个用于预测天气的模型。如果天气模式随时间发生显著变化(例如由于气候变化),这可能会导致模型的预测变得不那么准确。
-
模型给出的预测标签分布的变化
-
P(\hat{Y}| X)P(Y^∣X) 的变化
在分类模型的背景下,我们可以使用概率分布表示预测类别(y)和真实类别(x)。如果这些概率分布之间的差异随时间增加,则发生预测漂移。
检测预测或输出漂移的方法:
-
监控模型准确性。
-
使用交叉验证。
-
使用数据拆分。
使用 PaheHinkley 方法检测预测漂移。
作者提供的图片
预测漂移的一些例子:
-
股票价格预测——经济或公司绩效的变化。
-
欺诈检测模型——如果欺诈活动的模式随着时间变化。
-
客户流失预测:如果客户的行为或需求随时间变化。
-
医疗模型:如果患者的健康状态或风险因素随时间变化。
概念漂移:
-
概念漂移指的是被机器学习模型学习的基础数据分布的变化。
-
概念漂移可能会随着时间的推移发生,因为收集的数据发生变化,或者它可能是由于正在建模的现实世界现象的变化所导致的。
-
概念漂移可能使机器学习模型的准确性降低,因为它们可能无法准确识别数据中的模式和关系。
-
概念漂移是输入变量和标签之间关系的变化。概念漂移指的是输入数据(p(X))或输出数据(p(Y))的概率分布的变化。
-
概念漂移是 P(Y| X)P(Y∣X) 的分布变化
-
概念漂移会导致当前模型无效。
COVID-19 → 概念漂移:
例如,COVID-19 造成了以下影响
-
消费者行为的变化:人们在应对封锁、经济不确定性和其他因素时,改变了他们的购买模式。这导致了依赖消费者数据的行业,如零售、电商和广告,出现了概念漂移。
-
经济数据的变化:例如,由于疫情,失业率和消费者支出模式发生了显著变化,这导致了经济预测模型中的概念漂移。
-
社会数据的变化: 人们的行为和态度因封锁、社交距离和其他措施而发生了变化。这导致了社交媒体和其他在线平台以及市场营销和公众舆论研究中的概念漂移。
-
教育模式的变化: 学校和大学关闭或转为在线学习。这导致了用于预测学生结果的教育数据和算法中的概念漂移。
-
工作模式的变化: Covid 导致工作模式发生了显著变化,许多人转向远程工作或经历了工作时间表的变化。这导致了用于预测劳动力生产力和员工结果的数据和算法中的概念漂移。
-
旅行模式的变化: 人们已根据封锁和其他限制调整了旅行计划。这影响了用于预测旅行需求和模式的算法。
检测概念漂移的方法:
-
监控模型性能。
-
使用数据拆分。
-
使用统计测试。
-
使用漂移检测算法。
-
使用人工输入。
这是监控模型性能的代码。
作者提供的图像
上述代码定期在测试集上评估模型,并将准确性存储在列表中。如果过去 N 次评估间隔的平均准确性低于某个阈值,这可能表明概念漂移,并且模型会在新数据上重新训练。如果平均准确性仍高于阈值,循环继续,模型将在某个间隔后再次评估。
查看下面关于现实世界概念漂移的文章。
## Zillow Offers 的 5 亿美元+困境 - AI 模型出现了什么问题? - insideBIGDATA
在这篇贡献文章中,TruEra 的联合创始人、总裁兼首席科学家 Anupam Datta 讨论了 Zillow 及其……
insideBIGDATA Forbes [## 当 AI 袭击收益时
AI 可以带来惊人的收入增长——直到它不再有效。这一课正在被一家成长中的公司以艰难的方式学习中……
Forbes Aporia [## Unity 的奇特案例:机器学习与华尔街的碰撞
了解 Unity 软件的机器学习模型在生产中出现的故障如何影响他们的财报,以及……
www.aporia.com](https://www.aporia.com/blog/unity-software-ml-fault-causes-stock-drop/?source=post_page-----69f9bf8a2e46--------------------------------)
创建警报和通知:
要通知机器学习漂移,请执行以下操作。
例如
-
发送电子邮件
-
向 Slack 频道发送消息
-
向 MS Teams 频道发送消息
图片由作者提供
-
这段代码定义了三个函数:
**send_email**、**send_slack_message**和**send_teams_message**,可以分别用于向电子邮件地址、Slack 频道或 Microsoft Teams 频道发送消息。 -
要使用这些功能,你需要配置你的电子邮件服务器(如果发送电子邮件)、Slack API 客户端或 Microsoft Teams API 客户端,并提供适当的凭证和 API 密钥。
-
例如,你可以调用
**send_email**将电子邮件发送到指定的收件人,或者调用**send_slack_message**将消息发送到 Slack 频道。
可用于检测漂移的 Python 包:
一些可用于检测漂移的包包括
-
[**skmultiflow:**](https://scikit-multiflow.github.io/)的一个关键特性是其处理概念漂移的能力,即数据的基础分布随时间变化的情况。它包括用于检测概念漂移的算法,并实时调整机器学习模型以适应数据变化。这使它成为构建能够适应并持续表现良好的机器学习模型的理想工具。 -
[**evidently:**](https://www.evidentlyai.com/blog/evidently-001-open-source-tool-to-analyze-data-drift)开源工具来分析数据漂移。Evidently 是一个开源 Python 库,面向数据科学家和机器学习工程师。它有助于从验证到生产评估、测试和监控机器学习模型的性能。 -
Tensorflow 数据验证: Tensorflow 数据验证(TFDV)可以分析训练和服务数据,计算描述性统计数据,推断模式,并检测数据异常。核心 API 支持每个功能块,提供便捷的方法,并可以在笔记本环境中调用。
-
Deepchecks: Deepchecks 开源是一个针对数据科学家和机器学习工程师的 Python 库。该包包含广泛的机器学习模型和数据测试套件,设计灵活,可扩展和可编辑。
-
漂移检测: 这个包包含一些开发工具,用于检测和比较两个结构相似的 pandas 数据框之间的统计差异。其目的是检测数据漂移——即输入变量的统计特性随时间变化。它提供了一个
DataDriftDetector类,该类接受两个 pandas 数据框,并提供一些有用的方法来比较和分析这两个数据集之间的差异。
MLOps 公司专注于漂移检测:
这里是一些重要的关键参与者。
-
Datadog
-
DataRobot
-
H2O.ai
-
Anodot
-
Arize AI
-
Superwise.ai
-
Whylabs.ai
-
ModelOp
-
Domino Data Lab
-
Algorithmia
-
Databricks
-
Fiddler
-
Seldon
主要云服务提供商漂移检测:
Azure:
使用 Azure 机器学习监控数据漂移
## 使用 Azure 机器学习监控数据漂移 ## MLOps:机器学习模型管理 v1 - Azure 机器学习
适用对象:Azure CLI ml 扩展 v1 Python SDK azureml v1 在本文中,了解如何进行机器学习……
GCP:
## 监控特征偏差和漂移 | Vertex AI | Google Cloud
无论您的业务处于数字化转型的初期还是已经取得了显著进展,Google Cloud 都能帮助解决……
## 使用模型监控 | Vertex AI | Google Cloud 云计算平台 ## MLOps:机器学习中的持续交付和自动化管道 | 云架构中心
本文讨论了实施和自动化持续集成(CI)、持续交付的技术……
AWS:
[## 使用 Amazon SageMaker 检测数据漂移 | 亚马逊网络服务
随着公司继续接受云计算和数字化转型,他们利用历史数据来识别……
aws.amazon.com](https://aws.amazon.com/blogs/architecture/detecting-data-drift-using-amazon-sagemaker/?source=post_page-----69f9bf8a2e46--------------------------------) [## Amazon SageMaker 模型监控器 | 机器学习模型准确性 | 亚马逊网络服务
自动检测模型和数据漂移,并接收不准确预测的警报,以便你可以采取纠正措施……
aws.amazon.com](https://aws.amazon.com/sagemaker/model-monitor/?source=post_page-----69f9bf8a2e46--------------------------------) [## 使用自定义 Amazon SageMaker 模型监控器检测 NLP 数据漂移 | 亚马逊网络服务
自然语言理解被应用于广泛的使用案例,从聊天机器人和虚拟助手,到……
查看此 Twitter 线程。
结论:
数据漂移是机器学习中一个常见且潜在严重的问题。当模型训练所用的数据的分布或特征与模型应用时的数据的分布或特征有所不同时,就会发生数据漂移。这可能导致模型准确性降低、预测偏差,以及在生产环境中可能产生严重后果的其他问题。监控数据漂移并在发生时采取纠正措施至关重要。这有助于确保模型继续准确有效地执行,从而防止在生产环境中出现代价高昂的错误或失败。
参考文献:
-
Databricks-ML in Production-
github.com/databricks-academy/ml-in-production-english -
Evidently AI-
www.youtube.com/watch?v=HGIgUH11nVo -
AWS re:Invent 2020: 在生产中检测机器学习(ML)模型漂移-
www.youtube.com/watch?v=J9T0X9Jxl_w -
Databricks —
www.youtube.com/watch?v=tGckE83S-4s -
技术讨论 (E15): 机器学习模型漂移与 MLOps 管道-
www.youtube.com/watch?v=aW11vOkSScA -
ML Drift — 如何在问题成为麻烦之前识别问题 // Amy Hodler // MLOps Meetup #89-
www.youtube.com/watch?v=--KcBoInuqw&t=363s -
为全栈解决方案选择正确的监控工具-
devops.com/choosing-the-right-monitoring-tools-for-a-full-stack-solution/ -
Aparna dinakaran 文章-
aparnadhinak.medium.com/
MLOps:什么是操作节奏?
快速的周转不仅仅依赖于工具
·
关注 发表在 Towards Data Science ·14 分钟阅读·2023 年 6 月 1 日
--

图片由作者提供,灵感来自 Michele Bitetto 在 Unsplash
操作节奏是一个组织完成其操作或任务的速度和效率。在 MLOps 中,这可以包括模型开发、再训练部署、维护等任务。
一个良好的操作节奏有助于按时交付项目,控制成本,减少开发时间,并确保机器学习项目的高质量结果。

MLOps 节奏因素,作者创作
有几个因素可以影响操作节奏:
-
战略目标
-
数据质量和可用性
-
团队规模、组织结构和经验
-
基础设施、资源和工具
-
模型复杂性
-
监管要求
重要的是要记住,操作节奏不仅仅关乎工具或指标。虽然这些因素会影响操作节奏,但它们并不完全定义操作节奏。
1. 战略目标

Jason Goodman拍摄,图片来源于Unsplash
战略目标在塑造组织操作节奏中发挥了重要作用。它们提供了意图、焦点、方向和模型构建过程的框架。在构建之前,始终询问用户为什么需要一个 ML 解决方案,而不是如何做。关注项目目标的功能,而不是模型。
关注“为什么”可以显著影响 MLOps 中的操作节奏。通过了解项目的原因和意图,有助于确定你想要构建的方向。缺乏对“为什么”的沟通或明确性是项目未能投入生产的常见原因。这会造成瓶颈,使得开发可能停滞——甚至需要重新开始。

作者创作
在构建模型之前要问的好问题:
-
我们为什么要构建这个?
-
我们希望它做什么?
-
潜在风险是什么?
-
我们希望在什么时候完成?
回答这些问题的最佳方式?始终从业务用例开始,关注最终产品。数据产品需要首先成为关注点,而不是模型。你需要先从“为什么”开始,然后再转到“如何”。
示例
一个好的例子是我与海外的一家电商客户合作的案例,他们需要一个推荐模型。他们认为这种方法将增强客户参与度并增加销售。
当我们加入时,我们的咨询团队领导没有深入研究如何构建模型。相反,他们专注于倾听和提出关键问题:
-
我们为什么要构建这个模型?它替代了当前的哪个过程?
-
终端用户对模型及其输出的目标是什么?
-
有哪些潜在风险会拖慢进度?
-
我们有多少时间?什么是可行的?
我们的团队领导专注于理解“为什么”。重点是最终结果——数据产品。最终目标是个性化购物体验,推动客户参与,并向他们重新营销产品。
这种目的的明确性——产品与模型的心态——简化了整个开发过程。它将模型开发过程与更大的商业战略目标联系起来。它促进了我们咨询团队与业务团队之间的沟通和信任,提高了 MLOps 的节奏。
根据我们的建议,客户的团队能够有效分配资源,适应意外变化,并将精力集中在实现期望结果上:一个改进的推荐系统。
通过关注“为什么”,他们确保了项目的每个阶段都与提供个性化购物体验的战略目标保持一致。因此,ML 解决方案成功实施,显著改善了产品推荐系统,并带来了销售和客户满意度的明显提升。
MLOps 节奏中的所有其他因素都受到对“为什么”的清晰理解——战略目标和最终使用案例的影响。
2. 数据

数据和高效的数据操作对 MLOps 的成功至关重要。没有高质量的数据,良好的流程、经验丰富的工作人员和工具都无效。数据是机器学习模型开发的基础——它在操作节奏中发挥着决定性作用。
稳固的 DataOps 基础对维持良好的 MLOps 操作节奏至关重要。如果支撑 MLOps 过程的 DataOps 不成熟或不完整,可能会导致积压。一个强健的 DataOps 流程和成熟度确保了用于机器学习模型的数据具有高质量、一致性、及时性和准确性。

Data Ops 和 MLOps 流程,作者创作
MLOps 数据操作中的挑战:
-
数据不足阻碍了操作的速度。
-
缺乏明确的战略目标和沟通,降低了优质数据的影响力。
-
数据不可用或难以获取和转换以满足特定用例
获取高质量、最新和可扩展的数据可以加快模型开发过程,通过提供准确和相关的信息来训练模型。
示例
让我们通过一个例子来说明:一个金融客户试图改善其信用评分模型。目标?提供更好的风险评估,从而做出更好的贷款决策。
我们的客户拥有一支有能力且知识丰富的 MLOps 团队、良好的流程和模板以及最好的工具。然而,他们遇到了数据问题——没有高质量的相关数据,这些资源不足以构建有效的机器学习模型。这就像一把没有钉子的锤子或没有木材的锯子。
每个机器学习(ML)模型都需要数据才能工作。数据是创建所有 ML 模型的基石。我们注意到,客户没有一个良好的系统(DataOps)来确保数据的质量和在需要时的可用性。这个问题拖慢了他们的工作速度和开发进度,影响了他们的 MLOps 节奏。
我们的咨询团队通过与客户的数据工程团队紧密合作,帮助客户改善了其 DataOps 基础。目标?确保用于机器学习模型的数据具有高质量、一致性、及时性和准确性。在合同期间,我们的团队负责人强调了建立明确的战略目标和改进数据需求及使用的沟通。
访问高质量、最新的、可扩展的数据加快了模型开发过程。他们能够提供准确和相关的信息来训练信用评分模型,这显著影响了模型开发的速度和效率。
数据是任何机器学习项目的基础。因此,数据的质量和可用性,通过 DataOps 的帮助,可以显著影响模型开发的速度和效率。
3. 团队规模、组织和经验

团队规模、组织和经验是极大地影响操作执行的关键因素。一个由数据科学家、工程师、领域专家和项目经理组成的全面团队,可以有效地协作,创建、部署和维护机器学习模型。
影响操作节奏的因素包括:
-
缺乏时间框架(时间限制)、范围不明确和职责未定义。
-
团队间的沟通问题和协调挑战,减慢了项目进展。
-
无组织的团队因任务重叠或角色混淆而遇到延误和效率低下。
-
经验不足的团队需要更多时间进行学习和实验,从而影响项目的整体进度。

团队因素,作者创作
大型团队可以实现更高的操作节奏。这很大程度上归结于良好的委派、项目范围界定和沟通。如果团队通过分隔实验和模型开发并行工作,这将有助于提高操作节奏。他们的节奏依赖于个别团队之间的沟通、时间限制任务和明确的角色定义。
小型团队可能由于资源有限和需要执行多项任务而拥有较低的操作节奏。然而,它们也可能具有更简化的沟通和协调,这可以使它们移动更快并更有效地迭代。
示例
让我们使用另一个例子。这次我将用一个营销客户。该客户在开发机器学习模型以个性化营销定价方面 struggled with 效率的 MLOps 节奏。项目工作量超出了他们快速构建和灵活性的能力。情况糟糕到他们难以维持有效的操作节奏。
团队规模小,缺乏处理这些新项目所需的技能和经验平衡。项目范围通常模糊,职责未定义,并且存在多种沟通问题。较小的团队规模意味着他们资源有限,影响了他们同时处理模型多个方面的能力。
为了解决这些挑战,我们的咨询团队建议重新组织。市场客户聘请了额外的数据工程师、机器学习专家和有管理 AI 项目经验的项目经理。然后建议明确定义每个角色和责任,并且确保每个项目有明确定义的范围和时间表。
变革带来了重大改变。客户的团队规模更大,且更加多样化,处理机器学习项目的不同部分变得轻而易举。我清楚地注意到了明确定义的角色,改进了他们对模型进行测试和基准测试的方式,并改善了他们的沟通。这确实减少了困惑,增进了团队合作,加快了他们的 MLOps 速度和开发时间。
在团队规模、组织和经验之间保持平衡,并配合有效的项目管理,对于保持高效的 MLOps 速度和确保机器学习项目的成功至关重要。
基础设施和工具

基础设施和工具是可以显著影响机器学习开发和部署速度与灵活性的两个关键因素。基础设施确保及时交付预测结果。而工具则有助于您自动化重复的流程,增强从数据中获取的见解。
基础设施

扩展基础设施,作者创作
基础设施必须具备良好的计算能力,良好的数据存储能力和可靠的网络基础设施。这样可以加快开发和重新训练周期。还可以加快迭代和部署过程。
随着 ML 模型的扩展,需要更大量的计算能力。它们还需要数据存储空间,以保存实验、数据等。如果没有足够或正确的计算和存储资源,运营速度就会减慢。这限制了可以开发、部署或扩展的模型数量。
数据是机器学习模型开发的核心,支持该数据的基础设施是最关键的。在开始 MLOps(甚至是 ML 建模)之前,专注于构建强大的数据存储、数据管道和数据版本控制流程。如果您正在构建模型,最后一点尤为关键。
工具
工具在 ops 速度中也发挥着作用。它们不仅用于自动化重复的任务和复杂的流程,还用于改进可重现性、模型管理、数据管理、监控和安全性。工具自动化这些流程,但也可能减缓它们的速度 —— 尤其是如果它们不兼容或发生供应商锁定。

工具和 MLOps 速度,作者创作
一些常见的工具问题会减缓 MLOps 的运营速度:
-
互不兼容的第三方工具
-
冗余的重复类似流程的工具
-
团队之间的不同工具版本
-
过多的工具用来解决本可以手动完成的问题
对这些工具进行良好的审计和评估需要定期进行。这有助于消除工具冲突、重复、不同格式及其他因素造成的低效。
每一个问题可能都很小,但如果没有定期审计,可能会显著拖慢 MLOps 流程和模型开发进度。
示例 — 基础设施和工具
让我们再举一个例子。这次我将使用一个拥有推荐系统的客户。他们的目标是通过推荐与客户已互动的产品类似的商品来提升销售。最初,他们在机器学习操作(MLOps)中的技术设置和工作节奏都还不错。
然而,随着他们扩展机器学习模型以处理更多的数据和复杂性,他们遇到了障碍。他们的计算能力和数据存储变得不足,工作进度放慢了。这一限制也减少了他们能够创建、使用或改进的新模型数量。他们意识到需要改进数据处理流程,并在构建模型时更好地跟踪不同版本的数据。
工具也成了一个难题。有些工具无法很好地协同工作,有些工具在做同样的工作,还有不同团队使用相同工具的不同版本。
为了解决这些问题,我们的咨询团队改善了他们的技术设置,与他们的数据工程师合作以增加数据存储,并简化数据处理流程。基于此基础,客户还建立了更好的流程来跟踪数据版本,这有助于保持数据的完整性并使重新训练模型变得更加容易。
我们还建议定期进行维护审计。客户开始定期检查他们的工具,记录任何不必要的工具,标准化团队之间使用的版本,并更换那些不兼容的工具。这有助于改善他们的 MLOps 节奏。
我看到的这个经验突出了拥有合适的技术和工具相互补充的重要性,特别是随着业务的增长。机器学习操作(MLOps)的节奏可能会发生变化。当前工具和技术在某个阶段运作良好的方法,在规模扩大时可能不再足够。
建模复杂性

图片由Donny Jiang拍摄,来源于Unsplash
建模复杂性以三种方式影响 MLOps 的操作节奏:训练、技术和执行。MLOps 常常在这三个点上遇到减速问题。
训练复杂的模型可能是具有挑战性的。 数据科学家和工程师可能需要花更多时间进行实验和数据验证。对于数据工程师来说,复杂的模型需要额外的验证和数据质量检查。对于数据科学家来说,复杂模型的解释、维护、调试和优化难度更大。高复杂性意味着更多的范围界定、开发和测试时间。

模型复杂性,作者创作
技术复杂性也与模型成正比。模型越复杂,构建模型、工程管道、演示和执行用户验收测试所需的资源、人员和时间就越多。如果该模型在生产中失败,则需要更多的时间来重新训练和重建。即使模型成功,测试和验证也比简单模型更为广泛。
时间也是一个重要因素。特别是对于业务单位,你需要保持信息通畅。团队成员可能需要投入额外的时间和精力来规划这些模型,并向业务展示价值。为模型实验和开发设定明确的时间限制至关重要。
平衡模型复杂性与运营效率对于保持可管理的 MLOps 节奏至关重要。
示例 — 模型复杂性
以一个海外零售商为例,他们开始构建一个复杂的机器学习模型。其目标是为客户个性化产品推荐。然而,模型过于雄心勃勃和复杂,导致最终失败。
训练这个复杂模型需要大量时间和资源。数据科学家和工程师必须在实验、研究和数据验证上投入额外的努力。复杂性不仅增加了项目时间,还需要额外的验证和质量检查层次。解释、维护、调试和优化模型等任务变得更加具有挑战性和时间密集。
模型的技术复杂性增加了运营成本。客户需要更多的资源和人员,以及额外的时间来构建模型管道、展示模型和进行用户验收测试。当模型在实际环境中失败时,重新训练和重建的成本和时间显著增加。
项目范围过于雄心勃勃,没有有效解决业务问题。他们在考虑最终使用案例之前先做了模型。我们的团队大部分时间都在重构代码或与工程团队合作,以使其能够解决问题。
对复杂模型的广泛培训时间,加上不断上升的成本,开始影响操作的速度。业务相关者对可操作结果的延迟和不断上升的运营成本感到沮丧。这些问题最终导致了项目的终止。
平衡模型复杂性与运营效率和成本对于机器学习项目的成功实施至关重要。
数据监管要求

监管要求也改变了操作节奏。遵守不同的内部和外部规则可能会加速或减缓开发进度。
如果你在与国际客户或利益相关者合作,情况会变得更复杂。一些地理区域内的数据不能从一个区域转移到另一个区域使用。在一些地理区域中的模型需要更多文档。
这还扩展到用于构建模型的数据以及存储。GDPR 和其他规定可能限制用于构建模型的特征。团队需要实施适当的数据管理实践,并可能调整模型以维护隐私,这可能影响整体操作节奏。

数据法规,作者创作
某些行业规定可能还要求额外的模型验证或第三方审核。这些审核的模型治理和文档增加了开发时间。在与业务单位或客户合作之前,明确这些需求至关重要。这些规定甚至可能禁止使用某些模型。
由于监管因素,数据科学团队通常需要构建定制解决方案或合规模型,这会增加额外的开发时间和成本。
示例 — 数据法规
让我们考虑一个金融客户,他们正在开发一个预测贷款违约的机器学习模型。他们在北美和欧盟都有业务。问题在于数据法规的差异。
在北美,法规相对宽松。而在欧盟,随着 GDPR 的实施,他们被要求有更严格的审计流程——从数据到他们使用的模型。
他们的工作进展变慢了,因为他们必须遵守各种国际数据保护法律。数据转移受限,他们必须创建更多文书工作。欧洲法律通用数据保护条例(GDPR)要求他们修改模型并严格管理数据以确保用户隐私。
为了遵守这些规则,我看到我们的团队帮助客户创建了单独的云环境。对于欧洲数据,他们在 GDPR 合规的云环境中构建了机器学习模型。同时,他们将北美客户的数据存储在另一个云中,并用于针对这些客户的模型。
行业特定的规定给机器学习项目增加了更多复杂性。它们通常要求额外的模型验证、审计和全面的文档。这些规定中有些甚至限制了我们客户可以使用的预测模型类型。这要求我们的团队开发定制的合规解决方案——这需要广泛的研究和持续的模型合规性检查。
这个例子说明了跨国监管合规如何为机器学习项目增加时间、成本和复杂性,从而显著影响 MLOps 的节奏。
结论
操作节奏,尤其是 MLOps 不仅仅是技术。技术是推动它的一个因素。要加快模型开发时间,你需要
总结:
-
从明确的数据策略开始——设定清晰的目标
-
确保足够好的数据质量和可用性
-
评估组织的因素:规模、组织结构和经验
-
考虑因素:基础设施、资源和工具
-
确保模型复杂度不至于过于复杂——并且仍能回答业务问题
-
检查规定——尤其是当你处理国际数据时。
我经常写关于数据战略、MLOps 和云中的机器学习的文章。请通过Linkedin、YouTube和Twitter与我联系。
如果你喜欢这篇文章,你也可能会喜欢我的其他文章:
为什么这对你的数据科学职业很重要?
blaza-matt.com](https://blaza-matt.com/what-is-data-maturity-fcb454a4d8b7?source=post_page-----91a3e0657ce6--------------------------------) [## MLOps 简化:什么是工件?
在机器学习中,工件是指在机器学习过程中生成的任何文件或对象。
blaza-matt.com](https://blaza-matt.com/mlops-simplified-what-is-an-artifact-684b64e9f60?source=post_page-----91a3e0657ce6--------------------------------) [## 处理数据科学负担
4 种方法让你保持动力……当感觉一切都过于沉重时。

























浙公网安备 33010602011771号