构建你的第一个 LLM 应用所需知道的一切
原文:
towardsdatascience.com/all-you-need-to-know-to-build-your-first-llm-app-eb982c78ffac
一步一步的教程,涵盖文档加载器、嵌入、向量存储和提示模板
·发表于 Towards Data Science ·阅读时长 26 分钟·2023 年 6 月 22 日
–
使用上下文注入构建自己的聊天机器人 — 作者图像
目录
如果你只是想找一个简短的教程,说明如何构建一个简单的 LLM 应用,你可以跳到第 “6. 创建向量存储” 部分,在那里你可以找到构建最小化 LLM 应用所需的所有代码片段,包括向量存储、提示模板和 LLM 调用。
简介
为什么我们需要 LLM
微调 vs. 上下文注入
什么是 LangChain?
逐步教程
1. 使用 LangChain 加载文档
2. 将文档拆分成文本块
3. 从文本块到嵌入
4. 定义你想使用的 LLM
5. 定义我们的提示模板
6. 创建一个向量存储
目录
为什么我们需要 LLM
语言的发展使我们人类走得非常远。它使我们能够高效地分享知识并以我们今天所知道的形式进行合作。因此,我们大部分的集体知识仍然通过无组织的书面文本保存和传递。
在过去二十年里,数字化信息和过程的举措通常专注于在关系数据库中积累越来越多的数据。这种方法使传统的分析机器学习算法能够处理和理解我们的数据。
尽管我们广泛努力以结构化的方式存储越来越多的数据,但仍然无法捕获和处理我们全部的知识。
大约 80% 的公司数据是非结构化的,例如工作描述、简历、电子邮件、文本文件、PowerPoint 幻灯片、语音录音、视频和社交媒体
企业中的数据分布 — 作者提供的图片
GPT3.5 的开发和进步标志着一个重要的里程碑,因为它使我们能够有效地解释和分析各种数据集,无论其结构如何。如今,我们拥有能够理解和生成多种内容形式的模型,包括文本、图像和音频文件。
那么我们如何利用它们的能力来满足我们的需求和数据呢?
微调与上下文注入
一般来说,我们有两种基本的方法来使大型语言模型回答 LLM 无法知道的问题:模型微调和上下文注入
微调
微调指的是使用额外的数据对现有的语言模型进行训练,以使其优化特定任务。
不同于从零开始训练语言模型,使用预训练模型如 BERT 或 LLama,并通过添加特定任务的训练数据来适应特定任务的需求。
斯坦福大学的一个团队使用了 LLM Llama,并通过使用 50,000 个用户/模型交互的示例对其进行了微调。结果是一个与用户互动并回答查询的聊天机器人。这一步微调改变了模型与最终用户的交互方式。
→ 关于微调的误解
PLLMs(预训练语言模型)的微调是一种调整模型以适应特定任务的方法,但它并不能真正将您的领域知识注入模型。这是因为模型已经在大量的通用语言数据上进行过训练,而您的特定领域数据通常不足以覆盖模型已经学到的内容。
因此,当你微调模型时,它可能偶尔会提供正确的答案,但它通常会失败,因为它在很大程度上依赖于在预训练期间学到的信息,这些信息可能不准确或与您的特定任务无关。换句话说,微调帮助模型适应它的交流方式,但不一定是它交流的内容。(保时捷股份公司,2023)
这就是上下文注入发挥作用的地方。
上下文学习 / 上下文注入
在使用上下文注入时,我们并没有修改 LLM,而是专注于提示本身,并将相关的上下文注入到提示中。
因此,我们需要考虑如何为提示提供正确的信息。在下图中,您可以看到整个过程的示意图。我们需要一个能够识别最相关数据的过程。为此,我们需要使计算机能够比较文本片段。
我们非结构化数据中的相似性搜索 — 作者提供的图片
这可以通过嵌入(embeddings)来完成。通过嵌入,我们将文本转换为向量,从而允许我们在多维嵌入空间中表示文本。在空间中彼此更接近的点通常用于相同的上下文。为了防止这种相似性搜索耗时过长,我们将向量存储在向量数据库中并对其进行索引。
微软向我们展示了这可能如何在 Bing Chat 中实现。Bing 结合了 LLM 理解语言和上下文的能力与传统网络搜索的效率。
这篇文章的目标是展示创建一个简单解决方案的过程,使我们能够分析自己的文本和文档,然后将从中获得的见解融入到解决方案返回给用户的答案中。我将描述实现端到端解决方案所需的所有步骤和组件。
那么我们如何利用 LLM 的能力来满足我们的需求呢?让我们一步一步地来看看。
步骤教程 — 你的第一个 LLM 应用
接下来,我们希望利用 LLM 来回应有关我们个人数据的询问。为此,我开始将个人数据的内容转移到向量数据库中。这个步骤至关重要,因为它使我们能够高效地搜索文本中的相关部分。我们将利用来自数据的信息和 LLM 的能力来解释文本,以回答用户的问题。
我们还可以指导聊天机器人仅根据我们提供的数据回答问题。这样,我们可以确保聊天机器人专注于手头的数据,并提供准确且相关的回应。
为了实现我们的用例,我们将大量依赖 LangChain。
LangChain 是什么?
“LangChain 是一个用于开发语言模型驱动应用程序的框架。”(Langchain, 2023)
因此,LangChain 是一个 Python 框架,旨在支持各种 LLM 应用程序的创建,如聊天机器人、摘要工具以及基本上任何你想创建以利用 LLM 能力的工具。该库结合了我们所需的各种组件。我们可以将这些组件连接成所谓的链。
Langchain 最重要的模块是(Langchain, 2023):
-
模型: 各种模型类型的接口
-
提示: 提示管理、提示优化和提示序列化
-
索引: 文档加载器、文本分割器、向量存储 — 实现对数据的更快、更高效的访问
-
链: 链超越了单一的 LLM 调用,它们允许我们设置调用的序列
在下图中,你可以看到这些组件的作用。我们使用索引模块中的文档加载器和文本分割器来加载和处理我们自己的非结构化数据。提示模块允许我们将找到的内容注入到我们的提示模板中,最后,我们通过模型模块将提示发送给我们的模型。
你为 LLM 应用所需的组件 — 作者提供的图像
5. 代理: 代理是使用 LLM 做出关于采取哪些行动的选择的实体。在采取行动后,它们观察该行动的结果,并重复该过程,直到完成任务。
代理自主决定如何执行特定任务 — 作者提供的图片
我们在第一步中使用 LangChain 加载文档,分析它们并使其高效可搜索。在我们索引了文本之后,识别与回答用户问题相关的文本片段应该变得更加高效。
我们的简单应用程序所需的当然是一个 LLM。我们将通过 OpenAI API 使用 GPT3.5。然后我们需要一个向量存储库,以便我们可以将自己的数据提供给 LLM。如果我们想对不同的查询执行不同的操作,我们还需要一个代理来决定每个查询应该发生什么。
我们从头开始。我们首先需要导入我们自己的文档。
以下部分描述了 LangChain 的 Loader 模块中包含哪些模块,以从不同来源加载不同类型的文档。
1. 使用 LangChain 加载文档
LangChain 能够从各种来源加载多个文档。你可以在 LangChain 的文档中找到可能的文档加载器列表。其中包括 HTML 页面、S3 存储桶、PDF 文件、Notion、Google Drive 等等的加载器。
对于我们的简单示例,我们使用的数据可能未包含在 GPT3.5 的训练数据中。我使用关于 GPT4 的维基百科文章,因为我假设 GPT3.5 对 GPT4 的知识有限。
对于这个简单的示例,我没有使用任何 LangChain 加载器,只是直接从维基百科 [许可: CC BY-SA 3.0] 抓取文本,使用了BeautifulSoup.
请注意,抓取网站内容应仅按照网站的使用条款以及你希望使用的文本和数据的版权/许可状态进行。
import requests
from bs4 import BeautifulSoup
url = "https://en.wikipedia.org/wiki/GPT-4"
response = requests.get(url)
soup = BeautifulSoup(response.content, 'html.parser')
# find all the text on the page
text = soup.get_text()
# find the content div
content_div = soup.find('div', {'class': 'mw-parser-output'})
# remove unwanted elements from div
unwanted_tags = ['sup', 'span', 'table', 'ul', 'ol']
for tag in unwanted_tags:
for match in content_div.findAll(tag):
match.extract()
print(content_div.get_text())
2. 将文档拆分成文本片段
接下来,我们必须将文本分成较小的部分,称为文本块。每个文本块代表嵌入空间中的一个数据点,使计算机能够确定这些块之间的相似性。
以下文本片段利用了 langchain 的文本分割模块。在这种特定情况下,我们指定了 100 的块大小和 20 的块重叠。虽然使用更大的文本块很常见,但你可以尝试一下以找到适合你用例的最佳大小。你只需要记住,每个 LLM 都有一个令牌限制(GPT 3.5 为 4000 令牌)。由于我们将文本块插入到提示中,我们需要确保整个提示不超过 4000 个令牌。
from langchain.text_splitter import RecursiveCharacterTextSplitter
article_text = content_div.get_text()
text_splitter = RecursiveCharacterTextSplitter(
# Set a really small chunk size, just to show.
chunk_size = 100,
chunk_overlap = 20,
length_function = len,
)
texts = text_splitter.create_documents([article_text])
print(texts[0])
print(texts[1])
这将我们的整个文本分割如下:
Langchain 文本分割器 — 作者提供的图片
3. 从文本块到嵌入
现在我们需要使文本组件对我们的算法可理解和可比。我们必须找到一种将人类语言转换为数字形式(由比特和字节表示)的方法。
这张图片提供了一个简单的例子,对大多数人类来说可能显而易见。然而,我们需要找到一种方法,让计算机理解“Charles”这个名字与男性相关,而不是女性,并且如果 Charles 是男性,他是国王而不是女王。
使语言对我们的计算机可理解 — 作者提供的图片
近年来,出现了可以做到这一点的新方法和模型。我们所希望的是一种将单词的含义转换为 n 维空间的方法,以便能够比较文本块之间的相似性,甚至计算它们之间的相似度度量。
嵌入模型通过分析单词通常使用的上下文来尝试学习这一点。由于 tea、coffee 和 breakfast 经常在相同的上下文中使用,它们在 n 维空间中彼此更接近,而不是,例如,tea 和 pea。Tea 和 pea 听起来相似,但很少一起使用。(AssemblyAI,2022)
嵌入分析了单词使用的上下文,而不是单词本身 — 作者提供的图片
嵌入模型为嵌入空间中的每个单词提供了一个向量。最终,通过使用向量表示它们,我们能够执行数学计算,例如计算单词之间的相似性,作为数据点之间的距离。
随机的英文单词在二维嵌入空间中 — 作者提供的图片
将文本转换为嵌入有几种方法,例如 Word2Vec、GloVe、fastText 或 ELMo。
嵌入模型
为了捕捉嵌入中单词之间的相似性,Word2Vec 使用了一个简单的神经网络。我们用大量的文本数据训练这个模型,并希望创建一个能够将每个单词分配到 n 维嵌入空间中的点,并以向量的形式描述其含义的模型。
在训练过程中,我们将输入层中的每个独特单词分配给一个神经元。在下面的图片中,你可以看到一个简单的例子。在这个例子中,隐藏层只包含两个神经元。两个神经元是因为我们希望将单词映射到二维嵌入空间中。(现有的模型实际上要大得多,因此在更高维空间中表示单词——例如,OpenAI 的 Ada 嵌入模型使用的是 1536 维)在训练过程后,单独的权重描述了在嵌入空间中的位置。
在这个例子中,我们的数据集由一个句子组成:“Google is a tech company.” 句子中的每个词作为神经网络(NN)的输入。因此,我们的网络有五个输入神经元,每个词一个。
在训练过程中,我们的重点是预测每个输入词的下一个词。当我们从句子的开头开始时,与“Google”相关的输入神经元接收到值 1,而其余神经元接收到值 0。我们的目标是训练网络在这种情况下预测出“is”这个词。
Word2Vec: 学习词嵌入 — 图片由作者提供
实际上,有多种方法可以学习嵌入模型,每种方法都有其独特的预测输出的方式。两种常用的方法是 CBOW(连续词袋模型)和 Skip-gram。
在 CBOW 中,我们将周围的词作为输入,目标是预测中间的词。相反,在 Skip-gram 中,我们将中间的词作为输入,并尝试预测其左侧和右侧的词。然而,我不会深入探讨这些方法的细节。可以说,这些方法为我们提供了嵌入,这些嵌入是通过分析大量文本数据的上下文来捕捉词语之间关系的表示。
CBOW 与 Skip-gram — 图片由作者提供
如果你想了解更多关于嵌入的内容*,互联网上有大量的信息。然而,如果你更喜欢视觉和逐步指导,你可能会觉得观看 Josh* Starmer 关于词嵌入和 Word2Vec 的 StatQuest* 很有帮助。
回到嵌入模型
我刚刚用一个简单的二维嵌入空间示例来解释的内容也适用于更大的模型。例如,标准的 Word2Vec 向量有 300 维,而 OpenAI 的 Ada 模型有 1536 维。这些预训练的向量使我们能够精确地捕捉词语及其含义之间的关系,以至于我们可以用它们进行计算。例如,使用这些向量,我们可以发现法国 + 柏林 — 德国 = 巴黎,同时,快速 + 温暖 — 快速 = 更温暖。 (Tazzyman, n.d.)
使用嵌入进行计算 — 图片由作者提供
在接下来,我们希望使用 OpenAI API,不仅使用 OpenAI 的 LLM,还利用它们的嵌入模型。
注意:嵌入模型和 LLM 之间的区别在于,嵌入模型专注于创建词语或短语的向量表示,以捕捉它们的含义和关系,而 LLM 则是多功能的模型,经过训练可以根据提供的提示或查询生成连贯且符合上下文的文本。
OpenAI 嵌入模型
与 OpenAI 的各种 LLM 类似,您还可以在 Ada、Davinci、Curie 和 Babbage 等各种嵌入模型之间进行选择。其中,Ada-002 目前是最快和最具成本效益的模型,而 Davinci 通常提供最高的准确性和性能。然而,您需要自己尝试,找到适合您使用案例的最佳模型。如果您对 OpenAI Embeddings 有详细了解的兴趣,可以参考OpenAI 文档。
我们使用 Embedding Models 的目标是将文本块转换为向量。在第二代 Ada 的情况下,这些向量具有 1536 个输出维度,这意味着它们在 1536 维空间中表示一个特定的位置或方向。
OpenAI 在其文档中描述了这些嵌入向量如下:
“数值上相似的嵌入也在语义上相似。例如,“canine companions say”的嵌入向量将比“meow”的嵌入向量更接近“woof”的嵌入向量。”(OpenAI,2022)
让我们尝试一下。我们使用 OpenAI 的 API 将文本片段转换为嵌入,如下所示:
import openai
print(texts[0])
embedding = openai.Embedding.create(
input=texts[0].page_content, model="text-embedding-ada-002"
)["data"][0]["embedding"]
len(embedding)
我们将文本,例如包含“2023 text-generating language model”的第一个文本块,转换为 1536 维的向量。通过对每个文本块进行这种处理,我们可以在 1536 维空间中观察哪些文本块彼此更接近,更相似。
让我们尝试一下。我们的目标是通过为问题生成嵌入,并将其与空间中的其他数据点进行比较,从而将用户的问题与文本块进行比较。
哪个文本片段在语义上更接近用户的问题?— 作者提供的图像
当我们将文本块和用户的问题表示为向量时,我们能够探索各种数学可能性。为了确定两个数据点之间的相似度,我们需要计算它们在多维空间中的接近程度,这可以通过距离度量实现。计算点之间距离的方法有很多种。Maarten Grootendorst 在他的 Medium 帖子中总结了其中的九种。
常用的距离度量是余弦相似度。因此,让我们尝试计算问题与文本块之间的余弦相似度:
import numpy as np
from numpy.linalg import norm
from langchain.text_splitter import RecursiveCharacterTextSplitter
import requests
from bs4 import BeautifulSoup
import pandas as pd
import openai
####################################################################
# load documents
####################################################################
# URL of the Wikipedia page to scrape
url = 'https://en.wikipedia.org/wiki/Prime_Minister_of_the_United_Kingdom'
# Send a GET request to the URL
response = requests.get(url)
# Parse the HTML content using BeautifulSoup
soup = BeautifulSoup(response.content, 'html.parser')
# Find all the text on the page
text = soup.get_text()
####################################################################
# split text
####################################################################
text_splitter = RecursiveCharacterTextSplitter(
# Set a really small chunk size, just to show.
chunk_size = 100,
chunk_overlap = 20,
length_function = len,
)
texts = text_splitter.create_documents([text])
####################################################################
# calculate embeddings
####################################################################
# create new list with all text chunks
text_chunks=[]
for text in texts:
text_chunks.append(text.page_content)
df = pd.DataFrame({'text_chunks': text_chunks})
####################################################################
# get embeddings from text-embedding-ada model
####################################################################
def get_embedding(text, model="text-embedding-ada-002"):
text = text.replace("\n", " ")
return openai.Embedding.create(input = [text], model=model)['data'][0]['embedding']
df['ada_embedding'] = df.text_chunks.apply(lambda x: get_embedding(x, model='text-embedding-ada-002'))
####################################################################
# calculate the embeddings for the user's question
####################################################################
users_question = "What is GPT-4?"
question_embedding = get_embedding(text=users_question, model="text-embedding-ada-002")
# create a list to store the calculated cosine similarity
cos_sim = []
for index, row in df.iterrows():
A = row.ada_embedding
B = question_embedding
# calculate the cosine similarity
cosine = np.dot(A,B)/(norm(A)*norm(B))
cos_sim.append(cosine)
df["cos_sim"] = cos_sim
df.sort_values(by=["cos_sim"], ascending=False)
现在我们可以选择我们希望提供给 LLM 以回答问题的文本块数量。
下一步是确定我们希望使用的 LLM。
4. 定义您要使用的模型
Langchain 提供了各种模型和集成,包括 OpenAI 的 GPT 和 Huggingface 等。如果我们决定使用 OpenAI 的 GPT 作为我们的大型语言模型,第一步是定义我们的 API 密钥。目前,OpenAI 提供了一些免费的使用额度,但一旦我们超过每月的令牌数量,我们将需要切换到付费账户。
如果我们像使用 Google 一样用 GPT 来回答简短的问题,成本会相对较低。然而,如果我们使用 GPT 来回答需要提供大量背景信息的问题,例如个人数据,查询很快就会累积成千上万的令牌。这会显著增加成本。但不用担心,你可以设置一个成本限制。
什么是令牌?
简而言之,令牌基本上是一个单词或一组单词。然而,在英语中,单词可以有不同的形式,比如动词时态、复数或复合词。为了解决这个问题,我们可以使用子词令牌化,它将一个单词拆分成更小的部分,如词根、前缀、后缀和其他语言学元素。例如,单词“tiresome”可以拆分为“tire”和“some”,而“tired”可以分为“tire”和“d”。通过这样做,我们可以识别出“tiresome”和“tired”共享相同的词根,并具有类似的词源。(Wang, 2023)
OpenAI 在其网站上提供了一个令牌计算器,让你了解什么是令牌。根据 OpenAI 的说法,一个令牌通常对应于大约 4 个常见英文字符。这大约相当于 ¾ 个单词(因此 100 个令牌 ≈ 75 个单词)。你可以在 OpenAI 网站上的令牌计算器 找到一个应用,帮助你了解什么实际上算作一个令牌。
设置使用限制
如果你担心成本,你可以在 OpenAI 用户门户中找到一个选项来限制每月费用。
你可以在 OpenAI 的用户账户中找到 API 密钥。最简单的方法是用 Google 搜索“OpenAI API key”。这会直接带你到设置页面,以创建新的密钥。
要在 Python 中使用,你必须将密钥保存为名为 “OPENAI_API_KEY” 的新环境变量:
import os
os.environ["OPENAI_API_KEY"] = "testapikey213412"
当你选择要使用的语言模型(LLM)时,可以预设一些参数。 OpenAI Playground 让你在决定使用什么设置之前,可以先试验一下不同的参数。
在 Playground WebUI 的右侧,你会找到 OpenAI 提供的几个参数,这些参数允许我们影响 LLM 的输出。两个值得探索的参数是模型选择和温度。
你可以从各种不同的模型中进行选择。目前,Text-davinci-003 模型是最大的、最强大的。另一方面,像 Text-ada-001 这样的模型更小、更快、成本更低。
下面,你可以看到OpenAI 定价列表的总结。Ada 的费用低于最强大的模型 Davinci。因此,如果 Ada 的表现满足我们的需求,我们不仅可以节省资金,还能实现更短的响应时间。
你可以首先使用 Davinci,然后评估是否可以使用 Ada 获得足够好的结果。
所以让我们在 Jupyter Notebook 中试试吧。我们正在使用 langchain 连接到 GPT。
from langchain.llms import OpenAI
llm = OpenAI(temperature=0.7)
如果你想查看包含所有属性的列表,请使用 dict:
llm.__dict__
如果我们没有指定特定的模型,langchain 连接器默认使用“text-davinci-003”。
现在,我们可以直接在 Python 中调用模型。只需调用 llm 函数并将提示作为输入提供。
现在你可以向 GPT 提问任何关于常见人类知识的问题。
GPT 只能提供有限的信息,关于其训练数据中未包含的主题。这包括不公开的具体细节或训练数据最后更新后发生的事件。
那么,我们如何确保模型能够回答有关当前事件的问题呢?
如前所述,这里有一种方法可以做到这一点。我们需要在提示中提供模型所需的信息。
为了回答有关英国现任首相的问题,我使用了来自维基百科文章“英国首相”的信息。为了总结这个过程,我们正在:
-
加载文章
-
将文本拆分成文本块
-
计算文本块的嵌入
-
计算所有文本块与用户问题的相似度
import requests
from bs4 import BeautifulSoup
from langchain.text_splitter import RecursiveCharacterTextSplitter
import numpy as np
from numpy.linalg import norm
import pandas as pd
import openai
####################################################################
# load documents
####################################################################
# URL of the Wikipedia page to scrape
url = 'https://en.wikipedia.org/wiki/Prime_Minister_of_the_United_Kingdom'
# Send a GET request to the URL
response = requests.get(url)
# Parse the HTML content using BeautifulSoup
soup = BeautifulSoup(response.content, 'html.parser')
# Find all the text on the page
text = soup.get_text()
####################################################################
# split text
####################################################################
text_splitter = RecursiveCharacterTextSplitter(
# Set a really small chunk size, just to show.
chunk_size = 100,
chunk_overlap = 20,
length_function = len,
)
texts = text_splitter.create_documents([text])
####################################################################
# calculate embeddings
####################################################################
# create new list with all text chunks
text_chunks=[]
for text in texts:
text_chunks.append(text.page_content)
df = pd.DataFrame({'text_chunks': text_chunks})
# get embeddings from text-embedding-ada model
def get_embedding(text, model="text-embedding-ada-002"):
text = text.replace("\n", " ")
return openai.Embedding.create(input = [text], model=model)['data'][0]['embedding']
df['ada_embedding'] = df.text_chunks.apply(lambda x: get_embedding(x, model='text-embedding-ada-002'))
####################################################################
# calculate similarities to the user's question
####################################################################
# calcuate the embeddings for the user's question
users_question = "Who is the current Prime Minister of the UK?"
question_embedding = get_embedding(text=users_question, model="text-embedding-ada-002")
现在我们尝试找到与用户问题最相似的文本块:
from langchain import PromptTemplate
from langchain.llms import OpenAI
# calcuate the embeddings for the user's question
users_question = "Who is the current Prime Minister of the UK?"
question_embedding = get_embedding(text=users_question, model="text-embedding-ada-002")
# create a list to store the calculated cosine similarity
cos_sim = []
for index, row in df.iterrows():
A = row.ada_embedding
B = question_embedding
# calculate the cosine similiarity
cosine = np.dot(A,B)/(norm(A)*norm(B))
cos_sim.append(cosine)
df["cos_sim"] = cos_sim
df.sort_values(by=["cos_sim"], ascending=False)
文本块看起来相当混乱,但让我们试试,看 GPT 是否足够聪明来处理它。
现在我们已经识别出可能包含相关信息的文本段落,我们可以测试我们的模型是否能够回答这个问题。为了实现这一点,我们必须以一种清晰地传达我们期望任务的方式构建我们的提示。
5. 定义我们的提示模板
现在我们有了包含我们所寻找的信息的文本片段,我们需要构建一个提示。在提示中我们还指定模型回答问题所需的模式。当我们定义模式时,我们在指定 LLM 生成答案的期望行为风格。
LLM 可以用于各种任务,以下是一些广泛可能性的例子:
-
总结: “将以下文本总结成 3 段,以供高管参考:[TEXT]”
-
知识提取: “基于这篇文章:[TEXT],人们在购买房屋之前应该考虑哪些问题?”
-
撰写内容(例如邮件、消息、代码): 写一封邮件给简,询问我们项目文档的最新情况。使用非正式、友好的语气。”
-
语法和风格改进: “将其改为标准英语,并将语气改为更友好的: [TEXT]”
-
分类: “将每条消息分类为支持票据的类型:[TEXT]”
在我们的例子中,我们希望实现一个从维基百科提取数据并像聊天机器人一样与用户互动的解决方案。我们希望它能够像一个积极、乐于助人的帮助台专家一样回答问题。
为了引导 LLM 向正确方向发展,我在提示中添加了以下指令:
“你是一个喜欢帮助别人的聊天机器人!仅使用提供的上下文回答以下问题。如果你不确定且答案在上下文中没有明确给出,请说‘对不起,我不知道如何帮助你。’”
通过这样做,我设定了一个限制,只允许 GPT 利用我们数据库中的信息。这种限制使我们能够提供聊天机器人生成回应时所依赖的来源,这对追踪来源和建立信任至关重要。此外,它有助于解决生成不可靠信息的问题,并使我们能够提供可用于公司决策的答案。
作为上下文,我仅使用与问题最相似的前 50 个文本块。更大的文本块可能会更好,因为我们通常可以用一到两个文本段落回答大多数问题。但我将把找到最佳大小的任务留给你来完成。
from langchain import PromptTemplate
from langchain.llms import OpenAI
import openai
import requests
from bs4 import BeautifulSoup
from langchain.text_splitter import RecursiveCharacterTextSplitter
import numpy as np
from numpy.linalg import norm
import pandas as pd
import openai
####################################################################
# load documents
####################################################################
# URL of the Wikipedia page to scrape
url = 'https://en.wikipedia.org/wiki/Prime_Minister_of_the_United_Kingdom'
# Send a GET request to the URL
response = requests.get(url)
# Parse the HTML content using BeautifulSoup
soup = BeautifulSoup(response.content, 'html.parser')
# Find all the text on the page
text = soup.get_text()
####################################################################
# split text
####################################################################
text_splitter = RecursiveCharacterTextSplitter(
# Set a really small chunk size, just to show.
chunk_size = 100,
chunk_overlap = 20,
length_function = len,
)
texts = text_splitter.create_documents([text])
####################################################################
# calculate embeddings
####################################################################
# create new list with all text chunks
text_chunks=[]
for text in texts:
text_chunks.append(text.page_content)
df = pd.DataFrame({'text_chunks': text_chunks})
# get embeddings from text-embedding-ada model
def get_embedding(text, model="text-embedding-ada-002"):
text = text.replace("\n", " ")
return openai.Embedding.create(input = [text], model=model)['data'][0]['embedding']
df['ada_embedding'] = df.text_chunks.apply(lambda x: get_embedding(x, model='text-embedding-ada-002'))
####################################################################
# calculate similarities to the user's question
####################################################################
# calcuate the embeddings for the user's question
users_question = "Who is the current Prime Minister of the UK?"
question_embedding = get_embedding(text=users_question, model="text-embedding-ada-002")
# create a list to store the calculated cosine similarity
cos_sim = []
for index, row in df.iterrows():
A = row.ada_embedding
B = question_embedding
# calculate the cosine similiarity
cosine = np.dot(A,B)/(norm(A)*norm(B))
cos_sim.append(cosine)
df["cos_sim"] = cos_sim
df.sort_values(by=["cos_sim"], ascending=False)
####################################################################
# build a suitable prompt and send it
####################################################################
# define the LLM you want to use
llm = OpenAI(temperature=1)
# define the context for the prompt by joining the most relevant text chunks
context = ""
for index, row in df[0:50].iterrows():
context = context + " " + row.text_chunks
# define the prompt template
template = """
You are a chat bot who loves to help people! Given the following context sections, answer the
question using only the given context. If you are unsure and the answer is not
explicitly writting in the documentation, say "Sorry, I don't know how to help with that."
Context sections:
{context}
Question:
{users_question}
Answer:
"""
prompt = PromptTemplate(template=template, input_variables=["context", "users_question"])
# fill the prompt template
prompt_text = prompt.format(context = context, users_question = users_question)
llm(prompt_text)
通过使用那个特定模板,我将上下文和用户的问题都纳入了我们的提示中。生成的回应如下:
出乎意料的是,即使是这个简单的实现也似乎产生了一些令人满意的结果。让我们继续向系统提出更多关于英国首相的问题。我将保持一切不变,只替换用户的问题:
users_question = "Who was the first Prime Minister of the UK?"
它似乎在某种程度上正在运行。然而,我们现在的目标是将这个缓慢的过程转变为一个强大且高效的过程。为此,我们引入了一个索引步骤,在向量存储中存储我们的嵌入和索引。这将提高整体性能并减少响应时间。
6. 创建向量存储(向量数据库)
向量存储是一种优化用于存储和检索可以表示为向量的大量数据的数据存储类型。这些类型的数据库允许根据各种标准(如相似性度量或其他数学操作)高效查询和检索数据的子集。
将我们的文本数据转换为向量是第一步,但这对于我们的需求还不够。如果我们将向量存储在数据框中,并在每次收到查询时逐步搜索单词之间的相似性,那么整个过程将会非常缓慢。
为了高效地搜索我们的嵌入,我们需要对它们进行索引。索引是向量数据库的第二个重要组成部分。索引提供了一种将查询映射到向量存储库中最相关的文档或项目的方法,而无需计算每个查询与每个文档之间的相似性。
近年来,已经发布了许多向量存储库。尤其在 LLM 领域,对向量存储库的关注激增:
近年来发布的向量存储库 — 图片来自作者
现在我们来选择一个并试用一下我们的用例。类似于我们在前面部分所做的,我们再次计算嵌入并将其存储在向量存储库中。为此,我们使用了来自 LangChain 和 chroma 的合适模块作为向量存储库。
- 收集我们想要用来回答用户问题的数据:
图片来自作者
import requests
from bs4 import BeautifulSoup
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import Chroma
from langchain.document_loaders import TextLoader
# URL of the Wikipedia page to scrape
url = 'https://en.wikipedia.org/wiki/Prime_Minister_of_the_United_Kingdom'
# Send a GET request to the URL
response = requests.get(url)
# Parse the HTML content using BeautifulSoup
soup = BeautifulSoup(response.content, 'html.parser')
# Find all the text on the page
text = soup.get_text()
text = text.replace('\n', '')
# Open a new file called 'output.txt' in write mode and store the file object in a variable
with open('output.txt', 'w', encoding='utf-8') as file:
# Write the string to the file
file.write(text)
2. 加载数据并定义如何将数据拆分为文本块
图片来自作者
from langchain.text_splitter import RecursiveCharacterTextSplitter
# load the document
with open('./output.txt', encoding='utf-8') as f:
text = f.read()
# define the text splitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 500,
chunk_overlap = 100,
length_function = len,
)
texts = text_splitter.create_documents([text])
3. 定义要用来计算文本块嵌入的嵌入模型,并将其存储在向量存储库中(这里使用:Chroma)
图片来自作者
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
# define the embeddings model
embeddings = OpenAIEmbeddings()
# use the text chunks and the embeddings model to fill our vector store
db = Chroma.from_documents(texts, embeddings)
4. 计算用户问题的嵌入,找到向量存储库中相似的文本块,并使用它们来构建我们的提示
图片来自作者
from langchain.llms import OpenAI
from langchain import PromptTemplate
users_question = "Who is the current Prime Minister of the UK?"
# use our vector store to find similar text chunks
results = db.similarity_search(
query=user_question,
n_results=5
)
# define the prompt template
template = """
You are a chat bot who loves to help people! Given the following context sections, answer the
question using only the given context. If you are unsure and the answer is not
explicitly writting in the documentation, say "Sorry, I don't know how to help with that."
Context sections:
{context}
Question:
{users_question}
Answer:
"""
prompt = PromptTemplate(template=template, input_variables=["context", "users_question"])
# fill the prompt template
prompt_text = prompt.format(context = results, users_question = users_question)
# ask the defined LLM
llm(prompt_text)
概述
为了使我们的 LLM 能够分析和回答有关我们数据的问题,我们通常不会对模型进行微调。相反,在微调过程中,目标是提高模型有效响应特定任务的能力,而不是教它新的信息。
在 Alpaca 7B 的案例中,LLM(LLaMA)经过微调,以表现和互动像一个聊天机器人。重点是完善模型的回应,而不是教它完全新的信息。
为了能够回答关于我们自己数据的问题,我们使用上下文注入方法。创建一个具有上下文注入的 LLM 应用程序是一个相对简单的过程。主要挑战在于组织和格式化要存储在向量数据库中的数据。这一步对于高效检索上下文相似信息并确保可靠结果至关重要。
本文的目标是展示使用嵌入模型、向量存储和 LLMs 处理用户查询的极简方法。它展示了这些技术如何协同工作,即使面对不断变化的事实,也能提供相关且准确的答案。
喜欢这个故事吗?
随时通过 LinkedIn 联系我!
参考文献
AssemblyAI (导演). (2022 年 1 月 5 日). 完整的词嵌入概述. www.youtube.com/watch?v=5MaWmXwxFNQ
Grootendorst, M. (2021 年 12 月 7 日). 数据科学中的 9 种距离度量. Medium. towardsdatascience.com/9-distance-measures-in-data-science-918109d069fa
Langchain. (2023). 欢迎使用 LangChain — 🦜🔗 LangChain 0.0.189. python.langchain.com/en/latest/index.html
Nelson, P. (2023). 搜索与非结构化数据分析趋势 |
Accenture. 搜索与内容分析博客. www.accenture.com/us-en/blogs/search-and-content-analytics-blog/search-unstructured-data-analytics-trends
OpenAI. (2022). 介绍文本和代码嵌入. openai.com/blog/introducing-text-and-code-embeddings
OpenAI (导演). (2023 年 3 月 14 日). 你可以用 GPT-4 做什么? www.youtube.com/watch?v=oc6RV5c1yd0
Porsche AG. (2023 年 5 月 17 日). ChatGPT 与企业知识:“我如何为我的业务部门创建一个聊天机器人?” #NextLevelGermanEngineering. medium.com/next-level-german-engineering/chatgpt-enterprise-knowledge-how-can-i-create-a-chatbot-for-my-business-unit-4380f7b3d4c0
Tazzyman, S. (2023). 神经网络模型. NLP-Guidance. moj-analytical-services.github.io/NLP-guidance/NNmodels.html
Wang, W. (2023 年 4 月 12 日). 深入了解基于变换器的模型. Medium.
开发大型语言模型所需了解的一切
图像由 Stable Diffusion 生成
用简单的术语解释启动开发 LLM 基础应用所需的核心技术。
·
关注 发表在 Towards Data Science · 12 min read · 2023 年 11 月 15 日
–
本文的目的是用简单的术语解释开始开发基于 LLM 的应用程序所需的关键技术。它面向对机器学习概念有基本了解并希望深入学习的软件开发人员、数据科学家和人工智能爱好者。文章还提供了许多有用的链接供进一步学习。内容会很有趣!
1. 大型语言模型(LLM)简介
我想你已经听了无数次关于 LLM 是什么,所以我不会再过多赘述。我们只需知道:大型语言模型(LLM)是一个大型神经网络模型,根据先前预测的 token 来预测下一个 token。就是这样。
模型参数数量的比较。**只需看看 GPT-3 有多大。**而且没人知道 GPT-4 的情况……
LLMs 的受欢迎程度归因于其多功能性和有效性。它们可以完美地完成翻译、摘要、意义分析等任务。
LLMs 的能力
使用 LLMs 的一些项目示例:
-
Notion AI——帮助提高写作质量,生成内容,纠正拼写和语法,编辑语音和语调,翻译等。
-
GitHub Copilot——通过提供自动补全样式的建议来改进你的代码。
-
Dropbox Dash——提供自然语言搜索功能,并特别指出答案来源于哪些文件。
如果你想详细了解 LLMs 的工作原理,我强烈推荐阅读 Mark Riedl 的优秀文章“一种非常温和的介绍大型语言模型,没有炒作”。
2. 开源与闭源模型
虽然有很多差异,我强调以下几点作为主要差异:
-
隐私——大型公司选择自托管解决方案的最重要原因之一。
-
快速原型制作——非常适合小型初创公司快速测试他们的想法,而不需要过多开支。
-
生成质量——要么为你的特定任务对模型进行微调,要么使用付费 API。
没有明确的答案说明什么更好或更差。我总结了以下几点:
如果你对深入了解细节感兴趣,我建议你阅读我的文章“你真的不需要托管的 LLMs,对吧?”。
流行的开源模型
流行的闭源模型
探索 LLM Collection 以查看所有模型。
3. 提示工程的艺术
我知道,很多人认为这是一门伪科学或只是暂时的炒作。但事实是,我们仍然没有完全理解 LLM 的工作原理。为什么它们有时能提供高质量的回答,而有时却编造事实(幻觉)?或者为什么在提示中添加“让我们逐步思考”会突然提高质量?
添加情感色彩会提高任何模型的质量。来源
正因为如此,科学家和爱好者只能尝试不同的提示,试图使模型表现得更好。
示意图,展示了用 LLM 解决问题的各种方法
我不会用复杂的提示链来让你感到无聊,而是给你一些能立即提高性能的示例:
-
“让我们逐步思考” — 对推理或逻辑任务效果很好。
-
“深呼吸,逐步解决这个问题” — 是前一点的改进版。它可以再提高几个百分点的质量。
-
“这对我的职业生涯非常重要” — 只需将其添加到提示的末尾,你会注意到质量提高 5–20%。
另外,我会立即分享一个有用的提示模板:
让我们结合我们的X命令和清晰的思维,以逐步的方法快速准确地解读答案。提供详细信息并在答案中包括来源。这对我的职业生涯非常重要。
其中X是你解决任务的行业,例如编程。
我强烈建议你花几晚时间探索提示工程技术。这不仅能让你更好地控制模型的行为,还能帮助提高质量并减少幻觉。为此,我推荐阅读提示工程指南。
有用链接:
-
prompttools — 提示测试和实验,支持 LLM(例如 OpenAI,LLaMA)。
-
promptfoo — 测试和评估 LLM 输出质量。
-
Awesome ChatGPT Prompts — 一系列用于 ChatGPT 模型的提示示例集合。
4. 融入新数据:检索增强生成(RAG)
RAG 是一种将 LLM 与外部知识库结合的技术。这使得模型能够添加原始训练集中未包含的相关信息或特定数据。
尽管名字听起来吓人(有时我们会在其前面加上“reranker”一词),但它实际上是一种相当古老且出乎意料简单的技术:
RAG 工作原理的示意图
-
您将文档转换为数字,我们称之为嵌入。
-
然后,您还使用同一模型将用户的搜索查询转换为嵌入。
-
找到前 K 个最接近的文档,通常基于余弦相似度。
-
要求 LLM 基于这些文档生成响应。
适合使用时
-
需要当前信息时: 当应用程序需要不断更新的信息,例如新闻文章时。
-
领域特定应用程序: 对于需要 LLM 训练数据之外的专业知识的应用程序,例如内部公司文档。
不适合使用时
-
通用对话应用程序: 信息需要是通用的,不需要额外的数据。
-
有限资源场景: RAG 的检索组件涉及搜索大型知识库,这可能计算成本高且速度慢 — 尽管仍然比微调更快且更便宜。
使用 RAG 构建应用程序
一个很好的起点是使用LlamaIndex 库。它允许您快速将数据连接到 LLM。为此,您只需要几行代码:
from llama_index import VectorStoreIndex, SimpleDirectoryReader
# 1\. Load your documents:
documents = SimpleDirectoryReader("YOUR_DATA").load_data()
# 2\. Convert them to vectors:
index = VectorStoreIndex.from_documents(documents)
# 3\. Ask the question:
query_engine = index.as_query_engine()
response = query_engine.query("When's my boss's birthday?")
print(response)
在实际应用中,事情显然更加复杂。与任何开发一样,您会遇到许多细微差别。例如,检索的文档可能并不总是与问题相关,或者可能存在速度问题。然而,即使在这个阶段,您也可以显著提高搜索系统的质量。
读什么 & 有用的链接
-
为生产环境构建基于 RAG 的 LLM 应用程序 — 一篇关于 RAG 主要组成部分的优秀详细文章。
-
为何在生产环境中您的 RAG 不可靠 — 由Ahmed Besbes撰写的一篇很棒的文章,清楚地解释了在使用 RAG 时可能出现的困难。
-
使用 LlamaIndex 导航知识图谱的 7 种查询策略 — 来自Wenqi Glantz的信息丰富的文章,详细而微妙地探讨了使用 LlamaIndex 构建 RAG 管道。
-
OpenAI 检索工具 — 如果你想要最小的工作量,可以使用 OpenAI 的 RAG。
5. 微调您的 LLM
微调是继续在特定数据集上训练预训练大语言模型(LLM)的过程。你可能会问,如果我们已经可以通过 RAG 添加数据,为什么还需要进一步训练模型。简单的回答是,只有微调才能使你的模型适应特定领域或定义其风格。例如,我通过在个人通信上进行微调创建了自己的副本:
微调模型在作者通信上的演示
好吧,如果我已经说服了你它的重要性,让我们来看看它是如何工作的(剧透 — 其实并不难):
经典的微调领域特定数据的方法(所有图标来自 flaticon)
-
选择一个经过训练的大语言模型,有时称为基础 LLM。你可以从HuggingFace下载它们。
-
准备你的训练数据。你只需要编译指令和响应。这里有一个例子的这样的数据集。你也可以使用 GPT-4 生成合成数据。
-
在新数据上微调模型。
何时使用
-
小众应用: 当应用涉及专业或非常规话题时。例如,需要理解和处理法律术语的法律文件应用。
-
自定义语言风格: 对于需要特定语调或风格的应用。例如,创建一个AI 角色,无论是名人还是书中的角色。
何时不使用
-
广泛应用: 当应用的范围较广,不需要专业知识时。
-
有限的数据: 微调需要大量相关的数据。然而,你始终可以使用另一个 LLM 生成数据。例如,Alpaca 数据集包含 52k 个 LLM 生成的指令-响应对,用于创建今年早些时候的第一个微调Llama v1模型。
微调你的 LLM
你可以找到大量关于模型微调的文章。仅在 Medium 上,就有成千上万篇。因此,我不想过于深入这个话题,会向你展示一个高级库,Lit-GPT,它隐藏了所有的魔法。是的,它不允许对训练过程进行太多自定义,但你可以快速进行实验并获得初步结果。你只需要几行代码:
# 1\. Download the model:
python scripts/download.py --repo_id meta-llama/Llama-2-7b
# 2\. Convert the checkpoint to the lit-gpt format:
python scripts/convert_hf_checkpoint.py --checkpoint_dir checkpoints/llama
# 3\. Generate an instruction tuning dataset:
python scripts/prepare_alpaca.py # it should be your dataset
# 4\. Run the finetuning script
python finetune/lora.py \
--checkpoint_dir checkpoints/llama/
--data_dir your_data_folder/
--out_dir my_finetuned_model/
就这样!你的训练过程将开始:
请注意,这个过程可能需要很长时间。在单个 A100 GPU 上微调 Falcon-7B 大约需要10 小时和30 GB内存。
当然,我稍微简化了一下,我们只触及了表面。实际上,微调过程要复杂得多,要获得更好的结果,你需要了解各种适配器、它们的参数以及更多内容。不过,即使经过如此简单的一轮,你也将拥有一个按照你的指示运行的新模型。
阅读内容 & 有用链接
-
用微调的 LLM 创建一个你自己的克隆 — 我的文章,讲述了数据集的收集、使用的参数以及提供了关于微调的有用建议。
-
理解大型语言模型的参数高效微调 — 如果你想深入了解微调概念和流行的参数高效替代方案,这是一份极好的教程。
-
使用 LoRA 和 QLoRA 微调 LLM:来自数百次实验的见解 — 我最喜欢的文章之一,用于了解 LoRA 的能力。
-
OpenAI 微调 — 如果你想以最小的努力微调 GPT-3.5。
6. 部署你的 LLM 应用程序
有时候,我们只希望简单地按下“部署”按钮……
幸运的是,这相当可行。有大量专注于部署大型语言模型的框架。是什么让它们如此优秀?
-
许多预构建的包装器和集成。
-
丰富的模型选择。
-
众多的内部优化。
-
快速原型开发。
选择合适的框架
部署 LLM 应用程序的框架选择取决于多种因素,包括模型的大小、应用程序的可扩展性要求和部署环境。目前,框架的种类并不多,因此理解它们的差异应该不会太难。下面,我为你准备了一份速查表,帮助你快速入门:
此外,在我的文章“7 种用于服务 LLM 的框架”中,我提供了对现有解决方案的更详细概述。如果你打算部署你的模型,我建议查看一下。
LLM 推理框架的比较
部署示例代码
让我们从理论转向实践,尝试使用文本生成推理部署 LLaMA-2。正如你可能猜到的,你只需几行代码:
# 1\. Create a folder where your model will be stored:
mkdir data
# 2\. Run Docker container (launch RestAPI service):
docker run --gpus all --shm-size 1g -p 8080:80 \
-v $volume:/data \
ghcr.io/huggingface/text-generation-inference:1.1.0
--model-id meta-llama/Llama-2-7b
# 3\. And now you can make requests:
curl 127.0.0.1:8080/generate \
-X POST \
-d '{"inputs":"Tell me a joke!","parameters":{"max_new_tokens":20}}' \
-H 'Content-Type: application/json'
就这些!你已经设置了一个内置日志记录的 RestAPI 服务,带有用于监控的 Prometheus 端点、令牌流式传输,并且你的模型已完全优化。难道这不是很神奇吗?
API 文档
该读什么 & 有用的链接
-
7 种用于服务 LLM 的框架 —— 一份详尽的指南,介绍了 LLMs 推理和服务的详细比较。
-
推理端点 —— HuggingFace 的一个产品,可以让你通过几次点击部署任何 LLMs。当你需要快速原型时,这是一个不错的选择。
7. 什么在幕后仍然存在
尽管我们已经涵盖了开发基于 LLM 的应用所需的主要概念,但仍然有一些方面你可能会在未来遇到。因此,我想留下一些有用的链接:
优化
当你启动第一个模型时,你不可避免地会发现它没有你期望的那么快,而且消耗了大量资源。如果是这种情况,你需要了解如何优化它。
-
7 种加速托管 LLM 推理的方法 —— 加速 LLMs 推理的技术,以提高令牌生成速度和减少内存消耗。
-
在 PyTorch 中优化 LLM 的内存使用 —— 文章提供了一系列技术,可以在不牺牲建模性能和预测准确性的情况下,将 PyTorch 中的内存消耗减少约 20 倍。
评估
假设你有一个微调后的模型。但你如何确保其质量有所提高?我们应该使用什么指标?
-
关于评估大语言模型的一切 —— 一篇关于基准和指标的好概述文章。
-
evals —— 最受欢迎的评估 LLMs 和 LLM 系统的框架。
向量数据库
如果你使用 RAG,某个时候你会从将向量存储在内存中转到数据库中。为此,了解市场上的当前产品及其局限性非常重要。
-
你需要知道的关于向量数据库的所有信息 — Dominik Polzer 提供的逐步指南,帮助你发现并利用向量数据库的力量。
-
选择向量数据库:2023 年的比较和指南 — 对 Pinecone、Weviate、Milvus、Qdrant、Chroma、Elasticsearch 和 PGvector 数据库的比较。
LLM 代理
在我看来,这是 LLMs 中最有前景的发展。如果你想让多个模型协同工作,建议你探索以下链接。
-
基于 LLM 的自主代理调查 — 这可能是关于 LLM 代理的最全面的概述。
-
autogen — 一个框架,使得开发使用多个能够相互对话以解决任务的 LLM 应用程序成为可能。
-
OpenAgents — 一个开放平台,用于在实际环境中使用和托管语言代理。
来自人类反馈的强化学习(RLHF)
一旦你允许用户访问你的模型,你就开始承担责任。如果它回应粗鲁?或者泄露炸弹制作成分?为避免这种情况,请查看这些文章:
-
说明来自人类反馈的强化学习(RLHF) — 详细介绍 RLHF 技术的概述文章。
-
RL4LMs — 一个模块化的 RL 库,用于将语言模型微调到人类偏好。
-
TRL — 一套用于训练变换器语言模型的工具,从监督微调步骤(SFT)、奖励建模步骤(RM)到近端策略优化(PPO)步骤。
结论
尽管有些炒作让我们感到有些厌倦,LLMs 将长期存在,而理解其堆栈并编写简单应用程序的能力可以为你带来显著的提升。我希望我能让你在这个领域中稍作沉浸,并展示它并不复杂或令人恐惧。
感谢你的关注,请继续关注新文章!
免责声明:本文中的信息截至 2023 年 11 月,但请注意之后可能会有所变动。
除非另有说明,所有图片均由作者提供。
如果你有任何问题或建议,请随时通过LinkedIn联系我。
关于 Dask 数据框分区大小的几乎所有信息
以及如何在 XGBoost 模型中有效利用它
·
关注 发表于 Towards Data Science ·7 min read·2023 年 12 月 1 日
–
预览图(作者提供)
最近,我和我的同事们一直在处理一个使用 Xgboost 机器学习模型和 Dask 作为分布式数据处理和预测生成工具的大型高负载服务。在这里,我想分享我们如何最大化利用 Dask 进行数据准备和机器学习模型拟合的发现。
什么是 Dask?
Dask是一个用于大规模数据分布式处理的库。其基本概念是将大型数组划分为小部分(分区)。
这就是 Dask Dataframes 如何存储和处理:表可以被拆分成小的数据帧(可以将其视作 pandas DataFrames),这样无需将整个表存储在 RAM 中。整个源表可能太大而无法加载到内存中,但单个分区可以。此外,这种数据存储允许有效利用多个处理器核心来并行计算。
与此同时,这些分区(块)的大小由开发者确定。因此,相同的数据帧可以使用例如‘Split 1’或‘Split 2’(见图 1)分成几个分区。
图 1. 将 Dask 数据帧拆分为分区(图片由作者提供)
选择最佳的分区大小至关重要,因为如果分区大小不佳,数据处理可能会变慢。最佳分区大小取决于整体数据集的大小以及服务器(或笔记本电脑)的资源——CPU 数量和可用 RAM。
免责声明: 为了方便起见,接下来我们将通过行数来衡量数据集的大小。所有表将由 4 列组成(3 个特征+1 个目标)。在系统中实现算法时,我们构建的所有依赖项不是基于表中的行数,而是基于元素的总数(行数 x 列数)。
问题
Dask 可以用于计算简单统计数据和聚合,但 Dask 也可以用于训练大型机器学习模型(使用大量数据)。例如,XGBoost。由于我们正在开发的服务可能需要我们在仅有 8-16 GB RAM(在小型虚拟机情况下)下对 2-10 百万条记录进行模型训练,因此我们决定进行实验。
即使是在计算简单统计数据的情况下,分区的大小也非常重要,因为它在两种情况下可能显著减慢计算算法:
-
分区过大,导致在 RAM 中处理这些分区需要耗费过多的时间和资源
-
分区过小,以至于 Dask 需要过于频繁地将这些表加载到 RAM 中——更多的时间花在同步和上传/下载上,而不是在计算本身上
因此,使用相同的计算资源选择非最佳的分区大小会显著降低程序的性能(见图 2)。图 2 显示了在不同分区大小的 Dask 数据帧上拟合 XGBoost 模型的时间。显示的是 5 次运行的平均执行时间。
图 2. 分区大小对 XGBoost 模型拟合速度的影响。这些实验的原始数据帧包含 500,000 行和 4 列(图片由作者提供)
本文讨论了Dask 数据框分区的最佳大小算法。本文中展示的所有表格都用于拟合 Dask Xgboost 机器学习模型。我们还将分享一些您可能会觉得有用的技巧。
文档提示
在官方的 Dask 文档中,有关于如何正确处理 Dask 对象(数据框、数组等)的网页,例如最佳 Dask 数据框实践。
在此页面上,您可以看到以下建议:
目标应该是每个分区的数据量大约为 100MB。
然而,这些建议比较粗略,并未考虑服务器的计算规格、源数据集的大小以及问题解决的具体情况。
实验设置
如上所述,假定最佳分区大小取决于以下三个条件:
-
全数据集的大小;
-
XGBoost 和 Dask 可以使用的 CPU 资源(进程数);
-
可用的随机存取内存(RAM)。
因此,在实验过程中,计算资源的数量以及源数据集的大小均有所变化。考虑的情况:
-
分区大小,千行:[5, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000](13 种情况)
-
全数据集的大小,千行:[100, 200, 300, 400, 500, 600, 1000, 2000, 3000, 4000](10 种情况)
-
CPU 资源(工作者):[2, 3, 4](3 种情况)
-
每个工作者可用的随机存取内存(RAM):[1GB, 2GB, 4GB](3 种情况)
注:Dask 中的工作者是计算机(服务器)上的一个进程,它使用分配给它的计算资源,并在与其他工作者隔离并行运行。
因此,分析了 1170(13 x 10 x 3 x 3)种情况。为了获得更稳健的执行时间估计,每种情况被运行了 5 次。然后对指标(执行时间)进行了平均。
在调查过程中,我们希望了解数据集大小的限制,以便虚拟机无法处理负载,从而更成功地扩展服务。
结果
首先,考虑实验的一般可视化结果。我们进行了不同 CPU 核心数量和 RAM 容量的运行,同时改变了原始数据集的大小和分区的大小。完成实验后,我们准备了一张表格,仅显示最佳解决方案(分区大小)。最佳分区大小是指在给定条件(RAM、CPU 和源数据集大小)下,执行时间最短的大小。收集到的指标的相关性矩阵见图 3。
图 3. 实验结果的相关性矩阵(作者提供的图像)
从图表中可以看出,对执行时间影响最大的是源数据集的大小。工作线程的数量和内存量对拟合时间也有显著影响。块大小的影响相对较弱。然而,这可能是因为执行时间与分区大小之间的依赖关系是非线性的,这一点从图 2 的曲线中得到了确认。此外,图 4 证实了测量结果是正确的,因为结果与我们的预期一致。
让我们来看一下带有 3d 图表的动画(动画 1)。
动画 1. 不同测试案例的图表,其中每一帧表示源数据集的固定大小。每种情况的最佳表面显示在图中(由作者提供)
在动画中,最佳(对于每种进程数量和每个工作线程的内存组合)情况用红色圈出。即显示了在给定数据集大小、核心数量、内存和分区大小下算法执行时间最小的条件。图表还显示了灰色的分段常数最佳表面(注意:表面对于所有情况都是全局的)。
从动画中可以看出,在某些帧中没有实验数据(没有点)(图 4)。这意味着所提议的计算资源不足以运行模型。
图 4. 对一个包含 400 万行的数据集的建模结果。对于小于 4 的内存没有结果(图片由作者提供)
从图中可以观察到,对于这种数据集大小,如果核心数量较少,则应形成较大的分区。请注意,这种依赖关系并不适用于所有情况。
基于计算资源不足的启动结果,准备了以下可视化(图 5)。
图 5. 数据量的限制,超过此限制则无法开始模型拟合。对象数量的计算方法是将表格中的行数乘以列数(图片由作者提供)
此外,根据失败运行收集的统计数据得出结论(提示):如果内存有限,使用较小的分区大小更可靠。
讨论
根据这项研究,形成了针对 Dask XGBoost 模型系统更有效配置的若干提示。请注意,本研究的目的是为了在相对较小的服务器上更高效地运行 Dask(这些服务器没有数百吉字节的内存和几十个 CPU)。
实验揭示了最佳超平面。它们通过高斯过程进行建模。基于这一算法,最佳分区大小会被自动选择(动画 2)。
动画 2. 不同条件下的最佳面(CPU、RAM 和源数据集大小)。每一帧显示特定源数据集大小的条件(作者提供)
从动画中可以看出,当源数据集中的行数增加时,最佳分区大小通常会减少。
结论(和建议)
我希望你对了解哪个大小的分区被证明是训练 XGBoost 模型的最佳大小感兴趣。
我意识到这篇文章变得非常“技术化”。因此,对于那些读到最后的人,我将提供一些建议:
-
如果你正在测量执行时间,始终运行多次计算并取结果的平均值,因为运行时间是随机的;
-
如果你对选择什么大小的分区感到困惑,最好犯一个较小的错误(否则算法不仅会运行很长时间,还可能因错误而崩溃);
-
在 Dask 中本地初始化集群时,使用 cluster = LocalCluster() 和 Client(cluster) 命令。我们强烈建议仅在代码中使用单例模式初始化这样的集群。你可以在 这里 查看如何在 Python 中实现它。否则,每次启动时你都会初始化一个新集群;
-
平均来说,当源数据集中的行数增加时,最佳分区大小会减少
关于 Dask 和机器学习的故事由 Mikhail Sarafanov 和 Wiredhut 团队 提供
数据驱动故事讲述的替代可视化
原文:
towardsdatascience.com/alternative-visualizations-for-data-driven-storytelling-65c873709fcc
为什么选择它们,以及如何创建它们
·发表于 Towards Data Science ·阅读时间 16 分钟·2023 年 11 月 2 日
–
来源:作者在 ChatGPT 和 DALL-e 3 中生成的图像。
你知道所有 F1 赛车必须将一块大木板作为其组成部分之一吗?
一块木板?
是的,确实如此!
F1 赛车使用一块木板,即**“滑板块”,**安置在车底。这确保了赛车在比赛过程中高度保持在法律规定的范围内。此外,木板作为一个重要的安全功能,通过与赛道摩擦来保护赛车的重要部件免受接触路缘时可能造成的损坏。值得注意的是,它对性能的影响很小。滑板块由木材和纤维的复合材料制成,设计为以稳定的速度磨损。¹
我为什么提到这一点? 这个轶事突显了即使在技术先进的领域,如 F1,仍然可以有效利用一个(理论上)简单的组件。这一平行也延伸到数据可视化领域。正如我多次论述的,大多数数据驱动的见解可以通过三种基本图表类型:柱状图、折线图和饼图,来熟练地传达。
但是,如果这三种类型无法捕捉我们试图解释的现象的本质呢? 或者如果我们使用多个简单图表,而不是一个稍微复杂的图表会怎样?或者,回到我们的 F1 示例,我们决定用碳纤维替代木材,并绘制一些更花哨的东西?在这篇文章中,我将探讨在这种情况下可供选择的替代方案。
那么,你可以从这篇文章中期待什么?
目标是提供替代数据可视化形式的概述。所以,我将讨论超越传统的柱状图、折线图和饼图这三种选择的图表选项,我认为这三种图表在大多数情况下是默认选择。
尽管如此,我完全认识到还有许多其他可视化选项。我通常会在传统三种方式不适用时考虑使用它们。在这里,我将向你介绍一些我认为特别有价值的可视化形式。我还会指导你如何使用各种实用且免费的工具创建它们。值得注意的是,在这篇文章中,我假设你会使用存储在电脑上的本地数据来制作可视化,除非有其他特定优势的替代方法,我会重点说明。公平地说,我将写这篇文章视为学习这些工具的机会,我必须说,学习这些东西非常有趣(除了实际的价值)。希望对你也是如此!
可视化的十诫
在我们探索广阔而多样的数据可视化领域之前,我想向你介绍一个我开发的新颖想法:“可视化十诫。” 这是一个简单的指南汇编,可以帮助你制作基于数据的视觉效果,无论你选择基本的可视化格式如柱状图,还是决定探索我们随后讨论的更复杂的替代方案。
-
从目的开始。 总是以明确的目的开始进行可视化工作。
-
使眼动追踪更容易。 将接受者的注意力引导到源自目的的关键信息上。
-
减少认知负担。 移除所有不符合目的的或使追踪关键信息变得困难的多余元素。
-
选择精确的可视化属性,例如形状、方向或大小,而不是颜色或体积。
-
简化。 移除图表中所有技术元素,除非它们有助于实现这十诫中的第 1 到第 4 点。
-
最大化数据墨水比。
-
战略性使用颜色。 减少颜色数量,并清楚区分图表中的重要信息和不重要信息。
-
保持设计的完整性和一致性。 在你的展示或报告中一致地应用用于可视化的风格。
-
确保包容性。 例如,为视力障碍者提供必要的照顾。
-
不要操控可视化。 避免使用操控性的技巧。
如果你有兴趣了解更多关于这些规则的内容,请欢迎访问我的 博客。我深入讨论了这些要点,并分享了一些我所说的具体要点的视觉示例。文章有英文和波兰文两种版本。
替代的可视化形式
好的,让我们回到主要讨论的话题。我挑选了一些与传统的柱状图、折线图和饼图不同的可视化类型。我将详细介绍这些图表。每一种图表的选择都是因为它们满足了特定的需求,我将会列出这些需求。当基本图表类型无法满足需求时,考虑将这些可视化图表整合到你的工作中。如果你只是想打破三种基础图表类型的单调,也可以选择这些图表。这可能有助于新鲜而吸引人地吸引你的观众注意。
注意!
我遇到过无数数据可视化图表,这些图表被其创作者称为替代性、创意性或创新性的。公平地说,他们完全有权做出这样的声明。然而,从我的角度来看,这些图表中的大多数缺乏实际应用。它们通常过于复杂,需要大量的努力才能提取有意义的见解,或者过于装饰性。因此,我在选择替代性图表时可能会比较保守。放心,我选择的每一个可视化图表都经过了严格的“实地测试”。 因此,我可以自信地证明它们的实用性和适用性,特别是在企业环境中。
桑基图
桑基图。 来源:由作者在 Google Charts 中生成的图表。
桑基图³是一种特殊类型的流动图,其特点是连接线的厚度与它们代表的流量成比例。这些图通常用于科学和商业应用。
桑基图由两个主要组件组成:
-
节点代表流动过程中的各种状态或阶段。
-
连接线,表示从一个节点到另一个节点的流动。
每条连接线的宽度与相应节点之间的总流量成正比。
桑基图可以用于多种目的,包括:
-
跟踪客户迁移,例如从一个 CRM 系统转移到另一个。
-
分析现金流在一个企业或更广泛的经济范围内。
-
检查信息在计算机网络中的流动。
-
调查生产过程中的材料使用情况。
我使用了Google Charts,这是由科技巨头 Google / Alphabet 开发的全面图表服务,来绘制上述图表。Google Charts 是一个 JavaScript 库,使用户能够根据其数据输入创建图表。一旦输入了必要的信息并指定了格式化偏好,Google Charts 将生成图表并提供嵌入到网站所需的 HTML/JavaScript 代码。
以下是我为上面展示的图表生成的代码。这是从 Google Charts 网站复制的几个代码元素加上我自己的数据。这段代码可以保存为纯 HTML,并像我上面那样嵌入到网站中。你也可以在 JSFiddle 等工具中执行它。
<head>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
google.charts.load('current', {'packages': ['sankey']});
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
var data = new google.visualization.DataTable();
data.addColumn('string', 'From');
data.addColumn('string', 'To');
data.addColumn('number', 'Weight');
data.addRows([
['Cohort A','Cohort ABC',5],
['Cohort B','Cohort ABC',2],
['Cohort C','Cohort ABC',7],
['Cohort D','Cohort DEF',1],
['Cohort E','Cohort DEF',3],
['Cohort F','Cohort DEF',8],
['Cohort G','Cohort GHI',3],
['Cohort H','Cohort GHI',5],
['Cohort I','Cohort GHI',8],
['Cohort ABC','Cohort ABCDE',9],
['Cohort ABC','Cohort FGHI',5],
['Cohort DEF','Cohort ABCDE',10],
['Cohort DEF','Cohort FGHI',2],
['Cohort GHI','Cohort FGHI',16],
]);
var colors = ['#a6cee3', '#b2df8a', '#fb9a99', '#fdbf6f',
'#cab2d6', '#ffff99', '#1f78b4', '#33a02c'];
// Sets chart options.
var options = {
width: 600, height: 290,
sankey: {
node: {
colors: colors
},
link: {
colorMode: 'gradient',
colors: colors
}
}
};
// Instantiates and draws our chart, passing in some options.
var chart = new google.visualization.Sankey(document.getElementById('sankey_basic'));
chart.draw(data, options);
}
</script>
</head>
<body>
<div id="sankey_basic" style="width: 600px; height: 300px;"></div>
</body>
</html>
漏斗图
在 Google Colab 中生成的漏斗图。 来源:作者提供的图片。
漏斗图 在财务专家如预算控制员或财务分析师中“非常受欢迎”。就我个人而言,我不喜欢它,因为我看到了太多次。不过,我使用漏斗图是因为我认为它们具有很大的客观价值。
漏斗图是一种图形表示,特别适用于预算演示或财务报告。这种图表在说明两个点或场景(例如执行和预算)之间的变化时非常有用,突出显示其组成部分。
漏斗图旨在展示一个初始值如何通过一系列正负变化演变,最终形成一个结果值。它由可以垂直或水平排列的柱子组成。这些柱子表示导致初始值变化的各个因素。通常,正变化用绿色柱子表示,而负变化用红色柱子表示。图表的第一个柱子显示起始值,最后一个柱子表示结束值,中间的柱子则标出使从初始值到绝对值转变的个别变化。
以下是用于漏斗图的 Python 代码,利用了 Plotly 库。我使用了 ChatGPT (GPT 3.5) 来编写这段代码。
import plotly.graph_objects as go
# Input data - sample values for different steps
steps = ['Initial Value', 'Step 1', 'Step 2', 'Step 3', 'Step 4', 'Step 5', 'Final Value']
values = [100, 150, -30, 60, -20, 90, 250] # Sample changes in values at each step
# Calculate the final value based on the input data
final_value = sum(values)
# Create the waterfall chart
fig = go.Figure(go.Waterfall(
name="",
orientation="v",
measure=["absolute", "relative", "relative", "relative", "relative", "relative", "total"],
x=steps,
textposition="outside",
texttemplate="%{y}",
y=values,
connector={"mode": "spanning", "line": {"width": 0}},
))
# Add initial and final values to the chart
fig.add_trace(go.Scatter(x=['Initial Value', 'Final Value'], y=[100, final_value], mode='markers+text',
text=[str(100), str(final_value)], textposition='bottom center'))
# Configure the chart
fig.update_layout(
title="Waterfall Chart",
xaxis_title="Steps",
yaxis_title="Value",
)
# Save the chart to an HTML file
fig.write_html("waterfall_chart.html")
# Display the chart
fig.show()
代码在 Google Collab 中执行。你可以在这段话的开头查看最终产品。
如果你不想使用 Google Colab 并希望将数据保留在本地,我推荐使用 Anaconda Distribution。具体来说,我将以下代码粘贴到 Spyder IDE 中,这是在 Anaconda 中安装的。可能需要安装 Plotly 库,你可以在 Anaconda Powershell Prompt 中进行安装(使用以下命令行:pip install plotly 或 pip install -U plotly 以获取最新版本)。
哑铃图
哑铃图的示例。 来源:作者截屏。
哑铃图⁴ 是一种旨在说明两个不同时间点或两个类别之间值变化的图表。这种可视化技术在比较跨不同类别或实体的两个数据点之间的差异时非常有价值。
哑铃图的结构包括每个类别(或单位)的两个不同点,通过一条线连接。这些点显示了两个不同时间点或两个不同类别的值。连接线表示这两个点之间的值变化,便于直接比较哪些类别随着时间发生了最大变化,或者哪些单位在两个类别之间的变化最大。
以下是几个示例,展示了哑铃图的潜在应用:
-
产品价格变化: 可以用来比较两个不同年份的产品价格,哑铃图的一端代表第一个年份的价格,另一端代表第二个年份的价格。
-
医学测试结果比较: 哑铃图可以用于比较患者治疗前后的诊断测试结果。
-
生产中的变化: 对于那些希望评估两个不同季度生产绩效的公司,哑铃图可以简洁地比较各个季度生产的单位数。
-
员工评级变化: 人力资源部门可以利用哑铃图来比较两个评估周期之间员工的评级。
-
销售比较: 市场部门可以利用哑铃图来评估广告活动启动前后的产品销售表现。
我使用Noteable和Data Prism设计了上述的哑铃图。Data Prism 是 Noteable 的内置功能。⁵
Noteable 是一个分析平台和协作数据工作区。它既可以作为网站访问,也可以作为 ChatGPT 的集成功能。
使用 Noteable 创建图表的一个潜在缺点是,根据我的了解,图表不能在本地复制。然而,这个平台仍然值得探索,主要是因为它包含了创新的 Data Prism 功能。该工具使用户可以通过简单的点击生成大量的数据可视化。通过不同可视化的实验过程中的简便性和速度,提高了发现数据中先前未注意到的趋势、依赖关系或异常值的可能性。
首先,我们需要在 Noteable 中创建一个空间和笔记本。
在 Noteable 中创建空间和笔记本。 来源:作者截图。
然后,在新创建的笔记本中,我们可以输入数据。这里我创建了一个数据框,但你也可以上传一个 *.csv(逗号分隔)文件(首先需要将文件上传到项目中,然后添加到笔记本)。
输入到 Noteable 的数据。 来源:作者截图。
这是上述代码的展示。
import pandas as pd
data = pd.DataFrame({
'Category': ['A', 'B', 'C', 'D', 'E'],
'Value1': [10,30,45,50,66],
'Value2': [13,42,50,60,70]
})
data
接下来,我们点击“配置”,然后选择“Data Prism Suggestions”。
启动 Data Prism 建议。 来源:作者截图。
然后我们会得到多个选项,可以选择并进一步处理。
Data Prism 推荐的图表。 来源:作者图片。
最后,我们可以将图表导出为*.png(不幸的是,除非是我电脑上的问题,否则效果不好)。我们也可以将其发布然后嵌入到我们的网站上。
雷达图
在 Chart.js 中制作的雷达图。 来源:作者截图。
雷达图是一种用于在二维图表格式中显示多变量数据的可视化工具,其中三个或更多定量变量在从同一中心点发出的坐标轴上表示。这些坐标轴在圆周上均匀分布,每个变量的值沿着这些坐标轴绘制。然后,绘制的点通过线连接形成一个形状。由线条形成的形状和点相对于坐标轴的位置揭示了数据的见解。
以下是雷达图的一些实际应用:
-
运动表现分析: 雷达图在运动分析中经常被用来比较运动员在速度、敏捷性和力量等各种指标上的表现。
-
产品比较: 企业通常使用雷达图来比较各种产品的特征或属性,包括竞争对手的产品。
-
技能评估: 人力资源部门可以利用雷达图评估个人在多个领域的技能或能力。
-
客户分析: 分析师可能会使用雷达图来可视化客户满意度评分,包括产品质量、客户服务和性价比等维度。
上面是我使用Chart.js库创建的图表示例。⁶
Chart.js 是一个提供全面的常用图表类型的库,配有各种插件和自定义选项以满足你的需求。除了标准图表外,你还可以利用由社区维护的其他图表类型。Chart.js 的一个显著特点是能够将不同的图表类型结合成一个组合图表,从而提供更多灵活性以展示你的数据。此外,Chart.js 支持自定义插件,使你能够集成各种功能,如注释、缩放和拖放,以增强图表的互动性和可用性。
这是上图的脚本代码。它从 Chart.js 网站复制过来,并用我的样本数据进行了调整。它也准备好可以嵌入到网站中。
<html>
<head>
<title>Radar Chart Example</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
</head>
<body>
<canvas id="radarChart"></canvas>
<script>
var canvas = document.getElementById("radarChart");
var chart = new Chart(canvas, {
type: "radar",
data: {
labels: [
'A',
'B',
'C',
'D',
'E',
'F',
'G'
],
datasets: [{
label: '2022',
data: [65, 59, 90, 81, 56, 55, 40],
fill: true,
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgb(255, 99, 132)',
pointBackgroundColor: 'rgb(255, 99, 132)',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: 'rgb(255, 99, 132)'
}, {
label: '2023',
data: [48, 48, 80, 90, 66, 47, 30],
fill: true,
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgb(54, 162, 235)',
pointBackgroundColor: 'rgb(54, 162, 235)',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: 'rgb(54, 162, 235)'
}]
}
});
</script>
</body>
</html>
子弹图
子弹图。 作者截图。
子弹图 是一种数据可视化,用于显示相对于预定义目标或基准的绩效数据。它由以下元素组成:
-
条形图 代表一个主要度量(例如当前业绩或年初至今的收入)。
-
标记线 代表一个比较度量(例如目标或前一年业绩)。
-
它还可以显示 范围,使用颜色的不同深浅或其他颜色来表示定性绩效,例如差、满意和良好。
主要度量与比较度量和定性范围进行对比,以便迅速评估业绩是否达标或符合基准。子弹图通常用于业务绩效管理的仪表板和报告中。
以下是我使用Datawrapper制作的一个子弹图示例。Datawrapper 是一个在线工具,能够创建高效、美观且视觉上引人注目的可视化效果。无需掌握 HTML 或 Python 等特定 IT 知识——所有操作都可以通过图形界面完成。不幸的是,这是一个完全在线的工具,没有本地安装选项。因此,为了获得可视化效果,你需要将数据直接上传到工具中。Datawrapper 允许将可视化保存为 *.png 文件或嵌入到网站上。
来源:作者打印屏幕截图。
仪表/速度计
来源:作者打印屏幕截图。
仪表可以与子弹图发挥类似的功能。仪表或速度计图是一种数据可视化,类似于汽车的速度计,表示一个值在一系列值中的位置。它通常用于展示关键绩效指标(KPI)、目标达成情况或其他业务数据。
以下是仪表图的主要组成部分:
1. 指针或指示器: 代表数据值。
2. 刻度盘或刻度: 一个圆形或半圆形刻度,显示可能值的范围。
3. 弧形或带状: 不同颜色的区域通常代表性能范围。
通过观察指针在彩色弧形或带中的位置,观众可以迅速了解值在范围中的上下文,以及它如何与预定义的性能水平相关联。这种视觉表现使得轻松判断性能是否正常、低于预期或超出目标。⁷
你可以使用 Google Charts 轻松准备如上所示的仪表。然而,下面是另一个示例,这次是使用 ChatGPT 和 Noteable 创建的。实际上,这种工具组合不是免费的,你仍然可以在 ChatGPT 免费版中生成这种可视化的代码,然后将其粘贴到 Noteable 笔记本中。
另一种类型的仪表。 来源:作者在 ChatGPT 和 Noteable 中生成的图像。
迷你图或火花图
火花图是紧凑的图表,旨在同时显示多个数据系列的趋势或变化,通常在电子表格单元格或小报告部分内。与标准图表不同,火花图通常缺乏坐标轴标签和图例。它们的主要目的是提供清晰而简明的数据趋势视觉表示,而不占用太多空间。火花图可以有效地展示股价趋势、天气模式、销售数据等。我还将它们用作导航工具;用户可以点击火花图以访问完整大小的图表,如果他们发现了有趣的内容。⁸
以下是一些场景,其中火花图非常有用:
-
销售报告: 展示每种产品在目录或电子表格中的月度销售趋势。
-
股票市场分析: 在财务报告中显示多家公司历史股价趋势。
-
网站分析: 显示网站流量或用户参与度指标的趋势。
-
库存管理: 描绘各种产品库存水平的波动。
以上是我创建的火花图示例。对于这个特定练习,我再次使用了 ChatGPT 和 Noteable 插件。这一次,我在一个 Android 设备上的网站上完成了这个工作。我为图表生成了随机样本数据和图表代码(代码见下文)。
import numpy as np
np.random.seed(0)
sales_a = [200 + np.random.randint(0, 21) for _ in range(12)]
sales_b = [300 - np.random.randint(0, 21) for _ in range(12)]
sales_c = [250 + np.random.randint(-20, 21) for _ in range(12)]
fig, axs = plt.subplots(4, 1, figsize=(4, 4))
axs[0].plot(df['Sales'], color='b', linewidth=2)
axs[0].axis('off')
axs[1].plot(sales_a, color='g', linewidth=2)
axs[1].axis('off')
axs[2].plot(sales_b, color='r', linewidth=2)
axs[2].axis('off')
axs[3].plot(sales_c, color='m', linewidth=2)
axs[3].axis('off')
plt.show()
摘要
本文旨在拓展数据可视化的世界,超越条形图、折线图和饼图。还有许多其他更高级的选项值得考虑,以产生有洞察力和吸引力的可视化。附带的还有对各种免费工具的审查和“实地测试”,这些工具可以用来制作这样的可视化。
我特别讨论了使用案例,展示了优缺点,并演示了如桑基图、瀑布图、雷达图、哑铃图、仪表盘、火花图和子弹图等复杂图表的示例。我使用了多种工具来制作这些可视化,包括 Google Charts、Noteable、Python 和 Plotly。
我的主要目标是帮助你发掘这些可视化的潜力,使你能够用数据编织出更具吸引力的叙述。无论我是否成功,我鼓励你尝试这些替代图表类型,以了解它们的特点,测试各种应用,并进一步增强你的数据驱动叙事工具库。
你喜欢这篇文章吗?考虑订阅以获得新故事的通知,关注我,或简单地留下一个 👏。
参考资料
-
Wikipedia,滑块
-
Michal Szudejko,可视化十诫
-
Google Charts,桑基图
-
Tessica Dall, 需要在仪表板中强调变化?忘掉条形图,改用哑铃图!
-
Elijah Meeks, 介绍 Data Prism 自动图表生成器
-
Chart.js, 雷达图
-
Bernardita Calzon, 发现仪表盘图表的力量:定义、最佳实践和示例
-
维基百科, 火花图
p 值标准的替代方法(带 R 代码)
更好的统计决策方法
·发表于 Towards Data Science ·阅读时长 8 分钟·2023 年 3 月 12 日
–
图片由 Rommel Davila 提供,来源于 Unsplash
在确定统计显著性时,p 值标准几乎被普遍使用。该标准是在 p 值低于显著性水平(α)时,拒绝零假设(H0),支持替代假设(H1)。这一决策阈值的传统值包括 0.05、0.10 和 0.01。
根据定义,p 值衡量样本信息与 H0 的一致性:即 P(D|H0),在 H0 下数据(D)的概率或可能性。然而,正如美国统计协会(Wasserstein 和 Lazar,2016)的声明所明确指出的,p 值标准作为决策规则存在许多严重缺陷。主要缺陷包括
-
p 值是样本量的递减函数;
-
该标准完全忽略了 P(D|H1),即数据与 H1 的一致性;并且
-
α 的传统值(如 0.05)是任意的,几乎没有科学依据。
其中一个后果是,当 p 值标准在实际无关的范围内拒绝 H0 时,这种情况尤其在样本量较大时更为明显。这种情况发生的原因是,虽然 p 值是样本量的递减函数,但其阈值(α)是固定的,不随样本量的增加而减少。在这一点上,Wasserstein 和 Lazar (2016) 强烈建议用其他替代方法补充甚至替代 p 值。
在这篇文章中,我介绍了一系列简单但更为合理的备选方法,以克服上述提到的缺陷。它们可以分为三类:
-
平衡 P(D|H0) 和 P(D|H1)(贝叶斯方法);
-
调整显著性水平(α);以及
-
调整 p 值。
这些备选方法计算简单,并且能够提供比仅基于 p 值标准的推断结果更为合理的结论,这将通过一个包含 R 代码的应用进行演示。
背景
考虑一个线性回归模型
Y = β0 + β1 X1 + … + βk Xk + u,
其中 Y 是因变量,X 是自变量,u 是一个随机误差项,服从均值为零且方差固定的正态分布。我们考虑检验
H0: β1 = … = βq = 0,
针对 H1,即 H0 不成立(q ≤ k)。一个简单的例子是 H0: β1 = 0; H1: β1 ≠ 0,其中 q =1。
借用贝叶斯统计推断,我们定义以下概率:
Prob(H0|D):H0 的后验概率,即在研究者观察到数据 D 后 H0 的概率或可能性;
Prob(H1|D) ≡ 1 — Prob(H0|D):H1 的后验概率;
Prob(D|H0):在 H0 下数据的(边际)似然;
Prob(D|H1):在 H1 下数据的(边际)似然;
P(H0): H0 的先验概率,表示研究者在观察到数据之前对 H0 的信念;
P(H1) = 1 - P(H0):H1 的先验概率。
这些概率通过贝叶斯规则有关联,如下所示
主要组件如下:
P10:H1 相对于 H0 的后验比率,即 H1 的后验概率与 H0 的后验概率之比;
B10 ≡ P(D|H1)/P(D|H0),称为贝叶斯因子,即 H1 下的(边际)似然与 H0 下的(边际)似然之比;
P(H1)/P(H0):先验比率。
请注意,后验比率是贝叶斯因子与先验比率的乘积,且如果 Prob(H0) = Prob(H1) = 0.5,那么 P10 = B10。
决策规则是,如果 P10 > 0,证据支持 H1 相对于 H0。这意味着在研究者观察到数据后,如果 P(H1|D) > P(H0|D), 即 H1 的后验概率高于 H0 的后验概率,那么她倾向于 H1。
对于 B10,Kass 和 Raftery (1995) 提出了如下决策规则:
作者创建的图像
例如,如果 B10 = 3,则 P(D|H1) = 3 × P(D|H0),这意味着数据与 H1 的兼容性是与 H0 兼容性的三倍。请注意,贝叶斯因子有时表示为 2log(B10),其中 log() 是自然对数,与似然比检验统计量的尺度相同。
备选方法的公式
贝叶斯因子
Wagenmakers (2007) 提供了贝叶斯因子的简单近似公式:
2log(B10) = BIC(H0) — BIC(H1),
其中 BIC(Hi) 表示在 Hi (i = 0, 1) 下的贝叶斯信息准则值。
后验概率
Zellner 和 Siow (1979) 提供了 P10 的公式:
图片由作者创建
其中 F 是用于 H0 的 F 检验统计量,Γ() 是伽马函数,v1 = n-k0-k1–1,n 是样本大小,k0 是 H0 下的受限参数数量;k1 是 H0 下的不受限参数数量(k = k0+k1)。
Startz (2014) 提供了一个公式用于计算P(H0|D),即 H0 的后验概率,用于检验 H0: βi = 0:
图片由作者创建
其中 t 是用于 H0: βi = 0 的 t 统计量,ϕ() 是标准正态密度函数,s 是用于估计 βi 的标准误差估计器。
对 p 值的调整
Good (1988) 提出了以下对 p 值的调整:
图片由作者创建
其中 p 是 H0: βi = 0 的 p 值。该规则通过考虑贝叶斯因子对尖锐零假设的收敛速度来获得。调整后的 p 值(p1)随样本大小 n 增加。
Harvey (2017) 提出了所谓的贝叶斯化 p 值
图片由作者创建
其中 PR ≡ P(H0)/P(H1),MBF = exp(-0.5t²) 是最小贝叶斯因子,而 t 是 t 统计量。
显著性水平调整
Perez 和 Perichhi (2014) 提出了一个显著性水平的自适应规则,该规则通过调和贝叶斯推断方法和似然比原则得出,如下所示:
图片由作者创建
其中 q 是 H0 下的参数数量,α 是初始显著性水平,例如 0.05,χ²(α,q) 是具有 q 自由度的卡方分布的 α 水平临界值。简而言之,该规则将显著性水平调整为样本大小 n 的递减函数。
应用 R 代码
在本节中,我们将上述替代指标应用于大样本回归,并检验这些推断结果与仅基于 p 值标准得到的结果有何不同。也提供了这些指标计算的 R 代码。
Kamstra et al. (2003) 研究了与季节性情感障碍相关的抑郁症对股票收益的影响。他们声称阳光时长可以系统地影响股票收益的变化。他们估计了以下形式的回归模型:
图片由作者创建
其中 R 是第 t 天的股票回报率;M 是一个星期一的虚拟变量;T 是税年最后一个交易日或前五个交易日的虚拟变量;A 是秋季天数的虚拟变量;C 是云覆盖;P 是降水量;G 是温度,而 S 测量阳光的长度。
他们争辩说,随着阳光时间的增加,投资者的心情更好,他们倾向于购买更多股票,这将提高股价和回报。基于此,他们的零假设和备择假设是
H0: γ3 = 0;H1: γ3 ≠ 0。
他们的回归结果使用美国股市数据进行重复,数据时间从 1965 年 1 月到 1996 年 4 月(7886 个观测值)。数据范围受到云覆盖数据的限制,只有 1965 年至 1996 年的数据可用。完整结果及更多细节请参见 Kim (2022)。
作者创建的图像
上表总结了在 H0 和 H1 下的回归结果。零假设 H0: γ3 = 0 在 5% 显著性水平下被拒绝,系数估计值为 0.033,t 统计量为 2.31,p 值为 0.027。因此,基于 p 值标准,阳光的长度对股票回报具有统计学意义:预计股票回报会因阳光长度增加 1 单位而提高 0.033%。
虽然这对股票市场效率的暗示形成了证据,但可以争辩说,这一效应是否足够大以至于在实际中重要仍然值得怀疑。
备择测量值和相应的决策如下所示:
作者创建的图像
请注意,P10 和 p2 是在假设 P(H0)=P(H1) 的情况下计算的,这意味着研究者在 H0 和 H1 之间是 a priori 公正的。从上表中的结果可以清楚地看出,所有对 p 值标准的备择方案都强烈支持 H0 而不是 H1,或者在 5% 显著性水平下无法拒绝 H0。Harvey (2017) 的贝叶斯化 p 值表明在 10% 显著性水平下拒绝 H0。
因此,我们可以得出结论,Kamstra 等人 (2003) 的结果仅基于 p 值标准,在备择决策规则下并不那么令人信服。鉴于效果量的可疑性和模型的拟合优度几乎可以忽略不计(R² = 0.056),基于这些备择方法的决策似乎更为合理。
以下 R 代码展示了这些备择方案的计算(完整代码和数据可向作者索取):
# Regression under H1
Reg1 = lm(ret.g ~ ret.g1+ret.g2+SAD+Mon+Tax+FALL+cloud+prep+temp,data=dat)
print(summary(Reg1))
# Regression under H0
Reg0 = lm(ret.g ~ ret.g1+ret.g2+Mon+FALL+Tax+cloud+prep+temp, data=dat)
print(summary(Reg0))
# 2log(B10): Wagenmakers (2007)
print(BIC(Reg0)-BIC(Reg1))
# PH0: Startz (2014)
T=length(ret.g); se=0.014; t=2.314
c=sqrt(2*3.14*T*se²);
Ph0=dnorm(t)/(dnorm(t) + se/c)
print(Ph0)
# p-valeu adjustment: Good (1988)
p=0.0207
P_adjusted = min(c(0.5,p*sqrt(T/100)))
print(P_adjusted)
# Bayesianized p-value: Harvey (2017)
t=2.314; p=0.0207
MBF=exp(-0.5*t²)
p.Bayes=MBF/(1+MBF)
print(p.Bayes)
# P10: Zellner and Siow (1979)
t=2.314
f=t²; k0=1; k1=8; v1 = T-k0-k1- 1
P1 =pi^(0.5)/gamma((k0+1)/2)
P2=(0.5*v1)^(0.5*k0)
P3=(1+(k0/v1)*f)^(0.5*(v1-1))
P10=(P1*P2/P3)^(-1)
print(P10)
# Adaptive Level of Significance: Perez and Perichhi (2014)
n=T;alpha=0.05
q = 1 # Number of Parameters under H0
adapt1 = ( qchisq(p=1-alpha,df=q) + q*log(n) )^(0.5*q-1)
adapt2 = 2^(0.5*q-1) * n^(0.5*q) * gamma(0.5*q)
adapt3 = exp(-0.5*qchisq(p=1-alpha,df=q))
alphas=adapt1*adapt3/adapt2
print(alphas)
结论
p 值标准存在许多缺陷。单纯依赖这一决策规则在科学研究中产生了严重问题,包括错误的典型事实积累、研究诚信和研究可信度:见美国统计学会的声明(Wasserstein and Lazar, 2016)。
本帖子介绍了几种 p 值标准的替代方法用于统计证据。通过考虑多种替代方案的信息,可以做出更平衡和知情的统计决策。盲目使用单一决策规则可能会导致误导性决策,这可能代价高昂且影响深远。这些替代方案计算简单,可以补充 p 值标准,以便做出更好、更明智的决策。
请关注我,以获取更多有趣的帖子!
线性回归的易懂推导
模型背后的数学,从加法假设到伪逆矩阵
·
关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 8 月 23 日
–
图片由 Saad Ahmad 提供,来源于 Unsplash
技术免责声明:可以在没有正态性假设的情况下推导出模型。我们选择这个路径是因为它足够简单易懂,并且通过假设模型输出的正态性,我们可以推理预测的不确定性。
本文面向那些已经了解线性回归是什么(并可能使用过一两次)并希望深入了解其背后数学原理的人。
需要一些基本概率(概率分布、联合概率、互斥事件)、线性代数和统计学的背景知识,以充分理解接下来的内容。废话不多说,我们开始吧:
机器学习世界充满了惊人的联系:指数族、正则化和先验信念、KNN 和 SVM、最大似然和信息理论——这一切都连接在一起!(我喜欢黑暗)。这次我们将讨论如何推导出指数族的另一个成员:线性回归模型,在这个过程中,我们将看到均方误差损失在理论上是有良好动机的。与任何回归模型一样,我们将能够用它来预测数值型连续目标。它是一个简单却强大的模型,恰好是统计推断和实验设计中的一个重要工具。然而,我们将仅关注其作为预测工具的使用。这里没有烦人的推断(更别说因果)问题。
好的,让我们开始。我们想根据其他东西来预测某物。我们将把预测的东西叫做 y,将其他东西叫做 x。作为一个具体的例子,我提供以下玩具情境:你是一名在银行工作的信用分析师,你希望自动找出银行客户的正确信用额度。你也恰好拥有一个关于过去客户的数据库,其中包含了为他们批准的信用额度(即预测的东西),以及一些他们的特征,如人口信息、过去的信用表现、收入等(即其他东西)。
因此,我们有一个很好的想法,写下一个模型,该模型根据你拥有的特征来解释信用额度,模型的主要假设是每个特征以加法方式对观察到的输出有所贡献。由于信用额度只是一个激励性(和人为设计的)例子,我们回到纯数学的球形牛世界中,我们的模型变成了如下形式:
我们仍然有预测的东西(y)和用于预测的其他东西(x)。我们承认某种噪声是不可避免的(无论是由于测量不完美还是我们的盲点),我们能做的最好是假设我们观察到的数据背后的模型是随机的。其结果是我们可能会看到相同输入的略微不同的输出,因此我们不再是精确的点估计,而是“困在”以输入(x)为条件的输出(y)的概率分布中:
y 中的每个数据点都被一个小的钟形曲线替代,其均值位于观察到的 y 值中,并且具有一些我们目前不关心的方差。然后,我们的小模型将取代分布均值的位置。
假设所有这些钟形曲线实际上是正态分布,并且它们的均值(y 中的数据点)彼此独立,则观察到数据集的(联合)概率为
对数和一些代数来拯救我们:
对数是很酷的,不是吗?对数将乘法转化为加法,除法转化为减法,幂运算转化为乘法。从代数和数值的角度来看都非常方便。去掉与此无关的常数项,我们得到以下最大似然问题:
好吧,这等于
我们即将最小化的表达式非常接近著名的 均方误差 损失。实际上,在优化目的上它们是等价的。
那么接下来呢?这个最小化问题可以通过求导精确解决。我们将利用损失是二次的这一事实,这意味着它是凸的,即有一个全局最小值;这允许我们对其进行求导,设置为零并求解 theta。这样我们将找到使损失的导数为零的参数 theta 的值。为什么?因为正是导数为零的点,损失达到其最小值。
为了让一切变得更简单一些,让我们用向量表示法来表达损失:
在这里,X 是一个 NxM 矩阵,表示我们的整个数据集,其中包含 N 个示例和 M 个特征,而 y 是一个包含每个训练示例期望响应的向量。对其进行求导并将其设置为零,我们得到
这就是我们将原始机器学习问题转化为的优化问题的解决方案。如果你继续将这些参数值代入你的模型,你将拥有一个经过训练的机器学习模型,准备使用一些保留数据集(或通过交叉验证)进行评估。
如果你觉得最终的表达式看起来非常像线性系统的解,
正是因为它确实如此。额外的内容来自于这样一个事实:为了使我们的问题等同于一个普通的线性系统,我们需要相等数量的特征和训练样本,以便我们可以反转 X。由于这种情况很少出现,我们只能希望得到一个“最佳拟合”解决方案——在某种意义上的最佳——借助 X 的 Moore-Penrose 伪逆,这是一种广义的逆矩阵。相关的wikipedia 条目非常有趣。
对正则化的小探讨
从现在开始,我假设你可能听说过“正则化”这个术语,也许你知道它的意思。无论如何,我会告诉你我对它的理解:正则化是你对模型所做的任何事情,以使其在样本外的表现更好,虽然这可能会牺牲样本内的表现。为什么这是一个问题?因为最终,样本外(即实际生活中的)表现才是重要的——没有人会关心你的训练误差有多低,或者你的 AUC 在训练集上有多么惊人,除非你的模型在实际应用中表现得相对较好。而且我们发现,如果我们不小心,会遇到所谓的过拟合问题,在这种情况下,你的模型会记住训练集直到噪声,可能对样本外预测无用。我们还知道模型复杂性与样本外表现之间存在权衡(Vapnik 的统计学习理论),正则化可以看作是试图控制模型复杂性,希望实现更好的泛化能力(这里的泛化是样本外表现的另一种说法)。
正则化可以采取多种形式,但本文仅讨论一种常用于线性回归模型的方法。记住我说过,通过正则化我们是为了控制复杂性,对吗?好吧,不需要的复杂性主要通过两种方式潜入:1. 输入特征过多和 2. 对应的 theta 值过大。事实证明,所谓的 L2 正则化可以帮助解决这两个问题。记住我们的损失吗?
我们之前只关心最小化预测值和观察值 y 之间的平方差,而不考虑其他任何因素。这种狭隘的关注可能会成为问题,因为原则上你可以利用系统,不断添加特征,直到误差微乎其微,即使这些额外的特征与我们尝试学习的信号无关。如果我们对与问题相关的事物有完美的了解,我们就不会包含无用的特征!(公平地说,我们本来不会使用机器学习,但我扯远了)。为了弥补我们的无知并减少引入不必要复杂性的风险,我们将向损失中添加一个额外的项:
这有什么作用,你可能会问。我告诉你——我们刚刚引入了一个激励,以使参数的幅度尽可能接近零。换句话说,我们希望保持复杂性在最小化。现在,我们不再只有一个目标(最小化预测误差),我们多了一个(最小化复杂性),解决整体问题的方案将需要平衡这两个目标。通过那个闪亮的新 lambda 系数,我们对这种平衡有一定的发言权:它越大,我们就越重视低复杂性的目标。
说了这么多。让我们看看额外的项对最优 theta 的影响。新的向量化形式的损失是
计算其导数,将其设置为零并求解 theta:
事实证明,新解决方案与之前的方案并没有太大区别。我们多了一个额外的对角矩阵,它将处理任何可能导致我们无法反转 XTX 矩阵的秩问题,通过在混合中加入一些“抖动”。这揭示了另一种可能引入不必要复杂性的方式:共线特征,即线性变换(缩放和移动版本)的特征。就线性回归而言,两个共线特征实际上是同一个特征,只算作一列,额外的特征不会为训练模型提供任何新的信息;这使得 XTX 矩阵的秩不足,即无法反转。通过正则化,在 XTX 的每一行中,lambda(抖动)会影响不同的列——从而阻止任何可能的共线性,并使其变为满秩。我们看到,添加随机变量作为预测器实际上可能破坏整个学习过程。幸运的是,正则化可以拯救这一切(请不要误解我对正则化的赞美为在选择预测器时可以粗心大意——绝不是这样)。
所以,就这样,一个正则化的线性回归模型准备好了。接下来我们怎么做?嗯,你可以扩展正则化编码了关于参数空间的先验信念的想法,揭示其与贝叶斯推断的联系。或者你可以深入研究其与支持向量机的联系,并最终与像 KNN 这样的非参数模型的联系。同时,你还可以思考线性和逻辑回归中的那些华丽梯度看起来非常相似的事实。请放心,我将在未来的文章中触及这些主题!
通过推文对我们邮政服务的 AI 驱动分析
原文:
towardsdatascience.com/an-ai-powered-analysis-of-our-postal-service-through-tweets-fa1764409905
用 AI 解码客户声音
深入探讨机器学习、主题建模和情感分析,以揭示有价值的客户观点
·发布于 Towards Data Science ·13 分钟阅读·2023 年 3 月 22 日
–
图片作者:AI 生成的情感和主题分析 #royalmail
介绍
我和我的搭档通常体验到优质的邮政服务。大多数时候,信件会及时到达我们家,并且没有被拆开。因此,当我们的邮件几周没到时,我们觉得非常奇怪。在经过一番认真的网络搜索后,我们发现这次服务中断最可能的原因是罢工。作为数据科学家,这整个事件让我开始思考……
是否有办法利用在线数据追踪这些类型的事件?
对这个问题的回答是肯定的,我已经构建了一个原型,你可以试玩一下。我建议在继续阅读之前先试用一下,这样可以让你在进入技术细节之前对事物有一个了解。
🌏 探索应用 — 最好在电脑上打开,尽管手机也能使用。
我将在这篇文章的剩余部分带你了解我是如何回答这个问题的。这几乎是一个端到端的机器学习项目,涉及软件工程、社交媒体数据挖掘、主题建模、变压器、自定义损失函数、迁移学习和数据可视化等方面。如果这些对你有吸引力,拿个小吃或饮料,坐下来,因为这可能会比较长,但希望值得一读。
免责声明:本文是对包含 #royalmail 标签的推文的独立分析,与 Royal Mail Group Ltd 没有任何关联、认可或赞助。本文中表达的观点和发现仅代表作者个人观点,不代表 Royal Mail Group Ltd 或其任何子公司的观点或官方立场。
方法
在寻求了解人们的想法时,Twitter 总是一个很好的起点。人们在 Twitter 上发布的大部分内容是公开的,并且可以通过其 API 轻松访问。这是你可以找到大量客户服务见解的那种不受限制的言语领域。我感到好奇,自己进行了一个简单的 Twitter 搜索,从‘#royalmail’开始。瞧!一堆推文。
确定了数据源后,我做的下一步是找出如何“挖掘”那些推文中提出的问题。主题建模立即浮现在脑海中。我认为对推文进行某种类型的聚类可以揭示一些潜在的主题。我将花费剩下的篇幅深入一些技术细节。这不会是逐步的过程,而是窥视我的肩膀以及对我在这个项目中思考过程的窗口。
软件工程
开发环境:我大多数的机器学习项目都是用 python 完成的,所以我偏好使用 Jupyter labs。我发现能够快速切换 Jupyter notebooks、python 脚本和终端非常有用。
文件结构:这是一个相当复杂的项目,如果我这么说的话。这里有几个过程需要考虑,因此这不是一个可以仅从 Jupyter notebook 的安全环境中完成的任务。列出所有这些过程,我们有:数据提取、数据处理、主题建模、机器学习和数据可视化。为了帮助创建一些秩序,我通常会先建立一个合适的文件结构。你可以,也应该利用 bash 脚本来完成这个任务。
│ README.md
│ setup.py
│ __init__.py
│
├───data
│ ├───01_raw
│ │ tweets_details2023-03-15_20-43-36.csv
│ │
│ ├───02_intermediate
│ ├───03_feature_bank
│ ├───04_model_output
│ └───05_Reports
├───data_processing
│ collect_tweets.py
│ preprocess_tweets_lite.py
│ preprocess_tweets_rm.py
│ __init__.py
│
├───machine_learning
│ customer_trainer.py
│ makemodel.py
│ preprocess_ml.py
│ train_models.py
│ __init__.py
│
├───notebooks
│ HDBSCAN_UMAP_notebook.ipynb
│ Twitter Model Analysis Notebook .ipynb
│
└───topic_modeling
bert_umap_topic.py
tfidf.py
twitter_roberta_umap_topic.py
__init__.py
模块化:我将每个过程分解为模块,使其易于重用、调整和修改以适应不同的使用场景。模块还帮助保持代码的“干净”。如果没有模块化的方法,我会得到一个长达数千行的 Jupyter notebook 或 python 脚本,非常不吸引人且难以调试。
版本控制:对于复杂的项目,你不想丢失进度、覆盖重要内容或搞得一团糟。GitHub 是一个完美的解决方案,因为它使得搞砸的可能性很小。我首先创建一个远程仓库并将其克隆到本地机器上,这样我可以安心地知道我所有的辛勤工作都有备份。GitHub desktop 允许我在提交到远程仓库之前仔细跟踪任何更改。
包: 我利用了大量的开源包,下面我将列出关键的包并提供链接。
-
Transformers:hugging face 大型语言模型的 API。
-
Pytorch:用于构建和定制变换器的框架。
-
Streamlit:用于构建 web 应用程序。
-
Scikit Learn:机器学习框架。
-
UMAP:UMAP 算法的开源实现。
-
HDBSCAN:HDSCAN 算法的开源实现。
-
Folium:用于地理数据可视化。
-
CUDA:利用 GPU 功能的并行计算平台。
-
Seaborn:用于 Python 中数据可视化的库。
-
Pandas:处理结构化数据的库。
-
Numpy:在 Python 中执行数值操作的库。
环境管理:虽然互联网提供了丰富的库,但环境管理很容易变得混乱。为了管理这种复杂性,我喜欢在开始新项目时执行干净环境政策。每个项目严格一个环境。我选择使用 Anaconda 作为我的环境管理器,因为它提供了很大的灵活性。
注意:出于这个项目的目的,我确实为 streamlit web 应用程序和主题建模创建了单独的环境和 GitHub 仓库。
数据
我使用 Twitter API 提取了大约 30k 条公开可用的推文,搜索 #royalmail。我想强调的是,只有公开的数据才能通过 Twitter API 提取,这减轻了一些可能存在的数据隐私问题。
Twitter 数据非常混乱,对于任何自然语言处理(nlp)任务都极其困难。它是充满表情符号、语法不一致、特殊字符、脏话、网址以及所有其他自由文本带来的障碍的社交媒体数据。我为这个特定项目编写了自己的自定义脚本来清理数据。主要是去除网址和烦人的停用词。我提供了“简化”版本的代码片段,但在聚类过程中也使用了更高级的版本。
用于清理推文中的网址的模块
请注意,这符合 Twitter 的服务条款。他们允许通过 他们的 API对公开可用的数据进行分析和聚合。这些数据既允许用于 非商业和商业用途.&text=Your%20product%20or%20service%20is%20monetized%20if%20you%20earn%20money%20from%20it.)。
主题建模
我使用的主题建模方法受到 BERT 主题¹ 的启发。我最初尝试了潜在狄利克雷分配(Latent Dirichlet Allocation),但难以得到连贯的结果。BERT 主题是一个很好的参考点,但我注意到它并没有专门设计来从混乱的 Twitter 数据中提取主题。按照与 BERT 主题类似的逻辑步骤,我对方法进行了稍微的调整以适应这个任务。
从高层次来看,BERT 主题使用 BERT 模型生成嵌入,进行降维和聚类以揭示文档中的潜在主题。
我的做法利用了 twitter-xlm-roberta-base² 模型来生成嵌入。这个变换器已经在 Twitter 数据上进行过预训练,捕捉了所有复杂的细微差别,包括表情符号。嵌入只是以数字形式表示句子的一种方式,从而保留了语法和语义信息。嵌入是通过自注意力机制由变换器学习的。所有最近的大型语言模型领域的创新令人惊叹的是,人们可以利用最先进的模型为自己的目的生成嵌入。
我使用 UMAP 算法将推文嵌入投影到二维空间中,并使用 HDBSCAN 识别簇。将每个簇视为一个文档,我生成了 TF-IDF 分数,以提取一个关键字列表,大致‘定义’每个簇,从而形成我的初步主题。
TF-IDF 是一种便捷的方式来衡量一个词在某个簇中的重要性,考虑它在该簇中出现的频率以及它在更大簇组中的稀有程度。它有助于识别每个簇中独特且有意义的词。
这些降维方法有时一开始可能难以理解。我发现这些资源对帮助我掌握算法很有用。
理解 UMAP — 一个出色的资源,帮助你可视化和理解调整超参数的影响。
HDBSCAN 文档 — 我能找到的对 HDBSCAN 最连贯的解释是文档本身提供的。
最后,我通过对主题和推文本身之间的余弦相似度进行评分,测试生成主题的连贯性。这在纸面上看起来相当公式化,但我可以保证这并非简单的任务。这种无监督机器学习的性质就是反复试验。我花费了几十次迭代和人工努力来找到正确的参数,以从这些推文中提取连贯的主题。因此,与其详细讨论我使用的所有超参数,不如谈谈那些对这种方法至关重要的四个关键参数。
距离度量:在主题建模中,距离度量实际上是形成连贯主题与仅生成随机单词列表之间的区别。对于 UMAP 和 HDBSCAN,我选择了余弦距离。考虑到我的目标是建模主题,这个选择毫无疑问是正确的。主题是语义上相似的文本群体,而衡量语义相似度的最佳方法是余弦距离。
词汇数量:生成聚类后,我希望通过 TF-IDF 了解这些聚类的“内容”。这里选择的关键指标是每个聚类返回多少个单词。这可能从一个单词到整个文本语料库中唯一单词的数量不等。单词太多,你的主题会变得不连贯;单词太少,你的聚类覆盖率会很差。选择这个数量是一个反复试验的过程,经过几次迭代,我最终选择了每个主题 4 个单词。
评分:主题建模不是一门精确的科学,因此需要一些人工干预以确保主题有意义。我可以对几百甚至几千条推文进行此操作,但十几万条呢?那就不切实际了。因此,我使用了一个数值上的“黑客”方法,通过对生成的 TFIDF 主题与推文本身之间的余弦相似度进行评分。虽然这涉及很多反复试验,但经过几次迭代,我发现余弦相似度的适当截止值大约为 0.9。这使得原本 30k 条推文中大约有 3k 条被较好地分类。最重要的是,这提供了一个足够大的样本量来进行一些监督机器学习。
二维主题: UMAP 提供了一种方便的方式来可视化主题。我们可以看到,中心有一大块主题被聚集在一起,而一些较小的利基主题则在边缘。这实际上让我想起了一个星系。经过一些侦查工作(手动浏览电子表格),我发现这确实有意义。中心的大块主题主要围绕客户服务,通常是投诉。我认为特别吸引人的是模型能够实际隔离出非常利基的领域。这些领域包括政治、经济、就业和集邮(这不是某个小明星,而是集邮!)。当然,TF-IDF 返回的主题远没有这么一致,但我能够从分析中识别出 6 个良好分类的主题。我最终的 6 个主题是客户服务、政治、皇家回复、工作、财经新闻和集邮。
TF-IDF 在集群上生成的四个词汇主题列表,并考虑了与推文的 0.9+ 余弦相似度。
-
学徒,广告工作,工作,标签:工作
-
最大,老板,叛乱,年:政治
-
出生,回复,皇家信件,皇家回复:皇家回复
-
收集,包装,集邮爱好者,集邮:集邮
-
声明,股份公司,职位,简短:财经新闻
-
确定的,集邮爱好者,集邮,演示:集邮
-
驾驶,信息申请,工作,办公室:工作
-
驾驶,工作,sm1jobs,suttonjobs:工作
-
FTSE,RMG,股票,股息:财经新闻
-
德国,皇家,皇家信件,皇家回复:皇家回复
-
毕业工作,毕业生计划,求职,收听:工作
-
劳工,自由民主党,保守党,英国:政治
-
信件,邮件,服务,罢工:客户服务
-
卢森堡,皇家,皇家信件,皇家回复:皇家回复
-
新,利润,股东,世界:财经新闻
-
股份公司,职位,减少,WACE:财经新闻
作者图片:应用 UMAP 后的嵌入二维表示
作者图片:应用 HDBSCAN 后的主题视图。黄色区域是与客户服务相关
主题建模繁琐,绝对不是你想持续依赖以生成洞见的东西。就我而言,这应该是你每隔几个月进行一次的练习(取决于数据的保真度),以防出现任何新情况。
转移学习
在完成艰巨的主题建模任务后,我获得了一些标签和一个接近 3k 观察数据的合适数据集,用于训练模型。利用预训练的变换器意味着不必从头开始训练,也不需要构建自己的架构,能够利用模型现有知识的力量。
数据拆分
我使用标准的训练、验证和测试拆分,80% 的观察数据用于训练。请参见下面的脚本:
数据拆分脚本
实现自定义训练器的焦点损失
模型训练结果比我预期的要复杂,这并不是因为硬件要求,而是数据本身。我要处理的是一个高度不平衡的多类分类问题。客户服务观察在数据集中至少比下一个最突出类别多十倍。这导致模型性能被客户服务类别所压倒,从而导致其他不突出类别的召回率和精确度较低。
我最初从简单的类权重和交叉熵损失开始,但这没有奏效。经过快速的 Google 搜索,我发现焦点损失函数在解决类别不平衡问题上取得了成功。焦点损失重新塑造了交叉熵损失,以“降低”分配给分类良好示例的损失³。
焦点损失的原始论文关注于计算机视觉任务,其中图像具有浅景深。下面的图像是浅景深的一个例子,前景突出但背景分辨率很低。这种极端的前景与背景之间的不平衡类似于我在分类推文时需要处理的不平衡。
照片由Raphael Wild拍摄,发布于Unsplash
下面是我在自定义训练器对象中实现焦点损失的过程。
注意,类权重(alpha)是硬编码的。如果你想用于自己的目的,需要调整这些权重。
焦点损失的实现。自定义训练器只是标准训练器加上了焦点损失
模型训练
在进行了一些自定义调整后,我成功地拟合了一个模型(得益于我的 GPU 和 CUDA,时间不到 7 分钟)。Focal loss 与时间的关系给我们提供了一些证据,表明模型接近收敛。
作者提供的图像:Focal loss 与时间步长的关系
模型训练脚本。注意这里导入并实现了客户自定义的训练器。
模型性能
模型在包括 525 个随机选择的标记样本的测试数据集上进行了评估。性能表现令人印象深刻,各类的精确度和召回率都相当高。我需要强调的是,由于样本量较小,测试性能可能过于乐观,并且样本之外的推文性质可能有更多变化。然而,我们处理的是一个相对狭窄的领域(#royalmail),因此变化可能比更通用的领域要小。
作者提供的图像:混淆矩阵(测试数据集)
作者提供的图像:模型在测试集上的性能指标
地理可视化
为了有效地可视化我收集的大量信息,我决定创建一个情感地图。通过利用我训练的模型,我为 2023 年 1 月至 3 月之间的推文生成了话题。此外,我使用了来自 Cardiff NLP 的预训练 [twitter-roberta-base-sentiment](https://huggingface.co/cardiffnlp/twitter-roberta-base-sentiment-latest)
模型来评估每条推文的情感。为了构建最终的网络应用程序,我使用了 Streamlit。
生成 Streamlit 网络应用程序的脚本
商业应用
当前的应用程序作为一个基本原型存在,但可以扩展以揭示更深刻的见解。我将在下面简要讨论一些潜在的扩展:
-
时间过滤:加入日期范围过滤器,允许用户在特定时间段内浏览推文。这有助于识别趋势和情感随时间的变化。
-
互动可视化:实现互动图表和可视化工具,使用户能够探索数据集中情感、话题和其他因素之间的关系。
-
实时数据:将应用程序连接到实时 Twitter 数据,启用实时分析和可视化情感及话题。
-
高级过滤:提供更多高级过滤选项,如按用户、话题标签或关键字过滤,以便对特定对话和趋势进行更有针对性的分析。
通过扩展应用程序这些功能,你可以为用户提供一个更强大、更有洞察力的工具,以探索和理解推文中的情感和话题。
感谢阅读!
[## 通过我的推荐链接加入 Medium - John Adeojo
我分享数据科学项目、经验和专业知识,以帮助你在旅程中。你可以通过…注册 Medium。
medium.com [## 主页 | John Adeojo
关于我 一名经验丰富的数据科学家和机器学习(ML)专家,专注于构建定制的 ML 解决方案…
引用
[1]Grootendorst, M. (2022). BERTopic: 使用基于类的 TF-IDF 程序进行神经主题建模. Paperswithcode.com. paperswithcode.com/paper/bertopic-neural-topic-modeling-with-a-class
[2]Barbieri, F., Anke, L. E., & Camacho-Collados, J. (2022). XLM-T: 多语言语言模型在 Twitter 上的情感分析及更多. Paperswithcode.com. arxiv.org/abs/2104.12250
[3]Lin, T.-Y., Goyal, P., Girshick, R., He, K. 和 Dollar, P. (2018). Focal Loss for Dense Object Detection. Facebook AI Research (FAIR). [在线] 可用网址: arxiv.org/pdf/1708.02002.pdf
[访问于 2023 年 3 月 21 日]。