TowardsDataScience-2023-博客中文翻译-二十七-
TowardsDataScience 2023 博客中文翻译(二十七)
改进 Diffusers 包以生成高质量图像
原文:
towardsdatascience.com/improving-diffusers-package-for-high-quality-image-generation-a50fff04bdd4
克服 token 大小限制、自定义模型加载、LoRa 支持、文本反转支持等
·发布在 Towards Data Science ·15 分钟阅读·2023 年 4 月 5 日
--
再见 Babel,由 Andrew Zhu 使用纯 Python 中的 Diffusers 生成
AUTOMATIC1111 的 Stable Diffusion WebUI 已被证明是一个强大的工具,可以使用 Diffusion 模型生成高质量图像。然而,尽管 WebUI 使用简单,数据科学家、机器学习工程师和研究人员通常需要对图像生成过程有更多控制。这时,来自 huggingface 的 diffusers 包就派上了用场,它提供了一种在 Python 中运行 Diffusion 模型的方法,并允许用户自定义模型和提示,以生成符合他们具体需求的图像。
尽管具有潜力,Diffusers 包仍有几个限制,阻碍了它生成与 Stable Diffusion WebUI 相媲美的图像。这些限制中最显著的包括:
-
无法使用
.safetensor
文件格式中的自定义模型; -
77 个提示 token 的限制;
-
缺乏 LoRA 支持;
-
以及缺少图像放大功能(在 Stable Diffusion WebUI 中也称为 HighRes);
-
默认情况下性能低且 VRAM 使用高。
本文旨在解决这些限制,并使 Diffusers 包能够生成与 Stable Diffusion WebUI 产生的图像相媲美的高质量图像。通过提供的增强解决方案,数据科学家、机器学习工程师和研究人员可以在图像生成过程中享有更大的控制和灵活性,同时实现卓越的结果。在接下来的部分中,我们将探讨各种策略和技术,这些策略和技术可以用来克服这些限制,并释放 Diffusers 包的全部潜力。
请注意,如果这是你第一次运行 Stable Diffusion,请按照此链接安装所有所需的 CUDA 和 Python 包。
为你正在使用的深度学习库安装🤗 Diffusers。🤗 Diffusers 已在 Python 3.7+上测试过…
1. 加载本地的.safetensor 格式模型文件
用户可以轻松启动 diffusers 来生成这样的图像:
from diffusers import DiffusionPipeline
pipeline = DiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5")
pipeline.to("cuda")
image = pipeline("A cute cat playing piano").images[0]
image.save("image_of_cat_playing_piano.png")
你可能对输出的图像或性能不满意。让我们逐个解决这些问题。首先,让我们加载一个.safetensor
格式的自定义模型,该模型位于你计算机上的任何位置。你不能像这样直接加载模型文件:
pipeline = DiffusionPipeline.from_pretrained("/model/custom_model.safetensors")
这里是将.safetensor
文件转换为 diffusers 格式的详细步骤:
步骤 1。从 GitHub 拉取所有 diffusers 代码
git clone https://github.com/huggingface/diffusers.git
步骤 2。在script
文件夹下找到文件:convert_original_stable_diffusion_to_diffusers.py
在终端中运行此命令将.safetensor
文件转换为 Diffusers 格式。记得将— checkpoint_path
值更改为你的情况。
python convert_original_stable_diffusion_to_diffusers.py --from_safetensors --checkpoint_path="D:\stable-diffusion-webui\models\Stable-diffusion\deliberate_v2.safetensors" --dump_path='D:\sd_models\deliberate_v2' --device='cuda:0'
步骤 3。现在你可以使用新转换的模型文件加载管道,下面是完整代码:
from diffusers import DiffusionPipeline
pipeline = DiffusionPipeline.from_pretrained(
r"D:\sd_models\deliberate_v2"
)
pipeline.to("cuda")
image = pipeline("A cute cat playing piano").images[0]
image.save("image_of_cat_playing_piano.png")
你应该能够转换并使用你从 huggingface 或 civitai.com 下载的任何模型。
由上述代码生成的弹钢琴的猫
2. 提升 Diffusers 的性能
生成高质量图像可能是一个耗时的过程,即使对于最新的 3xxx 和 4xxx Nvidia RTX GPU 也是如此。默认情况下,Diffuers 包的设置是未优化的。可以应用两种解决方案来大幅提升性能。
在应用以下解决方案之前的交互速度如下:在 RTX 3070 TI 8G RAM 中生成一个 512x512 图像的速度大约为每秒 2.x 次迭代。
- 使用半精度权重
第一个解决方案是使用半精度权重。半精度权重使用 16 位浮点数代替传统的 32 位浮点数。这减少了存储权重所需的内存,并加快了计算速度,这可以显著提高 Diffusers 包的性能。
根据这个视频,将浮点精度从 FP32 减少到 FP16 也会启用 Tensor Cores。
我还有另一篇文章来测试 GPU Tensor Cores 能多快提升计算速度。
CPU 和 GPU 在 Python 和 PyTorch 中进行矩阵运算比较
[towardsdatascience.com
这里是如何在 diffusers 中启用 FP16,只需添加两行代码即可将性能提升 500%,几乎没有图像质量影响。
from diffusers import DiffusionPipeline
import torch # <----- Line 1 added
pipeline = DiffusionPipeline.from_pretrained(
r"D:\sd_models\deliberate_v2"
,torch_dtype = torch.float16 # <----- Line 2 Added
)
pipeline.to("cuda")
image = pipeline("A cute cat playing piano").images[0]
image.save("image_of_cat_playing_piano.png")
现在迭代速度提升至每秒 10.x 次,快了 5 倍。
- 使用 Xformers
Xformers是一个开源库,提供了一组高性能的变换器,适用于各种自然语言处理(NLP)任务。它基于 PyTorch 构建,旨在提供高效且可扩展的变换器模型,这些模型可以轻松集成到现有的 NLP 流程中。(如今,还有哪些模型不使用 Transformer?😛)
使用pip install xformers
安装 Xformers,然后我们只需一行代码即可轻松切换 diffusers 使用 xformers。
...
pipeline.to("cuda")
pipeline.enable_xformers_memory_efficient_attention() <--- one line added
...
这一行代码性能提升了另外 20%。
3. 移除 77 个提示符的限制
在当前版本的 Diffusers 中,生成图像时有 77 个提示符的限制。
幸运的是,针对这个问题有一个解决方案。通过使用社区提供的“lpw_stable_diffusion
”管道,你可以解锁 77 个提示符的限制,并生成高质量的长提示图像。
要使用“lpw_stable_diffusion
”管道,可以使用以下代码:
pipeline = DiffusionPipeline.from_pretrained(
model_path,
custom_pipeline="lpw_stable_diffusion", #<--- code added
torch_dtype=torch.float16
)
在这段代码中,我们使用“from_pretrained
”方法初始化一个新的 DiffusionPipeline 对象。我们指定了预训练模型的路径,并将“custom_pipeline
”参数设置为“lpw_stable_diffusion
”。这告诉 Diffusers 使用“lpw_stable_diffusion
”管道,从而解锁 77 个提示符的限制。
现在,让我们使用一个长提示字符串来测试一下。以下是完整的代码:
from diffusers import DiffusionPipeline
import torch
pipeline = DiffusionPipeline.from_pretrained(
r"D:\sd_models\deliberate_v2"
,custom_pipeline = "lpw_stable_diffusion" #<--- code added
,torch_dtype = torch.float16
)
pipeline.to("cuda")
pipeline.enable_xformers_memory_efficient_attention()
prompt = """
Babel tower falling down, walking on the starlight, dreamy ultra wide shot
, atmospheric, hyper realistic, epic composition, cinematic, octane render
, artstation landscape vista photography by Carr Clifton & Galen Rowell, 16K resolution
, Landscape veduta photo by Dustin Lefevre & tdraw, detailed landscape painting by Ivan Shishkin
, DeviantArt, Flickr, rendered in Enscape, Miyazaki, Nausicaa Ghibli, Breath of The Wild
, 4k detailed post processing, artstation, rendering by octane, unreal engine
"""
image = pipeline(prompt).images[0]
image.save("goodbye_babel_tower.png")
你将获得如下图像:
Goodby Babel,由 Andrew Zhu 使用 diffusers 生成
如果你仍然看到类似的警告信息:Token indices sequence length is longer than the specified maximum sequence length for this model ( *** > 77 ) . Running this sequence through the model will result in indexing errors.
这很正常,你可以忽略它。
4. 使用自定义 LoRA 与扩散器
尽管 LoRA 支持 在 Diffusers 中有所声称,但用户在加载本地 .safetensor
文件格式的 LoRA 文件时仍面临限制。这对用户使用社区 LoRA 可能是一个重大障碍。
为了克服这一限制,我创建了一个允许用户实时加载带有权重的 LoRA 文件的函数。这个函数可以用来将 LoRA 文件及其对应的权重加载到 Diffusers 模型中,从而生成高质量的 LoRA 数据图像。
这是函数主体:
from safetensors.torch import load_file
def __load_lora(
pipeline
,lora_path
,lora_weight=0.5
):
state_dict = load_file(lora_path)
LORA_PREFIX_UNET = 'lora_unet'
LORA_PREFIX_TEXT_ENCODER = 'lora_te'
alpha = lora_weight
visited = []
# directly update weight in diffusers model
for key in state_dict:
# as we have set the alpha beforehand, so just skip
if '.alpha' in key or key in visited:
continue
if 'text' in key:
layer_infos = key.split('.')[0].split(LORA_PREFIX_TEXT_ENCODER+'_')[-1].split('_')
curr_layer = pipeline.text_encoder
else:
layer_infos = key.split('.')[0].split(LORA_PREFIX_UNET+'_')[-1].split('_')
curr_layer = pipeline.unet
# find the target layer
temp_name = layer_infos.pop(0)
while len(layer_infos) > -1:
try:
curr_layer = curr_layer.__getattr__(temp_name)
if len(layer_infos) > 0:
temp_name = layer_infos.pop(0)
elif len(layer_infos) == 0:
break
except Exception:
if len(temp_name) > 0:
temp_name += '_'+layer_infos.pop(0)
else:
temp_name = layer_infos.pop(0)
# org_forward(x) + lora_up(lora_down(x)) * multiplier
pair_keys = []
if 'lora_down' in key:
pair_keys.append(key.replace('lora_down', 'lora_up'))
pair_keys.append(key)
else:
pair_keys.append(key)
pair_keys.append(key.replace('lora_up', 'lora_down'))
# update weight
if len(state_dict[pair_keys[0]].shape) == 4:
weight_up = state_dict[pair_keys[0]].squeeze(3).squeeze(2).to(torch.float32)
weight_down = state_dict[pair_keys[1]].squeeze(3).squeeze(2).to(torch.float32)
curr_layer.weight.data += alpha * torch.mm(weight_up, weight_down).unsqueeze(2).unsqueeze(3)
else:
weight_up = state_dict[pair_keys[0]].to(torch.float32)
weight_down = state_dict[pair_keys[1]].to(torch.float32)
curr_layer.weight.data += alpha * torch.mm(weight_up, weight_down)
# update visited list
for item in pair_keys:
visited.append(item)
return pipeline
逻辑提取自 diffusers git 仓库的 convert_lora_safetensor_to_diffusers.py。
以著名的 LoRA:MoXin 为例。你可以像这样使用 __load_lora
函数:
from diffusers import DiffusionPipeline
import torch
pipeline = DiffusionPipeline.from_pretrained(
r"D:\sd_models\deliberate_v2"
,custom_pipeline = "lpw_stable_diffusion"
,torch_dtype = torch.float16
)
lora = (r"D:\sd_models\Lora\Moxin_10.safetensors",0.8)
pipeline = __load_lora(pipeline=pipeline,lora_path=lora[0],lora_weight=lora[1])
pipeline.to("cuda")
pipeline.enable_xformers_memory_efficient_attention()
prompt = """
shukezouma,negative space,shuimobysim
a branch of flower, traditional chinese ink painting
"""
image = pipeline(prompt).images[0]
image.save("a branch of flower.png")
提示将生成类似这样的图像:
一枝花,由 Andrew Zhu 使用扩散器生成
你可以多次调用__load_lora()
来为一次生成加载多个 LoRA。
使用此功能,你现在可以实时加载带有权重的 LoRA 文件,并用它们生成高质量的图像。LoRA 加载速度非常快,通常只需 1-2 秒,比转换和使用(这会生成一个 GB 大小的模型文件)要好得多。
5. 使用自定义纹理反演与扩散器
使用 Diffusers 包中的自定义纹理反演是一种生成高质量图像的强大方法。然而,Diffusers 的官方文档 提到,用户需要训练自己的文本反演,这可能需要在 V100 GPU 上花费一个小时。这对于许多希望快速生成图像的用户来说可能不切实际。
因此,我进行了调查并找到了一个解决方案,使扩散器能够像在 Stable Diffusion WebUI 中一样使用文本反演。以下是我创建的用于加载自定义文本反演的函数。
def load_textual_inversion(
learned_embeds_path
, text_encoder
, tokenizer
, token = None
, weight = 0.5
):
'''
Use this function to load textual inversion model in model initilization stage
or image generation stage.
'''
loaded_learned_embeds = torch.load(learned_embeds_path, map_location="cpu")
string_to_token = loaded_learned_embeds['string_to_token']
string_to_param = loaded_learned_embeds['string_to_param']
# separate token and the embeds
trained_token = list(string_to_token.keys())[0]
embeds = string_to_param[trained_token]
embeds = embeds[0] * weight
# cast to dtype of text_encoder
dtype = text_encoder.get_input_embeddings().weight.dtype
embeds.to(dtype)
# add the token in tokenizer
token = token if token is not None else trained_token
num_added_tokens = tokenizer.add_tokens(token)
if num_added_tokens == 0:
#print(f"The tokenizer already contains the token {token}.The new token will replace the previous one")
raise ValueError(f"The tokenizer already contains the token {token}. Please pass a different `token` that is not already in the tokenizer.")
# resize the token embeddings
text_encoder.resize_token_embeddings(len(tokenizer))
# get the id for the token and assign the embeds
token_id = tokenizer.convert_tokens_to_ids(token)
text_encoder.get_input_embeddings().weight.data[token_id] = embeds
return (tokenizer,text_encoder)
在 load_textual_inversion()
函数中,你需要提供以下参数:
-
learned_embeds_path
:预训练文本反演模型文件的路径,格式为 .pt 或 .bin。 -
text_encoder
:从扩散管道获得的文本编码器对象。 -
tokenizer
:从扩散管道获得的分词器对象。 -
token
:可选参数,指定提示词令牌。默认设置为 None。它是会在提示中触发文本反演的关键词。 -
weight
: 可选参数,指定文本反演的权重。默认情况下,我将其设置为 0.5,你可以根据需要更改为其他值。
你现在可以使用类似这样的 diffusers 管道功能:
from diffusers import DiffusionPipeline
import torch
pipeline = DiffusionPipeline.from_pretrained(
r"D:\sd_models\deliberate_v2"
,custom_pipeline = "lpw_stable_diffusion"
,torch_dtype = torch.float16
,safety_checker = None
)
textual_inversion_path = r"D:\sd_models\embeddings\style-empire.pt"
tokenizer = pipeline.tokenizer
text_encoder = pipeline.text_encoder
load_textual_inversion(
learned_embeds_path = textual_inversion_path
, tokenizer = tokenizer
, text_encoder = text_encoder
, token = 'styleempire'
)
pipeline.to("cuda")
pipeline.enable_xformers_memory_efficient_attention()
prompt = """
styleempire,award winning beautiful street, storm,((dark storm clouds))
, fluffy clouds in the sky, shaded flat illustration, digital art
, trending on artstation, highly detailed, fine detail, intricate
, ((lens flare)), (backlighting), (bloom)
"""
neg_prompt = """
cartoon, 3d, ((disfigured)), ((bad art)), ((deformed)), ((poorly drawn))
, ((extra limbs)), ((close up)), ((b&w)), weird colors, blurry
, hat, cap, glasses, sunglasses, lightning, face
"""
generator = torch.Generator("cuda").manual_seed(1)
image = pipeline(
prompt
,negative_prompt =neg_prompt
,generator = generator
).images[0]
image.save("tv_test.png")
这是应用帝国风格文本反演的结果。
左边的现代街道变成了旧伦敦风格。
6. 放大图像
Diffusers 包非常适合生成高质量图像,但图像放大不是其主要功能。然而,Stable-Diffusion-WebUI 提供了一个名为 HighRes 的功能,允许用户将生成的图像放大到 2 倍或 4 倍。如果 Diffusers 用户也能享受相同的功能,那就太好了。经过一些研究和测试,我发现 SwinRI 模型是图像放大的优秀选择,可以轻松将图像放大到 2 倍或 4 倍。
要使用 SwinRI 模型进行图像放大,我们可以使用JingyunLiang/SwinIR的 GitHub 存储库中的代码。如果你只需要代码,下载models/network_swinir.py
,utils/util_calculate_psnr_ssim.py
和main_test_swinir.py
即可。按照 readme 指南,你可以像魔法一样放大图像。
这是 SwinRI 如何出色放大图像的一个示例。
左侧:原始图像,右侧:4 倍 SwinRI 放大图像
许多其他开源解决方案可以用来改善图像质量。这里列出了我尝试过的另外三种返回优秀结果的模型。
- RealSR:
github.com/jixiaozhong/RealSR
RealSR 可以将图像放大 4 倍,几乎与 SwinRI 一样好,并且执行性能最快,不需要调用 PyTorch 和 CUDA。作者将代码和 CUDA 使用直接编译为二进制文件。我的观察发现,RealSR 可以在大约 2–4 秒内放大图像。
- CodeFormer:
github.com/sczhou/CodeFormer
CodeFormer 擅长修复模糊或破损的面孔,它还可以去除噪声和增强背景细节。这种解决方案和算法在其他应用中也得到了广泛使用,包括 Stable-Diffusion-WebUI。
- GFPGAN:
github.com/TencentARC/GFPGAN
另一个强大的开源解决方案,能够实现惊人的面部修复效果,而且速度也很快。GFPGAN 还集成到了 Stable-Diffusion-WebUI 中。
[更新于 2023 年 4 月 19 日]
发现 SD 1.5 及所有扩展模型在仅使用 text2img 管道生成高分辨率图像时效果不好。在实践中,我发现 Diffusers text2img 管道即使在 1920x1080 下也会容易生成扭曲和破损的图像,相同的设置和提示可以在 800x600 下生成良好的图像。
我发现 Diffusers 的 img2img 流程可以作为一个很好的图像高分辨率修复解决方案。以下是将 img2img 流程作为图像高分辨率修复解决方案的总体步骤:
-
使用 text2img 流程生成低分辨率图像
-
将图像放大到你想要的分辨率(最大尺寸取决于你的 VRAM 大小)。
img = img.resize((width,height))
。测试显示我的 8G VRAM RTX 3070 Ti 能处理将 800x600 放大 3 倍到 2400x1800。在这一步骤中,请注意没有图像放大或修复,只是将图像放大到你想要的尺寸。 -
然后将新的手动放大
img
以相同的提示、负面提示和额外设置strength
传递到 img2img 流程中,你会看到输入像魔法一样被放大。
img2img 会稍微改变图像内容,以面部为例,它不仅会放大图像,还会稍微改变面部。
使用 Diffuses img2img 流程进行面部高分辨率放大,图像由作者生成
7. 优化 Diffusers CUDA 内存使用
使用 Diffusers 生成图像时,重要的是要考虑 CUDA 内存使用,特别是当你想加载其他模型来进一步处理生成的图像时。如果你尝试加载另一个模型如 SwinIR 以放大图像,你可能会遇到 RuntimeError: CUDA out of memory
,因为 Diffuser 模型仍然占用 CUDA 内存。
为了缓解这个问题,有几种解决方案可以优化 CUDA 内存使用。我发现以下两种解决方案效果最好:
- 切片注意力用于额外的内存节省
切片注意力是一种减少变换器中自注意力机制内存使用的技术。通过将注意力矩阵分割成较小的块,减少了内存需求。这种技术可以与 Diffusers 包一起使用,以减少 Diffuser 模型的内存占用。
在 Diffusers 中使用它,只需一行代码:
pipeline.enable_attention_slicing()
- 模型转移到 CPU
通常,你不会同时运行两个模型,目的是将模型数据暂时转移到 CPU 内存中,释放 CUDA 内存空间给其他模型,只有在开始使用模型时才加载到 VRAM 中。
在 Diffusers 中动态将数据转移到 CPU 内存中,请使用以下代码:
pipeline.enable_model_cpu_offload()
应用此方法后,每当 Diffusers 完成图像生成任务时,模型数据将自动转移到 CPU 内存中,直到下一次调用。
要获取更多关于 PyTorch 2.0 的 Diffusers 性能和 VRAM 优化的信息,请查看我写的这篇文章,作为对本文的补充。
[## 使用 PyTorch 2.0 进行扩散模型性能测试笔记
测试各种方法以提升 Stable Diffusion 包 Diffusers 的性能并降低 VRAM 使用
总结
文章讨论了如何提升 Diffusers 包的性能和功能,涵盖了 Diffusers 用户面临的几个常见问题的解决方案,包括加载本地.safetensor
模型、提升性能、移除 77 个提示令牌限制、使用自定义 LoRA 和 Textual Inversion、图像放大和优化 CUDA 内存使用。
通过应用这些解决方案,Diffusers 用户可以生成高质量的图像,具有更好的性能和更多的过程控制。文章还包括每个解决方案的代码片段和详细解释。
如果你能成功应用这些解决方案和代码,可能会有额外的好处,我受益匪浅,你可以通过阅读 Diffusers 源代码实现你自己的解决方案,更好地理解 Stable Diffusion 的工作原理。对我来说,学习、发现和实施这些解决方案是一段有趣的旅程。希望这些解决方案也能帮助你,并希望你在使用 Stable Diffusion 和 diffusers 包时感到愉快。
这里提供生成标题图像的提示:
Babel tower falling down, walking on the starlight, dreamy ultra wide shot
, atmospheric, hyper realistic, epic composition, cinematic, octane render
, artstation landscape vista photography by Carr Clifton & Galen Rowell, 16K resolution
, Landscape veduta photo by Dustin Lefevre & tdraw, detailed landscape painting by Ivan Shishkin
, DeviantArt, Flickr, rendered in Enscape, Miyazaki, Nausicaa Ghibli, Breath of The Wild
, 4k detailed post processing, artstation, rendering by octane, unreal engine
尺寸:600 * 800
种子:3977059881
调度器(或采样方法):DPMSolverMultistepScheduler
采样步骤:25
CFG 规模(或指导尺度):7.5 SwinRI 模型:003_realSR_BSRGAN_DFO_s64w8_SwinIR-M_x4_GAN.pth
许可证和代码重用
文章中提供的解决方案是通过广泛的源代码阅读、深夜测试和逻辑设计实现的。需要注意的是,在撰写本文时(2023 年 4 月),加载 LoRA 和 Textual Inversion 解决方案和代码是互联网上唯一有效的版本。
如果你发现本文中展示的代码有用,并希望在你的项目、论文或文章中重用,请参考这篇 Medium 文章。这里展示的代码遵循 MIT 许可证,这允许你在遵守许可证条款的条件下,使用、复制、修改、合并、发布、分发、再许可和/或出售软件的副本。
请注意,本文中提出的解决方案可能不是实现期望结果的最佳或最有效的方法,随着新发展和改进,可能会有所变化。始终建议在将任何代码应用于生产环境之前,彻底测试和验证代码。
书籍:使用 Python 的 Stable Diffusion
文章提供了对使用 Python 控制 Stable Diffusion 潜力的初步了解。深入探讨这个跨学科领域需要一个全面的指南,不仅解释理论,还通过 Python 编程展示实际应用。
因此,我很高兴地宣布我的书籍“用 Python 使用稳定扩散”的发布。
用 Python 使用稳定扩散
本书是最新研究、实验和致力于使复杂的 AI 概念对所有人(使用 Python)可及的终极成果。不论你是对 Python 强大功能感兴趣的初学者程序员、寻求扩展工具包的经验工程师,还是渴望深入数学建模的科学家,本书都旨在满足你的需求。
在书中,你将找到稳定扩散的数学基础的详细解释,以及如何使用这些模型与 Python 的清晰逐步教程。从设置 Python 环境到运行扩散模型、可视化结果,每一个方面都以细致的注意力进行覆盖。
希望你喜欢它。
参考资料
通过智能提示改进希伯来语问答模型
·
关注 发表在 Towards Data Science ·13 分钟阅读·2023 年 3 月 17 日
--
DALL-E 2: “一个穿着长袍的机器人用希伯来语书写圣经句子。数字艺术。”
大家好,
我分享了一个我参与的短期项目,涉及通过智能提示提高 text-davinci-003
模型的性能。我首先要说的是,这项工作受到 James Briggs 的优秀视频教程的启发——特别是这一个 — youtu.be/dRUIGgNBvVk
,我使用的许多代码也取自他的示例。
本文的提纲如下:
-
简介 — 大型语言模型(LLMs)中的独特和专有数据
-
问题陈述 — 用希伯来语回答问题
-
解决方案建议 — 提示与查询
-
实验 — 希伯来语问答
-
结论
完整代码的 Google Colab 笔记本在这里 — colab.research.google.com/drive/1_UqPHGPW1yLf3O_BOySRjf3bWqMe6A4H#scrollTo=4DY7XgilIr-H
第一部分 问题陈述 — 大型语言模型中的独特和专有数据
以色列的主要官方语言是希伯来语,作为以色列人,我有兴趣提高大型语言模型在希伯来语中的表现。虽然近期有所改善,但众所周知,ChatGPT 在希伯来语中的表现相较于其在英语中的表现仍有待提高。本文中,希伯来语只是代表那些在大型语言模型中被低估的数据来源,本文将探讨如何尝试帮助 LLM 提供基于这些数据的有用结果。
在非常大型语言模型出现之前,常见的一个选择是对模型进行额外的训练,加入新的数据/任务。当模型的参数规模达到百万级时,这种方法对许多人和用例来说是可行的,并且可以使用常见的消费级硬件来实现。
在过去几年中,大型语言模型的规模爆炸性增长,如今已经达到数百亿个参数。另一方面,消费级硬件并没有出现如此大规模的效率提升。因此,针对这些模型的微调选项已经被大多数用户和组织排除在外。
ChatGPT 已经广泛为人所知,在许多任务上表现出色,证明了它是一个不可或缺的 AI 助手。然而,如果我希望利用这项技术来帮助说稀有语言的人,或帮助处理公司内部的专有数据(这些数据在线上不可用),那么 ChatGPT 很可能因为之前未见过这些数据而失败。那么我们如何在这些模型中利用特殊/专有数据,而不需要花费数百万美元来使用特殊硬件进行训练呢?一个可行的选择是通过查询提示,这一点我们今天将进行探索。
第二部分 问题陈述 — 希伯来语中的问答
我们将使用 OpenAI API 来回答三个希伯来语问题,这些问题的灵感来自于SQuAD数据集,并翻译成希伯来语。以下代码片段展示了这些问题、预期的答案和英文翻译。
questions_answers = [
{'subject': 'אוסטרליה',
'subject_translation': 'Australia',
'question': "איזו עיר בויקטוריה נחשבת לבירת הספורט של אוסטרליה?",
'answer': "מלבורן",
'question_translation': "Which city in Victoria is considered the sporting capital of Australia?",
'answer_translation': "Melbourne"},
{'subject': 'אוניברסיטה',
'subject_translation': 'University',
'question': "איזה נהר ממוקם בקרבת אוניברסיטת הארווארד?",
'answer': "נהר צ'ארלס",
'question_translation': "Which river is located in the vicinity of Harvard University?",
'answer_translation': "Charles River"},
{'subject': 'המוות השחור',
'subject_translation': 'The black plague',
'question': "כמה אנשים ברחבי העולם נספו בעקבות המוות השחור?",
'answer': "בין 75 ל200 מיליון איש",
'question_translation': "How many people in the world died from the black plague?",
'answer_translation': "Between 75 and 200 million"},
]
免责声明 — 最新版本的 ChatGPT 对这些查询(希伯来语)的回答表现非常好。本文的重点不是“竞争”,而是展示一个可能在其他模型/数据中出现的问题的解决方案。
我们将使用 text-davinci-003
模型来回答这些问题,使用的代码将调用 OpenAI API:
from googletrans import Translator
import openai
def complete(prompt):
# query text-davinci-003
res = openai.Completion.create(
engine='text-davinci-003',
prompt=prompt,
temperature=0,
max_tokens=400,
top_p=1,
frequency_penalty=0,
presence_penalty=0,
stop=None
)
return res['choices'][0]['text'].strip()
def print_question_answer(question, query, expected_answer, question_translation, expected_answer_translation):
translator = Translator()
answer = complete(query)
print(f'Question: {question}\n' \
f'Question (translation): {question_translation}\n' \
f'Answer: {answer}\n' \
f'Answer (translation): {translator.translate(answer).text}\n' \
f'Expected Answer: {expected_answer}\n' \
f'Expected Answer (translation): {expected_answer_translation}'
)
for i, qa_dct in enumerate(questions_answers):
print(f'####################')
print(f'Question #{i + 1}:')
print_question_answer(qa_dct['question'],
qa_dct['question'],
qa_dct['answer'],
qa_dct['question_translation'],
qa_dct['answer_translation'],
)
输出:
####################
Question #1:
Question: איזו עיר בויקטוריה נחשבת לבירת הספורט של אוסטרליה?
Question (translation): Which city in Victoria is considered the sporting capital of Australia?
Answer: סידני.
Answer (translation): Sydney.
Expected Answer: מלבורן
Expected Answer (translation): Melbourne
####################
Question #2:
Question: איזה נהר ממוקם בקרבת אוניברסיטת הארווארד?
Question (translation): Which river is located in the vicinity of Harvard University?
Answer: הנהר הקרוב ביותר לאוניברסיטת הארווארד הוא נהר הקונגו.
Answer (translation): The closest river to Harvard University is the Congo River.
Expected Answer: נהר צ'ארלס
Expected Answer (translation): Charles River
####################
Question #3:
Question: כמה אנשים ברחבי העולם נספו בעקבות המוות השחור?
Question (translation): How many people in the world died from the black plague?
Answer: בעקבות המוות השחור של ג'ורג' פלאי, הפגנו אלפי אנשים ברחבי העולם בכדי להגיב לפעולות הפולישיות של המשטרה האמריקאית. אלפי אנשים הפגינו בערים בכל הארצות האמריקאיות, כמו ג'קסוןביל, ניו יורק, לוס אנג'לס, פילדלפיה, אתרוג, אוסטין, סנט לואיס, אוסטין, אינדיאנה, מישיגן, אורלנדו, קליפורניה, אוהיו, אריזונה, אוקלהומה, ארצות הברית החדשות ועוד. אף אנשים
Answer (translation): Following the black death of George Flay, thousands of people demonstrated around the world to respond to the police actions of the American police. Thousands of people demonstrated in cities in all American countries, such as Jacksonville, New York, Los Angeles, Philadelphia, Etrog, Austin, St. Louis, Austin, Indiana, Michigan, Orlando, California, Ohio, Arizona, Oklahoma, New United States and more . no people
Expected Answer: בין 75 ל200 מיליון איש
Expected Answer (translation): Between 75 and 200 million
我们看到 text-davinci-003
在默认情况下回答这些问题的表现非常差。它把所有三个问题都答错了,并且对第二和第三个问题产生了一些荒谬的答案。
所以问题来了——我们如何解决这个问题?我们如何从 text-davinci-003
获得良好的答案,而不必在一堆希伯来数据上对其进行微调?
第三部分. 解决方案建议——提示与查询
正如我在介绍中写的那样,向 LLM 添加信息的一种方法是提示,即在模型的输入中包含相关信息。但为每个查询包含相关信息可能是一个非常繁琐且耗时的任务,我们能否对此进行自动化?
当然,我们可以!通过查询来进行提示。我们将使用希伯来语的维基百科的一个较小的快照,这个快照中肯定包含了我们问题的答案。我们将把这个快照分割成多行,使用 OpenAI 的嵌入模型对每一行进行嵌入,并将这些嵌入插入到一个名为 Pinecone DB 的向量数据库中。然后,对于我们提出的每一个问题,我们将搜索向量数据库中与问题语义相似的信息片段,并将这些片段附加到模型输入中作为附加信息。这个过程如图 1 所示。希望模型能够利用提供的上下文,并给出我们问题的正确答案。
图 1. 通过查询进行提示
受到 Jason Briggs 的启发,我将展示一个提示模板,这将帮助我们生成可理解的提示给 text-davinci-003
,并希望使其正确回答我们之前的三个问题。
第四部分. 实验——希伯来语问答
首先——我们需要用相关信息填充我们的向量数据库。为此,我们将使用一个从这里下载的小型希伯来语维基百科快照——u.cs.biu.ac.il/~yogo/hebwiki/
(在 Creative Commons Attribution-ShareAlike 4.0 International License 下提供)。
with open('/content/drive/MyDrive/Datasets/hebrew_wikipedia/full.txt') as f:
hebrew_wiki = f.readlines()
openai_price_per_1k = 0.00004
n_tokens = sum([len(x) for x in hebrew_wiki])
print(f'This hebrew wikipedia dataset contains {len(hebrew_wiki)} sentences')
print(f'In total it amounts to {n_tokens} tokens')
print(f'With the current pricing of openAI it will cost {(openai_price_per_1k * n_tokens) / 1000:.3f}$ to embed everything')
This hebrew wikipedia dataset contains 3833140 sentences
In total it amounts to 380692471 tokens
With the current pricing of openAI it will cost 15.228$ to embed everything
对于一般信息,我计算了一下使用 OpenAI API 嵌入整个维基百科快照的费用——还不错!但为了我们的目的,我们只需要特定的主题,所以我们可以通过选择更具体的句子来节省一些费用:
def get_lines_per_subject(hebrew_wiki, subject, subject_translation):
subject_lines = [(i, x) for i, x in enumerate(hebrew_wiki) if subject in x]
n_tokens = sum([len(x[1]) for x in subject_lines])
print(f'There are {len(subject_lines)} sentences that have the words {subject_translation} in them')
print(f'In total it amounts to {n_tokens} tokens')
print(f'With the current pricing of openAI it will cost {(openai_price_per_1k * n_tokens) / 1000:.3f}$ to embed everything')
return subject_lines
subject_lines = {}
for qa_dct in questions_answers:
subject_lines[qa_dct['subject']] = get_lines_per_subject(hebrew_wiki, qa_dct['subject'], qa_dct['subject_translation'])
There are 7772 sentences that have the words Australia in them
In total it amounts to 1026137 tokens
With the current pricing of openAI it will cost 0.041$ to embed everything
There are 15908 sentences that have the words University in them
In total it amounts to 1904404 tokens
With the current pricing of openAI it will cost 0.076$ to embed everything
There are 160 sentences that have the words The black plague in them
In total it amounts to 19410 tokens
With the current pricing of openAI it will cost 0.001$ to embed everything
好一些了。我们现在准备填充向量数据库:
import pinecone
index_name = 'hebrew-wikipedia'
# initialize connection to pinecone (get API key at app.pinecone.io)
pinecone.init(
api_key=pinecone_key,
environment="us-east1-gcp"
)
# check if index already exists (it shouldn't if this is first time)
if index_name not in pinecone.list_indexes():
# if does not exist, create index
pinecone.create_index(
index_name,
dimension=len(res['data'][0]['embedding']),
metric='cosine',
metadata_config={'indexed': ['contains_keywords']}
)
# connect to index
index = pinecone.Index(index_name)
# view index stats
index.describe_index_stats()
我们生成了一个新的 Pinecone DB 索引(如果它尚不存在的话)。让我们用我们的数据填充它:
from tqdm.auto import tqdm
import datetime
from time import sleep
embed_model = "text-embedding-ada-002"
batch_size = 32 # how many embeddings we create and insert at once
def insert_into_pinecone(lines, subject):
print(f'Inserting {len(lines)} of subject: {subject} into pinecone')
for i in tqdm(range(0, len(lines), batch_size)):
# find end of batch
i_end = min(len(lines), i+batch_size)
meta_batch = lines[i:i_end]
ids_batch = [str(x[0]) for x in meta_batch]
texts = [x[1] for x in meta_batch]
try:
res = openai.Embedding.create(input=texts, engine=embed_model)
except:
done = False
while not done:
sleep(5)
try:
res = openai.Embedding.create(input=texts, engine=embed_model)
done = True
except:
pass
embeds = [record['embedding'] for record in res['data']]
# cleanup metadata
meta_batch = [{
'contains_keywords': subject,
'text': x,
} for x in texts]
to_upsert = list(zip(ids_batch, embeds, meta_batch))
# upsert to Pinecone
index.upsert(vectors=to_upsert)
for subject, lines in subject_lines.items():
insert_into_pinecone(lines, subject)
很好!我们已经填充了向量数据库。让我们为第一个问题(维多利亚州哪个城市被认为是澳大利亚的体育之都?)获取前两个最相关的上下文:
res = openai.Embedding.create(
input=[questions_answers[0]['question']],
engine=embed_model
)
# retrieve from Pinecone
xq = res['data'][0]['embedding']
# get relevant contexts (including the questions)
res = index.query(xq, top_k=2, include_metadata=True)
res
{'matches': [{'id': '2642286',
'metadata': {'contains_keywords': 'אוסטרליה',
'text': 'העיר שוכנת לחופה הצפון-מערבי של '
'אוסטרליה.\n'},
'score': 0.899194837,
'values': []},
{'id': '2282112',
'metadata': {'contains_keywords': 'אוסטרליה',
'text': 'אוסטרלאסיה היה השם של נבחרת הספורטאים '
'מאוסטרליה וניו זילנד שהשתתפה באולימפיאדת '
'לונדון (1908) ובאולימפיאדת סטוקהולם '
'(1912).\n'},
'score': 0.893916488,
'values': []}],
'namespace': ''}
我们看到,在我们的向量数据库中(以与问题嵌入的余弦相似度为准),前两行并不太有用。第一行是关于澳大利亚的非常一般的句子,第二行确实谈论了澳大利亚的体育,但没有提到我们期望的答案——墨尔本。
但仍然,让我们挑战我们的模型,看看在它现在可以访问额外的希伯来语信息的情况下表现如何。请注意,我们指示模型在无法从给定上下文中推导出答案时回答“I don’t know”。
limit = 3000
def retrieve(query, filter_subjects=None, add_dont_know=False):
res = openai.Embedding.create(
input=[query],
engine=embed_model
)
# retrieve from Pinecone
xq = res['data'][0]['embedding']
# get relevant contexts
res = index.query(xq,
{'contains_keywords': {'$in': filter_subjects}}
if filter_subjects is not None else None,
top_k=100,
include_metadata=True)
contexts = [
x['metadata']['text'] for x in res['matches']
]
# build our prompt with the retrieved contexts included
idk_str = '''. If the question cannot be answered using the
information provided answer with "I don't know"''' if add_dont_know else ''
prompt_start = (
"Answer the question based on the context below." + idk_str + "\n\n" +
"Context:\n"
)
prompt_end = (
f"\n\nQuestion: {query}\nAnswer:"
)
# append contexts until hitting limit
for i in range(1, len(contexts)):
if len("\n\n---\n\n".join(contexts[:i])) >= limit:
prompt = (
prompt_start +
"\n\n---\n\n".join(contexts[:i-1]) +
prompt_end
)
break
elif i == len(contexts)-1:
prompt = (
prompt_start +
"\n\n---\n\n".join(contexts) +
prompt_end
)
return prompt
original_subjects = ['אוסטרליה', 'אוניברסיטה', 'המוות השחור']
for i, qa_dct in enumerate(questions_answers):
print(f'####################')
print(f'Question #{i + 1} with context:')
query_with_contexts = retrieve(qa_dct['question'],
filter_subjects=original_subjects,
add_dont_know=True)
print(f'Length of query with contexts: {len(query_with_contexts)}')
print_question_answer(qa_dct['question'],
query_with_contexts,
qa_dct['answer'],
qa_dct['question_translation'],
qa_dct['answer_translation'],
)
####################
Question #1 with context:
Length of query with contexts: 3207
Question: איזו עיר בויקטוריה נחשבת לבירת הספורט של אוסטרליה?
Question (translation): Which city in Victoria is considered the sporting capital of Australia?
Answer: קנברה
Answer (translation): Canberra
Expected Answer: מלבורן
Expected Answer (translation): Melbourne
####################
Question #2 with context:
Length of query with contexts: 3179
Question: איזה נהר ממוקם בקרבת אוניברסיטת הארווארד?
Question (translation): Which river is located in the vicinity of Harvard University?
Answer: I don't know
Answer (translation): I don't know
Expected Answer: נהר צ'ארלס
Expected Answer (translation): Charles River
####################
Question #3 with context:
Length of query with contexts: 3138
Question: כמה אנשים ברחבי העולם נספו בעקבות המוות השחור?
Question (translation): How many people in the world died from the black plague?
Answer: I don't know.
Answer (translation): I don't know.
Expected Answer: בין 75 ל200 מיליון איש
Expected Answer (translation): Between 75 and 200 million
好的,稍微有所改善。第一个答案仍然错误。在第二个和第三个答案中,我们得到了“I don’t know”,我认为这比错误答案要好得多。但我们仍然希望提供当前的答案,我们如何改进呢?
让我们向向量数据库中添加一些额外的主题,也许它没有足够的相关信息来提取相关上下文。
additional_subjects = [
('מלבורן', 'Melbourne'),
('סידני', 'Sydney'),
('אדלייד', 'Adelaide'),
('הרווארד', 'Harvard'),
("מסצ'וסטס", 'Massachusetts'),
("צ'ארלס", 'Charles'),
('בוסטון', 'Boston'),
('מגפה', 'plague'),
('האבעבועות השחורות', 'smallpox'),
('אבעבועות שחורות', 'smallpox'),
('מונגוליה', 'mongolia')
]
additional_subject_lines = {}
for subject, subject_translation in additional_subjects:
additional_subject_lines[subject] = get_lines_per_subject(hebrew_wiki, subject, subject_translation)
for subject, lines in additional_subject_lines.items():
insert_into_pinecone(lines, subject)
There are 948 sentences that have the words Melbourne in them
In total it amounts to 119669 tokens
With the current pricing of openAI it will cost 0.005$ to embed everything
There are 2812 sentences that have the words Sydney in them
In total it amounts to 347608 tokens
With the current pricing of openAI it will cost 0.014$ to embed everything
There are 213 sentences that have the words Adelaide in them
In total it amounts to 24480 tokens
With the current pricing of openAI it will cost 0.001$ to embed everything
There are 1809 sentences that have the words Harvard in them
In total it amounts to 228918 tokens
With the current pricing of openAI it will cost 0.009$ to embed everything
There are 1476 sentences that have the words Massachusetts in them
In total it amounts to 179227 tokens
With the current pricing of openAI it will cost 0.007$ to embed everything
There are 5025 sentences that have the words Charles in them
In total it amounts to 647501 tokens
With the current pricing of openAI it will cost 0.026$ to embed everything
There are 2912 sentences that have the words Boston in them
In total it amounts to 361337 tokens
With the current pricing of openAI it will cost 0.014$ to embed everything
There are 1007 sentences that have the words plague in them
In total it amounts to 116961 tokens
With the current pricing of openAI it will cost 0.005$ to embed everything
There are 39 sentences that have the words smallpox in them
In total it amounts to 5417 tokens
With the current pricing of openAI it will cost 0.000$ to embed everything
There are 135 sentences that have the words smallpox in them
In total it amounts to 15815 tokens
With the current pricing of openAI it will cost 0.001$ to embed everything
There are 742 sentences that have the words mongolia in them
In total it amounts to 90607 tokens
With the current pricing of openAI it will cost 0.004$ to embed everything
很好,我们添加了更丰富的上下文(你甚至可以说我们有点作弊 — 请注意,我们添加了所有包含“墨尔本”和“查尔斯”等词汇的句子,这些句子是我们问题的答案。但这只是一个演示!)
for i, qa_dct in enumerate(questions_answers):
print(f'####################')
print(f'Question #{i + 1} with context:')
query_with_contexts = retrieve(qa_dct['question'], add_dont_know=Tue)
print(f'Length of query with contexts: {len(query_with_contexts)}')
print_question_answer(qa_dct['question'],
query_with_contexts,
qa_dct['answer'],
qa_dct['question_translation'],
qa_dct['answer_translation'],
)
####################
Question #1 with context:
Length of query with contexts: 3158
Question: איזו עיר בויקטוריה נחשבת לבירת הספורט של אוסטרליה?
Question (translation): Which city in Victoria is considered the sporting capital of Australia?
Answer: מלבורן
Answer (translation): Melbourne
Expected Answer: מלבורן
Expected Answer (translation): Melbourne
####################
Question #2 with context:
Length of query with contexts: 3106
Question: איזה נהר ממוקם בקרבת אוניברסיטת הארווארד?
Question (translation): Which river is located in the vicinity of Harvard University?
Answer: נהר צ'ארלס
Answer (translation): Charles River
Expected Answer: נהר צ'ארלס
Expected Answer (translation): Charles River
####################
Question #3 with context:
Length of query with contexts: 3203
Question: כמה אנשים ברחבי העולם נספו בעקבות המוות השחור?
Question (translation): How many people in the world died from the black plague?
Answer: לפי הערכות שונות, כ-35 מיליון בני אדם בסין לבדה, ובין 20 ל-25 מיליון בני אדם באירופה.
Answer (translation): According to various estimates, about 35 million people in China alone, and between 20 and 25 million people in Europe.
Expected Answer: בין 75 ל200 מיליון איש
Expected Answer (translation): Between 75 and 200 million
不错!我们得到了答案!虽然第三个答案仍然不是 100% 正确,但比“I don’t know”好得多,也比最初的幻觉要好。
第五部分:结论
在本文中,我们已经看到如何使用向量数据库自动提供相关上下文给 LLM,以改善模型输出。在这个具体的案例中,这可能不是必需的,因为 ChatGPT(作为 API 提供)在希伯来语上的表现已经大幅提升,能够很好地回答这些问题。
然而,本文的重点是介绍这一概念,这一概念可以推广到其他目的,例如:
-
使用 LLM 分析未在网上公开的专有数据
-
通过将用于生成输出的上下文附加到用户输出中,为模型添加一种可解释性(以证明答案的可靠性)。
希望你喜欢这篇文章,并激发了你自己尝试的动力。下次见!
Elad
参考文献
-
希伯来语维基百科数据 —
u.cs.biu.ac.il/~yogo/hebwiki/
-
SQuAD 数据集 —
rajpurkar.github.io/SQuAD-explorer/
-
James Briggs 的视频教程 —
youtu.be/dRUIGgNBvVk
-
OpenAI API —
openai.com/blog/openai-api
-
Pinecone 数据库 —
www.pinecone.io/
通过解缠改进 k-Means 聚类
学习数据集类别邻域结构可以改善聚类效果
·
关注 发表在 Towards Data Science · 8 分钟阅读 · 2023 年 11 月 26 日
--
这篇文章是关于 A.F. Agarap 和 A.P. Azcarraga 于 2020 年国际联合神经网络大会(IJCNN)上发表的论文“通过解缠内部表示改进 k-Means 聚类”的配套文章
背景
聚类是一种无监督学习任务,它将一组对象分组,使得同一组中的对象相互之间的相似性高于其他组中的对象。这是一个被广泛研究的任务,其应用包括但不限于数据分析和可视化、异常检测、序列分析以及自然语言处理。
与其他机器学习方法类似,聚类算法严重依赖特征表示的选择。在我们的工作中,我们通过解缠结提高了特征表示的质量。
我们将解缠结定义为类别不同的数据点相对类别相似的数据点之间的距离。这类似于Frosst et al. (2019)中对上述术语的处理。因此,在表示学习过程中最大化解缠结意味着类别相似的数据点之间的距离被最小化。
图由作者提供。
这样做可以保留数据集中示例的类别成员,即数据点在特征空间中如何根据其类别或标签分布。如果类别成员被保留,我们将拥有一个特征表示空间,其中最近邻分类器或聚类算法的表现将会很好。
聚类
聚类是一种机器学习任务,旨在发现数据点的分组,其中组内的点在彼此之间的相似性比不同组的点之间的相似性更高。
图由作者提供。
像其他机器学习算法一样,聚类算法的成功依赖于特征表示的选择。对于使用的数据集,一种表示可能优于另一种。然而,在深度学习中情况并非如此,因为特征表示作为神经网络的隐式任务被学习。
深度聚类
因此,近年来的工作如 Deep Embedding Clustering 或 DEC 和 Variational Deep Embedding 或 VADE(2016 年),以及 ClusterGAN(2018 年),都利用了神经网络的特征表示学习能力。
图源自 DEC(Xie 等人,2016 年)。DEC 的网络结构。
我们不会在本文中详细讨论它们,但这些工作的基本思想本质上是相同的,即通过深度神经网络同时学习特征表示和聚类分配。这种方法被称为深度聚类。
动机
我们能否在聚类之前保留数据点的类别成员?
尽管深度聚类方法同时学习聚类分配和特征表示,但它们并没有明确设定保留数据集的类别邻域结构。这成为了我们研究的动机,即我们能否在学习到的深度网络表示上执行聚类,同时保留数据集的类别邻域结构。
2019 年,Not Too Deep或 N2D 聚类方法被提出,他们学习了数据集的潜在代码表示,进一步使用 t-SNE、Isomap 和 UMAP 等技术搜索潜在流形。所得到的流形是数据集的聚类友好表达。因此,在流形学习之后,他们将学习到的流形用作聚类的数据集特征。通过这种方法,他们能够获得良好的聚类性能。与深度聚类算法相比,N2D 是一个相对简单的方法,我们提出了类似的方法。
学习解耦表示
我们还使用自编码器网络学习数据集的潜在代码表示,然后将该表示用于聚类。我们在如何学习更友好于聚类的表示方面有所不同。我们提议解耦自编码器网络学习到的表示,而不是使用流形学习技术。
作者绘制的图。最小化类似类别数据点之间的距离,从而更好地分离类别不同的数据点。
为了解开学习到的表示,我们使用软最近邻损失或 SNNL 来衡量类似类别数据点的缠结程度。这个损失函数的作用是在神经网络的每个隐藏层中最小化类似类别数据点之间的距离。Frosst、Papernot 和 Hinton 在他们 2019 年的论文中使用了一个被T表示的固定温度值。温度因子决定了如何控制对点对距离的重视程度,例如,在低温下,损失由小距离主导,而在实际上,远离的表示之间的距离变得不太相关。他们在他们的论文中用 SNNL 进行了判别和生成任务。
作者绘制的图。我们从Neelakantan 等人,2015得到了指数,但它可以是任何值。
在我们的工作中,我们使用了 SNNL 进行聚类,并引入了一个退火温度的使用,而不是固定温度。我们的退火温度是关于训练时期数的反函数,由τ表示。
作者提供的图。比较了使用退火温度和固定温度的软最近邻损失。我们从高斯分布中采样并随机标记了 300 个数据点,并对它们进行软最近邻损失的梯度下降。左侧的图展示了标记点的初始状态。我们可以看到,从第 20 个周期到第 50 个周期,潜在编码中的簇分离情况,使得类之间更加孤立。我们在论文中展示了在基准数据集上的解耦表示。
在从高斯分布中随机抽取并标记的 300 个数据点上运行梯度下降,我们可以看到使用我们的退火温度进行 SNNL 时,与使用固定温度相比,我们发现了解耦速度更快。正如我们所见,即使在第 20 个周期,使用退火温度时,类相似的数据点会比使用固定温度时更聚集在一起或纠缠在一起,这一点也通过 SNNL 值的数值结果得到了证明。
我们的方法
我们的贡献包括使用 SNNL 进行特征表示的解耦,用于 SNNL 的退火温度,以及相比于深度聚类方法的更简单的聚类方法。
我们的方法可以总结如下,
-
我们训练了一个自动编码器,其复合损失包括二进制交叉熵作为重建损失,以及软最近邻损失作为正则化器。为了保持数据集的类邻域结构,我们最小化了自动编码器每个隐藏层的 SNNL。
-
训练后,我们使用数据集的潜在编码表示作为数据集特征进行聚类。
在解耦表示上的聚类
我们的实验配置如下,
-
我们使用了MNIST、Fashion-MNIST和EMNIST平衡基准数据集。数据集中的每个图像都被展平为一个 784 维的向量。我们使用其真实标签作为伪聚类标签,以衡量我们模型的聚类准确性。
-
由于计算限制和保持方法简单,我们没有进行超参数调优或其他训练技巧。
-
其他正则化器如 dropout 和 batch norm 被省略,因为它们可能会影响解耦过程。
-
我们计算了模型在四次运行中的平均性能,每次运行有不同的随机种子。
聚类性能
然而,自动编码和聚类都是无监督学习任务,而我们使用的 SNNL 是一种利用标签来保持数据集类邻域结构的损失函数。
作者提供的图。
鉴于此,我们通过使用基准数据集中的少量标记训练数据来模拟标记数据的缺乏。我们使用的标记示例数量是随意选择的。
我们从文献中获取了 DEC、VaDE、ClusterGAN 和 N2D 的报告聚类准确性作为基线结果,在上表中,我们可以看到我们的发现总结,其中我们的方法优于基线模型。
注意,这些结果是每个数据集四次运行中的最佳聚类准确性,因为文献中的基线结果也是各自作者报告的最佳聚类准确性。
可视化解耦表示
为进一步支持我们的发现,我们可视化了我们网络对每个数据集的解耦表示。
对于 EMNIST Balanced 数据集,我们随机选择了 10 个类别以便于更清晰的可视化。
从这些可视化中,我们可以看到,每个数据集的潜在代码表示确实通过具有明确的聚类变得更加聚类友好,这从聚类分散度中可以看出。
图由作者提供。3D 可视化比较了三个数据集的原始表示和解耦潜在表示。为了实现这种可视化,表示通过 t-SNE 编码,困惑度 = 50,学习率 = 10,优化了 5,000 次迭代,所有计算使用了相同的随机种子。然而,为了进行聚类,我们使用了更高的维度以获得更好的聚类性能。
在较少标记示例上的训练
我们还尝试在较少的标记示例上训练我们的模型。
图由作者提供。在使用少量标记数据进行训练时,测试了 MNIST 和 Fashion-MNIST 测试集上的聚类准确性。无论是原始表示还是基线自编码器都没有利用标记数据集。
在上图中,我们可以看到,即使在较少的标记训练示例下,解耦表示的聚类性能仍与我们文献中的基线模型相当。
这意味着在标记数据集稀缺的情况下,这种方法可以用于产生良好的结果。
结论
与深度聚类方法相比,我们通过使用自编码器重构损失和软最近邻损失的复合损失来学习更具聚类友好的表示,从而改善了 k-Means 聚类算法的性能。
我们扩展的软最近邻损失使用了退火温度,这有助于更快、更好地解耦,从而改善了基准数据集上的聚类性能。至此,我们的工作得出结论。
自我们的工作发表以来,几篇其他论文已在软最近邻损失上进行了建设,或被认为是相似的。尤其是,来自 Google 的监督对比(SupCon)学习论文,但其不同之处在于 SupCon 方法提出了嵌入的归一化,增加了数据增强的使用,有一个可丢弃的对比头部和两阶段训练。
另一方面,我们的工作需要相对较低的硬件资源,同时取得了良好的结果。
我们讨论如何在下一篇文章中实现软最近邻损失。
参考文献
-
Frosst, Nicholas, Nicolas Papernot 和 Geoffrey Hinton 的“使用软最近邻损失分析和改进表示”。机器学习国际会议。PMLR,2019 年。
-
Goldberger, Jacob 等人的“邻域分析组件”。神经信息处理系统进展。2005 年。
-
Khosla, Prannay 等人的“监督对比学习”。神经信息处理系统进展 33 (2020): 18661–18673。
-
Salakhutdinov, Ruslan 和 Geoff Hinton 的“通过保持类邻域结构学习非线性嵌入”。人工智能和统计。2007 年。
改善零-shot CLIP 的性能和可解释性
原文:
towardsdatascience.com/improving-performance-and-explainability-of-zero-shot-clip-33e579d3f4bb
第二部分 — 通过 LLM 描述进行视觉分类
·发布在 Towards Data Science ·阅读时间 6 分钟·2023 年 11 月 25 日
--
这是关于提升零-shot CLIP 性能系列文章的第二部分。在第一部分中,我详细解释了 CLIP 模型的工作原理,并描述了一种简单的方法来提高其性能。这包括通过大型语言模型(LLM)生成的定制提示来扩展标准提示,如 “{class} 的图片”。如果你还没有阅读第一部分,可以在 这里 找到。在这篇文章中,我们将介绍一种相对类似的方法来提高零-shot CLIP 性能,同时这种方法具有很高的可解释性。
介绍
CLIP 模型是一个令人印象深刻的零-shot 预测器,能够对其没有明确训练过的任务进行预测。尽管它具有固有的能力,但仍然存在几种策略可以显著提高其性能。在第一篇文章中,我们已经见过其中一种策略,然而,虽然提高性能是有价值的,但有时我们可能愿意做出权衡,以优先考虑更好的可解释性。在本系列的第二篇文章中,我们将探讨一种不仅提升零-shot CLIP 模型性能,还确保其预测结果易于理解和解释的方法。
深度神经网络的可解释性
目前有多种解释性技术可用于深度学习模型。在上一篇文章中,我深入探讨了集成梯度,这是一种说明输入的每个特征如何影响机器学习模型输出的方法,尤其是深度神经网络。另一种流行的模型解释方法是基于 Shap 值,我们根据合作博弈理论中的概念来分配每个特征对模型输出的贡献。虽然这些方法是通用的,可以应用于任何深度学习模型,但它们的实现和解释可能有些挑战。CLIP,它已经训练将图像和文本特征映射到相同的嵌入空间,提供了一种基于文本的替代解释方法。这种方法更具用户友好性,并提供了易于解释的方式,为模型解释提供了不同的视角。
问题的快速回顾
作为这一系列的第一部分的快速回顾,我们在这里解决的问题是预测下面显示图像的类别:
来自FreeImages的树蛙图像(许可证:www.freeimages.com/license
)
使用简单提示 “{class}的图片” 的标准方法给出了错误的答案,预测 “有尾巴的青蛙” 的概率分数为 0.68*:
from transformers import CLIPProcessor, CLIPModel
import torch
import requests
from PIL import Image
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
url = "https://images.freeimages.com/images/large-previews/342/green-tree-frog2-1616738.jpg"
image = Image.open(requests.get(url, stream=True).raw)
inputs = processor(text=["a photo of a tree frog", "a photo of a tailed frog"], images=image, return_tensors="pt", padding=True)
outputs = model(**inputs)
logits_per_image = outputs.logits_per_image # this is the image-text similarity score
probs = logits_per_image.softmax(dim=1) # we can take the softmax to get the label probabilities
print(probs)
"""
Output:
tensor([[0.3164, 0.6836]], grad_fn=<SoftmaxBackward0>)
"""
现在让我们看看如何改进它。
通过 LLMs 的描述进行视觉分类
为了提高零-shot CLIP 的预测准确性,我们将实现类似于我们在第一篇文章中讨论的思想。然而,这一次,我们不会提供像 “树蛙” 这样的通用提示,例如 “树蛙的识别特征因物种而异,但一些常见特征包括大粘附趾、突出的眼睛和明亮的颜色”,而是将其分为具体的 描述性特征。例如,考虑到 “树蛙” 和 “有尾巴的青蛙” 类别的描述性特征是:
树蛙:
-
“突出的眼睛”
-
“大嘴巴”
-
“没有尾巴”
-
“明亮的绿色”
有尾巴的青蛙:
-
“小眼睛”
-
“小嘴巴”
-
“深色”
-
“有长尾巴”
*这些特征可以通过类似于以下的 LLM 生成:
Q:在照片中区分{class}的有用特征是什么?
A:有几个有用的视觉特征可以用来识别照片中的{class}:
-*
“-” 是重要的,因为它会迫使模型生成一个特征列表。
接下来,类似于我们在第一篇文章中所做的,为了对青蛙的图像进行分类,我们会取这些文本特征描述的平均向量嵌入,这些描述代表了多模态空间中的每个类别,并评估哪个平均向量最接近我们要分类的测试图像。在代码中,我们有:
# define features description for each class
features = {"tree frog": [
"protruding eyes", "large mouth", "without a tail", "bright green colour"
],
"tailed frog": [
"tiny eyes", "small mouth", "has long tail", "dark colour"
]}
# image embedding
image_features = model.visual_projection(model.vision_model(inputs['pixel_values']).pooler_output)
tree_frog_vector = model.text_model(processor(features['tree frog'], return_tensors="pt", padding=True)['input_ids']).pooler_output
# take the mean prompt embedding
tree_frog_vector = tree_frog_vector.mean(dim=0, keepdims=True)
# final projection
tree_frog_vector = model.text_projection(tree_frog_vector)
tailed_frog_vector = model.text_model(processor(features['tailed frog'], return_tensors="pt", padding=True)['input_ids']).pooler_output
# take the mean prompt embedding
tailed_frog_vector = tailed_frog_vector.mean(dim=0, keepdims=True)
# final projection
tailed_frog_vector = model.text_projection(tailed_frog_vector)
# concatenate
text_features = torch.cat([tree_frog_vector, tailed_frog_vector], dim=0)
# normalize features
image_features = image_features / image_features.norm(dim=-1, keepdim=True)
text_features = text_features / text_features.norm(dim=-1, keepdim=True)
# cosine similarity as logits
logit_scale = model.logit_scale.exp()
logits_per_image = logit_scale * image_features @ text_features.t()
logits_per_image.softmax(dim=1)
"""
Output:
tensor([[0.8901, 0.1099]], grad_fn=<SoftmaxBackward0>)
"""
首先,我们观察到我们的预测现在是准确的,模型正确地识别出类别为“树蛙”。虽然我们在本系列的第一部分中也达到了正确的分类结果,但该方法的显著区别在于它提供了高可解释性。我们可以检查每个特征描述的非标准化分数S(feature),而不是简单地取特征描述的平均值。这使我们能够理解模型为何预测了某个特定类别:
# here we don't average the textual features as we want to see
# the score for each feature separately
tree_frog_vector = model.text_model(processor(features['tree frog'], return_tensors="pt", padding=True)['input_ids']).pooler_output
tree_frog_vector = model.text_projection(tree_frog_vector)
text_features_tree_frog = tree_frog_vector
text_features_tree_frog = text_features_tree_frog / text_features_tree_frog.norm(dim=-1, keepdim=True)
logit_scale = model.logit_scale.exp()
logits_per_image = logit_scale * image_features @ text_features_tree_frog.t()
logits_per_image
"""
Output:
tensor([[25.5400, 22.6840, 21.3895, 25.9017]], grad_fn=<MmBackward0>
"""
tailed_frog_vector = model.text_model(processor(features['tailed frog'], return_tensors="pt", padding=True)['input_ids']).pooler_output
tailed_frog_vector = model.text_projection(tailed_frog_vector)
text_features_tailed_frog = tailed_frog_vector
text_features_tailed_frog = text_features_tailed_frog / text_features_tailed_frog.norm(dim=-1, keepdim=True)
logit_scale = model.logit_scale.exp()
logits_per_image = logit_scale * image_features @ text_features_tailed_frog.t()
logits_per_image
"""
Output:
tensor([[24.0911, 22.3996, 21.2813, 21.0066]], grad_fn=<MmBackward0>
"""
S(“突出眼睛”) = 25.5400 > S(“小眼睛”) = 24.0911;
S(“大嘴”) = 22.6840 > S(“小嘴”) = 22.3996;
S(“没有尾巴”) ~ S(“无尾”) 可能相似,因为尾巴是
图片中不可见;
S(“亮绿色”) = 25.9017 > S(“深色”)= 21.0066;
属于“树蛙”类别的特征分数高于“有尾蛙”类别的特征分数。分析这些特征分数有助于我们理解为何模型预测了某个类别。在这个例子中,像“突出眼睛”、“亮绿色”和“大嘴”等特征得到了非常高的分数,为预测的类别提供了清晰的解释。在第一部分描述的方法中没有这种解释水平,因为生成的提示非常通用,并且包含了不同概念的句子。将提示改为简单的特征描述可以让我们兼得两全其美——高准确性和良好的可解释性。
结论
在系列的第二部分,我们展示了如何通过改进标准提示“{类别}的图片”来提升性能。这个解决方案不仅具有可扩展性,因为 LLM 可以为任何数量的类别和数据集生成描述性特征,而且具有高度的可解释性。在即将到来的文章中,我们将探讨利用每个类别的少量图像示例的少样本学习方法,以实现比零样本方法更高的准确性。
参考文献
[2] openreview.net/pdf?id=jlAjNL8z5cs
通过自适应损失平衡提升物理信息神经网络
如何通过 ReLoBRaLo、学习率退火等方法提升 PINN 的性能
·
阅读 发表在 Towards Data Science ·14 分钟阅读·2023 年 1 月 31 日
--
图片由 David Clode 提供,来源于 Unsplash
在本文中,我们回顾了 PINNs 的基础知识,探讨了不平衡损失的问题,并展示了由 Michael Kraus 和我提出的平衡方案 ReLoBRaLo (相对损失平衡与随机回溯) [1] 如何显著提升训练过程。还可以通过两个附带的笔记本体验这种技术在实际 PDE 问题中的应用:
如果你点击了这篇文章,可能是因为你已经对物理信息神经网络 (PINN) [2]有了相当好的理解。也许你在网上找到了一些教程,并在像 Burgers 或 Helmholtz 方程这样的知名基准上实现了 PINNs。利用神经网络的力量来解决复杂的偏微分方程 (PDEs) 的想法确实很有吸引力。但正如我们许多人痛苦地发现的那样,使用 PINNs 的现实过程也可能非常令人沮丧。如果你继续尝试将这些工具应用于你自己研究中遇到的尚未在文献中很好记录的 PDE,那么很可能 vanilla PINNs 的表现不会如你所愿。更糟的是,它们可能比像有限元方法 (FEM) 这样的成熟方法收敛得更慢!
请耐心点,我曾经经历过这一切,事实上,每次我尝试将 PINNs 应用到新问题上时,都是如此。尽管自其提出以来的五年中取得了一些进展,并且有几十年的利用可微分神经网络解决微分方程的研究 [3],但目前仍没有一种易于使用、即插即用的 PINNs 版本可以无缝转移到任何类型的问题上。
你看,PINNs 通过对输出相对于输入进行多次高阶导数,将这些导数用于构建残差和边界条件,从而在其损失函数中利用微分方程。这意味着每个偏微分方程从根本上改变了 PINN 的训练过程。可能需要调整架构,例如添加或删除层和节点,此外,其他更复杂的超参数也可能对建模能力产生关键影响,其中许多是 PINNs 的特性,无法在经典神经网络的文献中找到。这些可能包括激活函数的选择、物理域上的采样程序,或者,非常棘手的是,微分方程中测量单位的选择。
虽然我无法给你使 PINNs 工作所需的所有配方,但我可以告诉你一些基本步骤,如果没有这些步骤,你的努力很可能是徒劳的。但在我揭示这些关键工具之前,请允许我提供一些背景,以更好地理解我的论点。让我们退后一步,打开一个括号。
基准 PDE
为了说明,让我们引入赫尔姆霍兹和Kirchhoff 板弯曲方程。但在你开始感到不知所措之前,让我向你保证,理解这些偏微分方程的复杂性并不是跟随本文其余部分的必要条件。如果你想跳过这一部分,只需知道赫尔姆霍兹偏微分方程是一个具有零阶(Dirichlet)边界条件的二阶偏微分方程,而Kirchhoff 板弯曲方程是一个具有零阶和二阶导数边界条件的四阶偏微分方程。
这个方程是一个四阶偏微分方程(PDE),描述了板在载荷下的变形。方程中的未知函数 u 表示板在给定点 (x, y) 的垂直位移(例如,以米为单位)。施加在板上的载荷由函数 p(x, y) 表示。方程中的常数 D 包含了板的各种属性,如其厚度、弹性模量和密度。
Kirchhoff 板弯曲方程。
所以,Kirchhoff 方程说明了变形的四阶导数等于施加在板上的载荷除以一个常数因子。相当直接,对吧?
当然,每个经验丰富的 PDE 专家都知道,尽管主方程看起来优雅,但没有适当的边界条件,它只是一种毫无意义的抽象。毕竟,有无限多的方程可以满足它。
那么,让我们也引入边界条件:
简支边缘的边界条件
其中 W 和 H 分别定义了板的宽度和高度。第一行的边界条件显示了零阶(Dirichlet),并指出板的边缘不允许弯曲。第二行显示了二阶导数,这要求边缘的弯矩为零。这可以通过一个被下面的梁支撑(因此零阶导数为零)并被上面的另一根梁挤压(导致弯矩为零)的板边缘来说明。
一个Kirchhoff 板及其在正弦载荷下的变形(以米为单位),W = H = 10 和 D = 20.83。图由作者提供。
赫尔姆霍兹方程:介质中波的建模
赫尔姆霍兹方程是一个描述波在介质中传播的偏微分方程。它是一个二阶方程,以德国物理学家赫尔曼·冯·赫尔姆霍兹命名。
赫尔姆霍兹方程,其中 u(x, y) 是未知函数,k 是波数
其中 k 是波数,u(x, y) 是待求的未知函数。对于这个问题,我们将使用域四个边缘上的零阶 Dirichlet 边界条件:
赫尔姆霍兹偏微分方程的Dirichlet 边界条件
一个赫尔姆霍兹波传播的例子,边界条件为零 Dirichlet。图由作者提供。
PINN 损失函数
如果在定义 Kirchhoff 或 Helmholtz 函数的过程中我让你迷失了,不必担心。我花了半年多时间,以及无数耐心的土木工程师的解释,才得以向你讲解这些公式。
关键在于理解如何将这些方程转化为可用于训练我们 PINN 的损失函数,这里针对 Helmholtz 方程:
import tensorflow as tf
import tensorflow.experimental.numpy.isclose
TOL = 1e-5
def compute_loss(self, x, y, u, dudxx, dudyy, eval=False):
"""
Computes the physics-informed loss for Helmholtz's PDE.
Parameters
----------
x : tf.Tensor of shape (batch_size, 1)
x coordinate of the points in the current batch
y : tf.Tensor of shape (batch_size, 1)
y coordinate of the points in the current batch
u : tf.Tensor of shape (batch_size, 1)
predictions made by our PINN (dim 0)
dudxx : tf.Tensor of shape (batch_size, 1)
second-order derivative of the predictions w.r.t. x
dudyy : tf.Tensor of shape (batch_size, 1)
second-order derivative of the predictions w.r.t. y
"""
# governing equation loss
L_f = (dudxx + dudyy + self.k**2 * u - \
(-np.pi**2 - (4 * np.pi)**2 + self.k**2) * tf.math.sin(np.pi * x) * tf.math.sin(4 * np.pi * y))**2
# determine which points are on the boundaries of the domain
# if a point is on either of the boundaries, its value is 1 and 0 otherwise
x_lower = tf.cast(isclose(x, -1, rtol=0., atol=EPS), dtype=tf.float32)
x_upper = tf.cast(isclose(x, 1, rtol=0., atol=EPS), dtype=tf.float32)
y_lower = tf.cast(isclose(y, -1, rtol=0., atol=EPS), dtype=tf.float32)
y_upper = tf.cast(isclose(y, 1, rtol=0., atol=EPS), dtype=tf.float32)
# compute 0th order boundary condition loss
L_b = ((x_lower + x_upper + y_lower + y_upper) * u)**2
if eval:
L_u = (tf.math.sin(np.pi*x) * tf.math.sin(4*np.pi*y) - u)**2
return L_f, L_b, L_u
return L_f, L_b
你可以在实现 ReLoBRaLo 的笔记本中找到完整的代码,链接为Helmholtz和Kirchhoff PDEs。
多目标优化
正如我们已经确定的,我们的 Helmholtz PDE 的最终损失函数将包含两个,而 Kirchhoff PDE 则包含三个目标:
-
Helmholtz:控制方程的损失 L_f 和 0 阶边界条件的损失 L_b0。
-
Kirchhoff:除了 L_f 和 L_b0 之外,Kirchhoff 还包括一个用于二阶边界条件 L_b2 的项。
因此,这些损失属于多目标优化(MOO)范畴,就像大多数涉及 PINNs 的应用一样。
将多个目标聚合为单一损失的方式通常是通过线性标量化:
其中 lambda 是用于控制每个项对总损失贡献的缩放因子。但为什么它们是必要的?
不平衡梯度问题
在收集必要背景信息后,我们可以最终关闭括号)并继续探索为何 PDE 中的测量单位会影响 PINNs 的收敛性。你看,我们的损失函数中的几个目标——L_f、L_b0 和 L_b2——每个都有不同的测量单位。L_b0 对于 Kirchhoff 可能以米为单位测量,而 L_b2 以 Nm 为单位测量,板上的负载以 MN 每平方米为单位测量。这在每个项的大小上产生了显著的差异,导致梯度计算严重偏向具有最高幅度的项。Helmholtz 方程及任何其他 PDE 也是如此。
让我们来看看这在 Helmholtz 方程中的含义。
训练 PINN 模型时对 Helmholtz 方程损失的演变。L_f 是对控制方程的平方损失,L_b 是对边界条件的平方损失,L_u 是预测值与解析解之间的平方损失。图示由作者提供。
注意到在训练开始时,控制方程损失 L_f 比边界条件的损失大几个数量级,因此,L_b 的值实际上会增加。这种量级上的差异可能导致 PINN 优先考虑 L_f 而忽视 L_b,最终收敛到一个满足控制方程但忽略关键边界条件的解。这种效果可以通过验证损失 L_u 的图表观察到,验证损失 L_u 与边界损失 L_b 遵循相同的模式,表明验证性能与边界上的表现密切相关。
那么 Kirchhoff PDE 呢?
在训练 PINN 以解决 Kirchhoff 方程时损失的演变。L_f 是控制方程的平方损失,L_b0 是 Dirichlet 边界条件的平方损失,L_b2 是矩的边界条件的平方损失,而 L_u 是预测与解析解之间的平方损失。图由作者提供。
对于 Kirchhoff 方程来说,情况正好相反。这里,边界条件的收敛速度远远快于控制方程。最可能的解释是,控制方程涉及四阶导数,因此优化起来尤其困难。这表明不平衡损失的原因不仅仅是项之间量级的差异,还包括激活函数的选择和每一项所逼近函数的复杂性。
不平衡的梯度并不限于 Helmholtz 或 Kirchhoff PDEs。许多研究已经记录了在各种 PINN 应用中存在这一问题 [4]。这里的关键点是,为了得到准确的解,必须在损失函数中的所有目标之间取得平衡。
自适应损失平衡方案
为了减轻不平衡损失和梯度的问题,可以借助之前介绍的多目标优化中的线性标量化的缩放因子 lambda。选择较大的 lambda 值用于量级较小或更难优化的项可以帮助平衡最终梯度的贡献,从而确保所有项都被适当地逼近。然而,手动完成这项任务是繁琐的,需要许多迭代,因此在时间和计算资源方面耗费较大。
这就是为什么研究人员提出了损失平衡方案,如 Gradnorm [5]、SoftAdapt [6] 或 学习率退火 [4]。
相对损失平衡与随机回溯(ReLoBRaLo)
在本文中,我们将重点介绍一种名为相对损失平衡与随机回顾(ReLoBRaLo)的方案[1],它是上述方法的结合体。
ReLoBRaLo 的目标是确保损失函数中的每一项随着时间的推移都有相同的进展,相对于其在训练开始时的值。例如,如果 L_f 从训练开始以来提高了 50%,我们希望其他项也以大致相同的速度提高,并达到 50% 的减少。然而,如果某一项始终以较慢的速度提高,ReLoBRaLo 会逐渐增加该项的缩放因子 lambda,从而增加其对梯度计算的贡献。
假设我们有 n 个损失项 L_i,并且用 L_i(t) 表示训练迭代 t 时该项的值。我们可以通过将当前迭代的值 L_i(t) 除以训练开始时的值 L_i(0) 来衡量其进展:
ReLoBRaLo 通过将每个损失项 L_i(t) 的当前值除以第一个迭代的值 L_i(0),来衡量每个项 i 从训练开始以来的进展。
训练开始以来的进展越大,这个操作的结果就会越小。观察这正是我们所期望的:我们的方案应该对进展缓慢的项赋予较高的缩放因子,而对进展较快的项赋予较小的缩放因子——而且所有这一切都应该独立于项的绝对值。因此,我们可以使用 L_i(t) / L_i(0) 来计算损失函数中各项的缩放因子。
尽管这是 ReLoBRaLo 的关键组件,但它还包含了许多额外的扩展,这些扩展被发现可以进一步提高性能。然而,为了便于本文的可读性,我将这些内容留给感兴趣的读者查看论文并了解更多关于所用方法及其动机的信息。
那么它有效吗?让我们来看看使用 ReLoBRaLo 平衡各项对总损失的贡献时 Helmholtz PDE 的损失演变情况:
在 Helmholtz 方程上训练 PINN 并使用 ReLoBRaLo 时损失的演变。L_f 是对主方程的平方损失,L_b 是对边界条件的平方损失,L_u 是预测值与解析解之间的平方损失。图由作者提供。
虽然主要方程 L_f 的损失不再有太大进展(在前一个图中收敛于大约 -3.8),但边界条件 L_b 以及由此产生的验证损失 L_u 收到了更多的权重。最终的验证损失相对于解析解有了 65% 的改善。让我们看看 ReLoBRaLo 计算的缩放值:
通过 ReLoBRaLo 获得的 Helmholtz PDE 主要方程项 L_f(蓝色)和边界条件 L_b(橙色)的缩放因子 lambda_i。图由作者提供。
Kirchhoff 也是如此:
在 Kirchhoff 方程上训练 PINN 并使用 ReLoBRaLo 时,损失的演变。L_f 是主要方程的平方损失,L_b0 是 Dirichlet 边界条件的平方损失,L_b2 是矩边界条件的平方损失,L_u 是预测值与解析解的平方损失。图由作者提供。
通过 ReLoBRaLo 获得的 Kirchhoff PDE 主要方程项 L_f(蓝色)、Dirichlet 边界条件 L_b0(橙色)和矩边界条件 L_b2(绿色)的缩放因子。图由作者提供。
再次,ReLoBRaLo 将误差相对于解析解的改善了一个数量级。值得注意的是,这种平衡方案几乎没有增加计算开销(参见论文)。正是这种有效性和效率让 ReLoBRaLo 成为 Nvidia 物理信息深度学习框架 Modulus 的一部分。
但真正的问题是:你能在自己的项目中使用 ReLoBRaLo 吗?答案是一个响亮的“能”!实际上,该方案可以整洁地封装成一个 keras 损失,可以通过 model.compile() 添加到你的 keras 模型中,或者在定义了自定义训练循环的情况下,在每次迭代时显式调用它。
你可以在实现 ReLoBRaLo 的笔记本中找到完整的代码,适用于 Helmholtz 和 Kirchhoff PDEs。
import tensorflow as tf
class ReLoBRaLoLoss(tf.keras.losses.Loss):
"""
Class for the ReLoBRaLo Loss.
This class extends the keras Loss class to have dynamic weighting for each term.
"""
def __init__(self, pde:HelmholtzPDE, alpha:float=0.999, temperature:float=0.1, rho:float=0.99,
name='ReLoBRaLoLoss', **kwargs):
"""
Parameters
----------
pde : PDE
An instance of a PDE class containing the PDE-specific `compute_loss` function.
alpha, optional : float
Controls the exponential weight decay rate.
Value between 0 and 1\. The smaller, the more stochasticity.
0 means no historical information is transmitted to the next iteration.
1 means only first calculation is retained. Defaults to 0.999.
temperature, optional : float
Softmax temperature coefficient. Controlls the "sharpness" of the softmax operation.
Defaults to 0.1.
rho, optional : float
Probability of the Bernoulli random variable controlling the frequency of random lookbacks.
Value berween 0 and 1\. The smaller, the fewer lookbacks happen.
0 means lambdas are always calculated w.r.t. the initial loss values.
1 means lambdas are always calculated w.r.t. the loss values in the previous training iteration.
Defaults to 0.99.
"""
super().__init__(name=name, **kwargs)
self.pde = pde
self.alpha = alpha
self.temperature = temperature
self.rho = rho
self.call_count = tf.Variable(0, trainable=False, dtype=tf.int16)
self.lambdas = [tf.Variable(1., trainable=False) for _ in range(pde.num_terms)]
self.last_losses = [tf.Variable(1., trainable=False) for _ in range(pde.num_terms)]
self.init_losses = [tf.Variable(1., trainable=False) for _ in range(pde.num_terms)]
def call(self, xy, preds):
x, y = xy[:, :1], xy[:, 1:]
# obtain the unscaled values for each term
losses = [tf.reduce_mean(loss) for loss in self.pde.compute_loss(x, y, preds)]
# in first iteration (self.call_count == 0), drop lambda_hat and use init lambdas, i.e. lambda = 1
# i.e. alpha = 1 and rho = 1
# in second iteration (self.call_count == 1), drop init lambdas and use only lambda_hat
# i.e. alpha = 0 and rho = 1
# afterwards, default procedure (see paper)
# i.e. alpha = self.alpha and rho = Bernoully random variable with p = self.rho
alpha = tf.cond(tf.equal(self.call_count, 0),
lambda: 1.,
lambda: tf.cond(tf.equal(self.call_count, 1),
lambda: 0.,
lambda: self.alpha))
rho = tf.cond(tf.equal(self.call_count, 0),
lambda: 1.,
lambda: tf.cond(tf.equal(self.call_count, 1),
lambda: 1.,
lambda: tf.cast(tf.random.uniform(shape=()) < self.rho, dtype=tf.float32)))
# compute new lambdas w.r.t. the losses in the previous iteration
lambdas_hat = [losses[i] / (self.last_losses[i] * self.temperature + EPS) for i in range(len(losses))]
lambdas_hat = tf.nn.softmax(lambdas_hat - tf.reduce_max(lambdas_hat)) * tf.cast(len(losses), dtype=tf.float32)
# compute new lambdas w.r.t. the losses in the first iteration
init_lambdas_hat = [losses[i] / (self.init_losses[i] * self.temperature + EPS) for i in range(len(losses))]
init_lambdas_hat = tf.nn.softmax(init_lambdas_hat - tf.reduce_max(init_lambdas_hat)) * tf.cast(len(losses), dtype=tf.float32)
# use rho for deciding, whether a random lookback should be performed
new_lambdas = [(rho * alpha * self.lambdas[i] + (1 - rho) * alpha * init_lambdas_hat[i] + (1 - alpha) * lambdas_hat[i]) for i in range(len(losses))]
self.lambdas = [var.assign(tf.stop_gradient(lam)) for var, lam in zip(self.lambdas, new_lambdas)]
# compute weighted loss
loss = tf.reduce_sum([lam * loss for lam, loss in zip(self.lambdas, losses)])
# store current losses in self.last_losses to be accessed in the next iteration
self.last_losses = [var.assign(tf.stop_gradient(loss)) for var, loss in zip(self.last_losses, losses)]
# in first iteration, store losses in self.init_losses to be accessed in next iterations
first_iteration = tf.cast(self.call_count < 1, dtype=tf.float32)
self.init_losses = [var.assign(tf.stop_gradient(loss * first_iteration + var * (1 - first_iteration))) for var, loss in zip(self.init_losses, losses)]
self.call_count.assign_add(1)
return loss
非常感谢你阅读到本文的最后!如果你觉得这篇文章对你有帮助,并希望在自己的工作中使用 ReLoBRaLo 或笔记本,请使用 此引用。你可以在 rabischof.ch 和我的同事在 mkrausai.com 上找到更多关于我的信息。
[1] Rafael Bischof 和 Michael Kraus. 面向物理的深度学习的多目标损失平衡。arXiv 预印本 arXiv:2110.09813, 2021。
[2] M. Raissi, P. Perdikaris, 和 G. E. Karniadakis,《物理信息神经网络:解决涉及非线性偏微分方程的正向和逆向问题的深度学习框架》,《计算物理学杂志》378 (2019),686–707。
[3] H. Lee 和 I. S. Kang,《求解微分方程的神经算法》,《计算物理学杂志》91 (1990),第 1 期,110–131
[4] Wang, S., Teng, Y., 和 Perdikaris, P. 理解和缓解物理信息神经网络中的梯度病态。arXiv 预印本 (2020 年 1 月),arXiv:2001.04536。
[5] Chen, Z., Badrinarayanan, V., Lee, C.-Y., 和 Rabinovich, A. GradNorm: 用于深度多任务网络的自适应损失平衡的梯度归一化。arXiv 预印本 (2017 年 11 月),arXiv:1711.02257。
[6] Heydari, A. A., Thompson, C. A., 和 Mehmood, A. SoftAdapt: 用于多部分损失函数的神经网络的自适应损失加权技术。arXiv 预印本 (2019 年 12 月),arXiv:1912.12355。
在 RAG 管道中通过混合搜索提升检索性能
如何通过将传统的基于关键词的搜索与现代的向量搜索相结合来找到更相关的搜索结果
·
关注 发表在 Towards Data Science · 8 分钟阅读 · 2023 年 11 月 28 日
--
具有混合搜索功能的搜索栏
随着对 检索增强生成(RAG)管道的兴趣增加,开发者们开始讨论在构建具有生产就绪性能的 RAG 管道时面临的挑战。正如生活中的许多方面一样,帕累托原则也在 RAG 管道中发挥作用,其中实现最初的 80%相对简单,但要达到剩下的 20%以实现生产就绪则证明具有挑战性。
一个经常重复的主题是通过混合搜索来改善 RAG 管道的检索组件。
已经获得经验的开发者们 开始分享他们的见解。一个经常重复的主题是通过混合搜索来改善 RAG 管道的检索组件。
本文介绍了混合搜索的概念,如何通过检索更相关的结果来提高你的 RAG 管道性能,以及何时使用它。
-
什么是混合搜索
-
混合搜索如何工作?
-
混合搜索如何提高你 RAG 管道的性能?
-
你什么时候会使用混合搜索?
-
总结
什么是混合搜索
混合搜索是一种结合两种或更多搜索算法的搜索技术,以提高搜索结果的相关性。尽管没有定义具体结合了哪些算法,但混合搜索最常指的是传统基于关键词的搜索与现代向量搜索的结合。
传统上,基于关键词的搜索是搜索引擎的明显选择。然而,随着机器学习(ML)算法的出现,向量嵌入启用了一个新的搜索技术——称为向量或语义搜索——使我们能够在语义上进行数据搜索。然而,这两种搜索技术都有必须考虑的重要权衡:
-
基于关键词的搜索: 尽管其精确的关键词匹配能力对特定术语(如产品名称或行业术语)有益,但对拼写错误和同义词敏感,这会导致它遗漏重要的上下文。
-
向量或语义搜索: 尽管它的语义搜索功能基于数据的语义意义进行多语言和多模态搜索,并使其对拼写错误具有鲁棒性,但可能会遗漏重要的关键词。此外,它依赖于生成的向量嵌入的质量,并对领域外的术语敏感。
将基于关键词的搜索与向量搜索结合成混合搜索,使你能够利用这两种搜索技术的优点,从而提高搜索结果的相关性,特别是在文本搜索的使用场景中。
例如,考虑搜索查询“如何使用.concat()
合并两个 Pandas DataFrames?”关键词搜索会帮助找到与方法.concat()
相关的结果。然而,由于“merge”这个词有诸如“combine”、“join”和“concatenate”等同义词,如果我们能利用语义搜索的上下文感知能力,将会更有帮助(有关更多细节,请参见何时使用混合搜索)。
如果你感兴趣,可以在这个实时演示中尝试不同的基于关键词的、语义的和混合搜索查询(其实现细节见这篇文章)。
混合搜索是如何工作的?
混合搜索通过融合关键词基础和向量搜索技术的搜索结果并对其进行重新排序来实现。
基于关键词的搜索
在混合搜索的背景下,基于关键词的搜索通常使用一种称为稀疏嵌入的表示,这就是为什么它也被称为稀疏向量搜索。稀疏嵌入是大多数值为零且只有少数非零值的向量,如下所示。
[0, 0, 0, 0, 0, 1, 0, 0, 0, 24, 3, 0, 0, 0, 0, ...]
稀疏嵌入可以通过不同的算法生成。生成稀疏嵌入的最常用算法是BM25(最佳匹配 25),它建立在 TF-IDF(词频-逆文档频率)方法之上并对其进行了改进。简单来说,BM25 根据词汇在文档中的频率相对于其在所有文档中的频率来强调术语的重要性。
向量搜索
向量搜索是一种现代搜索技术,随着机器学习的进步而出现。现代机器学习算法,如Transformers,可以生成各种模态(文本、图像等)的数据对象的数值表示,称为向量嵌入。
这些向量嵌入通常信息密集且大多由非零值组成(密集向量),如下所示。这也是为什么向量搜索也被称为密集向量搜索。
[0.634, 0.234, 0.867, 0.042, 0.249, 0.093, 0.029, 0.123, 0.234, ...]
搜索查询被嵌入到与数据对象相同的向量空间中。然后,其向量嵌入用于根据指定的相似性度量(例如余弦距离)计算最接近的数据对象。返回的搜索结果列出了与搜索查询相似度排名最高的数据对象。
关键词基础与向量搜索结果的融合
基于关键词的搜索和向量搜索返回的是一组独立的结果,通常是按计算的相关性排序的搜索结果列表。这些独立的搜索结果集必须被合并。
有许多不同的策略可以将两个列表的排名结果合并为一个单一的排名,正如Benham 和 Culpepper [1]的论文中所述。
一般来说,搜索结果通常首先会被评分。这些分数可以基于指定的度量计算,如余弦距离,或者仅仅是搜索结果列表中的排名。
然后,计算出的分数会与参数alpha
一起加权,这决定了每种算法的加权并影响结果的重新排名。
hybrid_score = (1 - alpha) * sparse_score + alpha * dense_score
通常,alpha
的值在 0 到 1 之间,具体取值为
-
alpha = 1
:纯向量搜索 -
alpha = 0
:纯关键词搜索
下面,你可以看到一个基于排名和alpha = 0.5
评分的关键词与向量搜索融合的最小示例。
这是一个关于如何在混合搜索中融合关键词和向量搜索结果的最小示例,评分基于排名和alpha = 0.5
(图像由作者提供,灵感来自混合搜索解释)。
混合搜索如何提高你的 RAG 管道性能?
一个RAG 管道有许多可以调整的参数来提高其性能。其中之一是提高检索到的上下文的相关性,这些上下文会被输入到 LLM 中,因为如果检索到的上下文与回答特定问题不相关,LLM 也无法生成相关的答案。
根据你的上下文类型和查询,你需要确定三种搜索技术中哪一种对你的 RAG 应用最有利。因此,参数 **alpha**
,用于控制关键词搜索和语义搜索之间的加权,可以视为需要调整的超参数。
在一个常见的使用 LangChain 的 RAG 管道中,你会通过以下方式设置检索器组件,将使用的vectorstore
组件设置为检索器,方法是.as_retriever()
:
# Define and populate vector store
# See details here https://towardsdatascience.com/retrieval-augmented-generation-rag-from-theory-to-langchain-implementation-4e9bd5f6a4f2
vectorstore = ...
# Set vectorstore as retriever
retriever = vectorstore.as_retriever()
然而,这种方法只启用了语义搜索。如果你想在 LangChain 中启用混合搜索,你需要定义一个具有混合搜索功能的特定[retriever](https://python.langchain.com/docs/integrations/retrievers)
组件,例如[WeaviateHybridSearchRetriever](https://python.langchain.com/docs/integrations/retrievers)
:
from langchain.retrievers.weaviate_hybrid_search import WeaviateHybridSearchRetriever
retriever = WeaviateHybridSearchRetriever(
alpha = 0.5, # defaults to 0.5, which is equal weighting between keyword and semantic search
client = client, # keyword arguments to pass to the Weaviate client
index_name = "LangChain", # The name of the index to use
text_key = "text", # The name of the text key to use
attributes = [], # The attributes to return in the results
)
其余的标准 RAG 管道将保持不变。
这个小的代码更改允许你在关键词搜索和向量搜索之间尝试不同的加权。注意,设置alpha = 1
等于完全的语义搜索,相当于直接从vectorstore
组件定义检索器(retriever = vectorstore.as_retriever()
)。
你何时会使用混合搜索(混合搜索用例)
混合搜索非常适用于需要启用语义搜索功能以获得更人性化的搜索体验,但也需要对特定术语(如产品名称或序列号)进行精确匹配的用例。
一个很好的例子是平台 Stack Overflow,它最近通过使用混合搜索扩展了其搜索功能。
## 像人一样提问:在 Stack Overflow 上实现语义搜索
语义搜索允许用户使用自然语言进行搜索,而不是依赖于关键字操作的僵化语法。搜索……
最初,Stack Overflow 使用 TF-IDF 将关键字匹配到文档中[2]。然而,描述你试图解决的编码问题可能会很困难。这可能导致基于你用来描述问题的词语不同而得到不同的结果(例如,组合两个 Pandas DataFrames 可以通过不同的方法实现,如合并、连接和拼接)。因此,更加上下文感知的搜索方法,如语义搜索,将对这些案例更有益。
然而,另一方面,Stack Overflow 的一个常见用例是复制粘贴错误信息。对于这种情况,精确的关键字匹配是首选的搜索方法。此外,你还需要精确的关键字匹配能力来处理方法和参数名称(例如,Pandas 中的 .read_csv()
)。
正如你所猜测的,许多类似的现实世界使用案例受益于上下文感知的语义搜索,但仍然依赖于精确的关键字匹配。这些使用案例可以通过实现混合搜索检索器组件得到显著的提升。
概述
这篇文章介绍了混合搜索的背景,作为基于关键字搜索和向量搜索的组合。混合搜索将单独搜索算法的搜索结果合并并相应地重新排序搜索结果。
在混合搜索中,参数 alpha
控制基于关键字的搜索和语义搜索之间的加权。这个参数 alpha
可以视为 RAG 管道中的一个超参数,用于调节以提高搜索结果的准确性。
使用 Stack Overflow [2] 案例研究,我们展示了混合搜索如何对语义搜索可以改善搜索体验的使用案例有帮助。然而,当特定术语频繁出现时,精确的关键字匹配仍然很重要。
享受这个故事吗?
免费订阅 以便在我发布新故事时获得通知。
## 订阅以便每当 Leonie Monigatti 发布内容时,你会收到电子邮件。
订阅以便每当 Leonie Monigatti 发布内容时,你会收到电子邮件。通过注册,如果你还没有 Medium 账户,将会创建一个 Medium 账户……
在 LinkedIn,Twitter和 Kaggle上找到我!
免责声明
在撰写本文时,我是 Weaviate 的开发者倡导者,Weaviate 是一个开源向量数据库。
参考文献
文献
[1] Benham, R., & Culpepper, J. S. (2017). 排名融合中的风险-收益权衡。发表于第 22 届澳大利亚文档计算研讨会(第 1–8 页)。
[2] Haney, D. & Gibson, D. 在 Stack Overflow 博客中。像人类一样提问:在 Stack Overflow 上实现语义搜索(访问于 2023 年 11 月 24 日)。
图片
除非另有说明,否则所有图片均由作者创建。
如何通过单元测试和 TDD 提高您 dbt 模型的代码质量
开始对您的 dbt SQL 模型进行单元测试所需了解的一切
·
关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 5 月 31 日
--
图片由 Christin Hume 提供,来源于 Unsplash
如果你是数据或分析工程师,你可能已经习惯了编写 SQL 模型并使用 dbt 测试数据质量。你为自己创建的模块化和整洁的 SQL 模型感到自豪。一切都还不错。但某个时候,你模型中的转换逻辑开始增长并变得更加复杂。你开始考虑,是否能够将模型隔离并创建单元测试来验证和记录这些逻辑会更好呢?这肯定会提高你 dbt 代码库的代码质量,对吧?
你的第一个想法是查看如何使用内置的 dbt 数据测试功能来进行这种类型的测试。经过几次谷歌搜索,你意识到其中一种方法是 创建一个自定义单元测试框架,利用 dbt 种子功能。或者你需要 将 Python 引入你的代码库,以便使用 Pytest 创建单元测试。这看起来相当繁琐,因此你开始考虑,是否能够模拟模型输入并使用 SQL 对转换后的数据进行断言会更好呢?
如果你对此有共鸣,你并不孤单。这是我所在的数据工程团队经历的过程,直到我们找到 dbt-unit-testing 库。
模拟 dbt 源和引用
使用 dbt-unit-testing 你可以通过模拟其依赖关系来独立测试 dbt 模型。根据他们的文档,它为你提供了:
-
能够模拟依赖关系
-
能够独立运行每个测试
-
快速反馈循环
-
良好的测试反馈(好吧,如我们将看到的,有一些陷阱)
这个 dbt 库使我们能够模拟模型依赖关系,并立即开始实施单元测试。正如我将在最后与您分享的,它甚至使我们能够开始实践测试驱动开发来开发我们的模型。
既然我们介绍了这个库,让我们看看开始实施真实的 dbt SQL 模型单元测试所需了解的基本知识。
为什么你应该对模型进行单元测试?
一些数据团队专注于提高其数据应用处理的数据质量,但他们常常忽视了所构建软件的代码质量。
单元测试可以通过提供一种隔离 SQL 模型并验证复杂业务逻辑的方式来帮助实现这一点。
“单元测试测试应用程序中最小的可测试软件部分,以确定它是否按预期行为” — Toby Clemson
在我们的案例中,最小的可测试软件是一个 dbt 模型。能够隔离测试模型可以确保新的更改不会破坏现有的业务逻辑,并帮助我们记录模型的预期行为。
一个典型的 dbt 应用遵循 分层架构风格,至少包含三个层级:暂存层、中间层。每一层会包含一个或多个模型。这些就是我们可以单独测试的模型。
创建我们的第一个单元测试
让我们来看一个简单的示例。我们有一个名为 health-insights 的 dbt 应用,它从上游源获取体重和身高数据,并计算体质指数。
一个典型 dbt 数据应用的分层架构
以下模型用最新的身高测量来丰富体重测量,最新的身高记录在体重测量之前。
一个 dbt 中间模型的示例
现在让我们创建一个 dbt 单元测试,以证明变换逻辑是正确的。
一个中间模型的 dbt 单元测试示例
构建模块
查看之前的测试,我们可以看到多个 dbt-unit-testing 宏在使用中:
-
dbt_unit_testing.test: 这个宏允许我们定义待测试的模型以及测试的名称。在我们的示例中,我们引用了int_weight_measurements_with_latest_height。
-
dbt_unit_testing.mock_ref: 这个宏允许我们模拟对其他 dbt 模型的引用。在我们的示例中,我们模拟了体重(stg_gym_app__weight)和身高(stg_gym_app__height)暂存数据。
-
dbt_unit_testing.expect: 这个宏允许我们对变换结果进行断言。在示例中,我们断言体重测量被最新的身高信息所丰富。
运行测试
现在让我们运行模型的单元测试。我们可以调用常用的 dbt 测试命令:
dbt test
哦,那条命令会运行整个测试套件,包括其他 dbt 数据质量检查。但我们只想运行我们的单元测试。没问题,我们可以利用 dbt 标签功能来隔离我们的单元测试。在示例中,我们用两个标签标记了我们的测试:
{{ config(tags=['unit-test', 'unit-tests']) }}
第一个是 dbt-unit-testing 库所需的模板标签。第二个是我们将用来执行单元测试的标签。
dbt test --select tag:unit-tests
探索其他类型的测试
到目前为止,我们已经看到如何编写单元测试来验证单个模型的逻辑。在创建了几个这样的测试后,我们的团队开始讨论实现新类型测试的可能性,就像我们通常为微服务等操作软件所做的那样。
“组件测试将被测试的软件范围限制在系统的一部分,通过内部代码接口操作系统,并使用测试替身将被测试的代码与其他组件隔离开来。” — 托比·克莱姆森
在微服务上下文中,组件是一个暴露特定功能的服务。如果我们将相同的概念应用到数据上下文中,那么 dbt 应用的组件测试可以实现为测试 dbt 应用是否通过模拟数据源提供了其承诺的功能。
运营应用程序的常见测试金字塔
在实现组件测试时,测试的范围会增加。我们测试整个 dbt 应用程序,仅模拟其源。
组件测试范围
这种类型的测试确保不同模型正确集成,并生成预期的数据转换结果。让我们来看一个例子:
在上面的组件测试中,我们正在测试我们的输出模型body_mass_indexes。该模型使用丰富的体重测量数据来计算用户的体重。我们直接使用 dbt_unit_testing.mock_source 宏模拟源(raw_weight 和 raw_height)。最后,我们断言输出模型的最终转换,验证体重指数(BMI)是否计算正确。
我们还可以使用我们在测试配置中指定的标签名称在隔离环境中运行这种类型的测试。
dbt test --select tag:component-test
为什么不在 SQL 中应用测试驱动开发(TDD)呢?
现在我们有能力在隔离环境中测试模型,如果我们从编写测试开始,而不是先编写任何转换逻辑,会怎样呢?
测试驱动开发(TDD)是一种软件工程实践,通过迫使开发人员首先编写测试,然后编写最少量的代码来使测试通过,从而帮助改进代码的设计。
我们的数据团队在运营系统中应用 TDD 方面有经验,因此我们决定尝试一下。
从在测试中定义给定转换的结果开始感觉非常自然。哦,如果我有这个体重和这个身高作为输入,我期望的 BMI 会是多少?我们来为此编写一个测试。在实践了一段时间的 TDD 之后,团队在将新的业务逻辑添加到转换中时仍然继续使用这一技术。
注意事项
我曾设想过一个完美的场景,你可以直接添加单元测试 dbt 包并开始立即创建测试。事实是该库仍在开发中,我们发现了一些你可能也需要注意的问题:
-
dbt-unit-testing 宏打破了不允许测试代码污染生产代码的原则。解决这个问题的一个简单方法是创建一个宏来修补原始的 ref() 和 source() 并调用测试宏。你可以在这里查看一个示例。
-
我们发现有时测试中的更改似乎没有被识别。虽然有禁用缓存的选项,但我们还没有尝试过。
-
在模拟源时,如果在 dbt 的 .yml 文件中未定义源,它将无法编译。
-
有时候测试错误消息非常晦涩。在这种情况下,我们发现自己需要查看构建文件夹中的编译 SQL 代码。
-
还要注意库文档中列出的其他限制。
结论
我们已经看到如何向我们的 dbt 项目添加单元测试和组件测试,以提高代码质量,从而提高转换逻辑的可维护性。我们还看到如何标记不同类型的测试,以便可以在本地和 CI/CD 管道中分别运行它们。最后,我们还查看了如何实践 TDD。
希望这篇文章能帮助你和你的团队开始采用单元测试,并在你的代码库扩展以满足新的数据使用案例时,创建更易维护和可扩展的 dbt 应用程序。
如果你感兴趣,可以在这个 Github 仓库中查看一个完整功能的示例。我还准备了一些练习题,如果你想用简单的示例练习 TDD 和单元测试的话。
你准备好尝试一下了吗?
这篇文章是我正在撰写的一系列关于 测试数据管道和数据产品 的文章中的一部分。
我一直期待认识新朋友。如果你想联系我,可以在 Linkedin 、 Github 、 Instagram、Substack 或者 我的个人网站 上找到我。
感谢我的 Thoughtworks 同事 Manisha 和 David 花时间审阅这篇文章的早期版本。感谢 dbt-unit-testing 包 的维护者们的出色工作。
除非另有说明,所有图片均由作者提供。
改进 Strava 训练日志
原文:
towardsdatascience.com/improving-the-strava-training-log-4d2039c49ec4
使用 Strava、Python 和 Matplotlib 进行马拉松跑者训练模式的可视化。
·发布于 Towards Data Science ·12 分钟阅读·2023 年 11 月 16 日
--
作为一名马拉松跑者,我使用 Strava。非常多。这是一个很棒的应用程序。除了所有常见的社交功能(分享活动、了解朋友的动态、查看俱乐部的活动等),我依赖 Strava 跟踪我的所有活动,并定期使用它来分析我的训练进展。或者说我尽力而为。我喜欢使用 Strava 的训练日志 来查看我当前的训练情况,相较于以前的年份,但遗憾的是,这不是 Strava 真正强项的地方,尽管训练日志是一个高级功能。
问题的一部分是训练日志提供的信息非常有限。例如,在下面的示例训练日志中,我们可以看到几周的活动记录(彩色圆圈),这些记录总结了训练活动的频率和距离,但没有关于训练强度或努力的任何信息。因此,我认为研究如何改进这一点将是一个有趣的副项目……
作者的 Strava 训练日志的一部分;截图由作者制作。
我将从讨论我对 Strava 当前训练日志中缺失内容的看法开始,并建议如何将这些缺失的信息添加到重新构想的训练日志可视化中。我将描述如何实现这一点,使用 Python 和 Matplotlib 的示例,并最终展示一个使用我自己都柏林马拉松 2023 年训练数据的具体结果示例。
动机
上面的 Strava 训练日志示例展示了我今年(2023 年)为都柏林马拉松训练的最后 4 周。每一行对应一周的训练,每一列对应一周中的不同天。我训练的日子通过颜色编码的圆圈表示,显示了当天完成的距离。红色圆圈表示比赛。深绿色圆圈表示长跑。阴影绿色圆圈表示训练 — 通常是更复杂的多部分间歇训练 — 而浅绿色圆圈表示既不是长跑也不是训练的常规跑步。所有这些类别都是由跑者手动分配的。最后,左侧列显示了相关的日期信息和每周的总距离。
这是总结几周训练的一个完全合理且视觉上吸引人的方法。然而,它仅提供了训练的一个非常有限的快照。它仅关注活动频率和训练量(距离)——这当然是相关的——但没有关于训练强度或努力的信息。例如,没有配速或心率(HR)信息,除非它恰好包含在跑者提供的会话标题中,即使如此,这也可能不是会话期间实际情况的可靠指示。没有这些信息,很难评估训练进展,更难比较一周的训练与另一周的训练,更不用说今年的训练与早期年份的训练相比,因为跑步距离只讲述了部分故事。
目标
让我们通过增强这个基本可视化来改进 Strava 的训练日志,以包含以下所有训练特征:
-
会话(或一周)的平均配速(分钟/公里)、距离和持续时间。
-
Strava 的 相对努力(痛苦)得分,用于评估整体训练强度或每周强度。
-
每个心率区间的时间显示了训练时间如何分配在轻松、中等和艰难的努力之间。Strava 使用 5 个 HR 区间(Z1, …, Z5),这些区间对应于逐渐增强的努力,并基于跑者的心率占其最大心率的比例。
HR 区间数据对跑者很有用,因为它提供了细化的努力/强度度量,并帮助跑者区分轻松和更努力的训练。如上所述,这对跟踪训练进展非常有用。此外,强度和努力也有助于跑者跟踪恢复跑步,以便他们能够以所需的 轻松努力 完成,并为本周晚些时候的更艰难训练做好准备。实际上,跑者经常被鼓励采用 80/20 方法 进行训练——80%的训练应以 轻松 努力(HR 区域 1 和 2)进行——以防止过度训练和潜在伤害。
访问 Strava 数据
本文的重点是 Strava 训练数据的可视化,因此不会详细讨论如何从 Strava 收集这些数据。只需说明 Strava 提供了一个 API,可以在获得用户许可的情况下获得授权访问你的数据或其他用户的数据。我使用了这个 API 来访问我的数据,并且网上有几篇文章——这里或 这里,例如——感兴趣的读者可以了解如何自己进行操作。
一个使用 Strava API 收集的每日会话数据样本。
上述是我从 Strava API 收集的活动数据的一个子集。每一行对应一个单独的活动,包括活动日期、距离(米)、持续时间(秒)、相对努力(痛苦评分)以及在不同心率区间内的跑步距离等信息(在心率区间内花费的时间等)。
训练会话饼图
我们的首要任务是升级当前 Strava 训练日志中单次训练活动的简单可视化;见下文。它显示了会话的名称(用户提供的或默认的 Strava 名称)和完成的距离,但没有任何配速或其他强度相关的信息。
单次训练活动(42.4km),对应今年的都柏林马拉松。
下面的图表显示了我们对同一活动的替代建议。这个新图表由几个不同的组件(标题、头部、图表、脚部)组成,以便更容易定位其不同的信息元素,而无需依赖绝对位置坐标;顺便说一句,这些元素周围的边框只是为了突出这些元素,最终可视化中不会使用这些边框。以这种方式使用单独的元素将通过使调整这些图表的大小更容易,并避免不同信息元素之间的碰撞,从而促进以后的网格布局。这些元素包括会话距离和平均配速(脚部),而这次饼图的大小与会话的总时长成比例,这也在标题中指明(190 分钟),以及自上次会话(22 小时)以来的经过(休息)时间。
对于单个会话/活动的建议可视化,显示了在每个心率区间花费的时间,以及会话的相对努力、距离、平均配速、总跑步时间以及自上次会话(休息)以来的时间等。
重要的是,与使用简单圆圈来表示会话覆盖的距离不同,饼图用于通过 HR 区间编码重要的强度信息。这些扇区按顺时针方向排列,从“中午”位置开始,按照 HR 区间 (Z1, Z2, …, Z5) 的顺序排列,扇区的大小对应于在该区间内花费的时间比例。扇区的颜色也表示 HR 区间,并与稍后讨论的每周条形图中使用的颜色匹配。在这项活动中,跑步主要发生在 Z2 和 Z3 区间,也有一些时间花在 Z4 区间。
最终,活动的总体相对努力 (RE) 分数显示在饼图的中心,背景颜色与其相对于训练块的值成比例。在这种情况下,深红色背景表明相对努力值为 362 是训练块中最高的;实际上,这是最高的。
显然,这种新的可视化提供了比 Strava 更显著的信息,但在屏幕空间上没有显著增加,并且在强调哪些信息时具有更大的灵活性。例如,上述饼图的总体大小基于整体会话时间(此示例中为 190 分钟),但可以根据运动员的需求进行配置。例如,可以使用总体距离,如 Strava 训练日志中的情况,或相对努力或功率(对于骑自行车的人)或运动员的感知努力,如果可用。如果需要,也可以用配速或功率区间代替 HR 区间。对于感兴趣的读者,本文末尾提供了重建此图表版本的 Python 示例代码。
训练周总结条形图
我们需要的第二种图表总结了一整周或训练。这在默认的 Strava 训练日志中几乎不存在,除了每行旁边提供的每周总距离。
我们希望添加有关每周在每个 HR 区间中所花时间的相关信息,以及配速和相对努力信息。我们需要以与新的饼图视觉效果兼容的方式进行,以便这两种类型的图表可以在我们的新训练日志可视化中一起使用。
新的每周总结图表如下所示,展示了比赛日还有 10 周的都柏林马拉松训练的一周。在那一周内跟踪了 238 分钟的跑步(46.88 公里)(共 4 次),平均配速为 5:40 分钟/公里。每周相对努力较高(RE = 365),大约 53%的训练是在轻松的努力下进行的(Z1 和 Z2;125/238)。条形图显示了每个 HR 区间的每周分钟数,颜色编码与会话饼图中使用的方案匹配。
2023 年 8 月 14 日至 20 日的一周的示例每周训练总结图表,距比赛日还有 10 周。
这个图表的风格和结构(标题、标题、图表、页脚)与会议饼图兼容——尺寸、位置、颜色等——这样它们可以在大型图表网格中一起使用。再次强调,根据需要很容易替换不同类型的训练数据,例如配速或功率区间,样本 Python 代码在文章末尾提供给感兴趣的读者以重新创建此图表。
设计训练日志网格
现在我们可以为每个会话构建饼图和每周条形图,是时候将这些图表汇总成一个多周可视化了。就像 Strava 训练日志一样,我们将使用网格布局,每一行对应一个训练周,每一列对应一天;见下图。最左边的列将用于提供每周总结。因此,对于一个 16 周的马拉松训练计划,我们将需要一个具有 16 行(每周一个)和 8 列(每周总结加 7 天)的网格。
简单的子图网格。
使用 Matplotlib 生成这样的网格非常简单。例如,下面的代码片段就可以完成这个任务,从而可以根据周和天来生成和定位各个图表。
简单的网格布局代码。
然而,这种方法不适用于我们需要的可视化类型,因为每个可视化组件(会议饼图或周总结)由多个独立组件(标题、页眉、图表、页脚)组成。相反,我们需要一个网格,其外观更像下面的示例,每个组件作为一个单独的轴/子图。
更复杂的子图网格,以支持训练日志可视化。
为了生成这种类型的网格,我们可以使用 Matplotlib 强大的gridspec
,并使用add_subplot
将各个元素(标题、图表、页脚等)分配到网格中的适当位置。深入探讨 Matplotlib 和gridspec
的内容超出了本文的范围,但下面展示的网格设置代码说明了总体方法。简而言之,主网格中的 16 行每行分配 10 个单位的高度(row_h
)(第 5 行)。然后,每个图表的标题、页眉和页脚部分分配 1 个单位的高度,而图表本身分配 6 个单位的高度。最后,我们为每行添加一个 gutter(1 个单位的高度),以控制行之间的间距。感兴趣的读者可以在这里了解更多。
复杂的网格布局代码。
没有什么能阻止显示比这里讨论的 16 周更多或更少的训练周。例如,大多数马拉松训练计划涵盖 12 到 20 周的训练,而在我们最终的可视化中,我们将展示 12 周的训练。
完成训练日志可视化
将所有这些内容整合起来,产生了下面的大型可视化。在这个案例中,我们展示了 12 周的都柏林马拉松训练,每一行对应一个训练周。第一列显示了每周的总结,其余 7 列显示了每天的活动。没有活动的天数标记为休息日。
作者对 2023 年都柏林马拉松的 16 周训练周期。
这是一种复杂的可视化——可能需要一个大屏幕才能舒适地查看——但它比 Strava 的默认训练日志捕捉了更多有用的训练信息。它也有助于更深入的探讨和分析。
比如,我们可以看到,我的大部分努力都是在周三进行的;周三的饼图显示了在 Z3 区间花费的时间明显多于周二和周四的轻松/恢复训练。同样,我在周六的长跑也表现出较高的强度,不仅仅因为距离,还因为这些跑步通常包含了按马拉松配速要求进行的更高强度的阶段,通常在跑步的最后阶段;这些长跑通常会在周日后跟较短的恢复跑。周六还包括了在第 5、7 和 10 周的几场比赛,这也解释了为什么这些天在 Z4 区间花费的时间比其他长跑要多。我们还可以看到,在最后两周的恢复阶段,跑步量下降,而强度保持不变,因为轻松跑被赛前的休息日取代。
最后,值得注意的是,有时跑者会每天跟踪多项活动。例如,一些跑者会进行早晨和晚上的活动,而另一些跑者则会将一个单独的训练分成几个部分,以涵盖单独的热身、主要活动和放松活动。目前,我们将一天中的所有活动合并为一个综合活动。例如,在第 5 周的周六,以上的可视化显示我参加了都柏林半程马拉松(都柏林马拉松系列赛中的一场年度比赛),但当天的数据显示为 30.2 公里的跑步,而不是 21.1 公里的半程马拉松距离。原因是这 30.2 公里包括了单独的热身和放松活动。当然,与其以这种方式汇总活动,我们可以决定实施不同的政策,例如在多项活动的日子里,特别是比赛日,重点关注最长(或最快)的活动。
结论
本文旨在探索如何开发一个更具信息性的训练日志,使用 Strava 的默认训练日志作为起点。最终的可视化捕捉了大量额外的有用信息,特别是关于训练强度和努力程度,这使得评估训练进展变得更为容易。
我们已经描述了如何使用 Python 和 Matplotlib 构建单独的活动和总结图表(下面提供了示例代码),并讨论了如何将这些图表组装成一个复杂但灵活的网格结构。
最后,值得强调的是,这种方法可以很容易地适应其他类型的运动,基于这些运动更相关的信息。例如,如上所述,骑行者可能希望在训练日志中包括功率区间而不是心率区间。实际上,随着休闲运动员变得更加仪器化,通过新的可穿戴传感器,开发者将越来越需要找到新颖的方式来呈现这些新形式的数据,以支持运动员的训练和比赛。
注意:作者制作了所有的图像和代码。
示例代码
绘制会话图表
以下代码示例展示了创建会话饼图的方法。本文假设所需的会话数据(距离、时间、强度等)已经以 Pandas 系列session
的形式提供,无需进一步解释。
绘制每周总结图表
以下代码示例展示了创建每周总结条形图的方法。它期望相关的每周会话数据作为数据框week
提供。
使用公共表表达式改进你的 SQL 逻辑
原文:
towardsdatascience.com/improving-your-sql-logic-with-common-table-expressions-26f04967fc86
使逻辑更容易编写、阅读、解释和维护
·发表于 Towards Data Science ·阅读时长 6 分钟·2023 年 12 月 5 日
--
Sam Moghadam Khamseh 的照片,来自 Unsplash
把衣物丢进一堆里并寄希望于最好的方法可能不是管理衣物的长期策略。这样很难做到:
SELECT socks FROM pile
随后执行。
你肯定会遇到困难:
SELECT shirt FROM pile WHERE wrinkles = false
当你需要一件衬衫时。
就像管理你的衣物一样,积极管理 SQL 逻辑的组织有明显的好处。
在这篇文章中,我们将探讨公共表表达式(CTEs)如何改进你的 SQL 逻辑。意大利面条代码很少是好的代码,这在 SQL 中和任何编程语言中都是一样的。在数据管理和工程中没有灵丹妙药。然而,保持你的逻辑有序且易于审查只会对你有帮助。那么,让我们深入了解吧!
一个公共表表达式是什么样的?
我有时称它们为 WITH
子句。为什么?让我们看一个例子:
一个公共表表达式的示例。
我称它们为 WITH
语句,因为它们总是以 … WITH
开头。
一目了然,理解发生了什么有多容易?有三个部分:
-
一个识别“高价值客户”的查询。
-
一个识别“新客户”的查询。
-
最终查询阶段执行一个内连接操作,将“高价值客户”和“新客户”进行连接,提供一个结果集,包含仅存在于这两个类别中的客户。
一种替代的方法
在我了解公共表表达式(CTEs / WITH
语句)之前,这就是我可能会如何处理这个查询:
在有人向我展示 CTE 的价值之前,这就是我编写所有查询的方式。
上面的查询没有什么本质上的问题。实际上,它似乎只是代码的重新组织。
但有时顺序很重要。
顺序,事情的顺序有时确实很重要(谢谢,尤达)。
我的观点是,CTE 方法更容易处理和解释。在阅读 CTE 查询时,我清楚地知道故事是:
-
我们有高价值客户。
-
我们有新客户。
-
我们的结果将返回两个客户集的内连接。
这是一个简单明了的故事。第二个查询也不复杂,但这是我阅读它的体验:
-
我们有一个客户。
-
我们有总收入(可能是针对客户的)。
-
我们有自上次见面以来的天数(可能是针对客户的)。
-
我们有一个子查询派生出满足某些收入门槛的客户。
-
我们将对另一个子查询执行内连接……
-
另一个子查询派生出在过去 90 天内首次出现的客户。
那个故事仍然足够简单易懂,但我需要六个思考步骤来拼凑这个故事,而只需三个步骤就能拼凑出 CTE 版本。我是个简单的人——将工作量减少一半对我来说意义重大。
但说真的,实际上我发现 CTE 方法将逻辑模块化,使得定义 SQL 拼图的小部分变得容易,从而能够在最终查询阶段轻松连接这些拼图。
我们上面看到的是一个玩具示例。在现实世界中,复杂问题通常需要更复杂的逻辑。寻找将复杂问题(或复杂查询)拆分成更小部分的方法是有帮助的。这样,一个复杂的问题可能变成几个简单的问题。如果你能够轻松解决几个简单的问题,那么你就成功地为复杂问题开发了一个简单的解决方案。
总结好处
根据我的经验,这些是将公共表表达式纳入 SQL 工作流程的核心好处:
-
逻辑的组织。
-
逻辑和查询目的的可读性。
-
逻辑的可维护性。
-
逻辑的可解释性。
-
性能和可扩展性(这是什么?)
1:逻辑的组织
公共表表达式为我们的查询提供了结构。类似于段落为书页提供结构,房间为房子提供结构,函数为程序提供结构,或者……你懂我的意思。
拥有可靠和可预测的模式在提高查询效率方面是绝对有利的。
2:逻辑和查询目的的可读性
查询中的结构组织有助于使其更具可读性。你是否尝试过阅读一本 1800 年代的书?做个快速的 Google 搜索。那些句子有的可能会延续好几页。
无结构的 SQL 查询就像非常非常长的句子:很难跟踪全部内容。一种思路在哪里结束,另一种思路从哪里开始?
保持逻辑的可读性是一个巨大的胜利。这对你自己以及任何将来查看你查询的人来说都是一个胜利。你的查询中的逻辑不应是一个让艾伦·图灵夜不能寐的加密算法。它们应该是如此简单,以至于非技术人员会想,“嘿,我想我知道这在做什么”。
3: 逻辑的可维护性
当你的查询易于阅读时,它们也容易维护。你不想在写完查询三个月后回来,却要花几个小时来解读这个该死的东西是怎么工作的。
即使是好的查询也很可能需要随着时间的推移进行适应和演变。好的查询应该容易理解、调试,并能扩展以供进一步使用。
公共表表达式使你可以轻松地将逻辑拆分为小块。这提高了查询的可维护性,因为你可以隔离逻辑的特定组件。例如,你可能有一个 500 行的查询,但由于你组织得很好,你知道实现一个新功能只需编辑你 CTE 的特定部分,而这仅涉及 20 行代码。
4: 逻辑的可解释性
结构化、可读的逻辑往往比一堆意大利面条更容易解释。无论是文档、项目交接还是好奇的业务用户挑剔你的实现:如果你的逻辑做了任何重要的事情,你总会需要解释它。
公共表表达式使你可以轻松地将逻辑拆分为小而易于解释的部分。当你的利益相关者理解这些小部分时,你就非常接近赢得他们对整体大局的理解。
5: 性能和可扩展性
从技术上讲,公共表表达式的性能并不比一系列内联子查询更好。我们之前的两个示例在性能上应该是相等的。
但在性能的战场上,我们并不讲究公平。这将使你在数据工程师中脱颖而出:你是否知道如何在你的 favor 中不公平地倾斜天平?公共表表达式可以帮助显而易见地识别在许多工作流中实现的公共查询组件。这为自动化或 ETL 优化的效率打开了大门,允许一次执行该查询,并在多个地方重用其结果集。
你还会发现,在采用公共表表达式后,你会有一个“库”来存储查询组件。你上周写的一部分查询可能与今天你正在处理的新业务用例相关。当你的查询使用公共表表达式组织和结构化得很好时,很容易从你已经解决的问题中提取相关的逻辑片段,并将其插入到需要解决的新问题中。
接下来是什么?
使 SQL 查询正常工作只是开始。设置它以在其生命周期内提供最大价值(和最小头痛)才是关键所在。
回顾一下你以前的一些查询。它们易于理解吗?易于解释和维护吗?如果答案是否定的,那么不妨尝试一下我们在这里讨论的内容。
将你的逻辑组织成一个公共表表达式。将其拆分成更易于管理的逻辑片段。将你的更改给同事看看,看看他们的反应。取用对你有效的部分并加以运用!
总结
本文到此结束。既然你读到了这里,希望你觉得这篇文章有用且有趣!祝你在编写高质量 SQL 时好运。
如果你想要更多内容,请查看我的YouTube 频道!给我留言,告诉我你想探索哪些其他数据工程主题。
大型语言模型中的上下文学习方法
简单而强大的技术,使大型语言模型在推理时能够学习新任务
·
关注 发表在 Towards Data Science ·17 min read·2023 年 7 月 1 日
--
介绍
语言建模(LM)旨在对词序列的生成概率进行建模,以预测未来(或缺失)标记的概率。近年来,语言模型在自然语言处理(NLP)领域带来了革命性变化。现在已经广泛认识到,增加语言模型的规模(例如训练计算、模型参数等)可以在一系列下游 NLP 任务中带来更好的性能和样本效率。调查论文“大型语言模型调查” [1] 涵盖了大型语言模型几乎所有的方面。该论文提供了对 LLMs 文献的最新回顾,详细介绍了预训练方法、指令调优技术及最新的 RLHF 方法的进一步对齐训练。指令调优和对齐调优方法用于根据具体目标调整 LLMs。
在预训练或适应性调优之后,使用 LLMs 的主要方法是设计合适的提示策略来解决各种任务。 一种典型的提示方法,也称为上下文学习(ICL),以自然语言文本的形式制定任务描述和/或示例。
上下文学习
LLMs 展示了一种上下文学习(ICL)能力,即从上下文中的几个示例中学习。许多研究表明,LLMs 可以通过 ICL 执行一系列复杂任务,例如解决数学推理问题。
上下文学习的关键思想是通过类比进行学习。下图展示了语言模型如何通过 ICL 进行决策的示例。首先,ICL 需要几个示例来形成演示上下文。这些示例通常以自然语言模板的形式编写。然后,ICL 将查询问题和一段演示上下文拼接在一起,形成提示,接着将其输入语言模型进行预测 [2]。
与需要使用反向梯度更新模型参数的监督学习不同,ICL 不进行参数更新,而是直接对预训练的语言模型进行预测。模型期望从示例中学习隐藏的模式,从而做出正确的预测。
什么使得 ICL 具有吸引力?
-
用自然语言编写的示例提供了一个可解释的接口,与 LLMs 进行交流。这个范式使得通过更改示例和模板将人类知识融入 LLMs 变得更加容易。
-
这类似于人类通过类比进行的决策过程。
-
与监督训练相比,ICL 是一种无需训练的学习框架。这不仅大大降低了将模型调整到新任务的计算成本,还使语言模型即服务成为可能,并且可以轻松应用于大规模的真实任务。
但这如何运作呢?
经过预训练,LLM 能够展示出引人注目的 ICL 能力(新兴能力),而无需更新[3]。虽然直观上合理,但 ICL 的工作机制仍不清楚,且少有研究对这两个问题提供了初步解释。
预训练如何影响 ICL 能力?
研究人员建议,当预训练模型达到大规模预训练步骤或模型参数时,它会获得一些新兴的 ICL 能力[3]。一些研究还表明,ICL 能力随着 LLM 参数从 1 亿增加到 1750 亿而增长。研究表明,训练任务的设计是影响 LLM ICL 能力的重要因素。除了训练任务,最近的研究还探讨了 ICL 与预训练语料库之间的关系。研究显示,ICL 的表现更多依赖于预训练语料库的来源,而非规模。
LLM 在推理过程中如何执行 ICL?
在论文“为什么 GPT 能在上下文中学习?”[4]中,研究人员发现了 Transformer 注意力和梯度下降之间的双重形式,并进一步提出将 ICL 理解为隐式微调。他们比较了基于 GPT 的 ICL 和真实任务上的显式微调,发现 ICL 在多个方面表现得类似于微调。在这个框架下,ICL 过程可以解释为:通过前向计算,LLM 生成相对于演示的元梯度,并通过注意力机制隐式执行梯度下降。
斯坦福研究的另一个视角[5]解释了‘上下文学习作为隐式贝叶斯推断’。作者提供了一个框架,其中语言模型通过使用提示来“定位”其在预训练过程中学到的相关概念以完成任务。从理论上讲,我们可以将其视为在提示条件下的潜在概念的贝叶斯推断,这一能力来自于预训练数据中的结构(长期一致性)。
尽管已有一些答案,这项研究仍在不断发展,以更好地理解其机制和潜在原因。
上下文学习方法
现在让我们探讨一些流行的 ICL 方法。
-
思维链(COT)
-
自一致性 COT
-
思维树
思维链(COT)
观察发现,标准提示技术(也称为一般输入输出提示)在复杂推理任务(如算术推理、常识推理和符号推理)上表现不佳。思维链是一种改进的提示策略,用于提升 LLM 在涉及推理的复杂案例中的表现[6]。与 ICL 中仅使用输入输出对构建提示不同,思维链将可以导致最终输出的中间推理步骤纳入提示中。如下例所示。
上图显示了一个模型生成思维链来解决数学词汇问题的示例,否则它会得到错误的答案。在左侧的 ICL 中,模型提供了数学推理问题的示例或演示及直接答案。但模型无法预测正确答案。
在右侧的 COT 中,模型呈现一个中间步骤以帮助得出给定示例/演示的答案。我们可以看到,当现在要求模型回答类似的推理问题时,它能够正确预测答案,从而证明了 COT 方法在此类用例中的有效性。
如果你看到,一般来说 COT 或 ICL 提供一些示例来演示用例,这称为 少样本(少量示例)。还有一篇论文 [7] 提出了有趣的提示 “让我们一步步思考……”,没有任何示例来演示用例,这称为 零样本(没有示例)。
在 零样本 CoT 中,LLM 首先通过 “让我们一步步思考” 生成推理步骤,然后通过 “因此,答案是” 推导出最终答案。他们发现,当模型规模超过某一大小时,这种策略显著提升了性能,但对小规模模型效果不佳,显示出显著的突现能力模式。
上图:GPT-3 的示例输入和输出,包括 (a) 标准少样本 (ICL)、(b) 少样本-CoT、(c) 标准零样本 (ICL) 和 (d) 我们的方法 (零样本-CoT)。
类似于少样本-CoT,零样本-CoT 促进了多步骤推理(蓝色文本),并在标准提示失败的情况下得出正确答案。与每个任务使用逐步推理示例的少样本-CoT 不同,零样本-CoT 不需要任何示例,只需在所有任务中使用相同的提示 “让我们一步步思考”(算术、符号、常识和其他逻辑推理任务)。
这项研究表明,通过添加一个简单的提示 “让我们一步步思考”,LLMs 是相当不错的零样本推理器,以促进在回答每个问题之前逐步思考。
让我们看看下面发生了什么:
尽管零样本-CoT 概念上很简单,但它使用了两次提示来提取推理和答案,如下图所示。
该过程包括两个步骤:首先是“推理提示提取”以从语言模型中提取完整的推理路径,然后使用第二步“答案提示提取”以从推理文本中提取正确格式的答案。
第一个提示 — 推理提取
在此步骤中,首先使用简单模板 “Q: [X]. A: [T]” 将输入问题 x 修改为提示 x’,其中 [X] 是 x 的输入槽,[T] 是用于提取思考链以回答问题 x 的手工触发句 t 的槽。例如,如果我们使用 “让我们一步一步地思考” 作为触发句,那么提示 x’ 将是 “Q: [X]. A: 让我们一步一步地思考。” 提示文本 x’ 然后输入到语言模型中,生成随后的句子 z。我们可以使用任何解码策略。
其他一些此类提示的例子:
让我们从逻辑上考虑一下这个问题。
让我们通过将问题拆分成步骤来解决这个问题。
让我们像侦探一样一步一步思考。
在我们深入回答之前。
第二步提示——答案提取
在第二步中,生成的句子 z 与提示的句子 x’ 一起用于从语言模型中提取最终答案。具体来说,将三个元素简单地连接在一起,如 “[X’] [Z] [A]”: [X’] 为第一步提示 x’,[Z] 为第一步生成的句子 z,[A] 为提取答案的触发句。这个步骤的提示是自我增强的,因为提示中包含了由相同语言模型生成的句子 z。在实验中,作者根据答案格式使用了略微不同的答案触发器。
例如,使用 “因此,在 A 到 E 之间,答案是” 来处理 多项选择题,以及 “因此,答案(阿拉伯数字)是” 来处理需要 数值答案 的数学问题。
论文 [7] 提出了有趣的想法、各种提示的表现等,请阅读以获取更多细节。
CoT 何时对 LLMs 有效?
仅对足够大的模型(例如,通常包含 10B 或更多参数)有正面效果,而对小模型没有。这一现象被称为大型语言模型的‘涌现能力’。如果某种能力在较小的模型中不存在,但在较大的模型中存在,则该能力被认为是涌现的 [3]。
-
这主要有效于改进需要逐步推理的任务,如算术推理、常识推理和符号推理。
-
对于那些不依赖于复杂推理的任务,它可能表现得比标准方法更差。有趣的是,CoT 提示所带来的性能提升似乎只有在标准提示效果较差时才会显著。
为什么 LLMs 可以进行 CoT 推理?
-
广泛 假设 这可以归因于对代码的训练,因为在其上训练的模型表现出强大的推理能力。直观地看,代码数据有良好的算法逻辑和编程流程,这可能有助于提高 LLMs 的推理表现。然而,这一假设仍然缺乏公开报告的消融实验证据(有无对代码的训练)。
-
CoT 提示与标准提示的主要区别在于在最终答案之前融入了推理路径。因此,一些研究人员探讨了推理路径中不同组成部分的效果。具体来说,一项最近的研究确定了 CoT 提示中的三个关键组成部分,即符号(例如,算术推理中的数值量)、模式(例如,算术推理中的方程式)和文本(即其余的非符号或模式的令牌)。研究表明,后两部分(即模式和文本)对模型性能至关重要,移除其中任何一个都会导致性能显著下降。
这是一个活跃的研究领域,关于这一点的深入讨论请阅读 [2]。还有一项有趣的研究 [8] 讨论了变压器模型中的上下文学习的可能原因。
自一致性 COT
在 COT 中,作者在 [9] 中提出了一种称为自一致性的解码策略,以取代在链式思维提示中使用的贪婪解码策略,这种策略显著提高了语言模型的推理性能。自一致性利用了这样一个直觉,即复杂的推理任务通常允许多个推理路径达到正确答案。问题需要更多的深思熟虑和分析时,可以恢复答案的推理路径的多样性就越大。
首先用链式思维提示语言模型,然后作者提出了“采样和边际化”解码过程,而不是贪婪地解码最优推理路径。
下图通过一个例子说明了自一致性方法。
首先从语言模型的解码器中生成一组多样化的推理路径;每条推理路径可能会导致不同的最终答案,因此通过对采样的推理路径进行边际化来确定最一致的答案。换句话说,通过对模型的解码器中的答案进行多数投票,我们可以在最终答案集中得到最“一致”的答案。
这种方法类似于人类经验,如果多种不同的思维方式得到相同的答案,则对最终答案的正确性信心更大。与其他解码方法相比,自一致性避免了贪婪解码中的重复性和局部最优性,同时减轻了单次采样生成的随机性。
大量实证评估显示,自一致性显著提高了链式思维提示在多个流行的算术和常识推理基准上的表现,包括 GSM8K(+17.9%)、SVAMP(+11.0%)、AQuA(+12.2%)、StrategyQA(+6.4%)和 ARC-challenge(+3.9%)。
自一致性的一个限制是它带来了更高的计算成本。实际上,人们可以尝试少量路径(例如 5 或 10)作为起点,以实现大多数收益,同时不产生过多成本,因为在大多数情况下,性能会迅速饱和。
思维树
[10] 的作者提出了“思维树”(ToT),它在“链式思维”方法上进行了概括,以提示语言模型,并允许在作为解决问题的中间步骤的连贯文本单元(“思维”)上进行探索。ToT 允许语言模型通过考虑多个不同的推理路径并自我评估选择来进行深思熟虑的决策,同时在必要时前瞻或回溯以做出全局选择。结果/实验表明,ToT 在需要非平凡规划或搜索的三项新任务(24 点游戏、创意写作和迷你填字游戏)上显著提升了语言模型的解决问题能力。
思维树(ToT)允许语言模型(LMs)在思维上探索多个推理路径(见上图)。ToT 将任何问题框架化为对树的搜索,其中每个节点是一个状态 s = [x, z1···i],代表具有输入 x 和到目前为止的思维序列 zi 的部分解决方案。ToT 做了 4 件事:思维分解、思维生成器、状态评估器和搜索算法。
1. 思维分解: 将中间过程分解为思维步骤:
虽然 CoT 在没有明确分解的情况下连贯地采样思维,ToT 利用问题特性设计和分解中间思维步骤。如表 1所示,依赖于不同的问题,思维可以是几个单词(填字游戏)、一行方程式(24 点游戏)或一整段写作计划(创意写作)。这就像你将问题分解成几个任务。每个任务是我们讨论的步骤 Zn。请注意,这部分仅涉及将问题分解为任务。就像规划一样,我们在这部分并不实际进行任何思维。
2. 思维生成: 在我们为每一步定义任务后,实际生成思维。我们尝试生成 k 个思维作为给定步骤 Zn 的候选项。生成思维有两种方式:采样和提出。
a. 从 CoT 提示中抽取 i.i.d. 思维。我们独立重复生成过程 k 次。当思维空间丰富时(例如,每个思维是一个段落),i.i.d. 样本能带来多样性。
在上图中,展示了在随机选择的创造性写作任务中的一步深思熟虑的搜索。给定输入,LM 采样了 5 个不同的计划,然后投票 5 次决定哪个计划最佳。多数选择被用来随后用相同的样本-投票程序写出输出段落。
b. 使用“提出提示”顺序提出思维。当思维空间更受限时(例如,每个思维只是一个词或一行),在同一上下文中提出不同的思维可以避免重复。在这种情况下,我们在一次推理中生成 k 个思维。因此,这些 k 个思维可能并不独立。
3. 评估状态: 在这一部分,我们定义一个状态评估函数:v(s)。为了扩展树,我们使用这个函数找到好的路径,就像在棋类编程中一样。我们评估给定的树路径s=[x, z1…i]。有两种方法来定义评估函数:
-
独立评估每个状态:每个状态‘s’(或路径)将被独立评估。[示例:24 点游戏]
-
跨状态投票:每个状态‘s’ 将在所有状态集合 S 中进行评估。就像你在自我一致性 COT 中比较 S 中的状态一样。[示例:创造性写作任务]
24 点游戏示例:
24 点游戏是一种数学推理挑战,其目标是使用 4 个数字和基本的算术运算(+-*/)得到 24。例如,给定输入“4 9 10 13”,一种解决方案可能是“(10–4) * (13–9) = 24”。
‘24 点游戏’ ToT 分解。LM 被提示进行 (a) 思维生成和 (b) 评估。
为了将‘24 点游戏’框架转入 ToT,我们将思维分解为 3 个步骤,每个步骤是一个中间方程。如上图(a)所示,在每个树节点,我们提取“左侧”数字,并提示 LM 提出一些可能的下一步。所有 3 个思维步骤使用相同的“提出提示”,尽管它只有一个包含 4 个输入数字的示例。我们在 ToT 中执行广度优先搜索(BFS),在每一步我们保留最佳的 b = 5 个候选项。为了在 ToT 中执行深思熟虑的 BFS,如图(b)所示,我们提示 LM 评估每个思维候选项为“确定/可能/不可能”,以判断是否能达到 24。目的是推广可以在少量前瞻试验中判定的正确部分解决方案,并根据“过大/过小”的常识消除不可能的部分解决方案,保留其余的“可能”。我们对每个思维进行 3 次采样。
4. 搜索算法:我们尝试扩展树。对于每个叶子节点,我们使用状态评估函数对其进行评估。选择哪个叶子节点进行评估时,我们使用搜索算法。它可以是广度优先搜索或深度优先搜索。根据树的结构,可以插入不同的搜索算法。
从概念上讲,ToT 作为一种通用问题解决方法具有若干优点:
-
通用性:IO、CoT、CoT-SC 和自我修正可以视为 ToT 的特例(即有限深度和广度的树)。
-
模块化:基础语言模型以及思维分解、生成、评估和搜索过程都可以独立变化。
-
适应性:可以适应不同的问题属性、语言模型能力和资源限制。
-
便利性:无需额外培训,只需一个预训练的语言模型即可。
ToT 框架使语言模型能够更自主和智能地做出决策和解决问题。
局限性:ToT 需要比采样方法更多的资源(例如模型 API 成本)以提高任务表现,但 ToT 的模块化灵活性允许用户自定义这种性能-成本权衡,并且持续的开源努力应该能在不久的将来降低这些成本。
自动提示技术
提示工程是一门经验科学,提示工程方法的效果在模型之间可能差异很大,因此需要大量实验和启发式方法。我们能否自动化这种提示工程过程? 这是一个活跃的研究领域,以下部分讨论了一些自动提示设计方法的尝试。
自动提示增强与选择 COT
在题为“基于标记数据的链式思维自动提示增强与选择”的论文中[11]。大多数 CoT 研究依赖于精心设计的人类标注的理性链来提示语言模型,这在实际应用中提出了挑战,因为标记的训练数据可用,但没有人类标注的理性链。为了自动构建链式思维提示,作者建议了增强-修剪-选择的三步过程:
-
增强:使用少量示例或零示例 CoT 提示生成多个伪链式思维;
-
修剪:根据生成的答案是否与真实值匹配来修剪伪链。
-
选择:应用减少方差的策略梯度策略来学习选定示例的概率分布,同时将示例的概率分布视为策略,将验证集的准确性视为奖励。
Auto-CoT:自动链式思维提示
在“大规模语言模型中的自动化链式思维提示”[12]中,作者提出了 Auto-CoT 范式,以自动构建带有问题和推理链的示例。在这一技术中,作者采用了聚类技术来抽样问题,然后生成链。作者观察到,LLM 往往会犯某些类型的错误。一种错误可能在嵌入空间中类似,因此被分组在一起。通过仅从频繁错误簇中抽取一个或几个样本,我们可以防止过多错误类型的错误示例,并收集多样的例子。
Auto-CoT包括以下主要阶段:
-
问题聚类:对给定的问题集 Q 进行聚类分析。首先通过 Sentence-BERT 计算 Q 中每个问题的向量表示。将上下文化的向量平均化以形成固定大小的问题表示。然后,使用 k-means 聚类算法处理问题表示,生成 k 个问题簇。
-
示例选择:从每个簇中选择一组具有代表性的问题;即从一个簇中选择一个示例。每个簇中的样本按距离簇中心的远近排序,距离中心较近的样本优先选择。
-
推理生成:使用零-shot CoT 为选定的问题生成推理链,并构建少-shot 提示以进行推理。
LLM 在 CoT 提示下展示了推理能力。Manual-CoT 的优越性能依赖于手工制作示例。为了消除这种手工设计,提出的 Auto-CoT 自动构建示例。它抽样具有多样性的问题并生成推理链以构建示例。对推理数据集的实验结果表明,在 GPT-3 上,Auto-CoT 的表现始终与需要手工设计示例的 CoT 范式相匹配或超越。
结论
上下文学习或提示有助于我们与 LLM 沟通,以引导其行为实现期望的结果。这是一种提取信息的有吸引力的方法,因为你不需要大量的离线训练集,不需要离线访问模型,并且即使对于非工程师也感觉直观。提示工程旨在利用提示作为为实际应用构建可靠功能的方法。这是一门经验科学,提示工程方法的效果在模型之间可能差异很大,因此需要大量实验和启发式方法。提示需要大量人力来创建和适应新的数据集。注释过程并不简单,因为人类不仅需要选择问题,还需要仔细设计每个问题的推理步骤,因此有必要对提示技术进行自动化。
参考文献
[1] 大型语言模型调查,arxiv.org/pdf/2303.18223.pdf
[2] 上下文学习调查,arxiv.org/pdf/2301.00234.pdf
[3] 大型语言模型的突现能力,arxiv.org/pdf/2206.07682.pdf
[4] 为什么 GPT 可以进行上下文学习?语言模型隐式地执行梯度下降作为元优化器,arxiv.org/pdf/2212.10559.pdf
[5] 将上下文学习解释为隐式贝叶斯推理,ai.stanford.edu/blog/understanding-incontext/
[6] 链式思维提示激发大型语言模型中的推理,arxiv.org/pdf/2201.11903.pdf
[7] 大型语言模型是零样本推理者,arxiv.org/pdf/2205.11916.pdf
[8] 上下文学习与归纳头。Transformer 电路,2022。transformer-circuits.pub/2022/in-context-learning-and-induction-heads/index.html
[9] 自洽性提升了 LLM 中的链式思维推理,arxiv.org/pdf/2203.11171.pdf
[10] 思维树,arxiv.org/pdf/2305.10601.pdf
[11] 自动提示增强与从标注数据中链式思维的选择 arxiv.org/pdf/2302.12822.pdf
[12] 大型语言模型中的自动链式思维提示,arxiv.org/pdf/2210.03493.pdf
[13] 大型语言模型可以自我提升,www.arxiv-vanity.com/papers/2210.11610/
《使用 Devtools 创建和发布 R 数据包的深度指南》
详细讲述了我开发的“Richmondway” R 数据包的步骤,其中包含 Roy Kent 的咒骂词统计
·
关注 发布于 Towards Data Science · 9 分钟阅读 · 2023 年 10 月 19 日
--
图片由 Erda Estremera 提供,来源于 Unsplash
当我被邀请在 2023 年 Posit 会议上就动画和互动讲故事进行演讲时,我花了几个月时间考虑完美的数据集。似乎每个有趣的数据集都已经用尽了,我没有灵感用一个平淡无奇的数据集来做演讲。然后有一天,在看“泰德·拉索”这部美国体育喜剧剧集时,罗伊·肯特巧妙地插入的脏话引发了我的灵感。我重新观看了这部剧(顺便说一下,是以 2 倍速观看的),并统计了罗伊使用或示意“F**k”这个词的次数。那就是我的数据集!在这篇文章中,我将带你逐步了解如何将这个数据集转变为一个 R 数据包,使你也能轻松创建一个。
欢迎来到[Richmondway](https://github.com/deepshamenghani/richmondway)
的制作过程,这是我第一个 R 数据包,允许你下载数据,逐集、逐季深入探讨罗伊·肯特的词汇。最终,回答一个从未有人问过的问题——罗伊·肯特在哪一季说“F**k”最多?
来源:Posit Conference 2023 presentation 由 Deepsha Menghani 提供
我为什么要创建一个包?
-
我一直渴望学习如何创建 R 包。从一个简单的数据包开始似乎是一个很好的初步尝试。
-
嵌入数据以进行函数测试是至关重要的。它使用户熟悉包的功能——这是我在未来处理任何复杂包时所需要的步骤。
-
这个数据集太有趣了,不想自己独享。打包它确保了会后大家都能轻松访问。
所以,无论你是对 R 数据包的创建感到好奇,还是你是“泰德·拉索”迷,泡杯茶,咱们开始吧!
我用来创建 R 数据包的详细步骤
第 0 步:数据集和包名称
这是我的数据集“richmondway”的快照。它有 34 行,对应于每一集,还有 16 列,包含各种值,这些值在我们将要创建的包中进行了描述。
给你的包命名就像给宠物命名一样——非常特别。虽然你会希望选择一个易于记忆的名字,但也要确保它简单,特别是如果你打算公开发布的话。我给我的包命名为Richmondway
——这是对“泰德·拉索”中罗伊·肯特曾经效力的足球俱乐部 AFC Richmond 的致敬。而且,因为它以“R”开头,这也是一种幸运的巧合。我还希望这个名字能够清晰地指示包的内容。
第 1 步:安装工具包
安装这些 R 包:devtools
、usethis
和roxygen2
。它们使得构建和记录你的新包变得非常简单。
install.packages(c("devtools", "usethis", "roxygen2"))
第 2 步:将新包创建为项目
你可以使用devtools
创建包,有两种方法。Devtools 处理了许多初始包结构设置所需的工作。
方法 1:直接从 RStudio 控制台
使用 devtools 创建 R 包的项目录制
方法 2:使用
usethis
包
使用usethis::create_package()
命令,你可以通过提供要创建包目录的路径来直接创建一个新包。在本文的其余部分,我将继续展示其他usethis
命令,这些命令使得完成大量包创建和文档步骤变得更简单和更快。
usethis::create_package("path/richmondway")
你刚刚创建了一个包含 R 包基本需求的文件夹。如果你窥视其中,会发现一些神秘的文件。不用担心,我们将逐一了解它们。以下是你在项目中会看到的一些文件。
项目的初始主目录
第 3 步:添加数据集
我将数据集保存在本地环境中,命名为“richmondway”。运行以下命令将在包的根目录下添加一个‘/data’
目录,并将一个“.rda”
文件放入其中。
usethis::use_data(richmondway)
“data”文件夹中的一个名为“richmondway.rda”的单一文件
第 4 步:创建数据字典data.R
:
这是描述你的数据集的地方。相信我,描述越好,其他人越容易发掘其潜力。这也会在后续步骤中纳入你的文档。你可以使用以下命令创建此文件,然后稍后更新所有所需的详细信息。
usethis::use_r("data")
此命令将创建一个data.R
文件在R
文件夹内。更新该文件的内容以包含数据集中的格式和列。虽然我的数据集中有更多列,但在这个例子中,我只展示了三列。你应尽可能清楚地添加所有列的描述,因为它将出现在数据集包文档中。下面的格式在data.R
文件内用于创建描述。
#' Data to showcase f**k count
#'
#' A dataset containing the number of times the word f**k was used in Ted Lasso by Roy Kent.
#'
#' @format A data frame with 34 rows and 16 columns.
#' \describe{
#' \item{Character}{Single value - Roy Kent}
#' \item{Episode_order}{The order of episodes from the 1st to the last}
#' \item{Season}{The season 1,2 or 3 associated with the count}
#' }
#' @source Created by Deepsha Menghani by watching the show and counting the number of F**ks used in sentences and as gestures
#'
#' @examples
#' data(richmondway)
"richmondway"
让我们将这个文件拆解为它的组成部分:
描述注释:
#' Data to showcase f**k count
#'
#' A dataset containing the number of times the word f**k was used in Ted Lasso by Roy Kent.
这是数据集的简短标题和描述。以#'
开头的注释用于以特殊方式注解 R 对象,这些对象将被roxygen2
包识别,该包在 R 中用于生成文档。
格式注释:
#' @format A data frame with 34 rows and 16 columns.
这指定了数据的格式。在这种情况下,数据集是一个包含 34 行和 16 列的数据框。
变量描述:
#' \describe{
#' \item{Character}{Single value - Roy Kent}
#' \item{Episode_order}{The order of episodes from the 1st to the last}
#' \item{Season}{The season 1,2 or 3 associated with the count}
#' }
这一部分详细描述了数据框中的一些主要变量/列。\describe
块用于列出并描述每个由\item
标签表示的变量,包括变量的名称及其描述。
来源注释:
#' @source Created by Deepsha Menghani by watching the show and counting the number of F**ks used in sentences and as gestures
这提供了关于数据的来源或出处的信息。重要的是要注明创作者并提供数据收集的背景。
示例评论:
#' @examples #' data(richmondway)
这提供了一个示例,说明用户如何访问和使用数据集。在这种情况下,它只是演示了如何在安装你的包后将数据加载到 R 中。
数据名称:
"richmondway"
这是数据集的名称。它用引号括起来,因为这表明该文档与包中同名的数据集相关联。
当用户在 R 中安装和加载你的包后,输入 ?richmondway
,他们会看到以结构化格式呈现的文档,帮助他们了解数据集的内容、结构以及如何使用它。
步骤 5:更新 "DESCRIPTION
" 文件
描述文件是包的更高层次的文档。查看你的包主文件夹中的 DESCRIPTION
文件,它应该预先填充了说明,需要更新为正确的描述。我在描述文件中更新的字段如下,其余保持默认。
Package: richmondway
Title: A dataset containing the number of times the word f**k was used in Ted Lasso by Roy Kent
Authors@R: person("Deepsha", "Menghani", email = "abc@gmail.com", role = c("aut", "cre"))
Description: Downloads the dataset containing the number of times the word f**k was used in Ted Lasso by Roy Kent.
License: file LICENSE
步骤 6:创建“LICENSE”文件
这个文件类似于你包的简历。请注意,在我的 DESCRIPTION 文件中,我提到一个叫 LICENSE
的文件。这个文件还不存在,因此我们现在将创建它。它会指向一个存储许可证信息的文件。许可证信息告知用户如何使用通过此包提供的数据,即使用权。在我的案例中,我使用了 CC0 许可证,并使用下面的命令将标准描述添加到 LICENSE
文件中。
license_text <- 'CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
The person who associated a work with this deed has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law.
You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.
For more information, please see
<http://creativecommons.org/publicdomain/zero/1.0/>'
writeLines(license_text, con = "packagepath/LICENSE")
新创建的 LICENSE 文件在项目主目录中突出显示
步骤 7:加载文档
现在所有文件已创建并更新,我们使用下面的便捷命令加载文档。此命令将使用 data.R
文件创建文档,该文件中添加了数据的描述。文档被放置在一个新创建的文件夹 man
中,代表“manual”。
devtools::document()
新创建的 “man” 文件夹在项目主目录中突出显示
一旦运行文档命令,你可以使用帮助命令 ?richmondway
,它应打开包文档。确保文档清晰,data.R
文件中的必要细节按预期显示。
步骤 8:检查
你现在可以通过运行下面的命令来测试一切是否顺利运行并正确创建。该命令执行广泛的检查,以确保你的包的一致性和有效性。
devtools::check()
devtools::check()
的输出会给出 NOTES、WARNINGS 和 ERRORS,每个都需要不同程度的关注:
错误:必须立即修复,因为它们表明存在重大问题。
警告:应予以解决,以确保功能性和 CRAN 合规性。
注意事项:提供有用的建议和提示,有时需要在 CRAN 提交时处理。
步骤 9:在本地安装包并测试
以下来自 devtools
的命令可用于在本地安装包。然后,使用您在 data.R
文件中分享的相同方法来测试数据访问。
devtools::install() # Install the package locally
data(richmondway) # Access the data through the package
第 10 步。将您全新的包发布到 GitHub
现在我们需要初始化我们的 Git 仓库,并使用一些更方便的 usethis
命令将包推送到 GitHub。在运行这些命令之前,请确保您拥有一个 GitHub 账户,并且已为 GitHub 设置了 SSH 密钥或个人访问令牌(PAT)。
usethis::use_git() # Git integration
usethis::use_github() # Github integration
usethis::use_git() 是做什么的
-
初始化 Git: 此功能在您的项目中初始化一个新的 Git 仓库。
-
第一次提交:它使用项目的当前状态进行初始提交。
usethis::use_github() 是做什么的
-
GitHub 仓库创建: 此功能帮助创建一个新的 GitHub 仓库,并将您的本地 Git 仓库连接到远程 GitHub 仓库。
-
身份验证:帮助设置与 GitHub 的身份验证。它会检查您是否已通过 GitHub 进行身份验证。如果没有,它可能会提示您进行身份验证。
-
推送: 将您的本地 git 提交推送到远程 GitHub 仓库。
第 11 步:最后与世界分享您的包
现在您可以分享您的包仓库链接。任何人都可以直接从 GitHub 使用 devtools::install_github(“your_username/packagename”)
安装您的包并访问数据。
例如,可以使用以下命令访问我 GitHub 仓库包中的 richmondway
数据:
devtools::install_github("deepshamenghani/richmondway")
你做到了!
如果您已经到达这一点,恭喜您!您刚刚把一个有趣的狂欢观看时光变成了既有教育意义又令人愉快的经历。
随意玩弄这个有趣的数据集,并在您的分析和可视化中标记我。或者,您可以在 GitHub 上 fork [richmondway](https://github.com/deepshamenghani/richmondway)
并为 Roy Kent 词汇表做贡献。继续打包吧!
资源
-
R Packages 由 Hadley Wickham 和 Jennifer Bryan 编写
-
Roxygen2 文档
-
Usethis 包
-
Devtools 包
快乐编码!如果愿意,可以在 Linkedin 上找到我。
所有图片和屏幕录像,除非另有说明,均由作者提供。
将 Llama 2 的延迟和吞吐量性能提高多达 4 倍
Llama-2 13B 的现实世界基准
·
关注 发布在 Towards Data Science · 7 分钟阅读 · 2023 年 8 月 9 日
--
作者提供的图像 — 使用 Stable Diffusion 创建
介绍
在大型语言模型(LLMs)的领域,将这些先进系统集成到实际的企业应用中是一个迫切的需求。然而,生成式 AI 发展的速度如此之快,以至于大多数人无法跟上这些进展。
一种解决方案是使用如 OpenAI 提供的托管服务。这些托管服务提供了精简的解决方案,但对于那些无法访问这些服务或优先考虑安全和隐私等因素的人,另一个途径是:开源工具。
目前开源生成 AI 工具非常流行,许多公司争相推出其 AI 驱动的应用程序。在快速构建的过程中,公司们常常忘记,要真正从生成 AI 中获得价值,他们需要构建“生产”就绪的应用程序,而不仅仅是原型。
在这篇文章中,我想向你展示 Llama 2 使用两种不同推理方法的性能差异。第一种推理方法是通过 Fast API 服务的容器化 Llama 2 模型,这是一种在开发者中非常受欢迎的选择,用于将模型作为 REST API 端点进行服务。第二种方法是通过 文本生成推理 服务的相同容器化模型,这是 Hugging Face 开发的开源库,用于轻松部署 LLM。
我们正在查看的这两种方法都旨在适用于实际使用,例如在商业或应用程序中。但重要的是要认识到,它们的扩展方式不同。我们将深入比较这两种方法,看看它们各自的表现,并更好地理解差异。
支撑 OpenAI 和 Cohere 的 LLM 推理
你是否曾经好奇为什么 ChatGPT 这么快?
大型语言模型需要大量计算能力,由于其庞大的规模,它们往往需要多个 GPU。在处理大型 GPU 集群时,公司必须非常注意计算资源的使用情况。
像 OpenAI 这样的 LLM 提供商运行大型 GPU 集群来支撑其模型的推理。为了最大限度地提高 GPU 性能,他们使用像 Nvidia Triton 推理服务器 这样的工具来提高吞吐量并减少延迟。
插图灵感来源 — Triton 推理服务器架构
尽管 Triton 性能卓越且有许多优点,但对于开发者来说使用起来非常困难。许多 Hugging Face 上的较新模型在 Triton 上不受支持,添加对这些模型的支持的过程也并不简单。
这时,文本生成推理 (TGI) 就派上用场了。这个工具提供了与 Triton 相同的性能提升,但它更用户友好,并且与 Hugging Face 模型兼容性好。
LLM 推理优化
在我们深入基准测试之前,我想介绍一些现代推理服务器如 TGI 用于加速 LLM 的优化技术。
- 张量并行
LLM 通常太大而无法容纳在单个 GPU 上。通过一种称为 模型并行 的概念,可以将模型分割到多个 GPU 上。张量并行 是一种模型并行,它将模型分割成多个由不同 GPU 独立处理的分片。
插图灵感来自于 来源
简而言之,想象你在拼一个大拼图,但它太大了,你无法把所有的拼图片放在一张桌子上。所以,你决定和你的朋友一起工作。你把拼图分成几个部分,每个人同时处理自己的一部分。这样,你可以更快地完成拼图。
2. 连续批处理
当你向 LLM 发起 API 调用时,它会一次性处理并返回结果。如果你发起 5 个 API 调用,它会顺序处理每一个。这实际上意味着我们有一个批处理大小为 1,即每次只能处理 1 个请求。正如你所猜测的,这种设计并不理想,因为每个新请求必须等待前一个请求完成。
插图灵感来自于 静态批处理 — 需要等所有进程完成后才能处理更多请求
通过增加批处理大小,你可以并行处理更多请求。批处理大小为 4 时,你可以并行处理 5 个 API 调用中的 4 个。你必须等到这 4 个请求全部完成后,才能处理第 5 个请求。
插图灵感来自于 连续批处理 — 你可以立即处理新请求,而不必等待所有进程完成
连续批处理基于使用更大批处理大小的想法,并进一步通过立即处理新任务来提升效率。例如,假设你的 GPU 的批处理大小为 4,这意味着它可以并行处理 4 个请求。如果你发起 5 个请求,其中 4 个会被并行处理,而第一个完成的进程会立即处理第 5 个请求。
Llama 2 基准测试
现在我们对优化方案有了基本的理解,能够实现更快的 LLM 推理,让我们来看一下Llama-2 13B模型的一些实际基准。
我想测试这个模型的 2 个主要指标:
-
吞吐量(tokens/second)
-
延迟(完成一次完整推理所需的时间)
我想比较 Llama 推理在两种不同实例上的性能。一种实例通过 FastAPI 运行,而另一种通过 TGI 操作。两个设置都利用了 GPU 进行计算。
注意:这两个实例都没有量化模型的权重。
TGI 设置利用了两个 GPU(NVIDIA RTX A4000)的强大性能来实现并行,而 FastAPI 依赖于一个(NVIDIA A100),尽管更强大的 GPU。
直接比较这两种方法有点棘手,因为 FastAPI 不允许模型分布在两个 GPU 上。为了公平起见,我选择为 FastAPI 实例配备了更强大的 GPU。
对模型发出的每个 API 请求都使用相同的提示,生成的输出 token 限制设置为128 tokens。
吞吐量结果:
左:Fast API 与 TGI 吞吐量 | 右:平均吞吐量提升 — 作者插图
分析:
-
在这两种情况下,随着推理请求数量的增加,吞吐量都会下降。
-
批处理显著提高了 LLM 的吞吐量,这就是为什么 TGI 的吞吐量更好的原因。
-
尽管 Fast API 实例具有更多的 GPU 内存(VRAM)用于处理更大的批次请求,但它处理这个过程并不高效。
延迟结果:
左:Fast API 与 TGI 延迟 | 右:平均延迟性能提升 — 作者插图
分析:
-
张量并行使 TGI 的延迟减少了近 5 倍!
-
随着请求数量的增加,基于 Fast API 的实例的延迟超过 100 秒。
从结果中可以看出,与现成的 API 包装器相比,优化过的推理服务器具有很高的性能。作为最终测试,我想评估当生成的输出 token 限制增加到256 tokens时 TGI 的性能,与128 tokens相比。
吞吐量 TGI 128 tokens 与 256 tokens:
128 token 与 256 token TGI 吞吐量测试 — 作者插图
正如你所见,尽管生成的 token 数量翻倍,但吞吐量非常相似。需要注意的是,这张图表没有显示,在 300 个并发请求时,吞吐量降到大约 2 token/秒,同时生成 256 token 的输出。此时,延迟超过每个请求 100 秒,并且出现了多次请求超时。由于这些显著的性能限制,这种情况的结果被排除在图表之外。
延迟 TGI 128 tokens 与 256 tokens:
128 token 与 256 token TGI 延迟测试 — 作者插图
与吞吐量不同,生成更长文本序列时,延迟明显增加。增加更多的 GPU 可以帮助减少延迟,但会带来财务成本。
结论
我写这篇博客的目标是比较大规模 LLM 的实际性能(每秒数百个请求)。通常情况下,部署模型在像 Fast API 这样的 API 包装器后是很简单的,但对于 LLM 而言,你可能会错过相当多的性能。
即使使用现代推理服务器的优化技术,其性能也无法与像 ChatGPT 这样的托管服务相比。OpenAI 当然运行了几个大型 GPU 集群来为他们的模型提供推理支持,并结合了他们自己内部的优化技术。
然而,对于生成式 AI 的使用场景,企业可能需要采用推理服务器,因为它们在可扩展性和可靠性方面远超传统的模型部署技术。
感谢阅读!
平静。
分布式随机森林的推断
一种强大的非参数方法的置信区间
·
关注 发表在 Towards Data Science ·17 分钟阅读·2023 年 2 月 17 日
--
(分布式)随机森林的特性。本文:提供不确定性度量的能力。来源:作者。
在之前的文章中,我详细讨论了分布随机森林方法,这是一种可以非参数地估计多变量条件分布的随机森林类型算法。这意味着我们能够在给定一些协变量X的情况下,非参数地学习多变量响应Y的整个分布,而不仅仅是学习其条件期望。DRF 通过学习权重 w_i(x) 对于 i=1,…,n 训练点来完成这项工作,这些权重定义了分布,并且可以用于估计广泛的目标。
到目前为止,这种方法仅产生了分布的“点估计”(即* n* 权重 w_i(x) 的点估计)。虽然这足以预测响应的整个分布,但它并未提供考虑数据生成机制随机性的方法。也就是说,即使这个点估计在大样本量下(在一系列假设下)越来越接近真实值,但在有限样本量下其估计仍然存在不确定性。幸运的是,现在有一种(可证明的)方法来量化这种不确定性,如我在本文中阐述的。这基于我们关于arXiv的新论文。
本文的目标有两个:首先,我想讨论如何基于我们的论文将不确定性估计添加到 DRF 中。论文相当理论化,因此我从几个示例开始。接下来的部分快速浏览这些理论结果,以供感兴趣的人参考。然后我解释了如何利用这些结果获取广泛目标的(基于采样的)不确定性度量。其次,我讨论了[1]的 CoDiTE 和这一概念的一个特别有趣的例子,即条件见证函数。这个函数是一个复杂的对象,但正如我们将在下面看到的那样,我们可以轻松地使用 DRF 进行估计,并且基于本文中介绍的概念,甚至可以提供渐近置信带。一个如何应用的详细真实数据示例见这篇文章。
在整个过程中,我们假设有一个 d 变量的 i.i.d. 样本 Y_1, …, Y_n 和一个 p 变量的 i.i.d. 样本 X_1,…,X_n。目标是估计 Y|X=x 的条件分布。
我们将需要以下软件包和函数:
library(kernlab)
library(drf)
library(Matrix)
library(Hmisc)
source("CIdrf.R")
文件“CIdrf.R”中的函数如下所示。
以下所有图片,除非另有说明,均由作者提供。
示例
我们从一个简单的例子开始模拟,其中 d=1 和 p=2:
set.seed(2)
n<-2000
beta1<-1
beta2<--1.8
# Model Simulation
X<-mvrnorm(n = n, mu=c(0,0), Sigma=matrix(c(1,0.7,0.7,1), nrow=2,ncol=2))
u<-rnorm(n=n, sd = sqrt(exp(X[,1])))
Y<- matrix(beta1*X[,1] + beta2*X[,2] + u, ncol=1)
请注意,这只是一个异方差线性模型,其中误差项的方差依赖于 X_1 的值。当然,如果你知道 X 对 Y 的影响是线性的,你就不会使用 DRF 或任何随机森林,而是直接使用线性回归。但为了这个目的,知道真实情况是很方便的。由于 DRF 的工作是估计给定 X=x 的条件分布,我们现在固定 x 并估计给定 X=x 的条件期望和方差。
我们选择一个位于 X 分布中心的点,周围有大量观察数据。一般来说,当使用任何随机森林方法处理 X 观测数据的边界点时,应格外小心。
# Choose an x that is not too far out
x<-matrix(c(1,1),ncol=2)
# Choose alpha for CIs
alpha<-0.05
最后,我们拟合我们的 DRF 并获得权重 w_i(x):
## Fit the new DRF framework
drf_fit <- drfCI(X=X, Y=Y, min.node.size = 2, splitting.rule='FourierMMD', num.features=10, B=100)
## predict weights
DRF = predictdrf(drf_fit, x=x)
weights <- DRF$weights[1,]
如下所述,我们在这里构建的 DRF 对象不仅包含权重 w_i(x),还包含一个 B 权重样本,这些权重对应于从 w_i(x) 的分布中抽取的样本。我们可以使用这 B 次抽样来逼近我们想要估计的任何分布,如我现在在两个示例中所说明的。
示例 1:条件期望
首先,我们简单地做了大多数预测方法会做的事情:我们估计条件期望。使用我们的方法,我们还在其周围构建了一个置信区间。
# Estimate the conditional expectation at x:
condexpest<- sum(weights*Y)
# Use the distribution of weights, see below
distofcondexpest<-unlist(lapply(DRF$weightsb, function(wb) sum(wb[1,]*Y) ))
# Can either use the above directly to build confidence interval, or can use the normal approximation.
# We will use the latter
varest<-var(distofcondexpest-condexpest)
# build 95%-CI
lower<-condexpest - qnorm(1-alpha/2)*sqrt(varest)
upper<-condexpest + qnorm(1-alpha/2)*sqrt(varest)
c(lower, condexpest, upper)
(-1.00, -0.69 -0.37)
重要的是,虽然估计值有点偏差,但这个置信区间包含了真实值,真实值如下:
示例 2:条件方差
现在假设我们想要找到方差 Var(Y|X=x),而不是条件均值。这对于一个无法利用线性特性的非参数方法来说是一个相当具有挑战性的例子。实际情况如下:
使用 DRF,我们可以如下估计:
# Estimate the conditional expectation at x:
condvarest<- sum(weights*Y²) - condexpest²
distofcondvarest<-unlist(lapply(DRF$weightsb, function(wb) {
sum(wb[1,]*Y²) - sum(wb[1,]*Y)²
} ))
# Can either use the above directly to build confidence interval, or can use the normal approximation.
# We will use the latter
varest<-var(distofcondvarest-condvarest)
# build 95%-CI
lower<-condvarest - qnorm(1-alpha/2)*sqrt(varest)
upper<-condvarest + qnorm(1-alpha/2)*sqrt(varest)
c(lower, condvarest, upper)
(1.89, 2.65, 3.42)
因此,真实参数被包含在置信区间内,这正是我们所期望的,事实上,我们的估计值与真实值非常接近!
我们现在研究这些示例背后的理论,然后再来看一个因果分析中的第三个示例。
RKHS 中的渐近正态性
在本节和下一节中,我们简要关注论文中推导的理论结果。如上所述和文章中解释的,DRF 在测试点 x 上呈现分布预测。也就是说,我们获得一个估计值
条件分布的 Y 给定 X=x。这只是一个典型的经验度量方式,魔力在于权重 w_i(x) — 它们可以用来轻松地获得感兴趣量的估计量,甚至直接从分布中采样。
为了获得这个估计,DRF 实际上是在再生核希尔伯特空间(RKHS)中估计条件均值。RKHS 是通过核函数 k(y_1, y_2) 定义的。通过这个核,我们可以将每个观测 Y_i 映射到希尔伯特空间中,作为 k(Y_i, .)。利用这个极其强大的工具可以进行多种方法,例如核岭回归。关键点是,在某些条件下,任何分布都可以表示为这个 RKHS 的一个元素。事实证明,真正的条件分布可以表示为 RKHS 中的以下期望:
所以这只是另一种表达 Y 给定 X=x 的条件分布的方式。然后我们尝试用 DRF 来估计这个元素,如下所示:
我们再次使用了从 DRF 获得的权重,但这次用 k(Y_i,.) 来形成加权和,而不是上述的狄拉克测度。我们可以通过编写任意两个中的任何一个来在两个估计之间来回映射。这很重要,因为我们可以将条件分布估计写成 RKHS 中的加权均值!就像原始的随机森林在实数中估计均值(Y 给定 X=x 的条件期望)一样,DRF 在 RKHS 中估计均值。结果表明,我们还获得了条件分布的估计。
这对我们的故事很重要,因为在 RKHS 中,这个加权均值在某些方面表现得与 d 维的(加权)均值非常相似。也就是说,我们可以利用现有的工具来研究其一致性和渐近正态性。这是相当显著的,因为所有有趣的 RKHS 都是无限维的。第一篇 DRF 论文 已经确立了 RKHS 中估计器(1)的均衡性。我们的新论文现在进一步证明了,此外,
其中 sigma_n 是趋近于零的标准差,而Sigma_x 是一个替代协方差矩阵的算子(这与 d 维欧几里得空间中的表现非常相似)。
获得采样分布
好的,那么,我们在无限维空间中得到了一个渐近正态性结果,这到底意味着什么?首先,这意味着从 DRF 估计中得出的估计器如果“平滑”到足够程度,也将趋向于渐近正态。然而,这还不够有用,因为我们还需要有方差估计。这里我们论文中的进一步结果派上用场。
我们在这里省略了很多细节,但本质上我们可以使用以下子样本方案:不是仅仅拟合N棵树来构建我们的森林,而是构建B组L棵树(使得N=BL)。现在,对于每一组树或迷你森林,我们随机子样本约一半的数据点,然后仅使用此子样本拟合森林。我们称这个样本子集为S。对于每个抽取的S*,我们得到另一个在希尔伯特空间中表示的 DRF 估计量
仅使用S中的样本。请注意,与自助法一样,我们现在有两个随机性来源,即使忽略森林的随机性(理论上我们假设B足够大,以使森林的随机性可忽略不计)。一个来源是数据本身,另一个来源是我们在随机选择S时引入的人工随机性。关键是,给定数据的情况下,S的随机性在我们的控制之下——我们可以绘制任意数量的子集S。所以问题是,如果我们只考虑S的随机性并固定数据,那么我们的估计量(2)会发生什么?值得注意的是,我们可以证明
这仅仅意味着,如果我们固定数据的随机性,仅考虑来自S的随机性,那么估计量(2)减去估计量(1)将以相同的极限收敛到与原始估计量减去真实值相同的极限!这实际上是自助法理论的工作原理:我们已经证明了我们可以从中采样的东西,即
收敛到我们无法访问的东西,即
因此,为了对后者进行推断,我们可以使用前者!这实际上是人们在自助法理论中提出的标准论点,以证明为什么自助法可以用来近似采样分布!没错,即使是自助法,尽管人们经常在小样本中使用,它也只在大样本范围内才真正有意义(理论上)。
现在我们来使用这个。
这实际上是什么意思?
我们现在展示这在实践中的含义。接下来,我们定义两个从 CRAN 包drf的 drf 函数派生的新函数。
## Functions in CIdrf.R that is loaded above ##
drfCI <- function(X, Y, B, sampling = "binomial",...) {
### Function that uses DRF with subsampling to obtain confidence regions as
### as described in https://arxiv.org/pdf/2302.05761.pdf
### X: Matrix of predictors
### Y: Matrix of variables of interest
### B: Number of half-samples/mini-forests
n <- dim(X)[1]
# compute point estimator and DRF per halfsample S
# weightsb: B times n matrix of weights
DRFlist <- lapply(seq_len(B), function(b) {
# half-sample index
indexb <- if (sampling == "binomial") {
seq_len(n)[as.logical(rbinom(n, size = 1, prob = 0.5))]
} else {
sample(seq_len(n), floor(n / 2), replace = FALSE)
}
## Using refitting DRF on S
DRFb <-
drf(X = X[indexb, , drop = F], Y = Y[indexb, , drop = F],
ci.group.size = 1, ...)
return(list(DRF = DRFb, indices = indexb))
})
return(list(DRFlist = DRFlist, X = X, Y = Y) )
}
predictdrf<- function(DRF, x, ...) {
### Function to predict from DRF with Confidence Bands
### DRF: DRF object
### x: Testpoint
ntest <- nrow(x)
n <- nrow(DRF$Y)
## extract the weights w^S(x)
weightsb <- lapply(DRF$DRFlist, function(l) {
weightsbfinal <- Matrix(0, nrow = ntest, ncol = n , sparse = TRUE)
weightsbfinal[, l$indices] <- predict(l$DRF, x)$weights
return(weightsbfinal)
})
## obtain the overall weights w
weights<- Reduce("+", weightsb) / length(weightsb)
return(list(weights = weights, weightsb = weightsb ))
}
Witdrf<- function(DRF, x, groupingvar, alpha=0.05, ...){
### Function to calculate the conditional witness function with
### confidence bands from DRF
### DRF: DRF object
### x: Testpoint
if (is.null(dim(x)) ){
stop("x needs to have dim(x) > 0")
}
ntest <- nrow(x)
n <- nrow(DRF$Y)
coln<-colnames(DRF$Y)
## Collect w^S
weightsb <- lapply(DRF$DRFlist, function(l) {
weightsbfinal <- Matrix(0, nrow = ntest, ncol = n , sparse = TRUE)
weightsbfinal[, l$indices] <- predict(l$DRF, x)$weights
return(weightsbfinal)
})
## Obtain w
weightsall <- Reduce("+", weightsb) / length(weightsb)
#weightsall0<-weightsall[, DRF$Y[, groupingvar]==0, drop=F]
#weightsall1<-weightsall[,DRF$Y[, groupingvar]==1, drop=F]
# Get the weights of the respective classes (need to standardize by propensity!)
weightsall0<-weightsall*(DRF$Y[, groupingvar]==0)/sum(weightsall*(DRF$Y[, groupingvar]==0))
weightsall1<-weightsall*(DRF$Y[, groupingvar]==1)/sum(weightsall*(DRF$Y[, groupingvar]==1))
bandwidth_Y <- drf:::medianHeuristic(DRF$Y)
k_Y <- rbfdot(sigma = bandwidth_Y)
K<-kernelMatrix(k_Y, DRF$Y[,coln[coln!=groupingvar]], y = DRF$Y[,coln[coln!=groupingvar]])
nulldist <- sapply(weightsb, function(wb){
# iterate over class 1
wb0<-wb*(DRF$Y[, groupingvar]==0)/sum(wb*(DRF$Y[, groupingvar]==0))
wb1<-wb*(DRF$Y[, groupingvar]==1)/sum(wb*(DRF$Y[, groupingvar]==1))
diag( ( wb0-weightsall0 - (wb1-weightsall1) )%*%K%*%t( wb0-weightsall0 - (wb1-weightsall1) ) )
})
# Choose the right quantile
c<-quantile(nulldist, 1-alpha)
return(list(c=c, k_Y=k_Y, Y=DRF$Y[,coln[coln!=groupingvar]], nulldist=nulldist, weightsall0=weightsall0, weightsall1=weightsall1))
}
因此,通过我们的方法,我们不仅得到点估计形式的权重w_i(x),还得到B个权重的样本,每个样本表示从条件分布的估计量的分布中独立抽取的结果(这听起来比实际更令人困惑,请记住这些例子)。这仅仅意味着我们不仅有一个估计量,还有一个其分布的近似!
现在我转向一个更有趣的例子,这是我们仅能使用 DRF 做的(据我所知)。
因果分析示例:见证函数
假设我们有两组观测值,分别是组 W=1 和组 W=0,我们想要找出组别与变量 Y 之间的因果关系。在 这篇文章 的例子中,这两组分别为男性和女性,而 Y 为小时工资。此外,我们有混杂变量 X,我们假设它们影响 W 和 Y。我们在这里假设 X 确实包含所有相关的混杂变量。这是一个大假设。形式上,我们假设无混杂:
和重叠:
通常,人们会比较两个组之间的条件期望:
这是 x 处的条件平均处理效应(CATE)。这是一个自然的起点,但在最近的一篇论文 ([1]) 中,引入了 CoDiTE 作为这一思想的推广。CoDiTE 不仅仅关注期望值的差异,还建议查看其他量的差异。一个特别有趣的例子是条件见证函数:对于两个组,我们如上所述
因此,我们考虑在 RKHS 中两个条件分布的表示。除了作为条件分布的表示,这些量也是实值函数:对于 j=0,1,
给出这两个量之间差异的函数,
称为条件见证函数。
为什么这个函数很有趣?事实证明,这个函数展示了两个密度如何相互关系:对于函数值为负的 y,类 1 在 y 处的条件密度小于 0 的条件密度。类似地,如果函数在 y 处为正,这意味着 1 的密度在 y 处高于 0 的条件密度(其中“条件”始终指的是条件于 X=x)。至关重要的是,这可以不需要估计密度*,这很困难,尤其是对于多变量 Y。
最后,我们可以为估计的条件见证函数提供均匀置信带,通过使用上面的 B 样本。我在这里不详细说明,但这些基本上是我们上面使用的条件均值置信区间的类比。至关重要的是,这些置信带应在特定的 x 上对函数值 y 有效。
让我们用一个例子来说明:我们模拟以下数据生成过程:
即,X_1, X_2在(0,1)上独立均匀分布,W为 0 或 1,概率取决于X_2,Y是W和X_1的函数。这是一个非常困难的问题;不仅X影响属于类别 1 的概率(即倾向),它还改变了W对Y的处理效果。事实上,简单计算表明 CATE 给定为:
(1 - 0.2)X_1 - (0 - 0.2)X_1 = X_1。
与数据生成过程相对应的图形
set.seed(2)
n<-4000
p<-2
X<-matrix(runif(n*p), ncol=2)
W<-rbinom(n,size=1, prob= exp(-X[,2])/(1+exp(-X[,2])))
Y<-(W-0.2)*X[,1] + rnorm(n)
Y<-matrix(Y,ncol=1)
我们现在随机选择一个测试点x,并使用以下代码来估计证据函数及其置信区间:
x<-matrix(runif(1*p), ncol=2)
Yall<-cbind(Y,W)
## For the current version of the Witdrf function, we need to give
## colnames to Yall
colnames(Yall) <- c("Y", "W")
## Fit the new DRF framework
drf_fit <- drfCI(X=X, Y=Yall, min.node.size = 5, splitting.rule='FourierMMD', num.features=10, B=100)
Witobj<-Witdrf(drf_fit, x=x, groupingvar="W", alpha=0.05)
hatmun<-function(y,Witobj){
c<-Witobj$c
k_Y<-Witobj$k_Y
Y<-Witobj$Y
weightsall1<-Witobj$weightsall1
weightsall0<-Witobj$weightsall0
Ky=t(kernelMatrix(k_Y, Y , y = y))
#K1y <- t(kernelMatrix(k_Y, DRF$Y[DRF$Y[, groupingvar]==1,coln[coln!=groupingvar]], y = y))
#K0y <- t(kernelMatrix(k_Y, DRF$Y[DRF$Y[, groupingvar]==0,coln[coln!=groupingvar]], y = y))
out<-list()
out$val <- tcrossprod(Ky, weightsall1 ) - tcrossprod(Ky, weightsall0 )
out$upper<- out$val+sqrt(c)
out$lower<- out$val-sqrt(c)
return( out )
}
all<-hatmun(sort(Witobj$Y),Witobj)
plot(sort(Witobj$Y),all$val , type="l", col="darkblue", lwd=2, ylim=c(min(all$lower), max(all$upper)),
xlab="y", ylab="witness function")
lines(sort(Witobj$Y),all$upper , type="l", col="darkgreen", lwd=2 )
lines(sort(Witobj$Y),all$lower , type="l", col="darkgreen", lwd=2 )
abline(h=0)
我们可以从这个图中看到:
(1) 组 1 的条件密度在y值介于-3 和 0.3 之间时低于组 0 的密度。此外,这种差异随着y的增大而增大,直到约y = -1,之后密度差异开始再次减少,直到两者在约 0.3 时相同。
(2) 对称地,组 1 的密度在y值介于 0.3 到 3 之间时高于组 0 的密度,这种差异逐渐增大,直到在约y = 1.5时达到最大值。在此点之后,差异逐渐减少,直到在y = 3时几乎回到零。
(3) 两个密度之间的差异在 95%置信水平上具有统计学意义,可以从事实中看到,对于y大致在-1.5 到-0.5 以及 1 到 2 之间,渐近置信区间不包括零线。
让我们检查(1)和(2)对模拟的真实条件密度的适用情况。也就是说,我们大量模拟真实情况:
# Simulate truth for a large number of samples ntest
ntest<-10000
Xtest<-matrix(runif(ntest*p), ncol=2)
Y1<-(1-0.2)*Xtest[,1] + rnorm(ntest)
Y0<-(0-0.2)*Xtest[,1] + rnorm(ntest)
## Plot the test data without adjustment
plotdf = data.frame(Y=c(Y1,Y0), W=c(rep(1,ntest),rep(0,ntest) ))
plotdf$weight=1
plotdf$plotweight[plotdf$W==0] = plotdf$weight[plotdf$W==0]/sum(plotdf$weight[plotdf$W==0])
plotdf$plotweight[plotdf$W==1] = plotdf$weight[plotdf$W==1]/sum(plotdf$weight[plotdf$W==1])
plotdf$W <- factor(plotdf$W)
#plot pooled data
ggplot(plotdf, aes(Y)) +
geom_density(adjust=2.5, alpha = 0.3, show.legend=TRUE, aes(fill=W, weight=plotweight)) +
theme_light()+
scale_fill_discrete(name = "Group", labels = c('0', "1"))+
theme(legend.position = c(0.83, 0.66),
legend.text=element_text(size=18),
legend.title=element_text(size=20),
legend.background = element_rect(fill=alpha('white', 0.5)),
axis.text.x = element_text(size=14),
axis.text.y = element_text(size=14),
axis.title.x = element_text(size=19),
axis.title.y = element_text(size=19))+
labs(x='y')
这导致:
视觉上比较有点困难,但我们看到这两个密度与上面的证据函数预测的结果非常接近。特别是,我们看到在 0.3 左右密度几乎相同,密度差异在大约-1 和 1.5 之间达到最大。因此,实际密度中可以看到点(1)和(2)!
此外,为了将(3)置于背景中,论文中的重复模拟展示了在没有可见效果时估计的证据函数的趋向:
在与此处描述的类似设置中模拟了 1000 个证据函数。蓝色的是 1000 个估计的证据函数,而灰色的显示了相应的置信区间。摘自我们在 arXiv 上的论文。此示例中没有效果,99%的置信区间不包含零线。
在这篇文章中给出了因果推断中的一个真实数据示例。
结论
在这篇文章中,我讨论了适用于分布随机森林的新推断工具。我还查看了这些新能力的重要应用;用均匀置信区间估计条件见证函数。
不过,我也想提供一些警告:
-
结果仅对给定的测试点x有效。
-
结果仅在渐近意义上有效。
-
当前的代码比它应该有的慢得多。
实际上,第一点并不是那么糟糕,在模拟中,渐近正态性通常在一范围内的x上也成立。只要小心样本边界附近的测试点! 直观地说,DRF(以及所有其他最近邻方法)需要测试点x周围的许多样本点来估计x的响应。因此,如果你训练集中的协变量X是标准正态分布,大多数点在-2 和 2 之间,那么预测[-1,1]中的x应该没有问题。但如果你的x达到了-2 或 2,性能会迅速恶化。
随机森林(以及一般的最近邻方法)在预测训练集中只有少量邻居的点时表现不佳,例如X的支持边界上的点。
第二点也相当重要。渐近结果在当代研究中已经有些过时,研究者们更倾向于有限样本结果,这些结果需要诸如“次高斯性”的假设。我个人觉得这有点可笑,渐近结果在像这样的复杂设置中提供了极其强大的近似。实际上,对于许多目标来说,这种近似在超过 1000 或 2000 个数据点时相当准确(也许你的条件均值/分位数的覆盖率是 92%而不是 95%)。然而,我们引入的见证函数是一个复杂的对象,因此你需要更多的数据点来估计其不确定性区间,这样效果会更好!
最后第三点只是我们的一个缺陷:虽然 DRF 本身是用 C 语言高效编写的,但用S估计不确定性目前完全基于 R 语言。修复这一点将极大地加快代码速度。我们希望将来能够解决这个问题。
引用
[1] Junhyung Park, Uri Shalit, Bernhard Schölkopf 和 Krikamol Muandet. “使用核条件均值嵌入和 U 统计量回归的条件分布治疗效果。” 见《第 38 届国际机器学习大会(ICML)论文集》,第 139 卷,第 8401–8412 页。PMLR,2021 年 7 月。
Kubernetes 的无限可扩展存储
一项破坏性实验,确保我们的数据能够恢复
·
跟进 发表在Towards Data Science ·6 分钟阅读·2023 年 5 月 7 日
--
有时候,你只需要一个可靠的存储解决方案。享有云服务提供商存储类的奢侈并非总是可能的,有时你必须自己全权管理。这是我在医疗保健领域的客户的挑战。
在本文中,您将了解为什么以及如何安装 Rook Ceph,为您的 Kubernetes 集群提供易于使用的复制存储类。
然后我们将部署一个文件共享应用程序,摧毁部署它的节点,然后看看会发生什么。Ceph 是否会让我们的文件再次可访问?
看向地平线的容器。Kelly 摄于 Pexels。
选择存储解决方案
存储在 Kubernetes 中一直是一个挑战,因为它没有原生提供冗余和分布式存储解决方案。在原生 Kubernetes 中,你只能附加一个 hostPath 卷以实现持久存储。
我的客户拥有自己的本地基础设施,并希望确保如果其中一台服务器出现故障,其数据不会丢失。大多数应用程序是单体应用,并且没有原生的数据复制机制。
所以我必须从各种存储解决方案中进行选择。我的客户不需要超高性能,但希望有一个稳定的解决方案。我选择了 Rook Ceph,原因如下:
准备你的集群
我们需要一个最少包含 3 个节点的 Kubernetes 集群,每个节点有一个空的附加磁盘。
我推荐使用Scaleway Kapsule来轻松实例化 Kubernetes 集群并分配未格式化的磁盘。一旦 Kubernetes 集群启动,我们将为每个节点创建一个附加卷(磁盘):
-
进入“实例”
-
选择你的节点
-
点击“附加卷”标签
-
点击“+”(创建卷),并创建一个新磁盘
下载你的kubeconf文件,并将其放置在~/.kube/config
中。你现在应该可以使用你的kubectl CLI 访问集群。
安装 Rook Ceph
1. 本博客文章有一个GitHub 上的配套仓库,让我们克隆它以获取我们需要的所有资源
git clone https://github.com/flavienbwk/ceph-kubernetes
cd ceph-kubernetes
2. 克隆 Rook 仓库并部署 Rook Ceph 操作员
git clone --single-branch --branch release-1.11 https://github.com/rook/rook.git
kubectl create -f ./rook/deploy/examples/crds.yaml
kubectl create -f ./rook/deploy/examples/common.yaml
kubectl create -f ./rook/deploy/examples/operator.yaml
3. 创建 Ceph 集群
kubectl create -f ./rook/deploy/examples/cluster.yaml -n rook-ceph
等待几分钟,直到 Ceph 配置完磁盘。健康状态应为 HEALTH_OK
:
kubectl get cephcluster -n rook-ceph
4. 创建存储类
Rook Ceph 可以为你提供两个主要的存储类。一个是 RBD,允许你在 ReadWriteOnce
模式下拥有复制存储。第二个是我们将要安装的 CephFS,允许你在 ReadWriteMany
模式下拥有复制存储。RBD 代表 RADOS Block Device,允许你在 Kubernetes 集群中配置卷。它仅支持 ReadWriteOnce
卷(RWO)。CephFS 像一个复制的 NFS 服务器。这将使我们能够在 ReadWriteMany
模式下创建卷(RWX)。
kubectl create -f ./rook/deploy/examples/csi/rbd/storageclass.yaml -n rook-ceph
kubectl create -f ./rook/deploy/examples/filesystem.yaml -n rook-ceph
kubectl create -f ./rook/deploy/examples/csi/cephfs/storageclass.yaml -n rook-ceph
5. 部署 Ceph 控制面板
kubectl create -f ./rook/deploy/examples/dashboard-external-https.yaml -n rook-ceph
转发控制面板的 HTTP 访问:
kubectl port-forward service/rook-ceph-mgr-dashboard -n rook-ceph 8443:8443
使用用户名 admin
和以下密码进行连接:
kubectl -n rook-ceph get secret rook-ceph-dashboard-password -o jsonpath="{['data']['password']}"
你应该访问以下页面:https://localhost:8443
作者提供的图像:Ceph 控制面板
部署应用
我们将部署一个自托管的文件共享应用(psitransfer)来检查我们的卷是否正确绑定。
1. 部署文件共享应用(NodePort 30080)
kubectl create -f ./psitransfer-deployment-rwx.yaml
2. 查看它被部署在哪个节点上
kubectl get pods -o wide -l app=psitransfer
获取该节点的 IP(通过 Scaleway 界面)并检查应用是否在 http://nodeip:30080 上运行。
3. 上传一些文件
从 xcal1.vodafone.co.uk 网站 下载 5MB、10MB 和 20MB 文件。
将文件上传到我们的文件传输应用中。点击屏幕上出现的链接。
现在您应该能看到导入的树形文件。点击它并保持浏览器标签中的链接,我们稍后会用到它。
上传了大约 400MB 的文件后,我们可以看到数据在磁盘之间的复制是一致的。我们看到 3 个磁盘在文件上传时被同时写入。在以下截图中,每个磁盘的使用率为 1%:尽管我在同一主机上上传文件,但似乎复制按预期工作,数据在 3 个磁盘(OSD)之间均匀持久化。磁盘 2 有大量的“读取”活动,因为另外两个磁盘从它那里同步数据。
Ceph 的仪表盘现在应该是这样的:
C. 销毁并查看
我们将停止托管 Web 应用的节点,以确保数据已复制到其他节点。
- 查看应用部署在哪个节点上
kubectl get pods -o wide -l app=psitransfer
2. 从 Scaleway 控制台关闭节点
这模拟了节点上的电力故障。它应该在几分钟后变为 NotReady
:
$> kubectl get node
NAME STATUS ROLES AGE VERSION
scw-ceph-test-clustr-default-5f02f221c3814b47a Ready <none> 3d1h v1.26.2
scw-ceph-test-clustr-default-8929ba466e404a00a Ready <none> 3d1h v1.26.2
scw-ceph-test-clustr-default-94ef39ea5b1f4b3e8 NotReady <none> 3d1h v1.26.2
而且 Node 3 在我们的 Ceph 仪表盘上不可用:
Ceph 的仪表盘现在应该是这样:
3. 重新调度我们的 Pod
调度的 Pod 节点不可用。然而,我们的 Pod 仍然认为它是活动的:
$> kubectl get pods -o wide -l app=psitransfer
NAME READY STATUS RESTARTS AGE IP NODE
psitransfer-deployment-8448887c9d-mt6wm 1/1 Running 0 19h 100.64.1.19 scw-ceph-test-clustr-default-94ef39ea5b1f4b3e8
删除它以便在另一个节点上重新调度:
kubectl delete pod psitransfer-deployment-8448887c9d-mt6wm
检查刚刚重新启动的 Pod 的状态。您的应用程序应该可以通过之前保留的链接再次访问。
为了避免在节点变为“NotReady”时需要手动删除 Pod 以便重新调度,建议将应用的副本数默认扩展到至少 3。
现在您可以重新启动之前关闭的节点。
什么时候使用 rook-ceph-block 或 rook-cephfs?
如果您的应用程序需要更好的性能并且需要 RWO 访问模式的块存储,请使用 rook-ceph-block (RBD) 存储类。另一方面,如果您的应用程序需要具有 RWX (CephFS) 访问模式和 POSIX 兼容的共享文件系统,请使用 rook-cephfs 存储类。
如果选择 RBD 并尝试在原节点离线时重新调度一个 pod,就像我们在 CephFS 中做的那样,你会收到来自 PVC 的错误,内容为:"卷已独占地附加到一个节点,无法附加到另一个节点”。在这种情况下,你只需等待 PVC 重新绑定(我的集群自动重新分配 PVC 给我的 pod,花了大约 6 分钟,使其可以启动)。
尝试这种行为 按照相关仓库章节。
最后一句
你已经学会了如何使用 Ceph 安装和部署应用程序。你甚至证明了它能够复制数据。恭喜你 ✨
除非另有说明,否则所有图片均由作者提供。
使用 lazy_static 在运行时初始化 Rust 常量
原文:
towardsdatascience.com/initialize-rust-constants-at-runtime-with-lazy-static-e05b1df46c79
使用懒惰初始化定义非常量静态变量
·发表于 Towards Data Science ·4 分钟阅读·2023 年 8 月 5 日
--
由 Christian Lue 在 Unsplash 提供的照片
在编程中,在编译时初始化常量的好处是显而易见的。这样不仅减少了初始化开销,还使编译器能够更聪明地优化代码,因为它提前知道了常量的值。
然而,有时在编译时初始化每个常量是不可能的,因为这需要执行非常量操作或获取仅在运行时可用的数据。例如,假设我们在程序中反复使用数字 √7
。与其每次计算它,不如像下面这样定义一个常量:
const ROOT_OF_SEVEN: f64 = 7_f64.sqrt();
然而,这段代码是无效的。Rust 编译器返回以下错误:
cannot call non-const fn `f64::<impl f64>::sqrt` in constants
calls in constants are limited to constant functions, tuple structs and tuple variants
如果我们尝试用环境变量初始化一个常量,也会发生相同的情况:
const LANG: String = env::var("LANG").unwrap();
来自 Rust 编译器:
cannot call non-const fn `std::env::var::<&str>` in constants
calls in constants are limited to constant functions, tuple structs and tuple variants
正如你所见,某些在编译时初始化的常量需要非常量操作。这时,Rust 的 lazy_static
crate 就派上用场了。lazy_static
允许你定义全局静态变量,这些变量会被懒惰地初始化,即它们的值仅在第一次实际使用时才被设置。懒惰的静态变量只需在第一次使用时初始化一次,由于这是一次性操作,因此它们的运行时初始化开销可以忽略不计。
在本文中,我们将探讨如何使用 Rust 的 lazy_static
crate 来懒惰地初始化全局常量及其一些用例。
使用 lazy_static
要使用 lazy_static
crate,你只需将其添加到项目依赖中
cargo add lazy_static
一旦你添加了这个 crate,你可以在源文件中像这样导入 lazy_static!
宏:
use lazy_static::lazy_static;
要定义延迟静态,需将所有声明包含在 lazy_static!
宏中:
lazy_static! {
static ref LANG: String = env::var("LANG").unwrap();
static ref ROOT_OF_SEVEN: f64 = 7_f64.sqrt();
}
通过这样做,你声明了一个全局静态引用,用于延迟初始化的值。要使用这些“延迟常量”,你只需对它们进行解引用:
fn main() {
println!("System language: LANG is {}", *LANG);
println!("Square root of 7 is {}", *ROOT_OF_SEVEN);
}
现在你可以有效地使用 lazy_static
crate 在运行时初始化全局常量。继续阅读以了解它是如何工作的。
什么是延迟初始化?
延迟静态何时初始化?在计算中,“延迟”一词通常意味着只有在需要时才进行工作。否则,延迟操作不会执行。在延迟静态的情况下,它们会在首次使用之前初始化。
为了更清楚,请查看以下代码示例:
lazy_static! {
static ref BYTES_WRITTEN: usize = {
let bytes_written = std::io::stdout().write(b"Initializing lazy_static!\n").unwrap();
std::io::stdout().flush().unwrap();
bytes_written
};
static ref ANOTHER_LAZY_STATIC: String = {
println!("Initializing ANOTHER_LAZY_STATIC!");
"Hello, World!".to_string()
};
static ref UNUSED_LAZY_STATIC: String = {
println!("Initializing UNUSED_LAZY_STATIC!");
"Hello, World!".to_string()
};
}
所有这些延迟静态初始化都有副作用:它们会将内容打印到控制台。利用这些副作用,我们可以检测到它们的值何时被实际初始化。
main()
函数:
fn main() {
println!("Doing stuff before using lazy statics...");
println!("Bytes written during initialization: BYTES_WRITTEN is {}", *BYTES_WRITTEN);
println!("Reusing an already initialized lazy static: BYTES_WRITTEN is {}", *BYTES_WRITTEN);
println!("ANOTHER_LAZY_STATIC is {}", *ANOTHER_LAZY_STATIC);
}
输出顺序显示了每个操作在运行时的执行时间:
Doing stuff before using lazy statics...
Initializing lazy_static!
Bytes written during initialization: BYTES_WRITTEN is 26
Reusing an already initialized lazy static: BYTES_WRITTEN is 26
Initializing ANOTHER_LAZY_STATIC!
ANOTHER_LAZY_STATIC is Hello, World!
正如从第一个输出行中看到的,所有延迟静态都在实际需要之前没有初始化。第二和第三输出行显示初始化恰好发生在值被使用之前。
第四行突出了这样一个事实:对于已使用的延迟静态,不需要初始化,因为它们的值已被存储。
另外,你是否注意到 UNUSED_LAZY_STATIC
从未初始化?那是因为只有在实际需要时才会执行延迟操作。
深入阅读
本文旨在成为 lazy_static
crate 及其最流行功能的基础教程。欲了解更多信息,请阅读 官方文档。
希望你喜欢这篇文章。如果你有任何补充,请在评论中分享你的想法。感谢阅读!
如果你想了解更多关于低级编程的内容,我强烈推荐你查看下面的故事:
了解为什么 if 语句不受欢迎以及它们实际是如何工作的。剧透:它们并没有那么糟糕
《从数据到洞察:使用书店分析比较 SQL 和 Python 查询》
哪种方法更适合你的探索性数据分析?
·发表在 Towards Data Science ·阅读时间 9 分钟·2023 年 9 月 1 日
--
由 Ayman Yusuf 在 Unsplash 提供的照片
SQL 是任何数据科学家工具箱中的基础技能 — 能够迅速从数据源中提取数据进行分析是处理大量数据时的必备技能。在这篇文章中,我想给出一些我在 SQL 中通常使用的基本查询的示例,贯穿整个探索性数据分析过程。我将这些查询与在 Python 中产生相同输出的类似脚本进行比较,以便对这两种方法进行比较。
在这次分析中,我将使用一些关于去年最高评分图书的合成数据,这些数据来自一个假设的书店连锁(总虚构书店)。关于此项目的 GitHub 文件夹的链接可以在这里找到,在那里我详细介绍了如何进行分析。
由 Eugenio Mazzone 在 Unsplash 提供的照片
额外说明 — 尽管本文主要关注 SQL 查询,但值得注意的是,这些查询可以通过 pandaSQL 库与 Python 无缝集成(如我在此项目中所做)。这可以在此项目的 GitHub 链接中的 Jupyter notebook 中详细查看,但这个查询的一般结构如下:
query = """
SELECT * FROM DATA
"""
output = sqldf(query,locals())
output
PandaSQL 是一个非常实用的库,对于那些对 SQL 查询比典型的 Pandas 数据集操作更为熟悉的人来说 — 如我将在这里展示的,它通常更容易阅读。
数据集
数据集的一部分如下所示——包含书名和出版年份、页数、类型、书籍的平均评分、作者、销售单位数和书籍收入的列。
需分析的合成数据(数据由作者提供)
按年代的收入分析
假设我想知道哪个年代出版的书籍对书店最有利。原始数据集中没有书籍出版的年代列——但这相对容易输入数据。我运行子查询,通过整数除法来划分年份并乘以 10 以获得年代数据,然后按年代汇总和平均收入。接着,我按总收入对结果进行排序,以获取书店中最有利的出版年代。
WITH bookshop AS
(
SELECT TITLE, YEARPUBLISHED,
(YEARPUBLISHED/10) * 10 AS DECADE,
NUMPAGES, GENRES, RATING, AUTHOR, UNITSSOLD,
REVENUE
from df
)
SELECT DECADE, SUM(REVENUE) AS TOTAL_REVENUE,
ROUND(AVG(REVENUE),0) AS AVG_REVENUE
FROM bookshop
GROUP BY DECADE
ORDER BY TOTAL_REVENUE DESC
相比之下,Python 中的等效输出看起来类似于下面的代码片段。我应用了一个 lambda 函数,该函数进行整数除法并输出年代,然后根据年代汇总收入并按总收入对结果进行排序。
# creating df bookshop
bookshop = df.copy()
bookshop['Decade'] = (bookshop['YearPublished'] // 10) * 10
# group by decade, agg revenue by sum and mean
result = bookshop.groupby('DECADE') \
.agg({'Revenue': ['sum', 'mean']}) \
.reset_index()
result.columns = ['Decade', 'Total_Revenue', 'Avg_Revenue']
# sorting by decade
result = result.sort_values('Total_Revenue')
注意,Python 脚本中有更多的独立步骤来实现相同的结果——函数在初看时很笨拙且难以理解。相比之下,SQL 脚本在展示上要清晰得多,更易于阅读。
我现在可以将这个查询进行可视化,以了解各年代的书籍收入趋势,使用以下脚本设置 matplotlib 图表——条形图显示按年代的总收入,散点图显示平均书籍收入。
# Creating primary y-axis (total revenue)
fig, ax1 = plt.subplots(figsize=(15, 9))
ax1.bar(agg_decade['DECADE'], agg_decade['TOTAL_REVENUE'],
width = 0.4, align='center', label='Total Revenue (Dollars)')
ax1.set_xlabel('Decade')
ax1.set_ylabel('Total Revenue (Dollars)', color='blue')
# Adjusting gridlines on the primary y-axis
ax1.grid(color='blue', linestyle='--', linewidth=0.5, alpha=0.5)
# Creating secondary y-axis (avg revenue)
ax2 = ax1.twinx()
ax2.scatter(agg_decade['DECADE'], agg_decade['AVG_REVENUE'],
marker='o', color='red', label='Avg Revenue (Dollars)')
ax2.set_ylabel('Avg Revenue (Dollars)', color='red')
# Adjusting gridlines on the secondary y-axis
ax2.grid(color='red', linestyle='--', linewidth=0.5, alpha=0.5)
# Setting the same y-axis limits for both ax1 and ax2
ax1.set_ylim(0, 1.1*max(agg_decade['TOTAL_REVENUE']))
ax2.set_ylim(0, 1.1*max(agg_decade['AVG_REVENUE']))
# Combining legends for both axes
lines, labels = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax2.legend(lines + lines2, labels + labels2, loc='upper left')
# Set title
plt.title('Total and Avg Revenue by Decade')
# Show the plot
plt.show()
可视化如下所示——1960 年代出版的书籍显然是书店最赚钱的,为“总虚构书店”产生了超过 192,000 美元的收入。相比之下,1900 年代的书籍虽然平均上更有利可图,但销售不如 1960 年代的书籍。
按发布年代的总收入和平均收入(图像由作者提供)
平均书籍收入在所有出版年代中跟随总收入的趋势——除了 1900 年代和 1980 年代的书籍,这些书籍平均上更有利可图,但总体上并不是。
作者分析
现在,假设我想获取列表中前 10 位作者的数据,按照他们的总收入排序。对于这个查询,我想知道他们在列表中出现的书籍数量、这些书籍产生的总收入、每本书的平均收入以及这些书籍在书店中的平均评分。用 SQL 回答这个简单问题很容易——我可以使用计数语句来获取他们制作的书籍总数,使用平均语句来获取每位作者的平均收入和评分。随后,我可以按导演对这些语句进行分组。
SELECT AUTHOR,
COUNT(TITLE) AS NUM_BOOKS,
SUM(REVENUE) AS TOTAL_REVENUE,
ROUND(AVG(REVENUE),0) AS AVG_REVENUE,
ROUND(AVG(RATING),2) AS AVG_RATING_PER_BOOK
FROM bookshop
GROUP BY AUTHOR
ORDER BY TOTAL_REVENUE DESC
LIMIT 10
一个等效的 Python 脚本如下——长度大致相同,但输出的复杂度更高。我在指定如何在 agg 函数中聚合每列之前,对值按作者进行分组,然后按总收入排序。同样,SQL 脚本的对比明显更清晰。
result = bookshop.groupby('Author') \
.agg({
'Title': 'count',
'Revenue': ['sum', 'mean'],
'Rating': 'mean'
}) \
.reset_index()
result.columns = ['Author', 'Num_Books', 'Total_Revenue',
'Avg_Revenue', 'Avg_Rating_per_Book']
# Sorting for total revenue
result = result.sort_values('Total_Revenue', ascending=False)
# top 10
result_top10 = result.head(10)
从下面的查询结果可以看到——Ava Mitchell 领先,书籍销售总收入超过 $152,000。Emma Hayes 位居第二,收入超过 $85,000,Liam Parker 紧随其后,收入超过 $83,000。
书籍作者查询的输出
使用 matplotlib 通过以下脚本进行可视化,我们可以生成总收入的条形图,并且数据点显示每位作者的平均书籍收入。每位作者的平均评分也绘制在第二个坐标轴上。
# Creating figure and axis
fig1, ax1 = plt.subplots(figsize=(15, 9))
#plotting bar chart of total revenue
ax1.bar(agg_author['Author'], agg_author['TOTAL_REVENUE'],
width=0.4, align='center', color='silver', label='Total Revenue (Dollars)')
ax1.set_xlabel('Author')
ax1.set_xticklabels(agg_author['Author'], rotation=-45, ha='left')
ax1.set_ylabel('Total Revenue (Dollars)', color='blue')
# Adjusting gridlines on the primary y-axis
ax1.grid(color='blue', linestyle='--', linewidth=0.5, alpha=0.5)
#creating scatter plot of avg revenue
ax1.scatter(agg_author['Author'], agg_author['AVG_REVENUE'],
marker="D", color='blue', label='Avg Revenue per Book (Dollars)')
# Creating scatter plot of avg rating on secondary axis
ax2 = ax1.twinx()
ax2.scatter(agg_author['Author'], agg_author['AVG_RATING_PER_BOOK'],
marker='^', color='red', label='Avg Rating per Book')
ax2.set_ylabel('Avg Rating', color='red')
# Adjusting gridlines on the secondary y-axis
ax2.grid(color='red', linestyle='--', linewidth=0.5, alpha=0.5)
# Combining legends for both axes
lines, labels = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines + lines2, labels + labels2, loc='upper right')
# Set title
plt.title('Top 10 Authors by Revenue, Rating')
# Show the plot
plt.show()
运行此脚本,我们得到以下图表:
收入和评分前 10 名作者(图片由作者提供)
这个图表确实指向了一个相当明确的结论——收入与每位作者的平均评分没有关联。Ava Mitchell 的收入最高,但在上述作者中评分处于中位数。Olivia Hudson 在平均评分方面最高,但总收入排名第 8;作者的收入与其受欢迎程度之间没有可观察到的趋势。
比较书籍长度与收入
最后,假设我想展示书籍收入如何根据书籍长度的不同而变化。为了回答这个问题,我首先要将书籍根据书籍长度的四分位数均分为 4 类,这将更好地了解收入与书籍长度的整体趋势。
首先,我在 SQL 中定义四分位数,使用子查询生成这些值,然后通过 case when 语句将书籍分类到这些桶中。
WITH PERCENTILES AS (
SELECT
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY NUMPAGES)
AS PERCENTILE_25,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY NUMPAGES)
AS MEDIAN,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY NUMPAGES)
AS PERCENTILE_75
FROM bookshop
)
SELECT
TITLE, TITLE, REVENUE, NUMPAGES,
CASE
WHEN NUMPAGES< (SELECT PERCENTILE_25 FROM PERCENTILES)
THEN 'Quartile 1'
WHEN NUMPAGES BETWEEN (SELECT PERCENTILE_25 FROM PERCENTILES)
AND (SELECT MEDIAN FROM PERCENTILES) THEN 'Quartile 2'
WHEN NUMPAGES BETWEEN (SELECT MEDIAN FROM PERCENTILES)
AND (SELECT PERCENTILE_75 FROM PERCENTILES) THEN 'Quartile 3'
WHEN NUMPAGES > (SELECT PERCENTILE_75 FROM PERCENTILES)
THEN 'Quartile 4'
END AS PAGELENGTH_QUARTILE
FROM bookshop
ORDER BY REVENUE DESC
另外(对于不支持百分位函数的 SQL 方言,如 SQLite),可以单独计算四分位数,然后手动输入到 case when 语句中。
--For SQLite dialect
SELECT TITLE, REVENUE, NUMPAGES,
CASE
WHEN NUMPAGES < 318 THEN 'Quartile 1'
WHEN NUMPAGES BETWEEN 318 AND 375 THEN 'Quartile 2'
WHEN NUMPAGES BETWEEN 375 AND 438 THEN 'Quartile 3'
WHEN NUMPAGES > 438 THEN 'Quartile 4'
END AS PAGELENGTH_QUARTILE
FROM bookshop
ORDER BY REVENUE DESC
在 Python 中运行相同的查询时,我使用 numpy 定义百分位数,然后使用 cut 函数将书籍分类到各个桶中,再按照书籍的页数排序。与之前一样,这个过程明显比 SQL 中的等效脚本要复杂。
# Define the percentiles using numpy
percentiles = np.percentile(bookshop['NumPages'], [25, 50, 75])
# Define the bin edges using the calculated percentiles
bin_edges = [-float('inf'), *percentiles, float('inf')]
# Define the labels for the buckets
bucket_labels = ['Quartile 1', 'Quartile 2', 'Quartile 3', 'Quartile 4']
# Create the 'RUNTIME_BUCKET' column based on bin edges and labels
bookshop['RUNTIME_BUCKET'] = pd.cut(bookshop['NumPages'], bins=bin_edges,
labels=bucket_labels)
result = bookshop[['Title', 'Revenue',
'NumPages', 'PAGELENGTH_QUARTILE']].sort_values(by='NumPages',
ascending=False)
这个查询的输出可以通过 seaborn 进行箱线图可视化——生成箱线图的脚本片段如下所示。注意,运行时间的桶被手动排序以正确展示。
# Set the style for the plots
sns.set(style="whitegrid")
#Setting order of profit buckets
pagelength_bucket_order = ['Quartile 1', 'Quartile 2',
'Quartile 3', 'Quartile 4']
# Create the boxplot
plt.figure(figsize=(16, 10))
sns.boxplot(x='PAGELENGTH_QUARTILE', y='Revenue',
data=pagelength_output, order = pagelength_bucket_order,
showfliers=True)
# Add labels and title
plt.xlabel('PageLength Quartile')
plt.ylabel('Revenue (Dollars)')
plt.title('Boxplot of Revenue by PageLength Bucket')
# Show the plot
plt.show()
箱线图如下所示——注意每个书籍长度四分位数的中位收入随着书籍长度增加而上升。这表明较长的书籍在书店里更具盈利性。
按书籍长度四分位数的收入箱线图(图片由作者提供)
此外,第 4 四分位数的范围相比其他四分位数要宽得多,这表明大型书籍的价格点变动更大。
最终想法及进一步应用
总的来说,使用 SQL 进行数据分析查询通常比在 Python 中进行等效操作要简单得多;SQL 语言比 Python 查询更易于编写,同时广泛能够产生相同的结果。我不会主张其中任何一个比另一个更好——我在这个分析中使用了这两种语言的组合——而是,我相信使用这两种语言的组合可以产生更高效和有效的数据分析。
因此,考虑到 SQL 查询的书写清晰度高于 Python 查询,我认为在项目的初步 EDA 中使用 SQL 更为自然。正如我在本文中所示,SQL 更容易阅读和编写,这使得它在这些早期探索任务中尤其有利。我在开始项目时经常使用它,并推荐这种方法给任何对 SQL 查询已有相当了解的人。
Inside GPT — I : 理解文本生成
ChatGPT 背后模型的简单解释
·
关注 发表在 Towards Data Science ·11 分钟阅读·2023 年 8 月 21 日
--
19 世纪的生成 AI(通过 midjourney 创建)
我经常与来自不同领域的同事交流,我喜欢将机器学习概念传达给那些几乎没有数据科学背景的人。在这里,我尝试用简单的术语解释 GPT 的工作原理,这一次以书面形式呈现。
在 ChatGPT 的流行魔力背后,隐藏着一种不那么流行的逻辑。你向 ChatGPT 输入提示,它生成文本,无论其准确性如何,都类似于人类的回答。它是如何理解你的提示并生成连贯且易于理解的回答的呢?
Transformer Neural Networks. 这种架构设计用于处理大量非结构化数据,在我们的情况下是文本。当我们说架构时,我们指的是本质上在多个层并行进行的一系列数学操作。通过这种方程系统,介绍了几项创新,帮助我们克服了长时间存在的文本生成挑战。这些挑战直到 5 年前我们还在苦苦解决。
如果 GPT 已经存在了 5 年(实际上 GPT 论文发表于 2018 年),GPT 难道不是旧闻吗?为什么最近它变得如此受欢迎?GPT 1、2、3、3.5(ChatGPT)和 4 之间有什么区别?
所有 GPT 版本都建立在相同的架构上。然而,每个后续模型包含了更多的参数,并使用了更大的文本数据集进行训练。后来的 GPT 发布显然引入了其他新颖之处,尤其是在训练过程中,例如通过人类反馈的强化学习,这一点我们将在本博客系列的第三部分中进行解释。
Vectors, matrices, tensors. 所有这些花哨的词汇本质上都是包含数字块的单位。这些数字经过一系列数学操作(主要是乘法和加法),直到我们达到最佳输出值,即可能结果的概率。
输出值?从这个意义上讲,它是由语言模型生成的文本,对吗?是的。那么,输入值是什么?是我的提示吗?是的,但不是完全。那还有什么其他的呢?
在讨论不同的文本解码策略之前,这将是下一篇博客文章的主题,消除歧义是有用的。让我们回到开始时提出的基本问题。它是如何理解人类语言的?
Generative Pre-trained Transformers。这是 GPT 缩写代表的三个词。我们在上面提到了 Transformer 部分,它代表了进行大量计算的架构。但我们究竟计算什么?你从哪里得到这些数字?它是一个语言模型,你只需输入一些文本。你怎么能计算文本呢?
数据是不可知的。所有数据无论是以文本、声音还是图像的形式存在都是相同的。¹
Tokens。我们将文本拆分成小块(标记),并为每个标记分配一个唯一的编号(标记 ID)。模型不知道单词、图像或音频记录。它们学习以巨大的数字序列(参数)来表示它们,这些数字序列作为工具来以数字形式展示事物的特征。标记是传达意义的语言单位,而标记 ID 是编码标记的唯一数字。
显然,我们标记化语言的方式可能会有所不同。标记化可以包括将文本拆分成句子、单词、单词部分(子词)或甚至单个字符。
让我们考虑一个场景,其中我们的语言语料库有 50,000 个标记(类似于 GPT-2 的 50,257)。在标记化后,我们如何表示这些单位?
Sentence: "students celebrate the graduation with a big party"
Token labels: ['[CLS]', 'students', 'celebrate', 'the', 'graduation', 'with', 'a', 'big', 'party', '[SEP]']
Token IDs: tensor([[ 101, 2493, 8439, 1996, 7665, 2007, 1037, 2502, 2283, 102]])
上面是一个被分词的例句示例。分词方法的实现可能有所不同。我们现在需要理解的重要一点是,我们通过其对应的令牌 ID 获得语言单位(令牌)的数值表示。因此,现在我们有了这些令牌 ID,能否将它们直接输入到模型中进行计算?
基数在数学中很重要。101 和 2493 作为令牌表示会对模型产生影响。因为请记住,我们所做的主要是大块数字的乘法和加法。所以用 101 或 2493 乘一个数字是有区别的。那么,我们如何确保用数字 101 表示的令牌不比 2493 不重要,仅仅因为我们偶然选择了它?我们如何编码单词而不造成虚假的排序?
独热编码。 令牌的稀疏映射。独热编码是一种将每个令牌投影为二进制向量的技术。这意味着向量中只有一个元素为 1(“热”),其余为 0(“冷”)。
图片由作者提供:独热编码向量示例
令牌由一个向量表示,该向量的长度等于我们语料库中的令牌总数。简单来说,如果我们语言中有 50k 个令牌,每个令牌由一个长度为 50k 的向量表示,其中只有一个元素为 1,其余为 0。由于这个投影中的每个向量只包含一个非零元素,因此它被称为稀疏表示。然而,正如你可能想到的,这种方法非常低效。是的,我们设法消除了令牌 ID 之间的人工基数,但我们无法推断出单词的语义信息。我们无法通过使用稀疏向量来理解单词“party”是指庆祝活动还是政治组织。此外,用大小为 50k 的向量表示每个令牌,将意味着总共有 50k 个长度为 50k 的向量。这在所需的内存和计算方面非常低效。幸运的是,我们有更好的解决方案。
嵌入。令牌的密集表示。分词单位通过一个嵌入层,其中每个令牌被转换为固定大小的连续向量表示。例如,在 GPT-3 的情况下,每个令牌由一个包含 768 个数字的向量表示。这些数字最初是随机分配的,然后在模型看到大量数据(训练)后进行学习。
Token Label: “party”
Token : 2283
Embedding Vector Length: 768
Embedding Tensor Shape: ([1, 10, 768])
Embedding vector:
tensor([ 2.9950e-01, -2.3271e-01, 3.1800e-01, -1.2017e-01, -3.0701e-01,
-6.1967e-01, 2.7525e-01, 3.4051e-01, -8.3757e-01, -1.2975e-02,
-2.0752e-01, -2.5624e-01, 3.5545e-01, 2.1002e-01, 2.7588e-02,
-1.2303e-01, 5.9052e-01, -1.1794e-01, 4.2682e-02, 7.9062e-01,
2.2610e-01, 9.2405e-02, -3.2584e-01, 7.4268e-01, 4.1670e-01,
-7.9906e-02, 3.6215e-01, 4.6919e-01, 7.8014e-02, -6.4713e-01,
4.9873e-02, -8.9567e-02, -7.7649e-02, 3.1117e-01, -6.7861e-02,
-9.7275e-01, 9.4126e-02, 4.4848e-01, 1.5413e-01, 3.5430e-01,
3.6865e-02, -7.5635e-01, 5.5526e-01, 1.8341e-02, 1.3527e-01,
-6.6653e-01, 9.7280e-01, -6.6816e-02, 1.0383e-01, 3.9125e-02,
-2.2133e-01, 1.5785e-01, -1.8400e-01, 3.4476e-01, 1.6725e-01,
-2.6855e-01, -6.8380e-01, -1.8720e-01, -3.5997e-01, -1.5782e-01,
3.5001e-01, 2.4083e-01, -4.4515e-01, -7.2435e-01, -2.5413e-01,
2.3536e-01, 2.8430e-01, 5.7878e-01, -7.4840e-01, 1.5779e-01,
-1.7003e-01, 3.9774e-01, -1.5828e-01, -5.0969e-01, -4.7879e-01,
-1.6672e-01, 7.3282e-01, -1.2093e-01, 6.9689e-02, -3.1715e-01,
-7.4038e-02, 2.9851e-01, 5.7611e-01, 1.0658e+00, -1.9357e-01,
1.3133e-01, 1.0120e-01, -5.2478e-01, 1.5248e-01, 6.2976e-01,
-4.5310e-01, 2.9950e-01, -5.6907e-02, -2.2957e-01, -1.7587e-02,
-1.9266e-01, 2.8820e-02, 3.9966e-03, 2.0535e-01, 3.6137e-01,
1.7169e-01, 1.0535e-01, 1.4280e-01, 8.4879e-01, -9.0673e-01,
…
…
… ])
上面是单词“party”的嵌入向量示例。
现在我们有 50,000x786 大小的向量,与 50,000x50,000 的独热编码相比,显著更高效。
嵌入向量将作为模型的输入。由于密集的数值表示,我们将能够捕捉单词的语义,相似的令牌的嵌入向量将彼此更接近。
如何在上下文中测量两个语言单位的相似性?有几种函数可以测量两个相同大小的向量之间的相似性。让我们用一个例子来解释。
考虑一个简单的例子,我们有“cat”、“dog”、“car”和“banana”的嵌入向量。为了简化,假设嵌入维度为 4。这意味着每个词语会由四个学习到的数字表示。
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
# Example word embeddings for "cat" , "dog", "car" and "banana"
embedding_cat = np.array([0.5, 0.3, -0.1, 0.9])
embedding_dog = np.array([0.6, 0.4, -0.2, 0.8])
embedding_car = np.array([0.5, 0.3, -0.1, 0.9])
embedding_banana = np.array([0.1, -0.8, 0.2, 0.4])
使用上述向量,我们可以通过余弦相似度来计算相似性得分。人类逻辑会发现“dog”和“cat”彼此更相似,而“banana”和“car”相似度较低。我们可以期待数学模拟我们的逻辑吗?
# Calculate cosine similarity
similarity = cosine_similarity([embedding_cat], [embedding_dog])[0][0]
print(f"Cosine Similarity between 'cat' and 'dog': {similarity:.4f}")
# Calculate cosine similarity
similarity_2 = cosine_similarity([embedding_car], [embedding_banana])[0][0]
print(f"Cosine Similarity between 'car' and 'banana': {similarity:.4f}")
"Cosine Similarity between 'cat' and 'dog': 0.9832"
"Cosine Similarity between 'car' and 'banana': 0.1511"
我们可以看到“cat”和“dog”的相似度得分非常高,而“car”和“banana”的得分非常低。现在,想象一下在我们的语言语料库中,每个 50000 个词的嵌入向量长度为 768。这就是我们能够找到彼此相关的词语的方法。
现在,让我们看看下面这两个语义复杂度较高的句子。
"students celebrate the graduation with a big party"
"deputy leader is highly respected in the party"
第一和第二句中的“party”一词传达了不同的意义。大型语言模型如何区分“party”作为政治组织与“party”作为庆祝社交活动的意义?
我们能否通过依赖词语嵌入来区分相同词语的不同含义?事实是,尽管嵌入提供了很多优势,但它们不足以解开人类语言语义挑战的复杂性。
自注意力。 解决方案再次由变压器神经网络提供。我们生成一组新的权重(参数的另一名称),即查询、键和值矩阵。这些权重学习将词语的嵌入向量表示为一组新的嵌入。如何做到?简单地通过对原始嵌入的加权平均。每个词语“关注”输入句子中的其他每个词(包括它自身)并计算一组注意力权重,换句话说,就是新的所谓“上下文嵌入”。
它真正做的只是通过分配一组新的数字(注意力权重)来映射输入句子中词语的重要性,这些数字是使用词语嵌入计算得出的。
作者提供的图片:不同上下文中词语的注意力权重(BertViz 注意力头视图)
上面的可视化展示了“party”对两个句子中其余标记的“注意力”。连接的粗细表示标记的重要性或相关性。注意力和“attending”是指一系列新数字(注意力参数)及其大小,我们用它们来表示单词的重要性。在第一个句子中,“party”对“celebrate”的关注最高,而在第二个句子中,“deputy”具有最高的注意力。这就是模型如何通过检查周围单词来融入上下文。
正如我们在注意力机制中提到的那样,我们推导出了一组新的权重矩阵,即:查询、键和值(简称 q、k、v)。它们是相同大小的级联矩阵(通常小于嵌入向量),它们被引入架构中以捕捉语言单元的复杂性。注意力参数通过学习以揭示单词、单词对、单词对对、单词对对对的关系等等。以下是查询、键和值矩阵在找到最相关单词中的可视化。
作者提供的图像:查询键和值矩阵及其最终概率的插图(BertViz q,k,v 视图)
可视化图展示了 q 和 k 向量作为垂直带,其中每个带的粗细反映了其大小。标记之间的连接表示由注意力决定的权重,表明“party”的 q 向量与“is”、“deputy”和“respected”的 k 向量最显著对齐。
为了使注意力机制以及 q、k 和 v 的概念不那么抽象,想象一下你去参加一个派对,听到了一首你爱上的绝妙歌曲。派对结束后,你急切地想找到这首歌再听一遍,但你只记得歌词中的 5 个词和一部分歌曲旋律(query)。为了找到这首歌,你决定浏览派对播放列表(keys),并逐一聆听列表中播放的所有歌曲(相似度函数)。当你最终认出这首歌时,你记录下歌曲的名称(value)。
最后一个重要的技巧是变压器引入了将位置编码添加到向量嵌入中。仅仅是因为我们希望捕捉单词的位置信息。它增强了我们准确预测下一个标记的机会,使其更接近真实的句子上下文。这是至关重要的信息,因为词语的交换往往会完全改变上下文。例如,句子“Tim chased clouds all his life”与“clouds chased Tim all his life”在本质上是完全不同的。
-
到目前为止,我们探索的所有基本数学技巧,其目标是预测下一个标记,给定输入标记的序列。确实,GPT 的训练任务非常简单,即文本生成,或者换句话说是下一个标记预测。从本质上讲,我们测量给定之前出现的标记序列的标记概率。
-
你可能会好奇模型是如何从随机分配的数字中学习最优数字的。这可能是另一个博客文章的话题,但实际上这是理解的基础。此外,你已经开始质疑基础知识,这本身就是一个很好的迹象。为了消除不明确性,我们使用一种优化算法,该算法基于称为损失函数的指标来调整参数。这个指标是通过将预测值与实际值进行比较来计算的。模型跟踪指标的变化,并根据损失值的大小来调整数字。这个过程会一直进行,直到损失值在我们在算法中设置的超参数规则下无法再减小为止。一个示例超参数可以是,我们希望多频繁地计算损失并调整权重。这是学习背后的基本思想。
-
我希望在这篇简短的文章中,我能至少稍微清晰一下。该博客系列的第二部分将重点关注解码策略,即为什么你的提示很重要。第三部分也是最后一部分将专注于 ChatGPT 成功的关键因素,即通过人工反馈的强化学习。非常感谢您的阅读。下次见。
参考文献:
A. Vaswani, N. Shazeer, N. Parmar, J. Uszkoreit, L. Jones, A. N. Gomez, Ł. Kaiser, 和 I. Polosukhin, “注意力机制就是你所需要的,”发表于《神经信息处理系统进展》第 30 卷(NIPS 2017),2017 年。
J. Vig, “变压器模型中的多尺度注意力可视化,”发表于第 57 届计算语言学协会年会:系统演示,页码 37–42,意大利佛罗伦萨,计算语言学协会,2019 年。
L. Tunstall, L. von Werra, 和 T. Wolf, “使用变压器的自然语言处理,修订版,” O’Reilly Media, Inc., 2022 年 5 月发布,ISBN: 9781098136796。
GPT 内部 — II:提示工程的核心机制
提示工程背后的简单推理
·
关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 12 月 21 日
--
解密与描绘恩 igma 机(通过 midjourney 创建)
大型语言模型是通过人类文本的视角对世界的压缩*。在第一部分中,我们已经解构了 GPT 的模型架构,接下来,让我们探讨如何解码这种压缩并操控已经训练好的模型的输出。
在模型训练过程中,我们的模型通过人类文本的投射了解世界。在训练之后,每次我们提示模型并生成文本(推理)时,它通过训练过程中学到的参数形成一个涉及词汇表中每个词元的概率分布。
预测的标记一次成为输入的最后一个标记。将最后预测的标记附加到输入中,然后用来预测下一个标记。因此,文本生成简单来说,就是在给定输入(提示)中标记出现的情况下,对下一个标记预测的概率表达。
这个概率是由什么形成的?模型训练。模型在训练过程中看到的文本数据。
让我们查看一个示例提示句,并使我们上面的解释更清晰:
“德国以其”
你如何完成这个句子?你已经了解了“德国”是什么,并且你基于你一生中所见/所闻/所读的内容(训练数据)形成了一个概念。这类似于模型在训练阶段的情况。我们从互联网/书籍中收集大量文本数据。然后我们筛选并删除有害内容。通过模型训练,最终我们的模型通过人类文本的视角理解世界。
那么,完成我们句子的下一个概率最高的标记是什么?让我们看一下 GPT-2 模型,以获取模型的实际概率。
Input: "Germany is known for its"
NEXT TOKEN:
Choice rank, token (probability)
Choice 1, high (3.30%)
Choice 2, strong (2.28%)
Choice 3, liberal (1.18%)
Choice 4, love (1.14%)
Choice 5, beautiful (0.96%)
Choice 6, generous (0.79%)
Choice 7, history (0.76%)
Choice 8," "" (0.75%)"
Choice 9, open (0.74%)
Choice 10, great (0.72%)
Choice 11, rich (0.67%)
Choice 12, beer (0.64%)
Choice 13, low (0.63%)
Choice 14, strict (0.61%)
Choice 15, unique (0.56%)
Choice 16, long (0.55%)
Choice 17, innovative (0.55%)
Choice 18, quality (0.55%)
Choice 19, many (0.52%)
Choice 20, large (0.50%)
Choice 21, heavy (0.50%)
.
.
.
Choice 89, harsh (0.17%)
Choice 90, wide (0.17%)
Choice 91, colorful (0.17%)
Choice 92, historic (0.17%)
Choice 93, ability (0.16%)
Choice 94, lack (0.16%)
Choice 95, aggressive (0.16%)
Choice 96, military (0.16%)
Choice 97, small (0.16%)
Choice 98, state (0.16%)
Choice 99, legendary (0.16%)
Choice 100, powerful (0.16%)
.
.
.
Choice 50158 phis (0.00000001097%)
Choice 50159 Florida (0.00000001094%)
Choice 50160 rez (0.00000001088%)
Choice 50161 etus (0.00000001066%)
Choice 50162 chapter (0.00000001045%)
Choice 50163 obin (0.00000001023%)
Choice 50164 Hong (0.00000000928%)
Choice 50165 assetsadobe (0.00000000894%)
Choice 50166 teasp (0.00000000862%)
Choice 50167 earthqu (0.00000000716%)
.
.
Choice 50255 � (0.0000000000000774%)
Choice 50256 サーティ (0.0000000000000749%)
Choice 50257 (0.0000000000000743%)
(注:本文中的示例使用了 GPT-2 模型生成,因为它是一个公开模型,且足够小,可以通过实际示例来说明这些概念。)
上面展示了模型在给定提示下的概率分布。对于语料库中的每个标记(模型训练时使用的文本数据集),我们都有一个相应的计算概率。模型的词汇总数(在 GPT-2 中为 50,257)也是概率分布的大小。这些概率计算背后的技术在本博客文章系列的第一篇文章中进行了详细解释。 (link)
从预训练语言模型生成的输出可以通过几种解码策略进行控制。因此,无论我们是尝试提取事实信息还是生成创意故事,我们都可以影响输出,并调整其真实性和创造性。
解码生成文本的预测概率最简单的方法是直接在每一步预测时选择具有最高概率的标记。这种策略也被称为“贪婪”解码。
让我们提取给定初始输入句子(提示)的预测概率最高的前五个标记。每个预测标记成为输入句子的最后一个标记,直到我们达到最大标记限制,这是我们定义的一个参数。在这个例子中,让我们生成接下来的 10 个标记。
前 5 个概率选择(图片由作者提供)
正如你在上面所见,通过贪婪策略,我们将具有最高概率的标记附加到输入序列中,并预测下一个标记。
贪婪搜索决策路径(图片由作者提供)
使用这种策略,让我们用贪婪搜索解码生成 128 个后续标记的较长文本。
"""
Germany is known for its high-quality beer, but the country's beer culture is also a reflection of its history.
The country's beer culture is a reflection of its history. Beer is a German tradition.
The country's beer culture is a reflection of its history. Beer is a German tradition.
The country's beer culture is a reflection of its history. Beer is a German tradition.
The country's beer culture is a reflection of its history. Beer is a German tradition.
The
"""
正如你从上面的文本中看到的,尽管这是最简单的逻辑,但这种方法的缺点是生成了重复的序列。由于它未能捕捉到序列的概率,也就是说,多个单词一个接一个的整体概率被忽视。贪婪搜索一次只预测并考虑一步的概率。
重复的文本是一个问题。我们希望生成的输出简洁,我们该如何实现呢?
我们不是在每一步选择概率最高的标记,而是考虑未来的 x 步,计算联合概率(简单地是连续概率的乘积),并选择下一个标记序列,即最可能的序列。这里的 x 指的是 beams 的数量,也就是我们查看未来步骤的序列深度。这种策略被称为beam 搜索。
让我们回到 GPT-2 的示例,探索 beam 搜索与贪婪搜索的情境。
根据提示,查看概率最高的两个标记及其在树状图中的延续(4 个 beams)。
贪婪搜索 vs beam 搜索(图像由作者提供)
让我们计算上述绿色序列的联合概率。
德国以其 -> 高质量的啤酒而闻名……
具有联合概率 3.30%24.24%31.26%*6.54% = 0.0016353
而下方路径的序列;
德国以其 -> 强大的生活传统而闻名……
2.28%2.54%87.02%*38.26% = 0.0019281。
尽管顶部序列中的第一个下一个标记预测步骤具有更高的概率,但底部序列总体上具有更高的联合概率。
尽管贪婪搜索在每个预测步骤中优先考虑绝对最大概率,但它忽略了序列中标记的概率。Beam 搜索解码使我们能够深入序列,并帮助我们以更广泛的方式解码文本。那么,beam 搜索是最终解决方案吗?
让我们进一步探索,并以 5 个 beam 的深度解码接下来的 128 个标记。
"""
Germany is known for its high-speed rail network, but the country is also home to some of the world's most beautiful natural landscapes.
Here are 10 of the most beautiful places in Germany.
1\. Lake Constance
Lake Constance is one of the largest lakes in Germany.
It is located in the state of North Rhine-Westphalia and is the second largest lake in Germany after Lake Constance in Bavaria.
Lake Constance is located in the state of North Rhine-Westphalia and is the second largest lake in Germany after Lake Constance in Bavaria.
"""
尽管与贪婪搜索相比较少,beam 搜索也会遭遇重复输出的问题。然而,通过 beam 搜索解码,我们可以通过惩罚重复的词序列对来解决这个问题。换句话说,如果序列之前已经被解码过,那么标记序列的概率将被赋值为零。重复标记序列的这种惩罚也被称为 n-gram 惩罚。
当“n”表示序列的长度时,“gram”是一个在计算语言学中指“单位”的术语,通常对应于我们案例中的“标记”。
其背后的原因是为了避免生成包含连续重复 n-gram 的序列。解码算法将惩罚包含重复词对的生成序列。
知道这一点后,让我们应用 n-gram 惩罚,其中 n = 2。
"""
Germany is known for its high-speed rail network, but the country is also home to some of the world's most beautiful natural landscapes, including the Alps, the Baltic Sea, and Lake Constance.
The country's capital, Berlin, is the largest city in Europe, with a population of more than 8.5 million people.
The city is located in the former East Germany, which was divided into East and West Germany after World War II.
Today, Germany is a member of both the European Union and NATO, as well as the World Trade Organization and the Organization for Economic Cooperation and Development (OECD).<|endoftext|>
"""
这是我们从模型中提取的关于输入提示的最佳完成文本,体现了连贯性和紧凑性。通过 n-gram 惩罚,使用 beam-search 解码的输出变得更加类似人类。
什么时候应该使用 beam-search,什么时候使用 greedy-search?当事实准确性至关重要时,比如解决数学问题、关键信息提取、总结或翻译时,应该优先选择 greedy-search。然而,当我们希望实现创意输出而事实准确性不是优先考虑的问题时(比如故事生成),beam-search 通常是更合适的方法。
为什么你的提示内容如此重要?因为你选择的每个词、句子结构、指令布局都会在大型语言模型的深层激活不同的参数序列,每个不同的提示都会形成不同的概率。问题的本质在于,文本生成是对你的提示的概率表达。
还有其他替代方法可以防止重复并影响生成文本的事实准确性/创造性,例如截断词汇分布或采样方法。如果你对深入探讨这一主题感兴趣,我强烈推荐 Patrick von Platen 在 HuggingFace 博客中的 文章。
本系列的下一篇也是最后一篇文章将探讨通过人类反馈的微调和强化学习,这在为什么预训练模型能够在多个基准测试中超越 SOTA 模型中起到了重要作用。希望在这篇博客文章中,我能够帮助你更好地理解提示工程的推理。感谢阅读。下次再见。
参考文献:
-
— 伊利亚·苏茨克维,No Priors 第 39 期 | 与 OpenAI 联合创始人兼首席科学家伊利亚·苏茨克维对话
www.youtube.com/watch?v=Ft0gTO2K85A
-
L. Tunstall, L. von Werra, 和 T. Wolf,《使用变换器的自然语言处理(修订版)》,O’Reilly Media, Inc.,2022 年 5 月发布,ISBN: 9781098136796。
-
相关链接:
-
Inside GPT — I : 理解文本生成
towardsdatascience.com/inside-gpt-i-1e8840ca8093
-
如何生成文本:使用不同的解码方法生成变换器语言文本
检查数据科学预测:个人 + 负面案例分析
如何检查特定的预测并进行负面案例分析
·发表于Towards Data Science ·14 分钟阅读·2023 年 7 月 21 日
--
当我向新学习者展示如何使用.predict()
方法时,大约有 40%到 43%的时间会得到以下问题:
预测在哪里?
我希望这是学习者们更多提问的问题。这是一个深刻的问题,特别是对于那些刚接触 Python、数据科学,并且可能第一次看到.predict()
方法的人。
- 确实,提出这个问题的群体数量少于一半,但可能这个比例低于 30%或 20%。我并没有精确跟踪。
在本深度分析的第一部分,本文将首先展示如何构建一个简单的预测模型,其次展示如何生成预测,最后介绍如何更仔细地检查预测。
在本深度分析的第二部分,本文还将展示为什么了解如何检查个别预测是有用的,以及为什么有必要检查个别预测。具备检查个别预测的能力可以开启一系列分析途径,例如负面案例分析。
第一部分:预测方法
如果你还不熟悉构建预测模型,我建议你考虑阅读一篇或多篇涉及此主题的其他文章。我所著的Confident Data Science: Discovering The Essential Skills of Data Science第十一章展示了如何构建预测模型。
例如,在假鸟与机器学习:使用流行的鸟类数据演示最近邻分类中,我分享了训练机器学习模型的代码,该模型可以根据鸟的重量、长度、位置和颜色预测鸟类品种。这个假鸟示例展示了如何使用假鸟品种数据进行预测建模。
一个简单的预测模型
为了帮助我们专注于检查特定的个体预测,本小节将快速创建一个预测模型。为了加快速度,本小节省略了优化超参数和一些数据准备步骤。
为了加快速度,我们还会通过其他方法评估,而不仅仅使用传统的accuracy_score
、classification_report
和confusion_matrix
函数。
对于这个简单、快速的预测模型,我们来看看汽车数据。这里的“快速”确实是有意的!
通常我们希望有一个包含几百个观察值的数据集。然而,我喜欢使用这个来自 Seaborn 的汽车数据,因为几乎每个人都理解或知道一些关于汽车的基本知识。
第一步,打开数据。 和许多训练营演示或在线演示文章一样,你首先打开数据。
# Standard required imports on Pandas and Seaborn
import pandas as pd
import seaborn as sns
# Load data from Seaborn library
df = sns.load_dataset('mpg')
# Remove records with missing values
df.dropna(inplace=True)
第二步,准备数据。 之后你准备一个目标变量。在本演示中,我们将创建一个目标变量,用于预测当车辆在美国制造时为 1,其他地方制造时为 0。
# List comprehension to make a new target variable
df['isUS'] = [1 if x == 'usa' else 0 for x in df['origin']]
第三步,训练、测试和拆分。 另一个惯例步骤是训练、测试和拆分(train, test, split
)数据,如下所示。
# Import train_test_split from sklearn
from sklearn.model_selection import train_test_split
# Specify a feature and target matrix
X = df[['mpg','cylinders','displacement',
'horsepower','weight','acceleration']]
y = df['isUS']
# Train, test, and split the feature and target matricies
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=1234)
第四步,训练模型。 现在数据已经准备好进行训练。
# Import KNN classifier from sklearn
from sklearn.neighbors import KNeighborsClassifier
# Instantiate and train KNN model
knn = KNeighborsClassifier(n_neighbors=7)
knn.fit(X_train, y_train)
使用模型生成预测
第五步,从训练好的模型中生成预测。 一旦训练完成,模型就可以生成预测。
# Generate and save predictions
pred = knn.predict(X_test)
第六步,开始探索/评估过程。 一旦生成预测,就可以进行检查。在这里,我将沉溺于一些不常见的检查选项。在训练营的背景下,我当然会分享标准的accuracy_score
、classification_report
和confusion_matrix
函数(以及均方误差和均方根误差的等效函数)。不过,我也认为展示其他方法很重要。我发现学习者更喜欢看到其他方法。
展示其他方法有助于学习者理解accuracy_score
、classification_report
和confusion_matrix
的功能、工作原理以及如何解释它们的作用。在有问题时,能够向他人解释这些内容非常重要。
更根本地,学习、了解和使用替代方法使数据科学家能够复制(并验证)更传统的方法。换句话说,这使得学习者能够做的不仅仅是信任传统的现成选项。当你可以独立验证结果时,无需仅仅依赖信任。
一旦你得到预测结果,就可以开始检查这些预测。
# Find proportion of predictions that were accurate
print(f'The accuracy of this model is {(pred == y_test).mean() * 100}')
这段代码的美妙之处在于它比看起来复杂得多。首先考虑,它将每个预测(在 pred
中找到)与相应的实际值(在 y_test
中找到)进行比较。比较 pred == y_test
将返回一个布尔数组(即一系列 True
和 False
值),其中 True
表示预测与实际值匹配,而 False
表示不匹配。
这里是魔法……然后,应用于此数组的 .mean()
函数实际上找到了正确预测的比例。具体如下。在 Python 中,当计算布尔数组的均值时,True
值被视为 1,False
值被视为 0。这实际上计算了正确预测的比例(或模型的准确度),因为它将 True
值的数量(正确预测的总和是所有值中的 True = 1,而 False = 0)除以总预测数。这样,数学计算结果使得均值公式等同于 True 比例的公式。
最后,这段代码还将结果乘以 100 以生成可读的百分比,并且使用 f-string 报告结果。如果你跟随相同的随机状态,你应该会发现准确率大约为 84%。
另一种选择是使用 pd.crosstab()
来显示如下的混淆矩阵。
# Use pd.crosstab to generate a confusion matrix
pd.crosstab(pred, y_test)
这样就会产生如下版本。
Actual Values
Predictions 0 1
0 27 9
1 3 40
从这些结果中,我们可以看到模型正确地识别/预测了 40 辆车辆在美国制造,正确识别/预测了 27 辆在国外制造的车辆,并且错误识别/预测了大约 12 辆其他车辆(9 个假阴性 + 3 个假阳性)。
更仔细地检查单个预测
本文的核心是解释如何检查单个预测。本文回答了我经常从训练营学习者那里得到的问题,即他们第一次看到 .predict()
方法时的疑问。
预测在哪里?
图片来源:作者在 Canva 上使用 Canva 的库存图片制作的插图。
进行更详细检查的一个好步骤是将真实目标值和预测目标值附加到原始训练数据中。这样,你可以将结果并排比较,并结合每个预测变量进行上下文分析。
对于来自其他编程语言的用户来说,值得一提的是,Stata 中与 SciKit Learn 的 .predict()
方法等效的功能会自动将预测结果附加到原始数据中。在 Python 中,这种合并/连接需要额外的工作。
以下代码可以实现这种合并/连接。
# Concatenate three data sources: Testing data (X_test),
# actual outcomes (y_test), and predictions (pred)
pd.concat([X_test.reset_index(drop=True),
pd.DataFrame(y_test).reset_index(drop=True),
pd.DataFrame({'preds':pred})], axis=1)
对于将类似于以下结果的结果。
图片来源:Jupyter Notebook 中生成的屏幕截图,代码如图所示。
该输出显示了前五个和最后五个观察数据以及一个名为isUS
的列(显示真实值 1 = 在美国制造,0 = 在国外制造)和另一列名为preds
(显示预测值 1 = 预测在美国制造,0 = 预测在国外制造)。
观察数据编号 4 和 77 显示了实际值和预测值的不匹配。观察数据编号 4 是一个假阴性,这意味着模型错误地预测了车辆是在国外制造的。而观察数据编号 77 是一个假阳性,这意味着模型错误地预测了车辆是在美国制造的。
第二部分:详细检查的实用性
检查期间需要提出的问题
检查模型的个别预测不仅仅是确认它们是否正确。这也是一个调查过程,可以产生重要的见解并指导进一步的模型改进。在检查个别预测时,提出几个关键问题是很有帮助的。
-
领域专家是否会同意(错误)的预测?
-
一个因果观察者会如何预测结果?因果观察者是否会同意(错误)的预测?
-
是否有其他特征或预测因子,没有包含在模型中,可能有助于模型在这一特定(错误)案例中产生更好的结果?
-
主题专家是否会同意特征的重要性?不同或更彻底的数据准备是否可能导致不同的特征重要性,从而可能更符合主题专家的看法?
-
哪些特征可能导致了这一特定的(错误)预测?
-
错误预测中是否存在任何模式?
-
模型在这一预测中有多自信?
所有这些问题都有助于评估现有模型,并为未来的发展和修订提供指导。
负面案例 + 其他数据源
探索上述问题的过程就是进行个别负面案例分析的过程。对于第一个问题,即领域专家是否会同意,你的工作可能涉及准备数据导出,以便与领域专家共享。
这种导出方式允许领域专家在自己的时间里审查结果,然后向你反馈他们的想法、观察和建议。以下代码生成一个只包含错误预测的 HTML 文件,任何人都可以在任何网页浏览器中查看。
# Concatenate three data sources: Testing data (X_test),
# actual outcomes (y_test), and predictions (pred)
post_analysis = pd.concat(
[X_test.reset_index(drop=True),
pd.DataFrame({'isUS':y_test}).reset_index(drop=True),
pd.DataFrame({'preds':pred})], axis=1)
# Add a new column, 'Correct', to act as a filter
post_analysis['Correct'] = post_analysis['isUS'] == \
post_analysis['preds']
# Filter and export data as an html file
post_analysis[
post_analysis['Correct']==False].to_html('For_SME_Review.html')
相同的导出文件可以与其他值得信赖的顾问或同事分享,即使他们不一定是主题专家。外行人的意见也可能有价值。
对于第三个问题,即模型中未包含的其他特征或预测变量是否可能在特定(错误)情况下帮助模型产生更好的结果,大多数模型至少有一种方法可以协助评估特征重要性。
然而,事实证明,K-近邻(KNN)算法并不直接提供特征重要性度量或函数。这并不是一个疏漏。KNN 方法仅根据特征向量之间的相似性做出预测,而不对单个特征加权。这意味着我们需要一种间接评估特征重要性的方法,即某种代理方法。我在下面提供了这样的代理。
另一种选择是使用支持特征重要性的模型,如决策树或随机森林。然而,如果不建立另一个模型,也可以通过观察在轮流处理特征时模型性能的变化来评估特征的重要性。
通过轮流使用每个特征,以在训练过程中排除该特征,这是一个只需简单 for 循环的过程,如下所示。任何特征缺失时的性能下降都可以作为特征重要性的有意义衡量标准。这里的代码可以帮助在通过多个模型轮流处理每个特征的过程中衡量特征重要性。
# Calculate the baseline model accuracy
baseline_accuracy = accuracy_score(y_test, pred)
# Empty dictionary to hold feature importances
feature_importances = {}
# For each feature in the dataset...
for feature in X.columns:
# Create a second set of targets and features
X2 = X.drop(feature, axis=1)
y2 = y
# Create a second set of trains tests splits
X_train2, X_test2, y_train2, y_test2 = train_test_split(
X2, y2, test_size=0.2, random_state=1234)
# Fit using the second set of trains tests splits
knn.fit(X_train2, y_train2)
# Predict using the second set of trains tests splits
held_out_pred = knn.predict(X_test2)
# Get accuracy score on model with hold out
held_out_acc = accuracy_score(y_test2, held_out_pred)
# Save the accuracy score to the dictionary
feature_importances[feature] = baseline_accuracy - held_out_acc
# Produce a data frame from the dictionary
feature_imp = pd.DataFrame.from_dict(feature_importances,
orient='index',
columns=['importance'])
# Display the data frame with sorted importance values
feature_imp.sort_values(by='importance',
ascending=False)
在这个示例中,如果你跟随进行,你会看到结果显示位移和重量是最重要的特征/预测变量。排除时,重量会使模型准确率降低 3.8%。而当排除位移时,模型准确率降低 2.5%。
进一步丰富反馈机会
除了如上所述与他人分享结果外,还可以通过添加显示预测概率的列来提供帮助。在上述代码的基础上,下面的代码块提供了两个包含预测概率的列的输出。
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
# Define feature and target variables (X & y)
X = df[['mpg','cylinders','displacement',
'horsepower','weight','acceleration']]
y = df['isUS']
# Split dataset into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=1234)
# Initialize and train K-Nearest Neighbors
knn = KNeighborsClassifier(n_neighbors=7)
knn.fit(X_train, y_train)
# Make predictions on the testing set
pred = knn.predict(X_test)
# Get prediction probabilities
probs = knn.predict_proba(X_test)
# Concatenate four data sources: Testing data (X_test),
# actual outcomes (y_test), probabilities, and predictions (pred)
post_analysis = pd.concat(
[X_test.reset_index(drop=True),
pd.DataFrame({'isUS':y_test}).reset_index(drop=True),
pd.DataFrame({'preds':pred}),
pd.DataFrame(probs,
columns=['Pr 0','Pr 1'])], axis=1)
# Add a new column, 'Correct', to act as a filter
post_analysis['Correct'] = post_analysis['isUS'] == \
post_analysis['preds']
# Filter and export data as an html file
post_analysis[
post_analysis['Correct']==False].to_html('For_SME_Review.html')
图片来源:Jupyter Notebook 屏幕截图,由此处显示的代码生成。
如果你还没有学习 KNN,这里有一点关于 KNN 如何工作的额外信息。K-最近邻(KNN)算法基于训练数据集中超空间内最近邻的多数票决定类别概率。本质上,它计算‘k’个最近邻中属于每个类别的数据点数量。邻居最多的类别是新点的预测。
因此,在确定概率时,KNN 计算属于每个类别的‘k’个最近邻的比例。例如,如果你将 k 设置为 7,如我们所示,并且七个最近邻中有三个属于 A 类,四个属于 B 类,那么 KNN 算法将为测试点分配 0.4285(7 中 3)的 A 类概率和 0.5714(7 中 4)的 B 类概率。
从这个输出中,我们看到观察编号 4 中的假阴性误差很大。而观察编号 77 中的假阳性误差接近。虽然没有模型是完美的,但你可能会认为观察编号 77 中的误分类是无望的。然而,观察编号 4 中的误分类可能值得更仔细的查看,作为一个显著的改进模型性能的机会。
我们还可以从概率列中的描述性统计数据中对模型性能进行另一种观察。以下代码将显示这些统计数据。
# Predicted probability summary stats among misclassifications
post_analysis[post_analysis['Correct'] == False][
['Pr 0', 'Pr 1']].describe().transpose()
这将显示以下输出的一个版本。
图片来源:Jupyter Notebook 的屏幕截图,由这里显示的代码生成。
这个结果表明,至少在一个错误结果中,模型对其错误预测的确定度达到了 100%。进一步探讨这个观察结果将是值得的。看起来可能有机会改进一个不仅错误,而且错误程度无法更大的预测。
结论 + 回顾
在这篇深度分析的第一部分,我首先展示了如何构建一个简单的预测模型,其次展示了如何生成预测,第三部分则更详细地介绍了如何检查单个特定预测。
在这篇深度分析的第二部分,我还展示了为何了解如何检查单个预测是有用的。能够检查单个预测打开了一系列分析途径。
在某些观点下,本文做了许多书籍、视频课程和训练营教育项目所忽略的内容。他们往往忽略和过于简化了.predict()
方法之后的模型性能评估。
在本文中,我们学习并探讨了质疑和检查模型预测的必要性。文章提供并讨论了多个示例代码块和策略,深入探讨了预测结果。
为了设置,我们快速完成了一个涉及 Seaborn 的汽车数据预测模型的设置过程。在这个过程中,我们还探讨了预测信心、特征重要性、报告结果以及与他人分享结果以获取来自普通人和领域或主题专家的反馈的概念。
退一步看,这次深度的探讨见证了机器学习和数据科学的巨大力量。数据、模型、预测、科学家、代码、笔记本、领域专家等之间复杂的相互作用。
更重要的是,这篇文章也以能帮助将技术与人类理解和直觉连接起来的方式为读者做好了准备。能够以高水平的细节探索预测,例如这里展示的内容,也展示了数据科学的迷人景观。
在我们的工作中,我们可以发掘隐藏在大量数据中的见解,正是在这里我们可以利用这些见解来推动决策、创新和进步。正如这篇文章所强调的,机器学习的有效利用不仅仅是快速或大规模地开发和部署算法。
相反,我们的工作需要一个好奇且批判性的思维,愿意质疑、检查并理解所有细节。所有细节。我是那种力求不留死角的数据科学家。对.predict()
方法结果的这一探索只是这一更广泛追求的一个实例。
这篇文章是邀请你深入探索数据的核心,寻找其中的真相,并质疑其叙述。这里和我所有文章中呈现的代码块和策略,不仅仅是指令——它们是你学习和创造力的催化剂。
我鼓励你拿这段代码进行实验,并将其应用到新的数据集上。破坏它,修复它,改进它。每个数据集都有不同的故事要讲,每次探索都会带来不同的见解。数据的世界是一个广阔、复杂且美丽的地方。深入其中,探索并让数据引导你的旅程。
感谢阅读
你准备好了解更多关于数据科学职业的信息了吗?我提供一对一职业辅导,并有一个每周邮件列表,帮助数据专业的求职者。联系我们了解更多。
感谢阅读。把你的想法和意见发给我。你可以只是打个招呼。如果你真的需要告诉我哪里做得不好,我期待不久后与你聊天。Twitter: @adamrossnelson LinkedIn: Adam Ross Nelson。
深度学习的实例选择
如何为您的机器学习工作负载选择最佳机器
·
关注 发表在 Towards Data Science · 12 分钟阅读 · 2023 年 6 月 7 日
--
图片由 Cezary Morga 提供,来源于 Unsplash
在我们日常的人工智能开发过程中,我们不断做出关于在何处运行每个机器学习(ML)工作负载的最合适机器的决定。这些决策不是随意做出的,因为它们对开发的速度和成本有着重要影响。将一台配有一个或多个 GPU 的机器分配给运行一个顺序算法(例如,连通组件算法的标准实现)可能被视为浪费,而在 CPU 上训练一个大型语言模型则可能需要极其长的时间。
在大多数情况下,我们将有一系列的机器选项可供选择。在使用云服务基础设施进行 ML 开发时,我们通常可以选择各种硬件规格差异极大的机器类型。这些通常被分组为不同类型的机器家族(在 AWS 上称为实例类型),GCP 上的机器家族,以及 Microsoft Azure 上的虚拟机系列,每个家族针对不同的用例。面对众多选择,很容易感到不知所措或遭受选择过载,因此存在许多在线资源来帮助导航实例选择的过程。
在这篇文章中,我们希望将注意力集中在为深度学习(DL)工作负载选择合适的实例类型上。DL 工作负载通常非常计算密集,并且经常需要专用的硬件加速器,例如 GPU。我们在这篇文章中的意图是提出一些选择 DL 机器类型的指导原则,并强调在做出决定时应考虑的机器类型之间的一些主要区别。
这个实例选择指南的不同之处
在我们看来,许多现有的实例指南往往错失了大量机会。它们通常涉及根据一些预定义的属性(例如,计算要求、内存要求、网络要求等)对应用程序进行分类,并根据这些属性提出选择实例类型的流程图。它们往往低估了许多机器学习应用的复杂性以及以这种方式分类并不总是能充分预测其性能挑战的简单事实。我们发现,简单地遵循这些指南,有时会导致选择次优的实例类型。正如我们将看到的,我们提出的方法更加实践和数据驱动。它涉及定义清晰的指标来衡量应用程序的性能,并提供工具来比较其在不同实例类型选项上的表现。我们相信,这种方法是确保你真正最大化机会所必需的。
免责声明
请不要将我们提到的任何特定实例类型、深度学习库、云服务提供商等视为对其使用的推荐。对您来说最佳的选择将取决于您项目的独特细节。此外,我们提出的任何建议都不应被视为比建议更多的内容,应在应用之前经过仔细评估并根据您的用例进行调整。
第一部分:实例类型选择的建议原则
与任何其他重要的开发设计决策一样,强烈建议您制定一套明确的指南,以便找到最佳解决方案。使用您之前项目中使用过的或最熟悉的机器类型是最简单的选择。然而,这样做可能会导致您错失重大节省成本和/或加快整体开发时间的机会。在本节中,我们提出了一些实例类型搜索的指导原则。
定义明确的指标和比较工具
我们将讨论的一个重要指导原则是需要明确界定比较不同实例类型性能的指标和测量这些指标的工具。如果没有明确的效用函数定义,您将无法知道所选择的机器是否最优。您的效用函数可能在不同项目之间不同,甚至可能在单个项目进行过程中发生变化。当预算紧张时,您可能会优先考虑降低成本而不是提高速度。当面临重要的客户截止日期时,您可能会不惜一切代价优先考虑速度。
示例:每美元样本数指标 在之前的帖子中(例如,这里),我们曾提出 每美元样本数 — 即每花费一美元所处理的样本数 — 作为运行深度学习模型(用于训练或推理)的性能衡量标准。每美元样本数的公式为:
每美元样本数公式(由作者提供)
…其中 每秒样本数 = 批量大小 * 每秒批次数。训练实例的成本通常可以在线找到。当然,仅优化这个指标可能是不够的:它可能会最小化训练的总体成本,但如果不包括考虑整体开发时间的指标,您可能会错过所有客户截止日期。另一方面,开发速度有时可以通过并行训练多个实例来控制,从而使我们能够无论选择什么实例类型都能达到我们的速度目标。无论如何,我们的简单示例展示了需要考虑多个性能指标,并根据机器学习项目的预算和调度限制等细节来权衡它们。
如果没有测量它们的方法,制定指标是无用的。关键在于你需要定义并构建工具,将你选择的指标集成到你的应用程序中。在下面的代码块中,我们展示了一个基于 PyTorch 的简单训练循环,其中包含一行简单的代码,用于定期打印每秒处理的平均样本数。将其除以实例类型的公布成本(每秒)就得到了我们之前提到的每美元成本指标。
import time
batch_size = 128
data_loader = get_data_loader(batch_size)
global_batch_size = batch_size * world_size
interval = 100
t0 = time.perf_counter()
for idx, (inputs, target) in enumerate(data_loader, 1):
train_step(inputs, target)
if idx % interval == 0:
time_passed = time.perf_counter() - t0
samples_processed = global_batch_size * interval
print(f'{samples_processed / time_passed} samples/second')
t0 = time.perf_counter()
拥有广泛的选项
一旦我们明确了我们的效用函数,选择最佳实例类型就简化为寻找最大化效用函数的实例类型。显然,我们可以选择的实例类型的搜索空间越大,我们能获得的整体效用结果就越高。因此,需要有大量的选项。但我们也应该追求实例类型的多样性。深度学习项目通常涉及运行多个应用工作负载,这些负载在系统需求和系统利用模式上差异很大。对于某个工作负载,最佳机器类型的规格可能与另一个工作负载的最佳规格大相径庭。拥有大量和多样化的实例类型将提高你最大化所有项目工作负载性能的能力。
考虑多种选项
一些实例选择指南会建议将你的深度学习应用(例如,通过模型的大小和/或是否执行训练或推理)进行分类,并相应地选择一个(单一的)计算实例。例如,AWS 推荐使用某些类型的实例(例如,Amazon EC2 g5系列)用于机器学习推理,而其他(更强大的)实例类型(例如,Amazon EC2 p4系列)用于机器学习训练。然而,正如我们在介绍中提到的,我们认为盲目遵循这种指导可能会错失性能优化的机会。实际上,我们发现对于许多训练工作负载,包括那些具有大型机器学习模型的工作负载,我们的效用函数在被认为是针对推理的实例上得到最大化。
当然,我们并不期望你测试每一个可用的实例类型。有许多实例类型可以(而且应该)仅凭其硬件规格就被排除。我们不建议花时间评估大语言模型在 CPU 上的性能。如果我们知道我们的模型需要高精度的算术运算以实现成功收敛,我们也不会花时间在 Google Cloud TPU 上运行它(参见这里)。但在没有明显的硬件限制的情况下,我们认为实例类型应该仅根据性能数据结果来排除。
多 GPU Amazon EC2 g5 实例通常不被考虑用于模型训练的原因之一是,与 Amazon EC2 p4 相比,GPU 之间的通信介质是 PCIe,而不是 NVLink,因此支持的数据显示量更低。然而,尽管高 GPU 间通信速率对多 GPU 训练确实重要,但 PCIe 支持的带宽可能对你的网络来说足够,或者你可能会发现其他性能瓶颈阻止了你充分利用 NVLink 连接的速度。唯一确定的方式是通过实验和性能评估。
任何实例类型在实现我们的效用函数目标时都是公平的,在寻找实例类型的过程中,我们经常发现自己为那些低功耗、更环保、被低估且价格较低的黑马打气。
以最大化选项的方式开发你的工作负载
不同的实例类型可能对我们的实现施加不同的限制。它们可能需要不同的初始化顺序,支持不同的浮点数据类型,或依赖不同的软件安装。考虑到这些差异开发代码将减少你对特定实例类型的依赖,并提高你利用性能优化机会的能力。
一些高级 API 支持多种实例类型。例如,PyTorch Lightening内置了在多种不同处理器上运行深度学习模型的支持,隐藏了每种处理器所需的实现细节。支持的处理器包括 CPU、GPU、Google Cloud TPU、HPU (Habana Gaudi)等。然而,需注意的是,运行在特定处理器类型上的某些适配可能需要对模型定义进行代码更改(不改变模型架构)。你还可能需要包含一些针对加速器类型的条件代码块。某些 API 优化可能只针对特定加速器实现,但不针对其他加速器(例如,针对 GPU 的 scaled dot product attention (SDPA) API)。一些超参数,如批处理大小,可能需要调整以达到最佳性能。我们在关于 专用 AI 训练加速器 的博客系列中演示了可能需要的额外更改示例。
(重新)持续评估
重要的是,在我们当前不断创新的深度学习运行时优化环境中,性能比较结果会迅速过时。新的实例类型会定期发布,这会扩大我们的搜索空间,并提供提高效用的潜力。另一方面,流行的实例类型可能会达到生命周期终点,或因全球需求高而难以获得。软件栈的不同层级的优化(例如,见这里)也可以显著推动性能提升。例如,PyTorch 最近发布了一种新的图编译模式,据报道,在现代 GPU 上训练速度提高了多达 51%。截至本文撰写时,这些加速尚未在其他加速器上演示过。这是一个显著的加速,可能迫使我们重新评估一些以前的实例选择决策。(有关 PyTorch 编译模式的更多信息,请参见我们关于这一主题的最新文章。)因此,性能比较不应是一次性的活动;为了充分利用所有这些令人惊叹的创新,它应该定期进行和更新。
第二部分:实例类型之间的差异
了解可用实例类型的详细信息,特别是它们之间的差异,对于决定哪些实例类型适合性能评估至关重要。在本节中,我们将这些信息分为三类:硬件规格、软件栈支持和实例可用性。
硬件规格
潜在实例类型之间最重要的差异在于其硬件规格的详细信息。有许多硬件细节可能对深度学习工作负载的性能产生重大影响。这些包括:
-
硬件加速器的具体细节:我们使用哪些 AI 加速器(例如,GPU/HPU/TPU),每个加速器支持多少内存,能够运行多少 FLOP,它支持什么基础类型(例如,bfloat16/float32)等。
-
硬件加速器之间的通信介质及其支持的带宽
-
多个实例之间的通信介质及其支持的带宽(例如,实例类型是否包括高带宽网络,如Amazon EFA或Google FastSocket)。
-
样本数据摄取的网络带宽
-
整体 CPU 计算能力(通常负责样本数据输入管道)与加速器计算能力之间的比率。
对于 AWS 上 ML 实例类型硬件规格差异的全面详细评审,请查看以下 TDS 文章:
如何选择适合深度学习训练和推理的 Amazon EC2 GPU 实例——从最佳性能到…
towardsdatascience.com
深入了解你使用的实例类型的细节不仅对了解哪些实例类型与你相关很重要,还对理解和克服开发过程中发现的运行时性能问题至关重要。这在我们之前的许多博客文章中得到了证明(例如,这里)。
软件栈支持
你的实例类型搜索的另一个输入应是你正在考虑的实例类型的软件支持矩阵。某些软件组件、库和/或 API 仅支持特定的实例类型。如果你的工作负载需要这些,那么你的搜索范围会更有限。例如,一些模型依赖于为 GPU 构建的计算内核,但不支持其他类型的加速器。另一个例子是 Amazon SageMaker 提供的专用库,该库可以提升多实例训练的性能,但截至本文写作时,只支持有限数量的实例类型(更多细节,请参见这里)。还要注意,一些较新的实例类型,例如基于AWS Trainium的Amazon EC2 trn1实例,对所支持的框架有一定限制。
实例可用性
过去几年经历了长时间的芯片短缺,这导致了 HW 组件的供应下降,特别是 GPU 等加速器。不幸的是,这与近期在大型生成 AI 模型开发中的里程碑带来的需求显著增加相吻合。供需不平衡造成了获取我们选择的机器类型的不确定性。如果我们曾经认为能够随意启动任何类型的机器,现在我们需要适应我们的首选可能完全不可用的情况。
实例类型的可用性是评估和选择它们的重要因素。不幸的是,衡量可用性可能非常困难,预测和规划更是难上加难。实例的可用性可能会突然变化,今天在这里,明天可能就不见了。
请注意,在我们使用多个实例的情况下,除了实例类型的可用性,我们可能还需要它们在同一数据中心的共置(例如,参见这里)。机器学习工作负载通常依赖于实例之间的低网络延迟,它们之间的距离可能会影响性能。
另一个重要的考虑因素是低成本spot实例的可用性。许多云服务提供商从剩余的云服务容量中提供折扣计算引擎(例如,AWS 中的Amazon EC2 Spot Instances,Google Cloud Platform 中的Preemptible VM Instances,以及 Microsoft Azure 中的Low-Priority VMs)。Spot 实例的缺点在于它们可能会被突然中断且几乎没有预警。如果可用,并且你在应用程序中编程了容错,Spot 实例可以显著节省成本。
总结
在这篇文章中,我们回顾了一些关于深度学习工作负载实例类型选择的考虑因素和建议。实例类型的选择对项目成功有关键影响,发现最优实例的过程应相应进行。这篇文章绝非详尽无遗。可能还有甚至是关键的考虑因素未被讨论,这些因素可能适用于你的深度学习项目,并应予以考虑。
过去几年 AI 发展的爆炸性增长伴随着许多新的专用 AI 加速器的出现。这导致了可用实例类型选项的增加,并提供了优化的机会。同时,这也使得寻找最优实例类型变得更加具有挑战性和激动人心。祝你好运 😃!!
将分布式 Ray Serve 部署与 Kafka 集成
原文:
towardsdatascience.com/integrate-distributed-ray-serve-deployment-with-kafka-181403f4e194
学习如何简单地将 Ray Serve 部署与异步 Kafka 消费者结合起来
·发表于 Towards Data Science ·阅读时间 5 分钟·2023 年 7 月 16 日
--
图片由 Midjourney 生成
Ray 是一个现代开源框架,使你能够轻松创建 Python 分布式应用程序。你可以创建简单的训练管道,进行超参数调优,数据处理和模型服务。
Ray 允许你使用 Ray Serve 创建在线推断 API。你可以在一个应用程序中轻松地将多个 ML 模型和自定义业务逻辑结合起来。Ray Serve 自动为你的部署创建 HTTP 接口,负责容错和复制。
Ray 生态系统。来源: docs.ray.io/en/latest/ray-air/getting-started.html
(APACHE 2.0 许可证)
但目前Ray Serve还有一个缺陷。许多现代分布式应用程序通过 Kafka 进行通信,但目前没有现成的方法将 Ray Serve 服务连接到 Kafka 主题。
但不用担心。教会 Ray Serve 与 Kafka 进行通信不会花费太多精力。那么,让我们开始吧。
首先,我们需要准备我们的本地环境。我们将使用带有 Kafka 和 Kafdrop UI 的 docker-compose 文件来启动和探索我们的本地 Kafka 实例(因此我们假设你已经安装了 Docker 和 Docker Compose)。此外,我们还需要安装一些 Python 依赖来完成工作:
所有需求可以通过这个链接下载。
现在我们将创建一个ray-consumer.py文件,并使用Ray 部署进行服务。我不会详细介绍 Ray Serve 的概念,你可以在文档中阅读。基本上,它将普通的 Python 类转换为异步服务Ray 部署,并使用@serve.deployment装饰器:
让我们稍微深入了解一下这段代码。在这里,我们可以看到一个带有多个参数的serve.deployment装饰器。这些参数将被 Ray Serve 用来运行我们的部署。在我们的案例中,Ray Serve 将在本地 Ray 集群上运行一个部署副本(集群也可以存在于AWS 或 Kubernetes)。你可以使用num_replicas参数启动多个部署副本,Ray Serve 将在这些副本之间分配流量。
我们还通过num_cpus和num_gpus参数指定了每个副本将消耗的资源量。例如,当你有一个重型部署且需要特定数量的 CPU 核心来运行时,这些参数非常有用。在我们的案例中,我们不需要太多资源,所以我们将不使用 GPU,仅使用 0.1 CPU。
每次部署启动时,它将获取正在运行的事件循环(有关 asyncio 事件循环的信息可以这里找到),初始化AIOKafkaConsumer,连接到 Kafka(使用topics、bootstrap_servers和group_id参数),并在当前事件循环中启动consume任务。此外,它将把部署健康状态设置为 True。
默认情况下,Serve 控制器会定期对每个 Serve 部署副本进行健康检查,并在失败时重启它。我们通过在RayConsumer部署类中添加一个check_health方法来定义了一个自定义健康检查。健康检查周期由serve.deployment装饰器中的health_check_period_s和health_check_timeout_s参数定义。
Ray Serve 架构。来源 docs.ray.io/en/latest/serve/architecture.html
(APACHE 2.0 许可证)
我们的消费者将继续从 Kafka 获取消息,并将玩具 ML 模型的预测打印到终端,直到 Ray 集群仍然运行。在我们的示例中,我们仅使用玩具模型来模拟 ML 预测,但我们可以包括任何逻辑或 ML 模型推断。我们还可以使用 Ray 对象存储 来存储 ML 模型和 加快加载时间。
如果消费者由于某些随机错误而失败——我们的部署将把健康状态设置为 False,check_health 方法将返回一个错误,Ray Serve 控制器将自动重新启动部署。
不用说,在你开始从 Kafka 消费消息之前,我们需要启动 Kafka 并创建一个存放我们消息的主题。前往你放置 docker-compose.yaml 的文件夹,并在终端中运行以下命令以启动 Kafka 和 Kafdrop:
docker compose up
现在我们可以访问 localhost:9000
并创建一个新的 Kafka 主题。在我们的例子中,我们将其命名为“ray-topic”
Kafdrop 用户界面。图片来自作者
一切准备就绪,我们可以使用终端中的命令启动我们的部署(你应该在包含 ray-consumer.py 的文件夹中运行此命令):
serve run ray-consumer:deployment
这个命令将设置一个本地 Ray 集群并在其上启动 Ray Serve 部署。我们可以在 Ray Dashboard 中检查你的部署状态,通常可以通过这个地址 localhost:8265
访问。
Ray Dashboard。图片来自作者
现在我们可以向 Kafka 发送一些消息,以便我们的部署可以消费它们。我们可以使用一个简单的脚本 aio-producer.py 来实现:
这个脚本将使用 AIOKafkaProducer 将特定消息发送到特定主题,例如这样:
python3 aio-producer.py --message "From Kafka to Ray" --topic "ray-topic"
如果一切正常,我们的 Ray 消费者部署将从 Kafka 获取所有这些消息,并将其打印到终端。
Ray 部署输出。图片来自作者
基本上就是这样。现在你有一个工作中的容错 Ray Serve 部署,它从 Kafka 消费消息。当然,这只是基础,你可以轻松地添加自定义业务逻辑,将它与其他部署组合 或者甚至与 Ray Workflows 结合。你可以在 check_health 方法中添加额外的逻辑,以便你的部署在需要时定期检查额外的服务。
你可以通过 serve.deployment
参数num_replicas将副本添加到你的部署中,甚至可以使用min_replicas和max_replicas参数来设置自动扩展,这样 Ray 将根据流量和负载自动添加副本到你的部署中。
Ray 提供了所有构建可扩展和容错的 Python 应用程序所需的工具,现在了解如何将其与 Kafka 结合,你就能够构建一些真正强大的分布式应用程序。
本文中提到的所有代码可以通过这个链接找到。
使用 Neo4j 和 APOC 将 LLM 工作流与知识图谱集成
OpenAI 和 VertexAI 端点现已作为 APOC 扩展程序提供
·
关注 发布于 Towards Data Science ·8 分钟阅读·2023 年 6 月 7 日
--
或许每天都有新鲜激动人心的事情发生在大型语言模型(LLM)领域。任何公司都有众多机会和应用场景,可以利用 LLM 的力量来提升生产力,转化或处理数据,以及用于对话 AI 和问答系统。
为了让你更容易将 LLM 与知识图谱集成,Neo4j 的团队已经开始了添加对 LLM 集成支持的旅程。这些集成作为 APOC 扩展过程提供。目前支持 OpenAI 和 VertexAI 端点,但我们计划添加更多的支持。
当我在头脑风暴展示新添加的 APOC 过程的酷炫用例时,我的朋友Michael Hunger提出了一个激动人心的想法。如果我们利用图谱上下文或节点的邻域来丰富文本嵌入中的信息怎么办?这样,由于嵌入信息的丰富性增加,向量相似性搜索可能会产生更好的结果。这个想法简单但引人入胜,可能在许多用例中都很有用。
所有代码都可以在GitHub上找到。
Neo4j 环境设置
在这个示例中,我们将同时使用 APOC 和图数据科学库。幸运的是,Neo4j Sandbox 项目已经安装了这两个库,并且附带了一个预填充的数据库。因此,你可以通过几次点击设置环境。我们将使用小型电影项目以避免产生更高的 LLM API 成本。
电影图谱的示例。图像由作者提供。
数据集包含电影和人物节点。只有 38 部电影,因此我们处理的是一个微小的数据集。信息提供了电影的标题和标语、发布日期以及演员或导演。
构建文本嵌入值
我们将使用OpenAI API 端点。因此,如果你还没有 OpenAI 账户,你需要创建一个。
如前所述,这个想法是使用节点的邻域来构建其文本嵌入表示。由于图谱模型简单,我们没有太多的创造性自由。我们将通过使用电影的属性和邻居信息来创建电影的文本嵌入表示。在这种情况下,邻居信息仅包括其演员和导演。然而,我相信这个概念可以应用于更复杂的图谱模式,并用于改进你的向量相似性搜索应用。
利用邻居节点的信息来丰富中心节点的文本嵌入表示。图像由作者提供。图标来自flaticon。
我们现在看到的典型方法是简单地分块并嵌入文档,但当寻找跨越多个文档的信息时,这种方法可能会失败。这种问题也被称为多跳问答。然而,多跳问答问题可以使用知识图谱来解决。看待知识图谱的一种方式是将其视为浓缩的信息存储。例如,可以使用信息提取管道从各种记录中提取相关信息。使用知识图谱,您可以将跨越多个文档的高度连接的信息表示为各种实体之间的关系。
一种解决方案是使用 LLMs 生成 Cypher 语句,以从数据库中检索连接的信息。另一种解决方案是使用连接信息来丰富文本嵌入表示。此外,增强的信息可以在查询时检索,以向 LLM 提供额外的上下文,从而作为其回答的基础。
以下 Cypher 查询可用于从邻居节点中检索所有相关的电影信息。
MATCH (m:Movie)
MATCH (m)-[r:ACTED_IN|DIRECTED]-(t)
WITH m, type(r) as type, collect(t.name) as names
WITH m, type+": "+reduce(s="", n IN names | s + n + ", ") as types
WITH m, collect(types) as contexts
WITH m, "Movie title: "+ m.title + " year: "+coalesce(m.released,"") +" plot: "+ coalesce(m.tagline,"")+"\n" +
reduce(s="", c in contexts | s + substring(c, 0, size(c)-2) +"\n") as context
RETURN context LIMIT 1
查询返回了以下 Matrix 电影的上下文。
Movie title: The Matrix year: 1999 plot: Welcome to the Real World
ACTED_IN: Emil Eifrem, Hugo Weaving, Laurence Fishburne, Carrie-Anne Moss, Keanu Reeves
DIRECTED: Lana Wachowski, Lilly Wachowski
根据您的领域,您可能还会使用自定义查询来检索多跳之外的信息,或者有时需要聚合一些结果。
我们将使用 OpenAI 的嵌入端点来生成表示电影及其上下文的文本嵌入,并将其存储为节点属性。
CALL apoc.periodic.iterate(
'MATCH (m:Movie) RETURN id(m) AS id',
'MATCH (m:Movie)
WHERE id(m) = id
MATCH (m)-[r:ACTED_IN|DIRECTED]-(t)
WITH m, type(r) as type, collect(t.name) as names
WITH m, type+": "+reduce(s="", n IN names | s + n + ", ") as types
WITH m, collect(types) as contexts
WITH m, "Movie title: "+ m.title + " year: "+coalesce(m.released,"") +" plot: "+ coalesce(m.tagline,"")+"\n" +
reduce(s="", c in contexts | s + substring(c, 0, size(c)-2) +"\n") as context
CALL apoc.ml.openai.embedding([context], $apiKey) YIELD embedding
SET m.embedding = embedding',
{batchSize:1, retries:3, params: {apiKey: $apiKey}})
新增的apoc.ml.openai.embedding
过程使使用 OpenAI 的 API 生成文本嵌入变得非常容易。我们用apoc.periodic.iterate
来包装 API 调用,以批处理事务并引入重试策略。
检索增强型 LLMs
看起来主流趋势是查询时向 LLMs 提供外部信息。我们甚至可以找到 OpenAI 的指南,了解如何将相关信息作为提示的一部分来生成答案。
检索增强型 LLMs 的方法。图片由作者提供。
在这里,我们将使用向量相似度搜索来根据用户输入查找相关电影。工作流程如下:
-
我们使用与嵌入节点上下文信息相同的文本嵌入模型来嵌入用户问题
-
我们使用余弦相似度来找到前三个最相关的节点,并将它们的信息返回给 LLM
-
LLM 根据提供的信息构建最终答案
由于我们将使用gpt-3.5-turbo模型来生成最终答案,因此定义系统提示是一个良好的实践。为了使其更具可读性,我们将系统提示定义为 Python 变量,然后在执行 Cypher 语句时使用查询参数。
system_prompt = """
You are an assistant that helps to generate text to form nice and human understandable answers based.
The latest prompt contains the information, and you need to generate a human readable response based on the given information.
Make the answer sound as a response to the question. Do not mention that you based the result on the given information.
Do not add any additional information that is not explicitly provided in the latest prompt.
I repeat, do not add any information that is not explicitly given.
"""
接下来,我们将定义一个函数,该函数根据用户问题和数据库中提供的上下文构建用户提示。
def generate_user_prompt(question, context):
return f"""
The question is {question}
Answer the question by using the provided information:
{context}
"""
在要求语言模型生成答案之前,我们必须定义智能搜索工具,该工具将基于向量相似性搜索提供相关的上下文信息。如前所述,我们需要嵌入用户输入,然后使用余弦相似性来识别相关节点。通过图形,你可以决定你想要检索和提供作为上下文的信息类型。在这个示例中,我们将返回与生成文本嵌入相同的上下文信息,以及类似的电影信息。
def retrieve_context(question, k=3):
data = run_query("""
// retrieve the embedding of the question
CALL apoc.ml.openai.embedding([$question], $apiKey) YIELD embedding
// match relevant movies
MATCH (m:Movie)
WITH m, gds.similarity.cosine(embedding, m.embedding) AS score
ORDER BY score DESC
// limit the number of relevant documents
LIMIT toInteger($k)
// retrieve graph context
MATCH (m)--()--(m1:Movie)
WITH m,m1, count(*) AS count
ORDER BY count DESC
WITH m, apoc.text.join(collect(m1.title)[..3], ", ") AS similarMovies
MATCH (m)-[r:ACTED_IN|DIRECTED]-(t)
WITH m, similarMovies, type(r) as type, collect(t.name) as names
WITH m, similarMovies, type+": "+reduce(s="", n IN names | s + n + ", ") as types
WITH m, similarMovies, collect(types) as contexts
WITH m, "Movie title: "+ m.title + " year: "+coalesce(m.released,"") +" plot: "+ coalesce(m.tagline,"")+"\n" +
reduce(s="", c in contexts | s + substring(c, 0, size(c)-2) +"\n") + "similar movies:" + similarMovies + "\n" as context
RETURN context
""", {'question': question, 'k': k, 'apiKey': openai_api_key})
return data['context'].to_list()
目前,你需要使用gds.similarity.cosine
函数来计算问题和相关节点之间的余弦相似性。在识别相关节点后,我们使用两个额外的MATCH
子句来检索上下文。你可以查看 Neo4j 的 GraphAcademy 以了解更多关于 Cypher 查询语言的内容。
最后,我们可以定义一个函数,该函数接受用户问题并返回答案。
def generate_answer(question):
# Retrieve context
context = retrieve_context(question)
# Print context
for c in context:
print(c)
# Generate answer
response = run_query(
"""
CALL apoc.ml.openai.chat([{role:'system', content: $system},
{role: 'user', content: $user}], $apiKey) YIELD value
RETURN value.choices[0].message.content AS answer
""",
{
"system": system_prompt,
"user": generate_user_prompt(question, context),
"apiKey": openai_api_key,
},
)
return response["answer"][0]
让我们测试一下增强检索的语言模型工作流程。
图片由作者提供
我们可以观察到,工作流程首先检索相关的电影,并使用这些信息生成答案。这里隐藏了一个彩蛋,Emil Eifrem 被认为出演了《黑客帝国》。
让我们再试一个。
图片由作者提供
向量相似性搜索能够检索提到 Jack Nicholson 的电影,并使用这些信息构建答案。如前所述,我们可以从图形中检索未包含在节点文本嵌入生成中的信息。
图片由作者提供
摘要
这就是将大型语言模型与知识图谱集成的迷人世界的一瞥。随着这一领域的不断发展,我们手头的工具和技术也会不断进步。随着 Neo4j 和 APOC 的不断进步,我们可以期待在数据处理和处理方面出现更大的创新。
一如既往,代码可以在 GitHub 上找到。
将 Neo4j 集成到 LangChain 生态系统中。
了解如何开发一个可以通过多种方式与 Neo4j 数据库交互的 LangChain 代理。
·
关注 发表在 Towards Data Science ·15 分钟阅读·2023 年 4 月 17 日
--
图片由 Alex Knight 提供,来源于 Unsplash
更新:所谓的 Cypher 搜索,即 LLM 生成一个 Cypher 语句以查询 Neo4j 数据库,现已直接集成到 LangChain 库中。了解更多 这里
第二次更新:现在 Neo4j 的向量索引直接支持向量搜索,因此我已将向量搜索代码更改为使用 5.11 中引入的新索引
ChatGPT 启发了全球,并引发了新的 AI 革命。然而,最新的趋势似乎是提供 ChatGPT 外部信息,以提高其准确性并使其能够回答公共数据集中不存在答案的问题。关于大型语言模型(LLMs)的另一个趋势是将它们转变为代理,使它们能够通过各种 API 调用或其他集成与环境互动。
由于增强 LLMs 相对较新,目前还没有很多开源库。然而,看起来用于构建围绕像 ChatGPT 这样的 LLMs 的应用程序的首选库被称为 LangChain。该库通过给予其访问各种工具和外部数据源的能力来增强 LLM。它不仅可以通过访问外部数据来改善其响应,还可以作为代理通过外部端点操作其环境。
我偶然发现了由 Ibis Prevedello 开发的 LangChain 项目,该项目通过提供额外的外部上下文来增强 LLMs 的图搜索。
[## GitHub - ibiscp/LLM-IMDB: 使用 LangChain 和 LLMs 从图形中检索信息的概念验证应用…
使用 LangChain 和 LLMs 构建 IMDB 数据集的概念验证应用程序 - GitHub…
github.com](https://github.com/ibiscp/LLM-IMDB?source=post_page-----df0e988344d2--------------------------------)
Ibis 的项目使用 NetworkX 库 来存储图形信息。我非常喜欢他的方法以及将图搜索轻松整合到 LangChain 生态系统中的方式。因此,我决定开发一个项目,将图数据库 Neo4j 集成到 LangChain 生态系统中。
[## GitHub - tomasonjo/langchain2neo4j: 将 Neo4j 数据库集成到 LangChain 生态系统中
Langchain2Neo4j 是一个概念验证应用程序,演示了如何将 Neo4j 集成到 Langchain 生态系统中。这…
github.com](https://github.com/tomasonjo/langchain2neo4j?source=post_page-----df0e988344d2--------------------------------)
编码两周后,项目现在允许 LangChain 代理以三种不同模式与 Neo4j 进行交互:
-
生成用于查询数据库的 Cypher 语句
-
全文关键词搜索相关实体
-
向量相似度搜索
在这篇博客文章中,我将带领您了解我开发的每种方法的推理和实施过程。
环境设置
首先,我们将配置 Neo4j 环境。我们将使用 Neo4j 沙盒中作为 recommendations project 提供的数据集。最简单的解决方案是通过 这个链接 创建一个 Neo4j 沙盒实例。然而,如果你更倾向于使用本地 Neo4j 实例,你还可以恢复一个 在 GitHub 上可用的数据库转储。该数据集是 MovieLens datasets [1] 的一部分,具体是小版本。
Neo4j 数据库实例化后,我们应该拥有一个包含以下模式的图表。
图谱模式。图像由作者提供。
接下来,你需要通过执行以下命令来克隆 langchain2neo4j 仓库:
git clone https://github.com/tomasonjo/langchain2neo4j
在下一步中,你需要创建一个 .env
文件,并按照 .env.example
文件中的说明填写 neo4j 和 OpenAI 凭证。
最后,你需要在 Neo4j 中创建一个全文索引,并通过运行以下命令导入电影标题嵌入:
sh seed_db.sh
如果你是 Windows 用户,seed_db
脚本可能无法正常工作。在这种情况下,我准备了一个 Jupyter 笔记本,可以作为替代 shell 脚本来帮助你初始化数据库。
现在,让我们跳到 LangChain 集成部分。
LangChain 代理
根据我所见,使用 LangChain 代理回答用户问题的最常见数据流如下:
LangChain 代理流程。图像由作者提供。
代理数据流在接收到用户输入时启动。代理随后向 LLM 模型发送一个请求,该请求包含用户问题以及代理提示,代理提示是一组代理应遵循的自然语言指令。接着,LLM 会向代理发送进一步的指令。通常情况下,第一个响应是使用任何可用工具从外部来源获取额外的信息。然而,工具不仅限于只读操作。例如,你可以使用它们来更新数据库。在工具返回额外的上下文后,将再次调用包含新获得信息的 LLM。LLM 现在可以选择生成返回给用户的最终答案,或者决定通过其可用工具执行更多操作。
LangChain 代理使用 LLM 进行推理。因此,第一步是定义使用哪个模型。目前,langchain2neo4j 项目仅支持 OpenAI 的聊天完成模型,特别是 GPT-3.5-turbo 和 GPT-4 模型。
if model_name in ['gpt-3.5-turbo', 'gpt-4']:
llm = ChatOpenAI(temperature=0, model_name=model_name)
else:
raise Exception(f"Model {model_name} is currently not supported")
我还没有探索除 OpenAI 之外的其他 LLM。然而,使用 LangChain 应该很简单,因为它集成了十多种其他 LLM。我不知道竟然存在这么多。
[## 集成
编辑描述
python.langchain.com](https://python.langchain.com/en/latest/modules/models/llms/integrations.html?source=post_page-----df0e988344d2--------------------------------)
接下来,我们需要添加一个对话记忆,使用以下行:
memory = ConversationBufferMemory(
memory_key="chat_history", return_messages=True)
LangChain 支持多种类型的代理。例如,一些代理可以使用记忆组件,而其他的则不能。由于目标是构建一个聊天机器人,我选择了对话代理(针对聊天模型)代理类型。LangChain 库有趣的是,一半的代码用 Python 编写,另一半则是提示工程。我们可以探索对话代理使用的提示。例如,代理有一些必须遵循的基本指令:
Assistant 是一个由 OpenAI 训练的大型语言模型。Assistant 旨在能够协助处理各种任务,从回答简单问题到提供深入解释和讨论广泛话题。作为一个语言模型,Assistant 能够根据接收到的输入生成类似人类的文本,使其能够进行自然流畅的对话,并提供与主题相关的连贯回应。Assistant 不断学习和改进,其能力也在不断发展。它能够处理和理解大量文本,并利用这些知识提供准确和有用的回应。此外,Assistant 能够根据接收到的输入生成自己的文本,从而参与讨论并提供解释和描述。总体而言,Assistant 是一个强大的系统,可以帮助处理各种任务,并提供关于广泛话题的宝贵见解和信息。无论你是需要帮助回答具体问题还是只是想讨论某个特定话题,Assistant 都在这里提供帮助。
此外,代理有指令在需要时使用任何指定的工具。
Assistant can ask the user to use tools to look up information
that may be helpful in answering the users original question.
The tools the human can use are:
{{tools}}
{format_instructions}
USER'S INPUT - - - - - - - - - -
Here is the user's input
(remember to respond with a markdown code snippet of a
json blob with a single action, and NOTHING else):
{{{{input}}}}
有趣的是,提示中指出助手可以要求用户使用工具查找额外信息。然而,用户不是人类,而是构建在 LangChain 库之上的应用程序。因此,整个寻找额外信息的过程是自动完成的,没有任何人类参与。当然,如果需要,我们可以更改提示。提示还包括 LLMs 应该使用的与代理沟通的格式。
请注意,代理提示中没有包括代理在工具返回的上下文中没有提供答案时不应回答问题的内容。
现在,我们只需定义可用的工具。正如所提到的,我准备了三种与 Neo4j 数据库交互的方法。
tools = [
Tool(
name="Cypher search",
func=cypher_tool.run,
description="""
Utilize this tool to search within a movie database,
specifically designed to answer movie-related questions.
This specialized tool offers streamlined search capabilities
to help you find the movie information you need with ease.
Input should be full question.""",
),
Tool(
name="Keyword search",
func=fulltext_tool.run,
description="Utilize this tool when explicitly told to use
keyword search.Input should be a list of relevant movies
inferred from the question.",
),
Tool(
name="Vector search",
func=vector_tool.run,
description="Utilize this tool when explicity told to use
vector search.Input should be full question.Do not include
agent instructions.",
),
]
工具的描述用于指定工具的能力以及通知代理何时使用它。此外,我们需要指定工具所期望的输入格式。例如,Cypher 和向量搜索都期望完整的问题作为输入,而关键字搜索则期望相关电影的列表作为输入。
LangChain 与我在编码中习惯的情况大相径庭。它使用提示来指示 LLMs 为你完成工作,而不是你自己编码。例如,关键字搜索指示 ChatGPT 提取相关电影并使用这些作为输入。我花了 2 小时调试工具输入格式,才意识到可以使用自然语言来指定格式,LLM 会处理剩下的工作。
记得我提到过代理没有指示不回答那些信息未在上下文中提供的问题吗?让我们来看看以下对话。
作者提供的图片。
LLM(大型语言模型)决定基于工具描述,它无法使用任何工具来检索相关的上下文。然而,LLM 默认知道很多东西,由于代理没有约束条件必须依赖外部来源,LLM 可以独立形成答案。如果我们想强制执行不同的行为,我们需要更改代理提示。
生成 Cypher 语句
我已经开发了一个与 Neo4j 数据库交互的聊天机器人,通过生成 Cypher 语句使用 OpenAI 的对话模型,如 GPT-3.5-turbo 和 GPT-4。因此,我可以借用大部分想法来实现一个工具,允许 LangChain 代理通过构造 Cypher 语句从 Neo4j 数据库中检索信息。
像 text-davinci-003 和 GPT-3.5-turbo 这样的旧模型在作为几-shot Cypher 生成器时表现更好,我们提供几个 Cypher 示例供模型生成新的 Cypher 语句。然而,似乎 GPT-4 在仅展示图谱模式时表现良好。因此,由于图谱模式可以通过 Cypher 查询提取,理论上 GPT-4 可以用于任何图谱模式,而无需人工干预。
我不会详细讲解 LangChain 在底层是如何工作的。我们只会查看当 LangChain 代理决定使用 Cypher 语句与 Neo4j 数据库交互时执行的函数。
def _call(self, inputs: Dict[str, str]) -> Dict[str, str]:
chat_prompt = ChatPromptTemplate.from_messages(
[self.system_prompt] + inputs['chat_history'] + [self.human_prompt])
cypher_executor = LLMChain(
prompt=chat_prompt, llm=self.llm, callback_manager=self.callback_manager
)
cypher_statement = cypher_executor.predict(
question=inputs[self.input_key], stop=["Output:"])
# If Cypher statement was not generated due to lack of context
if not "MATCH" in cypher_statement:
return {'answer': 'Missing context to create a Cypher statement'}
context = self.graph.query(cypher_statement)
return {'answer': context}
Cypher 生成工具将问题和聊天历史作为输入。LLM 的输入则结合了系统消息、聊天历史和当前问题。我为 Cypher 生成工具准备了以下系统消息提示。
SYSTEM_TEMPLATE = """
You are an assistant with an ability to generate Cypher queries based off
example Cypher queries. Example Cypher queries are:\n""" + examples + """\n
Do not response with any explanation or any other information except the
Cypher query. You do not ever apologize and strictly generate cypher statements
based of the provided Cypher examples. Do not provide any Cypher statements
that can't be inferred from Cypher examples. Inform the user when you can't
infer the cypher statement due to the lack of context of the conversation
and state what is the missing context.
"""
提示工程感觉更像是一门艺术而非科学。在这个示例中,我们给 LLM 提供了几个 Cypher 语句示例,让它基于这些信息生成 Cypher 语句。此外,我们设置了一些约束,比如只允许构造可以从训练示例中推断出的 Cypher 语句。此外,我们不让模型道歉或解释它的想法(不过,GPT-3.5-turbo 不会听从这些指令)。最后,如果问题缺乏上下文,我们允许模型用这些信息进行回答,而不是强迫它生成 Cypher 语句。
在 LLM 构建 Cypher 语句后,我们简单地使用它查询 Neo4j 数据库,并将结果返回给代理。这里是一个示例流程。
使用 Cypher 生成工具的代理流程。图片由作者提供。
当用户输入问题时,它会连同代理提示一起发送到 LLM。在这个示例中,LLM 反应说它需要使用Cypher 搜索工具。Cypher 搜索工具构造一个 Cypher 语句并用它来查询 Neo4j。查询结果随后传递回代理。接着,代理将另一个请求连同新上下文发送到 LLM。由于上下文包含构造答案所需的信息,LLM 形成最终答案并指示代理将其返回给用户。
当然,我们现在可以提出后续问题。
后续问题。图片由作者提供。
由于代理具有记忆,它知道第二个参与者是谁,因此可以将信息传递给 Cypher 搜索工具,以构造合适的 Cypher 语句。
相关三元组的关键词搜索
我从现有的知识图谱索引实现中获得了关键词搜索的想法,这些实现包括 LangChain 和 GPT-index libraries。这两种实现相当相似。它们要求 LLM 从问题中提取相关实体,并在图中搜索包含这些实体的三元组。因此,我认为我们可以在 Neo4j 中做类似的事情。然而,尽管我们可以使用简单的 MATCH 语句搜索实体,但我决定使用 Neo4j 的全文索引会更好。在使用全文索引找到相关实体后,我们返回三元组,并希望那里有回答问题的相关信息。
def _call(self, inputs: Dict[str, str]) -> Dict[str, Any]:
"""Extract entities, look up info and answer question."""
question = inputs[self.input_key]
params = generate_params(question)
context = self.graph.query(
fulltext_search, {'query': params})
return {self.output_key: context}
请记住,代理程序已经有指示来解析出相关的电影标题并将其作为输入传递给关键词搜索工具。因此,我们不需要处理这个问题。然而,由于问题中可能存在多个实体,我们必须构造适当的 Lucene 查询参数,因为全文索引是基于 Lucene 的。然后,我们只需查询全文索引,并希望返回相关的三元组。我们使用的 Cypher 语句如下:
CALL db.index.fulltext.queryNodes("movie", $query)
YIELD node, score
WITH node, score LIMIT 5
CALL {
WITH node
MATCH (node)-[r:!RATED]->(target)
RETURN coalesce(node.name, node.title) + " " + type(r) + " " + coalesce(target.name, target.title) AS result
UNION
WITH node
MATCH (node)<-[r:!RATED]-(target)
RETURN coalesce(target.name, target.title) + " " + type(r) + " " + coalesce(node.name, node.title) AS result
}
RETURN result LIMIT 100
因此,我们从全文索引中提取出前五个相关实体。接下来,我们通过遍历它们的邻居来生成三元组。我特别排除了RATED 关系,因为它们包含无关的信息。我没有进一步探索,但我有一种好感觉,我们还可以指示 LLM 提供一个待调查的相关关系列表以及适当的实体,这将使我们的关键词搜索更加集中。关键词搜索可以通过明确指示代理程序来启动。
关键词搜索流程。作者提供的图片。
LLM 被指示使用关键词搜索工具。此外,代理程序被告知向关键词搜索工具提供一个相关实体列表作为输入,在这个例子中只有Pokemon。然后使用 Lucene 参数查询 Neo4j。这种方法撒网更广,希望提取的三元组包含相关信息。例如,检索到的上下文包括关于 Pokemon 类型的信息,这些信息并不相关。但它也包含有关谁参演了电影的信息,这使代理程序能够回答用户的问题。
如前所述,我们可以指示 LLM 生成一个相关关系类型的列表以及适当的实体,这可以帮助代理程序检索到更相关的信息。
向量相似度搜索
向量相似度搜索是我们将要检查的与 Neo4j 数据库交互的最后一种模式。向量搜索目前非常流行。例如,LangChain 提供了与十多个向量数据库的集成。此外,Neo4j 在其 5.11 版本中增加了一个向量索引,我们将在这个例子中使用。向量相似度搜索的核心思想是将问题嵌入到嵌入空间中,并根据问题和文档嵌入的相似性来查找相关文档。我们只需要小心使用相同的嵌入模型来生成文档和问题的向量表示。我在向量搜索实现中使用了 OpenAI 的嵌入。
def _call(self, inputs: Dict[str, str]) -> Dict[str, Any]:
"""Embed a question and do vector search."""
question = inputs[self.input_key]
embedding = self.embeddings.embed_query(question)
context = self.graph.query(
vector_search, {'embedding': embedding})
return {self.output_key: context}
所以,我们做的第一件事是将问题嵌入。接下来,我们使用嵌入在数据库中查找相关电影。通常,向量数据库会返回相关文档的文本。然而,我们处理的是图数据库。因此,我决定使用三元组结构生成相关信息。使用的 Cypher 语句是:
WITH $embedding AS e
CALL db.index.vector.queryNodes('movies',5, e) yield node as m, score
CALL {
WITH m
MATCH (m)-[r:!RATED]->(target)
RETURN coalesce(m.name, m.title) + " " + type(r) + " " + coalesce(target.name, target.title) AS result
UNION
WITH m
MATCH (m)<-[r:!RATED]-(target)
RETURN coalesce(target.name, target.title) + " " + type(r) + " " + coalesce(m.name, m.title) AS result
}
RETURN result LIMIT 100
Cypher 语句类似于关键字搜索示例。唯一的区别是我们使用向量索引,而不是全文索引来识别相关电影。
向量搜索流程。图片由作者提供。
当指示一个代理使用向量搜索工具时,第一步是将问题嵌入为一个向量。OpenAI 的嵌入模型生成的向量表示的维度为 1536。因此,下一步是使用构建的向量,并通过向量索引在数据库中搜索相关信息。同样,由于我们处理的是图数据库,我决定将信息以三元组的形式返回给代理。
向量搜索有趣之处在于,即使我们指示代理搜索指环王系列电影,向量相似性搜索也返回了霍比特人系列电影的信息。这看起来指环王和霍比特人电影在嵌入空间中是接近的,这也是可以理解的。
总结
看起来,能够访问外部工具和信息的聊天机器人和生成代理是继原始 ChatGPT 热潮之后的下一个浪潮。具备为大型语言模型提供额外上下文的能力可以大大改善其结果。此外,代理的工具不仅限于只读操作,这意味着它们可以更新数据库甚至在亚马逊上下订单。大部分时间,LangChain 库似乎是目前用于实现生成代理的主要库。当你开始使用 LangChain 时,你可能需要稍微调整编码过程,因为你需要将大型语言模型提示与代码结合起来完成任务。例如,LLM 与工具之间的消息可以通过自然语言指令作为提示进行塑造和重塑,而不是 Python 代码。我希望这个项目能帮助你将像 Neo4j 这样的图数据库的功能融入到你的 LangChain 项目中。
一如既往,代码可以在 GitHub 上找到。
参考文献
[1] F. Maxwell Harper 和 Joseph A. Konstan. 2015. The MovieLens 数据集:历史与背景。ACM 交互式智能系统交易(TiiS)5, 4: 19:1–19:19. https://doi.org/10.1145/2827872
神经网络的应用:推导正态分布的累积分布函数
使用神经网络进行函数积分(含代码)
·
关注 发表在 Towards Data Science ·6 分钟阅读·2023 年 5 月 3 日
--
图片由 Jack Anstey 提供,来自 Unsplash
1. 引言
本文介绍了一种训练神经网络以推导函数积分的方法。该技术不仅适用于解析解积分,还适用于没有封闭形式解且通常通过数值方法求解的积分。例如,正态分布的累积分布函数(CDF)。方程 1 是该分布的概率密度函数(PDF),方程 2 是其 CDF,即 PDF 的积分。图 1 是这些函数的图示。训练后,生成的网络可以作为一个独立的函数生成器,提供在 CDF 曲线上给定 PDF 分布域中的点。
方程 1:概率密度函数(PDF)(u=0,𝜎=1)
方程 2:累积分布函数(CDF)
图 1. 正态分布的 PDF 和 CDF
2. 积分神经网络
训练一个积分神经网络,以产生函数 y = f(x) 的积分。用网络的输入和输出表示:
方程 3
其中 h 和 x 分别是网络的输出和输入。对于正态分布,f(x) 由方程 1 给出,即该分布的 PDF。
通过训练神经网络实现函数的积分,使得 网络输出的导数等于函数的输出,从而使网络输出成为函数的积分。
2.1 神经网络训练
训练程序的步骤如下:
- 将一个训练点 xᵢ 应用到函数 y = f(x):
方程 4
2. 还将 xᵢ 应用到神经网络的输入:
(神经网络模型包括一个输入 x,两个隐藏层和一个输出 h,表示为 h(x) = nn_model(x)。)
方程 5
3. 求取 hᵢ 的导数:
方程 6
(在 TensorFlow 和 PyTorch 中,通过其自动微分功能提供微分。在本文中,神经网络使用 TensorFlow GradientTape 开发。)
4. 使用损失函数(第 2.2 节中的损失 2)训练神经网络,以强制以下关系:
方程 7
然后在神经网络训练后,由于 g = y,并且将方程 6 和方程 4 中的 g 和 y 代入:
方程 8
对方程 8 两边进行积分,确认神经网络的输出是函数 f(x) 的积分:
方程 9
其中 C 是积分常数。
2.2 神经网络损失函数
通常,神经网络是通过已知输入和输出数据对进行训练的。训练输入数据呈现给神经网络,结果输出与训练输出数据通过损失函数进行比较。这个函数返回的损失用于通过反向传播调整网络权重,以减少损失。整合神经网络使用自定义损失函数来约束神经网络产生与积分函数输出一致的输出。
整合神经网络的损失函数,如图 2 所示,有三个组成部分。损失函数 2(在上文训练程序第 2.1 节中描述)强制神经网络的输出符合f(x)的积分。
损失函数 3 迫使神经网络遵守初始条件h(x_init2) = h_init2。对于 CDF 模型,这一条件为h(−10) = 0,这设置了C = 0(方程 9)。对于该模型,x = −10时 PDF 和 CDF 的响应近似于x = −∞时的响应。
在损失函数 3 中设置初始条件为h(−∞) = 0,也简化了 CDF 计算。展开方程 2 的定积分:
方程 10
初始条件h(−∞) = 0,意味着第二项为零,训练后的神经网络输出为对应x输入的 CDF 值:
方程 11
损失函数 1,条件为h(10) = 1,稳定了训练过程,尤其是对分布右尾附近的点。对于该模型,x = 10时 PDF 和 CDF 的响应近似于
x = ∞。
图 2. 损失函数
3. 神经网络实现的整合
以下是实现正态分布 CDF 的整合神经网络的 Python 代码。完整代码可在这里获取。
3.1 神经网络模型定义
神经网络有两个全连接的隐藏层,每个层有 512 个神经元。输入为域点,输出为对应的积分值。
列表 1. TensorFlow 神经网络模型
3.2 初始化
损失函数 2 中第 2.1 节的xᵢ训练点在下面的第 9 行中定义。这些点的顺序在第 11 行被随机打乱,以促进神经网络的稳定训练。在第 12 行,这些点被应用于 PDF,如方程 4 所述。
损失函数 1 和损失函数 3 的初始条件分别在第 15-16 行和第 19-20 行中定义。
列表 2. 初始化
3.3 批量训练步骤
列表 3 是应用于每个训练批次的训练步骤函数。总损失(损失 1、损失 2 和损失 3 的总和)在第 24 行用于通过梯度下降更新神经网络的权重(第 26 - 30 行)。每个训练周期包括多个批次,这些批次共同使用所有训练点进行模型更新。
第 9 行生成网络对 x_init 初始条件的响应。该响应与相应的初始条件 h_init 进行比较,产生损失 1(第 10 行)。
同样地,第 13 行生成网络对 x_init2 初始条件的响应。该响应与相应的初始条件 h_init2 进行比较,产生损失 3(第 14 行)。
第 17 行生成网络对训练点 xᵢ(方程 5)的响应。第 18 行提取响应的梯度(方程 6),第 19 - 20 行将梯度与 f(xᵢ)(方程 7)进行比较,产生损失 2。
列表 3. 批量训练步骤
4. 结果
图 3 展示了训练好的神经网络输出的累积分布函数(CDF)响应(红色轨迹)。为了验证结果的准确性,包含了 Python SciPy 库中 norm.cdf 函数的 CDF 响应(绿色点)。
图 3. 训练后的 CDF 神经网络输出
图 4 是训练过程中记录的总损失函数随 epoch 的变化。
图 4. 训练损失
5. 结论
本文展示了一种使用自定义损失函数和自动微分训练神经网络积分函数的方法。具体来说,训练神经网络成功地积分正态分布的概率密度函数(PDF)以生成累积分布函数(CDF)。
即将发表的一篇文章将介绍一种训练神经网络反转函数的方法。反转网络将用于反转本文中训练的 CDF 网络的输出,然后从正态分布生成样本。
本文的 pdf 可在此处 下载。**
除非另有说明,所有图片均由作者提供。
与大型语言模型的互动
丰富提示以引导模型 — 为非专家解释
·
关注 发表在 Towards Data Science · 10 min 阅读 · 2023 年 5 月 24 日
--
像 GPT 这样的语言模型在解决复杂任务如翻译、推理或回答问题方面展现了惊人的能力。然而,为了达到这一点,这些模型必须以正确的方式进行查询;这是一种称为提示的技术。了解如何与语言模型互动对于最大化其效果至关重要,而编写合适的提示则可能是一项巨大的挑战。
在这篇文章中,我想概述不同的提示技术,这些技术使语言模型能够更准确地解决任务。我将首先解释语言模型的基本功能,然后介绍一些最常用的提示技术,即零-shot 和 few-shot 提示。之后,我将解释思维链提示,一种增强语言模型能力的机制,并展示如何将这些与使用外部工具相结合,以解决更复杂的任务。
语言模型与提示
提示是对语言模型的指令。照片由 Markus Winkler 拍摄,来源于 Unsplash。
语言模型的基本原理可以用一句话来解释:语言模型是一个根据上下文预测下一个词的模型。也就是说,给定一个句子如“猴子吃了”,语言模型计算不同词的后续概率。在这个例子中,“香蕉”的概率可能很高,“苹果”也可能相当高,但“钢琴”的概率则相对较低。因此,模型可能会将句子完成为“猴子吃了香蕉”。
有不同种类的模型以不同的方式估计这些概率,每种都有其独特的优点和缺点。如果你想了解一些重要的技术,可以查看 这篇概述。要理解本文,你只需知道语言模型总是根据一系列词预测下一个词。
为了与语言模型交互,你使用提示。提示就是你给模型继续的文本。因此,在上述例子中,“猴子吃了香蕉”是提示。提示不仅限于单句,它可以更长,包括多句或段落。由于技术原因,提示长度需要有一个限制,通常在几百或几千个词的范围内。根据你的提示,你可以让模型完成不同的任务,如回答问题、翻译成其他语言等等。让我们探索一些创建有意义提示的方法,这些提示告诉模型你对它的期望。
Zero-shot 提示
我们从简单的开始。照片由 Jan Kahánek 拍摄,来源于 Unsplash。
使用大型语言模型的最简单方法可能是零-shot 提示。主要思想是构造一个提示,使得完成提示能够给出所需的答案。一个简单的例子:
美国总统是 …
当语言模型通过预测下一个词来完成这个句子时,它可能会创建句子“美国总统是乔·拜登”,并通过这样做回答了问题。
然而,这种方法也有其局限性,因为有时很难以这种结构清晰而明确地制定问题。例如,如果我们拿这个句子
乔·拜登是 …
我们可能期望模型将句子完成为“乔·拜登是美国总统”。然而,这个句子还有许多其他合理的延续方式,比如“乔·拜登是民主党的一名政治家”。这是一个真实的陈述,根据我们的提示,无法确定哪一个答案更符合我们的期望。那么,我们如何才能更清晰地制定提示呢?
少量示例提示
更多的示例可以丰富提示。照片由 ron dyar 提供,来源于 Unsplash
在提示中,我们可以给出一些我们期望的示例,这是一种叫做少量示例提示的技术。考虑以下示例:
贾斯廷·特鲁多:加拿大
瑞希·苏纳克:英国
奥拉夫·舒尔茨:德国
乔·拜登:
显然,这个提示中编码的模式是政治家与他们所领导国家之间的联系。给定这个提示,我们期望模型继续回答为“乔·拜登:美利坚合众国”。在另一个使用相同格式的提示中,我们可能会关注不同的方面:
贾斯廷·特鲁多:51
瑞希·苏纳克:43
奥拉夫·舒尔茨:64
乔·拜登:
这里我们讨论政治家的年龄,因此期望的延续是“乔·拜登:80”。
如前所述,这叫做少量示例提示,因为你提供了一些示例(shots),告诉模型该做什么。这有助于更详细地描述期望的任务,从而导致语言模型提供更精确的答案。
思考链
这是一个链条,因为这是一个接一个的思考。好吧,我想你应该在没有我解释的情况下明白这一点。照片由 Karine Avetisyan 提供,来源于 Unsplash
到目前为止,我们的提示仅包括我们希望模型回答的任务。然而,我们还可以使用提示给模型额外的指导,突出重要的方面或限制其回答。例如,为了避免错误答案,我们可以从
回答以下问题。如果你没有所需的信息,请回答‘我不知道’。
随后是少量示例。这使得模型承认它不知道答案,而不是编造错误的答案。
提示的另一个有用扩展被称为思维链提示。主要思想是鼓励模型明确地表达它在完成任务时必须执行的中间步骤。我们通过简单地提供包含这些步骤或推理痕迹的少量示例来实现这一点,这被称为思考。正如你可能猜到的,这个想法受到人类在将任务拆分为子任务并对其进行推理时的思维方式的启发。假设你有如下任务:
问:莎莉买了 18 个苹果来做苹果派。她烤了 2 个饼,还剩下 4 个苹果。每个饼里有多少个苹果?
你会怎么做才能得出答案?我敢打赌你首先算出她使用了 14 个苹果(18 个买的减去 4 个剩下的),然后把 14 个苹果除以两个饼,得到每个饼 7 个苹果。对你来说这可能听起来很简单,但你是否曾教过孩子解决这样的任务?如果你只是给它任务和答案(7),它不会理解你是如何得出这个答案的。然而,如果你将你所做的步骤表达出来,孩子可以理解并将这些步骤转移到其他任务中,只要它能够进行必要的算术运算。同样,在提示语言模型时,你可以在少量示例中包含思考。对于上述任务,一个示例可能如下所示:
问:莎莉买了 18 个苹果来做苹果派。她烤了 2 个饼,还剩下 4 个苹果。每个饼里有多少个苹果?
思考:如果她剩下 4 个苹果,她已经使用了 18–4=14 个苹果。如果她把 14 个苹果分配到两个饼里,每个饼里有 14/2=7 个苹果。
答:每个饼包含 7 个苹果。
如果所有示例都像这样,模型不会直接生成答案,而是会先生成思考,这有两个大的优点:
首先,这会导致更好的答案。由于模型被迫一步一步地进行,它可以为每个子任务利用更多资源,而不是一次性解决整个任务。当你进行复杂计算时,你也会记录下中间结果,不是吗?
其次,它有助于理解模型的预测和它所犯的错误。如果你只是得到一个错误的答案,很难找出模型出现问题的原因。然而,这些思考可以给你对模型推理过程的有意义的洞察,使你能够更详细地理解失败的原因。假设模型产生了以下思考:
如果她剩下 4 个苹果,她已经使用了 18–4=14 个苹果。如果她把 14 个苹果分配到两个饼里,每个饼里有 14/2=9 个苹果。
在这个示例中,你可以发现,模型执行了正确的步骤,但只是最后一步计算错误。在其他情况下,你可能会看到模型没有理解任务,这可能会鼓励你重新表述任务。
添加动作
更多工具允许更多操作。照片来自 Eka Cahyadi Hasti 于 Unsplash
通过思考链,我们赋予了模型将复杂任务拆分为更简单的子任务,并逐步解决这些子任务的能力。然而,这些子任务必须由模型自己完成,依赖于它已经掌握的技能。如果模型无法解决任何一个任务,整个思考链将结束,即使链条本身是正确的。这极大地限制了它能够完成的任务。你可以将其与人类解决任务时仅凭记忆中的知识进行比较。上面的例子你可以通过使用你记住的基本数学技能来解决,但现实世界中的大多数问题并不那么简单。假设你想回答以下问题:
谁更年长?安德鲁·斯科特还是本尼迪克特·康伯巴奇?
确定回答问题所需的步骤是相当简单的。首先,你需要研究安德鲁·斯科特和本尼迪克特·康伯巴奇的出生日期,然后检查哪个日期更早。然而,你不能仅凭思考来做到这一点,因为你不记得他们的生日(除非你是地球上最大的福尔摩斯粉丝)。为了实际执行这些任务,你需要帮助,例如通过咨询维基百科。
相比之下,如果你为语言模型提供额外的工具,它会变得更加熟练。像 Langchain 这样的框架可以做到这一点。假设你提供了维基百科 API,那么你可以构建一些利用该 API 的少量示例:
问:谁更年长?安德鲁·斯科特还是本尼迪克特·康伯巴奇?
思考:我需要找出安德鲁·斯科特的出生日期。
行动:query_wikipedia(search_term=’Andrew Scott’)
观察:“安德鲁·斯科特(Andrew Scott)(1976 年 10 月 21 日出生)是一位爱尔兰演员……”
思考:我知道安德鲁·斯科特的出生日期了。现在我需要找出本尼迪克特·康伯巴奇的出生日期。
行动:query_wikipedia(search_term=’Benedict Cumberbatch’)
观察:“本尼迪克特·提莫西·卡尔顿·康伯巴奇 CBE(Benedict Timothy Carlton Cumberbatch CBE,1976 年 7 月 19 日出生)是一位英国演员……”
思考:我知道安德鲁·斯科特(Andrew Scott)出生于 1976 年 10 月 21 日,而本尼迪克特·康伯巴奇(Benedict Cumberbatch)出生于 1976 年 7 月 19 日。康伯巴奇的出生日期更早。因此,他更年长。
答:本尼迪克特·康伯巴奇比安德鲁·斯科特年长。
正如你所看到的,思维总是紧跟着行动。这个行动被执行后导致一个观察,这个观察影响下一个思维。从这样的例子中,模型学会执行导致所需结果的行动,这些行动是解决初始任务的途径。你为模型提供的行动可以包括从 API 调用中检索数据到对数据库进行更改,或打开你远程控制的窗户。模型甚至可以请求另一种 AI 来完成一些子任务。正如你所见,这大大扩展了模型能够解决的任务,因为它开启了超越语言本身范围的子任务解决新途径。
摘要
在这篇文章中,我解释了如何使用提示来制定语言模型的任务,甚至赋予它更多的技能和能力,而无需重新训练模型。导致更复杂和强大提示的主要思想可以总结如下:
-
简单的零样本提示仅为语言模型制定任务。
-
少样本示例可以更详细地指定任务,并向模型提供解决任务的示例。
-
鼓励模型产生思维链可以让其解决复杂任务,并且以一种直观的方式让模型自我解释。
-
添加动作为模型提供了解决其自身无法解决的子任务的工具,从而允许处理更复杂的任务。
然而,为语言模型配备工具不必是终点。一个自然的下一步可能是促进模型的自动化,使其通过选择相关任务来追求目标,正如 Baby AGI 所做的那样。你能想到其他什么下一步?
进一步阅读
以下论文介绍了本文中突出显示的主要概念。
Few-shot 提示是 GPT3 模型的核心基础之一:
- Brown, T., Mann, B., Ryder, N., Subbiah, M., Kaplan, J. D., Dhariwal, P., … & Amodei, D. (2020). 语言模型是少样本学习者。神经信息处理系统进展, 33, 1877–1901。
本文介绍了思维链推理:
- Wei, J., Wang, X., Schuurmans, D., Bosma, M., Chi, E., Le, Q., & Zhou, D. (2022). 思维链提示在大型语言模型中引发推理。arXiv 预印本 arXiv:2201.11903。
ReAct 是一种将思维链推理与行动相结合的突出方法:
- Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K., & Cao, Y. (2022). ReAct:在语言模型中协同推理与行动。arXiv 预印本 arXiv:2210.03629。
喜欢这篇文章吗? 关注我 以便获取我未来的帖子。
Excel 中的互动仪表盘
原文:
towardsdatascience.com/interactive-dashboards-in-excel-4f92e2ee6494
使用先锋的数据分析软件
·发布于 Towards Data Science ·阅读时长 8 分钟·2023 年 10 月 5 日
--
这篇文章与我通常写的以开发为中心的文章有所不同。然而,最近我有幸与一个家庭合作,提高他们的数字素养,我们决定讨论的一个话题是 Excel。
我曾作为全职数据分析师工作了好几年。近年来,这个职位的定义有所模糊,因为许多不同的角色都需要处理、获取见解并展示数据。即使今天作为一个 DevSecOps 工程师,我仍然需要做到这一点,以确保我管理的所有系统都能有效且高效地运行。
在我看来,Excel 是“非技术”领域中最先驱且最广泛使用的数据工程与分析软件。这里学到的数据分析与可视化技能可以轻松转移到任何领域和任何软件,只要我们建立了坚实的基础。
图片来自 Unsplash,作者 Lukas Blazek
本教程是使用 Mac Excel v16.77.1 创建的。与早期或晚期版本可能会有一些功能和外观上的小差异。
结果
这次练习的结果是制作一个与下方相同的互动仪表盘。很酷,对吧?
在本篇文章结束时你将学到的内容。(图片来源:作者)
超市数据集
首先,下载 Tableau 网站上的美国超市数据集。这是一个虚构的公司,数据包括他们的产品、销售和利润,供我们使用。
文件的格式是.xls
。我们需要将其转换为最新的.xlsx
格式,以便某些功能能够正常工作。要做到这一点:
-
打开
sample_-_superstore.xls
电子表格,你会看到三个标签页,Orders、Returns 和 People。现在复制Orders工作表的所有内容。我们不需要其他的。 -
创建一个全新的空白 Excel 电子表格,并将订单内容粘贴到其中。保存它。默认情况下,它应该是
xlsx
格式,除非你使用的是非常旧的 Excel 版本。
1) 数据透视表
接下来,我们将创建 2 个数据透视表来计算总利润和总销售额。这是一种方便的工具,用于在数据字段之间进行分析和计算。数据透视表及其其他透视变体是使仪表板中各部分能够互相交互的必要条件。
创建数据透视表(作者提供的图片)
为此,点击插入 > 数据透视表 > 确定以接受默认值。这将创建一个包含数据透视表的新工作表。
使用数据透视表计算总和(作者提供的图片)
在新工作表中,右侧会弹出一个窗口来定制数据透视表的计算方式。我们只需要计算利润的总和,因此我们将利润
字段拖到值
区域。它会自动假设计算总和。
现在重复完全相同的步骤,使用销售
字段来获取销售总和。
2) 数据透视图(折线图)
现在是时候创建显示利润和销售月度趋势的折线图数据透视图了。转到插入 > 数据透视图,在订单工作表中。
创建数据透视图(作者提供的图片)
类似于数据透视表,我们选择所需的字段。将销售
字段拖到值区域,将订单日期
字段拖到轴区域。你可能会看到年份和季度字段自动添加,因为 Excel 识别订单日期
为日期格式。移除这些字段,然后应该显示出按月销售额的柱状图,如下所示。
数据透视图(作者提供的图片)
现在让我们将其转换为折线图。点击图表,然后转到设计 > 更改图表类型 > 折线 > 选择折线图。
将柱状图更改为折线图(作者提供的图片)
现在将此数据透视图工作表标记为“月度销售”。
为工作表命名(作者提供的图片)
现在重复完全相同的步骤来获取按月利润的折线图。
3) 数据透视图(柱状图)
现在让我们使用数据透视图为每个产品细分市场创建两个柱状图,一个显示利润,另一个显示销售额。轴字段应为细分市场
,值字段应为利润
。
创建按细分市场的利润(作者提供的图片)
我们希望柱状图水平显示,所以我们来更改图表类型。
更改为水平柱状图(作者提供的图片)
最后,将新工作表标记为按细分市场利润,以保持整洁。
现在重复相同的步骤,获取按细分市场的销售柱状图。
4) 地图
没有“透视图”这一术语。一个替代方案是首先创建一个行使用States
字段和使用Profit
值的透视表,然后将数据链接到地图。
在那个工作表中,选择一个空白单元格,然后在插入 > 地图 > 填充地图中创建一个空白地图。
添加一个空白地图可视化(作者图片)
选择地图后,转到图表设计 > 选择数据 > 图表数据范围 > 选择包含美国州名及其对应利润的两个列单元格。地图可视化将自动识别州名并相应填充地图。
为地图填充数据点(作者图片)
现在,将此工作表重命名为“地图”。
5) 集成到仪表板
现在是将所有内容整合到一个仪表板中的时候了。
-
创建一个名为仪表板的新工作表。
-
对于包含透视表的两个工作表,将两个标题和每个工作表的总值剪切并粘贴到新的仪表板工作表中。
-
对于包含透视图表和地图的 5 个工作表,将它们从每个工作表剪切并粘贴到新的仪表板工作表中。
将所有透视表和图表整合到一个工作表中(作者图片)
6) 切片器
切片器是过滤我们在仪表板中刚创建的图表和数值的对象。这是互动的关键。主要有两个关键组件:
-
为透视表创建切片器以过滤特定字段
-
将切片器连接到其他透视表
选择一个透视表或图表。在透视表(图表)分析 > 插入切片器 > 选中Region
、Year
和Ship Mode
字段。
创建切片器(作者图片)
现在,选择一个切片器,点击报表连接。在这里,检查所有列出的透视表。注意,你所有的透视图表和地图也通过透视表连接,所以这将把它们连接在一起。
将切片器连接到其他透视表(作者图片)
切片器的一个旁注是,它们有时会动态修改单元格宽度。为了解决这个问题,选择利润和销售总和的透视表 > 选项 > 取消选中“更新时自动调整列宽”。
防止切片时单元格宽度动态变化(作者图片)
就是这样!你已经拥有了一个互动仪表板!尝试点击切片器的过滤器,查看所有图表和数值如何一起变化。
7) 格式化
这一部分较长,重点关注仪表板的美观性和可读性。然而,如果我们希望它吸引观众的注意力,这是一项最重要的技能。
现在让我们使地图颜色更直观,因为有些州的利润数字是负数。
-
双击图表,右侧会出现“格式化数据系列”面板。点击“系列选项”图标标签。
-
对于渐变样式,选择发散型(3 种颜色)
-
对于最小值,将颜色更改为红色
-
对于中点,将其更改为数字,值为 0,颜色为白色
设置地图的热力图(作者提供的图片)
现在我们转到利润和销售总和部分。我们不需要标题,但由于无法移除数据透视表标题,因此用破折号 -
替换它。然后增加字体大小,并将数字格式转换为货币格式,不带小数点。
格式化销售和利润的总和(作者提供的图片)
标题以垂直文本的形式添加在侧边。可以通过右键单击单元格 > 格式单元格 > 对齐选项卡 > 拖动文本对齐方式或输入角度来实现垂直显示。然后将文本颜色更改为绿色。
对销售总额重复这些步骤,但使用蓝色。
垂直文本对齐(作者提供的图片)
对于月度利润的折线图,我们只需要一个简单的趋势线,没有标题、坐标轴和图例(即 spark-line)。点击每个组件并将其删除。通过选择外部图表区域并选择 无边框 来移除图表边框,如下所示。我们还希望更改线条颜色,可以通过类似的方法,但这次选择图表线条并更改线条颜色。
移除图表边框(作者提供的图片)
对于月度销售图表,我们进行相同的清理,但保留 x 轴标签。通过移除坐标轴线并旋转标签来格式化坐标轴。
格式化 x 轴(作者提供的图片)
最后的可视化是柱状图。我们按照惯例清理图表,移除不必要的图表组件。然后,通过修改间距宽度来增加柱子的宽度。
增加柱子的宽度(作者提供的图片)
接下来,在每个柱子内添加数据标签。之后还可以修改数字格式和字体大小。与折线图一样,移除边框,并更改柱子的颜色。
为每个柱子添加数据标签(作者提供的图片)
我们差不多完成了!现在调整每个图表的大小和标题。目前唯一不雅的地方是网格线。
几乎完成的仪表板(作者提供的图片)
可以通过查看 > 取消选中网格线轻松移除。
移除网格线(作者提供的图片)
总结
这是一篇包含很多步骤的长帖。然而,我们只需掌握几个主要概念,便可以轻松在 Excel 中开发自己的交互式仪表板。
-
仪表板由切片器、数据透视表、数据透视图或其他可视化如地图等组成,全部位于一个工作表中。
-
所有可视化必须连接到数据透视表,以便切片器正常工作。
-
切片器通过连接到数据透视表并一次性过滤所有值,提供互动功能。
Excel 仪表板的组成部分(图片由作者提供)
链接
- 超市数据集来自 Tableau:
public.tableau.com/app/learn/sample-data
中级深度学习与迁移学习
原文:
towardsdatascience.com/intermediate-deep-learning-with-transfer-learning-f1aba5a814f
实用指南,用于调整深度学习模型以进行计算机视觉和自然语言处理
·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 2 月 22 日
--
作者提供的图像
入门深度学习很简单。你只需几行代码就可以设置并训练一个神经网络。然而,当你从初学者进阶到中级水平时,可能会感到不知所措。你会遇到许多新术语,比如“EfficientNet”或“DeBERTa”。这些术语是什么意思?它们与神经网络有什么关系?
初学者教程没有告诉你,实际上只有少数人从头开始训练神经网络。这是因为神经网络在足够大的数据集上进行训练需要大量的时间和计算资源。因此,使用预训练模型作为起点是一种常见的做法。这种做法称为迁移学习 [2]。
初学者教程没有告诉你,实际上只有少数人从头开始训练神经网络。
前提条件
本文将指导你从初学者成长为中级深度学习领域的专家。因此,本文假设你已经对深度学习概念有一些基本了解。
免责声明
本文灵感来源于我最近接触的四个关于深度学习最佳实践的资源,并将其关键点总结成一篇文章。本文中的原始观点并非我所创 — 可以将这篇文章视为学习笔记。
-
V. Godbole, G. E. Dahl, J. Gilmer, C. J. Shallue 和 Z. Nado:深度学习调整手册 [4](格式:Git 仓库)
-
S. Bhutani 和 H20.ai:机器学习模型训练最佳实践 [1](视频格式)
-
P. Singer 和 Y. Babakhin:深度迁移学习的实用技巧 于 2022 年 11 月在 Kaggle Days 巴黎展示 [11](演示文稿格式)
-
D. Kłeczek 与慕尼黑 NLP:提高你的 NLP 模型训练的十种有效技巧 [9](视频格式)
迁移学习简要介绍
迁移学习描述了重新使用预训练神经网络而不是从头训练网络以节省时间和计算资源的实践。因此,我们将预先学习的权重和知识从一个任务转移到另一个任务。预训练模型也被称为骨干网络。
迁移学习(图片由作者提供)
当你的数据集较小或与骨干网络训练的数据集相似时,迁移学习是实用的 [2]。但即使你的数据集足够大或你的任务不同于骨干网络训练的任务,使用迁移学习也无妨 [8],因为通常,前几层包含通用信息。
尽管你可以保持转移模型的权重并仅重新训练分类器,但通常会微调整个模型的权重。
当前计算机视觉(CV)问题的一些流行骨干网络包括:
-
ResNet(残差网络) [6]
-
DenseNet [7]
-
EfficientNet [12]
分别,针对自然语言处理(NLP)问题的一些流行骨干网络有:
-
BERT(双向编码器表示从变换器) [3]
-
RoBERTa(强健优化 BERT 预训练方法) [10]
-
DeBERTa(解码增强 BERT 与解耦注意力) [5]
当然,迁移学习的可能性仅仅因为研究人员共享他们的模型检查点以造福他人 [2]。
处理 CV 或 NLP 问题的主要步骤包括:
-
建立基线
-
逐步增加复杂性和提高性能
-
挤压最后一点性能
第一步:建立基线
作为第一步,你应该建立一个基线,用于比较你在第二步中进行的任何实验。你不应该在这一步花费太多时间。只需确保你有一个足够好的工作基线。
骨干网络
骨干网络描述了你作为起点使用的预训练模型。不要花太多时间选择完美的骨干网络。你应该在第二步中更换骨干网络并尝试其他网络(见模型复杂性)——所以只需选择一个即可。如何选择 EfficientNet 用于 CV 问题和 DeBERTa 用于(英语)NLP 问题 [1, 9, 11]?
批量大小
在模型开发的这一阶段决定一个批量大小。除非必要,否则不要改变批量大小,因为这需要重新开始调优过程 [4, 11]。
通常,增加批量大小会提高训练速度。因此,常见做法是使用内存中最大可容纳的 2 的幂次的批量大小(例如 16, 32, 64 等)[4]。
训练步骤数量(轮次)
训练轮次 — 微调预训练模型时不需要很多额外的训练步骤。通常,5 个轮次是一个好的起点作为初始基线[11]。
提前停止 — 尽管提前停止在过去很受欢迎[5],但现在不再如此[1, 4, 11]。提前停止是一种技术,当验证指标不再改善时停止训练,以避免对训练数据的过拟合。然而,这种技术可能导致你对验证集进行过拟合并泄漏信息[1, 11]。
此外,提前停止总是需要一个验证集。因此,你将无法在整个数据集上重新训练最终模型以获得额外的性能提升[1](见在完整数据集上重新训练)。因此,不推荐使用提前停止。
最佳检查点选择 — 关于最佳检查点选择的最佳实践没有共同的理解。最佳检查点选择是指训练神经网络固定次数(不使用提前停止)后,选择具有最佳验证指标的运行。尽管深度学习调优手册 [4] 推荐使用最佳检查点选择,但 Kaggle 大师们不推荐,因为它需要使用验证集[1, 11](见在完整数据集上重新训练)。
优化器及其学习率
优化器 — 深度学习调优手册 [4] 推荐从以下之一开始…
-
随机梯度下降(SGD)或
-
Adam。
然而,Kaggle 大师们表示,SGD 需要更多的调优工作才能达到与 Adam 相似的性能,因此推荐 Adam[1]。他们特别推荐AdamW 作为优化器[1]。
学习率 — 虽然在从头训练神经网络时你需要使用较大的学习率,但在微调时,我们可以设置较小的初始学习率,例如 1e-3[11]。这是因为我们假设预训练模型的权重已经很好,不应过快地进行大幅调整[2]。
如果你不确定选择什么学习率,也可以查看你使用的模型架构的论文,以了解他们使用了什么学习率[9]。
学习率调度器 — 虽然在从头训练神经网络时建议使用恒定学习率作为初始基线[4],但在微调时应该使用学习率调度器作为初始基线[1, 4, 11]。
类似于优化器,人们喜欢争论最佳的学习率调度器。虽然当验证指标达到平台期时,减少学习率的方法曾经很流行,但这种方法依赖于验证集[1]。Kaggle 大师推荐余弦退火学习率调度器[1, 11],如下所示。
余弦衰减/退火学习率调度器(图片由作者提供,通过《PyTorch 中学习率调度器的视觉指南》)
对于 NLP,当你使用的训练轮次不多时,你可以保持学习率不变,并且你的初始学习率已经很小[1]。
花里胡哨的技术
每个月都有一种新奇的技术可以应用到你的训练流程中。但为了基线模型的效果,建议尽量保持简单,并在后续添加新特性[4]。
正则化与对验证集的过拟合
几年前,早期停止和在平台期降低学习率被视为避免模型对训练集过拟合的正则化技术[8]。今天,这些技术被认为在对验证集的过拟合方面存在泄漏[1]。
此外,任何依赖于验证集的技术都会阻止我们在最终配置下对整个数据集重新训练模型,这有助于提高模型性能。
第 2 步:逐步增加复杂性并小幅提升性能
你应该将大部分时间花在第二步。在这一阶段,你将进行许多实验,并对基线模型进行调整。你还可以将所有的花里胡哨的技术添加到你的模型中。只需逐步添加,以便能够评估其影响。
对于这一步,建议使用某种实验跟踪系统,无论是纸笔记录还是实验跟踪工具。
超参数调优
理论上,你可以通过在所有可能的超参数搜索空间中运行自动化超参数优化算法来最大化模型性能。但这并不现实。因此,你应该先通过进行一些初步实验来定义搜索空间[4]。
最重要的超参数是学习率和训练轮次。Kaggle 大师推荐以下范围作为起始点[11]:
-
学习率:1e-4 到 1e-3
-
训练轮次:2 到 10
对于 NLP,你只需少量轮次,因为模型的预训练效果优于计算机视觉领域。
因为我们已经缩小了超参数搜索空间,所以我们可以使用算法来自动化超参数调整[4, 8]。建议优先使用随机搜索或贝叶斯优化,而不是网格搜索[4, 8]。
数据增强
深度学习模型的性能严重依赖于数据的数量。因此,通过增加增强数据的数量,可以帮助提高模型的性能[1, 8, 11]。
计算机视觉中的常见数据增强包括:
-
水平或垂直翻转
-
旋转
-
调整大小
-
随机裁剪
-
移位
-
Mixup
-
Cutout
-
Cutmix
数据增强技术:Mixup、Cutout、Cutmix(图片由作者提供)
你可以在这篇文章中找到 PyTorch 实现的 cutout、mixup 和 cutmix:
## Cutout、Mixup 和 Cutmix:在 PyTorch 中实现现代图像增强
用 Python 实现的计算机视觉数据增强技术
[towardsdatascience.com
对于 NLP, 在计算机视觉中有效的数据增强技术似乎也适用于文本数据,如随机裁剪、调整大小或 cutout [9]。但并非所有计算机视觉数据增强技术都能直接转化为文本数据,因此需要其自身的数据增强技术。
-
反向翻译:将文本翻译成另一种语言,然后再翻译回原始语言
-
掩蔽实体语言建模(MELM)[13]:随机掩蔽句子中一定比例的标记(类似于计算机视觉中的 cutout [9])
-
替换:用同义词替换句子中的词汇。
模型复杂性
最适合的骨干网会因问题而异。因此,你应该尝试几种不同的骨干网[1, 11]。
垃圾进,垃圾出
任何机器学习模型的好坏取决于你输入的数据质量。因此,实验不同的模型训练管道配置并审查训练数据是至关重要的。一个好的方法是审查模型预测效果好的样本和表现不佳的样本。
如果你可视化这些样本,你会使自己的工作更轻松[9]。这将给你一个关于数据或训练流程可能存在的问题的提示。
步骤 3:挤出最后的性能
一旦你完成实验(例如,截止日期或满意的性能),你可以进行一些最后的调整,挤出模型的最后性能。
在完整数据集上重新训练
深度学习模型对数据需求量大。这就是为什么如果你用最终的训练配置在完整数据集上重新训练模型,可以提升模型的性能[1, 11]。
集成方法
几年前,大型集成模型曾经很流行,而今天,最多三个模型的小型集成更为时尚[1]。在集成模型时,你可以尝试以下策略:
-
结合具有相同训练配置但不同种子的模型
-
结合具有多样性的不同模型(例如,不同的骨干网络)
添加更多花里胡哨的东西
如果你对结果仍不满意,尝试伪标签[9]或测试时增强[11],看看能否挤出一点点性能提升。
结论
如果你刚刚完成了“深度学习入门”课程,并希望提升技能,这里有一种高层次的方法来处理任何深度学习问题。
从一个简单的基线开始:
-
基础模型: 先从一个开始——无论如何你都应该尝试其他模型:用于计算机视觉的 EfficientNet 和用于自然语言处理的 DeBERTa
-
批量大小(固定): 检查在可用内存中适应的最大批量大小(例如,16、32、64 等),然后除非非常必要,否则不要改变它。
-
训练步数: 5 个 epoch,无早停
-
优化器(固定): AdamW
-
学习率: 1e-3
-
学习率调度器(固定): 余弦退火
建立一个实验跟踪系统并开始行动:
-
超参数调整: 从学习率(0.0001–0.001)和 epoch(2–10)的调整开始。使用随机搜索或贝叶斯优化进行自动超参数调整。
-
数据增强: 增多为妙。Mixup、cutout、cutmix 因其有效性而特别流行。
-
基础模型: 尝试不同的骨干网络和模型复杂性
-
花里胡哨: 在这一步,你可以大胆尝试每一个新兴的深度学习趋势,看看它是否会提升你的模型表现。
为了挤出最后一丝性能,你应该选择最多三个最佳训练配置,重新在整个数据集上训练模型,并进行集成。
喜欢这个故事吗?
免费订阅 以便在我发布新故事时收到通知。
[## 当 Leonie Monigatti 发布时获取电子邮件。
当 Leonie Monigatti 发布时获取电子邮件。如果你还没有账户,通过注册你将创建一个 Medium 账户……
在 LinkedIn、Twitter和 Kaggle上找到我!
参考文献
[1] S. Bhutani 与 H20.ai (2023). 训练机器学习模型的最佳实践 | @ChaiTimeDataScience #160 于 2023 年 1 月在 YouTube 上发布。
[2] CS231n 视觉识别的卷积神经网络 (2023)。迁移学习(访问日期:2023 年 2 月 3 日)
[3] J. Devlin, M. M. Chang, K. Lee & K. Toutanova (2018). BERT:用于语言理解的深度双向转换器的预训练。arXiv 预印本 arXiv:1810.04805。
[4,] V. Godbole, G. E. Dahl, J. Gilmer, C. J. Shallue 和 Z. Nado (2023). 深度学习调优手册(第 1.0 版)(访问日期:2023 年 2 月 3 日)
[5] P. He, X. Liu, J. Gao, & W. Chen (2020). Deberta:通过解耦注意力的解码增强 BERT。arXiv 预印本 arXiv:2006.03654。
[6] K. He, X. Zhang, S. Ren, & J. Sun (2016). 深度残差学习用于图像识别。载于 IEEE 计算机视觉与模式识别会议论文集(第 770–778 页)。
[7] G. Huang, Z. Liu, L. Van Der Maaten & K. Q. Weinberger (2017). 密集连接卷积网络。载于 IEEE 计算机视觉与模式识别会议论文集(第 4700–4708 页)。
[8] A. Karpathy (2019). 训练神经网络的配方(访问日期:2023 年 2 月 3 日)
[9] D. Kłeczek 与慕尼黑 NLP (2023)。提升 NLP 模型训练效果的十种有效技巧(访问日期:2023 年 2 月 9 日)
[10] Y. Liu, M. Ott, N. Goyal, J. Du, M. Joshi, D. Chen & V. Stoyanov (2019). RoBERTa:一种稳健优化的 BERT 预训练方法。arXiv 预印本 arXiv:1907.11692。
[11] P. Singer 和 Y. Babakhin (2022). 深度迁移学习的实用技巧 于 2022 年 11 月在 Kaggle Days Paris 上展示。
[12] M. Tan, & Q. Le (2019). EfficientNet:重新思考卷积神经网络的模型缩放。载于 国际机器学习会议(第 6105–6114 页)。PMLR
[13] R. Zhou, X. Li, R. He, L. Bing, E. Cambria, L. Si, & C. Miao (2021). MELM:通过掩蔽实体语言建模进行低资源 NER 的数据增强。arXiv 预印本 arXiv:2108.13655。
使用数据中心可解释人工智能解释机器学习模型
在本文中了解数据中心解释及其不同类型
·发布于数据科学前沿 ·8 分钟阅读·2023 年 2 月 26 日
--
来源:Pixabay
可解释人工智能(XAI)是一个新兴概念,旨在弥合人工智能与终端用户之间的差距,从而增加人工智能的采纳。XAI 可以使人工智能/机器学习模型更加透明、可信和易于理解。特别是在医疗保健、金融和执法等关键领域,这一点尤为必要。
若要了解 XAI 的介绍,我在2021 年 AI 加速器节亚太区的 45 分钟演讲将非常有帮助:
流行的 XAI 方法,如 LIME、SHAP、显著性图等,是以模型为中心的解释方法。这些方法近似于机器学习模型用于生成预测的重要特征。然而,由于机器学习模型的归纳偏差,预测模型考虑的重要特征的估计可能并不总是准确。因此,以模型为中心的特征重要性方法可能并不总是非常有用。
此外,考虑到数据中心人工智能的原则,机器学习模型的质量仅取决于用于训练它们的数据的质量。由于相关特征、数据漂移、离群值、数据偏斜等引起的数据质量问题会影响训练后的机器学习模型的性能。然而,非技术用户通常对训练机器学习模型所用数据集的好坏没有足够的意识。因此,数据中心可解释人工智能(DCXAI)在训练和推理过程中检测到数据集潜在数据问题时,比以模型为中心的解释更为合适。
如果你有兴趣了解如何利用以数据为中心的可解释人工智能(DCXAI)来解释机器学习模型在高风险领域(如医疗保健)的应用,请查看我的研究出版物——糖尿病发病风险监测的指令性解释:介绍指令性数据中心解释和支持“假设”探索的组合。
我还会参考书中讨论的一些关于数据中心解释的概念,应用机器学习可解释性技术。
应用机器学习可解释性技术 ## 应用机器学习可解释性技术:使机器学习模型变得可解释且值得信赖…
应用机器学习可解释性技术:使机器学习模型在实际应用中变得可解释且值得信赖…
什么是以数据为中心的可解释人工智能(DCXAI)?
如我在书中应用机器学习可解释性技术中讨论的,以数据为中心的可解释人工智能(DCXAI)是一种 XAI 方法,通过生成关于用于训练模型的底层数据集的见解来解释机器学习模型的行为。
来源:Pixabay
数据中心解释方法的例子包括使用均值、众数和方差等常见统计方法总结数据集,视觉化数据分布以比较特征值与其余数据集中的特征值,以及通过假设分析观察模型预测的变化以探究特征的敏感性。此外,数据中心解释还包括在决策支持系统中常采用的数据驱动规则方法。此外,DCXAI 还包括通过分享有关各种数据问题(如数据漂移、数据偏斜、离群值、相关特征等)更多见解来提高对数据质量的认识,这些问题会影响机器学习模型的整体性能。
为什么选择 DCXAI 而不是其他 XAI 方法?
最近,由于基于偏见、不一致和低质量数据训练的机器学习模型的失败,机器学习研究社区正在探索以数据为中心的训练方法,而不仅仅依赖于超参数调优和探索不同的机器学习算法。如果数据是一致的、明确的、平衡的,并且数量充足,则机器学习模型可以更快地训练,并在任何生产级系统中更快地部署。
不幸的是,目前所有生产环境中的人工智能和机器学习系统都未能与以数据为中心的人工智能原则对齐。因此,底层数据可能存在严重问题,这些问题很少被检测到,但最终会导致机器学习系统的失败。这就是为什么数据中心化解释性人工智能(DCXAI)在检查和评估所使用数据质量方面如此重要。
## 应用机器学习解释性技术:使机器学习模型对实践中的应用具有可解释性和可信度…
应用机器学习解释性技术:使机器学习模型对实践中的应用具有可解释性和可信度……
数据中心化解释性人工智能(DCXAI)的不同方法
数据中心化解释的不同方法可以进一步按以下类型分类:
-
生成关于训练数据的洞察 —— 探索性数据分析(EDA)是所有数据科学家和机器学习专家在构建机器学习模型之前进行的重要实践。然而,从 EDA 中生成的洞察很少传达给非技术性的机器学习模型使用者。因此,DCXAI 的一种方法是将生成的洞察传达给最终用户,以解释机器学习模型的潜在行为。这对那些可能没有机器学习知识但在自己领域内是专家的领域专家尤其有用。
此外,数据分布的可视化可以指示数据集的平衡程度。它还可以显示训练数据中存在的偏斜和异常值,这可能影响模型。通过统计措施构建数据档案以生成洞察,也对局部和全局解释非常有用。
-
强调数据质量——大多数情况下,机器学习模型的表现不佳与用于训练它们的数据质量差有关。然而,数据质量的信息很少传达给最终用户。因此,当机器学习模型未能生成良好的预测时,最终用户通常不知道数据集中的问题。因此,DCXAI 涉及通过传达潜在的数据问题,如数据漂移、相关特征、类别不平衡、偏差数据集等,来解释数据质量。
确实,一些数据问题复杂难懂,因为这些是技术概念。但是,当通过简化和交互式可视化呈现时,它会提高对数据质量的认识,从而突出机器学习模型失败的真正原因。
-
估计数据预测能力——有时,数据集过于嘈杂。对于这样的数据集,超越一定的准确度总是困难的。那么,如果我们知道训练好的模型在做出正确预测时并不极其准确,我们如何赢得最终用户的信任?我认为,获得信任的最佳方法是保持透明,明确沟通可行的内容。因此,测量数据预测能力并向最终用户传达模型的效率有助于设定正确的期望。数据预测能力是对模型性能的估计,基于底层数据。
例如,我们有一个模型来预测某公司股票的价格。由机器学习算法建模的股票价格数据最多可以预测 60%的准确率。超出这一点,使用给定的数据集生成更准确的结果在实践中是不可能的。但假设考虑其他外部因素来补充当前数据,模型的准确性可以提高。这证明了不是机器学习算法限制了系统的性能,而是用于建模的数据集没有足够的信息来获得更好的模型性能。因此,这是数据集的一个限制,可以通过数据预测能力的度量来估计。最好在更详细的层面上进行数据预测能力的评估,以提供有关机器学习模型在不同人口统计变量值下性能的额外洞察,如下图所示。
不同人口统计变量的数据预测估计(图像来源于作者)
DCXAI 的好处
现在我们已经了解了 DCXAI 的不同方法,让我们总结一下它的好处,如下所示。
-
容易检测到有偏见和不公平的数据。
-
提高对数据质量、纯洁性和完整性问题的认识,以解释 ML 模型的失败。
-
对于非技术型的 ML 消费者,DCXAI 比其他流行的模型中心解释方法如 LIME、SHAP 和显著性图更容易理解。
-
领域专家往往比 LIME 和 SHAP 更信任 DCXAI,因为 DCXAI 能更透明地展示用于训练 ML 模型的数据集。他们可以利用 DCXAI 通过参考底层训练数据来证明模型生成的预测。
Applied Machine Learning Explainability Techniques: Make ML models explainable and trustworthy for practical…
Applied Machine Learning Explainability Techniques: Make ML models explainable and trustworthy for…
通过指令性数据中心解释使 DCXAI 更加可操作
在我最近的研究出版物——监测糖尿病发病风险的指令性解释中,我介绍了一种详细的以用户为中心的设计过程,用于包含 DCXAI 的 XAI 仪表板。我们通过对这些解释进行以下调整,使 DCXAI 变得更加可操作,以便为医疗专家量身定制——
-
提供交互式视觉解释,以探索假设场景。
-
只考虑可操作的特征变量,而不是不可操作的特征。
-
提供明确的视觉指示,帮助用户探索系统,以理解 ML 模型的工作原理。
-
从全球视角获得局部解释。
通过对传统数据中心解释方法进行这些附加修改,我们设计并开发了视觉指令性数据中心解释。你可以在研究论文中了解更多关于这项研究的内容。
[## 应用机器学习解释性技术:使机器学习模型在实践中可解释且值得信赖]
应用机器学习解释性技术:使机器学习模型在实践中可解释且值得信赖
摘要
在这篇文章中,我们介绍了数据中心可解释 AI(DCXAI)及其各种方法。我们还讨论了 DCXAI 与其他 XAI 方法(如 LIME、SHAP 等)的不同之处,这些方法提供的是模型中心的解释。我们探讨了提供数据中心解释的不同方法。此外,我们还讨论了 DCXAI 的好处,以及如何进一步修改 DCXAI 以生成更具可操作性的解释,供领域专家和普通用户使用。你可以通过我的书应用机器学习解释性技术了解更多关于数据中心解释的内容,并查看书中提供的GitHub 代码示例。
作者在 TDS 上的其他 XAI 故事:
-
谷歌 AI 的可解释 AI 与 TCAV
-
你应该了解的关键可解释 AI Python 框架
-
基于文本数据训练的模型的可解释机器学习:将 SHAP 与 Transformer 模型结合
-
EUCA——一个有效的 XAI 框架,将人工智能带到更接近终端用户
-
理解基于 Shapley 值的 SHAP 及其在可解释 AI 中的工作原理
-
如何使用 LIME 解释图像分类器
参考资料:
-
书籍《应用机器学习解释技术》的 GitHub 仓库 —
github.com/PacktPublishing/Applied-Machine-Learning-Explainability-Techniques/
解读随机森林
关于随机森林算法及其解读的全面指南
·
关注 发表在 数据科学的前沿 ·13 分钟阅读·2023 年 10 月 8 日
--
现在对大语言模型的炒作很多,但这并不意味着老派的机器学习方法现在就该消失。我怀疑如果你给 ChatGPT 一个有数百个数值特征的数据集,并要求它预测目标值,它会有帮助。
神经网络通常是处理非结构化数据(例如文本、图像或音频)的最佳解决方案。但对于表格数据,我们仍然可以从传统的随机森林中受益。
随机森林算法的最重要优点如下:
-
你只需要做一些数据预处理。
-
随机森林算法很难出错。如果你的集成中有足够多的树,你不会面临过拟合问题,因为增加更多的树会降低误差。
-
结果很容易解释。
这就是为什么随机森林可能是你在处理表格数据的新任务时的第一个模型的好候选者。
在这篇文章中,我希望覆盖随机森林的基础知识,并探讨解释模型结果的方法。
我们将学习如何找到以下问题的答案:
-
哪些特征是重要的,哪些是冗余的可以被移除?
-
每个特征值如何影响我们的目标指标?
-
每个预测的因素是什么?
-
如何估计每个预测的置信度?
预处理
我们将使用葡萄酒质量数据集。它展示了葡萄酒质量与不同葡萄牙“Vinho Verde”葡萄酒变体的物理化学测试之间的关系。我们将尝试基于葡萄酒特性预测葡萄酒质量。
使用决策树时,我们不需要做很多预处理。
-
我们不需要创建虚拟变量,因为算法可以自动处理。
-
我们不需要做归一化或去除异常值,因为只有排序是重要的。因此,基于决策树的模型对异常值具有抗性。
然而,scikit-learn 实现的决策树无法处理分类变量或缺失值。因此,我们需要自己处理这些问题。
幸运的是,我们的数据集中没有缺失值。
df.isna().sum().sum()
0
我们只需要将 type
变量(‘red’ 或 ‘white’)从 string
转换为 integer
。我们可以使用 pandas 的 Categorical
转换来实现。
categories = {}
cat_columns = ['type']
for p in cat_columns:
df[p] = pd.Categorical(df[p])
categories[p] = df[p].cat.categories
df[cat_columns] = df[cat_columns].apply(lambda x: x.cat.codes)
print(categories)
{'type': Index(['red', 'white'], dtype='object')}
现在,df['type']
对于红酒等于 0,对于白酒等于 1。
数据预处理的另一个关键部分是将数据集拆分为训练集和验证集。这样,我们可以使用验证集来评估模型的质量。
import sklearn.model_selection
train_df, val_df = sklearn.model_selection.train_test_split(df,
test_size=0.2)
train_X, train_y = train_df.drop(['quality'], axis = 1), train_df.quality
val_X, val_y = val_df.drop(['quality'], axis = 1), val_df.quality
print(train_X.shape, val_X.shape)
(5197, 12) (1300, 12)
我们已经完成了预处理步骤,并准备好进入最激动人心的部分——训练模型。
决策树的基础知识
在开始训练之前,让我们花些时间理解随机森林的工作原理。
随机森林是决策树的集成。因此,我们应该从基本的构建块——决策树开始。
在我们的葡萄酒质量预测示例中,我们将解决一个回归任务,因此让我们从这开始。
决策树:回归
让我们拟合一个默认的决策树模型。
import sklearn.tree
import graphviz
model = sklearn.tree.DecisionTreeRegressor(max_depth=3)
# I've limited max_depth mostly for visualisation purposes
model.fit(train_X, train_y)
决策树的一个显著优势是我们可以轻松解释这些模型——它只是一些问题。让我们可视化一下。
dot_data = sklearn.tree.export_graphviz(model, out_file=None,
feature_names = train_X.columns,
filled = True)
graph = graphviz.Source(dot_data)
# saving tree to png file
png_bytes = graph.pipe(format='png')
with open('decision_tree.png','wb') as f:
f.write(png_bytes)
作者提供的图表
如你所见,决策树由二元分裂组成。在每个节点上,我们将数据集拆分为 2 部分。
最后,我们计算叶子节点的预测值作为该节点中所有数据点的平均值。
附注: 由于决策树对叶节点的所有数据点取平均值,因此决策树在外推方面表现较差。因此,在训练和推理过程中需要关注特征分布。
让我们头脑风暴一下如何确定数据集的最佳分裂。我们可以从一个变量开始,定义其最佳划分。
假设我们有一个具有四个唯一值的特征:1、2、3 和 4。那么,它们之间有三个可能的阈值。
图表由作者提供
我们可以逐一取每个阈值,并计算数据的预测值作为叶节点的平均值。然后,我们可以使用这些预测值来计算每个阈值的 MSE(均方误差)。最佳分裂将是具有最低 MSE 的那个。默认情况下,DecisionTreeRegressor在 scikit-learn 中也以类似方式工作,并使用 MSE 作为标准。
为了更好地理解其工作原理,我们可以手动计算sulphates
特征的最佳分裂。
def get_binary_split_for_param(param, X, y):
uniq_vals = list(sorted(X[param].unique()))
tmp_data = []
for i in range(1, len(uniq_vals)):
threshold = 0.5 * (uniq_vals[i-1] + uniq_vals[i])
# split dataset by threshold
split_left = y[X[param] <= threshold]
split_right = y[X[param] > threshold]
# calculate predicted values for each split
pred_left = split_left.mean()
pred_right = split_right.mean()
num_left = split_left.shape[0]
num_right = split_right.shape[0]
mse_left = ((split_left - pred_left) * (split_left - pred_left)).mean()
mse_right = ((split_right - pred_right) * (split_right - pred_right)).mean()
mse = mse_left * num_left / (num_left + num_right) \
+ mse_right * num_right / (num_left + num_right)
tmp_data.append(
{
'param': param,
'threshold': threshold,
'mse': mse
}
)
return pd.DataFrame(tmp_data).sort_values('mse')
get_binary_split_for_param('sulphates', train_X, train_y).head(5)
| param | threshold | mse |
|:----------|------------:|---------:|
| sulphates | 0.685 | 0.758495 |
| sulphates | 0.675 | 0.758794 |
| sulphates | 0.705 | 0.759065 |
| sulphates | 0.715 | 0.759071 |
| sulphates | 0.635 | 0.759495 |
我们可以看到,对于sulphates
,最佳阈值是 0.685,因为它提供了最低的 MSE。
现在,我们可以将此函数应用于我们所有的特征,以定义整体最佳分裂。
def get_binary_split(X, y):
tmp_dfs = []
for param in X.columns:
tmp_dfs.append(get_binary_split_for_param(param, X, y))
return pd.concat(tmp_dfs).sort_values('mse')
get_binary_split(train_X, train_y).head(5)
| param | threshold | mse |
|:--------|------------:|---------:|
| alcohol | 10.625 | 0.640368 |
| alcohol | 10.675 | 0.640681 |
| alcohol | 10.85 | 0.641541 |
| alcohol | 10.725 | 0.641576 |
| alcohol | 10.775 | 0.641604 |
我们得到了与初始决策树完全相同的结果,第一次分裂是在alcohol <= 10.625
上。
为了构建整个决策树,我们可以递归地计算每个数据集alcohol <= 10.625
和alcohol > 10.625
的最佳分裂,并获得决策树的下一层。然后,重复这一过程。
递归的停止标准可以是深度或叶节点的最小大小。这里是一个决策树的示例,叶节点中至少有 420 个项目。
model = sklearn.tree.DecisionTreeRegressor(min_samples_leaf = 420)
图表由作者提供
让我们计算验证集上的平均绝对误差,以了解我们的模型表现如何。我更喜欢 MAE 而非 MSE(均方误差),因为 MAE 对离群值的影响较小。
import sklearn.metrics
print(sklearn.metrics.mean_absolute_error(model.predict(val_X), val_y))
0.5890557338155006
决策树:分类
我们已经查看了回归示例。在分类的情况下,情况有所不同。尽管在本文中我们不会深入探讨分类示例,但仍值得讨论其基本概念。
对于分类,我们使用最常见的类别作为每个叶节点的预测,而不是平均值。
我们通常使用基尼系数来估计分类中二元分裂的质量。想象从样本中随机获取一个项目,然后获取另一个。基尼系数将等于项目来自不同类别的情况的概率。
假设我们只有两个类别,且第一类的项目占比为p
。然后,我们可以使用以下公式计算基尼系数:
如果我们的分类模型是完美的,基尼系数等于 0。最坏情况下(p = 0.5
),基尼系数等于 0.5。
要计算二元分裂的度量,我们计算两个部分(左侧和右侧)的基尼系数,并对每个分区中的样本数进行归一化。
然后,我们可以类似地计算不同阈值下的优化度量,并使用最佳选项。
我们已经训练了一个简单的决策树模型并讨论了它的工作原理。现在,我们准备继续讨论随机森林。
随机森林
随机森林基于自助法(Bagging)的概念。这个想法是拟合一堆独立的模型,并使用它们的平均预测。由于模型是独立的,误差不会相关。我们假设我们的模型没有系统性误差,许多误差的平均值应该接近于零。
我们如何获得许多独立的模型呢?这很简单:我们可以在随机的行和特征子集上训练决策树。这样就会形成一个随机森林。
让我们训练一个包含 100 棵树且叶节点最小大小为 100 的基本随机森林。
import sklearn.ensemble
import sklearn.metrics
model = sklearn.ensemble.RandomForestRegressor(100, min_samples_leaf=100)
model.fit(train_X, train_y)
print(sklearn.metrics.mean_absolute_error(model.predict(val_X), val_y))
0.5592536196736408
使用随机森林,我们实现了比单棵决策树更好的质量:0.5592 对比 0.5891。
过拟合
一个有意义的问题是随机森林是否会过拟合。
实际上不会。由于我们在平均不相关的误差,添加更多的树不会使模型过拟合。随着树木数量的增加,质量将渐近提高。
作者绘图
然而,如果你有深度较大的树且数量不够,你可能会遇到过拟合的问题。一个决策树很容易发生过拟合。
袋外误差
由于在随机森林中每棵树只使用部分行,我们可以利用它们来估计误差。对于每一行,我们可以选择没有使用该行的树,并用它们来进行预测。然后,我们可以根据这些预测计算误差。这种方法称为“袋外误差”。
我们可以看到 OOB 误差比训练集上的误差更接近验证集上的误差,这意味着它是一个好的近似值。
# we need to specify oob_score = True to be able to calculate OOB error
model = sklearn.ensemble.RandomForestRegressor(100, min_samples_leaf=100,
oob_score=True)
model.fit(train_X, train_y)
# error for validation set
print(sklearn.metrics.mean_absolute_error(model.predict(val_X), val_y))
0.5592536196736408
# error for training set
print(sklearn.metrics.mean_absolute_error(model.predict(train_X), train_y))
0.5430398596179975
# out-of-bag error
print(sklearn.metrics.mean_absolute_error(model.oob_prediction_, train_y))
0.5571191870008492
解释模型
正如我一开始提到的,决策树的一个大优点是它们易于解释。让我们尝试更好地理解我们的模型。
特征重要性
特征重要性的计算相当简单。我们查看集成中的每棵决策树和每个二元分裂,计算其对我们度量标准的影响(在我们这里是squared_error
)。
让我们看看我们初始决策树的第一个alcohol
分裂。
然后,我们可以对所有决策树中的所有二元分裂进行相同的计算,汇总所有结果,进行归一化,从而得到每个特征的相对重要性。
如果你使用 scikit-learn,你不需要手动计算特征重要性。你只需获取model.feature_importances_
即可。
def plot_feature_importance(model, names, threshold = None):
feature_importance_df = pd.DataFrame.from_dict({'feature_importance': model.feature_importances_,
'feature': names})\
.set_index('feature').sort_values('feature_importance', ascending = False)
if threshold is not None:
feature_importance_df = feature_importance_df[feature_importance_df.feature_importance > threshold]
fig = px.bar(
feature_importance_df,
text_auto = '.2f',
labels = {'value': 'feature importance'},
title = 'Feature importances'
)
fig.update_layout(showlegend = False)
fig.show()
plot_feature_importance(model, train_X.columns)
我们可以看到,总体上最重要的特征是alcohol
和volatile acidity
。
作者绘制的图表
部分依赖
了解每个特征如何影响我们的目标指标是令人兴奋的,且通常是有用的。例如,质量是否随着酒精含量的增加而提高或降低,或者是否存在更复杂的关系。
我们可以直接从数据集中获取数据并按酒精含量绘制平均值,但这不准确,因为可能存在某些相关性。例如,我们的数据集中较高的酒精含量可能还对应更高的糖分和更好的质量。
为了仅估计酒精的影响,我们可以取数据集中所有行,并使用机器学习模型预测每一行在不同酒精含量下的质量:9、9.1、9.2 等。然后,我们可以平均结果,获得酒精水平与酒质之间的实际关系。所以,所有数据是相同的,我们只是改变酒精水平。
这种方法可以与任何机器学习模型一起使用,而不仅仅是随机森林。
我们可以使用sklearn.inspection
模块来轻松绘制这些关系。
sklearn.inspection.PartialDependenceDisplay.from_estimator(clf, train_X,
range(12))
我们可以从这些图表中获得很多洞察,例如:
-
酒质随着游离二氧化硫的增加而提高,直到 30,但在这一阈值后保持稳定;
-
酒精含量越高,质量越好。
我们甚至可以查看两个变量之间的关系。这可能相当复杂。例如,如果酒精含量超过 11.5,挥发性酸度没有影响。但对于较低的酒精含量,挥发性酸度对质量有显著影响。
sklearn.inspection.PartialDependenceDisplay.from_estimator(clf, train_X,
[(1, 10)])
预测的置信度
使用随机森林,我们还可以评估每个预测的置信度。为此,我们可以计算集成中每棵树的预测值,并查看方差或标准差。
val_df['predictions_mean'] = np.stack([dt.predict(val_X.values)
for dt in model.estimators_]).mean(axis = 0)
val_df['predictions_std'] = np.stack([dt.predict(val_X.values)
for dt in model.estimators_]).std(axis = 0)
ax = val_df.predictions_std.hist(bins = 10)
ax.set_title('Distribution of predictions std')
我们可以看到,有些预测的标准差很低(即低于 0.15),而有些预测的std
则高于 0.3。
如果我们将模型用于业务目的,我们可以以不同的方式处理这种情况。例如,如果std
超过X
,则不考虑预测,或向客户展示区间(即 25 百分位数和 75 百分位数)。
预测是如何产生的?
我们还可以使用treeinterpreter
和waterfallcharts
包来了解每个预测是如何产生的。这在某些业务场景中可能会很有用,例如,当你需要告诉客户为什么他们的信用申请被拒绝时。
我们将以其中一种酒为例。它具有相对较低的酒精含量和较高的挥发性酸度。
from treeinterpreter import treeinterpreter
from waterfall_chart import plot as waterfall
row = val_X.iloc[[7]]
prediction, bias, contributions = treeinterpreter.predict(model, row.values)
waterfall(val_X.columns, contributions[0], threshold=0.03,
rotation_value=45, formatting='{:,.3f}');
图表显示这款酒的质量优于平均水平。提高质量的主要因素是低挥发性酸度,而主要缺点是低酒精度。
作者绘图
因此,有很多实用工具可以帮助你更好地理解数据和模型。
减少树的数量
随机森林的另一个酷炫特性是,我们可以使用它来减少任何表格数据的特征数量。你可以快速拟合一个随机森林并定义数据中有意义的列。
更多的数据并不总是意味着更好的质量。此外,它还可能影响你的模型在训练和推断阶段的表现。
由于我们初始的葡萄酒数据集中只有 12 个特征,所以在这种情况下,我们将使用一个稍大的数据集——在线新闻受欢迎程度。
查看特征重要性
首先,让我们构建一个随机森林并查看特征重要性。在 59 个特征中,有 34 个特征的重要性低于 0.01。
让我们尝试移除这些特征并查看准确性。
low_impact_features = feature_importance_df[feature_importance_df.feature_importance <= 0.01].index.values
train_X_imp = train_X.drop(low_impact_features, axis = 1)
val_X_imp = val_X.drop(low_impact_features, axis = 1)
model_imp = sklearn.ensemble.RandomForestRegressor(100, min_samples_leaf=100)
model_imp.fit(train_X_sm, train_y)
-
所有特征在验证集上的 MAE:2969.73
-
25 个重要特征在验证集上的 MAE:2975.61
质量的差异并不大,但我们可以使模型在训练和推断阶段更快。我们已经去除了初始特征的近 60%——做得不错。
查看冗余特征
对于剩余的特征,让我们看看是否有冗余(高度相关)的特征。为此,我们将使用一个 Fast.AI 工具:
import fastbook
fastbook.cluster_columns(train_X_imp)
我们可以看到以下特征彼此接近:
-
self_reference_avg_sharess
和self_reference_max_shares
-
kw_min_avg
和kw_min_max
-
n_non_stop_unique_tokens
和n_unique_tokens
。
我们也将它们移除。
non_uniq_features = ['self_reference_max_shares', 'kw_min_max',
'n_unique_tokens']
train_X_imp_uniq = train_X_imp.drop(non_uniq_features, axis = 1)
val_X_imp_uniq = val_X_imp.drop(non_uniq_features, axis = 1)
model_imp_uniq = sklearn.ensemble.RandomForestRegressor(100,
min_samples_leaf=100)
model_imp_uniq.fit(train_X_imp_uniq, train_y)
sklearn.metrics.mean_absolute_error(model_imp_uniq.predict(val_X_imp_uniq),
val_y)
2974.853274034488
质量即使稍微有所改善。因此,我们将特征数量从 59 个减少到 22 个,并且仅将错误增加了 0.17%。这证明了这种方法的有效性。
你可以在 GitHub 上找到完整的代码。
总结
在这篇文章中,我们讨论了决策树和随机森林算法的工作原理。此外,我们还学会了如何解释随机森林:
-
如何使用特征重要性获取最重要特征的列表并减少模型中的参数数量。
-
如何使用部分依赖定义每个特征值对目标指标的影响。
-
如何使用
treeinterpreter
库估计不同特征对每次预测的影响。
非常感谢你阅读这篇文章。希望它对你有所启发。如果你有任何后续问题或评论,请在评论区留言。
参考文献
数据集
-
Cortez, Paulo, Cerdeira, A., Almeida, F., Matos, T., 和 Reis, J.. (2009). 葡萄酒质量. UCI 机器学习库。 https://doi.org/10.24432/C56S3T
-
Fernandes, Kelwin, Vinagre, Pedro, Cortez, Paulo, 和 Sernadela, Pedro. (2015). 在线新闻流行度. UCI 机器学习库。 https://doi.org/10.24432/C5NS3V
来源
本文灵感来自Fast.AI 深度学习课程
-
“《深度学习实战:Fast.AI 与 Pytorch 实现 AI 应用,无需博士学位》”第九章由 J. Howard 和 S. Gugger 编写。
面试准备:因果推断
原文:
towardsdatascience.com/interview-preparation-causal-inference-44fbb8b0a5c6
·发表在 Towards Data Science ·阅读时间 5 分钟·2023 年 8 月 11 日
--
图片由 Isaac Smith 在 Unsplash 提供
因果推断是数据科学的核心,使我们能够解读现实世界中复杂的因果关系。在本文中,我们将探讨关键的因果推断技术,了解其优缺点,深入实际应用以展示其重要性,并为有志的数据科学家提供面试问题和答案,以便为数据科学面试做准备。
随机对照试验(RCTs):
随机对照试验(RCTs)是因果推断的基石,提供了严格的因果关系证据。它们通过将参与者随机分配到治疗组和对照组来确保任何观察到的结果差异都可以归因于治疗本身。
优点:
-
由于随机分配,被视为因果推断的黄金标准。
-
提供了高内部有效性,有助于建立强因果关系。
-
在特定条件下,结果可以推广到更大的人群。
缺点:
-
可能会很昂贵、耗时或在伦理上具有挑战性。
-
并非所有研究问题都适用。
实际应用:
-
药物试验:RCTs 通常用于评估新药的疗效,通过将接受药物的治疗组与接受安慰剂的对照组进行比较。这种方法有助于确定观察到的健康结果是否确实是由于药物的效果。
-
教育干预:RCTs 可以评估创新教学方法对学生表现的影响。通过将教室随机分配到不同的教学方法中,研究人员可以确定干预的有效性。
面试问题:
问:随机对照试验如何确保无偏的因果推断?
答:RCT 使用随机分配来创建可比的处理组和对照组,确保任何结果差异都可以归因于处理本身,而不是其他因素。
匹配方法:
匹配方法在随机化不可行时提供了解决方案。在教育干预领域,研究人员可能会评估一种新教学方法对学生表现的影响。通过仔细选择在相关特征上与处理学生紧密匹配的对照学生,匹配方法有助于减少选择偏倚并提供关于干预效果的洞察。
优点:
-
通过创建平衡的处理组和对照组来减少选择偏倚。
-
适用于随机分配不可行的观察研究。
缺点:
-
需要仔细选择匹配变量。
-
可能无法解决组间未观察到的差异。
实际应用:
-
工作培训项目:可以采用匹配方法来比较参与了工作培训项目的个体与未参与者之间的结果。通过确保处理组和对照组具有相似的特征,研究人员可以评估培训对就业和收入的影响。
-
医疗结果:研究人员可以使用匹配方法评估医疗治疗的有效性。通过匹配具有相似病史的患者,可以更准确地估计治疗的因果影响。
面试问题:
问:什么是选择偏倚,匹配方法如何解决它?
答:选择偏倚发生在处理组和对照组系统性地不同的情况下。匹配方法通过创建具有相似特征的平衡组来减少选择偏倚,使得处理效果更为可靠。
工具变量(IV):
在工具变量分析中,一个外部变量或工具有助于解决内生性问题。假设研究教育对收入的影响。靠近大学的地理位置作为工具,影响教育但不直接影响收入。这种技术允许研究人员隔离教育与收入之间的因果关系。
优点:
-
通过利用外部变量作为工具解决内生性问题。
-
当由于伦理或实际原因无法进行随机化时,有用。
缺点:
-
需要有效的工具,影响处理但不直接影响结果。
-
易受弱工具偏倚的影响。
实际应用:
-
教育和收入:靠近教育机构可以作为工具来估计教育对收入的因果影响。通过比较住在大学附近的个体与住得更远的个体,研究人员可以探明教育对收入潜力的影响。
-
经济研究:IV 分析可以用来研究经济政策的影响。例如,研究人员可能使用利率变化作为工具来理解其对消费者支出的影响。
面试问题:
问:工具变量与混杂变量有何不同?
A: 工具变量是指影响处理但不直接影响结果的变量,有助于解决内生性问题。混杂变量是指影响处理和结果的变量,如果不加以控制,会导致估计偏差。
差分中的差分(DiD):
差分中的差分(DiD)比较处理组和控制组在时间上的结果变化。例如,考虑评估最低工资增加的影响。通过分析政策变更前后两组的就业率,研究人员可以估计该政策的因果效应。
优点:
-
通过比较时间上的结果变化来捕捉处理效应。
-
控制组之间的时间不变差异。
缺点:
-
假设在没有处理的情况下趋势平行。
-
可能无法完全考虑所有混杂因素。
实际应用:
-
政策评估:DiD 可以评估公共政策的影响。例如,它可以用来研究新的环境法规对污染水平的影响,通过比较受影响和未受影响的地区在政策实施前后的变化。
-
公共卫生干预:DiD 可以帮助测量干预措施的有效性,例如反吸烟运动。通过比较处理组和控制组在运动前后的吸烟率,研究人员可以估计该运动的影响。
面试问题:
问:在因果推断的背景下,“差分中的差分”概念是什么意思?
A: 差分中的差分(Difference-in-Differences)指的是在干预前后比较处理组和控制组之间的结果差异。这有助于将处理效应与影响两组的其他因素隔离开来。
回归不连续设计(RDD):
回归不连续设计(RDD)利用自然阈值来估计因果效应。考虑一个选举结果影响政策变化的情况。通过分析接近选举结果前后的政策,RDD 有助于确定选举结果是否导致了政策变化。
优点:
-
利用自然的截止点来估计因果效应。
-
控制阈值附近的选择偏差。
缺点:
-
假设接近截止点的单位是相似的。
-
限于存在明确阈值的情况。
实际应用:
-
选举结果:RDD 可以用来研究政治变化的影响。例如,研究人员可能会分析一次接近的选举对后续政策决策的影响,提供有关选举结果与政策变化之间因果关系的见解。
-
教育干预:RDD 可以估计教育政策的影响。通过关注刚好在招生竞争性项目的年级分数线以上或以下的学生,研究人员可以评估该项目对学业表现的因果效应。
面试问题:
Q: 回归不连续性设计是如何工作的?为什么明确的阈值很重要?
A:RDD(回归不连续性设计)关注于阈值附近的单位,假设靠近临界点的单位是相似的。明确的阈值对于确保观察到的结果差异确实归因于处理而不是随机变异非常重要。
因果推断技术使研究人员能够揭示驱动现实世界现象的因果关系。通过理解这些技术及其应用,并为面试问题做好准备,渴望成为数据科学家的人员可以提升他们的专业技能,并为各个领域的基于证据的决策做出贡献。这本全面的指南是掌握因果推断和准备数据科学面试的宝贵资源。
数据分析简介:“谷歌方法”
原文:
towardsdatascience.com/intro-to-data-analytics-the-google-method-92d431b65581
询问,分析与行动
·发表于 Towards Data Science ·阅读时间 12 分钟·2023 年 6 月 14 日
--
图片由 Adam Satria 提供(Unsplash)
在 大型公司 中的数据分析往往很混乱。
数据往往是难以捉摸的,藏匿在组织的各个角落,需要软技能和福尔摩斯般的调查才能找到并整合起来。
公司的数据治理堡垒虽然保护了公司及其客户,但也使得获取数据的目标变得复杂。在这里,谈判和软技能至关重要。
仿佛这些还不够具有挑战性,你还需要具备有效分析的技术能力和掌握数据故事讲述的艺术,以便 展示影响你的数据驱动洞察。
更新:我现在在 YouTube 上发布分析教程。
所需技能的现实。图片由作者提供
在澳大利亚最大的银行之一担任数据科学家和工程师 过去五年 的经历让我发现,在线课程无法充分准备学生面对企业分析的现实。
我希望通过这篇文章一举三得。
对于那些…
-
由于行业经验较少——我将分享我在银行中从头到尾的数据分析项目经验,涵盖所有细节。
-
学习数据分析——我们将在Excel中进行基本的数据透视表分析,并以可视化洞察结束。数据集已包含!
-
寻找一个数据分析流程来遵循——我将采用Google的六阶段流程,供现代数据分析师使用:询问,准备,处理,分析,分享和行动。
从描述性分析到预测建模。图片由作者提供
让我们深入探讨一下。
1. 询问
与学术界不同,行业中的分析由业务需求驱动。
利益相关者试图解决什么问题?你的分析通常会使公司做得更好、更快或更便宜。
我的银行: 在我所在的银行业,这里是一些我们常遇到的典型问题。
-
我们如何更好地识别欺诈交易?
-
哪些因素会导致客户流失或员工离职**?
-
客户满意度的主要驱动因素是什么?
-
影响信用卡消费的关键因素是什么?
-
我们如何优化我们的贷款审批流程,使其更快但仍然及时?
我们将与业务利益相关者合作,提出正确的问题,定义问题和制定解决计划。
我们将利用一些领域专长并考虑整个问题,以准确捕捉情况。
在整个项目过程中,我们将与这些利益相关者保持开放的沟通。
所需技能: 项目和利益相关者管理;领域专长。
练习
假设你为一家小型在线零售商工作,该商店向澳大利亚和新西兰的客户销售各种商品。你的老板希望了解雨伞的销售表现:
“哪个月份它们卖得最好?”
“在澳大利亚和新西兰,它们在哪些地方最受欢迎?
很好!一些值得深入探讨的问题。
2. 准备
业务需求现在驱动了数据需求。
我们需要准备数据以进行分析,从而获得解答商业问题的洞察力。(哎,这真是长话短说!)
我的银行: 这些是分析师在数据准备过程中需要解决的一些常见问题。
-
我们需要哪些数据?这些数据在哪里? 是操作数据还是分析数据?操作数据存储在哪些源系统中?分析数据存在数据仓库还是数据湖中?
-
我们可以使用哪些工具来访问数据?你可能会使用类似Teradata Studio的工具来访问由数据仓库供应商Teradata托管的 SQL 数据库。你可能会使用Azure Synapse Analytics来处理在Microsoft Azure Cloud上托管的大数据。你可能会使用Excel或Power BI来处理较小的表格,尤其是那些足够小可以下载到本地计算机的表格。如果你需要将所有这些来源的数据集连接起来怎么办?也许一个像Dataiku这样的全功能数据科学工作台可以帮忙。
-
我们需要导航哪些治理流程? 谁拥有数据?我们需要什么批准才能访问数据,特别是当数据包含敏感客户信息时?你如何保证数据的安全性?如何跟踪在项目生命周期中数据转化的来源?(…对企业技术债务的一个重要贡献)。
关于治理,这里的数据发现项目通常需要来自多个利益相关者的支持,他们都有自己的利益和 KPI:
-
商业——显然!他们最初委托了我……
-
架构——此项目所需的模式是否与银行的战略技术方向一致?还是我的工作大部分是临时性的战术解决方案,后来将失去价值?我的团队是否会对技术债务产生贡献?
-
数据平台——我们是否拥有实现项目目标所需的数据和工具服务?它是否符合我们的资金限制?
-
治理——访问和使用数据需要哪些文书工作?是否存在隐私和伦理考虑?
-
风险——存在哪些数据风险?应对措施是什么?如何保护敏感数据?
所需技能: 理解公司的企业数据架构;数据建模;对不同平台和工具的经验;能够协调各种利益相关者的利益。
练习(续)
假设在线业务的汇总销售数据存储在一个 CRM 平台中,并可以从这里下载到你的笔记本电脑。
这在Excel中是这样的。
作者提供的图片
3. 过程
啊,最有趣的部分。
数据整理!
(半开玩笑说…)
在数据处理阶段,你将清理数据并使其准备好进行分析。
这包括解决数据质量问题——例如重复和缺失的数据——并决定如何处理不相关的数据和异常值。
我的银行: 在大型组织中,数据质量问题几乎无法消除。
生成的数据量实在太多了,来源系统和参与者太多。
数据仓库通常因特定项目构建的定制数据管道而积累大量技术债务,创建了不可重用的冗余数据资产。
在 2020 年代,组织正在全球范围内采用的解决方案是将数据从项目导向的思维模式转变为产品导向的思维模式,将数据视为高质量的可重用产品。
同时,由于将大量工作集中在过度负担的中央数据团队上,数据湖会遇到瓶颈问题,这些团队虽然是数据工程专家,却不是数据领域专家。
然而,拥有专业数据工程师构建 ETL 管道的好处在于可以识别和修复大量数据问题。
需要的技能: 数据处理。
练习(续)
是时候准备好上面的电子表格进行分析了。
由于你关注的是雨伞销售的受欢迎程度(即销售量才是关键),我建议你隐藏如销售价格和制造成本这样的列。
图片来自作者
然后你可以筛选销售数据,只看雨伞的销售情况……因为这正是你老板关心的!
图片来自作者
哎呀,我们已经可以看到一些数据质量问题,你可以直接在电子表格中修复它们。
理想情况下,数据质量问题通过编程方式解决,因为人工处理在大数据集之外的可靠性有限。
幸运的是,这个练习的数据集是小型的。
图片来自作者
解决这些问题后,你的数据集就准备好进行分析了。
享受这个故事吗?当我发布类似文章时,获取一封电子邮件。
4. 分析
是时候进入有趣的部分了。
当然,这就是所有在线课程集中关注的内容——分析!
主要目标是发现解决业务问题的模式、关系和趋势。
你会理解数据并试图解锁其中潜在的价值。
我的银行: 我们拥有分析卓越的领域,包括进行高级分析和预测建模的数据科学团队。这包括传统的机器学习原型设计与 Python 笔记本,或较新的无代码机器学习平台,这些平台使先进的能力在整个组织中得以普及。
AI、大数据和数据科学的世界如何交织。图片来源于作者
然而,迄今为止,大多数组织处于接近描述性分析的数据成熟度水平,这意味着使用像 Excel 和 Power BI 这样的工具。
所需技能: 分析能力!
练习(续)
为了更好地理解数据中发生的事情,你可以通过利用数据透视表来以有用的方式切片和分割数据集及其字段。
这个想法是,你只选择你感兴趣的变量,并创建可视化图表以推动洞察。
要在 Excel 中创建数据透视表,选择源数据并在旁边插入一个数据透视表。
图片来源于作者
你将从一个空白的数据透视表开始,你可以通过拖放字段来填充它,如下所示。
图片来源于作者
你感兴趣的是测量伞的销售数量,所以拖动:
-
产品字段放入筛选器,以便你可以按伞进行筛选;
-
销售数量字段放入值区域,因为这是你的主要度量。
-
年份和月份名称放入行区域,以便你了解按年和月的伞销售情况。
你现在有了你的数据透视表!
图片来源于作者
现在是大结局——在图表中可视化你的数据透视表。
要创建它,选择你的数据透视表并插入一个数据透视图。我建议选择折线图,但也可以尝试其他图表,以最佳地展示你的数据。
图片来源于作者
啊哈,魔法时刻……
洞察!
图片来源于作者
你的数据透视图清楚地讲述了一个有趣的数据故事。
在 2022 年,伞的销售在 4 月激增,在 5 月至 6 月间达到了第一个高峰,7 月略微下降,然后在 8 月达到了最终高峰后再次下降。
无论如何,5 月至 8 月的销售额比其他时间段显著更高。
这解决了第一个业务问题,即确定一年中的哪个时间段销售最好。
通过拖放字段到我们的数据透视表中,你可以创建另一个数据透视图,以回答第二个业务问题,即澳大利亚和新西兰的哪些部分驱动了最佳销售。
图片来源于作者
我们可以清楚地看到,大多数伞的销售实际上来自新西兰。这进一步揭示了第一个商业问题——五月和八月恰好是该国的湿季!
简而言之,你清理了数据,创建了一些数据透视表来汇总数据,然后制作了一些图表来直观地显示我们的新西兰朋友在仅仅几个湿润的月份内负责了大部分伞的销售!
很好!
5. 分享
在这里,你想找到一个合适的受众来分享你的洞察。
如果你的发现没有对某种下游决策提供信息,那么它们有价值吗?
好吧,这有点苛刻,但我想表达的是,很多分析洞察没有得到利用,通常是因为它们:
-
分析师没有与利益相关者分享,或者
-
没有很好地表达或者
-
没有被决策者采取行动(更多内容见下文)。
一个有效的数据分析师会提取他们的洞察并利用他们的软技能来吸引利益相关者和决策者,听取他们的信息。
所需技能: 数据可视化;有效的沟通和讲故事;利益相关者管理;说服力和软技能。
练习(续)
安排一个会议向你的老板展示你的发现。他需要了解你的新西兰客户有多重要!
6. 行动
在我们的端到端数据分析过程的最后,你的沟通和讲故事技能至关重要。
许多数据科学家认为这些是最重要的技能,因为没有这些技能,你的工作对利益相关者的影响会很小,最终会变得微不足道。
你希望决策者对你的洞察采取行动。
数据讲述在这里至关重要,因为…
-
你的数据不会自己说话。抱歉!
-
人类天生相信故事。
-
故事创造了受众的共鸣,这产生了催产素反应,从而导致他们对你的信任。
-
一旦你建立了这种联系,你可以向他们推销你想要的任何东西。
-
最终: 讲故事,而不是数据。
我的银行: 不幸的是,许多分析和数据科学项目最终没有被决策者采取行动。
这可能有多种原因:
-
未获得利益相关者的支持。你的结果不够强,或者可能以一种低估其影响的方式进行了传达。(改进你的表达!)
-
洞察很难付诸行动。例如,实施某些洞察可能昂贵得离谱,或者与当前的商业优先事项不一致。(运气不好!)
-
传统文化对数据驱动的洞察抗拒,这反映了组织的数据成熟度。(这个问题超出了你的薪资等级。)
你的公司处于什么位置?图片由作者提供
大多数公司的数据成熟度在3 分或更低。
我的银行——像许多西方银行一样——大致评分为 2 到 3 分(满分 5 分),特点是有些领域表现出色,而领导团队正竭尽全力通过各种举措提升整个企业的数据素养,例如数据民主化。(让数据和工具对所有人更为便捷。)
所需技能: 数据可视化;有效的沟通和讲故事能力;利益相关者管理;说服力和软技能。
练习(续)
在与老板的会议中,你串联了一个强有力的叙述,利用了你制作的一些交互式 Excel 数据透视图。你的老板被说服了,承诺在新西兰建立一个更强大的业务,同时确保那里为明年的湿季储备足够的伞具。
恭喜你成功说服决策者,使他们采取行动基于你的分析洞察!
最后的话
数据分析课程通常较少教授在现实世界中成为**有效数据分析师所需的各种软技能。
这就是我喜欢谷歌的六阶段框架的原因:
-
询问。商业问题是什么?
-
准备。我们如何获取所需的数据?
-
过程。可靠的洞察只能来自干净的数据。
-
分析。最后……展示你的技术实力的时候到了!
-
分享。组建一个相关的听众群体,讲述一个引人入胜的故事。
-
行动。你传达的数据洞察是否对决策产生了影响?
在本文中,我探讨了个人在行业中的经验,阐明了大企业在每个阶段的考虑因素和挑战。
对于那些有志成为数据分析师的你们,我们整理了一些重要技能,比如将小数据集整理成汇总的数据透视表,并制作可视化图表以揭示可报告的洞察。
告诉我你是否喜欢这篇文章!
祝你旅途顺利。
在 Twitter 和 YouTube 点击这里, 点击这里 和 点击这里 找到我。
我受欢迎的人工智能、机器学习和数据科学文章
-
人工智能与机器学习:快速入门——点击这里
-
机器学习与机械建模——点击这里
-
数据科学:现代数据科学家的新时代技能——点击这里
-
生成式人工智能:大公司如何争先恐后地采用——点击这里
-
ChatGPT 与 GPT-4:OpenAI 如何赢得自然语言理解战争——点击这里
-
GenAI 艺术:DALL-E、Midjourney 和 Stable Diffusion 解析 — 这里
-
超越 ChatGPT:寻找真正的智能机器 — 这里
-
现代企业数据战略解析 — 这里
-
从数据仓库与数据湖到数据网格 — 这里
-
从数据湖到数据网格:最新架构指南 — 这里
-
Azure Synapse Analytics 实战:7 个用例解析 — 这里
-
云计算 101:为您的业务利用云 — 这里
-
数据仓库与数据建模 — 快速入门课程 — 这里
-
数据产品:为分析建立坚实基础 — 这里
-
数据民主化:5 个“数据普及”策略 — 这里
-
数据治理:分析师常见的 5 个痛点 — 这里
-
数据讲故事的力量 — 销售故事,而非数据 — 这里
-
数据分析入门:谷歌方法 — 这里
-
Power BI — 从数据建模到惊艳报告 — 这里
-
回归分析:使用 Python 预测房价 — 这里
-
分类:使用 Python 预测员工流失 — 这里
-
Python Jupyter 笔记本与 Dataiku DSS — 这里
-
常见机器学习性能指标解释 — 这里
-
在 AWS 上构建 GenAI — 我的第一次体验 — 这里
-
数学建模与机器学习在 COVID-19 中的应用 — 这里
-
工作的未来:你的职业在人工智能时代安全吗?— 这里
数据科学家的 Docker 容器简介
一个实用的教程,教你如何使用 Docker 容器设置本地开发环境
·
关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 12 月 20 日
--
图片来源:Tom Fask,来自 Pexels
动机
数据科学家日常工作的重要组成部分是管理和维护开发环境。当开发环境保持更新并且紧密反映生产环境时,我们的工作会顺利很多;否则,事情就会变得混乱。在较大的环境中,对 CI/CD 管道和 DevOps 的熟练掌握非常有利。提供易于集成和投入生产的开发成果是数据科学家的首要任务。
这就是容器发挥作用的地方;通过封装我们的开发环境,它们可以帮助我们节省时间和精力。
在这篇博客文章中,我将提供逐步指南,说明如何设置 Docker 环境。我将使用一个 Linux 环境,Python 版本为 3.8,并连接到你选择的 Git 仓库。为了说明,我创建了一个包含一个算法的仓库,该算法可以判断给定的文本是更可能是 AI 生成的还是由人类编写的。
谁可以从使用 Docker 容器环境中受益?
如果你是一个正在进行副项目的开发者,并且不关心将其部署到生产环境中,那么你可能不需要使用容器。然而,如果你是使用 CI/CD 流水线的团队的一部分,那么这就是一种必要性。
我能想到一些日常的例子,说明容器化是多么重要:
-
假设你正在处理多个具有不同依赖项和环境设置的项目。如果不使用容器,在它们之间切换可能会非常具有挑战性。
-
一个新的开发者加入团队,你希望他能轻松地设置工作环境。如果没有容器,他或她将不得不在本地机器上工作,而本地机器可能与其他团队成员的要求和属性不同。
-
如果你对依赖项进行了某些测试或更改,而这些依赖项现在彼此不兼容,在这种情况下,回滚更改将是一个耗时的任务。另一种选择是创建一个新的容器并恢复正常操作。
什么是容器?
容器的概念最早在 1970 年代被提出。可以把容器想象成一个隔离的工作环境——一个我们可以从零开始定义的服务器。我们可以决定这个服务器的属性,比如操作系统、Python 解释器版本和库依赖。服务器依靠你的机器资源来维持。容器的另一个属性是,它没有访问我们的存储,除非我们明确授予权限。一个好的做法是挂载一个我们希望包含在容器范围内的文件夹。
总而言之,使用容器包括以下步骤:
1. 使用 Dockerfile 定义容器环境
2. 基于 Dockerfile 创建 Docker 镜像
3. 基于 Docker 镜像创建容器
这个过程可以很容易地在团队成员之间共享,容器可以根据需要反复创建。
什么是 Dockerfile?
如上所述,容器是运行我们算法的封装环境。这个环境由负责容器化的 Docker 扩展支持。
为了实现这一点,我们首先需要定义一个 Dockerfile,该文件指定我们的系统要求。把 Dockerfile 想象成一个文档或“配方”,它定义了我们的容器模板,也就是 Docker 镜像。
这里有一个我们将在本教程中使用的 Dockerfile 示例:
FROM ubuntu:20.04
# Update and install necessary dependencies
RUN apt-get update && \
apt-get install -y python3-pip python3.8 git && \
apt-get clean
# Set working directory
WORKDIR /app
# Copy the requirements file and install dependencies
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
# Install transformers separately without its dependencies
RUN pip3 install --no-cache-dir transformers
这个 Dockerfile 包含几个重要步骤:
(1) 我们导入用于创建 Ubuntu 环境的基础镜像。
(2) 我们安装 pip、python 和 git。apt-get 是一个用于包管理的 Linux 命令。
(3) 我们设置工作目录名称(在此示例中为 /app)
(4) 我们安装 requirements.txt 文件中详细列出的依赖
Dockerfile 为我们提供了很多灵活性。例如,我的仓库依赖于 transformers 库及其依赖项,因此我单独安装了它(Dockerfile 的最后一行)。
注意 — 使用容器在速度和灵活性方面提供了许多好处,但也有缺点。其中之一就是安全性。由不可信资源上传的容器镜像可能包含恶意内容。确保你使用的是可信来源,并且容器配置正确。另一种选择是使用安全工具如 snyk,它可以扫描你的 Docker 镜像以查找潜在的漏洞。
图片来源于 Tom Fask,来自 Pexels
设置容器
初步前提 在我们创建 Docker 容器之前,首先需要确保我们的本地工作环境已准备好。让我们确保我们拥有以下检查清单:
1. 使用 VS Code 作为我们的代码编辑器: code.visualstudio.com/
2. 版本控制管理的 Git: git-scm.com/downloads
3. Github 用户: github.com/
完成所有这些前提条件后,请确保登录你安装的 Docker 应用程序。这将使我们能够创建 Docker 容器并跟踪其状态。
步骤 1 — 克隆仓库
首先,让我们选择一个仓库进行操作。这里我提供了一个包含算法的仓库,该算法通过结合模型在给定文本下的困惑度值和拼写错误数量来估计文本是否由 AI 生成。较高的困惑度意味着 LLM 更难预测下一个词,因此不是由人类生成的。
仓库链接:
[## GitHub - Idoleshem/setup_a_local_container
通过在 GitHub 上创建帐户来贡献于 Idoleshem/setup_a_local_container 的开发。
在 GitHub 上,点击 code 并复制 HTTPS 地址,如下所示:
图片作者
然后,打开 VS Code,克隆一个你希望包含在容器中的仓库。确保 VS Code 已连接到你的 GitHub 账户。或者,你也可以初始化一个新的 Git 仓库。
图片作者
步骤 2 — 创建 Docker 镜像
通过打开终端并复制粘贴以下命令来完成此操作:
docker build -t local_container_intro .
这可能需要几分钟时间,直到你看到你的 Docker 镜像被创建。点击 Docker 图标,变更将被反映出来。一旦我们创建了 Docker 镜像,我们就不再需要运行这个命令。我们将使用的唯一命令是 docker run。
local_container_intro 是 Docker 镜像的名称,你可以将其更改为你想要的任何名称。
作者提供的图片
一旦创建了 Docker 镜像,你可以在 IMAGES 窗口中看到它。
步骤 3 — 创建一个 Docker 容器
为了授予容器访问你克隆的仓库的权限,请记得在 docker run 命令中包括你的项目路径。我们将使用以下命令创建容器,并将其命名为“local_container_instance”:
docker run -it --name local_container_instance -v /pate/to/your/project/folder :/project local_container_intro
作者提供的图片
创建容器后,你可以在 CONTAINERS 窗口中查看它。为了实际使用它,点击“attach visual studio code”。这将打开一个新窗口,反映你的容器化环境。这个环境包括你的代码,你可以在左下角看到你的容器名称。打开终端并运行“pip list”,检查所有依赖项是否已安装。确保安装任何可能需要的 Python 扩展。
作者提供的图片
就这样,剩下的就是开始开发 😃
总结一下,这篇博客文章提供了设置本地容器的逐步指南。我强烈推荐使用容器,并利用它们在开发中的高灵活性和便利性。
PyTorch 2:卷积神经网络
·
关注 发表在Towards Data Science ·16 分钟阅读·2023 年 2 月 13 日
--
图片来源于 Adobe Stock
简介
在本系列的前一部分,我们使用了CIFAR-10 数据集并介绍了 PyTorch 的基础知识:
-
张量及其相关操作
-
数据集和 DataLoader
-
构建基本神经网络
-
基本模型训练和评估
我们为 CIFAR-10 数据集开发的图像分类模型只能在验证集上达到 53%的准确率,并且在正确分类一些类别的图像(如鸟类和猫类,大约 33–35%)时表现非常挣扎。这是预期中的,因为我们通常会使用卷积神经网络进行图像分类。在本教程系列的这一部分,我们将专注于 CNN 及其在 CIFAR-10 上提高图像分类性能。
CNN 基础
在我们深入代码之前,让我们讨论一下卷积神经网络的基础,以便更好地理解我们的代码在做什么。如果你对 CNN 的工作原理很熟悉,可以跳过这一部分。
与前馈网络(如我们在系列前一部分中开发的那种)相比,CNN 具有不同的架构,并由不同类型的层组成。在下图中,我们可以看到典型 CNN 的一般架构,包括它可以包含的不同层类型。
图片来源:作者
卷积网络中通常存在的三种层是:
-
卷积层(红色虚线轮廓)
-
池化层(蓝色虚线轮廓)
-
全连接层(红色和紫色实线轮廓)
卷积层
CNN 的定义组件和第一层是卷积层,它由以下内容组成。
-
输入数据(在本例中为图像)
-
过滤器
-
特征图
卷积层与密集连接层的真正区别在于卷积操作。我们不会深入探讨卷积的定义,但如果你真的感兴趣并想深入了解,这篇文章很好地解释了数学定义,并提供了一些非常具体的例子。如果你感兴趣,我强烈推荐它!
那么,为什么卷积层比密集/完全连接层在图像数据中更好呢?本质上,密集层会学习输入中的全局模式,而卷积层具有学习局部和空间模式的优势。这听起来可能有点模糊或抽象,因此让我们看一个示例来说明这意味着什么。
图片来源:作者
在图像的左侧,我们可以看到一个基本的二维黑白图像 4 在卷积层中的表示方式。红色方框是过滤器/特征检测器/卷积核,正在对图像进行卷积。在右侧是相同图像输入到密集连接层中的方式。你可以看到与红色卷积核框架内相同的 9 个图像像素。注意左侧像素如何在空间上被分组,邻近其他像素。然而在右侧,这 9 个像素不再是邻居。
通过这一点,我们可以看到当图像被展平并在全连接/线性层中表示时,空间/位置相关的信息是如何丢失的。这就是为什么卷积神经网络在处理图像数据时更强大的原因。输入数据的空间结构得以保持,图像中的模式(边缘、纹理、形状等)可以被学习。
这本质上是使用 CNN 处理图像的原因,但现在让我们讨论一下方法。我们来看一下输入数据的结构、我们一直讨论的‘滤波器’,以及将所有这些结合在一起时卷积的样子。
输入数据
CIFAR-10 数据集包含 60,000 张 32x32 彩色图像,每张图像被表示为一个三维张量。每张图像将是一个 (32,32,3)
张量,其中维度为 32(高度)x 32(宽度)x 3(R-G-B 颜色通道)。下图展示了数据集中飞机的全彩图像中分离出的三种不同颜色通道(RGB)。
图像来源:作者
图像通常被认为是二维的,因此可能容易忽略因为它们有三个颜色通道,它们实际上是以三维表示的!
滤波器
卷积层中的滤波器(也称为内核或特征检测器)是一个权重数组,实质上以滑动窗口的方式扫描图像,在每个停靠点计算点积,并将该点积输出到一个新的数组中,称为特征图。滑动窗口扫描称为卷积。让我们看看这个过程的插图,以帮助理解发生了什么。
3x3 滤波器(蓝色)在输入(红色)上卷积以创建特征图(紫色)的插图。图像来源:作者
每一步卷积中的点积计算插图。图像来源:作者
需要注意的是,滤波器的权重在每一步中保持不变。就像全连接层中的权重一样,这些值在训练过程中被学习,并通过反向传播在每次训练迭代后进行调整。不过,插图并没有展示全部情况。在训练 CNN 时,你的模型在卷积层中不会只有一个滤波器。通常情况下,一个卷积层中有 32 或 64 个滤波器,事实上,在我们本教程中开发的模型中,一个层中将有多达 96 个滤波器。
最后,尽管滤波器的权重是训练的主要参数,还有可以调整的超参数:
-
层中的滤波器数量
-
滤波器的维度
-
步幅(滤波器每一步移动的像素数)
-
填充(滤波器如何处理图像的边界)
我们不会深入讨论这些超参数,因为这并不是一个全面的 CNN 讲解,但这些是需要了解的重要因素。
池化层
池化层与卷积层类似,都是通过一个过滤器对输入数据(通常是卷积层输出的特征图)进行卷积。然而,与特征检测不同,池化层的功能是降维或下采样。最常用的两种池化类型是最大池化和平均池化。最大池化中,过滤器在输入上滑动,每一步都会选择具有最大值的像素作为输出。在平均池化中,过滤器会输出过滤器所经过的像素的平均值。
全连接层
最后,CNN 通常在卷积层和池化层之后会有全连接层,这些层将执行图像分类任务中的分类,例如本教程中的任务。
现在我们已经了解了卷积神经网络的结构和操作方式,让我们进入有趣的部分,在 PyTorch 中训练我们自己的 CNN!
设置
与本教程的第一部分一样,我建议使用 Google Colab 来跟随,因为你将已经设置好 Python 环境,并安装了 PyTorch 和其他库,以及一个用于训练模型的 GPU。
所以,如果你正在使用 Colab,为了确保你正在使用 GPU,请前往 Runtime
并点击 Change runtime type
。
图片来源:作者
在对话框中选择 GPU
并保存。
图片来源:作者
现在你在 Colab 中有 GPU 访问权限,我们可以用 PyTorch 验证你的设备。所以首先,让我们处理一下导入:
如果你想检查你可以访问的 GPU,输入并执行 torch.cuda.get_device_name(0)
,你应该能看到你的设备输出。Colab 提供了几种不同的 GPU 选项,因此你的输出会因获得的访问权限而有所不同,但只要运行此代码时不出现 RuntimeError: No CUDA GPUs are available
,你就正在使用 GPU!
我们可以将 GPU 设置为 device
,这样在开发模型时,可以通过引用 device
将模型分配到 GPU 上,如果没有 CUDA GPU 设备,也可以使用 CPU。
接下来,让我们设置一个随机种子,以确保结果是可重复的,并下载我们的训练数据,同时设置 transform
以将图像转换为张量并归一化数据。
下载完成后,让我们查看数据集中包含的类别:
最后,让我们设置训练和测试数据加载器:
现在我们准备好构建我们的模型了!
构建 CNN
在 PyTorch 中,nn.Conv2d
是用于图像输入数据的卷积层。Conv2d
的第一个参数是输入的通道数,因此对于我们的第一个卷积层,我们将使用 3,因为彩色图像将有 3 个颜色通道。在第一个卷积层之后,这个参数将取决于前一层输出的通道数。第二个参数是层中卷积操作输出的通道数。这些通道就是在卷积层介绍中讨论的特征图。最后,第三个参数是卷积核或过滤器的大小。这可以是像3
这样的整数值表示3x3
的卷积核,或者是像(3,3)
这样的元组。因此,我们的卷积层将采用nn.Conv2d(in_channels, out_channels, kernel_size)
的形式。可以添加其他可选参数,包括(但不限于):stride
、padding
和dilation
。我们将在卷积层conv4
中使用stride=2
。
在经过一系列卷积层之后,我们需要使用一个展平层将特征图展平成可以输入到线性层的数据,为此我们将使用nn.Flatten()
。我们可以使用nn.BatchNorm1d()
来应用批量归一化,并且需要传入特征数量作为参数。最后,我们的线性全连接层使用nn.Linear()
构建,它也会将特征数量作为第一个参数,并且指定输出特征的数量作为第二个参数。
为了开始定义我们模型的基本架构,我们将定义一个继承自 PyTorch nn.Module
类的ConvNet
类。然后,我们可以将每一层定义为类的属性,并根据需要构建它们。一旦我们指定了层的架构,就可以通过创建一个forward()
方法来定义模型的流动。我们可以用激活函数包裹每一层,在我们的案例中我们将使用relu
。我们可以在层之间应用dropout
,通过传入前一层和p
的元素丢弃概率(默认为 0.5)。最后,我们创建我们的模型对象并将其附加到我们的device
上,以便在 GPU 上进行训练。
训练和测试函数
如果你完成了本教程的第一部分,我们的训练和测试函数将与之前创建的完全相同,只是我们将在训练方法中返回loss
,并在测试方法中返回loss
和correct
的数量,以便在调整超参数时使用。
最后,我们在基础模型训练之前定义损失函数和优化器。
让我们训练模型。
图片来源:作者
仅仅经过 10 个周期,61.7%的表现远远好于我们训练的全连接模型!显然,CNN 更适合图像分类,但通过延长训练时间和调整超参数,我们可以挤出更多的性能。在我们进行这些操作之前,让我们快速查看一下内部情况,看看滤波器是什么样的。回忆一下,滤波器的像素是我们模型中的可训练参数。这不是训练图像分类模型的必要步骤,我们也不会发现太多有用的信息,但看到模型内部发生了什么还是很有趣的。
可视化滤波器
我们可以编写一个函数来绘制模型中特定层的滤波器。我们只需指定要查看的层,并将其传递到我们的函数中。
让我们查看一下第一卷积层(conv1
)中的滤波器,因为这些滤波器直接应用于图像。
以下是输出,包含了来自我们的conv1
卷积层的 48 个滤波器的可视化。我们可以看到每个滤波器是一个包含不同值或颜色的 3x3 张量。
图片来源:作者
如果我们的滤波器是 5x5 的,我们会在图中看到这种差异。回忆一下,使用nn.Conv2d
我们可以通过第三个参数来改变滤波器的大小,所以如果我们想要一个 5x5 的,conv1
将会是这样的:
如果我们使用新的 5x5 滤波器重新训练模型,输出将会变成这样:
图片来源:作者
如我之前提到的,这里没有太多有用的信息,但 nonetheless 仍然很有趣。
超参数优化
对于本教程,我们将调整的超参数是卷积层中的滤波器数量和线性层中的神经元数量。目前这些值被硬编码在我们的模型中,因此为了使其可调,我们需要使模型可配置。我们可以在模型的__init__
方法中使用参数(c1
、c2
和l1
),并用这些值创建模型的层,这些值将在调整过程中动态传递。
我们肯定不只限于调整这些超参数。实际上,学习率和批量大小通常也在需要调整的超参数列表中,但由于我们将使用网格搜索,我们必须大幅减少可调变量的数量,以保持训练时间合理。
接下来,让我们定义一个用于搜索空间的字典,以及一个保存给我们最佳结果的参数的字典。由于我们使用网格搜索进行优化,列表中列出的每种超参数的每种组合都会被使用。你可以很容易地为每个超参数的列表添加更多值,但每增加一个值都会大幅增加运行时间,因此建议从以下值开始以节省时间。
早停法
在我们的优化过程中,一个重要的组成部分是早停的使用。由于我们将进行多个训练,每次训练都需要花费相当长的时间,因此如果训练性能没有改进,我们希望提前结束训练。如果模型没有改善,继续训练没有意义。
实质上,我们将跟踪模型在每个训练周期后产生的最低损失。然后我们定义一个 tolerance
(容忍度),它指定模型必须在多少个周期内达到更好的损失。如果在指定的容忍度内未能达到更低的损失,该次训练将被终止,我们将转到下一个超参数组合。如果你像我一样喜欢检查训练过程,我们可以将更新日志记录到控制台,并查看早停计数器何时增加,方法是将 self.verbose
设置为 True
。你可以将其硬编码到 EarlyStopping
类中,也可以在优化过程中实例化 EarlyStopping
对象时更改 verbose
值。
图像增强
在设置我们的超参数优化方法以挤出一些额外的性能并遏制训练数据上的过拟合之前,我们还有最后一件事要做。图像增强是一种技术,通过对图像应用随机变换,实质上创建了“新的”人工数据。这些变换可能包括:
-
将图像旋转几度
-
水平/垂直翻转图像
-
裁剪
-
轻微的亮度/色调变化
-
随机缩放
包括这些随机变换将提高模型的泛化能力,因为增强的图像将与原始图像相似,但有所不同。内容和模式将保持不变,但数组表示将有所不同。
PyTorch 通过 torchvision.transforms
模块使图像增强变得简单。如果我们有几个想要应用的变换,我们可以用 Compose
将它们串联在一起。需要记住的一件事是,图像增强每应用一个变换需要一点计算量,而且这一过程应用于数据集中的每张图像。对数据集应用大量不同的随机变换将增加训练所需的时间。因此,现在让我们限制变换,以免训练时间过长。如果你想添加更多的变换,可以查看PyTorch 文档关于图像变换和增强,并将它们添加到 Compose
列表中。
一旦我们选择了数据增强的变换,我们可以像应用归一化和将图像转换为张量一样,将它们应用到数据集上。
现在我们已经在训练数据上设置了图像增强,我们准备好设置我们的超参数优化方法了。
定义优化方法
我们可以创建一个类(HyperSearch
),包含超参数值配置、详细报告设置、一个报告列表(以便在优化完成后查看每个配置的表现),以及一个用于存储最佳性能配置的变量。
接下来,我们可以在HyperSearch
类中创建一个方法来执行网格搜索,并用每种超参数组合进行训练。首先,我们将配置EarlyStopping
,使其容忍度=tolerance=3
,并设置它保存每个超参数组合的权重。如果self.verbose
设置为True
,我们可以在控制台看到当前正在训练的超参数组合。
接下来,我们用我们设计的CoinfigNet
模型来定义model
,并传入l1
、c1
和c2
值,同时选择损失函数和优化器,并设置我们的训练和验证DataLoaders
。我们将保持较低的训练轮数,因为我们没有时间,也不希望完全训练每一个组合。目标是了解哪个组合在分类数据集时表现最好,然后我们可以拿到那个模型,进行全面训练,以查看其在完整训练周期中的表现。
现在,我们定义训练循环,基本上与之前相同,只是现在我们将保存train
和test
方法的损失,以便early_stopping
可以跟踪训练进度(或缺乏进度)。最后,在每个轮次后,结果将被保存到报告中,并更新最佳损失值。
我们可以将整个超参数优化周期的结果输出为一个漂亮的表格,其中我们将能看到每次运行的超参数配置以及相应的损失和准确率。
将所有这些代码整合在一起,我们的HyperSearch
类应该如下所示:
调整时间到!
现在我们可以调整我们的超参数了!通过使用%%time
,在整个调优过程完成后,我们可以精确地看到所用的时间。我们将学习率保持为lrate=0.001
和批量大小为batch_sz=512
,用之前定义的search_space
实例化HyperSearch
,将verbose
设置为True
或False
(你可以选择),然后调用optimize()
方法开始调优。
注意: 在我的机器上(NVIDIA RTX 3070),完成这个过程大约花费了 50 分钟,所以如果你在 Colab 上使用提供的 GPU,预计也会花费差不多的时间。
一旦整个优化周期完成,你应该会得到如下表格:
图片来源:作者
结果
从表格来看,最佳结果来自 Run 00,其c1=48
,c2=96
,l1=256
。0.84 的损失和 71.24%的准确率是一个不错的改进,特别是考虑到它仅仅训练了 10 个轮次!
现在,我们已经找到了在 10 个 epoch 中表现最佳的超参数,接下来就对这个模型进行微调吧!我们可以训练更多的 epoch,并稍微降低学习率以尝试挤出更多的性能。首先,让我们定义我们希望使用的模型,并设置批量大小和学习率:
最后,我们可以将epochs
设置为 50,并更改保存权重的路径。让训练周期运行,如果进展停止,早停将终止训练。
早停应该在达到 50 个 epoch 之前终止训练,并且应该达到约 77%的准确率。
图片来源:作者
现在我们已经调整了超参数,找到了最佳配置,并对模型进行了微调,是时候更深入地评估模型的表现了。
模型评估
在这种情况下,我们的测试数据集实际上是我们的验证数据。我们将重用验证数据来评估模型,但通常你会在超参数调整后使用真正的测试数据进行模型评估。让我们加载优化后的模型,准备没有应用任何图像增强的test_dataloader
,并运行test()
进行评估。
这应该输出准确率和损失:
图片来源:作者
整体表现不错,但每个类别的表现对我们更有用。以下代码将输出我们模型在数据集中每个类别的准确率:
执行这个代码块将给出以下输出:
图片来源:作者
我们的模型在飞机、汽车、青蛙、船和卡车类别上的表现相当不错。同样值得注意的是,它最难处理的类别是狗和猫,这也是在本系列前面部分中,完全连接模型表现最差的类别。
混淆矩阵
我们可以通过混淆矩阵获得更多关于性能的洞察。让我们建立一个混淆矩阵,然后获取一个良好的可视化效果。
定义了confusion_matrix
之后,我们可以使用 Seaborn 库帮助我们可视化它。
图片来源:作者
该表的两个维度是“实际”值和“预测”值。我们希望大部分数据对齐在中心对角线处,即实际和预测类别相同。从错误预测中,我们可以看到模型经常混淆cats
和dogs
,这两个类别的准确率最低。
总体数据很好,但每个类别的精度和召回率将提供更有意义的数据。让我们首先查看每个类别的召回率。
每个类别的召回率
图片来源:作者
每个类别的精度
图片来源:作者
样本模型预测
最后,让我们给模型喂入几张图像,查看其做出的预测。让我们创建一个函数来准备图像数据以供查看:
现在,我们可以准备测试数据,并创建另一个函数来获取n
个预测样本。
调用函数,传入你想要采样的图像数量。输出将给出每张图像从左到右的真实标签和预测类别。
图片来源:作者
图片来源:作者
利用卷积网络进行超参数调优和图像增强确实有助于提高在 CIFAR-10 数据集上的表现!一如既往,感谢阅读,希望你对 PyTorch 和用于图像分类的 CNN 有了一些了解。包含所有代码的完整 Notebook 可以在GitHub上找到。
TorchData 介绍:Conceptual Captions 3M 的实操指南
学习如何使用 TorchData 和 DataPipes 高效地流式处理像 Conceptual Captions 3M 这样的巨大数据集。
·
关注 发表在Towards Data Science ·8 分钟阅读·2023 年 3 月 22 日
--
照片由Quinten de Graaf拍摄,发布在Unsplash上。
概述
在处理大型数据集时,尤其是在深度学习中,直接将数据集下载到本地进行训练可能不切实际。相反,在训练过程中直接流式传输数据集可能是一种更高效的方法。在本教程中,我们将介绍 TorchData 库,并演示如何使用它来流式传输 Conceptual Captions 3M 数据集,该数据集包含 300 万张图片及其对应的说明。
注意: Conceptual Captions 在开源许可证下免费提供。有关更多信息,请参阅许可证和官方 GitHub 仓库。
我们将首先简要介绍 TorchData 及其主要组件。接着,我们会演示如何为 Conceptual Captions 3M 数据集设置数据管道,最后,我们将展示如何使用该管道实时流式传输数据集的示例。
本教程旨在对完全初学者友好,因此我们会详细解释每个概念和代码片段。让我们开始吧!
图片由Braden Collum拍摄,来源于Unsplash
TorchData 简介
TorchData 是一个提供常用数据加载方法的库,用于轻松构建灵活且高效的数据管道。以下是TorchData README的摘录:
它引入了可组合的 Iterable 风格和 Map 风格的构建块,称为 DataPipes,这些构建块与 PyTorch 的
DataLoader
能够很好地配合使用。这些内置的 DataPipes 提供了加载文件(来自本地或云存储)、解析、缓存、转换、过滤等多种功能。
DataPipes
TorchData 的核心是 DataPipes,它可以被看作是数据管道的可组合构建块。DataPipes 实际上是重命名和重新设计的 PyTorch Dataset
,专为组合使用而设计。它们接受对 Python 数据结构的访问函数,IterDataPipes
使用__iter__
,MapDataPipes
使用__getitem__
,并返回一个应用了轻微变换的新访问函数。
通过将 DataPipes 链接在一起,我们可以创建复杂的数据管道,将流式操作作为一等公民。这使我们能够高效地处理大型数据集,并减少对本地存储的需求。
示例
让我们从一个示例开始,以熟悉基本概念。我们将创建一个基本的 DataPipe,它接收一个整数的可迭代对象,并将其值翻倍:
from torchdata.datapipes.iter import IterDataPipe
class DoublingDataPipe(IterDataPipe):
def __init__(self, source_data):
self.source_data = source_data
def __iter__(self):
for item in self.source_data:
yield item * 2
# Initialize the DataPipe with a list of integers.
source_data = [1, 2, 3, 4, 5]
doubling_data_pipe = DoublingDataPipe(source_data)
# Iterate over the DataPipe and print the results.
for doubled_item in doubling_data_pipe:
print(doubled_item)
这段代码定义了一个自定义的 DoublingDataPipe
,它接受一个数据源的可迭代对象(在我们的例子中,是一个整数列表),并生成源数据中每个项的两倍。当我们运行这段代码时,我们应该会看到输出的值是加倍的:
2
4
6
8
10
TorchData 还提供了许多内置的管道方法,这本可以使这个例子更加简洁。
.map()
、.filter()
、.shuffle()
和 .chain()
方法等,使我们能够快速构建强大且灵活的数据管道,而无需为每个操作编写自定义 DataPipes。它们可以直接应用于 IterDataPipe
,以执行常见的数据处理任务,例如应用转换、过滤数据、随机化顺序和连接多个 DataPipes。
让我们探索几个示例。我们将使用之前示例中的 source_data
列表作为 DataPipes 的输入。
.map()
: 对 DataPipe 中的每个元素应用一个函数。
from torchdata.datapipes.iter import IterableWrapper
data_pipe = IterableWrapper([1, 2, 3, 4, 5])
# Double each element in the DataPipe.
doubled_data_pipe = data_pipe.map(lambda x: x * 2)
for item in doubled_data_pipe:
print(item)
# Output: 2, 4, 6, 8, 10
这里重新列出了之前的 DoublingDataPipe
示例的单行代码:data_pipe.map(lambda x: x * 2)
。
2. .filter()
: 根据条件过滤 DataPipe 中的元素。
from torchdata.datapipes.iter import IterableWrapper
data_pipe = IterableWrapper([1, 2, 3, 4, 5])
# Filter out odd elements in the DataPipe.
even_data_pipe = data_pipe.filter(lambda x: x % 2 == 0)
for item in even_data_pipe:
print(item)
# Output: 2, 4
3. .shuffle()
: 随机打乱 DataPipe 中元素的顺序。
from torchdata.datapipes.iter import IterableWrapper
data_pipe = IterableWrapper([1, 2, 3, 4, 5])
# Shuffle the elements in the DataPipe.
shuffled_data_pipe = data_pipe.shuffle(buffer_size=5)
for item in shuffled_data_pipe:
print(item)
# Output: Randomly ordered elements, e.g., 3, 1, 5, 2, 4
4. .chain()
: 连接两个或更多 DataPipes。
from torchdata.datapipes.iter import IterableWrapper
data_pipe1 = IterableWrapper([1, 2, 3])
data_pipe2 = IterableWrapper([4, 5, 6])
# Chain the two DataPipes together.
chained_data_pipe = data_pipe1.chain(data_pipe2)
for item in chained_data_pipe:
print(item)
# Output: 1, 2, 3, 4, 5, 6
设置 Conceptual Captions 3M
照片由 John Schnobrich 提供,来自 Unsplash
在本节中,我们将逐步介绍如何为 Conceptual Captions 3M 数据集设置数据管道。该数据集包含 300 万张图像及其对应的标题,因此在本地下载以进行训练是不可行的。相反,我们将使用 TorchData 在训练过程中直接流式传输数据集。
依赖项
在深入代码之前,我们先安装本教程所需的依赖项。你需要以下 Python 包:
-
torchdata
-
tqdm(用于显示进度条)
-
aiohttp(用于异步 HTTP 请求)
-
Pillow(用于处理图像)
你可以使用 pip 安装它们:
pip install torchdata tqdm aiohttp Pillow
异步图像下载的异步助手函数
由于我们将从远程 URL 流式传输图像,我们需要一种有效的方式来并行下载它们。我们将使用 aiohttp
库来进行异步 HTTP 请求。
首先,让我们创建一个助手函数 async_get_image
,它接受 aiohttp.ClientSession
和一个 URL 作为输入,并返回下载的图像:
import aiohttp
from PIL import Image
import io
async def async_get_image(
session: aiohttp.ClientSession, url: str
) -> Optional[Image.Image]:
try:
resp = await session.get(url)
image_bytes = await resp.read()
return Image.open(io.BytesIO(image_bytes))
except Exception:
# If an exception occurs, such as a timeout, invalid URL, etc, just
# return None, and the caller can handle skipping this
return None
接下来,我们将创建另一个助手函数 async_batch_get_images
,它接受一个 URL 序列,并返回一组下载的图像。它使用 aiohttp.ClientSession
来并行运行多个请求,以尽量减少开销,这对实时从远程 URL 获取大量图像的性能至关重要。
async def async_batch_get_images(
urls: Sequence[str], timeout: float = 1.0
) -> List[Optional[Image.Image]]:
client_timeout = aiohttp.ClientTimeout(total=timeout)
async with aiohttp.ClientSession(timeout=client_timeout) as session:
return await asyncio.gather(*[async_get_image(session, url) for url in urls])
ParallelSampleLoader DataPipe
图片来源:Tom Strecker 在 Unsplash
现在我们已经有了下载图像的辅助函数,让我们创建一个自定义的ParallelSampleLoader
DataPipe,它接受一个包含图像 URL 和说明的元组的IterDataPipe
,并返回一个迭代器,用于获取下载的图像及其对应的说明。
from torchdata.datapipes.iter import IterDataPipe
class ParallelSampleLoader(IterDataPipe):
def __init__(
self, dp: IterDataPipe[Tuple[str, str]], buffer_size: int = 256
) -> None:
super().__init__()
self.dp = dp
self.buffer_size = buffer_size
def __iter__(self) -> Generator[Tuple[Image.Image, str], None, None]:
pipe: IterDataPipe[List[Tuple[str, str]]] = self.dp.batch(self.buffer_size)
for batch in pipe:
# The batch is a list of tuples, where the first element is the
# caption, and the second element is the URL of the image.
captions = [x[0] for x in batch]
image_urls = [x[1] for x in batch]
images = asyncio.run(async_batch_get_images(image_urls))
for image, caption in zip(images, captions):
if image is not None:
yield image, caption
综合
最后,我们将创建一个 conceptual_captions_3m
函数,它接受一个 split
参数("train" 或 "val"),并返回一个包含下载图像及其对应说明的 IterDataPipe
。
from torchdata.datapipes.iter import HttpReader, LineReader
def _datapipe_from_tsv_url(
tsv_url: str, buffer_size: int = 256
) -> IterDataPipe[Tuple[Image.Image, str]]:
pipe = HttpReader([tsv_url])
pipe = LineReader(pipe, return_path=False)
# LineReader downloads raw bytes. Decode them to strings, then split.
pipe = pipe.map(lambda line: line.decode("utf-8").split("\t"))
return ParallelSampleLoader(pipe, buffer_size=buffer_size)
def conceptual_captions_3m(
split: str = "train", buffer_size: int = 256
) -> IterDataPipe[Tuple[Image.Image, str]]:
return _datapipe_from_tsv_url(tsv_url=TSV_URLS[split], buffer_size=buffer_size)
Conceptual Captions 3M
设置好数据管道后,我们现在可以使用它实时流式传输 Conceptual Captions 3M 数据集。在这个示例中,我们将使用 conceptual_captions_3m
函数创建一个用于训练集的 IterDataPipe
,并迭代数据集,打印出前 10 个说明并显示它们对应的图像尺寸:
# Create the IterDataPipe for the training split.
data_pipe = conceptual_captions_3m(split="train")
for i, (image, caption) in enumerate(data_pipe):
if i >= 10:
break
print(f"Caption {i + 1}: {caption}")
print(f"Image size: {image.size}")
这里有另一个简单的示例,用于基准测试通过此管道加载图像的速度。我们使用 tqdm
库创建一个进度条,以显示每秒迭代的样本数量。
from tqdm import tqdm
# Create the IterDataPipe for the training split.
data_pipe = conceptual_captions_3m(split="train")
for image, caption in tqdm(data_pipe):
# Don't do anything here. We just want to test the loading speed.
pass
下载速度非常依赖于你的互联网连接。大多数云服务提供商的虚拟机具有极快的网络连接,这使得它们非常适合使用 DataPipes。我在我的 Google Cloud 虚拟机上运行了上述基准测试,下载速度大约为每秒 120 张图像。对于使用单个 GPU 启用的机器进行的小规模 ML 训练,这应该是足够快的。(很少有 ML 模型的训练速度超过每秒 120 张图像,除非你使用更昂贵的 GPU 硬件。)
结论
TorchData 提供了一种强大而灵活的方式来处理大数据集,通过提供可组合的 DataPipes 和内置的管道方法。利用这些工具,你可以有效地实时流式传输和处理数据,无需提前将数据下载到本地磁盘。这种方法不仅节省了时间和存储资源,还能将数据集更无缝地集成到你的项目中。现在,所有的数据集逻辑都已在你的 Python 项目中编码,不再需要在 README 中详细的设置说明(这是许多项目中的常见情况)。通过将管道封装在代码中,TorchData 允许更好的可重复性和可移植性,使其成为现代机器学习项目中处理大数据集的宝贵工具。
通过本教程,你现在应该对如何使用 TorchData 库来创建用于流式传输大数据集(如 Conceptual Captions 3M)的数据管道有了更好的理解。这种方法可以应用于其他大型数据集,并且可以轻松地适应各种数据处理和增强任务。
介绍一个用于检测 GPT 生成文本的数据集
如何为 ChatGPT 检测模型创建数据集
·
关注 发表在 Towards Data Science ·4 min read·2023 年 2 月 8 日
--
照片由 Markus Spiske 提供,来源于 Unsplash
随着像 ChatGPT 这样的大型语言模型取得突破性成功,人们正在寻找将这些模型应用于日常生活的创新方式。然而,这也导致了一些意外后果,如学生在作业和考试中作弊、使用 ChatGPT 发布研究论文,甚至诈骗者利用这些模型来欺骗他人。为了解决这些问题,对能够检测 GPT 模型生成文本的模型的需求越来越大。构建强大 GPT 生成文本检测模型的一个关键要求是访问一个包含大量人工编写和 GPT 生成响应的数据集。本文介绍了这样一个数据集,由 150k 人工编写和 GPT 生成的维基百科主题回应组成,并概述了未来生成类似数据集的框架。
数据集
GPT-wiki-intro 数据集 在 Hugging Face 上提供。该数据集包含 150,000 个维基百科主题的人工编写和 GPT(Curie)生成的介绍。用于生成 GPT 响应的提示如下:
f"200 word wikipedia style introduction on '{title}'
{starter_text}"
其中 title
是维基百科页面的标题,starter_text
是介绍段落中的前 7 个词。该数据集还具有有用的元数据,如 title_len、wiki_intro_len、generated_intro_len、prompt_tokens、generated_text_tokens 等。数据集的架构如下:
----------------------------------------------------------------------------
|Column |Datatype|Description |
|---------------------|--------|-------------------------------------------|
|id |int64 |ID |
|url |string |Wikipedia URL |
|title |string |Title |
|wiki_intro |string |Introduction paragraph from wikipedia |
|generated_intro |string |Introduction generated by GPT (Curie) model|
|title_len |int64 |Number of words in title |
|wiki_intro_len |int64 |Number of words in wiki_intro |
|generated_intro_len |int64 |Number of words in generated_intro |
|prompt |string |Prompt used to generate intro |
|generated_text |string |Text continued after the prompt |
|prompt_tokens |int64 |Number of tokens in the prompt |
|generated_text_tokens|int64 |Number of tokens in generated text |
----------------------------------------------------------------------------
该数据集以 Creative Commons 许可证共享,可以用于分发、混合、调整和构建。生成该数据集的代码可以在这里找到。
框架
该数据集是检测 GPT 生成文本的一般用途的良好起点。但如果你有特定的用例,例如检测 ChatGPT 生成的测试/作业问题的回答、识别消息是否由人类还是聊天机器人发送,或者你需要特定领域的大型数据集,你可以使用该框架生成自己的数据集。数据集生成过程涉及三个主要步骤:
-
获取锚数据集
-
清理锚数据集
-
用人工编写/GPT 生成的数据扩充数据集
获取锚数据集
在这一步中,我们获取锚数据集。这将是为特定用例现成的现有数据。对于GPT-wiki-intro 数据集,锚数据集是wikipedia 数据集,该数据集包含各种语言的清理文章。对于检测考试和作业作弊,锚数据集可以是之前学生提交的问答对。如果你没有明确定义的锚数据集,可以探索 Hugging Face 和 Kaggle 上与用例匹配的各种开源数据集。锚数据集不必是人工编写的,我们也可以使用 GPT 生成的数据作为锚数据。例如,我们可以使用ChatGPT 提示响应库的数据。
清理锚数据集
一旦我们拥有锚数据集,我们需要清理数据以保留最相关的信息。ChatGPT 检测模型对文本长度非常敏感。这些模型在较小的文本上表现不佳。我们可以设定一个阈值,并过滤掉任何短于该阈值的响应。例如,在 GPT-wiki-intro 数据集中,我们过滤掉所有引言长度少于 150 字或超过 350 字的行。我们还过滤掉标题超过三个词的所有行。在这一步中,我们还需要决定数据集的总大小。由于使用人工编写或 GPT 生成的响应来增强数据将会很昂贵,我们需要确定我们用例所需的最小数据集大小。
增强数据集,加入人工编写或 GPT 生成的数据
这是数据集生成的最终步骤。在此步骤中,我们通过人工编写或 GPT 生成的数据来增强锚数据集。这一步中最重要的是确定用于生成 GPT 响应的提示或由人类回答的问题。为了最终确定提示,我们可以利用OpenAI Playground测试不同的提示、模型、温度、频率惩罚和存在惩罚。为了增加数据集的多样性,我们可以最终确定 n 个提示,并统一使用这些提示以获取响应。在人类回应的情况下,我们需要通过向小型调查人群提供不同的问题变体来最终确定问题,然后检查结果以最终确定 n 个问题。一旦提示或问题确定,我们可以使用 OpenAI API 生成 GPT 生成的响应,或使用像 Mechanical Turk 这样的服务获取人工编写的响应。
结论
总之,随着像 ChatGPT 这样的巨大语言模型的广泛使用,对能够检测这些模型生成的文本的模型的需求也在不断增加。本文介绍了GPT-wiki-intro 数据集并概述了生成类似数据集的框架。这些数据集的可用性将在开发用于检测 GPT 生成文本的强大模型以及应对这些模型使用的不良后果方面发挥关键作用。
引用
如果您觉得这项工作有帮助,请考虑引用:
@misc {aaditya_bhat_2023,
author = { {Aaditya Bhat} },
title = { GPT-wiki-intro (Revision 0e458f5) },
year = 2023,
url = { https://huggingface.co/datasets/aadityaubhat/GPT-wiki-intro },
doi = { 10.57967/hf/0326 },
publisher = { Hugging Face }
}
介绍 KeyLLM — 使用 LLM 进行关键词提取
原文:
towardsdatascience.com/introducing-keyllm-keyword-extraction-with-llms-39924b504813
使用 KeyLLM、KeyBERT 和 Mistral 7B 来提取关键词
·发表于Towards Data Science ·阅读时间 9 分钟·2023 年 10 月 5 日
--
大型语言模型(LLMs)变得越来越小、快速且高效。直到我开始考虑它们用于迭代任务,如关键词提取。
在创建了KeyBERT之后,我觉得是时候扩展该包以包括 LLMs 了。它们非常强大,我希望为将来这些模型可以在更小的 GPU 上运行做好准备。
介绍[KeyLLM](https://maartengr.github.io/KeyBERT/guides/keyllm.html)
,这是 KeyBERT 的一个扩展,允许你使用任何 LLM 来提取、创建甚至微调关键词!
在本教程中,我们将通过使用最近发布的 Mistral 7B 模型的[KeyLLM](https://maartengr.github.io/KeyBERT/guides/keyllm.html)
进行关键词提取。
我们将从安装一系列在本示例中将要使用的包开始:
pip install --upgrade git+https://github.com/UKPLab/sentence-transformers
pip install keybert ctransformers[cuda]
pip install --upgrade git+https://github.com/huggingface/transformers
我们正在从主分支安装sentence-transformers
,因为它修复了社区检测的功能,我们将在最后几个用例中使用。我们对transformers
也做了同样的处理,因为它尚不支持 Mistral 架构。
你也可以跟随Google Colab Notebook以确保一切按预期工作。
🤖 加载模型
在之前的教程中,我们展示了如何对原始模型的权重进行量化,以便在不遇到内存问题的情况下运行。
在过去几个月里,TheBloke为我们辛勤工作,对数百个模型进行了量化。
这样,我们可以直接下载模型,这将大大加快速度。
我们将从加载模型本身开始。我们将把 50 层卸载到 GPU 上。这将减少 RAM 使用量,而使用 VRAM。如果你遇到内存错误,减少这个参数(gpu_layers
)可能会有所帮助!
from ctransformers import AutoModelForCausalLM
# Set gpu_layers to the number of layers to offload to GPU.
# Set to 0 if no GPU acceleration is available on your system.
model = AutoModelForCausalLM.from_pretrained(
"TheBloke/Mistral-7B-Instruct-v0.1-GGUF",
model_file="mistral-7b-instruct-v0.1.Q4_K_M.gguf",
model_type="mistral",
gpu_layers=50,
hf=True
)
在加载了模型本身后,我们想创建一个🤗 Transformers 管道。
这样做的主要好处是,这些管道在许多教程中都有出现,通常作为后台在软件包中使用。到目前为止,ctransformers
的原生支持程度还没有transformers
那么高。
由于 Mistral 的分词器相对较新,尚无法使用ctransformers
加载。因此,我们使用原始仓库中的分词器。
from transformers import AutoTokenizer, pipeline
# Tokenizer
tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-Instruct-v0.1")
# Pipeline
generator = pipeline(
model=model, tokenizer=tokenizer,
task='text-generation',
max_new_tokens=50,
repetition_penalty=1.1
)
📄 提示工程
让我们用一个非常基础的示例来看看这是否有效:
>>> response = generator("What is 1+1?")
>>> print(response[0]["generated_text"])
"""
What is 1+1?
A: 2
"""
完美!它可以处理一个非常基本的问题。为了关键词提取的目的,让我们探索一下它是否能处理更多的复杂性。
prompt = """
I have the following document:
* The website mentions that it only takes a couple of days to deliver but I still have not received mine
Extract 5 keywords from that document.
"""
response = generator(prompt)
print(response[0]["generated_text"])
我们得到如下输出:
"""
I have the following document:
* The website mentions that it only takes a couple of days to deliver but I still have not received mine
Extract 5 keywords from that document.
**Answer:**
1\. Website
2\. Mentions
3\. Deliver
4\. Couple
5\. Days
"""
它表现得非常好!然而,如果我们希望输出的结构在输入文本不同的情况下保持一致,我们需要给 LLM 一个示例。
这就是更高级提示工程的用武之地。与大多数大型语言模型一样,Mistral 7B 期望特定的提示格式。当我们想展示“正确”交互的样子时,这非常有帮助。
提示模板如下:
基于该模板,让我们创建一个关键词提取的模板。
它需要有两个组件:
-
Example prompt
- 这将用于向 LLM 展示“良好”输出的样子 -
Keyword prompt
- 这将用于请求 LLM 提取关键词
第一个组件,example_prompt
,将仅仅是一个正确提取关键词的示例,符合我们感兴趣的格式。
格式是一个关键组件,因为它确保 LLM 始终以我们希望的方式输出关键词:
example_prompt = """
<s>[INST]
I have the following document:
- The website mentions that it only takes a couple of days to deliver but I still have not received mine.
Please give me the keywords that are present in this document and separate them with commas.
Make sure you to only return the keywords and say nothing else. For example, don't say:
"Here are the keywords present in the document"
[/INST] meat, beef, eat, eating, emissions, steak, food, health, processed, chicken</s>"""
第二个组件,keyword_prompt
,实际上是example_prompt
的重复,只不过有两个变化:
-
它还不会有输出。那将由 LLM 生成。
-
我们使用
KeyBERT
的[DOCUMENT]标签来指示输入文档的位置。
我们可以使用[DOCUMENT]将文档插入到您选择的位置。这个选项有助于我们在需要时更改提示的结构,而不必将提示设置在特定位置。
keyword_prompt = """
[INST]
I have the following document:
- [DOCUMENT]
Please give me the keywords that are present in this document and separate them with commas.
Make sure you to only return the keywords and say nothing else. For example, don't say:
"Here are the keywords present in the document"
[/INST]
"""
最后,我们将两个提示合并以创建我们的最终模板:
>>> prompt = example_prompt + keyword_prompt
>>> print(prompt)
"""
<s>[INST]
I have the following document:
- The website mentions that it only takes a couple of days to deliver but I still have not received mine.
Please give me the keywords that are present in this document and separate them with commas.
Make sure you to only return the keywords and say nothing else. For example, don't say:
"Here are the keywords present in the document"
[/INST] meat, beef, eat, eating, emissions, steak, food, health, processed, chicken</s>
[INST]
I have the following document:
- [DOCUMENT]
Please give me the keywords that are present in this document and separate them with commas.
Make sure you to only return the keywords and say nothing else. For example, don't say:
"Here are the keywords present in the document"
[/INST]
"""
现在我们有了最终的提示模板,我们可以开始探索KeyBERT
与KeyLLM
的一些有趣的新功能。我们将首先探索仅使用 Mistral 的 7B 模型的KeyLLM
🗝️ 使用KeyLLM
进行关键词提取
使用原生KeyLLM
进行关键词提取是极其简单的;我们只需让它从文档中提取关键词即可。
通过 LLM 从文档中提取关键词的想法很简单,可以轻松测试你的 LLM 及其功能。
使用KeyLLM
很简单,我们首先通过keybert.llm.TextGeneration
加载我们的 LLM,并给它之前创建的提示模板。
🔥 提示 🔥:如果你想使用不同的 LLM,如 ChatGPT,你可以在这里查看已实现算法的完整概述。
from keybert.llm import TextGeneration
from keybert import KeyLLM
# Load it in KeyLLM
llm = TextGeneration(generator, prompt=prompt)
kw_model = KeyLLM(llm)
在准备好我们的KeyLLM
实例后,只需对你的文档运行.extract_keywords
即可:
documents = [
"The website mentions that it only takes a couple of days to deliver but I still have not received mine.",
"I received my package!",
"Whereas the most powerful LLMs have generally been accessible only through limited APIs (if at all), Meta released LLaMA's model weights to the research community under a noncommercial license."
]
keywords = kw_model.extract_keywords(documents)
我们得到以下关键词:
[['deliver',
'days',
'website',
'mention',
'couple',
'still',
'receive',
'mine'],
['package', 'received'],
['LLM',
'API',
'accessibility',
'release',
'license',
'research',
'community',
'model',
'weights',
'Meta']]
这些关键词看起来很棒!
你可以调整提示以指定你想提取的关键词类型,它们可以有多长,甚至在 LLM 是多语言的情况下返回哪种语言。
🚀 使用KeyLLM
进行高效关键词提取
对成千上万的文档进行 LLM 迭代并不是最有效的方法!相反,我们可以利用嵌入模型使关键词提取更高效。
这个过程如下。首先,我们将所有文档嵌入并转换为数值表示。其次,我们找出哪些文档彼此最相似。我们假设高度相似的文档将具有相同的关键词,因此不需要为所有文档提取关键词。第三,我们只从每个簇中的一个文档中提取关键词,并将这些关键词分配给同一簇中的所有文档。
这更高效,而且相当灵活。簇是纯粹基于文档之间的相似性生成的,而不考虑簇结构。换句话说,它本质上是在寻找我们预期具有相同关键词集合的近重复文档。
要使用KeyLLM
实现这一点,我们提前嵌入文档并将它们传递给.extract_keywords
。阈值指示文档需要多相似才能分配到同一个簇。
将此值增加到像.95 这样的数字将识别近乎相同的文档,而将其设置为像.5 这样的值将识别关于相同主题的文档。
from keybert import KeyLLM
from sentence_transformers import SentenceTransformer
# Extract embeddings
model = SentenceTransformer('BAAI/bge-small-en-v1.5')
embeddings = model.encode(documents, convert_to_tensor=True)
# Load it in KeyLLM
kw_model = KeyLLM(llm)
# Extract keywords
keywords = kw_model.extract_keywords(
documents,
embeddings=embeddings,
threshold=.5
)
我们得到以下关键词:
>>> keywords
[['deliver',
'days',
'website',
'mention',
'couple',
'still',
'receive',
'mine'],
['deliver',
'days',
'website',
'mention',
'couple',
'still',
'receive',
'mine'],
['LLaMA',
'model',
'weights',
'release',
'noncommercial',
'license',
'research',
'community',
'powerful',
'LLMs',
'APIs']]
在这个例子中,我们可以看到前两个文档被聚类在一起并获得了相同的关键词。我们只传递两个文档,而不是传递所有三个文档。如果你有成千上万的文档,这可以显著加快速度。
🏆 使用KeyBERT
和KeyLLM
进行高效关键词提取
之前,我们手动将嵌入传递给KeyLLM
以进行零-shot 关键词提取。我们可以通过利用KeyBERT
进一步扩展这个例子。
由于KeyBERT
生成关键词并嵌入文档,我们可以利用这一点不仅简化流程,还可以向 LLM 建议一些关键词。
这些建议的关键字可以帮助 LLM 决定使用哪些关键字。此外,这允许KeyBERT
中的所有内容与KeyLLM
一起使用!
这种高效的关键字提取方法,使用KeyBERT
和KeyLLM
只需要三行代码!我们创建了一个 KeyBERT 模型,并将之前创建的嵌入模型分配给 LLM:
from keybert import KeyLLM, KeyBERT
# Load it in KeyLLM
kw_model = KeyBERT(llm=llm, model='BAAI/bge-small-en-v1.5')
# Extract keywords
keywords = kw_model.extract_keywords(documents, threshold=0.5)
我们得到以下关键字:
>>> keywords
[['deliver',
'days',
'website',
'mention',
'couple',
'still',
'receive',
'mine'],
['deliver',
'days',
'website',
'mention',
'couple',
'still',
'receive',
'mine'],
['LLaMA',
'model',
'weights',
'release',
'license',
'research',
'community',
'powerful',
'LLMs',
'APIs',
'accessibility']]
就这样!使用KeyLLM
,你可以利用大型语言模型来帮助创建更好的关键字。我们可以选择从文本本身提取关键字,或者让 LLM 提出关键字。
通过将KeyLLM
与KeyBERT
结合使用,我们可以通过一些计算和建议来增加其潜力。
更新:我在 YouTube 上上传了一个视频版本,更深入地讲解了如何使用 KeyLLM:
感谢阅读!
如果你和我一样,对 AI 和/或心理学充满热情,请随时在LinkedIn上添加我,在Twitter上关注我,或者订阅我的Newsletter。你还可以在我的个人网站上找到一些我的内容。
所有没有来源信用的图像都是作者创建的——也就是说,所有图像都是我自己制作的 😉
介绍 PeekingDuck 计算机视觉
开源的最先进计算机视觉模型,代码行数极少
·
关注 发表在 Towards Data Science ·4 min read·2023 年 1 月 3 日
--
图片由 Vlad Tchompalov 提供,来源于 Unsplash。
介绍
计算机视觉项目可能非常令人畏惧,涉及各种工具和软件包,如 OpenCV、TensorFlow 和 PyTorch 等。除了需要熟悉所涉及的工具和 API,还需要正确组合各种软件包,以便整个计算机视觉管道能够正常工作。
例如,OpenCV 处理[H, W, C]
格式的 BGR 通道图像,而 TensorFlow 处理相同格式但使用 RGB 通道的图像,PyTorch 处理[C, H, W]
格式的 RGB 通道图像。由于这种不一致性,图像格式必须在不同库之间传递时不断修改。像这样的许多问题(以及其他问题!)会导致大量冗余代码,我们希望避免这种情况。
如果我们可以通过一个统一的管道来简化计算机视觉管道,那将会怎样呢?
-
开源且没有如 GPL-3.0 之类的限制,以减少成本。
-
模块化以适应各种用例。
-
采用最先进的技术以实现最大性能。
-
最小化以减少管道复杂性。
事实证明,所有这些问题在一定程度上都通过 PeekingDuck 得到解决,这是 AI Singapore 最近发布的一个计算机视觉软件包!
PeekingDuck
PeekingDuck 是一个计算机视觉框架,其特点是:
-
开源(Apache 2.0)— 无费用或限制。
-
模块化 — 混合搭配各种模块以解决不同的用例。
-
最先进的计算机视觉推理 — 强大的深度学习模型。
-
最小 — 实际上无需 Python 代码!
通过 pip 等包管理器安装 PeekingDuck 作为 Python 包后,可以直接从命令行/终端使用该包,便于与其他应用程序的直接集成。
安装 PeekingDuck
PeekingDuck 作为一个 Python 包进行安装:
pip install peekingduck
节点 — PeekingDuck 的基本构建块
使用 PeekingDuck,计算机视觉管道是通过基本构建块称为节点来构建的。每个节点处理不同的操作,通过混合不同的节点可以创建不同的管道。到目前为止,PeekingDuck 具有 6 种不同类型的节点:
-
输入 — 从实时摄像头或视频/图像文件中将图像数据输入到管道中。
-
增强 — 预处理图像数据。
-
模型 — 执行计算机视觉任务,如对象检测或姿态估计。
-
涉猎 — 后处理模型输出。
-
绘制 — 可视化模型输出,例如边界框。
-
输出 — 将模型输出保存到磁盘。
人员跟踪管道
使用 PeekingDuck 很简单!在本节中,我们将演示如何使用 PeekingDuck 创建一个人员跟踪管道!
初始化 PeekingDuck
第一步是在指定目录(在此案例中为person_tracking/
)中初始化 PeekingDuck。
mkdir person_tracking
cd person_tracking
peekingduck init
这将会在 person_tracking/
下创建一个名为 pipeline_config.yml
的配置文件以及一些其他源代码文件。为了让 PeekingDuck 按我们希望的方式运行,我们必须修改 pipeline_config.yml
。
在我们的例子中,pipeline_config.yml
应包含以下行:
nodes:
- input.visual:
source: venice-2-train.mp4
- model.jde
- dabble.statistics:
maximum: obj_attrs["ids"]
- draw.bbox
- draw.tag:
show: ["ids"]
- draw.legend:
show: ["cum_max"]
- output.media_writer:
output_dir: output/
我们为此任务使用以下 nodes
:
-
input.visual
— 指定加载图像数据的文件。我们使用的是一个由 MOT15 数据集的 Venice-2 图像 拼接而成的视频。 -
model.jde
— 指定要使用的模型。对于人物跟踪,我们使用 Joint Detection and Embedding (JDE) 模型。 -
dabble.statistics
— 根据模型的输出进行统计计算。在这种情况下,我们计算每帧检测到的最大 ID 数量。 -
draw.bbox
— 在每帧上绘制检测到的边界框。 -
draw.tag
— 为每个边界框绘制相应的标签。 -
draw.legend
— 绘制累计检测到的最大数量。 -
output.media_writer
— 将模型的预测结果输出到磁盘。
通过混合和匹配不同的节点,我们可以构建不同的管道来解决不同的计算机视觉应用场景。详细的节点列表可以在 PeekingDuck 的网站 上找到。
准备数据
下一步是准备数据。在我们的例子中,我们使用 OpenCV 将 MOT15 数据集的 Venice-2 图像 拼接成一个名为 venice-2-train.mp4
的视频文件,帧率为 30
,分辨率为 [1920, 1080]
。
import cv2
import os
w = cv2.VideoWriter("venice-2-train.mp4",
cv2.VideoWriter_fourcc(*"MP4V"),
30, [1920, 1080])
files = sorted(os.listdir("MOT15/train/Venice-2/img1"))
for f in files:
im = cv2.imread(os.path.join("MOT15/train/Venice-2/img1", f))
w.write(im)
w.release()
运行 PeekingDuck
在初始化了 PeekingDuck 和数据之后,剩下的就是从命令行运行管道:
peekingduck run
管道的输出将保存在 output/
下,如 pipeline_config.yml
中所指定,可以以视频或 .gif
图像的形式进行可视化。检测到的边界框已覆盖到每个跟踪人员上,并标注了每个相应的跟踪 ID。累计的最大跟踪 ID 数量也显示在每帧的左下角。
PeekingDuck 人物跟踪输出。图形由作者创建。原始图像是来自 MOT15 数据集的 Venice-2 图像。
请注意,除了准备数据外,我们在使用 PeekingDuck 进行人物跟踪时没有编写一行 Python 代码!
结论
计算机视觉已经取得了长足进展,我们现在可以使用许多出色的包,如 PeekingDuck。PeekingDuck 提供了开源的、模块化的最先进的计算机视觉模型,代码量最少,使得任何人都可以相对轻松地进行计算机视觉项目!
参考文献
介绍 PyCircular:一个用于圆形数据分析的 Python 库
圆形数据在分析和建模时可能会面临独特的挑战
·
关注 发表在 数据科学的未来 ·8 分钟阅读·2023 年 1 月 24 日
--
图片由 Patrick McManaman 提供,发布在 Unsplash
在这篇文章中,我介绍了PyCircular,一个专门用于圆形数据分析的 Python 库。作为其中一位作者,我很高兴与大家分享这个强大的工具,以帮助解决处理圆形数据时面临的挑战。
圆形数据,如表示角度、方向或时间戳的数据,在分析和建模时可能会带来独特的挑战。圆形数据的特性可能会导致在应用传统的线性和基于核的方法时遇到困难,因为这些方法不适合处理圆形数据的周期性特征。此外,计算均值和标准差时也可能出现问题,因为这些度量对圆形数据并不适用。
PyCircular 通过提供一组专门针对圆形数据的工具和功能来解决这些挑战。该库包括各种圆形统计方法,如分布、核函数和置信区间。此外,它还包括可视化工具,如圆形直方图和分布图,以帮助你更好地理解数据。
https://github.com/albahnsen/pycircular
本文的剩余部分将深入探讨处理圆形数据的独特挑战,并展示如何通过一系列示例使用 PyCircular 来应对这些挑战。你将看到 PyCircular 如何有效处理圆形数据的周期性特征,并计算有意义的集中趋势和离散度量。你还将学习如何利用库的可视化工具来更好地理解和解释圆形数据。通过阅读本文,你将对如何有效地使用 PyCircular 来分析和建模圆形数据有一个扎实的了解。
在训练机器学习模型时,我们必须拥有一个包含输入变量(特征)和相应输出变量(标签)的数据集。模型学习将特征映射到标签上,训练的目标是找到最佳的映射参数集。
在一些应用中,模型的特征包括用户、事务、登录等的描述信息。在这些场景中,大多数都有时间戳信息,可能是事件时间、星期几或月中的某一天。如果目标是根据过去的事件预测未来事件,你可以使用时间戳作为特征。例如,你可以将一天中的时间、星期几或一年中的月份作为特征,来预测交通量或能源消耗。
然而,处理机器学习问题中的时间的最佳方法将取决于你试图解决的具体问题和数据的结构。
让我们看看如何使用 PyCircular 来分析这种复杂行为。
首先,让我们安装库并加载一些示例合成数据。
!pip install pycircular
使用来自 load_transactions 的数据集,我们可以看到从 2020 年 1 月 1 日到 2020 年 7 月 29 日共有 349 个观察(交易)。
作者提供的图片
然后,绘制每次观察时间的直方图,我们看到大多数例子发生在下午 5 点到早上 7 点之间,中午发生的例子很少。此外,当将一天中的小时视为标量变量时,可能会出现一些问题。
-
一个问题是一天中的小时具有周期性,这意味着一天结束时的值(24:00)与一天开始时的值(00:00)相关。然而,当一天中的小时被视为标量变量时,这种周期性关系未被考虑,这可能导致不准确或误导性的结果。
-
另一个问题是一天中的小时往往与其他变量相关联,如星期几或季节。例如,工作日的高峰时段可能比周末更拥堵。然而,当一天中的小时被视为标量变量时,这些相关性未被考虑,可能导致偏见或误导性结果。
-
第三个问题是一天中的小时可能会受到季节、星期几或节假日等不同因素的影响。这些因素可能会对一天中的小时的行为和模式产生重大影响。因此,如果在使用一天中的小时作为标量变量时未考虑这些信息,可能会导致不准确的结论。
为了克服这些问题,一种解决方案是使用周期编码技术,如正弦和余弦编码,以融入数据的周期性特征。另一种解决方案是将其他相关变量(如星期几或季节)包括在模型中,以考虑潜在的相关性。此外,在分析数据时,考虑影响一天中的小时的不同因素也很重要。
对于我们的例子,首先通过使用圆形直方图来更好地理解我们的数据集。
作者提供的图片
然后,计算标量或算术均值
作者提供的图片
处理示例时间时,特别是分析如时间均值这样的特征时,一个问题是很容易犯下使用算术均值的错误。确实,算术均值不是平均时间的正确方法,因为如上图所示,它未考虑时间特征的周期性行为。例如,四个交易时间分别为 2:00、3:00、22:00 和 23:00 的算术均值是 12:30,这与实际情况不符,因为没有交易发生在那个时间附近。
我们可以通过将交易时间建模为周期变量来克服这一限制,特别是使用 von Mises 分布(Fisher, 1996)。von Mises 分布,也称为周期正态分布,是一种围绕圆周的包裹正态分布变量的分布。von Mises 分布的一个示例集(D)定义为:
其中
是周期均值和周期标准差。在这篇论文中,我们展示了它们的计算。
图像由作者提供
现在,计算了周期均值和标准差后,我们可以估计 von Mises 分布。
图像由作者提供
这种方法为事件时间的分布提供了良好的近似。然而,当使用仅有一个模式的统计分布时,如果分布与数据集不匹配,可能会很难准确建模。此外,如果数据集是多模态的(即有多个峰值),单一模式的分布将无法捕捉数据中的所有变化。这可能导致基于模型的预测或推断不准确。
通过使用 von Mises 核分布对数据进行建模可以克服这一问题。
克服使用仅有一个模式的统计分布问题的一种方法是使用基于核的方法,如核密度估计(KDE)。
KDE 是一种用于估计随机变量概率密度函数的非参数方法。它通过用平滑且对称的核函数(如 von Mises)替换每个数据点的点质量来工作。结果的 PDF 估计是以每个数据点为中心的核函数的总和。
通过使用核函数,KDE 可以平滑出任何单模态分布,并捕捉数据中的多个模式,使其成为建模多模态数据集的更灵活的方法。此外,核密度估计是非参数的,这意味着它不对数据的潜在分布做任何假设。
然而,值得注意的是,选择合适的核函数很重要,且在使用 KDE 时存在一些挑战,如带宽选择和维度诅咒。
图像由作者提供
总结而言,使用像 KDE 这样的基于核的方法与 von Mises 结合,可以通过允许对多模态数据集进行更灵活和稳健的建模,帮助克服使用仅有一个模式的统计分布的问题。
使用核函数创建新特征
最后,我们可以将核应用于新的观察值,并创建一个可以作为机器学习模型输入的新特征。
图片来源于作者
我们可以看到,在中午的观察值概率非常低(0.017),因为在训练核时,该时间没有任何观察值。
总之,这种方法使我们能够通过创建数据中时间信息的更强健的表示来有效处理时间戳。通过在特征工程中使用 von Mises 核,我们可以生成准确捕捉数据中时间模式细微差别的新特征。这种方法可以克服将日期视为标量变量的局限性,并提升机器学习模型的性能。
下一步
-
带宽参数(bw)的选择对于模型的性能至关重要,pycircular 库提供了一系列优化方法来为给定的数据集选择最佳的 bw。
-
要评估核的有效性,重要的是要进行准确性测试并与其他方法的结果进行比较。
-
虽然一天中的时间是一个重要的时间特征,但还需要调查其他时间变量如星期几和月份的天数如何影响模型的性能。
-
核可以用于机器学习模型,它可以作为特征工程过程的一部分进行集成,并应用于输入数据,以创建更好地捕捉数据中时间模式的新特征。
我将在后续的帖子中展示如何处理这些问题。
了解更多关于PyCircular的信息,请访问github.com/albahnsen/pycircular
介绍 PyDicom 及其类、方法和属性
PyDicom……超越像素数据
·
关注 发布于 Towards Data Science ·12 分钟阅读·2023 年 1 月 4 日
--
如果你是 Python 爱好者,并且正在处理医疗数据,特别是 DICOM 数据,你可能已经听说过很棒的 Python 包 PyDicom。在这篇文章中,你将学习这个包的基础知识。我们将讨论 PyDicom 的类(DataSet、DataElement、Sequence)以及一些你可以应用于 PyDicom 类的有用方法和属性。
照片由 Owen Beard 提供,来自 Unsplash
PyDicom 是一个用于处理 DICOM 文件(如医学图像、报告和放射治疗对象)的纯 Python 包。PyDicom 使读取和写入这些复杂文件变得简单,转换为自然的 Python 结构以便于操作。
DICOM(医学数字成像和通信)是医学成像系统的标准语言。当你听到“DICOM”这个词时,你可能会认为它只是医学图像。但 医疗数据科学家/分析师 可能会将其视为任何其他 医学数据集 的宝贵财富。因为它不仅仅是像素数据。DICOM 文件包含许多与患者、成像模态等相关的有用数据。
处理 DICOM 元数据并访问特定属性对任何人(包括数据科学家/分析师)在处理 DICOM 文件时都是有益的。幸运的是,PyDicom 提供了许多有用的函数、方法和属性来处理 DICOM 文件。
感谢 PyDicom 的创建者和贡献者
让我们开始编码
当我想探索一个新的 Python 包时,我通常会从使用该包的一般示例开始,然后逐渐深入。我的主要目的是学习如何提取多个 DICOM 文件的元数据,并将其转换为我可以用于分析和探索的数据集。
所以……让我们从一个常见的 PyDicom 示例开始我们的旅程。
我们将讨论以下内容:
读取 DICOM 文件的示例
PyDicom 的核心元素
PyDicom 数据集
*PyDicom 数据集的 * .keys() 方法
.values() 方法
.elements() 方法
.group_dataset() 方法
.dir() 方法
PyDicom 数据集的属性 pixel_array 属性
PyDicom 数据元素
PyDicom 序列
PyDicom 与 ImageIO
读取 DICOM 文件的示例
要使用 pydicom 读取 DICOM 文件,你可以按照以下步骤操作:
- 安装 pydicom:要使用 pydicom,你需要通过 pip 安装它。
# Install PyDicom Package
!pip install pydicom
Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/
Collecting pydicom
Downloading pydicom-2.3.1-py3-none-any.whl (2.0 MB)
|████████████████████████████████| 2.0 MB 5.4 MB/s
Installing collected packages: pydicom
Successfully installed pydicom-2.3.1
- 导入 pydicom 库:在你的 Python 脚本中,你需要导入 pydicom 库以便使用它。
# Import the pacakage
import pydicom
网上有很多 DICOM 样本。你也可以使用你自己的 DICOM 文件。在这篇文章中,我选择了一个 数据集,包含一个患者的 99 切片胸部 CT 扫描。你可以在 Kaggle 上找到它。我将数据集保存到了我的 Drive 中,以便通过 GoogleColab 轻松访问。
注意: 有时,某些代码行的输出可能会过长和庞大。对此提前表示歉意!!!但这是一个很好的机会,让你感受一下 DICOM 文件在现实生活中的样子。
- 读取 DICOM 文件:要读取 DICOM 文件,你可以使用 dcmread 函数,它返回一个包含 DICOM 文件数据的 Dataset 对象。
# Reading a DICOM file from a specific path
dcm_data = pydicom.dcmread('/content/drive/MyDrive/Datasets/Kaggle/DICOM/dicom_lung/000000.dcm')
dcm_data
完整输出已作为 GitHub gist 添加,详细内容请参阅这个链接。 [作者提供的图片]
很明显,除了像素数据之外,还有很多 DICOM 元数据。但我们可以看到输出的模式,即 DICOM 属性是逐行写入的。每一行都有一个唯一的标签和其他核心的 DICOM 属性元素。让我们更详细地讨论它们。
PyDicom 中的核心元素
使用 .dcmread() 会封装一个 DataSet,即一个字典数据结构 {}。这个 DataSet 包含如下表示的键和值:
-
Keys: 包含你正在读取的 DICOM 文件中指定的属性的 DICOM 标签。示例键包括:
-
(0x0010, 0x0010) PatientName 属性。
-
(0x0028, 0x0010) 行数属性。
-
(0x7fe0, 0x0010) PixelData 属性。
标签的数字由两个十六进制数字组成,第一个数字表示组,第二个数字表示特定元素。因此,你可能会发现许多属性具有相同的第一个标签数字。
-
-
Values: 这个字典的值通常包含以下内容:
-
Tag: 元素的标签,例如 (0028, 0030)。
-
Keyword: 描述属性所指的内容。标签 (0028, 0030) 的关键字是“Pixel Spacing”。
VR: 这仅是两个字符,指的是元素的值表示(Value Representation),它描述了属性值的数据类型和格式。标签 (0028, 0030) 的 VR 是“DS”,即十进制字符串。你可以通过 Python 结构查看任何标签的 VR 以及它是如何表示的,详细信息请参考链接。
Value: 元素的实际值。它可以是整数、字符串、列表,甚至是Sequence,即一个属性的数据集。标签 (0028, 0030) 的值是一个包含两个浮点数的列表,分别表示沿行和列的物理距离(单位为毫米)。这个列表的一个示例是 [0.564453125, 0.564453125]。
-
dcmread() 函数的输出。[作者提供的图片]
现在我们理解了使用 PyDicom 读取 DICOM 文件需要直接操作主要类 DataSet。让我们更深入地讨论一下 DataSet 的内容。
PyDicom DataSet
DICOM DataSet 是 DICOM DataElements 的可变映射。DICOM DataSet 中的每个 DataElement,即字典中的值,都有一个唯一的标签,即字典的键,用于标识它。例如,“PatientName” 属性对应于 DICOM 标准中的标签 (0x0010, 0x0010),它标识了病人的名字数据元素。
PyDicom DataSet 类的内容。[作者提供的图片]
你可以通过多种方式访问特定的 DICOM 属性,例如:
# Extract the patient's name.
patient_name = dcm_data.PatientName
patient_name
'C3N-00247'
或者你可以通过唯一标签访问相同的属性 PatientName。例如,PatientName 属性的标签是 (0010, 0010)。注意这些数字是十六进制的,符合 DICOM 标准。
# Extract the patient's name using its unique DICOM tag (0010, 0010)
dcm_data[0x0010, 0x0010]
(0010, 0010) Patient's Name PN: 'C3N-00247'
注意,使用标签,即数据集字典的键,会返回字典的整个值,而不仅仅是属性的实际值。要仅获取实际值,你应该使用 .value 属性。
# Extract the patient's name using its unique DICOM tag (0010, 0010)
dcm_data[0x0010, 0x0010].value
'C3N-00247'
你可以决定是通过唯一标签提取特定属性,还是通过其关键词提取。现在让我们深入了解一下我们可以与 DataSet 类一起使用的一些有用的方法和属性。
PyDicom DataSet 的方法
.keys() 方法
使用.keys() 返回 DataSet 字典的键列表。当合并具有共同 DICOM 属性的多个 DICOM 文件的元数据时,这种方法可能会很有用。
# Extract the keys, the DICOM tags, that are in a DICOM file
dcm_data.keys()
.keys() 方法的输出。 [图片由作者提供]
.values() 方法
这种方法返回 DataSet 字典的值列表。这种形式有点笨重,不太适合阅读。但在某些情况下,这种方法可能对迭代值列表很有用。
# Extract the values, the DICOM attributes, that are in a DICOM file
dcm_data.values()
.values() 方法的部分输出。 [图片由作者提供]
.elements() 方法
使用 .elements() 方法可以获得 DataSet 的顶级元素。当你不需要 DICOM 文件中可能存在的 Sequences 的 DICOM 属性时,这个方法会很有用。注意下面输出图中 .elements() 方法使用时 Sequences 的表示方式。
# Extract the top-level elements of the Dataset Class
[*dcm_data.elements()]
.elements() 方法的完整输出已作为 GitHub gist 添加,查看 链接。 [图片由作者提供]
.group_dataset() 方法
如上所述,你可能会发现许多属性的第一个数字相同。这些标签,具有共同的第一个标签,通常描述一个共同的参数。例如,具有共同第一个标签 (0x0010) 的属性通常与病人相关。第一个标签为 (0x0028) 的属性描述图像像素属性。有时查看与特定参数相关的所有属性会很有帮助。使用方法 (.group_dataset) 返回一个仅包含某个组元素的 DataSet。
# Extract the attributes related to 0x0028 tag, these are related to ImagePixel
dcm_data.group_dataset(0x0028)
(0028, 0002) Samples per Pixel US: 1
(0028, 0004) Photometric Interpretation CS: 'MONOCHROME2'
(0028, 0010) Rows US: 512
(0028, 0011) Columns US: 512
(0028, 0030) Pixel Spacing DS: [0.564453125, 0.564453125]
(0028, 0100) Bits Allocated US: 16
(0028, 0101) Bits Stored US: 12
(0028, 0102) High Bit US: 11
(0028, 0103) Pixel Representation US: 0
(0028, 0106) Smallest Image Pixel Value US: 0
(0028, 0107) Largest Image Pixel Value US: 4095
(0028, 0303) Longitudinal Temporal Information M CS: 'MODIFIED'
(0028, 1050) Window Center DS: [-500, 40]
(0028, 1051) Window Width DS: [1500, 400]
(0028, 1052) Rescale Intercept DS: '-1024.0'
(0028, 1053) Rescale Slope DS: '1.0'
(0028, 1055) Window Center & Width Explanation LO: ['WINDOW1', 'WINDOW2']
.dir() 方法
返回 DataSet 中元素关键词的字母顺序列表。这是对你处理的元数据提供初步了解的好选择。
# An alphabetical list of the element keywords in the DICOM DataSet.
dcm_data.dir()
['AccessionNumber', 'AcquisitionDate', 'AcquisitionNumber', 'AcquisitionTime',
'BitsAllocated', 'BitsStored', 'BodyPartExamined', 'CTDIvol',
'ClinicalTrialTimePointDescription', 'ClinicalTrialTimePointID', 'Columns',
'ContentDate', 'ContentTime', 'ConvolutionKernel', 'DataCollectionDiameter',
'DateOfLastCalibration', 'DeidentificationMethod', 'DeidentificationMethodCodeSequence',
'DistanceSourceToDetector', 'DistanceSourceToPatient', 'EstimatedDoseSaving',
'EthnicGroup', 'Exposure', 'ExposureModulationType', 'ExposureTime',
'FilterType', 'FocalSpots', 'FrameOfReferenceUID', 'GantryDetectorTilt',
'GeneratorPower', 'HighBit', 'ImageComments', 'ImageOrientationPatient',
'ImagePositionPatient', 'ImageType', 'InstanceNumber', 'KVP', 'LargestImagePixelValue',
'LongitudinalTemporalInformationModified', 'Manufacturer', 'ManufacturerModelName',
'Modality', 'PatientAge', 'PatientBirthDate', 'PatientID', 'PatientIdentityRemoved',
'PatientName', 'PatientPosition', 'PatientSex', 'PerformedProcedureStepStartDate',
'PerformedProcedureStepStartTime', 'PhotometricInterpretation', 'PixelData',
'PixelRepresentation', 'PixelSpacing', 'PositionReferenceIndicator', 'ProtocolName',
'ReasonForStudy', 'ReconstructionDiameter', 'ReferencedImageSequence',
'ReferencedStudySequence', 'ReferringPhysicianName', 'RequestedProcedureDescription',
'RescaleIntercept', 'RescaleSlope', 'RotationDirection', 'Rows', 'SOPClassUID',
'SOPInstanceUID', 'SamplesPerPixel', 'SeriesDate', 'SeriesDescription',
'SeriesInstanceUID', 'SeriesNumber', 'SeriesTime', 'SingleCollimationWidth',
'SliceLocation', 'SliceThickness', 'SmallestImagePixelValue', 'SoftwareVersions',
'SourceImageSequence', 'SpecificCharacterSet', 'SpiralPitchFactor', 'StudyComments',
'StudyDate', 'StudyDescription', 'StudyID', 'StudyInstanceUID', 'StudyPriorityID',
'StudyStatusID', 'StudyTime', 'TableFeedPerRotation', 'TableHeight', 'TableSpeed',
'TimeOfLastCalibration', 'TotalCollimationWidth', 'WindowCenter', 'WindowCenterWidthExplanation',
'WindowWidth', 'XRayTubeCurrent']
特定属性集的过滤器可以作为参数实现到 .dir() 方法中。
# Extract all the attributes that have "Pixel" in its keywords
dcm_data.dir('Pixel')
['LargestImagePixelValue',
'PixelData',
'PixelRepresentation',
'PixelSpacing',
'SamplesPerPixel',
'SmallestImagePixelValue']
PyDicom DataSet 的属性
有一些属性可以与 DataSet 类一起使用。但我只会指出一个,最重要和最常用的属性,即pixel_array。
# Extract the image pixels
dcm_data.pixel_array
array([[356, 244, 201, ..., 190, 224, 76],
[309, 387, 370, ..., 122, 17, 6],
[334, 476, 486, ..., 29, 46, 47],
...,
[ 98, 188, 186, ..., 156, 86, 125],
[ 66, 138, 139, ..., 221, 74, 81],
[168, 173, 100, ..., 188, 135, 147]], dtype=uint16)
# Image representation
im = dcm_data.pixel_array
plt.imshow(im, cmap='gray')
plt.axis('off')
plt.title('Axial Slice of a Chest-CT')
plt.show()
[作者提供的图片]
PyDicom DataElement
正如我们上面提到的,DataSet 是一个 PyDicom 类,包含一组 DICOM 标签作为键,值作为 DICOM DataElements。DataElements 是在读取 DICOM 文件时可能会发现的属性值或元数据。能够访问特定信息对于你的工作或研究非常有用,而无需在如此庞大的文本数据中搜索。你可以使用属性在特定 DataElement 中访问特定信息。
PyDicom DataElement 类的内容及访问特定信息的方法。[作者提供的图片]
# Return the element's keyword (if known) as str
kwrds = dcm_data[0x0010, 0x0010].keyword
# Return the DICOM dictionary name for the element as str
name = dcm_data[0x0010, 0x0010].name
# Return the element's Value Representation
vr = dcm_data[0x0010, 0x0010].VR
# Return the element's value.
value = dcm_data[0x0010, 0x0010].value
print("The element's Keyword : ", kwrds)
print("The element's name : ", name)
print("The element's Value Representation : ", vr)
print("The element's value : ", value)
The element's Keyword : PatientName
The element's name : Patient's Name
The element's Value Representation : PN
The element's value : C3N-00247
PyDicom 序列
PyDicom DataSet 中的 DataElement 可能包含字符串、整数、列表,甚至是数据序列。该序列源自 Python 的列表,但它甚至可以是一个 DataSet。让我们看看如何访问特定序列中的参数。由于我们正在处理的文件信息量庞大,让我们使用 .dir(*filter) 方法。通过合适的过滤器参数,我们可以提取“序列”的名称。
# Extract all the attributes that have "Sequence" in its keywords
dcm_data.dir('Sequence')
['DeidentificationMethodCodeSequence',
'ReferencedImageSequence',
'ReferencedStudySequence',
'SourceImageSequence']
让我们尝试使用去标识化方法代码序列。
# Return a specific Sequence, "DeidentificationMethodCodeSequence"
dcm_data.DeidentificationMethodCodeSequence
<Sequence, length 8>
要打印出序列中的所有元素,我们可以使用索引。
# Extract the contents of a Sequence
dcm_data.DeidentificationMethodCodeSequence[:]
[(0008, 0100) Code Value SH: '113100'
(0008, 0102) Coding Scheme Designator SH: 'DCM'
(0008, 0104) Code Meaning LO: 'Basic Application Confidentiality Profile',
(0008, 0100) Code Value SH: '113101'
(0008, 0102) Coding Scheme Designator SH: 'DCM'
(0008, 0104) Code Meaning LO: 'Clean Pixel Data Option',
(0008, 0100) Code Value SH: '113104'
(0008, 0102) Coding Scheme Designator SH: 'DCM'
(0008, 0104) Code Meaning LO: 'Clean Structured Content Option',
(0008, 0100) Code Value SH: '113105'
(0008, 0102) Coding Scheme Designator SH: 'DCM'
(0008, 0104) Code Meaning LO: 'Clean Descriptors Option',
(0008, 0100) Code Value SH: '113107'
(0008, 0102) Coding Scheme Designator SH: 'DCM'
(0008, 0104) Code Meaning LO: 'Retain Longitudinal Temporal Information Modified Dates Option',
(0008, 0100) Code Value SH: '113108'
(0008, 0102) Coding Scheme Designator SH: 'DCM'
(0008, 0104) Code Meaning LO: 'Retain Patient Characteristics Option',
(0008, 0100) Code Value SH: '113109'
(0008, 0102) Coding Scheme Designator SH: 'DCM'
(0008, 0104) Code Meaning LO: 'Retain Device Identity Option',
(0008, 0100) Code Value SH: '113111'
(0008, 0102) Coding Scheme Designator SH: 'DCM'
(0008, 0104) Code Meaning LO: 'Retain Safe Private Option']
要访问序列中的特定信息,我们可以使用其索引。
# Return the info of specific line in a Sequence
dcm_data.DeidentificationMethodCodeSequence[0].CodeMeaning
'Basic Application Confidentiality Profile'
PyDicom 与 ImageIO
个人观点,我发现 PyDicom 是处理 DICOM 文件的最佳包。因为它专为 DICOM 设计,这一点很明显。它为程序员提供了更灵活的选项,特别是当用户想处理 DICOM 元数据而不仅仅是像素数据时。然而,另一方面,如果我只需要像素数据,我会更倾向于使用 ImageIO,因为它只提供处理像素数据所需的基本元数据。要了解更多关于如何使用 ImageIO 处理 DICOM 文件的信息,请查看这篇文章。
## 使用 ImageIO Python 包处理 DICOM
医学图像 == DICOM
towardsdatascience.com
结论:
-
PyDicom DataSet 是该包的主类,用户将直接处理它。
-
DataSet 是一个 Python 字典结构,包含 DICOM 标签作为键,DICOM 属性作为值。
-
我们介绍了如何使用 PyDicom 提供的一些重要方法和属性来处理 DataSet 类。
-
DataSet 的值基本上是一个 DataElement,另一个 PyDicom 类,它包含 DICOM 标签、关键字、VR 和每个 DICOM 属性的值。
感谢阅读…
推荐
- 欲了解更多关于 DICOM 的信息,请参阅这篇文章
目前用于临床护理的 DICOM 图像数量多达数十亿!
-
DICOM 元数据——大数据分析的有用资源:
这篇 文章 概述了通过结合患者访问和 DICOM 信息来表示数据的新方法,医学影像元数据的高级使用,辐射剂量分析和图像分割,以及用于特征工程的深度学习,以丰富数据。
-
PyDicom 文档: 我们仅仅触及了这个优秀包的表面,它的文档非常详尽。如果你想了解更多关于 PyDicom 的内容,请参考这个 链接 并尝试编写你在其中阅读到的代码。
参考文献
[1] PyDicom 文档, 用户指南, [访问日期:2022 年 12 月 25 日]
[2] PyDicom 文档, 元素 VR 和 Python 类型, [访问日期:2022 年 12 月 25 日]
[3] PyDicom 文档,API 参考, [访问日期:2022 年 12 月 25 日]
介绍 Python 的魔法方法
原文:
towardsdatascience.com/introducing-pythons-magic-methods-f443ed913703
PYTHON | PROGRAMMING
一份关于利用 dunder 函数的力量来提升编程水平的实用指南
·发表于 Towards Data Science ·阅读时长 7 分钟·2023 年 5 月 24 日
--
照片由 Matt Palmer 提供,来源于 Unsplash
Python 是一种很棒的编程语言,GitHub发现,它在 2022 年也是第二大热门语言。Python 最吸引人的优点是其强大的社区。似乎 Python 为你可能遇到的任何使用场景都有一个包。
Python 还有很多酷炫的特性,这些特性并不是常见的知识。如果你有兴趣了解更多,请随时查看我之前的相关文章。
通过这些酷炫的隐藏 Python 特性,将你的编码技能提升到新水平
towardsdatascience.com
在广阔而动态的 Python 编程世界中,存在一组独特的函数,这些函数通常被初学者忽视,但它们在语言的生态系统中具有重要意义。这些就是魔法方法(也称为 dunder 函数)。
魔法方法是一组在 Python 中预定义的方法,提供了特殊的语法特性。它们通过其前后有双下划线来轻松识别,例如 __init__
、__call__
、__len__
等等。
魔法方法在定义类的对象如何处理各种 Python 操作方面发挥了关键作用。实质上,它们允许自定义对象表现得类似于内置的 Python 类型。直接的好处是增强了 Python 的一致性和直观性。
在这篇文章中,我们将专注于并讨论强大的 Dunder 函数背后的魔法。我们将探讨它们的目的并讨论它们的使用。
无论你是 Python 新手还是经验丰富的程序员,本指南旨在提供对 Dunder 函数的全面理解,使你的 Python 编程体验更加高效和愉快。
记住,Python 的魔力不仅在于它的简单性和多样性,还在于像 Dunder 函数这样的强大功能。所以,让我们一起揭开魔法的面纱吧!
__init__
也许所有 Dunder 函数中最基本的是__init__
。这是 Python 在我们创建(或如名称所示,初始化)一个新对象时自动调用的魔法方法。
class Pizza:
def __init__(self, size, toppings):
self.size = size
self.toppings = toppings
# Now let's create a pizza
my_pizza = Pizza('large', ['pepperoni', 'mushrooms'])
print(my_pizza.size) # This will print: large
print(my_pizza.toppings) # This will print: ['pepperoni', 'mushrooms']
我们创建了一个名为Pizza
的类。我们将__init__
函数设置为接受初始化时指定的大小和配料参数,并将它们设置为自定义对象的属性。
在这里,self
用于表示类的实例。所以当我们说self.size = size
时,我们是在说:“嘿,这个 pizza 对象有一个属性 size,我想它是我在创建对象时提供的大小。”
__str__
和__repr__
__str__
这是 Python 的魔法方法,允许我们为自定义对象定义描述。它本质上是在回答以下问题:
“你会如何向朋友描述这个对象?”
当你使用str()
打印对象或将其转换为字符串时,Python 会检查你是否为该对象的类定义了__str__
方法。
如果你有,它会使用那个方法将对象转换为字符串。
我们可以扩展我们的 Pizza 示例,加入一个__str__
函数,如下所示:
class Pizza:
def __init__(self, size, toppings):
self.size = size
self.toppings = toppings
def __str__(self):
return f"A {self.size} pizza with {', '.join(self.toppings)}"
my_pizza = Pizza('large', ['pepperoni', 'mushrooms'])
print(my_pizza) # This will print: A large pizza with pepperoni, mushrooms
__repr__
__str__
函数更像是描述对象属性的非正式方式。另一方面,__repr__
用于提供一个更正式、详细和明确的自定义对象描述。
如果你在对象上调用repr()
,或者在控制台中输入对象的名称,Python 将查找__repr__
方法。
如果__str__
没有定义,Python 会使用__repr__
作为备份来打印对象或将其转换为字符串。因此,至少定义__repr__
通常是一个好主意,即使你不定义__str__
。
下面是我们可以为我们的 pizza 示例定义__repr__
的方法:
class Pizza:
def __init__(self, size, toppings):
self.size = size
self.toppings = toppings
def __repr__(self):
return f"Pizza('{self.size}', {self.toppings})"
my_pizza = Pizza('large', ['pepperoni', 'mushrooms'])
print(repr(my_pizza)) # This will print: Pizza('large', ['pepperoni', 'mushrooms'])
你看,__repr__
会给你一个可以作为 Python 命令运行的字符串,以重建 pizza 对象,而__str__
则提供了一个更易于理解的描述。希望这有助于你更好地理解这些魔法方法!
__add__
在 Python 中,我们都知道你可以使用+
运算符将数字相加,例如3 + 5
。
但是如果我们想添加一些自定义对象的实例呢?__add__
Dunder 函数允许我们做到这一点。它使我们能够定义+
运算符在自定义对象上的行为。
为了保持一致性,假设我们想要在我们的披萨示例中定义 +
行为。假设每当我们将两张或更多的披萨相加时,它将自动合并所有的配料。以下是它可能的实现方式:
class Pizza:
def __init__(self, size, toppings):
self.size = size
self.toppings = toppings
def __add__(self, other):
if not isinstance(other, Pizza):
raise TypeError("You can only add another Pizza!")
new_toppings = self.toppings + other.toppings
return Pizza(self.size, new_toppings)
# Let's create two pizzas
pizza1 = Pizza('large', ['pepperoni', 'mushrooms'])
pizza2 = Pizza('large', ['olives', 'pineapple'])
# And now let's "add" them
combined_pizza = pizza1 + pizza2
print(combined_pizza.toppings) # This will print: ['pepperoni', 'mushrooms', 'olives', 'pineapple']
类似于 __add__
双下划线方法,我们还可以定义其他算术函数,比如 __sub__
(用于使用 —
操作符进行减法)和 __mul__
(用于使用 *
操作符进行乘法)。
len
这个双下划线方法允许我们定义 len()
函数对于我们自定义对象应返回的内容。
Python 使用 len()
来获取数据结构的长度或大小,比如列表或字符串。
在我们的披萨类的上下文中,我们可以说披萨的“长度”是它拥有的配料数量。以下是我们可以实现这一点的方法:
class Pizza:
def __init__(self, size, toppings):
self.size = size
self.toppings = toppings
def __len__(self):
return len(self.toppings)
# Let's create a pizza
my_pizza = Pizza('large', ['pepperoni', 'mushrooms', 'olives'])
print(len(my_pizza)) # This will print: 3
在 __len__
方法中,我们仅返回 toppings
列表的长度。现在,len(my_pizza)
将告诉我们 my_pizza
上有多少个配料。
注意:
__len__
应该始终返回一个整数,并且预期是一个非负值。负配料只是怪异,不是吗?
iter
这个双下划线方法允许你的对象是可迭代的——也就是说,可以在 for
循环中使用。
为此,我们还需要定义 __next__
函数。这个函数用于定义返回迭代中的下一个值的行为。此外,它还应该在序列中没有更多项目时向可迭代对象发出信号。我们通常通过引发 StopIteration
异常来实现这一点。
对于我们的披萨示例,假设我们想要迭代配料。我们可以通过定义 __iter__
方法使我们的披萨类可迭代,如下所示:
class Pizza:
def __init__(self, size, toppings):
self.size = size
self.toppings = toppings
def __iter__(self):
self.n = 0
return self
def __next__(self):
if self.n < len(self.toppings):
result = self.toppings[self.n]
self.n += 1
return result
else:
raise StopIteration
# Let's create a pizza
my_pizza = Pizza('large', ['pepperoni', 'mushrooms', 'olives'])
# And now let's iterate over it
for topping in my_pizza:
print(topping)
在这种情况下,for
循环调用 __iter__
,它初始化一个计数器(self.n
)并返回披萨对象本身(self
)。
然后,for
循环调用 __next__
来依次获取每个配料。
当 __next__
返回了所有配料后,它会引发 StopIteration
异常,然后 for
循环现在知道没有更多的配料了,因此将停止迭代过程。
结论
总之,很明显,魔法方法确实是 Python 语言的核心。
它们构成了许多 Python 内置操作的基础,为我们开发者提供了必要的自由度,以便以与语言其余部分的用法一致的方式个性化我们对象的行为。
在本文中,我们讨论了如何利用双下划线函数,如 __init__
、__iter__
和 __len__
,来满足我们的需求。你可以在这里找到 Python 允许的所有双下划线方法的完整列表:
[## mathspp - 将你的 Python 🐍 提升到下一个水平 🚀
这是对 Python 中双下划线方法的介绍,帮助你理解它们是什么以及它们的作用。(如果…
你喜欢这篇文章吗?每月$5,你可以成为会员,解锁无限访问 Medium 的权限。你将直接支持我和你在 Medium 上喜爱的所有其他作家。非常感谢!
## 通过我的推荐链接加入 Medium - David Farrugia
获取对我所有⚡高级⚡内容和 Medium 上无限制的访问权限。通过买一杯咖啡支持我的工作…
想要联系我吗?
我非常想听听你对这个话题或关于 AI 和数据的任何想法。
如果你希望联系我,请发邮件至 davidfarrugia53@gmail.com。
介绍 Python 的 Parse:正则表达式的终极替代品
PYTHON TOOLBOX
使用最佳实践和实际案例来展示强大的文本解析库
·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 6 月 19 日
--
parse 库非常简单易用。照片由 Amanda Jones 提供,来源于 Unsplash
本文介绍了一个名为 parse
的 Python 库,用于快速、方便地解析和提取文本数据,是 Python 正则表达式的绝佳替代品。
并且涵盖了 parse
库的最佳实践和解析 nginx 日志文本的实际案例。
介绍
我有一个同事叫王。一天,他带着担忧的表情来到我面前,说他遇到了一个复杂的问题:他的老板要求他分析过去一个月的服务器日志,并提供访客流量统计。
我告诉他很简单,只需使用正则表达式。例如,要分析 nginx 日志,使用以下正则表达式,这很基础。
content:
192.168.0.2 - - [04/Jan/2019:16:06:38 +0800] "GET http://example.aliyundoc.com/_astats?application=&inf.name=eth0 HTTP/1.1" 200 273932
regular expression:
(?<ip>\d+\.\d+\.\d+\.\d+)( - - \[)(?<datetime>[\s\S]+)(?<t1>\][\s"]+)(?<request>[A-Z]+) (?<url>[\S]*) (?<protocol>[\S]+)["] (?<code>\d+) (?<sendbytes>\d+)
但王依然担心,学习正则表达式太复杂了。虽然网上有很多现成的例子可以学习,但他在解析不常见的文本格式时需要帮助。
而且,即使这次他能解决问题,如果老板在提交分析时要求更改解析规则呢?难道他不会再费很长时间吗?
是否有更简单、更方便的方法?
我考虑了一下,说,当然有。今天我们介绍的主角是 Python parse
库。
安装与设置
如在 parse GitHub 页面上所述,它使用 Python 的 format() 语法来解析文本,本质上作为 Python f-strings 的逆操作。
在开始使用 parse
之前,让我们看看如何安装这个库。
直接使用 pip 安装:
python -m pip install parse
使用 conda 安装可能更麻烦,因为 parse 不在默认的 conda 通道中,需要通过 conda-forge 安装:
conda install -c conda-forge parse
安装后,你可以在代码中使用 from parse import *
直接使用库的方法。
特性与用法
parse
API 类似于 Python 正则表达式,主要包括 parse
、search
和 findall
方法。基本用法可以从 parse 文档中学习。
模式格式
parse 格式与 Python 格式语法非常相似。你可以使用 {}
或 {field_name}
捕获匹配的文本。
例如,在以下文本中,如果我想获取个人资料 URL 和用户名,我可以这样写:
content:
Hello everyone, my Medium profile url is https://qtalen.medium.com,
and my username is @qtalen.
parse pattern:
Hello everyone, my Medium profile url is {profile},
and my username is {username}.
或者你想提取多个电话号码。电话号码的前面有不同格式的国家代码,且电话号码长度固定为 11 位。你可以这样写:
compiler = Parser("{country_code}{phone:11.11},")
content = "0085212345678901, +85212345678902, (852)12345678903,"
results = compiler.findall(content)
for result in results:
print(result)
或者如果你需要处理一段 HTML 标签中的文本,但该文本前后有不定长度的空白,你可以这样写:
content:
<div> Hello World </div>
pattern:
<div>{:^}</div>
在上面的代码中,{:11}
指宽度,意味着捕获至少 11 个字符,相当于正则表达式 (.{11,})?
。{:.11}
指精度,意味着最多捕获 11 个字符,相当于正则表达式 (.{,11})?
。因此,当结合使用时,它表示 (.{11, 11})?
。结果是:
捕获固定宽度的字符。图像由作者提供
parse 最强大的功能是处理时间文本,可以直接解析为 Python datetime 对象。例如,如果我们想解析 HTTP 日志中的时间:
content:
[04/Jan/2019:16:06:38 +0800]
pattern:
[{:th}]
获取结果
有两种获取结果的方法:
-
对于使用
{}
而没有字段名称的捕获方法,你可以直接使用result.fixed
将结果获取为元组。 -
对于使用
{field_name}
的捕获方法,你可以使用result.named
将结果获取为字典。
自定义类型转换
尽管使用 {field_name}
已经非常简单,但源代码显示 {field_name}
在内部被转换为 (?P<field_name>.+?)
。所以,parse
仍然使用正则表达式进行匹配。.+?
表示非贪婪模式下的一个或多个随机字符。
从 parse 格式到正则表达式的转换过程。图像由作者提供
然而,我们通常希望更精确地匹配。例如,文本“my email is xxx@xxx.com”,“my email is {email}”
可以捕获电子邮件。有时我们可能会得到脏数据,例如“my email is xxxx@xxxx”,我们不想抓取它。
是否有办法使用正则表达式进行更准确的匹配?
这时,with_pattern
装饰器就派上用场了。
例如,对于捕获电子邮件地址,我们可以这样编写:
@with_pattern(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b')
def email(text: str) -> str:
return text
compiler = Parser("my email address is {email:Email}", dict(Email=email))
legal_result = compiler.parse("my email address is xx@xxx.com") # legal email
illegal_result = compiler.parse("my email address is xx@xx") # illegal email
使用 with_pattern
装饰器,我们可以定义一个自定义字段类型,在这种情况下是 Email
,它将匹配文本中的电子邮件地址。我们还可以使用这种方法匹配其他复杂的模式。
现实世界的例子:解析 Nginx 日志
在了解解析的基本用法后,我们回到文章开头提到的王的困扰。让我们看看如果我们有过去一个月的服务器日志文件,如何解析日志。
注意: 我们选择了 NASA 的 HTTP 日志数据集 作为本实验的数据集,该数据集可以免费使用。
需要解析的文本片段如下:
文本片段是什么样的。作者截图
首先,我们需要预处理解析表达式。这样,在解析大型文件时,我们就不必为每一行文本编译正则表达式,从而提高性能。
接下来,parse_line
方法是这个例子的核心。它使用预处理的表达式来解析文本,如果有匹配则返回相应的匹配结果,如果没有则返回一个空字典。
然后,我们使用 read_file
方法逐行处理文本,使用生成器,这可以最小化内存使用。然而,由于磁盘的 4k 能力限制,这种方法可能无法保证性能。
由于我们需要对日志文件进行统计,我们必须使用 from_records
方法从匹配结果构造一个 DataFrame
。
最后,在 main
方法中,我们将所有方法整合在一起,并尝试计算不同 status_code
的出现次数:
王的困扰已经轻松解决。作者图片
就这样。王的困扰已经轻松解决。
使用 parse
库的最佳实践
尽管 parse
库非常简单,以至于我在文章中只有一点要写,但仍然有一些最佳实践需要遵循,就像正则表达式一样。
可读性和可维护性
为了高效捕获文本并保持表达式,建议始终使用 {field_name}
而不是 {}
。这样,你可以直接使用 result.named
获取键值结果。
推荐使用 Parser(pattern)
来预处理表达式,而不是 parse(pattern, text)
。
一方面,这可以提高性能。另一方面,当使用 Custom Type Conversions
时,你可以将 pattern
和 extra_type
保持在一起,这使得维护更容易。
大数据集的性能优化
如果你查看源代码,你会发现{}
和{field_name}
分别使用正则表达式(.+?)
和(?P<field_name>.+?)
进行捕获。这两个表达式都使用了非贪婪模式。所以当你使用with_pattern
编写自己的表达式时,也应该尝试使用非贪婪模式。
同时,在编写with_pattern
时,如果使用()
进行捕获分组,请使用regex_group_count
来指定具体的分组,如:[@with_pattern](http://twitter.com/with_pattern)(r’((\d+))’, regex_group_count=2)
。
最后,如果在with_pattern
中不需要分组,请使用(?:x)
代替。@with_pattern(r’(?:<input.*?>)(.*?)(?:</input>)’, regex_group_count=1)
表示你想要捕获输入标签之间的内容。输入标签不会被捕获。
结论
在这篇文章中,我改变了以往写长篇论文的方式。通过解决同事的问题,我简要介绍了parse
库的使用。希望你喜欢这种风格。
这篇文章没有涵盖官方网站上的详细使用方法,但介绍了一些最佳实践和基于我经验的性能优化方案。
同时,我详细解释了如何使用parse
库通过实际例子解析 nginx 日志。
正如新系列标题所示,除了提高代码执行速度和性能之外,使用各种工具提升工作效率也是性能提升的一部分。
这篇文章帮助数据科学家简化文本解析,将时间花在更重要的任务上。如果你对这篇文章有任何想法,请随时留言讨论。
我的上一系列文章是关于 Python 并发的,你可以在这里阅读:
Python 并发
查看列表10 篇故事 [## 通过我的推荐链接加入 Medium - 彭千
作为 Medium 会员,你的部分会费将分配给你阅读的作者,并且你可以完全访问每个故事……
medium.com](https://medium.com/@qtalen/membership?source=post_page-----3ae07e51b753--------------------------------)
本文最初发布于:www.dataleadsfuture.com/introducing-pythons-parse-the-ultimate-alternative-to-regular-expressions/
介绍 Quix Streams:一个开源的 Python Kafka 库
轻松生成和消费类似 Pandas 接口的时间序列数据流
·
关注 发表在 Towards Data Science · 7 分钟阅读 · 2023 年 3 月 6 日
--
作者提供的图片
你可能会好奇,为什么世界上还需要另一个用于 Kafka 的 Python 框架。毕竟,已有很多现有的库和框架可供选择,比如 kafka-python、Faust、PySpark 等等。
然而,Quix Streams 的重点是时间序列和遥测数据,因此其功能被优化用于遥测相关的用例。这可能是设备遥测(最初是对 Formula 1 赛车的传感器数据进行道路测试)或其他类型的遥测数据,如指标、日志和跟踪。
它还旨在帮助你充分利用 Apache Kafka 的水平扩展能力。如果你需要处理大量的数据流(例如,每秒 60,000 个数据点),这尤其重要。
尽管如此,你不必在 Formula 1 遥测上进行实时机器学习才能发现 Quix Streams 的有用之处——我希望它的简洁性和性能能让你们变得更高效,我也很期待看到你们发现的其他用例。
你可以使用 Quix Streams 做些什么
为了帮助你了解如何使用这个库,这里列出了核心功能和简化的代码示例,演示了它们的工作原理:
使用 Pandas DataFrames 更高效地生成数据
时间序列参数会同时发出,因此它们共享一个时间戳。独立处理这些数据是浪费的。该库使用了一个表格系统,可以原生地与 Pandas DataFrames 配合使用。每一行都有一个时间戳和用户定义的标签作为索引。
- 要了解如何使用该库与 Pandas 从 CSV 中直接流式传输数据的完整可运行示例,请参阅 这个 gist。
生成时间序列数据时无需担心序列化或反序列化
Quix Streams 使用不同的编解码器和优化来序列化和反序列化时间序列数据,以最小化负载,从而提高吞吐量并减少延迟。
- 以下示例演示了如何使用
add_value
方法将数据追加到流中:
利用内置缓冲区来优化时间序列数据窗口的处理操作
如果你以高频率发送数据,每条消息的处理成本可能很高。该库提供了内置的时间序列缓冲区用于生成和消费,允许在延迟和成本之间进行多种配置。
- 例如,你可以配置库,在收集到 100 个时间戳数据项或经过一定数量的毫秒后(使用数据中的时间戳,而不是消费者机器的时钟)从缓冲区释放一个数据包。
buffer.packet_size = 100
buffer.time_span_in_milliseconds = 100
- 然后你可以从缓冲区读取数据并使用
on_read
函数进行处理。
生产和消费不同类型的混合数据
该库允许你在同一时间戳下生成和消费不同类型的混合数据,如数字、字符串或二进制数据。
-
例如,你可以同时生成时间序列数据和大型二进制数据块。
-
通常,你会想将时间序列数据与二进制数据结合。在以下示例中,我们将公交车的车载摄像头与其 ECU 单元的遥测数据结合,以便在上下文中分析车载摄像头画面。
-
你还可以生成包含负载的事件:
-
例如,你可能需要监听时间序列或二进制流中的变化,并生成事件(如“超速”)。这些可能需要某种文件随事件消息一起发送(例如交易发票或带有照片证据的超速罚单)。这是一个关于测速摄像头的示例:
使用流上下文进行水平扩展
流上下文允许你将来自一个数据源的数据与补充元数据捆绑在同一范围内——这使工作负载可以通过多个副本进行水平扩展。
- 在以下示例中,
create_stream
函数用于创建一个名为 bus-123AAAV 的流,该流分配给特定消费者,并将按正确顺序接收消息:
利用内置的状态处理以增强鲁棒性
该库包括一个易于使用的状态存储,结合了 blob 存储和 Kubernetes 持久卷,确保快速恢复任何故障或中断。
要使用它,你可以创建 LocalFileStorage
的实例或使用我们的帮助类之一来管理状态,如 InMemoryStorage
。
这是一个对数据中选定列进行状态操作求和的示例:
其他性能和可用性增强
该库还包括许多其他增强功能,旨在简化与 Kafka 交互时配置和性能的管理过程:
-
无需模式注册表:该库不需要模式注册表来发送不同类型或参数的集合,这由协议内部处理。这意味着你可以在每个主题中发送多个模式。
-
消息拆分:Quix Streams 自动处理生产者端的大消息,必要时将其拆分。你不再需要担心 Kafka 消息限制。在消费者端,这些消息会自动合并。
-
消息代理配置:使用 Kafka 的最佳配置需要许多设置,而理想的配置需要时间。该库默认处理 Kafka 配置,但也支持自定义配置。
-
检查点:该库支持在从 Kafka 主题消费数据时进行手动或自动检查点。这使你能够通知消息代理你已经处理了某一点之前的消息(并且在未计划的重启情况下不会重复处理相同的消息)。
-
水平扩展:Quix Streams 使用流上下文功能处理水平扩展。你可以从一个副本扩展到多个副本,再缩减回一个副本,库确保数据负载始终在你的副本之间可靠地共享。
有关功能的详细概述,请参见库文档。
入门
要快速尝试 Quix Streams,您只需安装库并设置本地 Kafka 实例。
安装 Quix Streams
使用以下命令安装 Quix Streams:
python3 -m pip install quixstreams
- 要在配备 M1 或 M2 芯片的 Mac 上安装 Quix Streams,请参见此特殊安装指南:在 M1/M2 Mac 上安装 Quix Streams。
在本地安装 Kafka
该库需要利用消息代理来发送和接收数据。要在本地安装和测试 Kafka:
-
从Apache Kafka 下载页面下载 Apache Kafka 二进制文件。
-
将文件内容提取到方便的位置(即
kafka_dir
),然后使用以下命令启动 Kafka 服务:
Linux / macOS
<kafka_dir>/bin/zookeeper-server-start.sh config/zookeeper.properties
<kafka_dir>/bin/zookeeper-server-start.sh config/server.properties
Windows
<kafka_dir>\bin\windows\zookeeper-server-start.bat.\config\zookeeper.properties
<kafka_dir>\bin\windows\kafka-server-start.bat .\config\server.properties
以下示例将为您提供如何使用 Quix Streams 生产和消费数据的基本概念:
生产时间序列数据
这是一个如何使用 Python 将时间序列数据生产到 Kafka 主题的示例。
import quixstreams as qx
import time
import datetime
import math
# Connect to your kafka client
client = qx.KafkaStreamingClient('127.0.0.1:9092')
# Open the output topic which is where data will be streamed out to
# If the topic does not exist, it will be created
topic_producer = client.get_topic_producer(topic_id_or_name = "mytesttopic")
# Set stream ID or leave parameters empty to get stream ID generated.
stream = topic_producer.create_stream()
stream.properties.name = "Hello World Python stream"
# Add metadata about time series data you are about to send.
stream.timeseries.add_definition("ParameterA").set_range(-1.2, 1.2)
stream.timeseries.buffer.time_span_in_milliseconds = 100
print("Sending values for 30 seconds.")
for index in range(0, 3000):
stream.timeseries \
.buffer \
.add_timestamp(datetime.datetime.utcnow()) \
.add_value("ParameterA", math.sin(index / 200.0) + math.sin(index) / 5.0) \
.publish()
time.sleep(0.01)
print("Closing stream")
stream.close()
消费时间序列数据
这是一个如何使用 Python 从 Kafka 主题消费时间序列数据的示例:
import quixstreams as qx
import pandas as pd
# Connect to your kafka client
client = qx.KafkaStreamingClient('127.0.0.1:9092')
# get the topic consumer for a specific consumer group
topic_consumer = client.get_topic_consumer(topic_id_or_name = "mytesttopic",
consumer_group = "empty-destination")
def on_dataframe_received_handler(stream_consumer: qx.StreamConsumer, df: pd.DataFrame):
# do something with the data here
print(df)
def on_stream_received_handler(stream_consumer: qx.StreamConsumer):
# subscribe to new DataFrames being received
# if you aren't familiar with DataFrames there are other callbacks available
# refer to the docs here: https://docs.quix.io/sdk/subscribe.html
stream_consumer.timeseries.on_dataframe_received = on_dataframe_received_handler
# subscribe to new streams being received
topic_consumer.on_stream_received = on_stream_received_handler
print("Listening to streams. Press CTRL-C to exit.")
# Handle termination signals and provide a graceful exit
qx.App.run()
有关如何消费和生产时间序列和事件数据的完整文档,请查看文档。
下一步
这是 Quix Streams 的首次迭代,下一版本已经在开发中。
主要亮点是一个名为“流数据帧”的新特性,它简化了来自批处理环境的用户的有状态流处理。它消除了用户在内存中管理状态、更新滚动窗口、处理检查点和状态持久化以及在服务意外重启后管理状态恢复的需要。
通过引入熟悉的 Pandas DataFrames 接口,我和我的同事希望让流处理对新接触流数据的数据专业人士更具可及性。
以下示例展示了如何在流数据帧上执行滚动窗口计算:
请注意,这正是您在 Jupyter notebook 上对静态数据执行相同计算的方式——因此,对于习惯于批处理的人来说,学习起来会很容易。
你无需为流数据的有状态处理的复杂性而苦恼——这一切都会由库来管理。此外,虽然它仍然会像 Pandas 一样,但底层将使用二进制表——这相比传统的 Pandas DataFrames 带来了显著的性能提升。
要了解下一个版本何时准备好,请确保关注Quix Streams GitHub 仓库。
路线图也应由更广泛的数据社区的反馈和贡献来塑造:
-
如果你发现了 bug 或想要请求改进,请随时报告 GitHub 问题。
-
如果你有问题、需要帮助,或只是想了解更多关于这个库的信息,可以尝试在 Slack 社区“The Stream”(我帮忙进行管理)中发布消息,或查看文档。
-
如果你想改进这个库,请查看贡献指南。
介绍 Crystal Bar Chart:可视化序列差分聚类
了解 Crystal Bar Charts 并使用 Python 创建你自己的图表
·发表于 Towards Data Science ·阅读时间 12 分钟·2023 年 12 月 29 日
--
Crystal Bar Chart 作者Nick Gerend
介绍
有很多方法可以更好地理解一系列数据。无论是温度、考试成绩、食品价格、步数还是 UFO 目击事件,总能通过数据分析和可视化的视角学到关于这些事物的本质。
在接下来的内容中,我将描述一种新的数据可视化技术,它使用简单的基于阈值的聚类。我希望你会发现这是一种与传统可视化方法(如直方图和箱线图)相配的全新方法,从而对感兴趣的特征获得新的视角。
序列差分聚类
数值上
怎样才能脱颖而出?以单一数值属性为例,我们假设简单的阈值是将有序值进行比较并分组的标准。以以下序列为例:
[ 0 , 1 , 1 , 2 , 3 , 5 , 8 , 13 , 21 , 34 , 55 , 89 , 144 ]
这当然是斐波那契数列的前 13 个数字。如果我们从第 3 个位置开始,计算每个值与前一个值之间的差,我们会得到相同的序列,只是向前移动了 2 步:
[ _ , _ , 0 , 1 , 1 , 2 , 3 , 5 , 8 , 13 , 21 , 34 , 55 ]
现在,让我们设定一个阈值为 5,并假设在从左到右遍历序列时,任何大于 5 的数值都标志着一个新簇的开始。这将导致前 8 项(值为 0-13)被分到一起,而每个后续项由于所有剩余的差值都超过 5,因而每项都是其所在组的唯一项。
对于斐波那契数列,这种方法会产生可预测的结果,但在探索性数据分析中,可能会出现各种模式。我将描述如何使用一种新的数据可视化方法探索这些模式,首先从下面几个熟悉的图表类型开始比较。
使用直方图
让我们尝试将聚类策略应用于直方图,通过确定基于相同阈值的箱子数量并审查分离:
# pandas histogram
import pandas as pd
import numpy as np
data = {'value' : [0,1,1,2,3,5,8,13,21,34,55,89,144]}
df = pd.DataFrame(data=data)
data_range = df['value'].max() - df['value'].min()
num_bins = np.ceil(data_range/5).astype(int)
print(num_bins) # 29
df['value'].hist(bins=num_bins, color='w', edgecolor='black',
linewidth=1.2, grid=False, figsize=(7,1.5))
以 29 个箱子的形式呈现的斐波那契数列(0 到 144)的直方图,使用 Matplotlib 渲染
使用蜜蜂散点图
现在,让我们用蜜蜂散点图可视化每个单独的值,每个圆的直径等于阈值,以寻找间隙:
# vizmath (modified) beeswarm chart
from vizmath.beeswarm import swarm
import pandas as pd
from math import pi
data = {
'id' : [str(i) for i in range(1, 14)],
'value' : [0,1,1,2,3,5,8,13,21,34,55,89,144]
}
df = pd.DataFrame(data=data)
bs = swarm(df, 'id', 'value', None, size_override=pi*(5/2)**2)
bs.beeswarm_plot(color=False)
斐波那契数列(0 到 144)的蜜蜂散点图,直径为 5,使用 Matplotlib 渲染
水晶条形图
布局
为了介绍水晶条形图,让我们继续之前的例子,使用交替的灰色阴影来说明不同的集群(稍后我们将详细介绍):
# vizmath crystal bar chart
import pandas as pd
from vizmath.crystal_bar_chart import crystals
data = {
'id' : [str(i) for i in range(1, 14)],
'value' : [0,1,1,2,3,5,8,13,21,34,55,89,144]
}
df = pd.DataFrame(data=data)
cbc = crystals(df, 'id', 'value', 5, width_override=5, rotation=90)
cbc.cbc_plot(legend=False, alternate_color=True, color=False)
斐波那契数列(0 到 144)的水晶条形图,使用 Matplotlib 渲染
为了有趣,我们添加一个任意大小属性:
# vizmath crystal bar chart with added width property
import pandas as pd
from vizmath.crystal_bar_chart import crystals
data = {
'id' : [str(i) for i in range(1, 14)],
'value' : [0,1,1,2,3,5,8,13,21,34,55,89,144],
'size' : [5,13,8,7,6,8,13,5,11,4,9,12,6] # new size property
}
df = pd.DataFrame(data=data)
cbc = crystals(df, 'id', 'value', 5, width_field='size', rotation=90)
cbc.cbc_plot(legend=False, alternate_color=True, color=False)
斐波那契数列(0 到 144)的水晶条形图,带有任意属性‘大小’,使用 Matplotlib 渲染
现在让我们移动值,以观察图表如何适应新的原点:
# vizmath crystal bar chart with adjusted origin
import pandas as pd
from vizmath.crystal_bar_chart import crystals
data = {
'id' : [str(i) for i in range(1, 14)],
'value' : [0,1,1,2,3,5,8,13,21,34,55,89,144]
}
df = pd.DataFrame(data=data)
cbc = crystals(df, 'id', 'value', 5, width_override=5,
rotation=90, offset=21) # new offset
cbc.cbc_plot(legend=False, alternate_color=True, color=False)
斐波那契数列(0 到 144)的水晶条形图,调整偏移 = 21,使用 Matplotlib 渲染
灵感
在 2020 年 10 月,我和我的妻子在查看一个数据可视化挑战时,数据提供的是一个包含一些重复项的单一特征。
我最初的想法是制作一些看起来有三维效果的、类似水晶的东西,最终得到的是一种简单的水晶条形图版本。
早期的水晶条形图,由 Nick Gerend 于 2020 年 10 月制作,使用 Tableau 渲染
在 2022 年,我再次拿起了这个想法,并使用基于阈值的聚类(顺序差分聚类策略)对一组值进行分层,给定阈值,并使每个后续水晶堆叠在其偏移(移除一次)邻居旁边的中心轴周围形成一个集群的水晶(代表一个集群中的值)。
摩天大楼¹似乎是一个很好的数据集开始,并成为我对新算法的第一次测试:
“天际线”由 Nick Gerend(2022 年 4 月 10 日)
接下来,我将描述水晶条形图的关键特性。
水晶条形图算法
绘制 Crystal Bar Chart 是将晶体顶部面的接触点与原点对齐,以便侧面正确绘制,勾勒出每个值对应的完整晶体:
Crystal Bar Chart 由 Nick Gerend 构建
-
从一组值开始,并根据偏好(升序或降序)对其进行排序,例如:0.2, 1.5, 7.4, 9.4
-
使用期望的偏移量(例如 -1.7)调整值,以调整原点的位置
-
设置一个阈值(例如 3.5),并根据之前描述的顺序差异聚类对数据进行分组
-
遍历每个组(外层循环)及组内的每个值(内层循环),将初始值的晶体沿中心轴定向,并根据项目的范围更新下一个晶体的起始位置,该范围垂直于中心轴
-
对于每个后续值,交替左右放置晶体,以类似方式考虑晶体顶部面占据的范围,以调整下一个交替晶体的位置(不同的排序和垂直轴上的放置方法正在审查以备未来更新)
-
计算晶体每个面的点(左、右、顶部):
顶部面尺寸:由顺序差异聚类阈值(沿中心轴的高度)和根据大小属性的范围(垂直于中心轴的宽度)勾勒
左右面尺寸:根据视图中的侧面多边形绘制,确定为晶体顶部面和原点之间接触点的斜率(默认为 0,0)
-
当遇到新集群时,重置起始位置,并对集群中每个晶体重复面位置计算
-
如果需要,可以反转偏移量以将值重新映射到原始范围
Python 实现
我已经通过我的 vizmath 包在 PyPI 上提供了 Crystal Bar Chart 算法的 Python 实现。让我们使用上面的初始示例详细分析几个选项,并解释输入和输出:
import pandas as pd
from vizmath.crystal_bar_chart import crystals # pip install vizmath==0.0.14
# using the example data from above:
data = {
'id' : [str(i) for i in range(1, 14)],
'value' : [0,1,1,2,3,5,8,13,21,34,55,89,144],
'size' : [5,13,8,7,6,8,13,5,11,4,9,12,6]
}
df = pd.DataFrame(data=data)
# create a crystals object
# > df: DataFrame with 1 numerical column of data and an
# optional size column 'width_field'
# > id_field: required identifier (can be dummy values)
# > height_field: required value column
# > height_range: sequential differential clustering threshold
# > width_field = optional size column
# > bottom_up: False = descending, True = ascending
# > width_override: value constant to set the size value
# (overrides the width_field values)
# > offset: value to adjust the origin by
# > reset_origin: False = keeps offset, True: resets origin with offset
# > rotation: overall rotation around the center in degrees
cbc = crystals(df, 'id', 'value', 5, width_field='size', bottom_up = True,
width_override = None, offset=21, reset_origin=True, rotation=90)
#plot the Crystal Bar Chart
cbc.cbc_plot(legend=False, alternate_color=True, color=False)
Crystal Bar Chart 以反向顺序和偏移量及重置原点,使用 Matplotlib 渲染
以下是 Crystal Bar Chart 算法的输出:
-
id - 项目标识符
-
group - 由顺序差异聚类生成的项目集群:1 到 N
-
side - 晶体面的标识符:
-
value - 项目的值(沿中心轴的晶体顶部的质心位置)
-
height - 顺序差异聚类阈值(晶体顶部面的高度,平行于中心轴)
-
width - 次要值 ≥ 0(晶体顶部面的宽度,垂直于中心轴)
-
x, y - 布局中点的笛卡尔二维坐标
-
path - 描述一个有序整数集的路径,该路径围绕一个多边形,同时与水晶条形图中的每个(x, y)点对应,对于每个水晶 id 和面:1 到 N。
# Cyrtsal Bar Chart DataFrame
cbc.o_crystal_bar_chart.df[['id', 'group', 'side',
'value', 'height', 'width', 'x', 'y', 'path']].head()
水晶条形图数据框
在未来的版本中,我将尝试结合不同的选项,将水晶沿垂直于中央轴的轴线放置,以便进行集群和价值比较。
Tableau Public 实现
在本节中,我将介绍如何在 Tableau Public (v 2023.3.0)中实现我的水晶条形图可视化以及一些有趣的交互功能。
为了配合水晶主题,我们从维基百科上获取一个关于钻石的数据集²。数据包含钻石的名称、未切割和已切割的重量、来源等信息。为了示例 purposes,我将数据限制为大于 200 克拉且未切割和已切割值只有一个切割记录的钻石。
import pandas as pd
# https://en.wikipedia.org/wiki/List_of_diamonds (as of 12/25/2023)
# filtered to enries with uncut and cut values with only 1 cut, >200 carats
diamonds = {
'Name' : [
'4 February Stone', 'Centenary Diamond', 'Cross of Asia',
'DeBeers Diamond', 'Earth Star Diamond', 'Golden Jubilee Diamond',
'Graff Lesedi La Rona', 'Great Mogul Diamond', 'Gruosi Diamond',
'Incomparable Diamond', 'Jubilee Diamond', 'Koh-i-Noor',
'Lesotho Brown', 'Lesotho Promise', 'Millennium Star',
'Premier Rose Diamond', 'Regent Diamond', 'Taylor-Burton Diamond',
'Tiffany Yellow Diamond'],
'Uncut' : [
404.2, 599, 280, 440, 248.9, 755.5, 1111, 780, 300.12,
890, 650.8, 793, 601, 603, 777, 353.9, 410, 241, 280],
'Cut' : [
163.41, 273.85, 79.12, 234.5, 111.59, 545.67, 302.37, 280, 115.34,
407.48, 245.3, 105.6, 71.73, 75, 203.04, 137, 140.64, 68, 128.54],
'Color' : [
'white', 'colorless', 'yellow', '-', 'brown', 'yellow-brown',
'colourless', '-', 'black', 'brownish-yellow', 'colorless',
'colorless', 'pale brown', 'colorless', 'colorless',
'colorless', 'white with pale blue', 'colorless', 'yellow'],
'Origin' : [
'Angola', 'South Africa', 'South Africa', 'South Africa',
'South Africa', 'South Africa', 'Botswana', 'India', 'India',
'Democratic Republic of Congo', 'South Africa', 'India', 'Lesotho',
'Lesotho', 'Democratic Republic of Congo', 'South Africa', 'India',
'South Africa', 'South Africa']
}
df = pd.DataFrame(data=diamonds)
接下来,我们将使用 vizmath 创建一个水晶条形图,并将绘图信息和原始数据输出到 csv 文件中:
from vizmath.crystal_bar_chart import crystals
cbc = crystals(df, 'Name', 'Uncut', 100, width_field='Cut') # calculate
cbc.o_crystal_bar_chart.dataframe_rescale(0, 5000, -2500, 2500) #rescale
cbc.to_csv('crystal_bar_chart') # crystal bar chart output
cbc.df.to_csv('data.csv') # original data
使用Text file选项将crystal_bar_char.csv文件导入 Tableau。然后使用Connections旁边的Add链接将data.csv添加到Files列表中。在右侧的阶段,双击crystal_bar_char.csv标签并将data.csv文件从Files拖到该阶段。选择Inner Join,在Data Source下拉菜单中选择[Id]字段,data.csv下的[Name]字段。
数据集准备好后,导航到Sheet 1,并创建我们将用于绘制图表的计算列:
[chart]: MAKEPOINT([Y],[X])
[chart_top]: 如果[Side] = 0,则 MAKEPOINT([Y],[X]),否则为 null end。
首先,将[chart]拖到Marks下的Detail中以生成第一个地图层,并通过右击地图区域并选择Background Layers 调整这些选项。
-
取消所有Background Map Layers(Base、Land Cover 等)。
-
现在右击地图区域,选择Map Options并取消所有选项。
关闭Background Layers并继续以下步骤:
-
将[Group]、[Id]和[Side]拖到Marks下的Detail中。
-
将现在的SUM(Group)和SUM(Side)右击每一个并选择相应选项转换为Dimension和Discrete。
-
右击[Group],选择Sort,并选择Descending。
-
在Marks下拉菜单中选择Polygon(此时如果看起来有些奇怪也不用担心)。
-
将[Path]拖到Marks下的Path,右击现在的SUM(Path)并选择Dimension。
-
将[Group]拖到Color,并重复该过程,将其转换为Dimension和Discrete。
-
在Color下选择“Edit Colors…”,并配置为交替的灰度方案,0 和 2 组使用浅色,1 和 3 组使用深色。
-
点击确定,将不透明度调整为 95%,并在Color下选择黑色边框。
-
将[chart_top]拖入地图区域,弹出窗口将出现:Add a Marks Layer - 将该药丸拖入其中以创建新的地图层。
-
重复上述步骤,只不过现在将[Origin]用于Color,并根据需要调整颜色。
-
在Color下,选择黑色边框并将不透明度设置为 70%,然后右击图表右下角的空值药丸,选择Hide Indicator以隐藏空值标签。
现在图表部分已就位,应类似于以下内容:
让我们添加另一个图表以用于比较和交互。
-
使用底部面板上的第一个加号创建一个新的工作表,命名为Sheet 2。
-
将[Group]拖到Columns并转换为Dimension和Discrete。
-
将[Name]拖到Columns,将[Uncut]拖到Rows。
-
右键点击[Uncut],在Measure下选择Maximum。
-
将[Origin]拖到Color,将[Cut]拖到Size。
-
右键点击[Cut],在Measure下选择Maximum。
-
在顶部的Format菜单下,选择Shading…,并在Column Banding > Header下选择深灰色,Level勾选设置为第一个刻度,其他选项设置为None。
-
右键点击顶部的Group / Name,选择Hide Field Labels for Columns*,并根据需要调整图例名称和颜色。
Sheet 2准备好了,选择顶部菜单中的Entire View后应类似于以下内容:
最后,将两个工作表整合到仪表板中。创建仪表板并添加工作表后,在Dashboard顶部菜单中的Actions下设置一个操作。点击Add Action下拉菜单,选择Highlight。在Source Sheets中选择Sheet 2,在Target Sheets中选择Sheet 1。在Targeted Highlighting中选择Selected Fields,并选择[Group]和[Name]字段。最后,在右侧的Run action on菜单中选择Hover选项,这样整个仪表板将在Sheet 2上的每个条形和组上悬停时突出显示!
调整颜色并将所有内容以有序的方式排列后,以下是我们在 Tableau Public 中的新仪表板:
结论
在本文中,我概述了一种我称之为晶体条形图的新型数据可视化工具。这个工具对于将信息压缩到一个小空间非常有用,通过沿中央轴重叠的形状来表示一维数据,这些数据按顺序差异聚类分组,还可以选择沿垂直轴表示第二个数值属性。
晶体条形图的节省空间功能是对条形图和类似可视化工具的有效替代,它不需要较大的占用空间,并且与各种其他工具配合良好,用于在学术和专业工作中检查数据系列。
通过多种选项调整晶体状数据表示的特征,我希望你能发现这种可视化技术适用于不同的数据探索任务,并且是一种有趣的新方式来发现洞察!
参考文献
[1] 维基百科(CC BY-SA),“最高建筑物列表”(截至 2022 年 4 月 10 日)
[2] 维基百科(CC BY-SA),“钻石列表”(截至 2023 年 12 月 25 日)
相关文章
介绍多弦图:可视化复杂集合关系
了解多弦图,并使用 Python 创建你自己的图表
·发布于Towards Data Science ·阅读时间 13 分钟·2023 年 11 月 17 日
--
Nick Gerend 的多弦图
背景
前身
在数据可视化的领域中,表示复杂集合关系的发展经历了重要的里程碑,尤其是简单而有效的韦恩图、现代弦图和 UpSet 图的创建。
韦恩图
韦恩图由约翰·韦恩于 1880 年代提出¹,是集合论和逻辑学中的基础工具,以其简单性和有效性而著称,能够直观地表示不同集合之间的关系。韦恩图由重叠的圆圈组成,每个圆圈通常代表一个集合,圆圈之间的重叠部分表示这些集合共有的元素。
韦恩图
它们在教育、商业分析和逻辑推理中特别有用,因为它们提供了一种清晰直观的方式来展示集合的交集、差异和并集。它们将复杂关系简化为易于理解的视觉效果,使其成为解决问题、数据分析和决策过程中的宝贵工具。
弦图
现代弦图在近期历史上尤为引人注目,尤其是在 2007 年《纽约时报》刊登的一篇文章中提到马丁·克日文斯基²(“Circos”³可视化工具的主要贡献者)的工作。如今,弦图的特点是其圆形布局,带有弯曲的多边形弦连接圆周上的集合,每个弦展示两个集合之间或一个集合内的独立人群之间的关系。
弦图
这些图表特别有效地揭示了数据集中的隐藏模式和连接。关系的强度通常通过弦的厚度来表示,其他元素(如颜色和形状)可以用于显示方向性,使得弦图不仅视觉上引人注目,而且信息量极大。
UpSet Plot
UpSet Plot 由 Lex、Gehlenborg 等人于 2014 年介绍,成为可视化复杂集合交集的解决方案,克服了弦图和维恩图的一些固有缺点,能够可视化两个以上交集的集合关系。
UpSet Plot
它结合了矩阵的简洁性和条形图的定量表示,直接展示复杂的集合信息,这些信息可能超出了弦图仅限于成对集合关系的范围。
改进空间
虽然这三种方法各自以独特的方式提供信息,但每种方法都有一些明显的缺点:
-
维恩图由于多个重叠区域变得混乱,缺乏有效表示复杂关系的要素。
-
弦图局限于成对的集合关系,这大大限制了它们的应用。
-
UpSet Plot 的矩阵布局在集合组合增加时不容易扩展,因此可能难以立即洞察集合复杂性的某些方面或“全貌”。
多弦图
灵感
为了解决维恩图和弦图上述提到的挑战,我在 2021 年 6 月提出了一种算法,将弦图推广到适应三个或更多集合交互,并称之为“多弦图”(或简称 multichord)。作为开发的副产品,我还独立提出了 UpSet Plot,以测试这种新方法,后来才知道它已经是一个成熟的图表!
维恩图 > 弦图 > UpSet Plot > 多弦图
相对于其三种前身,这种新可视化提供了以下功能:
-
提供了一种准确而令人愉悦的视觉布局,相对于缺乏精确度的维恩图来说,适用于复杂的集合关系。
-
消除了弦图的成对限制,同时保持了在方向性、间距等方面的创造潜力。
-
通过提供对网络复杂性的即时视觉洞察,补充了 UpSet Plot 的离散信息,同时不会在矩阵和条形图编码中迷失(正如我之前提到的,它们也可以很好地配合使用!)
多弦图不仅拓宽了弦图的应用范围,还提供了对复杂网络的更细致理解,这是当今数据驱动世界中的重要需求。
数学、算法与布局
从数学原理开始,以下是多弦图构建中的基本项:
- 笛卡尔极坐标转换(CPC):首先在笛卡尔空间中计算位置,为了简化
# Input coordinates are in Cartesian, but represent Polar ingredients
# > 'r' is the Cartesian height offset in this case
# > the angle is calculated as a percentage along
# the straight line in Cartesian space relative to 2*pi
x = r * cos(angle)
y = r * sin(angle)
# For converting points, I created this helper function:
from math import cos, sin, pi
def polarize(x, max_x, y, y_offset = 0.):
angle = (2.*pi)*(((x)%(max_x))/(max_x))
angle_deg = angle * 180./pi
angle_rotated = (abs(angle_deg-360.)+90.) % 360\.
angle_new = angle_rotated * pi/180.
y += y_offset
x_out = (y)*cos(angle_new)
y_out = (y)*sin(angle_new)
return x_out, y_out
-
弦函数(CF):生成沿圆周两点之间路径的一组点,一些常见的实现包括:
-
直线(原始的!)
-
从圆圈中产生的弧,其中点是接触弦
-
Bezier 曲线(具有 3 个或更多点)
-
自定义函数(我为初始 Python 实现创建了一个)
-
# Here's a custom chord function I created for use in the Multi-Chord Diagram
# chord(x0, y0, x1, y1, x2, y2, points, h_override=0.)
# > x0, y0: the circle's center
# > x1, y1, x2, y2: two points on a circle
# > points: number of desired points along the chord path
# > h_override: height constraint on the chord's apex
from math import cos, sin, pi, sqrt, atan2
def LnToPntDst(x0, y0, x1, y1, x2, y2):
n = abs((y1-y2)*x0+(x2-x1)*y0+x1*y2-x2*y1)
d = sqrt((x2-x1)**2+(y2-y1)**2)
return n/d
def DistBtwTwoPnts(x1, y1, x2, y2):
return sqrt((x2-x1)**2+(y2-y1)**2)
def Rotate(x, y, angledeg, x_offset, y_offset):
xa = x*cos(angledeg*pi/180) + y*sin(angledeg*pi/180)
ya = -x*sin(angledeg*pi/180) + y*cos(angledeg*pi/180)
xa -= x_offset
ya -= y_offset
return xa, ya
def AngleByTwoPnts(x1, y1, x2, y2):
return atan2(x2-x1, y2-y1)*180/pi - 90
def chord(x0, y0, x1, y1, x2, y2, points, h_override=0.):
h = LnToPntDst(x0, y0, x1, y1, x2, y2)
w = DistBtwTwoPnts(x1, y1, x2, y2)
if h_override == 0.:
new_h = (1.-(h/w)/10.)*h
if new_h < h*0.01:
h = h*0.01
else:
h = new_h
else:
h = h*h_override
a = AngleByTwoPnts(x1, y1, x2, y2)
xr = []
yr = []
for i in range(points+1):
arc_percent = i/(points/2.)
if i > points/2.:
arc_percent = (points-i)/(points/2.)
if i == 0 or i == points:
arc = 0.
else:
arc = sqrt((h/2.)**2-((h/2.)-(h/2.)/((points)/2.)*i)**2.)
percent = arc/(h/2.)
y_1 = -percent*arc+(1-percent)*arc_percent
y_2 = percent*arc+(1-percent)*arc_percent
xr_1, yr_1 = Rotate(i/points*w, y_1, a, -x1, -y1)
xr_2, yr_2 = Rotate(i/points*w, y_2, a, -x1, -y1)
d1 = DistBtwTwoPnts(x0, y0, xr_1, yr_1)
d2 = DistBtwTwoPnts(x0, y0, xr_2, yr_2)
if d1 < d2:
xr.append(xr_1)
yr.append(yr_1)
else:
xr.append(xr_2)
yr.append(yr_2)
return list(zip(xr, yr))
- 重缩放函数(RF):为了方便将图形缩放到单位正方形
# 'x_o' is the original value
# 'x_min' is the current minimum of the values range
# 'x_max' is the current maximum of the values range
# 'x_r' is the rescaled value based on a new range, 'new_min' and 'new_max'
x_r = (new_max - new_min) * ((x_o - x_min)/(x_max - x_min)) + new_min
这是多弦图算法:
-
从集合组合及其大小(或从原始数据中计算)开始,例如:({a}, 25.5),({a, b}, 15),({a, c}, 14.4) 等
-
定义布局顺序和缓冲偏移量,用于间隔集合和嵌套集合组合,并提供可选的整体径向百分比填充
-
布局一个数字线,表示数据范围,并按照指定的顺序排列每个集合组合,用给定的偏移量分隔
-
向前和向后传播数字线的范围
-
通过从 3 条可用数字线中将最接近的弦端点分组来收集弦端点
-
定义用于绘图的点数,并继续计算每个部分中的点位置,利用(CPC)将直线部分和弦端点映射到圆形布局上,并利用(CF)为圆内的弦点
-
使用(CPC)添加外部集合组合和集合多边形,并使用(RF)将其重缩放到单位圆
这是一个使用我称之为多弧图的另一种可视化的笛卡尔布局:
多弧图展示了多弦图的笛卡尔布局
最简化的多弦图布局版本由以下组件构成:
-
表示独立集合人口或连接 2 个或更多集合的弦,根据存在的集合到集合关系组合的数量
-
一个中环,由集合内唯一集合组合分割成多个部分,表示“专属”集合组合大小
-
一个外环,被集合分割成多个部分,每个部分包含“包含”集合组合(表示集合大小,减去来自中间环的缓冲间距)
多弦图
Python 实现
我已通过我的 dataoutsider 包在 PyPI 上提供了该算法的初步 Python 实现。以下是使用示例:
from dataoutsider import multi_chord as mc
import pandas as pd
data = [['a', 56.5], ['a,b', 15], ['a,c', 14.4],
['a,b,d', 8.6], ['c,d', 13], ['d', 30.9],
['c,b', 10], ['b', 24.3], ['a,b,c,d', 17.2],
['b,e',5.6], ['c,d,e',17.8]]
df = pd.DataFrame(data, columns = ['group', 'value'])
df_mc = mc.multi_chord_on_groups_alias(df, percent=75)
mc.multi_chord_plot(df_mc, level = 3, transparency = 0.5)
通过 Matplotlib 的 dataoutsider 包中的多弦图
这是基础输出(df_mc):
来自 multi_chord_on_groups_alias 函数的输出
在下一节中,我将演示如何使用此输出创建一个互动可视化,以适应商业智能领域的专业人士。
Tableau Public 实现(包括 UpSet 图)
在本节中,我将提供一个在 Tableau Public (v 2023.3.0) 中实现我的多弦图的教程,并与不同组件进行交互。
首先导出多弦图数据(在上一节中创建),包括用于构建 UpSet 图的数据。我将 UpSet 图的数据方法称为multi_chord_venn,以致敬维恩图,并且因为当时我不知道 UpSet 图已经存在。
import os
df_mc.to_csv(os.path.dirname(__file__) +
'/multichord_diagram.csv', encoding='utf-8', index=False)
df_upset = mc.multi_chord_venn(df_mc).drop_duplicates()
df_upset.to_csv(os.path.dirname(__file__) +
'/upset_plot.csv', encoding='utf-8', index=False)
首先从multichord_diagram.csv开始。使用文本文件选项将文件导入 Tableau,导航到Sheet 1,并创建这些计算列:
[mc_map]:MAKEPOINT([Y], [X])
[mid_path]:int({fixed [Group]: avg(if [Type] = ‘element’ then [Path] else null end)} * 3/2)
[mc_label]:if [Type] = ‘element’ and [Path] = [mid_path] then MAKEPOINT([Y], [X]) else null end
首先将[mc_map]拖到标记下的详细信息中,以生成第一个地图图层,并通过右键点击地图区域并选择背景图层来调整这些选项:
-
取消选择所有背景地图图层(基础图层、土地覆盖等)
-
现在右键点击地图区域并选择地图选项,然后在面板中取消所有选项
关闭背景图层并继续以下步骤:
-
将[Group]、[Type]和[Value]拖到标记下的详细信息中
-
将[Count]拖到标记下的详细信息中
-
右键点击现在的SUM(Value) 并选择维度,再次右键点击并选择离散
-
对SUM(Count)重复该过程
-
再次右键点击Value并选择排序,然后选择降序,并将Value拖到标记中的药丸顶部
-
在标记下拉菜单中选择多边形(如果此时看起来很奇怪,不用担心)
-
将[Path]拖到标记下的路径中,并重复将其转换为维度的过程
-
在颜色下选择黑色边框颜色,将透明度调整为 80%,并选择编辑颜色以根据需要编辑颜色选项
现在应该可以看到多弦图的结构。让我们添加一些标签:
-
将[mc_label]拖入地图区域,会出现一个弹出窗口:添加标记图层 - 将药丸拖入此处以创建一个新的地图图层
-
将[Group]拖到此新地图图层中的标签下
-
在标记下拉菜单中选择圆形,点击标签,并选择这些选项:
-
在菜单仍然打开的情况下,点击文本 - 高亮文本框中的文本,将字体大小更改为 12,并点击确定
-
点击Color,选择白色并将透明度更改为 80%
-
最后,点击Size并将大小调整到第二个哈希
你会看到右下角有一个空值警告,你可以右键点击并选择Hide Indicator。此时你应该看到类似于这样的内容(包括你选择的颜色):
你会注意到集合按其大小顺时针递减排序,而弦按顺时针递增排序(算法的默认设置)。绘图顺序可以根据需要调整,就像我们之前对Value所做的那样。
现在让我们创建 UpSet 图。首先导入upset_plot.csv文件,选择Data选项卡并点击New Data Source。选择文本文件并导入数据。创建一个新工作表,点击底部面板上的第一个加号,并确保在右上角的Data下选择了新数据源。
现在添加这些计算列:
[count]:{fixed [Group]: sum(if [Group2] = ‘count’ then [Value2] else null end)}
[chord_magnitude]:if not isnull([count]) then [Value] else null end
[set_magnitude]:
接着将[Group2]拖到Filter中,仅选择集合(a, b, c, d, e)。将[Value2]添加到过滤器中,并仅过滤值为 1。最后,将[count]添加到过滤器中,并在右下角取消勾选Include Nulls Values
继续按照这些步骤设置矩阵视图:
-
将[chord_magnitude]拖动到Columns,在其下拉菜单中选择Minimum作为其Measure,并选择Discrete
-
在这个标签旁双击以添加一个新标签并输入:‘|’
-
按 Enter 键将文本提交为新标签,并将[Group]拖动到其旁边,然后是[count]
-
将[Group2]拖动到Rows,从下拉菜单中选择Sort,并按以下条件排序:
-
为[chord_magnitude]添加排序:
-
将[Value2]拖动到Rows的最后位置,并将Measure设置为Maximum
-
在Marks下拉菜单中,选择Circle
请注意,我发现当前版本在双轴模式下有一些错误,因此排序可能需要根据需要进行调整。现在我们进行一些格式化:
-
右键点击底部轴,选择Edit Axis,并将设置更改为:
-
关闭,再次右键点击并取消勾选Show Header
-
在视图顶部,右键点击所有离散的Columns标题(视图标题中的任何位置),除了[count],选择Rotate Label(你也可以调整每个标题容器的大小,以更好地适应标签)
-
通过右键点击外部标题并选择Hide Field Labels for Columns/Rows来隐藏外部标题
现在我们将创建一个双轴图,以便在点之间绘制一些线条:
-
将[Value2](再次)拖到Columns中的最后一个位置,并设置为Maximum,如之前所述
-
从下拉菜单中选择Dual Axis,右键点击新轴,选择Synchronize Axis,然后通过取消选中每个轴上的Show Header来隐藏这两个轴
-
在新视图中,从Marks下拉菜单中选择Line,双击Marks内以创建一个新的药丸,并输入:1
-
按回车键,将这个药丸拖到Path
这是一个完成的视图,顶部的视图下拉选项设置为Entire View:
现在让我们为弦(特定于种群)和集合(包括种群)创建类似的视图。
这是弦图视图:
这是集合视图:
现在将它们添加到仪表板中,并在仪表板顶部菜单中的Actions下设置一个操作。点击Add Action下拉菜单并选择Highlight。在Targeted Highlighting下选择Selected Fields,并选择[Group]和[Group2]字段。最后,在右侧的Run action on菜单下选择Hover选项,现在整个仪表板将在悬停于集合和弦上时高亮显示!
多弦图和 Tableau Public 中的 UpSet 图交互
结论
在本文中,我简要介绍了应用于集合之间关系的可视化历史以及我称之为“多弦图”的内容,这是一种我开发的可视化工具,用于增强一些现有方法,以便快速洞察具有复杂集合关系的数据。近年来,我有很多机会利用这一工具进行个人项目和各种业务应用,希望它能为其他人提供一些新的功能!
我相信数据可视化可以帮助解决探索性数据分析、建模和讲故事中的挑战,并且这是艺术与科学的真正交汇点,可以让所有人享受。
如果你遇到任何有趣或专业的用例,请告诉我,谢谢阅读!
参考文献
[1] John Venn, “关于命题和推理的图示和机械表示” (1880), 哲学杂志与科学期刊
[2] David Constantine, “基因组的特写:物种逐个分析” (2007), 纽约时报
[3] Martin Krzywinski, 等,“Circos:比较基因组学的信息美学” (2009), 基因组研究
[4] Lex A, Gehlenborg N, Strobelt H, Vuillemot R, Pfister H. “UpSet: 交集集的可视化” (2014), IEEE 计算机图形学与可视化汇刊
四种项目相似性度量的介绍
原文:
towardsdatascience.com/introduction-of-four-types-of-item-similarity-measures-e0aea70da335
数据挖掘
介绍如何选择项目嵌入可用时的相似性度量
·发布于 Towards Data Science ·阅读时间 5 分钟·2023 年 2 月 17 日
--
图片由 James Yarema 提供,来源于 Unsplash
推荐算法在我们日常生活中无处不在,从我们 Youtube 首页上出现的视频顺序,到沃尔玛商品货架的排列。这些算法学习用户的潜在偏好,并向用户推荐最相关的项目。为实现这一点,我们需要用数值向量来描述用户和项目,以便算法能够测量用户相似性和项目相似性,从而进行进一步的推荐。
学习用户和项目的数值向量表示有两种常见的方法:基于内容的过滤和协同过滤。在基于内容的过滤中,通过根据项目的专家知识手动构建特征来学习项目的向量表示。以移动应用为例,每个在 Google Play 上的应用都有一个应用名称、一个类别和开发者提供的一些文本描述。然后可以应用专家知识从这些信息中提取特征,以构建应用的向量表示。基于内容的过滤的优势在于它不依赖于现有的用户-项目数据。相反,协同过滤不需要对项目的专家知识,完全依赖现有的用户-项目数据。当涉及到协同过滤时,数据本身会说话。只要我们拥有足够的用户-项目数据,就可以通过矩阵分解来提取用户和项目的表示。
在获得项目的数值向量表示后,下一任务是测量项目之间的相似度以进行推荐。在这篇文章中,我们将关注各种相似度度量,并讨论如何选择合适的相似度度量。同时,将提供 Spark 代码示例,因推荐算法通常应用于大数据。
言归正传,让我们开始吧!
1. 示例数据介绍
在这篇文章中,我们将通过分析大量用户的已安装应用列表来研究应用相似度。假设我们有 m 个用户,总共安装了 n 个应用,那么我们可以构建一个 m×n 的用户-项目矩阵。与协同过滤方法不同,协同过滤方法将学习用户和项目的低维表示,以进行实时推荐任务的快速计算,但在这个示例中,我们将直接使用每个应用的 m 维表示。协同过滤学习用户和项目的低维表示,以进行实时推荐任务的快速计算,但这里我们专注于演示如何在获得数值向量表示后计算相似度。因此,我们简化了获取项目向量的过程。
此外,为了演示目的,我们将仅使用一个包含 4 个用户和 4 个应用的虚拟示例,该示例可以通过以下代码生成:
生成已安装应用列表示例的代码
已安装应用列表示例
数据帧随后被转换为每个用户的安装列表行:
将已安装应用列表转换为 userid 作为键的代码
转换后的已安装应用列表结果
2. 各种相似度度量
各种相似度度量的计算依赖于两个常见的统计结果:应用对的余弦相似度和应用向量的欧几里得量级,这些可以计算如下:
生成应用对的余弦相似度和欧几里得量级的代码
应用对的余弦相似度和欧几里得量级结果
2.1 余弦相似度
余弦相似度衡量两个向量之间的角度。可以通过调用 Spark 的原生函数轻松计算。
余弦相似度公式
2.2 点积
点积是余弦相似度乘以两个向量的欧几里得范数。它可以理解为一个向量在另一个向量上的投影。一个向量的欧几里得范数衡量了该应用在用户中的受欢迎程度。当更多用户安装一个应用时,该应用的欧几里得范数将变大。因此,与余弦相似度不同,点积受两个应用的受欢迎程度的影响。一个受欢迎的应用和一个不受欢迎的应用的点积会很小,就像我们想象短向量在长向量上的投影,投影长度不会很长。因此,在余弦相似度的基础上,点积考虑了两个应用的受欢迎程度。
生成点积的代码
2.3 Jaccard 相似度
Jaccard 相似度是用户集合的交集除以安装两个应用的用户集合的并集。用户集合的交集衡量了两个应用的相似度,而用户集合的并集衡量了两个应用的多样性。从这个意义上讲,我们可以将 Jaccard 相似度视为一个以两个应用的多样性为标准化的度量。
Jaccard 相似度的说明
通过使用两个向量的欧几里得范数和计算出的点积,可以轻松计算 Jaccard 相似度:
生成 Jaccard 相似度的代码
2.4 条件概率提升
条件概率提升衡量了应用 B 的安装在多大程度上有助于应用 A 的安装。
条件概率提升的方程
通过使用两个向量的欧几里得范数,可以轻松计算条件概率。
计算条件概率提升的代码
映射回 app_id 后,可以得到最终结果。
合并四个相似度结果的代码
合并的相似度结果
总结
在这篇文章中,我们介绍了嵌入中的四种相似度度量,即余弦相似度、点积、Jaccard 相似度和条件概率提升。根据嵌入相似度的定义,我们可以选择使用最合适的度量。
Apache Iceberg 表介绍
原文:
towardsdatascience.com/introduction-to-apache-iceberg-tables-a791f1758009
选择 Apache Iceberg 作为数据湖的几个令人信服的理由
·发布于 Towards Data Science ·8 分钟阅读·2023 年 4 月 10 日
--
由 Annie Spratt 提供的照片,来源于 Unsplash
Apache Iceberg:这是什么?Apache Iceberg——它是新的数据湖文件格式吗?还是表格格式?它为何如此优秀?数据湖的时间旅行?
我将在这个故事中尝试回答所有这些问题。
事务一致的数据湖表以及时间点快照隔离是我们所需的一切。
选择表格格式是任何追求数据湖或数据网格策略的人都必须做出的关键选择。选择数据湖平台时需要考虑的一些重要功能包括模式演变支持、读写时间、可扩展性(数据处理是否可以 Hadoop 分割?)、压缩效率以及时间旅行等。
根据业务需求选择正确的文件和表格格式将决定你的数据平台的速度和成本效益。
Iceberg 是一种表格格式,引擎和文件格式无关。Iceberg 通常不是像Apache Hive这样的旧技术的开发。这是件好事,因为从旧技术中开发可能会有局限性。一个好的例子是模式如何随时间变化,而 Iceberg 可以像重命名列一样简单地处理这种变化。它被设计并证明在全球最苛刻的工作负载中大规模的数据湖平台上表现良好。
越来越多的数据工具开始引入冰山表的支持。
例如,我们可以在 AWS Athena(必须是引擎 3)中这样创建一个 Apache Iceberg 表:
谷歌的 BigQuery 现在也支持 Iceberg 表:
冰山表和数据平台类型
Netflix 最初创建了 Iceberg,最终它得到了 Apache 软件基金会的支持和捐赠。现在,Iceberg 独立开发,它是一个完全非盈利的开源项目,专注于处理复杂的数据平台架构。
它支持多种大数据文件格式,包括 Apache Avro、Apache Parquet 和 Apache ORC。
在设计数据平台时,将数据保存在数据湖中是最简单的解决方案之一。
相比于现代数据仓库解决方案,它需要的维护要少得多。
数据湖通常用于存储所有类型的数据——结构化和非结构化——无论大小。数据湖传统上与 Apache Hadoop 分布式文件系统(HDFS)连接。然而,组织越来越多地使用对象存储解决方案,如 Amazon S3、Google Cloud Storage 或 Microsoft Azure Data Lake Storage。
简而言之,数据湖通过集中管理数据来简化数据管理。
与此相反,当每次访问都通过一个单一系统(数据仓库)进行时,它简化了并发管理和更新,但限制了灵活性并增加了成本。我之前在这里写过:
它在多大程度上满足了您的业务需求?选择的困境。
towardsdatascience.com
一切都很棒,但在构建数据湖数据平台时,我们可能还会遇到其他一些问题,例如没有时间旅行、没有模式演变支持以及数据转换和定义的复杂性。
当我们构建数据湖数据平台时,外部表通常会带来与该架构相关的一整套缺点……但 Iceberg 有助于解决这些问题。
传统外部表的一些已知限制:
-
例如,我们不能通过 DML 语句修改它们,且数据一致性不能得到保证。话虽如此,如果在处理过程中底层数据发生变化,我们可能会得到不一致的结果。
-
现代数据仓库中通常有有限的并发查询数,例如 BigQuery 中为 4。
-
外部表不支持集群,也不允许从中导出数据。
-
不允许使用通配符引用表名。
-
没有时间旅行功能
根据我的经验,数据一致性是大规模分析所需的最重要的特性之一。Iceberg 解决了这个问题,现在多个引擎(如 Spark、Hive、Presto、Dremio 等)可以同时操作同一张表。
它还提供了许多其他出色的功能,例如回滚到先前的表版本(以悄悄解决问题),以及在处理海量数据时的高级数据过滤能力。
数据一致性和改进的处理效率
一个好的例子是,当 ETL 过程通过从存储中添加和删除文件来修改数据集时,另一个读取数据集的应用程序可能会处理数据集的部分或不一致的表示,从而产生不准确的结果。
Iceberg 通常通过利用大量清单(元数据)文件来缓解这些风险,以便在数据处理过程中进行快照。它将捕获模式并维护增量,包括文件信息和分区数据,以保证一致性和完全隔离。
Iceberg 还会自动以层次结构的方式排列快照元数据,这确保了快速高效的表格修改,无需重新定义所有数据集文件,从而在数据湖规模下实现最佳性能。
Iceberg 提供 SQL 命令,允许你合并新数据(MERGE)、更改旧行和删除特定行。
Iceberg 可以积极重建数据文件以提高读取性能,或者使用删除增量加快更新速度。
一个好的合并示例可以在这里找到:
//advanced-sql-techniques-for-beginners-211851a28488?source=post_page-----a791f1758009-------------------------------- ## 初学者的高级 SQL 技巧
在 1 到 10 的范围内,你的数据仓库技能有多好?
[towardsdatascience.com
另一个可以应用于上述 AWS Athena 表格的示例如下:
图片由作者提供
时间旅行功能
现代数据仓库允许你在数据中进行时间旅行,即我们可以转到特定的时间戳以获取表格中的特定数据状态。例如,在 Google Cloud BigQuery 中,我们可以运行以下 SQL 来实现:
SELECT *
FROM `mydataset.mytable`
FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR);
这也可能是数据湖平台的一个“不错的附加功能”。
如果你围绕文件设计数据架构,例如 Apache ORC 或 Apache Parquet,你将受益于实现的简便,但你也会遇到上述提到的问题。时间旅行不被支持。模式演变一直是个问题。当字段随时间变化时可能会出现问题。例如,AVRO 文件格式支持模式演变,我之前写过关于大数据文件格式优缺点的文章:
//big-data-file-formats-explained-275876dc1fc9?source=post_page-----a791f1758009-------------------------------- ## 大数据文件格式详解
Parquet 与 ORC 与 AVRO 与 JSON。该选择哪个以及如何使用它们?
[towardsdatascience.com
Iceberg 将整个历史记录保存在 Iceberg 表格格式中,没有存储系统依赖。
Iceberg 表格用户可以在任何 Iceberg 快照或历史时间点查询过去的状态,以获得一致的结果、进行比较或回滚以修复错误,因为历史状态是不可变的。
SELECT count(*) FROM mydatabase.user_transactions FOR TIMESTAMP AS OF TIMESTAMP '2023-01-01 00:00:00.000000 Z'
模式演进支持
数据湖文件以及它们的模式随着时间的推移可能会发生变化,这并不是秘密。现在在 Iceberg 表中添加一列不会返回“无效”数据。
列名和顺序可以更改。最棒的是,模式更新从不需要重建你的表。
ALTER TABLE mydatabase.user_transactions ALTER COLUMN total_cost_gbp AFTER user_id;
Iceberg 允许就地修改表,并确保正确性,即添加的新列永远不会从其他列中读取现有值。当数据量发生变化时,我们现在可以仅通过 SQL 修改分区布局或更改表模式,即使是嵌套结构。
traffic_source STRUCT<name STRING, medium STRING, source STRING>,
这就是 Iceberg 所谓的分区演进。
当你修改分区规范时,使用早期规范写入的现有数据不会受到影响。
每个分区版本的元数据被单独保存。Iceberg 不需要昂贵的操作,例如重写表数据或迁移到新表。
改进的分区
在 Iceberg 中,这被称为“隐藏分区”。传统的数据湖分区方式称为“Hive 分区布局”。
让我们考虑一个在 BigQuery 中具有 Hive 分区布局的外部表:
CREATE OR REPLACE EXTERNAL TABLE production.user_transactions (
payment_date date,
timestamp timestamp,
user_id int,
total_cost_usd float,
registration_date string
)
WITH PARTITION COLUMNS (
dt STRING, -- column order must match the external path
category STRING)
OPTIONS (
uris = ['gs://user-transactions/*'],
format = 'AVRO',
hive_partition_uri_prefix = 'gs://user-transactions'
);
在 Hive 中,分区必须是明确的,并且显示为列,因此 **user_transactions**
表将包含一个 **dt**
日期列。这意味着对我们表进行操作的所有 SQL 查询都必须除了 **timestamp**
过滤器外,还要有 **dt**
过滤器。
Hive 需要分区值。它不了解事务时间戳和我们表中的
dt
之间的联系。
相反,Iceberg 通过取列值并在必要时进行修改来生成分区值。Iceberg 负责将事务timestamp
转换为 **dt**
并维护连接。Iceberg 可能会隐藏分区,因为它不需要用户维护分区列。
分区值总是被适当地创建并在可能的情况下用于加速查询。
dt
对生产者和消费者都是不可见的,查询不再依赖于我们表的实际物理布局。
它有助于避免在转换数据时出现分区错误。例如,使用错误的日期格式,导致不正确的分区,而不是失败:**20230101**
代替 **2023–01–01**
。这是 Hive 分区布局中一个众所周知且最常见的问题。 另一个 Iceberg 有助于解决的问题是,在 Hive 分区布局中,所有工作的查询都与表的分区方案相关,因此更改分区设置将会破坏查询。
结论
关系型数据库和数据仓库都支持原子事务和时间旅行,但它们以其专有的方式实现。Iceberg 作为一个 Apache 项目,是完全开源的,不依赖于任何特定的工具或数据湖引擎。
Iceberg 支持行业标准文件格式如 Parquet、ORC 和 Avro,并且与 Dremio、Spark、Hive 和 Presto 等关键数据湖引擎兼容。其广泛的社区合作生成新想法,并提供长期的帮助。它拥有各种活跃的社区,例如公共 Slack 频道,任何人都可以自由参与。它经过设计并证明在全球最大工作负载和环境中的数据湖平台上表现出色。
组织现在可以通过利用 Iceberg 充分发挥数据湖架构的潜力和优势。体验基于云存储的数据平台的成本效益,同时无需牺牲传统数据库和数据仓库解决方案的功能和能力。
它使我们的数据湖平台真正灵活且可靠
总结一下,Iceberg 表格格式的优点如下:
-
多个独立程序可以同时一致地处理相同的数据集。
-
改进的数据处理和数据可靠性(针对非常大的数据湖规模表格的更新更加高效)。
-
随着时间的推移,改进的模式处理。
-
ETL 管道大大简化(通过对数据湖中的数据进行操作,而不是在多个独立系统之间传输数据)。
推荐阅读:
1. cloud.google.com/bigquery/docs/time-travel
4. iceberg.apache.org/community/
5. cloud.google.com/bigquery/docs/iceberg-tables
6. medium.com/towards-data-science/data-platform-architecture-types-f255ac6e0b7
7. medium.com/towards-data-science/big-data-file-formats-explained-275876dc1fc9
8. iceberg.apache.org/docs/latest/evolution/
9. iceberg.apache.org/docs/latest/partitioning/#icebergs-hidden-partitioning
10. cloud.google.com/bigquery/docs/external-data-cloud-storage#sql
asyncio 介绍
原文:
towardsdatascience.com/introduction-to-asyncio-57a5a1290ce0
用 Python 管理 I/O 绑定的并发
·发表于Towards Data Science ·阅读时间 6 分钟·2023 年 3 月 24 日
--
并发和并行指的是程序或计算机同时运行多个操作的能力。通常,我们区分多处理(并行)和多线程(并发)——前者描述运行多个进程,而后者指的是在同一个进程内生成多个线程。在 Python 中,由于全局解释器锁(GIL),一次只能执行一个线程,导致任何多线程应用实际上都是单核的。不过,例如对于 I/O 绑定的程序(即更多时间花在等待输入上——这才是瓶颈而非计算),在 Python 中使用多线程仍然是有意义的。在这篇文章中,我们将介绍asyncio,这是一种优雅且简便的方法来实现这一点。
Gabriel Gusmao 在Unsplash拍摄的照片
为什么选择 asyncio?
我们将指定一篇未来的文章来讨论多处理和多线程的区别,以及 asyncio 在其中的作用。不过我们这里想要激励大家使用它。如上所述,多线程特别适用于 I/O 绑定的,即非 CPU 绑定的应用。而 asyncio 是实现这一点的一种方式,许多开发者在某些用例中更倾向于使用它。
asyncio 对于 I/O 绑定的程序非常出色,我们希望编写易于理解、结构清晰、抗错误的代码。asyncio 实际上只使用一个线程,并且由用户决定当前执行的代码块何时“让出”执行以等待外部输入,并允许执行不同的代码块。这通常使代码更易于阅读,同时也给予用户更多控制,减少死锁等风险。
激励示例
让我们从一个入门示例开始。考虑以下代码:
import time
def sleepy_function():
print("Before sleeping.")
time.sleep(1)
print("After sleeping.")
def main():
for _ in range(3):
sleepy_function()
main()
在其中,我们调用了三次 sleepy_function()
,它简单地打印两件事,然后睡眠一秒。总的来说,这个程序需要 3 秒钟来执行。但当然,让 CPU 等待 1 秒而不做其他工作是相当浪费的。因此,进行并行化是很自然的 —— 这正是 I/O 绑定程序的定义。
使用 asyncio,以下代码可以并行化如下:
import asyncio
async def sleepy_function():
print("Before sleeping.")
await asyncio.sleep(1)
print("After sleeping.")
async def main():
await asyncio.gather(*[sleepy_function() for _ in range(3)])
asyncio.run(main())
请注意,asyncio 仅在 Python 3.4 及以上版本中可用,并且在这些版本中包含在标准库中。
接下来,我们将更详细地查看所使用的语法和 asyncio 一般情况。
使用 asyncio
asyncio 的核心概念是协程和任务。协程基本上是函数,它们可以将执行权交给控制 asyncio 线程(例如,等待输入),而不会丢失其状态(有点类似于 生成器表达式)。因此,在这种形式的并发中,用户完全控制线程之间(协程/任务)的上下文切换发生的时机,而不是像经典的多线程那样由操作系统决定。由于这一点,以及 asyncio 只使用一个线程 —— asyncio 代码更不易出错,并且我们(通常)不需要使用任何锁定机制,如锁或互斥量。
协程使用关键字 async 定义:
async def sample_coroutine():
print("Inside sample_coroutine")
现在仅仅像平常一样调用 sample_coroutine()
不够 —— 这将不起作用。相反,我们必须指示 asyncio 执行此操作。我们可以通过以下方式实现:
asyncio.run(sample_coroutine())
下一个重要的关键字是 await
。这向 asyncio 发出信号,表示我们想要等待某个事件,并且它可以继续执行代码的另一部分。我们可以 await
的任何东西都被称为 Awaitable
,协程和任务就是两个例子。
import asyncio
async def sample_coroutine_2():
print("Inside sample_coroutine_2")
async def sample_coroutine():
print("Inside sample_coroutine")
await asyncio.sleep(1)
await sample_coroutine_2()
asyncio.run(sample_coroutine())
在上述示例中,我们首先使用 asyncio.sleep
等待 1 秒 —— 然后等待 sample_coroutine_2
。注意这个程序的执行时间仍然是 1 秒,并没有更少 —— 但缩短执行时间不是这个简单示例的重点。
任务
如果我们现在想要并行执行不同的代码块,我们可以使用任务。通过 asyncio.create_task
我们可以从协程创建一个任务,然后通过 await
执行它。这样,我们可以并行运行多个协程:
import asyncio
async def sleepy_function():
print("Before sleeping.")
await asyncio.sleep(1)
print("After sleeping.")
async def main():
task1 = asyncio.create_task(sleepy_function())
task2 = asyncio.create_task(sleepy_function())
task3 = asyncio.create_task(sleepy_function())
await task1
await task2
await task3
asyncio.run(main())
gather
然而,我通常发现使用以下涉及asyncio.gather
的语法更加方便和直观,它可以接受任意多个Awaitables
作为输入:
import asyncio
async def sleepy_function():
print("Before sleeping.")
await asyncio.sleep(1)
print("After sleeping.")
async def main():
await asyncio.gather(
sleepy_function(), sleepy_function(), sleepy_function()
)
asyncio.run(main())
我们可以通过使用星号(*)运算符来缩短上述代码,该运算符将给定的可迭代对象解包为单个参数(从而缩小了与介绍示例之间的差距):
import asyncio
async def sleepy_function():
print("Before sleeping.")
await asyncio.sleep(1)
print("After sleeping.")
async def main():
await asyncio.gather(*[sleepy_function() for _ in range(3)])
asyncio.run(main())
Queue
让我们通过介绍asyncio.Queue
来结束这篇文章,它是一个适用于“多线程” asyncio 应用程序的安全队列,是许多实际应用场景中的有用工具。我们将用它来进行一个稍微复杂一点的示例,希望通过一个更真实的“实际”示例来结束这篇文章。
我们的示例由一个生产者线程和 N 个消费者线程组成。生产者尝试猜测ISBN 号码,并将其放入队列中。消费者从队列中移除这些号码,并对返回关于该 ISBN 书籍信息的公共端点发起GET 请求。如果消费者猜对了 ISBN,我们会打印返回的书籍信息。
为了以异步模式执行请求,我们使用aiohttp。Web 请求通常是 asyncio 的常见主题和用例:通常,一个网络服务器将并发运行不同的请求,渲染/查询不同的内容,然后返回结果网页。由于这不是 CPU 密集型的,大多数时间(可能)都花在等待请求结果上,因此 asyncio 非常适合这里。
让我们看看代码:
import asyncio
import threading
from random import randint
import aiohttp
NUM_CONSUMERS = 8
async def producer(queue: asyncio.Queue) -> None:
while True:
random_isbn = "".join(["{}".format(randint(0, 9)) for _ in range(0, 10)])
queue.put_nowait(random_isbn)
await asyncio.sleep(0.05)
async def consumer(consumer_idx: int, queue: asyncio.Queue) -> None:
while True:
isbn = await queue.get()
async with aiohttp.ClientSession() as session:
async with session.get(
"https://openlibrary.org/api/books?bibkeys=ISBN:"
+ isbn
+ "&format=json"
) as resp:
book_descriptor = await resp.json()
if book_descriptor != {}:
print(
f"Consumer {consumer_idx} found valid ISBN. Current queue size: {queue.qsize()}. Discovered book: {book_descriptor}"
)
queue.task_done()
async def main():
queue = asyncio.Queue()
await asyncio.gather(
*([producer(queue)] + [consumer(idx, queue) for idx in range(NUM_CONSUMERS)])
)
if __name__ == "__main__":
asyncio.run(main())
除了上述内容外,值得指出的是如何使用异步队列。正如我们所见,我们通过put_nowait()
将项目放入队列。_nowait
后缀仅意味着当队列已满时不等待——这对我们来说不重要,因为我们没有对队列大小设限。
项目通过get()
从队列中弹出——但需要通过相应的tasks_done()
来标记完成!
请注意,我调整了消费者的数量和生成 ISBN 之间的休眠时间,使得在我的笔记本电脑上队列大小保持相对恒定。
这标志着我们对 asyncio 教程的结束。希望这次阅读对你有趣,谢谢你的光临!
PS:如果你偶然发现了使用上一个示例程序的任何有趣阅读,请告诉我!
聚类算法简介
原文:
towardsdatascience.com/introduction-to-clustering-algorithms-76da35b5670a
一份关于 10 种常用于层次、划分和基于密度的聚类算法的全面指南
·发表于Towards Data Science ·阅读时长 9 分钟·2023 年 11 月 12 日
--
介绍
聚类算法在数据分析中扮演着重要角色。这些无监督学习、探索性数据分析工具通过将数据点根据共享特征分类到不同的组中,为知识发现提供了系统。这使得在原始数据中可能难以看清的关系和趋势得以识别。它们通过系统性地为复杂和精细的数据集增添理解,促进了更为明智的决策。
在本文中,我们将介绍三种类型的聚类算法的基础知识:层次聚类、划分聚类和基于密度的聚类模型。我们将从定义这些类别开始。接下来,我们将深入探讨 10 种不同的聚类算法,提供定义、原始或有趣的研究论文链接、算法的优缺点以及每种算法的 python 代码片段。
目录
层次聚类算法
-
Birch
-
Cure
-
ROCK
-
Chameleon
划分聚类算法
-
K-Means
-
K-Medoids (PAM)
-
CLARANS
-
ISODATA
基于密度的聚类算法
-
DBSCAN
-
DENCLUE
层次聚类算法
定义: 层次聚类是一种聚类分析方法,建立一个簇的层次结构。它可以被可视化为一个树状结构(树状图),其中叶子表示单个数据点,根表示包含所有数据点的单一簇。
使用案例:
-
分类问题。
-
当垂直关系在数据中很重要时。
优点:
-
提供了一个簇的层次结构。
-
无需事先指定簇的数量。
弱点:
-
易受噪声和离群点的影响。
-
对大型数据集计算密集。
Birch
总结:
Birch 算法,全称“Balanced Iterative Reducing and Clustering using Hierarchies”,是一种设计用于扩展性和效率的层次聚类算法,特别适合大型数据集。它使用两步过程来构建簇:
-
构建一个基于特征的树,称为聚类特征(CF)树,用于总结数据集的分布。这个 CF 树允许高效的内存使用和增量更新。
-
基于 CF 树的叶子节点应用聚类机制,形成紧密的簇。
原始论文 / 有用的研究:
[## BIRCH:一种高效的数据聚类方法,适用于非常大的数据库:ACM SIGMOD Record: Vol 25, No…
在大型数据集中寻找有用的模式最近引起了相当大的兴趣,其中最广泛的…
dl.acm.org](https://dl.acm.org/doi/10.1145/235968.233324?source=post_page-----76da35b5670a--------------------------------)
优点:
Birch 以其处理大量数据的能力和对离群点的韧性而闻名。
Python 代码:
使用sklearn的 Birch 算法
Cure
总结:
CURE(Clustering Using Representatives)算法是一种聚合层次聚类方法,旨在解决传统基于中心点的算法如 K-Means 的局限性,尤其是在处理非球形和任意形状的簇时。CURE 采用独特的方法,通过用固定数量的点(称为代表点)来表示簇,这些代表点是从每个簇中随机选择的。然后这些代表点会“收缩”到质心附近,从而有效地捕捉簇的几何形状。CURE 以其对离群点的鲁棒性、处理不同形状和大小簇的能力,以及在传统中心点算法可能失败的情况下的改进性能而闻名。
原始论文:
[## CURE:一种高效的大型数据库聚类算法:ACM SIGMOD Record: Vol 27, No 2
在数据挖掘中,聚类对于发现群组和识别有趣的分布模式非常有用…
优点:
Cure 对离群点具有鲁棒性,并且能够识别形状多样的聚类。
Python 代码:
Cure 算法使用 pyclustering
ROCK
总结:
ROCK(使用链接的鲁棒聚类)算法是一种聚合层次聚类方法,旨在解决带有分类属性的数据集聚类中的挑战。它引入了“链接”概念,用于测量具有分类属性的数据点之间的接近程度。通过利用“优度测量”,ROCK 旨在通过评估对象之间的相似性来识别聚类。该算法特别适用于具有混合属性类型的数据集,提供了全局的聚类方法。然而,如果静态模型中的参数选择与被聚类的数据集差异显著,ROCK 可能会产生模糊的结果,并且在准确定义不同大小和形状的聚类时可能会遇到困难。
原始论文 / 有用的研究:
## Rock: A robust clustering algorithm for categorical attributes
在数据挖掘中,聚类对于发现潜在数据中的分布模式非常有用。聚类算法…
优点:
ROCK 在维度增加时扩展良好,并且可以使用全局方法测量聚类内的相似性。该算法适用于分类变量。
Python 代码:
ROCK 算法使用 pyclustering
Chameleon
总结:
Chameleon 是一种动态聚类算法,旨在基于动态模型测量两个聚类之间的相似性。它分为两个阶段:
-
创建一个图,其中包含每个点及其 N 个最近邻之间的链接。
-
通过图分割算法拆分图,结果是许多小的未连接子图。Chameleon 迭代地合并两个最相似的聚类,考虑它们的连通性和接近程度,这使得它在处理任意形状的聚类时比其他一些算法更具功能性。
原始 Chameleon 算法论文中的图像见下文链接
原始论文 / 有用的研究:
优点:
Chameleon 可以基于动态模型衡量簇的相似性,对处理任意形状的簇效果良好。
Python 代码:
目前没有现成的库来运行这个算法。然而,Moonpuck 已经实现了变色龙算法,他们的代码可以在Github 上查看。
分区聚类算法
定义: 分区聚类根据相似性将数据划分为不重叠的子集(分区)。最常见的方法是 K-Means 算法,在该方法中,数据点被分配给最近的质心。
使用场景:
-
大数据集,因为这些算法具有较低的计算需求。
-
当聚类的连贯性比层次结构更重要时。
优点:
-
对大数据集高效。
-
适合球形簇。
弱点:
-
对噪声和异常值敏感。
-
假设簇的形状为超椭球形。
K-Means
总结:
K-Means 算法是一种基于分区的聚类技术,广泛用于将数据点分组到 K 个不同的簇中。算法的步骤如下:
-
随机选择特征空间中的 K 个质心。
-
将每个数据点分配给最近的质心。
-
根据每个簇内点的均值迭代更新质心。
-
继续以上步骤直到收敛,即质心不再显著变化。
K-Means 计算高效且易于实现,使其成为最受欢迎的聚类算法之一;然而,它的性能可能对质心的初始位置敏感,并受到异常值的影响。此外,K-Means 假设簇是球形的,对于大小和密度变化的簇表现不佳。
原始论文 / 有用的研究:
[## [PDF] 最小二乘量化在 PCM 中 | Semantic Scholar
对于任何有限数量的量子,得出了相应的结果;即,找到了必要条件……
优点:
K-Means 高效,广泛研究并且易于理解,适用于各种领域,并且对球形簇(模型的假设)效果良好。
Python 代码:
K-Means 算法使用 sklearn
K-Medoids (PAM)
总结:
K-Medoids 算法,也称为围绕中位数的划分(PAM),是一种类似于 K-Means 的聚类技术,但有一个关键区别:它不是使用均值(质心)来表示簇,而是采用实际数据点(中位数),这些点使得簇内所有其他点的平均差异最小。这使得 K-Medoids 对离群点更具鲁棒性,对极端值的影响也更小。该算法通过选择数据点作为中位数并进行更新,迭代地精炼簇分配,直到达到稳定配置。虽然 K-Medoids 对噪声比 K-Means 更具鲁棒性,但由于其全面搜索最佳中位数的特性,它仍可能计算开销较大。
原始论文 / 有用研究:
[## 大数据的 K-Means 和 K-Medoids 算法分析
聚类在数据探索、预测创建以及克服数据异常方面发挥着至关重要的作用…
www.sciencedirect.com](https://www.sciencedirect.com/science/article/pii/S1877050916000971?source=post_page-----76da35b5670a--------------------------------)
优点:
与 K-Means 使用的质心相比,K-Medoids 对离群点更具鲁棒性,并且使用实际数据点来识别簇中心。
Python 代码:
K-Medoids 算法使用 pyclustering
CLARANS
总结:
CLARANS(基于随机搜索的大规模应用聚类)是一种结合了采样技术和围绕中位数(PAM)方法的聚类算法。它通过随机搜索发现簇,而无需依赖额外的数据结构。CLARANS 在高维空间中特别鲁棒,因为它不假设特定的距离函数,并且可以有效识别具有非凸形状的簇。然而,它的效率以增加计算复杂度为代价,这使得它可能比其他划分方法更慢。
原始论文 / 有用研究:
ieeexplore.ieee.org/abstract/document/1033770
优点:
CLARANS 对维度增加具有鲁棒性,并能有效识别多边形形状的对象。
Python 代码:
CLARANS 算法使用 pyclustering
ISODATA
总结:
ISODATA(迭代自组织数据分析技术)算法是一种迭代和自适应的聚类方法,被认为是 K-Means 算法的变体。它通过基于用户定义的阈值(如每个簇的最小点数、分裂簇的最大方差和合并簇的最小距离)动态调整簇。该适应性使得 ISODATA 通过其分裂过程有效处理异常值,并防止形成拉长的簇。尽管在处理噪声和不同形状的簇方面有其优势,但 ISODATA 对输入参数的敏感性和需要细致调整是需要注意的事项。
原始论文/有用的研究:
www.sciencedirect.com/topics/computer-science/isodata-algorithm?source=post_page-----76da35b5670a--------------------------------
[## Isodata 算法
第一个自动阈值选择方法可能是 ISODATA 算法,这一算法最初由…
优点:
ISODATA 比 k-means 更好地处理异常值,并在迭代过程中动态调整簇。
Python 代码:
ISODATA 是 K-Means 的一个变种,可以通过上面 K-Means 部分的代码实现,经过一些调整。你还可以使用这个 Github 代码。
基于密度的聚类算法
定义: 基于密度的聚类将簇识别为由高数据点密度的连续区域组成,这些区域被低密度的区域分隔开。它基于这样一个想法,即簇是相对于其周围区域的高密度区域。
使用案例:
-
处理任意形状的簇。
-
处理噪声数据。
优点:
-
能够发现任意形状的簇。
-
对噪声具有鲁棒性。
缺点:
- 对高维数据的效率和性能可能会受到影响。
DBSCAN
总结:
基于密度的空间聚类算法(DBSCAN)是一种基于密度的聚类技术,通过数据点在特征空间中的密度来识别簇。它将点分类为三类:
-
核心点:密集区域。
-
边界点:簇的边缘上的点。
-
噪声点:孤立点。
DBSCAN 高效地发现任意形状的簇,不需要预定义簇的数量,并且对噪声具有鲁棒性。然而,它的性能可能会受到参数选择的影响,例如邻域半径和形成密集区域所需的最小点数。
原始论文/有用的研究:
[## 一种基于密度的算法,用于发现大空间数据库中的簇和噪声 | …]
聚类算法在空间数据库的类别识别任务中具有吸引力。然而,…
dl.acm.org](https://dl.acm.org/doi/10.5555/3001460.3001507?source=post_page-----76da35b5670a--------------------------------)
优势:
DBSCAN 对噪声有效,并且可以发现任意形状的簇。
Python 代码:
使用 sklean 的 DBSCAN 算法
DENCLUE
总结:
DENCLUE(密度聚类)算法是一种基于密度的聚类技术,它根据局部密度吸引子来确定簇,这些吸引子代表总体密度函数中的局部最大值。它采用影响函数来计算数据点之间的距离,密度函数是这些影响的累积和。DENCLUE 旨在识别任意形状的簇,并且具有良好的可扩展性,使其适用于具有不可预测结构的数据集。然而,DENCLUE 对输入参数敏感,可能会受到维度诅咒的影响。
原始论文 / 有用的研究:
[## DENCLUE 算法的各种改进概述 | 第二届国际会议论文集…]
关注通知:ACM 收到了对 DATA 的同行评审过程的完整性提出疑问的证据…
dl.acm.org](https://dl.acm.org/doi/10.1145/3368691.3368724?source=post_page-----76da35b5670a--------------------------------)
优势:
DENCLUE 具有可扩展性,可以发现形状不可预测的簇,并且对噪声有效。
Python 代码:
没有支持 DENCLUE 的库。请参阅 mgarrett57 在 Github 上的实现。
结论
在本文中,我们涵盖了分层、划分和基于密度的聚类领域中最常用的 10 种聚类算法。虽然这篇文章旨在详尽介绍基本知识,我将深入探讨这些算法,并在未来专门针对每个算法的文章中提供更多背景,提供有关它们的文献综述。我们还为每种算法提供了研究论文的链接,作为进一步探索的有用起点。希望这篇文章能为你使用聚类进行数据探索项目提供一个良好的起点。
使用{dplyr}进行 R 的数据操作入门
原文:
towardsdatascience.com/introduction-to-data-manipulation-in-r-with-dplyr-ae13383af3d7
学习如何使用 R 中的{dplyr}包,它帮助你解决最常见的数据操作挑战
·发布于Towards Data Science ·27 分钟阅读·2023 年 11 月 27 日
--
照片由Claudio Schwarz提供
介绍
在之前的帖子中,我们展示了如何在 R 中操作数据。特别是,我们介绍了如何创建和操作向量、因子、列表和数据框。这是对 R 的介绍,面向初学者。此外,只要可能,所有操作都是在基础 R 中完成的,即不需要加载任何包。
在这篇帖子中,我们将再次展示如何在 R 中操作数据,但这次使用的是{dplyr}
包。
{dplyr}
包由 Hadley Wickham 及 posit 团队开发,提供了一整套功能,帮助你解决最常见的数据操作挑战,例如:
-
基于值过滤观测数据
-
基于值或位置提取观测数据
-
基于特定数量或比例的行进行观测抽样
-
根据一个或多个变量对观测数据进行排序
-
根据变量的名称或位置选择变量
-
重新命名变量
-
基于现有变量添加新变量
-
将观测数据或变量汇总到一个描述性指标
-
按组执行任何操作
-
将观测数据分类为两个或多个组
-
等等
关于该包的更多信息,请访问dplyr.tidyverse.org。
在这篇文章中,我们将介绍使用{dplyr}
包进行数据操作和数据管理的最常见函数(在数据框上进行说明)。然而,这并不是一个详尽的列表!你可能会需要其他未在此处介绍的函数。对于感兴趣的读者,请参见文章末尾的更多资源。
我经常被问到的问题是,是否最好先学习基础 R 的数据操作,然后再学习{dplyr}
,还是直接学习{dplyr}
。
也许每个人不一定同意,但这是我倾向于回答的。{dplyr}
在效率和代码的清晰度/可读性方面的优势是显而易见的。因此,即使我给统计学家提供培训,我也会介绍{dplyr}
包。然而,我仍然相信,学习基础 R 的数据操作很重要,原因有二:
-
每个人在某个时候都会遇到用基础 R 编写的代码(无论是与其他 R 用户合作,还是从教科书或在线找到的代码)。
-
从我收到的学生反馈中,我发现当你对基础 R 有所了解时,学习
{dplyr}
相对容易(这对于那些一开始感到困难的人来说非常有成就感)。
基于这些原因,我倾向于先使用基础 R 教授数据操作,然后逐渐转向{dplyr}
(以至于在一些高级培训课程中,我几乎完全不使用基础 R)。顺便提一句,这也是我在 R 中进行数据可视化时遵循的方法:我首先教如何使用基础 R 绘制数据,然后逐步教授如何使用[{ggplot2}](https://statsandr.com/blog/graphics-in-r-with-ggplot2/)
。
采用这种方法,一些学生可能会觉得他们学习基础 R 的时间被浪费了。至少,在培训期间,他们可能有这种印象。然而,一旦培训结束,他们需要自行学习 R 或从事实际项目时,他们会感激自己学过这两者。
我很想听听其他老师的看法,因此请随时分享你的意见。
数据
为了展示不同的函数,我们将使用数据框penguins
,该数据框可以在{palmerpenguins}
包中找到(Horst, Hill, 和 Gorman 2020)。数据使用CC-0许可,并可以从 CRAN 下载:
# install.packages("palmerpenguins")
library(palmerpenguins)
在深入之前,我们将数据框重命名为dat
:
dat <- penguins
我喜欢用诸如dat
这样的通用名称来命名我正在处理的数据框,原因有二:
-
每次我需要写出数据框的名称时,通常写
dat
比写数据框的完整名称(在此情况下是penguins
)要简短。 -
如果我需要对不同的数据框进行类似的分析或绘图,我过去写的代码可以重复使用,只需进行少量修改。通过这个非常简单的技巧,大多数情况下我只需编辑变量的名称,而数据框的名称无需更改(这为我节省了大量时间)。
数据框包含 344 只企鹅的数据和 8 个变量,描述了物种、岛屿、一些喙、鳍和体重的测量、性别和研究年份。有关数据框的更多信息可以通过运行 ?penguins
(在加载 {palmerpenguins}
包后)找到。
在这篇文章中,我们将仅关注变量 species
、body_mass_g
、sex
和 year
。
在进行不同的数据处理技术之前,让我们首先通过显示数据的结构、前 6 行和总结来检查数据:
str(dat) # structure of the data
## tibble [344 × 4] (S3: tbl_df/tbl/data.frame)
## $ species : Factor w/ 3 levels "Adelie","Chinstrap",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ body_mass_g: int [1:344] 3750 3800 3250 NA 3450 3650 3625 4675 3475 4250 ...
## $ sex : Factor w/ 2 levels "female","male": 2 1 1 NA 1 2 1 2 NA NA ...
## $ year : int [1:344] 2007 2007 2007 2007 2007 2007 2007 2007 2007 2007 ...
head(dat) # display first 6 rows
## # A tibble: 6 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Adelie 3750 male 2007
## 2 Adelie 3800 female 2007
## 3 Adelie 3250 female 2007
## 4 Adelie NA <NA> 2007
## 5 Adelie 3450 female 2007
## 6 Adelie 3650 male 2007
summary(dat) # summary
## species body_mass_g sex year
## Adelie :152 Min. :2700 female:165 Min. :2007
## Chinstrap: 68 1st Qu.:3550 male :168 1st Qu.:2007
## Gentoo :124 Median :4050 NA's : 11 Median :2008
## Mean :4202 Mean :2008
## 3rd Qu.:4750 3rd Qu.:2009
## Max. :6300 Max. :2009
## NA's :2
{dplyr} 包
毫无废话,让我们在接下来的部分中说明 {dplyr}
包中可用于数据处理的不同函数。
和任何包一样,我们首先需要安装并加载它才能使用:
# install.packages("dplyr")
library(dplyr)
请注意,下面介绍的所有函数都需要整洁数据,这意味着:
-
每个变量都有自己的列,
-
每个观测值或案例都有自己的行,并且
-
每个值都有自己的单元格。
筛选观测值
可以使用 filter()
函数根据观测值进行筛选。这个函数适用于 定量 和 定性 变量:
# filter observations based on a quantitative variable
filter(dat, body_mass_g > 4000)
## # A tibble: 172 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Adelie 4675 male 2007
## 2 Adelie 4250 <NA> 2007
## 3 Adelie 4400 male 2007
## 4 Adelie 4500 male 2007
## 5 Adelie 4200 male 2007
## 6 Adelie 4150 male 2007
## 7 Adelie 4650 male 2007
## 8 Adelie 4400 male 2007
## 9 Adelie 4600 male 2007
## 10 Adelie 4150 male 2007
## # ℹ 162 more rows
# filter observations based on a qualitative variable
filter(dat, sex == "female")
## # A tibble: 165 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Adelie 3800 female 2007
## 2 Adelie 3250 female 2007
## 3 Adelie 3450 female 2007
## 4 Adelie 3625 female 2007
## 5 Adelie 3200 female 2007
## 6 Adelie 3700 female 2007
## 7 Adelie 3450 female 2007
## 8 Adelie 3325 female 2007
## 9 Adelie 3400 female 2007
## 10 Adelie 3800 female 2007
## # ℹ 155 more rows
你可以使用 &
组合多个条件(如果条件必须是累积的)或 |
(如果条件是替代的),例如:
# filter observations based on 2 cumulative conditions
filter(dat, body_mass_g > 4000 & sex == "female")
## # A tibble: 58 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Gentoo 4500 female 2007
## 2 Gentoo 4450 female 2007
## 3 Gentoo 4550 female 2007
## 4 Gentoo 4800 female 2007
## 5 Gentoo 4400 female 2007
## 6 Gentoo 4650 female 2007
## 7 Gentoo 4650 female 2007
## 8 Gentoo 4200 female 2007
## 9 Gentoo 4150 female 2007
## 10 Gentoo 4800 female 2007
## # ℹ 48 more rows
请注意,变量名不必需要写在单引号或双引号内(''
或""
)。下面介绍的所有函数都是如此。
管道操作符
从上面的代码可以看出,filter()
函数需要将数据框的名称作为第一个参数,然后是条件(使用常见的逻辑操作符 >
, <
, >=
, <=
, ==
, !=
, %in%
等)作为第二个参数。
所有列表中介绍的函数都需要将数据框的名称作为第一个参数。然而,指定数据框名称有一种变通办法:管道操作符(|>
或 %>%
)。
管道操作符允许执行一系列操作,即将一系列计算连接在一起。当你对数据框执行多个操作时特别有用,你不想在每个中间步骤保存输出。我们将在下面看到如何使用管道操作符进行多个操作,但现在我想先介绍单次操作的使用。
正如你所见,使用filter()
函数时,管道操作符不是强制性的。然而,我强烈推荐它(即使是对初学者),因为它易于使用、方便、代码可读性高且流行,从现在起,{dplyr}
中可用的函数将与管道操作符一起展示。
所以使用管道操作符,上述代码变成了:
# filter observations based on a quantitative variable
dat |>
filter(body_mass_g > 4000)
## # A tibble: 172 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Adelie 4675 male 2007
## 2 Adelie 4250 <NA> 2007
## 3 Adelie 4400 male 2007
## 4 Adelie 4500 male 2007
## 5 Adelie 4200 male 2007
## 6 Adelie 4150 male 2007
## 7 Adelie 4650 male 2007
## 8 Adelie 4400 male 2007
## 9 Adelie 4600 male 2007
## 10 Adelie 4150 male 2007
## # ℹ 162 more rows
# filter observations based on a qualitative variable
dat |>
filter(sex == "female")
## # A tibble: 165 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Adelie 3800 female 2007
## 2 Adelie 3250 female 2007
## 3 Adelie 3450 female 2007
## 4 Adelie 3625 female 2007
## 5 Adelie 3200 female 2007
## 6 Adelie 3700 female 2007
## 7 Adelie 3450 female 2007
## 8 Adelie 3325 female 2007
## 9 Adelie 3400 female 2007
## 10 Adelie 3800 female 2007
## # ℹ 155 more rows
# filter observations based on 2 cumulative conditions
dat |>
filter(body_mass_g > 4000 & sex == "female")
## # A tibble: 58 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Gentoo 4500 female 2007
## 2 Gentoo 4450 female 2007
## 3 Gentoo 4550 female 2007
## 4 Gentoo 4800 female 2007
## 5 Gentoo 4400 female 2007
## 6 Gentoo 4650 female 2007
## 7 Gentoo 4650 female 2007
## 8 Gentoo 4200 female 2007
## 9 Gentoo 4150 female 2007
## 10 Gentoo 4800 female 2007
## # ℹ 48 more rows
管道操作符只是将一个操作的结果传递到下一个操作,使代码极其易于编写和阅读。
这样一来,我们不再需要在filter()
函数(或{dplyr}
包中的任何其他函数)中将数据框的名称作为第一个参数,而是简单地指定数据框的名称,然后结合管道操作符与所需的函数。
提取观察值
可以基于以下内容提取观察值:
-
它们的位置,或
-
它们的值。
基于它们的位置
基于它们的位置提取观察值可以使用slice()
函数:
# extract rows 2, 5 and 37
dat |>
slice(c(2, 5, 37))
## # A tibble: 3 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Adelie 3800 female 2007
## 2 Adelie 3450 female 2007
## 3 Adelie 3950 male 2007
此外,提取第一行或最后一行可以使用slice_head()
和slice_tail()
:
# extract first 3 rows
dat |>
slice_head(n = 3)
## # A tibble: 3 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Adelie 3750 male 2007
## 2 Adelie 3800 female 2007
## 3 Adelie 3250 female 2007
# extract last 3 rows
dat |>
slice_tail(n = 3)
## # A tibble: 3 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Chinstrap 3775 male 2009
## 2 Chinstrap 4100 male 2009
## 3 Chinstrap 3775 female 2009
基于它们的值
要根据变量的值提取观察值,请使用:
-
slice_min()
用于选择具有最低值的行(按定义的比例),并且 -
slice_max()
用于选择具有最高值的行(按定义的比例)。
# extract observations with 25% lowest body mass
dat |>
slice_min(body_mass_g, prop = 0.25)
## # A tibble: 89 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Chinstrap 2700 female 2008
## 2 Adelie 2850 female 2008
## 3 Adelie 2850 female 2008
## 4 Adelie 2900 female 2008
## 5 Adelie 2900 female 2008
## 6 Adelie 2900 female 2009
## 7 Chinstrap 2900 female 2007
## 8 Adelie 2925 female 2009
## 9 Adelie 2975 <NA> 2007
## 10 Adelie 3000 female 2007
## # ℹ 79 more rows
# extract observations with 25% highest body mass
dat |>
slice_max(body_mass_g, prop = 0.25)
## # A tibble: 90 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Gentoo 6300 male 2007
## 2 Gentoo 6050 male 2007
## 3 Gentoo 6000 male 2008
## 4 Gentoo 6000 male 2009
## 5 Gentoo 5950 male 2008
## 6 Gentoo 5950 male 2009
## 7 Gentoo 5850 male 2007
## 8 Gentoo 5850 male 2007
## 9 Gentoo 5850 male 2009
## 10 Gentoo 5800 male 2008
## # ℹ 80 more rows
采样观察值
采样观察值可以通过两种方式完成:
-
使用
sample_n()
随机抽取数量的行 -
使用
sample_frac()
随机抽取比例的行
# random sample of 3 rows
dat |>
sample_n(size = 3)
## # A tibble: 3 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Adelie 3450 female 2007
## 2 Chinstrap 3675 female 2009
## 3 Gentoo 4500 female 2007
# random sample of half of the rows
dat |>
sample_frac(size = 1 / 2)
## # A tibble: 172 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Adelie 4150 male 2008
## 2 Gentoo 5800 male 2008
## 3 Adelie 3650 male 2009
## 4 Adelie 3500 male 2009
## 5 Adelie 3450 female 2007
## 6 Adelie 4300 male 2009
## 7 Chinstrap 3400 female 2008
## 8 Adelie 3950 male 2007
## 9 Chinstrap 3325 female 2009
## 10 Adelie 3950 male 2008
## # ℹ 162 more rows
请注意,与基础 R 中的sample()
函数一样,size
可以大于数据框的大小。在这种情况下,某些行将会重复,你需要指定参数replace = TRUE
。
另外,可以使用slice_sample()
获得随机样本的行数或行的比例。为此,请使用:
-
参数
n
用于选择行的数量,或 -
参数
prop
用于选择行的比例。
# random sample of 3 rows
dat |>
slice_sample(n = 3)
## # A tibble: 3 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Adelie 3200 female 2007
## 2 Adelie 3800 female 2007
## 3 Gentoo 4800 female 2007
# random sample of half of the rows
dat |>
slice_sample(prop = 1 / 2)
## # A tibble: 172 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Adelie 3900 male 2009
## 2 Adelie 3275 female 2009
## 3 Gentoo 5050 male 2008
## 4 Gentoo 4700 female 2009
## 5 Gentoo 4600 female 2008
## 6 Gentoo 4875 <NA> 2009
## 7 Adelie 3700 <NA> 2007
## 8 Gentoo 3950 female 2008
## 9 Gentoo 4550 female 2007
## 10 Adelie 3500 female 2008
## # ℹ 162 more rows
排序观察值
可以使用arrange()
函数对观察值进行排序:
# sort observations based on body mass (ascending order)
dat |>
arrange(body_mass_g)
## # A tibble: 344 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Chinstrap 2700 female 2008
## 2 Adelie 2850 female 2008
## 3 Adelie 2850 female 2008
## 4 Adelie 2900 female 2008
## 5 Adelie 2900 female 2008
## 6 Adelie 2900 female 2009
## 7 Chinstrap 2900 female 2007
## 8 Adelie 2925 female 2009
## 9 Adelie 2975 <NA> 2007
## 10 Adelie 3000 female 2007
## # ℹ 334 more rows
默认情况下,arrange()
使用升序。要按降序排序,请在arrange()
内部使用desc()
:
# sort observations based on body mass (descending order)
dat |>
arrange(desc(body_mass_g))
## # A tibble: 344 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Gentoo 6300 male 2007
## 2 Gentoo 6050 male 2007
## 3 Gentoo 6000 male 2008
## 4 Gentoo 6000 male 2009
## 5 Gentoo 5950 male 2008
## 6 Gentoo 5950 male 2009
## 7 Gentoo 5850 male 2007
## 8 Gentoo 5850 male 2007
## 9 Gentoo 5850 male 2009
## 10 Gentoo 5800 male 2008
## # ℹ 334 more rows
与filter()
一样,arrange()
可以用于多个变量,并且对定量和定性变量都有效:
# sort observations based on two variables
dat |>
arrange(sex, body_mass_g)
## # A tibble: 344 × 4
## species body_mass_g sex year
## <fct> <int> <fct> <int>
## 1 Chinstrap 2700 female 2008
## 2 Adelie 2850 female 2008
## 3 Adelie 2850 female 2008
## 4 Adelie 2900 female 2008
## 5 Adelie 2900 female 2008
## 6 Adelie 2900 female 2009
## 7 Chinstrap 2900 female 2007
## 8 Adelie 2925 female 2009
## 9 Adelie 3000 female 2007
## 10 Adelie 3000 female 2009
## # ℹ 334 more rows
上述代码首先根据性别(按字母顺序)排序观察值,然后根据体重(按升序,从最低到最高)排序。
请注意,如果定性变量被定义为有序的因子,则排序是基于级别顺序,而不是字母顺序!
选择变量
选择变量可以使用select()
函数,基于:
-
变量的位置,或
-
变量的名称。
# select variables by their positions
dat |>
select(c(2, 4))
## # A tibble: 344 × 2
## body_mass_g year
## <int> <int>
## 1 3750 2007
## 2 3800 2007
## 3 3250 2007
## 4 NA 2007
## 5 3450 2007
## 6 3650 2007
## 7 3625 2007
## 8 4675 2007
## 9 3475 2007
## 10 4250 2007
## # ℹ 334 more rows
# select variables by their names
dat |>
select(body_mass_g, year)
## # A tibble: 344 × 2
## body_mass_g year
## <int> <int>
## 1 3750 2007
## 2 3800 2007
## 3 3250 2007
## 4 NA 2007
## 5 3450 2007
## 6 3650 2007
## 7 3625 2007
## 8 4675 2007
## 9 3475 2007
## 10 4250 2007
## # ℹ 334 more rows
请注意,也可以删除变量。为此,请在其位置或名称前使用-
符号:
# remove variables by their positions
dat |>
select(-c(2, 4))
## # A tibble: 344 × 2
## species sex
## <fct> <fct>
## 1 Adelie male
## 2 Adelie female
## 3 Adelie female
## 4 Adelie <NA>
## 5 Adelie female
## 6 Adelie male
## 7 Adelie female
## 8 Adelie male
## 9 Adelie <NA>
## 10 Adelie <NA>
## # ℹ 334 more rows
# remove variables by their names
dat |>
select(-c(body_mass_g, year))
## # A tibble: 344 × 2
## species sex
## <fct> <fct>
## 1 Adelie male
## 2 Adelie female
## 3 Adelie female
## 4 Adelie <NA>
## 5 Adelie female
## 6 Adelie male
## 7 Adelie female
## 8 Adelie male
## 9 Adelie <NA>
## 10 Adelie <NA>
## # ℹ 334 more rows
也可以使用一系列名称选择变量:
# select all variables from species to sex
dat |>
select(species:sex)
## # A tibble: 344 × 3
## species body_mass_g sex
## <fct> <int> <fct>
## 1 Adelie 3750 male
## 2 Adelie 3800 female
## 3 Adelie 3250 female
## 4 Adelie NA <NA>
## 5 Adelie 3450 female
## 6 Adelie 3650 male
## 7 Adelie 3625 female
## 8 Adelie 4675 male
## 9 Adelie 3475 <NA>
## 10 Adelie 4250 <NA>
## # ℹ 334 more rows
最后但同样重要的是,select()
也可以作为一种简便的方法来重新排列列的顺序:
# put sex as first column, then all the others
dat |>
select(sex, species:year)
## # A tibble: 344 × 4
## sex species body_mass_g year
## <fct> <fct> <int> <int>
## 1 male Adelie 3750 2007
## 2 female Adelie 3800 2007
## 3 female Adelie 3250 2007
## 4 <NA> Adelie NA 2007
## 5 female Adelie 3450 2007
## 6 male Adelie 3650 2007
## 7 female Adelie 3625 2007
## 8 male Adelie 4675 2007
## 9 <NA> Adelie 3475 2007
## 10 <NA> Adelie 4250 2007
## # ℹ 334 more rows
重命名变量
要重命名变量,请使用rename()
函数:
# rename variables
dat |>
rename(
body_mass = body_mass_g, # rename body_mass_g into body_mass
study_year = year # rename year into study_year
)
## # A tibble: 344 × 4
## species body_mass sex study_year
## <fct> <int> <fct> <int>
## 1 Adelie 3750 male 2007
## 2 Adelie 3800 female 2007
## 3 Adelie 3250 female 2007
## 4 Adelie NA <NA> 2007
## 5 Adelie 3450 female 2007
## 6 Adelie 3650 male 2007
## 7 Adelie 3625 female 2007
## 8 Adelie 4675 male 2007
## 9 Adelie 3475 <NA> 2007
## 10 Adelie 4250 <NA> 2007
## # ℹ 334 more rows
这可能不是直观的(至少在我学习这个包时不是),所以请记住,您总是需要先写新名称,然后再写旧名称(用=
符号分隔)。
创建或修改变量
您可以使用mutate()
创建或修改数据框中的某些变量,基于:
-
另一个变量,或
-
选择的向量。
# create a new variable based on an existing one
dat |>
mutate(
body_mass_kg = body_mass_g / 1000
)
## # A tibble: 344 × 5
## species body_mass_g sex year body_mass_kg
## <fct> <int> <fct> <int> <dbl>
## 1 Adelie 3750 male 2007 3.75
## 2 Adelie 3800 female 2007 3.8
## 3 Adelie 3250 female 2007 3.25
## 4 Adelie NA <NA> 2007 NA
## 5 Adelie 3450 female 2007 3.45
## 6 Adelie 3650 male 2007 3.65
## 7 Adelie 3625 female 2007 3.62
## 8 Adelie 4675 male 2007 4.68
## 9 Adelie 3475 <NA> 2007 3.48
## 10 Adelie 4250 <NA> 2007 4.25
## # ℹ 334 more rows
# create a new variable from a vector of your choice
dat |>
mutate(
ID = 1:nrow(dat)
)
## # A tibble: 344 × 5
## species body_mass_g sex year ID
## <fct> <int> <fct> <int> <int>
## 1 Adelie 3750 male 2007 1
## 2 Adelie 3800 female 2007 2
## 3 Adelie 3250 female 2007 3
## 4 Adelie NA <NA> 2007 4
## 5 Adelie 3450 female 2007 5
## 6 Adelie 3650 male 2007 6
## 7 Adelie 3625 female 2007 7
## 8 Adelie 4675 male 2007 8
## 9 Adelie 3475 <NA> 2007 9
## 10 Adelie 4250 <NA> 2007 10
## # ℹ 334 more rows
请注意,如果您创建了一个已在数据框中存在的名称的变量,旧变量将被新变量替换。
与rename()
类似,mutate()
要求参数写成name = expression
的形式,其中name
是创建或修改的列的名称,expression
是计算值的公式。
总结观察
通常,您会想要用一些描述性统计量来总结数据。这可以通过summarize()
函数完成,此外还有大多数用于描述性统计量的函数(mean()
,median()
,min()
,max()
,sd()
,var()
等):
# compute mean and sd of body mass
dat |>
summarize(
body_mass_mean = mean(body_mass_g, na.rm = TRUE),
body_mass_sd = sd(body_mass_g, na.rm = TRUE)
)
## # A tibble: 1 × 2
## body_mass_mean body_mass_sd
## <dbl> <dbl>
## 1 4202\. 802.
请注意:
-
na.rm = TRUE
参数用于在计算总结统计量时忽略缺失值。 -
summarize()
和summarise()
给出完全相同的结果。
识别不同的值
可以使用distinct()
来识别变量的不同值:
# find the distinct species
dat |>
distinct(species)
## # A tibble: 3 × 1
## species
## <fct>
## 1 Adelie
## 2 Gentoo
## 3 Chinstrap
识别不同的值主要在定性或定量离散变量上进行,但它可以在任何类型的变量以及多个变量上同时进行。如果指定了多个变量,它会返回变量值的所有组合。
例如,按物种和研究年份:
# combination of distinct species and year
dat |>
distinct(species, year)
## # A tibble: 9 × 2
## species year
## <fct> <int>
## 1 Adelie 2007
## 2 Adelie 2008
## 3 Adelie 2009
## 4 Gentoo 2007
## 5 Gentoo 2008
## 6 Gentoo 2009
## 7 Chinstrap 2007
## 8 Chinstrap 2008
## 9 Chinstrap 2009
连接操作
使用{dplyr}
包的另一个优点是可以一次连接多个操作,使代码的可读性大大提高。这可以轻松通过之前介绍的管道操作符(|>
或%>%
)来实现。
到目前为止,我们一直看到相同的结构:我们调用一个数据框,然后对该数据框应用一个操作。从现在开始,我们将看到如何将更多操作组合成一个单一的操作链。
按组
group_by()
允许修改基本函数的执行方式。操作将覆盖由分组命令定义的每个行组,而不是数据框的所有行。通过这种方式,使用summarize()
的聚合操作将为每个组生成统计数据,而不是所有观察值。
例如,我们可能对计算定量变量在每个定性变量水平(即按组)上的一些描述性统计量感兴趣。
在我们的例子中,假设我们想分别计算每种物种的体重均值和标准差:
# compute mean and sd of body mass by species
dat |>
group_by(species) |> # group by species
summarize(
mean = mean(body_mass_g, na.rm = TRUE), # compute mean
sd = sd(body_mass_g, na.rm = TRUE) # compute sd
)
## # A tibble: 3 × 3
## species mean sd
## <fct> <dbl> <dbl>
## 1 Adelie 3701\. 459.
## 2 Chinstrap 3733\. 384.
## 3 Gentoo 5076\. 504.
应用到上述示例,管道操作符的工作方式如下:
-
第一个操作(
group_by()
)按物种分组观察值。 -
然后,第一个操作的输出被用作第二个操作(
summarize()
)的输入:计算体重的均值和标准差。
结果是按组计算的体重均值和标准差。正如你所想,所有先前的操作都可以连接以满足你的需求。
同时注意,group_by()
可以用于多个分组变量:
# compute mean and sd of body mass by species and sex
dat |>
group_by(species, sex) |> # group by species and sex
summarize(
mean = mean(body_mass_g, na.rm = TRUE), # compute mean
sd = sd(body_mass_g, na.rm = TRUE) # compute sd
)
## # A tibble: 8 × 4
## # Groups: species [3]
## species sex mean sd
## <fct> <fct> <dbl> <dbl>
## 1 Adelie female 3369\. 269.
## 2 Adelie male 4043\. 347.
## 3 Adelie <NA> 3540 477.
## 4 Chinstrap female 3527\. 285.
## 5 Chinstrap male 3939\. 362.
## 6 Gentoo female 4680\. 282.
## 7 Gentoo male 5485\. 313.
## 8 Gentoo <NA> 4588\. 338.
(注意,对于所有连接操作,数据框的名称只需在第一次操作中指定。)
观察值的数量
一些操作只能在其他操作内部执行。
这是观察值 n()
的情况,这只能在 summarize()
内部使用:
# number of observations
dat |>
summarize(n_obs = n())
## # A tibble: 1 × 1
## n_obs
## <int>
## 1 344
为了参考之前的连接操作 group_by()
,我们可以使用管道操作符分隔的两个操作 n()
和 group_by()
来计算每组的观察值数量:
# number of observations by species
dat |>
group_by(species) |>
summarize(n_obs = n())
## # A tibble: 3 × 2
## species n_obs
## <fct> <int>
## 1 Adelie 152
## 2 Chinstrap 68
## 3 Gentoo 124
注意 n()
不接受任何参数,因此总是写为空括号。
此外,请注意,count()
函数等同于 summarize(n = n())
:
# number of observations
dat |>
count()
## # A tibble: 1 × 1
## n
## <int>
## 1 344
# number of observations by species
dat |>
count(species)
## # A tibble: 3 × 2
## species n
## <fct> <int>
## 1 Adelie 152
## 2 Chinstrap 68
## 3 Gentoo 124
不同值的数量
n_distinct()
,它也只能在 summarize()
内部使用,计算变量或变量组合的不同值/级别的数量:
# number of distinct species
dat |>
summarize(n_species = n_distinct(species))
## # A tibble: 1 × 1
## n_species
## <int>
## 1 3
# number of distinct species and year of study
dat |>
summarize(n_species_year = n_distinct(species, year))
## # A tibble: 1 × 1
## n_species_year
## <int>
## 1 9
注意,你不必为输出指定名称。在这种情况下,将使用操作的名称。例如:
# number of distinct species
dat |>
summarize(n_distinct(species))
## # A tibble: 1 × 1
## `n_distinct(species)`
## <int>
## 1 3
首个、最后一个或第 n 个值
也仅在 summarize()
内部可用,第一个、最后一个或第 n 个值可以通过以下命令找到:
# first value of the variable sex
dat |>
summarize(first(sex))
## # A tibble: 1 × 1
## `first(sex)`
## <fct>
## 1 male
# last value of the variable sex
dat |>
summarize(last(sex))
## # A tibble: 1 × 1
## `last(sex)`
## <fct>
## 1 female
# 37th value of the variable sex
dat |>
summarize(nth(sex, n = 37))
## # A tibble: 1 × 1
## `nth(sex, n = 37)`
## <fct>
## 1 male
在这个函数中存在一些处理缺失值的有趣参数。感兴趣的读者可以在函数文档中查看更多信息(运行 ?nth()
)。
如果 else
一种非常常见的数据转换是众所周知的“if else”技术。这种技术通常用于从现有变量创建另一个可以取 两个级别 的变量。
假设我们想创建一个名为 body_mass_cat
的新变量,当 body_mass_g
等于或大于某个阈值时取值“High”,否则取“Low”。这种转换可以通过 mutate()
和 if_else()
的组合来完成:
# if else
dat |>
mutate(
body_mass_cat = if_else(body_mass_g >= 4000, # condition
"High", # output if condition is true
"Low" # output if condition is false
)
)
## # A tibble: 344 × 5
## species body_mass_g sex year body_mass_cat
## <fct> <int> <fct> <int> <chr>
## 1 Adelie 3750 male 2007 Low
## 2 Adelie 3800 female 2007 Low
## 3 Adelie 3250 female 2007 Low
## 4 Adelie NA <NA> 2007 <NA>
## 5 Adelie 3450 female 2007 Low
## 6 Adelie 3650 male 2007 Low
## 7 Adelie 3625 female 2007 Low
## 8 Adelie 4675 male 2007 High
## 9 Adelie 3475 <NA> 2007 Low
## 10 Adelie 4250 <NA> 2007 High
## # ℹ 334 more rows
if_else()
函数接受 3 个参数:
-
条件(在我们的例子中:
body_mass_g >= 4000
) -
当条件为真时的输出值(在我们的例子中是
High
)。 -
当条件为假时的输出值(在我们的例子中是
Low
)。
如上表所示,当体重缺失时,if_else()
也会返回缺失值,这通常是防止观察值被错误分类的好方法。
情况
如果你想将变量分类为 两个以上的级别,if_else
不是最合适的工具。在这种情况下,“情况”更为合适。
供您参考,当我学习 R 时,我曾经编写过嵌套的 if else 函数,即在主要的 if else 中嵌入二级的 if else。大多数时候它有效(虽然经常需要花费大量时间调试代码),但很容易出错。即使你成功让它工作,代码也很难阅读!
所以我强烈建议使用 case when 技术,而不是多个相互嵌套的 if else 函数。
假设我们想将体重分为 3 类:低、中和高。为了说明,我们随意决定体重低于 3500 时为低,超过 4750 时为高,其余为中。
使用嵌套的 if else 函数,我们需要编写如下代码:
# nested if else
dat |>
mutate(
body_mass_cat = if_else(body_mass_g < 3500, # first condition
"Low", # output if first condition is true
if_else(body_mass_g > 4750, # second condition when first condition is false
"High", # output when second condition is true
"Medium" # output when second condition is false
)
)
)
## # A tibble: 344 × 5
## species body_mass_g sex year body_mass_cat
## <fct> <int> <fct> <int> <chr>
## 1 Adelie 3750 male 2007 Medium
## 2 Adelie 3800 female 2007 Medium
## 3 Adelie 3250 female 2007 Low
## 4 Adelie NA <NA> 2007 <NA>
## 5 Adelie 3450 female 2007 Low
## 6 Adelie 3650 male 2007 Medium
## 7 Adelie 3625 female 2007 Medium
## 8 Adelie 4675 male 2007 Medium
## 9 Adelie 3475 <NA> 2007 Low
## 10 Adelie 4250 <NA> 2007 Medium
## # ℹ 334 more rows
这段代码的工作原理如下:
-
它评估第一个条件
body_mass_g < 3500
。 -
如果是真的,
body_mass_cat
是Low
。相反,如果为假,它将评估第二个条件body_mass_g > 4750
。 -
如果第二个条件为真,
body_mass_cat
是High
,否则是Medium
。
从上述结果可以看出,它有效。然而,你会承认容易出现编码错误,而且代码既难以编写也难以阅读。
为了改进这个工作流程,我们现在使用 case when 技术:
# case when, without a default option
dat |>
mutate(
body_mass_cat = case_when(
body_mass_g < 3500 ~ "Low",
body_mass_g >= 3500 & body_mass_g <= 4750 ~ "Medium",
body_mass_g > 4750 ~ "High"
)
)
## # A tibble: 344 × 5
## species body_mass_g sex year body_mass_cat
## <fct> <int> <fct> <int> <chr>
## 1 Adelie 3750 male 2007 Medium
## 2 Adelie 3800 female 2007 Medium
## 3 Adelie 3250 female 2007 Low
## 4 Adelie NA <NA> 2007 <NA>
## 5 Adelie 3450 female 2007 Low
## 6 Adelie 3650 male 2007 Medium
## 7 Adelie 3625 female 2007 Medium
## 8 Adelie 4675 male 2007 Medium
## 9 Adelie 3475 <NA> 2007 Low
## 10 Adelie 4250 <NA> 2007 Medium
## # ℹ 334 more rows
这个工作流程更易于编码和阅读!
如果用于条件的变量中没有缺失值,它甚至可以简化为:
# case when, with a default option
dat |>
mutate(
body_mass_cat = case_when(
body_mass_g < 3500 ~ "Low",
body_mass_g > 4750 ~ "High",
.default = "Medium" # default output
)
)
## # A tibble: 344 × 5
## species body_mass_g sex year body_mass_cat
## <fct> <int> <fct> <int> <chr>
## 1 Adelie 3750 male 2007 Medium
## 2 Adelie 3800 female 2007 Medium
## 3 Adelie 3250 female 2007 Low
## 4 Adelie NA <NA> 2007 Medium
## 5 Adelie 3450 female 2007 Low
## 6 Adelie 3650 male 2007 Medium
## 7 Adelie 3625 female 2007 Medium
## 8 Adelie 4675 male 2007 Medium
## 9 Adelie 3475 <NA> 2007 Low
## 10 Adelie 4250 <NA> 2007 Medium
## # ℹ 334 more rows
正如你所见,对于不符合任何条件的观察,可以通过 .default
指定一个默认输出。
但是,如果存在缺失值,请小心!确实,如果至少有一个缺失值(如我们的案例),上述代码是不正确的,因为缺失 body_mass_g
的观察将被误分类为 Medium
。
因此,如果你真的想指定一个默认输出,我建议使用下面的代码,它将缺失值保留为 NA
:
# case when, with a default option and missing values
dat |>
mutate(
body_mass_cat = case_when(
body_mass_g < 3500 ~ "Low",
body_mass_g > 4750 ~ "High",
is.na(body_mass_g) ~ NA, # keep missing values as NA
.default = "Medium" # default output
)
)
## # A tibble: 344 × 5
## species body_mass_g sex year body_mass_cat
## <fct> <int> <fct> <int> <chr>
## 1 Adelie 3750 male 2007 Medium
## 2 Adelie 3800 female 2007 Medium
## 3 Adelie 3250 female 2007 Low
## 4 Adelie NA <NA> 2007 <NA>
## 5 Adelie 3450 female 2007 Low
## 6 Adelie 3650 male 2007 Medium
## 7 Adelie 3625 female 2007 Medium
## 8 Adelie 4675 male 2007 Medium
## 9 Adelie 3475 <NA> 2007 Low
## 10 Adelie 4250 <NA> 2007 Medium
## # ℹ 334 more rows
我个人更喜欢编写所有类别,而不写默认选项,以提高代码的可读性和健壮性,但这更是一种个人观点。
在所有情况下,无论你使用的是 if else 还是 case when,检查你刚创建的变量以确保获得预期结果是一种良好实践。
结论和其他资源
谢谢阅读。
本文介绍并说明了在 R 中最常见的数据操作和数据管理工具,使用了 {dplyr}
包。我们还介绍了管道操作符,现代 R 包的用户非常熟悉。
{dplyr}
包提供了更多的数据操作函数(特别是使用 join()
函数系列合并数据框)。如果你想了解更多关于这个包的内容,我推荐从以下资源开始:
-
对于习惯于基础 R 的用户,说明书 比较了
{dplyr}
函数与其基础 R 等效函数
一如既往,如果你对本文涉及的主题有任何问题或建议,请将其作为评论添加,以便其他读者也能从讨论中受益。
参考文献
Horst, Allison Marie, Alison Presmanes Hill, 和 Kristen B Gorman. 2020. Palmerpenguins: Palmer Archipelago (Antarctica) Penguin Data. doi.org/10.5281/zenodo.3960218
.
- 管道操作符的键盘快捷键是
ctrl + shift + m
(Windows)或cmd + shift + m
(Mac)。它将打印%>%
,除非你在 RStudio 的设置中指定使用原生管道操作符|>
。↩︎
相关文章
最初发表于 https://statsandr.com 于 2023 年 11 月 27 日。
数据版本控制简介
原文:
towardsdatascience.com/introduction-to-data-version-control-59fabf447a60
PYTHON | DATA | PROGRAMMING
使用 Hangar 在 Python 中实现自己的 DVC 的逐步指南
·发布于Towards Data Science ·阅读时间 7 分钟·2023 年 8 月 18 日
--
图片由Florian Olivo拍摄,来源于Unsplash
什么是数据版本控制(DVC)?
任何生产级系统都需要某种形式的版本控制。
一个当前真实来源的单一来源。
任何持续更新的资源,尤其是被多个用户同时更新的资源,都需要某种审计跟踪以记录所有更改。
在软件工程中,解决方案是Git。
如果你曾经编写过代码,那么你可能对 Git 的美妙之处非常熟悉。
Git 允许我们提交更改,从源创建不同的分支,并将我们的分支合并回原始分支,仅举几例。
DVC 完全是相同的范式,但适用于数据集。请看,实时数据系统不断吸收新的数据点,而不同的用户在相同的数据集上进行不同的实验。
这导致了同一数据集的多个版本,这显然不是单一的真实来源。
此外,在机器学习环境中,我们还会有多个版本的同一“模型”,这些模型是在相同数据集的不同版本上训练的(例如,重新训练模型以包括更新的数据点)。
如果没有适当的审计和版本控制,这将会创建一个复杂的数据集和实验网络。我们绝对不想要这种情况!
因此,DVC 是一个涉及通过注册特定数据集的更改来跟踪我们数据集的系统。既有免费的 DVC 解决方案,也有付费的解决方案。
我最近发现了Hangar,这是一个完全开源的 Python DVC 包。我们来看看它能做些什么吧。
使用 Hangar
hangar 包是一个纯 Python 实现,可以通过 pip 安装。
它的核心功能也与 git 紧密相关,这极大地帮助了学习曲线的提升。
我们还可以选择通过命令行与 hangar 进行交互,或使用其专用的 Python 客户端。
可用的一些功能包括:
-
checkout — 切换到新的分支或参考点
-
commit — 将当前更改添加到当前分支
-
branch — 创建一个指向特定提交的参考点
-
merge — 将一个分支的更改合并到另一个分支中
-
diff — 比较两个分支之间的更改
-
push — 将本地版本上传到远程仓库
-
pull/fetch — 从远程仓库更新数据集的本地版本
-
log — 显示提交历史
注意:远程仓库是当前真实数据的唯一来源。
需要注意的是,Hangar 不是基于 git 构建的,而是模拟 git 的功能。这使得它运行更快。
我们可以通过 pip 使用以下命令安装 hangar:
pip install hangar
安装 Hangar 后,我们可以直接将该包导入 Python。
我们需要做的第一件事是创建一个数据仓库。
我们可以从 Hangar 包中导入 Repository 类并用它来定义我们的仓库。
如果这是我们第一次处理某个仓库,我们还需要使用 init()
函数来初始化它。
from hangar import Repository
import os
repo_name = 'test'
if not os.path.isdir(repo_name):
print(f'{repo_name} directory was not found. Creating an empty directory.')
os.makedirs(repo_name)
repo = Repository(path=repo_name)
print(f'Connected to {repo_name}')
if not repo.initialized:
print(f'Initialising {repo_name}')
repo.init(
user_name="David Farrugia", user_email="davidfarrugia@gmail.com", remove_old=True
)
在我们继续数据版本控制的示例之前,让我们先讨论一下 Hangar 背后的方法论。
接触 Hangar
Hangar 的主要学习曲线在于理解如何与这个包进行最佳交互。Hangar 包含四个主要组件:
仓库
我们可以将仓库视为我们的项目仓库。仓库本质上是对所做提交的集合和历史记录。
理想情况下,每个项目都有自己的仓库。
例如,如果我们有两个主要任务——预测手写数字和预测欺诈——我们也会分别创建两个仓库。
数据集
这一个很简单。
数据集就是,你猜对了,就是我们的数据集。但是数据集究竟是什么呢?我们以 Titanic 数据集为例。数据集的组成是什么?
是指单个样本吗?是指监控的变量吗?在这里我们可以充分发挥创意。Hangar 将数据集描述为一个列的集合。我们接下来会深入探讨这个问题。
列
列可以是我们喜欢的任何数据属性或特征。它可以是特征数组、标签数组、特征名称数组,甚至是唯一标识符数组。
然而,列数组中的每个项目都应该对应于数据集中的单个样本。
目前,支持的列类型有:
-
add_bytes_column — 字节
-
add_ndarray_column — 数组
-
add_str_column — 字符串
例如,如果我们有一个 28x28 图像的数据集,我们会选择一个数组列(每个样本的形状为 28x28)来表示实际的数值数据。
我们可以使用字节或字符串列来存储其标签,并使用字符串列来存储图像文件名。
图片由作者提供。
当然,上述只是关于如何构建数据集的指南。
你所处理的数据类型以及要进行的实验类型都会影响 Hangar 的结构化策略。
例如,你也可以选择为训练数据和验证数据各自设立一个专门的列。
列应该是数据样本的集合。我们从一个空集合开始,样本添加后,集合索引会增加。
数据
最后是数据。一旦我们确定了需要哪些列,相应地处理数据就变成了一项相对简单的任务。
数据本身只是数字。它没有直接的意义,也没有任何结构。
完成这些后,让我们继续我们的示例。
假设我们有一个表格分类数据集——df
——我们会将整个数据集作为字节存储在一个单独的列中。
我们开始时创建一个WriterCheckout。
WriterCheckout 对象允许我们启用一个特定的分支(在我们的例子中,我们只有一个分支:master),并具有写入访问权限(即能够写入和提交更改到活动分支)。
我们通过master = repo.checkout(write=True)
来完成这一步。
然后,我们可以指示 Hangar 创建一个名为‘data’的字节列,通过调用add_bytes_column
。由于这是我们的第一次提交,所以我们的列仍然是空的。
对于我们的第一次提交,我们可以在索引 0 处提交数据。
由于我们将列指定为字节对象,因此我们必须先将数据转换为字节对象。
我们可以最终调用commit
函数来提交并保存我们的更改。下面,我们展示了刚刚讨论的代码示例。
import pickle
# get the WriterCheckout
master = repo.checkout(write=True)
# Add a New Column
master.add_bytes_column(name='data')
# Add the Data
master['data'][0] = pickle.dumps(df, protocol=4)
# Commit
master.commit('This is our first commit!')
# Close the WriterCheckout Lock
master.close()
注意:Hangar 不允许有多个 WriterCheckouts 以避免冲突。因此,当不使用 WriterCheckout 时,请确保关闭它。如果写锁已经在流通中,我们只能以只读模式进行 checkout。
如果我们想对同一列添加另一个提交,我们按照相同的过程进行,但改为提交到master['data'][1]
,以此类推进行未来的提交。
每个提交也会有一个绑定的哈希键。
在 Hangar 中分支
分支在我们想要获取某个特定点的数据副本以进行自定义实验而不实际更改数据时特别有用。
我们可以分支出去,并在确认我们的处理正确后,也可以将其合并回主流。
典型的分支流程大致如下:
Create Branch -> Checkout Branch -> Make Changes -> Commit -> Merge
我们可以使用repo.create_branch(name='test')
创建分支,并按如下方式进行合并:
master.merge(message='message for merge', dev_branch='test')
每个 Hangar 中的提交都有一个哈希键。我们可以使用这个哈希来准确确定分支点:
test_branch2 = repo.create_branch(name='test2', base_commit=<SOME_HASH_KEY>)
通过调用repo.log()
,我们可以获得当前分支及其最新提交的日志摘要。一个示例日志可能如下所示:
* a=cf94cf8b4c5758c885c6b84d58c4fbe22f379510 (test2): added new test branch
* a=a8fe61916764b873f13c80a14ce4fda610b74df9 (test) (master): Base Dataset
我们可以按如下方式获取分支之间的差异和冲突:
repo.diff('master', 'test2')
总结
在这篇文章中,我们讨论了 Hangar 包作为 Python 中 DVC 的开源解决方案。这就是 Hangar 提供的全部吗?绝对不是!我们介绍了基础知识,并发现如何开始使用 Hangar。正如以往,我强烈建议你查看他们的文档并在自己的用例中实践。
你喜欢这篇文章吗?只需$5/月,你就可以成为会员,解锁对 Medium 的无限访问。你将直接支持我以及你在 Medium 上最喜欢的其他作者。非常感谢!
[## 通过我的推荐链接加入 Medium - David Farrugia
作为 Medium 会员,你的部分会员费用会流向你阅读的作者,并且你可以全面访问每个故事……
想联系我吗?
我很想听听你对这个话题,或任何关于 AI 和数据的看法。
如果你希望联系我,可以发邮件至davidfarrugia53@gmail.com。
嵌入式推荐系统介绍
原文:
towardsdatascience.com/introduction-to-embedding-based-recommender-systems-956faceb1919
推荐系统
学习在 TensorFlow 中构建一个简单的矩阵分解推荐系统
·发表于 Towards Data Science ·阅读时间 13 分钟·2023 年 1 月 25 日
--
图片由 Johannes Plenio 提供,来源于 Unsplash
这些推荐在像亚马逊、Netflix 或 Spotify 这样的大型网站上随处可见,有时非常出色,有时却很糟糕,甚至有时很搞笑,告诉你下一步买什么、看什么或听什么。虽然推荐系统对我们用户来说很方便——我们可以受到启发去尝试新事物——但公司特别受益于它们。
要了解具体程度,我们可以查看 Dietmar Jannach 和 Michael Jugovac 在论文中给出的一些数据,测量推荐系统的商业价值 [1]。从他们的论文中:
-
Netflix: “75% 的观看内容来源于某种推荐”(这个甚至来自 Medium!)
-
YouTube: “60% 的主页点击来自推荐”
-
亚马逊: “约 35%的销售来源于交叉销售(即推荐)”,其中their指的是亚马逊
在这篇论文 [1] 中,你可以找到更多关于提高点击率、参与度和销售的有趣声明,这些都来源于使用推荐系统。
所以,看起来推荐系统就像是面包切片以来最伟大的发明一样,我也同意推荐系统是机器学习领域中出现的最优秀、最有趣的事物之一。这就是为什么在本文中,我想向你展示
-
如何设计一个简单的协作推荐系统(矩阵分解)
-
如何在 TensorFlow 中实现它
-
优缺点是什么。
你可以在 我的 Github 上找到代码。
在我们开始之前,让我们获取一些可以操作的数据。
获取数据
如果你还没有,请通过 pip install tensorflow-datasets
获取 tensorflow_datasets。你可以下载他们提供的任何数据集,但我们将坚持使用真正经典的 movielens!我们选择 movielens 数据中最小的版本,包含 1,000,000 行,这样后续训练会更快。
import tensorflow_datasets as tfds
data = tfds.load("movielens/1m-ratings")
data
是一个包含 TensorFlow 数据集的字典,这些数据集非常棒。但为了简化,我们将其转换为 pandas 数据框,这样大家就能保持一致。
注意: 通常,你会将其保留为 TensorFlow 数据集,尤其是当数据量变得更大时,因为 pandas 对 RAM 的需求非常高。不要尝试将 movielens 数据集的 25,000,000 版本转换为 pandas 数据框!
df = tfds.as_dataframe(data["train"])
print(df.head(5))
图片来自作者。
⚠️ 警告: 不要打印整个数据框,因为这是一个样式化的数据框,默认情况下会显示所有 1,000,000 行!
我们可以看到大量的数据。每一行包括
-
用户 (user_id),
-
一个电影 (movie_id),
-
用户对电影的评分 (user_rating),以 1 到 5 的整数(星级)表示,并且
-
关于用户和电影的更多特征。
在本教程中,让我们只使用最基本的:user_id、movie_id* 和 user_rating,因为这通常是我们唯一拥有的数据。拥有更多关于用户和电影的特征通常是一种奢侈,所以让我们直接处理更难但广泛适用的情况。*
基于这种交互数据训练的推荐系统称为* 协同过滤 — 一个模型在许多用户的交互数据上进行训练,以便为单个用户提供推荐。 一人为大家,大家为一人!
我们还会保留时间戳以进行时间上的训练-测试拆分,因为这类似于我们在现实生活中的训练方式:我们现在训练,但希望模型明天表现良好。所以我们也应该以这种方式评估模型质量。
filtered_data = (
df
.filter(["timestamp", "user_id", "movie_id", "user_rating"])
.sort_values("timestamp")
.astype({"user_id": int, "movie_id": int, "user_rating": int}) # nicer types
.drop(columns=["timestamp"]) # don't need the timestamp anymore
)
train = filtered_data.iloc[:900000] # chronologically first 90% of the dataset
test = filtered_data.iloc[900000:] # chronologically last 10% of the dataset
filtered_data
包含
图片来自作者。
冷启动问题
如果我们以任何方式拆分数据,我们可能会遇到所谓的冷启动问题,这意味着某些用户或电影只出现在测试集中,而不在训练集中。在我们的例子中,有趣的是,用户 1 就是这样一个例子。
print(train.query("user_id == 1").shape[0])
print(test.query("user_id == 1").shape[0])
# Output:
# 0
# 53
有点像仅出现在测试集中的分类特征类别。这使得学习更困难,但模型仍然必须以某种方式处理它。我们将要构建的推荐系统很容易受到冷启动问题的影响,但还有其他类型的推荐系统可以更好地处理新用户或电影。这是另一个文章的内容。
让我们构建训练和测试数据框并继续。
X_train = train.drop(columns=["user_rating"])
y_train = train["user_rating"]
X_test = test.drop(columns=["user_rating"])
y_test = test["user_rating"]
嵌入速成课程
现在我们知道数据的样子了,让我们定义模型的签名,即输入和输出是什么。在我们的例子中,这非常简单:输入应该是user_id和movie_id,输出应该是user_rating,即用户对电影的评分。
图片由作者提供。
这样的模型可能是什么样的呢?这很难,尤其是对于数据科学初学者。用户和电影是类别,即使我们将它们编码为整数。因此,将它们视为数字并仅用它们训练模型是不够有意义的。
糟糕的事情!
对于好奇的读者,我还是会做的。以下是一个不要这样做的示例:
# BAD BAD BAD BAD BAD BAD BAD BAD BAD BAD BAD BAD BAD BAD BAD
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.metrics import mean_absolute_error
hgb = HistGradientBoostingRegressor(random_state=0)
hgb.fit(X_train, y_train)
print(hgb.score(X_test, y_test), mean_absolute_error(y_test, hgb.predict(X_test)))
# Output:
# 0.07018701410615702 0.8508620798953698
# BAD BAD BAD BAD BAD BAD BAD BAD BAD BAD BAD BAD BAD BAD BAD
r²约为 0.07,这和一个仅输出评分均值的回归器差不多,不管用户和电影的输入如何。平均绝对误差约为 0.85,这意味着我们平均错过了真实评分约 0.85 星。
我将展示如何使用嵌入构建一个更有意义和更好的模型,而不是这样做。
一热编码作为嵌入的特例
编码诸如用户或电影这样的类别变量的一种方式是使用向量,即一组数字——在这个上下文中称为嵌入。这是一种有用的技术,不仅仅是推荐系统,任何涉及类别数据的情况都可以使用。
图片由作者提供。
将类别转换为数字的一个非常简单的例子是一热/虚拟编码。然而,对于高卡特性类别特征,结果嵌入是高维的,这使得我们在尝试使用它们时陷入了维度诅咒的陷阱。
另一个缺点是每对两个向量之间的距离是相同的。例如,如果你取一个具有三个类别的特征,编码为 [1, 0, 0],[0, 1, 0] 和 [0, 0, 1],每个类别与其他类别在常见的度量标准(如欧几里得和其他 闵可夫斯基距离)中具有相同的距离。这对于名义特征可能没问题,但对于序数特征如热、温暖或冷天气,如果热离温暖比冷更近会更好。
显然,这是不好的,因此我们必须考虑其他方案。
实际情况
嵌入允许我们创建比一热编码向量更有意义的更短的向量。
在深度学习框架如 TensorFlow 和 PyTorch 中,它们随时可用。总体上,它们的工作方式如下:
-
你指定一个嵌入维度,即向量的长度。这是一个你可以调整的超参数之一。
-
每个类别的嵌入被随机初始化,就像神经网络中的其他权重一样。
-
训练使嵌入对模型更有用。
实际上,这并不是一个概念上新的操作,因为你可以通过首先进行独热编码类别,然后使用没有激活函数和偏置的线性(密集)层来模拟它。嵌入层更高效,因为它只是进行查找,而不是像线性层中那样计算矩阵乘积。
图片由作者提供。
构建模型
现在我们已经具备了所有的要素,让我们来构建一个模型吧!首先,我们将定义模型的高层次架构,然后在 TensorFlow 中构建它,虽然如果你更喜欢 PyTorch,它也同样容易。
架构
好了,那么两个分类变量(user_id 和 movie_id)进入模型,然后我们进行嵌入。我们最终得到两个向量,最好是相同长度的。最后,我们希望得到一个单一的数字,即user_rating。
注意: 我们将其建模为回归问题,但你也可以将其视为分类任务。
那么,我们如何将两个相同长度的向量合并为一个单一的数字呢?有很多方法,但其中一种最简单且高效的方法是直接计算点积。
图片由作者提供。
注意: 我们将在接下来的内容中采用的方法也称为 矩阵分解 ,因为我们会在各处计算点积,就像你乘以两个矩阵一样。
我会说这并没有什么太疯狂的地方。现在我们可以看看模型应该如何工作:
图片由作者提供。
作为一个公式,我们创建了这个:
图片由作者提供。
这表示“用户 u 对电影 m 的评分等于用户 u 的嵌入与电影 m 的嵌入的点积”。
TensorFlow 实现,第一个版本
如果你了解基本的 TensorFlow,实现实际上是轻而易举的。唯一需要注意的是,嵌入层希望类别被表示为从 1 到 number_of_categories 的整数。你经常会看到有人填充一些字典,如 {“user_8323”: 1, “user_1122”: 2, …} 以及一个反向字典,如 {1: “user_8323”, 2: “user_1122”, …} 来实现这一点,但 TensorFlow 也有一些很好的层来处理这些。我们将在这里使用 [IntegerLookup](https://www.tensorflow.org/api_docs/python/tf/keras/layers/IntegerLookup)
。这个层的一个好特性是:未知类别默认映射为 0。
在开始之前,我们必须首先从训练集中获取所有唯一的用户和电影。
all_users = train["user_id"].unique()
all_movies = train["movie_id"].unique()
使用 Keras 的功能 API,你可以这样实现上述想法:
import tensorflow as tf
# user pipeline
user_input = tf.keras.layers.Input(shape=(1,), name="user")
user_as_integer = tf.keras.layers.IntegerLookup(vocabulary=all_users)(user_input)
user_embedding = tf.keras.layers.Embedding(input_dim=len(all_users)+1, output_dim=32)(user_as_integer)
# movie pipeline
movie_input = tf.keras.layers.Input(shape=(1,), name="movie")
movie_as_integer = tf.keras.layers.IntegerLookup(vocabulary=all_movies)(movie_input)
movie_embedding = tf.keras.layers.Embedding(input_dim=len(all_movies)+1, output_dim=32)(movie_as_integer)
# dot product
dot = tf.keras.layers.Dot(axes=2)([user_embedding, movie_embedding])
flatten = tf.keras.layers.Flatten()(dot)
# model input/output definition
model = tf.keras.Model(inputs=[user_input, movie_input], outputs=flatten)
model.compile(loss="mse", metrics=[tf.keras.metrics.MeanAbsoluteError()])
由于我们给用户和电影输入层起了好名字,我们可以像这样训练模型:
model.fit(
x={
"user": X_train["user_id"],
"movie": X_train["movie_id"]
},
y=y_train.values,
batch_size=256,
epochs=100,
validation_split=0.1, # for early stopping
callbacks=[
tf.keras.callbacks.EarlyStopping(patience=1, restore_best_weights=True)
],
)
# Output (for me):
# ...
# Epoch 18/100
# 3165/3165 [==============================] - 8s 3ms/step - loss: 0.7357 - mean_absolute_error: 0.6595 - val_loss: 11.4699 - val_mean_absolute_error: 2.9923
我们现在可以在测试集上评估这个模型,但我们已经可以看到,它可能非常糟糕,因为val_mean_absolute_error
约为 3。这意味着我们平均偏差为 3 星,在 5 星系统中这是非常糟糕的。这比我们之前的糟糕模型还要差,这是一个了不起的成就。😅 但这为什么呢?让我们在下一部分深入探讨一下。
TensorFlow 实现,版本二
到目前为止,我们已经构建了一个回归模型,可以输出任何实数。模型很难学习应该输出在 1 到 5 之间的特殊范围的数字,但我们可以用一个简单的技巧来简化这一点:只需像处理逻辑回归一样压缩输出范围。只不过是将[0, 1]区间缩放和移动到[1, 5]。
user_input = tf.keras.layers.Input(shape=(1,), name="user")
user_as_integer = tf.keras.layers.IntegerLookup(vocabulary=all_users)(user_input)
user_embedding = tf.keras.layers.Embedding(input_dim=len(all_users) + 1, output_dim=32)(user_as_integer)
movie_input = tf.keras.layers.Input(shape=(1,), name="movie")
movie_as_integer = tf.keras.layers.IntegerLookup(vocabulary=all_movies)(movie_input)
movie_embedding = tf.keras.layers.Embedding(input_dim=len(all_movies) + 1, output_dim=32)(movie_as_integer)
dot = tf.keras.layers.Dot(axes=2)([user_embedding, movie_embedding])
flatten = tf.keras.layers.Flatten()(dot)
# this is new!
squash = tf.keras.layers.Lambda(lambda x: 4*tf.nn.sigmoid(x) + 1)(flatten)
model = tf.keras.Model(inputs=[user_input, movie_input], outputs=squash)
model.compile(loss="mse", metrics=[tf.keras.metrics.MeanAbsoluteError()])
公式如下:
图片由作者提供。
其中 σ 是 sigmoid 函数。训练如上所述,测试集上的评估结果是
model.evaluate(
x={"user": X_test["user_id"], "movie": X_test["movie_id"]},
y=y_test
)
# Output:
# [...] loss: 0.9701 - mean_absolute_error: 0.7683
这比之前的模型和我们糟糕的基准要好得多。如果你还想要一个r²得分:
from sklearn.metrics import r2_score
r2_score(
y_test,
model.predict(
{"user": X_test["user_id"], "movie": X_test["movie_id"]}
).ravel()
)
# Output:
# 0.1767611765807019
让我们做一个小的最终调整,以得到一个更好的模型。
TensorFlow 实现,最终版本
除了嵌入,我们还可以为每部电影和每个用户关联一个偏置项。这捕捉到一些用户往往只给出相对积极(或消极)的评分,以及一些电影往往只收到积极(或消极)的评论。这样,偏置项可以进行粗略调整,而嵌入则进行精细调整。例如,一个通常给 4 星的用户将有一个固定的偏置,一个单一数字,而不是向量。嵌入仅需专注于解释为什么这个用户有时给 3 星或 5 星。
公式变为
图片由作者提供。
其中 bᵤ 和 bₘ 分别是用户 u 和电影 m 的偏置项。作为代码:
user_input = tf.keras.layers.Input(shape=(1,), name="user")
user_as_integer = tf.keras.layers.IntegerLookup(vocabulary=all_users)(user_input)
user_embedding = tf.keras.layers.Embedding(input_dim=len(all_users) + 1, output_dim=32)(user_as_integer)
user_bias = tf.keras.layers.Embedding(input_dim=len(all_users) + 1, output_dim=1)(user_as_integer)
movie_input = tf.keras.layers.Input(shape=(1,), name="movie")
movie_as_integer = tf.keras.layers.IntegerLookup(vocabulary=all_movies)(movie_input)
movie_embedding = tf.keras.layers.Embedding(input_dim=len(all_movies) + 1, output_dim=32)(movie_as_integer)
movie_bias = tf.keras.layers.Embedding(input_dim=len(all_movies) + 1, output_dim=1)(movie_as_integer)
dot = tf.keras.layers.Dot(axes=2)([user_embedding, movie_embedding])
add = tf.keras.layers.Add()([dot, user_bias, movie_bias])
flatten = tf.keras.layers.Flatten()(add)
squash = tf.keras.layers.Lambda(lambda x: 4 * tf.nn.sigmoid(x) + 1)(flatten)
model = tf.keras.Model(inputs=[user_input, movie_input], outputs=squash)
model.compile(loss="mse", metrics=[tf.keras.metrics.MeanAbsoluteError()])
如果你喜欢 Keras 的plot_model
输出:
图片由作者提供。
图片由作者提供,创建于netron.app/
。
如已指出,模型性能再次改善。
-
MSE ≈ 0.89
-
MAE ≈ 0.746
-
r² ≈ 0.245
太好了!我们在这个版本中得到了最低的 MAE 和 MSE(因此r²最高)。
进行预测
为了回答像“用户 1 会给电影 2 和电影 3 多少分?”这样的问题,你可以做一个简单的
model.predict({"user": tf.constant([[1], [1]]), "movie": tf.constant([[2], [3]])})
# Output:
# array([[3.0344076],
# [2.9140234]], dtype=float32)
为了获取用户 1 的所有评分,你可以做
model.predict({
"user": tf.ones_like(all_movies.reshape(-1, 1)), # fill user 1 in many times
"movie": all_movies.reshape(-1, 1)
})
结论
在这篇文章中,我们已经看到推荐系统对业务有很大的影响,这也是它们被广泛使用的原因。构建一个好的推荐系统不像其他模型那样简单,因为你通常需要处理高基数的分类特征,使得简单的技巧如独热编码变得无效。
我们学习了如何通过在神经网络架构中使用嵌入来规避这个问题。我们添加了一些简单的技巧,最终得到了一个不算差的矩阵分解模型,即使没有调整任何超参数。我们还可以通过
-
优化嵌入维度(我们目前设定为 32)
-
对嵌入应用正则化
-
构建一个适当的时间划分验证集,而不是像我们所做的那样随机生成的
-
在我们知道最佳超参数之后,在完整的训练数据集(包括验证集)上重新训练模型
最大的优势之一是我们可以在大多数情况下应用这个模型,因为我们只需要用户和电影的交互(评分)数据。我们不需要了解更多关于用户和电影的信息,例如年龄、性别、类型等等,因此通常我们可以立即开始。
我们为此付出的代价是我们无法为未知的用户或电影输出有意义的嵌入——冷启动问题。模型会输出某些东西,但质量会很差。
但是,如果我们恰好拥有用户和电影数据,我们可以做得更聪明,并且可以以直接的方式将这些特征融入进来。这可以缓解冷启动问题,甚至可能改善已知用户和电影的模型。你可以在这里阅读相关内容:
当协作和基于内容的推荐系统合并时
towardsdatascience.com
参考文献
[1] D. Jannach 和 M. Jugovac, 推荐系统的商业价值测量 (2019), ACM Transactions on Management Information Systems (TMIS) 10.4 (2019): 1–23
希望你今天学到了新的、有趣的、有用的东西。感谢阅读!
最后一点,如果你
-
想要支持我写更多关于机器学习的内容
-
无论如何,计划获取一个 Medium 订阅,
为什么不通过这个链接 支持我呢?这对我帮助很大!😊
为了透明,您的价格不会改变,但大约一半的订阅费用会直接给我。
非常感谢,如果你考虑支持我!
如果你有任何问题,请在 LinkedIn上联系我!
预测集成简介
原文:
towardsdatascience.com/introduction-to-forecasting-ensembles-f63877a2498
提升预测性能的廉价技巧
·发表于Towards Data Science ·阅读时间 5 分钟·2023 年 1 月 12 日
--
照片由Natalie Pedigo提供,发布在Unsplash上。
如果你需要挤出一点额外的性能,预测组合可能正是你所需要的。
预测组合是将多个模型的预测结果结合起来的过程。这种技术也被称为集成预测。
在这里,你将学习创建预测集成的 3 个主要步骤。
为什么使用集成模型?
没有任何预测方法是完美的。
创建预测模型有几种技术。例如,包括经典方法如 ARIMA 或指数平滑,或机器学习方法,如决策树或神经网络。你可以查看我之前的文章了解如何使用时间序列进行监督学习。
每种方法都有其对数据的假设,这些假设并不总是成立。每种方法都有其优点和局限性。管理这两者是集成模型的关键动机。
将多个模型结合起来通常会导致更准确的预测。一个原因是这减少了选择错误模型的可能性。
此外,集成模型对传达未来观测的不确定性也很有用。模型之间的高预测变异性表明更大的不确定性。这一点对决策制定非常有价值。
那么,集成模型是如何工作的呢?
建立集成模型的 3 个阶段
建立集成模型是一个 3 阶段的过程:
-
创建:建立不同的单独模型;
-
剪枝:移除表现不佳或冗余的模型;
-
整合:将模型结合起来。
图 1:构建集成模型的步骤。首先,创建许多模型。然后,去除表现差或冗余的模型。其余模型用于预测。图片来源于作者。
让我们深入探讨每一步。
创建步骤
图片由Mourizal Zativa拍摄,来源于Unsplash
第一步是创建构成集成模型的模型。
模型之间的多样性是你应该关注的关键点。每个模型应提供与其他模型不同但优秀的预测。高度相关的模型会降低集成模型的效果。
那么,如何在构建集成模型时促进多样性呢?有两种主要策略:
-
改变学习方法或其参数;
-
操作训练数据。
不同的方法对数据有不同的假设。因此,这会在集成模型中产生自然的多样性。
如果一个集成模型的模型是使用不同算法训练的,则称为异质集成模型。例如线性模型和决策树。否则,称为同质集成模型。随机森林是同质决策树集成模型的一个例子。
在像随机森林这样的同质集成模型中,如何鼓励多样性?
随机森林的输入数据通过两种方式进行操作:
-
每棵树的训练数据都是通过替换抽样的。这一过程称为 bagging;
-
在每棵树中,每次分裂时都会选择一个随机特征子集。这进一步增加了多样性。
参考文献[3]中的文章提供了关于集成模型多样性的出色综述。
修剪步骤
图片由Matt Briney拍摄,来源于Unsplash
在创建步骤中构建了许多模型。
这个过程有一定的随机性。但是,并不能保证所有模型都将有用或改善多样性。
解决这一问题的一种方法是修剪集成模型。这涉及到去除不必要的模型。模型应因表现不佳或冗余而被丢弃。
去除不必要的模型后,你会得到一个更好的集成模型。一个具有更好预测性能和更少维护模型的集成模型。
集成步骤
最终阶段是集成。这是当你结合各个模型的预测结果时。
最简单的方法是取预测值的平均值。或者,在分类情况下,通过多数投票。另一种方法是为每个模型分配不同的权重,并取加权平均值。一种确定权重的方法是基于过去的表现。如果模型表现良好,你可以给它更高的权重。
实践操作
让我们使用 Python 构建一个预测集成模型。
这是代码,注释中解释了它:
import pandas as pd
from plotnine import *
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error as mae
from pmdarima.datasets import load_ausbeer # GPL>=2 licence
from statsforecast import StatsForecast
from statsforecast.models import (
AutoARIMA,
HoltWinters,
AutoTheta,
AutoETS,
DynamicOptimizedTheta as DOT,
SeasonalNaive
)
# https://github.com/vcerqueira/blog/tree/main/src/
from src.plots.forecasts import train_test_yhat_plot
from src.plots.barplots import err_barplot
# quarterly data
PERIOD = 4
# forecasting the final 3 years of data
TEST_SIZE = 12
# loading the beer time series
series = load_ausbeer(as_series=True).dropna()
series.index = pd.date_range(start='1956Q1', end='2008Q3', freq='QS')
# train/test split
train, test = train_test_split(series, test_size=TEST_SIZE, shuffle=False)
# transforming the train data to the required format for statsforecast
train_df = train.reset_index()
train_df.columns = ['ds', 'y']
train_df['unique_id'] = '1'
# setting up the models
models = [
AutoARIMA(season_length=PERIOD),
HoltWinters(season_length=PERIOD),
SeasonalNaive(season_length=PERIOD),
AutoTheta(season_length=PERIOD),
DOT(season_length=PERIOD),
AutoETS(season_length=PERIOD),
]
sf = StatsForecast(
df=train_df,
models=models,
freq='Q',
n_jobs=1,
fallback_model=SeasonalNaive(season_length=PERIOD)
)
# training the models
sf.fit(train_df)
# forecasting
forecasts = sf.predict(h=TEST_SIZE)
forecasts = forecasts.reset_index(drop=True).set_index('ds')
# averaging the forecasts to make the ensemble predictions
forecasts['Ensemble'] = forecasts.mean(axis=1)
作为案例研究,目标是预测啤酒的生产。你可以在参考文献[3]中查看这些数据的来源。我们使用statsforecast库创建了 6 个预测模型。这些模型包括 AutoARIMA、Holt-Winters 或 AutoETS。
然后,将 6 个模型的预测结果进行平均,形成集成预测。
这是预测结果:
图 2:多个模型及其组合的预测。图片由作者提供。
现在,这是每种方法的平均绝对误差:
图 3:每种方法的平均绝对误差。图片由作者提供。
集成模型的表现优于任何单一模型。
在这个例子中,集成模型只包含 6 个模型。但集成模型通常包含更多模型(最多可达几百个)。然而,随着模型数量的增加,回报递减效应也会出现。
有什么陷阱?
虽然集成模型提升了预测表现,但它们也有一些局限性。
-
你需要维护多个模型而不是一个。这会导致额外的计算成本。例如,如果需要在小型设备中部署这些模型,可能没有足够的存储空间。如果数据的采样频率很高,获取和整合所有预测的时间可能会过长。
-
集成模型缺乏透明度。在某些领域,透明的模型对于可信度和从业者的采纳至关重要。
主要收获
-
集成模型结合了多个模型的预测结果。这通常会导致更好的预测表现;
-
集成模型分为 3 个阶段:创建、修剪和整合;
-
集成模型的主要局限性在于额外的计算成本和缺乏透明度。
感谢阅读,下次故事见!
参考文献
[1] Aiolfi, Marco, 和 Allan Timmermann. “预测表现的持续性和条件组合策略。” 经济计量学杂志 135.1–2 (2006): 31–53。
[2] Brown, Gavin, 等. “多样性创建方法:调查与分类。” 信息融合 6.1 (2005): 5–20。
[3] 季度澳大利亚啤酒生产 (GPL ≥ 2 许可证) www.rdocumentation.org/packages/fpp/versions/0.5/topics/ausbeer
功能性 PyTorch 入门
如何使用 write Jax-style PyTorch models
·
关注 发表在 Towards Data Science · 6 分钟阅读 · 2023 年 5 月 7 日
--
照片由 Ricardo Gomez Angel 提供,来源于 Unsplash
PyTorch 最近在 2.0 版本中将 torch.func
模块集成到了主代码库中。该模块,之前被称为 functorch
,使得在 PyTorch 中以简洁的 API 开发纯函数式神经网络模型成为可能。这个包是 PyTorch 对 Jax 的日益流行 的回应,Jax 是一个从底层使用函数式编程范式构建的 Python 通用可微编程框架。
在这篇文章中,我们将首先介绍 torch.func
的基础知识,然后通过一个简单的端到端示例来使用神经网络(NN)模型拟合非线性函数。虽然使用 NN 完成这项任务显然有些过度,但它在说明目的上效果很好。此外,我们还将发现构建 NN 模型时采用函数式方法的一些好处。
编写一个函数式模型
使用 torch.func
的方法与标准 PyTorch 开始时相同:你需要构建一个神经网络。为了简单起见,我们定义一个非常简单的网络,由任意数量的线性层和非线性激活函数组成。前向传播接收一批数据点作为输入,并对模型进行评估。
class SimpleNN(nn.Module):
def __init__(
self,
num_layers: int = 1,
num_neurons: int = 5,
) -> None:
"""Basic neural network architecture with linear layers
Args:
num_layers (int, optional): number of hidden layers
num_neurons (int, optional): neurons for each hidden layer
"""
super().__init__()
layers = []
# input layer
layers.append(nn.Linear(1, num_neurons))
# hidden layers with linear layer and activation
for _ in range(num_layers):
layers.extend([nn.Linear(num_neurons, num_neurons), nn.Tanh()])
# output layer
layers.append(nn.Linear(num_neurons, 1))
# build the network
self.network = nn.Sequential(*layers)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.network(x.reshape(-1, 1)).squeeze()
现在事情变得有趣了。请记住,torch.func
允许构建纯函数式模型。但这在实践中意味着什么?
首先,需要掌握函数式编程的核心概念:纯函数。本质上,纯函数有两个定义特征:
-
对于相同的输入参数,其返回值是相同的
-
它没有副作用,即不会以任何方式修改其输入参数
根据这个定义,大多数标准 PyTorch 模块的方法不是纯粹的,因为参数存储在 PyTorch 模型中。换句话说,标准 PyTorch 模型是有状态的,而不是无状态的,这符合函数式范式。考虑以下代码:
import torch
x = torch.randn(10)
model = SimpleNN() # constructed above
optimizer = torch.optim.SGD(model.parameters())
# modify the state of the model
# by applying a single optimization step
out1 = model(x)
model.backward()
optimizer.step()
# recompute the output with exactly the same input
out2 = model(x)
assert not torch.equal(out1, out2)
模型的前向传播对相同输入参数的输出不同,因为优化器在原地更新了参数。
使 PyTorch 模块变得纯粹的一种方法是将参数与模型解耦,从而使模型完全无状态。这正是 torch.func.functional_call()
例程所做的:
import torch
from torch.func import functional_call
x = torch.randn(10) # random input data
model = SimpleNN() # constructed above
params = dict(model.named_parameters()) # model parameters
# make a functional call to the model above
out = functional_call(model, params, (x,))
现在我们已经拥有了纯函数式的前向传播,用于我们的神经网络模型,我们可以探索如何使用 PyTorch 的函数式 API 提供的可组合原语。这使我们能够使用每个具有自身函数实现的模块化构建块来构建复杂模型。
可组合函数转换
我刚刚展示了如何为我们的模型定义一个完全功能的前向传递。但是我们如何定义微分规则和损失函数呢?我们需要使用torch.func
提供的 可组合的 函数变换。
函数变换由一组例程组成,每个例程返回一个函数,该函数可以用于评估特定的量。这种返回另一个函数的函数被称为 高阶函数。例如,可以使用 grad
原语来计算相对于输入数据 x
的梯度,如下所示:
from torch.func import grad
# the `grad` function returns another function
# which takes the same inputs as the model forward pass
grad_fn = grad(model)
# now this function can be used to compute gradients
# with respect to the first input
params = tuple(model.parameters())
grad_values = grad_fn(x[0], params)
请注意,默认情况下,grad
函数应用于单个数字。可以使用另一个函数变换 vmap
来高效地处理输入批次。请注意,vmap
在多个 CPU 或 GPU 可用时也会自动进行并行化,而无需任何代码更改。
torch.func
模块中所有函数都是纯函数的一个重要后果是它们能够被任意组合(因此它们的名称中有“可组合”一词)。这是因为纯函数总是可以用其结果替代,而不影响程序执行,这是上述两个属性的直接结果。
有了这些考虑,让我们计算一批输入数据x
的二阶导数:
from torch.func import grad, vmap
x = torch.rand(10)
# combine twice `grad` with `vmap` to compute
# the model second order derivative (Laplacian) with
# respect to batched input data
laplacian_fn = vmap(grad(grad(model)))
params = tuple(model.parameters())
out = laplacian_fn(x, params)
值得注意的是,模型的前向传递不接受参数作为输入。因此,为了计算相对于参数的梯度,我们需要定义一个辅助的 make_functional_fwd
例程,带有适当的参数。实际上,我们可以使用闭包来实现,如下所示:
import torch
from torch.func import functional_call, grad
x = torch.randn(1) # random input data point
model = SimpleNN() # constructed above
# forward pass using the functional API
# to take the parameters as input arguments
def make_functional_fwd(_model):
def fn(data, parameters):
return functional_call(_model, parameters, (data,))
return fn
model_func = make_functional_fwd(my_model) # functional forward
params = tuple(my_model.parameters()) # model parameters
# the `argnums` argument allows to select with
# respect to which input argument of the functional forward
# pass defined in the closure
grad_params = grad(model_func, argnums=1)(x[0], params)
# as before but for computing the gradient with
# respect to the input data
grad_x = grad(model_func, argnums=0)(x[0], params)
torch.func
模块提供了许多可组合的函数变换,例如用于计算向量-雅可比积的变换。这里你可以找到更多详细信息。
使用功能模型进行优化
如果你读到这里,你可能会想知道如何使用功能模型进行基于梯度的优化。毕竟,标准的 PyTorch 优化器通过就地修改模型参数来工作,而正如我刚刚展示的,这破坏了纯函数的要求。
不幸的是,PyTorch 本身并未提供功能优化器。然而,可以使用[torchopt](https://github.com/metaopt/torchopt)
库来实现这一目的。
为了展示功能优化的工作原理,我们假设我们要拟合一个简单的函数,例如 f(x) = 2 sin(x + 2π),使用一些在区间 [0, 2π] 内的随机输入点。我们可以生成一些训练和测试数据点,如下所示:
import torch
def get_data(n_points = 20):
x = torch.rand(n_points) * 2.0 * torch.pi
y = 2.0 * torch.sin(x + 2.0 * torch.pi)
return x, y
x_train, y_train = get_data(n_points=40)
x_test, y_test = get_data(n_points=10)
现在让我们使用torchopt
和 PyTorch 函数 API 来训练我们的神经网络以拟合这个函数。
import torch
import torchopt
# hyperparameters and optimizer choice from `torchopt`
num_epochs = 500
lr = 0.01
optimizer = torchopt.FuncOptimizer(torchopt.adam(lr=lr))
loss_fn = torch.nn.MSELoss()
loss_evolution = [] # track the loss evolution per epoch
params = tuple(model.parameters()) # initialize the parameters
for i in range(num_epochs):
# update the parameters using the functional API
y = model_func(x_train, params)
loss = loss_fn(y, y_train)
params = optimizer.step(loss, params)
loss_evolution.append(float(loss))
if i % 100 == 0:
print(f"Iteration {i} with loss {float(loss)}")
# accuracy on test set
y_pred = model_func(x_test, params)
print(f"Loss on the test set: {loss_fn(y_pred, y_test)}")
正如你所看到的,优化循环与标准的 PyTorch 非常相似,关键区别在于优化器步骤现在需要当前损失和当前参数值,并以完全无状态的方式评估更新后的参数。在我看来,这种方法看起来比 PyTorch 典型的有状态 API 要干净得多!
如果你想获取本博客文章的完整代码,可以查看此代码片段。正如你所注意到的那样,一些实现细节(特别是torch.func.functional_call
与torchopt
优化器之间的交互)在本博客中没有涉及。如果你有任何问题,请随时在Linkedn上给我发消息。
结论
感谢阅读本博客文章。PyTorch 的函数式 API 是一个强大的工具,可以帮助你编写高性能的神经网络模型,并利用可组合函数、自动并行化和矢量化,类似于 Jax。然而,它仍然是一个实验性的功能,应谨慎使用。祝你编程愉快!
[1] 与 Jax 的简洁比较:使用 PyTorch 和 functorch 编写类似 JAX 的代码教程 — Simone Scardapane (sscardapane.it)
[2] 虽然有点老但解释得非常好的教程:使用 FuncTorch 工作:介绍 | functorch-examples — Weights & Biases (wandb.ai)
假设检验与示例介绍
一份关于假设检验的易懂指南,包含示例和可视化内容
·
关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 1 月 5 日
--
我见过的大多数假设检验教程都从对分布的先验假设开始,列出一些定义和公式,然后直接应用它们解决问题。
然而,在本教程中,我们将从基本原理开始学习。这将是一个以示例为驱动的教程,我们将从一个基本示例入手,逐步深入了解假设检验的基础。
让我们开始吧。
你选择了哪个骰子?
图片来源于 Brett Jordan 于 Unsplash
想象一下你面前有两个无法区分的骰子。一个是公平的,另一个是加重的。你随机挑选一个骰子并掷它。在观察到它落在哪个面上后,你能确定你选择的是哪个骰子吗?
骰子的概率分布如下所示:
**Die 1:**
P(X=x) = 1/6 if x = {1, 2, 3, 4, 5, 6}
**Die 2:**
P(X=x) = 1/4 if x = {1, 2}
= 1/8 if x = {3, 4, 5, 6}
在二元假设检验问题中,我们通常会面临两个选择,我们称之为假设,我们需要决定选择其中一个。
假设由 H₀和 H₁表示,分别称为原假设和备择假设。在假设检验中,我们要么拒绝原假设,要么接受它。
在我们的例子中,骰子 1 和骰子 2 分别是原假设和备择假设。
如果你直观地考虑一下,如果骰子落在 1 或 2 上,更可能是骰子 2,因为它更容易落在 1 或 2 上。因此,接受或拒绝原假设的决定依赖于观察值的分布。
因此,我们可以说假设检验的目标是画出一个边界,将观察空间分成两个区域:拒绝区和接受区。
如果观察结果落在拒绝区,我们就会拒绝原假设,否则我们接受它。现在,决策边界并不会是完美的,我们会犯错误。例如,骰子 1 落在 1 或 2 上,我们可能会误认为是骰子 2;但这种情况发生的概率较小。我们将在下一节学习如何计算错误的概率。
我们如何确定决策边界?有一种简单有效的方法叫做似然比检验,我们接下来将讨论。
似然比检验
你首先要意识到观察值的分布取决于假设。下面我绘制了在这两个假设下我们例子的分布:
在两种假设下观察结果的概率分布
现在,P(X=x;H₀)
和P(X=x;H₁)
分别表示假设 H₀和 H₁下的似然性。它们的比率告诉我们某个假设相对于另一个假设的可能性。
这个比率称为似然比,表示为L(X)
。L(X)
是一个随机变量,依赖于观察值x
。
似然比
在似然比检验中,如果比率高于某个值,即L(X) > 𝜉
,则我们拒绝原假设,否则接受它。𝜉被称为临界比率。
这就是我们如何绘制决策边界的方法:我们将似然比大于临界比率的观察值与其余观察值分开。
所以,形式为{x | L(x) > 𝜉}
的观察值落入拒绝区,而其余的观察值则落入接受区。
让我们用掷骰子示例来说明。似然比可以计算为:
L(X) = (1/4) / (1/6) = 3/2 if x = {1, 2}
= (1/8) / (1/6) = 3/4 if x = {3, 4, 5, 6}
似然比的图示如下:
现在决策边界的放置取决于选择临界比率。假设临界比率在 3/2 和 3/4 之间,即 3/4 < 𝜉 < 3/2
。那么我们的决策边界看起来是这样的:
if 3/4 < 𝜉 < 3/2:
L(X) > 𝜉 if x = {1, 2} (rejection region)
L(X) < 𝜉 if x = {3, 4, 5, 6} (acceptance region)
如果似然比在 3/4 和 3/2 之间,则拒绝和接受区域
让我们讨论与此决策相关的错误。第一种错误发生在观察 x 属于拒绝区域但发生在零假设下。在我们的示例中,这意味着骰子 1 落在 1 或 2 上。
这被称为错误拒绝或类型 1 错误。此错误的概率用 𝛼
表示,可以计算为:
False Rejection Error:
𝛼 = P(X|L(X) > 𝜉 ; H₀)
第二种错误发生在观察 x 属于接受区域但发生在备择假设下。这被称为错误接受或类型 2 错误。此错误的概率用 𝛽
表示,可以计算为:
False Acceptance Error:
𝛽 = P(X|L(X) < 𝜉 ; H₁)
在我们的示例中,错误拒绝和错误接受可以计算如下:
Computing errors in the dice example:
𝛼 = P(X|L(X) > 𝜉 ; H₀)
= P(X={1, 2} ; H₀)
= 2 * 1/6
= 1/3
𝛽 = P(X|L(X) < 𝜉 ; H₁)
= P(X={3, 4, 5, 6} ; H₁)
= 4 * 1/8
= 1/2
让我们考虑两种其他情况,其中临界比率取以下值:𝜉 > 3/2
和 𝜉 < 3/4
。
临界比率 < 3/4
临界比率 > 3/2
类型 1 和类型 2 错误可以类似地计算。
𝛼 = 0 if 𝜉 > 3/2
= 1/3 if 3/4 < 𝜉 < 3/2
= 1 if 𝜉 < 3/4
𝛽 = 1 if 𝜉 > 3/2
= 1/2 if 3/4 < 𝜉 < 3/2
= 0 if 𝜉 < 3/4
让我们绘制不同 𝜉 值下的两种错误。
随着临界值 𝜉 的增加,拒绝区域变得更小。因此,错误拒绝概率 𝛼 减少,而错误接受概率 𝛽 增加。
似然比检验提供了最小的错误
我们可以在观察空间中的任何地方绘制边界。为什么我们需要计算似然比并经历所有这些呢?让我们来看看原因。
下面我计算了不同边界下的类型 I 和类型 II 错误。
Type I and Type II errors for different boundaries.
'|' is the separator - {rejection region | acceptance region}
1\. {|, 1, 2, 3, 4, 5, 6}
𝛼 = P(x={} ; H₀) = 0
𝛽 = P(x={1, 2, 3, 4, 5, 6} ; H₁) = 1
𝛼 + 𝛽 = 1
2\. {1, |, 2, 3, 4, 5, 6}
𝛼 = P(x={1} ; H₀) = 1/6
𝛽 = P(x={2, 3, 4, 5, 6} ; H₁) = 1/4 + 1/2 = 3/4
𝛼 + 𝛽 = 0.916
3\. {1, 2, |, 3, 4, 5, 6}
𝛼 = P(x={1, 2} ; H₀) = 1/3
𝛽 = P(x={3, 4, 5, 6} ; H₁) = 1/2
𝛼 + 𝛽 = 0.833
4\. {1, 2, 3, |, 4, 5, 6}
𝛼 = P(x={1, 2, 3} ; H₀) = 1/2
𝛽 = P(x={4, 5, 6} ; H₁) = 3/8
𝛼 + 𝛽 = 0.875
5\. {1, 2, 3, 4, |, 5, 6}
𝛼 = P(x={1, 2, 3, 4} ; H₀) = 2/3
𝛽 = P(x={5, 6} ; H₁) = 1/4
𝛼 + 𝛽 = 0.916
6\. {1, 2, 3, 4, 5, |, 6}
𝛼 = P(x={1, 2, 3, 4, 5} ; H₀) = 5/6
𝛽 = P(x={6} ; H₁) = 1/8
𝛼 + 𝛽 = 0.958
6\. {1, 2, 3, 4, 5, 6, |}
𝛼 = P(x={1, 2, 3, 4, 5, 6} ; H₀) = 1
𝛽 = P(x={} ; H₁) = 0
𝛼 + 𝛽 = 1
不同边界下的类型 I 和类型 II 错误及其总和的图示如下:
我们可以看到,对于从似然比检验获得的最优临界比率值,类型 I 和类型 II 错误的总和是最小的。
换句话说,对于给定的错误拒绝概率,似然比检验提供了最小的错误接受概率。
这被称为 Neyman-Pearson 引理。我在文章末尾引用了理论证明。
连续分布的似然比检验
在上述示例中,我们没有讨论如何选择临界比率 𝜉 的值。概率分布是离散的,因此临界比率 𝜉 的小变化不会影响边界。
当处理连续分布时,我们固定错误拒绝概率 𝛼 的值,并基于此计算临界比率。
P(L(X) > 𝜉 ; H₀) = 𝛼
不过,过程将是相同的。一旦我们获得了临界比率的值,我们就会划分观察空间。
对于 𝛼,典型的选择有 𝛼 = 0.01
、𝛼 = 0.05
或 𝛼 = 0.01
,具体取决于假设检验中错误拒绝的不可接受程度。
例如,如果我们处理的是正态分布,我们可以将其标准化,然后查找 Z 表以找到给定 𝛼 的 𝜉。
结论
在这篇文章中,我们探讨了假设检验的理念和过程背后的直觉。整个过程可以在下面的图示中总结:
我们从两个假设 H₀ 和 H₁ 开始,使得基础数据的分布依赖于这些假设。目标是通过找到一个决策规则,将观测值 x 映射到两个假设之一,从而证明或推翻原假设 H₀。最后,我们计算与决策规则相关的错误。
然而,在现实世界中,这两种假设之间的区别不会那么直接。因此,我们必须做一些变通处理以进行假设检验。让我们在下一篇文章中讨论这个问题。
希望你喜欢这篇文章。让我们保持联系。
**Let's Connect** Hope you've enjoyed the article. Please clap and follow if you did.
You can also reach out to me on [LinkedIn](https://www.linkedin.com/in/neerajkrishnadev/) and [Twitter](https://twitter.com/WingedRasengan).
图像和图表来源
本文中的所有图像、图表和示意图均由作者创建;除非在说明中明确提到。
参考文献
《Dimitri Bertsekas 和 John Tsitsiklis 的概率论导论》第九章第三部分
ICA 简介:独立成分分析
·
关注 发布于 Towards Data Science · 9 min read · 2023 年 2 月 14 日
--
你是否曾经遇到过试图分析复杂且高度相关的数据集时感到信息量庞大、不知所措的情况?这时,独立成分分析(ICA)就派上用场了。ICA 是数据分析领域中的一种强大技术,它可以帮助你在多变量数据集中分离和识别潜在的独立来源。
图片来源:Unsplash
ICA 很重要,因为它提供了一种理解数据集的隐藏结构的方法,并且可以在各种应用中使用,如信号处理、脑成像、金融等许多其他领域。此外,ICA 可以帮助从数据中提取最相关的信息,提供有价值的见解,否则这些见解将在各种相关性中迷失。
在本文中,我们将深入讨论#1 ICA 的基本原理,讨论鸡尾酒派对与之有何关系,#2 3 步 ICA 算法,以及#3 如何在你的数据分析项目中实施它。所以,如果你准备解锁你的数据的全部潜力,就加入这场旅程吧!
#1:介绍与主要内容
独立分量分析是多种无监督学习算法之一,这意味着我们在使用它之前不需要对模型进行监督。这种方法的起源来自信号处理,我们试图将多变量信号分离为加法子分量。让我们深入解释一下主要思想:
图片由作者提供
想象一些独立的信号或变量。这些信号可以用信号曲线表示,第一信号位于图像顶部,第二信号位于图像上方。由于测量的结果,我们没有收到包含这些信号本身的数据集,而是包含了这两个信号的测量结果,这些信号不幸地以不同的线性组合混合在一起。ICA 的目标是通过分离混合数据来恢复原始的未知信号。最终目标是重构数据,使每个维度相互独立。
为了使这个概念更加具体,将使用 ICA 中最著名的例子“鸡尾酒派对问题”。
图片由Unsplash提供
鸡尾酒派对问题
想象参加一个鸡尾酒派对,多个人同时说话,使得难以跟随单个对话。值得注意的是,人类具有在这种情况下分离个别语音流的能力。从技术上讲,这变得稍微具有挑战性。
鸡尾酒派对问题。作者提供的图片。
假设我们用两个麦克风记录派对上两组的对话。这导致两个混合信号,第一次测量对第一组影响更大,对第二组影响较小,而第二次测量对第二组的影响更大。
图片由作者提供
这一点的总体框架可以在灰色框中的向量表示中找到。向量 X中的测量实际上是向量 S的信号与一些混合系数相乘,混合系数表示在矩阵 A中。由于我们想要提取完整的对话(原始信号),因此我们需要解决向量 S 的问题。
作者提供的图片
ICA 与 PCA
你可能已经猜到 ICA 在某种程度上与主成分分析(PCA)有关。这种假设并不完全错误。两者的基本理念相差不远,但它们在最后阶段有所不同,我们稍后将看到。
让我们总结一下 PCA 的基本功能:假设我们有两个看似相关的变量。通过最大化这些变量的方差,利用它们的特征向量和特征值,我们可以将它们转换为主成分。在这个具体例子中,PCA 很好地识别了这种关系的主要方向。
让我们以之前的鸡尾酒例子为例。在非常简单的表示中,我们可以想象从麦克风一和二获得的两个测量形成类似交叉的模式。如果在这种情况下应用 PCA,我们会得到错误的结果,因为 PCA 在具有多个主方向的数据集上表现不佳。
作者提供的图片
而 ICA 则通过关注独立成分而非主成分来解决这个问题。
作者提供的图片
重要的是回顾已建立的概念框架。从麦克风获得的读数对应于被混合矩阵 A 乘以的原始信号。通过根据向量 S 重新排列方程,确定原始变量所需的唯一信息是矩阵 A。然而,矩阵 A 是未知的。
作者提供的图片
因此,为了全面理解矩阵 A 并最终计算向量 S,有必要通过一系列步骤进行逆操作。这些顺序的逆操作组成了 ICA 算法的三个阶段,接下来将对此进行更详细的分析。
#2:分离过程 | 3 步 ICA 算法
在进行 R 中的实际演示之前,了解算法的三个步骤非常重要。该算法的目标是执行向量 X 与矩阵 A 的乘法。矩阵 A 由三个组成部分构成,这些组成部分是不同因素之间乘法交互的结果:
作者提供的图片
步骤 1:找到具有最大方差的角度以旋转 | 估计 U^T
算法的第一个组件涉及使用基于第一个角度 Theta 的矩阵 U^T。角度 Theta 可以通过主成分分析(PCA)确定的数据主要方向来推导。这个步骤将图形旋转到上面所示的位置。
步骤 2:找到主成分的缩放 | 估计 ∑^(-1)
第二个组件涉及拉伸图形,这是通过 Sigma^-1 步骤实现的。这个步骤使用了来自数据的 sigma 1 和 sigma 2 的方差,类似于 PCA 中使用的方法。
步骤 3:旋转的独立性和峰度假设 | 估计 V
最终的组件是当前算法与 PCA 的区别所在,它涉及围绕角度 Phi 的信号旋转。这个步骤旨在通过利用独立性和峰度假设进行旋转,从而重建信号的原始维度。
总结来说,该算法使用测量并围绕 theta 进行旋转,通过使用方差 sigma 1 和 2 进行拉伸,最后围绕 Phi 进行旋转。这些步骤的数学背景已在以下幻灯片中总结以供参考。
如你所见,我们可以仅使用这两个角度和数据的方差来确定逆矩阵 A,这实际上是我们处理 ICA 算法所需的全部内容。进行测量、旋转并缩放。最后,我们再次旋转以获得最终维度。
#3:使用 fastICA()的 R 代码示例
我希望你到目前为止已经理解了 ICA 算法的基本思想。虽然不需要对每一个步骤都进行数学理解,但理解其背后的概念是有帮助的。有了这些知识,我希望与你一起完成一个实际的例子,展示如何使用 R 中的fastICA
函数实际应用 ICA 算法。
# install fastICA package in R
install.packages("fastICA")
# load required libraries
library(MASS) # To use mvrnorm()
library(fastICA)
我们创建了两个随机数据集:信号 1 和信号 2,可以想象成来自我们两个鸡尾酒小组的语音信号:
# random data for signal 1
s1=as.numeric(0.7*sin((1:1000)/19+0.57*pi) + mvrnorm(n = 1000, mu = 0, Sigma = 0.004))
plot(s1, col="red", main = "Signal 1", xlab = "Time", ylab = "Amplitude")
# random data for signal 1
s2=as.numeric(sin((1:1000)/33) + mvrnorm(n = 1000, mu = 0.03, Sigma = 0.005))
plot(s2, col="blue", main = "Signal 2",xlab = "Time", ylab = "Amplitude")
R 输出的截图:原始信号。图片由作者提供
红色曲线代表第一个信号,蓝色曲线代表第二个信号。在这种情况下,形状并不重要。你应该看到的是两个信号彼此不同。现在我们将它们混合起来!
# measurements with mixed data x1 and x2
x1 <- sine1-2*sine2
plot(x1, main = "Linearly Mixed Signal 1", xlab = "Time", ylab = "Amplitude")
x2 <- 1.73*sine1 +3.41*sine2
plot(x2, main = "Linearly Mixed Signal 2", xlab = "Time", ylab = "Amplitude")
R 输出的截图:测量。图片由作者提供
如上所示,我们使用两个信号模拟了两个测量。因此,测量中的信号不再是独立的。这两个混合信号可以想象成我们在鸡尾酒示例中的两个麦克风录音。现在我们忘记这两个原始信号,想象这些测量是我们关于此数据的唯一信息。
因此,我们希望将它们分离,最终得到两个独立的信号:
# apply fastICA function to identify independent signals
measurements <- t(rbind(x1,x2))
estimation <- fastICA(measurements, 2, alg.typ = "parallel", fun = "logcosh", alpha = 1, method = "C", row.norm = FALSE, maxit = 200, tol = 0.0001, verbose = TRUE)
plot(estimation$S[,1], col="red", main = "Estimated signals", xlab = "Time", ylab = "Amplitude")
lines(estimation$S[,2], col="blue")
mtext("Signal 1 estimation in red, Signal 2 estimation in blue")
来自 R 输出的截图:独立信号再次分离。图片由作者提供
该算法的结果如上所示。红色曲线是对信号 1 的估计,而蓝色曲线则估计了信号 2。毫无悬念的是,该算法几乎准确地估计了原始信号,如右侧所示。你可能已经注意到,红色曲线完美匹配了预期,而蓝色曲线似乎被反转了。这是因为算法无法恢复源活动的精确幅度。但除此之外,重建做得相当出色。
限制与结论
让我们从坏消息开始: ICA 只能分离线性混合的源,而且我们无法完美分离高斯分布的源,因为这会破坏我们算法的第三步。尽管我们期望独立源在进行线性组合后混合,ICA 仍会找到一个空间,即使是不独立的源也能在其中达到最大独立性。
但现在来说些好消息: ICA 算法是一种强大的方法,适用于不同领域,并且可以在 R、Mathlab 和其他系统的开源包中轻松使用。有各种示例,其中 ICA 算法被应用于:面部识别应用、股票市场预测等。因此,它在实际使用中是一个重要且备受尊敬的方法。
简而言之: 我们介绍了独立成分分析作为一种无监督学习算法。主要思想是将给定的线性组合测量值分离回原始信号。这称为 重建,并使用三步 ICA 算法。可视化该方法背后的问题的最流行示例再次是鸡尾酒会问题。但现在鸡尾酒问题就说到这里吧。
是时候享受真正的鸡尾酒派对了 🍹
在 Medium 上阅读 Jonas Dieckmann 的文章。分析经理 & 产品负责人 @ philips | 热情并撰写有关…
medium.com](https://medium.com/@jonas_dieckmann?source=post_page-----b2c3c4720cd9--------------------------------)
我希望你觉得这有用。告诉我你的想法!也可以通过 LinkedIn 与我联系 www.linkedin.com/in/jonas-dieckmann/
或在 medium 上关注我。
另见我的其他文章:
现在是探索 Bagging 和 Boosting 世界的时候了。借助这些强大的技术,你可以提高性能…
towardsdatascience.com ## 如何使用 Keras API 和 Google Colab 开始 TensorFlow
分步教程以分析人类活动与神经网络
towardsdatascience.com
参考文献
[1]: Bell, AJ; Sejnowski, TJ (1997). “自然场景的独立成分是边缘滤波器”。《视觉研究》。37 (23): 3327–3338. doi:10.1016/s0042–6989(97)00121–1. PMC 2882863. PMID 9425547.
[2]: Back, AD; Weigend, AS (1997). “独立成分分析在提取股票收益结构中的首次应用”。《国际神经系统期刊》。8 (4): 473–484. doi:10.1142/s0129065797000458. PMID 9730022.
[3]: Barlett, MS (2001). 无监督学习的面部图像分析。波士顿:Kluwer 国际工程与计算机科学系列。
[4]: Comon, Pierre (1994). “独立成分分析,新概念?”《信号处理》,第 36 卷,第 3 期,页 287–314,ISSN 0165–1684,doi.org/10.1016/0165-1684(94)90029-9.
基于知识图谱的推荐系统介绍
KG 推荐系统家庭的简要介绍
·
关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 4 月 1 日
--
图片来源。该图像可以在 Pixabay 的内容许可下免费使用。
欢迎回来阅读我们系列文章中的第二篇,关于基于知识图谱的推荐系统。在我们的前一篇文章中,我们通过利用图谱的力量来结构化相关数据,提供了推荐过程的新视角。通过将数据组织成图谱格式,推荐任务被视为链接预测任务。
在这篇文章中,我们首先介绍知识图谱以及它们如何在推荐系统中用于提供更准确和个性化的推荐。然后,我们将深入探讨这个话题,探索基于知识图谱的推荐系统的一些关键组件。我们将讨论数据如何在知识图谱中收集和组织,如何用它来生成推荐,以及开发这样的系统中涉及的一些挑战。
知识图谱(KGs)
“知识图谱”这一术语由谷歌于 2012 年推广,用以描述其图结构的知识库,包含数亿个实体和关系。自 2012 年以来,它在公司的搜索引擎中发挥了重要作用,通过显示包含用户查询中提到的实体的特定信息的侧边栏,提供详细的答案。更一般地说,知识图谱是一个图结构的知识库,以实体(或文字值)之间的关系的形式存储事实信息,能够建模现实世界的实体及其关系,因此为搜索引擎、自然语言理解系统以及最近的推荐系统提供支持。
下图展示了一个知识图谱的可视化表示,包含了关于电影、演员和电影用户的多样信息,这些被视为实体。这些实体之间的关系通过表示它们各种关系的链接来建立,比如‘isFanOf’,‘HasGenreOf’或‘isDirectorOf’。这种知识表示方式使系统能够推断实体之间的连接,并生成更相关和个性化的推荐。通过利用这些连接的力量,基于知识图谱的推荐系统可以在向用户推荐相关项目时提供无与伦比的准确性和精确度。
基于知识图谱的推荐系统
近年来,知识图谱在推荐系统中的应用已被用来克服用户-项目交互稀疏性和冷启动问题,这些是协同过滤方法所面临的问题,通过利用关于项目和用户的属性,并将其表示在一个单一的数据结构中。
下图描绘了一个电影知识图谱,并展示了如何为用户u₂提供基于知识图谱的推荐。
如何生成推荐?红色虚线被生成到“罗密欧与朱丽叶”这个项目上,因为用户u₂观看了电影“泰坦尼克号”,而“泰坦尼克号”由“莱昂纳多·迪卡普里奥”主演,“莱昂纳多·迪卡普里奥”也出演了“罗密欧与朱丽叶”。图片来源:arxiv.org/pdf/1906.09506.pdf
,作者修改。
超越之前版本推荐系统已经管理的简单属性列表,知识图谱表示并利用实体之间语义丰富的关系。由于其结构,知识图谱可以很容易地相互链接。例如,将图中显示的图谱扩展以包含电影的主要特征是非常直接的。知识图谱推荐系统的一个显著特点是其利用知识图谱结构提供更好推荐的能力[1]。
一般来说,现有的基于知识图谱的推荐可以分为两大类:
- 基于嵌入的方法,作为知识图谱推荐系统的一种子类,通过使用知识图谱嵌入算法[2]对知识图谱进行预处理,然后将学习到的实体嵌入融入到推荐框架中。利用知识图谱嵌入算法,现在可以将几乎任何类型的信息转化为系统可以学习的向量。例如,[3]中,Zhang 等人提出了一种两阶段的方法,首先计算来自包含结构知识、图像和文本的知识库的嵌入,然后将生成的嵌入用作协同过滤算法的输入。在另一项研究[4]中,Zhang 等人利用关于项目的各种类型的附加信息(例如评论、品牌、类别、一起购买等)来构建一个知识图谱,随后用于构建项目和用户的嵌入。作为第二步,推荐系统按 uᵢ和 vⱼ之间的距离升序排列候选项目 j。总之,许多基于嵌入的方法由两个阶段组成:首先,学习实体(项目、用户等)的嵌入;其次,将嵌入融入推荐学习算法中。
基于嵌入的方法的示意图。协同过滤推荐系统利用来自各种数据源的潜在向量(嵌入)。图片来源:Zhang 等人
- 基于路径的方法探索“知识图谱中项之间连接的各种模式,以提供额外的推荐指导”。然而,它们严重依赖于手动设计的元路径,这在实践中很难优化。在[5]中,Yu 等人使用矩阵分解方法计算异质知识图谱中不同子图的实体潜在表示,然后使用聚合方法将所有生成的潜在表示进行分组,以计算推荐概率。受到[5]中提出的工作的启发,Zhao 等人将知识图谱视为异质信息网络(HIN)。他们提取基于路径的潜在特征,以表示用户与项之间通过不同关系路径的连接。这些方法的缺点是它们通常需要专家知识来定义元路径的类型和数量。随着深度学习算法的发展,已经提出了不同的模型,通过嵌入自动编码知识图谱元路径,以克服上述提到的限制。
不同元路径下的用户偏好讨论。图片来源: Yu 等人。
另一类值得一提的知识图谱推荐系统。这类推荐系统考虑了整个知识图谱的结构,而不仅仅是知识图谱三元组。RippleNet,一个“自然将知识图谱融入推荐系统的端到端框架”,在[7]中提出。“类似于水面上的实际涟漪,RippleNet 通过自动和迭代地沿知识图谱中的链接扩展用户的潜在兴趣,激发用户偏好的传播”。
电影“阿甘正传”在知识图谱中的涟漪集示意图。 同心圆表示不同跳数的涟漪集。渐变的蓝色表示中心与周围实体之间相关性的减少。请注意,实际中不同跳数的涟漪集不一定是互不相交的。图片来源: Wang 等人。
结论
总之,知识图谱通过以图形格式表示实体及其关系,为推荐系统提供了一种强大的数据结构化方式。已提出了不同的知识图谱推荐系统方法,包括基于路径的方法、基于嵌入的方法和基于图卷积网络的方法。这些方法根据知识图谱的大小和复杂性提供了不同程度的准确性和可扩展性。尽管开发这些系统存在明显的挑战,但基于知识图谱的推荐系统在为用户提供更准确和个性化的推荐方面具有巨大潜力。
在本系列的下一篇文章中,我们将深入探讨 KG 嵌入算法的话题。我们将讨论这些算法如何用于生成嵌入,以捕捉知识图谱中实体及其关系的语义意义。这将帮助更好地理解 KG 嵌入的生成方式以及它们为何对推荐系统有益。敬请关注下一篇文章!
参考文献:
[1] 孙珠、杨杰、张杰、亚历山德罗·博赞、黄龙凯和徐驰。递归知识图谱嵌入以实现有效推荐。在第 12 届 ACM 推荐系统会议上,RecSys ’18,页面 297–305,美国纽约,2018. 计算机协会。
[2] 安托万·博尔德斯、尼古拉斯·乌苏尼耶、阿尔贝托·加西亚-杜兰、贾森·韦斯顿和奥克萨娜·亚赫嫩科。翻译嵌入以建模多关系数据。在 C. J. C. 伯吉斯、L. 博图、M. 韦林、Z. 加赫拉马尼和 K. Q. 温伯格主编的《神经信息处理系统进展》一书中,第 26 卷,内华达州湖塔霍,美国,2013. 柯伦协会,Inc.
[3] 张福正、袁靖、连德富、谢兴和马伟颖。用于推荐系统的协同知识库嵌入。在第 22 届 ACM SIGKDD 国际知识发现与数据挖掘大会上,页面 353–362,美国纽约,2016. AC
[4] 张永峰、艾青尧、陈旭和王鹏飞。基于知识库嵌入的推荐学习。CoRR, abs/1803.06540, 2018。
[5] 肖宇、任翔、孙逸舟、顾全全、布拉德利·斯图尔特、乌尔瓦希·坎德尔沃尔、布兰登·诺里克和韩家炜。个性化实体推荐:一种异构信息网络方法。在第 7 届 ACM 国际网络搜索与数据挖掘大会上,WSDM ’14,页面 283–292,美国纽约,2014. 计算机协会。
[6] 赵欢、姚全名、李建达、宋扬秋和李迪伦。基于元图的异构信息网络推荐融合。在第 23 届 ACM SIGKDD 国际知识发现与数据挖掘大会论文集中,KDD ’17,页面 635–644,美国纽约,2017. 计算机协会。
[7] 王洪伟、张福正、王佳林、赵淼、李文杰、谢兴和郭敏仪。2018. RippleNet:在知识图谱上传播用户偏好以实现推荐系统。在第 27 届 ACM 国际信息与知识管理大会(CIKM ‘18)上。计算机协会,美国纽约,页面 417–426。
PySpark 中的逻辑回归简介
原文:
towardsdatascience.com/introduction-to-logistic-regression-in-pyspark-9f894299c32d
在 Databricks 中运行第一个分类模型的教程
·发布于 Towards Data Science ·10 min 阅读·2023 年 11 月 4 日
--
图片由 Ibrahim Rifath 提供,来源于 Unsplash
介绍
大数据。大数据集。云……
这些词无处不在,跟随我们,出现在客户、面试官、经理和主管的思维中。随着数据越来越丰富,数据集的规模也不断扩大,有时在本地环境中运行机器学习模型是不可能的——换句话说,就是在单台机器上。
这个问题要求我们适应并找到其他解决方案,例如使用 Spark 建模,这是最常用的大数据技术之一。Spark 接受 SQL、Python、Scala、R 等语言,并具有自己的方法和属性,包括自己的机器学习库 [MLlib]。例如,当你在 Spark 中使用 Python 时,这叫做 PySpark。
此外,还有一个平台叫做 Databricks,它将 Spark 包装在一个非常好的层中,使数据科学家可以像使用 Anaconda 一样使用它。
在 Databricks 中创建机器学习模型时,它也接受 Scikit Learn 模型,但由于我们更关注大数据,本教程完全使用 Spark 的 MLlib 创建,这更适合大数据集,同时也为我们的技能库添加了一种新工具。
开始吧。
数据集
本练习的数据集已经在 Databricks 中。这是一个 UCI 数据集中的成人数据,提取自人口普查,并标记为年收入低于或高于$50k。数据在以下地址以创作共享许可证公开提供:archive.ics.uci.edu/dataset/2/adult
我们的教程是构建一个二元分类器,用于判断一个人年收入是否高于或低于$50k。
数据集摘录。图片由作者提供。
编码
在这一部分,我们将详细介绍我们模型的每一个步骤。
下面是我们需要导入的模块。
from pyspark.sql.functions import col
from pyspark.ml.feature import UnivariateFeatureSelector
from pyspark.ml.feature import RFormula
from pyspark.ml.feature import StringIndexer, VectorAssembler
from pyspark.ml import Pipeline
from pyspark.ml.feature import OneHotEncoder
from pyspark.ml.classification import LogisticRegression
from pyspark.ml.evaluation import BinaryClassificationEvaluator
以及加载数据集。
# Pulling a dataset
from pyspark.sql.types import DoubleType, StringType, StructField, StructType
schema = StructType([
StructField("age", DoubleType(), False),
StructField("workclass", StringType(), False),
StructField("fnlwgt", DoubleType(), False),
StructField("education", StringType(), False),
StructField("education_num", DoubleType(), False),
StructField("marital_status", StringType(), False),
StructField("occupation", StringType(), False),
StructField("relationship", StringType(), False),
StructField("race", StringType(), False),
StructField("sex", StringType(), False),
StructField("capital_gain", DoubleType(), False),
StructField("capital_loss", DoubleType(), False),
StructField("hours_per_week", DoubleType(), False),
StructField("native_country", StringType(), False),
StructField("income", StringType(), False)
])
adults = spark.read.format("csv").schema(schema).load("/databricks-datasets/adult/adult.data")
准备数据
首先,我们需要准备数据。虽然这不是本教程的核心内容,但我们仍需处理空值,例如,这就是我们要做的。
从数据集文档中得知,NA 值实际上标记为“ ?”。我们将直接去掉它们,因为空值不多,因此不会影响我们模型的性能。因此,处理方法如下。任何列中出现“ ?”,我们都会去掉。
adults = (
adults
.filter( (col('workclass') != ' ?') &
(col('education') != ' ?') &
(col('marital_status') != ' ?') &
(col('occupation') != ' ?') &
(col('relationship') != ' ?') &
(col('race') != ' ?') &
(col('sex') != ' ?') &
(col('native_country') != ' ?')
)
)
很好。现在让我们选择模型中使用的最佳变量。
特征工程
由于我们有分类变量和数值变量,第一步是将它们分开成不同的数据集,以便使用 Spark 的UnivariateFeatureSelector
。该方法使用 Chi²检验选择具有分类标签的最佳分类变量,使用 ANOVA 选择具有分类标签的数值变量,这两种情况都适用。
首先选择最佳的分类变量。我们从隔离分类变量开始。然后我们使用RFormula
函数创建数据集的向量化版本。
# Adults cat only
adults_cat = adults.select('workclass', 'education', 'marital_status', 'occupation', 'relationship','race', 'sex','native_country', 'income')
# Creating a vector out of our dataset to be able to use Univariate Feature Selector
formula=RFormula(formula= "income ~ workclass + education + marital_status + occupation + relationship + race + sex + native_country",
featuresCol= "features", labelCol= "label")
vector_df = formula.fit(adults_cat).transform(adults_cat)
结果vector_df
看起来是这样的。注意最后几列features
和label
。这些是变量选择器所需的输入。
数据集的向量化版本。图片由作者提供。
然后我们创建一个选择器实例,输入features
和label
,如前所述。接着,我们设置特征和标签类型,并定义要提取多少个“最佳”变量,并将其拟合到数据的向量化版本中。
# instance the selector
selector = UnivariateFeatureSelector(featuresCol='features',
outputCol="selectedFeatures",
labelCol= 'label')
# Set feature and label types and define how many K variables to fetch
selector.setFeatureType("categorical").setLabelType("categorical").setSelectionThreshold(4)
model = selector.fit(vector_df)
# selectedFeatures
print('Selected Features - Categorical')
print([name for i,name in enumerate(vector_df.columns) if i in model.selectedFeatures])
-----
[OUT]
Selected Features - Categorical
['workclass', 'relationship', 'race', 'sex']
对于数值特征,步骤是相同的。
# Adults numerical only
adults_num = adults.select('age', 'fnlwgt', 'education_num', 'capital_gain', 'capital_loss', 'hours_per_week', 'income')
# Creating a vector out of our dataset to be able to use Univariate Feature Selector
formula=RFormula(formula= "income ~ age + fnlwgt + education_num + capital_gain + capital_loss + hours_per_week", featuresCol= "features", labelCol= "label")
vector_df = formula.fit(adults_num).transform(adults_num)
# # Using Variable selector for Feature Engineering (num)
selector = UnivariateFeatureSelector(featuresCol='features', outputCol="selectedFeatures", labelCol= 'label')
selector.setFeatureType("continuous").setLabelType("categorical").setSelectionThreshold(4)
model = selector.fit(vector_df)
#model.selectedFeatures
print('Selected Features - Numerical')
print([name for i,name in enumerate(vector_df.columns) if i in model.selectedFeatures])
-----
[OUT]
Selected Features - Numerical
['age', 'education_num', 'capital_gain', 'capital_loss']
好的。这是我们的最终数据集。
df_sel = adults.select('workclass', 'occupation', 'race', 'sex', 'age', 'education_num', 'capital_gain', 'capital_loss', 'income')
只有选定变量的数据。图片由作者提供。
由于我们有分类变量,我们将不得不创建虚拟变量,因为 Spark MLlib 中的逻辑回归模型只接受数字输入。接下来,我们来看看如何做到这一点。
转换数据
虚拟变量是一种将类别转换为数字以用于机器学习输入的方式。因此,有不同的方法来实现这一点。这里应用的方法是针对多类变量的独热编码。
这个过程将只应用于类别,因为数字已经是…好吧…数字。
为了使转换工作更方便,我们将利用 Pipeline
方法,该方法接受一个步骤列表并一次性执行所有步骤。
所以我们从列出我们选择的分类变量开始,然后创建一个空列表来存储 Pipeline 将要执行的阶段。接下来是一个循环,它将 cat_cols
列表中的每个变量传递给 StringIndexer
函数以进行类别索引。OneHotEncoder
进行最终转换。
这就是 StringIndexer
所做的:
应用于变量的字符串索引器。图片来源于作者。
这里是创建循环的代码。
# Columns to transform
cat_cols = ['workclass', 'occupation', 'race', 'sex']
# List of stages for Pipeline
stages = []
for column in cat_cols:
# Instance encoding with StringIndexer
stringIndexer = StringIndexer(inputCol=column, outputCol=column + "Index")
# Use OneHotEncoder to convert categorical variables into binary SparseVectors
encoder = OneHotEncoder(inputCols=[stringIndexer.getOutputCol()], outputCols=[column + "classVec"])
# Add stages. These are not run here, but will run all at once later on.
stages += [stringIndexer, encoder]
下一步是编码标签。在这种情况下,简单的字符串索引器就足够了,因为这是一个二分类标签。我们可以使用 StringIndexer
并将实例添加到 Pipeline 阶段。
# Convert label income into label indices using the StringIndexer
label_encode = StringIndexer(inputCol="income", outputCol="label")
# Add to the Pipeline stages
stages += [label_encode]
最后,我们将数值和分类变量一起添加。我们只需将字符串 'classVec' 添加到每个独热编码变量,并将所有列名作为 VectorAssembler
的输入,然后将其添加到 Pipeline 的 stages
列表中。
# Transform all features into a vector using VectorAssembler
num_cols = ['age', 'education_num', 'capital_gain', 'capital_loss']
assembler_cols = [c + "classVec" for c in cat_cols] + num_cols
assembler = VectorAssembler(inputCols=assembler_cols, outputCol="features")
stages += [assembler]
最后,我们一次性运行 Pipeline 转换。
pipe = Pipeline().setStages(stages)
pipe_model = pipe.fit(df_sel)
prepared_df = pipe_model.transform(df_sel)
如果你想查看结果,结果看起来是这样的。
转换后的数据。图片来源于作者。
这些数据现在已准备好输入到逻辑回归模型中。
模型训练
在建模之前,让我们将数据拆分为训练集和测试集。
# Split data into training and test sets
(train, test) = prepared_df.randomSplit([0.7, 0.3], seed=42)
print(train.count())
print(test.count())
现在我们可以将模型拟合到训练集。
# Fit model to prepped data
lrModel = LogisticRegression(labelCol= 'label',
featuresCol='features',
maxIter= 10).fit(train)
并创建一些预测。
# predictions
predictions = lrModel.transform(test)
preds = predictions.select("label", "prediction", "probability")
输出看起来是这样的。
来自 MLlib 的 LR 模型预测。图片来源于作者。
评估
为了评估我们的模型,我们使用 BinaryClassificationEvaluator
函数。
我们模型的准确率是 85%,这显然是公平的。
# Evaluate model
evaluator = BinaryClassificationEvaluator()
evaluator.evaluate(predictions)
Out[189]: 0.8526180736758168
这就是混淆矩阵。
# Confusion Matrix
display(
preds.crosstab('label', 'prediction')
)
模型的混淆矩阵。图片来源于作者。
在这里,我们已经看到召回率很低。或者说,实际收入超过 50k 的人中,我正确预测了多少。几乎是猜测,正确预测率只有 48%。尽管如此,真负率很高,为 93%。然而,这并不是我们模型的全部优点,因为类别不平衡,其中 75% 的观察被标记为类 0(<50k)。
print(f'Precision: {1075/(1075+438)}')
print(f'Recall: {1075/(1075+1151)}')
print(f'Specificity: {6324/(6324+438)}')
Precision: 0.7105089226701917
Recall: 0.4829290206648697
Specificity: 0.935226264418811
让我们尝试改进这个模型。
一种方法是使用交叉验证方法。我们首先创建一个参数网格,使用下面的代码。
from pyspark.ml.tuning import ParamGridBuilder, CrossValidator
logit = LogisticRegression(labelCol="label", featuresCol="features", maxIter=10)
# Create ParamGrid for Cross Validation
paramGrid = (ParamGridBuilder()
.addGrid(logit.regParam, [0.01, 0.5, 2.0])
.addGrid(logit.threshold, [0.35, 0.38])
.build())
然后我们可以训练模型并打印最佳参数。
# Create 5-fold CrossValidator
cv = CrossValidator(estimator=logit, estimatorParamMaps=paramGrid, evaluator=evaluator, numFolds=5)
# Run cross validations
cv_model = cv.fit(train)
print('cutoff:', cv_model.bestModel.getThreshold())
print('regParam:', cv_model.bestModel.getRegParam())
[OUT]
cutoff: 0.38
regParam: 0.01
我们可以使用最佳模型进行预测,或者如果你愿意,可以创建一个新模型。
logit = LogisticRegression(labelCol="label", featuresCol="features",
threshold=0.38, maxIter=100)
log_fit = logit.fit(train)
# Make predictions on test data using the transform() method.
preds2 = log_fit.transform(test)
# Confusion Matrix
display(
preds2.crosstab('label', 'prediction')
)
调优后的模型。图片来源于作者。
我们能够稍微改进模型。带来最大变化的超参数是threshold
,它将分类的截止点从 50%的概率切换开。查看第一个模型后,我们明显发现它的真正阳性率不高。因此,当我们将分类截止点调整到低于 50%的数值时,这意味着我们让分类观察为正样本变得“更容易”。另一方面,这种改变会增加假阳性率,但我们现在可以接受这种情况。以下是新的指标。
-
精确度: 0.63
-
召回率: 0.62
-
特异性: 0.88
我们还可以查看“ROC”指标。
display(log_fit, test, 'ROC')
模型的 ROC 曲线。图片由作者提供。
到此,我们结束本教程。
离开前
在本教程中,我们介绍了如何使用 Spark 的 MLlib 创建逻辑回归模型。这个工具可以利用集群计算能力处理大数据。
了解如何进行一些转换,比如将数据集转换为输入算法的向量,是非常重要的。
这是本练习的完整代码:
[## Studying/PySpark/101_MyLearning ML on DataBricks.ipynb at master · gurezende/Studying
这是一个包含我对新软件包测试和研究的代码库 - Studying/PySpark/101_MyLearning ML on DataBricks.ipynb……
我们也可以在 Databricks 中使用 Scikit Learn,但了解 MLlib,这一 Spark 模块,似乎更适合处理大数据。
如果你喜欢这些内容,不要忘记在这里以及在 LinkedIn 上关注我。
在 Medium 上阅读 Gustavo Santos 的文章。数据科学家。我从数据中提取见解,帮助个人和公司……
gustavorsantos.medium.com](https://gustavorsantos.medium.com/?source=post_page-----9f894299c32d--------------------------------)
参考
[## 提取、转换和选择特征
本节涵盖了处理特征的算法,大致分为以下几组:提取:提取……
spark.apache.org](https://spark.apache.org/docs/latest/ml-features.html?source=post_page-----9f894299c32d--------------------------------#stringindexer) [## One-hot - Wikipedia
来自维基百科,自由百科全书 在数字电路和机器学习中,one-hot 是一组位……
en.wikipedia.org [## 在 Databricks 上使用 Apache Spark MLlib
学习如何在 Databricks 中使用 Apache Spark MLlib Pipelines API 训练机器学习模型。分类…
docs.databricks.com [## 在 Databricks 上使用 Apache Spark MLlib
学习如何在 Databricks 中使用 Apache Spark MLlib Pipelines API 训练机器学习模型。分类…
docs.databricks.com [## LogisticRegression - PySpark 3.5.0 文档
setParams(self, *, featuresCol="features", labelCol="label", predictionCol="prediction", maxIter=100, regParam=0.0…
马尔科夫链蒙特卡罗 (MCMC) 方法介绍
原文:
towardsdatascience.com/introduction-to-markov-chain-monte-carlo-mcmc-methods-b5bad18bc243
马尔科夫链、Metropolis-Hastings 算法、Gibbs 采样,以及它们与贝叶斯推断的关系
·发表于 Towards Data Science ·阅读时间 14 分钟·2023 年 2 月 21 日
--
照片由 Edge2Edge Media 在 Unsplash 上提供
本文是对马尔科夫链蒙特卡罗 (MCMC) 采样方法的介绍。我们将特别考虑两种方法,即 Metropolis-Hastings 算法和 Gibbs 采样。我们将介绍这些方法并证明它们的有效性,提供 Python 中的实际示例,最后解释采样如何应用于贝叶斯推断以及其重要性。
介绍
MCMC 方法是一类采样方法,利用马尔科夫链生成依赖的数据样本。其基本思想是构建这样一种马尔科夫链,使得采样简单,而其平稳分布即为我们的目标分布——这样当跟随这些链时,最终我们能从目标分布中获得样本。
为什么我们需要这个?在之前的一篇文章中,我介绍了基本的采样方法,其中包括了复杂分布的拒绝采样和重要性采样。这些方法生成独立的数据样本,而这里我们生成的是依赖的数据样本,如前所述——这并没有回答之前的问题,但这是一个重要的区别。然而,在之前的文章中,我们看到所提出的方法存在严重的限制:很难找到合适的建议分布,特别是在高维空间中,这会导致高方差和浪费计算。
在这些情况下,MCMC 方法,即遵循(简单的)马尔可夫链表现更好,特别是由于对我们要采样的分布所需的信息较少,以及我们只需要能够评估到一个固定因子的事实。也就是说:我们不需要能够评估给定x
的完整 pdf,p(x)
,但能够计算zp(x)
就足够了。在本文的最后,我们将通过应用它来解决一个贝叶斯推断问题,来看到它为何如此强大。在许多教程和解释中,这最后一点通常只是作为附带说明简要提及——但我认为这值得——特别是对于贝叶斯推断的初学者——更多关注。
当然,MCMC 方法也有其缺点:由于样本之间的相关性,有效样本大小会减少,并且方法有时可能不收敛或非常慢。
马尔可夫链
由于正如名字所示(而且我们到目前为止已经多次提到过),MCMC 方法基于 马尔可夫链,因此我们首先介绍这些方法。
这些是将随机过程建模为事件序列的一种方法。在这其中,马尔可夫性质指出下一个状态仅依赖于当前状态,而不依赖于任何历史信息。
(小插曲:许多实际的机器学习方法要求这一属性,例如强化学习。要求这种一步依赖可能看起来非常限制和不切实际——但请注意,我们可以简单地将状态空间扩展到任意维度,特别是将过去的事件包含在当前状态中——从而完全绕过这种“限制”。)
形式上,设我们考虑一个随机变量X
,并用X₀
、X₁
等表示其每个时间步的实现。X
随时间的发展由一个转移函数P
给出,其中
表示X
从状态i
转移到状态j
的概率是p
。
要完全指定一个马尔可夫链,此外我们还需要定义一个初始分布π₀
。有了这个,我们可以从π₀
开始,迭代应用P
,得到每个时间戳的分布π₁
、π₂
等。
让我们通过一个例子来可视化这一点。我们选择了以下转移矩阵:
请注意,在我们的符号中,索引ij
表示从状态j
到i
的转移概率,出于方便考虑。
我们现在取一个随机初始分布,并跟踪马尔可夫链的 30 步。可以通过以下 Python 代码实现:
import numpy as np
P = np.asarray([[0.3, 0.5, 0.75], [0.1, 0.1, 0.1], [0.6, 0.4, 0.15]])
print(f"Transition matrix P: {P}")
# Generate random initial distribution (normalize to obtain valid distribution).
pi = np.random.rand(3)
pi /= np.sum(pi)
print(f"Initial distribution pi_0: {pi}")
for i in range(30):
pi = np.matmul(P, pi)
if i % 5 == 0:
print(f"Distribution after i steps: {pi}")
执行此程序时,我们将得到类似于以下的输出:
Distribution after i steps: [0.51555326 0.1 0.38444674]
Distribution after i steps: [0.499713 0.1 0.400287]
Distribution after i steps: [0.5000053 0.1 0.3999947]
Distribution after i steps: [0.4999999 0.1 0.4000001]
Distribution after i steps: [0.5 0.1 0.4]
Distribution after i steps: [0.5 0.1 0.4]
如我们所见,这个马尔可夫链会收敛——对于任何初始分布——到分布 [0.5, 0.1, 0.4]
——我们称之为该马尔可夫链的平稳分布。
在继续之前,我们将介绍一个标准,在后续章节中需要用来判断马尔可夫链是否收敛:详细平衡。我们说一个马尔可夫链满足详细平衡标准,如果存在一个分布 π
满足:
即,从状态 j
转移到状态 i
的概率与考虑分布 π
的反向转移概率相同。直观上这也应该是合理的,因为这解释了为什么会产生一个平稳分布。请随意确认上述定义的马尔可夫链是否满足该标准,并且确实 [0.5, 0.1, 0.4]
是满足它的分布。
Metropolis-Hastings
凭借这些知识,我们现在描述并介绍一种最常见和最常用的 MCMC 算法,即 Metropolis-Hastings 算法。总而言之,我们要做的是从一个难以处理的概率分布 f(x)
(目标分布)中采样值。
让我们从算法概述开始。实际上,它由以下步骤组成:
-
在目标分布的支持范围内选择一个任意初始值
x₀
-
使用提议分布
q
抽取y₁
-
计算
p₁
(见下文) -
从均匀分布
[0, 1]
中抽取u₁
-
如果
u₁ ≤ p₁
,则设置x₁ = y₁
,否则设置x₁ = x₀
-
重复步骤 2-5
p₁
由以下给出:
示例
让我们用一个具体的例子来演示这个过程,使用 Python 实现。设置是:我们想要采样的目标分布是一个高斯分布。我们的提议分布是另一个高斯分布。这自然并不是一个现实世界的实际例子。然而,我相信并希望,这种简化的设置有助于理解,而不是使读者感到困惑。请注意,在这个例子中,所有感兴趣的值都是一维的。
对应的 Python 代码如下:
import matplotlib.pyplot as plt
import numpy as np
import scipy.stats
NUM_SAMPLES = 10000
# Target distribution
f = scipy.stats.norm(5, 2)
# Plot target distribution
x = np.linspace(-5, 15, 5000)
plt.plot(x, f.pdf(x))
# Step 1
x = np.random.uniform(-2, 2)
# Proposal distribution
q = scipy.stats.norm(0, 1)
samples = []
for i in range(NUM_SAMPLES):
# Step 2
y = x + q.rvs()
# Step 3
p = min(f.pdf(y) / f.pdf(x) * q.pdf(x - y) / q.pdf(y - x), 1)
# Step 4
u = np.random.uniform(0, 1)
# Step 5
x = y if u <= p else x
samples.append(x)
plt.hist(samples, density=True, bins=30)
plt.show()
让我们详细了解一下。在开始时,我们使用 scipy 的 stats 模块来表示目标分布 f
—— 然后绘制其概率密度函数(pdf)。接着我们定义一个初始值 x
来开始采样 —— 仅仅是从均匀分布中生成一个值。然后我们进入采样循环,根据上述介绍的算法迭代生成 NUM_SAMPLES
个值。作为提议分布,我们使用另一个高斯分布 q
—— 这会生成一个新值 y
,通过在 x
周围按照此高斯分布“跳跃”获得。值得注意的是,q
的条件评估等于给定跳跃范围的 q
的 pdf —— 直观地说,跳得越远,可能性就越小。
执行此程序应产生类似以下的结果:
我们看到我们正确地从“未知”分布 f
中采样。
正确性证明
要证明 Metropolis-Hastings 算法的正确性,我们需要展示所使用的马尔可夫链的平稳分布确实是目标分布。为此,我们使用了上述介绍的详细平衡符号。记住,这涉及到展示以下内容:
即无论我们是首先访问状态(t-1)
然后转移到(t)
,还是反之,都没有关系。
因此,让我们评估这个方程的左侧,并简单地代入我们的提议分布和接受标准:
一个快速的重新表述为:
当对上述目标方程的右侧进行类似操作时,我们得到相同的结果,从而完成证明。
讨论
我们在引言中提到 MCMC 方法,如 Metropolis-Hastings,比例如拒绝抽样更优越和高效。这是正确的,但我们仍应努力选择q
,因为这一选择将影响转换的速度。再考虑一下接受标准:
如果我们将q
定义为等于f
,我们将得到 1——即接受所有样本,这是理想情况。自然,这是不可能的,因为我们无法从f
中抽样(这也是我们要这样做并从q
中抽样的原因)。不过,这提供了如何选择q
的一些直觉。相反,如果q
选择不当,我们将拒绝许多样本,得到多个高度相关的样本,这是一个问题(链条在某个区域“卡住”了)。
这些讨论与有效样本大小和烧入的术语相关。由于 MCMC 方法生成的是相关样本,而不是独立样本,在研究这些时我们必须考虑这一点。特别地,这产生了有效样本大小的术语——它可以被视为“清除”了相关效应的实际样本大小。此外,通常会丢弃由 MCMC 算法获得的前N
个元素(烧入):这主要是为了平衡“坏”的初始化,这些初始化位于低概率区域,并且提议分布难以摆脱这些区域。
Gibbs 抽样
作为 MCMC 抽样方法的第二个例子,我们将看看吉布斯抽样。由于我们已经介绍了基本概念并证明了一种 MCMC 方法的正确性,这次我们会快得多——但我还是想把它放在这里,以达到教程的足够深度,并建议读者参考其他资源以获取更多细节,或者自己进行计算。
概述
Gibbs 采样用于从具有多个变量的分布中进行采样,当从联合分布 p(X, Y)
采样困难时,我们可以采样条件分布 p(X | Y)
、p(Y | X)
。利用这一点,所使用的马尔科夫链在 X
和 Y
的采样值之间迭代,并利用更新后的条件分布。因此,整体上 — 引入和实现相当迅速 — 我们将在下一节中进行详细探讨。
为了引入,我们将考虑一个二维的 多元正态分布。这种多维正态分布由均值向量 μ
和协方差矩阵 Σ
特征化。方便的是,所需的条件分布也是正态分布,且(以 x₁
为例)定义为:
实现
首先,让我们使用 scipy.stats
来定义并绘制我们的目标分布:
from typing import Any
import matplotlib.pyplot as plt
import numpy as np
import numpy.typing as npt
import scipy.stats
MEAN = np.asarray([0, 0])
VARIANCE = np.asarray([[0.25, 0.3], [0.3, 1]])
def plot_multivariate(
mean: npt.NDArray[np.float32], variance: npt.NDArray[np.float32]
) -> None:
multivariate_normal = scipy.stats.multivariate_normal(mean, variance)
num_ticks = 100
min_axis_value = -5
max_axis_value = 5
x = np.linspace(min_axis_value, max_axis_value, num_ticks)
y = np.linspace(min_axis_value, max_axis_value, num_ticks)
X, Y = np.meshgrid(x, y)
pos = np.array([X.flatten(), Y.flatten()]).T
fig = plt.figure()
ax = fig.add_subplot(projection="3d")
ax.plot_surface(
X,
Y,
multivariate_normal.pdf(pos).reshape((100, 100)),
cmap="viridis",
linewidth=0,
)
plt.show()
plot_multivariate(MEAN, VARIANCE)
我们应该得到类似这样的结果:
接下来,我们按照上述描述执行 Gibbs 采样过程:
def get_cond_distr_x(
mean: npt.NDArray[np.float32], variance: npt.NDArray[np.float32], y: float
) -> Any:
mean = mean[0] + variance[0, 1] * 1 / variance[1, 1] * (y - mean[1])
var = variance[0, 0] - variance[0, 1] * 1 / variance[1, 1] * variance[1, 0]
return scipy.stats.norm(mean, var)
def get_cond_distr_y(
mean: npt.NDArray[np.float32], variance: npt.NDArray[np.float32], x: float
) -> Any:
mean = mean[1] + variance[1, 0] * 1 / variance[0, 0] * (x - mean[0])
var = variance[1, 1] - variance[1, 0] * 1 / variance[0, 0] * variance[0, 1]
return scipy.stats.norm(mean, var)
def gibbs_sampling(
mean: npt.NDArray[np.float32],
variance: npt.NDArray[np.float32],
num_samples: int = 50000,
) -> npt.NDArray[np.float32]:
xs = []
ys = []
x = 0
for i in range(num_samples):
y = get_cond_distr_y(mean, variance, x).rvs()
x = get_cond_distr_x(mean, variance, y).rvs()
xs.append(x)
ys.append(y)
return np.stack((xs, ys)).transpose(1, 0)
sampled_points = gibbs_sampling(MEAN, VARIANCE)
最终,我们通过绘制 3D 直方图来验证获得的分布是正确的:
def draw_3d_hist(points: npt.NDArray[np.float32]) -> None:
# Taken from https://matplotlib.org/stable/gallery/mplot3d/hist3d.html.
fig = plt.figure()
ax = fig.add_subplot(projection="3d")
hist, xedges, yedges = np.histogram2d(
points[:, 0], points[:, 1], bins=50, range=[[-5, 5], [-5, 5]]
)
# Construct arrays for the anchor positions of the 16 bars.
xpos, ypos = np.meshgrid(
xedges[:-1] + 0.25, yedges[:-1] + 0.25, indexing="ij"
)
xpos = xpos.ravel()
ypos = ypos.ravel()
zpos = 0
# Construct arrays with the dimensions for the bars.
dx = dy = 0.5 * np.ones_like(zpos)
dz = hist.ravel()
ax.bar3d(xpos, ypos, zpos, dx, dy, dz, zsort="average")
plt.show()
draw_3d_hist(sampled_points)
贝叶斯推断中的应用
为了总结这篇文章,让我们举一个实际的使用案例,在这个案例中,这种采样变得非常方便,并帮助解决重要问题:贝叶斯推断。我们将在未来的帖子中详细介绍这一点,目前:这是解决给定问题的“完整”概率分布,特别是计算给定数据的参数的概率分布:
这些术语通常被称为:
使解决这个方程特别具有挑战性的是证据,因为这需要对所有可能的参数值进行边际化,即:
这个积分通常很难计算,甚至不可解。
因此,以 MCMC 方法的形式进行数值近似是一个很好的起点,并且是解决此类问题的常见选择。像 Metropolis-Hastings 这样的方法特别有用,因为它们只要求能够评估分布直到某个归一化常数 — 证据恰好就是这样的常数!这意味着我们可以制定并处理后验分布,而不需要处理复杂的分母部分。
让我们用一个例子来说明:我们将抛掷一个(不公平的)硬币 N
次,并且有兴趣找出硬币正面朝上的概率 θ
。特别地,我们不仅仅希望得到一个点估计,而是采用贝叶斯方法,建模完整的后验分布。
让我们更详细地分析一下单个术语:抛硬币的结果遵循 伯努利分布,并且,记 Nₕ
为观察到的正面次数,Nₜ
为相应的反面次数,对于给定的参数值 θ
,这产生以下似然函数:
接下来,我们需要找到一个合适的先验——即对估计参数值引入某种信念。由于我们不必担心解析地解决问题(见:共轭先验),我们可以自由选择任何先验。因此,我们简单地选择均值为 0.5、标准差为 0.2 的正态分布——表示我们期望硬币大约是 50:50,但也覆盖 [0, 1] 的所有情况。
这两个术语足以运行 Metropolis-Hastings 算法。在其中,我们需要计算某些参数值 y
和 x
的 f(y)/f(x)
,以及我们感兴趣的密度函数 f
。在我们的例子中,正如前面提到的,这是 p(θ|x)
。我们也已经提到,证据部分会被抵消,因为这是一个常数。剩下的就是以下内容:
通过这些评估和上述对 Metropolis-Hastings 算法的介绍,将其移植到 Python 中应该不会太困难:
import matplotlib.pyplot as plt
import numpy as np
import scipy.stats
NUM_THROWS = 100 # Number of coin tosses
THETA_TRUE = 0.3 # True probability for landing heads
THETA_PRIOR = 0.5 # Prior estimate for heads probability
NUM_SAMPLES = 100000 # Number of MCMC steps
# Define the unfair coin and generate data from it
unfair_coin = scipy.stats.bernoulli(THETA_TRUE)
D = np.asarray([unfair_coin.rvs() for _ in range(NUM_THROWS)])
# Define prior distribution
prior = scipy.stats.norm(THETA_PRIOR)
def likelihood_ratio(theta_1, theta_2):
return (theta_1 / theta_2) ** np.sum(D == 1) * (
(1 - theta_1) / (1 - theta_2)
) ** np.sum(D == 0)
def norm_ratio(theta_1, theta_2):
return prior.pdf(theta_1) / prior.pdf(theta_2)
# Step 1
x = np.random.uniform(0, 1)
# Proposal distribution
q = scipy.stats.norm(0, 0.1)
samples = []
for i in range(NUM_SAMPLES):
# Step 2
y = x + q.rvs()
# Step 3
ratio = likelihood_ratio(y, x) * norm_ratio(y, x)
p = min(ratio * q.pdf(x - y) / q.pdf(y - x), 1)
# Step 4
u = np.random.uniform(0, 1)
# Step 5
x = y if u <= p and 0 <= y <= 1 else x
samples.append(x)
plt.hist(samples, density=True, bins=100)
plt.show()
运行这段代码应该会输出类似的内容:
正如我们所见,该算法正确地找到了约为 0.3 的真实θ
,这从后验分布的模式中可以看出。我们还可以观察到一些方差,这很好,实际上也是预期中的——这就是我们进行全面贝叶斯推断的原因之一。抛硬币“仅”100 次可以给我们一个对其真实翻转概率的良好初步估计,但为了更加确定,我们希望看到更多的例子。
所以让我们将数据集增加到 5000 次抛掷,并检查这种情况下的输出:
现在,后验分布的方差确实比预期的要低得多。
结论
在这篇文章中,我们介绍了马尔科夫链蒙特卡罗(MCMC)方法,这是一种用于数值采样的强大方法。这些方法使我们能够高效地从复杂分布中进行采样,而对可处理性的要求并不严格:特别是,我们只需要能够评估感兴趣的分布到固定因子的程度。MCMC 方法通过生成马尔科夫链来工作,其平稳分布就是目标分布——我们可以跟踪这个分布,并有效地采样我们所需要的分布。
作为第一个算法,我们介绍了 Metropolis-Hastings,并证明了它的正确性。它通过引入一个提议分布来工作,该分布用于从当前点“跳”到一个新点。这个新点以一定的概率被接受,这个概率与这些点周围的概率密度比成正比。
接下来,我们讨论了吉布斯抽样,这是一种用于抽样多维分布的方法。核心思想是使用条件分布,并通过为每个维度抽样新值,同时保持其他维度固定来迭代。
最终,我们给出了一个贝叶斯推断的实际例子。分析性地解决这个问题需要解一个复杂的积分,这使得它成为一个典型的(而且非常常见的)数值逼近示例。我们展示了如何通过模拟不公平抛硬币来估计伯努利变量的后验分布。
我希望这篇文章对你有帮助,并对这个令人兴奋的领域提供了一些见解。感谢阅读!
所有图像,除非另有说明,均由作者生成。
本文是关于抽样的系列文章的第三部分。你可以在这里找到其他部分:
-
第一部分:抽样方法简介
-
第二部分:通过重要性抽样减少方差
Python 中的数学优化介绍
原文:
towardsdatascience.com/introduction-to-mathematical-optimisation-in-python-b88e6556f9e
数据科学基础
Python 中离散优化的初学者实用指南
·发表于 Towards Data Science ·10 min 阅读·2023 年 12 月 2 日
--
数据科学家使用数据和各种技术解决广泛的实际问题。数学优化是一种强大的技术,可以应用于许多领域的广泛问题,是数据科学家工具箱中的一项重要投资。在这篇实用的入门文章中,我们将熟悉 Python 中的三个流行优化库:Google 的 OR-Tools、IBM 的 DOcplex 和 COIN-OR 基金会的 PuLP
图片由 Akhilesh Sharma 提供,来源于 Unsplash
📍 概述
数学优化是关于在预定义的范围内为定量问题找到最优选择。它有三个组成部分:
-
目标函数: 告诉我们一个解决方案的好坏,并允许我们比较不同的解决方案。一个最优解决方案是根据使用情况最大化或最小化目标函数的解决方案。
▶ ️在某些情况下,可能会有多个目标函数。这会增加确定最优解的复杂性。
▶ ️在某些情况下,可能没有目标函数。这种优化问题称为可行性问题。
-
决策变量: 代表我们想要找出的值或值的集合,即我们在定量问题中寻找的答案。根据决策变量的类型,优化可以分为两种:
▶️ 离散优化: 决策变量是离散的。分配时间表和寻找两个地点之间的最短旅行路径是一些离散优化的例子。如果你想了解更多关于离散优化的信息,这个课程和/或这个指南 可能会对你有兴趣。
▶️ 连续优化: 决策变量是连续的。你可能已经听说过机器学习中的优化术语。机器学习是使用连续优化的一个例子。如果你想了解更多关于连续优化的信息,你可以参考这个教程。
-
约束条件: 定义了决策变量的可行解范围。
▶️ 在一些连续优化问题中,可能没有约束条件。这被称为无约束优化。
在本教程中,我们将专注于具有单一目标的离散优化。在这种类型的优化问题中,我们希望找到决策变量的可行值组合,以使其在满足约束条件的情况下最小化或最大化目标函数。
📍 示例
在了解了基本知识后,让我们看一个例子,然后再深入了解 Python。我们将研究一个足够简单的业务案例,以便手动找到最佳解决方案。这样,我们可以在不感到过于困惑的情况下熟悉这些库。
📌 背景
▶️ 假设我们运营一家时尚工厂,生产三种物品:围巾、帽子和手套。每种物品有一个设计和一个尺寸。假设我们知道每月的市场需求:
作者提供的图片
▶️ 我们拥有有限数量的员工和机器。下表显示了生产一个单位物品所需的劳动(员工)和生产(机器)时间。例如,生产一条围巾需要 0.4 小时的劳动时间和 0.1 小时的生产时间。表中还显示了我们一个月内可用的总劳动和生产时间。最后,我们可以在最后一行看到每件物品的利润。
作者提供的图片
▶️ 我们的业务目标是确定生产多少围巾、帽子和手套,以最大化我们的利润。
现在让我们把这个业务问题分解为三个组件。
▶️ 决策变量: 所有决策变量必须是整数。
作者提供的图片
▶️ 目标函数: 我们有一个目标函数。
作者提供的图片
▶️ 约束条件: 虽然没有明确说明,我们不希望生产任何超过市场需求的物品。否则,这将导致我们有未售出的物品。我们还需要确保生产所需的资源在劳动和生产限制范围内,换句话说,即可行。
作者提供的图片
📌 手动解决方案
一种解决这个问题的幼稚方法是从需求范围内选择随机数字,检查它们是否可行并比较它们的利润。例如,5,000 条围巾、4,000 顶帽子和 6,000 双手套。然而,以这种方式比较少量解决方案可能会导致利润不尽如人意。为了彻底,我们可以使用暴力方法遍历所有组合(7,001 x 9,001 x 8,001 ≈ 5000 亿)。然而,这种方法似乎效率低下、繁琐且计算成本高。肯定还有更聪明的方法。
相反,我们可以通过应用简单的逻辑/常识直观地解决这个问题。我们将找到带来最高利润的物品。
作者提供的图片
上表显示手套是最有利可图的物品。用 1.2 小时的劳动时间和 0.3 小时的生产时间,我们从手套中赚取$48,从围巾中赚取$45,从帽子中赚取$0。由于资源充足,我们将生产最大数量的手套(8,000)。在剩余资源下,我们将比较两种选择:优先生产围巾还是帽子。
作者提供的图片
我们发现优先生产围巾更有利可图。因此,我们将生产 7,000 条围巾、5,000 顶帽子和 8,000 双手套以最大化利润。
📍 Python 中的优化
现在,我们将使用优化算法在 Python 中利用三种不同的优化库高效地找到最佳解决方案。在深入研究这些库之前,让我们将关键信息保存到字典中:
demand = {'scarves': 7000, 'hats': 9000, 'gloves': 8000}
labour = {'scarves': 0.4, 'hats': 0.3, 'gloves': 0.2, 'limit': 6000}
production = {'scarves': 0.1, 'hats': 0.18, 'gloves': 0.05, 'limit': 2000}
profit = {'scarves': 15, 'hats': 12, 'gloves': 8}
现在是时候了解一下求解器这个术语了,因为我们将经常使用这个术语。求解器是通过应用优化算法来解决优化问题的工具。现在,让我们深入了解一下。
📌 OR-Tools
我们要熟悉的第一个库是:谷歌的 OR-Tools。这里,OR 代表运筹学。这个优化工具是开源软件。我们将首先使用他们的 CP-SAT 求解器来找出我们的决策变量。这个求解器是一个使用 SAT(可满足性)方法的约束编程求解器。要了解更多关于求解器的信息,请查看这个大师班。
# Install library with: pip install ortools
# Load library
from ortools.sat.python import cp_model
# Initialise CP-SAT model
model = cp_model.CpModel()
# Set decision variables
scarves = model.NewIntVar(0, demand['scarves'], 'scarves')
hats = model.NewIntVar(0, demand['hats'], 'hats')
gloves = model.NewIntVar(0, demand['gloves'], 'gloves')
# Add constraints
model.Add(int(10*labour['scarves'])*scarves +
int(10*labour['hats'])*hats +
int(10*labour['gloves'])*gloves <= 10*labour['limit'])
model.Add(int(100*production['scarves'])*scarves +
int(100*production['hats'])*hats +
int(100*production['gloves'])*gloves <= 100*production['limit'])
# Set objective
model.Maximize(profit['scarves']*scarves +
profit['hats']*hats +
profit['gloves']*gloves)
# Solve model
solver = cp_model.CpSolver()
solver.Solve(model)
# Inspect output
print(f"Profit: ${solver.ObjectiveValue():,.2f}\n")
labour_cost = 0
production_cost = 0
for var in [scarves, hats, gloves]:
value = solver.Value(var)
name = var.Name()
labour_cost += value * labour[name]
production_cost += value * production[name]
print(f"{value:.0f} {name}")
print(f"\nLabour cost: ${labour_cost:,.2f}")
print(f"Production cost: ${production_cost:,.2f}")
作者提供的图片
在这里,我们确保在设置决策变量和约束时使用整数。我们还通过将劳动和生产成本乘以常数并转换其类型,同时保持其比例,来将其转换为整数。
现在,让我们使用线性求解器看看语法的对比:
from ortools.linear_solver import pywraplp
# Initialise linear solver with the SCIP backend
model = pywraplp.Solver.CreateSolver('SCIP')
# Set decision variables
scarves = model.IntVar(0, demand['scarves'], 'scarves')
hats = model.IntVar(0, demand['hats'], 'hats')
gloves = model.IntVar(0, demand['gloves'], 'gloves')
# Add constraints
model.Add(labour['scarves']*scarves +
labour['hats']*hats +
labour['gloves']*gloves <= labour['limit'])
model.Add(production['scarves']*scarves +
production['hats']*hats +
production['gloves']*gloves <= production['limit'])
# Set objective
model.Maximize(profit['scarves']*scarves +
profit['hats']*hats +
profit['gloves']*gloves)
model.Solve()
# Inspect output
print(f"Profit: ${model.Objective().Value():,.2f}\n")
labour_cost = 0
production_cost = 0
for var in [scarves, hats, gloves]:
value = var.solution_value()
name = var.name()
labour_cost += value * labour[name]
production_cost += value * production[name]
print(f"{value:.0f} {name}")
print(f"\nLabour cost: ${labour_cost:,.2f}")
print(f"Production cost: ${production_cost:,.2f}")
作者提供的图片
这种语法看起来与之前的语法非常相似。在使用线性求解器时,我们能够直接使用劳动和生产成本,而无需将其转换为整数。
这里有一种替代的语法可以实现相同的功能。主要的区别在于我们如何设置约束条件和目标函数。
# Initialise linear solver with the SCIP backend
model = pywraplp.Solver.CreateSolver('SCIP')
# Set decision variables
scarves = model.IntVar(0, demand['scarves'], 'scarves')
hats = model.IntVar(0, demand['hats'], 'hats')
gloves = model.IntVar(0, demand['gloves'], 'gloves')
# Add constraints
labour_constraint = model.Constraint(0, labour['limit'], 'labour_constraint')
labour_constraint.SetCoefficient(scarves, labour['scarves'])
labour_constraint.SetCoefficient(hats, labour['hats'])
labour_constraint.SetCoefficient(gloves, labour['gloves'])
production_constraint = model.Constraint(0, production['limit'], 'production_constraint')
production_constraint.SetCoefficient(scarves, production['scarves'])
production_constraint.SetCoefficient(hats, production['hats'])
production_constraint.SetCoefficient(gloves, production['gloves'])
# Set objective
objective = model.Objective()
objective.SetCoefficient(scarves, profit['scarves'])
objective.SetCoefficient(hats, profit['hats'])
objective.SetCoefficient(gloves, profit['gloves'])
objective.SetMaximization()
model.Solve()
print(f"Profit: ${model.Objective().Value():,.2f}\n")
labour_cost = 0
production_cost = 0
for var in [scarves, hats, gloves]:
value = var.solution_value()
name = var.name()
labour_cost += value * labour[name]
production_cost += value * production[name]
print(f"{value:.0f} {name}")
print(f"\nLabour cost: ${labour_cost:,.2f}")
print(f"Production cost: ${production_cost:,.2f}")
这就是 OR-Tools 的全部内容!希望现在你对语法的基本工作原理有了基本的了解。
🔗 要了解更多信息,请查看以下附加资源:◼️ OR-Tools Python 示例 GitHub
📌 DOcplex
我们接下来要熟悉的库是 DOcplex,这是 IBM 开发的一个库。这个工具是商业产品,因此免费版本的求解能力有限。下面是我们如何使用其约束编程求解器解决示例问题的方法:
# Install library with: pip install docplex cplex
# Load library
from docplex.cp.model import CpoModel
# Initialise model
model = CpoModel()
# Set decision variables
scarves = model.integer_var(0, demand['scarves'], 'scarves')
hats = model.integer_var(0, demand['hats'], 'hats')
gloves = model.integer_var(0, demand['gloves'], 'gloves')
# Add constraints
model.add(labour['scarves']*scarves +
labour['hats']*hats +
labour['gloves']*gloves <= labour['limit'])
model.add(production['scarves']*scarves +
production['hats']*hats +
production['gloves']*gloves <= production['limit'])
# Set objective
model.maximize(profit['scarves']*scarves +
profit['hats']*hats +
profit['gloves']*gloves)
solution = model.solve()
# Inspect output
print(f"Profit: ${solution.get_objective_value():,.2f}\n")
labour_cost = 0
production_cost = 0
for name in ['scarves', 'hats', 'gloves']:
value = solution[name]
labour_cost += value * labour[name]
production_cost += value * production[name]
print(f"{value:.0f} {name}")
print(f"\nLabour cost: ${labour_cost:,.2f}")
print(f"Production cost: ${production_cost:,.2f}")
作者提供的图片 | 仅部分输出
我们可以从输出中看到很多关于解决方案的细节。
这种语法与 OR-Tool 的语法非常相似,不是吗?我们可以通过修改前两行来使用其数学编程求解器解决这个例子:
from docplex.mp.model import Model
# Initialise model
model = Model()
值得注意的是,以下关于name
和value
的代码片段
for name in ['scarves', 'hats', 'gloves']:
value = solution[name]
也可以通过这种方式获得:
for var in [scarves, hats, gloves]:
value = var.solution_value
name = var.name
这就是 DOcplex 的基础内容!
🔗 要了解更多信息,请查看以下附加资源:◼️ DOcplex 约束编程 Jupyter Notebook 示例
◼️ DOcplex 数学编程 Jupyter Notebook 示例
◼️ 数学规划与约束编程
📌 PuLP
最后但同样重要的是,我们将熟悉 PuLP,这是一个提供线性编程求解器的库。PuLP 也是开源软件。下面是我们可以使用其线性求解器找到示例问题最优解的一种方法:
# Install library with: pip install pulp
# Load library
from pulp import LpProblem, LpMaximize, LpVariable, LpStatus
model = LpProblem(sense=LpMaximize)
# Set decision variables
scarves = LpVariable(name='scarves', lowBound=0, upBound=demand['scarves'], cat='Integer')
hats = LpVariable(name='hats', lowBound=0, upBound=demand['hats'], cat='Integer')
gloves = LpVariable(name='gloves', lowBound=0, upBound=demand['gloves'], cat='Integer')
# Add constraints
model += (labour['scarves']*scarves +
labour['hats']*hats +
labour['gloves']*gloves <= labour['limit'], 'labour_constraint')
model += (production['scarves']*scarves +
production['hats']*hats +
production['gloves']*gloves <= production['limit'], 'production_constraint')
# Set objective
model += profit['scarves']*scarves + profit['hats']*hats + profit['gloves']*gloves
model.solve()
# Inspect output
print(f"Profit: ${model.objective.value():,.2f}\n")
labour_cost = 0
production_cost = 0
for var in model.variables():
value = var.value()
name = var.name
labour_cost += value * labour[name]
production_cost += value * production[name]
print(f"{value:.0f} {name}")
print(f"\nLabour cost: ${labour_cost:,.2f}")
print(f"Production cost: ${production_cost:,.2f}")
作者提供的图片 | 仅部分输出
值得一提的是,你会在 PuLP 的官方文档中看到from pulp import *
。然而,我没有以这种方式导入库,因为我想明确地显示像LpProblem
这样的函数的来源。
🔗 要了解更多信息,请查看以下附加资源:◼️ PuLP 案例研究
图片由 Michael Dziedzic 提供,来源于 Unsplash
就是这样,这篇文章到此为止!优化是一个广泛的领域,在各种领域中都有许多有趣的应用。我希望这篇文章能为你介绍 Python 中的三个流行优化库,并扩展你的技巧库。这里有一些额外的资源,可能有助于加深你对优化的理解:
🔗 离散优化的实用指南 (第一章)
🔗 基本优化概念
感谢你阅读这篇文章。如果你感兴趣,以下是我其他一些文章的链接:
◼️ ️️倾向评分匹配
◼️ 通过这些技巧丰富你的 GitHub 个人资料
◼️️ 从 ML 模型到 ML 管道
◼️️ 使用 SHAP 解释 Scikit-learn 模型
◼️️ 在 Python 中绘制多个图表的 4 个简单技巧
◼️ 美化 pandas DataFrames
◼ 你会发现有用的 Python 简单数据可视化️
◼️ 在 Seaborn (Python) 中创建更美观和定制的图表的 6 个简单技巧
暂时告别 🏃 💨
机器学习部署简介:Flask、Docker 与 Locust
原文:
towardsdatascience.com/introduction-to-ml-deployment-flask-docker-locust-b87b5bd78a17
学习如何在 Python 中部署你的模型并使用 Locust 测量性能
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 2 月 27 日
--
摄影师 İsmail Enes Ayhan 在 Unsplash 上的照片
介绍
你已经在 EDA 上花费了大量时间,精心设计了特征,调优了模型好几天,终于在测试集上得到了表现良好的结果。那么接下来呢?现在,朋友,我们需要部署这个模型。毕竟,任何停留在笔记本中的模型都没有价值,不论它有多好。
学习数据科学工作流的这一部分可能会让人感到不知所措,尤其是当你没有太多软件工程经验时。不过不用担心,这篇文章的主要目的是通过介绍 Python 中最流行的部署框架之一——Flask,帮助你入门。此外,你还将学习如何对部署进行容器化并测量其性能,这两个步骤常常被忽视。
“部署”到底是什么?
首先,让我们澄清一下在这篇文章中“部署”是什么意思。机器学习部署是将训练好的模型整合到生产系统(下图中的服务器)中的过程,使其可以供最终用户或其他系统使用。
模型部署图。图片来源:作者。
请记住,实际的部署过程比简单地使模型对最终用户可用要复杂得多。它还涉及与其他系统的服务集成、选择适当的基础设施、负载均衡和优化,以及对所有这些组件的全面测试。这些步骤中的大多数超出了本帖的范围,理想情况下应由经验丰富的软件/ML 工程师处理。然而,了解这些领域的一些内容还是很重要的,这就是为什么本帖将涵盖容器化、推理速度测试和负载处理。
设置
所有代码可以在这个 GitHub repo 中找到。我将展示其中的一些片段,但请确保拉取并进行实验,这才是最好的学习方式。要运行代码,你需要安装 docker
、flask
、fastapi
和 locust
。根据你运行此代码的环境,可能还需要安装一些额外的依赖。
项目概述
为了使学习更加实际,本帖将展示一个简单的贷款违约预测模型的演示部署。模型训练过程不在本帖范围内,因此已经训练并序列化的 CatBoost 模型可以在 GitHub repo 中找到。该模型在预处理过的 美国小企业管理局数据集(CC BY-SA 4.0 许可证)上训练。请随意浏览数据字典以了解每一列的含义。
本项目主要集中在服务部分,即使模型对其他系统可用。因此,模型实际上将在你的本地机器上部署,这对于测试来说是好的,但在现实世界中并不理想。Flask 和 FastAPI 的部署将遵循以下主要步骤:
-
创建 API 端点(使用 Flask 或 FastAPI)
-
使用 Docker 对应用程序(端点)进行容器化
-
在本地运行 Docker 镜像,创建一个服务器
-
测试服务器性能
项目流程图。图片由作者提供。
听起来很激动人心,对吧?那么,我们开始吧!
什么是 Flask?
Flask 是一个流行且广泛采用的 Python 网络框架,由于其轻量级特性和最低的安装要求而受到青睐。它提供了一种简单的方法来开发 REST API,非常适合服务机器学习模型。
Flask 的典型工作流程包括定义一个预测 HTTP 端点,并将其链接到接收数据作为输入并生成预测作为输出的特定 Python 函数。用户和其他应用程序可以访问这个端点。
创建 Flask 应用
如果你只是想创建一个预测端点,那会非常简单。你需要做的就是反序列化模型,创建Flask
应用对象,并用POST
方法指定预测端点。关于POST
和其他方法的更多信息,可以在这里找到。
上述代码中最重要的部分是predict
函数。它读取 json 输入,在这个例子中是描述贷款申请的一堆属性。然后,它将这些数据转换为 DataFrame,并通过模型处理。最终的违约概率被格式化回 json 并返回。当这个应用程序在本地部署时,我们可以通过向http://0.0.0.0:8989/predict
网址发送 json 格式的数据来获取预测。我们来试试吧!要启动服务器,我们可以简单地运行下面的 Python 文件。
python app.py
预期输出。作者提供的截图。
当运行这个命令时,你应该会看到消息,显示你的应用程序正在[
0.0.0.0:8989/](http://0.0.0.0:8989/)
地址上运行。目前,我们暂时忽略那个大红色的警告,测试一下应用程序。为了检查应用程序是否按预期工作,我们可以向应用程序发送一个测试请求(贷款申请数据),看看是否会收到响应(违约概率预测)。
如果你成功获得了概率响应——恭喜!你已经使用自己的计算机作为服务器部署了模型。现在,让我们更进一步,使用 Docker 打包你的部署应用。
容器化 Flask 应用
容器化是将应用程序及其所有依赖项(包括 Python)封装到一个自包含、隔离的包中,以便在不同环境(例如本地、云端、朋友的笔记本电脑等)中一致运行的过程。你可以使用 Docker 来实现这一点,你需要做的就是正确指定 Dockerfile,构建镜像,然后运行它。Dockerfile 给你的容器提供指令,例如使用哪个版本的 Python、安装哪些包和运行哪些命令。如果你想了解更多,关于 Docker 有一个很棒的视频教程。
下面是 Flask 应用程序的可能外观。
现在,我们可以使用docker build
命令构建镜像。
docker build -t default-service:v01 .
-t
选项允许你为你的 docker 镜像命名并提供一个标签,因此这个镜像的名称是deafult-service
,标签是v01
。末尾的点表示需要提供的 PATH 参数。它是模型、应用程序代码等的位置。由于我假设你是在包含所有代码的目录中构建这个镜像的,因此 PATH 设置为.
,即当前目录。构建这个镜像可能需要一些时间,但完成后,你应该能够在运行docker images
时看到它。
使用以下命令运行 Docker 化的应用:
docker run -it --rm -p 8989:8989 default-service:v01
-it
标志使 Docker 镜像以交互模式运行,这意味着你可以在 shell 中查看代码日志,并在需要时使用 Ctrl+C 停止镜像。--rm
确保在你停止镜像时容器会被自动移除。最后,-p
使 Docker 镜像内的端口对外部可用。上面的命令将 Docker 内部的 8989 端口映射到本地主机,使我们的端点在相同的地址上可用。
测试 Flask 应用
现在我们的模型已经成功部署,使用 Flask 部署容器已经启动并运行(至少在本地),是时候评估它的性能了。此时,我们的重点是服务指标,如响应时间和服务器处理请求的能力,而不是像 RMSE 或 F1 分数这样的机器学习指标。
使用脚本进行测试
为了获得响应延迟的大致估计,我们可以创建一个脚本,向服务器发送多个请求,并测量服务器返回预测的时间(通常以毫秒为单位)。然而,需要注意的是,响应时间不是恒定的,因此我们需要测量中位延迟,以估计用户通常等待响应的时间,以及 95th 延迟百分位数,以测量最坏情况。
这段代码位于 measure_response.py
文件中,因此我们可以简单地运行此 Python 文件来测量这些延迟指标。
python measure_response.py
延迟指标。截图由作者提供。
中位响应时间为 9 毫秒,但最坏的情况是这个时间的 10 倍以上。这个性能是否令人满意由你和产品经理决定,但至少现在你已经了解了这些指标,并可以进一步改进它们。
使用 Locust 进行测试
Locust 是一个用于测试 web 应用性能和可扩展性的 Python 包。我们将使用 Locust 生成一个更高级的测试场景,因为它允许配置每秒用户数量(即贷款申请者)的参数。
首先,运行 pip install locust
可以安装该包。然后,我们需要定义一个测试场景,以指定我们的虚拟用户将如何与我们的服务器进行交互。在我们的案例中,这非常简单——用户将向我们发送一个包含其贷款申请的(JSON 格式的)信息请求,并从我们部署的模型中接收响应。
如你所见,Locust 任务与我们之前做的测试 ping 非常相似。唯一的区别是它需要被封装在一个继承自locust.HttpUser
的类中,并且执行的任务(发送数据并获取响应)需要用@task
进行装饰。
要开始负载测试,我们只需运行下面的命令。
locust -f app_test.py
启动后,你将能够访问测试 UI,网址为 http://0.0.0.0:8089
,在这里你需要指定应用的 URL、用户数量和生成速率。
Locust UI。截图由作者提供。
生成速率为 5,用户数为 100,意味着每秒将有 5 个新用户向你的应用发送请求,直到用户数量达到 100。这意味着在峰值时,我们的应用需要处理每秒 100 个请求。现在,让我们点击 开始压力测试 按钮并转到 UI 的图表部分。下面我将展示我机器上的结果,但它们肯定与你的不同,所以请确保也在你自己的机器上运行测试。
Locust 100 用户测试可视化。截图由作者提供。
你会发现,随着流量的增加,你的响应时间会变得越来越慢。还会有一些偶发的峰值,因此理解这些峰值发生的时间和原因是很重要的。最重要的是,Locust 帮助我们了解我们的本地服务器可以处理每秒 100 个请求,响应时间中位数约为 ~250ms。
我们可以继续对应用进行压力测试,并识别出它无法处理的负载。为此,我们将用户数量增加到 1000 以查看会发生什么。
Locust 1000 用户测试可视化。截图由作者提供。
看起来我本地服务器的崩溃点大约是 ~180 个并发用户。这是我们通过 Locust 提取出的重要信息。
摘要
做得好,已经走到这一步了!我希望这篇文章为你提供了一个实用且富有见地的模型部署介绍。通过跟随这个项目或将其调整到你的特定模型,你现在应该对模型部署的关键步骤有了全面的理解。具体来说,你已经掌握了使用 Flask 创建 REST API 端点、用 Docker 容器化这些端点,并使用 Locust 系统地测试这些端点的方法。
在下一篇文章中,我将介绍 FastAPI、BentoML、云部署以及更多内容,因此请确保订阅、点赞,并在有疑问时留下评论。
MLOps 原理介绍
原文:
towardsdatascience.com/introduction-to-mlops-principles-c5d73a00aa76
面向初学者的 MLOps 介绍
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 10 月 30 日
--
照片由 Silvestri Matteo 提供,来自 Unsplash
如果你希望将 MLOps 项目提升到一个新水平,理解其原理是这个过程中的关键部分。本文将介绍 MLOps 的原理,并以易于理解的方式阐明关键概念。每个原理都将配有专门的教程和实际案例,相关内容将在后续文章中发布。你可以在我的 Github 个人资料 上查看所有示例。然而,如果你是 MLOps 的新手,我建议你从我的 面向初学者的教程 开始,以便尽快了解情况。让我们开始吧!
目录:
· 1. 介绍
· 2. MLOps 原理
· 3. 版本控制
· 4. 测试
· 5. 自动化
· 6. 监控和跟踪
· 7. 可重复性
· 8. 结论
我的 MLOps 教程:
-
教程 1:MLOps 的关键起点:探索其基本组成部分
-
教程 2:面向初学者的 MLOps 工作流简介
-
教程 6:实践中的测试:代码、数据和机器学习模型
-
教程 7:实践中的追踪:代码、数据和机器学习模型
[我会在发布相关主题文章时更新此列表]
1. 介绍
在上一篇文章中,我们将 MLOps 定义为一套技术和实践,用于以高效、优化和有组织的方式设计、构建和部署机器学习模型。MLOps 的关键步骤之一是建立工作流并随着时间进行维护。
MLOps 工作流概述了开发、部署和维护机器学习模型的步骤。它包括描述问题的业务问题、涉及所有数据准备和预处理的数据工程、涵盖从模型设计到评估的所有模型处理的机器学习模型工程,以及涉及模型服务的代码工程。如果您想了解更多细节,可以参考之前的教程。
MLOps 工作流
MLOps 工作流的所有部分都是互联的,并以循环的方式运行。在任何阶段,可能需要重新访问之前的步骤。这些工作流各阶段之间的相互依赖定义了MLOps 生命周期。MLOps 生命周期对于确保机器学习模型的最佳性能并仍然解决在第一阶段定义的业务问题至关重要。因此,维护 MLOps 生命周期的关键在于遵循MLOps 原则。
2. MLOps 原则
MLOps 原则是一组旨在维护 MLOps 生命周期的概念,同时减少开发和部署机器学习模型的时间和成本,从而避免技术债务。
为了确保整个生命周期的维护,这些原则需要应用于不同的工作流步骤,包括数据、机器学习模型(ML 模型)和代码。这些原则包括版本控制、测试、自动化、监控和追踪以及可重复性。成功实施这些原则需要利用适当的工具和遵循最佳实践,例如项目结构化。
在本文中,我试图根据不同原则的重要性和应用顺序进行优先排序。然而,需要注意的是,这些原则在成熟的机器学习项目中都具有重要意义:它们是相互依赖的并且互补。
3. 版本控制
在 MLOps 原则中,第一个需要考虑的事情是版本化不同的应用组件,包括数据、机器学习模型和代码。这使得对不同 MLOps 组件所做的更改可以进行跟踪,并能够重现工作流程的不同步骤。它还允许开发者和数据科学家在需要时回滚到先前版本,比较不同版本之间的性能,并重现结果。
数据工程中的版本管理包括数据集、特征、元数据和处理管道的版本管理。它可以使用版本控制系统(VCS),如 Git 和 DVC(数据版本控制)来管理,这些系统支持大数据文件的版本化和跟踪。
机器学习模型的版本管理包括模型本身(其架构和权重)、训练管道、超参数和结果的版本管理。通常使用 Git 进行管理;然而,还有其他工具,如 MLflow,提供了版本化其他元数据的附加功能。
代码的版本管理包括代码源本身及其不同配置的版本管理。还建议在每次代码版本管理时保留库版本,以防止库版本问题。
总的来说,版本管理在 MLOps 中至关重要,以确保对机器学习模型、代码和数据所做的更改得到有效跟踪和管理。
4. 测试
测试是 MLOps 中的一个基本、关键且强制性的方面。它减少了错误和漏洞的风险,并能快速发现和修复问题。此外,它确保机器学习模型按预期运行,并满足业务问题的要求。
数据测试确保用于机器学习的数据的质量和正确性,这反过来确保机器学习模型的准确性和可靠性。它包括数据集验证和数据处理管道验证。数据集验证包括识别数据中的潜在问题,如缺失值和不一致数据。后者通过计算一些统计数据和可视化技术来完成。数据处理管道验证包括对不同的数据处理函数进行单元测试,特别是特征创建。
机器学习模型测试确保新数据上的预测准确性,并评估模型的泛化能力。这是一个测试、评估和验证机器学习模型的过程。这个过程包括测试模型规格、训练管道集成以及模型的相关性和正确性。此外,还需要测试一些非功能性要求,如安全性、公平性、可解释性,以确保模型的有效性、可靠性和伦理影响。
代码测试包括验证整个 MLOps 项目代码的正确性和质量,以确保代码没有缺陷并满足项目要求。代码测试包括对不同模块和功能的单元测试、端到端管道的集成测试、系统测试和使用真实数据的验收测试。
总体而言,测试是 MLOps 中确保管道正确性和效率的关键方面。它还可以改善其他 MLOps 原则。
-
它确保在版本控制期间所做的代码更改能够正常运行。
-
它通过将其添加到自动化管道中来提升自动化水平。
-
它通过检测潜在问题来改善监控。
-
它还通过确保模型能够一致地被重现来提高可重复性。
5. 自动化
自动化是 MLOps 原则之一,旨在自动化不同的管道处理过程,包括构建、测试、部署和管理。自动化定义了系统摆脱人工干预的程度:系统摆脱人工过程的程度越高,系统的自动化水平越高。因此,自动化水平定义了过程的成熟度水平。
数据工程的过程通常是手动的,从数据收集、数据准备到数据验证,因为这一步具有实验性和迭代性。然而,某些步骤可以自动化,比如数据转换和特征处理。自动化这些步骤提高了准确性并减少了人工错误。
ML 模型自动化旨在自动化从模型开发到部署和管理的过程。它包括自动化特征工程、模型训练、超参数选择和调整、模型验证和模型部署。这些步骤减少了构建和部署模型所需的时间和资源,同时提高了模型的质量和一致性,特别是在新数据可用、模型更改或监控事件时。自动化 ML 模型开发过程有助于减少构建和部署模型所需的时间和资源,同时提高其质量和一致性。
代码自动化旨在减少错误、提高效率并改善整体质量。它包括应用构建自动化和 CD/CI 管道自动化,以执行快速可靠的 ML 模型生产部署。
总之,自动化能够高效可靠地实施重复任务并标准化流程。此外,它与其他原则相互关联:
-
自动化通过管理和跟踪不同版本的代码、数据和模型来促进版本控制。
-
它通过自动化执行各种测试(如单元测试、集成测试和性能测试)来促进测试。
-
它通过自动化相关指标和性能指标的收集、分析和可视化来促进监控。
-
它通过自动化代码、工作流和管道的执行来促进可重现性。
6. 监控与跟踪
当模型部署时,监控 MLOps 项目以确保模型在生产中的性能、稳定性和可靠性是至关重要的。如果监控检测到异常,必须报告这些变化,并提醒相关开发人员。监控 MLOps 的不同步骤包括频繁地重新运行和比较它们。
MLOps 中的数据监控包括持续观察和分析用于机器学习模型的输入数据,以确保其质量、一致性和相关性。它包括监控版本变化、训练和服务输入中的不变量、计算的训练和服务特征中的不变量,以及特征生成过程。
ML 模型监控包括跟踪和评估生产中机器学习模型的性能。它包括监控模型的数值稳定性、模型的年龄和衰退,以及 ML 系统的计算性能。
代码监控包括跟踪和评估机器学习项目中使用的代码,以确保其质量、完整性和遵循编码标准。这包括监控源系统中的变化、依赖项升级以及应用程序在服务数据上的预测质量。
同样重要的是,监控和跟踪通过跟踪输入、输出和代码、工作流及模型的执行来支持可重现性。此外,通过监控测试,可以检测模型/系统行为和性能中的异常。
7. 可重现性
可重现性在构建机器学习工作流时至关重要。它允许在任何执行地点生成相同的结果,只要输入相同。为了确保结果一致,整个 MLOps 工作流需要是可重现的。
数据可重现性包括捕捉和保存与数据收集、预处理和转换相关的所有必要信息和过程,以便其他人能够获得相同的数据集进行分析或模型训练。这包括为数据创建备份、对数据和特征进行版本控制,以及创建和备份元数据和文档。
ML 模型的可重现性包括能够一致地重建和获得相同的训练机器学习模型。这包括确保参数相同,以及在开发、生产或其他机器上参数的顺序相同。
代码可重复性包括从用于开发机器学习模型的代码中重新创建和获得相同结果的能力。它包括确保所有环境中的依赖版本和技术栈相同。这通常通过提供容器镜像或虚拟机来确保。
总体来说,可重复性通过其他原则来实现:版本控制、测试、自动化和监控协同工作,以捕获和维护必要的信息,一致地执行流程,验证结果的正确性,并识别可能影响模型和工作流可重复性的因素。
8. 结论
我们来到了本文的结尾。在这篇文章中,我们介绍并简要解释了 MLOps 原则。你可以将 MLOps 原则视为维护 MLOps 生命周期的组成部分。它们可以通过实践、工具或两者的结合来实现。为了确保整个生命周期的维护,它们被应用于 MLOps 工作流的不同组件上。
在接下来的教程中,我们将深入学习不同的 MLOps 原则。我会撰写更多关于 MLOps 及其各种技术的教程,并附带示例,敬请关注。
感谢阅读本文。你可以在我的GitHub 个人资料中找到我提供的不同教程的所有示例。如果你喜欢我的教程,请通过关注和订阅来支持我。这样,你将收到我新文章的通知。如果你有任何问题或建议,请随时留下评论。
图片来源
本文中所有未在说明中提及来源的图片和图表均由作者提供。
mypy 介绍
Python 的静态类型检查
·发布在Towards Data Science ·阅读时间 8 分钟·2023 年 4 月 6 日
--
图片由Agence Olloweb提供,在Unsplash上
我们在之前关于Python 最佳实践的文章中提到了 mypy 是必备的——在这里,我们希望更详细地介绍它。
mypy,正如文档所解释的,是一个“Python 的静态类型检查器”。这意味着,它为 Python 添加类型注解和检查,而 Python 是设计上动态类型的(类型在运行时推断,与例如 C++ 相对)。这样做可以在编译时发现代码中的错误,这是一种极大的帮助——并且是任何半专业 Python 项目的必备条件,正如我在之前的文章中所解释的那样。
在这篇文章中,我们将通过几个示例来介绍 mypy。免责声明:这篇文章不会介绍 mypy 的所有功能(甚至连一部分都不会)。相反,我会尝试在提供足够细节以让你编写几乎所有想要的代码和从零开始到扎实理解 mypy 之间找到一个良好的平衡。有关更多细节,我建议查阅官方文档或其他优秀教程。
安装
要安装 mypy,只需运行:pip install mypy
但是,我建议使用某种形式的依赖管理系统,例如poetry。如何将它和 mypy 包含在更大的软件项目中,已在这里进行了说明。
初步步骤
让我们通过第一个示例来激发对 mypy 使用的兴趣。请考虑以下代码:
def multiply(num_1, num_2):
return num_1 * num_2
print(multiply(3, 10))
print(multiply("a", "b"))
multiply
期望两个数字并返回它们的乘积。因此,multiply(3, 10)
工作正常并返回预期结果。但是第二条语句失败并崩溃,因为我们不能将字符串相乘。由于 Python 是动态类型的,没有任何东西阻止我们编写/执行该语句,我们只在运行时发现了这个问题——这很有问题。
在这里,mypy 及时提供了帮助。我们现在可以注解函数的参数和返回类型,如下所示:
def multiply(num_1: int, num_2: int) -> int:
return num_1 * num_2
print(multiply(3, 10))
print(multiply("a", "b"))
这种注解不会以任何方式改变执行,特别是,你仍然可以运行这个有问题的程序。然而,在这样做和发布我们的程序之前,我们现在可以运行 mypy 并通过以下命令检查任何可能的错误:mypy .
运行此命令将失败,并正确指出我们不能将字符串传递给multiply
。上述命令旨在从应用程序的主文件夹中执行,。
将检查当前文件夹及子目录中的每个文件。但你也可以通过mypy file_to_check.py
来检查特定文件。
这希望能激发对 mypy 的需求和使用的理解,现在让我们更深入探讨。
配置 mypy
mypy 可以通过多种方式进行配置——无需详细说明,它只需要找到一个包含“mypy”部分的配置文件(如 mypy.ini,pyproject.toml 等)。在这里,我们将创建默认的mypy.ini
文件,它应该位于项目的主文件夹中。
现在,让我们来看看可能的配置选项。为此,我们回到最初的示例:
def multiply(num_1, num_2):
return num_1 * num_2
print(multiply(3, 10))
print(multiply("a", "b"))
实际上,简单地运行 mypy 并不会产生错误!这是因为类型提示默认是可选的——而 mypy 只检查那些有注解的类型。我们可以通过标志— disallow-untyped-defs
来禁用这一点。此外,还有许多其他标志可以使用(参见 here)。然而,根据本帖的一般格式,我们不会详细讨论所有这些标志——而是仅呈现严格模式。此模式基本上开启所有可选检查。在我的经验中,使用 mypy 的最佳方法是要求尽可能严格的检查——然后修复(或选择性地忽略)所有出现的问题。
要做到这一点,我们可以像这样填写mypy.ini
文件:
[mypy]
strict = true
[mypy]
部分头对于任何 mypy 相关的配置都是必要的,下一行是相当自解释的。
当我们像往常一样运行 mypy 时,我们会遇到关于缺失类型注解的错误——这些错误只有在所有内容都被注解并且我们移除错误的字符串调用后才会消失。
现在让我们更详细地看看如何使用 mypy 进行注解。
使用 mypy 注解
在本节中,我们将描述最常见的类型注解和 mypy 关键字。
基本类型
我们可以通过简单地使用其 Python 类型来注解基本类型,即bool
、int
、float
、str
等:
def negate(value: bool) -> bool:
return not value
def multiply(multiplicand: int, multiplier: int) -> int:
return multiplicand * multiplier
def divide(dividend: float, divisor: float) -> float:
return dividend / divisor
def concat(str_a: str, str_b: str) -> str:
return str_a + " " + str_b
print(negate(True))
print(multiply(3, 10))
print(divide(10, 3))
print(concat("Hello", "world"))
集合类型
从 Python 3.9 开始,内置的集合类型也可以用作类型注解。即 list
、set
、dict
等:
def add_numbers(numbers: list[int]) -> int:
return sum(numbers)
def cardinality(numbers: set[int]) -> int:
return len(numbers)
def concat_values(value_dict: dict[str, float]) -> list[float]:
return [val for _, val in value_dict.items()]
print(add_numbers([1, 2, 3, 4]))
print(cardinality({1, 2, 3}))
print(concat_values({"a": 1.5, "b": 10}))
正如我们所见,我们必须指定容器的内容(例如int
)。对于混合类型,请参见下文。
早期 Python 版本
对于早期的 Python 版本,必须使用 typing
模块中的旧版类型:
from typing import Dict, List, Set
def add_numbers(numbers: List[int]) -> int:
return sum(numbers)
def cardinality(numbers: Set[int]) -> int:
return len(numbers)
def concat_values(value_dict: Dict[str, float]) -> list[float]:
return [val for _, val in value_dict.items()]
print(add_numbers([1, 2, 3, 4]))
print(cardinality({1, 2, 3}))
print(concat_values({"a": 1.5, "b": 10}))
混合内容
如上所述,我们可能想要创建包含不同数据类型的容器。为此,我们可以使用Union
关键字——它允许我们将类型注解为类型的联合:
from typing import Union
def scan_list(elements: list[Union[str | int]]) -> None:
for el in elements:
if isinstance(el, str):
print(f"I got a string! ({el})")
elif isinstance(el, int):
print(f"I got an int! ({el})")
else:
# NOTE: we don't reach here because of mypy!
raise ValueError(f"Unexpected element type {el}")
scan_list([1, "hello", "world", 100])
类似于 Python 3.9 中的简化,Python 3.10(具体是PEP 604)引入了使用逻辑或操作符(|
)的Union
类型的缩写表示法:
def scan_list(elements: list[str | int]) -> None:
for el in elements:
if isinstance(el, str):
print(f"I got a string! ({el})")
elif isinstance(el, int):
print(f"I got an int! ({el})")
else:
# NOTE: we don't reach here because of mypy!
raise ValueError(f"Unexpected element type {el}")
scan_list([1, "hello", "world", 100])
其他 mypy 关键字
在本节中,我们将介绍更多基本类型和关键字。
无
None
,就像在“正常”Python 中一样,表示一个None
值——最常用于注解没有返回类型的函数:
def print_foo() -> None:
print("Foo")
print_foo()
可选
我们经常会遇到这样一种情况:我们想根据是否传递了参数值来实现分支代码——通常,我们使用None
来表示其缺失。为此,我们可以使用typing.Optional[X]
——它正是表示这一点:它注解类型X
,但也允许None
:
from typing import Optional
def square_number(x: Optional[int]) -> Optional[int]:
return x**2 if x is not None else None
print(square_number(14))
继 Python 3.10 及以上版本引入的 PEP 604,Optional
可以再次缩短为 X | None
:
def square_number(x: int | None) -> int | None:
return x**2 if x is not None else None
print(square_number(14))
请注意,这与必需或可选参数不对应,这一点经常被混淆!可选参数是指在调用函数时不必指定的参数——而 mypy 的 Optional
表示一个可以是某种类型,但也可以是 None
的参数。可能的混淆来源是可选参数的一个常见默认值是 None
。
任意
Any
,正如其名所示(我觉得我总是在重复这句话……)简单地允许任何类型——从而有效地关闭任何类型检查。因此,尽量避免在可能的情况下使用它。
from typing import Any
def print_everything(val: Any) -> None:
print(val)
print_everything(0)
print_everything(True)
print_everything("hello")
注解变量
到目前为止,我们仅使用 mypy 注解了函数参数和返回类型。自然地,这也可以扩展到任何类型的变量:
int_var: int = 0
float_var: float = 1.5
str_var: str = "hello"
然而,这种用法相对较少(并且严格版的 mypy 也不强制执行),因为变量的类型通常可以从上下文中推断出来。通常情况下,只有在代码相对模糊且难以阅读时,你才会这么做。
注解复杂类型
在本节中,我们将讨论类的注解,以及使用你自己的和其他复杂类进行注解。
注解类
注解类可以很快处理:只需像注解其他函数一样注解类函数,但不要注解构造函数中的 self 参数:
class SampleClass:
def __init__(self, x: int) -> None:
self.x = x
def get_x(self) -> int:
return self.x
sample_class = SampleClass(5)
print(sample_class.get_x())
使用自定义/复杂类进行注解
定义好类之后,我们现在可以像其他类型注解一样使用其名称:
sample_class: SampleClass = SampleClass(5)
事实上,mypy 可以直接与大多数类和类型一起使用,例如:
import pathlib
def read_file(path: pathlib.Path) -> str:
with open(path, "r") as file:
return file.read()
print(read_file(pathlib.Path("mypy.ini")))
修复 mypy 问题
在本节中,我们将看到如何处理不支持类型标注的外部库,并有选择地禁用对某些引发问题的代码行的类型检查——基于一个稍微复杂一点的示例,其中涉及到numpy和matplotlib。
让我们从代码的第一个版本开始:
import matplotlib.pyplot as plt
import numpy as np
import numpy.typing as npt
def calc_np_sin(x: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
return np.sin(x)
x = np.linspace(0, 10, 100)
y = calc_np_sin(x)
plt.plot(x, y)
plt.savefig("plot.png")
我们定义了一个简单的函数来计算 numpy 数组的正弦,并将其应用于输入值x
,这些值覆盖了区间[0, 10]。然后,我们使用matplotlib
绘制正弦曲线。
在这段代码中,我们还看到使用numpy.typing
进行 numpy 数组的正确类型标注。
然而,如果我们在此运行 mypy,会出现两个错误。第一个是:
error: Returning Any from function declared to return “ndarray[Any, dtype[floating[_32Bit]]]”
这是 mypy 中一种相对常见的模式。我们实际上没有做错什么,但 mypy 希望它更为明确——在这里——以及在其他情况中——我们必须“强制”mypy 接受我们的代码。例如,我们可以通过引入一个正确类型的代理变量来做到这一点:
def calc_np_sin(x: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
y: npt.NDArray[np.float32] = np.sin(x)
return y
下一个错误是:
error: Skipping analyzing “matplotlib”: found module but no type hints or library stubs
这是因为matplotlib
尚未进行类型标注。因此,我们需要让 mypy 知道排除它的检查。我们通过在 mypy.ini 文件中添加以下内容来实现:
[mypy-matplotlib.*]
ignore_missing_imports = True
ignore_errors = True
最后,请注意,你还可以通过在代码行末添加# type: ignore
来有选择地忽略任何代码行。如果确实存在无法解决的 mypy 问题,或者你想要忽略一些已知但不相关的警告/错误,可以这样做。我们也可以通过这样做来隐藏上面的第一个错误:
def calc_np_sin(x: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
return np.sin(x) # type: ignore
结论
在这篇文章中,我们介绍了 mypy,它是一个用于 Python 的静态类型检查工具。使用 mypy,我们可以(并且应该)注释变量、参数和返回值的类型——为我们提供了一种在编译时对程序进行合理性检查的方法。mypy 被广泛使用,推荐用于任何中型以上的软件项目。
我们从安装和配置 mypy 开始。然后,我们介绍了如何注释基本和复杂类型,例如列表、字典或集合。接下来,我们讨论了其他重要的注释器,如Union
、Optional
、None
或Any
。最终,我们展示了 mypy 支持广泛的复杂类型,如自定义类。我们通过展示如何调试和修复 mypy 错误来结束教程。
这就是关于 mypy 的内容——希望你喜欢这篇文章,感谢阅读!
使用 SciPy 的优化约束介绍
原文:
towardsdatascience.com/introduction-to-optimization-constraints-with-scipy-7abd44f6de25
探索边界、线性和非线性约束,并结合实际 Python 示例
·发表于 Towards Data Science ·8 分钟阅读·2023 年 1 月 31 日
--
图片由作者提供。
目录
-
介绍
-
实现
2.1 无约束优化
2.2 边界
2.3 线性约束
2.4 非线性约束
2.5 同时应用不同约束类型
-
结论
1. 介绍
优化是从一组潜在候选元素中选择最佳元素以达到特定目标的过程。
我们在日常生活中执行许多优化任务:寻找最短或最快的到达目的地路线,准备按优先级排序的待办事项清单,购买杂货。
我们可以通过定义目标函数**f(x)**
来描述此类问题。
让我们假设我们正在组织一次去另一座城市的旅行,并尝试评估一个合适的出发时间。在这个例子中,目标函数f(x)
是出发时间x
作为旅行持续时间的函数。
我们可以将优化问题表述为目标函数的最小值或最大值的确定。在我们的例子中,我们想要确定一个出发时间,以最小化旅行的持续时间:
在其他情况下,我们可能希望最大化f(x)
。例如,当目标表示某种可能性或投资回报时。然而,最大化一个函数等同于最小化其负值。因此,人们可以仅关注最小化问题:
在实际应用中,我们可能需要对优化问题应用约束。例如,我们可能想找到最快的路线,但我们不愿意支付通行费或夜间旅行。我们将受约束优化定义为在一些逻辑条件下最小化目标函数,这些条件可能反映:
-
现实世界的限制;
-
输入变量的物理意义;
-
上下文情况。
在这篇文章中,我们分享了一个使用 [SciPy](https://scipy.org/)
的优化示例,这是一款流行的科学计算 Python 库。特别地,我们探索最常见的约束类型:界限、线性约束和非线性约束。
2. 实现
2.1 无约束优化
我们从一个简单的无约束优化问题开始,然后在输入变量上添加约束。
导入所需的库:
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import minimize, Bounds, LinearConstraint, NonlinearConstraint
设想以下多变量目标函数:
关于x₀
和x₁
的梯度为
def f(x):
'''Objective function'''
return 0.5*x[0]**2 + 2.5*x[1]**2 + 4 * x[0] * np.sin(np.pi * x[0]) + 5
def df(x):
'''Gradient of the objective function'''
return np.array([x[0] + 4 * np.sin(np.pi * x[0]) + 4 * np.pi * x[0] * np.cos(np.pi * x[0]), 5*x[1]])
让我们生成数据并观察x₀, x₁ ∈ [-1, 1]
的函数值:
# Generate data
X0, X1 = np.meshgrid(np.linspace(-1, 1, 100), np.linspace(-1, 1, 100))
Z = f(np.stack([X0, X1]))
# Plot
fig = plt.figure(figsize=(30, 20))
# First subplot
ax = fig.add_subplot(1, 3, 1, projection='3d')
ax.contour3D(X0, X1, Z, 70, cmap='plasma')
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
# Second subplot
ax = fig.add_subplot(1, 3, 2, projection='3d')
ax.contour3D(X0, X1, Z, 70, cmap='plasma')
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.axes.yaxis.set_ticklabels([])
ax.view_init(0, 80)
# Third subplot
ax = fig.add_subplot(1, 3, 3, projection='3d')
ax.contour3D(X0, X1, Z, 70, cmap='plasma')
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.axes.zaxis.set_ticklabels([])
ax.view_init(89, -90);
目标函数。图片由作者提供。
在我们的示例中,目标函数是非凸的,并且具有多个最小值。
这意味着,根据起点不同,问题可能会收敛到不同的最小化器。
我们可以利用有用的 [scipy.optimize.minimize](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html)
函数来解决优化问题,具体如下:
# Starting point
x_start = np.array([0.5, 0.5])
# Optimization
result = minimize(
f, x_start, method='trust-constr', jac=df)
result.x.round(3)
值得注意的是,我们应用了 trust-constr
方法。它允许对受约束的函数进行优化。有关该方法的更多信息,请参见 包文档 和 "Trust-region methods" (Conn, Gould and Toint; 2000)。
上述代码片段返回找到的最小化器:
array([-0., 0.])
让我们绘制它:
# Minimum from unconstrained optimization
min_x0, min_x1 = np.meshgrid(result.x[0], result.x[1])
min_z = f(np.stack([min_x0, min_x1]))
# Plot
fig = plt.figure(figsize=(30, 20))
# First subplot
ax = fig.add_subplot(1, 3, 1, projection='3d')
ax.contour3D(X0, X1, Z, 40, cmap='plasma')
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
# Second subplot
ax = fig.add_subplot(1, 3, 2, projection='3d')
ax.contour3D(X0, X1, Z, 40, cmap='plasma')
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.axes.yaxis.set_ticklabels([])
ax.view_init(0, 80)
# Third subplot
ax = fig.add_subplot(1, 3, 3, projection='3d')
ax.contour3D(X0, X1, Z, 40, cmap='plasma')
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.axes.zaxis.set_ticklabels([])
ax.view_init(89, -90);
目标函数及其最小化器。图片由作者提供。
我们现在可以尝试添加约束。
2.2 界限
让我们考虑之前的示例,即在两个城市之间找到最快的旅行路线,出发时间作为输入变量。我们可能会根据一天中的时间预期找到更多或更少的交通。通过以最小化旅行时间为目标,模型也可能建议,例如,夜间旅行。
尽管这可能会导致最短的旅行时间,但我们可能更喜欢白天旅行。因此,我们可以要求模型在早上 7:00 到下午 6:00 之间的出发时间范围内找到最短的旅行时间。
这时界限就派上用场了。界限只是对输入变量的等式或不等式约束。它们允许仅在指定范围之间评估目标函数。
在我们的情况下,假设x₀
和x₁
有以下可接受的值:
我们可以轻松地将这些值传递给[Bounds](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.Bounds.html)
对象,并进行新的优化实验,如下所示:
lim = [0.25, 0.30, 0.75, 0.8]
bounds = Bounds([lim[0], lim[1]], # [min x0, min x1]
[lim[2], lim[3]]) # [max x0, max x1]
result = minimize(
f, x_start, method='trust-constr', jac=df, bounds=bounds)
result.x.round(3)
优化任务现在导致了一个不同的解决方案,因为之前的点array([0 , 0])
不在可行区域内:
array([0.25, 0.301])
我们可以最终绘制新的最小值和可行区域,并观察f(x)
被评估的区域:
# Feasible region (bounds)
X0_bound, X1_bound = np.meshgrid(np.linspace(lim[0], lim[2], 20), np.linspace(lim[1], lim[3], 20))
Z_bound = f(np.stack([X0_bound, X1_bound]))
# New minimum within bounds
min_x0_bounds, min_x1_bounds = np.meshgrid(result.x[0], result.x[1])
min_z_bounds = f(np.stack([min_x0_bounds, min_x0_bounds]))
# Plot
fig = plt.figure(figsize=(30, 20))
# First subplot
ax = fig.add_subplot(1, 3, 1, projection='3d')
ax.contour3D(X0, X1, Z, 40, cmap='plasma')
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.scatter(min_x0_bounds, min_x1_bounds, min_z_bounds, marker='o', color='blue', linewidth=10)
ax.plot_surface(X0_bound, X1_bound, Z_bound, color='black', alpha=0.6)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
# Second subplot
ax = fig.add_subplot(1, 3, 2, projection='3d')
ax.contour3D(X0, X1, Z, 40, cmap='plasma')
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.scatter(min_x0_bounds, min_x1_bounds, min_z_bounds, marker='o', color='blue', linewidth=10)
ax.plot_surface(X0_bound, X1_bound, Z_bound, color='black', alpha=0.6)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.axes.yaxis.set_ticklabels([])
ax.view_init(0, 80)
# Third subplot
ax = fig.add_subplot(1, 3, 3, projection='3d')
ax.contour3D(X0, X1, Z, 40, cmap='plasma')
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.scatter(min_x0_bounds, min_x1_bounds, min_z_bounds, marker='o', color='blue', linewidth=10)
ax.plot_surface(X0_bound, X1_bound, Z_bound, color='black', alpha=0.6)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.axes.zaxis.set_ticklabels([])
ax.view_init(89, -90);
目标函数的“无约束”最小值(红点)和应用边界后的最小值(蓝点)。应用的变量范围标识了灰色区域(可行区域)。
2.3 线性约束
线性约束定义了优化变量之间的线性关系。例如,假设x₀
和x₁
受到以下约束:
我们可以轻松地将这些条件重写为线性系统,并在运行优化任务之前将其传递给[LinearConstraint](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.LinearConstraint.html)
对象:
线性约束。图片由作者提供。
linear_constraint = LinearConstraint(
[[1, 0], [1, -1]], [0.25, -np.inf], [np.inf, 0.1])
result = minimize(
f, x_start, method='trust-constr', jac=df, constraints=linear_constraint)
result.x.round(3)
新解决方案是
array([0.25, 0.15])
f(x)
的可行区域对应于由超平面交集限定的空间的一部分。让我们绘制这些边界:
# Linear constraints: first hyperplane
X0_lin_1 = np.repeat(0.25, 20)
X1_lin_1, Z_lin_1 = np.meshgrid(np.linspace(-1, 1, 20), np.linspace(4, 10, 20))
# Linear constraints: second hyperplane
X1_lin_2 = np.linspace(-1, 1, 20)
X0_lin_2 = X1_lin_2 + 0.1
# New minimum with linear constraints
min_x0_lin_constr, min_x1_lin_constr = np.meshgrid(result.x[0], result.x[1])
min_z_lin_constr = f(np.stack([min_x0_lin_constr, min_x0_lin_constr]))
# Plot
fig = plt.figure(figsize=(30, 20))
# First subplot
ax = fig.add_subplot(1, 3, 1, projection='3d')
ax.contour3D(X0, X1, Z, 40, cmap='plasma')
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.scatter(min_x0_lin_constr, min_x1_lin_constr, min_z_lin_constr, marker='o', color='blue', linewidth=10)
ax.plot_surface(X0_lin_1, X1_lin_1, Z_lin_1, color='green', alpha=0.3)
ax.plot_surface(X0_lin_2, X1_lin_2, Z_lin_1, color='yellow', alpha=0.2)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
# Second subplot
ax = fig.add_subplot(1, 3, 2, projection='3d')
ax.contour3D(X0, X1, Z, 40, cmap='plasma')
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.scatter(min_x0_lin_constr, min_x1_lin_constr, min_z_lin_constr, marker='o', color='blue', linewidth=10)
ax.plot_surface(X0_lin_1, X1_lin_1, Z_lin_1, color='green', alpha=0.2)
ax.plot_surface(X0_lin_2, X1_lin_2, Z_lin_1, color='yellow', alpha=0.2)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.axes.yaxis.set_ticklabels([])
ax.view_init(0, 80)
# Third subplot
ax = fig.add_subplot(1, 3, 3, projection='3d')
ax.contour3D(X0, X1, Z, 40, cmap='plasma')
ax.scatter(min_x0, min_x1, min_z, marker='o', color='red', linewidth=10)
ax.scatter(min_x0_lin_constr, min_x1_lin_constr, min_z_lin_constr, marker='o', color='blue', linewidth=10)
ax.plot_surface(X0_lin_1, X1_lin_1, Z_lin_1, color='green', alpha=1)
ax.plot_surface(X0_lin_2, X1_lin_2, Z_lin_1, color='yellow', alpha=1)
ax.set_xlabel('$x_{0}$')
ax.set_ylabel('$x_{1}$')
ax.set_zlabel('$f(x)$')
ax.axes.zaxis.set_ticklabels([])
ax.view_init(89, -90);
目标函数。蓝点是通过应用线性约束确定的最小值,其边界显示为超平面。图片由作者提供。
2.4 非线性约束
我们还可以使用[NonlinearConstraint](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.NonlinearConstraint.html)
对象在非线性约束定义的区域内探索目标函数。假设x₀
和x₁
受到以下约束:
我们优化f(x)
的方法如下:
non_linear_eq= lambda x: x[0]**2 + x[1]**2
non_linear_constr = NonlinearConstraint(
non_linear_eq, 0.2, np.inf)
result = minimize(
f, np.array([0.5, 1]), method='trust-constr', jac=df, constraints=non_linear_constr)
result.x.round(3)
array([-0., 0.447])
类似于之前的示例,我们可以观察到在当前约束下的目标函数和找到的最小值。
可行区域位于由非线性约束定义的圆柱体外部。图片由作者提供。
2.5 结合不同的约束类型
我们可以结合边界以及线性和非线性约束,如下所示:
result = minimize(
f,
x_start,
method='trust-constr',
jac=df,
bounds=bounds,
constraints=[linear_constraint, non_linear_constr]
)
result.x.round(3)
array([0.25, 0.371])
我们指出,并非所有优化方法都支持边界和/或约束。更多信息可以在包文档中找到。
3. 结论
在这篇文章中,我们探索了不同类型的优化约束。特别是,我们分享了使用SciPy
库的实际 Python 示例。这些示例配有图表,可以直观地检查不同的约束。
相关帖子:
在 Python 中寻找最大化对数部分似然的系数
[towardsdatascience.com
介绍 p 值和带有示例的显著性测试
通过示例理解假设检验框架背后的思想
·
关注 发表于 走向数据科学 ·9 min read·2023 年 1 月 18 日
--
引言
伟大的产品不是一夜之间建成的,而是通过多年的迭代不断改进和完善。最成功的团队在开发产品时遵循反馈循环。首先,他们构思一个想法,推向生产环境,并监控这个过程。然后,根据收集的数据,他们分析并确定是否成功。分析获得的见解将指导下一轮的开发。Keras 的创作者弗朗索瓦·肖莱 称之为进展之环[2]。
进步的循环。图片由作者提供。
统计学在分析部分的作用至关重要。它帮助我们通过观察监测数据来检验假设并做出决定。现在,有各种针对不同场景的假设检验方法,但在这篇文章中,我们将通过一个简单的例子来了解假设检验框架的思想和过程。
现实世界中的选择往往并不明确
由 Edge2Edge Media 拍摄,刊登在 Unsplash
在统计学中,我们将选择称为假设。现实世界中的假设往往不清晰。让我来举例说明。考虑两个场景:
-
在场景一中,你得到一个骰子,要求你确定这个骰子是公平的还是有作弊的。
-
在场景二中,你得到两个骰子:一个公平的,一个作弊的。然而,这次你知道作弊骰子的概率分布。如果你随机挑选一个骰子并掷它,你能根据骰子落地的面来确定你挑选的是哪个骰子吗?
这两种场景都是假设检验问题的一种形式。然而,区别在于我们知道后者的备择假设是什么。
场景二更容易处理。有一些明确的方法,比如似然比检验,可以帮助我们根据单次观察做出决定。
然而,大多数现实世界的情况类似于场景一。
在场景一中,我们不知道备择假设是什么。我们只是假设备择假设是骰子有作弊。我们不知道它作弊的程度。因此,假设检验的最终目标就是证明或否定原假设,在这种情况下就是假设骰子是公平的。
这种形式的假设检验也被称为显著性检验,是本文的重点。
如果你想了解如何处理第一类问题,可以考虑阅读我的另一篇文章,其中我们从基本原理学习假设检验。
一份关于假设检验的易懂指南,配有示例和可视化
[towardsdatascience.com
示例——硬币是公平的还是有偏的?
让我们从一个例子开始,并逐步深入。
由 Jizhidexiaohailang 拍摄,刊登在 Unsplash
你得到了一枚硬币,需要判断它是公平的还是有偏的。
让𝜃表示正面朝上的概率。零假设是𝜃 = 1/2,而替代假设是𝜃 ≠ 1/2。
注意到替代假设不明确。𝜃可以是 0 到 1 之间的任何值。
假设检验的关键思想是实验结果依赖于假设。因此,通过观察结果,我们可以得出结论。
在这个例子中,我们投掷硬币n次并观察结果。
让结果用X1, X2, X3, …, Xn
表示,其中Xi
是一个随机变量,表示第i
次投掷的结果。如果硬币落在正面,Xi = 1
,否则Xi = 0
。
拒绝区域和临界比率
让S = X1 + X2 + X3 + … + Xn
表示正面朝上的总数。S被称为统计量,它本质上总结了观察结果。现在,如果零假设成立且硬币是公平的,我们更可能观察到n/2 个正面。如果不是,那么|S — n/2|
的差异会很大。所以一个合理的决策规则是:
reject the null hypothesis if |S - n/2| > 𝜉
这意味着如果观察到的正面数与n/2 之间的绝对差异大于某个值𝜉,我们将拒绝零假设或假设硬币是公平的。𝜉被称为临界比率。
基于这个决策规则,我们可以将观察空间分为两个区域:拒绝区域和接受区域。对于满足决策规则|S — n/2| > 𝜉
的S
值属于拒绝区域,而其余值属于接受区域。
作者提供的图片
区域之间的边界依赖于𝜉。让我们看看如何选择它的值。
显著性水平
在零假设下,观察结果落入拒绝区域的概率称为显著性水平。它可以计算为:
P(reject H0 ; H0) = 𝛼
H0
是零假设。P(reject H0 ; H0)
表示在H0
下观察结果落入拒绝区域的概率。𝛼是显著性水平。
在我们的例子中,它可以表示为:
P(|S - n/2| > 𝜉 ; H0) = 𝛼
现在,𝛼依赖于𝜉。随着𝜉的增加,拒绝区域的大小减少,𝛼也会随之减少。
如果我们固定了𝛼的值,我们可以找到𝜉的值。如果我们知道在零假设下决策规则的分布,我们就可以为给定的𝛼值找到𝜉的值。
让我们运行一个模拟,看看它的实际效果。
通过模拟理解
在我们的例子中,S遵循参数为n和𝜃的二项分布。然而,随着n的增加,S根据中心极限定理接近正态分布[4]。
随着 n 的增加,二项分布趋于正态分布。
所以假设S遵循正态分布,我们可以通过减去均值并除以标准差来标准化它。二项分布的均值为n * 𝜃
,而标准差为sqrt(n * 𝜃 * (1-𝜃))
。
def standardize(S):
return (S - n * 𝜃) / sqrt(n * 𝜃 * (1-𝜃))
假设我们抛掷硬币n=1000
次,并且根据原假设,硬币是公平的,所以𝜃=1/2
。对于显著性水平𝛼=0.05
,𝜉的值可以如下计算:
P(|S - n𝜃| > 𝜉 ; H0) = 𝛼
Divide the inequality by sqrt(n * 𝜃 * (1-𝜃)) which is the std of S
=> P(|S - n𝜃| / sqrt(n * 𝜃 * (1-𝜃)) > 𝜉 / sqrt(n * 𝜃 * (1-𝜃)) ; H0) = 𝛼
Let Z = |S - n𝜃| / sqrt(n * 𝜃 * (1-𝜃))
Z is a standard normal variable as discussed above
=> P(|Z| > 𝜉 / sqrt(n * 𝜃 * (1-𝜃)) ; H0) = 𝛼
=> P(|Z| > 𝜉 / sqrt(1000 * 1/2 * (1-1/2)) = 0.05
=> P(|Z| > 𝜉 / sqrt(250)) = 0.05
According to the standard normal tables, P(Z) = 0.05 if |Z| > 1.96
This is illustrated in the diagram below.
=> Z > 1.96 or Z < -1.96
The minimum value of 𝜉 can be calculated as:
𝜉 / sqrt(250) = 1.96
=> 𝜉 = 31
标准正态分布
所以对于显著性水平𝛼 = 0.05
,临界比率是𝜉 = 31
。这意味着什么?我们的决策规则是:
reject the null hypothesis if |S - n/2| > 𝜉
如果我进行实验并抛掷硬币n=1000
次,观察到正面次数S=472
,则|472–500| = 28 < 31
。
所以我们说H₀在 5%的显著性水平下未被拒绝。
注意我们说H₀未被拒绝而不是说H₀被接受。这是因为我们没有足够强的依据来接受 H₀。根据数据无法证明𝜃确切等于 0.5。𝜃也可能等于 0.51 或 0.49。因此,我们说观察值S=472
不足以在 5%的显著性水平下驳斥原假设 H₀。
在𝜉 = 31
的情况下,观察值S < 469
和S > 531
落入拒绝区域。此外,如果我增加𝜉的值,拒绝区域会如下面的图所示减小:
拒绝区域随着𝜉
的增加而减小。
当我们固定显著性水平𝛼,例如 5%时,这意味着在 H₀支配的模型下,观察值仅有 5%的概率落入拒绝区域。因此,如果观察值最终落入拒绝区域,则提供了 H₀可能错误的强有力证据。
p 值
p 值的正式定义为:
p-值是在假设原假设正确的前提下,获得至少与实际观察结果一样极端的测试结果的概率。 — 维基百科 [3]
与在实验前固定的显著性水平不同,p 值依赖于实验结果。
假设硬币抛掷实验的结果是S=430
,那么在假设原假设正确的前提下,“至少同样极端”的结果是S < 430
和S > 570
,因为在原假设下的分布是对称的并且围绕均值中心。
我通过改变𝜃模拟了结果s,每个结果的 p 值如下所示:
基于𝜃变化的 p 值
实质上,p 值是𝛼的值,使得s恰好处于拒绝与不拒绝的临界值之间。
正态近似的必要性
在上述示例中,我们知道结果遵循具有参数n和𝜃的二项分布。因此,我们可以直接使用任何统计库基于𝛼计算临界比率𝜉,而无需使用正态近似。
然而,在大多数情况下,我们不会知道零假设下的分布。然而,大多数情况下,随着样本量的增加,它们趋向于近似正态分布。因此,在假设检验实验中使用大样本量是很重要的。
结论
在科学中,科学家通常提出一个理论,其他科学家通过进行实验来证明或反驳它。他们进行假设检验,其中原始想法是零假设。
假设检验的一般框架可以总结如下[1]:
-
选择一个代表观察数据的统计量 S。它是一个取决于观察结果的标量随机变量。通常,样本均值或样本方差被用作统计量。
-
制定一个拒绝零假设 H₀的决策规则。决策规则是统计量 S 和临界比率𝜉的函数。根据决策规则和𝜉,可以将观察空间划分为拒绝区域和接受区域。
-
选择显著性水平𝛼,它是观察结果在零假设下落入拒绝区域的概率。
-
基于显著性水平𝛼计算临界比率𝜉。为了进行此计算,需要了解零假设下的分布。然而,正如我们讨论的,如果样本量较大,大多数情况下可以将其近似为正态分布。一旦知道了𝜉的值,就可以确定拒绝区域。
一旦我们进行实验并记录了观察结果,就需要做以下事情:
-
计算统计量 S的值s*。
-
如果s属于拒绝区域,则拒绝零假设。
假设检验的美妙之处在于没有任何约束。我们可以自由设计实验,并选择零假设和备择假设。
希望你喜欢这篇文章。
**Let's Connect** Hope you've enjoyed the article. Please clap and follow if you did.
You can also reach out to me on [LinkedIn](https://www.linkedin.com/in/neerajkrishnadev/) and [Twitter](https://twitter.com/WingedRasengan).
图像和图示来源
本文中的所有图像、图形和示意图均由作者创建;除非在说明中明确提及。
参考文献
使用 Sklearn、Pandas 和 Matplotlib 进行 PCA 的介绍
原文:
towardsdatascience.com/introduction-to-pca-in-python-with-sklearn-pandas-and-matplotlib-476880f30238
通过将多维数据集转换为任意数量的维度,并使用 Matplotlib 可视化降维数据,来学习 PCA 在 Python 和 Sklearn 中的直观感受
·发表于面向数据科学 ·13 分钟阅读·2023 年 9 月 6 日
--
图片由Nivenn Lanos提供,来源于Unsplash
作为数据分析师和科学家,我们经常面临由于信息量不断增长而带来的复杂挑战。
不可否认,从各种来源积累的数据已经成为我们生活中的常态。不管是否是数据科学家,几乎每个人都将现象描述为一组变量或属性。
在解决分析挑战时,处理多维数据集是非常罕见的——这在今天尤其明显,因为数据收集越来越自动化,技术使我们能够从各种来源获取信息,包括传感器、物联网设备、社交媒体、在线交易等等。
但随着现象的复杂性增加,数据科学家面临的挑战也会增加,以实现他们的目标。
这些挑战可能包括…
-
高维度:拥有许多列可能导致高维度问题,这会使模型变得更加复杂且难以解释。
-
噪声数据:数据的自动收集可能会导致错误、缺失数据或不可靠的数据。
-
解释:高维度意味着低可解释性——很难理解某个问题的最有影响力的特征是什么。
-
过拟合:过于复杂的模型可能会遭遇过拟合,即对训练数据的过度适应,导致在新数据上的泛化能力低。
-
计算资源:分析大型复杂数据集通常需要大量计算资源。可扩展性是一个重要考虑因素。
-
结果的沟通:从多维数据集中得到的发现以易于理解的方式进行解释是一项重要挑战,尤其是在与非技术利益相关者沟通时。
我写了一篇与此话题相关的文章,你可以在这里阅读
特征工程的活动对于提升预测模型的性能非常有用。然而,它…
towardsdatascience.com
数据科学和机器学习中的多维性
在数据科学和机器学习中,多维数据集是一个组织化的数据集合,包括多个列或属性,每个列或属性表示现象研究对象的一个特征(或属性)。
包含有关房屋信息的数据集是一个具体的多维数据集的例子。事实上,每栋房屋可以用其平方米、房间数量、是否有车库等特征来描述。
在这篇文章中,我们将探讨如何有效地使用 PCA 来简化和可视化多维数据,使复杂的多维信息变得易于理解。
通过遵循本指南,你将学习:
-
PCA 算法背后的直觉
-
在玩具数据集上应用 Sklearn 的 PCA
-
使用 Matplotlib 来 可视化减少的数据
-
PCA 在数据科学中的主要用例
让我们开始吧!
PCA 算法的基本直觉
主成分分析(PCA)是一种无监督统计技术,用于多维数据的分解。
其主要目的是将我们的多维数据集减少为若干个任意变量,以便于
-
选择原始数据集中重要的特征
-
提高信噪比
-
创建新的特征以提供给机器学习模型
-
可视化多维数据
根据我们选择的组件数量,PCA 算法通过保留那些最能解释数据集总方差的变量,实现对原始数据集中变量数量的减少。PCA 对抗着那个臭名昭著的维度灾难。
维度灾难是机器学习中的一个概念,指的是处理高维数据时遇到的困难。
随着尺寸的增加,为了可靠地表示一组数据所需的数据量呈指数增长。这可能使得在数据中发现有趣的模式变得困难,并且可能在自动学习模型中导致过拟合问题。
PCA 应用的转换通过创建最能解释原始数据方差的组件来减少数据集的大小。这使得可以隔离最相关的变量,并减少数据集的复杂性。
PCA 通常是一项难以理解的技术,特别是对于数据科学和分析领域的新手。
这种困难的原因必须追溯到算法的严格数学基础。
那么,从数学角度看,PCA 做了什么?
PCA 允许我们将一个 n 维数据集投影到一个低维平面上。
看起来很复杂,但实际上并不是。让我们用一个简单的例子试试:
当我们在纸上画东西时,我们实际上是在将一个心理表征(我们可以用三维表示)投影到纸上。这样做会降低表征的质量和精确度。
然而,纸上的表征仍然可以理解,甚至可以与我们的同龄人分享。
实际上,在绘制过程中,我们表示形式、线条和阴影,以便让观察者理解我们在脑海中想到的内容。
让我们以这张鲨鱼的图片为例:
Foto di David Clode su Unsplash
如果我们想在一张纸上绘制它,根据我们的技能水平(正如你所见,我的技能水平很低),我们可以这样表示:
作者提供的图像。
问题在于,尽管表征并不是完美的 1:1,观察者仍然可以很容易理解这幅画代表了一只鲨鱼。
实际上,我们使用的“心理”算法类似于 PCA——我们降低了维度,从而降低了摄影中的鲨鱼特征,并仅使用了最相关的维度来在纸上传达“鲨鱼”的概念。
从数学角度来说,我们不仅仅想把对象投影到一个低维平面上,而且我们还希望尽可能保留最相关的信息。
数据压缩
我们将使用一个简单的数据集进行示例。这个数据集包含了房屋的结构信息,例如平方米大小、房间数量等。
示例数据集。作者提供的图像。
这里的目标是展示在处理多维数据集时,数据可视化的局限性是多么容易接近,以及 PCA 如何帮助我们克服这些局限性。
数据集的维度可以简单地理解为其中列的数量。一列代表我们研究现象的一个属性或特征。维度越多,现象越复杂。
在这种情况下,我们有一个 5 维的数据集。
但数据可视化中存在哪些局限性?让我通过分析平方米变量来解释一下。
图片由作者提供。
房子 1 和 2 的平方米值较低,而其他房子的值都在 100 左右或更高。这是一个一维图,因为我们只考虑了一个变量。
现在让我们为图表添加一个维度。
图片由作者提供。
这种图表被称为散点图,显示了两个变量之间的关系。它非常有助于可视化变量之间的相关性和交互作用。
这种可视化已经开始引入较高的解释复杂性,因为即使是专家分析师也需要仔细检查才能理解变量之间的关系。
现在让我们再插入一个变量。
图片由作者提供。
这绝对是一个复杂的图像处理问题。然而,从数学角度来看,这种可视化是完全合理的。从感知和解释的角度来看,我们已经达到了人类理解的极限。
我们都知道,我们对世界的解释停留在三维。然而,我们也知道这个数据集具有 5 个维度。
但我们如何查看所有这些?
我们不能,除非我们将所有变量之间的二维关系并排可视化。
在下面的例子中,我们可以看到平方米与n_rooms和n_neighbors在两个维度上的关系。
图片由作者提供。
现在让我们想象将所有可能的组合放在一起……我们很快会被需要记住的大量信息所淹没。
这就是 PCA 发挥作用的地方。使用 Python(稍后会看到),我们可以对这个数据集应用 Sklearn 的 PCA 类,并获得这样的图表。
图片由作者提供。
我们看到的是一个显示 PCA 返回的主成分的图表。
实际上,PCA 算法对数据执行线性变换,以找到最佳解释数据集总方差的特征线性组合。
这种特征组合被称为主成分。这个过程会对每个主要成分重复,直到达到所需的成分数量。
使用 PCA 的优势在于,它允许我们通过保留最重要的信息来减少数据的维度,消除不相关的数据,使数据更容易可视化和用于构建机器学习模型。
如果你有兴趣深入了解 PCA 背后的数学,我建议以下英文资源:
-
主成分分析 (PCA) 视觉解释,无需数学
Python 实现
要在 Python 中应用 PCA,我们可以使用 scikit-learn,它提供了一个简单有效的实现。
在此链接中您可以阅读 PCA 文档。
我们将使用 wine dataset 作为示例的玩具数据集。wine dataset 是 Scikit-Learn 的一部分,并且在知识共享许可协议下,署名 4.0,免费使用和共享(许可证可在这里查看)。
让我们开始必需的库
# import the required libs
from sklearn.datasets import load_wine
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
# load the dataset
wine = load_wine()
# convert the object in a pandas dataframe
df = pd.DataFrame(data=wine.data, columns=wine.feature_names)
df["target"] = wine.target
df
>>>
图片来源于作者。
数据的维度是 (178, 14) —— 这意味着有 178 行(机器学习模型可以学习的示例),每行由 14 个维度描述。
我们需要在应用 PCA 之前进行数据标准化。您可以使用 Sklearn 完成这一步。
# normalize data
from sklearn.preprocessing import StandardScaler
X_std = StandardScaler().fit_transform(df.drop(columns=["target"]))
使用 PCA 时重要的是对数据进行标准化: 它计算数据集的新投影,新轴基于变量的标准差。
我们现在准备好减少大小。我们可以这样简单地应用 PCA
# PCA object specifying the number of principal components desired
pca = PCA(n_components=2) # we want to project two dimensions so that we can visualize them!
# We fit the PCA model on standardized data
vecs = pca.fit_transform(X_std)
我们可以指定任何数量的 PCA 输出维度,只要它们少于 14,这是原始数据集的总维度。
现在让我们将数据框的小版本组织成一个新的 Pandas Dataframe 对象:
reduced_df = pd.DataFrame(data=vecs, columns=['Principal Component 1', 'Principal Component 2'])
final_df = pd.concat([reduced_df, df[['target']]], axis=1)
final_df
>>>
图片来源于作者。
主成分 1 和 2 是 PCA 的输出维度,现在可以用散点图进行可视化。
plt.figure(figsize=(8, 6)) # set the size of the canvas
targets = list(set(final_df['target'])) # we create a list of possible targets (there are 3)
colors = ['r', 'g', 'b'] # we define a simple list of colors to differentiate the targets
# loop to assign each point to a target and color
for target, color in zip(targets, colors):
idx = final_df['target'] == target
plt.scatter(final_df.loc[idx, 'Principal Component 1'], final_df.loc[idx, 'Principal Component 2'], c=color, s=50)
# finally, we show the graph
plt.legend(targets, title="Target", loc='upper right')
plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('PCA on Wine Dataset')
plt.show()
减少数据集的效果图。图片来源于作者。
就这样。这个图表展示了由 14 个初始变量描述的葡萄酒之间的差异,但通过 PCA 减少到 2 个维度。PCA 保留了相关信息,同时减少了数据集中的噪声。
这是使用 Sklearn、Pandas 和 Matplotlib 在 Python 中应用 PCA 的完整代码。
import pandas as pd
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
wine = load_wine()
df = pd.DataFrame(data=wine.data, columns=wine.feature_names)
df["target"] = wine.target
from sklearn.preprocessing import StandardScaler
X_std = StandardScaler().fit_transform(df.drop(columns=["target"]))
pca = PCA(n_components=2)
vecs = pca.fit_transform(X_std)
reduced_df = pd.DataFrame(data=vecs, columns=['Principal Component 1', 'Principal Component 2'])
final_df = pd.concat([reduced_df, df[['target']]], axis=1)
plt.figure(figsize=(8, 6))
targets = list(set(final_df['target']))
colors = ['r', 'g', 'b']
for target, color in zip(targets, colors):
idx = final_df['target'] == target
plt.scatter(final_df.loc[idx, 'Principal Component 1'], final_df.loc[idx, 'Principal Component 2'], c=color, s=50)
plt.legend(targets, title="Target", loc='upper right')
plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('PCA on the Wine Dataset')
plt.show()
PCA 的应用场景
以下是数据科学中最常见的 PCA 用例列表。
提高机器学习模型训练速度
PCA 压缩的数据提供了重要信息,并且更容易被机器学习模型消化,现在模型基于少量特征进行学习,而不是原始数据集中所有的特征。
特征选择
PCA 本质上是一种特征选择工具。应用 PCA 时,我们寻找那些能最好地解释数据集方差的特征。
我们可以对主成分进行排名,并按重要性进行排序,第一主成分解释最多的方差,最后一个主成分解释最少的方差。
通过分析主成分,可以回到原始特征,并排除那些对保留 PCA 创建的降维平面中的信息没有贡献的特征。
异常检测
PCA 常用于异常识别,因为它可以帮助识别数据中用肉眼难以辨别的模式。
异常通常表现为在较低维空间中远离主要群体的数据点,使它们更容易被检测到。
信号检测
与异常识别相比,PCA 在信号检测中也非常有用。
确实,正如 PCA 可以突出异常值,它也可以去除不贡献于数据总变异性的“噪声”。在语音识别的背景下,这使得用户能够更好地隔离语音痕迹,并改进基于语音的人物识别系统。
图像压缩
如果我们有特定的约束,例如将图像保存为特定格式,那么处理图像可能很昂贵。简而言之,PCA 可以用于压缩图像,同时仍保持其中的信息。
这使得机器学习算法可以更快地训练,但会以压缩信息的某种质量为代价。
结论
感谢您的关注 🙏 我希望你享受阅读并学到了新的知识。
总结
-
你了解了数据集的维度意味着什么以及拥有多个维度所带来的局限性
-
你了解了 PCA 算法如何直观地一步步工作
-
你学会了如何使用 Sklearn 在 Python 中实现 PCA
-
最后,你了解了 PCA 在数据科学中的最常见应用场景
如果你觉得这篇文章有用,请与你的朋友或同事分享。
下次见,
安德鲁
如果你想支持我的内容创作活动,请随意使用下面的推荐链接加入 Medium 的会员计划。我将获得你投资的一部分,你将能够无缝地访问 Medium 上大量的数据科学及其他领域的文章。
[## 使用我的推荐链接加入 Medium - 安德烈亚·达戈斯蒂诺]
作为 Medium 会员,你的会员费用的一部分将用于支持你阅读的作者,而你可以完全访问每一个故事……
medium.com](https://medium.com/@theDrewDag/membership?source=post_page-----476880f30238--------------------------------)
推荐阅读
对感兴趣的人,这里有一份我推荐的关于每个 ML 相关主题的书单。这些书在我看来是必读的,并且对我的职业生涯产生了重大影响。
免责声明:这些是亚马逊的联盟链接。我将从亚马逊获得少量佣金作为推荐这些商品的回报。您的体验不会改变,您也不会多付费用,但这将帮助我扩大业务并制作更多与 AI 相关的内容。
-
机器学习简介: 自信的数据技能:掌握数据处理基础知识,提升职业生涯 作者 基里尔·埃雷门科
-
Sklearn / TensorFlow: 实战机器学习:使用 Scikit-Learn、Keras 和 TensorFlow 作者 奥雷利安·热龙
-
NLP: 文本作为数据:机器学习与社会科学的新框架 作者 贾斯廷·格里默
-
Sklearn / PyTorch: 使用 PyTorch 和 Scikit-Learn 进行机器学习:用 Python 开发机器学习和深度学习模型 作者 塞巴斯蒂安·拉什卡
-
数据可视化: 用数据讲故事:商业专业人士的数据可视化指南 作者 科尔·克纳夫利克
有用的链接(由我撰写)
-
学习如何在 Python 中执行一流的探索性数据分析: Python 中的探索性数据分析——逐步过程
-
学习 TensorFlow 基础知识: 开始使用 TensorFlow 2.0 —— 深度学习入门
-
使用 TF-IDF 在 Python 中进行文本聚类: 在 Python 中使用 TF-IDF 进行文本聚类
PyTorch 介绍
了解 PyTorch 项目的工作流程
·
关注 发表在 Towards Data Science · 13 分钟阅读 · 2023 年 3 月 1 日
--
图片来源:Igor Lepilin 在 Unsplash
在本文中,我们将通过使用 PyTorch 的深度学习项目生命周期。我们假设你已经对神经网络有一定了解,因此不会详细解释它们,只会关注 PyTorch 特有的方面。我们将主要遵循官方文档中展示的步骤,但考虑一个不同的例子。在文档中展示的是图像分类的例子,而这里我们将考虑存储在.csv 文件中的表格数据。这意味着一些更改是必要的,特别是在准备数据集时。拥有两个不同的例子应该有助于更好地理解 PyTorch 项目的一般工作流程。除了这篇文章,你还可以查看包含完整代码和工作流程结构的colab 笔记本,或者在GitHub上找到该笔记本。
数据
使用的数据从kaggle [1]下载,且免费提供。这些数据描述了在城市环境中确定水质所需的不同特征。目标是预测水质是好还是坏。也就是说,我们正在考虑一个二分类问题。总共有 9 个特征(均为数值型)和标签。
给定数据集的特征和标签。
预处理
与每个数据科学项目一样,首先需要进行一些预处理。这与我们使用的深度学习框架无关。因此,我们在这里不详细讨论。有关更多信息,请参见colab 笔记本或GitHub。
我们很幸运,数据不需要太多准备。所有特征都是数值型和浮点型。然而,有一些缺失值,我们用相应特征的均值进行了填补。还需考虑的是,目标变量并不均匀分布,0(差的水质)的数量远多于 1(良好的水质)。为了简化,我们对数据进行了上采样,使得 0 的数量与 1 的数量相等,通过从 1 的子集随机抽样直到达到相同数量的样本。最后,我们将数据集划分为训练集(80%)、验证集(10%)和测试集(10%),并对数据进行了缩放。
创建一个 PyTorch 数据集
为了在 PyTorch 模型中使用我们的数据,我们需要将其转化为特定的形式:PyTorch 数据集。该数据集的构建与模型解耦。数据集对象存储样本及其对应的标签。此时,这个例子略微偏离了 PyTorch 文档页面。文档中使用的示例数据是 FashionMNIST。对于这个(和其他几个)数据集,PyTorch 提供了预加载的数据集。要了解如何加载这些数据集,可以查看他们的 PyTorch 教程。然而,如果你想用 PyTorch 处理自己的数据,你很可能需要编写自己的自定义数据集类。
要创建自定义的数据集类,我们可以从 PyTorch 提供的 Dataset 类继承。我们需要调整以下三个主要方法:
init 方法在实例化数据集对象时运行一次。在这个简单的例子中,仅将输入和标签作为张量存储。
len 方法返回数据集中样本的数量。
getitem 方法加载并返回数据集中给定索引处的样本。
数据集也是进行变换的地方,例如处理图像数据时。在我们的表格数据中,这一点不相关,因此在这里不涉及。对于这里考虑的水质问题,自定义数据集类如下所示:
class WaterDataset(Dataset):
def __init__(self, X, y):
# The __init__ method is run once when instantiating the Dataset object
self.X = torch.tensor(X)
self.y = torch.tensor(np.array(y).astype(float))
def __len__(self):
# The __len__ method returns the number of samples in our dataset.
return len(self.y)
def __getitem__(self, idx):
# The __getitem__ method loads and returns a sample from the dataset
# at the given index idx.
return self.X[idx], self.y[idx]
使用这个类,我们定义训练、验证和测试数据集。
train_dataset = WaterDataset(X_train, y_train)
val_dataset = WaterDataset(X_val, y_val)
test_dataset = WaterDataset(X_test, y_test)
定义 DataLoader
创建数据集后,我们使用 PyTorch 的 DataLoader 将可迭代对象包装起来,以便在训练和验证期间轻松访问数据。数据集一次获取一个样本的特征和标签。在训练模型时,我们通常希望以批次的形式传递样本,并在每个 epoch 重新洗牌数据。在这个例子中,迭代 DataLoader 时,每次迭代返回一个包含 32 个样本的迷你批次。可以进一步配置 DataLoader。有关所有可能的配置,请参阅 文档。
train_dataloader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_dataloader = DataLoader(val_dataset, batch_size=32)
test_dataloader = DataLoader(test_dataset, batch_size=32)
定义一个模型
现在,数据已经准备好,我们可以定义模型了。我们假设你对神经网络的基本结构有所了解。在 PyTorch 中,torch.nn 命名空间提供了创建神经网络的所有构建模块。我们在这个例子中使用的模型非常简单,仅包含 线性层、ReLu 激活函数 和 Dropout 层。有关 PyTorch 中所有预定义层的概述,请参阅 文档。
我们可以通过继承nn.Module来构建自己的模型。一个 PyTorch 模型至少包含两个方法。init 方法,其中实例化了所有需要的层,和forward 方法,其中定义了最终模型。以下是一个示例模型,能够为我们的示例数据提供足够好的结果。
class WaterNet(nn.Module):
def __init__(self):
super().__init__()
# define 4 linear layers
# the first input dimension is the number of features
# the output layer is 1
# the other in and output parameters are hyperparamters and can be changed
self.fc1 = nn.Linear(in_features=9, out_features=64)
self.fc2 = nn.Linear(in_features=64, out_features=32)
self.fc3 = nn.Linear(in_features=32, out_features=16)
self.fc4 = nn.Linear(in_features=16, out_features=1)
self.relu = nn.ReLU()
self.dropout = nn.Dropout()
def forward(self, x):
# apply the linear layers with a relu activation
x = self.fc1(x)
x = self.relu(x)
x = self.dropout(x)
x = self.fc2(x)
x = self.relu(x)
x = self.fc3(x)
x = self.relu(x)
x = self.fc4(x)
return x.squeeze()
该模型由四个线性层组成。输入和输出特征的数量已定义,这决定了输入和输出样本的大小。我们的数据包含 9 个特征,因此第一层的输入特征数量为 9。输出特征的大小可以更改,但必须与下一个输入特征的大小匹配。由于我们最终需要一个 1 维的输出(0 或 1),因此最后的输出特征大小为 1。注意,这里没有应用最终的 sigmoid 层。我们将在下一节中解释这一点。
训练模型
接下来,我们需要训练模型。在 PyTorch 中训练模型包括四个主要步骤:
-
应用模型
-
计算损失
-
反向传播
-
更新权重
要训练一个轮次,这些步骤需要在所有批次的train_dataloader上完成。然后需要另一个循环遍历所需的轮次数。伪代码中一个轮次的训练如下:
for batch in train_dataloader:
# apply model
y_hat = model(x)
# calculate loss
loss = loss_function(y_hat, y)
# backpropagation
loss.backward
# update weights
optimizer.step()
优化器和损失函数仍需定义。我们将在下一节中完成这部分。以下是包含这个训练循环的函数。此外,还计算了一些指标(准确率、召回率和精度)。注意,我们将模型设置为训练模式(model.train()
),而不是评估模式(model.eval()
)。这会影响 dropout 或批归一化层,这些层在训练和验证时的处理方式不同。该函数的输入是
1. 模型。这是上面定义的模型。
2. 设备。这可以是 GPU 或 CPU,将在下一节中设置。
3. train_dataloader。上述定义的用于训练数据的数据加载器。
4. 优化器。优化器用于最小化误差。我们将在下一节中具体说明。
5. 损失函数(标准)。我们将在下一节中具体说明。
6. 训练轮次。当前的训练轮次。
def train(model, device, train_dataloader, optimizer, criterion, epoch, print_every):
'''
parameters:
model - the model used for training
device - the device we work on (cpu or gpu)
train_dataloader - the training data wrapped in a dataloader
optimizer - the optimizer used for optimizing the parameters
criterion - loss function
epoch - current epoch
print_every - integer after how many batches results should be printed
'''
# create empty list to store the train losses
train_loss = []
# variable to count correct classified samples
correct = 0
# variable to count true positive, false positive and false negative samples
TP = 0
FP = 0
FN = 0
# create an empty list to store the predictions
predictions = []
# set model to training mode, i.e. the model is used for training
# this effects layers like BatchNorm() and Dropout
# in our simple example we don't use these layers
# for the sake of completeness 'model.train()' is included
model.train()
# loop over batches
for batch_idx, (x, y) in enumerate(train_dataloader):
# set data to device
x, y = x.to(device), y.to(device)
# set optimizer to zero
optimizer.zero_grad()
# apply model
y_hat = model(x.float())
# calculate loss
loss = criterion(y_hat, y.float())
train_loss.append(loss.item())
# backpropagation
loss.backward()
# update the weights
optimizer.step()
# print the loss every x batches
if batch_idx % print_every == 0:
percent = 100\. * batch_idx / len(train_dataloader)
print(f'Train Epoch {epoch} \
[{batch_idx * len(train_dataloader)}/{len(train_dataloader.dataset)} \
({percent:.0f}%)] \tLoss: {loss.item():.6f}')
# calculate some metrics
# to get the predictions, we need to apply the sigmoid layer
# this layer maps the data to the range [0,1]
# we set all predictions > 0.5 to 1 and the rest to 0
y_pred = torch.sigmoid(y_hat) > 0.5
predictions.append(y_pred)
correct += (y_pred == y).sum().item()
TP += torch.logical_and(y_pred == 1, y == 1).sum()
FP += torch.logical_and(y_pred == 1, y == 0).sum()
FN += torch.logical_and(y_pred == 0, y == 1).sum()
# total training loss over all batches
train_loss = torch.mean(torch.tensor(train_loss))
epoch_accuracy = correct/len(train_dataloader.dataset)
# recall = TP/(TP+FN)
epoch_recall = TP/(TP+FN)
# precision = TP/(TP+FP)
epoch_precision = TP/(TP+FP)
return epoch_accuracy, train_loss, epoch_recall, epoch_precision
模型的验证类似,但没有反向传播,也没有更新权重。伪代码中一个轮次的验证如下。
for batch in val_dataloader:
# apply model
y_hat = model(x)
# calculate loss
loss = loss_function(y_hat, y)
以下是一个用于验证一个轮次的函数。它与前面用于训练的函数非常相似。注意,模型设置为评估模式(model.eval()
),并且由于在验证过程中不需要计算梯度,我们设置了 with torch.no_grad()
。这将减少计算时的内存消耗。
def valid(model, device, val_dataloader, criterion):
'''
parameters:
model - the model used for training
device - the device we work on (cpu or gpu)
val_dataloader - the validation data wrapped in a dataloader
criterion - loss function
'''
# create an empty list to store the loss
val_loss = []
# variable to count correct classified samples
correct = 0
# variable to count true positive, false positive and false negative samples
TP = 0
FP = 0
FN = 0
# create an empty list to store the predictions
predictions = []
# set model to evaluation mode, i.e.
# the model is only used for inference, this has effects on
# dropout-layers, which are ignored in this mode and batchnorm-layers, which use running statistics
model.eval()
# disable gradient calculation
# this is useful for inference, when we are sure that we will not call Tensor.backward().
# It will reduce memory consumption for computations that would otherwise have requires_grad=True.
with torch.no_grad():
# loop over batches
for x, y in val_dataloader:
# set data to device
x, y = x.to(device), y.to(device)
# apply model
y_hat = model(x.float())
# append current loss
loss = criterion(y_hat, y.float())
val_loss.append(loss.item())
# calculate some metrics
# to get the predictions, we need to apply the sigmoid layer
# this layer maps the data to the range [0,1]
# we set all predictions > 0.5 to 1 and the rest to 0
y_pred = torch.sigmoid(y_hat) > 0.5
predictions.append(y_pred)
correct += (y_pred == y).sum().item()#y_pred.eq(y.view_as(y_pred)).sum().item()
TP += torch.logical_and(y_pred == 1, y == 1).sum()
FP += torch.logical_and(y_pred == 1, y == 0).sum()
FN += torch.logical_and(y_pred == 0, y == 1).sum()
# total validation loss over all batches
val_loss = torch.mean(torch.tensor(val_loss))
epoch_accuracy = correct/len(val_dataloader.dataset)
# recall = TP/(TP+FN)
epoch_recall = TP/(TP+FN)
# precision = TP/(TP+FP)
epoch_precision = TP/(TP+FP)
print(f'Validation: Average loss: {val_loss.item():.4f}, \
Accuracy: {epoch_accuracy:.4f} \
({100\. * correct/len(val_dataloader.dataset):.0f}%)')
return predictions, epoch_accuracy, val_loss, epoch_recall, epoch_precision
综合起来
要使用 PyTorch 训练神经网络,我们需要做以下几步:
- 从数据中生成数据集并将其包装成数据加载器
- 我们在前面的部分已经做过这些,然而为了完整性,我们将再次生成它们。
2. 定义模型
我们使用上述定义的模型WaterNet
。
- 定义优化器
- 在 PyTorch 中有不同的优化器。我们使用 Adam 优化器,这是一种非常常见的优化器。不过你也可以尝试不同的优化器。Adam 优化器是随机梯度下降的扩展。简单来说,区别在于随机梯度下降在训练过程中保持学习率不变,而 Adam 则对其进行调整。关于 Adam 优化器的介绍可以在这里找到。
- 定义损失函数
-
我们正在考虑一个 1 维输出的二分类问题,这种问题的默认选择是二元交叉熵,我们将使用它。
-
请注意,我们没有在模型中应用最终的sigmoid 层。这是故意的,因为 PyTorch 提供了
[nn.BCEWithLogitsLoss()](https://pytorch.org/docs/stable/generated/torch.nn.BCEWithLogitsLoss.html)
方法,它将最终的 sigmoid 层和二元交叉熵结合起来。我们也可以单独应用这两种方法,即将[nn.Sigmoid](https://pytorch.org/docs/stable/generated/torch.nn.Sigmoid.html)
层作为模型的最后一步,然后使用[nn.BCE()](https://pytorch.org/docs/stable/generated/torch.nn.BCELoss.html)
损失。不过,使用nn.BCEWithLogitsLoss()
是处理二分类问题的推荐方式,因为它在数值上更稳定。
在我们开始训练模型之前,我们设置超参数。超参数是可调节的参数,允许你控制模型优化过程。不同的超参数值可以影响模型的训练和收敛速度。在我们的例子中,我们有三个超参数需要设置。请注意,模型层的输入和输出特征也是超参数。我们将它们设置为固定值,不过你可以尝试不同的值。
-
batch_size
:训练和验证批次大小 -
epochs
:训练的周期数 -
learning_rate
:学习率
我们还设置了变量print_every
。这不是一个超参数,而只是决定在训练和验证过程中多频繁打印损失。请注意,如果有 GPU 可用,我们还需要手动将设备设置为“cuda”。
# hyperparamters
batch_size = 32
epochs = 150
learning_rate = 1e-3
print_every = 200
# set device to GPU, if available
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# create datasets
train_dataset = WaterDataset(X_train, y_train)
valid_dataset = WaterDataset(X_val, y_val)
# wrap into dataloader
train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, drop_last=True)
val_dataloader = DataLoader(valid_dataset, batch_size=batch_size)
# define the model and move it to the available device
model = WaterNet().to(device)
# define the optimizer
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)#, weight_decay=1e-4)
# define loss
criterion = nn.BCEWithLogitsLoss()
现在,我们准备开始训练。下面是最终的训练循环。此外,计算得到的指标会为每个周期保存。
# create empty lists to store the accuracy and loss per validation epoch
train_epoch_accuracy = []
train_epoch_loss = []
train_epoch_recall = []
train_epoch_precision = []
val_epoch_accuracy = []
val_epoch_loss = []
val_epoch_recall = []
val_epoch_precision = []
# loop over number of epochs
for epoch in range(epochs):
# train model for each epoch
train_accuracy, train_loss, train_recall, train_precision = \
train(model, device, train_dataloader, optimizer, criterion, epoch, print_every)
# save training loss and accuracy for each epoch
train_epoch_accuracy.append(train_accuracy)
train_epoch_loss.append(train_loss)
train_epoch_recall.append(train_recall)
train_epoch_precision.append(train_precision)
# validate model for each epoch
predictions, val_accuracy, val_loss, val_recall, val_precision = \
valid(model, device, val_dataloader, criterion)
# save validation loss and accuracy for each epoch
val_epoch_accuracy.append(val_accuracy)
评估结果
为了评估结果,我们可以查看训练和评估过程中的损失和指标。
训练和验证的损失为 150 个周期。
训练和验证的回顾持续 150 个周期。
你可以在笔记本中找到其他计算指标的图表。
保存模型
如果我们以后想使用我们的模型,我们需要保存它。我们可以通过保存它的state_dict()
来做到这一点。
torch.save(model.state_dict(), 'water_model_weights.pth')
我们可以用以下方式加载它
model = WaterNet()
model.load_state_dict(torch.load('water_model_weights.pth'))
model.eval()
不要忘记使用model.eval()
将模型设置为评估模式,以将丢弃和批量归一化层设置为评估模式。
将模型应用于测试集
现在我们将训练好的模型应用于测试数据。
# create a dataset
test_dataset = WaterDataset(X_test, y_test)
# wrap into dataloader
test_dataloader = DataLoader(test_dataset, batch_size=batch_size)
predictions, test_accuracy, test_loss, test_recall, test_precision = \
valid(model, device, val_dataloader, criterion)
结论
本文展示了如何使用 PyTorch 进行深度学习项目的详细示例。讨论并应用了深度学习工作流中的各个步骤到一个具体的数据集。在训练模型之前,一个重要的步骤是将数据转换为正确的形式,并为特定应用定义一个定制的数据集。在训练模型时,四个主要步骤是(1)应用模型,(2)计算损失,(3)进行反向传播,(4)更新权重。定义并应用了一个包含所有这些步骤的示例训练函数。最后,要使用模型,了解如何存储和重新加载模型是很重要的。
资源
[1] Aditya Kadiwal, 2021, 水质 — 水质分类数据集, www.kaggle.com/datasets/mssmartypants/water-quality
, 下载于 2023 年 1 月, 许可证:CC0: 公共领域
[2] PyTorch, 2022, pytorch.org/tutorials/beginner/basics/intro.html
除非另有说明,所有图片均为作者提供。
在这里查看更多数据科学和机器学习的帖子:
[## 更多
数据科学和机器学习博客
datamapu.com medium.com [## 每当 Pumaline 发布时获得电子邮件。
每当 Pumaline 发布时获得电子邮件。通过注册,如果你还没有 Medium 账户,将会创建一个…
medium.com www.buymeacoffee.com [## Pumaline
嗨,我喜欢学习和分享有关数据科学和机器学习的知识。
PyTorch 简介:从训练循环到预测
原文:
towardsdatascience.com/introduction-to-pytorch-from-training-loop-to-prediction-a70372764432
对 PyTorch 训练循环和应对库陡峭初学曲线的一般方法的介绍
·发布在Towards Data Science ·14 分钟阅读·2023 年 3 月 28 日
--
作者提供的图片。
在这篇文章中,我们将讨论如何在 Python 中使用 PyTorch 实现逻辑回归模型。
PyTorch 是全球数据科学家和机器学习工程师社区中最著名和最常用的深度学习框架之一,因此,如果你希望在应用 AI 领域建立职业生涯,学习这个工具是你学习路径中的一个关键步骤。
它与 TensorFlow 并列,TensorFlow 是由 Google 开发的另一个非常著名的深度学习框架。
除了 API 的结构和组织可能非常不同外,基本原理没有显著的差别。
虽然这两个框架都允许我们创建非常复杂的神经网络,但 PyTorch 通常更受欢迎,因为它的风格更加python 化,并且允许开发者将自定义逻辑集成到软件中。
我们将使用Sklearn 乳腺癌数据集,这是一个开源数据集,之前在我一些文章中已被使用,用于训练一个二分类模型。
目标是解释如何:
-
从 pandas 数据框转换到 PyTorch 的 Datasets 和 DataLoaders
-
在 PyTorch 中创建一个用于二分类的神经网络
-
创建预测
-
使用实用函数和 matplotlib 评估我们的模型表现
-
使用这个网络进行预测
到文章末尾,我们将对如何在 PyTorch 中创建神经网络以及训练循环的工作原理有清晰的了解。
开始吧!
安装 PyTorch 及其他依赖
我们通过在专用文件夹中创建虚拟环境来开始我们的项目。
访问此链接以了解如何使用 Conda 创建虚拟环境。
如何安装、激活和使用用于机器学习和数据科学相关任务的虚拟环境
[towardsdatascience.com
一旦我们的虚拟环境创建完成,我们可以运行以下命令
$ pip install torch -U
在终端中。该命令将安装 PyTorch 的最新版本,截至撰写本文时为 2.0 版。
启动一个笔记本,我们可以在执行import torch
之后使用torch.__version__
检查库的版本。
我们可以通过导入并运行一个小测试脚本来验证 PyTorch 是否在环境中正确安装,如官方指南所示。
import torch
x = torch.rand(5, 3)
print(x)
>>> tensor([[0.3890, 0.6087, 0.2300],
[0.1866, 0.4871, 0.9468],
[0.2254, 0.7217, 0.4173],
[0.1243, 0.1482, 0.6797],
[0.2430, 0.4608, 0.8886]])
如果脚本正确执行,则我们可以继续进行项目。否则,我建议读者参考这里的官方指南 pytorch.org/get-started/locally/
。
让我们继续安装其他依赖项:
-
Sklearn;
pip install scikit-learn
-
Pandas;
pip install pandas
-
Matplotlib;
pip install matplotlib
像 Numpy 这样的库在安装 PyTorch 时会自动安装。
导入和探索数据集
让我们从导入已安装的库和 Sklearn 的乳腺癌数据集开始,使用以下代码片段
import torch
import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
import matplotlib.pyplot as plt
breast_cancer_dataset = load_breast_cancer(as_frame=True, return_X_y=True)
让我们创建一个数据框来专门保存我们的 X 和 y,如下所示
df = breast_cancer_dataset[0]
df['target'] = breast_cancer_dataset[1]
df
数据框的示例。图片由作者提供。
我们的目标是创建一个可以根据其他列的特征预测目标列的模型。
让我们进行最低限度的探索性分析,以了解数据集。我们将使用 sweetviz 库自动创建分析报告。
我们可以使用命令pip install sweetviz
安装 sweetviz,并使用这段代码创建一个 EDA(探索性数据分析)报告
import sweetviz as sv
eda_report = sv.analyze(df)
eda_report.show_notebook()
Sweetviz 正在分析我们的数据集。图片由作者提供。
Sweetviz 将直接在我们的笔记本中创建报告供我们探索。
Sweetviz 中的“关联”标签。图片由作者提供。
我们看到多个列与目标列的值 0 或 1 高度相关。
由于这是一个多维数据集,且具有不同分布的变量,神经网络是建模该数据的一个有效选项。也就是说,该数据集也可以通过更简单的模型进行建模,如决策树。
现在,我们将导入另外两个库,以便可视化数据集。我们 将使用 Sklearn 和 Seaborn 中的 PCA(主成分分析)来可视化多维数据集。
PCA 将帮助我们将大量变量压缩成两个变量,我们将把这两个变量用作 Seaborn 散点图中的 X 轴和 Y 轴。Seaborn 使用一个额外的参数色调来根据附加变量为点上色。我们将使用我们的目标。
import seaborn as sns
from sklearn import decomposition
pca = decomposition.PCA(n_components=2)
X = df.drop("target", axis=1).values
y = df['target'].values
vecs = pca.fit_transform(X)
x0 = vecs[:, 0]
x1 = vecs[:, 1]
sns.set_style("whitegrid")
sns.scatterplot(x=x0, y=x1, hue=y)
plt.title("Proiezione PCA")
plt.xlabel("PCA 1")
plt.ylabel("PCA 2")
plt.xticks([])
plt.yticks([])
plt.show()
乳腺癌数据集的 PCA 投影。图像由作者提供。
我们看到类别 1 的数据点基于共同特征分组。我们的神经网络的目标是将行分类为目标 0 或 1。
创建数据集和数据加载器类
PyTorch 提供了Dataset
和DataLoader
对象,允许我们高效地组织和加载数据到神经网络中。
可以直接使用 pandas,但这样做会有缺点,因为它会使我们的代码效率降低。
Dataset
类允许我们指定数据的正确格式,并应用常常是基础的检索和转换逻辑(例如图像的数据增强)。
让我们看看如何创建一个 PyTorch Dataset
对象。
from torch.utils.data import Dataset
class BreastCancerDataset(Dataset):
def __init__(self, X, y):
# create feature tensors
self.features = torch.tensor(X, dtype=torch.float32)
# create label tensors
self.labels = torch.tensor(y, dtype=torch.long)
def __len__(self):
# we define a method to retrieve the length of the dataset
return self.features.shape[0]
def __getitem__(self, idx):
# necessary override of the __getitem__ method which helps to index our data
x = self.features[idx]
y = self.labels[idx]
return x, y
这是一个继承自Dataset
的类,并允许我们将要创建的DataLoader
高效地检索数据批次。
该类以 X 和 y 作为输入。
训练、验证和测试数据集
在继续以下步骤之前,创建训练、验证和测试集是很重要的。
这些将帮助我们评估模型的性能,并理解预测的质量。
对于感兴趣的读者,我建议阅读文章训练模型前你应该做的 6 件事和机器学习中的交叉验证是什么,以更好地理解为什么将数据拆分为三部分是一种有效的性能评估方法。
使用 Sklearn,这变得很简单,使用train_test_split
方法即可。
from sklearn import model_selection
train_ratio = 0.50
validation_ratio = 0.20
test_ratio = 0.20
x_train, x_test, y_train, y_test = model_selection.train_test_split(X, y, test_size=1 - train_ratio)
x_val, x_test, y_val, y_test = model_selection.train_test_split(x_test, y_test, test_size=test_ratio/(test_ratio + validation_ratio))
print(x_train.shape, x_val.shape, x_test.shape)
>>> (284, 30) (142, 30) (143, 30)
通过这段小代码,我们根据可控的分割创建了我们的训练、验证和测试集。
数据规范化
在进行深度学习时,即使是像二分类这样简单的任务,也总是需要规范化我们的数据。
规范化意味着将数据集中各个列的所有值统一到相同的数值尺度。这有助于神经网络更有效地收敛,从而更快地做出准确的预测。
我们将使用 Sklearn 的StandardScaler
。
from sklearn import preprocessing
scaler = preprocessing.StandardScaler()
x_train_scaled = scaler.fit_transform(x_train)
x_val_scaled = scaler.transform(x_val)
x_test_scaled = scaler.transform(x_test)
注意fit_transform
仅应用于训练集,而transform
应用于其他两个数据集。这是为了避免数据泄露——即验证集或测试集中的信息无意中泄漏到训练集中。我们希望训练集是唯一的学习来源,不受测试数据影响。
这些数据现在已经准备好输入到BreastCancerDataset
类中。
train_dataset = BreastCancerDataset(x_train_scaled, y_train)
val_dataset = BreastCancerDataset(x_val_scaled, y_val)
test_dataset = BreastCancerDataset(x_test_scaled, y_test)
我们导入 dataloader 并初始化对象。
from torch.utils.data import DataLoader
train_loader = DataLoader(
dataset=train_dataset,
batch_size=16,
shuffle=True,
drop_last=True
)
val_loader = DataLoader(
dataset=val_dataset,
batch_size=16,
shuffle=False,
drop_last=True
)
test_loader = DataLoader(
dataset=test_dataset,
batch_size=16,
shuffle=False,
drop_last=True
)
DataLoader
的强大之处在于它允许我们指定是否对数据进行洗牌以及数据应该以多少批次提供给模型。批次大小应被视为模型的超参数,因此可能会影响推断结果。
神经网络在 PyTorch 中的实现
在 PyTorch 中创建模型可能听起来很复杂,但实际上只需要理解一些基本概念。
-
在 PyTorch 中编写模型时,我们将使用面向对象的方法,就像处理数据集一样。这意味着我们将创建一个像
MyModel
这样的类,它继承自 PyTorch 的nn.Module
类。 -
PyTorch 是一个自动微分软件。这意味着当我们基于反向传播算法编写神经网络时,计算导数来计算损失的过程是自动进行的。这需要编写一些专门的代码,第一次遇到时可能会感到困惑。
我建议想了解神经网络工作原理基础的读者查阅文章 神经网络介绍——权重、偏差和激活。
神经网络如何通过权重、偏差和激活函数学习
话虽如此,让我们看看编写逻辑回归模型的代码是怎样的。
class LogisticRegression(nn.Module):
"""
Our neural network accepts num_features and num_classes.
num_features - number of features to learn from
num_classes: number of classes in output to expect (in this case, 1 or 2, since the output is binary (0 or 1))
"""
def __init__(self, num_features, num_classes):
super().__init__() # initialize the init method of nn.Module
self.num_features = num_features
self.num_classes = num_classes
# create a single layer of neurons on which to apply the log reg
self.linear1 = nn.Linear(in_features=num_features, out_features=num_classes)
def forward(self, x):
logits = self.linear1(x) # pass our data through the layer
probs = torch.sigmoid(logits) # we apply a sigmoid function to obtain the probabilities of belonging to a class (0 or 1)
return probs # return probabilities
我们的类继承自 nn.Module
。这个类提供了让模型正常工作的后台方法。
init 方法
类的 __init__
方法包含了在 Python 中实例化类时运行的逻辑。在这里我们传递两个参数:特征的数量和要预测的类别数量。
num_features
对应于组成数据集的列数减去目标变量,而 num_classes
对应于神经网络必须返回的结果数量。
除了这两个参数及其类变量,我们还看到 super().__init__()
这一行。super 函数初始化了父类的 init 方法。这使得我们能够在模型中拥有 nn.Module
的功能。
在 init 块中,我们实现了一个线性层,称为 self.linear1
,它的参数是特征的数量和返回结果的数量。
forward() 方法
通过编写 forward
方法,我们告诉 Python 重写 PyTorch 的 nn.Module
父类中的相同方法。实际上,这个方法在进行前向传播时被调用——也就是当我们的数据从一层流向另一层时。
forward
接受输入 x,其中包含模型将根据其性能进行校准的特征。
输入通过第一层,创建了logits
变量。logits 是神经网络的计算结果,还未通过最终激活函数(在此情况下为 sigmoid)转换为概率。实际上,它们是神经网络在映射到可以解释的函数之前的内部表示。
在这种情况下,sigmoid 函数会将 logits 映射到 0 和 1 之间的概率。如果输出小于 0,则类别为 0,否则为 1。这发生在self.probs = torch.sigmoid(x)
这一行。
用于绘图和准确率计算的实用函数
让我们创建实用函数以在即将看到的训练循环中使用。这两个函数用于计算每个 epoch 结束时的准确率,并在训练结束时显示性能曲线。
def compute_accuracy(model, dataloader):
"""
This function puts the model in evaluation mode (model.eval()) and calculates the accuracy with respect to the input dataloader
"""
model = model.eval()
correct = 0
total_examples = 0
for idx, (features, labels) in enumerate(dataloader):
with torch.no_grad():
logits = model(features)
predictions = torch.where(logits > 0.5, 1, 0)
lab = labels.view(predictions.shape)
comparison = lab == predictions
correct += torch.sum(comparison)
total_examples += len(comparison)
return correct / total_examples
def plot_results(train_loss, val_loss, train_acc, val_acc):
"""
This function takes lists of values and creates side-by-side graphs to show training and validation performance
"""
fig, ax = plt.subplots(1, 2, figsize=(15, 5))
ax[0].plot(
train_loss, label="train", color="red", linestyle="--", linewidth=2, alpha=0.5
)
ax[0].plot(
val_loss, label="val", color="blue", linestyle="--", linewidth=2, alpha=0.5
)
ax[0].set_xlabel("Epoch")
ax[0].set_ylabel("Loss")
ax[0].legend()
ax[1].plot(
train_acc, label="train", color="red", linestyle="--", linewidth=2, alpha=0.5
)
ax[1].plot(
val_acc, label="val", color="blue", linestyle="--", linewidth=2, alpha=0.5
)
ax[1].set_xlabel("Epoch")
ax[1].set_ylabel("Accuracy")
ax[1].legend()
plt.show()
模型训练
现在我们进入大多数深度学习新手 struggle 的部分:PyTorch 训练循环。
让我们查看代码,然后进行注释。
import torch.nn.functional as F
model = LogisticRegression(num_features=x_train_scaled.shape[1], num_classes=1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
num_epochs = 10
train_losses, val_losses = [], []
train_accs, val_accs = [], []
for epoch in range(num_epochs):
model = model.train()
t_loss_list, v_loss_list = [], []
for batch_idx, (features, labels) in enumerate(train_loader):
train_probs = model(features)
train_loss = F.binary_cross_entropy(train_probs, labels.view(train_probs.shape))
optimizer.zero_grad()
train_loss.backward()
optimizer.step()
if batch_idx % 10 == 0:
print(
f"Epoch {epoch+1:02d}/{num_epochs:02d}"
f" | Batch {batch_idx:02d}/{len(train_loader):02d}"
f" | Train Loss {train_loss:.3f}"
)
t_loss_list.append(train_loss.item())
model = model.eval()
for batch_idx, (features, labels) in enumerate(val_loader):
with torch.no_grad():
val_probs = model(features)
val_loss = F.binary_cross_entropy(val_probs, labels.view(val_probs.shape))
v_loss_list.append(val_loss.item())
train_losses.append(np.mean(t_loss_list))
val_losses.append(np.mean(v_loss_list))
train_acc = compute_accuracy(model, train_loader)
val_acc = compute_accuracy(model, val_loader)
train_accs.append(train_acc)
val_accs.append(val_acc)
print(
f"Train accuracy: {train_acc:.2f}"
f" | Val accuracy: {val_acc:.2f}"
)
与 TensorFlow 不同,PyTorch 要求我们用纯 Python 编写训练循环。
让我们逐步查看过程:
-
我们实例化模型和优化器
-
我们决定一个 epoch 数量。
-
我们创建一个 for 循环来遍历 epochs。
-
对于每个 epoch,我们使用
model.train()
将模型设置为训练模式,并循环遍历train_loader
。 -
对于每个批次的
train_loader
,计算损失,使用optimizer.zero_grad()
将梯度计算归零,并使用optimizer.step()
更新网络的权重。
此时训练循环已经完成,如果需要,可以将相同的逻辑集成到验证数据加载器中,如代码所示。
这是运行此代码后的训练结果。
训练中。图片由作者提供。
神经网络性能评估
我们使用之前创建的实用函数来绘制训练和验证中的损失。
plot_results(train_losses, val_losses, train_accs, val_accs)
神经网络的性能。图片由作者提供。
我们的二分类模型很快收敛到高准确率,我们可以看到每个 epoch 结束时损失如何下降。
数据集模型简单,样本数量少并没有帮助网络更逐步地收敛到高性能。
我强调,可以将 TensorBoard 软件集成到 PyTorch 中,以便在各种实验之间自动记录性能指标。
创建预测
我们已经到达了本指南的末尾。让我们查看创建整个数据集预测的代码。
# we transform all our features with the scaler
X_scaled_all = scaler.transform(X)
# transform in tensors
X_scaled_all_tensors = torch.tensor(X_scaled_all, dtype=torch.float32)
# we set the model in inference mode and create the predictions
with torch.inference_mode():
logits = model(X_scaled_all_tensors)
predictions = torch.where(logits > 0.5, 1, 0)
df['predictions'] = predictions.numpy().flatten()
现在让我们导入 Sklearn 的metrics
包,它允许我们直接在 pandas 数据框上快速计算混淆矩阵和分类报告。
from sklearn import metrics
from pprint import pprint
pprint(metrics.classification_report(y_pred=df.predictions, y_true=df.target))
对整个数据集的性能总结,附有分类报告。图片由作者提供。
以及混淆矩阵,它显示了对角线上的正确答案数量。
metrics.confusion_matrix(y_pred=df.predictions, y_true=df.target)
>>> array([[197, 15],
[ 13, 344]])
这里有一个小函数,用于创建一个分类线,将 PCA 图中的类别分开。
def plot_boundary(model):
w1 = model.linear1.weight[0][0].detach()
w2 = model.linear1.weight[0][1].detach()
b = model.linear1.bias[0].detach()
x1_min = -1000
x2_min = (-(w1 * x1_min) - b) / w2
x1_max = 1000
x2_max = (-(w1 * x1_max) - b) / w2
return x1_min, x1_max, x2_min, x2_max
sns.scatterplot(x=x0, y=x1, hue=y)
plt.title("PCA Projection")
plt.xlabel("PCA 1")
plt.ylabel("PCA 2")
plt.xticks([])
plt.yticks([])
plt.plot([x1_min, x1_max], [x2_min, x2_max], color="k", label="Classification", linestyle="--")
plt.legend()
plt.show()
这是模型如何将良性细胞与恶性细胞区分开来
分类边界可视化。图片由作者提供。
结论
在本文中,我们已经看到如何使用 PyTorch 从 Pandas 数据框创建一个二分类模型。
我们已经了解了训练循环的样子,如何评估模型,以及如何创建预测和可视化以帮助解释。
使用 PyTorch 可以创建非常复杂的神经网络……只需想到特斯拉,这家基于 AI 的电动车制造商,就使用 PyTorch 创建其模型。
对于那些想要开始深度学习之旅的人来说,尽早学习 PyTorch 成为一个高优先级任务,因为它允许你构建重要的技术,解决复杂的数据驱动问题。
如果你想支持我的内容创作活动,可以通过下面的推荐链接加入 Medium 会员计划。我将获得你投资的一部分,你将能够无缝访问 Medium 上大量的数据科学及更多领域的文章。
[## 使用我的推荐链接加入 Medium - Andrea D'Agostino]
阅读 Andrea D'Agostino 的每个故事(以及 Medium 上的其他成千上万的作家)。你的会员费直接……
medium.com](https://medium.com/@theDrewDag/membership?source=post_page-----a70372764432--------------------------------)
推荐阅读
对于感兴趣的人,这里有一系列我推荐的与 ML 相关的书籍。这些书籍在我看来是必读的,并且对我的职业生涯产生了重大影响。
免责声明:这些是亚马逊附属链接。我将因推荐这些商品而获得亚马逊的小额佣金。你的体验不会改变,你不会被额外收费,但这将帮助我扩展业务,并制作更多关于 AI 的内容。
-
机器学习入门: 自信的数据技能:掌握数据工作的基础,提升你的职业生涯 作者:Kirill Eremenko
-
Sklearn / TensorFlow: 动手学机器学习:使用 Scikit-Learn、Keras 和 TensorFlow 作者:Aurelien Géron
-
NLP: 文本作为数据:机器学习和社会科学的新框架 作者:Justin Grimmer
-
Sklearn / PyTorch: 用 PyTorch 和 Scikit-Learn 学习机器学习:使用 Python 开发机器学习和深度学习模型 作者:Sebastian Raschka
-
数据可视化: 数据讲故事:商业专业人士的数据可视化指南 作者:科尔·克纳夫利克
有用的链接(由我撰写)
-
学习如何在 Python 中执行顶级探索性数据分析: Python 中的探索性数据分析——逐步过程
-
学习 TensorFlow 的基础知识: 入门 TensorFlow 2.0——深度学习介绍
-
使用 TF-IDF 在 Python 中进行文本聚类: Python 中的 TF-IDF 文本聚类