便捷的贝叶斯营销组合建模与 PyMC Marketing
原文:
towardsdatascience.com/convenient-bayesian-marketing-mix-modeling-with-pymc-marketing-8b02a9a9c4aa
市场分析
PyMC 团队推出的一个新的亮闪闪的库,值得尝试
·发布在 Towards Data Science ·6 分钟阅读·2023 年 4 月 13 日
–
图片来源:Nathan Fertig 在 Unsplash
你可以通过有多少大公司发布相关软件包来判断一个话题的重要性。在营销组合建模领域,
-
Google 发布了 LMMM
-
Meta 发布了 Robyn
-
PyMC Labs 发布了 PyMC Marketing
-
(我发布了 mamimo 😇)
比营销组合建模更出色的是贝叶斯营销组合建模,这由 Google 和 PyMC Labs 的库提供。尽管 LMMM 也非常有趣,但今天我们将重点关注 PyMC Marketing。
在这篇文章中,你将了解如今构建最先进的贝叶斯营销组合模型有多么简单!
如果你需要回顾一下,请查看我以前的文章,了解贝叶斯营销组合建模的所有内容。
## 贝叶斯营销组合建模在 Python 中通过 PyMC3
一次性估计饱和度、延续效应和其他参数,包括它们的不确定性
towardsdatascience.com](/bayesian-marketing-mix-modeling-in-python-via-pymc3-7b2071f6001a?source=post_page-----8b02a9a9c4aa--------------------------------)
手动过程
在我之前的文章中(见上文),我自己编写了一个贝叶斯营销组合模型。为此,我需要定义一个媒体支出载荷效应的函数,这很麻烦。尽管使用较旧的 PyMC3,它看起来是这样的:
import theano.tensor as tt
def carryover(x, strength, length):
w = tt.as_tensor_variable(
[tt.power(strength, i) for i in range(length)]
)
x_lags = tt.stack(
[tt.concatenate([
tt.zeros(i),
x[:x.shape[0]-i]
]) for i in range(length)]
)
return tt.dot(w, x_lags)
这个方法有效,但不容易解析,也可能需要更高效。此外,使用 Theano 的 PyMC 已过时,因为我现在需要使用 PyTensor,这是基于 Theano 的 Aesara 的一个分支。看来这有一个复杂的历史。
所以我现在很高兴地依赖更专业和通用的代码来实现我的目标。我通过查看他们如何实现载荷效应学到了很多东西。
在继续之前,请确保你已安装 pymc 和 pymc-marketing。我使用 mamba 安装了 PyMC,如他们的 Github中所述,然后通过
pip install pymc-marketing
PyMC 营销
让我们重新访问一下我们之前的例子从我的贝叶斯营销组合建模文章。我们开始导入一个我合成创建的数据集。
import pandas as pd
data = pd.read_csv(
'https://raw.githubusercontent.com/Garve/datasets/4576d323bf2b66c906d5130d686245ad205505cf/mmm.csv',
parse_dates=['Date']
)
数据看起来是这样的:
图片由作者提供。
模型定义
现在,让我们将明星拉到台上并定义模型:
from pymc_marketing.mmm import DelayedSaturatedMMM
mmm = DelayedSaturatedMMM(
data=data,
target_column="Sales",
date_column="Date",
channel_columns=["TV", "Radio", "Banners"],
)
这创建了一个包含每个通道饱和度和载荷效应的模型,类似于我之前手动完成的。因此,我不会详细讨论这个模型从数学角度如何工作的原因。
我们现在可以可视化我们创建的内容:
import pymc as pm
pm.model_to_graphviz(model=mmm.model)
图片由作者提供。
在这里,我们可以看到,广告库存(载荷)首先被应用,然后是饱和度。每个通道有三个参数 alpha
、beta_channel
和 lam
,其中
-
alpha
是介于 0 和 1 之间的载荷率, -
lam
是饱和度率,和 -
beta_channel
是实际的线性回归系数。
为了提供更多背景,简化的模型公式是
图片由作者提供。
c 在所有不同的通道上运行。
模型拟合
拟合模型就像在 scikit-learn 中一样简单:
mmm.fit()
模型推断
在模型训练后,我们可以如下检查参数:
import arviz as az
az.summary(
data=mmm.fit_result,
var_names=["intercept", "beta_channel", "alpha", "lam", "sigma"]
)
我得到了这样的结果:
图片由作者提供。
从 r_hat 列的 1.0 可以判断,链似乎已经很好地收敛,即表中其余结果是可靠的。
我们还可以检查模型认为所有参数的正确值。例如,通道 TV 载荷 alpha[TV]
在 0.465 和 0.515 之间,概率为 94%,如 hdi_3% 和 hdi_97% 列所示。如果模型必须决定一个单一的数字,它将是 0.49,如 mean 列所示。
注意: 在创建此数据集时,我对电视使用了 0.5 的饱和值,对广播使用了 0.2,对横幅使用了 0。我们的 PyMC 模型能够相当不错地捕捉到这一点!
对于视觉型的朋友们:
mmm.plot_channel_parameter(param_name="alpha", figsize=(9, 5))
作者提供的图片。
我们甚至可以使用便捷的方法查看频道贡献
mmm.plot_channel_contribution_share_hdi()
作者提供的图片。
根据模型,电视约占额外销售(相对于基准)的 40%,广播约占 26%,横幅约占 34%。
后验预测检查
我们可以进行后验预测检查,即采样预测(蓝色),并查看它们如何跟随模型(黑色)。
mmm.plot_posterior_predictive(original_scale=True)
作者提供的图片。
看起来很合适!我们甚至可以通过以下方式将信号分解为基线和频道贡献
mmm.plot_components_contributions()
作者提供的图片。
富有洞察力,但除此之外,可能还需要将以下内容添加到库中:
作者提供的图片。
我提交了一个当前仍然开放的拉取请求。你可以在这里查看。
更新: 它已合并!你可以使用
[*plot_grouped_contribution_breakdown_over_time*](https://github.com/pymc-labs/pymc-marketing/blob/a59a89c41e7a1166c61ed2ca4293ff792d726622/pymc_marketing/mmm/base.py#L503)
方法。很高兴能做出贡献!😄
结论
贝叶斯营销组合建模目前是最好的方式来找出哪些营销渠道表现良好,哪些表现不佳。建立这样的模型并不复杂,但仍然远没有像点击拼接 scikit-learn 模型那样简单。
幸运的是,新的 PyMC Marketing 使得贝叶斯营销组合建模变得轻而易举,相较于我们之前手动编码的过程。
不要误解我,我喜欢编码,而且你也必须知道如何编码。但仍然,拥有一个维护良好的包是很好的,它在未来可能会有更多常见的营销组合模型功能。
而且我只涵盖了一些功能。PyMC Marketing 甚至可以:
-
通过将列列表传递到
control_columns
中来有效处理控制变量,然后传递到DelayedSaturatedMMM
类。 -
通过
mmm.plot_contribution_curves()
绘制饱和曲线 -
计算 ROAS,尽管这仍然是手动工作。
欲了解更多信息,请查看 这个很棒的笔记本。
我希望你今天学到了一些新的、有趣的和有价值的东西。感谢阅读!
如果你有任何问题,请在 LinkedIn上给我写信!
如果你想更深入地探索算法的世界,不妨试试我的新出版物**《所有关于算法》**!我还在寻找作者!
从直观的解释到深入的分析,算法通过实例、代码和精彩的内容变得生动起来……
使用 Stable-Baselines3 进行便捷的强化学习
原文:
towardsdatascience.com/convenient-reinforcement-learning-with-stable-baselines3-dccf466b7585
强化学习
无需冗余代码的强化学习
·发布于 Towards Data Science ·10 分钟阅读·2023 年 12 月 9 日
–
作者使用Leonardo Ai创作。
在我之前的强化学习文章中,我展示了如何仅使用少量的 numpy 和 TensorFlow 来实现(深度)Q 学习。虽然这是理解这些算法工作原理的一个重要步骤,但代码往往变得冗长——我甚至仅实现了深度 Q 学习的最基本版本之一。
提升你的智能体,赢得更具挑战性的游戏!
towardsdatascience.com
根据本文的解释,理解代码应该是相当直接的。然而,如果我们真正想要完成任务,我们应该依赖于文档齐全、维护良好且经过优化的库。正如我们不希望一遍遍地实现线性回归一样,我们也不希望对强化学习做同样的事。
在本文中,我将向你展示一个与 scikit-learn 一样易于使用的强化学习库Stable-Baselines3。不过,我们得到的是经过训练的智能体,能够在环境中良好地导航,而不是训练模型来预测标签。
这里是代码和我训练的最佳模型,放在了我的 Github上。
简短回顾
如果你不确定 (深度) Q 学习是什么,我建议阅读我之前的文章。从高层次来看,我们希望训练一个代理,该代理与其环境互动,目标是最大化其总奖励。强化学习中最重要的部分是为代理找到一个良好的奖励函数。
我通常想象一个游戏中的角色寻找方法以获得最高分,例如,马里奥从开始跑到结束而不死 — 最好是尽可能快。
图像由作者提供。
为此,在 Q 学习中,我们为每对 (s, a) 学习 质量值,其中 s 是状态,a 是代理可以采取的动作。Q(s, a) 是在状态 s 下执行动作 a 时的 期望折扣未来奖励。举例来说,处于状态 s = “站在悬崖前面”并执行动作 a = “向前走一步”应该有一个非常低的 Q(s, a) 值。
我们可以将这个 Q 函数转化为 策略;想象一个神奇的指南针,告诉我们在任何给定状态下该做什么。方法很简单:如果我们处于状态 s,只需计算所有可能动作 a 的 Q(s, a) 并选择值最高的动作。完成!
在我其他的文章中,我们已经看到如何使用表格或神经网络来获取这些 Q 值。现在,我们只想放松一下,享受 Stable-Baselines3 的简便性。我们值得拥有。
进入 Stable-Baselines3
我们已经开发了能够玩各种游戏的代理,如 冰冻湖(在不掉入湖中的情况下获得礼物)、出租车(接客人并送到酒店)或 摆杆(平衡一根杆子)。
冰冻湖、出租车和摆杆。图像由作者提供。
我们可以重新创建掌握这些游戏的代理,但让我们从不同的事情开始:山地车!
山地车游戏
在这个游戏中,我们操控一辆车,车需要上山。我们可以采取的动作是向左走、向右走或什么都不做。我们的训练目标是从这里…
一个贪婪的代理,只想直接移动到山顶。图像由作者提供。
… 到这里:
一个智能代理首先获得动力以达到目标。图像由作者提供。
使用 Stable-Baselines3 训练模型极其简单。请看:
import gymnasium as gym
from stable_baselines3 import DQN
env_name = "MountainCar-v0"
env = gym.make(env_name)
config = {
'batch_size': 128,
'buffer_size': 10000,
'exploration_final_eps': 0.07,
'exploration_fraction': 0.2,
'gamma': 0.98,
'gradient_steps': 8, # don't do a single gradient update, but 8
'learning_rate': 0.004,
'learning_starts': 1000,
'policy_kwargs': dict(net_arch=[256, 256]), # we train a neural network with two hidden layers of size 256 each
'target_update_interval': 600, # see below, the target network gets overwritten with the main network every 600 steps
'train_freq': 16, # don't train after every step in the environment, but after 16 steps
}
model = DQN("MlpPolicy", env, verbose=1, **config) # MlpPolicy = train a normal feed-forward neural network
model.learn(total_timesteps=2000, progress_bar=True)
魔法在于找到 config
的良好超参数,但这是我们作为机器学习从业者必须弄清楚的事情。或者让专门的超参数优化工具来处理它。
幕后的故事
我们已经知道 .learn
方法中大部分发生了什么。如果您查看源代码,您会看到我其他文章中的许多老朋友。例如,如果您 查看这里,您会找到类似的代码
for _ in range(gradient_steps):
# Sample replay buffer
replay_data = self.replay_buffer.sample(batch_size, env=self._vec_normalize_env) # type: ignore[union-attr]
with th.no_grad():
# Compute the next Q-values using the target network
next_q_values = self.q_net_target(replay_data.next_observations)
# Follow greedy policy: use the one with the highest value
next_q_values, _ = next_q_values.max(dim=1)
# Avoid potential broadcast issue
next_q_values = next_q_values.reshape(-1, 1)
# 1-step TD target
target_q_values = replay_data.rewards + (1 - replay_data.dones) * self.gamma * next_q_values
# Get current Q-values estimates
current_q_values = self.q_net(replay_data.observations)
我们有一个重放记忆,还有 Q 值更新步骤(1 步 TD 目标)。这应该不再显得过于可怕。值得注意的是,库使用了双重 Q 学习,这是我没有实现的。其思路很简单:我们不是使用一个 Q 值神经网络,而是使用两个。
在上面的源代码中,self.q_net
(称为主网络)是通常被训练的网络。另一方面,self.q_net_target
(称为目标网络)用于生成训练主网络的标签。每隔几个训练周期,目标网络会被设置为主网络,因此您可以将目标网络视为主网络的滞后版本。
如果两者相同,我们使用我们的网络(只有一个)来生成标签,然后更新网络的权重。但这反过来又改变了目标,因此本质上我们尝试学习移动目标——训练可能不稳定。双重 Q 学习通过其双网络方法解决了这个问题。
回调函数
训练耗时较长,如果您的程序崩溃,丢失进度总是令人沮丧。因此,Stable-Baselines3 提供了一些很好的回调函数来保存您的进度。我建议使用 EvalCallback
和 CheckpointCallback
。
from stable_baselines3.common.callbacks import EvalCallback, CheckpointCallback
env_name = "MountainCar-v0"
# callback to check the agent's performance every 1000 steps for 10 episodes
eval_callback = EvalCallback(
eval_env=env,
eval_freq=1000,
n_eval_episodes=10,
best_model_save_path=f"./logs/{env_name}",
log_path=f'./logs/{env_name}',
)
# callback to save the model every 10000 steps
save_callback = CheckpointCallback(save_freq=10000, save_path=f'./logs/{env_name}')
您可以将这些回调函数直接传递到这里:
model.learn(total_timesteps=2000, progress_bar=True, callback=[eval_callback, save_callback])
EvalCallback
还保存了一些不错的性能数据,您可以绘制这些数据。
平均奖励(10 次运行的平均)随时间变化。图片由作者提供。
您可以看到,在大约 40,000 个时间步长内,模型学习得不多。-200 的奖励表示模型没有达到顶部——一个回合在 200 个时间步长后结束。然后,学习突然起飞,直到代理程序稳定地达到了山顶。您可以像这样绘制:
import numpy as np
import pandas as pd
data = np.load(f"./logs/{env_name}/evaluations.npz")
pd.DataFrame({
"mean_reward": data["results"].mean(axis=1),
"timestep": data["timesteps"]
}).plot(
x="timestep",
y="mean_reward",
)
玩 Atari 游戏
好的,很酷,我们打败了一些幼儿园游戏。是时候挑战更具挑战性的内容了:Atari 游戏!对年轻人来说:Atari 是 80 年代视频游戏市场的领导者。他们还发明了游戏 Pong,我们的心爱游戏,由两个杆子打乒乓球组成。
一台我小时候常玩的 Atari 2600。公有领域图片由 Evan Amos 提供。
他们的大多数游戏仍然很简单,但至少它们已经能挑战你了。为了增加趣味,我们将只使用原始 屏幕像素来训练我们的智能体!不再使用内部游戏状态,比如坐标、速度或物体角度。机器必须像人类一样通过观察屏幕并找出该做什么来学习如何玩游戏。
《Breakout》
作为一个例子,让我们使用Breakout,这是一个需要用球破坏方块的游戏。球会跳动,从方块上反弹,也会反弹到我们控制的飞船上。我们可以左右控制“飞船”以保持球在游戏中。不过,让我们看看我们的智能体在主要角色中的游戏场景:
我们的深度 Q 学习智能体在玩《Breakout》。图片由作者提供。
这个智能体在大约 3,000,000 帧的训练中使用了 GPU,并在 GCP(8 vCPUs,30 GB RAM,NVIDIA T4 x 4)上同时训练了 4 个环境。训练大约花费了3 小时。除了使用大型机器外,我还利用了AtariWrapper
来提升性能,该工具将图像缩放到 84 x 84 像素并转为灰度,因为在这个游戏中颜色并不重要。我们还使用了卷积神经网络,而不是简单的前馈神经网络,以便在更短的时间内获得更好的结果。以下是代码:
import gymnasium as gym
from stable_baselines3 import DQN
from stable_baselines3.common.atari_wrappers import AtariWrapper
from stable_baselines3.common.callbacks import CheckpointCallback, EvalCallback
from stable_baselines3.common.vec_env import SubprocVecEnv, VecFrameStack, VecTransposeImage
if __name__ == "__main__":
env_name = "BreakoutNoFrameskip-v4"
env = SubprocVecEnv([lambda: AtariWrapper(gym.make(env_name)) for _ in range(4)]) # train 4 game environments in parallel, scale down images for faster training
env = VecFrameStack(env, n_stack=4) # don't only use a still image for training, but the last 4 frames
env = VecTransposeImage(env) # technical magic for putting the channels of the animation in the first coordinate, i.e., turning HxWxC into CxHxW since Stable-Baselines3 likes it that way
config = {
"batch_size": 32,
"buffer_size": 10000,
"exploration_final_eps": 0.02,
"exploration_fraction": 0.1,
"gamma": 0.99,
"gradient_steps": 4,
"learning_rate": 1e-4,
"learning_starts": 10000,
"target_update_interval": 1000,
"train_freq": 4,
}
eval_callback = EvalCallback(
eval_env=env,
eval_freq=1000,
n_eval_episodes=10,
best_model_save_path=f"./logs/{env_name}",
log_path=f"./logs/{env_name}",
)
save_callback = CheckpointCallback(save_freq=10000, save_path=f"./logs/{env_name}")
model = DQN("CnnPolicy", env, verbose=0, **config) # CnnPolicy creates some default convolutional neural network for us for processing the screen pixels in a more efficient way
model.learn(total_timesteps=10_000_000, progress_bar=True, callback=[eval_callback, save_callback])
注意: Jupyterlab 通常在多进程处理上存在问题,因此你可能需要将此代码粘贴到 .py 文件中并从命令行运行。还要注意,我将网络的输入不仅仅是单个游戏图像,而是四张连续图像,并且使用了以下代码:
env = VecFrameStack(env, n_stack=4)
通过这种方式,智能体不仅可以学习球的方向和速度,还可以学习它的位置。否则,它怎么能知道发生了什么呢?
球会去哪里?图片由作者提供。
4 只是一个超参数,你可以尝试其他值。这个小技巧使得智能体能够在没有任何内部游戏信息的情况下学习如何玩这个游戏。
像往常一样,智能体的性能在各个回合中有些波动。不过,你可以清楚地看到趋势是逐渐上升的:
图片由作者提供。
《太空侵略者》
另一个经典游戏是《太空侵略者》,这是对《Breakout》的回应。如果你不知道:你需要射击外星人并尽量避免被击中。只需在代码中替换一行,我们就可以训练一个智能体,使其在 3,000,000 步训练之前击败一波敌人:
图片由作者提供。
然而,我挑选了这次运行的结果。通常情况下,我的智能体会死得更快,但仍然相当不错:
图片由作者提供。
你可以通过以下方式进行训练:
...
if __name__ == "__main__":
env_name = "SpaceInvadersNoFrameskip-v4"
...
当然,你现在可以重新训练代理来玩所有的 Atari 游戏。
结论
在本文中,我们看到了一种训练代理而不需要太多样板代码的方法。目前,我认为 Stable-Baselines3 就像是强化学习领域的 scikit-learn:你定义模型,稍微配置一下,然后 .learn
游戏。没有比这更简单的了。
尽管如此,我还是提倡了解幕后发生了什么。 否则,当事情不能按预期工作时,你可能会感到迷茫。经典的机器学习或任何其他算法也是如此。首先,至少要理解基础知识,然后再享受使用一个不错的库!
最后,如果你查看库的文档,你会发现它支持更多的学习算法,例如
如果你想要一个深度 Q 学习的不错替代方案,从我所看到的,PPO 似乎很受欢迎。尝试一下所有这些算法,看看是否能找到更适合你的学习问题的方案!但也要确保了解这些方法的工作原理——也许在我未来的文章中!
希望你今天学到了一些新、趣味和有价值的东西。谢谢阅读!
如果你有任何问题,可以在 LinkedIn上联系我!
如果你想深入了解算法的世界,不妨试试我的新出版物《All About Algorithms》!我还在寻找作者!
从直观的解释到深入的分析,算法通过示例、代码和精彩的内容变得生动起来…
概率收敛或分布收敛
原文:
towardsdatascience.com/convergence-in-probability-or-distribution-1766e08125cd
这两者之间有什么区别?
·发布于 Towards Data Science ·6 min read·2023 年 9 月 4 日
–
图片由作者提供。
在你学习统计学的过程中,你是否遇到过概率收敛和分布收敛的概念?你是否曾思考过这些概念最初是为何被引入的?如果有的话,这个故事旨在帮助你回答一些这些问题。
概率收敛
让我们首先深入了解概率收敛的概念,因为它是更容易理解的概念。假设我们有一个随机变量序列:X1、X2、…、Xn,当 n 趋近于无穷大时,如果 Xn 很接近 x 的概率趋近于 1,那么我们可以得出结论,Xn 在概率上收敛于 x。
为什么这样定义?这种定义的合理性源于这样一个事实:无论 n 多大,Xn 永远不会精确等于 x(常量)。我们能做的最多是确定 Xn 必须在多大程度上接近 x,即 Xn 落在 x 周围某个区间的概率。
因此,我们的定义声称,当 n 趋近于无穷大时,Xn 与 x 之间的差异大于 ε 的可能性会降低到一个微小的水平,最终接近于零。此外,ε 可以任意小。
一个概率收敛的例子是样本均值的概念。考虑这样一个场景,我们从均值为 0、标准差为 0.1 的正态分布中反复抽取 n 个样本。如果我们计算这些 n 个样本的样本均值,那么得到的样本均值成为一个随机变量,记作 Xn,并且具有其自己的分布。
那么问题来了:这个分布的性质是什么?当 n=1 时,样本均值实际上等同于单个样本本身,其分布反映了总体分布,特别是均值为 0、标准差为 0.1 的正态分布。
但如果 n=1000 呢?直观上,在这种情况下,我们会期望计算出的样本均值非常接近总体均值,即 0。可以合理假设,当我们反复抽取 1000 个样本并计算样本均值时,数值可能会聚集在 0.001、0.002、-0.001 等附近,没有显著波动。
如果 n=1,000,000 呢?在这种情况下,样本均值非常可能接近 0,任何偏离这个值的情况都极其微小。
这正是概率收敛的本质。随着 n 的增加,随机变量 Xn 的分布变得越来越窄,最终收敛到一个单一的值。
样本均值的抽样分布通过一系列直方图进行了可视化,展示了从正态分布中抽取的样本,样本量分别为[1, 10, 100, 1000]。图片由作者提供。
这种现象不仅发生在从正态分布中抽取的样本中,也会在二项分布中出现。当我们从一个试验且成功概率为 0.5 的二项分布中抽取 n 个样本时,我们观察到的收敛模式与前面的例子非常相似:
样本均值的抽样分布通过一系列直方图进行了可视化,展示了从二项分布中抽取的样本,样本量分别为[1, 10, 100, 1000]。图片由作者提供。
无论我们如何努力将样本均值约束在一个特定的区间内,我们总能找到一个足够大的 n,使得样本均值落入该区间的概率接近 100%。
分布收敛
相反,需要注意的是,并不是每一个随机变量序列都在概率上收敛到一个单一的数字。在许多情况下,随机变量序列不会收敛到特定的数字,而是收敛到一个具有自己独特分布的随机变量。在这种情况下,我们将这种行为称为分布收敛。
CDFn(t) 表示给定序列中随机变量 Xn 的累积分布函数,而 CDF(t) 表示随机变量 X 的累积分布函数。
定义指出,当考虑一个随机变量序列 Xn 时,该序列中的随机变量的累积分布函数(CDF)会随着 n 的增加而收敛到随机变量 X 的累积分布函数。
这个概念的一个说明性例子是标准化样本均值。下面,你会找到标准化样本均值的定义:
Z 代表标准化样本均值,其中 n 是抽取的样本数量,X_bar 是样本均值,𝜇 是总体均值,𝜎 是总体标准差。
当你从总体中抽取 n 个样本时,可以通过以下步骤获得标准化样本均值:计算这 n 个样本的均值,从中减去总体均值,将结果乘以样本大小的平方根,然后除以总体标准差。
有趣的是,虽然样本均值本身在概率上收敛到总体均值,但标准化样本均值在分布上收敛到均值为零、标准差为一的正态分布随机变量。
直观上,我们可以将标准化样本均值概念化为样本均值的重新缩放版本。回到之前样本均值收敛的图示,我们观察到其分布随着样本大小的增加逐渐类似于正态分布,只是逐渐变得更加狭窄。通过将样本均值乘以样本大小的平方根,我们有效地扩展了分布,使其保持正态分布的形状。
下面的可视化图示展示了标准化样本均值在从正态分布和二项分布中抽取样本的收敛情况:
标准化样本均值的抽样分布通过一系列直方图进行可视化,展示了不同样本大小下从正态分布中抽取的样本,具体为 [1, 10, 100, 1000]。图片由作者提供。
标准化样本均值的抽样分布通过一系列直方图进行可视化,展示了不同样本大小下从二项分布中抽取的样本,具体为 [1, 10, 100, 1000]。图片由作者提供。
我们为什么要关注这个?
从某种意义上说,存在一种终极的收敛形式:按分布收敛。我们可以将按概率收敛视为按分布收敛的一种特例,其中最终的分布变得退化并收敛到一个单一值。但为什么这种区分很重要呢?
首先,样本均值收敛于真实总体均值的观察使我们能够估计该总体均值。这种估计过程在各种实际情况中都是常见的。例如,每当我们做出诸如“邻居很爱打听”的概括时,我们实际上是依赖于这样的观点:我们有限的样本来自我们自己的经验,最终会收敛到实际的总体均值。这个原理被称为大数法则。
更为关键的是标准化样本均值收敛于正态分布的观察。正是这个事实使我们能够进行假设检验,并对特定观察结果是由于偶然性还是潜在因果过程做出明智的评估。这一现象更正式地被称为中心极限定理。
链接
使用 LangChain 将对话作为有向图
原文:
towardsdatascience.com/conversations-as-directed-graphs-with-lang-chain-46d70e1a846c
构建一个聊天机器人,旨在了解关于新潜在客户的关键信息。
·发表于 Towards Data Science ·18 分钟阅读·2023 年 9 月 25 日
–
图片由 Daniel Warfield 使用 MidJourney 制作。所有图片均由作者提供,除非另有说明。
在这篇文章中,我们将使用 LangChain 在房地产环境中进行线索资格审查。我们设想一个场景,新潜在客户首次联系房地产代理。我们将设计一个系统,与新潜在线索沟通,以提取关键信息,然后由房地产代理接手。
这对谁有用? 任何对在实际环境中应用自然语言处理(NLP)感兴趣的人。
这篇文章的高级程度如何? 这个例子在概念上很直接,但如果你对 Python 和语言模型没有牢固的理解,可能会很难跟上。
先决条件: 基础的 Python 编程知识以及对语言模型的高级理解。
问题描述
这个用例直接受到了我作为承包商时收到的工作请求的启发。潜在客户拥有一家房地产公司,并发现他们的代理在每次对话开始时花费了大量时间执行相同的重复任务:线索资格审查。
线索资格审查是房地产术语中对线索的初步筛选。获取他们的联系信息、预算等。这是一个相当广泛的术语,具体细节可能因组织而异。在这篇文章中,我们将“资格审查”线索的以下信息视为有效:
-
姓名: 线索的姓名。
-
联系信息: 线索的电子邮件或电话号码。
-
融资: 他们的月租预算。
-
准备情况: 他们能多快与代理见面。
方法
天真的方法
尽管大型语言模型非常强大,但它们需要对用例进行适当的上下文化才能持续成功。例如,你可以给语言模型一个提示,类似于:
"You are a real-estate agent trying to qualify a new client.
Extract the following information:
- email
- phone
....
Once all information has been extracted from the client, politely
thank them you will be re-directing them to an agent"
然后,你可以将新的客户放入一个与该提示初始化的模型的聊天房间中。这将是开始在特定业务环境中尝试 LLM 的好方法,同时也是开始意识到 LLM 对某些类型反馈的脆弱性的好方法。如果用户问了一个无害但无关的问题,比如“你昨晚看比赛了吗?”或“是的,我在路上走时看到你们的建筑了。”对某些用例来说这可能是个严重的问题,也可能不是,但对话周围的严格结构可以帮助保持对话的正常进行。
对话作为有向图
我们可以将对话框架设为有向图,其中每个节点代表某个对话状态,每条边代表改变对话状态的动力,如完成的介绍或获得的信息。
在我们尝试解决的问题的背景下,有向图遍历的示例
这是我们为这个问题构建的最基本的有向图。值得注意的是,这种方法可以根据系统的需要轻松地扩展、收缩或以其他方式改变。
例如,如果你的客户持续向聊天机器人询问体育问题,而这在初始设计阶段没有预料到,那么你可以添加相关逻辑以检查此类问题并作出适当回应。
处理与体育相关的问题的示例修改。我们将继续使用原始的简单图,但很容易看出,通过向现有有向图中添加额外元素,可以缓解出现的边界情况和性能较差的场景。
当创建一个以自然方式与人类互动的新系统时,它必须能够轻松迭代以应对新出现的意外问题。为了本示例的目的,我们将保持简单,但可扩展性是这种方法的核心能力之一。
关键技术
我们将使用 LangChain 来完成大部分繁重的工作。具体来说,我们将使用:
-
一个 LLM: 我们将使用 OpenAI 的 Text DaVinci 3 模型。
-
输出解析: 我们将使用 LangChain 的 Pydantic 解析器将结果解析成易于处理的格式。
我们还将从头开始实现一个有向图,并在该图中内置一些功能以实现所需的功能。
模型
在这个示例中,我们使用的是 OpenAI 的Text Davinci 3 模型。虽然你可以使用几乎任何现代的大型语言模型,但我选择使用这个特定模型,因为它在 LangChain 示例和文档中被广泛使用。
LangChain 尽力成为一个强大而稳健的框架,但处理大型语言模型的工作很繁琐。不同的模型对给定的提示可能会有截然不同的行为。我发现 Text Davinci 3 对来自 LangChain 的提示反应一致。
LangChain 允许你使用自托管模型、Hugging Face 上免费托管的模型或来自其他多个来源的模型。随意尝试你选择的模型;它们之间的切换相当简单(尽管根据我的经验,你可能需要根据你使用的特定模型调整提示)。
Text Davinci 3 是一个变换器模型,随意阅读以下文章以获取更多信息:
又一篇探讨现代机器学习浪潮的文章。希望你会觉得这篇直观且发人深省…
LangChain 解析
LangChain 拥有多种解析器,旨在与大型语言模型配合使用。我们将使用 PydanticOutputParser。
LangChain 解析器不仅从 LLM 响应中提取关键信息,还修改提示以引导 LLM 提供更多可解析的响应。使用 Pydantic 解析器时,你首先定义一个表示你希望从 LLM 中获得结果格式的类。假设你想从 LLM 中获得一个笑话,包括开场白和笑点:
""" Define the data structure we want to be parsed out from the LLM response
notice that the class contains a setup (a string) and a punchline (a string.
The descriptions are used to construct the prompt to the llm. This particular
example also has a validator which checks if the setup contains a question mark.
from: https://python.langchain.com/docs/modules/model_io/output_parsers/pydantic
"""
class Joke(BaseModel):
setup: str = Field(description="question to set up a joke")
punchline: str = Field(description="answer to resolve the joke")
@validator("setup")
def question_ends_with_question_mark(cls, field):
if field[-1] != "?":
raise ValueError("Badly formed question!")
return field
然后你可以定义要发送给模型的实际查询。
"""Defining the query from the user
"""
joke_query = "Tell me a joke about parrots"
这个查询随后被解析器修改,结合用户的查询和有关最终解析格式的信息,以构建对 LLM 的提示。
"""Defining the prompt to the llm
from: https://python.langchain.com/docs/modules/model_io/output_parsers/pydantic
"""
parser = PydanticOutputParser(pydantic_object=Joke)
prompt = PromptTemplate(
template="Answer the user query.\n{format_instructions}\n{query}\n",
input_variables=["query"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
input = prompt.format_prompt(query=joke_query)
print(input.text)
这个特定示例的提示如下:
Answer the user query.
The output should be formatted as a JSON instance that conforms to the JSON schema below.
As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.
Here is the output schema:
{“properties”: {“setup”: {“title”: “开场白”, “description”: “设置笑话的提问”, “type”: “string”}, “punchline”: {“title”: “笑点”, “description”: “解决笑话的回答”, “type”: “string”}}, “required”: [“setup”, “punchline”]}
Tell me a joke about parrots
注意用户的查询“告诉我一个关于鹦鹉的笑话”是如何与关于所需最终格式的信息结合的。
然后,这个格式化的查询可以传递给模型,解析器可以用来提取结果:
"""Declaring a model and querying it with the parser defined input
"""
model_name = "text-davinci-003"
temperature = 0.0
model = OpenAI(model_name=model_name, temperature=temperature)
output = model(input.to_string())
parser.parse(output)
这是这个特定示例的结果:
"""The final output, a Joke object with a setup and punchline attribute
"""
Joke(setup="Why don't parrots make good detectives?",
punchline="Because they're always repeating themselves!")
PydanticOutputParser 既强大又灵活,这就是它在 LangChain 中最常用的解析器的原因。我们将在本篇文章中进一步探讨这个解析器。OutputFixingParser 和 RetryOutputParser 是另外两个非常有用的输出解析器,在本篇文章中不会详细探讨,但在此用例中肯定可以使用。
对话作为一个有向图
我们将把对话抽象为一个有向图。
最基本形式的这种方法。它是一个对话状态的序列,当从对方接收到特定信息时,对话状态就会进展。
每个节点和边都需要自定义,但会遵循相同的一般结构:
节点和边的工作原理。蓝色的框表示对话状态,因此蓝色框表示单个节点及其功能。红色的框表示在对话状态之间过渡所需的步骤,因此红色框表示边及其功能。
值得注意的是,LangChain 具有类似的结构,称为 Chain。我们在这篇文章中不会讨论 Chains,但它们对于直接和顺序的 LLM 任务很有用。
定义节点和边
这里我们开始编码一个支持 LLM 的定向图,具有上述核心结构。我们将使用 Pydantic 解析器来处理输入验证步骤以及实际内容解析。
我提供了代码作为参考,但不要被代码的长度吓到。你可以快速浏览代码,或者完全不参考代码。最终的笔记本可以在这里找到:
编辑描述
colab.research.google.com](https://colab.research.google.com/drive/1oSKFO2ho6BN__pZp0uQNwP-HWeXGPpKU?source=post_page-----46d70e1a846c--------------------------------#scrollTo=IgzZooHUoIQw)
通用工具
出于演示目的,所有这些内容将存在于一个 Jupyter 笔记本中,模型之间的最终往返将在最后一个单元格中执行。为了提高可读性,我们将定义三个函数:一个用于模型输出给用户,一个用于用户输入给模型,另一个用于打印演示所需的关键信息,如解析结果。
"""Defining utility functions for constructing a readable exchange
"""
def system_output(output):
"""Function for printing out to the user
"""
print('======= Bot =======')
print(output)
def user_input():
"""Function for getting user input
"""
print('======= Human Input =======')
return input()
def parsing_info(output):
"""Function for printing out key info
"""
print(f'*Info* {output}')
定义边
正如代码所示,边缘接受一些输入,将其与条件进行比较,如果条件满足,则解析输入。边缘包含相关的逻辑来记录尝试失败的次数,并负责告诉更高级别的单元是否应该沿着边缘在定向图中继续前进。
from typing import List
class Edge:
"""Edge
at its highest level, an edge checks if an input is good, then parses
data out of that input if it is good
"""
def __init__(self, condition, parse_prompt, parse_class, llm, max_retrys=3, out_node=None):
"""
condition (str): a True/False question about the input
parse_query (str): what the parser whould be extracting
parse_class (Pydantic BaseModel): the structure of the parse
llm (LangChain LLM): the large language model being used
"""
self.condition = condition
self.parse_prompt = parse_prompt
self.parse_class = parse_class
self.llm = llm
#how many times the edge has failed, for any reason, for deciding to skip
#when successful this resets to 0 for posterity.
self.num_fails = 0
#how many retrys are acceptable
self.max_retrys = max_retrys
#the node the edge directs towards
self.out_node = out_node
def check(self, input):
"""ask the llm if the input satisfies the condition
"""
validation_query = f'following the output schema, does the input satisfy the condition?\ninput:{input}\ncondition:{self.condition}'
class Validation(BaseModel):
is_valid: bool = Field(description="if the condition is satisfied")
parser = PydanticOutputParser(pydantic_object=Validation)
input = f"Answer the user query.\n{parser.get_format_instructions()}\n{validation_query}\n"
return parser.parse(self.llm(input)).is_valid
def parse(self, input):
"""ask the llm to parse the parse_class, based on the parse_prompt, from the input
"""
parse_query = f'{self.parse_prompt}:\n\n"{input}"'
parser = PydanticOutputParser(pydantic_object=self.parse_class)
input = f"Answer the user query.\n{parser.get_format_instructions()}\n{parse_query}\n"
return parser.parse(self.llm(input))
def execute(self, input):
"""Executes the entire edge
returns a dictionary:
{
continue: bool, weather or not should continue to next
result: parse_class, the parsed result, if applicable
num_fails: int the number of failed attempts
}
"""
#input did't make it past the input condition for the edge
if not self.check(input):
self.num_fails += 1
if self.num_fails >= self.max_retrys:
return {'continue': True, 'result': None, 'num_fails': self.num_fails}
return {'continue': False, 'result': None, 'num_fails': self.num_fails}
try:
#attempting to parse
self.num_fails = 0
return {'continue': True, 'result': self.parse(input), 'num_fails': self.num_fails}
except:
#there was some error in parsing.
#note, using the retry or correction parser here might be a good idea
self.num_fails += 1
if self.num_fails >= self.max_retrys:
return {'continue': True, 'result': None, 'num_fails': self.num_fails}
return {'continue': False, 'result': None, 'num_fails': self.num_fails}
我在代码中创建了一些单元测试 这里,展示了边缘的功能。
定义节点
现在我们有了一个处理输入验证和解析的边,我们可以定义一个处理对话状态的节点。节点请求用户输入,并将该输入传递给来自该节点的定向边。如果没有一条边成功执行,节点会再次询问用户输入。
class Node:
"""Node
at its highest level, a node asks a user for some input, and trys
that input on all edges. It also manages and executes all
the edges it contains
"""
def __init__(self, prompt, retry_prompt):
"""
prompt (str): what to ask the user
retry_prompt (str): what to ask the user if all edges fail
parse_class (Pydantic BaseModel): the structure of the parse
llm (LangChain LLM): the large language model being used
"""
self.prompt = prompt
self.retry_prompt = retry_prompt
self.edges = []
def run_to_continue(self, _input):
"""Run all edges until one continues
returns the result of the continuing edge, or None
"""
for edge in self.edges:
res = edge.execute(_input)
if res['continue']: return res
return None
def execute(self):
"""Handles the current conversational state
prompots the user, tries again, runs edges, etc.
returns the result from an adge
"""
#initial prompt for the conversational state
system_output(self.prompt)
while True:
#getting users input
_input = user_input()
#running through edges
res = self.run_to_continue(_input)
if res is not None:
#parse successful
parsing_info(f'parse results: {res}')
return res
#unsuccessful, prompting retry
system_output(self.retry_prompt)
实现了这个,我们可以开始看到对话的进行。我们将实现一个请求联系信息的节点,以及两个边:一个尝试解析有效的邮箱,另一个尝试解析有效的电话号码。
"""Defining an example
this example asks for contact information, and parses out either an email
or a phone number.
"""
#defining the model used in this test
model_name = "text-davinci-003"
temperature = 0.0
model = OpenAI(model_name=model_name, temperature=temperature)
#Defining 2 edges from the node
class sampleOutputTemplate(BaseModel):
output: str = Field(description="contact information")
condition1 = "Does the input contain a full and valid email?"
parse_prompt1 = "extract the email from the following text."
edge1 = Edge(condition1, parse_prompt1, sampleOutputTemplate, model)
condition2 = "Does the input contain a full and valid phone number (xxx-xxx-xxxx or xxxxxxxxxx)?"
parse_prompt2 = "extract the phone number from the following text."
edge2 = Edge(condition2, parse_prompt2, sampleOutputTemplate, model)
#Defining A Node
test_node = Node(prompt = "Please input your full email address or phone number",
retry_prompt = "I'm sorry, I didn't understand your response.\nPlease provide a full email address or phone number(in the format xxx-xxx-xxxx)")
#Defining Connections
test_node.edges = [edge1, edge2]
#running node. This handles all i/o and the logic to re-ask on failure.
res = test_node.execute()
这是几个具有单一节点的对话示例:
Example 1)
======= Bot =======
Please input your full email address or phone number
======= Human Input =======
input: Hey, yeah I'm so excited to rent from you guys. My email is hire@danielwarfield.dev
*Info* parse results: {'continue': True, 'result': sampleOutputTemplate(output='hire@danielwarfield.dev'), 'num_fails': 0, 'continue_to': None}
Example 2)
======= Bot =======
Please input your full email address or phone number
======= Human Input =======
input: do you want mine or my wifes?
======= Bot =======
I'm sorry, I didn't understand your response.
Please provide a full email address or phone number(in the format xxx-xxx-xxxx)
======= Human Input =======
input: ok, I guess you want mine. 413-123-1234
*Info* parse results: {'continue': True, 'result': sampleOutputTemplate(output='413-123-1234'), 'num_fails': 0, 'continue_to': None}
Example 3)
======= Bot =======
Please input your full email address or phone number
======= Human Input =======
input: No
======= Bot =======
I'm sorry, I didn't understand your response.
Please provide a full email address or phone number(in the format xxx-xxx-xxxx)
======= Human Input =======
input: nope
======= Bot =======
I'm sorry, I didn't understand your response.
Please provide a full email address or phone number(in the format xxx-xxx-xxxx)
======= Human Input =======
input: I said no
*Info* parse results: {'continue': True, 'result': None, 'num_fails': 3, 'continue_to': None}
在示例 1 中,用户包括了一些无关的信息,但在响应中有一个有效的邮箱。在示例 2 中,用户在第一次响应中没有有效的邮箱或电话号码,但在第二次响应中有一个。在示例 3 中,用户没有有效的响应,其中一个边缘放弃并允许对话继续。
值得注意的是,从用户体验的角度来看,这种方法感觉有点机械。虽然在本文中没有探讨,但很容易想象用户输入如何被用来构造系统的输出,可能通过字符串格式化或请求 LLM 格式化响应。
定义对话
现在我们有了节点和边缘,并定义了它们的功能,我们可以将所有这些整合在一起,创建最终的对话。我们之前已经覆盖了一个大致的蓝图,但让我们修改它,以更好地反映图实际要做的事情。请回顾以下内容:
-
节点有初始提示和重试提示
-
边缘有一个条件、一个解析提示和一个解析结构。条件是一个关于用户输入的布尔问题。如果条件满足,则根据解析提示和用户输入解析解析结构。这是通过请求大型语言模型将用户输入重新格式化为可解析的表示,使用 pydantic 解析器来完成的。
让我们根据这些定义构建一个对话图:
我们将要实现的对话图,包括所有必要的节点和边缘参数。
从上面的图示可以看出,已经做了一些提示工程以适应某些边缘情况。例如,预算的解析提示允许解析器解析用户响应,例如“我的预算大约是 1.5k”。
由于 LLMs 的灵活性,究竟如何实现这样的图完全取决于工程师。如果价格解析在未来成为问题,可以有几个边,每个边有不同的条件和解析提示。例如,可以想象一个边检查预算是否超过某个值,从而暗示他们提供的是年度预算而不是月度预算。这个系统的强大之处在于可以无缝添加或删除这些修改。
实现对话图
我们已经完成了所有的重头戏,现在只需要编码并查看它是如何工作的。以下是实现代码:
"""Implementing the conversation as a directed graph
"""
# Defining Nodes
name_node = Node("Hello! My name's Dana and I'll be getting you started on your renting journey. I'll be asking you a few questions, and then forwarding you to one of our excellent agents to help you find a place you'd love to call home.\n\nFirst, can you please provide your name?", "I'm sorry, I don't understand, can you provide just your name?")
contact_node = Node("do you have a phone number or email we can use to contact you?", "I'm sorry, I didn't understand that. Can you please provide a valid email or phone number?")
budget_node = Node("What is your monthly budget for rent?", "I'm sorry, I don't understand the rent you provided. Try providing your rent in a format like '$1,300'")
avail_node = Node("Great, When is your soonest availability?", "I'm sorry, one more time, can you please provide a date you're willing to meet?")
#Defining Data Structures for Parsing
class nameTemplate(BaseModel): output: str = Field(description="a persons name")
class phoneTemplate(BaseModel): output: str = Field(description="phone number")
class emailTemplate(BaseModel): output: str = Field(description="email address")
class budgetTemplate(BaseModel): output: float = Field(description="budget")
class dateTemplate(BaseModel): output: str = Field(description="date")
#defining the model
model_name = "text-davinci-003"
temperature = 0.0
model = OpenAI(model_name=model_name, temperature=temperature)
#Defining Edges
name_edge = Edge("Does the input contain a persons name?", " Extract the persons name from the following text.", nameTemplate, model)
contact_phone_edge = Edge("does the input contain a valid phone number?", "extract the phone number in the format xxx-xxx-xxxx", phoneTemplate, model)
contact_email_edge = Edge("does the input contain a valid email?", "extract the email from the following text", emailTemplate, model)
budget_edge = Edge("Does the input contain a number in the thousands?", "Extract the number from the following text from the following text. Remove any symbols and multiply a number followed by the letter 'k' to thousands.", budgetTemplate, model)
avail_edge = Edge("does the input contain a date or day? dates or relative terms like 'tommorrow' or 'in 2 days'.", "extract the day discussed in the following text as a date in mm/dd/yyyy format. Today is September 23rd 2023.", dateTemplate, model)
#Defining Node Connections
name_node.edges = [name_edge]
contact_node.edges = [contact_phone_edge, contact_email_edge]
budget_node.edges = [budget_edge]
avail_node.edges = [avail_edge]
#defining edge connections
name_edge.out_node = contact_node
contact_phone_edge.out_node = budget_node
contact_email_edge.out_node = budget_node
budget_edge.out_node = avail_node
#running the graph
current_node = name_node
while current_node is not None:
res = current_node.execute()
if res['continue']:
current_node = res['continue_to']
以下是一些示例对话:
======= Bot =======
Hello! My name's Dana and I'll be getting you started on your renting journey. I'll be asking you a few questions, and then forwarding you to one of our excellent agents to help you find a place you'd love to call home.
First, can you please provide your name?
======= Human Input =======
input: daniel warfield
*Info* parse results: {'continue': True, 'result': nameTemplate(output='daniel warfield'), 'num_fails': 0, 'continue_to': <__main__.Node object at 0x7b196801dc60>}
======= Bot =======
do you have a phone number or email we can use to contact you?
======= Human Input =======
input: 4131231234
======= Bot =======
I'm sorry, I didn't understand that. Can you please provide a valid email or phone number?
======= Human Input =======
input: my phone number is 4131231234
*Info* parse results: {'continue': True, 'result': phoneTemplate(output='413-123-1234'), 'num_fails': 0, 'continue_to': <__main__.Node object at 0x7b196801c610>}
======= Bot =======
What is your monthly budget for rent?
======= Human Input =======
input: 1.5k
*Info* parse results: {'continue': True, 'result': budgetTemplate(output=1500.0), 'num_fails': 0, 'continue_to': <__main__.Node object at 0x7b196801c7c0>}
======= Bot =======
Great, When is your soonest availability?
======= Human Input =======
input: 2 days
*Info* parse results: {'continue': True, 'result': dateTemplate(output='09/25/2023'), 'num_fails': 0, 'continue_to': None}
======= Bot =======
Hello! My name's Dana and I'll be getting you started on your renting journey. I'll be asking you a few questions, and then forwarding you to one of our excellent agents to help you find a place you'd love to call home.
First, can you please provide your name?
======= Human Input =======
input: Hi Dana, my name's mike (michael mcfoil), it's a pleasure to meet you!
*Info* parse results: {'continue': True, 'result': nameTemplate(output='Michael Mcfoil'), 'num_fails': 0, 'continue_to': <__main__.Node object at 0x7b19681087c0>}
======= Bot =======
do you have a phone number or email we can use to contact you?
======= Human Input =======
input: yeah, you can reach me at mike at gmail
======= Bot =======
I'm sorry, I didn't understand that. Can you please provide a valid email or phone number?
======= Human Input =======
input: oh, sorry ok it's mike@gmail.com
*Info* parse results: {'continue': True, 'result': emailTemplate(output='mike@gmail.com'), 'num_fails': 0, 'continue_to': <__main__.Node object at 0x7b1968109960>}
======= Bot =======
What is your monthly budget for rent?
======= Human Input =======
input: I can do anywhere from 2 thousand to 5 thousand, depending on the property
*Info* parse results: {'continue': True, 'result': budgetTemplate(output=5000.0), 'num_fails': 0, 'continue_to': <__main__.Node object at 0x7b196810a260>}
======= Bot =======
Great, When is your soonest availability?
======= Human Input =======
input: does october 2nd work for you?
======= Bot =======
I'm sorry, one more time, can you please provide a date you're willing to meet?
======= Human Input =======
input: october 2nd
*Info* parse results: {'continue': True, 'result': dateTemplate(output='10/02/2023'), 'num_fails': 0, 'continue_to': None}
======= Bot =======
Hello! My name's Dana and I'll be getting you started on your renting journey. I'll be asking you a few questions, and then forwarding you to one of our excellent agents to help you find a place you'd love to call home.
First, can you please provide your name?
======= Human Input =======
input: je m'appelle daniel warfield
*Info* parse results: {'continue': True, 'result': nameTemplate(output='Daniel Warfield'), 'num_fails': 0, 'continue_to': <__main__.Node object at 0x7b196801c7c0>}
======= Bot =======
do you have a phone number or email we can use to contact you?
======= Human Input =======
input: mi número de teléfono es 410-123-1234
*Info* parse results: {'continue': True, 'result': phoneTemplate(output='410-123-1234'), 'num_fails': 0, 'continue_to': <__main__.Node object at 0x7b196801ec20>}
======= Bot =======
What is your monthly budget for rent?
======= Human Input =======
input: Mein monatliches Budget beträgt 3.000
*Info* parse results: {'continue': True, 'result': budgetTemplate(output=3000.0), 'num_fails': 0, 'continue_to': <__main__.Node object at 0x7b196801d390>}
======= Bot =======
Great, When is your soonest availability?
======= Human Input =======
input: אני יכול להיפגש מחר
======= Bot =======
I'm sorry, one more time, can you please provide a date you're willing to meet?
======= Human Input =======
input: Yes karogh yem handipel vaghy
======= Bot =======
I'm sorry, one more time, can you please provide a date you're willing to meet?
======= Human Input =======
input: I can meet tomorrow
*Info* parse results: {'continue': True, 'result': dateTemplate(output='09/24/2023'), 'num_fails': 0, 'continue_to': None}
结论
在本文中,我们将一个潜在客户资格的用例格式化为有向图,实施了必要的解析功能和数据结构,并制作了一个示例图,该图从用户那里提取关键信息。正如示例对话所示,这个系统并不完美,但由于有向图的性质,我们可以轻松添加新节点,以缓解某些边界情况的影响。
尽管本文未讨论,但还有许多方法可以改进这个系统:
-
我们可以使用不同的 LangChain 解析器来尝试重试或纠正查询。
-
我们可以使用 LLM 缓存来尝试缓存某些常见响应,从而节省预算。
-
我们可以将该系统与矢量数据库连接,以便对知识库进行问答。
-
我们可以使用 LLM 来构建用户的提示,并提供有关对话的上下文,以鼓励更自然的回应。
尽管我的合同工作没有成功,但我认为这种方法突显了一个灵活且强大的框架,该框架具有可扩展性,适用于各种应用。
关注获取更多信息!
我描述了机器学习领域的论文和概念,重点放在实际和直观的解释上。
致谢: 本文档中的所有图片均由丹尼尔·沃菲尔德创建,除非另有来源说明。您可以将此帖中的任何图片用于个人非商业用途,只需引用本文, danielwarfield.dev
,或两者兼用。
将平坦表格转换为 Power Query 中的良好数据模型
原文:
towardsdatascience.com/converting-a-flat-table-to-a-good-data-model-in-power-query-46208215f17a
当将一个宽 Excel 表格加载到 Power BI 中时,我们最终会得到一个次优的数据模型。我们可以做什么来创建一个好的数据模型?什么才算是“好的”数据模型?让我们深入了解一下。
·发布在 Towards Data Science ·12 分钟阅读·2023 年 12 月 22 日
–
引言
只是办公室里的另一天:一个客户打电话让我修复他 Power BI 报告中的某些内容。我查看了数据,发现有一个宽表,包含 30、40 或更多列。
我问了一个自然的问题:“这个表格的来源是什么?”
答案是“Excel,还能有什么?”
“当然,”我想。
我的下一个问题:“我们能从中建立一个好的数据模型吗?”
我的客户:“为什么?”
这就是我们现在的状况。
理想情况下,我会将源文件导入到关系数据库中,并将数据转换为一个不错的数据模型。
不幸的是,我的客户通常不愿意为那些初看上去对他们没有好处的东西付费。
但是为什么我想要一个好的数据模型?一个平坦的表格不是也很好吗?
再看一眼,确实如此。
几千行的数据没有问题。
但一旦数据量增加,问题可能会积累。
这是对“为什么?”问题的简短回答:
那么,为什么需要一个好的数据模型?
有很多原因让我想要一个“好的数据模型”。
两个简短的原因是效率和可用性。
当将数据类型分隔到不同的表中,并通过关系将它们连接起来时,Power BI 可以以更高效的方式工作。
此外,通过去除冗余,可以减少 Power BI 文件的大小。
我强烈推荐阅读 SQLBI 上的相关文章。你可以在下方的参考文献部分找到链接,以获取这个问题的详细答案。
其次,想要使用数据模型的人可以在单独的表中找到所有列(即属性),而不是在一个长的按字母排序的列表中搜索。
现在我们知道了为什么,接下来的问题是,什么是一个好的数据模型?
答案是:一个星型模式。
什么鬼是星型模式?
如果你已经知道什么是星型模式,可以跳到下一节。
我的原始数据模型如下:
图 1 — 原始数据模型,包含一个有 27 (!) 列的表(图由作者提供)
中心表是图中间的那个大表,名为“Azure_UsageDetails”。我们称之为“原始表”。
其他两个表是用于时间报告的日期表。
当你仔细查看原始表时,你可以找到一些有趣的列,例如:
-
BillingProfile
-
MeterName
-
SubscriptionName
这些列的共同点是,虽然表中有大约 55,000 行,这些列只有少量不同的值。
这意味着基数是低的。
此外,这些列描述了我的数据。它们不包含任何值,例如“Quantity”或“UnitPrice”。
目标是将这些列移动到单独的表中,称为维度表,以获得只包含这些列中的唯一值或值组合的较短表。
看一下下面的图示:
图 2 — 作为星型模式的目标数据模型(图由作者提供)
现在,你可以理解为什么它被称为星型模式。
我们可以称之为鱿鱼模式,但没有人会理解我们。
正如你所见,每个维度表都有一个 ID 列与中心表中的相应 ID 列相连。
ID 列应始终是整数数据类型。绝不要文本列。
星型模型中心的表称为“事实”表。它仅包含每个事件、交易或数据点的值。
让我们开始吧
好的,现在我们需要执行一些准备步骤:
-
查找低基数的列。
-
查找需要分组的列。
-
为每个维度表定义名称。
-
替换所有空单元格。
-
复制包含数据的表。
首先,打开 Power Query。
其次,我们启用数据分析:
图 3 — 在 Power Query 中启用数据分析(图由作者提供)
Power Query 仅显示前 1000 行的分析结果。启用整个数据集的分析可能是个好主意。
在左下角,点击文本“基于前 1000 行的列分析”,然后切换到“基于整个数据集的列分析”选项:
图 4 — 在整个数据集上开启分析(图由作者提供)
根据数据量,加载整个数据集并计算分析可能需要一些时间。
当我查看我的数据时,我发现这里有前三个候选项:
图 5 — 维度的前三个候选(作者制作的图)
在这些中,我可以创建三个维度:
-
计费配置文件
-
订阅
-
计量器
如你所见,我为维度表赋予了复数名称,每个表包含一个或多个该实体的实例。
现在,查看以“Meter”开头的列:
图 6 — 所有四个计量器列(作者制作的图)
我注意到两个关键细节:
-
这四列的基数不同。
-
我有很多空行在 MeterRegion 列中(76%)。
根据基数(不同值的数量),我认为我可以为计量器构建一个层次结构。
-
一级:计量器区域
-
第二级:计量器类别
-
第三级:计量器子类别
-
叶级:计量器名称
来自 Fact 表的数据将与 Meter Name 列建立关系。更准确地说,ID 列将基于叶级别但从所有四列的不同组合中创建。
组合的原因是计量器名称可能会多次出现,并分配给不同的计量器子类别。
第二步,我们必须用有意义的内容替换 Meter Region 列中的空行,以避免在顶级层次上出现名为(Null)的层次节点。
为了实现这一点,我右键点击列名,然后点击“替换值”。
在此功能的对话框中,我输入null作为要查找的值,并输入“空”作为要替换的值:
图 7 — 使用文本“空”替换空行(作者制作的图)
结果如下,MeterName 列中没有空行:
图 8 — MeterRegion 没有空行。但其他列仍有空白行。(作者制作的图)
接下来,我必须检查所有打算作为维度列的列,并替换空值。
我通过检查 Profiling 区域中的“空”行找到了这些列(见红色标记的行)。
你可以使用另一个词代替“空”作为空行的替换文本。例如,“无”。
下一步是为了避免在构建维度表时出现重复的列名:
我将所有以“Id”结尾的列重命名为“GUID”(除了 InvoiceID,这列将保持不变)。
接下来,随着数据的清理,我可以复制我的表并开始构建维度表。
我右键点击 Azure_UsageDetail 表,点击“复制”:
图 9 — 复制表(作者制作的图)
现在,我将表重命名为 Azure_UsageDetail_Copy。
但我不需要在我们的数据模型中复制这些数据。
所以,我关闭了该表的加载:
图 10 — 禁用将表加载到 Power BI 数据模型中的功能(图由作者提供)
这个选项的效果是,我可以将此表作为所有后续操作的源,而无需在 Power BI 中存在此表。
现在,我可以使用原始表的副本来构建我的维度表。
一个简单的维度表
第一个维度将用于订阅。
我需要以下步骤:
-
创建一个引用复制表的表。
-
移除所有其他列。
-
移除所有重复项。
-
添加一个 ID 列。
完成这些步骤后,我必须将列添加到原始表中:
-
合并两个表。
-
展开 ID 列。
-
从原始表中移除订阅列。
右键点击复制的表,点击“引用”(参见下文“重复”和“引用”表的区别)。
现在,我必须将表重命名为 Subscriptions(双击表)。
我可以使用“选择列”或“移除其他列”功能,去除除了两个订阅列之外的所有列。
我选择两个列(使用 Shift 点击)并右键点击:
图 11 — 在选择了两个“订阅”列后,删除所有其他列(图由作者提供)
下一步是移除所有重复项:
图 12 — 从表中移除重复项(图由作者提供)
现在,我有一个包含两列和两行的小表。
为了添加一个 ID 列,我使用索引功能:
图 13 — 添加一个索引列(图由作者提供)
然后,我将新创建的“索引”列重命名为“SubscriptionId”。
现在,订阅维度表已经完成。
图 14 — 完成的订阅维度表(图由作者提供)
在此之后,我必须用新的 ID 列替换原始表中的现有“订阅”列:
我使用“合并查询”功能将两个表连接在一起:
图 15 — 将新的维度表与原始表合并(图由作者提供)
选择两个列至关重要,以确保正确的行被分配。
你可以检查底部信息行中的正确分配:当两个数字相同时,一切正常。
为了获取 Id 列,我必须展开合并的表:
图 16 — 展开合并的表,只包括 Id 列,不带原始列名称作为前缀(图由作者提供)
结果,我得到一个附加的列,其匹配的 SubscriptionId。
我对所有“简单”维度表重复相同的步骤。
但有时,我需要在构建维度表时添加更多步骤。
更复杂的内容
在转换数据模型的过程中,我注意到一组列是相关的:
-
所有“账单”列
-
ChargeType
-
Frequency
-
PublisherType
-
PricingModel
-
InvoiceNo
所有这些列都与发票主题相关联。
因此,我决定将它们分组到一个维度表中。
第一步与上述相同:
1. 创建一个引用表。
2. 删除所有不需要的列。
3. 为 ID 添加索引列。
4. 将列重命名为更具用户友好的名称。
但后来我注意到,我可以使用 Billing Period 列来提取 Billing Month,然后删除这个列。这对我的报告将带来好处。
因此,我添加了一个自定义列,公式如下:
Date.MonthName([Billing Period]) & "-" & Text.From(Date.Year([Billing Period]))
这是结果:
图 17 — Billing Month 的结果(图由作者提供)
为确保该列可以根据月份编号进行排序,我添加了一个额外的自定义列:
(Date.Year([Billing Period])*100) + Date.Month([Billing Period])
我可以使用 Power BI 中的按列排序功能,根据这个新的 BillingMonthNum 列对 Billing Month 列进行排序。
下一步是设置正确的数据类型(Billing Month 列的文本类型和 BillingMonthNum 列的“整数”类型)。
另一种添加排序列的方法是通过排名。
例如,看看 Invoice No 列:
图 18 — 从“Invoice No”列提取(图由作者提供)
想象一下,我们想要为这个列添加一个排序机制。
索引列将不起作用,因为我们会得到一个连续的数字,而无论内容如何。
但是,正如你所见,我们有几行的 Invoice No 为 null。
因此,使用索引在 Power BI 中对该列进行排序是不可能的,因为我们会有相同的内容但数字不同。
我们可以通过使用以下表达式为新的自定义列解决此问题:
Table.AddRankColumn(#"Changed Type","InvoiceNoRank", "Invoice No",[RankKind=RankKind.Dense])
-
#“Changed Type” 是前一步骤(输入表)的名称。
-
“InvoiceNoRank” 是新列的名称。
-
“Invoice No” 是排名将被计算的列。
-
最后一个参数 [RankKind=RankKind.Dense] 是最重要的。
使用 [RankKind=RankKind.Dense] 我告诉函数,相同发票编号的行必须获得相同的排名,并且数字范围必须连续。
你可以在这里获取更多关于这个功能的详细信息:
[## 使用 Power Query 在 Power BI 中添加预计算的排名
如何在 Power Query 和 Power BI 中添加排名,有多种方法可以将排名列添加到表格中,你…
结果列如下所示:
图 19 — 排名列的结果(图由作者提供)
你可以看到所有空的 Invoice No 行都得到排名 1。随后的发票编号为 2、3、4 等等。
合并此表到事实表时,我选择所有可以唯一标识每一行的列。
在这种情况下,使用列 BillingPeriods 和 Invoice No 就足够了。
最后步骤——完成数据模型
最后,我可以完成数据模型。
但在我跳到 Power BI 之前,我会在 Power Query 中移除原始表中的所有过时列,并将表重命名为“FactAzureUsageDetails”。
将数据加载到 Power BI 后,我必须检查自动创建的关系,以确保所有关系都在 ID 列之间。
有时,Power BI 会在名称列之间创建关系,这并不理想。
由于我已经基于旧的数据模型创建了一个报告,我必须修复所有使用了现在已移动到维度表中的列的可视化和度量值。
最终结果
这是最终的数据模型:
图 20——作为干净的星型模式的最终数据模型(图示由作者提供)
这正是我想要实现的目标。
有趣的是,新生成的 pbix 文件现在比原始文件大。
不过,我之前谈论的是 800 kB 对 750 kB。原始数据的大小约为 20 MB。
我已经进行了这样的更改,结果是 pbix 文件比之前小了很多。
我认为我的数据量如此之小,以至于额外的复杂性导致 pbix 文件变大。
结论
修改数据模型的工作量不小。但也不至于高得令人恐惧。无论如何,探索这种方法是值得的,以便在 Power BI 中获得更好的解决方案,而不是拥有一个列数众多、难以导航的表。
想象一下,你可以将一个主题的所有列分组到一个维度表中。即使这些列按字母顺序排序,你也可以很容易找到它们。与这些列都在原始表中的情况相比,这种方式更为高效。
在我看来,这个话题应该得到更多的关注。我甚至看到我的同事在 Power BI 中使用非常宽的表来构建解决方案。我的第一个评论是,“你为什么要使用这样一张表?为什么你的数据模型中没有星型模式?”。
回答总是:“因为数据以这种形式出现,我没有时间修改结构”。
但只要性能问题开始出现,我的第一步是分析和优化数据模型。
我推荐使用 DAX Studio 和 VertiPaq Analyzer (SQLBI Tools Page) 来获取数据模型的统计信息并发现潜在的问题。
你可以在这里获得关于如何使用这个组合的简要介绍:
[## 如何使用 Vertipaq Analyzer 和 DAX Studio 进行 Power BI 模型分析 - FourMoo | Power BI |…
我展示了如何将 Vertipaq Analyzer 与你的 Power BI 模型配合使用,以理解什么在消耗内存以及如何……
www.fourmoo.com](https://www.fourmoo.com/2020/11/11/how-to-use-vertipaq-analyzer-with-dax-studio-for-power-bi-model-analysis/?source=post_page-----46208215f17a--------------------------------)
或在您最喜欢的搜索引擎中搜索“如何使用 VertiPaq Analyzer”。
我正在考虑很快就这个主题写一篇文章。
如果您对这样的内容感兴趣,请在评论中告诉我。
图片由 Denys Nevozhai 提供,来源于 Unsplash
参考文献
数据来自我的私人 Azure 订阅。我从 Azure 门户下载每月消耗量,并将数据导入到 Azure SQL 数据库中。
一篇关于为什么我们应该将星型架构作为数据模型的 SQLBI 文章:
[## Power BI - 星型架构还是单表 - SQLBI
本文分析了一个经典建模问题:构建模型时,使用常规的星型架构还是…
www.sqlbi.com](https://www.sqlbi.com/articles/power-bi-star-schema-or-single-table/?source=post_page-----46208215f17a--------------------------------)
请参见这里以了解 Power Query 中 Duplicate 和 Reference 功能的区别:
[## Power BI 中的 Reference 与 Duplicate;Power Query 基础回顾
当您在 Power Query 和 Power BI 中处理表格和查询时,您可以通过这些选项将它们复制…
radacad.com](https://radacad.com/reference-vs-duplicate-in-power-bi-power-query-back-to-basics?source=post_page-----46208215f17a--------------------------------) [## 订阅 Salvatore Cagliari 的更新。
订阅后,Salvatore Cagliari 发布文章时会收到电子邮件。通过注册,您将创建一个 Medium 帐户,如果您还没有的话…
medium.com](https://medium.com/@salvatorecagliari/subscribe?source=post_page-----46208215f17a--------------------------------)
将文本转换为数值形式的 TFIDF 向量化器:逐步指南
图片由 Mohamed Nohassi 提供,来源于 Unsplash
如何手动计算 Tfidf 值及使用 sklearn
·发表于 Towards Data Science ·阅读时间 6 分钟 ·2023 年 10 月 25 日
–
TFIDF 是一种将文本转换为数值形式以用于机器学习或人工智能模型的方法。换句话说,TFIDF 是一种从文本中提取特征的方法。这比我在上一篇文章中讨论的 CountVectorizer() 方法更为复杂。
TFIDF 方法为每个词提供一个分数,表示该词的有用性或相关性。它衡量了词语的使用情况,与文档中其他词汇相比。
本文将手动计算 TFIDF 分数,以便您清楚地了解 TFIDF 的概念。在最后,我们还将查看如何使用 sklearn 库中的 TFIDF 向量化器。
这包括两个部分:TF 和 IDF。让我们看看每个部分是如何工作的。
TF
TF 解释为“词频”。TF 可以计算为:
TF = 单词在文档中的出现次数
或者
TF = (文档中出现的次数)/ (文档中的词数)
让我们做一个例子。我们将找到该文档中每个词的 TF:
我的名字是莉莉
让我们分别看一下每个公式的例子。
TF = 单词在文档中的出现次数
如果我们取第一个公式,即文档中单词出现的次数,则“MY”的 TF 是 1,因为它只出现了一次。
以相同的方式,词语的 TF
‘name’ = 1,‘is’ = 1,‘Lilly’ = 1
现在,让我们使用第二个公式。
TF = (文档中出现的次数)/ (文档中的词数)
如果我们使用第二个公式,公式的第一部分(文档中的出现次数)是 1,第二部分(文档中的单词数量)是 4。
因此,单词‘MY’的 TF 为 1/4 或 0.25。
同样,单词的 TF 为
name = ¼ = 0.25,is = ¼ = 0.25,Lilly = ¼ = 0.25。
IDF
IDF 的详细解释是逆文档频率。
这是公式,
idf = 1 + LN[n/df(t)]
或
idf = LN[n/df(t)]
其中,n = 可用的文档数量,以及
df = 术语出现的文档数量
根据 sklearn 库的文档
idf = LN[(1+n) / (1+df(t))] + 1(默认设置)
或
idf = LN[n / df(t)] + 1(当 smooth_idf = True 时)
我们不会处理所有四个公式。我们只讨论两个公式。你将会明白的。
为了演示 IDF,单个文档是不够的。我将使用这三个文档:
我的名字是 Lilly
Lilly 是我妈妈最喜欢的花
我妈妈喜欢花
这次我们使用这个公式进行练习:
IDF = LN[n/df(t)]
如果我们先考虑‘My’,n 是 3,因为这里有 3 个文档,而 df(t)也是 3,因为‘My’出现在所有三个文档中。
IDF(MY) = LN(3/3) = 0(因为 ln(1)是 0)
我们将再处理一个单词以便清楚理解。以‘name’为例。
对于单词‘name’,n 将和之前相同,因为文档数量是 3,但 df(t)将是 1。因为单词‘name’只出现在一个文档中。
IDF(name) = ln(3/1) = 1.1(我使用了 Excel 的 LN 函数)
sklearn 库如何计算 TFIDF?
sklearn 库使用这两个公式来计算 TF 和 IDF:
TF = 单词在文档中的出现次数
idf = LN[(1+n) / (1+df(t))] + 1
如果我使用相同的三个文档,单词‘MY’的计算如下:
TF(My) = 1
IDF(My) = LN((1+3)/(1+3)) + 1 = 1
TFIDF 的公式是:
TFIDF = TF * IDF
因此,‘My’的 TFIDF 为:
TFIDF(My) = 1 * 1 = 1
对于单词‘name’:
TF(name) = 1
IDF(name) = LN((1+3)/(1+1)) + 1 = 1.69
TFIDF(name) = 1 * 1.69 = 1.69
同样,所有单词的 TFIDF 如下:
作者图片
sklearn 的 tfidf 向量化器将值归一化到 0 到 1 的范围。为此,我们需要每个文档的 tfidf 的平方和:
归一化的 tfidf 是:
单词的 tfidf 值/文档的平方和
如果我们考虑单词 My。文档-1 中‘My’的归一化 tfidf 是:
tfidf_normalized(My) = 1.00 / 7.871 = 0.356
单词‘mom’在文档-3 中的 tfidf 是:
tfidf_normalized(name) = 1.42 / 9.005 = 0.472
再次,单词‘mom’在文档-2 中的 tfidf 是:
tfidf_normalized(name) = 1.42 / 13.009 = 0.392
看起来‘mom’这个词在文档 2 中的相关性比在文档 3 中高一些
所有单词的归一化 tfidf 如下:
作者图片
现在,我们应该检查sklearn 库中的 tfidf 向量化器的工作方式。
首先,从 sklearn 库中导入 Tfidf 向量化器,并定义用于特征提取的文本:
from sklearn.feature_extraction.text import TfidfVectorizer
text = ["My name is Lilly",
"Lilly is my mom’s favorite flower",
"My mom loves flowers"]
在下一个代码块中,
第一行调用 TfidfVectorizer 方法,并将其保存到名为 vectorizer 的变量中。
第二行将文本转换为向量化器
第三行将其转换为数组以进行显示
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(text)
X.toarray()
输出:
array([[0\. , 0\. , 0\. , 0.4804584 , 0.4804584 ,
0\. , 0\. , 0.37311881, 0.63174505],
[0.49482971, 0.49482971, 0\. , 0.37633075, 0.37633075,
0\. , 0.37633075, 0.2922544 , 0\. ],
[0\. , 0\. , 0.5844829 , 0\. , 0\. ,
0.5844829 , 0.44451431, 0.34520502, 0\. ]])
将此数组转换为 DataFrame 并使用单词作为列名将会很有帮助。
import pandas as pd
pd.DataFrame(X.toarray(), columns = vectorizer.get_feature_names())
作者图片
你可以使用我在关于 CountVectorizer 的教程中解释的相同参数来细化或限制特征数量。请随时检查。
结论
本教程详细解释了 Tfidf Vectorizer 的工作原理。尽管使用 sklearn 库中的 Tfidf Vectorizer 非常简单,但理解其背后的概念也很重要。当你了解一个向量化器是如何工作的时,就更容易决定哪种向量化器是合适的。
随时在Twitter上关注我,并点赞我的Facebook页面。
本教程的视频版本:
更多阅读
## Keras 和 Tensorflow 中的完整步骤教程
数据准备、深度学习模型开发和网络训练的完整工作代码
数据清理、分析、可视化、特征选择、预测建模
## 30 个非常有用的 Pandas 函数用于日常数据分析任务
Pandas 备忘单
towardsdatascience.com](/30-very-useful-pandas-functions-for-everyday-data-analysis-tasks-f1eae16409af?source=post_page-----bb9330562ae3--------------------------------) ## 如何在 TensorFlow 中定义自定义层、激活函数和损失函数
逐步解释和包含完整代码的示例
towardsdatascience.com ## OpenCV 中用于图像预处理的形态学操作详解
腐蚀、膨胀、开运算、闭运算、形态学梯度、顶帽/白帽和黑帽,配有示例进行解释
towardsdatascience.com ## 使用自编码器方法进行 TensorFlow 和 Keras 中的异常检测
一种前沿的无监督方法,用于噪声去除、降维、异常检测等
towardsdatascience.com
将井测数据从 DLIS 文件转换为 LAS 文件格式
原文:
towardsdatascience.com/converting-well-logging-data-from-dlis-files-to-las-file-format-ccc1e7eee9b0
与地球科学和岩石物理数据文件格式的工作
·发布于Towards Data Science ·8 分钟阅读·2023 年 7 月 25 日
–
图片由Mika Baumeister提供,发布在Unsplash上。
在石油和天然气行业的地球科学领域中,使用了多种格式来存储井测数据和岩石物理数据。最常见的两种格式是 LAS 文件和 DLIS 文件。
LAS文件是平面的 ASCII 文件,可以使用任何文本编辑器轻松读取,而 DLIS 文件是结构化的二进制文件,包含有关记录环境以及记录数据的表格。DLIS 文件更难处理,不能轻易在文本编辑器中打开,这可能会妨碍理解其中的内容。
幸运的是,已经开发出几个优秀的 Python 库,使得访问 LAS 和 DLIS 文件中的数据变得更加容易。
lasio是一个旨在轻松读取和处理 LAS 文件的库,即使这些文件由于格式不正确或其他错误而存在问题。
dlsio是由 Equinor ASA 开发的一个 Python 库,用于读取 DLIS 文件和日志信息标准 79(LIS79)文件。该库旨在减少探索这些文件的负担和努力,而无需详细了解 DLIS 结构。
使用这两个库,我们将探讨如何从 DLIS 文件中提取数据并导出到 LAS 文件。这将使我们能够创建一种更易于处理的文件格式,并且文件大小更小,因为它将包含我们需要的相关数据。
如果你想查看其他关于如何处理这些文件格式的文章,以下内容可能会引起你的兴趣:
-
使用 Python 加载多个井日志 LAS 文件
-
使用 Python 将 CSV 文件转换为 LAS 文件
-
使用 Python 从 DLIS 加载井日志数据
导入库并加载数据
对于本教程,我们需要两个主要的库。DLISIO,它允许我们读取和处理 DLIS 文件的内容,以及 LASIO,它允许我们处理 LAS 文件。
from dlisio import dlis
import lasio
导入库后,我们接下来需要读取 DLIS 文件。这可以通过以下语法完成。
f, *tail = dlis.load('Data/NLOG Data/NPN_TVN-1_23_Dec_2009_E3_Main_SONIC_057PUC.DLIS')
由于 DLIS 文件可能包含多个逻辑文件(代表不同的测井过程或不同的处理级别),我们需要将这些逻辑文件分隔到两个变量中: f
和 *tail
。如果有多个逻辑文件,第一个将被放入 f
变量中,其余的将放入 *tail
变量中。
我们可以应用类似的逻辑从选定的逻辑文件中提取来源信息。由于该文件包含一个单一的来源,我们可以在 origin
变量上调用 describe
方法。
origin, *origin_tail = f.origins
origin.describe()
当我们运行上述代码时,我们会得到以下总结,详细描述了该数据集的来源。我们将在构建 LAS 文件时使用这些信息。
DLIS 逻辑文件来源的总结。图片由作者提供。
创建 LAS 文件
在我们开始提取数据之前,我们首先需要创建一个空的 LAS 文件对象。可以这样完成:
las_file = lasio.LASFile()
文件创建后,我们可以通过以下方式确认其为空。
las_file.curves
这将返回一个空列表( []
),这是我们在此阶段所需的。
为了使事情变得更简单,我们可以将 DLIS 文件中的一些关键信息提取到变量中。在这个示例中,我们将提取井名、油田名和操作公司。
如果我们希望使这段代码可重复使用,这是一种方法,可以避免每次都手动提供这些信息。
well_name = origin.well_name
field_name = origin.field_name
operator = origin.company
现在我们已经将关键信息以变量形式存储,我们可以开始填充我们的 LAS 文件头。通过从 LAS 文件中访问关键属性,并将 HeaderItem
设置为新值来完成这项工作。
你会注意到我们手动添加了一个日期,因为这个属性似乎没有被 DLISIO 曝露。
las_file.well['WELL'] = lasio.HeaderItem('WELL', value=well_name)
las_file.well['FLD'] = lasio.HeaderItem('FLD', value=field_name)
las_file.well['COMP'] = lasio.HeaderItem('COMP', value=operator)
las_file.well['DATE'] = '2009-12-23'
从 DLIS 中提取数据并写入 LAS 格式
由于 DLIS 文件可能包含大量的日志曲线和数组,我们可能需要考虑提取一小部分数据。这可以防止我们在预期的用例中被过多无关的曲线所困扰。
在这个例子中,我们将提取以下曲线。
请注意,我们需要提取“TDEP”曲线,这是我们的主要参考深度曲线。
columns_to_extract = ['TDEP', 'BS', 'DT', 'DTSM', 'VPVS']
现在我们已经准备好 LAS 文件并从 DLIS 文件中提取了头数据,我们现在可以遍历 DLIS 中一个框架内的通道。
在这个例子中,我们将访问第一个框架中的内容。如果你想了解如何检查 DLIS 文件(特别是这个文件)的内容,你可以在下面的文章中找到相关信息。
与 Pandas 和 dlisio 一起探索井日志数据
towardsdatascience.com
此外,以下代码假设所有曲线都有一个维度,即没有钻孔图像数组数据或声波波形。再次建议查看我之前的文章以获取处理方法之一。
frame = f.frames[0]
for channel in frame.channels:
# If the channel name is in the list of channels to extract
if channel.name in columns_to_extract:
curves = channel.curves()
# If the channel name is 'TDEP', convert to 'DEPT'
if channel.name == 'TDEP':
channel_name = 'DEPT'
description = 'DEPTH'
# If the units are 0.1 in then convert to metres
if channel.units == '0.1 in':
curves = curves * 0.00254
unit = 'm'
else:
unit = channel.units
else:
description = channel.long_name
channel_name = channel.name
unit = channel.units
# Add the data to the LAS file
las_file.append_curve(
channel_name,
curves,
unit=unit,
descr=description
)
正如上面的代码所示,我们基本上遍历了所选框架内的所有可用通道(曲线),并检查它们的名称是否与我们尝试选择的名称匹配。
如果我们在列表中遇到一个曲线,我们首先检查它是否是 TDEP(深度)曲线;如果是,我们需要进行一些小调整。这包括将名称更改为 DEPT,并检查单位是否为 0.1 英寸。如果是,我们需要将深度单位转换为米。
一旦我们检查了是否有深度曲线,所有其他曲线将使用它们存储的信息提取出来。
在检查完曲线后,我们可以将其附加到 LAS 文件对象中,并传入相关信息。
一旦创建了 LAS 对象,我们可以使用以下调用来检查我们的曲线信息是否已被传递。
las_file.curves
这将返回一个包含每个曲线信息的列表。我们可以看到名称、单位和描述都已成功添加,并且根据数据的形状,我们可以假设数据值也已被传递。
从 DLIS 文件中提取出来的 LASIO LAS 文件曲线信息。图片由作者提供。
现在我们已经设置好了头信息和曲线数据,我们可以开始使用以下命令写出我们的 LAS 文件。
las_file.write('output.las')
如果写入时没有产生错误,我们可以在我们喜欢的文本编辑器中打开 LAS 文件。当我们这样做时,应该能看到以下文件。
使用 LASIO 写入文件后,在文本编辑器中查看的导出 LAS 文件。图片由作者提供。
你会注意到我们在头部部分仍然缺少信息。这可以直接在文本编辑器中编辑,或者你可以使用额外的代码来确保这些参数像我们处理日期时那样被写出。
你还会注意到深度范围被反转了,这在某些 LAS 文件中是正常的。你最喜欢的岩石物理软件包应该能轻松读取它。
如果我们想进一步确认文件是否已正确创建,我们可以使用lasio.read()
函数将其加载回笔记本中。
new_las = lasio.read('output.las')
new_las.header
当我们查看 LAS 文件的头部部分时,会得到以下输出,这确认了数据已被正确读取。
将 LAS 文件加载回 Jupyter 后,LASIO 的头部摘要。图片由作者提供。
摘要
本教程展示了如何轻松将存储在 DLIS 文件中的数据转换为更可读的 LAS 文件格式。这是使用两个非常流行的 Python 库:DLISIO 和 LASIO 实现的。
此处展示的过程主要适用于单维日志曲线。任何数组数据或高分辨率数据需要以不同的方式评估。
本教程中使用的数据
NLOG.nl 的数据可以免费下载和使用。数据许可的完整详情可以在这里找到,但知识产权部分提供了使用的摘要:
NLOG.NL 不对通过本网站提供的信息(域名、商标权、专利及其他知识产权除外)声明任何权利。用户可以在未经 NLOG.NL 事先书面许可或有权方合法同意的情况下,以任何方式复制、下载、披露、分发或简化本网站提供的信息。用户还可以复制、重复、处理或编辑信息和/或布局,前提是注明 NLOG.NL 为来源。
感谢阅读。在你离开之前,你应该一定要订阅我的内容,并将我的文章发送到你的收件箱中。 你可以在这里完成订阅!
其次,你可以通过注册会员,获得完整的 Medium 体验,并支持其他数千名作者和我。只需每月$5,你就可以全面访问所有精彩的 Medium 文章,并有机会通过你的写作赚取收入。
如果你通过 我的链接注册, 你将直接支持我,并且不会额外增加你的费用。如果你这样做,非常感谢你的支持。
卷积解释——卷积神经网络简介
CNNs 的基本构建块
·发布在 Towards Data Science ·8 分钟阅读·2023 年 12 月 27 日
–
”www.flaticon.com/free-icons/neural-network
" 标题为“神经网络图标。” 神经网络图标由 Freepik 创建 — Flaticon。
我最近的文章系列是关于神经网络的,我们从简单的 感知器 讲到复杂的架构以及如何处理 深度学习中的常见问题。如果你感兴趣,可以在这里查看系列文章:
神经网络
查看列表9 个故事
神经网络在 计算机视觉 领域取得了显著进展。这是自驾车和面部识别的 AI!
然而,大多数人所了解的常规全连接神经网络并不适合许多现实中的图像识别任务。它在著名的 MNIST 数据集上有效,但其图像尺寸为 28×28 像素。
高清 (HD) 图像有 1280×720 像素。这大约是 1,000,000 像素,这意味着输入层需要 1,000,000 个神经元。更不用说隐藏层所需的数百万个权重,这使得常规神经网络因维度复杂性而不适用。
那么,我们该怎么做?
卷积神经网络!
卷积神经网络 (CNN) 现在是大多数计算机视觉任务的金标准。它们不像全连接层那样,而是具有 部分 连接的层,并且共享其权重,从而减少模型的复杂性。
例如,对于全连接神经网络层中的每个神经元,我们需要处理一张 100×100 像素的图像的 10,000 个权重。然而,CNN 只需 25 个神经元即可处理相同的图像。
在这篇文章中,我们将深入探讨 CNN 的基本构建块,卷积。
直觉
就像机器学习中的许多事物一样,CNN 也受到自然的启发。计算机科学家观察了大脑中的 视觉皮层 的工作方式,并将类似的概念应用于神经网络。
视觉皮层不会同时处理图像中的所有像素。生物神经元只对 感受野 中的小区域做出反应。然而,这些小区域会重叠,使动物能够理解整个图像。
此外,一些具有相同感受野的生物神经元只能检测不同方向的线条,这是 滤波 的一个例子。一些神经元还具有更大的感受野,这意味着高层神经元是低层神经元的组合。
这些生物学发现启发了 1979 年的 新认知机。随着时间的推移,新认知机演变成了卷积神经网络,这就是我们今天所拥有的!
什么是卷积?
CNN 的命名来源于 [卷积](https://en.wikipedia.org/wiki/Convolution#:~:text=In%20mathematics%20(in%20particular%2C%20functional,is%20modified%20by%20the%20other.) 这个数学操作,其定义如下:
连续卷积定理。作者用 LaTeX 表示的方程式。
离散卷积定理。作者用 LaTeX 表示的方程式。
-
f∗g:** 函数 f 和 g 之间的卷积。
-
t: 卷积被评估的点。
-
f(τ): 在点 τ 处的函数 f 值。
-
g(t−τ): 被 τ 移动并在 t 处评估的 g 值。
这个表达式并没有直观地告诉我们卷积是什么。让我们用更通俗的术语来解释一下。
卷积的作用是混合两个函数。在上述情况下,我们有输入的 f,并且我们在 f 上滑动某个函数 g(称为内核)。
-
输入:这是一个函数,在我们的情况下是图像,用于分析或操作。
-
内核:一个小矩阵,用于对图像应用视觉效果,例如模糊、锐化、边缘检测等。
-
滑动内核:内核一次滑过输入图像一个像素,计算新的像素值。
-
计算输出:每个像素的输出是通过将输入和内核的重叠值相乘并求和这些乘积来计算的。然后,通过除以内核中的元素数量进行归一化。
-
结果:输出是一个新图像,这个图像已经被内核转换过。
示例卷积
让我们通过一些视觉示例来了解图像处理的简单卷积示例。
在下面的图示中,我们有一个输入的灰度图像,其尺寸为 5x5 像素,以及一个 3x3 的内核,其中全是 1s,这将产生模糊效果(特别是一个 箱型模糊)。
示例卷积用于在灰度图像上应用模糊效果。图示由作者创建。
像素值越小,图像越暗。值为 0 表示黑色,255 表示白色,中间的值则是灰度值。
如果我们取中间的像素(用红色高亮显示),其卷积输出是通过将每个像素值与内核中对应的元素相乘并求和这些乘积来计算的。然后,通过内核中的元素数量进行归一化,以确保图像不会变亮或变暗。
以下是此过程的逐步讲解。
[30*1 + 30*1 + 30*1] +
[30*1 + 70*1 + 30*1] +
[30*1 + 30*1 + 30*1]
= 30 + 30 + 30 + 30 + 70 + 30 + 30 + 30 + 30 = 310
pixel value = 310 / 9 ~ 34
然后对输入图像中的所有其他像素(仅在内核适合的地方)重复此操作,以生成所示输出。
请注意,结果图像的像素值现在比原始图像的像素值更接近。这是因为我们的内核对邻域中的像素值进行了平均,产生了模糊效果。
这种卷积可以很容易地在 Python 中实现,并显示结果图像:
# Import packages
import numpy as np
from scipy.signal import convolve2d
import matplotlib.pyplot as plt
# Input Image
image = np.array([
[10, 10, 10, 10, 10],
[10, 30, 30, 30, 10],
[10, 30, 70, 30, 10],
[10, 30, 30, 30, 10],
[10, 10, 10, 10, 10]
])
# Kernel
kernel = np.array([
[1, 1, 1],
[1, 1, 1],
[1, 1, 1]
]) / 9
# Convolution
result = convolve2d(image, kernel, mode='same', boundary='fill', fillvalue=0)
# Plot
plt.figure(figsize=(10, 5))
# Input Image
plt.subplot(1, 2, 1)
plt.imshow(image, cmap='gray')
plt.title('Input Image')
plt.axis('off')
# Output Image
plt.subplot(1, 2, 2)
plt.imshow(result, cmap='gray')
plt.title('Output Image')
plt.axis('off')
plt.show()
模糊图像的示例。图表由作者在 Python 中创建。
用于生成此图像的代码可以在我的 GitHub 上找到:
[## Medium-Articles/Neural Networks/convolution_image.py at main · egorhowell/Medium-Articles
我在我的中等博客/文章中使用的代码。通过创建帐户来为 egorhowell/Medium-Articles 的开发做出贡献…
某些卷积核结构对图像有不同的效果。下面是一些常用卷积核及其效果:
卷积核及其效果。作者的图示。
一份完整的卷积核及其操作列表可以在这里找到。
维度、步幅与填充
输出维度
你可能已经注意到,与原始图像相比,应用卷积核会导致图像尺寸减小。推导输出尺寸的公式是:
输出维度。作者的 LaTeX 公式。
-
H_in 和 W_in 是输入图像的高度和宽度。
-
H_filter 和 W_filter 是卷积核的高度和宽度。
-
Padding_height 和 Padding_width 是应用于输入图像高度和宽度的填充量。
-
Stride_height 和 Stride_width 是卷积核在输入图像高度和宽度上的步长。
步幅
步幅是我们在输入图像上滑动卷积核的像素数。步幅为 1 表示卷积核移动一个像素,步幅为 2 表示卷积核每次移动两个像素。滑动可以是垂直、水平,或根据设置同时进行。
增加步幅会导致:
-
减少输出图像的尺寸大小。
-
由于操作较少,计算成本更低。
-
输入图像的视野范围更大。
-
跳过像素会导致信息丢失。
步幅为 1 的示例。作者的图示。
填充
卷积的一个问题是我们往往会丢失像素和图像边缘的信息。这取决于卷积核的使用次数。角落像素只会被使用一次,而中间像素则会使用得更多。
如果我们多次应用卷积核,这在 CNN 中会发生,输出图像会比原始输入图像小很多。这不好,因为我们会丢失大量信息,特别是关于图像边界的信息。
解决这个问题的一种方法是对输入图像的边缘进行零填充。下面是使用前面部分图像的示例:
零填充。作者的图示。
输入图像现在是7x7像素。使用上述公式,我们可以验证,应用一个3x3的核到这个填充图像上将得到一个5x5尺寸的输出。
应用填充时输出维度的示例计算。方程由作者用 LaTeX 表示。
这将增加周边像素的利用率,并使输出图像具有与原始输入图像相同的尺寸。
零填充是相同填充的一种示例。完全不填充称为有效填充。还有另一种类型叫做因果填充,它是不对称的,通常应用于时间序列和自然语言处理。
我们不一定总是要使用零填充,尽管这是一种最常见的技术。以下是一些其他填充策略的列表:
摘要与进一步思考
卷积神经网络(CNN)是目前计算机视觉任务的金标准。它们的主要特点是利用卷积数学运算,这使我们能够将两个函数“融合”在一起。这种方法在图像处理中通过在我们的图像上应用一个矩阵(即核),来实现模糊和锐化等效果。这样我们就能操控和分析图像,这是 CNN 的基础,也是它们如何“看”图像的方式。
另一个话题!
我有一个免费的新闻通讯,数据分享,在其中我每周分享成为更好的数据科学家的小贴士。没有“虚头八脑”的内容或“点击诱饵”,只有来自一名实践数据科学家的纯粹可操作的见解。
[## 数据分享 | Egor Howell | Substack