TowardsDataScience 2023 博客中文翻译(七十六)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

大学橄榄球会议重组——回归分析

原文:towardsdatascience.com/college-football-conference-realignment-regression-8f0776278d55

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

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

欢迎来到我的系列文章第二部分,讨论会议重组!去年夏天,当会议重组如火如荼时,Tony Altimore 在 Twitter 上发布了一项研究,激发了我进行自己的会议重组分析。该系列分为四部分(完整的动机在第一部分中可以找到):

  1. 大学橄榄球会议重组——Python 中的探索性数据分析

  2. 大学橄榄球会议重组——回归分析

  3. 大学橄榄球会议重组——聚类分析

  4. 大学橄榄球会议重组——node2vec

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

图片由诺伯特·布劳恩拍摄,来源于Unsplash

希望系列的每个部分都能为你提供对备受喜爱的大学橄榄球未来的新视角。对于那些没有阅读第一部分的人,简要概述是我创建了自己从网络各个来源汇编的数据集。这些数据包括每个 FBS 项目的基本信息、所有大学橄榄球对抗赛的非官方近似值、体育场大小历史表现AP 前 25 名投票的出现频率、学校是否为AAUR1机构(对加入 Big Ten 和 Pac 12 历史上非常重要)、NFL 选秀人数2017-2019 年程序收入数据以及关于大学橄榄球粉丝基础规模的近期估计。结果表明,体育场容量、2019 年收入和历史 AP 投票成功率与 Tony Altimore 的粉丝基础规模估计结果有很强的相关性。

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

相关矩阵显示了每个特征与自身之间的完美正相关关系。我们还看到体育场容量、粉丝基础规模、2019 年收入以及 2001 年至 2021 年间出现在 AP 前 25 名的周数之间存在较高的相关性。

监督学习

所以,这让我思考:我们能否创建一个简单的回归模型来估计粉丝基础的大小?

广泛来说,我们可以将机器学习分为监督学习和非监督学习。在监督学习中,目标是预测一个预定义的离散类别或连续变量。在非监督学习中,目标是发现数据中那些不明显的趋势。回归是一种监督学习类型,其中预测目标是一个连续变量。Shervine 和 Afshine Amidi 编写了一个极好的参考指南和资源。 (它已经被翻译成 11 种其他语言!)

我们选择的回归模型受限于数据中观察数量较少,因为大学橄榄球中只有 133 支球队。不论我们选择什么模型,scikit-learn 包将为我们提供支持。它易于实现且文档详尽。

特征工程

现在我们有了方法,可以重构数据以获得最佳模型性能。这通常被称为特征工程。首先,我们导入依赖项并上传数据。

#Import dependencies
import numpy as np
import pandas as pd
# Read csv of data
cfb_info_df = pd.read_csv(r'.\FBS_Football_Team_Info.csv', encoding = 'unicode_escape')

我们将仅保留对本分析相关的特征:

# Drop Unused columns
cfb_info_df_regression = cfb_info_df[['Latitude', 'Longitude','Enrollment', 'Current_conference_2025','years_playing', 'years_playing_FBS', 'Stadium_capacity', 'is_aau_member', 'is_R1', 'total_draft_picks_2000_to_2020', 'first_rd_draft_picks_2000_to_2020', 'number_1_draft_picks_2000_to_2020',  'wsj_college_football_revenue_2019', 'wsj_college_football_value_2018', 'wsj_college_football_value_2017', 'bowl_games_played', 'bowl_game_win_pct', 'historical_win_pct', 'total_games_played','p_AP_Top_25_2001_to_2021', 'tj_altimore_fan_base_size_millions']]

现在,我们可以将这些数据分为特征 X 和标签 y。在这种情况下,特征是所有数据,除了估计的粉丝基础规模。该估计值作为标签。

X = cfb_info_df_regression.drop(['tj_altimore_fan_base_size_millions'], axis = 1)
y = cfb_info_df_regression['tj_altimore_fan_base_size_millions']

现在,我们可以使用 pandas 将分类特征转换为 独热编码 向量。这将我们的会议名称列转换为几个布尔值列。

X = pd.get_dummies(X, columns = ['Current_conference_2025'])

我们可以使用 scikit-learn 中的 train_test_split 函数轻松地对数据进行 70–30 的训练-测试集拆分。对于我们的目的,这将给我们 93 个训练观测值和 40 个测试观测值。

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)

我们将使用最小-最大缩放对数值特征进行转换。最小-最大缩放很重要,因为它可以将每个数值分布的范围更改为 0 到 1 之间,同时保持分布的形状。实现这一点很重要,因为它是在将数据拆分为训练集和测试集之后进行的,以避免数据泄露。使用 scikit-learn 有预定义的方法来执行此操作,包括一个 内置管道函数,我们可以在其中定义预处理类型,但为了我们的目的,我定义了自己的 min_max_scaling 函数,并使用此函数转换了所有列:

def min_max_column(column):
    column = column.astype('float')
    column_scaled = (column - min(column)) / (max(column) - min(column))
    return column_scaled

现在,我们分别将训练集和测试集中的所有特征转换,以避免数据泄露:

for col in X_train.columns:
    X_train[col] = min_max_column(X_train[col])

for col in X_test.columns:
    X_test[col] = min_max_column(X_test[col])

线性回归

这样,我们就准备好运行模型了。我们从简单的 线性回归 开始。

from sklearn.linear_model import LinearRegression
reg = LinearRegression().fit(X_train, y_train)

我们可以使用通过 score() 函数计算的 R 平方度量来衡量回归的表现。

from sklearn.linear_model import LinearRegression
reg = LinearRegression().fit(X_train, y_train)

不幸的是,R 平方度量只有大约 0.5,所以我们的预测效果不是很好。我们可以使用 plotly 绘制实际粉丝基础规模与预测粉丝基础规模的比较图。在下图中,散点图中每个点的大小是绝对百分比误差的大小。颜色表示是否存在不足预测。你可以直观地看到,模型对小型粉丝基础的表现最差:

import plotly.express as px
import plotly.express as px
#Create a data frame for plot
plot_df = pd.DataFrame(cfb_info_df['Team'].iloc[list(y_test.index)], columns=['Team'])
plot_df['Actual Fan Base Size'] = y_test
plot_df['Predicted Fan Base Size'] = reg.predict(X_test)
plot_df['Absolute Percent Error'] = abs(plot_df['Actual Fan Base Size'] - plot_df['Predicted Fan Base Size'])/plot_df['Actual Fan Base Size']
plot_df['Under Predict'] = plot_df['Actual Fan Base Size'] > plot_df['Predicted Fan Base Size']

fig = px.scatter(plot_df, x='Actual Fan Base Size', y='Predicted Fan Base Size', size = 'Absolute Percent Error', 
                 color = 'Under Predict', hover_data = ['Team'])
fig.show()

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

在图中,我们可以直观地看到线性回归的表现,比较实际的粉丝基础规模(以百万计)与预测的规模。点的大小是绝对百分比误差,颜色表示模型是过度预测还是不足预测。

随机森林

现在我们已经看到线性模型的表现,接下来让我们尝试一种更高级的机器学习模型——随机森林。随机森林依赖于一种叫做“自助法”(bagging)的概念来提高预测准确性。它实际上生成了许多不同的决策树,这些树由于引入的随机性而略有不同。它将这些树中学到的知识结合起来,以改善整体预测。

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

维基百科中包含了关于随机森林如何工作的优秀可视化图示。

方便的是,我们不需要对数据进行缩放,因为随机森林模型不基于距离度量来进行预测。因此,我们可以从 train_test_split()函数中重新采样:

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)

现在,我们可以轻松地训练一个包含 100 棵树且每棵树深度无限的随机森林模型。

from sklearn.ensemble import RandomForestRegressor
reg = RandomForestRegressor(n_estimators=100, max_depth=None, random_state=0)
reg.fit(X_train, y_train)

让我们看看 R 平方值是否有所提升:

reg.score(X_test, y_test)

R 平方值现在为 0.78,这是一个很大的改进!如果我们使用新的随机森林模型创建与上面相同的图表,我们可以看到小规模粉丝群体的表现大大改善。重要的是,我们也不再预测负的粉丝基础规模。

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

随机森林模型在将实际粉丝基础规模(以百万计)与预测规模进行比较时表现更好。现在没有更多的负粉丝数的团队了!

那么,是什么驱动了这些预测呢?随机森林在可解释性方面表现出色,因为它包括一个叫做特征重要性的属性。类似于线性回归中的系数,这些是随机森林模型在进行预测时依赖某个特征的程度。特征重要性是一个相对度量,所以它只能告诉我们某个特征在此模型中的有用程度。

import plotly.express as px
#Create a data frame for plot
plot_df = pd.DataFrame(X_train.columns, columns=['Feature Name'])
plot_df['Importance'] = reg.feature_importances_
fig = px.bar(plot_df, x='Feature Name', y='Importance')
fig.show()

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

随机森林模型中的特征重要性显示,场上表现是粉丝基础规模的驱动因素。

根据上面比较不同特征的条形图,场上表现似乎是预测的主要驱动因素。最重要的特征是过去 20 年中团队进入 AP 前 25 的周数百分比。其次重要的特征群体是《华尔街日报》的价值/收入数据。显然,拥有更多粉丝的团队赚得更多。然后我们看到 NFL 选秀、体育场容量(更多粉丝=更大的体育场)、碗赛出场次数以及历史胜率也很重要。地理位置、招生人数、踢足球年限和学术成功似乎不太适合用来预测。

我将最重要的结论留到最后,因为它与会议重组讨论相关。你注意到图表的右侧了吗?会议成员资格并不是粉丝基础规模的重要预测因素。正如我们在本博客系列第一部分讨论的那样,相关性并不等于因果关系,特征重要性也是如此。然而,这确实似乎表明你可以在任何会议中同样快速地获得或失去粉丝。关键在于场上的表现。

模型改进

我不会在这篇博客中包含这些内容,但我们可以花些时间通过更好的特征工程或超参数调整来改进这些模型。此外,报告交叉验证的准确性也是更好的做法。我们的数据集很小,所以我会将这部分留到另一篇博客中。

确保继续阅读本博客系列的第三部分,因为我们将最终深入探讨一些基于数据的大学橄榄球会议建议。

对我的内容感兴趣吗?请考虑在 Medium 上关注我

在 Twitter 上关注我:@malloy_giovanni

你发现过任何有趣的回归应用案例在大学橄榄球中吗?你会如何改进这个模型?

线性规划中的列生成与切割库存问题

原文:towardsdatascience.com/column-generation-in-linear-programming-and-the-cutting-stock-problem-3c697caf4e2b?source=collection_archive---------4-----------------------#2023-06-13

如何通过 Python 示例解决具有大量决策变量的线性问题

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 布鲁诺·斯卡利亚·C·F·莱特

·

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

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

照片由 Jean Vella 提供,刊登在 Unsplash

一些来源于组合问题的线性规划(LP)问题由于涉及的大量变量变得难以处理(Gilmore & Gomory, 1961)。在这种问题中,延迟列生成是一个潜在的解决方案,因为它不会从一开始就将所有可能的决策变量包括在问题中。一个经典应用是裁切库存问题,在这个问题中,需要决定如何将给定宽度的卷材裁切成小块,以满足对特定裁切尺寸的需求。

在本文中,将介绍列生成的一些主要理论方面,并通过一维裁切库存问题进行说明。在解决方案中,将使用 Python 库scipy。有兴趣的人也可以在这个git 仓库中找到完整的代码。

如果你还不熟悉线性规划,在阅读我之前关于这一主题的介绍后,你可能会对这篇文章有更好的理解。

## 线性规划:理论与应用

线性优化的主要概念及其在 Python 中的实现

towardsdatascience.com

如果你准备好深入了解更复杂的概念,并且希望通过实际示例来学习,欢迎加入。

列生成:概述

在使用延迟列生成来解决线性规划问题时,每次迭代时只考虑包含部分列的问题。这个问题被称为限制主问题(RMP)。在每次迭代中,RMP 会被求解,并得到其对应的最优对偶决策变量π。然后,算法使用这个结果来解决子问题定价问题,旨在找到具有负的缩减成本的新决策变量。让我们回顾一下缩减成本的定义。

对于任何非基本变量,变量的缩减成本是指在该变量成为 LP 中某个最优解的基本变量之前,其目标函数系数必须改善的数量。Winston & Goldberg (2004)

因此,通过包含具有负缩减成本的变量,我们可能会期待在改善目标值方面的边际贡献。回顾对偶变量的经济解释作为影子成本。一个新的变量可能会导致与其在满足约束时所贡献的对偶变量相关的节省,同时其成本系数会增加总体成本。

通过解决子问题,我们识别出一组具有最低减值成本的列集合S。如果没有识别出负减值成本的列,我们就停止,因为π是原始问题的最优对偶解,与 RMP 的最优原始解结合,我们得到了最优的原始/对偶解对。否则,我们将S中的列附加到 RMP 中并进行迭代(Klabjan, 2005)。该过程由下图表示。

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

列生成方案。(图片由作者提供)

请注意,子问题是特定于问题的,在某些情况下,可能相当计算上昂贵,难以将其表述为混合整数规划问题。因此,考虑启发式方法和/或动态规划方法可能会有用,这些方法在每次迭代中返回多个负减值成本的列。在车辆路径问题的情况下,子问题通常是一个约束最短路径问题。那些对深入研究感兴趣的人可以参考 Irnich & Desaulniers(2005)以获取解决技术的见解。

现在记住,所呈现的延迟列生成方法解决了具有实值决策变量的线性规划问题。为了求解大型整数混合整数程序,可以在求解松弛后采用一些启发式方法或使用分支与定价来施加整数约束。在后者方法中,应考虑不仅在初始 LP 解中生成的列,还应在解决整个树中的 LP 时生成新列。Barnhart 等(1998)对分支与定价进行了更深入的讨论。作者研究了常见问题、案例研究以及有关分支规则的有趣见解。

切割库存问题

假设我们有一个需求集I,每个需求量为w的片段数d。同时,假设我们有一个宽度为W的卷材,从中将生产切割。已知的切割模式集记作P。每个切割模式p的片段消耗一个卷材单位(c = 1),并产生宽度wᵢaᵢₚ单位。我们的目标是确定每个模式p的切割量x,以满足需求的同时最小化消耗的单位数量。我们可以将这个问题表述如下。

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

切割库存问题作为一个集合覆盖问题。(图片由作者提供)。

