使用 FastAPI 和 Angular 构建 WebSocket 应用程序
原文:
towardsdatascience.com/build-a-websocket-application-with-fastapi-and-angular-988157dce554
学习如何使用 WebSocket 协议构建双向交互通信应用程序
·发表在 Towards Data Science ·7 分钟阅读·2023 年 1 月 30 日
–
图片由 geralt 提供(移动智能手机股票交易)在 Pixabay
类似于 HTTP,WebSocket 也是一种用于客户端-服务器通信的协议。然而,与 HTTP 不同的是,WebSocket 是一种双向交互协议,允许客户端向服务器发送消息,并且可以被动地从服务器接收事件驱动的响应,而无需向服务器发出请求。
WebSocket 广泛应用于聊天和游戏中,这些场景需要实时数据。它也可以用于其他需要实时或近实时数据的领域。例如,使用历史价格预测未来价格的应用程序可以从 WebSocket 中受益。当来自客户端的数据流中出现新数据时,使用某些机器学习或深度学习模型预测的结果可以自动发送到客户端。
在本文中,我们将使用 FastAPI 和 Angular 构建一个简单的 WebSocket 应用程序,其中前者用于构建 WebSocket 服务器,后者用于客户端。这个概念可能对你来说全新,并且构建这样的应用程序可能看起来很令人畏惧。然而,正如你将在本文中看到的,它实际上并没有那么复杂,我们可以用几行代码快速构建一个 WebSocket 应用程序。
使用 FastAPI 构建 WebSocket 服务器
FastAPI 使用 Starlette 作为底层框架,支持 WebSocket,并提供了一些标准方法来接受客户端连接,接收和发送数据。
在本文中,我们将把后端和前端代码放在同一个代码库中,分别位于 backend
和 frontend
文件夹中。FastAPI 代码将放在 backend/app
文件夹里:
# backend/app/main.py
import asyncio
import logging
from datetime import datetime
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("FastAPI app")
app = FastAPI()
async def heavy_data_processing(data: dict):
"""Some (fake) heavy data processing logic."""
await asyncio.sleep(2)
message_processed = data.get("message", "").upper()
return message_processed
# Note that the verb is `websocket` here, not `get`, `post`, etc.
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
# Accept the connection from a client.
await websocket.accept()
while True:
try:
# Receive the JSON data sent by a client.
data = await websocket.receive_json()
# Some (fake) heavey data processing logic.
message_processed = await heavy_data_processing(data)
# Send JSON data to the client.
await websocket.send_json(
{
"message": message_processed,
"time": datetime.now().strftime("%H:%M:%S"),
}
)
except WebSocketDisconnect:
logger.info("The connection is closed.")
break
该代码片段的关键点:
-
在 WebSocket 端点的路径装饰器中,使用的是
@app.websocket
,而不是像@app.get
、@app.post
等具有 HTTP 动词的装饰器。 -
WebSocket
对象可以在路径操作函数中注入,它可以用来接受客户端连接,接收来自客户端的数据以及通过一些标准方法(accept
、receive_json
和send_json
)向客户端发送数据。代码实际上相当自解释,你可以在你的应用中类似地使用它。
现在你可以使用 uvicorn
启动 WebSocket 服务器:
uvicorn main:app --reload --host 0.0.0.0 --port 8000
与 HTTP 端点不同,独立测试 WebSocket 端点没有客户端时并不那么简单。设置一个客户端并从那里进行测试更为容易。在这篇文章中,我们将使用 Angular 构建一个 WebSocket 客户端,并从那里与 WebSocket 服务器进行交互。
设置 Angular 项目
在这篇文章中,使用的是 Node.js 18.13.0 和 Angular v15.1.2。如果你想跟随并看到展示的确切结果,建议使用这些版本。然而,旧版本或新版本也应该能够正常工作。
要安装 Angular CLI,请运行:
npm install -g @angular/cli@15.1.2
然后,我们可以使用 ng 命令创建一个工作区:
ng new frontend
我们不会为这个简单的演示项目添加路由,所有其他设置可以保持默认。
我们不会创建新的组件,而是直接更新现有的 app
组件。
不过,我们将创建一个新的服务文件来管理与 WebSocket 相关的代码。
Angular 中的服务类只是一个普通的类加上一些设置,通常只有 Injectable
装饰器中的 providedIn
设置。我们可以直接在 app
文件夹中创建服务文件(websocket.service.ts
)。
创建 WebSocket 服务
多亏了 RxJS 库,在 Angular 中使用 WebSocket 非常简单。我们可以从 RxJS 中导入 WebSocket
工厂函数,并使用它创建一个 WebSocketSubject
,然后可以用来向 WebSocket 服务器发送数据和接收数据。
服务文件的代码如下:
// frontend/src/app/websocket.service.ts
import { Injectable } from '@angular/core';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { environment } from '../environments/environment';
interface MessageData {
message: string;
time?: string;
}
@Injectable({
providedIn: 'root',
})
export class WebSocketService {
private socket$!: WebSocketSubject<any>;
public receivedData: MessageData[] = [];
public connect(): void {
if (!this.socket$ || this.socket$.closed) {
this.socket$ = webSocket(environment.webSocketUrl);
this.socket$.subscribe((data: MessageData) => {
this.receivedData.push(data);
});
}
}
sendMessage(message: string) {
this.socket$.next({ message });
}
close() {
this.socket$.complete();
}
}
此文件的重点:
-
webSocket
工厂函数和WebSocketSubject
主题是 Angular 中使用 WebSocket 的关键。 -
要创建
WebSocketSubject
,我们需要传入 WebSocket 服务器的 URL,在这个示例中是ws://localhost:8000/ws
。请注意协议是ws
,而不是http
。这是我们在上面的 FastAPI 应用中设置的,并作为环境变量保存在environment.ts
中。 -
WebSocketSubject
的工作方式与普通的 RxJSSubject
相同。因此,它可以被订阅以接收来自服务器的数据,我们也可以使用next()
向服务器发送消息。 -
当我们不再需要与 WebSocket 服务器交互,或当相应的组件被销毁时,我们可以在
WebSocketSubject
上调用complete()
来完成它,从而关闭连接。我们应该在后端代码中妥善处理连接关闭事件,否则会引发异常。 -
接收到的数据被保存到一个数组中,稍后会一起显示给用户。
在组件中使用 WebSocket 服务
现在 WebSocket 服务已创建,我们可以在组件中使用它。我们应有一些机制来打开和关闭与 WebSocket 服务器的连接。我们还将实现向服务器发送消息和显示接收消息的功能。
这是组件文件的代码:
// frontend/src/app/app.component.ts
import { Component, OnDestroy } from '@angular/core';
import { WebSocketService } from './websocket.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnDestroy {
message = '';
constructor(public webSocketService: WebSocketService) {
this.webSocketService.connect();
}
sendMessage(message: string) {
this.webSocketService.sendMessage(message);
}
ngOnDestroy() {
this.webSocketService.close();
}
}
如我们所见,所有的数据逻辑都封装在WebSocketService
中。
这个模板文件演示了如何发送数据和显示接收到的数据:
<!-- frontend/src/app/app.component.html -->
<h2>Send a message to the server:</h2>
<form (ngSubmit)="sendMessage(message); message = ''">
<input [(ngModel)]="message" name="message" type="text" autocomplete="off" />
<button type="submit" style="margin-left: 10px;">Send</button>
</form>
<h2>Received messages from the server:</h2>
<ul>
<li *ngFor="let data of webSocketService.receivedData">
{{ data.time }}: {{ data.message }}
</li>
</ul>
注意,为了使用ngModel
,我们需要在app.module.ts
中导入FormsModule
。
一切设置好后,我们可以运行ng serve
在本地启动 Angular 应用程序,并访问 http://127.0.0.1:4200。尝试依次发送一些消息,看看会发生什么:
如演示的那样,我们可以不断地向服务器发送消息,当后台处理的数据准备好时会弹出,无需轮询服务器回复。
对后端和前端代码进行 Docker 化
为了使代码更容易在不同平台上共享,我们将后端和前端代码进行 Docker 化。
对于 FastAPI 代码,我们需要创建一个包含Dockerfile
的自定义 Docker 镜像:
# backend/Dockerfile
FROM python:3.11
WORKDIR /app
COPY ./requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
COPY ./app /app
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
requirements.txt
包含了应用程序的依赖项:
# backend/requirements.txt
fastapi>=0.89.0,<0.90.0
uvicorn[standard]>=0.20.0,<0.21.0
注意,你必须安装uvicorn[standard]
而不仅仅是uvicorn
,否则 WebSocket 无法在 FastAPI 中使用。
然后,我们需要创建一个docker-compose.yaml
文件,以便更轻松地启动后端和前端代码:
# ./docker-compose.yaml
version: "3.9"
services:
fastapi_app:
build:
context: ./backend
image: fastapi_app:latest
ports:
- target: 8000
published: 8000
volumes:
- type: bind
source: ./backend/app
target: /app
networks:
- websocket
angular:
image: node:18.13.0-alpine
working_dir: /frontend
ports:
- target: 4200
published: 4200
volumes:
- type: bind
source: ./frontend
target: /frontend
networks:
- websocket
command: npm start
networks:
websocket:
name: websocket
driver: bridge
端口设置的方式使得一切与上述示例中的工作完全一致,而不需要使用 Docker。
整个项目的代码可以在这里找到。欢迎下载并自行测试。
当一切设置好后,可以使用docker-compose
启动服务:
docker-compose build
docker-compose up -d
如果遇到端口冲突问题,可以使用以下命令找出哪些应用程序正在使用端口(4200 和 8000),并在适用时终止它们:
sudo lsof -i -P -n | grep LISTEN
如果一切正常,你可以像以前一样在 http://localhost:4200 访问 WebSocket Web 应用程序。一切应该与以前完全相同。
在这篇文章中,我们介绍了如何编写后端和前端代码,使用 WebSocket 设置一个简单的实时互动 Web 应用程序。WebSocket 服务器使用 FastAPI 创建,网页用户界面使用 Angular。FastAPI 和 Angular(通过 RxJS)都提供了简单的方式来实现 WebSocket 协议,使得使用 WebSocket 连接创建 Web 应用程序变得非常容易。
这篇文章可以帮助你开始使用 WebSocket 构建低延迟或实时的 Web 应用程序。它也可以成为数据处理的有价值工具。例如,大多数机器学习模型在构建和预测结果时需要时间。利用 WebSocket,我们可以构建一个接近实时的应用程序,使机器学习预测的数据在后台变得可用时不断弹出。
相关文章:
建立并玩耍!你自己的 V&L 模型配备 LLM!
开发集成 LLM 的 GIT 视觉语言模型。
·
关注 发表在 Towards Data Science ·21 分钟阅读·2023 年 9 月 7 日
–
本文摘要:
-
解释由微软开发的 GIT 视觉语言模型。
-
使用 PyTorch 和 Hugging Face 的 Transformers 替换 GIT 的语言模型为大型语言模型(LLMs)。
-
介绍如何使用 LoRA 微调 GIT-LLM 模型。
-
测试和讨论开发的模型。
-
探讨由 GIT 的图像编码器嵌入的“图像嵌入”是否在与“文本嵌入”相同的空间中指示特定字符。
大型语言模型(LLM)正展现出越来越多的价值。将图像纳入 LLM 使其作为视觉语言模型更加有用。在这篇文章中,我将解释一个称为 GIT-LLM 的模型的开发,这是一种简单但强大的视觉语言模型。某些部分,如代码解释,可能会显得有些繁琐,所以可以直接跳到结果部分。我进行了各种实验和分析,希望你能喜欢我所取得的成果。
实现已公开发布,所以请试试。
[## GitHub - turingmotors/heron
通过在 GitHub 上创建一个账户来为 turingmotors/heron 的发展做贡献。
github.com](https://github.com/turingmotors/heron?source=post_page-----afa773b9249e--------------------------------)
将 GIT 转化为 LLM
让我们深入探讨这篇技术博客的主要话题。
什么是 GIT?
生成式图像到文本变换器(Generative Image-to-text Transformer),或称 GIT,是微软提出的一种视觉语言模型。
arXiv: arxiv.org/abs/2205.14100
代码: github.com/microsoft/GenerativeImage2Text
它的架构相当简单。它将从图像编码器提取的特征向量转换为可以像文本一样处理的向量,使用一个投影模块。这些向量随后输入到语言模型中,以生成图像的标题或进行问答。该模型也可以以类似的方式处理视频。
该图摘自“GIT: A Generative Image-to-text Transformer for Vision and Language”
尽管它很简单,但如果你查看“Paper with code”的排行榜,你会发现它在许多任务中排名靠前。
paperswithcode.com/paper/git-a-generative-image-to-text-transformer
最初,GIT 使用像 CLIP 这样的强大模型作为其图像编码器,并从头开始训练语言模型部分。然而,在这篇文章中,我尝试使用一个强大的 LLM 并对其进行微调。在这里,我称该模型为“GIT-LLM”。
使用 Hugging Face 的 Transformers 来实现 LLM
我将使用 Hugging Face 的Transformers库来开发 GIT-LLM。Transformers 是一个用于处理机器学习模型的 Python 库。它提供了许多最先进的预训练模型,你可以立即进行推理。它还提供了训练和微调模型的工具。我相信 Transformers 在最近的 LLM 衍生品的发展中做出了重要贡献。几乎所有可用的 LLM 都可以用 Transformers 处理,许多从这些 LLM 衍生出的多模态模型也使用 Transformers 作为基础进行开发和微调。
这是使用 Transformers 模型的最简单代码。你可以通过使用AutoModel和AutoTokenizer轻松尝试 LLMs。
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "facebook/opt-350m"
model = AutoModelForCausalLM.from_pretrained(model_name).to("cuda")
tokenizer = AutoTokenizer.from_pretrained(model_name)
prompt = "Hello, I'm am conscious and"
input_ids = tokenizer(prompt, return_tensors="pt").to("cuda")
sample = model.generate(**input_ids, max_length=64)
print(tokenizer.decode(sample[0]))
# Hello, I'm am conscious and I'm a bit of a noob. I'm looking for a good place to start.
让我们查看 OPT 模型所包含的参数。打印由AutoModelForCausalLM创建的模型。
OPTForCausalLM(
(model): OPTModel(
(decoder): OPTDecoder(
(embed_tokens): Embedding(50272, 512, padding_idx=1)
(embed_positions): OPTLearnedPositionalEmbedding(2050, 1024)
(project_out): Linear(in_features=1024, out_features=512, bias=False)
(project_in): Linear(in_features=512, out_features=1024, bias=False)
(layers): ModuleList(
(0-23): 24 x OPTDecoderLayer(
(self_attn): OPTAttention(
(k_proj): Linear(in_features=1024, out_features=1024, bias=True)
(v_proj): Linear(in_features=1024, out_features=1024, bias=True)
(q_proj): Linear(in_features=1024, out_features=1024, bias=True)
(out_proj): Linear(in_features=1024, out_features=1024, bias=True)
)
(activation_fn): ReLU()
(self_attn_layer_norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
(fc1): Linear(in_features=1024, out_features=4096, bias=True)
(fc2): Linear(in_features=4096, out_features=1024, bias=True)
(final_layer_norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
)
)
)
)
(lm_head): Linear(in_features=512, out_features=50272, bias=False)
)
这非常简单。初始embed_tokens的输入维度和最终lm_head的输出维度为 50,272,表示训练此模型时使用的标记数量。让我们验证一下分词器词汇表的大小:
print(tokenizer.vocab_size)
# 50265
包括像bos_token、eos_token、unk_token、sep_token、pad_token、cls_token和mask_token这样的特殊标记,它预测了从总共 50,272 种标记中下一个单词的概率。
你可以通过查看实现来理解这些模型是如何连接的。一个简单的图示将表示如下流程:
OPT 的简化模型架构(图像由作者制作)
结构和数据流非常简单。〇〇Model 和〇〇ForCausalLM 在不同的语言模型中具有类似的框架。〇〇Model 类主要表示语言模型的“Transformer”部分。例如,如果你想执行文本分类任务,你只需使用这一部分。〇〇ForCausalLM 类用于文本生成,将分类器应用于处理后转换器的向量中的标记计数。损失计算也是在该类的前向方法中完成的。embed_positions表示位置编码,它会被加到project_in上。
使用 GIT 与 Transformers
我将根据GIT 的官方文档页面尝试一下。由于我也会处理图像,所以我会使用一个同时包含 Tokenizer 的 Processor。
from PIL import Image
import requests
from transformers import AutoProcessor, AutoModelForCausalLM
model_name = "microsoft/git-base-coco"
model = AutoModelForCausalLM.from_pretrained(model_name)
processor = AutoProcessor.from_pretrained(model_name)
# Downloading and preprocess an image
url = "http://images.cocodataset.org/val2017/000000039769.jpg"
image = Image.open(requests.get(url, stream=True).raw)
pixel_values = processor(images=image, return_tensors="pt").pixel_values
# Preprocessing text
prompt = "What is this?"
inputs = processor(
prompt,
image,
return_tensors="pt",
max_length=64
)
sample = model.generate(**inputs, max_length=64)
print(processor.tokenizer.decode(sample[0]))
# two cats sleeping on a couch
给定输入图像生成的输出为“两只猫在沙发上睡觉”,这表明它的效果很好。
让我们也来看看模型的结构:
GitForCausalLM(
(git): GitModel(
(embeddings): GitEmbeddings(
(word_embeddings): Embedding(30522, 768, padding_idx=0)
(position_embeddings): Embedding(1024, 768)
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(image_encoder): GitVisionModel(
(vision_model): GitVisionTransformer(
...
)
)
(encoder): GitEncoder(
(layer): ModuleList(
(0-5): 6 x GitLayer(
...
)
)
)
(visual_projection): GitProjection(
(visual_projection): Sequential(
(0): Linear(in_features=768, out_features=768, bias=True)
(1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
)
)
)
(output): Linear(in_features=768, out_features=30522, bias=True)
)
虽然有点长,但如果你拆解开来,它其实也很简单。在 GitForCausalLM 中,有一个 GitModel,内部包含以下模块:
-
embeddings (GitEmbeddings)
-
image_encoder (GitVisionModel)
-
encoder (GitEncoder)
-
visual_projection (GitProjection)
-
output (Linear)
与 OPT 的主要区别在于存在GitVisionModel和GitProjection,这正是将图像转换为类似提示的向量的模块。虽然语言模型对 OPT 使用 Decoder,对 GIT 使用 Encoder,但这仅意味着注意力掩码构建方式的不同。变压器层可能存在细微差别,但它们的功能本质上是相同的。GIT 使用 Encoder 这一名称,因为它使用独特的注意力掩码,该掩码对图像的所有特征应用注意力,并对文本特征使用因果掩码。
查看模型的连接;
GIT 的简化模型架构(图像由作者制作)
图像信息由GitVisionModel和GitProjection处理,以匹配文本的嵌入。之后,它与文本的嵌入一起输入到语言模型的“Transformer”层中。虽然存在细微差别,但与语言模型相关的部分几乎以相同的方式开发。
GIT 的注意力掩码
一般语言模型和 GIT 语言模型的架构几乎相同,但应用注意力掩码的方式不同。
对于语言模型,应用注意力掩码以避免在预测未来标记时查看过去的标记。这是一种称为“因果注意力”的方法,对应于下图的左侧。第一列标记仅引用自身,确保对后续词没有自注意力。第二列对第二个词应用自注意力,而从第三个词开始变为 0。这样的掩码使得模型能够有效地训练以预测下一个词。
GIT 输入有两种类型的标记:图像标记和文本标记。由于所有图像标记是同时使用的,并且不用于预测下一个标记,因此因果注意力不适用。另一方面,文本标记仍然需要因果注意力。设计了如图右侧所示的掩码来实现这一点。对于图像信息的前三行,自注意力应用于所有标记信息。从文本标记开始,向下移动一列会增加可以参考的单词数量。
因果注意力掩码与 Git 注意力掩码的区别(图像由作者制作)
让我们还检查一下制作 GIT 掩码的代码。创建 GIT 掩码的代码片段如下:
import torch
def create_git_attention_mask(
tgt: torch.Tensor,
memory: torch.Tensor,
) -> torch.Tensor:
num_tgt = tgt.shape[1]
num_memory = memory.shape[1]
# Areas where attention is applied are 0, areas without attention are -inf
top_left = torch.zeros((num_memory, num_memory))
top_right = torch.full(
(num_memory, num_tgt),
float("-inf"),
)
bottom_left = torch.zeros(
(num_tgt, num_memory),
)
# Causal Attention Mask
bottom_right = torch.triu(torch.ones(tgt.shape[1], tgt.shape[1]), diagonal=1)
bottom_right = bottom_right.masked_fill(bottom_right == 1, float("-inf"))
# Concatenate masks
left = torch.cat((top_left, bottom_left), dim=0)
right = torch.cat((top_right, bottom_right), dim=0)
# add axis for multi-head
full_attention_mask = torch.cat((left, right), dim=1)[None, None, :]
return full_attention_mask
# batch_size, sequence, feature_dim
visual_feature = torch.rand(1, 3, 128)
text_feature = torch.rand(1, 4, 128)
mask = create_git_attention_mask(tgt=text_feature, memory=visual_feature)
print(mask)
"""
tensor([[[[0., 0., 0., -inf, -inf, -inf, -inf],
[0., 0., 0., -inf, -inf, -inf, -inf],
[0., 0., 0., -inf, -inf, -inf, -inf],
[0., 0., 0., 0., -inf, -inf, -inf],
[0., 0., 0., 0., 0., -inf, -inf],
[0., 0., 0., 0., 0., 0., -inf],
[0., 0., 0., 0., 0., 0., 0.]]]])
"""
你将掩码添加到注意力权重中。因此,自注意力发生的部分为 0,而不包括在注意力中的部分为 -inf。通过向前提供此掩码,只有文本部分可以进行因果注意力。对于视觉语言模型来说,像这样有效地创建和使用掩码非常重要。
连接 GIT 和 OPT
现在,让我们连接 GIT 和 OPT。目标是创建如图所示的模型。
GIT-OPT 的简化模型架构(图由作者制作)
对于通用实现,你可以参考[modeling_git.py](https://github.com/huggingface/transformers/blob/main/src/transformers/models/git/modeling_git.py)
。
最重要的部分是GitOPTModel。在其中,一个视觉编码器需要与 LLM 连接。我会解释一些关键组件。
class GitOPTModel(OPTModel):
def __init__(self, config: OPTConfig):
super(GitOPTModel, self).__init__(config)
self.image_encoder = CLIPVisionModel.from_pretrained(config.vision_model_name)
self.visual_projection = GitProjection(config)
在*init函数内部,实例化了各种模块。super初始化了OPTModel*。在 GIT 中,推荐使用训练有素的 CLIP 图像编码器,因此我使其与 CLIP 训练的 ViT 兼容。GitProjection来自原始 GIT 实现。
让我们看看 forward 函数内部。实现基于OPTDecoder的 forward 部分,并添加了来自图像编码器的信息。虽然实现有点冗长,但我在代码中添加了注释,请按步骤进行。
class GitOPTModel(OPTModel):
...
def forward(
self,
input_ids: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
pixel_values: Optional[torch.Tensor] = None,
) -> BaseModelOutputWithPooling:
seq_length = input_shape[1]
# 1\. Extract image features using ViT
visual_features = self.image_encoder(pixel_values).last_hidden_state
# 2\. Convert features extracted by ViT into prompt-like Image Embeddings
projected_visual_features = self.visual_projection(visual_features)
# 3\. Vectorize the tokens
inputs_embeds = self.decoder.embed_tokens(input_ids)
# 4\. Obtain Positional Encoding
pos_embeds = self.embed_positions(attention_mask, 0)
# 5\. Dimension adjustment of Text Embeddings specific to OPT
inputs_embeds = self.decoder.project_in(inputs_embeds)
# 6\. Text Embeddings + Positional Encoding
embedding_output = inputs_embeds + pos_embeds
# 7\. Concatenate Image Embeddings and Text Embeddings
hidden_states = torch.cat((projected_visual_features, embedding_output), dim=1)
# 8\. Create Causal Attention Mask for Text region
tgt_mask = self._generate_future_mask(
seq_length, embedding_output.dtype, embedding_output.device
)
# 9\. Create Attention Mask for GIT
combined_attention_mask = self.create_attention_mask(
tgt=embedding_output,
memory=projected_visual_features,
tgt_mask=tgt_mask,
past_key_values_length=0,
)
# 10\. Pass through the Decoder layer repeatedly, the main part of the language model
for idx, decoder_layer in enumerate(self.decoder.layers):
layer_outputs = decoder_layer(
hidden_states,
attention_mask=combined_attention_mask,
output_attentions=output_attentions,
use_cache=use_cache,
)
hidden_states = layer_outputs[0]
# 11\. Dimension adjustment MLP specific to OPT
hidden_states = self.decoder.project_out(hidden_states)
# 12\. Align the output interface
return BaseModelOutputWithPast(
last_hidden_state=hidden_states,
past_key_values=next_cache,
hidden_states=all_hidden_states,
attentions=all_self_attns,
)
虽然看起来可能很复杂,但如果你逐步了解每个步骤,你会发现它遵循了图示中的流程。实际代码可能看起来有点复杂,但先掌握主要流程将使理解其他部分更容易。这是伪代码,对于详细部分,请参考发布的实现。
最后,让我们简要看看GITOPTForCausalLM部分。
class GitOPTForCausalLM(OPTForCausalLM):
def __init__(
self,
config,
):
super(GitOPTForCausalLM, self).__init__(config)
self.model = GitOPTModel(config)
def forward(
...
) -> CausalLMOutputWithPast:
outputs = self.model(
...
)
sequence_output = outputs[0]
logits = self.lm_head(sequence_output)
loss = None
if labels is not None:
# Predict the next word as the task
num_image_tokens = self.image_patch_tokens
shifted_logits = logits[:, num_image_tokens:-1, :].contiguous()
labels = labels[:, 1:].contiguous()
loss_fct = CrossEntropyLoss()
loss = loss_fct(shifted_logits.view(-1, self.config.vocab_size), labels.view(-1))
return CausalLMOutputWithPast(
loss=loss,
logits=logits,
...
)
模型内部的处理很简单。当提供labels时,即在训练过程中,损失计算也在 forward 中进行。在shifted_logits中,从第一个 token 到文本 tokens 的倒数第二个 token 被提取。然后,它计算与labels偏移一个词的 Cross Entropy Loss 作为正确答案。
一点需要注意的是,在初始化函数中分配GitOPTModel的变量需要命名为self.model。如果你查看父类OPTForCausalLM的实现,你会看到 OPT 在super初始化期间首先被放置到self.model中。如果你更改这个实例变量名,你将最终持有两个 OPT,这可能会增加内存负担。
LoRA 扩展
为了有效地微调 LLM,我将使用一个名为 Parameter-Efficient Fine-Tuning(PEFT)的库。由于它由 Hugging Face 开发,它与 Transformers 无缝集成。虽然 PEFT 中有各种方法,但这次我将使用一种常见的方法,即低秩适配(LoRA)进行实验。
如果模型支持 PEFT,模型可以用 LoRA 在几行代码中应用。
from transformers import AutoModelForCausalLM
from peft import get_peft_config, get_peft_model, LoraConfig
model = AutoModelForCausalLM.from_pretrained('microsoft/git-base')
peft_config = LoraConfig(
task_type="CAUSAL_LM",
r=8,
lora_alpha=32,
lora_dropout=0.1,
target_modules=["v_proj"]
)
peft_model = get_peft_model(model, peft_config)
target_modules 参数指定了你想要转换为 LoRA 的模块。如果提供了列表作为 target_modules,则会将每个字符串结尾的模块转换为 LoRA。为了简化,LoRA 仅应用于自注意力模块的“value” (v_proj)。
在模型中,ViT 用于图像编码部分。请小心,因为这样指定的话,ViT 的自注意力部分可能也会应用 LoRA。这有点繁琐,但通过指定到键名不重叠的部分并将其传递给 target_modules,你可以避免这种情况。
target_modules = [f"model.image_encoder.vision_model.encoder.{i}.self_attn.v_proj" for i in range(len(model.model.decoder))]
结果模型变成了 PeftModelForCausalLM 类的一个实例。它有一个名为 base_model 的实例变量,保存了转换为 LoRA 的原始模型。作为示例,我展示了 LoRA 如何应用于 ViT 的自注意力中的 v_proj。
(self_attn): GitVisionAttention(
(k_proj): Linear(in_features=768, out_features=768, bias=True)
(v_proj): Linear(
in_features=768, out_features=768, bias=True
(lora_dropout): ModuleDict(
(default): Dropout(p=0.1, inplace=False)
)
(lora_A): ModuleDict(
(default): Linear(in_features=768, out_features=8, bias=False)
)
(lora_B): ModuleDict(
(default): Linear(in_features=8, out_features=768, bias=False)
)
(lora_embedding_A): ParameterDict()
(lora_embedding_B): ParameterDict()
)
(q_proj): Linear(in_features=768, out_features=768, bias=True)
(out_proj): Linear(in_features=768, out_features=768, bias=True)
)
在 v_proj 线性层内部,你会发现添加了如 lora_A 和 lora_B 的全连接层。LoRA 转换后的 Linear 模块是一个名字相同的 Linear 类,继承自 PyTorch 的 Linear 和 LoraLayer。这是一个有些独特的模块,有兴趣了解细节的人可以查看实现。
请注意,使用 PEFT 创建的模型默认不会保存除 LoRA 部分之外的任何内容。虽然可以通过 merge_and_unload 方法保存,但你可能希望在 Trainer 训练过程中保存所有中途保存的模型。重载 Trainer 的 _save_checkpoints 方法是一种方法,但为了避免麻烦,我这次通过在训练阶段仅获取 PeftModel 中原始模型部分来处理。
model = get_peft_model(model, peft_config)
model.base_model.model.lm_head = model.lm_head
model = model.base_model.model
我相信还有更高效的处理方法,所以我仍在研究中。
使用 GIT-LLM 进行实验
现在让我们进行一些使用目前开发的模型的实验。
关于训练配置和其他设置的详细信息,请参考已发布的实现,因为它们本质上遵循相同的方法。
数据集:M3IT
对于实验,我想使用一个将图像与文本配对并且易于集成的数据集。在浏览Hugging Face 的 Datasets时,我发现了 M3IT,这是一个由上海 AI 实验室开发的用于 Instruction Tuning 的多模态数据集。Instruction Tuning 是一种即使在数据量有限的情况下也能产生令人印象深刻结果的方法。看起来 M3IT 重新标注了各种现有数据集,专门用于 Instruction Tuning。
huggingface.co/datasets/MMInstruction/M3IT
这个数据集很容易使用,所以我决定在接下来的实验中利用它。
要使用 M3IT 进行训练,必须创建一个自定义的 Pytorch Dataset。
class SupervisedDataset(Dataset):
def __init__(
self,
vision_model_name: str,
model_name: str,
loaded_dataset: datasets.GeneratorBasedBuilder,
max_length: int = 128,
):
super(SupervisedDataset, self).__init__()
self.loaded_dataset = loaded_dataset
self.max_length = max_length
self.processor = AutoProcessor.from_pretrained("microsoft/git-base")
# Setting up the corresponding Processor for each model
self.processor.image_processor = CLIPImageProcessor.from_pretrained(vision_model_name)
self.processor.tokenizer = AutoTokenizer.from_pretrained(
model_name, padding_side="right", use_fast=False
)
def __len__(self) -> int:
return len(self.loaded_dataset)
def __getitem__(self, index) -> dict:
# cf: https://huggingface.co/datasets/MMInstruction/M3IT#data-instances
row = self.loaded_dataset[index]
# Creating text input
text = f'##Instruction: {row["instruction"]} ##Question: {row["inputs"]} ##Answer: {row["outputs"]}'
# Loading the image
image_base64_str_list = row["image_base64_str"] # str (base64)
img = Image.open(BytesIO(b64decode(image_base64_str_list[0])))
inputs = self.processor(
text,
img,
return_tensors="pt",
max_length=self.max_length,
padding="max_length",
truncation=True,
)
# batch size 1 -> unbatch
inputs = {k: v[0] for k, v in inputs.items()}
inputs["labels"] = inputs["input_ids"]
return inputs
在 init 函数中,image_processor 和 tokenizer 分别对应其各自的模型。传递的 loaded_dataset 参数应来自 MMInstruction/M3IT 数据集。
coco_datasets = datasets.load_dataset("MMInstruction/M3IT", "coco")
test_dataset = coco_datasets["test"]
对于 COCO Instruction Tuning 数据集,训练、验证和测试的划分与原始数据集相同,分别为 566,747、25,010 和 25,010 对图像-文本对。其他数据集,如 VQA 或 Video,也可以类似处理,使其成为一个多用途的验证数据集。
示例数据如下:
图像引用自 M3IT 数据。
该图片的说明如下:
##Instruction: 写一个简洁的图像描述,捕捉其主要组成部分、它们之间的关系以及任何显著细节。 ##Question: ##Answer: 一名戴红色头盔的男子骑在小型摩托车上,行驶在泥土道路上。
对于 COCO 数据集,该数据集用于描述,问题部分保持为空。
让我们深入探讨处理器的操作。本质上,它对图像进行归一化并对文本进行分词。短于 max_length 的输入也会被填充。处理器返回的数据是一个包含以下内容的字典:
-
input_ids: 一个分词文本的数组。
-
attention_mask: 用于分词文本的掩码(填充部分为 0)。
-
pixel_values: 归一化图像的数组,也转换为 Channel-first。
这些关键名称对应于模型的前向函数的参数,因此不应更改。最后,input_ids 直接传递给名为 labels 的关键。GitOPTForCausalLM 的前向函数通过预测下一个词(偏移一个标记)来计算损失。
实验 1:确定微调位置
在 GIT 模型的研究论文中,解释了使用了强大的视觉编码器,并且语言模型采用了随机参数。这一次,由于目标是最终使用 7B 类语言模型,因此将应用预训练模型。以下模块将用于微调。GIT Projection 作为一个初始化模块,总是包括在内。一些组合可能看起来冗余,但它们在此试验中被探讨而无需过多担忧。
设置为训练的模块会获得梯度,而其余模块则修改为没有梯度。
# Specifying the parameters to train (training all would increase memory usage)
for name, p in model.model.named_parameters():
if np.any([k in name for k in keys_finetune]):
p.requires_grad = True
else:
p.requires_grad = False
本次检查所用的 Vision Encoder 和 LLM 是:
-
openai/clip-vit-base-patch16
-
facebook/opt-350m
训练使用 COCO 数据集,持续 5 轮。
以下是每个实验中训练的目标模块:
-
Proj: GIT Projection。随机初始化,因此总是进行训练。
-
LoRA: 语言模型中的自注意力的 Query、Key 和 Value 被应用。
-
OPT: 所有层都经过训练。
-
ViT: 所有层都经过训练。
-
Head: OPT 的最终 lm_head 已经过训练。
(注意:虽然 LoRA 可以应用于 ViT,但为了避免使实验过于复杂,这次未包含在内。)
该图显示了训练损失。图例中的 Proj、LoRA、OPT、ViT 和 Head 是上述训练模块。(图由作者制作)
如训练损失图所示,一些组的表现明显不佳。这些情况发生在 OPT 被包括在训练中时。尽管所有实验在相似的条件下进行,但在微调语言模型时可能需要更详细的调整,如学习率。接下来将检查排除 OPT 的训练模型的结果。
该图显示了没有完全微调结果的训练损失。图例中的 Proj、LoRA、OPT、ViT 和 Head 是上述训练模块。(图由作者制作)
该图显示了验证损失。图例中的 Proj、LoRA、OPT、ViT 和 Head 是上述训练模块。(图由作者制作)
无论是训练还是验证损失,Projection+LoRA模型的减少幅度最大。对最终Head层进行微调显示出几乎相同的结果。如果 ViT 也被训练,损失值似乎略高,结果也显得不稳定。即使在 ViT 训练期间添加了 LoRA,损失仍然倾向于较高。对于这个数据的微调,似乎使用一个未更新参数的预训练 ViT 模型会产生更稳定的结果。LoRA 的有效性在多个地方得到了认可,从这个实验中可以明显看出,将 LoRA 添加到 LLM 中改善了训练和验证损失。
评估一些测试数据的推理结果:
GIT-OPT 的示例结果。图片引用自 M3IT 数据集,文本结果由作者的模型生成。
当训练 OPT 本身时,结果与损失结果一样差,使得模型无言以对。此外,训练 ViT 时,输出结果有语义意义,但描述的内容与给定的图像完全不同。然而,其他结果似乎在某种程度上捕捉到了图像的特征。例如,第一张图提到了“猫”和“香蕉”,第二张图识别为“交通标志”。比较有无 LoRA 的结果,后者倾向于重复使用类似的词汇,但使用 LoRA 似乎使其略微更自然。训练Head时得到的输出非常有趣,例如第一张图用“playing”代替“eating”。虽然这些结果中有些元素不自然,但可以推测训练成功捕捉了图像特征。
实验 2:比较亿级模型
对于早期实验中的微调条件,使用了稍小的语言模型 OPT-350m。现在的意图是将语言模型切换到 7B 模型。不仅仅满足于 OPT,还将引入更强的 LLM,如 LLaMA 和 MPT。
将这两个模型集成可以按照与 OPT 类似的方式进行。参考LlamaModel和MPTModel的前向函数,将投影的图像向量与文本标记结合,并将掩码从Causal Attention Mask更改为GIT 的 Attention Mask。需要注意的是:对于 MPT,掩码不是(0, -inf),而是(False, True)。随后的过程可以类似地实现。
要使用 7B 级模型与 OPT,只需将模型名称从 facebook/opt-350m 更改为 facebook/opt-6.7b。
对于 LLaMA,考虑到 LLaMA2 的可用性,它将是首选模型。使用这个预训练模型需要 Meta 和 Hugging Face 的批准。需要一个 Hugging Face 账户,所以确保设置好。批准通常在几小时内完成。之后,登录到执行训练的终端上的 Hugging Face。
huggingface-cli login
你可以使用在 Hugging Face 账户中创建的令牌登录 → 设置 → 访问令牌。
训练参数保持一致,使用 COCO 数据集并持续 3 个 epoch。根据实验 1 的结果,微调的模块设置为Projection + LoRA。
让我们来看看结果。
此图显示了训练损失(图由作者制作)
此图显示了验证损失(图由作者制作)
通过查看损失,可以明显看出,使用 LLaMA2 和 MPT 作为 LLM 的模型显示了更令人满意的减少。让我们也观察一下推理结果。
GIT-LLMs 的示例结果。图片引用自 M3IT 数据集,文本结果由作者的模型生成。
关于第一张图片,对于所有模型,与 OPT-350m 相比,表情似乎更自然。没有像“一个香蕉和一个香蕉”这样的奇怪表情,突出了 LLM 的优势。对于第二张图片,仍然存在像“交通灯”或“建筑物”这样的短语困难。对于这种复杂的图像,可能需要考虑升级 ViT 模型。
最后,让我们对在 GPT-4 中变得流行的图像进行推理。
GIT-LLMs 的示例结果。图片引用自这里,文本结果由作者的模型生成。
尽管使用 LLM 时预期会有流畅的响应,但结果相当简单。这可能是因为模型仅在 COCO 上进行了训练。
实验 3:增加数据量
鉴于之前实验的结果不尽如人意,决定在训练中引入 COCO 以外的数据。当前使用的 M3IT 数据集相当全面,能够处理与 COCO 格式相同的大量数据。
该表格引用自“M3IT:面向多模态多语言指令调优的大规模数据集”的表 3
打算使用来自该来源的数据,但排除“中文”和“视频”类别。最初,COCO 训练数据集包含 566,747 条数据。通过与其他来源结合,总数增加到 1,361,650。尽管规模大致翻倍,但由于任务多样性增加,数据集的质量被认为有所提高。
使用ConcatDataset可以轻松处理多个 Pytorch 数据集。
dataset_list = [
datasets.load_dataset("MMInstruction/M3IT", i) for i in m3it_name_list
]
train_dataset = torch.utils.data.ConcatDataset([d["train"] for d in dataset_list])
训练进行了 1 轮,并使用 LLaMA2 模型对Projection 和 LoRA进行了微调,与实验 2 类似。
由于这次没有可以比较的损失值,我们直接进入推理结果。
GIT-LLaMA2 的示例结果。图片来自 M3IT 数据集,文本结果由作者的模型生成
GIT-LLaMA2 的示例结果。图片来自 M3IT 数据集,文本结果由作者的模型生成
GIT-LLaMA2 的示例结果。图片来自 M3IT 数据集,文本结果由作者的模型生成
除了解决简单问题外,模型现在还处理更复杂的挑战。通过添加比仅仅是描述更复杂的任务数据集,能力显著扩展。仅用 1 轮训练就达到这样的准确性令人惊讶。
让我们用以下示例图像进行测试。鉴于数据集的多样性增加,问题的呈现方式略有修改。
GIT-LLaMA2 的示例结果。一张图片来自这里,文本结果由作者的模型生成
尽管“伞状”这一描述仍有些奇怪,但感觉越来越好。为了进一步改进,需要增加训练轮次,添加更多类型或量的数据集,并利用更强大的 ViT 或 LLM。尽管如此,能够在仅半天内开发出这样的模型,考虑到计算和数据资源,确实令人印象深刻。
奖励实验。图像变成文字了吗?
再看一下 GIT 结构。
GIT-LLM 的简化模型架构(图像由作者制作)
如图所示,在视觉编码器进行特征提取后,图像通过Visual Projection与向量化的文本平等对待。换句话说,Visual Projection 可能将图像向量转换为文本向量。进行了调查以查看Visual Projection之后的向量是什么样的。
虽然有使用Head将投影后的向量还原为文本的选项,但发现即使是使用Embedding模块向量化的向量也无法通过这种方法还原为原始文本。因此,应将与输入到 LLM 之前的文本向量最接近的向量分配为相应的单词。所有在分词器中注册的令牌都使用 Embedding 模块进行向量化,并且选择了余弦相似度最高的词作为目标词。
本实验使用的图像是一只猫。
图片摘自 M3IT 数据集。
现在,让我们进行分析(完整分析可在这里查看)。首先,对所有注册的令牌进行向量化。
coco_datasets = datasets.load_dataset("MMInstruction/M3IT", "coco")
test_dataset = coco_datasets["test"]
supervised_test_dataset = SupervisedDataset(model_name, vision_model_name, test_dataset, 256)
ids = range(supervised_test_dataset.processor.tokenizer.vocab_size)
all_ids = torch.tensor([i for i in ids]).cuda()
token_id_to_features = model.model.embed_tokens(all_ids)
接下来,将提取本来会被 ViT 和 Projection 转换为单词的图像向量。
inputs = supervised_test_dataset[0] # Picking a sample arbitrarily
pixel_values = inputs["pixel_values"]
out_vit = model.model.image_encoder(pixel_values).last_hidden_state
out_vit = model.model.visual_projection(out_vit)
计算了这些向量和单词向量的点积,最大值的结果被解码为相关的令牌 ID。
# Dot product
nearest_token = out_vit[0] @ token_id_to_features.T
# The index of the maximum value corresponds to the relevant token ID
visual_out = nearest_token.argmax(-1).cpu().numpy()
decoded_text = supervised_test_dataset.processor.tokenizer.batch_decode(visual_out)
print(decoded_text)
"""
['otr', 'eg', 'anto', 'rix', 'Nas', ...]
"""
如打印出的decoded_text所示,一些不熟悉的单词出现了。由于一些单词重复出现,它们被统计了。
print(pd.Series(decoded_text).value_counts())
"""
mess 43
atura 29
せ 10
Branch 10
Enum 9
bell 9
worden 7
...
"""
似乎出现了大量不熟悉的单词。根据位置,它们可能传达有意义的信息。让我们将这些单词绘制在猫的图像上。
n_patches = 14
IMAGE_HEIGHT = 468
IMAGE_WIDTH = 640
y_list = np.arange(15, IMAGE_HEIGHT, IMAGE_HEIGHT//n_patches)
x_list = np.arange(10, IMAGE_WIDTH, IMAGE_WIDTH//n_patches)
plt.figure()
plt.axis("off")
plt.imshow(np.array(image), alpha=0.4)
for index in np.arange(n_patches ** 2):
y_pos = index // n_patches
x_pos = index - y_pos * n_patches
y = y_list[y_pos]
x = x_list[x_pos]
# The first token is the bos token, so it is excluded
word = decoded_text[index + 1]
# For differentiating words by color
plt.annotate(word, (x, y), size=7, color="blue")
plt.show()
plt.clf()
plt.close()
图片由作者制作
经常出现的单词用颜色编码。结果似乎表明它们并不仅仅是投射到有意义的单词上。虽然“Cat”这个词可能被叠加在猫的图像上,赋予它一定的相关性,但其含义仍不明确。
该实验中不确定的结果可能是由于强行选择了一个余弦相似度高的单词。无论如何,这种方法并不是简单地将单词投射到图像提示上。从图像中提取的向量通过Visual Projection 转换为令牌空间中的向量,这些向量似乎在意义上有些相似,充当神秘的提示。可能最好不要深入探讨这一点。
结论
在这篇技术博客文章中,我介绍了将 LLM 集成到视觉语言模型 GIT 的方法。此外,还使用开发的模型进行了各种实验。虽然有成功也有失败,但我希望继续进行视觉语言模型的实验,以积累见解。请将本文作为参考,并鼓励你创建自己的视觉语言模型,探索其潜力。
这是一张使用 Stable Diffusion 创建的 GIT-LLM 插图。(图片由作者制作)
使用 SQL 构建客户旅程
教程
学习如何跟踪跨多个渠道的消费者
·
关注 发表在 Towards Data Science · 5 min read · 2023 年 3 月 8 日
–
图片来源:Krivec Ales,Pixabay
客户旅程市场
全球客户旅程分析(CJA)市场在 2020 年的价值达到了83 亿美元,预计到 2026 年将增长到251 亿美元,预测期内年均增长率为20.3%。参与这一市场的代价不菲,并不是每个公司都能拿出一大笔资金。尽管关于 CJA 的信息很多,但关于如何构建数据集以获得客户旅程洞察的资料却几乎没有。在本教程中,我将展示如何使用结构化查询语言(SQL)创建客户旅程数据集。
什么是客户旅程?
我们可以将客户旅程定义为客户在多个渠道之间的一系列互动。这些渠道可能包括电子邮件、网站、移动应用、呼叫中心、社交媒体或店内购买。一个旅程可能展示客户与电子邮件互动,接着是网络,然后是直接邮件,最后是购买。这些时间戳互动使我们能够理解客户与公司产品的互动时间和顺序。旅程中的每一步都可以提供有价值的见解,改善客户体验、转换率及后续的营销工作。
客户旅程分析:使用 SQL 构建
旅程分析使 CX 团队能够可视化客户在不同渠道和时间上的行为,定义显示转换可能性的旅程信号,并监控终端旅程成功的表现。
我们的数据包括一个虚构公司的互动和订单,该公司销售汽车。public.interactions
表中的互动(即网页、移动端等)是客户在购买之前与我们公司互动的渠道。我们在 public.orders
表中捕捉购买数据。
构建客户旅程数据集需要三个步骤。步骤 1:主要代码根据互动日期对客户互动进行排序。步骤 2:主要代码 CTE创建主表的临时表,并使其可用于步骤 3。步骤 3:构建客户旅程如其名所示,创建客户旅程数据集。因此,让我们分析下面 SQL 代码中的每一步,以了解它们如何工作。
作者,SQL 代码
主要代码(步骤 1)
我们通过客户 ID 将互动表和订单表连接起来,public.interactions = public.orders on t1.customer_id = t2.customer_id
。我们在订单表中捕捉完成的购买。因此,结果旅程将仅包含已购买的客户的互动。
为了捕捉和排序互动,我们使用Lead()函数。Lead 函数基于偏移值访问下一行的数据。Lead 函数的语法如下:
LEAD(return_value [,offset[, default ]]) OVER (
PARTITION BY expr1, expr2,...
ORDER BY expr1 [ASC | DESC], expr2,...
)
在下表中,第一次互动由函数 lead(t1.interaction,**0**) over (**partition by** t1.customer_id **order by** t1.interaction_date asc)
决定。首先,partition by
基于 t1.customer_id
对客户记录进行分组。其次,order by t1.interaction_date asc
按日期对互动进行排序。最后,offset 访问下一行,或者当前行之后的第二行,或者当前行之后的第三行,依此类推。在这里,offset 为零,lead(t1.interaction,**0**)
返回值为 Mobile
。如果偏移量为 1,lead(t1.interaction,**1**)
返回值为 Web
。
我建议将数据导入数据库,并应用上面的 SQL 代码以充分理解信息。你可以在这里找到数据集。
将主代码封装在 CTE 中(第 2 步)
我们通过将主代码添加到公共表表达式(CTE)中来处理它。CTE 的语法是:
with ctedata as
(
--Step 1 code
)
我们使用 CTE 的主要原因是为了能够使用where
子句,并通过**Lead()**或Row_Number()函数派生的列来过滤行。在主代码中使用函数创建的列不能在where
子句中使用。这为第 3 步准备了数据。
构建客户旅程数据集(第 3 步)
在第 1 步创建的数据集中,一些行缺少数据。我们通过过滤where offset = 0
来移除缺失的行。我们还通过对首次互动和第二次互动的总购买客户进行汇总来聚合数据,如下所示。
作者,数据集
上表展示了客户在购买前的互动情况。在这个例子中,购买复古汽车的客户通过移动和电子邮件渠道进行互动。通过客户旅程数据,我们可以洞察哪些渠道表现最好,能促进互动并带来销售;哪些客户路径会导致流失;什么时机是与消费者互动的最佳时机,以及不同受众采取的路径[2]。
这些知识可能有助于制定营销策略。虽然表格可以有效地解释数据,但可视化数据可以帮助描绘直观的客户旅程。因此,让我们在下一部分尝试一下。
可视化客户旅程
下图中的桑基图对于说明跨多个渠道(如电子邮件、网站、移动应用、呼叫中心、社交媒体)的客户旅程顺序非常有用。
作者,桑基图数据
我们从左到右阅读桑基图。每个垂直条代表一个包含消费者的节点。例如,蓝色节点显示 23.24%的消费者首先通过直接邮寄与品牌互动。这些客户接着通过移动、直接邮寄、网页和电子邮件的组合进行互动。带状图的厚度直观地表示了在互动之间流动的消费者数量。带状图越厚,表示流向第二个互动节点的消费者越多。如果你想创建桑基图,我推荐尝试Chart Expo或Visual Paradigm这两个付费服务。我偏爱 Chart Expo,因为它提供了在线教程视频。如果你决定使用它,记得按照提供的 SQL 代码格式导出数据。
自定义旅程:继续学习
学习如何跟踪客户在多个渠道中的行为有很多内容需要探索。为此,我在本教程中包含了一个视频。视频覆盖了相同的内容,但通过插图讨论示例可能会增强学习效果。如果你发现理解某些概念有困难,我鼓励你观看视频。
作者,视频
好的,目前就这些了。希望你觉得这个教程对你有帮助。请随时联系我提问。我在这里分享和成长。
参考文献:
[1]: Markets and Markets,客户旅程分析市场按组件组织和规模
[2]: Karolina Matuszewska,Marek Juszczyński。 (2022 年 10 月 3 日)。什么是客户旅程分析以及它为何对你的业务重要
构建可部署的机器学习管道
原文:
towardsdatascience.com/build-deployable-machine-learning-pipelines-a6d7035816a6
利用 Kedro 构建生产就绪的机器学习管道
·发表于Towards Data Science ·8 分钟阅读·2023 年 6 月 30 日
–
作者提供的图片:使用 Midjourney 生成
背景 — 笔记本不能“部署”
许多数据科学家最初接触编码的方式是通过笔记本风格的用户界面。笔记本对于探索至关重要——这是我们工作流程的一个关键方面。然而,它们并不是为生产环境准备的。这是我在众多客户中观察到的一个关键问题,其中一些人询问如何将笔记本投入生产。与其将笔记本投入生产,最优的生产准备路径是编写模块化、可维护和可重复的代码。
在本文中,我展示了一个用于训练模型以分类欺诈信用卡交易的模块化机器学习管道示例。希望在本文结束时,你能:
-
了解和掌握模块化机器学习管道。
-
感到受启发,自己动手构建一个吧。
如果你想最大化地发挥机器学习模型的效益,编写模块化代码是一个重要的步骤。
首先对模块化代码进行一个简要定义。模块化代码是一种软件设计范式,强调将程序分解为独立的、可互换的模块。我们应该力求在我们的机器学习管道中达到这种状态。
快速绕道 — 项目、数据和方法
该机器学习项目的数据来源于Kaggle。数据集包含 284,807 个匿名信用卡交易记录,其中 492 个为欺诈交易。任务是构建一个分类器来检测欺诈交易。
本项目的数据在开放数据公共许可证下许可用于任何目的,包括商业用途。
我使用了利用Ludwig的深度学习方法,这是一个开源的声明式深度学习框架。我在这里不会详细介绍 Ludwig,不过我之前写过一篇关于框架的文章。
Ludwig 深度神经网络通过**.yaml**文件进行配置。对那些感兴趣的人,你可以在模型注册 GitHub找到它。
使用 Kedro 构建模块化管道
使用开源工具使得构建模块化机器学习管道变得更加容易,我最喜欢的工具之一是Kedro。不仅因为我见证了它在行业中的成功应用,还因为它帮助我提升了我的软件工程技能。
Kedro 是一个开源框架(根据 Apache 2.0 许可证)用于创建可重复、可维护和模块化的数据科学代码。我是在为一家银行开发 AI 策略时发现它的,考虑到我的团队可以利用哪些工具来构建生产就绪的代码。
免责声明:我与 Kedro 或 McKinsey 的 QuantumBlack(该开源工具的创建者)没有任何关联。
模型训练管道
作者提供的图片:使用 Kedro 可视化生成的端到端模型训练管道
Kedro 方便地允许你可视化你的管道,这是一个很棒的功能,可以帮助你清晰地理解你的代码。这个管道在机器学习中是标准的,因此我只会简单介绍每个方面。
-
导入数据:从外部来源导入信用卡交易数据。
-
拆分数据:使用随机拆分将数据分成训练集和保留集。
-
运行实验:使用 Ludwig 框架在训练数据集上训练深度神经网络。Ludwig 实验 API 方便地为每次实验运行保存模型工件。
-
运行预测:使用前一步训练的模型对保留数据集进行预测。
-
模型诊断:生成两个诊断图表。首先是跟踪每个周期的交叉熵损失。其次是测量模型在保留数据集上的性能的ROC 曲线。
作者提供的图片:模型训练过程中的损失曲线
作者提供的图片:在保留数据集上模型评估的 ROC 曲线
管道的核心组件
现在我们已经建立了一个高层次的视图,让我们深入探讨这个管道的一些核心组件。
项目结构
C:.
├───conf
│ ├───base
│ │ └───parameters
│ └───local
├───data
│ ├───01_raw
│ ├───02_intermediate
│ ├───03_primary
│ ├───04_feature
│ ├───05_model_input
│ ├───06_models
│ │ ├───experiment_run
│ │ │ └───model
│ │ │ ├───logs
│ │ │ │ ├───test
│ │ │ │ ├───training
│ │ │ │ └───validation
│ │ │ └───training_checkpoints
│ │ └───experiment_run_0
│ │ └───model
│ │ ├───logs
│ │ │ ├───test
│ │ │ ├───training
│ │ │ └───validation
│ │ └───training_checkpoints
│ ├───07_model_output
│ └───08_reporting
├───docs
│ └───source
│
└───src
├───fraud_detection_model
│ ├───pipelines
│ ├───train_model
└───tests
└───pipelines
Kedro 提供了一个模板化的目录结构,这个结构在你启动项目时就已经建立。从这个基础上,你可以以编程方式将更多的管道添加到你的目录结构中。这种标准化的结构确保了每个机器学习项目的一致性和易于文档化,从而便于维护。
数据管理
数据在机器学习中扮演着至关重要的角色。当在商业环境中使用机器学习模型时,跟踪数据的能力变得尤为重要。你经常会面临审计,或者需要在他人的机器上生产化或重现你的管道。
Kedro 提供了两种方法来强制执行数据管理的最佳实践。第一种是目录结构,专为机器学习工作负载设计,为数据转换过程中生成的中间表和模型工件提供了明确的位置。第二种方法是data catalogue。作为 Kedro 工作流的一部分,你需要在 .yaml 配置文件中注册数据集,从而在管道中利用这些数据集。这种方法初看可能不寻常,但它使你和其他参与管道工作的人员能够轻松跟踪数据。
调度 — 节点和管道
这就是魔法发生的地方。Kedro 提供了开箱即用的管道功能。
管道的初始构建块是nodes。每个可执行的代码片段可以封装在一个节点中,节点只是一个接受输入并产生输出的 Python 函数。然后,你可以将一个管道结构化为一系列节点。通过调用节点并指定输入和输出,可以轻松构建管道。Kedro 会确定执行顺序。
一旦管道构建完成,它们会被注册在提供的pipeline_registry.py文件中。这种方法的美妙之处在于,你可以创建多个管道。这在机器学习中尤其有用,你可能会有一个数据处理管道、一个模型训练管道、一个推理管道等。
一旦设置完成,修改管道的各个方面就会变得相当简单。
显示 nodes.py 脚本示例的代码片段
显示 Pipeline 脚本示例的代码片段
配置
Kedro 的最佳实践规定所有配置应通过提供的 parameters.yml 文件来处理。从机器学习的角度来看,超参数也属于这一类别。这种方法简化了实验过程,因为你可以简单地用另一组超参数替换一个 parameters.yml 文件,这也更容易追踪。
我还在 parameters.yml 配置中包含了我的 Ludwig 深度神经网络 model.yaml 和数据源的位置。如果模型或数据位置发生变化——例如,在开发者之间转移时——调整这些设置会非常简单。
显示parameters.yml
文件内容的代码片段
可复现性
Kedro 包含一个 requirements.txt 文件作为模板结构的一部分。这使得监控你的环境和确切的库版本变得非常简单。然而,如果你愿意,你可以使用其他环境管理方法,如 environment.yml 文件。
建立工作流
如果你正在开发机器学习管道并考虑使用 Kedro,起初可能会有陡峭的学习曲线,但采用标准工作流会简化这一过程。以下是我建议的工作流:
-
建立你的工作环境:我更喜欢使用 Anaconda 来完成这项任务。我通常使用一个 environment.yml 文件,其中包含我环境所需的所有依赖项,并使用 Anaconda Powershell 命令行来 创建 我的环境。
-
创建 Kedro 项目:一旦你安装了 Kedro——希望它已在你的 environment.yml 中声明——你可以通过 Anaconda 命令行界面 创建 一个 Kedro 项目。
-
在 Jupyter Notebooks 中探索:我在 Jupyter notebooks 中构建初始管道,这个过程对大多数数据科学家来说都很熟悉。唯一的不同之处在于,一旦你的管道构建完成,你应该整理它,以便每个单元格可以作为你 Kedro 管道中的一个节点。
-
注册你的数据:在数据 catalogue 中记录每个数据处理或数据摄取步骤的输入和输出。
-
添加你的管道:在笔记本中进行探索后,你会想要 创建一个管道。这是通过命令行界面完成的。运行此命令将会在 ‘pipelines’ 中添加一个额外的文件夹,文件夹名称为你刚刚创建的管道名称。在这个文件夹中,你将构建你的节点和管道。
-
定义你的管道:这是将代码从你的 Jupyter 笔记本迁移到你管道文件夹中的 node.py 文件的阶段,确保你希望成为管道一部分的节点都有输入和输出。一旦节点设置完毕,接下来在 pipeline.py 文件中定义你的管道。
-
注册你的管道: pipeline_registry.py 文件提供了一个模板,用于注册你新创建的管道。
生产就绪的管道适应于更广泛的机器学习操作生态系统。阅读我的关于 MLOps 的文章,进行更深入的了解。
支持你的 AI 战略的有效 MLOps 蓝图
towardsdatascience.com
结论
Kedro 是一个出色的框架,用于交付生产就绪的机器学习管道。除了本文讨论的功能外,还有许多与其他开源库的集成,以及用于文档和测试的包。Kedro 并不能解决与模型部署相关的所有问题——例如,模型版本控制可能更适合使用其他工具如 DVC 处理。然而,它将帮助商业环境中的数据科学家生成更可维护、模块化和可重复的生产就绪代码。对于完全的新手来说,学习曲线相对较陡,但文档清晰,并包括引导教程。与这些包中的任何一个一样,最好的学习方法是直接动手试验。
完整的 GitHub 仓库 链接
在 LinkedIn 上关注我
订阅 medium 以获得更多我的见解:
我分享数据科学项目、经验和专业知识,以帮助你在旅程中前行。你可以通过以下方式注册 medium…
如果你有兴趣将 AI 或数据科学整合到你的业务运营中,我们邀请你安排一次免费的初步咨询:
通过免费咨询,发现我们在帮助企业实现雄心勃勃的目标方面的专业知识。我们的数据科学家和…
www.data-centric-solutions.com
使用 Mercury 从 Jupyter Notebook 直接构建优雅的 Web 应用程序。
瞬间将 Notebook 转换为美丽的 Web 应用程序
·
关注 发表在 Towards Data Science · 7 分钟阅读 · 2023 年 4 月 17 日
–
有效的沟通在所有数据驱动的项目中都至关重要。数据专业人员常常需要向包括业务领导、技术团队和其他数据科学家在内的利益相关者传达他们的发现和见解。
尽管传统的数据见解沟通方法,如 PowerPoint 演示和静态报告被广泛偏爱,但它们通常耗时较长。
更重要的是,这些服务要求用户离开 Jupyter Notebook 的舒适环境——这是数据科学家们大多数时间的工作场所。
分离展示工具和 Jupyter(作者提供的图像)
如果我们能通过从 Jupyter Notebook 创建互动且优雅的网页应用来分享我们的发现,那岂不是很好吗?
为此,Mercury 是一个开源工具,它简化了从 Jupyter Notebook 舒适环境中创建网页应用的过程。
因此,在本文中,我将演示如何使用 Mercury 创建令人惊叹的网页应用并与他人分享。
你可以在这里找到这个博客的代码:GitHub。
让我们开始 🚀!
开始使用 Mercury
Mercury 创建的网页应用主要由两个因素驱动:
#1) Jupyter Notebook:
这里是你开发网页应用的地方。我们使用 Mercury 的 input 和 output 小部件来实现互动。
输入小部件允许用户提供输入并与应用互动。Mercury 支持的一些输入小部件如下所示:
Mercury 的小部件(作者提供的图像)
输出小部件用于展示输出。这包括 Markdowns(带变量)、JSON 等。此外,Jupyter 单元格的输出也由 Mercury 渲染。
因此,如果你的应用创建了图表或打印了数据框等,它们将出现在网页应用的输出面板中。
#2) Mercury 服务器
服务器将 Jupyter Notebook 渲染为网页应用。
使用 Mercury 部署应用(作者提供的图像)
正如我们将看到的,渲染笔记本只需运行一个命令。你只需在笔记本中创建你的网页应用。
使用 Mercury 设置网页应用需要几个简单步骤。
安装 Mercury
首先,通过 pip 安装库:
pip install mercury
完成!
现在我们可以使用输入和输出小部件创建我们的网页应用。
使用 Mercury 开发网页应用
如上所述,使用 Mercury 创建的网页应用主要由其小部件驱动。
#1) 导入库
要使用它们,我们首先导入库。再重申一次,我们将从 Jupyter Notebook 开始一切。
## mercury_app.ipynb
import mercury as mr
此外,你可以根据需要导入其他任何库。对于这个博客,我将创建一个网页应用来分析一个自创的虚拟员工数据框。因此,我还将使用以下库:
## mercury_app.ipynb
import pandas as pd
import seaborn as sns
from matplotlib import pyplot as plt
sns.set()
#2) 配置应用
接下来,我们通过提供 title
和 description
来实例化一个 Mercury 应用。
## mercury_app.ipynb
app = mr.App(title="Employee Data Analysis",
description="Employee Report in Mercury")
#3) 用小部件填充应用
接下来,让我们添加一些小部件,以允许用户与以下虚拟数据进行互动:
虚拟数据集(作者提供的图像)
从本质上讲,我们将执行以下操作:
-
添加一个小部件以上传 CSV 文件。
-
让用户根据
Company_Name
列中的条目过滤数据。这将是一个MultiSelect
小部件。 -
此外,用户还可以使用
Slider
根据Credits
过滤数据。
一旦数据被过滤,我们将显示以下内容:
-
过滤后数据框的维度。
-
Employee_Salary
和Employee_Rating
的散点图。 -
显示
Employee_Status
列分布的条形图。
让我们现在来构建它!
首先,我们添加文件上传小部件。
## mercury_app.ipynb
data_file = mr.File(label="Upload CSV")
文件的名称可以通过data_file
对象的filepath
属性访问。因此,一旦文件上传成功,我们将使用 Pandas 读取它,如下所示:
## mercury_app.ipynb
emp_df = pd.read_csv(data_file.filepath)
现在,我们将添加两个小部件——Company_Name
上的MultiSelect
小部件和Credits
列上的Slider
。
## mercury_app.ipynb
company = mr.MultiSelect(value=emp_df.Company_Name.unique(),
choices=emp_df.Company_Name.unique(),
label="Select Companies")
此处,value
参数指的是初始值,choices
以可供选择的值列表显示,而label
是出现在小部件旁边的自定义文本。
接下来,我们有滑块小部件。
## mercury_app.ipynb
credits_filter = mr.Slider(value=1,
min=emp_df.Credits.min(),
max=emp_df.Credits.max(),
label="Credits Filter", step=1)
此处,value
参数定义初始值,min
和max
指的是值的范围,label
如之前所述,是自定义文本。最后,step
定义滑块小部件的步长值。
到此为止,我们完成了交互性的小部件添加。最后一步是根据小部件中的值创建图表。
#4) 填充输出面板
首先,我们根据从小部件接收的值过滤数据框。您可以使用WidgetObj.value
属性访问它。
换句话说,要检索company
小部件的值,我们可以引用company.value
属性。
## mercury_app.ipynb
new_df = emp_df[(emp_df.Company_Name.isin(company.value)) &
(emp_df.Credits>=int(credits_filter.value))]
接下来,使用 Markdown 输出小部件,我们打印过滤后的数据框的维度。
## mercury_app.ipynb
mr.Md(f"""The DataFrame has {new_df.shape[0]} rows
and {new_df.shape[1]} columns.""")
Mercury 的 Markdown 有一个很酷的功能,那就是您还可以使用 f-strings,如上所示。
最后,我们创建图表:
## mercury_app.ipynb
fig, ax = plt.subplots(1, 2, figsize = (16, 9))
sns.scatterplot(data = new_df, ax = ax[0],
x = "Employee_Rating", y = "Employee_Salary") ## scatter plot
sns.countplot(x = new_df.Employment_Status, ax = ax[1]) ## count plot
plt.show();
就这样。现在我们的 Mercury 应用已经准备好了。
#5) 运行网络应用
要运行应用,请在命令行中导航到应用的文件夹并运行以下命令:
mercury run
因此,我们看到了以下内容:
首次查看网络应用(作者图片)
正如预期的那样,我们有一个上传文件的小部件。让我们在这里上传虚拟数据集。
上传数据集(作者图片)
一旦我们上传了 CSV,图表立即显示出来。
现在,我们可以操控输入小部件来分析数据。
数据集分析(作者图片)
随着我们更新过滤器,图表和行数也会更新。这是通过 Mercury 服务器实现的,它在笔记本和应用之间保持持续的交互。
实际上,如果我们更新笔记本,变化会立即反映出来。
Mercury 与 Streamlit
此时一个相关的问题是水银相对于 Streamlit 的表现如何,Streamlit 是我在许多之前的博客中使用过的,如这里和这里。
Streamlit 确实已成为创建网页应用的最常见选择之一。尽管整体体验非常棒,但 Streamlit 也确实有很多限制:
#1) 无 Jupyter 支持
由 Streamlit 驱动的应用程序主要由 Python 脚本提供支持,而不是互动的 Python 内核。因此,在使用 Streamlit 开发应用程序时,必须重复运行脚本以查看进展。
水银与 Streamlit — Jupyter 支持(图片来源:作者)
然而,使用水银创建的应用程序由 Jupyter Notebook 驱动,每次更新都会立即反映在网页应用中。
#2) 导出为 PDF/HTML
使用水银创建的网页应用可以通过点击按钮轻松导出。
水银与 Streamlit — 导出应用程序(图片来源:作者)
这使您可以轻松地通过电子邮件、聊天等方式与他人分享您的应用程序,接收者不一定需要安装 Mercury。
然而,Streamlit 并不提供这种支持。
#3) 创建演示文稿
最后,使用水银创建的网页应用可以轻松地作为互动演示运行。
水银与 Streamlit — 演示(图片来源:作者)
然而,Streamlit 应用程序并不提供这样的支持。
#4) 使用认证保护应用程序
有时候,确保只有授权用户才能访问您的应用程序可能非常重要。这可能是由于存在敏感信息。
水银与 Streamlit — 安全性(图片来源:作者)
使用水银,您可以立即启用认证以保护您的网页应用。Streamlit 原生不支持认证。
因此,当有人运行您的网页应用时,他们将被提示验证其详细信息,如下所示:
水银认证窗口(图片来源:作者)
结论
这样,我们就结束了这篇博客。
在这篇文章中,我们学习了如何从 Jupyter Notebook 的舒适环境中使用水银构建一个简单的网页应用。
最后,我们查看了水银与 Streamlit 的比较以及它如何服务于各种数据通信用例。
作为结尾,您还可以通过Mercury Cloud将您的笔记本托管在云端。只需上传笔记本即可完成。
如果您不希望将网页应用专门托管在 Mercury Cloud 上,也可以将其部署在任何带有 Docker 的服务器上。
感谢阅读!
使用检索增强生成技术构建行业特定的语言模型
组织们正在竞相采用大型语言模型。让我们深入了解如何通过 RAG 构建行业特定的语言模型。
·发布在 Towards Data Science ·阅读时间 10 分钟·2023 年 5 月 31 日
–
公司可以通过像 ChatGPT 这样的语言模型获得大量生产力提升。但试着问 ChatGPT “美国当前的通货膨胀率是多少”,它给出的答案是:
对于混淆表示歉意,但作为一个人工智能语言模型,我没有实时数据或浏览能力。我的回答基于截至 2021 年 9 月的信息。因此,我无法提供美国当前的通货膨胀率。
这就是一个问题。ChatGPT 明显缺少相关的及时上下文,而这些上下文在做出明智决策时可能至关重要。
微软是如何解决这个问题的
在微软的 Build 会议中 Vector Search Isn’t Enough,他们展示了将较少上下文感知的语言模型与向量搜索结合的产品,以创造更具吸引力的体验。
讲座从与本文相反的方向开始——从 Elastic Search(或向量搜索)的角度——以及搜索本身的局限性,并且添加语言模型层可以大幅提升搜索体验。
基本思路是,向语言模型中添加相关上下文可以大幅提升用户体验,尤其是在大多数商业场景中,在这些场景下,语言模型未曾见过这些数据。向量搜索帮助选择相关上下文,当你拥有大量数据,包括数百份(或更多)文档时。
向量搜索 101
向量搜索 101 | Skanda Vivek
向量化是将数据转换为维度。在上述情况下,你可以看到 2 个维度:大小和类型。大小有 2 个值(小或大),类型有 2 个值(树或动物)。这只是一个概念性示例,可以扩展到数百个(或更多值)。
向量搜索 101 与词汇 | Skanda Vivek
你可以用单词或句子做同样的事情,而不是图片。注意在上述示例中,向量化能够捕捉到语义表示,即它知道谈论一只鸟俯冲到小松鼠上的句子应该位于(小,动物)象限,而谈论昨天风暴中一棵大树倒在路上的句子应该位于(大,树)象限。实际上,维度不止 2 个——句子转换嵌入有 384 个维度(句子或文本片段通过 BERT 转换器,输出 384 个数字,对应 384 个维度)。
向量搜索 + LLM(RAG)
向量搜索现在在所有搜索引擎中都很常见(Google、Bing,选择你喜欢的搜索引擎)。想象一下,如果你的整个搜索数据库仅包含这 4 份文档。你可以提出问题,“告诉我关于一棵大树的事情。”
输出将是:
在昨天的风暴中,一棵大树倒在了路上。
仅仅是数据库中最相关的文档。但如果你向 LLM 提出相同的问题(并在最后附上这 4 份“文档”),你会得到这样的结果(以 ChatGPT 为例):
一棵大树通常是一个大型的、耸立的植物,具有坚固的树干、枝条和广泛的根系。这些树木可以达到令人印象深刻的高度,通常高出周围的植被。它们是许多生态系统的重要组成部分,为各种生物提供遮荫、庇护和食物。
你提到的关于一棵大树在风暴中倒在路上的事件表明了恶劣天气条件的力量和影响。强风的风暴可以连根拔起或折断树木,导致道路阻塞,对车辆和行人构成潜在的危险。
在你提到的另一个场景中,当你在班迪布尔的道路上行驶时,你遇到了一只大象穿过道路。班迪布尔是印度的一座国家公园和野生动物保护区,以其丰富的生物多样性和各种动物群体而闻名。大象是班迪布尔的主要物种之一,通常会穿越该地区,有时会穿过道路。这些遭遇突显了野生动物保护的重要性以及人类与动物在其自然栖息地共存的必要性。
转到盆景,它们是生长在小花盆中的迷你树木,通过精心修剪来保持其小巧的尺寸。盆景栽培是一种起源于东亚的古老艺术形式,现已在全球范围内获得普及。由于其根部空间有限,盆景树需要定期浇水以防止干枯。小花盆可能导致土壤迅速干燥,因此必须监控湿度水平并相应地浇水。
最后,你提到通过窗户观察到一只鸟俯冲向一只小花栗鼠。这一观察反映了自然行动的一个瞬间,其中一只鸟可能在寻找食物。猛禽和某些鸟类,如鹰或猫头鹰,通常会俯冲捕捉像花栗鼠这样的微小哺乳动物,作为其自然猎食行为的一部分。目睹这种互动非常吸引人,因为它们提供了关于动物王国动态的见解。
总的来说,这些自然片段——无论是涉及大树、大象、盆景还是鸟类捕食——都突显了围绕我们自然世界的多样性和奇观。
那个回答似乎更加全面。ChatGPT 利用了其从数万亿文档中训练得到的内部知识,并结合了所提供的上下文。
但假设你有的不仅仅是 4 个文档——可能是成千上万甚至百万个,无法全部放入 ChatGPT 的提示中。在这种情况下,你可以使用向量搜索来缩小最可能包含答案的上下文,将其附加到提示中,并按照以下方式提问:
这是它现在给出的(截断的)答案:
ChatGPT 回答 | Skanda Vivek
然后你可以有一个数据库,存储文档和嵌入。你可以有另一个数据库,存储查询,并根据查询找到最相关的文档:
文档数据库(左)和 Quey 数据库(右) | Skanda Vivek
一旦你通过查询获得最相似的文档,你可以将其输入到任何像 ChatGPT 这样的语言模型中。通过这个简单的技巧,你已经增强了你的语言模型,使用了文档检索!这也被称为检索增强生成(RAG)。
使用 RAG 构建行业特定的问答模型
RAG 原型 | Skanda Vivek
上面的图表概述了如何构建一个基本的 RAG,该 RAG 利用 LLM 对自定义文档进行问答。第一部分是将多个文档拆分成可管理的片段,相关的参数是 最大片段长度。这些片段应为包含回答典型问题所需的文本的典型(最小)大小。这是因为有时你提出的问题可能在文档的多个位置都有答案。例如,你可能会问“X 公司在 2015 年到 2020 年的表现如何?”而你可能有一份大文档(或多份文档)包含公司多年表现的具体信息,这些信息分布在文档的不同部分。你理想的做法是捕获包含这些信息的文档的所有不同部分,将它们链接在一起,然后传递给 LLM 进行基于这些过滤和拼接后的文档片段的回答。
最大上下文长度 基本上是将各种片段拼接在一起的最大长度——为问题本身和输出答案留出一些空间(记住,像 ChatGPT 这样的 LLM 有严格的长度限制,包括所有内容:问题、上下文和答案)。
相似度阈值 是将问题与文档片段进行比较的方式,以找到最有可能包含答案的顶部片段。余弦相似度是通常使用的度量,但你可能希望对不同的度量进行加权。例如,可以包括一个关键词度量,以便对包含特定关键词的上下文给予更多权重。例如,当你要求 LLM 总结一份文档时,你可能希望对包含“摘要”或“总结”这两个词的上下文给予更多权重。
如果你想测试生成性问答在自定义文档上的简单方法,可以查看我的 API 和 代码,它们在后台使用了 ChatGPT。
原型 ChatGPT 通过 RAG 增强
让我们通过一个例子来说明 RAG 的实用性。EMAlpha 是一家提供新兴市场洞察的公司——基本上是像印度、中国、巴西等新兴国家的经济(完全披露——我在 EMAlpha 担任顾问)。该公司正在开发一个 ChatGPT 驱动的应用程序,根据用户输入生成对新兴经济体的洞察。仪表盘大致如下——你可以比较 ChatGPT 的输出与能够在后台从 IMF 查询财务文档的 RAG 版本 ChatGPT(EM-GPT)的结果:
EMAlpha 的 EM-GPT | Skanda Vivek
以下是 ChatGPT 对问题“尼泊尔按年份的 GDP 是多少?”的回答:
ChatGPT 响应 | Skanda Vivek
ChatGPT 只返回了 2019 年之前的 GDP 数据,并表示如果需要更多信息,可以查看 IMF。但是,如果你想找出这些数据在 IMF 网站上的具体位置,这很困难,你需要对网站上的文档存储位置有个大致了解。经过一些搜索,你可以在这里找到文档。即便如此,确定 GDP 信息的具体位置仍需要大量的滚动。
IMF 关于尼泊尔经济的文档 | Skanda Vivek
如上所示,找到这些数据确实很困难。但当你问 EM-GPT 同样的问题时,它会追踪到相关上下文,并找到如下答案:
EM-GPT 答案 | Skanda Vivek
以下是发送给 ChatGPT 以回答这个问题的确切提示。它能够理解这个格式化的文本,提取正确的信息——并将其格式化成一个易读的格式,这一点相当令人印象深刻!
使用基于查询检索的上下文的 ChatGPT 提示 | Skanda Vivek
我花了半小时在 IMF 网站上查找这些信息,而 RAG 修改版的 ChatGPT 只用了几秒钟。仅使用向量搜索是不够的,因为它最多只能找到“名义 GDP”这段文本,而不会将数字与年份关联起来。ChatGPT 在过去已经训练过多个此类文档,因此一旦添加了相关上下文,它就知道文本中的哪些部分包含答案以及如何以良好的可读格式格式化这个答案。
结论
RAG 提供了一种很好的方式来使用基于自定义文档的 LLMs。像微软、谷歌和亚马逊这样的公司正在竞相开发组织可以以即插即用的方式使用的应用程序。然而,这一领域仍处于初期阶段,利用向量搜索驱动的 LLMs 对自定义文档进行行业特定应用的公司可以成为首批先行者并超越竞争对手。
虽然有人问我应该使用哪个 LLM,以及是否需要对自定义文档进行微调或完全训练模型,LLM 与向量搜索之间的同步工程作用被低估了。 以下是一些可以显著提高或降低响应质量的考虑因素:
-
文档块的长度。 如果正确答案更可能分布在文本的不同部分并需要拼接在一起,则文档应分割成较小的块,以便可以将多个上下文附加到查询中。
-
相似性和检索度量。 有时候,单纯的余弦相似度是不够的。例如,如果许多文档包含关于同一主题的冲突信息,你可能需要根据这些文档中的元数据限制搜索范围。为此,除了相似度,还可以使用其他过滤度量。
-
模型架构: 我展示的架构是一个原型。为了提高效率和可扩展性,需要考虑多个方面,包括向量嵌入模型、文档数据库、提示、LLM 模型选择等。
-
避免幻觉: 你可能注意到我上面展示的例子是几乎正确的。增强版 ChatGPT 对尼泊尔 GDP 的金额是正确的——但年份是错误的。在这种情况下,需要在选择提示、以 ChatGPT 友好的格式提取数据和评估在多少情况下会出现幻觉以及哪些解决方案有效之间进行大量反馈。
现在你已经知道如何将 LLM 应用到你的自定义数据上,快去构建令人惊叹的 LLM 基础产品吧!
如果你喜欢这篇文章,请关注我——我写的内容涉及在实际应用中应用最先进的 NLP 技术,以及数据与社会之间的交集。
随时通过 LinkedIn与我联系!
如果你还不是 Medium 会员并希望支持像我这样的作者,请通过我的推荐链接注册: https://skanda-vivek.medium.com/membership
以下是一些相关的文章:
构建低延迟和可扩展的 ML 模型预测管道,使用 Spark Structured Streaming 和 MLflow
MLOps 实践系列——分享关键 MLOps 组件的设计和实施模式。今天的文章重点是构建模型预测管道。
·发布于 Towards Data Science ·阅读时间 8 分钟·2023 年 1 月 10 日
–
为了使 ML 模型在实际生产环境中工作,最关键的步骤之一是将训练好的模型部署以进行预测。模型部署(发布)是一个过程,使你能够将训练好的 ML 模型集成到生产环境中,以对实际数据做出决策。谈到模型部署,通常有两种类型:
-
一种是批量预测,即在某个时间间隔(如每天一次或每周一次,具体取决于模型在特定业务背景下的使用方式)调用训练好的模型,并将一批数据输入模型,以定期生成预测结果。
-
另一种是在线预测,即将训练好的模型打包成 REST API 或容器化微服务,模型通过响应 API 请求返回预测结果(通常为 JSON 格式)。使用在线预测,模型实时生成预测结果,即一旦 API 被调用,模型预测结果将立即返回。此外,模型 REST API 通常作为 web 应用的一部分集成,以供最终用户或下游应用进行交互。
然而,在批量预测和在线预测之间,我们已经看到越来越多的场景,其中模型不需要打包成 REST API,但模型预测所需的延迟非常低。因此,为了满足这些场景的需求,我想分享一个解决方案——使用 Spark Structured Streaming 和 MLflow 构建低延迟和可扩展的 ML 模型预测管道。
今天文章的内容是:
-
快速介绍 Spark Structured Streaming 和 mlflow;
-
低延迟和可扩展的机器学习预测管道的关键架构组件;
-
使用 Spark Structured Streaming 和 mlflow 构建低延迟和可扩展的机器学习预测管道的实现细节;
让我们开始吧!
照片由 Jen Theodore 提供,来源于 Unsplash
Spark Structured Streaming 和 mlflow 简介
Spark Structured Streaming — Structured Streaming 是一个基于 Spark SQL 引擎构建的可扩展且容错的流处理引擎。内部默认情况下,Structured Streaming 查询通过微批处理引擎进行处理,该引擎将数据流处理为一系列小批量作业,从而实现低至 100 毫秒的端到端延迟和精确一次的容错保证。
MLflow — MLflow 是一个用于管理整个机器学习生命周期的开源平台。
-
跟踪 — MLflow 的跟踪组件是一个用于记录参数、代码版本、指标和输出文件的 API 和 UI,当运行机器学习代码时以及后续可视化结果时使用。
-
模型 — MLflow 模型是一种标准格式,用于打包机器学习模型,可以在各种下游工具中使用。该格式定义了一种约定,使你可以以不同的“风味”保存模型,以便不同的下游工具理解。内置模型风味可以在这里找到。值得一提的是,python_function模型风味作为 MLflow Python 模型的默认模型接口。任何 MLflow Python 模型都应该能够被加载为 python_function 模型。在今天演示的解决方案中,我们将训练好的模型加载为 python 函数。此外,我们还利用了log_model() 和 load_model(). 的模型 API 调用。
-
模型注册表 — MLflow 的模型注册表组件是一个集中的模型存储库,包含一组 API 和 UI,用于协作管理 MLflow 模型的完整生命周期。它提供模型血统(提供对训练好的 ML 模型的可见性和可追溯性,这些模型来源于特定 MLflow 实验和运行的组合)、模型版本控制、阶段转换(例如从测试阶段到生产阶段)以及注释。
-
项目 — MLflow 项目是一种以可重用和可再现的方式打包数据科学代码的格式,主要基于约定。每个项目实际上是一个包含代码的文件目录或 Git 仓库。
如果你对了解更多关于 Spark Structured Streaming 感兴趣,你可以查看我另一篇文章,专门讨论如何构建流数据管道。
现在让我们深入探讨构建低延迟和可扩展 ML 预测管道的关键架构组件。
低延迟和可扩展 ML 预测管道的关键架构组件
低延迟和可扩展 ML 预测管道 | 作者提供的图片
如上图所示,构建低延迟和可扩展 ML 预测管道需要 3 个关键架构组件:
-
第一步是构建一个流数据管道,以摄取原始数据,将原始数据转换为 ML 特征,并以低延迟将 ML 特征输入到 ML 模型中;
-
第二步是将训练和注册的 ML 模型加载为 Spark 用户定义函数(UDF),以便模型可以并行进行预测,利用 Spark 的分布式计算能力。当需要进行预测的数据量很大时,这一点尤其有用。
-
第三步是将模型预测结果保存到存储在 AWS S3 桶中的 Delta 表中。然后,这些模型预测结果可以用于下游数据消费者和应用程序。例如,你可以在模型预测结果基础上构建一个商业智能(BI)仪表板,以支持业务决策。你还可以构建实时监控机制,根据模型预测生成通知和警报,以提高操作效率。
在展示如何构建低延迟和可扩展 ML 预测管道的实现之前,我们首先需要设置一些前提条件。
- 首先是训练模型的模式,如下文件所示:
{"model_purpose" : "predicts the quality of wine using wine attributes",
"model_flavor" : ["python_function","sklearn"],
# The python_function model flavor serves as a default model interface for MLflow Python models.
# Any MLflow Python model is expected to be loadable as a python_function model.
# This enables other MLflow tools to work with any python model regardless of
# which persistence module or framework was used to produce the model.
"model_algorithm" : "sklearn.linear_model.ElasticNet",
{"model_signature" :
"model_input_schema":[
{"name": "fixed acidity", "type": "string"},
{"name": "volatile acidity", "type": "string"},
{"name": "citric acid", "type": "string"},
{"name": "residual sugar", "type": "string"},
{"name": "chlorides", "type": "string"},
{"name": "free sulfur dioxide", "type": "string"},
{"name": "total sulfur dioxide", "type": "string"},
{"name": "density", "type": "string"},
{"name": "pH", "type": "string"},
{"name": "sulphates", "type": "string"},
{"name": "alcohol", "type": "string"}],
"model_output_schema" [
{"type": "tensor", "tensor-spec": {"dtype": "float64", "shape": [-1]}}
]
},
"model_registry_location" : "runs:/<RUN_ID>/<MODEL_NAME>",
# If you are using mlflow to manage the lifecycle of your models,
# the model is loggged as an artifact in the current run using MLflow Tracking
"model_stage" : "Production",
# With mlflow, you can transition a registered model to one of the stages:
# Staging, Production or Archived.
# In the demo of this article, the model is alreay transitioned to the "production" stage.
"model_owner" : "<MODEL_OWNER_EMAIL/MODEL_OWNER_GROUP_EMAIL>"
}
- 第二个是训练数据和测试数据的模式。确保输入模型的数据模式与模型输入模式匹配对于避免因模式不匹配而导致的预测错误至关重要。数据模式如下所示:
StructType([
StructField('fixed acidity', StringType(), True),
StructField('volatile acidity', StringType(), True),
StructField('citric acid', StringType(), True),
StructField('residual sugar', StringType(), True),
StructField('chlorides', StringType(), True),
StructField('free sulfur dioxide', StringType(), True),
StructField('total sulfur dioxide', StringType(), True),
StructField('density', StringType(), True),
StructField('pH', StringType(), True),
StructField('sulphates', StringType(), True),
StructField('alcohol', StringType(), True),
StructField('quality', StringType(), True)
])
本文中使用的数据来自这里。请随意了解有关数据的更多细节。
现在我们对模型模式和数据模式有了清晰的理解,我们可以开始使用 Spark Structured Streaming 和 MLflow 实现 ML 预测管道。完整的解决方案将在下一节中详细解释。
完整解决方案 — 使用 Spark 结构化流处理和 MLflow 构建低延迟且可扩展的机器学习预测管道
- 第 1 步 — 构建一个流数据摄取管道,以低延迟加载用于预测的数据。结构化流处理允许你通过设置微批间隔来定义数据处理的速度。在今天的演示中,我们将微批间隔设置为 5 分钟,这意味着每 5 分钟,流处理管道将拉取原始数据并调用已部署的机器学习模型进行预测。以下是一个样本流数据摄取管道,将原始数据(CSV 格式)加载到 Spark 流数据框中。
streamingDF = (spark
.readStream
.option("sep",",")
.option("header", "True")
.option("enforceSchema", "True")
.schema(csvSchema)
.csv(<YOUR-CSV-DATA-LOCATION>))
- 第 2 步 — 将注册的模型加载为 Spark 用户定义函数(UDF)函数。
import mlflow
logged_model = 'runs:/<RUN_ID>/<MODEL_NAME>'
# Load model as a Spark UDF.
# Override result_type if the model does not return double values.
loaded_model = mlflow.pyfunc.spark_udf(spark, model_uri=logged_model, result_type='double')
- 第 3 步 — 对流数据框进行预测,并将机器学习模型预测结果保存到 Delta 表中供下游消费者使用。
# Predict on a Spark DataFrame.
from pyspark.sql.functions import struct, col
streamingDF.withColumn('predictions', loaded_model(struct(*map(col, streamingDF.columns))))
完整解决方案
import mlflow
from pyspark.sql.functions import struct, col
from pyspark.sql.types import StructType
logged_model = 'runs:/<RUN_ID>/<MODEL_NAME>'
loaded_model = mlflow.pyfunc.spark_udf(spark, model_uri=logged_model, result_type='double')
checkpointLocation = <STREAMING_CHECKPOINT_LOCATION>
deltaLocation = <PREDICTION_STORAGE_LOCATION>
streamingDF = (spark
.readStream
.option("sep",",")
.option("header", "True")
.option("enforceSchema", "True")
.schema(csvSchema)
.csv(<YOUR-CSV-DATA-LOCATION>)
.withColumn('predictions', loaded_model(struct(*map(col, streamingDF.columns)))))
(streamingDF.writeStream
.format("delta")
.outputMode("append") # .outputMode("complete"), .outputMode("update")
.option("checkpointLocation",checkpointLocation)
.option("path", deltaLocation)
.trigger(processingTime='5 minutes') # trigger(availableNow=True), .trigger(once=True), .trigger(continuous='1 second')
.queryName("streaming csv files")
.start())
总结
感谢阅读今天的文章。希望你可以在你的机器学习驱动的应用程序中利用这一模型部署模式。
在我之前的文章中,MLOps 实践 — 将机器学习解决方案架构拆解为 10 个组件,我将端到端的机器学习解决方案架构拆解为 10 个组件。今天的文章着重解释构建机器学习服务管道的热门模式之一。
我将继续分享 MLOps 实践系列文章,解释关键 MLOps 组件的设计和实现模式。
如果你想在这些文章发布时获得通知,请随时在 Medium 上关注我。我一般每周发布 1 到 2 篇关于数据和 AI 的文章。
如果你想查看更多指南、深入探讨和关于现代高效数据+AI 堆栈的见解,请订阅我的免费通讯 — 高效的数据+AI 堆栈,谢谢!
参考文献
注意:如果你还没有成为 Medium 会员,而且你真的应该成为会员,因为这样你可以无限制访问 Medium,你可以使用我的 推荐链接 注册!
非常感谢你的支持!
多做分析,少做仪表盘
为什么我们应该转向一个以分析为默认第一步的世界
·
关注 发表在Towards Data Science ·7 分钟阅读·2023 年 5 月 13 日
–
自从 Seth Rosen 的trashboard推文已经快 2 年了,但 trashboard 疫情依然真实存在。我们仍然遇到许多公司,他们对拥有成千上万个仪表盘感到奇怪的满意,而他们的分析师则被迫在ad hoc请求的深渊中摸索,只剩下一个 IDE。
当然,所有这些都是出于良好的意图——ad hoc 工作可能很痛苦,而自动化解决方案听起来很有吸引力。更重要的是,仪表盘是一种安全的选择——毕竟我们仍然生活在一个仪表盘被视为分析的默认工具的世界中。
让我们谈谈我们是如何走到这一步的,为什么需要改变,以及我们应该如何前进。简而言之,接下来我将论证:
-
几十年的传统使我们来到了一个奇怪的地步,在这里仪表板占据了优先地位。
-
但仪表板并不总是为组织提供价值的最佳方式,尤其是在前几个之后。
-
如果你相信上述两个观点,你的信念、你的流程、你的工具需要根本性的改变。
图片来源于 Midjourney,已获许可使用。
问题:在帕累托世界中仪表板的优先地位
仪表板与分析,从烹饪的角度来看
所以不要误解——仪表板是很棒的,它们只是有些粗糙。它们可以满足无尽的需求,但在这样做的过程中,它们很少能完美地满足特定的需求。这就像数据的海森堡不确定性原理:你暴露的数据越多,你的洞察力就越不尖锐。仪表板的本质是优先展示数据。它们仅提供数据,而解读则留给用户。当然,某些核心仪表板是绝对必要的。每个业务单元都有核心的指标和事件需要跟踪,这些必须在某处展示——仪表板在这里充当业务的脉搏。但除此之外,每增加一个仪表板的价值会大幅减少。
相对而言,分析则更为尖锐。它们揭示了解读,使得与业务问题的相关性更加精准。如果仪表板像沙拉吧,那么分析就是按需定制的餐点。正如特定的餐点更能满足口味,分析总是更好地满足特定业务需求。另一方面,增量的沙拉吧项目(更多仪表板)则收益递减——它们永远不能完全满足某种特定的需求,但可以粗略地解决大部分饥饿相关的问题。拥有几个这样的仪表板可以带来很大好处,但你不需要无休止地继续构建它们。
一个沙拉吧。图片来源于 Midjourney,已获许可使用。
那么为什么仪表板仍然具有优先地位?
虽然我发现大家通常都同意这种观点,但我们生活在一个奇特的历史时期,仪表板仍然保持着其优先地位。当数据处理速度慢、数据难以处理、仪表板制作繁琐时——核心的仪表板维护是分析师工作的全部。但随着数据仓库速度的加快、分析需求的变化、额外仪表板的收益递减,我们的思维方式从未做出相应的调整。
我们不知不觉中被某种沉没成本谬误的扭曲困住了:我们将所有增量资源都分配给了仪表板,因此我们开始相信仪表板是至关重要的。
图片来源:作者。
我必须承认,仪表盘本身确实具有吸引力:仪表盘让我们能够考虑到高覆盖率进行构建。它们售卖了这样一个梦想:有一天,我们的临时请求量会降为零。有了足够的仪表盘,总有一天我们会解决所有问题及其后续问题。
但不幸的是,经营业务很少是如此的公式化。幂律分布泛滥。优先连接产生了价值和无价值的地方,变化和停滞,积累和空虚。并非所有事物都是平等的。这是帕累托法则的世界,其中雪崩推动变革。在这个世界里,20% 的努力驱动了 80% 的结果。
作者提供的图片。
在这个世界中,分析也不例外。如果你按影响力对你做过的所有工作进行排名,我猜你会发现你的大部分价值来自于一些关键的分析。在这个世界里,仪表盘则陷入困境 — 我们在详尽性上获得的同时,也在从中挤取大量的信息。
我们从这里该去哪里?
我长期以来一直是提升分析师角色的支持者。但最近几个月很少做分析工作,最近收到了第一个临时请求,我承认我立即跳进了数据中。我强烈希望尽快回答问题 — 我视这项工作为一种阻碍,而不是一个机会。这是试图减少我所需的工作,但在这样做的过程中,我从联合创始人跌落到了人类 API。我成为了破坏分析的恶性循环的受害者,尽管自称是这种边缘分析宗教的策展人。
“一切都是自画像。一切都是日记。当你关心某事时,这对每个人来说显而易见。当你关心时,你的投入或纪律是有方向的。大多数人能直觉地感受到。”
- 查克·帕拉尼克
Midjourney 提供的图片,已获得许可使用。
因此,我们需要改变我们的默认设置 — 我们的信念,我们的流程,我们的工具 — 否则我们就会冒着重新陷入同样旧习惯的风险。
信念:数据提取之外的价值。
推动任何行为改变的第一步,仅仅是内化你在做某事时有一个明确的原因。我们需要相信我们的价值不仅限于获取数据。如果你认为你的组织主要需要一组核心仪表板,那就设置它们,并将精力集中在这上面。但如果你有一种直觉,认为你可以在此之外提供更多的 leverage —— 通过帮助人们做出更好的决策,甚至通过保持其他人在交流中的思想诚实 —— 那么内化这一点,并在此假设为真的前提下继续。
过程:默认进行分析,而不是默认生成仪表板。
在某种程度上,我相信我们都希望担任更有影响力的角色,为业务创造更多价值,参与更高层次的战略问题解决。但仅仅认识到这一点是远远不够的,剩下的障碍仍然相当可观。据我估计,从现在开始最令人丧失活力的阻碍是,陷入坏习惯太容易了。我们需要建立系统,使正确的事情做起来容易,做错误的事情变得更难。
“设定目标有助于确定方向,但系统才是取得进展的最佳方式。”
- James Clear,《原子习惯》
如果你已经大量投入了自助系统和一组核心仪表板,我会考虑转向默认进行分析的世界。如果我们花费我们的时间尽力最大化我们工作的有用性(分析),而不是试图最大化我们工作的可扩展性(仪表板),那么我们的工作将会更有用,这是理所当然的。
此外,建立模板和标准,规范工作应该如何进行。为您的团队编写坚实的操作原则。您甚至可以制定严格的政策来强化更好的行为:例如,分析不放在我们的集中、共识的地方就不算数。采用强化良好行为的仪式:每天举行站立会议,展示这项工作;将仪式遵从性融入您的绩效评估中。尽一切可能确保人们做您知道会让他们变得更好的事情。
图片由作者提供。
工具:笔记本?至少,是一个以分析为先的工作流程。
如果我们信奉默认进行分析,我们需要一套工具来颠覆常见的工作模式,并推动我们优先进行分析,然后再是仪表板,而不是反过来。当然,我有偏见 —— 这是构建Hyperquery背后的理念 —— 即使你对我们的愿景并不完全认同,确保你不是为每一次分析都匆忙使用 Tableau,也是一个值得称赞的第一步。至少,首先是讲述故事。通过简单地强迫自己默认将文字作为交付物,而不是数据,你已经在打破一种有害的习惯了。
当我在 Airbnb 时,我经常会收到利益相关者的问题。很长一段时间,我的第一反应是立即跳到 SQL。我们都经历过这种情况:为了提供快速的价值,我们全力以赴地进行少数派报告,现场编码。这种方式有其适用的时间和场合,但我们却过于急切地前往那里,而不是思考——综合并借鉴我们已经进行的过去实验和深度挖掘中的学习。我们未能提供杠杆价值,而是寻求对我们闪亮(无与伦比,当然!)的技术能力的认可。而这悄然成为了他人对我们的期望。
以叙事为首的方式强调对齐、相关性和价值,而非即时性。叙事优先将对话提升到战略贡献的层面。叙事优先向你的利益相关者暗示你的价值在于叙事,而非代码。
最后的评论
分析领域正在发生变革。即使你不认同我所提出的观点,不可否认的是,在过去十年里,我们的行业发生了天文数字的变化。随着新车的出现,也带来了新道路和新法规。至少,我希望这篇文章能促使你思考那个新世界应当是什么样的。
👋 你好!我是 Robert, Hyperquery 的 CPO 和前数据科学家 + 分析师。可以在 LinkedIn 或 Twitter 上找到我——我总是乐于交流。🙂
使用检索增强生成构建更强大的大型语言模型
原文:
towardsdatascience.com/build-more-capable-llms-with-retrieval-augmented-generation-99d5f86e9779
检索增强生成如何通过整合知识库提升您的大型语言模型
·发表于 Towards Data Science ·阅读时间 12 分钟·2023 年 8 月 9 日
–
作者提供的图片:使用 Midjourney 生成
ChatGPT 的局限性
ChatGPT 在生成代码以外的许多实际业务场景中是有限的。这些局限性源自训练数据以及模型的幻觉倾向。在撰写时,如果你询问 Chat-GPT 关于 2021 年 9 月之后发生的事件,你很可能会收到如下回应:
作者提供的图片
这并没有帮助,那么我们如何解决这个问题呢?
选项 1 — 对模型进行训练或微调,以使用最新的数据。
微调或训练模型可能不切实际且昂贵。撇开成本不谈,准备数据集所需的工作量也足以使这一选项不被考虑。
选项 2 — 使用检索增强生成(RAG)方法。
RAG 方法使我们可以让大型语言模型访问最新的知识库。这比从头训练模型或进行微调便宜得多,而且实现起来也更简单。在这篇文章中,我将展示如何使用 RAG 来利用你的 OpenAI 模型。我们将通过对模型进行短期测试,以分析其回答有关 2022 年俄罗斯-乌克兰冲突的问题的能力,知识库来源于 维基百科。
注意:尽管这个话题比较敏感,但由于当前 ChatGPT 模型对此没有相关知识,因此被选择作为讨论的内容。
库和前置条件
你需要一个 OpenAI API 密钥,你可以直接从他们的网站获取,或者按照这个 教程。RAG 使用的框架是 Deepset 的 Haystack,它是开源的。他们提供了 API,允许你在大型语言模型之上构建应用程序。我们还利用了来自 Hugging Face 的句子变换器和变换器库。
句子嵌入帮助模型理解文本
在深入技术细节之前,我们应简要介绍句子嵌入。理解这个概念对获得 RAG 方法如何工作的直觉至关重要。
这可能是一个陈词滥调,尤其是对于那些有数据科学背景的人,但模型实际上并不理解文本,它们只理解数字。语言建模的大部分内容是关于制定有效地将文本编码为数字的方式,目前我们通过句子嵌入来做到这一点。
句子嵌入是一种将句子表示为密集向量的方式,同时保留其语义结构。嵌入是从深度神经网络中的密集层中学习的,该网络的结构可以因网络而异。
更简单地说,句子嵌入可以被认为是我们句子的数字表示,保留了它们的语义信息。我们可以从预训练模型中获取句子嵌入。Hugging Face 通过他们的句子变换器库提供了开源模型。
预处理和存储
在我们可以构建 RAG 启用的模型之前,我们需要预处理和存储文档。让我们探索一下如何做到这一点,但首先注意这个过程的架构,以帮助你理解。
注意:架构图也适用于我们在下一节中定义的抽取式问答管道。
图片由作者提供:高层次架构预处理、向量存储和抽取式问答管道
预处理我们的文档
Haystack 为我们提供了方便的工具来预处理大多数类型的文本文件(包括 .pdf、.txt、.docx)。预处理步骤很简单;我们使用 convert_files_to_docs() 函数读取知识库,该函数可以自动检测文件类型并将其转换为我们需要的下游格式。
Haystack 还提供了一个 PreProcessor 类,使我们能够对文档应用预处理步骤。你应用的步骤将很大程度上取决于你的具体应用。
注意:处理器将允许你将长文档拆分为较小的文档列表,定义为句子。对于问答应用,一个常见的方法是句子之间有一些重叠;我将其设置为大约 30%。
向量存储
我们利用了FAISS,这是 Meta 开发的一个库,用于在我们的句子嵌入中进行高效的相似性搜索。这一点在接下来的章节中将变得更加明显。下面的脚本展示了我们如何为文本文档设置预处理,并建立 FAISS 向量存储。
完整的预处理脚本如下:
作者脚本:文档预处理和 FAISS 向量存储
定义抽取式问答管道
下一步是构建我们的抽取式问答管道。我们的管道由按顺序运行的节点组成,形成一个有向无环图(DAG)。在这种情况下,管道由两个节点组成,一个检索器和一个阅读器。
检索器
检索是根据用户的查询从知识库中找到相关信息的方法。在定义我们的检索器节点时,我们从句子变换器库中指定一个句子嵌入模型,这里我们使用all-mpnet-base-v2,它生成 768 维的嵌入。
一旦我们完全定义了检索节点,我们可以在 FAISS 向量存储中计算并存储句子嵌入。相同的句子嵌入模型用于生成用户查询的句子嵌入。
注意:在选择句子嵌入模型时,计算效率和信息损失之间存在权衡。通常,维度更高的句子嵌入模型能捕捉更多信息,但计算效率较低。
记住,检索器节点的最终目的是找到与查询在语义上相关的信息。这是通过在查询的句子嵌入和我们向量存储中的文档之间执行相似性搜索来完成的。此节点返回最相关的前 k 个句子嵌入作为输出。
阅读器
阅读器节点是一个经过微调的大型语言模型,用于问答任务。在我们的用例中,我们利用Roberta-base-squad2作为我们的阅读器模型。阅读器处理来自检索器的输出和用户的初始查询,并返回相关的文本片段以回答查询。阅读器会对检索器输出中的每个文档进行处理,为每个文档分配一个置信度分数。答案按分数排序,返回前 k 个答案。
管道的完整脚本如下:
作者脚本:定义我们的抽取式问答管道
利用 LLM 驱动的代理
现在我们已经预处理了文档并定义了我们的管道,我们可以构建我们的代理。一个代理由大型语言模型提供支持,在我们的案例中,是 OpenAI 的 gpt-4(或 gpt-3.5-turbo)。
我们使用的代理基于 Zero-shot ReAcT(Reason + Act)提示。我们提示大型语言模型返回任务的口头推理轨迹和行动。代理可以根据我们提供的工具集对口头推理轨迹进行“行动”。代理可以观察并推理使用工具后的输出,帮助它决定下一步行动。
下面的动画展示了我们 ReAct 代理工作的简化视图。
作者 GIF:我们代理工作的简化视图
注意: 研究 表明 ReAct 提示已被证明能够有效减少 LLM 的幻觉。
以下脚本展示了我们如何构建我们的代理:
作者脚本:定义我们的零-shot ReAct 代理
在这个用例中,我们为代理提供的工具是我们之前定义的提取式问答管道。本质上,代理可以解释我们的查询,使用我们的管道作为工具在知识库中找到相关回答,推理是否回答了查询,选择响应,或再次执行该操作以获得更好的答案。
工具简要说明
明确地定义我们的工具非常重要。对于我们的特定用例,我们只有一个工具,但我们需要提供该工具的描述,以便 LLM 驱动的代理知道如何以及何时使用该工具。当一个代理可以访问的不仅仅是一个工具时,这一点变得更加重要。
测试实施
我们已经定义了我们的 RAG 启用模型;让我们通过向它提问关于 2022 年俄乌冲突的琐事来进行测试。我选择了两个问题,每个问题都可以通过维基百科知识库中的信息来回答。除了提供的回答,我还提供了每个代理的工作流程跟踪,你应该仔细检查这些以更好地了解 ReAct 提示如何与代理协同工作。
国际社会如何回应 2022 年俄罗斯入侵乌克兰的事件?
RAG 启用模型的回应:
国际社会对 2022 年俄罗斯入侵乌克兰的反应是谴责并施加新的制裁。这些制裁是全面的,涉及个人、银行、企业以及包括货币交换、银行转账、出口和进口在内的各种经济活动。具体措施之一包括最高 15 年的监禁。
注意:最高 15 年的监禁似乎是一个幻觉。模型错误地将其建议为制裁措施。
作者提供的 Gif:回应“国际社会如何回应俄罗斯在 2022 年入侵乌克兰?”的问题。
来自 Google 搜索的回应:
作者提供的图片:来自 Google 搜索的回应:“国际社会如何回应俄罗斯在 2022 年入侵乌克兰?”
以下是追踪代理工作流的记录:
Agent custom-at-query-time started with
{'query':
'How did the international community respond to
Russia’s invasion of Ukraine in 2022?', 'params': None}
The 'transcript' parameter is missing from
the Agent's prompt template. All ReAct agents
that go through multiple steps to reach
a goal require this parameter. Please append
{transcript} to the end of the Agent's
prompt template to ensure its proper functioning.
A temporary prompt template with {transcript} appended
will be used for this run.
find out the details about the international
community's response to Russia's invasion of Ukraine
in 2022.
I don't have this information readily available,
so I will use the document_qa tool to
find it.
Tool: document_qa
Tool Input: "International community response to Russia's invasion of Ukraine
in 2022"
Batches: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 13.05it/s]
Inferencing Samples: 100%|█████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:09<00:00, 9.98s/ Batches]
Observation: 308
Thought: The document_qa tool provided a response
code of 308, which is not informative.
I need to try again to get
the required information.
Tool: document_qa
Tool Input: "How did the international community respond to Russia's invasion
of Ukraine in 2022?"
Batches: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 9.72it/s]
Inferencing Samples: 100%|█████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:07<00:00, 7.78s/ Batches]
Observation: condemnation
Thought: The document_qa tool responded with "condemnation".
This suggests that the international community condemned
Russia's invasion of Ukraine in 2022\. However,
this is a very broad response and
doesn't provide specific details about the actions
taken by the international community. I need
to find more detailed information.
Tool: document_qa
Tool Input: "What specific actions did the international community take in response to Russia's invasion of Ukraine in 2022?"
Batches: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 13.41it/s]
Inferencing Samples: 100%|█████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:07<00:00, 7.95s/ Batches]
Observation: new sanctions
Thought: The document_qa tool responded with "new
sanctions". This suggests that the international community
imposed new sanctions on Russia in response
to their invasion of Ukraine in 2022.
However, this still doesn't provide a comprehensive
picture of the international community's response. I
need to find more detailed information.
Tool: document_qa
Tool Input: "What were the new sanctions imposed by the international community on Russia in 2022?"
Batches: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 12.66it/s]
Inferencing Samples: 100%|█████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:10<00:00, 10.77s/ Batches]
Observation: prison sentences of up to 15 years
Thought: The document_qa tool responded with "prison
sentences of up to 15 years". This
suggests that part of the sanctions imposed
by the international community on Russia in
2022 included prison sentences of up to
15 years. However, this seems to be
a specific measure and may not represent
the full range of sanctions. I need
to find more detailed information.
Tool: document_qa
Tool Input: "What other sanctions were imposed by the international community on Russia in 2022?"
Observation: individuals, banks, businesses, monetary exchanges,
bank transfers, exports, and imports
Observation: individuals, banks, businesses, monetaryexchanges, bank transfers,
exports, and importsThought: The document_qa tool responded
with "individuals, banks, businesses, monetary exchanges, bank
transfers, exports, and imports". This suggests that
the sanctions imposed by the international community
on Russia in 2022 were comprehensive, affecting
individuals, banks, businesses, and various types of
economic activity including monetary exchanges, bank transfers,
exports, and imports.
Final Answer: The international community responded
to Russia's invasion of Ukraine in 2022
with condemnation and the imposition of new
sanctions. These sanctions were comprehensive, affecting individuals,
banks, businesses, and various types of economic
activity including monetary exchanges, bank transfers, exports,
and imports. Some of the specific measures
included prison sentences of up to 15 years.
谁领导了 2022 年 8 月 29 日调查该工厂的 IAEA 团队?
RAG 支持的模型的回应:
拉斐尔·格罗西领导了 2022 年 8 月 29 日调查该工厂的国际原子能机构(IAEA)团队。
作者提供的 Gif:回应“谁领导了 2022 年 8 月 29 日调查该工厂的 IAEA 团队?”的问题。
来自 Google 搜索的回应:
作者提供的图片:来自 Google 搜索的回应:“谁领导了 2022 年 8 月 29 日调查该工厂的 IAEA 团队?”
以下是追踪代理工作流的记录:
Agent custom-at-query-time started with {'query': 'Who
led the IAEA team that investigated the
plant on 29 August\xa02022?', 'params': None}The 'transcript'
parameter is missing from the Agent's prompt
template. All ReAct agents that go through
multiple steps to reach a goal require
this parameter. Please append {transcript} to the
end of the Agent's prompt template to
ensure its proper functioning.
A temporary prompt template with {transcript} appended will
be used for this run.
find out what the IAEA is and
what plant was investigated on 29 August 2022.
Then, I need to find out who led
the IAEA team during this investigation.
I don't have this information at hand,
so I will use the document_qa tool
to find it.
Tool: document_qa
Tool Input: Who led the IAEA team that
investigated the plant on 29 August 2022?
Observation: Rafael Grossi
Thought: The document_qa tool has provided the name
Rafael Grossi in response to the query.
This suggests that Rafael Grossi led the
IAEA team that investigated the plant on
29 August 2022.
Final Answer: Rafael Grossi led the IAEA team
that investigated the plant on 29 August 2022.
出于你的好奇,可以查看ChatGPT 的回应,你也可以尝试自己提问以确认回应。
结论
检索增强生成(RAG)使大型语言模型能够连接到现有的知识库。支持 RAG 的语言模型可以访问最新的信息,使其在各种使用场景中更为实用。
检索增强生成(RAG)方法使模型能够查询大量文本,从而克服了大型语言模型自身面临的上下文限制问题。像 Haystack 这样的开源框架使得快速构建 RAG 支持的 LLM 原型变得简单。
需要注意的一些关键点是,该方法的性能仅与提供的知识库的质量相当。此外,通过在合适的基础设施上部署模型,可以显著加快推理时间。
对于更复杂的查询,代理可能会遇到无法在分配的步骤数量内回应的情况。观察增加步骤数量或加入记忆组件以使流程更具对话性会很有趣。利用更高效的句子嵌入模型也可能有助于提高整体性能。
代码库在GitHub仓库中(包括前端),你可以在这里进行实验。
这里还有一个项目的YouTube演示。
感谢阅读。
如果你希望提升人工智能技能,可以加入我的课程的等待名单,在那里我将引导你开发基于大型语言模型的应用程序。
如果你希望为你的业务实现 AI 转型,今天就预约一次发现电话。
在 Brainqub3,我们开发定制的人工智能软件。我们使用最新的人工智能技术创建 qub3s,即先进的人工智能大脑,以…
www.brainqub3.com](https://www.brainqub3.com/?source=post_page-----99d5f86e9779--------------------------------)
欲了解更多关于人工智能、数据科学和大型语言模型的见解,您可以订阅YouTube频道。