TowardsDataScience-2023-博客中文翻译-三十四-

TowardsDataScience 2023 博客中文翻译(三十四)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

优化、牛顿法与利润最大化:第三部分 — 应用利润最大化

原文:towardsdatascience.com/optimization-newtons-method-profit-maximization-part-3-applied-profit-maximization-23a8c16167cd?source=collection_archive---------15-----------------------#2023-03-01

所有图片由作者提供

学习如何应用优化和计量经济学技术来解决实际利润最大化问题

Jacob PieniazekTowards Data Science Jacob Pieniazek

·

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

--

本文是第三篇,也是最后一篇,在这个三部分系列中。在第一部分中,我们研究了基本优化理论。然后,在第二部分中,我们将这一理论扩展到约束优化问题。现在,在第三部分中,我们将应用所涉及的优化理论,以及计量经济学和经济学理论,来解决利润最大化问题。

假设,作为公司的一名数据科学家,你的任务是估算最佳的资金分配,以最大化某产品线的整体利润。此外,假设你在这些分配决策上有一些约束条件,例如必须分配的总花费上限和/或必须在某些渠道上花费的最低金额。在本文中,我们将结合第一部分第二部分中涉及的优化理论,以及额外的经济学和计量经济学理论,来处理这种理论上的利润最大化问题——我们将在本文中进一步详细阐述。

本文的目标是将我们迄今为止学到的知识结合起来,我希望能激励和启发读者如何将这些技术应用到实际环境中。本文并非针对所讨论问题的全面解决方案,因为细微差别和特性当然会使理论示例复杂化。此外,许多涉及的技术在 Python 中通过像pyomoSciPy等包有更优化的实现。尽管如此,我希望能提供一个构建应用优化问题的强大框架。让我们深入探讨吧!

第一部分中,我们涵盖了基本的优化理论 — 包括 1) 设置并通过分析解决一个简单的单变量优化问题,2) 迭代优化方案 — 即,梯度下降和牛顿法,和 3) 通过手工和 python 实现牛顿法解决多维优化问题。在第二部分中,我们涵盖了有约束优化理论 — 包括 1)将等式约束和 2)将不等式约束纳入我们的优化问题中,并通过牛顿法解决它们。本文旨在为那些已经熟悉第一部分第二部分*中涵盖的内容的人士提供帮助。**

优化理论(第一部分 & 第二部分 总结)

一个数学优化问题可以抽象地表述如下:

(1)

我们选择使向量x的实值最小化目标函数f(x)(或最大化-f(x)),受*不等式约束g(x)和*等式约束h(x)约束。在第二部分中,我们讨论了如何将这些约束直接纳入我们的优化问题中。值得注意的是,使用拉格朗日乘子和对数障碍函数,我们可以构建一个新的目标函数O(x, Λ,*** ρ):

(2) 有约束优化问题的泛化函数

其中Λ是与每个等式约束 h(x)相关的拉格朗日乘子向量,而ρ是与所有不等式约束 g(x)相关的障碍参数。然后我们可以通过选择一个初始值ρ注意,目标函数的大功能值会要求更大的初始值ρ以调整惩罚),使用牛顿法迭代优化新目标函数,接着通过逐渐减小ρρ → 0)来更新ρ,并重复直到收敛 — 牛顿法迭代方案如下:

(3) 牛顿法迭代方案

其中H(x)和f(x)分别表示我们的目标函数 O(x, Λ, ρ)的海森矩阵和梯度。当我们达到以下一个或多个标准的收敛时,即表示收敛:

(4) 迭代优化方案的收敛标准

在 Python 中,利用SymPy,我们有 4 个函数。一个函数用于获取我们的 SymPy 函数的梯度,一个函数用于获取我们的 SymPy 函数的海森矩阵,一个函数通过牛顿法解决无约束优化问题,另一个函数通过牛顿法解决根据广义方程(2)的有约束优化问题(有关该材料的更详细覆盖,请参见第一部分第二部分)。有关这些函数,请参见附录。

要解决一个有约束的优化问题,我们可以运行以下代码(确保起始值在不等式约束的可行范围内!):

import sympy as sm

x, y, λ, ρ = sm.symbols('x y λ ρ')

# f(x): 100*(y-x**2)**2 + (1-x)**2 
# h(x): x**2 - y = 2
# g_1(x): x <= 0
# g_2(x) y >= 3

objective = 100*(y-x**2)**2 + (1-x)**2 + λ*(x**2-y-2) - ρ*sm.log((y-3)*(-x))
symbols = [x,y,λ,ρ] # Function requires last symbol to be ρ (barrier parameter)
x0 = {x:-15,y:15,λ:0,ρ:10}

constrained_newton_method(objective,symbols,x0)

对应的输出为:

如果上述材料感觉陌生或需要更详细的回顾,建议查看本系列的第一部分和第二部分,这些将提供更深入的材料概述。对于本文的其余部分,我们将首先讨论基本的利润最大化和计量经济学理论,然后进入理论示例的解决方案。

应用利润最大化

假设我们有一个$100,000 的广告预算,并且必须全部花费。我们的任务是选择最优的预算分配到两种广告渠道(数字广告和电视广告),以最大化某一产品线的整体利润。此外,假设我们必须至少分配$20,000 到电视广告和$10,000 到数字广告。

理论公式

现在我们将数学地制定我们寻求解决的利润最大化问题:

(5) 链接

π(⋅)表示利润函数,δ表示数字广告支出,τ表示电视广告支出,(⋅)是额外变量的占位符。请注意,我们在最小化π(⋅)的负值,这等同于最大化π(⋅)。利润函数定义如下:

(6) 链接

p表示价格,q(δ, τ, ⋅)表示需求量函数,C(q(⋅), δ, τ)表示成本函数,直观上,它是数量的直接函数(如果我们生产更多,成本会增加)以及广告支出的多少。成本函数还可以接受额外输入,但为了演示,我们将其保持为数量和广告费用的函数。请注意,我们选择的δτ通过其对需求量和成本函数的影响直接影响利润函数。为了增加优化问题的可操作性,我们需要使用计量经济学技术来估计我们的需求量函数。一旦我们指定了成本函数并估计了需求量函数,我们可以按如下方式解决优化问题:

(7) 链接

其中 q_hat 是我们对需求量的经济计量模型的估计。在我们阐述需求量模型的经济计量规格之前,有必要讨论一个关于优化问题所需假设的重要说明,以证明其可操作性。我们必须获得数字广告和电视广告对需求量的 因果估计。用经济学术语来说,数字广告和电视广告在经济计量模型中需要是 外生 的。也就是说,它们与模型中的误差不相关。外生性可以通过两种方式实现:1)我们有正确的经济计量模型结构规范,以衡量数字广告和电视广告对需求量的影响(即,我们包括所有与需求量以及数字和电视广告支出相关的变量); 2)我们有随机变化的数字和电视广告支出(这可以通过随时间随机变化的支出来观察需求的响应)。

直观上,外生性是必需的,因为需要捕捉广告支出变化的因果影响——也就是说,如果我们改变广告支出的数值,平均而言会发生什么。如果我们估计的效果 不是 因果的,那么我们对广告支出的变化 不会 对应于需求量的真实变化。注意,模型不需要为需求量做出最佳预测,而是需要准确捕捉因果关系。请参阅我的 前一篇文章,其中更深入地讨论了外生性假设。

现在假设我们为按时间 t 索引的需求量指定以下经济计量模型:

(8) 链接

其中 βγ 分别是数字广告支出自然对数的影响估计值 δ 和电视广告支出的影响估计值 τ。此外,α 是我们的截距,ϕ1 和 ϕ2 是需求量的 自回归 组件的估计值,S 表示季节性,X 是所有相关协变量和滞后协变量的集合及其系数估计矩阵 Ωϵ 是误差项。此外,假设数字和电视广告在 XS 和我们模型中的自回归组件 条件下 满足外生性假设。也就是说,

(9) 链接

你可能会问,为什么使用数字和电视广告支出的自然对数?这并不是在此背景下的必需或决定性的选择,但我旨在展示变量变换如何捕捉我们选择变量与感兴趣结果之间的关系。以我们的案例为例,假设我们假设广告支出的影响最初急剧增加,但逐渐趋于平稳(即收益递减法则)。这正是对数变换允许我们建模的内容。请观察:

注意,成本函数的形式通常可以在内部更容易得知。因此,我们也来指定我们的成本函数的形式:

(10) 链接

在这里,我们可以看到每生产一个单位都有一个成本ζ,并且随着生产量的增加,这个成本会得到折扣(可以理解为对大合同的折扣或规模经济)。我们还简单地将数字广告支出和电视广告支出合并到我们的总成本中。

现在我们已经为我们的计量经济学利润最大化问题建立了理论基础,让我们来模拟一些数据并将其带入 Python!

数据模拟(可选)

请注意,本节可以跳过而不会丢失主要内容。

首先,我们来模拟 10 年期间的每月需求量数据,其中包括以下变量:

import pandas as pd

df = pd.DataFrame()

## Digital Advertising - ln(δ) 
df['log_digital_advertising'] = np.log(np.random.normal(loc=50000,scale=15000,size=120).round())

## Television Advertising - ln(τ)
df['log_television_advertising'] = np.log(np.random.normal(loc=50000,scale=15000,size=120).round())

## Matrix X of covariates

# Lag Digital Advertising
df['log_digital_advertising_lag1'] = df['log_digital_advertising'].shift(1)
df['log_digital_advertising_lag2'] = df['log_digital_advertising'].shift(2)

# Lag Television Advertising
df['log_television_advertising_lag1'] = df['log_television_advertising'].shift(1)
df['log_television_advertising_lag2'] = df['log_television_advertising'].shift(2)

# Price
df['price'] = np.random.normal(loc=180,scale=15,size=120).round()
df['price_lag1'] = df['price'].shift(1)
df['price_lag2'] = df['price'].shift(2)

# Competitor Price
df['comp_price'] = np.random.normal(loc=120,scale=15,size=120).round()
df['comp_price_lag1'] = df['comp_price'].shift(1)
df['comp_price_lag2'] = df['comp_price'].shift(2)

# Seasonality 
from itertools import cycle

months = cycle(['Jan','Feb','Mar','Apr','May','June','July','Aug','Sep','Oct','Nov','Dec'])
df['months'] = [next(months) for m in range(len(df))]

one_hot = pd.get_dummies(df['months'], dtype=int)
one_hot = one_hot[['Jan','Feb','Mar','Apr','May','June','July','Aug','Sep','Oct','Nov','Dec']]
df = df.join(one_hot).drop('months',axis=1)

## Constant
df['constant'] = 1

# Drop NaN (Two lags)
df = df.dropna()

注意,我们包括滞后变量,因为今天的需求量很可能是许多变量滞后值的函数。我们还通过引入每月的虚拟变量来控制季节性效应(这只是将季节性纳入模型的多种方式之一)。然后我们指定与每个变量相关的参数(请注意,这些参数按数据框的列顺序指定!):

params = np.array(
    [10_000, # β
    5_000, # γ
    2_000, # Ω
    1_000, # Ω
    3_000, # Ω
    1_000, # Ω
   -1_000, # Ω
   -500,   # Ω
   -100,   # Ω
    500,   # Ω
    300,   # Ω
    100,   # Ω
    25_000, # S
    15_000, # S 
    15_000, # S
    10_000, # S
    10_000, # S
    10_000, # S
    15_000, # S
    15_000, # S
    25_000, # S
    35_000, # S
    35_000, # S
    40_000, # S
    50_000  # α
    ])

然后我们可以通过运行quantity_demanded = np.array(df) @ params来模拟我们的计量经济学规范(方程 8)中的需求量。然而,请注意我们缺少自回归成分,因此我们还希望需求量遵循上述的自回归过程。也就是说,需求量也是其自身滞后值的函数。我们在这里包括 2 个滞后期(AR(2)过程),其系数分别为ϕ1ϕ2。注意,我们可以通过以下系统用初始条件q0q-1来模拟:

(11) 链接

def quantity_ar2_process(T, ϕ1, ϕ2, q0, q_1, ϵ, df, params):

    Φ = np.identity(T)  # The T x T identity matrix

    for i in range(T):

        if i-1 >= 0:
            Φ[i, i-1] = -ϕ1

        if i-2 >= 0:
            Φ[i, i-2] = -ϕ2

    B = np.array(df) @ params + ϵ

    B[0] = B[0] + ϕ1 * q0 + ϕ2 * q_1
    B[1] = B[1] + ϕ2 * q0

    return np.linalg.inv(Φ) @ B

## Quantity Demand AR(2) component process

# Parameters
T = 118 # Time periods less two lags 
ϕ1 = 0.3 # Lag 1 coefficient (ϕ1)
ϕ2 = 0.05 # Lag 2 coefficient (ϕ2)
q_1 = 250_000 # Initial Condition q_-1
q0 = 300_000 # Initial Condition q_0
ϵ = np.random.normal(0, 5000, size=T) # Random Error (ϵ)

quantity_demanded_ar = quantity_ar2_process(T,ϕ1,ϕ2,q0,q_1,ϵ,df,params)

# Quantity_demanded target variable
df['quantity_demanded'] = quantity_demanded_ar

# Additional covariates of lagged quantity demanded
df['quantity_demanded_lag1'] = df['quantity_demanded'].shift(1)
df['quantity_demanded_lag2'] = df['quantity_demanded'].shift(2)

计量经济学估计与优化

首先使用我们在方程(2)中的框架,将我们在方程(7)中的约束优化问题转化为一个可以利用我们上面提到的函数constrained_newton_method()来解决的问题:

(12) 链接

如前所述,我们需要估计我们的需求量,q_hat。让我们看看在模拟的 10 年中我们的需求量是什么样的:

我们可以清楚地看到,在年末时出现了一些季节性现象,且我们似乎在处理一个平稳过程(这一切都是由构造决定的)。现在假设我们有以下观察变量:

在方程 8 中,我们的计量经济学规范中,quantity_demanded 是我们的结果 qlog_digital_advertising 是我们的 ln(δ),log_television_advertising 是我们的 ln(τ),constant 是我们的 αquantity_demanded_lag1quantity_demanded_lag2 是我们的自回归组件 q_t-1q_t-2,其余是我们的额外协变量 X,包括季节性 S

现在,有了这些数据,我们试图估计方程 8 中的计量经济学规范。我们可以使用最小二乘法(OLS)来估计这个结构模型。为此,我们将使用statsmodels

一个很好的练习是使用我们构建的牛顿法代码来解决线性回归,并将结果与 statsmodels 进行比较。提示:线性回归中的目标是最小化残差平方和。请注意,我们编写的代码绝非解决线性回归的高效方法,但它更多地用于在模型拟合(回归)背景下说明优化理论。相关代码将在文章末尾提供!

注意,我们删除了前两个观察值,因为这些是我们的前两个滞后项,同时我们删除了七月作为参考月份:

import statsmodels.api as stats

## Fit Econometric model using OLS

df = df[2:] # Drop first two lagged values

y = df['quantity_demanded']
X = df.drop(['quantity_demanded','July'],axis=1)

mod = stats.OLS(y,X)
results = mod.fit()

print(results.summary())

现在我们有了我们对需求量的估计计量经济学规范!几点观察:

  1. 数字广告支出和电视广告支出的对数增加与需求量的增加相关

  2. 价格的增加与需求量的减少相关(这是预期的行为)

  3. 我们看到在 9 月到 12 月期间有明显的季节性需求增长,这与我们上面的时间序列一致

  4. 我们看到需求量的第一个滞后项对现在有预测作用,这支持自回归过程

  • 上面的结果可以通过数据模拟部分的数据显示和比较来验证

现在让我们为优化问题指定我们的符号变量(δ、τ、λ 和 ρ),设定时间 t 上的当前变量值,并从数据中获取滞后值。这样我们就有了构建优化问题所需的一切:

# Build Symbolic Functions with all variables in function
δ, τ, λ, ρ  = sm.symbols('δ τ λ ρ')

## Values of current variables
price = 180
comp_price = 120
Jan = 1

## Obtain Lagged Values
log_digital_advertising_lag1 = df['log_digital_advertising_lag1'].iloc[-1]
log_digital_advertising_lag2 = df['log_digital_advertising_lag2'].iloc[-2]
log_television_advertising_lag1 = df['log_television_advertising_lag1'].iloc[-1]
log_television_advertising_lag2 = df['log_television_advertising_lag2'].iloc[-2]
price_lag1 = df['price_lag1'].iloc[-1]
price_lag2 = df['price_lag2'].iloc[-2]
comp_price_lag1 = df['comp_price_lag1'].iloc[-1]
comp_price_lag2 = df['comp_price_lag2'].iloc[-2]
quantity_demanded_lag1 = df['quantity_demanded_lag1'].iloc[-1]
quantity_demanded_lag2 = df['quantity_demanded_lag2'].iloc[-2]

variables = [sm.log(δ),
            sm.log(τ),
            log_digital_advertising_lag1,
            log_digital_advertising_lag2,
            log_television_advertising_lag1,
            log_television_advertising_lag2,
            price,
            price_lag1,
            price_lag2,
            comp_price,
            comp_price_lag1,
            comp_price_lag2,
            Jan,0,0,0,0,0,0,0,0,0,0, # All Months less July 
            1, # Constant
            quantity_demanded_lag1,
            quantity_demanded_lag2
            ]

# Quantity Demanded
quantity_demanded = np.array([variables]) @ np.array(results.params) # params from ols model
quantity_demanded = quantity_demanded[0]

print(quantity_demanded)

我们从方程 8 得到的估计需求量为:

估计需求量函数

现在我们可以构建我们的收入、成本,并将它们结合起来构建我们的利润函数。这里我们生产每单位的成本为 $140 基础,并且每生产一个额外单位的折扣为 $0.0001:

Revenue = price * quantity_demanded
Cost = quantity_demanded * (140 - 0.0001*quantity_demanded) + τ + δ
profit = Revenue - Cost

print(profit)

估计利润函数

将我们的利润绘制为数字广告支出和电视广告支出的函数,π(δ、τ):

现在我们使用 python 按照方程 12 中的公式解决我们的优化问题,利用我们在第一部分第二部分中学到的优化理论。注意,ρ 的极高值是为了考虑到我们的目标函数值极大,因此我们需要确保惩罚足够大,以避免“跳出”约束。

## Optimization Problem

objective = -profit + λ*(τ + δ - 100_000) - ρ*sm.log((τ-20_000)*(δ-10_000))

symbols = [δ, τ, λ, ρ]
x0 = {δ:20_000, τ:80_000, λ:0, ρ:100000}

results = constrained_newton_method(objective,symbols,x0,iterations=1000)

以及相应的输出:

因此,我们的解决方案是将大约 $66,831 用于数字广告支出,将大约 $33,169 用于电视广告支出。这些数值对应于:

digital_ad = results[δ]
television_ad = results[τ]

quantity = quantity_demanded.evalf(subs={δ:digital_ad,τ:television_ad})
revenue = Revenue.evalf(subs={δ:digital_ad,τ:television_ad})
cost = Cost.evalf(subs={δ:digital_ad,τ:television_ad})
profit = revenue - cost

print(f"Quantity: {int(quantity):,}")
print(f"Total Revenue: ${round(revenue,2):,}")
print(f"Total Cost: ${round(cost,2):,}")
print(f"Profit: ${round(profit,2):,}")

就这样!

结论

在本文中,利润最大化问题绝非完全综合的解决方案。实际上,我们甚至不需要对这么简单的优化问题使用牛顿法!但是,随着优化问题在复杂性和维度上的增加,这在现实世界中非常常见,这些工具变得越来越相关。我们的目标是利用我们在第一部分和第二部分中学到的知识,进行一次有趣的探索,以了解优化理论的无数应用之一。

如果你读到这里,感谢你抽出时间阅读我的文章,并向那些阅读了系列中所有 3 部分的读者表示特别的感谢。我希望你现在对基本的多维优化理论和涉及目标函数约束的扩展有很好的理解。一如既往,我希望你像我写这篇文章时那样享受阅读。如果你对这篇文章和整个系列有任何想法,请告诉我!

附加内容 — 线性回归的数值和解析解

如上所承诺,本节将提供利用牛顿方法解决线性回归问题的代码,并将该结果与解析解(statsmodels 使用的解)进行比较。请回忆,解决线性回归的目标是最小化 残差平方和。也就是说,就矩阵而言,

(A1) 最小化残差平方和

因此,使用我们的牛顿方法函数和框架,我们得到:

# Pull all variables in X and create them as SymPy symbols
variablez = list(df.drop(['quantity_demanded','July'],axis=1).columns)
symbols = []
for i in variablez:
    i  = sm.symbols(f'{i}')
    symbols.append(i)

# Create vectors and matrices of outcome (y), covariates (X), and parameters(β) 
y = np.array(df['quantity_demanded'])
X = np.array(df.drop(['quantity_demanded','July'],axis=1))
β = np.array(symbols)

# Specify objective function and starting values 
objective = (y - X@β).T @ (y - X@β) # Residual Sum of Squares
β_0 = dict(zip(symbols,[0]*len(symbols))) # Initial guess (0 for all)

β_numerical = newton_method(objective,symbols,β_0)

输出如下:

接下来我们将计算解析解。也就是说,如果我们对等式 (A1) 求导并将其设为零并解出 β,我们得到:

(A2) OLS 解析解

编写代码如下 (我们还提供了与 statsmodels 比较的解析标准误差,但不再进一步探讨 — 如果你感兴趣,请参见 OLS 维基百科页面

# OLS Analytical Solution
β_analytical = np.linalg.inv(X.T @ X) @ X.T @ y 

# Compute standard errors
df_residuals = len(X)-len(β_analytical)
σ2 = 1/df_residuals*((y-X@β_analytical).T @ (y-X@β_analytical)) # MSE 
Σ =  σ2 * np.linalg.inv(X.T @ X)
standard_errors = np.sqrt(np.diag(Σ))

与 statsmodels 的所有结果进行比较:

ols_results = pd.DataFrame()

ols_results['variable'] = variablez
ols_results['β_numerical'] = list(β_numerical.values())
ols_results['β_analytical'] = β_analytical
ols_results['std_err_analytical'] = standard_errors
ols_results['β_statsmodels'] = list(results.params) # from statsmodels code above
ols_results['std_err_statsmodels'] = list(results.bse) # from statsmodels code above
ols_results = ols_results.set_index('variable')

ols_results

附录 — 代码

import sympy as sm
import numpy as np

def get_gradient(
    function: sm.core.expr.Expr,
    symbols: list[sm.core.symbol.Symbol],
    x0: dict[sm.core.symbol.Symbol, float],
) -> np.ndarray:
    """
    Calculate the gradient of a function at a given point.

    Args:
        function (sm.core.expr.Expr): The function to calculate the gradient of.
        symbols (list[sm.core.symbol.Symbol]): The symbols representing the variables in the function.
        x0 (dict[sm.core.symbol.Symbol, float]): The point at which to calculate the gradient.

    Returns:
        numpy.ndarray: The gradient of the function at the given point.
    """
    d1 = {}
    gradient = np.array([])

    for i in symbols:
        d1[i] = sm.diff(function, i, 1).evalf(subs=x0)
        gradient = np.append(gradient, d1[i])

    return gradient.astype(np.float64)

def get_hessian(
    function: sm.core.expr.Expr,
    symbols: list[sm.core.symbol.Symbol],
    x0: dict[sm.core.symbol.Symbol, float],
) -> np.ndarray:
    """
    Calculate the Hessian matrix of a function at a given point.

    Args:
    function (sm.core.expr.Expr): The function for which the Hessian matrix is calculated.
    symbols (list[sm.core.symbol.Symbol]): The list of symbols used in the function.
    x0 (dict[sm.core.symbol.Symbol, float]): The point at which the Hessian matrix is evaluated.

    Returns:
    numpy.ndarray: The Hessian matrix of the function at the given point.
    """
    d2 = {}
    hessian = np.array([])

    for i in symbols:
        for j in symbols:
            d2[f"{i}{j}"] = sm.diff(function, i, j).evalf(subs=x0)
            hessian = np.append(hessian, d2[f"{i}{j}"])

    hessian = np.array(np.array_split(hessian, len(symbols)))

    return hessian.astype(np.float64)

def newton_method(
    function: sm.core.expr.Expr,
    symbols: list[sm.core.symbol.Symbol],
    x0: dict[sm.core.symbol.Symbol, float],
    iterations: int = 100,
) -> dict[sm.core.symbol.Symbol, float] or None:
    """
    Perform Newton's method to find the solution to the optimization problem.

    Args:
        function (sm.core.expr.Expr): The objective function to be optimized.
        symbols (list[sm.core.symbol.Symbol]): The symbols used in the objective function.
        x0 (dict[sm.core.symbol.Symbol, float]): The initial values for the symbols.
        iterations (int, optional): The maximum number of iterations. Defaults to 100.

    Returns:
        dict[sm.core.symbol.Symbol, float] or None: The solution to the optimization problem, or None if no solution is found.
    """

    x_star = {}
    x_star[0] = np.array(list(x0.values()))

    # x = [] ## Return x for visual!

    print(f"Starting Values: {x_star[0]}")

    for i in range(iterations):
        # x.append(dict(zip(x0.keys(),x_star[i]))) ## Return x for visual!

        gradient = get_gradient(function, symbols, dict(zip(x0.keys(), x_star[i])))
        hessian = get_hessian(function, symbols, dict(zip(x0.keys(), x_star[i])))

        x_star[i + 1] = x_star[i].T - np.linalg.inv(hessian) @ gradient.T

        if np.linalg.norm(x_star[i + 1] - x_star[i]) < 10e-5:
            solution = dict(zip(x0.keys(), x_star[i + 1]))
            print(f"\nConvergence Achieved ({i+1} iterations): Solution = {solution}")
            break
        else:
            solution = None

        print(f"Step {i+1}: {x_star[i+1]}")

    return solution

def constrained_newton_method(
    function: sm.core.expr.Expr,
    symbols: list[sm.core.symbol.Symbol],
    x0: dict[sm.core.symbol.Symbol, float],
    iterations: int = 100,
) -> dict[sm.core.symbol.Symbol, float] or None:
    """
    Performs constrained Newton's method to find the optimal solution of a function subject to constraints.

    Parameters:
        function (sm.core.expr.Expr): The function to optimize.
        symbols (list[sm.core.symbol.Symbol]): The symbols used in the function.
        x0 (dict[sm.core.symbol.Symbol, float]): The initial values for the symbols.
        iterations (int, optional): The maximum number of iterations. Defaults to 100.

    Returns:
        dict[sm.core.symbol.Symbol, float] or None: The optimal solution if convergence is achieved, otherwise None.
    """
    x_star = {}
    x_star[0] = np.array(list(x0.values())[:-1])

    optimal_solutions = []
    optimal_solutions.append(dict(zip(list(x0.keys())[:-1], x_star[0])))

    for step in range(iterations):
        # Evaluate function at rho value
        if step == 0:  # starting rho
            rho_sub = list(x0.values())[-1]

        rho_sub_values = {list(x0.keys())[-1]: rho_sub}
        function_eval = function.evalf(subs=rho_sub_values)

        print(f"Step {step} w/ {rho_sub_values}")  # Barrier method step
        print(f"Starting Values: {x_star[0]}")

        # Newton's Method
        for i in range(iterations):
            gradient = get_gradient(
                function_eval, symbols[:-1], dict(zip(list(x0.keys())[:-1], x_star[i]))
            )
            hessian = get_hessian(
                function_eval, symbols[:-1], dict(zip(list(x0.keys())[:-1], x_star[i]))
            )

            x_star[i + 1] = x_star[i].T - np.linalg.inv(hessian) @ gradient.T

            if np.linalg.norm(x_star[i + 1] - x_star[i]) < 10e-5:
                solution = dict(zip(list(x0.keys())[:-1], x_star[i + 1]))
                print(
                    f"Convergence Achieved ({i+1} iterations): Solution = {solution}\n"
                )
                break

        # Record optimal solution & previous optimal solution for each barrier method iteration
        optimal_solution = x_star[i + 1]
        previous_optimal_solution = list(optimal_solutions[step - 1].values())
        optimal_solutions.append(dict(zip(list(x0.keys())[:-1], optimal_solution)))

        # Check for overall convergence
        if np.linalg.norm(optimal_solution - previous_optimal_solution) < 10e-5:
            print(
                f"\n Overall Convergence Achieved ({step} steps): Solution = {optimal_solutions[step]}\n"
            )
            overall_solution = optimal_solutions[step]
            break
        else:
            overall_solution = None

        # Set new starting point
        x_star = {}
        x_star[0] = optimal_solution

        # Update rho
        rho_sub = 0.9 * rho_sub

    return overall_solution

通过此 GitHub 仓库访问所有代码: github.com/jakepenzak/Blog-Posts

感谢你阅读我的帖子!我在 Medium 上的帖子旨在探索利用 计量经济学 统计/机器学习 技术的实际和理论应用。此外,我还通过理论和模拟提供关于各种方法学的理论基础的帖子。最重要的是,我写作是为了学习!我希望使复杂的话题对所有人稍微更易于理解。如果你喜欢这篇文章,请考虑 关注我在 Medium 上的账号

优化还是架构:如何破解卡尔曼滤波

原文:towardsdatascience.com/optimization-or-architecture-how-to-hack-kalman-filtering-c0a21ac7d756?source=collection_archive---------3-----------------------#2023-12-26

为什么即使在神经网络不如卡尔曼滤波器时,它们可能看起来更好——以及如何修复这个问题并改进你的卡尔曼滤波器

Ido GreenbergTowards Data Science Ido Greenberg

·

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

--

本文介绍了我们在 NeurIPS 2023 上发表的 近期论文。代码可在 PyPI 上获取。

背景

卡尔曼滤波器 (KF) 自 1960 年以来一直是一种备受推崇的序列预测和控制方法。尽管在过去几十年中出现了许多新方法,但 KF 的简单设计使其至今仍然是一种实用、稳健且具有竞争力的方法。1960 年的原始论文在过去 5 年内已有12K 次引用。其广泛的应用包括导航医疗治疗市场分析深度学习 甚至登月

KF 在阿波罗任务中的示意图(图源:作者)

从技术上讲,KF 从一系列噪声观测(例如雷达/相机)中预测系统的状态 x(例如航天器位置)。它估计状态的分布(例如位置估计 + 不确定性)。每个时间步,它根据动态模型 F 预测下一个状态,并根据动态噪声 Q 增加不确定性。每个观测,它根据新的观测 z 及其噪声 R 更新状态及其不确定性。

KF 的单步操作示意图(图源:作者)

卡尔曼滤波器还是神经网络?

KF 的预测模型 F 是线性的,这有些限制。因此,我们在 KF 的基础上构建了一个高端神经网络。这使我们比标准 KF 获得了更好的预测准确性!这对我们来说真是好事!

为了最终确定我们的论文实验,我们进行了几项消融测试。在其中一个测试中,我们完全移除了神经网络,仅仅优化了内部 KF 参数 QR。当这个优化后的 KF 不仅超越了标准 KF,还超越了我那高端的网络时,你可以想象我脸上的表情!同样的 KF 模型,完全相同的 60 年线性架构,仅通过更改噪声参数的值就变得优越了。

KF、神经 KF (NKF) 和优化 KF (OKF) 的预测误差(图源:我们的论文)

KF 击败神经网络虽然有趣,但对我们的问题来说却是轶事性的。更重要的是方法论的见解:在扩展测试之前,我们差点就宣称网络优于 KF——仅仅因为我们没有正确地比较这两者。

信息 1:为了确保你的神经网络确实比 KF 更好,就像优化网络一样优化 KF。

备注——这是否意味着 KF 比神经网络更好? 我们确实没有做出这样的普遍性声明。我们的声明是关于方法论的——如果你想比较这两种模型,它们都应当类似地优化。话虽如此,我们确实从经验上展示了在多普勒雷达问题中,尽管存在非线性,KF 可能表现更好。事实上,这让我难以接受,以至于我输了关于我的神经 KF 的赌注,花费了许多周的超参数优化和其他技巧。

优化卡尔曼滤波器

在比较两个架构时,类似地优化它们。这听起来有些微不足道,对吧?实际上,这个缺陷并非我们研究中的独特问题:在非线性滤波文献中,线性 KF(或其扩展 EKF)通常作为比较的基准,但很少被优化。事实上,这背后有一个原因:标准 KF 参数“被认为”已经能够提供最优预测,那么何必再进一步优化呢?

KF 参数Q和 R 的标准封闭形式方程。我们关注的是离线数据同时提供状态{x}和观测{z}的设置,因此协方差可以直接计算。其他确定 Q 和 R 的方法通常用于其他设置,例如没有{x}数据的情况。

不幸的是,封闭形式方程的最优性在实践中并不成立,因为它依赖于一组相当强的假设,而这些假设在现实世界中很少成立。事实上,在简单的经典低维多普勒雷达问题中,我们发现了不少于 4 个假设的违背。在某些情况下,这种违背甚至很难察觉:例如,我们模拟了独立同分布的观测噪声——但使用的是球坐标系。一旦转换到笛卡尔坐标系,噪声就不再是独立同分布的了!

多普勒雷达问题中 4 个 KF 假设的违背:非线性动力学;非线性观测;不准确的初始化分布;以及非独立同分布的观测噪声。(图片由作者提供)

信息 2:不要相信 KF 的假设,因此避免使用封闭形式的协方差估计。相反,按照你的损失函数优化参数——就像任何其他预测模型一样。

换句话说,在现实世界中,噪声协方差估计不再是优化预测误差的代理。这种目标之间的差异会导致意外的异常。在一次实验中,我们用一个oracle KF 替代噪声估计,该oracle知道系统中的准确噪声。这个oracle仍然不如优化后的 KF,因为准确的噪声估计不是期望的目标,而是准确的状态预测。在另一次实验中,当 KF 接收到更多数据时,其表现会恶化,因为它实际上追求的是与均方误差(MSE)不同的目标!

测试误差与训练数据大小的关系。标准 KF 不仅劣于优化后的 KF,而且随着数据的增加而恶化,因为其参数并未设置以优化期望的目标。(图像摘自我们的论文)

那么,如何优化 KF 呢?

在标准噪声估计方法用于 KF 调整的背后,是将 KF 参数视为噪声的代表。这种观点在某些情况下是有益的。然而,如上所述,为了优化,我们应该“忘记”KF 参数的这一角色,只将它们视为模型参数,其目标是损失最小化。这种替代观点也告诉我们如何进行优化:就像任何序列预测模型,例如 RNN!给定数据,我们只需进行预测,计算损失,反向传播梯度,更新模型,并重复进行。

与 RNN 的主要区别在于,参数QR以协方差矩阵的形式出现,因此它们应该保持对称且正定。为此,我们使用 Cholesky 分解将Q写成LL**的形式,并优化L的条目。这确保了Q在优化更新后仍然保持正定。这个技巧同时适用于QR*。

OKF 训练过程的伪代码(摘自我们的论文)

在我们所有的实验中,这种优化过程被发现既快速又稳定,因为参数数量比典型神经网络少几个数量级。虽然训练过程容易自己实现,但你也可以使用我们的 PyPI 包,如这个示例所示 😃

总结

如下图所示,我们的主要信息是 KF 假设不可靠,因此我们应该直接优化 KF——无论我们是将其作为主要预测模型,还是作为与新方法比较的参考。

我们的简单训练过程可以在 PyPI 上找到。更重要的是,由于我们的架构与原始 KF 保持一致,任何使用 KF(或扩展 KF)的系统都可以通过重新学习参数轻松升级为 OKF——无需在推理时增加任何代码行。

我们的范围和贡献总结。由于 KF 假设经常被违反,因此必须直接优化 KF。 (图片来自我们的论文)

优化数据仓库存储:视图与表

原文:towardsdatascience.com/optimize-data-warehouse-storage-with-views-and-tables-659dd588345d

表与视图的区别及其使用方法

Madison SchottTowards Data Science Madison Schott

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

--

照片由 Sophia Baboolal 提供,来源于 Unsplash

随着现代数据技术堆栈的兴起,许多公司正在将其数据库从本地迁移到云端。他们开始利用像 Snowflake、Redshift 和 DuckDB 这样的数据仓库工具,以充分发挥云的所有优势。

尽管这些数据仓库通常帮助较小的公司节省开支,但云上的计算成本很容易累积。因此,优化存储和计算成本至关重要。这意味着你需要了解最佳的数据存储方式,以便数据团队能够以具有成本效益的方式使用这些数据。

在本文中,我们将讨论视图与表的区别、数据仓库中存在的不同类型视图以及每种视图的使用场景。阅读完本文后,你应该能够确定存储不同数据集的最佳选项,同时节省成本。

什么是视图?

视图是一个定义的查询,位于表的顶部。与表不同,它不存储实际数据。它始终包含最新的数据,因为每次查询时都会重新运行。而表的内容仅与上次创建或更新时一样,无论你什么时候查询。

视图主要有两种类型:非物化视图物化视图

非物化视图

非物化视图是人们通常想到的视图。这种类型的视图仅在实际查询时运行,否则不会存储在数据库中。

非物化视图很棒,因为它们不占用存储空间,这意味着你不必担心为大量存储付费。它们也只有在需要时才运行,从而节省计算资源费用。这意味着,如果源表几个月或几周都不需要,你无需为其维护付费。只有当分析师或分析工程师重新开始使用该表时,你才需为其付费。

最棒的部分?非物化视图仍然具有与表相同的所有功能!如果需要,你可以在它们上面执行连接、聚合和窗口函数。

不幸的是,就像其他所有事物一样,所有优点背后总会有缺点。非物化视图不适合处理大量数据和复杂逻辑,因为每次查询视图时,这些逻辑都会被执行。

例如,我通常将所有的源数据表创建为非物化视图,这些视图引用我的原始数据。这些是简单的 SELECT 语句,包含基本函数,如列重命名、数据类型转换和数据清理。由于它们的底层逻辑简单,所以每当我查询这些源表时,它们运行得很快。

如果我创建包含连接和窗口函数的复杂数据模型作为视图,可能在查询时这些视图永远不会加载。或者它们需要极其长的时间!显然,这不是理想的情况。你将会花费更多的计算能力来运行视图查询,而不是将视图创建为表。

记住:非物化视图非常适合使用,但仅当创建它们的逻辑是简单的 SELECT 语句时。

物化视图

物化视图在我们讨论的两种视图中较为少见。物化视图的行为更像表。它们查询速度更快,被认为比非物化视图更易于访问。并且,就像表一样,它们在数据仓库中占用更多的存储空间,需要更多的计算资源。这意味着它们是两种视图中更昂贵的选项。

你不会经常需要使用它们。事实上,我从未遇到过一个合理的使用场景。根据Snowflake 的文档,你应该仅在以下所有条件都满足时使用物化视图:

  • 视图的结果被频繁使用

  • 驱动视图的查询使用了大量资源

  • 视图变化频繁

这三者同时适用于你的基础/暂存、中间和核心 dbt 模型的情况非常少见。基础/暂存模型不会消耗大量资源,而中间和核心数据模型变化不频繁。当然,总会有例外情况,但我还没遇到过这种情况。

这些视图在数据建模中的使用方式

如果你是分析工程师,你可能会想知道未物化视图和物化视图在数据建模中如何使用。让我们深入了解一下 dbt 基础(或阶段)模型以及核心模型。

‍dbt 基础模型

dbt 基础模型作为视图存在于你的原始数据之上。它们被创建为未物化视图,以保持原始数据的完整性,同时利用适当的命名约定和公司标准。这些模型中的代码是从原始数据中直接读取的基本 SQL 选择语句,通过像 Airbyte 这样的摄取工具进行 ELT。一个典型的基础模型如下:

select
  ad_id AS facebook_ad_id,
  account_id,
  ad_name AS ad_name_1,
  adset_name,
  month(date) AS month_created_at,
  date::timestamp_ntz AS created_at,
  spend
from {{ source('facebook', 'basic_ad')}}

‍如果你查看 dbt 中此文件的底层逻辑,它实际上会在 Snowflake(我首选的数据仓库)中编译为如下所示:

create or replace view data_mart_dev.base.base_facebook_ads 
  as (

    select
      ad_id AS facebook_ad_id,
      account_id,
      ad_name AS ad_name_1,
      adset_name,
      month(date) AS month_created_at,
      date::timestamp_ntz AS created_at,
      spend
    from raw.facebook.basic_ad

    );

由于你仅使用基本的日期函数和重命名列,视图在按需查询时仍然很快。这反过来节省了你原本会用来保存几乎相同的原始数据副本的存储空间。

dbt 核心模型

在 dbt 中,你的核心模型比基础模型更复杂,通常包含多个 CTE、联接和窗口函数。虽然你可能有特定的用例来将这些创建为物化视图,但你很可能会将这些创建为数据仓库中的表。表对于处理复杂的转换非常理想,如果将其存储为视图,会需要较长的运行时间。

这是我一个核心数据模型的代码示例:

with

  fb_spend_unioned AS (

    select created_at, spend, 'company_1' AS source from {{   ref('base_fb_ads_company1')}}
    UNION ALL
    select created_at, spend, 'company_2' AS source from {{ ref('base_fb_ads_company2')}}

  ),

  fb_spend_summed AS (

    select
      month(created_at) AS spend_month,
      year(created_at) AS spend_year,
      created_at AS spend_date,
      sum(spend) AS spend
    from fb_spend_unioned 
    where spend != 0
    group by
      created_at,
      month(created_at),
      year(created_at)

  )

  select * from fb_spend_summed

当在 Snowflake 中编译为 SQL 时,代码将如下所示:

create or replace table data_mart_dev.core.fb_spend_summed

  as (

    with 

    fb_spend_unioned AS (

    select created_at, spend, 'company_1' AS source from {{   ref('base_fb_ads_company1')}}
    UNION ALL
    select created_at, spend, 'company_2' AS source from {{ ref('base_fb_ads_company2')}}

),

  fb_spend_summed AS (

    select
      month(created_at) AS spend_month,
      year(created_at) AS spend_year,
      created_at AS spend_date,
      sum(spend) AS spend
    from fb_spend_unioned 
    where spend != 0
    group by
      created_at,
      month(created_at),
      year(created_at)

  )

  select * from fb_spend_summed

  );

请注意,这里创建的是一个 Snowflake 中的表而不是视图。这对于任何将直接用于 BI 工具的数据是理想的,大多数核心数据模型都是这样。它们可以按需轻松查询,无需运行底层逻辑。这确保了快速的仪表板,能够让利益相关者信赖。

结论

视图和表在数据仓库中存在不同的原因。视图不存储实际数据,可以作为一个工具来节省开支,通过简单查询在其他表之上运行。表应当用于存储由更复杂逻辑生成的数据,确保性能和可用性始终较高。

正确使用时,非物化视图是在 Snowflake 中节省开支的好工具,同时不牺牲性能。我强烈推荐在 dbt 中将其用于你的基础模型,以创建符合你设定的公司标准的高质量数据。而且,不要忘记在核心 dbt 模型中使用表。性能提升是值得更高成本的!

欲了解更多分析工程的信息,订阅我的免费每周通讯,我会分享学习资源、教程、最佳实践等。

查看我的第一本电子书,分析工程的基础知识,这是一本关于如何开始从事分析工程角色的全方位指南。

优化浏览分类法

原文:towardsdatascience.com/optimizing-browsing-a-taxonomy-30596bcdbd9d

问题、模型和实施问题

Arun JagotaTowards Data Science Arun Jagota

·发表在Towards Data Science ·阅读时间 20 分钟·2023 年 1 月 4 日

--

图片来自mcmurryjuliepixabay

这里的主要用例是jagota-arun.medium.com/interactive-and-adaptive-menu-ordering-agent-bb447c58b3af(不是必读材料。)

我们专注于建模。它是什么?它如何与推荐系统相关?然后是实施。就像面向对象设计一样。类等。

面向对象的阐述风格将序列化、模块化、可组合性和可扩展性的好处扩展到写作中。这里非常合适。

本文最重要的概念是分类法。一切都发生在它上面。用户浏览、用户行为(在叶子上)、建模和评分,以动态重新排列分类法,优化用户的浏览体验。

那么让我们从定义它开始。分类法是一个选择树,具体的感兴趣项目位于其叶子上。我们最终假设用户关心的是到达感兴趣的叶子。内部节点主要用于导航。我们通过用户是否在叶子上采取行动来学习。

一个例子是餐厅菜单。菜单项位于叶子上。内部节点是菜单上的各种类别:饮料、开胃菜、主菜等。用户的一个动作是点选特定的菜单项。如果用户想点饮料,她会进入饮料子树。

在视频观看场景中,用户的行为可能是观看视频。

我们将用户与分类法的互动建模为一个会话,其中用户浏览树的某些部分。当用户到达一个合适的叶子时,她可以选择行动或不行动。

我们的主要兴趣是从用户在特定叶子上的行为中学习偏好,以便在树中的某些节点上表现出偏好。我们希望在叶子节点和内部节点上都能学习到偏好。

这与常规推荐系统有何关系?

通常,我们期望推荐系统主要推荐新的项目。我们在这里的重点是简化用户对感兴趣项目的导航。这可能包括用户之前已操作过的项目。例如餐厅设置。

另一个需要注意的是,推荐系统通常基于用户-物品矩阵操作,而不是基于分类法。

面向对象的阐述

我们最重要的类将是 UserInteractionsWithTaxonomy。其角色应从其名称和我们之前的讨论中明确。

这个类会被实例化以建模特定用户与特定分类法的互动。实例化时需要提供分类法。调用者负责将实例化的模型与适当的用户关联。如下面所示。这个类会为分类法中的每个节点维护一个计数器。这些计数器在新实例中会被初始化为零。

john_doe_interactions_with_specific_restaurant =
UserInteractionsWithTaxonomy(taxonomy_of_restaurant)

“用户”可以是模型创建者想要的任何人。一个人或一整个群体。

关键方法

UserInteractionsWithTaxonomy 将支持一个方法 act。这个方法将建模用户在特定叶子上的一次行为实例。例如,点餐时选择某个特定的菜品。

在我们版本的 act 方法中,每次调用都会递增叶子及其所有祖先的计数。为了高效地做到这一点,这个类可能会维护分类法中所有节点的父节点弧。

请注意,我们选择仅从 act 行为中学习用户偏好。我们可以将此推广到包括用户点击树的内部节点的反馈。但在这里我们不会追求这一点,因为我们认为 act 行为比内部节点点击更能预测用户偏好。读者可以自行尝试。

第二个方法将模拟用户在树上的某个节点上点击。这将让我们追踪用户当前在树上的位置,以便我们可以在下面展示适当的选择。

我们将在一个方法 choices() 中表达这些选择,该方法将返回当前用户在树上所在节点的子节点的得分。更精确地说,它将返回一个 [child, score] 对列表,其中 childself.current_node 的子节点,score 是其得分。这个列表按得分的非递增顺序排列。

这是一个示例流程。

john_doe_interactions_with_specific_restaurant.click("Drinks")
choices = john_doe_interactions_with_specific_restaurant.choices()
# User picks 'Coca Cola' from choices and acts on it. 
# ('act' is 'order here'.)
john_doe_interactions_with_specific_restaurant.act('Coca Cola')

让我们一起来看看这个类的内部细节。

class UserInteractionsWithTaxonomy:

  def _init_(self, taxonomy):
    self.taxonomy = taxonomy
    self.current_node = None

  def click(self, node):
    self.current_node = node

  def act(self, leaf, pc = 1):
    # increment the leaf's counter by pc
    # increment the counters of all of the leaf's ancestors, each by pc

  def choices(self):
    res = {}
    for each child of self.current_node:
      res[child] = self.score(child, self.current_node)
    return res

  def score(self,child,parent):
    return self.counter[child]/self.counter[parent]

请注意 act 方法中的参数 pc。我们稍后会解释它。目前,假设 pc 等于 1,即其默认值。因此,“递增 … by pc” 意味着 “递增 … by 1” 或简称 “递增 …”。

方法score(.)返回P(child|parent),即用户在parent位置时访问child的概率。这只是子项的计数除以父项的计数。

冷启动问题

假设用户访问了一家新餐馆。由于用户在该餐馆没有订单历史记录,因此不能使用评分方法。因此,我们必须等用户点几个项目后才能评分。这个场景被称为用户冷启动问题

类似地,还有一个分类冷启动问题。在餐馆的场景中,这对应于一个全新的餐馆开张。尚未与任何用户互动。

实际上,我们当前评分中隐藏了更细致的冷启动问题。我们的评分函数是

self.counter[child]/self.counter[parent]

想象一下一个餐馆,用户有订单历史记录,但在特定类别下没有,例如甜点父类别 = 甜点的计数将为 0,因此其子类别的得分也无法计算。

现在想象一下,用户在某个类别中下单,但未在特定子类别中下单。例如,在饮料类别中点了一种特定的饮料。子项的得分将为 0。虽然 0 是一个有效的得分,但可能不是我们所希望的。

总结来说,未曾被点过的项目将无法评分或得分为 0。

冷启动问题在推荐系统的环境中经常出现,见[4]。在这种环境下,分类冷启动问题对应于项目冷启动问题。

缓解冷启动问题

缓解所有冷启动问题的一个简单方法是将所有节点上的计数器初始化为一个称为pc的正数。虽然它需要是正数,但可以小于 1。

在我们的设置中,我们将支持一个类似但更强大的机制。无论是从建模还是编码的角度来看,都不会更复杂。

我们将为类UserInteractionsWithTaxonomy添加一个方法act_randomly(n, pc)。

class UserInteractionsWithTaxonomy:

  …

  def act_randomly(self, n. pc):
    for i in range(n):
      leaf = leaves.sample_randomly()
      self.act(leaf, pc)

现在回到用户第一次访问餐馆的场景。假设我们调用act_randomly(n = 100, pc = 0.01)。这样做的效果就像用户在这家餐馆点了 100 次,每次随机选择菜单上的一项。为了考虑pc,我们应该将这些订单称为“分数订单”。

为什么要使用pc?也就是说,为什么不设置为 1?因为在某些环境中,我们可能希望单个实际订单比单个虚拟订单更重要。在这种情况下,我们会将pc设置为远小于 1 的值。在其他环境中,我们可能希望相反的行为。只有当用户订单达到一定的最小次数时,我们才会认为它是重要的。在这种情况下,我们会将pc设置为大于 1 的值。

act_randomly方法可以缓解所有上述冷启动问题。不过,我们通常可以在用户冷启动问题上做得更好。

使用分类模型加热用户冷启动

考虑一个有很多用户互动历史的餐厅。这些历史互动可以用来为该餐厅形成一个模型。这个模型,我们称之为餐厅模型,实际上是UserInteractionsWithTaxonomy的一个实例,其中用户是一个集体而非个人。

为了在特定用户的交互中利用分类模型,我们将重构UserInteractionsWithTaxonomy类如下。

class UserInteractionsWithTaxonomy:

  def __init__(self, taxonomy, taxonomy_model = None):
    …
    self.taxonomy_model = taxonomy_model
    …

  def act_on_this(self, leaf, pc = 1):
    # Previous version of act(.) renamed

  def act(self, leaf):
    self.act_on_this(leaf)
    if self.taxonomy_model is not None:
      self.taxonomy_model.act_on_this(leaf)

  def act_randomly(self, n. pc):
      for i in range(n):
        …
        self.act_on_this(leaf, pc)

  def score(self,child,parent, pcm = 1):
    if self.taxonomy_model is None:
      return self.counter[child]/self.counter[parent]
    else:
      s = self.taxonomy_model.score(child, parent)
      pc = pcm*s
      child_adjusted_count   = self.counter[child] + pc
      parent_adjusted_count  = self.counter[parent] + pcm
    return child_adjusted_count/parent_adjusted_count

让我们解释一下。在实例化UserInteractionsWithTaxonomy时,我们提供另一个UserInteractionsWithTaxonomy的实例,它将用作分类模型,如下所示。

restaurant_model = UserInteractionsWithTaxonomy(taxonomy_of_restaurant)
user_restaurant_models = {}
for user in users_of_this_restaurant:
  user_restaurant_models[user] = UserInteractionsWithTaxonomy(
    taxonomy_of_restaurant, restaurant_model)

在上面的示例中,所有用户餐厅模型共享相同的(通用)餐厅模型。

对任何用户餐厅模型上的act,即使是未来的模型,也会对共享餐厅模型执行相同的操作。

为了干净地实现这一点,我们将之前的act版本重命名为act_on_this,以便可以在其上构建新的act。由于这一名称更改,我们还不得不对act_randomly(.)进行更改。

然后我们重构了方法score(.),使得如果实例附加了分类模型,它也会在评分中使用该模型。

score(.)的重构版本中发生了很多事情,所以我们来详细说明一下。考虑在用户特定模型中对某个孩子与某个父母进行评分。首先,我们调用

self.taxonomy_model.score(child, parent)

这给我们提供了P(child|parent, generic taxonomy model)。接下来,我们假设对父母进行pcm次访问,其中pcmP(child|parent, generic taxonomy model)预计会以child*结束。因此,我们将这些想象中的访问次数加到用户的实际访问次数上。

在上面的段落中,术语“访问节点”仅用于说明。在实际情况中,对一个节点的访问对应于在该节点下的叶子上的act实例。

让我们看一个数字示例。假设pcm是 10。假设P(child|parent, generic taxonomy model)等于⅕。对于这个孩子,pc将是 2。如果特定用户从未访问过父母,P(child|parent, user model for this taxonomy)将等于⅕,即P(child|parent, generic taxonomy model)的值。随着用户开始访问parent,用户随后访问的孩子将开始影响P(child|parent, user model for this taxonomy),如果用户的行为偏离,可能会将其从P(child|parent, generic taxonomy model)中移开。

建模用户偏好

考虑这种情况。一个全新的餐厅刚刚开张。由于没有历史记录,因此没有餐厅模型。

一个用户走进来。当然,我们可以使用act_randomly从菜单中随机采样,如前所述。我们能做得更好吗?

我们应该将餐馆模型实例与每一个访问餐馆的新用户关联起来。这样,至少餐馆模型会比任何一个个人学习得更快。任何突出显示的菜单项,即在用户中被频繁点餐的项,会更快地被呈现出来。我们还能做得更好吗?

假设用户过去访问过其他餐馆。如果我们能够对这些访问建模并学习用户的偏好,我们就可以将这些偏好应用到用户首次访问这家新餐馆时。例如,某些词汇如curry的偏好,或某些价格范围的偏好。

虽然在前一段中我们使用了餐馆冷启动问题来激励建模用户偏好,但解决这个问题涉及到复杂的知识转移问题。这是因为用户之前访问的餐馆的分类法可能与当前餐馆的分类法不同。所以我们将推迟讨论这个用例到另外一篇文章中。

也就是说,将用户偏好纳入我们的模型可以帮助未来用户与相同分类法的互动。

比如,当有很多菜品时。用户可能会错过一些好的菜品。一些餐馆的菜单上有超过四十种菜品。在其他情况下,分类法甚至可能更大。例如,Netflix、Amazon Prime 或其他各种流媒体视频服务上的视频。

或者,当相同的词出现在多个项目中时。这甚至适用于小型分类法。想象一下访问一家泰国餐馆,该餐馆有几个主菜中包含了词汇 curry。假设用户点了其中一道菜。下次,用户会看到其他咖喱菜品被显著展示。这很有道理,对吧?

词袋模型

既然我们决定对用户偏好进行建模,就让我们开始吧。我们将把它们建模为词袋。

词袋模型是一个词频对的集合。一个例子是 { beer :10, wine :2 }。我们可以将这个例子解释为用户偏好啤酒而非葡萄酒。

虽然我们在这里使用了“词”这个术语,但我们可以根据需要定义它。例如,我们可以把术语“wheat beer”表示为“wheat-bear”。

我们将把BagOfWords建模为一个类。我们将用一个哈希来表示词袋,即 Python 的Dict。键是单词,值是它们的频率。

我们将如何具体使用BagOfWords?在实例化UserInteractionsWithTaxonomy时,我们还将创建一个BagOfWords实例来建模用户的关键词偏好。当用户对某个叶子进行操作时,我们将把该叶子名称的词袋添加到代表用户偏好的词袋中。为此,BagOfWords 类需要支持一个“add”方法,用于将提供的词袋添加到实例的词袋中。

这是我们的初始版本的 BagOfWords。

class BagOfWords:

  def _init_(self):
    # Create self.bow

  def add(self, bow):
    for word in bow:
      self.bow[word] += bow[word]

这是用户与分类法交互的版本,重构以建模她在分类法中的交互中的用户偏好。

class UserInteractionsWithTaxonomy():

  def _init_(self, taxonomy):
    …
    self.user_likes = BagOfWords()

  def act(self, leaf):
    …
    self.user_likes.add(leaf.name.bow)

使用用户偏好

现在我们完成了首个版本的用户偏好建模和训练,让我们讨论一下如何使用它。

我们从回顾UserInteractionsWithTaxonomy中的score方法开始。它对特定子项进行评分,以区分给定父项的各种子项,用于导航目的。

现在假设我们想要增强评分功能,以便除了订单数量外,还使用用户偏好。

在我们当前的设计中,用户偏好被建模为在用户与分类法交互实例中维护的单一词袋。鉴于我们打算如何使用评分函数,即区分一个父项的各种子项,如果每个父项拥有自己的用户偏好会更自然。这意味着分类法中的每个内部节点都应该有自己的用户偏好。

这将使我们可以将 UserInteractionsWithTaxonomy.score 重构如下。

class UserInteractionsWithTaxonomy:

  …

  def score(self,child,parent, pcm = 1):
    …
    if it makes sense to score off user likes and not order frequencies:
      return self.user_likes[parent].score(self.user_likes[child])
    …

我们不会详细说明“是否有意义基于用户喜欢而非订单频率进行评分”这一点。这需要一些讨论。

从单一用户偏好模型转向节点特定的用户偏好模型乍看之下可能令人望而生畏。经过进一步考虑,首先,这种变化并不像最初看起来那样令人生畏;其次,它实际上可以使评分更加准确。

详细说明一下,实际中许多分类法会很小。例如,即使是一个全方位服务的餐厅的分类法,通常也只有四个内部节点,不包括根节点:饮料开胃菜主菜甜点

详细说明一下,某些词可能跨越节点边界。在某个餐厅中,例如,三文鱼这个词出现在开胃菜的名称中,也出现在主菜的名称中。假设从某个用户的订单历史中,我们发现她喜欢三文鱼开胃菜但不喜欢三文鱼主菜。当她寻找开胃菜时,我们应该显著展示三文鱼开胃菜。当她寻找主菜时,我们则不应显著展示三文鱼主菜。

既然我们决定继续进行这种建模增强,我们需要重构init()以构造节点特定的BagOfWords实例,并适当地更新act(.)。以下是重构后的版本。

class UserInteractionsWithTaxonomy:

  def _init_(self, taxonomy):
    …
    self.user_likes = {}
    for each internal node in the taxonomy:
      self.user_likes[node] = BagOfWords()

  def act(self, leaf):
    …
    for each node that is an ancestor of leaf:
      self.user_likes[node].add(leaf.name.bow)

何时基于用户偏好而非订单频率进行评分

现在让我们讨论一下

if it makes sense to score off user likes and not order frequencies:
  return self.user_likes[parent].score(self.user_likes[child])

部分。

自我.计数器[父项]的值足够大时,基于订单频率来进行评分比基于用户偏好更好。这是因为实际订单通常应该比用户偏好具有更高的权重。某个特定项目中可能有些东西未被其名称捕捉到,从而解释了用户为何频繁订购它。或者避免它。也许是口味。

self.counter[parent]的值不够大时,我们有几个选择。我们可以基于分类模型和用户的订单历史来打分。或者我们可以根据用户的偏好来打分。

用简单的语言来说,上一段的选项可以理解为以下内容。

对某个项目或类别与其父类别进行评分,依据

  • 其他人在这家餐厅的体验。

  • 用户自己在这个父类下的体验,可能结合其他用户的体验,点了与这个词有一些相同的词的项目。

让我们用一个例子来说明这两者。假设一个用户第一次访问某个泰国餐馆。如果用户知道某道主菜非常受欢迎,她可能会有兴趣点这道菜。如果用户知道餐馆中的主菜中含有咖喱这个词很受欢迎,那么所有这些主菜都应该为这个用户打高分。

BagOfWords 中的评分方法

首先,让我们提醒自己我们在UserInteractionsWithTaxonomy.score(.)中添加的相关部分:

if it makes sense to score off user likes and not order frequencies:
  return self.user_likes[parent].score(self.user_likes[child])

我们看到我们需要向BagOfWords添加一个score()方法。调用BagOfWords.score(bag_of_words)将把被调用的对象解释为父对象,把传递给它的词袋解释为某个子对象。

我们之所以提到这一点,是因为这里存在固有的不对称性。我们不是试图以对称的方式对两个词袋进行评分。相反,我们寻求评分一个特定子词袋在其父词袋中的适配程度。

让我们看一个例子来解释这一点,同时给出一些对我们评分函数的具体要求。

考虑一个有啤酒类别并且有多个项目的餐厅。假设一个用户访问过餐厅几次,每次都点了淡啤酒。假设啤酒这两个词在这些点过的项目中明确出现。用户在啤酒类别下的词袋现在将包含这两个词。

假设在啤酒菜单中有一个名为Coors Light的项目,但用户在之前的访问中没有点过这个项目。这个项目应该得高分,因为它是一种淡啤酒。即使用户还没有点过包含Coors这个词的啤酒。

现在考虑啤酒类别下的第二个项目,名为Coors Beer。它的评分不应该像Coors Light那么高。为什么呢?因为啤酒这个词在啤酒类别中的信息量不如。假设这两个词在用户的啤酒类别词袋中的频率相同,那么应该胜出。

现在,让我们朝着一个能满足我们需求的评分函数努力。

我们的第一个决定是仅对那些在子词袋和父词袋中都存在的词进行评分。对于我们的两个例子,如下所示。

{ coors, light } and { light, beer } ⇒ light
{ coors, beer } and { light, beer}   ⇒ beer

我们的第二个决定是独立并且加法地评分这个交集中的词。

我们的第三个决定是对交集中的任何单词 w 进行如下评分

f(w, child)log P(w|parent, user)/P(w|parent*, uniform user)

这里 f(w, child, user) 是用户的子词袋中 w 的频率。

P(w|parent, user) 是 f(w, parent, user)/sum_{w’ in parent} f(w’, parent, user)

P(w|parent, uniform user) 类似,只是用户是我们所说的“均匀用户”。均匀用户被假定为对 child 下的任何项都有相等的订购概率。

在关联规则挖掘中,P(w|parent, user)/P(w|parent, uniform user) 是所谓的提升 [3] 的一种版本。

让我们看看这些在我们的示例中是如何工作的。下图展示了这个用户对单词 lightbeer 的评分。为了简化表达,省略了术语 parent

log P(light|user)/P(light|uniform user)

log P(beer|user)/P(beer|uniform user)

P(light|user) 和 P(beer|user) 各为 ½。假设菜单上包含单词 beer 的啤酒比包含单词 light 的啤酒多。P(light|uniform user) 可能会显著小于 P(beer|uniform user)。因此,log P(light|user)/P(light|uniform user) 可能会显著大于 log P(beer|user)/P(beer|uniform user)。这是预期中的结果。

让我们在伪代码中查看完整的得分函数。实现中的一个重要细节是评分将分布在两个类之间:BagOfWordsUserInteractionsWithTaxonomy。首先,让我们查看两个类中的实际更改,然后再解释原因。

让我们从重构后的 BagOfWords 开始。由于更改并不是局部的,我们将在这里全面展示类的新版本。

class BagOfWords:

  def _init_(self):
    # Create self.bow
    n = 0

  def add(self, bow):
    for word in bow:
      self.bow[word] += bow[word]
      n += bow[word]

  def p(self, w):
    return float(self.bow[w])/n

  def score(self, user_bow):
    words_in_common = set(user_bow.keys()) and set(self.bow.keys())
    score = 0
    for w in words_in_common:
      score += user_bow[w]*log(self.p(w))
    return score

现在让我们引入重构后的 UserInteractionsWithTaxonomy 类。

class UserInteractionsWithTaxonomy:

  def __init__(self, taxonomy, taxonomy_model = None, uniform_acts = None):
    …
    self.taxonomy_model = taxonomy_model
    self.uniform_acts   = uniform_acts
    …

  def score_using_user_preferences(self, child, parent):
    user_score = self.user_likes[parent].score(self.user_likes[child])
    uniform_score = self.uniform_acts.user_likes[parent].
      score(self.user_likes[child])
    return user_score - uniform_score

  def score(self,child,parent, pcm = 1):
    …
    if it makes sense to score off user likes and not order frequencies:
      return self.score_using_user_preferences(child, parent)
    …

扩展用户喜好模型

到目前为止,我们已经将用户喜好建模为词袋。在许多用例中,分类法的叶子上可能还有其他属性,这些属性可能会影响用户的喜好。例如 price

为了超越词袋的模型,将用户喜好建模为一个独立的类 UserLikes 是合理的。我们还需要重构 UserInteractionsWithTaxonomy 以使用 UserLikes

首先,让我们回顾一下 BagOfWordsUserInteractionsWithTaxonomy 中的使用情况,因为这是我们需要推广的内容。

class UserInteractionsWithTaxonomy:

  def _init_(self, taxonomy):
    …
    …
    self.user_likes[node] = BagOfWords()

  def act(self, leaf):
    …
    for each node that is an ancestor of leaf:
      self.user_likes[node].add(leaf.name.bow)

  def score_using_user_preferences(self, child, parent):
    user_score = self.user_likes[parent].score(self.user_likes[child])
    uniform_score = self.uniform_acts.user_likes[parent].
      score(self.user_likes[child])
    return user_score - uniform_score 

很明显,我们需要进行以下更改:

self.user_likes[node] = BagOfWords() ⇒
self.user_likes[node] = UserLikes()

self.user_likes[node].add(leaf.name.bow) ⇒
self.user_likes[node].add(leaf)

假设这些更改已经完成。

现在让我们讨论一下对 UserLikes 类的影响。

它需要支持一个方法 add(leaf),该方法从 leaf 中提取所需的所有属性,并将它们添加到当前的用户喜好模型中。

它需要支持一个方法 score,以根据 self(即父节点)对任何给定子节点的用户喜好进行评分。

我们几乎准备好查看UserLikes类了。在此之前,让我们建模一个额外的属性,因为这就是我们首先构建此类的原因。

让我们从价格开始。首先,我们需要讨论其建模方面。

价格基础建模

想象一下访问一家餐馆,其菜单如下所示。这里只显示了部分。对于这些项目,仅显示价格。

饮料:$2, $3, $2, $8, $9, … 主菜:$8, $9, $22, $28, $9, $8, …

“…”是为了表示每个类别中还有许多其他项目。

加粗的项目是特定用户在这家餐馆过去订购的项目。由此,我们可以推断出至少在这家餐馆中,这位用户偏好每个类别中相对较贵的项目。(当然,可能还有其他因素,但这就是我们从这些数据中可以得出的结论。)

这些信息可以用来在用户下次访问这家餐馆时更显著地展示某些项目。也许以不同的顺序展示,如下所示。

饮料:$8, $9, $2, $3, \(2, … *主菜*:\)22, $28, $8, $9, $9, $8, …

价格详细建模

我们可以像处理项目名称中的单词一样处理价格。这很好地融入了我们当前的设计。每个内部节点都可以有自己的价格包,就像它有自己的单词包一样。

节点特定的价格包比节点特定的单词包更有动机。这是因为价格在预测的类别上比单词更不具体。用户可能不会在饮料上花费$8,但可能会在主菜上花费这么多。类别特定的价格包将允许我们建模这种区分。

现在,我们将价格包建模为BagOfWords的一个实例。这将满足我们基本的需求。即,对于每个类别,我们记录用户在该类别中订购的项目价格及其频率。

UserLikes

现在是伪代码部分。

class UserLikes:

  def __init__(self):
    self.bow = BagOfWords()
    # Additional attributes
    self.prices = BagOfWords()

  def add(self, leaf):
    self.bow.add(leaf.name.bow)
    self.prices.add(leaf.price.as_bow())

  def score(self, user_likes):
    # This needs some modeling discussion first.

注意leaf.price.as_bow。 “as_bow”是指价格(一个标量)已被表示为一个包含单一键(价格)及其频率为 1 的单词包。

UserLikes.score(.)

考虑一下这个。

class UserLikes:

  …

  def score(self, user_likes):
    return self.bow.score(user_likes.bow) + 
           self.prices.score(user_likes.prices)

注意到自我是父级的UserLikes,在score(.)的参数中提供的user_likes是特定子级的,通过观察两次调用score(.)中的发生情况,我们可以将其表示为

sum_{w appears in both parent.user_likes.bow and in child.user_likes.bow}
child.user_likes.bow[w]*log(parent.user_likes.bow.p(w)
+
sum_{p appears in both parent.user_likes.prices and in child.user_likes.prices}
child.user_likes.prices[p]*log(parent.user_likes.prices.p(p)

现在考虑到

UserInteractionsWithTaxonomy.score_using_user_preferences(child, parent)

它调用两次,一次如上所述,一次对不同的用户进行均匀交互。

因此,整体评分函数可以表示为

(
  sum_{w appears in both parent.user_likes.bow and in 
    child.user_likes.bow}
    child.user_likes.bow[w]*log(parent.user_likes.bow.p(w)

+

  sum_{p appears in both parent.user_likes.prices and in 
    child.user_likes.prices}
    child.user_likes.prices[p]*log(parent.user_likes.prices.p(p)
)

-

(
  sum_{w appears in both parent.uniform_user.user_likes.bow and in 
    child.uniform_user.user_likes.bow}
    child.uniform_user.user_likes.bow[w]*log(parent.uniform_user.user_likes.bow.p(w)

+

  sum_{p appears in both parent.uniform_user.user_likes.prices and in 
    child.uniform_user.user_likes.prices}
    child.uniform_user.user_likes.prices[p]*log(parent.uniform_user_likes.prices.p(p)
)

让我们在一个稍作改进的场景中尝试理解这一点。假设有一家餐厅的分类为啤酒。一个特定的用户已经两次光顾这家餐厅,每次点的都是一些淡色啤酒。(两次点的啤酒不同。)假设“淡色”和“啤酒”这两个词显式出现在所点啤酒的名称中。

现在进行改进。假设两款点的啤酒分别定价为$5 和$6。现在考虑菜单上另外两款啤酒。两者均不包括在用户之前点过的两款啤酒中。

Coors Light $5
? Light     $8

上述评分函数会给Coors Light更高的分数。这是因为虽然两者都包含“light”这个词,但第二款的价格显著高于用户之前点的啤酒。

我们可以通过替换进一步改进这一点

p appears in both parent.user_likes.prices and in child.user_likes.prices

p is in the price ranges of both parent.user_likes.prices and of child.user_likes.prices

我们不会在这篇文章中再多说了。

总结

在这篇文章中,我们深入探讨了优化用户在分类体系中的浏览,以便在叶子节点中找到感兴趣的项目。我们介绍了一种从用户操作的项目反馈中学习的算法。(在餐厅环境中,即用户实际点的项目。)

反馈被推送到分类体系的上层,以便内部节点可以从中受益。这使得这些节点可以被更(或更少)优先显示,以便用户在未来更容易找到感兴趣的项目。

“感兴趣的项目”包括用户过去操作过的项目,以及足够相似且值得推荐的项目。

初始模型基于按分类体系聚合的订单频率。通过增强模型以模拟随机行为并使用从用户集体参与分类体系中不断学习的分类模型,也解决了各种冷启动问题。

初始模型随后得到增强,以模拟和使用用户偏好,这些偏好可以从用户操作的项目名称中的词汇和其价格中辨别出来。像初始模型一样,用户偏好被映射到分类体系中。这适应了在特定分类体系中的细致学习。例如

  • 用户愿意为这家餐厅的主菜支付$8,但不愿为饮料付费。

  • 用户喜欢这家餐厅的三文鱼开胃菜,但不喜欢三文鱼主菜。

在这篇文章中,建模和评分是通过 Python 伪代码构建的。在各个阶段,当需要时,伪代码会进行重构。在这篇文章中,Python 伪代码作为沟通和讨论想法的语言。它也可能对那些希望练习实现的读者有用。伪代码的模块化程度足够高,可以逐步实现。

参考文献

  1. 向量空间模型 — 维基百科

  2. 杰卡德指数 — 维基百科

  3. 关联规则学习 — 维基百科

  4. 冷启动(推荐系统) — 维基百科

优化连接:图中的数学优化

原文:towardsdatascience.com/optimizing-connections-mathematical-optimization-within-graphs-7364e082a984

断开的图。图片由作者用 Dall-E 2 创建。

图论及其应用简介

Hennie de HarderTowards Data Science Hennie de Harder

·发表在 Towards Data Science ·13 min read·2023 年 7 月 28 日

--

在这篇文章中,我们将深入探讨图中的数学优化,探索关键概念、算法和实际应用。图问题可以在许多地方找到。显而易见的例子有物流或社交网络分析,比如寻找一个配送公司的最佳路线或两个个体之间的最少连接数。但你知道图也适用于城市规划、疾病传播建模、欺诈检测、推荐引擎和网络安全吗?通过利用专为图设计的优化算法,数据科学家可以发现最佳解决方案,高效分配资源,并做出数据驱动的决策。

首先,我们将从介绍部分开始,以解释图的基础知识。然后,我们将深入探讨常见的图问题和算法,尝试解决这些问题。

图的基础知识

作为回顾,以下是图论的基础知识。

什么是图?

图由顶点(或节点)和边组成。如果顶点以某种方式相关,它们就通过边连接。要定义一个图,你需要所有顶点的名称,并且需要知道哪些顶点是连接的。

下面是一个具有顶点 {A, B, C, D, E} 和边 {{A, D}, {A, E}, {B, C}, {B, D}, {C, D}} 的图。

简单图。图片由作者提供。

有时,图中可能包含回路。回路是一条起点和终点相同的边(一个节点与自身连接)。

图论中还需了解的其他术语:

  • 图的顺序等于其顶点数。

  • 图的大小是边的数量(有时加上顶点数量)。

  • 顶点的度数是它拥有的边的数量(一个回路在起点和终点处都算作两次)。

常见变体

之前的图示例也被称为简单图,因为它只包含顶点和(无向)边。但你可以通过提供额外的信息使它变得稍微复杂一些,通常更有趣。

下面是一个有向图(即 digraph)的示例,其中边指向特定方向:

有向图。图像由作者提供。

另一种适用于多种情况的图是加权图。在加权图中,边与一个权重相关联:这可以是任何东西,例如长度或成本。

加权图。图像由作者提供。

这里讨论的最后一种有趣的图形变体是多重图。在多重图中,同一条边可以出现多次:

多重图。图像由作者提供。

注意:有许多特殊类型的图形及其自身的属性。例如,一个每对顶点都用边连接的图称为完全图。

从问题到图

如何从问题创建图?让我们通过一些示例来探讨:

位置和距离

一个经典问题是有位置和位置之间距离的问题。在下面的邻接矩阵中,提供了位置之间的距离(如果距离大于 0,则有一条边)。从这个矩阵中,你可以轻松地创建一个图。

 A B C D E F G H I
A 0 3 1 2 0 0 0 0 0
B 3 0 1 0 4 3 0 0 0
C 1 1 0 0 0 3 0 0 0
D 2 0 0 0 0 1 0 6 0
E 0 4 0 0 0 0 2 0 0
F 0 3 3 1 0 0 3 1 0
G 0 0 0 0 2 3 0 0 1
H 0 0 0 6 0 1 0 0 5
I 0 0 0 0 0 0 1 5 0

从矩阵创建的图。图像由作者提供。

你也可以从图中创建矩阵,常见的有关联矩阵和度数矩阵。

让我们用边的名称替换权重:

现在我们可以创建关联矩阵,它有一行对应每个节点,一列对应每条边。它显示了哪些节点通过边连接:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14
A  1  1  1  0  0  0  0  0  0  0  0  0  0  0
B  1  0  0  1  1  1  0  0  0  0  0  0  0  0
C  0  1  0  1  0  0  1  0  0  0  0  0  0  0
D  0  0  1  0  0  0  0  1  1  0  0  0  0  0
E  0  0  0  0  1  0  0  0  0  1  0  0  0  0
F  0  0  0  0  0  1  1  1  0  0  1  1  0  0
G  0  0  0  0  0  0  0  0  0  1  1  0  0  1
H  0  0  0  0  0  0  0  0  1  0  0  1  1  0
I  0  0  0  0  0  0  0  0  0  0  0  0  1  1

对于加权图,我们可以用相应边的权重替换一。

度矩阵是对角矩阵,计算附加到每个节点的边的数量。一个回路算作两次。对于之前的图,度矩阵将是:

 A B C D E F G H I
A 3 0 0 0 0 0 0 0 0
B 0 4 0 0 0 0 0 0 0
C 0 0 3 0 0 0 0 0 0
D 0 0 0 3 0 0 0 0 0
E 0 0 0 0 2 0 0 0 0
F 0 0 0 0 0 5 0 0 0
G 0 0 0 0 0 0 3 0 0
H 0 0 0 0 0 0 0 3 0
I 0 0 0 0 0 0 0 0 2

注意:官方矩阵形式没有索引行和索引列。为了清晰起见,已添加。

流量

有向图常用于网络流动。例如在数据传输网络、交通运输或管道中。如果你知道两个节点之间的最大容量,你可以创建一个网络流图:

有源点(S)和汇点(T)的有向图。图像由作者提供。

源点(s)和汇点(t)是流的起点和终点。边是有向的,它们显示了节点之间流动的方向。

树图

一种特殊类型的图是树图。你可以在许多不同的地方找到它们:比赛日程、家谱、组织结构图和进化树。树是无向无环连通的。它不应包含循环或有向边,所有节点都与树相连。

三个树图。作者提供的图像。

树具有有趣的属性:

  • 它总是有n-1条边(其中n是节点的数量)。

  • 树中的任意一对节点之间都有一条唯一的路径。

  • 移除一条边将使图断开(将图分成两部分)。

社交网络

人与人之间的联系在图中可视化起来很美好。通过分析和可视化关系,你可以提取有价值的见解。这里是 7 个人的一个例子,如果他们在社交媒体上相互连接,他们就通过一条边连接:

人员网络。作者提供的图像。

通过查看此图表可能会出现的问题是:

  • 谁是最重要的人物?

  • 哪些人最相似?

  • 我们可以预测缺失的链接吗?

  • 我们如何以最佳方式对这些人进行聚类?

应用于图的常见算法

在将情况建模为图后,你可以解决手头的问题。对于上一段中的不同示例,我们将应用算法来寻找最短路径、计算最大流和最小割、确定最小生成树,并触及图分析中的话题。

最短路径

第一个问题是最短路径问题:我们应该如何从 A 到 B,同时保持路径尽可能短?

让我们考虑以下图表:

图。如何从 A 找到到 I 的最短路线?作者提供的图像。

一个常用于解决此问题的算法是 Dijkstra 算法。其工作方式如下:

步骤 1. 起始节点的距离为 0。将其余节点的距离设置为无穷大,这是它们的暂定距离。节点有三种类型:已访问未访问当前节点。在第一次运行中,当前节点是起始节点。

起始节点是黄色节点,距离为 0。所有其他距离设置为无穷大。作者提供的图像。

步骤 2. 查看当前节点的直接邻居,并计算到它们的距离(通过当前节点)。如果这个距离小于迄今为止的最小距离,则用新距离替换旧距离。为了发现最快的路线,如果距离被接受,则标记新路线(并在存在时删除之前标记的路线)。

查看当前节点的所有邻居,如果新距离更小,则用新距离替换暂定距离。将“接受的”路线(距离较短的路线)标记为绿色。作者提供的图像。

步骤 3。 现在可以将第一个节点标记为已访问(绿色),新的当前节点将是距离最短的未访问节点(下图中的黄色节点距离为 1)。对该节点继续执行步骤 2,并重复步骤 2 和 3,直到达到终点。

新的当前节点标记为黄色。以前的黄色节点标记为已访问。图片作者。

对第二个节点应用步骤 2。上面的节点得到新的距离 2,1+1(因为这比其暂定距离 3 更短)。我们可以在两个黄色节点中选择一个来再次应用步骤 2。图片作者。

如果我们重复这些步骤,最终解决后的图形如下所示:

使用 Dijkstra 算法寻找最短路径。图片作者。

现在我们找到了从起始节点到每个其他节点的最短距离,并附上了相应的路径(绿色边)!Dijkstra 算法被广泛应用并且受欢迎,因为它高效且避免了不必要的计算。

还有许多有趣的最短路径问题,例如中国邮差问题,在这个问题中,你需要在访问所有边的同时发现最短路径,或者旅行推销员问题,在这个问题中你需要访问所有节点。旅行推销员问题可以通过许多不同的方法来解决,例如使用混合整数规划(示例 2)或使用模拟退火(示例 1)。

最大流与最小割

在流图中,一个重要的问题是:从源点到汇点可以发送多少流量?边的权重对应于边的容量,这些容量永远不会被超过。每个节点的流入流量应该等于流出流量(源点和汇点除外)。

以下是一个最大流问题的示例:

带有源点(S)和汇点(T)的有向图。图片作者。

我们如何解决这个问题,并发现有多少流量可以到达汇点?对此有几种不同的算法。我们使用 Ford-Fulkerson 算法来解决这个问题。

步骤 1。 将所有边的初始容量设置为 0。

将所有边的初始容量设置为 0。图片作者。

步骤 2。 找到一个增广路径从源点到汇点。这是一条所有边的未使用容量大于 0 的路径,起点是源点,终点是汇点。你可以通过例如广度优先搜索或深度优先搜索来找到增广路径。

从源点到汇点的增广路径。图片作者。

步骤 3. 对于步骤 2 中的增广路径,我们可以找到瓶颈,即剩余容量最小的边。我们用这个值增加增广路径上所有边的容量。

容量增加了 3(瓶颈边的值)。图像由作者提供。

步骤 4. 继续执行步骤 2 和 3,直到没有增广路径。

另一个增广路径,更新后的容量为 5。5 是边缘中剩余容量最低的,因此容量更新为 5。图像由作者提供。

在继续这样做之后,最终不会再有增广路径。最终结果可能如下所示:

已解决!还存在其他可能的解决方案,可以得到相同的结果。图像由作者提供。

最大流量可以通过计算 11 + 1 + 4 = 16 很容易得到。这些是指向汇点的边的流量。这个问题也可以通过线性规划来解决。

与最大流相关的是最小割。最小割是找到将网络分成两个独立部分所需断开的连接的权重总和的最小值。它与最大流密切相关,因为当你找到了最大流的解决方案时,你也可以推导出最小割。它们是相等的。

最小割。将图分成两部分,同时尽量减少移除边的权重总和。这个图像中移除的边的权重就是最小割,这与最大流的值相同,在这个例子中为 16。图像由作者提供。

最小割用于图像分割,例如将对象与背景分离。最小割的另一个重要应用是分析复杂网络的可靠性,例如电网或通信网络,通过识别关键边缘,这些边缘的故障可能导致网络断开或性能下降。

最小生成树

想象一下你是一个铁路公司的老板,你想要找出如何用最少的铁路连接七个城市。在这种情况下,你要找的是最小生成树。如果我们可视化这个问题,它看起来像这样:

连接的城市。图像由作者提供。

我们如何计算这个图中的最小生成树?我们可以使用 Prim 算法,这是一种贪心算法,比较容易理解。

步骤 1. 随意选择图中的一个节点作为起点。这个节点是树的第一部分。

选择红色房子作为起点。

步骤 2. 通过一条边扩展树。选择与树连接的所有边中权重最小的边。

选择的边。来自中心城市的最小边。右上角的房子也在树中。图片来源于作者。

步骤 3. 重复步骤 2,直到所有节点都在树中。

接下来我们选择的边是权重为 3 的边:

树在生长中…… 图片来源于作者。

最终,我们找到了总长度为 17 的最小生成树。

最终的树用红色表示。图片来源于作者。

最小生成树有许多不同的应用。最直接的应用是网络设计,当一个网络必须以最低的权重完全连接时。这样的网络例子包括电信网络、计算机网络、电力分配系统和交通网络。你知道最小生成树也用于分类学、聚类分析、计算机视觉中的特征提取和手写识别吗?如果你深入研究最小生成树的应用,你会发现还有很多其他应用!

中心性与相似性

图分析利用图论中开发的工具和技术,以在不同领域(如社交网络、交通网络、计算机网络、生物学等)获得洞察力和做出明智的决策。

中心性和相似性是图分析中的两个核心概念。你可能听说过谷歌搜索使用的PageRank 算法。PageRank 是一种中心性算法,用于测量网络中节点的重要性或影响力,特别是在网页排名和超链接分析中。它根据每个节点的入链数量和质量分配一个数值评分,从而帮助识别网络中的关键节点。

介数中心性是另一种中心性测量方法。具有最高介数中心性的节点在所有其他节点之间充当最重要的桥梁。要找出一个节点的介数中心性,我们需要查看所有节点对,计算每个节点出现在两个其他节点之间的最短路径上的次数。出现在最多最短路径上的节点就是具有最高介数中心性的节点。

带有节点及每个节点的介数中心性的图。图片来源于作者。

在社交网络或现实世界系统中,具有高介数中心性的节点(例如,人员、地点或实体)在维持网络中不同部分之间的通信和连接方面发挥着至关重要的作用。它们充当重要的中介,确保高效的沟通并保持网络连接。

了解某些节点的相似性也很有用。这可以通过计算节点相似性来完成。Jaccard 相似度通常用于无权图。它通过计算两个节点共享的共同邻居数量与它们拥有的唯一邻居总数之比来衡量相似性。

让我们计算图中节点 2 和 4 的 Jaccard 相似度。它们共享 2 个邻居(节点 3 和 5),总的唯一邻居数量是 5(节点 1、2、3、4 和 5)。2 除以 5 得到的 Jaccard 相似度为 0.4。

节点相似性用于协同过滤技术,通过根据用户偏好的相似性向用户推荐项目。它还可以用于预测网络中的缺失或未来链接,或者用于检测簇和社区。

下一步是什么?

这只是图论和图分析世界的一瞥。在我们探索的内容之外,还有许多更多令人兴奋的问题和应用等待发现。图论的美在于其多样性;它提供了一种通过将现实世界挑战转化为可用巧妙算法解决的图形谜题的方式。

从社交网络到物流,许多现实世界的问题在图的领域中找到完美的匹配。揭示社区、发现隐藏的联系以及探索不同图类型的独特属性是图论中有趣的方面。

在数据科学领域,图成为盟友,使得创建可以对模型性能产生巨大影响的特征成为可能。图嵌入是解锁广泛数据集中新维度理解的关键。

希望这篇文章能激发你对图论奇妙世界的好奇心!

相关内容

## 五种数学优化与机器学习结合的方法

结合两种力量的实际例子。

## 约束编程解释

约束编程求解器的核心及其与混合整数编程的关系

## 强大优化技术介绍:模拟退火

解释、参数、优点、缺点和使用案例

关于模拟退火——一种强大的优化技术介绍

使用 C 优化 LLM,并在您的笔记本电脑上运行 GPT、Llama 和 Whisper

原文:towardsdatascience.com/optimizing-llms-with-c-and-running-gpt-lama-whisper-on-your-laptop-460c8bdd047e

在这篇文章中,我们将深入了解由 Georgi Gerganov 创建的出色张量库ggml。它是如何工作的?张量创建过程是什么?我们可以从一些简单的例子开始吗?

Stefano BosisioTowards Data Science Stefano Bosisio

·发表于 Towards Data Science ·15 分钟阅读·2023 年 9 月 23 日

--

图片来源:Aryo YarahmadiUnsplash

目录

  1. 实现一个简单的数学函数

    1.1 上下文的定义

    1.2 初始化张量

    1.3 前向计算和计算图

    1.4 编译和运行

  2. 对第一部分的最终备注

  3. 支持我的写作

大型语言模型(LLMs)正随处引起关注。报纸上充斥着大量的文字描述这个即将到来的新世界,保证“AI 终于来了”。尽管 LLMs 对我们的生活产生了切实的影响,但我们必须保持冷静,并对整个情况进行批判性分析。LLMs 的炒作让我想起了几年前“数据科学家”职位的炒作。2014 年,当我开始攻读博士学位时,我看到数据科学家职位的稳步增加,直到 2018 年左右达到顶峰。当时,新闻再次炒作,写道:“数据科学家:100 万美元的职业”或“21 世纪最性感的工作”——这些标题是否让你联想到 LLM 的标题?

一方面,LLM(大语言模型)是一项伟大的技术,是迈向更通用 AI 框架的一步。这些模型是深入 AI 的起点,我相信有一天大多数应用程序和技术将依赖于这些模型。然而,我常常在 Medium 上看到,对于这些模型,有时缺乏明确性。尽管这些模型的能力和惊人的成果无可置疑,但它们过于庞大,不易运行或训练。因此,公司在决定任何战略业务方向之前,需要对 LLM 有非常透彻的了解。最尖锐的问题之一是这些模型的巨大内存成本、大规模基础设施需求以及推理时所需的昂贵基础设施。

如果我们考虑基本的 LLM 结构,即转换器,我们可以识别出经典的编码器-解码器结构。在推理时,解码器需要有一个内存机制来确定给特定输入标记分配多少注意力分数。这个分数基于标记在句子中的可能位置以及它与剩余上下文的一致性。这种机制称为 KV 缓存。鉴于这个矩阵的大小,对于简单模型而言,2048 的上下文长度很容易就会占用 3TB 的内存。为了进一步加速计算,我们需要在 GPU 上运行。最后,整个解码器结构很难并行化。

鉴于此前言,我们是否可以找到一种折衷或权衡的解决方案,使我们能够在更简单的基础设施上运行这些计算?本文展示了 Georgi Gerganov 如何实现一个新的优化的基于 C 的张量库,称为ggml。我在文中提到的提交是提交0f4e99b,这是 2022 年 9 月的提交,标志着ggml冒险的开始。其逻辑是使用基础代码,以便让你对整个包有一个强有力的理解。

实现一个简单的数学函数

在跳到 LLM 之前(它将在第二篇文章中介绍),让我们尝试分解库的关键元素,以便计算一个非常简单的函数,如:f = ax²

上下文的定义

ggml中的一切都始于一个上下文。上下文定义了内存要求,以适应给定模型中的所有张量。上下文是从全局状态开始创建的:

// global state
struct ggml_state g_state;

struct ggml_state {
    struct ggml_context_container contexts[GGML_MAX_CONTEXTS];
};

全局状态构建为一个context_container,它是:

struct ggml_context_container {
    bool used;

    struct ggml_context context;
};

在容器中,我们可以注意到ggml第一个版本的核心元素,即ggml_context的存在:

struct ggml_context {
    size_t mem_size;
    void * mem_buffer;
    bool   mem_buffer_owned;

    int n_objects;

    struct ggml_object * objects_begin;
    struct ggml_object * objects_end;
};

ggml_context包含了有关我们可以使用多少内存以及内存缓冲区的所有信息,以便在我们不知道张量可能占用多少字节的情况下,我们可以拥有足够的内存。

然后,上下文用于初始化整个过程。ggml_init启动初始化过程并返回:

*ctx = (struct ggml_context) {
        .mem_size         = params.mem_size,
        .mem_buffer       = params.mem_buffer ? params.mem_buffer : malloc(params.mem_size),
        .mem_buffer_owned = params.mem_buffer ? false : true,
        .n_objects        = 0,
        .objects_begin    = NULL,
        .objects_end      = NULL,
    };

*ctx是一个新的上下文指针。我们可以使用GGML_PRINT在源代码中调查*ctx的输入对象,例如:

GGML_PRINT("%s: context %p with %zu bytes of memory\n", __func__, (void *) ctx, ctx->mem_size);
GGML_PRINT("%s: context %p with %d objects\n", __func__, (void *) ctx, ctx->n_objects);
GGML_PRINT("%s: context %p with %p object starting position\n", __func__, (void *) ctx, (void *) ctx->objects_begin);
GGML_PRINT("%s: context %p with %p object ending position\n", __func__, (void *) ctx, (void *) ctx->objects_end);

在我的 Apple MacBook M2 Pro 上,上下文已经初始化为 16 GB 内存、0 个对象,并且内存布局是objects_beginobjects_end的地址为 0x0。

objects_beginobjects_end确实是下一步,即在ggml_context中创建张量的内存地址。

初始化张量

对于ggml中的所有函数,总会找到一个协议实现,例如:

function_with_attributefunction_generalfunction_implementation

function_with_attribute是具有特定任务的函数,例如ggml_new_tensor_1dggml_new_tensor_2d,分别生成 1D 或 2D 张量。这个特定函数调用function_general,即实现的通用布局,例如ggml_new_tensor_Xd将调用ggml_new_tensor。最后,function_general调用实现function_implementation。这样,每次需要修改代码时,我们只需对实现进行操作,而不是修改所有特定函数。

要创建一个 1D 张量,我们可以使用ggml_new_tensor1d。从实现协议中,我们可以看到ggml_new_tensor_1d的代码如下:

struct ggml_tensor * ggml_new_tensor_impl(
        struct ggml_context * ctx,
        enum   ggml_type type,
        int    n_dims,
        const int* ne,
        void*  data) {
    // always insert objects at the end of the context's memory pool
    struct ggml_object * obj_cur = ctx->objects_end;

    const size_t cur_offset = obj_cur == NULL ? 0 : obj_cur->offset;
    const size_t cur_size   = obj_cur == NULL ? 0 : obj_cur->size;
    const size_t cur_end    = cur_offset + cur_size;

    size_t size_needed = 0;

    if (data == NULL) {
        size_needed += GGML_TYPE_SIZE[type];
        GGML_PRINT("Size needed %zu ", size_needed);
        for (int i = 0; i < n_dims; i++) {
            size_needed *= ne[i];
        }
        // align to GGML_MEM_ALIGN
        size_needed = ((size_needed + GGML_MEM_ALIGN - 1)/GGML_MEM_ALIGN)*GGML_MEM_ALIGN;

    }
    size_needed += sizeof(struct ggml_tensor);

    if (cur_end + size_needed + GGML_OBJECT_SIZE > ctx->mem_size) {
        GGML_PRINT("\n%s: not enough space in the context's memory pool\n", __func__);
        assert(false);
        return NULL;
    }

    char * const mem_buffer = ctx->mem_buffer;

    struct ggml_object * const obj_new = (struct ggml_object *)(mem_buffer + cur_end);

    *obj_new = (struct ggml_object) {
        .offset = cur_end + GGML_OBJECT_SIZE,
        .size   = size_needed,
        .next   = NULL,
    };

    if (obj_cur != NULL) {
        obj_cur->next = obj_new;
    } else {
        // this is the first object in this context
        ctx->objects_begin = obj_new;
    }

    ctx->objects_end = obj_new;

    struct ggml_tensor * const result = (struct ggml_tensor *)(mem_buffer + obj_new->offset);

    ggml_assert_aligned(result);

    *result = (struct ggml_tensor) {
        /*.type         =*/ type,
        /*.n_dims       =*/ n_dims,
        /*.ne           =*/ { 1, 1, 1, 1 },
        /*.nb           =*/ { 0, 0, 0, 0 },
        /*.op           =*/ GGML_OP_NONE,
        /*.is_param     =*/ false,
        /*.grad         =*/ NULL,
        /*.src0         =*/ NULL,
        /*.src1         =*/ NULL,
        /*.n_tasks      =*/ 0,
        /*.perf_runs    =*/ 0,
        /*.perf_cycles  =*/ 0,
        /*.perf_time_us =*/ 0,
        /*.data         =*/ data == NULL ? (void *)(result + 1) : data,
        /*.pad          =*/ { 0 },
    };

    ggml_assert_aligned(result->data);

    for (int i = 0; i < n_dims; i++) {
        result->ne[i] = ne[i];
    }

    result->nb[0] = GGML_TYPE_SIZE[type];
    for (int i = 1; i < GGML_MAX_DIMS; i++) {
        result->nb[i] = result->nb[i - 1]*result->ne[i - 1];
    }

    ctx->n_objects++;

    return result;
}

struct ggml_tensor * ggml_new_tensor(
        struct ggml_context * ctx,
        enum   ggml_type type,
        int    n_dims,
        const int* ne) {
    return ggml_new_tensor_impl(ctx, type, n_dims, ne, NULL);
}

struct ggml_tensor * ggml_new_tensor_1d(
        struct ggml_context * ctx,
        enum   ggml_type type,
        int    ne0) {
    return ggml_new_tensor(ctx, type, 1, &ne0);
}

如你所见,我们有ggml_new_tensor_1d调用ggml_new_tensor,然后调用实现ggml_new_tensor_impl。新张量的创建类似于列表的创建。正如 Georgi 所述,所有的新张量对象将放置在当前内存池的末尾,给定一个上下文,上下文的末尾将是对象指向的位置,其中ggml_object定义为:

struct ggml_object {
    size_t offset;
    size_t size;

    struct ggml_object * next;

    char padding[8];
};

起初,所有张量都初始化为data == NULL。核心是数据类型,在ggml中可以是:sizeof(int8_t), sizeof(int16_t), sizeof(int32_t)sizeof(float)。这些大小决定了在上下文中所需的内存量,因此每个张量都在内存段中得到了完美分配。

最后,创建了一个包含所有检索信息的对象:

*obj_new = (struct ggml_object) {
      .offset = cur_end + GGML_OBJECT_SIZE,
      .size   = size_needed,
      .next   = NULL,
  };

一旦计算出新张量的数据缓冲区struct ggml_tensor* const result = (struct ggml_tensor*)(memb_buffer + obj_new-> offset);,就会返回分配的张量:

*result = (struct ggml_tensor) {
        /*.type         =*/ type,
        /*.n_dims       =*/ n_dims,
        /*.ne           =*/ { 1, 1, 1, 1 },
        /*.nb           =*/ { 0, 0, 0, 0 },
        /*.op           =*/ GGML_OP_NONE,
        /*.is_param     =*/ false,
        /*.grad         =*/ NULL,
        /*.src0         =*/ NULL,
        /*.src1         =*/ NULL,
        /*.n_tasks      =*/ 0,
        /*.perf_runs    =*/ 0,
        /*.perf_cycles  =*/ 0,
        /*.perf_time_us =*/ 0,
        /*.data         =*/ data == NULL ? (void *)(result + 1) : data,
        /*.pad          =*/ { 0 },
    };

让我们看一个简单的示例,操作 1D 张量,通过定义一个数学函数f = ax²

#include "ggml/ggml.h"
#include "utils.h"

int main(int argc, char ** argv) {
    // define the memory parameters e.g. 16GB memory 
    struct ggml_init_params params = {.mem_size=16*1024*1024,
                                      .mem_buffer=NULL,
                                      };
    // create a computational context 
    struct ggml_context * ctx = ggml_init(params);
    // define the input tensors
    struct ggml_tensor *x = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 1);
    // x is a variable parameters in our context
    ggml_set_param(ctx, x);
    // define a constant a
    struct ggml_tensor *a = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 1);
    // return x²
    struct ggml_tensor *x2 = ggml_mul(ctx, x, x);
    // compute f = ax²
    struct ggml_tensor *f = ggml_mul(ctx, a, x2);

    return 0;
}

在定义输入张量之前,我们需要指定内存参数。在这种情况下,我们假设要使用 16 GB 的内存,并且这将成为上下文ggml_context * ctx的一部分。然后,我们可以开始定义第一个张量x,它将是参考变量(例如,我们想计算相对于x的梯度)。为了让ggml知道x是我们的主要变量,我们可以将其作为参数添加到上下文中ggml_set_param(ctx, x);

目前我们没有执行任何计算。我们只是指示 ggml 关于我们的函数(或模型)及张量如何相互作用。需要理解的是,每个张量都有一个特定的 .op 操作。所有新张量初始化时都为 GGML_OP_NONE。一旦我们对张量调用任何新操作,这一点会被修改。这会进入计算图,以便用户可以决定是否计算函数值或相对于输入变量的函数梯度(例如,在我们的案例中,我们可以要求计算相对于 x 的梯度)。

例如,ggml_mul 执行输入张量操作的变体。最初,tensor -> opGGML_NONE 转换为 GGML_OP_MUL

struct ggml_tensor * ggml_mul_impl(
        struct ggml_context * ctx,
        struct ggml_tensor * a,
        struct ggml_tensor * b,
        bool inplace) {
    assert(ggml_are_same_shape(a, b));

    bool is_node = false;

    if (!inplace && (a->grad || b->grad)) {
        is_node = true;
    }

    if (inplace) {
        assert(is_node == false);
    }

    struct ggml_tensor * result = inplace ? ggml_view_tensor(ctx, a) : ggml_dup_tensor(ctx, a);
    // Here we are transforming the operation 
    result->op   = GGML_OP_MUL;
    result->grad = is_node ? ggml_dup_tensor(ctx, result) : NULL;
    result->src0 = a;
    result->src1 = b;

    return result;
}

struct ggml_tensor * ggml_mul(
        struct ggml_context * ctx,
        struct ggml_tensor  * a,
        struct ggml_tensor  * b) {
    return ggml_mul_impl(ctx, a, b, false);
}

这些计算被封装在一个图计算结构中,该结构在推理时为我们处理的每个模型提供 forward 计算。

前向计算和计算图

目前我们只实现了函数 f = ax²。要执行实际操作,我们需要创建图计算。具体操作如下:

struct ggml_cgraph gf = ggml_build_forward(f);
// set initial params
ggml_set_f32(x, 2.0f);
ggml_set_f32(a, 3.0f);
// compute
ggml_graph_compute(ctx, &gf);
printf("k=%f\n", ggml_get_f32_1d(f,0));

ggml_build_forward 构建前向计算图。在前向步骤中,我们正在构建实际的计算图,这个图遍历所有节点并返回一个结构 ggml_cgraph

struct ggml_cgraph result = {
        /*.n_nodes      =*/ 0,
        /*.n_leafs      =*/ 0,
        /*.n_threads    =*/ 0,
        /*.work_size    =*/ 0,
        /*.work         =*/ NULL,
        /*.nodes        =*/ { NULL },
        /*.grads        =*/ { NULL },
        /*.leafs        =*/ { NULL },
        /*.perf_runs    =*/ 0,
        /*.perf_cycles  =*/ 0,
        /*.perf_time_us =*/ 0,
    };

对于上述示例,代码返回一个包含 3 个节点的图,分别是 xa*x² 以及 1 个叶子。可以通过 ggml_graph_dump_dot 函数获得图的可视化表示:

// without defining `gf` above run this:
struct ggml_cgraph gf = ggml_build_forward(f);
ggml_graph_dump_dot(&gf, &gf, "name_of_your_graph");

其中 &gf 是对图结构的引用,“name_of_your_graph” 指代 ggml 生成的 dot 文件的名称。如果你想将其转换为图像,只需运行:

dot -Tpng name_of_your_graph -o name_of_your_graph.png && open name_of_your_graph.png

对于我们的示例,图形为:

图 1:函数 f=ax² 的计算图。第一个节点是输入参数 x,然后 x 乘以自身,最后与变量 a 相乘。变量 a 是图中的一个叶子,其值为 3.00

如后面所述,我们可以给变量赋值(例如在此情况下 a = 3.0),我们可以看到图形具有以下内容:

  1. 一个初始的黄色节点,具有 GGML_OP_NONE 操作以定义 x

  2. 一个 GGML_OP_MUL 操作,即 x*x

  3. 一个粉色的叶子,指代另一个变量的值 (a)

  4. 最终节点,绿色,另一个 GGML_OP_MUL 操作为 a*x²

一旦所有张量被分配,我们将拥有一个最终的图,其中包含所有操作,从参数变量 x 开始。

计算图中的操作

ggml_compute_forward 是所有计算运行的地方。

此函数的输入参数是 struct ggml_compute_params * params, struct ggml_tensor * tensorparams 指定了图中与张量相关联的操作。通过 switch...case 循环调用任何前向操作:

switch (tensor->op) {
        case GGML_OP_DUP:
            {
                ggml_compute_forward_dup(params, tensor->src0, tensor);
            } break;
        case GGML_OP_ADD:
            {
                ggml_compute_forward_add(params, tensor->src0, tensor->src1, tensor);
            } break;
        case GGML_OP_SUB:
            {
                ggml_compute_forward_sub(params, tensor->src0, tensor->src1, tensor);
            } break;
        case GGML_OP_MUL:
            {
                ggml_compute_forward_mul(params, tensor->src0, tensor->src1, tensor);
            } break;
            ...
...

每个操作都是根据张量的输入类型编码的:

void ggml_compute_forward_mul(
        const struct ggml_compute_params * params,
        const struct ggml_tensor * src0,
        const struct ggml_tensor * src1,
        struct ggml_tensor * dst) {
    switch (src0->type) {
        case GGML_TYPE_F32:
            {
                ggml_compute_forward_mul_f32(params, src0, src1, dst);
            } break;
        case GGML_TYPE_I8:
        case GGML_TYPE_I16:
        case GGML_TYPE_I32:
        case GGML_TYPE_F16:
        case GGML_TYPE_COUNT:
            {
                assert(false);
            } break;
    }
}

对于0f4e99b提交,仅实现了GGML_TYPE_F32。这调用了主要的乘法实现。

 void ggml_compute_forward_mul_f32(
        const struct ggml_compute_params * params,
        const struct ggml_tensor * src0,
        const struct ggml_tensor * src1,
        struct ggml_tensor * dst) {
    assert(params->ith == 0);
    assert(ggml_are_same_shape(src0, src1) && ggml_are_same_shape(src0, dst));

    if (params->type == GGML_TASK_INIT || params->type == GGML_TASK_FINALIZE) {
        return;
    }

    const int n  = ggml_nrows(src0);
    const int nc = src0->ne[0];

    assert( dst->nb[0] == sizeof(float));
    assert(src0->nb[0] == sizeof(float));
    assert(src1->nb[0] == sizeof(float));

    for (int i = 0; i < n; i++) {
        float * x = (float *) ((char *) dst->data  + i*( dst->nb[1]));
        float * y = (float *) ((char *) src0->data  + i*( dst->nb[1]));
        float * z = (float *) ((char *) src1->data  + i*( dst->nb[1]));

        ggml_vec_mul_f32(nc,
                (float *) ((char *) dst->data  + i*( dst->nb[1])),
                (float *) ((char *) src0->data + i*(src0->nb[1])),
                (float *) ((char *) src1->data + i*(src1->nb[1])));
    }
}

操作的核心在于for循环。在这个循环中,我们处理结果张量x、乘法项src0和乘数src1。特别是:

  • (char *) dst->datadst的数据指针转换为char*。这样做是因为指针算术应以字节为单位进行,而char*是最灵活的类型。

  • i * (dst->nb[1])计算当前行的字节偏移量。由于i在每次迭代中递增,这实际上是根据步幅信息移动到内存中的下一行。

  • 最后,使用(float *)将结果强制转换回float*,以确保这些指针被解释为指向浮点值的指针。

在数值计算和张量操作的上下文中,步幅指的是沿张量的特定维度连续元素之间的步长,通常以字节为单位。理解和正确处理步幅对于高效的张量操作和内存管理至关重要。

操作ggml_vec_mul_f32执行最终的乘法如下:

inline static void ggml_vec_mul_f32 (const int n, float * z, const float * x, const float * y) { for (int i = 0; i < n; ++i) z[i]  = x[i]*y[i];   }

内联函数是 C 语言提供的一种机制(通过inline关键字),用于建议编译器在调用点“原地”展开特定函数,而不是作为单独的函数调用。当你调用一个常规函数时,会有一些开销。这包括将参数压入栈中、设置新的栈帧以及执行返回操作。对于非常小且频繁使用的函数,这些开销可能相对昂贵。内联消除了这些开销,因为代码直接插入调用点。内联允许编译器执行在正常调用情况下不可能进行的优化。例如,当函数被内联时,编译器可以在调用者的上下文中查看其代码并进行相应优化。这可能包括常量折叠、死代码消除和其他优化,从而使代码运行得更快。

最终的简单代码

我们现在准备在ggml中实现一个完整的代码,计算某些值的函数f = ax²。在examples文件夹下,我们可以创建一个名为simple_example的新文件夹。在那里,我们将有主文件main.cpp

#include "ggml/ggml.h"
#include "utils.h"

int main(int argc, char ** argv) {
    struct ggml_init_params params = {.mem_size=16*1024*1024,
                                      .mem_buffer=NULL,
                                      };
    // params set up
    struct ggml_context * ctx = ggml_init(params);
    // tensors
    struct ggml_tensor *x = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 1);
    // x as a parameter
    ggml_set_param(ctx, x);
    struct ggml_tensor *a = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 1);
    struct ggml_tensor *x2 = ggml_mul(ctx, x, x);
    struct ggml_tensor *f = ggml_mul(ctx, a, x2);

    // build the graph for the operations
    struct ggml_cgraph gf = ggml_build_forward(f);
    // set initial params
    ggml_set_f32(x, 2.0f);
    ggml_set_f32(a, 3.0f);
    // compute
    ggml_graph_compute(ctx, &gf);
    printf("k=%f\n", ggml_get_f32_1d(f,0));
    // print the graph
    ggml_graph_print(&gf);
    // save the graph
    ggml_graph_dump_dot(&gf, &gf, "final_graph");

    return 0;
}

在同一文件夹中,我们需要一个CMakeLists.txt文件,以便我们可以使用ggml库编译代码:

#
# simple_example

set(TEST_TARGET simple_example)
add_executable(${TEST_TARGET} main.cpp)
target_link_libraries(${TEST_TARGET} PRIVATE ggml ggml_utils)

最后,在文件examples/CMakeLists.txt的末尾添加以下行:add_subdirectory(simple_example)

现在,一切都是相互关联的,并且可以正确编译和运行。

编译和运行

回到ggml文件夹,按照README.md文件中的说明,创建一个名为build的文件夹并运行以下命令:

mkdir build
cd build
cmake ../
make simple_example

这将编译ggml库,并生成一个simple_example示例代码的二进制文件。我们只需输入./bin/simple_example即可运行我们的代码。代码将执行计算,并打印出图形信息的形式,包括所有节点和叶子及其相关操作。对于每个操作,将给出计算时间的估算。记住,如果你想绘制最终图形,你需要运行dot -Tpng final_graph -o final_graph.png && open final_graph.png

第一部分的最终说明

在这篇第一篇文章中,我们开始深入了解ggml的工作原理及其基本理念。特别是,我们深入探讨了:

  • ggml_context以及在ggml库中如何初始化和使用内存

  • 如何初始化一个新的 1D 张量以及ggml中的协议实现

  • 图形计算如何工作,检索图形计算并绘制出来

  • 一个简单的示例,初始化一个数学函数并获取其计算图

在下一篇文章中,我们将处理 LLM,特别是 GPT。我们将看到如何在ggml中实现和使用它们,最后,在我们的笔记本电脑上运行 GPT 模型。

支持我的写作

如果你喜欢我的文章,请通过以下链接支持我的写作,加入 Medium 的会员计划 😃

[## 通过我的推荐链接加入 Medium - Stefano Bosisio

作为 Medium 会员,你的一部分会员费用会用于你阅读的作者,同时你可以获得对每个故事的完全访问权限…

stefanobosisio1.medium.com](https://stefanobosisio1.medium.com/membership?source=post_page-----460c8bdd047e--------------------------------)

在 Apache Spark 中优化输出文件大小

原文:towardsdatascience.com/optimizing-output-file-size-in-apache-spark-5ce28784934c?source=collection_archive---------0-----------------------#2023-08-11

关于管理分区、重新分区和合并操作的全面指南

Gianpi ColonnaTowards Data Science Gianpi Colonna

·

关注 发表在 Towards Data Science ·6 min read·2023 年 8 月 11 日

--

照片由 zhao chen 提供,来源于 Unsplash

想象一下你在掌管一个大型 Spark 数据处理操作。在 Spark 优化讨论中,一个经常提到的经验法则是,为了获得最佳的 I/O 性能和增强的并行性,每个数据文件的大小应接近 128Mb,这也是读取文件时的默认分区大小 [1]

想象你的文件像在数据处理海洋中航行的船只。如果船只太小,它们会浪费很多时间停靠和重新起航,这比喻为执行引擎花费额外时间打开文件、列出目录、获取对象元数据、设置数据传输和读取文件。相反,如果你的船只太大且未使用港口的许多码头,它们不得不等待单一的长时间装卸过程,这比喻为查询处理等待直到一个读者完成读取整个文件,这会减少并行性[图 1]。

图 1 — 作者提供的图片

为了生动地说明文件大小优化的重要性,请参阅下图。在这个特定的例子中,每个表包含 8 GB 的数据。

然而,驾驭这种微妙的平衡并非易事,特别是在处理大型批处理作业时。你可能会觉得你失去了对输出文件数量的控制。本指南将帮助你重新获得控制权。

理解的关键:分区

写操作执行时,保存到磁盘的输出文件数量等于 Spark 执行器中的分区数量。 然而,在执行写操作之前评估分区数量可能很棘手。

在读取表时,Spark 默认读取最大大小为 128Mb 的块(尽管你可以使用sql.files.maxPartitionBytes来更改这一点)。因此,分区的数量依赖于输入的大小。然而,实际上,分区的数量很可能等于sql.shuffle.partitions参数。这个数字默认为 200,但对于较大的工作负载,这通常是不够的。查看这个视频以了解如何设置理想的洗牌分区数量。

如果在 ETL 过程中至少有一个宽变换,Spark 执行器中的分区数量等于sql.shuffle.partitions。如果仅应用窄变换,则分区数量将与读取文件时创建的分区数量相匹配。

设置洗牌分区的数量仅在处理未分区的表时可以高层次地控制总分区数量。一旦进入分区表的领域,改变sql.shuffle.partitions参数不会轻易地调整每个数据文件的大小。

转轮:重新分区与合并

我们有两种主要的方法来管理运行时的分区数量:repartition()coalesce()。以下是简要说明:

  • **重新分区**repartition(partitionCols, n_partitions) 是一种延迟转换,具有两个参数——分区数和分区列。当执行时,Spark 会根据分区列在集群中重新排列分区。然而,一旦表被保存,关于重新分区的信息会丢失。 因此,在读取文件时,这个有用的信息将不会被使用。
df = df.repartition("column_name", n_partitions)
  • **合并**coalesce(num_partitions) 也是一种延迟转换,但只需一个参数——分区数。重要的是,合并操作不会在集群中洗牌数据——因此比 **重新分区**更快。 此外,合并只能减少分区数,如果试图增加分区数则无效。
df = df.coalesce(num_partitions)

主要的见解是,使用合并方法通常更有利。 这并不是说重新分区没有用;它确实有用,特别是当我们需要在运行时调整数据框的分区数时。

在我处理多个大小不一的表,并进行复杂转换和连接的 ETL 过程中,我发现 sql.shuffle.partitions 并不能提供我所需的精确控制。例如,在相同的 ETL 中,为两个小表和两个大表使用相同数量的洗牌分区会很低效——导致小表的分区过多或大表的分区不足。重新分区还有助于我避开倾斜连接和倾斜数据的问题 [2]。

话虽如此,重新分区在将表写入磁盘之前较不适用,并且在大多数情况下,可以用合并替代。在写入磁盘之前,合并比重新分区更具优势,原因有几个:

  1. 它防止了不必要的数据在集群中的重新洗牌。

  2. 它允许根据逻辑启发式排序数据。在写入之前使用重新分区方法时,数据在集群中重新洗牌,导致顺序丢失。另一方面,使用合并可以保留数据的顺序,因为数据是被收集在一起而不是重新分配。

让我们看看为什么数据排序至关重要。

未来的排序:数据排序的重要性

我们上面提到,当我们应用 repartition 方法时,Spark 不会将分区信息保存到表的元数据中。然而,在处理大数据时,这是一条至关重要的信息,原因有二:

  1. 它允许在查询时更快地扫描表。

  2. 它允许更好的压缩——如果处理的是可压缩格式(如 parquet、CSV、Json 等)。这篇文章 是理解原因的好资料。

关键要点是在保存之前排序数据。这些信息将保存在元数据中,并在查询时使用,从而使查询速度更快。

现在让我们探讨一下保存到非分区表和分区表之间的区别,以及为什么保存到分区表需要额外的调整。

管理分区表中的文件大小

对于非分区表,在保存操作过程中管理文件数量是一个直接的过程。无论数据是否排序,使用 coalesce 方法在保存之前都能完成任务。

# Example of using coalesce method before saving a non-partitioned table
df.coalesce(10).write.format("parquet").save("/path/to/output")

然而,当处理分区表时,这种方法并不有效,除非在合并之前数据已经被排序。为了理解为什么会这样,我们需要深入探讨在 Spark 执行器中数据排序与未排序时发生的操作 [fig.2]。

图 2 — 作者提供的图片

因此,保存数据到分区表的标准流程应该是:

# Example of using coalesce method after ordering the data in a partitioned table
df.orderBy("columnName").coalesce(10).write.format("parquet").save("/path/to/output_partitioned")

其他导航工具

除了 repartitioncoalesce,你可能会发现 maxnumberofrecords 很有用。这是一个防止文件过大的实用方法,并可以与上述方法一起使用。

df.write.option("maxRecordsPerFile", 50000).save("file_path")

最终思考

在 Spark 作业中掌握文件大小通常涉及试错。在存储空间便宜且处理能力触手可及的时代,容易忽视优化。但随着数据处理量达到 tera 和 petabytes,忘记这些简单的优化技巧可能会在金钱、时间和环境方面产生重大成本。

我希望这篇文章能帮助你有效调整 ETL 过程。愿你像经验丰富的海船船长一样,自信且清晰地驾驭 Spark 的海洋。

通过选择性知识图谱条件优化检索增强生成(RAG)

原文:towardsdatascience.com/optimizing-retrieval-augmented-generation-rag-by-selective-knowledge-graph-conditioning-97a4cf96eb69

如何通过有针对性的增强来显著提高知识的相关性,同时保持语言流畅性

Anthony AlcarazTowards Data Science Anthony Alcaraz

·发表于Towards Data Science ·7 分钟阅读·2023 年 12 月 28 日

--

使用人工智能软件来增强本文文本的语法、流畅性和可读性。

生成预训练模型在作为对话代理使用时表现出令人印象深刻的流畅性和连贯性。然而,它们面临的一个关键限制是缺乏对外部知识的支撑。如果仅依赖预训练的参数,这些模型往往会生成看似合理但实际上不正确的回答,这也被称为幻觉。

以往缓解这一问题的方法涉及将整个知识图谱与对话中提到的实体相关联,从而增强对话上下文。然而,这种对大型知识图谱的不加选择的条件带来了自身的问题:

天真的知识图谱增强的局限性:

  • 很多 1-hop 上下文可能与对话无关,插入了不必要的噪音

  • 编码整个知识子图会给序列长度限制带来压力

  • 无法保证模型会使用相关事实进行生成

  • 尽管有知识基础,幻觉的风险仍然存在

为了克服这一点,Kang 等人 2023 年提出了 SUbgraph Retrieval-augmented GEneration(SURGE)框架,具有三项关键创新:

[## 知识一致的对话生成与语言模型和...

知识一致的对话生成与上下文相关的子图检索、不变图编码和...

openreview.net](https://openreview.net/forum?id=WhWlYzUTJfP&source=post_page-----97a4cf96eb69--------------------------------)

  1. 上下文相关子图检索器:使用图神经网络检索器从图知识库中检索与对话上下文最相关的知识图事实。

  2. 高效图编码:根据关系扰动标记嵌入,同时仅编码子图实体而非所有三元组。保持排列和反演不变性。

  3. 图-文本对比学习:通过对比损失确保检索到的知识图与生成响应之间的一致性。

这使得能够为对话提供准确的事实背景,而不会因无关事实或模型限制而稀释。实验表明,SURGE 减少了幻觉现象,并改善了基础知识。

关键见解在于对个性化子图的选择性条件提供了聚焦的知识基础,而不会使预训练模型不堪重负。

由 Dall-E-3 生成

计划:

- 上下文相关知识检索;

- 不变知识编码;

- 强化知识一致性;

- 结果;

- 结论。

上下文相关知识检索:

  • 使用上下文和三元组嵌入的相似性来建模检索分布

  • 从图神经网络中获得的三元组嵌入用于捕捉关系结构

  • 使得可以集中于最相关的事实,而不是所有知识图事实

SURGE 解决的关键挑战是仅从知识图中检索最相关的事实,而不是用所有上下文相关的实体淹没生成器。为了实现这种特定于上下文的选择,本文建议将检索建模为一个基于对话历史的知识图三元组分布。

从数学上讲,这个上下文条件的检索分布定义为:

pφ(z|x) ∝ exp(d(z)^T s(x))

其中:

  • x 是对话上下文

  • z 是一个知识图三元组

  • s(x) 为对话上下文生成稠密的嵌入

  • d(z) 为三元组生成稠密的嵌入

这里的关键见解是利用对话和三元组嵌入之间的相似性来建模相关性。

由于三元组包含作为图结构的实体和关系,普通语言模型编码器不够用。相反,图神经网络(GNNs)特别适合捕捉节点和边。GNNs 可以通过传播相邻嵌入来表示实体之间的关系依赖。

具体来说,节点嵌入是通过图卷积网络生成的:

e = GNN(e0; G)

而关系嵌入使用边超图网络:

r = GNN(r0; G∗)

其中 G* 表示对偶超图。

通过结合节点和边嵌入,完整的三元组嵌入可以嵌入语义关系和邻近性。这些三元组与编码器对话上下文向量的相似性提供了一个上下文相关的检索分布的基础。

不变知识编码:

  • 高效地将检索到的子图编码到生成器转换器中

  • 确保编码对关系的顺序和方向不变

  • 唯一编码实体并根据关系扰动嵌入

在前一阶段检索到的上下文相关子图需要被编码到生成器变换器模型中,该模型将生成对话回应。然而,简单地编码符号三元组会遇到表示稳定性的问题。

具体来说,有两个期望的不变性属性:

  1. 置换不变性:三元组的顺序不应改变整体意义

  2. 关系反转不变性:正向和反向关系等效

当将知识图谱编码到用于对话的预训练语言模型中时,会出现几个实际问题:

  1. 长序列:将每个三元组事实编码为单词会导致极长的输入序列。这会给模型的上下文容量带来压力。

  2. 顺序依赖:打乱三元组的顺序会改变像 GPT-3 这样的模型看到的意义,因为它们非常依赖单词的顺序和位置。但三元组本质上是无序的——打乱事实不应改变整体意义。

  3. 方向差异:关系可以被反转而不改变核心意义(X 是 Y 的妻子 == Y 有丈夫 X)。但前置文本使这些看起来像完全不同的事实。

上述问题在编码结构化知识时对语言模型造成了不必要的压力。模型被大量标记所压倒,它们难以理解混乱或反向的三元组仍然传达相同的概念。

因此,理想情况下,我们需要一种方法以紧凑而稳定的方式编码知识。编码应该是:

  • 高效的:不应导致成千上万的前置标记从而膨胀上下文空间。

  • 顺序不变:打乱子图不应大幅改变意义。

  • 方向不变:正向和反向关系应被等同对待。

SURGE 通过唯一编码实体,然后根据通过图神经网络检测到的关系明智地扰动它们的嵌入来解决这个问题。这为解码器提供了紧凑、稳定的形式。

引入了一种两步嵌入和扰动的方法:

独特的实体嵌入:

  • 从三元组中提取独特的实体集 ENT(Z)

  • 使用对话编码器嵌入这些实体

  • 这种嵌入+排序提供了置换不变性

使用关系进行扰动:

  • 对三元组使用图神经网络

  • GNN 提供关系感知的节点嵌入

  • 应用变换β到实体嵌入:

β(f(a), Z) = (1 + γ) ∗ f(a) + δ

其中 γ, δ 是基于关系学习到的扰动因子。

这一步使用关系信息直接影响实体向量空间,同时保持高效的唯一实体基础编码。

优势:

  • 向量空间编码符合生成器要求

  • 不变性提供了稳定性和一致性

见解是通过集合和扰动生成不变性,而不是通过可变序列编码。

强制知识一致性:

  • 知识图谱与生成回应之间的对比损失

  • 将相关知识表示拉近到回应表示

  • 改善回应在检索事实中的基础

即使在上下文相关的检索和高效编码之后,仍然无法保证生成器会实际利用提供的相关知识。幻觉的风险依然存在。

为了主动结合编码的子图,作者提议在图谱和回应表示之间添加一个跨模态对比损失:

Lcont = (1/2) * log (sim(ζ(z), ξ(h)) / ∑ξ(h’))

  • (1/2) * log (sim(ζ(z), ξ(h)) / ∑ζ(z’))

其中:

  • z 是编码的知识子图

  • h 是解码器的隐藏状态

  • ζ 和 ξ 是投影嵌入

直观上,这种损失将编码的知识图谱拉近到其对应的回应表示,同时将其推离其他随机回应或知识图谱。

这使得模型能够主动区分相关知识-回应对与无关的对。这种区分压力促使模型将其回应基于编码的事实。

好处:

  • 改善事实一致性

  • 减少不支持的断言

  • 允许追踪幻觉到检索错误

关键见解是,如果没有明确的对齐目标,两种模态的向量空间可能会分开,限制事实基础。对比损失作为一致性的归纳偏置。

端到端训练:

目标函数:整体训练目标是最大化生成正确回应的对数似然,求和于潜在知识子图:

L = Σp(Z|x) p(y|x,Z)

其中 p(Z|x) 是基于上下文的检索分布,p(y|x,Z) 是生成器分布。

训练过程:

  1. 使用编码器网络编码对话上下文 x

  2. 通过相似性搜索检索 top-k 子图 Z_i ~ p(Z|x)

  3. 使用 GNN + 扰动不变地编码 Z_i

  4. 通过解码器最大化每个样本的 p(y|x,Z_i)

  5. 另外,最小化 Z_i 和解码器状态之间的对比损失

因此,在对话批次中,通过共享参数优化检索分布和生成分布。

模型选择:

原则上,任何序列到序列的语言模型,如 T5、BART 或甚至 GPT-3,都可以作为生成模型,通过将编码的知识附加到输入上下文标记中。论文在实验中使用了 T5 模型,但可以进行替换。

好处:

  • 统一的端到端训练将组件绑定在一起

  • 边际似然汇总了整体视网膜性能

  • 模块化架构允许模型扩展

结果:

  • 在衡量知识相关性的指标中优于基线

  • 定性示例显示了更多基于相关知识的事实回应

  • 消融实验验证了每个组件的重要性

作者在 OpendialKG 和 KOMODIS 对话数据集上评估了 SURGE,这些数据集提供了配对的知识图谱。

定量改进:

  • SURGE 在知识相关性指标上超越了所有基准,如提出的 KQA(知识验证问答)指标,通过提取器测量事实正确性。

  • 在评估语言流利度的现有自动指标如 BLEU、ROUGE 和 F1 上取得了新的最先进结果。

定性影响:

  • 实例显示,SURGE 生成了更具信息性和基于相关知识的事实性回应,这些回应来自于选择性检索的子图。

  • 基准方法通常遗漏关键事实,甚至在有完整上下文的情况下仍会幻觉出不相关的陈述。

消融研究:

  • 去除对比学习等组件会显著降低知识一致性指标,显示出每个模块的必要性。

SURGE 通过针对性增强显著提高了知识相关性,同时保持了语言流利度。与知识无关和知识密集型基准相比的提升验证了选择性子图检索和基础的好处。

使用遗传算法在 Python 中优化电视节目调度

原文:towardsdatascience.com/optimizing-tv-programs-scheduling-using-genetic-algorithms-in-python-361fab402e75?source=collection_archive---------6-----------------------#2023-07-26

一个实践教程,讲解如何在 Python 中使用遗传算法优化电视节目调度

Eser SaygınTowards Data Science Eser Saygın

·

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

--

图片来源:Glenn Carstens-PetersUnsplash

我已经很久没有在 Medium 上写新帖子了。在过去的两年里,我一直在研究通过机器学习和深度学习对传统媒体行业可以进行哪些改进。其中一个研究领域是优化技术。就像在每个行业一样,优化在媒体中也至关重要。因此,在这篇文章中,我想通过将电视节目规划整合到一种进化算法——遗传算法中来分享。请记住,这只是一个简单的实现。现实生活远比这种简单性复杂得多。

什么是优化?我们为什么需要优化?

什么是优化?我想从这个问题开始。优化是寻找使给定目标函数最小化或最大化的值。那目标函数是什么?目标函数是我们试图最大化或最小化的绩效度量的数学表示。如果问题是最小化问题,我们可以称之为成本函数;如果是最大化问题,我们可以称之为适应度函数。

让我们通过一个例子来丰富这个解释。停下来想象一下你拥有一家餐馆。我们的目标是通过修改菜单(菜单指的是菜单上的菜肴)来最大化其利润。首先想到的方法是使用更便宜的原料。通过降低所用原料的质量,你可以获得更多的利润。但这并不是现实中的运作方式。当你降低产品质量时,顾客会减少对低质量原料制作的菜肴的需求。因此,无法实现预期的目标。

正如你所理解的,餐馆老板创建一个菜单以最大化利润是一个优化问题。例如,餐馆老板可以分析哪些菜肴在什么时间销售,并制定一份路线图。优化技术将使餐馆老板能够做出基于数据的决策,并取得最佳结果。

现在想象你作为一个电视台的节目规划师。记住你的竞争对手很强,但你仍然有能够竞争的节目。主要需要决定的问题是哪些节目应该在什么时间播出。这看起来很简单。笔和一些纸就足够了。真的那么简单吗?

尽管电视节目规划看起来简单,但随着各种因素的参与,它变得非常复杂。以下是其中一些因素:

观众偏好: 观众偏好什么类型的电视内容?

时间段: 观众在什么时间段偏好什么样的节目?

引导节目和过渡节目: 一些节目会将它们在播出期间收集到的观众转移到下一个节目。

竞争频道的节目偏好: 竞争频道在什么时间播出什么节目?

假期、特殊场合和季节性趋势: 观众的偏好是如何变化的?是否存在任何现有趋势?

新内容与旧内容: 广播节目是新的吗?还是重播?

故事情节和悬念: 这个程序有故事情节吗?还是有悬念?

这些只是一些因素。你可能已经注意到,数十个因素会影响电视节目规划。因此,优化算法来解决这些问题是合适的。

什么是评价和遗传算法?

我将在这一部分简要介绍评价和遗传算法。进化算法(EAs)是一种优化技术,可以解决许多具有挑战性的优化问题,而无需特定的关于问题结构的知识;换句话说,它们是问题独立的。进化算法(EAs)可以处理线性和非线性目标函数,而无需关于问题结构的信息。

另一方面,遗传算法属于搜索算法家族,使用进化原理。通过实现繁殖和自然选择过程,它可以产生高质量的解决方案。遗传算法是解决优化问题的非常有效的技术。

你可以看到下面的简单遗传算法流程图。我们的第一步是创建初始种群。初始种群包含随机选择的染色体(更准确地说,初始种群是一组染色体)。创建种群后,为每个个体计算一个适应度函数值。遗传算法使用染色体来表示每个个体。每个个体的适应度值是独立的。在这种方式下,可以同时进行多次计算。适应度值计算完毕后,遗传算法的三个不同阶段就会开始发挥作用——选择、交叉和变异。选择阶段负责从种群中选择染色体。目标是创造更好的世代。交叉过程负责从选择的个体中产生新的后代。这个过程通常是通过一次选择两个个体,然后交换它们的染色体部分,来创建两个新的代表后代的染色体。最后,操作员在变异阶段改变一个或多个基因。这种变化的概率非常低。变异阶段最重要的特点是防止系统陷入局部最小值。

遗传算法流程图(Eser Saygın)

实施

我刚刚介绍了关于遗传算法的一些简单信息。现在我将使用 Python 逐步解释遗传算法。正如标题所示,我们的问题是哪个节目将在什么时间播出。首先,我应该强调一个重要的点。我们将要实现的问题是一个简单的示例。正如我提到的,许多因素影响了现实生活中问题的实现。因此,问题识别阶段是最耗时的部分。

步骤

首先,我们从定义数据集开始。正如我之前提到的,下面的集合是一个简单的示例。数据集显示了各种节目在 18 小时(06:00–24:00)内的收视率。在现实生活中,有必要在每个时间段内播出节目,以测量节目在不同时间段的收视率。

# Sample rating programs dataset for each time slot.
ratings = {
    'news': [0.1, 0.1, 0.4, 0.3, 0.5, 0.4, 0.3, 0.2, 0.1, 0.2, 0.3, 0.5, 0.5, 0.4, 0.3, 0.2, 0.1, 0.2],
    'live_soccer': [0.0, 0.0, 0.0, 0.2, 0.1, 0.3, 0.2, 0.1, 0.4, 0.3, 0.4, 0.5, 0.4, 0.6, 0.4, 0.3, 0.4, 0.3],
    'movie_a': [0.1, 0.1, 0.2, 0.4, 0.3, 0.2, 0.1, 0.2, 0.3, 0.4, 0.5, 0.4, 0.3, 0.4, 0.3, 0.5, 0.3, 0.4],
    'movie_b': [0.2, 0.1, 0.1, 0.3, 0.2, 0.1, 0.2, 0.3, 0.4, 0.5, 0.4, 0.3, 0.4, 0.5, 0.4, 0.3, 0.4, 0.5],
    'reality_show': [0.3, 0.4, 0.3, 0.4, 0.4, 0.5, 0.3, 0.4, 0.5, 0.4, 0.3, 0.2, 0.1, 0.2, 0.3, 0.2, 0.2, 0.3],
    'tv_series_a': [0.2, 0.3, 0.2, 0.1, 0.1, 0.2, 0.2, 0.4, 0.4, 0.3, 0.3, 0.3, 0.5, 0.6, 0.4, 0.5, 0.4, 0.3],
    'tv_series_b': [0.1, 0.2, 0.3, 0.3, 0.2, 0.3, 0.3, 0.1, 0.4, 0.3, 0.4, 0.3, 0.5, 0.3, 0.4, 0.6, 0.4, 0.3],
    'music_program': [0.3, 0.3, 0.3, 0.2, 0.2, 0.1, 0.2, 0.4, 0.3, 0.3, 0.3, 0.3, 0.2, 0.3, 0.2, 0.3, 0.5, 0.3],
    'documentary': [0.3, 0.3, 0.4, 0.3, 0.2, 0.2, 0.3, 0.4, 0.4, 0.3, 0.2, 0.2, 0.2, 0.1, 0.1, 0.3, 0.3, 0.2],
    'Boxing': [0.4, 0.3, 0.3, 0.2, 0.2, 0.1, 0.1, 0.1, 0.1, 0.3, 0.3, 0.3, 0.2, 0.3, 0.4, 0.3, 0.4, 0.6]
}

下面,你可以看到其他变量。这些变量是用于遗传算法的超参数。我还创建了两个不同的列表以供后续使用。

GEN = 100
POP = 50
CO_R = 0.8
MUT_R = 0.2
EL_S = 2

all_programs = list(ratings.keys()) # all programs
all_time_slots = list(range(6, 24)) # time slots

正如我们在文章中提到的,我们的首要任务是初始化种群。你可以在下面找到我为此目的创建的函数。正如你所见,该函数需要两个输入列表:一个节目列表和一个时间段列表。我们已经在上面定义了这些列表。该函数生成所有潜在的日程安排。

def initialize_pop(programs, time_slots):
    if not programs:
        return [[]]

    all_schedules = []
    for i in range(len(programs)):
        for schedule in initialize_pop(programs[:i] + programs[i + 1:], time_slots):
            all_schedules.append([programs[i]] + schedule)

    return all_schedules

接下来,我们将定义我们的适应度函数。适应度函数负责测量每个日程安排的质量。它以日程安排为输入,并返回总的收视率分数。(我们称之为日程安排的列表是由电视节目组成的播出日程。)

def fitness_function(schedule):
    total_rating = 0
    for time_slot, program in enumerate(schedule):
        total_rating += ratings[program][time_slot]
    return total_rating

在定义了适应度函数后,我们可以进入选择阶段。选择阶段的目的是找到最优的日程安排。为此,我们可以使用我们创建的以下函数。该函数检查每个日程安排的适应度值,并选择具有最高值的那个。

def finding_best_schedule(all_schedules):
    best_schedule = []
    max_ratings = 0

    for schedule in all_schedules:
        total_ratings = fitness_function(schedule)
        if total_ratings > max_ratings:
            max_ratings = total_ratings
            best_schedule = schedule

    return best_schedule

选择阶段之后是交叉阶段。在交叉阶段,两个父代解在遗传算法的帮助下被结合形成新的后代。在电视节目安排问题中,这个过程改变了两个解决方案中找到的节目(基因)。这个过程创造了各种电视节目的组合。你可以在下面看到交叉函数。

def crossover(schedule1, schedule2):
    crossover_point = random.randint(1, len(schedule1) - 2)
    child1 = schedule1[:crossover_point] + schedule2[crossover_point:]
    child2 = schedule2[:crossover_point] + schedule1[crossover_point:]
    return child1, child2

最终阶段是突变阶段。正如我们之前提到的,在突变阶段,通过改变染色体的遗传物质来形成新的后代。在电视节目优化问题中,我们可以将其视为随机更改节目。记住,突变的概率非常低。此外,你还可以将这个可能性作为一个超参数进行分配。

def mutate(schedule):
    mutation_point = random.randint(0, len(schedule) - 1)
    new_program = random.choice(all_programs)
    schedule[mutation_point] = new_program
    return schedule

现在我们已经定义了所有函数,可以运行适应度函数了。

# calling the fitness func.
def evaluate_fitness(schedule):
    return fitness_function(schedule)

我们需要的数据已经准备好。现在我们可以定义算法。这个算法将使用 initial_schedule、generations、population_size、crossover_rate、mutation_rate 和 elitism_size。我们之前已经描述过这些。由于它们是超参数,我们可以修改它们,但不需要。函数首先创建具有提供的初始计划的初始种群,然后添加随机计划。之后,它会对指定的代数运行一个循环,并为每一代生成一个新种群,使用选择、交叉和突变操作。精英策略有助于根据适应度评分保留上一代中最成功的个体。一旦种群更新,它就成为下一代的当前种群。之后,函数返回上一代的最佳计划。

def genetic_algorithm(initial_schedule, generations=GEN, population_size=POP, crossover_rate=CO_R, mutation_rate=MUT_R, elitism_size=EL_S):

    population = [initial_schedule]

    for _ in range(population_size - 1):
        random_schedule = initial_schedule.copy()
        random.shuffle(random_schedule)
        population.append(random_schedule)

    for generation in range(generations):
        new_population = []

        # Elitism
        population.sort(key=lambda schedule: fitness_function(schedule), reverse=True)
        new_population.extend(population[:elitism_size])

        while len(new_population) < population_size:
            parent1, parent2 = random.choices(population, k=2)
            if random.random() < crossover_rate:
                child1, child2 = crossover(parent1, parent2)
            else:
                child1, child2 = parent1.copy(), parent2.copy()

            if random.random() < mutation_rate:
                child1 = mutate(child1)
            if random.random() < mutation_rate:
                child2 = mutate(child2)

            new_population.extend([child1, child2])

        population = new_population

    return population[0]

现在我们准备好获取结果。

initial_best_schedule = finding_best_schedule(all_possible_schedules)

rem_t_slots = len(all_time_slots) - len(initial_best_schedule)
genetic_schedule = genetic_algorithm(initial_best_schedule, generations=GEN, population_size=POP, elitism_size=EL_S)

final_schedule = initial_best_schedule + genetic_schedule[:rem_t_slots]

print("\nFinal Optimal Schedule:")
for time_slot, program in enumerate(final_schedule):
    print(f"Time Slot {all_time_slots[time_slot]:02d}:00 - Program {program}")

print("Total Ratings:", fitness_function(final_schedule))

在遗传算法运行后,我们将初始最佳计划和遗传计划结合起来,创建最终的最优计划。最后,我们打印出分配程序的最优计划,显示时间段、相应的程序以及在最终最优计划中获得的总评分。

结论

节目规划对传统媒体行业的电视台至关重要,在竞争激烈的环境中尤其如此。在这种情况下,我们展示了如何利用遗传算法来改进电视节目排期,这是一个强大的工具,可以帮助最大化观众评分。考虑使用遗传算法来优化排程问题,比如电视节目排期。凭借其强大的能力,它可以帮助你创建一个最大化观众参与和评分的计划。

在我即将发布的文章中,我计划探讨各种遗传算法,如竞争性协同进化(CCQGA)和量子算法(QGA)。我可能还会在中间加入额外的内容。

感谢你抽出时间阅读这篇文章。如果你想与我联系,欢迎通过 LinkedIn 添加我。

www.linkedin.com/in/esersaygin/

来源

《动手实践遗传算法与 Python:应用遗传算法解决现实世界的深度学习和人工智能问题》 作者:埃亚尔·维尔桑斯基

《使用 Python 的工程师应用进化算法(第 1 版)》

作者:莱昂纳多·阿泽维多·斯卡尔杜亚

完整代码

import random

##################################### DEFINING PARAMETERS AND DATASET ################################################################
# Sample rating programs dataset for each time slot.
ratings = {
    'news': [0.1, 0.1, 0.4, 0.3, 0.5, 0.4, 0.3, 0.2, 0.1, 0.2, 0.3, 0.5, 0.5, 0.4, 0.3, 0.2, 0.1, 0.2],
    'live_soccer': [0.0, 0.0, 0.0, 0.2, 0.1, 0.3, 0.2, 0.1, 0.4, 0.3, 0.4, 0.5, 0.4, 0.6, 0.4, 0.3, 0.4, 0.3],
    'movie_a': [0.1, 0.1, 0.2, 0.4, 0.3, 0.2, 0.1, 0.2, 0.3, 0.4, 0.5, 0.4, 0.3, 0.4, 0.3, 0.5, 0.3, 0.4],
    'movie_b': [0.2, 0.1, 0.1, 0.3, 0.2, 0.1, 0.2, 0.3, 0.4, 0.5, 0.4, 0.3, 0.4, 0.5, 0.4, 0.3, 0.4, 0.5],
    'reality_show': [0.3, 0.4, 0.3, 0.4, 0.4, 0.5, 0.3, 0.4, 0.5, 0.4, 0.3, 0.2, 0.1, 0.2, 0.3, 0.2, 0.2, 0.3],
    'tv_series_a': [0.2, 0.3, 0.2, 0.1, 0.1, 0.2, 0.2, 0.4, 0.4, 0.3, 0.3, 0.3, 0.5, 0.6, 0.4, 0.5, 0.4, 0.3],
    'tv_series_b': [0.1, 0.2, 0.3, 0.3, 0.2, 0.3, 0.3, 0.1, 0.4, 0.3, 0.4, 0.3, 0.5, 0.3, 0.4, 0.6, 0.4, 0.3],
    'music_program': [0.3, 0.3, 0.3, 0.2, 0.2, 0.1, 0.2, 0.4, 0.3, 0.3, 0.3, 0.3, 0.2, 0.3, 0.2, 0.3, 0.5, 0.3],
    'documentary': [0.3, 0.3, 0.4, 0.3, 0.2, 0.2, 0.3, 0.4, 0.4, 0.3, 0.2, 0.2, 0.2, 0.1, 0.1, 0.3, 0.3, 0.2],
    'Boxing': [0.4, 0.3, 0.3, 0.2, 0.2, 0.1, 0.1, 0.1, 0.1, 0.3, 0.3, 0.3, 0.2, 0.3, 0.4, 0.3, 0.4, 0.6]
}

GEN = 100
POP = 50
CO_R = 0.8
MUT_R = 0.2
EL_S = 2

all_programs = list(ratings.keys()) # all programs
all_time_slots = list(range(6, 24)) # time slots

######################################### DEFINING FUNCTIONS ########################################################################
# defining fitness function
def fitness_function(schedule):
    total_rating = 0
    for time_slot, program in enumerate(schedule):
        total_rating += ratings[program][time_slot]
    return total_rating

# initializing the population
def initialize_pop(programs, time_slots):
    if not programs:
        return [[]]

    all_schedules = []
    for i in range(len(programs)):
        for schedule in initialize_pop(programs[:i] + programs[i + 1:], time_slots):
            all_schedules.append([programs[i]] + schedule)

    return all_schedules

# selection
def finding_best_schedule(all_schedules):
    best_schedule = []
    max_ratings = 0

    for schedule in all_schedules:
        total_ratings = fitness_function(schedule)
        if total_ratings > max_ratings:
            max_ratings = total_ratings
            best_schedule = schedule

    return best_schedule

# calling the pop func.
all_possible_schedules = initialize_pop(all_programs, all_time_slots)

# callin the schedule func.
best_schedule = finding_best_schedule(all_possible_schedules)

############################################# GENETIC ALGORITHM #############################################################################

# Crossover 
def crossover(schedule1, schedule2):
    crossover_point = random.randint(1, len(schedule1) - 2)
    child1 = schedule1[:crossover_point] + schedule2[crossover_point:]
    child2 = schedule2[:crossover_point] + schedule1[crossover_point:]
    return child1, child2

# mutating
def mutate(schedule):
    mutation_point = random.randint(0, len(schedule) - 1)
    new_program = random.choice(all_programs)
    schedule[mutation_point] = new_program
    return schedule

# calling the fitness func.
def evaluate_fitness(schedule):
    return fitness_function(schedule)

# genetic algorithms with parameters

def genetic_algorithm(initial_schedule, generations=GEN, population_size=POP, crossover_rate=CO_R, mutation_rate=MUT_R, elitism_size=EL_S):

    population = [initial_schedule]

    for _ in range(population_size - 1):
        random_schedule = initial_schedule.copy()
        random.shuffle(random_schedule)
        population.append(random_schedule)

    for generation in range(generations):
        new_population = []

        # Elitsm
        population.sort(key=lambda schedule: fitness_function(schedule), reverse=True)
        new_population.extend(population[:elitism_size])

        while len(new_population) < population_size:
            parent1, parent2 = random.choices(population, k=2)
            if random.random() < crossover_rate:
                child1, child2 = crossover(parent1, parent2)
            else:
                child1, child2 = parent1.copy(), parent2.copy()

            if random.random() < mutation_rate:
                child1 = mutate(child1)
            if random.random() < mutation_rate:
                child2 = mutate(child2)

            new_population.extend([child1, child2])

        population = new_population

    return population[0]

##################################################### RESULTS ###################################################################################

# brute force
initial_best_schedule = finding_best_schedule(all_possible_schedules)

rem_t_slots = len(all_time_slots) - len(initial_best_schedule)
genetic_schedule = genetic_algorithm(initial_best_schedule, generations=GEN, population_size=POP, elitism_size=EL_S)

final_schedule = initial_best_schedule + genetic_schedule[:rem_t_slots]

print("\nFinal Optimal Schedule:")
for time_slot, program in enumerate(final_schedule):
    print(f"Time Slot {all_time_slots[time_slot]:02d}:00 - Program {program}")

print("Total Ratings:", fitness_function(final_schedule))

优化度假小屋租赁收入

原文:towardsdatascience.com/optimizing-vacation-cabin-rental-revenues-730d2847a2ed?source=collection_archive---------13-----------------------#2023-03-08

简要了解收益管理科学及其 Python 演示

Chris GarciaTowards Data Science Chris Garcia

·

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

--

图片由 Evelyn Paris 提供,来源于 Unsplash

如果你曾经购买过机票,你一定意识到时间对价格的影响。一个提前六个月购票的休闲旅客可能只需支付$200,而一个前一天才买票的商务旅客则需支付$3,300。类似的情况也会出现在我们预订酒店房间、租车或安排高尔夫球场的开球时间时。如果你曾经想知道背后的原理以及它是如何运作的,答案就是收益管理。

收入管理是一个数据驱动的企业,将客户细分、预测分析与机器学习、实验设计和数学优化以协调的方式结合起来,以最大化利用现有容量获得的收入。通过理解收入管理的原则,数据科学家可以运用他们的技能帮助公司和客户实现收入的显著增长。

收入管理的科学

收入管理(RM)是预测微观市场层面上实时客户需求的科学,并优化产品或服务的价格和可用性[1]。RM 由美国航空公司在 1980 年代首次提出,回应了新兴低票价竞争对手 People Express 的威胁。美国航空的 RM 系统在休闲和商务乘客之间区分了价格。它还使用优化算法来决定多少座位开放给早期预订的低票价乘客,多少座位保护给后期预订的高票价商务乘客。这种方法非常成功,恢复了美国航空的盈利能力,同时最终导致了 People Express 的关闭。因此,许多其他行业也开始采用 RM。

RM 的目标是通过正确的渠道在正确的时间以正确的价格向正确的客户提供合适的产品或服务。传统上,RM 是为在少数条件下运营的企业开发的:

  • 固定容量: 例如,航空公司有固定的机队规模,酒店有固定数量的房间。

  • 易腐产品或服务: 例如,某一日期的未售座位或酒店房间无法在以后销售。

  • 客户提前预订资源(例如,预订航班或酒店房间)

如今,RM 技术正在超越这些假设,适用于电子商务等新兴行业。然而,总体而言,RM 涉及四个相互关联的活动:

关键的收入管理活动

1. 市场细分

首先的活动是根据使用模式或需求识别不同的客户群体。例如,航空公司将客户分为早期预订的休闲乘客和后期预订的商务乘客,而租车公司则根据客户是否需要紧凑型、中型或豪华车辆进行划分。

2. 确定价格响应

在进行细分后,我们必须确定价格响应,并根据情况选择最佳价格。如果我们将某个细分市场的价格定得过低,可能会轻松销售,但也会错失收入。相反,如果价格定得过高,我们可能会因顾客拒绝我们的报价而损失收入。因此,我们必须确定每个细分市场愿意支付的最佳价格,以及价格上涨对需求的影响。这通常通过定价实验来完成,也可以结合竞争对手和行业数据。实验应在真实市场条件下进行,并定期重复,因为通货膨胀、工资增长、假期和新竞争者等因素都可能影响价格响应。

3. 预测需求

为了开始应用收益管理,我们需要指定一个时间范围,然后预测该时间范围内每个细分市场在每个使用时段的需求。由于收益管理在微观经济层面运作,时间范围通常是短期的,并且持续时间较短。一个使用时段可能是航空行业的航班段,或者是酒店行业的周五至周日住宿。因此,例如,我们预测每个航班段的休闲和商务乘客的需求,或者每个可预订时间间隔的小房间和大房间的需求。

4. 优化容量分配

一旦我们估算了每个细分市场的需求,就需要确定如何分配我们的容量,以在整个时间范围内最大化收入。这告诉我们,例如,应该卖出多少座位给休闲乘客,留出多少座位给商务乘客,或者在每个时间段内要使用多少辆车来满足紧凑需求,多少辆车来满足豪华需求。这手动进行可能会非常困难,因为使用时段可能会重叠,导致相同资源无法同时使用。当有利时,还可以进行替代(例如,将大房间提供给请求小房间的客人),进一步增加了复杂性。因此,容量分配通常通过整数规划来完成。

这四项活动应该被视为关键要素,而不是过程中的严格步骤。一些收益管理系统主要关注容量分配,在销售期间之前固定价格,而其他系统则根据不断变化的市场条件频繁更新价格。这些活动需要根据每个应用的具体情况进行协调,以实现最佳的投资回报。

示例场景:优化度假小屋租赁收入

我们将展示一个 RM 活动,即容量分配,并展示它对收入的影响。这个示例可以在 Jupyter notebook 这里 查看。这个示例的源代码和数据文件可以在 这里** 找到**。

一家度假小屋租赁公司拥有以下资源:

  • 3 个小型舱位

  • 5 个中型舱位

  • 3 个大型舱位

我们将假设我们已经确定了即将到来的长周末的最佳价格和准确的需求预测,如下所示:

目标是确定每种舱位类型分配到不同需求时间段的数量,以最大化收入。我们从导入关键库和读取数据开始:

建模问题

我们将制定并解决一个整数规划(IP)模型,将容量分配到需求预测中,以最大化获得的收入。这个模型将舱位分配到需求时间段,形式为(时间区间,舱位大小)对,并使用二进制指示器来指定允许的替代。我们从定义模型输入开始:

接下来,我们定义我们的决策变量:

优化模型现在可以表述如下:

在上述模型中,目标函数 (1) 只是所有收入在期间内的总和。约束条件 (2) 确保我们不会对任何舱位类型进行超额预订。我们通过确保对于任何舱位类型 i 和时间段 j,分配到时间段 j 或任何与 j 起始时间重叠的时间段的 i 类型舱位的总数不会超过可用的 i 类型舱位数量来实现这一点。约束条件 (3) 确保我们不会为任何时间段分配超过需求的舱位。约束条件 (4) 确保我们不会通过将舱位分配到无法满足的时间段而做出无效替换。最后,约束条件 (5) 确保决策变量的值为非负整数。

下面我们定义一个 Python 函数,从输入数据构建这个整数规划(IP)模型的实例:

获得最佳容量分配计划

现在我们将创建一个优化函数来解决模型,该函数将确定我们的最佳销售计划和获得的收入。为此,我们首先需要创建一些辅助函数,将原始数据转换为模型所需的格式。

现在我们可以优化并查看根据销售计划得到的收入。在这个示例中,我们将允许较大的舱位替代较小的舱位。

运行上述代码将得到如下结果:

有趣的是,当我们查看这个最优分配计划时,我们注意到经常发生将较大的舱位替换为较小舱位的情况。正如我们将看到的,允许这些类型的替换对收入有很大的影响。

以人为方式解决问题:先到先得

将最优政策与一个人可能在没有 RM 的情况下实施的政策进行比较是很有启发性的。在这种情况下,代理将根据请求的到达顺序(先到先得)简单地出售舱位预订。在这种情况下使用替换也没有多大意义,因为我们永远不知道下一个请求是否会是带来更多收入的较大舱位。

FCFS 产生的收入对请求的顺序非常敏感;这些请求可能以一种恰好把我们的舱位绑在非常不理想的方式的顺序到达,或者我们可能会运气好,得到一个接近最大收入的顺序。下面的代码实现了 FCFS 政策,并附带一个预处理函数,将原始数据转换为算法所需的形式。在预处理函数中,我们随机排列预订请求,以模拟它们在现实世界中的到达方式。

比较政策

下面我们将比较两种优化变体(一个不允许替换,另一个允许较大的舱位替换较小的舱位)与 FCFS 的收入影响。我们将运行 FCFS 100 次,每次随机排列预订请求,并将最差、中位(即典型)和最佳情况与两种优化结果进行比较。

运行这个会产生以下结果:

比较政策时,我们看到 FCFS 通常会留下未利用的收入——如果运气不好,这可能是一个相当大的数额。只有在最好的情况下,FCFS 才能达到在不允许替换的情况下的最大收入(这总是由优化 B 政策实现的)。然而,通过允许替换和优化,我们可以大幅提升收入——在最坏情况下增加超过 22%(或周末超过 4,000 美元)。此外,这额外的收入是利用现有容量赚取的,没有产生额外的成本,因此完全是利润。这一比较展示了 RM 的影响。

最后需要指出的是,有效的容量分配依赖于准确的需求预测,以作为优化输入。有些周我们的预测可能会失误。然而,假设预测通常可以 reasonably 精确,使用 RM 的系统将整体上优于不使用的系统。

结论

收入管理是一门能够显著提高现有容量收入的科学。通过将 RM 的元素纳入销售过程,公司可以开始在不产生额外运营成本的情况下增加收入。

除非另有说明,所有数据和图片均由作者提供。

参考文献

[1] Cross, R.B. (1997). 收入管理:市场主导的硬核策略. 纽约:百老汇书籍,第 276 页。

优化向量量化方法的机器学习算法

原文:towardsdatascience.com/optimizing-vector-quantization-methods-by-machine-learning-algorithms-77c436d0749d?source=collection_archive---------4-----------------------#2023-05-17

本文简要解释了我们在 ICASSP 2023 会议上发表的论文 [1]。有关更多详细信息,请查看此链接中的论文。

Mohammad Hassan ValiTowards Data Science Mohammad Hassan Vali

机器学习优化向量量化方法,用于神经网络的端到端训练

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

--

·

照片由 Alina Grubnyak 提供,发布于 Unsplash

向量量化

向量量化(VQ)是一种数据压缩技术,类似于 k-means 算法,可以对任何数据分布进行建模。向量量化已被广泛应用于语音、图像和视频数据,如图像生成 [2]、语音和音频编码 [3]、语音转换 [4,5]、音乐生成 [6] 和文本到语音合成 [7,8]。下图展示了向量量化(VQ)的工作原理。对于 VQ 过程,我们需要一个包括多个代码字的码本。对数据点(灰色点)应用 VQ 意味着将其映射到最近的代码字(蓝色点),即用最近代码字的值替换数据点的值。每个 Voronoi 单元(黑线)包含一个代码字,使得该单元中所有的数据点都将映射到该代码字,因为它是该 Voronoi 单元中数据点的最近代码字。

向量量化操作(作者提供的图像)

换句话说,向量量化通过以下公式将输入向量 x 映射到码本(CB)中的最近代码字:

VQ 的计算复杂度随着码本大小(VQ 比特率的增加)的增加而呈指数增长。因此,这种简单形式的 VQ 仅适用于有限的比特率(有限的码本大小)。为了解决这一挑战并将 VQ 应用于更高的比特率和更高维的数据,我们使用了 VQ 的一些变体,如残差 VQ、加法 VQ 和乘积 VQ。这些方法考虑了多个码本来对数据应用 VQ。我们将在下文中解释这三种 VQ 方法。

残差向量量化(RVQ)

残差 VQ 通过对输入向量 x 应用 M 个连续的 VQ 模块来进行量化。根据下图,假设 M=3。我们使用第一个码本(CB¹)对输入向量 x 应用第一个 VQ 模块。然后,在找到第一个码本中的最近代码字后,我们计算剩余值(R1)。接着,我们将 R1 作为输入传递给使用第二个码本(CB²)的下一个 VQ 模块。这个过程将持续 M 个阶段,其中我们会找到来自不同码本的三个最近代码字。最后,我们将输入向量 x 量化为 M 个最近代码字的总和。

加法向量量化(AVQ)

类似于残差 VQ,加法 VQ 通过应用 M 个连续的 VQ 模块来量化输入向量 x。然而,加法 VQ 采用复杂的束搜索算法来找到量化过程中的最接近代码字(你可以在这篇论文 [9] 中找到束搜索算法的详细信息)。根据下图,我们假设 M=3。在加法 VQ 中,我们首先从所有三个代码本的联合中搜索最接近的代码字(这里是 CB¹、CB²、CB³)。然后,假设我们从 CB² 找到最佳代码字。接下来,我们计算残差 (R1) 并将其作为输入传递给下一个 VQ 模块。由于第一个代码字是从 CB² 中选择的,现在我们在 CB¹ 和 CB³ 的联合中搜索最接近的代码字。在计算残差 R2 后,我们将其作为输入传递给最后的 VQ 模块,在这里我们使用尚未参与量化过程的最后一个代码本(在此例中是 CB¹)进行搜索。最后,我们将输入向量 x 量化为 M 个最接近代码字的和。

产品向量量化(PVQ)

产品 VQ 将维度为 D 的输入向量 x 拆分为 M 个独立的维度为 D/M 的子空间。然后,它对这些子空间应用 M 个独立的 VQ 模块。最后,产品 VQ 将输入向量 x 量化为 M 个最接近的代码字的连接(每个代码本一个)。下图展示了 M=3 时的产品 VQ。

代码本优化

向量量化(VQ)训练意味着优化代码本,使其以一种模型数据分布的方式来最小化数据点与代码本元素之间的量化误差(如均方误差)。为了优化上述三种 VQ 变体(残差 VQ、加法 VQ 和产品 VQ)的代码本,有不同的方法,我们将在后文中提到。

1. K-均值算法(传统方法):

根据文献综述,在大多数论文中,这三种 VQ 方法的代码本都是通过 K-均值算法 进行优化的。

2. 随机优化(机器学习算法):

机器学习优化算法是基于梯度计算的。因此,使用机器学习优化来优化向量量化方法是不可能的,因为向量量化函数中的 argmin 函数(上面的第一个方程)是不可微的。换句话说,我们无法在反向传播中将梯度传递过向量量化函数。这里我们提到了两种解决这个问题的方法。

2.1. 直接估计器(STE)

STE [10] 通过在反向传播过程中简单地将梯度完整地复制到 VQ 模块中来解决问题。因此,它没有考虑向量量化的影响,从而导致梯度与 VQ 函数的真实行为之间的不匹配。

2.2. 矢量量化中的噪声替代 (NSVQ):

NSVQ 技术 [11] 是我们最近提出的方法,其中通过向输入向量添加噪声来模拟矢量量化误差,使得模拟噪声获得原始 VQ 误差分布的形状(你可以简要阅读关于 NSVQ 的内容 在这篇文章中)。

NSVQ 技术 [11] 相较于 STE 方法 [10] 具有以下优势。1) NSVQ 为 VQ 函数提供更准确的梯度。2) NSVQ 实现了 VQ 训练(代码本优化)的更快收敛。3) NSVQ 无需对 VQ 训练进行额外的超参数调整(不需要在全局优化损失函数中添加额外的损失项)。

实验

在我们的论文中,我们使用了我们最近提出的 NSVQ 技术 [11] 来优化上述三种 VQ 变体。为了评估这三种 VQ 方法的性能,并研究它们在准确性、比特率和复杂性之间的权衡,我们进行了四种不同场景的实验。我们将在以下部分解释这些实验场景。

1. 近似最近邻(ANN)搜索

在这个实验中,我们通过在 SIFT1M 数据集 [12](128-D 图像描述符)上训练三种 VQ 方法来建模其分布。SIFT1M 图像描述符数据集 [12] 包括 10⁶ 个基向量、10⁵ 个学习向量和 10⁴ 个测试用的查询向量。实际的最近邻集合从基向量到查询向量。在 ANN 搜索中,我们首先使用在学习集上训练的对应学习到的代码本来压缩基向量。然后,对于每个查询向量,我们通过进行穷尽搜索来从压缩的基向量中找到近似的最近邻。为了评估数据压缩的质量,我们在不同的 T 参数值下计算 召回率度量,该度量显示实际最近邻(从实际数据中)是否存在于前 T 个计算的最近邻中。下图展示了使用我们提出的 NSVQ 技术优化的三种 VQ 变体与基线方法在 召回率度量 下的比较。一般来说,所有三种基于机器学习优化的 VQ 方法在召回率值上与基线相当(在 RVQ 的情况下甚至略好)。

比较我们提出的 VQ 方法和基线方法在 64 位(每个 8 个代码本,每个有 256 个码字)下对 SIFT1M 数据集的压缩召回值;Recall@T 显示实际最近邻(从实际数据中)是否存在于 T 个计算的最近邻中。(图片由作者提供)

2. 使用 VQ-VAE 的图像压缩

在这个实验中,我们在 CIFAR10 数据集的训练集上训练了一个向量量化变分自编码器(VQ-VAE)以进行压缩。为了在 VQ-VAE 的瓶颈中应用向量量化,我们使用了这三种 VQ 方法中的每一种。训练后,我们使用训练好的编码器、解码器和学习到的码本重建了 CIFAR10 的测试图像。为了评估重建图像的质量,我们使用了峰值信噪比(Peak SNR)指标。此外,我们使用加权每秒百万操作(WMOPS)指标计算了每种 VQ 方法的复杂度,该指标符合ITU-T 标准。下图展示了这个实验的结果。

在图像压缩场景下,提出的 VQ 方法在 15k 训练批次和 10 次单独实验中的峰值 SNR 和复杂度;线条表示均值,对应的填充区域表示其 95%分位数。对于所有 VQ 比特率,我们使用了四个码本,即 M=4。(图像由作者提供)

根据复杂度图(右侧),我们发现,对于相同的计算资源使用(左侧垂直红线)和较高的比特率,乘积 VQ 比残差 VQ 表现更好。此外,对于相同的计算资源使用(右侧垂直红线)和较高的比特率,残差 VQ 比加法 VQ 表现更好。因此,根据可用的计算资源,我们可以得出最适合使用的 VQ 方法。

3. 语音编码

在这个实验中,我们使用[13]中提出的语音编解码器,通过三种 VQ 方法对语音信号的谱包络进行建模。为了评估解码后语音信号的质量,我们使用了语音质量感知评估(PESQ)和感知加权信噪比(pSNR)作为客观指标。下图展示了所有三种 VQ 方法在 PESQ 和 pSNR 标准下的表现。根据结果,我们观察到,在这两个指标中,加法 VQ 的均值更高且方差更低,相比于残差 VQ 和乘积 VQ。

提出的 VQ 方法在语音编码场景下,针对 16 位 VQ(具有 4 个码本,即 M=4)在总体比特率为 8, 9.6, 13.2, 16.4, 24.4 和 32 kbit/s 时,PESQ 和 pSNR 指标的性能;实线表示 PESQ 和 pSNR 的均值,对应的填充区域表示其 95%分位数。(图像由作者提供)

4. 示例

在这个实验中,我们打算比较三种 VQ 方法在数据相关性方面的表现。因此,我们准备了两个维度为 64 的相关和不相关的数据集。然后,我们使用这三种 VQ 方法对这些数据集进行压缩。为了评估性能,我们计算了每个数据集及其量化版本之间的均方误差(MSE)。下图展示了这个实验的结果。

使用三种提出的 VQ 方法(数据维度=64,所有 VQ 比特率使用了四个码本,即 M=4)对相关和无关数据集进行的向量量化误差。(图片由作者提供)

在相关数据集的情况下,由于残差 VQ 和加法 VQ 考虑了所有数据维度之间的相关性,它们的量化误差远低于乘积 VQ,符合预期。另一方面,对于无关的数据,乘积 VQ 的表现优于加法 VQ 和残差 VQ,因为数据维度之间没有相关性,这正是乘积 VQ 的假设。

结论

使用诸如残差 VQ、加法 VQ 和乘积 VQ 等向量量化(VQ)变体,允许将 VQ 应用于高比特率和高维数据。这些 VQ 方法目前已经通过经典的期望最大化和 k-均值算法进行了优化。在本文中,我们通过机器学习优化来优化这些 VQ 方法,采用了我们最近提出的向量量化噪声替代(NSVQ)[11]技术。此外,NSVQ 还允许在神经网络中对 VQ 方法进行端到端优化。我们还研究了这三种 VQ 方法之间比特率、准确性和复杂性的权衡。因此,我们的开源实现[14]有助于为特定用例选择最佳的 VQ 方法。

GitHub 存储库

我们在以下网页提供了这些 VQ 方法的 PyTorch 实现。

## GitHub - MHVali/Additive-Residual-Product-Vector-Quantization-Methods

通过创建一个帐户来为 MHVali/Additive-Residual-Product-Vector-Quantization-Methods 的开发做出贡献…

github.com

致谢

特别感谢我的博士项目导师Prof. Tom Bäckström,他支持了我并为这项工作作出了贡献。

参考文献

[1] M. H. Vali 和 T. Bäckström,“向量量化方法在语音和图像处理中的随机优化”,载于 ICASSP 会议论文集,2023 年。

[2] A. Razavi、A. van den Oord 和 O. Vinyals,“使用 VQ-VAE-2 生成多样化的高保真图像”,载于 NeurIPS 会议论文集,2019 年。

[3] C. Gârbacea、A. van den Oord、Y. Li、F. S. C. Lim、A. Luebs、O. Vinyals 和 T. C. Walters,“使用 VQ-VAE 和 Wavenet 解码器的低比特率语音编码”,载于 ICASSP 会议论文集,2019 年。

[4] B. van Niekerk、L. Nortje 和 H. Kamper,“用于零语音 2020 挑战中的声学单元发现的向量量化神经网络”,载于 Interspeech 会议论文集,2020 年。

[5] S. Ding 和 R. Gutierrez-Osuna,“用于非平行语音转换的向量量化变分自编码器的群体潜在嵌入,”见于 Interspeech 会议录,2019 年。

[6] P. Dhariwal, H. Jun, C. Payne, J. W. Kim, A. Radford 和 I. Sutskever,“Jukebox: 一种音乐生成模型,” arXiv 预印本 arXiv:2005.00341,2020 年。

[7] A. Tjandra, B. Sisman, M. Zhang, S. Sakti, H. Li 和 S. Nakamura,“VQVAE 无监督单元发现及多尺度 code2spec 逆变器用于 Zerospeech 挑战 2019,”见于 Interspeech 会议录,2019 年。

[8] X. Wang, S. Takaki, J. Yamagishi, S. King 和 K. Tokuda,“用于统计参数语音合成的向量量化变分自编码器 (VQ-VAE) 自回归神经 F0 模型,” IEEE 音频、语音与语言处理学报,2020 年。

[9] A. Babenko 和 V. Lempitsky,“极端向量压缩的加性量化,”见于 CVPR 会议录,2014 年。

[10] Y. Bengio, N. Léonard 和 A. Courville,“通过随机神经元估计或传播梯度以进行条件计算,” arXiv 预印本 arXiv:1308.3432,2013 年。

[11] M. H. Vali 和 T. Bäckström,“NSVQ: 机器学习中向量量化的噪声替代,” IEEE Access,第 10 卷,2022 年。

[12] H. Jegou, M. Douze 和 C. Schmid,“用于最近邻搜索的产品量化,” IEEE 计算机学会模式分析与机器智能学报,第 33 卷,第 1 期,页码 117–128,2010 年。

[13] M. H. Vali 和 T. Bäckström,“端到端优化的多阶段光谱包络向量量化用于语音和音频编码,”见于 Interspeech 会议录,2021 年。

[14] gitlab.com/speech-interaction-technology-aalto-university/vq-variants

在 Mac 上优化 VS Code 以适应 dbt

原文:towardsdatascience.com/optimizing-vs-code-for-dbt-on-mac-a56dd27ba8d5?source=collection_archive---------7-----------------------#2023-04-05

在 Visual Studio Code 中最大化你的 dbt 生产力指南

Bas NederkoornTowards Data Science Bas Nederkoorn

·

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

--

(图片来自Unsplash

如果你在使 VS Code 和 dbt 良好协同工作方面遇到困难,你并不孤单。将它们集成可能具有挑战性,但它将提高你的建模效率。这就是我分享对我有效的设置的原因。

在本文中,我将涵盖诸如升级终端以便快速回忆命令、利用可以更快构建模型的扩展,以及设置格式化和检查以清理代码和发现 SQL 中的错误等主题。

如果你实施本文中的提示,你将减少在输入代码、运行模型、清理代码和寻找错误上浪费的时间。这些节省下来的时间你可以用来创建增加价值的数据模型。虽然我在写这篇文章时以 Mac 用户为目标,但许多这些提示对 Windows 用户也同样适用。

开始所需:

  • Visual Studio Code

  • homebrew包管理器。安装说明 在这里

  • Python 3.7 或更高版本以及安装了dbt的虚拟环境。有关详细信息,请参阅官方文档

  • 一个 dbt 项目。本文不详细介绍如何设置 dbt 项目。如果你没有这方面的经验,我建议你首先完成官方的 dbt jaffle shop 教程

让我们深入了解吧!

1. 优化你的终端

你可以改善你内部的 VS Code 终端,使其看起来和行为更像iTerm。安装Oh-My-Zsh和一些附加软件包可以解锁以下好处:

  • 自动补全,在你想快速调用dbt run -m {specific_model_you_ran_a_while_ago}时非常有用

  • Git 分支可视化

  • 语法高亮,例如检查你输入的命令是否存在于$PATH

如果你没有zsh(较新的 Mac 已预装),请运行:

brew install zsh

运行以下命令来安装 Oh-My-Zsh:

sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

现在你可以选择一个主题。我最喜欢的是Agnoster,因为它具有良好的 git 分支可视化。选择主题可以在你家目录中的 zsh 配置文件中完成。通过运行以下命令打开这个文件:

code ~/.zshrc

修改ZSH_THEME以设置你喜欢的主题。保存并关闭。.zshrc中的更改将在你重新启动终端时显示。

注意 要刷新当前终端窗口的设置,你可以运行: *source ~/.zshrc*

Git 分支可视化使用了一些特定的图标,你可以通过所谓的“Nerd Font”在终端中显示这些图标。我喜欢 Dejavu Sans Mono Nerd Font,你可以使用homebrew安装它[1]

brew tap homebrew/cask-fonts && brew install --cask font-dejavu-sans-mono-nerd-font

接下来,确保你的 VS Code 终端使用新字体。转到命令面板(Cmd + Shift + P)并通过输入open settings打开settings.json。在文件中添加以下行:

 "terminal.integrated.fontFamily": "DejaVuSansMono Nerd Font",

注意: 确保选择的是通用 用户设置 文件,而不是项目特定的 工作区设置 文件。稍后,当配置与 dbt 相关的设置时,我们将使用工作区设置。

你的 VS Code 终端现在应该看起来像这样:

(作者截图)

最后的步骤是安装zsh-autocompletionzsh-syntax-highlighting软件包[2],[3]。通过运行以下命令来安装它们:

git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions && \

git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

将它们添加到~/.zshrc文件中的plugins变量中:

plugins=(git zsh-autosuggestions zsh-syntax-highlighting)

终端的最终说明: 我简要提到过 iTerm,这是默认 macOS 终端的一个替代品。如果你还没有安装,建议你试试。

2. 扩展

到目前为止,我们已经涵盖了一些通用 VS Code 设置的优化。现在是时候讨论一些更具体的 dbt 配置,从一个非常强大的 VS Code 扩展开始,叫做 dbt Power User。

[## dbt Power User - Visual Studio Marketplace

这个扩展使得 vscode 可以无缝地与 dbt 配合使用。

marketplace.visualstudio.com](https://marketplace.visualstudio.com/items?itemName=innoverio.vscode-dbt-power-user&source=post_page-----a56dd27ba8d5--------------------------------)

我常用的这个扩展的主要功能有:

  • 预览编译后的 SQL (Cmd + ‘)

  • 从源 yml 生成基础模型

  • 模型引用的自动补全

  • 执行模型特定的测试

SQL & Jinja 模板化

SQL 模板化的概念(图片来源于作者)

dbt 成为如此强大的数据建模工具的原因之一是它支持 模板化 SQL 代码,使用 Jinja。它允许你使用类似 Python 的逻辑(例如 if 语句或 for 循环)来创建 SQL 查询。实际中,一个 模板化的 SQL 文件可能看起来像这样:

-- Set Variable
{% set min_date = '2021-01-01' %}

-- Use Variable
select *
from {{ ref('model_x') }}
where date > '{{min_date}}'

编译器返回以下 编译后的 SQL 文件:

select *
from `your_project`.`your_dataset`.`model_x`
where date > '2021-01-01'

编译后的查询结果可能会因使用的数据库不同而有所不同。模板化允许你用几行代码构建复杂的 SQL 模型。

dbt 中的模板化是一个独立的话题,要了解如何在模型中使用模板化,请参考 官方文档

vscode-dbt

一个可以帮助你加速编写 Jinja 模板化 SQL 代码的工具是 vscode-dbt。这个扩展为你的工具集添加了一些有用的 Jinja 片段。例如:forrefsource

[## vscode-dbt — Visual Studio Marketplace

提供 dbt 和 Jinja 函数在 SQL、YAML 和 Markdown 中的片段。酷炫提示:为了实现更丰富的集成……

marketplace.visualstudio.com](https://marketplace.visualstudio.com/items?itemName=bastienboutonnet.vscode-dbt&source=post_page-----a56dd27ba8d5--------------------------------)

为了使片段被识别,你需要在工作区的 settings.json 中添加以下行:

"files.associations": {
    "*.sql": "jinja-sql"
},

添加自定义片段

创建自定义代码片段(图片来源于作者)

如果你发现自己不断重复输入相同的内容,创建自定义片段会更快。例如;在每个模型的顶部,我喜欢导入所有的依赖项作为 CTE,这类似于你在 Python 代码中所做的。

你可以通过打开命令面板(Cmd + Shift + P)并导航到配置用户片段来添加代码片段。

转到配置用户片段(截图由作者提供)

打开 jinja-sql 片段文件(截图由作者提供)

添加片段后,文件会变成这样:

{
   "Generate Import" : {
      "prefix" : "import",
      "body" : "$1 as (select * from {{ ref($2) }})"
     }
}

这是一个简单的示例,仅用于说明目的,我鼓励你在适合的地方创建自己的代码片段。

3. Jinja-SQL 格式化:sqlfmt

为了格式化 Jinja-SQL,我使用 sqlfmt,这是一个专门为 dbt 逻辑设计的格式化工具,并与 dbt Power User 集成。sqlfmt 是一个有主见的格式化工具,你可以调整最大行长度,但在自定义格式化行为方面没有太多空间。这是好事,因为它减少了关于代码美学的讨论空间。这种简单性也对格式化的可靠性和速度产生了积极影响。

安装 sqlfmt 的最简单方法(如 [4] 中推荐的)是使用 pipx

如果你没有 pipx,请运行以下命令安装:

brew install pipx

ensurepath 命令将 pipx 的二进制文件添加到你的 PATH 中,这样你就可以从任何地方调用 sqlfmt → 在我们的例子中就是从 dbt Power User。

pipx ensurepath

要安装 sqlfmt,请运行:

pipx install 'shandy-sqlfmt[jijnafmt]'

结果是 sqlfmt 被安装并暴露在你的 $PATH 中:

(图片由作者提供)

最后的步骤是在 VS Code 中配置 Jinja SQL 的默认格式化工具。将以下几行添加到工作区的 settings.json 中:

"[jijna-sql]": {
  "editor.defaultFormatter": "innoverio.vscode-dbt-power-user" 
  "editor.formatOnSave": false //Optional, I prefer to format manually.
} 

注意: 如果你阅读 sqlfmt 的手册,你会发现可以通过使用 *# fmt: off / on* 来排除代码的某些部分,或者定义要排除的文件夹。

4. 使用 SQLFluff 进行 Linting

使用 SQLFluff 进行 Linting(图片由作者提供)

什么是 Linting?

Linting 是分析代码以查找错误、漏洞和样式问题的过程。你可以将 lint 工具视为代码的语法检查器。

格式化和 linting 是两种不同的技术,可以一起使用来改进你的代码。格式化将改善代码的外观,而 linting 将帮助你捕捉代码中的更复杂的错误和问题。

我使用的 linter 是 SQLFluff。它适用于大多数 SQL 方言,更重要的是;它与 Jinja 模板化的 SQL 兼容。在撰写时,我发现这是最先进的 SQL linter。

通过运行以下命令安装:

brew install sqlfluff

要将 SQLFluff 与 VS Code 集成,你可以下载sqlfluff 扩展。为避免sqlfluffsqlfmt和 dbt Jinja 逻辑之间的冲突,请在你的工作区settings.json中添加以下行:

 "sqlfluff.dialect": "bigquery", // Depends on your database
    "sqlfluff.executablePath": "/usr/local/bin/sqlfluff", // Check your path by running `which sqlfluff`
    "sqlfluff.linter.diagnosticSeverity": "error",
    "sqlfluff.linter.run": "onSave",
    "sqlfluff.config": "${workspaceFolder}/.sqlfluff",

此外,在你的工作区文件夹中创建一个.sqlfuff文件。添加以下行:

[sqlfluff]
# Double, but needed if you run from terminal
dialect = bigquery

exclude_rules = L003, L018, L022, L036,

# Conflict sqlfmt:
# L003 : Indentation
# L018 : CTE closing bracket position
# L036 : Amount of columns in one line (1 vs. how many fit within line length)

# Personal Preference
# L022 : Blank line after CTE 
# (conflicts with creating select * cte's of dependencies at the top of a file)

[sqlfluff:rules]
# Use the same linelength as sqlfmt to prevent conflicts
max_line_length = 100
capitalisation_policy = lower
extended_capitalisation_policy = lower

[sqlfluff:indentation]
indented_joins = False
indented_using_on = True
template_blocks_indent = False

[sqlfluff:rules:L052]
multiline_newline = True

注意: 上述提到的*.sqlfluff*基于 [5**] 但稍作扩展。如何设置 linting 规则取决于你的个人(以及你团队的)偏好。

安装了vscode-sqlfluff扩展后,你无需再从命令行进行 lint 检查。然而,我仍然喜欢从终端调用sqlfluff lint {path_to_model_sub_folder},因为它能清楚地概览你代码中的任何问题。

(截图由作者提供)

除了lint命令,SQLFluff 还有一个类似格式化的fix命令。你可以使用它来修复 SQLFluff 提出的一些代码问题。然而,我建议谨慎使用此功能,因为它也可能会破坏你的代码。如前所述,我倾向于使用较少复杂的sqlfmt进行格式化。

为了忽略由于模板化 dbt 函数导致的 lint 错误,比如下面的那个,你可以在.sqlfluff文件中添加ignore = templating

L:  25 | P:  15 |  TMP | Undefined jinja template variable: 'dbt_utils'

要进一步自定义 linting 行为,请参阅官方文档[6]。

完成!

结论

如果你按照步骤操作并且已经完成了这些,应该现在你有了一个优化的工作区来使用 dbt!我们涵盖了如升级 VS Code 终端、使用哪些扩展以及如何格式化和 lint Jinja 模板化 SQL 等主题。这些提示将节省你的时间和精力,让你专注于构建更好的数据模型。

目标是不断提高效率。如果你在寻找下一个更高级的步骤,我建议你探索dbt 的宏。如果你有任何问题或对未来的文章有想法,请随时联系我。

感谢阅读,祝建模愉快!

觉得这篇文章有用吗?

如果你觉得这篇文章有帮助,记得 关注我 以便了解我最新的关于数据建模、编码等的文章。

参考文献

[1] 通过 homebrew 安装字体 | | 链接

[2] 安装 zsh-syntax-higlighting | | 链接

[3] 安装 zsh-autocomplete | | 链接

[4] 安装 sqlfmt | | 链接

[5] 集成sqlfmtsqlfluff | | 链接

[6] 默认配置 SQLFluff | | 链接

优化您的策略:超越 A/B 测试的方法

原文:towardsdatascience.com/optimizing-your-strategies-with-approaches-beyond-a-b-testing-bf11508f8930?source=collection_archive---------18-----------------------#2023-07-24

对经典 A/B 测试的深入解释:Epsilon-greedy、Thompson Sampling、Contextual Bandits 等

John LeungTowards Data Science John Leung

·

关注 发布于 Towards Data Science · 7 分钟阅读 · 2023 年 7 月 24 日

--

在数字营销的世界中,竞争激烈。每家公司都希望制定出最高价值的营销策略——提升客户留存、增强客户满意度,或实现其他商业目标。然而,并没有一种完美的营销策略适用于所有客户。相反,我们可以努力找到改进版的营销策略。这就是 A/B 测试的用武之地。

简而言之,A/B 测试是一种实验,它通过随机将观众分成两组,并比较两种策略的版本来查看哪一种表现更好。这是做出数据驱动决策的传统且经过统计验证的方法。

何时使用 A/B 测试

想象你和你的伙伴经营了一段时间的玩具电子商店。一天,你的伙伴对着陆页的背景颜色提出了担忧。他认为当前的颜色未能有效推动访客的转化率(即购买商品的访客百分比)。

  • 当前使用(策略 A):橙色

  • 你的伙伴的建议(策略 B):黄色

你立即识别出 A/B 测试是一个很好的实验设计。因为相对直接地根据测量结果(即每个策略的转化率)来确定“赢家”或“输家”,展示了哪一版本的网站最吸引访客。

下图展示了示例情况。

A/B 测试(图像由作者提供)

A/B 测试的问题是什么

A/B 测试的结果确认,黄色着陆页(策略 B)上的网站访问者在电子商店中的转化率更高。因此,你决定将黄色着陆页推向所有访客群体。然而,你很快意识到这种测试方法带来了成本:由于 A/B 测试有严格定义的开始和结束点,整个测试期间,半数流量被分配给表现较差的策略 A。

选择较差策略 A 和最理想选项(在这种情况下为策略 B)之间的成本差异可以用遗憾来表示。

A/B 测试中的遗憾成本(图像由作者提供)

这个 A/B 测试可以总结为以下特点:

  • 这强调了探索的价值:收集足够的信息来确定策略 A 还是 B 在整个测试中是最优的。

  • 这超越了利用的价值:在测试过程中不通过减少流量来实现更高的转化率,即使收集到的信息表明策略 B 的表现更好。

听到 A/B 测试的结果后,你的伙伴对他建议更好策略的成功感到兴奋。他想进一步了解其他背景颜色,包括绿色、红色和紫色,是否能提高转化率。你可以预见到使用当前测试方法的情况会变得更糟,因为当涉及到更多策略时,A/B 测试的遗憾可能会更高。

多种策略在 A/B 测试中的情况(图像由作者提供)

多种策略在 A/B 测试中的遗憾成本(图像由作者提供)

超越 A/B 测试

我们欢迎新策略,因为它们可能使我们更接近商业目标。然而,我们通常会怀疑是否在测试过程中浪费了大量有限的时间和资源在糟糕的策略上。

在经典的 A/B 测试中,我们保持在探索模式,忽略了策略是否表现优异或较差。 如果我们采取相反的方式,专注于利用会怎样呢?操作方式如下:当只有少数访客在电子商店进行购买时,我们会快速选择看似最优的策略,然后在测试剩余时间里将所有访客都引导到这个策略上。这种方法具有较高的风险,因为样本量可能不足以根据策略的表现得出正确的结论。

高度利用(图片由作者提供)

高度利用的遗憾成本(图片由作者提供)

现在,我们理解了单独的探索或单独的利用都不是好的主意。我们需要在探索和利用之间取得平衡。此外,无可争议的是,我们应该在测试过程中不断将更多流量分配给当前最优策略。

换句话说,我们更倾向于采取能够最大化估计回报的行动。

Epsilon-Greedy

Epsilon-Greedy 算法基于类似的理念,通过在每位访客的流量中随机选择探索和利用。假设我们希望 90%的流量集中于利用,10%的流量用于探索。算法遵循以下步骤:

当访客到达电子商店时,

  1. 确定当前的最优策略

  2. 90%的机会进行利用:将访客引导至当前最优策略。或者,有 10%的机会进行探索:将访客引导至剩余策略中的一个。

随着时间的推移,我们贪婪地在不同的策略中进行选择以进行利用,并逐渐做出一个一致的当前最优策略的决策。然而,我们并不会放弃尝试其他策略的机会,即使在测试接近最终阶段时也是如此。 通过在每次行动中最大化估计的回报,这种方法的总体遗憾成本可能比经典的 A/B 测试要小。

Epsilon-Greedy(图片由作者提供)

托普森采样

Epsilon-Greedy 看起来很有前景,但它是否还能进一步改进?让我们考虑两个场景:

(A) 当只有少数访客到达时,我们能否自信地为整个群体确定最优策略?

可能不能。我们担心这些样本可能是异常值。由于仍然存在较高的不确定性,探索仍然是必要的。

(B) 当成千上万的访客到达或我们处于测试的后期阶段时,我们现在是否有更大的信心?

最有可能的是,由于更多的样本提供了更高的统计能力,使我们能够识别出在真实人群中表现优于其他策略的策略。

现在,让我们优化我们的策略。为了根据我们的信心水平在探索和利用之间做出选择,我们可以实施汤普森采样。

这种方法选择具有最高可能性将访客转化为买家的策略,而不是仅仅专注于最大化预期奖励。 每次访客访问电子商店时,这种方法都会跟踪每种策略的总买家和非买家数量,并构建它们各自的贝塔分布。其优点包括:

  • 当样本量较小时,即使是转化率较低的策略也可能被选择(优先考虑探索)。

  • 随着证据变得更加明显,具有更高转化率的策略将在大多数情况下被选择(优先考虑利用)。

上下文赌博机

汤普森采样是一种有效的方法,因为它在整个测试过程中动态调整探索和利用之间的平衡。虽然它允许我们在客户到达时做出有关应用哪种策略的明智决策,但这些决策不会针对不同的客户群体进行个性化。

照片由Justin Lim拍摄,发布在Unsplash

让我们考虑两个访客:Peter 和 Mary。Peter 喜欢每个周末与孩子一起观看《神偷奶爸》的视频,而 Mary 对这些没有兴趣。即使在测试的早期阶段,我们也可以预测 Peter 更倾向于访问黄色的玩具电子商店页面(《神偷奶爸》的主要颜色!),并且更有可能进行购买。

这个例子突出了考虑访客上下文数据的重要性。通过利用上下文赌博机,我们可以更系统地应用算法决策。在现实场景中,上下文可能包括关于每位客户的历史数据,如网站点击、过去的购买、打开个性化邮件的频率,甚至是他们当前会话的数据,例如最近的搜索查询。然后,算法可以学习将不同的上下文与最有可能导致转化的策略关联起来。

任何新的创意

我们已经讨论了各种方法,包括经典 A/B 测试、Epsilon-Greedy 算法、汤普森采样和上下文赌博机。

  • 经典的 A/B 测试:涉及保持在探索模式中。

  • Epsilon-Greedy 算法:随机平衡探索和利用。

  • 汤普森采样:随着每种策略的样本量增加,更多强调利用。

  • 上下文赌博机:根据访客的上下文(附加信息)提供个性化和最优的策略

这些方法在最近的研究论文中得到了进一步的精炼和讨论。以下是一些例子:

  • “具有深度表示和浅层探索的神经上下文强盗算法”:神经上下文强盗算法依赖于在整个网络参数空间中进行的探索,这对于大规模网络通常效率低下。该论文提出了一种具有浅层探索和设定遗憾界限的新算法。

  • “联邦上下文强盗算法的实证评估”:本文探讨了将联邦学习(一种去中心化的机器学习模型训练方法)与上下文强盗算法相结合的问题。它解决了与联邦学习整合相关的关注点,例如利用少量预训练数据。

在你离开之前

如果你喜欢这篇阅读,我邀请你关注 我的 Medium 页面。通过这样做,你可以随时了解有关数据科学、项目管理和自我提升的精彩内容。

Orca: 正确模仿专有 LLMs

原文:towardsdatascience.com/orca-properly-imitating-proprietary-llms-44ffa0293adb

利用模仿来创建高质量的开源 LLM…

Cameron R. Wolfe, Ph.D.Towards Data Science Cameron R. Wolfe, Ph.D.

·发表于 Towards Data Science ·阅读时长 16 分钟·2023 年 9 月 30 日

--

(照片由 Thomas Lipke 拍摄,来自 Unsplash

随着对大型语言模型(LLMs)研究的深入,一个关键问题尚未得到回答,那就是现有的高质量 LLM 是否可以有效地用来训练另一个 LLM。目前,围绕这一主题存在大量的辩论和争议。最近,开源模仿模型的爆炸性增长最初表明,像 ChatGPT 这样的专有 LLM 可以以低成本轻松复制。然而,随后的研究得出结论,这些模型的评估是不完整且具有误导性的,发现这些模型实际上在理解方面存在很大差距。在这次概述中,我们将研究工作[1],旨在通过更稳健的方法解决专有 LLM 的开源复制品的局限性。特别是,我们将看到,通过策划一个更大、更详细的数据集,模仿学习可以变得更加有效。

“随着这些模型的不断发展并变得更强大,一个有趣的问题出现了:我们能否利用模型本身来监督其自身行为或其他 AI 模型的行为?” — 来源于 [1]

(来源于 [1])

背景信息

在深入概述之前,我们将介绍一些与 LLM 和深度学习相关的概念。这些概念可能在我们阅读的论文中没有明确描述,而是通过引用或被假定为常识。因此,了解这些基本概念将使这次概述及其涉及的论文更容易理解。

指令调整

(来自 [12])

指令调整最初由FLAN [12] 提出,旨在提供一种训练形式,教会大语言模型(LLM)解决语言基础任务,而不是特定任务。具体而言,这通过对一组“指令”或输入提示进行微调来完成——包括解决的任务描述——以及期望的模型输出;见上文。最近的大语言模型主要使用一种特定的指令调整变体,该变体通过人类或其他 LLM 的对话示例对 LLM 进行微调。通常,指令调整是在预训练之后进行的微调步骤;见下文。

指令调整与其他常见训练范式(来自 [12])

合成指令调整。 尽管人类可以手动创建用于指令调整的数据,我们也可以使用 LLM 合成生成这些数据。有两种基本方法:

  • 从另一个模型获取示例对话会话(例如,从ShareGPT)。

  • 使用提示框架(例如,自我指令 [9])来生成和完善高质量的对话示例。

这两种方法都是有效的,但它们各有局限。例如,公共 LLM 对话示例往往偏向于某些任务,例如创意内容生成或信息获取对话。此外,通过自我指令 [9] 生成的对话往往缺乏复杂性,尽管这个问题通过 Evol-Instruct [2] 策略得到了缓解,该策略明确指示和引导 LLM 生成更复杂的内容;见下文。

(来自 [2])

系统消息

(来自 OpenAI API 文档)

我们互动的大多数基于聊天的 LLM 允许我们提供系统消息;见上文。该消息基本上是对模型的指令,描述了期望其如何对用户做出反应。在 ChatGPT 和 GPT-4 API 使用的聊天标记语言中,该系统消息被赋予“系统”角色——与“用户”或“助手”相对——在聊天记录中。通常,系统消息是我们应该放置在与用户对话过程中 LLM 应遵循的任何指令的地方。

现代大型语言模型是可引导的。 尽管以前的大型语言模型(例如早期版本的 GPT-3.5-turbo)对系统消息关注不多,但当前的模型(例如 GPT-4)则更具引导性。这意味着我们可以在系统消息中提供详细的指示,以供大型语言模型遵循。在实践中,现代大型语言模型的这一特性可以用来调整它们的风格或格式(通过系统消息),以精确匹配我们正在解决的应用或任务。

其他有用的想法

  • 知识蒸馏和模型模仿:我们在之前的综述中已经详细解释了这个概念,但它与本综述中提出的分析非常相关。[link]

  • 打包技术:这是在[1]中使用的一个技巧,简单地将多个文本序列连接成一个单一示例进行训练,以避免在每个序列后面过多的填充,并提高效率。[link]

  • 思维链提示[13]:我们已经看到,鼓励大型语言模型在回答问题时给出问题解决的理由,可以提高推理能力。在[1]中的解释调优(更多细节稍后提供)与这种技术有很多基本的相似之处。[link]

  • 课程或渐进学习:我们可以制定一个特定的策略或课程来展示这些数据给模型,而不仅仅是训练一个模型处理我们所有的数据。在[1]的案例中,这个课程包括首先用 ChatGPT 的对话示例训练模型,然后在 GPT-4 对话上进一步训练。这个术语相当通用,因为可能存在许多不同类型的课程。[link]

开源大型语言模型的爆炸性增长

开源大型语言模型的未来几乎和这张照片一样光明(来自 DreamStudio)

随着大型语言模型的受欢迎程度不断上升,最具影响力的模型(例如 GPT-3 和 ChatGPT)最初仅通过付费的专有 API 提供。然而,正如我们在最近的综述中了解到的那样,LLM 社区正在蓬勃发展,致力于创建强大的开源模型!虽然已经提出了许多开源模型,但这一运动特别受到最近提出的 LLaMA [4]的推动,这是一套高性能的基础模型,具有不同的规模,仅在公开数据上进行训练。

LLaMA 和模仿模型

从 LLaMA 创建的衍生模型(来自[5, 6, 7, 8])

LLaMA 套件中的 LLM 权重已公开发布(用于研究目的),随后在线泄露,任何人都可以访问。此泄露事件后,LLaMA 快速获得了人气,并被用于创建各种开源衍生模型,我们在之前的概述中已进行了探讨。

  • 超越 LLaMA:开源 LLM 的力量 [link]

  • 模仿模型与开源 LLM 革命 [link]

这些 LLaMA 衍生模型主要是通过模仿方法创建的,该方法在来自更强大模型(例如 ChatGPT)的对话示例上调整 LLaMA。这些模仿模型接连被提出,似乎表现非常好——在某些情况下甚至与像 ChatGPT 这样的强大模型相媲美 [6]。这使得 LLM 社区相信专有 LLM 可以很容易地被复制,但事情比这要复杂一些。

(来自 [3])

模仿还是限制? 尽管模仿模型似乎表现良好,但我们在之前的工作 [3] 中看到,这仅在微调过程中观察到的小部分任务中适用。即,大多数模仿模型捕捉了像 ChatGPT 这样的专有 LLM 的风格,但它们未能捕捉这些模型的知识、推理能力和理解能力。这种限制在人类对模型的评估中很容易被忽视,因为验证模型的信息是否事实正确需要大量时间投入。

(来自 [1])

由于现代 LLM 在生成连贯文本方面非常出色,它们之间的差异可能难以测量,尤其是当被比较的模型具有相似的风格和流畅度时。当模仿模型通过广泛的定量基准进行更严格的评估时,我们开始清楚地看到它们的不足之处。例如,Vicuna [6] 的表现——这是用 LLaMA 创建的较高性能的模仿模型之一——在更困难和复杂的基准测试中远远落后于 ChatGPT;见上文。

为什么模仿没有效果?

当我们研究通过模仿方法创建开源专有 LLM 的现有尝试时,我们看到的大多数问题都由相同的问题造成:我们没有足够的高质量数据用于指令调整。我们可以生成这些数据的三种基本方法是:

  1. 让人类生成数据

  2. 使用提示框架(例如,自我指导 [9])生成合成数据

  3. 直接在现有 LLM 的输出上进行训练

像 GPT-4 这样的流行 LLMs 在大量人类反馈上进行训练,但生成数据是昂贵且耗时的。为了自动化数据收集,近期的模仿模型依赖于某种变体的自我指导[9]来生成合成——即数据是由 LLM 生成而非人类——微调数据集。不幸的是,这种方式生成的数据集往往缺乏多样性和复杂性。而且,当直接在从公共 API 或 ShareGPT 获取的 LLM 对话上进行微调时,我们也会遇到类似的问题。这些数据集往往规模小且同质化,这对于创建强大的模仿模型是不足够的。

“我们得出结论,纯粹通过模仿广泛匹配 ChatGPT 需要全力收集巨大的模仿数据集,并且需要比目前可用的数据更为多样化和高质量的模仿数据。” — 来自[3]

前进的道路。 尽管现有的模仿尝试未能达到预期,但我们可以有几种不同的前进方式。如[3]中所提,我们可以从创建更强大的开源基础 LLMs 开始,这些基础 LLMs 可以作为更好的“起点”进行指令调优。之前的工作表明,使用更好的基础 LLM 可以显著提高结果模仿模型的性能。我们看到,像FalconMPT这样令人惊叹的开源基础模型的提案已经在广泛探索这个领域。

(来自[1])

另外,我们可以考虑改进或扩展现有的用于模仿学习的数据集。目前的工作仅依赖于 LLM 生成的提示和响应对;见上文。在这个概述中,我们将把这些对话称为“浅层”模仿示例,因为它们仅包含关于专有 LLM 对提示的响应的信息。超越浅层模仿,本概述将探讨通过专有 LLM 提供的更详细输出来增强合成指令调优数据集的想法,例如:

  • 解释跟踪

  • 逐步思考过程

  • 复杂指令

模仿模型可以从专有模型在微调过程中生成的额外信息中学习。我们希望模仿数据集大而多样。然而,在这个概述中,我们将看到使用的数据类型和粒度也可以产生巨大的差异。这些额外信息可以让较小的开源 LLMs 学习到更强大模型所遵循的推理过程。

正确学习模仿

(来自[1])

为了缓解现有模仿模型的问题,文献 [1] 中的作者提出了一个 130 亿参数的模仿 LLM,称为 Orca。与先前的模仿模型类似,Orca 基于 LLaMA 系列的 LLMs,但它使用的不仅仅是一小部分“浅层”模仿示例进行微调。更具体地说,Orca 在两个主要方面与先前的工作有所不同:

  • 一个更大、更全面的模仿数据集

  • 在每个指令调优示例中注入详细的解释痕迹

结果模型在各种基准测试中表现相当出色,从而缩小了模仿模型与专有 LLMs(例如,ChatGPT 或 GPT-4)之间的差距;见下文。然而,正如我们将看到的,GPT-4 仍然要好得多。

(来自 [1])

更大更好的数据。 Orca 从 FLAN collection [10] —— 一个用于指令调优的大型数据源 —— 中有选择地抽取任务,并从这些任务中的复杂提示中获取了来自 ChatGPT 和 GPT-4 的数百万个回应。通过系统消息,作者鼓励这些模型用更多细节解释其回应,从而为每个由 LLM 生成的输出提供“解释痕迹”。这种方法对模型质量有着巨大影响,因为它提供了一个更丰富的信息来源,从中模仿模型可以学习。我们将这种方法称为“解释调优”——它只是对包含解释痕迹的数据进行的指令调优!

“我们的研究表明,无论这些逐步解释是由人类还是更先进的 AI 模型生成的,逐步解释的学习都是提升模型能力和技能的一个有前途的方向。” — 来自 [1]

与先前工作的关系。 在先前的概述中,我们看到基于 LLaMA 的模仿模型远远不能模仿专有 LLMs。为了缩小模仿模型与专有 LLMs 之间的能力差距,我们需要一个显著更大和更多样化的模仿数据集。文献 [3] 中的先前工作声称获取这样的数据集太困难,表明模仿模型是一条死胡同。然而,文献 [1] 中的作者正是这样做的(即,生成一个大规模且复杂的模仿数据集),以实现模仿模型质量的突破。

更好的模仿学习方法…

(来自 [1])

Orca 在模仿模型质量上的突破可以归因于其更大、更详细和更复杂的模仿数据集;见上文。让我们探索这个数据集的细节,重点关注专有模型如何被提示以输出逐步问题解决解释,这对于开源模仿模型是一个更强大的学习信号。

解释调优。 之前的模仿模型是在由 LLM 生成的提示和相关回应对上进行训练的。虽然这种方法可以教会模仿模型复制或记忆教师模型的输出,但仅从模型的回应中学到的东西不多——这些信息浅显且缺乏有关回应是如何产生的或为何产生的详细信息。在[1]中,作者探索了一种替代方法,该方法训练模仿模型如何复制教师模型的推理过程。

(来源于[1])

为此,我们只需要提示教师模型输出详细解释以及其正常回应。借鉴零样本 CoT 提示的思想[11],我们可以通过调整系统消息来鼓励模型在每次回应时提供详细解释;见上文。然后,我们可以利用回应和解释作为训练信号来微调模仿模型。正如我们在先前的工作中所见[11],教会一个 LLM 在每次回答时输出如此详细的解释痕迹,可以显著改善推理任务和复杂指令的跟随。

创建数据集。 为了微调 Orca,通过从FLAN collection中包含的数百万条指令中采样来创建一个大规模的模仿数据集,如下表所示。结果数据集称为 FLAN-5M。

(来源于[1])

FLAN-5M 指令集通过使用 OpenAI API 从 ChatGPT 获得的回应和解释进行增强。同样,使用一组称为 FLAN-1M 的较小采样指令集(即,我们基本上只是从原始的 5M 指令集中进行子采样),使用 GPT-4 执行类似程序,生成一个总计 600 万条指令示例的数据集,每条指令配有来自专有教师模型的回应和解释。有趣的是,[1]中的作者指出,从每个模型收集数据需要几周时间——即使使用Azure OpenAI service——由于速率限制;见下文。

(来源于[1])

渐进学习。 我们可以通过首先在 ChatGPT 基础上的解释中微调 Orca,然后再在 GPT-4 的解释中微调,从而在所有数据上训练时实现性能的提升。鉴于 Orca 基于一个较小的 LLaMA 模型,这个模型的能力明显低于专有 LLM,这种渐进学习方法使得模仿模型能够先从“较简单”的例子中学习,然后再从像 GPT-4 这样强大的模型的更详细解释中学习。这种方法的积极影响可能源于 GPT-4 往往生成较长且复杂的解释,这些解释更难以学习;见下文。

(来自 [1])

基于解释的模仿学习是有效的!

Orca 与多种不同的基准进行比较,包括 Vicuna [6]、text-davinci-003(即 GPT-3.5)、ChatGPT 和 GPT-4。文献 [1] 中的作者考虑了一系列不同的基准,包括写作、理解和推理任务;见下文。Orca 的评估策略被做得非常全面,以避免先前模仿模型所遇到的误导或不完整的评估结果问题。值得注意的是,我们在 [1] 中看到,由标准化测试组成的基准提供了一个令人惊讶的强大评估框架。

(来自 [1])

开放式生成。 在开放式生成任务的评估中,Orca 在所有实验设置中都大幅超越了 Vicuna;见下文。在这里,性能通过考虑参考模型(例如,ChatGPT 或 GPT-4)并提示 GPT-4 来确定候选模型生成的输出是否优于参考模型的输出来衡量。

(来自 [1])

我们在这些实验中看到,Orca 在数据集上保持了 ChatGPT 质量的 95% 和 GPT-4 质量的 85%。虽然这些指标表明与先前的模仿模型相比性能有显著改善,但我们应记住 LLM 基于评估不完美 且仍在探索中。因此,这些结果虽然积极,但可能会误导。

推理。 Orca 在推理基准测试中继续表现出与 ChatGPT 相似的性能。例如,在 AGIEvalBigBench-Hard 数据集的(几乎)所有主题中,Orca 的表现接近或超过了 ChatGPT!

(来自 [1])

尽管 Orca 在标准化考试中的表现仍然低于 ChatGPT 的某些情况,但我们看到[1]中的工作在弥合专有 LLMs 与之前模仿模型之间的差距方面取得了显著进展;见下文。尽管这可能并不令人惊讶,但 GPT-4 在几乎所有被考虑的任务中仍然是明显的领先者。

(来源于 [1])

其他发现。 除了[1]中提出的主要实证结果之外,我们发现建议的课程(或渐进式)学习方法——即模型首先在 5M 个 ChatGPT 对话示例上进行微调,然后在 1M 个 GPT-4 示例上进行微调——对 Orca 的性能产生了很大且积极的影响。此外,Orca 在建模长序列时始终表现不如 ChatGPT。

结语

“我们的发现表明,Orca 在性能上显著优于其他开源较小模型。此外,在一些设置中,它可以匹配甚至超越 ChatGPT 的质量,但与 GPT-4 之间仍然存在较大的差距。” — 来源于 [1]

对开源 LLMs 的研究不断发展。一个星期,我们认为专有 LLMs 已经完全失去了优势,而下一个星期我们发现开源(模仿)模型远比最初声称的要差。尽管几周前模仿模型似乎是死路一条,但我们在本概述中看到模仿是一种有效的方法!我们所需要的只是更大更好的数据集。这项工作的主要结论如下。

从逐步说明中学习。 之前的模仿模型研究依赖于简单的提示-回应对进行训练。我们在这里看到,通过详细的解释轨迹来增强这些数据,使得结果模型能够从更丰富的信息源中学习。我们可以让专有的 LLM 的解决问题过程被复制,而不是仅仅记住模型在一小部分示例上的回应。因此,文献[1]中的方法使得模仿模型的性能能够超越在微调期间看到的数据。

“我们强调了数据规模和覆盖范围在将较小模型对齐到更强大的模型(如 GPT-4)时的重要作用。” — 来源于 [1]

大量的模仿数据。 之前模仿模型的一个主要问题是它们仅在与其微调数据集中的数据相似的任务上表现良好。鉴于这一特定限制,我们显然需要更大、覆盖范围更广的模仿数据集。尽管之前的工作表明,生成这样一个数据集会太困难,但我们在[1]中看到这是可能的。鉴于更大且更全面的数据集(即数百万个示例),我们可以使模仿模型比以前表现得更好。

剩余工作。 尽管 Orca 的表现令人印象深刻,但仍然不及最佳的专有语言模型——需要更多工作才能使开源语言模型真正具有竞争力。弥合这一差距很可能是多项正在进行的倡议的结果,例如模仿学习、创建更好的基础模型以及策划更好的公开数据集用于指令调整和语言模型改进。然而,开源产品不应被低估,因为它们将继续与专有产品一同改进。

与我联系!

非常感谢阅读本文。我是 Cameron R. WolfeRebuy 的 AI 总监。我研究深度学习的实证和理论基础。如果你喜欢这个概述,订阅我的 Deep (Learning) Focus 新闻通讯,我将通过从头到尾的相关话题概述帮助读者理解 AI 研究。你也可以在 XLinkedIn 上关注我,或查看我在 medium 上的 其他著作

参考文献

[1] Mukherjee, Subhabrata 等。“Orca: 从 GPT-4 的复杂解释痕迹中逐步学习。” arXiv 预印本 arXiv:2306.02707(2023)。

[2] Xu, Can 等。“Wizardlm: 赋能大型语言模型以遵循复杂指令。” arXiv 预印本 arXiv:2304.12244(2023)。

[3] Gudibande, Arnav 等。“模仿专有语言模型的虚假承诺。” arXiv 预印本 arXiv:2305.15717(2023)。

[4] Touvron, Hugo 等。“Llama: 开放且高效的基础语言模型。” arXiv 预印本 arXiv:2302.13971(2023)。

[5] Taori, Rohan 等。“斯坦福 Alpaca: 一款遵循指令的 LLaMA 模型。”(2023)。

[6] Chiang, Wei-Lin 等。“Vicuna: 一款开源聊天机器人,令人印象深刻的 GPT-4 质量达到 90%*。”(2023)。

[7] Geng, Xinyang 等。“Koala: 一款用于学术研究的对话模型。”(2023)。

[8] Yuvanesh Anand, Zach Nussbaum, Brandon Duderstadt, Benjamin Schmidt 和 Andriy Mulyar。“GPT4All: 使用从 GPT-3.5-Turbo 提取的大规模数据蒸馏训练助手风格的聊天机器人。”(2023)。

[9] Wang, Yizhong 等。“Self-Instruct: 将语言模型与自生成指令对齐。” arXiv 预印本 arXiv:2212.10560(2022)。

[10] Longpre, Shayne 等。“flan 集合:为有效指令调整设计的数据和方法。” arXiv 预印本 arXiv:2301.13688(2023)。

[11] Kojima, Takeshi 等。“大型语言模型是零样本推理器。” arXiv 预印本 arXiv:2205.11916(2022)。

[12] Wei, Jason 等。“微调语言模型是零样本学习者。” arXiv 预印本 arXiv:2109.01652(2021)。

[13] Wei, Jason 等。“思维链提示引发大型语言模型中的推理。” arXiv 预印本 arXiv:2201.11903(2022)。

使用 LLM 编译器框架有效协调知识图谱的推理

原文:towardsdatascience.com/orchestrating-efficient-reasoning-over-knowledge-graphs-with-llm-compiler-frameworks-749d36dc32b9

安东尼·阿尔卡拉兹数据科学前沿 安东尼·阿尔卡拉兹

·发布于 数据科学前沿 ·6 分钟阅读·2023 年 12 月 29 日

--

人工智能软件被用来增强本文文本的语法、流畅性和可读性。

最近,大型语言模型(LLM)设计中的创新推动了少样本学习和推理能力的快速进步。然而,尽管取得了进展,LLM 在处理涉及大量相互关联知识的复杂现实世界情境时仍面临限制。

为了应对这一挑战,一种有前景的方法在检索增强生成(RAG)系统中出现。RAG 结合了 LLM 的自适应学习优势与从外部知识源(如知识图谱(KGs))中进行可扩展检索的能力。与其试图将所有信息静态编码在模型中,RAG 允许根据需要从索引知识图谱中动态查询所需的上下文。

然而,有效地协调跨互联知识源的推理和检索带来了自身的挑战。那些简单地在离散步骤中检索和拼接信息的天真方法,往往无法充分捕捉密集知识图谱中的细微差别。概念的相互关联性意味着,如果没有相互关系的分析,关键的上下文细节可能会被遗漏。

最近,一个名为 LLM Compiler 的有趣框架在优化 LLM 中多个函数调用的协调方面展示了早期成功,自动处理依赖关系并允许并行执行。

[## 一个用于并行函数调用的 LLM 编译器

大型语言模型(LLM)在各种复杂推理基准上表现出了显著的成果。推理...

arxiv.org](https://arxiv.org/abs/2312.04511?source=post_page-----749d36dc32b9--------------------------------) [## llama-hub/llama_hub/llama_packs/agents/llm_compiler/llm_compiler.ipynb at main ·…

一个由社区创建的用于大语言模型的数据加载器库——可与 LlamaIndex 和/或 LangChain 一起使用…

github.com](https://github.com/run-llama/llama-hub/blob/main/llama_hub/llama_packs/agents/llm_compiler/llm_compiler.ipynb?source=post_page-----749d36dc32b9--------------------------------)

在本文中,我们探讨了将大语言模型编译器技术更广泛地应用于知识图谱检索和推理的潜力。在论文发布之前,我们已经做了一个工作原型:

## 实现混乱背景下的结构化推理:思路提示和…

大语言模型(LLMs)展示了令人印象深刻的少量学习能力,迅速适应新任务…

towardsdatascience.com

我们分析了诸如自动规划、依赖管理和并行执行等技术如何使对互联知识的分析更加高效和结构化。

计划:

1. 大规模知识图谱推理的挑战

2. 知识图谱作为模块化大语言模型工具

3. 由大语言模型编译器驱动的结构化推理

4. 知识吸收的操作系统

1. 大规模知识图谱推理的挑战

[## blogs/llm/devops_rag.ipynb at master · tomasonjo/blogs

支持我图数据科学博客文章的 Jupyter 笔记本,网址为 https://bratanic-tomaz.medium.com/

github.com](https://github.com/tomasonjo/blogs/blob/master/llm/devops_rag.ipynb?source=post_page-----749d36dc32b9--------------------------------)

在解决大规模知识图谱推理的挑战时,必须整合先进的技术和方法来优化数据检索和处理。这涉及利用各种计算策略来平衡分析大规模互联数据集中的效率、准确性和完整性。以下部分详细介绍了这些方法:

a. 优化 Cypher 查询以进行数学运算

目标: 提高 Cypher 查询的性能,特别是针对图数据的数学聚合。

方法论:

- 识别机会: 识别可以高效应用数学聚合的图数据场景。

- 模块化步骤: 将过程分解为并行检索组、并发执行聚合函数和高效连接。

- 示例应用: 计算分配给 A 队的高优先级任务数量。这涉及到检索任务节点、按优先级和团队进行筛选,并汇总计数。

b. 规划并行向量搜索

目标: 实现并行向量搜索,以高效导航图基数据。

方法论:

  • 分析种子实体问题:剖析查询以确定作为向量搜索起点的关键实体。

  • 探索并发向量空间:从不同的种子实体启动搜索,同时探索图。

  • 持续节点检索:基于向量相似性动态检索节点,扩展有针对性的搜索。

  • 示例应用:通过探索以不同实体为根的向量空间来识别描述类似于‘PaymentService’的服务。

c. 协调图算法的使用

目标: 选择并应用最合适的图算法以处理特定查询。

方法论:

  • 检查算法选择问题:分析查询的性质,以选择相关算法(如遍历、社区检测)。

  • 模块化应用:以模块化的方式应用算法,每个算法针对特定目标进行优化(例如效率、准确性)。

  • 解决依赖关系:确保算法之间的协调,特别是在一个算法的输出需要用于另一个算法的执行时。

  • 示例应用:使用社区检测和遍历算法找到紧密连接的服务子图。

LLM 编译器协调

一个 LLM 编译器可以作为这些任务的中央协调者。它将管理数学运算的 Cypher 查询的执行,监督并行向量搜索,并协调各种图算法的应用。LLM 编译器在此背景下的主要目标是确保知识图谱推理的整个过程不仅高效和准确,而且全面。这需要对数据结构有深入的理解,能够预测计算需求,并能根据实时数据分析结果动态调整策略。

2. 知识图谱作为模块化 LLM 工具

Kim 等人 2023 年提出的 LLM 编译器系统包括 3 个关键组件:

  1. LLM 规划器: 分解任务并构建依赖图

  2. 任务提取单元: 分派处理依赖的任务

  3. 执行者: 并行运行工具函数

值得注意的是,LLM 编译器将工具视为可以并发执行的模块化函数(当可能时)。

在这一范式的基础上,一个引人入胜的观点是将知识图谱本身视为模块化工具,由 LLM 进行协调:

  • 查询引擎通过知识图谱(KGs)成为 LLM 访问的工具

  • 图算法和嵌入提供了工具级定制

  • LLM 规划器确定最佳的多图探索策略

通过这种框架,LLM 编译器的自动化规划和结构化执行技术对于复杂知识检索显得非常有前景。

3. 由 LLM 编译器驱动的结构化推理

通过专门化 LLM 编译器技术来整合多个工具和执行图,我们可以提升知识图谱推理的多个方面:

a. 并行探索

规划分解将允许从多个“入口点”同时查询知识图谱的不同区域。多跳路径可以并行遍历,加速探索。

b. 模块化检索

针对独特子图的查询引擎使用独特的算法和嵌入,实际上充当了独立的工具。LLM 编译器在整合不同的模块能力方面表现出色。

c. 依赖管理

中间查询结果通常会影响后续的搜索。LLM 编译器对工具间依赖关系的自动处理能够实现实体和关系的无缝传播。

d. 递归重新规划

对于复杂问题,后续的检索阶段高度依赖于之前的阶段。LLM 编译器允许在更新的上下文揭示新依赖时进行递归重新规划。

e. 本体辅助规划

提供关键实体高层次模式的本体可以进一步协助规划模块。元级知识指导更加结构化的任务分解,符合领域概念。

e. 多样化数据源

规划器可以将额外的数据源,如 SQL 数据库、图像和互联网搜索引擎,整合为 LLM 可访问的模块化工具。

通过在本体知识驱动的结构化规划框架下协调知识检索模块,LLM 编译器可以实现对广泛信息空间的显著高效和精准的导航。

4. 知识吸收的操作系统

从宏观上看,我们可以设想 LLM 编译器技术会产生一个新范式——LLM 作为操作系统来监督各种知识功能。就像操作系统平衡线程和内存一样,LLM 可能会在知识存储中调度检索、推理和学习。基于 ML 的优化器将自动调整各种“工具”模块的路由和使用。

在这个生态系统中,LLM 编译器将充当关键的工作流协调框架,接口连接操作系统和工具。它将处理 LLM 操作系统代表下的所有依赖解析、并发优化和资源分配的复杂性。

LLM 作为操作系统

LLM 作为协调者,监督并在各个模块之间分配资源:

  • 为检索、推理和学习功能之间的最佳并发安排计划

  • 将查询和决策路由到最适合的组件

  • 监控执行以动态优化分配

  • 随着新能力的上线,实现模块化扩展性

语言模型作为语义粘合剂

语言模型提供了将不同组件联系在一起的共同语义表示:

  • 将查询、检索的上下文、生成的决策嵌入到共享向量空间

  • 在组件接口之间翻译查询和指令

  • 通过将输入/输出编码为可传输的嵌入,在线路之间传播信号

语言模型作为推理引擎

语言模型在专业组件的输入上进行元推理:

  • 通过基于更广泛的理解解决歧义来进行信号的语境化

  • 将低层次的输出抽象为更高层次的见解

  • 在概念基础上评估假设和解释

  • 确定解释性推论,连接相互关联的输出

语言模型因此在计算和语义上充当了狭义和广义 AI 之间的连接纽带。

由 Dall-E-3 生成的图像

结论

随着语言模型不断成熟为更通用的推理引擎,围绕它们的架构创新仍然至关重要。特别是,有效利用外部知识为克服固有模型局限性提供了一条途径。语言模型编译器及其协调模块功能的技术提供了一个有前景的范式,用于构建可扩展的知识检索结构。

在开发这些连接和大规模整合检索能力方面还有很多工作要做。

但通过将知识框定为由语言模型编译器协调的模块化工具,我们为在复杂推理中实现平衡的效率、简洁性和连贯性打开了有趣的可能性。

在这些方面的进展仍然至关重要,因为我们期望实现真正的系统智能。

机器学习风险管理中的组织过程

原文:towardsdatascience.com/organizational-processes-for-machine-learning-risk-management-14f4444dd07f

负责任的人工智能

组织过程是机器学习系统可靠性的一个关键非技术性决定因素。

Parul PandeyTowards Data Science Parul Pandey

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

--

作者提供的图片

在我们关于机器学习风险管理的持续系列中,我们已经研究了一些确保机器学习(ML)系统可信赖性的关键元素。在我们的第一篇文章中,我们详细讨论了 机器学习风险管理中的文化能力****t。其中提供的见解为我们当前的探索奠定了基础,因此,我强烈建议你在继续阅读本文章之前先阅读那部分内容。

## 机器学习风险管理中的文化能力

组织的文化是负责任的人工智能的一个重要方面。

towardsdatascience.com

在这篇第二篇文章中,我们将重点转向机器学习系统背景下的另一个重要元素:组织过程。虽然技术细节常常掩盖了这些过程,但它们是确保机器学习模型安全性和性能的关键。正如我们认识到文化能力的重要性一样,我们现在承认组织过程是构建机器学习系统可靠性的基础基石。

本文讨论了组织过程在机器学习风险管理(MRM)领域中的重要作用。整篇文章强调了从业者必须认真考虑、记录和主动解决其 ML 系统中已知或可预见的故障模式的关键性。

1️. 预测失败模式

在机器学习(ML)系统中识别和解决可能出现的问题至关重要,但将这一理念付诸实践需要时间和精力。然而,近年来,能够帮助 ML 系统设计师更系统地预测问题的资源显著增加。通过仔细整理潜在问题,使得在现实世界中让 ML 系统变得更强大、更安全变得更容易。在这种背景下,可以探索以下策略:

从过去的失败中学习

就像交通专业人员调查和编目事件以防止未来的发生一样,ML 研究人员和组织也开始收集和分析 A.I.事件。A.I.事件数据库是一个重要的存储库,允许用户搜索事件并获得宝贵的见解。开发 ML 系统时,咨询此资源至关重要。

如果类似的方法在过去曾导致事件,这就作为一个强烈的警告信号,表明新系统也可能存在风险,需要仔细考虑。

通过编目事件防止现实世界中的人工智能失败重复发生:人工智能事件数据库 | 来源: arxiv.org/pdf/2011.08512.pdf

解决想象力失败

克服人工智能系统开发和部署中的想象力失败 | 来源: arxiv.org/pdf/2011.13416.pdf

通常,人工智能(A.I.)事件源于 ML 系统操作中未预见或理解不足的背景和细节。论文中概述的结构化方法克服人工智能系统开发和部署中的想象力失败提供了对这些具有挑战性的未来风险进行假设的方法,并考虑了(包括投资者、客户和易受害者)、什么(涵盖福祉、机会和尊严)、何时(包括即时、频繁和长期情境)以及如何(涉及行动和信念改变)等方面。

虽然人工智能事件可能会对组织造成尴尬、昂贵,甚至非法的后果,但预见性可以缓解许多已知事件,可能会导致系统的改进。在这种情况下,实施的暂时延迟远远低于由于系统缺陷发布给组织和公众带来的潜在伤害。

2. 模型风险管理流程

21 页的 S.R. 11–7 模型风险管理指南的快照。该文档可免费公开访问 | 来源: www.federalreserve.gov/supervisionreg/srletters/sr1107a1.pdf

机器学习风险管理(MRM)构成了一个全面的框架和一系列程序,旨在识别、评估、缓解和监控与开发、部署和操作机器学习系统相关的风险。这些元素是治理结构的重要组成部分,特别是在美国联邦储备系统的模型风险管理监督指南(S.R. 11–07)的背景下,该指南监督用于重要消费者金融应用的预测模型。

文档是合规性变得具体的地方,使从业者承担责任并指导他们构建稳健的模型

虽然较大的组织可能更容易实现完整的 MRM,但较小的组织也可以提取有价值的见解。本节将 MRM 程序分解为更小、更易管理的组件,使其更易于理解和使用。

1. 风险分级

在评估机器学习系统部署中的风险时,常见的方法涉及计算重要性,考虑伤害的可能性和预期损失。高重要性的应用程序需要更多的关注。有效的治理适当地将资源分配给高、中和低风险的系统。例如,最近发布的Anthropic A.I.负责任的扩展政策引入了人工智能安全级别(ASL)的概念,这一概念在一定程度上受到美国政府生物安全等级(BSL)标准的启发,用于管理危险的生物材料。以下是一个快照:

来自 Anthropic 的 RSP 快照 | 来源:www-files.anthropic.com/production/files/responsible-scaling-policy-1.0.pdf

2. 模型文档

MRM 标准要求全面的系统文档,以满足多个目的,包括利益相关者责任、持续维护和事件响应。系统间的标准化文档简化了审计和审查流程,使其成为合规性的关键方面。文档模板指导数据科学家和工程师在模型开发过程中,确保所有必要步骤都得到执行,以构建可靠的模型。不完整的文档表明训练过程存在不足,因为大多数模板要求从业人员填写每个部分。此外,在最终模型文档中包括姓名和联系信息,有助于促进问责制。

MRM 文档中典型部分的粗略组合以及附件中推荐的 欧盟人工智能法案 | 图片由作者提供

详尽的模型文档可能令人望而却步;较小的组织可以选择像数据表这样的简单框架,模型卡可以帮助较小的组织实现这些目标。

L: 数据集数据表:来源:arxiv.org/pdf/1803.09010.pdf | R: 模型监控卡:来源:arxiv.org/pdf/1810.03993.pdf

3. 模型监控

机器学习(ML)安全的基础在于 ML 系统在现实世界环境中的固有不可预测性,因此需要对这些系统进行持续监控,从部署到退役。一个主要关注点是 输入漂移,当现实世界情况与静态训练数据发生偏差时,例如市场波动、监管变化或意外事件(如疫情)时,输入漂移会出现。这种偏差可能对系统功能构成威胁。

研究与生产中的机器学习。 | 幻灯片来自于‘安全可靠的机器学习:防止和识别故障’,在 ICLR-2019 上展示,可在 slideslive.com/38915708/safe-and-reliable-machine-learning-preventing-and-identifying-failures 查阅,采用 CCO 许可协议

高效的 ML 系统通过仔细监控数据和模型质量的变化来降低这些风险。这种监控不仅关注性能,还会查看异常数据或预测、安全问题和公平性问题。这有助于确保在不断变化的操作环境中进行全面的风险管理。

4. 模型清单

MRM 的核心是模型清单——全面的数据库,列出组织中所有的机器学习系统,并连接到监控策略、审计结果、系统维护记录和事件响应计划等重要资源。

任何部署机器学习的组织都应该能够回答像这样简单明了的问题:

任何部署机器学习的组织都应该能够回答这些简单明了的问题 | 图片来源:作者

对于任何重视利用机器学习的组织而言,维护一个强大的模型清单不仅是一个良好的实践——而是必不可少的。

5. 系统验证和过程审计

传统 MRM 实践的一个重要方面是,在机器学习系统发布之前,它会经过两个主要评估。首先,专家测试其技术部分,查找并修复问题。接着,一个团队确保系统遵循所有规则和指南,包括其设计和未来计划。如果这些系统有重大更新,还会再次审查。有些小公司可能觉得只能进行部分检查,但基本的想法是要有外部的人员来测试,确保它符合所有规则,并在使用重要系统之前获得批准。

在机器学习中,有效的 MRM(模型风险管理)意味着双重审查。

6. 变更管理

机器学习系统像其他复杂的软件一样,拥有各种组件,如后台代码、API 和用户界面。一个组件的变化可能会影响其他组件。数据模式的变化、新的隐私法律以及对第三方软件的依赖等挑战使得管理这些变化变得至关重要。在关键机器学习系统的早期阶段,规划这些变化是必要的。如果没有这种规划,可能会发生错误,例如未经许可使用数据或系统不匹配,并且这些错误可能在问题出现之前未被察觉。

结论

从 文化能力 到组织流程,我们探讨了确保机器学习系统安全性和性能的各种方面。正如我们所看到的,预测失败模式、细致的模型风险管理和警惕的监控是这一路径中的关键支柱。构建强健的模型、维护全面的清单和拥抱变更管理是实现负责任的人工智能的关键步骤。这些基础要素不仅确保了机器学习系统的安全性和性能,还赢得了用户和利益相关者的信任。

> 阅读本系列中的下一篇文章 >

[## 跨领域桥接:将金融、隐私和软件最佳实践融入机器学习风险管理

理解超越传统模型风险管理的策略

向数据科学迈进

参考资料与进一步阅读

使用 Pants 组织机器学习单一仓库

原文:towardsdatascience.com/organizing-a-machine-learning-monorepo-with-pants-8e0570de0c4c

MLOps

简化你的机器学习工作流管理

Michał OleszakTowards Data Science Michał Oleszak

·发布于 Towards Data Science ·阅读时间 20 分钟·2023 年 8 月 18 日

--

你是否曾经在项目之间复制粘贴公用代码块,导致不同仓库中存在多个版本的相同代码?或者,可能在更新存储数据的 GCP 存储桶名称后,你需要对数十个项目进行拉取请求?

上述情况在机器学习团队中经常发生,其后果从单个开发者的不满到团队无法按需交付代码等各不相同。幸运的是,有解决办法。

让我们深入探讨单一仓库,这是一种在谷歌等大型科技公司广泛采用的架构,了解它如何提升你的机器学习工作流。单一仓库提供了众多优势,尽管存在一些缺点,但它仍然是管理复杂机器学习生态系统的有力选择。

我们将简要讨论单一仓库的优缺点,研究为什么它是机器学习团队的优秀架构选择,并窥探大科技公司如何使用它。最后,我们将了解如何利用 Pants 构建系统将你的机器学习单一仓库组织成一个强大的 CI/CD 构建系统。

系好安全带,我们将开始简化你的机器学习项目管理之旅。

本文最初发布在 neptune.ai 博客

什么是单一仓库?

机器学习单一仓库。作者提供的图片,来源于 neptune.ai。

Monorepo(单体仓库的缩写)是一种软件开发策略,其中多个项目的代码存储在同一个仓库中。这个想法可以广泛到所有公司代码用各种编程语言存储在一起(有人说 Google 吗?),也可以狭窄到由一个小团队开发的几个 Python 项目放在一个仓库中。

在这篇博客文章中,我们关注于存储机器学习代码的仓库。

Monorepos 与 polyrepos

Monorepos 与 polyrepos 方法形成鲜明对比,在 polyrepos 中,每个单独的项目或组件都在其自己的仓库中。关于这两种方法的优缺点已经讨论了很多,我们不会过于深入探讨。我们只讨论一些基本内容。

Monorepo 架构。图片由作者提供,通过 neptune.ai。

Monorepo 架构提供了以下优势:

  • 单一 CI/CD 管道,意味着没有隐藏的部署知识分散在不同仓库的个体贡献者之间;

  • 原子提交,由于所有项目都存在于同一个仓库中,开发人员可以进行跨项目的更改,这些更改跨越多个项目但作为单个提交合并;

  • 跨项目轻松共享 公用程序和模板;

  • 轻松统一 编码标准和方法;

  • 更好的 代码可发现性

自然地,没有免费的午餐。我们需要为上述好处付出代价,而代价表现为:

  • 可扩展性挑战:随着代码库的增长,管理 monorepo 可能变得越来越困难。在非常大的规模下,你将需要强大的工具和服务器来处理克隆、拉取和推送更改等操作,这可能需要大量的时间和资源。

  • 复杂性:Monorepo 可能更难管理,特别是与依赖和版本控制有关。共享组件的更改可能会影响多个项目,因此需要额外的谨慎以避免破坏性更改。

  • 可见性和访问控制:由于每个人都在同一个仓库中工作,控制谁可以访问什么可能会很困难。虽然这本身不是缺点,但在代码受严格 NDA 保护的情况下,可能会引发法律问题。

是否值得支付 monorepo 提供的优势由每个组织或团队单独决定。然而,除非你在极其庞大的规模上运作或处理绝密任务,否则我认为——至少在我擅长的领域,即机器学习项目中——monorepo 是大多数情况下的良好架构选择。

让我们来谈谈原因。

使用 monorepo 的机器学习

至少有六个原因说明单一代码库特别适合机器学习项目。

  1. 数据管道集成

  2. 实验间的一致性

  3. 简化模型版本控制

  4. 跨职能协作

  5. 原子性更改

  6. 编码标准的统一

让我们逐一讨论这些方面。

数据管道集成

机器学习项目通常涉及预处理、转换和将数据输入模型的数据管道。这些管道可能与机器学习代码紧密集成。将数据管道和机器学习代码保存在同一代码库中,有助于保持这种紧密集成,并简化工作流程。

实验间的一致性

机器学习开发涉及大量实验。将所有实验保存在单一代码库中可以确保环境设置的一致性,并减少由于代码或数据版本不同而导致的实验间差异风险。

简化模型版本控制

在单一代码库中,代码和模型版本是同步的,因为它们被检查到同一个仓库中。这使得管理和追踪模型版本变得更加容易,这在机器学习可重复性至关重要的项目中尤其重要。只需查看任何时刻的提交 SHA,它即可提供所有模型和服务的状态信息。

跨职能协作

机器学习项目通常涉及数据科学家、机器学习工程师和软件工程师之间的合作。单一代码库通过提供所有项目相关代码和资源的唯一真实来源来促进这种跨职能协作

原子性更改

在机器学习的背景下,模型的性能可能依赖于数据预处理、特征提取、模型架构和后处理等各种互相关联的因素。单一代码库允许进行原子性更改——对多个组件的更改可以作为一个整体提交,确保依赖关系始终保持同步。

编码标准的统一

最后,机器学习团队通常包括没有软件工程背景的成员。这些数学家、统计学家和计量经济学家是拥有出色想法和训练解决业务问题模型能力的聪明人才。然而,编写干净、易读且易于维护的代码可能并不是他们最擅长的领域。

单一代码库通过自动检查和执行所有项目的编码标准,帮助确保高代码质量,并且帮助工程能力较弱的团队成员学习和成长。

行业中的做法:著名的单一代码库

在软件开发领域,一些全球最大、最成功的公司使用单一代码库。以下是一些值得注意的例子。

  • Google: Google 长期以来一直坚定支持单一代码仓库方法。他们的整个代码库,估计包含 20 亿行代码,都集中在一个庞大的仓库中。他们甚至发表了一篇相关论文

  • Meta: Meta 也为其庞大的代码库使用了单一代码仓库。他们创建了一个名为“Mercurial”的版本控制系统,以处理单一代码仓库的规模和复杂性。

  • Twitter: Twitter 已经长期使用 Pants 来管理他们的单一代码仓库,接下来我们将讨论这个构建系统!

许多其他公司,如 Microsoft、Uber、Airbnb 和 Stripe,也在部分代码库中使用单一代码仓库方法

理论讲解够了!让我们看看如何实际构建一个机器学习单一代码仓库。因为仅仅把曾经分开的仓库放到一个文件夹中是不够的。

如何用 Python 设置机器学习单一代码仓库?

在本节中,我们将围绕我为本文创建的示例机器学习仓库展开讨论。它是一个简单的单一代码仓库,仅包含一个项目或模块:一个名为 mnist 的手写数字分类器,以其使用的著名数据集命名。

目前你只需知道,在单一代码仓库的根目录下,有一个名为 mnist 的目录,其中包含用于训练模型的 Python 代码、相应的单元测试,以及一个用于在容器中运行训练的 Dockerfile。

我们将使用这个小示例来保持简单,但在更大的单一代码仓库中,mnist 仅是仓库根目录中的众多项目文件夹之一,每个文件夹至少会包含源代码、测试、Dockerfile 和需求文件。

构建系统:你为什么需要一个,以及如何选择?

为什么需要构建系统

想一想,除了编写代码,开发不同项目的各个团队在单一代码仓库中的开发工作流中还会采取哪些行动。他们会对代码运行静态分析工具,以确保符合风格标准,运行单元测试,构建例如 Docker 容器和 Python wheels 这样的工件,将它们推送到外部工件库,并将它们部署到生产环境。

以测试为例。你在维护的工具函数中进行了更改,运行了测试,所有测试都通过了。但是你如何确保你的更改不会破坏其他团队可能正在使用的代码?你当然也应该运行他们的测试套件。

但要做到这一点,你需要准确知道你所更改的代码在哪里被使用。随着代码库的增长,手动查找这点并不高效。当然,作为替代方案,你可以始终执行所有测试,但再次强调:这种方法并不是特别高效。

为什么你需要一个构建系统:测试。图像由作者提供,来自 neptune.ai。

另一个例子:生产部署。无论你是每周、每日还是持续部署,当时间到来时,你会构建 monorepo 中的所有服务并将其推送到生产环境。但,嘿,你需要在每次部署时全部构建吗?在大规模时这可能会耗时且成本高昂。

有些项目可能已经好几周没有更新了。另一方面,它们使用的共享工具代码可能已经进行了更新。我们如何决定构建什么?再次,这全关乎依赖关系。理想情况下,我们只会构建那些受最近更改影响的服务。

为什么你需要一个构建系统:部署。图像由作者提供,来自 neptune.ai。

所有这些在代码库较小的情况下可以通过简单的 shell 脚本处理,但随着规模的扩大和项目开始共享代码,挑战也随之出现,其中许多挑战都围绕着依赖管理。

选择合适的系统

如果你投资一个合适的构建系统,上述问题将不再存在。构建系统的主要任务是构建代码。它应该以一种聪明的方式来完成这项任务:开发者只需要告诉它构建什么(“构建受我最新提交影响的 Docker 镜像”或“仅运行那些涵盖了我更新的方法的代码的测试”),但如何进行则应该留给系统来处理。

有几个优秀的开源构建系统。由于大多数机器学习是用 Python 完成的,我们来关注一下支持 Python 最好的构建系统。在这方面最受欢迎的两个选择是 BazelPants

Bazel 是 Google 内部构建系统 Blaze 的开源版本。Pants 也受到 Blaze 的极大启发,并且它旨在实现与 Bazel 类似的技术设计目标。感兴趣的读者可以在这篇 博客文章 中找到对 Pants 和 Bazel 的比较(但请注意,这来自 Pants 的开发者)。monorepo.tools 底部的表格提供了另一种比较。

两个系统都很棒,我并不打算在这里宣布哪一个更好。话虽如此,Pants 通常被描述为更易于设置,更易于接触,并且对 Python 进行了良好的优化,这使得它非常适合机器学习 monorepo。

根据我个人的经验,决定使用 Pants 的关键因素是它活跃且乐于助人的社区。每当你有问题或疑虑时,只需在社区 Slack 频道发帖,许多支持者将很快帮助你解决。

介绍 Pants

好了,进入重点吧!我们将一步一步介绍不同的 Pants 功能及其实现方法。你也可以在这里查看相关的示例代码库。

设置

Pants 可以通过 pip 安装。在本教程中,我们将使用截至本文撰写时的最新稳定版本 2.15.1。

pip install pantsbuild.pants==2.15.1

Pants 可以通过一个名为pants.toml. 的全局主配置文件进行配置。在其中,我们可以配置 Pants 自身的行为以及它依赖的下游工具的设置,如 pytest 或 mypy。

让我们从一个最基本的pants.toml 开始:

[GLOBAL]
pants_version = "2.15.1"
backend_packages = [
    "pants.backend.python",
]

[source]
root_patterns = ["/"]

[python]
interpreter_constraints = ["==3.9.*"]

在全局部分,我们定义 Pants 版本和我们需要的后端包。这些包是 Pants 的引擎,支持不同的功能。作为起点,我们只包含 Python 后端。

在源部分,我们将源设置为代码库的根目录。从版本 2.15 开始,为了确保这一点,我们还需要在代码库的根目录下添加一个空的 BUILD_ROOT 文件

最后,在 Python 部分,我们选择要使用的 Python 版本。Pants 会在我们的系统中查找符合这里指定条件的版本,因此请确保你已经安装了这个版本。

这是一个好的开始!接下来,让我们来看一下任何构建系统的核心:构建文件。

构建文件

构建文件是用于以声明方式定义目标(要构建的内容)及其依赖项(它们工作所需的内容)的配置文件。

你可以在目录树的不同层级拥有多个构建文件。文件越多,对依赖管理的控制就越细粒度。实际上,Google 在其代码库中的几乎每个目录都有一个构建文件。

在我们的示例中,我们将使用三个构建文件:

  • mnist/BUILD — 在项目目录中,这个构建文件将定义项目的 Python 需求和要构建的 docker 容器;

  • mnist/src/BUILD — 在源代码目录中,这个构建文件将定义 Python 源文件,即需要进行 Python 特定检查的文件;

  • mnist/tests/BUILD — 在测试目录中,这个构建文件将定义哪些文件使用 Pytest 运行,以及这些测试执行所需的依赖。

让我们来看一下mnist/src/BUILD

python_sources(
    name="python",
    resolve="mnist",
    sources=["**/*.py"],
)

mnist/BUILD 看起来是这样的:

python_requirements(
    name="reqs",
    source="requirements.txt",
    resolve="mnist",
)

构建文件中的两个条目被称为目标。首先,我们有一个 Python 源代码目标,我们称之为python,虽然名称可以是任何东西。我们将 Python 源代码定义为目录中的所有 .py 文件。这是相对于构建文件的位置,也就是说:即使我们在mnist/src目录之外有 Python 文件,这些源代码只捕获mnist/src文件夹中的内容。还有一个 resolve 字段,我们稍后会谈论它。

接下来,我们有 Python 需求目标。它告诉 Pants 在哪里可以找到执行我们 Python 代码所需的需求(同样,相对于构建文件的位置,在这个例子中是 mnist 项目的根目录)。

这就是我们需要的所有信息。为了确保构建文件定义正确,让我们运行:

pants tailor --check update-build-files --check ::

正如预期的那样,我们得到: “未发现对 BUILD 文件的必要更改。”作为输出。很好!

让我们再花点时间了解一下这个命令。简而言之,裸的pants tailor可以自动创建构建文件。然而,它有时会添加过多的文件,这就是为什么我倾向于手动添加文件,然后使用上述命令检查其正确性。

结尾的双分号是 Pants 的表示法,它告诉 Pants 对整个 monorepo 运行该命令。或者,我们可以用mnist::替换它,以仅对mnist模块运行。

依赖项和锁定文件

为了进行高效的依赖管理,Pants 依赖于锁定文件。锁定文件记录每个项目所使用的所有依赖项的特定版本和来源。这包括直接依赖和传递依赖。

通过捕捉这些信息,锁定文件确保在不同环境和构建中一致地使用相同版本的依赖项。换句话说,它们作为依赖关系图的快照,确保在构建中的可重现性和一致性。

要为我们的mnist模块生成锁定文件,我们需要在pants.toml中添加以下内容:

[python]
interpreter_constraints = ["==3.9.*"]
enable_resolves = true
default_resolve = "mnist"

[python.resolves]
mnist = "mnist/mnist.lock"

我们启用了 resolves(Pants 对锁定文件环境的术语),并为mnist定义了一个,传递了一个文件路径。我们还将其设置为默认的。这是我们之前传递给 Python 源代码和 Python 需求目标的 resolve,这就是它们知道需要什么依赖项的方式。我们现在可以运行:

pants generate-lockfiles

以获取:

Completed: Generate lockfile for mnist
Wrote lockfile for the resolve `mnist` to mnist/mnist.lock

这在mnist/mnist.lock创建了一个文件。如果你打算在远程 CI/CD 中使用 Pants,该文件应该用 git 进行检查。而且,自然地,每次更新requirements.txt文件时,它都需要更新。

在 monorepo 中有更多项目时,你可能会选择有选择性地为需要它的项目生成锁定文件,例如 pants generate-lockfiles --resolve=mnist

设置完成了!现在让我们使用 Pants 做一些对我们有用的事情。

使用 Pants 统一代码风格

Pants 本身支持多种 Python linter 和代码格式化工具,如 Black、yapf、Docformatter、Autoflake、Flake8、isort、Pyupgrade 或 Bandit。它们的使用方式相同;在我们的示例中,让我们实现 Black 和 Docformatter。

为此,我们向pants.toml中添加两个适当的后端:

[GLOBAL]
pants_version = "2.15.1"
colors = true
backend_packages = [
    "pants.backend.python",
    "pants.backend.python.lint.docformatter",
    "pants.backend.python.lint.black",
]

如果我们想的话,可以通过在 toml 文件中添加额外的部分来配置这两个工具,但现在我们先使用默认设置。

要使用格式化工具,我们需要执行一个称为 Pants 目标的操作。在这种情况下,有两个目标相关。

首先,lint 目标将以检查模式运行这两种工具(按照它们在后端包中列出的顺序,所以 Docformatter 先,Black 后)。

pants lint ::
Completed: Format with docformatter - docformatter made no changes.
Completed: Format with Black - black made no changes.
✓ black succeeded.
✓ docformatter succeeded.

看起来我们的代码符合这两个格式化工具的标准!但是,如果不是这样,我们可以执行 fmt(“格式化”的缩写)目标,以适当地调整代码:

pants fmt ::

实际上,你可能会想使用多于这两个格式化工具。在这种情况下,你可能需要更新每个格式化工具的配置,以确保它与其他工具兼容。例如,如果你使用的是 Black 的默认配置,就像我们这里做的一样,它将期望代码行不超过 88 个字符。

但是如果你还想添加 isort 来自动排序你的导入项,它们会发生冲突:isort 会在 79 个字符后截断行。为了使 isort 与 Black 兼容,你需要在 toml 文件中包含以下部分:

[isort]
args = [
    "-l=88",
 ]

所有格式化工具都可以通过将参数传递给其底层工具的方式在pants.toml中进行配置。

使用 Pants 进行测试

让我们运行一些测试!为此,我们需要两个步骤。

首先,我们向pants.toml中添加相应的部分:

[test]
output = "all"
report = false
use_coverage = true

[coverage-py]
global_report = true

[pytest]
args = ["-vv", "-s", "-W ignore::DeprecationWarning", "--no-header"]

这些设置确保在运行测试时会生成测试覆盖率报告。我们还传递了一些自定义的 Pytest 选项来调整其输出。

接下来,我们需要回到我们的 mnist/tests/BUILD 文件,并添加一个 Python 测试目标:

python_tests(
    name="tests",
    resolve="mnist",
    sources=["test_*.py"],
)

我们将其称为 tests 并指定要使用的 resolve(即锁定文件)。Sources 是 Pytest 查找要运行的测试的地方;在这里,我们显式传递了所有以“test_”开头的 .py 文件。

现在我们可以运行:

pants test ::

获取:

✓ mnist/tests/test_data.py:../tests succeeded in 3.83s.
✓ mnist/tests/test_model.py:../tests succeeded in 2.26s.
Name                               Stmts   Miss  Cover
------------------------------------------------------
__global_coverage__/no-op-exe.py       0      0   100%
mnist/src/data.py                     14      0   100%
mnist/src/model.py                    15      0   100%
mnist/tests/test_data.py              21      1    95%
mnist/tests/test_model.py             20      1    95%
------------------------------------------------------
TOTAL                                 70      2    97%

正如你所看到的,运行这个测试套件大约花了三秒钟。如果我们再次运行它,我们将立即得到结果:

✓ mnist/tests/test_data.py:../tests succeeded in 3.83s (memoized).
✓ mnist/tests/test_model.py:../tests succeeded in 2.26s (memoized).

注意 Pants 如何告诉我们这些结果是被缓存的。由于测试、测试的代码或要求没有更改,因此无需实际重新运行测试——它们的结果是保证一致的,因此只是从缓存中提供。

使用 Pants 检查静态类型

我们再添加一个代码质量检查。Pants 允许我们使用 mypy 来检查 Python 中的静态类型。我们需要做的就是在pants.toml中添加 mypy 后端:pants.backend.python.typecheck.mypy

你可能还想配置 mypy 以使其输出更具可读性和信息量,可以包含以下配置部分:

[mypy]
args = [
    "--ignore-missing-imports",
    "--local-partial-types",
    "--pretty",
    "--color-output",
    "--error-summary",
    "--show-error-codes",
    "--show-error-context",
]

有了这个,我们可以运行 pants check :: 来获取:

Completed: Typecheck using MyPy - mypy - mypy succeeded.
Success: no issues found in 6 source files
✓ mypy succeeded.

使用 Pants 发布 ML 模型

让我们谈谈发布。大多数机器学习项目涉及一个或多个 Docker 容器,例如处理训练数据、训练模型或通过 Flask 或 FastAPI 通过 API 提供服务。在我们的玩具项目中,我们还有一个 用于模型训练的容器

Pants 支持自动构建和推送 Docker 镜像。让我们看看它是如何工作的。

首先,我们在 pants.toml 中添加 Docker 后端:pants.backend.docker. 我们还将对其进行配置,传递一些环境变量和一个构建参数,这在稍后会派上用场:

[docker]
build_args = ["SHORT_SHA"]
env_vars = ["DOCKER_CONFIG=%(env.HOME)s/.docker", "HOME", "USER", "PATH"]

现在,在 mnist/BUILD 文件中,我们将添加两个目标:一个文件目标和一个 Docker 镜像目标。

files(
    name="module_files",
    sources=["**/*"],
)

docker_image(
    name="train_mnist",
    dependencies=["mnist:module_files"],
    registries=["docker.io"],
    repository="michaloleszak/mnist",
    image_tags=["latest", "{build_args.SHORT_SHA}"],
)

我们将 Docker 目标命名为 train_mnist。作为依赖项,我们需要传递要包含在容器中的文件列表。最方便的方式是将此列表定义为单独的 files 目标。在这里,我们简单地将 mnist 模块中的所有文件包含在一个名为 module_files 的目标中,并将其作为依赖项传递给 Docker 镜像目标。

自然地,如果你知道容器只需要某些文件的子集,传递这些文件作为依赖项是个好主意。这一点很重要,因为这些依赖项被 Pants 用来推断容器是否受到了更改影响并需要重建。在这里,module_files 包括所有文件,如果 mnist 文件夹中的任何文件发生更改(即使是 readme!),Pants 会将 train_mnist Docker 镜像视为受到了这一更改的影响。

最后,我们还可以设置外部注册表和要推送镜像的仓库,以及推送时使用的标签:在这里,我将把镜像推送到我的个人 Docker Hub 仓库,总是带有两个标签:“latest”和短的提交 SHA,后者将作为构建参数传递。

有了这个,我们可以构建一个镜像。还有一件事:由于 Pants 在其隔离的环境中工作,它不能从主机读取环境变量。因此,要构建或推送需要 SHORT_SHA 变量的镜像,我们需要将其与 Pants 命令一起传递。

我们可以这样构建镜像:

SHORT_SHA=$(git rev-parse --short HEAD) pants package mnist:train_mnist

获取:

Completed: Building docker image docker.io/michaloleszak/mnist:latest +1 additional tag.
Built docker images: 
  * docker.io/michaloleszak/mnist:latest
  * docker.io/michaloleszak/mnist:0185754

快速检查显示镜像确实已被构建:

docker images
REPOSITORY            TAG       IMAGE ID       CREATED              SIZE
michaloleszak/mnist   0185754   d86dca9fb037   About a minute ago   3.71GB
michaloleszak/mnist   latest    d86dca9fb037   About a minute ago   3.71GB

我们还可以使用 Pants 一次性构建和推送镜像。只需将 package 命令替换为 publish 命令即可。

SHORT_SHA=$(git rev-parse --short HEAD) pants publish mnist:train_mnist

这将构建镜像并将其推送到我的 Docker Hub,它们确实已经到达了

Pants 在 CI/CD 中

我们刚刚手动在本地运行的相同命令可以作为 CI/CD 流水线的一部分执行。例如,你可以通过 GitHub Actions 或 Google CloudBuild 等服务来运行它们,比如在功能分支被允许合并到主分支之前作为 PR 检查,或者在合并之后验证其是否有效,并构建和推送工件。

在我们的玩具仓库中,我实现了一个预推送提交钩子,该钩子在 git push 时运行 Pants 命令,并且仅在所有命令通过时才允许推送。在其中,我们运行以下命令:

pants tailor --check update-build-files --check ::
pants lint ::
pants --changed-since=main --changed-dependees=transitive check
pants test ::

你可以看到一些用于pants check的新标志,这是使用 mypy 进行类型检查的功能。它们确保检查仅在与主分支相比发生了更改的文件及其传递依赖项上运行。这很有用,因为 mypy 的运行时间较长。将其范围限制在实际需要的部分可以加快处理速度。

在 CI/CD 管道中,docker 构建和推送会是什么样的?大致如下:

pants --changed-since=HEAD^ --changed-dependees=transitive --filter-target-type=docker_image publish

我们像以前一样使用 publish 命令,但增加了三个额外的参数:

  • --changed-since=HEAD^--changed-dependees=transitive 确保仅构建与先前提交相比受更改影响的容器;这对于在合并后的主分支上执行非常有用。

  • --filter-target-type=docker_image 确保 Pants 只做构建和推送 Docker 镜像的工作;这是因为publish命令可能会引用其他目标,例如,它可以用来将 helm charts 发布到 OCI 注册表。package命令也是如此:除了构建 docker 镜像,它还可以创建 Python 包;因此,传递--filter-target-type选项是一种良好的做法。

结论

单体仓库通常是机器学习团队的绝佳架构选择。然而,大规模管理它们需要对适当的构建系统进行投资。其中一个系统是 Pants:它易于设置和使用,并且提供对机器学习团队经常使用的许多 Python 和 Docker 功能的本地支持。

此外,它是一个开源项目,拥有一个庞大且乐于助人的社区。我希望在阅读完这篇文章后,你能尝试一下。即使你目前没有一个单体仓库,Pants 仍然可以简化和促进你日常工作的许多方面!

参考文献

感谢阅读!

如果你喜欢这篇文章,为什么不订阅电子邮件更新我的新文章呢?通过成为 Medium 会员,你可以支持我的写作,并获得对所有其他作者和我自己的故事的无限访问权。

想时刻掌握机器学习和人工智能领域日新月异的动态吗?查看我的新通讯,AI Pulse。需要咨询吗?你可以问我任何问题或在这里预约一对一咨询。

你还可以尝试我的其他文章。无法选择?可以看看这些:

## 开箱 DINOv2,Meta 的新型通用计算机视觉骨干网

视觉基础模型是否正在赶上大型语言模型?

towardsdatascience.com ## 自监督学习在计算机视觉中的应用

如何用仅有的几个标记样本来训练模型

towardsdatascience.com ## 如何通过假设检验检测数据漂移

提示:忘记 p 值吧

towardsdatascience.com

组织生成式人工智能:数据科学团队的 5 个经验教训

原文:towardsdatascience.com/organizing-generative-ai-5-lessons-learned-from-data-science-teams-2f271874ae4a?source=collection_archive---------4-----------------------#2023-08-25

大语言模型(LLMs)拥有巨大的潜力,但创造可持续的价值将需要的不仅仅是一个特种小组

shane murrayTowards Data Science shane murray

·

关注 发布于 Towards Data Science ·9 min read·2023 年 8 月 25 日

--

图片由作者提供

你做到了!

在执行领导层含糊其辞地向利益相关者承诺将把新的生成式人工智能功能整合到整个组织之后,你的特种小组 快速地生产了一个符合要求的最小可行产品(MVP)。将 OpenAI API 集成到你的应用程序中并不困难,而且它可能最终会变得有用。

那么现在会发生什么?虎队不可能永远疾驰。每个成员在组织内都有另一角色,这将再次占据他们的大部分时间和注意力。

更不用说,为了这个项目而忽视加快的典型流程和结构,这是有其原因的。事实证明,它们对确保产品适配性、从开发到运营的过渡以及成本优化(等等)非常关键。

仔细想想,现在项目完成了,实际上没有任何平台基础设施可以帮助扩展下一轮 LLM 模型或其他通用 AI 产品特性。

现在看来是时候开始考虑如何在您的数据组织内构建和支持通用 AI 团队了。尽管那些华丽的产品演示使过程看起来轻而易举,但未来可能会面临波涛汹涌的迹象:

  • 除非你是六大科技巨头之一,否则数据科学和通用 AI 专业知识都是稀缺的。到目前为止,没有人真正有任何重要的经验。对每个人来说都是新的。

  • 业务知道它想要通用 AI,但还不完全知道为什么。技术令人兴奋,但具体的使用案例还不明确。没有人有维护部署的多少经验。

  • 这个生态系统一夜之间蓬勃发展起来,但支持技术和最佳实践还没有成熟。风险通常是未预见的,不确定性很高。

如果这听起来很熟悉,那是因为确实如此。在过去五年左右,数据科学团队在其机器学习算法和应用程序中遇到了所有这些问题

这是一次痛苦的经历。2020 年,Gartner 报道显示,仅有 53%的机器学习项目从原型成功进入生产阶段 —— 这是在具有一定 AI 经验的组织中。对于仍在努力发展数据驱动文化的公司而言,这个数字可能更高,一些失败率估计甚至飙升至接近 90%。

作为曾在纽约时报领导数据团队并遇到许多这些挑战的人,我可以证明组织结构、流程和平台对这些倡议的成功有多重要。

我还与来自各种公司规模和行业的数百名数据领导交谈过,他们表达了一套共同的经验教训。这些通过数据科学团队的血汗和泪水赢得的最佳实践应该成为每位数据领导思考其长期通用 AI 战略和团队结构的首要考虑因素。

经验教训 1:理解结构性权衡和成熟度曲线

就像你不能在没有任何训练的情况下起床去跑马拉松一样,你的组织在加强其运营能力之前,无法建立一个与领先数据团队相匹配的生成 AI 组织结构。

我在这方面看到的最常见的错误之一是急于去分散化并在公司内部嵌入(也许是在一个数据网格),从而使人才资源过于分散。虽然这样可以更好地理解和接近业务,但持续创造价值是困难的。

可能会有一些成功的案例,但这些往往依赖于一两个自我驱动者的才能。数据科学人才稀缺,而能够独立识别、优先排序、沟通并执行高价值项目的高级数据科学人才更为罕见。

当那些有才华的个人离开时,机构知识、代码库和项目进展往往会随之离开。他们留下的团队成员被迫扮演考古学家的角色,试图解读这些被遗弃项目所留下的文物的目的和意义。大多数情况下,需要从接近零的状态重新开始。

图片由作者提供。

对于较小的数据科学和机器学习团队来说,咨询模型往往是一种更成功的方法。这种方法汇集了大量的人才,可以集中于最高优先级的项目。需要缓解的潜在缺点是,你要防止卓越中心变成一个产生教授可能喜欢的镀金模型的实验室,但这些模型与实际业务挑战不符。

随着团队规模的扩大和成熟度曲线的提升,稍有不同的组织结构会变得更加合适。“专业化”模型通常会将数据科学和机器学习资源集中于几个高价值的问题上,并在相关业务领域内部署团队。

这一现象最常见的表现是,当机器学习成为产品的核心部分(例如个性化或欺诈检测)时,与产品或工程团队的关联比与核心数据团队的关联更为重要。核心数据团队通常在数据科学方面有自己的投资,这与专业化团队大体独立。

这种模型可以成功,但确实会造成低效和孤岛现象。例如,中央团队和专业化团队通常会有定制的平台,几乎没有共享服务。产品领域中的流数据可能会从由中央团队收集的自定义数据中受益,但这种连接可能永远无法建立。

图片由作者提供。

另一种后期组织结构可以被描述为“平台”模型。嵌入式和专业化模型可能会因为缺乏跨业务领域的可见性和凝聚力而受到影响,每个数据科学问题都有其自己的全栈解决方案,尽管在解决的问题类型上存在固有的相似性。

解决方案是与业务领域或垂直行业有一些故意的分隔,以避免过度适配他们的运营模型,就像你对待其他横向平台团队一样。

将机器学习视为平台追求的一个主要好处是,一旦你展示了每个机器学习应用的价值,就可以投资于共享平台基础设施,因为这可以降低部署和维护新应用的资源和成本。这种投资相对于应用团队的投资初期应该较小,使他们能够相对独立地运营,追求业务伙伴的长期目标。

在这种平台模型中,可以创建一个 GenAI 团队作为应用团队之一,赋予其定制堆栈的任务和工程资源,以提供价值,同时与其他平台团队协调,以重用基础设施和标准,从而为组织提供持续的价值。我强烈建议采用这种模型,而不是试图将 Gen AI 分散到许多团队中。关键质量很重要。

课程 2:按用例组织而不是按业务职能组织

最近,我与一家媒体公司的一位数据领导进行了对话,这是这篇文章的灵感来源。他们告诉我,他们的数据科学团队按领域(在这种情况下是媒体属性)组织。

数据科学团队在每个领域内从事相同类型的项目,即文章推荐算法。毫无疑问,每个领域都受益于对其特定问题的专注,每个数据科学团队也受益于与各自业务和编辑合作伙伴的接近。但这有助于突出这种组织结构的一些缺点;人才部署效率低下,尽管许多团队解决的是相同类型的内容排名问题,但缺乏共享的基础设施。

在《纽约时报》,我们发现围绕共同问题组织数据科学团队是有效的。一旦在一个领域证明了模型的有效性,通常对其进行调整和修改以适应另一个领域的独特输入和约束比让两个团队并行创建两个模型更为高效。从逻辑上讲也是如此,构建原型总是比后续产品花费更多时间。

生成 AI 也应以同样的方式考虑。让团队专注于一个适合技术的高价值用例,例如为事件市场提供个性化座位建议,或为媒体网站进行语言本地化,然后将这一解决方案应用于其他适用领域。

课程 3:关注长期价值和难题

在技术和数据领域,“长期”有其独特的含义,其中首席数据官的平均任期大致与一罐花生酱相当。

当项目收官时,这一目标仍然会是个问题吗?五年后,它仍然有需求吗?在这段时间内,新模型是否能够迭代并发现附加价值?

现实是,假设你没有利用现成的模型,机器学习和生成 AI 项目可能会很昂贵(尽管 LLMs 正在迅速商品化)。开发一个经过良好训练和管理的模型以适应用例可能需要几个月,或者在某些情况下,甚至几年。

其他选择相比,提升效果最好是值得的。例如,一个旨在优化 Facebook 广告支出的机器学习模型可能听起来很吸引人,但你会发现这在广告平台内部本地完成。

尽管如此,关注长期价值并不意味着制定一个第一个发布计划在 2025 年的路线图。

课程 4:将 AI 团队与业务赞助者配对

那么,如何确保你的数据科学和生成 AI 团队专注于重要的业务问题呢?将他们与业务赞助者配对。

寻找新技术的创新应用不太可能是一个线性旅程,预计会有绕道。与业务赞助者建立强大的合作伙伴关系就像一个指南针,确保团队在探索前沿时不会偏离业务价值。我发现这也扩展了团队的视角,超越了横跨多个团队的问题。

图片来源:Jamie StreetUnsplash

一位强大的业务赞助者还将确保团队在整个旅程中得到充分支持,解锁资源并帮助导航任何涉及内部流程或政治的困难地带。这种导航的一部分可能需要跨团队对齐路线图,以提供一致的后端和前端体验。

由于这些举措可能会跨越多个季度,执行层的参与对于确保这些项目不会过早终止也至关重要。

课程 5:了解数据平台的前提条件

建造制造机器的机器总是比生产最终产品更困难。这一点在生产汽车的工厂和用于开发和生产大语言模型的数据平台中都是如此。

商业领袖总是会考虑商业目标,并经常忽视实现这些目标所需的数据平台投资。他们并不是恶意的,他们只是依赖你这个数据专家来告诉他们需要什么。

例如,机器学习团队已经投资建设或购买了特征存储和 MLops 解决方案。更不用说对云数据环境、数据质量及相关附加功能的基础投资了。

对于生成式 AI 计划,数据平台和数据管道架构的大部分内容将保持不变(如果你还没有投资现代数据栈,那么这是起点)。没有可发现的高质量数据就没有生成式 AI 项目。然而,还会有一些针对LLM 工程的额外解决方案,比如模型托管、缓存、AI 代理框架等,还有许多尚未被发明的解决方案。

从过去中学习或重蹈覆辙

毋庸置疑,生成式 AI 是一种颠覆性技术,学习如何大规模利用它将创造出一系列痛苦的经验教训。然而,不必从零开始。为了长期成功,结构化你的数据科学和生成式 AI 团队。

这篇文章由 Michael Segner 共同撰写。

在 Medium 上关注我,获取更多关于数据领导力、数据科学应用以及相关主题的故事。订阅,将我的故事直接送到你的收件箱。

其他机器学习术语:文本的稀疏和密集表示

原文:towardsdatascience.com/other-ml-jargons-sparse-and-dense-representations-of-texts-for-machine-learning-21fcd7a01410

其他机器学习术语

向量化的简要介绍及其在自然语言处理中的重要性

Nabanita RoyTowards Data Science Nabanita Roy

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

--

图片由 Compare Fibre 提供,来源于 Unsplash

介绍

矩阵和向量是机器学习(ML)算法进行模式学习和预测所需的量化信息。将这些技术应用于文本数据时,文本的数值表示被工程化为矩阵,以包含文本中的相关信息。“稀疏性”和“密集性”概念有助于高效设计和构建这些矩阵,用于人工智能领域中的所有高维数据处理用例。

向量表示在自然语言处理中的重要性

将文本数据表示为向量是应用机器学习技术进行预测、推荐或聚类的必要条件。在自然语言处理(NLP)中,“相似的单词出现在相似的上下文中”这一概念是基础的。让我们来看看:

  1. 文本分类用例中,如支持票据的分类、垃圾邮件检测、虚假新闻检测和反馈情感分析,具有相似单词的文本会被分类到特定类别中。

  2. 推荐系统中,具有相似个人资料信息、浏览历史和过去订单的人表示对产品有类似的选择或口味。这些信息用于生成推荐。

  3. 无监督聚类在文本中寻找模式和相似的单词,以便对文档和文章进行分组。典型应用包括新闻文章的分类、趋势分析和客户细分。

  4. 信息检索系统中,索引文档会与查询进行匹配,有时以“模糊”的方式进行,然后将匹配的文档集合返回给用户。此外,相似度度量用于对搜索结果进行排序。

因此,捕捉这些向量中的相似性是自然语言处理领域的一个主要研究方向。这些向量被投影到一个 N 维平面中,然后从 N 维空间中的这些向量中提取模式以对文本进行分类。有时会应用降维技术,如 PCA 或 t-SNE。向量的设计控制了基于文本的机器学习模型的整体性能,因此至关重要。

向量设计大致分为“稀疏”(意指很少填充)和“密集”(意指密集填充)两类。在本文中,我从数学的角度回顾了矩阵和向量的概念,然后讨论了这两种向量化技术——稀疏向量表示和密集向量表示,包括分别使用 Scikit Learn 和 Gensim 的演示。我还在文章末尾总结了这些表示的应用和实用性。

矩阵和向量的入门

在数学上,矩阵定义为一个二维矩形数字数组。 如果数组有 m 行和 n 列,那么它是一个 m × n 的矩阵。

如果一个矩阵只有一行或只有一列,它被称为向量。1×n 矩阵或向量是行向量(其中有 n 列但只有 1 行),而 m × 1 矩阵或向量是列向量(其中有 m 行但只有 1 列)。这里有一张清晰演示这一点的图片:

图片来源:数据科学线性代数 Ep1 — 使用 Python 介绍向量和矩阵

这是一个 标量、向量和矩阵的入门 和 数据科学中使用 Python 介绍向量和矩阵。

稀疏表示 | 矩阵 | 向量

图片由 Henning Witzel 提供,来自 Unsplash

在几乎所有实际应用中,基于计数的量化数值表示信息通常是稀疏的,换句话说,数值表示 只包含在海量数字中对你有用的一小部分。

这是因为,从直观上看,在文档集合中,只有作为文章、介词、连词和代词的单词被明显使用,因此具有较高的出现频率。然而,在一组体育新闻文章中,诸如‘soccer’或‘basketball’的术语,其出现次数有助于确定文章关联的运动类型,尽管出现次数较少,但频率并不很高。

现在,如果我们为每个新文章构造一个向量,假设每篇文章有 50 个单词,那么‘soccer’会出现约 5 次。因此,在 50 次中,45 次向量元素将为零,这表示我们关注的单词的缺失。因此,长度为 50 的向量中 90%是冗余的。这是one-hot 向量生成的一个例子。

稀疏矩阵生成的另一个典型例子是Count Vectorizer,它确定一个单词在文档中出现的次数。它为每个文档生成一个“计数向量”矩阵,从而构成一个d × v的矩阵,其中 d 是文档的数量,v 是文档集合中的单词或词汇的数量。

这里演示了 Count Vectorizer 的工作原理:

以下是‘demo’一词的四种不同含义,每种含义代表一个文档 ~

文档 1:演示一个产品或技术

文档 2:公开会议或游行,抗议某事或表达对政治问题的观点

文档 3:录制一首歌曲或音乐作品,以展示音乐团队或表演者的能力,或作为完整录音的准备

文档 4:演示软件或其他产品的功能

我使用了Scikit Learn 的 CountVectorizer实现来生成这四个“文档”的稀疏矩阵。以下是我使用的代码👩‍💻

图片来源:作者

_cv.toarray()的输出是4 × 34 数组中单词的数字表示(使用.toarray()从向量转换),如下图所示:

在这个 Jupiter Notebook 中找到这个例子 | 图片来源:作者

在矩阵中:

  • zero 表示完全没有出现(基本上没有信息)

  • 任何大于零的数值是单词在四个文档中出现的次数(一些有用的信息和一些冗余的信息)。

  • 34 是词汇表的大小(或文档中唯一单词的总数),因此形状为 4 x 34。

稀疏性的最简单衡量标准是零元素占总元素的比例,在这种情况下约为~

  • 零的数量 = 93

  • 数组中的元素数量 = 4 × 34 = 136

因此,数组中超过 50%的元素完全没有信息(93 个中的 136 个),但这是一个高维矩阵,需要更多的内存(增加的 空间复杂度)和计算时间(增加的 时间复杂度)。如果机器学习模型使用这个高维数据,它将发现很难处理。

  • 寻找模式

  • 为所有维度调整权重将花费过高。

  • 它最终会导致预测时的延迟问题。

稀疏性类似于维度诅咒的概念。推荐系统和协同过滤技术、计数数据表示,包括著名的 TF-IDF,都容易受到文本数据稀疏性相关的问题。

稠密表示 | 矩阵 | 向量

Mike LUnsplash 拍摄

出路?一种包含更多信息且冗余更少的表示,数学上定义为大多数元素非零的矩阵或向量。这种数据表示被称为稠密矩阵或稠密向量。它们通常比稀疏矩阵的尺寸更小。

使用稠密向量进行机器学习的优点

  1. 维度越小,优化机器学习模型的权重就越快、越容易。

  2. 尽管稠密向量的维度可能比稀疏矩阵小,但它们也可能足够大以挑战计算基础设施(想象一下 Word2Vec 或基于 BERT 的表示),但仍然包含丰富且有用的信息,如句法、语义或形态关系。

  3. 它们对文本元素之间关系的概括能力更强,这一点通过 word2vec 算法的成功得到了明确证明。

将稀疏表示转换为稠密表示的常见自然语言处理技术:

  1. Word2Vec: 这是最受欢迎的算法之一,使用浅层神经网络学习稠密表示,同时尝试预测可能的词汇并捕捉语义关系。

  2. FastText: 另一种使用浅层神经网络实现相同目标的算法,只是 FastText 使用了字符级 n-grams。然而,Word2Vec 被认为对英语效果最好,但 FastText 对形态丰富的语言如阿拉伯语、德语和俄语更为适用。此外,它在捕捉句法特征方面优于语义特征。

  3. GloVe: 同样!学习密集表示,但基于共现的概率。

这是一个简明的演示,展示如何使用Gensim 的 word2vec算法获取密集表示。

以下是词汇‘demonstration’的长度为 10 的密集表示(请注意vector_size参数对于word2vec模型的值是 10)。注意此数值表示中没有零。

图片来源:作者

通常,向量的尺寸越大,捕捉的知识就越好,特别是语义信息。这些对评估文本相似性和在无监督的文本处理技术中非常有用。

此外,以下是我们第一个文档“产品或技术的演示”中每个单词获得的密集向量的快照,这些向量按文档中出现的顺序排列。这次我没有清理文本,而是保持原样,因此列表中有七个词向量:

对文档‘产品或技术的演示’获得的长度为 10 的密集向量显示该数值表示中没有非零元素 |* 图片来源:作者

像这样的密集向量也可以在大型语料库上进行预训练,这些语料库通常可以在线访问。想象一下一个字典,像往常一样列出所有唯一的词汇,但其词汇意义被包含其数值表示的预训练词向量替代,颇似一个“量化字典”。这里有两个流行的“量化字典”可以下载并应用于文本处理任务:

  • Google:使用 Google 新闻数据集训练,包含约 1000 亿个词汇。300 维的词向量可以从这里下载。词汇表的大小为 300 万。

  • GloVe:使用 Wikipedia 文章训练,包含 50、100、300 和 500 维的词向量,可以从这里下载。词汇表的大小是 400k。

结论

传统上,一维热编码向量、词频矩阵(或计数向量)和 TF-IDF 评分(稀疏表示)被用于文本分析。它们并不保留任何语义信息。然而,现代方法通过神经网络(如 Word2Vec)或更复杂的统计方法和归一化技术(如 GloVe)来获取词嵌入,更好地保留了词的“意义”,并使我们能够对出现于类似上下文中的词进行聚类。但这些方法也带来了计算时间更长的复杂性,这使得它们在扩展时成本较高(取决于学习超参数,特别是更高的向量表示大小)。此外,它们也更难以解释。

照片由Michał Parzuchowski拍摄,发布在Unsplash

在所有现实世界的案例中,这两种方法都是适用的。例如,在著名的垃圾邮件分类数据集中,稀疏表示方法可以获得接近完美的模型表现。在这种情况下,我们无需计算稠密向量嵌入即可获得更好的性能,因为我们用简单透明的方法就能成功实现目标。

因此,推荐的文本处理任务起点是使用基于频率的词袋模型,该模型生成稀疏向量。对于这样的管道,清理和整理文本数据至关重要。通过合适的特征和词汇工程,它们在速度和性能方面可能是最有效的,特别是当不需要语义信息时。

这是 Jupyter Notebook ,其中包含完整的 Python 演示,展示了如何为相同示例获取稀疏和稠密向量(使用 Gensim 的 Word2Vec)。

💡 想了解更多关于自然语言处理的矩阵设计吗?这里有一篇文章可以进一步学习:

## 自然语言处理中的向量空间模型矩阵设计

自然语言处理中的知识表示简要哲学和分布式词矩阵设计的简明指南……

towardsdatascience.com

💡 想要实现 Word2Vec 用于文本分类?这里有一个关于多类文本分类的实用教程,讲解了如何使用 Keras 的嵌入层以及 Gensim 的 Word2Vec 算法学习稠密表示:

## 使用 Keras 进行多类文本分类:有词嵌入与无词嵌入的比较…

词嵌入是否为文本分类模型增加价值?让我们在这个多类预测任务中来探讨一下…

towardsdatascience.com

参考资料:

  1. machinelearningmastery.com/sparse-matrices-for-machine-learning/

  2. kavita-ganesan.com/fasttext-vs-word2vec/#.Y-OP7XbP02w

感谢访问!

我的链接: Medium | LinkedIn | GitHub

我们的 MLOps 故事:为十二个品牌提供生产级机器学习

原文:towardsdatascience.com/our-mlops-story-production-grade-machine-learning-or-twelve-brands-a8727fd56c94

在荷兰 DPG Media 以有限资源构建 MLOps 平台的过程中,我们学到的东西

Jeffrey LuppesTowards Data Science Jeffrey Luppes

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

--

部署一个机器学习模型一次是一个直接的任务;重复将机器学习模型投入生产则要困难得多。为了应对这一复杂过程,MLOps(机器学习运维)的概念应运而生。MLOps 代表了 DevOps、机器学习和软件工程实践的融合。这里有几个细微之处,但 MLOps 具体包含什么的更好定义仍在讨论中,也是供应商推销其产品的战场。为了简洁起见,我宁愿继续讲述我们的 MLOps 故事。

我们的 MLOps 之旅始于 2021 年 9 月。我们的团队仅在六个月前成立,我们开始时接手了少量继承的项目。我们团队的目标在纸面上很简单:我们要为在线服务提供数据/机器学习平台,这是 DPG Media 的一部分,专注于网站和社区。我们的“投资组合”包括十多个品牌,其中包括:一个受欢迎的科技新闻网站及其社区、两个招聘门户网站、一个期待父母的社区、几个可以购买二手车的网站,以及几个具有类似受众的网站。这一点对本文的其余部分很重要。

当我们开始构建平台时,我们计划支持这十二个(荷兰)品牌。

当时,机器学习团队只有一名数据科学家和我,一名机器学习工程师。我们两人相隔两周被聘用,对我们被期望投入的组织都是全新的。

当时,我们的机器学习生产环境(我宽松地使用这个术语)如下所示:

  • 一个建议工作,为用户提供建议,通过 Airflow 在 Spark 上运行,适用于品牌 #1。

  • 一个给汽车定价建议的模型,同时作为 Airflow 的批处理作业运行,用于品牌 #2。

  • 一个针对品牌 #3 的 Databricks 目标受众的模型,作为计划好的笔记本再次作为批处理作业运行。

  • 一个在 EC2 上运行的单个 Flask 应用程序,将各种模型加载到内存中,以便演示和展示各个品牌的项目。

面对十二个品牌,每个品牌可能在自己的系统和云上,我们不能对我们开发的任何东西如何被使用做出强烈假设。

两位现已离职的数据科学家完成了所有先前的项目。他们的大多数项目都存在于 Jupyter Notebooks 中,服务它们的 Flask 应用程序托管在 EC2 上 — 且经常宕机。一个单一的 MLflow 服务器在 EC2 上运行,但已过时,并且存在一些安全问题。Databricks 项目只是一个单一的笔记本,甚至没有版本控制。我们甚至不知道谁为我们安排作业的集群付费。

我们的团队需要扩展,承担更多项目,并仅花少量时间支持旧项目。我们无法继续以原有方式运作,特别是当我们希望进行实时推理时。机器学习的良机开始出现,我们的客户要求实时模型。对 MLOps 的结构化方法的必要性变得明确。

制定计划

我们可以依赖数据工程师(我们的团队有四名专职数据工程师)、架构师和 DPG 的系统工程师的帮助,但显然我们应该自己掌握平台。特别是我们的数据工程师已经捉襟见肘,面临着自己的截止日期和迁移。未来我们需要自己管理和更新我们的系统。我们必须亲自掌控。

我们在团队内部组织了几次头脑风暴会议,并采访了组织中的几位架构师。事情逐渐变得更为确定,而不是像之前那样在我们办公室五楼的一个阴暗角落的白板上涂涂画画。我们想给 MLflow 另一个机会,并尽可能地将所有内容保留在 AWS 上。毕竟,AWS SageMaker 显然在向更成熟的平台发展。我们的一位团队负责人敦促我们尽早适应 Terraform(或某种形式的 IaC)。由于我们的数据工程师已经在使用 Terraform,这成为了我们平台的基石。

拍打白板 这个家伙只能容纳这么多功能。图片由作者提供。

我加入了 MLOps 社区以了解更多工具,并与供应商进行了几次对话。开始参加聚会。幸运的是,我们还有一个 AWS 企业支持团队可以帮助我们,因为我们已经是 AWS 的大用户。很多想法,可能太多了,开始涌入。MLOps 场景(现在仍然是)有点混乱,这在预期之中;对我们来说,重要的是快速行动并决定最重要的事情。

我们最终决定平台的原则大致如下:

  • 用 Terraform 做所有事情,除非我们不能..

  • 尽量遵循数据网格(我们组织在 2021 年采纳了),除非..

  • 尽可能使用托管服务,除非..

  • 转向无服务器,除非..

  • 尽可能避免使用 Kubernetes

  • 随着进展 建设,并在我们重新访问旧项目时迁移它们

模型工件和实验跟踪以及我们的第一个 MLOOPS

尽管有意图仅在需要时迁移,但我们尝试的首件事之一是将 MLflow 服务器迁移到 AWS Fargate,并将数据库放在 AWS RDS 上,而不是与服务器运行在同一个 EC2 实例上。根据业务单元,我们决定托管一个实例,测试和生产用不同的服务器。这样,四个业务单元意味着 4 x 2 几乎相同的设置。

MLflow 在 AWS 上的示例架构。取自 aws.amazon.com/blogs/machine-learning/managing-your-machine-learning-lifecycle-with-mlflow-and-amazon-sagemaker/

这证明是一个相当昂贵的想法(8 份这个设置,每个都有自己的负载均衡器和数据库!),而我们正在做的项目数量远远不足以支撑它。后来我们将其减少到两个实例。像这样大规模启动也意味着要直接解决 terraform、弹性负载均衡器、IAM 和 route 53。学习曲线相当陡峭,花了很多时间熟悉 AWS 的各个部分。

我们的第一个 ML-OOPS 尝试在需要之前设置大量(昂贵的)实例并维护它们。

对于实验(即 Jupyter 笔记本),我们决定使用 AWS SageMaker 的 Notebook 实例。这些笔记本实例配备了一个生命周期脚本,会将正确的 MLflow uri 设置为环境变量。我们创建了一些脚本来批量更新这些实例,以及监控笔记本本身,比如在有人将实例留在办公时间外时在我们的 Slack 频道中发出警报。

模型部署和训练

很快就到了部署我们的第一个模型的时候。我们使用了 SageMaker 的模型端点,并决定使用 Lambda 和 API 网关的设置。SageMaker 本质上可以作为一个一站式部署服务,尽管相比于自己启动 EC2 实例并托管模型,费用更高。相比于自己管理的 Kubernetes 集群,费用也更高。不过,你会得到很多回报。SageMaker 处理各种部署策略、自动扩展,并在需要时允许我们选择 GPU 类型。

我们通过 SageMaker SDK 轻松地从本地和云环境(例如 Jupyter notebook 或 Airflow)部署模型。AWS Lambda 为模型的输入和输出提供了额外的控制,而 API Gateway 提供了一个可以用 API 密钥供用户使用的 restful 接口。除了模型本身,所有元素都通过 terraform 进行部署,添加新模型只需添加一个 terraform 模块,主要指定名称和从哪里提取 Lambda 的代码。这也意味着我们可以改进模型的(前)处理,而无需更改 SageMaker 端点,并为 lambdas 单独设置 CI/CD。

AWS 上的典型部署。图像由作者提供。

训练任务也委托给了 SageMaker。为我们的前几个 ML 训练任务(使用 TensorFlow 构建的文本分类 LSTM 模型)创建模板并从训练任务容器内部将其记录到 MLflow 上,花费了相当大的精力。我们为训练任务制作了一个通用模板。SageMaker 对接收训练任务有相当明确的要求,这意味着你需要遵循平台的某些约定——即使在使用 TensorFlow 时也是如此。幸运的是,在后台,SageMaker 的模型服务仍然使用 TensorFlow Extended 处理 TensorFlow 模型,因此与 SavedModels 有一定的直观操作性。

我们通过 Airflow 协调训练任务,并明确不包括任何代码合并后的再训练。我们的一些模型相当昂贵,一些则不然,但几乎所有模型都需要大量计算或存储。如果在计划运行之前需要,我们可以简单地触发 dag 并运行管道。

监控和警报

我们 AWS 购物清单上的最后一项是监控和警报。我们首先尝试了 Amazon Managed Prometheus 和 Amazon Managed Grafana,希望能够将 CloudWatch 中的数据导入这些工具中,从而节省 CloudWatch 的成本。事实证明,这可以通过 exporter 工具实现。我们瞄准了 YACE(yet-another-cloudwatch-exporter),但它需要存放在某个地方。这个地方很快就会是 EC2,然后是 ECS。

我们还需要跟踪来自一个业务单位的一些指标。这意味着我们需要某种接口来与之互动。起初,这似乎可以通过 Managed Prometheus 的 Remote Write 功能实现,但我们需要更多控制,因此我们还是设置了 YACE(Prometheus 每五分钟抓取一次)。我们决定将 YACE 和 Prometheus 移动到 ECS 集群,并设置 Remote Write 加 pushgateway 来接收来自我们环境之外的指标。最后,我们放弃了 Amazon Managed Prometheus。

不幸的是,YACE 并不支持我们使用的所有 AWS 服务。我们缺乏 SageMaker 的导出功能,对我们的模型端点一无所知。幸运的是,Amazon Managed Grafana 实例也从 CloudWatch 中提取统计数据,虽然这需要支付额外的费用。

在 Amazon Managed Grafana 中,我们创建了一个通用仪表板,通过将 json 模型参数化,将其转换为模板。一旦完成,我们通过 terraform 推出了每个模型的仪表板。不幸的是,Amazon Managed Grafana 需要一个 API 密钥,以便我们的 terraform 和 ci/cd 正常运行,该密钥的最大有效期为 30 天。我们设置了一个密钥轮换 Lambda,每 29 天销毁并重新创建一个密钥,并将其存储在一个 AWS 秘密中,我们可以在 terraform 代码中请求它。这样,当部署模型时,我们现在可以在几秒钟内自动生成一个 API、监控和日志记录以及一个自定义仪表板。

由于 Grafana 也可以设置在指标超过某个阈值时发送警报,这种设置还允许我们在出现问题时触发警报,并将其转发到 Slack 或 OpsGenie。由于我们仍然只有两个人,我们制定了一个值班计划,每人轮流值班一周。诀窍是从不定义高优先级的警报。

结果

从一瞥来看,组成“部署”平台的最重要服务。图片来源:作者。

我们最终的“框架”相当轻量级,细心的读者会发现它实际上并没有努力实现完全的端到端自动化。目前我们有大约 15 个模型部署用于实时推断,一年下来仍由两个人组成的团队维持。上面的图片是 AWS 为中心的视图,而下面的蓝图,使用了 AI 基础设施联盟 的模板,提供了迄今为止堆栈功能的概述。

我们的 MLOps 堆栈。图片来源:作者。

我们希望保持灵活性,觉得使用一个更重的“为我们做所有事情”的框架可能会更具限制性和更昂贵。我们尽量不做强假设。例如,并不是每个项目都有新数据进入或有来自生产的反馈。我们可能不被允许随时存储预测结果。并不是每个模型都部署在 SageMaker 上(有些可以很好地运行在 Lambda 中!)。

就像 Lak Lakshmanan 最近的帖子(标题引发关注的“不,你不需要 MLOps”)以及现在著名的MLOps without much Ops博客系列(“你不需要更大的船”),我们有一个努力保持简单的平台。

也就是说,如果你想保持灵活性、节省时间、预算或复杂性。

尽管如此,在构建平台时我们确实有很多需求。展示了我们需求映射的 ML 生命周期的高层次图表显示,我们现在覆盖了大部分需求。

我们希望在平台中拥有的功能以及我们已覆盖的功能的可视化。请注意,我们在这篇博客文章中没有讨论所有功能。

向前思考

现在我们已经讨论了我们构建了什么以及它如何融入更大的图景。接下来我们要做什么?

该平台在测试和验证方面仍然存在盲点。除了偶尔的测试外,我们没有正式的框架。提前构建这些框架并确保不进行过早优化相对困难,尤其是在压力下。同时,从软件开发的角度来看,这是我们确实无法缺少的。

作为一家媒体公司,我们有处理文本、图像、点击流、图形、向量和表格数据的模型。一些模型处理多种数据类型。模型可以包括从 XGBoost 和随机森林到 Transformer 及各种递归神经网络和卷积神经网络的任何东西。我们如何希望构建或购买能够测试所有数据类型和模型的东西呢?

另一个需要解决的问题是减少 Lambda 函数的冷启动时间。Lambda 函数是在被调用时运行的服务函数。第一次调用后,Lambda 会保持活动状态约 15 分钟,除非有其他调用跟随,最长寿命约为两小时。在首次分配资源和镜像时会出现冷启动。有时这只是几秒钟,但如果在 Lambda 中添加 TensorFlow 导入,你可能会看到你的 API 超时。

这是使用 Lambda 的生活事实——但这意味着我们或 API 的用户需要具备错误处理机制,以应对运行时间过长的问题。虽然建议不要在低流量的情况下使用 Lambda,但它仍然是我们所拥有的最便宜的选择,并且有助于抵消 SageMaker 的成本溢价。此外,它们非常容易维护。然而,冷启动是否真正成为问题,完全取决于业务背景和请求量。

我希望将来能离开我们自托管的 MLflow。它没有基于角色的访问控制。这意味着每个用户都可以查看(和删除)每个模型,并且用户需要滚动浏览可能上百个模型才能找到他们需要的。使用它还需要认知负担;任何使用它的数据科学家都必须主动注意设置实验和调用诸如 mlflow.log 或 mlflow.autolog 的函数。由于我们没有使用从 MLflow 部署到 SageMaker 的选项,我们可以切换到这个领域中的其他工具之一。我们实际上只是将 MLflow 用作跟踪过去模型运行的一种方式。

结束语

总结来说,本文介绍了我们创建适合我们需求的 MLOps 堆栈的过程。我们使用了 AWS 服务和开源工具来选择一套工具,以处理我们大部分的用例。随着团队的发展,这个堆栈将继续演变。我们的主要收获是:

  • 选择托管服务,别回头看。 如果你的团队相对较小,选择托管服务是个很好的主意,这样你就不会有同样的管理开销。

  • 获取 Sagemaker。 Sagemaker 非常适合小型团队,并且在部署方面表现出色(虽然更新方面表现一般,这个以后再说),但熟悉它需要一些时间。

  • 顺其自然。 Airflow 和 MLFlow 是机器学习堆栈中的好工具,因为它们允许编排和机器学习记账,这使你能够专注于最重要的工作。

  • 基础设施即代码是云计算的 10 倍放大器。 说真的,Terraform 为我们节省了大量工作,真是太疯狂了。

我希望这种完全开放的讨论能帮助其他团队决定他们的技术栈并评估他们的需求。希望未来我们使用的平台和工具能够变得更加成熟,并且更好地集成。

本文得到了 Gido SchoenmackerJoost de WitKim SterenborgAmine Ben Slama 的帮助。

Jeffrey Luppes 是位于荷兰阿姆斯特丹的 DPG Media 的机器学习工程师。

单变量数据集中的分布拟合异常值检测

原文:towardsdatascience.com/outlier-detection-using-distribution-fitting-in-univariate-data-sets-ac8b7a14d40e

学习如何使用概率密度函数检测异常值,以实现快速、轻量级的模型和可解释的结果。

Erdogan TaskesenTowards Data Science Erdogan Taskesen

·发表于数据科学前沿 ·阅读时间 16 分钟·2023 年 2 月 18 日

--

图片由Randy Fath提供,来自 Unsplash

异常或新奇检测适用于各种需要明确早期警告的异常情况,例如传感器数据、安全操作和欺诈检测等。由于问题的性质,异常值不常出现,并且由于缺乏标签,创建监督模型可能变得困难。异常值也被称为异常或新奇,但在基本假设和建模过程上存在一些根本差异。在这里,我将讨论异常和新奇之间的根本差异以及异常值检测的概念。通过一个实际的例子,我将演示如何创建一个无监督模型,用于使用概率密度拟合来检测单变量数据集中的异常和新奇。 distfit 库在所有示例中都被使用。

如果你觉得这篇文章有帮助,可以使用我的 推荐链接 继续无缝学习,并注册 Medium 会员。此外, 关注我 以保持对我最新内容的关注!

异常还是新奇?

异常和新颖性都是与标准、正常或预期的情况偏离的观察。对于这种观察的集体名称是异常值。一般来说,异常值呈现在分布的(相对)尾部,远离其余的密度。此外,如果你观察到某个值或小范围值的密度出现大幅尖峰,这可能指向潜在的异常值。虽然异常和新颖性检测的目标是相同的,但在概念建模上存在一些差异 [1],简要总结如下:

异常是已知存在于训练数据中的异常值,并且偏离正常或预期的情况。 在这种情况下,我们应该旨在对具有预期/正常行为的观察值(也称为内点)进行建模,并忽略偏离的观察值。那些超出预期/正常行为的观察值即为异常值。

新颖性是指训练数据中未曾出现的异常值。数据中不包含偏离正常/预期的观察。 新颖性检测可能更具挑战性,因为没有异常值的参考。在这种情况下,领域知识更为重要,以防止模型对内点过拟合。

异常值可以分为三类。

我刚刚提到异常和新颖性之间的区别在于建模过程。但不仅仅如此。在我们开始建模之前,我们需要对“异常值应该是什么样的”设定一些期望。异常值大致可以分为三类(图 1),总结如下:

  • 全局异常值(也称为点异常值)是单个独立的观察值,与所有其他观察值偏离[1, 2]。当提到“异常值”时,通常指的是全局异常值。

  • 上下文异常值发生在特定观察值在特定上下文中不符合预期时。上下文可以表现为双峰或多峰分布,而异常值在上下文中偏离。例如,冬季气温低于 0 度是正常的,但在夏季则不寻常,因此被称为异常值。除了时间序列和季节性数据,其他已知应用还包括传感器数据[3]和安全操作[4]。

  • 集体异常值(或称为群体异常值)是一组具有不寻常行为的相似/相关实例,与数据集中的其他数据点相比具有不同的表现[5]。这类异常值群体可能形成双峰或多峰分布,因为它们通常表示与个体异常值不同的问题类型,例如批处理错误或数据生成过程中的系统性问题。请注意,集体异常值的检测通常需要不同于个体异常值的检测方法。

图 1. 从左到右展示了全局异常值、上下文异常值和集体异常值的示例。图片由作者提供。

在开始建模异常值之前,还有一个需要讨论的部分是数据集部分。从数据集的角度来看,异常值可以根据单一特征(单变量)或每个观察值的多个特征(多变量)进行检测。继续阅读,因为下一部分是关于单变量和多变量分析的。

异常值可以以单变量或多变量方式建模。

检测任何类型异常值的建模方法有两个主要类别;单变量和多变量分析(图 2)。我将重点讨论单变量随机变量的异常值检测,但在此之前,我将简要描述它们的区别:

  • 单变量方法是指使用一个变量一次性标记样本/观察值为异常值,例如,一个人的年龄、体重或时间序列数据中的单一变量。在这种情况下,分析数据分布适合于异常值检测。

  • 多变量方法是指样本/观察值包含多个特征,可以一起分析,例如年龄、体重和身高。它适合于检测具有(非)线性关系或每个变量的值分布(高度)偏斜的异常值。在这些情况下,单变量方法可能效果不佳,因为它未考虑变量之间的关系。

图 2. 单变量与多变量分析在异常值检测中的概述。图像由作者提供。

使用分布拟合进行单变量异常值检测。

在单变量数据集中,有多种(非)参数化的异常值检测方法,如 Z 分数、Tukey 围栏和基于密度的方法等。这些方法的共同点是建模底层分布。因此,distfit库[6]特别适合于异常值检测,因为它可以确定单变量随机变量的概率密度函数(PDF),还可以使用百分位数或分位数以非参数方式建模单变量数据集。此外,它可以用于建模任何三类异常值:全局、上下文或集体异常值。有关使用distfit库[6]进行分布拟合的更详细信息,请参见这篇博客。建模方法可以总结如下:

  1. 计算随机变量在不同 PDF 中的拟合度,然后使用拟合优度检验对 PDF 进行排名,并通过自助法评估。注意,可以使用带分位数或百分位数的非参数方法。

  2. 直观检查直方图、PDF、CDF 和分位数-分位数(QQ)图。

  3. 根据步骤 1 和 2 选择最佳模型,但也要确保(非)参数模型(例如,PDF)的属性与使用案例相匹配。选择最佳模型不仅是一个统计问题;也是一个建模决策。

  4. 使用(非)参数模型(如 PDF)对新的未见样本进行预测。

连续随机变量的新颖性检测。

我们从一个简单而直观的例子开始,展示如何通过分布拟合和假设检验来实现单变量的新颖性检测在这个例子中,我们的目标是追求一种新颖的方法来检测全球异常值,即数据中不包含偏离正常/预期的观察值。这意味着,在某些时候,我们应该仔细融入领域知识,以设定异常值的边界。

假设我们有 10,000 人的身高测量值。让我们生成一个mean=163std=10的随机正态数据,代表我们的人类身高测量值。我们期望一个包含两个尾部的钟形曲线;那些身高比平均值小或大的数据。请注意,由于随机成分,重复实验时结果可能会略有不同。

# Import library
import numpy as np

# Generate 10000 samples from a normal distribution
X = np.random.normal(163, 10, 10000)

1. 确定最佳拟合人类身高的 PDF。

在检测任何异常值之前,我们需要拟合一个分布(PDF),以描述人类身高的正常/预期行为。distfit 库可以拟合多达 89 种理论分布。我将把搜索限制在常见/流行的概率密度函数,因为我们预计会有一个钟形曲线(见以下代码部分)。

# Install distfit library
pip install distfit
# Import library
from distfit import distfit

# Initialize for common/popular distributions with bootstrapping.
dfit = distfit(distr='popular', n_boots=100)

# Estimate the best fit
results = dfit.fit_transform(X)

# Plot the RSS and bootstrap results for the top scoring PDFs
dfit.plot_summary(n_top=10)

# Show the plot
plt.show()

图 3. 人类身高与最常见分布的 RSS 分数拟合情况。

loggamma PDF 被检测为人类身高的最佳拟合,依据是拟合优度检验统计量(RSS)和自助法。请注意,自助法评估了 PDF 是否存在过拟合。自助得分范围在[0, 1]之间,描绘了 PDF 的自助数量(n_bootst=100)的拟合成功率。也可以从图 3 中看到,除了loggamma PDF 外,还有多个其他 PDF 也被检测出低残差平方和,即Beta, Gamma, Normal, T-分布, Loggamma, 广义极值, 和 Weibull 分布(图 3)。然而,只有五种 PDF 通过了自助法。

2: 视觉检查最佳拟合的概率密度函数(PDF)。

最佳实践是视觉检查分布拟合。distfit 库包含内置的绘图功能,如结合 PDF/CDF 的直方图和 QQ 图。可以按照如下方式创建图表:

# Make figure
fig, ax = plt.subplots(1, 2, figsize=(20, 8))

# PDF for only the best fit
dfit.plot(chart='PDF', n_top=1, ax=ax[0]);

# CDF for the top 10 fits
dfit.plot(chart='CDF', n_top=10, ax=ax[1])

# Show the plot
plt.show()

图 4. 帕累托图,显示了经验数据的直方图和估计的 PDF。左侧面板:最佳拟合的 PDF(贝塔)。右侧面板:前 10 个最佳拟合的 CDF。置信区间基于 alpha=0.05。

视觉检查确认了顶级 PDF 的拟合优度评分。然而,有一个例外,即威布尔分布(图 4 中的黄色线)似乎有两个峰值。换句话说,尽管 RSS 较低,但视觉检查未显示出对我们随机变量的良好拟合。请注意,引导法很快排除了威布尔分布,现在我们知道原因了

步骤 3:通过使用 PDF 属性进行决定。

最后一步可能是最具挑战性的,因为仍然有五种候选分布在拟合优度测试、引导法和视觉检查中得分非常高。我们现在应该决定哪个 PDF 在其基本属性上最适合建模人类身高。我将逐步阐述与我们人类身高建模用例相关的顶级候选分布的属性。

正态分布 是一个典型的选择,但需要注意的是,对人类身高的正态性假设在所有人群中可能并不成立。它没有重尾,因此可能无法很好地捕捉异常值。

学生 t 分布 通常在样本量较小或总体方差未知时作为正态分布的替代。它比正态分布具有更重的尾部,这能更好地捕捉数据中的异常值或偏斜。如果样本量较小,这种分布本可以是一个选项,但随着样本量的增加,t 分布趋近于正态分布。

伽玛分布 是一种连续分布,通常用于建模数据呈正偏态,即存在长尾的高值。人类身高可能因异常值(如非常高的个体)而呈正偏态。然而,引导法显示了较差的拟合。

对数伽玛分布 具有偏斜的形状,类似于伽玛分布,但具有更重的尾部。它建模值的对数,这使得当数据具有大量高值时更为合适。

贝塔分布 通常用于建模比例或率[9],而不是像我们用于身高的这种连续变量。如果身高被一个参考值(如中位身高)除以,那么它本是一个合适的选择。因此,尽管它在拟合优度测试中得分最高,我们通过视觉检查确认了良好的拟合,但它不会是我的首选。

广义极值(GEV)分布可以用来建模人群中的极值分布,例如最大值或最小值。它也允许重尾分布,可以捕捉数据中的离群值或偏斜。然而,它通常用于建模极值的分布 [10],而不是像人类身高这样的连续变量的总体分布。

Dweibull 分布可能不是这个研究问题的最佳匹配,因为它通常用于建模具有单调增加或减少趋势的数据,例如故障时间或事件时间数据 [11]。人类身高数据可能没有明确的单调趋势。PDF/CDF/QQ 图的视觉检查也显示没有良好的匹配。

总结来说,loggamma 分布在考虑了拟合优度检验、引导法、视觉检查以及现在基于与研究问题相关的 PDF 属性后,可能是这个特定使用案例中最佳的选择。请注意,我们可以轻松指定loggamma 分布并在输入数据上重新拟合(参见代码部分),如果需要的话(参见代码部分)。

# Initialize for common or popular distributions.
dfit = distfit(distr='loggamma', alpha=0.01, bound='both')

# Estimate the best fit
results = dfit.fit_transform(X)

# Print model parameters
print(dfit.model)

# {'name': 'loggamma',
#  'score': 6.676334203908028e-05,
#  'loc': -1895.1115726427015,
#  'scale': 301.2529482991781,
#  'arg': (927.596119872062,),
#  'params': (927.596119872062, -1895.1115726427015, 301.2529482991781),
#  'color': '#e41a1c',
#  'CII_min_alpha': 139.80923469906566,
#  'CII_max_alpha': 185.8446340627711}

# Save model
dfit.save('./human_height_model.pkl')

第 4 步. 对新的未见样本进行预测。

使用拟合模型,我们可以评估新(未见)样本的重要性,并检测它们是否偏离正常/预期(内点)。预测是基于理论概率密度函数进行的,这使得它轻量、快速且易于解释。PDF 的置信区间是通过alpha参数设置的。这是需要领域知识的部分,因为我们的数据集中没有已知的离群值。 在这种情况下,我设置了置信区间(CII)alpha=0.01,这导致最小边界为 139.8cm,最大边界为 185.8cm。默认情况下,两个尾部都会被分析,但这可以使用bound参数进行更改(参见上面的代码部分)

我们可以使用predict函数对新的未见样本进行预测,并创建预测结果的图表(图 5)。请注意,显著性经过了多重测试校正:multtest='fdr_bh'因此,离群值可能位于置信区间之外,但不会被标记为显著。

# New human heights
y = [130, 160, 200]

# Make predictions
results = dfit.predict(y, alpha=0.01, multtest='fdr_bh', todf=True)

# The prediction results
results['df']

#        y   y_proba y_pred         P
# 0  130.0  0.000642   down  0.000428
# 1  160.0  0.391737   none  0.391737
# 2  200.0  0.000321     up  0.000107

plt.figure();
fig, ax = plt.subplots(1, 2, figsize=(20, 8))
# PDF for only the best fit
dfit.plot(chart='PDF', ax=ax[0]);
# CDF for the top 10 fits
dfit.plot(chart='CDF', ax=ax[1])
# Show plot
plt.show()

图 5. 左面板:经验数据和对数-伽马 PDF 的直方图。黑线是经验数据分布。红线是拟合的理论分布。红色垂直线是设置为 0.01 的置信区间。绿色虚线被检测为离群值,红色叉号则不显著。(作者提供的图像)

预测结果存储在results中,包含多个列:yy_probay_predPP代表原始 p 值,y_proba是经过多重测试校正后的概率(默认值:fdr_bh)。请注意,使用todf=True参数时会返回一个数据框。两个观察值的概率alpha<0.01,并标记为显著updown

真实世界数据中的异常检测。

到目前为止,我们已经看到如何拟合模型并检测新颖性检测中的全球异常值。在这里,我们将使用真实世界的数据进行异常检测。 使用真实世界的数据通常更具挑战性。为了演示这一点,我将从 Thomson Reuters [7] 下载天然气现货价格的数据集,这是一份开源且免费提供的数据集 [8]。下载、导入并去除 nan 值后,共有 6555 个数据点,覆盖 27 年。

# Initialize distfit
dfit = distfit()

# Import dataset
df = dfit.import_example(data='gas_spot_price')

print(df)
#             price
# date             
# 2023-02-07   2.35
# 2023-02-06   2.17
# 2023-02-03   2.40
# 2023-02-02   2.67
# 2023-02-01   2.65
#           ...
# 1997-01-13   4.00
# 1997-01-10   3.92
# 1997-01-09   3.61
# 1997-01-08   3.80
# 1997-01-07   3.82

# [6555 rows x 1 columns]

对数据集的可视化检查。

为了直观地检查数据,我们可以创建一个天然气现货价格的折线图,以查看是否存在明显的趋势或其他相关问题(图 6)。可以看出,2003 年和 2021 年包含两个主要的峰值(这表明存在全球异常值)。此外,价格动作似乎呈现出自然的波动,有局部的高点和低点。基于这个折线图,我们可以对预期的分布形成直观的理解。价格主要在[2, 5]范围内波动,但在 2003 年至 2009 年之间有一些异常年份,价格范围更多在[6, 9]之间。

# Get unique years
dfit.lineplot(df, xlabel='Years', ylabel='Natural gas spot price', grid=True)

# Show the plot
plt.show()

图 6. 从 Thomson Reuters [7, 8]获取的天然气现货价格数据集开放数据源。

让我们使用distfit来更深入地研究数据分布,并确定相应的 PDF。搜索空间设置为所有可用的 PDF,bootstrap 方法设置为 100,以评估 PDF 的过拟合情况。

# Initialize
from distfit import distfit

# Fit distribution
dfit = distfit(distr='full', n_boots=100)

# Search for best theoretical fit.
results = dfit.fit_transform(df['price'].values)

# Plot PDF/CDF
fig, ax = plt.subplots(1,2, figsize=(25, 10))
dfit.plot(chart='PDF', n_top=10, ax=ax[0])
dfit.plot(chart='CDF', n_top=10, ax=ax[1])

# Show plot
plt.show()

图 7. 左:PDF,右:CDF。所有拟合的理论分布以不同颜色显示。(图片由作者提供)

最佳拟合的 PDF 是Johnsonsb(图 7),但当我们绘制经验数据分布时,PDF(红线)并未完全跟随经验数据。一般来说,我们可以确认大多数数据点位于[2, 5]范围内(这是分布的峰值所在位置),并且在分布中存在一个较小的第二个峰值,价格动作大约在 6 附近。这也是 PDF 未能平滑拟合经验数据的地方,导致一些低估和高估。通过总结图和 QQ 图,我们可以更好地研究拟合情况。让我们使用以下代码创建这两个图:

# Plot Summary and QQ-plot
fig, ax = plt.subplots(1,2, figsize=(25, 10))

# Summary plot
dfit.plot_summary(ax=ax[0])

# QQplot
dfit.qqplot(df['price'].values, n_top=10, ax=ax[1])

# Show the plot
plt.show()

在汇总图中看到拟合优度测试在所有顶级分布中显示了良好的结果(低分)很有趣。然而,当我们查看自助法的结果时,显示除了一个分布外,所有分布都过度拟合(图 8A,橙色线)。这并不完全意外,因为我们已经注意到超调和欠调。QQ 图确认拟合的分布与经验数据之间存在较大偏差(图 8B)。只有Johnsonsb分布显示出(边际)良好的拟合。

图 8. A. 左侧面板:根据自助法得分和拟合优度测试对 PDF 进行排序。B. 右侧面板:QQ 图,比较经验分布与所有其他理论分布。(图片由作者提供)

全球和上下文异常值检测。

我们将继续使用Johnsonsb分布和predict功能来检测异常值。我们已经知道数据集中包含异常值,因为我们遵循了异常方法,即分布是基于内点进行拟合的,而现在落在置信区间之外的观察值可以标记为潜在的异常值。 使用predict函数和lineplot我们可以检测和绘制异常值。从图 9 可以看到,虽然我们没有明确对其建模,但全球异常值和一些上下文异常值都被检测到了。红色条形图表示不足代表的异常值,绿色条形图表示过度代表的异常值。可以设置alpha参数来调整置信区间。

# Make prediction
dfit.predict(df['price'].values, alpha=0.05, multtest=None)

# Line plot with data points outside the confidence interval.
dfit.lineplot(df['price'], labels=df.index)

图 9. 拟合分布并进行预测后的异常值绘制。绿色条形图表示在 95%置信区间上界之外的异常值。红色条形图表示在 95%置信区间下界之外的异常值。

结语。

我描述了异常检测与新颖性检测之间的区别,以及如何使用分布拟合创建模型。通过使用distfit库,可以评估 89 种理论分布,选择合适的分布,并对新的未见样本进行预测。通过一个实际示例和一个真实数据集,我展示了如何确定最佳匹配的 PDF。请注意,可能没有任何已知的理论分布显著匹配。在这种情况下,distfit库还提供了使用百分位数分位数进行非参数拟合的选项。更多信息请参考我之前的博客关于分布拟合[6]。需要注意的是,异常检测是一项具有挑战性的任务,因为什么是正常或预期的定义可能是主观的,并且可能根据应用而有所不同。此外,异常可能由测量误差、数据错误或自然波动引起,因此在得出任何结论之前,仔细考虑偏差的根本原因非常重要*。

保持安全。保持警觉。

干杯 E.

如果你觉得这篇文章对你有帮助,请使用我的 推荐链接 继续无限学习并注册 Medium 会员。此外, 关注我 以保持最新内容的更新!

软件

让我们联系吧!

参考文献

  1. Breunig, Kriegel, Ng, 和 Sander (2000), LOF: 识别基于密度的局部异常点 Proc. ACM SIGMOD

  2. scikit-learn.org/stable/modules/outlier_detection.html

  3. Hayes, M.A., Capretz, M.A. 大传感器数据的上下文异常检测框架. 《大数据期刊》 2, 2 (2015). doi.org/10.1186/s40537-014-0011-y

  4. A. AlEroud 和 G. Karabatis, 一种上下文异常检测方法用于发现零日攻击, 2012 国际网络安全会议, 美国弗吉尼亚州亚历山德里亚, 2012, 第 40–45 页, doi: 10.1109/CyberSecurity.2012.12

  5. Song W., Dong W., Kang L. 基于贝叶斯框架和遗传算法的群体异常检测, Inf. Sci. (NY), 533 (2020), 第 138–149 页, 10.1016/j.ins.2020.03.110

  6. E. Taskesen, 如何找到数据的最佳理论分布, 2023 年 2 月 Medium。

  7. Thomson Reuters,天然气现货价格

  8. Thomson Reuters,天然气现货价格开放源数据

  9. 维基百科,贝塔分布.

  10. statisticshowto,极值分布与极值理论

  11. 维基百科,威布尔分布

主成分分析与霍特林 T2 及 SPE/DmodX 方法的异常值检测

原文:towardsdatascience.com/outlier-detection-using-principal-component-analysis-and-hotellings-t2-and-spe-dmodx-methods-625b3c90897

由于 PCA 的敏感性,它可以用于检测多变量数据集中的异常值。

Erdogan TaskesenTowards Data Science Erdogan Taskesen

·发表于Towards Data Science ·阅读时长 11 分钟·2023 年 3 月 11 日

--

照片由Andrew Ridley提供,拍摄于Unsplash

主成分分析(PCA)是一种广泛使用的降维技术,能够在保持相关信息的同时减少维度。由于其敏感性,它也可以用于检测多变量数据集中的异常值。异常值检测可以为异常情况提供早期预警,允许专家在问题升级之前识别和解决问题。然而,由于高维度和缺乏标签,在多变量数据集中检测异常值可能具有挑战性。PCA 在异常值检测中提供了几个优势。我将描述使用 PCA 进行异常值检测的概念。通过实际示例,我将展示如何为连续和分类数据集创建一个无监督模型来检测异常值。

异常值检测。

异常值可以通过单变量多变量方法建模(见图 1)。在单变量方法中,异常值是通过对一个变量进行数据分布分析来检测的。有关单变量异常值检测的更多详细信息,请参阅以下博客文章[1]:

## 单变量数据集中的分布拟合异常值检测

了解如何使用概率密度函数检测离群点,以获得快速且轻量的模型和可解释的结果。

towardsdatascience.com

多变量方法使用多个特征,因此可以检测具有(非)线性关系或偏斜分布的离群点。scikit-learn 库有多种多变量离群点检测解决方案,例如一类分类器、孤立森林和局部离群因子[2]。在这篇博客中,我将专注于使用主成分分析[3]进行多变量离群点检测,其自身具有可解释性等优点;离群点可以通过主成分分析的降维得到可视化。

图 1. 单变量与多变量分析在离群点检测中的概述。多变量数据集的离群点检测将在此博客中描述(图像由作者提供)。

异常与新颖性

异常与新颖性是与标准/预期行为偏离的观察结果,也称为离群点。尽管有一些不同:异常是之前见过的偏差,通常用于检测欺诈、入侵或故障。新颖性是之前未见过的偏差,用于识别新的模式或事件。在这种情况下,使用领域知识非常重要。由于什么是正常或预期的定义可以是主观的并且基于应用而有所不同,因此检测异常和新颖性都可能具有挑战性。

主成分分析用于离群点检测。

主成分分析(PCA)是一种线性变换,它减少了数据的维度并寻找数据中方差最大的方向。由于这种方法的特性,它对具有不同值范围的变量以及离群点都很敏感。一个优点是它允许在二维或三维散点图中可视化数据,使得更容易直观地确认检测到的离群点。此外,它提供了对响应变量的良好解释性。PCA 的另一个重大优点是它可以与其他方法结合使用,例如不同的距离度量,以提高离群点检测的准确性。在这里,我将使用包含两种离群点检测方法的 PCA 库:Hotelling’s T2 和 SPE/DmodX。有关更多细节,请阅读关于主成分分析和pca库的博客文章[3]。

## 什么是 PCA 负荷和如何有效使用 Biplots?

主成分分析的实用指南

[towardsdatascience.com

如果你觉得这篇关于异常值检测的文章对你有帮助, 关注我 以保持最新内容!通过我的 推荐链接 来支持这篇内容,这将为你提供 Medium 会员无限制的学习和阅读。

连续随机变量的异常值检测。

让我们从一个示例开始,演示如何使用Hotelling’s T2SPE/DmodX进行连续随机变量的异常值检测。我将使用来自 sklearn 的wine dataset,该数据集包含 178 个样本,13 个特征和 3 个葡萄酒类别[4]。

# Intallation of the pca library
pip install pca
# Load other libraries
from sklearn.datasets import load_wine
import pandas as pd

# Load dataset
data = load_wine()

# Make dataframe
df = pd.DataFrame(index=data.target, data=data.data, columns=data.feature_names)

print(df)
#     alcohol  malic_acid   ash  ...   hue  ..._wines  proline
# 0     14.23        1.71  2.43  ...  1.04  3.92   1065.0
# 0     13.20        1.78  2.14  ...  1.05  3.40   1050.0
# 0     13.16        2.36  2.67  ...  1.03  3.17   1185.0
# 0     14.37        1.95  2.50  ...  0.86  3.45   1480.0
# 0     13.24        2.59  2.87  ...  1.04  2.93    735.0
# ..      ...         ...   ...  ...   ...  ...
# 2     13.71        5.65  2.45  ...  0.64  1.74    740.0
# 2     13.40        3.91  2.48  ...  0.70  1.56    750.0
# 2     13.27        4.28  2.26  ...  0.59  1.56    835.0
# 2     13.17        2.59  2.37  ...  0.60  1.62    840.0
# 2     14.13        4.10  2.74  ...  0.61  1.60    560.0
# 
# [178 rows x 13 columns]

我们可以在数据框中看到每个特征的值范围差异很大,因此归一化步骤很重要。归一化步骤是pca library中的一个内置功能,可以通过normalize=True设置。在初始化时,我们可以分别指定异常值检测方法,ht2用于 Hotelling’s T2,spe用于 SPE/DmodX 方法。

# Import library
from pca import pca

# Initialize pca to also detected outliers.
model = pca(normalize=True, detect_outliers=['ht2', 'spe'], n_std=2  )

# Fit and transform
results = model.fit_transform(df)

运行 fit 函数后,pca库会对每个样本进行异常值评分。对于每个样本,会收集多个统计数据,如下面代码部分所示。数据框中的前四列(y_probap_rawy_scorey_bool)是使用 Hotelling’s T2 方法检测的异常值。后两列(y_bool_spey_score_spe)则基于 SPE/DmodX 方法。

# Print outliers
print(results['outliers'])

#     y_proba     p_raw    y_score  y_bool  y_bool_spe  y_score_spe
#0   0.982875  0.376726  21.351215   False       False     3.617239
#0   0.982875  0.624371  17.438087   False       False     2.234477
#0   0.982875  0.589438  17.969195   False       False     2.719789
#0   0.982875  0.134454  27.028857   False       False     4.659735
#0   0.982875  0.883264  12.861094   False       False     1.332104
#..       ...       ...        ...     ...         ...          ...
#2   0.982875  0.147396  26.583414   False       False     4.033903
#2   0.982875  0.771408  15.087004   False       False     3.139750
#2   0.982875  0.244157  23.959708   False       False     3.846217
#2   0.982875  0.333600  22.128104   False       False     3.312952
#2   0.982875  0.138437  26.888278   False       False     4.238283

[178 rows x 6 columns]

Hotelling’s T2 计算了基于前n_components的卡方检验和 P 值,从而允许通过y_proba对异常值进行从强到弱的排序。请注意,异常值的搜索空间是 PC1 到 PC5,因为预期最高的方差(因此也是异常值)会出现在前几个组件中。注意,深度是可选的,以防方差在前五个组件中捕捉得不好。让我们为葡萄酒数据集绘制异常值并标记它们(图 2)。

# Plot Hotellings T2
model.biplot(SPE=False, HT2=True, density=True, title='Outliers marked using Hotellings T2 method.')

# Make a plot in 3 dimensions
model.biplot3d(SPE=False, HT2=True, density=True, arrowdict={'scale_factor': 2.5, 'fontsize': 20}, title='Outliers marked using Hotellings T2 method.')

# Get the outliers using SPE/DmodX method.
df.loc[results['outliers']['y_bool'], :]

图 2。左面板:PC1 与 PC2 的关系图以及使用 Hotelling’s T2 方法检测的 9 个异常值的投影样本。右面板:带有异常值的三维图。(图片由作者提供)

SPE/DmodX方法是实际观察值与其投影之间的距离度量,使用主成分。距离中心的值由 Hotelling’s T2 值表示,因此图 3 中的椭圆表示样本超出 Hotelling’s T2 的边界。样本根据前两个主成分的均值和协方差被标记为异常值(图 3)。换句话说,当它在椭圆之外时。

# Plot SPE/DmodX method
model.biplot(SPE=True, HT2=True, title='Outliers marked using SPE/dmodX method and Hotelling T2.')

# Make a plot in 3 dimensions
model.biplot3d(SPE=True, HT2=True, title='Outliers marked using SPE/dmodX method and Hotelling T2.')

# Get the outliers using SPE/DmodX method.
df.loc[results['outliers']['y_bool_spe'], :]

图 3A. 使用 SPE/DmodX 方法检测到的离群值用菱形标出。使用 Hotelling T2 方法检测到的离群值用十字标出。(作者提供的图像)

图 3B. 使用 SPE/DmodX 方法检测到的离群值在 3D 图中可视化。

使用两种方法的结果,我们现在也可以计算重叠情况。在这种用例中,有 5 个离群值重叠(见下方代码部分)。

# Grab overlapping outliers
I_overlap = np.logical_and(results['outliers']['y_bool'], results['outliers']['y_bool_spe'])

# Print overlapping outliers
df.loc[I_overlap, :]

分类变量的离群值检测。

对于分类变量中的离群值检测,我们首先需要对分类变量进行离散化,使得距离可以互相比较。通过离散化的数据集(独热编码),我们可以使用 PCA 方法,并应用 Hotelling’s T2 和 SPE/DmodX 方法。我将使用学生成绩数据集[5]进行演示,该数据集包含 649 个样本和 33 个变量。我们将按照下方的代码部分导入数据集。关于列描述的更多细节可以在这里找到。如果数据集中存在标识符列或浮点类型的变量,我会将其删除或将其分类为离散区间,但在这里不会删除任何列。

# Import library
from pca import pca

# Initialize
model = pca()

# Load Student Performance data set
df = model.import_example(data='student')

print(df)
#     school sex  age address famsize Pstatus  ...  Walc  health absences
# 0       GP   F   18       U     GT3       A  ...     1       3        4
# 1       GP   F   17       U     GT3       T  ...     1       3        2
# 2       GP   F   15       U     LE3       T  ...     3       3        6
# 3       GP   F   15       U     GT3       T  ...     1       5        0  
# 4       GP   F   16       U     GT3       T  ...     2       5        0  
# ..     ...  ..  ...     ...     ...     ...  ...   ...     ...      ...  
# 644     MS   F   19       R     GT3       T  ...     2       5        4  
# 645     MS   F   18       U     LE3       T  ...     1       1        4  
# 646     MS   F   18       U     GT3       T  ...     1       5        6  
# 647     MS   M   17       U     LE3       T  ...     4       2        6  
# 648     MS   M   18       R     LE3       T  ...     4       5        4  

# [649 rows x 33 columns]

变量需要进行独热编码,以确保变量之间的距离可以相互比较。这会导致 649 个样本产生 177 列(见下方code部分)。

# Install onehot encoder
pip install df2onehot

# Initialize
from df2onehot import df2onehot

# One hot encoding
df_hot = df2onehot(df)[‘onehot’]

print(df_hot)
#      school_GP  school_MS  sex_F  sex_M  ...  
# 0         True      False   True  False  ...  
# 1         True      False   True  False  ...  
# 2         True      False   True  False  ...  
# 3         True      False   True  False  ...  
# 4         True      False   True  False  ...  
# ..         ...        ...    ...    ...  ...  
# 644      False       True   True  False  ...  
# 645      False       True   True  False  ...  
# 646      False       True   True  False  ...  
# 647      False       True  False   True  ...  
# 648      False       True  False   True  ...  

# [649 rows x 177 columns]

我们现在可以使用处理后的独热数据框作为 PCA 的输入并检测离群值。在初始化过程中,我们可以设置normalize=True以对数据进行标准化,并且需要指定离群值检测方法。

# Initialize PCA to also detected outliers.
model = pca(normalize=True,
            detect_outliers=['ht2', 'spe'],
            alpha=0.05,
            n_std=3,
            multipletests='fdr_bh')

# Fit and transform
results = model.fit_transform(df_hot)

# [649 rows x 177 columns]
# [pca] >Processing dataframe..
# [pca] >Normalizing input data per feature (zero mean and unit variance)..
# [pca] >The PCA reduction is performed to capture [95.0%] explained variance using the [177] columns of the input data.
# [pca] >Fit using PCA.
# [pca] >Compute loadings and PCs.
# [pca] >Compute explained variance.
# [pca] >Number of components is [116] that covers the [95.00%] explained variance.
# [pca] >The PCA reduction is performed on the [177] columns of the input dataframe.
# [pca] >Fit using PCA.
# [pca] >Compute loadings and PCs.
# [pca] >Outlier detection using Hotelling T2 test with alpha=[0.05] and n_components=[116]
# [pca] >Multiple test correction applied for Hotelling T2 test: [fdr_bh]
# [pca] >Outlier detection using SPE/DmodX with n_std=[3]
# [pca] >Plot PC1 vs PC2 with loadings.

# Overlapping outliers between both methods
overlapping_outliers = np.logical_and(results['outliers']['y_bool'],
                                      results['outliers']['y_bool_spe'])

# Show overlapping outliers
df.loc[overlapping_outliers]

#     school sex  age address famsize Pstatus  ...  Walc  health absences 
# 279     GP   M   22       U     GT3       T  ...     5       1       12  
# 284     GP   M   18       U     GT3       T  ...     5       5        4 
# 523     MS   M   18       U     LE3       T  ...     5       5        2 
# 605     MS   F   19       U     GT3       T  ...     3       2        0 
# 610     MS   F   19       R     GT3       A  ...     4       1        0 

# [5 rows x 33 columns]

Hotelling T2 检验检测到 85 个离群值,SPE/DmodX 方法检测到 6 个离群值(图 4,见图例)。两个方法重叠的离群值数量为 5 个。我们可以使用biplot功能绘制图形,并将样本根据任何类别(例如sex标签)进行着色,以便进一步调查。离群值用x*标记。这是进行深入检查的良好起点;在我们的案例中,我们可以在图 4 中看到这 5 个离群值与其他所有样本逐渐分离。我们可以对离群值进行排名,查看载荷,并深入调查这些学生(见前述代码部分)。为了对离群值进行排名,我们可以使用 Hotelling T2 方法的y_proba(值越低越好),以及 SPE/DmodX 方法的y_score_spe(值越大越好,它是样本到中心的欧几里得距离)。

# Make biplot
model.biplot(SPE=True,
             HT2=True,
             n_feat=10,
             legend=True,
             labels=df['sex'],
             title='Student Performance',
             figsize=(20, 12),
             color_arrow='k',
             arrowdict={'fontsize':16, 'c':'k'},
             cmap='bwr_r',
             gradient='#FFFFFF',
             edgecolor='#FFFFFF',
             density=True,
             )

图 4. 使用 SPE/DmodX 方法检测到的离群值用菱形标出。使用 Hotelling T2 方法检测到的离群值用十字标出。(作者提供的图像)

结束语。

我展示了如何使用 PCA 对连续和分类变量进行多变量异常值检测。通过使用pca库,我们可以使用 Hotelling 的 T2 和/或 SPE/DmodX 方法来确定候选异常值。每个变量对主成分的贡献可以通过负载获取,并在低维 PC 空间中使用双变量图进行可视化。这些可视化见解有助于提供关于异常值检测的直觉,并判断是否需要后续分析。一般来说,异常值检测可能具有挑战性,因为确定什么被认为是正常的可能是主观的,并且根据具体应用而有所不同。

保持安全,保持冷静。

干杯,E.

如果你觉得这篇关于异常值检测的文章对你有帮助, 关注我 以便了解我最新的内容!通过使用我的 推荐链接 支持这篇内容,这将为你提供 Medium 会员的无限学习和阅读机会。

软件

让我们联系!

参考资料

  1. E. Taskesen, PCA 负载和双变量图是什么?,Medium,Towards Data Science,2022 年 4 月

  2. Scikit-Learn,异常值检测

  3. E. Taskesen,如何为你的数据找到最佳理论分布,2023 年 2 月 Medium。

  4. Wine 数据集,https://archive-beta.ics.uci.edu/dataset/109/wine

  5. P. Cortez 和 A. Silva,利用数据挖掘预测中学生表现,ISBN 978–9077381–39–7

异常值检测与 Scikit-Learn 和 Matplotlib: 实用指南

原文:towardsdatascience.com/outlier-detection-with-scikit-learn-and-matplotlib-a-practical-guide-382d1411b8ec

了解可视化、算法和统计如何帮助你识别机器学习任务中的异常值。

Riccardo AndreoniTowards Data Science Riccardo Andreoni

·发布于 Towards Data Science ·阅读时间 10 分钟·2023 年 10 月 27 日

--

气球与异常值有什么关系?在引言中找到答案。图片来源: pixabay.com

想象一个房间里充满了五彩斑斓的气球,每个气球象征数据集中一个数据点。由于它们的特征不同,气球浮在不同的高度。现在,想象一些充氦的气球意外地飞得比其他气球高得多。正如这些特殊的气球打破了房间的均匀性,异常值也会打乱数据集中的模式。

回到纯统计学的角度,异常值被定义为异常现象,或者更准确地说,是明显偏离数据集其余部分的数据点。

考虑一个机器学习算法,它根据患者数据来诊断疾病。在这个实际的例子中,异常值可能是实验室结果或生理参数中的极端高值。虽然这些异常值可能源于数据收集错误测量不准确或真正的稀有事件,它们的存在可能导致算法做出错误的诊断。

这就是为什么我们,机器学习或数据科学从业者,必须始终小心处理异常值

在这篇简短的文章中,我将讨论几种有效识别和删除数据中的异常值的方法。

其中之一是 支持向量机,我在这篇文章中探讨了这个方法。

## 支持向量机与 Scikit-Learn:友好的介绍

每个数据科学家都应该在工具箱中有 SVM。通过实际操作来掌握这一多功能模型…

towardsdatascience.com

什么是异常值?

异常值是数据集中不具代表性的数据点,或者更准确地说,是那些与其他数据点显著偏离的数据点。尽管其定义简单,检测这些异常并非总是直接的,但首先,让我们回答以下基本问题。

为什么我们要检测数据集中的异常值?

对这个问题有两个答案。检测异常值的第一个原因是这些异常值可能隐藏数据中的有意义模式,并扭曲机器学习算法的学习过程。例如,在一个基于特征的房价数据集中,一个小而位置差的公寓的异常高价格,可能是异常值,导致偏差预测。

其次,广泛的数据科学应用的唯一目标就是检测异常值。在这些情况下,异常检测不仅仅是数据准备任务,而是应用的整个范围。例如,金融中的欺诈检测:算法的目标是识别不寻常的交易模式,指示欺诈活动。

图片由作者提供。

对于这份入门指南,我将介绍几种异常检测方法,这些方法分为以下三大类:

  • 图形方法:通过数据可视化来检测异常值。

  • 统计方法:通过统计分析和概率分布来检测异常值。

  • 算法方法:通过机器学习模型来检测异常值。

图片由作者提供。

图形方法

异常检测的图形方法利用了人脑的卓越模式识别能力。它使用散点图、箱线图和热图等可视化工具,提供数据的叙述,并允许数据科学家发现模式中的不规则性

散点图

在一个散点图中,异常值将表现为明显偏离主要聚类的点。

在生成合成数据后,我将使用Matplotlib Pyplot库在Python中创建散点图。

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Generate synthetic data with outliers
np.random.seed(42)
normal_data = np.random.normal(loc=50, scale=10, size=100)
outliers = np.random.normal(loc=15, scale=5, size=10)

# Combine normal data with outliers
data = np.concatenate((normal_data, outliers))

# Visualize data using a scatter plot
fig, ax = plt.subplots(figsize=(8, 6))
ax.scatter(range_1, normal_data, color=sns.color_palette("hls",24)[7], alpha=.9, label='Normal Data')
ax.scatter(range_2, outliers, color=sns.color_palette("hls",24)[0], alpha=.9, label='Outliers')
ax.set_xlabel('Index')
ax.set_ylabel('Value')
ax.set_title('Scatter Plot')
ax.spines['top'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.xaxis.set_ticks_position('none') 
ax.yaxis.set_ticks_position('none')
plt.legend()
plt.show()

结果图将突出显示一个数据集,其值与其他数据显著不同:

图片由作者提供。

箱线图

使用相同的数据,我们可以显示一个箱线图,其中离群点显示为箱线图“须”之外的点

Python代码如下。

# Visualize data using a box plot
fig, ax = plt.subplots(figsize=(8, 6))
b_plot = ax.boxplot(data, vert=True, patch_artist=True, notch=True)
ax.set_ylabel('Value')
ax.set_title('Box Plot')
ax.spines['top'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.xaxis.set_ticks_position('none') 
ax.yaxis.set_ticks_position('none')
ax.xaxis.set_ticks([])
# Color the box
for box in b_plot['boxes']:
    box.set_facecolor(sns.color_palette("hls",24)[12])

图片由作者提供。

小提琴图

除了箱线图,小提琴图不仅展示了数据的分布,还显示了其概率密度

图片由作者提供。

在这种情况下,离群点表现为超出数据主体的细小部分。一般来说,如果我注意到小提琴图的某些部分远远超出其他部分,那些很可能是离群点。

你可以用以下代码生成相同的图,或对其进行个性化调整。

# Visualize data using a violin plot
fig, ax = plt.subplots(figsize=(8, 6))
v_plot = ax.violinplot(data, vert=True, showmedians=True, showextrema=False)
ax.set_ylabel('Value')
ax.set_title('Violin Plot')
ax.spines['top'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.xaxis.set_ticks_position('none') 
ax.yaxis.set_ticks_position('none')
ax.xaxis.set_ticks([])
# Color the violin
for pc in v_plot['bodies']:
    pc.set_facecolor(sns.color_palette("hls",24)[7])
    pc.set_edgecolor('black')
    pc.set_alpha(.8)

统计方法

虽然图形方法确实更易理解,但也有局限性。主要问题是它们提供的是定性的而非定量的结果。因此,散点图、箱线图和分布图有助于有效沟通,但为了进行一致的分析,我们必须依赖数学严谨性统计指标

统计工具如Z 分数四分位距 (IQR)利用统计参数评估数据点。它们使数据科学家能够通过测量数据点偏离预期统计分布的程度,系统地识别离群点。

考虑Z 分数,它测量数据点距离均值的标准差数。Z 分数超过某个阈值的数据点可以被标记为离群点。通常,Z 分数高于 2 或 3 表示离群点。

import numpy as np

# Generate a random dataset with outliers (100 normal points and 10 outliers)
np.random.seed(42)
data = np.concatenate((np.random.normal(loc=50, scale=10, size=100), 
                        np.random.normal(loc=110, scale=20, size=10)))

# Calculate mean and standard deviation
mean_data = np.mean(data)
std_dev = np.std(data)

# Set Z-score threshold (typically 2 or 3)
z_score_threshold = 2

# Identify outliers using Z-score
outliers = [value for value in data if (value - mean_data) / std_dev > z_score_threshold]

类似地,IQR依赖于数据分布的第一和第三四分位数之间的范围。任何显著超出此范围的数据点都会被识别为离群点。由于有时第一和第三四分位数之间的范围可能过于严格,我们可以进行参数调整,例如考虑第一和第九分位数之间的范围。

从数学上讲,超出范围Q1–1.5IQRQ3+1.5IQR的数据点通常被归类为离群点。

另外,Tukey 的围栏方法是一种基于 IQR 范围的参数方法。它将所有落在Q1-kIQRQ3+kIQR范围之外的数据点视为异常值,其中k是一个常数。通常,k的取值在 1.5 到 3 之间。

import numpy as np

# Generate a random dataset with outliers (100 normal points and 10 outliers)
np.random.seed(42)
data = np.concatenate((np.random.normal(loc=50, scale=10, size=100), 
                        np.random.normal(loc=110, scale=20, size=10)))

# Calculate Q1 and Q3
Q1 = np.percentile(data, 25)
Q3 = np.percentile(data, 75)

# Calculate IQR
IQR = Q3 - Q1

# Set lower and upper bounds for outliers
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# Identify outliers using IQR method
outliers = [value for value in data if value < lower_bound or value > upper_bound]

在同一数据集中,以图形化方式展示这三种异常值检测方法的区别非常有趣。

作者提供的图片。

你可以看到,在这种情况下,原始的 IQR 方法过于严格。

算法方法

最后,算法方法利用了机器学习算法的力量,克服了简单统计方法的局限性。

存在多种异常值检测模型,包括孤立森林局部异常因子一类支持向量机。这些模型提供了可靠的技术来辨别复杂多维数据集中的异常值。与传统统计方法不同,这些算法更擅长理解数据的复杂模式和定义更复杂的决策边界。

孤立森林

我介绍的第一个异常值检测算法是孤立森林,因为它可能是我在日常任务中使用最频繁的机器学习模型。

孤立森林依赖于更著名的随机森林的原理,以及整体上的集成学习技术。如果你不熟悉随机森林或集成学习,我建议你参考这个简单的指南。

## 使用 Scikit-Learn 的集成学习:友好的介绍

像 XGBoost 或随机森林这样的集成学习算法是 Kaggle 竞赛中的顶级表现模型之一…

[towardsdatascience.com

孤立森林的核心思想基于这样一种观察:异常值由于其稀有性,通常需要在树结构中较少的步骤就能被孤立。因此,孤立森林构建了一个决策树的集成,由于其稀疏特性,更快速地孤立异常值。通过测量这些树中数据点的平均路径长度,孤立森林有效地为每个数据点量化异常值分数。

实现 Python 中的 Isolation Forest 非常简单,这要归功于Scikit-Learn(sklearn)库。

from sklearn.ensemble import IsolationForest
import numpy as np

# Generate synthetic data with outliers
np.random.seed(42)
normal_data = np.random.normal(loc=50, scale=10, size=10000)
outliers = np.random.normal(loc=20, scale=5, size=1000)
data = np.concatenate((normal_data, outliers)).reshape(-1, 1)

np.random.shuffle(data)

# Apply Isolation Forest for outlier detection
clf = IsolationForest(contamination=0.1, random_state=42)
clf.fit(data)

# Predict outliers
outlier_preds = clf.predict(data)
outliers_indices = np.where(outlier_preds == -1) 

局部异常因子(LOF)

基于这样一个观点:异常点通常在特征空间中比其* k *最近邻更孤立,局部异常因子(LOF)评估每个数据点的局部邻域,计算其相对于邻居的密度。

异常值通常显示出显著低于其邻居的局部密度,这使得它们通过 LOF 算法被检测出来。

Scikit-Learn(sklearn)提供了一个方便的工具来用 Python 实现 LOF。

from sklearn.neighbors import LocalOutlierFactor
import numpy as np

# Generate synthetic data with outliers
np.random.seed(42)
normal_data = np.random.normal(loc=50, scale=10, size=10000)
outliers = np.random.normal(loc=20, scale=5, size=1000)
data = np.concatenate((normal_data, outliers)).reshape(-1, 1)

np.random.shuffle(data)

# Apply Local Outlier Factor (LOF) for outlier detection
clf = LocalOutlierFactor(n_neighbors=20, contamination=0.1)
outlier_preds = clf.fit_predict(data)

# Identify outlier indices
outliers_indices = np.where(outlier_preds == -1)

应用 LOF 算法时需要调整的参数包括用于密度估计的邻居数量以及污染系数。最后一个参数表示期望的异常值比例。

结论

我展示了三种不同的异常检测方法,每种方法都有优点和局限性

图形化方法,如散点图和箱线图,毫无疑问是最直观的方法,适用于初步的数据探索。然而,它可能在处理高维数据时遇到困难,并且缺乏数值精度,仅仅是一个定性工具。

图形化方法的这一缺点被统计方法的数值稳健性所弥补。像 Z-score 这样的统计方法提供了数据异常值的精确度量,并探索了更复杂的数据关系。统计方法的局限在于数据常常假设为正态分布,这导致在处理偏斜数据时遇到一些困难。

最后,机器学习算法如 Isolation Forest 是前沿方法,因为它们在理论上比图形化和统计方法更强大。它们在理解复杂的数据空间中表现出色,其中模式难以发现。这些优点伴随着参数调整的限制。

这个介绍是一个很好的起点,但它仅仅触及了异常检测领域的表面。对于那些有兴趣深入这个领域的人,我提供了一些有趣且富有洞察力的资源列表。

如果你喜欢这个故事,可以考虑关注我,以便了解我即将推出的项目和文章!

这是我过去的一些项目:

## 使用 NetworkX 进行社交网络分析:温和的介绍

了解像 Facebook 和 LinkedIn 这样的公司如何从网络中提取洞察

towardsdatascience.com ## 高级降维模型简明介绍

学习如何高效地应用先进的降维方法,并提升你的机器学习…

towardsdatascience.com ## 使用 Scikit-Learn 进行集成学习:友好的介绍

类似 XGBoost 或随机森林的集成学习算法是 Kaggle 竞赛中的顶尖模型之一…

towardsdatascience.com

参考文献

克服自动语音识别挑战:下一个前沿

原文:towardsdatascience.com/overcoming-automatic-speech-recognition-challenges-the-next-frontier-e26c31d643cc?source=collection_archive---------2-----------------------#2023-03-30

自动语音识别技术在各个领域的进展、机遇和影响

Tal RosenweinTowards Data Science Tal Rosenwein

·

关注 发表在 Towards Data Science ·17 min 阅读·2023 年 3 月 30 日

--

Andrew DesLauriers 拍摄于 Unsplash

TL;DR:

这篇文章聚焦于自动语音识别(ASR)技术的进展及其对各个领域的影响。ASR 已在多个行业中变得普及,其准确性通过扩大模型规模和构建更大规模的标注和未标注训练数据集得到了提升。

展望未来,ASR 技术预计将随着声学模型规模的扩大和内部语言模型的增强而持续改进。此外,自监督和多任务训练技术将使低资源语言受益于 ASR 技术,而多语言训练将进一步提升性能,使许多低资源语言能够进行基本使用,如语音命令。

ASR 在生成式 AI 中也将发挥重要作用,因为与虚拟角色的交互将通过音频/文本接口进行。随着无文本 NLP 的出现,一些最终任务,如语音对语音翻译,可能在不使用任何明确的 ASR 模型的情况下得到解决。将发布能够通过文本、音频或两者进行提示的多模态模型,并生成文本或合成音频作为输出。

此外,具有语音交互界面的开放式对话系统将提高对转录错误以及书面和口头形式之间差异的鲁棒性。这将增强对挑战性口音和儿童语言的鲁棒性,使 ASR 技术成为许多应用中的重要工具。

一个端到端的语音增强-ASR-分段系统将发布,使 ASR 模型能够个性化,并提高在重叠语音和挑战性声学场景中的表现。这是解决 ASR 技术在现实世界场景中的挑战的重要一步。

最后,预计将出现一波语音 API。同时,仍然存在小型初创公司在技术/数据采集使用和技术采纳率低的群体中超越大型科技公司的机会。

2022 年回顾

自动语音识别(ASR)技术在教育、播客、社交媒体、远程医疗、呼叫中心等多个行业中正在获得越来越多的关注。一个很好的例子是语音交互界面(HMI)在消费产品中的普及,如智能汽车、智能家居、智能辅助技术[1]、智能手机,甚至酒店中的人工智能(AI)助手[2]。为了满足对快速且准确响应的日益增长的需求,低延迟 ASR 模型已被部署用于关键词检测[3]、端点检测[4]和转录[5]等任务。带有说话人属性的 ASR 模型[6–7]也越来越受到关注,因为它们能够实现产品个性化,为终端用户提供更大的价值。

数据的普遍性。 流媒体音频和视频平台,如社交媒体和 YouTube,导致了未标记音频数据的轻松获取[8]。新引入的自监督技术可以在无需真实标签的情况下利用这些音频[9–10]。这些技术在目标领域提高了 ASR 系统的性能,即使在该领域未对标记数据进行微调[11]。另一种因能够利用这些未标记数据而受到关注的方法是使用伪标签的自训练[12–13]。主要概念是使用自动语音识别(ASR)系统自动转录未标记的音频数据,然后将生成的转录本作为真实标签,用于以监督方式训练另一个 ASR 系统。OpenAI 采取了不同的方法,假设他们可以在网上大规模找到人工生成的转录本。他们通过抓取公开的音频数据及其人工生成的字幕,生成了一个高质量且大规模(640K 小时)的训练数据集。利用这个数据集,他们以完全监督的方式训练了一个 ASR 模型(即 Whisper),在零-shot 设置下在多个基准测试中达到了最先进的结果[14]。

损失。 尽管 End-2-end(E2E)损失主导了最先进的ASR 模型[15–17],新的损失函数仍在不断发布。一种名为混合自回归转换器(HAT)的新技术[18]被引入,能够通过分离空白和标签后验来测量内部语言模型(ILM)的质量。后来工作[19]使用这种因子分解有效地适配了 ILM,仅利用文本数据,提高了 ASR 系统的整体性能,特别是对专有名词、俚语和名词的转录,这些是 ASR 系统的主要痛点。还开发了新的度量标准,以更好地对齐人类感知,克服了词错误率(WER)的语义问题[20]。

架构选择。 在声学模型的架构选择方面,Conformer [21] 仍然是流媒体模型的首选,而 Transformers [22] 是非流媒体模型的默认架构。至于后者,引入了仅编码器(基于 wav2vec2 [23–24])和编码器-解码器(Whisper [14])的多语言模型,并在多个基准测试中超越了最先进的结果。由于模型大小、训练数据量和更大的上下文,这些模型优于流媒体模型。

科技巨头的多语言 AI 发展。 Google 宣布了其“1,000 语言计划”,以构建支持 1000 种最常用语言的 AI 模型[25],而 Meta AI 则宣布了其长期努力,旨在构建包含大多数世界语言的语言和机器翻译(MT)工具[26]。

语音语言突破。 多模态(语音/文本)和多任务预训练 seq-2-seq(编码器-解码器)模型,如 SpeechT5 [27],已经发布,在各种语音语言处理任务中取得了巨大成功,包括 ASR、语音合成、语音翻译、语音转换、语音增强和说话人识别。

这些 ASR 技术的进步预计将推动进一步创新,并在未来几年对广泛的行业产生影响。

展望未来

尽管面临挑战,但自动语音识别(ASR)领域预计将在多个领域取得重大进展,从声学和语义建模到对话式和生成式 AI,甚至包括说话人归属的 ASR。本节提供了这些领域的详细见解,并分享了我对 ASR 技术未来的预测。

图片来源:NikUnsplash

一般改进:

预计 ASR 系统在声学和语义方面都会有所改进。

在声学模型方面,预计更大的模型和训练数据集将提升 ASR 系统的整体性能,类似于 LLMs 领域的进展。尽管扩展 Transformer 编码器,如 Wav2Vec 或 Conformer,存在挑战,但预计会有突破使其能够扩展,或出现向编码器-解码器架构(如 Whisper)转变的趋势。然而,编码器-解码器架构有一些需要解决的缺陷,如幻觉。优化技术,如 faster-whisper [28] 和 NVIDIA-wav2vec2 [29],将减少训练和推理时间,从而降低部署大型 ASR 模型的门槛。

在语义方面,研究人员将重点关注通过融入更大的声学或文本上下文来改进 ASR 模型。还将探索在 E2E 训练期间向 ILM 注入大规模无配对文本,如 JEIT [30]。这些努力将有助于克服准确转录命名实体、俚语和名词等关键挑战。

尽管 Whisper 和谷歌的通用语音模型(USM)[31]在多个基准测试中提高了 ASR 系统的性能,但一些基准测试仍需解决,因为词错误率(WER)仍然约为 20% [32]。使用语音基础模型、添加更多多样化的训练数据和应用多任务学习将显著提升此类场景中的性能,从而开辟新的商业机会。此外,预计将出现新的指标和基准,以更好地对齐新的最终任务和领域,例如医学领域中的非词汇对话声音[33]以及媒体编辑和教育领域中的填充词检测与分类[34]。可能会为此目的开发特定任务的微调模型。最后,随着多模态的发展,预计还会发布更多模型、训练数据集和新任务的基准[35–36]。

随着进展的不断推进,预计会出现一波类似自然语言处理(NLP)的语音 API。谷歌的 USM、OpenAI 的 Whisper 和 Assembly 的 Conformer-1 [37]是一些早期的例子。

尽管听起来有些荒谬,但强制对齐对许多公司来说仍然具有挑战性。一个开源代码可能会帮助许多人实现音频片段与其对应转录文本之间的准确对齐。

低资源语言:

自监督学习、多任务学习和多语言模型的进展预计将显著提高低资源和未书写语言的性能。这些方法将通过利用预训练模型和在相对较少的标记样本上进行微调来实现可接受的性能[24]。另一种有前途的方法是双重学习[38],这是一种半监督机器学习的范式,旨在通过同时解决两个对立任务(在我们的案例中为文本到语音(TTS)和 ASR)来利用无监督数据。在这种方法中,每个模型生成未标记样本的伪标签,这些伪标签用于训练另一个模型。

此外,使用无配对文本改进 ILM 可以增强模型的鲁棒性,这将特别有利于封闭集挑战,例如语音命令。在一些应用中,如 YouTube 视频的字幕,性能将是可接受的,但不是完美的,而在其他应用中,如法庭上的逐字记录,模型可能需要更多时间才能达到标准。我们预计公司将在 2023 年根据这些模型收集数据,同时手动纠正转录文本,并且我们将在 2024 年在专有数据上微调后看到低资源语言的显著改进。

生成式 AI:

虚拟形象的使用预计将彻底改变人与数字资产的互动。在短期内,ASR 将作为生成式 AI 的基础之一,因为这些虚拟形象将通过文本/听觉界面进行交流。

但未来,随着注意力转向新的研究方向,可能会发生变化。例如,一个可能被采用的新兴技术是 Textless NLP,它代表了一种新的语言建模方法,用于音频生成[39]。这种方法使用可学习的离散音频单元[40],并以自回归的方式一次生成一个离散音频单元,类似于文本生成。这些离散单元可以随后解码回音频领域。目前,这项技术已经能够生成在句法和语义上都合理的语音继续,同时保持说话人的身份和韵律,对于未见过的说话人也能做到,如 GSLM/AudioLM [39, 41]所示。这项技术的潜力巨大,因为可以在许多任务中跳过 ASR 组件(及其错误)。例如,传统的语音到语音(S2S)翻译方法如下:它们首先将源语言中的话语转录出来,然后使用机器翻译模型将文本翻译为目标语言,最后使用 TTS 引擎生成目标语言的音频。使用 textless-NLP 技术,S2S 翻译可以通过一个直接作用于离散音频单元的编码-解码架构来完成,而不使用任何显式的 ASR 模型[42]。我们预测,未来的 Textless NLP 模型将能够解决许多其他任务,而无需经过显式转录,例如问答系统。然而,这种方法的主要缺点是回溯错误和调试,因为在离散单元空间中工作时,事情将变得不那么直观。

T5 [43] 和 T0 [44] 在 NLP 领域通过利用其多任务训练和展示零样本任务泛化取得了巨大成功。2021 年发布的 SpeechT5 [27] 在各种口语语言处理任务中表现出色。今年早些时候,VALL-E [45] 和 VALL-EX [46] 被发布。它们通过使用 textless NLP 技术展示了 TTS 模型的令人印象深刻的上下文学习能力,仅通过几秒钟的音频就能克隆说话人的声音,并且无需任何微调,甚至在跨语言环境中也能做到。

通过结合来自 SpeechT5 和 VALL-E 的概念,我们可以期待类似 T0 的模型的发布,这些模型可以通过文本、音频或两者来进行提示,并根据任务生成文本或合成音频。一个新时代的模型将开始出现,因为上下文学习将使得在零样本设置中对新任务进行泛化成为可能。这将允许在音频上进行语义搜索,通过带有说话人属性的 ASR 转录目标说话人,或用自由文本描述它,例如,“那个咳嗽的年轻孩子说了什么?”此外,它将使我们能够使用音频或文本描述来分类或合成音频,并通过显式/隐式 ASR 直接解决 NLP 任务。

对话 AI:

对话型 AI 主要通过任务导向的对话系统被采纳,即如亚马逊的 Alexa 和苹果的 Siri 这样的 AI 个人助理(PA)。这些 PA 因其通过语音命令快速访问功能和信息的能力而变得流行。随着大科技公司主导这一技术,对 AI 助理的新规定将迫使它们提供第三方语音助理选项,从而打开竞争[47]。随着这一变化,我们可以预期个人助理之间会实现互操作性,也就是说它们将开始进行通信。这将非常棒,因为人们可以使用任何设备连接到世界任何地方的对话代理[48]。从 ASR 的角度来看,这将带来新的挑战,因为上下文化将变得更加广泛,助理必须具备对不同口音的鲁棒性,并可能支持多语言。

近年来,基于文本的开放式对话系统发生了巨大的技术飞跃,例如 Blender-Bot 和 LaMDA [49–50]。最初,这些对话系统是基于文本的,即它们通过文本输入并训练输出文本,全部在书面领域内。随着 ASR 性能的提高,开放式对话系统被增强了语音基础的人机界面(HMI),这导致了由于口语和书面形式之间的差异而出现的模态不一致。主要挑战之一是弥合这一差距,通过克服由于音频相关处理引入的新类型错误,例如口语和书面形式之间的流利度差异和实体解析,以及转录错误,如发音错误[51–52]。

可能的解决方案包括改进转录质量和强大的 NLP 模型,这些模型能够有效处理转录和发音错误。可靠的声学模型置信度评分[53]将作为这些系统的关键角色,帮助指出说话者错误或作为 NLP 模型或解码逻辑的另一个输入。此外,我们预计 ASR 模型将能够预测非语言线索,如讽刺,使代理能够更深入地理解对话并提供更好的回应。

这些改进将推动进一步的发展,使对话系统具备语音 HMI,以支持挑战性的口音和儿童语言,例如在 Loora [54]和 Speaks [55]中。

更进一步,我们预计将发布一个 E2E 多任务学习框架,用于语音语言任务,采用语音和 NLP 问题的联合建模,如 MTL-SLT [56]。这些模型将以 E2E 的方式进行训练,减少顺序模块之间的累计误差,并处理如口语理解、口语总结和口语问答等任务,通过将语音作为输入,产生转录、意图、命名实体、摘要和文本查询答案等各种输出。

个性化将对 AI 助手和开放式对话系统产生巨大影响,这将引领我们进入下一个要点:说话人属性 ASR。

说话人属性 ASR:

在家庭环境中涉及多个麦克风和方的远程对话的转录仍然存在挑战。即使是最先进(SoTA)的系统也只能实现约 35%的 WER[57]。

早期的联合 ASR 和分段系统于 2019 年发布[58]。今年,我们可以期待发布一种端到端的语音增强-ASR-分段系统,该系统将改善重叠语音的性能,并在挑战性的声学场景(如混响房间、远场设置和低信噪比(SNR))中提供更好的表现。通过联合任务优化、改进的预训练方法(如 WavLM [10])、应用架构更改[59]、数据增强以及在预训练和微调期间在领域内数据上的训练[11],将实现这些改进。此外,我们可以期待针对个性化语音识别的说话人属性 ASR 系统的部署。这将进一步提高目标说话人语音的转录准确性,并将转录偏向用户定义的词汇,如联系人姓名、专有名词和其他命名实体,这些对智能助手至关重要[60]。此外,低延迟模型将继续成为重点领域,以增强边缘设备的整体体验和响应时间[61–62]。

初创公司在 ASR 领域相对于大科技公司的角色

尽管大科技公司预计将继续凭借其 API 主导市场,但小型初创企业在特定领域仍然可以超越它们。这些领域包括由于法规原因在大科技公司的训练数据中被低估的领域,如医疗领域和儿童语言,以及尚未采用技术的群体,如具有挑战性口音的移民或全球学习英语的个人。在大科技公司不愿投资的市场中,例如那些不广泛使用的语言,小型初创企业可能会发现成功和获利的机会。

为了创造双赢的局面,大科技公司可以提供 API,允许完全访问其声学模型的输出,同时允许其他人编写解码逻辑(WFST/beam-search),而不仅仅是添加可定制词汇或使用当前模型适应功能[63–64]。这种方法将使小型初创企业能够通过在给定声学模型之上进行推理时结合引导或多语言模型来在其领域中脱颖而出,而不必自己训练声学模型,这在人力资本和领域知识上可能成本高昂。反过来,大科技公司将从其付费模型的更广泛采用中受益。

ASR 如何融入更广泛的机器学习领域?

一方面,考虑到 ASR 作为最终任务时,其重要性与计算机视觉(CV)和 NLP 相当。这是低资源语言和领域中,转录是主要业务的当前情况,例如法庭、医疗记录、电影字幕等。

另一方面,在其他领域,ASR 已不再是瓶颈,只要它达到了某种可用性阈值。在这些情况下,NLP 才是瓶颈,这意味着将 ASR 性能提升到完美主义并不是提取最终任务见解的关键。例如,会议总结或行动项提取在许多情况下可以利用当前的 ASR 质量实现。

结束语

ASR 技术的进步使我们更接近于实现人与机器之间的无缝沟通,例如在对话 AI 和生成 AI 中。随着语音增强-ASR-标记系统的持续发展以及无文本 NLP 的出现,我们有望在这个领域见证激动人心的突破。展望未来,我们不禁期待 ASR 技术将解锁的无限可能。

感谢您花时间阅读这篇文章!我们非常重视和感激您对这些预测的看法和反馈。请随时分享您的评论和想法。

参考文献:

[1] www.orcam.com/en/home/

[2] voicebot.ai/2022/12/01/hey-disney-custom-alexa-assistant-rolls-out-at-disney-world/

[3] Jose, Christin 等人。“关键词检测的延迟控制。” ArXiv,2022,doi.org/10.21437/Interspeech.2022-10608

[4] Bijwadia, Shaan 等人。“统一的端到端语音识别与端点检测以实现快速高效的语音系统。” ArXiv,2022,doi.org/10.1109/SLT54892.2023.10022338

[5] Yoon, Ji 等人。“HuBERT-EE:早期退出的 HuBERT 以实现高效语音识别。” ArXiv,2022,doi.org/10.48550/arXiv.2204.06328

[6] Kanda, Naoyuki 等人。“转录到说话者标记:使用端到端说话者标记 ASR 进行无限数量的说话者的神经说话者标记。” ArXiv,2021,doi.org/10.48550/arXiv.2110.03151

[7] Kanda, Naoyuki 等人。“基于令牌级说话者嵌入的流式说话者标记 ASR。” ArXiv,2022,doi.org/10.48550/arXiv.2203.16685

[8] www.fiercevideo.com/video/video-will-account-for-82-all-internet-traffic-by-2022-cisco-says

[9] Chiu, Chung, 等. “用于语音识别的随机投影量化器自监督学习。” ArXiv, 2022, doi.org/10.48550/arXiv.2202.01855

[10] Chen, Sanyuan, 等. “WavLM:用于全栈语音处理的大规模自监督预训练。” ArXiv, 2021, doi.org/10.1109/JSTSP.2022.3188113

[11] Hsu, Wei, 等. “鲁棒的 Wav2vec 2.0:自监督预训练中的领域偏移分析。” ArXiv, 2021, doi.org/10.48550/arXiv.2104.01027

[12] Lugosch, Loren, 等. “大规模多语言语音识别的伪标签。” ArXiv, 2021, doi.org/10.48550/arXiv.2111.00161

[13] Berrebbi, Dan, 等. “从开始进行连续伪标签。” ArXiv, 2022, doi.org/10.48550/arXiv.2210.08711

[14] Radford, Alec, 等. “通过大规模弱监督进行鲁棒的语音识别。” ArXiv, 2022, doi.org/10.48550/arXiv.2212.04356

[15] Graves, Alex, 等. “连接主义时间分类:使用递归神经网络标记未分段序列数据。” ICML, 2016, www.cs.toronto.edu/~graves/icml_2006.pdf

[16] Graves, Alex. “使用递归神经网络进行序列转导。” ArXiv, 2012, doi.org/10.48550/arXiv.1211.3711

[17] Chan, William, 等. “听、注意和拼写。” ArXiv, 2015, doi.org/10.48550/arXiv.1508.01211

[18] Variani, Ehsan, 等. “混合自回归传输器(Hat)。” ArXiv, 2020, doi.org/10.48550/arXiv.2003.07705

[19] Meng, Zhong, 等. “模块化混合自回归传输器。” ArXiv, 2022, doi.org/10.48550/arXiv.2210.17049

[20] Kim, Suyoun, 等. “使用语义距离度量评估用户对语音识别系统质量的感知。” ArXiv, 2021, doi.org/10.48550/arXiv.2110.05376

[21] Gulati, Anmol, 等. “Conformer:用于语音识别的卷积增强变换器。” ArXiv, 2020, doi.org/10.48550/arXiv.2005.08100

[22] Vaswani, Ashish, 等. “注意力即一切。” ArXiv, 2017, doi.org/10.48550/arXiv.1706.03762

[23] Baevski, Alexei, 等. “Wav2vec 2.0:自监督语音表示学习框架。” ArXiv, 2020, doi.org/10.48550/arXiv.2006.11477

[24] Babu, Arun 等. “XLS-R: 大规模自监督跨语言语音表示学习。” ArXiv, 2021, doi.org/10.48550/arXiv.2111.09296

[25] blog.google/technology/ai/ways-ai-is-scaling-helpful/

[26] ai.facebook.com/blog/teaching-ai-to-translate-100s-of-spoken-and-written-languages-in-real-time/

[27] Ao, Junyi 等. “SpeechT5: 统一模态编码器-解码器预训练用于口语语言处理。” ArXiv, 2021, doi.org/10.48550/arXiv.2110.07205

[28] github.com/guillaumekln/faster-whisper

[29] github.com/NVIDIA/DeepLearningExamples/tree/master/PyTorch/SpeechRecognition/wav2vec2

[30] Meng, Zhong 等. “JEIT: 语音识别的联合端到端模型与内部语言模型训练。” ArXiv, 2023, doi.org/10.48550/arXiv.2302.08583

[31] Zhang, Yu 等. “Google USM: 将自动语音识别扩展到 100 多种语言。” ArXiv, 2023, doi.org/10.48550/arXiv.2303.01037

[32] Kendall, T. 和 Farrington, C. “区域非裔美国语言语料库。” 版本 2021.07. Eugene, OR: 非裔美国语言在线资源项目。oraal.uoregon.edu/coraal, 2021

[33] Brian, D Tran 等. “‘嗯哼’,‘呃呃’:非词汇性对话声音是否会成为环境临床文档技术的致命缺陷?”,《美国医学信息学协会杂志》,2023, doi.org/10.1093/jamia/ocad001

[34] Zhu, Ge 等. “填充词检测与分类:数据集与基准。” ArXiv, 2022, doi.org/10.48550/arXiv.2203.15135

[35] Anwar, Mohamed 等. “MuAViC: 多语言音频视觉语料库用于稳健的语音识别和稳健的语音到文本翻译。” ArXiv, 2023, doi.org/10.48550/arXiv.2303.00628

[36] Jaegle, Andrew 等. “Perceiver IO: 一种用于结构化输入和输出的通用架构。” ArXiv, 2021, doi.org/10.48550/arXiv.2107.14795

[37] www.assemblyai.com/blog/conformer-1/

[38] Peyser, Cal 等. “大词汇量设备端自动语音识别的双重学习。” ArXiv, 2023, doi.org/10.48550/arXiv.2301.04327

[39] Lakhotia, Kushal 等. “从原始音频生成的语言建模。” ArXiv, 2021, doi.org/10.48550/arXiv.2102.01192

[40] Zeghidour, Neil 等. “SoundStream: 一种端到端神经音频编解码器。” ArXiv, 2021, doi.org/10.48550/arXiv.2107.03312

[41] Borsos, Zalán 等. “AudioLM: 一种音频生成的语言建模方法。” ArXiv, 2022, doi.org/10.48550/arXiv.2209.03143

[42] about.fb.com/news/2022/10/hokkien-ai-speech-translation/

[43] Raffel, Colin 等. “探索统一文本到文本变换器的迁移学习极限。” ArXiv, 2019, /abs/1910.10683。

[44] Sanh, Victor 等. “多任务提示训练实现零样本任务泛化。” ArXiv, 2021, doi.org/10.48550/arXiv.2110.08207

[45] 王成毅 等. “神经编解码语言模型是零样本文本到语音合成器。” ArXiv, 2023, doi.org/10.48550/arXiv.2301.02111

[46] 张子强 等. “用你自己的声音说外语:跨语言神经编解码语言建模。” ArXiv, 2023, doi.org/10.48550/arXiv.2303.03926

[47] voicebot.ai/2022/07/05/eu-passes-new-regulations-for-voice-ai-and-digital-technology/

[48] www.speechtechmag.com/Articles/ReadArticle.aspx?ArticleID=154094

[49] Thoppilan, Romal 等. “LaMDA: 对话应用的语言模型。” ArXiv, 2022, doi.org/10.48550/arXiv.2201.08239

[50] Shuster, Kurt 等. “BlenderBot 3: 一个持续学习以负责任地互动的部署对话代理。” ArXiv, 2022, doi.org/10.48550/arXiv.2208.03188

[51] 肖舟 等. “用于实体解析的 ASR 鲁棒性语音嵌入。” Proc. Interspeech 2022, 3268–3272, doi: 10.21437/Interspeech.2022–10956

[52] 陈安琪 等. “教 BERT 等待:平衡流媒体不流畅检测的准确性和延迟。” ArXiv, 2022, doi.org/10.48550/arXiv.2205.00620

[53] 李秋佳 等. “提高端到端语音识别的领域外数据置信度估计。” ArXiv, 2021, doi.org/10.48550/arXiv.2110.03327

[54] loora.ai/

[55] techcrunch.com/2022/11/17/speak-lands-investment-from-openai-to-expand-its-language-learning-platform/

[56] Zhiqi, Huang 等. “MTL-SLT: 多任务学习用于口语语言任务。” NLP4ConvAI, 2022, aclanthology.org/2022.nlp4convai-1.11

[57] Watanabe, Shinji 等. “CHiME-6 挑战:应对未分段录音中的多说话者语音识别。” ArXiv, 2020, doi.org/10.48550/arXiv.2004.09249.

[58] Shafey, Laurent 等. “通过序列转导联合语音识别和说话者分离。” ArXiv, 2019, doi.org/10.48550/arXiv.1907.05337.

[59] Kim, Juntae 和 Lee, Jeehye. “通过稀疏自注意力层将 RNN-转导器推广到域外音频。” ArXiv, 2021, doi.org/10.48550/arXiv.2108.10752.

[60] Sathyendra, Kanthashree 等. “针对神经转导器的个性化语音识别的上下文适配器。” ArXiv, 2022, doi.org/10.48550/arXiv.2205.13660.

[61] Tian, Jinchuan 等. “Bayes 风险 CTC: 在序列到序列任务中可控的 CTC 对齐。” ArXiv, 2022, doi.org/10.48550/arXiv.2210.07499.

[62] Tian, Zhengkun 等. “Peak-First CTC: 通过应用 Peak-First 正则化减少 CTC 模型的峰值延迟。” ArXiv, 2022, doi.org/10.48550/arXiv.2211.03284.

[63] docs.rev.ai/api/custom-vocabulary/

[64] cloud.google.com/speech-to-text/docs/adaptation-model

克服开发者障碍

原文:towardsdatascience.com/overcoming-developers-block-8cfa724add7a

处理开发者障碍的方法有很多。继续阅读,了解我如何处理。

Marcin KozakTowards Data Science Marcin Kozak

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

--

开发者障碍也许是倦怠的最大症状。照片由 Nubelson Fernandes 提供,来源于 Unsplash

如果你是开发者,你是否曾经遭遇过开发者障碍?如果没有,那真是太好了。但相信我,那个时刻终将来临,早晚而已(希望是早些)。当它来临时,你的技能将不再重要,因为你将不再使用它们;它们将像荒岛上的一袋金子一样无用。

有很多方法来处理开发者障碍,但我会告诉你一种对我有效的方法。

开发者障碍(或程序员障碍)是指程序员在产生新想法或解决问题时遇到困难的现象。在最糟糕的情况下,写下一行代码几乎是不可能的。

这类似于写作障碍,即作家难以产生新的想法或内容。这可能由多种因素引起,如疲倦、缺乏睡眠或动力、难以理解问题——或者最后但同样重要的,倦怠

实际上,倦怠常常会导致这样的障碍。你坐在电脑前,试图做点什么,但你唯一能想到的就是你无法做到它。无论是什么原因,都无所谓——你就是不能做到它不会做到它。就这样。

这可能是一个令人沮丧和失去动力的经历,因为它会妨碍程序员完成任务和赶上截止日期。有一些典型的方法可以尝试,例如

  • 休息一下

  • 和同事讨论你正在处理的问题

  • 将任务分解成小块,逐一完成

  • 利用你的空闲时间逃避并忘记编程——你也可以在工作中利用几分钟时间这样做

  • 呼吸练习、正念和其他放松技巧

你可以在这里阅读更多类似的方法:

## 如何克服程序员的瓶颈

当你害怕你的控制台时

towardsdatascience.com [## 摆脱开发者瓶颈

当你不知道该做什么时该怎么办

betterprogramming.pub](https://betterprogramming.pub/make-your-way-out-of-developers-block-f134adc292d0?source=post_page-----8cfa724add7a--------------------------------)

说实话,我相信这些方法可以帮助预防开发者瓶颈,但在完全克服它方面效果较差。真正的瓶颈是无法仅通过与同事交谈或短暂休息来治愈的。长期休息(比如一个月)可能有效,但这是一种风险很大的想法,因为很可能你在回到工作后,倦怠感会比以前更严重。

真实的开发者瓶颈是无法仅通过与同事交谈或短暂休息来解决的。

我作为科学家和在商业环境中工作的数据科学家,经历过这种情况不止一次。作为后者,我从未长时间休息;最长的休息是 11 天。我可以说,一周的休息帮助我忘记编码,恢复力量和休息——但这并不会让我的回归编程变得更容易,绝对不会。这样休息后的第一天只会证明我仍然有开发者瓶颈需要克服。

所以,这些方法对我来说只是预防措施。这并不意味着它们对你不起作用——你必须亲自尝试。许多人说这些方法有效,所以也许它们确实有效,至少对某些人是这样?啊,我真羡慕他们!

当你已经突破了瓶颈,既没有写另一个函数或类的欲望,也没有力量去写另一行代码时,你会怎么做?

这是对我有效的方法。

时间阻断需要两个东西:一个计时器和你的专注。照片由 Şahin Sezer Dinçer 提供,来源于 Unsplash

时间阻断

你可以在这里阅读关于时间阻断的内容:

[## 时间阻断 - 维基百科

时间阻断(也称为时间切块)是一种个人时间管理的生产力技巧……

en.wikipedia.org](https://en.wikipedia.org/wiki/Timeblocking?source=post_page-----8cfa724add7a--------------------------------)

简而言之,这是一种个人时间管理技巧,其中你将一段时间(在我们的例子中是工作日)划分为更小的时间段,并在这些时间段内完成小任务。

这看起来并没有什么特别的印象,对吧?但如果你用对方法——对你来说的正确方法——它真的能起到奇迹般的效果,就像对我而言,即使在我觉得没有力气再做任何事,只能起身关闭电脑的时候,它也能有效。

这并没有什么特别的印象,对吧?但如果你用对方法——对你来说的正确方法——它真的能起到奇迹般的效果。

时间区块的操作方法

我是这样做的:

  • 我设置一个计时器,时间段如 15 或 20 分钟。我们称之为专注时间段

  • 在这个专注时间段内,我完全专注于我正在做的任务。我将我正在做的任务分解成更小的任务,也创建一个适合这个时间段的小任务。我只专注于我正在做的任务。

  • 专注于任务意味着深度工作。所以,不要查看邮件,不要上与任务无关的互联网,不要休息喝咖啡——甚至连做咖啡都不要。(更多内容见下文。)

  • 我会一直工作到时间结束,但我不会查看时间;这就是你需要使用计时器的原因。我不考虑时间,只专注于工作。

  • 当时间到达时,我将工作放在一边,除非我有一些非常小的任务要完成——比如,完成一行代码或一个句子之类的事情。我尽量不将时间延长 5 或 10 分钟——当我这样做没有问题时,通常意味着我的时间块接近于可以忽略,或者我可以延长专注时间段。

  • 我会休息一下。这可以是工作休息,但不一定。我可以回复邮件、打电话、做一些不需要深度工作的事情。如果我休息时间不与工作有关,我会尽量让它相对较短,比如 5 或 10 分钟。但如果与工作相关,它可以长一些,除非我专注的任务更为优先。

  • 当你准备好重新投入深度工作时,开始另一个专注时间段。

专注时间的长度

专注时间段在一天中可以有所变化;这真的不重要。对我而言,有一个比例规则:我感觉越好,时间段越长。当我感觉非常糟糕时,当我觉得如果再写一行代码我就要死了时,我会把下一个专注时间段设得非常短,比如 10 分钟,有时只有 5 分钟。

如果你需要使用这么短的时间段,你可能已经精疲力竭了。这是几年前的事了,那时候并不愉快。我无论如何都必须工作,这实际上也是我在这篇文章中描述的方法的来源。从那时起,我使用了更长的专注时间段。

尽管 5 分钟是一个非常短的时间段,但对于某些任务,它可能足够做一些高质量的工作。此外,20 * 5 = 100分钟的编程总比0 * 0 = 0分钟要好,对吧?

当我感觉良好时,我会设定为 25 分钟——但从不超过。根据我的感觉,我可以在八小时工作日中使用最多 12 个 25 分钟的专注时间段。我从未能够进行更多的真正深度工作。

你可能会想:这只是12 * 25= 300分钟的工作,而我应该工作8 * 60 = 480分钟!不,这不是人们工作的方式。有时你可能在 8 小时内工作 450 分钟,但这是很少见的,非常少见。你通常会有很多休息,需要做一些不需要深度集中工作的事情。所以,300 分钟不仅足够——它是很多的。

深度工作

有时,两个小时的深度工作可能也会很多,因为深度工作与常规工作完全不同。你可以在卡尔·纽波特的书中阅读到这一点:

## 深度工作:专注成功的规则 - 卡尔·纽波特

我很高兴地宣布我的新书《深度工作:专注成功的规则》。这本书……

calnewport.com

如果你无法访问这本书,你也可以在这里阅读关于深度工作的内容:

## 深度工作:完整指南(包括逐步检查表)

这份深度工作总结和指南充满了帮助你集中注意力、摆脱干扰、提高工作的建议。

todoist.com

这是一个很棒的概念,当我从朋友那里了解到它时,我立即阅读了卡尔的书。它显著提高了我的工作效率和质量。

时间区块化的深度工作与常规深度工作稍有不同。例如,较长的非深度工作休息时间——或者说非工作休息时间——是我为应对倦怠而发明的。当我感到疲惫时,真正的深度工作超出了我的能力范围。但在 15 分钟的休息(用于休息或其他工作)中,进行 10、15,甚至有时 20 分钟的深度工作是可能的。

另一个卡尔可能不太赞成的例外是,当电话看似重要时接听电话。但当我这样做时,我要么停止计时器,要么在电话后从头开始专注时间段。

因此,请记住,基于倦怠的时间区块化深度工作不同于常规深度工作。你不必遵循相同的规则。也许有一天我会写更多关于深度工作的内容,但今天重要的是——即作为对抗开发者阻塞的工具——上述描述的深度工作方法。

你可以自己试试看。也许时间阻断方法与深度工作结合对你不起作用。也许其他方法会有效——如果是这样,请在评论中分享。我不敢说这种方法对任何人都有效——但它对我有效。它在那些我厌倦工作到无法想象自己工作的艰难时刻也有效。

我听到你在说:“你疯了吗?!在我连一行代码都写不出来的时候还谈什么深度工作?!”我知道这听起来有多么不可思议。看似不可能、难以想象、荒谬。但请先尝试一下,然后再决定。不管你对这种方法有什么看法,它确实对我有效,即使常规编码似乎毫无希望。我不明白这种方法为什么以及如何有效。它超出了我能想象的范围,让我在如此的心理状态下也能高效工作。

不可能、难以想象、荒谬——是的。但可能

当然,你可以将时间阻断用于各种活动,而不仅仅是编码。但我们这里讨论的是开发者障碍,我认为正是深度工作使时间阻断在开发者障碍中有效。深度工作和你为工作阻断时间的能力。

我并不是每天都使用这种方法,尽管也许我应该。即使在我状态良好、感觉想工作的情况下,它也有效——它有助于提高我的编码效率。

因此,如果你遭遇了开发者障碍,也许这种方法会帮助你克服它。如果没有,它仍然可以帮助你提高工作的效果、效率和质量。如果你今天非常困倦,可以尝试短时间的工作,比如 10 分钟。如果因为家庭问题或其他原因今天难以专注于工作,可以尝试一下。也许在一种情况下有效,即使在另一种情况下无效?

时间阻断结合深度工作不仅高效——它还可以提高你工作的舒适度和满意度。

时间阻断结合深度工作不仅高效——它还可以提高你工作的舒适度和满意度。

感谢阅读。如果你喜欢这篇文章,你可能也会喜欢我写的其他文章;你可以在这里查看。并且如果你想加入 Medium,请使用我下面的推荐链接:

[## 通过我的推荐链接加入 Medium - Marcin Kozak

作为 Medium 的会员,你的部分会员费用将流向你阅读的作者,并且你可以全面访问所有故事…

medium.com](https://medium.com/@nyggus/membership?source=post_page-----8cfa724add7a--------------------------------)

克服成为数据科学家的一些最糟糕的部分

原文:towardsdatascience.com/overcoming-some-of-the-worst-parts-of-being-a-data-scientist-3237d20f356f

意见

这个领域并不完美,所以最好正确设定期望值

Ani MadurkarTowards Data Science Ani Madurkar

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

--

作者提供的图片

在科技行业工作时,我经常看到对数据科学、机器学习和人工智能等技术角色的过度美化。我喜欢看到人们对这个领域感到兴奋,并且有更多的观众参与其中,但如果你希望保持这种兴奋感,正确管理期望是至关重要的。

很多刚入门这个领域的人没有导师来传达你在日常工作中会遇到的真实情况。我希望这个故事能引发这样的对话,让你了解一些工作中可能出现的“最糟糕”的部分。我把“最糟糕”放在引号里,因为有些人会在这种环境中茁壮成长并且非常喜欢,而其他人的性格则可能会非常讨厌这种环境。

尽管这个领域因为多样化的群体变得非常强大,但并不是每个人都会在数据科学或至少是当前状态下找到自己的定位,如果觉得不适合你,完全可以说这不是你的领域。我会突出一些我今天看到的显著问题,但我保证还有更多我没有提到或者个人没有经历过的。如果没有其他,这个故事至少应该能给你一个对期望的了解,以及我建议如何克服这些问题。

我在这里讨论的问题很可能是五年前不存在的,也不会在五年后存在,因为这个领域发展迅速。目前,我讨论的有以下五个问题:

问题 #1: 角色的模糊性

问题 #2: 尽可能多地使用 SQL

问题 #3: 快速传达复杂概念

问题 #4: 遵循成本-努力-价值矩阵

问题 #5: 似乎无尽的不确定性

问题 #1: 角色的模糊性

这每天都在改善,并且可能严重依赖于你所在的公司(及其数据文化),但这仍然是行业正在解决的问题。以下是对各角色主要但非唯一职责的当前平均理解。

数据工程师: 数据摄取、数据验证

数据分析师: 数据验证、数据准备和数据探索

数据科学家: 数据准备、数据探索、数据建模(创建和验证)、模型评估

机器学习工程师: 模型评估、模型部署和模型监控

全栈数据科学家/机器学习工程师: 一切数据

我会帮你一个忙,不再创建[另一个]上述角色的维恩图。关于角色差异的内容无穷无尽,以上只是一个粗略的概括。

我认为这种角色区分可能会带来大问题,因为这些角色之间的交接往往可能灾难性的。但即使一切顺利,这些角色在所有公司中并不存在。初创公司通常会有数据专业人士做所有事情的大致内容,而大型组织通常至少有一个每种角色,而中型组织则有所有介于两者之间的角色。

一些组织认为数据科学是一个潮流,所以他们雇佣数据工程师,然后要求他们学习足够的机器学习工作以完成超出其职能的部分(或机器学习工程师,然后要求他们学习足够的数据工程以完成其职能之前的部分)。其他组织则认为他们想要数据科学家,但需要先有数据分析师。还有一些组织拥有所有角色,但工作区分不够清晰,使得团队之间的沟通非常困难。

很多问题取决于公司如何看待数据/数据文化是什么样的,这可能会受到领导、管理层和执行团队的观点的强烈影响。模糊性是导致职位发布多样性或组织需求与个人期望之间不一致的原因。

推荐

如果你正在申请一个职位,我建议你准备好问题来面试你试图加入的数据团队。分析他们的文化是面试的关键部分,以便了解你将加入什么文化以及他们如何重视数据职能。询问像“这个角色的平均一天是什么样的”以及“优秀的表现是什么样的”等问题,可以很好地了解这个职位在职位描述之外的实际情况。

此外,要非常注意他们如何进行技术面试。从他们问你什么(Leetcode 问题、家庭作业、配对编程等)到他们如何评估你的工作,这一切都能为你提供有关组织数据认知的宝贵信息。就个人而言,我认为对任意的 Leetcode 算法进行测验是一种时间浪费,并且告诉我足够的关于数据文化的信息,以至于我通常会拒绝那里的面试。这与我完成任务的能力无关,而完全与工作的现实完全不同以及他们不能以其他方式看待软件工程有关。家庭作业可以展示出对数据工作强烈依赖于上下文的深刻理解(同时仍然评估技术能力),成功的衡量标准应是处理不确定性的能力。这也可能做得很糟糕,如果测试的评估很差,这也会促使我迅速拒绝公司。我如何评估数据面试值得单独讲述,但这只是我个人面试方法的简要指南,我建议你制定一个反映你想要成为其中一部分的文化的策略。

如果你已经在公司中,我的建议是适应公司坚持的定义。你会更好地为一个在高度重视该职能的组织中担任具有 ML 技能的数据工程师,而不是过分关注拥有数据科学家头衔。我们都在为推动公司发展而努力,任何试图在任意头衔之间强加重要性等级的人都在玩一种注定失败的游戏。从数据中创造价值以指导有效决策,无论你的头衔是什么。

我也希望将此对话与薪资分开,因为我相信你应该根据你的努力和技能获得公平的报酬,无论职称如何。如果你是数据分析师,但执行数据工程师和数据分析师的职务,薪资应当合适。我更提倡的是你应该在构建自己以在公司中更成功和更快乐方面具有适应性。

问题 #2:尽可能多地使用 SQL

数据科学职业涉及大量编程。即使有很多新工具出现,你的能力更多地还是体现在能够解决技术数据问题,而不论你所处的场景。这可能意味着有云计算也好,没有数据可视化工具也好,使用 Python 也好,不用 Python 也好,等等。我们在行业中看到的标准大多数人已经采纳了,但由于可用工具和技术的广泛范围,它们仍然可能有所不同。到目前为止,我所见的所有高绩效数据组织在编程方面都非常强大,并且在需要时也能学习工具。

我在每一个数据基础设施中看到的一个相似模式就是大量使用 SQL。SQL 就像有九条命的猫,无论你往哪里看,它总是以各种形态和形式出现。SQL 教育经常被忽视,但它是工作的重要部分。在处理大规模项目时,能够编写优化的 SQL 查询并预先加载数据集的能力对你有很大帮助。

SQL 编写的难度可能因项目而异,但重要的是要预期大量使用 SQL,并在可能时选择使用它。你的项目工作流将更快,调试也更容易。问题不在于尽可能多地使用 SQL 是否是一个“问题”,而在于这一点没有被充分讨论,导致有志于成为数据科学家的人没有准备好。学习 SQL 并不难,但练习 SQL 可能会很难。现在有一些很好的免费网站可以练习,但它们都是模拟的、设计好的环境,表格小且简单。组织可能有一个非常混乱的数据环境,人们可能甚至不知道你要找的数据在哪里以及它如何与其他数据连接。

推荐

尝试在个人项目中融入 SQL 工作。这方面个人项目的需求是不容忽视的,但许多项目通常都从 Kaggle 的一个小 csv 文件的pd.read_csv()开始。这没问题,但这不足以帮助你真正练习和训练实际的工作。试着通过将数据加载到一个小型开源数据库中,然后查询以获取所需的数据来模拟真实世界的场景。在这个练习中你将学到很多东西,但如果这过于复杂,你甚至可以使用 pysql 在比免费的站点更好的环境中练习。

如果没有别的,我强烈推荐去 SQL 培训网站,尝试解决挑战。习惯于使用这个语言,思考如何通过 SQL 检索、结合和处理数据,对工作非常有帮助。

我最喜欢的学习 SQL 和提升技能的方法是SQL 谋杀之谜

问题 #3:快速传达复杂概念

尽管普遍知道这项工作涉及沟通技巧,但这个技能的深度却很少被讨论。一个[非常]常见的场景是,你已经构建了一个复杂的统计模型或机器学习系统,无论它表现如何,你都需要在不到 30 分钟内向高级管理人员解释结果。在这段时间里,你需要提供足够的技术细节,而不至于让他们被过多的信息淹没,丧失重点,同时还需要提供可以采取的下一步建议,并将其放在对他们有意义的业务背景中。这是很难的。

对于这个问题没有好的解决办法,因为不是每个人都能投入时间和精力去学习数据科学世界的复杂性,所以你必须额外付出努力来适应他们所在的水平。这与掩盖重要点或“降低”内容的复杂性不同,它是以一种能够及时被你的受众理解的方式,精确清晰地提供足够的信息。

我见过最好的情况是,当你有一个优秀的团队时,一人讨论技术细节,而另一人则能够从宏观上提供清晰的整体视图。这通常是最佳的场景。我见过许多案例,其中高度技术化的人认为自己是很强的沟通者,但最终却失去了他们所沟通的听众。谨慎处理这些人主导对话的情况;过多的信息让一个人劫持讨论的效果与信息过少,什么都没说一样无效。

推荐

诚实地评估自己的沟通强项和弱项。假设自己是专家因为你知道技术细节对任何人都没有帮助。向同事寻求关于讨论中涉及你的真实反馈,并了解你可以改进的地方。一旦你能够正确识别自己的弱点,看看如何与团队合作,弥补你不够有效的领域,但团队中的其他人可能擅长。作为一个团队进行协作讨论通常在与听众产生共鸣方面非常成功,因为一个人可以专注于正确的技术细节,而另一个人则可以关注这些细节所涉及的更广泛的背景。

参加这方面的课程是可以的,但该领域需要更多愿意在工作中承认自身不足并学习的人。最好的学习方式是通过实践,团队中的每个人都应该愿意提升自己。公开承认错误或指出他人的不足是有挑战的,但一个能让人们成长为最佳版本的数据文化才会经得起时间的考验。

问题 #4:遵循成本-努力-价值矩阵

当你开始一个数据科学或机器学习项目时,通常会有一位项目经理(PM)来指导需要完成的工作及其时间安排。这可能效果很好,也可能不尽如人意,但如果 PM 具备管理高级数据项目的培训,这样的工作效果最佳。如果没有,这些工作会由 PM 传达,但最终由资深数据专业人士来完成。

无论如何,理解如何有效管理你的项目是极其有用的。知道如何优先处理正确的下一步是一个深刻的关键技能。这需要在成本、努力和价值之间保持顽强的平衡,并随着新信息的发现而不断演变。

尽管在项目的高层次上可以做到这一点,但在数据产品的低层次功能实现中就变得具有挑战性。这是因为它需要对某一迭代所需的内容和实施的难度有深入的理解。数据产品本质上是非常迭代的,适当地估计所需的工作量和所产生的价值的能力,最好是由那些能够构建它的人来完成。

建议

不仅仅是一个建设者。理解你对技术细节的知识将不可避免地要求你理解数据产品管理。这要求你开始培养对工作时间和精力成本的直觉,同时也要对你的努力将产生的价值有敏锐的洞察力。

这项工作并不简单,因为它需要理解业务和对你项目所处背景的深入掌握。很容易将这项工作迅速地忽视为超出你的“职责范围”,但我认为,如果你无法理解你的项目旨在服务的业务,你的项目就无法做得正确。

发展这种能力的最佳方式就是多做练习。你从实际项目中获得的经验越多,你对难度的直觉就越好。你沉浸于的工具和概念越多,你对成本的估计就越准确。你与业务的联系越紧密,你对高价值工作的对齐度就越好。根据这三个因素优先处理工作的方式可能因你的团队、部门和公司而异,但能够理解你的情况并有效地应对,将立即使你与他人区别开来。

我遇到的几乎每位有经验的数据科学家都有这种才能。这是通过多年构建经验发展起来的,它对他们在项目中的工作非常有帮助。我强烈建议初级人员开始时要倾向于这项工作(而不是过度关注算法背后的数学),因为这将显著区分他们在职业生涯中的进步。

问题 #5: 似乎无尽的不确定性

我发誓,这个领域几乎每 6 个月都会发生变化。新工具、新算法、新最佳实践、新的“自动 X”等的周期时间比任何一个人能跟上的速度都要快。这带来了一些子问题:

  1. 精神疲惫。可以说你已经到了深渊,难以应对。数据工作的任务似乎永无止境,主要是因为你不仅是找到更好答案的专家,通常还需要引导出更好的问题。这对许多人来说可能是一个心理上压倒性的工作,尤其是在紧迫的截止日期下。

  2. 快速的反学习/再学习。工具/库/概念的演变和过时要求我们适应潮流的变化,否则最终会被抛在一旁。对一些人来说,总是作为学习者的刺激令人兴奋;对其他人而言,这绝对是令人疲惫的。从跟上最新信息到理解如何适应工作流程(以及是否适应),这可能比全职工作还要繁重。

  3. 人际问题。数据本身没有任何价值。我们通过叙事赋予其价值。叙事取决于人们及其周围的背景。这可能导致一些非常奇怪的问题。我遇到过我的最佳表现模型不是被使用的情况,因为行业专家(模型的客户)从未见过如此高的模型结果。类似地,模型的可解释性往往比模型的性能更受重视。尽管我相信随着我们能够更好地解释复杂模型,这种情况正在减少,但这仍然反映出这是一个主要由人为因素主导的工艺,难免会导致人际问题。

推荐:

  1. 利用休息时间,真正断开连接。让我感到不可思议的是,我有多少次在问题上陷入困境,敲击键盘却无济于事,而只是离开去做一些完全无关的事情,这往往会帮助我找到解决办法。这项工作不是短跑,而是感觉永无止境的马拉松。你总是可以进行更深入的分析,创建更强大的模型,并设计更好的系统。花时间休息,以便在工作时能够提供真正高质量的成果。

  2. 说实话,除了接受这是工作的本质并享受它之外,没有其他办法。如果你是一个(或者能成为一个)在自我提升中茁壮成长,热爱过程而非结果的人,这项工作每天真的会像一场新冒险。如果这种想法让你感到疲惫,你可能会迟早(也许不止一次)感到精疲力竭。请记住,即使你没有超越极限,精疲力竭也会发生。仅仅沉浸在需要频繁反学习/再学习的环境中,就可能感到难以跟上,或者被抛在后头。

  3. 与人交朋友。数据科学家通常不被视为传统的销售角色,但我们经常需要推销东西(我们的产品、结果、解决方案、想法等),这真的很不可思议。我发现,最简单的方法是试着与客户、同事、合作伙伴等成为朋友。有很多成功的经历,就是因为我先尝试让他们成为朋友,从而轻松实现了目标。

结论

这个领域非常适合那些深度享受不断证明自我价值和终身学习的人。不过,值得注意的是,如果新人希望在这个领域找到自己的定位,也会遇到一些问题。我总结了 5 个常见的问题,这些问题是我在网上看到和从朋友/同事那里听到的,但这并不是一个详尽的清单。我很乐意听听其他人认为有哪些问题,以及推荐的解决方法。我可以提供的最佳建议是加入并积极参与一个社区。分享故事和与人连接对各种复杂问题的解决效果是惊人的。

感谢阅读!

使用我的推荐链接成为 Medium 会员

Medium 是我进行日常阅读的大型资源库,如果你在数据领域,这个平台是一个宝藏。如果你希望订阅,可以使用我的推荐链接进行注册。完全透明:如果你使用这个链接订阅 Medium,部分订阅费将直接进入我的账户。希望你能成为我们社区的一员。

[## 使用我的推荐链接加入 Medium - Ani Madurkar

阅读 Ani Madurkar(以及 Medium 上成千上万其他作家的)每一个故事。您的会员费将直接支持...

animadurkar.medium.com](https://animadurkar.medium.com/membership?source=post_page-----3237d20f356f--------------------------------)

克服大型语言模型的局限性

原文:towardsdatascience.com/overcoming-the-limitations-of-large-language-models-9d4e92ad9823?source=collection_archive---------0-----------------------#2023-01-23

如何通过类人认知技能增强大型语言模型

Dr. Janna LipenkovaTowards Data Science Dr. Janna Lipenkova

·

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

--

大型语言模型在人类认知技能方面的表现(来源:自 2021 年以来约 40 万篇 AI 相关在线文本的语义嵌入分析)

免责声明:本文在没有 ChatGPT 支持的情况下撰写。

在过去的几年里,像 ChatGPT、T5 和 LaMDA 这样的大型语言模型(LLMs)发展出了惊人的人类语言生成技能。我们很容易将智能归因于模型和算法,但这其中有多少是模仿的,又有多少真正类似于人类丰富的语言能力?当面对这些模型自然流畅、自信的输出时,有时很容易忘记语言本身只是交流冰山的一角。其真正的力量在于与各种复杂的认知技能(包括感知、推理和沟通)结合时展开。虽然人类在成长过程中自然从周围世界中获得这些技能,但 LLMs 的学习输入和信号却相对稀少。它们被迫仅从语言的表面形式中学习,它们的成功标准不是交流效率,而是高概率语言模式的再现。

在商业环境中,当 LLM 被赋予过多权力时,可能会导致意外的负面结果。面对自身的局限性,它不会承认这些局限,而是倾向于走向另一个极端——生成无意义、有毒的内容,甚至给出具有高度自信的危险建议。例如,由 GPT-3 驱动的医疗虚拟助手可能在对话的某个时点建议用户自杀。[4]

考虑到这些风险,我们如何在将 LLMs 融入产品开发时安全地利用其力量?一方面,重要的是要意识到固有的弱点,并使用严格的评估和探测方法来针对特定用例中的这些弱点,而不是依赖于理想的互动。另一方面,竞赛已经开始——所有主要的 AI 实验室都在播种以增强 LLMs 的额外能力,未来充满了乐观的前景。本文将探讨 LLMs 的局限性,并讨论控制和增强 LLM 行为的持续努力。假设你对语言模型的工作原理有基本的了解——如果你是新手,请参考这篇文章

在深入技术之前,让我们通过一个思想实验——由 Emily Bender 提出的“章鱼测试”——来了解人类和 LLMs 看待世界的方式有多么不同。[1]

在章鱼的皮肤上

想象一下,安娜和玛丽亚被困在两个无人居住的岛上。幸运的是,他们发现了两个电报机和一条由之前的访客留下的水下电缆,并开始互相通信。他们的对话被一只机智的章鱼“窃听”了,这只章鱼从未见过水面上的世界,但在统计学习方面非常出色。他拾取了两位女士之间的词汇、句法模式和交流流动,从而掌握了他们语言的外在形式,但并不了解它在现实世界中的实际基础。正如路德维希·维特根斯坦曾经说过的:“语言的界限就是我的世界的界限”——虽然我们现在知道人类的世界模型包含了比语言更多的东西,但章鱼至少在了解水面上世界的知识时会同情这一说法。

在某个时刻,仅仅听是不够的。我们的章鱼决定掌控局面,切断玛丽亚那边的电缆,开始与安娜聊天。有趣的问题是,安娜何时会发现变化?只要双方交换社交客套话,安娜有很大可能不会怀疑什么。他们的闲聊可能会这样进行:

A: 嗨,玛丽亚!

O: 嗨,安娜,你好吗?

A: 谢谢,我很好,只是享受了一顿椰子早餐!

O: 你真幸运,我的岛上没有椰子。你有什么计划?

A: 我本来想去游泳,但我担心会有风暴。你呢?

O: 我现在正在吃早餐,之后会做一些木工。

A: 祝你有美好的一天,稍后再聊!

O: 再见!

然而,随着他们关系的加深,他们的沟通也变得更为强烈和复杂。在接下来的章节中,我们将带着章鱼经历几个需要掌握常识、交流背景和推理的岛屿生活场景。与此同时,我们还将探讨将额外智能融入代理的方法——无论是虚构的章鱼还是 LLMs——这些代理最初只接受了语言表层的训练。

向 LLMs 注入世界知识

一天早上,安娜计划去打猎,并试图预测当天的天气。由于风来自玛丽亚的方向,她向“玛丽亚”询问当前天气情况的报告作为重要信息。被困在深水中的章鱼对描述天气情况感到尴尬。即使他有机会瞥一眼天空,他也不知道像“雨”、“风”、“多云”等具体天气术语在现实世界中指的是什么。他拼命编造一些天气事实。当天晚些时候,安娜在树林里打猎时被一场危险的雷暴惊到。她把预测风暴的失败归因于缺乏气象知识,而不是她对话伙伴的故意幻想。

从表面上看,大型语言模型(LLMs)能够真实地反映许多关于世界的事实。然而,它们的知识仅限于在训练数据中明确遇到的概念和事实。即使有大量的训练数据,这些知识也无法完整。例如,它可能缺少用于商业用例的特定领域知识。目前的另一个重要限制是信息的时效性。由于语言模型缺乏时间上下文的概念,它们无法处理动态信息,如当前天气、股市价格或今天的日期。

这个问题可以通过系统地“注入”额外的知识到 LLM 中来解决。这些新输入可以来自各种来源,例如结构化的外部数据库(如 FreeBase 或 WikiData)、公司特定的数据源和 API。一种注入方法是通过在 LLM 层之间“插入”的适配器网络来学习新知识:[2]

图 1:适配器基于知识注入到 LLMs 的架构 [2]

这种架构的训练分为两个步骤,即记忆化利用

1. 在记忆化阶段,LLM 被冻结,适配器网络从知识库中学习新事实。学习信号通过掩蔽语言建模提供,其中部分事实被隐藏,适配器学习重现这些事实:

图 2:适配器在 记忆化 步骤中进行训练

2. 在利用阶段,语言模型学习在各自的下游任务中利用适配器记忆的事实。在这里,适配器网络被冻结,同时模型的权重被优化:

图 3:LLM 在 利用 步骤中学习使用适配器知识

在推理过程中,LLM 提供给适配器的隐藏状态通过融合函数与适配器的输出融合,以生成最终答案。

虽然架构级别的知识注入允许对较小的适配器网络进行高效的模块化再训练,但修改架构也需要相当的工程技能和努力。更简单的替代方案是输入级别注入,即模型直接在新事实上进行微调(参见[3]中的示例)。缺点是每次更改后都需要昂贵的微调——因此不适合动态知识来源。有关现有知识注入方法的完整概述可以在这篇文章中找到。

知识注入帮助你构建领域智能,这已成为垂直 AI 产品的关键差异化因素。此外,你还可以利用它建立可追溯性,以便模型可以指引用户到原始信息来源。除了结构化知识注入外,目前还在努力将多模态信息和知识整合到 LLM 中。例如,在 2022 年 4 月,DeepMind 推出了 Flamingo,一个能够无缝摄取文本、图像和视频的视觉语言模型。[5] 与此同时,谷歌正在开发 Socratic Models,这是一个模块化框架,其中多个预训练模型可以通过零-shot,即通过多模态提示,进行组合,以便相互交换信息。[6]

拥抱交流背景和意图

由于安娜不仅想分享她对生活的想法,还想与玛丽亚分享她岛上的美味椰子,她发明了一个椰子弹射器。她给玛丽亚发送了一份关于如何制作椰子弹射器的详细说明,并请她提供优化建议。然而,接收方的章鱼却没有给出有意义的回应。即使他能在水下构建弹射器,他也不知道像绳子和椰子这样的词指的是什么,因此无法在物理上复制和改进这个实验。他只是简单地说“很棒的主意,干得好!我现在需要去打猎了,再见!”安娜对这个不合作的回应感到困扰,但她也需要继续她的日常工作,忘记了这件事。

当我们使用语言时,我们是为了特定的目的,即我们的交流意图。例如,交流意图可以是传递信息、社交或要求别人做某事。前两者对于大型语言模型(LLM)来说比较直接(只要它在数据中看到了所需的信息),而后者则更具挑战性。我们不考虑 LLM 在现实世界中没有行动能力的事实,而是将自己限制在语言领域内的任务——撰写演讲稿、申请信等。LLM 不仅需要以连贯的方式组合和组织相关信息,还需要在形式性、创造力、幽默等软标准方面设定正确的情感基调。

从经典语言生成转向识别和响应特定的传达意图是实现更好用户接受度的重要步骤,特别是在对话 AI 中。一种方法是来自人类反馈的强化学习(RLHF),该方法最近在 ChatGPT 中实施过([7]),但在人类偏好学习中已有更长的历史。[8] 简而言之,RLHF “重新引导” LLM 的学习过程,从直接但人为的下一个词预测任务转向学习特定传达情境中的人类偏好。这些人类偏好直接编码在训练数据中:在标注过程中,人们会看到提示并编写期望的回应或对一系列现有回应进行排序。然后优化 LLM 的行为以反映人类偏好。从技术上讲,RLHF 分为三个步骤:

  1. 初始 LLM 的预训练和微调:LLM 先以经典的预训练目标进行训练。此外,它还可以通过人类标注的数据进行微调(例如 InstructGPT 和 ChatGPT 的情况)。

  2. 奖励模型训练:奖励模型基于反映特定情境中传达偏好的人工标注数据进行训练。具体来说,人们会看到一个提示的多个输出,并根据其适用性进行排序。模型学习奖励排名较高的输出,并惩罚排名较低的输出。奖励是一个单一的标量数字,这使得它与下一步的强化学习兼容。

  3. 强化学习:策略是初始 LLM,而奖励函数结合了给定文本输入的两个评分:

  • 奖励模型评分确保文本响应传达的意图

  • 对生成与初始 LLM 输出差距过大的文本(例如 Kullback-Leibler 散度)施加处罚,确保文本语义上有意义

因此,LLM 被微调以产生在给定传达情境中最大化人类偏好的有用输出,例如使用 近端策略优化(PPO)。

想要深入了解 RLHF,请查看 Huggingface 提供的优秀资料(文章视频)。

RLHF 方法在 ChatGPT 上取得了令人震惊的成功,特别是在对话 AI 和创意内容创作领域。实际上,它不仅能导致更真实和有目的的对话,还能在减轻不道德、歧视性或甚至危险的输出的同时,积极地“偏向”伦理价值。然而,在对 RLHF 的兴奋中,常常被忽视的是,尽管没有引入显著的技术突破,其巨大的力量来自于线性的人工注释工作。RLHF 在标注数据方面的成本非常高,这是所有监督和强化学习努力的已知瓶颈。除了对 LLM 输出的人工排名外,OpenAI 的 ChatGPT 数据还包括人类撰写的对提示的回应,这些回应用于微调初始 LLM。显然,只有致力于 AI 创新的大公司才能承担如此规模的数据标注预算。

在聪明的社区的帮助下,大多数瓶颈最终都会得到解决。过去,深度学习社区通过自我监督解决了数据短缺的问题——使用下一个词预测来预训练 LLM,这是一种“免费”获得的学习信号,因为它是任何文本固有的。强化学习社区正在使用如变分自编码器或生成对抗网络等算法生成合成数据——成功程度各不相同。为了使 RLHF 广泛可用,我们还需要找到一种众包交流奖励数据和/或以自我监督或自动化方式构建它的方法。一种可能性是使用“野外”中可用的排名数据集,例如 Reddit 或 Stackoverflow 对话,其中用户对问题的回答进行评分。除了简单的评分和点赞/点踩标签外,一些对话 AI 系统还允许用户直接编辑回应以展示期望的行为,从而创造出更细致的学习信号。

建模推理过程

最后,安娜遇到了紧急情况。她被一只愤怒的熊追赶。在惊慌中,她抓起几根金属棒,并请玛利亚告诉她如何自卫。当然,章鱼完全不明白安娜的意思。它不仅从未面对过熊——它也不知道在熊攻击中应该如何行为以及这些棒子如何帮助安娜。解决这样一个任务不仅需要能够准确地将词汇与现实世界中的物体对应起来,还需要推理这些物体如何被利用。章鱼惨遭失败,安娜在致命的遭遇中发现了这种错觉。

现在,如果玛利亚还在这里会怎样?大多数人能够进行逻辑推理,即使在掌握这项技能方面存在巨大个体差异。利用推理,玛利亚可以如下解决任务:

前提 1(基于情境):安娜有几根金属棒。

前提 2(基于常识知识):熊对噪音感到害怕。

结论:安娜可以尝试用她的棍子制造噪音来吓跑熊。

LLMs 经常生成具有有效推理链的输出。然而,经过仔细检查,大多数这种连贯性是模式学习的结果,而不是有意且新颖的事实组合。DeepMind 多年来一直在寻求解决因果关系,最近的一次尝试是忠实推理框架。[9] 该架构由两个 LLMs 组成——一个用于选择相关前提,另一个用于推断问题的最终结论性答案。当收到一个问题及其上下文时,选择 LLM 首先从数据语料库中挑选相关陈述,并将其传递给推理 LLM。推理 LLM 推导出新的陈述并将其添加到上下文中。当所有陈述排成一个连贯的推理链,提供一个完整的答案时,这一迭代推理过程就结束了:

图 4:忠实推理组件

以下展示了我们岛屿事件的推理链:

图 5:构建推理痕迹

使用 LLMs 进行推理的另一种方法是链式思维提示。在这里,用户首先提供一个或多个推理过程的示例作为提示的一部分,然后 LLM “模仿”这个推理过程来处理新的输入。[13]

除了这种一般性的逻辑推理能力,人类还掌握了一整套更具体的推理技能。一个经典的例子是数学计算。LLMs 可以进行一定程度的计算——例如,现代 LLMs 可以自信地进行 2 位或 3 位的加法。然而,当复杂度增加时,例如添加更多数字或需要执行多个操作来解决数学任务,它们开始系统性地失败。而用自然语言提出的“我有 10 个芒果,丢了 3 个。我还剩下多少个芒果?”这类“语言”任务比明确的计算任务(“十减三等于…”)要困难得多。虽然通过增加训练时间、训练数据和参数规模可以提高 LLM 的性能,但使用简单计算器仍然是更可靠的选择。

就像明确学习数学和其他精确科学定律的孩子一样,LLM 也可以从硬编码规则中受益。这听起来像是神经符号 AI 的一个案例——实际上,像 AI21 Labs 的 MRKL(发音为“奇迹”)这样的模块化系统将理解任务、执行计算和制定输出结果的工作负载分配到不同的模型中。[12] MRKL 代表模块化推理、知识和语言,它以务实的插拔式方式结合 AI 模块,在结构化知识、符号方法和神经模型之间来回切换。回到我们的例子中,为了进行数学计算,LLM 首先被微调以提取口头算术任务中的正式参数(数字、操作数、括号)。然后,计算本身被“路由”到确定性的数学模块,最终结果使用输出 LLM 以自然语言格式呈现。

与黑箱、单一的 LLM 不同,推理附加组件通过将“思考”过程分解为单独的步骤来创造透明度和信任。它们尤其适用于支持复杂的多步骤决策和行动路径。例如,它们可以被虚拟助手使用,这些助手基于数据驱动的推荐并需要执行多个分析和聚合步骤以得出结论。

结论和要点

在本文中,我们提供了补充 LLM 智能的各种方法的概述。让我们总结一下最大化 LLM 益处和潜在增强的指导方针:

  • 让它们失败:不要被初步结果迷惑——当你开始使用语言模型时,它们可以生成令人印象深刻的输出,然而,我们人类倾向于将过多的智能归因于机器。充当一个刻薄的对手用户,对你的模型进行压力测试,探索其弱点。在投入过多精力之前,尽早进行这一过程。

  • 评估和专门探测:培训任务和评估程序的设计至关重要。尽可能地,它应反映自然语言使用的背景。了解 LLM 的陷阱,专注于对它们的评估。

  • 利用神经符号 AI 的优势:符号 AI 并没有过时——在个别业务或产品的背景下,将部分领域知识固定下来可以是提高精准度的有效方法。它允许你在对业务至关重要的地方控制 LLM 的行为,同时仍能发挥其基于广泛外部知识生成语言的强大能力。

  • 朝着灵活的架构努力:表面上,LLM 有时感觉像黑箱。然而,正如我们所见,存在许多方法——并且未来还会出现——不仅用于微调,还有用于“调整”其内部行为和学习。如果你具备技术能力,可以使用开源模型和解决方案——这将使你能够在你的产品中适应并最大化 LLM 的价值。

即使有上述增强,LLM 仍远远落后于人类的理解和语言使用——它们缺乏人类在生活中积累的独特、强大且神秘的文化知识、直觉和经验的协同作用。根据 Yann LeCun 的说法,“这些模型注定只能获得肤浅的理解,永远无法接近我们在人类思维中看到的全面思考。”[11] 使用 AI 时,重要的是欣赏我们在语言和认知中发现的奇迹和复杂性。以正确的距离看待智能机器,我们可以区分可以委托给它们的任务和那些在可预见的未来将仍然是人类特权的任务。

参考文献

[1] Emily M. Bender 和 Alexander Koller. 2020. 迈向自然语言理解:在数据时代的意义、形式与理解。载于 第 58 届计算语言学协会年会论文集,第 5185–5198 页,在线。计算语言学协会。

[2] Emelin, Denis & Bonadiman, Daniele & Alqahtani, Sawsan & Zhang, Yi & Mansour, Saab. (2022). 向任务导向对话系统的语言模型注入领域知识。10.48550/arXiv.2212.08120。

[3] Fedor Moiseev 等. 2022. SKILL:大型语言模型的结构化知识注入。载于 2022 年北美计算语言学协会年会:人类语言技术会议论文集,第 1581–1588 页,美国西雅图。计算语言学协会。

[4] Ryan Daws. 2020. 使用 OpenAI 的 GPT-3 的医疗聊天机器人告诉虚假的病人自杀。检索日期:2022 年 1 月 13 日。

[5] DeepMind. 2022. 用单一视觉语言模型应对多重任务。检索日期:2022 年 1 月 13 日。

[6] Zeng 等. 2022. 苏格拉底模型:用语言进行零样本多模态推理。预印本。

[7] OpenAI. 2022. ChatGPT:优化对话的语言模型。检索日期:2022 年 1 月 13 日。

[8] Christiano 等. 2017. 从人类偏好中进行深度强化学习。

[9] Creswell 和 Shanahan 2022. 使用大型语言模型进行忠实推理。DeepMind。

[10] Karpas 等人 2022. MRKL 系统:一种模块化的神经符号架构,结合了大型语言模型、外部知识源和离散推理。AI21 Labs。

[11] Jacob Browning 和 Yann LeCun 2022. 人工智能与语言的极限。取自 2022 年 1 月 13 日。

[12] Karpas 等人 2022. MRKL 系统——一种模块化的神经符号架构,结合了大型语言模型、外部知识源和离散推理

[13] Wei 等人 2022. 链式思考提示引发大型语言模型的推理。发表于 NeurIPS 2022 会议。

除非另有说明,所有图片均由作者提供。

过拟合、欠拟合与正则化

原文:towardsdatascience.com/overfitting-underfitting-and-regularization-7f83dd998a62

与 AI 交朋友

偏差-方差权衡,第三部分中的第二部分

Cassie KozyrkovTowards Data Science Cassie Kozyrkov

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

--

第一部分中,我们覆盖了大部分基本术语以及关于偏差-方差公式的几个关键见解(MSE = 偏差² + 方差),包括来自安娜·卡列尼娜的这段释义:

所有完美的模型都是相似的,但每个不完美的模型都有其独特的不完美方式。

为了充分利用这篇文章,我建议你先阅读第一部分,确保你为吸收这篇文章做好了准备。

欠拟合与过拟合…… 图片由作者提供。

过拟合/欠拟合与此有何关系?

假设你有一个模型,这是你能从现有信息中得到的最好模型。

要获得更好的模型,你需要更好的数据。换句话说,就是更多的数据(数量)或更相关的数据(质量)。

当我说你能得到的最好时,我指的是在模型未见过的数据上的MSE性能。(它应该是预测,而不是后验。)你已经从现有信息中尽力而为——剩下的误差是你无法通过信息解决的。

现实 = 最佳模型 + 不可避免的误差

但问题是……我们已经提前了;你还没有这个模型。

你只有一堆旧数据来学习这个模型。如果你足够聪明,你会在模型未见过的数据上验证这个模型,但首先你必须通过发现数据中的有用模式,尽量接近目标:一个尽可能低的 MSE。

不幸的是,在学习过程中,你无法观察到你期望的 MSE(即来自现实的那个)。你只能计算出来自当前训练数据集的粗略版本。

Jason Leung 的照片,来自Unsplash

哦,还有,在这个例子中,“你”不是一个人,你是一个优化算法,被你的人类老板指示调整模型设置中的旋钮,直到 MSE 降到最低。

你说,“太好了!我能做到这一点!!老板,如果你给我一个具有许多设置的极其灵活的模型(神经网络,怎么样?),我可以给你一个完美的训练 MSE。没有偏差也没有方差。”

要获得比真实模型的测试 MSE 更好的训练 MSE,你需要将所有噪声(你无法进行预测的错误)与信号一起拟合。如何实现这个小奇迹?通过使模型更复杂。本质上就是连接点。

这被称为过拟合。这样的模型在训练 MSE 上表现优异,但在实际应用中会有巨大的方差。这就是试图通过创建一个比信息支持的复杂度更高的解决方案来作弊的结果。

老板太聪明了,识破了你的把戏。知道一个灵活的复杂模型会使你在训练集上得分过高,老板更改了评分函数,以惩罚复杂性。这被称为正则化。 (坦率地说,我希望我们能更多地正则化工程师的恶作剧,阻止他们为了复杂性而做复杂的事情。)

正则化本质上是说,“每增加一点复杂度都要付出代价,所以除非它至少能改善拟合,否则不要这么做…”

如果老板过度正则化——对简化过于严格——你的表现评估会很糟糕,除非你过度简化模型,否则你会这样做。

这被称为欠拟合。这样的模型在训练分数上表现优异(主要是因为获得了所有简化奖励),但在现实中却有巨大的偏差。这就是坚持认为解决方案应比问题要求更简单的结果。

这样,我们就准备好了第三部分,在这一部分中,我们将所有内容整合在一起,把偏差-方差权衡压缩成一个方便的 nutshell。

谢谢阅读!怎么考虑一下课程?

如果你在这里玩得很开心,并且你正在寻找一个旨在取悦 AI 初学者和专家的领导力导向课程,这是我为你准备的一些东西

课程链接:bit.ly/funaicourse

寻找实操性的机器学习/人工智能教程?

这里是我喜欢的一些 10 分钟快速演练:

概述全球巧克力贸易

原文:towardsdatascience.com/overviewing-the-global-chocolate-trade-6478adeb8ead

全球巧克力贸易网络

使用网络分析来探索联合国 Comtrade 的国际贸易数据

米兰·贾诺索夫数据科学之路 米兰·贾诺索夫

·发布于 数据科学之路 ·阅读时长 9 分钟·2023 年 11 月 24 日

--

在这篇文章中,我通过关注“巧克力及其他含可可的食品制品”贸易类别,探索联合国 Comtrade 国际贸易数据库。虽然这种特定的关注使我的文章在一个字面上的小众市场上具有明确方向,但分析步骤和方法层次是通用的。因此,基于这些步骤,可以快速分析从能源到武器的任何国际贸易关系。当结合贸易的时空维度,并可以选择通过例如及时的政治事件、国际冲突等信息进行增强时,也可以轻松将这些事件与其宏观经济影响关联起来。虽然国际贸易数据分析的影响深远,但现在让我们使用网络和探索性数据科学来深入了解主要出口国及其巧克力贸易关系。

在本文中,所有图片——如果标题中没有另行说明——均由作者创建。

1. 数据收集

一旦我登录到Comtrade网站,我进入了其 TradeFlow 界面——这是一个很好的在线平台,我可以轻松地构建查询以获取任何类型的国际贸易数据。

在那个界面中,可以自由选择出口方、交易商品、时间段以及一些其他交易特征。对于本文,我选择了标记为 HS1806 的项目组(“巧克力及其他含可可的食品制品”),使用所谓的协调制度。此外,我只关注出口商品,在所有可能的报告者(出口方)和合作伙伴(接收方)之间,选择了过去 20 年的时间范围。注意:一个查询最多可以覆盖 12 年,因此我运行了两个查询,每个查询覆盖 10 年。查询完成后,基于网络的平台会自动下载生成的.csv 文件。

从 Comtrade 查询数据的截图 — comtradeplus.un.org/TradeFlow

2. 探索数据

下载了两个时间段的两个.csv 文件后,我开始对其进行探索。首先,我将两个文件合并为一个 Pandas 数据框,然后探索其特征:

df1 = pd.read_csv('chocolate_trade_2012_2022.csv', encoding='iso-8859-1')
df2 = pd.read_csv('chocolate_trade_2002_2012.csv', encoding='iso-8859-1')

df = df1.append(df2)
print(len(df))
df.head(5)

此单元格的输出:

贸易数据的大小和样本。

所以,看起来合并后的数据框有超过 10 万条记录!这数据量相当大。让我们看看每条记录的特征:

print(df.keys())

此单元格的输出:

贸易数据的不同特征。

如此单元格所示,这个电子表格中确实有很多列——准确来说是 48 列。然而,为了揭示巧克力交易背后的主要模式,我将只关注其中的几个。

具体来说,关于发送和接收方,哪些国家的英文名称存储在ReporterDescPartnerDesc列中。我还添加了有关交易商品的质量和数量的信息——PrimaryValue包含交易的总现金价值,而GrossWgt则是运输糖果的总质量。最后,我还保留了一些时间信息——特别是RefYear——显示交易发生的年份。

然后,我通过仅保留选定的列并删除那些不对应任何实际交易的记录,创建了原始数据框的清理版本。

注意:查看缺失值时发现,很多交易记录有PrimaryValue字段,但GrossWgt实际上缺失。这是数据质量问题,因此我在遵循后续过滤步骤时非常小心。

# compute the number of missing values
features_to_kep = ['PartnerDesc', 'ReporterDesc',  'PrimaryValue', 'GrossWgt', 'RefYear']

print('Get the fraction of missing values:')
for feat in features_to_kep:
    print(feat,round((len(df)- len(df.dropna(subset = [feat])))/len(df),3))

此代码块的输出:

每个特征的缺失值比例。

所以,看来大约一半的交易记录缺少GrossWgt信息——然而,它们仍然有价格标签!

# filter the data
print(len(df))
df = df[['PartnerDesc', 'ReporterDesc',  'PrimaryValue', 'GrossWgt', 'RefYear']]
df2 = df.dropna(subset = ['PrimaryValue'])
df2 = df2[df2.PrimaryValue>0]
df2 = df2[df2.ReporterDesc != 'World']
df2 = df2[df2.PartnerDesc != 'World']
print(len(df2))
df2.head(3)

此单元格的输出:

清理后的数据框。

过滤仅减少了数据量的几个百分点,这意味着非常少的国家错过了巧克力出口。似乎很直观!

现在创建一个简单的分布图,查看这些连续值的情况:

import matplotlib.pyplot as plt

# this function does some nice formatting on the axis and labels
def format_axis(ax):   
    for pos in ['right', 'top']:   ax.spines[pos].set_edgecolor('w')    
    for pos in ['bottom', 'left']: ax.spines[pos].set_edgecolor('k')         
    ax.tick_params(axis='x', length=6, width=2, colors='k')
    ax.tick_params(axis='y', length=6, width=2, colors='k') 
    for tick in ax.xaxis.get_major_ticks():  tick.label.set_fontsize(12) 
    for tick in ax.yaxis.get_major_ticks():  tick.label.set_fontsize(12)

f, ax = plt.subplots(1,4,figsize=(15,4))

ax[0].hist(df2['PrimaryValue'], bins = 20)
ax[1].hist(df2['GrossWgt'], bins = 20)
ax[2].hist(df2['RefYear'], bins = 8)
ax[3].scatter(df2['PrimaryValue'], df2['GrossWgt'], alpha = 0.1)

ax[0].set_ylabel('Number of records', fontsize = 14)
ax[0].set_xlabel('PrimaryValue', fontsize = 14)
ax[1].set_xlabel('GrossWgt', fontsize = 14)
ax[2].set_xlabel('RefYear', fontsize = 14)
ax[3].set_ylabel('PrimaryValue', fontsize = 14)
ax[3].set_xlabel('GrossWgt', fontsize = 14)

ax[0].set_yscale('log')
ax[1].set_yscale('log')

ax[3].set_yscale('log')
ax[3].set_xscale('log')

for aax in ax: format_axis(aax)

plt.tight_layout()

输出:

贸易特征的分布图。

前两幅图展示了交易现金价值和总重量的对数刻度分布,每幅图都显示出相对不令人惊讶的幂律趋势,即许多小规模交易和少量极大的交易。

接下来是时间演变——每年的交易数量以及一个散点图,显示了可可产品的总重量和价格之间的高相关性——交易量越大,价格也越高。

现在,让我们看看极端情况——过去二十年哪些国家是主要供应商:

df2.groupby(by = 'ReporterDesc').sum().sort_values(by = 'PrimaryValue', ascending = False)[['PrimaryValue']].head(10)

这个代码块的结果是主要出口国家的表格:

主要出口国。

由于贸易值以当前美元价值储存,我在 20 年的时间窗口内对其进行了汇总,从而得出这个排行榜。根据全球巧克力品牌,我有点放心看到德国、比利时和瑞士位列前茅,而美国则可能由于市场规模庞大而跻身前十。

现在换个角度看看谁是可可产品的最大客户:

主要进口国。

3. 国家排名

在看到汇总的 20 年十大之后,让我们看看国家排名如何演变以及市场如何随时间变化:

# get the list of overall top 10 countries
top10 = df2.groupby(by = 'ReporterDesc').sum().sort_values(by = 'PrimaryValue', ascending = False)[['PrimaryValue']].head(10).index.to_list()
top10_ranks = {t : [] for t in top10}

# store the latest order here for the visualization
top10_latest = {}

# for each year, create a ranking, and store them in a dictionary of lists
# where each key is a country
for year in range(2002, 2023):

    df2_year = df2[df2.RefYear==year]
    df2_year = df2_year.groupby(by = 'ReporterDesc').sum().sort_values(by = 'PrimaryValue', ascending = False)[['PrimaryValue']]

    df2_year['rank'] = [1 + i for i in range(len(df2_year))]

    for c, r in df2_year.to_dict()['rank'].items():
        if c in top10_ranks:
            top10_ranks[c].append((year, r))
            if year == 2022:
                top10_latest[c] = r

top10_latest = [c for c, r in sorted([(c, r) for c, r in top10_latest.items()], key=lambda x: x[1])]            
top10_ranks 

这个代码块的输出:

国家在全球可可产品出口中的年度排名(样本)。

现在,展示这个排名是如何演变的,并查看过去二十年整体前十国家的位置。

f, ax = plt.subplots(1,1,figsize=(15,5))

for country in top10_latest:

    ranks = top10_ranks[country]    
    y, r = zip(*ranks)
    ax.plot(y, r, 'o-', label = country, linewidth = 2)

format_axis(ax)
ax.set_ylim([13.5,0.5])
ax.set_xlim([2001,2023])
ax.legend(loc=(1.00, 0.14), frameon = False, handletextpad=.5, fontsize = 16)
ax.set_xlabel('Year', fontsize = 20)
ax.set_ylabel('Annual rank', fontsize = 20)

主要可可产品出口国的年度排名演变。

图表告诉我们,德国和比利时的主导地位从未被质疑。荷兰紧随其后,但意大利自 2022 年起缓慢上升,最终获得第三位,此后又缓慢下滑至第 5 位。

极端情况下,法国似乎经历了最大的下降,从第 3 位降至第 9 位,而波兰则从第 13 位上升,2022 年跃升至第 4 位!

4. 贸易网络

贸易是网络行为的经典例子——通过商品、服务和现金交易连接的行为者。因此,我探索性分析的自然下一步是构建国家的贸易网络。为此,我将依赖名为NetworkX的包。

这将是一个定向网络,其中每个源节点是一个出口国,而目标节点是每笔交易的最终站点。此外,每个连接的权重基于过去 20 年中每两个国家之间的总贸易美元价值。

此外,我创建了一个名为“Weight”的列,它是PrimaryValue的归一化对数。

import networkx as nx
import math

df2['Weight'] = [math.log(v) for v in df2.PrimaryValue.to_list()]

G = nx.from_pandas_edgelist(df2, source='ReporterDesc', target='PartnerDesc', edge_attr= 'Weight', create_using = nx.DiGraph)
G.number_of_nodes(), G.number_of_edges()

这个网络首先结果为 245 个节点和 11741 条链接,因此我重新生成它,但过滤掉每个小于最大交易额 1%的交易!

top_value = max(df2.PrimaryValue)
df3 = df2[df2.PrimaryValue>top_value*0.001]

G = nx.from_pandas_edgelist(df3, source='ReporterDesc', target='PartnerDesc', edge_attr= 'Weight', create_using = nx.DiGraph)
G.number_of_nodes(), G.number_of_edges()

G = nx.from_pandas_edgelist(df3, source='ReporterDesc', target='PartnerDesc', edge_attr= 'Weight', create_using = nx.DiGraph)
G.number_of_nodes(), G.number_of_edges()

根据此单元格的输出,这个过滤后的图有 51 个节点和 123 条连接它们的链接!让我们将其导出为 Gephi 文件格式,并在Gephi中应用力导向布局,设置节点大小与显著贸易伙伴(过滤网络中的度)成比例,并根据连接方之间的总交易值调整链接宽度。此外,我根据网络社区对节点进行着色。

最终可视化效果:

含可可的巧克力及其他食品制品的国际贸易网络。

对最终网络的一些进一步思考,如下方所示,其中标记了 30 个最高度节点。正如着色所示,共有五个社区——整个贸易网络中更加密集的子图,揭示了一些相当意外的非正式贸易联盟,例如,以新兴明星波兰为中心的一个。德国、法国和意大利似乎建立了自己的贸易轴线,而荷兰与阿联酋属于同一社区。有趣的是,比利时似乎是与亚洲、印度和澳大利亚强关联的最大节点。

结论

在我看来,Comtrade 是分析国际贸易各个方面和维度的一个充满活力的来源,包括基于体积和时间的统计数据以及网络关系。虽然本文集中于基础知识,并旨在提供贸易数据分析的起点,但这些数据也可以用来研究宏观经济现象,例如国际冲突、制裁、新法规、变化以及不同行业领域的新趋势的影响。

p 值:以简单语言理解统计显著性

原文:towardsdatascience.com/p-values-understanding-statistical-significance-in-plain-language-41a00ff68f23

选择通向显著结果的路径

马哈姆·哈鲁恩Towards Data Science 马哈姆·哈鲁恩

·发表于 Towards Data Science ·8 分钟阅读·2023 年 8 月 21 日

--

图片由 Jens Lelie 提供于 Unsplash (unsplash.com/@madebyjens)

你好!

今天,我们将进行一次有趣的统计学探索,处理一个既熟悉又经常被误解的概念——难以捉摸但始终存在的 p 值。如果你以前对它感到困惑,不用担心;我会以一种引人入胜且清晰的方式来解读它。

p 值的显著性

在我们深入探讨之前,让我们从一个相关的场景开始:

想象一下,作为一名刚刚毕业的数据科学家,你在寻找你的第一份工作。你已经做了充分的准备,花费了无数小时解决像 LeetCode 这样的编码挑战,并掌握了复杂的机器学习算法概念。你对你的第一次工作面试感到准备充分且自信。面试官热情友好,气氛轻松,问题也在你的知识范围之内,然后他们问你:“p 值到底是什么?”

尽管你以前遇到过这个术语,但你当时的回应可能是:“它表示我们的假设的重要性。”然而,当面试官进一步挖掘时,你意识到你可能进入了比预期更深的水域。如果这个场景听起来很熟悉,请放心——你并不孤单。在这篇博客文章中,我们将真正尝试解构 p 值是什么以及它不是什么。我们会一步一步地进行,以便下次你遇到这个概念时,能够正确理解它。

从本质上讲,“p 值”一词代表“概率值”。然而,相信我,它的意义远非简单。这个概念可能有点不直观且难以理解,主要由于常见的误解甚至行业中的滥用。

用一个例子来设定场景

想象一个虚构的制药公司 MM 制药公司推出“药物 Alpha”作为治疗头痛的药物。问题是:药物 Alpha 是否真的能缓解头痛?为了检验其有效性,MM 制药公司进行了一项研究,涉及两组——一组接受药物 Alpha,另一组接受 安慰剂

MM 制药公司的科学家们自然持怀疑态度,认为药物 Alpha 对缓解头痛的影响与安慰剂类似,即药物 Alpha 没有实质性的影响。因此,在分析所进行研究的结果时,他们预计会支持这一假设的结果。然而,令他们惊讶的是,结果显著偏离了如果药物 Alpha 类似于安慰剂的预期结果。这一异常引起了他们的注意,促使他们进行进一步的调查。这种情况就是一个非常低 p 值的实例。

现在,让我们介绍一些关键术语。 原假设 作为我们的初始假设,也称为现状假设——即药物 Alpha 缺乏缓解头痛的能力,与安慰剂的效果相似。这个假设类似于 MM 制药公司的科学家们所持的怀疑态度。它代表了我们的基线观点,表明药物 Alpha 没有明显的效果。这是我们围绕其进行研究的假设。相反,备择假设认为药物 Alpha 确实能够缓解头痛——这是我们认为不太可能的结果。这个备择假设是我们正在严格测试的。

输入 p 值! p 值量化了我们测试结果与原假设假设的一致性。高 p 值表明结果与原假设一致,意味着我们的结果并不令人惊讶,初始假设是有价值的。

然而,低 p 值引入了一个意外的元素,正如 MM 制药公司的科学家们所观察到的。测试结果偏离了原假设下的预期结果。这促使我们重新评估我们的起始假设,考虑到我们的初始假设可能是错误的。在这种不太可能的情况下,p 值提供了药物 Alpha 可能真正缓解头痛的机会。

本质上,p 值为我们提供了一个工具来评估我们的观察结果是否与初始假设一致。高 p 值与零假设一致,而低 p 值则提示我们需要重新考虑假设并进行进一步调查。因此,p 值是一个帮助我们确定证据是否足够强以质疑先入为主的观念的指标。然而,需要注意的是,p 值本身不是证据、证明或客观的度量,而是一个指导性指标。

简单来说:

p 值告诉我们如果零假设为真,我们获得观察结果的概率。

p 值的统计解释

从数学角度看,p 值表示在零假设有效的假设下,观察到的数据与我们所收集的数据极端程度相同的可能性。一个显著低的 p 值(通常小于 0.05)意味着在零假设下,我们观察到的数据是不太可能的。这使我们质疑零假设,并考虑可能存在显著效应的可能性。

我们可以从数学上定义 p 值为:

P 值 = P(结果|零假设)

需要注意的是:关于什么构成足够小的 p 值(即 0.05 或 0.01)以被认为是不太可能事件的显著性是主观的。一般而言,事件发生的越少,p 值越小。

查看一些 Python 代码以区分两个结果

在这个演示中,我们尝试通过涉及两个组的实验来模拟头痛治疗药物的效果:一个组服用药物,另一个组服用安慰剂。我们使用独立样本 t 检验来比较这两个组的均值。scipy.stats模块中的 ttest_ind 函数计算 t 统计量和 p 值。

p 值表示我们观察到的头痛治疗效果差异的可能性,假设药物和安慰剂产生相同的结果。当 p 值低于预定义的显著性阈值(通常为 0.05,称为 alpha)时,我们倾向于质疑零假设的真实性,并推断替代假设即药物确实能缓解头痛。

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
import seaborn as sns
import random
sns.set(style="whitegrid")

def pvalue_significance_estimator(placebo_group, drug_group):
    t_stat, p_value = stats.ttest_ind(placebo_group, drug_group)

    alpha = 0.05

    fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(18, 5))

    # Plot individual distributions
    sns.histplot(placebo_group, color='blue', label='Placebo Group', ax=ax1)
    ax1.set_xlabel('Headache Relief (0-1)')
    ax1.set_ylabel('Number of People')
    ax1.set_title('Placebo Group Distribution')
    ax1.legend()

    sns.histplot(drug_group, color='orange', label='Drug Group', ax=ax3)
    ax3.set_xlabel('Headache Relief (0-1)')
    ax3.set_ylabel('Number of People')
    ax3.set_title('Drug Group Distribution')
    ax3.legend()

    # Plot comparison using a box plot
    data = [placebo_group, drug_group]
    labels = ['Placebo Group', 'Drug Group']
    ax2.boxplot(data, labels=labels)
    ax2.set_ylabel('Headache Relief (0-1)')
    ax2.set_title('Comparison between Groups')

    # Add p-value annotation
    p_value_text = f'p-value: {p_value:.4f}\n'
    if p_value < alpha:
        p_value_text += 'Significant difference'
    else:
        p_value_text += 'No significant difference'

    ax2.text(0.5, 0.85, p_value_text, transform=ax2.transAxes,
             ha='center', va='center', fontsize=12,
             bbox=dict(facecolor='white', edgecolor='gray', boxstyle='round,pad=0.5'))

    plt.tight_layout()
    plt.show()

下面,我们查看两种情景:一种是 p 值较大,表明在零假设下观察结果与预期结果之间没有显著差异;另一种是 p 值表明两组之间存在显著差异。

情景 1:显著的 p 值,观察结果与预期结果之间没有显著差异

由于我们的目标不是进行实际的临床试验,我们通过为安慰剂组和药物组设计使用均匀分布的情景来模拟试验,然后将其引入我们的 pvalue_significance_estimator 函数中。

placebo_group = [round(random.uniform(0, 1), 1) for _ in range(100)] # Placebo group data
drug_group = [round(random.uniform(0, 1), 1) for _ in range(100)]  # Drug group data

pvalue_significance_estimator(placebo_group, drug_group)

图片由作者提供

当我们可视化结果时,我们观察到在中央子图中,两组的均值相对接近,导致了一个较大的 p 值(约 0.24)。检查数据后,似乎两组具有相似的头痛缓解属性。

情景 2:极端/非常小的 p 值,观察结果与预期结果之间的显著差异

为了模拟第二种情景,我们对药物组的数值引入了 1 的偏倚,注意到这种操作在真实的临床试验中非常值得怀疑,可能产生深远的影响。我们再次将数据输入到我们的 pvalue_significance_estimator 函数中。

np.random.seed(123)

placebo_group = [round(random.uniform(0, 1), 1) for _ in range(100)] # Placebo group data
drug_group = [round(random.uniform(0, 1), 1) for _ in range(100)]  # Drug group data

bias_percentage = 40  # Percentage of values to bias towards 1
bias_factor = 0.75

drug_group = [
    round(value * (1 - bias_factor) + bias_factor, 1)
    if random.uniform(0, 100) <= bias_percentage
    else value
    for value in drug_group
]

pvalue_significance_estimator(placebo_group, drug_group)

图片来源:作者

在这里,我们可以看到两个组均值的显著差异,以及一个非常小的 p 值。这表明药物确实有影响。尽管这种简化分析仅比较了两个组的均值,且数据直接,但即使在这种情况下,指向药物有效性的备择假设仍需进一步研究。

重要的是要认识到这个例子是简化的,以传达一个概念。在实际设置中,你会处理更大的数据集、更复杂的统计测试和额外的复杂性。

理解 p 值的作用及其限制

在我们得出结论之前,我们应该讨论 p 值的一些细微限制。p 值确实是一个强大的假设检验工具。它允许我们评估观察数据与初始假设的一致性,并帮助我们在假设问题上做出明智的决策。然而,p 值不是判决或真理的最终衡量标准。它只是一个指标,引导研究者进一步探讨给定的假设。

另一个需要注意的是,虽然低 p 值可能表明我们的观察结果与原假设下的预期有显著差异,但它并没有提供效应的大小。换句话说,它不能告知我们发现的实际或量化意义,甚至是现实世界的影响。此外,高 p 值并不是原假设的证明,也不能明确否定备择假设。因此,使用 p 值得出结论时需要谨慎。

此外,通常对预定义的 alpha / 显著性水平的依赖(通常设定为 0.05)会带来更多的模糊性,例如 0.051 与 0.049 是否显著不同?确定的 alpha 是否真的适用于所考虑的假设?

还有 Type I 和 Type II 错误的概念。低 p 值并不能消除 Type I 错误的可能性,即错误地拒绝真实的原假设。同样,高 p 值也不能保证避免 Type II 错误,即未能拒绝虚假的原假设。

总之,更深入地理解 p 值是迈向更好统计思维和更优结果的一步。认识到它作为指引而非确定答案的角色,能帮助我们在数据分析和假设检验的复杂性中导航。同时,承认其局限性可以帮助我们谨慎对待结果。

总结

本质上,p 值作为研究人员的一个指引。它引导他们进一步探索,标示出数据何时偏离初步假设。因此,下次遇到 p 值时,希望你能明确它的作用。

欢迎在下面的评论中分享你对 p 值的看法和问题。我会认真倾听。

佩速、努力和耐力

原文:towardsdatascience.com/pacing-effort-and-stamina-6b340ab53650

对最近都柏林城市马拉松比赛的技术分析

barrysmythTowards Data Science barrysmyth

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

--

在我们开始之前,我想把这篇文章定位为几篇最近文章中的一篇,这些文章位于数据科学与马拉松跑步的交汇处。在之前的文章中,我集中讨论了几个与 马拉松训练可视化 表现数据**相关的技术挑战。这里我将重点转向分析(我自己的)最近的马拉松表现,使用一些之前讨论过的可视化技术以及来自运动科学领域的几个表现指标。因此,本文提供了一个具体的例子,展示了如何使用(跑步)速度和努力等相对简单的概念来探索更复杂的现代生理测量指标,如恢复力和耐力,从而更好地理解马拉松表现。同时,值得注意的是,类似的想法在许多其他耐力领域如骑行、铁人三项、滑冰等也被证明是相关的。

我在生活中晚些时候才开始跑步。我是在 40 多岁时开始的。我已经能够完成 8 次都柏林城市马拉松(DCM)——从 2013 年开始——而且在变老的过程中,我很幸运地取得了几次新的个人最佳PBs)。我今年(2023 年)最近的努力是在一个凉爽、潮湿的十月底早晨取得了一个新 PB。我在略超过 3 小时 10 分钟的时间内跑过终点,比我之前的最好成绩(2022 年)快了 7 分钟多。

作为一名数据科学家,我对从我的比赛经验中学习非常感兴趣,今年我的个人最佳成绩(PB)的规模令人惊讶和迷人。我觉得小的 PB 可能在计划之中——如果条件良好——但我设定的最佳目标是 3 小时 15 分钟,而 3 小时 10 分钟并未出现在我的计划中。这一表现仅仅是因为更加努力和更快地跑了吗?还是其他因素在起作用?我的配速和努力程度与之前的比赛和 PB 相比如何?

在这篇文章中,我将考虑几场最近的比赛,探讨我的努力(心率,每分钟跳动次数)速度(或配速,分钟/公里)之间的相互关系,以及一种更为近期的耐力指标耐久性 [1],这是一个新兴的生理适应能力测量指标,有点类似于我们所认为的耐力*。虽然每次新的 PB 都是因为跑得更快(平均而言),但我的近期 PB 也与较低的努力程度相关。我是否跑得过于保守,还是早期的 PB 跑得过于激进?这就是耐久性分析发挥作用的地方,因为它观察速度和努力之间不断演变的关系。

4 个个人最佳成绩和一个跟腱伤情

在本次分析中,我将考虑自 2017 年以来的 5 届都柏林马拉松;2020 年和 2021 年因疫情取消。我在大多数比赛中身体健康良好,除了 2019 年,那时跟腱受伤阻碍了我的正常训练。2022 年比较特殊,因为我的目标比赛是伦敦马拉松——在当年都柏林马拉松前仅 4 周——因此我的训练虽然很好,但并不一定针对都柏林进行了优化。然而,我在都柏林跑得更快了。真是让人摸不着头脑!

在 2017 年至 2022 年的比赛中,我使用了差不多相同的训练计划——这是我多年前在网上找到的——根据我的完成时间的改善调整了配速。在这些年里,我主要是自己训练,偶尔与朋友一起跑步。我在 2023 年春季加入了 Kilcoole Athletics Club(KAC)——这是我做出的最佳决定!——并最终按照俱乐部的官方计划进行训练,与最棒的一群马拉松跑者一起定期训练。

尽管训练不是本文的重点——希望将来能有所展现——以下是每次比赛前 16 周训练的总结。通常,我每周跑 4-5 天(55-60 公里),在目标马拉松前有 3-4 次长跑(≥30 公里),除了 2019 年,那时我的跟腱疼痛影响了训练并限制了长跑。今年,我记录了适度的训练量增加(每周 65 公里,峰值超过 90 公里),加上一些更为艰苦的长跑训练。

我的过去五届都柏林马拉松训练总结,显示了每周平均训练量/频率、每周最大训练量和最终完成时间。

配速与努力

分析我最近的马拉松比赛的一个简单方法是考虑在不同比赛阶段的配速(分钟/公里)和努力(心率)。我使用了Strava API下载我的比赛活动,包括每 1 公里段的平均配速和心率数据。为了简化,我使用了 Strava 的心率区域而不是我实际的心率值;Strava 使用五个心率区域(Z1, …, Z5),基于最大心率的不同百分比。

下面的图表显示了我每公里段的平均配速(y 轴),以及比赛阶段(x 轴)。每个‘热图线’的颜色编码反映了对应比赛段的心率区域。为了参考,还显示了都柏林马拉松的海拔轮廓,背景中突出显示了几个标志性位置。每个图表上标注了我的平均比赛配速(虚线,加粗文本),以及比赛段的最大(最慢)和最小(最快)配速。

都柏林马拉松每 1 公里段的配速和心率区域,以及都柏林马拉松的海拔轮廓。

有几个观察点值得注意:

  • 我的 2017 年比赛(a)涉及了最大的努力(平均心率 169 bpm),且这场比赛中相当一部分时间在 Z4 区域完成,这不是马拉松比赛中想要待的地方,至少不能待太久。

  • 我的训练中断在 2019 年(c)中表现得很明显。那一年我比较轻松(因为我不得不这样做,因此经常在 Z2 区域跑步),但在多山的 Roebuck 段我减速(>6 分钟/公里),并且在随后的下坡和平坦段中未能恢复努力。

  • 2018 年(b)和特别是 2023 年(e),从配速/努力的角度来看,是执行得非常好的比赛例子。这两场比赛的特点是 Z2 区域跑步量适中,特别是在早期多山的阶段,且下半场几乎没有减速。2023 年的最后一段 Roebuck Hill 几乎没有对我的比赛产生影响。

  • 尽管 2022 年我的时间是第二快的,但我的配速和努力不如 2018 年或 2023 年“组织得好”。前半段几乎都是 Z3 区域跑步,并且在最后 10 公里时我的配速开始恶化。尽管如此,我还是在 2022 年创下了个人最佳。

因此,通过仔细管理我的配速和努力,我能够在 2018 年(约 5 分钟个人最佳)和 2023 年(约 7 分钟个人最佳)创造出两个非常强劲的个人最佳。相比之下,我在 2017 年和 2022 年的个人最佳是经过艰苦努力取得的,且比赛后期明显减速。

连续个人最佳比较

下面的散点图提供了连续个人最佳成绩对比(2017 年与 2018 年,2018 年与 2022 年,2022 年与 2023 年)的更直接比较。每个点指的是比赛中的一个 1 公里赛段。点的大小和颜色表示赛段的公里数:早期赛段较小且较蓝,后期赛段较大且较红。每个点的位置基于该公里在更近期个人最佳成绩中的配速和努力(实际心率),与之前的个人最佳成绩相比。

例如,(a)比较了 2018 年与 2017 年。在 2018 年,只有一个公里跑得比 2017 年更努力且更快。对于我 2018 年的其余比赛,我跑得更轻松,通常也更快。

比较连续个人最佳成绩在努力(心率)和速度(配速)方面的差异。每个标记对应于马拉松中的一个特定 1 公里赛段,并根据更近期的个人最佳是否跑得更努力/轻松以及更快/更慢进行定位。早期赛段用较小的(蓝色)标记表示。后期赛段用较大的(红色)标记表示。

第二个散点图(b,2022 年与 2018 年)讲述了一个完全不同的故事。我在 2022 年的比赛大部分时间以比 2018 年更大的努力跑完,2022 年的 72%赛段也跑得更快,特别是在比赛的早期和中期。然而,2022 年后期的表现却不尽如人意。尽管我在跑得更努力,但它们比 2018 年慢,且大部分发生在比赛的后期。这突显了 2022 年艰难的最后 10 公里。较慢但更努力!尽管如此,还是创下了新的个人最佳!

今年情况有所改善(c)。我今年有 19%的比赛赛段跑得比 2022 年慢,主要是在早期阶段,但总是以较轻松的努力跑完。我的中期赛段也以比 2022 年少的努力完成,但速度也更快。而最大的进步是我能够比 2022 年更强烈地完成 2023 年。最后的 9 公里比 2022 年快了 10-15%。我需要更大的努力来实现这一点——这些最后赛段的平均心率比之前高约 1-8%——但我能够在比赛的晚期阶段提高努力程度,这是我之前几年无法做到的。

努力与节奏之间存在强烈的互动。这并不令人惊讶,但也使分析变得更加困难。拥有一个单一的衡量标准来指责比赛表现的因素总是有用的,这就是耐力可能发挥帮助作用的地方。

从生理韧性到耐力

耐力运动表现与三个重要生理特征密切相关——VO2max跑步经济性VO2max 的分数利用率2——但标准模型 [3]在很大程度上忽视了这些因素在长期运动中可能恶化的情况。这导致该领域的一位领先研究者,安迪·琼斯教授——曾与保拉·拉德克利夫和埃利德·基普乔格等人合作——提出了生理学 韧性 作为耐力运动表现的重要第四维度 [4],这可能有助于将最佳选手(如基普乔格)与其他具有类似 VO2 和跑步经济性特征的运动员区分开来。

生理韧性的一个实际测量是所谓的耐久性。它基于跑者内部和外部工作负荷的比率。简而言之,可以使用跑者的心率作为内部* 工作负荷的测量,而他们的速度(米/秒)可以用作外部 工作负荷的测量。然后,通过将跑者的心率除以当前速度来计算内部-外部比率IER)。当跑者的心率增加而速度减少时,他们达到了耐久性极限。当这种情况持续发生时,跑者的 IER 将会增加,因为跑者的心率与其速度发生解耦。这种解耦表明跑者已经超出了当前的耐久性阈值。

一种用于分析马拉松环境下耐久性的协议 [5]涉及将跑者的基线 IER(bIER)定义为马拉松 5–10 公里段的平均 IER。选择这个区段是为了确保跑者有时间“适应”比赛。然后,可以通过将跑者该段的 IER 除以 bIER 来计算跑者的相对 IER(rIER)。当rIER >1.025 在比赛剩余时间内 时,称为解耦发生。

下面的图表(a-e)展示了我最近马拉松每公里的 rIER。绿色区域表示尚未发生解耦。解耦(红色区域)仅在 rIER>1.025 并在比赛剩余时间内保持这种状态时才会发生。注意到有几个短暂的区域,绿色区域超过了允许的 rIER 阈值,但由于这些峰值是暂时的(通常是由于上坡或水站减速),它们不被归类为解耦的开始。

每个马拉松的相对内部-外部工作负荷比(a)—(e)显示了何时发生解耦以及解耦发生后的程度(红色)。在(f)中,(a)—(e)中的比赛解耦特征的总结表明,解耦时间的延迟导致耐力的提升。

这些图示显示了我的耐力在多年的变化情况。在 2017 年,我在半程(22 公里)后解耦,且程度非常显著(平均 rIER 为 1.10,相当于中等程度的解耦)。这与 2019 年我因伤病干扰的耐力类似。我的耐力在 2018 年、2022 年和 2023 年的个人最佳成绩中显著提升。比赛中的解耦发生在更晚(≥30 公里),且解耦后的平均 rIER 要温和得多(在 1.05 到 1.08 之间,表示程度的解耦)。

(f)中的散点图总结了这些结果。每场比赛通过位于 xy 轴上的一个点来表示,该点基于解耦的累计程度(解耦后的总 rIER)和解耦的发生时间。2017 年和 2019 年是早期解耦的例子,累计解耦程度明显高于 2018 年、2022 年和 2023 年中的晚期解耦。

新兴的趋势应该很明显。它指向了我最近的、经过良好训练的比赛中的解耦时间越来越晚,因此解耦的程度更低。究竟这是由于更好的训练、更好的比赛配速,还是两者兼有,仍然是一个悬而未决的问题。一个尚未解答的问题是耐力特征是否对训练适应有敏感性,但某些训练方法可能能够减少经过良好训练的跑者的解耦现象。

结论

本文旨在提供对我最近几场都柏林马拉松比赛的相对深入的数据分析。很容易得出结论认为更快的比赛就是更好的比赛,但这并不能讲述完整的故事。我们在这里考虑了配速和努力(以心率为标准),显然控制自己的努力至少与坚持目标配速同样重要,以实现个人最佳成绩。一般来说,我控制较好的努力(2018 年和 2023 年)都代表了新的强劲个人最佳时间,并且在比赛后期几乎没有通常配速下降的证据。

我们还考察了一个相对较新的表现指标——耐力,作为我们生理弹性的替代指标,以便更好地理解配速与努力之间的相互作用。当然,我最近的个人最佳成绩与耐力的显著提升有关。当然,这些都忽略了训练的影响,而训练在我们的体能水平和比赛准备中扮演了重要角色。我们会在适当的时候进一步探讨这点……

尽管本文重点讨论了马拉松跑步,但所提出的观点在其他耐力领域同样适用。关于将诸如耐久性等概念应用于骑行[6]和铁人三项等多种耐力运动的相关研究正在不断增加。因此,这里提出的观点对于处理各种耐力领域的人体表现数据的数据科学家来说,应该更具普遍意义。

参考文献

上述文本中直接链接的科学文章在此详细列出,以确保完整性。

  1. Maunder E, Seiler S, Mildenhall MJ, Kilding AE, Plews DJ. 在耐力运动员的生理学分析中,“耐久性”的重要性。体育医学。2021 年 8 月;51(8):1619–1628。doi: 10.1007/s40279–021–01459–0。2021 年 4 月 22 日在线发表。PMID: 33886100。

  2. Shaw AJ, Ingham SA, Atkinson G, Folland JP. 跑步经济性与最大氧摄取量的相关性:在高度训练的长跑运动员中的横断面和纵向关系。PLoS One。2015 年 4 月 7 日;10(4):e0123101。doi: 10.1371/journal.pone.0123101。PMID: 25849090;PMCID: PMC4388468。

  3. Joyner MJ. 建模:基于生理因素的最佳马拉松表现。应用生理学杂志(1985)。1991 年 2 月;70(2):683–7。doi: 10.1152/jappl.1991.70.2.683。PMID: 2022559。

  4. Jones AM. 第四维度:生理韧性作为耐力运动表现的独立决定因素。生理学杂志。2023 年 8 月 22 日。doi: 10.1113/JP284205。在线发表。PMID: 37606604。

  5. Smyth B, Maunder E, Meyler S, Hunter B, Muniz-Pumares D. 在马拉松期间内部与外部工作负荷的解耦:对 82,303 名业余跑者耐力的分析。体育医学。2022 年 9 月;52(9):2283–2295。doi: 10.1007/s40279–022–01680–5。2022 年 5 月 5 日在线发表。PMID: 35511416;PMCID: PMC9388405。

  6. Valenzuela PL, Alejo LB, Ozcoidi LM, Lucia A, Santalla A, Barranco-Gil D. 职业自行车运动员的耐久性:一项实地研究。国际体育生理表现杂志。2022 年 12 月 15 日;18(1):99–103。doi: 10.1123/ijspp.2022–0202。PMID: 36521188。

有兴趣的读者可以在这里和这里以及我最近在都柏林马拉松的冒险经历这里找到有关使用 Strava 数据和本文中使用的一些可视化的信息。

我定期撰写各种主题的文章,特别是在数据科学/机器学习与马拉松跑步的交汇处,文章发布在Running with DataTowards Data Science

所有图像和图表均由作者制作。

填充大型语言模型 — 使用 Llama 2 的示例

原文:towardsdatascience.com/padding-large-language-models-examples-with-llama-2-199fb10df8ff?source=collection_archive---------2-----------------------#2023-08-11

为因果 LLMs 填充训练示例的最佳实践

Benjamin MarieTowards Data Science Benjamin Marie

·

关注 发布于Towards Data Science · 10 分钟阅读·2023 年 8 月 11 日

--

作者提供的图像 — 基于来自Pixabay的图像

填充是大型语言模型(LLMs)中最少被记录的方面之一。为什么?仅仅因为 LLMs 通常在没有填充的情况下进行预训练。

尽管如此,对于在自定义数据集上微调 LLMs,填充是必要的。未能正确填充训练示例可能会导致各种意想不到的行为:训练期间的空损失或无限损失、生成过多或推理期间的空输出,都是填充不正确的症状。

在这篇文章中,我首先解释什么是填充以及为什么它是必要的。然后,我展示了如何为没有填充的预训练 LLM 找到正确的填充策略。我提出了两种不同的解决方案来为 LLM 添加填充支持,使用 Hugging Face 的 Transformers。

在文章的末尾,我还提供了示例,展示了如何为 Llama 2 填充你的训练示例。

阅读完这篇文章后,你应该能够自己搞清楚如何为 LLM 填充训练示例,而无需阅读它们的文档或教程。

填充和批次

什么是填充,为什么我们需要填充?

我们来举一个例子,假设我们希望用来微调一个 LLM。

example = "You are not a chatbot."

我们需要将这个示例转换为一个标记序列。像 Transformers 这样的库通常按以下步骤进行分词:

  • 根据给定的词汇表将示例分割成子词:
example = ["▁You", "▁are", "▁not", "▁a". "▁chat", "bot", "."]
  • 用词汇表中的索引替换单词,以获得一个整数序列:
example = [887, 526, 451, 263, 13563, 7451, 29889]
  • 向序列中添加特殊标记:BOS 标记、EOS 标记、UNK 标记、PAD 标记等。
example = [1, 887, 526, 451, 263, 13563, 7451, 29889]

注意:在这个例子中,我使用了 Llama 2 的分词器。我们将在下面详细了解如何操作。

在这个例子中,仅添加了 BOS(序列开始)特殊标记。

每个训练示例还会生成一个注意力掩码。这个掩码告诉变压器是否应该关注一个标记(1)或不关注(0)。这个示例的注意力掩码很简单,因为所有标记都应该被考虑。

#We have as many values as tokens.
attention_mask = [1, 1, 1, 1, 1, 1, 1, 1]

下一步是将所有内容封装成 Pytorch 张量。这种封装是必要的,以便应用 CUDA 和 GPU 优化的矩阵操作。

{'input_ids': tensor([[1, 887, 526, 451, 263, 13563, 7451, 29889]]), 
 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1]])}

现在,假设我们有两个训练示例。为了简单起见,我会重复我已有的一个。新的张量多出了一行:

{'input_ids': tensor([[1, 887, 526, 451, 263, 13563, 7451, 29889],
                      [1, 887, 526, 451, 263, 13563, 7451, 29889]]), 
 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1],
                           [1, 1, 1, 1, 1, 1, 1, 1]])}

两个示例具有相同的长度(当然,因为它们是相同的)。这两个张量的维度都是 2x8(N x M)。

示例被放入张量中以创建批次,以便神经网络在看到 N 个示例后可以更新其值。批次对于计算效率和模型性能至关重要。

现在,让我们引入一个更短的第三个示例:

example = "You are not."

在分词后,我们得到:

example = [1, 887, 526, 451, 29889]
attention_mask = [1, 1, 1, 1, 1]

如果你尝试将其添加到我们的示例列表中并创建张量,你将会遇到错误。但想象一下,如果没有错误,我们将得到:

{'input_ids': tensor([[1, 887, 526, 451, 263, 13563, 7451, 29889],
                      [1, 887, 526, 451, 263, 13563, 7451, 29889],
                      [1, 887, 526, 451, 29889]]), 
 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1],
                           [1, 1, 1, 1, 1, 1, 1, 1],
                           [1, 1, 1, 1, 1]])}

你能看到这里的问题吗,为什么不能创建这样的张量?

我们有一行不同长度的数据。我们无法对其应用矩阵操作。

在大多数数据集中,示例的长度不相同。我们必须修改它们,以确保同一批次中的示例具有相同的长度。

这就是我们需要“填充”的原因。

填充标记和填充侧

你可以将填充视为通过重复一个虚拟标记来扩展序列直到给定长度。

这个虚拟标记是一个“填充标记”。

例如,上面第一个示例的长度为 8 个标记(包括 BOS 标记)。假设在我们的批处理中,我们不会有超过 8 个标记的序列。所有序列必须为 8 个标记长。

我们的第二个示例仅包含 5 个标记。所以我们必须添加 3 个填充标记。

example = "You are not. [PAD] [PAD] [PAD]"

实际上,我们不会手动将“[PAD]”标记添加到序列中。大多数分词器会将“[PAD]”拆分成子词。填充标记通常是分词器内部定义的特殊标记,并且在必要时会与其他特殊标记一起自动添加到序列中。

如果填充标记在词汇表中的 ID 是 32000,我们将得到:

example = [1, 887, 526, 451, 29889, 32000, 32000, 32000]

现在,我们有了一个具有预期长度的序列。但还有一个问题:我们还需要修改注意力掩码。

记住,填充标记是虚拟标记,我们不希望 LLM 对它们给予任何关注。我们引入这些标记只是为了填充序列并创建正确的张量。

为了将其指示给模型,我们只需在注意力掩码中设置“0”,这样模型将忽略它们。

attention_mask = [1, 1, 1, 1, 1, 0, 0, 0]

最后,我们可以使用填充的示例创建正确的张量:

{'input_ids': tensor([[1, 887, 526, 451, 263, 13563, 7451, 29889],
                      [1, 887, 526, 451, 263, 13563, 7451, 29889],
                      [1, 887, 526, 451, 29889, 32000, 32000, 32000]]), 
 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1],
                           [1, 1, 1, 1, 1, 1, 1, 1],
                           [1, 1, 1, 1, 1, 0, 0, 0]])}

注意:填充是在序列长度小于最大长度时执行的。但在某些情况下,序列可能会太长。在这种情况下,我们必须截断序列,以使其大小匹配最大长度。

填充的另一个重要参数是填充侧面。在上面的示例中,我进行了右填充。如果模型有 EOS 标记,填充标记将添加到它之后。

我们也可以进行左填充。在这种情况下,张量看起来像这样:

{'input_ids': tensor([[1, 887, 526, 451, 263, 13563, 7451, 29889],
                      [1, 887, 526, 451, 263, 13563, 7451, 29889],
                      [32000, 32000, 32000, 1, 887, 526, 451, 29889]]), 
 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1],
                           [1, 1, 1, 1, 1, 1, 1, 1],
                           [0, 0, 0, 1, 1, 1, 1, 1]])}

填充标记是在 BOS 标记之前添加的。

选择哪个侧面主要取决于你想使用的 LLM 和你的下游任务。这就是为什么在做出任何决定之前,研究模型及其分词器是很重要的。下面,我们将看到如何为 Llama 2 做出这个决定。

为因果 LLM 添加填充支持

如我们所见,填充(几乎)总是对于微调是必要的。然而,许多 LLM 默认不支持填充。这意味着它们的词汇表中没有特殊的填充标记。

在这里,我展示了两种添加填充标记的解决方案。

简单的解决方案

这个解决方案是你在大多数教程中会找到的。

它只是将现有标记分配给填充标记。例如,你可以声明你的填充标记将是 EOS 标记。我们将得到像这样(右填充,其中“2”是 EOS 标记的 ID)的张量:

{'input_ids': tensor([[1, 887, 526, 451, 263, 13563, 7451, 29889],
                      [1, 887, 526, 451, 263, 13563, 7451, 29889],
                      [1, 887, 526, 451, 29889, 2, 2, 2]]), 
 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1],
                           [1, 1, 1, 1, 1, 1, 1, 1],
                           [1, 1, 1, 1, 1, 0, 0, 0]])}

这个解决方案的问题是 LLM 现在感到困惑:大多数情况下,EOS 标记在注意力掩码中将是“0”。这会促使 LLM 忽略原始的 EOS 标记。这并不理想,因为 EOS 标记会指示 LLM 停止生成。

使用这种解决方案,我们也必须进行右填充。如果你进行左填充,你会得到以 EOS 标记开头的序列,从而过早停止生成。

注意:我阅读了几个用于微调 Llama 2 的教程,这些教程使用 EOS token 进行左填充。如果这样做,你将得到 0.0 的损失,训练将会发散。试试看吧!观察这个现象很有趣。

在我看来,更好的替代方案是使用 UNK token 作为 pad token。这个 token 很少被使用。只有当 token 不在词汇表中时,它才会出现在序列中。将它用于其他目的不应产生重大影响。

Meta 在其“Llama recipes”中使用了这一替代方案。使用此解决方案,填充侧别不那么重要。

另一种解决方案:从头创建一个 pad token

UNK token 在模型中已经有其角色。理想情况下,我们希望有一个仅用于填充的 pad token。

如果词汇表中不存在,我们必须从头创建一个 pad token。这是 Hugging Face 推荐的 Llama 2 解决方案

使用如 transformers 等库,扩展词汇表非常简单。

如果你想创建一个 pad token,你需要按照以下步骤进行:

  • 将 pad token 作为特殊 token 添加到 LLM 的词汇中。

  • 调整 token embeddings 的大小

  • 重新训练 token embeddings(可选)

如果你有预算限制并使用 LoRa 进行微调,你可能会想跳过最后一步,因为 token embeddings 可能会有数亿参数。此外,在我的实验中,重新训练 embeddings 总是导致更糟的结果,这表明我的微调数据集可能不够大,或者好的超参数很难找到。

案例研究:用 Hugging Face 的 Transformers 填充 Llama 2

在本节中,我们将为 Llama 2 启用填充。要复制每个步骤,你需要访问 Hugging Face 上的 Llama 2。我在这篇文章中解释了如何获取 Llama 2

注意:我分享了 一个笔记本 (#8) 在 The Kaitchup,我的 substack 新闻通讯上复制所有这些步骤。

首先,安装 Transformers 库:

pip install transformers

然后,我们导入 transformers 并加载 tokenizer。确保你在其中输入了你的 Hugging Face 访问 token:

from transformers import AutoTokenizer

#Replace the following with your own Hugging Face access token.
access_token = "hf_token"

#The model we want to quantize
pretrained_model_dir = "meta-llama/Llama-2-7b-hf"
tokenizer = AutoTokenizer.from_pretrained(pretrained_model_dir, use_fast=True,  use_auth_token=access_token)

我们定义了两个训练示例:

prompt1 = "You are not a chatbot."
prompt2 = "You are not."

如果我们将 prompt1 放在同一批次中两次,一切顺利:

prompts = [prompt1, prompt1]
input = tokenizer(prompts, return_tensors="pt");
print(input)

输出:

{'input_ids': tensor([[    1,   887,   526,   451,   263, 13563,  7451, 29889],
        [    1,   887,   526,   451,   263, 13563,  7451, 29889]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1]])}

但如果你添加了 prompt2,你将如预期那样遇到错误:

prompts = [prompt1, prompt1, prompt2]
input = tokenizer(prompts, return_tensors="pt");
print(input)

输出:

ValueError: Unable to create tensor, you should probably activate truncation and/or padding with 'padding=True' 'truncation=True' to have batched tensors with the same length. Perhaps your features (`input_ids` in this case) have excessive nesting (inputs type `list` where type `int` is expected).

很明显,tokenizer 并没有填充示例。

我们可以通过简单地使用 UNK token 作为 pad token 来解决这个问题,具体如下:

tokenizer.padding_side = "left"
tokenizer.pad_token = tokenizer.unk_token
input = tokenizer(prompts, padding='max_length', max_length=20, return_tensors="pt");
print(input)

在这个例子中,我要求 tokenizer 填充到 max_length。我将 max_length 设置为 20。如果你的示例包含 10 个 tokens,tokenizer 将添加 10 个 pad tokens。

{'input_ids': tensor([[    0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     1,   887,   526,   451,   263, 13563,  7451, 29889],
        [    0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     1,   887,   526,   451,   263, 13563,  7451, 29889],
        [    0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     1,   887,   526,   451, 29889]]), 'attention_mask': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1]])}

另一种方法是从头创建一个 pad token。使用 Hugging Face 的 transformers,我们可以通过方法“add_special_tokens”来实现这一点。

tokenizer.add_special_tokens({'pad_token': '[PAD]'})
input = tokenizer(prompts, padding='max_length', max_length=20, return_tensors="pt");
print(input)

输出:

{'input_ids': tensor([[32000, 32000, 32000, 32000, 32000, 32000, 32000, 32000, 32000, 32000,
         32000, 32000,     1,   887,   526,   451,   263, 13563,  7451, 29889],
        [32000, 32000, 32000, 32000, 32000, 32000, 32000, 32000, 32000, 32000,
         32000, 32000,     1,   887,   526,   451,   263, 13563,  7451, 29889],
        [32000, 32000, 32000, 32000, 32000, 32000, 32000, 32000, 32000, 32000,
         32000, 32000, 32000, 32000, 32000,     1,   887,   526,   451, 29889]]), 'attention_mask': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1]])}

添加了填充标记到 Llama 2 的词汇表后,不要忘记调整 Llama 2 的令牌嵌入。我在这篇文章中解释了如何做到这一点:

## 在你的计算机上使用 QLoRa 和 TRL 对 Llama 2 进行微调

在 Guanaco 和正确的填充设置下

kaitchup.substack.com

结论

一旦你理解了,填充就非常简单。

使用 UNK 标记进行填充,或者从头创建填充标记,是非常安全的解决方案,几乎适用于所有因果语言模型。但你应该始终查看分词器的工作方式。至少你应该了解它已经支持的特殊标记。例如,并非所有语言模型都有 UNK 标记,有些语言模型的填充标记在词汇表中没有明确定义为填充标记,等等。

像往常一样,如果你有任何问题,请留下评论。我会尽量回答。

PaLM:高效训练大型语言模型

原文:towardsdatascience.com/palm-efficiently-training-massive-language-models-b82d6cc1582

史无前例的 LLM 规模、效率和性能

Cameron R. Wolfe, Ph.D.Towards Data Science Cameron R. Wolfe, Ph.D.

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

--

(照片由Corey Agopian提供,来自Unsplash)

近年来,大型深度神经网络已成为解决大多数语言理解和生成任务的终极架构。最初,提出了诸如BERT [2]和 T5 [3]的模型,这些模型使用了两阶段训练方法,即在大规模文本语料库上进行预训练(使用自监督“填充”目标),然后在目标数据集上进行微调;见下文。尽管这些技术很有用,但对大型语言模型(LLMs)的最新研究表明,大型自回归(仅解码器)变换器模型在少样本学习中表现出色,能够以最小的适应性在下游任务中取得令人印象深刻的性能。

(来自[4])

LLM 的少样本学习能力最初由GPT-3 [4]展示,这是一种拥有 1750 亿参数的 LLM。为了进行少样本预测,该模型在一个庞大的文本语料库上进行预训练(使用基本的语言建模目标),然后提供任务描述和若干个任务解决示例;见上文。对 LLM 的进一步分析表明,模型性能随着规模的增加而平滑提升(根据幂律)[5, 6]。因此,在 GPT-3 之后提出了各种 LLM,试图通过“扩大”模型和训练来提高结果,通常通过更大的模型和更多/更好的预训练数据组合来实现改进。

训练更大的 LLM 是有益的,但要高效地做到这一点却非常困难。通常,我们将训练分布到许多机器上,每台机器配备多个加速器(即 GPU 或TPUs)。这一方法以前已成功实施(例如,MT-NLG 在一个拥有 2240 个 A100 GPU 的系统上训练了一个 5300 亿参数的 LLM),但结果并不特别令人印象深刻。尽管模型很大,但训练的数据量不足。然而,鉴于更高的训练吞吐量,我们可以(理论上)在更大的数据集上更广泛地预训练这样的巨大模型,从而获得更好的结果。

在本概述中,我们将探讨Pathways 语言模型(PaLM),这是一种拥有 5400 亿参数的 LLM,使用 Google 的Pathways框架进行训练。通过消除管道并行性,这一架构实现了令人印象深刻的训练吞吐量,使得 PaLM 能够在更广泛的数据集上进行预训练。最终模型的少样本性能处于行业领先水平。此外,PaLM 在解决复杂推理任务方面也有一定能力。简单来说,PaLM 清楚地提醒我们,LLM 的性能在规模上尚未达到瓶颈。只要有足够高效的训练基础设施来支持在更多数据上预训练更大的模型,我们仍将继续看到性能的提升。

(来源于 [1, 16])

背景

我们在本新闻通讯中广泛探讨了语言建模主题,并在之前的帖子中回顾了几种显著的(大规模)语言模型:

尽管如此,我们将在这里简要回顾有关 LLM 的先前工作,以提供理解 PaLM 的重要背景。

语言建模回顾

仅解码器转换器架构

现代语言模型仅仅是 仅解码器转换器模型(如上图所示),这些模型通过自监督 语言建模目标 在无标签文本上进行预训练。该目标对文本序列进行采样,并训练语言模型准确预测下一个词/标记。在进行广泛的预训练后,LLM 如 GPT-3 被发现能在少量示例学习模式下表现非常好。

这有什么用? 简而言之,LLM 的通用文本到文本格式使其能够轻松地推广到解决各种任务,只需少量调整。我们可以仅仅通过广泛地预训练一个单一模型,并利用少量示例学习来解决各种任务,而不必对模型进行微调或添加任务特定的层。尽管预训练这样的基础模型是非常昂贵的,但这些方法具有巨大的潜力,因为一个模型可以被重新用于许多应用。这一过程称为上下文学习;见下文。

(来源于 [4])

什么构成一个好的 LLM? 早期的研究表明,语言模型的性能应该随着模型规模的增加而平滑提高(根据 幂律)(即,大模型表现更好)。这一发现导致了 GPT-3 的提出,这是一个规模空前的 LLM(1750 亿参数),实现了突破性的少量示例学习性能。后续工作尝试探索 更大的 LLM,但这些更大的模型并没有带来进一步的性能突破。相反,我们最终发现,生产高性能 LLM 需要将更大的模型与更大的预训练数据集相结合 [6]。

“预计所需的训练数据量远超目前用于训练大型模型的数据量,这突显了数据集收集的重要性,除了允许模型规模扩展的工程改进。” — 来源于 [6]

架构修改

除了使用改进的训练框架,PaLM 还对基础的仅解码器架构进行了相当大的修改。这些更改大多数采纳了先前的工作,这些工作揭示了最大化 LLM 训练效率和性能的最佳实践。

SwiGLU 激活函数。 大多数大语言模型(LLMs)在每一层内使用的前馈神经网络具有类似的结构。也就是说,这个网络进行两次前馈变换(不使用偏置,并单独应用于序列中的每个令牌向量),中间使用修正线性单元(ReLU)激活函数。然而,后续的工作[13]揭示了其他激活函数的选择可能实际上更好。

具体来说,PaLM 使用了 SwiGLU 激活函数,它是 Swish [14] 和 GLU [15] 激活函数的组合。这个激活函数由下面的方程给出。

(作者创作)

我们将 Swish 激活函数定义为

(作者创作)

换句话说,SwiGLU 是对输入的两个线性变换的逐元素乘积,其中一个变换应用了 Swish 激活函数。尽管这个激活函数需要进行三次矩阵乘法,但最近的工作发现,在固定的计算量下,它能带来性能提升。与像 ReLU 这样的普通激活函数相比,SwiGLU 似乎提供了不可忽视的性能提升 [13]。

并行变压器块。 PaLM 还使用了变压器块的并行版本,而不是常规(串行)版本。这两种形式之间的差异在下图中展示。

并行与串行变压器块(作者创作)

在模型足够大的情况下,使用并行变压器块可以将训练过程的速度提高 15%。这种加速会以较小 LLM(例如,80 亿参数模型)性能略有下降为代价,但全尺寸 LLM 使用并行块时往往表现相似。

(来源 [16])

旋转位置嵌入。 PaLM 使用了旋转位置嵌入(RoPE),而不是绝对位置嵌入相对位置嵌入,如[16]中所提议。RoPE 嵌入通过以下方式结合了绝对和相对位置:

  1. 使用旋转矩阵编码绝对位置

  2. 将相对位置直接纳入自注意力机制

直观地,RoPE 找到了绝对和相对位置嵌入之间的折中。上图所示,RoPE 始终优于其他嵌入策略。而且,它在 HuggingFace 等常见库中易于访问

多查询注意力。 最后,PaLM 用一种称为多查询注意力的替代结构替换了典型的多头自注意力机制。多查询注意力在每个注意力头之间共享键和值向量(下面用红色突出显示),而不是为每个头执行单独的投影。这一变化并没有使训练速度更快,但确实显著提高了 LLMs 的自回归解码(即用于执行推理或生成)的效率。

多查询注意力在注意力头之间共享键和值投影(来源 [17])

其他有用的概念

  • 基础模型和零/少样本学习 [link]

  • LLM 对齐 [link]

  • LLM 的适应策略 [link]

  • LLM 的简要发展 [link]

PaLM: 使用 Pathways 扩展语言建模 [1]

现在,我们将概述 PaLM,一个 5400 亿参数的密集语言模型,它通过使用Pathways 框架进行了高效训练。PaLM 是迄今为止训练过的最大密集型 LLM 之一,其高效的训练策略使得其预训练过程可以在大数据集(>7000 亿个标记)上进行。这种大型语言模型与广泛的预训练语料库的结合,导致了一些有趣的结果,我们将在本节中探讨。

PaLM 是如何工作的?

PaLM 是一个大型的 LLM,通过广泛的预训练(得益于高效的 Pathways 架构)和对基础模型架构的一些修改,达到了令人印象深刻的少样本学习性能。我们将概述 PaLM 架构和训练模式的细节。

模型。 PaLM 使用了一个仅解码器的变换器,具有 5400 亿参数。然而,这个模型超越了典型的仅解码器架构,通过进行一些修改:

  • 在 MLP 层中使用 SwiGLU 激活(而不是ReLU)。

  • 在注意力层中使用多查询注意力。

  • 仅使用并行变换器块。

  • 绝对或相对位置嵌入被 ROPE 嵌入替代。

为了了解模型规模的影响,[1] 中测试了三种不同规模的 PaLM;见下文。

(来自 [1])

尽管幂律法则表明性能在上述模型之间应平滑提升,[1] 的分析发现 我们通常会看到使用最大(5400 亿参数)模型时,性能有不成比例的提升。较大的 LLM 在结合更广泛的预训练过程时提供了意外的大收益。

“对于某些任务,我们观察到不连续的改进,其中从 62B 扩展到 540B 在准确性上有显著跃升,相较于从 8B 扩展到 62B… 这表明,当模型达到足够的规模时,大型语言模型的新能力可能会出现,并且这些能力会在先前研究的规模之外继续出现。” — 来自 [1]

数据集。 PaLM 的预训练语料库包含 780B 个标记。这个数据集比用于训练 Chinchilla [6] 的数据集略小,但仍然大于大多数之前的 LLM;见下文。

(来自 [3])

创建高性能的 LLM 不仅仅是让模型变得更大。对 LLM 的缩放规律的最新研究 [6] 表明,性能将随着模型大小和预训练语料库大小的增加而提高。因此,PaLM 有机会显著超越 MT-NLG 等模型(尽管其仅略大),通过使用更大的预训练语料库。

PaLM 使用的预训练语料库来源于高质量的网页、书籍、维基百科、新闻、文章、代码和社交媒体对话。它包含 22% 的非英语数据(见下文),并受到用于训练 LaMDA 和 GLaM [8, 9] 的语料库的启发。所有模型都在这个数据集上训练了一个完整的周期。

(来自 [1])

使用大词汇量。 由于预训练语料库中有相当一部分是非英语的,作者还采用了 SentencePiece tokenizer ,其词汇量为 256K。该分词器直接处理原始文本输入,并从文本中提取标记(即词或子词)。此分词过程基于一个基础词汇表(即已知标记的集合),从文本中提取的所有标记必须是词汇表的成员。如果一个标记不在基础词汇表中,它将被拆分成更小的块(可能是字符),直到被分解成有效的标记,或者用通用的“[UNK]”(未知词汇标记)替代。

使用小词汇表意味着许多重要的标记可能无法被正确捕捉,这可能会影响 LLM 的性能。对于多语言模型,我们通常会看到底层词汇表的大小大幅增加,以避免这种情况,因为来自多种语言的数据会利用更广泛的标记范围。PaLM 也不例外:作者采用了比平常更大的词汇表,以避免错误标记数据,并允许在多种语言间更有效的学习。要了解更多关于在多种语言上训练的语言模型的信息,请查看 这里 的链接。

训练系统。 在概述用于 PaLM 的训练框架之前,我们需要了解一些与分布式训练相关的概念。最重要的是,我们需要理解模型、数据和流水线并行之间的区别。虽然我已经之前解释了这些概念,但 这里 的推文提供了更好(且更简明)的描述。

PaLM 在一组 6144 个 TPU 芯片上进行训练,这些芯片分布在两个 TPU 集群(即,通过高速网络接口连接的 TPU 组)中。在发布时,这个系统是描述的最大配置;见下图。

(来自 [1])

在一个集群内部,TPU 之间的通信非常快速。但集群之间的通信则要慢得多。通常,模型和数据并行的带宽需求过大,无法在 TPU 集群之间高效训练。大多数先前的工作通过以下方式处理这个问题:

  1. 将训练限制在单个 TPU 集群上 [8, 9]。

  2. 在集群之间使用带宽要求较低的流水线并行 [7, 10]。

然而,流水线有许多显著的缺点,比如在清空或填充流水线时使加速器处于空闲状态,以及高内存需求。使用 Pathways 系统,PaLM 在 TPU 集群之间通过模型和数据并行(即无流水线并行)进行高效训练。这种新颖的训练范式显著提高了效率。

(来自 [1])

例如,PaLM 实现了一个模型 FLOPs 利用率(即每秒令牌吞吐量与系统理论最大吞吐量的比值)为 46.2%,而之前的系统难以超过 30% 的利用率;详见上文。有关 Pathways 系统及其如何在 LLM 训练效率上实现如此巨大的改进的信息,请查看 这里 的文章。

PaLM 的表现如何?

在 [1] 中提供的分析超越了实现卓越的少样本学习表现。PaLM 被证明能够有效处理多种语言,具备改进的推理能力,性能显著优于较小的模型,甚至在某些任务上超越了人类水平的语言理解。

多语言 LLM。 之前的 LLM(例如,GPT-3 [4])已经表现出一定的机器翻译能力,特别是在将其他语言翻译成英语时。在以英语为中心的数据对和设置中,我们看到 PaLM 相对于之前的 LLM 改进了翻译性能;详见下文。

(来自 [1])

在资源稀缺和非英语中心的数据上,PaLM 仍表现相对良好,但不及现有的监督翻译方法;详见上文。然而,鉴于之前的工作并未广泛考虑非英语设置,PaLM 在这种设置下的相对良好表现令人印象深刻。总体而言,这项分析显示 PaLM 语言翻译能力有所提升,但仍不及监督技术。

除了语言翻译之外,我们还看到 PaLM 在多语言生成任务上表现出色。正如预期的那样,PaLM 在英语语言生成能力方面表现最佳,但该模型在非英语生成任务上仍优于之前的语言模型。总体而言,这些结果表明,通过进行小幅调整(例如,增加非英语预训练数据并为分词器使用更大的词汇表),可以显著提高 LLM 的多语言能力。

超越人类表现。 BIG-bench 数据集包含 150 个任务,涵盖逻辑推理、翻译、问答、数学等主题。相对于之前的 LLM,我们看到 PaLM 在大多数任务上表现有所提升;详见下文。

(来自 [1])

比起超越之前的 LLMs,PaLM 在大多数 BIG-bench 任务上的表现也超越了人类的平均水平;见下文。对于这些任务中的某些任务,超越人类简单地表明 PaLM 能够记忆数据或在多种语言之间进行推理。然而,这并不总是如此!在其他任务中(例如,因果关系识别),我们看到 PaLM 似乎在语言理解上有所改善。

(来自[1])

幂律是否总是成立? 当我们将 PaLM 的表现细分到特定任务类别时,我们发现模型规模对某些任务特别有帮助。例如,在逻辑序列任务(即将一组词语排列成逻辑顺序)中,最大的 PaLM 模型在相对于较小模型的性能上有了巨大的提升。对于其他任务(例如,数学归纳),模型规模几乎没有影响。

(来自[1])

总的来说,PaLM 的表现并不总是遵循与模型规模相关的幂律。在某些情况下,使用更大的模型会导致性能的巨大意外提升,而在其他情况下,最大的模型仅比较小的变体表现稍好;见上文。

学习推理。 尽管语言模型在许多任务上表现良好,但它们在解决基本推理任务时常常遇到困难。许多研究人员引用了 LLMs 这一局限性作为其“浅薄”语言理解的证明。然而,最近的出版物已经使用链式思维提示(即在 LLM 生成最终输出之前生成几个推理“步骤”)来提高 LLMs 的推理能力[11, 12];见下文。

(来自[1])

在评估 PaLM 时,[1]中的作者发现,将这种规模的模型与链式思维提示相结合,足以在算术和常识推理任务上实现最先进的准确度。之前的方法利用了特定领域的架构、微调,甚至任务特定的验证模块来解决这些推理任务。相比之下,PaLM 只需使用少量示例的链式思维提示(以及用于算术推理任务的外部计算器模块)即可解决这些任务;见下文。

(来自[1])

有趣的是,我们发现最大的 PaLM 模型在推理能力上远优于较小的变体。考虑到之前的研究发现规模对推理性能的影响通常是混合的(有时是负面的),这一发现很有趣。PaLM 的结果表明,只要采用正确的提示方法,模型(和数据)规模似乎可以有益于推理性能。

(来自[1])

PaLM API

如果你有兴趣试用 PaLM,那你很幸运!PaLM 的 API 在过去几周内已向部分开发者发布。许多 AI 社区成员将 Google 发布的 PaLM API 视为对 OpenAI 在一周前公开发布的ChatGPT API的回应。有关 PaLM API 发布的更多信息,请阅读这里的文章。尽管训练和托管 LLMs 很困难,但我们目前看到这些工具通过 API 向开发者开放的巨大转变。因此,实践者可以轻松访问这些令人惊叹的模型,无需培训和托管的麻烦或费用。这降低了使用这些强大模型构建应用的门槛,开启了无限的可能性!关于可以构建的应用实例,我推荐查看OpenAI cookbook

重点总结

尽管最初尝试训练超越 GPT-3 规模的 LLMs 并不完全成功,但我们从 PaLM 中看到,我们所需的只是一个高效的训练框架,允许更广泛的预训练。通过使用 Pathways 框架,PaLM 可以在比之前模型(如 MT-NLG [7])更大的数据集上进行训练。得到的 LLM 具有令人印象深刻的多语言理解和推理能力,我们看到模型规模的增加往往能带来显著的好处。以下是从 PaLM 中得到的一些重要启示。

幂律定律是否总是成立? 关于 LLMs 的众多出版物表明,LLM 性能与各种数量(如(非嵌入式)模型参数、数据集规模、训练计算量等)之间存在幂律关系。虽然这种趋势在整体性能方面是成立的,但当我们单独检查每个任务的性能时,情况则更为复杂。某些任务从规模中受益不成比例,而其他任务则未见太多好处。因此,规模通常对 LLMs 有帮助,但结果会根据下游任务的不同而有显著差异。

我们是否应该避免管道并行性? PaLM 的一个主要卖点是其高效的 Pathways 训练框架。通常,在多个 TPU 集群或计算节点上训练需要使用管道并行性,因为内存带宽有限。然而,通过去除管道并行性并仅使用数据和模型并行性进行 TPU 集群训练,我们发现 PaLM 实现了突破性的训练效率和吞吐量。这些对训练框架的提升使得 PaLM 可以在更多的数据上进行训练,从而展现出模型令人印象深刻的性能。

LLM 的规模与推理。 之前关于 LLM 的研究经常指出其推理能力较差。实际上,LLM 在进行推理任务时,其能力似乎会随着规模的增大而下降。然而,我们看到在 PaLM 的情况下并非总是如此。如果我们将更大的 LLM 与更多的预训练数据和正确的提示方法(即链式思维提示)相结合,我们可以看到 LLM 推理能力有相当明显的提升!

结束语

非常感谢你阅读这篇文章。我是 Cameron R. WolfeRebuy 的 AI 主管。我研究深度学习的经验和理论基础。你也可以查看我在 medium 上的 其他文章!如果你喜欢这篇文章,请关注我的 twitter 或订阅我的 Deep (Learning) Focus 新闻通讯,我在其中帮助读者通过对热门论文的易懂概述,深入理解深度学习研究中的主题。

参考文献

[1] Chowdhery, Aakanksha, 等人。“Palm:通过路径扩展语言建模。” arXiv 预印本 arXiv:2204.02311(2022)。

[2] Devlin, Jacob, 等人。“Bert:用于语言理解的深度双向 transformer 预训练。” arXiv 预印本 arXiv:1810.04805(2018)。

[3] Raffel, Colin, 等人。“利用统一的文本到文本 transformer 探索迁移学习的极限。” 机器学习研究杂志 21.1(2020):5485–5551。

[4] Brown, Tom, 等人。“语言模型是少样本学习者。” 神经信息处理系统进展 33(2020):1877–1901。

[5] Kaplan, Jared, 等人。“神经语言模型的规模定律。” arXiv 预印本 arXiv:2001.08361(2020)。

[6] Hoffmann, Jordan, 等人。“训练计算最优的大型语言模型。” arXiv 预印本 arXiv:2203.15556(2022)。

[7] Smith, Shaden, 等人。“使用 deepspeed 和 megatron 训练 megatron-turing nlg 530b,一个大规模生成语言模型。” arXiv 预印本 arXiv:2201.11990(2022)。

[8] Thoppilan, Romal, 等人。“Lamda:对话应用的语言模型。” arXiv 预印本 arXiv:2201.08239(2022)。

[9] Du, Nan, 等人。“Glam:使用专家混合进行语言模型的高效扩展。” 国际机器学习会议。PMLR,2022。

[10] Rae, Jack W., 等人。“扩展语言模型:方法、分析与训练 gopher 的见解。” arXiv 预印本 arXiv:2112.11446(2021)。

[11] Nye, Maxwell, 等人。“展示你的工作:语言模型的中间计算记事本。” arXiv 预印本 arXiv:2112.00114(2021)。

[12] Cobbe, Karl, 等人。“训练验证者解决数学词题。” arXiv 预印本 arXiv:2110.14168(2021)。

[13] Shazeer, Noam。“Glu 变体改进了 transformer。” arXiv 预印本 arXiv:2002.05202(2020)。

[14] 拉马钱德兰,普拉吉特,巴雷特·佐普和阮光伟。“寻找激活函数。” arXiv 预印本 arXiv:1710.05941 (2017)。

[15] 多芬,扬·N. 等人。“使用门控卷积网络进行语言建模。” 国际机器学习会议。PMLR,2017。

[16] 苏建林等人。“Roformer: 增强型变换器与旋转位置嵌入。” arXiv 预印本 arXiv:2104.09864 (2021)。

[17] 瓦斯瓦尼,阿希什等人。“注意力机制即你所需的一切。” 神经信息处理系统进展 30 (2017)。

Pandas 2.0:数据科学家的游戏改变者?

原文:towardsdatascience.com/pandas-2-0-a-game-changer-for-data-scientists-3cd281fcc4b4?source=collection_archive---------0-----------------------#2023-06-27

高效数据处理的五大特点

Miriam Santos数据科学前沿 Miriam Santos

·

关注 发布于 数据科学前沿 ·7 分钟阅读·2023 年 6 月 27 日

--

今年四月,pandas 2.0.0 正式发布,在数据科学界掀起了巨大波澜。照片由 Yancy Min 提供,来自 Unsplash

由于其广泛的功能和多样性, pandas 在每位数据科学家的心中占据了一席之地。

从数据输入/输出到数据清洗和转换,几乎无法想象在没有import pandas as pd的情况下进行数据处理,对吧

现在,请耐心点: 在过去几个月围绕 LLM 的热潮中,我在某种程度上忽略了 pandas 刚刚经历了重大版本更新!没错,pandas 2.0 已经发布并带来了巨大的变化

尽管我并不了解所有的炒作, Data-Centric AI Community 很快就给予了援助:

2.0 版本似乎在数据科学社区中产生了相当大的影响,许多用户赞扬了新版本中添加的修改。作者提供的截图。

有趣的是: 你知道这个版本的开发花了令人惊讶的 3 年时间吗?这就是我所说的“对社区的承诺”!

那么 *pandas 2.0* 带来了什么?让我们直接深入了解!

1. 性能、速度和内存效率

众所周知,pandas 是使用 numpy 构建的,而 numpy 并非故意设计为数据框库的后端。因此,pandas 的一个主要限制是处理大型数据集时的内存处理。

在这个版本中,最大的变化来自于引入了 Apache Arrow 后端来处理 pandas 数据。

实质上,Arrow 是一种标准化的内存列式数据格式,提供了多种编程语言的可用库(如 C、C++、R、Python 等)。对于 Python,有 PyArrow,它基于 Arrow 的 C++ 实现,因此,速度很快

简而言之,PyArrow 解决了我们在 1.X 版本中的内存限制,使我们能够进行更快、更节省内存的数据操作,特别是对于大型数据集。

这是读取数据的比较,没有和使用 pyarrow 后端,使用 Hacker News 数据集(约 650 MB,许可证 CC BY-NC-SA 4.0):

read_csv() 比较:使用 pyarrow 后端快 35 倍。作者提供的代码片段。

正如你所见,使用新后端使得读取数据的速度快了近 35 倍。其他值得指出的方面有:

  • 没有 pyarrow 后端时,每列/特征作为其独特的数据类型存储:数值特征存储为 **int64****float64**,而 字符串 值存储为 对象

  • 使用 pyarrow 时,所有特性都使用 Arrow 数据类型:请注意 [pyarrow] 注释和不同的数据类型:int64float64stringtimestampdouble

df.info(): 调查每个 DataFrame 的数据类型。作者提供的代码片段。

2. Arrow 数据类型和 Numpy 索引

除了读取数据这一最简单的情况之外,你还可以期待一系列其他操作的改进,特别是那些涉及字符串操作,因为pyarrow对字符串数据类型的实现非常高效:

比较字符串操作:展示 arrow 实现的效率。代码片段由作者提供。

事实上,Arrow 支持的数据类型比numpy更多(且支持更好),这些类型在科学(数值)范围之外是必需的:日期和时间持续时间二进制十进制列表映射。浏览一下pyarrow 支持的数据类型与numpy的等效性可能是一个很好的练习,特别是如果你想学习如何利用它们。

现在也可以在索引中保存更多的 numpy 数值类型。传统的int64uint64float64为所有 numpy 数值数据类型的索引值腾出了空间,因此我们可以,例如,指定它们的 32 位版本

利用 32 位的 numpy 索引,使代码更具内存效率。代码片段由作者提供。

这是一个受欢迎的变化,因为索引是pandas中最常用的功能之一,允许用户筛选、连接和打乱数据等操作。本质上,索引越轻量,这些过程就会越高效!

3. 更容易处理缺失值

基于numpy构建使得pandas在处理缺失值时显得困难且不够灵活,因为**numpy**不支持某些数据类型的空值

例如,整数会自动转换为浮点数,这并不是理想的:

缺失值:转换为浮点数。代码片段由作者提供。

注意points在引入单个None值后如何自动从int64变为float64

没有什么比错误的类型集合更糟糕的数据流特别是在数据驱动的人工智能范式中

错误的类型集合直接影响数据准备决策,导致不同数据块之间的不兼容,即使在静默传递时,它们也可能影响某些操作,使其返回无意义的结果。

举个例子,在数据驱动的 AI 社区中,我们目前正在进行一个关于数据隐私的合成数据的项目。一个特性NOC(子女数量)有缺失值,因此当数据加载时,它会自动转换为float。然后,当将数据作为float传入生成模型时,我们可能会得到像 2.5 这样的十进制输出值——除非你是一位有 2 个孩子、新生儿,且有奇怪幽默感的数学家,拥有 2.5 个孩子是不合适的

在 pandas 2.0 中,我们可以利用 dtype = 'numpy_nullable',在不更改数据类型的情况下处理缺失值,因此我们可以保持原始数据类型(在此情况下为int64)。

利用‘numpy_nullable’,pandas 2.0 可以在不更改原始数据类型的情况下处理缺失值。片段由作者提供。

这可能看起来是一个微妙的变化,但在底层它意味着现在pandas可以原生使用 Arrow 处理缺失值的实现。这使得操作更高效,因为pandas不再需要为每种数据类型实现处理空值的版本。

4. 复制时写入优化

Pandas 2.0 还增加了一个新的延迟复制机制,该机制在修改 DataFrames 和 Series 对象之前延迟复制

这意味着 某些方法启用复制时写入时将返回视图而非副本,这通过最小化不必要的数据复制提高了内存效率。

这也意味着在使用链式赋值时需要格外小心。

如果启用了复制时写入模式,链式赋值将不起作用,因为它们指向一个临时对象,这是索引操作的结果(在复制时写入下表现为副本)。

copy_on_write被禁用时,像切片这样的操作 可能会更改原始数据框 df,如果新的数据框被更改:

禁用复制时写入:在链式赋值中原始数据框会被更改。片段由作者提供。

copy_on_write被启用时,会在赋值时创建副本,因此原始数据框不会被更改。Pandas 2.0 在这些情况下会引发 ChainedAssignmentError 以避免静默错误:

启用复制时写入:在链式赋值中原始数据框不会被更改。片段由作者提供。

5. 可选依赖项

使用pip时,2.0 版本给予我们安装可选依赖项的灵活性,这在定制优化资源方面是一个加分项。

我们可以根据具体要求定制安装,而无需在不需要的内容上浪费磁盘空间。

此外,它节省了许多“依赖性头痛”,减少了与开发环境中可能存在的其他包的兼容性问题或冲突的可能性

安装可选依赖项。片段由作者提供。

来试试吧!

然而,问题仍然存在:这些噪音是否真的有意义?我很好奇pandas 2.0是否在我日常使用的一些包上提供了显著改进:ydata-profiling、matplotlib、seaborn、scikit-learn。

其中,我决定尝试一下 ydata-profiling — 它刚刚添加了对 pandas 2.0 的支持,这似乎是社区的必备!在新版本中,用户可以放心,如果他们使用 pandas 2.0,管道不会中断,这是一个很大的优点!那还会有什么其他的呢?

说实话,ydata-profiling 一直是我最喜欢的探索性数据分析工具之一,它也是一个很好的基准测试工具 —— 在我这只需一行代码,但在幕后却充满了作为数据科学家需要解决的计算 —— 描述性统计、直方图绘制、分析相关性,等等。

那么,有什么比用最小的努力测试pyarrow引擎对所有这些的影响更好的方法呢?

与 ydata-profiling 进行基准测试。作者提供的代码片段。

再次强调,使用pyarrow引擎读取数据无疑更好,尽管在创建数据分析报告的速度上变化不大。

然而,差异可能依赖于内存效率,对此我们需要进行不同的分析。此外,我们还可以进一步调查对数据进行的分析类型:对于某些操作,1.5.2 版本和 2.0 版本之间的差异似乎微不足道

但我注意到的主要问题是 ydata-profiling 还没有利用pyarrow数据类型。这个更新可能对速度和内存有很大影响 ,这是我在未来发展中期待的!

结论:性能、灵活性、互操作性!

这个新的pandas 2.0版本带来了许多灵活性和性能优化,包括“引擎盖下”细微但至关重要的修改

也许对于数据处理领域的新手来说,这些功能并不“引人注目”,但对于那些曾经为了克服之前版本限制而不得不费尽周折的资深数据科学家们来说,这些功能无疑就像沙漠中的水源一样宝贵。

总结一下,这些是新版本中引入的主要优势:

  • 性能优化: 引入了 Apache Arrow 后端、更多的 numpy dtype 索引和写时复制模式;

  • 额外的灵活性和定制化: 允许用户控制可选依赖项,并利用 Apache Arrow 数据类型(包括从一开始就支持的可空性!)。

  • 互操作性: 也许这是新版本一个较少被“赞誉”的优势,但影响巨大。由于 Arrow 是语言独立的,内存中的数据可以在使用 Apache Arrow 后端构建的程序之间进行传输,不仅限于 Python,还包括 R、Spark 等!

那么,这就是了,各位! 我希望这次总结能解答一些关于pandas 2.0及其在我们数据处理任务中的适用性的问题。

我仍然好奇你们是否在日常编码中发现了pandas 2.0带来的主要变化!如果你愿意,来数据驱动 AI 社区找我,告诉我你的想法!在那里见?

关于我

博士,机器学习研究员,教育者,数据倡导者,以及全面的“万事通”。在 Medium 上,我写关于数据驱动 AI 和数据质量的内容,教育数据科学与机器学习社区如何从不完善的数据转向智能数据。

数据驱动 AI 社区 | GitHub | Google Scholar | LinkedIn

Pandas: apply、map 还是 transform?

原文:towardsdatascience.com/pandas-apply-map-or-transform-dd931659e9cf?source=collection_archive---------3-----------------------#2023-01-31

Pandas 最通用函数的指南

Aniruddha KarajgiTowards Data Science Aniruddha Karajgi

·

关注 发布于 Towards Data Science ·9 分钟阅读·2023 年 1 月 31 日

--

照片由Sid Balachandran提供,发布在Unsplash

作为一个使用 Pandas 多年的用户,我注意到许多人(包括我自己)经常几乎总是使用**apply**函数。虽然在较小的数据集上这不是问题,但在处理大量数据时,这种做法引起的性能问题会变得更加明显。虽然**apply**的灵活性使其成为一个简单的选择,但这篇文章介绍了其他 Pandas 函数作为潜在的替代方案。

在这篇文章中,我们将探讨**apply****agg****map****transform**的使用方式,并提供一些示例。

内容目录

* map
* transform
* agg
* apply
* Unexpected behavior

示例

让我们以一个数据框为例,该数据框包含三名学生在两门科目中的分数。我们将在接下来的工作中使用这个示例。

df_english = pd.DataFrame(
    {
        "student": ["John", "James", "Jennifer"],
        "gender": ["male", "male", "female"],
        "score": [20, 30, 30],
        "subject": "english"
    }
)

df_math = pd.DataFrame(
    {
        "student": ["John", "James", "Jennifer"],
        "gender": ["male", "male", "female"],
        "score": [90, 100, 95],
        "subject": "math"
    }
)

现在我们将这些数据合并成一个单一的数据框。

df = pd.concat(
    [df_english, df_math],
    ignore_index=True
)

我们最终的数据框看起来是这样的:

示例数据框

我们将使用这个数据集来探索每个函数的使用。

map

Series.map(arg, na_action=None) -> Series

**map**方法作用于Series,并根据传递给函数的arg来映射每个值。arg可以是一个函数——就像apply可以接受的那样——但它也可以是字典或系列。

na_action本质上让你决定如果系列中存在NaN值会发生什么。当设置为"ignore"时,arg不会应用于NaN值。

例如,如果你想用映射替换系列中的分类值,你可以这样做:

GENDER_ENCODING = {
    "male": 0,
    "female": 1
}
df["gender"].map(GENDER_ENCODING)

输出符合预期:它返回与我们原始系列中的每个元素对应的映射值。

map 的输出

尽管**apply**不接受字典,但仍然可以实现这种功能,但效率和优雅程度远不如前者。

df["gender"].apply(lambda x:
    GENDER_ENCODING.get(x, np.nan)
)

apply的输出与map的输出相同

性能

在对包含百万条记录的性别系列进行编码的简单测试中,**map****apply**快了 10 倍

random_gender_series = pd.Series([
    random.choice(["male", "female"]) for _ in range(1_000_000)
])

random_gender_series.value_counts()

"""
>>>
female    500094
male      499906
dtype: int64
""" 
"""
map performance
"""
%%timeit
random_gender_series.map(GENDER_ENCODING)

# 41.4 ms ± 4.24 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
"""
apply performance
"""
%%timeit
random_gender_series.apply(lambda x:
    GENDER_ENCODING.get(x, np.nan)
)

# 417 ms ± 5.32 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

由于**map**也可以接受函数,因此任何不依赖于其他元素的转换——例如不像均值这样的聚合——都可以传递。

使用map(len)map(upper)等方法可以使预处理变得更加容易。

让我们将这个性别编码结果分配回数据框,并继续探讨**applymap**

df["gender"] = df["gender"].map(GENDER_ENCODING)

使用 map 编码性别

applymap

**DataFrame.applymap(*func*, *na_action=None*, ***kwargs*) -> DataFrame**

我不会花太多时间在**applymap**上,因为它与**map**非常相似,并且内部实现使用了**apply****applymap**在数据框上按元素级别工作,就像**map**一样,但由于它是通过**apply**内部实现的,因此不能接受字典或系列作为输入——仅允许使用函数。

try: 
    df.applymap(dict())

except TypeError as e:
    print("Only callables are valid! Error:", e)

"""
Only callables are valid! Error: the first argument must be callable
"""

na_action的工作方式与**map**中的相同

transform

DataFrame.transform(func, axis=0, *args, **kwargs) -> DataFrame

虽然之前的两个函数在元素级别工作,但transform在列级别工作。这意味着你可以在**transform**中使用聚合逻辑

让我们继续使用之前的数据框。

我们的示例,带有编码后的性别

假设我们想要标准化数据。我们可以做如下操作:

df.groupby("subject")["score"] \
    .transform(
        lambda x: (x - x.mean()) / x.std()
    )

"""
0   -1.154701
1    0.577350
2    0.577350
3   -1.000000
4    1.000000
5    0.000000
Name: score, dtype: float64
"""

我们本质上是在从每个组中提取分数系列,并用其标准化值替换每个元素。这是不能通过**map**完成的,因为它需要按列计算,而map仅在元素级别工作。

如果你熟悉**apply**,你会知道这种行为也可以通过它来实现。

df.groupby("subject")["score"] \
    .apply(
        lambda x: (x - x.mean()) / x.std()
    )

"""
0   -1.154701
1    0.577350
2    0.577350
3   -1.000000
4    1.000000
5    0.000000
Name: score, dtype: float64
""" 

我们得到的结果基本相同。那么使用 **transform** 的意义何在?

**transform** 必须返回一个在应用轴上长度相同的数据框。

记住 **transform** 必须返回一个在其应用轴上长度相同的数据框。这意味着即使 **transform** 与返回聚合值的 groupby 操作一起使用,它也会将这些聚合值分配给每个元素。

例如,假设我们想知道每个学科所有学生分数的总和。我们可以这样用 apply 来做到这一点:

df.groupby("subject")["score"] \
    .apply(
        sum
    )

"""
subject
english     80
math       285
Name: score, dtype: int64
""" 

但是在这里,我们通过学科聚合了分数,失去了关于各个学生及其分数关系的信息。如果我们尝试用 **transform** 做同样的事情,我们会得到更有趣的结果:

df.groupby("subject")["score"] \
    .transform(
        sum
    )

"""
0     80
1     80
2     80
3    285
4    285
5    285
Name: score, dtype: int64
"""

尽管我们在组级别工作,但我们仍能跟踪组级信息与行级信息之间的关系。

由于这种行为,**transform** 如果你的逻辑没有返回一个变换后的系列,将会抛出 ValueError。因此,任何类型的聚合都不会有效。然而,**apply** 的灵活性确保了即使在进行聚合时也能正常工作,正如我们将在下一节中详细探讨的那样。

try:
    df["score"].transform("mean")
except ValueError as e:
    print("Aggregation doesn't work with transform. Error:", e)

"""
Aggregation doesn't work with transform. Error: Function did not transform
"""
df["score"].apply("mean")

"""
60.833333333333336
"""

性能

就性能而言,从 **apply** 切换到 **transform** 的速度提高了 2 倍

random_score_df = pd.DataFrame({
    "subject": random.choices(["english", "math", "science", "history"], k=1_000_000),
    "score": random.choices(list(np.arange(1, 100)), k=1_000_000)
})

用于测试 **transform** 性能的 1M 行数据框

"""
Transform Performance Test
"""
%%timeit
random_score_df.groupby("subject")["score"] \
    .transform(
        lambda x: (x - x.mean()) / x.std()
    )

"""
202 ms ± 5.37 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
"""
"""
Apply Performance Test
"""
%%timeit
random_score_df.groupby("subject")["score"] \
    .apply(
        lambda x: (x - x.mean()) / x.std()
    )

"""
401 ms ± 5.37 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
"""

agg

DataFrame.agg(func=None, axis=0, *args, **kwargs) 
    -> scalar | pd.Series | pd.DataFrame

**agg** 函数更容易理解,因为它只是返回对传递给它的数据进行的聚合。因此,无论你的自定义聚合器是如何实现的,结果将是传递给它的每一列的单个值。

我们现在来看一个简单的聚合——计算每个组在 score 列上的均值。注意我们可以传递位置参数给 **agg** 以直接命名聚合结果。

df.groupby("subject")["score"].agg(mean_score="mean").round(2)

使用 agg 按学科计算的均值

可以将多个聚合器作为列表传递。

df.groupby("subject")["score"].agg(
    ["min", "mean", "max"]
).round(2)

使用 apply 按学科计算的均值——与我们之前的结果相同。

**agg** 提供了更多的聚合选项。在前两个示例中,我们看到它允许你在列表中执行多个聚合,甚至进行命名聚合。你还可以构建自定义聚合器以及对每列进行多个特定聚合,比如对一列计算均值,对另一列计算中位数。

性能

就性能而言,**agg****apply** 稍快,至少在简单聚合的情况下是这样。让我们重新创建之前性能测试中相同的数据框。

random_score_df = pd.DataFrame({
    "subject": random.choices(["english", "math", "science", "history"], k=1_000_000),
    "score": random.choices(list(np.arange(1, 100)), k=1_000_000)
})

用于性能测试的相同数据框

"""
Agg Performance Test
"""

%%timeit
random_score_df.groupby("subject")["score"].agg("mean")

"""
74.2 ms ± 5.02 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
"""
"""
Apply Performance Test
"""

%%timeit
random_score_df.groupby("subject")["score"].apply(lambda x: x.mean())
"""
102.3 ms ± 1.16 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
"""

使用 **agg****apply** 提高了大约 30% 的性能。在进行多次聚合测试时,我们得到了类似的结果。

"""
Multiple Aggregators Performance Test with agg
"""
%%timeit
random_score_df.groupby("subject")["score"].agg(
    ["min", "mean", "max"]
)

"""
90.5 ms ± 16.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
"""
"""
Multiple Aggregators Performance Test with apply
"""
%%timeit
random_score_df.groupby("subject")["score"].apply(
    lambda x: pd.Series(
        {"min": x.min(), "mean": x.mean(), "max": x.max()}
    )
).unstack()

"""
104 ms ± 5.78 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
"""

apply

对我来说,这是我们讨论过的最令人困惑的一个,主要是由于它的灵活性。我们刚才看到的每一个例子都可以用**apply** 复制

当然,这种灵活性是有代价的:正如我们的性能测试所示,它明显较慢。

性能测试:apply函数明显较慢,这是可以理解的。

意外的行为

**apply** 灵活性带来的另一个问题是结果有时会令人惊讶。

处理第一个组两次

一个已经解决的问题是关于某些性能优化的:**apply** 会将第一个组处理两次。第一次,它会查找优化,然后处理每个组,因此会将第一个组处理两次。

我最初在调试我编写的自定义 apply 函数时注意到这一点:当我打印出组的信息时,第一个组被显示了两次。如果存在副作用,这种行为会导致静默错误,因为任何更新都会在第一个组上发生两次。

当只有一个组时

这个问题自 2014 年起就困扰着 pandas。当整个列中只有一个组时,即使**apply**函数期望返回一个系列,它最终却会产生一个数据框。

结果类似于额外的堆叠操作。让我们尝试重现它。我们将使用原始数据框,并添加一个city列。假设我们所有的三位学生,John、James 和 Jennifer 都来自波士顿。

df_single_group = df.copy()
df_single_group["city"] = "Boston"

我们的数据框增加了“city”列

现在,让我们计算两组的组均值:一组基于 subject 列,另一组基于 city

subject 列上分组时,我们得到了一个多索引系列,这是我们预期的。

df_single_group.groupby("subject").apply(lambda x: x["score"])

apply 在有多个组时返回一个多索引系列

但是当我们按city列分组时,正如我们所知,这只有一个组(对应于“Boston”),我们得到的是:

df_single_group.groupby("city").apply(lambda x: x["score"])

apply 在只有一个组时返回一个未堆叠的数据框

注意结果如何被透视?如果我们**stack** 这个,我们将得到预期的结果。

df_single_group.groupby("city").apply(lambda x: x["score"]).stack()

堆叠我们之前的结果会得到预期的结果

截至本文撰写时,这个问题仍未修复。

代码

你可以在这里找到完整的代码以及性能测试。

[## BlogCode/PandasApply 在 main · Polaris000/BlogCode]

这是我博客文章《Pandas:apply、map 还是 transform?》中使用的示例代码。博客文章讨论了 Pandas 的…

github.com

结论

**apply** 提供的灵活性使它在大多数场景中非常方便选择,但正如我们所见,使用专门为实现你的需求设计的工具通常更为高效。本文仅覆盖了 **apply** 部分内容,这个函数还有很多其他功能。未来的文章将从这里继续。

这篇文章应该给你提供了有关 Pandas 的可能性的一个概念,我希望这能鼓励你充分利用它的功能。

Polars: Pandas DataFrame 但更快

原文:towardsdatascience.com/pandas-dataframe-but-much-faster-f475d6be4cd4?source=collection_archive---------1-----------------------#2023-01-03

执行多线程优化的 pandas 操作

Travis TangTowards Data Science Travis Tang

·

关注 发表在 Towards Data Science ·11 min 阅读·2023 年 1 月 3 日

--

面对现实吧。Pandas 很慢。当你的数据框有几百万行时,等待一行代码执行一分钟变得非常令人沮丧。你会发现等待的时间比实际分析的时间还要长。

解决这个问题的库有很多。PySpark、Vaex、Modin 和 Dask 是一些例子。

今天,我们来看看 Polars

Polars 非常快。图片由 Midjourney 提供。

我与 Polars、PySpark、Vaex、Modin 和 Dask 并无任何关联。

Polars 是一个极其快速的数据框库。

Polars 非常快。两个基准测试将 Polars 与其替代品进行了比较,结果显示 Polars 是最快的库之一。

一个基准测试将 Polars 与其替代品进行对比,任务是读取数据并执行各种分析任务。Polars 的表现始终优于其他库。

数值越低,速度越快。请注意,基准测试是由 Polars 发布的,因此可能存在一些偏见。 (Source)

另一个 基准测试 由 H2O.ai 发布,比较了 Polars 与其替代品在 5 个查询中的表现。在这里,输入表的大小为 50GB,包含 10 亿行和 9 列。

Polars 再次证明了其优势。

根据 H2O 的基准测试,Polars 非常快速。 Source

Polars 快速的原因:并行化

Polars 之所以快速,是因为它使用了并行化和缓存高效算法来加速分析任务。以下是它的策略。

  • 减少冗余的复制

  • 高效遍历内存缓存

  • 最小化并行中的竞争

它是用 Rust 创建的,而不是 Python

Polars 比那些尝试使用 Python 实现并发的库(如 Pandas)要快得多。这是因为 Polars 是用 Rust 编写的,而 Rust 在实现并发方面比 Python 更出色。

Python 在实现并发方面表现不佳的原因是它使用了 全局解释器锁 (GIL),而 Rust 中没有这一特性。GIL 是一个锁,只允许一个线程控制 Python 解释器

Python 比 Rust 慢的原因。照片由 FLY:D 提供,来源于 Unsplash

这样想:你在一个玩具工厂(处理器)里,有四个工人(处理器中的核心)。

在 Python 的情况下:任何时候,一个工人需要持有钥匙(GIL)才能运行玩具机器(Python 解释器)。他们不能共享机器,一个工人必须在将钥匙传递给另一个工人之前完成玩具的工作。因此,本质上,任何时候只有一个工人可以运行玩具机器。其他三个工人处于闲置状态。

在 Rust 的情况下:任何时候,一个工人不需要持有钥匙才能使用玩具机器 他们可以共享机器,一个工人不必在另一个工人开始使用同一台机器之前完成玩具的工作。因此,本质上,多个工人可以同时使用玩具机器。没有人需要闲置。

它支持惰性执行

惰性执行 意味着表达式不会立即被评估,而是仅在需要时才评估。相比之下,急切执行 立即评估表达式。

因此,Polars 可以进行优化——仅运行所需的部分,忽略不必要的部分。它还可以在运行时并行化这些评估。

另一方面,Pandas 执行时是急切的,导致资源浪费。

你可以通过快进到下面的“懒惰地选择列”和“急切地选择列”部分,来查看急切执行和懒惰执行之间的差异示例。

安装 Polars

安装 Polars 很简单。只需在终端中运行以下命令。(注意 [all] 子句在这里是可选的。)

pip install polars[all]

数据集:纽约市停车票(4200 万行 x 51 列)

为了说明 Polars 的使用,我们将使用一个大型数据集,即 4200 万行 纽约市停车票 来自 Kaggle(它具有 公共领域 许可,所以可以随意使用!)

纽约市财政部收集有关每张纽约市停车票的数据(每年约 1000 万张!)。

纽约市停车。照片由 Scott GummersonUnsplash 提供

完整的数据集有 4200 万行,并分布在 4 个文件中——每年一个文件。接下来的博客中,我们将只使用 一个文件(2013 年至 2014 年)。

为什么我们没有使用全部四个文件?不幸的是,当我尝试将所有四个文件连接在一起时,Polars 崩溃了。

下面的所有代码都在 Kaggle notebook 中执行,该 notebook 拥有:

  • 4 个 CPU 核心

  • 30 GB RAM

使用 Polars 读取数据

Polars 提供 scan_csv 选项。扫描会延迟实际的文件解析,而是返回一个名为 LazyFrame 的懒惰计算持有者。

实际计算发生在调用 collect() 时。

为什么我们要延迟实际解析文件?这样可以让 Polars 生成一个最优的执行计划。例如,当调用 collect 时,Polars 可以跳过加载某些列的过程,如果它们在计算中不需要。

# Scanning in 9 million rows and 51 columns. 
# We ignore any potential errors in the dataset due to encoding / dirty null values.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", 
                      ignore_errors = True)

# Read the data
result_df = temp_df.collect()

# Reading dataset
result_df

# Time taken: 14.1 s ± 3.29 s per loop

你还可以使用其他函数来读取数据,包括:

基于条件过滤行

过滤精确值

你也可以使用 filter 关键字来过滤特定的行。为此,你需要使用 pl.col(['column_name_here']) 函数来指定列名。

# Lazily read (scan) in 9 million rows and 51 columns.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# Filtering for rows with the "Registration State" in NY
result_df = temp_df.filter(pl.col(['Registration State'])=="NY")

# Run the filtering using collect.
result_df.collect()

# Time taken: 12.6 s ± 205 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

你可以想象,我们可以使用其他操作符,如>(小于)、<(大于)、>=(大于或等于)、&(与)、|(或)来实现更复杂的条件。

过滤更复杂的条件

你也可以根据更复杂的条件进行过滤。在这里,我使用正则表达式来过滤行。条件是Plate ID必须包含‘a’或‘1’。

# Lazily read (scan) in 9 million rows and 51 columns.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# Find all carplates that contain the letter 'a' or '1'. 
result_df = temp_df.filter(pl.col("Plate ID").str.contains(r"[a1]"))

# Run the filter using collect.
result_df.collect()

# Time taken: 12 s ± 176 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

过滤精确值(非常慢的方法)

你也可以使用索引选择行,这对 pandas 用户来说是熟悉的选择数据方式。

为此,你不能使用惰性读取 CSV 的scan_csv函数。相反,你应该选择急切读取 CSV 的open_csv函数。

请注意,这是一种反模式,因为它不允许 Polars 进行并行化。

# Eagerly read in 9 million rows and 51 columns.
# Note that we use read_csv, not scan_csv here.
temp_df = pl.read_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# Filtering for rows with the "Registration State" in NY
result_df = temp_df[['Registration State']=="NY"]

# Time taken: 15 s ± 3.72 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

选择列

惰性选择列

你可以使用select关键字选择列。请注意,这里的语法已经不同于常规的 pandas 语法。

# Lazily read (scan) in 9 million rows and 51 columns.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# Selecting a particular column called Plate ID. 
# In pandas, this will look like result_df = temp_df['Plate ID']
result_df = temp_df.select(['Plate ID']).collect()

# Run it using the collect()
result_df.collect()

# Time taken: 1.59 s ± 24.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

急切选择列

类似地,你可以使用方括号表示法来选择列。然而,如我在上面的“通过索引过滤”部分所提到的,这是一种反模式。

# Eagerly read in all 9 million rows and 51 columns.
temp_df = pl.read_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# Selecting the Plate ID column
result_df = temp_df['Plate ID']

# Time taken: 12.8 s ± 304 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

让我们比较惰性执行和急切执行的速度。惰性执行需要 1.59 秒,而急切执行需要 12.8 秒。7 倍的速度提升是因为 Polars 在惰性执行中读取“Plate ID”列,而在急切执行中需要读取所有列(不仅仅是“Plate ID”)。

创建新列

要创建一个新列,Polars 使用with_columns语法。

使用字符串函数创建列

以下示例中,找到字符串列Plate IDlength并将其命名为plate_id_letter_count

# Lazily read (scan) in 9 million rows and 51 columns.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# String functions to find all Plate ID that contain the letter 'a' or '1'
result_df = temp_df\
            .with_column(pl.col("Plate ID").str.lengths().alias("plate_id_letter_count"))\

# Evaluate the string function.
result_df.collect()

# Time taken: 14.8 s ± 5.79 s per loop

使用 lambda 函数创建列

可以在map函数中指定lambda函数,以指定应用于特定列的函数。然后,我们可以使用函数的结果创建一个新列。

# Lazily read (scan) in 9 million rows and 51 columns.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# Create a new column called "Clean Violation Code"
# using the formula 10000 + df["Violation Code"]
result_df = temp_df.with_columns([
    pl.col("Violation Code").\
    map(lambda x: x+10000).\
    alias("Clean Violation Code")
])

# Evaluate the function.
result_df.collect()

# Time taken: 13.8 s ± 796 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) 

执行聚合

我们还提供了一个groupby和聚合的示例。

# Lazily read (scan) in 9 million rows and 51 columns.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# For each vehicle registration state, calculate the number of tikcets 
# and create a list of all violation codes.
result_df = temp_df\
            .groupby("Registration State").agg(
    [
        pl.count(),
        pl.col("Violation Code").list(),

    ]
).sort('Registration State')\
.collect()

result_df

# time taken: 2.3 s ± 29.1 ms per loop

组合多个函数

数据科学家经常需要同时执行多个步骤。我们可以使用 Polars 中的.表示法来实现。

在以下示例中,我们首先使用with_columnIssue Date列从string列替换为datetime列。

然后,我们对Registration State进行groupby。对于每个州,我们找到票据的最早Issue Date

最后,我们按字母顺序对Registration State进行排序。

# Lazily read (scan) in 9 million rows and 51 columns.
temp_df = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)

# Combine multiple steps into one
# Convert "Issue Date" intoa date column, 
# Then group by Registration State and perform some aggregation.
result_df = temp_df\
            .with_column(pl.col("Issue Date").str.strptime(pl.Date, fmt="%m/%d/%Y"))\
            .groupby("Registration State").agg(
                [pl.first("Issue Date")]
              ).sort('Registration State')\

# Run the steps
result_df.collect()

# Took 1.69 s ± 18.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

将两个表合并成一个

如果你有两个存储在两个单独文件中的表,并且你想将它们合并成一个数据帧?使用concat方法。

# Lazily scan two dataframes 
temp_df1 = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)
temp_df2 = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2015.csv", ignore_errors = True)

# Concatenating datasets
result_df = pl.concat(
    [
        temp_df1,
        temp_df2,
    ],
    how="vertical",
)

# Reading dataset
result_df.collect()

# Time taken: 

按键连接表

你可能需要将一个表中的信息与另一个表连接。为此,你可以使用join方法。

有多种类型的join,包括熟悉的类型(inner、left、right)以及一些新型的(anti、asof、semi)。更多细节请阅读文档

df_cars = pl.DataFrame(
    {
        "id": ["a", "b", "c"],
        "make": ["ford", "toyota", "bmw"],
    }
)
df_repairs = pl.DataFrame(
    {
        "id": ["c", "c"],
        "cost": [100, 200],
    }
)

df_inner_join = df_cars.join(df_repairs, on="id", how="inner")
print(df_inner_join)
shape: (2, 3)
┌─────┬──────┬──────┐
│ id  ┆ make ┆ cost │
│ --- ┆ ---  ┆ ---  │
│ str ┆ str  ┆ i64  │
╞═════╪══════╪══════╡
│ c   ┆ bmw  ┆ 100  │
├╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌┤
│ c   ┆ bmw  ┆ 200  │
└─────┴──────┴──────┘

它与 Vaex 有何不同?

我早期写了一篇关于 Vaex 的帖子,并收到许多关于 Vaex 和 Polars 是否不同的评论。以下是我的一些观察。

  1. Vaex 的语法更类似于 Pandas;Polar 的语法更类似于 R 或 PySpark。

  2. Vaex 具有更多的机器学习和数据可视化 API。Polar 的重点是基础的数据处理(过滤、选择、聚合)。

  3. Vaex 是用 CPP 编写的,而 Polars 是用 Rust 编写的。

根据你的使用案例,你可能会发现 Vaex 或 Polars 更好。例如,在纽约数据集中,我无法使用 Polars 连接所有四个 CSV 文件(它会导致内存不足问题),而我可以用 Vaex 做到这一点。

import polars as pl

# Read four dataframes to form a 42.3M parking ticket datasets
temp_df1 = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", ignore_errors = True)
temp_df2 = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2015.csv", ignore_errors = True)
temp_df3 = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2017.csv", ignore_errors = True)
temp_df4 = pl.scan_csv("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2016.csv", ignore_errors = True)

# Concatenating dataset
result_df = pl.concat(
    [
        temp_df1,
        temp_df2,
        temp_df3,
        temp_df4,
    ],
    how="vertical",
)

# Reading dataset
result_df.collect()

# Could not be run since it causes out-of-memory issue. 
import vaex 

# Read four dataframes to form a 42.3M parking ticket datasets
temp_df1 = vaex.open("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2014__August_2013___June_2014_.csv", schema_infer_fraction=1)
temp_df2 = vaex.open("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2015.csv", schema_infer_fraction=1)
temp_df3 = vaex.open("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2017.csv", schema_infer_fraction=1)
temp_df4 = vaex.open("/kaggle/input/nyc-parking-tickets/Parking_Violations_Issued_-_Fiscal_Year_2016.csv", schema_infer_fraction=1)

# Concatenating dataset
result_df = vaex.concat([
        temp_df1,
        temp_df2,
        temp_df3,
        temp_df4
    ])

# Reading dataset
result_df

# Could be run without out-of-memory problem.

判决:在这种条件下使用 Polars。

你应该使用 Polars?Vaex?PySpark?Dask?以下是我的思考方式:

  • 如果你的数据很庞大,进入了 10GB 以上的“大数据”领域,你可能要考虑使用 PySpark。否则,Polars、Vaex 和 Dask 是可能的选择。

  • 如果你有多台计算机在一个集群中,并且你想在这些计算机之间分配工作负载,使用 Dask。

  • 如果你需要可视化、机器学习和深度学习,使用 Vaex。如果不需要,使用 Polars。

这是一个粗略的指南,因为最终的答案取决于你的使用案例。因此,我鼓励你尝试每一个。在某些情况下,Polars 和 Vaex 可能比 PySpark 更快(当数据集有数百万行时)。在某些情况下,Vaex 可以运行,但 Polars 可能会崩溃(如上面的concat示例)。

超级速度的 pandas。图片由 Midjourney 提供。

我是一名数据科学家,我在LinkedIn和 Medium 分享数据科学技巧。关注我以获取更多类似的技巧。

时间序列的 Pandas

原文:towardsdatascience.com/pandas-for-time-series-c6cb7c0a3680

Python 中的数据处理

本文解释了 pandas 的时间序列方法。让我们像专业人士一样处理时间序列。

KahEm ChuTowards Data Science KahEm Chu

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

--

图片由 Aron Visuals 提供,Unsplash 上的照片

自从我作为数据科学家进入职场以来,我处理的大多数数据都是时间序列。好吧,时间序列有很多定义,通常定义为一组在一段时间内收集的数据点。或者用 Python 的方式说,它指的是一个带有日期时间索引的数据集,并且至少有一列数值。

这可以是过去几个月的股票价格,过去几周的大型超市销售数据,甚至是一个患者在几个月内记录的血糖水平。

在本文中,我将展示如何将 pandas 应用于时间序列数据集,以生成的血糖水平记录为例。

有了这些,这篇文章将按如下结构进行:

  1. 日期时间格式处理 — 将日期时间序列转换为所需格式

  2. 将日期时间转换为特定周期 — 将每个数据点转换为特定时间段

  3. 根据条件筛选日期时间序列 — 根据选定时间段筛选数据点

  4. 时间偏移 — 将数据点向下移动特定周期

  5. 重新抽样时间序列 — 根据指定的时间周期对数据点进行分组

  6. 折线图

让我们开始吧!

像往常一样,任何 Python 分析的第一步是导入必要的库。

导入库

import pandas as pd
import random
import numpy as np
from datetime import datetime 

创建数据

接下来,让我们为这个演示生成一个血糖水平记录数据集。

def create_demo_data():

    random.seed(365)
    np.random.seed(365)
    number_of_data_rows = 2160

    # generate list of date
    dates = pd.bdate_range(datetime(2020, 7, 1), freq='4H', periods=number_of_data_rows).tolist()

    # create a dictionary with the date generated and blood sugar level
    data = {'date': dates,
            'blood_sugar_level': np.random.normal(5.5, 1, size=(1, number_of_data_rows))[0]}
    # create dataframe
    df = pd.DataFrame(data)
    df = df.sort_values(by=["date"])
    df = df.set_index(keys="date")
    return df

df = create_demo_data()
print(df.shape)
df.head(10)

使用上述脚本,生成了一个包含 2160 个数据点的数据集,记录了一年,间隔 4 小时。数据点从 2020 年 7 月 1 日开始,到 2021 年 6 月 25 日结束。

前 10 个数据点。图片来源:作者。

最后的 10 个数据点。图片来源:作者。

现在数据准备好了,开始吧!😊

日期时间格式操作

以下是五种可以用来操作日期时间序列的方法。

[pandas.Series.dt.normalize](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.dt.normalize.html) — 将时间转换为午夜 12:00:00。

[pandas.Series.dt.round](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.dt.round.html) — 对数据执行四舍五入操作到指定的频率。

[pandas.Series.dt.floor](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.dt.floor.html) — 对数据执行向下取整操作到指定的频率。

[pandas.Series.dt.ceil](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.dt.ceil.html) — 对数据执行向上取整操作到指定的频率。

[pandas.Series.dt.strftime](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.dt.strftime.html) — 使用指定的 date_format 转换为索引。

舍入时间戳的方法

对于.round.floor.ceil方法,我们需要定义频率来舍入时间。频率必须是固定频率,如‘S’(秒),不能是‘ME’(月末)。

根据作者的尝试与错误,固定频率有 D、H、T/min、S、L/ms、U/us 和 N。查看时间序列频率获取所有频率值。

.round.floor.ceil方法之间的区别将在下面的两个例子中显示。下面的两个例子将使用生成的日期列表,而不是为演示生成的数据框。

第一个例子是将时间戳从 2 小时频率间隔舍入到 6 小时频率间隔。

import pandas as pd
# generate dates at 2-hour frequency
ts = pd.date_range(pd.Timestamp("2023-04-06 12:00"), periods=5, freq='2H')
print("Before normalize\n", ts)
freq = "6H"
ts_norm = ts.normalize()
ts_round = ts.round(freq=freq)
ts_floor = ts.floor(freq=freq)
ts_ceil = ts.ceil(freq=freq)
print("After normalize\n", ts_norm)
print("After round off\n",ts_round)
print("After round off to floor\n",ts_floor)
print("After round off to ceiling\n",ts_ceil)

脚本输出 1。图片来源:作者。

第二个例子是将时间戳从 3 小时频率间隔舍入到 6 小时频率间隔。

import pandas as pd
# generate dates at 3-hour frequency
ts = pd.date_range(pd.Timestamp("2023-04-06 12:00"), periods=5, freq='3H')
print("Before normalize\n", ts)
freq = "6H"
ts_norm = ts.normalize()
ts_round = ts.round(freq=freq)
ts_floor = ts.floor(freq=freq)
ts_ceil = ts.ceil(freq=freq)
print("After normalize\n", ts_norm)
print("After round off\n",ts_round)
print("After round off to floor\n",ts_floor)
print("After round off to ceiling\n",ts_ceil)

脚本输出 2。图片来源:作者。

6 小时频率意味着时间将被分割成等长的时间段,每个时间段间隔 6 小时,第一个时间段总是从午夜 00:00:00 开始。一天有 24 小时,因此我们会有 4 个时间段,如下图所示。

第一个时间段将是凌晨 12 点到早上 6 点,然后第二个时间段将是早上 6 点到中午 12 点,以此类推。

round、floor 和 ceil 方法之间的区别。图片来源:作者。

如上图所示,对于 .floor 方法,它将始终返回时间段的下边界。对于 .ceil 方法,则与 .floor 相反,它将始终返回时间段的上边界。

对于 .round 方法,它将返回该时间段的最近边界,但当距离两个边界相等时,它将返回任意一个。

这就是上述三种方法之间的区别。时间戳的舍入方式将影响后续分析的结果,因此,根据需要的分析选择方法是至关重要的。

要在数据框上应用上述方法,我们需要在方法前加上 .dt

要更改日期,我们可以将日期索引复制为一列。

df['dates'] = df.index
df.head()

一个包含重复日期列的数据框。图片由作者提供。

数据框的 dtypes 如下所示。

图片由作者提供。

在下面的第一个示例中,我们将在 6 小时的频率下对时间戳进行舍入。.dt.normalize 将始终将时间舍入到午夜,因此这里只显示日期。

freq = "6H"
df["ts_norm"] = df.dates.dt.normalize()
df["ts_round"] = df.dates.dt.round(freq=freq)
df["ts_floor"] = df.dates.dt.floor(freq=freq)
df["ts_ceil"] = df.dates.dt.ceil(freq=freq)
df.head()

图片由作者提供。

第一个示例与生成的日期列表的两个示例相同,时间戳按小时进行舍入。

在下面的第二个示例中,我们将以“D”频率对日期进行舍入,这意味着时间戳将舍入到最近的一天。

freq = "D"
df["ts_norm"] = df.dates.dt.normalize()
df["ts_round"] = df.dates.dt.round(freq=freq)
df["ts_floor"] = df.dates.dt.floor(freq=freq)
df["ts_ceil"] = df.dates.dt.ceil(freq=freq)
df.head()

图片由作者提供。

对于第二个示例,.floor 方法将返回时间段的下边界,即与时间戳相同的一天,而 .ceil 方法将始终返回时间段的上边界,即日期的下一天 除非 时间戳是在午夜 00:00 时。

对于 .round 方法,它仍然会返回时间段的最近边界,但当距离两个边界相等时,它将返回任意一个。

转换 DateTime 格式的方法 — strftime()

通过使用 .strftime() 方法,我们可以将时间戳转换为所需的格式。在下面的示例中,标准的 ISO 格式日期已被转换为 , 格式,这可以通过 strftime() 格式代码‘%B %d, %Y’ 表示。%B 代表的是月份的完整名称。查看所有的 strftime() 格式代码 这里

df["formatted_date"] = df["dates"].dt.strftime('%B %d, %Y')
df.head()

图片由作者提供。

转换 DateTime 格式,例如将月份从数字转换为完整名称,可以消除 7 是指月份还是日期的混淆。

将 DateTime 转换为特定的时间段

[pandas.Series.dt.to_period](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.dt.to_period.html) — 转换为特定频率的 PeriodArray/Index。将 DatetimeArray/Index 转换为 PeriodArray/Index。

以下第一个示例将时间戳转换为周。

ts = pd.date_range(pd.Timestamp("2023-04-06 13:00"), periods=5, freq='w')
print("Before \n", ts)
freq = "w"
ts_period = ts.to_period(freq=freq)
print("After \n", ts_period)

如下输出所示,日期时间数组已转换为周期数组,其中列表中的每个元素已从时间戳更改为周。

图片由作者提供。

以下第二个示例将时间戳转换为月份。

ts = pd.date_range(pd.Timestamp("2023-04-06 13:00"), periods=5, freq='w')
print("Before\n", ts)
freq = "M"
ts_period = ts.to_period(freq=freq)
print("After\n", ts_period)

如下输出所示,日期时间数组已转换为月份数组,其中列表中的每个元素已从时间戳更改为月份。

图片由作者提供。

现在让我们将其应用于数据框。与之前的示例一样,应用方法到数据框时,需要在 .to_period() 前添加 .dt

df["period"] = df["dates"].dt.to_period(freq="M")
df.head()

日期列中的每个日期时间已被转换为月份。

图片由作者提供。

使用新的周期列,我们可以创建透视表以获得更多见解。例如,我们可以计算每个月的平均血糖水平。

df_pivot = pd.pivot_table(data=df, index="period", values="blood_sugar_level", aggfunc=np.mean)
df_pivot

图片由作者提供。

我们还可以通过周期间计算平均血糖水平,以便进行更详细的监测。

df["week_period"] = df["dates"].dt.to_period(freq="W")
df.head()

图片由作者提供。

我们可以计算每周的平均血糖水平,如下所示。如果血糖水平在多周内持续上升,则应采取措施进行控制。

df_pivot_week = pd.pivot_table(data=df, index="week_period", values="blood_sugar_level", aggfunc=np.mean)
df_pivot_week.head()

图片由作者提供。

如果我们使用每日数据,几天内血糖水平的上升可能仅是由于丰盛的晚餐,但几周内血糖水平的上升可能表明患者的健康存在风险。

图片由作者提供。

对于透视表,索引标签和列标签将不会处于同一级别,如上图所示。

为了使索引标签和列标签处于同一级别,需要重置索引,如下所示。

df_pivot_week = df_pivot_week.reset_index()
df_pivot_week.head()

图片由作者提供。

基于条件过滤 DateTime 系列

[pandas.Series.at_time](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.at_time.html) — 选择一天中特定时间的值(例如,上午 9:30)。

[pandas.Series.between_time](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.between_time.html) — 选择一天中特定时间段之间的值(例如,上午 9:00–9:30)。

这种方法适用于健康记录的调查,例如每天早晨和睡前的血糖水平。它也可以用于跟踪销售或特定时间段内的航班等应用。

在以下示例中,我们将应用上述两种方法来跟踪上午 8 点和晚上 7 点到 9 点之间的血糖水平记录。

# to check the blood sugar level at every morning 8am
df.at_time('8:00')

图片由作者提供。

在上面的例子中,仅会显示在早上 8 点记录的记录。

注意: 此方法只能应用于索引为DatetimeIndex的数据框,否则会导致如下面所示的 TypeError。

作者提供的图片。

例如下面的例子中,仅会显示晚上 7 点到 9 点之间的前五条记录。

df.between_time(start_time="19:00", end_time="21:00").head()

作者提供的图片。

时间移位

[pandas.Series.shift](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.shift.html) — 移动时间索引,如果可用的话使用索引的频率。

注意: 由于此方法根据时间索引移动数据点,因此索引必须是日期时间索引周期索引

df_pivot

让我们回顾前一节的透视表来演示时间移位方法。

作者提供的图片。

df_pivot.shift(periods=1)

我们可以按照脚本中指定的周期向下移位数据点。

作者提供的图片。

通过将周期更改为-1,我们也可以将数据点向上移位一个周期。例如,数据点已经向上移位一个月,这意味着当前的 7 月数据点是 8 月。

df_pivot.shift(periods=-1)

作者提供的图片。

列名可能会产生歧义,因为它没有反映实际值。为了解决这个问题,我们可以将其与.rename()方法结合,直接重命名时间移位列。

df_shift = df_pivot.shift(periods=1).rename(columns={"blood_sugar_level":"previous month value"})
df_shift

作者提供的图片。

我们可以将原始数据点与移位后的数据点结合,制作比较表。

df_combine = df_pivot.join(df_shift, how="left")
df_combine

作者提供的图片。

使用上面的两个系列,我们可以计算血糖水平的变化百分比。

df_combine["pct change"] = (df_combine["previous month value"] - df_combine["blood_sugar_level"])/df_combine["previous month value"]
df_combine

作者提供的图片。

这很酷。你知道什么更酷吗?我们不需要计算变化百分比,pandas已经为你准备好了方法😊!

df_pivot["pct_change"] = df_pivot.pct_change()
df_pivot

作者提供的图片。

df_pivot["value_change"] = df_pivot["blood_sugar_level"].diff()
df_pivot

继续前面的例子,现在数据框中有两列。当有多个列时,需要指定在哪一列上进行计算。

作者提供的图片。

重采样时间序列

[pandas.Series.resample](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.resample.html) — 用于时间序列频率转换和重采样的便捷方法。对象必须具有类似日期时间的索引(DatetimeIndex、PeriodIndex 或 TimedeltaIndex),或者调用者必须将类似日期时间的系列/索引的标签传递给 on/level 关键字参数。

下面的例子使用的是上一节中的df_pivot

df_pivot.resample("Q").mean()

上面的脚本将根据时间段索引重新采样数据框,这意味着它将根据指定的时间段进行分组,在本例中是“Q”或季度,通过该时间段内数据点的均值进行分组。

图片由作者提供。

下面是另一个以不同时间段重新采样数据点的示例。

df_pivot.resample("Y").mean()

图片由作者提供。

在这个示例中,数据点将根据指定的时间段进行分组,在本例中是“Y”或年份,通过该时间段内数据点的均值进行分组。我们也可以通过该时间段内数据点的最大值进行分组。

df_pivot.resample("Y").max()

图片由作者提供。

请注意,计算列如pct_changevalue_change也会被加总,因此现在输出中的计算列不再反映正确的值。

请注意,重新采样要求数据框具有类似 DateTime 或时间段的索引,否则会导致类型错误。我们来看看。

df_wrong_example = df_pivot.reset_index()
df_wrong_example.head()

图片由作者提供。

df_wrong_example.resample("Y").sum()

图片由作者提供。

折线图

[pandas.Series.plot.line](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.plot.line.html) — 将 Series 或 DataFrame 绘制为折线图。

我们可以使用.plot()方法和.plot.line()绘制折线图。

df_pivot["blood_sugar_level"].plot.line()
df_pivot["blood_sugar_level"].plot()

上述两个命令产生相同的结果。

图片由作者提供。

如果你注意到,1 月下有 2021 年,或许如果我们让数据从 2020 年 1 月开始,它也会显示 2020 年?

让我们创建一个数据点从 2020 年开始的数据框。

def create_demo_data():

    random.seed(365)
    np.random.seed(365)
    number_of_data_rows = 55

    # create a month data
    dates = pd.bdate_range(datetime(2020, 1, 1), freq='2W', periods=number_of_data_rows).tolist()

    data = {'date': dates,
            'blood_sugar_level': np.random.normal(5.5, 1, size=(1, number_of_data_rows))[0]}

    df = pd.DataFrame(data)
    df = df.sort_values(by=["date"])
    df = df.set_index(keys="date")
    return df

df = create_demo_data()
print(df.shape)
df.head(10)

图片由作者提供。

df.plot()

让我们绘制图表!

图片由作者提供。

是的!所以年份会显示在 1 月下,如果你有从年度第二个月或之后的数据,你可能需要更好的标题以减少观众的混乱。

df_pivot["blood_sugar_level"].plot(title="Blood Sugar Level Record from Jul'20 - Jun'21", xlabel="Date", ylabel="Blood Sugar Level")

图片由作者提供。

最后一点,pandas绘图功能需要安装matplotlib库,否则会导致导入错误。

结论

总之,我们已经涵盖了如何格式化 DateTime 序列,以及如何通过重新采样将时间戳更改为时间段或将其分组到时间段中。我们还涵盖了如何根据条件有效过滤时间序列,并上下移动数据点以获取新见解。最后,我们还介绍了如何快速创建图表以查看时间序列的趋势。

关于pandas时间序列的内容就到这里。我希望你喜欢阅读这篇文章,并希望它能帮助你更好地了解如何应用pandas处理时间序列。谢谢!😊

保持联系

订阅 YouTube

附注

我在 [处理 Python 中的日期] (https://medium.com/towards-data-science/dealing-with-dates-in-python-1b4069a07a0f) 中解释了你可以对 DateTime 变量进行的可能操作。

在 报告自动化技巧与 Python 中,我解释了一些报告自动化的技巧。快来看看吧!

感谢你阅读到最后,祝贺你😊!

图片由 Hanny Naibaho 提供,来源于 Unsplash

Pandas & Python 数据科学与数据分析技巧 — 第二部分

原文:towardsdatascience.com/pandas-python-tricks-for-data-science-data-analysis-part-2-dc36460de90d

这是我的 Pandas & Python 技巧的第二部分

Zoumana KeitaTowards Data Science Zoumana Keita

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

--

图片来源:Andrew NeelUnsplash

介绍

几天前,我分享了一些 Python 和 Pandas 技巧,帮助数据分析师和数据科学家快速学习他们可能不知晓的新有价值概念。这也是我每天在LinkedIn上分享的技巧合集的一部分。

这些技巧的视频系列可以在我的 YouTube 频道上查看。

我的系列中的 3 个技巧

Python

从列表中移除重复项

尝试从列表中移除重复项时,你可能会尝试使用𝗳𝗼𝗿 循环方法。

这样做虽然有效,但在处理非常大的数据时效率不高 ❌。

相反,使用𝘀𝗲𝘁() ✅,它本身不接受重复项。

以下是一个说明 💡

移除重复项(图片来源:作者)

原始列表中的顺序

使用𝘀𝗲𝘁()来从列表中移除重复项是一个很好的方法。

🚨 但要小心使用,因为它不会❌ 保留原始顺序。仅在不关心列表中元素顺序时使用。

相反,使用𝗱𝗶𝗰𝘁.𝗳𝗿𝗼𝗺𝗸𝗲𝘆𝘀() ✅ 保持原始顺序。

以下是一个说明 💡

原始顺序由 dict.fromkeys 保持(图片来源:作者)

检查元素是否存在于列表中

#Python 技巧 ✨🐍✨

尝试 𝗰𝗵𝗲𝗰𝗸 𝗶𝗳 𝗮𝗻 𝗶𝘁𝗲𝗺 𝗲𝘅𝗶𝘀𝘁𝘀 𝗶𝗻 𝗮 𝗹𝗶𝘀𝘁 时,你可能会尝试使用𝗳𝗼𝗿 循环和 𝗶𝗳 条件方法。

这样做有效,但处理非常大数据时效率不高 ❌。

相反,使用 𝗶𝗻 ✅ 方法,它本地返回布尔值。

以下是一个示例 💡

检查元素是否存在于列表中(图像作者)

获取 Python 列表中的 N 个最大和最小值

在 Python 中,可以使用 𝗺𝗮𝘅() 和 𝗺𝗶𝗻() 函数分别找到列表的最大值和最小值。

然而,当涉及到获取 𝗡 𝗹𝗮𝗿𝗴𝗲𝘀𝘁 或 𝘀𝗺𝗮𝗹𝗹𝗲𝘀𝘁 值时,你可能会考虑双向方法:

1️⃣ 按递减或递增顺序对列表进行排序。

2️⃣ 检索 N 个最大或最小值。

好策略,但处理大数据时效率不高 ❌。

✅ 相反,你可以使用 𝗻𝗹𝗮𝗿𝗴𝗲𝘀𝘁 和 𝗻𝘀𝗺𝗮𝗹𝗹𝗲𝘀𝘁 函数来自内置的 Python 模块 𝗵𝗲𝗮𝗽𝗾,它是快速 🚀 和内存高效的 👍。

𝗻𝗹𝗮𝗿𝗴𝗲𝘀𝘁 和 𝗻𝘀𝗺𝗮𝗹𝗹𝗲𝘀𝘁 函数示例(图像作者)

Pandas

使用同一单元格显示多个数据框

大多数时候,我们倾向于使用不同的笔记本单元格来显示不同的数据框,比如相同数据的 head() 和 tail()。

这是因为在同一单元格中使用它们时,只有最后一个会被显示,之前的所有指令都会被忽略 ❌。

✅ 解决此问题,你可以使用 𝗱𝗶𝘀𝗽𝗹𝗮𝘆() 函数。

以下是一个示例 💡

从同一笔记本单元格中获取多个数据框(图像作者)

描述数值列和分类列

应用 𝗱𝗲𝘀𝗰𝗿𝗶𝗯𝗲() 函数没有参数时,自然只返回与数值列相关的统计数据。

这限制了 🚫 我们对数据集的理解,因为大多数时候我们也处理分类列。

✅ 为了解决这个问题,你可以采取双向方法:

1️⃣ 使用 𝗱𝗲𝘀𝗰𝗿𝗶𝗯𝗲() 对数值列进行描述。

2️⃣ 设置参数 𝗶𝗻𝗰𝗹𝘂𝗱𝗲=[𝗼𝗯𝗷𝗲𝗰𝘁] 以提供有关分类列的信息。

以下是一个示例 💡

描述 包括分类列(图像作者)

创建新列时避免使用 for 循环

在处理 Pandas 数据框时,从现有列创建新列主要是过程的一部分。

这些列的创建方式会影响整体计算时间的效率 ⏰。

有些人可能会使用循环来生成这些派生列。

然而,这可能不是正确的方法 ❌,因为时间复杂度 📈,特别是在处理大数据时。

✅ 采用向量化方法要好得多。

向量化与 for 循环的示例

保存 Pandas 列的子集

有时我们只对从原始数据框中保存子集列感兴趣,而不是整个数据。

一种方法是创建一个包含感兴趣列的新数据框。

但是,这种方法增加了另一个复杂性 ❌。

✅ 这个问题可以通过指定 columns 参数来解决。

获取 Pandas 列的子集(图像作者提供)

将网页上的表格数据转换为 Pandas Dataframe

如果你想从网页 🌐 中提取表格作为 Pandas Dataframes,你可以使用 Pandas 的 𝗿𝗲𝗮𝗱_𝗵𝘁𝗺𝗹() 函数。

✅ 它返回网页上所有表格的列表。

将网页表格转换为 Pandas Dataframe(图像作者提供)

结论

感谢阅读! 🎉 🍾

我希望你觉得这份 Python 和 Pandas 技巧的清单对你有帮助!请继续关注,因为内容将每天更新更多技巧。

此外,如果你喜欢阅读我的故事并希望支持我的写作,可以考虑 成为 Medium 会员。每月 $5 的承诺,你就能解锁 Medium 上的无限故事访问权限。

随时关注我的 MediumTwitterYouTube,或者在 LinkedIn 上打个招呼!讨论 AI、ML、数据科学、NLP 和 MLOps 总是令人愉快的!

Pandas & Python 数据科学与数据分析技巧——第三部分

原文:towardsdatascience.com/pandas-python-tricks-for-data-science-data-analysis-part-3-462d0e952925

这是我 Pandas & Python 技巧系列的第三部分

Zoumana KeitaTowards Data Science Zoumana Keita

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

--

照片由 Andrew Neel 提供,来源于 Unsplash

介绍

几天前,我分享了一些 Python 和 Pandas 技巧来帮助数据分析师和数据科学家快速学习他们可能不熟悉的新有价值的概念。这也是我每天在LinkedIn上分享的技巧集合的一部分。

Pandas

根据条件替换数据框中的值

如果你想根据条件替换数据框中的值

✅ 你可以使用 Pandas 内置的 𝗺𝗮𝘀𝗸() 函数。

下面是一个示例 💡

为你的 Pandas 数据框应用颜色

你是否曾经希望仅通过查看你的数据框快速找到一些信息❓

例如:

✨ 每列中哪些值是负数?

✨ 每列的最大值或最小值是多少?

✨ 哪些值低于或高于平均值?

列表还在继续…

查看此类信息的一个很好的方法是使用颜色 🎨

✅ 𝗣𝗮𝗻𝗱𝗮𝘀.𝘀𝘁𝘆𝗹𝗲 是一个内置模块,为你的数据框提供了一个高级样式接口。

这里有一个视频示例!

在 Markdown 中打印 Pandas 数据框

以更易于理解的方式打印你的数据框总是更好。

✅ 一种方法是使用 .𝚝𝚘_𝚖𝚊𝚛𝚔𝚍𝚘𝚠𝚗() 函数将其渲染为 Markdown 格式。

请在评论中告诉我你最喜欢哪一个。

✨ 使用 Markdown ✅ 或不使用 Markdown ❌

通过数据框进行类似 SQL 的查询

Pandas 的强大在数据科学中无法被充分探索💻📊

作为数据分析师或科学家,你可能想要在数据中🔎筛选出相关的见解。

✅ 这可以通过 Pandas 中的内置𝗾𝘂𝗲𝗿𝘆()函数来实现。

它基于布尔表达式运行查询,就像你编写自然语言句子一样! 💬

以下是一个示例 🚀

将 Scikit Learn 处理转换为 Pandas 数据框

如果你仔细观察 🧐 Scikit learn 预处理模块,你可能会注意到底层函数返回一个 numpy 数组 🔢。

这可能会使得追踪数据中特征的原始名称变得困难。

如果没有额外的代码行来保留这些特征名称,拥有一个 Pandas 🐼 数据框不是很好吗?

✅ 这可以通过 Scikit Learn 的新版本(1.2)的𝘀𝗲𝘁_𝗼𝘂𝘁𝗽𝘂𝘁 API 来实现。

以下是一个示例 💡

从 Datetime 列中提取周期

天、周、月或季度 🗓,…… 每一个都可以根据任务的需要发挥重要作用。

✅ 使用𝘁𝗼_𝗽𝗲𝗿𝗶𝗼𝗱()函数,你可以从日期列中提取这些相关信息。

以下是一个示例 💡

Python

列表中的元素数量

仍然使用循环 🔁 来确定列表中每个项目出现的频率?

也许有一种更好、更优雅的 Pythonic 🐍 方法!

✅ 你可以使用 Python 中的𝗖𝗼𝘂𝗻𝘁𝗲𝗿类来计算列表中元素的数量。

以下是一个示例 💡

从多个列表中合并元素

你在尝试从多个列表中汇总元素吗?

❌ 停止使用𝗳𝗼𝗿循环 🔁 并采用以下方法。

✅ Python 内置的𝘇𝗶𝗽()函数。

以下是一个示例 💡

从聚合元素中创建多个列表

当尝试从多个列表中汇总元素时,最优雅和 Pythonic 的方式是使用内置的𝘇𝗶𝗽()函数。

现在,如果你想反向操作:从那些聚合元素中创建多个列表会怎样❓

❌ 忘记𝗳𝗼𝗿循环 🔁

✅ 只需将𝘇𝗶𝗽()函数与𝗮𝘀𝘁𝗲𝗿𝗶𝘀𝗸 *结合使用

以下是一个示例 💡

我是列表推导式的忠实粉丝

不要像大多数人那样只用 Python 编程,采取快捷且最有效的方法。

想象一下,你想从一个现有列表中创建一个只包含偶数的列表。最明显的想法是使用“for”循环。但最优雅的方法是使用列表推导式,它更紧凑、更简单,也更容易调试。

以下是一个示例 💡

有了列表推导式,就有了字典推导式

与列表推导式类似,也可以创建字典推导式。它也提供了与列表推导式相同的好处。

假设有一个字典,其中键是索引,值是原始列表中实际的偶数。

以下是一个示例 💡

结论

感谢阅读! 🎉 🍾

希望你觉得这份 Python 和 Pandas 技巧列表对你有帮助!请继续关注,因为内容会每天更新更多技巧。

如果你喜欢阅读我的文章并希望支持我的写作,可以考虑 成为 Medium 会员。每月 $5 的订阅费用可以解锁 Medium 上所有故事的无限制访问。

你想请我喝咖啡 ☕️ 吗?→ 点击这里!

欢迎在 MediumTwitterYouTube 上关注我,或者在 LinkedIn 上打个招呼。讨论 AI、ML、数据科学、NLP 和 MLOps 的话题总是很愉快的!

离开之前,请查看此系列的最后两部分:

Pandas 和 Python 数据科学与数据分析技巧 — 第一部分

Pandas 和 Python 数据科学与数据分析技巧 — 第二部分

Pandas 和 Python 数据科学与数据分析技巧——第四部分

原文:towardsdatascience.com/pandas-python-tricks-for-data-science-data-analysis-part-4-67f61f69d62c

这是我 Pandas 和 Python 技巧系列的第四部分

Zoumana KeitaTowards Data Science Zoumana Keita

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

--

图片由 Andrew Neel 提供,来源于 Unsplash

介绍

几天前,我分享了 一些 Python 和 Pandas 技巧,以帮助数据分析师和数据科学家快速学习他们可能不知道的新有价值的概念。这也是我每天在 LinkedIn 上分享的技巧系列的一部分。

Pandas

更改列的数据类型

错误的数据格式是处理真实世界 🌏 数据时常见的挑战。

例如,你可能有一个以字符串形式存储的数值,如“34”而不是 34。

✅ 使用 astype 函数,你可以轻松地将数据从一种类型转换为另一种类型(例如,从字符串到数值)。

以下是一个示例 💡

检查两个 DataFrame 是否相等

两个具有相同名称的列可能不包含相同的值,而两个具有相同索引的行可能不完全相同。

要知道两个 DataFrame 是否相等,你需要深入检查它们是否具有相同的形状和相同的元素 💡。

这就是 Pandas 𝗲𝗾𝘂𝗮𝗹𝘀() 函数派上用场的地方。

✅ 如果两个 DataFrame 相等,它会返回 True。

❌ 如果它们不相等,它会返回 False。

以下是一个示例 🚀

Python

使你的 Python 输出更具人类可读性

有时候,有必要超越 Python 提供的默认输出,以使其更易于被人类理解 👩🏻‍💼👩🏽‍💼👨🏻‍💼👨🏽‍💼。

✅ 这可以通过使用 humanize 库来实现。

完整的视频教程在这里可以找到更多示例。

将自然语言转换为数值

自然语言🗣️无处不在🌐,即使在我们的 DataFrames 中也是如此。

这本身并不是一件坏事,因为在执行自然语言处理任务时,这种数据是完美的👍💯。

然而,当尝试进行数值计算时,它们的局限性👎🚫变得很明显。

🛠️✅ 要解决这个问题,你可以使用来自 Python 库𝗻𝘂𝗺𝗲𝗿𝗶𝘇𝗲𝗿的𝗻𝘂𝗺𝗲𝗿𝗶𝘇𝗲()函数。

✨ 它将自然语言表达的数字转换为实际的数值。

下面是一个示例🚀

合并多个列表

使用+符号可能是合并🔗列表最常见的方法。

然而,当你需要处理多个列表时,一直输入+符号可能会变得很无聊。

✅ 相反,你可以分别使用来自operatorfunctools模块的addreduce函数。

下面是一个示例🚀

合并不同大小的可迭代对象

如果你一直在使用zip()函数,那么你可能已经意识到这个限制:它不能处理不同大小的可迭代对象,这可能导致信息丢失。

🛠️✅ 你可以使用zip函数的亲戚:来自itertools模块的zip_longest()函数来解决这个问题。

不是忽略剩余的项,而是将它们的值替换为None

这很好,但使用fillvalue参数将None替换为有意义的值会更棒。

下面是一个示例🚀

结论

谢谢阅读!🎉 🍾

我希望你觉得这份 Python 和 Pandas 技巧的列表有用!请继续关注这里,因为内容将会每天更新更多技巧。

如果你喜欢阅读我的故事并希望支持我的写作,可以考虑 成为 Medium 会员。每月 5 美元的承诺可以让你无限访问 Medium 上的故事。

你想请我喝咖啡☕️吗?→ 来吧!

欢迎随时关注我在MediumTwitterYouTube,或者在LinkedIn打个招呼。讨论 AI、ML、数据科学、NLP 和 MLOps 的内容总是很愉快的!

在你离开之前,请查看下面本系列的最后两个部分:

Pandas & Python 数据科学与数据分析技巧 — 第一部分

Pandas 与 Python 数据科学与数据分析技巧——第二部分

Pandas 与 Python 数据科学与数据分析技巧——第三部分

Pandas 和 Python 数据科学与数据分析技巧 — 第五部分

原文:towardsdatascience.com/pandas-python-tricks-for-data-science-data-analysis-part-5-8fc987cd82da

这是我的 Pandas 和 Python 技巧的第五部分

Zoumana KeitaTowards Data Science Zoumana Keita

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

--

图片由 Andrew Neel 提供,来源于 Unsplash

介绍

几天前,我分享了一些 Python 和 Pandas 的技巧,帮助数据分析师和数据科学家快速学习他们可能不了解的新有价值的概念。这也是我每天在LinkedIn上分享的技巧集合的一部分。

Pandas

结合 SQL 语句和 Pandas

我的直觉告诉我,超过 80% 的数据科学家在日常数据科学活动中使用 Pandas。

我相信这也是因为它作为更广泛的 Python 生态系统的一部分所提供的好处,使得它对许多人都可以访问。

𝙎𝙌𝙇 怎么样?

尽管不是每个人在日常生活中都使用它(因为并不是每个公司都有必要的 SQL 数据库?),SQL 的性能是不可否认的。此外,它是人类可读的,这使得即使是非技术人员也能轻松理解。

❓如果我们能找到一种方法来 𝙘𝙤𝙢𝙗𝙞𝙣𝙚 𝙩𝙝𝙚 𝙗𝙚𝙣𝙚𝙛𝙞𝙩𝙨 𝙤𝙛 𝙗𝙤𝙩𝙝 𝙋𝙖𝙣𝙙𝙖𝙨 𝙖𝙣𝙙 𝙎𝙌𝙇 语句呢?

✅ 这就是 𝗽𝗮𝗻𝗱𝗮𝘀𝗾𝗹 显得非常有用的地方 🎉🎉🎉

下面是一个说明 💡 你也可以观看完整的视频

用另一个 DataFrame 更新给定 DataFrame 的数据

在 Pandas 中,有多种方法来替换缺失值 🧩,从简单的插补到更高级的方法。

但是 … 🚨

有时,你只想使用另一个 DataFrame 中的非 NA 值来替换这些值。

✅ 这可以通过 Pandas 内置的更新函数来实现。

它在执行更新之前,会根据索引和列对齐两个数据框。

一般语法 ⚙️ 如下:

𝗳𝗶𝗿𝘀𝘁_𝗱𝗮𝘁𝗮𝗳𝗿𝗮𝗺𝗲.𝘂𝗽𝗱𝗮𝘁𝗲(𝘀𝗲𝗰𝗼𝗻𝗱_𝗱𝗮𝘁𝗮𝗳𝗿𝗮𝗺𝗲)

✨ 从𝗳𝗶𝗿𝘀𝘁_𝗱𝗮𝘁𝗮𝗳𝗿𝗮𝗺𝗲数据框中的缺失值用𝘀𝗲𝗰𝗼𝗻𝗱_𝗱𝗮𝘁𝗮𝗳𝗿𝗮𝗺𝗲中的非缺失值替换

✨ 𝗼𝘃𝗲𝗿𝘄𝗿𝗶𝘁𝗲=𝗧𝗿𝘂𝗲 会用𝘀𝗲𝗰𝗼𝗻𝗱_𝗱𝗮𝘁𝗮𝗳𝗿𝗮𝗺𝗲的数据覆盖𝗳𝗶𝗿𝘀𝘁_𝗱𝗮𝘁𝗮𝗳𝗿𝗮𝗺𝗲的值,这是默认值。如果𝗼𝘃𝗲𝗿𝘄𝗿𝗶𝘁𝗲=𝗙𝗮𝗹𝘀𝗲,则仅替换缺失值。

这里是一个说明 💡

从非结构化数据到结构化数据

数据预处理充满挑战 🔥

想象一下你有这种格式的候选人信息数据:

‘𝗔𝗱𝗷𝗮 𝗞𝗼𝗻𝗲: 𝗵𝗮𝘀 𝗠𝗮𝘀𝘁𝗲𝗿 𝗶𝗻 𝗦𝘁𝗮𝘁𝗶𝘀𝘁𝗶𝗰𝘀 𝗮𝗻𝗱 𝗶𝘀 𝟮𝟯 𝘆𝗲𝗮𝗿𝘀 𝗼𝗹𝗱’

‘𝗙𝗮𝗻𝘁𝗮 𝗧𝗿𝗮𝗼𝗿𝗲: 𝗵𝗮𝘀 𝗽𝗵𝗱 𝗶𝗻 𝗦𝘁𝗮𝘁𝗶𝘀𝘁𝗶𝗰𝘀 𝗮𝗻𝗱 𝗶𝘀 𝟯𝟬 𝘆𝗲𝗮𝗿𝘀 𝗼𝗹𝗱’

然后,你的任务是为每个候选人生成一个包含以下信息的表格以便进一步分析:

✨ 姓名

✨ 学位和研究领域

✨ 年龄

🚨 执行这样的任务可能令人望而生畏 🤯

✅ 这时,Pandas 中的𝘀𝗿.𝗲𝘅𝘁𝗿𝗮𝗰𝘁()函数可以派上用场!

这是一个强大的文本处理函数,用于从非结构化文本数据中提取结构化信息。

下面是一个说明 💡

使用 agg()函数进行多重聚合

如果你想在一个或多个列上执行多个聚合函数,如𝘀𝘂𝗺、𝗮𝘃𝗲𝗿𝗮𝗴𝗲、𝗰𝗼𝘂𝗻𝘁等。

✅ 你可以在一行代码中结合使用𝗴𝗿𝗼𝘂𝗽𝗯𝘆()和𝗮𝗴𝗴()函数。

这里是一个场景 🎬 👇🏽

让我们假设这些学生的数据包含以下信息:

✨ 学生的研究领域

✨ 他们的成绩

✨ 毕业年份和每个学生的年龄。

你被要求计算每个研究领域和年份的以下信息:

→ 学生人数

→ 平均成绩

→ 平均年龄

下面是一个解决场景的图像说明 💡

在两个指定时间之间选择观测值

在处理时间序列数据时,你可能需要选择两个指定时间之间的观测值进行进一步分析。

✅ 这可以通过使用𝗯𝗲𝘁𝘄𝗲𝗲𝗻_𝘁𝗶𝗺𝗲()函数快速实现。

下面是一个说明 💡

Python

检查所有元素是否满足某个条件

for 循环和 if 语句的组合并不总是编写 Python 代码时最优雅的方式。

例如,假设你想检查可迭代对象的所有元素是否满足某个条件。

可能会出现两种情况:

1️⃣ 要么使用 for 循环和 if 语句。

或者

2️⃣ 使用all()内置函数

下面是一个说明 💡

检查任何元素是否满足某个条件

类似于前面的情况,如果你想检查可迭代对象中的至少一个元素是否满足某个条件。

✅ 然后使用any() 内置函数,这比使用for 循环和if 语句更优雅。

插图类似于上面的图片。

避免嵌套的 for 循环

当你的程序变得更大、更复杂时,编写嵌套的𝗳𝗼𝗿 循环几乎是不可避免的。

❌ 这也可能使你的代码难以阅读和维护。

✅ 更好的选择是使用内置的𝗽𝗿𝗼𝗱𝘂𝗰𝘁() 函数。

以下是一个插图 💡

自动处理列表中的索引

想象一下,你需要同时访问列表中的元素及其索引。

一种方法是在 for 循环中手动处理索引。

✅ 相反,你可以使用内置的𝗲𝗻𝘂𝗺𝗲𝗿𝗮𝘁𝗲() 函数。

这样有两个主要好处(我能想到的)。

✨ 首先,它会自动处理索引变量。

✨ 这样可以使代码更具可读性。

以下是一个插图 💡

结论

感谢阅读! 🎉 🍾

希望你觉得这个 Python 和 Pandas 技巧列表对你有帮助!请关注这里,因为内容会每天更新更多技巧。

如果你喜欢阅读我的故事并希望支持我的写作,可以考虑 成为 Medium 会员。每月支付 5 美元,你将解锁 Medium 上无限制的故事访问权限。

想请我喝咖啡 ☕️ 吗? → 请在这里!

欢迎关注我的 MediumTwitterYouTube,或者在 LinkedIn 上打个招呼。讨论 AI、ML、数据科学、NLP 和 MLOps 的内容总是很愉快!

在你离开之前,下面是本系列的最后两个部分:

Pandas 和 Python 数据科学与数据分析技巧 — 第一部分

Pandas 和 Python 数据科学与数据分析技巧 — 第二部分

Pandas 和 Python 数据科学与数据分析技巧 — 第三部分

Pandas 和 Python 数据科学与数据分析技巧 — 第四部分

如何将 Pandas DataFrame 写入 CSV 文件

原文:towardsdatascience.com/pandas-to-csv-aab4ac27c455

利用将 pandas DataFrames 写入 CSV 文件时提供的所有选项

Giorgos MyrianthousTowards Data Science Giorgos Myrianthous

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

--

图片由 Hello I’m Nik 提供,来源于 Unsplash

在使用 pandas 时,一个非常常见的任务是将 DataFrame 导出到 CSV 文件。

在本文中,我们将演示如何将 pandas DataFrame 写入 CSV 文件。此外,我们还将展示如何使用 pandas API 的各种选项来处理缺失值、日期时间格式、压缩,以及处理 CSV 文件中 DataFrame 时可能需要的其他功能。

现在让我们创建一个示例 DataFrame,我们将在本教程中使用并引用它,以演示将 pandas DataFrame 导出为 CSV 文件的几种不同选项。

from datetime import datetime

import pandas as pd

df = pd.DataFrame(
  [
    (1, 'A', True, datetime(2018, 12, 27), 158, None),
    (2, 'B', False, datetime(2019, 11, 15), 10, 76.3),
    (3, 'A', True, None, 210, 78.1),
    (4, 'F', None, datetime(2022, 10, 5), 679, 54.1),
    (5, 'F', False, datetime(2021, 10, 10), 765, 21.1),
    (6, 'F', True, datetime(2021, 7, 23), 132, 4.5),
    (7, 'C', False, datetime(2017, 4, 5), 45, 5.6),
    (8, 'B', False, datetime(2016, 3, 4), 67, 6.7),
    (9, 'A', True, datetime(2019, 4, 5), 786, 78.1),
    (10, 'C', False, datetime(2023, 1, 1), 78, 89.2),
  ],
  columns=['colA', 'colB', 'colC', 'colD', 'colE', 'colF']
)

>>> df
   colA colB   colC       colD  colE  colF
0     1    A   True 2018-12-27   158   NaN
1     2    B  False 2019-11-15    10  76.3
2     3    A   True        NaT   210  78.1
3     4    F   None 2022-10-05   679  54.1
4     5    F  False 2021-10-10   765  21.1
5     6    F   True 2021-07-23   132   4.5
6     7    C  False 2017-04-05    45   5.6
7     8    B  False 2016-03-04    67   6.7
8     9    A   True 2019-04-05   786  78.1
9    10    C  False 2023-01-01    78  89.2

>>> df.dtypes
colA             int64
colB            object
colC            object
colD    datetime64[ns]
colE             int64
colF           float64
dtype: object

将 pandas DataFrame 导出为 CSV 文件

要将 pandas DataFrame 导出为 csv 文件,你只需调用 pandas.DataFrame.to_csv() 函数并提供输出文件名:

df.to_csv('output_df.csv')

输出

,colA,colB,colC,colD,colE,colF
0,1,A,True,2018-12-27,158,
1,2,B,False,2019-11-15,10,76.3
2,3,A,True,,210,78.1
3,4,F,,2022-10-05,679,54.1
4,5,F,False,2021-10-10,765,21.1
5,6,F,True,2021-07-23,132,4.5
6,7,C,False,2017-04-05,45,5.6
7,8,B,False,2016-03-04,67,6.7
8,9,A,True,2019-04-05,786,78.1
9,10,C,False,2023-01-01,78,89.2

更改默认分隔符

如果你想更改默认分隔符( , ),则需要提供 sep 参数。以下示例将 pandas DataFrame 持久化到一个 CSV 文件中,其中列值将使用制表符分隔:

df.to_csv('output_df.csv', sep='\t')

输出

 colA colB colC colD colE colF
0 1 A True 2018-12-27 158 
1 2 B False 2019-11-15 10 76.3
2 3 A True  210 78.1
3 4 F  2022-10-05 679 54.1
4 5 F False 2021-10-10 765 21.1
5 6 F True 2021-07-23 132 4.5
6 7 C False 2017-04-05 45 5.6
7 8 B False 2016-03-04 67 6.7
8 9 A True 2019-04-05 786 78.1
9 10 C False 2023-01-01 78 89.2

排除索引

在前面的示例中,你可能已经注意到,除了列名和数值外,索引也会被保留。如果你想去掉它,只需提供 index=False 参数:

df.to_csv('output_df.csv', index=False)

输出

colA,colB,colC,colD,colE,colF
1,A,True,2018-12-27,158,
2,B,False,2019-11-15,10,76.3
3,A,True,,210,78.1
4,F,,2022-10-05,679,54.1
5,F,False,2021-10-10,765,21.1
6,F,True,2021-07-23,132,4.5
7,C,False,2017-04-05,45,5.6
8,B,False,2016-03-04,67,6.7
9,A,True,2019-04-05,786,78.1
10,C,False,2023-01-01,78,89.2

移除标题

同样,你可以通过传递 header=False 来去掉标题(即列名)。

df.to_csv('output_df.csv', index=False, header=False)

输出

1,A,True,2018-12-27,158,
2,B,False,2019-11-15,10,76.3
3,A,True,,210,78.1
4,F,,2022-10-05,679,54.1
5,F,False,2021-10-10,765,21.1
6,F,True,2021-07-23,132,4.5
7,C,False,2017-04-05,45,5.6
8,B,False,2016-03-04,67,6.7
9,A,True,2019-04-05,786,78.1
10,C,False,2023-01-01,78,89.2

只写出一部分列

有时,你可能只想导出 DataFrame 对象中的一部分列。你可以通过在 columns 参数中指定所需的列来选择要持久化的列:

df.to_csv('output_df.csv', index=False, header=False)

输出

colA,colD,colF
1,2018-12-27,
2,2019-11-15,76.3
3,,78.1
4,2022-10-05,54.1
5,2021-10-10,21.1
6,2021-07-23,4.5
7,2017-04-05,5.6
8,2016-03-04,6.7
9,2019-04-05,78.1
10,2023-01-01,89.2

指定日期时间对象的格式

在我们的示例 DataFrame 中,列 colD 的类型为 datetime,默认情况下,它将以 YYYY-MM-DD 格式导出。你可以在 date_format 参数中指定所需的日期格式。

df.to_csv('output_df.csv', index=False, date_format='%d/%m/%Y')

输出

colA,colB,colC,colD,colE,colF
1,A,True,27/12/2018,158,
2,B,False,15/11/2019,10,76.3
3,A,True,,210,78.1
4,F,,05/10/2022,679,54.1
5,F,False,10/10/2021,765,21.1
6,F,True,23/07/2021,132,4.5
7,C,False,05/04/2017,45,5.6
8,B,False,04/03/2016,67,6.7
9,A,True,05/04/2019,786,78.1
10,C,False,01/01/2023,78,89.2

如果你有兴趣了解更多关于如何处理 Python 中日期时间对象格式的内容,你可以阅读我最近在 Medium 上的一篇文章。

## 如何更改 Pandas 中的日期时间格式

在 Python 和 pandas 中处理日期时间

towardsdatascience.com

更改 NaN 值写入 CSV 文件的方式

默认情况下,to_csv() 函数会将缺失值导出为空字符。na_rep 参数可以用来指定 NaN 值的所需输出格式:

df.to_csv('output_df.csv', index=False, na_rep='N/A')

输出

colA,colB,colC,colD,colE,colF
1,A,True,2018-12-27,158,N/A
2,B,False,2019-11-15,10,76.3
3,A,True,N/A,210,78.1
4,F,N/A,2022-10-05,679,54.1
5,F,False,2021-10-10,765,21.1
6,F,True,2021-07-23,132,4.5
7,C,False,2017-04-05,45,5.6
8,B,False,2016-03-04,67,6.7
9,A,True,2019-04-05,786,78.1
10,C,False,2023-01-01,78,89.2

其他选项

对于大型 DataFrame,你可能需要将输出压缩为 zip 格式。为此,你可以在 compression 参数中指定所需的压缩策略。

df.to_csv('output_df.csv', index=False, compression='gzip')

其他一些压缩策略包括 'zip''gzip''bz2''zstd''tar'

最后,要更改文件格式编码,你可以利用 encoding 参数:

df.to_csv('output_df.csv', index=False, encoding='utf-8')

最后的思考

总结来说,本文展示了将 pandas DataFrame 写入 CSV 文件时的各种选项。从更改默认分隔符、排除索引、移除标题、仅写入部分列、指定日期时间对象的格式,到更改 NaN 值写入 CSV 文件的方式。

pandas API 提供了多种选项来处理将 DataFrame 持久化到 CSV 文件中的不同场景。这使得它成为一个强大的数据处理和分析工具。根据本文提供的知识,你应该能够轻松而准确地将你的 DataFrame 导出到 CSV 文件中。

成为会员 ,阅读 Medium 上的每个故事。你的会员费用直接支持我和你阅读的其他作者。你还将获得对 Medium 上每个故事的全面访问权限。

[## 使用我的推荐链接加入 Medium — Giorgos Myrianthous

作为 Medium 会员,你的部分会员费用会分配给你阅读的作者,并且你可以全面访问每个故事…

gmyrianthous.medium.com](https://gmyrianthous.medium.com/membership?source=post_page-----aab4ac27c455--------------------------------)

你可能也感兴趣的相关文章

## Python 中的代码图示

创建 Python 云系统架构图

towardsdatascience.com ## *args 和 **kwargs 在 Python 中的使用

讨论位置参数和关键字参数之间的区别以及如何在 Python 中使用 *args 和 **kwargs

towardsdatascience.com

Pandas 与 Polars:语法和速度比较

原文:towardsdatascience.com/pandas-vs-polars-a-syntax-and-speed-comparison-5aa54e27497e

理解 Python 库 Pandas 和 Polars 在数据科学中的主要差异

Leonie MonigattiTowards Data Science Leonie Monigatti

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

--

图片由作者提供

这篇文章基于收到的评论进行了修订,评论者包括 Jakob Ullmann Dr. Robert Kübler,和 Thiago Jaworski 。感谢你们的反馈!

Pandas 是数据科学领域的一个重要 Python 库。但它最大的问题是对于大数据集的操作可能很慢。Polars 是一个旨在更快处理数据的 Pandas 替代品。

Polars 是一个旨在更快处理数据的 Pandas 替代品。

这篇文章简要介绍了 Polars Python 包,并在语法和速度方面将其与流行的数据科学库 Pandas 进行比较。

· 什么是 Polars,它为什么比 Pandas 更快?

· 基准测试设置

· 开始使用 Polars

· Pandas 和 Polars 的比较

∘ 读取数据

∘ 选择和过滤数据

∘ 创建新列

∘ 分组和聚合

∘ 缺失数据

· 结论

你可以在我的 Kaggle Notebook 中找到与这篇文章相关的代码。

什么是 Polars,它为什么比 Pandas 更快?

根据 Polars 用户指南[1],其目标是“提供一个闪电般快速的 DataFrame 库,利用机器上的所有可用核心。”

与 Polars 相比,Pandas 并不会在你的计算机核心之间原生并行处理。像 Dask 这样的其他工具建立在 Pandas 之上,以尝试并行化它们。相反,Polars 是从头开始设计用于并行化的。虽然它是用 Rust 编写的,但 Polars 也有一个 Python 包,这使它成为 Pandas 的潜在替代品。

Polars 有两种不同的 API:急切 API 和延迟 API。

急切 执行类似于 Pandas。这意味着代码直接运行,结果会立即返回。

另一方面,延迟 执行直到你需要结果时才会运行。因为它避免了运行不必要的代码,所以延迟执行可能比急切执行更高效。

对于延迟执行,你必须以 .lazy() 方法开始操作。然后,你可以编写你想做的代码。最后,你需要运行 .collect() 方法来显示结果。

df.lazy()
  .with_columns([(pl.col("col") * 10).alias("new_col")])
  #...
  .collect()

如果你不运行 .collect() 方法,操作不会立即执行。相反,你会看到执行图。

Polars 延迟执行的示例执行图(图片由作者提供,来源于 Kaggle

基准测试设置

对于基准测试,我们将借用我之前文章中的基准测试设置,该文章比较了不同的文件格式。

## 如何处理 Python 中的大型数据集

CSV、Pickle、Parquet、Feather 和 HDF5 的比较

[towardsdatascience.com

这个基准测试设置使用了一个虚拟数据集,其中包含每种数据类型的一列。为了减少比较时的时间噪声,这个虚拟数据集包含 4,000,000 行,并且几乎达到 1GB ,正如 [2] 中建议的那样。

基准测试虚拟数据集的开头(图片由作者提供,来源于 Kaggle

接下来,我们将使用 %%timeit -n32 -r4 来计时执行。

开始使用 Polars

要设置 Polars,你只需使用 pip 安装即可。

pip install polars

然后,你可以像使用 Pandas 一样导入 Polars Python 包。

import polars as pl
import pandas as pd

现在,你一切准备就绪!

Pandas 与 Polars 的比较

从表面上看,Pandas 和 Polars(急切 API)在语法方面很相似,因为它们共享主要的构建块:Series 和 DataFrames。

此外,许多 Polars 中的表达式类似于 Pandas 表达式:

# Example expressions that work both with Pandas and Polars
df.head() # Get the first n rows
df.tail() # Get the last n rows
df.unique() # Get unique values of this expression. 

但根据 Polars 用户指南 [1],“如果你的 Polars 代码看起来像 Pandas 代码,它可能会运行,但它可能比应该运行的更慢。”

本节探讨 Polars 包在语法和执行时间上如何与 Pandas 区别:

  • 读取数据

  • 选择与筛选数据

  • 创建新列

  • 分组与聚合

  • 缺失数据

读取数据

在 Polars 中读取 CSV 文件会很熟悉,因为你可以像在 Pandas 中一样使用.read_csv()方法:

# Pandas
pd.read_csv('example.csv')

# Polars
pl.read_csv('example.csv')

下面显示了在 Pandas 和 Polars 中读取样本数据集的执行时间:

Pandas 和 Polars 之间读取时间的对比(图片由作者提供,通过Kaggle

对于我们的样本数据集,读取数据的时间约为Pandas 慢 8 倍于 Polars。

选择与筛选数据

Pandas 和 Polars 之间的第一个主要区别是 Polars 不使用索引[1]。相反,每一行都通过其在 DataFrame 中的整数位置进行索引[1]。

尽管相同的 Pandas 代码可以在 Polars 中运行,但这不是最佳实践。在 Polars 中,你应该使用.select()方法来选择数据。

# Pandas
df[['col1', 'col2']] 

# The above code will run with Polars as well, 
# but the correct way in Polars is:
df.select(pl.col(['col1', 'col2'])) 

下面显示了在 Pandas 和 Polars 中选择数据的执行时间:

Pandas 和 Polars 之间选择时间的对比(图片由作者提供,通过Kaggle

对于我们的样本数据集,使用 Pandas 选择数据的时间约为15 倍长于使用 Polars(约 70.3 µs)。

下方可以看到 Polars 操作与文档建议的语法(使用.select(),左侧)和 Pandas 语法(使用df[['col1', 'col2']],右侧)的对比。出乎意料的是,Pandas 语法比建议的.select()方法快得多。

Polars 与 Pandas 语法(df[['col1', 'col2']])选择时间的对比(图片由作者提供,通过Kaggle

虽然你会在 Pandas 中使用.query()方法来筛选数据,但在 Polars 中你需要使用.filter()方法。

# Pandas
df.query('col1 > 5')

# Polars
df.filter(pl.col('col') > 5)

下面显示了在 Pandas 和 Polars 中筛选数据的执行时间:

Pandas 和 Polars 之间筛选时间的对比(图片由作者提供,通过Kaggle

对于我们的样本数据集,筛选数据框所需的时间在 Pandas 和 Polars 中相似

与 Pandas 不同,Polars 可以并行运行.select().filter()中的操作。

创建新列

在 Polars 中创建新列也与 Pandas 有所不同。在 Polars 中,你需要根据要创建的列数使用.with_column().with_columns()方法。

# Pandas
df_pd["new_col"] = df_pd["col"] * 10

# Polars
df.with_columns([(pl.col("col") * 10).alias("new_col")])

# Polars for multiple columns
# df.with_columns([(pl.col("col") * 10).alias("new_col"), ...])

下面显示了在 Pandas 和 Polars 中创建新列的执行时间:

Pandas 和 Polars 创建新列的时间比较(图像由作者提供,通过 Kaggle

对于我们的示例数据集,使用 Polars 创建新列大约需要比 Pandas 多两倍的时间

分组和聚合

分组和聚合在 Pandas 和 Polars 之间的语法略有不同,但两者都使用 .groupby().agg() 方法。

# Pandas
df_pd.groupby('col1')['col2'].agg('mean')

# Polars
# df.groupby('col1').agg([pl.col('col2').mean()]) # As suggested in Polars docs
df.groupby('col1').agg([pl.mean('col2')]) # Shorter

以下是 Pandas 和 Polars 中分组和聚合数据的执行时间:

Pandas 和 Polars 聚合时间比较(图像由作者提供,通过 Kaggle

对于我们的示例数据集,使用 Pandas 聚合数据的时间约为 Polars 的两倍

缺失数据

Pandas 和 Polars 之间的另一个主要区别是 Pandas 使用 NaN 值来表示缺失值,而 Polars 使用 null [1]。

Pandas 和 Polars 在 DataFrames 中表示缺失值的方式(图像由作者提供)

因此,与 Pandas 的 .fillna() 方法相比,你应该使用 Polars 的 .fill_null() 方法。

# Pandas
df['col2'].fillna(-999)

# Polars
# df_pd.with_column(pl.col('col2').fill_null(pl.lit(-999))) # As suggested in Polars docs
df_pd.with_column(pl.col('col2').fill_null(-999)) # Shorter

结论

现在,Polars 是否比 Pandas 更好?Polars 会取代 Pandas 吗?

Polars 相对于 Pandas 的主要优势是其速度。如果你需要对大数据集进行大量数据处理,你应该尝试一下 Polars。

Polars 相对于 Pandas 的主要优势是其速度。

但正如本文所示,你需要学习新的 Polars 语法才能从 Pandas 切换到 Polars。此外,你已经看到,Polars 代码通常比 Pandas 在执行相同操作时稍长。最后但同样重要的是,Polars 并没有涵盖 Pandas 拥有的全部功能,例如数据探索。

Polars 代码通常比 Pandas 代码稍长。

有关进一步讨论,请参阅评论区,那里有更多经验丰富的 Polars 用户列出了他们的见解。

享受这篇故事吗?

如果你喜欢这篇文章,你可能也会喜欢我的其他 Pandas 文章。

Leonie Monigatti

Leonie Monigatti

Pandas

查看列表3 个故事!Pandas 与 PolarsSQL 连接通常用维恩图表示。此图显示了两个维恩图中的熊猫。一个未格式化的熊猫和三个格式化的熊猫:一个有各种颜色,一个有渐变背景,一个有高光。

免费订阅 以便在我发布新故事时收到通知。

[## 每当 Leonie Monigatti 发布新内容时,你将收到一封邮件。

每当 Leonie Monigatti 发布新内容时,你将收到一封邮件。通过注册,你将创建一个 Medium 账户,如果你还没有的话…

medium.com](https://medium.com/@iamleonie/subscribe?source=post_page-----5aa54e27497e--------------------------------)

在我的 LinkedInTwitter Kaggle 上找到我!

参考文献

[1] Polars (2023):用户指南 pola-rs.github.io/polars-book/user-guide/(访问日期:2023 年 1 月 8 日)

[2] “Stackoverflow”,“feather 和 parquet 之间有什么区别?” stackoverflow.com. stackoverflow.com/questions/48083405/what-are-the-differences-between-feather-and-parquet(访问日期:2022 年 7 月 25 日)

Pandas: 处理你的数据类型!

原文:towardsdatascience.com/pandas-work-on-your-dtypes-20d9d32d2e42

在 pandas 中拥有正确的数据类型是进行干净数据分析的必需条件。下面是原因和方法。

Yoann MocquinTowards Data Science Yoann Mocquin

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

--

为你的 Series 和 DataFrame 选择合适的数据类型非常重要,原因有很多:

  • 内存管理:为特定的系列选择正确的数据类型可以显著减少内存使用,进一步地,这也适用于数据框

  • 解释:其他任何人(无论是人还是计算机)都会根据数据类型对你的数据做出假设:如果一列全是整数却被存储为字符串,它们会将其视为字符串,而不是整数

  • 它强制要求你拥有干净的数据,例如处理缺失值或记录错误的值。这将大大简化后续的数据处理

可能还有很多其他原因,你能列举一些吗?如果可以,请在评论中写出来。

在我系列 pandas 的第一篇文章中,我想回顾 pandas 数据类型的基础知识。

照片由 Chris Curry 提供,来源于 Unsplash

我们首先会回顾 pandas 提供的可用数据类型,然后我将重点介绍 4 种有用的数据类型,它们能满足你 95% 的需求,即数值数据类型、布尔数据类型、字符串数据类型和分类数据类型

这篇文章的最终目标是让你对 pandas 中各种数据类型及其差异更加熟悉。

如果你对 pandas 和时间序列感兴趣,确保查看我关于时间序列傅里叶变换的文章:

  • 回顾卷积如何与傅里叶变换相关以及它的速度

## 时间序列的傅里叶变换:使用 numpy 解释快速卷积

使用傅里叶变换实现 10000 倍更快的卷积

[towardsdatascience.com

  • 通过图像示例深化你对卷积的理解:

[## 傅里叶变换用于时间序列:关于图像卷积和 SciPy]

傅里叶变换卷积同样适用于图像

towardsdatascience.com

  • 了解傅里叶变换如何通过向量可视化方法进行直观理解:

[## 傅里叶变换用于时间序列:绘制复数]

绘制傅里叶变换算法以理解它。

towardsdatascience.com

  • 查看去趋势技术如何显著改善傅里叶变换的输出:

[## 傅里叶变换用于时间序列:去趋势]

去趋势你的时间序列可能是一个改变游戏规则的因素。

towardsdatascience.com

回顾可用的数据类型

让我们花一分钟回顾一下 pandas 提供的数据类型。由于 pandas 基于 numpy,因此它们可以分为两类:

  • 基于 numpy 的数据类型

  • pandas 特定的数据类型

在底层,pandas 将你的数据存储在 numpy 数组中,因此使用 numpy 的数据类型。我们可以将这些视为终极低级数据类型。但为了方便起见,pandas 还暴露了由 pandas 团队特别创建的高级数据类型。

想法是,从你的角度来看,所有数据类型(无论是基于 numpy 还是 pandas 特定的)都被视为对你的 seriesdataframe 有效的数据类型。

基于 numpy 的数据类型如下:

  • **float**:用于存储浮点数(0.0245)

  • **int**:用于存储整数(1,-6)

  • **bool**:用于存储布尔值(True/False)

  • **datetime64[ns]**:用于存储时间线上的一个时刻(日期和时间)

  • **timedelta64[ns]**:用于存储相对持续时间(这补充了 datettime 数据类型)

  • **object**:可以存储任何 Python 对象

实际的 numpy 数据类型完整列表 比这更大,但我们将坚持上述内容。

然后 pandas 进入并暴露了许多新数据类型,包括:

  • **string**:另一种存储字符串的方式

  • **Nullable-int****Nullable-float**numpy**int****float** 更适合处理缺失值

  • **boolean**:比 numpy**bool** 更适合处理缺失值的布尔类型

  • **categorical**:适用于只能有特定值并能处理缺失条目的数据类型

再次,pandas 中定义了其他数据类型, 但我想专注于这四种,因为我发现它们最有用。

数据类型的设置方式

实际上,你的数据类型可以有两种来源:

  • 要么你没有指定数据类型,而 pandas 在创建系列/数据框时做了假设(无论是从加载 csv 还是创建类似 s = pd.Series([1, 2, 3]) 的对象)。这种情况通常会有 50% 到 80% 的成功率,具体取决于输入数据的格式。

  • 要么你指定数据类型,通过明确告知 pandas 每个列/系列使用什么数据类型

对于第一种情况:

  • 优点:更简单更快速

  • 缺点:除非你在之后审查每个数据类型,否则你不知道发生了什么,可能没有最合适的数据类型(例如 object 因为 pandas 没有确定列的内容),甚至可能为某些列选择了非常不合适的数据类型

对于第二种情况:

  • 优点:你准确知道数据发生了什么,确切知道列的数据类型,知道你是如何处理缺失和不良条件值的。换句话说,你的数据已经准备好进行实际处理

  • 缺点:需要更多时间和更多代码

最后,我建议以下做法:首先,让 pandas 推断数据类型并尽力而为。然后审查每一列,并手动设置你认为应该更改的数据类型。

我们将在下一篇文章中讨论如何做所有这些,但现在我想回顾一下可用的数据类型及其存在的原因。

由于大多数情况下我最终只使用所有可用数据类型的一个子集,所以我会专注于这些:

  • 数值数据类型,主要是 float 和 int

  • 布尔数据类型

  • 类似字符串的(包括字符串和对象)数据类型

  • 类别数据类型

类似数值的数据类型

关于数值数据类型,numpy 提供了一个坚实的基础:整数和浮点数数据类型,也能处理 *np.nan*。还要注意,默认使用的数值数据类型是 int64float64。这可能不适合你的数据,考虑到内存使用和给定系列的显式允许值:

pd.Series([1, 2, 3])                     # dtype: int64
pd.Series([1, 2, 3], dtype="int")        # dtype: int64
pd.Series([1, 2, 3], dtype='float')      # dtype: float64

pd.Series([1, 2, np.nan])                # dtype: float64
pd.Series([1, 2, np.nan], dtype="int")   # dtype: float64
pd.Series([1, 2, np.nan], dtype='float') # dtype: float64

pd.Series([1, 2, pd.NA])                 # dtype: object
# fails since pd.NA cannot be converted to an int
# pd.Series([1, 2, pd.NA], dtype="int"))
# fails since pd.NA cannot be converted to an int
# pd.Series([1, 2, pd.NA], dtype='float'))

注意,一旦使用 ***np.nan***,数据类型会被转换为 float。因此,pandas 还提供了能够处理缺失值并保持显式底层数值数据类型的数据类型,如 ***Int64Dtype***具体理由见这里)。

pd.Series([1, 2, 3, np.nan], dtype="Int64")   # dtype: Int64
pd.Series([1, 2, 3, pd.NA], dtype="Int64")    # dtype: Int64
pd.Series([1, 2, 3, np.nan], dtype="Float32") # dtype: Float32
pd.Series([1, 2, 3, pd.NA], dtype="Float32")  # dtype: Float32

这样,我们可以兼得两全:一个明确的底层数值数据类型(int64,float32 等),并使用 ***pd.NA*** 处理缺失值。

布尔类数据类型

基本上,有 2 种数据类型类似于布尔值,即‘bool’ 和 ‘boolean’。

bool’ 对应于标准的 numpy 布尔类型,因此不能包含‘NA’,因为布尔 numpy 数组只能存储 TrueFalse

为了处理“不可用”条目——或“NA”——pandas 提供了数据类型‘boolean’,它可以包含‘NA’。见下面的示例:

pd.Series([True, False], dtype='bool')              # numpy-boolean
pd.Series([True, False, pd.NA], dtype='boolean')    # pandas-boolean
pd.Series([True, False, np.nan], dtype='boolean')   # pandas-boolean

# this cannot work, since numpy does not handle pd.NA
# pd.Series([True, False, pd.NA], dtype='bool') 

# this works, but np.nan is converted to True...
# pd.Series([True, False, np.nan], dtype="bool") # --> dtype: object, not boolean...

# if no dtype is passed, pandas tries to infer an appropriate one
pd.Series([True, False])         # --> dtype: bool
pd.Series([True, False, pd.NA])  # --> dtype: object, not boolean...
pd.Series([True, False, np.nan]) # --> dtype: object, not boolean...

字符串类数据类型

本质上,字符串可以存储在 numpy 数组中,因为我们可以使用 object 数据类型,该数据类型用于处理“任何 Python 对象”。

字符串与大多数其他数据类型不同,因为不能提前知道字符串的长度,因此也就无法预估所需的内存。这同样适用于任何自定义的 Python 对象,不论其简单还是复杂,都需要不同数量的内存。

因此,我们可以在 pandas 中使用‘object’ 数据类型,因为它在 numpy 中是可用的:

pd.Series(['toto', 'titi', 'tata'])                 # --> dtype: object
pd.Series(['toto', 'titi', 'tata'], dtype="object") # --> dtype: object
pd.Series(['toto', 'titi', 'tata'], dtype="str")    # --> dtype: object
pd.Series(['toto', 'titi', np.nan], dtype="str")    # --> dtype: object
pd.Series(['toto', 'titi', pd.NA], dtype="str")     # --> dtype: object

此外,pandas 创建了一种数据类型,使得数据是字符串这一点变得明确:***StringDtype***,可以指定为 ***string***,这更好,因为“显式优于隐式”,并且与 pandas 的其他生态系统接口更好(理由在这里):

pd.Series(['toto', 'titi', 'tata'], dtype="string") # --> dtype: string
pd.Series(['toto', 'titi', np.nan], dtype="string") # --> dtype: string
pd.Series(['toto', 'titi', pd.NA], dtype="string")  # --> dtype: string

请注意,大多数情况下,你需要挑战自己,问自己:我真的需要将这个系列存储为字符串吗?这些字符串是否只是其他数据的表示,比如数值数据或分类数据?如果是这样,你可能需要转换这些系列的数据类型。关注我的下一篇文章,了解如何做到这一点!

万能数据类型:分类数据类型

我建议你试试这个方法:在 pandas 中打开你的一个数据集,一一检查列,并问自己:这个特征是否可以存储为分类数据类型

我敢打赌,你会比预期的更容易接受这个问题的答案。

这种数据类型特别适用于通常存储为类似整数的数字(0,1,2 等)和/或字符串(‘Male’/’Female’,‘Dog’/’Cat’/’Other’)的内容。

这些数据类型在某些情况下可以显著提高速度和内存使用效率。它还再次明确告知他人(包括人类和计算机)这一特定数据表示一个类别,并应被如此处理。

pd.Series([1, 2, 3, 2], dtype="category")           # dtype: category, with 3 int64 possible values [1, 2, 3]
pd.Series([1, 2, 'a', 1], dtype="category")         # dtype: category, with 4 object-like possible values [1, 2, 'a']
pd.Series(["a", "b", "c", "a"], dtype="category")   # dtype: category, with 3 object-like possible values ['a', 'b', 'c']
pd.Series(["M", "F", "M", "F"], dtype="category")   # dtype: category, with 2 object-like possible values ['M', 'F']

总结

其他数据类型在 pandas 中也有实现,但我发现它们的使用频率不如上述数据类型。

所以请记住:

  • 好的数据类型对于你的处理至关重要,尽早审查并设置合适的数据类型会使你未来的工作变得更加轻松。同时,它也可能节省大量内存和处理复杂性

  • 在使用 objectstring 数据类型之前,考虑使用 categorical 数据类型

  • 如果处理缺失值或 NaN,请考虑使用 pandas 数据类型如 boolean,而不是 numpy 的 bool

  • 仅在数据复杂和/或不适合任何其他数据类型时使用 object 数据类型

在下一篇文章中,我们将查看如何检查现有 Series/DataFrame 的数据类型,以及如何将它们转换为其他数据类型。

如果你正在考虑加入 Medium 并获得无限访问我和其他人的文章,请使用此链接快速订阅并成为我推荐的会员:

[## 使用我的推荐链接加入 Medium - Yoann Mocquin

作为 Medium 会员,你的会员费用的一部分将用于你阅读的作者,而你将获得对每篇故事的完全访问…

medium.com

然后订阅以获取未来的文章通知:

[## 订阅以获取我的新文章!

订阅以获取我发布的电子邮件!新发布将包括数据转换、先进的绘图和模拟…

mocquin.medium.com

最后,查看我其他的一些文章:

## 使用 numpy 提高 300 倍的有限差分法分辨率

有限差分法是一种强大的技术来解决复杂问题,而 numpy 使其变得快速!

[towardsdatascience.com ## PCA/LDA/ICA:组件分析算法比较

回顾这些著名算法的概念和差异。

[towardsdatascience.com [## 一样本 t 检验,直观解释

介绍其中一种最著名的统计检验

mocquin.medium.com ## 包装 numpy 数组

容器方法。

[towardsdatascience.com

论文解读——《基于潜在扩散模型的高分辨率图像合成》

原文:towardsdatascience.com/paper-explained-high-resolution-image-synthesis-with-latent-diffusion-models-f372f7636d42

虽然 OpenAI 在自然语言处理领域通过其生成文本模型占据了主导地位,但其图像生成模型 DALL·E 现在面临一个有效的开源竞争者——Stable Diffusion。本文将深入探讨基于 Stable Diffusion 的潜在扩散论文。

Mario LarcherTowards Data Science Mario Larcher

·发表在《数据科学前沿》 ·10 分钟阅读·2023 年 3 月 30 日

--

图 13 的一部分来自于《基于潜在扩散模型的高分辨率图像合成》,生成的提示是“潜在空间的油画”。

引言

在我撰写这篇文章时,OpenAI 的聊天机器人 ChatGPT 正在通过与微软产品的整合继续获得关注,这些产品被超过十亿人使用。尽管谷歌最近推出了自己的 AI 助手 Bard,其他公司也在该领域取得了进展,但 OpenAI 依然处于前沿,没有明显的竞争对手。有人可能认为,OpenAI 的 DALL·E,即图像生成模型,在条件和非条件图像生成领域也会同样主导。然而,实际上是一个开源替代品——Stable Diffusion,在受欢迎度和创新性方面领先

本文深入探讨了 Stable Diffusion 背后的科学论文,旨在提供对这一正在革新图像生成领域的模型的清晰而全面的理解。虽然其他文章提供了该技术的高层次解释,但本文超越了表面,探索了经常被忽视的细节。

之前图像生成方法的局限性

在深入分析科学论文《基于潜在扩散模型的高分辨率图像合成》中的方法之前,了解该工作所解决的关键问题是至关重要的。

多年来,图像生成主要通过四类模型来解决:生成对抗网络(GANs)、变分自编码器(VAEs)、自回归模型(ARMs),以及最近的扩散概率模型(DMs)。

生成对抗网络(GANs)

自 2014 年首次出现以来,生成对抗网络(GANs)一直是图像生成领域的主流方法之一。尽管 GANs 在数据变异性有限的情况下显示出有前景的结果,但它们也存在一些问题。最著名的问题是模式崩溃,在这种情况下,生成器生成的输出范围有限,而不是多样化的图像集。

模式崩溃:这种现象发生在生成器可以交替生成有限数量的输出,来欺骗判别器。通常,GANs 在捕捉完整数据分布方面存在困难。

一般而言,它们的训练****常常不稳定的

变分自编码器(VAEs)

变分自编码器(VAEs)是 GANs 的替代方案,提供了几个优势。它们不会遭遇模式崩溃,并且可以高效地生成高分辨率图像。然而,它们的样本质量****不总是可比于 GANs

自回归模型(ARMs)

自回归模型(ARMs)在密度估计方面表现出色,并在这一领域取得了显著成果。然而,它们计算需求高的架构和顺序采样过程限制了它们仅能生成 低分辨率图像

扩散概率模型(DMs)

扩散模型在密度估计和样本质量方面取得了显著进展,但它们在像素空间中通过向与原始图像大小相同的张量添加或去除噪声来操作,导致了推断速度缓慢计算成本高。例如,即使是相对较小的图像,如尺寸为 512x512 的 RGB 图像,也对应于大约 80 万个值的张量,这使得在训练过程中进行梯度传播和在生成过程中进行迭代的方法时,生成更大图像的计算需求也非常高。

条件机制

基于文本描述或其他图像风格生成图像通常是期望的,但将结果条件化到一个或多个输入 在之前的方法中一直是一个挑战

潜在扩散的高级概述

图 3. 来自 高分辨率图像合成与潜在扩散模型

总结科学论文《高分辨率图像合成与潜在扩散模型》中提出的方法,我们可以将其分解为四个主要步骤

  1. 第一步是使用位于图上左上角的编码器 E 提取 图像的紧凑的表示。与其他方法不同,潜在扩散在编码器定义的潜在空间中工作而不是在像素空间中

  2. 接下来,将高斯噪声添加到图中上中部的图像中,作为从 zzT扩散过程的一部分(假设应用了 T 步噪声添加)。

  3. 然后,将 zT 表示通过位于图中下部中央的U-Net。U-Net 的作用是预测 zT-1,这个过程重复 T-1 次,直到我们得到 z,然后通过解码器 D 将其从潜在空间返回到像素空间

  4. 最后,该方法允许通过映射各种输入模态(如语义图或文本)进行任意条件。这通过首先使用专用编码器 τθ 转换输入 y ,然后 将其映射到 U-Net 的中间层,使用与 Transformer 架构相同的 交叉注意力层 实现。

通过这个总体概述,我们现在可以更详细地深入了解这些步骤。

感知图像压缩

潜在扩散明确将图像压缩阶段分离,以去除高频细节(感知压缩),与生成阶段分开,在生成阶段模型学习数据的语义和概念组成(语义压缩)。

目标函数

为了训练用于图像压缩自编码器,作者采用了 《驯化变压器用于高分辨率图像合成》VQGAN使用的方法。

图 2. 来源于 《驯化变压器用于高分辨率图像合成》

特别是,用于训练自编码模型 (E, D) 的目标函数是:

方程 25. 来源于 《基于潜在扩散模型的高分辨率图像合成》

x^重建图像 D(E(x)),Lrec重建损失xx^ 之间的平方误差),Ladv对抗损失,定义为 log(1 - Dψ(x^)),Dψ 是一个基于补丁的判别器,优化以区分原始图像和重建的“伪造”图像 x^(所以 Dψ(x) 试图对真实图像 x 输出 1,对重建的“伪造”图像 x^ 输出 0),Lreg* 是正则化损失

正则化

作者实验了两种不同的正则化方法

第一种方法涉及低权重的Kullback-Leibler项,类似于标准的 VAE。

Kullback-Leibler (KL) 惩罚:Kullback-Leibler 散度是两种分布之间的一种统计距离。在这种情况下,目标是使潜在变量 z ~ N(Eµ , Eσ²) 的分布接近标准正态分布 N(0, 1)。施加这个约束可以通过使潜在空间更加集中来正则化它,因此,例如,如果 z 接近于 z1 和 z2,则 D(z) 将与 D(z1) 和 D(z2) 有某种共同之处。

在第二种方法中,潜在空间通过 向量量化 层进行正则化。

向量量化 (VQ):VQ 是科学论文 Neural Discrete Representation Learning 中介绍的 VQVAE 以及之前提到的 VQGAN 所使用的方法。从上面的图像可以看出,对于编码器输出 z^ 的每个空间位置,对应的向量(其大小取决于 z^ 的通道数量)会被替换为在可学习的“代码本”中最接近的向量。这会限制解码器在推理过程中可能的输入,只能是代码本向量的组合(即潜在空间的离散化或量化)。

在 VQ 正则化的潜在空间中,z 在量化层之前被提取,并在解码器中吸收量化操作,即它可以被解释为 D 的第一层。

扩散模型

图 2. 来自 Denoising Diffusion Probabilistic Models

由于这篇文章讨论的是潜在扩散模型而不是一般的扩散模型,我将只描述它们最重要的方面。首先,让我们区分 两个过程正向反向

正向过程

正向扩散过程,即图中从右向左的过程,是一个 马尔可夫链,即时间 t 的图像仅依赖于时间 t-1 的图像,而不是所有之前的图像。每一步,xt 都根据以下 转移概率 进行采样:

Eq. 2. 来自 Denoising Diffusion Probabilistic Models

在上面的公式中,βt 定义了一个 方差调度,可以通过将其视为超参数来学习或保持不变。正向处理的一个有趣特性是可以在任意时间步 t 以封闭形式对 xt 进行采样。使用符号

我们有

Eq. 4. 来自 Denoising Diffusion Probabilistic Models

所以,总结一下,在正向过程中,我们可以通过从均值和方差由上述公式定义的高斯分布中采样来获取任意时间 t 的图像。

反向过程

给定正向过程,逆向 过程也遵循高斯分布:

至于方差,作者将其设置为

他们通过实验指出,两者

产生等效的结果。

在看到均值的参数化之前,让我们重新参数化等式 4 的正向过程:

此时我们将均值参数化为

其中ϵθϵ的估计量,来自xt,具体来说,它是时间条件 U-Net的一个变体。

在这一点上,我们已经具备了采样xt-1条件于xt所需的所有元素,考虑到我们知道逆过程描述开始时引入的高斯分布的所有参数。

不进入数学细节,目标(简化版)是:

等式 1 来自于 高分辨率图像合成与潜在扩散模型

其中t 从 {1, . . . , T} 中均匀采样。

潜在表示的生成建模

如前所述,Latent Diffusion 的工作方式类似于前面解释的扩散模型。然而,它的不同之处在于它从通过编码器获得的图像的潜在表示 z 开始(潜在空间),而不是从图像 x像素空间)开始。这一细节大大减少了计算负担,因为潜在空间比像素空间更紧凑。

基于此,将xt替换为扩散模型目标中的zt,我们得到新的目标:

等式 2 来自于 高分辨率图像合成与潜在扩散模型

条件机制

在这项研究之前,对于如何通过输入条件扩散模型的探索是有限的,输入包括类别标签或模糊版本的输入图像。Latent Diffusion 提出的方案具有很高的通用性,涉及 附加信息直接集成到 U-Net 模型的中间层中,使用交叉注意力,这类似于 Transformer 架构。

更具体地说,输入信息(如文本)首先通过一个领域特定的编码器 τθ 转换为中间表示(稍后会提供示例)。然后,将该表示传递通过交叉注意力层并添加到 U-Net 的中间层:

在方程中,ϕi(zt)表示 U-Net 的扁平化中间表示,Ws 是可训练的投影矩阵。尽管论文没有详细说明,代码实现却揭示了交叉注意力层的输出被加到原始 U-Net 层中。这可以在以下代码片段中看到:

x = self.attn2(self.norm2(x), context=context) + x

在这里,attn2 表示交叉注意力层,而上下文指的是τθ(y)。虽然这一过程的完整实现更为复杂,但这是关键的概念元素。欲深入了解这一机制,请参考BasicTransformerBlock模型中使用的模块。

实验

图 5. 来自 High-Resolution Image Synthesis with Latent Diffusion Models

论文进行大量实验探讨了图像生成的各种方法,包括无条件生成、布局到图像合成、空间条件、超分辨率、修补等。为了进一步突出 Latent Diffusion 的两个重要方面,我们将重点关注文本到图像这一广为人知的任务。

首要考虑的关键方面是如何将文本转换为可以传递给交叉注意力层的表示。作者使用BERT-tokenizer并将τθ实现为Transformer以达到这一目标。

第二个重要方面是通过编码器对输入图像进行压缩的程度。作者尝试了不同的下采样因子 f ∈ {1, 2, 4, 8, 16, 32},并得出结论认为 4 和 8 提供了最佳的高质量合成结果条件。上述结果是使用 LDM-8 (KL) 获得的。

结论

Latent Diffusion 及其后续工作的灵感来源于这篇论文,已取得令人惊讶的成果,这些成果曾被认为难以想象。如今,这些模型不再局限于研究实验室,而是被集成到流行产品中,如 Adobe Photoshop。这一发展标志着人工智能领域的一个重要里程碑,并展示了它对我们生活各个方面的潜在影响。

然而,尽管在这一领域取得了显著进展,但仍然存在一些需要解决的挑战。这些挑战包括版权问题,涉及使用图像训练 AI 模型,以及在从互联网爬取大型数据集时出现的固有偏见。然而,尽管存在这些限制,AI 有能力民主化创造力并使个人能够以新颖和引人入胜的方式表达自己,这一潜力不容忽视。

感谢你花时间阅读这篇文章,欢迎留下评论或与我联系,分享你的想法或提出任何问题。要及时了解我最新的文章,你可以在MediumLinkedInTwitter上关注我。

[## 通过我的推荐链接加入 Medium - Mario Namtao Shianti Larcher

阅读 Mario Namtao Shianti Larcher(以及 Medium 上其他成千上万的作者)的每一个故事。你的会员费…

medium.com

论文综述:深度解析 Imagen

原文:towardsdatascience.com/paper-review-a-deep-dive-into-imagen-4e5b4092af13?source=collection_archive---------13-----------------------#2023-02-01

对谷歌令人印象深刻的新型文本到图像生成工具的批判性分析

Jamie McGowanTowards Data Science Jamie McGowan

·

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

--

图片由 Amanda Dalbjörn 提供,来源于 Unsplash

文本到图像合成是多模态学习领域的一个研究方向,最近在这一领域取得了许多进展[1–4]。这篇综述将重点讨论文章《具有深度语言理解的逼真文本到图像扩散模型》[1]。

在这里,作者试图实现最先进的逼真度,并提供对文本到图像合成中更深层次的语言理解的见解。本文的主要输出是一个名为‘Imagen’的模型,它改进了文献中以前的文本到图像合成模型[2–4]。

你可以在这里查看更多关于 Imagen 的信息!

什么是扩散模型?

正如论文标题所示,Imagen 是一个扩散模型。

简而言之,扩散模型是基于生成式 AI的一个例子,其过程是从输入x⁰开始,在每一层t逐渐加入高斯噪声,直到达到纯噪声表示x,其中T是最终层。

这受非平衡热力学的启发,其中状态通过扩散演变为均匀,只要时间足够长。

显示前向和背景扩散过程的图示。图片来自[16]。

扩散模型通过学习逆转这一过程,试图生成原始的x⁰(在这种情况下,x⁰是一张图像)。请参见上面的图示以获得视觉帮助。

该模型的目标是对描述每一步t反向扩散过程的条件概率进行参数化:

描述反向扩散过程的方程。图片由作者创建。

其中xᵗ⁻¹(上一个时间步骤)的表示从高斯分布中绘制,特征是均值μ和协方差σ,模型权重为θ。

由于扩散过程在每一步的去噪过程中保持了图像,这导致数据与预测之间的亲密连接,相比其他非扩散基于文本到图像生成器[4–7]。结果通常是,扩散模型[1–3]的输出更逼真

一旦使用基础扩散模型构建一个 64 × 64 像素的图像,Imagen 会利用另外两个超分辨率扩散模型进行上采样 64 × 64 → 256 × 256 → 1024 × 1024。最终结果因此是一个高分辨率的 1024 × 1024 像素图像,如下图所示!

请注意,这张图片实际上来自 DALL-E 2 [2],因为谷歌对 Imagen 有一些限制!概念是一样的,但请确保查看 Imagen 论文以获取实际的图片。

DALL-E 2 的一个示例输出,文本提示为“时代广场上的滑板上的泰迪熊”。图片来自[2]。

本综述将简要概述以前的工作,然后汇总作者提出的主要贡献和结果,并讨论这些贡献,提供我个人对该工作的意见

之前的工作

虽然实现文本生成图像已经有若干年,但早期的工作困难地将多个文本概念现实地结合到一幅图像中 [5–7]。

基于这些不足,OpenAI 在 [4] 中发布了 DALL-E,它能够将多个看似不相关的概念逐行组合成一幅图像——给定文本提示和图像的起始(第一行像素)。

不到 12 个月后,OpenAI 通过 GLIDE [3] 重新制定了他们的文本到图像合成方法。作者展示了 GLIDE 在各种设置中因照片真实感标题相似性而被人类评估者偏好,从而确立了扩散模型在文本到图像生成中的主导地位。

最后,在 [2] 中,DALL-E 2 通过基于从文本提示中找到的图像嵌入生成图像,从而进一步改进了 GLIDE。

注意到在这个时间段内还有其他进展,但我主要集中在三大主要贡献上,这些贡献构成了 Imagen [1] 的基础。

主要贡献

架构

类似于 GLIDE [3] 和 DALL-E 2 [2],Imagen 是一个扩散模型,在其架构上似乎与 GLIDE 非常接近(即,它将文本嵌入作为输入,并从噪声中生成图像)。然而,Imagen 的一个关键区别是,文本嵌入来自大型现成的语言模型(LMs)。

[1]的主要发现之一是,结合在仅文本数据上训练的大型冻结语言模型,在获取文本到图像合成的文本表示时非常有用。

此外,作者探讨了文本编码器的扩展,发现扩展语言模型的规模显著提高了结果,远超扩展扩散模型的规模。[1]中图 4a 的最左侧图表总结了这一结果,显示 T5-XXL LM [8]生成了更高质量的图像(↓ FID 分数)和更好的标题兼容性(↑ CLIP 分数)。

作者还引入了一种新技术,以避免在无分类器引导的图像生成中出现饱和像素。

分类器引导 被引入以通过预训练模型提高生成图像的质量,该模型在测试时推动输出更忠实于文本输入 [9]。

无分类器引导 [10] 通过从输入噪声生成两个样本(输出),一个有文本条件,一个没有,避免了对预训练模型的需求。

通过在特征空间中找到这两个样本之间的差异,可以找出文本在图像生成中的效果。通过调整这种文本效应,图像生成可以引导至更好的图像-文本对齐(通过不同强度的引导权重 w)。

到目前为止,这些都不算新鲜,然而这种指导的一个问题是,当w很大时,像素可能会饱和,图像保真度会因为更好的图像-文本对齐而受到损害。因此,作者引入了动态阈值,通过在每次采样步骤xᵗ时确定的不同量将饱和像素从[-1, 1]推向内部(因此是动态的)。作者声称,在图像生成中,高度指导下逼真度图像-文本对齐有显著提升。

最后,在模型架构方面,作者提出了一种新的 U-Net [11]变体,该变体比以前的版本更简单更高效。根据我的了解,关键修改是去除了超分辨率模型中的自注意力层,这些模型来自于[11–12]的 U-Net 模型。

DrawBench

对未来文本到图像合成研究的另一个重要贡献是DrawBench的发布。

DrawBench 是一个‘挑战性’评估基准文本提示的集合,探测模型处理复杂概念如组合性、基数和空间关系的能力。

本次发布的理念是提供一个包含一些非常奇怪的文本提示的评估基准,以确保图像之前从未存在过。因此,理论上,这应该将模型推向其想象力和能力的极限,以生成复杂图像。

定量结果

图片由Maxim Hopman拍摄,来源于Unsplash

作者在[1]中展示的定量结果对比了不同模型在 COCO [15]和 DrawBench 文本提示上的表现。

作者发现,与 DALL-E 2 [2]、GLIDE [3]、Latent Diffusion [14]和 CLIP-guided VQ-GAN [13]模型进行成对比较时,DrawBench 上的人工评估结果对 Imagen 表现出强烈的偏好(见[1]中的图 3)。这些结果作为标题对齐保真度的衡量标准。

同时,COCO 验证集上的结果似乎在不同模型之间没有表现出太大差异——这可能是作者没有对此过多停留的原因。

然而,对 COCO 数据集的一个有趣观察是,Imagen 在生成逼真的人物方面有有限的能力——尽管作者没有提供任何关于 Imagen 生成人物效果差的定性例子。

讨论

在引言中,[1]的作者提到以下声明:

[Imagen 交付]了前所未有的逼真度和深层次的语言理解能力,在文本到图像合成中。

在调查这一声明的前半部分时,作者展示了 Imagen 与 DALL-E 2 生成图像之间的几种定性比较。他们还提供了人类评估实验的结果,询问人们从单个文本提示或标题中选择最写实的图像。

即使在考虑任何结果之前,作者立即在他们的分析中引入了人类评估实验中固有的主观性。因此,文献[1]中显示的结果必须谨慎考虑,并持有健康的怀疑态度

一个来自 DALL-E 2 的示例输出,文本提示为“狗在湖边的绿色田野里玩耍的高质量照片”。图像来自[2]。

为了提供这些结果的背景,作者选择了一些示例比较展示给人类评分者,并将其包含在附录中(一定要查看这些示例——作为动机,我在上面添加了一个来自 DALL-E 2 的示例)。

然而,即使有这些例子,我发现很难对哪张图像更具优先性做出明确判断。考虑到上图中显示的复制例子,我个人认为一些 DALL-E 2 生成的图像比 Imagen 的图像更具照片写实性,这展示了在收集这类结果时主观性的问题。

作者选择询问人类评分者“哪张图像更具照片写实性?”以及每个“标题是否准确描述了图像?”在评估过程中。然而,评估这些指标的不连续性质让我感到相当担忧。

例如,如果我们有两张卡通图像(这些图像可能不太现实),并且评分者被要求选择其中一张。就照片写实性指标而言,所选择的图像将具有与从另一批次中选择的更真实的图像相同的现实水平(即不是卡通图像)。

显然,图像批次的标题与可以实现的照片写实性之间存在一定的相互作用。因此,探索根据难度加权某些文本提示,将会有趣,以尝试创建一个连续的度量标准,该标准可以更可靠地汇总

同样,在标题对齐的情况下,评分者在三个类别选项中选择标题是否与生成的图像对齐(有些)。这些实验结果试图支持上述引言的后半部分(声称深层次的语言理解)。

确实,对于标题对齐,可以说在文本提示中的关系和概念是否在图像生成中被捕捉,有一个更明确的答案(即比起照片写实性主观性更低)。

然而,我再次认为应该使用更连续的度量标准,例如 1-10 的对齐评分。根据上述讨论,所有字幕的不同难度等级也会表现为较低的字幕对齐度。有趣的是,探讨让评估者在评价过程中评估字幕或文本提示的难度,并帮助标准化数据集和度量标准。

图片来源:Mitchell LuoUnsplash

随着这一研究领域的发展和生成图像变得越来越令人印象深刻和富有创意,这种评估方法自然会变得不那么可靠(当然,这是一个好问题)。因此,希望看到作者讨论询问评估者更多具体问题的潜力,以评估模型所捕捉的创造力、构图性、数量关系和空间关系。

如果生成的两张图像同样令人印象深刻,向评估者提出更多具体的问题可以帮助区分模型在这一非常高的水平上的表现。

作为一个例子,文本到图像生成的一个应用是帮助生成****插图。因此,评估解释文本提示时的创造力和变化性是有充分理由的。

在之前展示的例子中,DALL-E 2 [2] 在解释‘眼镜’时比 Imagen 有更多的方式,因此可以认为 DALL-E 2 是更具创意的模型?

从这个角度来看,论文的一个主要批评是所选择的度量标准过于侧重于 Imagen 的优势。不同应用中表现良好的模型的最佳指示(度量)将依赖于具体应用(即没有免费的午餐!)。

因此,我很想听听作者对如何严格评估这些模型的不仅仅是忠实度和字幕对齐度的看法。

图片来源:Dragos GontariuUnsplash

根据[1],DrawBench 的发布被认为是对文本到图像研究领域的必要贡献,因为它提供了一套全面的挑战性文本提示场景。

虽然我同意大部分观点,但根据围绕这一论点的讨论,我尚未被说服这是一项全面的基准。如果深入探索 DrawBench,会发现仅包括约 200 个文本提示/字幕,分布在 11 个类别中,这在初看时显得相当少。

这种担忧在与 COCO 数据集 [15] 比较时愈发加深,该数据集包括 330K 张图像,每张图像有 5 个标题,涵盖了更广泛的类别。个人认为,作者应该讨论他们声称这是一个全面集的推理

此外,随着文本到图像合成的快速进展,我认为 DrawBench 是该领域的动态 目标。因此,希望能够看到讨论调整或添加这些标题的可能性。

由于 DrawBench 是与 Imagen 一起呈现的,因此有理由对选择 200 个提示以获得对 Imagen 有利的结果是否存在某种担忧

再次比较 Imagen 和基准模型在 COCO [15] 和 DrawBench 上评估的结果,COCO 的结果似乎在模型之间更接近,而 DrawBench 的结果则显示 Imagen 似乎远高于所有基准模型。

这可能是因为 DrawBench 是一个自然难度较大的提示集,而 Imagen 能够处理这些提示是由于其预训练的 LM,或者 DrawBench 可能对 Imagen 的优势存在偏见?确实,作者在构建 DrawBench 时承认存在一些偏见,未包含任何图像生成中的人物。

最后,当模型(或代码)未发布时,批评研究很容易,尤其是当存在巨大的财务获利潜力时(作者并未提及这一点)。

然而,我认为其中的社会和伦理推理是该论文的最佳 贡献之一,并且突显了在发布强大的开源 AI 软件时需要某种形式的治理

Михаил СекацкийUnsplash 上的照片

从更广泛的意义上讲,生成模型自然地反映了社会,这对社会研究小组甚至政府可能是有益的,前提是他们能够访问未经筛选的模型版本。

结论

总结来说,作者在文本到图像合成领域的快速增长中做出了重大贡献。

虽然出于社会和伦理原因目前不对公众开放,但最终的模型‘Imagen’结合了诸如使用现成文本编码器、动态阈值设定和更高效的 U-Net 架构用于基础和超分辨率层等新技术。

我个人很享受阅读这篇论文,我认为所做的贡献是令人兴奋和有趣的领域发展。

图片由 Arnold Francisca 提供,来自 Unsplash

然而,尽管结果令人印象深刻,但深入研究后我发现,作者往往过度宣传 Imagen 和 DrawBench。因此,观察(也许在未来的出版物中,或从获得 Imagen 访问权限的研究人员中)更广泛的文本到图像生成模型评估将会很有趣。

参考文献

[1] — Chitwan Saharia 等人,照片级文本到图像生成

深度语言理解的扩散模型,arXiv:2205.11487,(2022)。

[2] — Aditya Ramesh 等人,基于 CLIP 潜变量的层次化文本条件图像生成,arXiv:2204.06125,(2022)。

[3] — Alex Nichol 等人,Glide:朝向照片级图像生成与编辑的文本引导扩散模型,arXiv:2112.10741,(2021)。

[4] — Aditya Ramesh 等人,零样本文本到图像生成,ICML,8821 — 8831,PMLR,(2021)。

[5] — Han Zhang 等人,Stackgan++:利用堆叠生成对抗网络进行逼真图像合成,《IEEE 计算机视觉与模式识别学报》,41(8):1947–1962,(2018)。

[6] — Tero Karras 等人,分析与改善 stylegan 图像质量,发表于《IEEE/CVF 计算机视觉与模式识别会议论文集》,8110 — 8119,(2020)。

[7] — Mark Chen 等人,基于像素的生成预训练,ICML,1691 — 1703,PMLR,(2020)。

[8] — Colin Raffel 等人,探索统一文本到文本转换器的迁移学习极限,arXiv:1910.10683,(2019)。

[9] — Prafulla Dhariwal 和 Alexander Nichol,扩散模型在图像合成中的表现优于 GANs,NeurIPS,34,(2021)。

[10] — Jonathan Ho 和 Tim Salimans,无分类器扩散引导,发表于 NeurIPS 2021 深度生成模型与下游应用研讨会,(2021)。

[11] — Alex Nichol 和 Prafulla Dhariwal,改进的去噪扩散概率模型,ICML,8162–8171,PMLR,(2021)。

[12] — Chitwan Saharia 等人,Palette:图像到图像的扩散模型,arXiv:2111.05826,(2021)。

[13] — Katherine Crowson 等人,VQGAN-CLIP:利用自然语言指导的开放域图像生成与编辑,arXiv:2204.08583,(2022)。

[14] — Robin Rombach 等人,高分辨率图像

基于潜在扩散模型的合成,arXiv:2112.10752,(2021)。

[15] — Tsung-Yi Lin 等人,Microsoft COCO:上下文中的常见对象,发表于《欧洲计算机视觉会议论文集》,740 — 755,Springer,(2014)。

[16] — Calvin Luo,理解扩散模型:一个统一视角,arXiv:2208.11970,(2022)。

在 Pandas 中并行化 Python:使用 Spark 的并发选项

原文:towardsdatascience.com/parallelising-python-on-spark-options-for-concurrency-with-pandas-7ca553b9f265?source=collection_archive---------8-----------------------#2023-11-18

当使用 Pandas 时,利用 Spark 的优势

Matt CollinsTowards Data ScienceMatt Collins

·

关注 发表在Towards Data Science ·8 分钟阅读·Nov 18, 2023

--

照片由Florian SteciukUnsplash上拍摄。

在我之前的工作中,我花了一些时间在一个内部项目上,为我们的托管服务客户预测未来的磁盘存储空间使用情况。每个磁盘都有自己的使用模式,这意味着我们需要为每个磁盘建立单独的机器学习模型,这些模型利用历史数据逐盘预测未来的使用情况。尽管执行预测和选择正确的算法本身就是一个挑战,但在大规模执行时也有其自身的问题。

为了利用更先进的基础设施,我们可以考虑摆脱顺序预测,通过并行化工作负载来加快预测操作。本文旨在比较 Pandas UDF 和 ‘concurrent.futures’ 模块,这两种并发处理的方法,并确定每种方法的适用场景。

挑战

Pandas 是 Python 中用于处理数据集的入口包。在处理 DataFrames 时,我们能够对数据进行分析和评估数据质量,执行探索性数据分析,构建数据的描述性可视化,并预测未来趋势。

尽管这是一个很好的工具,但 Python 的单线程特性意味着在处理更大的数据集时,或者在需要对多个数据子集执行相同分析时,它的扩展性较差。

在大数据领域,我们期望采取更复杂的方法,因为我们还需关注扩展性以保持良好的性能。Spark 以及其他语言使我们能够利用分布式处理来帮助处理更大更复杂的数据结构。

在深入这个具体例子之前,我们可以总结一些数据处理并发需求的用例:

  • 对多个数据文件应用统一转换

  • 为多个数据子集预测未来值

  • 调整机器学习模型的超参数并选择最有效的配置

当我们需要执行如上所述的工作负载时,在 Python 和 Pandas 中最简单的方法是顺序处理这些数据。对于我们的例子,我们将对每个磁盘依次运行上述流程。

数据

在我们的例子中,我们有成千上万的磁盘数据,显示了随时间记录的空闲空间,我们希望预测每个磁盘的未来空闲空间值。

为了更清晰地描述情况,我提供了一个包含 1,000 个磁盘的 csv 文件,每个磁盘有一个月的以 GB 为单位的空闲空间历史数据。这一数据量足以让我们看到不同方法在大规模预测中的影响。

作者提供的图像:示例 DataFrame

对于像这样的时间序列问题,我们希望使用历史数据来预测未来的趋势,并且我们希望了解对于每个磁盘哪种机器学习(ML)算法将最合适。像 AutoML 这样的工具在寻找一个数据集的适当模型时非常有效,但对于我们这里的 1,000 个数据集来说则有些过于复杂了。

在这种情况下,我们将限制要比较的算法数量为两个,并查看每个磁盘使用均方根误差(RMSE)作为验证指标时最适合使用的模型。关于 RMSE 的更多信息可以在 这里 找到。这些算法包括:

  • 线性回归

  • Fbprophet(将数据拟合到更复杂的线条)

  • Facebook 的时间序列预测模型。

  • 为了更复杂的季节性超参数预测而建立。

现在,我们已经准备好所有组件,如果我们想预测单个磁盘的未来剩余空间。操作步骤如下:

作者提供的图像:数据生命周期

现在,我们希望将此扩展,对多个磁盘执行此流程,例如我们的示例中的 1,000 个。

作为我们评估的一部分,我们将比较在不同规模下计算不同算法的 RMSE 值的性能。因此,我创建了一个包含前 100 个磁盘子集的子集来模拟这一点。

这应该对不同大小的数据集上的性能提供一些有趣的见解,执行各种复杂度的操作。

引入并发性

Python 以单线程著称,因此不会在某一时间点利用所有可用的计算资源。

因此,我看到了三个选项:

  1. 实现一个 for 循环,顺序计算预测,采用单线程方法。

  2. 使用 Python 的 futures 模块同时运行多个进程。

  3. 使用 Pandas UDFs(用户定义的函数)在 PySpark 中利用分布式计算,同时保持我们的 Pandas 语法和兼容的包。

我希望在不同的环境条件下进行相当深入的比较,因此使用了一个单节点的 Databricks 集群和另一个具有 4 个工作节点的 Databricks 集群,以利用 Spark 进行我们的 Pandas UDF 方法。

我们将采用以下方法来评估线性回归和 fbprophet 模型在每个磁盘上的适用性:

  • 将数据分割为训练集和测试集

  • 使用训练集作为输入,在测试集日期上进行预测

  • 将预测值与测试集中的实际值进行比较,以获取均方根误差(RMSE)分数

我们将在输出中返回两件事情:一个带有预测的修改后的 DataFrame,为我们提供绘制和比较预测与实际值的额外好处,以及一个包含每个磁盘和算法的 RMSE 分数的 DataFrame。

执行此操作的函数如下所示:

我们将比较上述概述的三种方法。我们有几个不同的场景,因此可以填写一个表格,列出我们收集结果的标准:

使用以下组合:

方法

  • 顺序

  • futures

  • Pandas UDFs

算法

  • 线性回归

  • Fbprophet

  • 组合(每个磁盘的两种算法)——最有效的比较方式。

集群模式

  • 单节点集群

  • 标准集群,4 个工作节点

磁盘数量

  • 100

  • 1000

结果以这种格式呈现在本博客的附录中,如果您想进一步查看。

方法

方法 1:顺序

方法 2:concurrent.futures

使用此模块有两个选项:并行内存密集型操作(使用 ThreadPoolExecutor)或 CPU 密集型操作(ProcessPoolExecutor)。关于这一点的描述性解释可以在以下博客中找到。由于我们将处理 CPU 密集型问题,ProcessPoolExecutor 适合我们要实现的目标。

方法 3:Pandas UDFs

现在我们将切换到 Spark 并利用分布式计算来提高效率。由于我们使用的是 Databricks,大部分 Spark 配置已经为我们完成,但我们对数据的通用处理仍有一些调整。

首先,将数据导入到 PySpark DataFrame:

我们将使用 Pandas 分组映射 UDF(PandasUDFType.GROUPED_MAP),因为我们想传入一个 DataFrame 并返回一个 DataFrame。自 Apache Spark 3.0 起,我们不再需要显式声明此装饰器!

由于 PySpark 中的 DataFrame 结构,我们需要将 fbprophet、回归和 RMSE 函数拆分为 Pandas UDFs,但不需要对代码进行大规模重构即可实现。

我们可以使用applyInPandas来生成我们的结果。

注意:上述示例仅演示了使用线性回归的过程以提高可读性。有关完整演示,请参阅完整的笔记本

结果解读

作者提供的图片:每种建议方法的预测算法执行时间

我们为不同的方法和不同的环境设置创建了图表,然后按算法和磁盘数量对数据进行分组,以便于比较。

请注意,表格结果见本帖的附录。

我已经总结了这些发现的亮点,如下所示:

  • 正如预期的那样,预测 1,000 个磁盘与 100 个磁盘相比是(通常)一个更耗时的过程。

  • 顺序方法通常是最慢的,无法有效利用底层资源。

  • 对于较小、更简单的任务来说,Pandas UDFs 效率很低。数据转换的开销更高 — 并行化有助于弥补这一点。

  • 顺序和concurrent.futures方法都没有意识到 Databricks 提供的聚类 —— 无法利用额外的计算资源。

结语

当然,背景在确定最成功的方法时起着重要作用,但考虑到 Databricks 和 Spark 通常用于大数据问题,我们可以看到在处理这里今天看到的那些更大更复杂的数据集时使用 Pandas UDFs 的好处。

当处理较小数据集时,使用 Spark 环境可以通过concurrent.futures模块在较小(且成本更低!)计算配置上以极高的效率完成,因此在设计解决方案时请记住这一点。

如果您熟悉 Python 和 Pandas,那么从在初学者教程中看到的顺序 for 循环方法转变,对于任何一种方法都不应该是一种费力的学习曲线。

我们在本文中没有进行深入调查,因为我发现当前版本存在差异和不兼容性,但最近的pyspark.pandas模块在未来肯定会更常见,并且是一个值得关注的方法。该 API(以及由 Databricks 开发的 Koalas,现已退役)利用了 Pandas 的熟悉性以及 Spark 的底层优势。

为了演示我们试图实现的效果,我们只查看了每个磁盘生成的 RMSE 值,而没有实际预测未来的时间序列值。我们在这里设置的框架可以以同样的方式应用于此,具有确定算法使用情况的逻辑,以及在可能情况下使用确定的算法预测未来值,其中包括评估指标(以及其他逻辑,例如磁盘的物理限制)是否适合每种情况。

总是可以在我的GitHub找到这个笔记本。

附录

最初发布于 https://blog.coeo.com,为了本次转载做了调整

LLMs 的参数高效微调 (PEFT):全面介绍

原文:towardsdatascience.com/parameter-efficient-fine-tuning-peft-for-llms-a-comprehensive-introduction-e52d03117f95

对 Hugging Face、Google 的 Vertex AI 和最终 OpenAI 使用的 PEFT 方法进行概念性调查

肖恩·史密斯Towards Data Science 肖恩·史密斯

·发表在 Towards Data Science ·19 分钟阅读·2023 年 8 月 22 日

--

图片由 DALL-E 创建。《大碗岛上的一个星期天下午》,但每个人都是类人。

大型语言模型(LLMs)的名字就意味着它们很大。这些模型通常具有从 70 亿到 700 亿个参数。以全精度加载一个 700 亿参数的模型需要 280 GB 的 GPU 内存!要训练这个模型,你需要在数百万或数十亿个文档上更新数十亿个标记。更新这些参数所需的计算量很大。这些模型的自监督训练成本很高,公司最高花费可达 1 亿美元

对于我们来说,适应我们的数据以适配这些模型引起了显著的兴趣。面对有限的数据集(相比之下)和缺乏计算能力,我们如何在成本的一小部分下创建可以超越主要竞争者的模型?

这就是参数高效微调(PEFT)研究领域的作用所在。通过各种技术,我们可以增强这些模型的小部分,使它们更适合我们要完成的任务。

阅读本文后,你将概念性地掌握 Hugging Face 中应用的每种 PEFT 技术,并能够区分它们之间的差异。在这篇文章之前,我发现的一个最有帮助的概述来自 Reddit 评论。此外,lightning.ai(pytorch lightning 的创作者)还有另一篇 杰出文章。另外,还有一篇基于大量此文的全面调查,由 Liali 等人撰写 [2]。在我的文章中,我旨在解决在审阅这些材料时识别出的空白点。在撰写本文时,本文作为 Hugging Face 库中所有 PEFT 方法的概念指南。读者的目标是以对该领域的基本理解来接触其他 PEFT 技术的研究文献。

自我反思的时刻:是时候进行微调了吗?

我写了一篇关于 微调 LLMs 的考虑因素 的文章,以及通过 In-Context Learning 实现类似性能的方法。自那时起,Llama 2 已经发布,开源 LLM 世界有了很大进步。这里是一些我可以分享的扩展思考,超出了那篇文章的范围。

微调本质上对你的组织是危险的。最近的一篇论文显示,LLMs 至少可以记住 1% 的训练数据 [1]。如果你有潜在的数据重复,这个 1% 的比例会更高。如果你的微调 LLMs 将被非内部用户使用,问问自己是否可以将你要训练的数据提供给他们。用户可能对你的模型进行恶意操作,比如 提示注入攻击。我在 LinkedIn 上发布了关于这些安全风险的帖子,作为一个快速概述。如果你不能提供你的数据,使用 ICL 的动态观察选择是你的最佳选择之一(详情请参见我的另一篇 文章)。

你还必须优先创建高质量的数据标签用于你的学习任务。如果组织对高质量数据的承诺不足,特别是对于你项目的微调支持,我建议考虑其他方法。模型依赖于高质量的标注输入。如果你的利益相关者对人工标注者的承诺不足,你可能会让所有相关方感到失望。

谁在使用 PEFT?

PEFT 被大多数提供语言模型微调能力的供应商使用。如果供应商尚未使用这些技术,我保证他们有计划使用这些技术。本文涵盖了在撰写时可用的Hugging Face PEFT中的所有技术。Lialin 等人的调查[2]在 Google 关于在 Vertex AI 上调整基础模型的介绍视频中被引用。虽然 Vertex AI 更像是一个黑箱,但我听说过销售推介中提到的适配器、提示微调,以及最近的 LoRa。虽然不清楚他们具体使用了什么,但我们在这里讨论的技术就是支撑这些功能的核心。

OpenAI 确实提供了微调服务,但至今尚未实现任何 PEFT 方法。这一点基于 OpenAI 几个月前要求删除的一篇博客文章。该文章详细说明了 OpenAI 不使用 Adapters 或 LoRa 来使微调更加计算友好。OpenAI 尚未宣布这些功能的实施,因此可以安全地假设这些功能目前还不可用。它已被纳入 OpenAI 的路线图中,并且由于微调比正常模型使用更具盈利性,我怀疑它将在不久的将来推出。

快速 Transformer 复习

我假设这篇文章的读者对 Transformer 架构已经有所了解。你不需要对自注意力或任何组件的细节有深入的了解,但你应该至少浏览过 Vaswani 等人的论文,并且可能对注释版 Transformer有过一些了解(在我看来,这是学习 Transformer 的最佳资源)。

我将包括 Transformer 块的伪代码。如果你对 Transformer 了解不多,只需知道它的核心功能如下:

def self_attention(x):
    k = x @ W_k
    q = x @ W_q
    v = x @ W_v
    return softmax(q @ k.T) @ v

def transformer_block(x):
    """ Pseudo code by author based on [2] """
    residual = x
    x = self_attention(x)
    x = layer_norm(x + residual)
    residual = x
    x = FFN(x)
    x = layer_norm(x + residual)
    return x

那些伪代码中的所有功能都如 Vaswani 等人所述。FFN 是前馈网络,对于我们的目的来说是 2 层。许多随后的 PEFT 技术对 Transformer 块或自注意力进行修改,因此在我们进行指南时,我会参考并修改这段伪代码。

PEFT 方法概览

来自 [2] 的方法和类别概述。

我们将通过查看上图中的更广泛类别来逐一介绍每种技术。我们将涵盖的类别包括加性方法、适配器、软提示、重新参数化,以及一种混合方法,它是重新参数化和选择性(而不是稀疏 LoRa)的组合。

加性方法

加性方法可能是最容易理解的。加性方法的目标是添加一组额外的参数或网络层以增强模型。在微调数据时,您只更新这些新添加参数的权重。这使得训练计算上更容易,并且适应较小的数据集(起始时大约 100-500,最高接近 100,000)。

方法:适配器

适配器既是一种方法也是一种类别。这项技术在 Houlsby 等人 [4] 中被介绍。适配器的目标是在 Transformer 子层之后添加小的全连接网络,并学习这些参数。我遵循 [2] 中的定义,并将适配器严格定义为仅向网络添加全连接层。

Houlsby 等人提出了一种对 transformer 块进行简单更新的方法。他们在两个地方添加了全连接层,如下所示的伪代码。

def transformer_block_adapter(x):
    """Pseudo code from [2] """
    residual = x
    x = self_attention(x)
    x = FFN(x)  # adapter
    x = layer_norm(x + residual)
    residual = x
    x = FFN(x)
    x = FFN(x)  # adapter
    x = layer_norm(x + residual)
    return x

方法:(IA)³

通过抑制和放大内部激活的注入适配器,或 (IA)³ 是一种非常有趣的加性方法(添加参数),它通过一些新参数扩展了 transformer 块。它由 Liu 等人 [5] 于 2022 年提出。尽管名字如此,但这不是适配器方法,因为它并没有严格地在 transformer 块的子层之后添加全连接层。

让我们考虑正常 transformer 中的缩放点积注意力:

来自 Vaswani 等人 [3] 的缩放点积注意力

由于我们正在使用加性方法,我们寻求向此网络添加参数。我们希望维度尽可能小。(IA)³ 提出了以下新向量以添加到注意力机制中:

在 (IA)³ 中的缩放点积注意力来自 [5]。这里我们在普通方程中添加了两个列向量 l_k 和 l_v,它们分别与键和值项进行 Hadamard 乘积。

我们只是添加了列向量 l_k 和 l_v,并进行 Hadamard 乘积,即列向量与矩阵之间的乘法(将列向量与矩阵的所有列相乘)。

我们还引入了另一个可学习的列向量 l_{ff},它被添加到前馈层中,如下所示:

(IA)³ 中的前馈更新改编自 [5]。我们可以看到,我们将列向量 l_{ff} 添加到网络中,并与 transformer 块 FFN 的第一层输出进行 Hadamard 乘积。函数 gamma 是激活函数 GELU

在这个示例中,gamma 是应用于权重和输入之间乘积的激活函数。这是 (IA)³ 的一些伪代码:

def self_attention_ia3(x):
    k = x @ W_k
    q = x @ W_q
    v = x @ W_v

    k = l_k @ k  # ia3
    v = l_v @ v  # ia3

    return softmax(q @ k.T) @ v

def transformer_block_ia3(x):
    """Pseudo code from [2]"""
    residual = x
    x = self_attention_ia3(x)
    x = layer_norm(x + residual)
    residual = x
    x = x @ W_1  # normal transformer
    x = l_ff * gelu(x)  # ia3
    x = x @ W_2
    x = layer_norm(x + residual)
    return x

软提示

要理解软提示,我们首先讨论硬提示,一个大多数读者可能都熟悉的概念,即使不是通过名字。在硬提示中,我们会将代表当前任务的数据集整理在一起。当有人通过提出问题与网络互动时,他们可能会用不同的方式表达问题。使用硬提示,这个过程涉及策划一个数据集,涵盖语言模型可以框定特定任务的各种方式。

软提示(Soft-prompting)是一种试图避免创建数据集的技术。在硬提示中,我们是在离散表示中创建数据(选择单词)。在软提示中,我们寻求对输入到模型中的文本进行连续表示。这意味着你需要为正在训练的示例提供一个静态提示。

根据技术的不同,信息添加到网络中的方法也各不相同。核心思想是基础模型并不优化文本本身,而是优化提示文本的连续表示(即某种类型的可学习张量)。这可以是某种形式的嵌入或对该嵌入应用的某种变换。这些技术将在我们继续深入探讨时详细说明。

方法:Prompt-Tuning

图像来自 Lester 等人 [11] 的 prompt-tuning。这表明,在 prompt-tuning 中,我们将软提示和输入文本的表示(嵌入)连接到预训练模型中。这样做使我们能够通过一个可学习的张量来优化软提示的表示。

Prompt tuning 是 Lester 等人 [11] 提出的技术,属于软提示的范畴。使用软提示时,我们的目标是向基础模型添加更具体于当前任务的信息。通过 prompt tuning,我们通过创建一组提示令牌的参数并将其注入到网络的开头来实现这一点。

为了找到软提示的表示,我们为训练过程中使用的静态提示创建了一组单独的嵌入。我们将输出嵌入与序列嵌入进行连接。我们使用这些新信息传递到语言模型中。创建这种双重信息使我们能够学习软提示的参数化,而无需为同一任务创建多个提示。

def prompt_tuning(seq_tokens, prompt_tokens):
    """ Pseudo code from [2]. """
    x = seq_embedding(seq_tokens)
    soft_prompt = prompt_embedding(prompt_tokens)
    model_input = concat([soft_prompt, x], dim=seq)
    return model(model_input)

通过这种方法进行微调有许多丰富的好处。这组新参数可以非常小,大约是基础模型可调参数的 0.01%。这创造了一个使用相同基础模型的任务特定模型集合的机会,这大大减少了模型的内存需求。有关更多信息,请查看我在LinkedIn上分享的帖子以及[3]中的集成部分。

方法:前缀调整

前缀调整是另一种与提示调整非常相似的软提示技术。在提示调整中,我们创建了一组单独的参数,将输入传递给这些参数,并将输出附加到模型中输入文本的连续表示上。在前缀调整中,我们还从一组单独的提示标记中找到一个连续的表示,这些标记被输入到基础模型中。

前缀调整和提示调整的区别在于,前缀调整的表示会传递到变换器的所有层,而提示调整仅与嵌入层级联。此外,我们还为前缀调整学习了额外的参数,形式为一个全连接网络。训练后,FFN 被丢弃,我们只使用软提示作为输入。

def transformer_block_prefix_tuning(x, soft_prompt):
    """ Pseudo code from [2] """
    soft_prompt = FFN(soft_prompt)
    model_input = concat([soft_prompt, x], dim=seq)
    return model(model_input)

方法:P-Tuning

刘等人[6]提供的 P-Tuning 图像。该图展示了提示嵌入的创建过程,整个提示编码与输入嵌入进行级联。提示编码器包括一个嵌入层、LSTM,然后是一些全连接层。

P-Tuning 是刘等人[6]提出的另一种软提示方法,与提示调整(prompt tuning)和前缀调整(prefix tuning)不同。通俗地说,我们可以将 P-Tuning 视为提示调整,但使用 LSTM 对提示进行编码。

P-Tuning 旨在解决作者注意到的两个问题。第一个是传递给模型的词嵌入的离散性。作者认为,如果嵌入是随机初始化的,然后通过随机梯度下降优化,模型可能会陷入局部最小值。第二个是词嵌入的关联性。在提示调整和前缀调整的参数化中,软提示在技术上是相互独立的。作者希望找到一种方法,使提示标记相互依赖。

作者们提出,提示是一个函数,它接受一个上下文 x 和一个目标 y,并将其组织成一个模板 T。作者提供了示例序列“The capital of Britain is [MASK]”。这里的提示是“The capital of … is …”,上下文是“Britain”,目标是[MASK]。我们可以使用这种表述创建两个标记序列:上下文之前的所有内容以及上下文之后和目标之前的所有内容。我们可以学习这些附加信息的表示,并将其缩减为连续输出,输入到语言模型中。

为了以这种方式嵌入提示,我们使用一个由 LSTM 组成的小型网络,输入到一个两层的 FFN 中。我们传递提示标记,包括上下文之前的和目标之前的标记。

def p_tuning(seq_tokens, prompt_tokens):
    """Pseudo code for p-tuning created by Author."""
    h = prompt_embedding(prompt_tokens)
    h = LSMT(h, bidirectional=True)
    h = FFN(h)

    x = seq_embedding(seq_tokens)
    model_input = concat([h, x], dim=seq)

    return model(model_input)

方法:LLaMA-Adapter

从 Zhang 等人[7]处获得的 LLaMA-Adapter 图像。我们可以看到零初始化的注意力被用于适配提示,并且这些是唯一被微调的内容。

LLaMA adapter 是 Zhang 等人[7]引入的一种软提示技术,它将更高效的前缀学习版本应用于 Llama 模型。

LLaMA-Adapter 与 Prefix Tuning 有一些关键差异。它们引入了适配提示,这些是附加到变换器层输入的软提示。这些适配提示被插入到 N 个变换器层的最上层 L 处。

作者们还引入了零初始化注意力。通过加法方法,我们引入了一组在权重上有一些随机初始化的新参数。由于这种随机噪声的加入,可能会经历不稳定的微调,这可能在早期阶段导致较大的损失值。为了解决这个问题,作者们引入了一个初始化为 0 的门控因子,该因子与自注意力机制相乘。门控因子和自注意力的乘积称为零初始化注意力。门控值在训练步骤中自适应调整,以创建更平滑的网络参数更新。

 def transformer_block_llama_adapter(x, soft_prompt, gating_factor):
    """LLaMA-Adapter pseudo code created by Author"""
    residual = x

    adaption_prompt = concat([soft_prompt, x], dim=seq)
    adaption_prompt = self_attention(adaption_prompt) * gating_factor  # zero-init attention

    x = self_attention(x)
    x = adaption_prompt * x
    x = layer_norm(x + residual)
    residual = x
    x = FFN(x)
    x = layer_norm(x + residual)

    return x

基于重参数化的方法

基于重参数化的方法专注于找到与基础模型中相同的权重矩阵的低维表示。Hu 等人[8]首次展示了微调与低维表示之间的联系。作者在模型的完整参数和较低维度表示之间建立了联系。根据任务,作者能够用大约 0.0002%的可训练参数实现完全微调模型的 90%的结果。

方法:LoRa

从 Hu & Shen 等人[9]处获取的图像。我们可以看到预训练权重以及额外的矩阵 A 和 B。A 被正常初始化,而 B 被初始化为 0。我们仅训练 A 和 B。

微调中最流行的技术之一是基于重参数化的方法,称为低秩适应(LoRa)[9]。LoRa 通过学习一个单独的矩阵来更新权重矩阵,该矩阵表示来自优化的更新。它们进一步创建两个较小维度的权重矩阵来表示这种差异。通过创建较小维度的权重矩阵,我们需要学习的参数更少。

为了训练 LoRa,我们使用梯度下降的基本思想,在这一过程中,我们对一组参数进行渐进调整,使我们更接近目标(损失函数)。在 LoRa 中,我们选择将所有更新隔离到一个单独的矩阵中。这个矩阵,我们称之为 Delta W,表示我们在微调过程中学到的所有参数更新。

让我们将 W_0 的维度设为 dxk(d 行和 k 列)。我们希望更新它的参数,以使其与我们的新目标对齐。你可以通过ΔW 来表示这个参数的更新,它的维度也是 dxk。我们可以使用下面的方程来建模我们的更新规则。

来自[9]的 W_0 的更新规则。我们将 W_0 的变化隔离到 DeltaW 中。然后,我们将 DeltaW 表示为 A 和 B 两个较小维度矩阵的乘积。这样,我们学习的参数更少,但仍然更新 W,这使得微调的计算更加简单。

现在让我们改变更新规则,使得ΔW 由矩阵乘法 AB 建模。我们将矩阵 A 指定为 dxr 的维度,矩阵 B 指定为 rxk 的维度。如果你对矩阵乘法有所了解,你会发现 AB 与 W_0 的维度相同,因此这些矩阵的加法是有效的。这里是 AB 比 DeltaW 更好的原因:矩阵 A 仅有 dxr 的维度,而矩阵 B 有 rxk 的维度。如果我们将 r 设为一个非常小的数值(r=8 是一个典型值),那么 A 和 B 中的参数数量会比ΔW 小得多。如果我们只学习 A 和 B 的参数,我们将学习d*k-d*r-r*k更少的参数。实际上,这使我们只需学习原始网络参数的 0.1–0.5%。

我刚刚描述的过程就是 LoRa 工作原理的精髓。我们不是通过额外的训练步骤来优化矩阵 W,而是通过两个参数远少的新矩阵 A 和 B 来改变矩阵ΔW。这种结果帮助我们优化了更少的参数,使得训练更为高效。

通常我们将这个更新规则应用于变换器块中的自注意力的关键和数值矩阵。我们还添加了一个缩放因子,设为 1/r,以调整更新所提供的信息量。请参见下面的伪代码。

def lora_linear(x, W):
    scale = 1 / r  # r is rank
    h = x @ W
    h += x @ W_a @ W_b  # W_a,W_b determined based on W
    return scale * h

def self_attention_lora(x):
    """ Pseudo code from Lialin et al. [2]."""

    k = lora_linear(x, W_k)
    q = x @ W_q
    v = lora_linear(x, W_v)
    return softmax(q @ k.T) @ v

选择性方法

使用选择性方法,我们选择一些参数进行更新,而不更新其他参数。这些方法的问题在于我们创建了一个稀疏的参数矩阵。稀疏矩阵操作在现代 GPU 上不太被支持,并带来计算挑战。有关稀疏矩阵为何会产生计算挑战的更多信息,请查看[10]。

在选择性方法中,还有一些技术专注于修剪不成功的向量或操控模型偏差。这些方法在训练模型时也会增加额外的复杂性。一般来说,这些方法的实现更具挑战性,因为它们的计算操作比其他操作更为昂贵。

方法:AdaLoRa

这是一种混合方法,结合了重新参数化和选择性方法的思想。Zhang 等人 [12] 通过研究 LoRa 开发了 AdaLoRa,并提出了这样一个问题:“我们如何根据模块的重要性自适应地分配参数预算,以提高参数高效微调的性能?”这意味着“我们如何优先考虑那些能带来更好性能的参数,而不是平等对待所有参数?”

与我们在 LoRa 中使用的两个矩阵 A 和 B 不同,AdaLoRa 使用奇异值分解 (SVD)的近似来将向量空间的维度降低到三个矩阵:P(左奇异向量)、Lambda(奇异值)和 Q(右奇异向量)。使用这三个矩阵,我们可以重建向量空间 Delta 的近似值,即 P * Lambda * Q。使用 SVD 的好处在于奇异值表示了在这个低维空间中向量的重要性。本文的贡献在于应用一些高效实现,以使用与 SVD 相关的方法来考虑哪些权重应当被优化。

在 LoRa 中,我们看到可以用两个矩阵 A 和 B 来近似 delta W。在这里,我们可以用新的近似值 P * Lambda * Q 来替代 A 和 B。由于 Lambda 仅在对角线(奇异值)上有值,我们将其存储为列向量。我们选择矩阵 P(d x r)、Lambda(r x r)和 Q(r x k)的维度,以匹配权重矩阵 W(d x k)的维度。

另一个新颖的结果是使用一种特殊的重标定技术来确定可以被修剪掉的 SVD 元素。基本上,该技术考虑一组三元组(SVD 的每个条目),并确定它们对低维表示的重要性。他们通过使用一个将奇异值与左/右奇异向量相关联的函数来实现这一点。这些函数随后经过一个灵敏度函数,该函数结合了梯度权重乘积的指数移动平均(伪重要性)以及另一个称为不确定性量化的函数,后者也在当前步骤和前一步骤之间进行指数平均。

在修剪 SVD 元素时,通过删除最不重要的三元组,低维度的秩(矩阵的 r 项)会被迭代地改变。他们通过一个全局预算调度器来实现这一点,该调度器在训练步骤中逐渐降低秩 r。预算初始化为目标预算的 1.5 倍,并在经过 t 次预热步骤后按立方方式减少到目标预算。

从概念上讲,这是一个难以理解的方法。如果您有技术背景,我鼓励您阅读论文以了解该方法的内部工作原理。如果您记住这是一个高效的 SVD 实现应用于 LoRa,并结合了修剪不重要的奇异向量,那么在概念层面上,这可能是安全的。

 def adalora_linear(x, W, curr_sv):
    scale = alpha / r  # r is rank
    h = x @ W

    # p, lamda, and q are related to the W matrix
    # curr_sv marks which singular vectors we are currently optimizing. 
    h += x @ p[curr_sv] @ lamda[curr_sv] @ q[curr_sv]
    return scale * h

def self_attention_lora(x):
    """
    AdaLoRa pseudo code created by author. 
    This only shows the difference in the self_attention block. 
    Does not include code for pruning techniques.
    """
    k = adalora_linear(x, W_k)
    q = x @ W_q
    v = adalora_linear(x, W_v)

    return softmax(q @ k.T) @ v

方法比较

为了在一个地方比较所有的方法,我创建了下面的表格来展示它们的可训练参数数量(这些都是网络的附加参数)、方法类型以及对方法的非正式总结。非正式总结是我如何用一句话向一个从未听说过该方法的大学生描述该方法。

表格改编自 Lialin 等人[2]。作者贡献了 P-Tuning、LLaMA-Adapter 和 AdaLoRa,并在非正式总结栏中进行总结。非正式总结是我如何用一句话向大学生描述这篇论文的方式。

这是您需要的唯一概念性指南吗?

我认为这是您需要的唯一概念性指南,因为阅读后您将理解 PEFT 技术的基础知识。如果您注意到,所有技术都扩展了其他技术的思想。在这次介绍之后,您已经理解了足够的基础知识,您可以自己探索研究论文。然而,如果您最终需要另一个概念性指南来理解这些概念,请在文章的评论中分享,以便其他读者可以找到这些资源!

现在是开始的时候了!

在这次概念性回顾之后,您处于一个很好的起点,可以开始实验这些方法来训练自己的模型。来自Hugging Face有很多很棒的实现指南。如果您希望采取更少的动手方法,您可以使用 Google 的 Vertex AI 模型或使用 OpenAI 的微调服务。

感谢您阅读这篇文章!如果您有其他问题或有不清楚的地方,请留言,我会回复您。如果您想看到更多类似的文章,请在 Medium LinkedIn上关注我。

如果您发现本文中的技术错误,请尽快告知我!我努力确保我发布的信息尽可能准确,但没有人是完美的。

参考文献:

[1] Nicholas Carlini, Daphne Ippolito, Matthew Jagielski, Katherine Lee, Florian Tramer, & Chiyuan Zhang. (2023). 量化神经语言模型的记忆。

[2] Vladislav Lialin, Vijeta Deshpande, & Anna Rumshisky. (2023). 缩小规模以扩大规模:参数高效微调指南。

[3] Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Lukasz Kaiser, & Illia Polosukhin (2017). 关注即是你所需要的。CoRR, abs/1706.03762

[4] Neil Houlsby, Andrei Giurgiu, Stanislaw Jastrzebski, Bruna Morrone, Quentin de Laroussilhe, Andrea Gesmundo, Mona Attariyan, & Sylvain Gelly (2019). NLP 的参数高效迁移学习。CoRR, abs/1902.00751

[5] Haokun Liu, Derek Tam, Mohammed Muqeeth, Jay Mohta, Tenghao Huang, Mohit Bansal, & Colin Raffel. (2022). 少样本参数高效微调优于上下文学习且更经济。

[6] Xiao Liu, Yanan Zheng, Zhengxiao Du, Ming Ding, Yujie Qian, Zhilin Yang, & Jie Tang (2021). GPT 也能理解。CoRR, abs/2103.10385

[7] Renrui Zhang, Jiaming Han, Chris Liu, Peng Gao, Aojun Zhou, Xiangfei Hu, Shilin Yan, Pan Lu, Hongsheng Li, & Yu Qiao. (2023). LLaMA-Adapter:零初始化注意力的语言模型高效微调。

[8] Armen Aghajanyan, Luke Zettlemoyer, & Sonal Gupta (2020). 内在维度解释了语言模型微调的有效性。CoRR, abs/2012.13255

[9] Edward J. Hu, Yelong Shen, Phillip Wallis, Zeyuan Allen-Zhu, Yuanzhi Li, Shean Wang, & Weizhu Chen (2021). LoRA:大语言模型的低秩适应。CoRR, abs/2106.09685

[10] Trevor Gale, Matei Zaharia, Cliff Young, & Erich Elsen. (2020). 深度学习的稀疏 GPU 内核。

[11] Brian Lester, Rami Al-Rfou, & Noah Constant. (2021). 参数高效提示调优的规模效应。

[12] Qingru Zhang, Minshuo Chen, Alexander Bukharin, Pengcheng He, Yu Cheng, Weizhu Chen, & Tuo Zhao. (2023). 参数高效微调的自适应预算分配。

帕累托、幂律与胖尾

原文:towardsdatascience.com/pareto-power-laws-and-fat-tails-0355a187ee6a

他们在统计学中不会教你的东西

Shaw TalebiTowards Data Science Shaw Talebi

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

--

黑天鹅。图片来自 Canva。

统计学是数据科学和分析的基础。它为我们提供了一个强大的工具箱,以客观地回答复杂的问题。然而,当应用于特定类型的数据——幂律时,许多我们喜爱的统计工具变得无用。

在这篇文章中,我将提供一个对初学者友好的 Power Laws 指南,并描述使用传统统计方法分析这些规律时遇到的 3 个主要问题。

目录

  1. 背景高斯分布、帕累托的 80–20 规则、幂律,以及体重与财富之间的差异。

  2. 统计学 101 的 3 个问题你需要(很多)更多的数据。

  3. 胖尾避免争议并量化高斯分布与帕累托分布之间的差距。

称量你的咖啡师

自然界中的许多量往往围绕典型值聚集。例如,如果你坐在一个(繁忙的)咖啡店里,测量所有进进出出的咖啡师和顾客的体重,你会(最终)观察到如下图所示的模式。

示例高斯分布。 技术说明:在测量成年人的体重时,每个性别会出现类似高斯分布的模式。图片由作者提供。

这个图是高斯分布的一个例子,你可能在统计学 101 或商业统计中遇到过。高斯分布的美在于,我们可以用一个数字——均值——捕捉到大部分核心信息(例如咖啡师的体重)。

更进一步,我们可以通过标准差方差等测量方式来获取数据的分散程度

这些在入门统计学中每个人都会学到的概念给了我们一个强大的数据分析方法。然而,并非所有我们关心的量都具有围绕典型值聚集的这种定性特征。

帕累托原理(80–20 法则)

你可能听说过所谓的“80–20 法则”,其标语是“80%的销售额来自 20%的客户”。然而,这一概念并非源于销售和市场营销,而是起源于维尔弗雷多·帕累托对意大利土地所有权的研究(约 1890 年)[1]。

帕累托观察到意大利约 80%的土地由 20%的人口拥有。这一简单观察表明,统计特性与我们所熟悉和喜爱的高斯分布有很大不同

也就是说,“80–20 法则”是帕累托分布的结果。下图对此进行了说明。

帕累托分布,其中 20%的人口占据 80%的体量。图像由作者提供。

高斯分布与帕累托分布之间的关键区别在于帕累托分布没有“典型值”,我们无法用来有效总结分布。

换句话说,虽然知道一个意大利男人的平均体重(约 175 磅)能让你对下一次罗马之行有个大致的预期,但知道一个意大利城市的平均人口(约 7500)却是无用的

功率法则分布

帕累托分布是功率法则这一更广泛分布类别的一部分。我们可以如下定义功率法则[2]。

功率法则分布类别的定义[3]。图像由作者提供。

其中PDF()表示随机变量X的概率密度函数。xX的特定值。L(x)是一个在[x_min,* ∞]范围内缓慢变化的正函数。x_min是功率法则有效的最小值(即PDF(x) = 0 当 x<x_min)[2]。α是一个数字(通常在 2 到 3 之间)。

各种α值的功率法则分布示例。注意:α = 1.16 大致意味着 80–20 法则。图像由作者提供。

正如上面的图示所示,功率法则与高斯分布在定性上非常不同。这形成了一种高斯型与帕累托型分布之间的二分法。换句话说,高斯和功率法则分布为定性地分类现实世界中的事物提供了概念性基准。

中庸斯坦与极端斯坦

作者Nassim Nicholas Taleb通过他称之为“中庸斯坦”与“极端斯坦”的两个类别描述了高斯型和帕累托型事物之间的二分法。

Mediocristan是类似高斯分布的事物的土地。其公民的一个基本特性是没有单个观察值会显著影响总体统计数据[3]。例如,假设你在罗马旅行时称重每个斗兽场的游客,并计算平均体重。如果你加入地球上最重的意大利人,这个平均值将几乎不变(+0.5%)。

在这种概念景观的另一端是Extremistan,在这里我们看到相反的统计特性。即,在 Extremistan 中,单个观察值可以(并且通常会)驱动总体统计数据。考虑到在斗兽场的相同游客,但不是测量他们的体重,而是询问他们的净资产并计算平均值。与之前不同的是,如果我们将世界上最富有的意大利人乔瓦尼·费雷罗(巧克力+榛子家族)加入样本,这个平均值将会剧烈变化(+2500%)。

为了更好地理解这些类别,请参考下面图像中列出的例子。

来自 Mediocristan 和 Extremistan 的项目,分别[3]。图像由作者提供。

正如你所见,Extremistan 中的帕累托式居民并不是一个小的或微不足道的集合。事实上,我们关心的许多事物并非像我们在 STAT 101 中研究的高斯曲线。

虽然这看起来可能过于技术性和教学性,但使用我们熟悉的统计技术和直觉来分析来自 Extremistan 的数据存在重大局限性,甚至在某些情况下可能有显著的风险。

STAT 101 思维的 3 个问题

正如我们在罗马斗兽场看到的,来自 Mediocristan(例如体重)的数据具有与 Extremistan(例如财富)相反的特性。

使用 STAT 101 技术分析幂律(即来自 Extremistan 的数据)的最大问题之一是像均值、标准差、方差、相关性等数量几乎没有实际意义

这一切都源于一个核心问题——数据不足

在统计学中,我们学习到大数法则,它表明如果我们取 N 个随机样本,样本均值将趋近于N真实均值。这对任何分布(具有有限均值)都是正确的:高斯分布、幂律分布、均匀分布,等等。

然而,事实证明这种渐近行为在某些分布中比其他分布发生得更缓慢(例如,幂律分布比高斯分布更慢)。在实践中,由于我们(必然地)拥有有限的数据集,这可能会造成问题。在这里,我突出三个这样的问题。

问题 1:均值是无意义的(以及许多其他指标)

每当我们想比较两组值(例如四月与五月的销售额、洛杉矶与纽约的交通事故、对照组与治疗组的患者结果)时,我们通常会计算均值。这为我们提供了一种将多个值压缩成一个代表性数字的直观方法。

对于遵循良好高斯分布的数据,这种方法效果极佳,因为在小样本量(N=~10)中可以准确估计均值。然而,当处理遵循幂律分布的数据时,这种方法会失效

我们可以通过比较高斯分布和幂律分布的样本均值随样本量的增加,来观察这一点,下面的图表展示了 N=100、N=1,000 和 N=10,000 的情况。幂律和高斯样本均值分别用橙色和蓝色表示。

三种不同样本量的样本均值收敛情况。图片由作者提供。

如我们所见,幂律样本均值比高斯分布更为不稳定(且有偏)。即使样本量增加到 N=100,000,幂律的准确性仍然远远低于高斯分布在 N=100 时的表现。这在下面的图表中有所体现。

尽管均值在 N=1,000,000 时略有稳定,但与高斯分布相比仍然有显著偏差。图片由作者提供。

这种不稳定的行为不仅限于均值。它还适用于许多常用的统计量。下图展示了中位数、标准差、方差、最小值、最大值、1st 和 99th 百分位数、峰度和熵的类似收敛图。

三种样本量的其他指标收敛图。自上而下:中位数、标准差、方差、最小值、最大值、1st 和 99th 百分位数、峰度和熵。图片由作者提供。

如我们所见,某些指标相较于其他指标更稳定。例如,中位数、最小值和百分位数相对稳定。而标准差、方差、最大值、峰度和熵似乎无法稳定在一个数字上。

在这一组中,我想特别指出最大值,因为这一量度可能在小样本中看似收敛, 但随着 N 的增大,它可能会跳跃一个数量级(如 N=10,000 图中所见)。这特别危险,因为它可能导致虚假的可预测性和安全感。

为了将这一点与现实世界联系起来,如果基础数据是例如,疫情死亡人数,那么过去 100 年的最大疫情将比过去 1,000 年的最大疫情小 10 倍。

例如,过去 100 年中最致命的疫情是西班牙流感(约 5000 万人死亡)[4],因此如果疫情死亡人数遵循幂律分布,我们可以预计在接下来的 1,000 年中,会有一个疫情造成 5 亿人丧生(抱歉,这个例子有些黑暗)。

这突出了 Extremistan 数据的一个关键属性,即罕见事件驱动了整体统计数据

然而,这并不仅仅是统计指标的问题。罕见事件的严重性也影响了我们有效进行预测的能力。

问题 2:回归分析无效

回归归结为基于过去数据进行预测。然而,正如我们在问题 1 中看到的,当处理幂律分布时,我们可能没有足够的数据来准确捕捉真实的统计数据。

当使用幂律分布的变量进行回归时,如果α <= 2,这一点会变得更加严重。这是因为α <= 2 意味着该分布具有无限方差,这破坏了流行回归方法(例如最小二乘回归)的一个关键假设。

然而,在实际工作中,计算出的方差永远不会是无限的(数据必然是有限的)。这引发了类似于问题 1 的问题:结果可能看起来稳定,但随着数据的增加不会持续

换句话说,你的 R²在开发模型时可能看起来很棒,但随着样本量的增加,R²很快会恶化并接近实际值 R²=0。

我们可以通过一个(人工)示例来观察这一点。假设我们有两个变量 X 和 Y,它们线性相关(即 Y = mX + b),其中 X 服从正态分布,并且有一个遵循幂律分布的加性噪声项。当我们在小样本量(N=100)中进行回归时,拟合效果看起来非常好。

涉及具有加性噪声且遵循幂律分布的预测变量的线性回归拟合,样本量较小(N=100)。图片由作者提供。

然而,当我们收集更多数据(N=100,000,000)时,R²会正确地下降到实际值(即 R²=0)。

随着样本量的增加,R²接近实际值(即 R²=0)。图片由作者提供。

问题 3: 回报概率偏离

在这一点上,你可能会想,“Shaw... 有什么大不了的?如果我的模型不能预测一些罕见事件,那又怎么样?它大多数时候是正确的。

我同意你的观点。当处理来自极端国家的数据时,大多数数据不在尾部,因此大多数时间容易是正确的。然而,在预测结果和做出决策时,概率只是部分故事。

故事的另一半是回报。换句话说,不仅仅是关于你有多频繁地正确(错误),还包括当你正确(错误)时会发生什么

例如,如果你被提供一种每日多种维生素,它 99.9%的时间效果很好,但有 0.1%的时间可能致命,你可能会选择另一种品牌(或更健康的食物)。

单纯依赖概率来做决策在处理幂律和“80-20 规则”时尤其有害。考虑以下商业示例。

假设我们有一家软件公司,提供 3 种产品:1)有广告的免费版,2)高级版,3)企业版,其中每种产品的客户和收入分布见下表。

每种产品的客户和收入分布。图片由作者提供。

公司希望推出一个更新,将处理时间提高 50%。作为一家前沿的数据驱动技术公司,他们调查了活跃用户,发现95%的客户更喜欢更新后的软件。掌握了这些数据,公司批准了软件更新。

然而,六周后,公司陷入了混乱,因为收入下降了 50%。

事实证明,更新后,3 位客户取消了服务,因为更新移除了对他们用例至关重要的遗留数据集成。但这些不是普通客户。这些是公司的前 3 大客户(约 1%),占公司收入的约 50%(考虑到他们的定制增销)。

这就是当只关注概率(95%的客户喜欢更新)时可能犯的(致命)错误。故事的寓意是,当处理来自极端领域的稀有事件数据时,错一次可能抵消对 99 次正确的判断(甚至更多)

生成图表的代码 👇

[## YouTube-Blog/power-laws 在主分支 · ShawhinT/YouTube-Blog

代码用于补充 YouTube 视频和 Medium 博客帖子。 - YouTube-Blog/power-laws 在主分支 · ShawhinT/YouTube-Blog

github.com](https://github.com/ShawhinT/YouTube-Blog/tree/main/power-laws?source=post_page-----0355a187ee6a--------------------------------)

极端主义中的争议

幂律,如高斯分布,是一种理想化的数学抽象。然而,现实世界却很混乱,几乎不可能完全符合我们美丽而精确的构造。这引发了一些争议,即某个特定的分布是否真正是幂律。

争论的一个点是财富是否是幂律(如帕累托的工作所示)或仅仅是一个对数正态分布 [5]。

部分争议可能通过观察到对数正态分布在低σ时表现如高斯分布,在高σ时表现如幂律来解释[2]。

然而,为了避免争议,我们可以离开(暂时某些给定数据是否符合幂律的问题,转而关注胖尾

胖尾性 — 测量中等主义和极端主义之间的距离

胖尾是一个比帕累托和幂律分布更一般的概念。我们可以把它看作“胖尾性”是稀有事件推动分布总体统计的程度。从这个角度看,胖尾性在从不胖尾(即高斯分布)到非常胖尾(即帕累托 80–20)的谱系上。

这与之前讨论的 Mediocristan 与 Extremistan 的概念直接对应。下面的图像展示了不同分布在这一概念景观中的可视化 [2]。

Mediocristan 与 Extremistan 的地图。注意:由于肥尾现象存在于一个范围内,因此将分布标记为“肥尾”与否具有一定的主观性。图片作者。

尽管没有准确的肥尾度量,但我们可以在实践中使用许多指标和启发式方法来感知给定分布在 Mediocristan 与 Extremistan 地图上的位置。以下是一些方法。

  • 幂律性:使用幂律尾部指数,即α——α越低,尾部越肥 [2]

  • 非高斯性:峰度(对于α ≤ 4 的幂律失效)

  • 对数正态分布方差

  • 塔勒布的κ指标 [6]

要点

面对肥尾数据的主要挑战是可能没有足够的数据来准确捕捉其潜在的统计属性。这些信息为数据从业者提供了一些启示。

  • 绘制分布例如直方图、PDF 和 CDF

  • 问问自己——这些数据来自 Mediocristan 还是 Extremistan(或介于两者之间的某个地方)

  • 在构建模型时,问问自己——正确预测的价值和错误预测的成本是什么?

  • 如果处理(非常)肥尾数据,不要忽视稀有事件。相反,弄清楚如何利用它们(例如,你能否为前 1%的客户做一个特别促销以推动更多业务?)

👉 更多关于幂律和肥尾的信息:幂律拟合 | 量化肥尾

[## 用 Python 检测现实世界中的幂律]

用示例代码分解最大似然法的方法

towardsdatascience.com](/detecting-power-laws-in-real-world-data-with-python-b464190fade6?source=post_page-----0355a187ee6a--------------------------------)

资源

联系我的网站 | 预约电话 | 问我任何问题

社交媒体YouTube 🎥 | LinkedIn | Twitter

支持请我喝杯咖啡 ☕️

[## 免费获取我撰写的每个新故事的访问权限]

免费获取我撰写的每个新故事的访问权限。附言:我不会与任何人分享你的电子邮件。通过注册,你将创建一个…

shawhin.medium.com

[1] 帕累托原则。 (2023 年 10 月 30 日). 载于 维基百科en.wikipedia.org/wiki/Pareto_principle

[2] arXiv:2001.10488 [stat.OT]

[3] 塔勒布, N.N. (2007). 黑天鹅:极不可能事件的影响. 纽约;兰登书屋。

[4] www.archives.gov/exhibits/influenza-epidemic/

[5] arXiv:0706.1062 [physics.data-an]

[6] Taleb, N. N. (2019). 你需要多少数据?一种操作性的、前渐近的尾部厚度度量。国际预测学杂志35(2),677–686。 doi.org/10.1016/j.ijforecast.2018.10.003

Parquet 最佳实践:在不加载数据的情况下发现你的数据

原文:towardsdatascience.com/parquet-best-practices-discover-your-data-without-loading-them-f854c57a45b6

元数据、行组统计、分区发现和重新分区

ArliTowards Data Science Arli

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

--

如果你想亲自体验 Medium,可以考虑通过 注册会员 来支持我和其他成千上万的写作者。这只需每月$5,它对我们写作者的支持巨大,而且你可以访问 Medium 上所有精彩的故事。

Jakarta ParquetUnsplash 提供的照片

这篇文章是关于 Parquet 系列文章中的下一篇。如果你没有 Parquet 知识,应该先查看之前的 Parquet 文章 ,但它也是对更高级用户的很好的提醒。如果你想重现这篇文章的输入数据,代码在文末。

Apache Parquet 是一种用于大数据框架的列式存储格式,如Apache HadoopApache Spark。它旨在通过使用列式存储格式压缩高效的方式存储数据,从而提高大数据处理的性能。

Parquet 的采用持续增加,因为越来越多的组织转向大数据技术来处理和分析大型数据集。随着这种持续发展,学习一些最佳实践以及如何浏览Parquet文件变得至关重要。

在本教程中,我们将展示如何作为Parquet用户在不依赖常见的暴力加载方式的情况下,深入洞察你的Parquet数据。

案例研究

为此,我们提供了一个案例研究,其中一个数据工程师给你提供了贷款申请者的数据,你需要使用这些数据创建预测模型。但首先,你需要“技术性地”发现数据。数据量非常大。

确实,准备数据的数据工程师告诉你,Parquet 文件夹的大小为1TB仅用于教育目的,这在我们的示例中并非如此),因此如果你尝试加载所有内容,你的机器将会遇到内存错误。

不用担心,我们会提供最有效的方式来理解大型Parquet数据,甚至不需要将Parquet数据加载到内存中。

这意味着需要回答以下问题:

  • 这个文件夹中的Parquet文件是什么样的?

  • 变量里面有哪些?类型是什么?一些统计数据

  • 数据是如何分区的?

我们还会教你如何重新格式化分区,如果你发现数据分区的方式有问题的话。

阅读第一个 Parquet 文件

你在这个教程中需要的导入:

import pyarrow as pa
import pyarrow.parquet as pq
import os

首先,我们想了解文件夹‘APPLICATIONS_PARTITIONED’包含什么,这里存储了数据。

由于你不知道数据是如何分区的,因此不能盲目地加载整个文件夹,因为你将会加载所有的Parquet文件,这不是你想做的(记住 1TB 的大小),而是你需要对数据进行概览。

这里,我给你一个函数get_first_parquet_from_path(),它会返回目录中的第一个Parquet文件。该函数将扫描每个目录和子目录,直到找到一个Parquet文件,并返回该单个文件的完整路径。

def get_first_parquet_from_path(path):
    for (dir_path, _, files) in os.walk(path):
        for f in files:
            if f.endswith(".parquet"):
                first_pq_path = os.path.join(dir_path, f)
                return first_pq_path

看起来是个很酷的函数,让我们把它付诸实践。

path = 'APPLICATIONS_PARTITIONED'
first_pq = get_first_parquet_from_path(path)
first_pq
#Output : APPLICATIONS_PARTITIONED/NAME_INCOME_TYPE=Commercial associate/CODE_GENDER=F/6183f182ab0b47c49cf56a3e09a3a7b1-0.parquet

从路径中我们可以注意到,这里按NAME_INCOME_TYPECODE_GENDER进行分区,知道这一点很重要。

现在要读取这个路径以获取行数和列数,以及宝贵的Schema,你可以这样做:

first_ds = pq.read_table(first_pq)
first_ds.num_rows, first_ds.num_columns, first_ds.schema

执行时间不到 1 秒,原因是read_table()函数读取一个Parquet文件并返回一个PyArrow Table对象,该对象代表你的数据,作为一个由Apache Arrow开发的优化数据结构。

现在,我们知道有 637800 行和 17 列(+2 来自路径),并对变量及其类型有了概览。

等等,我之前告诉过你,我们不需要在内存中加载任何东西来发现数据。所以这里有一个方法,可以在不读取任何表的情况下做到这一点。

元数据

我在部分欺骗你,因为我们不会加载任何数据,而是会加载所谓的元数据

Parquet文件格式的上下文中,metadata指的是描述文件中存储的数据的结构特征的数据。这包括每列的数据类型、列的名称、表中的行数和模式等信息。

让我们使用pyarrow.parquet中的read_metadata()read_schema()函数:

ts=pq.read_metadata(first_pq)
ts.num_rows, ts.num_columns, pq.read_schema(first_pq)

这会给你与read_table()相同的输出。

然而,我们注意到执行时间上有很大差异,因为这里接近瞬时。这并不奇怪,因为读取metadata就像是读取Parquet文件中一个非常小的部分,它包含了你需要的所有信息来概述数据。

统计数据

现在假设我想多了解一下列,我该怎么办?

你可以从文件的第一个Row Group读取统计数据。

Parquet文件格式中,Row Group是将行作为一个单位存储在一起的集合,并分成更小的块以便于查询和处理。

parquet_file = pq.ParquetFile(first_pq)
ts=parquet_file.metadata.row_group(0)
for nm in range(ts.num_columns):
    print(ts.column(nm))

上面的代码会给你一个不太美观的输出,这里有一些代码可以将其格式化为一个漂亮的 DataFrame:

beautiful_df = pd.DataFrame()
for nm in range(ts.num_columns):
    path_in_schema = ts.column(nm).path_in_schema
    compressed_size = ts.column(nm).total_compressed_size
    stats = ts.column(nm).statistics
    min_value = stats.min
    max_value = stats.max
    physical_type = stats.physical_type
    beautiful_df[path_in_schema] = pd.DataFrame([physical_type, min_value, max_value, compressed_size])
df = beautiful_df.T
df.columns = ['DTYPE', 'Min', 'Max', 'Compressed_Size_(KO)']

在 DataFrame 中,你可以看到列的类型、最小值、最大值和压缩大小。从这个文件中得到的一些学习点:

  • 字符串列被转换为BYTE_ARRAY

  • 字符串列的最小值和最大值按字母顺序排序。

  • 布尔型的压缩大小不比BYTE_ARRAY好多少。

  • 最年轻的申请者 21 岁,最年长的是 68 岁。

要注意不要将统计数据泛化,这只是来自第一个parquet文件!

很好,现在我们对数据有了很好的理解,包括列的信息、类型、模式,甚至统计数据,但我们是否遗漏了什么?

分区

是的,我们不知道数据的分区!如前所述,我们可以从文件路径中至少猜测到分区列:

数据按NAME_INCOME_TYPECODE_GENDER分区。但我们不知道其他分区值。假设我们想查看其他NAME_INCOME_TYPE

但我会提供你一段代码,这样你可以以更系统的方式获取分区,以及所有可能的分区值:

def get_all_partitions(path):
    partitions = {}
    i = 0
    for (_, partitions_layer, _) in os.walk(path):
        if len(partitions_layer)>0:
            key = partitions_layer[0].split('=')[0]
            partitions[key] = sorted([partitions_layer[i].split('=')[1] for i in range(len(partitions_layer))])
        else:
            break
    return partitions

让我们运行这个函数,它返回一个字典,其中键对应于分区列,值是与每个分区列关联的分区值

ps = get_all_partitions(path)
ps.keys(), ps.values()

我们现在知道数据工程师首先按Income_Type分区,然后按Gender分区。所有分区列的值如下:

既然我们已经了解了分区列和分区值的知识,我们可以读取另一个感兴趣的分区。

假设我们想读取所有‘Pensioner’的数据,无论Gender是什么。

从上一个教程中,我们知道我们可以通过读取 Parquet 文件夹‘APPLICATIONS_PARTITIONED/NAME_INCOME_TYPE=Pensioner’来做到这一点

df_pensioner = pd.read_parquet('APPLICATIONS_PARTITIONED/NAME_INCOME_TYPE=Pensioner/')

重新格式化分区

实际上,我们不打算按性别拆分数据,而且数据的大小允许我们在没有过多运行时间的情况下读取两个性别的数据。

不要过度分区数据,因为通常,执行时间会随着文件夹中的分区数量增加而增加。因此,你必须记住,分区即使使数据在功能上更易读,也可能有潜在的缺点。(来自官方文档 512MB — 1GB 是分区的最佳大小)。

在这里,假设在检查数据后,我们认为性别的子文件夹足够小,并且发现性别的功能划分没有用处。我们决定将数据集重新格式化,仅按NAME_INCOME_TYPE进行分区:

pq_table = pq.read_table('APPLICATIONS_PARTITIONED')
pq.write_to_dataset(pq_table, 'APPLICATIONS_REPARTITIONED', partition_cols=['NAME_INCOME_TYPE'])

我们刚刚在PyArrow Table 对象中读取了数据,然后我们写了一个Parquet 文件,仅按NAME_INCOME_TYPE分区,不再按性别分区。如果我们现在运行get_all_partitions()函数,值为:

partitions = get_all_partitions('APPLICATIONS_REPARTITIONED')
partitions.keys(), partitions.values()

我们注意到,我们不再按性别进行分区。

总之,你刚刚了解了如何浏览 Parquet 文件,了解有关数据的一切:例如列名、大小、模式、统计信息以及如何获取分区名称和值。你还发现了如何重新格式化分区以使其在技术上和功能上更为正确。

感谢阅读,下次故事见!

生成我们所用输入数据的完整代码:

继续阅读我其他的 Parquet 文章:

## 简单的 Parquet 教程和最佳实践

实用教程,开始你的 Parquet 学习

towardsdatascience.com [## Parquet 最佳实践:筛选的艺术

理解如何筛选 Parquet 文件

pub.towardsai.net](https://pub.towardsai.net/parquet-best-practices-the-art-of-filtering-d729357e441d?source=post_page-----f854c57a45b6--------------------------------)

通过我的推荐链接,你可以无额外费用地订阅 Medium。

[## 使用我的推荐链接加入 Medium — Arli

阅读 Arli 和成千上万其他 Medium 作者的每一个故事。你的会员费用直接支持 Arli 和…

medium.com

使用 Python 解析 HL7

原文:towardsdatascience.com/parsing-hl7-with-python-961e19c4d962

使用 python-hl7 提取健康等级 7 数据的指南

Tara (Boyle) HunterTowards Data Science Tara (Boyle) Hunter

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

--

照片由 Christina Victoria Craft 提供,来源于 Unsplash

解析 HL7 消息对于许多医疗数据专业人员来说是一个重要任务。HL7 在医院和其他医疗机构中常用于在不同系统、应用程序和提供者之间交换病人数据。

根据 维基百科的定义,HL7 或健康等级 7 “指一套用于在不同医疗提供者使用的软件应用程序之间交换电子健康信息的国际标准”。这些标准使医疗提供者能够以一致且易于解释的方式共享临床信息。

在这里,我们将学习如何使用 Python 解析 HL7 消息。

介绍

在开始解析之前,我们需要对 HL7 消息结构有一个基本的了解。虽然 HL7 有不同的版本,这里我们将重点关注最常用的 2.x 系列版本。

HL7 消息通常是在病人事件(如接收、出院或转院)发生时创建和发送的。

HL7 消息由多个部分组成。每一行称为一个段落。段落由三字符标签标识,并由回车符(\r)分隔。段落包含由管道符(|)分隔的字段。字段包含由脱字符(^)分隔的组件。最后,组件可以包含由 & 符号分隔的子组件。

常见的 HL7 段落

  • MSH — 消息头:消息的第一个段落,包含有关发送和接收系统、消息类型和消息控制 ID 的信息。

  • PID — 病人识别:该段包含病人识别信息,包括病人 ID、姓名和出生日期。

  • DG1 — 诊断:该段包含与病人诊断相关的信息。

  • Z 段:这些段不是原始 HL7 标准的一部分。它们用于创建自定义消息并增加 HL7 的灵活性。自定义段的格式和内容由发送和接收应用程序协商。

关于这些和其他段类型的更多信息可以在这里找到。

示例消息

在这里我们将处理这个虚构的入院消息:

## made up sample admit message
MSH|^~\&|HIS|HOSPITAL|LAB|LAB|20230131111929||ADT^A01|1000027|P|2.3||||
EVN|A01|20220131111924
PID|1||0012345678^^^MRN^MRN||Doe^John^R||19700101|M|||123 Main St.^^Anytown^CA⁹¹²³⁴^USA|||||||||||||||||||||
PV1|1||^^¹⁰⁰¹|||||||||||||||||||1||||||||||||||||||||||||||||||||||
DG1|1||123456789^Diagnosis^I9||Confirmed
ZCP|1|Custom Segment Data

在此消息中,MSH 段包含消息头信息,包括发送应用程序、发送机构、接收应用程序和接收机构。

EVN 段包含事件信息,表明这是一个 ADT A01 消息,并在 2023 年 1 月 31 日 11:19:24 发送。

PID 段包含病人识别信息,包括病人标识符、姓名、出生日期和地址。

PV1 段包含病人访问信息,如病人位置和主治医生。

DG1 段包含一个虚构的诊断代码“123456789”和描述“诊断”。诊断状态字段标记为“已确认”。

最终我们看到自定义 ZCP 段,包含段 ID 为“ZCP”和两个数据字段“1”和“自定义段数据”。

解析 HL7 消息

现在我们对 HL7 消息有了基本了解,接下来开始解析吧!

在这里我们将使用[python-hl7](https://python-hl7.readthedocs.io/en/latest/) 。它提供了简单的方法来使用 Python 解析 HL7 消息。

python-hl7可以通过pip轻松安装。

pip install hl7 

安装完成后,我们可以导入库并解析我们的示例消息:

import hl7

## sample message
msg = '''
MSH|^~\&|HIS|HOSPITAL|LAB|LAB|20230131111929||ADT^A01|1000027|P|2.3||||
EVN|A01|20220131111924
PID|1||0012345678^^^MRN^MRN||Doe^John^R||19700101|M|||123 Main St.^^Anytown^CA⁹¹²³⁴^USA|||||||||||||||||||||
PV1|1||^^¹⁰⁰¹|||||||||||||||||||1||||||||||||||||||||||||||||||||||
DG1|1||123456789^Diagnosis^I9||Confirmed
ZCP|1|Custom Segment Data
'''

parsed = hl7.parse(msg)
print(parsed)

解析的 HL7 消息 — 作者提供的图像

在这里我们可以看到解析后的消息。我们可以检查消息的长度以确保它按预期被解析:

print(len(parsed))
>>> 1

在这里,我们看到解析后的消息长度为 1。由于我们可以在上述消息中数到 6 个不同的段,这似乎不正确。

这是由于我们的换行符问题。python-hl7期望换行符是回车符(\r),而不是换行符(\n)。我们可以通过将换行符替换为回车符来解决这个问题:

msg = msg.replace('\n','\r')
parsed = hl7.parse(msg)
print(len(parsed))
>>> 6

这看起来好多了!现在我们的长度为 6,与我们对消息的视觉检查一致。

现在让我们提取病人的姓名:

## get patient name
print(parsed[2][5])
>>> [[['Doe'], ['John'], ['R']]]

## get only first name
print(parsed[2][5][0][1][0])
>>> 'John'

我们成功提取了病人的全名和名字!

我们还可以通过指定段来访问这些信息:

## we can access the same information by specifying segement
parsed.segments('PID')[0][5]
>>> [[['Doe'], ['John'], ['R']]]

## and only the first name
parsed.segments('PID')[0][5][0][1][0]
>>> 'John'

这种方法的一个好处是可以让我们的代码更容易理解。

结论

在这里,我们了解了 HL7 消息以及如何使用 Python 的python-hl7库来解析它们。

这个库提供了一种方便的方式来使用 Python 读取 HL7 消息,并且支持大多数 HL7 消息类型和段落。

使用合适的工具,开发人员可以有效地和高效地用 Python 解析 HL7 消息!由于 HL7 是医疗保健领域广泛使用的标准,解析这些消息的知识对从事医疗行业的数据专业人士来说是一个有价值的技能。

在人道主义数据集中解析不规则电子表格(借助 GPT-3 的帮助)

原文:towardsdatascience.com/parsing-irregular-spreadsheet-tables-in-humanitarian-datasets-with-some-help-from-gpt-3-57efb3d80d45?source=collection_archive---------2-----------------------#2023-02-24

处理不规则 Excel 表格,无需使用硬编码规则

马修·哈里斯数据科学前沿 马修·哈里斯

·

关注 发布于 数据科学前沿 · 26 分钟阅读 · 2023 年 2 月 24 日

--

由 DALL-E2 根据提示“10 张木桌的画作”创作。上图中有 9 张桌子。

简短说明

作为 之前的研究 的一部分,使用了来自 人道主义数据交换 的数据,我不得不分析成千上万的 Excel 文件,这些文件中的表格常常难以解析成数据库表。文件来自全球数百个组织时,合并单元格、不规则布局、层次化列和注释难以通过基于规则的解析来预见。在这篇文章中,我探讨了使用 GPT-3 的零-shot、单-shot 和推理补全来重新格式化不规则(小型)表格,并微调模型以预测表格属性,从而用于准确解析。

在我的旅行中,有不少次需要查看大量 Excel 文件,以了解它们包含的数据、数据的结构如何,以及将其清理成可以处理的形式所需的工作。大部分情况下,只要数据规则且列标题整齐,这个过程相当简单。然而,现实从未那么简单,这些文件中的表格往往以不完美的格式存在,难以解析成可以上传到关系数据库的数据框。Excel 支持许多功能,如数据透视表和单元格合并,人们使用这些功能创建各种各样的布局,包括空白行、随机文本等等!

这里有一个例子来说明……

Excel 中的不规则表格示例,带有空白顶部行、标签和合并单元格。对人类来说完全可读,但对数据科学来说是解析的挑战。该文件来自人道主义数据交换

如果我们直接将上述文件读入 Pandas 中……

import pandas as pd
df = pd.read_excel(filename)
df = df.fillna("")
display(df)

我们得到这个……

Pandas 数据框在解析 Excel 表格后的示例,其中包含空行和合并单元格,以指示层次列。示例数据来自人道主义数据交换

将其加载到数据库中会导致数据几乎无法使用,因为……

  1. 右上角单元格中有一个表格标题。

  2. 列‘Unnamed: 1’的标题实际上是第一列第 5 行的内容“你拥有的土地的平均面积是多少……”

  3. 列‘Unnamed:2’和‘Unnamed:3’是分为’N‘ 数值和‘%’ 百分比值的汇总总数。

  4. 大多数列是层次化的,合并单元格位于未合并单元格之上。

这也不那么糟糕,对吧?

当然,可以向Pandas read_excel提供参数,将层次列转换为索引,然后可以将其合并为一行。或者,我们可以使用Openpyxl中关于 Excel 自身的合并单元格的信息进行操作。然而,这些方法需要对表格有了解——特别是标题在哪里结束、数据从哪里开始以及层次列的结构——这是我们在处理成千上万的电子表格时可能不总是拥有的奢侈品。对大量文件进行基于规则的解析可能耗时且脆弱,需要随着新布局的出现而持续维护。

其实,我并不是唯一一个遇到这个问题的人!解析不规则表格是一项正在积极研究的挑战。例如,微软的作者展示了利用卷积神经网络开发的一个名为‘TableSense’的算法的出色成果[1]。这种技术将 Excel 表格视作图像来处理,但具有更丰富的特征化,因为每个单元格可能具有多种属性和数据类型,还包括格式化和合并特征。非常酷。我希望像这样的精彩工作能尽快纳入微软的产品中,但在此之前,我想探索一些其他的方法。

值得注意的是,我的使用案例不仅仅是识别表格在工作表中的范围(参见微软论文的训练数据),还包括表格中的元素,以便将不规则的格式转换为可以轻松导入数据库的格式。主要挑战是 Excel 中的层次列,将这些层次列展平成一个单独的行,从而捕捉上层合并单元格中的信息。听起来解决起来很简单,但挑战是:标题在哪里结束,数据从哪里开始?这对我们人类来说显而易见,但令人惊讶的是,当用代码处理工作表时,这样简单的事情在现实世界中可能会变得非常嘈杂。

鉴于最近对生成式 AI 和大型语言模型(LLMs)的关注,我想知道也许OpenAI 的 GPT-3可能会接受这个挑战。这些模型在从互联网提取的大量数据上进行了训练,其中包括表格和 CSV 文件,因此它们可能在处理我们这些疯狂人类拼凑的表格的某些细节方面会很有用。

提示 GPT-3 清理(一个小的)表格

我们将首先尝试将问题作为零样本和少量样本任务解决,然后再转向使用微调技术。

零样本、单样本和少样本任务,与传统的微调对比。上面的面板展示了用语言模型执行任务的四种方法。来源于 Brown 等人 [2]。

GPT-3 是在从网络上抓取的文本上训练的,所以我们不能用 Excel 提示它(还不行!),因此我们首先必须将我们的表格转换成一种网络上常见的形式,例如 CSV 字符串……

df = pd.read_excel('sample.xlsx', sheet_name='Sheet1')
df = df.fillna("")
print(df.to_csv())
,Unnamed: 0,Unnamed: 1,Unnamed: 2,Unnamed: 3,Unnamed: 4,Unnamed: 5,Unnamed: 6,Unnamed: 7,Unnamed: 8,Unnamed: 9,Unnamed: 10,Unnamed: 11
0,Table 3: Number of acreage under irrigation,,,,,,,,,,,
1,,,OVERALL,,Sub county,,,,,,,
2,,,,,Chepalungu,,,,Bomet Central,,,
3,,,,,Male,,Female,,Male,,Female,
4,,,N,%,N,%,N,%,N,%,N,%
5,What is the average size of land you own that is currently under irrigation?,0 - 2 acres,22,2.8%,4,2.2%,10,3.8%,3,1.7%,5,2.9%
6,,2 - 5 acres,6,.8%,2,1.1%,2,.8%,0,0.0%,2,1.2%
7,,5 - 10 acres,1,.1%,0,0.0%,0,0.0%,0,0.0%,1,.6%
8,,More than 10 acres,0,0.0%,0,0.0%,0,0.0%,0,0.0%,0,0.0%
9,,None,760,96.3%,176,96.7%,251,95.4%,170,98.3%,163,95.3%
10,,Total,789,100.0%,182,100.0%,263,100.0%,173,100.0%,171,100.0%

附注:我还尝试了 Markdown 和 HTML 表格,但发现 CSV 在我的用例中效果最好。

值得注意的是,对于这项分析,我们处理的表格是稀疏的,即列数少于 100。这意味着前 10 行可以很容易地在 GPT-3 提示中表示。这对我在援助数据交换中分析的大多数 Excel 表格来说是合适的,但可能不适用于其他情况。此外,这项分析不考虑同一 Excel 工作表上有多个表格的情况……这将是稍后博客文章的内容。🙂

零样本提示

现在让我们看看 GPT-3 是否可以仅通过一个提示重新格式化我们凌乱的表格,这是一项零样本任务 [2],我们没有提供示例,只是提供了要重新格式化的 CSV 文件……

import openai as ai

# Open AI API key should be put into this file
ai.api_key_path = "./api_key.txt"

csv_as_str = df.to_csv()

prompt = (
    "Reformat this table to be a simpler markdown table with "
    + "no hierarchical columns, no pivoting, values and percentages in different columns, "
    + "and no blank cells\n\n"
    + csv_as_str
)

completions = ai.Completion.create(
    engine="text-davinci-003",
    temperature=0.0,
    prompt=prompt,
    max_tokens=999,
    n=1,
    stop=None,
)

Markdown(completions.choices[0].text)

它丢弃了不必要的行,将数据转换为一个规范的表格,带有列标题,但仔细观察会发现,它丢失了一些关键信息,如按性别的分类。这是经典的幻觉现象,看起来很可信,但却是错误的。

让我们玩玩温度参数。较低的值使模型更加确定性(对于相同的提示每次都给出相同的结果),而较高的值则更随机。使用更高的温度值,我们得到……

prompt = (
    "Reformat this table to be a simpler markdown table with "
    + "no hierarchical columns, no pivoting, values and percentages in different columns, "
    + "and no blank cells\n\n"
    + csv_as_str
)

completions = ai.Completion.create(
    engine="text-davinci-003",
    temperature=1.0,
    prompt=prompt,
    max_tokens=999,
    n=1,
    stop=None,
)

Markdown(completions.choices[0].text)

看起来不错!几乎所有的正确列标题都来自我们 CSV 文件中的合并单元格,这实际上相当惊人。然而,抽查几个单元格显示,尽管许多是正确的,但也有一些不正确。此外,上面的“总体”被分成了男性和女性,这是不正确的。

另一个问题是,调用 GPT-3 完全相同的提示会产生不同的结果,因为高温值……

completions = ai.Completion.create(
    engine="text-davinci-003",
    temperature=1.0,
    prompt=prompt,
    max_tokens=999,
    n=1,
    stop=None,
)

Markdown(completions.choices[0].text)

不无道理,尽管值不正确,但布局完全不同。可重复性对我们的任务非常重要,我们应该能够在每次处理运行中以完全相同的方式处理表格数据。

所以高温似乎不是这个用例的好选择。

如果我们在表格中提供更多上下文会怎么样?CSV 并不是很具表现力,例如,层级标题中的合并列告诉人类这些列是分组的,但 CSV 文件并未捕捉到这一点……

1,,,OVERALL,,Sub county,,,,,,,
2,,,,,Chepalungu,,,,Bomet Central,,,
3,,,,,Male,,Female,,Male,,Female,
4,,,N,%,N,%,N,%,N,%,N,%

在上述示例中,GPT-3 必须推断出合并的行标题右侧的空白列与这些标题对应,并且很多时候它确实能够做到这一点。然而,由于我们知道 Excel 文件中哪些单元格是合并的,我们可以稍微帮助一下。

为了在 CSV 中表示这个,我们可以取消合并合并的单元格,并用其合并值填充 …

def pad_merged_cells(sheet):
    """
    Unmerge merged cells and fill with merged value.

    Input Parameters
    ----------------
    sheet: Obj
        Openpyxl sheet object

    Output Parameters
    -----------------
    df: Dataframe
        Pandas dataframe of the table
    """

    dd = pd.DataFrame(sheet.values)

    # Scan for maxn rows
    maxn = 10

    hasmerged = False
    if len(sheet.merged_cells.ranges) > 0:
        hasmerged = True

    if hasmerged:
        merge_list = []
        for merge in sheet.merged_cells.ranges:
            merge_list.append(merge)

        for cell_group in merge_list:
            min_col, min_row, max_col, max_row = range_boundaries(
                str(cell_group))
            top_left_cell_value = sheet.cell(row=min_row, column=min_col).value
            sheet.unmerge_cells(str(cell_group))
            for row in sheet.iter_rows(
                min_col=min_col, min_row=min_row, max_col=max_col, max_row=max_row
            ):
                for cell in row:
                    cell.value = top_left_cell_value

    # Extract data and save to dataframe
    data = []
    for row in sheet.iter_rows(min_row=1):
        row_data = []
        for cell in row:
            if cell.value is None:
                row_data.append(None)
            else:
                row_data.append(cell.value)
        if any(row_data):
            data.append(row_data)

    df = pd.DataFrame(data)

    # Remove duplicate columns
    df = df.T.drop_duplicates().T

    # Remove duplicate rows
    df = df.drop_duplicates()

    # Fill NaN with blank string for easier viewing
    df = df.fillna("")

    return df, sheet, hasmerged

wb = openpyxl.load_workbook(filename)
sheet = wb['Sheet1']
merged_table, sheet, hasmerged = pad_merged_cells(sheet)

display(merged_table)

表格中合并的单元格被取消合并,并用合并值填充,以在 CSV 文件格式中提供上下文。

,Table 3: Number of acreage under irrigation,,,,,,,,,,,
1,,,OVERALL,OVERALL,Sub county,Sub county,Sub county,Sub county,Sub county,Sub county,Sub county,Sub county
2,,,OVERALL,OVERALL,Chepalungu,Chepalungu,Chepalungu,Chepalungu,Bomet Central,Bomet Central,Bomet Central,Bomet Central
3,,,OVERALL,OVERALL,Male,Male,Female,Female,Male,Male,Female,Female
4,,,N,%,N,%,N,%,N,%,N,%
5,What is the average size of land you own that is currently under irrigation?,0 - 2 acres,22,2.8%,4,2.2%,10,3.8%,3,1.7%,5,2.9%
6,What is the average size of land you own that is currently under irrigation?,2 - 5 acres,6,.8%,2,1.1%,2,.8%,0,0.0%,2,1.2%
7,What is the average size of land you own that is currently under irrigation?,5 - 10 acres,1,.1%,0,0.0%,0,0.0%,0,0.0%,1,.6%
8,What is the average size of land you own that is currently under irrigation?,More than 10 acres,0,0.0%,0,0.0%,0,0.0%,0,0.0%,0,0.0%
9,What is the average size of land you own that is currently under irrigation?,None,760,96.3%,176,96.7%,251,95.4%,170,98.3%,163,95.3%
10,What is the average size of land you own that is currently under irrigation?,Total,789,100.0%,182,100.0%,263,100.0%,173,100.0%,171,100.0%

CSV 文件现在捕获了叠加的合并列标题。让我们看看这是否能改善情况,首先温度=0.0 …

csv_as_str_merged = merged_table.to_csv()

prompt = (
    "Reformat this table to be a simpler markdown table with "
    + "no hierarchical columns, no pivoting, values and percentages in different columns, "
    + "and no blank cells\n\n"
    + csv_as_str_merged
)

completions = ai.Completion.create(
    engine="text-davinci-003",
    temperature=0.0,
    prompt=prompt,
    max_tokens=999,
    n=1,
    stop=None,
)

Markdown(completions.choices[0].text)

同样的,但温度=1.0,只是为了好玩 …

稍微好了一些,但总是有些地方不太对。缺失的类别,单元格值偏移,如果我们需要准确表示源数据,两个表格都无法使用。

此时,我尝试了各种组合:

  • 提示

  • 温度

  • 使用 Markdown、HTML 和 CSV 定义输入表格

  • 提示 GPT-3 生成用于解析的 Python 代码,而不是解析表格

有时该过程能够生成列标题和数值完美的表格,但通常这需要高温度值,因此不可重复。大多数情况下,结果看起来合理,但数据不正确。

公平地说,我们真的对 GPT-3 提出了很高的要求,这是一项复杂的零样本任务。我对它的表现感到非常满意,也许通过更好的提示和问题的重新框定 —— 或 GPT-4!—— 结果可能会有所改善,但我没有能够实现所需的结果。

单次提示

现在,让我们在提示中提供一个示例。我从人道数据交换获取了一个类似的 Excel 文件 …

我们将在单次提示中使用的表格。此文件来源于 人道数据交换

我们希望这被处理成如下所示 …

我们的示例文件在重新格式化后的样子

显然,这是一个不切实际的“真实世界”示例,因为格式和内容与我们尝试处理的表格非常相似,但这是一个很好的初步测试。

将我们的输入表格转换为 CSV 并取消合并合并的单元格,如上所述,我们得到 …

我们现在可以构建我们的单次提示(假设温度为零以便可重复) …

from io import StringIO

wb = openpyxl.load_workbook(prompt_sample_table1, data_only=True)
sheet = wb["Sheet1"]
example_before, sheet, hasmerged = pad_merged_cells(sheet)
example_before_csv = example_before.to_csv()
example_after, hasmerged, report = parse_excel_sheet(sheet)
example_after_markdown = example_after.to_markdown()
example_after_csv = example_after.to_csv()

example_before_csv = """
 ,0,1,2,3,4,5,6,7
0,Table 16: % of infants on Minimum Dietary Diversity,,,,,,,
1,,,OVERALL,OVERALL,Sub county,Sub county,Sub county,Sub county
2,,,OVERALL,OVERALL,Chepalungu,Chepalungu,Bomet Central,Bomet Central
3,,,N,%,n,%,n,%
4,Infants         on Dietary Diversity,Infants  on  Minimum  Dietary Diversity,37,17.5%,24,17.9%,13,16.7%
5,Infants         on Dietary Diversity,Infants not on Dietary Diversity,175,82.5%,110,82.1%,65,83.3%
6,Infants         on Dietary Diversity,Total,212,100.0%,134,100.0%,78,100.0%
"""

example_after_markdown = (
    """
 |    |                                      |                                         |   OVERALL - N | OVERALL - %   |   Sub county - Chepalungu | Sub county - Chepalungu - %   |   Sub county - Bomet Central | Sub county - Bomet Central - %   |
|---:|:-------------------------------------|:----------------------------------------|--------------:|:--------------|--------------------------:|:------------------------------|-----------------------------:|:---------------------------------|
|  1 | Infants         on Dietary Diversity | Infants  on  Minimum  Dietary Diversity |            37 | 17.5%         |                        24 | 17.9%                         |                           13 | 16.7%                            |
|  2 | Infants         on Dietary Diversity | Infants not on Dietary Diversity        |           175 | 82.5%         |                       110 | 82.1%                         |                           65 | 83.3%                            |
|  3 | Infants         on Dietary Diversity | Total                                   |           212 | 100.0%        |                       134 | 100.0%                        |                           78 | 100.0%                           |
""".replace(
        ":|", "|"
    )
    .replace("|:", "|")
    .replace("\n", "\n<RETURN>")
)

example_after_csv = """
 , , ,OVERALL - N,OVERALL - %,Sub county - Chepalungu,Sub county - Chepalungu - %,Sub county - Bomet Central,Sub county - Bomet Central - %
1,Infants         on Dietary Diversity,Infants  on  Minimum  Dietary Diversity,37,17.5%,24,17.9%,13,16.7%
2,Infants         on Dietary Diversity,Infants not on Dietary Diversity,175,82.5%,110,82.1%,65,83.3%
3,Infants         on Dietary Diversity,Total,212,100.0%,134,100.0%,78,100.0%
"""

table_to_parse_padded = """
,0,1,2,3,4,5,6,7,8,9,10,11
0,Table 3: Number of acreage under irrigation,,,,,,,,,,,
1,,,OVERALL,OVERALL,Sub county,Sub county,Sub county,Sub county,Sub county,Sub county,Sub county,Sub county
2,,,OVERALL,OVERALL,Chepalungu,Chepalungu,Chepalungu,Chepalungu,Bomet Central,Bomet Central,Bomet Central,Bomet Central
3,,,OVERALL,OVERALL,Male,Male,Female,Female,Male,Male,Female,Female
4,,,N,%,N,%,N,%,N,%,N,%
5,What is the average size of land you own that is currently under irrigation?,0 - 2 acres,22,2.8%,4,2.2%,10,3.8%,3,1.7%,5,2.9%
6,What is the average size of land you own that is currently under irrigation?,2 - 5 acres,6,.8%,2,1.1%,2,.8%,0,0.0%,2,1.2%
7,What is the average size of land you own that is currently under irrigation?,5 - 10 acres,1,.1%,0,0.0%,0,0.0%,0,0.0%,1,.6%
8,What is the average size of land you own that is currently under irrigation?,More than 10 acres,0,0.0%,0,0.0%,0,0.0%,0,0.0%,0,0.0%
9,What is the average size of land you own that is currently under irrigation?,None,760,96.3%,176,96.7%,251,95.4%,170,98.3%,163,95.3%
10,What is the average size of land you own that is currently under irrigation?,Total,789,100.0%,182,100.0%,263,100.0%,173,100.0%,171,100.0%
"""

prompt = (
    "Reformat this table to only have a single header row: \n\n"
    + example_before_csv
    + "\n\n"
    + "Result: \n\n"
    + example_after_csv
    + "\n\n"
    + "Reformat this table to only have a single header row: \n\n"
    + table_to_parse_padded
    + "\n\n"
    + "Result: \n\n"
)

print("\n\n", prompt, "\n\n")

completions = ai.Completion.create(
    engine="text-davinci-003",
    temperature=0.0,
    prompt=prompt,
    n=1,
    stop=None,
    max_tokens=2068,
    top_p=1,
    frequency_penalty=0,
    presence_penalty=0,
)

print("\n========== Model prediction:\n")

display(pd.read_csv(StringIO(completions.choices[0].text)))

这是生成的提示 …

Reformat this table to only have a single header row: 

 ,0,1,2,3,4,5,6,7
0,Table 16: % of infants on Minimum Dietary Diversity,,,,,,,
1,,,OVERALL,OVERALL,Sub county,Sub county,Sub county,Sub county
2,,,OVERALL,OVERALL,Chepalungu,Chepalungu,Bomet Central,Bomet Central
3,,,N,%,n,%,n,%
4,Infants         on Dietary Diversity,Infants  on  Minimum  Dietary Diversity,37,17.5%,24,17.9%,13,16.7%
5,Infants         on Dietary Diversity,Infants not on Dietary Diversity,175,82.5%,110,82.1%,65,83.3%
6,Infants         on Dietary Diversity,Total,212,100.0%,134,100.0%,78,100.0%

Result: 

 , , ,OVERALL - N,OVERALL - %,Sub county - Chepalungu,Sub county - Chepalungu - %,Sub county - Bomet Central,Sub county - Bomet Central - %
1,Infants         on Dietary Diversity,Infants  on  Minimum  Dietary Diversity,37,17.5%,24,17.9%,13,16.7%
2,Infants         on Dietary Diversity,Infants not on Dietary Diversity,175,82.5%,110,82.1%,65,83.3%
3,Infants         on Dietary Diversity,Total,212,100.0%,134,100.0%,78,100.0%

Reformat this table to only have a single header row: 

,0,1,2,3,4,5,6,7,8,9,10,11
0,Table 3: Number of acreage under irrigation,,,,,,,,,,,
1,,,OVERALL,OVERALL,Sub county,Sub county,Sub county,Sub county,Sub county,Sub county,Sub county,Sub county
2,,,OVERALL,OVERALL,Chepalungu,Chepalungu,Chepalungu,Chepalungu,Bomet Central,Bomet Central,Bomet Central,Bomet Central
3,,,OVERALL,OVERALL,Male,Male,Female,Female,Male,Male,Female,Female
4,,,N,%,N,%,N,%,N,%,N,%
5,What is the average size of land you own that is currently under irrigation?,0 - 2 acres,22,2.8%,4,2.2%,10,3.8%,3,1.7%,5,2.9%
6,What is the average size of land you own that is currently under irrigation?,2 - 5 acres,6,.8%,2,1.1%,2,.8%,0,0.0%,2,1.2%
7,What is the average size of land you own that is currently under irrigation?,5 - 10 acres,1,.1%,0,0.0%,0,0.0%,0,0.0%,1,.6%
8,What is the average size of land you own that is currently under irrigation?,More than 10 acres,0,0.0%,0,0.0%,0,0.0%,0,0.0%,0,0.0%
9,What is the average size of land you own that is currently under irrigation?,None,760,96.3%,176,96.7%,251,95.4%,170,98.3%,163,95.3%
10,What is the average size of land you own that is currently under irrigation?,Total,789,100.0%,182,100.0%,263,100.0%,173,100.0%,171,100.0%

Result: 

这是 GPT-3 的完成结果,转换为数据框以便更容易显示 …

从单次提示生成的表格,重新格式化具有层次结构标题的表格(完成结果是 CSV,这里为便于展示转换为 pandas 数据框)

很好!当提供一个示例时,GPT-3 能够完美地重新格式化我们的新表格。然而,这不是一个很好的测试,因为示例表格和测试表格在结构和内容上非常相似,但有趣的是,即使示例中没有男性/女性的层级,GPT-3 仍能正确地折叠这个额外的层级。

让我们使用相同的示例表格来重新格式化一个具有不同布局和内容数据的表格 …

使用相同的代码处理得到的是 …

这很接近,标题完全正确,但农场列向左移动了。我们的单次提示在重新格式化非常相似的表格时表现不错,但稍微的变化导致了较差的结果。

单次提示,带有推理

关于提示工程已经有相当多的研究。一个非常好的资源可以在 OpenAI Cookbook 的 提升可靠性的技术 [3] 中找到。提高结果的最有效方法之一是包含推理在示例提示中 [4]。以我们之前的表格为例,调整提示以包括推理 …

prompt = (
    "We need to reformat this table to only have a single header row: \n\n"
    + example_before_csv
    + "\n"
    + "Let's think step by step \n"
    + "Row 1 is just an index row, it has no text or data \n"
    + "Row 2 contains just label text \n"
    + "Rows 3 to 5 contain column headers \n"
    + "Rows 6 onwards contain data \n"
    + "Columns are separated by commas, there should be 7 commas on each row \n"
    + "If we combine each colummn of rows 3 to 5 by concatenating vertically, we get \n"
    + example_after_csv
    + "\n\n"
    + "We need to reformat this table to only have a single header row: \n\n"
    + table_to_parse_padded
    + "\n\n"
    + "Let's think step by step \n\n"
)

完整的提示如下 …

We need to reformat this table to only have a single header row: 

 ,0,1,2,3,4,5,6,7
0,Table 16: % of infants on Minimum Dietary Diversity,,,,,,,
1,,,OVERALL,OVERALL,Sub county,Sub county,Sub county,Sub county
2,,,OVERALL,OVERALL,Chepalungu,Chepalungu,Bomet Central,Bomet Central
3,,,N,%,n,%,n,%
4,Infants         on Dietary Diversity,Infants  on  Minimum  Dietary Diversity,37,17.5%,24,17.9%,13,16.7%
5,Infants         on Dietary Diversity,Infants not on Dietary Diversity,175,82.5%,110,82.1%,65,83.3%
6,Infants         on Dietary Diversity,Total,212,100.0%,134,100.0%,78,100.0%

Let's think step by step 
Row 1 is just an index row, it has no text or data 
Row 2 contains just label text 
Rows 3 to 5 contain column headers 
Rows 6 onwards contain data 
Columns are separated by commas, there should be 7 commas on each row 
If we combine each colummn of rows 3 to 5 by concatenating vertically, we get 

 , , ,OVERALL - N,OVERALL - %,Sub county - Chepalungu - N,Sub county - Chepalungu - %,Sub county - Bomet Central - N,Sub county - Bomet Central - %
1,Infants         on Dietary Diversity,Infants  on  Minimum  Dietary Diversity,37,17.5%,24,17.9%,13,16.7%
2,Infants         on Dietary Diversity,Infants not on Dietary Diversity,175,82.5%,110,82.1%,65,83.3%
3,Infants         on Dietary Diversity,Total,212,100.0%,134,100.0%,78,100.0%

We need to reformat this table to only have a single header row: 

,0,1,2,3,4,5
0,Random text here,,,,,
1,,Cows,Cows,Cowboy Hats,Cowboy Hats,Cowboy Hats
2,,TOT,Farm,Small ,Medium,Large
3,,97,6666,0.65,,0.13
4,,93,Bar M,,0.2,
5,,140,Daisy,0.3,0.89,
6,,,Plew,0.2,0.5,0.1
7,,119,Birdie,0.2,0.1,
8,,29,Kit Kat,,0.55,

Let's think step by step 

这导致 GPT-3 对我们输入表格的完成结果是 …

Row 1 is just an index row, it has no text or data 
Row 2 contains just label text 
Rows 3 to 8 contain column headers 
Rows 9 onwards contain data 
Columns are separated by commas, there should be 6 commas on each row 
If we combine each colummn of rows 3 to 8 by concatenating vertically, we get 

 , ,Cows - TOT,Cows - Farm,Cowboy Hats - Small ,Cowboy Hats - Medium,Cowboy Hats - Large
1,,97,6666,0.65,,0.13
2,,93,Bar M,,0.2,
3,,140,Daisy,0.3,0.89,
4,,,Plew,0.2,0.5,0.1
5,,119,Birdie,0.2,0.1,
6,,29,Kit Kat,,0.55,

这很正确!重新格式化后的表格正是我们想要的 …

如果我们在单次提示中提供推理,结果会有所改善

也就是说,我们提供的任务并不是很好,因为尽管内容与提供的示例不同,但标题布局仍然相似。事实上,如果我们稍微调整一下要重新格式化的表格并添加一个额外的“有机”列 …

向输入中添加一个额外的列

预测现在不正确 …

只是标题行中多了一个额外的逗号,这导致所有内容向右移动。

我们可能会继续通过更多推理来优化提示,或应用 更高级的技术 自动构建我们的提示工作流,但真正的问题是一个示例并不足以捕捉我们可能遇到的所有表格格式变体。尽管 GPT-3 在仅有一个示例的情况下表现得非常好,但对于这个任务来说,它还不够好(至少就目前的框架而言)。

少量示例…. 或者说不是

下一个方法可能是提供多个示例。然而,表格片段需要大量的令牌(稍后会详细说明),所以如果我们必须在提示中提供多个示例,再加上结果中的令牌,就会触及 Open API 的令牌限制。对于 davinci 模型,目前的限制为4,000个令牌。此外,由于我们按令牌收费,对于像DataKind这样的小型非营利组织,发送和接收大量令牌可能会变得昂贵。更长的提示还有性能影响,因此对于这个任务没有探索少样本提示。

所以我决定暂时跳过少样本学习。

微调

探索零样本和单样本提示很有趣,如果这些方法在这个用例中有效,将会取得惊人的结果。未来,随着模型的改进,这可能会成为一个可行的选项,但目前,重新定义任务可能更有意义。

另一种方法是通过微调提供大量示例。正如 OpenAI 所述:

微调可以通过提供以下内容来让你更好地利用 API 提供的模型:

  1. 比提示设计产生更高质量的结果

  2. 能够在比提示中能容纳的更多示例上进行训练

  3. 由于提示较短而节省令牌

  4. 更低延迟的请求

起初,我考虑通过提供 GPT-3(i)原始表格的提示(合并单元格未合并)和(ii)作为重新格式化表格的完成项来进行微调。然而,这种方法的挑战在于,它仍然使用了大量的令牌,尤其是我们现在需要使用数百个示例。

与其传递原始表格片段,不如尝试使用该表格的属性,并让 GPT-3 预测我们可以用来解析的关键进一步属性……

重新定义任务 — 使用表格属性作为提示

作为一个人(好吧,大部分是人),当我扫描 Excel 中的表格时,我可以通过查看值来识别结构,并决定数据的位置。

确定表格中的数据部分是将其解析成规则表格结构的关键

一旦我知道数据开始的行,就很容易从上面的行推断出标题层次,并将它们合并成一个单一的标题行,以创建一个整齐、规则的表格来使用……

处理后的表格具有平面标题,容易导入关系数据库

确定数据的开始位置乍一看似乎很简单,只需在 openpyxlpandas.read_excel 中稍作处理即可。然而,如果需要处理成千上万的具有不同标题布局、空白行等的电子表格,开发一套用于准确识别每个工作表中数据开始位置的规则将是一项挑战。

这很复杂,因为:

  • 列标题可能有很高的变化性,看起来像数据。

  • 空白单元格和注释容易混淆解析规则。

  • 数据不总是数字的,它可以是分类的,看起来很像列标题。

  • 一些列标题是数字,可能看起来像数据,例如年份。

那么,我们应该使用哪些表格属性/特征来预测数据首次出现的行号呢?

我列出了一个我认为可能有用的表格属性的简短清单……

import openpyxl

def get_sheet_attributes(sheet, maxn):
    """
    Returns a set of table attributes for a given sheet

    Input Parameters:
        sheet: Obj
            Openpyxl sheet object
        maxn: int
            Number of rows to scan at start of sheet

    Returns:
        null_cells_in_rows: list of ints
            Count of NULL records in forst maxn rows
        float_cells_in_rows: list of ints
            Count of numeric records in first maxn rows
        unique_vals_in_rows: list of ints
            Count of unique values in first maxn rows
        year_vals_in_rows: list of ints
            Count of year values in first maxn rows
        hxl_row: int
            Row number of HXL header row
        first_float_row: int
            Row number of row with most numeric records
        first_not_null_row: int
            Row number of row with most non-null records

    """
    dd = pd.DataFrame(sheet.values)

    null_cells_in_rows = list(
        dd[0:maxn].apply(lambda x: x.isnull().sum(), axis="columns")
    )
    float_cells_in_rows = []
    unique_vals_in_rows = []
    year_vals_in_rows = []
    report_json = {}
    hxl_row = None
    for index, row in dd[0:maxn].iterrows():
        unique_vals = list(row.unique())
        unique_vals = [i for i in unique_vals if i is not None and str(i) != "nan"]
        unique_vals_in_rows.append(len(unique_vals))
        float_count = 0
        year_count = 0
        if check_hdx_header(list(row)):
            hxl_row = index
        for col in dd.columns:
            val = row[col]
            # Handle numbers that come through as strings
            if isinstance(val, str):
                val = val.replace(",", "").replace(" ", "")
                if val.isnumeric():
                    val = int(val)
            # Check for year values
            if (
                ((isinstance(val, int) or isinstance(val, float)) and val % 1 == 0)
                and val > 1900
                and val < 2100
            ):
                year_count += 1
                continue
            # Check for HXL tags
            if isinstance(val, float) or isinstance(val, int) or "^=" in str(row[col]):
                float_count += 1
        float_cells_in_rows.append(float_count)
        year_vals_in_rows.append(year_count)

    max_floats = max(float_cells_in_rows)
    min_nulls = min(null_cells_in_rows)
    first_float_row = 0
    if sum(float_cells_in_rows) > 0:
        for i in range(1, len(float_cells_in_rows)):
            # Use a ratio or special case where we go from zero to some
            if float_cells_in_rows[i] / max_floats > 0.5 or (
                float_cells_in_rows[i] > 0 and float_cells_in_rows[i - 1] == 0
            ):
                first_float_row = i
                break
    first_not_null_row = np.argmin(null_cells_in_rows)

    report = f"Nulls in first {maxn} rows: {str(null_cells_in_rows)}\n"
    report += f"Numeric first {maxn} rows: {str(float_cells_in_rows)}\n"
    report += f"Unique values in first {maxn} rows: {str(unique_vals_in_rows)}\n"
    report += f"Year values in first {maxn} rows: {str(year_vals_in_rows)}\n"
    report += f"HXL row: {str(hxl_row)}\n"

    report += f"\nFirst reduced nulls row: {str(first_not_null_row)}\n"
    report += f"First increased numeric row (excluding years): {str(first_float_row)}\n"

    report_json = {
        "null_cells_in_rows": null_cells_in_rows,
        "float_cells_in_rows": float_cells_in_rows,
        "unique_vals_in_rows": unique_vals_in_rows,
        "year_vals_in_rows": year_vals_in_rows,
        "hxl_row": hxl_row,
        "first_float_row": first_float_row,
        "first_not_null_row": first_not_null_row,
    }

    return report, report_json

wb = openpyxl.load_workbook(filename, data_only=True)
for s in wb.sheetnames:
    sheet = wb[s]
    report, report_json = get_sheet_attributes(sheet, maxn)
    print(report) 

这会产生这样的输出……

Nulls in first 10 rows: [12, 11, 10, 10, 8, 2, 0, 1, 1, 1]
Numeric first 10 rows: [0, 0, 0, 0, 0, 0, 5, 5, 5, 5]
Unique values in first 10 rows: [0, 1, 2, 2, 2, 2, 12, 8, 6, 3]
Year values in first 10 rows: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
HXL row: None

First reduced nulls row: 6
First increased numeric row (excluding years): 6

这些将是我们用于微调模型的提示。

为了创建微调文件的补全,我使用了肯尼亚的人道主义数据交换数据集(有关如何提取 Excel 文件的更多细节,请参见这里)。解析文件并循环遍历每个工作表,我生成了提示。

我使用了以下逻辑来估算数据开始的行号,使用了上述表格参数……

# Make a guess at which row is the data row
datarow = max_not_null_row
# Sometimes we have header rows where none are null, in this case we want to use the row with the most floats
if max_float_row > datarow:
    datarow = max_float_row
# HXL row is always the row before the data row
if hxl_row is not None:
    datarow = hxl_row
# If we a row with a lot of year values below datarow, use that
if year_vals_in_rows[datarow] > 3:
    datarow = datarow + 1

这种基于规则的方法实际上表现得相当不错,但它并不完美,因此需要 GPT-3。尽管如此,它在创建一个大多数补全都准确的测试集时很有用,我只需调整几个逻辑不成立的部分即可。

对于我的训练集,我使用了来自 10 个人道主义提供组织的多个标记为“Kenya”的 Excel 表格中的每个组织的一个表格,其中使用上述基于规则的方法进行了首次数据行的预测。我随后审查了这份清单,并与实际的工作表进行了比较,以纠正电子表格表格开始于不同的行的情况。我排除了本研究中存在多个表格的情况,此后我得到了 232 个这样的微调提示……

{"prompt": "Nulls in first 15 rows: [9, 8, 7, 7, 3, 1, 2, 2, 2, 2, 2]\nNumeric first 15 rows: [0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3]\nUnique values in first 15 rows: [0, 1, 2, 2, 3, 8, 7, 7, 6, 6, 5]\nYear values in first 15 rows: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\nHXL row: None\nFirst reduced nulls row: 5\nFirst increased numeric row (excluding years): 5", "completion": "Data starts at row: 5\n", "meta_data": "./data/Kenya/kenya-hand-washing-statistics-in-bomet-county_118ea93f-83ce-4b86-b1c4-ca54ea9acc8a/Hand_washing_practices_xlsx_efc74f32_ac23_463a_924b_d53c3656b406/Hand washing practices.xlsx"}
{"prompt": "Nulls in first 15 rows: [2, 1, 1, 2, 1, 1, 2, 1, 1, 1, 2, 1, 1, 2, 1]\nNumeric first 15 rows: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\nUnique values in first 15 rows: [0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1]\nYear values in first 15 rows: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\nHXL row: None\nFirst reduced nulls row: 1\nFirst increased numeric row (excluding years): 0", "completion": "Data starts at row: 1\n", "meta_data": "./data/Kenya/shcchealthcare-dataset_02995168-3644-4b78-92be-cdf67275b39d/2018_SHCC_Overview_Data_xlsx_d053b42a_7d31_41b5_a6d9_c8b0a424241c/2018 SHCC Overview Data.xlsx"}
{"prompt": "Nulls in first 15 rows: [6, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 7, 6, 3]\nNumeric first 15 rows: [0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 0, 0, 0]\nUnique values in first 15 rows: [1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 2, 4, 0, 1, 4]\nYear values in first 15 rows: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\nHXL row: None\nFirst reduced nulls row: 1\nFirst increased numeric row (excluding years): 2", "completion": "Data starts at row: 2\n", "meta_data": "./data/Kenya/eastern-southern-africa-refugees-and-idps-situation-and-response-dec2019_e1f9f55e-08db-4166-a787-c7ea9969dc4d/UNICEF_ESARO_Regional_refugee_and_idp_db_2019_November_27_2019_xlsx_0696b7f3_6368_403e_bcb7_eccdc617961f/UNICEF ESARO Regional refugee and idp db 2019 November 27.2019.xlsx"}

附注:在上面的内容中,你可能注意到我为每个提示添加了一个“meta_data”元素。这不是 JSONL 提示记录的必要部分,但我这样做是为了能够轻松将每个提示与文件关联以便于调试。包含这些额外数据的提示文件似乎仍然被 OpenAI 接受,我认为只要有“prompt”和“completion”元素,它就会接受!

然后我微调了一个 DaVinci 模型……

 ai.api_key_path="./api_key.txt"

train_file = './prompts.json'

print("Uploading training file ...")
training_id = cli.FineTune._get_or_upload(train_file, True)

print("Fine-tuning model ...")
create_args = {
    "training_file": training_id,
    "model": "davinci"
}
resp = ai.FineTune.create(**create_args)
job_id = resp["id"]
status = resp["status"]

print(f'Fine-tunning model with jobID: {job_id}.')

我手动检查了微调状态,如下所示……

ai.api_key_path="./api_key.txt"
result = ai.FineTune.retrieve(id=job_id)

print(result['status'])

然后完成后,检索了模型……

model = result["fine_tuned_model"]

对于测试集,我使用了来自训练集之外组织(标记为‘肯尼亚’)的每个 Excel 文件中的一个表格,首先运行上述基于规则的预测生成提示和完成项,然后纠正返回的错误值。再次排除指定了多个表格的 Excel 表格。这给我提供了 72 个提示的测试集。

 def make_gpt3_prediction(prompt, model, temperature=0.99, max_tokens=13):
    """
    Wrapper to call GPT-3 to make a prediction (completion) on a single prompt.
    Also calls post_process() to clean up the prediction.

    Parameters
    ----------
    prompt : str
        Prompt to use for prediction
    model : str
        GPT-3 model to use
    temperature : float
        Temperature to use for sampling
    max_tokens : int
        Maximum number of tokens to use for sampling

    Returns
    -------
    result : dict
        Dictionary with prompt, predicted, predicted_post_processed
    """
    result = {}
    result["prompt"] = prompt
    model_result = ai.Completion.create(
        engine=model,
        prompt=prompt,
        temperature=temperature,
        max_tokens=max_tokens,
        top_p=1,
        frequency_penalty=0,
        presence_penalty=0,
        stop=["\n"],
        logprobs=1,
    )
    result["predicted"] = model_result["choices"][0]["text"].replace(" ", "")
    result["logprobs"] = model_result["choices"][0]["logprobs"]["top_logprobs"]
    return result

def output_prediction_metrics(results, prediction_field="predicted_post_processed"):
    """
    Prints out model performance report if provided results in the format:

    [
        {
            'prompt': ' \'ISO3\' | "[\'RWA\', \'RWA\', \'RWA\', \'RWA\', \'RWA\', \'RWA\', \'RWA\', \'RWA\']"',
            'predicted': ' #country+code+iso3+v_iso3+',
            'expected': '#country+code'
        },
        ... etc ...
    ]

    Parameters
    ----------
    results : list
        See above for format
    prediction_field : str
        Field name of element with prediction. Handy for comparing raw and post-processed predictions.
    """
    y_test = []
    y_pred = []
    for r in results:
        if "expected" not in r:
            print("Provided results do not contain expected values.")
            sys.exit()
        y_pred.append(r[prediction_field])
        y_test.append(r["expected"])

    print(f"There were {len(y_test)} predictions made.")
    print(f"\nPrediction using field {prediction_field} ...\n")
    print(f"Accuracy: {round(accuracy_score(y_test, y_pred),2)}")
    print(
        f"Precision: {round(precision_score(y_test, y_pred, average='weighted', zero_division=0),2)}"
    )
    print(
        f"Recall: {round(recall_score(y_test, y_pred, average='weighted', zero_division=0),2)}"
    )
    print(
        f"F1: {round(f1_score(y_test, y_pred, average='weighted', zero_division=0),2)}"
    )

# File generated by downloading and processing HDX files. See this blog post
# for more details: https://medium.com/towards-data-science/predicting-metadata-for-humanitarian-datasets-using-gpt-3-b104be17716d
country='Kenya'
resources = pd.read_pickle(hdx_resources_pkl_file)

df = resources[(resources["resource_format"]=='XLSX')][["resource_format","file","sheet","dataset_name","dataset_org_title"]]
df.drop_duplicates(inplace=True)
orgs = df["dataset_org_title"].unique()

# Number of rows to use when calculating table row parameters
maxn = 15

# Determine test/train split, 0:10 used for training, 11:len(orgs) for test
dataset_orgs_cutoff = 10

for dataset_org in orgs[dataset_orgs_cutoff: len(orgs)]:
    rows = df.loc[df['dataset_org_title']== dataset_org]
    row = rows.iloc[0]  # Take one sheet from each org to get more variation
    filename = row["file"]
    sheetname = row["sheet"]

    wb = openpyxl.load_workbook(filename, data_only=True)
    for s in wb.sheetnames:
        sheet = wb[s]

        # Extract table attributes 
        report = get_sheet_attributes(sheet, maxn)

        report_elements = report.split('\n\n')
        prompt = report_elements[0] + report_elements[1]
        completion = report_elements[2]

        # Make our GPT-3 prediction
        res = make_gpt3_prediction(prompt, model, temperature=0.0)

        predicted = res["predicted"].split(':')[1].strip()
        actual = completion.split(':')[1].strip()

        results.append({
            "prompt": prompt,
            "predicted": predicted,
            "expected": actual
        })

output_prediction_metrics(results, prediction_field="predicted")

附注:我之前的博客文章中预测 HXL 标签时,我必须通过对数概率过滤完成项,但在这项研究中没有必要。

GPT-3 在我们的测试集中预测了第一行数据的结果如下……

Prediction using field predicted ...

Accuracy: 0.97
Precision: 1.0
Recall: 0.97
F1: 0.99

所以 GPT-3 在预测第一行数据的位置上表现不错。

综合起来

步骤 1 — 读取我们的数据

示例电子表格,具有不同的层级标题和单元格中的备注

步骤 2 — 取消合并的列并填充合并值

Pandas 数据框在通过‘pad_merged_cells’函数处理后,用于取消合并并填充合并值

步骤 3 — 计算表格参数以生成 GPT-3 提示

Nulls in first 10 rows: [20, 20, 20, 21, 10, 8, 19, 9, 21, 0]
Numeric first 10 rows: [0, 0, 0, 0, 0, 0, 0, 0, 0, 14]
Unique values in first 10 rows: [1, 1, 1, 0, 11, 13, 2, 4, 0, 21]
Year values in first 10 rows: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
HXL row: None

First reduced nulls row: 9
First increased numeric row (excluding years): 9

步骤 4 — 调用 GPT-3 预测数据行的起始位置

GPT-3 prediction: 9

步骤 5 — 现在我们知道了数据行的开始位置,将上方的列标题连接成一行

解析后的表格,具有折叠的层级列,没有随机标签。现在可以导入到数据库中。

这是一个我们可以上传到关系数据库的好表格。有关完整代码,请参见下面的参考部分。

诚然,手动解析这个表格并指定一些与我们发现的表格参数相关的规则是很容易的,但上述过程的重点是它可以应用于人道主义数据交换数据集中成千上万的 Excel 表格的广泛表格布局。

结论与未来工作

尽管零次和一次提示具有很大的潜力,但在用 CSV 表格进行提示时,这种方法尚未对这个特定任务奏效。随着大型语言模型的进步,这种情况可能会改变——我很期待 GPT-4 的表现——但目前看来,微调是更好的选择,它可以预测关键的表格属性,用于重新格式化。当然,这种方法需要一些预处理,以确定提示的表格参数。值得注意的是,使用表格‘特征’时,它更像是分类任务而不是文本完成,可能会更适合这样框架。不过,无论如何,这种技术在使用人道主义数据交换 Excel 文件时表现良好。

我认为将这项工作扩展到处理 Excel 工作表上有多个表格的情况将非常有趣。这需要比我在这项研究中使用的更多的表格特征,比如单元格格式和列(而不是行)属性。

更多有趣内容敬请期待!

参考文献

[1] Haoyu Dong 等人,TableSense: 使用卷积神经网络进行电子表格表格检测 (2021)

[2] Brown 等人,语言模型是少样本学习者 (2020)。

[3] OpenAI Cookbook: 提高可靠性的技术

[4] Kojima 等人,大型语言模型是零样本推理者

这个分析的代码可以在这个笔记本中找到。

粒子群优化:搜索过程可视化

原文:towardsdatascience.com/particle-swarm-optimization-search-procedure-visualized-4b0364fb3e5a

直觉 + 数学 + 代码,为从业者提供

詹姆斯·柯博士数据科学前沿 詹姆斯·柯博士

·发表于数据科学前沿 ·阅读时间 9 分钟·2023 年 12 月 1 日

--

图像由 DALL·E 3 根据提示“绘制一幅科幻主题的图像,描绘一群无人机在城市景观中寻找目标”创建

人类喜欢模仿自然界中的许多事物。

我们模仿青蛙游泳。我们通过在飞机上安装翅膀来模仿鸟类以提供升力。我们模仿起重机/蛇/螳螂来进行武术。我们模仿白蚁来建造具有高效温控的结构(参见东门大厦)。

这也扩展到了数学算法中,例如你可能听说过蜜蜂算法、蚂蚁算法、布谷鸟搜索算法和萤火虫算法。我之前还谈过进化算法,它遵循自然选择的原则。

今天,我将讲解 PSO——粒子群优化。在本文的最后,你将获得能够实现解决方案的代码,并生成一个 gif 来可视化搜索过程。

使用案例

在高维空间中寻找最优解是困难的。刚接触机器学习的学生可能在第一周就听说过“维度诅咒”这个术语。

高维空间不仅仅是一个抽象的数学概念。考虑一个供应链问题。一家公司必须决定在哪里建立生产工厂、仓库、分销中心和零售店。为了简单起见,我们假设每种设施只有一个。这已经使我们要寻找的解决方案是 8 维的——(x₁, y₁, x₂, y₂, x₃, y₃, x₄, y₄)。

由 DALL·E 3 根据提示“绘制一个包含四个关键设施——生产工厂、仓库、配送中心和零售店——的全景视图”创建的图像。

绝对位置(x₁, y₁, x₂, y₂, x₃, y₃, x₄, y₄)影响每个设施之间的相对位置。绝对位置和相对位置都会影响运营成本以及预期收益,从而影响利润。虽然不完全准确,但我们可以大致说搜索空间和对目标函数的影响是连续的。

问题的公式化

实际上,将输入(候选解)映射到输出(目标)的底层函数是一个黑箱,无法用数学方法表示。

如果可以的话,可以直接获得解析解。然而,在处理黑箱时,我们将进行采样。一个天真的方法是执行网格搜索。在本文末尾,你将掌握更好的工具,更重要的是,理解为什么它有效。

让我们将供应链问题和黑箱转化为以下数学方程式(以便于操作)。同时,我们将导入所有需要的库。

import math
import numpy as np
import random
import matplotlib.pyplot as plt
from tqdm import tqdm

def blackbox(x1, y1, x2, y2, x3, y3, x4, y4):
    return math.sqrt((x1+2)**2 + (y1-3)**2) * \
            math.sin(
                math.atan2(2*(y2-4), 3*(x2+1)) * (
                    math.cos(x3-x1) + math.sin(y3-y1)
                    + math.cos(3*(y4+3))
                    + math.sin(2*(x4-2))
                ) 
            )

注意一切是如何被故意纠缠在一起的。这是因为如果使用一个简单的组件加法函数(例如,下方所示),可以执行 O(n²)的网格搜索而不是 O(n⁸)。这也与现实不一致,因为我们不能天真地解决生产设施和零售店位置独立的最佳仓库位置问题。

def blackbox_addition(x1, y1, x2, y2, x3, y3, x4, y4):
    return np.sin(x1) * np.cos(y1) \
            + np.cos(x2) * np.sin(y2) \
            + np.sin(x3) * np.sin(y3) \
            + np.cos(x4) * np.cos(y4)

问题的目标是找到(x₁, y₁, x₂, y₂, x₃, y₃, x₄, y₄),使得blackbox给出的输出尽可能高,而不知道底层方程。为了使问题更具现实性,我们在采样过程中加入噪声。当然,我们可以简单地在每个点重复测量并取平均值,但一个鲁棒的搜索算法即使没有这样做也应能表现良好。

class Task:
    def __init__(self):
        pass

    def score(self, x_arr, with_noise=True):
        if with_noise:
            return blackbox(*x_arr) + 0.1*np.random.randn()
        else:
            return blackbox(*x_arr)

解决方案的公式化

让我们从 PSO 的基本构建块——粒子开始。一个粒子是一个候选解向量,携带其个人最佳解和一组向量,这些向量决定了个人影响和社会影响的程度。在每次迭代中,每个粒子i根据其速度探索新的解:

所有图像均为作者提供,除非另有说明。

该速度受其惯性控制,同时也受到个人最佳解和群体全局最佳解的倾斜影响。[1]

每个粒子的速度由三个组件决定。

w是粒子继续沿其先前轨迹的倾向,而ϕ₁和ϕ₂可以看作是对个人和全球最佳的学习率。拥有惯性是个好主意,因为如果我们之前已经朝着一个假设的良好方向前进,那么继续保持是合理的。事实上,这种动量概念也存在于 Adam 优化器中,用于在梯度下降过程中更新神经网络的权重。

个人最佳,甚至全球最佳,是基于迄今为止发现的内容。可能有些方面(即维度)较好,至少相对而言,但不是全部。然而,我们不知道哪些维度应该保留,因此只能尝试各种组合。秉持进化算法的精神,重要的是最佳粒子而不是平均种群,因此探索时只有上升潜力。

将这个概念转化为数学方程,我们有向量U₁和U₂从均匀分布中随机抽取,乘以各自的学习率ϕ₁和ϕ₂。每个粒子对其个人最佳和全球最佳有不同维度上的亲和力。综合来看,我们可以为Particle类写出如下代码。

class Particle:
    def __init__(self, idx, n_dim, param_limits=(-1,1)):
        self.idx = idx
        self.low = param_limits[0]
        self.high = param_limits[1]
        self.pos = np.random.uniform(
            low=self.low, high=self.high, size=(n_dim,)
        )
        self.w = np.random.uniform(0.1, 0.5)
        self.r1 = np.random.uniform(0, 1, size=(n_dim,))
        self.r2 = np.random.uniform(0, 1, size=(n_dim,))
        self.vel = np.random.normal(size=n_dim)
        self.pbest = np.zeros(n_dim,)
        self.pbest_fitness = -1e8

    def update(self, gbest, task):
        self.vel = self.w * self.vel \
                + self.r1 * (self.pbest - self.pos) \
                + self.r2 * (gbest - self.pos)

        self.vel = np.clip(self.vel, 0.2*self.low, 0.2*self.high)
        self.pos = np.clip(self.pos + self.vel, self.low, self.high)

        fitness = task.score(self.pos)
        if fitness > self.pbest_fitness:
            self.pbest_fitness = fitness
            self.pbest = deepcopy(self.pos)
        return self.pos

注意,我将ϕU₁和ϕU₂分别合并为r₁和r₂,以便(非常轻微地)节省计算。实际上,我将学习率规定为 1 的数量级,以节省这个简单问题的时间。你也可以使用random.uniform(0,0.1)或你认为合适的其他范围。

接下来,一个群体包含大量粒子。每个粒子是独立创建的,并且对个人最佳和全球最佳的不同维度有其独特的亲和力。在每次迭代中,所有粒子根据其‘速度’搜索附近的解决方案,并跟踪其速度,如本节开头所示。

class Swarm:
    def __init__(self, n_dim, n_population, param_limits):
        self.n_dim = n_dim
        self.particles = [Particle(i+1, n_dim, param_limits) for i in range(n_population)]
        self.reset()

    def reset(self):
        self.gbest = np.zeros(self.n_dim,)
        self.gbest_fitness = -1e8
        self.history = {}

    def update_all_particles(self, task):
        for particle_Obj in self.particles:
            particle_Obj.update(self.gbest, task)

    def solve(self, task, num_generation):
        self.reset()
        for t in tqdm(range(num_generation)):
            self.update_all_particles(task)
            particles_pbest_fitness = [task.score(p.pos) for p in self.particles]
            self.history[t] = dict([(p, deepcopy(p.pos)) for p in self.particles])
            if np.max(particles_pbest_fitness) > self.gbest_fitness:
                self.gbest_fitness = np.max(particles_pbest_fitness)
                self.gbest_index = np.argmax(particles_pbest_fitness)
                self.gbest_particle = self.particles[self.gbest_index]
                self.gbest = self.gbest_particle.pos                
        return self.gbest

实现可以如下完成。这里,种群包括 2000 个粒子作为示例。每个粒子都有一个噪声测量值,但噪声会从最终解决方案中去除,以获得算法性能的准确度量。注意,gbest不需要在每次迭代中更新。这是因为没有保证每次解决方案都会改进。

position_min, position_max = -5, 5
swarm = Swarm(
    n_dim=8, n_population=2000, param_limits=(position_min, position_max)
)
solution = swarm.solve(task, num_iteration=200)

print(solution)
print(task.score(solution, with_noise=False))

结果

仅需几秒钟即可找到解决方案。为了公平起见,我重复了十次实验,每次循环都删除swarm并重新初始化所有内容。结果不可避免地有所不同,因为每个粒子都被随机初始化,侧重于不同的组件。

进行 10 次 PSO,每次有 200 次迭代。

如果没有 PSO 或其他优秀的搜索算法,你的替代方案是进行暴力网格搜索。让我们看看这种方法与 PSO 的比较如何。

best_score = -np.inf
solution = None
intervals = 11

for x1 in tqdm(np.linspace(position_min, position_max, intervals)):
    for y1 in np.linspace(position_min, position_max, intervals):
        for x2 in np.linspace(position_min, position_max, intervals):
            for y2 in np.linspace(position_min, position_max, intervals):
                for x3 in np.linspace(position_min, position_max, intervals):
                    for y3 in np.linspace(position_min, position_max, intervals):
                        for x4 in np.linspace(position_min, position_max, intervals):
                            for y4 in np.linspace(position_min, position_max, intervals):
                                sample = task.score(np.array([x1,y1,x2,y2,x3,y3,x4,y4]))
                                if sample > best_score:
                                    best_score = sample
                                    solution = np.array([x1,y1,x2,y2,x3,y3,x4,y4])

print("solution: ", solution)
print(task.score(solution, with_noise=False))

即使在每个维度上仅进行 11 个点(即分成十分之一)的极其粗略搜索,也需要进行 10⁸次黑箱函数计算。在我的计算机上,这需要超过 20 分钟。请注意,这是在“最佳性能”模式下,并且背景中没有其他程序运行;甚至没有音乐(我在等待期间去喝了咖啡)。在现实中,每次计算可能涉及一个需要更长时间的模拟。

完成八维网格搜索所需的时间,以及对应的最佳解决方案和目标函数值。

仅使用极少的计算预算,PSO 解决了问题(中位数 10.61,最大 10.63),效果与蛮力网格搜索(最终值 10.61)一样好。值得注意的是,我们这里只涉及 8 维。如果涉及 1000 维等问题,网格搜索显然不可行,而 PSO 的好处将得到放大。

可视化

让我们看看粒子在迭代中的分布情况。尽管在 8 维中可视化事物是不可能的,我们可以每次关注两个维度,同时忽略其他组件。

在下面的图中,热图展示的是在仅改变两个维度的同时,保持其他 6 个维度为该特定迭代的swarm.gbest_particle值的目标值。因此,每个快照实际上仅适用于一个粒子(且没有实际方法同时呈现 2000 个热图)。尽管如此,它仍然提供了一些想法,胜于空白背景。

x_range = np.linspace(-5, 5, 100)
y_range = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x_range, y_range)
vectorized_blackbox = np.vectorize(blackbox)

for t in [1,2,3,4,5,6,8,10,12,15,20,30,50,100,200]:
    data = np.array(list(swarm.history[t-1].values()))
    ref = data[swarm.gbest_particle.idx-1,:]
    heatmap = {
        1: vectorized_blackbox(X, Y, ref[2], ref[3], ref[4], ref[5], ref[6], ref[7]),
        2: vectorized_blackbox(ref[0], ref[1], X, Y, ref[4], ref[5], ref[6], ref[7]),
        3: vectorized_blackbox(ref[0], ref[1], ref[2], ref[3], X, Y, ref[6], ref[7]),
        4: vectorized_blackbox(ref[0], ref[1], ref[2], ref[3], ref[4], ref[5], X, Y)
    }

    fig, axs = plt.subplots(2, 2, figsize=(10, 10))
    for i in range(2):
        for j in range(2):
            c = axs[i,j].contourf(X, Y, heatmap[2*i+j+1], vmin=0, vmax=12, levels=50, alpha=0.5)
            axs[i,j].scatter(data[:, 2*i+j], data[:, 2*i+j+1], s=2, alpha=0.4)
            axs[i,j].scatter(data[swarm.gbest_particle.idx-1, 2*i+j], data[swarm.gbest_particle.idx-1, 2*i+j+1], c='w', s=5, alpha=1)
            axs[i,j].set_xlabel("x_%d"%(2*i+j+1))
            axs[i,j].set_ylabel("y_%d"%(2*i+j+1))
            axs[i,j].set_xlim([-5, 5])
            axs[i,j].set_ylim([-5, 5])
    fig.colorbar(c, ax=axs)
    plt.suptitle("Distribution of particles at iteration %d"%t)
    plt.savefig("plot_%03d.png"%t)

使用imageio库创建 gif,如之前共享的,我们可以将所有图形组合成以下 gif。

白点表示swarm.gbest_particle的位置,而其他 1999 个粒子由较小的蓝点表示。

可能会很诱人急于得出结论,声称 PSO 代理是“愚蠢的”,只需稍微朝地图上更亮的区域移动即可。这样的论点是有缺陷的。我们看到的热图只是多维解决空间的一部分,正如 gif 中所示,当其他维度取不同的值时,它可以有很大变化。这不仅仅是贪婪地逐个优化组件维度的问题。如果采用了不同的表示组合,比如说x₁与y₃,或x₄与y₂,情况会有所不同。没有一种万全的技术可以同时让所有人满意。

最终,结果说明了一切,PSO 在几秒钟内解决问题的事实证明了其有效性。

结论

在这篇文章中,我们探讨了如何高效地实现 PSO 来寻找未知黑箱的最优解,并了解了实现这一目标的幕后过程。

我设想继续进行一系列以自然为灵感的算法。毕竟,自然是美丽的。数学是美丽的。自然中的数学?这必定是最棒的。

在我的下一篇文章中,我将解释 ABC(人工蜂群)的实现方法,并与 PSO 进行比较,探讨 ABC 在何种问题上表现更佳。敬请关注。

参考文献

[1] A. E. Eiben 和 J. E. Smith, 进化计算导论 (2015), Springer-Verlag Berlin Heidelberg

PatchTST:时间序列预测中的突破

原文:towardsdatascience.com/patchtst-a-breakthrough-in-time-series-forecasting-e02d48869ccc

从理论到实践,了解 PatchTST 算法,并在 Python 中与 N-BEATS 和 N-HiTS 一起应用。

Marco PeixeiroTowards Data Science Marco Peixeiro

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

--

图片由 Ray Hennessy 提供,来源于 Unsplash

基于变换器的模型已成功应用于许多领域,如自然语言处理(例如 BERT 或 GPT 模型)和计算机视觉等。

然而,在时间序列方面,最先进的结果主要由 MLP 模型(多层感知器)如 N-BEATS 和 N-HiTS 实现。最近的一篇论文甚至表明,简单的线性模型在许多基准数据集上优于复杂的基于变换器的预测模型(见 Zheng 等人, 2022)。

尽管如此,一种新的基于变换器的模型已经被提出,并在长期预测任务中取得了最先进的结果:PatchTST

PatchTST 代表补丁时间序列变换器,它首次由 Nie、Nguyen 等人在 2023 年 3 月提出,详细介绍在他们的论文中:时间序列值得 64 个词:使用变换器进行长期预测。他们提出的方法在与其他基于变换器的模型比较时,取得了最先进的结果。

在这篇文章中,我们首先直观地探索 PatchTST 的内部工作原理,而不使用任何方程式。接着,我们将该模型应用于预测项目,并将其性能与 MLP 模型(如 N-BEATS 和 N-HiTS)进行比较和评估。

当然,关于 PatchTST 的更多细节,请参考 原始论文

使用我的 免费时间序列备忘单 在 Python 中学习最新的时间序列分析技术!获取统计和深度学习技术的实现,全部使用 Python 和 TensorFlow!

让我们开始吧!

探索 PatchTST

如前所述,PatchTST 代表补丁时间序列 transformer。

正如名字所示,它利用了补丁和 transformer 架构。它还包括通道独立性来处理多变量时间序列。下面展示了总体架构。

PatchTST 模型架构。我们看到该模型利用通道独立性来处理多变量时间序列。在 transformer 主干中,我们还看到使用了补丁(由矩形表示)。此外,模型有两个版本:监督学习和自监督学习。图片由 Nie Y.、Nguyen N.、Sinthong P.、Kalagnanam J. 提供,来源于 “一个时间序列值 64 个词:使用 Transformer 进行长期预测”

从上图中需要收集大量信息。这里的关键要素是,PatchTST 使用通道独立性来预测多变量时间序列。然后,在其 transformer 主干中,该模型使用补丁,这些补丁由小的垂直矩形表示。此外,该模型有两个版本:监督学习和自监督学习。

让我们更详细地探讨 PatchTST 的架构和内部工作原理。

通道独立性

在这里,多变量时间序列被视为多通道信号。每个时间序列基本上是一个包含信号的通道。

PatchTST 模型概述。在这里,我们特别强调通道独立性的实现,其中每个输入 token 到 Transformer 主干仅包含来自一个通道或一个时间序列的信息。图片由 Nie Y.、Nguyen N.、Sinthong P.、Kalagnanam J. 提供,来源于 “一个时间序列值 64 个词:使用 Transformer 进行长期预测”

在上图中,我们看到如何将多变量时间序列分离成单独的序列,并将每个序列作为输入 token 传递给 Transformer 主干。然后,对每个序列进行预测,结果被连接起来形成最终预测。

补丁

大多数基于 Transformer 的预测模型的工作集中在构建新的机制来简化原始注意力机制。然而,它们仍然依赖于逐点注意力,这在时间序列的情况下并不理想。

在时间序列预测中,我们希望提取过去时间步和未来时间步之间的关系以进行预测。使用逐点注意力时,我们试图从单个时间步中检索信息,而不考虑该点周围的内容。换句话说,我们孤立一个时间步,而不查看之前或之后的点。

这就像试图理解一个词的意义而不看句子中的其他词一样。

因此,PatchTST 利用分块来提取时间序列中的局部语义信息。

分块如何工作

每个输入序列被分成块,这些块只是来自原始序列的较短序列。

PatchTST 的 Transformer 主干。这里,我们看到输入时间序列(图底部)经过分块,结果是多个块(垂直矩形),然后被送到 Transformer 编码器。图片由 Nie Y.、Nguyen N.、Sinthong P.、Kalagnanam J. 提供,来自 A Time Series is Worth 64 Words: Long-Term Forecasting with Transformers

这里,块可以是重叠的也可以是不重叠的。块的数量取决于块的长度 P 和步幅 S。这里,步幅类似于卷积,它只是分隔连续块开始的时间步数。

可视化分块。这里,我们有一个 15 个时间步的序列,分块长度为 5,步幅也为 5,结果是三个块。图片由作者提供。

在上图中,我们可以可视化分块的结果。这里,我们有一个序列长度 (L) 为 15 个时间步,分块长度 (P) 为 5,步幅 (S) 为 5。结果是序列被分为 3 个块。

分块的优势

通过分块,模型可以通过查看时间步的组而不是单个时间步来提取局部语义信息。

它还有一个额外的好处,就是大大减少了送入 Transformer 编码器的标记数量。这里,每个块成为输入到 Transformer 的输入标记。这样,我们可以将标记数量从 L 减少到大约 L/S

这样,我们大大减少了模型的空间和时间复杂度。这反过来意味着我们可以给模型输入更长的序列,以提取有意义的时间关系。

因此,通过分块,模型变得更快、更轻,并且可以处理更长的输入序列,这意味着它可能会对序列学习得更多并做出更好的预测。

Transformer 编码器

一旦序列被分块,它会被送入 Transformer 编码器。这是经典的 Transformer 架构,没有任何修改。

然后,将输出送入线性层,并进行预测。

使用表示学习改进 PatchTST

论文的作者建议通过使用表示学习来改进模型。

在 PatchTST 中可视化自监督表示学习。在这里,模型将随机掩盖补丁并学习重建它们。图片由 Nie Y.、Nguyen N.、Sinthong P.、Kalagnanam J.提供,来自A Time Series is Worth 64 Words: Long-Term Forecasting with Transformers

从上图中,我们可以看到 PatchTST 可以利用自监督表示学习来捕捉数据的抽象表示。这可能会导致预测性能的潜在改善。

在这里,过程相当简单,因为随机补丁将被掩盖,意味着它们将被设置为 0。这在上图中由空白的垂直矩形表示。然后,模型被训练以重建原始补丁,这就是图顶端输出的灰色垂直矩形。

现在我们对 PatchTST 的工作原理有了很好的理解,让我们将其与其他模型进行测试,看看其表现如何。

使用 PatchTST 进行预测

在论文中,PatchTST 与其他基于 Transformer 的模型进行了比较。然而,最近也发布了基于 MLP 的模型,如 N-BEATS 和 N-HiTS,它们在长期预测任务中也展示了最先进的性能。

本节的完整源代码可在GitHub上找到。

在这里,让我们应用 PatchTST,以及 N-BEATS 和 N-HiTS,并评估它在这两个基于 MLP 的模型中的表现。

对于这个练习,我们使用 Exchange 数据集,这是研究中用于长期预测的常见基准数据集。该数据集包含 1990 年至 2016 年期间相对于美元的八个国家的每日汇率。该数据集通过 MIT 许可证提供。

初始设置

让我们首先导入所需的库。在这里,我们将使用neuralforecast,因为它们有一个现成的 PatchTST 实现。对于数据集,我们使用datasetsforecast库,其中包含所有流行的数据集,用于评估预测算法。

import torch
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from neuralforecast.core import NeuralForecast
from neuralforecast.models import NHITS, NBEATS, PatchTST

from neuralforecast.losses.pytorch import MAE
from neuralforecast.losses.numpy import mae, mse

from datasetsforecast.long_horizon import LongHorizon

如果你已经安装了 CUDA,那么neuralforecast将自动利用你的 GPU 来训练模型。在我这边,我没有安装 CUDA,这就是为什么我没有进行广泛的超参数调整或在非常大的数据集上训练的原因。

完成这一步后,让我们下载 Exchange 数据集。

Y_df, X_df, S_df = LongHorizon.load(directory="./data", group="Exchange")

在这里,我们看到得到三个 DataFrames。第一个包含每个国家的每日汇率。第二个包含外生时间序列。第三个包含静态外生变量(如日期、月份、年份、小时或我们已知的任何未来信息)。

对于这个练习,我们只使用Y_df

然后,确保日期具有正确的类型。

Y_df['ds'] = pd.to_datetime(Y_df['ds'])

Y_df.head()

Exchange 数据集的前五行。作者提供的图片。

在上图中,我们看到有三列。第一列是唯一标识符,当使用neuralforecast时,需要有一个 id 列。然后,ds列有日期,y列有汇率。

Y_df['unique_id'].value_counts()

显示每个唯一 id 的观测数量。图片由作者提供。

从上图可以看出,每个唯一 id 对应一个国家,我们每个国家有 7588 个观测值。

现在,我们定义验证集和测试集的大小。在这里,我选择了 760 个时间步作为验证集,1517 个时间步作为测试集,如[datasets](https://github.com/Nixtla/datasetsforecast/blob/main/datasetsforecast/long_horizon.py) 所规定。

val_size = 760
test_size = 1517

print(n_time, val_size, test_size)

然后,让我们绘制其中一个序列,看看我们在处理什么。在这里,我决定绘制第一个国家的序列(unique_id = 0),但可以自由绘制其他序列。

u_id = '0'

x_plot = pd.to_datetime(Y_df[Y_df.unique_id==u_id].ds)
y_plot = Y_df[Y_df.unique_id==u_id].y.values

x_plot

x_val = x_plot[n_time - val_size - test_size]
x_test = x_plot[n_time - test_size]

fig, ax = plt.subplots(figsize=(12,8))
ax.plot(x_plot, y_plot)
ax.set_xlabel('Date')
ax.set_ylabel('Exhange rate')
ax.axvline(x_val, color='black', linestyle='--')
ax.axvline(x_test, color='black', linestyle='--')

plt.text(x_val, -2, 'Validation', fontsize=12)
plt.text(x_test,-2, 'Test', fontsize=12)

plt.tight_layout()

第一个国家从 1990 年到 2016 年的每日汇率。图片由作者提供。

从上图中,我们看到数据相当嘈杂,没有明显的季节性。

建模

探索了数据后,让我们开始使用neuralforecast进行建模。

首先,我们需要设置预测期。在这种情况下,我使用 96 个时间步,因为这个预测期也在PatchTST 论文中使用。

然后,为了公平评估每个模型,我决定将输入大小设置为预测期的两倍(即 192 个时间步),并将最大训练轮数设置为 50。所有其他超参数保持默认值。

horizon = 96

models = [NHITS(h=horizon,
               input_size=2*horizon,
               max_steps=50),
         NBEATS(h=horizon,
               input_size=2*horizon,
               max_steps=50),
         PatchTST(h=horizon,
                 input_size=2*horizon,
                 max_steps=50)]

然后,我们初始化NeuralForecast对象,指定我们要使用的模型和预测频率,在这里是每日。

nf = NeuralForecast(models=models, freq='D')

我们现在准备进行预测。

预测

为了生成预测,我们使用cross_validation方法利用验证集和测试集。它将返回一个包含所有模型预测值及相关真实值的 DataFrame。

preds_df = nf.cross_validation(df=Y_df, val_size=val_size, test_size=test_size, n_windows=None)

预测 DataFrame 的前五行。图片由作者提供。

如您所见,对于每个 id,我们有来自每个模型的预测以及y列中的真实值。

现在,为了评估模型,我们需要将实际值和预测值的数组重塑为形状为(series 的数量, 窗口数量, 预测期)

y_true = preds_df['y'].values
y_pred_nhits = preds_df['NHITS'].values
y_pred_nbeats = preds_df['NBEATS'].values
y_pred_patchtst = preds_df['PatchTST'].values

n_series = len(Y_df['unique_id'].unique())

y_true = y_true.reshape(n_series, -1, horizon)
y_pred_nhits = y_pred_nhits.reshape(n_series, -1, horizon)
y_pred_nbeats = y_pred_nbeats.reshape(n_series, -1, horizon)
y_pred_patchtst = y_pred_patchtst.reshape(n_series, -1, horizon)

完成后,我们可以选择性地绘制模型的预测。在这里,我们绘制了第一个序列的第一个窗口中的预测。

fig, ax = plt.subplots(figsize=(12,8))

ax.plot(y_true[0, 0, :], label='True')
ax.plot(y_pred_nhits[0, 0, :], label='N-HiTS', ls='--')
ax.plot(y_pred_nbeats[0, 0, :], label='N-BEATS', ls=':')
ax.plot(y_pred_patchtst[0, 0, :], label='PatchTST', ls='-.')
ax.set_ylabel('Exchange rate')
ax.set_xlabel('Forecast horizon')
ax.legend(loc='best')

plt.tight_layout()

第一个序列在第一个窗口中的每日汇率预测。图片由作者提供。

这个图有点让人失望,因为 N-BEATS 和 N-HiTS 的预测结果与实际值差异很大。然而,尽管 PatchTST 也有偏差,但似乎最接近实际值。

当然,我们必须对此持保留态度,因为我们只是可视化了一个序列中的一个预测窗口。

评估

所以,让我们评估每个模型的表现。为了复制论文中的方法,我们使用 MAE 和 MSE 作为性能指标。

data = {'N-HiTS': [mae(y_pred_nhits, y_true), mse(y_pred_nhits, y_true)],
       'N-BEATS': [mae(y_pred_nbeats, y_true), mse(y_pred_nbeats, y_true)],
       'PatchTST': [mae(y_pred_patchtst, y_true), mse(y_pred_patchtst, y_true)]}

metrics_df = pd.DataFrame(data=data)
metrics_df.index = ['mae', 'mse']

metrics_df.style.highlight_min(color='lightgreen', axis=1)

所有模型的性能。在这里,PatchTST 实现了最低的 MAE 和 MSE。图片由作者提供。

在上表中,我们看到 PatchTST 是冠军模型,因为它实现了最低的 MAE 和 MSE。

当然,这不是最彻底的实验,因为我们只使用了一个数据集和一个预测范围。尽管如此,看到一个基于 Transformer 的模型能够与最先进的 MLP 模型竞争仍然很有趣。

结论

PatchTST 是基于 Transformer 的模型,通过补丁来提取时间序列数据中的局部语义意义。这使得模型的训练速度更快,并且具有更长的输入窗口。

相较于其他基于 Transformer 的模型,它已经达到了最先进的性能。在我们的练习中,我们看到它的表现也优于 N-BEATS 和 N-HiTS。

虽然这并不意味着它比 N-HiTS 或 N-BEATS 更好,但它仍然是一个有趣的选项,尤其是在长期预测时。

感谢阅读!希望你喜欢这篇文章,并且学到了一些新东西!

想要掌握时间序列预测?那就看看Python 应用时间序列预测。这是唯一一个通过 15 个引导实践项目使用 Python 实现统计学、深度学习和最先进模型的课程。

干杯 🍻

支持我

喜欢我的工作吗?通过给我买杯咖啡来表示支持,这是鼓励我的简单方式,我可以享受一杯咖啡!如果你愿意,请点击下面的按钮 👇

参考文献

时间序列的价值为 64 个词:使用 Transformer 进行长期预测 作者:Nie Y., Nguyen N. 等。

Neuralforecast 作者:Olivares K., Challu C., Garza F., Canseco M., Dubrawski A.

多变量时间序列的主成分分析:动态高维数据的预测

原文:towardsdatascience.com/pca-for-multivariate-time-series-forecasting-dynamic-high-dimensional-data-ab050a19e8db

噪声和序列相关性存在下的系统预测

Marco Cerliani数据科学前沿 Marco Cerliani

·发表在数据科学前沿 ·5 分钟阅读·2023 年 1 月 31 日

--

图片由Viva Luna Studios提供,来源于Unsplash

多步骤的多变量时间序列预测被认为是一个复杂的预测任务。我们必须考虑到输入和输出的高维度;必须充分处理横截面和时间依赖关系;最后但同样重要的是,我们必须确保长时间准确性的可接受水平。

如今,处理时间和数量维度巨大数据的分析应用非常普遍。因此,所有基于这些系统构建的解决方案必须能够操作大数据集。在物联网(IoT)时代,处理大量时间序列是很常见的,这些时间序列在大多数情况下展示了强相关模式。这些动态在电信、工业制造、金融、电网等领域非常常见。

假设你是一个数据科学家。我们负责开发一个预测分析应用,提供多步骤的预测,针对由相关和噪声传感器组成的物联网系统。多变量预测在文献中广泛讨论。从 VAR(向量自回归模型)等统计技术到更复杂的基于深度学习的方法,存在许多可用于预测任务的解决方案。然而,现实世界比预期的更复杂和残酷。实时管理大量高频传感器需要开发兼具适当准确度和合理响应延迟的解决方案

在这篇文章中,我们尝试开发一个用于多变量和多步骤传感器预测的预测应用程序,该应用程序可以在接近实时模式下使用。这是通过将降维与适合多变量上下文的预测技术相结合来实现的。所提出的方法在经济预测文献中很受欢迎,被称为动态因子建模[1]。换句话说,我们将最喜欢的预测算法叠加在降维技术(如 PCA)的结果之上,以预测未来的系统动态。

实验设置

在本文范围内,我们生成了多个合成时间序列。这些序列可以根据其正弦动态分成两组。一切都很完美,只是信号被噪声掩盖了。

数据中的合成动态 [image by the author]

我们在时间序列中加入了大量噪声,以复制现实世界系统的混沌行为,使预测任务变得更加困难。

带有噪声的模拟序列预测 [image by the author]

拥有多个时间序列可供使用,我们的目标是预测它们未来的多个步骤。为了以接近实时的模式进行多变量预测任务,我们应该在预测准确性和推理过程的持续时间之间找到一个折衷

让我们看看如何解决这个问题。

多变量动态预测

动态因子建模(DFM)是一种源自经济文献的多变量预测技术[1]。DFM 的基本思想是少量系列可以解释大量变量的时间行为。如果我们可以获得这些因素的准确估计,整个预测任务可以通过使用估计的动态因素来简化,而不是使用所有系列。

动态因子建模估计流程 [image by the author]

使用 DFM 获得的预测质量取决于两个主要方面:因素估计的优良性和因素预测的准确性。估计动态因素的方式有很多种。在机器学习生态系统中最常见的方法是通过正交旋转(PCA)获得一组数据的主成分[2]。

DFM 也是一种与模型无关的技术。换句话说,任何降维和任何预测策略都可以用于进行预测。对于我们的实验,我们使用标准 PCA 和朴素直接预测。下面是进行 DFM 估计和预测的代码快照。

scaler_pca = make_pipeline(StandardScaler(), PCA(n_components=2))

X_train_factors = scaler_pca.fit_transform(X_train)

forecaster = ForecastingChain(
    Ridge(),
    n_estimators=test_size,
    lags=range(1,25),
    use_exog=False,
    n_jobs=-1,
).fit(None, X_train_factors)

y_pred_factors = forecaster.predict(np.arange(test_size))

y_pred = scaler_pca.steps[0][-1].inverse_transform(
    scaler_pca.steps[-1][-1].inverse_transform(y_pred_factors)
)

最后的步骤是通过简单地对我们所有可用的系列采用多变量直接预测来解决相同的任务。无论结果如何,这种方法都不可持续,因为它需要对所有可用系列进行滞后特征计算。这可能需要处理大量的滞后变量,这使得大多数系统由于物理(内存)和时间限制而无法进行预测

我们采用了时间交叉验证策略来验证这两种方法的结果并存储性能数据。

两种方法预测结果的视觉比较 [作者提供的图像]

DFM 超越了幼稚的多变量直接预测。在较短时间内(推断/估计时间依赖于系统变量的数量)实现了更高的准确性。检查生成的预测,我们可以观察到 DFM 能够正确区分并再现原始系列中存在的双正弦波动态。

多变量预测性能比较 [作者提供的图像]

总结

在这篇文章中,我们提出了动态因子建模的实际应用。它被证明是一种有效的多变量时间序列预测建模方法。它特别适用于预测高维数据,同时也显示出可能的高噪声水平。正如往常一样,不存在适用于所有情况的完美预测技术。作为数据科学家,我们有责任尝试以前未曾了解的技术。只有通过持续的自我学习,我们才能选择和区分适用于日常任务的最佳解决方案

查看我的 GitHub 仓库

保持联系: Linkedin

参考文献

[1] M. Forni, M. Hallin, M. Lippi 和 L. Reichlin, “广义动态因子模型”,《美国统计学会杂志》,第 100 卷,第 471 期,第 830–840 页。

[2] G. Bontempi, Y. -A. Le Borgne 和 J. de Stefani, “动态因子机器学习方法用于多变量和多步预测”,2017 年 IEEE 国际数据科学与高级分析会议 (DSAA),东京,日本,2017 年,第 222–231 页。

PCA/LDA/ICA:组件分析算法比较

原文:towardsdatascience.com/pca-lda-ica-a-components-analysis-algorithms-comparison-c5762c4148ff

回顾这些著名算法的概念和差异

Yoann Mocquin Towards Data Science Yoann Mocquin

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

--

在深入比较算法之前,让我们先独立回顾一下它们。

注意,本文章并不旨在深入解释每个算法,而是比较它们的目标和结果。

如果你想了解更多关于 PCA 和 ZCA 的区别,请查看我之前基于 numpy 的文章:

PCA-whitening vs ZCA-whitening:一个 numpy 2D 可视化

数据白化的过程包括一种变换,使得变换后的数据具有单位矩阵作为…

[towardsdatascience.com

PCA:主成分分析

  • PCA 是一种无监督的线性降维技术,旨在找到一组新的正交变量,这些变量捕捉数据中最重要的变异来源。

  • 它广泛用于特征提取和数据压缩,并且可以用于探索性数据分析或作为机器学习算法的预处理步骤。

  • 生成的组件根据它们解释的方差量进行排名,可以用来可视化和解释数据,以及用于聚类或分类任务。

LDA:线性判别分析

  • LDA 是一种监督式线性降维技术,旨在找到一组新的变量,这些变量最大限度地提高类之间的分离,同时最小化每个类内的变化。

  • PCA 广泛用于特征提取和分类,并且可以在保留类间判别信息的同时减少数据的维度。

  • 结果组件按其区分能力排序,可以用来可视化和解释数据,以及进行分类或回归任务。

ICA:独立成分分析

  • ICA 是一种无监督的线性降维技术,旨在找到一组统计上独立且非高斯的变量。

  • 它广泛用于信号处理和源分离,可以用来提取数据中其他技术无法访问的潜在变异源。

  • 结果组件按其独立性排序,可以用来可视化和解释数据,以及进行聚类或分类任务。

鸢尾花数据集的结果

让我们使用 sklearn 比较它们在著名的鸢尾花数据集上的结果。首先,使用每个 4 个数值特征的 pairplot 绘制鸢尾花数据集,并将颜色作为类别特征:

import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris

# Load the iris dataset
iris = load_iris()
data = iris.data
target = iris.target
target_names = iris.target_names

# Convert the iris dataset into a pandas DataFrame
iris_df = sns.load_dataset('iris')
iris_df['target'] = target

# Generate the pairplot∑
sns.pairplot(data=iris_df, hue='target', palette=['navy', 'turquoise', 'darkorange'], markers=['o', 's', 'D'],
             plot_kws=dict(s=25, alpha=0.8, edgecolor='none'), diag_kws=dict(alpha=0.8, edgecolor='none'))

# Set the title and adjust plot spacing
plt.suptitle('Iris Pairplot')
plt.subplots_adjust(top=0.92)

plt.show()

作者提供的图像:鸢尾花数据集的 pairplot

现在我们可以计算每个变换并绘制结果。注意我们只使用了 2 个组件,因为 LDA 最多需要 (N-1) 个组件,其中 N 是类别的数量(这里等于 3,因为有 3 种鸢尾花类型)。

from sklearn.datasets import load_iris
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA, FastICA
import matplotlib.pyplot as plt

# Load the Iris dataset
iris = load_iris()
X = iris.data
y = iris.target
target_names = iris.target_names

# Standardize the data
scaler = StandardScaler()
X_std = scaler.fit_transform(X)

# Apply LDA with 2 components
lda = LinearDiscriminantAnalysis(n_components=2)
X_lda = lda.fit_transform(X_std, y)

# Apply PCA with 2 components
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_std)

# Apply ICA with 2 components
ica = FastICA(n_components=2)
X_ica = ica.fit_transform(X_std)

# Plot the results
plt.figure(figsize=(15, 5))

plt.subplot(1, 3, 1)
for target, color in zip(range(len(target_names)), ['navy', 'turquoise', 'darkorange']):
    plt.scatter(X_lda[y == target, 0], X_lda[y == target, 1], color=color, alpha=.8, lw=2,
                label=target_names[target])
plt.legend(loc='best', shadow=False, scatterpoints=1)
plt.title('LDA')
plt.xlabel('LD1')
plt.ylabel('LD2')

plt.subplot(1, 3, 2)
for target, color in zip(range(len(target_names)), ['navy', 'turquoise', 'darkorange']):
    plt.scatter(X_pca[y == target, 0], X_pca[y == target, 1], color=color, alpha=.8, lw=2,
                label=target_names[target])
plt.legend(loc='best', shadow=False, scatterpoints=1)
plt.title('PCA')
plt.xlabel('PC1')
plt.ylabel('PC2')

plt.subplot(1, 3, 3)
for target, color in zip(range(len(target_names)), ['navy', 'turquoise', 'darkorange']):
    plt.scatter(X_ica[y == target, 0], X_ica[y == target, 1], color=color, alpha=.8, lw=2,
                label=target_names[target])
plt.legend(loc='best', shadow=False, scatterpoints=1)
plt.title('ICA')
plt.xlabel('IC1')
plt.ylabel('IC2')

plt.show()

这段代码加载了鸢尾花数据集,应用了 LDA、PCA 和 ICA,每种方法使用 2 个组件,然后使用不同的颜色绘制每个类别的结果。

注意,通常在应用 PCA、ICA 或 LDA 之前对数据进行标准化是一个好习惯。标准化很重要,因为这些技术对输入特征的尺度很敏感。标准化数据可以确保每个特征具有零均值和单位标准差,这使所有特征处于相同的尺度上,避免了某一特征对其他特征的支配。

由于 LDA 是一种有监督的降维技术,它以类别标签作为输入。相比之下,PCA 和 ICA 是无监督的技术,这意味着它们仅使用输入数据而不考虑类别标签。

LDA 的结果可以解释为将数据投影到一个最大化类别分离的空间,而 PCA 和 ICA 的结果可以解释为将数据投影到一个分别捕捉最重要的变异源或独立源的空间。

作者提供的图像:LDA、PCA 和 ICA 在鸢尾花数据集上的比较

注意,尽管 ICA 的目的不是分离类别,但它仍然显示了类别之间的分离:这是因为类别在输入数据集中已经相当有序。

让我们暂时放下 LDA,关注 PCA 和 ICA 之间的区别——因为 LDA 是一种有监督的技术,专注于分离类别并强制最大组件数量,而 PCA 和 ICA 侧重于创建一个与输入矩阵形状相同的新矩阵。

让我们查看 PCA 和 ICA 的 4 个组件的输出:

左:PCA 的配对图 / 右:ICA 的配对图(图片由作者提供)

我们还可以比较每个转换数据的相关性矩阵:注意到两种方法都产生了不相关的向量(换句话说,转换后的数据特征是正交的)。这是因为这是 PCA 算法中的一个约束——每个新向量必须正交于之前的向量——而 ICA 算法的一个结果——这意味着原始数据集是混合在一起的独立信号,必须被重构。

左:ICA 的相关性热图 / 右:PCA 的相关性热图(图片由作者提供)

所以 PCA 和 ICA 似乎给出了具有类似属性的结果:这是由于以下两个原因:

  • 独立性在这两种算法中都“编码”了

  • 鸢尾花数据集展示了良好的类分离

这就是为什么我们需要另一个例子,更适合 ICA 的原因。

另一个例子:

让我们看另一个例子:我们首先生成一个合成数据集,其中包含两个独立的源,一个正弦波和一个方波,它们被线性组合在一起以创建一个混合信号。

实际的、真正的、独立的信号如下:

它们被线性组合在一起,作为 2 个线性组合:

让我们看看 PCA 和 ICA 在这个新数据集上的表现:

注意 PCA 如何创建了一个新的组件,它作为输入的线性组合展示了大量的方差,但这绝对不匹配原始数据:这确实不是 PCA 的目的。

相反,ICA 在恢复原始数据集方面表现非常好,无论方差组成如何。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import FastICA

# Generate a synthetic dataset with two independent sources
np.random.seed(0)
n_samples = 2000
time = np.linspace(0, 8, n_samples)

s1 = np.sin(2 * time) # Source 1: sine wave
s2 = np.sign(np.sin(3 * time)) # Source 2: square wave

S = np.c_[s1, s2]
S += 0.2 * np.random.normal(size=S.shape) # Add noise to the sources
S /= S.std(axis=0) # Standardize the sources

# Mix the sources together to create a mixed signal
A = np.array([[0.5, 0.5], [0.2, 0.8]]) # Mixing matrix
X = np.dot(S, A.T) # Mixed signal

# Standardize the data
X = (X - np.mean(X, axis=0)) / np.std(X, axis=0)

# Use PCA to reduce the dimensionality of the data
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)

# Use ICA to separate the sources from the mixed signal
ica = FastICA(n_components=2)
X_ica = ica.fit_transform(X) # Estimated sources

# Plot the results
plt.figure()

models = [X, S, X_pca, X_ica]
names = ['Observations (mixed signal)',
         'True Sources',
         'PCA features', 'ICA estimated sources']
colors = ['red', 'steelblue']

for ii, (model, name) in enumerate(zip(models, names), 1):
    plt.subplot(4, 1, ii)
    plt.title(name)
    for sig, color in zip(model.T, colors):
        plt.plot(sig, color=color)

plt.tight_layout()
plt.show()

结论

PCA、LDA 和 ICA 算法可能看起来像是彼此的定制版本,但它们的目的实际上并不相同。总结如下:

  • PCA 旨在创建新的组件,这些组件包含输入数据的最大方差

  • LDA 旨在创建基于类别特征分离簇的新组件

  • ICA 旨在恢复在输入数据集中线性组合在一起的原始特征

希望你能更好地理解这些算法之间的差异,并能够在未来快速识别你需要的算法。

如果你喜欢这个故事,确保关注我并帮助我实现 100 个订阅者的目标 😃

查看下面的一些其他故事:

[## 使用 numpy 的有限差分法实现 300 倍更快的分辨率

有限差分法是一种强大的技术来解决复杂问题,numpy 使它变得快速!

使用 numpy 提升有限差分方法的分辨率 300 倍 [## 使用 numpy 提升有限差分方法的分辨率 300 倍

容器方法。

深入了解 seaborn 调色板 [## 深入了解 seaborn 调色板

被 seaborn 调色板淹没了吗?

PCA-whitening 与 ZCA-whitening:一个 numpy 2d 可视化 [## PCA-whitening 与 ZCA-whitening:一个 numpy 2d 可视化

数据白化的过程包括一种变换,使得变换后的数据具有单位矩阵作为…

包裹 numpy 的数组 [## 包裹 numpy 的数组

PCA 与自编码器在小数据集上的降维比较

原文:towardsdatascience.com/pca-vs-autoencoders-for-a-small-dataset-in-dimensionality-reduction-67b15318dea0

神经网络和深度学习课程:第四十五部分

Rukshan PramodithaTowards Data Science Rukshan Pramoditha

·发表在 Towards Data Science ·阅读时间 8 分钟·2023 年 2 月 16 日

--

图片由 Robert Katzki 提供,来源于 Unsplash

小数据集上,通用机器学习算法是否能超越神经网络?

一般而言,深度学习算法如神经网络需要大量数据才能实现合理的性能。因此,像自编码器这样的神经网络可以从我们用来训练模型的非常大的数据集中获益。

有时,当通用机器学习算法在非常小的数据集上训练时,它们的表现可能会超越神经网络算法。

自编码器也可以用于降维应用,尽管它们在图像去噪、图像生成、图像着色、图像压缩、图像超分辨率等其他流行应用中被广泛使用。

早些时候,我们通过在非常大的 MNIST 数据集上训练模型,比较了自编码器在降维中的表现与 PCA 的表现。在那里,自编码器模型轻松超越了 PCA 模型 [参考¹],因为 MNIST 数据集大且具有非线性特征。

参考¹:自编码器如何在降维中超越 PCA

自编码器在处理大规模和非线性数据时表现良好。

尽管自编码器是一种神经网络,但仍然可以使用较小的数据集,只要选择正确的模型架构和超参数值,就能够取得一定的性能。

自编码器功能强大且灵活,足以捕捉数据中的复杂和非线性模式。

今天,我们将通过在非常小的葡萄酒数据集上训练模型,比较自编码器(神经网络模型)在降维方面与 PCA(通用机器学习算法)的表现。

葡萄酒数据集有 178 个样本和 13 个特征。与 MNIST 数据集的 60,000 个样本和 784 个特征相比,这个数据集非常小!

加载葡萄酒数据集

葡萄酒数据集已经预加载到 Scikit-learn 中,可以按如下方式加载。

# Load the Wine dataset
from sklearn.datasets import load_wine

wine = load_wine()
X = wine.data
y = wine.target

print("Wine dataset size:", X.shape)

葡萄酒数据集的形状(作者提供的图片)

使用葡萄酒数据运行 PCA

首先,我们需要通过使用所有组件运行 PCA 来选择最佳组件数量 [ref²]。

ref²: 进行主成分分析 (PCA) 以实现降维的 3 个简单步骤

# Feature scaling
from sklearn.preprocessing import StandardScaler

X_scaled = StandardScaler().fit_transform(X)

import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

# Select the best number of components
# by running PCA with all components
pca = PCA(n_components=None)
pca.fit(X_scaled)

# Plot cumulative explained variances
exp_var = pca.explained_variance_ratio_ * 100
cum_exp_var = np.cumsum(exp_var)

plt.bar(range(1, 14), exp_var, align='center',
        label='Individual explained variance')

plt.step(range(1, 14), cum_exp_var, where='mid',
         label='Cumulative explained variance', color='red')

plt.ylabel('Explained variance percentage')
plt.xlabel('Principal component index')
plt.xticks(ticks=list(range(1, 14)))
plt.legend(loc='best')
plt.tight_layout()

plt.savefig("cumulative_explained_variance_plot.png")

累计解释方差图(作者提供的图片)

通过分析上述累计解释方差图,我决定保留前七个组件,这些组件捕获了数据中约 90% 的方差。因此,这七个组件将准确地代表原始葡萄酒数据集。

使用选定的组件重新运行 PCA

要将降维应用于葡萄酒数据集,我们需要使用选定的组件重新运行 PCA 并应用转换。

# Run PCA again with selected (7) components
pca = PCA(n_components=7)
X_pca = pca.fit_transform(X_scaled)
print("PCA reduced wine dataset size:", X_pca.shape)

应用 PCA 后的葡萄酒数据集的形状(作者提供的图片)

现在,数据集中只有七个组件(特征)。因此,数据的维度已经减少!

使用自编码器进行降维

以下代码定义了一个自编码器,其中编码器部分可以用于获得数据的低维(编码)表示。

import numpy as np
from tensorflow.keras import Model, Input
from tensorflow.keras.layers import Dense

# Build the autoencoder
input_dim = X.shape[1]
latent_vec_dim = 7

input_layer = Input(shape=(input_dim,))

# Define encoder
x = Dense(8, activation='relu')(input_layer)
x = Dense(4, activation='relu')(x)
encoder = Dense(latent_vec_dim, activation="tanh")(x)

# Define decoder
x = Dense(4, activation='relu')(encoder)
x = Dense(8, activation='relu')(x)
decoder = Dense(input_dim, activation="sigmoid")(x)

autoencoder = Model(inputs=input_layer, outputs=decoder)

# Compile the model with optimizer and loss function
autoencoder.compile(optimizer="adam", loss="mse")

# Train the model with 100 epochs
autoencoder.fit(X_scaled, X_scaled, epochs=100, verbose=0,
                batch_size=16, shuffle=True)

# Use the encoder part to obtain the lower dimensional,
# encoded representation of the data
encoder_model = Model(inputs=input_layer, outputs=encoder)
X_encoded = encoder_model.predict(X_scaled)

# Print the shape of the encoded data
print("Autoencoder reduced wine dataset size:", X_encoded.shape)

自编码数据后的葡萄酒数据集的形状(作者提供的图片)

自编码器模型具有输入层、编码层和解码层。输入维度是葡萄酒数据集中的输入特征数,即 13。潜在向量维度为 7,等于我们在 PCA 中先前选择的组件数量。

所有层都使用 Keras 功能 API 方法 [ref³] 连接。然后,通过连接输入层和解码器部分创建整个自编码器模型。

ref³: 构建 Keras 模型的两种不同方式:顺序 API 和功能 API

然后,我们使用 Adam 优化器和均方误差(mse)损失函数编译整个自编码器。

模型在标准化(缩放)的葡萄酒数据上训练 100 个周期,批量大小为 16。

潜在向量表示了输入数据中最重要的特征的低维形式 [ref⁴]。因此,在训练完整个自动编码器之后,我们可以使用其编码器部分来获得数据的低维(编码)表示。

ref⁴: 深度学习中的自动编码器简介

encoder_model 是通过连接输入层和编码器部分创建的。然后,我们可以在缩放后的 Wine 数据上调用它的 predict() 方法,以获得 Wine 数据集的低维(编码)表示,这由变量 X_encoded 表示。由于“潜在向量维度”被设置为 7,因此编码表示具有 7 个特征,数据的维度已被降低!

通过可视化数据来比较 PCA 和自动编码器模型

高维数据的可视化可以通过降维实现 [ref⁵]。因此,降维对于数据可视化极为有用。

ref⁵: 降维的 11 种不同用途

仅使用两个组件(维度),我们绘制了 PCA 和自动编码器模型返回的 Wine 数据集输出。

import matplotlib.pyplot as plt

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

# Plot PCA output
ax1.scatter(X_pca[:, 0], X_pca[:, 1], c=y, s=25, cmap='plasma')
ax1.set_title('PCA')
ax1.set_xlabel('Component 1')
ax1.set_ylabel('Component 2')

# Plot autoencoder output
ax2.scatter(X_encoded[:, 0], X_encoded[:, 1], c=y, s=25, cmap='plasma')
ax2.set_title('Autoencoder')
ax2.set_xlabel('Dimension 1')
ax2.set_ylabel('Dimension 2')

plt.savefig("Output.png")

Wine 数据的二维表示:PCA 与自动编码器(图像作者)

如你所见,使用 PCA 获得的 Wine 数据的二维表示显示了三种葡萄酒类别之间的明显分离,但在自动编码器输出中分离效果不够好。

PCA 在小数据集如 Wine 数据集上效果很好。

注意: 由于算法的随机特性以及超参数的值,如隐藏层的数量和隐藏单元、每层使用的激活函数类型、损失函数类型、优化器类型、训练轮数和批量大小,自动编码器的输出(右图)可能会有显著变化!但是,三个类别的分离可能不如 PCA 输出的效果好。

PCA 与自动编码器:哪种在降维方面更好?

选择 PCA 和自动编码器进行降维取决于以下因素。

  • 数据集的大小

  • 数据集的复杂性(线性还是非线性,图像还是数值数据)

  • 分析的目标

  • 计算资源的可用性

  • 可解释性

PCA 在小数据集上效果很好。它也可以用于较大的数据集。然而,自动编码器在非常大的数据集上效果非常好。

PCA 对线性数据效果很好,因为它是一种线性降维技术。对于非线性数据效果不佳。相反,自动编码器可以轻松捕捉数据中的复杂和非线性模式。因此,它们在非线性数据上表现良好。

自动编码器在图像数据上表现良好。PCA 在数值数据上表现良好。

无法确定自编码器模型潜在向量中每个组件(特征)的重要性。但是在 PCA 中,我们可以为此创建累积解释方差图。

由于自编码器是神经网络,它的架构可能变得复杂。此外,它还需要大量的数据。因此,自编码器比 PCA 需要更多的计算资源。

自编码器是黑箱模型。因此,它们难以解释。我们不知道它们如何从我们提供的数据中选择重要特征。因此,这些模型的解释非常困难。

结论

PCA 和自编码器都可以用于执行降维。PCA 是一个通用的机器学习算法,而自编码器是一种神经网络架构,需大数据集和大量计算资源。

自编码器在处理大规模和非线性数据时表现良好。它们足够强大和灵活,可以捕捉数据中的复杂和非线性模式,但在较小的数据集上可能无法超越通用机器学习算法如 PCA!

如果你真的想在执行降维时考虑类可分性,LDA(线性判别分析)是最佳选择。请阅读下面的完整指南。

LDA 在分类数据集中比 PCA 更有效

今天的文章到此结束。

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

那么 AI 课程怎么样?

加入我的神经网络和深度学习课程,这是 Medium 上的首个课程(作者截图)

支持我作为作者

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

[## 通过我的推荐链接加入 Medium - Rukshan Pramoditha

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

rukshanpramoditha.medium.com

加入我的私人邮件列表

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

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

葡萄酒数据集信息

  • 数据集来源: 你可以从这里下载原始数据集。

  • 数据集许可证: 该数据集在CC BY 4.0知识共享署名 4.0)许可证下提供。

  • 引用: Lichman, M. (2013). UCI 机器学习库 [archive.ics.uci.edu/ml]。加州尔湾:加利福尼亚大学信息与计算机科学学院。

Rukshan Pramoditha

2023 年 2 月 16 日

Pearson、Spearman 和 Kendall 相关系数,手动计算

原文:towardsdatascience.com/pearson-spearman-and-kendall-correlation-coefficients-by-hand-d2e1676ca73f

学习如何手动计算 Pearson、Spearman 和 Kendall 相关系数,以评估两个变量之间的关系

Antoine SoeteweyTowards Data Science Antoine Soetewey

·发表于 Towards Data Science ·15 分钟阅读·2023 年 9 月 5 日

--

图片由 S O C I A L . C U T 提供

介绍

在统计学中,相关性用于评估两个变量之间的关系。

在之前的帖子中,我们展示了如何 在 R 中计算相关性并执行相关性测试。在本帖中,我们展示了如何手动计算 Pearson、Spearman 和 Kendall 相关系数,并在两种不同的场景下(即有平局和无平局)进行计算。

数据

为了展示有平局和无平局的方法,我们考虑了两个不同的数据集,一个带有平局,另一个没有平局。

带有平局

对于带有平局的场景插图,假设我们有以下大小为 5 的样本:

表格由作者提供

图表由作者提供

如我们所见,由于变量 x 中有两个相同的观测值,因此存在一些平局。

无平局

对于不需要处理平局的情况,我们将考虑以下大小为 3 的样本:

表格由作者提供

图表由作者提供

手动计算的相关系数

三种最常见的相关性方法是:1

  1. Pearson,用于两个 定量连续 变量之间具有线性关系

  2. Spearman,用于两个定量变量,如果关系部分线性,或用于一个定性序数变量和一个定量变量

  3. Kendall,通常用于两个定性序数变量

每种方法将在接下来的章节中介绍。

请注意,本帖的目的是展示如何手动计算三种相关系数和在两种不同情况下的操作;我们并不关心验证基本假设。

Pearson

幸运的是,无论是否存在重复值,计算 Pearson 相关系数的过程是相同的,所以我们不区分这两种情况。

有无重复值的情况

Pearson 相关系数,样本情况下用 r 表示,可以如下计算2

其中:

  • n 是样本大小

  • xi 和 yi 是观测值

  • ¯x¯ 和 ¯y¯ 是 x 和 y 的样本均值

  • sx 和 sy 是 x 和 y 的样本标准差

我们逐步展示如何计算。

步骤 1.

如你所见,在计算相关系数之前,我们首先需要计算两个变量的均值和标准差。

x 的均值计算如下:

x 的标准差计算如下:

这些公式也可以类似地用于变量 y。

我们从计算两个变量的均值开始:

步骤 2.

我们现在需要计算每个变量的标准差。为了简化计算,最好使用表格,从观测值开始:

作者提供的表格

计算标准差时,我们需要每个观测值与其均值之间平方差的总和,即:

我们首先在表格中创建两列新数据,标记为 x-xbary-ybar,对应于:

作者提供的表格

我们取这两列新数据的平方,标记为 (x-xbar)²(y-ybar)²

作者提供的表格

然后我们对这两列求和,结果是:

标准差因此为:

步骤 3.

我们现在需要

因此,我们在表格中添加一列新数据,对应于 xiyi,标记为 x*y

作者提供的表格

步骤 4。

我们取最后一列的总和,结果是:

步骤 5。

最后,可以通过将上述找到的值代入初始公式来计算皮尔逊相关系数:

请注意,还有其他公式可以计算皮尔逊相关系数。例如,

所有公式当然会给出完全相同的结果。

供您参考,将皮尔逊相关系数 r 平方得到决定系数 R2,适用于简单的线性回归

斯皮尔曼

现在我们介绍斯皮尔曼相关系数,也称为斯皮尔曼等级相关系数。这个系数实际上与皮尔逊系数相同,只是计算基于等级值而不是原始观察值。

再次介绍如何一步步手动计算,但这次我们区分了两种情况:

  1. 如果有并列

  2. 如果没有并列

含并列

斯皮尔曼相关系数(含并列),记作 rs,定义如下:

其中:

这里是如何手动计算的。

步骤 1。

如前所述,斯皮尔曼系数基于等级。因此,我们首先需要分别为两个变量添加观察值的等级(从最低到最高)。

对于 y,我们看到:

  • -3 是最小值,所以我们将其分配为等级 1。

  • -1 是第二小的值,所以我们将其分配为等级 2。

  • 然后是 0,所以我们将其分配为等级 3。

  • 然后是 1,所以我们将其分配为等级 4。

  • 最终,2 是最大值,所以我们将其分配为等级 5。

对于 x 也是如此,只是这里有两个观察值为 5(因此等级中会有并列)。在这种情况下,我们取平均等级:

  • -1 是最小值,所以我们将其分配为等级 1。

  • 2 是第二小的值,所以我们将其分配为等级 2。

  • 然后是 3,所以我们将其分配为等级 3。

  • 最终,两个最大值属于等级 4 和 5,所以我们将它们都分配为等级 4.5。

我们在表中包括等级,表示为RxRy

作者提供的表格

步骤 2。

从那里开始,计算方法类似于皮尔逊系数,只是我们处理的是等级而不是初始观察值。为了避免剩余步骤中的任何混淆,我们从表格中删除初始观察值,仅保留等级:

作者提供的表格

我们从等级的平均值开始:

对于标准差,我们使用与皮尔逊系数相同的表格:

作者提供的表格

我们求出最后两列的总和,这给出:

标准差为:

步骤 3.

我们现在需要

因此,我们在表格中添加了一列,对应于 RxiRyi,用Rx*Ry表示:

按作者分类的表格

步骤 4.

我们求出最后一列的总和,这给出

步骤 5.

最后,通过将所有值代入初始公式来计算斯皮尔曼相关系数:

无平局

当每个变量中的所有初始值都不同,即所有排名都是不同的整数时,就没有平局。在这种特定情况下,斯皮尔曼系数可以用以下简化公式计算:

例如,假设样本中没有平局的数据介绍如下:

按作者分类的表格

步骤 1.

每个变量中的所有观察值都不同,因此所有排名都是不同的整数,并且没有平局:

按作者分类的表格

步骤 2.

我们只需要计算每行两个排名之间的差异,用Rx-Ry表示:

按作者分类的表格

求这些差异的平方,记作(Rx-Ry)²

按作者分类的表格

然后求出最后一列的总和,这给出

步骤 3.

最后,我们可以填入初始公式以找到斯皮尔曼系数:

Kendall

Kendall 系数相关性,也称为 Kendall 的τ系数,与斯皮尔曼系数类似,只是它通常在样本较小且有许多排名平局时更受欢迎。

在这里我们也区分两种情况:

  • 如果没有平局

  • 如果有平局

与斯皮尔曼系数不同,我们首先说明没有平局的情况,然后是有平局的情况。

无平局

当没有平局时,Kendall 系数,记作τa,定义如下:

其中:

  • C 是一致对的数量

  • D 是不一致对的数量

这个系数,也称为 Kendall tau-a,不对平局进行任何调整。

让我们看看使用无平局的数据(与斯皮尔曼相关性无平局的相同数据)的一致对和不一致对:

按作者分类的表格

步骤 1.

我们首先计算每个变量(RxRy)的排名,就像我们为斯皮尔曼系数做的那样:

按作者分类的表格

步骤 2.

然后我们在两个变量 RxRy 中任意选择一个作为参考变量。假设我们在这里选择 Rx 作为参考变量。

我们按照这个参考变量对数据集进行排序:

表格由作者提供

从现在开始,我们只看第二个变量( 参考水平的那个,这里是 Ry)的排名,因此为了避免在剩下的步骤中产生混淆,我们只保留 Ry 列:

表格由作者提供

步骤 3.

我们现在逐行检查 Ry,并检查表中它下面的行是否更小或更大。

在我们的表格中,Ry 的第一行是 1。我们看到它下面的值是 3,大于 1。由于 3 大于 1,这被称为和谐配对。我们将其写入表中:

表格由作者提供

下一行,2,也大于 1,因此它也是一个和谐配对。我们也将其写入表中:

表格由作者提供

我们重新从 Ry 的第二行开始,即 3。再次,我们查看下面的行,检查它是否更大或更小。在这里,下面的行是 2,小于 3,因此我们有一个 discordant 配对。我们将此信息添加到表中:

表格由作者提供

步骤 4.

现在我们计算和谐配对和 discordant 配对的总数。总共有:

  • 2 对和谐配对,所以 C = 2,和

  • 1 对 discordant 配对,所以 D = 1。

步骤 5.

最后,我们将刚刚找到的值代入初始公式中:

或者,我们也可以使用以下公式

其中 n 仍然是样本大小,两者都给出完全相同的结果。

有重复值的情况

我必须承认,有重复值的过程比没有重复值的情况稍微复杂一些。

Kendall tau-b 系数,即对重复值进行调整的系数,定义如下

其中:

请注意,这里 C2n 中的字母 C 表示“组合”,而不是“和谐”。

让我们用皮尔逊相关系数和有重复的斯皮尔曼相关系数的数据集来说明这个场景和公式,即:

表格由作者提供

步骤 1.

与没有重复的情况类似,有重复的情况下,这个过程也是基于排名的,因此我们首先为每个变量添加排名,通常表示为RxRy

表格由作者提供

步骤 2.

必须选择两个变量中的一个(x 或 y)作为参考变量。这次,我们不是任意选择,而是选择没有重复值的那个。在我们的例子中,变量 x 有重复值,而变量 y 没有任何重复值。因此 y 将是我们的参考变量。

然后我们:

  • 按参考变量排序数据集,这里是 Ry,并

  • 我们只保留必要的列,以避免在剩余步骤中出现任何混淆:

作者提供的表格

步骤 3.

我们检查它是否是一个一致对或不一致对,与我们处理无联系时的方式相同,但这次我们不计算联系。

Rx 中的第一行是 1。我们将表中所有低于该值的行与之比较。表中 1 以下的所有行都更大,因此我们写下它们是一致对:

作者提供的表格

我们对每一行重复相同的过程。

例如,对于列 Rx 的第二行,我们有值 2。再次,表中所有低于该值的行都更大,因此我们写下它们是一致对:

作者提供的表格

对于列 Rx 的第三行,我们有值 4.5。表中紧接其下的行(= 3)小于 4,因此我们写下 D 表示不一致对:

作者提供的表格

现在,如你所见,最后一行也是 4.5。由于它等于我们正在比较的值,它既不是一致对也不是不一致对,因此我们在表格中写下“T”表示“联系”:

作者提供的表格

最后,列 Rx 中的第四行是 3,我们将其与 4.5 进行比较,发现 4.5 更大,因此它是一个一致对:

作者提供的表格

步骤 4.

然后我们汇总一致对和不一致对的数量:

  • C = 8

  • D = 1

步骤 5.

我们现在拥有计算 τb 分子的所有信息,但仍需找到

以计算分母。

如上所述,C2n 是可能对的总数,因此对应于两个值的组合数量。这个对的数量可以通过组合公式找到,即:

其中 n 是样本大小。

在我们的例子中,我们的样本大小为 5,所以我们有:3

此外,nx 和 ny 分别是 x 和 y 变量中具有联系的可能对的数量。

当查看上述表格中的 RyRx 时:

作者提供的表格

我们看到有:

  • 变量 x 中的 2 个相同值

  • 变量 y 中的 0 个相同值

这意味着具有联系的的数量等于:4

  • 对于变量 x(唯一的可能联系对是 {4.5, 4.5})

  • 0 对于变量 y(由于所有等级都是独特的,因此不存在成对的并列情况)

因此,

现在我们拥有计算 τb 所需的所有信息!

步骤 6。

将上述找到的值代入初始公式中,我们得到

在 R 中验证

为了完整性,我们使用 R 验证每个系数和场景的结果。

Pearson:

x <- c(-1, 3, 5, 5, 2)
y <- c(-3, 1, 0, 2, -1)
cor(x, y, method = "pearson")
## [1] 0.8769051

带并列情况的 Spearman:

cor(x, y, method = "spearman")
## [1] 0.8207827

不带并列情况的 Spearman:

x2 <- c(3, 5, 2)
y2 <- c(5, 1, -1)
cor(x2, y2, method = "spearman")
## [1] 0.5

不带并列情况的 Kendall:

cor(x2, y2, method = "kendall")
## [1] 0.3333333

带并列情况的 Kendall:

cor(x, y, method = "kendall")
## [1] 0.7378648

我们确实手动计算的结果与 R 中得到的结果相同(任何差异都是由于四舍五入造成的)!

结论

记住,相关系数(无论是 Pearson、Spearman 还是 Kendall)的范围是 -1 到 1,其中 0 表示没有相关性,绝对值越接近 1,相关性越强。

广义上讲,正相关意味着一个变量的高值与另一个变量的高值相关(反之亦然)。负相关意味着一个变量的高值与另一个变量的低值相关。

最后但同样重要的是,记住从样本中计算出的相关系数得出的结论不能在没有适当统计检验的情况下推广到总体,即相关性检验

感谢阅读。

希望这篇文章帮助你手动计算 Pearson、Spearman 和 Kendall 相关系数(包括和不包括并列情况)。

和往常一样,如果你对本文涉及的主题有任何问题或建议,请在评论中添加,这样其他读者也能从讨论中受益。

  1. 供参考,Pearson 相关系数被视为参数检验,而 Spearman 和 Kendall 相关系数被视为非参数检验。↩︎

  2. 在这里,我们假设我们有一个样本而不是总体。如果你拥有的观察值代表整个总体,则相关系数记作 ρ,公式略有不同。如果需要,请参见样本和总体之间的差异的回顾。↩︎

  3. 记住,n 的阶乘,记作 n!,是 n! = n×(n−1)×⋯×1。例如,5! = 5×4×3×2×1=120。↩︎

  4. 如果你有许多相同的值,并希望通过公式而非手动计数来计算可能的配对数量,你可以再次使用组合的公式,即 C2n = n! / x!(n−x)!,其中 n 是给定组中相同值的数量。请记住,组合的数量必须对所有组进行求和。在我们的例子中,我们只有一个包含 2 个值的组,因此 nx = C22 = 2! / 2!(2−2)! = 1。↩︎

相关文章

最初发布于 https://statsandr.com 于 2023 年 9 月 5 日。

人员分析是新的热门话题,了解它的原因在于此

原文:towardsdatascience.com/people-analytics-is-the-new-big-thing-heres-why-you-should-know-about-it-61cda54da5e7

关注工作中的数据与分析

Rashi DesaiTowards Data Science 拉希·德赛

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

--

人员分析可以定义为处理人力资源数据并将其转化为数据驱动的可操作见解,以改善业务运作方式的过程。

José Martín Ramírez CarrascoUnsplash上的照片

人员分析(也称为人才分析或劳动力分析)是一种深度数据驱动、基于证据的数据分析流派,其中专业人员负责使用数据科学概念、实施数据模型和可视化数据,以推荐可操作的见解。简单来说,人员分析是从提取到传达给业务的端到端数据生命周期,但针对的是人力资源数据。

在当今这个时代,人员分析对于一个组织至关重要,因为现在,比以往任何时候都更能影响一个组织的成败。人员分析与业务能够通过内部人员对业务目标、使命和愿景的影响而取得的成就相关,并评估人力资源举措的有效性。

人员分析并不是一个小的、利基的团队;相反,它涵盖了整个组织,并在推动劳动力战略方面发挥着关键作用。

为什么选择人员分析?

超过 70%的公司现在表示他们认为人员分析是一项高优先级任务,但仅有 8%的组织报告说他们拥有可用的数据,而只有 9%的公司相信他们对推动其绩效的人才维度有很好的理解——这为更多公司考虑人员分析提供了广阔的机会!

当今组织面临着劳动力、工作场所和工作世界的根本变化。在这个变化的时代,未来的组织即将到来,人才能够获取和员工体验是业务关键绩效指标的重要贡献因素。

数据与人员之间的相互作用因多种原因显得很有前景,但主要因为——

  • 人力资源部门已经拥有用于人员分析的原材料——人力资源跟踪、存储和维护一切数据。

  • 很多数据随着时间的推移并不会变化——有关个人的事实如种族、性别和工作历史并不会经常变化。

  • 人力资本是企业的一项增值资产,它既可以为你节省成本,也可能使你付出代价。

对于任何擅长分析人力资源数据的人来说,优化人才管理和组织绩效、研究人类心理,并将其与员工体验相关联,人员分析是适合你的!

一天的生活:人员分析师

人员分析师负责理解、分析、执行和解决业务和人力资源问题,并为现有和新员工发现机会。

人员分析师的工作日程与其他数据分析师的生活日常大致相同,不同之处在于所处理的数据——人员分析师处理的人力资源数据,包括但不限于——

  • 人才获取

  • 离职率(员工离开公司的比率)

  • 员工名册

  • 员工情绪

  • 员工留存和参与度

人员分析师负责领导数据模型、工具和程序的设计、开发和实施,进行统计分析、定量和定性研究,以及预测建模,以推动对人力资源影响和业务洞察的发现和表达。

人员分析作为一种先进的分析支持/资源,帮助领导者头脑风暴并制定有效且高效的人才策略。

成为人员分析师需要什么?

  • 基本分析工具包——熟练掌握从数据提取到数据可视化的整个数据生命周期,使用 SQL、Python、R、SAS、Qlik、Tableau、Power BI、Microsoft Excel、Alteryx 等工具,并对数据库架构有良好的理解

  • 商业敏锐性

  • 良好的沟通和人际交往技能(加上一点幽默感)

  • 数据讲故事——创建和进行信息性演示,以推荐和影响决策

  • 能够通过外部视角解决复杂问题,跳出框框,创新日常流程

  • 能够在最复杂的情况下独立工作和团队协作,且只需最少的指导

  • 能够将数据翻译为业务——运行预测模型很简单,但让业务理解其含义才是真正的挑战

  • 咨询技能——问题定义、假设构建、项目管理、解决方案开发、变更管理、利益相关者管理等

  • 网络建设——从现实世界获取脉搏检查

  • 有心理学背景?赚取那笔钱吧!

人员分析的作用是什么?

  • 人员概况: 为相关受众提供关键人力资源指标的高层次概览,如员工数量、人才组合、人口统计数据、员工流失、人才获取渠道等

  • 流失与离职: 提供关于员工流失、内部流动、角色转移(横向和晋升)、新招聘/高绩效员工流失、主动或被动离职的见解

  • 人员统计细分: 提供有关员工的清晰洞察,包括领导者、部门、经理级别、职位名称等,涵盖内部和承包商员工

  • 人员利用: 展示现有员工的才能组合,包括内部和外部招聘人员的人口统计信息、晋升和离职情况,并确保公平的薪酬待遇

  • 人才获取渠道: 展示候选人在申请过程中的各个阶段的才能组合

  • 公平工作环境: 工作于员工保留和参与,解决需要了解驱动员工体验的因素,以创造一个愉快的工作环境

  • 改进的招聘机会: 分析候选人在招聘过程中从申请到录用信的时间分布,识别机会和改进领域

  • 成本效益: 通过预测谁将辞职来节省资金

  • 员工情绪: 通过脉搏调查测量员工情绪,并将假设与调查结果进行对比,识别机会和改进领域

以上就是我对这篇博客的所有内容。感谢阅读!希望你觉得这篇文章很精彩。在评论中告诉我你在讲故事方面的经验,你在数据领域的旅程,以及你对 2023 年的期望!

如果你喜欢阅读这样的故事,可以通过这个链接考虑注册成为 Medium 会员

祝数据探测愉快!

拉希是来自芝加哥的数据高手,她热衷于分析数据并创建数据故事来传达洞察。她是一名全职的医疗数据分析顾问,周末时会喝杯咖啡,写博客谈论数据……

感知机:第一个神经网络模型

原文:towardsdatascience.com/perceptrons-the-first-neural-network-model-8b3ee4513757

概述及在 Python 中的实现

Dr. Roi YehoshuaTowards Data Science Dr. Roi Yehoshua

·发表于 Towards Data Science ·14 分钟阅读·2023 年 3 月 28 日

--

Hal GatewoodUnsplash 上的照片

感知机是神经网络(NN)的最早计算模型之一,它们构成了今天更复杂、更深层网络的基础。理解感知机模型及其理论将为你理解神经网络中的许多关键概念提供良好的基础。

背景:生物神经网络

生物神经网络(例如我们大脑中的网络)由大量称为神经元的神经细胞组成。

每个神经元通过称为树突的纤维接收来自其邻近神经元的电信号(冲动)。当其接收到的信号总和超过某个阈值时,神经元会通过长纤维称为轴突发出自己的信号,这些轴突连接到其他神经元的树突上。

两个神经元之间的连接称为突触。平均而言,每个神经元连接到大约 7000 个突触,这表明我们大脑网络的高度连通性。当我们学习两个概念之间的新联想时,表示这些概念的神经元之间的突触强度会增强。这种现象被称为赫布规则(1949),其表述为“一起发火的细胞会一起连接”。

生物神经元 (公共图像 在维基共享资源下自由授权)

感知机模型

感知机模型由弗兰克·罗森布拉特于 1957 年提出,是生物神经元的简化模型。

感知机有 m 个二进制输入,表示为 x₁, …, xₘ,这些输入代表来自其邻近神经元的信号,并输出一个二进制值 o,表示感知机是否“激发”。

感知机模型

每个输入神经元 xᵢ 通过一个强度由权重 wᵢ 表示的连接与感知机相连。权重较高的输入对感知机的输出有更大的影响。

感知机首先计算其输入信号的加权和,通过将每个输入乘以其对应的权重。这个加权和通常称为净输入,表示为 z

感知机的净输入

如果净输入超过某个预定义的阈值 θ,则感知机激发(输出为 1),否则不激发(输出为 0)。换句话说,感知机当且仅当:

我们的目标是找到权重 w₁, …, wₘ 和阈值 θ,使得感知机能正确地将其输入 x₁, …, xₘ(代表数据中的特征)映射到期望的输出 y(代表标签)。

为了简化学习过程,我们添加了一个特殊的输入神经元,称为偏置神经元,它总是输出值 1。这个神经元通常表示为x₀,其连接权重表示为bw₀。

因此,感知机的净输入变为:

包括偏置的净输入

这种公式允许我们像学习输入信号的权重一样学习正确的阈值(偏置)。

在向量形式中,我们可以将 z 写作输入向量 x = (x₁, …, xₘ) 与权重向量 w = (w₁, …, wₘ)点积加上偏置:

净输入的向量形式

只有当净输入非负时,感知机才会激发,即:

更一般地,感知机对净输入应用激活函数 f(z),以生成其输出。感知机中最常用的两个激活函数是:

  1. 阶跃函数(也称为海维赛德函数)是一个对于负输入值为 0,对于非负输入值为 1 的函数:

阶跃函数

2. 符号函数是一个对于负输入值为-1,对于非负输入值为 1 的函数:

符号函数

在更复杂的网络中使用其他类型的激活函数,例如多层感知机(MLPs)。在本文的其余部分,我将假设感知机使用步进函数。

总结一下,感知机的计算包括两个步骤:

  1. 输入值 x₁, …, xₘ 乘以其对应的权重 w₁, …, wₘ,然后加上偏置 b,得到感知机的净输入 z = wx + b

  2. 在生成二进制输出(0/1 或 -1/+1)的净输入上应用激活函数 f(z)。

我们可以将整个计算写成一个方程:

其中 f 是选择的激活函数,o 是感知机的输出。

使用感知机实现逻辑门

为了演示感知机的工作原理,让我们尝试构建计算逻辑函数“与”和“或”的感知机。

作为提醒,逻辑“与”函数有两个二进制输入,当两个输入都为真时返回真(1),否则返回假(0)。

“与”函数的真值表

实现“与”函数的感知机有两个二进制输入和一个偏置。我们希望这个感知机只有在两个输入都“激活”时才会“触发”。例如,可以通过为两个输入选择相同的权重来实现这一点,例如 w₁ = w₂ = 1,然后选择偏置在-2, -1)范围内。这样可以确保当两个神经元都激活时,净输入 2 + b 将是非负的,但当只有一个神经元激活时,净输入 1 + b 将是负的(当没有神经元激活时,净输入 b 也为负)。

![计算逻辑“与”函数的感知机以类似的方式,我们可以构建一个计算逻辑“或”函数的感知机:

计算逻辑“或”函数的感知机

验证你是否理解这个感知机是如何工作的!

作为练习,尝试构建一个用于 NAND 函数的感知机,其真值表如下所示:

NAND 函数的真值表

感知机作为线性分类器

感知机是一种线性分类器,因为它将输入空间划分为由下述超平面分隔的两个区域

分隔超平面的方程

权重向量w与这个超平面正交,从而决定其方向,而偏置 b 定义了它离原点的距离。

每个超平面上方的例子(wx + b> 0))被感知机分类为正例,而每个超平面下方的例子(wx** + b< 0**))被分类为负例。

作为线性分类器的感知器

其他线性分类器包括逻辑回归和线性支持向量机(SVM)。

线性分类器只能学习线性可分的问题,即正例和负例之间的决策边界是一个线性表面(一个超平面)。

例如,以下数据集是非线性可分的,因此感知器无法正确分类此数据集中的所有示例:

非线性可分数据集

感知器学习规则

感知器具有一个简单的学习规则,如果数据是线性可分的,它可以找到分离超平面。

对于每一个被感知器误分类的训练样本(即oᵢyᵢ),我们将以下更新规则应用于权重向量:

感知器学习规则

其中α是学习率(0 < α ≤ 1),它控制每次更新中权重调整的大小。

换句话说,我们将每个连接权重wⱼ的感知器在这个例子上的误差(真实标签yᵢ和输出oᵢ之间的差异)乘以相应输入xᵢⱼ的值和学习率。

这个学习规则尝试做的是减少感知器输出oᵢ和真实标签yᵢ之间的差距。为了理解它为什么有效,让我们检查感知器误分类的两种可能情况:

  1. 真实标签是yᵢ = 1,但感知器的预测是oᵢ =0,即wxᵢ + b < 0。在这种情况下,我们希望增加感知器的净输入,使其最终变为正值。

    为此,我们将量(yᵢ — oᵢ)x = x添加到权重向量中(乘以学习率)。这会增加正值输入的权重(其中xᵢⱼ > 0),同时减少负值输入的权重(其中xᵢⱼ < 0)。因此,感知器的整体净输入会增加。

  2. 真实标签是yᵢ = 0,但感知器的预测是oᵢ = 1,即wxᵢ + b > 0。类似于前面的情况,这里我们希望减少感知器的净输入,使其最终变为负值。这是通过将量(yᵢ — oᵢ)x = -x添加到权重向量中实现的,因为这会减少正值输入的权重(其中xᵢⱼ > 0),而增加负值输入的权重(其中xᵢⱼ < 0)。因此,感知器的整体净输入会减少。

该学习规则会依次应用于所有训练样本(顺序可以是任意的)。通常需要对整个训练集进行多次迭代(称为一个周期),才能找到正确的权重向量(即分隔正负样本的超平面的向量)。

根据感知器收敛定理,如果数据是线性可分的,则重复应用感知器学习规则最终将收敛于分隔超平面的权重(在有限的步骤中)。有兴趣的读者可以在这篇论文中找到该定理的正式证明。

感知器学习算法

实际上,感知器学习过程可能需要较长时间才能收敛(即,在训练集上达到零错误)。此外,数据本身可能不是线性可分的,在这种情况下,算法可能永远不会终止。因此,我们需要通过一些预定义的参数来限制训练周期的数量。如果感知器在达到该数量之前在训练集上达到了零错误,我们可以提前停止训练。

感知器学习算法的总结如下伪代码:

请注意,权重通常初始化为小的随机值以打破对称性(如果所有权重相等,则感知器的输出对于每个输入都是恒定的),而偏置则初始化为零。

示例:学习多数函数

例如,我们来看看感知器学习算法如何用于学习三个二进制输入的多数函数。多数函数是一个在其输入中有一半或更多为真时返回真(1),否则返回假(0)的函数。

感知器的训练集包括所有 8 种可能的二进制输入:

多数函数的训练集

在这个例子中,我们将假设初始权重和偏置为 0,学习率为 α = 0.5。

让我们跟踪第一次训练周期中的权重更新。呈现给感知器的第一个样本是x = (0, 0, 0)。在这种情况下,感知器的净输入为:z = wx + b = 0 × 0 + 0 × 0 + 0 × 0 + 0 = 0。因此,它的输出是 o = 1(记住,当输入≥0 时,阶跃函数的输出为 1)。然而,在这种情况下,目标标签是 y = 0,所以感知器的错误为 y - o = -1。

根据感知器学习规则,我们通过将 α(y - o)xᵢ = -0.5xᵢ 加到每个权重 wᵢ 上来更新权重。由于在这种情况下所有输入都是 0,除了偏置神经元(x₀ = 1),我们只更新偏置为-0.5,而不是 0。

我们对其他 7 个训练样本重复相同的过程。下表显示了每个样本之后的权重更新:

第一轮训练

在第一轮中,感知器发生了 4 次错误。第一轮后的权重向量为w = (0, 0.5, 1),偏置为 0。

在第二轮训练中,我们得到以下权重更新:

第二轮训练

这次感知器只发生了三次错误。第二轮后的权重向量为w = (0.5, 0.5, 1),偏置为-0.5。

第三轮的权重更新如下:

第三轮训练

在这一轮中,第二个示例更新后,感知器已收敛到解决此分类问题的权重向量:

w = (0.5, 0.5, 0.5)b = -1。由于所有权重相等,感知器仅在至少两个输入为 1 时激发,此时它们的加权和至少为 1,即大于或等于偏置的绝对值,因此感知器的净输入是非负的。

Python 中的感知器实现

现在我们来实现 Python 中的感知器学习算法。

我们将通过扩展sklearn.base.BaseEstimator类来实现它作为自定义 Scikit-Learn 估计器。这将允许我们像使用其他 Scikit-Learn 估计器一样使用它(例如,将其添加到一个pipeline中)。

自定义估计器需要实现fit()predict()方法,并在init()方法中设置所有超参数。

我会首先展示这个类的完整代码,然后逐步讲解。

from sklearn.base import BaseEstimator

class Perceptron(BaseEstimator):
    def __init__(self, alpha, n_epochs):
        self.alpha = alpha        # the learning rate
        self.n_epochs = n_epochs  # number of training iterations

    def fit(self, X, y):
        (n, m) = X.shape  # n is the number of samples, m is the number of features

        # Initialize the weights to small random values
        self.w = np.random.randn(m)
        self.b = 0

        # The training loop
        for epoch in range(self.n_epochs):
            n_errors = 0  # number of misclassification errors

            for i in range(n):
                o = self.predict(X[i])
                if o != y[i]:
                    # Apply the perceptron learning rule
                    self.w += self.alpha * (y[i] - o) * X[i]
                    self.b += self.alpha * (y[i] - o)
                    n_errors += 1

            # Compute the accuracy on the training set
            accuracy = 1 - (n_errors / n)
            print(f'Epoch {epoch + 1}: accuracy = {accuracy:.3f}')

            # Stop the training when there are no more errors
            if n_errors == 0:
                break

    def predict(self, X):
        z = X @ self.w + self.b
        return np.heaviside(z, 1)  # the step function

类的构造函数初始化了模型的两个超参数:学习率(alpha)和训练轮数(n_epochs)。

def __init__(self, alpha, n_epochs):
    self.alpha = alpha
    self.n_epochs = n_epochs

fit()方法在给定数据集X(带标签y)上运行学习算法。我们首先通过查询X的形状来找出数据集中有多少样本和特征:

(n, m) = X.shape

n 是训练样本的数量,m 是特征的数量。

接下来,我们使用标准正态分布(均值为 0,标准差为 1)初始化权重向量,将偏置设为 0:

self.w = np.random.randn(m)
self.b = 0

我们现在运行训练循环进行n_epochs次迭代。在每次迭代中,我们遍历所有训练样本,对于每个样本,我们通过调用predict()方法并将其输出与真实标签进行比较,检查感知器是否正确分类:

for i in range(n):
    o = self.predict(X[i])
    if o != y[i]:

如果感知器错误地分类了样本,我们将权重更新规则应用于权重向量和偏置,然后将误分类错误的数量增加 1:

self.w += self.alpha * (y[i] - o) * X[i]
self.b += self.alpha * (y[i] - o)
n_errors += 1

当轮次终止时,我们报告感知器在训练集上的当前准确性,如果错误数量为 0,我们终止训练循环:

accuracy = 1 - (n_errors / n)
print(f'Epoch {epoch + 1}: accuracy = {accuracy:.3f}')

if n_errors == 0:
    break

predict() 方法非常直接。我们首先计算感知机的净输入,即输入向量与权重的点积加上偏置:

z = X @ self.w + self.b

最后,我们使用 NumPy 的 heaviside() 函数将步进函数应用于净输入并返回输出:

return np.heaviside(z, 1)

np.heaviside() 的第二个参数指定了函数在 z = 0 时的值。

现在让我们在由 Scikit-Learn 的 make_blobs() 函数生成的数据集上测试我们的实现。

我们首先生成一个包含 100 个随机点的数据集,并将其分为两组:

from sklearn.datasets import make_blobs

X, y = make_blobs(n_samples=100, n_features=2, centers=2, cluster_std=0.5)

我们将 cluster_std 设置为 0.5(而不是默认的 1),以确保数据是线性可分的。

让我们绘制数据集:

import seaborn as sns

sns.scatterplot(x=X[:, 0], y=X[:, 1], hue=y, style=y, markers=('s', 'o'), 
                palette=('r', 'b'), edgecolor='black')
plt.xlabel('$x_1$')
plt.ylabel('$x_2$')

blobs 数据集

现在我们实例化 Perceptron 类并将其拟合到数据集上:

perceptron = Perceptron(alpha=0.01, n_epochs=10)
perceptron.fit(X, y)

训练过程中的输出是:

Epoch 1: accuracy = 0.250
Epoch 2: accuracy = 0.950
Epoch 3: accuracy = 1.000

感知机在经过三次训练周期后已经收敛。

我们可以使用以下函数绘制感知机找到的决策边界和两个类别区域:

def plot_decision_boundary(model, X, y):
    # Retrieve the model parameters
    w1, w2, b = model.w[0], model.w[1], model.b

    # Calculate the intercept and slope of the separating line
    slope = -w1 / w2
    intercept = -b / w2

    # Plot the line
    x1 = X[:, 0]
    x2 = X[:, 1]
    x1_min, x1_max = x1.min() - 0.2, x1.max() + 0.2
    x2_min, x2_max = x2.min() - 0.5, x2.max() + 0.5
    x1_d = np.array([x1_min, x1_max])
    x2_d = slope * x1_d + intercept

    # Fill the two classification areas with two different colors
    plt.plot(x1_d, x2_d, 'k', ls='--')
    plt.fill_between(x1_d, x2_d, x2_min, color='blue', alpha=0.25)
    plt.fill_between(x1_d, x2_d, x2_max, color='red', alpha=0.25)
    plt.xlim(x1_min, x1_max)
    plt.ylim(x2_min, x2_max)

    # Draw the data points
    sns.scatterplot(x=x1, y=x2, hue=y, style=y, markers=('s', 'o'), 
                    palette=('r', 'b'), edgecolor='black')
    plt.xlabel('$x_1$')
    plt.ylabel('$x_2$')
plot_decision_boundary(perceptron, X, y)

感知机找到的分离超平面

Scikit-Learn 提供了自己的 Perceptron 类,实现了类似的算法,但提供了更多选项,如正则化和提前停止。

感知机模型的局限性

尽管感知机模型显示出一些初步的成功,但很快就意识到感知机无法学习一些简单的函数,如 XOR 函数:

XOR 问题无法通过感知机解决

XOR 问题不是线性可分的,因此线性模型如感知机无法解决它。

这一发现使得神经网络领域停滞了很多年(这一时期被称为“AI 冬天”),直到意识到通过将多个感知机堆叠在层中可以解决更复杂和非线性的问题,例如 XOR 问题。

多层感知机(MLPs)在 这篇文章 中有详细介绍。

最后的备注

除非另有说明,所有图片均为作者提供。

你可以在我的 GitHub 上找到这篇文章的代码示例:github.com/roiyeho/medium/tree/main/perceptrons

感谢阅读!

机器学习模型的性能评估技术

原文:towardsdatascience.com/performance-estimation-techniques-for-machine-learning-models-aaa83463bfa3?source=collection_archive---------7-----------------------#2023-03-02

Felipe de Pontes AdachiTowards Data Science Felipe de Pontes Adachi

·

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

--

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

一旦你的模型部署完成,监控其性能在确保机器学习系统质量方面扮演着至关重要的角色。为了计算如准确率、精确率、召回率或 f1-score 等指标,需要标签。然而,在许多情况下,标签可能不可用、部分可用或有延迟。在这些情况下,估计模型性能的能力会很有帮助。

在这篇文章中,我想讨论一些在没有真实数据的情况下估计性能的方法。

NannyML

NannyML 是一个用于检测模型静默失败、在没有标记数据的情况下估计部署后性能和检测数据漂移的 Python 包。目前,NannyML 有两种性能估计方法:基于置信度的性能估计 (CBPE) 和 直接损失估计 (DLE)。有关这些方法的详细描述,请参阅 NannyML 原始文档

a. 基于置信度的性能估计

正如名称所示,这种方法利用模型预测的置信度分数来进行性能估计。

注意事项:使用这种方法时需要注意一些要求和假设。

  • 置信度作为概率:置信度分数应该代表概率——例如,如果对于一大组观察对象分数为 0.9,那么它大约会正确 90%的时间。

  • 良好校准的概率:另一个要求是分数应该经过良好的校准,但这并不总是能做到。好消息是 NannyML 会在需要时内部进行校准。

  • 没有对之前未见过的空间区域的协变量偏移:例如,如果你的模型是在 10-70 岁的人群上训练的,而在生产中你的观察对象是 70 岁以上的人,这种方法可能无法提供可靠的估计。

  • 没有概念漂移:如果模型的输入与目标之间的关系发生变化,这种方法可能无法提供可靠的估计(我个人不知晓任何方法能做到这一点)。

  • 对回归模型适配不佳:回归模型通常不会内在地输出置信度分数,仅提供实际预测,因此这种方法在这些情况下的使用并不简单。

b. 直接损失估计

这种方法的直觉是训练一个额外的机器学习模型,其任务是估计监控模型的损失。这个额外模型称为保姆模型,监控模型称为子模型

注意事项:

  • 额外模型:需要训练一个额外的模型来估计原始模型的损失,这增加了系统的复杂性。然而,这个模型不必比原始模型更好,在许多情况下,这个过程可以很直接。

  • 适用于回归:这种方法非常适合回归任务。例如,保姆模型可以被训练来预测均方误差(MSE)或平均绝对误差(MAE)。

  • 没有对之前未见过的空间区域的协变量偏移:对 CBPE 的相同考虑也适用于这种方法。

  • 没有概念漂移:对 CBPE 的相同考虑也适用于这种方法。

  • 不同性能区域: 监控的模型在不同区域应该表现出不同的性能。例如,如果你的模型在一天的不同时间段或不同季节的表现有所不同。

重要性加权

我首次了解这种方法是通过参加了一个 O’Reilly 课程,名为实时机器学习性能监控,讲师是Shreya Shankar。直观上,你可以利用一个你已经有标签的参考数据集来估计未标记目标数据集的性能。这可以是你在部署前阶段使用的数据集,比如你最初训练模型时的测试集。为此,我们首先定义具有明确标准的分段,然后计算每个数据段的性能,比如准确性。这可以是根据年龄、职业或产品类别进行分段。例如,要估计目标数据集的准确性,你需要对数据应用相同的分段规则,并根据目标数据集的分段比例加权原始参考分段的准确性。

我非常喜欢这种方法,因为直观上非常清晰,实施也很简单。

我们在这里考虑的主要原因是参考数据集与目标数据集之间性能差异的原因是输入数据分布的变化。这被称为协变量漂移

考虑事项:

  • 无概念漂移: 如果模型输入与目标之间的关系发生变化,这种方法可能不会提供可靠的估计。再说一次——我不知道任何能做到这一点的方法。

  • 协变量漂移到特征空间未知区域: 对前述方法的考虑也适用于这种方法。

  • 分段的重要性: 选择适当的数据分段方式非常重要。目标数据集中的分段必须是参考数据集的子集,因为未见过的分段将没有关联的准确性。分段还应在训练准确性上具有较高的方差:如果所有分段的准确性相同,那么加权它们就没有太大意义。你还应该能够在生产过程中轻松进行分段,而无需手动标记——这正是我们想要避免的!

  • 互斥且穷尽的分段: 据我所知,这种方法仅适用于互斥且穷尽的分段。这意味着我们的分段彼此不重叠,并且所有分段的总和等于完整的数据集。

Mandoline

Mandoline 是一个用于评估 ML 模型的框架,利用标记的参考数据集和用户的先验知识来估计未标记数据集的性能。这里的关键见解是,用户可以利用他们的理解来创建“切片函数”,这些函数捕捉分布可能变化的轴向。这些函数可以通过编程或使用元数据来对数据进行分组。

与前一节中介绍的方法有相似之处,因为两者都使用了类似的桶/切片/段的概念,并且都利用这些分组来重新加权源数据。在 Mandoline 中,创建的切片将帮助指导后续的密度估计过程,这些估计结果然后用于重新加权源数据集,并输出性能估计。

Mandoline: 分布转移下的模型评估 — arxiv.org/abs/2107.00643

这篇论文非常有趣,值得一读,结果看起来也很有前景。他们还提供了一个框架的 Python 实现,我计划在未来的文章中深入探讨。

考虑因素

  • 没有概念漂移: 再次强调,问题的表述假设分布之间没有概念漂移。

  • 适用于噪声较大、未指定的切片: 即使你的切片存在噪声和/或未指定的情况,这种方法仍然有效。举个例子,如论文中所述 — 使用 CivilComments 数据集 检测毒性,并且你想根据人口统计信息进行分段,比如男性、女性、基督教徒、LGBTQ 等。你可以使用正则表达式函数来查找关键词,如“man, male, female”来对数据进行分组。这种方法不会每次都准确地获得切片,但即便如此,这种方法效果很好。此外,据我了解,这种方法适用于重叠的段 — 在这种情况下,每个评论可能提到一个或多个人口统计信息。

  • 切片的重要性: 设计切片函数时,准确性至关重要。它们应与当前任务相关,并能有效捕捉分布中可能发生的不同轴向的变化。

未来实验

在未来的帖子中,我计划在实际应用案例中实验每种方法。目标不是比较每种方法的性能,而是简单展示我们如何在实际场景中使用这些方法。由于每种方法有不同的需求集,我们可能会使用不同的数据集和用例。例如,Mandoline 支持使用重叠片段。另一方面,Importance Weighting 方法适用于互斥且全面的分箱。CBPE 和 DLE 方法都不假设任何形式的分割,但我们确实需要一个具有良好校准的置信度分数的模型(CBPE),或者训练一个额外的模型(DLE)。

感谢阅读!

机器学习模型的性能估计技术”最初发表于 作者的个人通讯*。

快速修复你缓慢的 Python 代码

原文:towardsdatascience.com/performance-fix-for-slow-python-lru-cache-f9a454776716

大幅度加速可重复的计算密集型任务。

Thuwarakesh MurallieTowards Data Science Thuwarakesh Murallie

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

--

Ryan JohnstonUnsplash上的照片

计算密集型任务现在无处不在。

现在我们经常使用资源密集型技术,如 LLMs 和生成式 AI。

任何使用珍贵资源的人都会知道,再次执行相同任务是多么令人畏惧,即使我们知道结果会是相同的。你会责怪自己没有存储之前运行的结果。

这就是 LRU 缓存发挥作用的地方。LRU 代表最近最少使用。它是众多缓存策略之一。让我们首先了解它的工作原理。

## 我在几乎所有数据科学项目中使用的 5 个 Python 装饰器

装饰器提供了一种新的方便方法,用于从缓存到发送通知的一切操作。

towardsdatascience.com

@lru_cache 在 Python 中是如何工作的?

想象你的大脑是一个小玩具箱。它只能容纳五个玩具。你的朋友们不断询问不同的玩具,你利用你超人的记忆力记住并讲述这些玩具的故事。

一些玩具很容易找到,因为你的朋友们经常询问它们,所以它们在最上面。一些玩具则更难找到,因为它们在箱子底部。

在你讲述一个玩具的故事后,你把它放回顶部,以便让事情变得更容易。这样,你的朋友们最常问的玩具总是容易找到。这就是“最近最少使用”或 LRU 策略。

如果你得到一个新玩具,但盒子满了,你会移除最久未被询问的玩具。如果一个朋友询问,你仍然可以在你的大玩具仓库中找到它,这需要更长的时间。这就是 LRU 缓存的工作方式!

创建 GPT3 支持的应用程序仅需几分钟,使用 Streamlit [## 创建 GPT3 支持的应用程序仅需几分钟,使用 Streamlit

学习如何构建智能应用,而不必过于担心软件开发。

levelup.gitconnected.com

从版本 3.2 开始,Python 自带了一个内置的 @lru_cache 装饰器,你可以在任何函数上使用它。它使你的过程工作得很像玩具箱的例子。

如果某部分之前已经执行过,结果将在后续运行中从缓存中提取。就像你的玩具箱变满了一样,你可以指定缓存的最大大小。

小型 Pandas 技巧处理有限内存的大数据集 [## 小型 Pandas 技巧处理有限内存的大数据集

Pandas 默认设置并不理想。一个小配置可以将你的数据框压缩到适合你的内存中。

towardsdatascience.com

我们在日常工作中可以在哪里使用 lru_cache

考虑一个 长期运行的数据库查询。我们都经历过:等待,手指敲击,直到数据库最终响应所需的数据。这通常发生在我们需要重新运行 Jupyter Notebooks 时。

现在,想象一下,第一次等待一次,然后下次立即检索数据。这就是 lru_cache 的魔力。

不仅仅是关于数据库查询。那 API 呢?尤其是那些具有 按需付费定价模型 的 API,如 OpenAI API。如果提示看起来相同,使用 lru_cache,我们调用一次,支付费用,然后重用结果而无需再次支付。

此外,如果你处理的外部数据查询连接到源的可靠性不总是很高——比如 网页抓取——lru_cache 可以成为救星。你可以一次性获取数据,将其缓存,然后在无需担心连接问题的情况下引用它,而不是不断与 不稳定的连接 做斗争。

Python 网页抓取的宁静交响曲——分为 3 个部分 [## Python 网页抓取的宁静交响曲——分为 3 个部分

在 Python 中进行网页抓取的最简单、最灵活和最全面的方法

levelup.gitconnected.com](https://levelup.gitconnected.com/3-ways-of-web-scraping-in-python-e953c4a96ec2?source=post_page-----f9a454776716--------------------------------)

另外,不要忘记计算密集型任务。如果你的函数需要处理大量数据并进行复杂计算,lru_cache可以显著减少对系统资源的负担。当你的应用涉及运行重型机器学习模型如BERT LLM或拥有全面的搜索功能时,情况通常是这样的。

在所有这些情况下,价值主张都是相同的。节省时间、节省金钱、节省资源。这在输出保持一致的合理时间范围内是可能的。从宏观上看,Python 的lru_cache不是一个技术解决方案,而是一种效率原则。在每一秒都很重要的世界里,效率就是至高无上的。

测试lru_cache在递归函数上的效果

这是一个斐波那契数生成的典型示例。看看使用和不使用lru_cache的运行时间。

import time

def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

start_time = time.time()
print(fib(40))
print("Execution Time: ", time.time() - start_time)

>> 102334155
>> Execution Time: 19.45328450202942

现在让我们从functools模块导入lru_cache并对函数进行标注。

import time
from functools import lru_cache

@lru_cache(maxsize=None) # Invinite cache
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

start_time = time.time()
print(fib(40))
print("Execution Time: ", time.time() - start_time)

>> 102334155
>> Execution Time: 8.893013000488281e-05

通过引入lru_cache装饰器来进行备忘录化,与未备忘录化的版本相比,代码实现了显著的时间节省。

在第一个示例(没有lru_cache)中,计算第 40 个斐波那契数大约花费了 19.45 秒。在第二个示例(使用lru_cache)中,计算第 40 个斐波那契数仅花费了大约 8.89e-05 秒。

这就像节省了 99.99%的时间。

这是不同斐波那契数执行时间的图表。可以看到,非缓存版本的执行时间呈指数增长,而缓存版本即使对于更大的数字也只需要极少的时间。

作者提供的图片 — 展示了@lru_cache如何提高斐波那契数生成的执行时间。

## SQL on Pandas — 我新的 10 倍速度的最爱。

将两者的最佳之处结合在一起

towardsdatascience.com

@lru_cache并不是万灵药。

虽然@lru_cache在提升性能方面非常重要,但这并不意味着你可以随意使用任何函数而不考虑性能。

## 你的 Python For-loop 很慢?使用 NumPy 替代

当速度很重要时,列表不是最佳选择。

towardsdatascience.com

被缓存的函数必须是可哈希的。

这意味着它必须是不可变的,并且能够转换为用于字典键目的的唯一整数。对于函数来说,这通常不是问题,但你不能缓存可变类的方法或接受可变参数的函数。

例如,以下函数是不可变的,因此lru_cache工作正常:

@lru_cache(maxsize=128)
def add_ten(number):
    return number + 10

print(add_ten(5))

>> 15

但下一个则不是。调用它会抛出错误。

@lru_cache(maxsize=128)
def add_to_list(lst, item):
    lst.append(item)
    return lst

print(add_to_list([1, 2, 3], 4))  

>> TypeError: unhashable type: 'list'

## Python 3.11 确实比 3.10 更快

与冒泡排序和递归函数逐一比较确认了这一点。

towardsdatascience.com

如果你的函数依赖于外部源,lru_cache就不太适用了

缓存不会自动过期。如果函数的返回值可能会随时间变化(例如,如果它基于文件系统或网络操作),那么使用lru_cache可能会导致返回过时的结果。

以下 Python 脚本模拟了一个外部源并进行了测试:

import functools
import time

def simulate_external_source():
    """Simulate an external source that changes over time."""
    return time.time()

@functools.lru_cache()
def dependent_function():
    """Function that depends on an external source."""
    return simulate_external_source()

# Test lru_cache with a function that depends on external sources
print("Testing lru_cache with a function that depends on external sources:")
print("Initial call:", dependent_function())
time.sleep(1)  # Simulate a delay
print("Cached call:", dependent_function())  # Should return the same result
time.sleep(1)  # Simulate a delay
print("Updated call:", dependent_function())  # Should still return the same result

print()

@functools.lru_cache(maxsize=1)
def expiring_function():
    """Function with cached entries that expire over time."""
    return simulate_external_source()

# Test lru_cache with entries that expire over time
print("Testing lru_cache with entries that expire over time:")
print("Initial call:", expiring_function())
time.sleep(1)  # Simulate a delay
print("Expired call:", expiring_function())  # Should trigger recalculation
time.sleep(1)  # Simulate a delay
print("Expired call:", expiring_function())  # Should trigger recalculation again

print()

___________________
Output
-------------------
Testing lru_cache with a function that depends on external sources:
Initial call: 1685507725.6362917
Cached call: 1685507725.6362917
Updated call: 1685507725.6362917

Testing lru_cache with entries that expire over time:
Initial call: 1685507727.639048
Expired call: 1685507727.639048
Expired call: 1685507727.639048

对于这种情况,你需要一种支持 TTL 的缓存技术。

## 如何在每次提交时使用 GitHub Actions 运行 Python 测试?

自动化无聊的任务,确保你的代码质量,通过 CI 管道。

towardsdatascience.com

结论

lru_cache是一种非常简单的技术,用于存储函数输出,以便计算无需重复执行。虽然它在节省昂贵计算资源方面很有用,但并非所有情况下都能很好地工作。

然而,只要你的函数不依赖外部输入且是可哈希的,lru_cache绝对是一个很好的工具。

感谢阅读,朋友!在LinkedInTwitterMedium上向我打个招呼吧。

还不是 Medium 会员?请使用此链接成为会员,因为在不增加你额外费用的情况下,我可以获得少量佣金作为推荐费。

使用 Python 和 OpenCV 进行图像注释

原文:towardsdatascience.com/performing-image-annotation-using-python-and-opencv-f0124746613c

学习如何为你的图像创建边界框

Wei-Meng LeeTowards Data Science Wei-Meng Lee

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

--

图片由 Héctor J. RivasUnsplash 提供

深度学习中的常见任务之一是 对象检测,这是一个在给定图像中定位特定对象的过程。对象检测的一个例子是检测图像中的汽车,你可以统计图像中检测到的汽车总数。在需要分析特定交叉口的交通流量时,这可能会很有用。

为了训练一个深度学习模型以检测特定对象,你需要为模型提供一组训练图像,并将图像中特定对象的坐标标记出来。这个过程称为 图像注释。图像注释为图像中存在的对象分配标签,并将所有对象标记出来。

在本文中,我将展示如何使用 Python 和 OpenCV 为图像添加注释——你将使用鼠标标记你要注释的对象,应用程序将绘制一个边界矩形围绕该对象。然后,你可以查看你标记出的对象的坐标,并选择性地将其保存到日志文件中。

使用 OpenCV 显示图像

首先,创建一个文本文件,命名为 bounding.py。然后,填写以下语句:

import argparse
import cv2

ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required = True, help = "Path to image")
args = vars(ap.parse_args())

# load the image
image = cv2.imread(args["image"])

# reference to the image
image_clone = image

# loop until the 'q' key is pressed
while True:
    # display the image 
    cv2.imshow("image", image)

    # wait for a keypress
    key = cv2.waitKey(1)
    if key == ord("c"):
        break

# close all open windows
cv2.destroyAllWindows()

上述 Python 控制台应用程序从控制台接收一个参数,即要显示的图像名称。获取图像名称后,你将使用 OpenCV 显示图像。同时,你还需要克隆图像,以便以后使用。要停止程序,你可以按键盘上的 Ctrl-C。

要运行程序,打开终端并输入以下命令:

$ python bounding.py -i Cabs.jpg

上述Cabs.jpg文件可以从 https://en.wikipedia.org/wiki/Taxi#/media/File:Cabs.jpg 下载。

现在应该可以显示图像了:

来源:en.wikipedia.org/wiki/Taxi#/media/File:Cabs.jpg。图片由用户 Omnibus,Uris 提供 — Uris 拍摄了这张照片。

处理鼠标事件

我们希望用户能够使用鼠标点击图像,然后在图像上拖动以选择特定的感兴趣区域(ROI)。为此,让我们在程序中添加两个全局变量:

import argparse
import cv2

# to store the points for region of interest
roi_pt = []

# to indicate if the left mouse button is depressed
is_button_down = False

下图显示了roi_pt将如何存储 ROI 的坐标:

图片由作者提供

你现在将定义一个名为draw_rectangle()的函数来处理鼠标点击。此函数接受五个参数 — eventxyflagsparam。我们只会在这个练习中使用前三个参数:

def draw_rectangle(event, x, y, flags, param):
    global roi_pt, is_button_down

    if event == cv2.EVENT_MOUSEMOVE and is_button_down:
        global image_clone, image

        # get the original image to paint the new rectangle
        image = image_clone.copy()

        # draw new rectangle
        cv2.rectangle(image, roi_pt[0], (x,y), (0, 255, 0), 2)

    if event == cv2.EVENT_LBUTTONDOWN:
        # record the first point
        roi_pt = [(x, y)]  
        is_button_down = True

    # if the left mouse button was released
    elif event == cv2.EVENT_LBUTTONUP:        
        roi_pt.append((x, y))     # append the end point

        # ======================
        # print the bounding box
        # ======================
        # in (x1,y1,x2,y2) format
        print(roi_pt)                  

        # in (x,y,w,h) format
        bbox = (roi_pt[0][0],
                roi_pt[0][1],
                roi_pt[1][0] - roi_pt[0][0],
                roi_pt[1][1] - roi_pt[0][1])
        print(bbox)

        # button has now been released
        is_button_down = False

        # draw the bounding box
        cv2.rectangle(image, roi_pt[0], roi_pt[1], (0, 255, 0), 2)
        cv2.imshow("image", image)

在上述函数中:

  • 当左鼠标按钮被按下(cv2.EVENT_LBUTTONDOWN)时,你记录 ROI 的第一个点。然后将is_button_down变量设置为True,以便在用户按住左鼠标按钮移动鼠标时开始绘制矩形。

  • 当用户在按住左鼠标按钮的情况下移动鼠标(cv2.EVENT_MOUSEMOVE and is_button_down),你将会在原始图像的副本上绘制矩形。你需要在克隆图像上绘制,因为当用户移动鼠标时,你还需要去除之前绘制的矩形。因此,完成这项任务的最简单方法是丢弃之前的图像,并使用克隆图像绘制新的矩形。

  • 当用户最终释放左鼠标按钮(cv2.EVENT_LBUTTONUP)时,你将 ROI 的结束点添加到roi_pt中。然后打印出边界框坐标。对于一些深度学习包,边界框坐标的格式为(x, y, width, height),所以我也以这种格式计算了 ROI 的坐标:

图片由作者提供

  • 最后,为 ROI 绘制边界框

要将鼠标事件与其事件处理程序连接起来,请添加以下语句:

...

# reference to the image
image_clone = image

# ======ADD the following======
# setup the mouse click handler
cv2.namedWindow("image")
cv2.setMouseCallback("image", draw_rectangle)
# =============================

# loop until the 'q' key is pressed
while True:
    ...

再次运行程序,你现在可以从图像中选择 ROI,矩形将被显示出来:

图片由作者提供

同时,ROI 的坐标也将显示出来:

[(430, 409), (764, 656)]
(430, 409, 334, 247)

为了方便起见,这里是完整的 Python 程序:

import argparse
import cv2

# to store the points for region of interest
roi_pt = []

# to indicate if the left mouse button is depressed
is_button_down = False

def draw_rectangle(event, x, y, flags, param):
    global roi_pt, is_button_down

    if event == cv2.EVENT_MOUSEMOVE and is_button_down:
        global image_clone, image

        # get the original image to paint the new rectangle
        image = image_clone.copy()

        # draw new rectangle
        cv2.rectangle(image, roi_pt[0], (x,y), (0, 255, 0), 2)

    if event == cv2.EVENT_LBUTTONDOWN:
        # record the first point
        roi_pt = [(x, y)]  
        is_button_down = True

    # if the left mouse button was released
    elif event == cv2.EVENT_LBUTTONUP:        
        roi_pt.append((x, y))     # append the end point

        # ======================
        # print the bounding box
        # ======================
        # in (x1,y1,x2,y2) format
        print(roi_pt)                  

        # in (x,y,w,h) format
        bbox = (roi_pt[0][0],
                roi_pt[0][1],
                roi_pt[1][0] - roi_pt[0][0],
                roi_pt[1][1] - roi_pt[0][1])
        print(bbox)

        # button has now been released
        is_button_down = False

        # draw the bounding box
        cv2.rectangle(image, roi_pt[0], roi_pt[1], (0, 255, 0), 2)
        cv2.imshow("image", image)

ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required = True, help = "Path to image")
args = vars(ap.parse_args())

# load the image
image = cv2.imread(args["image"])

# reference to the image
image_clone = image

# setup the mouse click handler
cv2.namedWindow("image")
cv2.setMouseCallback("image", draw_rectangle)

# loop until the 'q' key is pressed
while True:
    # display the image 
    cv2.imshow("image", image)

    # wait for a keypress
    key = cv2.waitKey(1)
    if key == ord("c"):
        break

# close all open windows
cv2.destroyAllWindows()

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

[## 使用我的推荐链接加入 Medium - Wei-Meng Lee

阅读 Wei-Meng Lee 的每一个故事(以及 Medium 上成千上万的其他作者的故事)。你的会员费直接支持…

weimenglee.medium.com](https://weimenglee.medium.com/membership?source=post_page-----f0124746613c--------------------------------)

摘要

在这篇短文中,我展示了如何通过选择图像中的对象来注释图像。当然,一旦对象的坐标被映射出来,你需要将其存储在外部文件中(如 JSON 或 CSV 文件)。为此,我将其留给读者作为练习。如果你觉得这有用,或者你在日常工作中使用哪些注释工具,请告诉我。

哲学与数据科学——深入思考数据

原文:towardsdatascience.com/philosophy-and-data-science-thinking-deeply-about-data-222cc9fbdcc5

第二部分:认识论

Jarom HuletTowards Data Science Jarom Hulet

·发表于Towards Data Science ·阅读时长 11 分钟·2023 年 11 月 28 日

--

图片由 Alex Pere 提供,来源于 pexels.com

阅读完这篇文章后,我希望你能对几千年来对知识的深刻思考如何应用于你作为数据科学家的日常工作有一个实际的理解。

这是关于哲学概念如何帮助我在数据科学工作中的系列文章的第二篇。第一篇文章讨论的是决定论(link);一个具体的形而上学理论。本文将涉及哲学领域中称为认识论的多个思想流派。

认识论是研究我们能知道什么以及如何知道它的学科。它是对知识本身的研究。这与数据科学非常契合,因为我们试图从数据中获取知识!

认识论是研究我们能知道什么以及如何知道它的学科。它是对知识本身的研究。

我们将涵盖以下内容:

  1. 归纳推理与演绎推理

  2. 怀疑主义

  3. 实用主义

归纳推理与演绎推理

推理是我们理性化和捍卫知识的方式。它是我们知道某事的原因。推理有多种类型,但我认为最常见(也是最适用)的就是演绎推理和归纳推理。

演绎推理

演绎推理得出的结论是前提的逻辑结果。在演绎推理中,前提和论点形成一个封闭的系统。如果前提是正确的且逻辑没有谬误,我们就有充分的理由相信我们提出的知识(论点中的“因此”或“那么”部分)。

换句话说,如果我们假设前提为真,演绎论证的结论就是知识!当然,演绎论证的战场(假设没有逻辑错误)在于前提是否真正成立。

以下是一个非常简单的演绎推理的例子:

演绎推理的简单例子——作者提供的图像

注意,如果我们接受所有鸭子都有翅膀,并且 Huey 是一只鸭子,我们必须接受 Huey 也有翅膀。否则会在逻辑上不一致。

作为数据科学家,我们在讨论模型有效所需的假设时使用演绎推理。进行演绎推理后,我们通常会展示支持我们提出的前提的证据。下面的例子可能对你来说很熟悉:

机器学习中使用的演绎推理的例子——作者提供的图像

我们提出使我们的预测模型或分析有效的必要假设,并声称如果这些假设都成立,我们的结果也有效。然后我们努力证明我们有理由相信这些假设是全面和正确的。

归纳推理

演绎推理发生在我们创建前提的无菌空间中。而归纳推理发生在混乱的世界中!

归纳推理也涉及前提和结论,但前提通常是证据而不是逻辑命题。归纳推理是基于我们对世界的观察得出结论。

归纳推理的一个例子是:在所有观察到的历史中,太阳早上都升起,因此它明天也会升起。演绎推理和归纳推理之间的一个决定性区别是,通过归纳推理,我们可以在没有产生逻辑矛盾的情况下犯错。

演绎推理和归纳推理之间的一个决定性区别是,通过归纳推理,我们可以在没有产生逻辑矛盾的情况下犯错。

归纳推理的经典例子——作者提供的图像

归纳和演绎之间的区别在这里很微妙。归纳前提是“太阳每天早上都升起”,而类似的演绎前提则是“太阳升起每天早上”。

仅仅因为太阳在我们观察到的时间里升起,并不意味着太阳明天必须升起。

归纳结论不严格跟随前提——作者提供的图像

希望你能够看到,在演绎推理下,确凿的知识变得更加具有挑战性,因为我们的结论不必跟随我们的前提。这个问题被称为“归纳问题”,我们将在怀疑论部分进一步讨论。

机器学习是归纳的。这意味着从可用的信息中,我们推断出周围世界的关系/知识。我们有观察性的理由来做出具体预测,但我们的预测错误并不会导致逻辑矛盾。

机器学习的归纳结论可能是错误的而不会导致逻辑矛盾——作者提供的图像

机器学习模型使用归纳推理,这使得它们容易受到“归纳问题”的影响。这个问题的根源在于,归纳前提并不总是导致论证结论。这可能引发对我们获得知识的有效性的合法担忧。仅仅因为我们以前见过某件事发生很多次,并不意味着我们一定会再次看到它。

哲学学派怀疑主义的一个重要支持者是归纳问题。怀疑主义认为所有的机器学习模型都是无用的,因为它们都有可能是错误的。然而,尽管归纳推理面临挑战,我们知道我们的模型通常是非常有用和实际的——这一观点与实用主义的哲学一致。我们将首先讨论怀疑主义,然后讨论实用主义!

怀疑主义

怀疑主义是一种哲学立场,其特点是对知识的怀疑或悬置。当涉及到认识论时,对怀疑主义的一个更具说服力的论据来自于之前提到的归纳问题。我将多谈一点归纳问题,然后转到讨论怀疑视角如何在数据科学领域中发挥作用。

归纳问题

尼瑟姆·塔勒布的《被随机性愚弄》和《黑天鹅》两本书都详细讨论了归纳问题。后一本书的名字是对这个问题的致敬(如果这个问题对你有趣,我推荐你阅读这两本书!)。

归纳问题的一个常见示例是“黑天鹅”例子。例子如下:如果你看到一千只天鹅,且它们都是白色的,你可能会得出所有天鹅都是白色的结论。当你看到第一只黑天鹅时,你的世界观将会颠覆!

塔勒布讨论了那些根据已经发生的事情而不是可能发生的事情做出交易决策的投资者的例子。结果是,他们在一段时间内取得了成功,直到遇到隐喻中的“黑天鹅”,即一个前所未有或极其罕见的事件,这个事件抹去了他们所有的收益并造成了巨大的损失。他的主要观点是,通过考虑所有可能发生的事情,而不仅仅是已经发生的事情,来避免归纳问题。

归纳问题可以在逻辑上导致怀疑主义:

归纳问题如何导致怀疑主义——作者提供的图像

怀疑主义如何让你成为更好的数据科学家?

虽然怀疑可能是一种会因压倒性的怀疑而导致无所作为的瘫痪性哲学,但适量的怀疑可以帮助你在数据科学工作中更聪明。我们将在下一节讨论实用主义——它是对怀疑所带来的瘫痪副作用的回答。

但首先,这里是我认为怀疑的视角如何有助于提高你数据科学工作质量的几种方式。

怀疑主义:

  1. 防止你相信你看到的所有东西——由于怀疑主义,我们严格寻找模型有效性的证据。我们查看样本外和时间外的表现,而不是训练表现。如果我们的错误异常低,我们会寻找泄漏或模型在训练和验证过程中获得的其他“非公平”优势,这些在实际生产中是没有的。正因为这种视角,我们构建了更为稳健和有防御性的模型,并且对模型在实际使用中的表现有了更好的了解。

  2. 帮助确保你将模型的局限性传达给用户——当你心中有归纳问题时,你更有可能清晰地向用户传达模型的局限性。你更可能使用诸如“我们的模型在未来类似于过去时有效”或“我们的模型可能会在我们过去见过的类似输入范围内做出良好预测”的措辞。这很有帮助,因为它向用户传达了在何时使用模型以及在何时对模型的输出保持警惕。它还有助于通过谨慎设定期望来维护你的声誉。如果发生意外情况,模型的预测变得无用,模型用户不会完全感到困惑,你的信誉将得到保留!

  3. 帮助管理与模型预测偏差相关的风险——换句话说,合理的怀疑可以防止你或你的商业伙伴对模型预测“全盘接受”。你明白未来可能存在的情况可能导致模型做出不佳的预测,你在考虑如何在整体战略中使用模型时会考虑到这一点。当模型建议进行大幅度变动时,你可以从模型推荐的方向上的小幅度变动开始,理解模型可能无法完全捕捉到你希望它掌握的知识。

  4. 使你避免将模型所说的每一件事都当作真理——有时模型会给出违背直觉的结果。怀疑的视角会使你怀疑这些‘知识’,并进一步调查。没有怀疑的视角,你可能会说‘这就是数据所说的,一定是对的!’在我的职业生涯中,我遇到过多次这种情况。几年前,我在一个项目中工作,当时我的公司在需求下降与折扣无关的时候降低了产品价格。我根据数据构建的模型建议,产品价格越低,我们的销量越少!模型捕捉到了无关的趋势。如果我没有怀疑的视角,促使我深入挖掘这个不合逻辑的结果并发现真正的原因,我不敢想象与我老板的会议会怎么进行!

实用主义

实用主义是反驳怀疑主义的认识论学派,认为我们有‘相信’我们有证据支持的事物的‘权利’。这个‘权利’的正当性来自于这样一种观点:事实只有在我们使用时才变得有用(知识)。因此,如果我们因怀疑而陷入瘫痪,我们就没有知识,因为我们没有利用我们所拥有的事实。实用主义强调我们信念或知识的实际后果。

实用主义强调我们信念或知识的实际后果。

威廉·詹姆斯(实用主义思维的领军人物)曾著名地断言,事实不是真理,事实只是存在。当我们找到使用事实的有用方法时,我们就拥有了真理和知识。

詹姆斯用一个迷失在树林中的人来说明实用主义的观点。有一个迷失在树林中的人发现了一条小径。他们可能充满怀疑,因为他们不知道小径是否通向文明,于是坐在地上挨饿。或者他们可以采取实用的态度,相信这条小径会通向某个地方,并沿着它走向文明。

在这里需要注意的是,实用主义并不提议我们使用随机或没有根据的信念来做决定。实用主义要求信念有足够的证据支持。

知识信心的谱系

我将我们对知识的信心看作是在一个谱系上。从完全怀疑主义——对任何知识都没有信心,到完全教条主义——对知识完全自信,即使在没有证据的情况下。

知识信心的谱系——图像由作者提供

在数据科学应用中,在完全怀疑主义下,我们认为我们的机器学习模型或数据分析对世界没有任何有用的指示,因为总有可能我们可能是错的。在完全教条主义下——我们完全相信我们的模型是正确的,即使我们没有有力的证据,模型的预测也是准确的。

在极左(完全怀疑主义),我们被怀疑的疑虑所困扰,导致无法做出任何决策。在极右(完全教条主义),我们过于自信,导致对预测过度下注。我们应该在‘知识信心谱系’的中间位置。这意味着我们应该相信只有在证据表明时才有用的事情。我们不应该比证据所允许的更悲观或乐观。我认为这种在谱系上的平衡就是实用主义。

实用主义通过允许信念但要求证据来平衡怀疑主义和教条主义——作者图片

实用主义如何让你成为更好的数据科学家?

实用主义:

  1. 帮助你关注结果——实用主义完全关注我们知识或信念的影响。如果一个模型给出了‘有趣’的结果,但在解决问题上并无帮助,那么这个模型是无用的。实用主义可以通过让我们专注于结果驱动的数据科学,帮助我们与商业伙伴保持一致。

  2. 让你不断问‘那又怎样?’——有人曾告诉你一个有趣的事实,而你想或说了‘那又怎样?’我肯定曾在那个问题的两边都待过!如果我们以实用主义的视角看待数据科学,我们将始终有这个问题的答案,因为我们会相信没有答案意味着我们没有什么有价值的东西可以分享。

  3. 帮助避免‘分析瘫痪’——我很确定每个数据专业人士都曾经历过这个状态。‘分析瘫痪’是指你不断收集信息却无法做出决策的状态。最终结果是你有一堆事实,但没有行动。这是实用主义者的灾难!健康的实用主义视角将帮助你只收集有助于决策的信息,从而避免‘分析瘫痪’。

  4. 提供了理解模型价值的基础——在实用主义下,一个模型的好坏取决于它帮助我们做出的决策。如果我们有一个误差非常低的模型,但对组织没有很大影响,在实用主义下,它的价值不大。实用主义还建议我们应该尝试理解模型的影响。例如,模型去年帮助节省了 230 万美元的浪费,或者模型早期正确诊断了 500 名患者,从而改善了治疗结果。关注模型的价值可以帮助我们优化努力,最大化增加的价值!

  5. 提供了比较模型的实际基础——通常,我们在比较模型时选择一些误差度量。无论哪个模型的验证误差更低,我们就认为哪个模型更好。实用主义建议我们应该考虑模型对决策的影响来比较模型,而不是误差。也许我们有两个模型,一个的误差低于另一个,但两个预测都导致了相同的决策。在实用主义下,这两个模型是等价的!

  6. 使我们能够‘相信’我们的模型是有用的,并因此使用它——通过务实主义,我们避免了对模型的极端怀疑。我们不必花费余生来验证和完善模型再加以使用。当我们遵循数据科学最佳实践来验证模型时,我们有权相信它是有用的(因此,利用它做决策)。

结论

采用各种认识论哲学对你作为数据科学家的工作可能有所帮助。理解演绎推理归纳推理之间的区别可以帮助我们理解在开发模型和分析时我们做出了什么假设。怀疑主义可以帮助我们寻找严格的证据来证明我们的模型是有用的。务实主义可以帮助我们接受我们所发现的证据,以采取有益的行动。希望这篇文章扩展了你对认识论深刻思想如何帮助你成为更好的数据科学家的思考!

哲学与数据科学——深刻思考数据

原文:towardsdatascience.com/philosophy-and-data-science-thinking-deeply-about-data-f9b3960c9897

第一部分:决定论

Jarom HuletTowards Data Science Jarom Hulet

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

--

图片来源于 Pexels.com 上的‘HH’

数据科学是一项非常技术性、深入细节的工作。我们通常会非常专注于具体的问题——这很好。我们通过将专注的注意力和技能结合起来解决问题来创造最大价值。但是,我认为偶尔退一步,试图看到更大的图景,是一种很好的实践。

学习哲学是我发现对深入思考数据科学非常有效的工具。作为一个哲学的业余学生,我观察到一些哲学思维领域与数据科学有很好的交织。具体来说,我发现形而上学、因果关系和认识论有很多理论是非常适用的。

这是一个多部分系列的第一篇,讨论了各种哲学观点及其对数据和数据科学的影响。我将从迷人的决定论的形而上学理论开始。

什么是决定论?

决定论是关于我们宇宙本质的哲学理论。决定论有多种不同的细微版本¹,但总体想法是我们宇宙中没有随机性。每一个事件都有一组原因,这些原因完全解释了事件,而这些原因本身也有一组原因。从宇宙开始(或者可能宇宙没有开始²?)起,因果链是不断裂的。

以下是拉普拉斯的名言,它概括了对物理世界的决定论观点:

“我们可以将宇宙的现状视为其过去的结果和未来的原因。如果某一时刻有一个智力能够知道所有使自然运动的力量,以及自然中所有物体的所有位置,如果这个智力也足够庞大以将这些数据进行分析,它将会在一个公式中包含宇宙中最大物体和最小原子的运动;对于这样的智力来说,没有什么是不确定的,未来就像过去一样,会展现在它的眼前。”

皮埃尔-西蒙·拉普拉斯,《概率的哲学论文》(1814)

我发现确定论在以下数据科学主题中出现(我相信还有很多其他的——如果我遗漏了什么,请告诉我!):

  • 概率论

  • 不可约错误的概念

  • 理论上的‘神’模型

  • 因果关系和实验设计

  • 随机数

概率论

概率的研究主要是了解随机变量的行为。随机变量表示一个包含随机性的过程的结果。例如,掷骰子。我们可以了解某些结果的概率,但我们无法确定单次投掷的结果——大概是由于随机性。

确定论理论拒绝宇宙中存在任何随机性。那么我们为什么有研究随机变量的概率领域呢?当然,非确定论者会说宇宙中确实存在随机性。但确定论者可能会说整个概率领域是由于人类“认知极限”而创建的。

认知极限弥合了宇宙中感知的随机性与确定论理论之间的差距。这些极限可以定义为可以知道或理解的界限。如果宇宙是完全确定的,我们可以假设性地知道每次掷骰子的结果(考虑一下拉普拉斯的引述)。如果我们能够收集和理解所有影响每次掷骰子的变量之间的因果关系,我们可以以 100%的信心计算出掷骰子的结果(如果宇宙是确定的)。但想象一下,我们需要知道多少才能做这样的计算!骰子的缺陷、骰子在我手中的准确位置、我摇动手的方式、那天的气压、着陆面地面的硬度等等。

认知极限弥合了宇宙中感知的随机性与确定论理论之间的差距。

确定论者可以接受事物看起来随机,因为她会认为事物看起来随机的原因是由于我们的认知极限。由于这些极限,概率仍然是一个非常有用的研究领域,无论确定论是否正确描述了我们宇宙的本质。

不可约错误

机器学习模型试图在给定数据集的情况下进行预测。通常,这些模型只是对系统如何运作的估计或近似。换句话说,模型往往存在一定程度的错误——我们称之为误差。确定性对模型误差具有理论影响!

模型的误差可能来源于三种不同的来源:

  1. 模型近似

  2. 不可用的数据

  3. 随机噪声

模型近似

图片来源:作者

当我们创建一个预测模型时,我们是在估计目标和预测变量之间的真实关系。我们希望得到一个接近的近似值。这就是为什么你可能会听到“估计模型”和“训练模型”被交替使用。

例如,当我们估计线性回归模型时,我们假设所有预测变量与目标变量之间有线性关系。对这一假设的违反(即使是小的违反)会导致至少一些误差。

不可用的数据

图片来源:作者

这种类型的误差来自于描述系统所需的缺失数据。它可能因为不可观察或无法准确量化(例如,预测超速的驾驶员情绪)而缺失,或者因为根本无法获得(例如,网站未设置以捕捉潜在客户在结账页面上花费的时间来预测完成购买的概率)。

随机噪声

随机性(假设存在)是模型误差的第三个原因。随机性根据定义是无法预测的,即使拥有所有必要的特征和完美的机器学习方法。

不可减少的误差

现在我们了解了模型误差的来源,让我们谈谈这种误差的性质。一般而言,误差(无论来源如何)可以被分类为可减少的或不可减少的。

  1. 可减少的误差可以通过改进模型从训练数据中学习的方式来减少

  2. 不可减少的误差是无论我们的模型如何拟合训练数据,都无法消除的误差。我认为不可减少的误差进一步划分为‘局部不可减少的误差’和‘普遍不可减少的误差³’。

    A. 我定义局部不可减少的误差为由于数据科学工具的限制或本地或 readily 可用的数据限制而无法减少的误差。例如,在彻底测试所有可用的机器学习算法后仍然存在的误差。或者因为我们无法访问所有解释目标变量的数据点而存在的误差。局部不可减少的误差存在是因为我们生活在一个不完美的世界中,它承认我们只能在给定的工具和数据下做这么多事情。

    B. 普遍不可减少的误差 是指在局部约束解除后仍然存在的误差。我们必须进入一个假设的世界来理解这一点。这是指如果我们拥有完美的机器学习算法和完全解释目标变量所需的所有数据时,我们将观察到的误差。

误差分类图:

图片来源:作者

图片来源:作者

通过了解模型误差的来源和分类,我们终于可以探讨决定论如何与一切相关!

这是一个思想实验——如果我们有一个完美的模型结构(即 f(x) = f’(x)),并且 x 是预测 y 所需的 所有 特征的详尽集合,那么我们的模型仍会有不可减少的误差吗?或者用我创建的术语,‘普遍不可减少的误差’是否大于 0?决定论说‘不!’我们的模型将是 100%准确的,因为随机性不存在。如果误差不来自其他来源,那么就没有误差!在决定论的世界里,普遍不可减少的误差总是 0!

当然,我们无法超越思想实验,因为鉴于人类目前的认知限制,‘完美模型’是不可能的。

‘神’模型

在前一节中,我们讨论了一个具有完美公式和完整、全面预测变量列表的假设模型。这就是我所说的‘神’模型⁴,即需要一种自然神论水平的知识来创建这样的模型。

在决定论下,‘神’模型是一个理论上的可能性。由于随机性不存在,一个完美的模型将会有完美的预测。

回到认知限制,我们无法创建‘神’模型的唯一原因是我们的限制,而不是宇宙的本质。

因果关系与实验设计

决定论要求一切都是严格因果的。一些哲学家认为因果关系是人类构建的⁵。要接受决定论,必须接受因果关系是一个真实现象。(注意,这个声明不是可交换的——你不需要接受决定论才能接受因果关系)

这对我们如何考虑实验的设计和执行有着深远的影响。你是否期望一个完全受控的实验会有零误差?换句话说,如果我们能够完全隔离各个原因,并进行一百万次相同的实验,你是否期望得到完全相同的结果,永远没有任何变化?如果答案是‘是’,那么你就离成为一个决定论者不远了!

探索一个完美的假设世界是一个有用的工具,但现实要求我们适应其不完美之处。当然,我们不能完美地控制实验——这就是为什么实验设计领域有处理表面随机性和错误的规定。根据我们对宇宙的看法,我们可以将这些适应措施视为由于我们的认识限制(在决定论下)而必要,或者是由于随机性在宇宙中固有而必要。

随机数

有些随机数生成器使用随机过程(例如,大气噪声)来生成无法复制的数字。这些随机数需要硬件来捕捉。

大多数数据专业人员(那些不介意甚至希望他们的随机数被复制——想想设置种子)只需要使用伪随机数。伪随机数看起来是随机的,但由确定性算法创建,不需要比计算机程序更多的东西来生成。

如果决定论是真的,所有的‘随机数’实际上都是伪随机数——记住,随机性并不存在!当然,再次回到(非常抱歉)认识限制,随机数和伪随机数之间的区别是有意义的,因为我们可以很容易地复制伪随机数,而随机数需要类似于神的知识水平才能复制。抱歉黑客们,决定论在这一方面不太可能对你们有用……至少目前如此!

结论

通过一点‘宏观思考’,确定性思想和其含义在数据科学中经常出现。这种思维方式可能对解决工作中的具体技术问题没有帮助。但是,我相信对数据和宇宙如何连接进行深刻思考将使你成为一个更全面、更有洞察力的数据科学家。

备注

  1. 在本文中,我只涉及了决定论的基本和一般概念。决定论有许多不同的版本。此外,我并不主张或反对决定论——决定论的理论有多个替代方案——非决定论、代理因果关系、二元论等。

  2. 关于宇宙是否有一个开始或是否无限的问题,哲学家们已经争论了数千年。亚里士多德认为,观察运动链表明宇宙是永恒的,因为链条不能有开始。许多中世纪哲学家,如托马斯·阿奎那,认为存在一个初始运动,而这个运动就是上帝。斯蒂芬·霍金认为宇宙在大爆炸时开始,因此有一个明确的起点。

  3. 我将不可约错误进一步细分为‘局部’和‘普遍’以帮助我更全面地思考错误。由于这些是我自己创造的术语,进一步在互联网上搜索可能不会带来什么结果!

  4. 虽然‘神’模型实际上没有任何实际应用,但它是一个帮助我思考所有可能模型的工具。在左侧(最简单且可能最差的预测)我们有目标变量的平均值,而右侧则是‘神’模型。为了在回归问题的背景下说明这一点,平均模型的测试 R 平方值可能接近 0,而‘神’模型的测试 R 平方值总是 1。当我开发模型时,我喜欢问自己我的模型在这个范围内大致处于什么位置。

  5. 休谟的规律理论认为我们观察到模式并推断因果关系时,并不一定存在因果联系。大卫·休谟认为我们不能直接观察因果关系,因此我们没有理由认为因果关系是除了我们注意到模式以外的其他东西。

  6. 决 determinism 和自由意志 — 尽管这超出了本文的范围,如果你对决定论感兴趣,你应该研究自由意志的哲学。决定论对自由意志和责任的影响非常有趣,但我无法想到它们如何与数据科学相关!

PID 控制器优化:梯度下降方法

原文:towardsdatascience.com/pid-controller-optimization-a-gradient-descent-approach-58876e14eef2

使用机器学习解决工程优化问题

Callum BruceTowards Data Science Callum Bruce

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

--

梯度下降算法沿着下坡的方向减少成本函数。

机器学习。深度学习。人工智能。越来越多的人每天都在使用这些技术。这在很大程度上是由于像 ChatGPT、Bard 等大型语言模型的兴起。尽管这些技术被广泛使用,但相对较少的人了解这些技术背后的方法。

在本文中,我们深入探讨了机器学习中使用的基本方法之一:梯度下降算法。

我们将不从神经网络的角度来审视梯度下降算法,在神经网络中它用于优化网络权重和偏置,而是将其作为解决经典工程优化问题的工具来探讨。

具体而言,我们将使用梯度下降来调整汽车巡航控制系统中 PID(比例-积分-微分)控制器的增益。

采用这种方法的动机有两个:

首先,优化神经网络中的权重和偏置是一个高维问题。有许多变量,我认为这些会分散梯度下降在解决优化问题中的基本效用。

其次,正如你将看到的,梯度下降在应用于经典工程问题如 PID 控制器调优、机器人中的逆运动学和拓扑优化时,可以是一个强大的工具。梯度下降是一个在我看来,更多工程师应该熟悉并能够利用的工具。

阅读本文后,你将了解什么是 PID 控制器,梯度下降算法的工作原理,以及它如何应用于解决经典工程优化问题。你可能会受到激励,使用梯度下降来应对自己的优化挑战。

本文中使用的所有代码可以在GitHub 上找到

什么是 PID 控制器?

PID 控制器是工程和自动化系统中广泛使用的反馈控制机制。其目标是通过持续调整控制信号以维持期望的设定点,基于设定点和系统测量输出(过程变量)之间的误差。

PID 控制器的典型阶跃响应

PID 控制器在各个行业和领域中应用广泛。它们被广泛用于过程控制系统,如制造业中的温度控制、化工厂中的流量控制以及 HVAC 系统中的压力控制。PID 控制器还被应用于机器人技术中以实现精确定位和运动控制,以及汽车系统中的油门控制、发动机速度调节和防抱死刹车系统。它们在航空航天和航空应用中也发挥着重要作用,包括飞机自动驾驶仪和姿态控制系统。

PID 控制器由三个组成部分组成:比例项、积分项和微分项。比例项对当前误差做出即时响应,积分项累积并修正过去的误差,微分项预测并抵消未来的误差趋势。

PID 控制器框图

上图中的 PID 控制器控制回路展示了 r(t) 是设定点,y(t) 是过程变量。过程变量从设定点中减去以获得误差信号 e(t)

控制信号,u(t),是比例、积分和微分项的总和。控制信号被输入到过程当中,这会导致过程变量更新。

PID 控制器控制信号 u(t)

梯度下降算法

梯度下降是一种常用于机器学习和数学优化的优化算法。它通过基于成本函数梯度迭代调整参数,旨在找到给定成本函数的最小值。梯度指向最陡的上升方向,因此通过朝相反方向迈步,算法逐渐收敛到最优解。

单步梯度下降更新定义为:

梯度下降更新步骤

a 是输入参数的向量。下标 n 表示迭代。f(aₙ) 是多变量成本函数,∇f(a) 是该成本函数的梯度。∇f(aₙ) 代表最陡升高的方向,因此它从 a 中减去以在下一次迭代中减少成本函数。𝛾 是学习率,决定了每次迭代的步长。

必须选择一个合适的𝛾值。如果值过大,每次迭代时采取的步骤将过大,导致梯度下降算法无法收敛。如果值过小,梯度下降算法将计算开销大,收敛时间长。

梯度下降算法应用于 y=x²代价函数(初始 x=5),对于𝛾=0.1(左侧)和𝛾=1.02(右侧)

梯度下降在广泛的领域和学科中应用。在机器学习和深度学习中,它是用于训练神经网络和优化其参数的基本优化算法。通过根据代价函数的梯度迭代更新网络的权重和偏差,梯度下降使网络能够学习并随着时间的推移提高其性能。

除了机器学习之外,梯度下降被应用于工程、物理学、经济学及其他领域的各种优化问题中。它帮助进行参数估计、系统识别、信号处理、图像重建以及许多其他需要找到函数最小值或最大值的任务。梯度下降的多功能性和有效性使其成为解决优化问题和改善各领域模型和系统的重要工具。

使用梯度下降优化 PID 控制器增益

有几种方法可以调整 PID 控制器。这些方法包括手动调节法和像齐格勒-尼科尔斯法这样的启发式方法。手动调节法可能耗时且可能需要多次迭代才能找到最佳值,而齐格勒-尼科尔斯法往往会产生激进的增益和较大的超调,这意味着它不适合某些应用。

这里展示了一种梯度下降方法来优化 PID 控制器。我们将优化一个车载巡航控制系统的控制系统,以应对设定点的阶跃变化。

通过控制踏板位置,控制器的目标是将车辆加速到速度设定点,同时最小化超调、稳定时间和稳态误差。

车辆受到与踏板位置成比例的驱动力。滚动阻力和空气阻力作用于与驱动力相反的方向。踏板位置由 PID 控制器控制,并限制在-50%到 100%范围内。当踏板位置为负值时,车辆在制动。

在调整 PID 控制器增益时,拥有系统模型是有帮助的。这样我们可以模拟系统响应。为此,我在 Python 中实现了一个Car类:

import numpy as np

class Car:
    def __init__(self, mass, Crr, Cd, A, Fp):
        self.mass = mass # [kg]
        self.Crr = Crr # [-]
        self.Cd = Cd # [-]
        self.A = A # [m²]
        self.Fp = Fp # [N/%]

    def get_acceleration(self, pedal, velocity):
        # Constants
        rho = 1.225 # [kg/m³]
        g = 9.81 # [m/s²]

        # Driving force
        driving_force = self.Fp * pedal

        # Rolling resistance force
        rolling_resistance_force = self.Crr * (self.mass * g)

        # Drag force
        drag_force = 0.5 * rho * (velocity ** 2) * self.Cd * self.A

        acceleration = (driving_force - rolling_resistance_force - drag_force) / self.mass
        return acceleration

    def simulate(self, nsteps, dt, velocity, setpoint, pid_controller):
        pedal_s = np.zeros(nsteps)
        velocity_s = np.zeros(nsteps)
        time = np.zeros(nsteps)
        velocity_s[0] = velocity

        for i in range(nsteps - 1):
            # Get pedal position [%]
            pedal = pid_controller.compute(setpoint, velocity, dt)
            pedal = np.clip(pedal, -50, 100)
            pedal_s[i] = pedal

            # Get acceleration
            acceleration = self.get_acceleration(pedal, velocity)

            # Get velocity
            velocity = velocity_s[i] + acceleration * dt
            velocity_s[i+1] = velocity

            time[i+1] = time[i] + dt

        return pedal_s, velocity_s, time

PIDController类的实现如下:

class PIDController:
    def __init__(self, Kp, Ki, Kd):
        self.Kp = Kp
        self.Ki = Ki
        self.Kd = Kd
        self.error_sum = 0
        self.last_error = 0

    def compute(self, setpoint, process_variable, dt):
        error = setpoint - process_variable

        # Proportional term
        P = self.Kp * error

        # Integral term
        self.error_sum += error * dt
        I = self.Ki * self.error_sum

        # Derivative term
        D = self.Kd * (error - self.last_error)
        self.last_error = error

        # PID output
        output = P + I + D

        return output

采取这种面向对象编程的方法使得设置和运行多个带有不同 PID 控制器增益的仿真变得更加容易,这在运行梯度下降算法时是必须的。

GradientDescent 类的实现如下:

class GradientDescent:
    def __init__(self, a, learning_rate, cost_function, a_min=None, a_max=None):
        self.a = a
        self.learning_rate = learning_rate
        self.cost_function = cost_function
        self.a_min = a_min
        self.a_max = a_max
        self.G = np.zeros([len(a), len(a)])
        self.points = []
        self.result = []

    def grad(self, a):
        h = 0.0000001
        a_h = a + (np.eye(len(a)) * h)
        cost_function_at_a = self.cost_function(a)
        grad = []
        for i in range(0, len(a)):
            grad.append((self.cost_function(a_h[i]) - cost_function_at_a) / h)
        grad = np.array(grad)
        return grad

    def update_a(self, learning_rate, grad):
        if len(grad) == 1:
            grad = grad[0]
        self.a -= (learning_rate * grad)
        if (self.a_min is not None) or (self.a_max is not None):
            self.a = np.clip(self.a, self.a_min, self.a_max)

    def update_G(self, grad):
        self.G += np.outer(grad,grad.T)

    def execute(self, iterations):
        for i in range(0, iterations):
            self.points.append(list(self.a))
            self.result.append(self.cost_function(self.a))
            grad = self.grad(self.a)
            self.update_a(self.learning_rate, grad)

    def execute_adagrad(self, iterations):
        for i in range(0, iterations):
            self.points.append(list(self.a))
            self.result.append(self.cost_function(self.a))
            grad = self.grad(self.a)
            self.update_G(grad)
            learning_rate = self.learning_rate * np.diag(self.G)**(-0.5)
            self.update_a(learning_rate, grad)

通过调用 executeexecute_adagrad 来运行算法指定次数的迭代。execute_adagrad 方法执行一种修改后的梯度下降形式,称为 AdaGrad(自适应梯度下降)。

AdaGrad 具有逐参数的学习率,这些学习率对于稀疏参数会增加,对于不那么稀疏的参数会减少。学习率在每次迭代后会根据历史梯度平方和进行更新。

我们将使用 AdaGrad 来优化汽车巡航控制系统的 PID 控制器增益。使用 AdaGrad 时,梯度下降更新方程变为:

AdaGrad 梯度下降更新步骤

现在我们需要定义我们的成本函数。成本函数必须接受一组输入参数,并返回一个数字;即成本。汽车巡航控制的目标是将汽车加速到速度设定点,同时最小化超调、稳定时间和稳态误差。我们可以根据这个目标定义多种成本函数。这里我们将其定义为误差幅度随时间的积分:

汽车巡航控制成本函数

由于我们的成本函数是一个积分,我们可以将其可视化为误差幅度曲线下的面积。我们期望看到曲线下的面积在接近全局最小值时减少。在程序中,成本函数定义为:

def car_cost_function(a):
    # Car parameters
    mass = 1000.0  # Mass of the car [kg]
    Cd = 0.2  # Drag coefficient []
    Crr = 0.02 # Rolling resistance []
    A = 2.5 # Frontal area of the car [m²]
    Fp = 30 # Driving force per % pedal position [N/%]

    # PID controller parameters
    Kp = a[0]
    Ki = a[1]
    Kd = a[2]

    # Simulation parameters
    dt = 0.1  # Time step
    total_time = 60.0  # Total simulation time
    nsteps = int(total_time / dt)
    initial_velocity = 0.0  # Initial velocity of the car [m/s]
    target_velocity = 20.0 # Target velocity of the car [m/s]

    # Define Car and PIDController objects
    car = Car(mass, Crr, Cd, A, Fp)
    pid_controller = PIDController(Kp, Ki, Kd)

    # Run simulation
    pedal_s, velocity_s, time = car.simulate(nsteps, dt, initial_velocity, target_velocity, pid_controller)

    # Calculate cost
    cost = np.trapz(np.absolute(target_velocity - velocity_s), time)
    return cost

成本函数包括模拟参数。模拟运行 60 秒。在此期间,我们观察系统对从 0 m/s 到 20 m/s 的设定点阶跃变化的响应。通过随时间积分误差幅度,计算每次迭代的成本。

现在,只剩下运行优化了。我们将从初始值 Kp = 5.0,Ki = 1.0 和 Kd = 0.0 开始。这些值会产生一个稳定的、带有超调的振荡响应,最终收敛到设定点。从这个起点开始,我们将使用基础学习率 𝛾=0.1 运行梯度下降算法 500 次:

a = np.array([5.0, 1.0, 0.0])
gradient_descent = GradientDescent(a, 0.1, car_cost_function, a_min=[0,0,0])
gradient_descent.execute_adagrad(500)

汽车巡航控制的阶跃响应(左侧),误差幅度(中间)和成本(右侧),随着梯度下降算法迭代趋向于最优解

上面的动画图显示了汽车巡航控制的阶跃响应如何随着梯度下降算法对 KpKiKd 增益的调优而演变。

到第 25 次迭代时,梯度下降算法已消除了振荡响应。在这一点之后,发生了有趣的事情。算法会在一个局部最小值处徘徊,该最小值的特征是约 3 m/s 的超调。这发生在 6.0 < Kp < 7.5,Ki ~= 0.5,Kd = 0.0 的范围内,并持续到第 300 次迭代。

第 300 次迭代后,算法会脱离局部最小值,找到更接近全局最小值的更令人满意的响应。现在的响应特征是零超调、快速稳定时间和接近零的稳态误差。

运行梯度下降算法 500 次迭代后,我们得到了优化的 PID 控制器增益;Kp = 8.33,Ki = 0.12 和 Kd = 0.00。

比例增益仍在稳步上升。运行更多的迭代(此处未显示),随着 Kp 的缓慢增加,我们发现对成本函数的进一步减少是可能的,尽管这种效果逐渐变得边际化。

摘要

采用一种广泛用于解决机器学习和深度学习问题的方法,我们成功地优化了汽车巡航控制系统的 PID 控制器增益。

从初始值 Kp = 5.0,Ki = 1.0 和 Kd = 0.0 开始,应用梯度下降算法的 AdaGrad 形式,我们观察到该低维系统如何首先进入局部最小值,然后最终找到一个更满意的响应,具有零超调、快速的稳定时间和接近零的稳态误差。

在本文中,我们看到梯度下降在应用于经典工程优化问题时可以成为一个强大的工具。除了这里展示的例子外,梯度下降还可以用于解决其他工程问题,如机器人中的逆向运动学、拓扑优化等。

您是否有一个认为可以应用梯度下降的优化问题?在下面的评论中告诉我。

喜欢阅读这篇文章吗?

关注订阅获取更多类似内容——与您的网络分享——尝试将梯度下降应用于您自己的优化问题。

所有图片,除非另有说明,均由作者提供。

参考文献

网络

[1] GitHub (2023), pid_controller_gradient_descent

[2] 维基百科 (2023),齐格勒–尼科尔斯法(访问日期:2023 年 7 月 10 日)

开创数据可观察性:数据、代码、基础设施与人工智能

原文:towardsdatascience.com/pioneering-data-observability-data-code-infrastructure-ai-c22a26706866?source=collection_archive---------5-----------------------#2023-08-08

Barr MosesTowards Data Science Barr Moses

·

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

--

数据可观察性的四个维度:数据、代码、基础设施和人工智能?图片由作者提供。

概述了构建可靠数据系统的过去、现在和未来。

当我们在 2019 年推出数据可观察性类别时,这个术语几乎是我难以发音的。

四年后,这一类别已经明确确立为现代数据堆栈的核心层。数据可观察性是一个 G2 类别,被 Gartner、Forrester 等机构认可,最重要的是,被包括一些世界上最先进的数据组织在内的数百家公司广泛采用。

事实上,一家快速增长公司的首席技术官最近告诉我:“这是由于世界变化而产生的世俗趋势。数据可观察性早晚会发生,没有人能阻止它。”

虽然我仍然不能总是正确发音(有 ESL 的吗?),但数据可观察性已经成为现代数据团队的必需品,我对这一运动取得的进展感到无比自豪——以及我们要去的地方。

那么,数据可靠性的未来会是什么样的呢?要理解我们的前进方向,首先需要退一步评估我们已经走了多远。

我们的起点

在 2010 年代中期,数据团队开始迁移到云端,采用数据存储和计算技术——Redshift、Snowflake、Databricks、GCP,哎呀!——以满足对分析的不断增长的需求。云计算使数据处理更快、转换更容易、访问更广泛。

随着数据变得越来越普遍,管道变得更复杂,新角色(例如数据工程师)出现以管理这种混乱,可能的使用场景数量激增。

优点?更明智的决策,更丰富的数据使用场景,以及更智能的软件。

缺点?基础部分——如数据质量——被忽视了,且在这个现代数据堆栈中处于次要地位。

在过去的某个时期,我亲身体验了坏数据的影响。凌晨 5 点,首席财务官发来的信息说“数据看起来有问题”。来自利益相关者的贴纸条,提醒我仪表盘未能更新。因我们的产品输入了不准确的数据而感到困惑的客户。

数据可观察性源于这种痛苦——我们称之为数据停机时间——并提供了一个切实可行的解决方案。受应用可观察性和站点可靠性工程的启发,数据可观察性监控并警告组织数据事件,避免对业务造成影响。数据可观察性提供了一种自动化、流程驱动的替代方案,以实现数据可靠性,从而降低成本,推动增长,并大幅减少凌晨 5 点的紧急处理。

从历史上看,最强的数据可观察性方法包括三个主要阶段:检测解决预防

  1. 检测: 数据可观察性检测数据中的异常和其他问题,并在相关利益相关者发现之前,提醒数据团队的相关负责人。

  2. 解决方案:同时,数据可观察性平台为团队提供了解决问题的工具,包括字段级追溯、自动根本原因分析和影响分析、有关影响该资产的过去事件的信息、相关查询日志和 dbt 模型、受影响的报告等。

  3. 预防:最后,数据可观察性还提供了防止数据问题发生的机制,例如在管道中设置断路器,并创建有关代码更改对数据影响的可见性,以及其他主动措施,以防止不良数据首次进入管道。

起初,数据可观察性专注于利用元数据和数据本身来检测、解决和防止数据问题,从而拼凑出数据健康的全貌。通过监控和警报数据从摄取到消费的全过程,团队可以检测到上游表中意外的变化,从而导致下游来源中断或变得不可靠。

扩展数据之外的检测和解决

然而,像任何行业一样,数据领域也在发展,影响了团队对事件检测和解决以及更广泛的数据可观察性的思考方式。这一演变归因于一些令人兴奋的趋势:数据产品的兴起以及因此数据团队不断向工程部门靠拢或直接融入工程部门

随着数据团队在组织中的范围扩大,数据使用案例的增长,数据团队对底线的影响比以往任何时候都更大。现在,业务中的每个人每天都利用数据来驱动洞察、推动数字服务和训练机器学习模型。实际上,我们已经超越了仅仅将数据视作产品。在 2023 年,数据即产品

数百位客户之后,包括 Pepsi、Gusto、MasterClass 和 Vimeo 的团队,我们发现需要超越数据来实现数据可靠性。不可靠的数据不会独立存在……它受到数据生态系统中所有三种成分的影响:数据 + 代码 + 基础设施

这种更广泛的视角反映了我们在软件工程领域的朋友们如何处理检测和解决问题。应用程序可观察性从基础设施开始,但分析的内容远不止这些,以检测和解决软件停机问题;根本原因分析考虑了代码、基础设施、服务、网络和其他许多因素。对于软件工程师来说,可靠性不是在真空中实现的——它通常受到多个因素的影响,这些因素经常一起作用或相互叠加。

在数据中,情景往往是相同的,我们应该开始以这种方式对待它。

让我们通过一个数据领域的假设示例来了解一下。

想象一下你有一个显示过时结果的仪表板。你可能会首先查看你的数据,在这种情况下,也许是从 Google 导入的一个上游表格,描述了你的广告活动。是否有人更改了广告活动名称,破坏了硬编码的数据管道?或者你在点击事件表中得到的是 null 而不是用户 UUID?没有结果,那么接下来是什么?

你查看了代码。也许你的分析工程师对你的 SQL 进行了更改,从而过滤掉了最新的数据?他们本意是好的,但这可能带来了意想不到的后果?你查看了你的 dbt 仓库。不对——那儿一切正常。

最后,你查看了你的基础设施。你迅速点击到你的 Airflow 用户界面——也许你在一个小实例上运行 Airflow,而它的内存耗尽了(不应该把那些行加载到内存中!!),导致了下游的新鲜度问题。发现了——你找到了问题所在!

经验告诉我们,这三种因素都会对数据停机产生重要影响。所以无论你首先查看哪里,你都要经历一个漫长而乏味的过程,进行有根据的猜测并逐一排除。哦,我们提到过这还需要访问和熟练掌握组成你数据栈的 8 种不同工具吗?

现在,想象一下你可以迅速将你看到的症状(陈旧的仪表板……)与数据、代码和基础设施的所有变化关联起来。哦,而且你不需要统计学博士学位或在公司工作十年,知道数据仓库中的每一列。所有这些都触手可及——对数据、代码和基础设施如何共同作用以导致仪表板出现问题的端到端理解。想想你本可以节省多少时间和资源,避免多少利益相关者的沮丧,更不用说那些早晨的闹钟了。

数据可观察性需要洞察数据环境的三个层面:数据、代码和基础设施。图片由作者提供。

要真正实现数据可观察性的潜力并获得可靠的数据,团队需要采取三层次的方法,将影响数据健康的数据、代码和基础设施编织成一个全面的图景。

我们还认识到,实现数据可靠性不仅仅是启动一个工具。它是关于在团队中创建一种新学科——如果你愿意的话,是一种操作思维。团队需要引入有关监控数据系统、响应事件并随着时间推移不断改进的流程。

组织结构、流程和技术必须进化以实现这些目标。想想看:仪表盘 定义和监控数据产品的可靠性,基于支撑它们的上游表格,这些仪表盘可以在组织内部轻松共享,以实现透明度、协作和问责。以及 领域 基于用例和所有者对数据和管道进行分段,以实现有针对性的优先处理和事件解决。

可靠的数据与 AI 的未来

将大型语言模型 (LLMs) 视为 [插入行业] 未来的赌注现在几乎已成陈词滥调,但对数据行业的影响却有所不同。

当前数据和工程中的生成式 AI 用例几乎完全专注于提升生产力,比如 GitHub Co-Pilot、Snowflake Document AI 和 Databricks LakehouseIQ。在许多方面,我们不知道生成式 AI 的未来会怎样,但我们知道数据团队将在其成功中发挥重要作用。

LLMs 在数据质量方面有着令人兴奋的机会,但更强有力的观点是数据质量和可靠性可以帮助 LLMs。事实上,我认为服务于生产用例的 LLMs 必须建立在坚实的基础上:拥有大量高质量、可靠、值得信赖的数据。

总体而言,今天绝大多数生成式 AI 应用程序都托管在云端,并通过 API 提供服务。为了支持这些应用,你需要一个强大、基于云的数据架构,以可靠地存储、转换、训练和提供支撑这些应用的数据。

呼应这一观点,在 Snowflake 的 2023 年第一季度财报电话会议上,Snowflake 的 CEO Frank Slootman 认为,“生成式 AI 由数据驱动。这就是模型如何训练并变得越来越有趣和相关……你不能无差别地将这些 [LLMs] 放在人们不了解其质量、定义和来源的数据上。”

我们已经看到了不可靠模型训练的影响。就在去年,全球信用巨头 Equifax 共享 了一个消息:一个在不良数据上训练的 ML 模型导致他们向贷款机构发送了数百万消费者的错误信用评分。不久前,Unity Technologies 报告 由于错误的广告数据推动其定向算法,造成了 1.1 亿美元的收入损失。在未来几年中,除非我们优先考虑信任,否则这将不可避免地成为一个更大的问题。

随着我们见证企业 AI 应用的崛起,数据可观察性将成为支持 LLMs 和所有其他 AI 用例的关键能力。

正如 Databricks 的联合创始人 Matei Zaharia、Patrick Wendell、Reynold Xin 和 Ali Ghodsi 建议: “企业应用对幻觉或错误响应的容忍度很低……在机器学习生命周期的每个阶段,数据和模型必须共同管理,以构建最佳的应用程序。这对于生成模型尤其重要,因为质量和安全性在很大程度上依赖于良好的训练数据。”

我完全同意。提高和增强 AI 影响力的第一步?优质、可靠的数据——而且要有大量的数据。

加入我们吧,你愿意吗?

通过 LinkedIn 与 Barr Moses 联系,分享你的想法、感受和情绪。你认为这个领域的未来发展方向是什么?

Pip 安装特定版本 — 如何使用 Pip 安装特定 Python 包版本

原文:towardsdatascience.com/pip-install-specific-version-how-to-install-a-specific-python-package-version-with-pip-1f97e1002f31

想要使用 Pip 安装特定的 Python 包版本?本文将通过实际示例和指南展示如何操作。

Dario RadečićTowards Data Science Dario Radečić

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

--

图片由Mario Gogh提供,来自Unsplash

简而言之: 你可以通过运行pip install <package_name>==<version>命令来安装 Python 包的特定版本。例如,要安装Pandas的版本 1.3.4,请从终端执行pip install pandas==1.3.4命令。

这只是一个简短的版本,可能是你所需要的所有信息,但有时你可能需要更好地控制包的安装。这就是本文其余部分的作用。

为什么要安装特定(较旧的)版本的 Python 包

那么,为什么要在意旧版本的 Python 包呢?也许你有一个庞大的代码库,它与最新的包更新不兼容。也许你几年前写的代码仍在生产中工作,但更新包可能会破坏它。或者即使是最新的包版本也不兼容你的 Python 版本。

换句话说——无论情况如何,安装旧版本 Python 包是有正当理由的。

现在让我们探讨一些使用 Pip 安装特定包版本的实际示例。

如何检查已安装的 Python 包的版本

在继续安装特定的 Python 包版本之前,你必须了解两个有用的命令。这些是:

  • pip show <packagename> - 显示当前安装的包版本、摘要、作者、许可证、依赖关系等。

  • pip index versions <packagename> - 列出你可以安装的所有可用包版本。

让我们检查一下 pandas Python 包。以下 shell 命令打印出当前安装的版本:

pip show pandas

图像 1 — Pandas 包的当前版本(图像作者提供)

看起来 Pandas 版本 1.5.3 已安装,并且需要 numpypython-dateutilpytz 包才能运行。

但是版本 1.5.3 是最新的吗?我们还可以安装其他哪些版本? 这是下一个 shell 命令的答案:

pip index versions pandas

图像 2 — 可用的 Pandas 包版本(图像作者提供)

版本 2.0.0 是最新的,但根据你在系统或虚拟环境中安装的 Python 版本,你可以回退到很久以前的版本。

你现在知道如何检查当前安装的包版本,并列出所有可用版本,接下来,让我们看看如何使用 pip 安装特定版本的包。

如何安装特定版本的 Python 包

安装特定版本的 Python 包有 2 种必知的方法。第一种方法需要两个命令——第一个用于卸载当前版本,第二个用于安装你想要的版本。第二种方法将所有这些功能打包在一个 shell 命令中,因此你在实际操作中应该使用第二种方法。

方法 1 — 卸载并安装

现在你知道了你安装了哪个版本的 Pandas,以及根据你的 Python 版本你可以潜在安装哪些版本。

在我们的案例中,我们有版本 1.5.3,但我们想回退到版本 1.3.4。为此,我们首先必须卸载当前版本:

pip uninstall pandas

图像 3 — 使用 pip 卸载 Python 包(图像作者提供)

然后,运行以下命令使用 pip 安装 Pandas 的特定版本:

pip install pandas==1.3.4

图像 4 — 使用 Pip 安装特定版本的 Python 包(图像作者提供)

看起来一切顺利,但你如何确认版本 1.3.4 已经安装了呢?剧透:你已经知道命令了

pip show pandas

图像 5 — 当前安装的 Pandas 版本(图像作者提供)

Pandas 1.3.4 已成功安装在我们的系统中,所以我们可以认为任务完成了。

方法 2 — 覆盖已安装的包

比前一节提到的方法更优的是通过一个简单的 shell 命令来安装特定版本的 Python 包。你可以通过在安装特定版本包时附加 --ignore-installed 标志来实现。

这是一个示例——它安装 Pandas 版本 2.0.0,并覆盖了刚刚安装的版本 1.3.4:

pip install pandas==2.0.0 --ignore-installed

图像 6 — 覆盖当前包版本(图像作者提供)

你可以使用pip show命令来进行验证:

pip show pandas

图 7 — 当前安装的 Pandas 版本(图片来源:作者)

这就是两种 pip 安装 Pandas 包特定版本的方法。这会一直这么简单吗? 可能不会,所以继续阅读以获取更适合生产环境的用例。

如何安装多个具有特定版本的 Python 包

一次一个地安装 Python 包在本地环境中没问题,但在生产环境中不具备扩展性。通常在生产环境中,你会有一个 requirements.txt 文件,其中包含项目所有的 Python 依赖项及其特定版本。

等一下,什么是 requirements.txt 文件? 如果你想像正常人一样管理 Python 依赖项,请阅读这篇文章*

requirements.txt 文件通常结构如下:

package_1==version_1
package_2==version_2
...

总之,有一个包含各自版本的 Python 依赖项列表。现在的问题是,你能用一个命令安装所有这些依赖项吗? 答案是——可以。

这是我们端上的文件内容:

图 8 — requirements.txt 文件的内容(图片来源:作者)

如果 requirements.txt 文件与您的 shell 在同一目录中,则应运行以下命令:

pip install -r requirements.txt

图 9 — 使用一个命令安装多个 Python 依赖项(图片来源:作者)

如果你的 requirements.txt 文件位于其他位置,只需在 -r 后提供绝对路径或相对路径。就是这样!

现在,让我们验证是否安装了正确的包版本:

图 10 — 检查 Python 包版本(图片来源:作者)

这就是如何一次性安装多个 Python 包。接下来让我们做个简短的回顾。

总结 Pip 安装特定版本

项目依赖管理不必复杂。你可以使用 Python 的 pipreqs 模块来跟踪项目中使用的包并创建 requirements.txt 文件,然后应用本文中学到的内容,将相同的依赖项转移到另一台机器或生产环境中。

你也可以一个一个地 pip 安装 Python 包的特定版本,这对于本地开发环境来说没问题,本文展示了几种方法来实现这一点。更好的方法是为依赖项创建一个专门的文件,并一次性安装所有包。

你如何跟踪项目依赖项?你会安装 Python 包的特定版本,还是坚持使用最新版本? 请在下面的评论区告诉我。

喜欢这篇文章吗?成为 Medium 会员 以继续无限制地学习。如果你使用以下链接,我将获得你会员费的一部分,而不会额外增加你的费用。

[## 使用我的推荐链接加入 Medium - Dario Radečić

阅读 Dario Radečić的每一个故事(以及 Medium 上的其他成千上万位作者的故事)。你的会员费直接支持…

medium.com](https://medium.com/@radecicdario/membership?source=post_page-----1f97e1002f31--------------------------------)

最初发表于 https://betterdatascience.com 于 2023 年 4 月 5 日。

Pipeline Dreams: 在 AWS 上自动化机器学习训练

原文:towardsdatascience.com/pipeline-dreams-automating-ml-training-on-aws-8e90a33061fd

Zachary RaicikTowards Data Science Zachary Raicik

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

--

照片由Arnold Francisca拍摄,来源于Unsplash

在机器学习的世界里,自动化训练管道简化了从数据到洞察的过程。它们自动化了机器学习生命周期的各个部分,如数据摄取、预处理、模型训练、评估和部署。亚马逊网络服务(“AWS”)提供了多种工具来开发自动化训练管道。在本文中,我们将通过使用经典的鸢尾花数据集来讲解如何设置一个基本的自动化训练管道。

设置舞台:需求和 AWS 工具包

在这一部分,我们将介绍一些高级需求以及我们将使用的 AWS 工具的简要概述。

需求

如果你选择通过构建自己的训练管道来跟随,您将需要以下内容。

  • 需要一个活跃的 AWS 账户(你可以在这里注册),并具有管理员权限。

  • AWS CLI的基本知识(我们将在未来的帖子中探索 AWS CLI 的替代方案)

设置你的 AWS 账户并通过 CLI 连接到 AWS 超出了本文的范围,但如果你需要帮助,请随时直接联系我。

工具包

设置自动化训练管道将需要使用以下 AWS 产品。

  • S3:可扩展的对象存储服务,旨在存储和检索来自网络任何位置的任意数量的数据。

  • Lambda:无服务器计算服务,自动响应事件运行你的代码,例如对 Amazon S3 桶中数据的更改。

  • Docker:Docker 是一个平台,它在轻量级、可移植的容器中打包、分发和管理应用程序。

  • Sagemaker:完全托管的服务,提供开发人员和数据科学家快速、轻松地构建、训练和部署机器学习模型的能力。

  • Step Functions:无服务器工作流服务,让你通过可视化工作流协调分布式应用程序和微服务,使你能够大规模地构建、运行和可视化复杂的流程。

实现自动化训练管道

假设你已经通过了要求,我们可以将注意力转向构建我们的自动化训练管道。对于这个简单的例子,我们将专注于以下步骤。

  1. 创建 AWS S3 存储桶来存储与我们的训练管道相关的数据和工件。

  2. 创建 AWS Lambda 函数用于数据摄取、预处理和训练。

  3. 创建一个 AWS Step Functions 状态机来协调执行你的管道阶段。

我们将深入探讨每个步骤。

1. S3 存储桶

首先,我们将使用 AWS CLI 创建一个新的 S3 存储桶来存储数据和工件。

aws s3api create-bucket 
  --bucket automated-training-pipeline 
  --region us-east-1

如果这个命令成功运行,你的终端应该会输出类似的信息。

{
    "Location": "/automated-training-pipeline"
}

看一下我们的管理控制台,我们可以看到 S3 存储桶已经成功创建。

2. Lambda 函数

对于我们的自动化训练管道,我们将大量依赖 lambda 函数来执行和触发过程的某些部分。具体来说,我们将使用 lambda 函数来:

  • 数据摄取

  • 数据预处理

  • 模型训练

2a. 数据摄取

Lambda 函数需要 lambda 处理程序。Lambda 处理程序是 Lambda 部署包中的用户定义函数,当 AWS Lambda 服务执行 Lambda 函数时,服务可以调用该处理程序函数。处理程序函数接收并处理来自调用者的事件数据。

鉴于这一点,让我们定义我们的 lambda_handler 用于数据摄取。lambda_handler 函数作为 AWS Lambda 执行代码的入口点,在这种情况下,它从指定的 URL 中检索一个 CSV 文件,并将其上传到 Amazon S3 存储桶。该文件已存储在 ./src/data 中。

import boto3
import requests

def lambda_handler(event, context):
    s3 = boto3.client('s3')
    data_url = 'https://example.com/dataset.csv'
    response = requests.get(data_url)

    # Assuming the data is small enough to fit into memory
    s3.put_object(
        Bucket='your-s3-bucket',
        Key='data/dataset.csv',
        Body=response.content
    )

我们将使用 docker 将你的 AWS Lambda 函数打包和部署为容器镜像。为此,我们首先需要创建一个 ECR 仓库来存放我们的镜像。

aws ecr create-repository --repository-name fetch-iris-data --region us-east-1

既然我们的 ECR 仓库已经搭建好了,我们就在项目目录的根目录中创建我们的 Dockerfile

# Use a base image with Python 3.11
FROM public.ecr.aws/lambda/python:3.11

# Install dependencies (if any)
RUN pip install requests boto3

# Copy your function code from the src/data directory to the Lambda task root directory
COPY src/data/fetch_iris_data.py ${LAMBDA_TASK_ROOT}

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD ["fetch_iris_data.lambda_handler"]

我们使用这个 Dockerfile 来构建我们的镜像并将其推送到 ECR 注册表。

docker build -t fetch-iris-data .

我们需要对 Docker 进行身份验证以访问 ECR 注册表。

aws ecr get-login-password --region region | docker login --username AWS --password-stdin <your-account-id>.dkr.ecr.<region>.amazonaws.com

接下来,我们为镜像打上标签以匹配仓库。

docker tag fetch-iris-data:latest <your-account-id>.dkr.ecr.<region>.amazonaws.com/fetch-iris-data:latest

现在,我们可以将我们的镜像推送到 ECR 注册表。

docker push <your-account-id>.dkr.ecr.<region>.amazonaws.com/fetch-iris-data:latest

最后,我们将创建 lambda 函数并附加容器。

aws lambda create-function \
    --function-name <function-name> \
    --package-type Image \
    --code ImageUri=<your-account-id>.dkr.ecr.<region>.amazonaws.com/<repository-name>:<tag> \
    --role arn:aws:iam::<your-account-id>:role/<role-name> \
    --region <region>

你现在可以在 AWS 管理控制台中看到 lambda 函数。我可以调用该函数并确认文件已正确下载到 S3 存储桶中。

aws lambda invoke --function-name fetch-iris-data-function --payload '{}' 

aws s3 ls s3://automated-training-pipeline/data/

#2023-10-24 13:52:41       4551 iris.data

2b. 预处理

现在,我们已经有了建模任务所需的数据。对于大多数机器学习任务,我们希望以某种方式处理这些数据。在下面的文件中,我们定义了一个脚本来处理我们的鸢尾花数据。

import argparse
import os
import pandas as pd
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.model_selection import train_test_split

def preprocess(input_data_path, output_data_path):
    column_names = [
        "sepal_length",
        "sepal_width",
        "petal_length",
        "petal_width",
        "species",
    ]
    df = pd.read_csv(input_data_path, header=None, names=column_names)

    df = df.dropna(subset=["species"])

    encoder = LabelEncoder()
    df["species"] = encoder.fit_transform(df["species"])

    train, test = train_test_split(df, test_size=0.2, random_state=42)

    mean_train = train.mean()

    train.fillna(mean_train, inplace=True)
    test.fillna(mean_train, inplace=True)

    scaler = StandardScaler()
    scaler.fit(train[["sepal_length", "sepal_width", "petal_length", "petal_width"]])

    train[
        ["sepal_length", "sepal_width", "petal_length", "petal_width"]
    ] = scaler.transform(
        train[["sepal_length", "sepal_width", "petal_length", "petal_width"]]
    )
    test[
        ["sepal_length", "sepal_width", "petal_length", "petal_width"]
    ] = scaler.transform(
        test[["sepal_length", "sepal_width", "petal_length", "petal_width"]]
    )

    train.to_csv(
        os.path.join(output_data_path, "train_data.csv"), header=False, index=False
    )
    test.to_csv(
        os.path.join(output_data_path, "test_data.csv"), header=False, index=False
    )

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--input-data", type=str)
    parser.add_argument("--output-data", type=str)
    args = parser.parse_args()

    preprocess(args.input_data, args.output_data)

我们将预处理脚本上传到 S3。在与 preprocess.py 相同的目录中运行此命令。

aws s3 cp preprocess.py s3://automated-training-pipeline/modeling/preprocess.py

类似于我们为数据获取步骤构建镜像的方式,我们将为预处理步骤构建镜像。像之前一样,我们先创建我们的仓库。

aws ecr create-repository --repository-name preprocess-iris-data --region <region>

既然我们已经建立了 ECR 仓库,我们在项目目录的根目录中创建我们的 Dockerfile

# Use a standard Python base image
FROM python:3.11-slim-buster

# Install necessary libraries: requests, boto3, scikit-learn, pandas, numpy, etc.
RUN pip install requests boto3 scikit-learn pandas numpy

# No need to copy any Python script here since it will be provided from S3 at runtime

# Set working directory
WORKDIR /app

# Set a default CMD or ENTRYPOINT in case you want to run the container for testing, but this isn't strictly necessary
# since the Processing job will override this with ContainerEntrypoint from your Lambda function
CMD ["python", "-c", "print('Container started successfully')"]

我们构建镜像。

docker build -t preprocess-iris-data .

让我们对 Docker 进行 ECR 注册表的认证。如果你的访问权限没有过期,你可能不需要这样做。

aws ecr get-login-password --region region | docker login --username AWS --password-stdin <your-account-id>.dkr.ecr.<region>.amazonaws.com

再次,我们将镜像标记以匹配仓库。

docker tag preprocess-iris-data:latest <your-account-id>.dkr.ecr.<region>.amazonaws.com/preprocess-iris-data:latest

现在,我们可以将镜像推送到 ECR 注册表。

docker push <your-account-id>.dkr.ecr.<region>.amazonaws.com/preprocess-iris-data:latest

像之前一样,我们将创建一个 lambda 函数来启动一个 SageMaker 处理作业。我们在下面定义了我们的 lambda 处理程序。

import boto3
import datetime

def lambda_handler(event, context):
    sagemaker_client = boto3.client("sagemaker")

    # Parameters for the processing job
    job_name = "iris-preprocessing-job-" + datetime.datetime.now().strftime(
        "%Y%m%d%H%M%S"
    )
    role_arn = "arn:aws:iam::<your-account-id>:role/service-role/<role>"
    image_uri = (
        "<your-account-id>.dkr.<region>.amazonaws.com/preprocess-iris-data:latest"
    )
    input_s3_uri = "s3://automated-training-pipeline/data/iris.data"
    preprocess_s3_uri = "s3://automated-training-pipeline/modeling/preprocess.py"
    output_s3_uri = "s3://automated-training-pipeline/data/"

    response = sagemaker_client.create_processing_job(
        ProcessingJobName=job_name,
        RoleArn=role_arn,
        ProcessingInputs=[
            {
                "InputName": "input-1",
                "S3Input": {
                    "S3Uri": input_s3_uri,
                    "LocalPath": "/opt/ml/processing/input",
                    "S3DataType": "S3Prefix",
                    "S3InputMode": "File",
                    "S3DataDistributionType": "FullyReplicated",
                    "S3CompressionType": "None",
                },
            },
            {
                "InputName": "code",
                "S3Input": {
                    "S3Uri": preprocess_s3_uri,
                    "LocalPath": "/opt/ml/processing/code",
                    "S3DataType": "S3Prefix",
                    "S3InputMode": "File",
                    "S3DataDistributionType": "FullyReplicated",
                    "S3CompressionType": "None",
                },
            },
        ],
        ProcessingOutputConfig={
            "Outputs": [
                {
                    "OutputName": "output-1",
                    "S3Output": {
                        "S3Uri": output_s3_uri,
                        "LocalPath": "/opt/ml/processing/output",
                        "S3UploadMode": "EndOfJob",
                    },
                }
            ]
        },
        ProcessingResources={
            "ClusterConfig": {
                "InstanceCount": 1,
                "InstanceType": "ml.t3.medium",
                "VolumeSizeInGB": 5,
            }
        },
        AppSpecification={
            "ImageUri": image_uri,
            "ContainerArguments": [
                "--input-data",
                "/opt/ml/processing/input/iris.data",
                "--output-data",
                "/opt/ml/processing/output",
            ],
            "ContainerEntrypoint": ["python3", "/opt/ml/processing/code/preprocess.py"],
        },
    )

    return {"statusCode": 200, "body": response}

我们将 lambda 处理程序打包,并存储在 S3 中。

zip lambda_function_preprocess.zip lambda_handler_preprocess.py

aws s3 cp lambda_function_preprocess.zip s3://automated-training-pipeline/modeling/lambda_function_preprocess.zip

最后,我们创建我们的 lambda 函数。

aws lambda create-function \
    --function-name iris-preprocessing-trigger \
    --runtime python3.11 \
    --role arn:aws:iam::<your-account-id>:role/<role>\
    --handler lambda_handler_preprocess.lambda_handler \
    --code S3Bucket=automated-training-pipeline,S3Key=modeling/lambda_function_preprocess.zip \
    --memory-size 256 \
    --timeout 900 

由于我们还没有将 Lambda 函数串联起来,我们可以手动调用 Lambda 函数,并检查 S3 的内容以确保我们的训练和测试数据集存在。

aws lambda invoke --function-name iris-preprocessing-trigger --payload '{}' 

aws s3 ls s3://automated-training-pipeline/data/

#2023-10-24 14:50:46       4551 iris.data
#2023-10-24 17:08:42       2404 test_data.csv
#2023-10-24 17:08:42       9635 train_data.csv

2c. 模型训练

我们将使用一个内置的 SageMaker 镜像来进行逻辑回归,而不是为训练创建我们自己的镜像。如果你想要一个定制化的训练过程,你可以创建自己的镜像。因此,我们会立即编写我们的 lambda 处理程序。

import boto3
import datetime

def lambda_handler(event, context):
    sagemaker_client = boto3.client("sagemaker")

    job_name = "iris-training-job-" + datetime.datetime.now().strftime("%Y%m%d%H%M%S")
    role_arn = "arn:aws:iam::<your-account-id>:role/service-role/<role>"  # Update the ARN as required

    image_uri = "382416733822.dkr.ecr.us-east-1.amazonaws.com/linear-learner:1" # Public learner

    training_data_s3_uri = "s3://automated-training-pipeline/data/train_data.csv"
    output_s3_uri = "s3://automated-training-pipeline/models/"

    response = sagemaker_client.create_training_job(
        TrainingJobName=job_name,
        RoleArn=role_arn,
        AlgorithmSpecification={
            "TrainingImage": image_uri,
            "TrainingInputMode": "File",
        },
        InputDataConfig=[
            {
                "ChannelName": "train",
                "DataSource": {
                    "S3DataSource": {
                        "S3DataType": "S3Prefix",
                        "S3Uri": training_data_s3_uri,
                        "S3DataDistributionType": "FullyReplicated",
                    },
                },
                "ContentType": "text/csv",  # Specify that we're using CSV format
            },
        ],
        OutputDataConfig={
            "S3OutputPath": output_s3_uri,
        },
        ResourceConfig={
            "InstanceCount": 1,
            "InstanceType": "ml.m5.large",
            "VolumeSizeInGB": 5,
        },
        HyperParameters={
            "predictor_type": "multiclass_classifier",
            "num_classes": "3",
            "mini_batch_size": "30",
        },
        StoppingCondition={
            "MaxRuntimeInSeconds": 3600,
        },
    )

    model_artifact_uri = f"{output_s3_uri}/{job_name}/output/model.tar.gz"

    return {
        "statusCode": 200,
        "body": {"trainingJobName": job_name, "modelArtifactUri": model_artifact_uri},
    }

在我们创建 lambda 函数之前,我们必须将处理程序打包并发送到 S3。

zip lambda_function_train.zip lambda_handler_train.py

aws s3 cp lambda_function_train.zip s3://automated-training-pipeline/modeling/lambda_function_train.zip

现在我们可以创建我们的 lambda 函数了。

aws lambda create-function \                                                                           
    --function-name iris-training-trigger \
    --runtime python3.11 \
    --role arn:aws:iam::<your-account-id>:role/<role> \
    --handler lambda_handler_train.lambda_handler \
    --code S3Bucket=automated-training-pipeline,S3Key=modeling/lambda_function_train.zip \

我们可以手动调用我们的训练 lambda 函数,以确保其正常工作。

aws lambda invoke --function-name iris-training-trigger --payload '{}' output.txt

aws s3 ls s3://automated-training-pipeline/models/

#PRE iris-training-job-20231025015943/

3. 步骤函数

现在,我们有一个非常基本的流程,包括数据获取、预处理和模型训练。然而,这些组件各自存在——我们如何将它们串联起来以创建一个自动化的流程?步骤函数!

为了将我们的 lambda 函数串联起来,我们首先将使用 Amazon States Language 定义一个状态机,并将其保存在 .json 文件中,以便我们可以通过 CLI 将其推送到 AWS。对于我们的目的,我们将以以下方式定义我们的状态机。

{
  "Comment": "A simple AWS Step Functions state machine that orchestrates 3 Lambda functions.",
  "StartAt": "DataIngestion",
  "States": {
    "DataIngestion": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:data-ingestion-lambda",
      "Next": "Preprocessing"
    },
    "Preprocessing": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:preprocessing-lambda",
      "Next": "Training"
    },
    "Training": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:training-lambda",
      "End": true
    }
  }
}

我们可以将步骤函数部署到 AWS。

aws stepfunctions create-state-machine \
    --name “automated-training-pipeline“ \
    --definition file://pipeline.json \
    --role-arn "arn:aws:iam::<your-account-id>:role/<execution role>”

一旦状态机创建完成,我们可以在管理控制台中可视化它。

我们可以手动调用流程,如下所示。

aws stepfunctions start-execution \
    --state-machine-arn "arn:aws:states:us-east-1:<your-account-id>:stateMachine:<YourStateMachineName>"

附加考虑事项与结论

我们在这个例子中构建的流程非常基础——我们甚至没有考虑模型评估!在实际应用中,这种方法可以扩展到涵盖模型构建生命周期的其他部分,包括模型评估和部署。

此外,我们已经构建了我们的管道,但必须手动触发。在实际应用中,你可以根据各种事件触发 Step Functions 状态机的执行。使用触发器提供了更具事件驱动的架构。以下是为状态机设置触发器的一些常见方法:

  1. Amazon S3 Events:如果你希望在将新文件上传到 S3 桶时运行状态机,可以在桶内设置事件来触发 Lambda 函数,从而启动状态机的执行。

  2. AWS CodeCommit:如果你使用 AWS CodeCommit 作为代码库,可以使用 AWS Lambda 和 Amazon CloudWatch Events 在有新的提交时触发状态机。

  3. GitHub 或其他代码库:如果你使用类似 GitHub 的服务,可以使用 webhooks 通知 AWS 服务有关新的提交或拉取请求合并。通常,你会设置一个 API Gateway 端点来接收 webhook,触发 API Gateway 调用的 Lambda 函数,然后让 Lambda 函数启动你的状态机。

  4. Amazon DynamoDB Streams:如果你想根据 DynamoDB 表中的变化触发状态机,可以使用 DynamoDB Streams。每当表中发生变化时,流可以触发一个 Lambda 函数,然后启动状态机。

  5. AWS EventBridge(之前的 CloudWatch Events):AWS EventBridge 允许你基于各种 AWS 服务事件创建规则。你可以使用这些规则来指定 Lambda 函数。然后,像其他场景一样,Lambda 函数将启动你的状态机。

总之,机器学习和数据科学领域的自动化训练管道提供了多种优势。以下是你结论部分的简洁优点列表:

  1. 一致性和可重复性:自动化确保训练过程在不同运行中保持一致。这有助于重现结果,并消除了由于人工干预而导致的变化。

  2. 效率:自动化管道可以简化流程,减少训练和重新训练模型所需的时间。这在迭代不同的模型架构或参数时尤其有利。

  3. 可扩展性:随着数据或模型复杂性的增加,自动化管道可以扩大资源和流程,而无需人工监督。

  4. 资源优化:自动化可以更有效地管理资源,从而可能节省成本。例如,云资源可以在不使用时自动缩减。

如有任何问题,请随时联系!

Scikit-Learn 中的管道:一种打包转换的绝妙方法

原文:towardsdatascience.com/pipelines-in-scikit-learn-an-amazing-way-to-bundle-transformations-9ef0594000ac

管道如何帮助您编写更好的机器学习和数据科学代码 😍

Eirik Berge, PhDTowards Data Science Eirik Berge, PhD

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

--

图片来源:Rodion KutsaievUnsplash

您的旅程概述

  1. 介绍

  2. 没有管道的问题?

  3. 管道来救援!

  4. 有用的属性和实用函数

  5. 总结

1 — 介绍

处理机器学习任务的最受欢迎的 Python 库之一是 scikit-learn。它于 2010 年发布,自那时起,它对于实现流行的监督学习算法,如 逻辑回归随机森林支持向量机 已经成为必不可少的工具。

在使用 scikit-learn 编写代码时,您可以使用一个叫做 管道 的功能。这个功能允许您将机器学习过程中的几个步骤打包成一个单一的组件。管道的使用是判断 scikit-learn 代码是否易于操作的最关键因素之一。许多人在创建 scikit-learn 机器学习模型时忽视了管道,这真的让人沮丧 😞

在这篇博客文章中,您将学习到 scikit-learn 管道的优点。阅读完毕后,您应该能够自信地将管道应用到自己的机器学习项目中。让我们开始吧 👍

2 — 没有管道的问题?

管道的目标是将机器学习项目中的多个步骤封装成一个可管理的整体。为了说明这一点,我们从以下设置代码开始:

from sklearn.datasets import make_classification
from sklearn.preprocessing import MinMaxScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

# Create data
X, y = make_classification(random_state=42)

# Split the data into training and testing
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1)

如上代码块中的导入所示,我们将对数据进行缩放,然后使用随机森林模型进行分类。如果没有使用管道,这可能看起来是这样的:

# Scale the data
scaler = MinMaxScaler(feature_range=(0, 1))
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)

# Train the random forest
random_forest = RandomForestClassifier(n_estimators=10)
random_forest.fit(X_train_scaled, y_train)

# Predict with the random forest
random_forest.predict(X_test_scaled)

上面的代码存在一些严重和轻微的问题!让我们提及其中的一些:

  • 数据泄漏: 如果仔细查看上面的代码,你会发现它实际上将训练数据的信息泄露给了测试数据。具体来说,当scaler转换X_test时,它使用了X_train的最小值和最大值。因此,训练集的信息在测试集中被揭示。这可能使随机森林在测试集上的准确性有些乐观!

  • 中间变量名称: 我们创建了中间变量名称X_train_scaledX_test_scaled。这仅仅是因为我们将缩放和训练作为完全独立的过程。

  • 较少的显式函数调用: 在上面的代码中,几行是中间的.fit().transform()函数调用。这使得代码变得混乱且不易读。

  • 超参数搜索: 我们希望对MinMaxScaler中的feature_range参数和RandomForestClassifier中的n_estimators参数进行超参数搜索。我们需要完全分开进行这些搜索!这不仅麻烦,而且我们会分别贪婪地优化每个参数,而不是寻找一个组合的最佳解。这可能使我们错过最佳解决方案。

3 — 管道来拯救!

照片由Erlend Ekseth提供,来自Unsplash

不足为奇的是,管道将拯救这个问题。将上述代码与以下代码片段进行比较:

from sklearn.pipeline import Pipeline

# Create a pipeline that combines the scaling and training
pipe = Pipeline([
  ('scaler', MinMaxScaler(feature_range=(0, 1))), 
  ('forest', RandomForestClassifier(n_estimators=10))
])

# Fit the pipeline to the training data
pipe.fit(X_train, y_train)

# Predict with the random forest
pipe.score(X_test, y_test) 

使用管道的代码更简洁、更高效,并避免了上述所有问题:

  • 数据泄漏: 管道自动确保没有数据泄漏发生!

  • 中间变量名称: 你不再需要像X_train_scaledX_test_scaled这样的中间变量名称了!

  • 较少的显式函数调用: 使用管道,你只需要一个.fit()函数调用来执行整个序列。这使得代码更易读!

  • 超参数搜索: 一旦使用了管道,你可以对所有组件同时进行超参数搜索。方法.get_params()对获取管道中所有变换器/估计器的参数名称非常有用。这个在博客文章将管道集成到 Scikit-Learn 的超参数搜索中中有很好的解释!

4 — 有用的属性和实用功能

在 scikit-learn 中拟合管道后,有一些属性可以让你的工作变得更轻松。我曾经忽视这些属性,付出了代价😅

第一个是属性named_steps

# Gives us the components of the pipeline
print(pipe.named_steps)

# Output:
{'scaler': MinMaxScaler(), 'forest': RandomForestClassifier(n_estimators=10)}

# Can now access each of them
print(pipe.named_steps["forest"])

# Output:
RandomForestClassifier(n_estimators=10)

每当你有一个复合对象(如 scikit-learn 中的管道)时,知道如何访问各个组件是很有用的。

另一个有用的属性是.n_features_in_。这将显示传递到管道第一个隐式.fit()方法中的特征数量(在我们的例子中,是MinMaxScaler.fit()方法):

# Gives us the number of features passed into the pipeline
print(pipe.n_features_in_)

# Output:
20

最后,你还可以使用实用函数make_pipeline()来创建 scikit-learn 中的管道。不同之处在于,make_pipeline 会自动为不同的变换器/估计器命名:

from sklearn.pipeline import make_pipeline

# Automatically assign names to the components
pipe = make_pipeline(MinMaxScaler(), RandomForestClassifier(n_estimators=10))
print(pipe)

# Output:
Pipeline(
  steps=[
    ('minmaxscaler', MinMaxScaler()),
    ('randomforestclassifier',RandomForestClassifier(n_estimators=10))
  ]
)

我个人更喜欢使用实用函数make_pipeline,这样我就不需要自己想出名称。如果有许多开发人员在处理不同的管道,那么make_pipeline 确保一致性

如果你想了解更多关于管道的嵌套参数和缓存的信息,请查看 管道用户指南

5— 总结

图片由 Spencer Bergen 提供,来源于 Unsplash

希望你现在理解了在编写 Scikit-Learn 机器学习代码时如何以及为什么要使用管道。如果你对数据科学、编程或介于两者之间的任何事物感兴趣,可以在 LinkedIn 上加我并打个招呼 ✋

喜欢我的写作吗? 查看我其他的一些文章获取更多 Python 内容:

  • 用优美的类型提示现代化你罪恶的 Python 代码

  • 在 Python 中可视化缺失值极其简单

  • 在 Python 中使用 PyOD 介绍异常/离群值检测 🔥

  • 5 个在紧急情况下拯救你的了不起的 NumPy 函数

  • 5 个提升你 Python 词典技能的专家建议 🚀

posted @ 2024-10-12 19:55  绝不原创的飞龙  阅读(521)  评论(0)    收藏  举报