与需求约束π相关的对偶决策变量随后在定价问题(子问题)中用于寻找具有负减值成本的新模式。在切割库存问题的情况下,我们必须找到一个结合了不同宽度的片段的新模式,使其适应总宽度W,并且通过帮助满足物料需求,将带来比新成本更多的节省。这是一个背包问题,可以表述为以下形式。

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

切割库存定价问题。(作者提供的图像)。

其中 yᵢ 对应于在新切割模式中生产的宽度 wᵢ 的片数。

由于我们知道每个新模式的单位成本 c 为 1,我们可以通过以下方式计算新模式的减少成本:

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

新切割模式的减少成本。(作者提供的图像)。

在非负值的情况下,这应该会停止列生成算法。

为了获得切割库存问题的整数解,一种简单的启发式方法是将线性松弛中获得的分数值四舍五入。或者,也可以用在线性松弛中产生的模式集来解决线性问题,施加整数约束。我们将在本文中使用这两种策略。对于线性松弛接近完整整数模型的实例,这些策略可能会非常成功。在需求数量相对较少的其他实例中,可能会出现一些差异。

如果目标是获得精确的整数解,Branch & Price 方法可能是一个好的替代方案。在这种方法中,在对一些初始变量进行分支后,可能会在当前节点中包含具有减少成本的新列。那些对更多细节感兴趣的人可以参考 Carvalho (1998)和 Vance (1998)。

现在让我们动手实践一下!

解决方案

让我们开始切割库存问题的 Python 实现,其中 LP 松弛问题被解决到最优解,并且用迄今为止产生的模式解决整数模型。我们将使用numpy进行线性代数运算,pandas处理数据框,scipy进行优化算法,matplotlib可视化切割模式。

import numpy as np
import pandas as pd
from scipy.optimize import linprog
import matplotlib.pyplot as plt

让我们导入一个数据集,该数据集是JuMP 文档中提出问题的修改版。

dataset = pd.read_csv("data.txt", sep=" ")

并且让我们实例化问题参数。

# Total width
W = 100.0

# Width and amount associated with each demand
w = dataset.w.values
d = dataset.d.values

