双重机器学习,简化版:第二部分 — 目标设定与 CATE
学习如何利用 DML 估计特定的治疗效果,以实现数据驱动的目标设定
·
关注 发表在 Towards Data Science · 10 分钟阅读 · 2023 年 7 月 31 日
–
本文是关于简化和民主化双重机器学习的第二篇文章。在第一部分,我们介绍了双重机器学习的基础知识,以及两个基本的因果推断应用。现在,在第二部分中,我们将扩展这些知识,将我们的因果推断问题转变为预测任务,其中我们预测个体级别的治疗效应,以帮助决策和数据驱动的目标设定。
如我们在本系列第一部分中所学,双重机器学习是一种高度灵活的部分线性因果推断方法,用于估计治疗的平均治疗效应(ATE)。具体来说,它可以用于建模观察数据中高度非线性的混杂关系(尤其是当我们的控制/混杂变量集合具有极高的维度时)和/或在实验设置中减少关键结果的变异性。估计 ATE 对于理解特定治疗的平均影响尤为有用,这对未来的决策非常重要。然而,外推这一治疗效应假设效应的同质性;也就是说,无论我们将治疗推广到哪个人群,我们预期效应将与 ATE 相似。如果我们在未来推广中能够针对的个体数量有限,因此希望了解哪些子群体的治疗效果最为显著,从而驱动高效的推广,该怎么办?
上述问题涉及估计治疗效应的异质性。也就是说,我们的治疗效应如何影响不同的群体?幸运的是,DML 提供了一个强大的框架来实现这一点。具体来说,我们可以利用 DML 来估计条件平均治疗效应(CATE)。首先,让我们重新审视一下 ATE 的定义:
(1) 平均治疗效应
现在有了 CATE,我们可以在一组协变量X的条件下估计 ATE:
(2) 条件平均治疗效应
例如,如果我们想知道男性与女性的治疗效果,我们可以在条件变量等于每个感兴趣的子群体时估计 CATE。请注意,我们可以估计高度聚合的 CATE(即男性与女性层面),也可以允许 X 具有极高的维度,从而精确估计每个人的治疗效果。你可能会立即注意到这样做的好处:我们可以利用这些信息在未来的治疗目标上做出高度明智的决策! 更值得注意的是,我们可以创建一个 CATE 函数,预测我们对以前 未接触过的 个体的治疗效果的预测!
DML 提供了两种主要的方法来估计 CATE 函数,即线性 DML 和非参数 DML。我们将展示如何从数学上估计 CATE,然后为每种情况提供示例。
注意: CATE 的无偏估计仍然需要 exogeneity/CIA/Ignorability 假设成立,如在 第一部分中所述。
下面演示的所有内容都可以并且应该扩展到实验设置(RCT 或 A/B 测试),其中通过构造满足 exogeneity,如在 第一部分 的应用 2 中所述。
线性 DML 估计 CATE
在线性 DML 框架下估计 CATE 是对 DML 的一种简单扩展,类似于在 第一部分中对 ATE 的估计:
(3) DML 估计 ATE
其中 y 是我们的结果,T 是我们的治疗,& 𝑀𝑦 和 MT 是两个灵活的机器学习模型(我们的干扰函数),用于在给定混杂因素和/或控制变量 X 的情况下预测 y 和 T。要使用线性 DML 估计 CATE 函数,我们可以简单地包括治疗残差与协变量的交互项。观察:
(4) 线性 DML 估计 CATE
其中 Ω 是交互项系数的向量。现在我们的 CATE 函数,称之为 τ,具有形式 τ(X) = β₁ + XΩ,在给定 X 的情况下,我们可以预测每个个体的 CATE。如果 T 是连续的,则此 CATE 函数用于 T 的 1 单位增加。请注意,τ(X) = β₁ 在公式 (3) 中,其中 τ(X) 被假定为常数。让我们看看实际应用!
首先,让我们使用来自第一部分的相同因果 DAG,我们将研究个人在网站上花费的时间对其过去一个月购买金额或销售额的影响(假设我们观察到所有混杂因素):
那么我们接下来将使用与第一部分中类似的过程来模拟这个 DFP(请注意,所有值和数据都是为演示目的而任意选择和生成的,因此可能并不代表我们的 CATE 估计之外的大部分实际世界直觉)。请注意,我们现在在销售 DGP 中包含了交互项来建模 CATE 或处理效果的异质性(请注意,第一部分中的 DGP 在构建时没有处理效果异质性):
import numpy as np
import pandas as pd
# Sample Size
N = 100_000
# Confounders (X)
age = np.random.randint(low=18,high=75,size=N)
num_social_media_profiles = np.random.choice([0,1,2,3,4,5,6,7,8,9,10], size = N)
yr_membership = np.random.choice([0,1,2,3,4,5,6,7,8,9,10], size = N)
# Arbitrary Covariates (Z)
Z = np.random.normal(loc=50, scale = 25, size = N)
# Error Terms
ε1 = np.random.normal(loc=20,scale=5,size=N)
ε2 = np.random.normal(loc=40,scale=15,size=N)
# Treatment (T = g(X) + ε1)
time_on_website = np.maximum(10
- 0.01*age
- 0.001*age**2
+ num_social_media_profiles
- 0.01 * num_social_media_profiles**2
- 0.01*(age * num_social_media_profiles)
+ 0.2 * yr_membership
+ 0.001 * yr_membership**2
- 0.01 * (age * yr_membership)
+ 0.2 * (num_social_media_profiles * yr_membership)
+ 0.01 * (num_social_media_profiles * np.log(age) * age * yr_membership**(1/2))
+ ε1
,0)
# Outcome (y = f(T,X,Z) + ε2)
sales = np.maximum(25
+ 5 * time_on_website # Baseline Treatment Effect
- 0.2 * time_on_website * age # Heterogeneity
+ 2 * time_on_website * num_social_media_profiles # Heterogeneity
+ 2 * time_on_website * yr_membership # Heterogeneity
- 0.1*age
- 0.001*age**2
+ 8 * num_social_media_profiles
- 0.1 * num_social_media_profiles**2
- 0.01*(age * num_social_media_profiles)
+ 2 * yr_membership
+ 0.1 * yr_membership**2
- 0.01 * (age * yr_membership)
+ 3 * (num_social_media_profiles * yr_membership)
+ 0.1 * (num_social_media_profiles * np.log(age) * age * yr_membership**(1/2))
+ 0.5 * Z
+ ε2
,0)
df = pd.DataFrame(np.array([sales,time_on_website,age,num_social_media_profiles,yr_membership,Z]).T
,columns=["sales","time_on_website","age","num_social_media_profiles","yr_membership","Z"])
现在,为了估计我们的 CATE 函数,如等式 (4) 中所述,我们可以运行:
import statsmodels.formula.api as smf
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import cross_val_predict
# DML Procedure for Estimating the CATE
M_sales = GradientBoostingRegressor()
M_time_on_website = GradientBoostingRegressor()
df[‘residualized_sales’] = df["sales"] - cross_val_predict(M_sales, df[["age","num_social_media_profiles","yr_membership"]], df[‘sales’], cv=3)
df[‘residualized_time_on_website’] = df[‘time_on_website’] - cross_val_predict(M_time_on_website, df[["age","num_social_media_profiles","yr_membership"]], df[‘time_on_website’], cv=3)
DML_model = smf.ols(formula='residualized_sales ~ 1 + residualized_time_on_website + residualized_time_on_website:age + residualized_time_on_website:num_social_media_profiles + residualized_time_on_website:yr_membership', data = df).fit()
print(DML_model.summary())
得到如下结果:
在这里,我们可以看到线性 DML 逼近了 CATE 的真实 DGP(参见销售 DGP 中的交互项系数)。让我们通过将线性 DML 预测值与增加 1 小时网站停留时间的真实 CATE 进行比较,来评估我们 CATE 函数的表现:
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error
# Predict CATE of 1 hour increase
df_predictions = df[['residualized_time_on_website','age','num_social_media_profiles','yr_membership']].copy()
df_predictions['linear_DML_CATE']= (DML_model.predict(df_predictions.assign(residualized_time_on_website= lambda x : x.residualized_time_on_website + 1))
- DML_model.predict(df_predictions))
# True CATE of 1 hour increase
df_predictions['true_CATE'] = 5 - 0.2 * df_predictions['age'] + 2 * df_predictions['num_social_media_profiles'] + 2 * df_predictions['yr_membership']
# Performance Metrics
mean_squared_error(df_predictions['true_CATE'], df_predictions['linear_DML_CATE'])
mean_absolute_error(df_predictions['true_CATE'], df_predictions['linear_DML_CATE'])
r2_score(df_predictions['true_CATE'], df_predictions['linear_DML_CATE'])
在这里,我们得到了约 0.45 的均方误差(MSE),约 0.55 的平均绝对误差(MAE),以及约 0.99 的决定系数(R2)。通过绘制预测 CATE 和真实 CATE 的分布,我们得到:
此外,通过绘制预测值与真实值的关系,我们得到:
总体来说,我们的表现非常令人印象深刻!然而,这种方法的主要局限性在于我们必须手动指定 CATE 函数的功能形式,因此如果我们仅包含线性交互项,可能无法捕捉到真实的 CATE 函数。在我们的例子中,我们模拟了 DGP 以仅包含这些线性交互项,因此性能强劲 按构建,但让我们看看当我们将 CATE 的 DGP 随意调整为非线性时会发生什么:
# Outcome (y = f(T,X,Z) + ε2)
sales = np.maximum( 25
+ 5 * time_on_website # Baseline Treatment Effect
- 0.2 * time_on_website * age # Heterogeneity
- 0.0005 * time_on_website * age**2 # Heterogeneity
+ 0.8 * time_on_website * num_social_media_profiles # Heterogeneity
+ 0.001 * time_on_website * num_social_media_profiles**2 # Heterogeneity
+ 0.8 * time_on_website * yr_membership # Heterogeneity
+ 0.001 * time_on_website * yr_membership**2 # Heterogeneity
+ 0.005 * time_on_website * yr_membership * num_social_media_profiles * age # Heterogeneity
+ 0.005 * time_on_website * (yr_membership**3 / (1 + num_social_media_profiles**2)) * np.log(age) ** 2 # Heterogeneity
- 0.1*age
- 0.001*age**2
+ 8 * num_social_media_profiles
- 0.1 * num_social_media_profiles**2
- 0.01*(age * num_social_media_profiles)
+ 2 * yr_membership
+ 0.1 * yr_membership**2
- 0.01 * (age * yr_membership)
+ 3 * (num_social_media_profiles * yr_membership)
+ 0.1 * (num_social_media_profiles * np.log(age) * age * yr_membership**(1/2))
+ 0.5 * Z
+ ε2
,0)
df = pd.DataFrame(np.array([sales,time_on_website,age,num_social_media_profiles,yr_membership,Z]).T
,columns=["sales","time_on_website","age","num_social_media_profiles","yr_membership","Z"])
# DML Procedure
M_sales = GradientBoostingRegressor()
M_time_on_website = GradientBoostingRegressor()
df[‘residualized_sales’] = df["sales"] - cross_val_predict(M_sales, df[["age","num_social_media_profiles","yr_membership"]], df[‘sales’], cv=3)
df['residualized_time_on_website'] = df[‘time_on_website’] - cross_val_predict(M_time_on_website, df[["age","num_social_media_profiles","yr_membership"]], df[‘time_on_website’], cv=3)
DML_model = smf.ols(formula='residualized_sales ~ 1 + residualized_time_on_website + residualized_time_on_website:age + residualized_time_on_website:num_social_media_profiles + residualized_time_on_website:yr_membership', data = df).fit()
# Predict CATE of 1 hour increase
df_predictions = df[['residualized_time_on_website','age','num_social_media_profiles','yr_membership']].copy()
df_predictions['linear_DML_CATE']= (DML_model.predict(df_predictions.assign(residualized_time_on_website= lambda x : x.residualized_time_on_website + 1))
- DML_model.predict(df_predictions))
# True CATE of 1 hour increase
df_predictions['true_CATE'] = (5 - 0.2*df_predictions['age'] - 0.0005*df_predictions['age']**2 + 0.8*df_predictions['num_social_media_profiles'] + 0.001*df_predictions['num_social_media_profiles']**2
+ 0.8*df_predictions['yr_membership'] + 0.001*df_predictions['yr_membership']**2 + 0.005*df_predictions['yr_membership']*df_predictions['num_social_media_profiles']*df_predictions['age']
+ 0.005 * (df_predictions['yr_membership']**3 / (1 + df_predictions['num_social_media_profiles']**2)) * np.log(df_predictions['age'])**2)
# Performance Metrics
mean_squared_error(df_predictions['true_CATE'], df_predictions['linear_DML_CATE'])
mean_absolute_error(df_predictions['true_CATE'], df_predictions['linear_DML_CATE'])
r2_score(df_predictions['true_CATE'], df_predictions['linear_DML_CATE'])
在这里,我们看到性能急剧下降,我们得到了约 55.92 的均方误差(MSE),约 4.50 的平均绝对误差(MAE),以及约 0.65 的决定系数(R2)。通过绘制预测 CATE 和真实 CATE 的分布,我们得到:
此外,通过绘制预测值与真实值的关系,我们得到:
CATE 函数中的这种非线性正是非参数 DML 可以发光的地方!
用于估计 CATE 的非参数 DML
非参数 DML 更进一步,允许另一个灵活的非参数 ML 模型用于学习 CATE 函数!让我们看看如何在数学上准确执行此操作。让 τ(X) 继续表示我们的 CATE 函数。让我们从相对于 eq. 3 定义我们的误差项开始(请注意,我们放弃了截距 β₀,因为我们对于 CATE 的此参数不感兴趣;在线性 DML 公式中我们也可以类似地放弃此参数,但为了简单起见,并与第一部分保持一致,我们没有这样做):
(5) DML 框架中的误差
然后定义因果损失函数如下(请注意这只是均方误差!):
(6) 因果损失函数
这意味着什么?我们可以通过最小化我们的因果损失函数,直接用任何灵活的 ML 模型学习 τ(X) !这相当于一个带有我们的目标和权重的加权回归问题,分别为:
(7) 非参数 DML 中的目标与权重
稍作停顿,沉浸在这一结果的优雅之中…… 我们可以直接学习 CATE 函数,并预测个体的 CATE,给定我们的残差化结果, y*,和处理,* T*!*
现在让我们看看这在实际中是如何运作的。我们将重用在上述线性 DML 表现不佳示例中使用的非线性 CATE 函数的 DGP。为了构建非参数 DML 模型,我们可以运行:
# Define Target & Weights
df['target'] = df['residualized_sales'] / df['residualized_time_on_website']
df['weights'] = df['residualized_time_on_website']**2
# Non-Parametric CATE Model
CATE_model = GradientBoostingRegressor()
CATE_model.fit(df[["age","num_social_media_profiles","yr_membership"]], df['target'], sample_weight=df['weights'])
并且用来预测和评估性能:
# CATE Predictions
df_predictions['Non_Parametric_DML_CATE'] = CATE_model.predict(df[["age","num_social_media_profiles","yr_membership"]])
# Performance Metrics
mean_squared_error(df_predictions['true_CATE'], df_predictions['Non_Parametric_DML_CATE'])
mean_absolute_error(df_predictions['true_CATE'], df_predictions['Non_Parametric_DML_CATE'])
r2_score(df_predictions['true_CATE'], df_predictions['Non_Parametric_DML_CATE'])
在这里,我们获得了比线性 DML 更优越的性能,MSE 为 4.61,MAE 为 1.37,R2 为 0.97。绘制预测 CATE 和真实 CATE 的分布,我们得到:
另外,绘制预测值与真实值的图形,我们得到:
在这里我们可以看到,虽然不完美,但非参数 DML 方法能够比线性 DML 方法更好地建模 CATE 函数中的非线性。当然,我们可以通过调整我们的模型进一步提高性能。请注意,我们可以使用可解释的 AI 工具,如SHAP 值,来理解我们处理效应异质性的性质!
结论
至此!感谢您抽出时间阅读我的文章。希望这篇文章教会了您如何超越仅估计 ATE 并利用 DML 估计 CATE,以进一步理解处理效应的异质性,并推动更多因果推断和数据驱动的定位方案。
一如既往,希望您阅读本文能与我写作时一样愉快!
资源
[1] V. Chernozhukov, D. Chetverikov, M. Demirer, E. Duflo, C. Hansen, and a. W. Newey. 双机器学习用于处理和因果参数。ArXiv 电子打印,2016 年 7 月。
通过这个 GitHub 仓库访问所有代码: github.com/jakepenzak/Blog-Posts
感谢你阅读我的文章!我在 Medium 上的文章旨在探索利用 计量经济学 和 统计/机器学习 技术的现实世界和理论应用。此外,我还希望通过理论和模拟提供关于各种方法论的理论基础的文章。最重要的是,我写作是为了学习和帮助他人学习!我希望使复杂的话题对所有人稍微更易于理解。如果你喜欢这篇文章,请考虑 关注我在 Medium 上的账号!
使用 Python 下载 Landsat 卫星图像
原文:
towardsdatascience.com/downloading-landsat-satellite-images-with-python-a2d2b5183fb7
使用 landsatxplore Python 包简化 Landsat 场景下载
·发布在 Towards Data Science ·阅读时间 6 分钟·2023 年 5 月 9 日
–
Landsat 卫星是最常用的地球观测数据来源之一。它们已经提供了四十多年高质量的地球表面图像。然而,手动下载这些图像可能会很繁琐!幸运的是,使用 landsatxplore 包,你可以轻松地下载和处理 Landsat 场景,只需几行代码。
我们将探索 landsatxplore 包,并逐步演示如何使用 Python 下载 Landsat 卫星图像。这包括:
-
使用 USGS 帐户设置 API 访问
-
搜索和过滤 Landsat 场景
-
使用 Python 下载和处理场景
告别手动下载,迎接自动化、高效的工作流程!
设置 landsatxplore
步骤 1:注册 USGS
首先,你需要 设置一个 USGS 帐户。这是你用来通过 EarthExplorer 下载场景的相同帐户。记住你的 用户名 和 密码,因为我们稍后会用到它们。
一旦注册完成,你可以使用 USGS M2M API。然而,这需要一些设置工作。相反,我们将使用 landsatxplore 包,它会为你抽象出大部分技术细节。
步骤 2:安装 landsatxplore
按照 GitRepo 上的说明进行操作。
步骤 3:检查 API 连接
使用下面的代码确认一切设置正确。你应该用你注册 USGS 帐户时使用的 用户名 和 密码 替换掉这些占位符。
from landsatxplore.api import API
# Your USGS credentials
username = "XXXXXXXXXXXX"
password = "XXXXXXXXXXXX"
# Initialize a new API instance
api = API(username, password)
# Perform a request
response = api.request(endpoint="dataset-catalogs")
print(response)
响应的输出应如下所示:
{‘EE’: ‘EarthExplorer’, ‘GV’: ‘GloVis’, ‘HDDS’: ‘HDDS Explorer’}
这些是通过 API 提供的数据集。对于本教程,我们只考虑 EarthExplorer 数据集。
从 EarthExplorer 搜索场景
在我们继续使用 API 下载场景之前,我们将通过 EarthExplorer 进行手动搜索。这是为了将结果与使用 Python 看到的结果进行比较。如果你对 EarthExplorer 门户不熟悉,这个教程可能会有所帮助。
我们使用以下标准进行场景搜索:
-
这些场景必须包含给定纬度和经度的点。该点位于都柏林的布尔岛上。
-
采集时间为01/01/2020到12/31/2022
-
最大云量为50%
-
Level 2 Landsat 8 或 9 集合的一部分
EarthExplorer 搜索标准(来源:作者)
你可以在下面看到搜索结果。我们记录了一些内容以便与我们的 Python 搜索进行比较:
-
有54 个场景符合搜索条件。
-
有2 个切片包含布尔岛上的点。这些切片的路径和行值分别为**(206, 023)和(205, 023)**。
-
第一个场景的 ID 是LC08_L2SP_206023_20221118_20221128_02_T1。如果你对这个 ID 的含义感兴趣,请参见Landsat 命名规则。
EarthExplorer 搜索结果(来源:作者)
使用 landsatxplore Python 包
搜索场景
让我们使用 landsatxplore 进行等效搜索。我们使用下面的搜索功能来完成这一操作:
-
数据集 — 定义我们想要的卫星场景。我们使用的值是 Landsat 8 和 9 的数据集 ID。有关 Landsat 5 和 7 的 ID,请参见GitRepo。
-
纬度和经度提供了布尔岛上的相同点。我们已经将坐标转换为十进制度。
-
start_date、end_date和max_cloud_cover也与之前相同。
# Search for Landsat TM scenes
scenes = api.search(
dataset='landsat_ot_c2_l2',
latitude=53.36305556,
longitude=-6.15583333,
start_date='2020-01-01',
end_date='2022-12-31',
max_cloud_cover=50
)
# log out
api.logout()
搜索结果将以 JSON 字典的形式返回信息。我们将其转换为 Pandas DataFrame(第 4 行),其中每一行代表一个唯一的场景。返回了大量的元数据!因此,我们筛选出本应用程序所需的内容(第 5 行)。最后,我们按获取日期对其进行排序——即 Landsat 捕获场景的日期。
import pandas as pd
# Create a DataFrame from the scenes
df_scenes = pd.DataFrame(scenes)
df_scenes = df_scenes[['display_id','wrs_path', 'wrs_row','satellite','cloud_cover','acquisition_date']]
df_scenes.sort_values('acquisition_date', ascending=False, inplace=True)
你可以在下面看到这个数据集的快照。与我们使用 EarthExplorer 的搜索进行比较,我们可以确定结果是相同的。这个数据集有54 行,并且有两个唯一的wrs_path和wrs_row对——(206, 23)和(205, 23)。第一个display_id与我们之前看到的也是相同的。
df_scenes 数据集快照(来源:作者)
如果我们愿意,可以进一步筛选数据集。我们可以使用satellite列仅选择来自 Landsat 8 或 9 的图像。此外,cloud_cover列提供了图像被云层覆盖的百分比。当你对最终的场景列表感到满意时,可以继续下载它们。
下载数据
下面是用于下载 Landsat 场景的代码。我们使用 EarthExplorer 函数(第 5 行)。这与之前一样初始化——使用你的 USGS 凭证。要下载一个场景,我们需要使用其display_id并定义输出目录(第 12 行)。我们使用的是上面提到的第一个场景的display_id(第 8 行)。
from landsatxplore.earthexplorer import EarthExplorer
import os
# Initialize the API
ee = EarthExplorer(username, password)
# Select the first scene
ID = 'LC08_L2SP_206023_20221118_20221128_02_T1'
# Download the scene
try:
ee.download(ID, output_dir='./data')
print('{} succesful'.format(ID))
# Additional error handling
except:
if os.path.isfile('./data/{}.tar'.format(ID)):
print('{} error but file exists'.format(ID))
else:
print('{} error'.format(ID))
ee.logout()
你可能已经注意到上面的额外错误处理。这是因为包的问题。在某些情况下,场景将正确下载但仍然会出现错误。额外的错误处理会再次检查场景文件是否存在。
处理数据
场景将作为 tar 文件下载。文件的名称将是display_id后跟**.tar**:
LC08_L2SP_206023_20221118_20221128_02_T1.tar
我们可以直接在 Python 中处理这些数据。首先,我们需要解压 tar 文件(第 4–6 行)。新文件夹的名称设置为场景的display_id(第 5 行)。
import tarfile
# Extract files from tar archive
tar = tarfile.open('./data/{}.tar'.format(ID))
tar.extractall('./data/{}'.format(ID))
tar.close()
你可以看到下面的解压文件夹和所有可用的文件。这包括所有关于 Landsat level-2 科学产品的资料。数据的应用无穷无尽!例如,我们将使用可见光波段可视化这个场景。这些波段在下面突出显示的文件中可用。
Landsat level-2 科学产品文件(来源:作者)
我们加载蓝色、绿色和红色波段(第 6–8 行)。我们堆叠这些波段(第 11 行),对它们进行缩放(第 12 行)并裁剪以增强对比度(第 15 行)。最后,我们使用 matplotlib 显示这张图像(第 18–20 行)。你可以在下面看到这张图像。
import tifffile as tiff
import numpy as np
import matplotlib.pyplot as plt
# Load Blue (B2), Green (B3) and Red (B4) bands
B2 = tiff.imread('./data/{}/{}_SR_B2.TIF'.format(ID, ID))
B3 = tiff.imread('./data/{}/{}_SR_B3.TIF'.format(ID, ID))
B4 = tiff.imread('./data/{}/{}_SR_B4.TIF'.format(ID, ID))
# Stack and scale bands
RGB = np.dstack((B4, B3, B2))
RGB = np.clip(RGB*0.0000275-0.2, 0, 1)
# Clip to enhance contrast
RGB = np.clip(RGB,0,0.2)/0.2
# Display RGB image
fig, ax = plt.subplots(figsize=(10, 10))
plt.imshow(RGB)
ax.set_axis_off()
Landsat 场景的 RGB 通道可视化(来源:作者)
如果你想了解更多关于处理卫星图像 RGB 通道的细节,可以查看这篇文章:
在可视化卫星图像时,如何处理多个光谱带、大像素值和倾斜的 RGB 通道
[towardsdatascience.com
可视化 RGB 通道只是开始。下载数据后,我们可以进行任何遥感任务——从计算指标到训练模型。最棒的是,我们可以在不离开 Python 笔记本的情况下完成所有这些。
我希望你喜欢这篇文章!你可以通过成为我的 推荐会员 来支持我 😃
[## 通过我的推荐链接加入 Medium — Conor O’Sullivan
作为 Medium 会员,你的会员费用的一部分会流向你阅读的作者,并且你可以全面访问所有故事……
conorosullyds.medium.com](https://conorosullyds.medium.com/membership?source=post_page-----a2d2b5183fb7--------------------------------)
| Twitter | YouTube | Newsletter — 免费注册获取 Python SHAP 课程
自动驾驶中的可驾驶空间 — 学术界
原文:
towardsdatascience.com/drivable-space-in-autonomous-driving-a-review-of-academia-ef1a6aa4dc15
关于 2023 年可驾驶空间的学术研究的最新趋势
·发表于Towards Data Science ·13 分钟阅读·2023 年 5 月 18 日
–
可驾驶空间,或称为自由空间,在自动驾驶中扮演着至关重要的安全角色。在上一篇博客文章中,我们回顾了这一经常被忽视的感知特征的定义和重要性。在本文中,我们将回顾近期学术研究中的趋势。
可驾驶空间的定义与原因
towardsdatascience.com
可驾驶空间检测算法可以在两个维度上进行测量:输入和输出。关于输入传感器模态,可驾驶空间检测方法可以分为基于视觉的、基于激光雷达的或视觉-激光雷达融合的方法。关于输出空间表示,它们可以分为 2D 透视图像空间、3D 空间和鸟瞰视图(BEV)空间。
视觉图像本质上是 2D 的,而激光雷达点云测量本质上是 3D 的。正如在上一篇博客文章中讨论的那样,BEV 空间本质上是简化的或退化的 3D 空间,我们将在本博客中将 BEV 空间和 3D 空间互换使用。实质上,我们有一个 2x2 的输入输出矩阵用于评估所有可驾驶空间算法,如下图所示。绿色的右上象限是北极星,它具有最佳的表现力同时也是最具成本效益的。在接下来的章节中,我们将讨论三类算法:2D-to-2D、3D-to-3D 和 2D-to-3D。
感知算法范式矩阵(图像由作者创建)
值得注意的是,目前还没有一个普遍认可的标准来表达和评估自动驾驶中可驾驶空间的准确性。在这篇文章中,我们将回顾相关任务,这些任务可以有多种公式化方式。我们还提供了一些对学术界未来方向的见解,旨在加速对这一关键任务的研究。
2D 到 2D 方法(带图像)
在透视 2D 图像空间中检测可驾驶空间本质上是图像分割的任务。主要有两种方法:一种是基于stixel的障碍物检测,另一种是可驾驶空间的语义分割。
Stixel 表示法
stixel(stick 和 pixel 的组合) 方法的概念假设图像底部的像素对应的区域从驾驶角度来看是可驾驶的。然后,它向图像顶部延伸并生长一个棍子,直到遇到障碍物,从而获得该列的可驾驶空间。该方法最具代表性的工作之一是 StixelNet 系列。Stixel 将地面上的一般障碍物抽象为棍子,并将图像空间划分为可驾驶空间和障碍物。Stixel 表示法在像素和对象之间取得了良好的平衡,实现了良好的准确性和效率。
Stixel 概念在可驾驶空间检测中的示意图(来源:StixelNet for segmentation*)
语义分割
深度学习近年来取得了快速进展,使得检测可以直接通过卷积神经网络(CNN)建模为语义分割问题。这种方法与基于图像列表表示的方法不同,因为它直接对 2D 图像的像素进行是否为可驾驶空间的分类。典型的工作包括 DS-Generator 和 RPP。
与 Stixel 方法相比,一般的语义分割方法更为灵活。然而,它需要更复杂的后处理以使其对下游组件有用。例如,语义预测可能没有那么连续(不像以下示例中显示的结果那样干净)。在 Stixel 方法中,每列只选择一个像素以转换为 3D 信息。
语义分割 将可驾驶空间公式化为(来源:DS-Generator)
提升到 3D
由于预测和规划计算的下游任务发生在 3D 空间中,因此需要将 2D 图像中获得的可行驶空间结果转换为 3D 或退化的 BEV。常见的 2D 到 3D 后处理技术包括逆透视映射 (IPM)、单目/立体深度估计以及使用直接的 3D 物理测量,如激光雷达点云。此外,2D 到 2D 算法将每个相机流单独处理,因此需要明确的规则将它们拼接在一起,以实现多相机设置中的 360 度感知。
在 2D 透视空间和 2D 到 3D 转换中的繁琐后处理通常是手工制作的,这些脆弱的逻辑容易受到特殊情况的影响。实际上,2D 到 2D 算法在自动驾驶中很少使用,除了低速场景如停车。
2D DS 需要提升到 3D DS 以供下游使用(图像由作者创建)
3D 到 3D 方法(配合激光雷达)
这些 3D 可行驶空间算法接收激光雷达点云并直接生成 3D 可行驶空间。虽然早期研究主要基于激光雷达,但最近(2023 年初)我们看到基于视觉的语义 3D 占用预测的爆炸性增长,这将在下一节中深入探讨。
激光雷达地面分割
基于地面分割的方法旨在将激光雷达点云数据分为地面部分和非地面部分。这些方法可以分为几何规则基础算法和基于深度学习的算法。即使在深度学习广泛应用之前,基于几何规则的算法也已广泛应用于激光雷达点云中,以实现地面检测(或边石检测的补充任务)和一般障碍物检测任务。这些方法通常依赖于平面拟合和区域生长算法,这些算法首次在 2007 年和 2009 年于DARPA Urban Challenge中介绍。然而,简单的地面平面假设在遇到不平整的地面、坑洞以及上下坡场景时会失败。为了考虑局部不平整的道路和整体平滑度,一些研究建议引入基于高斯过程的算法优化。
基于深度学习的方法由于计算资源和大规模数据集的增加而逐渐流行。地面分割任务可以被形式化为对 LiDAR 点云的通用语义分割。LidarMTL 是一个典型的工作,提出了一个具有六个任务的多任务模型,其中包括动态障碍物检测之上的道路结构理解。对于道路场景理解,设计了两个语义分割任务:可驾驶空间和地面,并且还有一个地面高度回归任务。有趣的是,辅助任务,例如前景分割和物体内部部件定位,也被证明对动态物体检测有帮助。
LiDAR 点云的多任务语义分割(来源: LidarMTL)
以自由空间为中心的表示
Freespace Forecaster 和 其可微分版本 使用以自由空间为中心的表示来预测用于运动规划的可驾驶空间。这些方法从车辆作为中心,通过简单的射线投射获取地面和障碍物的点云或网格信息,使用极坐标计算可达关系。这种表示方式与 2D 透视空间中的 Stixel 表示非常相似,最近的障碍物位于每个区间内(2D 中的 stixel 和 3D 中的极角区间)。
以自由空间为中心的表示用于自由空间预测(来源: 可微分射线投射)
占用网格和场景流表示
在基于 LiDAR 的算法中,最通用的方法使用占用网格和场景流来分别表示一般障碍物的位置和移动。两个具有代表性的论文是 MotionNet 和 PointMotionNet。
动态物体检测和跟踪任务在公共数据集中很常见,使得学术研究人员能够生成占用网格和场景流的真实标签。占用表示,理论上,可以检测一般障碍物,涵盖具有任意形状的未知物体,以及静态障碍物和道路结构。然而,在实践中,量化算法的有效性是具有挑战性的,因为公共数据集中大多数物体是规则的。为了进一步推进这一领域的研究,需要一个检测一般障碍物的数据集和基准。
点云中的占用与流量预测(来源: MotionNet)
乍一看,物体检测算法生成的真实数据可以为更强大、更灵活的占用预测算法提供支持似乎不符合直觉。然而,有两个重要方面需要考虑。首先,物体检测可以帮助完成真实数据标注过程中的繁重工作和启动工作,而在实际生产环境中仍需人工质量保证和细微调整。其次,算法的制定起着关键作用。占用预测的制定使其更具灵活性,能够学习物体检测可能遗漏的细微差别。
基于激光雷达的占用算法与特斯拉提出的占用网络(Occupancy Network)不同。占用网络的概念是以视觉为中心的算法。
2D 到 3D 的方法(BEV 感知及更多)
多摄像头 BEV 感知框架(参见我之前关于 BEV 感知的博客文章)通过简化多摄像头后处理和后融合步骤,将视觉 3D 感知性能提升到一个新的水平。此外,该框架成功地在表达空间中统一了摄像头和 LiDAR 算法,为传感器融合提供了便捷的框架,包括前融合和后融合。
在 BEV 空间中检测道路的物理边缘是可驾驶空间检测任务的一个子集。虽然道路边缘和车道线可以用矢量线表示,但道路边界缺乏如平行性、固定车道宽度以及消失点的交集等约束,使得其形状和位置更加灵活。这些更加自由和多样的道路边缘可以通过几种方式建模:
-
基于热图的:由类似语义分割的解码器生成热图。热图需要处理为矢量元素,以供下游组件使用。
-
基于体素的:热图方法的扩展。热图中的 2D BEV 网格扩展为 3D 体素。
-
基于矢量的:生成基于原始几何元素(如多边形和多边形)的矢量化输出。这些输出可以直接传递给下游使用。
热图解码器
基于语义分割的方法可以根据目标建模方法分为两类:道路边界语义分割和道路布局语义分割。前者,如HDMapNet,可以预测车道线,同时输出道路边缘。神经网络的输出是一个热图,需要进行二值化和聚类,以生成可用于下游预测和调节的矢量输出。
HDMapNet 的架构图(来源: HDMapNet)
其他方法基于道路结构的语义分割,如PETRv2、CVT和Monolayout。输出是道路本身,其边缘是道路边界。神经网络输出仍然是需要二值化的热图,通过边缘操作可以得到向量化的道路边界。如果下游应用可以直接消耗道路结构本身,如基于占用网格的规划,使用这种感知方法会更直接。然而,这一话题更多与规制和控制相关,因此我在此不会详细展开。
CVT 的架构图(来源: CVT)
体素解码器(语义占用预测)
随着特斯拉在 2022 年提出占用网络的提案,2023 年初,单纯基于视觉的占用预测取得了爆炸性增长。这种体素输出表示可以看作是热图表示的扩展,每个热图网格位置预测一个额外的高度维度。
在这一领域,一个值得注意的工作是SurroundOcc。它首先设计了一个自动化流程,从稀疏点云中生成密集占用真值,然后利用这些密集真值来监督从多相机图像流中学习密集占用网格。有关各种方法的详细比较和工业应用中的潜在障碍,请参阅这篇语义占用预测的文献综述。
2023 年上半年的学术“占用网络”文献综述
语义占用预测的标注和训练流程(来源:SurroundOcc)
向量解码器
基于热图和体素的方法都依赖于语义分割,并且需要大量后处理来满足下游需求。相比之下,基于直接向量输出的方法更加直接。代表性的方法包括STSU、MapTR和VectorMapNet,这些方法直接输出向量化道路边缘。这种方法可以视为基于锚点的目标检测方法的变体,其中基本几何元素是具有 2N 自由度的多段线或多边形,其中 N 是多段线或多边形的点数。MapTR和VectorMapNet就是这样的例子。值得一提的是,STSU使用了具有 3 个控制点的 Bezier 曲线,这虽然新颖,但不如多段线和多边形灵活,其当前效果也不如后者。
BEV 解码器中的向量化(来源: MapTR)
摄像头和激光雷达融合
上述 2D 到 3D 方法利用了摄像头图像中的丰富纹理和语义信息来推断自车周围的 3D 环境。虽然激光雷达点云数据可能缺乏这些丰富的语义,但它提供了准确的 3D 位置测量信息。
多摄像头 BEV 空间和激光雷达 BEV 空间可以在 BEV 融合中轻松统一。例如,在HDMapNet中,激光雷达单独方法和摄像头-激光雷达融合方法也与摄像头单独基线进行了比较。尽管摄像头在 BEV 定位中可能表现稍差于激光雷达单独方法,但多摄像头 IoU 指标在车道分隔线和行人过街处仍然更好,而激光雷达在检测道路边界方面更优。这是可以理解的,因为道路边界通常伴随高度变化,更容易通过主动激光雷达测量来检测。
2D 到 3D DS 算法中视觉和激光雷达的性能比较(来源: HDMapNet)
公共数据集
目前在自动驾驶领域,没有广泛接受的可驾驶空间数据集。然而,有一些相关任务,如 3D 到 3D 方案中的点云分割和 2D 到 3D 方案中的 HD 地图预测。不幸的是,生成 3D 点云分割和 HD 地图的成本较高,这限制了学术研究使用少数公共数据集。这些数据集包括 NuScenes、Waymo、KITTI360 和 Lyft。这些数据集提供了 3D 点云分割和标注,并包含一些道路信息,如路面、路边和人行道。Lyft 数据集还包括道路区域的地图信息,有助于理解道路布局。
需要建立公共数据集和评估指标基准,以促进该领域的发展。特别需要关注两件事。
-
长尾角落情况。 必须关注困难样本的检测效果,以确保系统的可靠性。然而,长尾数据在不同区域和场景中可能有所不同,收集和标记这些数据需要时间和技术专长。值得探索平衡小样本数据和提高学习效果的方法。
-
输出格式。 3D 可驾驶空间的定义和实现与下游消费逻辑及自动驾驶系统设计密切相关。行业中很难建立统一标准,也不常作为学术研究或公共数据集竞赛中明确和独立定义的模块。多边形可能是一种足够灵活的格式。 通过物体检测得分和时间一致性可以评估跨帧检测到的多边形,因为下游需要一致的形状以实现精确的车辆操作。
重点
-
在学术界尚未有普遍接受的可驾驶空间定义或评估指标。重新标记公共数据集是一种可能的方法,但需要丰富长尾角落情况。
-
2D 图像空间像素级可驾驶空间检测依赖于来自 IPM 或其他模块的深度信息,但位置误差随着距离增加。在 3D 空间中,LiDAR 提供高几何精度但语义分类较弱。它可以通过地面拟合和其他方法检测道路边缘和一般障碍物,还可以通过光线投射自由空间或占用表达识别未知的动态和静态障碍物。
-
2D 到 3D BEV 感知方法因其性价比高和强大的表示能力而具有前景。然而,缺乏可驾驶空间的标准导致了各种输出格式。输出格式取决于下游规划和控制的要求。
这是关于可驾驶空间的第二篇文章,重点介绍了最近的学术进展。第一篇文章探讨了可驾驶空间的概念。在下一篇文章中,我们将讨论可驾驶空间的当前工业应用,包括如何将其扩展到自动驾驶之外的一般机器人领域。(更新:第三篇博客文章已经发布。)
截至 2023 年的行业应用最新趋势
注意:本博客文章中的所有图片要么由作者创建,要么来自公开的学术论文。有关详细信息,请参见说明。
参考文献
-
StixelNet:一种用于障碍物检测和道路分割的深度卷积网络,BMVC 2015
-
实时基于类别和通用的自动驾驶障碍物检测,ICCV 2018
-
DS-Generator:从立体图像中学习无碰撞空间检测,IEEE/ASME Transactions on Mechatronics 2022
-
RPP:使用深度全卷积残差网络与金字塔池化进行可驾驶道路分割,Cognitive Computation,2018
-
STSU:基于车载图像的结构化鸟瞰视图交通场景理解,ICCV 2021
-
HDMapNet:在线高清地图构建与评估框架,Arxiv 2021
-
PETRv2:一种统一的多摄像头图像 3D 感知框架,Arxiv 2022
-
CVT:用于实时地图视图语义分割的跨视图变换器,CVPR 2022
-
Monolayout:从单张图像中获得的模态场景布局,WACV 2020
-
Darpa 城市挑战 2007,ATZ worldwide 2008
-
DARPA 城市挑战:城市交通中的自动驾驶车辆,springer,2009
-
基于高斯过程的自动驾驶地面实时分割,Journal of Intelligent & Robotic Systems,2014
-
基于高斯过程的倾斜地形地面分割,ICRoM 2021
-
LidarMTL:一种简单高效的多任务网络,用于 3D 物体检测和道路理解,Arxiv 2021
-
Freespace Forecaster:基于自监督的安全局部运动规划与自由空间预测,CVPR 2021
-
MotionNet:基于鸟瞰图的自动驾驶联合感知与运动预测,CVPR 2020
-
PointMotionNet:针对大规模 LiDAR 点云序列的逐点运动学习,CVPR 2022
-
MapTR:用于在线矢量化高清地图构建的结构化建模与学习,ICLR 2023
-
VectorMapNet:端到端矢量化高清地图学习,Arxiv 2022
-
SurroundOcc:用于自动驾驶的多摄像头 3D 占用预测,Arxiv 2023
通过精心设计指标推动运营成功
将战略转化为运营指标的艺术
·
关注 发表在 Towards Data Science ·11 分钟阅读·2023 年 1 月 12 日
–
TL;DR:
-
数据/业务分析师有时会被赋予“惊人的机会”来帮助创建一些指标。或者他们可能会看到创建新指标的需求并主动承担这些任务。但虽然建立指标很容易,设计好的指标却很难。
-
除了追踪,指标还是组织使利益相关者围绕一个共同的愿景和目标对齐的方式,而这伴随着一系列挑战。
-
一个框架可以帮助运营团队确保他们正确设置指标:输入 > 输出 > 结果框架。
-
无论上述框架是否使用,一旦新的指标被设计出来,验证它们并确保它们通过几个测试是很重要的。
定义指标基本上是将战略转化为一组“数量”。 明确定义的指标帮助你确保保持在实现组织目标的正确轨道上。但翻译错误可能会很昂贵:如果你设计的指标没有完全代表战略的精神,组织很容易偏离原来的目标,最终虽然你有人员做得很好(他们达到了你为指标设定的 OKR),组织却完全没有达到你的预期状态:
想象一下你从洛杉矶飞往纽约市。如果一位从 LAX 起飞的飞行员将航向调整仅仅 3.5 度向南,你将会降落在华盛顿特区,而不是纽约。这样一个小的变化在起飞时几乎不易察觉——飞机的机头仅移动了几英尺——但当这种变化放大到整个美国,你最终会相隔数百英里。
图片来源于 Mitchel Boot 在 Unsplash
James Clear 在 《原子习惯》 中写了以上内容。诚然——这本书旨在阐明完全不同的内容。但与运营指标的想法是相同的:你的目的地是你的战略,指标将帮助你保持在正确的路径上。如果你没有正确设置你的指标,维持你设定的目标将会很困难。
指标:它们是什么以及我们为什么需要它们
简而言之,衡量指标让你能够量化某事。 你可以为任何事情构建一个指标,这实际上是指标设计工作中最大的挑战之一(数量 <> 质量)。
衡量指标让你了解‘一个过程’的表现——它们帮助你了解历史演变,它们让你进行基准测试,在‘领先指标’的情况下,它们提供对未来表现的早期预测
但最重要的是,衡量指标的真正力量在于它们的对齐。它们提供了组织内部的共同语言和共同视角。它们围绕一个共同目标进行联合。这也是为什么拥有正确的指标可能很棘手:在某些情况下,一旦指标被定义——它们往往会成为目标,团队将试图推动它。
当这种情况发生时——这通常是你开始看到指标可能存在/存在的不同问题时。
图片来源于 Pierre Bamin 在 Unsplash
衡量指标定义的常见挑战
定义度量指标伴随着许多挑战——但有两个挑战让我特别警觉,每当你在这样的项目中工作时,都需要格外小心。
这些指标可能会激励错误的行为。
指标可能会产生意想不到的后果,这些后果可能与公司的整体目标不一致。仔细考虑指标对行为的潜在影响,并确保它们的设计能够激励正确的行为和结果是非常重要的。
比如,假设减少通过电子邮件打开的支持票数是你团队的首要任务。一种解决方案可能是尽可能让联系电子邮件支持变得困难。例如,将你的支持电子邮件地址“隐藏”在你网站的随机页面上,使其非常复杂,并以.png 格式展示,以便人们必须手动重新输入。这样做可能会降低联系数量,但这可能会产生其他意想不到的后果(例如,增加“负面”社交媒体互动)。
这并不一定是因为人们想要操控系统——更可能是因为人们未必对组织及其所有内容有完全的理解。一旦某人的绩效与指标挂钩——那么他们试图改变该指标是合情合理的,因此,指标设计者必须确保“游戏规则”被清晰地阐明。
有许多方法可以解决这个问题,但这些方法也有其自身的挑战:
-
你可以设计一个配对指标,即另一个旨在“强制”某种行为的指标(例如,与注册数配对的指标可以是流失率)。当你开始有太多指标,并且这些指标并不一定指向相同方向时,可能会出现一些挑战——从噪音中解读信号,决定该做什么可能会变得困难。
-
你可以设计一个复合指标,考虑多个因素,以确保它不能被轻易操控。在《可靠的在线控制实验》(一本我推荐给任何对 A/B 测试感兴趣的人的好书,别被书名吓到!)中,作者解释了每当你进行实验时,应该构建“客观实验标准”(OEC)。OEC 是一个复合指标,考虑到你的实验应当影响的指标和你不希望实验负面影响的指标(例如成本指标、警戒指标、健康指标)——这最终将允许你创建一个二元决策规则,以决定你的实验是否成功。这个概念非常有趣,对于实验来说效果很好。说到这里,复合指标在跟踪过程时可能很难使用——你需要理解其底层逻辑,才能理解指标的变化并进行调试——因此你最终会跟踪所有构成复合指标的不同指标。
最终,这项任务落在了度量指标设计师的肩上,确保设置了正确的检查,以保证指标的变动与公司的目标一致。
这个指标具有欺骗性
想象一下:你是一个在大电子商务公司工作的分析师。某处有人发现交易数量与整体收入之间有很大相关性。根据这项研究,一位副总裁决定应对交易数量设定一个 OKR,并要求不同的团队去推动这个 OKR。
团队开始进行一些活动来增加交易数量(重新针对以前的客户、提供折扣等)。他们取得了成功——但目前还不清楚这对收入的影响如何。
不幸的是,后续分析揭示了公司大部分收入实际上来自于“高价值”商品,而交易数量的增加主要发生在“低价值”商品上——最终未能对收入产生任何实质性影响。
更清楚地说,团队确实增加了交易数量,但这并没有转化为收入。
在这种情况下,指标是具有欺骗性的。它未能考虑到业务的一个重要方面:并不是所有交易都能带来收入。这对公司来说成本相当高:公司建立了一个指标和一个报告基础设施,向所有不同的利益相关者进行沟通并解释了这些指标,这些利益相关者不得不改变他们的操作来推动该指标,公司还必须进行多项研究以理解其潜在影响,等等。总之,投入很大,而回报却不多。
一种为运营团队定义度量指标的简单方法
指标是一个很好的工具,但设计它们会面临许多挑战,并且通常不是一项容易的工作。但为了帮助,有几个框架存在。特别是“输入 > 输出 > 结果”框架,我将在下段中讨论。为了使话题不那么枯燥,我将以高中生教育项目为例,展示如何使用输入 > 输出 > 结果框架来定义指标,从而使该项目取得成功!
快速免责声明: 就像任何框架一样,它并不一定适用于所有情况 / 它也不一定是你业务的最佳选择。最终,框架只是一个帮助决策的工具——你不应该盲目遵循,而是应该根据自己的情况进行调整。
输入是你可以控制的
-
这就是 ROI 中的“I”
-
这就是你带到桌面上的:你在任务上花费的时间、生产某物使用的材料量等。
-
这应该完全在你的控制之下
在我们的例子(教育项目)中,教师人数、他们的资历、资金等都可能是我们系统的输入指标。
输出由你的输入驱动
-
输出直接跟随输入:如果你从漏斗的角度思考,输出是输入后的漏斗的下一步
-
这些可以通过你的输入来改变:如果输入增加(或减少),输出也会相应地变化。它们非常可操作。
-
他们对你的活动反应迅速,这意味着当输入增加或减少时,输出也会相应地变化。然而,它们并不完全在你的控制之下。
-
你的输出和结果之间有因果关系
这些指标通常是最难定义的。它们正好处于你的输入和结果之间,但定义这个‘之间’的确切位置可能会变得棘手——因为你要保持可操作性,同时也要考虑因果关系。
就像生活中的许多事情一样,一切都关于平衡。在我们的教育项目例子中,学生的成绩、他们的一致性、他们随时间的进步等都可能是我们的输出。
结果是北极星
结果对你来说是最重要的——是你通过所有活动想要推动的东西。它们是你业务健康的主要指标。它们代表了驱动你和你的团队的“WHY”。推动结果通常比输出更困难,需要多个输出的“帮助”,并且需要一些时间。虽然输出是你应在“日常”中跟踪的指标,但结果是你在一段时间后希望实现的目标。
在我们的例子中,高中毕业生人数可能是北极星。
这里是最终流程:教师人数、他们的平均资历、学校的资金(输入)帮助推动学生的成绩及其随时间的一致性更高(输出),这最终会导致更多学生顺利毕业(结果)。
验证指标
无论是否使用此框架——一旦新的指标被设计出来,验证它们并确保它们通过几个测试是很重要的:
确保指标正确地代表现象
第一步是确保你考虑使用的指标能够正确地代表你试图评估的现象。这高度依赖于你的业务/活动/公司,这里没有秘密公式。
“打开邮件”是否是“潜在客户阅读了我们的沟通”的良好代理?成绩是否是知识的良好代理?一般来说,想法是选择某种测量方法,并确保指标是可靠的(即测量是值得信赖的)和准确的(即它正确地描述了它应该描述的现象)。
确保指标分类明确,并且与其创建原因对齐
输入指标应该在你的直接控制范围内/直接可操作。输出指标应该直接“跟随”你的输入指标,即应该清楚地说明增加一个单位的输入会改变多少输出。然后,你的结果和输出之间应该有因果关系。如果你的输入不一定流入你的输出,或者最终你的输出似乎对你的结果没有任何影响,那么系统实际上并没有正常运作,你有可能推动错误的方向。
关于最后一点——这是最难证明的事情之一:你输出和结果之间的因果关系。根据你愿意承担的‘风险’级别(以及你手头拥有的工具),可能需要你进行一些实验——在正确定义你应该推动的结果之前。
确保指标不会激励“错误行为”
如前所述,你不想激励错误的行为。你不想让你的支持人员减少支持票据而不关心客户满意度。你不想推动你的销售人员进行销售而不关心留存率。
这里的想法是利用这一步来思考如何“游戏”指标的最坏方式——并据此构建配对指标,即阻止任何人采取可能对你的业务产生负面影响的“最小阻力路径”的次要指标。
如果你测量一些数量(例如销售额),你可能还需要确保测量一些质量指标(例如留存率)。如果你在短期内进行测量,你可能还需要确保在长期内也进行测量。在高效能管理一书中,安迪·格罗夫以类似的方式讨论了“配对指标”:
“指标往往会引导你的注意力集中在它们所监测的内容上。这就像骑自行车:你可能会把自行车转向你所看的方向。例如,如果你开始仔细测量你的库存水平,你可能会采取措施降低库存水平,这在一定程度上是好的。但你的库存可能变得过于精简,以至于无法应对需求变化而不造成短缺。因此,由于指标引导了一个人的活动,你应该防止过度反应。你可以通过配对指标来做到这一点,这样两个效果和反效果都会被测量。”
结束语:你应该将精力集中在哪里?
根据杰夫·贝索斯的说法,你应该专注于输入。其他一些商界人士也有相同的想法(例如,基思·拉博伊斯有一句有趣的名言:“为了赢得一场足球比赛,你不是专注于进球,而是专注于训练团队”)。
我个人分享这种想法,但我想提供一个更细致的观点,而且现在感觉你需要在文章中至少提到一次 chatGPT——我问了 chatGPT 它对这件事的“看法”。
简而言之——“这要看情况”。
希望你喜欢这篇文章!你有什么想分享的建议吗?在评论区告诉大家吧!
如果你想阅读更多我的文章,这里有一些你可能会喜欢的其他文章:
增强你对结果的信心,打造更强的个人品牌
[towardsdatascience.com ## 如何建立一个成功的仪表盘
来自那些曾经建立过几个不成功项目的人的检查清单
[towardsdatascience.com [## 如何…选择要从事的数据项目
如果你对如何使用时间有合理的方法,你可以优化你产生的价值。
通过可操作的分析驱动产品影响
原文:
towardsdatascience.com/driving-product-impact-with-actionable-analyses-d72430684908
作为分析师如何实现有影响力的产品变化
·发表于 Towards Data Science ·8 分钟阅读·2023 年 12 月 1 日
–
“哦,这很有趣,谢谢你调查这个问题。” —— 当你的利益相关者查看你的分析后,可能会这样说,然后转身继续正常业务。听起来很熟悉吗?这里有一个分析模板,可以确保你的分析带来可操作的结果,让你的利益相关者愿意采取行动。
图片来源 Unsplash
免责声明:我倾向于在文章中使用实际例子进行说明。在这篇文章中,我将引用 Spotify 有声书。 所有数据点均为虚构。
定义可操作性
进行分析可能是一个漫长而繁琐的过程。寻找相关数据,整理数据,提取有用信息,然后将所有内容总结成一个可呈现的格式可能需要数周时间。投入越多的工作,如果分析没有导致任何业务决策,可能会越令人沮丧。
要使分析具有影响力,它不仅需要及时进行,以免相关决策已作出,还必须包括切实的、可操作的见解,提供明确的下一步和易于利益相关者评估的选项。
可操作的见解不仅提供了一个可能有趣的特定数据点,还清晰地阐述了这个见解如何与当前问题相关,可能产生的影响,以及可以采取的选项和下一步行动及其相关的好处/风险。
我们来看一个例子,以 Spotify 的一个功能 有声书:
-
不可操作的: 25 岁以下的用户几乎不使用有声书。 → 这好还是不好?他们是否应该听有声书,我们是否需要对此采取措施?
-
可操作性: 25 岁以下的用户几乎不使用有声读物,因为他们从未在应用中探索这个功能。然而,听有声读物的用户留存率高出 20%。 → 这些信息告诉我们,有声读物代表了一个潜在的机会,可以提高年轻用户的留存率,但似乎还有更多工作需要完成,以鼓励用户探索这个功能。
这听起来合理,但在实际深入数据时并不总是那么清晰和直接。以下框架提供了一个良好的结构,以分解手头的问题空间,使你的分析能够提供可操作的洞察。
实现可操作分析的 4 个步骤
分析应通常围绕团队试图解决的具体业务问题进行:我们如何提高留存率?我们如何鼓励更多用户完成转化漏斗的最后一步?是什么阻碍了用户邀请他们的朋友?
从这个初始陈述出发,我们可以将分析分解为四个领域的层次结构,逐步进行:
1) 问题陈述:待解决的高层次业务问题
2) 机会领域:与当前问题有强关联的领域或问题
3) 杠杆:针对机会领域的不同工作方式
4) 实验 [可选]:具体实施的某个杠杆,用于帮助验证或反驳我们的假设。虽然我认为这一方面是可选的,但我总是发现通过具体的构思来传达建议和推荐会很有帮助。
作者提供的图片
解决业务关键问题
理想情况下,分析直接与一个关键业务问题相关,比如提高转化率、平均订单价值等。然而,这并不一定是必须的。也可以专注于一个指标或问题,只要有强假设(或者更好的证据)表明这些与业务结果高度相关。
在 Spotify 的情况下,可以调查的问题陈述可能是:我们如何在 Spotify 应用中增加付费用户的每日听音时间? 假设是每日听音时间与付费用户的留存率有很强的关联,从而影响到月收入。
找到解决当前问题的机会领域
机会领域通常通过深入的描述性分析来识别。例如,这里可能有必要研究并学习那些每日听音时间较长的用户行为,以便鼓励其他用户采取相同的行为。发现的内容可能包括使用自动生成播放列表的用户每日听音时间高出 x%,或订阅了至少 3 个播客的用户每日听音时间比没有订阅的用户高出 x%。
回到有声书的例子,另一种见解可能是,使用有声书的用户在应用中花费的时间显著更多。所有这些都是我们可以利用的机会领域,以鼓励其他用户以类似的方式进行操作,从而达到增加留存率的目标。
确定可操作的机会领域杠杆
对于每个机会领域,我们通常有多个不同的杠杆可以用来处理这个特定领域。每个杠杆的目标是推动机会领域的进展,以便我们可以验证在该领域的工作是否真正对主要问题产生了积极的结果。
首先,将机会领域转化为更正式的陈述可能是有帮助的:
将 25 岁以下用户使用有声书的比例从 x%提高到 y%。
用户不使用特定功能可能有多个原因,每个原因代表一个可能的杠杆:用户是否看不到该功能?用户是否看到但没有使用该功能?用户是否使用了该功能但在短时间内就中止了?
每个这些因素都可能直接影响当前的问题,同时需要非常不同的解决方案,从应用的用户体验和设计的改进,到库存问题(在这种情况下是可用的有声书)再到向用户展示的推荐。同样,在这种情况下,只有数据才能告诉我们真实情况,因此需要进一步分析。
一个可能的发现是:25 岁以下的用户对首页的参与度较低,而首页是唯一推广有声书的屏幕,因此他们没有看到应用中的这一功能,从而导致使用率和参与度低。
通过这个方法,在应用程序中增加有声书的显著性是提高 25 岁以下用户参与度的一个可能杠杆。请记住,通常有多个杠杆与特定问题相关联,应该根据不同的标准如覆盖范围和努力程度来优先排序。
拥有一个具体的、基于数据的杠杆列表,供团队处理特定问题,是分析最有价值的结果。锦上添花的是对每个杠杆的结构化优先级排序——我们在哪些方面看到最大的潜在影响,因此应该优先投资在哪里?杠杆的大小是一门艺术,我不会在此深入探讨,但良好的优先级排序对使分析结果更加可操作至关重要。
制定具体实验的策略
一旦我们对机会领域和解决该领域的最大杠杆有了充分了解,我们可以开始制定具体的产品更改计划,并进行实施和测试。这可以是分析的一部分,有助于传达具体的建议,也可以是与团队或利益相关者进行下一步的良好方式(这也是获得对你想法支持的好方法)。
回到有声书的例子,可能有很多不同的方式来增加在应用内对该功能的可见性:例如,在打开应用时使用一个宣传该功能的横幅,将有声书添加到主屏幕之外的其他屏幕等。虽然在设计特定变更时有相当大的自由度,但重要的是它实际上影响我们试图解决的问题(在这种情况下,是年轻用户未看到有声书功能)。
为了确保我们始终关注目标,围绕我们希望进行的可能实验制定假设是关键。一个例子可能是:
我们预测,当应用打开时添加一个宣传有声书的横幅 [实验变更] 将会增加年轻用户的日常听书时间 [问题],因为更多年轻用户将看到并听有声书 [杠杆]。当我们看到年轻用户使用有声书的增加,随后年轻用户的日常听书时间也有所增加 [验证指标],我们就能确认这一点。
作者提供的图片
这样的假设有助于在始终保持预期结果的前提下设计实验。此外,它有助于验证或否定一个想法:如果我们没有看到预期输出(听书时间增加)的增长,同时我们的杠杆(有声书使用)也没有显示任何变化,那么实验的处理可能是一个不好的选择,我们可以尝试其他方法。然而,如果我们看到用户使用有声书(杠杆)的显著增加,但日常听书时间(主要问题)没有变化,那么这个杠杆被否定,我们可以继续下一个。
得出洞察和推动决策
使用这个分析模板有助于保持对当前主题的关注,将分析结果直接与关键业务问题相连接。以杠杆和实验的形式提供的下一级详细信息进一步明确了如何处理发现的问题的具体行动。
总的来说,一个理想的结果可能看起来如下概览(理想情况下,优先级应具有具体的数字,例如目标指标的预期增加,然而通常表格中的 T 恤尺码足够)。
作者提供的图片
这提供了一份清晰的、优先级排序的杠杆清单,团队可以根据他们的能力进行选择。鉴于这些建议各自关联的明确潜在影响,您的利益相关者将很难找借口不采取行动,从而在下一次分析结果展示时错失潜在的影响。
结论
作为分析师推动产品影响不仅仅是提供有洞察力的分析;它还需要一种战略性的方法,以确保你的发现转化为可操作的改变。通过采用从问题陈述、机会领域和杠杆到实验的四步过程,分析师可以将他们的工作与关键业务目标对齐。
这有助于使分析对利益相关者更加相关和引人注目,以便下次当你的利益相关者审查你的分析时,它不仅仅是一个简单的确认,而是成为推动有意义行动和产品及业务积极成果的催化剂。
任何时间序列模型的动态符合区间
原文:
towardsdatascience.com/dynamic-conformal-intervals-for-any-time-series-model-d1638aa48527
使用回测应用和动态扩展一个区间
·发布于 Towards Data Science ·8 分钟阅读·2023 年 4 月 17 日
–
照片由 Léonard Cotte 提供,来源于 Unsplash
根据生成预测的目的,评估准确的置信区间可能是一个关键任务。大多数经典计量经济模型基于对预测和残差分布的假设,内置了这样的机制。当转向机器学习进行时间序列分析时,例如使用 XGBoost 或递归神经网络,情况可能会更复杂。一种流行的技术是符合区间——一种量化不确定性的无假设预测分布的方法。
简单的符合区间
这种方法的最简单实现是训练一个模型并保留一个测试集。如果这个测试集至少有 20 个观察值(假设我们希望 95%的确定性),我们可以通过在任何点预测上添加一个正负值来构建一个区间,该正负值代表测试集残差绝对值的第 95 百分位数。然后,我们在整个序列上重新拟合模型,并将这个正负值应用于未知范围内的所有点预测。这可以被看作是一个简单的符合区间。
Scalecast 是一个 Python 预测库,如果你想在应用优化的机器学习或深度学习模型之前对序列进行转换,然后轻松恢复结果,它表现良好。虽然其他库为 ML 模型提供了灵活和动态的区间,我不确定它们是否能够有效处理需要转换然后恢复的数据,特别是涉及差分的情况。如果我错了,请纠正我。
我专门为这个目的创建了 scalecast。使用该包变换和逆变换系列非常简单,包括使用交叉验证来寻找最佳变换组合的选项。然而,在差分水平上应用置信区间(任何区间)在映射到系列的原始水平时变得复杂。如果你只是以与点预测相同的方式逆差分区间,它很可能会过于宽泛。避免这种情况的建议是使用不需要平稳数据的模型——如 ARIMA、指数平滑等。但如果你真的想比较 ML 模型,而你的数据不是平稳的,那就没那么有趣了。
Scalecast 使用的解决方案是上述简单一致性区间。如果对系列进行第一次、第二次或季节性差分,然后进行逆操作,再计算测试集残差并在其上应用百分位函数是很简单的。我使用一种称为 MSIS 的度量评估了该区间的有效性,详细信息见过去的帖子。
pip install --upgrade scalecast
但这还可以更好。在时间序列中,当误差积累时,人们直观地认为,对于一个在时间上远离基准真实值的点预测,其区间将进一步扩展。预测我明天会做什么比预测一个月后的事情更容易。这种直观的概念已被纳入计量经济学方法中,但在简单区间中却不存在。
我们可以通过几种方式来解决这个问题,其中之一是一致性分位数回归,例如由Neural Prophet使用。这种方法可能有一天会被纳入 scalecast。但我将在这里概述的方法涉及使用回溯测试和根据每次回溯测试的残差应用百分位数。与采用假设不同,该方法将一切基于一些观察到的经验真相——实施模型与其实施的时间序列之间的真实关系。
回溯一致性区间
为此,我们需要将数据拆分成多个训练集和测试集。每个测试集需要与我们期望的预测范围长度相同,分割的数量应至少等于 1 除以 alpha,其中 alpha 是 1 减去期望的置信水平。同样,这将导致 95%置信度区间的 20 次迭代。考虑到我们需要通过整个预测范围的长度迭代 20 次或更多次,较短的序列可能在此过程完成之前就用尽观测值。一个缓解的方法是允许测试集重叠。只要测试集之间至少有一个观测值的差异,并且没有数据从任何训练集中泄漏,这样应该是可以的。这可能会使区间偏向于较新的观测值,但如果序列包含足够的观测值,可以选择在训练集之间增加更多空间。我解释的过程称为回测,但也可以视为修改版时间序列交叉验证,这是一种常见的方式来促进更准确的符合区间。Scalecast 通过管道和三个实用函数使获得这个区间的过程变得简单。
构建完整模型管道
首先我们构建一个管道。假设我们需要差分数据并使用 XGBoost 模型进行预测,管道可以是:
transformer = Transformer(['DiffTransform'])
reverter = Reverter(['DiffRevert'],base_transformer=transformer)
def forecaster(f):
f.add_ar_terms(100)
f.add_seasonal_regressors('month')
f.set_estimator('xgboost')
f.manual_forecast()
pipeline = Pipeline(
steps = [
('Transform',transformer),
('Forecast',forecaster),
('Revert',reverter)
]
)
重要的是要注意,这个框架也可以应用于深度学习模型、经典计量经济模型、RNN,甚至是简单模型。对于你想要应用于时间序列的任何模型,这都适用。
接下来,我们使用fit_predict()
方法生成 24 个未来观测值:
f = Forecaster(
y=starts, # an array of observed values
current_dates=starts.index, # an array of dates
future_dates=24, # 24-length forecast horizon
test_length=24, # 24-length test-set for confidence intervals
cis=True, # generate naive intervals for comparison with the end result
)
f = pipeline.fit_predict(f)
回测管道并构建残差矩阵
现在,我们进行回测。对于 95%区间,这意味着至少需要 20 次训练/测试分割,迭代地向后移动通过最新观测。这是过程中的计算最昂贵部分,具体取决于我们想通过管道发送多少模型(我们可以通过扩展forecaster()
函数来增加更多),是否想优化每个模型的超参数,以及是否使用多变量过程,可能会花费一些时间。在我的 Macbook 上,这个简单的管道在 20 次迭代下回测时间略超过一分钟。
backtest_results = backtest_for_resid_matrix(
f, # one or more positional Forecaster objects can go here
pipeline=pipeline, # both univariate and multivariate pipelines supported
alpha = 0.05, # 0.05 for 95% cis (default)
bt_n_iter = None, # by default uses the minimum required: 20 for 95% cis, 10 for 90%, etc.
jump_back = 1, # space between training sets, default 1
)
从这个函数得到的回测结果可以用于多种目的。我们可以用它们来报告模型的平均误差,或者从中获取许多样本外预测的见解,或者用它们来生成区间。要生成区间,我们需要:
backtest_resid_matrix = get_backtest_resid_matrix(backtest_results)
这会为每个评估的模型创建一个矩阵,其中一行代表每次回测迭代,一列代表每个预测步骤。每个单元格中的值是预测误差(残差)。
作者提供的图片
通过应用列级百分位函数,我们可以生成上下值,以找到每个预测步骤的绝对残差的第 95 百分位数。平均来说,随着预测的延伸,这个值应该更大。在我们的例子中,第 1 步的上下值为 15,第 4 步为 16,第 24 步(最后一步)为 46。并非所有值都比上一个值大,但通常是的。
图片由作者提供
构建回测区间
然后我们用新的动态区间覆盖了过时的简单区间。
overwrite_forecast_intervals(
f, # one or more positional Forecaster objects can go here
backtest_resid_matrix=backtest_resid_matrix,
models=None, # if more than one models are in the matrix, subset down here
alpha = .05, # 0.05 for 95% cis (default)
)
看!我们为时间序列模型构建了一个无需假设的动态拟合区间。
这个区间比默认区间好多少?使用MSIS,一种不为很多人所知或使用的度量方法,我们可以评估每个获得的区间在此过程前后的效果。我们还可以使用每个区间的覆盖率(实际观察值落在区间内的百分比)。我们为此目的预留了一部分与之前评估的测试集不重叠的数据。简单区间如下:
图片由作者提供
结果是一个准确的预测,具有紧凑的区间。它包含了 100%的实际观察值,MSIS 得分为 4.03。从我对 MSIS 的有限使用经验来看,低于 5 通常是相当不错的。我们应用动态区间,得到如下结果:
图片由作者提供
这很好。我们有一个扩展的区间,其平均值比默认区间更紧凑。MSIS 得分略微提高至 3.92。坏消息是:24 个测试集观察值中有 3 个超出了这个新区间的范围,覆盖率为 87.5%。对于 95%的区间来说,这可能并不理想。
结论
当然,这只是一个例子,我们应谨慎得出过于宽泛的结论。我相信回测区间几乎总会扩展,这使得它比默认区间更直观。它可能在平均上也更准确,只是获得它需要更多的计算能力。
除了获得新的区间,我们还获得了回测信息。在 20 次迭代中,我们观察到了以下误差指标:
图片由作者提供
相比仅使用一个测试集的误差,我们对报告这些结果感到更有信心。
感谢您的关注!如果您喜欢这些内容,请在 GitHub 上为scalecast点个赞,并查看与本文相关的完整笔记本。使用的数据可以通过FRED公开获取。
## GitHub - mikekeith52/scalecast: 实践者的预测库
Scalecast 帮助你预测时间序列。以下是如何初始化其主要对象:Uniform ML 建模(包括模型…
在 Power BI 中使用字段参数进行动态过滤
原文:
towardsdatascience.com/dynamic-filtering-with-field-parameters-in-power-bi-520a472127d5
字段参数太棒了!了解如何使用这一功能进行数据建模,并让用户能够完全自由地选择数据的显示方式!
·发布于 Towards Data Science ·阅读时长 5 分钟·2023 年 4 月 12 日
–
图片由作者提供
如果你定期关注我的文章,你可能已经注意到我对字段参数非常热衷。这个功能在 2022 年 5 月推出,显著减少了处理一些常见业务场景时的复杂性和开发工作量。
我已经写过如何利用 字段参数让你的 Power BI 报告更生动。然而,这不仅仅是关于数据可视化,因为这个功能也可以非常优雅地解决一些数据建模挑战。
在我展示一个我最近实现的极其实用的字段参数用例之前,让我们首先解释一下什么是字段参数,以及一旦你开始使用这个功能,幕后发生了什么。
简而言之,字段参数使你能够执行两个操作:
1. 动态更改用于切片和切块数据的属性 —— 意思是动态切换不同的列
2. 动态更改可视化中显示的指标 —— 意思是动态切换不同的度量值
我听见了,我听见了……尼古拉,我们以前也可以在没有字段参数的情况下做到这一点……是的,没错,但是 与 TREATAS 复杂性相比,或者编写 复杂冗长的 DAX SWITCH 语句,你现在只需点击几下而无需编写一行 DAX 代码就能完成所有设置!
探索内部机制
让我们快速看看字段参数创建的内部机制。
一旦你在字段参数窗口中拖动列和/或度量值,Power BI 将自动在你的数据模型中创建一个新表。你也可以选择自动创建一个包含字段参数值的切片器,并将其放置在报告页面上。
作者提供的图片
这个表格由三列内置列组成——一列对最终用户可见,而另外两列默认隐藏。
作者提供的图片
第一列,即将显示在切片器中的列名称,是对消费者暴露的标签。你可以在之后更改列的名称,而无需更改后台的整个逻辑。
这是可能的,因为表格中的第二列称为 Fields。这个列利用了 NAMEOF DAX 函数。NAMEOF 函数返回模型对象的完全限定名称。为什么这很重要?假设你想将列名从 Brand 更改为 Brand name…你可以做到这一点,而不会违反字段参数结构,因为 NAMEOF 函数仍会返回对象的新名称。显示名称保持不变,但它将引用一个具有不同名称的基础对象。
第三列是数字型的,表示字段参数中元素的顺序,从 0 开始。
好的,这三个 KPI 是在创建字段参数时自动提供的。然而,由于这只是一个表格,你也可以手动扩展这个表格,添加更多列。
如果你想知道这为什么有趣,请继续关注,我将向你展示如何利用这种“扩展性”来解决一些数据建模挑战。
设置场景
我的数据模型相当简单。我有一个包含各种保险产品数据的表格。对于每个产品,用户应该能够查看赚取和/或签发的保费(解释这两个 KPI 之间的区别超出了本文的范围)。此外,这些 KPI 中的每一个都可以以本币、欧元或美元显示:
作者提供的图片
这个想法是让用户能够根据不同的货币(欧元、美元、本币)和/或不同的 KPI(赚取与签发)来切片和筛选数据。
如果用户在货币切片器中选择了 EUR,则只应显示 EUR 中的 KPI。
在旧版客户端的解决方案中,这种动态选择是通过使用一些冗长的 DAX 来处理的:即 SWITCH 语句和 SELECTEDVALUE 函数的组合。这个解决方案不仅复杂,而且难以维护:假设你想要在范围内添加一种新货币,或者一个新的 KPI。你需要找到所有应用了该逻辑的度量值,并调整这些度量值的定义。
字段参数来拯救!
或者,你可以使用 Field 参数根据用户的选择过滤数据!让我们扩展之前创建的 Field 参数表:
作者提供的图像
第一个添加的列代表某个选项的货币,而第二列代表 KPI(保费类型)。我将在我的 Param_Premium 表中将这些列分别重命名为货币和保费类型。
现在,让我们打开模型视图,并建立维度表(货币和保费类型)与 Field 参数表(Param_Premium)之间的关系:
作者提供的图像
接下来,让我们从表格视觉中移除所有单独的列,并放入 Field 参数表中的“列”:
作者提供的图像
初看起来,它与之前的情况完全相同。但一旦我开始操作切片器…你看会发生什么!
作者提供的图像
运作得非常出色!
结论
Field 参数非常棒!这一功能不仅减少了许多 Power BI 常见任务的开发工作量,还丰富了 Power BI 报告的数据可视化方面——正如你所见,我们利用 Field 参数进行数据建模,并使报告用户在数据展示方式上拥有完全的灵活性。通过将数据模型扩展至 Field 参数逻辑,我们提供了动态控制报告页面数据的可能性。
感谢阅读!
从 Python 调用 R 进行动态预测组合
原文:
towardsdatascience.com/dynamic-forecast-combination-using-r-from-python-afcdf6adf85b
探索 rpy2 从 Python 调用 R 方法
·发表在 Towards Data Science ·6 分钟阅读·2023 年 1 月 25 日
–
图片由 Louis Hansel 提供,来源于 Unsplash
在这篇文章中,你将学习如何使用 rpy2 库从 Python 调用 R 方法。
我们将介绍一个与预测相关的示例。我们将定义并运行 R 函数,这些函数组合由基于 Python 的模型生成的预测结果。
介绍
即使 Python 是你首选的编程语言,R 有时仍然会有用。
我不想参与 Python 与 R 的辩论。现在我主要使用 Python。但是,许多优秀的方法仅在 R 中可用。重新从头实现这些方法实在是麻烦。
库 rpy2 满足了我们的需求。它允许你在 Python 中运行 R 代码。R 数据结构,如 matrix 或 data.frame,会被转换为 numpy 或 pandas 对象。将自定义 R 函数集成到你的 Python 工作流程中也很简单。
那么,rpy2 是如何工作的呢?
使用 Opera 的示例
我们将重点使用 R 包 opera。你可以使用这个包来组合预测结果。
在深入了解 rpy2 之前,让我们先回顾一下我们要解决的问题。
预测集成入门
通过结合多种不同的模型,集成方法提高了预测性能。
最常见的组合方式是使用简单的平均值。集成中的每个模型在最终预测中具有相同的重要性。但,一种更好的组合预测的方法是使用动态权重。因此,每个模型的权重会适应时间序列的变化。
Opera
动态预测组合的方法有很多种。你可以查看之前的一篇文章以获取不同方法的列表。
opera 有什么特别之处?
Opera 代表专家聚合的在线预测。一些最好的预测组合方法仅在这个 R 包中可用。它们包含有关预测组合最坏情况的有趣理论属性。这些属性对开发稳健的预测模型非常有价值。
你可以在这里找到 opera 如何工作的完整示例。
在本文的其余部分,我们将使用 opera 来组合 Python 模型的预测。
案例研究
像在上一篇文章中一样,我们将以能源需求时间序列作为案例研究。
这个示例包括三个步骤:
-
建立集合;
-
创建我们需要运行的 R 函数;
-
使用这些函数进行动态预测组合。
让我们逐步深入每一个步骤。
建立集合
首先,我们使用 Python 的 scikit-learn 方法建立一个集合。
下面是你可以这样做的方法:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.neighbors import KNeighborsRegressor
from sklearn.linear_model import Lasso, Ridge, ElasticNetCV
from pmdarima.datasets import load_taylor
# src module available here: https://github.com/vcerqueira/blog
from src.tde import time_delay_embedding
series = load_taylor(as_series=True)
series.index = pd.date_range(end=pd.Timestamp(day=27, month=8, year=2000), periods=len(series), freq='30min')
series.name = 'Series'
series.index.name = 'Index'
# train test split
train, test = train_test_split(series, test_size=0.1, shuffle=False)
# ts for supervised learning
train_df = time_delay_embedding(train, n_lags=10, horizon=1).dropna()
test_df = time_delay_embedding(test, n_lags=10, horizon=1).dropna()
# creating the predictors and target variables
X_train, y_train = train_df.drop('Series(t+1)', axis=1), train_df['Series(t+1)']
X_test, y_test = test_df.drop('Series(t+1)', axis=1), test_df['Series(t+1)']
# defining four models composing the ensemble
models = {
'RF': RandomForestRegressor(),
'KNN': KNeighborsRegressor(),
'LASSO': Lasso(),
'EN': ElasticNetCV(),
'Ridge': Ridge(),
}
# training and getting predictions
test_forecasts = {}
for k in models:
models[k].fit(X_train, y_train)
test_forecasts[k] = models[k].predict(X_test)
# predictions as pandas dataframe
forecasts_df = pd.DataFrame(test_forecasts, index=y_test.index)
我们创建了五个模型:一个随机森林,一个 K-最近邻,以及三个线性模型(Ridge、LASSO 和 ElasticNet)。这些模型以自回归方式训练。
这是它们预测的一个示例:
几个模型的预测。作者提供的图像。
现在,让我们使用 R 的 opera 来结合这些预测,使用 rpy2。我们将介绍这个库的两个有用的方面:
-
如何在 Python 中定义和使用 R 函数;
-
如何在这两种语言之间转换数据结构。
在 Python 中定义 R 函数
你可以在 Python 多行字符串中定义一个 R 函数:
import rpy2.robjects as ro
# polynomially weighted average
method = 'MLpol'
# defining the R function in a Python multi-line string
ro.r(
"""
define_mixture_r <-
function(model) {
library(opera)
opera_model <- mixture(model = model, loss.type = 'square')
return(opera_model)
}
"""
)
# storing the function in the global environment
define_mixture_func = ro.globalenv['define_mixture_r']
# using the function
opera_model = define_mixture_func(method)
包含函数的字符串传递给 rpy2.robjects 模块。然后,globalenv 方法使其在 Python 中可用。
你可以定义任何你想要的函数。注意,为了使其工作,系统中需要安装 R 及任何所需的 R 包。
关于上面示例中的函数。它用于创建一个 opera 对象(称为 mixture)。所需的参数是用于组合预测的方法。我们使用 MLpol,这是基于多项式加权平均的。
这里还有一些其他有用的替代方案:
-
EWA: 指数加权平均;
-
OGD: 在线梯度下降;
-
FTRL: 跟随正则化的领导者;
-
Ridge: 在线 Ridge 回归。
将数据从 pandas 转换为 R,反之亦然
这是我们需要的另一个函数:
from rpy2.robjects import pandas2ri
ro.r(
"""
update_mixture_r <-
function(opera_model, predictions,trues) {
library(opera)
for (i in 1:length(trues)) {
opera_model <- predict(opera_model, newexperts = predictions[i, ], newY = trues[i])
}
return(opera_model)
}
"""
)
update_mixture_func = ro.globalenv['update_mixture_r']
# activating automatic data conversions
pandas2ri.activate()
# using the function above
## predictions is a pandas DataFrame and trues is a pandas Series
## opera_model is a rpy2 object that represents a R data structure
new_opera_model = update_mixture_func(opera_model, predictions, trues)
# deactivating automatic data conversions
pandas2ri.deactivate()
函数定义与之前类似。但这个函数除了需要 opera_model(我们上面定义的)外,还需要额外的输入。我们需要传递一个 R data.frame(预测结果)和一个 vector(真实值)作为输入。
你可以使用 pandas2ri 在 Python 和 R 之间转换数据结构。这样,你可以传递一个 pd.DataFrame(预测结果)和一个 pd.Series(真实值)。rpy2 会自动进行转换。函数应用后,rpy2 会将结果转换回 Python 数据结构。
汇总
最后,让我们回到我们的案例研究中。
我将上述函数封装在一个名为 Opera 的 Python 类中。 你可以在我的 Github 上查看其代码。
以下是如何使用它的方法:
# https://github.com/vcerqueira/blog/blob/main/src/ensembles/opera_r.py
from src.ensembles.opera_r import Opera
opera = Opera('MLpol')
opera.compute_weights(forecasts_df, y_test)
ensemble = (opera.weights.values * forecasts_df).sum(axis=1)
下面是分配给每个模型的权重分布:
每个模型在集合中的权重分布。图片由作者提供。
这些权重随着时间变化以适应时间序列的动态:
随着时间推移,每个模型在集合中的权重。图片由作者提供。
关键要点
本文涉及两个主题:
-
在 Python 中使用 rpy2 库运行 R 代码;
-
使用 opera R 包进行动态预测组合。
我们使用 rpy2 在 Python 中定义和运行了多个 R 函数。我们专注于一个名为 opera 的特定包。不过,你可以定义和运行任何你想要的函数。
rpy2 还有很多其他内容。以下是文档链接:
opera 包对动态预测组合非常有用。其方法高效且提供了宝贵的预测性能理论保障。
感谢阅读,我们下次故事见!
相关文章
-
预测组合简介
-
如何组合预测结果
进一步阅读
[1] rpy2 文档: rpy2.github.io/doc/v3.5.x/html/
[2] Opera 文档: cran.r-project.org/web/packages/opera/vignettes/opera-vignette.html
Kubernetes 中的动态 MIG 分区
最大化 GPU 利用率并降低基础设施成本。
·
关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 1 月 26 日
–
为了减少基础设施开支,使用 GPU 加速器的高效方式至关重要。一种实现方法是将 GPU 划分为更小的分区,即切片,以便容器仅请求必要的资源。一些工作负载可能只需要 GPU 计算和内存的一小部分,因此在 Kubernetes 中能够将单个 GPU 分成多个切片,并允许个别容器请求这些切片是非常重要的。
这对用于运行人工智能 (AI) 和高性能计算 (HPC) 工作负载的大型 Kubernetes 集群特别相关,因为 GPU 利用效率低下可能对基础设施费用产生重大影响。这些低效通常是由于没有充分利用 GPU 的轻量级任务,如推理服务器和用于初步数据和模型探索的Jupyter Notebooks。
例如,欧洲核子研究组织 (CERN)的研究人员发布了一篇博客文章,介绍了他们如何使用 MIG GPU 分区来解决由波动性工作负载运行高能物理 (HEP) 模拟和代码效率低下导致的低 GPU 利用率问题。
NVIDIA GPU Operator 支持在 Kubernetes 中使用 MIG,但仅凭它不足以确保高效的 GPU 分区。在本文中,我们将探讨原因,并提供在 Kubernetes 中使用 MIG 的更有效解决方案:动态 MIG 分区。
Kubernetes 中的 MIG 支持
Kubernetes 中的 MIG 支持由NVIDIA 设备插件提供,该插件允许将 MIG 设备(即隔离的 GPU 分区)暴露为通用的nvidia.com/gpu
资源或特定资源类型,例如nvidia.com/mig-1g.10gb
。
通过nvidia-smi
手动管理 MIG 设备是不切实际的,因此 NVIDIA 提供了一种名为nvidia-mig-parted的工具。该工具允许集群管理员声明性地定义节点上所有 GPU 所需的 MIG 设备集。该工具自动管理 GPU 分区状态,以匹配所需的配置。例如,以下是从 nvidia-mig-parted GitHub 存储库中提取的配置示例:
version: v1
mig-configs:
all-disabled:
- devices: all
mig-enabled: false
all-enabled:
- devices: all
mig-enabled: true
mig-devices: {}
all-1g.5gb:
- devices: all
mig-enabled: true
mig-devices:
"1g.5gb": 7
all-2g.10gb:
- devices: all
mig-enabled: true
mig-devices:
"2g.10gb": 3
all-3g.20gb:
- devices: all
mig-enabled: true
mig-devices:
"3g.20gb": 2
在 Kubernetes 中,集群管理员通常不会直接使用 nvidia-mig-parted,而是通过NVIDIA GPU Operator来使用它。
这个操作程序进一步简化了 MIG 配置的应用。创建定义了一组允许的 MIG 配置的 ConfigMap 之后,NVIDIA GPU Operator 只需要你用nvidia.com/mig.config
标记节点,并指定作为值你想在该节点上应用的具体配置的名称。
例如,参考上述定义的配置,我们可以将配置all-3g.20gb
应用于节点node-1
,如下所示:
kubectl label nodes node1 "nvidia.com/mig.config=all-2g.20gb"
静态 MIG 配置会导致较差的可用性
NVIDIA GPU Operator 有一个显著的限制:MIG 设备是通过静态配置创建的。
这意味着集群管理员必须首先定义他们认为可能需要的所有 MIG 配置,然后根据需要手动将其应用于每个节点。
这种管理 MIG 设备的方式很容易导致 GPU 利用率低下以及集群管理员花费大量时间更改 MIG 配置。实际上,GPU 内存和计算需求因 Pod 而异,并且会随着时间变化。为了在创建具有不同 MIG 资源请求的新 Pod 时实现最佳 GPU 利用率,集群管理员必须不断花时间寻找和应用最合适的配置,这对每个节点来说非常不切实际。
作为一个简单的例子,假设我们需要调度多个需要 20GB GPU 内存的 Pods。因此,我们将创建并应用一个配置,该配置在集群中的所有 GPU 上提供 nvidia.com/mig-3.20gb
配置,因为它可以完美地利用所有 GPU 资源。然而,稍后,服务器收到创建一些需要较少资源的 Pods 的请求,例如 10GB GPU 内存,对应于 MIG 配置 nvidia.com/mig-2g.10gb
。这些 Pods 直到集群管理员更改至少一个节点的标签并应用提供所请求配置的 MIG 配置之前都不会被调度。
问题并没有在这里结束。虽然某个配置可能提供所需的 MIG 资源,但它同时可能会移除一些当前由容器使用的设备。在这种情况下,集群管理员需要寻找或创建最合适的配置,并确保不会删除任何正在使用的设备,从而引入显著的操作成本。
这种方法根本无法扩展。仅靠 NVIDIA GPU 操作员,不可能根据工作负载需求不断调整 MIG 配置,从而导致未使用的 MIG 设备和待处理的 Pods。
让我们看看如何通过动态 MIG 分区解决这个问题。
动态 MIG 分区
动态 MIG 分区根据集群中工作负载的实时需求自动创建和删除 MIG 配置,确保始终将最佳的 MIG 配置应用于可用的 GPU。
要应用动态分区,我们需要使用 [nos](https://github.com/nebuly-ai/nos)
,这是一个开源模块,它与 NVIDIA GPU 操作员一起运行,使 MIG 分区变得动态。
你可以把 nos
看作是 GPUs 的 集群自动扩展器:它不是增加节点和 GPU 的数量,而是动态地分区它们以最大化其利用率,从而导致闲置的 GPU 容量。然后,你可以调度更多的 Pods 或减少所需的 GPU 节点数量,从而降低基础设施成本。
有了
*nos*
,无需手动创建和管理 MIG 配置。只需将你的 Pods 提交到集群,请求的 MIG 设备将自动配置。
让我们深入了解 nos
和动态 MIG 分区在实际中的工作方式。
先决条件
如前所述,nos
并不会替代 NVIDIA GPU Operator,而是与其一起工作。因此,你需要首先安装它,并满足两个要求:
-
mig.strategy
必须设置为mixed
,这样每个不同的 MIG 配置文件就会作为特定的资源类型暴露给 Kubernetes。 -
migManager
必须被禁用
如果尚未完成,你可以通过 Helm 安装 NVIDIA GPU Operator,如下所示:
helm install --wait \
--generate-name \
-n gpu-operator \
--create-namespace nvidia/gpu-operator \
--set mig.strategy=mixed \
--set migManager.enabled=false
默认情况下,MIG 模式在 NVIDIA GPU 上未激活。因此,首先你需要在所有你希望进行分区的 GPU 上启用 MIG。你可以通过 SSH 进入节点并为每个 GPU 运行以下命令,其中 <index>
对应于各自的索引:
sudo nvidia-smi -i <index> -mig 1
根据你使用的机器类型,可能需要在此操作后重启节点。有关更多信息和故障排除,请参阅 NVIDIA MIG 用户指南。
安装
一旦你安装了 NVIDIA GPU Operator 并启用了你的 GPU 上的 MIG 模式,你可以简单地按如下方式安装 nos
:
helm install oci://ghcr.io/nebuly-ai/helm-charts/nos \
--version 0.1.0 \
--namespace nebuly-nos \
--generate-name \
--create-namespace
就是这样!现在你可以在你的节点上激活动态 MIG 分区。
动态分区操作中
首先,你需要向 nos
指定它应该管理哪些节点的 GPU 分区。将这些节点标记如下:
kubectl label nodes <node-names> "nos.nebuly.com/gpu-partitioning=mig"
这个标签将节点标记为“MIG 节点”,将所有节点 GPU 的 MIG 设备管理委托给 nos
。
之后,你可以提交请求 MIG 资源的工作负载。nos
将自动找到并应用你之前标记为“MIG 节点”的节点上的最佳 MIG 配置,创建 Pods 请求的缺失 MIG 设备,并删除不必要的未使用设备。
让我们看一个 nos
实际操作的简单示例。
假设我们操作一个简单的集群,其中一个节点有一个 NVIDIA A100 80GB。我们已经在该 GPU 上启用了 MIG 模式,因此我们可以为该节点启用自动分区:
kubectl label nodes aks-gpua100-24975740-vmss000000 "nos.nebuly.com/gpu-partitioning=mig"
kubectl describe node aks-gpua100–24975740-vmss000000
的输出显示节点没有任何可用的 MIG 资源,因为尚未请求或创建任何 MIG 设备:
Capacity:
cpu: 24
ephemeral-storage: 129886128Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 226783616Ki
pods: 30
Allocatable:
cpu: 23660m
ephemeral-storage: 119703055367
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 214314368Ki
pods: 30
让我们创建一些请求 MIG 资源的 Pods。在这种情况下,我们创建一个部署,包含 5 个 Pod 副本,每个 Pod 的容器请求 10 GB 内存的 GPU 切片。
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-1
namespace: demo
spec:
replicas: 5
selector:
matchLabels:
app: dummy
template:
metadata:
labels:
app: dummy
spec:
containers:
- name: sleepy
image: busybox:latest
command: ["sleep", "120"]
resources:
limits:
nvidia.com/mig-1g.10gb: 1
EOF
现在,命名空间 demo
中有 5 个待处理的 Pods,请求总共五个 nvidia.com/mig-1g.10gb
资源,而这些资源在集群中尚不可用:
❯ kubectl get pods -n demo
NAME READY STATUS RESTARTS AGE
deployment-1-f677f7555-7j7f6 0/1 Pending 0 5s
deployment-1-f677f7555-j9hdx 0/1 Pending 0 5s
deployment-1-f677f7555-lpg28 0/1 Pending 0 5s
deployment-1-f677f7555-lwpz5 0/1 Pending 0 5s
deployment-1-f677f7555-nj489 0/1 Pending 0 5s
几秒钟内,nos
将检测到这些待处理 Pods。它将尝试创建所请求的资源,选择最合适的 MIG 配置。在这个例子中,nos
应用了一种提供五个 1g.10gb 和一个 2g.20gb 设备的配置:
Capacity:
cpu: 24
ephemeral-storage: 129886128Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 226783616Ki
nvidia.com/mig-1g.10gb: 5
nvidia.com/mig-2g.20gb: 1
pods: 30
Allocatable:
cpu: 23660m
ephemeral-storage: 119703055367
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 214314368Ki
nvidia.com/mig-1g.10gb: 5
nvidia.com/mig-2g.20gb: 1
pods: 30
如果我们再次检查 Pods 的状态,可以看到这次它们现在处于运行状态:
❯ kubectl get pods -n demo
NAME READY STATUS RESTARTS AGE
deployment-1-f677f7555-7j7f6 1/1 Running 0 92s
deployment-1-f677f7555-j9hdx 1/1 Running 0 92s
deployment-1-f677f7555-lpg28 1/1 Running 0 92s
deployment-1-f677f7555-lwpz5 1/1 Running 0 92s
deployment-1-f677f7555-nj489 1/1 Running 0 92s
请注意,除了 1g.10gb 设备外,nos
还创建了额外的 2g.20gb 设备。这是因为每种 MIG GPU 模型仅支持 特定的配置集合,在这种情况下,满足所需设备的最佳配置也包括 2g.20gb 设备。请记住:
-
nos
选择允许调度最多待处理 Pods 的配置,这一计算是通过nos
内部调度器进行的调度模拟完成的。 -
已经在使用中的 MIG 设备不会被删除。任何需要删除这些设备的 MIG 配置都会被拒绝。
结论
请求 GPU 切片的可能性对提高 GPU 利用率和降低基础设施成本至关重要。
NVIDIA MIG 允许创建完全隔离的 GPU 实例,配备专用的内存和计算资源,但如果我们希望实现卓越的操作,NVIDIA GPU 操作员提供的 Kubernetes 支持是不够的。静态配置不能自动调整以适应工作负载的变化需求,因此无法为每个 Pod 提供所需的 GPU 切片,特别是在需要不同内存和计算切片的工作负载场景中,这些需求随着时间的推移而变化。
*nos*
通过动态 GPU 分区克服了 NVIDIA GPU 操作员静态配置的限制,这种方法提高了 GPU 利用率,减少了在集群节点上手动定义和应用 MIG 配置的操作负担。
值得注意的是,NVIDIA MIG 有其局限性,并不是唯一的分区技术,也不是提高 Kubernetes 集群利用率的唯一方法。具体而言,MIG 仅支持较新的架构(Ampere 和 Hopper),且不提供细粒度的 GPU 分区,这意味着无法创建具有任意内存和计算资源的 GPU 切片。
为了克服这些限制,nos
还提供了 通过 NVIDIA 多进程服务 (MPS) 的动态 GPU 分区,这是一种支持几乎所有 NVIDIA GPU 的分区技术,允许创建任意数量的内存切片。你可以在 这里 找到有关动态 MPS 分区的更多信息。
资源
版权
特别感谢Emile Courthoud对本文的审阅和贡献。
使用上下文强盗进行动态定价:通过实践学习
原文:
towardsdatascience.com/dynamic-pricing-with-contextual-bandits-learning-by-doing-b88e49f55894
将上下文添加到你的动态定价问题中可以增加机会,同时也带来挑战
·发表于 Towards Data Science ·阅读时间 17 分钟·2023 年 10 月 5 日
–
照片由 Artem Beliaikin 在 Unsplash 上提供
从多臂强盗到上下文强盗
在我的 上一篇文章 中,我对使用简单的多臂强盗应对动态定价问题的最流行策略进行了详细分析。如果你是从那篇文章过来的,首先,感谢你。那绝不是一篇容易阅读的文章,我非常感激你对这个话题的热情。其次,做好准备,因为这篇新文章会更加具有挑战性。然而,如果这是你首次接触这个话题,我强烈建议你先从上一篇文章开始阅读。在那篇文章中,我介绍了基础概念,这些概念在本讨论中我会假设读者已经熟悉。
不管怎样,简单回顾一下:之前的分析旨在模拟一个动态定价场景。主要目标是尽快评估各种价格点,找到能够产生最高累计奖励的价格。我们探索了四种不同的算法:贪婪算法、ε-贪婪算法、汤普森采样和 UCB1,详细描述了每种算法的优缺点。尽管那篇文章中采用的方法在理论上是可靠的,但它存在一些简化,这些简化在更复杂的现实世界情况下并不成立。其中最具问题的是假设基本过程是稳定的——即最优价格在外部环境无论如何都保持不变。这显然不符合实际。例如,节假日期间需求波动、竞争对手价格的突然变化或原材料成本的变化。
为了解决这个问题,上下文赌博机应运而生。上下文赌博机是多臂赌博机问题的扩展,其中决策代理不仅为每个行动(或“臂”)获得奖励,而且在选择臂之前还可以访问上下文或环境相关信息。上下文可以是任何可能影响结果的信息,例如客户的人口统计数据或外部市场条件。
它们的工作方式如下:在决定拉哪个臂(或者在我们的例子中,设置哪个价格)之前,代理会观察当前的上下文,并利用它做出更明智的决策。代理随后随着时间的推移学习哪些行动在特定上下文中效果最佳,并根据所获得的奖励以及这些奖励获得的上下文来调整其策略。这种持续学习和适应机制可以帮助企业动态调整其定价策略,以应对不断变化的外部因素,从而可能提高表现和增加利润。
上下文赌博机:架构、建模与挑战
上下文赌博机问题的架构可以被视为多臂赌博机问题的一个推广。这两种方法的最终目标都是在时间上最大化奖励,即找到探索新行动和利用已知行动之间的最佳平衡。此外,它们都从历史中学习:所做的决策以及所获得的相应奖励。
然而,它们做出决策和学习的方式存在根本的不同。最显著的区别在于上下文的概念。在多臂赌博机问题中,决策完全基于与每个臂相关的历史奖励记录,而上下文赌博机则将额外的外部信息或上下文纳入其决策框架。这些上下文通常提供了关键见解,可以显著影响所选择行动的结果。
上下文赌博机反馈循环的架构。注意到来自客户和/或环境的信息现在是赌博机的输入。图片由作者提供。
然而,上下文赌博机问题中的上下文存在,需要更复杂的建模方法。在这里,需要一个针对每个臂的预测模型(通常称为“预言机”),以帮助基于给定的上下文识别最佳动作。这涉及利用线性回归、逻辑回归、神经网络或其他预测算法等技术,这些技术能够有效地融入上下文以预测奖励。
鉴于这种额外的上下文维度,很明显上下文赌博机呈现出一种超越多臂赌博机的复杂性。它不仅仅是跟踪奖励,还需要更复杂的分析来学习不同上下文如何与不同动作的奖励相关联。从本质上讲,多臂赌博机提供了一个较简单、以历史为中心的视角,而上下文赌博机则提供了一个更丰富、更具适应性的视角,适应于变化的环境及其对潜在奖励的影响。
现在是坏消息。正如前面提到的,预言机通常是预测模型。它们可以是任何能够在特定上下文下生成预测和相关概率的机器学习模型。不幸的是,虽然大多数机器学习算法在生成预测和估计概率方面表现优异,但许多算法在提供预测的不确定性度量方面存在不足。不确定性对利用诸如汤普森采样或上置信界等方法至关重要。对于上下文赌博机来说,实施这些方法特别具有挑战性,因为创建一个不确定性度量(也许通过自助法或集成方法)将使本已复杂的架构更加复杂。虽然有框架融入了这些特性,但在这次讨论中我将把它们搁置,专注于两个主要的无不确定性算法:贪婪算法和ε-贪婪算法。
上下文建模
在对上下文赌博机有了更清晰的理解后,现在是时候建立环境来测试它们了。在上一篇文章中,我使用逻辑函数设计了一个简单的需求曲线。这个函数的目的是提供在任何给定价格点的假设客户的购买预期概率。
如前所述,参数b决定需求曲线的陡峭程度,指示曲线收敛到 0 的速度。这反过来会影响最优价格的确定。在多臂老虎机场景中,我们为a和b分配了单一值。然而,在上下文设置中,虽然我们仍将保持a不变(等于 2),但我们打算使b取决于我们上下文的值。具体来说,在我们的模拟中,我们将定义一个由两个特征组成的上下文:地理区域和年龄。本质上,我们假设我们的需求曲线——由b值表示——会根据这两个参数变化。
为了简化我们的分析,我们将限制输入空间。对于地理区域,我们将考虑两个区域:‘EU’和‘US’。对于年龄,我们将其分成四个不同的桶,而不是使用连续范围。因此,我们将有总共八个独特的上下文进行分析。显然,这是一个简化模型,它作为基础可以通过引入额外的上下文特征来发展更复杂的场景。另一个相关的假设是,上下文仅决定需求曲线的非平稳性。换句话说,虽然不同的上下文导致不同的需求曲线,但这些需求曲线是时间独立的,这意味着其参数不随时间步骤变化。
在我们明确了上下文之后,下一步是为每个上下文分配一个特定的b值。这将使我们能够确定针对每个上下文的最优价格点(有关如何找到给定需求曲线的最优价格的更多细节,请参见之前的文章)。
上下文映射与需求参数‘b’相关的值以及相应的最优价格。图片由作者提供。
上表阐明了上下文和b值的组合,以及相应的最优价格。我故意选择了b值,以便轻松生成一组候选价格来测试我们的模型。这些价格是(15, 20, 25, 30, 35, 40, 45, 50, 55, 60),它们代表了我们的‘臂’,即我们可以选择的可能行动,以最大化累积奖励。
为了让您更清楚我们老虎机的目标,我们来关注定价为 30 的臂/价格集合。管理这个臂的神谕需要确定所有 8 个上下文的购买概率。尽管这些概率在实际中是未知的,但在我们的模拟中,可以通过将价格=30,a=2,并根据特定上下文调整b,使用前面提供的需求曲线函数进行计算。作为参考,这里是神谕需要学习的与此臂相关的概率集合:
每个上下文的购买概率,价格=30。图片由作者提供。
毋庸置疑,其他所有臂部将处理各自特定的概率集。
为了实践这一点,这里是动态生成运行时上下文的代码。此外,在这段代码中,我们还构建了一个字典,将上下文与相关的最佳价格进行映射。这些字典在稍后计算遗憾时会特别有用:
设置预言机:逻辑回归
就像在简单的多臂赌博机场景中一样,我们的奖励函数将返回 1 或 0,反映客户是否进行了购买。实质上,对于每个可能的价格点,我们将逐步编译数据集,格式为:(地理区域,年龄范围,奖励)。乍一看,这确实像是一个典型的二分类问题,这使我们考虑测试最常见的二分类模型之一:逻辑回归。
然而,当涉足强化学习领域时,学习方法与传统的监督学习范式不同。具体来说,在强化学习中,模型会不断更新,每次引入新记录时都会进行更新。这种转变要求采用‘在线’算法,这些算法可以随着新记录的引入逐步更新。这样的算法对于优化计算效率和节省内存资源至关重要。由于这一要求,sklearn
提供的传统逻辑回归实现——不支持在线训练或“部分拟合”——变得不太理想。
进入SGDClassifier
。随机梯度下降(SGD)分类器是一个通过 SGD 优化的线性分类器。实质上,SGD 不是处理整个数据集来计算目标函数的梯度,而是使用一个或几个训练样本来近似计算。这使得它在在线学习场景中高度高效。为了将SGDClassifier
配置为模拟逻辑回归,我们需要将其损失函数设置为’log_loss’。这样,我们实际上采用了逻辑回归模型,但增加了 SGD 能够随着每个新数据点逐步更新模型的优势,完美契合我们的强化学习框架。
首先,我们将创建一个Model
辅助类。这个类不仅会封装SGDClassifier
实例,还会包含所有在模拟过程中需要使用的相关方法。此外,这个设置为快速集成其他模型到模拟中奠定了基础。还有,剧透警告,我们稍后会需要这样做。
这是类方法的简要描述:
-
add_sample
:接收新的数据点和标签,并将它们附加到现有数据集中。 -
can_predict
:确定模型是否能够进行预测。它确保数据集中既有正例也有负例,以避免由于缺乏多样化数据导致模型无法做出有意义预测的“冷启动”问题。 -
get_prob
:给定一个数据点,此方法返回它属于标记为‘1’的类别的概率。这个概率表示我们对在模型参考的价格下发生购买的可能性的估计。 -
fit
:一种特定于逻辑回归模型的部分拟合方法。它在最新数据点上增量训练模型,而无需重新训练整个数据集。
设置模拟
准备好Model
类后,我们可以创建一个模拟决策过程的函数:
run_simulation
代码模拟了选择最佳价格以最大化奖励的过程。它接受以下参数:
-
prices
:要提供的潜在价格列表。 -
nstep
:模拟的步骤或迭代次数。 -
strategy
:要使用的决策策略,可以是’random’、‘greedy’或’epsgreedy’。 -
model_type
:要使用的预测模型类型,默认为’logistic’。
在这些参数的基础上,函数根据选择的策略为每次迭代选择一个价格(称为arm
)。在每一步,使用generate_context()
函数生成一个客户上下文。根据策略,arm
要么随机选择(这是我们的评估基线),要么基于贪婪或ε-贪婪算法选择。然后使用get_reward()
函数模拟客户对选择价格的反馈,计算并累计遗憾(即采用最优价格与选择价格之间的奖励差异)。然后使用新数据更新与所选arm
对应的模型,并重新训练。该函数还处理“冷启动”问题,在早期迭代中,预测模型可能没有足够的数据来进行预测。在这种情况下,选择第一个无法预测的arm
。
贪婪和ε-贪婪算法在之前的文章中有详细描述。在本节中,我们将仅展示在新的上下文场景中实现这些算法的代码。它与多臂赌博机中的使用方式大致相同,主要区别在于这里我们使用模型概率来计算arms
的期望回报(即概率与相关价格的乘积)。
模拟函数的输出本质上是随机的。为了全面评估模型和策略,我们将运行此模拟 1,000 次。每次模拟将持续 10,000 个时间步骤,收集相关的中间数据以产生汇总的和统计上更可靠的结果。我们关注的两个主要指标是:
-
平均累计遗憾:就像在多臂赌博机的情况一样,这是一个在每次模拟运行中汇总这一错失机会的指标。我们将累计每次运行计算出的遗憾曲线,然后对它们进行平均。我们可以计算这个指标,因为在这次模拟中我们知道每个上下文的最优价格。显然,这在任何真实场景中都不适用。
-
每个上下文的平均最优价格估计:这代表了在所有模型预测中,每个上下文值的最高概率价格。简单来说,每次模拟运行后,对于每个可能的上下文,我们“询问”模型提供它们的概率。然后,我们选择具有最高预期回报的模型——更准确地说,是与该模型相关的价格。每次运行生成的价格估计随后被汇总并平均。通过将这些估计价格与最优价格进行比较,我们可以评估模型“理解”基本需求曲线的整体能力。
以下是运行模拟 1,000 次、收集所需数据并计算汇总指标的代码。
在我们深入探讨模拟结果之前,让我简要评论一下每次模拟中的数据量。说明每次模拟将运行 10,000 步,表明我们在每次运行中收集了 10,000 个数据点。在机器学习领域,10,000 条数据通常被视为获取有意义见解的最小阈值。然而,我们的情况更具挑战性。这 10,000 条记录实际上是“分布”在对应于每个价格候选(我们的行动)的 10 个模型中。因此,每个模型用于训练的记录可能不超过一千条,这通常是不够的。这带来了一个关于上下文赌博的关键见解:如果你有多个行动,可能需要大量数据才能获取有用的信息。而这个大量,就是很多。
结果
话不多说,来看一下累计遗憾的图表:
作者提供的图片
看着这个图表,你的初步反应可能是:“这个家伙搞错了图例标签!贪婪策略不可能优于ε-贪婪策略!”至少,这就是我第一次看到这个输出时的想法。惊讶的是,这个图表是准确的,尽管它非常违反直觉。让我们深入探讨,了解发生了什么,通过考虑平均价格估计来理解。
作者提供的图片
价格估算图基本上是“揭示罪魁祸首”。如果你观察前四个上下文并将它们与第二个上下文进行比较,你会很容易发现逻辑回归预测的价格在下降(如两个箭头所示)。问题的根源很简单:逻辑回归本质上是一个线性模型,即使我们对其进行非线性转换。遗憾的是,我们的数据本质上是非线性的,如红点所示。因此,逻辑回归仅仅是在努力适应数据,但显然不是这个场景下合适的模型。
我们所见的是:以某些初始的积极结果‘贪婪’地进行调整,比依赖模型的概率更少有害。这是因为模型从数据中学习得越多,它们在预测中的一致性误差就越大。归根结底,‘ε’探索仅仅确保了一些数据被用来完善模型,但这些模型学习得越多,就越会把我们引入歧途。
对于那些对这个解释不信服的人,考虑一个简单的一维场景:只有四个点(0,1,2,3),其分别获得 1 的概率为:0.01,0.08,0.95,0.97。在这种情况下,大家都很满意,因为逻辑回归效果很好,甚至可以得到一个漂亮的“按部就班”的图:
作者提供的图片
逻辑回归输出的概率表示曲线在特定点的值,在这里它们非常接近实际值(绿色点)。然而,让我们看看如果我们将概率修改为(0.1,0.8,0.5,0.3),更接近我们的定价场景,会发生什么:
作者提供的图片
在这种情况下,事情变得棘手,因为四个概率预测中的三个将完全错误(那些与输入 0,1 和 3 相关的)。现在,在每次迭代中,我们根据期望回报选择最佳的臂,即模型概率与相应臂价格的乘积。如果概率完全错误,那么我们将面临一个大问题。
那么,我们现在该怎么办?直接的诱惑可能是选择一个复杂的非线性模型,比如神经网络,甚至更好的是,选择一个深入学习领域的复杂架构。这当然是一个选择,确实有公司在探索这种定价方向。然而,我们首先想要考虑是否有更简单的解决方案。如果有一个算法可以根据在任何给定区域获得‘1’的概率来划分输入空间就好了……
调整预言机:决策树
最后一段应该向你耳边呐喊两个简单的词:决策树。为什么?因为这正是决策树的本质。决策树通过根据特定特征阈值递归地划分输入空间,从而形成了一个节点和分支的层次结构。每一次分裂或决策,都是以最佳方式将数据根据目标变量进行区分——在我们的上下文中,‘一’的概率在某个区域内相对均匀。
一旦树构建完成,每个终端节点(或叶子)代表输入空间中的一个特定区域,其中数据表现出类似的特征。对于给定输入的概率预测,然后是从落入相应叶子中的数据样本的‘一’的比例中得出的。例如,如果一个叶子节点有 80 个样本,其中 40 个被标记为‘一’,那么对于到达该叶子的输入,预测的概率将是 40/80=0.5。
所以,它们似乎正是我们问题所需的。遗憾的是,决策树没有在线实现,这意味着在每一步我们需要从头开始重新训练模型。好的一面是,决策树训练非常迅速,但如果你计划在实际应用中实时重新训练模型,可能需要记住这一点。
好的,首先我们需要更新我们的Model
类以将决策树作为一个选项。很简单——只需一系列的 if-then 语句以确保我们根据初始配置调用适当的函数:
接下来,我们可以通过简单地将‘dectree’添加到外部循环迭代的列表中来重新执行模拟。就这样,经过一段时间,我们得到了最终结果:
图片来源:作者
现在我们可以深入探讨了。首先,决策树表现最佳,确认了我们最初直觉的正确性。此外,ε-贪婪算法显著优于简单的贪婪算法,正如预期的那样。这表明,从上下文中学习确实有助于模型做出更好的预测,并且最终促进了更好的决策。比较预测价格与最优价格的图表看起来也更有希望:
图片来源:作者
ε-贪婪算法本质上识别了所有的最优价格,在每种上下文中都优于贪婪方法。
经验教训与下一步
-
数据越多越好(?): 你可能认为只要有数据就应该使用它。一般来说,这是对的。在 Contextual Bandits 领域,将数据(背景)添加到你的算法中可以导致更明智的决策和量身定制的行动。然而,有一个陷阱。‘Bandits’ 算法的主要目的是加速做出最佳决策的过程。添加背景会增加复杂性,可能会减缓达成预期结果的速度。过渡到 Contextual Bandits 也可能妨碍有效策略的利用,如 Thompson 采样或 UCB。正如数据科学中的许多选择一样,在目标和限制之间存在权衡。
-
了解你的数据和算法:必须小心对数据做出的假设。常见的观点是,逻辑回归通常适用于机器学习任务,因为“线性假设经常成立”。虽然这可能经常是这样,但并非绝对。错误的假设可能导致重大陷阱。这强调了利用指标的重要性,以确保我们能够快速评估初步假设的准确性,并在需要时调整我们的策略。
-
保持简单(尽可能长时间):即便在数据科学领域,也很容易被最新的趋势所吸引。在撰写本文时,人工智能领域正热衷于深度学习方法。虽然强大,但必须记住,有许多更简单且稳健的策略可能完全适合你的问题,不多也不少。
总之,Contextual Bandits 是一个令人兴奋的研究领域,确实值得更多关注。本文探讨了一个相对简单的背景,但背景可以在数据维度和类型(如文本或图像)上变得非常复杂。即使这似乎与关于简单性的前一点相矛盾,分析的逻辑进程是探索更复杂的算法,如深度学习。虽然我不打算在这一系列中添加更多章节,但我很想听听你在这个领域的经验。如果你已经深入研究过这个领域,请留下评论或直接联系我!
代码库
github.com/massi82/contextual_bandits
参考文献
contextual-bandits.readthedocs.io/en/latest/
www.youtube.com/watch?v=sVpwtj4nSfI
如果你为网站或应用开发个性化用户体验,Contextual Bandits 可以帮助你。使用……
[towardsdatascience.com [## 如何构建更好的上下文策略带机器学习模型 | Google Cloud Blog
上下文策略带:它是什么,企业如何应用它,以及如何使用 AutoML 实现它
courses.cs.washington.edu/courses/cse599i/18wi/resources/lecture10/lecture10.pdf
你喜欢这篇文章吗?如果你对人工智能、自然语言处理、机器学习和数据分析在解决现实世界问题中的应用感兴趣,你可能也会喜欢我的其他作品。我的目标是撰写出展示这些变革性技术在实际场景中应用的可操作性文章。如果这也是你的兴趣所在,请在 Medium 上关注我,以获取最新文章!
使用多臂老虎机进行动态定价:通过实践学习
将强化学习策略应用于实际案例,尤其是动态定价中,可以揭示许多惊喜
·
关注 发表在 Towards Data Science ·16 分钟阅读·2023 年 8 月 16 日
–
由 Markus Spiske 拍摄,照片来自 Unsplash
动态定价、强化学习与多臂老虎机
在决策问题的广阔世界中,有一个困境特别属于强化学习策略:探索与利用。想象一下走进一个赌场,那里有一排排的老虎机(也称为“单臂赌博机”),每台机器支付的奖励各不相同且未知。你是探索并玩每台机器以发现哪个机器的回报最高,还是坚持玩一台机器,希望它就是大奖?这个隐喻场景构成了多臂赌博机(MAB)问题的核心概念。目标是找到一个在一系列游戏中最大化奖励的策略。虽然探索提供了新的见解,但利用则是利用你已经拥有的信息。
现在,将这一原则转移到零售场景中的动态定价上。假设你是一个电子商务商店的老板,拥有一款新产品。你不确定其最佳销售价格。你该如何设定一个最大化收入的价格?你应该探索不同的价格以了解客户的支付意愿,还是应该利用一个在历史上表现良好的价格?动态定价本质上是一个伪装的多臂赌博机问题。在每一个时间点,每一个候选价格点都可以看作是老虎机的一个“臂”,而从该价格生成的收入则是其“奖励”。另一种看法是,动态定价的目标是迅速而准确地测量客户群体对不同价格点的需求反应。简单来说,目标是找出最能反映客户行为的需求曲线。
在这篇文章中,我们将探讨四种多臂赌博机算法,以评估它们在一个明确定义(尽管并不简单)的需求曲线下的效果。随后,我们将分析每种算法的主要优点和局限性,并深入研究评估其性能的关键指标。
需求曲线建模
传统上,经济学中的需求曲线描述了产品价格与消费者愿意购买的产品数量之间的关系。它们通常向下倾斜,表示一个常见的观察,即价格上涨时,需求通常会下降,反之亦然。想想智能手机或演唱会门票这样的热门产品。如果价格降低,更多的人往往会购买,但如果价格飙升,即使是忠实粉丝也可能会重新考虑。
然而,在我们的背景下,我们将稍微不同地建模需求曲线:我们将价格与概率进行对比。为什么?因为在动态定价场景中,特别是数字商品或服务的情况下,考虑在给定价格下销售的可能性往往比猜测确切数量更有意义。在这样的环境中,每次定价尝试都可以看作是对成功(或购买)可能性的探索,这可以简单地建模为一个伯努利随机变量,其概率p取决于给定的测试价格。
这里才是特别有趣的地方:虽然直观上我们可能认为我们的多臂赌博算法的任务是发现概率最高的购买价格,但事实并不是那么简单。实际上,我们的最终目标是最大化收入(或边际)。这意味着我们不是寻找能让最多人点击‘购买’的价格,而是寻找能让乘以其相关购买概率后获得最高预期回报的价格。想象一下,设定一个较高的价格,虽然卖出的人较少,但每笔销售却能产生相当可观的收入。相反,一个非常低的价格可能会吸引更多的买家,但总收入可能仍低于高价情况。因此,在我们的背景下,谈论‘需求曲线’有些不寻常,因为我们的目标曲线主要代表购买概率而不是需求直接。
现在,来谈谈数学,让我们先说消费者行为,特别是在处理价格敏感性时,并不总是线性的。线性模型可能会暗示每次价格增加时,需求都会线性下降。实际上,这种关系通常更为复杂和非线性。建模这种行为的一种方式是使用逻辑函数,它可以更有效地捕捉这种微妙的关系。我们选择的需求曲线模型是:
在这里,a确定了最大可达购买概率,而b调节了需求曲线对价格变化的敏感程度。b的值越高,曲线就越陡,随着价格的增加更快地接近较低的购买概率。
具有不同参数 a 和 b 组合的四个需求曲线示例
对于任何给定的价格点,我们将能够获得一个相关的购买概率,p。然后,我们可以将p输入到一个伯努利随机变量生成器中,以模拟顾客对特定价格提议的反应。换句话说,给定一个价格,我们可以轻松地模拟我们的奖励函数。
接下来,我们可以将这个函数乘以价格,以获得给定价格点的期望收入:
毫不奇怪,这个函数并不会在概率最高的地方达到最大值。此外,最大值所对应的价格并不取决于参数a的值,而最大期望回报却取决于a的值。
期望收入曲线与相关最大值
结合一些微积分知识,我们还可以推导出导数的公式(你需要同时使用乘法法则和链式法则)。这不是一个轻松的练习,但也没有特别困难的地方。这是期望收入的导数的解析表达式:
这个导数允许我们找到最大化期望收入曲线的确切价格。换句话说,通过使用这个特定的公式以及一些数值算法,我们可以轻松确定将其设为 0 的价格。反过来,这就是最大化期望收入的价格。
这正是我们需要的,因为通过固定a和b的值,我们将立即知道我们的赌博机需要找到的目标价格。用 Python 编码这只需几行代码:
对于我们的用例,我们将设置a=2 和b=0.042,这将给我们一个大约 30.44 的目标价格,关联的最佳概率为 0.436(→最佳平均奖励为 30.44*0.436=13.26)。这个价格在一般情况下显然是未知的,正是我们的多臂赌博机算法将要寻找的价格。
多臂赌博机策略
既然我们已经确定了目标,现在是时候探索各种策略以测试和分析它们的性能、优点和缺点了。虽然在 MAB 文献中存在几种算法,但在实际应用中,四种主要策略(及其变种)主要构成了基础。在本节中,我们将简要概述这些策略。我们假设读者对这些策略有基本了解;不过,对于那些有兴趣深入研究的人,我们在文章末尾提供了参考文献。在介绍每个算法后,我们还将展示其 Python 实现。尽管每种算法具有其独特的参数,但它们都普遍使用一个关键输入:arm_avg_reward
向量。该向量表示当前时间步t为止每个臂(或动作/价格)获得的平均奖励。这个关键输入指导所有算法做出有关后续价格设置的明智决策。
我将应用于我们的动态定价问题的算法如下:
贪婪策略:这一策略就像是每次都回到最初给你最多硬币的机器。尝试过每台机器后,它会坚持最初表现最好的那一台。但可能会有一个问题。如果那台机器只是最初运气好呢?贪婪策略可能会错过更好的选项。幸运的是,代码实现非常简单:
区分初始情境(所有回报均为 0 时)与常规情境是至关重要的。通常,你会发现只实现了‘else’部分,这确实在所有回报为 0 时也能工作。然而,这种方法可能导致对第一个元素的偏见。如果忽视这一点,你可能会因此付出代价,特别是当最优回报恰好与第一个臂相关时(是的,我经历过)。贪婪方法通常表现最差,我们将主要使用它作为性能基准。
ϵ-贪婪:ε-贪婪(epsilon-greedy)算法是对贪婪方法主要缺陷的一种改进。它引入了一个概率 ε(epsilon),通常是一个小值,用于选择一个随机的臂,促进探索。以概率 1−ε,它选择回报估计最高的臂,偏向于利用。通过在随机探索和已知回报的利用之间进行平衡,ε-贪婪策略旨在实现比纯粹的贪婪方法更好的长期回报。同样,这种实现是直接的,只需在贪婪代码上添加一个额外的‘if’。
UCB1(上置信界):UCB1 策略就像一个好奇的探险者,试图在新城市找到最好的餐馆。虽然有一个他们已经喜欢的地方,但每天都会有可能发现更好的地方。在我们的上下文中,UCB1 将已知价格点的回报与那些较少探索的价格点的不确定性相结合。数学上,这种平衡是通过一个公式实现的:价格点的平均回报加上一个基于距离上次尝试的时间的“未知奖励”奖金。这个奖金计算为
并代表了对未尝试价格的“增长的好奇心”。超参数 C 控制利用和探索之间的平衡,C 值较高时,鼓励更多探索较少采样的臂。通过始终选择已知回报和好奇心奖金的组合值最高的价格,UCB1 确保了既坚持已知又探索未知的混合,旨在揭示最大收入的最优价格点。我会从按部就班的实施开始,但我们很快会看到需要对其进行一些调整。
汤普森采样:这种贝叶斯方法通过基于后验奖励分布的概率选择臂来解决探索-利用困境。当这些奖励符合伯努利分布,表示像成功/失败这样的二元结果时,汤普森采样(TS)使用 Beta 分布作为共轭先验(参见此表)。算法从每个臂开始使用非信息性 Beta(1,1)先验,并在观察到奖励后更新分布的参数:成功增加 alpha 参数,而失败增加 beta 参数。在每次游戏中,TS 从每个臂的当前 Beta 分布中抽取样本,并选择具有最高抽样值的臂。这种方法使 TS 能够根据获得的奖励动态调整,巧妙地平衡了对不确定臂的探索和对已知奖励臂的利用。在我们的具体场景中,尽管基础奖励函数遵循伯努利分布(购买为 1,错过购买为 0),但实际感兴趣的奖励是该基础奖励与当前测试价格的乘积。因此,我们的 TS 实现将需要稍作修改(这也会带来一些惊喜)。
改动其实很简单:要确定最有前途的下一个臂,提取自后验估计的样本乘以其各自的价格点(第 3 行)。这一修改确保决策基于预期的平均收入,而非最高购买概率。
我们如何评估结果?
此时,在收集了所有关键因素以构建一个比较我们动态定价背景下四种算法性能的仿真后,我们必须问自己:我们究竟要测量什么?我们选择的指标至关重要,因为它们将指导我们在比较和改进算法实现的过程中。在这方面,我关注三个关键指标:
-
遗憾:该指标衡量所选择行动获得的奖励与采取最佳可能行动所能获得的奖励之间的差异。从数学上讲,时间t的遗憾定义为:遗憾(t)=最佳奖励(t)−实际奖励(t)。遗憾在时间上累积,提供了我们没有总是选择最佳行动而“失去”多少的洞察。与累计奖励相比,遗憾更为优选,因为它能更清晰地指示算法相对于最佳情况的表现。理想情况下,接近 0 的遗憾值表明接近于最佳决策。
-
反应性:这一指标衡量算法接近目标平均奖励的速度。本质上,它是算法适应性和学习效率的衡量标准。一个算法越快达到期望的平均奖励,它的反应性就越强,意味着更快地调整到最佳价格点。在我们的情况下,目标奖励设定为最佳平均奖励的 95%,即 13.26。然而,初始步骤可能表现出较高的波动性。例如,一个幸运的早期选择可能会导致从一个低概率的高价格臂中获得成功,迅速达到阈值。由于这些波动,我选择了一个更严格的反应性定义:达到 95%最佳平均奖励十次所需的步骤数,排除最初的 100 步。
-
臂分配:这表示每个算法使用可用臂的频率。以百分比形式呈现,它揭示了算法随时间选择每个臂的倾向。理想情况下,对于最有效的定价策略,我们希望算法将 100%的选择分配给表现最好的臂,0%分配给其他臂。这样的分配将固有地导致 0 的遗憾值,表示最佳性能。
运行仿真
评估 MAB 算法具有挑战性,因为其结果具有高度的随机性。这意味着由于确定量的固有随机性,结果在不同运行之间可能大相径庭。为了进行稳健的评估,最有效的方法是多次执行目标仿真,累积每次仿真的结果和指标,然后计算平均值。
初始步骤包括创建一个模拟决策过程的函数。这个函数将实现下图所示的反馈循环。
在仿真函数中实现的反馈循环
这是仿真循环的实现:
该函数的输入是:
-
prices
:我们希望测试的候选价格列表(本质上是我们的“臂”)。 -
nstep
:仿真中的总步骤数。 -
strategy
:我们旨在测试的用于决策下一个价格的算法。
最后,我们需要编写外循环的代码。对于每个目标策略,这个循环将调用run_simulation
多次,收集并汇总每次执行的结果,然后展示结果。
对于我们的分析,我们将使用以下配置参数:
-
prices
:我们的价格候选值 → [20, 30, 40, 50, 60] -
nstep
:每次仿真的时间步数 → 10000 -
nepoch
:仿真执行次数 → 1000
此外,通过设置我们的价格候选值,我们可以快速获得相关的购买概率,这些概率是(大约)[0.60, 0.44, 0.31, 0.22, 0.15]。
结果
在运行模拟之后,我们终于能够看到一些结果。我们从累计遗憾的图表开始:
从图表中,我们可以看到,在平均累计遗憾方面,TS 是赢家,但它需要大约 7,500 步才能超越 ε-greedy。另一方面,我们有一个明显的失败者,那就是 UCB1。在其基本配置下,它基本上表现与贪婪方法相当(稍后我们会再讨论)。让我们通过探索其他可用的指标来更好地理解结果。在所有四种情况下,反应性表现出非常大的标准差,因此我们将关注中位数值而非均值,因为它们对离群值更具抵抗力。
从图表中的初步观察显示,虽然 TS 在均值方面超越了 ε-greedy,但在中位数方面略显滞后。然而,其标准差较小。特别有趣的是反应性条形图,它展示了 TS 如何努力快速实现有利的平均奖励。起初,这对我来说有些反直觉,但在这个场景中 TS 的机制澄清了问题。我们之前提到 TS 估计购买概率。然而,决策是基于这些概率和价格的乘积。了解真实概率(如前所述,[0.60, 0.44, 0.31, 0.22, 0.15])让我们能够计算 TS 正在积极导航的期望奖励:[12.06, 13.25, 12.56, 10.90, 8.93]。本质上,尽管基础概率差异较大,但从 TS 的角度来看,期望收益值相对接近,尤其是在接近最佳价格时。这意味着 TS 需要更多时间来辨别最佳臂。虽然 TS 仍然是表现最好的算法(如果模拟时间延长,其中位数最终会低于 ε-greedy 的中位数),但在这种情况下,它需要更长时间来确定最佳策略。下面的臂分配饼图显示 TS 和 ε-greedy 在识别最佳臂(价格=30)并在模拟过程中大部分时间使用它方面做得相当不错。
现在让我们回到 UCB1。遗憾和反应性确认它基本上作为一个完全利用的算法:快速获得良好的平均奖励水平,但遗憾较大且结果变异性高。如果我们查看臂分配,这一点更为明显。UCB1 仅比贪婪方法稍微聪明一些,因为它更多地关注具有较高期望奖励的 3 个臂(价格为 20、30 和 40)。然而,它基本上完全没有探索。
进入超参数调优。显然,我们需要确定平衡探索与利用的权重 C 的最佳值。第一步是修改 UCB1 代码。
在这段更新的代码中,我加入了在添加“不确定性奖励”之前标准化平均奖励的选项,这个奖励是由超参数C加权的。这样做的原因是为了让最佳超参数的搜索范围保持一致(比如 0.5–1.5)。如果没有这个标准化,我们可能会得到类似的结果,但搜索区间需要根据每次处理的值范围进行调整。我会避免让你寻找最佳C值的无聊,它可以通过网格搜索轻松确定。事实证明,最佳值是 0.7。现在,让我们重新运行模拟并检查结果。
这真是个大反转,不是吗?现在,UCB1 显然是最好的算法。即使在反应性方面,与之前的得分相比,它也只是略微恶化。
此外,从臂分配的角度来看,UCB1 现在是无可争议的领导者。
经验教训和下一步
-
理论与经验:从书本学习开始是深入新主题的一个必要步骤。然而,你越早投入实际经验,你将越快将信息转化为知识。当你将算法应用到现实世界用例时,遇到的细微差别、复杂性和特殊情况将提供远超你可能阅读的任何数据科学书籍的洞察。
-
了解你的指标和基准:如果你不能衡量你所做的事情,你就不能改进它。在开始任何实现之前,必须了解你打算使用的指标。如果我仅仅考虑了遗憾曲线,我可能会得出“UCB1 不起作用”的结论。通过评估其他指标,特别是臂分配,明显发现算法只是没有足够探索。
-
没有一刀切的解决方案:虽然 UCB1 在我们的分析中脱颖而出,但这并不意味着它是你动态定价挑战的普遍解决方案。在这种情况下,调优相对简单,因为我们知道我们要寻找的最佳值。在现实生活中,情况从未如此明确。你是否具备足够的领域知识或手段来测试和调整 UCB1 算法的探索因子?也许你会倾向于像ε-贪婪这样的可靠有效选项,承诺立即见效。或者,你可能正在管理一个繁忙的电子商务平台,每小时展示 10000 次产品,你愿意耐心等待,相信汤普森采样最终会获得最大的累计奖励。是的,生活并不容易。
最后,若这项分析看起来令人生畏,不幸的是,它已经代表了一个非常简化的情况。在现实世界的动态定价中,价格和购买概率并不是在真空中存在的——它们实际上存在于不断变化的环境中,并受到各种因素的影响。例如,购买概率在一年内、不同的客户群体和地区之间保持一致的可能性极低。换句话说,为了优化定价决策,我们必须考虑客户的背景。这一考虑将是我下一篇文章的重点,我将在文中通过整合客户信息和讨论上下文赌博机进一步探讨这一问题。所以,请继续关注!
这里 是关于上下文赌博机的文章续篇!
代码库
github.com/massi82/multi-armed-bandit
参考文献
-
www.amazon.it/Reinforcement-Learning-Introduction-Richard-Sutton/dp/0262039249
-
www.geeksforgeeks.org/epsilon-greedy-algorithm-in-reinforcement-learning/
-
towardsdatascience.com/multi-armed-bandits-upper-confidence-bound-algorithms-with-python-code-a977728f0e2d
-
towardsdatascience.com/thompson-sampling-fc28817eacb8
你喜欢这篇文章吗?如果你对 AI、自然语言处理、机器学习和数据分析在解决现实问题中的应用感兴趣,你可能也会喜欢我的其他作品。我的目标是撰写可操作的文章,展示这些变革性技术在实际场景中的应用。如果你也是这样的人,可以在 Medium 上关注我,以了解我最新的文章!
从头开始的动态定价与强化学习:Q-Learning
介绍 Q-Learning 并附带实际的 Python 示例
·发布于 Towards Data Science ·12 分钟阅读·2023 年 8 月 26 日
–
探索价格以寻找最佳的行动-状态值来最大化利润。图片由作者提供。
目录
-
介绍
-
强化学习概述
2.1 关键概念
2.2 Q-函数
2.3 Q-值
2.4 Q-Learning
2.5 贝尔曼方程
2.6 探索与利用
2.7 Q-表
-
动态定价问题
3.1 问题陈述
3.2 实现
-
结论
-
参考文献
1. 介绍
在这篇文章中,我们介绍了强化学习的核心概念,并深入探讨 Q-Learning,一种使智能代理通过基于奖励和经验做出明智决策来学习最佳策略的方法。
我们还分享了一个从零开始构建的实际 Python 示例。特别是,我们训练一个代理掌握定价艺术,这是商业中的一个关键方面,以便它可以学习如何最大化利润。
话不多说,让我们开始我们的旅程吧。
2. 强化学习概述
2.1 关键概念
强化学习(RL)是机器学习的一个领域,其中代理通过试错来学习完成任务。
简而言之,代理尝试与正面或负面反馈相关的动作,通过奖励机制来调整其行为,以最大化奖励,从而学习实现最终目标的最佳行动路径。
让我们通过一个实际的例子介绍强化学习(RL)的关键概念。想象一个简化的街机游戏,在这个游戏中,一只猫需要穿越迷宫来收集宝物——一杯牛奶和一团毛线——同时避免施工现场:
图片由作者提供。
-
代理 是选择行动路径的个体。在这个例子中,代理是控制操纵杆决定猫的下一步动作的玩家。
-
环境 是代理操作的背景。在我们的例子中,是一个二维迷宫。
-
行动
a
是从一个状态移动到另一个状态所需的最小步数。在这个游戏中,玩家有有限的可能行动可供选择:上、左、下 和 右。 -
状态
s
表示玩家和环境的当前情况。它包括猫的当前和允许的位置、宝藏和陷阱的位置,以及游戏状态的其他相关特征(分数、剩余生命等)。 -
奖励
r
代表分配给采取某个行动结果的反馈。例如,游戏可能分配:• 奖励 +5 分,当到达毛线球时,
• 奖励 +10 分,针对牛奶杯,
• 惩罚 -1 分,针对空白单元格,
• 惩罚 -10 分,针对建造。
描述的 RL 框架在下图中显示:
RL 框架。图像由作者提供。
我们的目标是学习一个 策略 π
,即一套规则,使代理能够在最大化奖励的同时遵循行动路径,从而实现目标。
我们可以直接学习最优策略 π*
,或通过学习行动-状态对的值(奖励)间接学习,并利用它们决定最佳行动路径。这两种策略分别被称为 基于策略 和 基于价值。现在让我们介绍 Q 学习,一种流行的基于价值的方法。
2.2 Q 函数
我们介绍 Q 函数,表示为 Q(s,a)
,代表代理在状态 s
中采取行动 a
时,遵循策略 π
所能获得的期望累积奖励:
Q 函数。图像由作者提供。
在方程中:
-
π
是代理遵循的策略。 -
s
是当前状态。 -
a
是在该状态下采取的行动。 -
r
是与给定行动和状态相关的奖励。 -
t
代表当前迭代。 -
γ
是 折扣因子。它代表代理对即时奖励(利用)相对于延迟奖励(探索)的偏好。
2.3 Q 值
Q 值 指 Q 函数分配给特定状态-行动对的数值。在我们的例子中,Q 值提供了玩家通过在迷宫中通过特定行动移动猫到新位置,起始于某个状态时,可能获得的期望累积奖励。简言之,它告诉我们玩家的选择有多“好”。
2.4 Q 学习
鉴于 Q 值的概念,Q 学习 算法的工作原理如下:
-
初始化 Q 值 任意,例如
Q(s, a) = 0 ∀ s ∈ S, a ∈ A
。 -
对于每个回合:
- 初始化状态
s
对于每个步骤:
-
选择行动
a
,观察奖励r
,获得新状态s'
-
更新 Q 值使用Bellman 方程 3.
s ← s'
- 初始化状态
-
直到
s
是终止状态。
2.5 Bellman 方程
Bellman 方程允许代理用累积期望奖励的值来表示状态-动作对的价值。它用于在 Q 学习算法中更新 Q 值,如下所示:
Bellman 方程。图片由作者提供。
在之前的表达式中:
-
学习率
α
(介于 0 和 1 之间)决定了代理基于新经验更新 Q 值的程度。 -
折扣率
γ
(介于 0 和 1 之间)影响代理对即时奖励与未来奖励的偏好。较高的γ
可以促进利用,因为代理会倾向于偏好已知的、带来即时收益的动作。
2.6 探索与利用
代理如何选择下一个动作?
代理可以“探索”新的动作,或“利用”已知与更高奖励相关的动作。
为了学习有效的策略,我们应该在训练过程中在探索和利用之间取得平衡。在我们的例子中,我们可以通过定义一个探索概率,即介于 0 和 1 之间的浮点数,来采用一种简单的方法:
-
如果从(0, 1)的均匀分布中生成的随机数高于探索概率,代理将执行利用,偏好已知的、高奖励的动作。
-
如果数字小于探索概率,代理将执行探索,鼓励尝试新的动作。
这种方法称为epsilon-greedy算法(参见Cheng et Al. 2023, 附录 C)。
2.7 Q-表
当问题涉及有限数量的潜在动作时——例如向上、向左、向下和向上,可以简单地列举所有状态和动作的组合。这个表格,称为Q-表,在训练过程中会填充 Q 值,因为代理探索状态和动作对,并收集它们的相关奖励。在我们的例子中:
更新 Q-表。图片由作者提供。
3. 动态定价问题
给定一个与价格和需求相关的产品,我们的目标是训练一个智能代理,利用强化学习,随着时间的推移调整价格以最大化利润:
“动态定价与对易腐资源的价格固定相关,考虑需求以最大化收入或利润”(Fleischmann, Hall, Pyke, 2004)。
3.1 问题陈述
-
我们对一个简化的环境进行建模,该环境具有离散的动作空间
A
,其中代理可以增加、减少或保持价格不变:A = {+1, -1, 0}
。 -
动作(价格操控)会导致新的需求,我们将离散的需求水平创建为状态
S = {低需求, 中需求, 高需求}
。 -
为了从价格变化(动作
a
)中估算新的需求(状态s
),我们利用了价格弹性k
的概念。价格弹性估计价格Δp
变化与其导致的需求Δv
变化之间的敏感度,我们假设在我们的例子中这是已知的:
图像来源于作者。
- 奖励
r
对应于利润,它源于价格p
的应用及其相应的需求v
,并考虑到与产品相关的单位成本c
:
奖励 r
是动作(价格 p
)和状态(需求 v
)的函数。图像来源于作者。
- 当新价格与初始价格相比增加或减少过多时,我们会根据一个任意阈值分配负奖励。这样,我们惩罚价格的强烈波动。
3.2 实现
DynamicPricingQL
类实现了以下方法:
-
calculate_demand_level
将离散状态值(低、 中或高需求)分配给连续的需求量。 -
calculate_demand
使用输入价格通过价格弹性来估算需求量。 -
fit
训练代理。我们决定在达到最大步数时或利润(奖励)达到某个阈值时中断一个回合。 -
get_q_table
返回代理学习到的 Q-Table。 -
plot_rewards
显示了训练过程中获得的奖励图表。 -
predict
使用 Q 值来预测给定起始价格和需求作为输入的最佳价格。
import numpy as np
from typing import Union
import plotly.express as px
class DynamicPricingQL:
def __init__(self,
initial_price: int = 1000,
initial_demand: int = 1000000,
elasticity: float = -0.01,
cost_per_unit: int = 20,
learning_rate: float = 0.1,
discount_factor: float = 0.9,
exploration_prob: float = 0.2,
error_term: float = 0.2,
random_walk_std: float = 0.5,
target_reward_increase: float = 0.2) -> None:
'''Class that implements a Dynamic Pricing agent using
Q-Learning to find the optimal price for a given product.
Args:
- initial_price: starting price of the product
- initial_demand: starting volume of the product
- elasticity: price elasticity of the product
- cost_per_unit: unitary cost of the product
- learning_rate: learning rate for the Bellman equation
- exploration_prob: control the exploration-explotation trade-off
- error_term: error term added to the reward estimate to account for fluctuations
- random_walk_std: control the random walk fluctuations added to the demand estimate
- target_reward_increase: end the training when the reward reaches this target increase
'''
# Init variables
self.learning_rate = learning_rate
self.discount_factor = discount_factor
self.exploration_prob = exploration_prob
self.initial_price = initial_price
self.cost_per_unit = cost_per_unit
self.elasticity = elasticity
self.error_term = error_term
self.random_walk_std = random_walk_std
self.initial_demand = initial_demand
self.target_reward_increase = target_reward_increase
self.current_price = initial_price
self.current_demand = initial_demand
# Estimate current demand level from the initial demand
self.current_demand_level = self.calculate_demand_level(
self.initial_demand)
# Track whether the training procedure occurred or not
self.isfit = False
# The agent can only perform 3 actions:
# - Increase the price
# - Decrease the price
# - Keep the price constant
self.num_actions = 3
# Consider 3 different states as discrete demand level
self.num_demand_levels = 3
# Initialize Q-values
self.q_values = np.zeros((self.num_demand_levels,
self.num_actions))
# Store rewards per episode for plotting
self.episode_rewards = []
def calculate_demand_level(self,
demand: int,
demand_fraction: float = 0.3) -> int:
'''Estimate the demand level.
Demand levels represent the states of the Q-Learning agent.
In order to turn a continuous demand into a discrete set in three values,
we use a fraction of the initial value to estimate a low, medium or high demand level.
Args:
- demand: current demand for the product
- demand_fraction: fraction of demand controlling the assignment to the demand levels
'''
# Low demand level: 0
if demand < (1 - demand_fraction) * self.initial_demand:
return 0
# High demand level: 2
elif demand > (1 + demand_fraction) * self.initial_demand:
return 2
# Medium demand level: 1
else:
return 1
def calculate_reward(self,
new_price: int,
price_fraction: float = 0.2) -> float:
'''Calculate the reward.
The reward during an episode is the profit
under a certain price (action) and demand.
We add an error term to account for fluctuations.
Note: if the price is either too high or too low
with respect to the initial price, we assign a negative reward.
Args:
- new_price: new price of the product
- price_fraction: penalize price variations above or below this fraction
'''
# If the new price is more distant from the initial price
# than a certain value given by price_fraction
# then assign a negative reward to penalize high price changes
if new_price > self.initial_price * (1 + price_fraction)\
or new_price < self.initial_price * (1 - price_fraction):
# Negative reward to penalize significant price changes
return -1
else:
# Estimate the demand given the new price
demand = self.calculate_demand(new_price)
# Etimate profit given new price and demand
profit = (new_price - self.cost_per_unit) *\
demand *\
(1 - self.error_term)
# Return profit as reward for the agent
return profit
def calculate_demand(self,
price: int) -> int:
'''Calculate demand as:
current demand + delta(demand) + random walk fluctuation =
current demand + elasticity * (price - current price) + random walk fluctuation
Args:
- price: price of the product
'''
return np.floor(self.current_demand + \
self.elasticity * (price - self.current_price) +\
np.random.normal(0, self.random_walk_std))
def fit(self,
num_episodes: int = 1000,
max_steps_per_episode: int = 100) -> None:
'''Fit the agent for a num_episodes number of episodes.
Args:
- num_episodes: number of episodes
- max_steps_per_episode: max number of steps for each episode
'''
# For each episode
for episode in range(num_episodes):
# The state is the current demand level
state = self.current_demand_level
# To interrupt the training procedure
done = False
# The reward is zero at the beginning of the episode
episode_reward = 0
# Keep track of the training steps
step = 0
# Training loop
while not done:
# Depending on the exploration probability
if np.random.rand() < self.exploration_prob:
# Explore a new price ...
action = np.random.randint(self.num_actions)
else:
# ... or exploit prices known to increase the reward
action = np.argmax(self.q_values[state])
# Set the new price given the action (increase, decrease or leave the price as is)
new_price = self.current_price + action - 1
# Calculate the new demand and demand level
new_demand = self.calculate_demand(new_price)
new_demand_level = self.calculate_demand_level(new_demand)
# Estimate the reward (profit) under the current action
reward = self.calculate_reward(new_price)
# Save the reward
episode_reward += reward
# Bellman equation for the Q values
self.q_values[state, action] = self.q_values[state, action] + \
self.learning_rate * \
(reward + self.discount_factor * np.max(self.q_values[new_demand_level]) -\
self.q_values[state, action])
# Update price and demand for the next iteration
self.current_price = new_price
self.current_demand = new_demand
self.current_demand_level = new_demand_level
# Update the step counter
step += 1
# Exit the loop if the max number of steps was reached
# or if the reward increased more than a certain threshold
if step >= max_steps_per_episode or episode_reward >= self.target_reward_increase:
done = True
# Save the training results for plotting
self.episode_rewards.append(episode_reward)
# Acknowledge the accomplishment of the training procedure
self.isfit = True
print("Training completed.")
def get_q_table(self) -> np.ndarray:
'''Return the Q table'''
return self.q_values
def plot_rewards(self, width=1200, height=800) -> None:
'''Plot the cumulative rewards per episode using Plotly.
Args:
- width: width of the plot
- height: height of the plot
'''
# Plot rewards per episode
fig = px.line(
self.episode_rewards,
title = "Rewards per episode <br><sup>Profit</sup>",
labels = dict(index="Episodes", value="Rewards"),
template = "plotly_dark",
width = width,
height = height)
# Style colors, font family and size
fig.update_xaxes(
title_font = dict(size=32, family="Arial"))
fig.update_yaxes(
title_font = dict(size=32, family="Arial"))
fig.update_layout(
showlegend = False,
title = dict(font=dict(size=30)),
title_font_color = "yellow")
fig.update_traces(
line_color = "cyan",
line_width = 5)
# Show the plot
fig.show()
def predict(self,
input_price: int,
input_demand: int) -> Union[int, str]:
'''Predict the next price given an input price and demand.
Args:
- input_price: input price of the product
- input_demand: input demand of the product
'''
# If the model was fit
if self.isfit:
# State equals the current demand level
state = self.calculate_demand_level(input_demand)
# Identify the most profitable action from the Q values
action = np.argmax(self.q_values[state])
# The next price is given by the most profitable action
prediction = input_price + action - 1
# Return the predicted price
return prediction
# If the model was not fit
else:
return "Fit the model before asking a prediction for the next price."
让我们实例化并拟合代理:
# For reproducibility
np.random.seed(62)
# Instantiate the agent class
pricing_agent = DynamicPricingQL(
initial_price = 1000,
initial_demand = 1000000,
elasticity = -0.02,
cost_per_unit = 20)
# Fit the agent
pricing_agent.fit(num_episodes=1000)
Training completed.
训练后可以获得 Q-Table:
pricing_agent.get_q_table()
array([[0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
[7.92000766e+09, 8.01708509e+09, 7.98798684e+09],
[0.00000000e+00, 0.00000000e+00, 0.00000000e+00]])
我们还可以绘制奖励图:
pricing_agent.plot_rewards()
代码片段的输出。图像来源于作者。
我们观察到奖励在训练过程中增加,因为代理通过 Q 值学习了导致利润增加的定价策略。
我们可以通过训练好的代理使用 Q 值来预测下一个价格:
input_price = 500
input_demand = 10000
next_price = pricing_agent.predict(input_price, input_demand)
print(f"Next Price: {next_price}")
Next Price: 499
4. 结论
在这篇文章中,我们探讨了强化学习的关键概念,并介绍了用于训练智能代理的 Q 学习方法。我们还提供了一个从头开始构建的实际 Python 示例。特别地,我们实现了一个动态定价代理,该代理学习了产品的最佳定价策略,以最大化利润。
我们的例子是简化版的。我们旨在从头到尾分享一个功能全面的说明。对于实际应用,我们应考虑以下几点:
-
Q 学习需要离散的动作空间,这意味着连续动作必须被离散化为有限的值集合。因此,我们将价格操作转换为离散的动作集合
A = {+1, -1, 0}
。实际上,定价决策可能更加复杂和连续。 -
状态应捕捉有关环境的相关信息,以帮助智能体做出决策。虽然离散需求水平提供了简单直观的状态表示,但在实际应用中,这种选择可能会显得有限。相反,状态应包含对环境(业务场景)相关的任何特征。例如,在一个关于电子商务平台动态定价的研究中,Liu 等人(2021) 提出了由四类特征构成的状态表示:
-
价格特征
-
销售特征
-
顾客流量特征
-
竞争力特征。
-