大型语言模型使许多任务变得更加容易,如制作聊天机器人、语言翻译、文本摘要等。我们过去常常为摘要编写模型,然后总是会遇到性能问题。现在,我们可以通过使用大型语言模型(LLM)来轻松实现这一点。例如,最先进的LLM已经可以处理整本书的上下文窗口。但是,在摘要非常大的文档时仍然存在一些限制。
# LLM 大型文档摘要的局限性
LLM中的上下文限制或上下文长度是指模型可以处理的标记数量。每个模型都有自己的上下文长度,也称为最大标记或标记限制。例如,标准的GPT-4模型的上下文长度为128,000个标记。超过这个数量的标记将丢失信息。一些最先进的LLM的上下文限制可以达到1,000,000个标记。然而,随着上下文限制的增加,LLM会受到最近性和首要性等限制的影响。我们还可以探讨一些缓解这些影响的方法。
- LLM中的首要性效应指的是模型更加重视序列开头呈现的信息。
- 最近性效应是指模型强调其处理的最新信息。
这两种效应都会使模型偏向于输入数据的特定部分。模型可能会跳过序列中的重要信息。
第二个问题是成本。我们可以通过拆分文本来解决上下文限制的问题,但我们不能直接将整本书传递给模型。这将花费很多钱。例如,如果我们有一本书有1,000,000个标记,我们直接将其传递给GPT4模型,总成本将约为90美元(提示和完成标记)。我们必须找到一种中间方式来摘要我们的文本,考虑价格、上下文限制和书的完整上下文。
在本教程中,您将学习如何在考虑模型的价格和上下文限制的情况下摘要一本完整的书籍。让我们开始吧。
# 使用LangChain和OpenAI总结大型文档
# 设置环境
要跟随本教程,您需要安装:
- Python
- 一个IDE(VS Code可行)
要安装依赖项,请打开终端并输入以下命令:
pip install langchain openai tiktoken fpdf2 pandas
此命令将安装所有所需的依赖项。
# 加载书籍
您将使用查尔斯·狄更斯的书籍《大卫·科波菲尔德》作为此项目的公开可用书籍。让我们使用LangChain提供的PyPDFLoader
实用程序加载书籍。
from langchain.document_loaders import PyPDFLoader
# 加载书籍
loader = PyPDFLoader("David-Copperfield.pdf")
pages = loader.load_and_split()
它将加载完整的书籍,但我们只对内容部分感兴趣。我们可以跳过前言和引言等页面。
# 去掉开头和结尾的部分
pages = pages[6:1308]
# 合并页面,并将制表符替换为空格
text = ' '.join([page.page_content.replace('\t', ' ') for page in pages])
现在,我们有了内容。让我们打印前200个字符。
text[0:200]
# 预处理
让我们从文本中删除不必要的内容,如不可打印字符、额外的空格等。
import re
def clean_text(text):
# 删除特定短语“Free eBooks at Planet eBook.com”及其周围的空白字符
cleaned_text = re.sub(r'\s*Free eBooks at Planet eBook\.com\s*', '', text, flags=re.DOTALL)
# 删除额外的空格
cleaned_text = re.sub(r' +', ' ', cleaned_text)
# 删除不可打印字符,可选择在“大卫·科波菲尔德”之前
cleaned_text = re.sub(r'(David Copperfield )?[\x00-\x1F]', '', cleaned_text)
# 用空格替换换行符
cleaned_text = cleaned_text.replace('\n', ' ')
# 删除连字符周围的空格
cleaned_text = re.sub(r'\s*-\s*', '', cleaned_text)
return cleaned_text
clean_text=clean_text(text)
清理数据后,我们准备开始解决摘要问题。
# 加载OpenAI API
在使用OpenAI API之前,我们需要进行配置并在此处提供凭据。
import os
os.environ["OPENAI_API_KEY"] = "your-openai-key-here"
在那里输入您的API密钥,它将设置环境变量。
让我们看看书中有多少个标记:
from langchain import OpenAI
llm = OpenAI()
Tokens = llm.get_num_tokens(clean_text)
print (f"We have {Tokens} tokens in the book")
这本书中有超过466,000个标记,如果我们将它们全部直接传递给LLM,它将收费很多。因此,为了降低成本,我们将实现K-means聚类来从书中提取重要的片段。
注意:使用K-means聚类的决策受到数据专家Greg Kamradt的教程的启发。
为了获取书中的重要部分,让我们首先将书分成不同的块。
# 将内容拆分为文档
我们将使用LangChain的SemanticChunker
实用程序将书的内容拆分为文档。
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
text_splitter = SemanticChunker(
OpenAIEmbeddings(), breakpoint_threshold_type="interquartile"
)
docs = text_splitter.create_documents([clean_text])
SemanticChunker
接收两个参数,第一个是嵌入模型。该模型生成的嵌入用于基于语义拆分文本。第二个参数是breakpoint_threshold_type
,它根据语义相似性确定文本应该在哪些点上拆分为不同的块。
注意:通过处理这些较小的、语义相似的块,我们旨在最小化LLM中的最近性和首要性效应。这种策略使我们的模型能够更有效地处理每个小的上下文,确保更平衡的解释和响应生成。
# 找到每个文档的嵌入
现在,让我们获取每个生成文档的嵌入。您将使用OpenAI
的默认方法获取嵌入。
import numpy as np
import openai
def get_embeddings(text):
response = openai.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data
embeddings=get_embeddings([doc.page_content for doc in docs]
)
get_embeddings
方法给出了所有文档的嵌入。
注意:text-embedding-3-small
方法是由OpenAI特别发布的,被认为更便宜和更快。
# 重新排列数据
接下来,我们将文档内容和它们的嵌入列表转换为pandas DataFrame,以便更轻松地处理和分析数据。
import pandas as pd
content_list = [doc.page_content for doc in docs]
df = pd.DataFrame(content_list, columns=['page_content'])
vectors = [embedding.embedding for embedding in embeddings]
array = np.array(vectors)
embeddings_series = pd.Series(list(array))
df['embeddings'] = embeddings_series
# 应用Faiss进行高效聚类
现在,我们将将文档向量转换为与Faiss (opens new window)兼容的格式,使用K-means将它们聚类为50个组,然后创建Faiss索引以在文档之间进行高效的相似性搜索。
import numpy as np
import faiss
# 如果尚未转换为float32,请将其转换为float32
array = array.astype('float32')
num_clusters = 50
# 向量维度
dimension = array.shape[1]
# 使用Faiss训练KMeans
kmeans = faiss.Kmeans(dimension, num_clusters, niter=20, verbose=True)
kmeans.train(array)
# 直接访问质心
centroids = kmeans.centroids
# 为原始数据集创建新索引
index = faiss.IndexFlatL2(dimension)
# 将原始数据集添加到索引中
index.add(array)
这个K-means聚类将文档分成50个组。
注意:选择K-means聚类的原因是每个聚类将具有相似的内容或相似的上下文,因为该聚类中的所有文档都具有相关的嵌入,并且我们将选择最接近核心的文档。
# 选择导出文档
现在,我们将只选择每个聚类中最重要的文档。为此,我们将只选择最接近质心的第一个向量。
D, I = index.search(centroids, 1)
此代码在索引上使用搜索方法,以找到与质心列表中的每个质心最接近的文档。它返回两个数组:D
,其中包含最接近其各自质心的文档的距离,以及I
,其中包含这些最接近文档的索引。搜索方法中的第二个参数1
指定每个质心只找到单个最接近的文档。
现在,我们需要对所选的文档索引进行排序,因为文档是根据书的顺序排列的。
sorted_array = np.sort(I, axis=0)
sorted_array=sorted_array.flatten()
extracted_docs = [docs[i] for i in sorted_array]
# 获取每个文档的摘要
下一步是使用GPT-4模型获取每个文档的摘要,以节省成本。要使用GPT-4,让我们定义模型。
model = ChatOpenAI(temperature=0,model="gpt-4")
定义提示并使用LangChain创建提示模板,将其传递给模型。
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_template("""
You will be given different passages from a book one by one. Provide a summary of the following text. Your result must be detailed and atleast 2 paragraphs. When summarizing, directly dive into the narrative or descriptions from the text without using introductory phrases like 'In this passage'. Directly address the main events, characters, and themes, encapsulating the essence and significant details from the text in a flowing narrative. The goal is to present a unified view of the content, continuing the story seamlessly as if the passage naturally progresses into the summary.
Passage:
```{text}```
SUMMARY:
"""
)
此提示模板将帮助模型更有效、更高效地摘要文档。
下一步是使用LangChain Expression Language(LCEL)定义LangChain的链。
chain= (
prompt
| model
|StrOutputParser() )
摘要链使用StrOutputParser (opens new window)来解析输出。还有其他输出解析器 (opens new window)可供探索。
最后,您可以将定义的链应用于每个文档以获取摘要。
from tqdm import tqdm
final_summary = ""
for doc in tqdm(extracted_docs, desc="Processing documents"):
# 获取新的摘要。
new_summary = chain2.invoke({"text": doc.page_content})
# 更新最后两个摘要的列表:删除第一个摘要并在末尾添加新的摘要。
final_summary+=new_summary
上面的代码逐个应用链到每个文档上,并将每个摘要连接到final_summary
。
# 将摘要保存为PDF
下一步是将摘要格式化并以PDF格式保存。
from fpdf import FPDF
class PDF(FPDF):
def header(self):
# 选择Arial粗体15号字体
self.set_font('Arial', 'B', 15)
# 向右移动
self.cell(80)
# 带边框的标题
self.cell(30, 10, 'Summary', 1, 0, 'C')
# 换行
self.ln(20)
def footer(self):
# 距离底部1.5厘米
self.set_y(-15)
# 选择Arial斜体8号字体
self.set_font('Arial', 'I', 8)
# 页码
self.cell(0, 10, 'Page %s' % self.page_no(), 0, 0, 'C')
# 实例化PDF对象并添加一页
pdf = PDF()
pdf.add_page()
pdf.set_font("Arial", size=12)
# 确保“last_summary”文本被视为UTF-8
# 如果不同,请将“last_summary”替换为您的实际文本变量
# 确保您的文本是一个UTF-8编码的字符串
last_summary_utf8 = last_summary.encode('latin-1', 'replace').decode('latin-1')
pdf.multi_cell(0, 10, last_summary_utf8)
# 将PDF保存到文件
pdf_output_path = "s_output1.pdf"
pdf.output(pdf_output_path)
因此,我们在PDF格式中获得了整本书的完整摘要。
# 结论
在本教程中,我们解决了使用LLM摘要大型文本(如整本书)时的复杂性,同时解决了与上下文限制和成本相关的挑战。我们学习了预处理文本的步骤,并实施了一种将语义分块和K-means聚类相结合的策略,以有效地处理模型的上下文限制。通过使用高效的聚类,我们有效地提取了关键段落,减少了直接处理大量文本的开销。这种方法不仅通过减少处理的标记数量显著降低了成本,而且还减轻了LLM固有的最近性和首要性效应,确保对所有文本段落进行平衡考虑。
通过LLM的API开发AI应用程序引起了极大的兴趣,其中向量数据库通过提供高效的上下文嵌入存储和检索在其中起着重要的作用。MyScaleDB (opens new window)是一个专门为AI应用程序设计的向量数据库,考虑到成本、准确性和速度等因素。它的SQL友好界面使开发人员能够在不学习新知识的情况下开始开发他们的AI应用程序。
如果您想与我们进一步讨论,欢迎加入MyScale Discord (opens new window)分享您的想法和反馈。