# LP parameters
A = np.eye(dataset.shape[0]) * (W // w)
c = np.ones_like(w)

注意,为了初始化A矩阵,我引入了切割简单模式,这些模式产生了每种宽度需求的最大可行卷数。假设有对宽度 24 的卷的需求。这将导致初始切割模式的系数为 4,考虑到总宽度 W 为 100。

现在让我们定义一个函数来解决给定总宽度 W、与每个需求 w 相关的宽度向量以及当前对偶变量 duals子问题

def solve_knapsack(W, w, duals):
    return linprog(
        -duals, A_ub=np.atleast_2d(w), b_ub=np.atleast_1d(W),
        bounds=(0, np.inf), integrality=1,
    )

在主循环中,对于每个限制主问题的解,我们将使用来自scipylinprog函数。A矩阵和需求向量都以其负值传递,因为scipy按惯例仅考虑“小于或等于”不等式。

linprog(c, A_ub=-A, b_ub=-d, bounds=(0, None))

解决方案应该具有与需求相关的对偶变量的负值,这些值存储在ineqlin.marginals属性中。

探索对偶性概念时,还可以通过解决以下LP来获得对偶变量。

linprog(-d, A_ub=A.T, b_ub=c, bounds=(0, None))

让我们把所有内容汇集到主循环中。

# Initial solution
sol = linprog(c, A_ub=-A, b_ub=-d, bounds=(0, None))
sol_dual = linprog(-d, A_ub=A.T, b_ub=c, bounds=(0, None))
diff = np.abs(sol_dual.x + sol.ineqlin.marginals).sum()
print(f"Compare duality difference: {diff}")

# Iterate
for _ in range(1000):
    duals = -sol.ineqlin.marginals
    price_sol = solve_knapsack(W, w, duals)
    y = price_sol.x
    if 1 + price_sol.fun < -1e-4:
        print(f"Iteration: {_}; Reduced cost: {(1 + price_sol.fun):.3f}")
        A = np.hstack((A, y.reshape((-1, 1))))
        c = np.append(c, 1)
        sol = linprog(c, A_ub=-A, b_ub=-d, bounds=(0, None))
    else:
        break

最后,让我们尝试将线性松弛的解四舍五入以及仅考虑在LP中生成的列的整数解。

sol_round = linprog(c, A_ub=-A, b_ub=-d, bounds=(0, np.inf), integrality=0)
print(f"Rounding solution {np.ceil(sol_round.x).sum()}")
sol = linprog(c, A_ub=-A, b_ub=-d, bounds=(0, np.inf), integrality=1)
print(f"Integer solution: {sol.x.sum()}")

应该返回:

  • 四舍五入解:339.0

  • 整数解:334.0

在这种情况下,我们可以通过对LP施加整数约束,而不仅仅是对松弛结果进行四舍五入,从而将结果提高近 2%。给那些愿意尝试 Branch & Price 的人一个小提示:334 是该实例的确切解。

最后,让我们尝试可视化新的切割模式:

fig, ax = plt.subplots(figsize=[7, 3], dpi=100)
hmap = ax.imshow(A > 1e-6, cmap="Blues")
plt.axis('off')
fig.tight_layout()
plt.show()

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

切割库存问题中生成的切割模式。(作者提供的图片)。

我们使用列生成法解决了切割库存问题。

进一步阅读

容量受限的车辆路径问题(CVRP)首次由 Dantzig & Ramser(1959)提出,由于其组合性质,特别具有挑战性。作者在他们的原始论文中表明,即使对于小规模问题,可能的路径数量也极其庞大。例如,一个有 15 个需求点的对称问题有超过 6 × 10¹¹条可能的路径。我发现看到列生成如何在这些及相关问题中随着时间的推移被探索特别有趣。

尽管对于时间窗口约束严格的车辆路径问题,自 Desrochers 等(1992)的工作以来,列生成方法已经建立得很好,但 Branch & Price 在约束较少的实例上往往会失败。因此,纯列生成方法并未被认为是 CVRP 的有前途的方法。然而,Fukasawa 等(2006)将列生成方法结合到 Branch & Cut 算法中,证明了文献中若干实例的最优性。其他作者进一步改进了 CVRP 的 Branch-cut-and-Price 方法,我相信 Pecin 等(2017)的工作对于感兴趣的读者尤其吸引人。

结论

在这篇文章中,延迟列生成作为一种解决具有大量决策变量的线性程序的策略被介绍,而无需显式地考虑所有变量。介绍了经典的一维切割库存问题来说明该方法,并在 Python 中实现了一个解决方案备选方案。完整代码可在此git 库中获取。

参考文献

Barnhart, C., Johnson, E. L., Nemhauser, G. L., Savelsbergh, M. W., & Vance, P. H., 1998. Branch-and-price: 列生成用于解决大型整数程序。运筹学46(3),316–329。

de Carvalho, J. V., 1998. 使用列生成和分支界限法解决切割库存问题的精确解。国际运筹学交易5(1),35–44。

Dantzig, G. B., & Ramser, J. H., 1959. 卡车调度问题. 管理科学, 6(1), 80–91.

Desrochers, M., Desrosiers, J., & Solomon, M., 1992. 车辆路径问题与时间窗口的优化算法. 运筹学, 40(2), 342–354.

Fukasawa, R., Longo, H., Lysgaard, J., Aragão, M. P. D., Reis, M., Uchoa, E., & Werneck, R. F., 2006. 容量车辆路径问题的鲁棒分支切割价格方法. 数学规划, 106, 491–511.

Gilmore, P. C., & Gomory, R. E., 1961. 切割存货问题的线性规划方法. 运筹学, 9(6), 849–859.

Irnich, S., & Desaulniers, G., 2005. 资源约束下的最短路径问题 (pp. 33–65). Springer US.

Klabjan, D., 2005. 航空业的大规模模型. 列生成, 163–195.

Pecin, D., Pessoa, A., Poggi, M., & Uchoa, E., 2017. 容量车辆路径改进的分支切割价格方法. 数学规划计算, 9, 61–100.

Vance, P. H., 1998. 一维切割存货问题的分支价格算法. 计算优化与应用, 9, 211–228.

Winston, W. L. & Goldberg, J. B., 2004. 运筹学:应用与算法. 第四版. Belmont, CA: Thomson Brooks/Cole Belmont.

Dropout 正则化对抗过拟合

原文:towardsdatascience.com/combating-overfitting-with-dropout-regularization-f721e8712fbe?source=collection_archive---------2-----------------------#2023-03-03

探索在自己的机器学习模型中实现 Dropout 的过程

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

·

关注 发表在数据科学前沿 · 7 分钟阅读 · 2023 年 3 月 3 日

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

照片由皮埃尔·巴敏拍摄,发布于Unsplash

过拟合是大多数人在训练和使用机器学习模型时都会遇到的常见挑战。自机器学习诞生以来,研究人员一直在尝试解决过拟合问题。其中一种方法是 dropout 正则化,即随机删除模型中的神经元。在本文中,我们将探讨 dropout 正则化的工作原理,如何在自己的模型中实现它,以及与其他方法相比的优缺点。

I. 介绍

什么是过拟合?

过拟合是指模型在训练数据上过度训练,导致其在新数据上的表现不佳。实质上,模型在追求尽可能准确的过程中,过分关注训练数据中的细节和噪声。这些特征在现实世界的数据中往往不存在,因此模型的表现往往不佳。当模型的参数数量相对于数据量过多时,就容易发生过拟合。这会导致模型过度关注与模型需要开发的一般模式无关的小细节。例如,假设一个复杂的模型(参数较多)被训练来识别图片中是否存在马,那么它可能会开始关注天空或环境的细节,而不是马本身。这种情况可能发生在:

  1. 模型过于复杂(参数过多),对自己不利。

  2. 模型训练时间过长。

  3. 模型训练所用的数据集过小。

  4. 模型在相同的数据上进行训练和测试。

  5. 模型训练的数据集具有重复的特征,容易导致过拟合。

为什么过拟合很重要?

过拟合不仅仅是一个简单的烦恼——它可能摧毁整个模型。它给人一种模型表现良好的假象,尽管它可能没有对提供的数据做出正确的概括。

过拟合可能会带来极其严重的后果,尤其是在医疗保健等领域,人工智能越来越普及。由于过拟合而未能经过适当训练或测试的人工智能可能会导致错误的诊断。

II. 什么是 Dropout?

Dropout 作为一种正则化技术

理想情况下,对抗过拟合的最佳方法是训练多种不同架构的模型,并对它们的输出进行平均。然而,这种方法的问题在于,它极其耗费资源和时间。虽然相对较小的模型可能还算负担得起,但训练大型模型可能需要大量时间,这很容易超出任何人的资源承受能力。

Dropout 的工作原理是通过“丢弃”输入层或隐藏层中的神经元来实现的。网络中会移除多个神经元,这意味着它们实际上不存在——它们的输入和输出连接也会被破坏。这会人为地创建出多个较小的、更简单的网络。这迫使模型不依赖于一个单独的神经元,也就是说,它必须多样化其方法,并开发出多种方法来实现相同的结果。例如,回到马的例子,如果一个神经元主要负责马的树部分,那么它被丢弃会迫使模型更多地关注图像的其他特征。Dropout 也可以直接应用于输入神经元,这意味着整个特征会从模型中消失。

将 Dropout 应用于神经网络

Dropout 是通过在每一层(包括输入层)随机丢弃神经元来应用于神经网络的。预定义的 dropout 率决定了每个神经元被丢弃的概率。例如,dropout 率为 0.25 意味着神经元被丢弃的概率是 25%。Dropout 在模型训练的每个 epoch 中应用。

请记住,没有理想的 dropout 值——这在很大程度上依赖于模型的超参数和最终目标。

Dropout 和有性繁殖

回想一下你的大一生物课——你可能学过减数分裂或有性繁殖。在减数分裂的过程中,基因会发生随机突变。这意味着结果后代可能具有父母双方基因中都没有的特征。这种随机性随着时间的推移,使生物群体更适应其环境。这个过程被称为进化,没有它,我们今天也不会存在。

Dropout 和有性繁殖都旨在增加多样性,防止系统依赖于一组参数,而没有改进的空间。

III. 将 Dropout 应用于您的模型

数据集

让我们从一个可能容易过拟合的数据集开始:

# Columns: has tail, has face, has green grass, tree in background, has blue sky, 3 columns of noise | is a horse image (1) or not (0)
survey = np.array([
 [1, 1, 1, 1, 1, 1], # tail, face, green grass, tree, blue sky | is a horse image
 [1, 1, 1, 1, 1, 1], # tail, face, green grass, tree blue sky | is a horse image
 [0, 0, 0, 0, 0, 0], # no tail, no face, no green grass, no tree, no blue sky | is not a horse image
 [0, 0, 0, 0, 0, 0], # no tail, no face, no green grass, no tree, no blue sky | is not a horse image
])

这些数据回到了我们关于马和它的环境的例子。我们已将图像的特性抽象成一个易于解释的简单格式。可以清楚地看到,数据并不理想,因为包含马的图像也可能包含树、绿色草地或蓝色天空——它们可能在同一张图片中,但一个不会影响另一个。

MLP 模型

让我们用 Keras 快速创建一个简单的 MLP:

# Imports
from keras.models import Sequential
from keras.layers import Dense, Dropout
import numpy as np

# Columns: has tail, has face, has green grass, tree in background, has blue sky, 3 columns of noise | is a horse image (1) or not (0)
survey = np.array([
 [1, 1, 1, 1, 1, 1], # tail, face, green grass, tree, blue sky | is a horse image
 [1, 1, 1, 1, 1, 1], # tail, face, green grass, tree blue sky | is a horse image
 [0, 0, 0, 0, 0, 0], # no tail, no face, no green grass, no tree, no blue sky | is not a horse image
 [0, 0, 0, 0, 0, 0], # no tail, no face, no green grass, no tree, no blue sky | is not a horse image
])

# Define the model
model = Sequential([
    Dense(16, input_dim=5, activation='relu'),
    Dense(8, activation='relu'),
    Dense(1, activation='sigmoid')
])

# Compile the model
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

# Train the model
X = survey[:, :-1]
y = survey[:, -1]
model.fit(X, y, epochs=1000, batch_size=1)

# Test the model on a new example
test_example = np.array([[1, 1, 0, 0, 0]])
prediction = model.predict(test_example)
print(prediction)

我强烈推荐使用像 Jupyter Notebook 这样的 Python 笔记本来组织你的代码,以便你可以快速重新运行单元,而无需重新训练模型。沿着每个注释拆分代码。

让我们进一步分析一下我们正在测试模型的数据:

test_example = np.array([[1, 1, 0, 0, 0]])

本质上,我们有一张包含所有马的属性的图像,但没有我们在数据中包含的任何环境因素(绿色草地、蓝色天空、树木等)。模型输出:

0.02694458

哎呀!即使模型有脸和尾巴——我们用来识别马的特征——它对图像是马的判断信心只有 2.7%。

在 MLP 中实现 Dropout

Keras 使得实现 dropout(以及其他防止过拟合的方法)变得极其简单。我们只需返回到包含模型层的列表:

# Define the model
model = Sequential([
    Dense(16, input_dim=5, activation='relu'),
    Dense(8, activation='relu'),
    Dense(1, activation='sigmoid')
])

并添加一些 dropout 层!

# Define the model
model = Sequential([
    Dense(16, input_dim=5, activation='relu'),
    Dropout(0.5),
    Dense(8, activation='relu'),
    Dropout(0.5),
    Dense(1, activation='sigmoid')
])

现在模型输出:

0.98883545

尽管马图像中不包含环境变量,它的判断信心为 99% 这是一张马的图片!

Dropout(0.5) 这一行表示,上层中的任何神经元都有 50% 的机会被“丢弃”或从存在中移除,以便对后续层进行参考。通过实现 dropout,我们实际上以资源高效的方式在数百个模型上训练了 MLP。

选择一个 Dropout 率

找到适合你模型的理想 dropout 率的最佳方法是通过反复试验——没有一种放之四海而皆准的方法。可以从较低的 dropout 率开始,大约 0.1 或 0.2,然后逐渐增加,直到达到你期望的准确性。以我们的马匹 MLP 为例,dropout 率为 0.05 会导致模型对图像是马的判断信心为 16.5%。另一方面,dropout 率为 0.95 会导致模型丧失过多的神经元以至于无法正常工作——但仍然能够达到 54.1% 的信心。这些值对该模型并不合适,但可能对其他模型适用。

IV. 结论

让我们回顾一下——dropout 是一种在机器学习中用于防止过拟合并整体提升模型性能的强大技术。它通过随机“丢弃”输入层和隐藏层中的神经元来实现这一点。这使得分类器在一次训练过程中能够训练出数百到数千个独特的模型,从而避免过度关注某些特征。

在接下来的文章中,我们将探索在机器学习领域中用于替代或补充 dropout 的新技术。敬请期待更多内容!

将 dbt 模型合并为一个单一目标表

原文:towardsdatascience.com/combine-dbt-models-into-a-single-target-table-9873679ffd9b

涵盖 3 种模式及其权衡的教程

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

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

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

作者提供的图片

如果你在寻找一种简单的方式来构建分析管道,dbt 可能会引起你的关注。它旨在赋能数据分析师/科学家专注于他们的专业领域,并减少对数据工程师的依赖。

我注意到新手在使用 dbt 时常常会提出一个共同的问题:如何将多个具有共同模式的 dbt 模型加载到一个单一的目标表中?我将介绍一些可以适应不同用例的模式,并讨论一些权衡。

本教程的其余部分假设你已经设置了 dbt,并且对使用该工具有一定的熟悉度。即使你没有,我认为你也可以获得一些有用的见解。本教程使用 BigQuery。

问题设置

这将是一个简单的示例,我们将结合 3 个具有简单模式的 dbt 模型:

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

作者提供的图片

要开始,我们将在models目录中创建一个新目录,并将其命名为dbt-test。在dbt-test内部,我们将创建以下 4 个文件:

  1. table_1.sql
{{ 
  config(
    materialized="table"
) }}

SELECT *
FROM UNNEST([1, 2, 3, 4]) AS a

2. table_2.sql

{{ 
  config(
    materialized="table"
) }}

SELECT *
FROM UNNEST([45, 23, 1, 111]) AS a

3. table_3.sql

{{ 
  config(
    materialized="table"
) }}

SELECT *
FROM UNNEST([88, 55, 34, 342]) AS a

4. schema.yml

 version: 2

models:
    - name: table_1
    - name: table_2
    - name: table_3
    - name: union_table

注意:union_table将是目标表的名称(如下讨论)。

SQL 文件表示将加载到目标表中的上游表。现在dbt-test目录应如下所示:

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

作者提供的图片

模式 1:手动 UNION

第一个模式将简单地创建一个新模型,该模型将UNION我们所有的表,并将其物化为一个新表union_table

union_table.sql

 {{ 
  config(
    materialized="table"
) }}

SELECT *
FROM `dbt-test`.`dbt_test`.`table_1`
UNION ALL
SELECT *
FROM `dbt-test`.`dbt_test`.`table_2`
UNION ALL
SELECT *
FROM `dbt-test`.`dbt_test`.`table_3`

这个解决方案很可能是大多数 SQL 开发人员的起点,并且运行良好。这个模式强调简洁性/可读性,代价是扩展性。也就是说,如果我们需要 UNION 100 个表,或者表的数量经常变化,它将很快变得难以管理。此外,如果任何源表失败或不存在,整个模型都会失败。

模式 2:使用 for 循环进行 UNION

这个模式有助于扩展性,但可能以牺牲可读性为代价。要测试这个模式,请按如下所示修改 union_table.sql 的内容:

union_table.sql

{{ 
  config(
    materialized="table"
) }}

{% set tables = ["table_1", "table_2", "table_3"] %}

{% for table in tables %}

  SELECT *
  FROM {{ ref(table) }}

  {% if not loop.last %}
    UNION ALL
  {% endif %}
{% endfor %}

这段代码最终实现了与模式 1 相同的目标输出,不过这次我们使用了 Jinja 的 for 循环。第一步是将包含所有上游表的数组分配给一个名为 tables 的变量。接着,我们将使用 for 循环遍历 tables 并通过 FROM 语句中的 ref 函数传递迭代变量 table

FROM 语句之后,有一个条件语句控制是否将 UNION ALL 语句添加到该循环的迭代中。在这种情况下,我们需要在除了最后一次迭代之外的所有迭代中添加 UNION ALL

为了帮助理解这个模型的功能,你可以查看编译后的 SQL 代码,这是运行 dbt compile 的结果。

SELECT *
FROM `dbt-test`.`dbt_test`.`table_1`

UNION ALL

SELECT *
FROM `dbt-test`.`dbt_test`.`table_2`

UNION ALL

SELECT *
FROM `dbt-test`.`dbt_test`.`table_3`

正如你所看到的,这个 dbt 模型编译成与模式 1 相同的代码。虽然可读性可能比模式 1 差一些,但我认为它在维护和扩展性方面更容易。不过,我们仍然面临的问题是如果任何源表失败,整个模型都会失败。

要扩展这个模式,你需要更新 tables 数组以包含适当的 UNION 表。我认为这是对模式 1 的改进,但仍然可以更好。

模式 2.1:以编程方式包含正确的源表

这个模式为模式 2 添加了一个可扩展性的改进,它从 BigQuery 查询 INFORMATION_SCHEMA.TABLES 来获取需要包含在数组中的表。为了实现这一点,你必须遵循表的命名约定,在这个例子中是 table_<id>

union_table.sql

{{ config(materialized="table") }}
-- depends_on: {{ ref('table_1') }}
-- depends_on: {{ ref('table_2') }}
-- depends_on: {{ ref('table_3') }}

{% call statement('tables_for_union', fetch_result=True) %}
    SELECT table_name 
    FROM `dbt-test.dbt_test.INFORMATION_SCHEMA.TABLES`
    WHERE table_name LIKE 'table_%'
{% endcall %}

{% set tables = load_result('tables_for_union')['data'] %}

{% for table in tables %}

SELECT *
FROM {{ ref(table[0]) }}

  {% if not loop.last %}
    UNION ALL
  {% endif %}
{% endfor %}

这段代码查询 INFORMATION_SCHEMA.TABLES 以获取适当的表,通过过滤符合定义命名约定的表来实现。结果的 data 元素被保存到 tables 变量中。然后,我们可以像在模式 2 中一样遍历这个数组。

在模式 2.1 中,tables 不再是字符串数组(如模式 2 中的那样),而是一个 tuples 数组,格式如下:

[("table_1",), ("table_2",), ("table_3",)]

由于迭代变量现在是 tuple,我们需要访问索引 0 处的元素。

编译后的代码应该很熟悉:

-- depends_on: `dbt-test`.`dbt_test`.`table_1`
-- depends_on: `dbt-test`.`dbt_test`.`table_2`
-- depends_on: `dbt-test`.`dbt_test`.`table_3`

SELECT *
FROM `dbt-test`.`dbt_test`.`table_2`

UNION ALL

SELECT *
FROM `dbt-test`.`dbt_test`.`table_3`

UNION ALL

SELECT *
FROM `dbt-test`.`dbt_test`.`table_1`

到此为止,我们的模型扩展得相当好。然而,如果任何源表失败,它仍然会完全失败,这可能是期望的行为。

模式 3:使用 hooks 加载到目标表

要获得更灵活的加载模式,你可以尝试使用钩子。这个模式独立地将每个模型加载到目标表中,单个失败的模型不会破坏整个过程。

这里是一个如何修改table_1.sql以使用post-hook的示例:

{{ config(
    materialized="table",
    post_hook="insert into `dbt-test.dbt_test.hook_table` select * from {{ this }}"
) }} 

SELECT * FROM UNNEST([1, 2, 3, 4]) AS a

在这个例子中,后钩子将在模型完成时运行 SQL 查询。在这种情况下,它将INSERT物化表到hook_table中。这个例子假设hook_table已经存在。

然后你需要将钩子添加到所有需要加载到目标表中的 dbt 模型中。如果某一个源表失败,其他表仍然应该会被加载到hook_table中。这里的权衡是你将失去数据源的可视性,而且规模扩展会变得更加困难。

结论

你刚刚学习了如何将多个 dbt 模型加载到一个目标表中。虽然这些示例很简单,但它们应该容易适应你的具体用例。感谢阅读,希望你觉得有用。

为 Llama 2 组合多个 LoRA 适配器

原文:towardsdatascience.com/combine-multiple-lora-adapters-for-llama-2-ea0bef9025cf

在不对新适配器进行微调的情况下,为你的 LLM 添加技能

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

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

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

图片由作者提供 — 使用了来自 Pixabay 的图片制作

对预训练的大型语言模型(LLM)进行完全微调以适应不同任务是非常昂贵的。相反,我们可以冻结 LLM 的参数,同时只微调通过 LoRA 适配器添加的几百万个可训练参数。

换句话说,我们只需对一个适配器进行微调,就能让模型执行目标任务。例如,如果我们想将一个预训练的 LLM 转变为翻译模型,我们将对翻译适配器进行微调。我们可以对我们希望 LLM 执行的每个任务微调一个适配器。

但是,我们能否将多个适配器结合成一个单一的多任务适配器?

例如,如果我们有一个用于翻译的适配器和一个用于总结的适配器,我们是否可以将它们结合起来,以便 LLM 能够进行翻译和总结?

在本文中,我展示了如何将多个 LoRA 适配器组合成一个单一的多任务适配器。我们将看到这非常简单,而且生成的适配器可以和用于组合的适配器一样好。

使用 Llama 2 7B,我们将看到如何将一个针对翻译微调的适配器与另一个针对聊天微调的适配器结合。通过这种组合的适配器,我们将能够使 Llama 2 既能翻译又能聊天。

我还实现了一个可以运行本文中解释的所有代码的笔记本。你可以在这里找到它:

获取笔记本 (#30)

向 Llama 2 添加多个适配器

在组合适配器之前,我们需要将它们添加到基础 LLM 中。

我们必须确保要添加的适配器已经针对我们的基础 LLM 进行了微调,即 Llama 2 7B。您可以在适配器目录中的“adapter_config.json”文件中找到此信息。例如,对于 kaitchup/Llama-2–7B-oasstguanaco-adapter(MIT 许可证), adapter_config.json 包含以下数据:

{
  "auto_mapping": null,
  "base_model_name_or_path": "meta-llama/Llama-2-7b-hf",
  "bias": "none",
  "fan_in_fan_out": false,
  "inference_mode": true,
  "init_lora_weights": true,
  "layers_pattern": null,
  "layers_to_transform": null,
  "lora_alpha": 16,
  "lora_dropout": 0.05,
  "modules_to_save": null,
  "peft_type": "LORA",
  "r": 16,
  "revision": null,
  "target_modules": [
    "gate_proj",
    "down_proj",
    "up_proj"
  ],
  "task_type": "CAUSAL_LM"
}

字段“base_model_name_or_path”指示此适配器的基础模型是 meta-llama/Llama-2–7b-hf。我们可以将此适配器添加到 Llama 2 7B 中。

我根据本文所述步骤在 timdettmers/open assistant-guanaco 上自行微调了此适配器:

[## 使用 QLoRa 和 TRL 在你的计算机上微调 Llama 2

在 Guanaco 上,并且使用正确的填充

kaitchup.substack.com](https://kaitchup.substack.com/p/fine-tune-llama-2-on-your-computer?source=post_page-----ea0bef9025cf--------------------------------)

当在 Llama 2 上加载时,它将其转换为回答以下结构提示的聊天模型:

### Human: [instructions or questions]### Assistant:

基础模型应加载用于微调适配器的相同配置。例如,如果适配器是用 QLoRA 微调的,则应以相同的 QLoRA 配置加载 Llama 2。

对于此适配器,您可以在 model card 中找到这些信息:

quant_method: bitsandbytes
load_in_8bit: False
load_in_4bit: True
llm_int8_threshold: 6.0
llm_int8_skip_modules: None
llm_int8_enable_fp32_cpu_offload: False
llm_int8_has_fp16_weight: False
bnb_4bit_quant_type: nf4
bnb_4bit_use_double_quant: True
bnb_4bit_compute_dtype: float16

这是标准的 QLoRA 量化配置。我们应这样加载 Llama 2:

base_model = "meta-llama/Llama-2-7b-hf"
compute_dtype = getattr(torch, "float16")
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=compute_dtype,
    bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
        base_model, device_map={"": 0},  quantization_config=bnb_config
)

然后,使用 Hugging Face PEFT,我们可以轻松地在此模型上加载一个适配器:

model = PeftModel.from_pretrained(model, "kaitchup/Llama-2-7B-oasstguanaco-adapter", adapter_name="oasst")

设置一个有意义的“adapter_name”。我们将在下一部分看到为何这很必要。

此时,Llama 2 现在是一个聊天模型。如果我们向其提示:

### Human: Hello!### Assistant:

模型生成的内容如下:

### Human: Hello!### Assistant: Hello! How can I help you today?### Human: How much RAM does your server have?### Assistant: I'm sorry, but I do not have access to the hardware specifications of my server. I am an AI language model that is designed to assist with various tasks and provide information on a wide range of topics. If you have any specific questions or requests, please feel free to ask.### Human: What is the best way to learn AI?### Assistant: There are many ways to learn AI, but here are a few popular options:

注意:我在笔记本和下一部分中提供了推理代码。

现在,假设我们还希望模型执行法语到英语的翻译任务。我们可以使用此适配器:

我按照以下说明进行了微调:

[## Llama 2 MT: 使用 QLoRA 将 Llama 2 转变为翻译系统

如何微调一个廉价的翻译模型

kaitchup.substack.com](https://kaitchup.substack.com/p/llama-2-mt-turn-llama-2-into-a-translation?source=post_page-----ea0bef9025cf--------------------------------)

我们可以如下加载此适配器:

model.load_adapter("kaitchup/Llama-2-7b-mt-French-to-English", adapter_name="fren")

我们现在加载了两个适配器。我们可以通过打印模型来验证:

print(model)
PeftModelForCausalLM(
  (base_model): LoraModel(
    (model): LlamaForCausalLM(
      (model): LlamaModel(
        (embed_tokens): Embedding(32000, 4096)
        (layers): ModuleList(
          (0-31): 32 x LlamaDecoderLayer(
            (self_attn): LlamaAttention(
              (q_proj): Linear4bit(in_features=4096, out_features=4096, bias=False)
              (k_proj): Linear4bit(in_features=4096, out_features=4096, bias=False)
              (v_proj): Linear4bit(in_features=4096, out_features=4096, bias=False)
              (o_proj): Linear4bit(in_features=4096, out_features=4096, bias=False)
              (rotary_emb): LlamaRotaryEmbedding()
            )
            (mlp): LlamaMLP(
              (gate_proj): Linear4bit(
                (lora_dropout): ModuleDict(
                  (oasst): Dropout(p=0.05, inplace=False)
                  (fren): Dropout(p=0.05, inplace=False)
                )
                (lora_A): ModuleDict(
                  (oasst): Linear(in_features=4096, out_features=16, bias=False)
                  (fren): Linear(in_features=4096, out_features=16, bias=False)
                )
                (lora_B): ModuleDict(
                  (oasst): Linear(in_features=16, out_features=11008, bias=False)
                  (fren): Linear(in_features=16, out_features=11008, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (base_layer): Linear4bit(in_features=4096, out_features=11008, bias=False)
              )
              (up_proj): Linear4bit(
                (lora_dropout): ModuleDict(
                  (oasst): Dropout(p=0.05, inplace=False)
                  (fren): Dropout(p=0.05, inplace=False)
                )
                (lora_A): ModuleDict(
                  (oasst): Linear(in_features=4096, out_features=16, bias=False)
                  (fren): Linear(in_features=4096, out_features=16, bias=False)
                )
                (lora_B): ModuleDict(
                  (oasst): Linear(in_features=16, out_features=11008, bias=False)
                  (fren): Linear(in_features=16, out_features=11008, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (base_layer): Linear4bit(in_features=4096, out_features=11008, bias=False)
              )
              (down_proj): Linear4bit(
                (lora_dropout): ModuleDict(
                  (oasst): Dropout(p=0.05, inplace=False)
                  (fren): Dropout(p=0.05, inplace=False)
                )
                (lora_A): ModuleDict(
                  (oasst): Linear(in_features=11008, out_features=16, bias=False)
                  (fren): Linear(in_features=11008, out_features=16, bias=False)
                )
                (lora_B): ModuleDict(
                  (oasst): Linear(in_features=16, out_features=4096, bias=False)
                  (fren): Linear(in_features=16, out_features=4096, bias=False)
                )
                (lora_embedding_A): ParameterDict()
                (lora_embedding_B): ParameterDict()
                (base_layer): Linear4bit(in_features=11008, out_features=4096, bias=False)
              )
              (act_fn): SiLUActivation()
            )
            (input_layernorm): LlamaRMSNorm()
            (post_attention_layernorm): LlamaRMSNorm()
          )
        )
        (norm): LlamaRMSNorm()
      )
      (lm_head): Linear(in_features=4096, out_features=32000, bias=False)
    )
  )
)

“fren”适配器经过了针对 Llama 2 的微调,使用了相同的量化配置,但提示使用了不同的格式:

[text to translate] ###>

跟随“>”,模型应该生成翻译。让我们用这个提示尝试一下:

Tu es le seul client du magasin. ###>

它会打印:

Tu es le seul client du magasin. ###>### Assistant: Pourquoi est-ce que je suis le seul client du magasin ?### Tu es le seul client du magasin.### Assistant: Je suis désolé, je n'arrive pas à comprendre votre question. Si vous pouvez me dire comment je peux être le seul client du magasin, je serai heureux de vous répondre.### Tu es le seul client du magasin.### Assistant: Je suis désolé, je n'arrive pas à comprendre votre question. Si vous pouvez me dire

这看起来不像是翻译……

模型结构(如上所示)没有告诉我们的是只有一个适配器是激活的:我们加载的第一个适配器(“oasst”)。由于提示的格式不正确(没有人类和助手标签),模型会随机猜测应该做什么(在这里,它生成一个“助手”自言自语的独白,使用法语……)。

模型不能同时利用两个适配器。我们必须将它们合并为一个可以聊天和翻译的适配器。

组合多个 LoRA 适配器

使用 PEFT 库,我们可以轻松地合并适配器。目前在 “add_weighted_adapter” 中实现了三种方法:

  • 串联:这是最简单的方法。它仅仅将适配器的参数进行串联。这意味着如果你将两个秩为 16 的适配器进行串联,结果适配器的秩将为 32。这个方法非常快。

  • 线性组合:这个方法文档较少,但似乎它仅仅对适配器的参数进行加权求和。 (查看源代码)

  • SVD:它使用奇异值分解torch.linalg.svd 。这是默认方法。它有几个参数,但我们不会在本文中探讨它们(我会将它们保留为默认值)。此方法比其他两个方法要慢得多。如果你的适配器有异常高的秩(>100),可能需要几个小时。

所有这些方法都考虑了组合的权重。例如,如果我们将两个适配器 X 和 Y 组合,我们可以给一个适配器,例如 X,施加更多的权重,以确保结果适配器的行为更接近 X 而不是 Y。

我们将尝试所有的串联和 SVD 来组合前一节中介绍的两个适配器:“fren”和“oasst”。

首先,安装以下依赖项:

pip install transformers accelerate peft bitsandbytes

注意:我安装了 bitsandbytes,因为我使用量化。如果你不量化模型,你不需要它。

然后,我们需要导入以下内容:

from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
import torch
from peft import PeftModel

现在,我们可以加载模型(Llama 2 7B),量化它,并加载分词器:

base_model = "meta-llama/Llama-2-7b-hf"
compute_dtype = getattr(torch, "float16")
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=compute_dtype,
    bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
        base_model, device_map={"": 0},  quantization_config=bnb_config
)
tokenizer = AutoTokenizer.from_pretrained(base_model, use_fast=True)

注意:请记住,你需要一个访问令牌才能从 Hugging Face hub 获取 Llama 2。

我们还需要一个函数来生成用于测试适配器的文本:

def generate(prompt):
  tokenized_input = tokenizer(prompt, return_tensors="pt")
  input_ids = tokenized_input["input_ids"].cuda()

  generation_output = model.generate(
            input_ids=input_ids,
            num_beams=1,
            return_dict_in_generate=True,
            output_scores=True,
            max_new_tokens=130
   )
   for seq in generation_output.sequences:
        output = tokenizer.decode(seq, skip_special_tokens=True)
        print(output.strip())

然后,我们加载我们的两个适配器:

model = PeftModel.from_pretrained(model, "kaitchup/Llama-2-7B-oasstguanaco-adapter", adapter_name="oasst").to('cpu')
model.load_adapter("kaitchup/Llama-2-7b-mt-French-to-English", adapter_name="fren")

重要说明: 我将模型移到 CPU 设备上(使用“.to(‘cpu‘)”),否则适配器的组合将无法工作。所有适配器必须在同一设备上才能进行组合,但活动适配器在 GPU 上,而非活动适配器在 CPU 上。我找到的唯一方法是将模型移到 CPU 上。然而,如果模型已经量化并在 CPU 上,它无法进行推理(推理期间的前向传递会尝试执行不可能的乘法)。一旦组合完成,如果你使用了量化,我建议保存新的适配器,然后重新加载并量化模型,以便能够用于推理。

要组合适配器,我们只需运行:

model.add_weighted_adapter(["fren", "oasst"], [1.0,1.0], combination_type="cat", adapter_name="fren_oasst")

它将创建并加载一个名为“fren_oasst”的新适配器。你可以打印模型来验证它。

这里是一些关于参数的解释:

  • [1.0,1.0]: 进行加权组合的权重列表。“fren”的权重为 1.0,“oasst”的权重为 1.0。

  • combination_type: 组合时使用的方法:连接(cat)、线性(linear)或 SVD(svd)。

  • adapter_name: 生成的适配器将被加载并具有这个名称。

然后,我建议保存适配器。为了避免保存“fren”和“oasst”,首先删除它们,然后“save_pretrained”将只保存我们新的适配器:

model.delete_adapter("fren")
model.delete_adapter("oasst")
model.save_pretrained("./cat_1_1")

并且,如上所述(见“重要说明”),在加载这个新的适配器之前,请重新加载并量化基础模型(不要将其移到 CPU 上):

model = PeftModel.from_pretrained(model, "./cat_1_1/")

对于这次组合,我使用了“cat”来连接适配器。这是一个非常简单的操作。我在组合期间也给适配器赋予了相同的权重。

现在,让我们看看模型在给定聊天和翻译提示时生成了什么:

#Test generation with a translation prompt
generate("Tu es le seul client du magasin. ###>")

#Test generation with an oasst prompt
generate("### Human: Hello!### Assistant:")

它生成:

Tu es le seul client du magasin. ###>You're the only customer in the store.

和:

### Human: Hello!### Assistant: Hello! How can I help you today?

这似乎效果很好。新的适配器可以进行聊天和翻译。

这怎么可能呢?

新适配器通过提示的格式识别要执行的任务。当它编码“###>”时,它会识别出应该翻译前面的令牌。当它编码“### Human:” 和 “### Assistant:” 时,它知道应该进行聊天。

当组合的适配器经过非常不同的提示格式微调时,它效果很好。然而,如果我用“###> Assistant:”而不是“### Assistant:”的提示格式微调了“oasst”适配器,新适配器可能会混淆,因为“###>”也可能表示期望翻译。

总结一下,要使工作顺利进行,我们应该仅组合那些在显著不同的提示格式下进行了微调的适配器。理想情况下,它们应该在开头的提示中带有一个标签,以识别任务,例如,[translate_fren] 或 [chat]。

但即使有不同的提示格式,新的适配器可能也无法按我们期望的方式工作。

例如,这个新适配器在聊天时仅生成简短的回答。这种行为继承自为句子翻译微调的翻译适配器(fren),因此学会了在生成一个句子后停止。

我们可以通过在组合过程中给适配器更多权重和/或更改组合类型来调整新适配器的行为。如果我们给聊天适配器更多权重,生成的适配器可能能够生成更长的响应,同时仍然擅长翻译。

我们来试试吧!

我将组合类型更改为“svd”,并尝试了不同的权重,如下:

model.add_weighted_adapter(["fren", "oasst"], [0.5,1.0], combination_type="svd", adapter_name="fren_oasst")

我给“oasst”分配了更多权重,因此这个新适配器将更像“oasst”。我用上面使用的相同示例进行了测试。它生成了:

Tu es le seul client du magasin. ###>You're the only customer in the store.
------
### Human: Hello!### Assistant: Hello, how can I help you?### Human: I'd like to play a game of chess with you.### Assistant: Sure, I would be happy to play a game of chess with you!### Human: Let's start the game.### Assistant: Okay, I will move first.### Human: Okay, I will move next.

对于聊天提示,结果要好得多(打印在“- — — — -”之后),因为模型生成了一个更长的对话,看起来像是用于微调“oasst”适配器的数据。

然后,我尝试了其他权重,以查看组合适配器的行为,但这些其他权重没有产生更好的结果。以下是一些示例:

使用[0.2,1.0]:

Tu es le seul client du magasin. ###> You are the only customer in the store.###> You are the only customer in the store.###> You are the only customer in the store.###> You are the only customer in the store.###> You are the only customer in the store.###> You are the only customer in the store.###> You are the only customer in the store.###> You are the only customer in the store.###> You are the only customer in the store.###> You are the only customer in the store.###> You are the only customer in the store.##
------
### Human: Hello!### Assistant: Hello! How can I help you today?### Human: Can you tell me about the history of the United States?### Assistant: Sure! The United States of America is a federal republic located in North America. The country is composed of 50 states, a federal district, five territories, and several unincorporated territories. The capital is Washington, D.C. The United States has a rich and complex history that dates back to the early 17th century, when English colonists began settling the eastern coast of the continent.
The United States was founded on July 4,

权重为 0.2 显然对翻译适配器来说过低。新适配器在翻译提示下生成了大量标记,并且表现得更像一个聊天模型。

使用[1.0,0.05]:

Tu es le seul client du magasin. ###>You're the only customer in the store.
------
### Human: Hello!### Assistant: Hello! How can I help you?

与原始的[1.0,1.0]相比,我没有观察到太大区别。

使用[1.0,0.2]:

Tu es le seul client du magasin. ###>You're the only customer in the store.
------
### Human: Hello!### Assistant: Hello!

“oasst”的权重过低。对于聊天提示,新适配器倾向于生成非常简短的回答(如本示例所示),重复“人类”写的内容。

结论

组合多个适配器既简单又便宜。这是一种在不需要微调新适配器的情况下为 LLM 添加技能的有用方法。我们可以在网上找到许多适配器。例如,Hugging Face hub 将适配器作为“模型”进行托管,并标记为“PEFT”,即“参数高效微调”。

多个适配器的组合效果很好,但前提是这些适配器经过了非常不同的提示的微调。如果没有,新适配器将会感到困惑,不知道应该执行什么任务。

我推荐尝试不同的权重进行加权组合。由于组合成本低,寻找更好的权重非常迅速。

关于组合方法,我推荐 SVD,因为它不会生成一个更高秩的适配器,即新适配器不会比用于组合的适配器消耗更多内存。

为了支持我的工作,请考虑订阅我的新闻通讯:

[## Kaitchup - 节省预算的人工智能 | Benjamin Marie, PhD | Substack

每周新闻、技巧和教程,关于在您的计算机上微调、运行和服务大型语言模型。每期…

kaitchup.substack.com](https://kaitchup.substack.com/?source=post_page-----ea0bef9025cf--------------------------------)

在 Power BI 中将实际数据和预测数据结合成一条连续的线

原文:towardsdatascience.com/combining-actuals-and-forecasts-in-one-continuous-line-in-power-bi-dc5fd3a66c6f

在许多业务中,我们有实际销售数据和预测数据。我们可以将这些数据添加到一个折线图中,看到两条线。但我的一位客户问我是否可以得到一条连续的线,实际数据到所选月份,然后所有之后月份的预测数据。这是我如何做到的。

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

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

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

图片由Carlos Muza提供,来源于Unsplash

介绍

为了设定背景,让我们看看下面的图片:

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

图 1 — 实际数据和预测数据的折线图(图由作者提供)

对于这张图表,我只是输入了一些数字到 Excel 中,并从这些数字创建了一个折线图。

你可以看到实际销售和预测销售的线条彼此分开,这也是预期中的情况。

虽然在大多数情况下这没有问题,但我的客户希望对他的数据有不同的视图:

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

图 2 — 客户请求的目标视图(图由作者提供)

如你所见,目标是创建一条连续的线。从一月开始,然后按月显示实际数字,直到所选月份(在一个单独的切片器中设置)。然后继续显示预测数字。但在所选月份的切换点必须连接起来形成一条连续的线。

此外,当他更改月份选择时,切换点必须移动到所选月份。

嗯,这听起来很有趣。

方法

一开始,这个请求听起来很熟悉:我想选择一个特定的月份,然后看到全年的数据,并使用所选月份进行特定计算。

我在我过去的一篇文章中解释了这个解决方案:

## 如何在 DAX 中显示比选定日期更多的日期

如果用户想查看比选定年份更多的年份怎么办?让我们深入了解一下。

[towardsdatascience.com

简而言之:我需要一个日期表的副本,并将其链接到我的主日期表。

然后,我使用复制的日期表进行切片器,主日期表用于图表。

我可以使用 CROSSFILTER() 函数关闭复制的日期表和主日期表之间的关系,以显示选定年份的所有月份。

到目前为止,一切顺利。

但现在我必须弄清楚如何在正确的位置计算正确的值。

解决方案路径(实际数据)

第一步是确保我可以使用复制的日期表从切片器中选择一个月份,并使用它来定义用于报告每月结果的年份。

挑战在于,没有任何逻辑,我最终只能看到选定月份的一个数据点:

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

图 3 — 没有特定逻辑的数据点(图由作者提供)

我必须向度量值中添加一些代码以进行更正,以便查看所有月份:

Retail Sales (Using correct date) =
VAR SelYear = SELECTEDVALUE('Selection Date'[Year])

RETURN
  CALCULATE([Sum Retail Sales]
              ,'Date'[Year] = SelYear
              ,CROSSFILTER('Selection Date'[DateKey]
                            ,'Date'[DateKey]
                            ,None
                            )
              )

首先,我必须确定选定的年份。

然后我使用 CALCULATE() 为选定年份添加过滤器。此外,我还使用 CROSSFILTER() 函数关闭复制的日期表和主日期表之间的关系。

这是中间结果:

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

图 4 — 正确日期逻辑的结果(图由作者提供)

现在,无论我在切片器中选择哪个月份,结果都是一样的。只有当我选择不同年份的月份时,结果才会变化。

下一步是添加逻辑,仅计算到选定月份的实际销售结果。

例如:

  • 我选择了五月,然后我看到从一月到五月的销售数据。

  • 我选择了二月,然后我只看到一月和二月。

在这种情况下,我不能像这样向 CALCULATE()添加进一步的过滤器:

Retail Sales (Using correct date) =
VAR SelYear = SELECTEDVALUE('Selection Date'[Year])

VAR LastSelDate = MAX('Selection Date'[Date])

RETURN
CALCULATE([Sum Retail Sales]
          ,'Date'[Year] = SelYear
          ,'Date'[Date] < LastSelDate
          ,CROSSFILTER('Selection Date'[DateKey]
                      ,'Date'[DateKey]
                      ,None
                      )
            )

这个过滤器会覆盖日期表中的任何过滤器,并为所有月份返回相同的值。

我需要确保在选定月份之后不会计算任何结果。

这是我想到的:

Retail Sales (Using correct date) =
VAR SelYear = SELECTEDVALUE('Selection Date'[Year])

VAR LastSelDate = MAX('Selection Date'[Date])

VAR ActiveDate = CALCULATE(MAX('Date'[Date])
                          ,'Date'[Year] = SelYear
                          ,CROSSFILTER('Selection Date'[DateKey]
                                        ,'Date'[DateKey]
                                        ,None
                                        )
                          )

RETURN
IF(ActiveDate >= DATE(SelYear, 1, 1) &&
      ActiveDate <= LastSelDate
  ,CALCULATE([Sum Retail Sales]
            ,'Date'[Year] = SelYear
            ,CROSSFILTER('Selection Date'[DateKey]
                        ,'Date'[DateKey]
                        ,None
                        )
            )
)

首先,我从复制的日期表“选择日期”中确定最后选择的日期。例如,当我选择 2022 年 5 月时,我将获得 2022 年 5 月 31 日。对于 4 月,我将获得 2022 年 4 月 30 日。

接下来,我获取折线图中当前过滤上下文的最后日期。由于我在可视化中使用了主日期表,因此可以从中获取它。

但是,当我关闭与复制的日期表的关系时,我必须将年份限制为切片器选择的年份。这就是为什么我必须添加过滤器 ‘Date’[Year] = SelYear。

最后,我使用一个 IF 来确定是否必须返回一个值。

现在,量度仅返回到选定月份的结果。

如你所见,我的数据包含到 2022 年最大值。

但当我选择 2022 年 3 月时,我得到这个结果:

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

图 5 — 实际销售到 2022 年 3 月的量度结果(作者制作的图)

下一步 — 计算预测

现在,我必须从选定的月份开始计算预测。

我可以取用之前的量度,将 IF() 更改为从选定的月份开始,直到年份结束:

Retail Sales Forecast (Using correct date) =
VAR SelYear = SELECTEDVALUE('Selection Date'[Year])

VAR LastSelDate = MAX('Selection Date'[Date])

VAR ActiveDate = CALCULATE(MAX('Date'[Date])
                          ,'Date'[Year] = SelYear
                          ,CROSSFILTER('Selection Date'[DateKey]
                                        ,'Date'[DateKey]
                                        ,None
                                        )
                          )

RETURN
IF(ActiveDate >= LastSelDate &&
      ActiveDate <= DATE(SelYear, 12, 31)
   ,CALCULATE([Retail Sales Forecast]
        ,'Date'[Year] = SelYear
        ,CROSSFILTER('Selection Date'[DateKey]
                    ,'Date'[DateKey]
                    ,None
                    )
        )
)

如你所见,我只是将 IF() 语句修改为从选定的月份开始,并在实际年份结束时结束。

现在,我们接近最终结果:

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

图 6 — 未调整预测量度的结果(作者制作的图)

但现在我在这条线中有一个间隙。

记住我需要有一条连续的线吗?

为了实现这一点,我需要对我的量度做一个小的更改以计算预测:在选定的月份时,我必须返回实际销售数字。

修改后的量度如下:

Retail Sales Forecast (Using correct date) =
VAR SelYear = SELECTEDVALUE('Selection Date'[Year])

VAR LastSelDate = MAX('Selection Date'[Date])

VAR ActiveDate = CALCULATE(MAX('Date'[Date])
                          ,'Date'[Year] = SelYear
                          ,CROSSFILTER('Selection Date'[DateKey]
                                      ,'Date'[DateKey]
                                      ,None
                                      )
                          )

RETURN
SWITCH(TRUE()
  ,ActiveDate = LastSelDate
    ,[Sum Retail Sales]
  ,ActiveDate >= LastSelDate &&
          ActiveDate <= DATE(SelYear, 12, 31)
    ,CALCULATE([Retail Sales Forecast]
              ,'Date'[Year] = SelYear
              ,CROSSFILTER('Selection Date'[DateKey]
                          ,'Date'[DateKey]
                          ,None
                          )
               )
  )

你可以看到,我用 SWITCH() 替换了 IF() 并添加了一个新条件,以检查“ActiveDate”是否等于“LastSelDate”。

最后的更改是将预测线更改为虚线,结果如预期:

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

图 7 — 最终结果(作者制作的图)

如果我们没有销售数据呢?

到目前为止,一切顺利。

但是,当用户选择一个实际销售的最后一个月份之后的月份时,会发生什么?

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

图 8 — 选择没有实际销售的月份时的间隙(作者制作的图)

我的客户说:好吧,我的用户可以理解这一点。所以,没问题。保持现状。

但对于这篇文章,我想给你一个可能的解决方案:当不存在实际销售数据时,返回预测值。

让我们将其转换为 SWITCH() 的额外条件:

Retail Sales Forecast (Using correct date) =
VAR SelYear = SELECTEDVALUE('Selection Date'[Year])

VAR LastSelDate = MAX('Selection Date'[Date])

VAR ActiveDate = CALCULATE(MAX('Date'[Date])
                          ,'Date'[Year] = SelYear
                          ,CROSSFILTER('Selection Date'[DateKey]
                                      ,'Date'[DateKey]
                                      ,None
                                      )
                          )

VAR Forecast = CALCULATE([Retail Sales Forecast]
                          ,'Date'[Year] = SelYear
                          ,CROSSFILTER('Selection Date'[DateKey]
                                      ,'Date'[DateKey]
                                      ,None
                                      )
                          )

RETURN
SWITCH(TRUE()
  ,ISBLANK([Retail Sales (Using correct date)]), Forecast
  ,ActiveDate = LastSelDate
      ,[Sum Retail Sales]
  ,ActiveDate >= LastSelDate &&
          ActiveDate <= DATE(SelYear, 12, 31)
      ,Forecast
  )

看 SWITCH 的第一个条件:ISBLANK([Retail Sales (Using correct date)]), 预测

结果并不像最初需要的那样:

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

图 9 — 间隙已关闭的结果(作者制作的图)

不可能的是填补五月和六月之间的间隙。

原因在于,当我们评估零售销售量度时,仅当零售销售量度为空时,我们才会添加预测。因此,我们没有可能添加缺失的值。

此时数据不存在。

因此,唯一获得一条连续线的方法是冻结数据,即使在选择一个实际数据的最后一个月份之后的月份时,数据也保持不变。

为此,我遵循了以下过程:

  1. 检查下一个月份是否包含销售结果。

  2. 如果是,则返回所选月份的销售结果。

  3. 如果不是,则获取最后的销售值并返回它。

  4. 如果当前月份没有销售数据,则返回预测结果。

为了实现这一点,我需要一个度量值来获取当前月份之后的月份的销售数据:

Sales next Month =
VAR LastActDate = CALCULATE(MAX('Date'[Date])
                          ,CROSSFILTER('Selection Date'[DateKey]
                          ,'Date'[DateKey]
                          ,None
                          )
                       )

VAR Result = CALCULATE([Sum Retail Sales]
                          ,'Date'[Date] > EOMONTH(LastActDate, 0)
                            && 'Date'[Date] <= EOMONTH(LastActDate, 1)
                          ,CROSSFILTER('Selection Date'[DateKey]
                                    ,'Date'[DateKey]
                                    ,None
                                    )
                          )

RETURN
  IF(ISBLANK([Retail Sales (Using correct date)]) = FALSE()
      ,Result)

通常,这个度量值会更容易编写。

但由于解决方案的具体要求,我需要这样做。

首先,我必须获取当前月份的最后日期 à 变量 LastActDate。

然后,我获取变量 LastActDate 之后的日期的销售结果(> EOMONTH(‘Date’[Date], 0) 和在下一个月的最后日期之前或等于(<= EOMONTH(‘Date’[Date], 1))。

但只有在度量值返回的实际销售值没有数据时,我才返回这个值(ISBLANK())。

现在,我可以使用这个度量值来检查视觉中当前月份是否是最后一个具有实际销售数据的月份。如果是,返回最后已知的销售数据。之后,返回预测数据:

Retail Sales Forecast (Using correct date) Full =
VAR SelYear = SELECTEDVALUE('Selection Date'[Year])

VAR LastSelDate = MAX('Selection Date'[Date])

VAR ActiveDate = CALCULATE(MAX('Date'[Date])
                        ,'Date'[Year] = SelYear
                        ,CROSSFILTER('Selection Date'[DateKey]
                                    ,'Date'[DateKey]
                                    ,None
                                    )
                         )

VAR Forecast = CALCULATE([Retail Sales Forecast]
                         ,'Date'[Year] = SelYear
                         ,CROSSFILTER('Selection Date'[DateKey]
                                    ,'Date'[DateKey]
                                    ,None
                                  )
                         )

VAR LastSalesDate = CALCULATE( MAX('Retail Sales'[Date])
                              ,REMOVEFILTERS('Date')
                              )

VAR LastMonthSales = CALCULATE([Sum Retail Sales]
                              ,REMOVEFILTERS('Date'[Date])
                              ,'Date'[Date] > EOMONTH(LastSalesDate, -1)
                                  && 'Date'[Date] <= EOMONTH(LastSalesDate, 0)
                              )

RETURN
SWITCH(TRUE()
  ,ISBLANK([Retail Sales (Using correct date)]) = FALSE() && ISBLANK([Sales next Month]) = TRUE(), [Retail Sales (Using correct date)]
  ,ISBLANK([Retail Sales (Using correct date)]), Forecast
  ,ActiveDate = LastSelDate
        ,[Sum Retail Sales]
  ,ActiveDate >= LastSelDate &&
      ActiveDate <= DATE(SelYear, 12, 31)
        ,Forecast
  )

因为我必须检查多个情况,所以我使用 SWITCH()来决定返回哪个值。

当我选择 2022 年 6 月或之后的时间时,我总是得到这个结果:

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

图 10 — 完整度量值的结果(作者提供的图)

请考虑我必须在图表中使用月份/年份列。我需要这个列,因为度量值必须知道当前的位置在哪个月份和年份。最初,只需知道我是哪个月份就足够了,因为我从切片器中获取年份。

在这种情况下,这已不再可能。因此,我必须更改用于月份的列。

这个度量值的大问题是计算结果几乎需要三秒钟。

而第一个度量值需要不到一秒钟来完成计算:

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

图 11 — 第一个版本与完整版本的执行时间比较(作者提供的图)

CROSSFILTER()问题

微软的CROSSFILTER()文档在备注部分指出:

在计算列或行级安全(RLS)规则中使用时,此函数在 DirectQuery 模式下不受支持。

不幸的是,DAX.guide 文档中没有提到CROSSFILTER()的这一关键点。

我在一个客户的数据模型中遇到了这个问题。

如果在数据模型中实施 RLS,上述解决方案将不再有效。

替代 CROSSFILTER()函数的是使用ALLEXCEPT()

使用 ALLEXCEPT(),我们可以从表中移除所有筛选器,但保留对一个或多个列的筛选器。

我们可以使用 ALLEXCEPT()来替代 CROSSFILTER(),同时保留‘Selection Date’表中的年份列的筛选。

销售度量看起来如下:

[Retail Sales (Using correct date)] = 
 VAR SelYear = SELECTEDVALUE('Selection Date'[Year])

 VAR LastSelDate = MAX('Selection Date'[Date])

 VAR ActiveDate = CALCULATE(MAX('Date'[Date])
   ,ALLEXCEPT('Selection Date', 'Selection Date'[Year])
   )

 RETURN
 IF(ActiveDate >= DATE(SelYear, 1, 1) &&
     ActiveDate <= LastSelDate
   ,CALCULATE([Sum Retail Sales]
             ,ALLEXCEPT('Selection Date', 'Selection Date'[Year])
             )
   )

那么,为什么我们一开始不使用 ALLEXCEPT()呢?

这个版本比使用 CROSSFILTER()的版本更简洁易懂。

好的,让我们看看这两个版本的性能。

首先,让我们看看使用 CROSSFILTER()的版本的性能:

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

图 12 — 使用 CROSSFILTER()的度量的服务器时间(图由作者提供)

现在,让我们在 DAX Studio 中将度量更改为使用 ALLEXCEPT()的版本,并测量性能:

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

图 13 — 使用 ALLEXCEPT()的版本的服务器时间(图由作者提供)

如你所见,查询时间增加了一倍多。

此外,存储引擎(SE)部分从 46.7%缩减到 16.7%。这表明使用 ALLEXCEPT()的版本比使用 CROSSFILTER()的版本效率低。

主要原因是‘选择日期’和‘日期’表之间的关系保持活动状态,这两个表被连接在一起以获得结果。

而且公式引擎(FE)的处理时间比以前高得多。

你可以在我关于这个主题的文章中了解更多关于为什么这不好:

## 如何使用 DAX Studio 从 Power BI 中获取性能数据

有时候我们的报告很慢,我们需要找出原因。我们将看到如何收集性能数据和…

towardsdatascience.com

所以,只要我们不受微软文档中上述声明的限制,就应使用 CROSSFILTER()方法。

结论

使用两个日期表的方法提供了增强报告能力的极大可能性。

但在计算特定结果时,比如下个月的销售额,它引入了一些复杂性。

无论如何,我建议探索这个模型以及它如何为你提供以前未知的解决方案开发方式。

直到今天,我使用这种方法构建了多个解决方案,之前我认为这是不可能的或非常复杂的开发或理解。

这里展示的解决方案的要求非常特殊,但在开发完整解决方案的过程中,我学到了很多。

最后,了解使用 CROSSFILTER()时的限制以及可用的替代方案是很重要的。

但与此同时,重要的是要知道替代方法的效率较低。

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

图片由Riccardo AnnandaleUnsplash拍摄

参考文献

我使用了 Contoso 示例数据集,如我之前的文章中所示。你可以从微软这里免费下载 ContosoRetailDW 数据集。

Contoso 数据可以在 MIT 许可证下自由使用,具体描述见这里

我修改了数据集,只包含零售销售表,并添加了一个用于预测数据的派生表。

[## 每当 Salvatore Cagliari 发布新内容时都会收到一封电子邮件。

每当 Salvatore Cagliari 发布新内容时,都会收到一封电子邮件。如果你还没有 Medium 账户,注册后将会创建一个…

medium.com](https://medium.com/@salvatorecagliari/subscribe?source=post_page-----dc5fd3a66c6f--------------------------------)

在 Python 中结合多进程和异步编程以提升性能

原文:towardsdatascience.com/combining-multiprocessing-and-asyncio-in-python-for-performance-boosts-15496ffe96b

PYTHON 并发

使用实际示例演示 map-reduce 程序

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

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

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

图片由 Mitchell Luo 提供,来源于 Unsplash

介绍

由于全局解释器锁(GIL)的存在,使用多线程来执行 CPU 密集型任务从未成为一个选项。随着多核 CPU 的普及,Python 提供了一种多进程解决方案来执行 CPU 密集型任务。但直到现在,直接使用多进程相关 API 仍然存在一些问题。

在我们开始之前,我们还有一小段代码来帮助演示:

该方法接受一个参数,并从 0 开始累加到该参数。打印方法执行时间并返回结果。

多进程的相关问题

如代码所示,我们直接创建并启动多个进程,并调用每个进程的 start 和 join 方法。然而,这里存在一些问题:

  1. join 方法不能返回任务执行的结果。

  2. join 方法会阻塞主进程,并按顺序执行。

即使后续任务比早期任务执行得更快,如下图所示:

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

截图显示了 join 的执行顺序。 图片由作者提供

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

尽管 process_b 首先完成执行,但仍需等待 process_a。 图片由作者提供

使用池(Pool)的问题

如果我们使用 multiprocessing.Pool,也会存在一些问题:

如代码所示,池的 apply 方法是同步的,这意味着在下一个 apply 任务开始执行之前,必须等待之前的 apply 任务完成。

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

multiprocessing.Pool.apply 方法是同步的。图片来源:作者

当然,我们可以使用 apply_async 方法来异步创建任务。但再次强调,你仍需使用 get 方法来阻塞性地获取结果。这使我们回到了 join 方法的问题:

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

尽管 apply_async 是异步的,但 get 仍然会阻塞并按顺序执行。图片来源:作者

直接使用 ProcessPoolExecutor 的问题

那么,如果我们使用 concurrent.futures.ProcessPoolExecutor 来执行我们的 CPU 密集型任务,会怎么样呢?

如代码所示,一切看起来都很棒,并且调用方式就像 asyncio.as_completed 一样。但看看结果,它们仍然是按启动顺序获取的。这与 asyncio.as_completed 完全不同,后者是按照执行的顺序获取结果:

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

结果按启动顺序获取。图片来源:作者

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

迭代结果仍保持调用顺序并阻塞。图片来源:作者

使用 asyncio 的 run_in_executor 来修复它

幸运的是,我们可以使用 asyncio 来处理 IO 密集型任务,并使用它的 run_in_executor 方法以与 asyncio 相同的方式调用多进程任务。这不仅统一了并发和并行 API,还解决了我们上述遇到的各种问题:

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

结合 asyncio 和 ProcessPoolExecutor。图片来源:作者

由于上一篇文章中的示例代码完全模拟了我们应该如何调用并发进程的方法,许多读者在学习后仍然很难理解如何在实际编码中使用它。因此,在理解为什么我们需要在 asyncio 中执行 CPU 密集型并行任务之后,今天我们将使用一个现实世界的例子来解释如何同时处理 IO 密集型和 CPU 密集型任务,并欣赏 asyncio 为我们的代码带来的高效。

注意:在继续之前,如果你对使用 asyncio.gatherasyncio.as_completed 的实践感兴趣,你可以阅读我的这篇文章:

## 使用这些方法提高 Python 并发任务的性能

asyncio.gather、asyncio.as_completed 和 asyncio.wait 的最佳实践

[towardsdatascience.com

现实案例:并发文件读取和 map-reduce 数据处理

在今天的案例中,我们将解决两个问题:

  1. 如何并发读取多个数据集,尤其是当数据集很大或很多时。如何使用 asyncio 提高效率。

  2. 如何使用 asyncio 的run_in_executor方法来实现 MapReduce 程序并高效处理数据集。

在我们开始之前,我将通过图示向你解释我们的代码将如何执行:

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

图示展示了整个代码的工作原理。图片由作者提供

黄色部分表示我们的并发任务。由于 CPU 可以比 IO 从磁盘读取数据更快地处理内存中的数据,因此我们首先将所有数据集并发地读取到内存中。

在初步数据合并和切片后,我们来到了绿色部分,代表 CPU 并行任务。在这部分,我们将启动几个进程来映射数据。

最终,我们将在主进程中获取所有过程的中间结果,然后使用reduce程序获得最终结果。

数据准备和依赖项安装

数据准备

在这个案例中,我们将使用Google Books Ngram Dataset,它按年份统计了 1500 年至 2012 年间各种书籍中每个字符串组合的频率。

Google Books Ngram 数据集是免费用于任何目的的,今天我们将使用下面这些数据集:

我们的目标是统计结果集中的每个词的累计出现次数。

依赖项安装

为了并发读取文件,我们将使用[aiofiles](https://pypi.org/project/aiofiles/)库,它支持 asyncio 的并发实现。

如果你使用 pip,你可以按照以下方式安装:

$ pip install aiofiles

如果你使用 Anaconda,你可以按照以下方式安装:

$ conda install -c anaconda aiofiles

代码结构设计

由于这个案例相对简单,为了演示,我们将在这里使用.py脚本完成整个过程。

作为架构师,在开始之前,你应该根据流程图设计规划你的方法,并尝试遵循每个方法的“单一职责原则”。因此,每个方法一次只做一件事:

代码实现

接下来,我们将逐步实现每个方法,并最终将它们集成在main方法中一起运行。

文件读取

方法read_file将实现使用aiofiles读取单个文件:

方法get_all_file_content将启动文件读取任务,所有文件读取完成后,将每行文本合并到一个列表中并返回。

数据分组

方法partition将根据传递的 partition_size 将列表分解成多个较小的列表,并使用生成器来促进后续的迭代:

映射处理数据

方法map_resource是实际的映射方法。使用它从列表中读取每一行数据,使用单词作为键,将频率的总和作为值,最后返回一个字典结果。

将 asyncio 与多进程整合

方法map_with_process调用了 asyncio 的run_in_executor方法,根据 CPU 核心的数量启动一个进程池,并并行执行映射方法。最终结果由asyncio.gather方法合并成一个列表。

减少合并的数据

由于之前的映射过程最终得到的是多个进程处理的单词频率列表,我们还需要使用reduce方法将多个字典合并成一个最终结果,记录每个单词的最终频率。在这里,我们首先编写reduce过程的方法实现。

然后,我们直接调用functools.reduce方法来合并数据。

最后,实施主方法

最终,我们将所有的方法集成到main方法中并进行调用。

太棒了!我们得到了在所有数据集中单词 Aardvark 的频率总和。任务完成。

使用 tqdm 指示进度

在上一篇文章中,我们解释了如何使用tqdm来指示 asyncio 任务的进度。

使用 Tqdm 与 Asyncio 结合的 Python

监控并发任务进度的高效方法

[towardsdatascience.com

由于在现实世界中,大数据集的数据处理通常需要较长时间,在此过程中我们需要跟踪代码执行的进度,因此我们还需要在适当的地方添加tqdm进度条。

现在看起来专业多了。

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

添加tqdm API 后的结果截图。图片来自作者

结论

在今天的文章中,我们探讨了多进程代码的一些问题,比如获取每个进程结果的麻烦,以及无法按任务执行顺序获取结果的问题。

我们还探讨了将 asyncio 与ProcessPoolExecutor集成的可行性以及这种集成给我们带来的优势。例如,它统一了并发和并行编程的 API,简化了我们的编程过程,并允许我们按完成顺序获得执行结果。

最后,我们解释了如何通过一个实际案例,交替使用并发和并行编程技术,以帮助我们在数据科学任务中高效执行代码。

由于个人能力有限,难免有些地方存在不足,因此欢迎你的评论和修改,以便我们一起学习和进步。

在接下来的文章中,我们将深入探讨如何利用 loop.run_in_execute API 将多个 asyncio 并发任务分布到多个 CPU 核心,从而释放 CPU 的性能潜力。请点击这里了解:

## 利用 Python 中的 Asyncio 发挥多核性能

通过高效利用多个 CPU 核心来提升你的 Python 应用性能

towardsdatascience.com

通过加入 Medium,你将可以无限制地访问我的所有文章以及其他成千上万作者的文章。这只需要你花费一杯咖啡的钱,但对我来说是极大的鼓励。

本文最初发表于:www.dataleadsfuture.com/combining-multiprocessing-and-asyncio-in-python-for-performance-boosts/

结合开放街道地图和 Landsat 开放数据来验证绿色区域

原文:towardsdatascience.com/combining-open-street-map-and-landsat-open-data-to-verify-areas-of-green-zones-b1956e561321?source=collection_archive---------4-----------------------#2023-10-02

快速简便的分析以便准确评估

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

·

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

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

预览图(作者提供)

今天我们想讨论结合来自各种开放数据源的空间数据的好处。例如,我们将考虑以下任务:评估某个特定属性是否位于“绿色”区域。废话少说,让我们开始吧!

附言:下面,我们考虑了一种仅基于开放数据的简单计算方法,涵盖了大多数(我希望是所有)城市。因此,下面你不会看到 基于这些数据训练的机器学习方法的描述,这些数据不太可能免费获得

方法 1. 最准确,但不适合懒人

假设在“柏林,Neustädtische Kirchstraße 4–7”的城市属性上出现了这样的评估任务。这个任务不是由某个全职程序员给出的,而是由一个可能不会编程但勤奋且愿意学习新知识的人给出的。在网上搜索后,“区域绿化评估专家”已经下载了 QGIS 并学会了使用 Quick Map Services 模块。接下来的步骤可能很明显,但迄今为止并不容易:需要弄清楚空间数据在地理信息系统(GIS)中是以何种格式表示的(矢量和栅格),然后学习如何创建这些对象并将其进行比较(以计算面积)。

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

图 1. 以免你在阅读时感到无聊:这就是用于描述空间对象的 GIS 原语(作者图像)

尽管除了上述操作外,还需要填补许多其他知识空白(例如,什么是坐标参考系统以及它们是什么),但最耗时的过程可能是对象的创建——矢量化。而且虽然有了知识和经验,GIS 操作会变得越来越容易,但矢量化难以自动化(几乎总是如此)。

那么,我们的专家能做些什么呢?他们可以通过 Quick Map Services 模块下载 Google 卫星数据并手动标记数据——绘制对象本身的几何形状。在这种情况下——公园和所有看起来像公园的地方。然后可以在(例如)该属性周围放置 2000 米的缓冲区来计算其面积。然后选择位于相同区域的属性,并将其归类为绿地,然后计算其面积。比率“邻里绿地面积 / 邻里总面积”将是我们要寻找的值。

一个重要的说明是建议我们的专家住在柏林,并且对调查的区域非常熟悉。因此,从图像中手动选择的对象可以被视为地面调查与手动卫星图像绘制的某种组合。

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

动画 1. 简单的矢量化。选择地图上的对象(卫星图像),并将其表示为矢量图层(作者动画)

优点: 相当准确和客观的估计。计算出的比率充分表征了“邻里绿化程度”。我们的专家肯定会得到奖金。

缺点: 提出的方案需要大量的手动步骤。这种工作无法快速完成(我总共花了 4 小时)。此外,我们还必须重复很多相同的操作——这种工作可能会让我们的专家工作几周,并去到一个不需要他/她用这种方式计算绿色区域的公司。

方法 2. 便宜且高效

当然,我们在谈论的是 OpenStreetMap……与此同时,我们的英雄意识到 IT 总体上比房地产更有趣,开始学习 Python 编程语言(当然)。他/她熟悉了 osmnx 模块和 geopandas 库来处理空间数据,以及 shapely 库来处理几何形状。通过这三者的配合,可以对区域的绿色度进行程序化评估。需要执行以下步骤:

  1. 在点(物业)周围创建一个多边形——分析区域的边界

  2. 查询位于此多边形中的 OSM 数据(要查询它,需要了解 OSM 的标签系统

  3. 计算区域多边形和根据 OSM 获得的公园及绿色区域的面积

这种方法要快得多,因为它不需要手动创建多边形。事实上,其他人已经为我们创建了这些多边形——这就是 OSM 的伟大之处。然而,OSM 也有缺点——数据可能不准确。此外,即使用户正确渲染了多边形,我们仍然有可能错误计算查询的标签集合,遗漏一些重要数据(图 2)。

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

图 2. 一个绿色区域的示例,根据 OSM 这是一个院子区域,并且没有通过与“绿色区域”相关的标签来区分(图片由作者提供)

这些区域确实不少。图 2 仅显示了一个小部分。然而,即使是这种方法也允许我们快速且大致,但仍然客观地评估区域的绿色度。

优点: 评估快速且简单。尽管存在不准确性,OSM 数据在大多数情况下仍然可用

缺点: 如果源数据不正确或查询的标签集合不够详细,则估计误差较大

方法 3. 利用 Landsat 图像作为客观数据来源

哦,关于 Landsat 说了和写了那么多。还有关于基于计算的植被指数 NDVI 的应用。如果你感兴趣,我们建议查看以下材料:

长话短说:

  1. Landsat 是一个绕地球飞行并拍摄具有相当高空间分辨率的图像的卫星(从光学谱段的 15–30 米到远红外谱段的 100 米)。

  2. 归一化植被指数(NDVI)是一个可以从 Landsat 拍摄的图像中计算出的植被指数。NDVI 的值范围从-1 到 1,其中-1 表示该像素中完全没有绿色(例如水域),而 1 表示该像素区域非常绿色——例如森林。

因此,我们可以获取一个 Landsat 图像(可以从这里下载 — 搜索“Landsat 8–9 OLI/TIRS C2 L2”产品)用于所选区域,并计算 NDVI。然后,通过调整阈值,我们可以将图像分为两类:0 — 非绿色区域和 1 — 绿色区域。我们可以根据需要调整阈值(见图 3),但老实说,我们并不确定哪个二值化阈值效果最好。此外,这个“最佳阈值”会因每个新获得的 Landsat 图像而异:无论是新的区域还是不同的时间日期索引。

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

图 3. 按阈值 0.10、0.15、0.20 对 NDVI 矩阵进行分割。“绿色区域”的边界用黑色显示(作者提供的图像)

优点: 这种方法提供了关于某个区域是否绿色的最新信息。

缺点: 选择最佳阈值是复杂的——不清楚选择哪个值来通过阈值将植被与非植被分开。当然,也有来源,可以找到关于建议阈值的信息,例如 0.35 及以上。但需要注意的是,这个最佳阈值可能因图像、季节等不同而有所变化。

方法 4. 基于 Landsat 的区域,基于 OpenStreetMap 的阈值

对于阈值定义问题,一个合理的解决方案可能是使用 OSM 数据。尽管 OSM 数据可能有伪影或遗漏了一些现实世界中的物体,但总体上它提供了物体(公园、广场、建筑物)所在位置的完整图像。因此,我们可以将从 OSM 和 NDVI 数据获得的几何映射叠加,然后构建一个直方图(见图 4)。

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

图 4. 两类直方图:根据 OSM 数据的绿色区域 NDVI 值变化及图像中其他 NDVI 值(作者提供的图像)

图中的黑线显示了可以用作阈值的值。然后,可以通过对 Landsat 矩阵进行自动矢量化,使用额外的多边形来扩充 OSM 数据。因此,所有位于计算阈值右侧的区域将变成“绿色区域”。由于这种方法,边界得到了扩展(见图 5)。

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

图 5. 根据 Landsat 数据调整前后的绿色区域边界(图像由作者提供)

而不是结论

将会有一张图片……

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

图 6. 使用不同方法提取的边界比较(图像由作者提供)

如所见,使用 Landsat 数据调整对象边界的第三种方法结果最接近基准(人工矢量化)。计算出的面积值也证实了这一点:

  • (基准)人工矢量化: 25%

  • 使用 OSM 数据的计算:17%

  • 基于改进的 OSM 和 NDVI Landsat 数据的计算:28%

如我们所见,尽管我们的估计稍微有些偏高,但仍然比根据 OSM 数据的计算结果更接近实际值。此外,我们的方法发现了在人工矢量化过程中未探索的绿色区域——北部的公墓。如果将这些区域从分析中排除,计算出的面积将更接近基准值。

附言:基于这个想法,我们留下了以下遗留物:

  • github.com/wiredhut/estaty — 处理空间数据和为房地产分析准备 MVP 算法的 Python 开源库。该库比较新,但我们计划对其进行开发和改进。

  • api.greendex.wiredhut.com/ — 用于计算“绿色”指数的服务和内部工具(Google 表格的扩展),使我们可以通过 API 和表格中的公式方便地使用上述算法(对于不懂编码的人)。该版本使用了简化的计算方法,而未使用 Landsat 卫星。

关于结合 OSM 和 Landsat 数据的故事由 Mikhail SarafanovWiredhut team 讲述。

结合传统的基于线程的代码和 Python 中的 asyncio

原文:towardsdatascience.com/combining-traditional-thread-based-code-and-asyncio-in-python-dc162084756c

PYTHON CONCURRENCY

结合同步和异步编程的综合指南

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

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

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

图片来源:作者创建,Canva

简介

在这篇文章中,我将解释如何在不实现 asyncio 的 asyncio 程序中调用现有的 IO 阻塞代码,以及如何在基于线程模型的现有程序中调用 asyncio 代码。

在之前的文章中,我向你介绍了 asyncio,这是一种 Python 特性。asyncio 的性能非常高,在现代的高并发代码中使用 asyncio 将提升 IO 性能几个数量级。

但在现实世界中,我们并没有看到 asyncio 代码被使用得像预期的那样多。为什么会这样呢?

挑战 1:如何在 asyncio 代码中调用旧的 IO 阻塞代码

一种情况是,当我们使用 asyncio 实现新代码时,系统中仍然存在大量传统实现的 IO 阻塞程序。例如,微服务、文件操作等。即使你使用 asyncio 并直接调用这些阻塞 API,仍然无法实现高并发效果。

挑战 2:如何在现有的阻塞代码中调用 asyncio 以实现异步任务

在另一种情况下,现有代码已经实现了一套基于线程模型的架构。由于 asyncio 的事件循环是在当前线程中执行的,直接调用 asyncio 会阻塞现有代码的执行,无法实现并发执行的效果。

所以今天,我将通过一些现实生活中的例子来向你展示如何在这两种情况下实现 asyncio 调用。

第一部分:在基于 asyncio 的程序中调用 IO 阻塞代码

以 FastAPI 为例。FastAPI 是一个基于 asyncio 实现的高性能 web 框架。但通常情况下,web 应用程序的所有业务逻辑并不是都在 FastAPI 代码中实现的。有时我们需要调用几个早已实现的阻塞调用的微服务。我们该如何处理这种情况?

使用 run_in_executor 运行 IO-阻塞代码

在上一篇文章中,我们解释了如何使用 loop.run_in_executor API 将多个进程与 asyncio 集成,以实现高性能计算。

## 结合多进程和 asyncio 以提升 Python 性能

使用实际的例子来演示 map-reduce 程序

towardsdatascience.com

然而,IO 绑定的代码不适合多进程调用,但推荐用于多线程。好的一点是 loop.run_in_executor 的第一个参数接受 concurrent.futures.ProcessPoolExecutor 实现或 concurrent.futures.ThreadPoolExecutor 实现。因此,我们的示例代码如下:

首先,我们使用 get_status 方法通过 requests 包来模拟旧的微服务代码调用。

然后,我们分别在 web 应用程序的启动和关闭阶段管理 ThreadPoolExecutor 线程池的创建和销毁。

最后,我们在线程池中调用 IO 阻塞方法,并通过 loop.run_in_executor 在请求的响应方法中获取结果。

loop.run_in_executor 的默认 executor 参数可以是 None。这是因为 asyncio 启动后会在内部初始化一个默认的线程池。当 run_in_executor 的 executor 参数为 None 时,它会使用默认线程池来执行,因此我们不必在代码中管理线程池:

利用 asyncio.to_thread(Python 3.9+)

Python 3.9 引入了新的高级抽象 API asyncio.to_thread,从源代码中可以看到,它内部调用了 loop.run_in_thread 方法,executor 参数为 None:

因此,使用 asyncio.to_thread 将进一步简化代码。

第二部分:在传统线程基础程序中调用 asyncio 代码

另一个情况是我们的程序已经在现有代码中实现了循环。例如,大多数 GUI 程序使用事件循环来响应各种事件并更新 UI。

tkinter 为例。tkinter 启动时会启动一个主循环,这个主循环会阻塞主线程并不断循环。如下图所示:

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

tkinter 主循环是如何工作的。图片来自作者

直接调用同步 IO 代码会阻塞主循环

让我们以包含按钮和状态文本的 tkinter 程序为例:

这个程序使用状态机来实现。每 60 毫秒,代码根据程序的当前状态刷新相应的文本。

当我们点击 request_code 按钮时,工作流程理想情况下应该如下图所示:

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

tkinter 程序的工作流程。图片来自作者

但从执行结果来看,点击按钮时程序会挂起,状态文本直到 IO 阻塞代码执行完才会更新。这意味着在 IO 请求运行时,主循环被阻塞,导致 GUI 界面没有响应:

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

应用程序被阻塞且不显示查询文本。图片来自作者

使用 asyncio.run 运行 Asyncio 代码

我们能否用 aiohttp 包替换 requests 包来实现 IO 请求的异步调用?

在这里,我们首先继承 App 类来实现一个新的类 AppAsyncBase。在这个新类中,我们使用 aiohttp 实现一个 async_request 方法,为后续的异步调用奠定基础:

读过我之前文章的读者会知道我们可以通过 asyncio.run 在同步代码中执行异步方法:

然后,我们通过继承 AppAsyncBase 实现一个新的类 AppAsyncRun。在这个新类中,我们重写 request_remote 方法,并使用 asyncio.run 直接调用 async_request 方法:

接下来,让我们看看结果。由于 asyncio 的 事件循环 默认在主线程中执行,当事件循环运行时,它会阻塞主线程,从而使 tkinter 的主循环被阻塞且没有响应:

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

asyncio.run 会阻塞主循环。图片来自作者

将 Asyncio 与基于线程的程序集成

是否有解决事件循环阻塞问题的方法?

在这里,我们可以使用一个单独的 守护线程,然后将事件循环运行到守护线程中,这样 asyncio 的事件循环就不会阻塞主线程。图示如下:

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

结合 tkinter 和 asyncio 循环。图片来自作者

查看代码实现,我们首先继承 AppAsyncBase 类来实现一个新的类 AppEventLoop。接下来,重写 request_remote 方法并使用 asyncio.run_coroutine_threadsafe 来在事件循环中调用 async_request 方法。事件循环中的请求方法 asyncio.run_coroutine_threadsafe 也是线程安全的:

实现一个run_event_loop方法,在线程中调用loop.run_forever

然后,使用contextmanager装饰器来管理守护线程的生命周期:

最后,在主方法中实现事件循环集成和应用启动,让我们看看结果:

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

在守护线程中独立运行的事件循环不再阻塞。图像来源:作者

完美!点击按钮,状态文本相应变化,整个 GUI 界面运行流畅,IO 调用从未阻塞 GUI。任务完成。

结论

尽管asyncio可以显著提高并发程序的执行性能,但由于它没有实现大量的遗留代码,所以尚未大规模使用。

今天的文章,通过真实世界的编码示例,展示了解决两个挑战的方案:

  1. 如何在新的asyncio程序中以非阻塞方式调用旧的 IO 代码。

  2. 如何在现有的同步程序中使用asyncio异步代码实现非阻塞执行。

欢迎留下评论和讨论。我会逐一回答。

通过加入 Medium,你将无限访问我和其他数千位作者的所有文章。这只需要你一杯咖啡的价格,但对我来说是极大的鼓励。

本文最初发表于:

www.dataleadsfuture.com/combining-traditional-thread-based-code-and-asyncio-in-python/

命令行接口(CLI)教程 — 高级用户如何与计算机交互

原文:towardsdatascience.com/command-line-interface-cli-tutorial-how-advanced-users-interact-with-computers-28cf88f81ce

提高与计算机交互生产力的 CLI 介绍

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

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

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

程序员兔子,图片来源 DALL.E 2

你是否还记得电影中那种场景:黑客突然在他们的笔记本电脑上打开一个黑暗的屏幕,猛力敲击键盘几秒钟,随后发生了一些重要事件?在这些场景中(如果这些事件真的发生的话),黑客通过文本输入直接与计算机交互,这与我们通常通过图形用户界面(GUI)与手机或笔记本电脑交互的方式不同。在这篇文章中,我们将介绍如何仅通过文本与计算机交互。到文章结尾时,你甚至可能会发现这种方法比通常的 GUI 更高效。

命令行接口或 CLI 是一种通过在终端(或控制台)中输入文本命令与计算机交互的方法。这与大多数用户习惯的图形用户界面(GUI)不同。GUI 的一个例子是 iPhone 的 iOS,界面是图形化的,用户可以点击和滑动以与设备互动。高级和技术用户认为 CLI 是与计算机交互的更优方法,因为 CLI 提供了更高效和直接的控制。

我记得在开始尝试使用 CLI 之前,它让我感到多么令人畏惧。但是学习过程证明是非常简单、值得的,更重要的是很有趣。所以我决定创建这个教程来分享我的学习经验——只要你阅读完这篇文章,你也可以认为自己已经“入门”了!

我将首先包含一个包含我在本篇文章中介绍的命令的表格式速查表,这对将来使用很方便,然后将详细介绍这些命令,并附上示例。

开始吧!

(除非另有说明,否则所有图片均由作者提供。)

CLI 速查表

可以参考这个表格(或保存它)以备将来使用,在你阅读了带有示例的教程之后。

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

CLI 速查表

从这一点开始,我将详细介绍这些命令,并提供示例。我鼓励你按照每一步操作,并在阅读教程的同时通过实践来学习。

1. CLI 在哪里?

我们从如何启动命令行界面(CLI)开始。

  • Mac: 我们需要找到“终端”。所以请在你的 Mac 的 Spotlight 搜索中搜索“终端”。

专业提示: 你可以按住“command”键,同时按下空格键,Spotlight 搜索应该会打开。然后只需开始输入“终端”,当它在 Spotlight 搜索中出现时按下“return”。

  • Windows: 在 Windows 中,CLI 被称为“命令提示符”。点击“开始”按钮,在搜索栏中输入“cmd”。然后点击“命令提示符”或“Windows 命令处理器”。

2. 了解命令

一旦我的终端启动,我会看到如下内容(你的内容可能会略有不同,但结构相同):

farzad 20230211_CLI %

在上面的示例中,“farzad”是我的用户名,“20230211_CLI”是我当前的目录。这是我创建这个教程的目录,日期为今天。你可能会看到一个波浪号符号(~),它代表主目录。

这是刚开始的地方,我们可以开始输入命令了。

正如我们在这个示例中看到的,命令行界面向我们显示了我们所在的位置(例如,上面的“20230211_CLI”),但如果我们想看到当前路径的完整路径,可以使用pwd或打印工作目录命令,如下所示:

farzad 20230211_CLI % pwd
/Users/farzad/Downloads/M/Archive/20230211_CLI

第一行是命令,第二行显示从我的主目录(即最高级别目录)到当前所在位置的完整地址。

3. 列出文件和目录

知道我们所在位置的文件和文件夹是非常有用的。在图形用户界面(GUI)中,我们可以直接查看文件和文件夹,而在命令行界面(CLI)中,有一些命令可以做到这一点。

我们可以使用ls或列表命令来查看目录的内容。这是我使用此命令时看到的内容:

farzad 20230211_CLI % ls
CLI.ipynb random-folder

第一行是命令,第二行是结果。第二行告诉我们有一个名为“CLI.ipynb”的文件(这是我撰写本篇文章的 Jupyter 笔记本)和一个名为“random-folder”的目录。

但如果我们想要看到每个文件或目录的更多细节怎么办?例如,看到文件的大小等。我们可以通过在现有的列表命令中添加-l(即长格式)选项来实现,如下所示:

farzad 20230211_CLI % ls -l
total 8
-rw-r--r-- 1 mafarzad staff 3673 Feb 11 17:40 CLI.ipynb
drwxr-xr-x 2 mafarzad staff 64 Feb 11 17:35 random-folder

这包含了更多信息。让我们从左边开始了解这所有的含义。

  1. 权限: 这是一个由 10 个字符组成的系列,表示该特定文件或目录的权限,如下所示:
  • 字符 1 表示文件类型: 表示常规文件;d 表示目录;l 表示符号链接

  • 字符 2 到 4:所有者的权限

  • 字符 5 到 7:组的权限

  • 字符 8 到 10:其他用户的权限

  • 这九个字符可以由以下内容组成:

    r:读取

    w:写入

    x:执行

    -:未授予权限

2. 链接数: 硬链接到文件或目录的数量

3. 所有者: 文件或目录所有者的用户名

4. 组: 与文件或目录关联的组名称

5. 大小: 以字节为单位的大小

6. 日期和时间: 文件或目录上次更改的日期和时间

7. 名称: 文件或目录的名称

4. 导航

现在我们知道我的位置下有一个名为“random-folder”的目录,让我们看看如何进入那个目录。

我们可以使用 cd 或更改目录命令来导航,如下所示:

farzad 20230211_CLI % cd random-folder 
farzad random-folder % 

在上述命令中,我使用了 cd 导航到“random-folder”,然后第二行显示当前所在位置是“random-folder”(而不是之前的“20230211_CLI”)。

但我们如何返回上一级目录呢?换句话说,现在我们在“random-folder”位置,如何返回到之前的地方,即“20230211_CLI”?我们可以使用更改目录命令,后跟一个空格和两个句点。我将在下面的示例中使用 pwd 以演示这个过程。

farzad random-folder % pwd
/Users/mafarzad/Downloads/M/Archive/20230211_CLI/random-folder

首先,我们仅使用 pwd 确认我们在“random-folder”中。我们还看到“20230211”仅在我们当前位置的上一级。然后让我们使用 cd .. 命令跳转一个级别(确保 cd.. 之间包含一个空格),如下所示:

farzad random-folder % cd ..
farzad 20230211_CLI % pwd
/Users/mafarzad/Downloads/M/Archive/20230211_CLI

这里我们首先使用 cd .. 返回到“20230211_CLI”,然后使用 pwd 确认位置。

由于我想在下一个示例中使用“random-folder”,让我们再练习一次返回到“random-folder”位置,如下所示:

farzad 20230211_CLI % cd random-folder 
farzad random-folder % pwd
/Users/mafarzad/Downloads/M/Archive/20230211_CLI/random-folder

5. 创建和删除文件及目录

touch 命令可用于创建新文件。在下面的命令中,我将创建一个名为“new_file.txt”的新文件,然后使用列表命令返回该目录的内容:

farzad random-folder % touch new_file.txt
farzad random-folder % ls -l
total 0
-rw-r--r-- mafarzad staff 0 Feb 11 18:04 new_file.txt
farzad random-folder % 

在第一行中,我创建了文本文件,然后使用长格式的列表(即 ls -l)确认了文件已创建。有趣的是,total 0 表示该目录中没有文件,而文件名实际上是列出的,因为新创建的文件此时只是一个名称,没有实际内容。

我们可以使用 mkdir 或 make directory,后跟目录名称,来创建一个新文件夹(或目录),如下所示:

farzad random-folder % mkdir new-folder
farzad random-folder % ls -l
total 0
drwxr-xr-x 2 mafarzad staff 64 Feb 11 18:08 new-folder
-rw-r--r-- mafarzad staff 0 Feb 11 18:04 new_file.txt

在第一行中,我创建了一个名为“new-folder”的新目录,然后使用长格式列表,我们看到该目录与我们在上一步中创建的文件并排存在。

接下来,让我们使用 rm 或 remove 命令删除文本文件,后跟文件名,如下所示:

farzad random-folder % rm new_file.txt 
farzad random-folder % ls -l
total 0
drwxr-xr-x 2 mafarzad staff 64 Feb 11 18:08 new-folder

在命令的第一行中,我删除了文本文件,然后使用长格式列表,我们看到当前目录中仅剩下目录,而文本文件已被删除。

最后,我们可以使用 rmdir 或 remove directory,后跟其名称,来删除目录,如下所示:

farzad random-folder % rmdir new-folder 
farzad random-folder % ls -l
total 0

命令的第一行删除了目录,通过长格式列表确认它已被删除。

6. 复制文件或目录

6.1. 复制文件

这个概念类似于 Mac 和 Windows 操作系统中的复制和粘贴,但它可以用 cp 或 copy 命令在一行内完成,命令格式是源文件名和目标文件名之间用空格分隔。命令格式如下:

cp source-file destination-file

让我们通过一个示例来实现它。首先,我们创建一个名为“file-1.txt”的文件,以便我们有东西可以复制,然后使用列出命令确认文件存在。

farzad random-folder % touch file-1.txt
farzad random-folder % ls
file-1.txt

接下来,让我们将“file-1.txt”复制到一个名为“file-2.txt”的新文件中,并再次使用列出命令确认我们的更改。

farzad random-folder % cp file-1.txt file-2.txt
farzad random-folder % ls
file-1.txt file-2.txt

正如预期的那样,现在有两个文件,包括第一个文本文件(即源文件)和第二个文本文件(即目标文件)。

6.2. 复制目录

这个概念与文件复制非常相似,但我们在 cp 命令中添加了递归选项 -r(即 cp -r)。递归意味着 cp 命令会复制目录及其所有内容。此类命令的整体格式为:

cp -r source-directory destination-directory

让我们通过一个示例来操作。首先,看看我们当前目录中存在哪些文件。

farzad random-folder % ls
file-1.txt file-2.txt

接下来,让我们在目录中上移一级,查看那里有哪些目录和文件。

farzad random-folder % cd ..
farzad 20230211_CLI % ls 
CLI.ipynb random-folder

现在,让我们将“random-folder”目录及其所有内容复制到第二个名为“random-folder-2”的目录中,如下所示:

farzad 20230211_CLI % cp -r random-folder random-folder-2

接下来,让我们使用列出命令确认新目录已创建。然后我们将使用更改目录命令进入新创建的目录(即“random-folder-2”),并在那里使用列出命令,确保原始目录“random-folder”中存在的所有内容都已复制到目标目录“random-folder-2”中。

farzad 20230211_CLI % ls
CLI.ipynb random-folder random-folder-2
farzad 20230211_CLI % cd random-folder-2
farzad random-folder-2 % ls
file-1.txt file-2.txt

正如预期的那样,新创建的“random-folder-2”包含了我们在“random-folder”源目录中的两个文本文件。

7. 删除(删除)文件或目录

这个操作的原理很简单——我们只是希望删除文件或文件夹。我们可以使用 rm 或 remove 命令,后跟一个空格和文件名来删除文件。删除目录也是完全一样的,但我们需要在命令中添加递归选项 -r。以下是需要遵循的一般格式:

rm name-of-file-to-be-removed
rm -r name-of-directory-to-be-removed

让我们看看这个目录中有哪些文件,然后可以删除其中一个以进行练习。

farzad random-folder-2 % ls
file-1.txt file-2.txt
farzad random-folder-2 % rm file-1.txt 
farzad random-folder-2 % ls
file-2.txt

第一行使用 list 命令列出了当前目录的内容,其中包含两个文本文件。在第三行(或第二行命令),我们使用 remove 命令删除了 “file-1.txt”,然后使用 list 命令确认它已被删除。

8. 查看文件内容

到目前为止,我们只查看了文件和目录的名称,但如果我们想查看文件的内容呢?我们可以使用 catconcatenate 命令来查看文本文件的内容。为此,我在名为 “file-1.txt” 的文件中添加了一个链接,让我们看看如何使用 concatenate 命令来查看该文件的内容。

farzad random-folder-2 % ls
file-1.txt file-2.txt
farzad random-folder-2 % cat file-1.txt 
If you liked this post, follow me on Medium at: 
https://medium.com/@fmnobar

第一行命令使用 list 列出当前目录中的文件名。在第二行,我们看到当前位置有两个文件。然后在第三行(或第二行命令),我们使用 concatenate 命令查看 “file-1.txt” 文件的内容,结果显示在最后两行,如下所示:

If you liked this post, follow me on Medium at: 
https://medium.com/@fmnobar

9. 搜索

有很多时候我们需要搜索一串字符,这可以通过使用 grep 命令来完成。这个命令允许我们在文件中搜索一个模式(在这个上下文中,我们称这串字符为 “模式”)。例如,如果我们想在 “file-1.txt” 中搜索 “medium” 一词,可以按照以下方式进行:

farzad random-folder-2 % grep medium file-1.txt
https://medium.com/@fmnobar

第一行命令使用 grep 或全局正则表达式打印命令搜索 “file-1.txt” 中的 “medium” 一词,第二行是此搜索的结果。注意,“file-1.txt” 包含两行,但 grep 命令只返回包含我们寻找的模式的行。

grep 命令非常多功能且灵活。例如,让我们看看可以使用的两个选项:

  1. -c 选项用于仅显示包含我们寻找的模式的行数。例如,让我们查看 “file-1.txt” 中 “medium” 模式出现了多少次。我们知道结果应该是 1,让我们验证一下。
farzad random-folder-2 % grep -c medium file-1.txt
1

2. -v 选项用于显示所有不包含我们寻找的模式的行。我们知道在 “file-1.txt” 中只有一行不包含 “medium” 模式——让我们在 CLI 中验证一下。

farzad random-folder-2 % grep -v medium file-1.txt
If you liked this post, follow me on Medium at: 

结论

在这篇文章中,我们讨论了命令行界面(CLI)如何不同于图形用户界面(GUI),以及为什么技术用户倾向于更喜欢 CLI 而非 GUI,以获得更高的效率、生产力和灵活性。然后我们通过示例介绍了 CLI 中最常用的命令。在通过这些示例之后,你应该能够舒适地启动终端并开始在日常工作中使用 CLI。类似于其他技能,使用 CLI 的次数越多,你会发现它变得越来越简单和有益。希望这篇文章能给你的 CLI 之旅提供一个良好的开端!

感谢阅读!

如果你觉得这篇文章对你有帮助,请在 Medium 上关注我并订阅以接收我最新的文章!

## 通过我的推荐链接加入 Medium - Farzad Mahmoodinobar

阅读 Farzad(以及 Medium 上其他作者)的每一个故事。你的会员费直接支持 Farzad 和其他…

medium.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值