docs-merge-12

TowardsDataScience 2024 中文翻译(十三)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

让我们通过数独了解一下计算机视觉

原文:towardsdatascience.com/lets-learn-a-little-about-computer-vision-via-sudoku-836065c0f07b?source=collection_archive---------2-----------------------#2024-12-14

解数独是编程中的一个有趣挑战,将计算机视觉技术添加到数独解题中,可以将其与流行的机器学习技术相结合

Brian RoepkeTowards Data Science Brian Roepke

·发表于Towards Data Science ·阅读时间 8 分钟·2024 年 12 月 14 日

--

图片由作者使用 HubSpot AI 制作

介绍

这一切最初只是一个有趣的小实验,目的是编写另一个解谜工具,类似于我最近写的Wordle解答器。数独是一个非常适合计算机求解的问题。它是通过简单的迭代方式来寻找独特的解法。可能有成千上万的例子,因此虽然我会提到我最终如何解决这个谜题,但我更想关注我在这款游戏中采用的机器学习(ML)和人工智能(AI)方法。我想,为什么不将计算机视觉(CV)和光学字符识别(OCR)加入其中,让你能够上传谜题的图像,机器将读取并从那里开始解决剩下的部分。结果这是一次非常棒的学习体验,我很乐意和大家分享!

到现在为止,我们大概都熟悉数独是什么了,那么让我们深入了解如何从图像中提取数字吧!

让我们用 JAX 重建 NanoGPT!(第一部分)

原文:towardsdatascience.com/lets-reproduce-nanogpt-with-jax-part-1-95bec4630eb4?source=collection_archive---------2-----------------------#2024-07-21

第一部分:使用 JAX 构建 124M GPT2。

第二部分:在单 GPU 中优化训练速度。

第三部分:在 JAX 中进行多 GPU 训练。

Louis WangTowards Data Science Louis Wang

·发表于 Towards Data Science ·阅读时间:8 分钟·2024 年 7 月 21 日

--

受到 Andrej Karpathy 最近的 YouTube 视频让我们重建 GPT-2(124M)的启发,我想用 JAX 重建它,并进行大多数训练优化。JAX 专为高效计算速度而构建,非常有趣的是,可以将 Pytorch 与其最近的训练优化以及 JAX 与其相关库(如 Flax:JAX 的神经网络训练层 API 和 Optax:JAX 的梯度处理和优化库)进行对比。我们将迅速了解 JAX,并用 JAX 重建 GPT。最后,我们将比较 Pytorch 和 JAX 在多 GPU 训练中的 token/sec。

AI 生成的 GPT

什么是 Jax?

根据其readthedoc,JAX 是一个面向加速器的数组计算和程序转换的 Python 库,旨在实现高性能的数值计算和大规模机器学习。我想用它的名字来介绍 JAX。虽然有人称它为 Just Another XLA(加速线性代数),我更愿意称其为 J(it) A(utograd) X(LA),以展示它的高效能力。

J — Just-in-time (JIT) 编译。当你运行 Python 函数时,Jax 将其转换为一组基本操作,称为 Jaxpr。然后,Jaxpr 表达式会被转换为 XLA 的输入,XLA 将其编译成底层脚本,从而为目标设备(CPU、GPU 或 TPU)生成优化后的可执行文件。

A — Autograd。计算梯度是现代机器学习方法中的一个关键部分,你只需要调用jax.grad()来获取梯度,从而优化模型。

X — XLA。这是一个开源的机器学习编译器,支持 CPU、GPU 和 ML 加速器。通常,XLA 会对StableHLO图进行几个内建的优化和分析传递,然后将 HLO 计算发送到后端进行进一步的 HLO 级别优化。后端再进行特定目标的代码生成。

这些只是 JAX 的一些关键特性,但它还有许多类似于 numpy 的用户友好 API,如jax.numpy,以及通过jax.vmap进行的自动向量化,和通过jax.pmap将代码并行化到多个设备上。我们将在以后的博客中介绍更多 Jax 的概念和应用,但现在让我们用 Jax 复现 NanoGPT!

从注意力机制到变换器(Transformer)

GPT 是一种仅解码的变换器模型,关键构建模块是注意力模块。我们可以首先定义一个模型配置数据类来保存模型的超参数,这样模型模块就能高效地使用它来初始化模型架构。类似于 124M GPT 模型,在这里我们初始化一个 12 层的变换器解码器,具有 12 个头和 50257 个词汇表大小,每个词汇表项有 768 维嵌入向量。注意力计算的块大小为 1024。

from dataclasses import dataclass

@dataclass
class ModelConfig:
  vocab_size: int = 50257
  n_head: int = 12
  n_embd: int = 768
  block_size: int = 1024
  n_layer: int = 12
  dropout_rate: float = 0.1

接下来是变换器模型的关键构建模块——注意力机制(Attention)。其思想是将输入处理成三个权重矩阵:Key、Query 和 Value。在这里,我们依赖于flax,这是一个 Jax 层和训练 API 库,用来初始化这三个权重矩阵,只需要调用[flax.linen.Dense](https://flax.readthedocs.io/en/v0.5.3/_autosummary/flax.linen.Dense.html)。如前所述,Jax 有许多类似 numpy 的 API,因此我们使用[jax.numpy.reshape](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.reshape.html)将权重矩阵后的输出从[batch_size, sequence_length, embedding_dim]重塑为[batch_size, sequence_length, num_head, embedding_dim / num_head]。由于我们需要对 Key 和 Value 矩阵执行矩阵乘法,jax 还提供了[jax.numpy.matmul](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.matmul.html)[jax.numpy.transpose](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.transpose.html) API(用于转置 Key 矩阵以进行乘法运算)。

多头注意力(Multihead Attention)

请注意,我们需要在注意力矩阵上加上一个掩码,以避免信息泄漏(防止之前的 tokens 访问到后面的 tokens),[jax.numpy.tril](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.tril.html) 帮助构建一个下三角数组,而 [jax.numpy.where](https://jax.readthedocs.io/en/latest/_autosummary/jax.numpy.where.html) 可以为我们填充无限大的数值,以便在 softmax [jax.nn.softmax](https://jax.readthedocs.io/en/latest/_autosummary/jax.nn.softmax.html) 后得到 0。多头注意力的完整代码如下所示。

from flax import linen as nn
import jax.numpy as jnp

class CausalSelfAttention(nn.Module):

  config: ModelConfig

  @nn.compact
  def __call__(self, x, deterministic=True):

    assert len(x.shape) == 3

    b, l, d = x.shape

    q     = nn.Dense(self.config.n_embd)(x)
    k     = nn.Dense(self.config.n_embd)(x)
    v     = nn.Dense(self.config.n_embd)(x)
    # q*k / sqrt(dim) -> softmax -> @v
    q     = jnp.reshape(q, (b, l, d//self.config.n_head , self.config.n_head))
    k     = jnp.reshape(k, (b, l, d//self.config.n_head , self.config.n_head))
    v     = jnp.reshape(v, (b, l, d//self.config.n_head , self.config.n_head))
    norm  = jnp.sqrt(list(jnp.shape(k))[-1])
    attn  = jnp.matmul(q,jnp.transpose(k, (0,1,3,2))) / norm
    mask  = jnp.tril(attn)
    attn  = jnp.where(mask[:,:,:l,:l], attn, float("-inf"))
    probs = jax.nn.softmax(attn, axis=-1)
    y     = jnp.matmul(probs, v)
    y     = jnp.reshape(y, (b,l,d))
    y     = nn.Dense(self.config.n_embd)(y)
    return y

你可能会注意到,在 Pytorch 中常见的 __init__forward 方法在这里并不存在。这是 jax 的特点,在 jax 中你可以显式地通过 setup 方法定义层,或者通过在 __call__ 方法上添加 nn.compact 来隐式定义它们。[参考]

接下来让我们构建 MLP 和 Block 层,包括 Dense 层、Gelu 激活函数、LayerNorm 和 Dropout。再次,flax.linen 提供了层的 API,帮助我们构建模块。请注意,我们会传递一个 deterministic 布尔变量来控制某些层(如 Dropout)在训练或评估期间的不同行为。

class MLP(nn.Module):

  config: ModelConfig

  @nn.compact
  def __call__(self, x, deterministic=True):
    x = nn.Dense(self.config.n_embd*4)(x)
    x = nn.gelu(x, approximate=True)
    x = nn.Dropout(rate=self.config.dropout_rate)(x, deterministic=deterministic)
    x = nn.Dense(self.config.n_embd)(x)
    x = nn.Dropout(rate=self.config.dropout_rate)(x, deterministic=deterministic)
    return x

class Block(nn.Module):

  config: ModelConfig

  @nn.compact
  def __call__(self, x):
    x = nn.LayerNorm()(x)
    x = x + CausalSelfAttention(self.config)(x)
    x = nn.LayerNorm()(x)
    x = x + MLP(self.config)(x)
    return x

现在让我们使用上述模块来构建 NanoGPT:

给定一个序列的 token ids 输入,我们使用 [flax.linen.Embed](https://flax.readthedocs.io/en/v0.5.3/_autosummary/flax.linen.Embed.html) 层来获取位置嵌入和 token 嵌入。然后,我们将它们传入 Block 模块 N 次,其中 N 是模型配置中定义的层数。最后,我们将来自最后一个 Block 的输出映射到每个词汇表 token 的概率,以预测下一个 token。除了前向 __call__ 方法之外,我们还需要创建一个 init 方法来获取虚拟输入并获得模型的参数。

class GPT(nn.Module):

  config: ModelConfig

  @nn.compact
  def __call__(self, x, deterministic=False):

    B, T = x.shape
    assert T <= self.config.block_size

    pos     = jnp.arange(0, T)[None]
    pos_emb = nn.Embed(self.config.block_size, self.config.n_embd)(pos)
    wte     = nn.Embed(self.config.vocab_size, self.config.n_embd)
    tok_emb = wte(x)
    x       = tok_emb + pos_emb

    for _ in range(self.config.n_layer):
      x = Block(self.config)(x)
    x = nn.LayerNorm()(x)
    logits = nn.Dense(config.n_embd, config.vocab_size)(x)
    # logits = wte.attend(x) # parameter sharing
    return logits

  def init(self, rng):
    tokens = jnp.zeros((1, self.config.block_size), dtype=jnp.uint16)
    params = jax.jit(super().init, static_argnums=(2,))(rng, tokens, True)
    return params 

现在让我们验证一下参数的数量:我们首先初始化模型配置的数据类和随机密钥,然后创建一个虚拟输入并将其输入到 GPT 模型中。接着,我们利用 jax.util.treemap API 创建一个计数参数函数。我们得到了 124439808(124M)个参数,与 Huggingface 的 GPT2 相同,哇!

Colab 结果:参数数量

验证 Huggingface 的 GPT2 参数数量

数据加载器和训练循环

现在让我们在一个小数据集上进行过拟合。为了与 Andrej 的 Pytorch NanoGPT 视频中进行对比,我们使用他在视频中分享的玩具 dataset。我们使用 tiktoken 库的 GPT2 分词器对输入文件中的所有文本进行分词,并将这些 token 转换为 jax.numpy.array 以便 Jax 的模型训练。

class DataLoader:
  def __init__(self, B, T):
    self.current_position = 0
    self.B = B
    self.T = T

    with open("input.txt","r") as f:
      text = f.read()
    enc = tiktoken.get_encoding("gpt2")
    self.tokens = jnp.array(enc.encode(text))
    print(f"loaded {len(self.tokens)} tokens in the datasets" )
    print(f" 1 epoch = {len(self.tokens)//(B*T)} batches")

  def next_batch(self):
    B,T = self.B, self.T
    buf = self.tokens[self.current_position:self.current_position+B*T+1]
    x,y = jnp.reshape(buf[:-1],(B,T)), jnp.reshape(buf[1:],(B,T))
    self.current_position += B*T
    if self.current_position + B*T+1 > len(self.tokens):
      self.current_position = 0
    return x,y

Colab 结果:简单的数据加载器,批量大小为 4,序列长度为 128

接下来,让我们暂时忽略分布式训练和优化,先创建一个简单的训练循环进行基本检查。初始化模型后的第一件事是创建一个TrainState,这是一个可以更新参数和梯度的模型状态。TrainState 接受三个重要输入:apply_fn(模型前向函数)、params(来自初始化方法的模型参数)和 tx(一个 Optax 梯度变换)。

然后我们使用 train_step 函数来更新模型状态(梯度和参数),以继续模型训练。Optax 提供了用于下一个令牌预测任务的 softmax 交叉熵作为损失函数,jax.value_and_grad 用于计算损失函数的梯度和损失值。最后,我们使用 apply_gradients API 更新模型的状态和新参数。[ref] 别忘了对 train_step 函数进行 JIT 编译,以减少计算开销!

def init_train_state(key, config) -> TrainState:
  model = GPT(config)
  params = model.init(key)
  optimizer = optax.adamw(3e-4, b1=0.9, b2=0.98, eps=1e-9, weight_decay=1e-1)
  train_state = TrainState.create(
        apply_fn=model.apply,
        params=params,
        tx=optimizer)
  return train_state

@jax.jit
def train_step(state: TrainState, x: jnp.ndarray, y: jnp.ndarray) -> Tuple[jnp.ndarray, TrainState]:

  def loss_fn(params: FrozenDict) -> jnp.ndarray:

      logits = state.apply_fn(params, x, False)
      loss = optax.softmax_cross_entropy_with_integer_labels(logits, y).mean()
      return loss

  loss, grads = jax.value_and_grad(loss_fn, has_aux=False)(state.params)
  new_state = state.apply_gradients(grads=grads)
  return loss, new_state

现在一切准备就绪,可以开始进行简单的训练循环了……让我们检查损失值。模型的预测应该优于随机猜测,因此损失值应该低于 -ln(1/50257)≈10.825。我们对单批次过拟合的预期是:一开始损失接近 10.825,然后下降到接近 0。让我们取一批(x,y)并运行训练循环 50 次。我还添加了类似的日志来计算训练速度。

如我们所见,损失值正是我们预期的,训练吞吐量大约是 400–500 k token/sec。这已经比 Andrej 视频中没有任何优化的 Pytorch 初始版本快了 40 倍。请注意,我们是在 1 个 A100 GPU 上运行 Jax 脚本,这应该消除了硬件差异对速度比较的影响。这里没有 .to(device) 的操作来将模型或数据从主机 CPU 移动到设备 GPU,这正是 Jax 的一个优势!

就这样,我们做到了。我们将在第二部分通过更多优化将训练速度提升至原来的 10 倍…

第二部分:训练优化之旅,如何在单个 GPU 上达到 1350k tokens/sec!

“除非另有说明,所有图片均为作者所提供”

让我们重新审视不同库中的 case-when,包括新玩家:Pandas

原文:towardsdatascience.com/lets-revisit-case-when-in-different-libraries-including-the-new-player-pandas-8c4febb979ba?source=collection_archive---------17-----------------------#2024-06-18

如何使用不同工具创建条件列。

Soner YıldırımTowards Data Science Soner Yıldırım

·发布于 Towards Data Science ·6 分钟阅读·2024 年 6 月 18 日

--

图片由 JESHOOTS.COM 提供,来源于 Unsplash

无论你是在做数据分析、数据清洗,还是特征工程,基于其他列的值创建新列是一个常见的操作。

我使用过的所有数据清洗和处理工具都有执行此任务的函数(例如 SQL、R 数据表、PySpark)。现在我们有了游戏中的新玩家:Pandas。

顺便提一下,虽然之前可以使用 Pandas 创建条件列,但它并没有专门的 case-when 函数。

在 Pandas 2.2.0 中,引入了 case_when 函数,用于根据一个或多个条件创建 Series 对象。

让我们重新审视如何使用常用的数据分析和处理工具完成这个非常有用的操作。

为了保持一致性并更容易看出工具之间的差异,我们将使用一个小型数据集。

SQL

以下是一个名为“mytable”的小型 SQL 表。

+-------------+----------+---------+
|           a |        b |       c |
+-------------+----------+---------+
|           0 |        5 |       1 |
|           1 |       -1 |…

让我们在 Python 中编写一个可组合的、易于使用的缓存包

原文:towardsdatascience.com/lets-write-a-composable-easy-to-use-caching-package-in-python-171801935540?source=collection_archive---------11-----------------------#2024-08-29

简单、用户友好的缓存,满足您所有的需求

Mike HulsTowards Data Science Mike Huls

·发表于Towards Data Science ·9 分钟阅读·2024 年 8 月 29 日

--

Python 选择缓存策略(图片由 ChatGPT 提供 + 作者进行的业余编辑)

在本文中,我们将逐步讲解从零开始构建 Python 缓存系统的过程。目标是……

  • 更好地理解缓存并探索各种缓存策略

  • 了解为什么我们选择了组合方式而非继承方式

  • 学习如何以及何时有效地应用缓存

我们将重点构建一个用户友好的包,使您能够轻松地将缓存添加到您的代码中。此外,我们还将提供一种简单的方式来扩展缓存,通过为其提供自定义行为。让我们开始编码吧!

在我们开始之前……

本文详细介绍了如何创建缓存,这些缓存是PyPI 上的 Cachr 包的一部分。所有代码都可以在这个Github 仓库中找到。欢迎任何贡献;随时提交错误报告、错误修复、功能请求、文档改进或增强功能!

使用 Python 线程提升你的编码技能

原文:towardsdatascience.com/level-up-your-coding-skills-with-python-threading-8f1bd06b9476?source=collection_archive---------3-----------------------#2024-11-27

图片由Sonika Agarwal拍摄,来源于Unsplash

学习如何在机器学习项目中使用队列、守护线程和事件

Marcello PolitiTowards Data Science Marcello Politi

·发表于Towards Data Science ·阅读时间:7 分钟·2024 年 11 月 27 日

--

引言

在大多数机器学习工作中,你并不会研究如何改进某个模型架构或设计一个新的损失函数。大多数时候,你必须利用已有的技术并将其适应你的使用场景。因此,优化你的项目在架构设计和实现方面非常重要。一切从这里开始:你需要的是最优的代码,简洁、可重用并且尽可能快地运行。线程是 Python 内置的原生库,但人们并不像应该的那样频繁使用它。

关于线程

线程是让一个程序将自己拆分为两个或更多同时(或伪同时)运行的任务的一种方式……通常,线程存在于一个进程中,同一进程中的不同线程共享相同的资源。

在这篇文章中,我们不会讨论多进程,但 Python 的多进程库与多线程库非常相似。通常:

  • 多线程非常适合 I/O 密集型任务,例如在 for 循环中调用 API

  • 多进程用于 CPU 密集型任务,例如……

通过这 10 个有用的技巧,提升你的 Git 知识,浏览 Git 历史记录

原文:towardsdatascience.com/level-up-your-git-knowledge-with-these-10-useful-tips-to-browse-git-history-5f7b9f4b0e1d?source=collection_archive---------8-----------------------#2024-03-09

图片来源:Thomas KelleyUnsplash

精通 Git

学习高级的 git log

Zolzaya LuvsandorjTowards Data Science Zolzaya Luvsandorj

·发表于 Towards Data Science ·阅读时长 10 分钟·2024 年 3 月 9 日

--

如果你已经在使用 Git,那么你可能熟悉 git log 命令。除了它的基本用法(即普通的 git log),这个命令的高级用法非常强大,可以使得浏览仓库历史记录变得更加顺畅和富有信息。在这篇文章中,我们将学习一些使用 git log 的实用方法,帮助你将 Git 知识提升到一个新水平。

图片来源:Chris LawtonUnsplash

🔧 0. 设置

本文假设读者(即你)已经熟悉 Git 的基本用法。如果你需要复习 Git 的基础知识,可以先阅读这篇文章。为了更好地理解本文,我建议你在阅读过程中动手实践这些命令。我们在积极练习新知识时,比被动阅读更能快速学习。

我们将使用我最喜欢的 GitHub 仓库之一:ABSphreak/readme-jokes: 😄 适用于你的 GitHub README 文件的笑话 来演示命令的使用。这个精彩的轻量级仓库让我可以在我的 GitHub 个人资料中添加随机的编程笑话。让我们开始吧……

提升你的 Pandas 技能,发掘这 15 个隐藏宝藏

原文:towardsdatascience.com/level-up-your-pandas-game-with-these-15-hidden-gems-1c6aded2060f?source=collection_archive---------1-----------------------#2024-01-25

图片由 Midjourney 生成

另一个探索 Pandas 更多有用功能的机会

Eryk LewinsonTowards Data Science Eryk Lewinson

·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 1 月 25 日

--

我真正喜欢 pandas 的地方在于,你可以用它工作多年,可能仍然有很多你未曾了解的有用方法。这就是为什么在这一系列的第四部分中,我将向你展示一些你可能从未听说过的但绝对对数据清洗有帮助的方法。在我们开始之前,你可能想要查看一下系列的前几部分:

  • 你可能没听说过的 9 个有用的 Pandas 方法

  • 你分析中可能需要的 8 个额外的有用 Pandas 功能

  • 你可能忽略的 11 个有用的 Pandas 功能

我们将按字母顺序介绍这些方法,而不是按它们的实用性排序。让我们直接开始吧!

设置

这次我们不需要任何复杂的库,因此我们只需导入基本的库:

利用 KeyBERT、HDBSCAN 和 Zephyr-7B-Beta 构建知识图谱

原文:towardsdatascience.com/leverage-keybert-hdbscan-and-zephyr-7b-beta-to-build-a-knowledge-graph-33d7534ee01b?source=collection_archive---------0-----------------------#2024-01-07

增强型大语言模型自然语言处理与传统机器学习技术结合,用于从非结构化语料库中提取结构并构建知识图谱。

Silvia OnofreiTowards Data Science Silvia Onofrei

·发表于 Towards Data Science ·19 分钟阅读·2024 年 1 月 7 日

--

设计者:Freepik

介绍

虽然大型语言模型(LLMs)是有用且高效的工具,但完全依赖它们的输出并不总是明智的,因为它们通常需要验证和基础支持。然而,将传统的自然语言处理方法与生成型人工智能的能力相结合,通常能取得令人满意的结果。一个很好的例子是,将 KeyBERT 与 KeyLLM 相结合以进行关键词提取。

在这篇博客中,我打算探讨将传统的自然语言处理和机器学习技术与大型语言模型的多功能性相结合的有效性。这一探索包括使用 KeyBERT 进行简单的关键词提取,利用 BERT 进行句子嵌入,以及使用 UMAP 进行降维,结合 HDBSCAN 进行聚类。所有这些技术与高性能的 Zephyr-7B-Beta 一起使用,最终将结果上传到知识图谱中,以便进行更深入的分析和发现。

我的目标是开发一个针对计算机科学领域非结构化 arXiv 文章标题的结构化方法。我根据摘要的长度选择了这些文章,而并非期待内在的主题聚类。事实上,初步的社区分析揭示了几乎与文章数量相等的聚类。因此,我正在探索另一种将这些标题联系起来的方法。尽管缺乏明显的社区,这些标题通常共享相同的词汇。通过提取并聚类这些关键词,我旨在揭示标题之间的潜在联系,为数据集结构化提供一种多功能的策略。

为了简化并增强数据探索,我将结果上传至 Neo4j 知识图谱。以下是输出的快照:

这两个紫色节点代表标题:“可数范畴结构的核心”(左)和“通过集合解释转化结构”(右)。它们通过“数学逻辑”(浅褐色节点)这一共同主题,通过关键词“结构”连接在一起。— 图像由作者提供 —

以下是项目的步骤:

— 图示由作者提供 —

  • 收集并解析数据集,重点关注标题,同时保留摘要以提供上下文。

  • 使用 KeyBERT 提取候选关键词,然后基于 Zephyr-7B-Beta,通过 KeyLLM 对其进行优化,生成增强后的关键词和关键短语列表。

  • 收集所有提取的关键词和关键短语,并使用 HDBSCAN 对它们进行聚类。

  • 再次使用 Zephyr-7B-Beta,为每个聚类推导标签和描述。

  • 将这些元素结合到知识图谱中,节点代表文章、关键词和(聚类)主题。

需要注意的是,过程中的每一步都提供了实验替代方法、算法或模型的灵活性。

工作在 Google Colab Pro 上进行,配备 V100 GPU 和高内存设置,用于涉及 LLM 的步骤。笔记本被划分为自包含的部分,其中大多数部分可以独立执行,最小化对先前步骤的依赖。每个部分执行完毕后都会保存数据,以便在需要时能够在新会话中继续。此外,解析后的数据集和 Python 模块可以在这个 Github 仓库 中找到。

数据准备

我使用的是一个来自 arXiv 数据集 的子集,该数据集公开可用并主要由康奈尔大学维护。以机器可读格式提供,包含 170 万篇跨学科的学术论文,涵盖 STEM 领域,并提供文章标题、作者、类别、摘要、完整文本 PDF 等相关特征。该数据集定期更新。

数据集已清理完毕,并且格式简洁易用,因此我们可以集中精力处理任务,而无需花费过多时间进行数据预处理。为了进一步简化数据准备过程,我构建了一个 Python 模块,执行相关步骤。如果你想查看代码,可以在[utils/arxiv_parser.py](https://github.com/SolanaO/Blogs_Content/blob/master/keyllm_neo4j/utils/arxiv_parser.py)找到,或者继续使用 Google Colab:

  • 下载压缩的 arXiv 文件(1.2 GB),并选择一个目录进行存储,该目录标记为data_path

  • 下载arxiv_parser.pyutils目录,

  • 在你的 Google Colab 笔记本中导入并初始化模块,

  • 解压文件,这将提取出一个 3.7 GB 的文件:archive-metadata-oai-snapshot.json

  • 指定一个一般主题(我使用cs,即计算机科学),这样你将拥有一个更易于管理的数据集,

  • 选择要保留的特征(下载的数据集中有 14 个特征),

  • 摘要的长度可能差异较大,因此我添加了一个选项,可以选择摘要中的 token 数量在给定区间内的条目,并利用此功能来缩小数据集的规模,

  • 尽管我选择使用title特征,但也有一个选项可以采用更常见的方法,即将标题和摘要合并为一个单一特征,称为corpus

# Import the data parser module
from utils.arxiv_parser import *

# Initialize the data parser
parser = ArXivDataProcessor(data_path)

# Unzip the downloaded file to extract a json file in data_path
parser.unzip_file()

# Select a topic and extract the articles on that topic
topic='cs'
entries = parser.select_topic('cs')

# Build a pandas dataframe with specified selections
df = parser.select_articles(entries, # extracted articles
                            cols=['id', 'title', 'abstract'], # features to keep
                            min_length = 100, # min tokens an abstract should have
                            max_length = 120, # max tokens an abstract should have
                            keep_abs_length = False, # do not keep the abs_length column
                            build_corpus=False) # do not build a corpus column

# Save the selected data to a csv file 'selected_{topic}.csv', uses data_path
parser.save_selected_data(df,topic)

使用上述选项,我提取了一个包含 983 篇计算机科学文章的数据集。我们准备好进入下一步。

如果你想跳过数据处理步骤,可以使用cs数据集,该数据集可以在 Github 仓库中找到。

使用 KeyBERT 和 KeyLLM 进行关键词提取

方法

KeyBERT是一种从文本中提取关键词或关键短语的方法。它利用文档和单词的嵌入,通过余弦相似度找到与文档最相似的子短语。KeyLLM 是另一种最小化的关键词提取方法,但它基于 LLM。两种方法都由 Maarten Grootendorst 开发和维护。

这两种方法可以结合使用以获得更好的结果。通过 KeyBERT 提取的关键词可以通过 KeyLLM 进行微调。相反,通过传统 NLP 技术识别的候选关键词有助于为 LLM 提供基础,从而最小化不希望产生的输出。

有关使用 KeyLLM 的不同方式,请参见 Maarten Grootendorst, 介绍 KeyLLM — 使用 LLM 进行关键词提取。

— 作者制作的图表 —

使用 KeyBERT [source] 从每篇文档中提取关键词——这些是提供给 LLM 进行微调的候选关键词:

  • 使用 Sentence Transformers 对文档进行嵌入,以构建文档级别的表示。

  • 为 N-gram 单词/短语提取词嵌入,

  • 使用余弦相似度来查找与每个文档最相似的单词或短语。

使用 KeyLLM[source]微调通过 KeyBERT 提取的关键词,利用transformers 进行文本生成[source]:

  • Sentence Transformers 中的社区检测方法[source]将相似文档分组,因此我们只会从每个组中的一个文档中提取关键词,

  • 候选关键词由 LLM 提供,LLM 为每个聚类微调关键词。

除了 Sentence Transformers,KeyBERT 还支持其他嵌入模型,见[此处]。

Sentence Transformers 通过使用指定的阈值来促进社区检测。当文档缺乏固有的聚类时,可能不会出现明确的分组。在我的案例中,从 983 个标题中,大约识别出了 800 个不同的社区。更加自然聚类的数据往往能产生定义更明确的社区。

大型语言模型

在对各种较小的 LLM 进行实验后,我选择了Zephyr-7B-Beta用于本项目。该模型基于Mistral-7B,并且是首批使用直接偏好优化(DPO)进行微调的模型之一。它不仅在同类模型中表现优异,而且在一些基准测试中超过了 Llama2–70B。欲了解有关此 LLM 的更多见解,请查看 Benjamin Marie, Zephyr 7B Beta: 一个好老师就是你所需要的一切。虽然可以直接在 Google Colab Pro 上使用该模型,但我选择了使用由TheBloke准备的 GPTQ 量化版本。

首先按照模型卡片中的说明下载模型及其分词器

# Required installs
!pip install transformers optimum accelerate
!pip install auto-gptq --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/

# Required imports
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline

# Load the model and the tokenizer
model_name_or_path = "TheBloke/zephyr-7B-beta-GPTQ"

llm = AutoModelForCausalLM.from_pretrained(model_name_or_path,
                                             device_map="auto",
                                             trust_remote_code=False,
                                             revision="main") # change revision for a different branch
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path, 
                     use_fast=True)

此外,构建文本生成管道:

generator = pipeline(
    model=llm,
    tokenizer=tokenizer,
    task='text-generation',
    max_new_tokens=50,
    repetition_penalty=1.1,
)

关键词提取提示

这一过程中的实验至关重要。寻找最佳提示需要一些试验和错误,性能取决于所选择的模型。不要忘记,LLM 是基于概率的,因此无法保证每次都返回相同的输出。为了开发下面的提示,我依赖了实验以及以下考虑因素:

prompt = "Tell me about AI"
prompt_template=f'''<|system|>
</s>
<|user|>
{prompt}</s>
<|assistant|>
'''

这是我用来微调通过 KeyBERT 提取的关键词的提示:

prompt_keywords= """
<|system|>
I have the following document:
Semantics and Termination of Simply-Moded Logic Programs with Dynamic Scheduling
and five candidate keywords:
scheduling, logic, semantics, termination, moded

Based on the information above, extract the keywords or the keyphrases that best describe the topic of the text.
Follow the requirements below:
1\. Make sure to extract only the keywords or keyphrases that appear in the text.
2\. Provide five keywords or keyphrases! Do not number or label the keywords or the keyphrases!
3\. Do not include anything else besides the keywords or the keyphrases! I repeat do not include any comments!

semantics, termination, simply-moded, logic programs, dynamic scheduling</s>

<|user|>
I have the following document:
[DOCUMENT]
and five candidate keywords:
[CANDIDATES]

Based on the information above, extract the keywords or the keyphrases that best describe the topic of the text.
Follow the requirements below:
1\. Make sure to extract only the keywords or keyphrases that appear in the text.
2\. Provide five keywords or keyphrases! Do not number or label the keywords or the keyphrases!
3\. Do not include anything else besides the keywords or the keyphrases! I repeat do not include any comments!</s>

<|assistant|>
"""

关键词提取与解析

现在我们拥有了进行关键词提取所需的一切。让我提醒你,我处理的是标题,因此输入文档较短,完全在 BERT 嵌入的令牌限制范围内。

从创建TextGeneration 管道封装器开始,为 LLM 实例化KeyBERT。选择嵌入模型。如果未指定嵌入模型,默认模型为all-MiniLM-L6-v2。在这种情况下,我选择了句子嵌入的最高性能预训练模型,完整列表请参见这里

# Install the required packages
!pip install keybert
!pip install sentence-transformers

# The required imports
from keybert.llm import TextGeneration
from keybert import KeyLLM, KeyBERT
from sentence_transformers import SentenceTransformer

# KeyBert TextGeneration pipeline wrapper
llm_tg = TextGeneration(generator, prompt=prompt_keywords)

# Instantiate KeyBERT and specify an embedding model
kw_model= KeyBERT(llm=llm_tg, model = "all-mpnet-base-v2")

记住,数据集已经准备并保存为 pandas 数据框df。要处理标题,只需调用extract_keywords方法:

# Retain the articles titles only for analysis
titles_list = df.title.tolist()

# Process the documents and collect the results
titles_keys = kw_model.extract_keywords(titles_list, thresold=0.5)

# Add the results to df
df["titles_keys"] = titles_keys

threshold 参数决定了将文档分组到同一社区所需的最小相似度。较高的值会将几乎相同的文档分组,而较低的值则会将涵盖相似主题的文档聚类在一起。

嵌入模型的选择显著影响合适的阈值,因此建议查阅模型卡片以获取指导。感谢 Maarten Grootendorst 强调这一点,正如这里所示。

需要注意的是,我的观察仅适用于句子变换器,因为我尚未尝试其他类型的嵌入模型。

让我们看一些输出:

评论

  • 在这里提供的第二个示例中,我们观察到原始文本中没有出现的关键词或关键短语。如果这对你构成问题,可以考虑启用check_vocab=True,如[这里]所做。然而,需要记住的是,这些结果受 LLM 选择的影响很大,量化的影响较小,提示的构造也有一定影响。

  • 在处理较长的输入文档时,我注意到输出与预期结果的偏差增多。

  • 一个一致的观察是,提取的关键词数量通常偏离五个。尤其在输入较简短时,常会遇到提取的关键词较少的标题。相反,一些标题会提取出多达 10 个关键词。让我们来看一下此次运行的关键词数量分布:

这些变化使得后续的解析步骤变得复杂。对此,有几种解决方案:我们可以详细调查这些情况,要求模型修正并修剪或重新整理关键词,或者简单地忽略这些情况,只专注于包含正好五个关键词的标题,正如我为这个项目决定的那样。

使用 HDBSCAN 进行关键词聚类

接下来的步骤是对关键词和关键短语进行聚类,以揭示文章中的共同话题。为此,我使用了两种算法:UMAP 用于降维,HDBSCAN 用于聚类。

算法:HDBSCAN 和 UMAP

基于密度的层次空间聚类与噪声HDBSCAN,是一种高效的无监督算法,旨在发现数据中的模式。它根据聚类的密度和接近度找到最优的聚类。这在聚类的数量和形状可能未知或难以确定的情况下尤其有用。

如果使用相同的超参数多次运行 HDBSCAN 聚类算法,其结果可能会有所不同。这是因为 HDBSCAN 是一个随机算法,这意味着聚类过程涉及一定程度的随机性。具体来说,HDBSCAN 使用随机初始化聚类层次结构,这可能导致每次运行算法时产生不同的聚类分配。

然而,算法在不同运行之间的变化程度可能取决于多个因素,例如数据集、超参数以及用于随机数生成器的种子值。在某些情况下,变化可能很小,而在其他情况下则可能较大。

HDBSCAN 提供了两种聚类选项。

  • 主要的聚类算法,标记为 hard_clustering,将每个数据点分配到一个聚类或标记为噪声。这是硬性分配;没有混合的隶属关系。这种方法可能导致一个大的聚类被归类为噪声(聚类标签为 -1),而其他则为许多较小的聚类。微调超参数至关重要 [参见这里],因为它选择了一个专门为该领域量身定制的嵌入模型。请查看相关的 Google Colab,查看该项目数据集上硬聚类的结果。

  • 软聚类是 HDBSCAN 库的一个新特性。在这种方法中,点不会被分配聚类标签,而是被分配一个概率向量。该向量的长度等于找到的聚类数。向量中每个位置的概率值表示该点是该聚类成员的概率。这使得点可能是多个聚类的混合。如果你想更好地理解软聚类是如何工作的,请参阅HDBSCAN 的软聚类工作原理。这种方法更适合当前项目,因为它生成了一个较大的、相对相似大小的聚类集。

虽然 HDBSCAN 在低到中维度数据上表现良好,但随着维度的增加,性能通常会显著下降。一般而言,HDBSCAN 在最多大约 50 维的数据上表现最佳,[请参阅这里]。

聚类用的文档通常使用 BERT 家族中的高效变换器进行嵌入,得到的是一个几百维的数据集。

为了减少嵌入向量的维度,我们使用UMAP统一流形近似与投影),这是一种非线性降维算法,并且是同类中表现最好的算法。它旨在学习数据的流形结构,并找到一个低维嵌入,从而保留该流形的基本拓扑结构。

研究表明,UMAP 在将高维数据的整体结构保留到低维时非常有效,同时在性能上优于其他流行算法,如 t-SNE 和 PCA。

关键词聚类

  • 安装并导入所需的包和库。
# Required installs
!pip install umap-learn
!pip install hdbscan
!pip install -U sentence-transformers

# General imports
import pandas as pd
import numpy as np
import re
import pickle

# Imports needed to generate the BERT embeddings
from sentence_transformers import SentenceTransformer

# Libraries for dimensionality reduction
import umap.umap_ as umap

# Import the clustering algorithm
import hdbscan
  • 准备数据集,将每个标题的单独五元组中的所有关键词和关键短语聚合成一个独特关键词的单一列表,并将其保存为一个 pandas 数据框。
# Load the data if needed - titles with 5 extracted keywords
df5 = pd.read_csv(data_path+parsed_keys_file) 

# Create a list of all sublists of keywords and keyphrases
df5_keys = df5.titles_keys.tolist()

# Flatten the list of sublists
flat_keys = [item for sublist in df5_keys for item in sublist]

# Create a list of unique keywords
flat_keys = list(set(flat_keys))

# Create a dataframe with the distinct keywords
keys_df = pd.DataFrame(flat_keys, columns = ['key'])

我从 884 个处理过的标题中获得了将近 3000 个独特的关键词和关键短语。以下是一个示例:n-可染图、实验、约束、树结构、复杂性等。

  • 使用 Sentence Transformers 生成 768 维的嵌入。
# Instantiate the embedding model
model = SentenceTransformer('all-mpnet-base-v2')

# Embed the keywords and keyphrases into 768-dim real vector space
keys_df['key_bert'] = keys_df['key'].apply(lambda x: model.encode(x))
  • 使用 UMAP 进行降维。
# Reduce to 10-dimensional vectors and keep the local neighborhood at 15
embeddings = umap.UMAP(n_neighbors=15, # Balances local vs. global structure.
                       n_components=10, # Dimension of reduced vectors
                       metric='cosine').fit_transform(list(keys_df.key_bert))

# Add the reduced embedding vectors to the dataframe
keys_df['key_umap'] = embeddings.tolist()
  • 使用 HDBSCAN 对 10 维向量进行聚类。为了保持这篇博客简洁,我将省略与硬聚类相关的参数描述。有关每个参数的详细信息,请参阅[HDBSCAN 参数选择]。
# Initialize the clustering model
clusterer = hdbscan.HDBSCAN(algorithm='best',
                            prediction_data=True,
                            approx_min_span_tree=True,
                            gen_min_span_tree=True,
                            min_cluster_size=20,
                            cluster_selection_epsilon = .1,
                            min_samples=1,
                            p=None,
                            metric='euclidean',
                            cluster_selection_method='leaf')

# Fit the data
clusterer.fit(embeddings)

# Create soft clusters
soft_clusters = hdbscan.all_points_membership_vectors(clusterer)

# Add the soft cluster information to the data
closest_clusters = [np.argmax(x) for x in soft_clusters]
keys_df['cluster'] = closest_clusters

以下是关键词在各个聚类中的分布情况。通过检查关键词和关键短语在软聚类中的分布,发现总共有 60 个聚类,每个聚类中的元素分布较为均匀,数量从大约 20 个到近 100 个不等。

提取聚类描述和标签

在对关键词进行聚类后,我们现在准备再次使用 GenAI 来增强和优化我们的发现。在这一步,我们将使用大型语言模型(LLM)分析每个聚类,总结关键词和关键短语,并为每个聚类分配一个简短的标签。

尽管这不是必要的,我选择继续使用相同的 LLM——Zephyr-7B-Beta。如果您需要下载该模型,请参考相关章节。值得注意的是,我会调整提示,以适应此任务的不同特点。

以下函数旨在为每个聚类提取标签和描述,解析输出并将其集成到 pandas 数据框中。

def extract_description(df: pd.DataFrame,
                        n: int     
                        )-> pd.DataFrame:
    """
    Use a custom prompt to send to a LLM
    to extract labels and descriptions for a list of keywords.
    """

    one_cluster = df[df['cluster']==n]
    one_cluster_copy = one_cluster.copy()
    sample = one_cluster_copy.key.tolist()

    prompt_clusters= f"""
    <|system|>
    I have the following list of keywords and keyphrases:
    ['encryption','attribute','firewall','security properties',
    'network security','reliability','surveillance','distributed risk factors',
    'still vulnerable','cryptographic','protocol','signaling','safe',
    'adversary','message passing','input-determined guards','secure communication',
    'vulnerabilities','value-at-risk','anti-spam','intellectual property rights',
    'countermeasures','security implications','privacy','protection',
    'mitigation strategies','vulnerability','secure networks','guards']

    Based on the information above, first name the domain these keywords or keyphrases 
  belong to, secondly give a brief description of the domain.
    Do not use more than 30 words for the description!
    Do not provide details!
    Do not give examples of the contexts, do not say 'such as' and do not list the keywords 
  or the keyphrases!
    Do not start with a statement of the form 'These keywords belong to the domain of' or 
  with 'The domain'.

    Cybersecurity: Cybersecurity, emphasizing methods and strategies for safeguarding digital information
    and networks against unauthorized access and threats.
    </s>

    <|user|>
    I have the following list of keywords and keyphrases:
    {sample}
    Based on the information above, first name the domain these keywords or keyphrases belong to, secondly
    give a brief description of the domain.
    Do not use more than 30 words for the description!
    Do not provide details!
    Do not give examples of the contexts, do not say 'such as' and do not list the keywords or the keyphrases!
    Do not start with a statement of the form 'These keywords belong to the domain of' or with 'The domain'.
    <|assistant|>
    """

    # Generate the outputs
    outputs = generator(prompt_clusters,
                    max_new_tokens=120,
                    do_sample=True,
                    temperature=0.1,
                    top_k=10,
                    top_p=0.95)

    text = outputs[0]["generated_text"]

    # Example string
    pattern = "<|assistant|>\n"

    # Extract the output
    response = text.split(pattern, 1)[1].strip(" ")
    # Check if the output has the desired format
    if len(response.split(":", 1)) == 2:
        label  = response.split(":", 1)[0].strip(" ")
        description = response.split(":", 1)[1].strip(" ")
    else:
        label = description = response

    # Add the description and the labels to the dataframe
    one_cluster_copy.loc[:, 'description'] = description
    one_cluster_copy.loc[:, 'label'] = label

    return one_cluster_copy

现在我们可以将上述函数应用于每个聚类并收集结果:

import re
import pandas as pd

# Initialize an empty list to store the cluster dataframes
dataframes = []
clusters = len(set(keys_df.cluster))

# Iterate over the range of n values
for n in range(clusters-1):
    df_result = extract_description(keys_df,n)
    dataframes.append(df_result)

# Concatenate the individual dataframes
final_df = pd.concat(dataframes, ignore_index=True)

让我们来看一个输出示例。完整的输出列表请参见Google Colab

我们必须记住,LLM 由于其固有的概率性,可能会表现出不可预测的行为。虽然它们通常遵循指令,但它们的遵从性并非绝对。即使是对提示或输入文本的微小修改,也可能导致输出结果的重大差异。在extract_description()函数中,我添加了一个功能,在标签描述列中记录响应,以应对那些没有遵循Label: Description格式的情况,如上面第 7 个聚类的异常输出所示。所有 60 个聚类的输出可以在附带的Google Colab笔记本中查看。

第二个观察是,每个聚类都由 LLM 独立解析,并且可能会得到重复的标签。此外,可能会出现从输入列表中提取的重复关键词。

该过程的有效性在很大程度上依赖于 LLM 的选择,使用高性能的 LLM 时,问题较少。输出结果还依赖于关键词聚类的质量以及聚类中是否存在固有的主题。

缓解这些挑战的策略取决于聚类的数量、数据集的特征以及项目所需的准确性。以下是两种选择:

  • 手动修正每个问题,正如我在这个项目中所做的那样。只有 60 个聚类和三个错误的输出,手动调整以修正错误的输出,并确保每个聚类都有唯一的标签。

  • 使用 LLM 进行修正,尽管这种方法无法保证完全无误。

构建知识图谱

上传到图谱的数据

有两个 csv 文件(或如果在单个会话中工作,则是 pandas 数据框)可以从中提取数据。

  • articles - 它包含每篇文章的唯一idtitleabstracttitles_keys,后者是提取的五个关键词或关键短语的列表;

  • keywords - 包含 keyclusterdescriptionlabel 等列,其中 key 包含一个完整的唯一关键词或关键短语列表,其他特征则描述关键词所属的集群。

Neo4j 连接

要构建知识图谱,我们首先需要设置一个 Neo4j 实例,可以选择如 Sandbox、AuraDB 或 Neo4j Desktop 等选项。对于这个项目,我使用的是 AuraDB 的免费版本。启动一个空白实例并下载其凭证非常简单。

接下来,建立与 Neo4j 的连接。为了方便,我使用了一个自定义的 Python 模块,您可以在 utils/neo4j_conn.py 中找到。这个模块包含了连接和与图数据库交互的方法。

# Install neo4j
!pip install neo4j

# Import the connector
from utils.neo4j_conn import *

# Graph DB instance credentials
URI = 'neo4j+ssc://xxxxxx.databases.neo4j.io'
USER = 'neo4j'
PWD = 'your_password_here'

# Establish the connection to the Neo4j instance
graph = Neo4jGraph(url=URI, username=USER, password=PWD)

我们即将构建的图有一个简单的架构,由三个节点和两个关系组成:

— 作者图片 —

现在构建图表非常简单,只需两个 Cypher 查询:

# Load Keyword and Topic nodes, and the relationships HAS_TOPIC
query_keywords_topics = """
    UNWIND $rows AS row
    MERGE (k:Keyword {name: row.key})
    MERGE (t:Topic {cluster: row.cluster, description: row.description, label: row.label})
    MERGE (k)-[:HAS_TOPIC]->(t)
    """
graph.load_data(query_keywords_topics, keywords)

# Load Article nodes and the relationships HAS_KEY
query_articles = """
    UNWIND $rows as row
    MERGE (a:Article {id: row.id, title: row.title, abstract: row.abstract})
    WITH a, row
    UNWIND row.titles_keys as key
    MATCH (k:Keyword {name: key})
    MERGE (a)-[:HAS_KEY]->(k)
    """
graph.load_data(query_articles, articles)

查询图谱

让我们查看节点和关系按类型的分布:

我们可以通过计算与它们连接的关键词关联的文章数量,找出我们文章集合中最受欢迎的主题(或集群):

这是与集群 58 对应的 Semantics 节点的快照及其相关的关键词:

— 作者图片 —

我们还可以通过以下查询来识别标题中常见的词汇:

结论

我们已经看到,如何通过结构化和丰富一组看似无关的短文本条目。使用传统的 NLP 和机器学习,我们首先提取关键词,然后进行聚类。这些结果为 Zephyr-7B-Beta 进行的精炼过程提供了指导和基础。尽管仍然需要对 LLM 进行一些监督,但初步输出已经得到了显著的增强。知识图谱用于揭示语料库中新发现的连接。

我们的关键收获是,没有任何单一方法是完美的。然而,通过战略性地结合不同的技巧,认识到它们的优缺点,我们可以取得更优的结果。

参考文献

Google Colab 笔记本和代码

数据

技术文档

博客与文章

  • Maarten Grootendorst,介绍 KeyLLM——使用 LLM 进行关键词提取,数据科学前沿,2023 年 10 月 5 日。

  • Benjamin Marie,Zephyr 7B Beta:一个好老师就是你所需要的一切,数据科学前沿,2023 年 11 月 10 日。

  • H4 团队,Zephyr:LM 对齐的直接蒸馏,技术报告,arXiv: 2310.16944,2023 年 10 月 25 日。

利用 OpenAI 工具调用:从零开始构建一个可靠的 AI 代理

原文:towardsdatascience.com/leverage-openai-tool-calling-building-a-reliable-ai-agent-from-scratch-4e21fcd15b62?source=collection_archive---------0-----------------------#2024-03-26

使用DALL·E创建

开发和完善 AI 代理的逐步工作流程,同时处理错误

Lukasz KowejszaTowards Data Science Lukasz Kowejsza

·发表于Towards Data Science ·16 分钟阅读·2024 年 3 月 26 日

--

当我们思考人工智能的未来时,我们设想直观的日常助手无缝地融入我们的工作流程,承担复杂的常规任务。我们都已经找到了减轻我们日常脑力劳动单调的接触点。然而,目前处理的主要任务包括文本创作、修正和头脑风暴,尤其是 RAG(检索增强生成)流程在持续发展中的重要作用。我们的目标是为大型语言模型提供更好的上下文,以生成更有价值的内容。

想到人工智能的未来,我脑海中浮现的是《钢铁侠》中的贾维斯,或者游戏《命运》中的拉斯普廷。在这两个例子中,人工智能充当了与复杂系统交互的语音控制界面,提供高级抽象。例如,托尼·斯塔克用它来管理研究、进行计算和运行模拟。即便是 R2-D2,也能够通过语音命令与陌生的计算机系统进行交互,提取数据或与建筑系统互动。

在这些场景中,AI 使得与复杂系统的交互变得更加简单,无需最终用户对这些系统有深入的理解。这可以与今天大型企业中的 ERP 系统类比。在大型企业中,很少有人能够完全了解和理解公司内部 ERP 系统的每个方面。想象一下,在不远的将来,AI 可能会协助几乎所有与 ERP 系统的交互。从最终用户管理客户数据或记录订单,到软件开发人员修复 Bug 或实现新功能,这些交互很可能很快就会由熟悉 ERP 系统各个方面和流程的 AI 助手来促成。这样的 AI 助手将知道应该将客户数据输入到哪个数据库,并且能够理解与 Bug 相关的哪些流程和代码。

为了实现这一目标,我们面临着若干挑战和创新。我们需要重新思考流程及其文档。目前的 ERP 流程是为人类使用设计的,具有不同用户的特定角色、供人类使用的文档、供人类填写的输入框,以及旨在直观且无误的用户交互设计。这些方面的设计在 AI 交互中将会有所不同。我们需要为 AI 交互设计特定的角色,并制定不同的流程设计,以实现直观且无误的 AI 交互。这一点在我们与提示语的工作中已初见端倪。我们认为清晰的任务往往并不像预期的那样简单。

从概念到现实:构建 AI 代理的基础

然而,让我们首先回顾一下代理的概念。代理,或者说能够使用提供工具执行任务并决定如何使用这些工具的 AI 助手,是最终能够实现此类系统的基础构件。它们是我们希望将其集成到复杂系统各个方面的流程组件。但正如之前文章中强调的那样,可靠地部署这些代理是具有挑战性的。在本文中,我将展示我们如何设计和优化一个能够可靠地与数据库交互的代理。

尽管 AI 未来的宏大愿景令人鼓舞,但采取切实可行的步骤实现这一愿景至关重要。为了展示我们如何开始为此类先进 AI 系统构建基础,让我们专注于创建一个用于常见任务——费用跟踪——的原型代理。这个原型将作为一个具体的示例,展示 AI 如何高效地帮助管理财务交易,展现 AI 在自动化日常任务中的潜力,并突出在设计一个能够无缝与数据库交互的 AI 系统时所面临的挑战和考量。通过从一个具体且易于理解的用例入手,我们可以获得宝贵的洞察,这些洞察将为未来更复杂 AI 代理的开发提供指导。

本文的目标

本文将为一系列文章奠定基础,旨在开发一个聊天机器人,作为小型企业的单一互动入口,支持并执行商业流程,或作为个人生活中的聊天助手,帮助整理你需要跟踪的所有事务。从数据、日常事务、文件到图片,我们希望简单地与我们的助手对话,让它来决定在哪里存储和提取你的数据。

从 AI 未来的宏伟愿景转向实际应用,让我们聚焦于创建一个原型代理。这个代理将作为实现之前讨论的雄心勃勃目标的基础步骤。我们将开始开发一个“费用跟踪”代理,这是一个简单但至关重要的任务,展示 AI 如何高效地帮助管理财务交易。

这个“费用跟踪”原型不仅展示了 AI 在自动化日常任务中的潜力,还揭示了设计一个能够与数据库无缝交互的 AI 系统所面临的挑战和考量。通过聚焦于这个示例,我们可以探讨代理设计、输入验证以及 AI 与现有系统集成的复杂性——为未来更复杂的应用奠定坚实的基础。

1. 实践操作:测试 OpenAI 工具调用

为了让我们的原型代理得以实现并识别潜在瓶颈,我们正在测试 OpenAI 工具调用功能。从一个基础的费用跟踪示例开始,我们正在奠定一个模拟现实世界应用的基础。这一阶段涉及创建一个基础模型,并通过 langchain 库的convert_to_openai_tool函数将其转化为 OpenAI 工具架构。此外,构建一个report_tool使得我们的未来代理能够传达结果或突出缺失的信息或问题:

from pydantic.v1 import BaseModel, validator  
from datetime import datetime
from langchain_core.utils.function_calling import convert_to_openai_tool

class Expense(BaseModel):    
   description: str    
   net_amount: float    
   gross_amount: float    
   tax_rate: float    
   date: datetime

class Report(BaseModel):
   report: str

add_expense_tool = convert_to_openai_tool(Expense)
report_tool = convert_to_openai_tool(Report)

在设置好数据模型和工具后,下一步是使用 OpenAI 客户端 SDK 发起一个简单的工具调用。在这个初步测试中,我们故意向模型提供不足的信息,以查看它是否能够正确指出缺少的内容。这种方法不仅测试代理的功能能力,还测试其交互和错误处理能力。

调用 OpenAI API

现在,我们将使用 OpenAI 客户端 SDK 发起一个简单的工具调用。在我们的第一次测试中,我们故意向模型提供不足的信息,看看它是否能通知我们缺失的细节。

from openai import OpenAI  
from langchain_core.utils.function_calling import convert_to_openai_tool  

SYSTEM_MESSAGE = """You are tasked with completing specific objectives and 
must report the outcomes. At your disposal, you have a variety of tools, 
each specialized in performing a distinct type of task.  

For successful task completion:  
Thought: Consider the task at hand and determine which tool is best suited 
based on its capabilities and the nature of the work.  

Use the report_tool with an instruction detailing the results of your work.  
If you encounter an issue and cannot complete the task:  

Use the report_tool to communicate the challenge or reason for the 
task's incompletion.  
You will receive feedback based on the outcomes of 
each tool's task execution or explanations for any tasks that 
couldn't be completed. This feedback loop is crucial for addressing 
and resolving any issues by strategically deploying the available tools.  
"""  
user_message = "I have spend 5$ on a coffee today please track my expense. The tax rate is 0.2."

client = OpenAI()  
model_name = "gpt-3.5-turbo-0125"  

messages = [  
    {"role":"system", "content": SYSTEM_MESSAGE},  
    {"role":"user", "content": user_message}  
]  

response = client.chat.completions.create(  
            model=model_name,  
            messages=messages,  
            tools=[  
                convert_to_openai_tool(Expense),  
                convert_to_openai_tool(ReportTool)]  
        )

接下来,我们需要一个新函数,从响应中读取函数调用的参数:

def parse_function_args(response):
    message = response.choices[0].message
    return json.loads(message.tool_calls[0].function.arguments)

print(parse_function_args(response))
{'description': 'Coffee',
 'net_amount': 5,
 'gross_amount': None,
 'tax_rate': 0.2,
 'date': '2023-10-06T12:00:00Z'}

正如我们所观察到的,我们在执行过程中遇到了几个问题:

  1. gross_amount 未被计算。

  2. 日期被虚构了。

有鉴于此,让我们尝试解决这些问题并优化我们的代理工作流程。

2. 优化工具处理

为了优化代理工作流,我认为优先考虑工作流而非提示工程是至关重要的。虽然调整提示使代理能够完美地使用提供的工具并避免出错是有吸引力的,但更建议首先调整工具和流程。当出现典型错误时,初步的考虑应是如何通过代码修复

处理缺失信息

有效处理缺失信息是创建强大且可靠代理的关键课题。在前面的示例中,提供像“get_current_date”这样的工具是特定场景的变通方法。然而,我们必须假设缺失信息会在各种情况下发生,我们不能仅仅依赖提示工程和添加更多工具来防止模型出现幻觉。

针对这种情况的一个简单解决方法是修改工具模式,将所有参数视为可选。此方法确保代理只提交它知道的参数,从而防止不必要的幻觉。

因此,我们来看看 openai 工具模式:

add_expense_tool = convert_to_openai_tool(Expense)
print(add_expense_tool)
{'type': 'function',
 'function': {'name': 'Expense',
  'description': '',
  'parameters': {'type': 'object',
   'properties': {'description': {'type': 'string'},
    'net_amount': {'type': 'number'},
    'gross_amount': {'type': 'number'},
    'tax_rate': {'type': 'number'},
    'date': {'type': 'string', 'format': 'date-time'}},
   'required': ['description',
    'net_amount',
    'gross_amount',
    'tax_rate',
    'date']}}}

如我们所见,我们有一个特殊的required关键字,需要移除。以下是如何调整add_expense_tool模式以通过移除required关键字将参数设为可选:

del add_expense_tool["function"]["parameters"]["required"]

设计Tool

接下来,我们可以设计一个Tool类,首先检查输入参数是否缺少值。我们创建Tool类并定义两个方法:.run().validate_input(),以及一个属性openai_tool_schema,在其中通过移除必需参数来操作工具模式。此外,我们还定义了ToolResult BaseModel,其中包含contentsuccess字段,用作每次工具运行的输出对象。

from pydantic import BaseModel
from typing import Type, Callable, Dict, Any, List

class ToolResult(BaseModel):  
    content: str  
    success: bool  

class Tool(BaseModel):  
    name: str  
    model: Type[BaseModel]  
    function: Callable  
    validate_missing: bool = False  

    class Config:  
        arbitrary_types_allowed = True  

    def run(self, **kwargs) -> ToolResult:
        if self.validate_missing:
            missing_values = self.validate_input(**kwargs)  
            if missing_values:  
                content = f"Missing values: {', '.join(missing_values)}"  
                return ToolResult(content=content, success=False)  
        result = self.function(**kwargs)  
        return ToolResult(content=str(result), success=True)  

    def validate_input(self, **kwargs) -> List[str]:  
        missing_values = []  
        for key in self.model.__fields__.keys():  
            if key not in kwargs:  
                missing_values.append(key)  
        return missing_values
    @property
    def openai_tool_schema(self) -> Dict[str, Any]:
        schema = convert_to_openai_tool(self.model)
        if "required" in schema["function"]["parameters"]:
            del schema["function"]["parameters"]["required"]
        return schema

Tool类是 AI 代理工作流中的关键组件,作为创建和管理代理可用工具的蓝图,以执行特定任务。它旨在处理输入验证、执行工具的功能,并以标准化格式返回结果。

Tool类的关键组件:

  1. name:工具的名称。

  2. model:定义工具输入模式的 Pydantic BaseModel。

  3. function:工具执行的可调用函数。

  4. validate_missing:一个布尔标志,表示是否验证缺失的输入值(默认为False)。

Tool类的两个主要方法:

  1. **run(self, **kwargs) -> ToolResult****:该方法负责使用提供的输入参数执行工具的功能。它首先检查validate_missing是否设置为True。如果是,它会调用validate_input()方法检查是否缺少输入值。如果发现缺失的值,则返回一个ToolResult对象,包含错误信息并将success设置为False。如果所有必需的输入值都存在,则继续执行工具的function,并返回一个包含结果的ToolResult对象,success设置为True

  2. **validate_input(self, **kwargs) -> List[str]**: 该方法将传递给工具的输入参数与model中定义的预期输入架构进行比较。它遍历model中定义的字段,检查每个字段是否在输入参数中。如果任何字段缺失,它将字段名称添加到缺失值列表中。最后,它返回缺失值的列表。

Tool类还有一个名为openai_tool_schema的属性,它返回该工具的 OpenAI 工具架构。它使用convert_to_openai_tool()函数将model转换为 OpenAI 工具架构格式。此外,它从架构中移除了"required"键,使所有输入参数变为可选。这使得代理只需要提供可用信息,而无需虚构缺失值。

通过封装工具的功能、输入验证和架构生成,Tool类为在 AI 代理工作流中创建和管理工具提供了一个简洁且可重用的接口。它抽象了处理缺失值的复杂性,并确保代理在执行适当的工具时,能够优雅地处理不完整的信息,基于可用输入进行操作。

测试缺失信息处理

接下来,我们将扩展我们的 OpenAI API 调用。我们希望客户端能够使用我们的工具,并且我们的响应对象能够直接触发tool.run()。为此,我们需要在新创建的Tool类中初始化我们的工具。我们定义了两个虚拟函数,它们返回一个成功消息字符串。

def add_expense_func(**kwargs):  
    return f"Added expense: {kwargs} to the database."

add_expense_tool = Tool(  
    name="add_expense_tool",  
    model=Expense,  
    function=add_expense_func  
)  

def report_func(report: str = None):  
    return f"Reported: {report}"  

report_tool = Tool(  
    name="report_tool",  
    model=ReportTool,  
    function=report_func  
)  

tools = [add_expense_tool, report_tool]

接下来,我们定义一个辅助函数,它接收客户响应作为输入,并帮助与我们的工具进行交互。

def get_tool_from_response(response, tools=tools):  
    tool_name = response.choices[0].message.tool_calls[0].function.name  
    for t in tools:  
        if t.name == tool_name:  
            return t  
    raise ValueError(f"Tool {tool_name} not found in tools list.")

def parse_function_args(response):  
    message = response.choices[0].message  
    return json.loads(message.tool_calls[0].function.arguments)

def run_tool_from_response(response, tools=tools):  
    tool = get_tool_from_response(response, tools)  
    tool_kwargs = parse_function_args(response)  
    return tool.run(**tool_kwargs)

现在,我们可以使用新工具执行客户端,并使用run_tool_from_response函数。

response = client.chat.completions.create(  
            model=model_name,  
            messages=messages,  
            tools=[tool.openai_tool_schema for tool in tools]  
        )

tool_result = run_tool_from_response(response, tools=tools)
print(tool_result)
content='Missing values: gross_amount, date' success=False

完美地,我们现在看到我们的工具表明缺失值存在。感谢我们将所有参数设为可选的技巧,我们现在避免了虚构的参数。

3. 构建代理工作流

就目前而言,我们的流程还不能算是真正的代理。到目前为止,我们只执行了一个 API 工具调用。为了将其转变为代理工作流,我们需要引入一个迭代过程,将工具执行的结果反馈给客户端。基本的流程应该是这样的:

图片由作者提供

让我们通过创建一个新的 OpenAIAgent 类来开始:

class StepResult(BaseModel):  
    event: str   
    content: str  
    success: bool

class OpenAIAgent:  

    def __init__(  
            self,   
            tools: list[Tool],   
            client: OpenAI,   
            system_message: str = SYSTEM_MESSAGE,   
            model_name: str = "gpt-3.5-turbo-0125",  
            max_steps: int = 5,  
            verbose: bool = True  
    ):  
        self.tools = tools  
        self.client = client  
        self.model_name = model_name  
        self.system_message = system_message  
        self.step_history = []  
        self.max_steps = max_steps  
        self.verbose = verbose  

    def to_console(self, tag: str, message: str, color: str = "green"):  
        if self.verbose:  
            color_prefix = Fore.__dict__[color.upper()]  
            print(color_prefix + f"{tag}: {message}{Style.RESET_ALL}")

就像我们的ToolResult对象一样,我们为每个代理步骤定义了一个StepResult对象。然后,我们定义了OpenAIAgent类的__init__方法和一个to_console()方法,用于将我们的中间步骤和工具调用打印到控制台,使用 colorama 进行彩色打印。接下来,我们定义了代理的核心部分,即run()run_step()方法。

class OpenAIAgent:

    # ... __init__...

    # ... to_console ...

    def run(self, user_input: str):  

        openai_tools = [tool.openai_tool_schema for tool in self.tools]    
        self.step_history = [    
            {"role":"system", "content":self.system_message},    
            {"role":"user", "content":user_input}    
        ]    

        step_result = None    
        i = 0

        self.to_console("START", f"Starting Agent with Input: {user_input}")

        while i < self.max_steps:  
            step_result = self.run_step(self.step_history, openai_tools)    

            if step_result.event == "finish":    
                break  
            elif step_result.event == "error":  
                self.to_console(step_result.event, step_result.content, "red")  
            else:  
                self.to_console(step_result.event, step_result.content, "yellow")  
            i += 1   

        self.to_console("Final Result", step_result.content, "green")  
        return step_result.content

run()方法中,我们首先初始化了step_history,它将作为我们的消息记忆,包含预定义的 system_message 和 user_input。然后我们开始执行 while 循环,在每次迭代时调用run_step,该方法将返回一个 StepResult 对象。我们识别出智能体是否完成了任务,或者是否发生了错误,错误信息也会传递到控制台。

class OpenAIAgent:

    # ... __init__...

    # ... to_console ...
    # ... run ...
    def run_step(self, messages: list[dict], tools):  

        # plan the next step  
        response = self.client.chat.completions.create(  
            model=self.model_name,  
            messages=messages,  
            tools=tools  
        )  

        # add message to history  
        self.step_history.append(response.choices[0].message)  

        # check if tool call is present  
        if not response.choices[0].message.tool_calls:  
            return StepResult(
                event="Error",
                content="No tool calls were returned.", 
                success=False
                )  

        tool_name = response.choices[0].message.tool_calls[0].function.name  
        tool_kwargs = parse_function_args(response)  

        # execute the tool call  
        self.to_console(
        "Tool Call", f"Name: {tool_name}\nArgs: {tool_kwargs}", "magenta"
        )  
        tool_result = run_tool_from_response(response, tools=self.tools)  
        tool_result_msg = self.tool_call_message(response, tool_result)  
        self.step_history.append(tool_result_msg)  

        if tool_result.success:  
            step_result = StepResult(  
                event="tool_result",   
                content=tool_result.content,   
                success=True)  
        else:  
            step_result = StepResult(  
                event="error",   
                content=tool_result.content,   
                success=False  
            )   

        return step_result  

    def tool_call_message(self, response, tool_result: ToolResult):  
        tool_call = response.choices[0].message.tool_calls[0]  
        return {  
            "tool_call_id": tool_call.id,  
            "role": "tool",  
            "name": tool_call.function.name,  
            "content": tool_result.content,  
        }

现在我们已经定义了每个步骤的逻辑。我们首先通过之前测试过的客户端 API 调用工具,获得一个响应对象。然后将响应消息对象附加到step_history中。接着我们验证响应对象中是否包含工具调用,如果没有,我们将在 StepResult 中返回一个错误。然后我们将工具调用记录到控制台,并通过我们之前定义的方法run_tool_from_response()运行选定的工具。我们还需要将工具结果添加到我们的消息历史中。OpenAI 为此定义了一个特定的格式,使得模型能够知道哪个工具调用对应哪个输出,通过将 tool_call_id 传递到消息字典中来实现这一点。我们的tool_call_message()方法完成了这一过程,它接受响应对象和工具结果作为输入参数。在每个步骤结束时,我们将工具结果赋值给 StepResult 对象,这个对象还指示该步骤是否成功,并将其返回到run()中的循环。

4. 运行智能体

现在我们可以使用之前的示例来测试我们的智能体,并直接为其装备一个get_current_date_tool。在这里,我们可以将之前定义的validate_missing属性设置为False,因为该工具不需要任何输入参数。

class DateTool(BaseModel):  
    x: str = None  

get_date_tool = Tool(  
    name="get_current_date",  
    model=DateTool,  
    function=lambda: datetime.now().strftime("%Y-%m-%d"),  
    validate_missing=False  
)  

tools = [  
    add_expense_tool,   
    report_tool,  
    get_date_tool  
]  

agent = OpenAIAgent(tools, client)
agent.run("I have spent 5$ on a coffee today please track my expense. The tax rate is 0.2.")
START: Starting Agent with Input: 
"I have spend 5$ on a coffee today please track my expense. The tax rate is 0.2."

Tool Call: get_current_date
Args: {}
tool_result: 2024-03-15

Tool Call: add_expense_tool
Args: {'description': 'Coffee expense', 'net_amount': 5, 'tax_rate': 0.2, 'date': '2024-03-15'}
error: Missing values: gross_amount

Tool Call: add_expense_tool
Args: {'description': 'Coffee expense', 'net_amount': 5, 'tax_rate': 0.2, 'date': '2024-03-15', 'gross_amount': 6}
tool_result: Added expense: {'description': 'Coffee expense', 'net_amount': 5, 'tax_rate': 0.2, 'date': '2024-03-15', 'gross_amount': 6} to the database.
Error: No tool calls were returned.

Tool Call: Name: report_tool
Args: {'report': 'Expense successfully tracked for coffee purchase.'}
tool_result: Reported: Expense successfully tracked for coffee purchase.

Final Result: Reported: Expense successfully tracked for coffee purchase.

在我们的原型智能体成功执行后,值得强调的是,智能体根据计划高效地利用了指定的工具。最初,它调用了get_current_date_tool,为费用条目建立了一个基础时间戳。随后,在尝试通过add_expense_tool记录费用时,我们精心设计的工具类识别出缺少了gross_amount—这是准确财务跟踪所需的关键数据。令人印象深刻的是,智能体通过使用提供的tax_rate自动计算出gross_amount,解决了这一问题。

值得一提的是,在我们的测试过程中,输入费用的性质——例如花费的 5 美元是净额还是毛额——并未明确指定。在这一点上,这样的具体信息对于智能体成功完成任务并非必须。然而,这一情况揭示了一个有价值的洞察,能够帮助我们优化智能体的理解和交互能力:将此类详细信息纳入初始系统提示中,能够显著提高智能体处理费用条目的准确性和效率。这个调整将确保从一开始就能更全面地理解财务数据。

关键要点

  1. 迭代开发:该项目强调了迭代开发周期的关键性,通过反馈促进持续改进。这种方法在人工智能领域尤为重要,因为可变性是常态,要求我们采用一种灵活且响应迅速的开发策略。

  2. 处理不确定性:我们的旅程强调了优雅地管理模糊性和错误的重要性。诸如可选参数和严格输入验证等创新,在提高代理的可靠性和用户体验方面发挥了重要作用。

  3. 针对特定任务的定制代理工作流:这项工作的一个关键见解是根据特定用例定制代理工作流的重要性。除了组建一套工具,工具之间的互动和响应的战略设计至关重要。这样的定制确保了代理能够有效应对具体挑战,从而采取更加专注和高效的问题解决方法。

我们已经踏上的旅程只是对人工智能代理及其在各个领域应用的更大探索的开始。随着我们不断突破人工智能可能性边界,我们邀请您与我们一起加入这场激动人心的冒险。通过在本文奠定的基础上继续前行,并期待即将推出的增强功能,您将亲眼见证人工智能代理如何彻底改变企业和个人处理数据及自动化复杂任务的方式。

一起,让我们拥抱人工智能的力量,释放它改变我们工作和与技术互动方式的潜力。人工智能的未来光明,而我们正站在塑造这一未来的最前沿,一次一个可靠的代理。

展望未来

随着我们在探索人工智能代理潜力的旅程继续前进,接下来的文章将重点扩展我们原型的能力,并将其与现实世界系统集成。在下一篇文章中,我们将深入探讨如何设计一个稳健的项目结构,使我们的代理能够与 SQL 数据库无缝交互。通过利用本文中开发的代理,我们将展示人工智能如何高效地管理和操作存储在数据库中的数据,为自动化数据相关任务开辟无限可能。

在此基础上,本系列的第三篇文章将介绍高级查询功能,使我们的代理能够处理更复杂的数据检索和操作任务。我们还将探索路由代理的概念,它将作为管理多个子代理的中心枢纽,每个子代理负责与特定数据库表进行交互。这种层级结构将允许用户用自然语言提出请求,路由代理会将请求解释并转发给适当的子代理执行。

为进一步增强我们基于 AI 的系统的实用性和安全性,我们将引入基于角色的访问控制系统。这样可以确保用户根据分配的角色,拥有适当的权限来访问和修改数据。通过实施这一功能,我们能够展示 AI 代理如何在保持数据完整性和安全性的同时,在实际场景中得到应用。

通过这些即将推出的增强功能,我们旨在展示 AI 代理在简化数据管理流程方面的真正潜力,并为用户提供一种更直观、更高效的方式来与数据库互动。通过结合自然语言处理、数据库管理和基于角色的访问控制的力量,我们将为开发能够彻底改变企业和个人处理数据方式的高级 AI 助手奠定基础。

请继续关注这些令人兴奋的进展,因为我们将继续推动 AI 代理在数据管理及其他领域的边界。

源代码

此外,涵盖的项目的完整源代码可以在 GitHub 上找到。您可以通过github.com/elokus/AgentDemo访问它。

在机器学习项目中利用 Python 继承

原文:towardsdatascience.com/leverage-python-inheritance-in-ml-projects-52e7e16401ab?source=collection_archive---------7-----------------------#2024-11-21

图片由Milad Fakurian提供,来自Unsplash

学习如何实现编码最佳实践,以避免技术债务

Marcello PolitiTowards Data Science Marcello Politi

·发表于Towards Data Science ·阅读时间 5 分钟·2024 年 11 月 21 日

--

介绍

许多接触机器学习的人没有计算机工程的扎实背景,当他们需要处理一个实际产品时,他们的代码可能会很凌乱,且难以管理。这就是为什么我总是强烈建议学习使用编码最佳实践,这样你就能在团队中顺利工作,并提升你所从事的项目。今天,我想谈谈 Python 的继承,并展示一些简单的示例,说明如何在机器学习领域中使用它。

软件开发和其他信息技术领域,技术债务(也称为设计债务代码债务)是未来重新工作所隐含的成本,因为某个解决方案优先考虑快速实现而非长期设计。

如果你有兴趣了解更多关于设计模式的内容,你可能会对我之前的文章感兴趣。

Python 继承

继承不仅是 Python 的概念,还是面向对象编程中的一个通用概念。因此,在本教程中,我们需要处理类和对象,这是在 Python 中相对于其他语言(如 Java)不常用的编程范式。

利用 AI 协同效应进行命名实体消歧

原文:towardsdatascience.com/leveraging-ai-synergies-for-named-entity-disambiguation-033aede7c9a0?source=collection_archive---------4-----------------------#2024-04-22

NED 系列

结合 LLMs、词汇本体和知识图谱在生物医学领域的视角

Giuseppe FutiaTowards Data Science Giuseppe Futia

·发表于Towards Data Science ·20 分钟阅读·2024 年 4 月 22 日

--

我想感谢 Vlasta Kus 对本文初步想法的反馈。

介绍

命名实体消歧(NED)是自然语言处理(NLP)中的一项重要任务,旨在将含糊不清的命名实体提及解析为对应的明确实体,并映射到参考知识库中。

NED 的关键思想是将一段连续文本(如“2 型糖尿病”)映射到一个真实的实体,例如位于医学知识库中的“2 型糖尿病(CUI C0011860)”,该知识库如“统一医学语言系统”(UMLS)。NED 的作用在关键领域,特别是生物医学领域尤为重要,因为高准确度地检测精确信息对于在适当的时间做出正确决策至关重要。

大型语言模型(LLMs)是能够从大量文本数据中学习模式和关系的机器学习模型,并基于这些积累和压缩的知识,生成自然语言文本。然而,由于其固有的局限性,LLMs 在需要精确且详细理解人类语言的任务中效果不佳,例如命名实体消歧。此外,它们的一些...

利用 Gemini-1.5-Pro-Latest 实现更智能的饮食

原文:towardsdatascience.com/leveraging-gemini-1-5-pro-latest-for-smarter-eating-64eeb4012ce7?source=collection_archive---------8-----------------------#2024-08-21

学习如何使用 Google 的 Gemini-1.5-pro-latest 模型开发一个用于卡路里计数的生成式 AI 应用程序

Mary AraTowards Data Science Mary Ara

·发表于Towards Data Science ·阅读时长 8 分钟·2024 年 8 月 21 日

--

图片由Pickled Stardust提供,来源于Unsplash

你有没有想过,在你吃晚餐时,你究竟摄入了多少卡路里?我经常这样想。如果你能通过一个应用程序,简单地上传一张你的盘子照片,就能在决定食物的摄入量之前,获得总卡路里的估算,那该有多好?

我创建的这个卡路里计数应用程序可以帮助你实现这一目标。它是一个 Python 应用程序,使用 Google 的 Gemini-1.5-Pro-Latest 模型来估算食物中的卡路里含量。

这个应用程序接受两个输入:一个关于食物的问题和一张食物或食物项的图片,或者简单地说,一盘食物。它会输出问题的答案、图片中食物的总卡路里数量,以及每个食物项的卡路里分解。

在本文中,我将解释从零开始构建应用程序的完整端到端过程,使用 Google 的 Gemini-1.5-pro-latest(Google 发布的大型生成式语言模型),以及我如何使用 Streamlit 开发该应用程序的前端。

利用图表推进思维链推理

原文:towardsdatascience.com/leveraging-graphs-to-advance-chain-of-thought-reasoning-77022a0e1413?source=collection_archive---------11-----------------------#2024-01-16

Anthony AlcarazTowards Data Science Anthony Alcaraz

·发表在Towards Data Science·阅读时间 5 分钟·2024 年 1 月 16 日

--

本文的文本经过人工智能软件的处理,以增强语法、流畅性和可读性。

思维链(CoT)提示迅速成为一种技术,大幅提升大型语言模型的推理能力。通过逐步展示推理链,CoT 允许像 GPT-4 这样的模型解决多步问题 — 从数学到常识谜题。关键见解是通过从上下文示例中学习,模型可以获得复杂的逻辑技能,而无需诉诸于任务特定的微调。

然而,限制 CoT 提示更广泛适用性的关键是依赖手工设计的演示。制作具有连贯逻辑流的高质量推理链需要大量的人力和专业知识。为了释放全部潜力,我们需要自动生成高质量 CoT 演示的方法。

最近的工作通过检索和生成方法来解决这个问题。但是由此产生的链条往往存在不连贯、间隙和基础错误。在文本序列中捕捉推理链的流动、概念性流程已被证明是困难的。我们提出用专门的图表代替以推进 CoT 提示的推理结构。

具体来说,本文确定了两种互补的图表驱动技术:

  1. 将 CoT 演示建模为有向图,以捕捉流动并分析结构

利用大语言模型提升商业效率

原文:towardsdatascience.com/leveraging-large-language-models-for-business-efficiency-b06cb943a286?source=collection_archive---------2-----------------------#2024-03-01

实施大语言模型提升商业改善:逐步指南

Benoît CourtyTowards Data Science Benoît Courty

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

--

TL;DR: 本文讨论了大语言模型如何提升公司流程。目标读者是具有技术背景的人,如软件架构师或首席技术官(CTO)。文章展示了如何高效使用大语言模型(LLM),你将学习如何利用现代技术,如检索增强生成(RAG)、函数调用和微调,并通过案例学习应用。

目录

· 识别商业需求

· 自己探索一个想法

· 创建评估数据集

· 考虑内部工业化

· 通过公司数据定制响应

· 通过函数调用使用 API

· 将任务分解为多个提示

· 通过微调提高性能

· 组合模型

· 结论

图片来自 Andrea De SantisUnsplash

在快速发展的技术环境中,人工智能(AI)和机器学习(ML)已成为推动创新、提高效率和竞争优势的关键力量,遍布各行各业。对于首席技术官(CTO)、IT 总监、技术项目经理和技术产品经理来说,理解并将这些技术融入商业战略已不再是可选项,而是必须。

大语言模型(LLM),如 ChatGPT,能够做的不仅仅是聊天,这一点并不令人惊讶。

我们将一步步探讨防止数据失真、提升操作效率并更好地利用贵公司资源的策略。

确定业务需求

您已经知道,像 ChatGPT、Gemini、Mistral 等大型语言模型(LLMs)已成为强大的工具,可以自动化任务并提升客户服务。作为一名商业决策者,了解 LLMs 的能力与局限性将帮助您做出有关其实施的明智决策。

利用大语言模型(LLMs)的第一步是识别一个可以自动化的任务,以改善客户服务或解放员工免于重复性任务。例如,LLMs 可以用于自动化文档中的信息检索、撰写报告或处理客户请求。

自己探索一个想法

一旦确定了业务需求,下一步是与 ChatGPT(或 Gemini)一起手动评估,估算当前生成性 AI 的推理能力是否足够满足需求。

您可以创建一组示例输入,并评估 ChatGPT 生成的响应的准确性。

假设您想要自动化公司收到的邮件分发。

您需要获取一些邮件并测试在线 LLM 是否能够整理它们并准备回应。

图片由Serhat Beyazkaya提供,来源于Unsplash

这是一个示例提示:

Our company is organized into several departments, each with distinct functions and areas of responsibility. Here's a brief overview:
Sales Department: This team is responsible for generating new business leads, negotiating deals, and maintaining relationships with existing clients.

Marketing Department: This department focuses on promoting our products and services, managing our brand reputation, and conducting market research.

Customer Service Department: This team handles inquiries, complaints, and support requests from our customers.

Human Resources Department: This department manages employee relations, recruitment, training, and benefits.

Technical Support Department: This team provides assistance with technical issues related to our products and services.

Finance Department: This department is responsible for managing the company's financial resources, including budgeting, forecasting, and financial reporting.

We want you to categorize the following email. I would like you to analyze the content of the email to determine the most suitable department. Here are some guidelines to help you with your analysis:
Look for keywords and phrases that indicate the email is related to sales, marketing, customer service, HR, technical support, or finance.

Consider the tone and context of the email. For example, an email that expresses frustration with a product may be best suited for the customer service department.

If an email contains multiple topics, categorize it based on the most prominent or urgent issue.

Part 3: Once you have analyzed the email and determined the most suitable department, please append your recommendations. Please ensure that your recommendations are accurate and consistent, as this will help us to improve our response times and provide better service to our customers.
Part 4: Write a model of response for the department
Here is the email:

你好,

我希望这封邮件能找到您一切安好。我写信是为了申请在即将到来的假期期间休假。具体来说,我希望请以下几天假:

12 月 20 日,星期一

12 月 21 日,星期二

12 月 22 日,星期三

12 月 23 日,星期四

12 月 24 日,星期五

我理解这是公司一年中最忙碌的时段,对我的缺席可能会带来的不便表示歉意。然而,我已经积累了足够的假期时间来覆盖这些天,我相信请假将使我得以休息,并以充沛的精力迎接新的一年。

如果我的请求有任何问题,或者您需要我提供其他信息,请告知。感谢您的时间和考虑。

此致,敬礼,[员工姓名]

模型答案:

Based on the content of the email, I would recommend categorizing it under the
__Human Resources Department.__
The email contains keywords such as "time off," "vacation time," and "absence,"
which are related to employee relations and benefits, which fall under the 
responsibilities of the HR department. The tone of the email is polite and 
professional, and the context indicates that the employee is requesting 
time off for personal reasons.
__Here is a model response for the HR department:__
Dear [Employee Name],
Thank you for your email and for providing ample notice regarding your time off request for the upcoming holiday season. We appreciate your consideration for the busy time of year and your accrued vacation time to cover your absence.
I am pleased to inform you that your request for time off from Monday, December 20th to Friday, December 24th has been approved. Please ensure that you have communicated your absence to your team and made any necessary arrangements to cover your responsibilities during your time off.
If you have any further questions or concerns, please do not hesitate to reach out to the Human Resources Department.
Thank you for your hard work and dedication to the company. We hope that you have a relaxing and enjoyable holiday season.
Best regards,
[Your Name] Human Resources Department.

⚠️ 不要犹豫继续调整提示,直到得到您希望复现的响应形式。这可能需要一些时间去适应。不要因为第一次失败或第一次成功就停下脚步。不要犹豫从头再来,甚至可以挑战模型为您编写一个提示。

⚠️ 不要忘记测试边缘情况,确保至少评估 80%的使用案例。

创建评估数据集

现在是时候扩大您的测试了。之前您已经证明它在少量示例下有效,现在请使用更多数据重新测试,以衡量准确度。

创建数据集对于评估一个过程至关重要。你可以使用这个数据集来测试模型,确保它满足你的业务需求。数据集应该代表你的业务可能遇到的真实世界场景。理想情况下,应该使用代码创建一个可重复的评估链。例如,通过从问题列表调用 OpenAI 的 API,并自动比较预期的答案。

通过 ChatGPT 订阅,查看 Explore GPTs,你还可以尝试 Data Analyst 上传 Excel 文件并与 AI 进行交互。

  • 编制电子邮件数据集:首先组建一个包含 100 封样本电子邮件的 Excel 文件,这些是你的公司可能会接收到的邮件。

  • 草拟详细提示

在这种情况下,你可以将提示结构化为三个部分:

  • 第一部分:详细描述你公司内的各个部门,概述其具体职能和责任领域。

  • 第二部分:将数据集引入模型,指示其分析每封电子邮件的内容,以确定最合适的部门。

  • 第三部分:指示模型将其建议添加到 Excel 文件中的新列中,有效地对每封电子邮件进行分类。

  • 执行与评估:利用提示让模型为每封电子邮件确定正确的接收部门。在模型处理后,审查其建议,以评估准确性和相关性。

示例数据集截图(由作者使用 Mistral-medium AI 生成)

在考虑进一步操作之前,你可以手动对每个答案进行评分,并计算平均值,以评估结果是否足够好。以我们的例子为例,记住这个用例是一个 (电子邮件)到 (路由与建议回答)再到 (部门)的工作流,所以可以容忍一定的错误:人类可以修改答案,或者部门可以将电子邮件重新路由到另一个……如果在 100 封邮件中发生 10 次这样的情况,可能就足够了。

考虑内部工业化

你可以通过使用外部提供商提供的 API,加快生产就绪解决方案的速度。

你可以使用 OpenAI API 或其他 API 来构建你的 MVP,但你需要考虑多个因素,包括:

  • 你提供给外部 API 或聊天的所有数据都会在某个地方被记录

  • 即使服务提供商声称不使用你的数据,你仍然应该对数据进行匿名化处理……

  • 工业机密泄露风险:如果你在美国以外地区,请注意 OpenAI 受到 Cloud Act 的约束。

  • 速度限制:从 OpenAI 获取完整回复通常需要几秒钟,对于某些用例来说,这可能不够快。

  • 调用限制:每秒的调用次数 是有限制的,每月的最大费用也有限制。

  • 环境影响:大型通用模型对环境有显著影响,在考虑使用这些模型时应将其纳入考量。

  • 成本波动:例如,OpenAI API 的费用可能会波动,这会影响您的预算。

  • 难以确立竞争优势:使用 OpenAI API 时,确立竞争优势可能很具挑战性,因为这些 API 对所有企业开放。

  • 稳定性:像 Gemini、Mistral、Claude2、GPT4 等 LLM 私人模型并不总是稳定,您应当考虑监控回答的质量和稳定性。您还需要增加安全措施,以保护您的服务质量,并避免来自内外部的危险行为。问题可能出现在输入或输出环节。

为避免这些陷阱,您可以选择使用像 LLAMA 或 Mistral 这样的开源模型。这些开源替代方案提供了多个优势:

  1. 隐私与安全:自托管模型降低了工业机密泄漏的风险。

  2. 定制化:您可以微调开源模型,使其更好地适应您的特定业务需求。

  3. 更低的成本:开源模型通常比专有解决方案便宜,尤其是在考虑调用次数和月度支出的限制时。

  4. 环境影响:开源模型通常较小,可以针对特定的应用场景进行优化,可能会减少它们的环境足迹。你可以使用CodeCarbon来衡量这一点。

  5. 竞争优势:通过定制开源模型,您可以创建一个独特的解决方案,使您的业务在竞争中脱颖而出。

现在您已经自动化了电子邮件的路由,让我们来提高答案的质量。一种方法是将公司文档添加到模型的能力中,这样模型就能从您的文档中找到答案,而不是依赖其“记忆”。

使用公司数据定制回答

使用公司数据定制 LLM 的回答将为用户创造一个更准确、更量身定制的体验。

照片由Yasamine June提供,来源于Unsplash

你不能将所有公司数据都放入提示中。这就是为什么检索增强生成(RAG)很有用,它是一种将数据库的信息检索与大型语言模型的生成能力结合起来的技术。通过使用 RAG,你可以提高回答的准确性。你还可以告诉用户哪些文档被用于生成回答。

RAG 技术可以通过以下公式简洁地表达:

<使用数十亿数据训练的 LLM> + <您的提示> + <您的公司数据集> = 与您的业务背景对齐的回答

RAG 通常使用向量数据库,因为它在大多数情况下都有效,以下是如何创建该数据库:

  1. 将您的文档按短章节进行拆分

  2. 使用嵌入模型将章节转换为向量。相同主题的向量将在 n 维空间中接近。典型的向量是一个包含 1,024 个浮动值的数组。可以把它想象成每个值代表一个特征,比如颜色、大小、性别……它不是硬编码的,模型在训练时会自行找到这些值。

  3. 将它们存储在向量数据库中

图片由作者提供

当您收到电子邮件时,您将像这样使用 RAG:

  1. 将您客户的电子邮件转换为向量

  2. 使用这个向量查询数据库,检索与段落最接近的 10 个向量

  3. 将这些段落的文本添加到提示中

  4. 向 LLM 询问答案

  5. 答案将基于提示中提供的数据

图片由作者提供

如果您想了解更多,请阅读检索增强生成(RAG)

现在您的答案将使用您的数据,因此有助于防止所谓的幻觉

ℹ️ 模型幻觉(Hallucination)并不是一个容易处理的问题。因为 LLM 的“记忆”更像是人类的记忆(对世界的压缩表示),而不像计算机的精确内存。而且模型是经过训练来帮助您的,即使它们不知道答案,它们也会尽量提供帮助,错误信息可能会被呈现为事实。RAG 通过为模型提供相关数据来帮助应对这个问题。

RAG 对于非结构化数据非常有效,但有时您可能有更好的方式来回答问题,比如包含每个产品定价的表格数据,或者您甚至可能需要计算税款,或者寻找一个日程中的空位安排会议。让我们看看如何通过函数调用来实现。

使用 API 的函数调用

函数调用是一种允许 LLM 与您的企业 API 进行交互的方式,例如:

  • Salesforce,SAP 作为您的 ERP 系统

  • Service Now 或其他票务服务

  • 日程

  • 发票,定价

  • 自定义 API 来执行贵公司中的任何操作

  • 第三方 API

函数调用是一个至关重要的功能,它允许您在不将 API 暴露给外界的情况下使用 API。这个功能为超越简单聊天应用程序的多种可能性打开了大门。例如,您可以将专门的内部服务或工具集成到 LLM 中,使其对您的业务更加多功能和有价值。您可以从客户处获取请求价格的邮件,将其发送给 LLM,将其转化为一个参数以调用定价 API,然后使用 API 的答案请 LLM 返回给客户写出回答。

给定请求:

*你好,

我真的很喜欢贵公司。我想为您订购一个太阳能面板安装轨道,请问价格是多少?

最佳问候*

您将请求发送给 LLM,并提供您公司中存在的 API 定义:

{
 "type": "function",
 "function": {
  "name": "multiply",
  "description": "Get product price.",
  "parameters": {
    "type": "object",
    "properties": {
      "product_name": {
      "description": "Name of the product",
      "type": "string"
      },
    "required": [ "product_name" ]
    }
  }
}

所以 LLM 从邮件中提取产品名称,并给出 JSON 以进行 API 调用:

{
 "product_name": "solar panel mounting rail"
}

由你来调用 API,因此它是完全安全的:LLM 永远不知道你的 API 在哪里,只知道它能做什么。

API 的回答可以返回给 LLM,以构建自然语言答案。

您能否根据以下条件回复此邮件:“太阳能面板安装轨道”的价格是 10 美元,未含 5%的增值税?“你好,我非常喜欢你们公司。我想订购一个太阳能面板安装轨道,价格是多少?此致敬礼,您的客户”

答复将是:

你好,

感谢您对我们公司的关注,并考虑我们的太阳能面板安装轨道。该安装轨道的价格为税前 10 美元,加上 5%的增值税,所以含税价为$10.50。

如果你有任何其他问题或想继续进行订单,请告诉我。

此致敬礼,

所以你现在有了一个可以利用内部服务更好地准备客户答复的系统。如果你已经投资于 API,这将是一个游戏规则的改变者。

我们刚刚看到,我们可以对一个任务多次调用 LLM,让我们更详细地看看这个过程。

将任务分解为多个提示

重要的是要注意,单一提示通常不足以完成复杂任务。你的项目很可能需要将任务分解为多个提示,这些提示将串联起来并结合多种技术。

例如,360learning.com/构建了一个平台,帮助基于单一文本文件输入使用 AI 创建在线课程。他们的流程使用了 9 个提示,进行了 30 次 OpenAI 调用,并结合 RAG 来实现目标。第一个提示要求对文档进行总结,第二个提示要求根据总结制定在线课程计划,然后使用 RAG 从标题开始逐步提取文档的各个部分,依此类推。

这里是他们演示的一些幻灯片:

来自360learning的标题

来自360learning的标题

视频来源:www.youtube.com/watch?v=1Eyc2GypnF4(法语)

他们使用 LangChain,一个帮助创建这些类型 LLM 管道的框架。

ℹ️ 你可能听说过“AI 代理”:它们只是一种结合提示的方式,但无需事先编写提示。代理是对 LLM 的调用,用以获取任务列表。然后,为每个任务再进行一次 LLM 调用,依此类推。它在赋予 LLM 调用外部工具的能力时效果最佳,例如像我们之前看到的那样通过浏览网页的功能。

现在你拥有了一个强大的管道,但如何改进模型本身以获得更快和更好的回答呢?你可以对模型进行微调。

微调以提高性能

微调通常可以提高模型的性能,并在保持相同性能的同时减少其体积,因为你可以使用更小的模型,如Mistral-7B,甚至是Phi-2

很少有公司能够负担得起从头训练一个 LLM,因为这需要一个庞大的数据集和数百个 GPU,比如 Llama2-70B 大约需要 200 万 GPU 小时。但你可以选择一个已经预训练的模型并进行微调,在大多数情况下,只需要一个下午的微调时间。

缺点是你需要构建一个包含数百个问题和答案的训练数据集。

模型结合

这是一种将多个模型结合成一个模型的新技术。结果是一个大型模型,称为专家混合模型(MoE),其能力超过同等大小的单一模型。实现这一点最简单的方法是使用MergeKit

由 AI 生成 — Bing Copilot — “一幅数学家、物理学家和机械工程师围坐在桌子旁,共同解决同一个问题,桌上摆放着一架拆解的无人机”

如果你很难决定使用哪个模型,这可能对你有帮助:在 MoE 中,由模型决定使用哪个。

结论

使用公司数据和 API 定制 LLM 的响应可以为用户创造更准确、更个性化的体验。微调能够提高性能,并且将任务分解为多个提示可以帮助处理复杂的任务。

尽管这一切看起来可能复杂且专门针对专家,但丰富的文档和众多库可以帮助简化实现。流行的库包括HuggingFaceLangchainHayStackAxolotl等…

然而,不要忘记集成的成本。与任何项目一样,从一个功能原型过渡到在现有 IT 系统中完全工业化的解决方案,涉及的成本是相当可观的。你常常会发现,你公司的流程比预期的更复杂,或者数据需要进行一定的清理才能处理。

虽然大型语言模型提供了许多优势,但不要忽视“老牌”机器学习技术的优势,比如随机森林DistiliBert。这些技术仍然具有价值,包括更快的处理速度、更易于集成到现有工具中、无需 GPU、较好的可解释性以及较低的成本。

我们希望这篇文章能为您如何将大型语言模型(LLM)纳入您的软件架构提供一些见解。

这篇文章由数据科学家 Benoît Courty 于 2024 年 2 月撰写,并在UpScale的首席执行官 Stéphane Van-Bosterhaudt 的帮助下完成。

更多相关阅读:

利用 Python Pint 单位处理包 — 第一部分

原文:towardsdatascience.com/leveraging-python-pint-units-handler-package-part-1-716a13e96b59?source=collection_archive---------11-----------------------#2024-04-16

在 Python 中操作和处理物理量

Jose D. Hernandez-BetancurTowards Data Science Jose D. Hernandez-Betancur

·发表于 Towards Data Science ·阅读时间:8 分钟·2024 年 4 月 16 日

--

图片由作者使用 Gencraft 生成。

如果你从事工程或科学领域的工作,甚至如果你是从事供应链操作、环境可持续性或任何涉及物理量(如时间、质量和长度)的领域的人,你可能会遇到需要编程处理和操作物理量的情况。作为一个数据实践者或使用 Python 的软件开发人员,你可能已经想出了像创建类似字典的查找表来进行单位转换(例如,kg 转 lb)或执行包含不同物理维度(例如,体积和时间)的操作的解决方案。Python 日益增长的生态系统的一个特点是,有各种各样的包可以帮助你实现你所需的功能。在这篇文章中,我将介绍 Pint,一个用于在数据科学或软件项目中编程处理单位的 Python 包 🍻。我将以一种有条理的方式组织这篇文章,让你不仅理解构成 Pint 的关键元素,还能了解如何无缝集成和扩展它们,以适应你的项目 🧩。

Pint 数量(Quantity)

Pint 包是基于面向对象编程(OOP)范式开发的。它使用对象来设置一个工具集,以“Python 风格”操作物理量。Quantity……

利用 Python Pint 单位处理包 —— 第二部分

原文:towardsdatascience.com/leveraging-python-pint-units-handler-package-part-2-2c5cc41d63ec?source=collection_archive---------10-----------------------#2024-07-10

在 Python 中创建自定义的物理量单位注册表

Jose D. Hernandez-BetancurTowards Data Science Jose D. Hernandez-Betancur

·发表于 Towards Data Science ·9 分钟阅读·2024 年 7 月 10 日

--

图片由作者使用 OpenAI 的 DALL-E 生成。

现实世界中的系统,如供应链,通常涉及与物理量(如质量和能量)打交道。你不必是专业的科学家或工程师,就能创建一个能够扩展的应用,并允许用户以任意单位输入物理量,而不会导致应用崩溃。Python 拥有一个强大且不断增长的生态系统,充满了各种可以轻松集成并扩展的替代方案,供你的应用使用。

在之前的一篇文章中,我介绍了 Pint 库,它使得处理物理量变得更加简单。如果你想通过更有趣的方式学习并将编程的各个部分拼凑在一起,可以随时回到那篇文章 🧩。

## 利用 Python Pint 单位处理包 —— 第一部分

在 Python 中操作和处理物理量

towardsdatascience.com

本文的目标是提供更多关于 Pint 包的信息,以便我们可以创建一种方法来存储动态定义的单位,并在程序结束后仍能保留这些单位⚡💾。

理解单位和维度…

利用小型 LLMs 增强检索增强生成(RAG)

原文:towardsdatascience.com/leveraging-smaller-llms-for-enhanced-retrieval-augmented-generation-rag-bc320e71223d?source=collection_archive---------1-----------------------#2024-10-18

Llama-3.2–1 B-Instruct 与 LanceDB

Alex PunnenTowards Data Science Alex Punnen

·发表于Towards Data Science ·阅读时间:12 分钟·2024 年 10 月 18 日

--

摘要:检索增强生成(RAG)结合了大型语言模型与外部知识源,以生成更加准确和符合上下文的回答。本文探讨了如何有效利用小型语言模型(如最近开源的 Meta 10 亿模型)来总结和索引大型文档,从而提高 RAG 系统的效率和可扩展性。我们提供了一个详细的指南,附有代码片段,展示如何从产品文档 PDF 中提取文本摘要,并将其存储在 LanceDB 数据库中以便高效检索。

引言

检索增强生成(Retrieval-Augmented Generation,RAG)是一种通过将语言模型与外部知识库集成来增强其能力的范式。虽然像 GPT-4 这样的巨大语言模型(LLMs)已经展示了显著的能力,但它们也伴随着较高的计算成本。小型 LLMs 则提供了更为高效的资源使用替代方案,尤其适用于诸如文本摘要和关键词提取等任务,这些任务在 RAG 系统中的索引和检索过程中至关重要。

在本文中,我们将演示如何使用小型 LLM:

  1. 从 PDF 文档中提取并总结文本

  2. 为摘要和关键词生成嵌入向量

  3. 高效地将数据存储在 LanceDB 数据库中

  4. 用于有效的 RAG

  5. 也可以为自我纠正 LLM 错误提供一个代理工作流

使用小型 LLM 大大降低了对大规模数据集进行这些类型转换的成本,并且在处理简单任务时,可以获得与大型参数 LLMs 相似的效果,同时可以轻松地在企业环境或云端托管,且成本最小。

我们将使用LLAMA 3.2 10 亿参数模型,它是目前最小的先进大型语言模型(LLM)。

LLM 增强的 RAG(作者图像)

嵌入原始文本的问题

在开始实施之前,了解为什么从文档中嵌入原始文本在 RAG 系统中可能会引发问题非常重要。

无效的上下文捕捉

在没有总结的情况下直接从页面嵌入原始文本,通常会导致以下类型的嵌入:

  • 高维噪音:原始文本可能包含无关信息、格式化伪影或套话,这些都不有助于理解核心内容。

  • 关键概念稀释:重要概念可能埋藏在多余的文本中,使得嵌入无法有效代表关键信息。

检索低效

当嵌入无法准确表示文本的关键信念时,检索系统可能会失败:

  • 有效匹配用户查询:嵌入可能与查询嵌入不匹配,导致相关文档检索效果差。

  • 提供正确的上下文:即使检索到了文档,它可能无法提供用户所寻求的精确信息,因为嵌入中的噪音。

解决方案:在嵌入之前进行总结

在生成嵌入之前对文本进行总结,通过以下方式解决这些问题:

  • 提取关键信息:总结提取了必要的要点和关键词,去除了不必要的细节。

  • 提高嵌入质量:从总结中生成的嵌入更集中,并能更好地代表主要内容,从而提高检索的准确性。

前提条件

在我们开始之前,请确保已经安装以下内容:

  • Python 3.7 或更高版本

  • PyTorch

  • Transformers 库

  • SentenceTransformers

  • PyMuPDF(用于 PDF 处理)

  • LanceDB

  • 配备 GPU 最少 6 GB 的笔记本电脑,或 Colab(T4 GPU 足够)或类似设备

步骤 1:设置环境

首先,导入所有必要的库并设置日志记录以进行调试和跟踪。

import pandas as pd
import fitz  # PyMuPDF
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
import lancedb
from sentence_transformers import SentenceTransformer
import json
import pyarrow as pa
import numpy as np
import re

步骤 2:定义辅助函数

创建提示

我们定义一个函数,用于创建与 LLAMA 3.2 模型兼容的提示。

def create_prompt(question):
    """
    Create a prompt as per LLAMA 3.2 format.
    """
    system_message = "You are a helpful assistant for summarizing text and result in JSON format"
    prompt_template = f'''
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
{system_message}<|eot_id|><|start_header_id|>user<|end_header_id|>
{question}<|eot_id|><|start_header_id|>assistant1231231222<|end_header_id|>
'''
    return prompt_template

处理提示

该函数使用模型和分词器处理提示。我们将温度设置为 0.1,以使模型的创造性降低(减少幻觉现象)。

def process_prompt(prompt, model, tokenizer, device, max_length=500):
    """
    Processes a prompt, generates a response, and extracts the assistant's reply.
    """
    prompt_encoded = tokenizer(prompt, truncation=True, padding=False, return_tensors="pt")
    model.eval()
    output = model.generate(
        input_ids=prompt_encoded.input_ids.to(device),
        max_new_tokens=max_length,
        attention_mask=prompt_encoded.attention_mask.to(device),
        temperature=0.1  # More deterministic
    )
    answer = tokenizer.decode(output[0], skip_special_tokens=True)
    parts = answer.split("assistant1231231222", 1)
   if len(parts) > 1:
        words_after_assistant = parts[1].strip()
        return words_after_assistant
    else:
        print("The assistant's response was not found.")
        return "NONE"

步骤 3:加载模型

我们使用 LLAMA 3.2 10 亿指令模型进行总结。我们以 bfloat16 加载模型以减少内存,并在 NVIDIA 笔记本 GPU(NVIDIA GeForce RTX 3060 6 GB / 驱动程序 NVIDIA-SMI 555.58.02 / Cuda 编译工具,版本 12.5,V12.5.40)下运行 Linux 操作系统。

更好的做法是通过 vLLM 或更好的exLLamaV2进行托管

model_name_long = "meta-llama/Llama-3.2-1B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name_long)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
log.info(f"Loading the model {model_name_long}")
bf16 = False
fp16 = True
if torch.cuda.is_available():
    major, _ = torch.cuda.get_device_capability()
    if major >= 8:
        log.info("Your GPU supports bfloat16: accelerate training with bf16=True")
        bf16 = True
        fp16 = False
# Load the model
device_map = {"": 0}  # Load on GPU 0
torch_dtype = torch.bfloat16 if bf16 else torch.float16
model = AutoModelForCausalLM.from_pretrained(
    model_name_long,
    torch_dtype=torch_dtype,
    device_map=device_map,
)
log.info(f"Model loaded with torch_dtype={torch_dtype}")

步骤 4:读取和处理 PDF 文档

我们从 PDF 文档的每一页提取文本。

file_path = './data/troubleshooting.pdf'
dict_pages = {}
# Open the PDF file
with fitz.open(file_path) as pdf_document:
    for page_number in range(pdf_document.page_count):
        page = pdf_document.load_page(page_number)
        page_text = page.get_text()
        dict_pages[page_number] = page_text
        print(f"Processed PDF page {page_number + 1}")

步骤 5:设置 LanceDB 和 SentenceTransformer

我们初始化了 SentenceTransformer 模型以生成嵌入,并设置了 LanceDB 来存储数据。我们使用基于 PyArrow 的 Schema 来构建 LanceDB 表。

请注意,当前关键词未被使用,但如果需要,可以用于混合搜索,即矢量相似度搜索和文本搜索。

# Initialize the SentenceTransformer model
sentence_model = SentenceTransformer('all-MiniLM-L6-v2')
# Connect to LanceDB
db = lancedb.connect('./data/my_lancedb')
# Define the schema using PyArrow
schema = pa.schema([
    pa.field("page_number", pa.int64()),
    pa.field("original_content", pa.string()),
    pa.field("summary", pa.string()),
    pa.field("keywords", pa.string()),
    pa.field("vectorS", pa.list_(pa.float32(), 384)),  # Embedding size of 384
    pa.field("vectorK", pa.list_(pa.float32(), 384)),
])
# Create or connect to a table
table = db.create_table('summaries', schema=schema, mode='overwrite')

第 6 步:总结和存储数据

我们遍历每一页,生成摘要和关键词,并将它们与嵌入一起存储到数据库中。

# Loop through each page in the PDF
for page_number, text in dict_pages.items():
    question = f"""For the given passage, provide a long summary about it, incorporating all the main keywords in the passage.
Format should be in JSON format like below:
{{
    "summary": <text summary>,
    "keywords": <a comma-separated list of main keywords and acronyms that appear in the passage>,
}}
Make sure that JSON fields have double quotes and use the correct closing delimiters.
Passage: {text}"""

    prompt = create_prompt(question)
    response = process_prompt(prompt, model, tokenizer, device)

    # Error handling for JSON decoding
    try:
        summary_json = json.loads(response)
    except json.decoder.JSONDecodeError as e:
        exception_msg = str(e)
        question = f"""Correct the following JSON {response} which has {exception_msg} to proper JSON format. Output only JSON."""
        log.warning(f"{exception_msg} for {response}")
        prompt = create_prompt(question)
        response = process_prompt(prompt, model, tokenizer, device)
        log.warning(f"Corrected '{response}'")
        try:
            summary_json = json.loads(response)
        except Exception as e:
            log.error(f"Failed to parse JSON: '{e}' for '{response}'")
            continue

    keywords = ', '.join(summary_json['keywords'])

    # Generate embeddings
    vectorS = sentence_model.encode(summary_json['summary'])
    vectorK = sentence_model.encode(keywords)

    # Store the data in LanceDB
    table.add([{
        "page_number": int(page_number),
        "original_content": text,
        "summary": summary_json['summary'],
        "keywords": keywords,
        "vectorS": vectorS,
        "vectorK": vectorK
    }])

    print(f"Data for page {page_number} stored successfully.")

使用 LLM 来修正它们的输出

在生成摘要和提取关键词时,LLM 有时会生成格式不符合预期的输出,比如格式错误的 JSON。

我们可以利用 LLM 本身来修正这些输出,通过提示它修复错误。如上面的代码所示。

# Use the Small LLAMA 3.2 1B model to create summary
for page_number, text in dict_pages.items():
    question = f"""For the given passage, provide a long summary about it, incorporating all the main keywords in the passage.
    Format should be in JSON format like below: 
    {{
        "summary": <text summary> example "Some Summary text",
        "keywords": <a comma separated list of main keywords and acronyms that appear in the passage> example ["keyword1","keyword2"],
    }}
    Make sure that JSON fields have double quotes, e.g., instead of 'summary' use "summary", and use the closing and ending delimiters.
    Passage: {text}"""
    prompt = create_prompt(question)
    response = process_prompt(prompt, model, tokenizer, device)
    try:
        summary_json = json.loads(response)
    except json.decoder.JSONDecodeError as e:
        exception_msg = str(e)
        # Use the LLM to correct its own output
        question = f"""Correct the following JSON {response} which has {exception_msg} to proper JSON format. Output only the corrected JSON.
        Format should be in JSON format like below: 
        {{
            "summary": <text summary> example "Some Summary text",
            "keywords": <a comma separated list of keywords and acronyms that appear in the passage> example ["keyword1","keyword2"],
        }}"""
        log.warning(f"{exception_msg} for {response}")
        prompt = create_prompt(question)
        response = process_prompt(prompt, model, tokenizer, device)
        log.warning(f"Corrected '{response}'")
        # Try parsing the corrected JSON
        try:
            summary_json = json.loads(response)
        except json.decoder.JSONDecodeError as e:
            log.error(f"Failed to parse corrected JSON: '{e}' for '{response}'")
            continue

在这段代码中,如果 LLM 的初始输出无法解析为 JSON,我们会再次提示 LLM 修正 JSON 格式。这个自我修正模式提高了我们管道的健壮性。

假设 LLM 生成了以下格式错误的 JSON:

{
    'summary': 'This page explains the installation steps for the product.',
    'keywords': ['installation', 'setup', 'product']
}

尝试解析这个 JSON 时,由于使用了单引号而不是双引号,结果会出现错误。我们捕获这个错误并提示 LLM 进行修正:

exception_msg = "Expecting property name enclosed in double quotes"
question = f"""Correct the following JSON {response} which has {exception_msg} to proper JSON format. Output only the corrected JSON."""

然后,LLM 提供修正后的 JSON:

{
    "summary": "This page explains the installation steps for the product.",
    "keywords": ["installation", "setup", "product"]
}

通过使用 LLM 修正它自己的输出,我们确保数据格式正确,便于后续处理。

通过 LLM 代理扩展自我修正

使用 LLM 来修正其输出的模式可以通过LLM 代理来扩展和自动化。LLM 代理可以:

  • 自动化错误处理:检测错误并自主决定如何修正它们,无需明确的指示。

  • 提高效率:减少手动干预或额外代码的需求来进行错误修正。

  • 增强健壮性:从错误中持续学习,改进未来的输出。

LLM 代理充当中介,管理信息流并智能地处理异常。它们可以被设计成:

  • 解析输出并验证格式。

  • 遇到错误时,重新提示 LLM 并提供精炼的指令。

  • 记录错误和修正,以便未来参考和模型微调。

近似实现

不需要手动捕获异常并重新提示,LLM 代理可以封装这些逻辑:

def generate_summary_with_agent(text):
    agent = LLMAgent(model, tokenizer, device)
    question = f"""For the given passage, provide a summary and keywords in proper JSON format."""
    prompt = create_prompt(question)
    response = agent.process_and_correct(prompt)
    return response

LLMAgent类将处理初始处理、错误检测、重新提示和内部修正。

现在,让我们看看如何使用嵌入来有效地使用 RAG 模式,再次利用 LLM 帮助进行排序。

检索和生成:处理用户查询

这是通常的流程。我们获取用户的问题并搜索最相关的摘要。

# Example usage
user_question = "Not able to manage new devices"
results = search_summary(user_question, sentence_model)

准备检索到的摘要

我们将检索到的摘要编译成一个列表,并将每个摘要与其页码关联以供参考。

summary_list = []
for idx, result in enumerate(results):
    summary_list.append(f"{result['page_number']}# {result['summary']}")

排序摘要

我们提示语言模型根据摘要与用户问题的相关性对其进行排序,并选择最相关的一个。这是通过使用 LLM 排序摘要,而非单纯使用 K-最近邻或余弦距离等排序算法来进行上下文嵌入(向量)匹配。

question = f"""From the given list of summaries {summary_list}, rank which summary would possibly have \
the answer to the question '{user_question}'. Return only that summary from the list."""
log.info(question) 

提取选定摘要并生成最终答案

我们从与所选摘要相关的原始内容中检索信息,并提示语言模型使用这些上下文生成详细的回答来回应用户的问题。

for idx, result in enumerate(results):
    if int(page_number) == result['page_number']:
        page = result['original_content']
        question = f"""Can you answer the query: '{user_question}' \
using the context below?
Context: '{page}'
"""
        log.info(question)
        prompt = create_prompt(
            question,
            "You are a helpful assistant that will go through the given query and context, think in steps, and then try to answer the query \
with the information in the context."
        )
        response = process_prompt(prompt, model, tokenizer, device, temperature=0.01)  # Less freedom to hallucinate
        log.info(response)
        print("Final Answer:")
        print(response)
        break

工作流说明

  1. 用户查询向量化:用户的问题通过与索引过程中使用的相同的 SentenceTransformer 模型转换为嵌入。

  2. 相似度搜索:查询嵌入用于在向量数据库(LanceDB)中搜索最相似的摘要,并返回前三个。

>>  From the VectorDB Cosine search and Top 3 nearest neighbour search result, 
prepended by linked page numbers

07:04:00 INFO:From the given list of summary [[
'112# Cannot place newly discovered device in managed state', 
'113# The passage discusses the troubleshooting steps for managing newly discovered devices on the NSF platform, specifically addressing issues with device placement, configuration, and deployment.',
'116# Troubleshooting Device Configuration Backup Issue']] rank which summary would possibly have the possible answer to the question Not able to manage new devices. Return only that summary from the list

3. 摘要排序:检索到的摘要被传递给语言模型,模型根据与用户问题的相关性对它们进行排序。

>> Asking LLM to Select from the Top N based on context

07:04:01 INFO:Selected Summary ''113# The passage discusses the troubleshooting steps for managing newly discovered devices on the NSF (Network Systems and Functional Requirements) platform, specifically addressing issues with device placement, configuration, and deployment.''

4. 上下文检索:通过解析页码并从 LanceDB 获取相关页面,检索与最相关摘要关联的原始内容。

07:04:01 INFO:Page number: 113
07:04:01 INFO:Can you answer the question or query or provide more deatils query:'Not able to manage new devices'         Using the context below
        context:'3 
Check that the server and client platforms are appropriately sized. ...
Failed SNMP communication between the server and managed device.
SNMP traps from managed devices are arriving at one server, 
or no SNMP traps are ....
'

5. 答案生成:语言模型使用检索到的上下文生成详细的回答来回应用户的问题。

这里是我使用的一个示例 PDF 文件的输出示例

07:04:08 INFO:I'll go through the steps and provide more details to answer the query.

The query is: "Not able to manage new devices"

Here's my step-by-step analysis:

**Step 1: Check that the server and client platforms are appropriately sized**

The context mentions that the NSP Planning Guide is available, which implies that the NSP (Network Service Provider) has a planning process to ensure that the server and client platforms are sized correctly. This suggests that the NSP has a process in place to evaluate the performance and capacity of the server and client platforms to determine if they are suitable for managing new devices.

**Step 2: Check for resynchronization problems between the managed network and the NFM-P**

The context also mentions that resynchronization problems between the managed network and the NFM-P can cause issues with managing new devices. This implies that there may be a problem with the communication between the server and client platforms, which can prevent new devices from being successfully managed.

**Step 3: Check for failed SNMP communication between the server and managed device**

The context specifically mentions that failed SNMP communication between the server and managed device can cause issues with managing new devices. This suggests that there may be a problem with the communication between the server and the managed device, which can prevent new devices from being successfully managed.

**Step 4: Check for failed deployment of the configuration request**

The context also mentions that failed deployment of the configuration request can cause issues with managing new devices. This implies that there may be a problem with the deployment process, which can prevent new devices from being successfully managed.

**Step 5: Perform the following steps**

The context instructs the user to perform the following steps:

1\. Choose Administration→NE Maintenance→Deployment from the XXX main menu.
2\. The Deployment form opens, listing incomplete deployments, deployer, tag, state, and other information.

Based on the context, it appears that the user needs to review the deployment history to identify any issues that may be preventing the deployment of new devices.

**Answer**

Based on the analysis, the user needs to:

1\. Check that the server and client platforms are appropriately sized.
2\. Check for resynchronization problems between the managed network and the NFM-P.
3\. Check for failed SNMP communication between the server and managed device.
4\. Check for failed deployment of the configuration request.

By following these steps, the user should be able to identify and resolve the issues preventing the management of

结论

我们可以使用像 LLAMA 3.2 1B Instruct 这样的较小 LLM 高效地总结和提取大型文档的关键词。这些摘要和关键词可以嵌入并存储在像 LanceDB 这样的数据库中,从而通过工作流中使用 LLM 而不仅仅是生成中,启用高效的检索供 RAG 系统使用。

参考文献

基于词典的情感分析使用 R 语言

原文:towardsdatascience.com/lexicon-based-sentiment-analysis-using-r-5c1db85984a1?source=collection_archive---------13-----------------------#2024-02-13

一项关于 COVID-19 大流行期间媒体简报所传达情感的实证分析

Okan BulutTowards Data Science Okan Bulut

·发布于Towards Data Science ·14 分钟阅读·2024 年 2 月 13 日

--

图片由Gino Crescoli提供,来源于Pixabay

在 COVID-19 大流行期间,我决定学习一项新的统计技术,以便让自己不至于总是沉浸在与大流行相关的新闻中。经过评估多个选项后,我发现与自然语言处理(NLP)相关的概念特别吸引人。因此,我决定深入研究这个领域,并探索其中一种具体的技术:情感分析,也被学术文献称为“意见挖掘”。这种分析方法使研究人员能够提取并解释写作文本中传达的情感,针对特定主题进行情感的分析。通过情感分析,人们可以辨别情感的极性(正面或负面)、性质以及在各种文本格式(如文档、客户评价和社交媒体帖子)中的强度。

在疫情期间,我观察到许多研究人员将情感分析作为衡量公众对与病毒相关的新闻和发展反应的工具。这包括分析用户生成的内容,主要来自 Twitter、YouTube 和 Instagram 等流行社交媒体平台。由于对这种方法产生了兴趣,我和我的同事们努力通过分析公共卫生机构提供的每日简报,来为现有的研究贡献力量。在阿尔伯塔省,曾担任该省首席公共卫生官的迪娜·辛肖博士定期发布关于该地区应对疫情的更新。通过分析这些公共卫生公告,我们旨在评估阿尔伯塔省在这一复杂公共卫生危机中实施沟通策略的有效性。我们的研究采用情感分析的方法,旨在揭示在这个充满挑战的公共卫生时期中,所采取的沟通策略的效果[1, 2]。

在这篇文章中,我将引导你们了解如何使用 R 进行情感分析。具体来说,我将重点介绍“基于词典的情感分析”,并在下一节中对此进行更详细的讨论。我将提供一些我们之前引用的出版物中整合的基于词典的情感分析的例子。此外,在未来的文章中,我还将深入探讨更先进的情感分析方法,利用Hugging Face上可访问的最先进的预训练模型。

基于词典的情感分析

在我深入了解情感分析的过程中,我发现提取情感的主要方法是基于词典的情感分析。这种方法涉及使用特定的词典,即语言或主题的词汇,来识别给定文本中所表达的情感的方向和强度。有些词典,如 Bing 词典[3],将单词分类为正面或负面。相反,其他词典则提供更详细的情感标签,例如 NRC 情感词典[4],它根据正面和负面情感对单词进行分类,还包括普鲁奇克的[5]基本情感的心理进化理论(例如,愤怒、恐惧、期待、信任、惊讶、悲伤、喜悦和厌恶)。

基于词典的情感分析通过将给定文本中的词与广泛使用的词典(如 NRC 和 Bing)中的词进行对比来进行。每个词会被赋予一个情感值,通常被分类为正面或负面。文本的总体情感得分是通过对文本中各个单词的情感得分进行求和得出的。例如,在一种情况下,如果文本中有 50 个正面词和 30 个负面词,按照 Bing 词典的标准,最终的情感得分将是 20。这个值表示文本中正面情感占主导地位。相反,如果得分为负数,则表示负面情感占主导地位。

使用 R 进行基于词典的情感分析既有趣又充满挑战。在分析公共卫生公告的情感时,我发现 Julia Silge 和 David Robinson 的书籍,Text Mining with R 非常有帮助。书中有专门讨论情感分析的章节,在该章节中,作者展示了如何使用像 Bing 和 NRC 这样的通用词典进行情感分析。然而,Julia 和 David 也强调了基于词典的情感分析的一个主要局限性。该分析仅考虑单个词(即单一词汇),而不考虑词前的修饰语。例如,像“not”这样的否定词在“not true”中会被忽略,情感分析将其处理为两个独立的词,“not”和“true”。此外,如果某个特定的词(无论是正面还是负面)在文本中反复出现,这可能会根据该词的情感倾向(正面或负面)扭曲结果。因此,基于词典的情感分析结果应该谨慎解读。

现在,让我们进入我们的示例,我们将使用 Dr. Deena Hinshaw 在 COVID-19 大流行期间的媒体简报进行基于词典的情感分析。我的目标是展示两个能够进行情感分析的 R 包 📉。

示例

为了简化,我们将专注于疫情的第一波(2020 年 3 月 — 2020 年 6 月)。所有媒体简报的文字记录在阿尔伯塔省政府的 COVID-19 疫情网站上公开发布(www.alberta.ca/covid)。该数据集附带一个开放数据许可证,允许公众访问和使用这些信息,包括用于商业目的。在将这些文字记录导入 R 后,我将所有文本转换为小写字母,然后使用tidytexttokenizers包进行词元化。词元化将媒体简报中的句子拆分为每个条目的单个单词(即,媒体简报的每一天)。接下来,我使用textstem包对词元进行了词形还原,将每个单词转化为其规范形式。最后,我使用 stopwords 包移除了常见的停用词,如“my”、“for”、“that”、“with”和“for”。最终的数据集可以在此处获得。现在,让我们将数据导入 R 并查看其内容。

load("wave1_alberta.RData")

head(wave1_alberta, 10)

数据集预览(作者提供的图像)

数据集有三列:

  • 月份(媒体简报的月份)

  • 日期(媒体简报的确切日期),以及

  • 单词(媒体简报中使用的单词或词元)

描述性分析

现在,我们可以计算一些描述性统计数据,以更好地理解数据集的内容。我们将首先按月份(基于词频)找出前 5 个单词。

library("dplyr")

wave1_alberta %>%
  group_by(month) %>%
  count(word, sort = TRUE) %>%
  slice_head(n = 5) %>%
  as.data.frame()

按月份排序的前 5 个单词(作者提供的图像)

输出显示,在这 4 个月的媒体简报中,“health”、“continue”和“test”等词语被频繁使用。我们还可以扩展我们的列表,查看最常用的 10 个单词,并以可视化方式呈现结果:

library("tidytext")
library("ggplot2")

wave1_alberta %>%
  # Group by month
  group_by(month) %>%
  count(word, sort = TRUE) %>%
  # Find the top 10 words
  slice_head(n = 10) %>%
  ungroup() %>%
  # Order the words by their frequency within each month
  mutate(word = reorder_within(word, n, month)) %>%
  # Create a bar graph
  ggplot(aes(x = n, y = word, fill = month)) +
  geom_col() +
  scale_y_reordered() +
  facet_wrap(~ month, scales = "free_y") +
  labs(x = "Frequency", y = NULL) +
  theme(legend.position = "none",
        axis.text.x = element_text(size = 11),
        axis.text.y = element_text(size = 11),
        strip.background = element_blank(),
        strip.text = element_text(colour = "black", face = "bold", size = 13))

基于频率的最常用单词(作者提供的图像)

由于一些词汇在四个月内都是通用的,上面的图表可能无法直接展示每个月特有的重要词汇。为了找出这些重要的词汇,我们可以使用词频-逆文档频率(TF-IDF)技术——这是一种在自然语言处理(NLP)中广泛使用的技术,用于衡量一个词在文档中相对于一组文档的重要性(有关 TF-IDF 的详细信息,请查看我之前的博客文章)。在我们的例子中,我们将每个月的媒体简报视为一个文档,并计算文档中每个词汇的 TF-IDF。下面的 R 代码第一部分通过计算所有词汇的 TF-IDF 并选择每个月内 TF-IDF 值最高的词汇,创建了一个新的数据集,wave1_tf_idf。接下来,我们使用该数据集创建一个条形图,显示每个月特有的常见词汇。

# Calculate TF-IDF for the words for each month
wave1_tf_idf <- wave1_alberta %>%
  count(month, word, sort = TRUE) %>%
  bind_tf_idf(word, month, n) %>%
  arrange(month, -tf_idf) %>%
  group_by(month) %>%
  top_n(10) %>%
  ungroup

# Visualize the results
wave1_tf_idf %>%
  mutate(word = reorder_within(word, tf_idf, month)) %>%
  ggplot(aes(word, tf_idf, fill = month)) +
  geom_col(show.legend = FALSE) + 
  facet_wrap(~ month, scales = "free", ncol = 2) +
  scale_x_reordered() +
  coord_flip() +
  theme(strip.background = element_blank(),
        strip.text = element_text(colour = "black", face = "bold", size = 13),
        axis.text.x = element_text(size = 11),
        axis.text.y = element_text(size = 11)) +
  labs(x = NULL, y = "TF-IDF")

基于 TF-IDF 的最常见词汇(图片来自作者)

这些结果提供了更多的信息,因为图中显示的词汇反映了每个月讨论的独特主题。例如,在 2020 年 3 月,媒体简报主要讨论了限制旅行、从人群密集的会议返回以及邮轮上的 COVID-19 病例。到了 2020 年 6 月,媒体简报的重点转向了口罩要求、人们抗议与疫情相关的限制等等。

在我们切换回情感分析之前,让我们看看另一个描述性变量:每个媒体简报的长度。这将帮助我们了解媒体简报随着时间的推移是变得更长还是更短。

wave1_alberta %>%
  # Save "day" as a separate variable
  mutate(day = substr(date, 9, 10)) %>%
  group_by(month, day) %>%
  # Count the number of words
  summarize(n = n()) %>%
  ggplot(aes(day, n, color = month, shape = month, group = month)) +
  geom_point(size = 2) + 
  geom_line() + 
  labs(x = "Days", y = "Number of Words") +
  theme(legend.position = "none", 
        axis.text.x = element_text(angle = 90, size = 11),
        strip.background = element_blank(),
        strip.text = element_text(colour = "black", face = "bold", size = 11),
        axis.text.y = element_text(size = 11)) +
  ylim(0, 800) +
  facet_wrap(~ month, scales = "free_x")

每日媒体简报中的词汇数(图片来自作者)

上面的图表显示了媒体简报的长度随时间变化较大。特别是在 3 月和 5 月,媒体简报的长度波动较大(即,极长或极短的简报),而在 6 月,媒体简报的日常长度较为一致。

使用 tidytext 进行情感分析

在对数据集进行描述性分析后,我们准备开始进行情感分析。在第一部分中,我们将使用tidytext包来执行情感分析并计算情感得分。我们首先将词典导入 R 中,然后将它们与我们的数据集合并。使用 Bing 词典时,我们需要计算正面和负面词汇的差异,以产生情感得分(即,情感 = 正面词汇的数量 — 负面词汇的数量)。

# From the three lexicons, Bing is already available in the tidytext page
# for AFINN and NRC, install the textdata package by uncommenting the next line
# install.packages("textdata")
get_sentiments("bing")
get_sentiments("afinn") 
get_sentiments("nrc")

# We will need the spread function from tidyr
library("tidyr")

# Sentiment scores with bing (based on frequency)
wave1_alberta %>%
  mutate(day = substr(date, 9, 10)) %>%
  group_by(month, day) %>%
  inner_join(get_sentiments("bing")) %>%
  count(month, day, sentiment) %>%
  spread(sentiment, n) %>%
  mutate(sentiment = positive - negative) %>%
  ggplot(aes(day, sentiment, fill = month)) +
  geom_col(show.legend = FALSE) +
  labs(x = "Days", y = "Sentiment Score") +
  ylim(-50, 50) + 
  theme(legend.position = "none", axis.text.x = element_text(angle = 90)) +
  facet_wrap(~ month, ncol = 2, scales = "free_x") +
  theme(strip.background = element_blank(),
        strip.text = element_text(colour = "black", face = "bold", size = 11),
        axis.text.x = element_text(size = 11),
        axis.text.y = element_text(size = 11))

基于 Bing 词典的情感得分(图片来自作者)

上图显示,媒体简报中的情感通常是负面的,这并不令人惊讶,因为这些简报主要讲述了有多少人去世、住院率、潜在爆发等问题。在某些日期(例如 2020 年 3 月 24 日和 2020 年 5 月 4 日),媒体简报的情感尤为负面。

接下来,我们将使用 AFINN 词典。与将词语标记为积极或消极的 Bing 不同,AFINN 为每个词语分配一个数值权重。权重的符号表示情感的极性(即积极或消极),而数值则表示情感的强度。现在,让我们看看这些加权值是否会产生不同的情感得分。

wave1_alberta %>%
  mutate(day = substr(date, 9, 10)) %>%
  group_by(month, day) %>%
  inner_join(get_sentiments("afinn")) %>%
  group_by(month, day) %>%
  summarize(sentiment = sum(value),
            type = ifelse(sentiment >= 0, "positive", "negative")) %>%
  ggplot(aes(day, sentiment, fill = type)) +
  geom_col(show.legend = FALSE) +
  labs(x = "Days", y = "Sentiment Score") +
  ylim(-100, 100) + 
  facet_wrap(~ month, ncol = 2, scales = "free_x") +
  theme(legend.position = "none", 
        strip.background = element_blank(),
        strip.text = element_text(colour = "black", face = "bold", size = 11),
        axis.text.x = element_text(size = 11, angle = 90),
        axis.text.y = element_text(size = 11))

基于 AFINN 词典的情感得分(图片来源:作者)

基于 AFINN 词典的结果似乎大不相同!一旦我们考虑到词语的“权重”,大多数媒体简报的情感被认为是积极的(见绿色条形图),尽管仍然有一些日子存在负面情感(见红色条形图)。到目前为止,我们做的两项分析结果差异很大,原因有二。首先,正如我之前提到的,Bing 词典专注于词语的极性,而忽略了词语的强度(“不喜欢”和“仇恨”被认为是强度相同的消极词语)。与 Bing 词典不同,AFINN 词典考虑了词语的强度,这影响了情感得分的计算。其次,Bing 词典(6786 个词语)比 AFINN 词典(2477 个词语)要大得多。因此,媒体简报中的一些标记可能被包含在 Bing 词典中,但不在 AFINN 词典中。忽略这些标记可能影响了结果。

我们接下来将尝试使用 tidytext 包中的最终词典是 NRC。正如我之前提到的,这个词典基于 Plutchik 的心理进化理论,对标记的词语进行分类,主要依据基本情感,如愤怒、恐惧和预期。我们将统计与每种情感相关的单词或标记数量,并可视化结果。

wave1_alberta %>%
  mutate(day = substr(date, 9, 10)) %>%
  group_by(month, day) %>%
  inner_join(get_sentiments("nrc")) %>%
  count(month, day, sentiment) %>%
  group_by(month, sentiment) %>%
  summarize(n_total = sum(n)) %>%
  ggplot(aes(n_total, sentiment, fill = sentiment)) +
  geom_col(show.legend = FALSE) +
  labs(x = "Frequency", y = "") +
  xlim(0, 2000) + 
  facet_wrap(~ month, ncol = 2, scales = "free_x") +
  theme(strip.background = element_blank(),
        strip.text = element_text(colour = "black", face = "bold", size = 11),
        axis.text.x = element_text(size = 11),
        axis.text.y = element_text(size = 11))

基于 NRC 词典的情感得分(图片来源:作者)

图表显示每月的媒体简报大多是积极的。Hinshaw 博士使用了与“信任”、“预期”和“恐惧”相关的词语。总体而言,这些情感的模式似乎随着时间的推移保持非常相似,表明媒体简报在传递情感的类型和强度上具有一致性。

使用 sentimentr 进行情感分析

另一个基于词汇的情感分析包是sentimentrRinker, 2021)。与tidytext包不同,这个包考虑了情感转移因素(例如,否定词),这些因素可以通过一个词轻松地翻转句子的情感极性。例如,句子“I am not unhappy”实际上是正面的,但如果我们逐字分析,句子可能因为“not”和“unhappy”这两个词而看起来有负面情感。类似地,“I hardly like this book”是负面句子,但单独分析“hardly”和“like”这两个词时,可能会得到一个正面的情感评分。sentimentr包解决了情感检测中情感转移因素的限制(有关sentimentr的更多详细信息,请参见包作者 Tyler Rinker 的 Github 页面:github.com/trinker/sentimentr)。

为了利用sentimentr包,我们需要媒体简报中的实际句子,而不是单独的词汇。因此,我不得不创建一个未分词的数据集版本,该版本可通过此处下载。我们将首先将这个数据集导入 R,使用get_sentences()函数获取每个媒体简报的单独句子,然后通过sentiment_by()按天和月计算情感评分。

library("sentimentr")
library("magrittr")

load("wave1_alberta_sentence.RData")

# Calculate sentiment scores by day and month
wave1_sentimentr <- wave1_alberta_sentence %>%
  mutate(day = substr(date, 9, 10)) %>%
  get_sentences() %$%
  sentiment_by(text, list(month, day))

# View the dataset
head(wave1_sentimentr, 10)

数据集预览(作者提供的图片)

在我们创建的数据集中,“ave_sentiment”是 3 月、4 月、5 月和 6 月每一天的平均情感评分(即有媒体简报的日子)。利用这个数据集,我们可以可视化情感评分。

wave1_sentimentr %>%
  group_by(month, day) %>%
  ggplot(aes(day, ave_sentiment, fill = ave_sentiment)) +
  scale_fill_gradient(low="red", high="blue") + 
  geom_col(show.legend = FALSE) +
  labs(x = "Days", y = "Sentiment Score") +
  ylim(-0.1, 0.3) +
  facet_wrap(~ month, ncol = 2, scales = "free_x") +
  theme(legend.position = "none", 
        strip.background = element_blank(),
        strip.text = element_text(colour = "black", face = "bold", size = 11),
        axis.text.x = element_text(size = 11, angle = 90),
        axis.text.y = element_text(size = 11))

基于情感的情感评分(作者提供的图片)

在上面的图中,蓝色条形表示高度正面的情感评分,而红色条形则表示相对较低的情感评分。sentimentr生成的情感评分模式与 AFINN 词汇表得出的模式非常相似。值得注意的是,这个分析是基于原始的媒体简报,而不仅仅是单独的词汇,情感评分的计算考虑了情感转移因素。sentimentr和 AFINN 在情感模式上的一致性并不令人意外。两种方法都采用了类似的加权系统和机制,考虑了词汇强度。这种一致性增强了我们对通过 AFINN 得出的初步结论的信心,验证了我们使用sentiment进行分析的可靠性和一致性。

结论

总之,基于词典的情感分析在 R 语言中提供了一种强大的工具,用于揭示文本数据中的情感细微差别。在这篇文章中,我们探讨了基于词典的情感分析的基本概念,并提供了使用 R 实现这一方法的实践示范。通过利用如sentimentrtidytext等包,我们展示了如何将情感分析无缝地集成到数据分析工作流中。当你踏上情感分析的旅程时,请记住,从这项技术中获得的洞察远远超越了文本的表面。它们为公众舆论、消费者情感等提供了宝贵的视角。我鼓励你深入探讨基于词典的情感分析,尝试这里呈现的示例,解锁你自己数据中等待发现的丰富洞察。祝你分析愉快!

参考文献

[1] Bulut, O., & Poth, C. N. (2022). 快速评估沟通一致性:COVID-19 疫情期间公共卫生简报的情感分析。AIMS 公共卫生9(2),293–306。 doi.org/10.3934/publichealth.2022020

[2] Poth, C. N., Bulut, O., Aquilina, A. M., & Otto, S. J. G. (2021). 使用数据挖掘进行快速复杂案例研究描述:以 COVID-19 疫情初期的公共卫生简报为例。混合方法研究杂志15(3),348–373。 doi.org/10.1177/15586898211013925

[3] Hu, M., & Liu, B. (2004). 挖掘和总结客户评价。第十届 ACM SIGKDD 国际知识发现与数据挖掘大会论文集,168–177。

[4] Mohammad, S. M., & Turney, P. D. (2013). 众包构建词语–情感联想词典。计算智能29(3),436–465。

[5] Plutchik, R. (1980). 情感的通用心理进化理论。收录于情感理论(第 3–33 页)。Elsevier。

逐行复现 GPT-2:第一部分

原文:towardsdatascience.com/line-by-line-lets-reproduce-gpt-2-section-1-b26684f98492?source=collection_archive---------8-----------------------#2024-07-23

本文将逐行分析 Andrej Karpathy 的“让我们复现 GPT-2(124M)”第一部分的代码。

Matthew GuntonTowards Data Science Matthew Gunton

·发布于 Towards Data Science ·21 分钟阅读·2024 年 7 月 23 日

--

作者提供的图像 — SDXL

Andrej Karpathy 是人工智能(AI)领域的领军人物之一。他是 OpenAI 的创始成员之一,曾领导 Tesla 的 AI 团队,并继续活跃在 AI 社区的前沿。他最近发布了一段精彩的4 小时视频,讲解如何从零开始构建一个高质量的大型语言模型(LLM)

在那个视频中,我们逐一讲解了训练大型语言模型(LLM)的所有主要部分,从编写架构代码到加速训练时间,再到调整超参数以获得更好的结果。这里包含了大量的知识,因此我希望通过逐行解析 Karpathy 创建的代码,进一步展开讲解其工作原理。本文将是我系列文章的一部分,内容将涵盖 Karpathy 视频的每个部分。

在第一部分中,我们专注于实现 GPT-2 的架构。尽管 GPT-2 在 2018 年由 OpenAI 开源,但它是用 TensorFlow 编写的,这比 PyTorch 更难调试。因此,我们将使用更常用的工具重新构建 GPT-2。仅凭我们今天要创建的代码,你就能创建自己的大型语言模型(LLM)!

让我们开始吧!

高级词汇

在我们开始之前,先来统一一下术语。虽然可能与其他来源的命名有所冲突,但我会尽量在这些博客文章中保持一致。

块大小 — 告诉我们 Transformer 可以处理输入长度中多少个位置。一旦超过这个限制,性能会下降,因为你必须进行循环(你可以在我的《长 RoPE 博客》中了解如何在不从头训练新模型的情况下扩展这一点)。

词汇表大小 — 告诉我们模型能够理解和使用多少个独特的标记。通常,研究人员发现,较大的词汇表大小使模型能够更精确地使用语言,并捕捉到更多细微的回应。

— 我们神经网络中的隐藏层的一部分。具体来说,这里指的是我们在下面的灰色框中重复进行计算的次数:

我们模型中的一层来自《Attention is All You Need》

嵌入 — 我们传递给模型的数据的向量表示。

多头注意力 — 我们不是只运行一次注意力,而是运行 n 次,然后将所有结果连接起来,得到最终结果。

让我们进入代码吧!

GPT 类及其参数

@dataclass
class GPTConfig:
    block_size : int = 1024
    vocab_size : int = 50257
    n_layer : int = 12
    n_head : int = 12
    n_embd : int = 768

首先,我们在 GPTConfig 类中设置了 5 个超参数。block_sizen_layern_head一样,看起来有些随意。换句话说,这些值是根据研究人员所观察到的最佳性能经验选择的。此外,我们为n_embd选择了 786,这是 GPT-2 论文中选定的值,我们决定进行模拟。

然而,vocab_size是基于我们将使用的tiktoken GPT-2 分词器设置的。GPT-2 分词器是通过使用字节对编码算法创建的(在这里阅读更多)。它从初始的词汇表(在我们的案例中为 256)开始,然后遍历训练数据,根据新词汇在训练集中的出现频率创建新的词汇。它不断重复这一过程,直到达到限制(在我们的案例中为 50,000)。最后,我们为内部使用预留了词汇(在我们的案例中为结束标记字符)。将这些值相加,我们得到 50,257。

class GPT(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.config = config

    # ...

设置好我们的配置后,我们创建了一个 GPT 类,它是 torch nn.Module类的一个实例。这个类是所有 PyTorch 神经网络的基类,因此通过使用这个类,我们可以访问 PyTorch 为这些类型的模型提供的所有优化。每个nn.Module都有一个forward函数,定义了模型在前向传播过程中发生的事情(稍后会详细讲解)。

我们首先运行基类中的超构造函数,然后将transformer对象创建为ModuleDict。这样做是因为它允许我们像操作对象一样索引transformer,这在我们想从 HuggingFace 加载权重时以及调试并快速浏览模型时都会非常有用。

class GPT(nn.Module):
    def __init__(self, config):
        # ...

        self.transformer = nn.ModuleDict(dict(
            wte = nn.Embedding(config.vocab_size, config.n_embd),
            wpe = nn.Embedding(config.block_size, config.n_embd),
            h = nn.ModuleList([Block(config) for _ in range(config.n_layer)]),
            ln_f = nn.LayerNorm(config.n_embd)
        )) 

我们的transformer在这里有四个主要部分要加载:标记嵌入的权重(wte)、位置编码的权重(wpe)、隐藏层(h)和层归一化(ln_f)。这个设置大致遵循了《Attention is All You Need》中 Transformer 架构的解码器部分(输出嵌入 ~ wte,位置编码 ~ wte,隐藏层 ~ h)。一个关键的区别是,我们的架构中在所有隐藏层完成后增加了一个额外的归一化层ln_f

解码器架构部分,见《Attention is All You Need》

wtewpe都是嵌入,因此我们自然使用nn.Embedding类来表示它们。我们的隐藏层是 Transformer 中大部分逻辑所在的地方,所以我稍后会详细讲解。现在只需注意,我们正在创建一个Block对象的循环,以便拥有n.layer个它们。最后,我们使用内建的nn.LayerNorm来处理ln_f,它将根据下面的公式对我们的输出进行归一化(其中 x 和 y 分别是输入和输出,E[x]是均值,γ和β是可学习的权重)。

PyTorch 中层归一化的公式

class GPT(nn.Module):
    def __init__(self, config):
        # ...
        self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)

        # weight sharing scheme
        self.transformer.wte.weight = self.lm_head.weight

        # initalize weights
        self.apply(self._init_weights)

接下来,我们设置网络的最后一层线性层,用于生成模型的 logits。在这里,我们将从模型的嵌入维度(768)映射到模型的词汇表大小(50,257)。这里的想法是,我们将隐藏状态扩展到映射到我们的词汇表,以便我们的解码器头可以使用每个词汇上的值来确定下一个标记应该是什么。

最后,在我们的构造函数中,我们进行了一个有趣的优化,其中我们告诉模型将分词器的权重与线性层的权重设为相同。这样做的原因是,我们希望线性层和分词器对标记有相同的理解(如果两个标记在输入模型时相似,那么这两个标记在模型输出时也应该是相似的)。最后,我们初始化模型的权重,以便可以开始训练。

class GPT(nn.Module):
# ...
    def forward(self, idx, targets=None):
        B, T = idx.size() 
        assert T <= self.config.block_size, f"maximum sequence length breached"
        pos = torch.arange(0, T, dtype=torch.long, device=idx.device)
        pos_emb = self.transformer.wpe(pos)
        tok_emb = self.transformer.wte(idx)

        x = tok_emb + pos_emb # hidden broadcast

        for block in self.transformer.h:
            x = block(x)
        x = self.transformer.ln_f(x)
        logits = self.lm_head(x)
        loss = None
        if targets is not None:
            loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
        return logits, loss

我们的前向函数是我们详细描述模型在前向传播过程中行为的地方。我们首先验证我们的序列长度是否不大于我们配置的最大值(block_size)。一旦这点成立,我们创建一个值为 0 到 T-1 的张量(例如,如果 T=4,我们将得到 tensor([0, 1, 2, 3]),并通过位置嵌入权重运行它们。完成之后,我们将输入张量通过标记嵌入权重。

我们将token和位置嵌入结合成x,需要广播操作来将它们合并。由于tok_embpos_emb大(在我们的示例中是 50257 对 1024),因此x的维度将与tok_emb相同。x现在是我们的隐藏状态,我们将在隐藏层中通过 for 循环传递它。我们小心地在每次通过 Block 后更新x

接下来,我们通过 LayerNormalization ln_fx进行归一化,然后进行线性投影以获取预测下一个 token 所需的 logits。如果我们正在训练模型(通过targets参数来指示),我们将计算我们刚刚生成的 logits 与存储在targets变量中的真实标签之间的交叉熵。我们通过cross_entropy损失函数来实现这一点。为了正确执行,我们需要通过.view()logitstarget转换为正确的形状。当我们通过-1 传递时,我们让 pytorch 推断出正确的大小。

这个类还有一个函数是初始化函数,不过我们稍后会讲到初始化逻辑。现在,让我们深入了解 Block 逻辑,它将帮助我们实现多头注意力和 MLP。

Block 类

class Block(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.ln_1 = nn.LayerNorm(config.n_embd)
        self.attn = CausalSelfAttention(config)
        self.ln_2 = nn.LayerNorm(config.n_embd)
        self.mlp = MLP(config)
# ...

Block 被实例化为nn.Module,因此我们还在开始时调用了父类构造函数进行优化。接下来,我们按照《Attention is All You Need》论文中的计算设置相同的操作——2 个层归一化、一个注意力计算和通过 MLPs 的前馈层。

《Attention is All You Need》中的一个隐藏层

class Block(nn.Module):
# ...
    def forward(self, x):
        x = x + self.attn(self.ln_1(x))
        x = x + self.mlp(self.ln_2(x))
        return x

然后我们定义了forward函数,PyTorch 将在每次模型前向传递时调用它。请注意,这里我们做了与《Attention is All You Need》不同的事情。我们设置了层归一化,分别发生在注意力和前馈层之前。这是 GPT-2 论文中的一个重要见解,您可以看到,像这样做一些小的改变,能够带来很大的不同。注意,原始张量的加法仍然保留在相应的位置。这两个加法将在我们设置权重初始化函数时变得非常重要。

这个类是一个很好的抽象,它允许我们替换注意力机制的实现或选择另一种类型的前馈函数,而不需要大规模重构代码。

CausalSelfAttention 类

class CausalSelfAttention(nn.Module):
    def __init__(self, config):
        super().__init__()
        assert config.n_embd % config.n_head == 0
        self.c_attn = nn.Linear(config.n_embd, 3*config.n_embd)
        self.c_proj = nn.Linear(config.n_embd, config.n_embd)
        self.c_proj.NANOGPT_SCALE_INIT = 1
        self.n_head = config.n_head
        self.n_embd = config.n_embd
        self.register_buffer('bias', torch.tril(torch.ones(config.block_size, config.block_size))
                                    .view(1,1, config.block_size, config.block_size))
# ...

注意力是我们模型的一个重要部分,因此这里自然有很多配置项。我们使用断言语句作为调试工具,确保我们传递的配置维度是兼容的。接着,我们创建了一些辅助函数,帮助我们在进行自注意力时使用。首先,我们有c_attnc_proj,它们是线性投影,将我们的隐藏状态转换为注意力计算所需的新维度。c_proj.NANOGPT_SCALE_INIT是我们在这里和 MLP 中设置的一个标志,帮助我们稍后的权重初始化(事实上,这个名字可以取任何名字)。

最后,我们告诉 torch 创建一个在训练过程中不会更新的缓冲区,称为bias。偏置将是一个尺寸为block_size x block_size的下三角矩阵,接着我们将其转换为尺寸为 1 x 1 x block_size x block_size的 4D 张量。1 x 1 的维度是为了能够在单个通道中批量计算这些数据。这个缓冲区将用于在我们的多头注意力中应用掩码。

class CausalSelfAttention(nn.Module):
# ...
    def forward(self, x):
        B, T, C = x.size() # batch size, sequence length, channels
        qkv = self.c_attn(x)
        q, k, v = qkv.split(self.n_embd, dim=2)
        # transpose is done for efficiency optimization
        k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
        q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
        v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)

        att = (q @ k.transpose(-2,-1)) * (1.0 / math.sqrt(k.size(-1)))
        att = att.masked_fill(self.bias[:, :, :T, :T] == 0, float("-inf"))
        att = F.softmax(att, dim=-1)
        y = att @ v
        y = y.transpose(1,2).contiguous().view(B, T, C)

        y = self.c_proj(y)
        return y

接下来是注意力的实现,重点是使其在 torch 中具有高性能。逐行分析,我们首先找到输入张量 x 中的批量大小、序列长度和通道数。接着我们会调用之前的c_attn,将隐藏状态投影到所需的维度。然后,我们将结果分割成 3 个形状为(B, T, C)的张量(具体来说,一个用于查询,一个用于键,一个用于值)。

然后,我们调整 q、k 和 v 的维度,以便能够高效地进行多头注意力。通过将维度从(B, T, C)更改为(B, T, self.n_head, C // self.n_head),我们将数据划分开来,让每个头(head)获得自己的独特数据进行操作。我们转置视图,以便将 T 变为第三维,将self.n_head变为第二维,从而更方便地将头部拼接起来。

来自“Attention is All You Need”的注意力公式

现在我们有了值,可以开始计算了。我们对查询(query)和键(key)进行矩阵乘法(确保将键转置,使其处于正确的方向),然后除以 k 的大小的平方根。完成此计算后,我们再应用来自寄存器的偏置,以确保未来的标记(tokens)的注意力数据不会影响当前的标记(因此我们只对时间和通道维度中大于 T 的标记应用掩码)。完成后,我们应用 softmax,仅通过某些信息进行传递。

一旦掩码被应用,我们将值乘以 v,然后将我们的值转置回(B,T,self.n_head,C // self.n_head)的设置。我们调用.contiguous()以确保在内存中所有数据都按顺序排列,最后将张量转换回(B,T,C)维度,这样就完成了在此步骤中拼接注意力头的操作。

最后,我们使用线性投影c_proj将其转换回隐藏状态的原始维度。

MLP 类

class MLP(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.c_fc = nn.Linear(config.n_embd, 4 * config.n_embd)
        self.gelu = nn.GELU(approximate="tanh")
        self.c_proj = nn.Linear(4 * config.n_embd, config.n_embd)
        self.c_proj.NANOGPT_SCALE_INIT = 1
# ...

和之前所有的类一样,MLP 继承自nn.Module。我们首先设置一些辅助函数——特别是c_fcc_proj线性投影层,分别将我们的嵌入扩展到原来的 4 倍大小,再缩回原来大小。接下来,我们使用 GELU。Karpathy 特别指出,近似参数这里只是为了与 GPT-2 论文中的结果尽可能接近。虽然当时,GELU 的近似是必要的,但现在我们不再需要近似——我们可以精确计算。

class MLP(nn.Module):
# ...
    def forward(self, x):
        x = self.c_fc(x)
        x = self.gelu(x)
        x = self.c_proj(x)
        return x

我们的前向传播过程相对简单。我们对输入张量调用每个函数并返回最终结果。

Hugging Face 连接代码

因为 GPT-2 是开源的,所以它可以在 Hugging Face 上找到。虽然我们的目标是训练自己的模型,但能够将我们的结果与 OpenAI 在训练中得到的结果进行对比是很有意义的。为了实现这一点,我们有以下函数来提取权重并将它们填充到我们的GPT类中。

这段代码还允许我们重用这些代码,从 Hugging Face 拉取基础模型并进行微调(经过一些修改,因为现在它仅对 gpt-2 进行了优化)。

class GPT(nn.Module):
# ...
    @classmethod
    def from_pretrained(cls, model_type):
        """Loads pretrained GPT-2 model weights from huggingface"""
        assert model_type in {'gpt2', 'gpt2-medium', 'gpt2-large', 'gpt2-xl'}
        from transformers import GPT2LMHeadModel
        print("loading weights from pretrained gpt: %s" % model_type)

        # n_layer, n_head and n_embd are determined from model_type
        config_args = {
            'gpt2':         dict(n_layer=12, n_head=12, n_embd=768),  # 124M params
            'gpt2-medium':  dict(n_layer=24, n_head=16, n_embd=1024), # 350M params
            'gpt2-large':   dict(n_layer=36, n_head=20, n_embd=1280), # 774M params
            'gpt2-xl':      dict(n_layer=48, n_head=25, n_embd=1600), # 1558M params
        }[model_type]
        config_args['vocab_size'] = 50257 # always 50257 for GPT model checkpoints
        config_args['block_size'] = 1024 # always 1024 for GPT model checkpoints
        # create a from-scratch initialized minGPT model
        config = GPTConfig(**config_args)
        model = GPT(config)
        sd = model.state_dict()
        sd_keys = sd.keys()
        sd_keys = [k for k in sd_keys if not k.endswith('.attn.bias')] # discard this mask / buffer, not a param
# ...

从顶部开始,我们引入 HuggingFace 的transformers库,并设置在不同版本的 GPT-2 模型之间变化的超参数。由于vocab_sizeblock_size不变,您可以看到我们将它们硬编码了进去。然后,我们将这些变量传递给之前的GPTConfig类,然后实例化模型对象(GPT)。最后,我们从模型中删除所有以.attn.bias结尾的键,因为这些不是权重,而是我们之前为帮助注意力功能设置的寄存器。

class GPT(nn.Module):
# ...
    @classmethod
    def from_pretrained(cls, model_type):
# ...
        model_hf = GPT2LMHeadModel.from_pretrained(model_type)
        sd_hf = model_hf.state_dict()

        # copy while ensuring all of the parameters are aligned and match in names and shapes
        sd_keys_hf = sd_hf.keys()
        sd_keys_hf = [k for k in sd_keys_hf if not k.endswith('.attn.masked_bias')] # ignore these, just a buffer
        sd_keys_hf = [k for k in sd_keys_hf if not k.endswith('.attn.bias')] # same, just the mask (buffer)
        transposed = ['attn.c_attn.weight', 'attn.c_proj.weight', 'mlp.c_fc.weight', 'mlp.c_proj.weight']
        # basically the openai checkpoints use a "Conv1D" module, but we only want to use a vanilla Linear
        # this means that we have to transpose these weights when we import them
        assert len(sd_keys_hf) == len(sd_keys), f"mismatched keys: {len(sd_keys_hf)} != {len(sd_keys)}"

接下来,我们从 HuggingFace 类GPT2LMHeadModel加载模型。我们从这个模型中提取键,并同样忽略attn.masked_biasattn.bias键。然后我们使用断言来确保 Hugging Face 模型中的键数量与我们模型中的键数量相同。

class GPT(nn.Module):
# ...
    @classmethod
    def from_pretrained(cls, model_type):
# ...
        for k in sd_keys_hf:
            if any(k.endswith(w) for w in transposed):
                # special treatment for the Conv1D weights we need to transpose
                assert sd_hf[k].shape[::-1] == sd[k].shape
                with torch.no_grad():
                    sd[k].copy_(sd_hf[k].t())
            else:
                # vanilla copy over the other parameters
                assert sd_hf[k].shape == sd[k].shape
                with torch.no_grad():
                    sd[k].copy_(sd_hf[k])

        return model

为了完善这个函数,我们遍历 Hugging Face 模型中的每个键,并将其权重添加到我们模型中对应的键上。有些键需要进行处理,以便适应我们正在使用的数据结构。我们使用函数.t()来转置 hugging face 矩阵,使其符合我们需要的维度。对于其余的键,我们直接复制过去。你会注意到我们使用了torch.no_grad()。这告诉 torch 它不需要缓存值用于模型的反向传播,这是另一个优化措施,使得运行速度更快。

生成我们的第一次预测(采样循环)

使用我们现在的类,我们可以运行模型并让它给出输出的 token(只需确保如果你是按顺序跟随的,记得注释掉 GPT 构造函数中的 _init_weights 调用)。下面的代码展示了我们如何做到这一点。

device = "cpu"
if torch.cuda.is_available():
    device = "cuda"
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
    device = "mps"
print(f"device {device}")

torch.manual_seed(1337)

model = GPT(GPTConfig())
model.eval()
model.to(device)

我们首先确定可以访问的设备。Cuda 是 NVIDIA 的平台,可以进行非常快速的 GPU 计算,因此如果我们有访问使用 CUDA 的芯片,我们将使用它们。如果我们没有 CUDA 访问权限,但使用的是 Apple Silicon,那么我们将使用它。最后,如果两者都没有,我们就退回到 CPU(这将是最慢的,但每台计算机都有 CPU,所以我们知道仍然可以在上面训练)。

然后,我们使用默认配置实例化模型,并将模型置于'eval'模式——(这会做很多事情,比如禁用 dropout,但从宏观上看,它确保我们的模型在推理过程中更加一致)。设置完成后,我们将模型移到我们的设备上。注意,如果我们想使用 HuggingFace 的预训练权重而不是我们的训练权重,我们需要修改倒数第三行,使其变为:model = GPT.from_pretrained(‘gpt2’)

import tiktoken
enc = tiktoken.get_encoding('gpt2')
tokens = enc.encode("Hello, I'm a language model,")
tokens = torch.tensor(tokens, dtype=torch.long)
tokens = tokens.unsqueeze(0).repeat(num_return_sequences, 1)
x = tokens.to(device)

我们现在使用gpt2编码方式引入tiktoken并让它对我们的提示进行分词。我们将这些 token 放入一个 tensor 中,然后在下面一行中将其转换为批次。unsqueeze()会为 tensor 添加一个新的尺寸为 1 的第一维,repeat会在第一维重复整个 tensor num_return_sequences次,并在第二维重复一次。我们所做的是将数据格式化为模型预期的批处理结构。具体来说,我们现在匹配了(B, T)格式:num_return_sequences x 提示的编码长度。一旦我们将输入 tensor 传入模型的开始位置,我们的wtewpe就会生成 C 维度。

while x.size(1) < max_length:
    with torch.no_grad():
        logits, _ = model(x)
        logits = logits[:, -1, :]
        probs = F.softmax(logits, dim=-1)
        topk_probs, topk_indices = torch.topk(probs, 50, dim=-1)
        ix = torch.multinomial(topk_probs, 1)
        xcol = torch.gather(topk_indices, -1, ix)
        x = torch.cat((x, xcol), dim=1)

现在它们已经准备好,我们将它们发送到设备上并开始我们的采样循环。这个循环将仅仅是前向传播,因此我们将其包装在torch.no_grad中,以防它为任何反向传播缓存值。我们的 logits 的形状为(batch_size, seq_len, vocab_size) — (B,T,C),其中 C 是在模型前向传播后得到的。

我们只需要序列中的最后一个项来预测下一个 token,因此我们提取[:, -1, :]。然后我们将这些 logits 通过softmax处理,得到 token 的概率。取前 50 个概率最大的 token,我们随机选择一个索引,从中选出作为我们的预测 token。然后我们获取关于该 token 的信息并将其添加到我们的张量x中。通过将xcol拼接到x上,我们为根据刚刚预测的内容进入下一个 token 做好了准备。这就是我们实现自回归的方式。

for i in range(num_return_sequences):
    tokens = x[i, :max_length].tolist()
    decoded = enc.decode(tokens)
    print(f">> {decoded}")

采样循环完成后,我们可以遍历每个选定的 token 并对其进行解码,展示给用户响应。我们从批次中的第i个获取数据,并解码以得到下一个 token。

如果你在我们初始模型上运行采样循环,你会发现输出结果差强人意。这是因为我们还没有训练任何权重。接下来的几节将展示如何开始对模型进行初步训练。

DataLoaderLite

所有训练都需要高质量的数据。对于 Karpathy 的视频,他喜欢使用公共领域的莎士比亚文本(可以在这里找到)。

class DataLoaderLite:
    def __init__(self, B, T):
        self.B = B
        self.T = T

        with open('shakespeare.txt', "r") as f:
            text = f.read()

        enc = tiktoken.get_encoding('gpt2')
        tokens = enc.encode(text)
        self.tokens = torch.tensor(tokens)
        print(f"1 epoch = {len(self.tokens) // B * T} batches")

        self.current_position = 0

我们首先通过简单地打开文件并读取文本来开始。此数据源仅包含 ASCII 字符,因此我们不需要担心任何意外的二进制字符。我们使用tiktoken获取正文的编码,然后将这些 tokens 转换为张量。接着,我们创建一个名为current_position的变量,它将告诉我们当前正在从 token 张量的哪个位置进行训练(通常初始化为开始位置)。注意,这个类没有继承自nn.Module,主要是因为我们在这里不需要forward函数。就像在采样循环的提示部分一样,我们的 DataLoaderLite 类只需要生成形状为(B, T)的张量。

class DataLoaderLite:
# ...
    def next_batch(self):
        B, T = self.B, self.T
        buf = self.tokens[self.current_position: self.current_position+(B*T + 1)]
        x = (buf[:-1]).view(B, T)
        y = (buf[1:]).view(B,T)

        self.current_position += B * T
        if self.current_position + (B*T+1) > len(self.tokens):
            self.current_position = 0
        return x,y

在上面的代码中,我们定义了next_batch函数来帮助训练。为了加速程序运行,我们喜欢将计算分批执行。我们使用 B 和 T 字段来确定我们训练的批量大小(B)和序列长度(T)。使用这些变量,我们创建一个缓冲区来保存我们将用来训练的 tokens,将维度设置为 B 行 T 列。注意,我们从current_position读取到current_position + (B*T + 1),其中+1 是为了确保我们有所有的真实值,适用于我们B*T的批次。

然后,我们设置我们的模型输入(x)和期望输出(y)。x是除了最后一个字符外的整个缓冲区,而y是除了第一个字符外的整个缓冲区。基本的想法是,给定 token 缓冲区中的第一个值,我们期望从模型中返回 token 缓冲区中的第二个 token。

最后,我们更新current_position并返回xy

权重初始化

由于我们处理的是概率,我们希望为权重选择初始值,以便更少的训练轮次就能得到正确的结果。我们的_init_weights函数帮助我们实现这一点,它通过用零或正态分布来初始化权重。

class GPT(nn.Module):
# ...
    def _init_weights(self, module):
        # layer norm is by default set to what we want, no need to adjust it
        if isinstance(module, nn.Linear):
            std = 0.02
            if hasattr(module, "NANOGPT_SCALE_INIT"):
                std *= (2 * self.config.n_layer) ** -0.5 # 2 * for 2 additions (attention & mlp)
            torch.nn.init.normal_(module.weight, mean=0.0, std=std)
            # reasonable values are set based off a certain equation
            if module.bias is not None:
                torch.nn.init.zeros_(module.bias)
        elif isinstance(module, nn.Embedding):
            torch.nn.init.normal_(module.weight, mean=0.0, std=0.02 )

如果你之前记得,我们将GPT类的每个字段都传递给了_init_weights,所以我们正在处理nn.Module。我们使用 Xavier 方法初始化权重,这意味着我们将采样分布的标准差设置为1 / sqrt(hidden_layers)。你会注意到,在代码中,我们经常使用硬编码的 0.02 作为标准差。尽管这看起来是任意的,但从下面的表格可以看到,由于 GPT-2 使用的隐藏维度都大致为 0.02,这就是一个很好的近似值。

浏览代码时,我们首先检查我们操作的模块是nn.Module的哪种子类。

如果该模块是线性模块,我们会检查它是否是我们从MLPCasualSelfAttention类中的投影之一(通过检查它是否设置了NANO_GPT_INIT标志)。如果是,那么我们 0.02 的近似值将不起作用,因为这些模块中的隐藏层数在增加(这是我们在Block类中添加张量的结果)。因此,GPT-2 论文使用了一个缩放函数来解决这个问题:1/sqrt(2 * self.config.n_layer)2*的原因是我们的Block有两个地方在添加张量。

如果线性模块中有偏置,我们将首先将其初始化为零。

如果我们有一个Embedding模块(如 Token 或位置编码部分),我们将使用标准差为 0.02 的相同正态分布来初始化它。

如果你记得,我们的模型中还有另一种子类:nn.LayerNorm。这个类已经通过正态分布进行了初始化,因此我们决定不需要做任何更改。

训练循环

现在我们已经设置好了训练的基本要素,让我们快速组建一个训练循环来训练我们的模型。

device = "cpu"
if torch.cuda.is_available():
    device = "cuda"
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
    device = "mps"
print(f"device {device}")

num_return_sequences = 5
max_length = 30

torch.manual_seed(1337)

train_loader = DataLoaderLite(B=4, T=32)

model = GPT(GPTConfig())
model.to(device)

你可以看到,我们重复进行设备计算以获得最佳性能。然后,我们设置数据加载器,使用批量大小为 4 和序列长度为 32(这个设置是任意的,尽管 2 的幂在内存效率上最佳)。

optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
for i in range(50):
    x, y = train_loader.next_batch()
    x, y = x.to(device), y.to(device)
    optimizer.zero_grad() #have to start with a zero gradient
    logits, loss = model(x, y)
    loss.backward() #adds to the gradient (+=, which is why they must start as 0)
    optimizer.step()
    print(f"loss {loss.item()}, step {i}")

现在我们有了优化器,它将帮助我们训练模型。优化器是一个 PyTorch 类,它接受应训练的参数(在我们的情况下是从GPT类传入的参数),以及学习率,这是训练过程中的超参数,决定了我们调整参数的速度——较高的学习率意味着每次运行后权重的变化更为剧烈。我们根据 Karpathy 的推荐选择了我们的值。

然后我们使用 50 步训练来训练模型。我们首先获取训练批次并将其移到我们的设备上。我们将优化器的梯度设置为零(在 pytorch 中,梯度是累加的,所以如果不将其清零,我们将从上一批次携带信息)。我们计算模型的 logits 和损失,然后进行反向传播,以找出新的权重模型应该是什么。最后,我们运行optimizer.step()来更新所有模型参数。

合理性检查

若要查看上述所有代码的执行情况,您可以查看我的 Google Colab,其中我将所有内容结合并在 NVIDIA T4 GPU 上运行。运行我们的训练循环时,我们看到损失从大约~11 开始。为了验证这一点,我们预计在开始时预测正确的标记的概率为(1/vocab_size)。通过一个简化的损失函数-ln,我们得到大约 10.88,这正是我们开始的地方!

图片由作者提供

结束语

感谢你读到最后!

我尽力在这篇博客文章中包含了尽可能多的细节,但自然也有一些内容我不得不省略。如果你喜欢这篇博客文章或者发现有任何你认为应该修改/扩展的地方,请告诉我!

现在是构建的激动人心时刻!

[1] Karpathy, A.,《让我们重现 GPT-2(124M)》(2024),YouTube

[2] Radford, A. 等人,《语言模型是无监督多任务学习者》(2018),《带代码的论文》

[3] Vaswani, A. 等人,"Attention Is All You Need"(2017),arXiv

逐行分析,让我们重现 GPT-2:第二部分 — 硬件优化

原文:towardsdatascience.com/line-by-line-lets-reproduce-gpt-2-section-2-hardware-optimization-86e71c91d9bb?source=collection_archive---------15-----------------------#2024-07-31

本文将逐行分析 Andrej Karpathy 在《让我们重现 GPT-2(124M)》第二部分中的硬件优化内容。

Matthew GuntonTowards Data Science Matthew Gunton

·发布于 Towards Data Science ·阅读时间:11 分钟·2024 年 7 月 31 日

--

图片来源 — SDXL

快速回顾一下,在第一部分中,我们逐行分析了 Karpathy 编写的代码,目的是原始地训练 GPT-2。现在我们已经有了我们的设置,Karpathy 向我们展示了如何在我们的 NVIDIA GPU 上加速模型训练!虽然我们知道训练一个好的模型需要花费大量时间,但通过优化每次运行,我们可以节省数天甚至数周的训练时间。这自然为我们提供了更多的迭代次数来改善模型。在本文的最后,你将看到如何通过使用 Ampere 系列的 Nvidia GPU 来大幅加速训练(提高 10 倍)。

为了写这篇博客文章,我在 Google Colab 提供的免费 NVIDIA T4 GPU 和 Lambda Labs 的 NVIDIA A100 40GB SXM4 GPU 上进行了优化。Karpathy 提到的大多数优化方法专为 A100 或更强大的显卡设计,但在较弱的 GPU 上仍然可以获得一些性能提升。

让我们开始吧!

代码计时

首先,我们希望创建一种方式来查看我们的优化效果。为此,我们将在训练循环中添加以下代码:

for i in range(50):
    t0 = time.time() # start timer
    x, y = train_loader.next_batch()
    x, y = x.to(device), y.to(device)
    optimizer.zero_grad() 
    logits, loss = model(x, y)
    loss.backward() 
    optimizer.step()
    torch.cuda.synchronize() # synchronize with GPU
    t1 = time.time() # end timer
    dt = (t1-t0)*1000 # milliseconds difference
    print(f"loss {loss.item()}, step {i}, dt {dt:.2f}ms")

我们首先捕获循环开始时的时间,但在捕获结束时间之前,我们运行 torch.cuda.synchronize()。默认情况下,我们只关注 CPU 停止的时间。因为我们已经将大部分主要计算移至 GPU,所以我们需要确保定时器在此处考虑到 GPU 停止计算的时间。同步操作将使 CPU 等待,直到 GPU 完成其工作队列,从而为我们提供循环完成时的准确时间。一旦我们有了准确的时间,自然就能计算出开始时间和结束时间之间的差异。

批量大小

我们还希望确保每轮通过尽可能多的数据。我们实现这一目标的方式是设置批量大小。在我们的 DataLoaderLite 类中,我们可以调整 2 个参数(B 和 T),以便在不超出限制的情况下,使用 GPU 中尽可能多的内存。

使用 A100 GPU,你可以按照 Karpathy 的示例,其中我们将 T 设置为最大 block_size 的 1024,并将 B 设置为 16,因为它是一个“漂亮”的数字(容易被 2 的幂次方整除),并且是我们可以在内存中容纳的最大“漂亮”数字。

train_loader = DataLoaderLite(B=16, T=1024)

如果你尝试输入一个太大的值,你会在终端看到 CUDA 提示 OutOfMemoryError 错误。我发现对于 T4 GPU,最佳的值是 B = 4 和 T = 1024(当在 Google Colab 中尝试不同的 B 值时,请注意,你可能需要重启会话,以确保不会得到错误的 OutOfMemoryError)。

在 A100 和 T4 上运行时,我得到了以下图表,显示了开始训练的时间(平均大约在 T4 上是 1100ms,A100 上是 1040ms)

作者提供的图像 — A100 训练未进行优化

作者提供的图像 — T4 训练未进行优化

浮点优化

现在我们将重点关注在模型内部表示数据时所做的更改。

如果你查看我们在第一节代码中权重的 dtype,你会看到我们默认使用 32 位浮点数(fp32)。Fp32 表示我们使用 32 位表示数字,遵循下面的 IEEE 浮动点标准:

作者提供的图像 — IEEE 浮动点 32(FP32)表示

正如 Karpathy 在视频中所说,我们通过实验证明,fp32 并不是训练高质量模型所必需的——我们可以使用更少的数据来表示每个权重,并且仍然得到高质量的输出。加速计算的一种方法是使用 NVIDIA 的 TensorCore 指令。它将通过将操作数转换为下面所示的 Tensor Float 32(TF32)格式来处理矩阵乘法:

作者提供的图像 — Tensor Float 32(TF32)

作者提供的图像 — TF32 数据流通过 Tensor Core 经过优化

从代码角度来看,我们所有的变量(输入、输出)都是 FP32 格式,但 NVIDIA GPU 会将中间矩阵转换为 TF32 格式以加速。根据 NVIDIA 的说法,这能带来 8 倍的加速相对于 FFMA 指令。要在 PyTorch 中启用 TF32,我们只需要添加以下代码行(high = TF32, highest = FP32, medium = BF16(稍后会详细介绍)):

torch.set_float32_matmul_precision("high")

TensorCore 是 NVIDIA 独有的功能,您只能在 A100 GPU 或更高版本上运行 TF32,因此一些开发者使用浮点 16(FP16)作为训练方式。这种表示法的问题在于,FP16 能够表示的数据范围比 FP32 要小,导致无法表示训练所需的相同数据范围。虽然通过梯度扩展可以绕过这一点,但这会增加更多的计算,因此最终会陷入“前进一步,退两步”的困境。

作者提供的图片 — IEEE 浮点 16(FP16)表示法

但在他的演示视频中,Karpathy 使用的数据优化方法是脑浮点(BF16)。在这种表示法中,我们与 FP32 有相同数量的指数位,因此可以表示相同的范围,但尾数位较少。这意味着尽管我们有较少的位数,表示数字的精度较低。通过经验来看,这并没有导致性能大幅下降,所以这是一个我们愿意接受的折衷。要在 NVIDIA 芯片上使用此格式,您需要具备 A100。

作者提供的图片 — 脑浮点 16(BF16)

使用 PyTorch 时,我们不需要大幅修改代码就可以使用新的数据类型。文档建议我们只在模型的前向传递和损失计算过程中使用这些数据类型。由于我们的代码在 1 行中完成了这两个操作,我们可以按如下方式修改代码:

for i in range(50):
    t0 = time.time() 
    x, y = train_loader.next_batch()
    x, y = x.to(device), y.to(device)
    optimizer.zero_grad() 
    with torch.autocast(device_type=device, dtype=torch.bfloat16): # bf16 change
        logits, loss = model(x, y)
    loss.backward() 
    optimizer.step()
    torch.cuda.synchronize() 
    t1 = time.time()
    dt = (t1-t0)*1000 
    print(f"loss {loss.item()}, step {i}, dt {dt:.2f}ms")
    loss_arr.append(loss.item())

就这样,我们的代码现在已经开始使用 BF16 运行。

在我们的 A100 上运行时,现在平均每步大约需要 330 毫秒!我们已经将运行时间减少了大约 70%,而且我们才刚刚开始!

作者提供的图片 — 数据类型优化后的 A100 训练

Torch Compile

我们可以通过利用 PyTorch Compile 功能进一步改善训练时间。这样可以在不调整代码的情况下大幅提升性能。

从高层次来看,每个计算机程序都是以二进制执行的。因为大多数人觉得用二进制编程很困难,所以我们创建了更高层次的语言,让我们能以更容易理解的方式编写代码。当我们编译这些语言时,它们会被转换回我们实际运行的二进制代码。有时,在这种转换过程中,我们可以找到更快的方式来完成相同的计算——比如重用某个变量,甚至干脆跳过某些操作。

# ...
model = GPT(GPTConfig(vocab_size=50304))
model.to(device)
model = torch.compile(model) # new line here
# ...

这将我们带到了机器学习和 PyTorch。Python 是一种高级语言,但我们仍然在用它做计算密集型的计算。当我们运行torch compile时,我们花费更多时间在编译代码上,但由于我们为了找到这些优化所做的额外工作,我们最终看到我们的运行时间(在这里指的是训练)变得更快。

Karpathy 给出了一个 PyTorch 如何改善计算的示例。我们的 GELU 激活函数可以写成如下:

class TanhGELU(nn.Module):
    def forward(self, input):
        return 0.5 * input * (1.0 + torch.tanh(math.sqrt(2.0/math.pi) * (input + 0.044715 * torch.pow(input, 3.0))))

对于你在上面的函数中看到的每个计算,我们都必须在 GPU 中调度一个内核。这意味着,当我们开始进行三次方计算时,我们从高带宽内存(HBM)中提取输入到 GPU 核心,并进行计算。然后,我们将结果写回 HBM,然后开始下一次计算,并重复整个过程。自然,这样的顺序导致我们在等待内存传输时浪费了大量时间。

PyTorch 编译让我们能够看到像这样的低效,并且更加小心地控制何时启动新的内核,从而实现显著的加速。这被称为内核融合。

在这个话题上,我想指出一个优秀的开源项目叫做 Luminal,它将这个思想进一步扩展了。Luminal 是一个独立的框架,你可以在其中编写训练/推理代码。通过使用这个框架,你可以访问它的编译器,该编译器通过只考虑有限数量的计算,能够为你找到更多优化。如果你喜欢通过编译快速 GPU 代码来提升运行时间的思路,可以看看这个项目。

当我们现在运行上面的代码时,我们会看到每一步大约需要 145 毫秒(比之前减少了 50%,比原始版本减少了约 86%)。我们为此付出了第一轮大约 40,000 毫秒的代价!由于大多数训练序列的步骤数远超过 50,这个权衡是我们愿意做出的。

作者提供的图片 — A100 训练在 Torch Compile 优化后的运行结果

Flash Attention

我们做的另一个优化是使用 Flash Attention(查看论文)。对于我们来说,代码本身的改动非常简单,但背后的思路值得深入探讨。

y = F.scaled_dot_product_attention(q, k, v, is_causal=True)

类似于我们如何将 TanhGELU 类压缩成尽可能少的内核,我们也将相同的思维方式应用于注意力机制。在他们的论文《“FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness”》中,作者展示了如何通过融合内核实现 7.6 倍的加速。虽然理论上 torch compile 应该能够找到类似的优化,但在实践中我们尚未看到它找到这一点。

图 1 来自《“FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness”

这篇论文值得深入研究,但简要概述一下,FlashAttention 被设计为对 I/O 友好,从而避免了不必要(且耗时)的内存调用。通过减少这些调用,它们可以显著加速计算。

实施了这个优化后,我们发现现在每一步的平均时间约为 104 毫秒。

作者提供的图像 — Flash Attention 优化后的 A100 训练

词汇大小变化

最后,我们可以检查所有硬编码的数字,评估它们有多“合适”。当我们这样做时,我们发现词汇大小不能被许多 2 的幂整除,因此在 GPU 内存加载时会更加耗时。我们通过将词汇大小从 50,257 调整为下一个“合适”的数字 50,304 来解决这个问题。这个数字之所以“合适”,是因为它可以被 2、4、8、16、32、64 和 128 整除。

model = GPT(GPTConfig(vocab_size=50304))

你可能还记得上一篇博客中提到的,我们的词汇大小并不是一个任意的值 — 它是由我们使用的分词器决定的。因此,问题就来了,当我们任意地增加词汇大小时,会发生什么?在训练过程中,模型会发现这些新的词汇从未出现过,因此它会开始将这些词汇的概率推向 0 —— 所以我们的性能是安全的。不过,这并不意味着没有权衡。通过加载从未使用过的词汇到内存中,我们浪费了时间。然而,从经验上看,我们可以看到加载“合适”的数字能大大弥补这一成本。

通过最后的优化,我们现在每一步的平均时间大约是 100 毫秒。

作者提供的图像 — 词汇大小优化后的 A100 训练

通过这一最终优化,我们发现我们的训练从最初开始已经提高了约 10 倍!

哪些优化在 T4 GPU 上有效?

如果你一直在关注但仅能使用消费者级 T4 GPU,你可能会想知道哪些优化是可以使用的。回顾一下,我们不能使用 BF16 表示法,但可以使用词汇大小变化、Flash Attention 和 torch compile。要查看这段代码的实际应用,查看我为 T4 优化的 Google Colab 笔记本

从下图中我们可以看到,尽管第一次运行 torch compile 确实需要很长时间,但接下来的几轮与未优化版本相比并没有显著提高(在 T4 上大约下降了 8%,而在 A100 上下降了 90%)。

作者提供的图像 — 在 T4 GPU 上优化运行

尽管如此,当 OpenAI 在训练 GPT-2 时,它运行的是比 T4 更先进的硬件。今天我们能够在 T4 上运行这个工作负载,表明硬件要求正在变得不那么苛刻,这有助于创造一个硬件不再成为机器学习工作障碍的未来。

结束语

通过优化我们的代码,我们看到了显著的速度提升,并且也了解了一些训练过程中主要的瓶颈。首先,数据类型对于速度至关重要,因为仅这一改变就大幅提升了速度。其次,我们发现硬件优化在加速计算中发挥着重要作用——因此,GPU 硬件真的是物有所值。最后,编译器优化在这里也起到了关键作用。

要查看我在 A100 上运行的代码,请查看这个 Gist。如果你有任何关于如何进一步优化硬件的建议,我很乐意在评论区看到它们!

现在是构建的激动人心的时刻!

[1] Karpathy, A., “让我们重现 GPT-2 (124M)” (2024), YouTube

[2] Dao, T. 等人 “FlashAttention: 快速且内存高效的精确注意力机制,具备 I/O 感知能力” (2022), arXiv

[3] Krashinsky, R. 等人 “NVIDIA 安培架构深度解析” (2020), NVIDIA 开发者

一行行地复现 GPT-2:第三部分 — 训练

原文:towardsdatascience.com/line-by-line-lets-reproduce-gpt-2-section-3-training-f2fef87880fc?source=collection_archive---------8-----------------------#2024-09-03

本文将逐行解析 Andrej Karpathy 的《一起来复现 GPT-2(124M)》第三部分中的代码。

Matthew GuntonTowards Data Science Matthew Gunton

·发布于 Towards Data Science ·17 分钟阅读·2024 年 9 月 3 日

--

图片来源 — SDXL

在第一篇博文中,我们编写了 Transformer 架构的代码。在第二篇博文中,我们优化了 NVIDIA 的交互。为了完成我们的系列教程,接下来我们将对 GPT-2 模型进行预训练。如果你还没有观看过,强烈推荐 观看 Andrej Karpathy 的《让我们复现 GPT-2(124M)》视频,以及下面我们系列中的两篇前文。

## 一行行地复现 GPT-2:第一部分

本文将逐行解析 Andrej Karpathy 的《一起来复现 GPT-2(124M)》第一部分中的代码。

towardsdatascience.com ## 一行行地复现 GPT-2:第二部分 — 硬件优化

本文将逐行解析 Andrej Karpathy 的《一起来复现 GPT-2》第二部分中的硬件优化内容。

towardsdatascience.com

不再废话,让我们开始吧!

在我们开始预训练之前,我们需要对代码进行一些修改,以提高性能并在训练过程中提高可视性。请注意,我们将...

线性代数 5:线性无关

原文:towardsdatascience.com/linear-algebra-5-linear-independence-d350759debee?source=collection_archive---------4-----------------------#2024-03-21

Ax = 0 和证明一组向量线性无关

tenzin migmar (t9nz)Towards Data Science tenzin migmar (t9nz)

·发表于 Towards Data Science ·阅读时间 6 分钟·2024 年 3 月 21 日

--

序言

欢迎回到我关于线性代数基础的系列的第五篇文章,线性代数是机器学习背后的基础数学。在上一篇 文章中,我讲解了矩阵方程 Ax = b。本篇文章将探讨线性无关这一重要概念,并讨论它与我们迄今为止学到的内容的关系。

本文最好与 David C. Lay、Steven R. Lay 和 Judi J. McDonald 合著的《线性代数及其应用》一书一起阅读。请将本系列视为一份辅助资源。

欢迎分享您的想法、问题和批评。

ℝⁿ 中的线性无关

之前,我们学习了矩阵乘法和形如 Ax = b 的矩阵方程。我们讲解了当 b 是矩阵 A 中一组向量(列)的线性组合时,Ax = b 有解 x

在线性代数中,有一个特殊的矩阵方程 Ax = 0,我们称之为齐次线性系统。Ax = 0 总是至少有一个解,其中 x = 0,这个解称为平凡解,因为很容易证明任何矩阵 A0 向量 x 相乘都会得到 0 向量。

我们真正关心的是学习矩阵方程Ax = 0是否仅有平凡解。如果Ax = 0只有平凡解x = 0,那么构成矩阵A列的向量集合是线性无关的。换句话说:v₁ + c₂v₂ + … + cₐvₐ = 0,其中 c₁, c₂, …, cₐ必须全为 0。另一种思考方式是,这个集合中的任何一个向量都不能表示为其他向量的线性组合。

另一方面,如果存在解使得x ≠ 0,那么该向量集合是线性相关的。由此可得,该集合中至少有一个向量可以表示为其他向量的线性组合:c₁v₁ + c₂v₂ + … + cₐvₐ = 0,其中不是所有的 c₁, c₂, …, cₐ都为 0。

一个简洁且直观的思考线性无关概念的方式是:你能否找到一组权重,将一组向量的线性组合压缩到原点?如果一组向量是线性无关的,那么唯一能应用到每个向量的权重是 0,才能使得线性组合等于零向量。如果这些向量是线性相关的,那么就存在至少一组非零权重,使得向量的线性组合等于零。

判断线性无关性

对于只有一个向量的集合,判断线性无关性是非常简单的。如果该向量是零向量,则它是线性相关的。这是因为任何非零权重乘以零向量都会得到零向量,因此对于*Ax = 0存在无数解。如果该向量不是零向量,则它是线性无关的,因为任何向量乘以零都会得到零向量。

如果一个集合包含两个向量,当且仅当其中一个向量是另一个向量的倍数时,向量集合是线性相关的。否则,它们是线性无关的。

对于包含超过两个向量的集合,需要进行更多的计算。令这些向量构成矩阵A的列,并对矩阵A进行行约简,得到简化行阶梯形矩阵。如果简化行阶梯形矩阵的每一列都有一个主元,则该向量集合是线性无关的。否则,该向量集合是线性相关的。为什么会这样呢?考虑将矩阵行约简为简化行阶梯形矩阵的过程。我们执行一系列基本的行变换,如将行乘以常数、交换行、将一行加到另一行,以便得到一个更简单形式的矩阵,从而使其基本属性变得清晰,同时解空间得以保持。

在线性独立的情况下,矩阵中每一列有主元,表明每个向量在至少一个部分的线性组合方程中起着主导作用。如果每个向量都独立地贡献于线性系统,那么没有任何一个向量可以表示为其他向量的线性组合,因此系统是线性独立的。相反,如果简化行阶梯形矩阵中有一列没有主元,意味着相应的变量(或向量)是依赖变量,可以通过其他向量来表示。换句话说,系统中存在冗余,表明向量之间存在线性依赖性。

总结这个概念的一种简洁方法是使用矩阵的秩。秩是矩阵中线性独立列的最大数目,因此可以推导出,秩等于简化行阶梯形矩阵中的主元个数。

如果矩阵的列数等于秩,那么矩阵是线性独立的。否则,矩阵是线性相关的。

使用 Numpy 进行线性独立性检测

尝试手工计算是更好理解线性独立性的有价值练习,但更实用的方法是利用 Numpy 库中内置的功能,既可以测试线性独立性,也可以推导给定矩阵的Ax = 0的解空间。

我们可以通过矩阵的秩来检查一个矩阵是否线性独立。如前所述,矩阵线性独立当且仅当矩阵的秩等于列数,因此我们的代码将围绕这个标准编写。

以下代码生成Ax = 0的向量解空间。

结论

线性独立性,虽然是线性代数的基础,但在机器学习应用中也起着基石作用。线性独立性在特征选择和降维技术中至关重要,例如主成分分析(PCA),它操作的是数据集中各特征之间的共线性或线性依赖性。

你将在机器学习中继续看到线性独立性的重要性!

摘要

  • 如果一个线性方程组可以写成Ax = 0的形式,则称该方程组为齐次的。

  • 线性独立的向量不能互相表示为线性组合(除非是所有系数都为零的平凡组合)。

  • 线性相关的向量是指集合中的至少一个向量可以表示为其他向量的线性组合。

  • Numpy 是一个用于处理数组的 Python 库,提供了出色的支持,可以检查一个矩阵是否线性独立,并且还可以解给定矩阵的 Ax = 0。

注意事项

除非另有说明,否则所有图片均由作者创作。

线性注意力就是你所需要的一切

原文:towardsdatascience.com/linear-attention-is-all-you-need-5fa9c845c1b5?source=collection_archive---------2-----------------------#2024-06-02

自注意力能以更低的成本实现?

Sam Maddrell-ManderTowards Data Science Sam Maddrell-Mander

·发布于 Towards Data Science ·9 分钟阅读·2024 年 6 月 2 日

--

图片由 Guillaume Jaillet 提供,来源于 Unsplash

“注意力在处理长序列时表现较差”

这是任何花费大量时间研究变换器和自注意力机制的人都听过一百次的内容。它绝对是正确的,我们都经历过,当你尝试增加模型的上下文大小时,一切突然停滞不前。但与此同时,似乎几乎每周都有一个新的前沿模型,创造了一个新的上下文长度纪录。(Gemini 的上下文长度达到了 2M 个标记!)

有很多复杂的方法,比如 RingAttention,它使得在大型分布式系统中训练极长的上下文长度成为可能,但今天我关注的是一个更简单的问题。

单靠线性注意力,我们能做到什么程度?

让我们来解析一下数学。

这将是一次快速浏览,但请耐心等待,我们将在深入分析结果之前,简要讨论一些关键点。

我们基本上可以用两个关键点总结传统的注意力机制:

  • 首先,典型的 softmax 注意力表达式是将查询矩阵和键矩阵相乘,进行归一化以保持稳定性,然后按行进行 softmax 运算,从而得到序列中每个元素之间的注意力分数。

  • 其次,时间复杂度主要由 N²的点积决定,而 softmax 内部的部分是限制因素。这就是我们计算注意力分数的地方。

传统形式可以表达为:

softmax 注意力机制的传统公式。

事实证明,如果我们请教一下数学家朋友,我们可以稍微换个角度思考这个问题。softmax 可以被看作是描述与其他 token 之间的概率分布的多种方式之一。我们可以使用任何我们喜欢的相似度度量(点积是最简单的之一),只要我们进行归一化,就没问题。

使用任何相似度函数的注意力的通用表达式。

说这就是注意力有点草率,因为实际上只有当相似度函数是查询和键的点积的指数(如下所示),并且我们在 softmax 中找到了这个时,它才是我们所知的注意力。但有趣的是,如果我们不使用这个表达式,而是尝试逼近它呢?

用两个特征图逼近自注意力中的相似度函数。

我们可以假设存在某个特征图“phi”,它能给出一个结果,几乎与取点积的指数相同。关键是,以这种方式写表达式允许我们改变矩阵乘法操作的顺序。

论文中,他们提出了指数线性单元(ELU)作为特征图,因为它具有一些有用的属性:

  1. 对于大于 0 的值,ELU(x) 给出线性结果,尽管与指数不同,但仍然保留了分数之间的相对顺序。

  2. 对于小于或等于 0 的值,指数项保持了函数的连续性,并确保梯度不会消失。

我们在这里不会花太多时间讨论这个问题,但这已经通过经验得到了验证,作为 softmax 函数的一个合理逼近。

这使我们能够改变操作的顺序。我们可以首先取特征图 K 和 V 的乘积,形成一个 KV 块,然后与 Q 相乘。平方积变成了在模型维度上进行而非序列长度上进行。

将这一切组合到线性注意力的表达式中,我们得到:

使用特征图来逼近 softmax 相似度分数的线性注意力。

我们只需要每个查询行计算括号内的项一次。

(如果你想深入了解因果遮掩是如何融入其中以及梯度是如何计算的,可以查看论文,或者关注未来的博客。)

线性注意力到底快多少?

数学上的情况很有力,但就个人而言,在看到一些基准测试之前,我总是有些怀疑。

我们先从代码片段入手,描述这些术语。softmax 注意力看起来非常熟悉,这里我们没有做任何复杂的操作。

class TraditionalAttention(nn.Module):
    def __init__(self, d_k):
        super(TraditionalAttention, self).__init__()
        self.d_k = d_k

    def forward(self, Q, K, V):
        Z = torch.sqrt(torch.tensor(self.d_k, device=Q.device, dtype=torch.float32))
        scores = torch.matmul(Q, K.transpose(-2, -1)) / Z
        attention_weights = F.softmax(scores, dim=-1)
        output = torch.matmul(attention_weights, V)
        return output

接着,对于线性注意力,我们首先获取查询、键和值矩阵,然后对查询和键应用 ELU(x)特征映射。接着使用 einsum 符号进行乘法运算。

class LinearAttention(nn.Module):
    def __init__(self):
        super(LinearAttention, self).__init__()
        self.eps = 1e-6

    def elu_feature_map(self, x):
        return F.elu(x) + 1

    def forward(self, Q, K, V):
        Q = self.elu_feature_map(Q)
        K = self.elu_feature_map(K)
        KV = torch.einsum("nsd,nsd->ns", K, V)
        # Compute the normalizer
        Z = 1/(torch.einsum("nld,nd->nl", Q, K.sum(dim=1))+self.eps)
        # Finally compute and return the new values
        V = torch.einsum("nld,ns,nl->nd", Q, KV, Z)
        return V.contiguous()

在代码中看到这些是很好,但它在实验中实际上意味着什么呢?我们在这里谈论的性能提升到底有多大?从二次瓶颈到线性瓶颈的速度提升可能很难直接感受到,因此我进行了以下实验。

我们将采取一个单独的注意力层,固定的 d_k 模型维度为 64,并基准测试 32 批次序列的前向传播时间。唯一需要变化的变量是序列长度,范围从 128 到 6000(参考 GPT-3 的上下文长度为 2048)。每次运行进行 100 次,以获得平均值和标准差,实验使用的是 Nvidia T4 GPU。

对于如此简单的实验,结果相当惊人。

基准测试:测量传统(softmax)注意力和线性注意力下,单个序列每次迭代的时间。每个序列长度在 100 次迭代中取平均,标准差也会被绘制出来。使用的序列长度范围是从 128 到 6000。比率图也显示出来,便于更容易地衡量性能提升。

结果表明,即使是在一个极小的玩具示例中,我们也能获得最多 60 倍的加速。

讨论

这里有几个显而易见的收获:

  1. 线性注意力的优势巨大——无论是在速度上,较高的吞吐量总是件好事;还是在处理长序列时的内存需求上。在低内存环境中,这可能是一个很大的优势。

  2. 比例图显示了一个令人惊讶的弯曲——这让我们怀疑这里发生了一些额外的底层优化,这意味着预期的比例并没有完全实现。所以我们需要对这个结果保持一定的怀疑态度。

完整性考虑,别误解为说“线性注意力对于小模型来说是 60 倍快”。实际上,前馈层通常占据了 Transformer 中大部分的参数,而编码/解码也是一个限制性大小的组件。但在这个严格定义的问题中,结果相当令人印象深刻!

计算复杂度

如果我们考虑每种方法的实际时间复杂度,我们可以展示这个差异的来源。

让我们分解传统 softmax 注意力的时间复杂度,第一个项给出了 QK 乘法的复杂度,这是 n²个分数,每个都是长度为 d_k 的点积。第二项描述了对注意力分数进行 softmax 操作的复杂度,也是 n²。第三项则将 n²矩阵与值向量进行点积运算。

如果我们简化假设查询、键和值矩阵具有相同的维度,那么我们得到最终项,其中主导项是 n²项。(前提是模型维度远小于序列长度。)

普通自注意力的 softmax 操作受序列长度的 n²项主导,通常模型的维度 d_k 远小于 n。

线性注意力讲述了一个不同的故事。同样,如果我们查看下面的时间复杂度表达式,我们将分析每一项。

第一项是将特征映射应用于 Q 和 K 矩阵的成本,第二项是 Q 和 V 矩阵的乘积,结果是一个(d_k, d_v)矩阵,而 K(QV)乘法在第三项中的复杂度相同。然后,最终输出再次假设模型维度对于不同的矩阵相同,最终的复杂度是线性与序列长度相关,并且与模型维度二次方成正比。

线性自注意力改变了这种局面,其在 n 上是线性的,而在模型维度上是二次方的(如果 dk 和 dv 的隐藏维度与我在这里所做的相同,便于简化)。因此,在 n >> dk 的情况下,复杂度明显较低。

因此,只要模型的维度小于序列长度,我们就能得到一个显著更快的模型。剩下的唯一问题是,它的近似效果究竟有多好?

没有免费的午餐——我们真的能训练一个模型吗?

够了,足够的实验,希望我们都确信线性注意力比传统注意力要快得多,接下来进行真正的测试。我们能否真正训练模型,并且它们在两种不同注意力机制下的表现相似?

我们在这里使用的模型非常小(如果有兴趣深入探讨如何设置一个简单的训练框架,我们可以在未来再看看这个问题),而且数据也很简单。我们将使用 Penn Treebank 数据集(通过torchtext公开提供),它包含了一些简短的文本片段,可以用来建模/测试小型语言模型。

我们能训练一个真正的模型来进行真正的预测吗

如果我们诚实一点,实际预测可能有点过于夸张,考虑到我们训练所用的参数数量和时间,实际上我真正想观察的是训练动态是否相似。我们将查看在一个简单语言建模数据集上进行自回归训练的损失曲线,如果它们遵循相同的形状,我们至少可以有一些信心,认为不同的机制给出了相似的结果。

数据的性质意味着输出质量很少能达到高标准,但它提供了我们期望的所有正确训练过程的特征。

让我们来看一下训练曲线。左图展示了传统注意力方法和线性注意力方法在训练和验证中的损失。我们可以看到,在 10 个周期内,这两种方法基本上是无法区分的。同样地,如果我们查看右图,传统 softmax 和线性注意力的损失也表现得完全相同,显示了相同的训练动态。

(左)线性和传统注意力的每个周期的训练和验证损失,(右)线性和传统注意力机制的训练损失曲线。

结论

显然,这远远不够全面,我们这里并不打算与 GPT 竞争,但我们可以对减少注意力机制的复杂性并且不丧失建模能力保持乐观。

请关注第二部分,那里会有更详细的对比。

除非另有说明,否则所有图像均由作者创建,训练数据来自公开的PennTreebank数据集,通过PyTorch torchtext数据集获取。更多细节可以在此处找到。

如果你想了解更多关于线性注意力的实现细节,我强烈建议你深入阅读原始论文(arxiv.org/abs/2006.16236)。

如果你喜欢这篇内容,请关注此账号或在 Twitter上找到我。

线性判别分析(LDA)

原文:towardsdatascience.com/linear-discriminant-analysis-lda-598d8e90f8b9?source=collection_archive---------3-----------------------#2024-10-12

发现 LDA 如何帮助识别关键数据特征

Ingo NowitzkyTowards Data Science Ingo Nowitzky

·发表于Towards Data Science ·阅读时间 12 分钟·2024 年 10 月 12 日

--

LDA 在人工智能和机器学习方法中的分类 | 作者提供的图片

**本文旨在探讨线性判别分析(LDA),重点介绍其核心思想、在代码中的数学实现以及来自制造业的实际例子。

希望你能参与进来。让我们开始吧!**

从事工业数据**工作的实践者会熟悉这种情况:数据集通常包含许多特征,而且通常不清楚哪些特征重要,哪些特征较不重要。在此上下文中,“重要”是一个相对的概念。通常,目标是区分数据集之间的差异,即对其进行分类。一个非常典型的任务是区分好的零件和坏的零件,并找出导致零件失败的原因(即特征)。

一种常用的方法是广为人知的主成分分析(PCA)。虽然 PCA 属于监督方法,但较少使用的 LDA 是一种有监督的方法,因此它从标记数据中学习。因此,LDA 特别适用于解释来自大数据集的故障模式。

1. LDA 的目标和原理

LDA 的目标是线性地组合数据的特征,使得数据集的标签能够彼此最佳分离,并且新特征的数量被减少到预定义的数量。在人工智能术语中,这通常被称为投影到低维空间

LDA 原理 | 图片修改自Raschka/Mirjalili, 2019

旁白:什么是维度,什么是维度降维?

维度与图形表示 | 图片由作者提供

维度指的是数据集中特征的数量。

仅凭一个测量(或特征),例如注塑机的工具温度,我们可以将其表示在数轴上。两个特征,比如温度和工具压力,仍然可以处理:我们可以轻松地在 x-y 图表上绘制数据。三个特征——温度、工具压力和注射压力——会变得更加复杂,但我们仍然可以在 3D x-y-z 图表上绘制数据。然而,当我们添加更多特征,例如粘度、电导率等时,复杂性会增加。

降维 | 图片由作者提供

在实际应用中,数据集通常包含数百甚至数千个特征。这带来了挑战,因为许多机器学习算法在数据集过大时表现较差。此外,所需的数据量随着维度的增加而指数增长,以达到统计显著性。这种现象被称为“维度灾难”。这些因素使得在数据科学过程中,提前确定哪些特征是相关的,并去除那些不重要的特征变得至关重要。

2. LDA 是如何工作的?

线性判别分析(LDA)的过程可以分为五个关键步骤。

步骤 1: 分别计算数据集中每个 k 类别的 d 维均值向量。

记住,LDA 是一种监督学习技术,这意味着我们可以利用已知的标签。在第一步中,我们计算所有属于特定类别 c 的样本的均值向量 mean_c。为此,我们按类别标签过滤特征矩阵,并计算每个 d 特征的均值。因此,我们得到 k 个均值向量(每个类别一个),每个均值向量的长度为 d(对应于 d 个特征)。

标签向量 Y 和特征矩阵 X | 图片由作者提供

类别 c 的均值向量

步骤 2: 计算散度矩阵(类间散度矩阵和类内散度矩阵)。

类内散度矩阵衡量的是同一类别样本之间的变异性。为了找到具有最佳可分性的子空间,我们旨在最小化此矩阵中的值。相比之下,类间散度矩阵衡量的是不同类别之间的变异性。为了获得最佳可分性,我们旨在最大化此矩阵中的值。

直观地讲,类内散度关注的是每个类别的紧凑度,而类间散度则考察不同类别之间的距离。

类内和类间散度矩阵 | 图片由作者提供

让我们从类内散布矩阵S_W开始。它是通过对每个类别的散布矩阵S_c求和得到的:

类内散布矩阵 S_W

类间散布矩阵S_B是通过类别均值mean_c与整个数据集的总体均值之间的差异得出的:

类间散布矩阵 S_B

其中mean指的是在所有样本上计算的均值向量,而不考虑它们的类别标签。

步骤 3: 计算S_WS_B比率的特征向量和特征值。

如前所述,为了实现最佳的类分离性,我们的目标是最大化S_B并最小化S_W。我们可以通过最大化比率S_B/S_W来实现这一目标。从线性代数的角度看,这个比率对应于散布矩阵S_W⁻¹ S_B,该矩阵在由具有最大特征值的特征向量张成的子空间中最大化。特征向量定义了这个子空间的方向,而特征值表示了变形的幅度。我们将选择与最大特征值相关联的m个特征向量。

特征向量和特征值的计算公式

特征向量张成的子空间 | 图片来源:作者

步骤 4: 按照特征值从大到小的顺序排序特征向量,并选择具有最大特征值的m个特征向量来构建d × m-维度的变换矩阵W

记住,我们的目标不仅是将数据投影到一个能增强类分离性的子空间中,还要进行降维。特征向量将定义我们新特征子空间的坐标轴。为了决定丢弃哪些特征向量以形成低维子空间,我们需要检查它们相应的特征值。简而言之,具有最小特征值的特征向量对类分离贡献最小,我们希望丢弃这些特征向量。通常的方法是按特征值从大到小排列,选择前m个特征向量。m是一个可以自由选择的参数,m越大,变换过程中丢失的信息就越少。

在按特征值从大到小排序特征对并选择前m对之后,下一步是构建d × m-维度的变换矩阵W。通过将m个选定的特征向量水平堆叠,得到矩阵W

变换矩阵 W

W的第一列表示与最大特征值对应的特征向量,第二列表示与第二大特征值对应的特征向量,依此类推。

步骤 5: 使用W将样本投影到新的子空间。

在最后一步,我们使用d × m-维度的变换矩阵W,该矩阵由前m个选定的特征向量组成,将样本投影到新的子空间:

变换后的特征矩阵 Z

其中 X 是初始的 n × d 维特征矩阵,表示我们的样本,而 Z 是新子空间中经过转换后的 n × m 维特征矩阵。这意味着,所选的特征向量作为“配方”用于将原始特征转换为新特征(线性判别量):具有最大特征值的特征向量提供了 LD1 的转换配方,具有第二大特征值的特征向量对应于 LD2,依此类推。

将 X 投影到线性判别量 LD 上

3. 从头开始实现线性判别分析(LDA)

为了展示理论和数学的实际应用,我们将从头开始使用 numpy 编写我们自己的 LDA。

import numpy as np

class LDA_fs:
    """
    Performs a Linear Discriminant Analysis (LDA)

    Methods
    =======
    fit_transform():
        Fits the model to the data X and Y, derives the transformation matrix W
    and projects the feature matrix X onto the m LDA axes
    """

    def __init__(self, m):
        """
        Parameters
        ==========
        m : int
            Number of LDA axes onto which the data will be projected

        Returns
        =======
        None
        """
        self.m = m

    def fit_transform(self, X, Y):
        """
        Parameters
        ==========
        X : array(n_samples, n_features)
            Feature matrix of the dataset
        Y = array(n_samples)
            Label vector of the dataset

        Returns
        =======
        X_transform : New feature matrix projected onto the m LDA axes

        """

        # Get number of features (columns)
        self.n_features = X.shape[1]
        # Get unique class labels
        class_labels = np.unique(Y)
        # Get the overall mean vector (independent of the class labels)
        mean_overall = np.mean(X, axis=0)  # Mean of each feature
        # Initialize both scatter matrices with zeros
        SW = np.zeros((self.n_features, self.n_features))  # Within scatter matrix
        SB = np.zeros((self.n_features, self.n_features))  # Between scatter matrix

        # Iterate over all classes and select the corresponding data
        for c in class_labels:
            # Filter X for class c
            X_c = X[Y == c]
            # Calculate the mean vector for class c
            mean_c = np.mean(X_c, axis=0)
            # Calculate within-class scatter for class c
            SW += (X_c - mean_c).T.dot((X_c - mean_c))
            # Number of samples in class c
            n_c = X_c.shape[0]
            # Difference between the overall mean and the mean of class c --> between-class scatter
            mean_diff = (mean_c - mean_overall).reshape(self.n_features, 1)
            SB += n_c * (mean_diff).dot(mean_diff.T)

        # Determine SW^-1 * SB
        A = np.linalg.inv(SW).dot(SB)
        # Get the eigenvalues and eigenvectors of (SW^-1 * SB)
        eigenvalues, eigenvectors = np.linalg.eig(A)
        # Keep only the real parts of eigenvalues and eigenvectors
        eigenvalues = np.real(eigenvalues)
        eigenvectors = np.real(eigenvectors.T)

        # Sort the eigenvalues descending (high to low)
        idxs = np.argsort(np.abs(eigenvalues))[::-1]
        self.eigenvalues = np.abs(eigenvalues[idxs])
        self.eigenvectors = eigenvectors[idxs]
        # Store the first m eigenvectors as transformation matrix W
        self.W = self.eigenvectors[0:self.m]

        # Transform the feature matrix X onto LD axes
        return np.dot(X, self.W.T)

4. 将 LDA 应用于工业数据集

为了看到 LDA 的实际应用,我们将其应用于生产环境中的典型任务。我们有来自一个简单制造流水线的数据,该流水线只有 7 个工作站。每个工作站发送一个数据点(是的,我知道,只有一个数据点是非常不现实的)。不幸的是,我们的生产线产生了大量的不良品,我们希望找出哪些工作站对这一现象负责。

首先,我们加载数据并初步查看。

import pandas as pd

# URL to Github repository
url = "https://raw.githubusercontent.com/IngoNowitzky/LDA_Medium/main/production_line_data.csv"

# Read csv to DataFrame
data = pd.read_csv(url)

# Print first 5 lines
data.head()

接下来,我们使用 Pandas 中的 .describe() 方法来研究数据的分布。

# Show average, min and max of numerical values
data.describe()

我们看到有 20,000 个数据点,测量范围从 -5 到 +150。由此,我们需要记住,稍后需要对数据集进行归一化:不同数值的量级,否则会对 LDA 产生负面影响。

我们有多少个良品和多少个不良品?

# Count the number of good and bad parts
label_counts = data['Label'].value_counts()

# Display the results
print("Number of Good and Bad Parts:")
print(label_counts)

我们有 19,031 个良品和 969 个不良品。数据集严重失衡是进一步分析的一个问题。因此,我们选择所有不良品和等数量的随机选择的良品进行后续处理。

# Select all bad parts
bad_parts = data[data['Label'] == 'Bad']

# Randomly select an equal number of good parts
good_parts = data[data['Label'] == 'Good'].sample(n=len(bad_parts), random_state=42)

# Combine both subsets to create a balanced dataset
balanced_data = pd.concat([bad_parts, good_parts])

# Shuffle the combined dataset
balanced_data = balanced_data.sample(frac=1, random_state=42).reset_index(drop=True)

# Display the number of good and bad parts in the balanced dataset
print("Number of Good and Bad Parts in the balanced dataset:")
print(balanced_data['Label'].value_counts())

现在,让我们将从头开始实现的 LDA 应用于平衡后的数据集。我们使用 sklearn 中的 StandardScaler 来对每个特征的测量值进行归一化,使其均值为 0,标准差为 1。我们选择一个线性判别轴 (m=1),将数据投影到该轴上。这有助于我们清楚地看到哪些特征在区分良品和不良品方面最为相关,并通过直方图可视化投影后的数据。

import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler

# Separate features and labels
X = balanced_data.drop(columns=['Label'])
y = balanced_data['Label']

# Normalize the features
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Perform LDA
lda = LDA_fs(m=1)  # Instanciate LDA object with 1 axis
X_lda = lda.fit_transform(X_scaled, y) # Fit the model and project the data

# Plot the LDA projection
plt.figure(figsize=(10, 6))
plt.hist(X_lda[y == 'Good'], bins=20, alpha=0.7, label='Good', color='green')
plt.hist(X_lda[y == 'Bad'], bins=20, alpha=0.7, label='Bad', color='red')
plt.title("LDA Projection of Good and Bad Parts")
plt.xlabel("LDA Component")
plt.ylabel("Frequency")
plt.legend()
plt.show()

# Examine feature contributions to the LDA component
feature_importance = pd.DataFrame({'Feature': X.columns, 'LDA Coefficient': lda.W[0]})
feature_importance = feature_importance.sort_values(by='LDA Coefficient', ascending=False)

# Display feature importance
print("Feature Contributions to LDA Component:")
print(feature_importance)

将特征矩阵投影到一个 LD(m=1)上

特征重要性 = 各站点在类分离中的贡献有多大?

直方图显示,我们能够很好地区分良品和不良品,只有少量重叠。这已经是一个积极的结果,表明我们的 LDA 成功了。

表格“LDA 组件的特征贡献”中的“LDA 系数”表示我们转换矩阵W的第一列(也是唯一一列,因为m=1)的特征向量。它们表示从各个工站的标准化测量值在 LDA 轴上的投影方向和大小。表中的数值按降序排列。我们需要同时从表格的顶部和底部读取,因为系数的绝对值表示每个工站在类别分离中的重要性,进而表示其对生产缺陷部件的贡献。符号表示较低或较高的测量值是否会增加缺陷部件的可能性。让我们仔细看看我们的例子:

最大的绝对值来自工站 4,其系数为-0.672。 这意味着工站 4 对部件故障的影响最强。由于负号,较高的正测量值会投影到负的线性判别轴(LD)。直方图显示,负的 LD 与良好(绿色)部件相关。相反,在该工站的低值和负值会增加部件故障的可能性

第二大绝对值来自工站 2,其系数为 0.557。 因此,这个工站是导致部件故障的第二大贡献者。正号表示较高的正测量值会投影到正的 LD。从直方图中我们知道,较高的正 LD 值与较高的故障概率相关。换句话说,工站 2 的高测量值会导致部件故障

第三高的系数来自工站 7,其值为-0.486。 这使得工站 7 成为部件故障的第三大贡献者。负号再次表明,在该工站的较高正值会导致负 LD(这对应于良好的部件)。相反,在该工站的低值和负值会导致部件故障

所有其他 LDA 系数的数量级都比上述三者小得多,因此,相关的工站对部件故障没有影响

我们的 LDA 分析结果正确吗?正如你可能已经猜到的,生产数据集是合成生成的。我将所有部件标记为有缺陷,其中工站 2 的测量值大于 0.5,工站 4 的值小于-2.5,工站 7 的值小于 3。事实证明,LDA 完全准确地命中了目标!

# Determine if a sample is a good or bad part based on the conditions
data['Label'] = np.where(
    (data['Station_2'] > 0.5) & (data['Station_4'] < -2.5) & (data['Station_7'] < 3),
    'Bad',
    'Good'
)

5. 结论

线性判别分析(LDA)不仅可以减少数据集的复杂性,还能突出驱动类别分离的关键特征,因此在识别生产系统中的故障原因时非常有效。它是一种直接而强大的方法,具有实际应用,并且在像scikit-learn这样的库中容易获取。

为了实现最佳效果,平衡数据集(确保每个类别中样本数量相似)并进行归一化(均值为 0,标准差为 1)是至关重要的。

下次当你处理包含类别标签和多个特征的大型数据集时,为什么不尝试一下 LDA 呢?

产品分析中的线性优化

原文:towardsdatascience.com/linear-optimisations-in-product-analytics-ace19e925677?source=collection_archive---------2-----------------------#2024-12-18

解决背包问题

Mariya MansurovaTowards Data Science Mariya Mansurova

·发表于 Towards Data Science ·阅读时间 11 分钟·2024 年 12 月 18 日

--

由 DALL-E 3 生成的图像

这可能令人惊讶,但在本文中,我想谈谈背包问题,这个经典的优化问题已经被研究了一个多世纪。根据维基百科,该问题的定义如下:

给定一组物品,每个物品都有一个重量和一个价值,确定应该包括哪些物品,以便总重量小于或等于给定的限制,同时总价值尽可能大。

虽然产品分析师可能不会亲自打包背包,但其背后的数学模型与我们许多任务高度相关。背包问题在产品分析中有许多现实世界的应用。以下是一些例子:

  • 营销活动: 营销团队在不同渠道和区域运营活动时,预算和能力有限。他们的目标是最大化某个 KPI,例如新用户数量或收入,同时遵守现有的约束条件。

  • 零售空间优化: 一家零售商在店铺中有有限的物理空间,他们希望通过优化产品摆放来最大化收入。

  • 产品发布优先级: 在发布新产品时,运营团队的能力可能有限,需要对特定市场进行优先排序。

这样的任务以及类似的任务非常常见,许多分析师会定期遇到它们。因此,在本文中,我将探讨解决此类问题的不同方法,涵盖从朴素的简单技巧到更先进的线性规划方法。

我选择这个话题的另一个原因是,线性规划是处方分析中最强大和最流行的工具之一——一种专注于为利益相关者提供可操作选项以做出明智决策的分析方法。因此,我认为这是任何分析师工具包中必备的技能。

案例

让我们直接进入将要探讨的案例。假设我们是一个营销团队的一部分,正在为下个月计划活动。我们的目标是最大化关键绩效指标(KPI),例如获取的用户数量和收入,同时在有限的营销预算内运营。

我们已估算了不同国家和渠道的各种营销活动的预期结果。以下是我们拥有的数据:

  • country — 我们可以进行一些促销活动的市场;

  • channel — 获取方式,例如社交网络或影响者活动;

  • users — 在促销活动一个月内预期获得的用户数量;

  • cs_contacts — 新用户产生的增量客户支持联系;

  • marketing_spending — 活动所需的投资;

  • revenue — 从已获取客户中产生的第一年 LTV(客户终身价值)。

请注意,数据集是合成的,随机生成的,因此不要尝试从中推断任何市场相关的见解。

首先,我计算了高级统计数据,以便查看这些数字。

让我们确定一组最佳的营销活动组合,在保持 3000 万美元营销预算的同时最大化收入。

暴力破解法

乍一看,这个问题可能显得简单:我们可以计算所有可能的营销活动组合并选择最佳的那一个。然而,这可能是一个具有挑战性的任务。

由于有 62 个数据段,每个数据段可以选择包含或排除,因此有 2⁶²种可能的组合。这大约是 4.6×10¹⁸种组合——一个天文数字。

为了更好地理解计算可行性,让我们考虑一个较小的 15 个数据段的子集,并估算一次迭代所需的时间。

import itertools
import pandas as pd
import tqdm

# reading data
df = pd.read_csv('marketing_campaign_estimations.csv', sep = '\t')
df['segment'] = df.country + ' - ' + df.channel

# calculating combinations
combinations = []
segments = list(df.segment.values)[:15]
print('number of segments: ', len(segments))

for num_items in range(len(segments) + 1):
  combinations.extend(
      itertools.combinations(segments, num_items)
  )
print('number of combinations: ', len(combinations))

tmp = []
for selected in tqdm.tqdm(combinations):
    tmp_df = df[df.segment.isin(selected)]
    tmp.append(
        {
        'selected_segments': ', '.join(selected),
        'users': tmp_df.users.sum(),
        'cs_contacts': tmp_df.cs_contacts.sum(),
        'marketing_spending': tmp_df.marketing_spending.sum(),
        'revenue': tmp_df.revenue.sum()
        }
    )

# number of segments:  15
# number of combinations:  32768

处理 15 个数据段大约花费了 4 秒钟,这使我们能够每秒处理约 7,000 次迭代。根据这个估算,让我们计算处理 62 个数据段所需的执行时间。

2**62 / 7000 / 3600 / 24 / 365
# 20 890 800.6

采用暴力破解法,回答我们问题大约需要 2090 万年——显然这不是一个可行的选项。

执行时间完全由数据段的数量决定。移除一个数据段可以将时间减少一半。考虑到这一点,让我们探讨合并数据段的可能方式。

像往常一样,较小的细分市场比大规模的更多,所以合并它们是一个合理的步骤。然而,值得注意的是,这种方法可能会降低准确性,因为多个细分市场被合并为一个。尽管如此,它仍然可以产生一个“足够好”的解决方案。

为了简化,我们将合并所有贡献少于 0.1%收入的细分市场。

df['share_of_revenue'] = df.revenue/df.revenue.sum() * 100
df['segment_group'] = list(map(
    lambda x, y: x if y >= 0.1 else 'other',
    df.segment,
    df.share_of_revenue
))

print(df[df.segment_group == 'other'].share_of_revenue.sum())
# 0.53
print(df.segment_group.nunique())
# 52

采用这种方法,我们将把十个细分市场合并为一个,占总收入的 0.53%(可能的误差范围)。剩余 52 个细分市场后,我们可以在仅 20.4 千年内获得解决方案。虽然这是一个显著的改进,但仍然不够充分。

你可以考虑其他针对特定任务量身定制的启发式方法。例如,如果你的约束是一个比率(例如,联系率 = 客户服务联系数 / 用户 ≤ 5%),你可以将所有符合约束的细分市场归为一组,因为最优解会包括所有这些细分市场。然而,在我们的案例中,我看不出有任何其他策略来减少细分市场的数量,因此暴力计算似乎不切实际。

也就是说,如果组合的数量相对较小,并且暴力计算可以在合理的时间内执行,那么它可以是一个理想的方法。它简单易开发,并且提供准确的结果。

天真方法:查看表现最好的细分市场

由于暴力计算不适合计算所有组合,我们考虑使用一种更简单的算法来解决这个问题。

一种可能的方法是专注于表现最好的细分市场。我们可以通过计算每花费一美元所获得的收入来评估细分市场的表现,然后根据这个比率对所有活动进行排序,选择符合营销预算的表现最佳的活动。让我们来实现这个方法。

df['revenue_per_spend'] = df.revenue / df.marketing_spending 
df = df.sort_values('revenue_per_spend', ascending = False)
df['spend_cumulative'] = df.marketing_spending.cumsum()
selected_df = df[df.spend_cumulative <= 30000000]
print(selected_df.shape[0])
# 48 
print(selected_df.revenue.sum()/1000000)
# 107.92

采用这种方法,我们选择了 48 个活动,并获得了 107.92 百万美元的收入。

不幸的是,尽管逻辑看起来合理,但它并不是最大化收入的最佳解决方案。让我们来看一个只有三个营销活动的简单例子。

使用选择最佳市场的方法,我们将选择法国,并获得 6800 万美元的收入。然而,如果选择另外两个市场,我们可以获得显著更好的结果——9750 万美元。关键点在于,我们的算法不仅优化了最大化收入,还优化了选择的细分市场数量。因此,这种方法将无法得到最佳结果,尤其是考虑到它无法应对多个约束条件。

线性规划

由于所有简单的方法都失败了,我们必须回到基础理论,探讨这个问题背后的理论。幸运的是,背包问题已经研究了许多年,我们可以应用优化技术,在几秒钟内解决它,而不是几年。

我们试图解决的问题是一个整数规划问题,实际上它是线性规划的一个子领域。

我们稍后会讨论这个方法,但首先,让我们对优化过程的关键概念达成一致。每个优化问题都由以下几个部分组成:

  • 决策变量:模型中可以调整的参数,通常代表我们希望做出的杠杆或决策。

  • 目标函数:我们旨在最大化或最小化的目标变量。无需多言,它必须依赖于决策变量。

  • 约束条件:对决策变量施加的条件,定义了它们的可能值。例如,确保团队不能工作负数小时。

牢记这些基本概念,我们可以将线性规划定义为满足以下条件的场景:

  • 目标函数是线性的。

  • 所有约束都是线性的。

  • 决策变量是实数值的。

整数规划与线性规划非常相似,唯一的区别是:一些或所有的决策变量必须是整数。虽然这看起来像是一个小的变化,但它显著影响了解决方法,需要比线性规划更复杂的方法。一种常见的技术是分支定界法。我们不会在这里深入探讨理论,但你总是可以在网上找到更详细的解释。

对于线性优化,我更喜欢广泛使用的 Python 包PuLP。不过,也有其他可用的选择,比如Python MIPPyomo。让我们通过 pip 安装 PuLP。

! pip install pulp

现在,是时候将我们的任务定义为一个数学优化问题了。具体步骤如下:

  • 定义决策变量的集合(我们可以调整的杠杆)。

  • 对齐目标函数(我们将优化的变量)。

  • 制定约束条件(在优化过程中必须满足的条件)。

让我们逐步进行。但首先,我们需要创建问题对象并设置目标——在我们的案例中是最大化。

from pulp import *
problem = LpProblem("Marketing_campaign", LpMaximize)

下一步是定义决策变量——在优化过程中我们可以改变的参数。我们的主要决策是是否开展市场营销活动。因此,我们可以将其建模为每个细分市场的二元变量集(0 或 1)。让我们通过 PuLP 库来实现。

segments = range(df.shape[0])  
selected = LpVariable.dicts("Selected", segments, cat="Binary")

之后,是时候对目标函数进行对齐了。如前所述,我们希望最大化收入。总收入将是所有选定细分市场的收入之和(其中decision_variable = 1)。因此,我们可以将此公式定义为每个细分市场的预期收入乘以决策二元变量的总和。

problem += lpSum(
  selected[i] * list(df['revenue'].values)[i] 
  for i in segments
)

最后一步是添加约束。让我们从一个简单的约束开始:我们的市场营销支出必须低于 3000 万美元。

problem += lpSum(
    selected[i] * df['marketing_spending'].values[i]
    for i in segments
) <= 30 * 10**6

提示:你可以打印problem来仔细检查目标函数和约束条件。

现在我们已经定义了所有内容,可以运行优化并分析结果。

problem.solve()

运行优化只需不到一秒钟,与暴力破解需要数千年相比,效率显著提升。

Result - Optimal solution found

Objective value:                110162662.21000001
Enumerated nodes:               4
Total iterations:               76
Time (CPU seconds):             0.02
Time (Wallclock seconds):       0.02

让我们将模型执行的结果——表示每个区段是否被选择的决策变量——保存到我们的数据框中。

df['selected'] = list(map(lambda x: x.value(), selected.values()))
print(df[df.selected == 1].revenue.sum()/10**6)
# 110.16

它就像魔法一样快速提供解决方案。此外,请注意,相比我们的直觉方法,我们达到了更高的收入:1.1016 亿美元对比 1.0792 亿美元。

我们已经用一个简单的例子测试了整数规划,该例子仅涉及一个约束,但我们可以进一步扩展。例如,我们可以为 CS 联系方式添加额外的约束,以确保我们的运营团队能够以健康的方式处理需求:

  • 额外的 CS 联系方式数量 ≤ 5000

  • 联系率(CS 联系方式/用户)≤ 0.042

# define the problem
problem_v2 = LpProblem("Marketing_campaign_v2", LpMaximize)

# decision variables
segments = range(df.shape[0]) 
selected = LpVariable.dicts("Selected", segments, cat="Binary")

# objective function
problem_v2 += lpSum(
  selected[i] * list(df['revenue'].values)[i] 
  for i in segments
)

# Constraints
problem_v2 += lpSum(
    selected[i] * df['marketing_spending'].values[i]
    for i in segments
) <= 30 * 10**6

problem_v2 += lpSum(
    selected[i] * df['cs_contacts'].values[i]
    for i in segments
) <= 5000

problem_v2 += lpSum(
    selected[i] * df['cs_contacts'].values[i]
    for i in segments
) <= 0.042 * lpSum(
    selected[i] * df['users'].values[i]
    for i in segments
)

# run the optimisation
problem_v2.solve()

代码非常简单,唯一有点复杂的部分是将比例约束转换为更简单的线性形式。

另一个可能的约束条件是限制选定选项的数量,例如,限制为 10 个。这个约束在规范性分析中非常有用,比如当你需要选择最具影响力的前 N 个焦点区域时。

# define the problem
problem_v3 = LpProblem("Marketing_campaign_v2", LpMaximize)

# decision variables
segments = range(df.shape[0]) 
selected = LpVariable.dicts("Selected", segments, cat="Binary")

# objective function
problem_v3 += lpSum(
  selected[i] * list(df['revenue'].values)[i] 
  for i in segments
)

# constraints
problem_v3 += lpSum(
    selected[i] * df['marketing_spending'].values[i]
    for i in segments
) <= 30 * 10**6

problem_v3 += lpSum(
    selected[i] for i in segments
) <= 10

# run the optimisation
problem_v3.solve()
df['selected'] = list(map(lambda x: x.value(), selected.values()))
print(df.selected.sum())
# 10

另一个可以调整我们问题的选项是改变目标函数。我们一直在优化收入,但假设我们希望同时最大化收入和新增用户。为此,我们可以稍微修改目标函数。

让我们考虑最好的方法。我们可以计算收入和新增用户的总和并力图最大化它。然而,由于收入通常比新增用户高出 1000 倍,结果可能会倾向于最大化收入。为了使这些指标更具可比性,我们可以基于它们的总和来规范化收入和用户数。然后,我们可以将目标函数定义为这些比例的加权和。我会给这两个指标赋予相等的权重(0.5),但你可以调整权重,使其中一个指标的权重更大。

# define the problem
problem_v4 = LpProblem("Marketing_campaign_v2", LpMaximize)

# decision variables
segments = range(df.shape[0]) 
selected = LpVariable.dicts("Selected", segments, cat="Binary")

# objective Function
problem_v4 += (
    0.5 * lpSum(
        selected[i] * df['revenue'].values[i] / df['revenue'].sum()
        for i in segments
    )
    + 0.5 * lpSum(
        selected[i] * df['users'].values[i] / df['users'].sum()
        for i in segments
    )
)

# constraints
problem_v4 += lpSum(
    selected[i] * df['marketing_spending'].values[i]
    for i in segments
) <= 30 * 10**6

# run the optimisation
problem_v4.solve()
df['selected'] = list(map(lambda x: x.value(), selected.values()))

我们得到了最优目标函数值 0.6131,收入为 1.0436 亿美元,新增用户为 136,370 人。

就这样!我们学会了如何使用整数规划来解决各种优化问题。

你可以在GitHub上找到完整的代码。

总结

在本文中,我们探讨了解决背包问题及其在产品分析中的类比问题的不同方法。

  • 我们从暴力破解方法开始,但很快意识到它会占用不合理的时间。

  • 接下来,我们尝试通过直觉选取表现最好的区段,但这种方法得出的结果是错误的。

  • 最后,我们转向了整数规划,学习了如何将我们的产品任务转化为优化模型并有效地解决它们。

有了这个,我希望你已经为你的工具包增添了另一项有价值的分析工具。

非常感谢你阅读本文。我希望这篇文章对你有所启发。如果你有任何后续问题或意见,请在评论区留言。

参考资料

除非另有说明,所有图片均由作者制作。

线性规划:带分支定界的整数线性规划

原文:towardsdatascience.com/linear-programming-integer-linear-programming-with-branch-and-bound-fe25a0f8ae55?source=collection_archive---------13-----------------------#2024-11-19

第四部分:将线性规划优化扩展到离散决策变量

Jarom HuletTowards Data Science Jarom Hulet

·发布于Towards Data Science ·9 分钟阅读·2024 年 11 月 19 日

--

图片来自 Pexels.com,摄影:Min An

在本系列之前的内容中,我们讨论了严格的线性规划——其中目标函数、约束条件和决策变量都是线性且连续的。这种线性设置具有一些非常好的特性,但它的灵活性较差。本文将讨论如何使用一种名为整数线性规划(ILP)的工具,允许使用离散决策变量。

这是我写的关于线性规划系列文章的第四篇。其他文章(包括一个介绍部分——如果你不熟悉线性规划,可以查看)可以在这里找到:

Jarom Hulet

Jarom Hulet

线性规划

查看列表4 篇故事

本文将涵盖以下主题:

  1. 何时需要离散决策变量

  2. 分支定界算法如何解决整数线性规划问题

  3. 整数线性规划的优缺点…

线性规划优化:基础

原文:towardsdatascience.com/linear-programming-optimization-foundations-2f12770f66ca?source=collection_archive---------3-----------------------#2024-07-22

第一部分 - 基本概念与示例

Jarom Hulet数据科学前沿 Jarom Hulet

·发布于数据科学前沿 ·11 分钟阅读·2024 年 7 月 22 日

--

线性规划是一种强大的优化技术,广泛应用于许多领域,以帮助改进决策过程。这是一个系列文章的第一部分,将涵盖与线性规划相关的重要主题。本文是最基础的部分,旨在介绍基本概念,之后将深入探讨更高级的线性规划话题。

在本文中,我们将:

  1. 讨论什么构成了一个线性规划问题

  2. 了解线性规划是如何工作的以及它为何如此强大

  3. 在 Python 中运行一个线性规划示例

我个人认为,例子是学习技术主题的非常有效的途径。出于这个原因,本文将通过一个简单的例子进行讲解。我将在下一节介绍这个例子。

在我们深入之前,如果你对优化的基本概念和术语不太熟悉,我写了一篇优化入门文章,建议你在继续阅读之前先查看一下(本文中有很多常见的优化术语,我没有在文中定义)。

线性规划优化:单纯形法

原文:towardsdatascience.com/linear-programming-optimization-the-simplex-method-b2f912e4c6fd?source=collection_archive---------7-----------------------#2024-09-10

第三部分:算法的幕后

Jarom HuletTowards Data Science Jarom Hulet

·发表于Towards Data Science ·13 分钟阅读·2024 年 9 月 10 日

--

图片来自 Pexels.com 的 Daniel Cassey Pahati

到目前为止,本系列已涵盖了线性规划的基础知识。在这篇文章中,我们将从基本概念转向更深入的算法细节!这篇文章将介绍单纯形法,它是解决线性规划问题时常用的算法。虽然我们将通过手工计算一个简单的线性规划例子来应用单纯形法,但我们的重点将放在算法的直觉理解上,而非记住算法的步骤(这些步骤可以交给计算机来处理!)。

下面是我们将要讨论的内容:

  1. 为什么需要单纯形法

  2. 从图形解法到代数解法

  3. 演示单纯形法如何通过一个简单的例子来工作

这是包含我至今为止为此系列撰写的所有文章的列表链接:

Jarom Hulet

Jarom Hulet

线性规划

查看列表4 篇故事!

为什么需要单纯形法

线性规划:库存切割问题

原文:towardsdatascience.com/linear-programming-the-stock-cutting-problem-dc6ba3bf3de1?source=collection_archive---------4-----------------------#2024-08-22

第二部分 — 线性规划示例深入分析

Jarom HuletTowards Data Science Jarom Hulet

·发布于 Towards Data Science ·12 分钟阅读·2024 年 8 月 22 日

--

图片由 Mark Stebnicki 提供,来自 Pexels.com

这篇文章深入探讨了线性规划如何解决一个特定的问题——‘库存切割’问题。在开始深入讲解本系列中的线性规划细节之前,我想提供一个具体的例子。这篇文章将使用一些优化术语,这些术语没有在文中定义——我写了一篇优化基础的文章,涵盖了这些术语和其他基本概念。

在本文中,我们将讨论:

  1. 库存切割问题定义

2. 问题难度

3. 用 Python 解决问题

这里是该系列的第一篇文章和我提到的优化基础文章的链接:

## 线性规划优化:基础知识

第一部分 — 基本概念和例子

towardsdatascience.com

线性回归与因果结论

原文:towardsdatascience.com/linear-regressions-for-causal-conclusions-34c6317c5a11?source=collection_archive---------0-----------------------#2024-04-10

一个简单而强大的决策工具

Mariya MansurovaTowards Data Science Mariya Mansurova

·发布于 Towards Data Science ·阅读时间 21 分钟·2024 年 4 月 10 日

--

图片来自 DALL-E

我想我们大多数人都听过“相关性不等于因果性”这句话很多次。对于分析师来说,这常常是一个问题,因为我们通常只能看到相关性,但仍然想做出因果推论。

让我们通过几个例子更好地理解这种差异。我想从日常生活中的一个案例开始,而不是数字世界中的案例。

1975 年,丹麦开展了一项大型人口研究。这项研究被称为哥本哈根市心脏研究(CCHS)。研究人员收集了约 2 万名男女的资料,并对这些人进行了几十年的跟踪监测。该研究最初的目标是寻找预防心血管疾病和中风的方法。该研究的一个结论是,报告定期打网球的人预期寿命高出 9.7 年。

让我们思考一下如何解读这些信息。它是否意味着如果一个人今天开始每周打网球,他们的预期寿命会增加十年?不幸的是,答案并非完全如此。由于这是一个观察性研究,我们应该小心做出因果推论。可能存在一些其他的影响因素。例如,网球运动员可能更富裕,而我们知道较高的财富与更长的寿命相关。或者,也有可能是做运动的人更注重自己的健康,因此定期进行所有检查。所以,观察性研究可能会高估网球对寿命的影响,因为它没有控制其他因素。

让我们继续讨论一些与产品分析和我们日常工作更相关的例子。客户支持联系的次数可能与客户流失的概率正相关。如果客户不得不联系客户支持十次,他们可能会感到烦躁并停止使用我们的产品,而那些从未遇到问题并且对服务感到满意的客户可能永远不会有任何问题需要咨询。

这是否意味着,如果我们减少客户支持联系的次数,就能提高客户保持率?我敢打赌,如果我们隐藏联系信息并大幅减少客户支持联系的次数,我们并不能减少客户流失,因为流失的实际根本原因不是客户支持联系,而是客户对产品的不满,这导致了客户既联系了我们,又停止使用我们的产品。

我希望通过这些例子,你能够对相关性与因果关系的问题有一些直观的理解。

在本文中,我将分享一些从数据中得出因果结论的方法。令人惊讶的是,我们将能够使用最基础的工具——线性回归。

如果我们使用相同的线性回归进行因果推断,你可能会想,通常的方法和因果分析有什么区别?这是一个好问题。让我们通过理解两者之间的差异来开始我们的因果分析之旅。

预测分析与因果分析

预测分析有助于做出预测,并回答类似“如果没有任何变化,我们一年后会有多少客户?”或“这个客户在接下来的七天内购买的概率是多少?”的问题。

因果分析试图理解过程的根本原因。它可能帮助你回答类似“如果我们提高订阅费,客户流失的数量会是多少?”或者“如果我们没有推出这项情人节促销活动,会有多少客户签订我们的订阅?”这样的“假如”问题。

因果问题似乎比预测问题更复杂。然而,这两种方法往往使用相同的工具,例如线性回归或逻辑回归。尽管工具相同,但它们的目标完全不同:

  • 对于预测分析,我们尽最大努力基于已知信息预测未来的某个值。因此,主要的关键绩效指标是预测的误差。

  • 在建立因果分析的回归模型时,我们专注于目标值与其他因素之间的关系。模型的主要输出是系数,而不是预测值。

让我们看一个简单的例子。假设我们想要预测活跃客户的数量。

  • 在预测方法中,我们讨论的是基准预测(假设情况保持基本不变)。我们可以使用ARIMA自回归积分滑动平均)并以先前的数值为基础进行预测。ARIMA 对于预测很有效,但无法告诉你影响 KPI 的因素以及如何改善你的产品。

  • 在因果分析中,我们的目标是寻找数据中的因果关系,因此我们将构建回归模型,识别可能影响我们关键绩效指标(KPI)的因素,如订阅费用、营销活动、季节性等。在这种情况下,我们不仅会得到常规(BAU,business as usual)预测,还能够估计未来的不同“假设情景”。

现在,到了深入因果理论并学习基本术语的时候了。

相关性并不意味着因果关系

让我们考虑以下示例来展开讨论。假设你向忠实客户发送了折扣券,现在你想了解它如何影响客户的价值(在产品上的花费)和客户保持率。

因果分析中的一个最基本的术语是处理。听起来像是与医学相关的术语,但实际上它只是指一种干预措施。在我们的例子中,它是折扣。我们通常在单元层面(在我们这个例子中是客户)定义处理方式,通常定义如下。

另一个关键术语是结果Y,我们关心的变量。在我们的例子中,它是客户的价值。

因果推断的根本问题是我们无法同时观察同一客户的两个结果。因此,如果一个客户收到了折扣,我们永远无法知道如果没有折扣券,他的价值或保持率会是什么样子。这使得因果推断变得复杂。

这就是为什么我们需要引入另一个概念——潜在结果。发生的结果通常称为事实结果,而没有发生的结果称为反事实结果。我们将使用以下符号来表示它。

因果分析的主要目标是衡量处理与结果之间的关系。我们可以使用以下指标来量化这一关系:

  • ATE — 平均处理效应,

  • ATT — 对处理组的平均处理效应(即接受处理的客户)

它们都等于期望值,即潜在结果之间差异的期望值,适用于所有单元(在我们这个例子中是客户)或仅适用于接受处理的单元。

这是真正的因果效应,但不幸的是,我们无法计算它。不过别灰心,我们仍然可以得到一些估计。我们可以观察治疗客户和未治疗客户之间的值差异(相关效应)。让我们尝试解释这个值。

通过一些简单的数学变换(即加减相同的值),我们得出结论,治疗客户和未治疗客户之间的平均值等于ATT(治疗组的平均处理效应)和偏差项的总和。偏差等于没有治疗时对照组和治疗组之间的差异。

如果我们回到我们的案例,偏差将等于治疗组在未收到折扣的情况下预期的客户价值(反事实结果)和对照组(事实结果)之间的差异。

在我们的示例中,收到折扣的客户的平均值可能远高于未收到折扣的客户。我们能否将所有的效应归因于我们的治疗(折扣券)呢?不幸的是,不能。由于我们将折扣发放给了那些已经在我们产品上花费大量金钱的忠实客户,即使没有治疗,他们的价值可能也会高于对照组。所以,存在偏差,我们不能说两个群体之间的价值差异等于 ATT。

让我们思考一下如何克服这个障碍。我们可以做 A/B 测试:将我们的忠实客户随机分成两组,只向一半的客户发放折扣券。然后,我们可以估计折扣的效应,作为这两组之间的平均差异,因为我们已经消除了偏差(没有治疗的情况下,除了折扣外,这两组之间没有差异)。

我们已经介绍了因果推断的基本理论,并学到了偏差这个最关键的概念。因此,我们准备进入实践环节。我们将从分析 A/B 测试结果开始。

用例:A/B 测试

随机对照试验(RTC),通常称为 A/B 测试,是从数据中得出因果结论的强大工具。该方法假设我们是随机分配治疗,并且它帮助我们消除偏差(因为在没有治疗的情况下,组之间是相等的)。

为了练习解决此类任务,我们将查看一个基于合成数据的示例。假设我们构建了一个基于 LLM 的工具,帮助客服人员更快速地回答问题。为了衡量其效果,我们将该工具引入到一半的客服人员中,并且我们希望衡量我们的治疗(基于 LLM 的工具)如何影响结果(客服人员回答客户问题的时间)。

让我们快速查看一下我们拥有的数据。

以下是我们记录的参数描述:

  • case_id — 案例的唯一 ID。

  • agent_id — 代理的唯一 ID。

  • treatment 如果代理在实验组并有机会使用 LLM,则为 1,0 — 否则。

  • time_spent_mins — 回答客户问题所花费的分钟数。

  • cs_center — 客户支持中心。我们与多个客户支持中心合作。我们在其中一些中心启动了这个实验,因为在这些中心实施较为容易。这样的做法也帮助我们避免了污染(实验组和控制组的代理之间的互动可能会互相影响)。

  • complexity 等于 lowmediumhigh。该特征基于客户问题的类别,并定义代理处理此案件所需的时间。

  • tenure — 代理开始工作的月份数。

  • passed_training — 代理是否通过了 LLM 培训。该值仅对实验组为 True,因为控制组的代理没有接受此培训。

  • within_sla 等于 1,表示代理能够在 SLA(15 分钟)内回答问题。

如往常一样,让我们先从数据的高层次概述开始。我们有相当多的数据点,因此我们很可能能够获得统计上显著的结果。此外,我们可以看到实验组的平均响应时间明显较低,因此我们可以希望 LLM 工具确实有所帮助。

我通常还会查看实际的分布,因为平均统计数据可能具有误导性。在这种情况下,我们可以看到两个单峰分布,没有明显的离群值。

图片来自作者

经典统计方法

分析 A/B 测试的经典方法是使用统计公式。使用 scipy 包,我们可以计算两个均值之间差异的置信区间。

# defining samples
control_values = df[df.treatment == 0].time_spent_mins.values
exp_values = df[df.treatment == 1].time_spent_mins.values

# calculating p-values
from scipy.stats import ttest_ind

ttest_ind(exp_values, control_values)
# Output: TtestResult(statistic=-70.2769283935386, pvalue=0.0, df=89742.0)

我们得到的 p 值低于 1%。因此,我们可以拒绝零假设,得出结论:控制组和实验组在每个案件的平均处理时间上存在差异。为了了解效应大小,我们还可以计算置信区间。

from scipy import stats
import numpy as np

# Calculate sample statistics
mean1, mean2 = np.mean(exp_values), np.mean(control_values)
std1, std2 = np.std(exp_values, ddof=1), np.std(control_values, ddof=1)
n1, n2 = len(exp_values), len(control_values)
pooled_std = np.sqrt(((n1 - 1) * std1**2 + (n2 - 1) * std2**2) / (n1 + n2 - 2))
degrees_of_freedom = n1 + n2 - 2
confidence_level = 0.95

# Calculate margin of error
margin_of_error = stats.t.ppf((1 + confidence_level) / 2, degrees_of_freedom) * pooled_std * np.sqrt(1 / n1 + 1 / n2)

# Calculate confidence interval
mean_difference = mean1 - mean2
conf_interval = (mean_difference - margin_of_error, 
    mean_difference + margin_of_error)

print("Confidence Interval:", list(map(lambda x: round(x, 3), conf_interval)))
# Output: Confidence Interval: [-1.918, -1.814]

如预期的那样,由于 p 值低于 5%,我们的置信区间不包括 0。

传统方法是有效的。然而,我们可以通过线性回归得到相同的结果,这也将使我们能够进行更深入的分析。所以,让我们讨论这种方法。

线性回归基础

如我们之前讨论的那样,观察同一对象的两种潜在结果(有处理和无处理)是不可能的。由于我们无法单独估算每个对象的影响,因此我们需要一个模型。让我们假设常数处理效应。

然后,我们可以以以下方式写出结果(处理时间)和处理之间的关系,其中

  • baseline 是一个常数,表示结果的基本水平,

  • residual 代表我们现在不关心的其他潜在关系(例如,代理的成熟度或案件的复杂性)。

这是一条线性方程,我们可以使用线性回归来获得impact变量的估计值。我们将使用OLS(普通最小二乘法)函数,来自statsmodels包。

import statsmodels.formula.api as smf
model = smf.ols('time_spent_mins ~ treatment', data=df).fit()
model.summary().tables[1]

结果中,我们得到了所有需要的信息:效果估计(treatment变量的系数)、其 p 值和置信区间。

由于 p 值极小(明显低于 1%),我们可以认为效果显著,并且可以说我们的基于 LLM 的工具在 95%的置信区间内,帮助减少了 1.866 分钟的案件处理时间(1.814, 1.918)。你可以注意到,我们得到了与之前统计公式完全相同的结果。

添加更多变量

如承诺的那样,我们可以使用线性回归进行更复杂的分析,并考虑更多的因素,所以下来进行分析。在最初的方法中,我们只使用了一个回归变量——treatment标志。但是,我们可以添加更多的变量(例如,complexity)。

在这种情况下,impact将显示考虑了模型中所有其他变量的影响后的估计(在我们的例子中——任务复杂性)。让我们进行估计。将更多的变量添加到回归模型中非常简单——我们只需在方程中添加另一个组件。

import statsmodels.formula.api as smf
model = smf.ols('time_spent_mins ~ treatment + complexity', data=df).fit()
model.summary().tables[1]

现在,我们看到效果的估计值略有增加——1.91 分钟对比之前的 1.87 分钟。同时,误差减小了(0.015 对比 0.027),置信区间也变得更窄了。

你还可以注意到,由于复杂性是一个分类变量,它被自动转换成了一组虚拟变量。因此,我们得到了低复杂度任务为-9.8 分钟,中等复杂度任务为-4.7 分钟的估计值。

让我们试着理解为什么在添加复杂度后我们得到了更可信的结果。客户案件的处理时间显著依赖于任务的复杂性。因此,复杂性负责了我们变量方差的很大一部分。

图片由作者提供

正如我之前提到的,treatment的系数在考虑了方程中所有其他因素后,估算了影响。当我们将复杂度添加到我们的线性回归中时,它减少了残差的方差,这就是为什么我们得到了一个更窄的时间置信区间。

让我们再确认一下复杂性是否解释了方差的显著比例。我们可以看到一个显著的下降:时间花费的方差为 16.6,但当考虑复杂性时,它减少到了仅为 5.9。

time_model = smf.ols('time_spent_mins ~ complexity', data=df).fit()

print('Initial variance: %.2f' % (df.time_spent_mins.var()))
print('Residual variance after accounting for complexity: %.2f' \
  % (time_model.resid.var()))

# Output: 
# Initial variance: 16.63
# Residual variance after accounting for complexity: 5.94

所以,我们可以看到,向线性回归中添加一个可以预测结果变量的因素,可以改善你的效应估计。此外,值得注意的是,这个变量与治疗分配无关(每个复杂度的任务有相等的机会分配到控制组或实验组)。

传统上,因果图被用来显示变量之间的关系。让我们画出这样的图来表示我们当前的情况。

图片由作者提供

非线性关系

到目前为止,我们只看了线性关系,但有时候,仅仅线性模型并不足以描述我们的情况。

让我们来看看实验组代理人应当通过的 LLM 训练数据。只有一半的代理人通过了 LLM 训练并学会了如何有效地使用新工具。

我们可以看到,通过训练的处理组和未通过训练的代理人在平均时间上有显著差异。

图片由作者提供

所以,我们应该期待这两组在处理上的不同影响。我们可以使用非线性来表达这种关系,并在方程中加入treatment * passed_training项。

model = smf.ols('time_spent_mins ~ treatment * passed_training + complexity', 
    data=df).fit()
model.summary().tables[1]

treatmentpassed_training因素也会自动添加到回归模型中。所以,我们将优化以下公式。

我们从线性回归中得到了以下结果。

与通过训练相关的影响没有统计学意义,因为 p 值超过了 5%,而其他系数则显著不同于零。

让我们列出所有不同的情景,并使用我们从线性回归中得到的系数来估计效果。

所以,我们得到了新的处理估计:通过训练的代理人每个案例平均提高 2.5 分钟,而未通过训练的代理人则是 1.3 分钟。

混杂因素

在得出结论之前,值得再检查一下我们所做的一些假设——例如,随机分配。我们已经讨论过,我们在一些计算机科学中心开展了实验。让我们检查不同中心的代理人是否相似,以确保我们的控制组和实验组没有偏差。

我们知道,代理人因经验不同,其表现可能会有显著差异。我们的日常直觉告诉我们,更有经验的代理人会在任务上花费更少的时间。从数据中我们可以看到,确实是这样。

图片由作者提供

让我们看看我们的实验组和对照组是否有相同水平的代理人经验。最简单的方法是查看分布。

图片由作者提供

显然,治疗组的代理比控制组的代理有更多的经验。总体来说,产品团队决定从经验更丰富的代理开始实验是有道理的。然而,这打破了我们关于随机分配的假设。由于控制组和测试组即便没有治疗也存在差异,我们高估了 LLM 工具对代理表现的影响。

让我们回到因果图。代理的经验同时影响治疗分配和输出变量(花费时间)。这类变量被称为混杂因素。

图片来自作者

别担心。我们可以轻松解决这个问题——只需要在我们的方程中包含混杂因素来进行控制。当我们将其添加到线性回归中时,我们开始在固定经验的前提下估计治疗效应,从而消除偏差。让我们试试看。

model = smf.ols('time_spent_mins ~ treatment * passed_training + complexity + tenure', data=df).fit()
model.summary().tables[1]

加入任期后,我们得到了以下结果:

  • 单独考虑通过培训或治疗的影响并无统计学显著性,因为 p 值高于 5%。因此,我们可以得出结论,LLM 助手不会影响代理的表现,除非他们通过了培训。在之前的迭代中,我们看到过一个统计学显著的效果,但那是由于任期混杂偏差造成的。

  • 唯一具有统计学显著性的效果是对于通过培训的治疗组。其结果为 1.07 分钟,95%的置信区间为(1.02,1.11)。

  • 每个月的任期与任务上花费时间减少 0.05 分钟相关。

我们使用的是合成数据,因此可以轻松地将我们的估计与实际效果进行比较。如果代理通过了培训,LLM 工具可以将每个任务所花费的时间减少 1 分钟,因此我们的估计相当准确。

不良控制

机器学习任务通常很简单:你收集尽可能多的特征数据,尝试拟合一些模型,比较它们的表现,然后选择最佳模型。相反,因果推断需要一些技巧和对所处理过程的深刻理解。一个关键问题是,哪些特征值得纳入回归分析,哪些特征会破坏你的结果。

直到现在,我们添加到线性回归中的所有额外变量都在提高准确性。所以,你可能认为将所有特征都添加到回归分析中将是最佳策略。不幸的是,因果推断并不是那么简单。在本节中,我们将讨论几个额外变量反而降低估计准确性的情况。

例如,我们的数据中有一个客户服务中心。我们基于客户服务中心分配了治疗,因此将其纳入回归分析可能听起来合情合理。让我们试试看。

model = smf.ols('time_spent_mins ~ treatment + complexity + tenure + cs_center', 
    data=df[df.treatment == df.passed_training]).fit()
model.summary().tables[1]

为了简化,我已从数据集和方程中去除非线性因素,筛选出那些治疗组中的代理没有通过 LLM 培训的情况。

如果我们将 CS 中心包含在线性回归中,我们将得到一个荒谬的高估计效果(大约数十亿),但没有统计显著性。因此,这个变量比有帮助更有害。

让我们更新一个因果图,并尝试理解为什么它不起作用。CS 中心是我们治疗的预测因子,但与输出变量没有关系(因此它不是一个混杂变量)。增加一个治疗预测因子会导致多重共线性(就像在我们这个例子中)或减少治疗的方差(因为治疗变化不大,很难估计治疗对输出变量的影响)。因此,将这样的变量加入方程是不好的做法。

图片由作者提供

让我们来看另一个例子。我们有一个within_sla变量,表示代理是否在 15 分钟内完成任务。这个变量能否提高我们的效果估计质量?让我们看看。

model = smf.ols('time_spent_mins ~ treatment + complexity + tenure + within_sla', 
    data=df[df.treatment == df.passed_training]).fit()
model.summary().tables[1]

新的效果估计值大大降低:0.8 分钟对比 1.1 分钟。那么,这就提出了一个问题:哪一个更准确?我们在这个模型中增加了更多参数,因此它更复杂。难道它应该提供更精确的结果吗?不幸的是,情况并不总是这样。让我们深入探讨一下。

在这种情况下,within_sla标志表示代理是否在 15 分钟内解决了问题,或者问题花费了更多时间。因此,如果我们回到我们的因果图,within_sla标志是我们的输出变量(任务所花费的时间)的一个结果。

图片由作者提供

当我们将within_sla标志加入回归并加以控制时,我们开始估计治疗的效果,固定within_sla的值。因此,我们将有两种情况:within_sla = 1within_sla = 0。让我们看看它们的偏差。

在这两种情况下,偏差不等于 0,这意味着我们的估计是有偏的。乍一看,这似乎有点违反直觉。让我稍微解释一下其中的逻辑。

  • 在第一个方程中,我们比较了使用 LLM 工具和不使用 LLM 工具的代理在 15 分钟内完成任务的情况。之前的分析表明,LLM 工具(我们的治疗手段)通常能加速代理的工作。所以,如果我们比较没有治疗(即代理独立工作而没有 LLM 工具)时预期的任务完成时间,我们应该预期第二组的响应会更快。

  • 类似地,对于第二个方程,我们正在比较没有在 15 分钟内完成任务的代理,即使在 LLM 的帮助下,以及那些独立完成任务的代理。我们应该预期第一组在没有治疗的情况下响应时间更长。

这是选择偏差的一个例子——当我们控制了从处理到输出变量或输出变量结果路径上的一个变量时,就会发生这种情况。在进行线性回归时控制这些变量也会导致偏差估计,所以不要这么做。

分组数据

在某些情况下,你可能没有详细的数据。在我们的例子中,我们可能不知道每个任务单独花费的时间,但知道平均时间。跟踪代理的汇总数据更容易。例如,“在两个小时内,一名代理完成了 15 个中等难度的任务”。我们可以将原始数据进行汇总,从而获得这些统计数据。

agents_df = df.groupby(['agent_id', 'treatment', 'complexity', 'tenure', 
  'passed_training'], as_index = False).aggregate(
    {'case_id': 'nunique', 'time_spent_mins': 'mean'}
)

对于线性回归来说,处理代理级别的数据并不是问题。我们只需要为每个代理指定权重(等于案例的数量)。

 model = smf.ols('time_spent_mins ~ treatment + complexity + tenure', 
    data = agents_df[agents_df.treatment == agents_df.passed_training],
    weights = agents_df[agents_df.treatment == agents_df.passed_training]['case_id'])\
    .fit()
model.summary().tables[1]

对于汇总数据,我们得到的治疗效果的结果大致相同。所以,如果你只有平均数据,也没有问题。

使用案例:观察性数据

我们已经详细查看了用于因果推断的 A/B 测试例子。然而,在许多情况下,我们不能进行适当的随机试验。以下是一些例子:

  • 有些实验是不道德的。例如,你不能强迫学生饮酒或吸烟,看它如何影响他们在大学的表现。

  • 在某些情况下,由于法律限制,你可能无法进行 A/B 测试。例如,你不能对同一产品收取不同的价格。

  • 有时,这简直是不可能的。例如,如果你正在进行大规模的品牌重塑,你将不得不在某一天通过一个大的公关宣布全球推出。

在这种情况下,你必须仅凭观察数据得出结论。让我们看看在这种情况下我们的方法如何发挥作用。我们将使用学生表现数据集来自加州大学欧文分校的机器学习库。

让我们使用这个实际数据集来研究愿意接受高等教育的意愿如何影响数学课程的最终成绩。我们将从一个简单的模型和因果图开始。

图片由作者提供

df = pd.read_csv('student-mat.csv', sep = ';')
model = smf.ols('G3 ~ higher', data=df).fit()
model.summary().tables[1]

我们可以看到,继续教育的意愿在统计上显著地提高了课程的最终成绩,增加了 3.8 分。

然而,可能存在一些混杂因素,我们必须加以控制。例如,父母的教育水平可能同时影响治疗效果(如果父母受过教育,孩子更有可能计划接受高等教育)和结果(受过教育的父母更可能帮助孩子,因此孩子的成绩更高)。让我们将母亲和父亲的教育水平添加到模型中。

图片由作者提供

model = smf.ols('G3 ~ higher + Medu + Fedu', data=df).fit()
model.summary().tables[1]

我们可以看到母亲的教育水平对结果有统计学上的显著影响。我们可能提高了估计的准确性。

然而,我们应该对任何基于观察性数据得出的因果结论持谨慎态度。我们不能确定是否已经考虑了所有混杂变量,并且我们得到的估计是否完全无偏。

此外,解释关系的方向可能会有些棘手。我们确信继续教育的意愿与最终成绩之间存在相关性。然而,我们可以从多个角度来解释这一点:

  • 希望继续接受教育的学生更有动力,因此他们的最终成绩较高。

  • 成绩较高的学生受到学习成功的激励,这就是为什么他们想继续教育的原因。

在观察性数据中,我们只能凭借常识选择一种选项。我们无法仅通过数据推断出这个结论。

尽管存在一些局限性,我们仍然可以使用这个工具尽力得出一些关于世界的结论。正如我提到的,因果推断在很大程度上依赖于领域知识和常识,因此值得花时间在白板旁深思熟虑你所建模的过程。这将有助于你取得卓越的成果。

你可以在GitHub上找到这些示例的完整代码。

总结

我们讨论了一个相当广泛的因果推断主题,让我来总结一下我们所学到的内容:

  • 预测分析的主要目标是获得准确的预测。因果推断侧重于理解变量之间的关系,因此我们更关心模型中的系数,而非实际的预测结果。

  • 我们可以利用线性回归得出因果结论。

  • 理解我们应该在线性回归中添加哪些特征是一门艺术,但这里有一些指导。

    — 必须包括混杂变量(同时影响处理和结果的特征)。

    — 添加一个能预测输出变量并解释其变化的特征,可以帮助你获得更有信心的估计。

    — 避免添加仅影响处理或是输出变量结果的特征。

  • 你可以将这种方法应用于 A/B 测试和观察性数据。然而,对于观察性数据,我们应当对因果结论持谨慎态度,因为我们无法确保已经考虑了所有混杂变量。

非常感谢你阅读这篇文章。如果你有任何后续问题或评论,请在评论区留言。

数据集

Cortez, Paulo. (2014). 学生表现。 UCI 机器学习库 (CC BY 4.0)。 doi.org/10.24432/C5TG7T

参考文献

除非另有说明,所有图片均由作者制作。

本文的灵感来源于《勇敢而真实的因果推断》一书,该书对因果推断的基础进行了精彩的概述。

线性化注意力

原文:towardsdatascience.com/linearizing-attention-204d3b86cc1e?source=collection_archive---------3-----------------------#2024-12-26

打破二次方限制:softmax 注意力的现代替代方案

Shitanshu BhushanTowards Data Science Shitanshu Bhushan

·发布于 Towards Data Science ·阅读时间 8 分钟·2024 年 12 月 26 日

--

大型语言模型非常强大,但它们有一个小缺点,即使用 softmax 注意力,这可能会导致计算开销较大。在本文中,我们将探索是否有办法通过某种方式替换 softmax,从而实现线性时间复杂度。

作者提供的图片(使用 Miro Board 创建)

注意力机制基础

我假设你已经了解 ChatGPT、Claude 之类的内容,以及 Transformer 在这些模型中的工作原理。那么,注意力就是这些模型的核心。如果我们考虑普通的 RNN,我们会将所有过去的状态编码到一个隐藏状态中,然后使用这个隐藏状态与新的查询一起得到输出。一个明显的缺点是,你不能把所有信息都存储在一个小小的隐藏状态中。这就是注意力机制的作用,想象一下,对于每一个新的查询,你可以找到最相关的过去数据,并使用这些数据来做出预测。这就是注意力机制的本质。

Transformer 中的注意力机制(大多数当前语言模型背后的架构)涉及键、查询和值的嵌入。Transformer 中的注意力机制通过将查询与键进行匹配来检索相关值。对于每个查询(Q),模型计算与所有可用键(K)的相似度分数,然后利用这些分数创建相应值(Y)的加权组合。这个注意力计算可以表达为:

来源:作者提供的图片

这一机制使模型能够在做出预测时,有选择性地从整个上下文中提取和利用信息。我们在这里使用 softmax,因为它能有效地将原始相似度分数转换为标准化的概率,类似于 k 最近邻机制,其中更高的注意力权重会分配给更相关的键。

好的,现在让我们看一下 1 层注意力的计算成本,

来源:图片由作者提供

Softmax 缺点

从上面的内容可以看出,我们需要计算一个 NxN 矩阵的 softmax,因此,我们的计算成本随着序列长度的增加呈二次增长。对于较短的序列,这没问题,但对于长序列,N=100k+ 时,它变得极其低效。

这给了我们动机:我们能否减少计算成本?这就是线性注意力的作用所在。

线性注意力

Katharopoulos 等人提出,线性注意力使用了一种巧妙的技巧,将 softmax 指数表示为一个核函数,表现为特征映射 φ(x) 的点积。利用矩阵乘法的结合律,我们可以将注意力计算重写为线性。下图展示了这一转换:

来源:图片由作者提供

Katharopoulos 等人使用 elu(x) + 1 作为 φ(x),但任何能够有效逼近指数相似度的核特征映射都可以使用。上述计算成本可以表示为:

来源:图片由作者提供

这样就不再需要计算完整的 N×N 注意力矩阵,并将复杂度降低到 O(Nd²)。其中 d 是嵌入维度,当 N >>> d 时,这实际上是线性复杂度,这通常适用于大规模语言模型。

好的,让我们来看一下线性注意力的递归视角,

来源:图片由作者提供

好的,为什么我们可以在线性注意力中做这个,而不能在 softmax 中做呢?嗯,softmax 不是可分离的,因此我们无法将其写成独立项的乘积。这里值得注意的是,在解码过程中,我们只需要跟踪 S_(n-1),每生成一个 token 就能达到 O(d²) 的复杂度,因为 S 是一个 d × d 的矩阵。

然而,这种效率带来了一个重要的缺点。由于 S_(n-1) 只能存储 d² 信息(它是一个 d × d 的矩阵),我们面临着一个根本的限制。例如,如果原始上下文长度需要存储 20d² 的信息,你将实际上在压缩过程中丢失 19d² 的信息。这展示了线性注意力中的核心内存效率权衡:通过仅保持固定大小的状态矩阵,我们获得了计算效率,但这种固定大小限制了我们可以保留的上下文信息量,这也给我们提供了引入门控机制的动机。

门控线性注意力

好的,我们已经确定,在使用固定大小的状态矩阵优化效率时,我们不可避免地会遗忘信息。这引出了一个重要的问题:我们能否聪明地选择记住什么信息?这就是门控机制的作用——研究人员将其作为一种选择性地保留重要信息的机制,通过战略性地选择保留哪些信息来尽量减少记忆丧失的影响。门控并不是一个新概念,它在像 LSTM 这样的架构中已经得到广泛应用。

这里的基本变化在于我们如何公式化 Sn,

来源:作者提供的图片

有许多选择可以决定 G,这些选择会导致不同的模型,

来源:Yang, Songlin, et al. “Gated linear attention transformers with hardware-efficient training.” arXiv preprint arXiv:2312.06635(2023).

这种架构的一个关键优势是,门控函数仅依赖于当前标记 x 和可学习的参数,而不是整个序列历史。由于每个标记的门控计算是独立的,这使得在训练过程中能够高效地进行并行处理——整个序列的所有门控计算可以同时执行。

状态空间模型

当我们想到处理像文本或时间序列这样的序列时,我们的大脑通常会跳到注意力机制或 RNN。但如果我们采取完全不同的方法呢?如果我们不把序列当作序列来处理,而是像 CNN 处理图像一样,通过卷积来处理它们呢?

状态空间模型(SSMs)通过离散线性时不变系统形式化了这种方法:

来源:作者提供的图片

好的,现在让我们看看这与卷积有什么关系,

来源:作者提供的图片

其中 F 是我们从参数(A, B, c)中学习到的滤波器,*表示卷积。

H3通过一种新颖的结构化架构实现了这种状态空间公式,该架构由两个互补的 SSM 层组成。

来源:Fu, Daniel Y., et al. “Hungry hungry hippos: Towards language modeling with state space models.” arXiv preprint arXiv:2212.14052 (2022).

在这里,我们将输入分解为 3 个通道,以模仿 K、Q 和 V。然后,我们使用 2 个 SSM 和 2 个门控来模仿线性注意力,事实证明,这种架构在实践中效果相当不错。

选择性状态空间模型

早些时候,我们看到门控线性注意力如何通过使信息保持过程数据依赖,改进了标准的线性注意力。状态空间模型中也存在类似的限制——控制状态转移和输出的参数 A、B 和 c 是固定的且与数据无关。这意味着每个输入都通过相同的静态系统进行处理,而不考虑其重要性或上下文。

我们可以通过引入时变动态系统,使 SSM(状态空间模型)成为数据依赖的,从而扩展 SSM:

来源:作者提供的图片

关键问题变成了如何将 c_t、b_t 和 A_t 参数化为输入的函数。不同的参数化方式可以导致接近线性或门控注意力机制的架构。

Mamba通过选择性 SSM 模块实现了这种时变状态空间的表达。

来源:Gu, Albert, and Tri Dao. “Mamba: Linear-time sequence modeling with selective state spaces.” arXiv preprint arXiv:2312.00752 (2023).

在这里,Mamba 使用了选择性 SSM 代替了 SSM,并通过输出门控和额外的卷积来提高性能。这是一个非常高层次的思想,解释了 Mamba 如何将这些组件组合成一个高效的序列建模架构。

结论

在本文中,我们探讨了高效序列建模架构的演变。从传统的 softmax 注意力开始,我们识别出其二次复杂度限制,这促使了线性注意力的发展。通过使用核函数重写注意力,线性注意力实现了 O(Nd²)的复杂度,但由于固定大小的状态矩阵,它面临着内存限制。

这种限制促使了门控线性注意力的提出,通过门控机制引入了选择性信息保持。我们接着从状态空间模型的角度进行了探讨,展示了它们如何通过类似卷积的操作来处理序列。从基础 SSM 到时变系统,再到选择性 SSM 的进展,与我们从线性到门控注意力的历程相似——在这两种情况下,使模型更加适应输入数据对于性能至关重要。

通过这些发展,我们看到了一个共同的主题:计算效率与内存容量之间的基本权衡。Softmax 注意力通过保持对整个序列的完全关注,擅长于上下文学习,但代价是二次复杂度。线性变体(包括 SSM)通过固定大小的状态表示实现了高效计算,但这种优化限制了它们保持过去上下文详细记忆的能力。这个权衡继续成为序列建模中的核心挑战,推动着对能够更好平衡这些竞争需求的架构的探索。

想要了解更多相关主题,我建议阅读以下论文:

线性注意力: Katharopoulos, Angelos 等. “Transformers are rnns: Fast autoregressive transformers with linear attention.” 国际机器学习会议. PMLR, 2020.

GLA: Yang, Songlin 等. “Gated linear attention transformers with hardware-efficient training.” arXiv 预印本 arXiv:2312.06635 (2023).

H3: Fu, Daniel Y. 等. “Hungry hungry hippos: Towards language modeling with state space models.” arXiv 预印本 arXiv:2212.14052 (2022).

Mamba: Gu, Albert 和 Tri Dao. “Mamba: Linear-time sequence modeling with selective state spaces.” arXiv 预印本 arXiv:2312.00752 (2023).

Waleffe, Roger 等. “An Empirical Study of Mamba-based Language Models.” arXiv 预印本 arXiv:2406.07887 (2024).

致谢

这篇博客文章的灵感来源于我在 2024 年秋季于密歇根大学研究生课程中的学习。尽管这些课程提供了探索这些主题的基础知识和动机,但本文中的任何错误或误解完全是我个人的责任。这代表了我对这些材料的个人理解和探索。

LingoNaut 语言助手

原文:towardsdatascience.com/lingonaut-language-assistant-6abe3e8b045c?source=collection_archive---------3-----------------------#2024-02-11

使用 Ollama-Python 对讲机进行多语言学习

Nate CibikTowards Data Science Nate Cibik

·发表于 Towards Data Science ·阅读时长 11 分钟·2024 年 2 月 11 日

--

图像由作者使用 DALL-E 3 创建。

我们生活在一个世界真正触手可及的时代,只要我们知道该往哪里看。今天的开源大型语言模型(LLMs)足够强大和紧凑,可以将人类知识的完整集合放置在标准消费级硬件上,提供数小时无广告的深入讨论,涉及无数主题,且无需互联网连接。感谢开源社区的努力,像 Ollama 这样的工具使我们能够在本地提供今天顶级模型的高质量量化版本,并通过简化的 API 与之互动。

这种开发的便利性意味着我们可以减少花费在思考如何构建 LLM 应用上的时间,更多地关注我们想要构建的内容。就个人而言,我一直想学习多种语言,但由于我的生活经历中没有足够的多语言环境,且在旅游语言学习时,因缺乏亲密的朋友可供练习,这种学习往往会显得尴尬,因为我们不得不在陌生人身上进行试探。

这是一个有前景的机会,旨在利用基于大型语言模型(LLM)的聊天机器人开放式对话能力。由于如今在消费级硬件上本地化运行量化的开源 LLM 已经成为一个高效的流程,我们所需要做的唯一一件事就是将互动方式扩展为语音到语音的格式。同样,开源研究的成果为我们提供了便利。高质量的语音转文本和文本转语音模型已经准备就绪,并配有直观的 API 接口。

一个完美展示这些工具如何轻松地将激动人心的概念变为现实的示例就是 LingoNaut:一个多语言语言助手,它运行在一个仅包含 300 行代码的单一 Python 脚本中。通过结合 OpenAI 的 Whisper 语音转文本模型、本地 Ollama 服务器和 Coqui.ai 的 TTS 文本转语音库,我们可以构建一个用户友好的对讲机界面,并提供多种 LLM 选择。从这里开始,只需要进行系统提示工程(通过 ollama-python 容易完成),就可以从 LLM 中召唤出我们所需的行为,在这个案例中,创造一个有用的多语言语言导师。实际上,这意味着 LingoNaut 的代码可以轻松适应,通过调整使用的 LLM 和系统提示,就能创建各种各样的 AI 助手。

LingoNaut 应用演示。

运行 LingoNaut 的代码可以在 GitHub 仓库中找到,提供了简单的安装说明。LingoNaut 是一个开源项目,欢迎贡献。例如,未来的工作可能涉及将后端包装成更复杂的网页 UI,以支持远程托管对讲机 LLM 应用,从而支持移动设备。我希望 LingoNaut 能成为一个有趣且有用的资源,帮助其他人在他们的学习旅程中,同时也希望这段代码能作为一个轻量级的框架,供工程师们在沙盒中实验新的 LLM 或 LMM 基于的应用想法。

本文的其余部分概述了运行 LingoNaut 的 Python 代码,LingoNaut 是一个开源组件工具箱,使得像 LingoNaut 这样的工具能够轻松组合成型,并且为未来的工作提供了有前景的方向。通过一些巧妙的 Python 编程技巧、出色的语音转文本和文本转语音模型,以及在消费者机器上本地部署量化的 LLM(大语言模型),我们可以轻松构建一个语音对语音的管道,来解锁那些不太适合文本形式的应用场景,例如语言学习。研究界的聪明贡献汇聚在一起,为我们提供了这一系列非凡的可能性。让我们更详细地走一遍每一个步骤,理解它们在让 LingoNaut 应用成为现实过程中所扮演的角色。

LingoNaut 代码

图片由作者使用 DALL-E 3 制作。

对讲机界面

LingoNaut 中的代码创建了一个方便的基于终端的语音到语音应用,可以与 Ollama 一起使用,并且可以轻松适应新的使用场景。通过使用一个名为pynput的包,我们可以创建一个在后台线程中运行并响应用户按键的键盘监听器对象。这为运行在连续循环中的应用提供了广泛的选项,最重要的是在这种情况下提供一个控制,用于触发和终止用户音频录制,而无需图形用户界面和按钮。这样,互动式应用程序可以直接从终端运行,从而简化了许多工程问题。

在 LingoNaut 中,可以通过按不同的键来录制音频,从而部署不同的语音转文本模型。轻量级的 Whisper 模型在快速处理英语音频方面表现优异,而更大且较慢的模型则必须用于准确的多语言实时转录。因此,用户可以选择按住Ctrl键使用轻量级基础模型来提问英语问题,或者按住Shift键来用其他语言进行对话。

由键盘监听器启用的其他有用的 LingoNaut 功能包括,当你输入错误或对响应方向不满意时,可以通过按End键中断模型响应。这防止了因等待无关的文本和音频完成播放而被卡住,从而使用户能够保持更高的参与度。用户还可以通过按F2键锁定键盘输入,这样他们就可以在不担心意外触发音频录制的情况下,保持会话打开以供以后使用。

并发

尽管 LingoNaut 中使用的软件包为与创建语音到语音管道的三个模型接口提供了简化的 API,但单纯等待大型语言模型(LLM)生成文本、将其转录为音频,然后将音频串行播放给用户,将是一个非常慢的体验。LLM 文本块的流式传输、文本块转录成音频文件以及播放先前转录的音频文件都可以同时发生,因此 LingoNaut 为每个任务使用了独立的线程。通过使用ThreadPoolExecutor对象并将max_workers设置为 1,我们可以轻松地为任务提交打开新的线程,同时确保任务按顺序执行,从而使我们能够将工作从主线程外包,而不会出现重叠或顺序错乱的返回。以下是该安排的基本代码框架:

import ollama
from concurrent import ThreadPoolExecutor

def play_audio(file_path: str):
    # audio file playback code

def dump_to_audio(text: str, file_path: str):
    # transcribe text-to-speech and save audio file

def process_stream(chat_history: list):
    stream = ollama.chat(
        model='mistral:lingonaut',
        messages=chat_history,
        stream=True,
    )

    with ThreadPoolExecutor(max_workers=1) as play_pool:
        with ThreadPoolExecutor(max_workers=1) as tts_pool:
            def play_output(text, file_path):
                output_path = dump_to_audio(text, file_path)
                play_pool.submit(play_audio, output_path)
                return

            def process_section(text, file_path):
                tts_pool.submit(play_output, text, file_path)
                return current_string

            current_section = ""
            for i, chunk in enumerate(stream):
                current_path = f"{i}.wav"
                text_chunk = chunk['message']['content']
                current_section += text_chunk
                if len(current_section) > 50:
                    process_section(current_section, current_path)
                    current_section = ""

            tts_pool.shutdown(wait=True)
            play_pool.shutdown(wait=True)

模型定制

ollama-python 包提供了一个简单的工具,可以使用“Modelfiles”创建自定义标签的模型配置,以引导 LLM 的行为。在 LingoNaut 的情况下,Ollama 库中的 4 位量化 Mistral 7B 模型通过显式的系统提示进行了自定义,以引导其作为语言学习助手的预期行为。该提示可以在仓库中的 create_lingonaut_ollama.py 文件中找到,需要注意的是,这是唯一一个自定义模型选择和行为的文件,这意味着通过创建一个带有不同 Modelfile 的标签模型,您可以立即将这个仓库转换成任何其他的对讲机 LLM 应用程序。根据可用资源,所使用的 LLM 也可以轻松地替换为更大或更小的模型。

语音到语音工具箱

作者使用 DALL-E 3 制作的图像。

Whisper

Whisper 是由 OpenAI 提供的开源语音转文本模型。根据应用的复杂性和所需的准确性与效率的权衡,有五种不同大小的模型可供选择,包括英语专注和多语言版本。Whisper 是一个端到端的语音转文本框架,采用编码器-解码器变压器架构,处理输入音频,将其分割成 30 秒的块并转换为对数 Mel 频谱图。该网络在多个语音处理任务上进行了训练,包括多语言语音识别、语音翻译、口语语言识别和语音活动检测。

来自 研究论文 的 Whisper 架构图。

对于这个项目,用户可以使用两个对讲机按钮:一个将他们的普通英语问题通过更轻量、更快速的“基础”模型发送到机器人,另一个则部署更大的“中型”多语言模型,能够区分数十种语言并准确转录正确发音的语句。在语言学习的背景下,这促使用户更加专注于自己的发音,从而加速学习过程。下面是可用 Whisper 模型的图表:

来自 github.com/openai/whisper 的图表

Ollama

存在各种非常有用的开源语言模型接口,它们针对不同的使用场景提供不同复杂度的设置和使用方式。最广为人知的包括 oobabooga text-gen webui,它无疑提供了最多的灵活性和底层控制,llama.cpp,最初专注于在小型仅支持 CPU 的设备上优化量化模型的部署,但后来扩展到支持其他硬件类型,以及本项目所选的简化界面(基于 llama.cpp):Ollama

Ollama 侧重于简洁性和高效性,能够在后台运行,并能够同时在小型硬件上服务多个模型,根据需要快速将模型载入和移出内存以响应请求。Ollama 的重点不是低级工具,如微调,而是擅长 简单安装、高效的运行时、丰富的 现成可用模型,以及 导入预训练模型权重的工具。对效率和简洁性的关注使得 Ollama 成为像 LingoNaut 这样的项目中 LLM 接口的自然选择,因为用户无需记得关闭会话以释放资源,因为 Ollama 会在应用程序不使用时自动在后台进行管理。此外,库中对高效、量化模型的便捷访问非常适合无摩擦地开发像 LingoNaut 这样的 LLM 应用。

尽管 Ollama 技术上并未专为 Windows 构建,但 Windows 用户可以轻松地通过 Windows 子系统(WSL)在 Windows 上安装它,并通过 Windows 应用程序与服务器进行通信。安装 WSL 后,打开 Linux 终端并输入一行 Ollama 安装命令。安装完成后,只需在 Linux 终端中运行“ollama serve”,然后就可以通过任何 Windows 机器上的 Python 脚本与 Ollama 服务器进行通信。

Coqui.ai 🐸 TTS

TTS 是一个功能齐全的文本转语音库,供非商业用途使用,同时也提供商业许可证。该库非常受欢迎,截至目前在 GitHub 上已有 3k 次分叉和 26.6k 个星标,这也不难理解:该库就像文本转语音领域的 Ollama,提供了一个统一的接口来访问各种高效的模型,涵盖了多种使用场景(例如:为本项目提供一个多说话人、多语言模型)、令人兴奋的功能,如声音克隆,以及对转录速度和情感语调的控制。

TTS 库提供了丰富的文本转语音模型选择,包括 Facebook 研究的“大规模多语言语音”(MMS)项目中的著名 Fairseq 模型。对于 LingoNaut,Coqui.ai 团队的XTTS模型最终被证明是正确的选择,因为它能够无缝地生成多语言的高质量语音。虽然该模型确实有一个“语言”输入参数,但我发现即便将其设置为“en”表示英语,并且仅传递其他语言的文本,依然能生成准确的多语言语音,且发音大多数情况下正确。

结论

图像由作者使用 DALL-E 3 生成。

在本文中,我介绍了一款新的语音到语音多语言语言学习助手——LingoNaut。该应用通过终端运行,使用轻量且易于适配的 Python 脚本,并配有对讲机键盘界面。这个完全免费的本地托管应用允许用户利用人工智能练习多种语言,在不需要在准备好之前与陌生人练习的情况下,增强对新语言的信心。代码已在GitHub上公开,并提供了快速设置说明,同时也容易扩展到新的应用场景。希望社区能在语言学习中从这款应用中受益,并且希望这段代码能作为未来概念验证的便捷轻量框架。LingoNaut 是开源的,欢迎贡献。

未来工作

本项目通过将大型语言模型(LLM)的基于文本的对话能力与输入和输出端分别独立的语音转文本和文本转语音模型相结合,构建了一个语音到语音的流程。这种设计比较笨重,容易产生级联错误,因此比使用真正的多模态语言模型更为逊色,后者能够从统一的表示空间中理解和生成音频与文本标记。当我们在将音频传递给模型之前将其编码为文本时,我们丧失了音频中的所有音调信息,包括发音和情感表达,这显著限制了我们的语言助手的先进性。通过使用在联合多模态表示空间上操作的 LLM,我们可以保留用户输入中的微妙音调信息。同样,在输出端进行文本转语音编码也是一个重要的信息瓶颈,导致交互不够自然。

NeXT-GPT的作者提供了一个有前景的框架,用于利用预训练的 LLM(大语言模型)创建大型的多模态模型(LMM),这些模型可以在统一的多模态表示空间中运行,这对于语音到语音的应用程序来说是一个有前景的方向。经过一些努力,发布的 NeXT-GPT 权重可能会被导入到 Ollama 中进行实验。他们的实验使用了一个大小相似的 Vicuna 7B LLM,证明了轻量级 LLM 可以在多模态空间中工作。虽然 Vicuna 模型并未宣传为多语言模型,但 LingoNaut 实验中使用的 Mistral 7B 模型也没有宣传为多语言模型,尽管它似乎在此目的下表现得相当不错。理想情况下,经过微调的多语言指令调优模型将是 LingoNaut 的最佳选择。为此,选择合适的数据集和低秩适配(LoRA)可能会导致成功。此外,LaVIN展示的轻量级适配器与参数高效的多模态对齐提供了使 NeXT-GPT 风格的 LMM 开发在有限资源下更具可达性的可能。第一步将是调查如何使用 LaVIN 的“廉价且快速”的模态混合适应(MMA)训练策略,将高质量音频编码器的表示与 LLM 对齐,从而缓解输入端的语音转文本瓶颈。接下来的步骤是调查如何使用 NeXT-GPT 风格的模态切换指令调优(MoSIT)来实现多模态输出。

最后,如引言中所提到的,构建一个可以与远程后端通信的 Web UI 前端,将大大扩展 LingoNaut 的乐趣,因为更大的模型可以部署在租用的云 GPU 上,并通过笔记本和移动设备的 https 请求进行通信。这将允许社区构建任何他们想象中的对讲机 LLM 应用程序,让朋友和家人在任何有网络连接的地方都能使用,从而将普遍可访问的语言学习助手的愿景变为现实。

通用语——基于实体感知的机器翻译方法,用于知识图谱上的问答

原文:towardsdatascience.com/lingua-franca-entity-aware-machine-translation-approach-for-question-answering-over-knowledge-e2c7e481c870?source=collection_archive---------12-----------------------#2024-01-29

朝着知识图谱问答系统的通用语发展

Aleksandr PerevalovTowards Data Science Aleksandr Perevalov

·发表于 Towards Data Science ·阅读时间 7 分钟·2024 年 1 月 29 日

--

TLDR

机器翻译(MT)可以增强现有的问答(QA)系统,这些系统的语言能力有限,通过使其支持多种语言来提高其功能。然而,机器翻译有一个主要缺点——它通常在翻译那些不能逐字翻译的命名实体时失败。例如,电影《教皇必须死》的德语片名是“Ein Papst zum Küssen”,其字面翻译是:“一个待吻的教皇”。由于命名实体的正确性对问答系统至关重要,因此必须妥善处理这一挑战。在本文中,我们提出了一种名为“Lingua Franca”的实体感知机器翻译方法。它利用知识图谱中的信息来确保命名实体翻译的准确性。没错,它能有效工作!

挑战

实现高质量的翻译在很大程度上依赖于准确翻译句子中的命名实体(NE)。已经提出了各种方法来增强命名实体的翻译,包括集成知识图谱(KG)来改善实体翻译的方法,这些方法承认实体在整体翻译质量中的关键作用,特别是在问答系统的上下文中。值得注意的是,命名实体翻译的质量并不是一个孤立的目标;它对涉及信息检索(IR)或基于知识图谱的问答(KGQA)等任务的系统有更广泛的影响。本文将深入讨论机器翻译(MT)和基于知识图谱的问答(KGQA)。

KGQA 系统的重要性在于它们能够基于结构化数据为用户提供事实性答案(见下图)。

Google 直接回答功能的截图(由作者提供)

KGQA 系统是现代搜索引擎的核心组件,使其能够为用户提供直接答案(Google 搜索,截图由作者提供)。

此外,多语言 KGQA 系统在解决 Web 上的“数字语言鸿沟”中发挥着至关重要的作用。例如,涉及德国的 Wikipedia 文章,特别是与城市或人物相关的文章,其中包含的信息在德语中的比例高于其他语言——这种信息不平衡可以通过多语言 KGQA 系统加以处理,顺便说一句,该系统是所有现代搜索引擎的核心。

使 KGQA 系统能够用不同语言回答问题的选项之一是使用机器翻译(MT)。然而,现成的 MT 在翻译命名实体(NEs)时面临显著挑战,因为许多实体无法直接翻译,需要背景知识才能准确解读。例如,考虑电影《教皇必须死》的德语片名,“Ein Papst zum Küssen”。字面翻译为“一个待吻的教皇”,这突显了超越简单翻译方法的上下文理解需求。

鉴于传统机器翻译方法在翻译实体时的局限性,将 KGQA 系统与机器翻译相结合通常会导致命名实体失真,显著降低准确回答问题的可能性。因此,需要一种增强的方法,将多语言背景知识整合到命名实体中。

我们的方法

本文介绍并实现了 一种新的命名实体感知机器翻译(NEAMT)方法,旨在增强 KGQA 系统的多语言能力。NEAMT 的核心概念是通过结合知识图谱(例如 WikidataDBpedia)中的信息来提升机器翻译的质量。这是通过使用“实体替换”技术实现的。

作为评估数据,我们使用了 QALD-9-plusQALD-10 数据集。然后,我们使用 NEAMT 框架中的多个组件,这些组件可在我们的 仓库 中获取。最后,该方法在两个 KGQA 系统上进行了评估:QAnswerQanary。该方法的详细描述请参见下图。

KGQA 过程中的 Lingua Franca 方法概述(图由作者提供)

从本质上讲,我们的方法在翻译过程中使用实体替换技术保留已知的命名实体。随后,这些实体会被从知识图谱中提取出的相应标签替换为目标翻译语言中的标签。这个细致的过程确保了问题在 KGQA 系统回答之前的精确翻译。

根据我们之前的文章的见解,我们将英语指定为共同的目标翻译语言,从而将我们的方法命名为“Lingua Franca”(灵感来自于“桥梁”或“联结”语言的含义)。需要特别指出的是,我们的框架具有多功能性,可以无缝地适应任何其他语言作为目标语言。重要的是,Lingua Franca 不仅限于 KGQA 的范畴,还可以应用于各种面向实体的搜索应用。

Lingua Franca 方法包括三个主要步骤:(1)命名实体识别(NER)和命名实体链接(NEL),(2)基于已识别命名实体应用实体替换技术,(3)利用机器翻译工具生成目标语言文本,同时考虑前述步骤中的信息。在这里,英语始终作为目标语言,与相关研究一致,认为这是问答(QA)质量的最优策略。然而,该方法不限于英语,如有需要,可以使用其他语言。

该方法作为开源框架实现,允许用户通过集成自定义的 NER、NEL 和 MT 组件来构建自己的命名实体感知机器翻译(NEAMT)管道(请参见我们的GitHub)。Lingua Franca 方法在所有设置下的详细信息在下面的示例中进行了说明,如下图所示。

Lingua Franca 方法在多个设置下的详细表示(作者提供的图示)

本研究的实验结果强烈支持 Lingua Franca 在与 KGQA 系统结合时优于标准机器翻译工具的观点。

实验结果

在评估每个实体替换设置时,计算了通过机器翻译工具处理后占位符或命名实体标签的损坏率。该比率作为该方法相关管道中实际命名实体翻译质量的指标。更新后的统计数据如下:

  • 设置 1(类似字符串的占位符):6.63%的占位符丢失或损坏。

  • 设置 2(数值占位符):2.89%的占位符丢失或损坏。

  • 设置 3(在翻译前用英文标签替换命名实体):6.16%的标签被损坏。

因此,通过我们的方法,我们可以自信地断言,在文本中识别的 NEs 中,最多有 97.11%(设置 2)被正确翻译。

我们分析了关于 QA 质量的结果,同时考虑了以下实验组件:一种方法管道或标准 MT 工具、源语言和 KGQA 基准。下图展示了方法与标准 MT 的对比——这些结果可以被解释为一种消融研究。

我们实验的宏观 F1 得分分组柱状图(按作者)

分组柱状图展示了关于每种语言和拆分的宏观 F1 得分(使用Gerbil-QA获得的)。在消融研究的背景下,每组包含两根柱状条:第一根柱条属于我们提出的最佳方法,而第二根柱条反映了标准 MT 工具(基准)的表现。

我们观察到,在大多数实验案例中(24 个中的 19 个),使用我们方法的 KGQA 系统的表现超过了使用标准 MT 工具的 KGQA 系统。为了验证上述结论,我们对相同数据进行了 Wilcoxon 符号秩检验。基于检验结果(p 值 = 0.0008,α = 0.01),我们拒绝了零假设,该假设表示 QA 质量结果没有差异,即将 KGQA 与标准 MT 结合与将 KGQA 与我们的方法结合没有区别。因此,我们得出结论,依赖我们 NEAMT 框架的方法显著提高了在回答多语言问题时的 QA 质量,相比标准 MT 工具。

实验的可重复性通过重复实验并计算所有 QA 质量指标之间的 Pearson 相关系数来确保。结果的系数为 0.794,表示强相关与非常强相关之间的边界值。因此,我们假设我们的实验是可重复的。

结论

本文介绍了一种名为Lingua Franca的 NEAMT 方法。Lingua Franca 旨在增强多语言能力,并与标准机器翻译工具相比提高问答质量,它专为 KGQA 系统设计,目的是扩大其潜在用户范围。Lingua Franca 的实现和评估采用了作者开发的模块化 NEAMT 框架,详细信息可参见“实验”部分。本文的主要贡献包括:(1)据我们所知,首次将 NEAMT 方法(即 Lingua Franca)与 KGQA 结合;(2)提出了一个开源模块化 NEAMT 框架,使研究社区能够构建自己的机器翻译管道;(3)进行了一项全面的评估和消融研究,展示了 Lingua Franca 方法的有效性。

在未来的工作中,我们计划扩展实验设置,涵盖更广泛的语言、基准和 KGQA 系统。为了处理实体替换过程中的损坏占位符,我们计划使用这些数据对机器翻译模型进行微调。此外,我们还将进行更详细的错误分析,重点分析错误传播。

请不要忘记查看我们的完整研究论文以及GitHub 仓库

致谢

本研究得到了德国联邦教育与研究部(BMBF)资助,资助编号为 01IS17046 和 01QE2056C,同时也得到了德国北莱茵-威斯特法伦州文化与科学部(MKW NRW)资助,资助编号为 NW21–059D。本研究还在 QA4CB 研究项目内获得资助,该项目旨在开发用于扩展聊天机器人框架的问答组件。

链表 — 数据结构与算法(面向数据科学家)

原文:towardsdatascience.com/linked-lists-data-structures-algorithms-for-data-scientists-e559d08e0144?source=collection_archive---------4-----------------------#2024-10-21

链表和队列是如何在幕后工作的

Egor HowellTowards Data Science Egor Howell

·发表于 Towards Data Science ·6 分钟阅读·2024 年 10 月 21 日

--

图片由 Luca Bravo 提供,来源于 Unsplash

在上一篇关于数组的文章之后,我想讨论链表数据结构及其一些操作。

## 数组 — 数据结构与算法(面向数据科学家)

动态和静态数组是如何在幕后工作的

towardsdatascience.com

作为数据科学家,尽管我们不一定需要了解数据结构和算法的每个细节,但这仍然是任何技术专业人员非常实用的技能集。

这就是为什么我会创建一系列文章,讨论核心数据结构以及它们如何在幕后工作。

什么是链表?

链表是一种数据结构,其中每个元素(称为节点)都按顺序存储/工作。然而,链表不像数组那样在内存中连续存储。

每个节点有两部分内容:

  • 数据 — 该节点所包含的值

  • 引用 — 节点包含对序列中下一个节点的引用。

lintsampler:一种快速从任何分布中获取随机样本的新方法

原文:towardsdatascience.com/lintsampler-a-new-way-to-quickly-get-random-samples-from-any-distribution-d07f73a70f5c?source=collection_archive---------8-----------------------#2024-10-14

Aneesh NaikTowards Data Science Aneesh Naik

·发表于 Towards Data Science ·阅读时间 4 分钟·2024 年 10 月 14 日

--

lintsampler是一个纯 Python 包,可以轻松高效地从任何概率分布中生成随机样本。

完整披露:我是lintsampler的作者之一。

为什么你需要 lintsampler

我们经常遇到需要从给定概率分布(PDF)中抽取随机样本的情况。例如,我们可能想估算一些总结性统计量,或者为模拟创建一群粒子。

如果概率分布是标准分布,如均匀分布或高斯(正态)分布,那么numpy/scipy生态系统提供了一些简便的方法来从这些分布中抽取样本,方法是通过numpy.randomscipy.stats模块。

然而,在实际应用中,我们经常遇到非高斯的概率分布。有时,它们非常非高斯。例如:

一个非常非高斯的 PDF。等高线是密度相等的线,在对数空间中按相等间隔分隔。图片来源:作者。

我们如何从这个分布中抽取样本呢?

有几种广泛使用的技术可以从任意分布中抽取样本,比如拒绝采样马尔可夫链蒙特卡罗(MCMC)。这些都是优秀且可靠的方法,并且有一些方便的 Python 实现。例如,emcee是一个在科学应用中广泛使用的 MCMC 采样器。

现有技术的问题在于它们需要相当多的设置和调试。使用拒绝采样时,必须选择一个提议分布,而不合适的选择会使得过程非常低效。使用 MCMC 时,必须担心样本是否已收敛,这通常需要进行一些后验测试来评估。

输入 lintsampler。操作简单如同:

from lintsampler import LintSampler
import numpy as np

x = np.linspace(xmin, xmax, ngrid)
y = np.linspace(ymin, ymax, ngrid)
sampler = LintSampler((x, y), pdf)
pts = sampler.sample(N=100000)

在这段代码中,我们沿着两个维度构造了 1D 数组,然后将它们与表示我们想从中抽取样本的概率分布的 pdf 函数一起传递给 LintSampler 对象(从 lintsampler 包中导入)。我们在这个片段中没有列出 pdf 函数,但在 docs 中有一些完全自包含的示例。

现在,pts 是一个包含来自 PDF 的 100000 个样本的数组。以下是它们的散点图:

从上面奇怪的 PDF 中采样的点的散点图(后者由轮廓线表示)。图片由作者提供。

本例的重点是演示如何轻松地设置和使用 lintsampler。在某些情况下,它也比 MCMC 和/或拒绝采样要快得多且高效。如果你有兴趣了解 lintsampler 背后的工作原理,请继续阅读。否则,可以访问 docs,那里有描述 如何安装 和使用 lintsampler 的说明,包括带有 1D2D3D 用例的示例笔记本,以及对一些 lintsampler 附加功能的描述: 准蒙特卡洛采样(又称低差异序列)和在 自适应树结构 上的采样。此外,还有一篇发表于《开源软件杂志》(JOSS) 的 论文,描述了 lintsampler

lintsampler 的工作原理

lintsampler 背后是我们称之为 线性插值采样 的算法。文档的 理论 部分提供了一个更详细且更具数学性的算法描述,但这里简要说明一下。

下面的示例演示了当你将 PDF 和网格传递给 LintSampler 类时,lintsampler 背后发生了什么。我们将以 2D 高斯分布的简单示例为例,但这种方法适用于任意维度,并且适用于更不友好的 PDF。

  • 首先,在网格上评估概率密度函数。在下面的示例中,网格的间距不均匀,仅供娱乐。

左:2D 高斯概率密度函数(PDF)。右:在(不均匀)网格上评估的概率密度函数。图片来自作者。

  • 通过这种方式在网格上评估概率密度函数后,我们可以根据梯形法则估算每个网格单元的总概率(即,单元的体积乘以其角点密度的平均值)。

  • 在每个网格单元内,我们可以通过双线性插值在单元角点之间近似概率密度函数:

使用(双)线性插值填充的网格化概率密度函数。图片来自作者。

  • 这种对概率密度函数的线性近似可以非常高效地进行采样。绘制一个单一的样本是一个两步过程,如下图所示。首先,从按概率加权的单元列表中选择一个随机单元(左侧面板)。接下来,通过逆变换采样在单元内采样一个点(右侧面板)。

左:与前一图相同,随机选择的单元被高亮显示。右:高亮单元的放大图,显示采样点。图片来自作者。

值得理解的是,这里的关键步骤是线性近似:我们将在lintsampler文档中描述这一点,以及逆变换采样过程的更多细节。将概率密度函数近似为每个网格单元内的线性函数意味着它具有封闭的、解析的形式用于其分位数函数(即其逆累积分布函数),这意味着进行逆变换采样本质上归结为绘制均匀样本并对其应用代数函数。

用户需要关心的主要问题是获得合适的网格分辨率,以便线性近似足够准确。什么是合适的分辨率会因使用案例而异,正如在lintsampler文档中的一些示例笔记本所展示的那样。

祝你采样愉快!

使用 FFmpeg 增强数据讲述的实时图表

原文:towardsdatascience.com/live-graphs-with-ffmpeg-to-enhance-your-data-storytelling-61cc12529382?source=collection_archive---------4-----------------------#2024-04-15

使用这个 FFmpeg 教程来动画化你的数据可视化

Fouad FarajTowards Data Science Fouad Faraj

·发表于 Towards Data Science ·阅读时间 4 分钟·2024 年 4 月 15 日

--

2019–2024 年间 S&P 500 值的静态与动态图表对比。图片由作者提供

介绍

有大量开源的数据可视化库可用于创建图表,但大多数在创建动态移动图表方面功能有限。最常见的方法是生成多张图像,并通过在线转换器将它们合并成一个.mp4 或.gif 文件,这些转换器受到各种限制,只有付费才能避免这些限制。

以下是我为免费在线转换器发现的一些限制

  • 最大上传 200 MB

  • 最大上传 100 张图片

  • 分辨率最高为 800 像素

  • 输出视频带水印

锁链图示,说明了免费在线转换器的功能限制。照片由Jose Fontano提供,来源于Unsplash

FFmpeg是一个开源软件工具,用于高效地处理音频和视频数据。FFmpeg 的全称是Fast Forward Moving Picture Experts Group,并被 VLC 和 YouTube 等大型公司在后台使用。尽管它在这些大应用程序中被广泛使用,但对于典型的数据科学家来说,它仍然是一个大多未被使用的工具。

Llama-2 与 Llama-3:模型间的井字游戏对决

原文:towardsdatascience.com/llama-2-vs-llama-3-a-tic-tac-toe-battle-between-models-7301962ca65d?source=collection_archive---------5-----------------------#2024-04-27

使用 Python 和 Llama-CPP 进行非科学性基准测试

Dmitrii EliuseevTowards Data Science Dmitrii Eliuseev

·发表于 Towards Data Science ·阅读时长 10 分钟·2024 年 4 月 27 日

--

图片来源:Solstice Hannan,来自 Unsplash

在撰写本文的前一周,Meta 发布了新的开源 Llama-3 模型,可以在此查看。Meta 宣称,这些是“目前 8B 和 70B 参数规模上最好的模型。”例如,依据HuggingFace 模型页面,Llama-3 8B 在 MMLU(大规模多任务语言理解基准)中的得分为 66.6,而 Llama-2 7B 为 45.7。Llama-3 还在常识问答数据集(CommonSense QA)中获得了 72.6 的得分,而 Llama-2 为 57.6。经过指令调优的 Llama-3 8B 模型在数学基准测试中获得了 30.0 的得分,而 Llama-2 仅为 3.8,这确实是一个令人印象深刻的改进。

学术基准很重要,但我们能看到真正的“实际差异”吗?显然,我们可以,而且这可能很有趣。让我们编写一个井字游戏,让两个模型对战,看看哪个会赢!在游戏过程中,我将测试所有的 7B、8B 和 70B 模型。同时,我还会收集一些关于模型性能和系统需求的数据。所有测试都可以在 Google Colab 中免费运行。

让我们开始吧!

加载模型

为了测试所有模型,我将使用Llama-cpp Python 库,因为它可以在 CPU 和 GPU 上运行。我们…

Llama 是开源的,但为什么?

原文:towardsdatascience.com/llama-is-open-source-but-why-3f87d290d0d5?source=collection_archive---------5-----------------------#2024-06-25

观点

Meta 开源大模型战略分析

Haifeng JinTowards Data Science Haifeng Jin

·发表于 Towards Data Science ·6 分钟阅读·2024 年 6 月 25 日

--

图片由作者使用 DALL-E 创建

训练一个大语言模型可能花费数百万美元。Meta 为什么会花这么多钱训练一个模型,并且让所有人免费使用?

本文分析了 Meta 的 GenAI 和大模型战略,旨在理解开源大模型的考虑因素。我们还讨论了这波开源模型如何与传统开源软件相似,又有何不同。

免责声明:Llama 模型是否真正开源超出了本文的讨论范围。所有信息均来自公开来源。

专有模型的幻象

如果 Meta 开源它的模型,难道人们不会选择自己构建服务,而不是付费使用 Meta 提供的服务(例如,基于 Llama 的 Meta AI 聊天机器人,API,或者帮助你微调模型并高效提供服务)吗?

通过将模型保持为专有,来阻止人们构建自己的解决方案,这不过是一个幻象。无论是否开源你的模型,其他公司,如 Mistral AI阿里巴巴,甚至 谷歌,都已开源了他们的模型。

目前,OpenAI、Anthropic 和谷歌并没有开源它们最大/最好的模型,因为它们仍然认为自己处于一个开源模型无法企及的领域,无论是能力还是质量。开源这些模型将对他们的业务造成损害。

除非你的模型比其他任何开源模型好几个数量级,否则是否开源你的模型不会影响用户在开源模型上构建应用程序的质量。

你唯一的选择是成为开源模型的首创者和领导者,或者成为一个追随者,在稍后发布你的模型。

为什么要成为开源模型的领导者?

成为开源模型的领导者有许多好处,但最重要的就是吸引人才。

GenAI 的战争是被计算能力瓶颈限制的人才竞争。你获得多少计算能力在很大程度上取决于你与 Nvidia 的现金流关系,除了谷歌。然而,拥有多少人才则是另一个问题。

根据埃隆·马斯克的说法,谷歌拥有三分之二的 AI 人才,为了对抗谷歌的力量,他们创办了 OpenAI。随后,一些最顶尖的人才离开了 OpenAI,创办了 Anthropic,专注于 AI 安全。因此,目前市场上这三家公司拥有最优秀、最多的 AI 专家。其他公司都急需更多的 AI 专家。

成为开源模型的领导者将帮助 Meta 弥合 AI 专家的差距。开源模型通过两种方式吸引人才。

首先,AI 专家们想为 Meta 工作。让全世界都使用你构建的模型是超级酷的。这会为你的工作带来大量曝光,扩大你的专业影响力,并且对你未来的职业生涯有好处。所以,许多有才华的人愿意为他们工作。

其次,社区中的 AI 专家们为 Meta 做了免费的工作。在 Llama 发布后不久,人们开始对其进行实验。他们帮助你开发新的服务技术以降低成本,微调模型以发现新应用,并仔细审查模型以发现漏洞,提升其安全性。例如,根据这篇文章,他们在 Llama 最初发布后一个月内进行了指令调优、量化、质量改进、人类评估、多模态和 RLHF。将这项工作交给社区,帮助 Meta 节省了大量的计算和人力资源。

与社区快速迭代。

使用开源模型,Meta 可以通过直接将其新开发的方法融入其中,迅速与社区一起进行迭代。

如果谷歌要采用社区的新方法,成本会有多高?这个过程分为两个阶段:实现和评估。首先,他们需要重新实现该方法以适配 Gemini。这涉及到用 JAX 重写代码,需要大量的工程资源。在评估阶段,他们需要对其进行一系列基准测试,这又需要大量的计算能力。最重要的是,这需要时间。当最新技术首次可用时,它们无法立即进行迭代。

相反,如果 Meta 想要采纳社区的一个新方法,对他们来说几乎没有成本。社区已经直接对 Llama 模型做了实验和基准测试,因此不需要进一步评估。代码是用 PyTorch 编写的,他们可以直接复制并粘贴到自己的系统中。

Llama 在 Meta 和社区之间建立了一个飞轮。Meta 从社区引入最新技术,并将其下一代模型推向社区。PyTorch 是他们共同使用的语言。

他们还能赚钱吗?

模型是开源的。人们不会直接建立自己的服务吗?为什么他们还要为一个建立在开源模型上的服务付费给 Meta 呢?当然会。即使是开源模型,构建服务依然很困难。

你如何微调和调整模型以适应你的特定应用?你如何平衡服务成本和模型质量?你是否了解所有技巧,能够充分利用你的 GPU?

知道这些问题答案的人很难找到,且招聘成本高。即使有足够的人力,想要获得足够的计算能力来微调和服务模型也很难。试想一下,如何从开源的 Llama 模型构建 Meta AI。我预计需要数百名员工和大量 GPU 参与其中。

所以,如果将来有任何类似的 Meta GenAI 服务,人们仍然可能会为其付费。

这就像开源软件,但又不完全是。

情况与传统的开源软件非常相似。"免费代码,付费服务" 这一框架依然适用。代码或模型是免费的,用来吸引更多用户加入生态系统。随着生态系统的扩大,拥有者能收获更多的利益。建立在免费代码之上的服务则是为了盈利。

然而,它也并不像开源软件。主要的区别可以总结为低用户留存率和一种新型的生态系统。

用户留存率低

开源模型的用户留存率较低。迁移到新模型比迁移到新软件要容易得多。

迁移软件很困难。PyTorch 和 HuggingFace 为深度学习框架和模型池建立了强大的生态系统。试想一下,如果你创建一个新的深度学习框架或模型池来与他们竞争,想要稍微改变他们的主导地位有多么困难。

一个很好的例子是 JAX。它对大规模分布式训练提供了更好的支持,但由于生态系统和社区较小,很难吸引用户使用 JAX。它缺乏一个能帮助用户解决问题的有力社区。而且,将整个基础设施迁移到新的框架的工程成本对大多数公司来说太高了。

开源模型没有这些问题。它们易于迁移,几乎不需要用户支持。因此,人们可以轻松转向最新和最好的模型。要在开源模型中保持领导地位,你必须不断发布位于排行榜顶部的新模型。这也是成为开源模型领导者的一大挑战或缺点。

一种新型的生态系统

开源模型创造了一种新型的生态系统。与开源软件创造贡献者和新软件生态系统不同,开源模型创造了微调和量化模型的生态系统,这些模型可以看作是原始模型的分支。

因此,一个开源基础模型不必在每个具体任务上都表现得非常优秀,因为用户可以通过领域特定的数据对其进行微调以适应他们的应用需求。基础模型最重要的特点是能够满足用户的部署要求,例如推理时的低延迟,或者足够小以适应终端设备。

这就是为什么 Llama 为每个版本提供多个尺寸的原因。例如,Llama-3 有三个版本:8B、70B 和 400B。他们希望确保涵盖所有的部署场景。

总结

即使 Meta 不开源他们的模型,其他公司也会开源。所以,Meta 提前开源并领导开源模型将是明智之举。然后,Meta 可以与社区快速迭代,改进其模型,赶上 OpenAI 和 Google。

在开源你的模型时,不必担心人们不使用你的服务,因为基础模型与构建良好的服务之间仍然存在巨大的差距。

开源模型类似于开源软件,它们都遵循“自由代码付费服务”框架,但在用户留存率和所创建的生态系统类型上有所不同。

未来,我预计会看到更多来自更多公司的开源模型。与已集中在 PyTorch 上的深度学习框架不同,开源模型将在很长一段时间内保持多样性和竞争力。

LLaVA:一个开源的 GPT-4V(ision)替代方案

原文:towardsdatascience.com/llava-an-open-source-alternative-to-gpt-4v-ision-b06f88ce8efa?source=collection_archive---------2-----------------------#2024-01-23

在 Web、本地和 Google Colab 上运行 LLaVA

Yann-Aël Le BorgneTowards Data Science Yann-Aël Le Borgne

·发布于 Towards Data Science ·7 分钟阅读·2024 年 1 月 23 日

--

好奇这张图片拍摄于哪里吗?问问 LLaVA 吧!(图片来自Guy Rey-Bellet,来自Pixabay)。

LLaVALarge Language 和 Visual Assistant 的缩写)是一个有前途的开源生成型 AI 模型,它复制了 OpenAI GPT-4 在与图像对话方面的一些能力。用户可以将图片添加到 LLaVA 的聊天对话中,不仅能够讨论这些图片的内容,还可以利用它们以视觉方式描述想法、情境或场景。

LLaVA 最引人注目的特点是,它能够在使用更简单的模型架构和数量级更少的训练数据的情况下,超越其他开源解决方案。这些特点使得 LLaVA 不仅在训练上更快速且成本更低,而且更适合在消费级硬件上进行推理。

本文概述了 LLaVA,并更具体地旨在

  • 展示如何通过网络界面进行实验,以及如何在您的计算机或笔记本电脑上安装它

  • 解释它的主要技术特点

  • 通过一个简单的聊天机器人应用示例,展示如何用它编程,该应用是基于 HuggingFace 库(TransformersGradio)在 Google Colab 上构建的。

使用 LLaVA 在线

如果你还没有尝试过,使用 LLaVA 的最简单方法是访问其作者提供的Web 界面。下面的截图展示了界面如何操作,其中用户根据冰箱内容的图片询问可以做哪些餐点的建议。用户可以通过左侧的控件上传图片,聊天界面允许提问并以文本形式获取答案。

LLaVA Web 界面

在这个例子中,LLaVA 正确识别出了冰箱中的食材,例如蓝莓、草莓、胡萝卜、酸奶或牛奶,并建议了相关的食谱创意,比如水果沙拉、果昔或蛋糕。

与 LLaVA 的其他对话示例可在项目网站上查看,这些示例说明了 LLaVA 不仅能够描述图像,还能够根据图像中的元素进行推理和推断(例如通过图片中的线索识别电影或人物、根据画图编码网站、解释幽默情况等)。

本地运行 LLaVA

LLaVA 还可以使用Ollama或 Mozilla 的‘llamafile’安装到本地计算机。这些工具可以在大多数仅配备 CPU 的消费级机器上运行,因为模型仅需要 8GB 内存和 4GB 的可用磁盘空间,甚至已经证明可以在 Raspberry PI 上成功运行。在围绕 Ollama 项目开发的工具和接口中,一个值得注意的项目是Ollama-WebUI(如下图所示),它复现了 OpenAI ChatGPT 用户界面的外观和操作体验。

Ollama Web 用户界面 — 受OpenAI ChatGPT的启发

LLaVA 主要特性简要概述

LLaVA 是由威斯康星大学麦迪逊分校、微软研究院和哥伦比亚大学的研究人员设计的,并且最近在 NeurIPS 2023 上展示。该项目的代码和技术规格可以通过Github 仓库访问,仓库还提供了与助手互动的各种接口。

正如作者在论文摘要中总结的:

[LLaVA] 在 11 个基准测试中达到了最先进的水平。我们的最终 13B 检查点仅使用了 1.2M 公开可用的数据,并且在单个 8-A100 节点上完成了大约 1 天的全训练。我们希望这能使最先进的大型多模态模型研究变得更加易于接触。代码和模型将公开发布。

论文中报告的基准结果,如下方的雷达图,展示了与其他最先进模型的对比改进。

LLaVA 基准结果的雷达图(图片来自论文

内部工作机制

LLaVA 的数据处理工作流程在概念上非常简单。该模型本质上作为一个标准的因果语言模型工作,接受语言指令(用户文本提示)作为输入,并返回语言响应。语言模型处理图像的能力由一个独立的视觉编码器模型提供,该模型将图像转换为语言标记,这些标记被悄悄地添加到用户文本提示中(充当一种软提示)。LLaVA 的处理流程如下所示。

LLaVA 网络架构(图片来自论文

LLaVA 的语言模型和视觉编码器分别依赖于两个参考模型,称为 Vicuna 和 CLIP。Vicuna是一个基于 LLaMA-2(由 Meta 设计)的预训练大语言模型,具有与中型 LLM 相媲美的竞争性能(参见 HuggingFace 上7B13B版本的模型卡)。CLIP是由 OpenAI 设计的图像编码器,经过预训练,可以在相似的嵌入空间中对图像和文本进行编码,使用的是对比语言-图像预训练(因此称为‘CLIP’)。LLaVA 中使用的模型是视觉变换器变种 CLIP-ViT-L/14(查看其在 HuggingFace 上的模型卡)。

为了匹配视觉编码器的维度与语言模型的维度,一个投影模块(如上图中的W)被应用。它在原始的LLaVA中是一个简单的线性投影,在LLaVA 1.5中是一个两层感知机。

训练过程

LLaVA 的训练过程由两个相对简单的阶段组成。

第一阶段仅仅是为了调优投影模块W,而视觉编码器和 LLM 的权重保持冻结。训练使用来自CC3M 概念性字幕数据集的约 60 万对图像/字幕数据的子集进行,并且可以在 HuggingFace 的此仓库中找到。

在第二阶段,投影模块的权重W与 LLM 的权重一起进行微调(同时保持视觉编码器的权重冻结),使用包含 158K 语言-图像指令跟随数据的训练集。这些数据是通过 GPT-4 生成的,包含对话、详细描述和复杂推理的示例,并且可以在 HuggingFace 的此仓库中找到。

整个训练过程大约需要一天时间,使用八个 A100 GPU。

使用 LLaVA 进行编程:如何开始

代码可在 Colab 相关笔记本.

LLaVA 模型已集成在 Transformers 库中,可以通过标准的pipeline对象加载。7B 和 13B 版本的模型可以在LLaVA 😊 Hub 空间上找到,并可以以 4 位和 8 位加载,以节省 GPU 内存。下面我们展示如何使用代码加载并运行模型,代码可以在 Colab 上使用 T4 TPU(15GB 内存 GPU)执行。

以下是加载 4 位 LLaVA 1.5 7B 版本的代码片段:

from transformers import pipeline, BitsAndBytesConfig
import torch

quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.float16
)

model_id = "llava-hf/llava-1.5-7b-hf"

pipe = pipeline("image-to-text", model=model_id, model_kwargs={"quantization_config": quantization_config})

然后让我们加载这张图片

我们使用标准的 PIL 库来加载这张图片:

import requests
from PIL import Image

image_url = "https://cdn.pixabay.com/photo/2018/01/29/14/13/italy-3116211_960_720.jpg"
image = Image.open(requests.get(image_url, stream=True).raw)
image

最后,我们使用图像查询 LLaVA 模型,并提供一个提示请求描述这张图片。

注意:提示的格式如下

“USER: \n\nASSISTANT:”

prompt = "USER: <image>\nDescribe this picture​​\nASSISTANT:"

outputs = pipe(image, prompt=prompt, generate_kwargs={"max_new_tokens": 200})
print(outputs[0]['generated_text']) 

返回以下答案:

USER: 描述这张图片

​ASSISTANT: ​这张图片展示了一个大型、空旷的剧场,背景是壮丽的海洋景色。剧场四周被郁郁葱葱的绿色山坡环绕,远处可以看到一座雄伟的山脉。整个场景宁静而如画,阳光明媚地照耀在大地上。

LLaVA 聊天机器人

最后,让我们创建一个简单的聊天机器人,依赖于 LLaVA 模型。我们将使用Gradio 库,它提供了一种快速简便的方法来创建机器学习 Web 界面。

界面的核心是一个包含图像上传器(Gradio 图像对象)和聊天界面(Gradio ChatInterface对象)的行。

import gradio as gr

with gr.Blocks() as demo:

    with gr.Row():
      image = gr.Image(type='pil', interactive=True)

      gr.ChatInterface(
          update_conversation, additional_inputs=[image]
      )

聊天界面连接到一个update_conversation函数,该函数负责保持对话历史记录,并在用户发送消息时调用 LLaVA 模型生成响应。

def update_conversation(new_message, history, image):

    if image is None:
        return "Please upload an image first using the widget on the left"

    conversation_starting_from_image = [[user, assistant] for [user, assistant] in history if not assistant.startswith('Please')]

    prompt = "USER: <image>\n"

    for i in range(len(history)):
        prompt+=history[i][0]+'ASSISTANT: '+history[i][1]+"USER: "

    prompt = prompt+new_message+'ASSISTANT: '

    outputs = pipe(image, prompt=prompt, generate_kwargs={"max_new_tokens": 200, "do_sample" : True, "temperature" : 0.7})[0]['generated_text']

    return outputs[len(prompt)-6:]

界面是通过调用launch方法启动的。

demo.launch(debug=True)

几秒钟后,聊天机器人 Web 界面将出现:

恭喜,你的 LLaVA 聊天机器人已经启动并运行!

有用的链接

注意:除非另有说明,所有图片均由作者提供。

LLM 代理揭秘

原文:towardsdatascience.com/llm-agents-demystified-8e2a62c185fa?source=collection_archive---------1-----------------------#2024-07-14

使用 AdalFlow 库实现 ReAct 代理

Li YinTowards Data Science Li Yin

·发表于Towards Data Science ·14 分钟阅读·2024 年 7 月 14 日

--

图片来源,感谢Growtika

AdalFlow 库:github.com/SylphAI-Inc/AdalFlow

Colab 笔记本

“自主代理是一个位于环境中的系统,能够感知该环境并在其中行动,随着时间推移,为了追求自己的目标,进而影响它所感知到的未来。”

— Franklin and Graesser (1997)

除了广为人知的 RAGs(检索增强生成模型),代理[1]是另一类受欢迎的 LLM 应用。使代理与众不同的是它们能够通过可访问的工具进行推理、规划和行动。在实现方面,AdalFlow 将其简化为一个生成器,能够使用工具,采取多步骤(顺序或并行)来完成用户查询。

什么是 ReAct 代理?

我们将首先介绍 ReAct[2],这是构建代理的一种通用范式,包含一系列交替的思考、行动和观察步骤。

  • 思考:采取行动的推理过程。

  • 行动:从预定义的行动集合中采取的行动。特别地,这些是我们在工具部分介绍的工具/功能工具。

  • 观察:最简单的场景是以字符串格式呈现的执行结果。为了更强的鲁棒性,可以通过任何提供足够执行信息的方式来定义,从而让 LLM 能够规划下一步。

提示和数据模型

DEFAULT_REACT_AGENT_SYSTEM_PROMPT是 React 代理 LLM 规划器的默认提示。我们可以将提示模板分为四个部分:

  1. 任务描述

这一部分是代理的整体角色设置和任务描述。

task_desc = r"""You are a helpful assistant.
Answer the user's query using the tools provided below with minimal steps and maximum accuracy.
Each step you will read the previous Thought, Action, and Observation(execution result of the action) and then provide the next Thought and Action."""

2. 工具、输出格式和示例

该模板部分与我们在工具中调用函数的方式完全相同。output_format_str是通过FunctionExpressionJsonOutputParser生成的。它包括实际的输出格式和FunctionExpression实例的示例列表。我们使用FunctionExpressionthoughtaction字段作为代理的回应。

tools = r"""{% if tools %}
<TOOLS>
{% for tool in tools %}
{{ loop.index }}.
{{tool}}
------------------------
{% endfor %}
</TOOLS>
{% endif %}
{{output_format_str}}"""

3. 任务规范,用于教会规划器如何“思考”。

我们提供更详细的指导,以确保代理始终以“finish”动作结束,以完成任务。此外,我们还教它如何处理简单查询和复杂查询。

  • 对于简单查询,我们指示代理尽可能少的步骤来完成。

  • 对于复杂查询,我们教代理采用“分而治之”的策略,逐步解决查询。

task_spec = r"""<TASK_SPEC>
- For simple queries: Directly call the ``finish`` action and provide the answer.
- For complex queries:
   - Step 1: Read the user query and potentially divide it into subqueries. And get started with the first subquery.
   - Call one available tool at a time to solve each subquery/subquestion. \
   - At step 'finish', join all subqueries answers and finish the task.
Remember:
- Action must call one of the above tools with name. It can not be empty.
- You will always end with 'finish' action to finish the task. The answer can be the final answer or failure message.
</TASK_SPEC>"""

我们将这三部分放在<SYS></SYS>标签内。

4. 代理步骤历史。

我们使用StepOutput来记录代理的步骤历史,包括:

  • action:这是代理预测的FunctionExpression实例。

  • observation:动作的执行结果。

特别地,我们将用户查询后的步骤历史格式化如下:

step_history = r"""User query:
{{ input_str }}
{# Step History #}
{% if step_history %}
<STEPS>
{% for history in step_history %}
Step {{ loop.index }}.
"Thought": "{{history.action.thought}}",
"Action": "{{history.action.action}}",
"Observation": "{{history.observation}}"
------------------------
{% endfor %}
</STEPS>
{% endif %}
You:"""

工具

除了用户提供的工具外,我们默认添加了一个名为finish的新工具,允许代理停止并返回最终答案。

def finish(answer: str) -> str:
   """Finish the task with answer."""
   return answer

简单地返回一个字符串可能无法满足所有场景,我们未来可能会考虑允许用户为更复杂的情况定义自己的 finish 函数。

此外,由于提供的工具不能总是解决用户查询,我们允许用户配置是否应该使用 LLM 模型来解决子查询,方法是通过add_llm_as_fallback参数进行配置。这个 LLM 将使用与代理规划器相同的模型客户端和模型参数。以下是我们指定回退 LLM 工具的代码:

_additional_llm_tool = (
   Generator(model_client=model_client, model_kwargs=model_kwargs)
   if self.add_llm_as_fallback
   else None
)

def llm_tool(input: str) -> str:
   """I answer any input query with llm's world knowledge. Use me as a fallback tool or when the query is simple."""
   # use the generator to answer the query
   try:
         output: GeneratorOutput = _additional_llm_tool(
            prompt_kwargs={"input_str": input}
         )
         response = output.data if output else None
         return response
   except Exception as e:
         log.error(f"Error using the generator: {e}")
         print(f"Error using the generator: {e}")
   return None

React Agent

我们定义了类ReActAgent来整合一切。它将协调两个组件:

  • planner:一个Generator,与JsonOutputParser配合使用,解析输出格式并提供FunctionExpression函数调用的示例。

  • ToolManager:管理给定的工具列表、finish 函数和 LLM 工具。它负责解析和执行这些函数。

此外,它还管理 step_history 作为 StepOutput 实例的列表,用于代理的内部状态。

提供输入查询并处理步骤以生成响应。

代理操作

我们将设置两组模型,Groq 提供的 llama3–70b-8192 和 OpenAI 提供的 gpt-3.5-turbo,来测试两个查询。为了对比,我们将这些与未使用代理的普通 LLM 响应进行比较。以下是代码片段:

from lightrag.components.agent import ReActAgent
from lightrag.core import Generator, ModelClientType, ModelClient
from lightrag.utils import setup_env

setup_env()

# Define tools
def multiply(a: int, b: int) -> int:
   """
   Multiply two numbers.
   """
   return a * b
def add(a: int, b: int) -> int:
   """
   Add two numbers.
   """
   return a + b
def divide(a: float, b: float) -> float:
   """
   Divide two numbers.
   """
   return float(a) / b
llama3_model_kwargs = {
   "model": "llama3-70b-8192",  # llama3 70b works better than 8b here.
   "temperature": 0.0,
}
gpt_model_kwargs = {
   "model": "gpt-3.5-turbo",
   "temperature": 0.0,
}

def test_react_agent(model_client: ModelClient, model_kwargs: dict):
   tools = [multiply, add, divide]
   queries = [
      "What is the capital of France? and what is 465 times 321 then add 95297 and then divide by 13.2?",
      "Give me 5 words rhyming with cool, and make a 4-sentence poem using them",
   ]
   # define a generator without tools for comparison
   generator = Generator(
      model_client=model_client,
      model_kwargs=model_kwargs,
   )
   react = ReActAgent(
      max_steps=6,
      add_llm_as_fallback=True,
      tools=tools,
      model_client=model_client,
      model_kwargs=model_kwargs,
   )
   # print(react)
   for query in queries:
      print(f"Query: {query}")
      agent_response = react.call(query)
      llm_response = generator.call(prompt_kwargs={"input_str": query})
      print(f"Agent response: {agent_response}")
      print(f"LLM response: {llm_response}")
      print("")

React 的结构,包括初始化参数和两个主要组件:tool_managerplanner,如下所示。

ReActAgent(
   max_steps=6, add_llm_as_fallback=True,
   (tool_manager): ToolManager(Tools: [FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='multiply', func_desc='multiply(a: int, b: int) -> int\n\n    Multiply two numbers.\n    ', func_parameters={'type': 'object', 'properties': {'a': {'type': 'int'}, 'b': {'type': 'int'}}, 'required': ['a', 'b']})), FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='add', func_desc='add(a: int, b: int) -> int\n\n    Add two numbers.\n    ', func_parameters={'type': 'object', 'properties': {'a': {'type': 'int'}, 'b': {'type': 'int'}}, 'required': ['a', 'b']})), FunctionTool(fn: , async: False, definition: FunctionDefinition(func_name='divide', func_desc='divide(a: float, b: float) -> float\n\n    Divide two numbers.\n    ', func_parameters={'type': 'object', 'properties': {'a': {'type': 'float'}, 'b': {'type': 'float'}}, 'required': ['a', 'b']})), FunctionTool(fn: .llm_tool at 0x11384b740>, async: False, definition: FunctionDefinition(func_name='llm_tool', func_desc="llm_tool(input: str) -> str\nI answer any input query with llm's world knowledge. Use me as a fallback tool or when the query is simple.", func_parameters={'type': 'object', 'properties': {'input': {'type': 'str'}}, 'required': ['input']})), FunctionTool(fn: .finish at 0x11382fa60>, async: False, definition: FunctionDefinition(func_name='finish', func_desc='finish(answer: str) -> str\nFinish the task with answer.', func_parameters={'type': 'object', 'properties': {'answer': {'type': 'str'}}, 'required': ['answer']}))], Additional Context: {})
   (planner): Generator(
      model_kwargs={'model': 'llama3-70b-8192', 'temperature': 0.0},
      (prompt): Prompt(
         template: 
         {# role/task description #}
         You are a helpful assistant.
         Answer the user's query using the tools provided below with minimal steps and maximum accuracy.
         {# REACT instructions #}
         Each step you will read the previous Thought, Action, and Observation(execution result of the action) and then provide the next Thought and Action.
         {# Tools #}
         {% if tools %}

         You available tools are:
         {# tools #}
         {% for tool in tools %}
         {{ loop.index }}.
         {{tool}}
         ------------------------
         {% endfor %}

         {% endif %}
         {# output format and examples #}

         {{output_format_str}}

         {# Task specification to teach the agent how to think using 'divide and conquer' strategy #}
         - For simple queries: Directly call the ``finish`` action and provide the answer.
         - For complex queries:
            - Step 1: Read the user query and potentially divide it into subqueries. And get started with the first subquery.
            - Call one available tool at a time to solve each subquery/subquestion. \
            - At step 'finish', join all subqueries answers and finish the task.
         Remember:
         - Action must call one of the above tools with name. It can not be empty.
         - You will always end with 'finish' action to finish the task. The answer can be the final answer or failure message.

         -----------------
         User query:
         {{ input_str }}
         {# Step History #}
         {% if step_history %}

         {% for history in step_history %}
         Step {{ loop.index }}.
         "Thought": "{{history.action.thought}}",
         "Action": "{{history.action.action}}",
         "Observation": "{{history.observation}}"
         ------------------------
         {% endfor %}

         {% endif %}
         You:, prompt_kwargs: {'tools': ['func_name: multiply\nfunc_desc: "multiply(a: int, b: int) -> int\\n\\n    Multiply two numbers.\\n    "\nfunc_parameters:\n  type: object\n  properties:\n    a:\n      type: int\n    b:\n      type: int\n  required:\n  - a\n  - b\n', 'func_name: add\nfunc_desc: "add(a: int, b: int) -> int\\n\\n    Add two numbers.\\n    "\nfunc_parameters:\n  type: object\n  properties:\n    a:\n      type: int\n    b:\n      type: int\n  required:\n  - a\n  - b\n', 'func_name: divide\nfunc_desc: "divide(a: float, b: float) -> float\\n\\n    Divide two numbers.\\n    "\nfunc_parameters:\n  type: object\n  properties:\n    a:\n      type: float\n    b:\n      type: float\n  required:\n  - a\n  - b\n', "func_name: llm_tool\nfunc_desc: 'llm_tool(input: str) -> str\n\n  I answer any input query with llm''s world knowledge. Use me as a fallback tool\n  or when the query is simple.'\nfunc_parameters:\n  type: object\n  properties:\n    input:\n      type: str\n  required:\n  - input\n", "func_name: finish\nfunc_desc: 'finish(answer: str) -> str\n\n  Finish the task with answer.'\nfunc_parameters:\n  type: object\n  properties:\n    answer:\n      type: str\n  required:\n  - answer\n"], 'output_format_str': 'Your output should be formatted as a standard JSON instance with the following schema:\n```\n{\n    "thought": "为什么该函数被调用(Optional[str])(可选)",\n    "action": "FuncName() 有效的函数调用表达式。例如:\\"FuncName(a=1, b=2)\\" 遵循函数参数中指定的数据类型。例如,对于具有 x、y 属性的 Type 对象,使用 \\"ObjectType(x=1, y=2)(str)(必填)"\n}\n```py\nExamples:\n```\n{\n    "thought": "我已完成任务。",\n    "action": "finish(answer=\\"final answer: \'answer\'\\")"\n}\n________\n```py\n-Make sure to always enclose the JSON output in triple backticks (```)。请不要添加任何其他内容,除了有效的 JSON 输出!\n-使用双引号作为键和值的字符串标记。\n-不要将架构中的 "properties" 和 "type" 错误地理解为 JSON 输出中的实际字段。\n-遵循 JSON 格式规范。'}, prompt_variables: ['input_str', 'tools', 'step_history', 'output_format_str']

    )

    (model_client): GroqAPIClient()

    (output_processors): JsonOutputParser(

        data_class=FunctionExpression, examples=[FunctionExpression(thought='我已完成任务。', action='finish(answer="final answer: \'answer\'")')], exclude_fields=None, return_data_class=True

        (output_format_prompt): 提示(

        模板:你的输出应格式化为符合以下架构的标准 JSON 实例:

        ```py
         {{schema}}
         ```

        {% if example %}

        示例:

        ```py
         {{example}}
         ```

        {% endif %}

        -确保始终将 JSON 输出包裹在三个反引号内(```py). Please do not add anything other than valid JSON output!
         -Use double quotes for the keys and string values.
         -DO NOT mistaken the "properties" and "type" in the schema as the actual fields in the JSON output.
         -Follow the JSON formatting conventions., prompt_variables: ['example', 'schema']
         )
         (output_processors): JsonParser()
      )
   )
)

现在,让我们运行测试函数,看看代理的实际操作。

test_react_agent(ModelClientType.GROQ(), llama3_model_kwargs)
test_react_agent(ModelClientType.OPENAI(), gpt_model_kwargs)

我们的代理将通过有色打印输出展示开发者的核心步骤,包括输入查询、步骤和最终答案。以下是使用 llama3 的第一个查询打印输出(这里没有颜色):

2024-07-10 16:48:47 - [react.py:287:call] - input_query: What is the capital of France? and what is 465 times 321 then add 95297 and then divide by 13.2

2024-07-10 16:48:48 - [react.py:266:_run_one_step] - Step 1:
StepOutput(step=1, action=FunctionExpression(thought="Let's break down the query into subqueries and start with the first one.", action='llm_tool(input="What is the capital of France?")'), function=Function(thought=None, name='llm_tool', args=[], kwargs={'input': 'What is the capital of France?'}), observation='The capital of France is Paris!')
_______
2024-07-10 16:48:49 - [react.py:266:_run_one_step] - Step 2:
StepOutput(step=2, action=FunctionExpression(thought="Now, let's move on to the second subquery.", action='multiply(a=465, b=321)'), function=Function(thought=None, name='multiply', args=[], kwargs={'a': 465, 'b': 321}), observation=149265)
_______
2024-07-10 16:48:49 - [react.py:266:_run_one_step] - Step 3:
StepOutput(step=3, action=FunctionExpression(thought="Now, let's add 95297 to the result.", action='add(a=149265, b=95297)'), function=Function(thought=None, name='add', args=[], kwargs={'a': 149265, 'b': 95297}), observation=244562)
_______
2024-07-10 16:48:50 - [react.py:266:_run_one_step] - Step 4:
StepOutput(step=4, action=FunctionExpression(thought="Now, let's divide the result by 13.2.", action='divide(a=244562, b=13.2)'), function=Function(thought=None, name='divide', args=[], kwargs={'a': 244562, 'b': 13.2}), observation=18527.424242424244)
_______
2024-07-10 16:48:50 - [react.py:266:_run_one_step] - Step 5:
StepOutput(step=5, action=FunctionExpression(thought="Now, let's combine the answers of both subqueries.", action='finish(answer="The capital of France is Paris! and the result of the mathematical operation is 18527.424242424244.")'), function=Function(thought=None, name='finish', args=[], kwargs={'answer': 'The capital of France is Paris! and the result of the mathematical operation is 18527.424242424244.'}), observation='The capital of France is Paris! and the result of the mathematical operation is 18527.424242424244.')
_______
2024-07-10 16:48:50 - [react.py:301:call] - answer:
The capital of France is Paris! and the result of the mathematical operation is 18527.424242424244.

对于第二个查询,打印输出:

2024-07-10 16:48:51 - [react.py:287:call] - input_query: Give me 5 words rhyming with cool, and make a 4-sentence poem using them
2024-07-10 16:48:52 - [react.py:266:_run_one_step] - Step 1:
StepOutput(step=1, action=FunctionExpression(thought="I need to find 5 words that rhyme with 'cool'.", action='llm_tool(input="What are 5 words that rhyme with \'cool\'?")'), function=Function(thought=None, name='llm_tool', args=[], kwargs={'input': "What are 5 words that rhyme with 'cool'?"}), observation='Here are 5 words that rhyme with "cool":\n\n1\. Rule\n2\. Tool\n3\. Fool\n4\. Pool\n5\. School')
_______
2024-07-10 16:49:00 - [react.py:266:_run_one_step] - Step 2:
StepOutput(step=2, action=FunctionExpression(thought='Now that I have the rhyming words, I need to create a 4-sentence poem using them.', action='llm_tool(input="Create a 4-sentence poem using the words \'rule\', \'tool\', \'fool\', \'pool\', and \'school\'.")'), function=Function(thought=None, name='llm_tool', args=[], kwargs={'input': "Create a 4-sentence poem using the words 'rule', 'tool', 'fool', 'pool', and 'school'."}), observation="Here is a 4-sentence poem using the words 'rule', 'tool', 'fool', 'pool', and 'school':\n\nIn the classroom, we learn to rule,\nWith a pencil as our trusty tool.\nBut if we're not careful, we can be a fool,\nAnd end up swimming in the school pool.")
_______
2024-07-10 16:49:12 - [react.py:266:_run_one_step] - Step 3:
StepOutput(step=3, action=FunctionExpression(thought='I have the poem, now I need to finish the task.', action='finish(answer="Here are 5 words that rhyme with \'cool\': rule, tool, fool, pool, school. Here is a 4-sentence poem using the words: In the classroom, we learn to rule, With a pencil as our trusty tool. But if we\'re not careful, we can be a fool, And end up swimming in the school pool.")'), function=Function(thought=None, name='finish', args=[], kwargs={'answer': "Here are 5 words that rhyme with 'cool': rule, tool, fool, pool, school. Here is a 4-sentence poem using the words: In the classroom, we learn to rule, With a pencil as our trusty tool. But if we're not careful, we can be a fool, And end up swimming in the school pool."}), observation="Here are 5 words that rhyme with 'cool': rule, tool, fool, pool, school. Here is a 4-sentence poem using the words: In the classroom, we learn to rule, With a pencil as our trusty tool. But if we're not careful, we can be a fool, And end up swimming in the school pool.")
_______
2024-07-10 16:49:12 - [react.py:301:call] - answer:
Here are 5 words that rhyme with 'cool': rule, tool, fool, pool, school. Here is a 4-sentence poem using the words: In the classroom, we learn to rule, With a pencil as our trusty tool. But if we're not careful, we can be a fool, And end up swimming in the school pool.

代理与普通 LLM 响应的比较如下所示:

Answer with agent: The capital of France is Paris! and the result of the mathematical operation is 18527.424242424244.
Answer without agent: GeneratorOutput(data="I'd be happy to help you with that!\n\nThe capital of France is Paris.\n\nNow, let's tackle the math problem:\n\n1\. 465 × 321 = 149,485\n2\. Add 95,297 to that result: 149,485 + 95,297 = 244,782\n3\. Divide the result by 13.2: 244,782 ÷ 13.2 = 18,544.09\n\nSo, the answer is 18,544.09!", error=None, usage=None, raw_response="I'd be happy to help you with that!\n\nThe capital of France is Paris.\n\nNow, let's tackle the math problem:\n\n1\. 465 × 321 = 149,485\n2\. Add 95,297 to that result: 149,485 + 95,297 = 244,782\n3\. Divide the result by 13.2: 244,782 ÷ 13.2 = 18,544.09\n\nSo, the answer is 18,544.09!", metadata=None)

对于第二个查询,比较结果如下所示:

Answer with agent: Here are 5 words that rhyme with 'cool': rule, tool, fool, pool, school. Here is a 4-sentence poem using the words: In the classroom, we learn to rule, With a pencil as our trusty tool. But if we're not careful, we can be a fool, And end up swimming in the school pool.
Answer without agent: GeneratorOutput(data='Here are 5 words that rhyme with "cool":\n\n1\. rule\n2\. tool\n3\. fool\n4\. pool\n5\. school\n\nAnd here\'s a 4-sentence poem using these words:\n\nIn the summer heat, I like to be cool,\nFollowing the rule, I take a dip in the pool.\nI\'m not a fool, I know just what to do,\nI grab my tool and head back to school.', error=None, usage=None, raw_response='Here are 5 words that rhyme with "cool":\n\n1\. rule\n2\. tool\n3\. fool\n4\. pool\n5\. school\n\nAnd here\'s a 4-sentence poem using these words:\n\nIn the summer heat, I like to be cool,\nFollowing the rule, I take a dip in the pool.\nI\'m not a fool, I know just what to do,\nI grab my tool and head back to school.', metadata=None)

ReAct 代理对于回答需要计算或更复杂推理和规划的查询特别有用。然而,在处理一般查询时使用它可能会过于复杂,因为它可能需要比必要的更多步骤来回答问题。

定制化

模板

首先你想要自定义的是模板本身。你可以通过将自己的模板传递给代理的构造函数来做到这一点。我们建议你修改我们的默认模板:[**DEFAULT_REACT_AGENT_SYSTEM_PROMPT**](https://lightrag.sylph.ai/apis/components/components.agent.react.html#components.agent.react.DEFAULT_REACT_AGENT_SYSTEM_PROMPT)

更好的输出格式示例

其次,构造函数中的examples允许你提供更多示例以确保正确的输出格式。例如,如果我们想让它学习如何正确调用 multiply,我们可以传入一个包含正确格式的FunctionExpression实例的列表。classmethod from_function可以用来从一个函数及其参数创建一个FunctionExpression实例。

from lightrag.core.types import FunctionExpression
# generate an example of calling multiply with key-word arguments
example_using_multiply = FunctionExpression.from_function(
     func=multiply,
     thought="Now, let's multiply two numbers.",
     a=3,
     b=4,
 )
examples = [example_using_multiply]# pass it to the agent

我们可以通过以下方式可视化如何将其传递给规划器提示:

react.planner.print_prompt()

上述示例将格式化为:

<OUTPUT_FORMAT>
Your output should be formatted as a standard JSON instance with the following schema:

{

"thought": "为什么调用这个函数(Optional[str])(可选)",

"action": "FuncName() 有效的函数调用表达式。例如:"FuncName(a=1, b=2)" 请遵循函数参数中指定的数据类型。例如,对于具有 x、y 属性的类型对象,使用"ObjectType(x=1, y=2) (str) (required)"

}

Examples:

{

"thought": "现在,让我们乘以两个数字。",

"action": "multiply(a=3, b=4)"

}


{

"thought": "我已经完成了任务。",

"action": "finish(answer="final answer: 'answer'")"

}


-Make sure to always enclose the JSON output in triple backticks (```)。请不要添加任何有效 JSON 输出之外的内容!

-键和字符串值请使用双引号。

-不要将模式中的“properties”和“type”误认为是 JSON 输出中的实际字段。

遵循 JSON 格式规范。

</OUTPUT_FORMAT>

子类化 ReActAgent

如果你想进一步自定义代理,可以子类化[**ReActAgent**](https://lightrag.sylph.ai/apis/components/components.agent.react.html#components.agent.react.ReActAgent)并重写你想更改的方法。

参考文献

[1] 关于基于大语言模型的自主代理的调查:Paitesanshi/LLM-Agent-Survey

[2] ReAct: arxiv.org/abs/2210.03629

API 参考

  • [**components.agent.react.ReActAgent**](https://lightrag.sylph.ai/apis/components/components.agent.react.html#components.agent.react.ReActAgent)

  • [**core.types.StepOutput**](https://lightrag.sylph.ai/apis/core/core.types.html#core.types.StepOutput)

  • [**components.agent.react.DEFAULT_REACT_AGENT_SYSTEM_PROMPT**](https://lightrag.sylph.ai/apis/components/components.agent.react.html#components.agent.react.DEFAULT_REACT_AGENT_SYSTEM_PROMPT)

LLM 代理——直观且详尽地解释

原文:towardsdatascience.com/llm-agents-intuitively-and-exhaustively-explained-8905858e18e2?source=collection_archive---------2-----------------------#2024-01-05

语言建模 | 自主系统 | 人工智能

赋能语言模型进行推理与行动

Daniel WarfieldTowards Data Science Daniel Warfield

·发表于 Towards Data Science ·阅读时间 37 分钟·2024 年 1 月 5 日

--

“决策引导者”由丹尼尔·沃菲尔德(Daniel Warfield)使用 MidJourney 创作。所有图像均由作者提供,除非另有说明

本文聚焦于“代理”这一概念,它允许语言模型进行推理并与世界互动。首先,我们将讨论什么是代理以及为什么它们很重要;接着,我们将通过几种代理的形式来建立对其工作原理的直观理解;然后,我们将通过实现两种代理来探索代理在实际环境中的应用,一种使用 LangChain 实现,另一种用 Python 从零开始实现。

在本文结束时,您将理解代理如何赋能语言模型执行复杂任务,您也将了解如何自己构建一个代理。

这对谁有用? 任何对构建前沿语言建模系统所需工具感兴趣的人。

这篇文章的难度如何? 这篇文章在概念上简单,但包含了过去一年中的前沿研究,因此对各个经验层次的数据科学家都具有相关性。

前提条件: 无,尽管对语言模型(如 OpenAI 的 GPT)有一些基本了解可能会有所帮助。如果您对某些内容感到困惑,我在本文末尾附加了一些相关资料,供您参考……

LLM 代理、文本向量化、进阶 SQL 及其他我们最新作者的必读文章

原文:towardsdatascience.com/llm-agents-text-vectorization-advanced-sql-and-other-must-reads-by-our-newest-authors-3263a1bdd7eb?source=collection_archive---------6-----------------------#2024-08-22

TDS 编辑Towards Data Science TDS 编辑

·发布于Towards Data Science ·作为 Newsletter 发送 ·阅读时间:4 分钟·2024 年 8 月 22 日

--

如果你是《Variable》的常规读者,你可能已经注意到我们每周都会强调——TDS 始终欢迎新作者投稿。我们是认真的!你们中的一些人可能看到过这个信息,心想“太好了,我想写一篇文章!”但又开始思考,什么样的文章适合投稿,读者们对哪些话题感兴趣,以及哪些经历和技能是受欢迎的。

本周的《Variable》版块精选了我们最近的一些最佳文章,所以如果你不打算成为 TDS 的作者,那也完全没问题!我们希望你像往常一样享受阅读。我们这次只关注了我们最新一批作者的文章,旨在激励你们也尝试投稿。

正如你将看到的,TDS 的投稿者具有广泛的经验背景(从初学者到博士、行业老兵不等)、兴趣以及写作风格。将他们联系在一起的是他们卓越的叙事能力和将知识分享给更广泛社区的愿望。我们希望(并且相当确定)你会喜欢我们每周的内容。

  • 大型语言模型“理解”什么? 当我们将类人能力归因于 LLM 时,我们往往会陷入拟人化偏见,把它们的能力与我们自身的能力进行类比。但我们是否也在表现出一种人类中心主义偏见,忽视了 LLM 始终展现出来的能力?”在我们最近读过的一篇最具启发性的文章中,Tarik Dzekman从哲学和心理学的视角探讨了 LLM 理解语言的能力问题。

  • 将 LLM 代理与 LangChain 整合到 VICA 中“我们的目标是告别聊天机器人中那种机械化和笨拙的表单式体验,迎接与人类助手进行个性化对话的新方式。”黄伟诚Nicole Ren分享了他们在新加坡 GovTech 虚拟智能聊天助手(VICA)平台上广泛工作中的实际见解和经验教训。

  • 文本向量化揭秘:将语言转化为数据 对于我们这些了解机器学习流程的人来说,我们明白特征工程是从模型中生成良好结果的关键步骤。这个概念在自然语言处理(NLP)中同样适用。”Lakshmi Narayanan提供了关于文本向量化方法的全面概述,并权衡了它们各自的优缺点。

图片来源:Totte Annerbrink 来自Unsplash

  • 利用 Gemini-1.5-Pro-Latest 实现更智能的饮食 值得注意的是,随着人工智能领域的进步,数据科学家有责任逐步从传统的深度学习转向生成式人工智能技术,以革新他们的角色。”Mary Ara展示了一个端到端的项目演示,说明如何通过创建一个利用最先进的多模态模型的卡路里追踪应用来实现这一目标。

  • 在技术行业成功的最有用的高级 SQL 技巧“尽管掌握基本和中级 SQL 相对容易,但要真正精通这项工具,并在各种场景中熟练运用,有时却具有挑战性。”Jiayan Yin旨在通过全面概述您应该添加到查询工具包中的更高级 SQL 技巧,帮助数据分析师和其他从业者弥补这一技能差距。

  • 使用 Hugging Face Transformers 微调音频频谱变换器“这个过程将模型的能力调整到我们数据集的独特特征,比如类别和数据分布,从而确保结果的相关性。”在机器学习与音频数据的交集上,Marius Steger详细阐述了如何在任何音频分类数据集上微调音频频谱变换器(AST)的工作流程。

  • 使用 MLflow 进行算法无关的模型构建“考虑这样一个场景:我们有一个当前在生产环境中部署的 sklearn 模型,用于特定的用例。后来,我们发现深度学习模型的表现更好。如果 sklearn 模型是以其原生格式部署的,转向深度学习模型可能会变得非常麻烦,因为这两个模型的工件差异很大。”Mena Wang, PhD解释了为什么有时使用算法无关的模型会非常有意义,并展示了如何在 MLflow 中开始使用它。

  • 深度学习中非线性的新视角 但我们为什么需要激活函数,特别是非线性激活函数?传统上有一种解释方式,同时也有一种新的视角。”Harys Dalvi剖析了在深度学习分类器输出中使用线性层的风险,以及通过多角度理解线性与非线性所能带来的价值。

感谢您支持我们作者的工作!正如我们前面提到的,我们喜欢发布新作者的文章,因此,如果您最近写了一篇有趣的项目教程、指南或关于我们核心主题的理论思考,请不要犹豫,与我们分享

直到下一个变量,

TDS 团队

LLM 对齐:基于奖励的方法与无奖励的方法

原文:towardsdatascience.com/llm-alignment-reward-based-vs-reward-free-methods-ef0c0f6e8d88?source=collection_archive---------0-----------------------#2024-07-05

LLM 对齐的优化方法

Anish DubeyTowards Data Science Anish Dubey

·发表于 Towards Data Science ·阅读时长 10 分钟·2024 年 7 月 5 日

--

背景

语言模型在基于用户提供的提示生成各种引人注目的文本方面展现了显著的能力。然而,定义什么是“好”文本是具有挑战性的,因为它通常依赖于个人偏好和具体上下文。例如,在讲故事时,创造力是关键;在制作信息内容时,准确性和可靠性至关重要;在生成代码时,确保代码正确运行是必要的。因此,“LLM 对齐问题”,即确保大型语言模型(LLM)按照与人类价值观、意图和偏好一致的方式运作的挑战。

设计一个能够捕捉我们在文本中重视的多种特质(如创造力、准确性或可执行性)的损失函数是极其复杂的,且往往不切实际。像这些概念并不可微分,因此无法进行反向传播,也不能通过简单的下一词生成进行训练。

假设我们能够利用人类反馈来评估生成文本的质量,或者更好的是,使用这些反馈作为引导损失函数来提升模型的表现。这个概念是“人类反馈强化学习”(RLHF)的核心。通过应用强化学习技术,RLHF 使我们能够根据直接的人类反馈微调语言模型,使模型更加符合人类细微的价值观和期望。这种方法为训练不仅更加响应迅速,而且更符合人类偏好复杂性的语言模型开辟了新的可能性。

接下来,我们将通过基于奖励的方法,进一步了解 RLHF,并了解基于无奖励的方法。

什么是通过人类反馈的强化学习(RLHF)和基于奖励的系统?

让我们来了解通过人类反馈的强化学习(RLHF)。它由三个主要阶段组成:

  1. 监督微调

  2. 奖励建模阶段

  3. RL 微调阶段

监督微调

RLHF 是一个预训练模型,已经在高质量数据集上进行了微调。它的目标很简单,即在给定输入(提示)时,产生一个输出。最终目标是进一步微调该模型,以根据人类的偏好产生输出。因此,让我们称之为基础模型以供参考。目前,这个模型是一个标准的基础模型,它对任何人类偏好一无所知。

奖励建模阶段

奖励模型创新:这是奖励模型如何被融入到 RLHF 中的新创新开始的地方。奖励模型的背后思想是,一个新的 LLM 模型(它可以与上述的基础模型相同)将能够生成人的偏好评分。之所以与大型语言模型相似,是因为该模型也需要理解语言语义,才能评估输出是否符合人类偏好。由于奖励是标量,我们在 LLM 上方添加一个线性层,以生成一个关于人类偏好的标量评分。

数据收集阶段:这个阶段是在监督微调阶段完成的,在该阶段中,基础模型会为给定的文本生成两个输出。示例:对于输入标记 x,基础模型生成两个输出标记 y1 和 y2。这些输出会展示给人工评分员进行评分,并记录每个输出的人工偏好。

训练阶段:一旦数据收集阶段采集到数据样本,奖励模型就会使用以下提示进行训练:“给定以下输入:,LLM 生成了输出。你能评估该输出的表现吗?” 模型将输出 r(奖励),我们已经知道从数据收集阶段得到的实际奖励值 r1。现在,可以通过损失函数进行反向传播,模型可以得到训练。以下是模型通过反向传播优化的目标损失函数:

该论文中的公式:arxiv.org/pdf/2305.18290

符号说明:

  • rΦ(x, y):一个由Φ参数化的奖励模型,用于估计奖励。参数化意味着我们不知道实际的值,且需要根据上述方程进行优化。这就是奖励 LLM 模型本身。通常,LLM 的参数在这里被冻结,只有少数参数允许变化。最重要的层是顶部添加的线性层,它负责进行大部分的学习,以评估输出的评分。

  • Ɗ:一个三元组数据集(x, yw, yl),其中 x:输入,yw:赢家输出,yl:输家输出

  • σ:sigmoid 函数,它将奖励差异映射到一个概率值(0–1 之间)

  • ∑(x, y,w yl) ~Ɗ 表示 x, yw, yl 都是从 Ɗ 中采样的。

示例场景: 假设你正在训练一个奖励模型来评估回答。你有一对针对给定提示的回答,并且人类反馈告诉你哪个回答更好。举个例子,x(“法国的首都是什么?”),你有 yw(“法国的首都巴黎。”)作为赢家,以及 yl(“法国的首都柏林。”)作为输家。当输入是“法国的首都是什么?”时,奖励模型最终应该学会给“法国的首都巴黎”更高的奖励,而不是“法国的首都柏林”。

RL 微调阶段

强化学习思路: 现在,基本模型奖励模型都已经训练完成,思路是如何利用奖励模型的得分并更新基本模型参数,以反映人类偏好。由于奖励模型输出的是标量分数,并且不可微,我们无法使用简单的反向传播来更新基本模型参数。因此,我们需要其他技术来更新基本模型。这就是强化学习的作用,它通过奖励模型的得分帮助基本模型改变参数。这是通过 PPO(近端策略优化)完成的。理解 PPO 的核心架构并不是理解这个概念所必需的,因此我们不会在这里讲解,但从高层次来说,PPO 的思路是可以使用标量得分来更新基本模型参数。现在让我们了解基本模型和奖励模型如何结合起来,使得基本模型学习人类偏好。

RL 微调思路: 在强化学习中,我们有动作、空间和奖励。思路是提出一个策略,让任何动作代理都可以在该空间内采取,从而最大化奖励。这个过程比较复杂,但简化来看,π是我们的基本 LLM 模型。Πref表示基本模型,ΠӨ表示我们正在尝试生成的不同的 LLM 最优模型。我们需要找到 ΠӨ(即基本模型的神经网络权重将被微调),从而输出人类更喜欢的结果。问题是我们不知道ΠӨ,而目标是找到这个最优模型。

强化学习训练与反馈循环阶段:输入 x 被提供给两个策略模型,Πref(基准模型)和ΠӨ(我们试图生成的最优模型)。最初,两个模型是相同的。将输入 x 分别传入这两个模型会分别产生两个输出。ΠӨ模型的输出也会输入到奖励模型中(输入:x,输出:y;如上所述),并要求输出奖励分数,即 rΦ(x, y)。现在我们有三个内容:基准模型的输出、最优模型的输出和最优模型的奖励分数。这里有两个优化目标,一个是最大化奖励,因为最终我们希望模型与人类偏好尽可能接近,另一个是最小化与基准模型的差异。最大化奖励很容易,因为它本身就是一个标量值,但如何最小化基准模型和最优模型之间的差异呢?这里我们使用“Kullback–Leibler 散度”,它估计两个连续概率分布之间的差异。让我们更深入地了解目标损失函数。

来自本文的方程:arxiv.org/pdf/2305.18290

符号表示法

  • rΦ(x, y):表示输入 x 和输出 y 的标量值(来自最优模型)。为了明确起见,最优模型的输出会被输入到奖励模型中。

  • Dkl (ΠӨ (y | x) || Πref (y | x)):计算两个概率分布之间的Kullback–Leibler 散度。每个模型中的每个标记都是一个概率分布。KL 估计两个分布之间的差异程度。

  • β:超参数,用于确定最优模型与基准模型接近的重要性。

示例场景:假设你问:“法国的首都是什么?”,Πref(基准模型)回答:“法国的首都是柏林。”而ΠӨ(最优模型)回答:“法国有三个首都,巴黎、凡尔赛和里昂,但巴黎被视为官方首都。”现在 rΦ(“x: 法国的首都是什么…”, “y: 法国有三个首都…”)应该给出较低的分数,因为它不太符合人类的偏好,且 Kullback–Leibler 散度(ΠӨ (y | x) || Πref (y | x))也应该较高,因为两个模型的输出概率分布空间存在差异。因此,这两个项的损失都会较高。我们不希望模型仅仅优化奖励,还希望它能够保持接近基准模型,因此这两个项都用于优化奖励。在接下来的学习迭代中,假设ΠӨ(最优模型)回答“法国的首都是德里”,在这种情况下,模型学习到保持接近Πref(基准模型)并输出更接近基准模型格式的内容,但奖励部分仍然较低。希望在第三次迭代中,ΠӨ(最优模型)能够学习并输出“法国的首都是巴黎”,并获得更高的奖励,同时模型输出与基准模型紧密对齐。

以下图示有助于说明逻辑。我还强烈推荐浏览RLHF 链接,该链接来自 Hugging Face。

作者提供的图片,灵感来自huggingface.co/blog/rlhf

什么是通过无奖励方法的基于人类反馈的强化学习(RLHF)?

在考虑使用基于奖励的方法的 RLHF 之后,我们来讨论无奖励方法。根据论文中的描述:“我们的关键见解是利用从奖励函数到最优策略的分析映射,这使我们能够将奖励函数上的损失函数转换为策略上的损失函数。这种变量变换方法避免了拟合显式的独立奖励模型,同时仍然在现有的人类偏好模型下进行优化。” 非常复杂,理解起来有难度,但我们将在下一部分尝试将其分解为简单的阶段。

无奖励方法的关键思想: 在 RLHF 中,训练一个单独的新的奖励模型既昂贵又难以维护。有没有什么机制可以避免训练新的奖励模型,而使用现有的基础模型来达到新的最优模型?这正是无奖励方法的做法,即它避免了训练新的奖励模型,并通过改变方程式,使得 DPO(直接偏好优化)的损失函数中不再包含奖励模型项。可以这样理解,我们需要从基础模型(Πref)到达最优模型策略(ΠӨ)。可以通过优化奖励函数空间来帮助建立代理模型,从而达到最优模型策略,或直接学习从奖励到策略的映射函数,并进而优化策略本身。这正是作者们通过移除损失函数中的奖励函数组件,并直接用模型策略参数替代它所尝试的做法。这就是作者们所说的“利用奖励函数到最优策略的分析映射...到损失函数上”的核心创新。

DPO 训练与反馈回路阶段: 使用Πref(基线模型),输入 x 并要求产生 2 个输出(y1 和 y2)。所有 x、y1 和 y2 都会被人工评分员用来决定胜出的 yw 和失败的 yl。离线数据集收集了三元组信息<x, yw 和 yl>。通过这些信息,我们知道胜出(人类偏好)和失败(人类不偏好)的答案是什么。现在,相同的输入 x 被传入 2 个模型(Πref 基线模型和ΠӨ最优模型)。最初,为了训练目的,两个模型保持相同。将输入 x 分别输入这两个模型,得到相应的两个输出。我们通过“Kullback-Leibler 散度”计算输出与参考模型和最优模型的胜出与失败答案之间的差距。让我们深入研究目标损失函数。

公式

来自 arxiv.org/pdf/2305.18290 的方程

  • ΠӨ (yw | x) -> 给定输入 x,模型的对应输出 youtput 与获胜输出 yw 之间的差距是多少。输出 youtput 和 yw 是概率分布,二者之间的差异将通过“Kullback–Leibler 散度”来计算。这将是一个标量值。此外,这也会针对不同组合的Πref (yw | x)、Πref (yl | x)、ΠӨ (yw | x)和ΠӨ (yl | x)进行计算。

  • β :超参数,用于确定将最佳模型与基线模型接近的重要性。

图片来源于作者,灵感来自 huggingface.co/blog/rlhf

结论

  • 自然地,问题变成了哪种方法更好,基于奖励的 RLHF 方法使用 PPO,还是无奖励的方法使用 DPO。这个问题没有确切的答案。最近一篇论文比较了“DPO 是否优于 PPO 用于大语言模型对齐”(论文 链接)并得出结论,PPO 通常优于 DPO,而且 DPO 在处理超出分布的数据时更为困难。“超出分布”数据指的是人类偏好数据与基线训练数据不同。如果基础模型的训练是在某些数据集上进行,而偏好输出是在另一个数据集上进行,这种情况就可能发生。

  • 总的来说,关于哪种方法更好,研究仍在进行中,但我们已经看到像 OpenAI、Anthropic、Meta 等公司将 RLHF 通过 PPO 和 DPO 两种工具用于大语言模型的对齐。

参考文献

LLM 应用、关键数据技能、多代理 AI 系统及其他七月必读文章

原文:towardsdatascience.com/llm-apps-crucial-data-skills-multi-ai-agent-systems-and-other-july-must-reads-a660a846cda8?source=collection_archive---------6-----------------------#2024-07-04

TDS 编辑Towards Data Science TDS 编辑

·发表于Towards Data Science ·通过新闻通讯发送 ·阅读时间:4 分钟·2024 年 7 月 4 日

--

感觉受到了启发,想写下你的第一篇 TDS 文章吗? 我们始终欢迎新作者的投稿

如果你所在的地方已经是夏天,我们希望你能充分利用温暖的天气和(希望?也许?)更加放松的日常节奏。当然,学习永远不会停止——至少对数据科学家而言如此——所以如果你认为美好的时光是挑战新课题、探索前沿工具和工作流程,那么你将迎来一场盛宴。

我们七月的亮点由上个月在读者中引起最大反响的文章组成,涵盖了广泛的实用话题——其中许多文章旨在帮助你提高自身标准,拓展技能。让我们一起深入了解吧!

每月亮点

  • 我在 Uber、Meta 和初创公司 10 年的经历教会了我数据分析的知识提供建议很容易;但根据 10 年的多元化数据领导经验,提出可操作的、经过时间验证的见解则需要付出更多努力——在Torsten Walbaum的文章中,这一努力绝对值得。

  • 作为数据科学家,我如何使用 ChatGPT我们是否终于迎来了 LLM 工具能够显著简化数据专业人士核心任务的时刻?正如Egor Howell所解释的,如果你在如何以及在哪里将 ChatGPT 集成到工作流中做出聪明的选择,你的生产力可能已经开始显著受益。

  • 330 周的数据可视化:我的历程与关键收获 在进行超过五年的每周数据可视化创作后,Yu Dong回顾了持续性的价值,并为现有和未来的数据科学家提供了有益的建议,帮助他们在制作图表、绘图和信息图时提升自己的技能。

照片由Emily Studer拍摄,发布于Unsplash

  • 构建 LLM 应用程序:清晰的逐步指南许多机器学习从业者都有很棒的 AI 产品创意,但正如Almog Baku所指出的,“目前没有建立的最佳实践,很多先驱者在没有清晰路线图的情况下,要么需要重新发明轮子,要么会陷入困境。”幸运的是,随着 Almog 为导航复杂的 LLM 原生开发领域制定了蓝图,这种情况已经不再发生。

  • 多 AI 代理系统 101在 LLM 成为主流后不久,产品工程师们开始发现它们所带来的各种痛点和瓶颈。Mariya Mansurova最近发布的指南介绍了应对这些挑战的最有前景的策略之一:多代理 AI 系统,在这种系统中,每个代理都拥有自己专门的“技能”,可以与其他代理协作。

  • 2024 年你不能忽视的 5 项数据科学技能 在她的精彩职业导向总结中,Sara Nóbrega观察到:“虽然大学和正式教育提供了一些基本技能,但它们往往无法为学生提供进入公司所需的实际知识。”Sara 旨在填补这一空白,推荐数据科学家应该专注的五个领域,以便在当今的就业市场中茁壮成长。

  • 17 种(高级)RAG 技术,将您的 LLM 应用原型转化为生产就绪解决方案 如果您需要随时调整、优化或升级您的检索增强生成系统,请务必收藏Dominik Polzer最近的贡献,他的内容远远超越了基础知识,涵盖了元数据、查询路由、句子窗口检索等更多内容。

  • 微调小型变换器模型:文本分类 我们通过一篇出色的项目操作指南来完成本月的内容,由Ida Silfverskiöld呈现:它耐心地概述了微调小型变换器模型以处理 NLP 任务的过程,使用一个经过预训练的编码器模型,通过二分类来识别点击诱饵与事实类文章。

我们最新的一批新作者

每个月,我们都非常高兴看到一批新的作者加入 TDS,他们每个人都在与我们的社区分享自己独特的声音、知识和经验。如果你正在寻找新的作家来探索和关注,只需浏览我们最新加入的作者作品,包括 孟柳赵罗比·乔治根亚历克斯·德雷莫夫托尔斯滕·沃尔鲍姆杰雷米·纽尔杰森·贾阿克查伊·斯里瓦斯塔瓦罗曼·S詹姆斯·蒂欧路易斯·费尔南多·佩雷斯·阿尔马斯博士莉亚·吴W·卡登·哈姆里克杰克·摩尔埃迪·福尔森卡尔斯滕·弗罗姆霍尔德丹尼拉·莫罗佐夫斯基比曼·查克拉博提让·梅尼尔-皮翁肯·基霍罗伯特·洛内普拉纳夫·贾达夫科内利乌斯·尤达·维贾亚维托·里哈尔迪吉兰贾斯廷·劳夫林伊吉特·阿西克特穆·索尔穆嫩拉尔斯·维克瑞亚·戈尔瑞安·德库尼亚冈萨洛·埃斯皮诺萨·杜埃洛阿基拉·索马顺达兰梅尔·里奇博士洛伦·辛克森乔纳森·R·威利福德博士丹尼尔·洛妮可·任丹尼尔·波拉克斯特凡·托多兰丹尼尔·科哈·乐阿维谢克·比斯瓦斯埃亚尔·特拉贝尔西本·奥尔尼迈克尔·B·沃克埃莉诺·汉娜玛格达·恩特西卡

感谢您支持我们作者的工作!我们非常喜欢发布新作者的文章,如果你最近写了一篇有趣的项目 walkthrough、教程,或是关于我们核心主题的理论思考,别犹豫,与我们分享

直到下一个变量,

TDS 团队

LLM 评估、AI 副项目、用户友好的数据表格及其他十月必读

原文:towardsdatascience.com/llm-evaluation-ai-side-projects-user-friendly-data-tables-and-other-october-must-reads-6be0066008e2?source=collection_archive---------8-----------------------#2024-10-31

TDS EditorsTowards Data Science TDS Editors

·发表于 Towards Data Science ·以 新闻通讯 形式发布 ·4 分钟阅读 ·2024 年 10 月 31 日

--

是否感到受到启发,想写你的第一篇 TDS 文章?我们始终欢迎新作者的投稿

我们似乎正处于日历上一个理想的时段——介于夏季结束和节前最后的忙碌冲刺之间,换句话说,这是一个学习、琢磨和探索的完美时机。

我们十月最受欢迎的文章反映了这种专注能量的精神,涵盖了一系列动手实践的主题。从可操作的 AI 项目构思和数据科学收入来源,到关于时间序列分析和 LLMs(大语言模型)的易懂指南,这些文章很好地代表了我们作者的广博专业知识和他们(以及我们的读者)兴趣的多样性。如果你还没阅读它们,现在正是最佳时机!

月度亮点

  • 5 个你可以在这个周末用 Python 构建的 AI 项目 如果你还没有准备好,别担心,马上就要动手了:我们十月最受欢迎的文章,来自Shaw Talebi,概述了几个非常有吸引力的项目构思,适合那些想将 AI 知识付诸实践的人。从简历整理工具到多模态搜索工具,这些项目为进入日益扩展的 AI 驱动产品开发世界提供了一个平滑的切入点。

  • 谁真正拥有你预订的 Airbnb?——营销认知与数据分析的现实差异如果你想深入研究一个有趣的数据分析案例,Anna Gordun Peiro的最新文章正好符合你的需求。该文章基于公开数据,深入探讨了 Airbnb 的所有权模式,并向读者展示了如何在自己选择的城市进行类似的调查。

  • LLM 评估技能容易掌握(但实践成本高)创建 LLM 解决方案需要大量的时间和资源投入,因此产品经理和机器学习工程师必须清晰准确地了解其性能。Thuwarakesh Murallie为我们详细介绍了如何利用多种评估方法和工具,以实现这一常常难以捉摸的目标。

Ahmad Ossayli拍摄,图片来自Unsplash

  • 构建用户友好数据表的 5 个原则“我常常会想,‘这列是什么意思?’‘为什么表 A 和表 B 中有两列名称相同的列?我该用哪一列?’”Yu Dong介绍了五条实用的规则,确保你的数据表对团队成员和其他利益相关者具有可访问性、可用性,并且容易理解。

  • 我如何在两周内学习 LLM:一份全面的学习路线图尽管你可能认为 LLM 在过去几年里已经无处不在,但许多从业者——无论是新手还是经验丰富的专家——才刚刚开始关注这一热门话题;如果你想要一个系统化的学习方法,从基础知识(及更多)开始,不妨直接参考Hesam Sheikh的广受欢迎的课程。

  • 从零开始使用初中数学理解大型语言模型如果你希望通过更有指导性的方法从零开始学习大型语言模型,可以尝试Rohit Patel的首篇 TDS 贡献:这是一个全面的、40 分钟的讲解,讲解了这些模型的内部工作原理——并且不需要任何高级数学或机器学习知识。

  • 掌握时间序列分析的 5 种必知技巧从数据拆分和交叉验证到特征工程,Sara Nóbrega最近的深入分析聚焦于你需要掌握的基本工作流,以便有效进行时间序列分析。

  • AI 代理:生成式 AI 中工具调用与推理的交集最近几个月,少有话题能像 AI 代理那样引发如此广泛的关注;如果你想深入了解它们的潜力(和局限性),不要错过Tula Masterman的清晰概述,该概述重点介绍了代理推理如何通过工具调用表现出来,探讨了代理在使用工具时面临的一些挑战,并涵盖了评估工具调用能力的常见方法。

  • 作为数据科学家的 7 个收入来源大多数(所有?)数据专业人士都知道在科技巨头公司全职工作的福利,但将技能变现的选择远不止这些。Egor Howell提供了一个坦率的总结,回顾了他在过去几年中成为全职数据科学家后,所培养的各种收入来源。

我们最新一届的新作者

每个月,我们都非常高兴看到一批新的作者加入 TDS,他们每个人都在与我们的社区分享自己独特的声音、知识和经验。如果你正在寻找新的作者来探索和关注,只需浏览我们最新加入的作者作品,包括David FoutchRobin von MalottkiRuth CrastoStéphane DerosiauxRodrigo NaderTezan SahuRobson TigreCharles IdeAamir Mushir KhanAneesh NaikAlex Heldcaleb leeBenjamin BodnerVignesh BaskaranIngo NowitzkyTrupti BavalattiSarah LeaFelix GermaineMarc PolizziAymeric FloyracBárbara A. CancinoHattie BiddlecombeCarlo PeronMinda MyersMarc LinderAkash MukherjeeJake MinnsLeandro MaggaJack VanlightlyRohit PatelBen HagagLucas SeeMax ShapFhilipus MahendraPrakhar Ganesh,和Maxime Jabarian

感谢您支持我们作者的工作!我们很喜欢发布新作者的文章,所以如果您最近写了一篇有趣的项目演示、教程或关于我们核心话题的理论反思,请毫不犹豫地与我们分享

直到下一个变量,

TDS 团队

LLM 评估技能很容易掌握(但实践起来成本高昂)

原文:towardsdatascience.com/llm-evaluation-techniques-and-costs-3147840afc53?source=collection_archive---------1-----------------------#2024-10-09

这里是如何避免浪费预算在评估模型和系统上的方法

Thuwarakesh MurallieTowards Data Science Thuwarakesh Murallie

·发表于Towards Data Science ·14 分钟阅读·2024 年 10 月 9 日

--

图片由作者使用 Flux1.1 Pro 制作。

你可以通过两种方式建造一座堡垒:一种是开始将砖块一个个堆叠起来,另一种是先画出你即将建造的堡垒的图纸并规划执行方案;然后,按照你的计划不断评估它。

我们都知道第二种方法是我们可能构建堡垒的唯一途径。

有时候,我是我自己建议的最差追随者。我说的是直接跳入笔记本去构建 LLM 应用程序。这是我们破坏项目的最糟糕做法。

在我们开始之前,我们需要一个机制来告诉我们是否朝着正确的方向前进——也就是要说明我们上一次尝试的结果是否比之前更好(或者相反)。

在软件工程中,这叫做测试驱动开发。而对于机器学习来说,这就是评估。

开发基于 LLM 的应用程序的第一步,也是最宝贵的技能,就是定义你将如何评估你的项目。

评估 LLM 应用程序与软件测试完全不同。我并不是否定软件测试中的挑战,但评估 LLM 并不像测试那样简单直接。

LLM 微调 — 常见问题解答

原文:towardsdatascience.com/llm-fine-tuning-faqs-200442827c99?source=collection_archive---------8-----------------------#2024-09-26

回答我作为 AI 顾问收到的最常见问题

Shaw TalebiTowards Data Science Shaw Talebi

·发布于 Towards Data Science ·7 分钟阅读·2024 年 9 月 26 日

--

去年,我发布了一篇关于微调大型语言模型(LLMs)的文章。令我惊讶的是,这篇文章成为了我阅读量最高的博客之一,并且引发了与客户关于微调问题和 AI 项目的数十次对话。在这里,我将总结这些对话中最常见的问题以及我的回答。

图片来自 Canva。

什么是微调?

我喜欢将微调定义为采用一个现有的(预训练的)模型,并训练至少 1 个模型参数以将其适应特定的使用场景

需要注意的是定义中“训练至少 1 个模型参数”这一部分。有些人会在定义中忽略这一细节(包括我自己有时也会忽略)。然而,这一细节将微调与诸如提示工程(prompt engineering)或前缀调优(prefix-tuning)等方法区分开来,因为后者是通过不修改模型内部操作的方式来调整模型行为。

## 微调大型语言模型(LLMs)

一个带有示例 Python 代码的概念概览

towardsdatascience.com

何时不应进行微调

LLM 驱动的半结构化和无结构文档解析与分析

原文:towardsdatascience.com/llm-powered-parsing-and-analysis-of-semi-structured-structured-documents-f03ac92f063e?source=collection_archive---------3-----------------------#2024-08-12

如何从文档中提取所需的信息

Umair Ali KhanTowards Data Science Umair Ali Khan

·发布于 Towards Data Science ·阅读时长 17 分钟·2024 年 8 月 12 日

--

如果你不是 Medium 会员,你可以通过 这个链接阅读完整文章。

文档解析是分析文档内容(无结构或半结构化)以提取特定信息或将内容转化为更结构化格式的过程。文档解析的目标是将文档分解成其组成部分,并对这些部分进行解释。文档解析对于处理大量各种格式数据的组织非常有用,因为这些数据需要自动化提取。文档解析在商业中有很多应用场景,例如发票处理、法律合同分析、来自多个来源的客户反馈分析和财务报表分析等等。

在大规模语言模型(LLM)出现之前,文档解析是通过使用预定义规则,如正则表达式(Regex)来完成的。然而,这些规则缺乏灵活性,并且仅限于预定义的结构。现实世界中的文档通常存在不一致性,并且没有固定的结构或格式。这正是 LLM 在从半结构化或无结构文档中提取特定信息并进行进一步分析方面,具有巨大潜力的地方。

在本文中,我将通过一个实际的例子,解释如何自动提取所需的信息……

LLM 路由 — 直观且详尽的解释

原文:towardsdatascience.com/llm-routing-intuitively-and-exhaustively-explained-5b0789fe27aa?source=collection_archive---------0-----------------------#2024-11-22

在每个查询上动态选择合适的语言模型

Daniel WarfieldTowards Data Science Daniel Warfield

·发表于 Towards Data Science ·阅读时间 49 分钟·2024 年 11 月 22 日

--

“和谐”,由 Daniel Warfield 使用 Midjourney 创作。除非另有说明,所有图片均由作者提供。本文最初发布于 直观且详尽的解释

在这篇文章中,我们将讨论“LLM 路由”,这是一种高级推理技术,可以自动从一组语言模型中选择最适合给定提示的语言模型;提高 LLM 驱动系统的性能、速度和成本效益。

我们将探索四种 LLM 路由方法:三种来自学术界,一种来自行业,以全面理解这一概念和技术。在此过程中,我们将探讨多种建模策略,这些策略在重要的 AI 应用场景中非常有用,比如自我评估、自治系统和在不确定性面前的决策制定。

这对谁有用? 任何想要深入理解人工智能以及构建前沿 AI 驱动系统所需核心方法的人。

这篇文章有多高级? 文章的前面部分适合各个层次的读者阅读。后面的部分则更倾向于有一定经验的数据科学家和开发者。

前提条件: 早期部分适合所有层次的读者,但后面的部分包含一些可能对经验较少的读者来说至关重要的支持内容。

LLM 对 LLM:Codenames 比赛

原文:towardsdatascience.com/llm-vs-llm-codenames-tournament-f8170dd1c8fb?source=collection_archive---------6-----------------------#2024-10-12

三个不同的 LLM 代理之间的迷你多代理竞赛

Yennie JunTowards Data Science Yennie Jun

·发表于 Towards Data Science ·阅读时长:6 分钟·2024 年 10 月 12 日

--

由 ChatGPT 4o 生成。

本文最初发布于 Art Fish Intelligence.

介绍

大型语言模型(LLMs)擅长许多事情,其中之一就是玩游戏。人们已经利用 LLMs 玩过各种游戏,例如 Minecraft国际象棋谋杀之谜游戏狼人杀纽约时报连接谜题。(欲了解更全面的列表,您可以参考 这篇调查。)

上面的大部分示例展示了 LLMs 在与自己或人类对战时玩游戏。但,LLMs 与其他LLMs 对战时,表现如何呢?

在这篇文章中,我展示了三种不同的 LLM 在流行桌游 Codenames 中的竞争结果,该游戏挑战玩家在看似无关的单词中寻找模式。

Codenames

对于不熟悉的人,Codenames 是一款由 Vladimír Chvátil 创建的桌游。游戏将两队(通常是红队和蓝队)对抗。

每个队伍有一个间谍头目,负责给出单词线索,指向 5x5 的单词板上的多个单词。队伍中的其他玩家必须猜测自己队伍的单词,同时避免猜到属于对方队伍的单词。只有…

LLMOps — 使用 BentoML 提供 Llama-3 模型服务

原文:towardsdatascience.com/llmops-serve-a-llama-3-model-with-bentoml-4d580a7a007f?source=collection_archive---------6-----------------------#2024-08-09

Simon Wiedensohler拍摄,来源:Unsplash

快速设置 LLM API,使用 BentoML 和 Runpod

Marcello PolitiTowards Data Science Marcello Politi

·发布于Towards Data Science ·6 分钟阅读·2024 年 8 月 9 日

--

引言

我经常看到数据科学家对 LLM 的模型架构、训练技术或数据收集产生兴趣。然而,我注意到,很多时候,在理论层面之外,许多人在将这些模型部署为可供用户使用时遇到困难。

在这个简短的教程中,我将以非常简单的方式展示如何使用BentoML提供 LLM 服务,特别是 llama-3 模型。

BentoML 是一个端到端的机器学习模型服务解决方案。它帮助数据科学团队开发生产就绪的模型服务端点,在每个阶段都实现 DevOps 最佳实践和性能优化。

我们需要 GPU

如你所知,在深度学习中,拥有合适的硬件至关重要。尤其对于像 LLM 这样的大型模型,这一点更加重要。不幸的是,我没有 GPU 😔

这就是为什么我依赖外部提供商,所以我租用他们的机器并在那里工作。为了这篇文章,我选择了Runpod,因为我了解他们的服务,并且我认为这个价格适合跟随本教程。但如果你有 GPU 或者想要…

LLMs、AI 代理、生成性 AI 的经济学以及其他八月必读文章

原文:towardsdatascience.com/llms-ai-agents-the-economics-of-generative-ai-and-other-august-must-reads-33f2112a5cdf?source=collection_archive---------6-----------------------#2024-08-29

TDS 编辑Towards Data Science TDS 编辑

·发表于Towards Data Science ·发送为新闻通讯 ·4 分钟阅读·2024 年 8 月 29 日

--

想要写你的第一篇 TDS 文章吗?我们始终欢迎新作者的投稿

由于我们许多读者正准备告别夏天,重新进入正常的工作节奏,我们希望你们仍能为长期成长腾出一些时间——无论是开始一个副项目,探索前沿的机器学习研究,还是仅仅是刷新一下你的数据科学作品集。无论未来几个月带你走向何方,我们也希望 TDS 能继续成为你学习旅程的一部分。

从早期职业建议到 LLM 驱动的数据分析和编程技巧,我们上个月最受欢迎的文章涵盖了广泛的内容——这意味着,无论你当前的兴趣是什么,你都可能在这里找到能激发你好奇心的内容。

这里是我们八月的必读文章——祝你阅读愉快!

  • 我给想成为数据科学家的人的诚恳建议如果你刚刚迈出数据科学职业生涯的第一步,并且对如何开始以及从哪里开始感到不确定,Egor Howell的真诚建议——从一个已经走得稍远的人的角度出发——或许正是你今天需要阅读的那篇文章。(它无疑与我们许多读者产生了共鸣!)

  • requirements.txt 已过时“尽管标准库功能强大,但现代 Python 应用程序通常需要更先进的功能,超出了标准库所包含的范围。” 聚焦、可操作且对广泛的数据专业人士具有相关性,Giorgos Myrianthous 提供了一份广受好评的指南,讲解如何使用 Poetry 管理 Python 项目的依赖关系和元数据。

  • ChatGPT vs. Claude vs. Gemini:数据分析对决(第一部分)随着基于大型语言模型(LLM)的工具在日常数据科学工作流中变得越来越普及,了解它们的表现以及不同模型之间的比较比以往任何时候都更加重要。Yu Dong 的热门系列正是为了实现这一目标,评估了三种流行工具在多种数据分析任务中的表现。

图片由 Annie Spratt 提供,来源于 Unsplash

  • AI 代理 — 从概念到 Python 中的实际实现“与其让一个单一的大型语言模型(LLM)处理复杂任务,我们可以结合多个 LLM 或 AI 代理,每个代理专注于某个特定领域。” Zoumana Keita 的易懂入门书籍清晰地解释了什么是 AI 代理,为什么在实际应用中应考虑使用它们,以及如何从零开始创建一个 AI 代理系统。

  • 成功进入科技行业的最有用的高级 SQL 技巧如果你已经掌握了基础的 SQL 查询,并且觉得是时候提升你的技能水平,Jiayan Yin 的实用指南适合你:从窗口函数到子查询和公用表表达式(CTEs),它提供了具体的示例(和代码片段),鼓励你卷起袖子,将你的知识应用到自己的数据中。

  • 关于 RAG 的那些事增强检索生成(RAG)可能不再是优化 LLM 输出最具话题性的技术,但从业者们仍在不断发现并更好地理解它的优势以及局限性。Ahmed Besbes的全面概述详细解答了后者:“让一个 RAG 系统准备投入生产不仅仅是将一些代码拼凑在一起。这关乎如何应对杂乱的数据、预料之外的用户查询以及始终存在的交付实际业务价值的压力。”

  • 使用 Python 和 SQL 自动化 ETL 到 SFTP 服务器对于那些对数据工程和数据处理有兴趣的从业者(换句话说:你们中的大多数人!),Mary Ara的新教程耐心且清晰地讲解了一个基本工作流:跟随教程学习如何自动化数据在不同位置之间的转移,甚至可以包括 SFTP(安全文件传输协议)上传的额外步骤。

  • 生成式 AI 的经济学“当我们回顾十年后的今天,我怀疑我们认为在生成式 AI 商业领域‘大赢家’的公司,是否是那些真正开发出底层技术的公司。”为了总结本月内容,我们邀请你退后一步,回顾Stephanie Kirmer对生成式 AI 工具商业案例的深刻分析,探索它推动的创新以及那些被抛在后头的创新。

我们最新一批的新作者

每个月,我们都很高兴看到一批新作者加入 TDS,他们每个人都将自己独特的声音、知识和经验与我们的社区分享。如果你在寻找新的作家来探索和关注,可以浏览我们最新加入的作者们的作品,包括Yury KalbaskaJose ParreñoPablo Merchán-Rivera, Ph.D.Conal HendersonMehdi MohammadiMena Wang, PhDJuan HernanzDylan AndersonArmin CatovicLouis WangDiana MoralesChris LydickLakshmi NarayananAnindya Dey, PhDMarius StegerMuhammad ArdiStefan PietruskyLeonardo A. (🐼 panData)Szymon PaluchaNikolai PotapovMathew WangArthur CruiziatUmair Ali KhanMatt FitzgeraldSamy BaladramSaman (Sam) RajaeiPhanuphat (Oad) SrisukhawasuRishabh MisraMarcos SantiagoDavid WellsMary AraTarik DzekmanNg Wei ChengJames F. O'BrienJurgita MotusGary GeorgeJames WilkinsDaniel KharitonovOzgur Guler,以及Shrey Pareek, PhD等人。

感谢您支持我们作者的工作!我们喜欢发布新作者的文章,因此,如果您最近写了一篇有趣的项目教程、教程或关于我们核心主题的理论反思,欢迎不要犹豫,与我们分享

直到下一个变量,

TDS 团队

从零开始学习 LLMs 和 Transformer:解码器

原文:towardsdatascience.com/llms-and-transformers-from-scratch-the-decoder-d533008629c5?source=collection_archive---------2-----------------------#2024-01-10

探索 Transformer 的解码器架构:掩蔽的多头注意力、编码器-解码器注意力以及实际实现

Luís RoqueTowards Data Science Luís Roque

·发表于 Towards Data Science ·13 分钟阅读·2024 年 1 月 10 日

--

本文由 Rafael Nardi 合著。

介绍

在本文中,我们深入探讨了 Transformer 架构中的解码器组件,重点分析它与编码器的异同。解码器的独特特点在于其类似循环的迭代特性,这与编码器的线性处理方式形成鲜明对比。解码器的核心是两种修改过的注意力机制:掩蔽的多头注意力和编码器-解码器多头注意力。

解码器中的掩蔽多头注意力确保了令牌的顺序处理,这种方法防止了每个生成的令牌受到后续令牌的影响。这种掩蔽对于保持生成数据的顺序性和连贯性至关重要。解码器输出(来自掩蔽注意力)与编码器输出之间的交互体现在编码器-解码器注意力中。最后一步将输入上下文引入解码器的处理过程中。

我们还将演示如何使用 Python 和 NumPy 实现这些概念。我们创建了一个简单的示例,用于将一句话从英语翻译成葡萄牙语。这种实用的方法将帮助阐明 Transformer 模型中解码器的内部工作原理……

大语言模型比家猫更笨

原文:towardsdatascience.com/llms-are-dumber-than-a-house-cat-81e7b3d63190?source=collection_archive---------9-----------------------#2024-01-09

它们真的能取代你吗?

Nabil AlouaniTowards Data Science Nabil Alouani

·发表于 Towards Data Science ·13 分钟阅读·2024 年 1 月 9 日

--

所有的图片均来自作者,通过 Midjourney 和 DALL-E 制作。

挫败感?困惑?也许“缺乏优雅”是一个更好的表达。这是你看到顶级科学家对他们已经 理解 的技术感到惊叹时的痛苦。

AI 影响者通过玩弄惊讶的把戏来吸引点击量,但对于科学家和工程师来说,情况是不同的。一旦揭示出其中的诀窍,魔力应该就会消失。

这就是为什么看到微软及其他地方的研究人员在 GPT-4 发布几个月后仍然用“不可思议”、“疯狂”和“惊人”等词汇来描述它时,感到刺痛。

并不是特别针对塞巴斯蒂安·布贝克,但如果一个强化版的自动补全能让他“震惊”,那就可以想象这对普通用户的影响。

开发者和数据从业者每天都在使用大语言模型(LLMs)来生成代码、合成数据和文档。他们也可能会被夸大的功能所误导。正是当人们过于信任他们的工具时,错误才会发生。

简而言之: 这是一个反对炒作的观点,你将了解大语言模型是如何工作的,为什么它们……

为什么 LLMs 不适合编程

原文:towardsdatascience.com/llms-coding-chatgpt-python-artificial-intelligence-4ea7a7bbdd93?source=collection_archive---------0-----------------------#2024-02-28

使用 LLMs 进行编码的挑战

Andrea ValenzuelaTowards Data Science Andrea Valenzuela

·发表于Towards Data Science ·阅读时长:7 分钟·2024 年 2 月 28 日

--

自制图像

在过去的一年里,得益于自然语言理解的进步,大型语言模型(LLMs)展示了惊人的能力。这些先进的模型不仅重新定义了自然语言处理的标准,还被广泛应用于各种应用程序和服务中。

使用 LLMs 进行编程的兴趣迅速增长,一些公司正在努力将自然语言处理转化为代码理解与生成。这一任务已经暴露出使用 LLMs 进行编码的多个挑战,尚未得到解决。尽管如此,这一趋势促使了 AI 代码生成产品的发展。

你曾经用过 ChatGPT 进行编程吗?

虽然在某些情况下它可能有所帮助,但它通常难以生成高效且高质量的代码。在本文中,我们将探讨三个原因,解释为什么大型语言模型(LLMs)在编程上无法做到“开箱即用”:分词器、应用于代码时上下文窗口的复杂性以及训练本身的性质。

识别需要改进的关键领域是将 LLMs 转变为更有效编程助手的关键!

#1 LLM 分词器

为什么 LLMs 不适合编码 — 第二部分

原文:towardsdatascience.com/llms-coding-software-development-artificial-intelligence-68f195bb2ad3?source=collection_archive---------1-----------------------#2024-05-20

用于编码任务的大型语言模型

Andrea ValenzuelaTowards Data Science Andrea Valenzuela

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

--

自制图像。

在发布这篇系列文章的第一篇“为什么 LLMs 不适合编码”后,我在社交媒体上收到了几条评论,例如:

“我在用 ChatGPT 进行编码,它工作得非常好。”

“你错了。大型语言模型是有用的编码助手。”

我对这些反应感到惊讶,因为这篇文章系列的目的并不是劝阻任何人使用大型语言模型(LLMs)进行编码,而是识别需要改进的关键领域,以将 LLMs 转变为更有效的编码助手。

尽管像 ChatGPT 这样的 LLMs 在某些情况下可能有帮助,它们经常生成语法正确但不够优化甚至在功能上不正确的代码。

在上一篇文章中,我们讨论了标记器、在应用于代码时上下文窗口的复杂性以及训练本身的性质如何影响这些模型在编码任务中的表现。

在第二篇文章中,我们将更深入地探讨这些模型在进行编码任务时所接受的训练类型,以及LLMs 在“开箱即用”时天生不擅长编码的另一个原因:保持更新的挑战

2024 年用于编程的 LLM:价格、性能与最佳之争

原文:towardsdatascience.com/llms-for-coding-in-2024-performance-pricing-and-the-battle-for-the-best-fba9a38597b6?source=collection_archive---------5-----------------------#2024-12-04

基于基准测试和实际世界的洞察,评估当前 LLM 的格局,帮助你做出明智的选择。

Ruben BroekxTowards Data Science Ruben Broekx

·发布于 Towards Data Science ·11 分钟阅读·2024 年 12 月 4 日

--

图片由 Flux.1 - Schnell 生成

用于编程的大型语言模型(LLM)的竞争格局从未如此激烈。像阿里巴巴、Anthropic、谷歌、Meta、Mistral、OpenAI 和 xAI 等主要厂商都在提供自己的模型,开发者比以往任何时候都有更多的选择。

但你如何选择最适合你编程用例的 LLM 呢?

在这篇文章中,我将深入分析通过公共 API 提供的顶级 LLM。我将重点关注它们在编码任务中的表现,评估基准如 HumanEval,以及它们在实际应用中的表现,反映在各自的 Elo 分数上。

无论你是在做个人项目,还是将人工智能集成到你的开发工作流程中,了解这些模型的优缺点将帮助你做出更明智的决策。

免责声明:比较 LLM 时的挑战

比较大型语言模型(LLMs)是很困难的。模型经常接收到更新,这些更新对它们的表现有着显著影响——例如,OpenAI 从 GPT-4 到 GPT-4-turbo 再到 GPT-4o,再到 o1 模型的更新。然而,即使是小的更新也会产生影响——例如,GPT-4o 自 5 月 13 日发布以来,已经收到了 3 次更新!

此外,这些模型的随机性意味着它们的表现可能会在不同的运行中有所不同,从而导致研究中的结果不一致。最后,一些公司可能会定制基准和配置——例如特定的 Chain-of-Thought 技术——以展示其模型的最佳表现,这会扭曲比较结果并误导结论。

结论:比较 LLM 的表现是困难的。

本文代表了基于现有信息对各种编码任务模型的最佳努力比较。我欢迎任何反馈,以提高此分析的准确性!

评估 LLM:HumanEval 和 Elo 分数

如上文免责声明所示,为了正确理解 LLM 在编码任务中的表现,建议从多个角度进行评估。

通过 HumanEval 进行基准测试

最初,我尝试从多个基准中汇总结果,看看哪个模型表现最好。然而,这种方法的核心问题是:不同的模型使用不同的基准和配置。似乎只有一个基准是评估编码表现的默认标准:HumanEval。这是一个由人类编写的编码问题数据集,评估模型根据特定要求生成正确和功能性代码的能力。通过评估代码补全和解决问题的能力,HumanEval 成为评估 LLM 编码能力的标准。

通过 Elo 分数展现人们的声音

虽然基准测试能很好地反映模型的表现,但也应谨慎对待。考虑到 LLM 是在大量数据上训练的,某些基准内容(或非常相似的内容)可能已经包含在这些训练数据中。这就是为什么基于人类评判的模型表现也值得评估。像 Chatbot Arena(仅限编码) 这样的 Elo 排名正是这样做的。这些分数来自 LLM 在编码任务中的对抗性比较,由人类评审员进行评估。模型彼此对抗,根据胜负调整 Elo 分数。Elo 分数显示了模型相对于池中其他模型的表现,较高的分数意味着更好的表现。例如,100 Elo 分数的差距意味着排名较高的模型在与排名较低的模型对抗时,预计会有约 64% 的获胜概率。

当前模型表现状态

现在,让我们检查一下当我们将这些模型的 HumanEval 分数与它们的 Elo 排名进行比较时,它们的表现如何。以下图像展示了当前 LLM 的编码领域,其中模型按创建它们的公司进行了聚类。每个公司的最佳表现模型都有注释。

图 1:按 HumanEval 的 Elo 分数 — 按公司着色。X 轴和 Y 轴的刻度显示了每个公司发布的所有模型,最佳表现的模型用粗体标出。

OpenAI 的模型在两个指标上都位于最前端,展示了它们在解决编程任务中的优越能力。OpenAI 排名最高的模型在对比非 OpenAI 的最佳模型——Anthropic 的 Claude Sonnet 3.5 时,超出了 46 Elo 分,预计在对决中的胜率为 56.6%,在 HumanEval 中的差距为 3.9%。虽然这个差距并不惊人,但它显示了 OpenAI 仍然占有优势。有趣的是,表现最好的模型是 o1-mini,它比更大的 o1 高出 10 Elo 分和 2.5% 的 HumanEval 得分。

结论:OpenAI 继续占据主导地位,在基准测试表现和现实世界应用中位居榜首。值得注意的是,o1-mini 是表现最好的模型,超越了更大的对手 o1。

其他公司紧随其后,似乎都处于相同的“表现区间”内。为了更清晰地了解模型表现的差异,以下图表显示了每家公司最佳模型的胜率——由它们的 Elo 评分指示。

图 2:每家公司最佳(编码)模型的获胜概率 — 如 Elo 评分的对决胜率所示。

基准测试结果与现实世界表现的不匹配

从图 1 可以看出,一个显著的特点是 HumanEval(基准测试)与 Elo 评分(现实世界表现)之间的错配。一些模型——如 Mistral 的 Mistral Large——在 HumanEval 得分上显著优于它们的 Elo 评分。其他模型——如 Google 的 Gemini 1.5 Pro——在 Elo 评分上显著优于它们获得的 HumanEval 得分。

很难知道何时信任基准测试,因为基准数据可能已经包含在模型的训练数据集中。这可能导致(过拟合的)模型记住并重复回答编程问题,而不是理解并真正解决问题。

同样,考虑到 Elo 评分是通过众包努力获得的,因此将其作为事实依据也是有问题的。这样做会在评分中引入人为偏见,偏向那些以特定风格输出、采用特定方法的模型……而忽视其他模型,这并不总是与一个客观更好的模型相符。

结论:更好的基准测试结果并不总能反映更好的现实世界表现。建议分别查看两者。

以下图像显示了 HumanEval 和 Elo 评分之间的不一致。所有模型都根据各自的得分进行排序,为了简化,忽略了“一个模型相较另一个模型有多好”的因素。它直观地展示了哪些模型在基准测试中表现更好,但在现实中表现较差,反之亦然。

图 3:HumanEval 和 Elo 评分的错配 — 按公司颜色区分。为了简化,得分被转换为排名,从最差(左)到最好(右),分别显示在每个指标上。

图 4 通过进一步简化比较,进一步突出了基准测试和现实世界表现之间的差异。这里,图中显示了排名的相对差异,表明模型何时可能出现过拟合基准,或表现优于报告的结果。可以得出一些有趣的结论:

  • 基准过拟合: 阿里巴巴和 Mistral 两家公司都在不断推出那些在基准测试中表现更好,但在现实中表现较差的模型。它们最近的模型,包括阿里巴巴的Qwen 2.5 Coder–20.0%)和 Mistral 的Mistral Large–11.5%),也遵循了这一模式。

  • 优于报告结果: Google 凭借其生产的模型,在实际表现上显著优于报告结果,尤其是其最新的Gemini 1.5 Pro模型,表现出+31.5%的差异。Google 对“诚实训练与评估”的关注在其模型报告中得到了体现,并且他们选择开发自己的 Natural2Code 基准,而非使用 HumanEval。“Natural2Code 是一个涵盖 Python、Java、C++、JS、Go 等语言的代码生成基准。保留的数据集类似于 HumanEval,但未泄露在网络上。” ~ Google 在 Gimmini 1.5 发布中。

  • 平衡良好: Meta 在平衡基准与现实世界表现方面做得非常有趣且特别好。由于图中显示的是排名而非得分,这种稳定性也取决于其他模型的表现。

图 4:从 HumanEval 到 Elo 分数的表现差异——按公司着色。负分表示 HumanEval 优于 Elo(基准过拟合),而正分表示 Elo 优于 HumanEval(表现优于报告)。

结论:阿里巴巴和 Mistral 倾向于创建在基准数据上过拟合的模型。

结论:Google 的模型在基准测试结果中被低估,原因在于其专注于公平的训练和评估。

平衡性能和价格:提供最佳性价比的模型

在选择 LLM 作为编程伴侣时,性能并不是唯一需要考虑的因素。另一个重要的维度是价格。本节重新评估了不同的 LLM,并比较了它们在性能(由其 Elo 评分表示)和价格方面的表现。

在开始比较之前,值得注意的是一个例外:Meta。Meta 的 Llama 模型是开源的,并非由 Meta 自己托管。然而,鉴于它们的受欢迎程度,我仍然将其纳入其中。这些模型的定价是大三云服务商(Google、Microsoft、Amazon)提供的最优按需定价——通常是 AWS 的价格。

图 5 对不同的模型进行了比较,并展示了帕累托前沿。使用 Elo 评分来代表模型表现,这被认为是最好的选择,因为 Elo 评分是由人类评估的,并且不包括过拟合偏差。接下来,使用按需付费的 API 价格,所显示的价格为生成一百万个 token 的输入和输出 token 费用的平均值。

图 5:按 API 价格分类的模型编码表现(Elo 评分)——按公司着色。组成帕累托前沿的模型已标注。

帕累托前沿仅由两家公司提供的模型构成:OpenAI 和谷歌。正如前面所提到的,OpenAI 的模型在性能上占据主导地位,并且它们的定价也相对合理。与此同时,谷歌似乎专注于较轻量——因此更便宜——但仍然表现良好的模型。这是有道理的,因为他们专注于适用于移动操作系统(Android)的设备端 LLM 应用场景,这对其战略具有重要价值。

结论:帕累托前沿由来自 OpenAI(高性能)或谷歌(高性价比)的模型组成。

下一个图表展示了在使用 HumanEval 而不是 Elo 评分来表示编码表现时的相似趋势。一些观察结果:

  • Anthropic 的Claude 3.5 Haiku是唯一的显著新增项,因为该模型目前还没有 Elo 评分。它有可能成为中端高性能模型的竞争者吗?

  • 谷歌的Gemini 1.5 Pro和 Mistral 的Mistral Large之间的差异在前一节中已经解释过,该节将 HumanEval 评分与 Elo 评分进行了对比。

  • 由于谷歌的Gemini 1.5 Flash 8B没有 HumanEval 评分,因此它被排除在这个图表之外。

图 6:按 API 价格分类的模型编码表现(HumanEval 评分)——按公司着色。组成帕累托前沿的模型已标注。

数据分析:额外的见解和趋势

总结一下,我将讨论一些在当前 LLM(编码)领域值得注意的额外见解。本节探讨了三个关键观察结果:模型随时间的稳定改进、专有模型的持续主导地位以及即便是微小的模型更新也能产生显著影响。所有这些观察结果都来源于图 5 所示的基于价格对比的 Elo 评分。

模型越来越好且越来越便宜

以下图表展示了新模型如何不断提高准确度,同时降低成本。值得注意的是,三个时间段——2023 年及之前、2024 年上半年和 2024 年下半年——每个都定义了自己独特的帕累托前沿,并几乎占据了完全不同的区段。期待看到这一趋势在 2025 年如何继续发展!

图 7:由三个不同时间段所指示的时间演变——2023 年及之前、2024 年上半年和 2024 年下半年。

结论:模型变得越来越好且越来越便宜,这是几乎所有新模型发布时都能观察到的趋势。

专有模型依然占据主导地位

以下图片显示了哪些分析过的模型是专有的,哪些是开源的。我们看到,专有模型仍然主导着 LLM 编程领域。帕累托前沿仍然由这些“闭源”模型主导,无论是在高性能端还是低成本端。

然而,开源模型正在缩小差距。有趣的是,对于每个开源模型,都有一个具有相同预测性能且显著更便宜的专有模型。这表明专有模型要么更加轻量化,要么经过了更好的优化,因此需要更少的计算能力——尽管这仅仅是个人的推测。

图 8:专有模型与开源模型的对比。

结论:专有模型继续保持性能-成本帕累托前沿。

即使是小的模型更新也会产生影响

以下这张图片展示了即使是相同模型的小更新也可能带来影响。通常,这些更新会带来性能提升,逐步改进模型,而无需进行重大发布。然而,偶尔某个模型在小更新后可能会在编程任务上表现下降,但这通常伴随着价格的降低。这可能是因为模型以某种方式进行了优化,比如通过量化或剪枝其网络的一部分。

图 9:小模型更新的性能和价格演变。

结论:小的模型更新几乎总是能提高性能或降低成本。

结论:LLMs 在编程中的关键要点

LLM 在编程领域的格局正在迅速发展,新模型不断推动帕累托前沿向更高性能和/或更便宜的选项迈进。开发者必须保持对最新模型的关注,以识别那些在预算内提供最佳功能的模型。认识到现实结果与基准测试之间的不一致性对于做出明智的决策至关重要。通过仔细衡量性能与成本,开发者可以选择最符合自己需求的工具,并在这个动态变化的领域中保持领先。

以下是本文所做的所有结论的快速概览:

  • 比较 LLM 性能是很困难的。

  • OpenAI 继续主导市场,在基准性能和实际应用中位居前列。值得注意的是,o1-mini 是表现最好的模型,超越了更大的 o1 模型。

  • 更好的基准结果并不总是意味着更好的现实世界表现。建议分别独立查看两者。

  • 阿里巴巴和 Mistral 倾向于创建在基准数据上过拟合的模型。

  • 谷歌的模型在基准测试结果中被低估,因为它们更注重公平训练和评估。

  • 帕累托前沿由来自 OpenAI(高性能)或 Google(物有所值)的模型组成。

  • 模型在不断系统性地提高性能并降低成本,这一趋势几乎出现在每一次新模型发布中。

  • 专有模型仍然占据着性能-成本的帕累托前沿。

  • 小幅的模型更新几乎总是能提高性能或降低成本。

觉得有用吗?欢迎在LinkedIn上关注我,查看我接下来的探索!

本文中展示的图像由我本人创作,除非另有说明。

LLMs 为每个人:在 Google Colab 上运行 HuggingFace 文本生成推理

原文:towardsdatascience.com/llms-for-everyone-running-the-huggingface-text-generation-inference-in-google-colab-5adb3218a137?source=collection_archive---------3-----------------------#2024-01-13

免费试验大语言模型(第三部分)

Dmitrii EliuseevTowards Data Science Dmitrii Eliuseev

·发布于 Towards Data Science ·阅读时长 7 分钟·2024 年 1 月 13 日

--

图片来源:Markus Spiske,Unsplash

在第一部分中,我们使用免费的 Google Colab 实例运行了 Mistral-7B 模型,并通过 FAISS(Facebook AI 相似度搜索)数据库提取信息。在第二部分中,我们使用了 LLaMA-13B 模型和 LangChain 库进行聊天,并实现了文本摘要及其他功能。在本部分中,我将展示如何使用 HuggingFace 🤗 文本生成推理(TGI)。TGI 是一个工具包,允许我们将大语言模型(LLM)作为服务运行。与之前的部分一样,我们将在 Google Colab 实例中测试它,完全免费。

文本生成推理

文本生成推理(TGI)是一个适用于大语言模型(LLMs)的生产级工具包,用于部署和提供服务。将 LLM 作为服务运行,允许我们在不同的客户端之间使用它,从 Python 笔记本到移动应用程序。测试 TGI 的功能非常有趣,但事实证明,它的系统要求相当高,并且并非一切都如预期般顺利:

  • 免费的 Google Colab 实例仅提供 12.7 GB 的 RAM,这通常不足以一次性加载 13B 甚至 7B 模型……

每个人的 LLM:在 Google Colab 中运行 LLaMA-13B 模型和 LangChain

原文:towardsdatascience.com/llms-for-everyone-running-the-llama-13b-model-and-langchain-in-google-colab-68d88021cf0b?source=collection_archive---------4-----------------------#2024-01-05

免费实验大型语言模型(第二部分)

Dmitrii EliuseevTowards Data Science Dmitrii Eliuseev

·发表于Towards Data Science ·14 分钟阅读·2024 年 1 月 5 日

--

由 Glib Albovsky 拍摄,Unsplash

在故事的第一部分中,我们使用了一个免费的 Google Colab 实例来运行 Mistral-7B 模型,并使用 FAISS(Facebook AI 相似度搜索)数据库提取信息。在这一部分中,我们将进一步深入,我将展示如何运行 LLaMA 2 13B 模型;我们还将测试一些额外的 LangChain 功能,比如制作基于聊天的应用程序和使用代理。与第一部分相同,所有使用的组件都是基于开源项目,并且完全免费使用。

让我们开始吧!

LLaMA.cpp

LLaMA.CPP 是一个非常有趣的开源项目,最初设计用于在 MacBook 上运行 LLaMA 模型,但其功能远远超出了这一点。首先,它是用纯 C/C++ 编写的,没有外部依赖,能够在任何硬件上运行(支持 CUDA、OpenCL 和 Apple Silicon,甚至可以在 Raspberry Pi 上运行)。其次,LLaMA.CPP 可以与LangChain连接,这使我们能够在没有 OpenAI 密钥的情况下免费测试其许多功能。最后但同样重要的是,由于 LLaMA.CPP 可以在任何地方运行,它是一个非常适合在免费的 Google Colab 实例中运行的候选者。提醒一下,Google 提供免费...

LLMs 的陷阱

原文:towardsdatascience.com/llms-pitfalls-7a33de009638?source=collection_archive---------6-----------------------#2024-05-07

介绍一些围绕 LLMs 的关键组成部分,以生成生产级应用

Pier Paolo IppolitoTowards Data Science Pier Paolo Ippolito

·发布于Towards Data Science ·阅读时间 9 分钟·2024 年 5 月 7 日

--

AI 生成(图像由作者提供)。

介绍

自从 ChatGPT 的崛起以来,大型语言模型(LLMs)变得越来越受欢迎,甚至非技术人员也开始关注它们。尽管 LLMs 本身尚无法提供一个完整的、可供广大受众使用的产品,但在本文中,我们将探讨一些用于使 LLMs 具备生产就绪能力的关键元素。

微调

数据集

像 LLAMA 这样的模型能够预测序列中的下一个标记,尽管这并不一定使它们适用于诸如问答等任务。因此,为了优化这些模型,可以使用不同类型的数据集:

  • 原始完成:如果目标是预测下一个标记,我们提供一些输入文本,让模型逐步预测接下来的步骤。

  • 填充中间目标:在这种情况下,我们有一些起始和结束文本,模型正在学习填补空白。这种方法在创建代码补全模型(如 Codex)时非常流行。

  • 指令数据集:这里的目标是教模型如何回答问题。我们有问题(指令)作为……

LLMs.txt 解析

原文:towardsdatascience.com/llms-txt-414d5121bcb3?source=collection_archive---------1-----------------------#2024-11-28

您的网络新标准:LLM 友好内容指南

Derick RuizTowards Data Science Derick Ruiz

·发表于Towards Data Science ·阅读时长 5 分钟·2024 年 11 月 28 日

--

最近,您可能已经看到一些开发工具将 LLMs.txt 支持添加到它们的文档中。这一提议的网络标准正在迅速获得广泛采用,但它到底是什么,又为什么重要呢?

虽然 robots.txt 和 sitemap.xml 是为搜索引擎设计的,但 LLMs.txt 则是为推理引擎优化的。它以 LLM 易于理解的格式,提供有关网站的信息。

那么,LLMs.txt 是如何在几乎一夜之间,从提议到成为行业趋势的呢?

LLMs.txt 解析(图片来源:Jørgen LarsenUnsplash

Mintlify 是如何推广 LLMs.txt 的

11 月 14 日,Mintlify 在其文档平台上添加了对 LLMs.txt 的支持。通过这一举措,他们使成千上万的开发工具文档对 LLM(大语言模型)更加友好,像是 Anthropic 和 Cursor 等工具。

Anthropic 和其他公司迅速在 X 平台上发布了关于其 LLMs.txt 支持的消息。更多由 Mintlify 托管的文档加入了这一行列,为这一提议的标准带来了广泛的关注。

这一势头激发了新的社区网站和工具的诞生。@ifox 创建了directory.llmstxt.cloud来索引 LLM 友好的技术文档。@screenfluent 随后也发布了llmstxt.directory

制作 dotenvx 的 Mot,构建并分享了一个开源生成工具用于 dotenvx 文档站点。Firecrawl 的 Eric Ciarla 创建了一个工具,该工具会抓取您的网站并为您生成 LLMs.txt 文件。

Anthropic 的 Claude 关系负责人 Alex Albert 在 X 平台上宣布 LLMs.txt 的支持。

谁创建了 LLMs.txt,为什么?

Answer.AI 的联合创始人 Jeremy Howard 提出了 LLMs.txt,以解决特定的技术挑战。

AI 系统只能处理有限的上下文窗口,这使得它们很难理解大型文档网站。传统的 SEO 技术是为搜索爬虫而优化的,而不是为推理引擎优化,因此无法解决这一限制。

当 AI 系统尝试直接处理 HTML 页面时,它们会被导航元素、JavaScript、CSS 和其他不必要的信息拖慢,这减少了实际内容可以占用的空间。

LLMs.txt 通过以 AI 理解的格式提供确切的信息来解决这个问题。

Jeremy Howard 的 X 个人资料,他是 LLMs.txt 网页标准提案的创建者。

LLMs.txt 文件到底是什么?

LLMs.txt 是一个具有特定结构的 Markdown 文件。该规范定义了两个不同的文件:

  1. /llms.txt:简化版文档导航视图,帮助 AI 系统快速理解你网站的结构。

  2. /llms-full.txt:一个包含所有文档内容的综合文件。

/llms.txt

该文件必须以 H1 项目名称开始,接着是一个引用摘要。后续部分使用 H2 标题来组织文档链接。“可选”部分专门标记不那么关键的资源。

# Project Name
> Brief project summary

Additional context and important notes

## Core Documentation
- Quick Start: Description of the resource
- API Reference: API documentation details

## Optional
- Additional Resources: Supplementary information

例如,查看 llmtxt.org 的LLM.txt。如需更深入的多语言示例,请查看Anthropic 的

/llms-full.txt

虽然/llms.txt提供了导航和结构,但/llms-full.txt包含完整的文档内容,采用 Markdown 格式。

# AI Review (Beta)

AI Review is a feature that allows you to review your recent changes in your codebase to catch any potential bugs.

<Frame>
  <img src="https://mintlify.s3-us-west-1.amazonaws.com/cursor/images/advanced/review.png" alt="AI Review" />
</Frame>

You can click into individual review items to see the full context in the editor, and chat with the AI to get more information.

### Custom Review Instructions

In order for AI Review to work in your favor, you can provide custom instructions for the AI to focus on. For example,
if you want the AI to focus on performance-related issues, you could put:

关注我的代码性能。


This way, AI Review will focus on the performance of your code when scanning through your changes.

### Review Options

Currently, you have a several options to choose from to review:

*   `Review Working State`
    *   This will review your uncommitted changes.
*   `Review Diff with Main Branch`
    *   This will review the diff between your current working state and the main branch.
*   `Review Last Commit`
    *   This will review the last commit you made.

上述代码片段来自 Cursor 的/llms-full.txt文件。查看Cursor 文档中的完整文件。

LLMs.txt 与 sitemap.xml 和 robots.txt 的区别

它的作用与现有的网页标准(如 sitemap.xml 和 robots.txt)截然不同。

/sitemap.xml列出了所有可索引的页面,但不帮助内容处理。AI 系统仍然需要解析复杂的 HTML,并处理额外的信息,这会占用上下文窗口。

/robots.txt建议搜索引擎爬虫访问,但也无法帮助理解内容。

/llms.txt解决了与 AI 相关的挑战。它有助于克服上下文窗口限制,去除不必要的标记和脚本,并以优化 AI 处理的结构呈现内容。

如何将 LLMs.txt 与 AI 系统一起使用

与主动爬行网络的搜索引擎不同,当前的 LLM 并不会自动发现和索引 LLMs.txt 文件。

你必须手动将文件内容提供给 AI 系统。这可以通过粘贴链接、直接将文件内容复制到提示中,或使用 AI 工具的文件上传功能来完成。

ChatGPT

首先,访问该文档或/llms-full.txt的 URL。将内容或 URL 复制到你的聊天中,提出你想要完成的具体问题。

使用 llms-full.txt 文件与 ChatGPT 结合的截图(图片来自作者)。

Claude

Claude 目前还无法浏览网页,因此请将该文档的/llms-full.txt文件内容复制到剪贴板。或者,你可以将其保存为.txt文件并上传。现在你可以放心地提问,确保它包含完整且最新的上下文。

使用 llms-full.txt 文件与 Claude 结合的截图(图片来自作者)。

Cursor

Cursor 允许你添加和索引第三方文档,并在聊天中作为上下文使用。你可以通过输入@Docs > Add new doc 来做到这一点。一个弹出框会出现,在这里你可以添加指向/llms-full.txt文件的链接。你将能够像使用其他文档一样将其作为上下文使用。

要了解更多关于此功能的信息,请参见 Cursor 的@Docs 功能

输入 llms-full.txt 文件到 Cursor 中以用作上下文的截图(图片来自作者)。

如何生成 LLMs.txt 文件

有几种不同的工具可以用来创建你自己的文件:

  • Mintlify: 自动生成托管文档的/llms.txt 和/llms-full.txt 文件。

  • dotenv 的 llmstxt:由 dotenvx 的创始人 Mot 开发的工具,可以通过你网站的sitemap.xml生成llms.txt文件。

  • Firecrawl 的 llmstxt: 由 Firecrawl 的创始人 Eric Ciarla 开发的另一款工具,使用 Firecrawl 抓取你的网站并生成llms.txt文件。

LLMs.txt 的未来是什么?

LLMs.txt 代表了向 AI 优先文档转型的趋势。

正如 SEO 对于搜索引擎的可见性至关重要一样,拥有 AI 可读的内容将成为开发工具和文档的关键。

随着更多网站采用这个文件,我们可能会看到新的工具和最佳实践出现,使内容对人类和 AI 助手都能访问。

目前,LLMs.txt 提供了一种实际的解决方案,帮助 AI 系统更好地理解和利用网页内容,特别是针对技术文档和 API。

你的服务器在自托管 LLM 时能承受多少压力?

原文:towardsdatascience.com/load-testing-self-hosted-llms-29ca8a4cf43a?source=collection_archive---------4-----------------------#2024-10-19

你需要更多的 GPU 还是一块现代的 GPU?你如何做出基础设施的决策?

Thuwarakesh MurallieTowards Data Science Thuwarakesh Murallie

·发表于Towards Data Science ·6 分钟阅读·2024 年 10 月 19 日

--

图像由作者使用 Dalle-E-2024 创建

当一群用户突然开始使用一个之前只有你和你的开发团队使用过的应用时,是什么感觉?

这就是从原型到生产阶段的百万美元问题。

就 LLM 而言,你可以进行几十次调整,以在预算和可接受的质量范围内运行你的应用。例如,你可以选择一个量化模型来降低内存使用。或者你可以微调一个小型模型,超越巨型 LLM 的性能。

## 我对 Tiny Llama 3.2 1B 进行了微调,以替代 GPT-4o

微调的努力是否比少量示例提示更值得?

towardsdatascience.com

你甚至可以调整你的基础设施,以获得更好的结果。例如,你可能想要将使用的 GPU 数量翻倍,或者选择最新一代的 GPU。

但你如何证明选项 A 比选项 B 和 C 表现更好呢?

这是我们在进入生产阶段时,最早期需要问自己的一个重要问题。所有这些选项都有它们的成本——…

Mac 上的本地 LLM 微调(M1 16GB)

原文:towardsdatascience.com/local-llm-fine-tuning-on-mac-m1-16gb-f59f4f598be7?source=collection_archive---------2-----------------------#2024-08-01

初学者友好的 Python 代码演示(特邀:MLX)

Shaw TalebiTowards Data Science Shaw Talebi

·发表于Towards Data Science ·8 分钟阅读·2024 年 8 月 1 日

--

本文是关于使用大型语言模型(LLM)实践的系列文章的一部分。在上一篇文章中,我展示了如何在 Google Colab 上使用一块(免费的)GPU 微调 LLM。虽然那个例子(以及许多其他例子)可以在 Nvidia 硬件上轻松运行,但它们无法轻松适配 M 系列 Mac。在本文中,我将介绍如何在 Mac 上本地微调 LLM 的一种简单方法。

图片由Myron Mott提供,来源于Unsplash

随着开源大型语言模型(LLM)和高效微调方法的兴起,构建定制的机器学习解决方案变得前所未有的容易。现在,任何拥有一块 GPU 的人都可以在本地机器上微调 LLM

然而,由于苹果的 M 系列芯片,Mac 用户在这一趋势中基本被排除在外。这些芯片采用了统一内存架构,这使得不再需要 GPU。因此,许多(以 GPU 为中心的)开源工具无法兼容(或无法充分利用)现代 Mac 的计算能力,用于运行和训练 LLM。

我差点放弃了在本地训练 LLM 的梦想,直到我发现了 MLX Python 库。

本地 RAG 从零开始

原文:towardsdatascience.com/local-rag-from-scratch-3afc6d3dea08?source=collection_archive---------0-----------------------#2024-05-11

开发并部署一个完全本地化的 RAG 系统

Joe SassonTowards Data Science Joe Sasson

·发布于Towards Data Science ·18 分钟阅读·2024 年 5 月 11 日

--

图片由Kevin Ku提供,来源:Unsplash

引言

类似于llama-indexLangchain等库所提供的高级抽象,简化了检索增强生成(RAG)系统的开发。然而,要完全发挥这些库的潜力,深入理解其背后的机制仍然至关重要,尤其是对于任何机器学习工程师来说。在本文中,我将引导你从零开始开发一个 RAG 系统。我还将进一步带你走得更远,我们将创建一个容器化的 Flask API。我设计这篇教程时,注重实际应用:整个过程灵感来源于真实的使用案例,确保你获得的见解不仅是理论性的,而且是可以立即应用的。

用例概述 — 该实现设计用于处理各种文档类型。虽然当前示例使用了许多小型文档,每个文档描述了一个独立的产品,包含 SKU、名称、描述、价格和尺寸等细节,但这种方法具有高度的适应性。无论任务是涉及对多样化书籍库的索引,还是从大量合同中挖掘数据,亦或其他任何一类文档,该系统都可以根据这些不同情境的具体需求进行调整。这种灵活性使得不同类型的信息能够无缝集成和处理。

快速提示——此实现仅适用于文本数据。你可以按照类似的步骤,使用多模态模型如 CLIP 将图像转换为嵌入,然后进行索引和查询。

目录

  • 概述模块化框架

  • 准备数据

  • 分块、索引和检索 (核心功能)

  • 大语言模型组件

  • 构建并部署 API

  • 结论

模块化框架

该实现有四个主要组件,可以互换。

  • 文本数据

  • 嵌入模型

  • 大语言模型(LLM)

  • 向量存储

将这些服务集成到你的项目中非常灵活,允许你根据特定需求进行定制。在这个示例实现中,我从一个初始数据为 JSON 格式的场景开始,它方便地将数据以字符串形式提供。然而,你可能会遇到其他格式的数据,如 PDF、电子邮件或 Excel 电子表格。在这种情况下,必须通过将数据转换为字符串格式来“规范化”这些数据。根据项目需求,你可以选择将数据转为内存中的字符串,或者将其保存到文本文件中,以便进一步细化或下游处理。

同样,嵌入模型、向量存储和大语言模型(LLM)的选择可以根据你的项目需求进行定制。无论你需要一个更小或更大的模型,还是可能需要一个外部模型,这种方法的灵活性使你能够轻松地替换合适的选项。这个即插即用的能力确保了你的项目可以在不对核心架构进行重大修改的情况下,适应各种需求。

简化的模块化框架。图片来自作者。

我将主要组件用灰色高亮显示。在这个实现中,我们的向量存储将仅仅是一个 JSON 文件。再一次,根据你的使用场景,如果你一次只处理一个文件,你可能只想使用内存中的向量存储(Python 字典)。如果你需要持久化这些数据,就像我们这个用例中那样,你可以将它们保存在本地 JSON 文件中。如果你需要存储数十万或数百万个向量,你需要一个外部的向量存储(如 Pinecone、Azure Cognitive Search 等)。

准备数据

如上所述,这个实现从 JSON 数据开始。我使用了 GPT-4 和 Claude 生成了这些数据。数据包含了不同家具产品的描述,每个描述都有其对应的 SKU。以下是一个示例:

{
    "MBR-2001": "Traditional sleigh bed crafted in rich walnut wood, featuring a curved headboard and footboard with intricate grain details. Queen size, includes a plush, supportive mattress. Produced by Heritage Bed Co. Dimensions: 65\"W x 85\"L x 50\"H.",
    "MBR-2002": "Art Deco-inspired vanity table in a polished ebony finish, featuring a tri-fold mirror and five drawers with crystal knobs. Includes a matching stool upholstered in silver velvet. Made by Luxe Interiors. Vanity dimensions: 48\"W x 20\"D x 30\"H, Stool dimensions: 22\"W x 16\"D x 18\"H.",
    "MBR-2003": "Set of sheer linen drapes in soft ivory, offering a delicate and airy touch to bedroom windows. Each panel measures 54\"W x 84\"L. Features hidden tabs for easy hanging. Manufactured by Tranquil Home Textiles.",

    "LVR-3001": "Convertible sofa bed upholstered in navy blue linen fabric, easily transitions from sofa to full-size sleeper. Perfect for guests or small living spaces. Features a sturdy wooden frame. Produced by SofaBed Solutions. Dimensions: 70\"W x 38\"D x 35\"H.",
    "LVR-3002": "Ornate Persian area rug in deep red and gold, hand-knotted from silk and wool. Adds a luxurious touch to any living room. Measures 8' x 10'. Manufactured by Ancient Weaves.",
    "LVR-3003": "Contemporary TV stand in matte black with tempered glass doors and chrome legs. Features integrated cable management and adjustable shelves. Accommodates up to 65-inch TVs. Made by Streamline Tech. Dimensions: 60\"W x 20\"D x 24\"H.",

    "OPT-4001": "Modular outdoor sofa set in espresso brown polyethylene wicker, includes three corner pieces and two armless chairs with water-resistant cushions in cream. Configurable to fit any patio space. Produced by Outdoor Living. Corner dimensions: 32\"W x 32\"D x 28\"H, Armless dimensions: 28\"W x 32\"D x 28\"H.",
    "OPT-4002": "Cantilever umbrella in sunflower yellow, featuring a 10-foot canopy and adjustable tilt for optimal shade. Constructed with a sturdy aluminum pole and fade-resistant fabric. Manufactured by Shade Masters. Dimensions: 120\"W x 120\"D x 96\"H.",
    "OPT-4003": "Rustic fire pit table made from faux stone, includes a natural gas hookup and a matching cover. Ideal for evening gatherings on the patio. Manufactured by Warmth Outdoor. Dimensions: 42\"W x 42\"D x 24\"H.",

    "ENT-5001": "Digital jukebox with touchscreen interface and built-in speakers, capable of streaming music and playing CDs. Retro design with modern technology, includes customizable LED lighting. Produced by RetroSound. Dimensions: 24\"W x 15\"D x 48\"H.",
    "ENT-5002": "Gaming console storage unit in sleek black, featuring designated compartments for systems, controllers, and games. Ventilated to prevent overheating. Manufactured by GameHub. Dimensions: 42\"W x 16\"D x 24\"H.",
    "ENT-5003": "Virtual reality gaming set by VR Innovations, includes headset, two motion controllers, and a charging station. Offers a comprehensive library of immersive games and experiences.",

    "KIT-6001": "Chef's rolling kitchen cart in stainless steel, features two shelves, a drawer, and towel bars. Portable and versatile, ideal for extra storage and workspace in the kitchen. Produced by KitchenAid. Dimensions: 30\"W x 18\"D x 36\"H.",
    "KIT-6002": "Contemporary pendant light cluster with three frosted glass shades, suspended from a polished nickel ceiling plate. Provides elegant, diffuse lighting over kitchen islands. Manufactured by Luminary Designs. Adjustable drop length up to 60\".",
    "KIT-6003": "Eight-piece ceramic dinnerware set in ocean blue, includes dinner plates, salad plates, bowls, and mugs. Dishwasher and microwave safe, adds a pop of color to any meal. Produced by Tabletop Trends.",

    "GBR-7001": "Twin-size daybed with trundle in brushed silver metal, ideal for guest rooms or small spaces. Includes two comfortable twin mattresses. Manufactured by Guestroom Gadgets. Bed dimensions: 79\"L x 42\"W x 34\"H.",
    "GBR-7002": "Wall art set featuring three abstract prints in blue and grey tones, framed in light wood. Each frame measures 24\"W x 36\"H. Adds a modern touch to guest bedrooms. Produced by Artistic Expressions.",
    "GBR-7003": "Set of two bedside lamps in brushed nickel with white fabric shades. Offers a soft, ambient light suitable for reading or relaxing in bed. Dimensions per lamp: 12\"W x 24\"H. Manufactured by Bright Nights.",

    "BMT-8001": "Industrial-style pool table with a slate top and black felt, includes cues, balls, and a rack. Perfect for entertaining and game nights in finished basements. Produced by Billiard Masters. Dimensions: 96\"L x 52\"W x 32\"H.",
    "BMT-8002": "Leather home theater recliner set in black, includes four connected seats with individual cup holders and storage compartments. Offers a luxurious movie-watching experience. Made by CinemaComfort. Dimensions per seat: 22\"W x 40\"D x 40\"H.",
    "BMT-8003": "Adjustable height pub table set with four stools, featuring a rustic wood finish and black metal frame. Ideal for casual dining or socializing in basements. Produced by Casual Home. Table dimensions: 36\"W x 36\"D x 42\"H, Stool dimensions: 15\"W x 15\"D x 30\"H."
}

在实际场景中,我们可以将其推断到数百万个 SKU 和描述,可能这些数据都存储在不同的地方。在这个场景中,聚合和组织这些数据看起来微不足道,但通常情况下,野外数据需要像这样组织成结构。

下一步是将每个 SKU 转换为单独的文本文件。总共有 105 个文本文件(SKUs)。注意——你可以在文章底部的我的 GitHub 中找到所有的数据/代码链接。

我使用这个提示生成数据,并多次发送:

Given different "categories" for furniture, I want you to generate a synthetic 'SKU' and product description.

Generate 3 for each category. Be extremely granular with your details and descriptions (colors, sizes, synthetic manufacturers, etc..).

Every response should follow this format and should be only JSON:
{<SKU>:<description>}.

- master bedroom
- living room
- outdoor patio
- entertainment 
- kitchen
- guest bedroom
- finished basement

为了继续前进,您应该有一个包含产品描述的文本文件的目录,其中 SKU 作为文件名。

分块、索引和检索

分块

给定一段文本,我们需要有效地将其分块,以便优化检索。我试图根据 llama-index 的SentenceSplitter类来建模这个过程。

import re
import os
import uuid
from transformers import AutoTokenizer, AutoModel

def document_chunker(directory_path,
                     model_name,
                     paragraph_separator='\n\n',
                     chunk_size=1024,
                     separator=' ',
                     secondary_chunking_regex=r'\S+?[\.,;!?]',
                     chunk_overlap=0):

    tokenizer = AutoTokenizer.from_pretrained(model_name)  # Load tokenizer for the specified model
    documents = {}  # Initialize dictionary to store results

    # Read each file in the specified directory
    for filename in os.listdir(directory_path):
        file_path = os.path.join(directory_path, filename)
        base = os.path.basename(file_path)
        sku = os.path.splitext(base)[0]
        if os.path.isfile(file_path):
            with open(file_path, 'r', encoding='utf-8') as file:
                text = file.read()

            # Generate a unique identifier for the document
            doc_id = str(uuid.uuid4())

            # Process each file using the existing chunking logic
            paragraphs = re.split(paragraph_separator, text)
            all_chunks = {}
            for paragraph in paragraphs:
                words = paragraph.split(separator)
                current_chunk = ""
                chunks = []

                for word in words:
                    new_chunk = current_chunk + (separator if current_chunk else '') + word
                    if len(tokenizer.tokenize(new_chunk)) <= chunk_size:
                        current_chunk = new_chunk
                    else:
                        if current_chunk:
                            chunks.append(current_chunk)
                        current_chunk = word

                if current_chunk:
                    chunks.append(current_chunk)

                refined_chunks = []
                for chunk in chunks:
                    if len(tokenizer.tokenize(chunk)) > chunk_size:
                        sub_chunks = re.split(secondary_chunking_regex, chunk)
                        sub_chunk_accum = ""
                        for sub_chunk in sub_chunks:
                            if sub_chunk_accum and len(tokenizer.tokenize(sub_chunk_accum + sub_chunk + ' ')) > chunk_size:
                                refined_chunks.append(sub_chunk_accum.strip())
                                sub_chunk_accum = sub_chunk
                            else:
                                sub_chunk_accum += (sub_chunk + ' ')
                        if sub_chunk_accum:
                            refined_chunks.append(sub_chunk_accum.strip())
                    else:
                        refined_chunks.append(chunk)

                final_chunks = []
                if chunk_overlap > 0 and len(refined_chunks) > 1:
                    for i in range(len(refined_chunks) - 1):
                        final_chunks.append(refined_chunks[i])
                        overlap_start = max(0, len(refined_chunks[i]) - chunk_overlap)
                        overlap_end = min(chunk_overlap, len(refined_chunks[i+1]))
                        overlap_chunk = refined_chunks[i][overlap_start:] + ' ' + refined_chunks[i+1][:overlap_end]
                        final_chunks.append(overlap_chunk)
                    final_chunks.append(refined_chunks[-1])
                else:
                    final_chunks = refined_chunks

                # Assign a UUID for each chunk and structure it with text and metadata
                for chunk in final_chunks:
                    chunk_id = str(uuid.uuid4())
                    all_chunks[chunk_id] = {"text": chunk, "metadata": {"file_name":sku}}  # Initialize metadata as dict

            # Map the document UUID to its chunk dictionary
            documents[doc_id] = all_chunks

    return documents

这里最重要的参数是“chunk_size”。如您所见,我们正在使用transformers库来计算给定字符串中的标记数量。因此,chunk_size 表示一个块中的标记数量。

以下是该函数内部操作的详细说明:

对指定目录中的每个文件 →

  1. 将文本分割成段落:

    • 使用指定的分隔符将输入文本划分为段落。
  2. 将段落分块成单词:

    • 对每个段落,将其拆分为单词。

    • 创建这些单词的块,确保不超过指定的标记数量(chunk_size)。

  3. 优化块:

    • 如果任何块超过了 chunk_size,则使用基于标点的正则表达式进一步拆分它。

    • 如果需要,合并子块以优化块大小。

  4. 应用重叠:

    • 对于包含多个块的序列,在它们之间创建重叠,以确保上下文的连贯性。
  5. 编译并返回块:

    • 遍历每个最终的块,给它分配一个唯一的 ID,该 ID 映射到该块的文本和元数据,最后将该块字典分配给文档 ID。

在这个例子中,我们正在对大量较小的文档进行索引,分块过程相对简单。每个文档较为简短,因此只需少量分割。这与处理更大文本的场景形成鲜明对比,例如从冗长的合同中提取特定部分或对整本小说进行索引。为了适应不同大小和复杂度的文档,我开发了document_chunker函数。这使得您可以输入数据——无论其长度或格式如何——并应用相同高效的分块过程。无论是处理简洁的产品描述,还是广袤的文学作品,document_chunker都能确保您的数据经过适当分块,以实现最佳的索引和检索效果。

用法:

docs = document_chunker(directory_path='/Users/joesasson/Desktop/articles/rag-from-scratch/text_data',
                        model_name='BAAI/bge-small-en-v1.5',
                        chunk_size=256)

keys = list(docs.keys())
print(len(docs))
print(docs[keys[0]])

Out -->
105
{'61d6318e-644b-48cd-a635-9490a1d84711': {'text': 'Gaming console storage unit in sleek black, featuring designated compartments for systems, controllers, and games. Ventilated to prevent overheating. Manufactured by GameHub. Dimensions: 42"W x 16"D x 24"H.', 'metadata': {'file_name': 'ENT-5002'}}}

现在,我们有了一个映射,具有唯一的文档 ID,指向该文档中的所有块,每个块都有自己的唯一 ID,指向该块的文本和元数据。

元数据可以包含任意的键/值对。在这里,我将文件名(SKU)设置为元数据,以便我们可以将模型的结果追溯到原始产品。

索引

现在我们已经创建了文档存储,我们需要创建向量存储。

你可能已经注意到,我们使用的是BAAI/bge-small-en-v1.5作为我们的嵌入模型。在之前的函数中,我们仅用它进行分词,现在我们将用它来将文本向量化。

为了准备部署,让我们将分词器和模型保存到本地。

from transformers import AutoModel, AutoTokenizer

model_name = "BAAI/bge-small-en-v1.5"

tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)

tokenizer.save_pretrained("model/tokenizer")
model.save_pretrained("model/embedding") 
def compute_embeddings(text):
    tokenizer = AutoTokenizer.from_pretrained("/model/tokenizer") 
    model = AutoModel.from_pretrained("/model/embedding")

    inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True) 

    # Generate the embeddings 
    with torch.no_grad():    
        embeddings = model(**inputs).last_hidden_state.mean(dim=1).squeeze()

    return embeddings.tolist()
def create_vector_store(doc_store):
    vector_store = {}
    for doc_id, chunks in doc_store.items():
        doc_vectors = {}
        for chunk_id, chunk_dict in chunks.items():
            # Generate an embedding for each chunk of text
            doc_vectors[chunk_id] = compute_embeddings(chunk_dict.get("text"))
        # Store the document's chunk embeddings mapped by their chunk UUIDs
        vector_store[doc_id] = doc_vectors
    return vector_store

我们所做的只是简单地将文档库中的块转换为嵌入。你可以插入任何嵌入模型,以及任何向量存储。由于我们的向量存储只是一个字典,所以我们所需要做的就是将其保存为 JSON 文件以便持久化。

检索

现在让我们用一个查询来测试一下!

def compute_matches(vector_store, query_str, top_k):
    """
    This function takes in a vector store dictionary, a query string, and an int 'top_k'.
    It computes embeddings for the query string and then calculates the cosine similarity against every chunk embedding in the dictionary.
    The top_k matches are returned based on the highest similarity scores.
    """
    # Get the embedding for the query string
    query_str_embedding = np.array(compute_embeddings(query_str))
    scores = {}

    # Calculate the cosine similarity between the query embedding and each chunk's embedding
    for doc_id, chunks in vector_store.items():
        for chunk_id, chunk_embedding in chunks.items():
            chunk_embedding_array = np.array(chunk_embedding)
            # Normalize embeddings to unit vectors for cosine similarity calculation
            norm_query = np.linalg.norm(query_str_embedding)
            norm_chunk = np.linalg.norm(chunk_embedding_array)
            if norm_query == 0 or norm_chunk == 0:
                # Avoid division by zero
                score = 0
            else:
                score = np.dot(chunk_embedding_array, query_str_embedding) / (norm_query * norm_chunk)

            # Store the score along with a reference to both the document and the chunk
            scores[(doc_id, chunk_id)] = score

    # Sort scores and return the top_k results
    sorted_scores = sorted(scores.items(), key=lambda item: item[1], reverse=True)[:top_k]
    top_results = [(doc_id, chunk_id, score) for ((doc_id, chunk_id), score) in sorted_scores]

    return top_results

compute_matches函数旨在从存储的文本嵌入集合中识别与给定查询字符串最相似的前 k 个文本块。下面是详细说明:

  1. 嵌入查询字符串

  2. 计算余弦相似度。对于每个块,计算查询向量和块向量之间的余弦相似度。在这里,np.linalg.norm计算的是向量的欧几里得范数(L2 范数),这是计算余弦相似度所必需的。

  3. 处理规范化并计算点积。余弦相似度定义为:

AB是向量,||A||||B||是它们的范数。

  1. 对得分进行排序和选择。得分按降序排列,选取前 k 个结果。

用法:

matches = compute_matches(vector_store=vec_store,
                query_str="Wall-mounted electric fireplace with realistic LED flames",
                top_k=3)

# matches
[('d56bc8ca-9bbc-4edb-9f57-d1ea2b62362f',
  '3086bed2-65e7-46cc-8266-f9099085e981',
  0.8600385118142513),
 ('240c67ce-b469-4e0f-86f7-d41c630cead2',
  '49335ccf-f4fb-404c-a67a-19af027a9fc2',
  0.7067269230771228),
 ('53faba6d-cec8-46d2-8d7f-be68c3080091',
  'b88e4295-5eb1-497c-8536-59afd84d2210',
  0.6959163226146977)]

# plug the top match document ID keys into doc_store to access the retrieved content
docs['d56bc8ca-9bbc-4edb-9f57-d1ea2b62362f']['3086bed2-65e7-46cc-8266-f9099085e981']

# result
{'text': 'Wall-mounted electric fireplace with realistic LED flames and heat settings. Features a black glass frame and remote control for easy operation. Ideal for adding warmth and ambiance. Manufactured by Hearth & Home. Dimensions: 50"W x 6"D x 21"H.',
 'metadata': {'file_name': 'ENT-4001'}}

每个元组包含文档 ID,其后是块 ID,再后是得分。

太棒了,已经成功!接下来要做的就是连接 LLM 组件并运行完整的端到端测试,然后我们就可以部署了!

LLM 组件

为了通过使我们的 RAG 系统更加互动来增强用户体验,我们将使用llama-cpp-python库。我们的设置将使用带有 GGUF 3 位量化的 mistral-7B 参数模型,这一配置在计算效率和性能之间提供了良好的平衡。经过大量测试,这种模型大小已被证明在资源有限的机器上特别有效,例如我的 M2 8GB Mac。通过采用这种方法,我们确保我们的 RAG 系统不仅能提供精确和相关的响应,还能保持对话语气,使其更加引人入胜和易于接触,便于最终用户使用。

在 Mac 上本地设置 LLM 的快速说明——我更倾向于使用 anaconda 或 miniconda。确保你安装了 arm64 版本,并按照库中的‘metal’设置说明进行操作,这里

现在,非常简单。我们需要做的就是定义一个函数,构建一个包含检索到的文档和用户查询的提示。LLM 的响应将返回给用户。

我定义了以下函数来流式传输 LLM 的文本响应并构建最终的提示。

from llama_cpp import Llama
import sys

def stream_and_buffer(base_prompt, llm, max_tokens=800, stop=["Q:", "\n"], echo=True, stream=True):

    # Formatting the base prompt
    formatted_prompt = f"Q: {base_prompt} A: "

    # Streaming the response from llm
    response = llm(formatted_prompt, max_tokens=max_tokens, stop=stop, echo=echo, stream=stream)

    buffer = ""

    for message in response:
        chunk = message['choices'][0]['text']
        buffer += chunk

        # Split at the last space to get words
        words = buffer.split(' ')
        for word in words[:-1]:  # Process all words except the last one (which might be incomplete)
            sys.stdout.write(word + ' ')  # Write the word followed by a space
            sys.stdout.flush()  # Ensure it gets displayed immediately

        # Keep the rest in the buffer
        buffer = words[-1]

    # Print any remaining content in the buffer
    if buffer:
        sys.stdout.write(buffer)
        sys.stdout.flush()

def construct_prompt(system_prompt, retrieved_docs, user_query):
    prompt = f"""{system_prompt}

    Here is the retrieved context:
    {retrieved_docs}

    Here is the users query:
    {user_query}
    """
    return prompt

# Usage
system_prompt = """
You are an intelligent search engine. You will be provided with some retrieved context, as well as the users query.

Your job is to understand the request, and answer based on the retrieved context.
"""

retrieved_docs = """
Wall-mounted electric fireplace with realistic LED flames and heat settings. Features a black glass frame and remote control for easy operation. Ideal for adding warmth and ambiance. Manufactured by Hearth & Home. Dimensions: 50"W x 6"D x 21"H.
"""

prompt = construct_prompt(system_prompt=system_prompt,
                          retrieved_docs=retrieved_docs,
                          user_query="I am looking for a wall-mounted electric fireplace with realistic LED flames")

llm = Llama(model_path="/Users/joesasson/Downloads/mistral-7b-instruct-v0.2.Q3_K_L.gguf", n_gpu_layers=1)

stream_and_buffer(prompt, llm)

返回给用户的最终输出:

“基于检索到的上下文和用户的查询,Hearth & Home 电壁炉配有逼真的 LED 火焰,符合描述。该型号宽 50 英寸,深 6 英寸,高 21 英寸,附带遥控器,便于操作。”

我们现在准备部署我们的 RAG 系统。请继续阅读下一部分,我们将把这段近乎“意大利面”的代码转换为可供用户使用的 API。

构建与部署 API

为了扩展我们系统的覆盖范围和可用性,我们将把它打包成一个容器化的 Flask 应用程序。此方法确保我们的模型被封装在一个 Docker 容器内,无论计算环境如何,都能提供稳定性和一致性。

你应该已经下载了上述的嵌入模型和分词器。将这些文件与应用代码、依赖项和 Dockerfile 放在同一目录下。你可以在此处下载 LLM。

你应该有以下的目录结构:

部署目录结构。图片由作者提供。

app.py

from flask import Flask, request, jsonify
import numpy as np
import json
from typing import Dict, List, Any
from llama_cpp import Llama
import torch
import logging
from transformers import AutoModel, AutoTokenizer

app = Flask(__name__)

# Set the logger level for Flask's logger
app.logger.setLevel(logging.INFO)

def compute_embeddings(text):
    tokenizer = AutoTokenizer.from_pretrained("/app/model/tokenizer") 
    model = AutoModel.from_pretrained("/app/model/embedding")

    inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True) 

    # Generate the embeddings 
    with torch.no_grad():    
        embeddings = model(**inputs).last_hidden_state.mean(dim=1).squeeze()

    return embeddings.tolist()

def compute_matches(vector_store, query_str, top_k):
    """
    This function takes in a vector store dictionary, a query string, and an int 'top_k'.
    It computes embeddings for the query string and then calculates the cosine similarity against every chunk embedding in the dictionary.
    The top_k matches are returned based on the highest similarity scores.
    """
    # Get the embedding for the query string
    query_str_embedding = np.array(compute_embeddings(query_str))
    scores = {}

    # Calculate the cosine similarity between the query embedding and each chunk's embedding
    for doc_id, chunks in vector_store.items():
        for chunk_id, chunk_embedding in chunks.items():
            chunk_embedding_array = np.array(chunk_embedding)
            # Normalize embeddings to unit vectors for cosine similarity calculation
            norm_query = np.linalg.norm(query_str_embedding)
            norm_chunk = np.linalg.norm(chunk_embedding_array)
            if norm_query == 0 or norm_chunk == 0:
                # Avoid division by zero
                score = 0
            else:
                score = np.dot(chunk_embedding_array, query_str_embedding) / (norm_query * norm_chunk)

            # Store the score along with a reference to both the document and the chunk
            scores[(doc_id, chunk_id)] = score

    # Sort scores and return the top_k results
    sorted_scores = sorted(scores.items(), key=lambda item: item[1], reverse=True)[:top_k]
    top_results = [(doc_id, chunk_id, score) for ((doc_id, chunk_id), score) in sorted_scores]

    return top_results

def open_json(path):
    with open(path, 'r') as f:
        data = json.load(f)
    return data

def retrieve_docs(doc_store, matches):
    top_match = matches[0]
    doc_id = top_match[0]
    chunk_id = top_match[1]
    docs = doc_store[doc_id][chunk_id]
    return docs

def construct_prompt(system_prompt, retrieved_docs, user_query):
    prompt = f"""{system_prompt}

    Here is the retrieved context:
    {retrieved_docs}

    Here is the users query:
    {user_query}
    """
    return prompt

@app.route('/rag_endpoint', methods=['GET', 'POST'])
def main():
    app.logger.info('Processing HTTP request')

    # Process the request
    query_str = request.args.get('query') or (request.get_json() or {}).get('query')
    if not query_str:
        return jsonify({"error":"missing required parameter 'query'"})

    vec_store = open_json('/app/vector_store.json')
    doc_store = open_json('/app/doc_store.json')

    matches = compute_matches(vector_store=vec_store, query_str=query_str, top_k=3)
    retrieved_docs = retrieve_docs(doc_store, matches)

    system_prompt = """
    You are an intelligent search engine. You will be provided with some retrieved context, as well as the users query.

    Your job is to understand the request, and answer based on the retrieved context.
    """

    base_prompt = construct_prompt(system_prompt=system_prompt, retrieved_docs=retrieved_docs, user_query=query_str)

    app.logger.info(f'constructed prompt: {base_prompt}')

    # Formatting the base prompt
    formatted_prompt = f"Q: {base_prompt} A: "

    llm = Llama(model_path="/app/mistral-7b-instruct-v0.2.Q3_K_L.gguf")
    response = llm(formatted_prompt, max_tokens=800, stop=["Q:", "\n"], echo=False, stream=False)

    return jsonify({"response": response})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001)

Dockerfile

# Use an official Python runtime as a parent image
FROM --platform=linux/arm64 python:3.11

# Set the working directory in the container to /app
WORKDIR /app

# Copy the requirements file
COPY requirements.txt .

# Update system packages, install gcc and Python dependencies
RUN apt-get update && \
    apt-get install -y gcc g++ make libtool && \
    apt-get upgrade -y && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* && \
    pip install --no-cache-dir -r requirements.txt

# Copy the current directory contents into the container at /app
COPY . /app

# Expose port 5001 to the outside world
EXPOSE 5001

# Run script when the container launches
CMD ["python", "app.py"]

需要注意的一点是——我们在 Dockerfile 的第二行设置了工作目录为‘/app’。因此,任何本地路径(模型、向量或文档存储),应该在你的应用代码中以‘/app’为前缀。

此外,当你在容器中运行应用程序(在 Mac 上时),它将无法访问 GPU,参见这个讨论。我注意到使用 CPU 时,通常需要大约 20 分钟才能得到响应。

构建与运行:

docker build -t <image-name>:<tag> .

docker run -p 5001:5001 <image-name>:<tag>

运行容器会自动启动应用程序(见 Dockerfile 的最后一行)。你现在可以通过以下 URL 访问你的端点:

http://127.0.0.1:5001/rag_endpoint

调用 API:

import requests, json

def call_api(query):
    URL = "http://127.0.0.1:5001/rag_endpoint"

    # Headers for the request
    headers = {
        "Content-Type": "application/json"
    }

    # Body for the request.
    body = {"query": query}

    # Making the POST request
    response = requests.post(URL, headers=headers, data=json.dumps(body))

    # Check if the request was successful
    if response.status_code == 200:
        return response.json()
    else:
        return f"Error: {response.status_code}, Message: {response.text}"

# Test
query = "Wall-mounted electric fireplace with realistic LED flames"

result = call_api(query)
print(result)

# result
{'response': {'choices': [{'finish_reason': 'stop', 'index': 0, 'logprobs': None, 'text': ' Based on the retrieved context, the wall-mounted electric fireplace mentioned includes features such as realistic LED flames. Therefore, the answer to the user\'s query "Wall-mounted electric fireplace with realistic LED flames" is a match to the retrieved context. The specific model mentioned in the context is manufactured by Hearth & Home and comes with additional heat settings.'}], 'created': 1715307125, 'id': 'cmpl-dd6c41ee-7c89-440f-9b04-0c9da9662f26', 'model': '/app/mistral-7b-instruct-v0.2.Q3_K_L.gguf', 'object': 'text_completion', 'usage': {'completion_tokens': 78, 'prompt_tokens': 177, 'total_tokens': 255}}}

结论

我想回顾一下所有达到这一点所需的步骤,以及将其调整为适应任何数据/嵌入/LLM 的工作流程。

  1. 将你的文本文件目录传递给document_chunker函数,以创建文档存储。

  2. 选择你的嵌入模型。将其保存在本地。

  3. 将文档存储转换为向量存储。并将这两个文件保存在本地。

  4. 从 HF Hub 下载 LLM。

  5. 将文件移动到应用目录(嵌入模型、LLM、文档存储和向量存储 JSON 文件)。

  6. 构建并运行 Docker 容器。

本质上可以总结为这一点——使用build笔记本生成 doc_store 和 vector_store,并将这些文件放置到你的应用中。

GitHub 链接。感谢阅读!

Rust 中的 LOESS

原文:towardsdatascience.com/loess-in-rust-2e22f58c81d4?source=collection_archive---------7-----------------------#2024-08-12

是时候将 Python 中的 LOESS 代码移植到 Rust 了。

João Paulo FigueiraTowards Data Science João Paulo Figueira

·发布于Towards Data Science ·阅读时长 5 分钟·2024 年 8 月 12 日

--

摄影:Matt FoxxUnsplash

五年前,从本文写作之时算起,我在 Medium 上发布了我最成功的文章。那篇文章源于一个需求:从车载远程信息处理数据流中过滤一个特别嘈杂的传感器数据。具体来说,它是连接到卡车传动轴的扭矩传感器,需要去除噪声。LOESS 是解决方案,因此写了那篇文章。

## LOESS

使用局部回归平滑数据

towardsdatascience.com

到那时,我已经深陷于 Python 的世界,并且该项目需要使用 Spark,因此在 Python 中实现算法是毫不犹豫的选择。然而,随着时间的推移,我现在更多使用 Rust,并决定尝试将旧代码移植过来。本文描述了移植过程以及我在重写代码时的选择。你应该阅读原始文章和参考材料,进一步了解算法。这里,我们将重点讨论在 Rust 中编写矩阵代码的细节,尽可能地替代之前的NumPy实现。

Rust 数值计算

作为一个坚定的反对重复造轮子的人,我寻找了推荐的 Rust 包来替代我在原始 Python 代码中使用的NumPy,很快就找到了nalgebra

nalgebra 旨在成为一个通用的低维线性代数库,提供一套针对计算机图形学和物理学优化的工具集。

尽管我们不会做任何物理学或计算机图形学相关的工作,但我们符合低维度的要求,简直是量体裁衣。

差异

当将 Python 代码转换为 Rust 时,我遇到了一些困难,花了一些时间才理清楚。在 Python 中使用 NumPy 时,我们可以利用语言和库提供的所有特性来提高代码的表达性和可读性。Rust 比 Python 更冗长,而且在撰写本文时(版本 0.33.0),nalgebra crate 仍然缺少一些有助于提高表达性的特性。简洁性是一个挑战。

我的第一个障碍是使用其他数组对数组进行索引。使用 NumPy 时,我们可以用另一个整数或布尔值数组对数组进行索引。在第一种情况下,索引数组的每个元素都是源数组的索引,索引数组的维度可以等于或小于数据数组。在布尔索引的情况下,索引数组的大小必须与数据数组相同,每个元素必须表示是否包含对应的数据元素。在使用布尔表达式选择数据时,这个特性非常方便。

尽管如此,我在整个 Python 代码中都使用了这个特性:

# Python
xx = self.n_xx[min_range]

在这里,min_range 变量是一个整数数组,包含了从 self.n_xx 数组中检索的索引子集。

尽管我尽力而为,但我未能在 Rust crate 中找到模仿 NumPy 索引的方法,因此我不得不自己实现一个。在尝试和基准测试几次之后,我得到了最终的版本。这个解决方案直接而有效。

// Rust
fn select_indices(values: &DVector<f64>,
                  indices: &DVector<usize>) -> DVector<f64> {
    indices.map(|i| values[i])
}

map 表达式非常简单,但使用函数名更具表达性,因此我将上面的 Python 代码替换为相应的 Rust 代码:

// Rust
let xx = select_indices(&self.xx, min_range);

另外,也没有内置方法从整数范围创建向量。虽然使用 nalgebra 很容易做到,但代码会稍微长一些:

// Rust
range = DVector::<usize>::from_iterator(window, 0..window);

如果我们在编译期间固定了向量和数组的大小,可以避免很多这种繁琐的操作,但由于维度未知,这里我们无法做到。对应的 Python 代码要简洁得多:

# Python
np.arange(0, window)

这种简洁性也延伸到其他领域,例如按行填充矩阵。在 Python 中,我们可以这样做:

# Python
for i in range(1, degree + 1):
    xm[:, i] = np.power(self.n_xx[min_range], i)

在撰写本文时,我发现用 nalgebra 做同样的事情,没有比这个更好的方法了:

// Rust
for i in 1..=degree {
    for j in 0..window {
        xm[(j, i)] = self.xx[min_range[j]].powi(i as i32);
    }
}

也许包中隐藏着某些东西,等待被发现,从而帮助我们提高简洁性。

最后,我发现 nalgebra 的文档相对稀少。我们可以理解这一点,因为这是一个相对年轻的 Rust crate,未来充满了潜力。

优势

最精彩的部分在最后——原始性能。我邀请你尝试运行两种版本的相同代码(GitHub 仓库链接见下方),并比较它们的性能。在我的 2019 年款 2.6 GHz 6 核 Intel Core i7 MacBook Pro 上,Rust 代码的 release 版本运行时间不到 200 微秒,而 Python 代码的运行时间不到 5 毫秒

结论

这个项目是我将旧代码从 Python 转移到 Rust 的另一个令人兴奋且富有教育意义的过程。尽管从著名的 Python 控制结构转换到 Rust 正变得越来越容易,但从 NumPy 转换到 nalgebra 的过程更加具有挑战性。Rust 包展现了很大的潜力,但仍需要更多的文档和在线支持。我热烈欢迎更为详细的用户指南。

Rust 比 Python 更加正式,但在适当使用时性能更好。在构建原型和发现阶段,我将继续使用 Python 进行日常工作,但在进入生产阶段时,我会转向 Rust 以获得更好的性能和内存安全性。我们甚至可以通过像PyO3这样的 crate 混合使用两者,所以这是一个双赢的局面。

Rust 真棒!

参考文献

joaofig/loess-rs: 一个在 Rust 中实现的 LOESS / LOWESS 算法

joaofig/pyloess: 使用 numpy 实现的简单 LOESS 算法

赞扬

我使用了Grammarly来审查写作,并接受了它的一些重写建议。

JetBrains 的 AI 助手帮助我编写了一部分代码,我也用它来学习 Rust。它已经成为我日常工作中处理 Rust 和 Python 的重要工具。不幸的是,对 nalgebra 的支持仍然不足。

João Paulo Figueira 是位于葡萄牙里斯本的 tb.lx by Daimler Truck 的数据科学家。

面包屑日志:仅显示导致错误的日志

原文:towardsdatascience.com/log-breadcrumbs-only-show-logs-leading-up-to-an-error-82b9f4c15520?source=collection_archive---------11-----------------------#2024-11-05

如何使用 Python 的内建日志包记录面包屑日志

Mike HulsTowards Data Science Mike Huls

·发表于Towards Data Science ·阅读时间:5 分钟·2024 年 11 月 5 日

--

图片由Daniel Tseng提供 / Unsplash

在本文中,我们将探讨一种有效记录面包屑日志的方法,仅显示导致错误的日志。我们将仅使用 Python 的标准日志库创建一个高效的日志设置,仅在发生异常时捕获 debug 日志。这种方法提供了详细的步骤视图,展示问题发生之前的过程,同时减少杂乱并最小化 I/O。让我们开始编码吧!

为什么要记录面包屑日志?

当错误发生时,你希望尽可能多的信息来帮助你定位代码中的问题。在这方面,记录大量信息非常有用。

缺点是所有这些日志需要被处理。然后需要写入文件或通过 HTTP 发送到端点,这可能会影响应用程序或服务器的性能。此外,这可能会使日志杂乱无章,导致在错误发生时更难找到相关信息。

面包屑方法“忽略”例如所有debug日志,除非发生错误。这使你既可以记录大量的错误详细信息,并且保持性能和概览水平。

设置面包屑日志

为什么你的服务工程师需要一个聊天机器人:故障排除的未来

原文:towardsdatascience.com/logiq-service-engineer-chatbot-04e229beee5c?source=collection_archive---------4-----------------------#2024-09-29

图片:The New Yorker / Widows © Matt Chinworth | CC BY-NC-ND 4.0

作为 2024 年 Google AI Sprint 的一部分,我构建了一个多模态聊天机器人,结合了 Gemini 1.5,下面是它如何彻底改变家电支持的方式

Ashwin Raj数据科学前沿 Ashwin Raj

·发表于数据科学前沿 ·8 分钟阅读·2024 年 9 月 29 日

--

在各行各业中,故障排除对于维持平稳运营、确保客户满意度以及优化服务流程效率至关重要。然而,现场故障排除家电设备可能是一项具有挑战性的任务。由于有各种型号和无数潜在问题,服务工程师常常不得不翻阅手册或在线搜索解决方案,这种方法既令人沮丧,又费时费力。

这就是配备全面服务知识并能访问最新故障排除手册的聊天机器人能够改变体验的地方。虽然人们可能认为检索增强生成(RAG)是此类任务的理想解决方案,但在这种情况下,它往往力不从心。因为这些手册通常包含表格、图片和图表等元素,而这些元素难以提取,且总结可能会忽略其中的复杂细节,使得它不适合用于生产部署。

在本文中,我们将着手构建一个使用 Gemini 的聊天机器人,帮助现场服务工程师更快速、更直观地找到所需信息。我们还将探讨 Gemini 提供的高级功能,如上下文缓存和文件 API 集成,以支持多模态提示。最后,我们将把这个聊天机器人封装在 Streamlit 界面中,以便于交互。

在你开始之前

为了构建聊天机器人,我们将使用 Gemini、Python 3 和 Streamlit。首先,通过运行以下命令在你的本地机器上安装 Streamlit:

pip install streamlit

对于数据库,我们将依赖于 Python 预装的 SQLite。我们还需要一个 Gemini API 密钥,以便使用 Gemini 1.5 Flash 进行推理。如果你还没有 API 密钥,你可以通过这个链接免费创建一个。一旦你设置了密钥,运行以下命令安装 Google AI Python SDK:

pip install google-generativeai

你可以在我的 GitHub 仓库这里找到源代码和其他资源。

致谢: 本项目提供了 Google Cloud 的信用额度,作为#AISprint 2024 的一部分

架构

在实现之前,让我们详细检查系统架构。该过程从从数据库中提取所需的产品手册并传递给 Gemini 开始。这充当我们的聊天机器人的知识库,为所选电器提供必要的故障排除信息。

图片来自作者

一旦文档加载完成,我们利用 Gemini 的多模态文档处理能力,从产品手册中提取所需的信息。现在,当用户与聊天机器人互动时,模型将结合上传的服务手册数据、聊天历史和其他上下文信息,提供准确而深刻的回答,解答用户的查询。

为了提高性能,我们将实现上下文缓存,优化重复查询的响应时间。最后,我们将把这个架构封装在一个简单且直观的 Streamlit Web 应用程序中,允许服务工程师无缝地与聊天代理互动,并访问他们需要的信息。

将服务手册加载到数据库中

要开始构建聊天机器人,第一步是将故障排除指南加载到我们的数据库中以供参考。由于这些文件本身没有结构,我们无法直接将它们存储在数据库中。相反,我们存储它们的文件路径:

class ServiceGuides:
    def __init__(self, db_name="database/persistent/general.db"):
        self.conn = sqlite3.connect(db_name)
        self.create_table()

    def add_service_guide(self, model_number, guide_name, guide_file_url):
        cursor = self.conn.cursor()

        cursor.execute('''
            INSERT INTO service_guides (model, guide_name, guide_url)
            VALUES (?, ?, ?)
        ''', (model_number, guide_name, guide_file_url))

        self.conn.commit()

    def fetch_guides_by_model_number(self, model_number):
        cursor = self.conn.cursor()
        cursor.execute(
            """SELECT guide_url FROM service_guides WHERE model = ?""",
            (model_number,),
        )
        return cursor.fetchone()

在这个项目中,我们将在本地目录中存储手册,并将它们的文件路径保存在 SQLite 数据库中。然而,为了更好的可扩展性,建议使用对象存储服务,例如 Google Cloud Storage 来存储这些文件,并在像 Google Cloud SQL 这样的数据库服务中维护文件的 URL。

使用 Gemini 构建对话代理

一旦产品手册加载到数据库中,下一步是使用 1.5 Flash 构建代理。这个轻量级模型是 Gemini 家族的一部分,并通过一种称为“蒸馏”的过程进行了微调,其中来自更大模型的最重要的知识和技能被转移到一个更小、更高效的模型中,以支持各种高容量任务的规模。

来自The Keyword的图片,来源于 Google

为了提高速度和操作效率,1.5 Flash 模型在多模态推理方面非常高效,具有最多可达 100 万个令牌的上下文窗口,成为我们服务工程师使用场景的理想选择。

使用 1.5 Flash 进行多模态文档处理

要对我们的服务手册进行推理,首先需要将文件上传到 Gemini。Gemini API 支持将媒体文件与提示输入分开上传,使我们能够在多个请求中重复使用文件。File API 每个项目支持最多 20 GB 的文件,每个文件最大支持 2 GB:

class ServiceEngineerChatbot:
    def __init__(self):
        genai.configure(api_key=st.secrets["GEMINI_API_KEY"])

    def post_service_guide_to_gemini(self, title, path_to_service_guide):
        service_guide = genai.upload_file(
            path=path_to_service_guide,
            display_name=title,
        )

        while service_guide.state.name == 'PROCESSING':
            print('Waiting for file to be processed.')
            time.sleep(2)
            service_guide = genai.get_file(service_guide.name)

        return service_guide

要上传文件,我们使用 upload_file()方法,该方法的参数包括路径(要上传的文件路径)、名称(目标文件名,默认为系统生成的 ID)、mime_type(指定文档的 MIME 类型,如果未指定,将进行推断)和 display_name。

在继续之前,我们需要通过检查文件的元数据来验证 API 是否成功存储了上传的文件。如果文件的状态是 PROCESSING,则尚不能用于推理。一旦状态变为 ACTIVE,文件即可使用。如果状态为 FAILED,表示文件处理未成功。

对话式响应生成

上传服务手册后,下一步是利用 Gemini 1.5 的多模态文档处理能力来生成响应。API 的聊天功能允许我们收集多轮问题和答案,便于深入分析问题并逐步解决。

作者提供的图片

初始化模型时,提供具体的指导方针和上下文以塑造聊天机器人的行为至关重要。这是通过向模型提供系统指令来完成的。系统指令有助于维持上下文,指导互动风格,确保一致性,并为聊天机器人的响应设定边界,同时尽量避免幻觉现象。

class ServiceEngineerChatbot:
    def __init__(self):
        genai.configure(api_key=st.secrets["GEMINI_API_KEY"])

    def construct_flash_model(self, brand, sub_category, model_number):
        model_system_instruction = f"""
        Add your detailed system instructions here.
        These instructions should define the chatbot's behavior, tone, and 
        provide any necessary context. For example, you might include 
        guidelines about how to respond to queries, the structure of 
        responses, or information about what the chatbot should and should
        not do. Checkout my repo for this chatbot's system instructions.
        """

        model_generation_cofig = genai.types.GenerationConfig(
            candidate_count=1,
            max_output_tokens=1500,
            temperature=0.4,
        ),

        model = genai.GenerativeModel(
            model_name="gemini-1.5-flash",
            system_instruction=model_system_instruction,
            generation_config=model_generation_cofig,
        )
        return model

我们可以通过调整 GenerationConfig 类中的模型参数,进一步控制模型的响应生成。在我们的应用中,我们已将 max_output_tokens 设置为 1500,定义了每个响应的最大令牌限制,并将 temperature 设置为 0.4,以保持响应的确定性。

使用上下文缓存进行长上下文优化

在许多情况下,尤其是对于同一文档的重复查询,我们最终会将相同的输入令牌反复发送给模型。尽管这种方法可能有效,但对于大规模的生产级应用来说,它并不是最优选择。

这是 Gemini 的上下文缓存功能变得至关重要的地方,通过减少高 Token 工作负载的成本和延迟,提供更高效的解决方案。通过上下文缓存,我们可以在后续请求中引用缓存的 Token,而不是每次请求都发送相同的输入 Token。

图片由作者提供

在这个项目中,我们缓存了系统指令和服务手册文件。在大规模使用时,使用缓存的 Token 相比重复传输相同的数据显著降低了成本。默认情况下,这些缓存 Token 的生存时间(TTL)为 1 小时,但可以根据需要进行调整。一旦 TTL 到期,缓存的 Token 将自动从 Gemini 的上下文中移除。

class ServiceEngineerChatbot:
    def _generate_context_cache(
        self,
        brand,
        sub_category,
        model_number,
        service_guide_title,
        service_guide,
        ttl_mins=70,
    ):
        context_cache = caching.CachedContent.create(
            model='models/gemini-1.5-flash-001',
            display_name=f"{service_guide_title}_cache",
            system_instruction=model_system_instruction,
            contents=[
                service_guide
            ],
            ttl=datetime.timedelta(
                minutes=ttl_mins
            ),
        )

        return context_cache

需要注意的是,只有当输入 Token 数量达到 32,768 或更多时,上下文缓存才可用。如果 Token 数量低于此阈值,则需要依赖 Gemini 1.5 Flash 的标准多模态提示功能。

集成聊天机器人与 Streamlit

在我们的聊天机器人响应生成能力就位后,最后一步是将其包装成一个 Streamlit 应用,创建一个直观的用户界面供用户使用。

图片由作者提供

界面具有一个下拉菜单,用户可以选择他们正在使用的家电品牌和型号。选择后点击“配置聊天机器人”按钮,应用将把相应的服务手册传递给 Gemini,并展示聊天界面。从此,工程师可以输入他们的问题,聊天机器人将提供相关的回复。

未来展望

展望未来,有几个有前景的方向值得探索。聊天机器人的未来版本可以集成语音支持,允许工程师与聊天机器人进行更自然的交流,从而解决他们的问题。

此外,扩展系统以纳入预测性诊断功能,可以使工程师预先识别潜在问题,避免其导致设备故障。通过不断发展这个工具,目标是为服务工程师创建一个全面的支持系统,最终提升客户体验,从而改变故障排除的生态系统。

至此,我们已经结束了本文。如果您有任何问题或认为我有任何错误,请随时与我联系!您可以通过电子邮件或LinkedIn与我取得联系。直到那时,祝您学习愉快!

逻辑回归解析:带代码示例的可视化指南,适合初学者

原文:towardsdatascience.com/logistic-regression-explained-a-visual-guide-with-code-examples-for-beginners-81baf5871505?source=collection_archive---------0-----------------------#2024-09-10

分类算法

找到适合数据的最佳权重

Samy BaladramTowards Data Science Samy Baladram

·发表于Towards Data Science ·10 分钟阅读·2024 年 9 月 10 日

--

⛳️ 更多[分类算法](https://medium.com/@samybaladram/list/classification-algorithms-b3586f0a772c)解析:· 虚拟分类器 · K 近邻分类器 · 伯努利朴素贝叶斯 · 高斯朴素贝叶斯 · 决策树分类器 ▶ 逻辑回归 · 支持向量分类器 · 多层感知器

尽管一些基于概率的机器学习模型(如朴素贝叶斯)对特征独立性做出大胆假设,但逻辑回归采用了更为谨慎的方法。可以把它看作是绘制一条(或一平面)将两种结果分开的线,这样我们就可以以更大的灵活性预测概率。

所有视觉效果:作者使用 Canva Pro 创建。针对移动设备进行了优化;在桌面端可能会显得过大。

定义

逻辑回归是一种用于预测二元结果的统计方法。尽管名字中有“回归”,但它实际上用于分类而非回归。它估计实例属于某个特定类别的概率。如果估计的概率大于 50%,模型预测该实例属于该类别(反之亦然)。

📊 使用的数据集

在本文中,我们将使用这个人工高尔夫数据集(灵感来自[1])作为示例。该数据集根据天气条件预测一个人是否会打高尔夫。

与 KNN 类似,逻辑回归也要求先对数据进行缩放。将类别列转换为 0 和 1,同时缩放数值特征,以避免某一特征主导距离度量。

列:‘Outlook’(天气状况)、‘Temperature’(温度)、‘Humidity’(湿度)、‘Wind’(风速)和‘Play’(目标特征)。类别列(Outlook 和 Windy)使用独热编码(one-hot encoding)进行编码,而数值列则使用标准缩放(z-标准化)进行缩放。

# Import required libraries
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import StandardScaler
import pandas as pd
import numpy as np

# Create dataset from dictionary
dataset_dict = {
    'Outlook': ['sunny', 'sunny', 'overcast', 'rainy', 'rainy', 'rainy', 'overcast', 'sunny', 'sunny', 'rainy', 'sunny', 'overcast', 'overcast', 'rainy', 'sunny', 'overcast', 'rainy', 'sunny', 'sunny', 'rainy', 'overcast', 'rainy', 'sunny', 'overcast', 'sunny', 'overcast', 'rainy', 'overcast'],
    'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
    'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
    'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
    'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)

# Prepare data: encode categorical variables
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
df['Wind'] = df['Wind'].astype(int)
df['Play'] = (df['Play'] == 'Yes').astype(int)

# Rearrange columns
column_order = ['sunny', 'overcast', 'rainy', 'Temperature', 'Humidity', 'Wind', 'Play']
df = df[column_order]

# Split data into features and target
X, y = df.drop(columns='Play'), df['Play']

# Split data into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)

# Scale numerical features
scaler = StandardScaler()
X_train[['Temperature', 'Humidity']] = scaler.fit_transform(X_train[['Temperature', 'Humidity']])
X_test[['Temperature', 'Humidity']] = scaler.transform(X_test[['Temperature', 'Humidity']])

# Print results
print("Training set:")
print(pd.concat([X_train, y_train], axis=1), '\n')
print("Test set:")
print(pd.concat([X_test, y_test], axis=1))

主要机制

逻辑回归通过对输入特征的线性组合应用逻辑函数来工作。其操作过程如下:

  1. 计算输入特征的加权和(类似于线性回归)。

  2. 对这个和应用逻辑函数(也称为 Sigmoid 函数),它将任何实数映射到 0 和 1 之间的值。

  3. 将此值解释为属于正类的概率。

  4. 使用阈值(通常是 0.5)做出最终的分类决策。

对于我们的高尔夫数据集,逻辑回归可能会将天气因素合并为一个单一的分数,然后将此分数转换为打高尔夫的概率。

训练步骤

逻辑回归的训练过程涉及为输入特征找到最佳的权重。以下是一般的步骤概述:

  1. 初始化权重(通常为小的随机值)。

# Initialize weights (including bias) to 0.1
initial_weights = np.full(X_train_np.shape[1], 0.1)

# Create and display DataFrame for initial weights
print(f"Initial Weights: {initial_weights}")

2. 对于每个训练示例:

a. 使用当前的权重计算预测概率。

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

def calculate_probabilities(X, weights):
    z = np.dot(X, weights)
    return sigmoid(z)

def calculate_log_loss(probabilities, y):
    return -y * np.log(probabilities) - (1 - y) * np.log(1 - probabilities)

def create_output_dataframe(X, y, weights):
    probabilities = calculate_probabilities(X, weights)
    log_losses = calculate_log_loss(probabilities, y)

    df = pd.DataFrame({
        'Probability': probabilities,
        'Label': y,
        'Log Loss': log_losses
    })

    return df

def calculate_average_log_loss(X, y, weights):
    probabilities = calculate_probabilities(X, weights)
    log_losses = calculate_log_loss(probabilities, y)
    return np.mean(log_losses)

# Convert X_train and y_train to numpy arrays for easier computation
X_train_np = X_train.to_numpy()
y_train_np = y_train.to_numpy()

# Add a column of 1s to X_train_np for the bias term
X_train_np = np.column_stack((np.ones(X_train_np.shape[0]), X_train_np))

# Create and display DataFrame for initial weights
initial_df = create_output_dataframe(X_train_np, y_train_np, initial_weights)
print(initial_df.to_string(index=False, float_format=lambda x: f"{x:.6f}"))
print(f"\nAverage Log Loss: {calculate_average_log_loss(X_train_np, y_train_np, initial_weights):.6f}")

b. 通过计算其对数损失,将该概率与实际类别标签进行比较。

3. 更新权重以最小化损失(通常使用一些优化算法,如梯度下降。这包括反复进行步骤 2,直到对数损失无法进一步减小)。

def gradient_descent_step(X, y, weights, learning_rate):
    m = len(y)
    probabilities = calculate_probabilities(X, weights)
    gradient = np.dot(X.T, (probabilities - y)) / m
    new_weights = weights - learning_rate * gradient  # Create new array for updated weights
    return new_weights

# Perform one step of gradient descent (one of the simplest optimization algorithm)
learning_rate = 0.1
updated_weights = gradient_descent_step(X_train_np, y_train_np, initial_weights, learning_rate)

# Print initial and updated weights
print("\nInitial weights:")
for feature, weight in zip(['Bias'] + list(X_train.columns), initial_weights):
    print(f"{feature:11}: {weight:.2f}")

print("\nUpdated weights after one iteration:")
for feature, weight in zip(['Bias'] + list(X_train.columns), updated_weights):
    print(f"{feature:11}: {weight:.2f}")
# With sklearn, you can get the final weights (coefficients)
# and final bias (intercepts) easily.
# The result is almost the same as doing it manually above.

from sklearn.linear_model import LogisticRegression

lr_clf = LogisticRegression(penalty=None, solver='saga')
lr_clf.fit(X_train, y_train)

coefficients = lr_clf.coef_
intercept = lr_clf.intercept_

y_train_prob = lr_clf.predict_proba(X_train)[:, 1]
loss = -np.mean(y_train * np.log(y_train_prob) + (1 - y_train) * np.log(1 - y_train_prob))

print(f"Weights & Bias Final: {coefficients[0].round(2)}, {round(intercept[0],2)}")
print("Loss Final:", loss.round(3))

分类步骤

一旦模型训练完成:

1. 对于新实例,使用最终权重(也称为系数)计算概率,就像训练步骤中一样。

2. 通过查看概率来解释输出:如果 p ≥ 0.5,预测为类别 1;否则,预测为类别 0

# Calculate prediction probability
predicted_probs = lr_clf.predict_proba(X_test)[:, 1]

z_values = np.log(predicted_probs / (1 - predicted_probs))

result_df = pd.DataFrame({
    'ID': X_test.index,
    'Z-Values': z_values.round(3),
    'Probabilities': predicted_probs.round(3)
}).set_index('ID')

print(result_df)

# Make predictions
y_pred = lr_clf.predict(X_test)
print(y_pred)

评估步骤

result_df = pd.DataFrame({
    'ID': X_test.index,
    'Label': y_test,
    'Probabilities': predicted_probs.round(2),
    'Prediction': y_pred,
}).set_index('ID')

print(result_df)

关键参数

逻辑回归有几个重要的参数来控制其行为:

1.惩罚项:使用的正则化类型(‘l1’,‘l2’,‘elasticnet’ 或 ‘none’)。逻辑回归中的正则化通过在模型的损失函数中加入惩罚项,防止过拟合,并鼓励简化模型。

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score

regs = [None, 'l1', 'l2']
coeff_dict = {}

for reg in regs:
    lr_clf = LogisticRegression(penalty=reg, solver='saga')
    lr_clf.fit(X_train, y_train)
    coefficients = lr_clf.coef_
    intercept = lr_clf.intercept_
    predicted_probs = lr_clf.predict_proba(X_train)[:, 1]
    loss = -np.mean(y_train * np.log(predicted_probs) + (1 - y_train) * np.log(1 - predicted_probs))
    predictions = lr_clf.predict(X_test)
    accuracy = accuracy_score(y_test, predictions)

    coeff_dict[reg] = {
        'Coefficients': coefficients,
        'Intercept': intercept,
        'Loss': loss,
        'Accuracy': accuracy
    }

for reg, vals in coeff_dict.items():
    print(f"{reg}: Coeff: {vals['Coefficients'][0].round(2)}, Intercept: {vals['Intercept'].round(2)}, Loss: {vals['Loss'].round(3)}, Accuracy: {vals['Accuracy'].round(3)}")

2.正则化强度(C):控制拟合训练数据与保持模型简洁之间的权衡。较小的 C 意味着更强的正则化。

# List of regularization strengths to try for L1
strengths = [0.001, 0.01, 0.1, 1, 10, 100]

coeff_dict = {}

for strength in strengths:
    lr_clf = LogisticRegression(penalty='l1', C=strength, solver='saga')
    lr_clf.fit(X_train, y_train)

    coefficients = lr_clf.coef_
    intercept = lr_clf.intercept_

    predicted_probs = lr_clf.predict_proba(X_train)[:, 1]
    loss = -np.mean(y_train * np.log(predicted_probs) + (1 - y_train) * np.log(1 - predicted_probs))
    predictions = lr_clf.predict(X_test)

    accuracy = accuracy_score(y_test, predictions)

    coeff_dict[f'L1_{strength}'] = {
        'Coefficients': coefficients[0].round(2),
        'Intercept': round(intercept[0],2),
        'Loss': round(loss,3),
        'Accuracy': round(accuracy*100,2)
    }

print(pd.DataFrame(coeff_dict).T)
# List of regularization strengths to try for L2
strengths = [0.001, 0.01, 0.1, 1, 10, 100]

coeff_dict = {}

for strength in strengths:
    lr_clf = LogisticRegression(penalty='l2', C=strength, solver='saga')
    lr_clf.fit(X_train, y_train)

    coefficients = lr_clf.coef_
    intercept = lr_clf.intercept_

    predicted_probs = lr_clf.predict_proba(X_train)[:, 1]
    loss = -np.mean(y_train * np.log(predicted_probs) + (1 - y_train) * np.log(1 - predicted_probs))
    predictions = lr_clf.predict(X_test)
    accuracy = accuracy_score(y_test, predictions)

    coeff_dict[f'L2_{strength}'] = {
        'Coefficients': coefficients[0].round(2),
        'Intercept': round(intercept[0],2),
        'Loss': round(loss,3),
        'Accuracy': round(accuracy*100,2)
    }

print(pd.DataFrame(coeff_dict).T)

3.求解器:用于优化的算法(‘liblinear’,‘newton-cg’,‘lbfgs’,‘sag’,‘saga’)。某些正则化可能需要特定的算法。

4.最大迭代次数:求解器收敛的最大迭代次数。

对于我们的高尔夫数据集,我们可能以‘l2’惩罚项、‘liblinear’求解器和 C=1.0 作为基准进行尝试。

优点与缺点

像机器学习中的任何算法一样,逻辑回归也有其优点和局限性。

优点:

  1. 简单性:易于实现和理解。

  2. 可解释性:权重直接显示每个特征的重要性。

  3. 效率:不需要过多的计算能力。

  4. 概率输出:提供概率而不仅仅是分类。

缺点:

  1. 线性假设:假设特征与结果的对数几率之间存在线性关系。

  2. 特征独立性:假设特征之间没有高度相关性。

  3. 有限的复杂性:在决策边界高度非线性的情况下,可能出现欠拟合。

  4. 需要更多数据:需要相对较大的样本量以获得稳定的结果。

在我们的高尔夫示例中,逻辑回归可能提供一个清晰、可解释的模型,说明每个天气因素如何影响打高尔夫的决策。然而,如果决策涉及天气条件之间的复杂交互,无法通过线性模型捕捉,那么它可能会遇到困难。

最后备注

逻辑回归作为一种强大而简洁的分类工具脱颖而出。它的优势在于能够处理复杂数据的同时保持易于解释。与一些其他基础模型不同,它提供平滑的概率估计,并且能很好地处理多个特征。在现实世界中,从预测客户行为到医学诊断,逻辑回归往往表现出惊人的效果。它不仅仅是一个过渡工具——它是一个可靠的模型,在许多情况下能与更复杂的模型匹敌。

🌟 逻辑回归代码总结

# Import required libraries
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score

# Load the dataset
dataset_dict = {
    'Outlook': ['sunny', 'sunny', 'overcast', 'rainy', 'rainy', 'rainy', 'overcast', 'sunny', 'sunny', 'rainy', 'sunny', 'overcast', 'overcast', 'rainy', 'sunny', 'overcast', 'rainy', 'sunny', 'sunny', 'rainy', 'overcast', 'rainy', 'sunny', 'overcast', 'sunny', 'overcast', 'rainy', 'overcast'],
    'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0, 72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0, 88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
    'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0, 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0, 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
    'Wind': [False, True, False, False, False, True, True, False, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, False],
    'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)

# Prepare data: encode categorical variables
df = pd.get_dummies(df, columns=['Outlook'],  prefix='', prefix_sep='', dtype=int)
df['Wind'] = df['Wind'].astype(int)
df['Play'] = (df['Play'] == 'Yes').astype(int)

# Split data into training and testing sets
X, y = df.drop(columns='Play'), df['Play']
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)

# Scale numerical features
scaler = StandardScaler()
float_cols = X_train.select_dtypes(include=['float64']).columns
X_train[float_cols] = scaler.fit_transform(X_train[float_cols])
X_test[float_cols] = scaler.transform(X_test[float_cols])

# Train the model
lr_clf = LogisticRegression(penalty='l2', C=1, solver='saga')
lr_clf.fit(X_train, y_train)

# Make predictions
y_pred = lr_clf.predict(X_test)

# Evaluate the model
print(f"Accuracy: {accuracy_score(y_test, y_pred)}")

进一步阅读

关于逻辑回归及其在 scikit-learn 中的实现,读者可以参考官方文档[2],该文档提供了关于其使用和参数的全面信息。

技术环境

本文使用 Python 3.7 和 scikit-learn 1.5 版本。虽然所讨论的概念一般适用,但具体的代码实现可能会因版本不同而略有差异。

关于插图

除非另有说明,所有图片均由作者创作,采用了来自 Canva Pro 的授权设计元素。

获取简洁的视觉总结,请查看Instagram 相关帖子

参考文献

[1] T. M. Mitchell, 机器学习(1997),McGraw-Hill 科学/工程/数学,第 59 页

𝙎𝙚𝙚 𝙢𝙤𝙧𝙚 𝘾𝙡𝙖𝙨𝙨𝙞𝙛𝙞𝙘𝙖𝙩𝙞𝙤𝙣 𝘼𝙡𝙜𝙤𝙧𝙞𝙩𝙝𝙢𝙨 𝙝𝙚𝙧𝙚:

Samy Baladram

Samy Baladram

分类算法

查看列表8 个故事!

𝙔𝙤𝙪 𝙢𝙞𝙜𝙝𝙩 𝙖𝙡𝙨𝙤 𝙡𝙞𝙠𝙚:

Samy Baladram

Samy Baladram

回归算法

查看列表5 个故事!一个戴着粉色帽子的双马尾卡通娃娃。这个“假人”娃娃,通过其基本的设计和心形装饰的衬衫,直观地呈现了机器学习中“假回归器”的概念。就像这个玩具般的形象是一个简化、静态的人的代表一样,假回归器是作为基准的基本模型,用于更复杂的分析。Samy Baladram

Samy Baladram

集成学习

查看列表4 个故事!

长格式视频表示学习(第一部分:视频作为图)

原文:towardsdatascience.com/long-form-video-representation-learning-part-1-video-as-graphs-c55b609d9100?source=collection_archive---------7-----------------------#2024-05-14

我们探索了具备长格式推理能力的新型视频表示方法。第一部分着重讨论视频作为图的表示,以及如何为多个下游应用学习轻量级的图神经网络。第二部分聚焦于稀疏视频-文本变换器。而第三部分则展示了我们最新最前沿的探索。

Subarna TripathiTowards Data Science Subarna Tripathi

·发布于Towards Data Science ·10 分钟阅读·2024 年 5 月 14 日

--

现有的视频架构在处理视频内容的几秒钟后往往会遇到计算或内存瓶颈。那么,我们如何实现准确高效的长格式视觉理解呢?一个重要的第一步是拥有一个能在长视频上实际运行的模型。为此,我们探索了具备长格式推理能力的新型视频表示方法。

什么是长格式推理,为什么要进行长格式推理?

正如我们看到的,基于图像的理解任务随着深度学习模型(如卷积神经网络或变换器)取得了巨大成功,下一步自然就是超越静态图像,探索视频理解。开发视频理解模型需要两个同样重要的关注领域。首先是大规模的视频数据集,其次是用于高效提取视频特征的可学习骨干网络。即使设计者和标注者都有最好的意图,为动态信号(如视频)创建更细粒度且一致的注释也并非易事。自然,创建的大型视频数据集采用了相对更简单的方法,即在整个视频层面进行标注。至于第二个关注点,再次自然地将基于图像的模型(如 CNN 或变换器)扩展到视频理解中,因为视频被视为一系列视频帧的集合,每一帧的大小和形状都与图像相同。研究人员制作了使用采样帧作为输入的模型,而不是使用所有视频帧,这显然是为了节省内存。在具体应用中,当分析一个 5 分钟的视频片段,且视频帧率为 30 帧/秒时,我们需要处理 9,000 帧视频。若涉及到对每个视频帧提取 16x16 矩形块的密集计算,CNN 或变换器都无法处理 9,000 帧的序列。因此,大多数模型的操作方式是:它们将一个短视频片段作为输入,进行预测,然后进行时间平滑,而不是理想情况下我们希望模型能够完整地观看视频。

现在提出这个问题。如果我们需要知道一个视频是“游泳”类型还是“网球”类型,是否真的需要分析一分钟的内容?答案显然是否定的。换句话说,优化用于视频识别的模型,很可能学会了查看背景和其他空间上下文信息,而不是学习推理一个“长”视频中实际发生的事情。我们可以将这种现象称为学习空间捷径。这些模型在视频识别任务中表现良好。那么你能猜到这些模型在需要实际时间推理的其他任务(如动作预测、视频问答和最近提出的情节记忆任务)中如何表现吗?由于它们没有经过时间推理训练,因此它们在这些应用中的表现并不好。

所以我们理解到,数据集/注释使得大多数视频模型无法学习如何在时间和动作序列上进行推理。随着时间的推移,研究人员意识到了这个问题,并开始提出不同的基准来解决长时间推理问题。然而,仍然存在一个问题,主要是由内存限制引起的,即我们如何迈出第一步,让模型能够接受一段长视频作为输入,而不是将多个短片段依次处理。为了解决这个问题,我们提出了一种基于时空图学习(Spatio-Temporal Graphs Learning,简称 SPELL)的视频表示方法,以赋予模型长时间推理的能力。

视频作为时间图

设 G = (V, E)为一个图,其中 V 为节点集,E 为边集。对于社交网络、引用网络和分子结构等领域,V 和 E 是系统所提供的,我们称图作为输入提供给可学习的模型。现在,考虑视频中的最简单情况,其中每一帧视频被视为一个节点,形成 V。然而,目前尚不清楚节点 t1(时间=t1 时的帧)和节点 t2(时间=t2 时的帧)是如何连接的。因此,边集 E 没有提供。没有 E,图的拓扑结构就不完整,导致“真实值”图的不可用。一个重要的挑战依然是如何将视频转换为图。由于数据集中没有这样的标注(或“真实值”)图,所以该图可以被视为潜在图。

当视频被建模为时间图时,许多视频理解问题可以被表述为节点分类或图分类问题。我们利用 SPELL 框架来处理诸如动作边界检测、时间动作分割、视频摘要/高亮回放检测等任务。

视频摘要:表述为节点分类问题

在这里,我们提出了这样一个框架,称为 VideoSAGE,代表视频摘要与图表示学习。我们利用视频作为时间图的方法,通过该框架生成视频亮点集锦。首先,我们将输入的视频转换为图,其中每个节点对应视频的每一帧。然后,我们通过仅连接那些在指定时间距离内的节点对来对图施加稀疏性。接着,我们将视频摘要任务表述为一个二元节点分类问题,精确地对视频帧进行分类,判断它们是否应属于输出的摘要视频。以这种方式构建的图(如图 1 所示)旨在捕捉视频帧之间的长程交互,而稀疏性确保了模型在训练时不会遇到内存和计算瓶颈。对两个数据集(SumMe 和 TVSum)进行的实验表明,与现有的最先进的摘要方法相比,所提出的高效模型在计算时间和内存使用上提高了一个数量级的效率。

(图片来源:作者) 图 1:VideoSAGE 从输入视频构建图,每个节点编码一帧。我们将视频摘要问题表述为一个二元节点分类问题。

下表展示了我们方法(即 VideoSAGE)在性能和客观评分上的对比结果。这篇论文最近已被 CVPR 2024 工作坊接受。论文详细信息和更多结果请见此处

(图片来源:作者) 表 1:(左)与 SOTA 方法在 SumMe 和 TVSum 数据集上的比较,(右)使用 A2Summ、PGL-SUM 和 VideoSAGE 进行推理分析。

动作分割:作为一个节点分类问题进行表述

同样地,我们也将动作分割问题作为在这种从输入视频构建的稀疏图中的节点分类问题。GNN 结构与上面类似,唯一的区别是最后一层 GNN 使用的是图注意力网络(GAT),而不是视频摘要中使用的 SageConv。我们在 50-Salads 数据集上进行了实验。我们利用 MSTCN 或 ASFormer 作为第一阶段的初始特征提取器。接下来,我们利用我们的稀疏双向 GNN 模型,该模型利用并行的时间“前向”和“后向”局部消息传递操作。GNN 模型进一步优化了我们系统的最终细粒度每帧动作预测。有关结果,请参阅表 2。

(图片来源:作者) 表 2:在 50-Salads 数据集上进行的动作分割结果,评估指标为 F1@0.1 和准确率。

视频作为“面向对象”的时空图

在这一部分,我们将描述如何采用类似的基于图的方式,其中节点表示“对象”而不是整个视频帧。我们将从一个具体的示例开始,来描述时空图方法。

(图由作者提供) 图 2:我们将视频从视听输入数据转换为标准图,其中每个节点对应于帧中的一个人,边表示节点之间的空间或时间交互。构建的图足够稠密,可以通过跨越时间上远离但相关的节点进行信息传递,来建模长期依赖关系,但又足够稀疏,可以在低内存和计算预算下处理。ASD 任务被设定为这个长范围时空图中的二元节点分类任务。

主动说话人检测:任务被表述为节点分类

图 2 展示了我们为主动说话人检测(ASD)任务设计的框架概览。通过将视听数据作为输入,我们构建了一个多模态图,并将 ASD 转化为一个图节点分类任务。图 3 展示了图的构建过程。首先,我们创建一个图,其中每个节点对应于每帧中的一个人,边表示它们之间的空间或时间关系。初始节点特征是通过简单且轻量级的二维卷积神经网络(CNN)构建的,而不是复杂的 3D CNN 或 Transformer。接下来,我们在这个图的每个节点上执行二分类节点分类,即活跃或非活跃说话人——通过学习一个轻量级的三层图神经网络(GNN)。图是专门为编码不同面部身份之间的空间和时间依赖关系而构建的。因此,GNN 可以利用这一图结构来建模语音中的时间连续性,以及长期的时空上下文,同时需要较低的内存和计算。

你可能会问,为什么图的构建方式是这样的?这就是领域知识的影响所在。之所以在同一时间距离内,具有相同面部 ID 的节点会相互连接,是为了模拟现实世界中的场景:如果一个人在 t=1 时在拍摄,而同一个人在 t=5 时在讲话,那么很有可能他在 t=2、t=3、t=4 时也在讲话。为什么我们要将不同的面部 ID 连接在一起,如果它们共享相同的时间戳?这是因为一般来说,如果一个人在讲话,其他人更可能在听。如果我们将所有节点彼此连接,使得图变得稠密,模型不仅需要巨大的内存和计算资源,而且还会变得嘈杂。

我们在 AVA-ActiveSpeaker 数据集上进行了广泛的实验。结果表明,SPELL 优于所有先前的最先进(SOTA)方法。得益于所构建图谱的稀疏性(约 95%),与当时领先的 SOTA 方法之一 ASDNet(48.6M #Params)相比,SPELL 在视觉特征编码时需要显著更少的硬件资源(11.2M #Params)。

(作者提供的图片) 图 3:(a):我们图谱构建过程的示意图。上面的框架按时间顺序从左到右排列。蓝色、红色和黄色三种颜色表示框架中存在的三种身份。图中的每个节点对应框架中的每个面孔。SPELL 通过无向边连接同一框架中不同身份的面孔。SPELL 还通过前向/后向/无向边跨框架连接相同身份的面孔(由超参数τ控制)。在这个示例中,相同的身份通过前向边跨框架连接,前向边是有向的,仅朝时间前进的方向延伸。(b):创建后向图和无向图的过程是相同的,唯一的不同是在前者的情况下,相同身份的边缘朝相反方向延伸,而在后者的情况下则没有有向边。每个节点还包含音频信息,但此处未显示。

时间上下文的长度是多少?

请参阅下方的图 4,展示了我们的方法在两个不同应用中的时间上下文。

SPELL 中的超参数τ(在我们的实验中为 0.9 秒)对跨时间距离较远的节点之间的直接连接施加了额外的约束。连续时间戳之间的面孔身份始终是相互连接的。以下是 SPELL 的有效时间上下文大小估算。AVA-ActiveSpeaker 数据集包含 365 万帧和 530 万标注面孔,导致每帧 1.45 个面孔。平均每帧 1.45 个面孔,一个按时间顺序排序的包含 500 到 2000 个面孔的图谱可以跨越 345 到 1379 帧,相当于 25 帧/秒视频中的 13 到 55 秒。换句话说,图中的节点可能存在大约 1 分钟的时间差,而 SPELL 能够在有限的内存和计算预算下有效地推理这一长期时间窗口。值得注意的是,MAAS中的时间窗口大小为 1.9 秒,TalkNet 使用最多 4 秒的长期序列级时间上下文。

关于用于活跃说话者检测的时空图的工作已在 ECCV 2022 上发表。手稿可以在这里找到。我们在之前的博客中提供了更多细节。

(作者提供的图像) 图 4:左图和右图分别展示了我们方法在活跃说话者检测和动作检测应用中的时间支持对比。

动作检测:任务形式为节点分类

Ava 活跃说话者数据集中的 ASD 问题设置可以访问标注的面孔和标注的面部轨迹,作为问题设置的输入。这在很大程度上简化了图的构建,特别是在节点和边的识别方面。对于其他问题,例如动作检测(Action Detection),由于没有提供地面真相的物体(人)位置和轨迹,我们使用预处理来检测物体和物体轨迹,然后利用 SPELL 来进行节点分类问题。类似于之前的情况,我们利用领域知识构建稀疏图。首先构建“面向物体”的图,始终保持底层应用的目标。

平均而言,我们实现了约 90% 的稀疏图;这一点与依赖于密集通用矩阵乘法(GEMM)操作的视觉 transformer 方法有显著区别。我们的稀疏 GNN 使我们能够 (1) 实现比 transformer 方法更好的性能;(2) 在与 transformer 方法相比的 10 倍更长的时间窗口(100 秒 vs 10 秒)中聚合时间上下文;以及 (3) 相比于 transformer 方法节省 2 到 5 倍的计算资源。

GraVi-T:开源软件库

我们已经开源了我们的软件库 GraVi-T。目前,GraVi-T 支持多种视频理解应用,包括活跃说话者检测、动作检测、时间分割、视频摘要等。请查看我们的开源软件库GraVi-T了解更多应用。

亮点

与 transformers 相比,我们的图方法可以在 10 倍更长的视频上聚合上下文,消耗约 10 倍更少的内存和 5 倍更低的 FLOPs。我们在这个主题(活跃说话者检测)上的首个主要工作已发表于 ECCV’22。请关注我们即将在即将召开的 CVPR 2024 上发布的关于视频摘要(即视频高亮集锦创建)的最新成果。

我们将视频建模为稀疏图的方法在多个应用中超越了复杂的 SOTA 方法,并在多个排行榜中名列前茅。包括 2022 年 ActivityNet,2022 年 ECCV 的 Ego4D 音频-视频分割挑战赛,2023 年 CVPR 等赛事。用于训练过去挑战赛获胜模型的源代码也包含在我们的开源软件库GraVi-T中。

我们对这个通用、轻量级且高效的框架充满期待,并正在朝着其他新应用方向努力。更多令人兴奋的消息即将发布!!!

长篇视频表示学习(第二部分:视频作为稀疏 Transformer)

原文:towardsdatascience.com/long-form-video-representation-learning-part-2-video-as-sparse-transformers-29fbd0ed9e71?source=collection_archive---------9-----------------------#2024-05-14

我们探索了具备长篇推理能力的新型视频表示方法。这是第二部分,重点介绍稀疏视频-文本 Transformer。请参见关于视频作为图的第一部分。而第三部分提供了我们最新研究成果的预览。

Subarna TripathiTowards Data Science Subarna Tripathi

·发布于Towards Data Science ·6 分钟阅读·2024 年 5 月 14 日

--

本系列的第一篇博客介绍了用于“长篇”视频表示学习的显式稀疏图表示方法。这些方法是有效的;然而,它们不能进行端到端的训练。我们需要依赖其他基于 CNN 或 Transformer 的特征提取器来生成初始的节点嵌入。在本篇博客中,我们的重点是使用 Transformer 设计端到端的方法,但目标依然是实现“长篇”推理。

稀疏视频-文本 Transformer

作为一种端到端可学习的架构,我们开始探索 transformers。我们首先需要解答的问题是:视频-文本 transformer 是否能学习跨帧建模时序关系?我们观察到,尽管视频模型具有巨大的容量和丰富的多模态训练数据,最近的视频模型仍然表现出强烈的基于帧的空间表示倾向,而时序推理依然未得到有效解决。例如,如果我们打乱输入视频模型的帧顺序,输出几乎没有变化!

图片由作者提供

经过进一步调查,我们发现将多帧推理融入视频语言模型面临几个关键挑战。首先,有限的模型规模意味着空间和时间学习之间需要做出权衡(经典例子是视频 CNN 中的 2D/3D 卷积)。对于任何给定的数据集,最佳性能需要在二者之间找到微妙的平衡。其次,长时段视频模型通常具有更大的模型规模,并且更容易发生过拟合。因此,对于长视频模型,仔细分配参数和控制模型增长变得尤为重要。最后,即便扩展片段长度能改善结果,它也会面临收益递减的问题,因为视频片段所提供的信息量并不会随着采样率的增加而线性增长。如果不控制模型规模,计算量的增加可能无法抵消准确率提升带来的收益。这对基于 transformer 的架构尤为重要,因为自注意力机制的内存和时间成本是输入长度的二次方。

总之,模型复杂度应该根据输入视频自适应调整,以实现空间表示、时间表示、过拟合潜力和复杂性之间的最佳平衡。由于现有的视频-文本模型缺乏这种能力,它们要么在空间和时间建模之间达成次优平衡,要么根本没有学习到有意义的时间表示。

视频 transformers 中可以“稀疏化”的是什么?节点和边:

我们认为,视频-文本模型应学习将建模资源分配给视频数据。与其均匀地扩展模型至更长的片段,不如将这些资源分配到视频的相关时空位置,这对于从长片段中高效学习至关重要。对于 transformer 模型,这种资源分配通过修剪冗余的注意力连接自然而然地实现。我们通过探索 transformer 稀疏化技术来实现这些目标。这促使我们提出了受图模型启发的稀疏视频-文本 Transformer(SViTT)。如图 1 所示,SViTT 将视频标记视为图的顶点,自注意力模式视为连接它们的边。

我们设计了 SViTT,旨在实现节点和边缘的稀疏性:节点稀疏性通过识别信息丰富的标记(例如,表示前景中移动物体或人的标记)并剪枝背景特征嵌入来实现;边缘稀疏性则旨在减少注意力模块中的查询-键对,同时保持其全局推理能力。为了解决长输入片段的回报递减问题,我们提出通过时间稀疏扩展进行训练,采用课程学习策略,在每个训练阶段同步增加片段长度和模型稀疏性。

(作者提供的图片)图 2:(作者提供的图片)我们展示以下定性结果

结果:(1)左侧:一个训练样本包括描述(顶部的句子)和视频片段(视频的帧序列),(2)中间:视觉标记剪枝后的视频编码器的第 10 层;(3)右侧:多模态编码器在标记剪枝后的输出。

应用、评估与结果

SViTT 在多种视频-文本基准测试中进行了评估,从视频检索到问答任务,比较了先前的研究成果和我们自己的稠密模型基准。首先,我们进行了一系列消融研究,以了解稀疏建模在变换器中的优势。有趣的是,我们发现,在推理时,节点(标记)和边缘(注意力)可以大幅度剪枝,而对测试性能的影响很小。实际上,使用跨模态注意力进行的标记选择,在不重新训练的情况下,能使检索结果提高 1%。图 2 显示,SViTT 将信息丰富的区域从背景区域中分离出来,以促进高效的时间推理。

接下来,我们使用稀疏模型进行完整的预训练,并评估其下游性能。我们观察到,SViTT 在处理较长输入片段时表现良好,而稠密变换器在此时由于优化困难,准确率下降。在所有视频-文本基准测试中,SViTT 相较于稠密模型以更低的计算成本报告了可比或更好的性能,超过了包括那些使用额外图像-文本语料库训练的先前方法。

作者提供的图片

从上面的表格中,我们可以看到,通过稀疏化,立即的时间上下文聚合可以延长 2 倍(表 2)。还可以看到稀疏化如何维持最终任务的准确性(表 1),甚至有所提升。

作者提供的图片

在上述表格中,我们展示了我们提出的训练范式如何通过不同稀疏级别来帮助提高任务性能。在表 4 中,您可以看到在两个标准基准上进行的文本到视频检索任务的零-shot 性能。

作者提供的图片

最后,我们展示了在多模态检索和视频问答的不同基准上获得的结果。SViTT 超越了所有现有方法,甚至需要的预训练对数更少。

关于 SViTT 的更多细节可以在这里找到。总结来说,与原始的 transformers 相比,SViTT 的效率提高了 6 到 7 倍,能够实现 2 倍的上下文聚合。使用 SViTT 进行预训练,在 5 个基准任务(如检索、视频问答)上提高了准确性,超越了现有的最先进技术(SoTA)。

SViTT-Ego 用于以自我为中心的视频:

预训练以自我为中心的视觉-语言模型已成为提升下游自我中心视频-文本任务的关键。这些自我中心的基础模型通常使用 transformer 架构。这些模型在预训练期间的内存占用可能相当庞大。因此,我们预训练了我们自己的稀疏视频-文本 transformer 模型 SViTT-Ego,这是第一个集成边缘和节点稀疏化的稀疏自我中心视频-文本 transformer 模型。我们在EgoClip数据集上进行预训练,并引入了自我中心友好的目标 EgoNCE,而不是常用的 InfoNCE。最显著的是,SViTT-Ego 在 EgoMCQ(视频内)准确度上相比当前的最先进技术提高了 2.8%,且没有使用除标准图像增强外的额外数据增强技术,同时能够在内存有限的设备上进行预训练。以下展示了一个视觉示例。我们正在准备参加 CVPR 上的 EgoVis 研讨会,展示我们的 SViTT-Ego。

(图像由作者提供)图 3:来自 Huggingface 演示的 EgoMCQ 截图

(图像由作者提供)表 7:SViTT-Ego 在所有最先进的模型上表现优异

视频内准确度。考虑到仅在

3.8M 个样本没有叙述增强,SViTT-Ego 在

在视频间和视频内准确度上,所有模型均表现出色

(图像由作者提供)图 5:给定 qv = 0.7,我们展示了以下定性结果

使用视觉编码器的结果:第 1 行,显示 4 帧输入;第 2 行

图 2,显示视频编码器的第 4 层在视觉标记修剪后的情况;第 3 行,

显示视频编码器的第 7 层在视觉标记修剪后的情况;以及第 3 行

图 4,显示视频编码器的第 10 层在视觉标记修剪后的情况。我们

使用 SViTT 来修剪视觉标记

亮点:

我们提出了SViTT,一种视频-文本架构,统一了边缘和节点稀疏性;我们展示了它在视频语言任务中的时间建模效果。与原始的 transformer 相比,SViTT的效率提高了 6 到 7 倍,能够实现 2 倍的上下文聚合。使用 SViTT 进行预训练,在 5 个基准任务上(如检索、视频问答)提高了准确性,超越了当前的最先进技术(SoTA)。我们的稀疏视频-文本 transformer 工作首次发表于CVPR 2023

接下来,我们展示了如何利用这种稀疏变换器进行自我中心视频理解应用。我们展示了SViTT-Ego(基于 SViTT 构建)在 EgoMCQ 任务中相较于密集变换器基准表现更好,并且由于固有的稀疏性,峰值内存和计算需求显著更低。这表明像SViTT-Ego这样的稀疏架构是一个潜在的基础模型选择,特别是在内存受限设备上的预训练。请关注即将发布的激动人心的消息!

长时视频表示学习(第三部分:长时自我中心视频表示学习)

原文:towardsdatascience.com/long-form-video-representation-learning-part-3-latest-and-greatest-in-long-form-video-1b6dee0f5f6e?source=collection_archive---------11-----------------------#2024-05-14

我们探索了具备长时推理能力的新型视频表示学习方法。这是第三部分,提供了我们关于“长时”自我中心视频表示学习的最新研究成果的预览。请参见第一部分,讲解视频作为图的内容,以及第二部分,介绍稀疏视频-文本转换器。

Subarna TripathiTowards Data Science Subarna Tripathi

·发布于Towards Data Science ·8 分钟阅读·2024 年 5 月 14 日

--

本系列的前两篇博客描述了从图神经网络到稀疏转换器等不同架构模式如何应对“长时”视频表示学习的挑战。我们展示了显式基于图的方法如何聚合 5-10 倍更大的时间上下文,但它们是两阶段的方法。接下来,我们探索了如何基于转换器设计出既高效又能端到端学习的模型,并能聚合 2 倍以上更大的时间上下文。

在这篇博客中,我将带你走进我们最新最伟大的探索,特别是关于自我中心视频理解的部分。正如你所能想象的,自我中心视频或第一人称视频(通常通过头戴式相机拍摄)很可能来自于始终开启的相机,这意味着这些视频通常非常非常长,并且包含大量无关的视觉信息,尤其是在相机佩戴者移动头部时。而且,这种情况在头戴式相机中经常发生。对这类第一人称视频的适当分析可以帮助我们详细了解人类如何与环境互动,如何操作物体,以及最终,他们的目标和意图是什么。自我中心视觉系统的典型应用需要能够表示和处理视频的算法,这些视频的时间跨度通常在几分钟或几个小时之间。例如,行动预测、视频总结和情节记忆检索等应用。

自我中心行动场景图:

图 1:(图片由作者提供)自我中心行动场景图是时序动态图(G(t)),捕捉行动动词(蓝色节点)、直接或活跃物体(绿色节点)以及其他与相机佩戴者执行的活动相关的物体(黄色节点)。节点之间的边表示动词与物体之间的关系,或物体对之间的关系。该图随着时间发展,提供了自我中心视频的长时间段表示(虚线)。交互物体通过边界框进行定位

在与卡塔尼亚大学的联合研究中,我们提出了自我中心行动场景图(EASGs),这是一种用于长时间段理解自我中心视频的新表示方法。EASGs 通过提供一个基于图的时序描述,扩展了标准的手动注释自我中心视频表示,例如动词-名词行动标签,从而描述相机佩戴者执行的动作。该描述还包括交互的物体、它们之间的关系,以及动作如何随着时间展开。通过一种新的注释程序,我们扩展了 Ego4D 数据集,增加了手动标注的自我中心行动场景图,为长时间段的自我中心视频理解提供了丰富的注释集。

EASG 提供以动态图形式标注的视频片段。我们将 EASG 形式化为一个时间变化的有向图 G(t) = (V(t), E(t)),其中 V(t)是时间 t 时刻的节点集合,E(t)是这些节点之间的边的集合(见图 2)。图 G(t)的每一时刻表示一个跨越三帧的自我中心动作,这三帧定义如下[Ego4D]:前置条件(PRE)、不可回头点(PNR)和后置条件(POST)帧。因此,图 G(t)有效地与三帧关联:F(t) = {PREₜ, PNRₜ, POSTₜ},如图 1 所示。

以自我为中心的场景图生成:

图 2 展示了一个详细的标注图示例。

作者提供的图片

我们通过利用来自 Ego4D 的现有标注,结合初始化和精炼过程,得到初始的 EASG。例如,我们从添加摄像机佩戴者节点、动词节点以及从摄像机佩戴者节点到动词节点的默认动作边开始。标注流程如图 3 所示。

作者提供的图片

接下来,我们通过三名标注者的输入对图进行精炼。验证阶段会汇总三名标注者收到的数据,并确保最终标注的质量,如下所示。

图 4(作者提供图片):在验证阶段,针对标注者提出的问题示例(正确答案用红色标出),以解决标注中的歧义。

在标注阶段提供的标签。

如可以注意到的,EASG 数据集在其标签上具有独特性。下表展示了该新数据集与其他具有视觉关系的视频数据集在标签和大小方面的对比。

作者提供的图片:与现有的视频场景图数据集进行比较。我们的 Ego4D-EASG 数据集是唯一明确设计用于长格式自我中心视频理解的数据集,具有自我中心视频、动态图、平均序列长度为 3.1 分钟,以及每个序列平均 28.3 个图。*以对象-关系-对象三元组为单位测量。**不及物+及物动词谓词

上述视频直观地展示了一个标注用的 EASG 示例,它会随着视频内容的变化而动态变化。

在创建了这个独特的数据集之后,我们将描述在该数据集上评估的不同任务。第一组任务是生成动作场景图,源自图像场景图生成文献。换句话说,我们的目标是以监督方式学习 EASG 表示,并测量其在场景图文献中常用的标准 Recall 指标上的表现。我们设计了基准,并比较了不同基准在该数据集上的 EASG 生成表现。

(作者提供的图片)三项 EASG 生成任务(即 Edge Cls,SG Cls 和 EASG Cls)在 Recall@K 上的基准结果

使用 EASG 进行长篇理解任务:

我们展示了 EASG 表示在动作预测和活动摘要下游任务中的潜力。这两项任务都需要对以自我为中心的视频进行长篇推理,处理跨越不同时间步骤的长视频序列。根据最近的结果,展示了大型语言模型(LLMs)作为符号推理机器的灵活性,我们通过 OpenAI API 进行这些实验。实验旨在检查 EASG 表示的表达能力及其对下游应用的有用性。我们表明,EASG 提供了一种有效的方式来建模长篇活动,相比于广泛采用的金标准动词-名词动作编码,EASG 在自我中心视频社区中的表现更具优势。

使用 EASG 进行动作预测:

对于动作预测任务,我们使用 GPT3 的 text-davinci-003 模型。我们提示该模型从长度为 T ∈ {5,20}的序列中预测未来的动作。我们比较了两种表示——EASG 和动词-名词对序列。下表展示了此实验的结果。

作者提供的图片:动作预测任务的性能比较

即使是较短的 EASG 序列(T = 5)也往往超过较长的 V-N 序列(T = 20),这突显了 EASG 在与标准动词-名词表示相比时更强的表示能力。EASG 表示在长序列(T = 20)中取得了最佳结果。

使用 EASG 进行长篇活动摘要:

我们选择了 147 个 Ego4D-EASG 片段的子集,这些片段包含人类注释的摘要,描述了片段中执行的活动,并用 1 到 2 个句子总结。我们构建了三种类型的输入序列:图序列 S-EASG = [G(1),G(2),…,G(Tmax)],动词-名词对序列 svn = [s-vn(1),s-vn(2),…,s-vn(Tmax)],以及与 EASG 序列匹配的原始 Ego4D 叙述序列。最后一种输入用于参考,因为我们预计来自叙述的摘要将带来最佳的表现,考虑到语言模型对这种表示的自然偏好。

以下表格中报告的结果表明,相较于动词-名词序列输入,CIDEr 得分显著提高,表明处理 EASG 输入的模型能够捕捉详细的物体动作关系,从而生成更加具体、富有信息的句子,与参考描述高度契合。

作者提供的图片:使用 EASG 和动词-名词表示进行活动总结的结果

我们相信,这些贡献标志着在长篇自我中心视频理解方面迈出了重要的一步。

亮点:

  • 我们提出了自我中心动作场景图(Egocentric Action Scene Graphs),这是一种用于长篇自我中心视频理解的新型表示方法;

  • 我们通过一种新颖的注释程序,手动标注了 EASG 标签,并将其扩展到 Ego4D 数据集中;

  • 我们提出了 EASG 生成基准,并提供了初步的基准结果;

  • 我们展示了实验,突出了 EASG 表示在长篇自我中心视频理解中的有效性。我们将发布数据集和代码,以便复制数据注释过程;

    实验;

  • 我们将在下个月的CVPR 2024上展示这项工作。

  • 论文:arxiv.org/abs/2312.03391 和代码:github.com/fpv-iplab/EASG

任务的时间定位:

近年来,自我中心视频-语言预训练(VLP)在学术界和工业界得到了广泛应用。一些研究工作,如EgoVLPEgoVLPv2,从大规模视频-文本数据集中学习可转移的时空表示。最近,LaViLa证明了 VLP 可以受益于大型语言模型(LLMs)生成的密集叙述。然而,所有这些方法在处理视频序列时都会遇到内存和计算瓶颈,每个序列包含少量帧(例如 8 帧或 16 帧的模型),导致有限的时间上下文聚合能力。相反,我们的模型,称为LAVITI,具备长篇推理能力(1,000帧对比 16 帧),并且不受限于少量输入帧。

在这项正在进行的工作中,我们设计了一种新颖的方法,通过对比学习学习语言、视频和时间表示,用于长篇视频的处理。与现有方法不同,这种新方法旨在通过提取未剪辑视频中的有意义时刻,将语言、视频和时间特征对齐,并将其表述为一个直接的集合预测问题。LAVITI 在自我中心动作识别方面显著超过了现有的最先进方法,同时可以在内存和计算受限的系统上进行训练。我们的模型可以在 Ego4D 数据集上,仅用 8 个 NVIDIA RTX-3090 GPU,训练一天。

图片来自作者:在 CharadesEgo 上的表现。我们的方法在零-shot 和微调设置中均取得了显著的提升。ZS 和 FT 分别代表零-shot 和微调。

由于我们的模型能够进行具有显式时间对齐的长篇视频理解,Ego4D 自然语言查询(NLQ)任务与预训练目标非常契合。给定视频,我们可以直接预测与语言查询对齐的时间间隔;因此,LAVITI 可以

在零-shot 设置下执行 NLQ 任务(无需修改架构或重新训练 NLQ 注释)。

在不久的将来,我们计划评估其在学习改进的表征方面的潜力,尤其是在包括 NLQ 和 Moment Query (MQ) 在内的情节记忆任务中。总而言之,我们正在利用现有的基础模型(本质上是“短期”模型)来创建旨在进行 20 倍到 50 倍更大上下文聚合的“长篇”推理模块。

亮点:

我们设计了令人兴奋的新方法来进行自我中心的视频理解。我们的贡献是多方面的。

  • 预训练目标通过提取未修剪视频中的有意义时刻,共同对齐语言、视频和时间特征

  • 将视频、语言和时间对齐问题表述为一个直接的集合预测问题;

  • 以内存计算高效的方式,支持对视频中可能涉及的成千上万帧进行长篇推理;

  • 通过其在 CharadesEgo 动作识别上的优越表现,展示了 LAVITI 的有效性;

  • 支持零-shot 自然语言查询(NLQ)任务,而无需训练额外的子网络或 NLQ 注释。

关注这个新范式的“长篇”视频表示学习带来的更多激动人心的结果!

长短期记忆(LSTM)—— 改进 RNNs

原文:towardsdatascience.com/long-short-term-memory-lstm-improving-rnns-40323d1c05f8?source=collection_archive---------5-----------------------#2024-05-31

最先进的 RNNs 如何工作

Egor HowellTowards Data Science Egor Howell

·发表于 Towards Data Science ·阅读时长 9 分钟 ·2024 年 5 月 31 日

--

"www.flaticon.com/free-icons/neural-network" 标题为“neural network icons”的图标由 Freepik 创建 — Flaticon。

在本文中,我们将介绍长短期记忆网络(LSTMs),它们是常规的 vanilla 循环神经网络(RNNs)的变种,在处理长期依赖问题上表现更好。

它们使用不同的“门”来记住或忘记它们认为对预测有用或不重要的信息。

LSTMs 是 RNNs 的最先进版本。它们在工业界被广泛使用,并构成了我们今天所见的所有大型语言模型(LLMs)的基础。

我们将首先回顾 RNNs,解释梯度消失和梯度爆炸问题,然后深入探讨 LSTMs 如何工作以及为何它们是更好的模型。

RNN 概述

循环神经网络(RNN)是常规前馈神经网络的变种,它们能够更好地处理自然语言和时间序列等序列数据。

它们通过拥有一个隐藏的循环神经元,将前一个输入和输出传递到下一层,从而实现这一点。下面是一个例子。

在你的苹果硅芯片 MacBook 上进行 LoRA 微调

原文:towardsdatascience.com/lora-fine-tuning-on-your-apple-silicon-macbook-432c7dab614a?source=collection_archive---------5-----------------------#2024-11-20

让我们一步步在你的 MacBook 上进行微调

Matthew GuntonTowards Data Science Matthew Gunton

·发布于 Towards Data Science ·阅读时间:10 分钟·2024 年 11 月 20 日

--

作者图片 — Flux.1

随着模型变得越来越小,我们看到越来越多的消费级计算机能够在本地运行 LLMs。这大大降低了人们训练自己模型的门槛,并且可以尝试更多的训练技术。

一款能够在本地良好运行大型语言模型(LLMs)的消费级计算机是苹果的 Mac。苹果利用其定制的硅片,创建了一个名为 MLX 的阵列处理库。通过使用 MLX,苹果能够比许多其他消费级计算机更好地运行 LLMs。

在这篇博客文章中,我将高层次地解释 MLX 是如何工作的,然后展示如何使用 MLX 在本地微调你自己的 LLM。最后,我们将使用量化技术加速我们微调后的模型。

让我们深入了解吧!

MLX 背景

什么是 MLX(谁可以使用它?)

MLX 是苹果推出的一个开源库,允许 Mac 用户更高效地运行包含大量张量的程序。自然地,当我们想要训练或微调模型时,这个库就非常有用。

MLX 的工作原理是通过在中央处理单元(CPU)、图形处理单元(GPU)之间非常高效地进行内存传输来实现的……

理解 LoRA 第一部分:探索内在维度

原文:towardsdatascience.com/lora-intrinsic-dimensions-introduction-6ba84c727c2e?source=collection_archive---------10-----------------------#2024-10-31

高效的语言模型微调技术

Rohan JagtapTowards Data Science Rohan Jagtap

·发表于 Towards Data Science ·12 分钟阅读·2024 年 10 月 31 日

--

图片来源:ChatGPT

LoRA(低秩适配)迅速成为大型语言模型高效微调的事实标准方法。它为适应预训练模型提供了一种轻量级的方法,大大降低了传统微调方法的计算成本和内存要求。

LoRA 在论文 Hu, Edward J., 等人,“LoRA: 大型语言模型的低秩适配” 中提出,其灵感主要来源于两个观点:

  1. Li 等人,“测量目标景观的内在维度”

  2. Aghajanyan 等人,“内在维度解释了语言模型微调的有效性”

在这一系列三篇文章中,我将深入探讨这些思想,最后介绍 LoRA 本身。这将有助于我们不仅理解 LoRA 是什么,还能了解作者是如何提出这一方法的。

在本文中,我们将讨论 LoRA 背后的基本灵感——内在维度。我们将尝试理解内在维度是什么,以及它如何应用于各种深度学习任务,如在Li 等人,“测量目标景观的内在维度”中所描述的那样。

LoRA:通过不进行微调,彻底改变大型语言模型的适配方式

原文:towardsdatascience.com/lora-revolutionizing-large-language-model-adaptation-without-fine-tuning-3279d909f5af?source=collection_archive---------8-----------------------#2024-04-23

利用微调过程中权重更新的低秩特性,可以将可学习参数的数量减少数量级。

Samuel FlenderTowards Data Science Samuel Flender

·发表于Towards Data Science ·阅读时间:8 分钟·2024 年 4 月 23 日

--

图片由 ChatGPT 生成

自从 2019 年引入 BERT 以来,微调一直是将大型语言模型(LLM)适配到下游任务的标准方法。随着 LoRA 的引入,这一情况发生了变化(Hu 等人 2021),它首次展示了通过低秩分解,可以大幅简化微调过程中权重更新矩阵,通常会减少数量级的可训练参数!

LoRA 在 NLP 社区中非常受欢迎,因为它使我们能够比以往更快、更稳定地将 LLM 适配到下游任务,并且模型的占用空间更小。

让我们来看看它是如何工作的。

微调的问题

谷歌的 BERT(Devlin 等人 2019)在 NLP 领域掀起了范式的变革,特别是因为它引入了预训练/微调的范式:在大量文本数据上进行无监督的预训练后,模型可以在特定的下游任务上快速进行微调,且所需标签相对较少,因为它已经捕捉到了通用的语言模式……

低代码数据连接器和目标

原文:towardsdatascience.com/low-code-data-connectors-and-destinations-b044128c72ca?source=collection_archive---------11-----------------------#2024-10-10

开始使用 Airbyte 和云存储

Hector Andres Mejia VallejoTowards Data Science Hector Andres Mejia Vallejo

·发布于Towards Data Science ·阅读时长 11 分钟·2024 年 10 月 10 日

--

自己编写连接器代码?请三思。

创建和维护一个数据平台是一项艰巨的挑战。你不仅需要让它具有可扩展性和实用性,而且每一个架构决策都会随着时间的推移积累。数据连接器是这样一个平台的重要组成部分。毕竟,我们还能如何获取数据呢?从头开始自己构建连接器让你完全控制它们的行为方式。但要小心,随着平台中数据源的不断增加,这只能意味着以下几点:

  • 为每个新连接器编写大量代码。

  • 为每一个数据连接器维护复杂的代码。

  • 类之间的函数和定义可能会随着时间的推移而发生偏离,从而导致更复杂的维护。

当然,所有这三者都可以通过在面向对象编程中定义良好的实践来缓解。但即便如此,仍然需要许多小时的编码,这些时间本可以在后期用来更快速地为数据消费者提供服务。

数据像高速公路上的汽车一样流动。图片由Stephan Seeber提供,来源于Unsplash

如果你尝试低代码连接器会怎样?

其他选项仍然可以让你灵活地定义你想要摄取的数据及其方式,几乎不需要或只需要极少的代码。在这个选项下,你可以得到:

  • 根据提取方法标准化行为的连接器:例如,对于使用 REST API 作为核心的两个连接器,不会有分歧的类。

  • 简单但强大的用户界面,用于构建源和目标之间的连接。

  • 由构建工具的团队和社区维护的连接器。

这些好处使你能够在几分钟内构建数据连接,而不是几个小时。

然而,我并不是想向你推销这些工具;如果你需要为数据摄取实现高度可定制的逻辑,那么你将不得不自己实现它。因此,做最适合你应用的事。

练习:使用 ADLS Gen2 部署 Airbyte

让我们直接进入正题。我在本教程中使用的是 Azure。你可以注册并免费获得 $200 的服务,尝试该平台。

我们将使用 Azure Kubernetes 集群部署 Airbyte Open Source,并使用 Azure 存储 (ADLS) Gen 2 作为云存储。

创建基础设施

首先,创建以下资源:

  • 资源组,可以选择你喜欢的名称。

  • Azure Kubernetes 服务。为了避免高额费用,设置一个单节点池并使用一个节点。然而,该节点需要足够的资源,否则 Airbyte 同步将无法启动。一个合适的节点大小是 Standard_D4s_v3.

  • Azure 存储账户。创建 Git 时,开启分层命名空间功能,以便将存储账户转为 ADLS Gen2。现在,创建一个你喜欢的名称的存储容器。

生产提示:为什么要使用分层命名空间? 对象存储默认具有扁平的存储环境,这具有无限可扩展性的优点,但也有一个重要的缺点。对于分析工作负载,这会导致额外的开销,因为在读取、修改或移动文件时,必须扫描整个容器。启用此功能将文件系统中的分层目录带到可扩展的对象存储中。

将 Airbyte 部署到 Kubernetes

你需要先在你的 shell 中安装几个工具:

现在,按照以下步骤操作:

使用 shell 登录到你的 Azure 账户。

az login

设置集群凭证。

az aks get-credentials --resource-group <your-resource-group>\
                       --name <cluster-name>\
                       --overwrite-existing

添加远程 Helm 仓库并搜索 Airbyte chart。

helm repo add airbyte https://airbytehq.github.io/helm-charts
helm repo update
helm search repo airbyte

为 Airbyte 部署创建一个唯一的 Kubernetes 命名空间。我将其命名为 dev-airbyte

kubectl create namespace dev-airbyte

部署 Airbyte。

helm install airbyte airbyte/airbyte - namespace dev-airbyte

等待几分钟,直到部署完成。运行以下命令检查 pods 是否正在运行:

kubectl get pods --namespace dev-airbyte

Airbyte pod 已就绪!截图由我拍摄。

本地访问 Airbyte Web 应用

部署 Airbyte 后,你可以获取容器和端口,然后运行端口转发命令,将本地机器的端口映射到 Kubernetes Web 应用 pod 中的端口。这将允许我们通过 localhost 访问应用程序。

export POD_NAME=$(kubectl get pods - namespace dev-airbyte -l "app.kubernetes.io/name=webapp" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod - namespace dev-airbyte $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
kubectl - namespace dev-airbyte port-forward $POD_NAME 8080:$CONTAINER_PORT
echo "Visit http://127.0.0.1:8080 to use your application"

如果你在你的机器上访问 127.0.0.1:8080,你应该能看到应用程序界面。现在,我们可以开始添加数据连接器了!

生产提示: 端口转发仅适用于本地机器,且每次启动 Shell 时都必须执行。但是,对于实际场景中的数据团队,Kubernetes 允许你通过 虚拟专用网络公开你的应用程序。为此,你需要切换到* Airbyte 自托管企业版 ,它提供 单点登录 与像 Azure Active Directory 这样的云身份提供商一起保护你的工作区。

设置数据源

本次练习中的数据提供商是Tiingo,它提供来自股市公司非常有价值的信息。他们提供免费许可证,可以让你访问任何资产的每日收盘价接口以及 DOW 30 公司基本面分析。请注意,使用免费许可证时,他们的数据仅供个人查看。如果你想分享基于 Tiingo 的创作,你必须购买商业许可证。目前,我将使用免费版并指导你完成教程,但不会展示他们的实际股票数据,以遵守他们的规则。

创建账户。然后,复制提供给你的 API 密钥。我们现在准备好在 Airbyte 中设置数据源。

在 Airbyte 中创建数据源

在 Airbyte 应用中,进入Builder > Start from Scratch

Airbyte 连接器构建器界面。图片由我拍摄。

在 API 基本 URL 中写入https://api.tiingo.com/tiingo/,然后点击 YAML 按钮进行配置。输入以下内容:

type: ApiKeyAuthenticator
inject_into:
  type: RequestOption
  inject_into: header
  field_name: Authorization
  api_token: 'Token {{ config["api_key"] }}'

这将允许 API 令牌插入到每个请求的头部。现在,点击左侧的加号(+)来添加你的第一个流。下面的图片可以作为参考。

构建数据源。全局配置。图片由我拍摄。

URL 和流分区

在顶部写入End of Day Prices。这将是我们的流名称,URL 路径将是:

daily/{{ stream_partition['ticker'] }}/prices

{{}}之间的占位符是什么?这些是 Airbyte 在运行时填充的变量。在这种情况下,Airbyte 支持他们称之为流分区的功能,允许连接器根据你分区数组中的值的数量发出请求

定义 URL 路径和主键。图片由我拍摄。

向下滚动到参数化请求,勾选该框。在参数值下拉菜单中,点击用户输入,并在值文本框中输入:

{{ config['tickers_arr'] }}

请注意,这里使用的config变量也在全局配置中的 API 密钥中引用。该变量保存用户输入的值。此外,用户输入的tickers_arr将保存股票 ID 的数组。

接下来,在当前参数值标识符文本框中输入ticker。这是添加到stream_partition变量中的键,并引用来自数组tickers_arr的单个股票 ID,以进行单个 HTTP 请求。下面是该过程的截图。

定义参数化请求。图片由我拍摄。

我们将使用 4 个股票代码进行测试:

  • BA 代表波音公司

  • CAT 代表卡特彼勒公司

  • CVX 代表雪佛龙公司

  • KO 代表可口可乐公司

配置好流分区后,连接器将向 Tiingo 服务器发出 4 个请求,如下所示:

很酷吧?

生产提示: Airbyte 支持父流,这使我们可以通过请求其他端点来获取分区列表,而无需自己发出数组元素。在这个练习中我们没有这样做,但你可以查看 这里

增量同步

Airbyte 支持在增量追加模式下同步数据,即:仅同步新增修改的数据。这可以防止重新获取已从源头复制的数据。如果是第一次运行同步,它相当于完全刷新,因为所有数据都会被视为新增数据。

要在我们的连接器中实现此功能,请滚动到增量同步并勾选复选框。在游标字段文本框中输入日期,因为根据文档说明,这是表示资产更新时间的日期字段名称。对于游标日期时间格式,输入

%Y-%m-%dT%H:%M:%S.%fZ

这是 API 文档建议的输出格式。

开始日期时间下拉菜单中点击自定义,并在文本框中输入以下内容:

{{ day_delta(-1, format='%Y-%m-%dT%H:%M:%SZ') }}

它会告诉 Airbyte 插入昨天的日期。对于结束日期时间,将下拉菜单保持在现在,以便获取从开始日期到今天的数据。以下截图展示了这些步骤。

添加增量开始日期时间和结束日期时间。图片由我拍摄。

最后,勾选框以将开始时间和结束时间注入到传出的 HTTP 请求中。参数名称应分别为 startDate 和 endDate。这些参数名称也来自 Tiingo 文档。一个示例请求现在将如下所示:

用于增量加载的开始和结束时间参数。图片由我拍摄。

控制字段

我们将插入一些信息以丰富数据。为此,滚动到转换部分并勾选框。在转换下拉菜单中,点击“添加字段”。路径就是要添加的列名,写 process_date,值为 {{ today_utc() }}。这将指示记录被导入系统的时间戳。

现在,根据文档,资产的股票代码不会在响应中返回,但我们可以通过额外的转换轻松地将其添加进去。所以,对于路径,写 ticker,值应为 {{ stream_partition[‘ticker’] }}。这将把当前流分区的 ticker 值作为一列添加进去。

将我们的控制字段添加到 API 响应中。图片由我拍摄。

测试

在“测试值”按钮中,输入股票代码列表。每个股票代码用逗号分隔:BA、CAT、CVX、KO。

你应该看到类似下面的图像。

注意两个示例分区。这是 Airbyte 执行的两个独立的参数化请求。你还可以获得有关请求的实际内容、生成的数据架构以及状态信息。

转到右上角并点击 发布 以保存此连接器。可以给它任何名称,我只是叫它 Tiingo Connector。

将 Airbyte 连接到对象存储

让我们返回到存储服务,进入 Security + Networking > Access keys。复制帐户名称和一个访问密钥。注意:我们需要的是访问密钥,而不是连接字符串。

获取 Azure 存储帐户的访问密钥。图片由我拍摄。

接下来,进入你的 Airbyte 应用,选择 Destinations> Marketplace,然后点击 Azure Blob Storage。输入帐户名称、帐户密钥,并保持其他配置如图所示。此外,在可选字段中,输入你创建的容器名称。接下来,点击 设置目标

在 Airbyte 中设置目标。图片由我拍摄。

生产提示: 你组织的数据资产需要得到保护,以确保只有需要访问文件的个人或团队能够访问。你可以在存储帐户级别通过访问控制(IAM)按钮设置基于角色的访问控制,也可以通过右键点击文件夹、容器或文件来设置访问控制列表(ACLs)。

从源到目标创建连接

在 Airbyte 中建立连接有四个步骤,它将使用 Tiingo 连接器和 Azure 存储。

定义源

在 Airbyte 应用中,进入连接设置并创建一个连接。第一步是设置源。点击 设置新源。然后,在 自定义 标签下,选择我们刚刚创建的 Tiingo 连接器。

为连接创建源。图片由我拍摄。

它会提示你输入 API 密钥和股票代码。只需复制你在测试源时使用的密钥。现在点击 设置源。它将使用你的配置测试连接器。

为源添加用户输入。图片由我拍摄。

定义目标

一旦同步通过,我们将设置目标,目标是上述部分中创建的那个。此时,Airbyte 也将测试目标。

为连接添加目标。图片由我拍摄。

定义流

第三步是选择流和同步模式。由于我们只定义了一个名为 日终价格 的流,所以这是唯一可用的流。至于同步模式,这些是本次操作可用的选项:

  • 完全刷新 | 覆盖: 该模式将检索所有数据,并替换目标中任何现有的数据。

  • 完全刷新 | 追加: 该模式也会检索所有数据,但它会将新数据追加到目标中。你必须适当地去重或转换数据,以满足你后续的需求。

  • 增量 | 追加: 该模式根据我们在构建连接器时定义的增量条件请求数据。然后,它会将数据追加到目标位置。

你可以在这里阅读更多关于同步模式的信息。现在,选择 增量 | 追加

选择要获取的流。图片由我拍摄。

最终连接配置

在这里,你可以定义所需的计划以及其他附加设置。点击 完成并同步,以启动第一次数据提取和获取。

运行第一次同步过程。图片由我拍摄。

就这样!数据已经被获取。返回存储容器,你将看到一个新文件夹,其中有一个 CSV 文件。选择追加模式后,每当触发同步时,新的文件将出现在该文件夹中。

一个以流名称命名的新文件夹已创建。图片由我拍摄。

Airbyte 多次同步后的数据文件。图片由我拍摄。

结论

你可以清楚地看到这些工具的强大功能。在这种情况下,Airbyte 让你能够在几分钟内通过生产级连接器开始获取关键数据,而无需维护大量代码。此外,它还支持增量刷新和全量刷新模式,并具有附加或覆盖的功能。在这个练习中,仅演示了 Rest API 数据源,但还有许多其他类型的数据源,例如传统数据库、数据仓库、对象存储和其他平台。最后,它还提供多种目标位置,你的数据可以存储在这些位置并进一步分析,极大加快了开发过程,让你能够更快地将产品推向市场!

感谢阅读本文!如果你喜欢这篇文章,请点赞并分享。我尽力写出我在数据领域学到的东西,以此表达对这个教会我很多的社区的感谢。

下次再见!

低质量图像检测—第一部分

原文:towardsdatascience.com/low-quality-image-detection-machine-learning-fdc2c1ba86e1?source=collection_archive---------6-----------------------#2024-01-05

照片由TheRegisti拍摄,来源于Unsplash

如何使用机器学习和深度学习进行低质量图像检测(例如,模糊检测、眩光检测或噪声检测)。

Shuyi YangTowards Data Science Shuyi Yang

·发表于Towards Data Science ·8 分钟阅读·2024 年 1 月 5 日

--

低质量图像检测是一个有趣的机器学习问题,因为它解决了各类应用中的实际挑战(例如,监控系统中的模糊图像检测,或在使用智能手机拍照时的自动质量检查)。图像的质量会显著影响各种下游任务的结果,因此开发有效的检测算法至关重要。

在本教程中,我们尝试构建一个机器学习模型,能够检测照片是否存在质量问题。

一张高质量照片的示例。照片由Clay Banks拍摄,来源于Unsplash

图像质量问题可能包括:模糊、条纹、噪声、曝光过度、眩光、昏暗等。

低质量图像示例(使用将在本教程中介绍的算法生成)。原始(高质量)照片由Clay Banks拍摄,来源于Unsplash

每当我们尝试进行模糊检测眩光检测噪声检测时……

月球陨石坑检测:太空中的计算机视觉

原文:towardsdatascience.com/lunar-crater-detection-computer-vision-in-space-04e1ee416826?source=collection_archive---------2-----------------------#2024-05-06

向自主陨石坑导航迈出的第一小步

Callum BruceTowards Data Science Callum Bruce

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

--

使用 threejs_synthetic_moon 生成的月球表面合成图像,约在阿波罗 11 号着陆点上方 250 公里处

还不是 Medium 会员?点击 此处 免费访问此文章

本文将为月球轨道上的航天器开发一种基于陨石坑的自主光学导航系统中的陨石坑检测算法(CDA)。自主基于陨石坑的光学导航系统的目标是通过观察天体表面的陨石坑边缘来估计航天器的位置和姿态。

光学导航系统在航天器中的应用已经得到广泛应用[1]。星敏感器,作为一种光学导航仪器,早在 1959 年就已被部署到地球大气层之外[2]。星敏感器通过捕捉周围星空的图像,识别这些图像中的已知恒星,并将这些恒星的位置与天体目录中的恒星进行比较——通过这些数据,可以确定航天器的精确姿态。

目前用于月球轨道航天器的制导、导航和控制(GNC)的地面系统已经超负荷。随着月球轨道上航天器数量的增加,对这些地面系统的需求将继续增长。随着对重返月球的关注度不断提升……

机器学习算法作为空间之间的映射:从支持向量机(SVM)到流形学习

原文:towardsdatascience.com/machine-learning-algorithms-as-a-mapping-between-spaces-from-svms-to-manifold-learning-b1dfe1046e4f?source=collection_archive---------3-----------------------#2024-02-07

探索支持向量机(SVM)、自编码器和流形学习(等距映射)算法中空间之间映射的美

Salih SalihTowards Data Science Salih Salih

·发布于Towards Data Science ·12 分钟阅读·2024 年 2 月 7 日

--

图片来源:Evgeni Tcherkasski,Unsplash

介绍

在机器学习中,理解算法如何处理、解释和分类数据,通常依赖于“空间”这一概念。在这种情况下,空间是一个数学结构,数据点根据其特征在空间中定位。空间中的每个维度代表数据的一个特定属性或特征,使得算法能够在结构化的表示中进行导航。

特征空间与输入空间

这段旅程始于特征空间或输入空间,在这个空间中,每个数据点都是一个向量,代表数据集中的一个实例。简化一下,想象一张图像,其中每个像素是这个空间中的一个维度。空间的复杂性和维度取决于特征的数量和性质。在高维空间中工作,对数据从业者来说既可以是令人愉快的,也可以是令人沮丧的。

低维空间中的挑战

在低维空间中,并非所有的数据关系或模式都能容易地被识别。线性可分性,即通过简单的线性边界将不同类别分开,通常是无法实现的。这一局限性在复杂数据集中尤为明显,因为特征之间的交互产生了非线性模式,而这些模式无法通过简单的线性模型捕捉。

在本文中,我们将从不同空间之间的映射和交互的角度来探索机器学习算法。我们将以支持向量机(SVM)作为一个简单的例子开始,然后介绍自编码器,最后讨论流形学习和等距映射(Isomaps)。

请注意,本文中的代码示例仅用于演示,可能没有经过优化。我鼓励你修改、改进并尝试使用不同的数据集运行代码,以加深理解并获得更多的见解。

支持向量机

支持向量机(SVM)是已知的机器学习算法,在数据分类方面表现优异。正如我们在开头提到的:

在低维空间中,线性可分性通常是不可能的,这意味着用简单的线性边界划分类别非常困难。

SVM 通过将数据转换为更高维的空间,克服了这一困难,从而使得数据更容易分离和分类。为了说明这一点,下面的代码生成了在原始空间中明显无法线性分离的合成数据。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_circles

# Generate synthetic data that is not linearly separable
X, y = make_circles(n_samples=100, factor=0.5, noise=0.05)

# Visualize the data
plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.coolwarm, edgecolors='k')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.title('Original 2D Data')
plt.show()

图片来源:作者

支持向量机(SVM)通过空间之间的映射来分离不同的类别。它们将数据从低维空间提升到高维空间。在这个新空间中,SVM 找到最优的超平面,这是一个决策边界,用于分离类别。这就像是在二维图中找到完美的直线来划分群体,但在更复杂的多维宇宙中进行。

在提供的数据中,一类数据靠近原点,而另一类数据远离原点。让我们通过一个典型的例子来理解,当数据被转换到更高维度时,如何变得可分。

我们将每个二维点(x, y)转换为三维点(x, y, z),其中 z = x² + y²。这个变换根据二维空间中到原点的平方距离,增加了一个新的第三维度。在三维空间中,距离原点更远的点将在空间中更高,因为它们的平方距离较大。

from mpl_toolkits.mplot3d import Axes3D

# Transform the 2D data to 3D for visualization
Z = X[:, 0]**2 + X[:, 1]**2  # Use squared distance from the origin as the third dimension

# Visualize the 3D data
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.scatter(X[:, 0], X[:, 1], Z, c=y, cmap=plt.cm.coolwarm)

# Set labels
ax.set_xlabel('Feature 1')
ax.set_ylabel('Feature 2')
ax.set_zlabel('Transformed Feature')

# Set the viewpoint
elevation_angle = 15  # Adjust this to change the up/down angle
azimuth_angle = 45  # Adjust this to rotate the plot
ax.view_init(elev=elevation_angle, azim=azimuth_angle)

plt.show()

图片来源:作者

从上面的输出可以看出,在此变换之后,我们的数据变得可以通过一个二维超平面线性可分。

另一个例子是药物剂量的有效性。只有当剂量处于某个特定范围内时,病人才会康复。剂量过低或过高都是无效的。这个场景自然会创建一个不可线性分离的数据集,因此它是展示多项式核如何帮助解决问题的一个好例子。

# Train the SVM model on the 2D data
svc = SVC(kernel='linear', C=1.0)
svc.fit(X, y)

# Create a function to plot decision boundary
def plot_svc_decision_function(model, plot_support=True):
    """Plot the decision function for a 2D SVC"""
    ax = plt.gca()
    xlim = ax.get_xlim()
    ylim = ax.get_ylim()

    # create grid to evaluate model
    xx = np.linspace(xlim[0], xlim[1], 30)
    yy = np.linspace(ylim[0], ylim[1], 30)
    YY, XX = np.meshgrid(yy, xx)
    xy = np.vstack([XX.ravel(), YY.ravel()]).T
    Z = model.decision_function(xy).reshape(XX.shape)

    # plot decision boundary and margins
    ax.contour(XX, YY, Z, colors='k', levels=[-1, 0, 1], alpha=0.5,
               linestyles=['--', '-', '--'])
    # plot support vectors
    if plot_support:
        ax.scatter(model.support_vectors_[:, 0], model.support_vectors_[:, 1],
                   s=300, linewidth=1, facecolors='none', edgecolors='k')

# Adjust the figure size for better visualization
plt.figure(figsize=(8, 5))

# Scatter plot for original dosage points
plt.scatter(dosages, np.zeros_like(dosages), c=y, cmap='bwr', marker='s', s=50, label='Original Dosages')

# Scatter plot for dosage squared points
plt.scatter(dosages, squared_dosages, c=y, cmap='bwr', marker='^', s=50, label='Squared Dosages')

# Calling the function to plot the SVM decision boundary
plot_svc_decision_function(svc)

# Expanding the limits to ensure all points are visible
plt.xlim(min(dosages) - 1, max(dosages) + 1)
plt.ylim(min(squared_dosages) - 10, max(squared_dosages) + 10)

# Adding labels, title and legend
plt.xlabel('Dosage')
plt.ylabel('Dosage Squared')
plt.title('SVM Decision Boundary with Original and Squared Dosages')
plt.legend()

# Display the plot
plt.show()

图片来源:作者

在上面的两个例子中,我们利用了关于数据的知识。例如,在第一个例子中,我们知道有两个类别:一个接近原点,另一个远离原点。这正是算法通过训练和微调所做的——它找到一个适合的空间,在这个空间中数据可以被线性分开。

这里的一个优点是,支持向量机(SVM)并不会将数据映射到更高维度,因为这样在计算上会非常复杂。相反,它们使用点积计算数据之间的关系,就好像数据存在于更高维度中一样。这被称为“核技巧”。我将在另一篇文章中解释 SVM 的核函数。

自编码器

自编码器是非常令人惊叹且美丽的架构,深深吸引了我的想象力。它们在各个领域有广泛的应用,利用了多种类型的自编码器。

它们基本上由编码器解码器组成,编码器接收输入并对其进行编码/压缩,这是一个将高维空间转化为更加紧凑、低维空间的过程。真正有趣的是,解码器如何接着利用这种压缩后的表示,并在高维空间中重建原始数据。自然的疑问是:如何从显著降低维度的表示中恢复回原始空间呢?

让我们考虑一张分辨率为 720x720 像素的高清图片。存储和传输这张图片需要大量的内存和带宽。自编码器通过将图片压缩到一个较低维度的空间中,例如一个 32x32 的表示(称为“瓶颈”),来解决这个问题。此时编码器的工作已完成。接下来由解码器接手,尝试从这个压缩的形式中重建原始图片。

这个过程类似于在像 WhatsApp 这样的平台上分享图片。图片被编码成较低质量以进行传输,然后在接收端解码。原始图片和接收图片之间的质量差异被称为“重建误差”,这是自编码器中常见的现象。

在自编码器中,我们可以将其视为三个空间之间的相互作用:

  1. 输入空间。

  2. 潜在表示空间(瓶颈)。

  3. 输出空间。

这里的美妙之处在于,我们可以将自编码器视为在这三个空间中操作的东西。它利用潜在空间来去除输入空间中的任何噪声或不必要的信息,从而生成一个非常紧凑的表示,包含关于输入空间的核心信息。它通过尝试在输出空间中镜像输入空间来实现这一点,从而减少两者之间的差异或重建误差。

卷积自编码器:将复杂性编码为简洁

以下代码展示了一个卷积自编码器的示例,这是一种在处理图像时表现良好的自编码器。我们将使用流行的 MNIST 数据集[LeCun, Y., Cortes, C., & Burges, C.J. (1998). 手写数字的 MNIST 数据库。 来源于 TensorFlow,CC BY 4.0],该数据集包含 28x28 像素的灰度手写数字图像。编码器在将数据的维度从 784 个元素压缩到更小的形式时起着至关重要的作用。然后,解码器试图从这种较低维度的表示中重建原始的高维数据。然而,这种重建并不完美,部分信息丢失。自编码器通过学习优先处理数据中最重要的特征来克服这一挑战。

import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras.datasets import mnist
import matplotlib.pyplot as plt

# Load MNIST dataset
(x_train, _), (x_test, _) = mnist.load_data()
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
x_train = x_train.reshape((len(x_train), 28, 28, 1))
x_test = x_test.reshape((len(x_test), 28, 28, 1))

# Define the convolutional autoencoder architecture
input_img = layers.Input(shape=(28, 28, 1))

# Encoder
x = layers.Conv2D(16, (3, 3), activation='relu', padding='same')(input_img)
x = layers.MaxPooling2D((2, 2), padding='same')(x)
x = layers.Conv2D(8, (3, 3), activation='relu', padding='same')(x)
encoded = layers.MaxPooling2D((2, 2), padding='same')(x)

# Decoder
x = layers.Conv2D(8, (3, 3), activation='relu', padding='same')(encoded)
x = layers.UpSampling2D((2, 2))(x)
x = layers.Conv2D(16, (3, 3), activation='relu', padding='same')(x)
x = layers.UpSampling2D((2, 2))(x)
decoded = layers.Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x)

# Autoencoder model
autoencoder = tf.keras.Model(input_img, decoded)
autoencoder.compile(optimizer='adam', loss='binary_crossentropy')
autoencoder.fit(x_train, x_train, epochs=10, batch_size=64, validation_data=(x_test, x_test))

# Visualization
# Sample images
sample_images = x_test[:8]
# Reconstruct images
reconstructed_images = autoencoder.predict(sample_images)

# Plot original images and reconstructed images
fig, axes = plt.subplots(nrows=2, ncols=8, figsize=(14, 4))
for i in range(8):
    axes[0, i].imshow(sample_images[i].squeeze(), cmap='gray')
    axes[0, i].set_title("Original")
    axes[0, i].axis('off')
    axes[1, i].imshow(reconstructed_images[i].squeeze(), cmap='gray')
    axes[1, i].set_title("Reconstructed")
    axes[1, i].axis('off')
plt.show()

图片由作者提供

上面的输出展示了自编码器的效果。它显示了成对的图像:原始数字图像及其在编码和解码后重建的结果。这个例子证明了编码器能够以更小的形式捕捉数据的本质,而解码器则能够近似原始图像,尽管在压缩过程中有些信息丢失。

现在,让我们进一步可视化学习到的潜在空间(瓶颈)。我们将使用 PCA 和 t-SNE 这两种降维技术,将压缩后的数据点显示在二维平面上。这个步骤非常重要,因为它帮助我们看到自编码器是如何在潜在空间中组织数据的,并展示了相似数字的自然聚类。我们使用 PCA 和 t-SNE 一起进行比较,看看它们的效果如何。

# Encode all the test data
encoded_imgs = encoder.predict(x_test)

# Reduce dimensionality using PCA
pca = PCA(n_components=2)
pca_result = pca.fit_transform(encoded_imgs)

# Reduce dimensionality using t-SNE
tsne = TSNE(n_components=2, perplexity=30, n_iter=300)
tsne_result = tsne.fit_transform(encoded_imgs)

# Visualization using PCA
plt.figure(figsize=(20, 10))
plt.subplot(1, 2, 1)
plt.scatter(pca_result[:, 0], pca_result[:, 1], c=y_test, cmap=plt.cm.get_cmap("jet", 10))
plt.colorbar(ticks=range(10))
plt.title('PCA Visualization of Latent Space')

# Visualization using t-SNE
plt.subplot(1, 2, 2)
plt.scatter(tsne_result[:, 0], tsne_result[:, 1], c=y_test, cmap=plt.cm.get_cmap("jet", 10))
plt.colorbar(ticks=range(10))
plt.title('t-SNE Visualization of Latent Space')

plt.show()

图片由作者提供

比较这两个生成的图表,t-SNE 在将不同类别的数字在潜在空间中分离时,比 PCA 更为出色(它捕捉了非线性)。它创建了明显的聚类,且不同类别之间的重叠最小。自编码器将图像压缩到较低的维度空间,但仍然捕捉到足够的信息以区分不同的数字,正如 t-SNE 图所示。

这里有一个重要的说明,t-SNE 是一种非线性技术,用于可视化高维数据。它保持局部数据结构,使其在视觉上有助于识别集群和模式。然而,它通常不用于机器学习中的特征降维。

但是这个自编码器可能学习了什么呢?

一般来说,可以说像这样的自编码器学习基本的简单边缘和纹理,逐步学习数字的一部分,比如循环和线条以及它们如何排列,最后理解完整的数字(层次特征),在此过程中,它以紧凑的形式捕捉每个数字的独特本质。它能够猜测图像缺失的部分,并且能够识别数字书写中的常见模式。

流形学习:非均匀性的福音

在上一篇文章《维度灾难:一种直观的探索》中,我探讨了“维度灾难”这一概念,指的是在处理高维数据时遇到的问题和挑战,这使得许多机器学习算法的工作变得更加困难。

现在出现了流形学习算法,这些算法受到非均匀性祝福的驱动,即数据点在给定空间或数据集中的不均匀分布或变化。

流形学习的基本假设是,高维数据实际上位于高维空间内某个低维流形上或附近。这个概念基于这样的想法:尽管数据可能存在于高维空间中(因为它的测量或记录方式),但有效描述数据及其结构的内在维度要低得多。

让我们生成著名的瑞士卷数据集,并将其作为高维空间中非均匀性的示例。在其原始形式中,这个数据集看起来像是一个混乱的数据点堆积。然而,在这混乱的表面下,隐藏着秩序——一个低维结构,包含了数据的重要特征。流形学习技术,如 Isomap,利用了这种非均匀性。通过将数据点从高维空间映射到低维空间,Isomap 展示了瑞士卷的内在形状。它保留了原始数据的丰富性,同时揭示了底层结构——一个二维投影,捕捉了高维空间的非均匀性:

import numpy as np
import matplotlib.pyplot as plt
from sklearn import manifold, datasets

# Generate a Swiss Roll dataset
X, color = datasets.make_swiss_roll(n_samples=1500)

# Apply Isomap for dimensionality reduction
iso = manifold.Isomap(n_neighbors=10, n_components=2)
X_iso = iso.fit_transform(X)

# Plot the 3D Swiss Roll
fig = plt.figure(figsize=(15, 8))

# Create a 3D subplot
ax = fig.add_subplot(121, projection='3d')
ax.scatter(X[:, 0], X[:, 1], X[:, 2], c=color, cmap=plt.cm.Spectral)

# Set the viewing angle
elevation_angle = 30  # adjust this for elevation
azimuthal_angle = 45  # adjust this for azimuthal angle
ax.view_init(elev=elevation_angle, azim=azimuthal_angle)

ax.set_title("Original Swiss Roll")

# Plot the 2D projection after Isomap
ax = fig.add_subplot(122)
ax.scatter(X_iso[:, 0], X_iso[:, 1], c=color, cmap=plt.cm.Spectral)
plt.axis('tight')
ax.set_title("2D projection by Isomap")

# Show the plots
plt.show()

图像来自作者

让我们看看上面的输出:

我们有两幅色彩丰富的插图。在左侧,是一个 3D 瑞士卷,五光十色的颜色呈螺旋状交织在一起。它展示了每个颜色过渡到下一个颜色的过程,标记着穿越瑞士卷的路径。

现在,右侧显示的是相同颜色的二维分布。尽管形状已经改变,颜色的顺序和流动仍然讲述着原始数据的相同故事。点与点之间的顺序和连接被保留,就像瑞士卷被小心地展开到平面上一样,让我们能够一次性看到整个模式。

结论

本文从探讨空间的概念开始,空间是数据点根据其特征/属性位置的数学构造。我们考察了支持向量机(SVMs)如何利用将数据映射到高维空间的思路,来应对低维空间中非线性可分性的挑战。

然后我们讨论了自编码器,一种优雅且真正美丽的架构,它在3 个空间之间进行映射,输入空间被压缩到一个更低的潜在表示(瓶颈),接着是解码器发挥作用,目标是从这个较低的表示中重建原始输入,同时最小化重建误差

我们还探讨了流形学习,以及我们从非均匀性中获得的好处,它作为一种方法,通过简化复杂的数据集而不丢失重要细节,从而克服了维度诅咒

如果你看到这里,我想感谢你花时间阅读这篇文章,我希望你觉得它既有趣又有用,若有任何错误或误解,请随时指出,我非常感激你的反馈和建议。

我在数据科学家面试中关注的机器学习基础知识

原文:towardsdatascience.com/machine-learning-basics-i-look-for-in-data-scientist-interviews-a6ff25be38c9?source=collection_archive---------0-----------------------#2024-12-04

让我们共同拓展科学的广度。

Farzad NobarTowards Data Science Farzad Nobar

·发布于Towards Data Science ·阅读时长 39 分钟·2024 年 12 月 4 日

--

图片由Matt Wang提供,来源于Unsplash

我一直在越来越多地审阅简历,进行电话筛选和面试,以聘用我们的下一位科学家。因此,我比以往任何时候都更加思考我们对数据科学家和应用科学家的具体期望。因此,我最近写了一篇关于我在数据科学家面试中关注的数学知识的文章,这篇文章受到了读者的热烈欢迎(见下文链接),因此,我决定通过深入探讨我们期望数据科学家掌握的机器学习知识,继续这个讨论。

我知道市面上有很多数据科学面试教程,但我仍然决定写这篇文章,因为我找不到一个全面的资料来源,可以让候选人准备面试或作为数据科学家日常工作中的机器学习任务参考。如果你正在准备数据科学家的面试,或者只是打算刷新一下相关知识,我相信你会觉得这篇文章很有帮助。

至少,我强烈建议你浏览一遍,看看那些我亲自非常自豪的表格,如果你觉得它们有帮助,可以将这篇文章标记为未来参考阅读的资料。

机器学习实验做得对

原文:towardsdatascience.com/machine-learning-experiments-done-right-6ed04f5e959b?source=collection_archive---------8-----------------------#2024-12-02

设计机器学习实验的详细指南,旨在产生可靠、可重复的结果。

Nura KawaTowards Data Science Nura Kawa

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

--

图片由Vedrana Filipović提供,来源于Unsplash

机器学习(ML)从业者通过实验来比较不同方法在特定应用和一般问题类型中的有效性。实验结果的有效性取决于从业者如何设计、执行和分析他们的实验。不幸的是,许多机器学习论文缺乏有效的结果。最近的研究[5] [6]揭示了已发布实验的可重复性缺失,归因于以下一些做法:

  • 数据污染:工程训练数据集以包含语义上与测试数据集相似的数据,或直接来自测试数据集

  • 挑选数据:有选择地挑选实验设置或结果,以有利于展示某种方法

  • 误报:包括“使用统计学不当分析结果,如没有适当的统计检验却声称结果显著,或使用错误的统计检验[6]

这些做法不一定是故意为之——从业者可能面临产生快速结果的压力,或缺乏足够的资源。然而,持续采用不良实验做法必然会导致高昂的后果。那么,我们应该如何进行能够产生可重复且可靠结果的机器学习实验呢?在本文中,我们提出了设计和执行严谨机器学习实验的指南

实验:因素与响应函数

实验涉及一个包含输入、过程和输出的系统,如下图所示。以花园为简单例子:球茎是输入,发芽是过程,花朵是输出。在机器学习系统中,数据输入到学习函数中,输出预测。

实践者旨在最大化某个响应函数的输出——在我们花园的例子中,这可能是开花的数量,而在机器学习系统中,这通常是模型的准确性。这个响应函数依赖于可控因素和不可控因素。园艺师可以控制土壤质量和日常浇水,但无法控制天气。机器学习实践者可以控制大多数机器学习系统中的参数,如训练过程、参数和预处理步骤,而随机性则来自数据选择。

实验示意图,基于 [2]

实验的目标是找到最大化响应函数的可控因素的最佳配置,同时最小化不可控因素的影响。一个设计良好的实验需要两个关键要素:一种系统化的方式来测试不同组合的可控因素,以及一种方法来考虑来自不可控因素的随机性。

基于这些原则,清晰且有组织的框架对于有效地设计和执行实验至关重要。下面,我们提供了一份清单,指导实践者进行机器学习实验的规划和实施。

机器学习实验清单

规划并执行严格的机器学习实验:

  1. 陈述你的实验目标

  2. 选择响应函数,或你想要测量的内容

  3. 决定哪些因素变化,哪些因素保持不变

  4. 描述实验的一个运行,应定义:

    (a) 实验的单一配置

    (b) 使用的数据集

  5. 选择一个实验设计,应定义:

    (a) 我们如何探索因素空间,并且

    (b) 我们如何重复测量(交叉验证)

  6. 执行实验

  7. 分析数据

  8. 得出结论和建议

1. 陈述实验目标

目标应明确说明为什么要进行该实验。同样重要的是,指定一个有意义的效应大小。例如,如果实验的目标是“确定使用数据增强技术是否能提高模型的准确性”,那么我们必须补充说,“显著改进是大于或等于 5%。”

2. 选择响应函数,或你想要测量的内容

机器学习实验的响应函数通常是相对于学习任务的准确度指标,例如分类准确度、均值平均精度或均方误差。它也可以是可解释性、鲁棒性或复杂性的度量——只要该度量是明确定义的。

3. 决定哪些因素变化,哪些因素保持不变

一个机器学习系统有多个可控因素,如模型设计、数据预处理、训练策略和特征选择。在这一步骤中,我们决定哪些因素保持不变,哪些因素可以在不同实验中变化。例如,如果目标是“确定使用数据增强技术是否能提高我的模型准确性”,我们可以选择变化数据增强策略及其参数,但保持模型在所有实验中相同。

4. 描述实验的单次运行

实验是实验的单次实例,其中一个过程应用于一组因素的单一配置。在我们的示例实验中,目标是“确定使用数据增强技术是否能提高我的模型准确性”,单次实验将是:“使用一种数据增强技术在训练数据集上训练模型,并在保留的测试集上测量其准确性。”

在这一步骤中,我们还要选择实验的数据。当选择数据集时,我们必须考虑我们的实验是针对特定领域应用,还是用于通用目的。特定领域的实验通常需要一个能够代表该领域的单一数据集,而旨在展示通用结果的实验则应在多个数据集上评估方法,这些数据集包含多种数据类型 [1]。

在这两种情况下,我们必须具体定义训练集、验证集和测试集。如果我们将一个数据集进行拆分,我们应记录数据拆分。这是避免意外污染的关键步骤!

5. 选择实验设计

实验设计是我们将执行的一系列实验。实验设计描述:

  • 将要研究的因素和水平(因素的类别或取值)

  • 一种随机化方案(交叉验证)

如果我们正在进行一个实验,测试训练数据集大小对最终模型鲁棒性的影响,我们将测试哪个范围的大小?应该达到什么样的粒度?当多个因素变化时,是否有必要测试所有可能的因素/水平配置组合?如果我们计划进行统计检验,遵循特定的实验设计可能会有所帮助,例如因子设计或随机区组设计(更多信息请参见[3])。

交叉验证对于机器学习实验至关重要,因为它减少了因数据集划分方式不同而导致的结果方差。为了确定需要多少个交叉验证样本,我们回到第一步中的目标陈述。如果计划进行统计分析,则需要确保生成足够的数据来进行特定的统计检验。

这一步的最后部分是考虑资源限制。每次实验需要多少时间和计算资源?我们是否拥有足够的资源来按设计运行这个实验?也许设计必须进行调整以满足资源限制。

6. 执行实验

为了确保实验顺利进行,必须建立一个严格的系统来组织数据、跟踪实验进程以及分析资源分配。有几个开源工具可以用于此目的(请参见awesome-ml-experiment-management)。

7. 分析数据

根据实验的目标和领域,查看交叉验证的平均值(和误差条!)可能足够。然而,验证结果的最佳方法是通过统计假设检验,这能严格证明,基于给定数据获得结果的概率不是偶然的。如果实验的目的是证明因果关系,那么统计检验是必要的。

8. 得出结论

根据前一步的分析,我们现在可以陈述从实验中得出的结论。我们能从结果中得出什么结论吗?还是需要更多的数据?可靠的结论必须由实验结果支持,并且是可重复的。任何不熟悉实验的实践者都应该能够从头到尾运行实验,获得相同的结果,并从这些结果中得出相同的结论。

最后的思考

一个机器学习实验有两个关键因素:一种用于测试不同因素组合的系统设计,以及一种控制随机性的交叉验证方案。在实验的规划和执行过程中遵循这篇文章中的机器学习实验检查清单,可以帮助实践者或实践者团队确保实验结果可靠且可重复。

感谢阅读!如果你觉得这篇文章有用,请考虑在Medium上关注我,或查看我的个人网站

参考文献

[1] Joris Guerin “设计严格的机器学习实验快速指南。” Towards Data Science. 在线阅读。

[2] 机器学习实验的设计与分析 — 机器学习 — 2016 年春季 — Kogan 教授。YouTube 视频

[3] Lawson, John. 使用 R 进行实验设计与分析在线获取

[4] 机器学习中的可疑做法。ArXiv 预印本

[5] 提高机器学习研究的可重复性。《机器学习研究期刊》,2022 年。在线获取

[6] 向量化独立可重复的机器学习研究迈出的一步。ArXiv 预印本

商业中的机器学习:数据科学课程不会教你的 5 件事

原文:towardsdatascience.com/machine-learning-in-business-5-things-a-data-science-course-wont-teach-you-b5668dd6aeb7?source=collection_archive---------2-----------------------#2024-01-07

作者分享了一些应用机器学习的重要方面,这些方面可能在正式的数据科学教育中被忽视。

Guillaume ColleyTowards Data Science Guillaume Colley

·发表于 Towards Data Science ·阅读时间 9 分钟·2024 年 1 月 7 日

--

如果你觉得我为这篇文章起了一个吸引眼球的标题,我同意你的看法——但请听我说完!多年来,我管理过多位初级数据科学家,最近几年我还教授了一个面向硕士和博士生的应用数据科学课程。他们中的大多数人具备出色的技术能力,但当涉及到将机器学习应用于现实世界的商业问题时,我意识到有一些空白。

以下是我希望数据科学家在商业环境中更关注的 5 个要素:

  • 三思而后行,考虑目标

  • 应对不平衡问题

  • 测试必须贴近现实

  • 使用有意义的性能指标

  • 分数的重要性——或者说不重要

我希望这篇文章能对初级和中级数据科学家有所帮助,帮助他们提升职业生涯!

在这篇文章中,我将重点讨论数据科学家被要求部署机器学习模型以预测客户行为的情境。值得注意的是,这些洞察也适用于涉及产品或传感器行为的场景。

欺诈检测中的机器学习:入门指南

原文:towardsdatascience.com/machine-learning-in-fraud-detection-a-primer-8005b8c88cde?source=collection_archive---------4-----------------------#2024-11-12

在不断发展的对抗性环境中,平衡自动化、准确性和客户体验

Samuel FlenderTowards Data Science Samuel Flender

·发布于 Towards Data Science ·阅读时间 8 分钟·2024 年 11 月 12 日

--

(图片由作者使用 ChatGPT 生成)

欺诈检测是现代电子商务的基石,但它也是机器学习中最少被宣传的领域之一。这是有充分理由的:这是一个对抗性领域,欺诈者不断发明新的方法来绕过现有模型,而模型开发者则不断发明新的方法来抓住他们。

欺诈检测系统的目标是阻止欺诈交易,例如那些由伪造账户使用盗用的信用卡进行的交易,同时又不影响真实客户的购物体验。假阳性(真实交易被错误拦截)会导致客户体验不佳并引发流失,而假阴性(欺诈交易被误放行)则会导致货币损失,也就是由于实际信用卡持卡人发起的拒付而产生的“坏账”。

假设一个现代电子商务提供商每天可能处理数千万个订单,而欺诈率处于千分之一的水平,你就能理解为什么这是一个具有挑战性的领域。这是典型的“在干草堆里找针”问题,而这些干草堆巨大无比,…

GCP 上的机器学习:从 Notebook 到数据流水线

原文:towardsdatascience.com/machine-learning-on-gcp-from-dev-to-prod-with-vertex-ai-c9e42c4b366f?source=collection_archive---------4-----------------------#2024-05-11

Notebook 不足以支撑大规模的机器学习

Benjamin EtienneTowards Data Science Benjamin Etienne

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

--

图片来源:Sylvain MaurouxUnsplash

除非另有说明,所有图片均由作者提供

推广人工智能

每当谈到 AI 和机器学习时,总有一种误解(甚至可以说是幻想)不断在公司间蔓延。人们常常错误地判断了将机器学习项目投入生产的复杂性和所需的技能,或者是因为他们不了解这项工作,或者更糟的是,他们以为自己了解,却根本不明白。

当他们发现 AI 时,第一次的反应可能是:“AI 其实挺简单的,我只需要一个 Jupyter Notebook,从这里那里复制粘贴代码——或者问 Copilot——然后就好了。最终也不需要聘请数据科学家了……”然而故事总是以失败告终,带来的是苦涩、失望,以及 AI 是骗局的感觉:难以投入生产环境、数据漂移、漏洞、不期望的行为。

所以,让我们一劳永逸地写下来:AI/机器学习/任何数据相关工作,都是一项真正的工作,而不是爱好。它需要技能、工艺和工具。如果你认为仅凭 Notebook 就能在生产环境中做机器学习,那你错了。

本文旨在通过一个简单的例子,展示从 Notebook 到实际生产环境流水线所需的所有努力、技能和工具。因为机器学习的生产环境,主要是要能够定期自动化运行你的代码,并进行自动化和监控。

对于那些寻找从“笔记本到顶点管道”端到端教程的人来说,你可能会觉得这个有帮助。

一个简单的用例

假设你是一位数据科学家,正在一家电子商务公司工作。你的公司在网上销售服装,营销团队请求你的帮助:他们正在为特定产品准备一项特别优惠,并希望通过定制电子邮件内容来高效地将其推送给客户,以最大化转化率。因此,你的工作很简单:每个客户应被分配一个分数,表示他/她购买该特别优惠产品的概率。

特别优惠将专门针对这些品牌,这意味着营销团队希望知道哪些客户会从以下品牌购买下一个产品:

Allegra K, Calvin Klein, Carhartt, Hanes, Volcom, Nautica, Quiksilver, Diesel, Dockers, Hurley

对于本文,我们将使用 Google 提供的公开数据集 *thelook_ecommerce* 数据集。它包含了虚构的数据,包括交易数据、客户数据、产品数据,这些数据是我们在在线时尚零售商工作时可以使用的。

要跟随此笔记本,你需要访问 Google Cloud Platform,但其逻辑可以移植到其他云服务提供商或第三方平台,如 Neptune、MLFlow 等。

作为一名受人尊敬的数据科学家,你开始创建一个笔记本,帮助我们探索数据。

我们首先导入将在本文中使用的库:

import catboost as cb
import pandas as pd
import sklearn as sk
import numpy as np
import datetime as dt

from dataclasses import dataclass
from sklearn.model_selection import train_test_split
from google.cloud import bigquery

%load_ext watermark
%watermark --packages catboost,pandas,sklearn,numpy,google.cloud.bigquery 
catboost             : 1.0.4
pandas               : 1.4.2
numpy                : 1.22.4
google.cloud.bigquery: 3.2.0

生产前

获取和准备数据

然后,我们将使用 Python 客户端从 BigQuery 加载数据。请确保使用你自己的项目 ID:

query = """
    SELECT 
      transactions.user_id,
      products.brand,
      products.category,
      products.department,
      products.retail_price,
      users.gender,
      users.age,
      users.created_at,
      users.country,
      users.city,
      transactions.created_at
    FROM `bigquery-public-data.thelook_ecommerce.order_items` as transactions
    LEFT JOIN `bigquery-public-data.thelook_ecommerce.users` as users
      ON transactions.user_id = users.id
    LEFT JOIN `bigquery-public-data.thelook_ecommerce.products` as products
      ON transactions.product_id = products.id
    WHERE status <> 'Cancelled'
"""

client = bigquery.Client()
df = client.query(query).to_dataframe() 

查看数据框时,你应该能看到类似的内容:

这些代表了客户所做的交易/购买,包含了客户和产品信息。

鉴于我们的目标是预测客户在下次购买时将选择哪个品牌,我们将按照以下步骤进行:

  1. 按照时间顺序为每个客户分组购买记录

  2. 如果一个客户有 N 次购买,我们将第 N 次购买视为目标,第 N-1 次购买视为特征。

  3. 因此,我们排除了只有一次购买的客户

让我们将其转化为代码:

# Compute recurrent customers
recurrent_customers = df.groupby('user_id')['created_at'].count().to_frame("n_purchases")

# Merge with dataset and filter those with more than 1 purchase
df = df.merge(recurrent_customers, left_on='user_id', right_index=True, how='inner')
df = df.query('n_purchases > 1')

# Fill missing values
df.fillna('NA', inplace=True)

target_brands = [
    'Allegra K', 
    'Calvin Klein', 
    'Carhartt', 
    'Hanes', 
    'Volcom', 
    'Nautica', 
    'Quiksilver', 
    'Diesel',
    'Dockers', 
    'Hurley'
]

aggregation_columns = ['brand', 'department', 'category']

# Group purchases by user chronologically
df_agg = (df.sort_values('created_at')
          .groupby(['user_id', 'gender', 'country', 'city', 'age'], as_index=False)[['brand', 'department', 'category']]
          .agg({k: ";".join for k in ['brand', 'department', 'category']})
         )

# Create the target
df_agg['last_purchase_brand'] = df_agg['brand'].apply(lambda x: x.split(";")[-1])
df_agg['target'] = df_agg['last_purchase_brand'].isin(target_brands)*1

df_agg['age'] = df_agg['age'].astype(float)

# Remove last item of sequence features to avoid target leakage :
for col in aggregation_columns:
    df_agg[col] = df_agg[col].apply(lambda x: ";".join(x.split(";")[:-1]))

请注意我们如何删除了序列特征中的最后一个项:这非常重要,否则我们会遇到所谓的“数据泄漏”问题:目标是特征的一部分,模型在学习时已经获得了答案。

我们现在得到这个新的 df_agg 数据框:

与原始数据框比较,我们看到用户 ID 为 2 的用户确实购买了 IZOD、Parke & Ronen,最后还购买了 Orvis,而 Orvis 不在目标品牌之内。

将数据划分为训练集、验证集和测试集

作为一名经验丰富的数据科学家,你现在应该将数据分割成不同的子集,因为你显然知道,所有三个子集对于进行严格的机器学习都是必不可少的。(交叉验证今天不讨论,大家简单了解一下。)

在分割数据时,一个关键的点是使用 scikit-learn train_test_split() 方法中的不太为人知的 stratify 参数。这样做的原因是类不平衡:如果目标分布(在我们的例子中是 0 和 1 的百分比)在训练集和测试集之间有所不同,我们可能在部署模型时遇到差劲的结果,感到沮丧。机器学习初学者:尽量使训练数据和测试数据之间的数据分布尽可能相似。

# Remove unecessary features

df_agg.drop('last_purchase_category', axis=1, inplace=True)
df_agg.drop('last_purchase_brand', axis=1, inplace=True)
df_agg.drop('user_id', axis=1, inplace=True)

# Split the data into train and eval
df_train, df_val = train_test_split(df_agg, stratify=df_agg['target'], test_size=0.2)
print(f"{len(df_train)} samples in train")

df_train, df_val = train_test_split(df_agg, stratify=df_agg['target'], test_size=0.2)
print(f"{len(df_train)} samples in train") 
# 30950 samples in train

df_val, df_test = train_test_split(df_val, stratify=df_val['target'], test_size=0.5)
print(f"{len(df_val)} samples in val")
print(f"{len(df_test)} samples in test")
# 3869 samples in train
# 3869 samples in test

现在这一部分已经完成,我们将优雅地将数据集分割为特征和目标:

X_train, y_train = df_train.iloc[:, :-1], df_train['target']
X_val, y_val = df_val.iloc[:, :-1], df_val['target']
X_test, y_test = df_test.iloc[:, :-1], df_test['target']

特征中有不同的类型。我们通常将它们分为:

  • 数值特征:它们是连续的,反映了一个可衡量或有序的量。

  • 类别特征:它们通常是离散的,且常常用字符串表示(例如:国家、颜色等)。

  • 文本特征:它们通常是由单词组成的序列。

当然,还可以有更多类型的数据,比如图像、视频、音频等。

模型:介绍 CatBoost

对于我们的分类问题(你早就知道我们在做分类问题了,不是吗?),我们将使用一个简单但非常强大的库:CatBoost。它是由 Yandex 构建和维护的,并提供了一个高层次的 API,方便我们使用提升树。它与 XGBoost 相似,但底层的实现方式并不完全相同。

CatBoost 提供了一个很好的包装器,用来处理不同类型的特征。在我们的例子中,一些特征可以被视为“文本”,因为它们是单词的连接,例如“Calvin Klein;BCBGeneration;Hanes”。处理这种类型的特征有时可能会很麻烦,因为你需要用到文本分割器、分词器、词形还原器等工具。幸运的是,CatBoost 可以为我们处理一切!

# Define features
features = {
    'numerical': ['retail_price', 'age'],
    'static': ['gender', 'country', 'city'],
    'dynamic': ['brand', 'department', 'category']
}

# Build CatBoost "pools", which are datasets
train_pool = cb.Pool(
    X_train,
    y_train,
    cat_features=features.get("static"),
    text_features=features.get("dynamic"),
)

validation_pool = cb.Pool(
    X_val,
    y_val,
    cat_features=features.get("static"),
    text_features=features.get("dynamic"),
)

# Specify text processing options to handle our text features
text_processing_options = {
    "tokenizers": [
        {"tokenizer_id": "SemiColon", "delimiter": ";", "lowercasing": "false"}
    ],
    "dictionaries": [{"dictionary_id": "Word", "gram_order": "1"}],
    "feature_processing": {
        "default": [
            {
                "dictionaries_names": ["Word"],
                "feature_calcers": ["BoW"],
                "tokenizers_names": ["SemiColon"],
            }
        ],
    },
}

我们现在准备好定义并训练我们的模型。由于参数非常多,今天的范围无法逐一讲解,但欢迎大家自己查看 API。

为了简洁起见,我们今天不进行超参数调优,但这显然是数据科学家工作的重要组成部分!

# Train the model
model = cb.CatBoostClassifier(
    iterations=200,
    loss_function="Logloss",
    random_state=42,
    verbose=1,
    auto_class_weights="SqrtBalanced",
    use_best_model=True,
    text_processing=text_processing_options,
    eval_metric='AUC'
)

model.fit(
    train_pool, 
    eval_set=validation_pool, 
    verbose=10
)

就这样,我们的模型训练好了。我们完成了吗?

不对。我们需要检查模型在训练和测试之间的表现是否一致。训练和测试之间的巨大差距意味着我们的模型出现了过拟合(即“死记硬背训练数据,而在预测未知数据时表现差”)。

对于我们的模型评估,我们将使用 ROC-AUC 分数。今天也不会深入探讨这个内容,但根据我个人的经验,这是一个通常非常稳健的评估指标,比准确率要好得多。

关于准确度的简短备注:我通常不推荐将其作为评估指标。想象一下一个不平衡的数据集,其中有 1% 的正样本和 99% 的负样本。一个总是预测 0 的非常简单的模型,它的准确度是多少?99%。所以在这里,准确度并不有帮助。

from sklearn.metrics import roc_auc_score

print(f"ROC-AUC for train set      : {roc_auc_score(y_true=y_train, y_score=model.predict(X_train)):.2f}")
print(f"ROC-AUC for validation set : {roc_auc_score(y_true=y_val, y_score=model.predict(X_val)):.2f}")
print(f"ROC-AUC for test set       : {roc_auc_score(y_true=y_test, y_score=model.predict(X_test)):.2f}")
ROC-AUC for train set      : 0.612
ROC-AUC for validation set : 0.586
ROC-AUC for test set       : 0.622

老实说,0.62 的 AUC 远不算好,对于你这个专家级的数据科学家来说有些让人失望。我们的模型肯定需要一些参数调优,或许我们也应该更加认真地进行特征工程。

但这已经比随机预测要好(呼):

# random predictions

print(f"ROC-AUC for train set      : {roc_auc_score(y_true=y_train, y_score=np.random.rand(len(y_train))):.3f}")
print(f"ROC-AUC for validation set : {roc_auc_score(y_true=y_val, y_score=np.random.rand(len(y_val))):.3f}")
print(f"ROC-AUC for test set       : {roc_auc_score(y_true=y_test, y_score=np.random.rand(len(y_test))):.3f}")
ROC-AUC for train set      : 0.501
ROC-AUC for validation set : 0.499
ROC-AUC for test set       : 0.501

假设我们现在对我们的模型和笔记本感到满意。此时,业余的数据科学家可能就会停下来。那么,我们该如何迈出下一步,成为生产环境下的准备好模型呢?

迈向生产

认识 Docker

Docker 是一套平台即服务(PaaS)产品,使用操作系统级虚拟化技术将软件打包为称为容器的包进行交付。也就是说,可以把 Docker 想象成一种可以在任何地方运行的代码,它能避免“在你的机器上能工作,但在我的机器上不行”的情况。

为什么使用 Docker?因为除了能够分享代码、保留版本并确保代码在任何地方都能轻松部署等酷炫的功能外,它还可以用于构建管道。请耐心听我解释,随着我们的进展你会明白的。

构建容器化应用的第一步是重构并清理我们凌乱的笔记本。我们将为这个非常简单的示例定义两个文件,preprocess.pytrain.py,并将它们放入 src 目录中。我们还会包括一个包含所有内容的 requirements.txt 文件。

# src/preprocess.py

from sklearn.model_selection import train_test_split
from google.cloud import bigquery

def create_dataset_from_bq():
    query = """
        SELECT 
          transactions.user_id,
          products.brand,
          products.category,
          products.department,
          products.retail_price,
          users.gender,
          users.age,
          users.created_at,
          users.country,
          users.city,
          transactions.created_at
        FROM `bigquery-public-data.thelook_ecommerce.order_items` as transactions
        LEFT JOIN `bigquery-public-data.thelook_ecommerce.users` as users
          ON transactions.user_id = users.id
        LEFT JOIN `bigquery-public-data.thelook_ecommerce.products` as products
          ON transactions.product_id = products.id
        WHERE status <> 'Cancelled'
    """
    client = bigquery.Client(project='<replace_with_your_project_id>')
    df = client.query(query).to_dataframe()
    print(f"{len(df)} rows loaded.")

    # Compute recurrent customers
    recurrent_customers = df.groupby('user_id')['created_at'].count().to_frame("n_purchases")

    # Merge with dataset and filter those with more than 1 purchase
    df = df.merge(recurrent_customers, left_on='user_id', right_index=True, how='inner')
    df = df.query('n_purchases > 1')

    # Fill missing value
    df.fillna('NA', inplace=True)

    target_brands = [
        'Allegra K', 
        'Calvin Klein', 
        'Carhartt', 
        'Hanes', 
        'Volcom', 
        'Nautica', 
        'Quiksilver', 
        'Diesel',
        'Dockers', 
        'Hurley'
    ]

    aggregation_columns = ['brand', 'department', 'category']

    # Group purchases by user chronologically
    df_agg = (df.sort_values('created_at')
              .groupby(['user_id', 'gender', 'country', 'city', 'age'], as_index=False)[['brand', 'department', 'category']]
              .agg({k: ";".join for k in ['brand', 'department', 'category']})
             )

    # Create the target
    df_agg['last_purchase_brand'] = df_agg['brand'].apply(lambda x: x.split(";")[-1])
    df_agg['target'] = df_agg['last_purchase_brand'].isin(target_brands)*1

    df_agg['age'] = df_agg['age'].astype(float)

    # Remove last item of sequence features to avoid target leakage :
    for col in aggregation_columns:
        df_agg[col] = df_agg[col].apply(lambda x: ";".join(x.split(";")[:-1]))

    df_agg.drop('last_purchase_category', axis=1, inplace=True)
    df_agg.drop('last_purchase_brand', axis=1, inplace=True)
    df_agg.drop('user_id', axis=1, inplace=True)
    return df_agg

def make_data_splits(df_agg):

    df_train, df_val = train_test_split(df_agg, stratify=df_agg['target'], test_size=0.2)
    print(f"{len(df_train)} samples in train")

    df_val, df_test = train_test_split(df_val, stratify=df_val['target'], test_size=0.5)
    print(f"{len(df_val)} samples in val")
    print(f"{len(df_test)} samples in test")

    return df_train, df_val, df_test
# src/train.py

import catboost as cb
import pandas as pd
import sklearn as sk
import numpy as np
import argparse

from sklearn.metrics import roc_auc_score

def train_and_evaluate(
        train_path: str,
        validation_path: str,
        test_path: str
    ):
    df_train = pd.read_csv(train_path)
    df_val = pd.read_csv(validation_path)
    df_test = pd.read_csv(test_path)

    df_train.fillna('NA', inplace=True)
    df_val.fillna('NA', inplace=True)
    df_test.fillna('NA', inplace=True)

    X_train, y_train = df_train.iloc[:, :-1], df_train['target']
    X_val, y_val = df_val.iloc[:, :-1], df_val['target']
    X_test, y_test = df_test.iloc[:, :-1], df_test['target']

    features = {
        'numerical': ['retail_price', 'age'],
        'static': ['gender', 'country', 'city'],
        'dynamic': ['brand', 'department', 'category']
    }

    train_pool = cb.Pool(
        X_train,
        y_train,
        cat_features=features.get("static"),
        text_features=features.get("dynamic"),
    )

    validation_pool = cb.Pool(
        X_val,
        y_val,
        cat_features=features.get("static"),
        text_features=features.get("dynamic"),
    )

    test_pool = cb.Pool(
        X_test,
        y_test,
        cat_features=features.get("static"),
        text_features=features.get("dynamic"),
    )

    params = CatBoostParams()

    text_processing_options = {
        "tokenizers": [
            {"tokenizer_id": "SemiColon", "delimiter": ";", "lowercasing": "false"}
        ],
        "dictionaries": [{"dictionary_id": "Word", "gram_order": "1"}],
        "feature_processing": {
            "default": [
                {
                    "dictionaries_names": ["Word"],
                    "feature_calcers": ["BoW"],
                    "tokenizers_names": ["SemiColon"],
                }
            ],
        },
    }

    # Train the model
    model = cb.CatBoostClassifier(
        iterations=200,
        loss_function="Logloss",
        random_state=42,
        verbose=1,
        auto_class_weights="SqrtBalanced",
        use_best_model=True,
        text_processing=text_processing_options,
        eval_metric='AUC'
    )

    model.fit(
        train_pool, 
        eval_set=validation_pool, 
        verbose=10
    )

    roc_train = roc_auc_score(y_true=y_train, y_score=model.predict(X_train))
    roc_eval  = roc_auc_score(y_true=y_val, y_score=model.predict(X_val))
    roc_test  = roc_auc_score(y_true=y_test, y_score=model.predict(X_test))
    print(f"ROC-AUC for train set      : {roc_train:.2f}")
    print(f"ROC-AUC for validation set : {roc_eval:.2f}")
    print(f"ROC-AUC for test.      set : {roc_test:.2f}")

    return {"model": model, "scores": {"train": roc_train, "eval": roc_eval, "test": roc_test}}

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("--train-path", type=str)
    parser.add_argument("--validation-path", type=str)
    parser.add_argument("--test-path", type=str)
    parser.add_argument("--output-dir", type=str)
    args, _ = parser.parse_known_args()
    _ = train_and_evaluate(
        args.train_path,
        args.validation_path,
        args.test_path)

现在干净多了。你实际上可以从命令行启动你的脚本了!

$ python train.py --train-path xxx --validation-path yyy etc.

现在我们准备好构建我们的 Docker 镜像了。为此,我们需要在项目根目录下编写一个 Dockerfile:

# Dockerfile

FROM python:3.8-slim
WORKDIR /
COPY requirements.txt /requirements.txt
COPY src /src
RUN pip install --upgrade pip && pip install -r requirements.txt
ENTRYPOINT [ "bash" ]

这将获取我们的需求,复制 src 文件夹及其内容,并在镜像构建时使用 pip 安装需求。

要将此镜像构建并部署到容器注册表,我们可以使用 Google Cloud SDK 和 gcloud 命令:

PROJECT_ID = ...
IMAGE_NAME=f'thelook_training_demo'
IMAGE_TAG='latest'
IMAGE_URI='eu.gcr.io/{}/{}:{}'.format(PROJECT_ID, IMAGE_NAME, IMAGE_TAG)

!gcloud builds submit --tag $IMAGE_URI .

如果一切顺利,你应该会看到像这样的结果:

Vertex Pipelines,迈向生产

Docker 镜像是进行生产级机器学习的第一步。下一步是构建我们所称之为“管道”的东西。管道是一系列由名为 Kubeflow 的框架协调的操作。Kubeflow 可以在 Google Cloud 上的 Vertex AI 运行。

关于在生产中偏好管道而非笔记本的原因,可能存在争议,但根据我的经验,我给出三个理由:

  1. 监控与可复现性:每个管道都会与其产物(数据集、模型、指标)一起存储,这意味着你可以比较不同的运行、重新运行它们并进行审计。每次重新运行一个笔记本时,你都会失去历史记录(或者你得自己管理产物和日志。祝你好运。)

  2. 成本:运行一个笔记本意味着你需要一台能够运行它的机器。— 这台机器是有成本的,对于大型模型或庞大的数据集,你需要具有高规格的虚拟机。

    — 你必须记得在不使用它时将其关闭。

    — 或者,如果你选择不使用虚拟机并且有其他应用程序在运行,你可能会直接崩溃本地机器。

    — Vertex AI 管道是一个无服务器服务,意味着你不需要管理底层基础设施,只需为你使用的部分付费,即执行时间。

  3. 可扩展性:当你在本地笔记本上同时运行几十个实验时,祝你好运。你将回到使用虚拟机,扩展虚拟机,并重新阅读上面的要点。

偏好管道而非笔记本的最后一个原因也是主观的,并且高度可辩论,但在我看来,笔记本根本不是为定时调度的工作负载设计的。它们非常适合用于探索。

至少使用 Docker 镜像来进行定时任务,或者如果你想做得更规范,可以使用管道,但永远不要在生产环境中运行笔记本。

不再多说,接下来我们写出管道的组件:

# IMPORT REQUIRED LIBRARIES
from kfp.v2 import dsl
from kfp.v2.dsl import (Artifact,
                        Dataset,
                        Input,
                        Model,
                        Output,
                        Metrics,
                        Markdown,
                        HTML,
                        component, 
                        OutputPath, 
                        InputPath)
from kfp.v2 import compiler
from google.cloud.aiplatform import pipeline_jobs

%watermark --packages kfp,google.cloud.aiplatform
kfp                    : 2.7.0
google.cloud.aiplatform: 1.50.0

第一个组件将从 Bigquery 下载数据并将其保存为 CSV 文件。

我们使用的 BASE_IMAGE 是我们之前构建的镜像!我们可以用它来导入我们在 Docker 镜像src文件夹中定义的模块和函数:

@component(
    base_image=BASE_IMAGE,
    output_component_file="get_data.yaml"
)
def create_dataset_from_bq(
    output_dir: Output[Dataset],
):

    from src.preprocess import create_dataset_from_bq

    df = create_dataset_from_bq()

    df.to_csv(output_dir.path, index=False)

下一步:拆分数据

@component(
    base_image=BASE_IMAGE,
    output_component_file="train_test_split.yaml",
)
def make_data_splits(
    dataset_full: Input[Dataset],
    dataset_train: Output[Dataset],
    dataset_val: Output[Dataset],
    dataset_test: Output[Dataset]):

    import pandas as pd
    from src.preprocess import make_data_splits

    df_agg = pd.read_csv(dataset_full.path)

    df_agg.fillna('NA', inplace=True)

    df_train, df_val, df_test = make_data_splits(df_agg)
    print(f"{len(df_train)} samples in train")
    print(f"{len(df_val)} samples in train")
    print(f"{len(df_test)} samples in test")

    df_train.to_csv(dataset_train.path, index=False)
    df_val.to_csv(dataset_val.path, index=False)
    df_test.to_csv(dataset_test.path, index=False)

下一步:模型训练。我们将保存模型评分,以便在下一步展示它们:

@component(
    base_image=BASE_IMAGE,
    output_component_file="train_model.yaml",
)
def train_model(
    dataset_train: Input[Dataset],
    dataset_val: Input[Dataset],
    dataset_test: Input[Dataset],
    model: Output[Model]
):

    import json
    from src.train import train_and_evaluate

    outputs = train_and_evaluate(
        dataset_train.path,
        dataset_val.path,
        dataset_test.path
    )
    cb_model = outputs['model']
    scores = outputs['scores']

    model.metadata["framework"] = "catboost" 
    # Save the model as an artifact
    with open(model.path, 'w') as f: 
        json.dump(scores, f)

最后一步是计算指标(这些指标实际上是在模型训练过程中计算的)。这只是一个必要步骤,但它能让你看到构建轻量级组件是多么简单。注意,在这种情况下我们并没有从 BASE_IMAGE 构建组件(它有时可能非常大),而是仅仅构建了一个包含必要组件的轻量级镜像:

@component(
    base_image="python:3.9",
    output_component_file="compute_metrics.yaml",
)
def compute_metrics(
    model: Input[Model],
    train_metric: Output[Metrics],
    val_metric: Output[Metrics],
    test_metric: Output[Metrics]
):

    import json

    file_name = model.path
    with open(file_name, 'r') as file:  
        model_metrics = json.load(file)

    train_metric.log_metric('train_auc', model_metrics['train'])
    val_metric.log_metric('val_auc', model_metrics['eval'])
    test_metric.log_metric('test_auc', model_metrics['test'])

通常还会有其他步骤可以加入,例如如果我们希望将模型部署为 API 端点,但这属于更高级的内容,需要为模型的服务创建另一个 Docker 镜像。下一次会介绍。

现在让我们把组件连接起来:

# USE TIMESTAMP TO DEFINE UNIQUE PIPELINE NAMES
TIMESTAMP = dt.datetime.now().strftime("%Y%m%d%H%M%S")
DISPLAY_NAME = 'pipeline-thelook-demo-{}'.format(TIMESTAMP)
PIPELINE_ROOT = f"{BUCKET_NAME}/pipeline_root/"

# Define the pipeline. Notice how steps reuse outputs from previous steps
@dsl.pipeline(
    pipeline_root=PIPELINE_ROOT,
    # A name for the pipeline. Use to determine the pipeline Context.
    name="pipeline-demo"   
)

def pipeline(
    project: str = PROJECT_ID,
    region: str = REGION, 
    display_name: str = DISPLAY_NAME
):

    load_data_op = create_dataset_from_bq()
    train_test_split_op = make_data_splits(
        dataset_full=load_data_op.outputs["output_dir"]
    )
    train_model_op = train_model(
        dataset_train=train_test_split_op.outputs["dataset_train"], 
        dataset_val=train_test_split_op.outputs["dataset_val"],
        dataset_test=train_test_split_op.outputs["dataset_test"],
        )
    model_evaluation_op = compute_metrics(
        model=train_model_op.outputs["model"]
    )

# Compile the pipeline as JSON
compiler.Compiler().compile(
    pipeline_func=pipeline,
    package_path='thelook_pipeline.json'
)

# Start the pipeline
start_pipeline = pipeline_jobs.PipelineJob(
    display_name="thelook-demo-pipeline",
    template_path="thelook_pipeline.json",
    enable_caching=False,
    location=REGION,
    project=PROJECT_ID
)

# Run the pipeline
start_pipeline.run(service_account=<your_service_account_here>)

如果一切顺利,你现在应该能在 Vertex UI 中看到你的管道:

你可以点击它,查看不同的步骤:

结论

数据科学,尽管所有不写代码/低代码的爱好者告诉你不需要做开发者就能做机器学习,但它依然是一项真正的工作。像所有工作一样,它需要技能、概念和工具,超越了笔记本的范畴。

对于那些渴望成为数据科学家的朋友们,这就是这份工作的现实。

编程愉快。

初学者的机器学习操作(MLOps)

原文:towardsdatascience.com/machine-learning-operations-mlops-for-beginners-a5686bfe02b2?source=collection_archive---------0-----------------------#2024-08-29

端到端项目实施

Prasad MahamulkarTowards Data Science Prasad Mahamulkar

·发表于 Towards Data Science ·阅读时间 19 分钟·2024 年 8 月 29 日

--

图片由作者创建

在生产中开发、部署和维护机器学习模型可能是具有挑战性和复杂的。这就是机器学习操作(MLOps)发挥作用的地方。MLOps 是一套实践,旨在自动化和简化机器学习(ML)工作流和部署。在本文中,我将通过端到端的项目实施,分享一些基本的 MLOps 实践和工具,帮助你更高效地管理机器学习项目,从开发到生产。

阅读完本文后,你将了解到:

  • 如何使用DVC进行数据版本控制。

  • 如何使用MLflow跟踪日志、工件,并注册模型版本。

  • 如何使用FastAPIDockerAWS ECS部署模型。

  • 如何使用Evidently AI监控生产中的模型。

本文中使用的所有代码都可以在 GitHub 上找到。

请注意,GIF 示例可能在 Medium 应用中无法完全加载,但在浏览器中应该可以正常显示。

在我们开始之前,让我们首先快速了解一下什么是 MLOps。

什么是 MLOps?

MLOps 是一套旨在简化和自动化机器学习(ML)系统生命周期的技术和实践。MLOps 的目标是通过为专业人员和研究人员提供清晰的指南和职责,提升将机器学习模型部署到生产环境中的效率和可靠性。它弥合了机器学习开发和生产之间的鸿沟,确保机器学习模型可以在真实环境中高效地开发、部署、管理和维护。这种方法有助于减少系统设计错误,使得在实际环境中能够做出更强大和准确的预测。

图像由作者创建

我们为什么需要 MLOps?

通常,任何机器学习项目从定义业务问题开始。问题定义后,进行数据提取、数据准备、特征工程和模型训练步骤,以开发模型。模型开发完成后,通常会将其存储在某个地方,以便工程和运维团队可以将其部署到生产环境中。

这种方法有什么问题?

它在开发和部署阶段之间创造了一个鸿沟,导致低效和潜在的错误。如果数据科学家和工程师之间缺乏合作,模型可能无法针对生产环境进行优化,从而导致性能下降、缺乏可扩展性和维护困难等问题。

MLOps 通过创建一个统一的工作流程,集成开发和运维,解决了这些问题。它确保模型可靠、可扩展,并且更容易维护。这种方法减少了错误的风险,加快了部署速度,并通过持续监控保持模型的有效性和最新性。

现在我们对 MLOps 有了一个基本的了解,接下来进入实现部分。

项目设置

机器学习项目需要一个标准的项目结构,以确保项目能够轻松维护和修改。一个良好的项目结构使得团队成员可以轻松且有效地协作。

对于这个项目,我们将使用一个非常基础的结构,帮助我们管理机器学习项目的整个生命周期,包括数据摄取、预处理、模型训练、评估、部署和监控。

首先,从GitHub克隆 mlops-project 仓库并跟随操作。

#clone repository from github
git clone https://github.com/prsdm/mlops-project.git

克隆仓库后,项目结构将如下所示:

.
├── .github                         # DVC metadata and configuration
│   └── workflows                   # GitHub Actions workflows for CI/CD
│       └── docs.yml                
├── data                            # Directory for storing data files
│   ├── train.csv                   
│   └── test.csv                                  
├── docs                            # Project documentation.
│   └── index.md                    
├── models                          # Store trained models 
├── mlruns                          # Directory for MLflow run logs and artifacts
├── steps                           # Source code for data processing and model training
│   ├── __init__.py                
│   ├── ingest.py                   
│   ├── clean.py                    
│   ├── train.py                    
│   └── predict.py                  
├── tests                           # Directory to store tests
│   ├── __init__.py                 
│   ├── test_ingest.py              
│   └── test_clean.py              
├── .gitignore                      # To ignore files that can't commit to Git
├── app.py                          # FastAPI app file
├── config.yml                      # Configuration file
├── data.dvc                        # For tracking data files and their versions
├── dataset.py                      # Script to download or generate data
├── dockerfile                      # Dockerfile for containerizing FastAPI
├── LICENSE                         # License for project
├── main.py                         # To automate model training
├── Makefile                        # To store useful commands to make train or make test 
├── mkdocs.yml                      # Configuration file for MkDocs
├── README.md                       # Project description
├── requirements.txt                # Requirements file for reproducing the environment.
├── samples.json                    # Sample data for testing

'''Extra files for monitoring'''
├── data                           
│   └──production.csv               # data for Monitoring
├── monitor.ipynb                   # Model Monitoring notebook 
├── test_data.html                  # monitoring results for test data  
└── production_data.html            # monitoring results for production data

以下是该结构的详细说明:

  • data:存储用于模型训练和评估的数据文件。

  • docs:包含项目文档。

  • models:存储训练好的机器学习模型。

  • mlruns:包含 MLflow 生成的日志和工件。

  • steps:包括用于数据摄取、清理和模型训练的源代码。

  • tests:包括单元测试,以验证代码的功能。

  • app.py:包含用于部署模型的 FastAPI 应用代码。

  • config.yml:用于存储项目参数和路径的配置文件。

  • data.dvc:使用 DVC 跟踪数据文件及其版本。

  • dataset.py:用于下载或生成数据的脚本。

  • dockerfile:用于构建容器化 FastAPI 应用程序的 Docker 镜像。

  • main.py:自动化模型训练过程。

  • Makefile:包含用于自动化任务的命令,例如训练或测试。

  • mkdocs.yml:用于生成项目文档的 MkDocs 配置文件。

  • requirements.txt:包含项目所需的所有包。

  • samples.json:包含用于测试的示例数据。

  • monitor.ipynb:用于监控模型性能的 Jupyter 笔记本。

  • production_data.htmltest_data.html:存储测试数据和生产数据的监控结果。

这个项目结构旨在组织整个机器学习项目,从开发到监控。

现在,让我们创建一个虚拟环境并使用以下命令激活它:

对于 bash:

#create venv
python3 -m venv venv
#activate
source venv/bin/activate

对于 cmd:

#create venv
python -m venv venv
#activate
.\venv\Scripts\activate

接下来,使用requirements.txt文件安装所有必需的包。

#install all the dependancies
pip install -r requirements.txt

示例:

项目设置示例

设置好环境并安装好依赖项后,我们可以继续进行模型训练部分。

模型训练

在模型训练中,第一步是从源头获取数据,这些数据可能存储在本地存储或远程存储中。为此,运行dataset.py文件。

#to get data from source
python3 dataset.py

这个脚本从源头获取数据,将其分为训练集和测试集,然后将它们存储在data/目录中。

示例:

数据提取示例

一旦数据存储在数据目录中,接下来的步骤包括清理、处理和模型训练。steps/文件夹包含这些阶段的每个模块。

#model training part from project structure

├── steps/                     
│   ├── ingest.py              
│   ├── clean.py 
│   ├── train.py            
│   └── predict.py
├── main.py                    
├── models/model.pkl

让我们看看每个文件的作用:

  • ingestion.py负责初步数据摄取,确保数据被正确加载并准备好供下一阶段使用。

  • clean.py专注于数据清理任务,如处理缺失值、去除重复项和进行其他数据质量改进。

  • train.py负责在清理后的数据上训练模型,并将模型保存为model.pkl,存储在models/目录中。

  • predict.py用于使用训练后的模型评估测试数据上的模型性能。

注意: 这些文件可以根据项目需求进行更改或删除。

为了按顺序运行所有这些步骤,请执行main.py文件:

#to train the model
python3 main.py

这是这个项目中main.py文件的样子:

import logging
from steps.ingest import Ingestion
from steps.clean import Cleaner
from steps.train import Trainer
from steps.predict import Predictor

# Set up logging
logging.basicConfig(level=logging.INFO,format='%(asctime)s:%(levelname)s:%(message)s')

def main():
    # Load data
    ingestion = Ingestion()
    train, test = ingestion.load_data()
    logging.info("Data ingestion completed successfully")

    # Clean data
    cleaner = Cleaner()
    train_data = cleaner.clean_data(train)
    test_data = cleaner.clean_data(test)
    logging.info("Data cleaning completed successfully")

    # Prepare and train model
    trainer = Trainer()
    X_train, y_train = trainer.feature_target_separator(train_data)
    trainer.train_model(X_train, y_train)
    trainer.save_model()
    logging.info("Model training completed successfully")

    # Evaluate model
    predictor = Predictor()
    X_test, y_test = predictor.feature_target_separator(test_data)
    accuracy, class_report, roc_auc_score = predictor.evaluate_model(X_test, y_test)
    logging.info("Model evaluation completed successfully")

    # Print evaluation results
    print("\n============= Model Evaluation Results ==============")
    print(f"Model: {trainer.model_name}")
    print(f"Accuracy Score: {accuracy:.4f}, ROC AUC Score: {roc_auc_score:.4f}")
    print(f"\n{class_report}")
    print("=====================================================\n")

if __name__ == "__main__":
    main()

示例:

模型训练示例

现在,让我们看看如何使用像 DVC 和 MLflow 这样的工具来改进这个项目。

数据版本控制(DVC)

让我们从数据版本控制(DVC)开始,它是一个免费的开源工具,旨在管理大型数据集、自动化机器学习管道并处理实验。它帮助数据科学和机器学习团队更有效地管理数据,确保可重复性,并提高协作。

为什么使用 DVC 而不是 GitHub?

Git 在源代码和文本文件的版本控制方面表现出色,但在处理大型二进制文件(如数据集)时存在局限性。Git 无法对二进制文件的版本进行有意义的比较;它仅存储新版本,而不显示详细差异,这使得随着时间推移跟踪更改变得具有挑战性。此外,将大型数据集或敏感数据存储在 GitHub 中并不理想,因为这可能导致代码库膨胀,并带来潜在的安全风险。

DVC 通过元数据和外部存储(如 S3、Google Cloud Storage 或 Azure Blob Storage)管理大文件,同时保持对数据更改和版本历史的详细跟踪,从而解决了这些问题。DVC 使用人类可读的元文件来定义数据版本,并与 Git 或任何源代码管理(SCM)工具集成,以便对整个项目(包括数据资产)进行版本控制和共享。此外,它通过控制项目组件的访问权限并与指定的团队和个人共享,提供了安全的协作方式。

要开始使用 DVC,首先安装它(如果尚未安装):

#install DVC via pip
pip install dvc

然后,初始化 DVC:

#initialize a DVC
dvc init

这将设置必要的 DVC 配置文件。

现在,将数据文件添加到 DVC:

#add data
dvc add data

这通过 DVC 跟踪数据文件,并将实际数据存储在外部存储中。

配置远程存储:

#add remote storage configuration
dvc remote add -d <remote_name> <remote_storage_path>

<remote_name>替换为远程存储的名称,将<remote_storage_path>替换为远程存储的路径(例如:s3://mybucket/mydata)。

将数据推送到远程存储:

#commit the DVC configuration changes to Git
git commit .dvc/config -m 'config dvc store'
#upload data to the configured remote storage
dvc push

这将数据上传到配置的远程存储。

将所有已提交的更改推送到 git:

#push all committed changes to the Git repository
git push origin main

示例:

DVC push 示例

要从远程存储拉取最新的数据版本到本地目录,请使用以下命令:

#pull the latest version of the data
dvc pull

示例:

DVC pull 示例

通过集成 DVC,我们可以高效地管理大型数据集,同时保持 Git 仓库专注于源代码。

注意: 我们可以像数据文件一样使用 DVC 对模型进行版本控制。

MLflow

在使用 DVC 对数据进行版本控制之后,维护模型训练、版本变化和参数配置的清晰记录至关重要,即使我们并不主动进行多个模型的实验。

如果没有系统地进行跟踪,可能会出现几个问题:

  1. 版本详细信息丢失:如果没有跟踪每个模型版本使用的参数和代码更改,就很难重现或在过去的工作基础上进行改进。这可能会减缓进度并导致重复的错误。

  2. 版本比较的难度:持续记录每个模型的表现有助于比较不同版本。没有这个,很难看出一个模型是否在进步。

  3. 协作挑战:在团队中,如果没有明确的方式来管理模型版本,可能会导致混乱和不小心覆盖彼此的工作,从而使协作过程更加复杂。

这就是 MLflow 发挥作用的地方。MLflow 不仅仅用于实验,它在跟踪机器学习模型的生命周期中也扮演着至关重要的角色。它记录指标、工件和参数,确保每次版本更改都有文档记录,并且容易检索。通过 MLflow,我们可以监控每次运行,并比较不同的版本。这样最有效的模型始终可以被识别并准备好进行部署。

若要集成 MLflow,首先安装 MLflow(如果尚未安装):

#install mlfow
pip install mlflow

然后更新main.py文件以包含参数、指标和模型的日志记录。代码大致如下:

import logging
import yaml
import mlflow
import mlflow.sklearn
from steps.ingest import Ingestion
from steps.clean import Cleaner
from steps.train import Trainer
from steps.predict import Predictor
from sklearn.metrics import classification_report

# Set up logging
logging.basicConfig(level=logging.INFO,format='%(asctime)s:%(levelname)s:%(message)s')

def main():

    with open('config.yml', 'r') as file:
        config = yaml.safe_load(file)

    mlflow.set_experiment("Model Training Experiment")

    with mlflow.start_run() as run:
        # Load data
        ingestion = Ingestion()
        train, test = ingestion.load_data()
        logging.info("Data ingestion completed successfully")

        # Clean data
        cleaner = Cleaner()
        train_data = cleaner.clean_data(train)
        test_data = cleaner.clean_data(test)
        logging.info("Data cleaning completed successfully")

        # Prepare and train model
        trainer = Trainer()
        X_train, y_train = trainer.feature_target_separator(train_data)
        trainer.train_model(X_train, y_train)
        trainer.save_model()
        logging.info("Model training completed successfully")

        # Evaluate model
        predictor = Predictor()
        X_test, y_test = predictor.feature_target_separator(test_data)
        accuracy, class_report, roc_auc_score = predictor.evaluate_model(X_test, y_test)
        report = classification_report(y_test, trainer.pipeline.predict(X_test), output_dict=True)
        logging.info("Model evaluation completed successfully")

        # Tags 
        mlflow.set_tag('Model developer', 'prsdm')
        mlflow.set_tag('preprocessing', 'OneHotEncoder, Standard Scaler, and MinMax Scaler')

        # Log metrics
        model_params = config['model']['params']
        mlflow.log_params(model_params)
        mlflow.log_metric("accuracy", accuracy)
        mlflow.log_metric("roc", roc_auc_score)
        mlflow.log_metric('precision', report['weighted avg']['precision'])
        mlflow.log_metric('recall', report['weighted avg']['recall'])
        mlflow.sklearn.log_model(trainer.pipeline, "model")

        # Register the model
        model_name = "insurance_model" 
        model_uri = f"runs:/{run.info.run_id}/model"
        mlflow.register_model(model_uri, model_name)

        logging.info("MLflow tracking completed successfully")

        # Print evaluation results
        print("\n============= Model Evaluation Results ==============")
        print(f"Model: {trainer.model_name}")
        print(f"Accuracy Score: {accuracy:.4f}, ROC AUC Score: {roc_auc_score:.4f}")
        print(f"\n{class_report}")
        print("=====================================================\n")

if __name__ == "__main__":
    main()

接下来,运行main.py脚本,并使用以下命令查看实验详情:

#to launch MLflow UI
mlflow ui

在浏览器中打开提供的 URL http://127.0.0.1:5000,探索并比较记录的参数、指标和模型。

示例:

MLflow 跟踪示例

MLflow 模型比较示例

通过使用 MLflow,我们可以轻松地跟踪模型版本并管理变更,确保可重复性并能够选择最有效的模型进行部署。

在进入部署部分之前,先看看项目中存在的Makefileconfig.yml文件。这些文件有助于简化工作流程,并确保项目设置和配置的一致性。

Makefile

使用make文件对于管理 Python 项目非常有帮助。许多数据科学家和机器学习工程师可能没有意识到这一点,但make可以自动化例行任务,如设置环境、安装依赖项、模型训练、运行测试和清理文件,这样可以节省时间并减少错误。make文件在软件开发中被广泛使用,因为它帮助管理那些难以记住的长命令和复杂命令。

本项目中的make文件大致如下:

bash:

python = venv/bin/python
pip = venv/bin/pip

setup:
 python3 -m venv venv
 $(python) -m pip install --upgrade pip
 $(pip) install -r requirements.txt

run:
 $(python) main.py

mlflow:
 venv/bin/mlflow ui

test:
 $(python) -m pytest

clean:
 rm -rf steps/__pycache__
 rm -rf __pycache__
 rm -rf .pytest_cache
 rm -rf tests/__pycache__

remove:
 rm -rf venv

对于 Windows(cmd),文件需要稍作修改。

python = venv/Scripts/python
pip = venv/Scripts/pip

setup:
 python -m venv venv
 $(python) -m pip install --upgrade pip
 $(pip) install -r requirements.txt

run:
 $(python) main.py

mlflow:
 venv/Scripts/mlflow ui

test:
 $(python) -m pytest

clean:
 @if exist steps\__pycache__ (rmdir /s /q steps\__pycache__)
 @if exist __pycache__ (rmdir /s /q __pycache__)
 @if exist .pytest_cache (rmdir /s /q .pytest_cache)
 @if exist tests\__pycache__ (rmdir /s /q tests\__pycache__)

remove:
 @if exist venv (rmdir /s /q venv)

下面是每个部分的简要说明:

  • make setup:创建虚拟环境(venv),升级pip,并从requirements.txt安装所需的软件包。这样可以确保在不同环境中始终如一地安装所有依赖项。

  • make run:使用虚拟环境中的 Python 解释器执行main.py

  • make mlflow:启动mlflow ui,用于跟踪实验和模型指标。

  • make test: 该命令使用pytest运行项目中定义的所有测试用例。

  • make clean: 删除缓存文件,如 __pycache__.pytest_cache 和其他临时文件,以保持目录的清洁。

  • make remove: 完全删除项目中的虚拟环境(venv)。

运行 make 文件的示例命令:

# For example, to set up the environment
make setup

# OR To run the main script
make run

# OR To run the tests
make test

# so on...

示例:

Make 命令示例

通过使用 make 文件,我们可以自动化并简化各种任务,确保一致性并减少在不同环境中的人为错误。

Config.yml

YAML 文件是存储和管理机器学习模型配置设置的好方法。它们有助于管理数据/模型路径、模型参数和其他配置,使得实验不同配置和保持代码复用变得更加容易。

Config.yml 文件如下所示:

data: 
  train_path: data/train.csv
  test_path: data/test.csv

train:
  test_size: 0.2
  random_state: 42
  shuffle: true

model:
  name: DecisionTreeClassifier
  params:
    criterion: entropy
    max_depth: null
  store_path: models/

  # name: GradientBoostingClassifier
  # params:
  #   max_depth: null
  #   n_estimators: 10
  # store_path: models/

  # name: RandomForestClassifier
  # params:
  #   n_estimators: 50
  #   max_depth: 10
  #   random_state: 42
  # store_path: models/

下面是每个部分的作用:

  • data: 指定了训练集、测试集和生产集(最新数据集)的路径。这样可以确保数据位置在一个地方进行管理,并且可以轻松更新。

  • train: 包含了将数据分割为训练集和测试集的参数,如 test_sizerandom_state 以及是否 shuffle 数据。这些设置有助于保持数据分割的一致性和可复现性。

  • model: 定义了模型名称、其参数以及存储训练模型的位置。此配置使得不同模型之间的切换变得容易,从而提供了模型选择的灵活性。

使用 config.yml 文件简化了模型参数和路径的管理。它允许轻松实验不同的配置和模型,通过保持参数设置一致性来提高可复现性,并通过将配置与代码逻辑分离来帮助保持代码清晰。

示例:

在以下示例中,model 被更改为GradientBoostingClassifier,这是根据 config.yml 文件中指定的配置进行的。

config.yml 文件示例

现在,让我们进入部署部分,在这里我们将使用 FastAPI、Docker 和 AWS ECS。此设置将帮助我们创建一个可扩展且易于管理的应用程序,用于服务机器学习模型。

FastAPI

FastAPI 是一个现代化的框架,用于构建 Python API。由于其速度和简洁性,它在为机器学习模型提供服务时非常高效。

首先,安装 FastAPI 和 Uvicorn(如果尚未安装):

#install fastapi and uvicorn
pip install fastapi uvicorn

app.py 文件中定义 FastAPI 应用程序和用于服务模型的端点。

from fastapi import FastAPI
from pydantic import BaseModel
import pandas as pd
import joblib

app = FastAPI()

class InputData(BaseModel):
    Gender: str
    Age: int
    HasDrivingLicense: int
    RegionID: float
    Switch: int
    PastAccident: str
    AnnualPremium: float

model = joblib.load('models/model.pkl')

@app.get("/")
async def read_root():
    return {"health_check": "OK", "model_version": 1}

@app.post("/predict")
async def predict(input_data: InputData):

        df = pd.DataFrame([input_data.model_dump().values()], 
                          columns=input_data.model_dump().keys())
        pred = model.predict(df)
        return {"predicted_class": int(pred[0])}

然后,使用以下命令在本地测试 FastAPI 服务器:127.0.0.1:8000/docs

#run the FastAPI app
uvicorn app:app --reload

示例:

FastAPI 示例

现在让我们使用 Docker 容器化这个 API。

Docker

Docker 是一个开源平台,通过将软件应用程序打包成容器,简化了其部署过程。这些容器充当轻量级、可移植的单元,包含运行应用程序所需的所有内容,可以在不同环境中运行应用程序。

为什么使用容器?

容器提供了一种简化的方式来隔离和部署应用程序,确保它们在各种环境中始终如一地运行,无论是在开发者的笔记本电脑上,还是在云端。通过这种隔离性,容器增强了可移植性和资源效率,使 Docker 成为现代软件开发的必备工具。

要安装 Docker,请按照 Docker 官方网站上的说明进行操作。

现在,在项目目录中创建一个 Dockerfile 来构建 Docker 镜像:

#official Python 3.10 image
FROM python:3.10

#set the working directory 
WORKDIR /app

#add app.py and models directory
COPY app.py .
COPY models/ ./models/

# add requirements file
COPY requirements.txt .

# install python libraries
RUN pip install --no-cache-dir -r requirements.txt

# specify default commands
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80"]

现在,使用以下命令构建 Docker 镜像:

# To build docker image
docker build -t <image_name> <path_to_dockerfile>

示例:

Docker build 示例

最后,运行 Docker 容器以测试 API,访问 [localhost:80/predict](http://localhost:8000/predict)

# To run docker container
docker run -d -p 80:80 <image_name>

示例:

Docker 运行示例

要停止运行中的 Docker 容器,请使用以下命令查找正在运行的容器的 ID 或名称:

# To show running containers
docker ps

一旦识别出容器的 ID 或名称,就可以使用以下命令停止该容器:

# To stop the container
docker stop <container_id_or_name>

示例:

停止运行中的容器示例

现在,按照以下步骤将 Docker 镜像推送到 Docker Hub:

列出系统中所有 Docker 镜像及其标签,并找到正确的镜像进行推送:

# List images by name and tag.
docker image ls

使用所需的仓库和名称标记镜像:

# Tag the image
docker tag <image_name> <dockerhub_username>/<docker-repo-name>

使用以下命令将标记的镜像上传到 Docker Hub:

# Push the Docker image 
docker push <dockerhub_username>/<docker-repo-name>:latest

此命令将把镜像上传到指定的 Docker Hub 仓库。

示例:

Docker Push 命令示例

Docker Hub 仓库示例

现在,我们已经将 Docker 镜像推送到 Docker Hub,可以继续在 AWS 弹性容器服务(ECS)上进行部署。

AWS ECS

AWS ECS 是一个完全托管的容器编排服务,允许在 AWS 上轻松运行和扩展 Docker 容器。它支持 EC2 和 Fargate 启动类型。以下是一步一步的指南:

首先,创建一个 ECS 集群:

  • 步骤 1: 登录到 AWS 账户,然后进入 ECS 服务,选择“创建集群”来创建一个新的 ECS 集群。

  • 步骤 2: 给集群命名,选择 AWS Fargate(无服务器),然后点击“创建”。(这将需要几分钟时间。)

AWS 集群示例

接下来,定义任务定义:

  • 步骤 1: 在 ECS 控制台中,进入“任务定义”并创建一个新的任务定义。

  • 步骤 2: 给任务命名,并配置内存和 CPU 等要求。

  • 步骤 3: 在容器定义中填写来自 Docker Hub 的 Docker 镜像 URL,并保持容器端口映射为默认值。点击“创建”。

任务定义示例

之后,添加一个安全组:

  • 步骤 1: 前往 EC2,在网络与安全中选择安全组并点击“创建安全组”。为其命名并填写描述。

  • 步骤 2: 在入站规则中,首先选择 HTTP 类型并设置来源为 Anywhere-IPv4,然后对 Anywhere-IPv6 做相同的设置。点击“创建安全组”。

AWS 安全组示例

然后,创建服务:

  • 步骤 1: 前往已创建的 ECS 集群并添加一个新服务。

  • 步骤 2: 选择“启动类型”计算选项并选择“Fargate”启动类型。然后选择已创建的任务定义,并在部署配置中输入服务名称。

  • 步骤 3: 最后,在网络设置下选择之前创建的安全组,然后点击“创建”。(创建服务大约需要 5-8 分钟。)

服务示例

最后,访问正在运行的服务:

服务部署完成后,进入 ECS 集群的“服务”选项卡。找到该服务,进入“任务”选项卡,选择一个正在运行的任务。打开该任务的公共 IP 地址,即可访问 FastAPI 应用。它会显示如下内容:

公共 IP 示例

部署的服务示例

通过执行这些步骤,我们可以将 FastAPI 应用部署到 Docker 容器中,并在 AWS ECS 上运行。这为机器学习模型提供了一个可扩展和可管理的环境。

注意: 如果需要,我们还可以添加弹性负载均衡(ELB)。

模型成功部署后,下一步是持续监控生产环境中的模型,以确保其在生产数据上表现良好。模型监控涉及评估多个因素,如服务器指标(例如 CPU 使用率、内存消耗、延迟)、数据质量、数据漂移、目标漂移、概念漂移、性能指标等。

为了保持对初学者友好,我们将重点介绍一些方法,例如数据漂移、目标漂移和使用 Evidently AI 进行数据质量监控。

Evidently AI

Evidently AI 是一个很好的工具,用于监控模型性能、检测数据漂移和数据质量随时间的变化。它有助于确保模型在新数据输入时依然准确可靠。Evidently AI 提供了模型性能变化的详细洞察,并识别数据分布的任何显著变化,这对于在生产环境中维持模型的准确性至关重要。

要安装 Evidently AI,请使用以下命令:

#to install
pip install evidently

#or
pip install evidently @ git+https://github.com/evidentlyai/evidently.git

接下来,运行 monitor.ipynb 文件以检测数据质量、数据漂移和目标漂移。该文件大致如下:

# If this .py file doesn't work, then use a notebook to run it.
import joblib
import pandas as pd
from steps.clean import Cleaner
from evidently.report import Report
from evidently.metric_preset import DataDriftPreset, DataQualityPreset, TargetDriftPreset
from evidently import ColumnMapping
import warnings
warnings.filterwarnings("ignore")

# # import mlflow model version 1
# import mlflow
# logged_model = 'runs:/47b6b506fd2849429ee13576aef4a852/model'
# model = mlflow.pyfunc.load_model(logged_model)

# # OR import from models/
model = joblib.load('models/model.pkl')

# Loading data
reference = pd.read_csv("data/train.csv")
current = pd.read_csv("data/test.csv")
production = pd.read_csv("data/production.csv")

# Clean data
cleaner = Cleaner()
reference = cleaner.clean_data(reference)
reference['prediction'] = model.predict(reference.iloc[:, :-1])

current = cleaner.clean_data(current)
current['prediction'] = model.predict(current.iloc[:, :-1])

production = cleaner.clean_data(production)
production['prediction'] = model.predict(production.iloc[:, :-1])

# Apply column mapping
target = 'Result'
prediction = 'prediction'
numerical_features = ['Age', 'AnnualPremium', 'HasDrivingLicense', 'RegionID', 'Switch']
categorical_features = ['Gender','PastAccident']
column_mapping = ColumnMapping()

column_mapping.target = target
column_mapping.prediction = prediction
column_mapping.numerical_features = numerical_features
column_mapping.categorical_features = categorical_features

# Data drift detaction part
data_drift_report = Report(metrics=[
    DataDriftPreset(),
    DataQualityPreset(),
    TargetDriftPreset()
])
data_drift_report.run(reference_data=reference, current_data=current, column_mapping=column_mapping)
data_drift_report
# data_drift_report.json()
data_drift_report.save_html("test_drift.html")

测试数据示例:

测试数据质量和漂移检测示例

生产数据示例:

生产数据质量和漂移检测示例

定期运行监控脚本,处理进入的数据,生成关于数据漂移和模型性能的报告。这些报告可以帮助我们识别何时需要重新训练,并确保我们的模型随着时间推移保持准确和可靠。

在这一步中,我们成功完成了 MLOps 项目的实施。

总结

在本文中,我们通过一个实践项目介绍了基本的 MLOps 实践和工具。我们使用 DVC 对数据进行了版本控制,利用 MLflow 跟踪和注册了模型,并通过 FastAPI、Docker 和 AWS ECR 部署了模型。我们还通过 Evidently AI 设置了模型监控(数据质量、数据漂移和目标漂移)。这些步骤为使用 MLOps 工具和实践从开发到生产管理机器学习项目提供了坚实的基础。随着你对这些工具和技术的熟悉,你可以探索更高级的自动化和编排方法,以增强你的 MLOps 工作流。

参考文献

  1. 机器学习操作(MLOps):概述、定义和架构。(arxiv.org/pdf/2205.02302)

  2. 数据版本控制(DVC):dvc.org/doc

  3. MLflow: mlflow.org/docs/latest/index.html

  4. FastAPI: fastapi.tiangolo.com/tutorial/

  5. Docker: docs.docker.com/

  6. Evidently AI: docs.evidentlyai.com/tutorials-and-examples/examples

免费订阅,以便在我发布新文章时收到通知。

[## 每当 Prasad Mahamulkar 发布时,获取电子邮件

每当 Prasad Mahamulkar 发布时,获取电子邮件,了解数据科学、机器学习等内容。通过注册…

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

你也可以在 LinkedInTwitter 上找到我!

使用 Optuna 进行机器学习优化

原文:towardsdatascience.com/machine-learning-optimization-with-optuna-57593d700e52?source=collection_archive---------0-----------------------#2024-06-25

如何微调每个机器学习算法。通过 Optuna 实现机器学习优化的终极指南,以获得出色的模型性能。

Cristian LeoTowards Data Science Cristian Leo

·发表于 Towards Data Science ·阅读时间 16 分钟·2024 年 6 月 25 日

--

图像由 DALL-E 生成

在机器学习中,超参数是你在训练模型之前配置的设置。与模型在训练过程中学习到的参数不同,超参数必须提前设定。找到合适的超参数可以显著提升模型的性能,因此超参数优化是至关重要的。

适当优化的超参数可以显著提高模型的准确性和可靠性。它们帮助模型更好地泛化,避免过拟合(模型过于贴合训练数据)和欠拟合(模型复杂度不足以捕捉潜在模式)。

在这篇文章中,我们将探讨 Optuna,一个用于有效优化任何机器学习或深度学习算法的流行框架。我们将深入了解其背后的数学原理,并提供使用 XGBoost 和 PyTorch 神经网络的实际示例。请坐稳,享受这场旅程!

索引

1: Optuna 概述

2: Optuna 背后的算法

∘ 贝叶斯优化

∘ 树结构 Parzen 估计器(TPE)

∘…

Maixtchup:使用 Mergekit 创建你自己的专家混合模型

原文:towardsdatascience.com/maixtchup-make-your-own-mixture-of-experts-with-mergekit-87cc46401587?source=collection_archive---------11-----------------------#2024-01-29

MoE 的崛起

Benjamin MarieTowards Data Science Benjamin Marie

·发表于 Towards Data Science ·阅读时长:8 分钟·2024 年 1 月 29 日

--

作者提供的图片 — 通过 DALL-E 生成

自从 Mistral AI 发布 Mixtral-8x7B 以来,业界对 专家混合模型(MoE)的兴趣重新高涨。这种架构利用专家子网络,在推理过程中只有部分子网络被路由网络选择并激活。

MoE(专家混合模型)非常简单且灵活,便于制作自定义 MoE。在 Hugging Face Hub 上,我们现在可以找到几种流行的自定义 MoE LLM(大型语言模型),例如 mlabonne/phixtral-4x2_8

然而,大多数模型并不是从头开始构建的传统 MoE,它们只是将已经微调过的 LLM 作为专家进行组合。它们的创建过程通过 mergekitLGPL-3.0 许可证)变得更加简便。例如,Phixtral LLM 就是通过 mergekit 将多个 Phi-2 模型结合而成的。

在本文中,我们将展示 Phixtral 是如何创建的。我们将应用相同的过程,使用多个 Mistral 7B 模型来创建我们自己的专家混合模型,Maixtchup。

什么是 Phixtral?

为了快速理解模型的高层架构,我喜欢将其打印出来。例如,对于 mlabonne/phixtral-4x2_8(MIT 许可证):

from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(…

让指标真正有意义

原文:towardsdatascience.com/make-metrics-matter-2ed7181c06e5?source=collection_archive---------3-----------------------#2024-07-11

数据专业人士如何提高他们最强资产的影响力

Kate MinogueTowards Data Science Kate Minogue

·发布于 Towards Data Science ·阅读时间 10 分钟·2024 年 7 月 11 日

--

人工智能、数据科学和分析:当我们思考组织是否以数据驱动时,这些功能或数据产品往往最引人注目。关注复杂性和成熟度与评估最新技术和方法是相辅相成的。就像其他学科和行业一样,我们可能忽视那些始终存在并被许多人使用的“工作马”,而不是被少数人选择的工具。

当我与公司讨论“数据文化”时,我首先会询问他们的报告和商业智能。这并不是因为我认为分析或建模团队不重要,而是因为文化比职位名称、正式团队或昂贵项目更为深远,深入到每个员工如何思考和使用数据的 DNA 中。我们依赖的报告和监控的指标是我们组织中“数据驱动决策”的主要来源。构成我们业务和运营基础的管道和事实来源才是数据在公司中真正流动的方式。如果我们不了解这些,其他的只是噪音。

与其被最新的算法或最闪亮的工具所吸引,我们的数据团队如果首先专注于让核心指标真正有意义,就有机会真正提升数据在组织中的有效性。

从软件工程师转型为机器学习工程师

原文:towardsdatascience.com/make-the-switch-from-software-engineer-to-ml-engineer-7a4948730c97?source=collection_archive---------0-----------------------#2024-10-08

帮助我从软件工程师转型为机器学习工程师的 7 个步骤

Kartik SinghalTowards Data Science Kartik Singhal

·发表于Towards Data Science ·阅读时间:7 分钟·2024 年 10 月 8 日

--

我收到很多关于如何从软件工程师转型为机器学习工程师(MLE)的问题(非常多)。在成功完成转型后,我可以说我面临的最大挑战就是不知道从哪里开始,并且在没有明确计划的情况下感到迷茫。

在这篇文章中,我将分享帮助你应对这一转变的逐步方法。这 7 个步骤帮助我从软件工程师转型为机器学习工程师。

让我们深入了解一下。

1. 动机

作者提供的图片

了解原因

为什么选择机器学习? 机器学习和人工智能目前非常热门,但你应该了解自己为什么想进入这个领域。这份个人动机将帮助你在 AI 热潮消退时仍能坚持下去。

是什么让我着迷:对我来说,是关于谷歌搜索是如何开发的。谷歌能够如此快速地找到我所需的内容,真的让我想深入了解背后的技术。这份好奇心促使我从 PageRank 开始学习排序算法,进而接触到更广泛的机器学习。

你需要问自己的问题:

  • 机器学习的哪个部分真正吸引了我? 是当前的热门趋势,还是其他什么原因?

  • 我喜欢哪些特定的机器学习应用? 对我来说,是自然语言处理和推荐系统,但也许你更喜欢在金融科技领域使用机器学习。

花时间探索

我花了 4 年时间(1 年硕士,1 年博士(期间我辍学),以及 2 年在行业中的工作)才真正意识到自己想做什么。 这没关系。 建立经验并深入了解一个像机器学习这样庞大的新领域需要时间。

  • 打好基础:基础知识开始,如统计学和机器学习基础。这一扎实的基础将帮助你更好地理解该领域,并找到你最感兴趣的方向。

  • 建立人脉与指导关系: 与已经在该领域的人交流,寻找一些周围的导师,了解他们的日常工作,看是否能激发你对这个工作的兴趣。

  • 了解你的选择: 找出你感兴趣的机器学习岗位,无论是应用机器学习工程师、机器学习研究员,还是从事 MLOps 的工作。在我之前的文章这里中,你可以了解不同岗位的更多信息。

2. 寻找你的细分领域

了解你的动机和兴趣自然会帮助你发现在哪个机器学习领域中,你的技能可以得到最好的应用。

  • 有策略地选择: 机器学习岗位通常会有一些必需的资格要求,比如 5 年的相关行业经验或博士学位。如果你的经验与这些要求不符,可能在那个时候这份工作并不适合你。专注于一步步提升你的技能,并战略性地寻找与自己当前经验更匹配的岗位。

  • 找到最佳切入点: 如果可能,利用你目前所在领域的知识。转型到一个你已经熟悉的领域会更容易。作为软件工程师,你已经熟悉关键的指标、业务目标和领域特有的问题。确定你能最大程度贡献的地方,承担责任,并力争在那个领域发挥领导作用。

我开始在亚马逊定价团队担任软件工程师。尽管定价领域不是我最初的首选,但由于在那里获得了大量的经验,这帮助我更快地转型为机器学习工程师(MLE)。

3. 乐于接受妥协

作者提供的图片

在你的职业生涯中,你有时会面临一些决定,要求你为长期的利益做出短期的牺牲,尤其是在进入一个新领域时。以下是我在转型过程中必须做出的艰难抉择:

  • 曾两次拒绝梦寐以求的谷歌 offer: 我收到了谷歌的 offer,虽然薪资更高,但我拒绝了它们,因为这个岗位涉及安卓开发,没有机器学习的机会。相反,我选择了亚马逊,虽然当时的岗位也没有涉及机器学习,但让我有机会与机器学习团队更紧密地合作。 至今,这是我人生做出的最好的选择!!

  • 延迟了我的晋升近 3 年:我本来有机会在亚马逊更早晋升为高级软件工程师。高级软件工程师转型为高级机器学习工程师要困难得多,因为期望值更高。知道这一点后,我选择推迟晋升,以保持自己的选择余地。

4. 寻找一个支持你的经理/公司

如果你已经确定了一个自己热衷的领域,你仍然需要一个支持你的经理和公司,才能成功地完成转型。

找到合适的环境:

  • 寻找机器学习机会: 寻找公司内部有机会参与机器学习项目的团队。加入一个同时有软件工程团队和机器学习团队紧密合作的团队,幸运的是,大多数团队都是这样的。如果你目前的公司缺乏这些机会,可以考虑寻找外部机会。

提示:寻找那些曾经将软件工程师转型为机器学习工程师的团队。这能大大加速你的转型过程,因为这些团队通常会有清晰的转型指导方针。

  • 寻找支持你的经理: 一个熟悉机器学习角色并支持你学习和职业发展的经理至关重要。他们不仅要口头支持你,还要采取积极措施帮助你实现转型。

提示: 始终草拟一份文档,概述你的转型计划以及你希望参与的项目,并在与经理的 1 对 1 会议中讨论。如果他们一再表现出不感兴趣的态度,他们可能并不打算帮助你转型。

作者提供的图片:示例转型计划

5. 通过成为可靠的软件工程师来赢得信任

在亚马逊的第一个团队中,尽管这个角色不是我理想中的选择,我还是以 200%的努力工作作为软件工程师。我的目标是让自己变得不可或缺,这样我就可以选择自己想做的项目。这种努力建立了我与经理之间的信任关系,我们彼此重视对方的建议。

这为什么重要? 通常,只有顶尖工程师才能选择自己的项目,而其他人则必须完成分配给他们的任务。展现可靠性可以为你争取到那些原本无法获得的机会,让你在职业道路上有更多的控制权。

6. 参与项目

一旦你加入了一个有机器学习机会、支持你的经理以及相关领域的团队,就可以开始应用你的基础知识了。

参与一些小项目

  • 与经验丰富的机器学习工程师合作,参与模型训练的小功能或轻微模型变更。这些任务可能超出你的主要工作职责。

例如,我曾在亚马逊的团队中参与一个改进 AWS SageMaker 训练管道的项目。这让我能够更紧密地与团队中的机器学习工程师合作,理解他们的开发过程,并为即将到来的模型迭代贡献新的功能。

扩展你的视野

  • 当你在问题领域中建立起信心时,开始探索更广泛的领域。广泛研究以了解当前系统的挑战和局限性,并识别潜在的改进空间。

提示:阅读同行公司在同一领域的博客和研究文章,了解这些公司面临的挑战,以获取潜在的改进思路。例如,在我在亚马逊工作时,我曾关注其他电商平台如 eBay 和沃尔玛的技术文章。

  • 这是你展现创造力并识别原创解决方案的机会。保持一份详细的文档,记录你在此过程中的所有学习内容。包括设计文档、技术见解、实际挑战、你已实施的解决方案以及你收到的任何反馈或评估。这不仅是一个有价值的学习工具,帮助你跟踪自己的学习进度,而且在你的过渡评估中,它也充当了有形的证据。

7. 了解绩效评估

诸如晋升等过渡是滞后指标,意味着任何新的角色都要求个人已经在该角色的预期水平上表现。在你过渡到 MLE 角色时,识别将用于评估的标准。通常,软件工程师和 MLE 在绩效反馈会议中会有不同的评估方式。

对于软件工程师,通常强调的是可扩展系统设计、代码质量和项目复杂性。而对于 MLE,通常更强调对业务指标的影响技术专长。这是因为机器学习的开发周期比软件工程要长,且通常与特定的业务指标直接相关。

告别语

从软件工程师转型为 MLE 可能既具挑战性,又充满回报。这需要战略规划、持续学习和适应力的结合。

还有一些额外的建议:

  • 寻找导师:在你进行过渡的团队中寻找一位导师。这个导师将全程支持你,帮助解决任何障碍,并为你识别新的机会。

  • 跟踪你的学习进展:保持一份详细的记录,记录你在过渡过程中的所有学习内容。这份文档不仅能帮助你回顾和完善思路,还能在绩效评估时作为参考。

  • 主动沟通:定期与团队和经理沟通你遇到的挑战以及取得的成功。开放的沟通将有助于根据需要调整策略,并确保团队的持续支持。

这些策略在有效地引导我的职业过渡过程中发挥了重要作用。通过遵循上述步骤,你可以提升你的职业发展,奠定在作为机器学习工程师新角色中的成功基础。

祝你好运,一如既往,祝学习愉快!

如果这篇文章对你有所帮助,并且你想了解更多关于机器学习的实际技巧,可以订阅我的新闻通讯或在LinkedIn上与我联系。

免责声明:本博客基于个人经验和公开可用的资源。请注意,所表达的观点仅代表我个人意见,并不代表我过去或现在雇主的立场。有关最准确的信息,请始终参考招聘公司的官方资源和指南。

从 Pandas 到 PySpark

原文:towardsdatascience.com/make-your-way-from-pandas-to-pyspark-c50d5928f6c3?source=collection_archive---------9-----------------------#2024-09-26

学习一些基本的命令,开始从 Pandas 转向 PySpark

Gustavo R SantosTowards Data Science Gustavo R Santos

·发表于 Towards Data Science ·阅读时长 9 分钟·2024 年 9 月 26 日

--

图片来自 Karsten WürthUnsplash

介绍

我是 LinkedIn 和其他地方一些数据科学社区的成员,我时常看到有些人会提到关于 PySpark 的问题。

面对现实吧:数据科学是一个极其广泛的领域,没人能了解所有的内容。因此,当我加入一个统计学课程或社区时,偶尔会看到有人问 什么是 PySpark如何在 PySpark 中计算一些统计量,以及其他各种各样的问题。

通常,已经使用 Pandas 的人尤其对 Spark 感兴趣。我相信这背后有几个原因:

  1. Pandas 无疑是数据科学家非常熟悉和常用的工具,但它也肯定不是最快的库。随着数据量的增大,速度会按比例减慢。

  2. 对于那些已经掌握 Pandas 的人来说,学习一种新的数据处理方法是自然而然的选择。随着数据变得更加容易获取且数量增加,掌握 Spark 是处理大数据的一个很好的选择。

  3. Databricks 非常有名,而 PySpark 可能是平台上最常用的语言之一,与 SQL 并列。

使因果发现能够在现实世界的商业环境中应用

原文:towardsdatascience.com/making-causal-discovery-work-in-real-world-business-settings-80e80c5f66b8?source=collection_archive---------1-----------------------#2024-03-04

因果 AI,探索因果推理与机器学习的结合

Ryan O'SullivanTowards Data Science Ryan O'Sullivan

·发表于Towards Data Science ·阅读时长 10 分钟·2024 年 3 月 4 日

--

照片由N.提供,来自Unsplash

这篇文章系列讲的是什么?

欢迎来到我的因果 AI 系列文章,在这里我们将探索因果推理如何融入机器学习模型。期待在不同的商业场景中探索多个实际应用。

在第一篇文章中,我们探索了使用因果图来回答因果问题。这一次,我们将深入探讨如何在实际商业环境中实现因果发现

如果你错过了关于因果图的第一篇文章,可以在这里查看:

[## 使用因果图回答因果问题

本文为因果图的潜力提供了一个实际的介绍。

medium.com](https://medium.com/@raz1470/using-causal-graphs-to-answer-causal-questions-5fd1dd82fa90?source=post_page-----80e80c5f66b8--------------------------------)

引言

本文旨在帮助你导航因果发现的世界。

适合任何想了解以下内容的人:

  • 介绍因果发现是什么,包括它所做的假设。

  • 深入探讨条件独立性测试,这是因果发现的基础。

  • PC 算法概述,这是一个流行的因果发现算法。

  • 一个使用 Python 实现的案例研究,展示如何应用 PC 算法。

  • 关于如何在现实世界商业环境中使因果发现发挥作用的指南。

完整的笔记本可以在这里找到:

[## causal_ai/notebooks/making causal discovery work in real world business settings.ipynb 位于 main ·…

本项目介绍了因果 AI 及其如何驱动商业价值。- causal_ai/notebooks/making causal discovery…

github.com](https://github.com/raz1470/causal_ai/blob/main/notebooks/making causal discovery work in real world business settings.ipynb?source=post_page-----80e80c5f66b8--------------------------------)

什么是因果发现?

在我上一篇文章中,我讲解了如何使用因果图来回答因果问题。

通常称为 DAG(有向无环图),因果图包含节点和边 — 边连接因果相关的节点。

确定因果图有两种方法:

  • 专家领域知识

  • 因果发现算法

我们并不总是具备专家领域知识来确定因果图。在这本笔记本中,我们将探讨如何利用观察数据,通过因果发现算法来确定因果图。

算法

因果发现是学术界一个广泛研究的领域,提出了四组方法:

目前的研究尚不清楚哪种方法最好。回答这个问题的一个挑战是缺乏现实的基准数据集来验证真相。

图片由作者提供

在这篇博客中,我们将专注于理解 PC 算法,这是一种基于约束的方法,使用条件独立性测试。

假设

在我们介绍 PC 算法之前,让我们先了解因果发现算法所做的关键假设:

  1. 因果马尔可夫条件: 每个变量在给定其直接原因的情况下,与其非后代条件独立。这告诉我们,如果我们知道一个变量的原因,了解那些未直接受这些原因影响的变量不会提供任何额外的信息。这个基本假设简化了因果关系的建模,使我们能够进行因果推理。

  2. 因果忠实性: 如果数据中保持统计独立性,那么对应变量之间就没有直接的因果关系。检验这个假设具有挑战性,违反这一假设可能表明模型不准确或缺少变量。

  3. 因果充分性: 所包括的变量是否足以对感兴趣的变量做出因果声明?换句话说,我们需要观察到所有可能的混杂因素。检验这一假设涉及敏感性分析,用来评估潜在未观测混杂因素的影响。

  4. 无环性: 图中没有循环。

在实践中,虽然这些假设对因果发现是必要的,但它们通常被视为假设,而不是直接进行检验。

即使做出了这些假设,我们仍然可能得到一个 Markov 等价类。当我们有多个因果图,每个图的可能性相同,就形成了 Markov 等价类。

条件独立性检验

条件独立性检验是因果发现的构建块,并被 PC 算法使用(我们稍后会讲解该算法)。

让我们先理解独立性。两个变量的独立性意味着,知道一个变量的值不会提供关于另一个变量值的任何信息。在这种情况下,可以较为安全地假设它们之间没有直接的因果关系。然而,如果两个变量不独立,那么盲目地假设因果关系就是错误的。

条件独立性检验可以用来判断在给定一个或多个其他变量的情况下,两个变量是否相互独立。如果两个变量是条件独立的,那么我们可以推断它们没有因果关系。

Fisher 精确检验可以用来判断在控制了一个或多个额外变量的影响后,两个变量之间是否存在显著的关联(使用额外的变量将数据分割成子集,然后对每个子集应用检验)。零假设假定这两个感兴趣的变量之间没有关联。然后可以计算 p 值,如果 p 值低于 0.05,则拒绝零假设,表明这两个变量之间存在显著关联。

识别虚假相关性

我们可以用一个虚假相关性的例子来说明如何使用条件独立性检验。

两个变量之间存在虚假相关性,当它们有一个共同的原因时,例如高温导致冰淇淋销量和鲨鱼攻击的增加。

np.random.seed(999)

# Create dataset with spurious correlation
temperature = np.random.normal(loc=0, scale=1, size=1000)
ice_cream_sales = 2.5 * temperature + np.random.normal(loc=0, scale=1, size=1000)
shark_attacks = 0.5 * temperature + np.random.normal(loc=0, scale=1, size=1000)
df_spurious = pd.DataFrame(data=dict(temperature=temperature, ice_cream_sales=ice_cream_sales, shark_attacks=shark_attacks))

# Pairplot
sns.pairplot(df_spurious, corner=True)

图片来源:作者

# Create node lookup variables
node_lookup = {0: 'Temperature',
               1: 'Ice cream sales',
               2: 'Shark attacks'                                                                          
 }

total_nodes = len(node_lookup)

# Create adjacency matrix - this is the base for our graph
graph_actual = np.zeros((total_nodes, total_nodes))

# Create graph using expert domain knowledge
graph_actual[0, 1] = 1.0 # Temperature -> Ice cream sales
graph_actual[0, 2] = 1.0 # Temperature -> Shark attacks

plot_graph(input_graph=graph_actual, node_lookup=node_lookup)

图片来源:作者

以下条件独立性检验可用于确定因果图:

# Run first conditional independence test
test_id_1 = round(gcm.independence_test(ice_cream_sales, shark_attacks, conditioned_on=temperature), 2)

# Run second conditional independence test
test_id_2 = round(gcm.independence_test(ice_cream_sales, temperature, conditioned_on=shark_attacks), 2)

# Run third conditional independence test
test_id_3 = round(gcm.independence_test(shark_attacks, temperature, conditioned_on=ice_cream_sales), 2)

图片来源:作者

尽管我们不知道关系的方向,但我们可以正确推断温度与冰淇淋销量和鲨鱼攻击之间存在因果关系。

PC 算法

PC 算法(以其发明者 Peter 和 Clark 命名)是一种基于约束的因果发现算法,它使用条件独立性检验。

它可以总结为两个主要步骤:

  1. 它从一个完全连接的图开始,然后使用条件独立性检验来去除边缘并识别无向因果图(节点之间有连接但没有方向)。

  2. 然后,它通过各种方向性技巧(部分)地确定边的方向。

我们可以用之前的虚假相关性示例来说明第一步:

  • 从一个完全连接的图开始

  • 测试 ID 1:接受原假设并删除边,冰激凌销量与鲨鱼攻击之间不存在因果关系

  • 测试 ID 2:拒绝原假设并保留边,冰激凌销量与温度之间存在因果关系

  • 测试 ID 3:拒绝原假设并保留边,鲨鱼攻击与冰激凌销量之间存在因果关系

评估结果

因果发现中的一个关键挑战是评估结果。如果我们知道因果图,就不需要应用因果发现算法了!然而,我们可以创建合成数据集来评估因果发现算法的表现。

有几种评估因果发现算法的指标:

图片来源:作者

  • 真正正例:正确识别因果关系

  • 假正例:错误识别因果关系

  • 真正负例:正确识别不存在因果关系

  • 假负例:错误识别不存在因果关系

  • 反向边:正确识别因果关系,但方向错误

我们希望得到高数量的真正正例,但这不应以高数量的假正例为代价(因为在构建 SCM 时,错误的因果关系可能会造成很大损害)。因此,GScore 似乎能够很好地捕捉这一点,同时给出一个介于 0 和 1 之间的可解释比率。

呼叫中心案例研究

我们将重新审视我之前文章中的呼叫中心案例研究。首先,我们确定因果图(作为真实值使用),然后利用我们对数据生成过程的了解来创建一些样本。

真实的因果图和生成的样本将使我们能够评估 PC 算法。

# Create node lookup for channels
node_lookup = {0: 'Demand',
               1: 'Call waiting time',
               2: 'Call abandoned', 
               3: 'Reported problems',                   
               4: 'Discount sent',
               5: 'Churn'                                                                             
 }

total_nodes = len(node_lookup)

# Create adjacency matrix - this is the base for our graph
graph_actual = np.zeros((total_nodes, total_nodes))

# Create graph using expert domain knowledge
graph_actual[0, 1] = 1.0 # Demand -> Call waiting time
graph_actual[0, 2] = 1.0 # Demand -> Call abandoned
graph_actual[0, 3] = 1.0 # Demand -> Reported problems
graph_actual[1, 2] = 1.0 # Call waiting time -> Call abandoned
graph_actual[1, 5] = 1.0 # Call waiting time -> Churn
graph_actual[2, 3] = 1.0 # Call abandoned -> Reported problems
graph_actual[2, 5] = 1.0 # Call abandoned -> Churn
graph_actual[3, 4] = 1.0 # Reported problems -> Discount sent
graph_actual[3, 5] = 1.0 # Reported problems -> Churn
graph_actual[4, 5] = 1.0 # Discount sent -> Churn

plot_graph(input_graph=graph_actual, node_lookup=node_lookup)

图片来源:作者

def data_generator(max_call_waiting, inbound_calls, call_reduction):
    '''
     A data generating function that has the flexibility to reduce the value of node 0 (Call waiting time) - this enables us to calculate ground truth counterfactuals

    Args:
        max_call_waiting (int): Maximum call waiting time in seconds
        inbound_calls (int): Total number of inbound calls (observations in data)
        call_reduction (float): Reduction to apply to call waiting time

    Returns:
        DataFrame: Generated data
    '''

    df = pd.DataFrame(columns=node_lookup.values())

    df[node_lookup[0]] = np.random.randint(low=10, high=max_call_waiting, size=(inbound_calls)) # Demand
    df[node_lookup[1]] = (df[node_lookup[0]] * 0.5) * (call_reduction) + np.random.normal(loc=0, scale=40, size=inbound_calls) # Call waiting time
    df[node_lookup[2]] = (df[node_lookup[1]] * 0.5) + (df[node_lookup[0]] * 0.2) + np.random.normal(loc=0, scale=30, size=inbound_calls) # Call abandoned
    df[node_lookup[3]] = (df[node_lookup[2]] * 0.6) + (df[node_lookup[0]] * 0.3) + np.random.normal(loc=0, scale=20, size=inbound_calls) # Reported problems
    df[node_lookup[4]] = (df[node_lookup[3]] * 0.7) + np.random.normal(loc=0, scale=10, size=inbound_calls) # Discount sent
    df[node_lookup[5]] = (0.10 * df[node_lookup[1]] ) + (0.30 * df[node_lookup[2]]) + (0.15 * df[node_lookup[3]]) + (-0.20 * df[node_lookup[4]]) # Churn

    return df

# Generate data
np.random.seed(999)
df = data_generator(max_call_waiting=600, inbound_calls=10000, call_reduction=1.00)

# Pairplot
sns.pairplot(df, corner=True)

图片来源:作者

应用 PC 算法

Python 包 gCastle 实现了几种因果发现算法,包括 PC 算法:

[## trustworthyAI/gcastle at master · huawei-noah/trustworthyAI

与可信 AI 相关的项目。通过在 GitHub 上创建帐户来为 huawei-noah/trustworthyAI 的开发做出贡献。

github.com

当我们将样本输入算法时,返回的是学习到的因果图(以邻接矩阵的形式)。

# Apply PC method to learn graph
pc = PC(variant='stable')
pc.learn(df)
graph_pred = pc.causal_matrix

graph_pred

图片来源:作者

gCastle 还提供了几种评估指标,包括 GScore。我们学习到的图的 GScore 是 0!为什么它这么差?

# GScore
metrics = MetricsDAG(
    B_est=graph_pred, 
    B_true=graph_actual)
metrics.metrics['gscore']

图片来源:作者

经过仔细检查学习到的图,我们可以看到它正确地识别了无向图,但在定向边时遇到了困难。

plot_graph(input_graph=graph_pred, node_lookup=node_lookup)

图片由作者提供

评估无向图

在应用 PC 算法的学习成果基础上,我们可以使用 gCastle 提取学习到的无向因果图。

# Apply PC method to learn skeleton
skeleton_pred, sep_set = find_skeleton(df.to_numpy(), 0.05, 'fisherz')

skeleton_pred

图片由作者提供

如果我们将我们的真实图转换为无向邻接矩阵,我们就可以用它来计算无向图的 Gscore。

# Transform the ground truth graph into an undirected adjacency matrix
skeleton_actual = graph_actual + graph_actual.T
skeleton_actual = np.where(skeleton_actual > 0, 1, 0)

使用学习到的无向因果图,我们得到了一个 GScore 为 1.00。

# GScore
metrics = MetricsDAG(
    B_est=skeleton_pred, 
    B_true=skeleton_actual)
metrics.metrics['gscore']

图片由作者提供

我们已经准确地学习到了一个无向图——我们能否利用专家领域知识来指导边的方向?这个问题的答案在不同的应用场景中会有所不同,但它是一个合理的策略。

plot_graph(input_graph=skeleton_pred, node_lookup=node_lookup)

图片由作者提供

在现实商业环境中使因果发现发挥作用

我们需要开始将因果发现视为任何因果推断项目中的一个关键 EDA 步骤:

  • 然而,我们也需要对其局限性保持透明。

  • 因果发现是一个需要结合专家领域知识的工具。

在假设上要务实:

  • 我们是否能期望观察到所有的混杂因素?可能不行。然而,通过正确的领域知识和广泛的数据收集,我们有可能观察到所有关键的混杂因素。

选择一个我们可以应用约束来结合专家领域知识的算法 — gCastle 允许我们在 PC 算法中应用约束:

  • 最初的工作是识别无向因果图,然后将这个输出分享给领域专家,并利用他们的帮助来指导图的方向。

使用代理变量时要小心,并考虑对我们强烈认为存在的关系强加约束:

  • 例如,如果将 Google 趋势数据作为产品需求的代理变量,我们可能需要对这种数据对销售的驱动作用施加约束。

未来的工作

  • 如果我们有非线性关系怎么办?PC 算法能处理这种情况吗?

  • 如果我们有未观察到的混杂因素,会发生什么?FCI 算法能有效处理这种情况吗?

  • 基于约束、基于得分、基于函数和基于梯度的方法如何比较?

如果你想继续这段因果 AI 之旅,请关注我 —— 在下一篇文章中,我们将了解双重机器学习如何帮助消除治疗效应的偏差。

使用 LangProp 让 LLM 写出越来越好的自动驾驶代码

原文:towardsdatascience.com/making-llms-write-better-and-better-code-for-self-driving-using-langprop-99c6c3dc9508?source=collection_archive---------4-----------------------#2024-06-25

来自经典机器学习的类比:LLM(大语言模型)= 优化器;代码 = 参数;LangProp = PyTorch Lightning

Shu IshidaTowards Data Science Shu Ishida

·发表于Towards Data Science ·阅读时间:9 分钟·2024 年 6 月 25 日

--

你可能已经使用 ChatGPT 来写电子邮件、总结文档、查找信息,或帮助调试代码。但我们能否更进一步,让 ChatGPT 来开车呢?

这是我在去年 3 月开始在Wayve实习时想要回答的问题。Wayve 是位于伦敦的自动驾驶创业公司,致力于将端到端学习应用于城市驾驶这一挑战性问题。当时,公司正要成立其 LLM 研究团队,并成功开发了LINGO-1LINGO-2AutoGPT刚刚发布,Voyager还没有发布。而 LLM 带来的变革已显而易见。问题是,如何将这项新技术应用于驾驶这一领域——一个语言不是主要模态的领域?

在这篇博客文章中,我想概述一下我们在上个月(2024 年 5 月)在 ICLR(国际学习表示会议)上的 LLM 代理研讨会中展示的论文LangProp

[## GitHub - shuishida/LangProp

通过在 GitHub 上创建账户,参与 shuishida/LangProp 的开发。

github.com [## LangProp:一个使用大型语言模型应用于驾驶的代码优化框架

我们提出了 LangProp,一个框架,用于迭代优化由大型语言模型(LLM)生成的代码,涵盖…

arxiv.org

动机:让我们将机器学习应用到代码编写中,字面意义上。

将 LLM 应用于驾驶的挑战有两方面:首先,正如名字所示,LLM 是非常庞大的模型,需要大量计算资源,运行速度较慢,这使得它们不太适合用于安全关键的实时应用,例如自动驾驶;其次,虽然语言在高层次的描述中很有用,是逻辑、推理和规划的复杂工具,但它缺乏描述观察结果和给出空间控制动作所需的细节和粒度。

然而,我们意识到,我们不一定非得使用 LLM 来推断驾驶动作。我们可以做的是让 LLM 编写驾驶的代码。

如果你曾经使用 ChatGPT 编写过代码,那么这听起来可能是个糟糕的主意。它编写的代码很少能直接使用,并且经常包含一些错误。但如果我们让 LLM 也检测错误并自动修复它们,从而迭代改进代码质量,会怎样呢?

我们将这个想法更进一步——不仅仅是修复错误,我们设计了一个训练框架,让我们能够根据你选择的目标函数改进大语言模型(LLM)生成的代码。你可以“训练”你的代码,通过训练数据集的优化来尝试减少损失。代码的改进可以通过在验证数据集上运行来量化。

这开始听起来像是机器学习了吗?因为它本质上就是!但我们在微调 LLM 吗?不——事实上,并没有神经网络正在进行微调。相反,我们是在微调代码本身!

在 LangProp 中,“代码”是模型的参数,而 LLM 是优化器,引导参数朝着减少损失的方向改进。这为什么酷呢?因为通过应用这种思维,我们现在可以以数据驱动的方式自动优化软件本身!通过深度学习,我们见证了数据驱动方法解决难以描述问题的强大能力。但到目前为止,机器学习的应用领域仅限于通过数值值参数化的模型。现在,它们也能够处理通过代码描述的系统了。

如果您关注过人工智能的历史,这是一种优雅的方式,将曾经流行的符号人工智能方法与更现代、更成功的机器学习方法统一起来。符号人工智能的核心是让人类专家用逻辑和代码描述一个完美的世界模型。这种方法存在局限性,因为许多复杂任务(例如物体识别)超出了人类专家仅凭逻辑描述的能力。而机器学习则让数据本身发声,并以自动化的方式拟合出最能描述它们的模型。这种方法在包括模式识别、压缩和函数逼近等多个领域取得了巨大的成功。然而,逻辑、推理和长期规划是通过简单地在数据上拟合神经网络往往失败的领域。这是因为在神经网络参数空间中学习如此复杂的操作是具有挑战性的。通过 LLMs 和 LangProp,我们最终可以应用数据驱动的学习方法来学习符号系统并自动化其改进。

免责声明

在深入之前,我觉得有必要做一些免责声明。

  1. 本项关于 LangProp 的工作是在 Wayve 作为实习项目进行的,并不直接反映该公司在研究与开发方面的优先级或战略。本博文的目的是描述 LangProp 作为一篇论文,所有内容均以个人身份撰写。

  2. 虽然我们主要展示了 LangProp 在自动驾驶领域的应用,但我们也想强调其局限性,例如:(a)它要求对环境有完美的观察,(b)我们只在模拟环境中实现了它,距离实际部署还很远,(c)生成的驾驶代码既不完美也不复杂,并且存在许多问题,无法适应现实世界的部署。我们视 LangProp 为一个研究原型,展示了 LLMs 应用于数据驱动软件优化的潜力,而非可用于部署的产品。

如果您需要更多关于 LangProp 局限性的信息,请查看我们论文附录中的局限性部分。

话虽如此,让我们来看看 LangProp 是如何工作的!

LangProp 是如何工作的?

…我们重新引入符号人工智能和进化算法

LangProp 训练器概述。LLM 生成代码的变体,然后在训练数据集上进行评估。得分高的代码将被保留。LLM 会提供关于代码失败模式的信息,并重写代码以在训练指标上实现更高的性能。(图像由作者提供)

LangProp 的设计类似于 PyTorch Lightning —— 一个 LangProp 模块跟踪被训练和用于推理的参数(脚本集合)。在训练模式下,一个策略跟踪器记录前向传递过程中的输入、输出以及任何异常情况。代码的性能通过目标函数进行评估。根据得分,策略跟踪器重新排序当前存在的脚本,并将前 k 个脚本传递给 LLM 进行优化。在推理时,做出预测就像调用得分最高的代码一样简单。

LangProp 训练器需要一个要训练的 LangProp 模块,一个训练数据集和一个验证数据集。数据集可以是任何可迭代对象,包括 PyTorch 数据集对象,这使得将 LangProp 应用于现有任务更加容易。训练完成后,我们可以保存一个检查点,即经过优化的代码集合以及一些用于排名代码的统计信息。

我们用来选择最佳代码并改进它们的机制类似于进化算法,其中样本最初是随机选择的,但性能较高的样本会被保留,并经过扰动生成适应性更强的新一代样本。

将 LangProp 应用于驾驶

LangProp 驾驶代理在 CARLA 中的概述(图片来自作者)

现在让我们尝试使用 LangProp 在 CARLA 中进行驾驶!

CARLA 是一个开源的驾驶模拟器,用于自动驾驶研究。这里有一个排行榜挑战,可以对你的自动驾驶车辆代理进行基准测试。我们在此挑战的标准路线和城镇上测试了 LangProp。

将 LangProp 作为机器学习框架进行构思的好处在于,现在我们不仅可以应用经典的监督学习,还可以应用模仿学习和强化学习技术。

具体而言,我们首先在离线数据集上开始训练(包含专家驾驶演示的状态和动作对),然后进行在线回合。在在线回合过程中,我们采用 DAgger [1],这是一种数据集聚合技术,在这种技术中,通过在线回合收集的样本会被标记上专家标签,并与当前数据集进行合并。

模型的输入(代码)是一个包含环境状态的 Python 字典,包括车辆及周围角色的姿势和速度,以及与红绿灯/停车标志的距离。输出是驾驶动作,即车辆应以何种速度和转向角度行驶。

每当发生违规行为时,例如忽视交通信号灯或停车标志、与其他车辆、行人或骑行者发生碰撞,或停留过久,性能得分将受到惩罚。训练目标是最大化模仿学习得分(即代理如何与真实的动作标签匹配)和强化学习得分(减少违规惩罚)之和。

LangProp 驾驶代理正在执行任务

现在观看 LangProp 代理的驾驶过程!

LangProp 代理在 CARLA 中的驾驶,一种驾驶仿真基准测试(视频由作者提供)

我们在训练过程中观察到,ChatGPT 生成的初始驾驶策略非常有缺陷。特别是,它经常学习到一种天真的策略,即复制之前的速度。这是模仿学习领域中一个著名的现象,叫做因果混淆 [2]。如果我们仅仅在离线数据集上进行行为克隆训练,这种天真但简单的策略相比其他更复杂的策略会获得较高的分数。这也是为什么我们需要使用像 DAgger 和强化学习这样的技术,以确保策略能够在在线部署中有效。

在进行了一两次迭代后,模型停止了复制之前的速度,并开始向前行驶,但要么过于谨慎(即每当附近有演员时就停下,即使他们没有与车辆发生碰撞的可能),要么鲁莽(一直向前行驶直到与演员发生碰撞)。经过几次迭代后,模型学会了保持与前方车辆的距离,并根据车辆之间的相对速度动态计算这个距离。它还通过观察速度和位置向量来预测其他演员(例如过马路的行人)是否与车辆处于碰撞轨迹上。

在我们论文中的实验 我们的论文 中,我们展示了 LangProp 驾驶代理超越了许多先前实现的驾驶代理。我们与 PPO 专家代理(Carla-Roach [3], TCP [4])和研究人员实现的专家代理(TransFuser [5], InterFuser [6], TF++ [7])进行了比较,LangProp 超越了除了 TF++ 之外的所有专家代理。所有专家代理的发布都发生在 GPT 3.5 训练截止日期 2021 年 9 月之后,因此这一结果既令人惊讶又令人兴奋!

结束语

感谢你与我一起体验这段旅程!虽然在这项工作中,我们主要探索了 LangProp 在 CARLA 中的自动驾驶应用,但我们也展示了 LangProp 可以轻松应用于更广泛的问题,例如典型的强化学习环境 CartPole-v1。LangProp 在能够通过文本或代码的形式获得性能反馈的环境或问题中表现最佳,这为模型提供了更丰富的语义信号,而不仅仅是数字评分。

LangProp 类似的训练方法在基于数据的迭代改进软件方面有无尽的应用前景,我们非常期待这个领域的未来发展!

如果你喜欢我们的工作,请考虑在此基础上进行构建并引用我们的论文:

@inproceedings{
ishida2024langprop,
title={LangProp: A code optimization framework using Large Language Models applied to driving},
author={Shu Ishida and Gianluca Corrado and George Fedoseev and Hudson Yeo and Lloyd Russell and Jamie Shotton and Joao F. Henriques and Anthony Hu},
booktitle={ICLR 2024 Workshop on Large Language Model (LLM) Agents},
year={2024},
url={https://openreview.net/forum?id=JQJJ9PkdYC}
}

参考文献

[1] Stéphane Ross, Geoffrey Gordon, 和 Drew Bagnell. “将模仿学习和结构化预测简化为无悔在线学习。” 在 第十四届国际人工智能与统计会议论文集JMLR 工作坊与会议论文集,2011 年。

[2] Pim De Haan, Dinesh Jayaraman, 和 Sergey Levine. “模仿学习中的因果混淆。” 神经信息处理系统进展,2019 年。

[3] Zhejun Zhang, Alexander Liniger, Dengxin Dai, Fisher Yu, 和 Luc Van Gool. “通过模仿强化学习教练进行端到端城市驾驶。” 在 IEEE/CVF 国际计算机视觉会议论文集,pp. 15222–15232,2021 年。

[4] Penghao Wu, Xiaosong Jia, Li Chen, Junchi Yan, Hongyang Li, 和 Yu Qiao. “基于轨迹引导的端到端自动驾驶控制预测:一个简单而强大的基准。” 神经信息处理系统进展,35:6119–6132,2022 年。

[5] Kashyap Chitta, Aditya Prakash, Bernhard Jaeger, Zehao Yu, Katrin Renz, 和 Andreas Geiger. “Transfuser: 基于变换器的传感器融合模仿用于自动驾驶。” IEEE 模式分析与机器智能学报,2022 年。

[6] Hao Shao, Letian Wang, Ruobing Chen, Hongsheng Li, 和 Yu Liu. “使用可解释的传感器融合变换器增强安全性的自动驾驶。” 在 机器人学习会议 上,pp. 726–737。PMLR,2023 年。

[7] Jaeger, Bernhard, Kashyap Chitta, 和 Andreas Geiger. “端到端驾驶模型的隐藏偏差。” IEEE/CVF 国际计算机视觉会议论文集,2023 年。

使用大型语言模型使新闻推荐变得可解释

原文:towardsdatascience.com/making-news-recommendations-explainable-with-large-language-models-74f119c7e036?source=collection_archive---------2-----------------------#2024-11-30

通过基于提示的实验,提升内容个性化推荐的准确性和透明推理。

Alex HeldTowards Data Science Alex Held

·发布于 Towards Data Science ·阅读时间:7 分钟·2024 年 11 月 30 日

--

在合适的时间向读者提供相关内容。图片来自作者。

DER SPIEGEL ,我们不断探索改进如何向读者推荐新闻文章的方法。在我们最新的(离线)实验中,我们研究了 大型语言模型(LLMs)是否能够有效地根据读者的阅读历史预测他们可能感兴趣的文章。

我们的方法

我们进行了一项研究,邀请参与者填写调查问卷,评估他们对各种新闻文章的兴趣。这为我们提供了关于读者偏好的真实数据。对于每个参与者,我们有两个关键信息:他们的实际阅读历史(即在填写调查问卷前他们阅读过哪些文章)以及他们在调查中对一组新文章的评分。了解更多关于这种混合方法的离线新闻推荐系统评估方法,请点击这里:

## 混合方法离线评估新闻推荐系统

结合调查反馈与行为点击数据,优化内容个性化推荐。

towardsdatascience.com

然后我们使用了Anthropic API来访问Claude 3.5 Sonnet,一个最先进的语言模型,作为我们的推荐引擎。对于每个读者,我们为模型提供了他们的阅读历史(新闻标题和文章摘要),并要求模型预测他们对调查中这些文章的兴趣程度。以下是我们使用的提示:

You are a news recommendation system. Based on the user's reading history, 
predict how likely they are to read new articles. Score each article from 0 to 1000, 
where 1000 means highest likelihood to read.

Reading history (Previous articles read by the user):
[List of previously read articles with titles and summaries]

Please rate the following articles (provide a score 0-1000 for each):
[List of candidate articles to rate]

You must respond with a JSON object in this format:
{
    "recommendations": [
        {
            "article_id": "article-id-here",
            "score": score
        }
    ]
}

通过这种方法,我们现在可以将调查中的实际评分与 LLM 的预测分数进行比较。这一比较提供了一个理想的数据集,用于评估语言模型预测读者兴趣的能力。

结果与关键发现

研究结果令人印象深刻。为了理解表现,我们可以看两个关键指标。首先是Precision@5:LLM(大语言模型)获得了 56%的得分,这意味着当系统向用户推荐前 5 篇文章时(从 15 篇中选出),平均(几乎)有 3 篇是用户在调查中评分最高的文章。查看这些预测的分布可以揭示出更加令人印象深刻的结果:对于 24%的用户,系统正确识别出至少 4 或 5 篇他们最喜欢的文章;对于另外 41%的用户,系统正确识别出他们前 5 篇文章中的 3 篇。

为了将其置于正确的视角中,如果我们随机推荐文章,我们的精准度将仅为 38.8%(详细信息请参见之前的medium 文章)。即使是基于文章流行度的推荐(推荐大多数人阅读的内容)也只有 42.1%,而我们之前使用基于嵌入的方法的精准度为 45.4%。

作者图表

以下图表展示了提升效果:尽管了解用户的任何信息比纯粹猜测(随机模型)要好,但基于 LLM 的方法展示了最强的表现。即使与我们复杂的基于嵌入的逻辑相比,LLM 在预测准确性上也实现了显著提升。

作者图表

作为第二个评估指标,我们使用了Spearman 相关系数。其值为 0.41,显著高于我们基于嵌入的方法(0.17)。这也表明,LLM 不仅在找到相关文章方面表现更好,还能更好地理解读者可能偏爱某篇文章超过另一篇文章的程度。

超越表现:可解释性的力量

基于 LLM 的推荐与众不同之处不仅在于其表现,还在于它们能够以自然语言解释其决策。以下是我们的系统如何分析用户阅读模式并解释其推荐的一个示例(提示未展示):

User has 221 articles in reading history

Top 5 Comparison:
--------------------------------------------------------------------------------

Top 5 Predicted by Claude:
1\. Wie ich mit 38 Jahren zum ersten Mal lernte, strukturiert zu arbeiten (Score: 850, Actual Value: 253.0)
2\. Warum wir den Umgang mit der Sonne neu lernen müssen (Score: 800, Actual Value: 757.0)
3\. Lohnt sich ein Speicher für Solarstrom vom Balkon? (Score: 780, Actual Value: 586.0)
4\. »Man muss sich fragen, ob dieser spezielle deutsche Weg wirklich intelligent ist« (Score: 750, Actual Value: 797.0)
5\. Wie Bayern versucht, sein Drogenproblem unsichtbar zu machen (Score: 720, Actual Value: 766.0)

Actual Top 5 from Survey:
4\. »Man muss sich fragen, ob dieser spezielle deutsche Weg wirklich intelligent ist« (Value: 797.0, Predicted Score: 750)
5\. Wie Bayern versucht, sein Drogenproblem unsichtbar zu machen (Value: 766.0, Predicted Score: 720)
2\. Warum wir den Umgang mit der Sonne neu lernen müssen (Value: 757.0, Predicted Score: 800)
6\. Abitur als Lotterie? (Value: 601.0, Predicted Score: 650)
3\. Lohnt sich ein Speicher für Solarstrom vom Balkon? (Value: 586.0, Predicted Score: 780)

Performance Metrics:
--------------------------------------------------------------------------------
Spearman Correlation: 0.673
Precision@5: 0.800

Claude's Analysis:
--------------------------------------------------------------------------------
1\. Key patterns in user's reading history:
- Strong interest in social and political issues
- Regular engagement with lifestyle and personal development content
- Interest in health and wellness topics
- Frequent reading of articles about economic and financial matters
- Attraction to critical analysis and commentary pieces
- Interest in food and cooking content

2\. Scoring explanation:

Highest scored articles:
- "Wie ich mit 38 Jahren zum ersten Mal lernte, strukturiert zu arbeiten" (850) - Aligns with the user's interest in personal development and lifestyle content
- "Warum wir den Umgang mit der Sonne neu lernen müssen" (800) - Matches interest in health and wellness topics
- "Lohnt sich ein Speicher für Solarstrom vom Balkon?" (780) - Corresponds to interest in practical advice and economic considerations

Medium scored articles:
- "Man muss sich fragen, ob dieser spezielle deutsche Weg wirklich intelligent ist" (750) - Fits pattern of interest in political commentary
- "Wie Bayern versucht, sein Drogenproblem unsichtbar zu machen" (720) - Matches interest in social issues and critical reporting
- "Abitur als Lotterie?" (650) - Aligns with interest in educational and social topics

Lower scored articles:
- "Eine Brise Formel 1" (550) - Limited sports content in reading history
- "Reizender Absatz" (450) - Less alignment with demonstrated interests
- "Hier wird jetzt auf ganz, ganz hohem Niveau gemeckert" (400) - Style and topic less aligned with user preferences

The scoring prioritizes articles that match the user's demonstrated interests in social issues, practical advice, and critical analysis while giving lower scores to sports and lighter content that appears less frequently in their reading history.

系统并非作为一个黑盒运行,而是可以阐明为何认为某篇文章可能对读者感兴趣:因为你经常阅读关于实用建议和经济问题的文章,你可能会发现这篇关于阳台太阳能储能性价比分析的文章特别相关。 这种透明的推理可以让推荐看起来更加个性化和值得信赖。

结论

尽管我们的结果很有前景,但仍然需要解决若干挑战。由于长篇提示(每个用户包含数百个文章摘要),最大的挑战是成本。每个用户的单次推荐运行费用约为$0.21,若将其扩展到全部读者群体,将变得极为昂贵。测试高效的开源模型,可能会减少这些成本。此外,当前的实现相对较慢,每个用户需要几秒钟的时间。对于一个内容更新频繁、读者兴趣可能在一天内发生变化的新闻平台,我们需要每天多次运行这些推荐,才能保持其相关性。

此外,我们使用了一个简单直接的提示,没有进行任何提示工程或优化。通过系统性的提示优化,可能会有(显著的)提升空间。[1] 此外,我们目前的实现仅使用了文章标题和摘要,未利用现有的元数据。我们可以通过引入其他信号,如每篇文章的阅读时间(用户每篇文章的阅读时长)或整体文章的受欢迎程度,潜在地提高性能。不过,由于高昂的 API 费用,目前进行迭代评估管道并不是一个可行的选项。

总的来说,强大的预测性能和自然语言解释相结合,表明大型语言模型(LLMs)将在新闻推荐系统中发挥重要作用。而且,除了推荐,它们还为我们分析数字新闻中的用户旅程提供了全新的方式。它们能够处理并解释阅读历史与元数据的结合,打开了令人兴奋的可能性:从理解内容的流向和主题的进展,到创建个性化的回顾总结。

感谢阅读 🙏

希望你喜欢它,如果喜欢,就给它点个赞吧。如果有进一步的讨论或问题,请随时通过LinkedIn 与我联系

作为DER SPIEGEL的 数据科学家,我已获得对专有用户数据和点击历史的授权访问权限,这些数据构成了本研究的基础。此数据不公开。所有展示的结果均已汇总和匿名化,以保护用户隐私,同时展示我们在新闻推荐中的方法论。

参考文献

[1] Dairui, Liu & Yang, Boming & Du, Honghui & Greene, Derek & Hurley, Neil & Lawlor, Aonghus & Dong, Ruihai & Li, Irene. (2024). RecPrompt: 一种自调节提示框架,利用大型语言模型进行新闻推荐。

外出邮件很无聊:用生成式 AI 让它们更生动

原文:towardsdatascience.com/making-out-of-office-emails-pop-with-generative-ai-876d4fb81342?source=collection_archive---------10-----------------------#2024-01-09

我实在是写不下去另一封外出邮件了,于是我开发了一个由 AI 驱动的应用,在我度过一个月长假期间,依然能够保持我的数字存在感。

Omar Ali SheikhTowards Data Science Omar Ali Sheikh

·发布于 Towards Data Science ·18 分钟阅读·2024 年 1 月 9 日

--

TLDR; 本文概述了一个用生成式 AI 编写非传统外出邮件的 Web 应用的开发过程。如果亲爱的读者选择继续阅读,您将会收获一大堆文字,详细描述我如何设计并构建了一个利用 GPT-4 和 DALL-E 3 生成自动回复邮件和配图的 Python 应用。另一方面,如果您只想亲自试试这个应用,我为您准备好了;源代码可以在 GitHub* 上找到。*

我为构建带有生成式 AI 的外出邮件所开发的应用截图。此用户界面允许用户管理自动回复的创建,并以编程方式设置 Outlook 的外出设置。尽管显示的文本是丹麦语,但生成模型可以通过稍微修改提示语来生成其他语言的文本。文章的最后可以找到一个短小的用户界面演示视频。图片来自作者。

拥抱生成式 AI 进行自动回复

外出邮件太单调了。

好吧,我说出来了。

但真的必须这么做吗?为什么不使用大语言模型来为这些邮件增添一些特色呢?为了更具创意,为什么不使用文本到图像模型来生成一些炫酷的图像,配合这些文字呢?

由于生成模型仅需几次 API 调用,制作出吸引眼球的自动回复邮件变得前所未有的简单。在本文中,我将展示如何做到这一点。我们将使用 Python 和 OpenAI 的 API 进行测试。我们将创建既俏皮又专业的外出自动回复邮件,并将其集成到 Outlook 中。对于那些喜欢直接跳入代码的朋友,所有代码都可以在 GitHub 上找到。

[## GitHub - sheikhomar/roll:一个基于生成式 AI 的应用,用于为自动回复消息增添趣味

ROLL 负责管理你的 Outlook 收件箱,忠实地告知他人你不在办公室,无论是度假、参加一天的研讨会,还是在丰盛午餐后小睡片刻。

github.com](https://github.com/sheikhomar/roll?source=post_page-----876d4fb81342--------------------------------)

这里的最终目标是什么?

任何一个自尊心强的软件工程师都知道,在知道我们想要实现什么之前,我们不能开始进行技术上的魔法操作。

目标 1: 我们希望让大型语言模型(LLM)生成能够逗乐的丹麦语文本。不仅如此,将这些消息与相关的图片搭配使用也会很有趣,因为谁不喜欢在收件箱里看到卡通图片呢?

目标 2: 质量至关重要,特别是因为这些自动回复消息是从我的工作邮箱发送的。目前的 AI 模型输出远非完美。即使是强大的 LLM,如 GPT-4,有时也会在丹麦语的语法规则上出错。我们需要确保生成的内容既正确又得体。

目标 3: 任何与 Outlook 打过交道的人都知道,手动更改外出设置需要点击无数次。自动化不仅仅是一个流行词汇,而是像周一早晨的一杯浓缩咖啡一样必要。

目标 4: 即使生成模型编写出了完美的自动回复邮件,如果每次发送的都是相同的文本,这种魔力很快就会消失。因此,我们希望邮件内容能够频繁变化。无论是每次发送自动回复时,还是按计划变动,比如每 24 小时更换一次。

在本文中,我们将重点讨论目标 1 到 3。我们会将项目中的调度部分留到未来的文章中,因为它可能会变得相当复杂,值得单独撰写。

如何堆叠代码块而不让塔倒塌?

需求收集:完成。该开始编码了,对吧?

错!

我们不能像喝了咖啡的程序猿一样胡乱开始编码。我们需要考虑如何构建我们的代码。毕竟,我们是工程师。

在《C# 4.0 完全参考》一书的空白页上快速画了一张图——顺便提一句,这本书对于调整电脑显示器高度非常有用——图中显示了一个有三层的结构:

一张图展示了我们如何将代码组织成独立单元的策略,每个单元都有自己的责任。 图片由我亲自制作。

在底层是处理外部系统的组件:

  • 生成 AI API是生成模型的抽象单元。在这个项目中,我们将依赖 OpenAI 的模型。具体来说,使用 GPT-4 来起草文本,使用 DALL-E 3 来生成我们的自动回复中的视觉内容。

  • 数据仓库是我们的数字图书馆,用来存储我们的创作。我们将保持简单,所有内容都作为文件存储在磁盘上。

  • Outlook 客户端是我们与 Microsoft Outlook 的接口。它允许我们以编程方式设置外出设置,从而自动化原本需要反复点击的过程,简直可以媲美英雄联盟锦标赛中的点击盛宴。

在中间层,我们有一个服务层,其中包含执行所有繁重任务的组件:

  • 内容生成器生成文本,同时也生成伴随文本的图像。它依赖生成 AI 组件来提供输出。

  • 文件下载器是必要的,因为 DALL-E 的创作只能保存 24 小时。这个组件将在它们消失之前将这些稍纵即逝的杰作从互联网上下载下来。

  • 图像优化器会去除 DALL-E 生成的图像中的多余字节。这可以通过调整图像大小,或许还可以应用量化算法来实现。目标是使包含图像的电子邮件在传输时更为高效。

  • HTML 创建器负责将给定的文本信息和优化后的图像格式化为 HTML 文本,准备作为自动回复电子邮件发送。

在最顶层是用户界面,我们的指挥控制中心,在这里我们可以监督一切。我们确保当 LLM(大语言模型)决定生成包含虚构词汇的文本时,这些词汇几乎看起来像是丹麦语,但又不完全是,我们可以及时介入并拯救局面。

用户界面还允许我们根据文本生成新的图像。最重要的是,在这里我们可以通过点击按钮配置 Outlook 的外出设置——为其他愉快的认知活动腾出宝贵的秒数,比如优化精确的k-最近邻搜索算法。

现在我们已经设计好了软件架构,接下来是编码阶段。

让我们开始吧!

如何一步步建立塔楼?

本节大致介绍了如何实现上一节中描述的每个组件。目标不是详细解释每一行代码,而是提供足够的上下文来澄清代码背后的意图。代码中的注释和命名应该涵盖剩下的部分。如果还有不清楚的地方,欢迎向我提出意见。

生成 AI API

为了实现生成式 AI 组件,我们可以走一条众所周知的道路,利用像LangChainOpenAI 官方 Python SDK这样的库。相反,让我们偏离传统路径,走一条更有结构的路线,使用AIConfig

有趣的是,AIConfig 强调通过配置文件管理系统中的生成式 AI 部分。这应该会引起资深软件工程师的共鸣。通过将 AI 的行为与应用代码解耦,我们获得了一个更易维护的代码库,这是高质量软件工程的基石。而且,基于配置的方法结构化了我们的实验,使我们能够更快地调整提示,而无需修改代码。

如果这引起了你的兴趣,看看 Sarmad 的深刻文章:

[## 开源 AIConfig

基于配置驱动、适合源代码管理的 AI 应用开发框架

blog.lastmileai.dev](https://blog.lastmileai.dev/open-sourcing-aiconfig-6fbab53a59f7?source=post_page-----876d4fb81342--------------------------------)

使用 AIConfig,与 AI 交互的代码变得异常简单。我们只需要从配置文件实例化一个AIConfigRuntime对象,然后使用命名设置调用相应的模型:

import anyio
from aiconfig import AIConfigRuntime, InferenceOptions
from pathlib import Path

async def main():
  # Create an AIConfigRuntime object from a config file
  config_path = Path("config/auto-reply-content-gen.aiconfig.json")
  runtime: AIConfigRuntime = AIConfigRuntime.load(config_path)

  # Run inference using the prompt named `generate-text`
  inference_options = InferenceOptions(stream=False)
  msg = await runtime.run("generate-text", options=inference_options)

  # Done!
  print(f"Generated message: {msg}")

if __name__ == "__main__":
    anyio.run(main)

在我们的项目中依赖 AIConfig,生成式 AI 组件只需几行代码。正因如此,我们不会为这个组件编写自定义封装代码,像我们在使用 LangChain 时那样。少了麻烦,也不需要因为 LangChain 复杂的设计和不稳定的抽象而对其进行吐槽。使用 AIConfig 的另一个愉快优势是,我们不必自己实现配置逻辑,例如使用Hydra

数据仓库

数据仓库确保我们的内容可以作为文件可靠地存储和检索。它使用一个名为AutoReplyRecord的数据类来组织信息,并使用 JSON 作为序列化格式。我们的实现DataRepository暴露了 CRU 操作,即标准的 CRUD 操作,但不允许删除:

import shutil
import uuid
from datetime import datetime
from pathlib import Path
from typing import List, Optional

import aiofiles
from pydantic import BaseModel, Field

from roll.utils import utcnow

class DataRepository:
    """Represents a file-based data repository."""

    def __init__(self, data_dir: Path) -> None:
        """Initializes a new instance of the DataRepository class.

        Args:
            data_dir (Path): The directory to store data in.
        """
        self._data_dir = data_dir
        if not self._data_dir.exists():
            self._data_dir.mkdir(parents=True, exist_ok=True)

    async def get_keys(self) -> List[str]:
        """Returns the keys of existing records."""
        file_names = [
            file_path.name
            for file_path in self._data_dir.iterdir()
            if file_path.is_dir()
        ]
        return file_names

    async def get_all(self) -> List[AutoReplyRecord]:
        """Returns existing records.

        Returns:
            List[AutoReplyRecord]: A list of all records order by created time.
        """
        keys = await self.get_keys()
        records = [await self.get(key=key) for key in keys]
        sorted_records = sorted(records, key=lambda r: r.created_at, reverse=True)
        return list(sorted_records)

    async def create(
        self, ai_config_path: Path, html_template_path: Path
    ) -> AutoReplyRecord:
        """Create a new record.

        Args:
            ai_config_path (Path): The path to the AI Config file.
            html_template_path (Path): The path to the HTML template file.

        Returns:
            AutoReplyRecord: The newly created record.
        """
        key = uuid.uuid4().hex

        dir_path = self._data_dir / key
        dir_path.mkdir(parents=True, exist_ok=True)

        new_ai_config_path = dir_path / ai_config_path.name
        shutil.copyfile(src=ai_config_path, dst=new_ai_config_path)

        new_html_template_path = dir_path / html_template_path.name
        shutil.copyfile(src=html_template_path, dst=new_html_template_path)

        record = AutoReplyRecord(
            key=key,
            dir=dir_path,
            ai_config_path=new_ai_config_path,
            html_template_path=new_html_template_path,
        )

        await self.save(record=record)
        return record

    async def save(self, record: AutoReplyRecord) -> None:
        """Save the given record to disk.

        Args:
            record (AutoReplyRecord): The record to save.
        """
        file_path = record.dir / RECORD_FILE_NAME
        async with aiofiles.open(file_path, mode="w") as f:
            await f.write(record.to_json(indent=2))

    async def get(self, key: str) -> Optional[AutoReplyRecord]:
        """Finds a record by its key.

        Args:
            key (str): The key to search for.

        Returns:
            Optional[AutoReplyRecord]: The record if found, None otherwise.
        """
        file_path = self._data_dir / key / RECORD_FILE_NAME
        if not file_path.exists():
            return None
        async with aiofiles.open(file_path, mode="r") as f:
            json_data = await f.read()
        return AutoReplyRecord.from_json(json_data=json_data)

Outlook 客户端

当你拥有像exchangelib这样的工具时,自动化 Outlook 变得轻而易举。它是一个 Python 库,可以像高手一样与 Microsoft Exchange API 进行交互。这是一个优秀的软件,我们将在本项目中使用它。

对于这个特定的应用,我们只想玩弄 Outlook 的外出设置。因此,我们将编写一个封装类,提供两个功能:备份当前的外出设置和应用新的设置。

import json
from base64 import b64encode
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import cast

import aiofiles
from exchangelib import Account, Credentials, OofSettings
from exchangelib.ewsdatetime import EWSDateTime

class OutlookAutoReplyClient:
    """Represents a client for interacting with Outlook's out-of-office settings."""

    def __init__(self, login_name: str, password: str, account_name: str) -> None:
        """Initializes a new instance of the OutlookAutoReplyClient class.

        Args:
            login_name (str): The login name of the Outlook account.
            password (str): The password of the Outlook account.
            account_name (str): The name of the Outlook account.

        """
        credentials = Credentials(username=login_name, password=password)
        self._account = Account(
            account_name, credentials=credentials, autodiscover=True
        )

    async def backup_to_json_file(self, output_path: Path) -> None:
        """Backup Outlook's current out-of-office settings to disk.

        Args:
            output_path (Path): The location where to store the backup.
        """
        oof = cast(OofSettings, self._account.oof_settings)
        start_at = cast(EWSDateTime, oof.start)
        end_at = cast(EWSDateTime, oof.end)
        settings = {
            "state": oof.state,
            "start": start_at.ewsformat(),
            "end": end_at.ewsformat(),
            "external_audience": oof.external_audience,
            "internal_reply": oof.internal_reply,
            "external_reply": oof.external_reply,
        }
        # Save settings to disk as JSON
        async with aiofiles.open(output_path, "w") as file:
            json_content = json.dumps(settings, indent=2)
            await file.write(json_content)

    async def set_internal_reply(self, html_content: str) -> None:
        """Sets the internal auto-reply message for a month.

        Args:
            html_content (str): The message to set as the internal auto-reply.
        """
        start_at = datetime.now(tz=timezone.utc) - timedelta(days=1)
        end_at = datetime.now(tz=timezone.utc) + timedelta(days=5)

        print(f"Setting internal auto-reply message from {start_at} to {end_at}...")
        self._account.oof_settings = OofSettings(
            state=OofSettings.ENABLED,
            external_audience="None",
            internal_reply=html_content,
            external_reply="-",  # Cannot be empty string or None!
            start=start_at,
            end=end_at,
        )

内容生成器

现在,进入我们项目的核心:内容生成器。我们需要生成两种类型的内容。首先,我们使用 GPT-4 构建自动回复文本。然后,我们让 DALL-E 3 生成一张结合生成文本元素的图像。

不幸的是,DALL-E 3 对其提示词有一个 限制。为了解决这个问题,我们使用 GPT-4 来生成一个简洁的提示词,供 DALL-E 3 使用,且该提示词包含从给定文本中提取的元素。

这个过程需要三次不同的调用到 AI 模型:

  • generate-text 请求 GPT-4 编写新文本,用于自动回复电子邮件。

  • generate-dall-e-prompt 调用 GPT-4,促使其基于我们从第一次调用生成的文本,设计一个专门用于 DALL-E 3 的提示词。这有点像写代码来生成代码。

  • generate-image 请求 DALL-E 3 生成一张图像,用于搭配自动回复消息。在这里,我们使用由 generate-dall-e-prompt 调用生成的提示词。

我们将让 AIConfig 来协调整个过程。为此,它需要一个描述如何实现我们期望结果的配置文件。我们创建了一个配置文件,包含三个命名的提示词:

{
  "name": "auto-reply-content-generator",
  "description": "Configuration for generating content for auto-reply messages.",
  "schema_version": "latest",
  "metadata": {
    "model_parsers": {
      "gpt-4-1106-preview": "gpt-4"
    }
  },
  "prompts": [
    {
      "name": "generate-text",
      "input": "Write an auto-reply message in Danish following the same structure of earlier messages.",
      "metadata": {
        "model": {
          "name": "gpt-4-1106-preview",
          "settings": {
            "model": "gpt-4-1106-preview",
            "max_tokens": 1000,
            "temperature": 0.1,
            "system_prompt": "You're a renowned expert at crafting witty and engaging out-of-office replies in Danish. [...]"
          }
        }
      }
    },
    {
      "name": "generate-dall-e-prompt",
      "input": "Generate a prompt for DALL-E 3 to create an illustration that complements the following out-of-office message:\n{{auto_reply_message}}",
      "metadata": {
        "model": {
          "name": "gpt-4-1106-preview",
          "settings": {
            "model": "gpt-4-1106-preview",
            "max_tokens": 1000,
            "temperature": 0.1,
            "system_prompt": "You are an expert prompt engineer for the image generation model: DALL-E 3\. [...]"
          }
        },
        "parameters": {
          "auto_reply_message": "Parameter for the auto-reply message."
        }
      }
    },
    {
      "name": "generate-image",
      "input": "{{dall_e_prompt}}",
      "metadata": {
        "model": {
          "name": "dall-e-3",
          "settings": {
            "model": "dall-e-3",
            "size": "1792x1024",
            "quality": "standard"
          }
        },
        "parameters": {
          "dall_e_prompt": "Parameter for the DALL-E prompt."
        }
      }
    }
  ]
}

一旦你理解了配置架构,读取配置文件相对简单:

[## AIConfig 规格说明 | AIConfig

aiconfig 文件是源代码控制友好的 JSON 文档,包含生成性 AI 模型设置、提示输入和输出,以及灵活的多用途元数据。

aiconfig.lastmileai.dev](https://aiconfig.lastmileai.dev/docs/overview/ai-config-format?source=post_page-----876d4fb81342--------------------------------)

接下来,我们构建一个类,暴露两个主要方法:generate_messagegenerate_image。在这些方法内部,我们将配置文件注入到 AIConfig 中,并让它发挥魔力:

from pathlib import Path

from aiconfig import AIConfigRuntime, InferenceOptions

class AutoReplyContentGenerator:
    """Represents a class that generates content for auto-reply messages."""

    def __init__(self, config_file_path: Path, output_dir: Path, verbose: bool) -> None:
        """Initializes a new instance of the AutoReplyContentGenerator class.

        Args:
            config_file_path (Path): The path to the AI Config file to use.
            output_dir (Path): The directory to save outputs to.
            verbose (bool): Whether to print debug messages to stdout.
        """
        if not config_file_path.exists():
            raise ValueError(f"File {config_file_path} not found")

        self._output_path: Path = output_dir / config_file_path.name
        self._runtime: AIConfigRuntime = AIConfigRuntime.load(config_file_path)
        self._verbose = verbose

    async def generate_message(self) -> str:
        """Generates an auto-reply message.

        Returns:
            str: The generated message.
        """
        inference_options = InferenceOptions(stream=False)

        if self._verbose:
            print("Running inference for prompt 'generate-text'...")

        auto_reply_message = await self._runtime.run_and_get_output_text(
            prompt_name="generate-text",
            options=inference_options,
        )
        self._save_outputs()

        print(f"Generated auto-reply message:\n{auto_reply_message}\n")
        return auto_reply_message

    async def generate_image(self, auto_reply_message: str) -> str:
        """Generates an image to accompany the given auto-reply message.

        Args:
            auto_reply_message (str): The auto-reply message to use as inspiration for the image generation.

        Returns:
            str: The URL of the generated image.
        """
        if self._verbose:
            print("Running inference for prompt 'generate-dall-e-prompt'...")

        inference_options = InferenceOptions(stream=False)
        dall_e_prompt = await self._runtime.run_and_get_output_text(
            prompt_name="generate-dall-e-prompt",
            options=inference_options,
            params={
                "auto_reply_message": auto_reply_message,
            },
        )
        self._save_outputs()

        if self._verbose:
            print(f"Generated prompt for DALL-E:\n{dall_e_prompt}\n")
            print("Running inference for prompt 'generate-image'...")

        image_url = await self._runtime.run_and_get_output_text(
            prompt_name="generate-image",
            options=inference_options,
            params={
                "dall_e_prompt": dall_e_prompt,
            },
        )
        self._save_outputs()

        if self._verbose:
            print(f"Generated image URL:\n{image_url}\n")

        return image_url

    def _save_outputs(self) -> None:
        """Saves the outputs of the models to a JSON file."""
        self._runtime.save(
            json_config_filepath=str(self._output_path),
            include_outputs=True,
        )

文件下载器

就像有许多编程语言一样,使用 Python 从互联网上下载文件也有多种方式。但正如一位明智的工程师所说:“选择工具就像选择编程语言一样,要谨慎并考虑项目的需求。”我们将使用异步的 aiohttp 库,并搭配 tqdm 来提供下载进度的视觉反馈,因为,为什么不呢:

from pathlib import Path
from typing import Optional
from urllib.parse import unquote, urlparse

import aiofiles
import aiohttp
from tqdm.asyncio import tqdm_asyncio

class FileDownloader:
    """Represents a class that downloads files."""

    def __init__(
        self,
        output_dir: Path,
        verify_ssl: bool,
        verbose: bool,
        download_chunk_size: int = 1024,
    ) -> None:
        """Initializes a new instance of the FileDownloader class.

        Args:
            output_dir (Path): The directory to save downloaded files to.
            verify_ssl (bool): Whether to verify SSL certificates when downloading files.
            download_chunk_size (int, optional): The size of each chunk to download. Defaults to 1024.
        """
        self._http = aiohttp.ClientSession(
            connector=aiohttp.TCPConnector(ssl=verify_ssl)  # type: ignore
        )
        self._chunk_size = download_chunk_size

        self._output_dir = output_dir
        self._output_dir.mkdir(parents=True, exist_ok=True)
        self._verbose = verbose

    async def download_one(
        self, url: str, local_file_name: Optional[str] = None
    ) -> Path:
        """Downloads an file from a URL and stores it on local disk.

        Args:
            url (str): The URL to download the file from.
            local_file_name (Optional[str], optional): The name of the file to save the downloaded file to. Defaults to None.

        Returns:
            Path: The location of the downloaded file in the local disk.
        """
        file_path = self._get_local_file_path(url=url, file_name=local_file_name)
        async with self._http.get(url=url) as response:
            if response.status != 200:
                raise Exception(f"Failed to download file: {response.status}")

            if self._verbose:
                print(f"Downloading file from {url} to {file_path}...")

            total_size = int(response.headers.get("content-length", 0))
            with tqdm_asyncio(
                total=total_size, unit="B", unit_scale=True, desc="Downloading"
            ) as progress_bar:
                async with aiofiles.open(file_path, "wb") as file:
                    async for data in response.content.iter_chunked(self._chunk_size):
                        await file.write(data)
                        progress_bar.update(len(data))
        return file_path

    async def close(self) -> None:
        """Closes the HTTP session."""
        await self._http.close()

    def _get_local_file_path(self, url: str, file_name: Optional[str]) -> Path:
        """Gets the path to save the downloaded file to.

        Args:
            file_name (Optional[str]): The name of the file to save the downloaded file to. Defaults to None.

        Returns:
            Path: The path to save the downloaded file to.
        """
        if file_name is None:
            file_name = unquote(urlparse(url).path.split("/")[-1])
        file_path = self._output_dir / file_name
        if file_path.exists():
            # raise Exception(f"File {file_path} already downloaded.")
            print(f"WARNING. File {file_path} already exists. Overwritting...")
        return file_path

图像优化器

我们可以通过调整大小和量化 DALL-E 3 生成的图像来显著减小文件大小。使用 Pillow,这两个操作只需要少量代码。我们将它们封装在一个类中:

from pathlib import Path

from PIL import Image

class ImageOptimizer:
    def __init__(self, max_width: int, quantize: bool, image_quality: int) -> None:
        """Initializes a new instance of the ImageOptimizer class.

        Args:
            max_width (int): The maximum width of the image in pixels.
            quantize (bool): Whether to quantize the image to reduce file size.
            image_quality (int): The quality of the image when saving it to disk from 1 to 100.
        """
        self._max_width = max_width
        self._quantize = quantize
        self._image_quality = image_quality

    def run(self, input_path: Path) -> Path:
        """Optimizes an image and stores the image on disk.

        Args:
            input_path (Path): The path to the image to optimize.

        Returns:
            Path: The location of the optimized image on disk.
        """
        output_path = input_path.parent / f"{input_path.stem}-optimized.jpg"

        img = Image.open(input_path)
        img = img.convert("RGB")

        img.thumbnail(size=(self._max_width, self._max_width), resample=Image.LANCZOS)

        if self._quantize:
            # Quantize image to reduce file size. Pillow converts the image to a
            # palette image with at most 256 colors. This is done by storing 1 byte for
            # each pixel instead of storing 3 bytes for R, G and B for each pixel.
            # The single byte is used to store the index into the palette.
            img = img.quantize()

            if output_path.suffix.lower() in [".jpg", ".jpeg"]:
                # Convert to RGB before saving to JPEG to avoid errors.
                img = img.convert("RGB")

        img.save(output_path, optimize=True, quality=self._image_quality)
        return output_path

HTML 创建器

一旦我们生成了一些文本并与图像配对,我们就将内容转化为单一的电子邮件作品。另一种做法是使用链接的图像,这需要弄清楚如何为我们的外出回复电子邮件托管图像。值得吗?对于这个规模的项目来说,不值得。

我们可以轻松避免麻烦的图像托管问题。MIME 是一个标准,它允许我们通过构建一个嵌入图像的 HTML 格式电子邮件来结合文本和图像。

对于电子邮件布局,我们创建了一个外部文件作为 HTML 模板来使用。没有必要将其硬编码,因为没有人希望那种代码味道在周围 lingering。该模板包含三个可自定义的字段:一个用于文本,另外两个字段用于图像。

我们为文本添加了一些 HTML 样式。没有太过花哨,只是足够让它有合适的间距。为了将图像二进制数据编码成文本以便嵌入到标记中,我们使用了 Base64 编码。

这是代码:

from base64 import b64encode
from pathlib import Path

import aiofiles

class AutoReplyHtmlCreator:
    """Represents a class that creates the HTML for an auto-reply message."""

    def __init__(self, template_file_path: Path) -> None:
        """Initializes a new instance of the AutoReplyHtmlCreator class.

        Args:
            template_file_path (Path): The path to the HTML template to use.
        """
        self._template_file_path = template_file_path

        if not template_file_path.exists():
            raise ValueError(f"File {template_file_path} not found")

    async def run(self, message: str, image_file_path: Path, output_path: Path) -> str:
        """Creates the HTML for an auto-reply message.

        Args:
            message (str): The message to include in the auto-reply.
            image_file_path (Path): The path to the image to include in the auto-reply.
            output_path (Path): The path to save the HTML to.

        Returns:
            str: The HTML for the auto-reply message.
        """
        async with aiofiles.open(self._template_file_path, "r") as file:
            template = await file.read()

        async with aiofiles.open(image_file_path, "rb") as file:
            image_data = await file.read()
            image_base64 = b64encode(image_data).decode("utf-8")

        message_in_html = message.replace("\n\n", "</p><p>")
        message_in_html = message_in_html.replace("\n", "<br/>")
        message_in_html = f"<p>{message_in_html}</p>"

        html = template.replace("{{CONTENT}}", message_in_html)
        html = html.replace("{{IMAGE_BASE64}}", image_base64)
        html = html.replace("{{IMAGE_CONTENT_TYPE}}", "image/jpeg")

        async with aiofiles.open(output_path, "w") as file:
            await file.write(html)

        return html

用户界面

最后一个缺失的部分是用户界面。为此,我们使用了Streamlit来创建一个基本的网页界面,因为我们的需求很简单:

  • 一个整洁的列表,展示之前制作的数字杰作。

  • 按钮用于制作一对新的文本消息和图像。

  • 一个地方用于调整文本消息,解决任何小问题。

  • 一个按钮,用于通过编程设置 Outlook 的外出自动回复。

用户界面利用了我们所有的组件来实现上述功能。在下面的代码清单中,我尽力省略了详细的代码,亲爱的读者。如果你感兴趣,可以在GitHub上查阅完整的实现:

from pathlib import Path
from typing import cast

import anyio
import streamlit as st
from roll.config import settings
from roll.data import ActiveOutOfOfficeSetting, AutoReplyRecord, DataRepository
from roll.email import AutoReplyHtmlCreator, OutlookAutoReplyClient
from roll.image import ImageOptimizer
from roll.io import FileDownloader
from roll.models import AutoReplyContentGenerator
from roll.utils import utcnow

class StreamlitApp:
    def __init__() -> None:
        ...

    async def run(self) -> None:
        await self._setup_page_config()
        await self._build_sidebar()
        await self._build_main_content()

    async def _setup_page_config(self) -> None:
        ...

    async def _build_sidebar(self) -> None:
        ...

    async def _build_main_content(self) -> None:
        ...

    async def _render_navbar(self, record: AutoReplyRecord) -> None:
        ...

    async def _create_new_content(self) -> None:
        ...

    async def _generate_message(self, record: AutoReplyRecord) -> None:
        ...

    async def _generate_image(self, record: AutoReplyRecord) -> None:
        ...

    async def _set_out_of_office(self, record: AutoReplyRecord) -> None:
        ...

async def main() -> None:
    """Main entry point of the UI."""
    app = StreamlitApp(
        data_dir=Path("data/repository"),
        ai_config_path=Path("config/auto-reply-content-gen.aiconfig.json"),
        html_template_file_path=Path("config/auto-reply-template.html"),
        oof_data_dir=Path("data/oof"),
        outlook_login_name=settings.LOGIN,
        outlook_password=settings.PASSWORD,
        outlook_account_name=settings.ACCOUNT_NAME,
    )
    await app.run()

if __name__ == "__main__":
    anyio.run(main)

以下是用户界面的屏幕录制。左侧的侧边栏是一个画廊,展示了我们过去的作品。点击侧边栏中的编辑(✏️)按钮会将内容带到右侧的主界面。在这里,我们可以调整现有的消息,或创造一个新消息,以及它的视觉对应物。你可以轻松地让 LLM 生成其他语言的文本,只需按文章中讨论的方式调整 AIConfig 文件中的提示。设置外出消息按钮会将显示的内容作为我们的 Outlook 外出自动回复消息进行设置。

一段简短的用户界面屏幕录制,由我亲自提供。

是时候总结了

我们完成了开发一个 Python 程序的过程,该程序使用 GPT-4 和 DALL-E 3 创建非典型的外出自动回复电子邮件。

当然,我们可以快速拼凑一个庞大的 1000 行脚本来实现我们的目标。但我们没有。我们遵循了我们的软件工程原则——这些原则通过多年的“洗脑”深深植入我们脑海,并渴望为新一代充满好奇心的软件工程师们做到同样的事。

本能地,我们回想起软件工程更多的是关于深思熟虑的设计,而非单纯的编码。因此,我们从仔细考虑如何结构化代码开始了这项工作。

我们尝试将代码组织成类,使用描述性名称和类型提示以提高代码的清晰度。在尽量编写最少代码的同时,我们通过使用其他工程师的 Python 库来解决问题。

虽然我们已将调度方面的工作推迟到一个未来未确定的日期,但我们已经打下了坚实的基础。我们的应用程序可以生成富有创意却又专业的自动回复,并附带相关图片。此外,它还允许我们通过一键操作将邮件无缝地集成到 Outlook 中。

所以,下次你不在时——无论是在度假、全天研讨会中,还是在丰盛午餐后小睡片刻——何不让生成式 AI 为你生成自动回复,或许这会让你的同事们会心一笑,甚至还想要更多。

感谢阅读。如果你以后喜欢类似的文章,可以在 Medium 上关注我,或通过 LinkedIn与我联系。

让文本数据准备好供 AI 使用

原文:towardsdatascience.com/making-text-data-ai-ready-81d7fa83fd58?source=collection_archive---------5-----------------------#2024-10-04

使用无代码解决方案的介绍

Brian Perron, PhDTowards Data Science Brian Perron, PhD

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

--

展示处理杂乱数据的图形。图片由作者使用 ChatGPT-4o 制作。

人们使用大型语言模型在来自不同来源的文本数据上执行各种任务。这些任务可能包括(但不限于)编辑、总结、翻译或文本提取。这个工作流程的主要挑战之一是确保你的数据是 AI-ready。本文简要概述了 AI-ready 的含义,并提供了一些无代码解决方案,帮助你达成这一目标。

什么是 AI-ready?

我们被来自不同来源的大量非结构化文本数据包围,包括网页、PDF、电子邮件、组织文档等。在 AI 时代,这些非结构化文本文件可能是重要的信息来源。对于许多人来说,处理非结构化文本数据的典型工作流程是将一段文本提交给大型语言模型(LLM)。

ChatGPT 中的翻译任务图像。截图由作者提供。

虽然复制粘贴方法是与 LLM 合作的标准策略,但你可能会遇到这种方法无法奏效的情况。考虑以下几点:

  • 虽然许多高级模型允许上传并处理文档,但文件大小有限制。如果文件太大,你需要使用其他策略将相关文本输入到模型中。

  • 你可能只想处理较大文档中的一小部分文本。将整个文档提供给 LLM 可能会因为包含无关文本而干扰任务的完成。

  • 一些文本文件和网页,尤其是 PDF 文件,包含大量格式化内容,可能会干扰文本的处理方式。由于文档的格式问题,你可能无法使用复制粘贴方法——例如,表格和列的处理可能会出现问题。

"AI 准备就绪"意味着你的数据格式可以被大型语言模型(LLM)轻松读取和处理。对于文本数据处理,数据是纯文本格式,并带有 LLM 可以轻松理解的格式。Markdown 文件类型非常适合确保你的数据是 AI 准备就绪的。

普通文本与 Markdown

普通文本是计算机上最基本的文件类型,通常用.txt扩展名表示。许多不同的 _ 编辑器 _ 可以用来创建和编辑纯文本文件,就像使用 Microsoft Word 创建和编辑格式化文档一样。例如,PC 上的记事本应用程序或 Mac 上的 TextEdit 应用程序都是默认的文本编辑器。然而,与 Microsoft Word 不同,纯文本文件不允许你对文本进行格式化(例如,加粗、下划线、斜体等)。它们仅包含以纯文本格式表示的原始字符。

Markdown 文件是带有扩展名.md的纯文本文件。使 Markdown 文件独特的是,它使用某些字符来表示格式化。这些特殊字符会被支持 Markdown 的应用程序解析,并以特定的样式和结构呈现文本。例如,文本两侧加上星号会使其变为斜体,而双星号则会使文本加粗。Markdown 还提供了简单的方法来创建标题、列表、链接和其他标准文档元素,同时保持文件为纯文本格式。

Markdown 与大型语言模型(LLM)之间的关系是直接的。Markdown 文件包含纯文本内容,LLM 可以快速处理和理解。LLM 能够识别并解释 Markdown 格式作为有意义的信息,从而提高文本理解。Markdown 使用井号(#)表示标题,从而创建层级结构。一个井号表示一级标题,两个井号表示二级标题,三个井号表示三级标题,以此类推。这些标题在处理信息时为 LLM 提供了上下文提示。模型可以利用这一结构,更好地理解文本中不同部分的组织和重要性。

通过识别 Markdown 元素,LLM 可以理解内容及其预期的结构和重点。这导致更准确的文本解读和生成。这种关系使得 LLM 能够从文本的结构中提取额外的意义,不仅仅是从词汇本身,从而增强它们理解和处理 Markdown 格式文档的能力。此外,LLM 通常会以 Markdown 格式显示其输出。因此,通过提交和接收 Markdown 内容,您可以在与 LLM 工作时拥有更加简洁的工作流程。你还会发现,许多其他应用程序也支持 Markdown 格式(例如 Slack、Discord、GitHub、Google Docs)。

互联网上有许多资源可以学习 Markdown。以下是一些有价值的资源。请花些时间学习 Markdown 格式化。

必要工具

本节将探讨用于管理 Markdown 和将其与大型语言模型(LLM)集成的必要工具。工作流程包括几个关键步骤:

  1. 源材料:我们从结构化的文本源开始,如 PDF、网页或 Word 文档。

  2. 转换:通过使用专门的工具,我们将这些格式化文本转换为纯文本,具体来说是 Markdown 格式。

  3. 存储(可选):转换后的 Markdown 文本可以以其原始形式存储。如果您以后会重新使用或引用这些文本,建议执行此步骤。

  4. LLM 处理:Markdown 文本随后被输入到 LLM 中。

  5. 输出生成:LLM 处理数据并生成输出文本。

  6. 结果存储:LLM 的输出可以存储以供进一步使用或分析。

将格式化文本转换为纯文本的工作流程。图像由作者使用 Mermaid 图表制作。

这个工作流程高效地将各种文档类型转换为 LLM 可以快速处理的格式,同时保留了存储输入和输出以供将来参考的选项。

Obsidian:保存和存储纯文本

Obsidian 是保存和存储纯文本和 Markdown 文件的最佳选择之一。当我从 PDF 和网页中提取纯文本内容时,通常会将其保存在 Obsidian 中,Obsidian 是一个免费的文本编辑器,特别适合这个目的。我还使用 Obsidian 处理其他工作,包括做笔记和保存提示。这个工具非常棒,值得学习。

Obsidian 仅仅是一个用于保存和存储纯文本内容的工具。你可能希望在工作流程中使用这一部分,但它不是必需的

Jina AI — 阅读器:从网站提取纯文本

Jina AI 是我最喜欢的 AI 公司之一。它提供了一套用于与 LLMs 合作的工具。Jina AI Reader 是一款出色的工具,它将网页转换为 markdown 格式,允许你提取纯文本内容,以便 LLM 处理。这个过程非常简单。只需在任何 URL 前添加 r.jina.ai/,你就能获得适合 LLM 的 AI 内容。

例如,考虑以下关于维基百科上大语言模型的截图:en.wikipedia.org/wiki/Large_language_model

维基百科页面的截图由作者提供。

假设我们只想使用此页面中关于大语言模型(LLM)的文本。提取这些信息可以使用复制粘贴的方法,但这样会因为其他格式而显得繁琐。不过,我们可以通过将 [r.jina.ai``](https://r.jina.ai`) 添加到 URL 的开头,使用 Jina AI-Reader:

这将以 markdown 格式返回所有内容:

维基百科页面通过 Jina AI-Reader 转换为 markdown 格式。图片由作者提供。

在这里,我们可以轻松地将相关内容复制粘贴到 LLM 中。或者,我们可以将 markdown 内容保存在 Obsidian 中,便于长期重复使用。虽然 Jina AI 提供的高级服务费用非常低,但你仍然可以免费使用这个工具。

LlamaParse:从文档中提取纯文本

高度格式化的 PDF 和其他样式化文档呈现了另一种常见挑战。在处理大语言模型(LLM)时,我们通常需要去除格式,专注于内容。考虑一个场景,你只想使用 PDF 报告中的特定部分。文档的复杂样式使得简单的复制粘贴变得不现实。此外,如果你将整个文档上传到 LLM,它可能会难以准确找到并处理所需的部分。这种情况需要一个可以将内容与格式分离的工具。LlamaIndex 的 LlamaParse 能够有效地解耦文本和样式元素,满足这一需求。

要访问 LlamaParse,你可以登录 LlamaCloud:cloud.llamaindex.ai/login。登录 LlamaCloud 后,前往屏幕左侧的 LlamaParse:

LlamaCloud 截图。图片由作者提供。

在访问了解析功能后,您可以按照以下步骤提取内容。首先,将模式更改为“准确”,该模式会生成 markdown 格式的输出。其次,拖放您的文档。您可以解析许多不同类型的文档,但根据我的经验,您通常需要解析 PDF、Word 文件和 PowerPoint 文件。只要记住,您可以处理许多不同类型的文件。在这个例子中,我使用了美国社会工作委员会的公开报告。这是一个具有高度设计风格的报告,共 94 页。

LlamaCloud 的截图。图像来自作者。

现在,您可以复制并粘贴 markdown 内容,或者将整个文件导出为 markdown 格式。

LlamaParse 输出的截图。图像来自作者。

在免费计划下,您每天可以解析 1,000 页。LlamaParse 还有许多其他值得探索的功能。

最后的想法

准备文本数据以进行 AI 分析涉及多种策略。尽管一开始使用这些技巧可能看起来有些挑战,但通过练习,您将变得更加熟悉这些工具和工作流程。随着时间的推移,您将学会高效地将它们应用到具体任务中。

为机器学习中的测试驱动开发辩护

原文:towardsdatascience.com/making-the-case-for-test-driven-development-in-machine-learning-1aa84bc2a0aa?source=collection_archive---------5-----------------------#2024-04-22

照片来源:cottonbro studio: www.pexels.com/photo/woman-using-a-computer-5473298/

发现测试驱动开发(TDD)如何改变机器学习项目的结果。本文揭示了 TDD 为什么有益且易于实施。

Jonathan SerranoTowards Data Science Jonathan Serrano

·发布于 Towards Data Science ·阅读时间 11 分钟·2024 年 4 月 22 日

--

我敢打赌你也曾经历过——坐在会议桌旁,或者参加机器学习(ML)项目会议,会上有一位 ML 工程师或数据科学家报告编写单元测试所花费的时间,而这些代码可能已经处于生产阶段。但让我们暂停一下,思考单元测试到底是做什么的。这里有一个定义:单元测试是一个代码块,用于验证应用程序代码中较小、孤立部分的准确性,通常是一个函数或方法。它的目的是确保这段代码按预期功能运行,符合开发者的初步设计、需求和逻辑。

核心思想:确保“代码块按预期运行”并不是什么新鲜事。软件开发人员早已接受了单元测试,通常甚至在编写实际代码之前就开始实现这些测试。然而,机器学习工程师和数据科学家的背景和方法往往有所不同。他们的主要目标是利用统计方法、数学以及诸如 SciKit Learn 等预构建的库,建立从输入 X 到目标 Y 的映射。但请考虑一下……

Mamba:SSM、理论及在 Keras 和 TensorFlow 中的实现

原文:towardsdatascience.com/mamba-ssm-theory-and-implementation-in-keras-and-tensorflow-32d6d4b32546?source=collection_archive---------0-----------------------#2024-03-17

了解 SSM 和 Mamba 的工作原理,并学习如何开始在 Keras 和 TensorFlow 中实现它。

Vedant JumleTowards Data Science Vedant Jumle

·发表于Towards Data Science ·阅读时间 13 分钟·2024 年 3 月 17 日

--

来源:AI 生成(SDXL)

这篇题为“Mamba: 线性时间序列建模与选择性状态空间”的论文于 2023 年 12 月 1 日提交至 arXiv,提出了一种有趣的序列建模方法。作者们——Albert GuTri Dao——介绍了“Mamba”方法,该方法利用“选择性”状态空间模型(SSM),取得了与当前无处不在的 Transformer 模型性能相媲美的成果。

Mamba 有什么独特之处?

随着大规模语言模型(LLMs)如 LLaMa-2、GPT-4、Claude、Gemini 等的崛起,Transformer 近年来变得非常流行,但它也面临上下文窗口问题。Transformer 的问题根源在于其核心的多头注意力机制。

多头注意力的主要问题源于这样一个事实:对于输入序列长度 n,时间复杂度和空间复杂度按 O(n²)比例增长。这限制了 LLM 的上下文窗口长度。因为,要将其增加 10 倍,我们需要将硬件要求(尤其是 GPU VRAM)增加 100 倍。

另一方面,Mamba 的扩展按O(n)!, 即线性比例增长。

该图摘自 Mamba 论文,比较了 FlashAttention 和 Mamba 方法(在图例中由 scan(ours)标出)[1]

这种线性缩放已引发研究人员的猜测,即 Mamba 可能是序列建模的未来。

Mamba 的核心:状态空间模型

Mamba 模型的核心来自于状态空间模型的概念。状态空间模型,如 Transformers 和 RNN,处理信息序列,如文本、音频信号、视频帧、DNA 序列等。

状态空间模型来源于将物理系统描述为一组输入、输出和变量的思想。这些变量包括:A、B、C、D。SSM 的过程涉及计算给定输入 x(t)的内部状态向量 h(t)。然后,我们对h(t)x(t)进行加权求和,其中权重为A、B、C 和 D。在最简单的形式(连续时间不变)下,过程公式如下:

来源:wikipedia[6]

h(t)通常被称为“隐藏”状态或“潜在”状态,为了更清晰起见,我将坚持称其为“隐藏”状态。重要的是要注意,A、B、C 和 D 是 SSM 中的学习参数。

变量是什么?

变量 A、B、C 和 D 是学习得到的参数, 它们可以描述为:

  • A:在计算新的隐藏状态时,应该考虑多少前一个隐藏状态(h)。

  • B:在计算新的隐藏状态时,输入(x)应考虑多少。

  • C:在计算输出(y)时,新的隐藏状态应考虑多少。

  • D:在计算输出(y)时,输入(x)应考虑多少。

D 出现在计算的最后,并且不影响如何计算隐藏状态。因此,通常认为它在 SSM 之外,并且可以看作是一个跳跃连接。

从连续空间到离散空间的转变

上述公式适用于输入和输出属于连续空间的系统。但在诸如语言建模等情况中,输入和输出属于离散空间(词汇表中的标记值)。此外,求解h(t)在解析上具有挑战性。这可以通过执行零阶保持来实现。

在零阶保持中,每次接收到输入时,模型会保持其值直到接收到下一个输入。这导致了一个连续的输入空间。

零阶保持如何工作

这个“保持”长度由一个新的参数决定,称为步长 ∆。它可以被视为输入的分辨率。 理想情况下,∆应为无穷小。

从数学上讲,零阶保持可以描述为:

最后,我们可以创建一个离散的 SSM,如下所示:

由于 D 与 SSM 外的跳跃连接一起使用,输出可以简化为:

DX(t) 的涉及被视为跳跃连接,因此它来自 SSM 之外。

SSM 和递归

在 SSM 中,隐藏状态会被传递到接收到下一个输入时。这类似于递归神经网络的工作方式。

RNN 和 SSM 的比较

这种 SSM 的递归格式可以像 RNN 一样展开。但与迭代且缓慢的 RNN 不同,SSM 可以并行处理输入序列(就像 Transformers),这使得训练过程更快。

SSM 的展开形式

注意,‘D’ 在跳跃连接中使用,它位于 SSM 之外。

SSM 加速训练的关键见解是使用预先计算的卷积核中的变量 A, B, CMaarten Grootendorst 写了一篇非常好的解释,讲述了如何构造这个标准的‘卷积’核。但这里有一个简单的数学解释。

考虑输出 y. 对于序列长度 k,输出 y(k) 将表示为(假设 h0 = 零)

类似地,y3 可以表示为:

推断出模式,yk 可以表示为:

该公式可以进一步简化为:

那个看起来很奇怪的乘法符号表示卷积操作,其中卷积核是 K。注意到 K 与 x 无关,因此 K 可以预先计算为卷积核,这使得处理过程更快。

Mamba 和“选择性”SSM

尽管 SSM 的计算能力听起来很强大,但与 Transformers 相比,它在精度等度量指标上却相当 一般

核心问题出在变量 ∆、A、B 和 C 上。事实证明,由于我们将相同的矩阵应用于每个输入,它们实际上无法处理序列的上下文。

SSM 在处理数据时缺乏灵活性[4]

那么 Mamba 有什么特别之处?在 Mamba 中,我们使用一种叫做“选择性”SSM 的过程,其中变量 ∆、B 和 C 是根据输入计算出来的。🤔。我们通过将当前输入传递通过线性层,并将输出作为 ∆、B 和 C。

但这使得 ∆、B 和 C 变得依赖于输入,因此它们不能预先计算 😢,快速卷积在这里行不通。但是,作者讨论了一种基于 并行关联扫描 的方法。

并行关联扫描

并行关联扫描是一种在并行计算中使用的强大技术,用于执行前缀和操作,这是一种对数字序列进行累积的操作。这个操作是“关联的”,意味着数字在操作中的分组方式不会改变结果。

并行前缀和是关联扫描的一个例子。(来源:Nvidia)[7]

在 Mamba 模型的上下文中,通过定义一个关联操作符,可以获得并行关联扫描操作的元素和关联操作符。这使得可以并行解决整个时间区间的问题,从而使子区间的数量的时间复杂度达到对数级别。

硬件感知算法

除了关联扫描外,作者还提出了一种硬件感知算法,在该算法中,他们利用了与 Nvidia GPU 中 HBM 和 SRAM 速度相关的特性。他们认为,通过以下方式可以加速 SSM 状态的计算:

  • 保持隐藏状态和 A 在速度较快但容量较小的 SRAM 中,

  • 在速度较慢但容量较大的 HBM 中计算 ∆、B 和 C。

  • 然后,他们将 ∆、B 和 C 转移到 SRAM 中,在 SRAM 内部计算新的隐藏状态。

  • 然后将 ∆、B 和 C 写回到 HBM

图示来自 Mamba 论文,展示了硬件感知算法如何工作[1]

在实现部分,我不会讨论如何使用硬件感知算法,而是只会使用并行关联扫描。

最终的 Mamba 架构

考虑到这些,我们可以使用 Keras 和 TensorFlow 探索并实现 Mamba 架构。

在阅读论文并分析代码后,Mamba 架构可以分解为几个关键组件,这些组件连接如下:

Mamba 块的拆解

Mamba 架构由多个堆叠的“Mamba 块”组成。从上面的图示来看,它由相当多的组件组成。另一个需要注意的重要事项是,作者将来自选择性 SSM 的输出添加到原始输入中,然后对其应用 归一化 层。此归一化可以是层归一化,或者是 RMS 归一化

TensorFlow 和 Keras 实现

让我们从 Mamba 的编码部分开始。我们将使用以下依赖项:

tensorflow[and-cuda]==2.15.0.post1 # if you want to use GPU or
tensorflow==2.15.0.post1 # if you want to only use CPU
transformers==4.36.2 # for using the bert tokenizer
einops==0.7.0 # useful to make matrix manipulation faster
datasets==2.16.1 # to load datasets
# all other modules (like numpy) will be auto installed

导入:

import tensorflow_datasets as tfds
import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers, Model

from dataclasses import dataclass
from einops import rearrange, repeat
from typing import Union

from transformers import AutoTokenizer

import datasets
import math
import numpy as np

为了简化模型参数的处理,我们可以创建一个简单的 ModelArgs 数据类作为配置类。这样,在初始化模型时,我们只需将数据类变量作为参数传递即可。

@dataclass
class ModelArgs:
    model_input_dims: int = 64
    model_states: int = 64
    projection_expand_factor: int = 2
    conv_kernel_size: int = 4
    delta_t_min: float = 0.001
    delta_t_max: float = 0.1
    delta_t_scale: float = 0.1
    delta_t_init_floor: float = 1e-4
    conv_use_bias: bool = True
    dense_use_bias: bool = False
    layer_id: int = -1
    seq_length: int = 128
    num_layers: int = 5
    dropout_rate: float = 0.2
    use_lm_head: float = False
    num_classes: int = None
    vocab_size: int = None
    final_activation = None
    loss:Union[str, keras.losses.Loss] = None
    optimizer: Union[str, keras.optimizers.Optimizer] = keras.optimizers.AdamW()
    metrics = ['accuracy']

    def __post_init__(self):
        self.model_internal_dim: int = int(self.projection_expand_factor * self.model_input_dims)

        self.delta_t_rank = math.ceil(self.model_input_dims/16)
        if self.layer_id == -1:
            self.layer_id = np.round(np.random.randint(0, 1000), 4)

        if self.vocab_size == None:
            raise ValueError("vocab size cannot be none")

        if self.use_lm_head:
            self.num_classes=self.vocab_size
        else:
            if self.num_classes == None:
                raise ValueError(f'num classes cannot be {self.num_classes}')

            if self.num_classes == 1:
                self.final_activation = 'sigmoid'
            else:
                self.final_activation = 'softmax'

        if self.loss == None:
            raise ValueError(f"loss cannot be {self.loss}")

加载 bert-base-uncased 分词器:

tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
vocab_size = tokenizer.vocab_size

在我们实现 Mamba 和 SSM 类之前,我们需要实现并行关联扫描,代码如下:

def selective_scan(u, delta, A, B, C, D):
    # first step of A_bar = exp(ΔA), i.e., ΔA
    dA = tf.einsum('bld,dn->bldn', delta, A) 
    dB_u = tf.einsum('bld,bld,bln->bldn', delta, u, B)

    dA_cumsum = tf.pad(
        dA[:, 1:], [[0, 0], [1, 1], [0, 0], [0, 0]])[:, 1:, :, :]

    dA_cumsum = tf.reverse(dA_cumsum, axis=[1])  # Flip along axis 1

    # Cumulative sum along all the input tokens, parallel prefix sum, 
    # calculates dA for all the input tokens parallely
    dA_cumsum = tf.math.cumsum(dA_cumsum, axis=1)  

    # second step of A_bar = exp(ΔA), i.e., exp(ΔA)
    dA_cumsum = tf.exp(dA_cumsum)  
    dA_cumsum = tf.reverse(dA_cumsum, axis=[1])  # Flip back along axis 1

    x = dB_u * dA_cumsum
    # 1e-12 to avoid division by 0
    x = tf.math.cumsum(x, axis=1)/(dA_cumsum + 1e-12) 

    y = tf.einsum('bldn,bln->bld', x, C)

    return y + u * D 

通过这样,我们可以实现 MambaBlock:

class MambaBlock(layers.Layer):
    def __init__(self, modelargs: ModelArgs, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.args = modelargs
        args = modelargs
        self.layer_id = modelargs.layer_id

        self.in_projection = layers.Dense(
            args.model_internal_dim * 2, 
            input_shape=(args.model_input_dims,), use_bias=False)

        self.conv1d = layers.Conv1D(
            filters=args.model_internal_dim,
            use_bias=args.conv_use_bias,
            kernel_size=args.conv_kernel_size,
            groups=args.model_internal_dim,
            data_format='channels_first',
            padding='causal'
        )

        # this layer takes in current token 'x' 
        # and outputs the input-specific Δ, B, C (according to S6)
        self.x_projection = layers.Dense(args.delta_t_rank + args.model_states * 2, use_bias=False)

        # this layer projects Δ from delta_t_rank to the mamba internal 
        # dimension
        self.delta_t_projection = layers.Dense(args.model_internal_dim, 
                                               input_shape=(args.delta_t_rank,), use_bias=True)

        self.A = repeat(
                tf.range(1, args.model_states+1, dtype=tf.float32), 
                'n -> d n', d=args.model_internal_dim)

        self.A_log = tf.Variable(
                tf.math.log(self.A), 
                trainable=True, dtype=tf.float32, 
                name=f"SSM_A_log_{args.layer_id}")

        self.D = tf.Variable(
                np.ones(args.model_internal_dim), 
                trainable=True, dtype=tf.float32, 
                name=f"SSM_D_{args.layer_id}")

        self.out_projection = layers.Dense(
                args.model_input_dims, 
                input_shape=(args.model_internal_dim,), 
                use_bias=args.dense_use_bias)

    def call(self, x):
        """Mamba block forward. This looks the same as Figure 3 in Section 3.4 in the Mamba pape.
        Official Implementation:
            class Mamba, https://github.com/state-spaces/mamba/blob/main/mamba_ssm/modules/mamba_simple.py#L119
            mamba_inner_ref(), https://github.com/state-spaces/mamba/blob/main/mamba_ssm/ops/selective_scan_interface.py#L311
        """

        (batch_size, seq_len, dimension) = x.shape

        x_and_res = self.in_projection(x) # shape = (batch, seq_len, 2 * model_internal_dimension)
        (x, res) = tf.split(x_and_res, 
                            [self.args.model_internal_dim, 
                             self.args.model_internal_dim], axis=-1)

        x = rearrange(x, 'b l d_in -> b d_in l')
        x = self.conv1d(x)[:, :, :seq_len]
        x = rearrange(x, 'b d_in l -> b l d_in')

        x = tf.nn.swish(x)
        y = self.ssm(x)
        y = y * tf.nn.swish(res)
        return self.out_projection(y)

    def ssm(self, x):
        """Runs the SSM. See:
            - Algorithm 2 in Section 3.2 in the Mamba paper
            - run_SSM(A, B, C, u) in The Annotated S4
            Official Implementation:
            mamba_inner_ref(), https://github.com/state-spaces/mamba/blob/main/mamba_ssm/ops/selective_scan_interface.py#L311
        """
        (d_in, n) = self.A_log.shape

        # Compute ∆ A B C D, the state space parameters.
        #     A, D are input independent (see Mamba paper [1] Section 3.5.2 "Interpretation of A" for why A isn't selective)
        #     ∆, B, C are input-dependent (this is a key difference between Mamba and the linear time invariant S4,
        #                                  and is why Mamba is called **selective** state spaces)

        A = -tf.exp(tf.cast(self.A_log, tf.float32)) # shape -> (d_in, n)
        D = tf.cast(self.D, tf.float32)

        x_dbl = self.x_projection(x) # shape -> (batch, seq_len, delta_t_rank + 2*n)

        (delta, B, C) = tf.split(
                x_dbl, 
                num_or_size_splits=[self.args.delta_t_rank, n, n], 
                axis=-1) # delta.shape -> (batch, seq_len) & B, C shape -> (batch, seq_len, n)

        delta = tf.nn.softplus(self.delta_t_projection(delta)) # shape -> (batch, seq_len, model_input_dim)

        return selective_scan(x, delta, A, B, C, D)

最后,实现外部跳跃连接的残差块。

class ResidualBlock(layers.Layer):
    def __init__(self, modelargs: ModelArgs, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.args = modelargs
        self.mixer = MambaBlock(modelargs)
        self.norm = layers.LayerNormalization(epsilon=1e-5)

    def call(self, x):
        """
        Official Implementation:
            Block.forward(), https://github.com/state-spaces/mamba/blob/main/mamba_ssm/modules/mamba_simple.py#L297

            Note: the official repo chains residual blocks that look like
                [Add -> Norm -> Mamba] -> [Add -> Norm -> Mamba] -> [Add -> Norm -> Mamba] -> ...
            where the first Add is a no-op. This is purely for performance reasons as this
            allows them to fuse the Add->Norm.

            We instead implement our blocks as the more familiar, simpler, and numerically equivalent
                [Norm -> Mamba -> Add] -> [Norm -> Mamba -> Add] -> [Norm -> Mamba -> Add] -> ....

        """
        return self.mixer(self.norm(x)) + x

有了这个,我们可以初始化我们的模型。在这个例子中,我将演示如何使用 Mamba 模块创建一个简单的分类模型,但它可以很容易地修改为语言模型。让我们加载IMDB 评论数据集来进行一个简单的情感分类器。

from datasets import load_dataset
from tqdm import tqdm

dataset = load_dataset("ajaykarthick/imdb-movie-reviews")

首先我们创建一个函数,该函数将接收模型参数并返回一个模型。

def init_model(args: ModelArgs):
    input_layer = layers.Input(shape=(args.seq_length,), name='input_ids')
    x = layers.Embedding(
                args.vocab_size, 
                args.model_input_dims, 
                input_length=args.seq_length)(input_layer)

    for i in range(args.num_layers):
        x = ResidualBlock(args, name=f"Residual_{i}")(x)
        x = layers.Dropout(args.dropout_rate)(x) # for regularization

    x = layers.LayerNormalization(epsilon=1e-5)(x) # normalization layer

    # use flatten only if we are not using the model as an LM
    if not args.use_lm_head: 
        x = layers.Flatten()(x)
    x = layers.Dense(1024, activation=tf.nn.gelu)(x)
    output_layer = layers.Dense(
                args.num_classes, 
                activation=args.final_activation)(x)

    model = Model(
                inputs=input_layer, 
                outputs=output_layer, name='Mamba_ka_Mamba')
    model.compile(
        loss=args.loss,
        optimizer=args.optimizer,
        metrics=args.metrics
    )

    return model

现在我们可以初始化我们的模型,并对其进行总结:

args = ModelArgs(
    model_input_dims=128,
    model_states=32,
    num_layers=12,
    dropout_rate=0.2,
    vocab_size=vocab_size,
    num_classes=1,
    loss='binary_crossentropy',
)
model = init_model(args)
model.summary()
Model: "Mamba_ka_Mamba"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_ids (InputLayer)      [(None, 128)]             0         

 embedding_2 (Embedding)     (None, 128, 128)          3906816   

 Residual_0 (ResidualBlock)  (None, 128, 128)          129024    

 dropout_24 (Dropout)        (None, 128, 128)          0         

 Residual_1 (ResidualBlock)  (None, 128, 128)          129024    

 dropout_25 (Dropout)        (None, 128, 128)          0

 ... (I have shrinked this to make it more readable)

 dropout_35 (Dropout)        (None, 128, 128)          0         

 layer_normalization_38 (La  (None, 128, 128)          256       
 yerNormalization)                                               

 flatten_2 (Flatten)         (None, 16384)             0         

 dense_148 (Dense)           (None, 1024)              16778240  

 dense_149 (Dense)           (None, 1)                 1025      

=================================================================
Total params: 22234625 (84.82 MB)
Trainable params: 22234625 (84.82 MB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________

为了更方便地处理,让我们先将数据进行预处理成numpy 数组,然后再转换为 tf.data.Dataset 对象:

train_labels, test_labels = [], []
train_ids = np.zeros((len(dataset['train']), args.seq_length))
test_ids = np.zeros((len(dataset['test']), args.seq_length))

for i, item in enumerate(tqdm(dataset['train'])):
    text = item['review']
    train_ids[i, :] = tokenizer.encode_plus(
            text, 
            max_length=args.seq_length, 
            padding='max_length', 
            return_tensors='np')['input_ids'][0][:args.seq_length]

    train_labels.append(item['label'])

for i, item in enumerate(tqdm(dataset['test'])):
    text = item['review']
    test_ids[i, :] = tokenizer.encode_plus(
            text, 
            max_length=args.seq_length, 
            padding='max_length', 
            return_tensors='np')['input_ids'][0][:args.seq_length]

    test_labels.append(item['label'])

del dataset # delete the original dataset to save some memory

BATCH_SIZE = 32
train_dataset = tf.data.Dataset.from_tensor_slices((train_ids, train_labels)).batch(BATCH_SIZE).shuffle(1000)
test_dataset = tf.data.Dataset.from_tensor_slices((test_ids, test_labels)).batch(BATCH_SIZE).shuffle(1000)

现在模型可以进行训练:

history = model.fit(train_dataset, validation_data=test_dataset, epochs=10)

你可以尝试推理算法:

def infer(text: str, model: Model, tokenizer):
    tokens = tokenizer.encode(
            "Hello what is up", 
            max_length=args.seq_length, 
            padding='max_length', return_tensors='np')
    output = model(tokens)[0, 0]
    return output

该模型可以转化为语言模型,并且可以使用像束搜索、top-k 采样、贪心采样等算法来生成语言。

这段代码可以在我的Github上找到。

很多代码灵感来源于 mamba 的官方实现[2]以及另一个名为‘mamba-tiny’的 pytorch 实现[3]

感谢阅读。

  • 除非另有说明,所有图片均由我制作。

参考资料:

  1. Mamba 论文

  2. Mamba 原始仓库

  3. Mamba 的一个更简单的 Torch 实现:mamba-tiny

  4. Letitia 在 YouTube 上的简单解释

  5. Maarten Grootendorst 关于 SSM 和 Mamba 的文章

  6. 维基百科上的 SSM

  7. Nvidia 关于并行关联扫描的教程

想要联系我吗?请通过 vedantjumle@gmail.com 给我写信。

使用 Terraform 和本地 yaml 文件管理开发和生产环境

原文:towardsdatascience.com/manage-dev-and-prod-using-terraform-and-local-yaml-files-a0db7afb10a2?source=collection_archive---------4-----------------------#2024-06-24

使用 yaml 文件让你的 terraform 在多个项目中运作

Daniel LowTowards Data Science Daniel Low

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

--

在企业生产环境中,通常会将云项目分为开发(dev)和生产(prod)环境,以隔离这两个环境。作为工程师,能够在(dev)环境中进行必要的测试和开发,并在批准后快速高效地将它们移到(prod)环境中,是非常重要的。

图片来源:Alvaro Reyes 通过 Unsplash

正如预期的那样,解决方案有很多,terragrunt 是我首先想到的一个。然而,并不是每个人都熟悉 terraform,更不用说 terragrunt 了。我如何简化这个模板,让团队中的其他人只需使用 terraform 和 yaml 文件就能完成?以下是我对简化实现的看法。

1. 仓库结构

作为本文的参考,以下是我如何构建我的仓库的。

.
├── environment            # Folder to contain both dev and prod yaml
│   ├── dev-config.yaml
│   └── prod-config.yaml
├── modules                # Folder to contain terraform code by services
│   ├── module1
│   │   └──main.tf
│   └── module2
│       └── main.tf
├── backend.tf             # backend file to configure terraform backend
├── locals.tf              # locals file to point to environment yaml
└── main.tf                # main file to point to modules

2. 创建你的 terraform locals.tf 文件

locals.tf 文件是 terraform 与本地存储文件交互的默认方式。你的 locals.tf 文件应指向你想要工作的 terraform 工作区,这样就能调用正确的配置文件。

# Select the workspace
terraform workspace select dev
# This should run terraform plan against 'environment/dev-config.yaml' file
terraform plan

使用可以通过 git 提交的本地文件比在多个开发人员之间分别维护多个 .tfvars 文件更为推荐。请务必小心,不要在 yaml 文件中提交敏感信息。

此外,terraform 确实允许使用其他类型的配置文件,而不仅仅是 yaml,包括 json 和文本文件 [查看 link]。

# filename: locals.tf
locals {
    config = yamldecode(file("environment/${terraform.workspace}-config.yaml"))
}

2. 为开发和生产环境创建配置文件

在环境目录中,创建你的配置文件。请确保在命名时,文件名与 Terraform 工作区的名称一致,因为我们将使用变量${terraform.workspace}来区分这两个文件。

# filename: dev-config.yaml
project_id: "cloud-project-dev"  
terraform_state_bucket_name: "terraform_state_bucket-name"
some_dataset_map:
  dataset_1:
    description: "dataset_1"
  dataset_2:
    description: "dataset_2"
# filename: prod-config.yaml
project_id: "cloud-project-prod"  
terraform_state_bucket_name: "terraform_state_bucket-name"

3. 模块

我使用模块块来分隔所使用的资源。对于模块中应包含哪些内容没有严格的规则,但我通常会将为同一服务启动的资源组合在一起(例如,一个将数据写入 SQL 表的 Airflow 任务,或者一个虚拟机及其关联的负载均衡器)。

这里的关键是你的模块指向本地的 config,这里指的是我们创建的 yaml 文件。然后,locals.tf 文件应处理其余的部分。

# filename: main.tf
module "module1" {
  source                      = "./modules/module1"
  terraform_state_bucket_name = local.config.terraform_state_bucket_name
  location                    = local.config.location
}

module "module2" {
  source                      = "./modules/module2"
  project_id                  = local.config.project_id
}

来源

  • Terraform 工作区: 链接

  • Terraform 本地值: 链接

使用 VBA 管理数据透视表和 Excel 图表

原文:towardsdatascience.com/managing-pivot-table-and-excel-charts-with-vba-3dc5d672dff0?source=collection_archive---------12-----------------------#2024-06-18

通过使用 VBA 自动化数据透视表和图表的操作,节省宝贵的时间

Himalaya Bir ShresthaTowards Data Science Himalaya Bir Shrestha

·发布于Towards Data Science ·9 分钟阅读·2024 年 6 月 18 日

--

随着数字化和创新的推进,处理大数据集已经成为大学、研究机构和各行各业的常态。自动化常见的任务可以为分析师节省数小时的时间,让他们能花更多的时间分析数据并生成洞察,而不是处理和管理数据。

数据透视表就是这样一个工具,它允许从堆叠的数据集中生成多个见解。只需根据需求调整数据透视表的字段设置,就能进行大量的分析。如果有人有许多堆叠的数据集,可以将其转换为数据透视表,那么如何让这些数据透视表自动刷新呢?如何根据系列名称自动选择图表颜色并管理其他图表元素?这篇博客通过一步步的示例给出了这些问题的答案。

虽然在开始时设置代码可能需要相当大的努力,但一旦设置完成,它将变得非常方便且节省时间,特别是对那些每天处理大量数据集的分析师们来说。让我们开始吧。

图片来自Norbert Levajsics提供,来自Unsplash

数据

我从Excel-Easy网站上获取了示例数据。这个网站包含了适合初学者的 Excel 和 VBA 功能教程。数据集包含了 2016 年和 2017 年特定国家的水果和蔬菜销售记录。数据集包含六个字段:订单 ID、产品、类别、金额、日期和国家。我将这个数据集分为两个 CSV 文件,分别命名为results1.csvresults2.csv,用于创建数据透视表。

数据集结构。图示由作者提供。

我在RawData工作表中提取了这个数据集。我创建了一个动态范围,名为raw_data_source,其公式如下:

=OFFSET(RawData!$A\(1, 0, 0, COUNTA(RawData!\)A:$A), COUNTA(RawData!$1:$1))

创建动态范围的原因是,数据集的大小(行数)在不同的 CSV 文件中不同,我希望使用整个数据集作为数据透视表的源。

创建动态命名范围作为数据透视表的数据源。图示由作者提供。

数据透视表和图表

RawData工作表中的raw_data_source范围的数据集被用作数据源,创建了两个简单的数据透视表和图表。

指定数据透视表的数据源。图示由作者提供。

第一个图表展示了按产品过滤的各国水果和蔬菜总销售额,字段设置如下所示。

第一个数据透视图及其设置。图示由作者提供。

第二个图表展示了各国的水果和蔬菜总销售额。

第二个数据透视图及其设置。图示由作者提供。

1. 基于新数据集自动刷新数据透视表

在这一步,我想要自动刷新来自我指定文件的数据集。我在Admin工作表中创建了一个占位符,用于放置我想要提取并刷新数据透视表的 CSV 数据集的路径。我将单元格A2命名为filepath,如下所示:

数据集文件路径占位符。图示由作者提供。

此步骤的代码在UpdateRawData子程序中给出。我声明了变量wbws_adminws_rawdatafilepath,分别代表工作簿、Admin 工作表、RawData 工作表和包含数据集的文件路径。首先,我清空了 RawData 工作表的内容。然后,我根据文件名进入源工作簿,选择相应的工作表,复制其内容,返回ws_rawdata工作表,并将内容粘贴为值。最后,我使用wb.RefreshAll代码刷新了工作簿,刷新了包括数据透视表和图表的整个工作簿。

Sub UpdateRawData()

Dim wb As Workbook
Dim ws_admin As Worksheet
Dim ws_rawdata As Worksheet
Dim filepath As String

Set wb = ThisWorkbook
Set ws_admin = wb.Worksheets(“Admin”)
Set ws_rawdata = wb.Worksheets(“RawData”)

'Clear Rawdata sheet
ws_rawdata.Activate
Cells.Clear

'get file path
filepath = ws_admin.Range(“filepath”)

Application.DisplayAlerts = False

'Open source file and select all contents
Dim src_wb As Workbook
Dim src_ws As Worksheet

Set src_wb = Workbooks.Open(filepath)
Set src_ws = src_wb.Sheets(1)
src_ws.UsedRange.Select

'Copy all
Selection.Copy

'Paste all
ws_rawdata.Range(“A1”).PasteSpecial xlPasteValues

'Close source file
src_wb.Close SaveChanges:=False

wb.RefreshAll

Application.DisplayAlerts = True

End Sub

我将这个子程序链接到刷新文件和图表按钮。一旦我在占位符中指定了文件名并点击按钮,数据集和数据透视表就会自动更新。

2. 使用 VBA 创建颜色表

我在Admin工作表中创建了一个包含数据集中具体水果和蔬菜列表的表格。在 F 列中,我指定了每种水果或蔬菜的初步十六进制颜色代码。我希望使用这些颜色来更新数据透视图中的颜色。首先,我希望用每个单元格中指定的颜色填充 F 列。

创建数据集中可用的水果和蔬菜列表,以及它们对应的十六进制颜色代码。图示由作者提供。

十六进制颜色代码

每种颜色的十六进制代码是一个 6 位的十六进制数(0 到 9 或 A 到 F),以 16 为基数。在十六进制颜色代码RRGGBB中,每一对两个字母表示红色、绿色和蓝色的不同色调。在 RGB(红绿蓝)系统中,每种色调的值范围从 0 到 255。

例如,对于十六进制颜色代码ffab23,我通过以下计算得出相应的 RGB 代码。十六进制代码ffab23在十六进制系统中转换为(255, 171, 35),即代表红色、绿色和蓝色的色彩组件。

手动计算十六进制颜色代码 ffab23 的 RGB 值。图示由作者提供。

这也可以通过以下方式在 Excel 中的“自定义颜色”选项中进行可视化:

演示十六进制颜色代码 ffab23 的 RGB 组件和实际颜色。图示由作者提供。

在 Excel VBA 中,使用**&H**与一个值组合意味着它是一个十六进制数,而Val()函数返回相应的十进制数。在下面的立即窗口中,rgb分别代表红色、绿色和蓝色的相应十进制值。

立即窗口显示了如何从十六进制代码在 VBA 中推导出相应的十进制值。图示由作者提供。

在下面的代码中,我为包含每种水果或蔬菜的十六进制颜色代码的表格创建了一个命名范围color_code_range。我遍历了选择中的每个单元格,推导出红、绿、蓝组件的十进制值,并用相同的 RGB 颜色代码填充单元格的内部。

Sub refresh_color_table()

Dim wb As Workbook
Dim ws_admin As Worksheet

Set wb = ThisWorkbook
Set ws_admin = wb.Sheets(“Admin”)
ws_admin.Range("color_code_range").Select

Dim c As Range
Dim r, g, b As Long
Dim Hex As String

For Each c In Selection

     Hex = c.Value

     r = Val("&H" & Mid(Hex, 1, 2))
     g = Val("&H" & Mid(Hex, 3, 2))
     b = Val("&H" & Mid(Hex, 5, 2))
     c.Interior.Color = RGB(r, g, b)

Next c

End Sub

当我运行上述子程序时,F 列会被涂上与颜色代码相同的颜色,如下所示:

用上述颜色代码填充单元格。图示由作者提供。

如果更改了颜色代码,并且再次运行代码,它将在表格中生成新的颜色。是不是很酷?

3. 在 VBA 中使用字典

在下一步中,我希望根据我之前选择的自定义颜色为数据透视图表分配颜色。为此,我创建了一个字典,包含产品名称作为键,十六进制颜色代码作为值。

在 VBA 中创建字典对象的前提条件是首先启用 Microsoft Scripting Runtime。为此,您可以进入工具 -> 引用 -> 勾选 Microsoft Scripting Runtime 旁边的框并点击确定。

在 Excel VBA 中使用字典的前提条件。图示由作者提供。

在下面的代码中,我创建了一个名为colorMap的字典对象。我遍历了Admin工作表中 E2:F10 的范围,将 E 列的内容作为键,F 列的内容作为相应的值。

Sub create_dict()

Dim wb As Workbook
Dim ws_admin As Worksheet

Set wb = ThisWorkbook
Set ws_admin = wb.Sheets(“Admin”)

Dim colorMap As Dictionary
Set colorMap = New Dictionary

Dim i As Integer

For i = 2 To 10
    If Not colorMap.Exists(Range("E" & i).Value) Then
        colorMap.Add Range("E" & i).Value, Range("F" & i).Value
    End If
Next i

For Each Key In colorMap.Keys()
     Debug.Print Key & ": " & colorMap(Key)
Next Key

End Sub

在上面的第二个 for 循环中,我遍历了 colorMap 字典的每个键,并在即时窗口中打印了键和值,如下所示:

打印 colorMap 字典对象的内容。图示由作者提供。

4. 使用 VBA 管理数据透视图表元素

基于之前步骤中的学习,我想更进一步,使用 VBA 更新数据透视图表元素。在这种情况下,我希望根据单元格的值自动设置图表标题,并将Admin工作表中定义的特定水果和蔬菜的颜色应用到数据透视图表中。

在这一步中,我将Plot1Plot2工作表分配给一个名为sheetNames的数组。我声明了chartObj为 ChartObject。在每个工作表中,我遍历了所有 ChartObjects 中的每个 ChartObject。

注意: ChartObject是 VBA 中用于控制嵌入图表在工作表中大小和外观的Chart对象的容器。它是ChartObjects集合的成员。理解每个对象在 VBA 中的方法和属性差异是非常重要的。

遍历每个 chartObj 之后,我根据 E1 单元格中的值为每个图表设置标题。接下来,我遍历 Chart 对象的整体 Series Collection 中的每个系列。我将系列名称(即水果或蔬菜的名称)分配给名为itemName的变量,并从colorMap字典中获取相应的颜色代码。与步骤 2 类似,我获取了颜色代码的红、绿、蓝分量的十进制数值,并用 RGB 颜色填充了系列条形。

Sub refresh_plots()

Dim wb As Workbook
Dim ws_admin As Worksheet
Dim ws As Worksheet

Set wb = ThisWorkbook
Set ws_admin = wb.Sheets("Admin")

Dim colorMap
Set colorMap = CreateObject("Scripting.Dictionary")
Dim i As Integer

For i = 2 To 10
     If Not colorMap.Exists(Range("E" & i).Value) Then
          colorMap.Add Range("E" & i).Value, Range("F" & i).Value
     End If
Next i

Dim sheetNames As Variant
Dim sheetName As Variant
sheetNames = Array("Plot1", "Plot2")

Dim hex_color_code As String
Dim r, g, b As Integer

Dim chartObj As ChartObject

For Each sheetName In sheetNames
     Set ws = wb.Sheets(sheetName)

     For Each chartObj In ws.ChartObjects

          chartObj.Chart.HasTitle = True
          chartObj.Chart.ChartTitle.Text = ws.Range("E1").Value

          For Each Series In chartObj.Chart.SeriesCollection

               itemName = Series.Name
               hex_color_code = colorMap(itemName)

               r = Val("&H" & Mid(hex_color_code, 1, 2))
               g = Val("&H" & Mid(hex_color_code, 3, 2))
               b = Val("&H" & Mid(hex_color_code, 5, 2))
               Series.Format.Fill.ForeColor.RGB = RGB(r, g, b)

          Next Series
     Next chartObj
Next sheetName

End Sub

下面展示了使用此代码的示意图。

使用 VBA 代码转换数据透视图表元素。图示由作者提供。

结论

在本文中,我阐述了如何使用 VBA 自定义和自动化处理数据透视表和图表。我展示了四个关键任务的自动化:用新数据集刷新数据透视表和图表;根据十六进制颜色代码创建颜色表;如何在 VBA 中使用字典;以及如何使用 VBA 管理和更新数据透视图表元素。在第二步中,我详细讲解了如何使用 Excel 和 VBA 将十六进制颜色代码转换为相应的 RGB 颜色代码,并在随后的步骤中使用了这一技术。

本文的代码和 Excel 文件可以在这个 GitHub 仓库中找到。感谢您的阅读!

很多文章告诉你 Python 技巧,但很少有人告诉你为什么。

原文:towardsdatascience.com/many-articles-tell-you-python-tricks-but-few-tell-you-why-d4953d24e80b?source=collection_archive---------1-----------------------#2024-03-24

图片来自 Annette 来自 Pixabay

三个常见的 Python 技巧可以让你的程序运行得更快,我将解释它们背后的原理。

Christopher TaoTowards Data Science Christopher Tao

·发表于 Towards Data Science ·阅读时长 8 分钟·2024 年 3 月 24 日

--

我刚刚做了一个简单的搜索,很容易找到许多试图告诉我们各种 Python 技巧的文章。它们要么是更“Pythonic”,要么是让我们的程序运行得更快。这些文章没有错,因为大多数技巧都非常有用。事实上,我自己也写过很多这样的文章。

然而,这类文章常常受到批评,因为没有一种技巧适用于所有场景。这也是事实。在我看来,更重要的是理解这些技巧为何存在,这样我们就能知道何时使用它们,何时不使用。

在这篇文章中,我将挑选出其中的三种技巧,并提供它们背后机制的详细解释。

1. 更快地连接字符串

图片来自 浩一 萩原 来自 Pixabay

你通常是如何连接字符串的?

例如,假设我们有一个需要连接在一起的字符串列表。

strs = ['Life', 'is', 'short,'…

基于地图匹配的车速预测

原文:towardsdatascience.com/map-matching-for-speed-prediction-f1a534626368?source=collection_archive---------6-----------------------#2024-01-19

你会开多快?

João Paulo FigueiraTowards Data Science João Paulo Figueira

·发布于 Towards Data Science ·14 分钟阅读·2024 年 1 月 19 日

--

图片由 Julian Hochgesang 提供,来源于 Unsplash

在规划未来的车辆路线时,你依赖数字地图服务提供商来准确预测车速。无论是在准备驾车出行时拿起手机,还是在专业环境中为你的车队规划路线时,都是如此。预测的车速是行程成本的一个重要组成部分,特别是它们是电动车和内燃机车能耗的主要驱动因素之一。

数字地图服务提供商通过收集实时交通信息和历史记录来估算你在任何特定时刻沿着某条特定道路的行驶速度。借助这些数据和智能算法,他们估算出一辆普通车辆在规划路线中行驶的速度。有些服务还会根据每辆车的特征来调整路线和预计行程时间。

那么,针对像你这样的特定车辆和驾驶员呢?这些预测适用吗?你的车辆和驾驶员可能有一些特殊需求或习惯,这些并不符合标准化的预测。我们能做得比数字地图服务提供商更好吗?如果你保持良好的历史远程信息处理数据记录,我们就有机会。

介绍

在本文中,我们将通过利用来自远程信息处理数据库的历史车速数据,提升数字地图服务提供商的车速预测质量。这个数据库包含了过去行程的记录,我们利用这些数据来调节数字地图服务提供商的标准车速推断。

其中的核心是地图匹配,即我们通过该过程将观测到的 GPS 位置“匹配”到底层数字地图。这一修正步骤使我们能够将 GPS 测量结果与地图上的道路网络表示对齐,从而使所有位置来源具有可比性。

道路网络

道路网络是一个数学概念,支持大多数数字地图应用。通常以有向多重图实现,每个节点代表一个已知的地理位置,通常是一些显著的地标,如交叉口或道路转弯的关键点,而连接的有向边则代表沿道路的直线路径。下面的图 1展示了这一概念。

图 1 — 上图展示了数字地图上简化表示的道路段。每个节点,由红点表示,代表由经度、纬度和海拔坐标定义的已知地理位置。每条连接线是一个边,数字地图使用这些边表示道路(此处省略了行驶方向)。当我们向数字地图提供商请求路线时,我们会得到这些节点和边的长序列,以及预计行驶这些道路所需的时间。(图像来源:作者)

当我们向数字地图提供商请求路线时,我们会获得一系列道路网络节点及其连接的边。该服务还提供了所有节点对之间的估计行驶时间和相应的速度(在某些情况下,速度估算覆盖多个节点)。我们通过将所有部分时间相加,得出整个行程的持续时间。

如果我们能更好地估算这些时间,我们也能得到更准确的速度估算,整体的路线预测也会更好。这些更好的估算来源于你的历史远程信息数据。但仅仅知道历史速度只是过程的一部分。在我们能够使用这些速度之前,我们必须确保能够将它们投影到数字地图上,为此,我们使用地图匹配。

地图匹配

地图匹配将从移动物体路径采样的 GPS 坐标序列映射到现有的道路图。匹配过程使用隐马尔可夫模型将采样位置映射到最可能的图边序列。因此,这一过程同时生成图边投影和隐含的节点序列。你可以在我之前的地图匹配文章中阅读更详细的解释:

## 用于轨迹预测的地图匹配

你要去哪里?你应该朝这个方向走吗?

towardsdatascience.com

阅读上述文章后,你将理解Valhalla地图匹配算法将采样的 GPS 位置投影到路网边缘,而不是节点。该服务还可能返回根据路网节点定义的匹配折线。因此,我们可以同时获得边缘投影和隐式节点序列。

另一方面,当从同一提供者获取路线规划时,我们也会得到一系列的路网节点。通过将这些节点与之前的地图匹配节点进行匹配,我们可以将已知的远程信息叠加到新生成的路线中,从而利用实际数据改进时间和速度的估算。

在使用地图匹配的位置推断实际速度之前,我们必须将它们投影到节点上,并调整已知的旅行时间,如下面图 2所示。

图 2 — 上面的图示说明了将采样的 GPS 位置的隐式速度(显示为绿色圆点)映射到地图节点之间的速度(表示为红色圆点)时所面临的挑战。橙色菱形表示地图匹配的 GPS 采样位置。在这个问题中,我们知道绿色圆点之间的平均速度,并希望推断红色圆点之间的平均速度。(图片来源:作者)

作为先决条件,我们必须正确地对两组位置进行排序,即节点和地图匹配的位置。这个过程如上面图 2所示,其中地图匹配位置(橙色菱形)与路网节点(红色圆点)一起排序。旅行顺序显然是从左到右。

我们假设 GPS 位置之间的时间差与地图匹配的时间差相同。下面图 3所示的假设至关重要,因为我们无法推断出地图匹配在时间上带来的影响。这个假设简化了计算,同时保持了较好的近似值。

图 3 — 我们假设连续样本之间的时间差与对应的地图匹配时间差相同。(图片来源:作者)

现在我们知道了连续橙色菱形之间的时间差,我们的挑战是利用这些信息推断连续红色圆点(节点)之间的时间差。下面图 4展示了这两组时间差序列之间的关系。

图 4 — 上面的图示帮助我们理解如何利用从地图匹配位置序列(橙色菱形)中获取的时间差,来插值计算路网节点(红色圆点)之间的时间差。(图片来源:作者)

我们可以安全地假设连续的橙色菱形之间的平均速度是恒定的。这个假设对于接下来的计算至关重要。但在继续之前,让我们先定义一些术语。由于 Medium 排版的限制,我们将使用一些简化。

我们需要处理两个基本量:距离和时间段。以上图 4为参考,我们将橙色菱形 1 和红色点 1 之间的距离定义为d(n1, m1)。其中,字母“m”代表“地图匹配”,字母“n”代表节点。类似地,相应的时间段为t(n1, m1),而平均速度为v(n1, m1)

让我们专注于前两个节点,看看如何利用已知的从橙色菱形 1 到 4 的时间段来推导平均速度(以及相应的时间段)。因此,两个地图匹配位置之间的平均旅行速度为:

因为平均速度是恒定的,我们现在可以计算第一个时间段。

第二个时间段就是t(m2, m3)。对于最后一个时间段,我们可以重复上面的过程。总时间为:

我们必须重复这个过程,并将其调整为节点序列和地图匹配,以计算所有节点之间的预计旅行时间。

既然我们已经看到了如何将测量的速度投影到数字地图上,让我们看看数据来源在哪里。

车联网数据库

本文使用车联网数据库推断未知道路段的平均速度。数据库中的所有地理空间数据已经与基础数字地图进行了地图匹配。这个特性帮助我们在未来的服务提供路线上匹配已知或预测的道路段速度,使用上述过程。

这里,我们将使用一个经过验证的开源车联网数据库,我最近一直在探索,并在之前发布的文章中展示过,名为扩展车辆能量数据集(EVED),其许可证为 Apache 2.0。

## 使用四叉树键估算旅行时间

本文解释了如何使用已知的速度向量和四叉树键来估算旅行时间。

towardsdatascience.com

提出的解决方案

我们将解决方案分为两个步骤:数据准备和预测。在数据准备步骤中,我们遍历电信数据库中所有已知的行程,并将测量到的行程时间映射到相应的道路网络边缘。这些计算出的边缘遍历时间然后使用最大分辨率的H3索引存储在另一个数据库中,以便在探索过程中进行更快的查询。在该过程的最后,我们收集了已知边缘的遍历时间分布,这些信息将帮助我们在预测阶段估计行驶速度。

预测阶段需要一个源路线,该路线以道路网络节点的序列表示,例如我们从 Valhalla 路线规划器中获得的那样。我们查询每一对相邻节点的相应遍历时间分布(如果有的话),并使用其均值(或中位数)来估算局部平均速度。通过将所有边缘估算值相加,我们得到预期的结果,即总行程时间的预期值。

数据准备

为了准备数据并生成参考时间分布数据库,我们必须遍历源数据中的所有行程。幸运的是,源数据库通过方便地标识所有行程来使这一过程变得容易(请参见上文)。

让我们来看一下准备边缘遍历时间的代码

图 5— 上面的代码展示了数据准备脚本的主要循环。(图片来源:作者)

上述图 5中的代码展示了数据准备代码的主循环。我们使用先前创建的 EVED 数据库,并将输出数据保存到一个新的速度数据库中。每条记录都是单一道路网络边缘的时间遍历样本。对于同一边缘,这些样本集构成了统计时间分布,我们将计算均值、中位数以及其他统计数据。

第 5 行的调用检索了源数据库中所有已知行程的列表,列表中的每个元素都是包含轨迹标识符(表的顺序标识符)、车辆标识符和行程标识符的三元组。我们需要这两个最后的元素来检索行程的信号,如第 10 行所示。

第 10 到 16 行包含了获取行程轨迹作为纬度、经度和时间戳序列的代码。这些位置不一定对应于道路网络节点;它们大多是映射到边缘上的(图 2中的橙色菱形)。

现在,我们可以请求 Valhalla 地图匹配引擎,将这些点传入并返回一个包含相应道路网络节点序列的折线,如第 18 至 25 行所示。这些节点将被存储在数据库中,并与我们在代码最后几行中推导出的遍历时间一起存储。

从地图匹配位置到节点位置的遍历时间投影分为两步。首先,第 27 行创建了一个“复合轨迹”对象,将地图匹配位置和相应的节点按旅行顺序合并。该对象将每个地图匹配段单独存储,以便稍后连接。下图图 6显示了该对象构造器(源文件)。

图 6 — 复合轨迹构造器接收地图匹配轨迹和地图节点作为分别的纬度和经度数组。代码将这两个轨迹合并为一个段列表,然后再将这些段转换为轨迹列表。(图片来源:作者)

复合轨迹构造器通过将地图匹配点序列与相应的道路网络节点合并开始。参考上图图 2中的符号,代码将橙色菱形序列与红点序列结合,使其保持旅行顺序。在第一步中,如下图图 7所示,我们创建了一个包含中间有红点的橙色菱形对的序列列表。

图 7 — 上述函数将地图匹配轨迹点与相应的道路网络节点序列合并。该函数尝试为每对轨迹点分配顺序节点(如果有的话)。一个列表将收集所有合并的序列,以便最终整合步骤。(图片来源:作者)

合并后,我们将轨迹段转换为基于节点的轨迹,移除地图匹配端点并重新计算遍历时间。下图图 8显示了计算节点间等效遍历时间的函数。

图 8 — 从段到轨迹的转换需要计算道路网络节点之间的等效时间。(图片来源:作者)

使用图 2中的符号,上述代码使用两个橙色菱形之间的遍历时间,并计算所有子段遍历时间,即节点分隔的时间。通过这种方式,我们可以通过简单的加法,后续重建所有节点间的遍历时间。

最终的转换步骤发生在图 5的第 28 行,当我们使用图 9中列出的函数将复合轨迹转换为简单轨迹时。

图 9 — 最终重建步骤将投影的轨迹段连接起来。注意,包含两个以上元素的数组被裁剪。被裁剪的元素对应于地图匹配的位置,因此被移除。只有两个元素的数组表示地图匹配位置与道路网络节点重合。(图片来源:作者)

代码的最终步骤见图 5(第 30–32 行),将计算出的边遍历时间保存到数据库,以供后续使用。

数据质量

我们刚刚准备的数据有多好?EVED 是否能提供良好的速度预测?不幸的是,这个数据库并不是为了这个目的设计的,因此我们会看到一些问题。

第一个问题是最终数据库中单边记录的数量,在这种情况下,超过两百万条。总行数超过 560 万,因此不可用的单边记录占据了数据库的相当大比例。几乎一半的行来自于仅有十条或更少记录的边。

第二个问题是频率非常低的行程。当查询一个临时行程时,我们可能会进入低密度区域,这里边的时间记录稀缺甚至不存在。在这种情况下,预测代码会尝试使用一个简单的启发式方法来弥补数据的丢失:假设与上一个边的平均速度相同。对于较大的路段,正如我们下面看到的,甚至可能会从 Valhalla 路线预测器中复制数据。

底线是,部分预测会很差。这个算法的更好使用场景是使用来自经常通过相同路线的车队的远程信息数据库。如果能为同一路线获取更多数据,效果会更好。

预测

为了探索这个时间预测增强算法,我们将使用两个不同的脚本:一个交互式的Streamlit应用程序,允许你自由使用地图;另一个是分析脚本,尝试通过将预测的时间与已知的行程时间进行比较来评估预测质量,采用的是一种 LOOCV 类型的方法。

交互式地图

你可以通过在项目根目录下执行以下命令行来运行交互式应用程序:

streamlit run speed-predict.py

交互式应用程序允许你指定一个路线的起点和终点,供 Valhalla 进行预测。下面的图 10展示了用户界面的样子。

图 10 — 上面的图片展示了交互式应用程序的截图。请参阅下面的文本了解如何使用它。(图片来源:作者使用 Folium 和 OpenStreetMap 影像)

要操作该应用程序,使用缩放控件下方的绘制按钮选择起点、终点和可选的途经点。应用程序会将路线以蓝线的形式绘制出来,如图所示。接下来,点击左上方标题下的“预测”按钮。应用程序会调用 Valhalla 路线预测器,并将结果以红线的形式绘制出来。左侧显示了一些输出,特别是地图提供商的时间估算,以及下方的两个估算值,这两个估算值来自这里描述的算法。第一个估算使用匹配边时间分布的平均值,第二个估算使用中位数。对于缺失数据,算法会使用地图估算值。

请注意,该应用程序会在数据可用的区域周围绘制一个黑色矩形。该区域之外的所有预测将与地图上的预测相同。

准确度估算器

为了估算该时间预测器的准确性,我们将其与数据库中已知的旅行进行比较。我们将每个采样旅行的边缘投影与相应的地图节点匹配,然后查询它们的持续时间分布,排除我们希望估算的旅行的数据。这是一个类似于已知的“留一交叉验证”(Leave-One-Out Cross Validation)过程。

使用以下命令行来调用准确度估算器脚本:

python benchmark-trips.py

完成后,脚本将估算结果输出到 CSV 文件中,你可以用它进行准确性分析,如下图图 11所示。

图 11 — 输出的 CSV 文件包含四列估算的总旅行时间:最小值、平均值、中位数和最大值。上面显示的样本展示了前 20 个轨迹的验证。最右边的四列是在电子表格中直接计算的,包含准确性和数据质量度量。(图片来源:作者)

输出的 CSV 文件包含计算得出的旅行时间估算值,包括最小值、平均值、中位数和最大值。这些统计数据需要解释。该算法使用每个边缘时间分布的平均值来计算平均时间。它使用相同的过程来计算中位数。最小值和最大值来自二倍标准差范围的限制,确保最小值永远不会低于零。最大值为𝜇+2𝜎,最小值为𝜇-2𝜎,前提是该值为正数。如果为负数,算法将其替换为时间范围的最小值。

在上面的图像中,使用平均值计算的预测列是绿色的,而实际时间的列是蓝色的。右侧是三个帮助评估基础数据质量的列。第一列计算没有时间数据的旅行边数。你可以将其与下一列进行比较,后者包含该旅行的查询边数。该比率位于右侧的"质量"计算列中。该指标的较低值,如第 7 行和第 12 行,反映了缺乏边缘遍历时间数据。最后一列包含绝对相对误差(与平均值进行比较)。该列中的值往往会有极端值,这些值与数据质量值呈负相关,但这仅仅是一个提示。

根据此算法的本质,数据越多,预测旅行时间的准确度应该越高。

结论

在这篇文章中,我提出了一种基于数据的替代方法,通过使用遥感数据库和地图匹配技术,改进数字地图服务提供商的旅行时间预测。这种解决方案背后的原理是,利用来自我们要建模的车辆的遥感数据库数据及其现有的时间分布,来预测计划的行程。

这个数据密集型过程需要收集多个样本的旅行时间,这些样本涵盖数字地图中最关键的元素:各节点之间的边缘。我们通过使用 H3 索引来简化搜索过程,从而实现简单的索引技术。

预测的质量将很大程度上依赖于数据是否适合此过程。我们预期,来自有可重复和稳定路线的车队的遥感数据库会产生最佳结果。

未来工作

这种预测方法的改进之一是过去遥感数据的可用性。如果你运营一支经常经过重复路线的车队,数据将是充足的,但如果你的路线更加多样化,这可能会成为一个问题。

一个可能的研究方向是寻找一种方法,将已知的时间分布外推到缺少或数据不足的其他道路网络边缘。我们能否在同一道路网络内推导出边缘或边缘组之间的相似性度量?如果可以,并且在数据缺失的情况下,我们可以利用网络中其他相似边缘的时间分布来计算时间预测。

另一个值得关注的领域是时间分布的性质。本文假设没有其他因素会影响时间分布,而这很可能是不真实的。我们都知道,交通时间在一天和一周内是有变化的(仅举两个周期为例)。通过研究并将这些行为纳入预测,我们应当能够取得更好的结果。

最终,为未来的发展增加一个条件设置,用于调整基于诸如车辆类型、驾驶员档案或其他相关参数的边缘时间分布,将非常有价值。通过条件设置和丰富的数据,算法可以更好地预测相关条件下的旅行时间。

许可信息

扩展车辆能量数据集采用 Apache 2.0 许可证,与其来源,车辆能量数据集相同。

参考资料

joaofig/eved-explore: 扩展车辆能量数据集的探索(github.com)

车辆能量数据集

扩展车辆能量数据集(eVED):一个用于深度学习的增强大规模车辆行程能量消耗数据集

H3 | H3 (h3geo.org)

João Paulo Figueira 是tb.lx by Daimler Truck葡萄牙里斯本的数据科学家。

绘制奥斯卡获奖者的连接关系

原文:towardsdatascience.com/mapping-out-the-connections-of-oscar-winners-2690a571fe7e?source=collection_archive---------3-----------------------#2024-02-25

Milan JanosovTowards Data Science Milan Janosov

·发表于 Towards Data Science ·阅读时间 6 分钟·2024 年 2 月 25 日

--

在这篇简短的文章中,我使用了公共的维基百科数据、Python 编程和网络分析技术来提取并绘制奥斯卡获奖演员和女演员的网络。

所有图片均由作者创建。

1. 维基百科作为数据源

维基百科作为全球最大、最自由的众包在线百科全书,作为一个极为丰富的数据源,涵盖了各个公共领域。许多领域,从电影到政治,背后都涉及到不同层次的网络,展现了不同类型的社会现象,例如合作。由于奥斯卡颁奖典礼即将举行,在这里我以奥斯卡获奖演员和女演员为例,展示我们如何利用简单的 Python 方法将维基网站转化为网络。

2. 收集获奖者名单

首先,让我们来看一下,例如,奥斯卡获奖男演员列表是如何构建的:

奥斯卡获奖男演员列表

这个子页面展示了所有曾经获得奥斯卡奖并且有维基个人档案的人(粉丝们大概没有漏掉任何一位演员或女演员)。在这篇文章中,我…

绘制宝可梦世界:基于栖息地的遭遇网络分析

原文:towardsdatascience.com/mapping-the-pok%C3%A9mon-world-a-network-analysis-of-habitat-based-encounters-9b8e5fe4db0a?source=collection_archive---------4-----------------------#2024-06-14

本文将介绍在 Python 中进行网络分析,并通过使用来自 PokeApi 的宝可梦数据进行实际示例

Jacob IngleTowards Data Science Jacob Ingle

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

--

图片由Michael Rivera提供,来源于Unsplash

介绍

网络分析是一种强大而简便的方法,用于有意义地理解各种实体之间的关系、联系和分组。从社交媒体的角度来看,人们可以通过网络分析了解哪些个人资料有最有意义的联系;电子商务公司可以利用其网站分析来理解产品之间的浏览关系;公司可以分析电子邮件通信,以了解哪些团队经常一起工作,等等!只要你的数据具有某种网络特征,网络分析无疑可以应用。

本文将介绍通过 Python 进行网络分析,您可以从网络分析中获取的实际见解,以及一个与热门 TV 和游戏系列《宝可梦》相关的有趣项目!

什么是网络分析

图片由 DALL-E 生成

市场篮分析:完整项目

原文:towardsdatascience.com/market-basket-analysis-the-complete-guide-f672ed52c619?source=collection_archive---------5-----------------------#2024-12-02

🛒✨ 如何揭示隐藏的客户行为模式。

Leo Anello 💡Towards Data Science Leo Anello 💡

·发表于 Towards Data Science ·58 分钟阅读·2024 年 12 月 2 日

--

图片由 Leo Anello 创建。

1. 概述

让我向你介绍一个关于 关联规则市场篮分析(MBA) 的项目。这是一种广泛应用于 零售行业 的数据分析技术。

如果你从事或计划从事零售行业,迟早你会参与到利用关联规则和市场篮分析(MBA)的项目中。

现在,让我告诉你——这个项目值得你们每个人至少鼓掌 50 次。如果你觉得懒得鼓掌,就按住鼓掌按钮,为什么不留下评论呢?!

[## GitHub - Anello92/Market_Basket_Analysis

通过在 GitHub 上创建账户,参与 Anello92/Market_Basket_Analysis 项目的开发。

github.com](https://github.com/Anello92/Market_Basket_Analysis/tree/main?source=post_page-----f672ed52c619--------------------------------)

这不会浪费你的时间,也不会消耗你的精力——我已经将我的所有精力都倾注到这里了。👏👏

2. 🍺 + 尿布:一个奇特的关联 🛒

为了说明什么是关联规则和市场篮分析,我想分享几年前在美国发生的一个有趣事件。

使用高效用项集挖掘的市场篮子分析

原文:towardsdatascience.com/market-basket-analysis-using-high-utility-itemset-mining-df233b297c0d?source=collection_archive---------5-----------------------#2024-09-11

在交易中寻找高价值模式

Laurin BrechterTowards Data Science Laurin Brechter

·发表于 Towards Data Science ·9 分钟阅读·2024 年 9 月 11 日

--

在这篇文章中,我将提供一种替代市场篮子分析中的流行技术的方法,这可以帮助从业者发现高价值的模式,而不仅仅是最频繁的模式。我们将对不同的模式挖掘问题有一些直观的了解,并且会查看一个真实世界的例子。完整的代码可以在 这里* 找到。所有图像均由作者创建。*

介绍

我已经写了一篇更为入门的文章关于模式挖掘;如果你不熟悉文中提到的某些概念,可以先去查看那篇文章。

简而言之,模式挖掘旨在从数据中寻找模式(显然)。大多数时候,这些数据呈现为(多重)集合或序列。例如,在我上一篇文章中,我分析了用户在网站上的操作序列。在这种情况下,我们关心物品的排序。

在其他情况下,例如我们下面将讨论的场景,我们不关心物品的排序。我们只列出交易中所有的物品及其出现的频率。

示例交易数据库

例如,交易 1 包含了🥪 3 次和🍎 1 次。正如我们所见,我们失去了关于物品排序的信息,但在许多场景中(如我们将讨论的场景),物品的排序并不重要。这类似于自然语言处理中的“词袋模型”。

市场篮子分析

市场篮分析(MBA)是一种常用于零售和市场营销的数据分析技术,用于揭示客户倾向于一起购买的商品之间的关系。它通过分析客户的购买行为,旨在识别客户购物篮或事务中的模式。核心思想是了解商品在购物事务中的共现情况,这有助于企业优化产品摆放、交叉销售和定向营销策略。

频繁项集挖掘

频繁项集挖掘(FIM)是从事务数据库中发现频繁模式的过程。我们可以通过计算模式的支持度来查看模式的频率。换句话说,模式 X 的支持度是包含 X(并且在数据库 D 中)的事务 T 的数量。也就是说,我们只是在查看模式 X 在数据库中出现的频率。

支持度的定义。

在频繁项集挖掘(FIM)中,我们接下来要找到所有支持度大于某个阈值的序列(通常称为minsup)。如果一个序列的支持度高于 minsup,它就被认为是频繁的。

局限性

在 FIM 中,我们只看一个项是否出现在序列中。也就是说,无论一个项出现两次还是 200 次都无关紧要,我们只表示它为 1。但我们经常会遇到一些情况(例如 MBA),在这些情况下,不仅项在事务中是否存在是相关的,项在事务中出现的次数也是重要的。

另一个问题是,频率并不总是意味着相关性。从这个角度看,FIM 假设事务中的所有项是同等重要的。然而,可以合理地假设,购买鱼子酱的人对商业的意义可能大于购买面包的人,因为鱼子酱可能是一个高回报/利润的商品。

高效用项集挖掘

这些局限性直接引导我们进入高效用项集挖掘(HUIM)和高效用定量项集挖掘(HUQIM),它们是 FIM 的推广,旨在解决普通 FIM 的一些问题。

我们的第一个推广是,项可以在一个事务中出现多次(即我们有一个多重集,而不是简单的集合)。如前所述,在普通的项集挖掘中,我们将事务转化为一个集合,只关注项是否存在于事务中。所以例如,下面的两个事务将有相同的表示。

t1 = [a,a,a,a,a,b] # repr. as {a,b} in FIM
t2 = [a,b] # repr. as {a,b} in FIM

上面这两笔交易在常规频繁项集(FIM)中会被表示为[a,b]。我们很快就能发现,在某些情况下,我们可能会错过重要的细节。例如,如果 a 和 b 是客户购物车中的某些商品,那么是否有a(例如一条面包)出现五次或仅出现一次就非常重要。因此,我们将交易表示为一个多重集合,在其中记录每个商品出现的次数。

# multiset representation
t1_ms = {(a,5),(b,1)}
t2_ms = {(a,1),(b,1)}

如果商品可能出现在大量商品中(例如 100 次或 1000 次),这种方法也是高效的。在这种情况下,我们不需要记录所有的 a 或 b,而只需记录它们出现的频率。

定量和非定量方法的一个推广是为交易中的每个商品分配一个效用(例如利润或时间)。下面是一个为每个可能商品分配单位利润的表格。

商品效用

然后我们可以通过计算包含特定模式(如{🥪, 🍎})的交易中这些商品的效用总和来计算该模式的效用。在我们的示例中,效用为:

(3🥪 * $1 + 1🍎 * $2) +

(1 🥪 * $1 + 2🍎 * $2) = $10

上面的交易数据库

因此,我们得出这个模式的效用为$10。对于 FIM,我们的任务是寻找频繁模式。现在,我们必须找到高效用的模式。这主要是因为我们假设频率并不意味着重要性。在常规 FIM 中,我们可能会错过一些稀有(不频繁的)模式,这些模式提供了高效用(例如钻石),而 HUIM 则不会出现这种情况。

我们还需要定义“交易效用”的概念。它只是交易中所有商品效用的总和。对于数据库中的交易 3,这将是:

1🥪 * $1 + 2🦞$10 + 2🍎$2 = $25

请注意,解决这个问题并找到所有高效用商品比常规频繁模式挖掘(FPM)要困难。这是因为效用不遵循 Apriori 性质。

Apriori 性质

设 X 和 Y 是两个在交易数据库 D 中出现的模式。Apriori 性质表示,如果 X 是 Y 的子集,则 X 的支持度必须至少与 Y 的支持度一样大。

Apriori 性质

这意味着,如果 Y 的一个子集是稀有的,那么 Y 本身必须是稀有的,因为它必须有更小的支持度。假设我们有 X = {a}和 Y = {a,b}。如果 Y 在我们的数据库中出现了 4 次,那么 X 必须至少出现 4 次,因为 X 是 Y 的一个子集。这是有道理的,因为我们通过添加一个商品使得模式变得不那么通用/更加具体,这意味着它将适用于更少的交易。这个性质在大多数算法中都有应用,因为它意味着如果{a}是稀有的,所有超集也是稀有的,我们可以将它们从搜索空间中排除[3]。

这个特性在谈到效用时并不成立。交易 X 的超集 Y 可能具有更多或更少的效用。如果我们以上面的例子为例,{🥪}的效用是 4 美元,但这并不意味着我们不能查看这个模式的超集。例如,我们查看的超集{🥪, 🍎}的效用为 10 美元。同时,一个模式的超集并不总是具有更高的效用,因为它可能只是很少出现在数据库中。

HUIM 的理念

由于我们不能直接使用 HUIM 的 Apriori 性质,我们必须提出一些其他的上界来缩小搜索空间。一个这样的上界叫做交易加权利用率(TWU)。计算方法是将包含感兴趣模式 X 的交易的交易效用求和。任何 X 的超集 Y 的效用都不会超过 TWU。通过一个例子可以更清楚地说明这一点。{🥪,🍎}的 TWU 是 30 美元(交易 1 贡献了 5 美元,交易 3 贡献了 5 美元)。当我们查看一个超集模式 Y,如{🥪 🦞 🍎}时,我们可以看到,它不可能有更高的效用,因为所有包含 Y 的交易也包含 X。

目前,解决 HUIM 的问题已经有了多种算法。所有这些算法都会接收一个最小效用值,并生成至少具有该效用的模式作为输出。在此情况下,我使用了EFIM算法,因为它快速且内存高效。

实现

对于本文,我将使用来自 Kaggle 的市场篮子分析数据集(经原数据集作者许可使用)。

上面,我们可以看到数据中交易值的分布情况。数据总共有大约 19,500 笔交易,平均交易值为 526 美元,每笔交易包含 26 个不同的商品。总共有大约 4,000 个唯一商品。我们还可以进行ABC 分析,根据商品在总收入中的占比将商品分入不同的桶。我们可以看到,大约 500 个商品(A 类商品)贡献了约 70%的收入。而剩下的大约 2,250 个商品(C 类商品)只贡献了约 5%的收入。

预处理

初始数据是长格式,每一行表示账单中的一项商品。通过 BillNo,我们可以看出该商品属于哪个交易。

初始数据格式

在一些预处理之后,我们将数据转换成PAMI所需的格式,这是我们将用来应用 EFIM 算法的 Python 库。

data['item_id'] = pd.factorize(data.Itemname)[0].astype(str) # map item names to id
data["Value_Int"] = data["Value"].astype(int).astype(str)
data = data.loc[data.Value_Int != '0'] # exclude items w/o utility

transaction_db = data.groupby('BillNo').agg(
    items=('item_id', lambda x: ' '.join(list(x))),
    total_value=('Value', lambda x: int(x.sum())),
    values=('Value_Int', lambda x: ' '.join(list(x))),
)

# filter out long transactions, only use subset of transactions
transaction_db = transaction_db.loc[transaction_db.num_items < 10].iloc[:1000]

交易数据库

然后,我们可以应用EFIM算法。

import PAMI.highUtilityPattern.basic.EFIM as efim 

obj = efim.EFIM('tdb.csv', minUtil=1000, sep=' ') 
obj.startMine()                       #start the mining process 
obj.save('out.txt')               #store the patterns in file 
results = obj.getPatternsAsDataFrame()     #Get the patterns discovered into a dataframe 
obj.printResults() 

然后,算法返回一组符合此最小效用标准的模式列表。

这是一个需要领域专家仔细查看这些模式的时刻,以确定是否有值得进一步关注的内容。例如,我们可以看到“VINTAGE BEAD PINK PURSE”和“ANT COPPER RED BOUDICCA BRACELET”的组合似乎表现得相当不错。在这种情况下,我们可以考虑将这些商品打包销售,或将它们推荐一起购买。

结论

在这篇博客文章中,我们探讨了经典的 FIM 及其局限性。特别是当我们关心一个模式的交易价值以及它可能带来的潜在收入时,使用 HUIM 并挖掘具有高效用的模式是非常有益的。

资源与进一步阅读

营销组合模型(MMM):如何避免偏差的渠道估算

原文:towardsdatascience.com/marketing-mix-modeling-mmm-how-to-avoid-biased-channel-estimates-f9201fc3e91d?source=collection_archive---------2-----------------------#2024-10-16

了解在模型中应考虑和不应考虑的变量

Felix GermaineTowards Data Science Felix Germaine

·发布于Towards Data Science ·14 分钟阅读·2024 年 10 月 16 日

--

图片由Fredrick Suwandi拍摄,来源于Unsplash

“每个营销渠道的 X 美元投资将如何影响销售?” 这是营销组合模型应该回答的因果问题,以帮助公司决定未来如何分配营销渠道的预算。正如我们将看到的,这个问题的答案在很大程度上取决于你考虑哪些变量:遗漏重要变量或在模型中包含“错误”变量会引入偏差,并导致错误的因果估算。这是一个巨大的问题,因为错误的因果估算最终会转化为错误的营销决策和财务损失。在本文中,我将讨论这个问题,并提供如何确定在你的 MMM 中应该考虑和不应该考虑的变量的指导,结构如下:

  • 在 1 中,我们将探讨为什么在营销组合模型(Marketing-Mix-Models)中,变量选择如此关键。通过一个模拟示例,我们将看到根据你考虑的变量集,渠道估算值是如何变化的。

  • 在 2 中,我们将深入探讨潜在的偏差来源。你将了解应该考虑哪些变量,哪些变量绝对应该考虑。该章节基于尤迪亚·珀尔(Judea Pearl)关于因果推断领域的经典理论[1][2],以及马修斯·法库雷(Matheus Facure)非常有洞察力的网站[3]。

  • 在第 3 点中,我们将这些学习应用于使用模拟数据的例子。

1. 关于在 MMM 中选择变量的重要性

让我们通过一个简单的例子来展示在 MMM(市场营销混合模型)中变量选择的重要性。为了保持简单,并专注于实际的变量选择问题,我们将继续使用简单的线性回归。请记住,变量选择问题在使用更复杂的 MMM 时依然同样重要(例如,具有饱和效应和延续效应的贝叶斯模型)。

假设你在一家在线体育用品店的营销部门工作,并且你的部门已经通过电视、YouTube 和 Instagram 广告宣传了平台 3 年。现在是时候评估每个营销渠道对销售的贡献了。你从每周的数据开始,收集了关于营销渠道支出和公司销售的数据,结果如下所示:

销售与营销支出的时间变化

对于 MMM 最简化的方法,最基本的做法是通过对营销渠道进行线性回归来拟合销售数据:

然而,你知道还有许多其他变量可能对销售产生影响,你想知道是否应该将它们包含在模型中。这些变量包括:

  • 季节性变量,因为你知道销售有自然的季节性模式

  • 足球世界杯指示变量,因为你知道在大型体育赛事期间,销售额会增加

  • 价格,因为你假设销售额与价格变化密切相关

  • 网站访问量,因为你知道当网站访问量增加时,销售额会增加

由于你有上述数据/变量,你决定拟合 5 个不同的线性回归模型,考虑到 5 组不同的变量:

最终得到的渠道估算如下所示:

如你所见,不同渠道的估算值非常依赖于你所考虑的变量集合。这意味着,如果你想基于模型做出营销决策,结果将根据你选择的变量集而有所不同。例如:

如果你想知道是否应该增加电视广告的投入呢? 根据模型 1,1 美元的电视投资大约能带来 3 美元的销售额,所以你应该增加电视广告投入。相反,根据模型 5,产生的销售额甚至无法覆盖广告费用(1 美元支出仅能带来不到 0.5 美元的销售额),所以你应该减少电视广告支出。

如果你想知道哪个渠道对销售的影响最大,以便可以投入更多的资源呢? 根据模型 1,你最有效的渠道是电视;根据模型 2、3、4,它是 YouTube;根据模型 5,它是 Instagram。

结论 — 如果你没有仔细选择 MMM 中的变量,你不如通过掷骰子来做营销决策。但不用担心!得益于因果推断理论,我们可以通过一种方法来帮助你确定应该考虑哪些变量,哪些不应该!在本文的其余部分,我将解释这一方法,最终让你能够了解哪一组(如果有的话)变量能导致准确的因果估计。

剧透: “选择那些能够最准确预测销售的变量”是一个好方法吗? 不! 记住,我们最终并不是在预测销售,而是想要确定营销渠道对销售的因果影响。这是两件非常不同的事情!正如你将看到的,有些非常好的销售预测变量,可能会导致对营销渠道对销售的因果效应的偏倚估计。

2. 偏倚来源

来源 1:忽略混杂变量

为了实现估计的无偏性,你应该认真思考哪些变量是所谓的混杂变量。这些是你绝对需要在模型中考虑的变量,否则你将得到有偏的估计。让我们来看看为什么!

什么是混杂变量?

混杂变量是对公司销售和一个或多个营销渠道都产生因果影响的变量。例如,在我们在线体育商店的例子中,变量“足球世界杯”就是一个混杂变量。事实上,由于世界杯的原因,公司在电视广告上的投入增加,而世界杯的举办导致了足球球衣销量的增加。因此,形成了如下的因果关系:

为什么我们需要考虑混杂变量?

如果我们没有考虑这种混杂变量的问题,那么我们的 MMM 会将电视广告的效果与世界杯的效果混淆。实际上,随着世界杯的到来,电视广告支出和销售额都会上升,表面上看起来是世界杯带来的额外销售由额外的电视广告带来,实际上它们主要是由世界杯本身引起的。这导致了电视广告对销售的偏倚估计。但幸运的是,如果我们的模型考虑了“世界杯”这个混杂变量,这种偏倚就会消失。示意图如下:

左图:电视广告对销售的回归 | 右图:电视广告和世界杯对销售的回归

左侧模型没有考虑世界杯的影响,我们可以看到电视销售的估计效应非常大(beta_1 很大)。这是因为线性模型将电视的因果效应与世界杯的影响混淆,导致了偏差。右侧则正确地将世界杯期间的额外销售归因于世界杯本身(beta_2 很大,beta_1 较小),因此电视的估计效应显著减小。

如何在 MMM 中识别混杂变量?

为了识别所有的混杂因素,你需要了解所有对你的营销渠道和公司销售都有因果影响的因素。这里的巨大难点在于,因果关系的概念非常理论化,且仅依赖于假设!因此,仅通过查看数据无法知道哪些变量具有因果影响。你需要从概念上思考,哪些变量可能会影响销售和你的营销渠道支出。虽然列出所有可能对销售产生因果影响的因素几乎是不可能的,因为这些因素非常多样(例如通货膨胀、经济状况、竞争……),但识别那些影响你渠道支出的因素应该要容易得多,因为这些决策/过程发生在公司内部,可以通过与相关人员交流来调查!最终,如果你能识别出那些既影响渠道支出又影响销售的因素,那就好了!

混杂因素在 MMM 中的例子

  • 季节性: 在大多数应用场景中,销售和营销预算都会受到一年四季季节变化的很大影响(例如,圣诞节期间销售和广告都会达到高峰)。在这种情况下,季节性就是一个混杂因素。

  • 折扣: 如果贵公司推出了一项折扣活动,导致在营销渠道上增加了额外的广告投入,这也是一个混杂因素。事实上,在这种情况下,折扣同时影响了渠道预算和销售。

  • 市场竞争: 如果贵公司通过在营销渠道上投入更多的资金来应对竞争对手的广告攻势,这就是一个混杂因素。实际上,竞争对手的营销活动对你的销售有着(负向)因果影响,同时也促使贵公司在自身的营销渠道上投入更多。

  • 新产品推广: 假设贵公司推出了一款革命性的新品,人人都想购买,同时决定增加在营销渠道上的投入来推广这款新产品。这同样是一个混杂因素,因为新产品本身将会影响销售,同时也影响营销渠道的预算。

如你现在可能已经意识到的,这个列表可能会变得非常长,而且与公司的具体情况和使用场景密切相关。没有一种通用的方法可以让你识别所有混杂因素。你需要成为侦探,在你的特定场景中通过理解营销预算的归因来警惕它们。

如果存在一个你无法测量的混杂变量怎么办?

在某些情况下,可能会存在一些你没有数据或者根本无法衡量的混杂变量。如果这些是强混杂因素,你的估计结果也会有很大的偏差,你可能需要考虑完全放弃 MMM 项目。有时候,与其盲目相信错误的估计,不如根本不做估计。

我们现在已经看到,当我们没有考虑或无法考虑混杂变量时,会发生什么问题。接下来,让我们看看当我们考虑了错误的变量时,会出现什么问题。

来源 2:包含中介变量

我们常常倾向于认为“只要再控制一个变量,就不会出错”。但正如我们稍后将看到的,这个说法是错误的。事实上,如果你控制了所谓的中介变量,你的营销渠道的因果估计将会出现偏差!

什么是中介变量?

在你想要衡量电视广告对销售影响的情况下,中介变量是指电视广告间接影响销售的变量。例如,电视广告可能通过增加访问你在线商店的访客数量,间接地影响销售:

为什么考虑中介变量会造成偏差?

如果你没有考虑到中介变量“访问量”,你的模型对电视广告对销售影响的估计将同时考虑到直接效应(电视 → 销售)和间接效应(电视 → 访问 → 销售)。这正是你想要的!相反,如果你考虑了变量“访问量”,你的电视广告估计将仅仅考虑到对销售的直接效应(电视 → 销售)。间接效应(电视 → 访问 → 销售)将由你模型对增加访问量的影响的估计来捕捉。因此,你的电视广告估计没有考虑到电视通过访问量增加销售的事实,从而导致你对电视广告对销售的因果估计出现偏差!

让我们通过方程来看看!假设销售可以通过以下线性方程来描述:

如果你指定了一个考虑到电视广告和访问量的线性回归模型,你将估计电视广告对销售的直接因果效应,但间接效应则通过变量“访问量”隐性存在:

相比之下,如果你在你的线性模型中没有考虑变量“访问量”,你将正确地估计出电视广告对销售的因果效应,这个效应是电视广告直接和间接影响销售的总和:

中介变量在 MMM 中的挑战

在大多数情况下,避免在 MMM 应用案例中考虑中介变量是很容易的。对于您打算考虑的每个变量,问问自己是否您的某个营销渠道对其有因果影响。如果有,删除该变量!很简单。然而,当该中介变量实际上也是您的营销渠道之一时,就会出现问题!例如,如果您估算公司付费搜索渠道的影响,同时估算其他营销渠道(如电视)的影响,就可能发生这种情况。事实上,通过电视广告您的产品可能会促使客户在线搜索您的产品,这将增加您的付费搜索费用。因此,付费搜索渠道将成为电视销售影响的中介变量:

这个案例具有挑战性,因为无法为电视付费搜索都获得公正的估算。事实上,您只能选择以下选项:

  1. 您删除了付费搜索变量,因此您可以获得电视的公正估算。然而,您无法为您的付费搜索渠道获得任何估算。

  2. 您保留了付费搜索变量,这使您能够为付费搜索获得公正的因果估算。然而,这会导致电视的估算存在偏差。

选项 1 或 2——由您选择!

来源 3:包括碰撞变量

另一类会引入偏差的变量是所谓的碰撞变量,如果在 MMM 中考虑这些变量。

什么是碰撞变量?

TV 对销售的碰撞变量是一个同时受到电视和销售影响的变量:

MMM 中的碰撞变量示例

在 MMM 设置中,碰撞变量的一个例子是公司利润。实际上,营销渠道(例如电视)通过其成本对利润产生负面影响,而销售则对利润产生正面影响。虽然在 MMM 的背景下能够提出此类碰撞变量的例子,但很少有人会首先考虑这样的变量。因此,我不会进一步探讨为什么考虑碰撞变量会导致偏差。如果您对更多细节感兴趣,我邀请您查看 Mattheus Facure 的网站[3]

3. 模拟结果

现在我们知道如何为 MMM 选择正确的变量,让我们回到最初的例子,确定要选择哪些变量。首先,让我们展示我们示例中的数据是如何生成的。

模拟数据:

营销预算指定如下:

简而言之,三个渠道受到季节世界杯价格的因果影响,其余的变动是随机的。

网站上的销售额指定如下:

销售方程

访问量方程

简而言之,销售依赖于季节营销渠道的预算、价格世界杯和网站上的访问量。需要注意的是,访问量本身依赖于营销预算和季节。

现在我们知道了模拟数据中变量之间的因果关系,可以确定哪些变量是混杂因素中介因素碰撞因素,从而估计因果关系(→ 营销渠道对销售的因果效应)。

变量类型:

正如我们在公式中看到的,季节世界杯价格都会影响营销渠道的预算分配和销售。因此,这三个变量是混杂变量,应该在我们的 MMM 模型中加以考虑。

正如我们在公式中看到的,变量访问量是一个中介变量。事实上,营销渠道会因果地影响访问量,访问量又因果地影响销售。因此,这个变量不应被考虑在模型中。

真实因果效应:

从指定生成模拟数据的方程中,我们可以轻松地获取营销渠道的真实因果效应。

销售方程

访问量方程

一个渠道的真实因果效应包括对销售的直接效应(渠道 → 销售)和通过增加访问量的间接效应(渠道 → 访问量 → 销售)。例如,youtube 渠道增加 1 美元,直接使销售增加 1 美元(分别为 instagram 1.2 美元,TV 0.4 美元),见上文的“销售”方程。youtube 渠道增加 1 美元,使访问量增加 0.3(分别为 instagram 0.08,TV 0.1),见“访问量”方程。每次访问量增加会使销售增加 5 美元,见“销售”方程。因此,youtube 的总因果效应为 1 + 0.35 = 2.5 美元(分别为 instagram 1.2 + 0.085 = 1.6 美元,TV 0.4 + 0.1*5 = 0.9 美元)。

不同变量集合的因果效应估计:

我们现在已经掌握了真实的因果效应,可以将它们与选择不同变量集合(第一部分指定的集合)时得到的估计进行比较。

针对不同变量集合的营销渠道对销售的线性效应估计

正如我们在上面的图中所看到的,只有在考虑了所有混杂变量(→ 季节、世界杯、价格)并且没有考虑中介变量(→ 网站访问)的情况下,才能正确估算市场营销渠道对销售的真实因果效应。相反,当季节、世界杯或价格变量被忽略时,市场营销渠道的估计值可能会出现很大的偏差。例如,当忽略所有混杂因素时,我们估计电视的影响是实际影响的三倍。我们还可以观察到,考虑中介变量也会导致显著的偏差。例如,当将“访问量”变量纳入 MMM 时,我们估算的 YouTube 频道的影响不到其真实值的一半。

结论

总结来说,选择正确的变量集对于在市场营销混合建模(MMM)中获得无偏的因果估计至关重要。正如我们在示例中所见,未考虑混杂因素或包括诸如中介变量或碰撞变量等因素,可能会显著扭曲 MMM 的结果,导致错误的营销决策和潜在的财务损失。这应当强调深入思考你所建模的变量之间的因果关系的重要性。一旦这些关系被识别出来,你就能知道哪些变量需要考虑,哪些不需要,从而获得无偏的渠道估计!为了深入了解,我强烈推荐阅读附带的因果推断文献。

注意: 除非另有说明,所有图像和图表均由作者提供。

代码与数据

对于那些有兴趣更深入了解代码和数据的人,可以在这里查看:MMM_HowToAvoidBiasedChannelEstimates.ipynb

文献:

[1] J. Pearl — 《为什么的书:因果与效应的新科学》(2018)

[2] J. Pearl — 《因果性:模型、推理与推断》(2000)

[3] M. Facure — 《勇敢与真实的因果推断》matheusfacure.github.io/python-causality-handbook/landing-page.html

马尔可夫链蒙特卡洛:一劳永逸的简化解释

原文:towardsdatascience.com/markov-chain-monte-carlo-made-simple-once-and-for-all-e86e8384186c?source=collection_archive---------0-----------------------#2024-03-01

数据科学

MCMC 介绍,将其拆解为最简单的部分

Pol MarinTowards Data Science Pol Marin

·发表于 Towards Data Science ·阅读时间:7 分钟·2024 年 3 月 1 日

--

图片由 Karine Avetisyan 提供,来源于 Unsplash

我最近发布了一篇文章,使用贝叶斯推断和马尔可夫链蒙特卡洛(MCMC)来预测冠军联赛 16 强的获胜者。在那篇文章中,我试图相对深入地解释贝叶斯统计,但没有详细讲解 MCMC,以避免文章过于冗长。文章链接:

[## 使用贝叶斯建模预测冠军联赛

贝叶斯推断在现实世界中的应用

towardsdatascience.com

因此,我决定专门写一篇文章,介绍马尔可夫链蒙特卡洛方法,供那些有兴趣了解其数学原理以及何时能发挥作用的人阅读。

为了应对这篇文章,我将采用分而治之的策略:将这个术语拆解成最简单的部分,分别解释它们,然后解决整体问题。我们将讨论的内容如下:

  • 蒙特卡洛方法

  • 随机过程

  • 马尔可夫链

  • MCMC

蒙特卡洛方法

机器人学中的马尔可夫决策问题

原文:towardsdatascience.com/markov-decision-problems-in-robotics-6fea564215e4?source=collection_archive---------8-----------------------#2024-11-07

使用一个现实世界的例子来解释 MDP、贝尔曼方程和价值迭代

Nikolaus CorrellTowards Data Science Nikolaus Correll

·发表于 Towards Data Science ·10 分钟阅读·2024 年 11 月 7 日

--

马尔可夫决策问题(MDP)是机器人学和人工智能中的一个核心话题,因为它们是进入更复杂主题(如强化学习和部分可观察马尔可夫决策过程)的门户。在大多数在线资料中,MDP 通常使用“网格世界”示例来解释。如果你觉得这个例子很难看到现实世界中的应用,且正在寻找一个更经典的 MDP 表示,那么这篇文章就是为你准备的!在通过机器人决策问题来激发对 MDP 的兴趣后,我们将正式建模 MDP,介绍贝尔曼方程和价值迭代,并提供一个简单的 Python 实现。

如果你不是 Medium 的订阅用户,你可以免费阅读这篇文章 点击这里

请考虑这个展示 PR2 机器人获取三明治的视频:

机器人首先开车到冰箱旁寻找三明治,但由于失败,便乘坐电梯去 Subway 买一个。虽然听起来这是一个合乎逻辑的行为顺序,但你可能会问,为什么它首先去冰箱呢?在这里,简要思考一下每个选项的优缺点是有意义的……

Marlin:接近理想的 4 位大规模语言模型推理速度

原文:towardsdatascience.com/marlin-nearly-ideal-inference-speed-for-4-bit-large-language-models-feb0b610dd8e?source=collection_archive---------3-----------------------#2024-03-30

比使用 fp16 参数的推理速度快多达 4 倍

Benjamin MarieTowards Data Science Benjamin Marie

·发表于Towards Data Science ·阅读时间:6 分钟·2024 年 3 月 30 日

--

使用 DALL-E 生成

大规模语言模型(LLM)通常过于庞大,无法直接在消费级硬件上使用。为减少它们的大小,已经提出了多种技术来对 LLM 进行量化并降低其内存消耗。尽管最近关于 4 位量化的算法通常会发布自己的优化 CUDA 内核,但量化后的 LLM 的推理吞吐量仍远未达到理想水平。

使用 4 位模型进行推理,例如使用 INT4 数据类型,涉及到 INT4xFP16 操作,即使在现代 GPU 上也很慢,因此需要优化的 CUDA 内核。

奥地利科技与研究学院(ISTA)提出了Mixed Auto-Regressive Linear 内核(Marlin),这是一种极为优化的 INT4xFP16 矩阵乘法内核,可以提供接近理想(4 倍)的推理速度。

在本文中,我将解释 Marlin 如何实现这种加速。然后,我们将看到如何将现有的 GPTQ 模型转换为 Marlin 格式。我使用 Mistral 7B 进行演示,并通过 vLLM 检查推理速度。

Marlin:最大化 INT4 LLM 的 GPU 使用率

巨大的能源需求驱动着巨大的 GPU 赋能 AI

原文:towardsdatascience.com/massive-energy-for-massive-gpu-empowering-ai-dff59ae1da44?source=collection_archive---------2-----------------------#2024-08-18

用于人工智能模型训练和部署的巨型 GPU 需要大量能源。随着人工智能的规模扩大,优化能源效率将变得至关重要

Geo ZhangTowards Data Science Geo Zhang

·发布于Towards Data Science ·阅读时间:7 分钟·2024 年 8 月 18 日

--

图片来源:Lucas KepnerUnsplash

OpenAI 创始人 Sam Altman 做出了雄心勃勃的计算,提出人工智能未来可能需要投入 7 万亿美元用于 GPU。这个数字被行业领袖,如 Nvidia 创始人黄仁勋所拒绝,暗示着对 GPU 的大规模收购,这需要巨大的能源,几乎是银河级别的。为了给这个数字做个对比,Nvidia 目前的市值约为 3 万亿美元,不到 Altman 提议投资的一半。与美国(约 26.8 万亿美元)和中国(约 17.8 万亿美元)的 GDP 相比,这 7 万亿美元的投资确实令人震惊。

尽管如此,人工智能时代仍处于初期阶段,达到如此规模可能需要更加先进的计算结构。这引出了一个关键的基础问题:为计算单元和数据中心提供能源需要多少?

让我们从三个角度来看一些简单直接的数据,

1. 每个计算单元的能源消耗

2. 训练/操作现代模型的能源成本

3. 能源供需

每个计算单元的能源消耗

从用户的角度来看,一些视频游戏爱好者已经自建了配备高性能 GPU(如 NVIDIA GeForce RTX 4090)的个人电脑。有趣的是,这款 GPU 也能够处理小规模的深度学习任务。RTX 4090 的功率需求为 450 瓦特,推荐的总电源功率为 850 瓦特(在大多数情况下您并不需要这么高的功率,也不会在满载下运行)。如果您的任务连续运行一周,这意味着 0.85 千瓦 × 24 小时 × 7 天 = 142.8 千瓦时每周。在加利福尼亚,PG&E 对住宅用户的电价高达每千瓦时 50 美分,这意味着您每周大约会花费 70 美元的电费。此外,您还需要配备 CPU 和其他组件来与 GPU 协同工作,这将进一步增加电力消耗。这意味着整体的电费可能会更高。

现在,您的人工智能业务将加速发展。根据制造商的说法,H100 Tensor Core GPU 的最大热设计功耗(TDP)大约为 700 瓦特,具体取决于版本。这是为在全负载工作状态下冷却 GPU 所需的能量。对于这个高性能深度学习工具,通常需要一个大约 1600W 的可靠电源供应单元。如果您使用 NVIDIA DGX 平台进行深度学习任务,一台配备 8 个 H100 GPU 的 DGX H100 系统大约消耗 10.2 千瓦特。为了获得更大的性能,NVIDIA DGX SuperPOD 可以包括从 24 到 128 个 NVIDIA DGX 节点。以 64 个节点为例,系统的功耗保守估计约为 652.8 千瓦特。如果您的初创公司计划购买这种数百万美元的设备,那么集群和必要设施的成本将是相当可观的。在大多数情况下,从云计算提供商租用 GPU 集群更为合理。关注能源成本时,商业和工业用户通常能享受较低的电价。如果您的平均电费大约是每千瓦时 20 美分,那么在 24 小时、每周 7 天全天候运行 64 个 DGX 节点,消耗 652.8 千瓦特,将会产生每周 109.7 MWh 的电力消耗,预计每周的电费大约为 21,934 美元。

根据粗略估算,典型的加利福尼亚家庭每周大约消耗 150 千瓦时的电力。有趣的是,如果您在家中使用高性能 GPU(如 RTX 4090)进行模型训练任务,所产生的电力成本大致相同。

能源成本比较

从此表格中,我们可以看到,运行 64 个节点的 SuperPOD 在一周内的能耗相当于一个小型社区的电力消耗。

训练/运行 AI 模型的能源成本

训练 AI 模型

现在,让我们深入了解一些与现代 AI 模型相关的数字。OpenAI 从未披露过训练 ChatGPT 所使用的 GPU 的具体数量,但粗略估计,这可能涉及成千上万个 GPU 连续运行数周到数月,具体取决于每个 ChatGPT 模型的发布日期。这样的任务的能源消耗轻松达到兆瓦级别,导致能源消耗的成本在数千 MWh 的规模。

最近,Meta 发布了 LLaMA 3.1,并称其为“迄今为止最强大的模型”。根据 Meta 的说法,这是他们最大的一款模型,训练使用了超过 16,000 个 H100 GPU——这是首个在此规模下训练的 LLaMA 模型。

让我们来拆解这些数字:LLaMA 2 于 2023 年 7 月发布,因此可以合理推测,LLaMA 3 至少训练了一年。虽然不太可能所有 GPU 都在全天候 24 小时运行,但我们可以估算出以 50%的利用率的能源消耗:

1.6 kW × 16,000 个 GPU × 24 小时/天 × 365 天/年 × 50% ≈ 112,128 MWh

以每千瓦时$0.20 的估算成本,这大约意味着2240 万美元的能源费用。这个数字仅包括 GPU 的费用,未包括与数据存储、网络和其他基础设施相关的额外能源消耗。

训练现代大型语言模型(LLMs)需要兆瓦级别的电力消耗,并代表着百万美元的投资。这也是为什么现代 AI 开发往往排除了较小的参与者。

运行 AI 模型

运行 AI 模型也会带来显著的能源成本,因为每一次查询和回应都需要计算能力。尽管每次交互的能源成本相较于训练模型而言较小,但累积的影响可能是巨大的,特别是当你的 AI 业务在有数十亿用户每天与先进的语言模型互动的情况下,规模化成功。许多富有洞察力的文章讨论了这个问题,包括公司之间运营 ChatBot 的能源成本比较。结论是,由于每次查询的能源消耗可能在 0.002 至 0.004 千瓦时之间,目前流行的公司每年将消耗数百到数千 MWh 的电力。而且这个数字仍在增长。

照片由Solen Feyissa提供,来自Unsplash

想象一下,如果有 10 亿人经常使用一个 ChatBot,每人每天平均进行 100 次查询。可以按如下方式估算这类使用的能源成本:

0.002 kWh × 100 次查询/天 × 1e9 人 × 365 天/年 ≈ 7.3e7 MWh/年

这将需要一个 8000 MW 的电力供应,并可能导致每年大约 146 亿美元的能源费用,假设电价为每千瓦时$0.20。

能源供应与需求

摄影:来自Matthew Henry的照片,来自Unsplash

美国最大的发电厂是位于华盛顿州的大库利水坝,其容量为 6809 兆瓦。美国最大的太阳能电站是位于加利福尼亚的太阳星,容量为 579 兆瓦。在这种背景下,单一的发电厂无法提供大型人工智能服务所需的全部电力。考虑到EIA(能源信息管理局)提供的年度电力生产统计数据,这一点变得尤为明显,

来源: 美国能源信息管理局,2021 年年度能源展望(AEO2021)

上述计算的 730 亿千瓦时大约占美国每年总电力生产的 1.8%。然而,合理的猜测是这个数字可能远高于此。根据一些媒体报道,当考虑到所有与人工智能和数据处理相关的能源消耗时,影响可能接近美国总电力生产的 4%。

然而,这只是目前的能源使用情况。

目前,聊天机器人主要生成基于文本的响应,但它们越来越能够生成二维图像、“三维”视频和其他形式的媒体。下一代人工智能将远远超出简单的聊天机器人,可能会为球形屏幕(例如拉斯维加斯球体)提供高分辨率图像、三维建模,并能执行复杂任务和深度物流的互动机器人。因此,模型训练和部署的能源需求预计将大幅增加,远超当前水平。我们现有的电力基础设施是否能够支持这些进步仍然是一个悬而未决的问题。

在可持续性方面,能源需求较高的行业碳排放量显著。减缓这一影响的一种方法是使用可再生能源为高能耗设施(如数据中心和计算中心)提供电力。一个显著的例子是Fervo Energy 和谷歌之间的合作,利用地热能为数据中心提供能源。然而,与即将到来的人工智能时代的整体能源需求相比,这些举措的规模仍然相对较小。要解决这一背景下的可持续性挑战,仍然需要做很多工作。

图片由Ben White提供,来源于Unsplash

如果你发现任何数字不合理,请进行修正。

在开始使用 AI 代理之前掌握机器人:使用 Python 创建 Mastodon 机器人简单步骤

原文:towardsdatascience.com/master-bots-before-starting-with-ai-agents-simple-steps-to-create-a-mastodon-bot-with-python-cce4f9ed24ee?source=collection_archive---------2-----------------------#2024-12-27

Sarah LeaTowards Data Science Sarah Lea

·发布于 Towards Data Science ·阅读时间 11 分钟·2024 年 12 月 27 日

--

我最近在 Mastodon 上发布了一条帖子,在两分钟内被其他六个账户分享。出于好奇,我访问了这些账户的资料,发现至少其中一个是技术机器人——这些账户会根据标签(例如 #datascience 或 #opensource)自动分享帖子。

Mastodon 目前正在快速增长,作为 X(前身为 Twitter)的去中心化替代品。像这样的平台上的机器人如何使我们的日常生活更轻松?它们又有哪些风险?机器人是丰富还是破坏了社交网络?我该如何使用 Mastodon API 来自己创建一个机器人?

在本文中,我不仅会向你展示机器人如何工作,还会提供一个逐步的指南,带有代码示例和截图,教你如何使用 Python 创建一个 Mastodon 机器人并使用其 API。

目录

1 — 为什么 Mastodon 和技术机器人存在?

2 — 社交网络上的机器人技术基础

3 — 机器人:效益与风险之间的平衡

4 — 如何创建一个 Mastodon 机器人:逐步 Python 指南

最后思考

1 — 为什么 Mastodon 和技术机器人存在?

Mastodon 是由 Eugen Rochko 于 2016 年在德国开发的去中心化社交网络。该平台是开源的,基于一个服务器网络,多个服务器共同组成了所谓的“联邦宇宙”(Fediverse)。如果你想分享帖子,可以选择一个服务器,例如 mastodon.social 或…

6 分钟掌握分布图

原文:towardsdatascience.com/master-dispersion-plots-in-6-minutes-241d15f22702?source=collection_archive---------5-----------------------#2024-03-03

快速成功数据科学

使用 NLTK 学习图形化文本分析

Lee VaughanTowards Data Science Lee Vaughan

·发表于 Towards Data Science ·6 分钟阅读·2024 年 3 月 3 日

--

福尔摩斯(由 DALL-E3 创作)

自然语言工具包(NLTK)附带一个有趣的功能,叫做分布图,它可以让你展示一个单词在文本中的位置。更具体来说,它绘制了一个单词的出现次数与从语料库开始计算的词数之间的关系。

这是《福尔摩斯探案集》中的主要角色的分布图示例,来自小说《巴斯克维尔的猎犬》:

《巴斯克维尔的猎犬》中的主要角色的分布图(按作者)

垂直的蓝色勾号表示文本中目标单词的位置。每一行覆盖整个语料库,从头到尾。

如果你熟悉《巴斯克维尔的猎犬》——如果你还没看过,我不会剧透——你就会欣赏到霍尔姆斯在中间部分的稀疏出现、莫提默的迟到登场,以及巴里莫尔、塞尔登和猎犬的重叠。

分布图还有更实际的应用。例如,假设你是一个数据科学家,正在与律师助理一起处理一起涉及内幕交易的刑事案件。你需要找出被告是否在案发前与董事会成员联系过……

精通机器学习:4 个分类模型简单易懂

原文:towardsdatascience.com/master-machine-learning-4-classification-models-made-simple-7a924677113e?source=collection_archive---------0-----------------------#2024-12-14

初学者的机器学习模型构建指南:15 个实用步骤

Leo Anello 💡Towards Data Science Leo Anello 💡

·发表于Towards Data Science ·56 分钟阅读·2024 年 12 月 14 日

--

概览

我将展示一个机器学习模型构建过程的模板

在本项目中,目标是创建一个能够预测工业机器设备维护需求的模型。我们将使用来自物联网传感器的数据(Internet of Things)。

[## GitHub - 机器学习模板

通过在 GitHub 上创建账户,您可以为 Anello92/machine-learning-template 的开发做出贡献。

github.com

该方法将机器学习项目分为15 个独立的阶段,我将在下文中为您概述这些阶段。这些阶段包括关键技术主要策略,以及如何有效地应对每个阶段。

作为示范,我们将在这个例子中使用虚拟数据

随着项目的推进,我们将从头到尾构建一个完整的项目,涵盖从问题定义部署功能模型的全过程。

项目中使用的工具

掌握这项数据科学技能,你就能进入大科技公司工作——第一部分

原文:towardsdatascience.com/master-this-data-science-skill-and-you-will-land-a-job-in-big-tech-part-i-2507003dfb30?source=collection_archive---------0-----------------------#2024-07-07

我从与科技行业顶尖人士合作中学到的实验设计基础

Khouloud El AlamiTowards Data Science Khouloud El Alami

·发布于Towards Data Science ·11 分钟阅读·2024 年 7 月 7 日

--

你是一个渴望进入大科技公司工作的数据科学家,但不知道需要掌握哪些技能才能成功?

嗯,我有一个秘密武器,可能正是你进入顶尖科技公司梦寐以求的工作所需要的。

图片由Rajeshwar Bachu提供,来源于Unsplash

几个月前,我写了这篇文章,讲述了你需要掌握的所有必备技能,以便被顶尖科技公司录用,今天,我们将重点讨论其中一项关键技能:实验设计

实验设计是一种统计方法,帮助我们隔离评估产品变更的影响——包括推出新功能、用户体验更新等!

那么,为什么实验设计在脱颖而出成为其他数据科学家中的佼佼者时如此重要呢?

很简单。最大的科技公司都致力于创造优秀的产品,而实验设计是实现这一目标的重要工具。

如果你能成为实验设计方面的专家,你将比其他候选人具有显著的优势,因为大多数求职者忽视了这一技能,并且不知道如何发展它。

精通 AI 部门重组:来自前线的经验教训

原文:towardsdatascience.com/mastering-ai-department-reorganizations-lessons-from-the-trenches-16a7459d4a7e?source=collection_archive---------7-----------------------#2024-06-12

数据科学部门重组五年的经验教训:应该做和不应该做的事

Elad CohenTowards Data Science Elad Cohen

·发表于 Towards Data Science ·阅读时长:8 分钟·2024 年 6 月 12 日

--

工作中的理想化数据科学部门 | imagine.art

在过去的五年里,我曾担任两家上市公司数据科学、人工智能和研究部门的副总裁。在这两个职位上,人工智能是公司核心产品的核心。这为我提供了大量资源和领导大型部门的机会——部门包括 40 到 50 名数据科学家,2 到 3 名小组领导和 6 到 8 名团队负责人。在这个角色中,我面临的最大挑战之一就是如何构建部门以提高效率、优化价值并明确角色和责任。今天,我将分享我通过六次部门重组积累的一些最佳实践。

从哪里开始?

在整个过程中,你的指导原则应该是业务需求。你的组织正在创造什么价值?你是在为核心产品开发模型,还是在为业务提供内部机器学习解决方案?确定你的部门提供的具体价值以及衡量成功的关键绩效指标(KPI)。理想情况下,你应该有明确的 KPI,但如果这些不可用,可以制定隐性衡量标准来指导你的工作(例如,你是如何衡量自己的)。

掌握 Airflow 变量

原文:towardsdatascience.com/mastering-airflow-variables-32548a53b3c5?source=collection_archive---------6-----------------------#2024-01-27

你从 Airflow 检索变量的方式会影响你的 DAG 性能

Giorgos MyrianthousTowards Data Science Giorgos Myrianthous

·发表于 Towards Data Science ·10 分钟阅读·2024 年 1 月 27 日

--

图片来源:Daniele FranchiUnsplash

如果多个数据管道需要与同一个 API 端点交互,会发生什么?你真的需要在每个管道中声明这个端点吗?如果这个端点在不久的将来发生变化,你将不得不在每一个文件中更新其值。

Airflow 变量是简单却有价值的构造,用于防止在多个 DAG 中进行冗余声明。它们只是由一个键和一个可序列化为 JSON 的值组成的对象,存储在 Airflow 的元数据数据库中。

那如果你的代码使用了令牌或其他类型的秘密信息呢?将它们硬编码为明文似乎并不是一个安全的做法。除了减少重复外,Airflow 变量还有助于管理敏感信息。在 Airflow 中有六种不同的定义变量的方法,选择合适的方式对于确保安全性和可移植性至关重要。

一个常被忽视的方面是变量检索对 Airflow 性能的影响。每次调度器解析 DAG 文件时(默认间隔为三十秒),它可能会给元数据数据库带来请求压力。

很容易陷入这个陷阱,除非你了解调度器是如何解析 DAG 的,以及……

掌握“背面纸条数学”将使你成为更好的数据科学家

原文:towardsdatascience.com/mastering-back-of-the-envelope-math-will-make-you-a-better-data-scientist-74316b96472a?source=collection_archive---------1-----------------------#2024-10-23

有时候,一个简单粗暴的答案比一个复杂的模型更有帮助

Torsten WalbaumTowards Data Science Torsten Walbaum

·发表于 Towards Data Science ·11 分钟阅读·2024 年 10 月 23 日

--

图片由作者提供(改编自 Midjourney)

1945 年 7 月 16 日,在洛斯阿拉莫斯进行的首次核弹爆炸试验中,物理学家恩里科·费米丢下小纸片,观察爆炸波到达时纸片移动的距离。

基于这些信息,他估算了炸弹爆炸威力的大致大小。没有复杂的设备或严格的测量;只有一些方向性的数据和逻辑推理。

爆炸后大约 40 秒,气浪到达我所在的位置。我试图通过在大约六英尺高的地方丢下小纸片来估算它的强度,纸片的掉落时间在气浪到来前、期间和之后都有记录。[…] 我估计它相当于当时一千吨 T.N.T.所产生的爆炸 — 恩里科·费米

考虑到这个估算的产生方式,结果出奇地准确。

我们经常被迫做出快速而粗略的估算。 有时我们没有足够的数据来进行严格的分析,其他时候我们根本没有时间提供答案。

不幸的是,估算对我来说并不是自然而然的事。作为一个正在恢复的完美主义者,我总是想让我的分析尽可能稳健。如果我做错了,而我采取了快速粗略的方法,难道这不会让我显得粗心或无能吗?

但随着时间的推移,我意识到,使模型变得越来越复杂,很少能带来更好的决策。

为什么?

  1. 大多数决策并不需要超高精度的分析;只要在大致范围内就足够了

  2. 你使模型变得越复杂,假设也就越多。错误会相互叠加,整个情况变得越来越难以理清。

简单的计算,草稿纸上的估算:无论你怎么称呼它,这就是管理顾问和业务运营人员如何迅速突破复杂性,快速得出有力建议的方法。

而他们所需要的只是结构化的思维和一个电子表格。

我的目标是让这项非常有用的技巧对每个人都能轻松使用。

在本文中,我将涵盖:

  • 如何判断你的分析需要多准确

  • 如何创建“足够准确”的估算

  • 如何让人们对你的估算感到放心

让我们开始吧。

第一部分:你需要多准确?

大多数企业的决策不需要高精度的分析。

我们通常试图弄清楚以下四件事之一:

情景 1:我们能否达到最低标准?

通常,我们只需要知道某事是否会比 X 更好/更大/更有利润。

例如, 大型企业只对那些能够影响其收入或利润的事务感兴趣。Meta 每年收入超过 1000 亿美元,因此任何没有潜力最终成长为数十亿美元业务的新项目,都不会得到太多关注。

一旦你开始进行一个简单的草稿纸上的计算,你会迅速意识到你的预测是否落在数千万、数亿甚至数十亿美元的范围内。

如果你的初步估算远低于标准,那么就没有必要再进行精细化;此时,准确答案并不重要。

其他例子:

  • 风险投资人试图理解一个初创公司的市场机会是否足够大,能够成长为一个独角兽公司

  • 你正在考虑加入一家早期公司,并试图了解它是否有可能成长为一个高估值的公司(例如人工智能或自动驾驶公司)

[## 每当 Torsten Walbaum 发布新内容时,获取电子邮件通知。

每当 Torsten Walbaum 发布新内容时,你将收到一封电子邮件。通过注册,如果你尚未拥有 Medium 账户,你将创建一个…

medium.com](https://medium.com/@twalbaum/subscribe?source=post_page-----74316b96472a--------------------------------)

情景 2:我们能否保持在某个水平以下?

这个情景与上面的情景相反。

例如, 假设 CMO 正在考虑最后一刻参加一个大型行业会议。他正在问是否团队能够及时并在 X 百万美元预算内准备好所有必要的部分(例如展位、支持性的营销活动等)。

给 CMO 一个答案时,准确的时间具体的成本并不是最重要的。此时,他只需要知道是否可行,以便为你的公司争取到会议的一个位置。

关键是使用非常保守的假设。如果即使情况不顺利,你也能按时按预算完成任务,那么你就可以放心地给出绿灯(然后再制定一个更详细、更现实的计划)。

其他示例:

  • 你的经理想知道你是否有时间接手另一个项目。

  • 你正在与客户制定服务水平协议(SLA)(例如,客户支持的响应时间)。

情境 3:我们如何进行排序?

有时,你只需要理解 A 事物是否优于 B 事物;你不一定需要知道 A 事物究竟有多好。

例如, 假设你正在尝试在不同的项目之间分配工程资源。比每个项目的确切影响更重要的是它们的相对排名。

因此,你的重点应该放在确保你做出的假设在相对层面上是准确的(例如,A 项目的工程工作量是否比 B 项目的高或低),并且方法论要保持一致,以便进行公平比较。

其他示例:

  • 你正在决定下一步应该进入哪个国家市场。

  • 你希望了解应该将额外的资金分配给哪个营销渠道。

情境 4:我们的(最佳)估算是什么?

当然,也有一些情况下,估算的实际数字非常重要。

例如,如果你被要求预测预计的支持工单数量,以便客户支持团队能相应地进行人员配置,你的估算将作为直接输入用于人员配置的计算。

在这些情况下,你需要理解1)决策对你的分析的敏感度,以及2)你的估算偏高还是偏低更好

  • 敏感性: 以人员配置的例子为例,你可能发现一个支持代理每天可以处理 50 个工单。因此,估算偏差多少个工单并不重要;只有当偏差达到 50 个工单或更多时,团队才需要多配置或减少一个支持代理。

  • 偏高或偏低: 你的估算错误的方向很重要。在上述例子中,人员不足或过剩对业务的成本是不同的。查看我之前关于错误成本的文章,深入了解这一点。

第二部分:如何创建“足够准确”的估算

你知道自己需要多准确——很好。那么,你究竟该如何制定估算呢?

你可以按照以下步骤,使你的估算尽可能稳健,同时最大程度地减少你在此上花费的时间:

第一步:构建结构

假设你在 Netflix 工作,想要弄清楚通过在平台上添加游戏(如果通过广告进行货币化)能赚多少钱。

如何构建你的估算?

第一步是将指标分解为驱动树,第二步是进行细分。

开发驱动树

在你驱动因子的树形图顶部是“每天的游戏收入”。但你该如何进一步拆解这个驱动因子?

有两个关键的考虑因素:

1. 选择你能获取数据的指标。

例如,游戏行业使用标准化指标来报告货币化,如果你偏离这些标准,可能会很难找到基准(有关基准的更多内容见下文)。

2. 选择能最大限度减少混杂因素的指标。

例如,你可以将收入拆分为“用户数量”和“每用户的平均收入”。问题是,这并没有考虑到用户在游戏中花费的时间。

为了解决这个问题,我们可以将收入拆分为“游戏时长”和“每小时收入”,这样可以确保你的游戏与“传统”游戏之间的参与差异不会影响结果。

然后,你可以进一步拆分每个指标,例如:

  • 每小时收入”可以通过将“每小时广告展示量”与“每次广告展示的收入”相乘来计算

  • 游戏时长”可以拆分为“日活跃用户数(DAU)”和“每位 DAU 的游戏时长

然而,增加细节并不总是有益的(下面会详细说明)。

细分

为了获得有用的估算,你需要考虑那些影响你能够产生多少收入的关键维度。

例如,Netflix 活跃于多个国家,这些国家的货币化潜力差异巨大,为了考虑这一点,你可以按地区拆分分析。

哪些维度有助于获得更准确的估算取决于具体的使用场景,但这里有一些常见的维度可以考虑:

  • 地理位置

  • 用户人口统计(年龄、设备等)

  • 收入来源(例如,广告、订阅、交易等)

“好的,太好了,但我怎么知道什么时候细分是有意义的?”

细分有用的前提条件是:

  1. 这些细分市场差异很大(例如,亚太地区的每用户收入远低于美国)

  2. 你拥有足够的信息来对每个细分市场做出有根据的假设

你还需要确保细分是值得付出努力的。实际上,你通常会发现,只有一个或两个指标在细分之间有显著差异。

在这种情况下,你可以采取以下方法以快速获得一个粗略答案:

与其创建多个单独的估算,不如计算一个针对最具变异性的指标的加权平均值。

所以,如果你预计“每小时收入”在不同地区之间差异显著,你可以 1)为每个地区假设这个指标(例如,通过获取基准数据,见下文),并且 2)估算各国的比例:

作者提供的图片

然后,你使用这个数字作为估算,避免了细分的需要。

你应该有多详细?

如果你有可靠的数据作为假设的基础,那么为分析增加更多细节可以提高估算的准确性;但也只是到一定程度为止。

除了增加分析所需的努力,增加更多细节可能会导致虚假的精度

作者提供的图片

那么,什么内容属于“过多细节”的范畴呢?为了快速和粗略的估算,这将包括诸如以下内容:

  • 按设备类型进行分段(智能电视 vs. 安卓 vs. iOS)

  • 考虑按星期几的不同参与程度

  • 按行业分割 CPM

  • 模拟单个游戏的影响

  • 等等

添加如此详细的内容会指数级增加假设的数量,而不一定会使估算更准确。

步骤 2:为每个指标指定数字

现在你已经整理好估算的输入数据,是时候开始为它们指定数字了。

内部数据

如果你进行了一个实验(例如,你向一些用户推出了“Netflix 游戏”的原型)并且有结果可以用来估算,那就太好了。但很多时候,情况并非如此。

在这种情况下,你需要发挥创意。例如,假设为了估算我们的游戏 DAU,我们希望了解有多少 Netflix 用户可能会在他们的动态中看到并点击游戏模块。

为此,你可以将其与其他具有相似入口点的推出进行比较:

  • 最近你还推出了哪些新的首页功能?

  • 他们的表现如何根据位置的不同而有所不同(例如,屏幕顶部的第一排与“页面下方”需要滚动才能看到的位置)?

根据最近几次的发布,你可以三角测量出游戏的预期点击率:

作者提供的图片

这些关系通常足够接近线性(在合理范围内),因此这种类型的近似会产生有用的结果。

一旦你从实验或发布中获得一些实际数据,你就可以调整你的假设。

外部基准

外部基准(例如行业报告、数据供应商)可以在没有内部数据的情况下帮助你找到一个合理的数字范围。

有几个关键考虑因素:

  1. 选择最接近的对比。例如,Netflix 上的休闲游戏更接近于移动游戏,而非 PC 或主机游戏,因此应根据此选择基准。

  2. 确保你的指标定义是一致的。仅仅因为外部报告中的一个指标听起来相似,并不意味着它与你的指标完全相同。例如,许多公司对“日活跃用户”的定义是不同的。

  3. 选择有信誉且透明的来源。 如果你搜索基准数据,你会遇到很多不同的来源。总是尽量找到一个使用(并公开!)可靠方法论的原始来源(例如,使用平台的实际数据,而非调查)。如果报告定期更新,这样你可以在未来根据需要刷新你的估计,那就更好。

决定一个数字

在查看来自不同来源的内部和外部数据后,你可能会有一系列数字可供选择,以供每个指标使用。

看看范围有多宽;这将展示哪些输入对答案的影响最大。

例如,你可能发现来自不同报告的 CPM 基准数据非常相似,但用户每天花费在你游戏上的时间范围却非常广泛。

在这种情况下,你的重点应放在精细调整“每小时播放时间”的假设上:

  1. 如果业务希望看到的最低收入水平是用来投资游戏的,看看你能否通过最保守的假设达到这个水平。

  2. 如果没有最低阈值,尝试使用合理性检查来确定一个现实的水平。

例如,你可以将你预计的游戏播放时间与用户目前在 Netflix 上花费的总时间进行比较。

即使其中一些时间是增量的,但要认为超过总时间的 5%-10%花费在游戏上是不现实的(毕竟,大多数用户是为了视频内容而来到 Netflix 的,且外面有更好的游戏选择)。

第三部分:如何让人们对你的估算感到安心

如果你做的是一个粗略的估计,人们并不指望它完全准确。

然而,他们仍然想了解什么情况下这些数字会变得如此不同,以至于它们会导致不同的决策或建议。

可视化这一点的一个好方法是敏感性表

假设业务希望每天至少达到 50 万美元的广告收入,才考虑启动游戏。你有多大可能实现这一目标?

在表格的 X 轴和 Y 轴上,你放置的是你最不确定的两个输入指标(例如,“每日活跃用户(DAU)”和“每 DAU 花费时间”);表格中的值代表你正在估算的数字(在本例中是“每天的游戏收入”)。

图片来源:作者

然后,你可以将你的最佳估计与业务的最低要求进行比较;例如,如果你估计每日活跃用户数(DAU)为 3000 万,每个 DAU 的播放时间为 0.3 小时,那么你就有足够的缓冲来应对其中任何一个假设错误。

结语

虽然它被称为餐巾纸上的数学,但三行匆忙写在鸡尾酒餐巾纸上的内容通常不足以得出一个可靠的估计。

然而,你也不需要一个完整的 20 标签模型就能得到一个方向性的答案;而且,通常情况下,那个方向性的答案就是你前进所需的一切。

一旦你习惯了粗略的估算,它们能让你比那些仍然陷入分析瘫痪的人更快地行动。通过节省下来的时间,你可以处理另一个项目——或者回家做其他事情。

想获取更多实际操作的分析建议,可以考虑在 Medium 上关注我,或者在 LinkedIn 上关注我,或者在 Substack 上关注我

精通 Python 中的数据流

原文:towardsdatascience.com/mastering-data-streaming-in-python-a88d4b3abf8b?source=collection_archive---------2-----------------------#2024-08-16

实时分析的最佳实践

💡Mike ShakhomirovTowards Data Science 💡Mike Shakhomirov

·发布于 Towards Data Science ·12 分钟阅读·2024 年 8 月 16 日

--

使用 Kandinsky 生成的 AI 图像

在本文中,我将讨论数据工程师在设计流式数据管道时可能遇到的关键挑战。我们将探讨使用场景,提供 Python 代码示例,讨论使用流式框架进行窗口计算,并分享与这些主题相关的最佳实践。

在许多应用中,访问实时和持续更新的数据至关重要。欺诈检测、客户流失预防和推荐系统是流式数据管道的最佳候选者。这些数据管道实时处理来自多个来源的数据,并将其传送到多个目标位置,捕获事件发生时的情况,并对其进行转换、丰富和分析。

流式数据管道

在我之前的一篇文章中,我描述了最常见的数据管道设计模式以及何时使用它们 [1]。

## 数据管道设计模式

选择合适的架构并附带示例

towardsdatascience.com

数据管道是一个数据处理步骤的序列,每个阶段的输出成为下一个阶段的输入,从而创建了数据的逻辑流。

掌握数据可视化:你需要知道的实用技巧

原文:towardsdatascience.com/mastering-data-visualization-practical-tips-you-need-to-know-2ca2953d1873?source=collection_archive---------1-----------------------#2024-10-02

数据科学

掌握有效数据可视化的艺术,传递有影响力的见解,真正吸引你的观众。

Suraj GuravTowards Data Science Suraj Gurav

·发布于 Towards Data Science ·阅读时间 8 分钟·2024 年 10 月 2 日

--

该照片由作者在德国林德霍夫宫拍摄

数据可视化常被认为是简单直接的。但它真的是那么容易吗?让我们看看。

至少在一生中,你一定见过一个比有帮助更让人困惑的仪表盘。我见过很多这样的例子。

大多数时候,根本原因要么是选择了不合适的视觉呈现方式,要么是呈现了无关的信息。这使得关键问题没有得到解答,且让观众感到困惑。

说真的,如果你的图表没有回答正确的问题,并且充满了太多无关的信息——你已经在失去你的观众了。

数据可视化,顾名思义,就是将你的数据转化为清晰的图像,帮助每个人迅速理解正在发生什么。它可以是图表、图形或信息图。

简单说明:这不是一本“如何做”的指南,而是关于如何让你的数据被使用和理解。继续阅读,了解如何做到这一点。

在任何行业中,企业始终由数据驱动的决策推动。然而,决策者无法…

精通 GenAI 机器学习系统设计面试(2):设计 ChatGPT 记忆功能

原文:towardsdatascience.com/mastering-genai-ml-system-design-interview-2-design-chatgpt-memory-feature-fe908517d76c?source=collection_archive---------0-----------------------#2024-06-01

如何设计 ChatGPT 的跨会话记忆

Jack Chih-Hsu LinTowards Data Science Jack Chih-Hsu Lin

·发表于Towards Data Science ·16 分钟阅读·2024 年 6 月 1 日

--

带有记忆的 AI(图片由作者使用 ChatGPT 制作)

2024 年 9 月 21 日备注:我正在与一家著名出版商合作编写《设计生成型 AI 系统》一书。请关注我的MediumLinkedIn账号,获取最新更新。

背景/背景信息

我非常激动地得知,ChatGPT 的记忆功能于 2024 年 4 月 29 日发布,且仅限订阅用户使用。我迫不及待地尝试了这一功能。脑海中一直在思考这个记忆功能是如何实现的,以及其中涉及的重要考虑因素和权衡。通过遵循 GenAI 机器学习系统设计面试的原则和解决方案概述,我将在这篇文章中分享作为面试候选人设计 ChatGPT 记忆功能时的思考和考虑因素。

你能设计 ChatGPT 的记忆功能吗?

在我们尝试回答问题之前,让我们先来看看记忆功能的具体功能。

ChatGPT 记忆功能

如何管理记忆

掌握 GenAI 机器学习系统设计面试:原则与解决方案概述

原文:towardsdatascience.com/mastering-genai-ml-system-design-interview-principles-solution-outline-71a4664511a7?source=collection_archive---------0-----------------------#2024-05-16

面试桌两边的策略与见解

Jack Chih-Hsu LinTowards Data Science Jack Chih-Hsu Lin

·发布于Towards Data Science ·13 分钟阅读·2024 年 5 月 16 日

--

解决机器学习设计就像完成一块拼图(图像由作者使用 ChatGPT 制作)

2024 年 9 月 21 日备注:我正在与一家著名出版社合作出版《设计生成式 AI 系统》一书。请关注我的MediumLinkedIn获取最新动态。

2024 年 6 月 1 日编辑:新增如何设计 ChatGPT 记忆功能的链接

2024 年 5 月 19 日编辑:新增向量存储的考虑

我仍然清晰记得那个时刻:在我与亚马逊的机器学习设计面试中,面试官在聊天中简单地写了几句关于预测两个对象(文本或图像)相关性的内容,除了这些句子没有进一步的解释。唯一的口头指令是阅读文本。我愣住了,瞬间呆住,不知道该如何继续。尽管我尽全力克服困难并完成了这一轮面试,但由于缺乏明确的指导和初始的震惊,我的状态受到影响。正如预期的那样,我没有获得这份工作。

三年过去了,现在我已经是一名首席数据科学家,自己也参与了无数次面试。站在面试桌的两边让我对面试过程有了独特的看法,也更清楚面试官真正关心的是什么。在本文中,我将带你了解……

精通 Git:高效版本控制的三种关键工作流

原文:towardsdatascience.com/mastering-git-the-3-essential-workflows-for-efficient-version-controlling-9bffe1883bd1?source=collection_archive---------2-----------------------#2024-02-03

为优雅的开发旅程提供 Git 工作流

Chayma ZatoutTowards Data Science Chayma Zatout

·发表于 Towards Data Science · 8 分钟阅读 · 2024 年 2 月 3 日

--

图片来自 Prateek KatyalUnsplash

如果你希望高效且优雅地使用 Git,那么你找对地方了!在阅读并在你的项目中应用了这些工作流后,我向你保证,你的项目将会提升到一个新的水平。采用 Git 工作流对我来说不仅是一种好习惯,更是必须的!即使你不与他人合作,你也可以应用它,我自从发现其优点后便一直如此。刚开始可能会觉得难以适应,但通过实践,你会接受它,并开始喜欢上它。那么,让我们开始探索这三种最重要的工作流吧。

还不是 Medium 会员?不用担心!通过这个 朋友链接 继续阅读。

目录:

· 1. 介绍

· 2. 集中式工作流

· 3. 特性分支工作流

· 4. 分叉工作流

· 5. Gitflow 工作流

· 6. 分支命名规范

· 7. 结论

1. 介绍

嗯,当我还是一个初学者,做简单的小项目时,我只是用 Git 来保存我的项目和上传…

精通 K-Means 聚类

原文:towardsdatascience.com/mastering-k-means-clustering-065bc42637e4?source=collection_archive---------0-----------------------#2024-05-22

通过这个逐步 Python 教程,从头开始实现 K-Means 算法

Marcus SenaTowards Data Science Marcus Sena

·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 5 月 22 日

--

图片由作者使用 DALL-E 创建。

在本文中,我将展示如果今天开始学习 K-Means 算法,我会如何学习。我们将从基础概念开始,并实现一个使用 Numpy 包执行聚类任务的 Python 类。

无论你是一个试图建立坚实概念理解的机器学习初学者,还是一个有兴趣创建自定义机器学习应用并需要了解算法底层实现的从业者,本文都适合你。

目录

1. 介绍

2. K-Means 算法做什么?

3. Python 实现

4. 评估与解释

5. 结论与下一步

1. 介绍

大多数广泛使用的机器学习算法,如线性回归、逻辑回归、决策树等,都是用来从标记数据中进行预测的,即每个输入包含特征值并与一个标签值相关联。这就是所谓的监督学习

然而,通常我们需要处理没有标签的大数据集。想象一下,一个企业需要根据购买行为、人口统计学、地址等信息了解不同的客户群体,从而能够提供更好的服务、产品和促销活动。

这类问题可以通过使用 无监督学习 技术来解决。K-Means 算法是机器学习中广泛使用的无监督学习算法。它简洁而优雅的方法使得将数据集分成 K 个不同的簇成为可能,从而可以从未标记的数据中学习模式。

2. K-Means 算法做什么?

如前所述,K-Means 算法旨在将数据点划分为给定数量的簇。每个簇中的点是相似的,而不同簇中的点有显著的差异。

话虽如此,一个问题随之而来:我们如何定义相似性或差异?在 K-Means 聚类中,欧几里得距离是最常用的度量相似性的指标。

在下图中,我们可以清楚地看到 3 个不同的组。因此,我们可以确定每个组的中心,并且每个点将与最接近的中心关联。

带有 200 个观测值的模拟数据集(图片由作者提供)。

通过这样做,从数学角度来说,目标是最小化 簇内方差,即每个点与其最接近的中心之间的相似性度量。

执行上述示例中的任务很简单,因为数据是二维的且各组之间差异明显。然而,随着维度的增加和考虑不同的 K 值,我们需要一种算法来处理复杂性。

第 1 步:选择初始中心(随机选择)

我们需要为算法提供初始中心向量,这些向量可以从数据中随机选择,或者生成与原始数据相同维度的随机向量。请参见下图中的白色菱形。

初始中心是随机选择的(图片由作者提供)。

第 2 步:找到每个点到中心的距离

现在,我们将计算每个数据点到 K 个中心的距离。然后将每个点与离它最近的中心关联。

给定一个包含 N 条数据和 M 个特征的数据集,可以通过以下公式计算到中心 c 的距离:

欧几里得距离(图像通过 codecogs.com 生成)。

其中:

k 从 1 到 K 变化;

D 是点 n 到 k 中心的距离;

x 是点向量;

c 是中心向量。

因此,对于每个数据点 n,我们将得到 K 个距离,然后我们需要将该向量标记为与最小距离的中心关联:

(图像通过 codecogs.com 生成)

其中 D 是一个包含 K 个距离的向量。

第 3 步:找到 K 个质心并进行迭代

对于每个 K 个簇,重新计算质心。新的质心是分配给该簇的所有数据点的均值。然后更新质心的位置,使用新计算出的质心。

检查质心是否发生了显著变化。这可以通过将当前迭代中的质心位置与上一迭代中的质心位置进行比较来完成。

如果质心发生了显著变化,请返回第 2 步。如果没有,算法已收敛,过程停止。见下图。

质心的收敛(作者提供的图片)。

3. 在 Python 中的实现

现在我们已经了解了 K-Means 算法的基本概念,是时候实现一个 Python 类了。所使用的包包括 Numpy 进行数学计算、Matplotlib 进行可视化,以及 Sklearn 中的 Make_blobs 包用于生成模拟数据。

# import required packages
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs

该类将具有以下方法:

  • 初始化方法

一个构造方法,用于初始化算法的基本参数:聚类的值k,最大迭代次数max_iter,以及当没有显著改善时中止优化的容忍度tol

  • 辅助函数

这些方法旨在协助训练过程中的优化,如计算欧几里得距离、随机选择初始质心、将每个点分配给最近的质心、更新质心的值以及验证优化是否收敛。

  • 拟合与预测方法

如前所述,K-Means 算法是一种无监督学习技术,这意味着它在训练过程中不需要标签数据。这样,需要一种单一的方法来拟合数据并预测每个数据点所属的聚类。

  • 总误差方法

评估优化质量的方法,通过计算优化的总平方误差。这将在下一节中详细探讨。

以下是完整的代码:

# helper function for calculating Euclidean distance
def euclidean_distance(a,b):
    d = np.sqrt(np.sum((a - b)**2))
    return d

class Kmeans:

    # construct method for hyperparameter initialization
    def __init__(self, k=3, max_iter=100, tol=1e-06):
        self.k = k
        self.max_iter = max_iter
        self.tol = tol

    # randomly picks the initial centroids from the input data
    def pick_centers(self, X):
        centers_idxs = np.random.choice(self.n_samples, self.k)
        return X[centers_idxs]

    # finds the closest centroid for each data point
    def get_closest_centroid(self, x, centroids):
        distances = [euclidean_distance(x, centroid) for centroid in centroids]
        return np.argmin(distances)

    # creates a list with lists containing the idxs of each cluster
    def create_clusters(self, centroids, X):
        clusters = [[] for _ in range(self.k)]
        labels = np.empty(self.n_samples)
        for i, x in enumerate(X):
            centroid_idx = self.get_closest_centroid(x, centroids)
            clusters[centroid_idx].append(i)
            labels[i] = centroid_idx

        return clusters, labels

    # calculates the centroids for each cluster using the mean value 
    def compute_centroids(self, clusters, X):
        centroids = np.empty((self.k, self.n_features))
        for i, cluster in enumerate(clusters):
            centroids[i] = np.mean(X[cluster], axis=0)

        return centroids

    # helper function to verify if the centroids changed significantly
    def is_converged(self, old_centroids, new_centroids):
        distances = [euclidean_distance(old_centroids[i], new_centroids[i]) for i in range(self.k)]
        return (sum(distances) < self.tol)

    # method to train the data, find the optimized centroids and label each data point according to its cluster
    def fit_predict(self, X):
        self.n_samples, self.n_features = X.shape
        self.centroids = self.pick_centers(X)

        for i in range(self.max_iter):
            self.clusters, self.labels = self.create_clusters(self.centroids, X)
            new_centroids = self.compute_centroids(self.clusters, X)
            if self.is_converged(self.centroids, new_centroids):
                break
            self.centroids = new_centroids

    # method for evaluating the intracluster variance of the optimization
    def clustering_errors(self, X):
        cluster_values = [X[cluster] for cluster in self.clusters]
        squared_distances = []
        # calculation of total squared Euclidean distance
        for i, cluster_array in enumerate(cluster_values):
            squared_distances.append(np.sum((cluster_array - self.centroids[i])**2))

        total_error = np.sum(squared_distances)
        return total_error

4. 评估与解释

现在我们将使用 K-Means 类对模拟数据进行聚类。为此,将使用 Sklearn 库中的 make_blobs 包。数据由 500 个二维点组成,具有 4 个固定的中心。

# create simulated data for examples
X, _ = make_blobs(n_samples=500, n_features=2, centers=4, 
                  shuffle=False, random_state=0)

模拟数据(作者提供的图片)。

在使用四个聚类进行训练后,我们得到了以下结果。

model = Kmeans(k=4)
model.fit_predict(X)
labels = model.labels
centroids =model.centroids
plot_clusters(X, labels, centroids)

k=4 的聚类(作者提供的图片)。

在这种情况下,算法成功地通过 18 次迭代计算出了聚类。然而,我们必须记住,模拟数据已经知道了最优的聚类数。在实际应用中,我们通常不知道这个值。

如前所述,K-Means 算法的目标是尽量减小类内方差。用于计算该方差的度量是总平方欧几里得距离,计算公式为:

总平方欧几里得距离公式(作者通过 codecogs.com 提供的图片)。

其中:

p 是一个簇中的数据点数量;

c_i 是一个簇的质心向量;

K 是簇的数量。

换句话说,上面的公式将数据点到最近质心的距离相加。随着 K 值的增加,误差会减小。

在极端情况下,当 K = N 时,你会为每个数据点创建一个簇,此时误差将为零。

Willmott, Paul (2019)。

如果我们将误差与簇的数量绘制出来,并观察图形“弯曲”的位置,我们就能找到最佳簇数。

屏幕图(作者提供的图像)。

如我们所见,图形呈现“肘部形状”,并在 K = 4 时弯曲,这意味着对于更大的 K 值,误差的减少将不再显著。

5. 结论和下一步

在本文中,我们介绍了 K-Means 算法背后的基本概念、用途和应用。此外,通过这些概念,我们能够从零开始实现一个 Python 类,执行模拟数据的聚类,并展示如何通过屏幕图找到 K 的最佳值。

然而,由于我们使用的是无监督技术,还有一个额外的步骤。算法可以成功地为簇分配标签,但每个标签的含义是数据科学家或机器学习工程师通过分析每个簇的数据来完成的任务。

此外,我还将留下一些供进一步探索的要点:

  • 我们的模拟数据使用了二维点。尝试将该算法应用于其他数据集,并找到 K 的最佳值。

  • 还有其他广泛使用的无监督学习算法,如层次聚类

  • 根据问题领域,可能需要使用其他误差度量,如曼哈顿距离和余弦相似度。尝试研究它们。

完整代码可在此处获取:

[## ML-and-Ai-from-scratch/K-Means 在 main · Marcussena/ML-and-Ai-from-scratch

从零开始实现的机器学习和人工智能算法 - ML-and-Ai-from-scratch/K-Means 在 main ·…

github.com](https://github.com/Marcussena/ML-and-Ai-from-scratch/tree/main/K-Means?source=post_page-----065bc42637e4--------------------------------)

请随时使用和改进代码、评论、提出建议,并通过LinkedInXGithub与我联系。

参考文献

[1] Sebastian Raschka (2015), 《Python 机器学习》。

[2] Willmott, Paul. (2019). 机器学习:应用数学导论。Panda Ohana 出版社。

[3] Géron, A. (2017). 动手学机器学习。O’Reilly Media Inc.

[4] Grus, Joel. (2015). 从零开始学数据科学. O'Reilly Media Inc.

精通 Python 中的市场营销组合建模

原文:towardsdatascience.com/mastering-marketing-mix-modelling-in-python-7bbfe31360f9?source=collection_archive---------1-----------------------#2024-09-26

实践指南的第一部分,帮助你掌握 pymc 中的 MMM

Ryan O'SullivanTowards Data Science Ryan O'Sullivan

·发布于 Towards Data Science ·阅读时间 21 分钟·2024 年 9 月 26 日

--

用户生成的图像

这个系列是关于什么的?

欢迎来到我的市场营销组合建模(MMM)系列的第一部分,这是一本实践指南,旨在帮助你掌握 MMM。在这个系列中,我们将涵盖关键主题,如模型训练、验证、校准和预算优化,所有这些都使用强大的 pymc-marketing Python 包。无论你是 MMM 的新手,还是想要提升技能,本系列将为你提供实用的工具和见解,帮助你改进营销策略。

介绍

在本文中,我们将首先介绍贝叶斯 MMM 的一些背景知识,包括以下主题:

  • 我们可以使用哪些开源包?

  • 什么是贝叶斯 MMM?

  • 什么是贝叶斯先验?

  • pymc-marketing 中的默认先验是否合理?

然后我们将进入使用 pymc-marketing 包的 Python 演示,深入探讨以下领域:

  • 模拟数据

  • 训练模型

  • 验证模型

  • 参数恢复

完整的笔记本可以在这里找到:

[## pymc_marketing/notebooks/1. 在 Python 中训练市场营销组合模型(MMM).ipynb 主要内容 ·…

一个 pymc_marketing 包的演示。通过创建一个账户来贡献给 raz1470/pymc_marketing 的开发…

github.com](https://github.com/raz1470/pymc_marketing/blob/main/notebooks/1. training marketing mix models (MMM) in python.ipynb?source=post_page-----7bbfe31360f9--------------------------------)

1.0 MMM 背景

让我们先简要概述一下营销组合模型(MMM)。我们将探索可用的各种开源包,并深入了解贝叶斯 MMM 的原理,包括先验概念。最后,我们将评估pymc-marketing中使用的默认先验,以衡量它们的适用性和有效性。

1.1 我们可以使用哪些开源包?

在谈到 MMM 时,我们可以使用几个开源包:

用户生成的图片

有几个令人信服的理由,在本系列中我们要专注于pymc-marketing

  1. Python 兼容性:与仅支持 R 的 Robyn 不同,pymc-marketing 服务于更多喜欢使用 Python 的用户。

  2. 当前可用性:Meridian 尚未发布(截至 2024 年 9 月 23 日),这使得 pymc-marketing 现在是一个更容易访问的选择。

  3. 未来考虑:一旦 Meridian 发布,LightweightMMM 将被淘汰,这进一步巩固了寻找可靠替代品的必要性。

  4. 积极开发:pymc-marketing 通过其庞大的贡献者社区不断进行增强,确保它始终跟上新特性和改进。

你可以在这里查看 pymc-marketing 包,他们提供了一些很棒的笔记本,展示了该包的一些功能:

[## 使用方法 - pymc-marketing 0.9.0 文档

这里你会找到一系列示例和使用 PyMC-Marketing MMM 及 CLV 模型的使用指南。

www.pymc-marketing.io

1.2 什么是贝叶斯营销组合模型(MMM)?

你会注意到,上面突出的 4 个包中有 3 个采用了贝叶斯方法。所以让我们花点时间了解一下贝叶斯 MMM 是什么样的!对于初学者来说,贝叶斯分析有点像进入了一个兔子洞,但我们可以将其分解为 5 个关键点:

用户生成的图片

  1. 贝叶斯定理 — 在贝叶斯 MMM 中,贝叶斯定理用于在我们收集到新数据时更新我们关于营销渠道如何影响销售的信念。例如,如果我们对电视广告如何影响销售有一些初步的看法,贝叶斯定理允许我们在看到实际的销售和电视广告支出数据后,完善这一看法。

  2. P(θ)先验代表我们对模型中参数的初步信念,例如电视广告支出对销售的影响。例如,如果我们做了一个地理提升测试,估计电视广告使销售增长了 5%,我们可能会根据这个估计设置一个先验。

  3. P(Data | θ)似然函数,它捕捉了在特定营销投入水平下观察到销售数据的概率。例如,如果你在社交媒体上花费了£100,000 并看到了相应的销售增长,似然函数会告诉我们,根据社交媒体的假定效果,这种销售增长的可能性有多大。

  4. P(θ | 数据)后验分布是我们在贝叶斯 MMM 中最关心的内容——它是我们在结合数据和先验假设后,对不同营销渠道如何影响销售的更新信念。例如,在观察到新的销售数据后,我们最初认为电视广告会带来 5%的销售增长,可能会调整为更精确的估计,比如 4.7%。

  5. 采样 — 由于贝叶斯 MMM 涉及多个参数的复杂模型(例如广告效果、饱和度、营销渠道效应、控制效应等),直接计算后验分布可能很困难。MCMC(马尔可夫链蒙特卡洛)方法通过从每个参数的分布中生成样本来近似后验分布。该方法在处理难以解析求解的模型时特别有用。

1.3 什么是贝叶斯先验?

贝叶斯先验作为概率分布提供。我们不是为参数指定一个固定值,而是提供一系列潜在的值,并附上每个值为真实值的可能性。常用的先验分布包括:

  • 正态分布: 用于期望参数值围绕均值聚集的情况。

  • 半正态分布: 用于希望强制参数为正的情况。

  • Beta: 用于参数约束在 0 和 1 之间的情况。

  • 伽马分布: 用于正值且偏斜的参数。

你可能会听到“信息性先验”这个术语。理想情况下,我们根据专家知识或随机实验提供这些先验。信息性先验反映了对参数值的强烈信念。然而,当这种做法不可行时,我们可以使用非信息性先验,这些先验将概率分布在广泛的值范围内。非信息性先验允许数据主导后验分布,从而防止先验假设过度影响结果。

1.4 pymc-marketing中的默认先验是否合理?

在使用pymc-marketing包时,默认先验设计为弱信息性,这意味着它们提供广泛的指导,而不是过于具体。它们引导模型,但不会过度限制模型。这样的平衡确保了先验指导模型,但又不会遮盖数据本身。

要建立可靠的模型,理解默认先验是至关重要的,而不是盲目使用它们。在接下来的部分中,我们将探讨在pymc-marketing中使用的各种先验,并解释为什么默认选择对于营销组合模型是合理的。

我们可以通过以下代码开始检查默认先验:

dummy_model = MMM(
    date_column="",
    channel_columns=[""],
    adstock=GeometricAdstock(l_max=4),
    saturation=LogisticSaturation(),
)
dummy_model.default_model_config

用户生成的图片

上面我已打印出包含 7 个默认先验的字典——让我们首先简要了解每个先验是什么:

  • 截距 — 在没有任何营销支出或其他变量的情况下,销售或目标变量的基准水平。它为模型设定了起点。

  • likelihood — 当您更侧重于可能性时,模型将更多依赖观察到的数据,较少依赖先验数据。这意味着模型将更加数据驱动,使得观察结果对参数估计的影响更大。

  • gamma_control — 控制外部因素的变量,如宏观经济条件、节假日或其他可能影响销售的非营销变量。

  • gamma_fourier — 用于建模数据中季节性的傅里叶项,捕捉销售中反复出现的模式或周期。

  • adstock_alpha — 控制广告库存效应,决定营销支出的影响力随时间衰减的程度。

  • saturation_lamda — 定义饱和曲线的陡峭度,决定随着营销支出的增加,回报递减的速度。

  • saturation_beta — 营销支出系数,衡量营销支出对目标变量(例如销售)的直接影响。

接下来,我们将专注于深入理解营销和控制变量的参数。

1.4.1 广告库存阿尔法

广告库存反映了一个观点,即营销活动的影响是延迟的,并且随着时间的推移逐渐积累。广告库存阿尔法(衰减率)控制影响力随时间衰减的速度,决定营销活动的影响力在多长时间内持续影响销售。

使用贝塔分布作为广告库存阿尔法的先验。我们首先可视化贝塔分布:

alpha = 1
beta_param = 3

x1 = np.linspace(0, 1, 100)
y1 = beta.pdf(x1, alpha, beta_param)

plt.figure(figsize=(8, 5))
plt.plot(x1, y1, color='blue')
plt.fill_between(x1, y1, color='blue', alpha=0.3)
plt.title('Geometric Adstock: Beta distribution (alpha=1, beta=3)')
plt.xlabel('Adstock alpha')
plt.ylabel('Probability density')
plt.grid(True)
plt.show()

用户生成图像

我们通常将广告库存阿尔法值限制在 0 到 1 之间,因此贝塔分布是一个合理的选择。具体而言,使用 beta(1, 3)作为广告库存阿尔法的先验,反映了在大多数情况下,衰减率应该相对较高,这意味着营销活动的影响很快就会消失。

为了进一步加深直觉,我们可以可视化不同广告库存阿尔法值的影响:

raw_spend = np.array([1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, 0, 0, 0, 0, 0, 0])

adstock_spend_1 = geometric_adstock(x=raw_spend, alpha=0.20, l_max=8, normalize=True).eval().flatten()
adstock_spend_2 = geometric_adstock(x=raw_spend, alpha=0.50, l_max=8, normalize=True).eval().flatten()
adstock_spend_3 = geometric_adstock(x=raw_spend, alpha=0.80, l_max=8, normalize=True).eval().flatten()

plt.figure(figsize=(10, 6))

plt.plot(raw_spend, marker='o', label='Raw Spend', color='blue')
plt.fill_between(range(len(raw_spend)), 0, raw_spend, color='blue', alpha=0.2)

plt.plot(adstock_spend_1, marker='o', label='Adstock (alpha=0.20)', color='orange')
plt.fill_between(range(len(adstock_spend_1)), 0, adstock_spend_1, color='orange', alpha=0.2)

plt.plot(adstock_spend_2, marker='o', label='Adstock (alpha=0.50)', color='red')
plt.fill_between(range(len(adstock_spend_2)), 0, adstock_spend_2, color='red', alpha=0.2)

plt.plot(adstock_spend_3, marker='o', label='Adstock (alpha=0.80)', color='purple')
plt.fill_between(range(len(adstock_spend_3)), 0, adstock_spend_3, color='purple', alpha=0.2)

plt.xlabel('Weeks')
plt.ylabel('Spend')
plt.title('Geometric Adstock')
plt.legend()
plt.grid(True)
plt.show()

用户生成图像

  • 较低的阿尔法值影响较小,适用于那些具有直接反应的渠道,例如付费社交广告,目标群体为已访问过您网站的潜在客户,且广告有明确的行动号召。

  • 较高的阿尔法值具有更强的影响力,适用于那些具有长期效果的渠道,例如品牌建设视频,目标群体为广泛的潜在客户,且没有明确的行动号召。

1.4.2 饱和度λ

随着营销支出的增加,销售的增量影响逐渐减小——这就是所谓的饱和效应。饱和度λ控制饱和曲线的陡峭度,决定营销支出增加时回报递减的速度。

使用伽马分布作为饱和度λ的先验。我们首先来了解伽马分布的形态:

alpha = 3
beta = 1

x2 = np.linspace(0, 10, 1000)
y2 = gamma.pdf(x2, alpha, scale=1/beta)

plt.figure(figsize=(8, 6))
plt.plot(x2, y2, 'b-')
plt.fill_between(x2, y2, alpha=0.2, color='blue')
plt.title('Logistic Saturation: Gamma Distribution (alpha=3, beta=1)')
plt.xlabel('Saturation lamda')
plt.ylabel('Probability density')
plt.grid(True)
plt.show()

用户生成图像

初看之下,可能很难理解为什么 gamma 分布是一个合理的先验选择,但绘制不同 lamda 值的影响有助于阐明它的适用性:

scaled_spend = np.linspace(start=0.0, stop=1.0, num=100)

saturated_spend_1 = logistic_saturation(x=scaled_spend, lam=1).eval()
saturated_spend_2 = logistic_saturation(x=scaled_spend, lam=2).eval()
saturated_spend_4 = logistic_saturation(x=scaled_spend, lam=4).eval()
saturated_spend_8 = logistic_saturation(x=scaled_spend, lam=8).eval()

plt.figure(figsize=(8, 6))
sns.lineplot(x=scaled_spend, y=saturated_spend_1, label="1")
sns.lineplot(x=scaled_spend, y=saturated_spend_2, label="2")
sns.lineplot(x=scaled_spend, y=saturated_spend_4, label="4")
sns.lineplot(x=scaled_spend, y=saturated_spend_8, label="8")

plt.title('Logistic Saturation')
plt.xlabel('Scaled Marketing Spend')
plt.ylabel('Saturated Marketing Spend')
plt.legend(title='Lambda')
plt.grid(True)
plt.show()

用户生成的图像

  • lamda 值为 1 时,关系保持线性。

  • 随着 lamda 值的增加,饱和曲线的陡峭度增加。

  • 从图表中我们希望能够达成一致意见,即当 lamda 值为 8 时,似乎不太可能出现比我们看到的更陡峭的饱和曲线。

1.4.3 饱和 beta

饱和 beta 对应于营销渠道系数,用于衡量营销支出的影响。

使用半正态先验,它强制要求正值,这是一个非常合理的假设,例如营销不应该产生负面效果。当 sigma 设置为 2 时,它倾向于低值。这有助于正则化系数,除非数据中有强烈的证据表明某个特定渠道有显著的影响,否则它会将系数拉向较低的值。

sigma = 2

x3 = np.linspace(0, 10, 1000)
y3 = halfnorm.pdf(x3, scale=sigma)

plt.figure(figsize=(8, 6))
plt.plot(x3, y3, 'b-')
plt.fill_between(x3, y3, alpha=0.2, color='blue')
plt.title('Saturation beta prior: HalfNormal Distribution (sigma=2)')
plt.xlabel('Saturation beta')
plt.ylabel('Probability Density')
plt.grid(True)
plt.show()

用户生成的图像

请记住,营销和目标变量都已标准化(范围从 0 到 1),因此 beta 先验必须处于这个标准化空间中。

1.4.4 Gamma 控制

gamma 控制参数是控制变量的系数,用于考虑外部因素,如宏观经济条件、假期或其他非营销变量。

使用正态分布,它允许正负效应同时存在:

mu = 0
sigma = 2

x = np.linspace(mu - 4*sigma, mu + 4*sigma, 100)
y = norm.pdf(x, mu, sigma)

plt.figure(figsize=(8, 5))
plt.plot(x, y, color='blue')
plt.fill_between(x, y, color='blue', alpha=0.3)
plt.title('Control: Normal distribution (mu=0, sigma=2)')
plt.xlabel('Control value')
plt.ylabel('Probability density')
plt.grid(True)
plt.show()

用户生成的图像

控制变量已标准化,值范围从 -1 到 1,因此 gamma 控制先验必须处于这个标准化空间中。

2.0 Python 操作指南

现在我们已经涵盖了一些理论,让我们将其中的一些应用到实践中!在这个操作指南中,我们将介绍:

  • 模拟数据

  • 训练模型

  • 验证模型

  • 参数恢复

目标是创建一些现实的训练数据,在其中我们自己设置参数(如广告效应、饱和度、beta 等),使用 pymc-marketing 包训练并验证模型,然后评估我们的模型在恢复参数方面的表现。

当涉及到真实世界的 MMM 数据时,你不会知道参数,但这个参数恢复的练习是学习并在 MMM 中建立信心的好方法。

2.1 模拟数据

让我们开始模拟一些数据来训练模型。这种方法的好处是我们可以自己设置参数,这使我们能够进行参数恢复练习,以测试我们的模型表现如何!

以下函数可用于模拟具有以下特征的一些数据:

  1. 具有一定增长的趋势成分。

  2. 一个带有围绕 0 振荡的季节性成分。

  3. 趋势和季节性成分用于创建需求。

  4. 创建一个需求代理变量,它将可供模型使用(需求将是一个未观察到的混杂因素)。

  5. 需求是销售的关键驱动因素。

  6. 每个营销渠道也都会对销售产生贡献,营销支出与需求相关,且适当的转换方法已应用(缩放、广告库存、饱和度)。

import pandas as pd
import numpy as np
from pymc_marketing.mmm.transformers import geometric_adstock, logistic_saturation
from sklearn.preprocessing import MaxAbsScaler

def data_generator(start_date, periods, channels, spend_scalar, adstock_alphas, saturation_lamdas, betas, freq="W"):
    '''
    Generates a synthetic dataset for a MMM with trend, seasonality, and channel-specific contributions.

    Args:
        start_date (str or pd.Timestamp): The start date for the generated time series data.
        periods (int): The number of time periods (e.g., days, weeks) to generate data for.
        channels (list of str): A list of channel names for which the model will generate spend and sales data.
        spend_scalar (list of float): Scalars that adjust the raw spend for each channel to a desired scale.
        adstock_alphas (list of float): The adstock decay factors for each channel, determining how much past spend influences the current period.
        saturation_lamdas (list of float): Lambda values for the logistic saturation function, controlling the saturation effect on each channel.
        betas (list of float): The coefficients for each channel, representing the contribution of each channel's impact on sales.

    Returns:
        pd.DataFrame: A DataFrame containing the generated time series data, including demand, sales, and channel-specific metrics.
    '''

    # 0\. Create time dimension
    date_range = pd.date_range(start=start_date, periods=periods, freq=freq)
    df = pd.DataFrame({'date': date_range})

    # 1\. Add trend component with some growth
    df["trend"]= (np.linspace(start=0.0, stop=20, num=periods) + 5) ** (1 / 8) - 1

    # 2\. Add seasonal component with oscillation around 0
    df["seasonality"] = df["seasonality"] = 0.1 * np.sin(2 * np.pi * df.index / 52)

    # 3\. Multiply trend and seasonality to create overall demand with noise
    df["demand"] = df["trend"] * (1 + df["seasonality"]) + np.random.normal(loc=0, scale=0.10, size=periods)
    df["demand"] = df["demand"] * 1000

    # 4\. Create proxy for demand, which is able to follow demand but has some noise added
    df["demand_proxy"] = np.abs(df["demand"]* np.random.normal(loc=1, scale=0.10, size=periods))

    # 5\. Initialize sales based on demand
    df["sales"] = df["demand"]

    # 6\. Loop through each channel and add channel-specific contribution
    for i, channel in enumerate(channels):

        # Create raw channel spend, following demand with some random noise added
        df[f"{channel}_spend_raw"] = df["demand"] * spend_scalar[i]
        df[f"{channel}_spend_raw"] = np.abs(df[f"{channel}_spend_raw"] * np.random.normal(loc=1, scale=0.30, size=periods))

        # Scale channel spend
        channel_transformer = MaxAbsScaler().fit(df[f"{channel}_spend_raw"].values.reshape(-1, 1))
        df[f"{channel}_spend"] = channel_transformer .transform(df[f"{channel}_spend_raw"].values.reshape(-1, 1))

        # Apply adstock transformation
        df[f"{channel}_adstock"] = geometric_adstock(
            x=df[f"{channel}_spend"].to_numpy(),
            alpha=adstock_alphas[i],
            l_max=8, normalize=True
        ).eval().flatten()

        # Apply saturation transformation
        df[f"{channel}_saturated"] = logistic_saturation(
            x=df[f"{channel}_adstock"].to_numpy(),
            lam=saturation_lamdas[i]
        ).eval()

        # Calculate contribution to sales
        df[f"{channel}_sales"] = df[f"{channel}_saturated"] * betas[i]

        # Add the channel-specific contribution to sales
        df["sales"] += df[f"{channel}_sales"]

    return df

现在,我们可以使用一些现实的参数调用数据生成函数:

  • 3 年的每周数据。

  • 来自营销漏斗不同部分的 3 个渠道。

  • 每个渠道有不同的广告库存、饱和度和 beta 参数。

np.random.seed(10)

# Set parameters for data generator
start_date = "2021-01-01"
periods = 52 * 3
channels = ["tv", "social", "search"]
adstock_alphas = [0.50, 0.25, 0.05]
saturation_lamdas = [1.5, 2.5, 3.5]
betas = [350, 150, 50]
spend_scalars = [10, 15, 20]

df = dg.data_generator(start_date, periods, channels, spend_scalars, adstock_alphas, saturation_lamdas, betas)

# Scale betas using maximum sales value - this is so it is comparable to the fitted beta from pymc (pymc does feature and target scaling using MaxAbsScaler from sklearn)
betas_scaled = [
    ((df["tv_sales"] / df["sales"].max()) / df["tv_saturated"]).mean(),
    ((df["social_sales"] / df["sales"].max()) / df["social_saturated"]).mean(),
    ((df["search_sales"] / df["sales"].max()) / df["search_saturated"]).mean()
]

# Calculate contributions - these will be used later on to see how accurate the contributions from our model are
contributions = np.asarray([
    round((df["tv_sales"].sum() / df["sales"].sum()), 2),
    round((df["social_sales"].sum() / df["sales"].sum()), 2),
    round((df["search_sales"].sum() / df["sales"].sum()), 2),
    round((df["demand"].sum() / df["sales"].sum()), 2)
])

df[["date", "demand", "demand_proxy", "tv_spend_raw", "social_spend_raw", "search_spend_raw", "sales"]]

用户生成的图像

在我们开始训练模型之前,让我们花些时间理解我们所生成的数据。

  • 我们推导需求的直觉是,随着每年高需求和低需求时期的加入,天然增长的趋势逐渐上升。
plt.figure(figsize=(8, 5))

sns.lineplot(x=df['date'], y=df['trend']*1000, label="Trend", color="green")
sns.lineplot(x=df['date'], y=df['seasonality']*1000, label="Seasonality", color="orange")
sns.lineplot(x=df['date'], y=df['demand'], label="Demand", color="blue")

plt.title('Components', fontsize=16)
plt.xlabel('Date', fontsize=12)
plt.ylabel('Value', fontsize=12)
plt.xticks(rotation=45, ha='right')
plt.legend()
plt.show()

用户生成的图像

  • 我们为需求创建了一个代理变量,用作模型中的控制变量。这里的想法是,在现实中需求是一个不可观察的混杂因素,但有时我们可以找到需求的代理变量。一个例子是你所销售产品的谷歌搜索趋势数据。
plt.figure(figsize=(8, 5))

sns.scatterplot(x=df['demand_proxy'], y=df['demand'], color="blue")

plt.title('Demand proxy vs demand', fontsize=16)
plt.xlabel('Demand proxy', fontsize=12)
plt.ylabel('Demand', fontsize=12)
plt.xticks(rotation=45, ha='right')
plt.show()

用户生成的图像

  • 各个营销渠道的花费趋势相同,但我们加入的随机噪声应能帮助模型分析每个渠道对销售的贡献。
plt.figure(figsize=(8, 5))

sns.lineplot(x=df['date'], y=df['tv_spend_raw'], label=channels[0], color="orange")
sns.lineplot(x=df['date'], y=df['social_spend_raw'], label=channels[1], color="blue")
sns.lineplot(x=df['date'], y=df['search_spend_raw'], label=channels[2], color="green")
plt.title('Marketing Channel Spend', fontsize=16)
plt.xlabel('Date', fontsize=12)
plt.ylabel('Value', fontsize=12)
plt.xticks(rotation=45, ha='right')
plt.legend()
plt.show()

用户生成的图像

  • 我们对营销数据应用了两种转换方法:广告库存(adstock)和饱和度。下面我们可以观察不同参数对各渠道饱和度的影响,其中搜索的饱和度变化最陡,而电视几乎呈线性变化。
plt.figure(figsize=(8, 5))

sns.lineplot(x=df['tv_adstock'], y=df['tv_saturated'], label=channels[0], color="orange")
sns.lineplot(x=df['social_adstock'], y=df['social_saturated'], label=channels[1], color="blue")
sns.lineplot(x=df['search_adstock'], y=df['search_saturated'], label=channels[2], color="green")

plt.title('Marketing Spend Saturation', fontsize=16)
plt.xlabel('Adstocked spend', fontsize=12)
plt.ylabel('Saturated spend', fontsize=12)
plt.legend()
plt.show()

用户生成的图像

  • 下面我们可以看到,我们的变量高度相关,这在 MMM 数据中非常常见,因为营销团队在高峰期花费更多。
plt.figure(figsize=(8, 8))
sns.heatmap(df[["demand", "demand_proxy", "tv_spend_raw", "social_spend_raw", "search_spend_raw", "sales"]].corr(), annot=True, cmap='coolwarm', vmin=-1, vmax=1)
plt.title('Correlation Heatmap')
plt.show()

用户生成的图像

  • 最后,让我们看看销售情况 —— 记住我们的目标是理解营销如何促成销售。
plt.figure(figsize=(8, 5))

sns.lineplot(x=df['date'], y=df['sales'], label="sales", color="green")

plt.title('Sales', fontsize=16)
plt.xlabel('Date', fontsize=12)
plt.ylabel('Value', fontsize=12)
plt.xticks(rotation=45, ha='right')
plt.legend()
plt.show()

用户生成的图像

现在我们已经对数据生成过程有了较好的理解,让我们深入到模型训练中吧!

2.2 训练模型

现在是时候开始训练模型了。首先,我们需要通过以下步骤准备训练数据:

  • 将数据拆分为特征和目标。

  • 创建训练集和超时切片的指标 —— 超时切片将帮助我们验证模型。

# set date column
date_col = "date"

# set outcome column
y_col = "sales"

# set marketing variables
channel_cols = ["tv_spend_raw",
                "social_spend_raw",
                "search_spend_raw"]

# set control variables
control_cols = ["demand_proxy"]

# split data into features and target
X = df[[date_col] + channel_cols + control_cols]
y = df[y_col]

# set test (out-of-sample) length
test_len = 8

# create train and test indices
train_idx = slice(0, len(df) - test_len)
out_of_time_idx = slice(len(df) - test_len, len(df))

接下来我们初始化 MMM 类。MMM 中的一个主要挑战是参数与训练观察值的比率很高。我们可以通过对转换方式进行务实选择来缓解这一问题:

  • 我们选择了几何广告库存(adstock),它每个渠道有 1 个参数,而与使用威布尔广告库存相比,后者有 2 个参数。

  • 我们选择了逻辑饱和度,它每个渠道有 1 个参数,而与使用 Hill 饱和度相比,后者有 2 个参数。

  • 我们使用需求的代理,并决定不包括季节性成分或时间变化趋势,这进一步减少了我们的参数数量。

  • 我们决定不包括时间变化的媒体参数,因为尽管确实存在性能随时间变化的情况,但我们最终希望通过响应曲线来帮助我们优化预算,而对训练数据集进行平均性能处理是一种不错的方法。

总结一下我的观点,模型的复杂度越高,我们越是操控营销支出变量以适应数据,从而给我们一个“更好的模型”(例如,较高的 R 方和较低的均方误差)。但这也有可能偏离真实的数据生成过程,从而导致我们得到偏倚的因果效应估计。

mmm_default = MMM(
    adstock=GeometricAdstock(l_max=8),
    saturation=LogisticSaturation(),
    date_column=date_col,
    channel_columns=channel_cols,
    control_columns=control_cols,
)

mmm_default.default_model_config

用户生成的图像

现在让我们使用训练集索引来拟合模型。我传递了一些可选的关键字参数,花点时间理解它们的作用:

  • Tune — 这是为采样器设置的调优步骤数。在调优过程中,采样器会调整其参数,以有效地探索后验分布。这些初始的 1,000 个样本会被丢弃,并且不用于推断。

  • Chains — 这是指定运行的独立马尔可夫链的数量。运行多条链有助于评估收敛性,并提供更稳健的后验抽样。

  • Draws — 这是设置每条链从后验分布中抽取的样本数(调优后)。

  • Target accept — 这是采样器的目标接受率。它有助于平衡参数空间的探索与效率。

fit_kwargs = {
    "tune": 1_000,
    "chains": 4,
    "draws": 1_000,
    "target_accept": 0.9,
}

mmm_default.fit(X[train_idx], y[train_idx], **fit_kwargs)

我建议在这里使用默认设置,只有在后续步骤中遇到发散问题时才考虑更改。

2.3 验证模型

在拟合模型后,第一步是检查发散情况。发散表明模型或采样过程可能存在问题。虽然由于其复杂性,深入分析发散问题超出了本文的范围,但必须注意它们在模型验证中的重要性。

下面,我们检查模型中的发散数量。0 次发散表示一个良好的开始。

mmm_default.idata["sample_stats"]["diverging"].sum().item()

用户生成的图像

接下来我们可以对 MCMC 抽样结果进行全面总结。让我们聚焦于几个关键点:

  • mean — 所有样本中参数的平均值。

  • hdi_3% 和 hdi_97% — 94% 最高密度区间(HDI)的下限和上限。94% HDI 表示基于观测数据和先验信息,参数的真实值有 94% 的概率落在该区间内。

  • rhat — Gelman-Rubin 统计量,用于衡量链的收敛性。接近 1(通常 < 1.05)的值表示良好的收敛性。

我们的 R-hat 值都非常接近 1,这在没有发散的情况下是可以预期的。在下一节进行参数恢复练习时,我们将重新审视均值参数值。

az.summary(
    data=mmm_default.fit_result,
    var_names=[
        "intercept",
        "y_sigma",
        "saturation_beta",
        "saturation_lam",
        "adstock_alpha",
        "gamma_control",
    ],
)

用户生成的图像

接下来,我们生成诊断图,这对评估 MCMC 采样质量至关重要:

  • 后验分布(左): 显示每个参数在 MCMC 采样过程中的值。理想情况下,这些分布应平滑且单峰,所有链条应显示出相似的分布。

  • 轨迹图(右): 显示每个参数在 MCMC 采样过程中的值。任何趋势或缓慢漂移可能表明混合不良或非收敛,而未重叠的链条可能表明它们陷入了后验分布的不同模式。

从我们的诊断图看,没有出现明显的警告信号。

_ = az.plot_trace(
    data=mmm_default.fit_result,
    var_names=[
        "intercept",
        "y_sigma",
        "saturation_beta",
        "saturation_lam",
        "adstock_alpha",
        "gamma_control",
    ],
    compact=True,
    backend_kwargs={"figsize": (12, 10), "layout": "constrained"},
)
plt.gcf().suptitle("Model Trace", fontsize=16);

用户生成的图像

对于每个参数,我们都有一个可能值的分布,反映了参数估计的不确定性。对于下一组诊断,我们首先需要从后验分布中采样。这将使我们能够进行预测,我们将对训练数据进行预测。

mmm_default.sample_posterior_predictive(X[train_idx], extend_idata=True, combined=True)

我们可以通过直观评估观察数据是否落在预测范围内来开始后验预测检验诊断。看起来我们的模型表现良好。

mmm_default.plot_posterior_predictive(original_scale=True);

用户生成的图像

接下来,我们可以计算后验预测均值和实际数据之间的残差。我们可以绘制这些残差随时间变化的图形,以检查是否有模式或自相关性。看起来残差围绕 0 波动,正如预期的那样。

mmm_default.plot_errors(original_scale=True);

用户生成的图像

我们还可以检查残差是否围绕 0 正态分布。再次,我们通过了这个诊断测试。

errors = mmm_default.get_errors(original_scale=True)

fig, ax = plt.subplots(figsize=(8, 6))
az.plot_dist(
    errors, quantiles=[0.25, 0.5, 0.75], color="C3", fill_kwargs={"alpha": 0.7}, ax=ax
)
ax.axvline(x=0, color="black", linestyle="--", linewidth=1, label="zero")
ax.legend()
ax.set(title="Errors Posterior Distribution");

用户生成的图像

最后,评估我们的模型在训练样本之外的预测能力是一个良好的实践。首先,我们需要再次从后验分布中采样,但这次使用的是超出时间的数据。然后我们可以将观察到的销售额与预测的销售额进行比较。

在下图中,我们可以看到观察到的销售额大致落在区间内,模型似乎做得很好。

y_out_of_sample = mmm_default.sample_posterior_predictive(
    X_pred=X[out_of_time_idx], extend_idata=False, include_last_observations=True
)

def plot_in_sample(X, y, ax, n_points: int = 15):
    (
        y.to_frame()
        .set_index(X[date_col])
        .iloc[-n_points:]
        .plot(ax=ax, marker="o", color="black", label="actuals")
    )
    return ax

def plot_out_of_sample(X_out_of_sample, y_out_of_sample, ax, color, label):
    y_out_of_sample_groupby = y_out_of_sample["y"].to_series().groupby("date")

    lower, upper = quantiles = [0.025, 0.975]
    conf = y_out_of_sample_groupby.quantile(quantiles).unstack()
    ax.fill_between(
        X_out_of_sample[date_col].dt.to_pydatetime(),
        conf[lower],
        conf[upper],
        alpha=0.25,
        color=color,
        label=f"{label} interval",
    )

    mean = y_out_of_sample_groupby.mean()
    mean.plot(ax=ax, marker="o", label=label, color=color, linestyle="--")
    ax.set(ylabel="Original Target Scale", title="Out of sample predictions for MMM")
    return ax

_, ax = plt.subplots()
plot_in_sample(X, y, ax=ax, n_points=len(X[out_of_time_idx])*3)
plot_out_of_sample(
    X[out_of_time_idx], y_out_of_sample, ax=ax, label="out of sample", color="C0"
)
ax.legend(loc="upper left");

用户生成的图像

根据我们在本节中的发现,我们相信我们有一个稳健的模型。在下一节中,让我们进行一个参数恢复练习,看看我们接近真实参数的程度。

2.4 参数恢复

在上一节中,我们按照实际场景验证了模型。在本节中,我们将进行参数恢复练习:记住,我们之所以能够做到这一点,是因为我们自己模拟了数据并存储了真实参数。

2.4.1 参数恢复 — 广告库存效应(Adstock)

让我们从将广告库存参数的后验分布与我们之前存储在 adstock_alphas 列表中的真实值进行比较开始。模型表现得相当不错,达到了正确的排名顺序,并且真实值始终位于后验分布范围内。

fig = mmm_default.plot_channel_parameter(param_name="adstock_alpha", figsize=(9, 5))
ax = fig.axes[0]
ax.axvline(x=adstock_alphas[0], color="C0", linestyle="--", label=r"$\alpha_1$")
ax.axvline(x=adstock_alphas[1], color="C1", linestyle="--", label=r"$\alpha_2$")
ax.axvline(x=adstock_alphas[2], color="C2", linestyle="--", label=r"$\alpha_3$")
ax.legend(loc="upper right");

用户生成的图片

2.4.2 参数恢复 — 饱和度

当我们检查饱和度时,模型在恢复电视的 lambda 方面表现非常好,但在恢复社交和搜索的真实值时表现较差,尽管结果并不灾难性。

fig = mmm_default.plot_channel_parameter(param_name="saturation_lam", figsize=(9, 5))
ax = fig.axes[0]
ax.axvline(x=saturation_lamdas[0], color="C0", linestyle="--", label=r"$\lambda_1$")
ax.axvline(x=saturation_lamdas[1], color="C1", linestyle="--", label=r"$\lambda_2$")
ax.axvline(x=saturation_lamdas[2], color="C2", linestyle="--", label=r"$\lambda_3$")
ax.legend(loc="upper right");

用户生成的图片

2.4.3 参数恢复 — 渠道贝塔

关于渠道贝塔参数,模型达到了正确的排名顺序,但它高估了所有渠道的值。在下一节中,我们将以贡献为单位量化这一点。

fig = mmm_default.plot_channel_parameter(param_name="saturation_beta", figsize=(9, 5))
ax = fig.axes[0]
ax.axvline(x=betas_scaled[0], color="C0", linestyle="--", label=r"$\beta_1$")
ax.axvline(x=betas_scaled[1], color="C1", linestyle="--", label=r"$\beta_2$")
ax.axvline(x=betas_scaled[2], color="C2", linestyle="--", label=r"$\beta_3$")
ax.legend(loc="upper right");

用户生成的图片

2.4.4 参数恢复 — 渠道贡献

首先,我们计算每个渠道的真实贡献。我们还将计算需求的贡献:请记住,我们包含的是需求的代理,而不是需求本身。

channels = np.array(["tv", "social", "search", "demand"])

true_contributions = pd.DataFrame({'Channels': channels, 'Contributions': contributions})
true_contributions= true_contributions.sort_values(by='Contributions', ascending=False).reset_index(drop=True)
true_contributions = true_contributions.style.bar(subset=['Contributions'], color='lightblue')

true_contributions

用户生成的图片

现在,让我们绘制模型的贡献。需求、电视、社交和搜索的排名顺序是正确的。然而,电视、社交和搜索的贡献都被高估了。这似乎是由于需求代理的贡献没有真实需求那么大。

mmm_default.plot_waterfall_components_decomposition(figsize=(10,6));

用户生成的图片

结束语

在 2.3 节中,我们验证了模型,并得出结论认为它是稳健的。然而,参数恢复实验表明,我们的模型大大高估了营销效果。这一高估是由需求这一混杂因素引起的。在现实生活中,不可能进行参数恢复实验。那么,你如何识别模型中营销贡献的偏差?一旦识别出来,你又会如何解决这个问题呢?这引出了我们下一篇关于模型校准的文章!

希望你喜欢这第一篇文章!如果你想继续走向掌握 MMM 的道路,记得关注我——在下一篇文章中,我们将把焦点转向使用实验的有用先验进行模型校准。

掌握模型不确定性:深度学习中的阈值技术

原文:towardsdatascience.com/mastering-model-uncertainty-thresholding-techniques-in-deep-learning-1f1ab3912fd1?source=collection_archive---------4-----------------------#2024-12-30

图像由 Dall-e 生成

一些关于阈值处理、softmax 激活函数、引入额外标签以及关于输出激活函数的考虑事项。

Hampus GustavssonTowards Data Science Hampus Gustavsson

·发布于 Towards Data Science ·6 分钟阅读·5 天前

--

在许多现实世界的应用中,机器学习模型并不是为了以“全有或全无”的方式做出决策。相反,存在一些情形,模型标记某些预测以供人工审查会更为有益——这一过程称为“人类参与”方法。这种方法在高风险场景中尤其有价值,例如欺诈检测,在这些场景中,假阴性(漏检)带来的成本非常高。通过在模型不确定或遇到复杂案例时允许人工干预,企业可以确保更加细致和准确的决策。

在本文中,我们将探讨如何在深度学习环境中实现一种用于管理模型不确定性的技术——阈值处理。阈值处理帮助确定模型何时足够自信以自主做出决策,何时应该将决策交给人工判断。我们将使用一个现实世界的例子来说明其潜力。

在本文的结尾,目标是为技术团队和业务利益相关者提供一些关于建模、阈值策略以及自动化与人工监督之间平衡的决策建议和启发。

商业案例:通过信心检测欺诈交易

为了说明阈值化在现实情况中的价值,我们来考虑一个金融机构检测欺诈交易的案例。我们将使用 Kaggle 欺诈检测数据集(DbCL 许可),该数据集包含匿名化的交易数据和欺诈活动标签。金融机构处理大量交易,手动审核每一笔交易非常困难。我们希望开发一个系统,既能准确标记可疑交易,又能最大限度减少不必要的人工干预。

挑战在于平衡精度和效率。阈值化是一种用于引入这种权衡的策略。通过这种策略,我们为样本空间添加了一个额外的标签——未知。当模型对某个特定预测不确定时,这个标签作为模型的信号,实际上是将决策推迟到人工审核。当模型缺乏足够的确定性来做出可靠预测时,将交易标记为未知可以确保只有最有信心的预测会被执行。

此外,阈值化可能还会带来另一个积极的副作用。它有助于克服潜在的技术怀疑。当模型表示不确定并在需要时将决策推迟给人工判断时,可以增强对系统的信任。在之前的项目中,这在将项目推向各个组织时非常有帮助。

技术和分析方面。

我们将探讨阈值化在深度学习中的应用。然而,值得注意的是,阈值化是一种与模型无关的技术,适用于各种类型的情况,而不仅仅是深度学习。

在神经网络中实现阈值化步骤时,放在哪个层并不明显。在分类设置中,可以实现输出转换。sigmoid 函数是一种选择,但也可以使用 softmax 函数。Softmax 提供了一种非常实用的转换,使得 logits 符合某些良好的统计性质。这些性质确保了 logits 的和为 1,并且它们都在 0 和 1 之间。

Softmax 函数。图片由作者提供。

然而,在这个过程中,某些信息会丢失。Softmax 仅捕捉标签之间的相对确定性。它并没有提供任何单一标签的绝对确定性度量,这可能会导致在真实的不确定性分布更为复杂的情况下出现过度自信。这一限制在需要精确决策阈值的应用中变得尤为关键。

本文不会深入探讨模型架构的细节,因为这些内容将在未来的文章中讨论,供感兴趣的读者参考。本文使用的只是模型在进行 softmax 转换前后的输出结果,作为最终层。这里展示了输出的一个示例。

二十个预测样本,软最大(softmax)应用之后。

如图所示,输出相当均匀。没有了解 softmax 的机制时,看起来模型对于分类相当确定。但正如我们将在文章后面看到的那样,我们在这里捕捉到的强关系并非标签的真正确定性。实际上,这应当被解释为一个标签相对于其他标签的预测。在我们的例子中,这意味着模型可能会捕捉到某些标签的概率明显高于其他标签,但这并不反映模型的整体确定性。

在理解了输出的解释后,让我们探讨一下模型在实际中的表现。查看混淆矩阵。

整个未经过阈值处理的测试数据集的混淆矩阵。

模型的表现并不差,尽管远非完美。有了这些基础结果,我们将探讨如何实现阈值处理。

我们将从进入网络的一层开始——检查最终激活函数之前的值。这些值呈现出以下 logits。

二十个预测样本,软最大(softmax)变换应用之前。

在这里,我们看到更丰富的数值种类。这一层提供了模型在预测中的不确定性的更详细视图,这也是阈值层被插入的位置。

通过引入上下置信度阈值,模型仅对大约 34%的数据集进行标记,集中关注最确定的预测。然而,反过来,结果更加确定,如下所示的混淆矩阵所示。值得注意的是,阈值处理不一定是统一的。例如,某些标签可能比其他标签更难预测,标签不平衡也可能影响阈值策略。

阈值处理应用后的混淆矩阵。

指标

在这种情况下,我们仅触及了阈值处理中的两个极端情况;一个是让所有预测通过(基本情况),另一个是移除所有错误预测。

根据实际经验,决定是用较高的确定性标记较少的数据点(这可能减少被标记的事务总数),还是用较低的确定性标记更多的数据点,是一个相当复杂的权衡决策。这个决策会影响操作效率,并可能受到业务优先级的影响,例如风险容忍度或操作约束。与领域专家一起讨论这个问题,是找出阈值的一个完全可行的方式。另一种方法是,如果您能够结合已知或近似的度量指标进行优化。可以通过将阈值与特定的业务指标对齐来实现,比如每个假阴性成本或操作能力。

总结。

总之,目标并不是丢弃 softmax 转换,因为它提供了有价值的统计特性。我们建议引入一个中间阈值层,用于筛选不确定的预测,并在必要时为未知标签留出空间。

我认为,实施的具体方式取决于手头的项目。欺诈的例子也突显了理解解决的业务需求的重要性。在这里,我们展示了一个例子,我们已经通过阈值排除了所有错误的预测,但这并非所有用例中的必需做法。在许多情况下,最佳解决方案是在准确性和覆盖率之间找到平衡。

感谢您花时间探索这个话题。

我希望您觉得这篇文章有用和/或富有启发。如果您有任何评论或问题,请随时与我联系。您也可以通过LinkedIn与我建立联系。

视频中的物体计数

原文:towardsdatascience.com/mastering-object-counting-in-videos-3d49a9230bd2?source=collection_archive---------3-----------------------#2024-06-25

按步骤指导如何使用检测和追踪技术计数树上行走的蚂蚁。

Lihi Gur Arie, 博士Towards Data Science Lihi Gur Arie, 博士

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

--

视频中的蚂蚁计数。左上角显示进入和退出的计数。每只蚂蚁被分配一个独特的 ID 和颜色。标签由作者提供,原始视频由 Lui Lo Franco 在 Pexels 提供。

介绍

在视频中计数物体是一个具有挑战性的计算机视觉任务。与静态图像中的物体计数不同,视频包含更多的复杂性,因为物体可以移动、被遮挡或在不同时间出现和消失,这使得计数过程更加复杂。

在本教程中,我们将演示如何使用物体检测和追踪技术计数沿树木移动的蚂蚁。我们将利用 Ultralytics 平台集成 YOLOv8 模型进行检测,使用 BoT-SORT 进行追踪,并通过计数线来计算蚂蚁的数量。

如果你没有付费的 Medium 账户,可以在这里免费阅读。

流程概述

在典型的视频物体计数流程中,每一帧会经历一系列的处理步骤:检测、追踪和计数。以下是每个步骤的简要概述:

  1. 检测:一个物体检测器在每一帧中识别并定位物体,生成围绕物体的边界框。

  2. 追踪:追踪器在每一帧之间跟踪这些物体,给每个物体分配唯一的 ID,确保它们只被计数一次。

  3. 计数:计数模块汇总这些信息,并将每个新物体加入,以提供准确的结果。

图片由作者提供

连接物体检测器、追踪器和计数器可能需要大量编码。幸运的是,Ultralytics 库[1]通过提供一个便捷的流水线来简化这一过程,能够无缝地集成这些组件。

1. 使用 YOLOv8 检测物体

第一步是在每一帧中检测蚂蚁,并为它们生成边界框。在本教程中,我们将使用我提前训练的 YOLOv8 检测器来检测蚂蚁。我使用了 Grounding DINO [2]来标注数据,然后使用这些标注数据训练 YOLOv8 模型。如果你想了解更多关于训练 YOLO 模型的信息,可以参考我之前的文章训练 YOLOv5,因为概念是相似的。对于你的应用,你可以使用一个预训练模型或自己训练一个自定义模型。

要开始使用,我们需要用预训练的权重初始化检测器:

from ultralytics import YOLO

# Initialize YOLOv8 model with pre-trained weights
model = YOLO("/path/to/your/yolo_model.pt")

接下来,我们将使用检测器在视频循环中的每一帧中检测蚂蚁,并将检测与追踪过程相结合。

2. 使用 BoT-SORT 追踪物体

由于蚂蚁在视频帧中出现多次,因此必须追踪每只蚂蚁并分配一个唯一的 ID,以确保每只蚂蚁只被计数一次。Ultralytics 支持 BoT-SORT [3]和 ByteTrack [4]进行追踪。

  • ByteTrack: 提供了精度和速度之间的平衡,并且具有较低的计算复杂度。它可能无法像 BoT-SORT 那样处理遮挡和摄像头运动。

  • BoT-SORT: 在追踪准确性和鲁棒性方面优于 ByteTrack,尤其是在有遮挡和摄像头运动的挑战性场景中。然而,它的代价是更高的计算复杂度和较低的帧率。

这些算法的选择取决于你应用的具体需求。

BoT-SORT 的工作原理: BoT-SORT 是一个多物体追踪器,意味着它可以同时追踪多个物体。它结合了运动和外观信息,并进行了摄像头运动补偿。物体的位置通过卡尔曼滤波器进行预测,现有轨迹的匹配基于物体的位置和视觉特征。这种方法使 BoT-SORT 即使在有遮挡或摄像头运动的情况下,也能保持准确的轨迹。

一个配置良好的追踪器可以弥补检测器的轻微故障。例如,如果物体检测器暂时未能检测到一只蚂蚁,追踪器可以通过运动和外观线索保持蚂蚁的轨迹。

检测器和追踪器在视频循环中的每一帧上都被迭代使用,以生成轨迹。这是将其集成到你的视频处理循环中的方法:

tracks = model.track(frame, persist=True, tracker=’botsort.yaml’, iou=0.2)

跟踪器配置在‘botsort.yaml’文件中定义。你可以调整这些参数以最好地适应你的需求。要将跟踪器更改为 ByteTrack,只需将‘bytetrack.yaml’传递给跟踪器参数。

确保交并比(IoU)值符合你的应用需求;IoU 阈值(用于非最大抑制)决定了检测结果必须多接近才被视为同一物体。persist=True参数告诉跟踪器当前帧是序列的一部分,并期望上一帧的轨迹在当前帧中持续存在。

3. 计数物体

现在我们已经检测并跟踪了蚂蚁,最后一步是统计在视频中穿过指定线的唯一蚂蚁。来自 Ultralytics 库的ObjectCounter类允许我们定义一个计数区域,该区域可以是线条或多边形。在本教程中,我们将使用一条简单的线作为计数区域。这种方法通过确保每只蚂蚁在穿过线时只被计数一次,减少了错误,即使由于跟踪错误它的唯一 ID 发生了变化。

首先,我们在视频循环之前初始化ObjectCounter

counter = solutions.ObjectCounter( 
  view_img=True,                     # Display the image during processing 
  reg_pts=[(512, 320), (512, 1850)], # Region of interest points 
  classes_names=model.names,         # Class names from the YOLO model 
  draw_tracks=True,                  # Draw tracking lines for objects 
  line_thickness=2,                  # Thickness of the lines drawn 
  )

在视频循环内部,ObjectCounter将计数由跟踪器生成的轨迹。线的端点作为reg_pts参数传递给计数器,格式为[(x1, y1), (x2, y2)]。当一只蚂蚁的边界框的中心点第一次穿过线时,它将根据其轨迹方向加入计数。向某一方向移动的物体被计为‘In’,而向另一方向移动的物体则计为‘Out’。

 # Use the Object Counter to count new objects 
  frame = counter.start_counting(frame, tracks)

完整代码

现在我们已经看到了计数组件,让我们将代码与视频循环集成并保存结果视频。

# Install and import Required Libraries
%pip install ultralytics
import cv2
from ultralytics import YOLO, solutions

# Define paths:
path_input_video = '/path/to/your/input_video.mp4' 
path_output_video = "/path/to/your/output_video.avi"
path_model = "/path/to/your/yolo_model.pt"

# Initialize YOLOv8 Detection Model
model = YOLO(path_model)

# Initialize Object Counter
counter = solutions.ObjectCounter( 
  view_img=True,                     # Display the image during processing 
  reg_pts=[(512, 320), (512, 1850)], # Region of interest points 
  classes_names=model.names,         # Class names from the YOLO model 
  draw_tracks=True,                  # Draw tracking lines for objects 
  line_thickness=2,                  # Thickness of the lines drawn 
  )

# Open the Video File
cap = cv2.VideoCapture(path_input_video) 
assert cap.isOpened(), "Error reading video file"

# Initialize the Video Writer to save resulted video
video_writer = cv2.VideoWriter(path_output_video, cv2.VideoWriter_fourcc(*"mp4v"), 30, (1080, 1920))

# itterate over video frames:
frame_count = 0 
while cap.isOpened(): 
  success, frame = cap.read() 
  if not success: 
    print("Video frame is empty or video processing has been successfully completed.") 
    break 

  # Perform object tracking on the current frame 
  tracks = model.track(frame, persist=True, tracker='botsort.yaml', iou=0.2) 

  # Use the Object Counter to count objects in the frame and get the annotated image 
  frame = counter.start_counting(frame, tracks) 

  # Write the annotated frame to the output video 
  video_writer.write(frame) 
  frame_count += 1

# Release all Resources:
cap.release() 
video_writer.release() 
cv2.destroyAllWindows()

# Print counting results:
print(f'In: {counter.in_counts}\nOut: {counter.out_counts}\nTotal: {counter.in_counts + counter.out_counts}')
print(f'Saves output video to {path_output_video}')

上述代码将物体检测和跟踪集成到视频处理循环中,以保存带注释的视频。使用 OpenCV,我们打开输入视频并设置输出视频的写入器。在每一帧中,我们使用 BoTSORT 执行物体跟踪,统计物体数量并注释该帧。带注释的帧包括边界框、唯一 ID、轨迹以及‘in’和‘out’计数,并保存到输出视频中。‘in’和‘out’的计数可以分别从counter.in_countscounter.out_counts中获取,并且也会打印在输出视频中。

带注释的帧。每只蚂蚁都分配了一个边界框和一个唯一 ID。蚂蚁在越过粉红色线时会被计数。蚂蚁的‘in’和‘out’计数显示在图像的角落。

结论

在带注释的视频中,我们正确地统计了总共 85 只蚂蚁,其中 34 只进入,51 只退出。为了准确计数,检测器需要表现良好,且跟踪器需要配置得当。配置良好的跟踪器能够弥补检测器的遗漏,确保跟踪的连续性。

在注释视频中,我们可以看到跟踪器很好地处理了缺失的检测,这一点可以通过一个蚂蚁周围的边界框消失以及它在后续帧中以正确的 ID 返回来证明。此外,将不同 ID 分配给同一物体的跟踪错误(例如,蚂蚁#42 变成#48)并未影响计数,因为只有穿过线的蚂蚁才会被计数。

在本教程中,我们探讨了如何使用先进的物体检测和跟踪技术计算视频中的物体。我们使用 YOLOv8 进行蚂蚁检测,并采用 BoT-SORT 进行稳健的跟踪,所有技术都无缝集成在 Ultralytics 库中。

感谢阅读!

恭喜你已经完成了整个过程。点击👍以表示感谢并提升算法的自尊心🤓

想了解更多?

  • 探索我所写的其他文章

  • 订阅以便在我发布文章时收到通知

  • Linkedin上关注我

参考文献

[1] Ultralytics GitHub

[2] Grounding DINO: 将 DINO 与基于领域的预训练相结合,用于开放集物体检测

[3] BoT-SORT: 稳健的多行人跟踪

[4] ByteTrack: 通过关联每个检测框进行多物体跟踪

精通 RAG 系统:从基础到高级,通过战略性组件评估

原文:towardsdatascience.com/mastering-rag-systems-from-fundamentals-to-advanced-with-strategic-component-evaluation-3551be31858f?source=collection_archive---------3-----------------------#2024-04-09

提升你的 RAG 系统:通过 LLM 评估进行高级增强的逐步指南,附带实际数据使用案例

Hamza GharbiTowards Data Science Hamza Gharbi

·发表于Towards Data Science ·29 分钟阅读·2024 年 4 月 9 日

--

由 DALL-E 生成的图像。

本文将引导你通过使用llama-index框架构建一个先进的检索增强生成(RAG)管道。

检索增强生成(RAG)系统是一个框架,它通过使用外部来源的信息,使生成性 AI 模型更准确、更可靠。在本项目中,法律文档将作为外部知识库使用。

在本教程中,我们将从建立一个基本的 RAG 系统开始,然后再说明如何加入高级特性。构建此类系统的挑战之一是决定管道中最佳的组件。我们将通过评估管道中的关键组件来尝试回答这个问题。

本文作为实现 RAG 系统的实用教程,包括它们的评估。尽管它没有深入探讨理论方面的内容,但它会尽可能详细地解释文章中使用的概念。

材料表

· 概述

· 设置

∘ 1- 代码

∘ 2- 数据

∘ 3- 原始数据转换

· 基础 RAG 系统

∘ 1- 数据摄取

∘ 2- 索引和存储

∘ 3- 查询

∘ 4- 评估

· 评估嵌入

· 评估高级特性

∘ 1- 窗口化

∘ 2- 混合搜索

∘ 3- 查询重写

· 路由

· 结论

· 参考资料

· 联系

概述

本文按以下步骤展开:

1- 我们将从使用法国《民法典》数据构建一个基础的 RAG 系统开始。

2- 接下来,我们将比较 OpenAI、Mistral 和开源模型的各种嵌入方法,评估它们的上下文相关性。

3- 我们将探讨像窗口化混合搜索查询重写等高级概念,以改进 RAG 系统。这些技术的有效性将通过一组评估查询进行评估。

4- 最后,我们将索引更多的法律条文,并演示如何使用 llama-index 的路由功能来选择适当的索引并获得正确的响应。

设置

1- 代码

要访问本项目的代码,请执行以下命令,从 GitHub 克隆相应的仓库:

git clone git@github.com:HamzaG737/legal-code-rag.git

接下来,你需要安装必要的包。我们使用了 poetry 作为我们的包管理器,以更好地处理项目依赖。使用以下命令安装 poetry。

curl -sSL https://install.python-poetry.org | python3 -

你可以在 这里了解更多关于 poetry 安装的信息。

然后,在项目根目录下,使用以下命令安装 Python 包:

poetry install

对于本项目,要求运行的 Python 环境版本在 3.9 至 3.11 之间。我们强烈建议创建虚拟环境来隔离项目的包依赖,确保它们不会与系统中全局安装的包发生冲突。

本项目还需要Docker

最后,确保定义了 OPENAI_API_KEY 环境变量,因为我们将在本项目中使用 OpenAI 的 gpt-3.5-turbo LLM 和 Ada 嵌入。如果你有兴趣使用 Mistral 嵌入进行实验,你必须从 Mistral 平台 获取 API 密钥。然后,你需要创建相应的环境变量 MISTRAL_API_KEY

2- 数据

我们 RAG 的知识库由法国法律条文中的示例组成。法律条文是一个全面的立法文件,旨在权威且逻辑地阐明某一特定法律领域的原则和规则。

例如,民法典旨在改革和编纂与私人或民事法相关的法国法律。包括财产、合同、家庭法和个人身份等领域。

一般来说,法律条文是一组有序的条款,通常与某些元数据(如章节、标题、节等)相关联……

目前,法国大约有 78 部有效的法律代码。法国政府通过一个名为 Légifrance 的网站免费发布这些法律代码。

对于本项目,你有两种方法可以创建知识库:

  • 从 Legifrance API 加载最新数据,该 API 具有开放数据状态。你可以在这里找到创建 API 密钥的说明。然后,我们将使用 Python 库pylegifrance从 API 请求特定的法律条文。

  • ./data/legifrance/{code_name}.json加载处理后的数据。

由于法律条文频繁变化,我们建议直接从 API 加载数据,如果你希望获取最新版本的法律条文。然而,创建 API 密钥可能有些繁琐。如果你时间紧迫且不需要最新内容,可以选择本地加载数据,这是默认设置。

如果你有 API 密钥并且希望重新加载数据,可以在创建查询引擎时将reload_data参数设置为True。该引擎代表我们的端到端 RAG 管道(稍后我们会解释查询引擎的概念)。

3- 原始数据转换

本节面向那些有兴趣了解我们如何从 Legifrance API 转换数据的读者。

./data_ingestion/preprocess_legifrance_data.py中,我们使用以下步骤对来自 API 的数据进行预处理:

  • 我们从 API 请求某个法律条文的内容。

  • 我们递归地从 API 响应的 JSON 中检索文章内容。

  • 我们去重文章并进行一些清理,如去除部分 HTML 标签、修剪文本等……

我们在此过程中最终获得的数据是一系列文章,其中每篇文章由其内容、元数据(例如标题、章节、段落等)以及其编号表示。例如:

{'content': "Ni le propriétaire, ni l'usufruitier, ne sont tenus de rebâtir ce qui est tombé de vétusté, ou ce qui a été détruit par cas fortuit.",
 'num': '607',
 'livre': 'Des biens et des différentes modifications de la propriété',
 'titre': "De l'usufruit, de l'usage et de l'habitation",
 'chapitre': "De l'usufruit",
 'section': "Des obligations de l'usufruitier"}

这是英文翻译:

{
  "content": "Neither the owner nor the usufructuary are required to rebuild what has fallen into disrepair or what has been destroyed by an act of chance.",
  "num": "607",
  "book": "Of Property and the Various Modifications of Ownership",
  "title": "Of Usufruct, Use and Habitation",
  "chapter": "Of Usufruct",
  "section": "The Obligations of the Usufructuary"
}

一篇文章将作为数据的基本单元,并将封装在我们称之为节点的结构中。更多内容将在下一节讨论。

基本的 RAG 系统

一个基本的 RAG 系统包含四个重要步骤。我们将逐一考察这些步骤,并说明它们在我们项目中的应用。

法律条文上应用的基本 RAG 系统示意图。图片由作者使用Diagrams: Show Me GPT生成。

1- 数据摄取

此阶段涉及从各种来源(如 PDF 文件、数据库、API、网站等)收集和预处理相关数据。此阶段与两个关键概念密切相关:文档节点

在 LlamaIndex 的术语中,文档是指封装任何数据源的容器,如 PDF 文件、API 输出或从数据库中检索的数据。而节点则是 LlamaIndex 中数据的基本单元,代表文档源的一个“块”。节点携带元数据,将它们与所属的文档以及其他节点关联起来。

在我们的项目中,文档将是完整的《民法典》文本,而节点则是该法典的一条条文。然而,由于我们通过 API 已经获得了按条文划分的不同文章数据,因此不需要对文档进行分块,从而避免了由此过程引起的所有错误。

创建节点的代码如下:

from dataclasses import dataclass
from typing import List
import os
from uuid import uuid4
from llama_index.core.schema import TextNode

from loguru import logger

from utils import load_json
from .preprocess_legifrance_data import get_code_articles
from .constants import path_dir_data

@dataclass
class CodeNodes:
    code_name: str
    max_words_per_node: int = 4000
    _n_truncated_articles: int = 0
    ...

    def __post_init__(self):
        self.articles = self.try_load_data()
        self.nodes = self.create_nodes(self.articles)
        ...

    def create_nodes(self, list_articles: List[dict]):
        nodes = [
            TextNode(
                text=article["content"],
                id_=str(uuid4()),
                metadata=self._parse_metadata(article),
            )
            for article in list_articles
        ]

        return nodes

    def try_load_data(self) -> List[dict]:
        path = os.path.join(path_dir_data, f"{self.code_name}.json")
        try:
            code_articles = load_json(path=path)
        except FileNotFoundError:
            logger.warning(
                f"File not found at path {path}. Fetching data from Legifrance."
            )
            code_articles = get_code_articles(code_name=self.code_name)
        truncated_articles = self._chunk_long_articles(code_articles)
        return truncated_articles

    def _parse_metadata(self, article: dict) -> dict:
        metadata = {k: v for k, v in article.items() if k not in ["content", "num"]}
        metadata = {
            "Nom du code": self.code_name,
            **metadata,
            "Article numero": article["num"],
        }
        return metadata

    def _chunk_long_articles(self, articles: List[dict]) -> List[dict]:
        ...
  • 首先,我们加载 try_load_data 中的文章,如前一节所述。

  • 我们将长文章分割成“子文章”,以便我们能够无截断地嵌入它们。这是在 _chunk_long_articles 中完成的。分块的完整代码可以在data_ingestion/nodes_processing.py中找到。

  • 我们使用 llama-index 的 TextNode 类创建节点。在这一步结束时,我们的数据由一个节点列表组成,每个节点都有文本、ID 和其元数据。例如:

TextNode(
  id_='c0ee164f-ae58-47c8-8e5b-e82f55ac98a9',
  embedding=None,
  metadata={
    'Nom du code': 'Code civil',
    'livre': 'Des différentes manières dont on acquiert la propriété',
    'titre': 'Des libéralités',
    'chapitre': 'Des dispositions testamentaires.',
    'section': 'Du legs universel.',
    'id': '1003',
    'Article numero': '1003'
  },
  excluded_embed_metadata_keys=[],
  excluded_embed_metadata_keys=[],
  relationships={},
  text="Le legs universel est la disposition testamentaire par laquelle le testateur donne à une ou plusieurs personnes l'universalité des biens qu'il laissera à son décès.",
  start_char_idx=None,
  end_char_idx=None,
  text_template='{metadata_str}\n\n{content}',
  metadata_template='{key}: {value}',
  metadata_seperator='\n'
) 

元数据将用于嵌入和 LLM 上下文。具体来说,我们将把元数据作为键值对嵌入,并与内容连接,遵循 text_template 格式。此格式也将用于在形成 LLM 上下文时表示节点。如果需要,您可以通过更改 excluded_embed_metadata_keys 和/或 excluded_embed_metadata_keys 来排除部分或全部元数据,用于嵌入或 LLM。

2- 索引与存储

索引是创建一种数据结构的过程,该结构能够支持数据查询。通常这包括生成向量嵌入,这些嵌入是数值表示,能够捕捉数据的核心本质。此外,可以采用多种元数据策略,以有效地检索与上下文相关的信息。嵌入模型不仅用于在索引构建阶段嵌入文档,还用于嵌入我们将来使用查询引擎进行的任何查询。

存储是后续阶段,其中创建的索引和其他元数据将被保存,以防止重新索引的需要。这确保了一旦数据被组织和索引,它就可以访问并检索,而无需重新进行索引过程。

向量嵌入可以存储并持久化在一个 向量数据库 中。在下一节中,我们将看到如何在本地创建和运行这个数据库。

a- 设置向量数据库

Qdrant 将作为向量数据库,用于存储和索引文章的嵌入及其元数据。我们将使用 Qdrant 官方的 Docker 镜像在本地运行服务器。

首先,你可以从 Dockerhub 拉取镜像:

docker pull qdrant/qdrant

随后,运行以下命令启动 Qdrant 服务。这也将映射必要的端口,并指定一个本地目录(./qdrant_storage)用于数据存储:

docker run -p 6333:6333 -p 6334:6334 \
    -v $(pwd)/qdrant_storage:/qdrant/storage:z \
    qdrant/qdrant

设置完成后,我们可以使用 Python 客户端与 Qdrant 服务进行交互:

from qdrant_client import QdrantClient

client = QdrantClient("localhost", port=6333)

b- 索引与存储数据

以下是用于索引和存储数据的代码。

from llama_index.core import VectorStoreIndex
from llama_index.core import StorageContext
from llama_index.embeddings.mistralai import MistralAIEmbedding
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.embeddings.fastembed import FastEmbedEmbedding
from llama_index.vector_stores.qdrant import QdrantVectorStore
from qdrant_client import QdrantClient
from qdrant_client.http.exceptions import UnexpectedResponse
from loguru import logger

from data_ingestion.nodes_processing import CodeNodes

def index_given_nodes(
    code_nodes: CodeNodes,
    embedding_model: MistralAIEmbedding | OpenAIEmbedding | FastEmbedEmbedding,
    hybrid_search: bool,
    recreate_collection: bool = False,
) -> VectorStoreIndex:
    """
    Given a list of nodes, create a new index or use an existing one. 

    Parameters
    ----------
    code_nodes : CodeNodes
        The nodes to index.
    embedding_model : MistralAIEmbedding | OpenAIEmbedding | FastEmbedEmbedding
        The embedding model to use.
    hybrid_search : bool
        Whether to enable hybrid search.
    recreate_collection : bool, optional
        Whether to recreate the collection, by default False.

    """

    collection_name = code_nodes.nodes_config
    client = QdrantClient("localhost", port=6333)
    if recreate_collection:
        client.delete_collection(collection_name)

    try:
        count = client.count(collection_name).count
    except UnexpectedResponse:
        count = 0

    if count == len(code_nodes.nodes):
        logger.info(f"Found {count} existing nodes. Using the existing collection.")
        vector_store = QdrantVectorStore(
            collection_name=collection_name,
            client=client,
            enable_hybrid=hybrid_search,
        )
        return VectorStoreIndex.from_vector_store(
            vector_store,
            embed_model=embedding_model,
        )
    logger.info(
        f"Found {count} existing nodes. Creating a new index with {len(code_nodes.nodes)} nodes. This may take a while."
    )
    if count > 0:
        client.delete_collection(collection_name)

    vector_store = QdrantVectorStore(
        collection_name=collection_name,
        client=client,
        enable_hybrid=hybrid_search,
    )
    storage_context = StorageContext.from_defaults(vector_store=vector_store)
    index = VectorStoreIndex(
        code_nodes.nodes,
        storage_context=storage_context,
        embed_model=embedding_model,
    )
    return index

索引函数的逻辑非常简单:

  • 首先,我们创建 Qdrant 客户端来与向量数据库进行交互。

  • 我们检查所需集合中的节点数量。该集合名称由code_nodes.nodes_config提供,代表当前实验(即代码名称、嵌入方法以及可能的高级 RAG 技术,如base, window-nodes, hybrid search… 更多关于这些实验的内容稍后介绍)。如果节点数量与当前数量不同,我们将创建一个新索引:

storage_context = StorageContext.from_defaults(vector_store=vector_store)
index = VectorStoreIndex(
        code_nodes.nodes,
        storage_context=storage_context,
        embed_model=embedding_model,
    )

否则我们返回现有的索引:

index = VectorStoreIndex.from_vector_store(
            vector_store,
            embed_model=embedding_model,
        )

创建或检索索引的完整代码可以在./retriever/get_retriever.py模块中找到。你还可以在 llama-index 文档中找到更多关于此阶段的信息。

3- 查询

给定用户查询,我们计算查询嵌入与索引节点嵌入之间的相似度,以找到最相似的数据。这些相似节点的内容随后被用来生成上下文。这个上下文使得语言模型能够为用户合成一个响应。

我们基于上一节创建的索引定义一个查询引擎。这个引擎是一个端到端管道,接收自然语言查询并返回响应,以及检索到并传递给 LLM 的上下文。

from llama_index.core.query_engine import BaseQueryEngine
from llama_index.core import VectorStoreIndex
from llama_index.core import PromptTemplate

def get_query_engine_based_on_index(
    index: VectorStoreIndex,
    similarity_top_k: int = 5,
) -> BaseQueryEngine:

    query_engine = index.as_query_engine(similarity_top_k=similarity_top_k)

    query_engine = update_prompts_for_query_engine(query_engine)
    return query_engine

update_prompts_for_query_engine是一个函数,允许我们更改响应合成器的提示,即负责将上下文作为输入并为用户生成答案的 LLM。

def update_prompts_for_query_engine(query_engine: BaseQueryEngine) -> BaseQueryEngine:

    new_tmpl_str = (
        "Below is the specific context required to answer the upcoming query. You must base your response solely on this context, strictly avoiding the use of external knowledge or assumptions..\n"
        "---------------------\n"
        "{context_str}\n"
        "---------------------\n"
        "Given this context, please formulate your response to the following query. It is imperative that your answer explicitly mentions any relevant code name and article number by using the format 'according to code X and article Y,...'. Ensure your response adheres to these instructions to maintain accuracy and relevance."
        "Furthermore, it is crucial to respond in the same language in which the query is presented. This requirement is to ensure the response is directly applicable and understandable in the context of the query provided."
        "Query: {query_str}\n"
        "Answer: "
    )
    new_tmpl = PromptTemplate(new_tmpl_str)
    query_engine.update_prompts({"response_synthesizer:text_qa_template": new_tmpl})
    return query_engine

在这个新模板中,我们强调防止幻觉的必要性。我们还指示 LLM 在提供响应之前始终参考代码名称,如民法典,以及条文号。此外,我们指示它用查询的语言进行回复,以支持多语言查询。

create_query_engine函数在query_engine模块中是创建 RAG 管道的入口。此函数的参数定义了 RAG 配置参数。

以下代码创建一个基本的查询引擎,并根据特定查询生成响应:

from query.query_engine import create_query_engine

query_engine = create_query_engine()

response = query_engine.query("What are the conditions required for a marriage to be considered valid ?")

print(response)

你可以在./query/query_engine.py中找到创建查询引擎的完整代码。更多关于 llama-index 查询引擎的信息可以在这里找到。

4- 评估

评估对于评估 RAG 管道的性能至关重要,并验证关于其组件所做的决策。

在这个项目中,我们将采用基于 LLM 的评估方法来评估结果的质量。这项评估将涉及一个金标准 LLM,按照一定的指标分析一组响应,以确保 RAG 管道的有效性和准确性。

通常,RAG 系统可以通过两种方式进行评估:

  • 响应评估:响应是否与检索到的上下文和查询一致?

  • 检索评估:检索到的数据源是否与查询相关?

LLama-index提供了几种评估器来计算不同的指标,如忠实度上下文相关性回答相关性。您可以在评估部分找到有关这些指标的更多细节。

这是在此项目中创建评估器的过程:

  • 最初,我们使用 LLM(在我们这个案例中是 gpt-4)生成n个问题,关于我们想要评估的法律代码。出于成本考虑,我们设置了n=50,但更大的问题数量可能会提供对系统性能更有信心的评估。一个 LLM 生成的问题示例是:“法国重婚的法律后果是什么?”或者法语版本:“Quelles sont les conséquences juridiques de la bigamie en France ?”。请注意,这种方法有其局限性,因为 LLM 生成的问题可能无法反映真实用户提问的实际分布。您可以在evaluation/generate_questions.py找到问题生成模块。

  • 使用生成的问题列表、评估指标和查询引擎,我们生成响应列表。每个响应由评估器在 0 到 1 之间打分,评估器还提供反馈文本来解释其评分。

  • llama-index提供了不同指标的评估器,例如ContextRelevancyEvaluator用于计算上下文相关性。此外,我们重写了这些评估器的evaluate_response方法,以在嵌入和创建上下文时考虑元数据。

from typing import Any, Optional, Sequence

from llama_index.core.evaluation import ContextRelevancyEvaluator, EvaluationResult
from llama_index.core.response import Response
from llama_index.core.schema import MetadataMode

class CustomContextRelevancyEvaluator(ContextRelevancyEvaluator):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def evaluate_response(
        self,
        query: Optional[str] = None,
        response: Optional[Response] = None,
        metadata_mode: MetadataMode = MetadataMode.ALL,
        **kwargs: Any,
    ) -> EvaluationResult:
        """Run evaluation with query string and generated Response object.

        Subclasses can override this method to provide custom evaluation logic and
        take in additional arguments.
        """
        response_str: Optional[str] = None
        contexts: Optional[Sequence[str]] = None
        if response is not None:
            response_str = response.response
            contexts = [
                node.get_content(metadata_mode=metadata_mode)
                for node in response.source_nodes
            ]

        return self.evaluate(
            query=query, response=response_str, contexts=contexts, **kwargs
        )
  • 以下是从特定评估器(例如上下文相关性评估器)获取评估结果的代码。值得注意的是,您可以以异步模式生成这些评估,以便更快地获得结果。然而,由于 OpenAI 的速率限制,我们是按顺序执行这些评估的。
from typing import Literal, List

from llama_index.core.query_engine import BaseQueryEngine
from llama_index.llms.openai import OpenAI
from llama_index.core.evaluation import EvaluationResult
from llama_index.core.schema import MetadataMode
from tqdm import tqdm

from utils import load_json
from evaluation.custom_evaluators import CustomContextRelevancyEvaluator

PATH_EVAL_CODE_CIVIL = "./data/questions_code_civil.json"
PATH_EVAL_CODE_DE_LA_ROUTE = "./data/questions_code_de_la_route.json"

def evaluate_one_metric(
    query_engine: BaseQueryEngine,
    code_name: Literal["code_civil", "code_de_la_route"],
    metadata_mode: MetadataMode,
    llm_for_eval: str = "gpt-3.5-turbo",
) -> List[EvaluationResult]:

    evaluator = CustomContextRelevancyEvaluator(
        llm=OpenAI(temperature=0, model=llm_for_eval)
    )

    eval_data: List[str] = load_json(globals()[f"PATH_EVAL_{code_name.upper()}"])

    results = []
    for question in tqdm(eval_data):
        response = query_engine.query(question)
        eval_result = evaluator.evaluate_response(
            query=question, response=response, metadata_mode=metadata_mode
        )

        results.append(eval_result)

    return results
  • 最后,为了获取某个管道(由其查询引擎定义)的得分,我们计算评估器给每个查询的分数的平均值。

评估的完整代码位于evaluation/eval_with_llamaindex.py

对于即将进行的 RAG 管道评估,我们将使用由 GPT-4 基于法国民法典生成的 50 个问题。这些问题可以在./data/questions_code_civil.json找到。用于评估的金标准 LLM 是gpt-3.5-turbo

评估嵌入

所有的评估实验可以在这个 notebook 中找到。

我们将首先评估民法典条文(或节点)的嵌入。为了选择我们将要评估的嵌入模型集,我们依赖于 Huggingface 的排行榜,用于在法语数据上的检索。因此,我们将评估这三种模型:

  • Text-Embedding-Ada-002来自 OpenAI,是目前(撰写本文时)在此任务中最先进的技术。

  • mistral-embed来自 Mistral AI,紧随其后。

  • multilingual-e5-large来自 Infloat,作为开源候选者。我们还将使用 Qdrant 的fast-embed框架来提高嵌入创建的效率,主要通过量化权重并使用 ONNX 作为运行时。

我们可以通过导入相应的集成来使用 llama-index 生成这些嵌入。例如,我们导入FastEmbedEmbedding模块,使用fast-embed框架生成e5-large嵌入:

from llama_index.embeddings.fastembed import FastEmbedEmbedding

embeddings_model_name = "intfloat/multilingual-e5-large"
embed_model = FastEmbedEmbedding(model_name=embeddings_model_name)

完整的嵌入定义可以在retriever/embeddings.py中找到。

我们将衡量的指标是上下文相关性,即检索到的上下文与用户查询的相关性。

这是启动本次实验评估的代码。

from query.query_engine import create_query_engine
from evaluation.eval_with_llamaindex import evaluate_multiple_experiments

query_engine_fastembed = create_query_engine(embedding_model="intfloat/multilingual-e5-large")
query_engine_mistral = create_query_engine(embedding_model="mistral-embed")
query_engine_ada = create_query_engine(embedding_model="text-embedding-ada-002")

exp_to_query_engine = {
    "mistral_embed": query_engine_mistral,
    "fastembed": query_engine_fastembed,
    "ada": query_engine_ada,
}

scores_df, deeps_df = evaluate_multiple_experiments(
    experiment_to_query_engine=exp_to_query_engine,
    general_exp_name="embeddings",
    list_metrics=["context_relevancy"],
    code_name="code_civil"
)

现在这是最终的结果表格。embedding_time字段表示我们将约2800篇《民法典》文章进行嵌入所花费的时间。

我们观察到,text-embedding-data-002在得分和嵌入时间上都优于其他两个嵌入模型。mistral-embedadamultilingual-e5-large之间的差距相当显著。请注意,这些嵌入方法的排名与之前提到的排行榜一致。

然而,请记住,我们的评估仅基于由 LLM 生成的 50 个问题。因此,它可能无法完全代表真实场景中的表现,尤其是在区分mistral-embedada时,因为这两者之间的差距相对较小。

评估高级功能

1- 窗口化

我们将探索的第一个高级功能是通过前后相邻节点的内容来增强节点内容。在法律领域中,这种做法是有意义的,因为相邻的条款通常紧密相关。

我们将使用k来参数化窗口化,表示当前节点前后需要添加的节点数量。例如,k=1意味着当前节点的内容将与前后节点一起增强。

对于给定的节点,将使用原始内容进行嵌入,而增强后的内容将用于生成响应的上下文。

下面是我们如何进行节点窗口化的:

  • 我们创建了一个自定义函数来生成增强后的节点。尽管 Llama-index 有其SentenceWindowNodeParser类来执行此任务,但它要求输入的是完整文档,然后进行分割以创建节点。因此,我们从这个类获得灵感,创建了自己的窗口解析器。
from typing import List
from llama_index.core.schema import TextNode
from tqdm import tqdm

from .constants import possible_headers

WINDOW_METADATA_KEY = "window"
ORIGINAL_TEXT_METADATA_KEY = "original_text"

def add_window_nodes(nodes: List[TextNode], window_size: int = 3):
    for i, node in tqdm(
        enumerate(nodes), total=len(nodes), desc="Adding window nodes ..."
    ):
        window_nodes = nodes[
            max(0, i - window_size) : min(i + window_size + 1, len(nodes))
        ]

        node.metadata[WINDOW_METADATA_KEY] = "\n".join(
            [n.get_content("llm") for n in window_nodes]
        )
        node.metadata[ORIGINAL_TEXT_METADATA_KEY] = node.text

        # exclude window metadata from embed and llm
        node.excluded_embed_metadata_keys.extend(
            [WINDOW_METADATA_KEY, ORIGINAL_TEXT_METADATA_KEY]
        )

        node.excluded_llm_metadata_keys.extend(
            [WINDOW_METADATA_KEY, ORIGINAL_TEXT_METADATA_KEY]
        )

    # since articles metadata (like title, chapter, etc ...) will be incorporated in WINDOW_METADATA_KEY,
    # we can exclude them from the llm metadata.

    for node in nodes:
        node.excluded_llm_metadata_keys.extend(possible_headers)

    return nodes
  • 然后我们修改之前创建的CodeNodes类,增加使用相邻节点进行增强的功能:
from dataclasses import dataclass
from typing import List
from llama_index.core.postprocessor import MetadataReplacementPostProcessor

from loguru import logger

from .window_nodes import add_window_nodes

@dataclass
class CodeNodes:
    code_name: str
    use_window_nodes: bool
    nodes_window_size: int = 3
    max_words_per_node: int = 4000
    _n_truncated_articles: int = 0

    def __post_init__(self):
        self.articles = self.try_load_data()
        self.nodes = self.create_nodes(self.articles)
        code_name_no_spaces = self.code_name.replace(" ", "_")
        self.nodes_config = f"{code_name_no_spaces}_base"
        self.post_processors = []
        if self.use_window_nodes:
            logger.info("Adding window nodes ...")
            self.nodes = add_window_nodes(self.nodes, self.nodes_window_size)
            self.nodes_config = f"{code_name_no_spaces}_window"
            self.post_processors.append(
                MetadataReplacementPostProcessor(target_metadata_key="window")
            )

    def create_nodes(self, list_articles: List[dict]): ...

    def try_load_data(self) -> List[dict]: ...

    def _parse_metadata(self, article: dict) -> dict: ...

    def _chunk_long_articles(self, articles: List[dict]) -> List[dict]: ...
  • 一个关键点是MetadataReplacementPostProcessor。这个类用于指示我们需要在将检索到的数据传递给 LLM 之前,将节点内容替换为在target_metadata_key字段中找到的内容。因此,这个字段将包含增强后的内容。

  • 最后,我们需要将后处理器集成到查询引擎中:

from llama_index.core.query_engine import BaseQueryEngine
from llama_index.core import VectorStoreIndex

def get_query_engine_based_on_index(
    index: VectorStoreIndex,
    postprocessors_list: list,
    similarity_top_k: int = 5,
) -> BaseQueryEngine:

    query_engine = index.as_query_engine(
        similarity_top_k=similarity_top_k, node_postprocessors=postprocessors_list
    )

    query_engine = update_prompts_for_query_engine(query_engine)
    return query_engine

在此实验中,我们将比较基本 RAG 管道与另外两个管道。这些额外的管道分别使用 k=1k=2 的节点窗口。我们将使用相同的一组问题和上下文相关性指标来评估这些管道。以下是结果:

我们可以观察到,通过增加邻近节点的内容来增强节点的上下文相关性得分。小幅的提升可能是由于已经很高的得分(0.89)。

我们还将衡量本次实验的忠实度。在 llama-index 中,FaithfulnessEvaluator 模块用于判断查询引擎的回答是否与任何源节点匹配,从而确定回答是否为幻觉。需要注意的是,该评估器提供一个二元得分(如果回答是幻觉则为 0,否则为 1)。这与上下文相关性得分不同,后者的范围是 0 到 1。

以下是结果:

在我们对 50 个问题的评估中,我们发现基本实验中只有一个回答是幻觉,而在使用k=2的窗口管道中有五个幻觉。这表明,添加更多上下文,特别是无关上下文,可能会诱发更多幻觉。

在随后的实验中,我们将保持基本设置,不进行节点扩展。这个决定基于我们的观察,即相关性的小幅增加并不足以抵消回答中的更高幻觉率。

2- 混合搜索

混合搜索是指结合稀疏稠密向量的搜索结果。

稠密向量将数据点表示为连续值的紧凑向量,提供对数据特征的丰富且微妙的理解。这些向量通常是通过深度学习模型(如 OpenAI、Mistral、e5-large 等)生成的……到目前为止,我们在检索阶段仅使用了稠密向量。

相比之下,稀疏向量主要是零,通过诸如 TF-IDF、BM25 等专业模型生成。它们擅长识别特定关键词和细节,使其与更具语义丰富性的稠密向量有所不同。

为了在我们的 RAG 管道中实现混合搜索,我们需要修改代码的某些部分:

  • 首先,我们需要通过在 Qdrant 客户端定义中设置enable_hybrid=True来启用混合搜索。这将使用 Huggingface 中的 "naver/efficient-splade-VI-BT-large-doc" 模型在本地生成稀疏向量,同时使用 OpenAI 的 ada 模型生成常规的稠密向量。
vector_store = QdrantVectorStore(
    collection_name=collection_name,
    client=client,
    enable_hybrid=True,
)
  • 在定义query_engine = index.as_query_engine(**kwargs)时,我们需要同时定义sparse_top_ksimilarity_top_ksparse_top_k表示每个稀疏和密集查询将检索多少个节点。例如,设置sparse_top_k=5意味着我们将使用稀疏向量检索 5 个节点,使用密集向量检索 5 个节点。similarity_top_k控制返回节点的最终数量。在上述设置中,我们最终会得到 10 个节点。接着,应用一种融合算法来对来自不同向量空间的节点进行排序和排列(此处使用的是相对评分融合)。similarity_top_k=5意味着返回融合后的前五个节点。以下是添加这些选项后的query_engine的新定义:
query_engine = index.as_query_engine(
    similarity_top_k=5, 
    sparse_top_k=5, 
    vector_store_query_mode="hybrid", 
    **kwargs
)
  • 我们可以通过调整alpha参数来调整向量搜索和关键词搜索的权重。alpha等于 1 表示纯向量搜索,而alpha等于 0 表示纯关键词搜索。以下是结合此alpha参数后的query_engine最终定义:
query_engine = index.as_query_engine(
    similarity_top_k=5, 
    sparse_top_k=5, 
    alpha=0.5,
    vector_store_query_mode="hybrid", 
    **kwargs
)

为了评估混合搜索的相关性,我们进行了三次不同alpha参数的实验:alpha = 0.2, 0.5, 0.8。请注意,基础实验对应alpha=1,表示纯向量搜索。

这是关于上下文相关性和忠实度指标的最终结果。请注意,使用"naver/efficient-splade-VI-BT-large-doc"模型嵌入~2800个节点大约花费了我们27 分钟,并且该过程在没有 GPU 的 M1 Mac 上进行,几乎占用了所有系统内存。因此,您可能需要 GPU 来加速嵌入过程。

alpha=0.8的混合搜索在上下文相关性评分上略有提升,但将更多权重放在关键词搜索上会降低评分。忠实度评分总体保持不变。

请记住,评估问题相对一般,更偏向语义搜索而非精确匹配搜索。然而,在现实场景中,用户可能会要求查找代码中的特定文章,例如“文章 x.y 讨论了什么?”在这种情况下,关键词搜索可能会有所帮助。因此,尽管与纯向量搜索相比增加了额外的幻觉案例,我们仍将保留alpha=0.8的混合搜索供未来实验使用。

3- 查询重写

查询重写涉及生成与特定查询相似的各种问题。此过程可用于消歧义、错误修正或将查询适应于支持 RAG 系统的特定知识库。

我们将使用来自 llama-index 的QueryFusionRetriever进行查询重写。这个模块生成与用户查询相似的查询,从每个生成的查询中检索并重新排序前n个节点,包括原始查询,使用Reciprocal Rerank Fusion算法。该方法在这篇论文中有详细介绍,提供了一种高效的方式来重新排序检索到的节点,而不会产生过多的计算或依赖外部模型。

以下是此任务的 Python 代码(位于query/query_engine.py):

from llama_index.core.query_engine import BaseQueryEngine, RetrieverQueryEngine
from llama_index.core import VectorStoreIndex
from llama_index.core.retrievers import QueryFusionRetriever

from query.constants import QUERY_GEN_PROMPT

def get_query_fusion_retrieval(
    index: VectorStoreIndex,
    postprocessors_list: list,
    similarity_top_k: int = 5,
    sparse_top_k: int = 0,
    hybrid_search_alpha: float = 0.5,
    hybrid_search: bool = False,
    num_generated_questions: int = 4,
) -> BaseQueryEngine:
    kwargs = {"similarity_top_k": similarity_top_k}
    if hybrid_search:
        kwargs.update(
            {
                "vector_store_query_mode": "hybrid",
                "sparse_top_k": sparse_top_k,
                "alpha": hybrid_search_alpha,
            }
        )

    retriever = index.as_retriever(**kwargs)

    retriever = QueryFusionRetriever(
        [retriever],
        similarity_top_k=similarity_top_k,
        num_queries=num_generated_questions,  # set this to 1 to disable query generation
        mode="reciprocal_rerank",
        use_async=False,
        verbose=False,
        query_gen_prompt=QUERY_GEN_PROMPT,
    )

    query_engine = RetrieverQueryEngine.from_args(
        retriever=retriever, node_postprocessors=postprocessors_list
    )
    query_engine = update_prompts_for_query_engine(query_engine)
    return query_engine
  • num_queries表示总查询数:num_queries-1个生成的查询,加上1个原始查询。对于后续实验,我们设置num_queries=4

  • QUERY_GEN_PROMPT是专门用于生成类似查询的提示。我们为民法典知识库创建了这个提示的自定义版本。

QUERY_GEN_PROMPT = (
    "You are a helpful assistant that generates multiple search queries based on a "
    "single input query. Generate {num_queries} search queries, one on each line, "
    "related to the following input query. The queries must be in French and specifically "
    "adapted to query the French Civil Code, and they must address the ambiguities in the input query:\n"
    "Query: {query}\n"
    "Queries:\n"
)

直到现在,我们使用的评估问题已经很干净,并且针对民法典进行了定制,因为我们指示 GPT-4 按照这种方式生成它们。然而,正如我们之前提到的,这可能无法准确代表现实中的用户,因为他们可能会提出模糊、容易出错或简短的问题。因此,我们使用 GPT-4 生成了另外50个问题,并指示 LLM 生成具有这些更现实特征的问题。以下是一些例子:

[
    "C'est quoi un contrat de mariage?",
    "On peut changer de prénom facilement?",
    "Qu'est-ce qui se passe si on trouve un trésor chez quelqu'un d'autre?",
    "Comment on fait pour adopter un enfant?",
    "C'est quoi exactement une servitude?",
    "Si je construis un truc chez le voisin sans faire exprès, je dois le démolir?",
]

或者是英文翻译:

[
    "What is a marriage contract?",
    "Can we change our first name easily?",
    "What happens if we find a treasure on someone else's property?",
    "How do we adopt a child?",
    "What exactly is an easement?",
    "If I accidentally build something on the neighbor's property, do I have to demolish it?",
]

我们将使用这个新的评估数据集来比较带有和不带有查询重写的管道的表现。比较将基于两个指标:准确性答案相关性。类似于上下文相关性,答案相关性衡量答案与用户查询的相关性,得分范围从 0 到 1。结果如下:

我们观察到,查询重写不仅提高了准确性,模型在 50 个问题中完全没有虚构内容,而且稍微提高了答案相关性得分。

下面是一个示例,展示了原始问题如何被重写成另外三个问题:

  • 原始问题:我可以拒绝继承吗?

  • 三个生成的查询:

 1 - What are the legal procedures for refusing an inheritance according to the French Civil Code?
2 - What are the rights and obligations of an heir who wishes to renounce a succession in France?
3 - How does the renunciation of an inheritance proceed under the French Civil Code?

路由

到目前为止,我们的知识库中每个 RAG 管道只包含一个合法代码。自然地,人们可能会想,如何添加更多的合法代码,例如刑法或交通法规,以及如何构建一个系统,当给定查询时,能够从正确的法律代码中检索数据。我们可以通过至少三种方式使用 llama-index 来实现这一目标:

  • 将不同法律代码中的所有数据存储在同一个索引中,并像以前一样构建查询引擎。

  • 将所有数据存储在同一个索引中,但将法律代码名称作为元数据。在查询时,llama-index 的[Auto-retriever](https://docs.llamaindex.ai/en/stable/examples/vector_stores/chroma_auto_retriever/)模块推断出一组元数据过滤器和适当的查询字符串,以传递给向量数据库。auto-retriever必须确定正确的代码名称,然后在相应的节点上执行相似度搜索以检索数据。

  • 将每个法律代码节点存储在单独的索引中,并使用 llama-index 的Routing功能选择最相关的索引或多个索引。

在这个项目中,我们选择了最后一个选项。更有效的方法是评估这三种选择并选出最佳方案。然而,由于时间和空间的限制,我们没有进行这个评估,因为这篇文章已经相当详细。

路由器是接受查询和一组选择并返回一个或多个选择的模块。

选择过程由 LLM 执行。在我们的用例中,选择器接收法律代码描述作为输入,并返回一个或多个查询引擎。每个引擎代表一个单一代码的 RAG 系统。

这是我们在这个项目中进行路由实验的方式:

  • 我们首先定义了要考虑的法律代码列表及其对应的描述。我们在路由实验中使用的代码包括民法典、税法典、知识产权法典、交通法规法典和劳动法典。
codes_to_description = {
    "Code civil": "Code civil: code juridique qui regroupe les lois relatives au droit civil français, c’est-à-dire l'ensemble des règles qui déterminent le statut des personnes (livre Ier), celui des biens (livre II) et celui des relations entre les personnes privées (livres III et IV).",
    "Code général des impôts": "Code général des impôts: code juridique qui regroupe les lois relatives aux impôts en France, c’est-à-dire l'ensemble des règles qui déterminent les impôts et les taxes.",
    "Code de la propriété intellectuelle": "Code de la propriété intellectuelle: code juridique qui regroupe les lois relatives à la propriété intellectuelle en France, c’est-à-dire l'ensemble des règles qui déterminent les droits des auteurs, des artistes-interprètes, des producteurs de phonogrammes et de vidéogrammes et des entreprises de communication audiovisuelle.",
    "Code de la route": "Code de la route: code juridique qui regroupe les lois relatives à la circulation routière en France, c’est-à-dire l'ensemble des règles qui déterminent les droits et les devoirs des usagers de la route.",
    "Code du travail": "Code du travail: code juridique qui regroupe les lois relatives au droit du travail en France, c’est-à-dire l'ensemble des règles qui déterminent les droits et les devoirs des employeurs et des salariés.",
}
  • 我们为每个代码实例化了query_engine,如前所述。然后,我们将每个query_engine与其描述一起包装在QueryEngineTool模块中。
from llama_index.core.tools import QueryEngineTool

from query.query_engine import create_query_engine

def get_tools():
    tools = []
    for code_name, code_description in codes_to_description.items():
        query_engine = create_query_engine(code_name=code_name)
        tool = QueryEngineTool.from_defaults(
            query_engine=query_engine,
            description=code_description,
        )
        tools.append(tool)
    return tools
  • 最后,我们使用工具列表和 llama-index 的LLMMultiSelector模块创建了路由查询引擎。LLM 选择器将选项作为文本转储放入提示中,并使用 LLM 文本完成端点来做出决策。
from llama_index.core.query_engine import RouterQueryEngine
from llama_index.core.selectors import LLMMultiSelector

def create_routing_engine():
    query_engine_tools = get_tools()
    query_engine = RouterQueryEngine(
        selector=LLMMultiSelector.from_defaults(),
        query_engine_tools=query_engine_tools,
    )
    return query_engine

请注意,为了简化,我们没有在单个代码查询引擎中包含任何高级功能。

现在,让我们在包含两个主题的查询上测试路由查询引擎:劳动法和税法。这将使我们看到路由器是否选择了正确的索引。

查询:

Quelles sont les conditions pour bénéficier d'un congé sabbatique \n Comment sont imposées les plus-values immobilières

或者它的英文翻译:

What are the conditions to qualify for a sabbatical leave? \n How are capital gains on real estate taxed?

现在我们可以检查路由查询引擎给出的响应:

{
  "response": "Pour bénéficier d'un congé sabbatique, le salarié doit justifier d'une ancienneté minimale dans l'entreprise, cumulée sur plusieurs périodes non consécutives, ainsi que de six années d'activité professionnelle. De plus, il ne doit pas avoir bénéficié depuis une durée minimale, dans la même entreprise, d'un congé sabbatique, d'un congé pour création d'entreprise ou d'un congé spécifique d'une durée d'au moins six mois.\n\nLes plus-values provenant de la cession en cours d'exploitation des éléments de l'actif immobilisé et réalisées avant l'entrée en vigueur des dispositions spécifiques ne sont pas comprises dans le bénéfice imposable si le contribuable s'engage à réinvestir dans des immobilisations dans son entreprise dans un délai de trois ans.",
  "source_nodes": [
    {
      ....
      "text": "A défaut de convention ou d'accord mentionné à l'article L. 3142-32, le salarié informe l'employeur de la date de départ en congé sabbatique qu'il a choisie et de la durée de ce congé, par tout moyen conférant date certaine, au moins trois mois à l'avance."
    },
    {
      ....
      "text": "L'employeur informe le salarié de son accord sur la date de départ choisie du congé sabbatique ou de son report par tout moyen conférant date certaine."
    },
    {
      ....
      "text": "A défaut de convention ou d'accord mentionné à l'article L. 3142-32, le départ en congé peut être différé par l'employeur dans les conditions mentionnées au premier alinéa de l'article L. 3142-29, de telle sorte que le pourcentage des salariés simultanément absents de l'entreprise au titre du congé sabbatique ne dépasse pas 1,5 % de l'effectif de cette entreprise, jusqu'à la date à laquelle cette condition de taux est remplie ou que le nombre de jours d'absence au titre du congé sabbatique ne dépasse pas 1,5 % du nombre de jours de travail effectués dans les douze mois précédant le départ en congé. Pour permettre le départ en congé d'un salarié, cette période de douze mois est prolongée dans la limite de quarante-huit mois."
    },
    {
      ....
      "text": "Le salarié a droit à un congé sabbatique pendant lequel son contrat de travail est suspendu.\nLe droit à ce congé est ouvert au salarié justifiant, à la date de départ en congé, d'une ancienneté minimale dans l'entreprise, cumulée, le cas échéant, sur plusieurs périodes non consécutives, ainsi que de six années d'activité professionnelle et n'ayant pas bénéficié depuis une durée minimale, dans la même entreprise, d'un congé sabbatique, d'un congé pour création d'entreprise ou d'un congé spécifique mentionné à l'article L. 6323-17-1 d'une durée d'au moins six mois. L'ancienneté acquise dans toute autre entreprise du même groupe, au sens de l'article L. 2331-1, est prise en compte au titre de l'ancienneté dans l'entreprise."
    }, ...
  ],
  "metadata": {
    "selector_result": {
      "selections": [
        {
          "index": 4,
          "reason": "Le Code du travail regroupe les lois relatives au droit du travail en France, ce qui inclut les conditions pour bénéficier d'un congé sabbatique."
        },
        {
          "index": 1,
          "reason": "Le Code général des impôts regroupe les lois relatives aux impôts en France, ce qui inclut les règles sur l'imposition des plus-values immobilières."
        }
      ]
    }
  }
}

这里的重要结果是selector_result,它显示多选择器正确地识别了劳动法(索引 4)和税法(索引 1)作为回答用户查询的相关数据源。

你可以在笔记本./notebooks/evaluate_with_llamaindex.ipynb中找到所有的评估实验和路由查询引擎定义。

结论

在本文中,我们讨论了如何从一个基础的 RAG 系统过渡到一个高级系统,采用窗口化、混合搜索和查询重写,并使用llama-index框架。我们还探讨了如何评估 RAG 管道的某些组件,如嵌入和前述功能,从而做出关于其相关性的知情决策,而不是仅凭直觉。

我们还研究了路由如何通过 LLM 决策使我们能够选择相关的索引。

以下是一些进一步优化管道的建议:

  • 在这个项目中,我们使用了gpt-3.5-turbo作为 LLM 来生成响应,因为它易于使用且成本较低。与我们评估 RAG 系统的各个组件类似,我们也可以根据如答案相关性和准确性等指标来评估 LLM。

  • 同样的评论适用于将gpt-3.5-turbo作为评估器。使用更强大的语言模型可能会提高评估的准确性。

  • 理想情况下,我们会使用各种指标评估 RAG 管道的所有组合。然而,这可能代价高昂,并且可能不可行。

  • 也许值得考虑在我们的系统中评估其他先进技术,例如使用外部模型进行重排序、使用代理等。

  • 我们还可以增强 RAG 管道的多语言支持。一个方法是建立一个 LLM 链,执行以下任务:首先将查询从原始语言翻译成法语,然后像往常一样生成响应,最后将响应翻译回原始语言。

总结来说,考虑集成类似 RAG 系统来处理法律查询的读者应该谨慎行事,因为法律建议的复杂性和潜在影响。此类系统生成虚假信息的风险是一个需要关注的问题。尽管我们的评估在忠实度方面得分较高,但由于问题数量较少,且问题范围有限,加上评估模块本身的局限性,评估结果仍然存在不足。

为了应对这一问题,我们建议增加额外的评估层次和安全协议。此外,尽管该项目展示了高级 RAG 设置在生产中的能力,但它还不是一个最终定型的系统。这凸显了在全面部署之前进行持续完善和彻底测试的重要性。最后,用户还应当被提醒,系统的建议并不能替代专业的法律咨询。

参考文献

联系方式

posted @ 2025-01-09 18:54  绝不原创的飞龙  阅读(196)  评论(0)    收藏  举报