
如何用AI進行情感分析
我們首先需要安裝所需的Python包:
pip install --upgrade langchain openai tiktoken chromadb
安裝完成后,在代碼開頭導入以下模塊:
from langchain_openai import ChatOpenAI
from langchain.chains.summarize import load_summarize_chain
from langchain_community.document_loaders import WebBaseLoader
from langchain.chains.combine_documents.stuff import StuffDocumentsChain
from langchain.chains.llm import LLMChain
from langchain_core.prompts import PromptTemplate
from langchain.chains import MapReduceDocumentsChain, ReduceDocumentsChain
from langchain_text_splitters import CharacterTextSplitter
from langchain.docstore.document import Document
import tiktoken
import openai
import os
openai.api_key = os.getenv("OPENAI_API_KEY")
接下來,我們準備待總結的文本。本示例使用的是Harry Potter系列第四部”哈利波特與火焰杯”。由于OpenAI API調用需要付費,為了演示目的,我只截取了第31章之后的部分內容,包含45095個tokens。
這一部分剛好包含了哈利波特參加第三個火焰杯項目的內容,以及伏地魔復活的內容,算是一個比較完整連貫的情節。
# 讀入harry potter 4的文本
with open("Harry Potter and the Goblet of Fire.txt") as f:
text = f.read()
text = text.replace("\n", " ")
text = text.split("Chapter 31")[1]
看看待總結的文本前300個字符:
The Third Task
“Dumbledore reckons You-Know-Who’s getting stronger again as well?” Ron whispered.
Everything Harry had seen in the Pensieve, nearly everything Dumbledore had told and shown him afterward, he had now shared with Ron and Hermione — and, of course, with Sirius, to whom Harry had sent
我們用tiktoken來計算整個文檔的token數量:
# 計算整個待總結文檔的token數
def num_tokens_from_string(string: str, encoding_name: str) -> int:
"""Returns the number of tokens in a text string."""
encoding = tiktoken.get_encoding(encoding_name)
num_tokens = len(encoding.encode(string))
return num_tokens
doc_tokens = num_tokens_from_string(text, "cl100k_base")
print(f"待總結文檔共有 {doc_tokens} 個token")
輸出如下:
待總結文檔共有 45095 個token
由于GPT模型對輸入的上下文長度有限制,無法一次性處理過長的文本。因此,我們首先需要先將長文本切分成多個較短的文本塊。Langchain提供了多種文本切分器,可根據需求選擇合適的切分方式。
RecursiveCharacterTextSplitter會先將輸入文本按照指定的分隔符切分成粗糙的文本塊,然后根據chunk_size參數將這些塊進一步切分,使每個最終文本塊的字符數不超過指定值chunk_size=10000
?,而model_name="gpt-3.5-turbo-1106"
?指定使用gpt-3.5-turbo-1106模型的編碼信息。
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
model_name="gpt-3.5-turbo-1106",
chunk_size=10000,
chunk_overlap=20,
)
pages = text_splitter.split_text(text)
texts = [Document(page_content=p) for p in pages]
print(len(texts))
Stuffing是最簡單直接的總結方法,即直接將全文內容一次性傳遞給語言模型,讓模型直接基于全文生成摘要。
它的優點是只需調用API一次,速度較快;但缺點是受限于語言模型的最大上下文長度,當文本過長時無法處理全文(我們首先講Stuffing方法,只因為它是下面兩類長文本總結方法的一個對照組)。
因此,在下面的代碼中,我們只傳入了切分后的第一個文本塊texts[0]
,之前我們在切分文本的函數中設置了chunk_size=10000
,因此我們拿到的單個文本塊不會超過GPT模型的上下文窗口限制。
# 寫法1:使用 load_summarize_chain
prompt_template = """Write a concise summary in chinese of the following:
"{text}"
CONCISE SUMMARY:"""
prompt = PromptTemplate(template=prompt_template, input_variables=["text"])
llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo-1106")
chain = load_summarize_chain(llm, chain_type="stuff",prompt=prompt)
result = chain.run([Document(texts[0].page_content)])
result
輸出如下:
'哈利和他的朋友們討論了鄧布利多的警告,認為伏地魔正在變得更加強大。他們還討論了斯內普的信任問題,以及里塔·斯基特的報道。哈利和朋友們準備參加第三個任務,他們在迷宮中遇到了各種挑戰,包括巨大的怪獸和謎題。在迷宮中,哈利遇到了克魯姆,他使用了不可饒恕的咒語對待了其他選手。最后,哈利遇到了一個人面獅身的斯芬克斯,她提出了一個謎題,哈利成功回答后繼續前進。'
我們也可以使用StuffDocumentsChain
來實現文本總結的功能。寫法2輸出的寫法1的輸出是等價的。
# 寫法2:使用 StuffDocumentsChain
prompt_template = """Write a concise summary in chinese of the following:
"{text}"
CONCISE CHINESE SUMMARY:"""
prompt = PromptTemplate.from_template(prompt_template)
llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo-1106")
llm_chain = LLMChain(llm=llm, prompt=prompt)
chain = StuffDocumentsChain(llm_chain=llm_chain, document_variable_name="text")
result = chain.run([Document(texts[0].page_content)])
result
當要總結的文本長度超過模型的最大上下文長度時,Stuffing就無用了。Map-Reduce是一種更高級的文本摘要策略,核心思想是“分而治之”:
優點是可以總結任意長度的文本。缺點是需要多次調用API,速度較慢,成本較高。
我們可以使用LangChain實現Map-Reduce文本摘要:
# 寫法1:使用 load_summarize_chain
prompt_template = """Write a concise summary in chinese of the following text:
"{text}"
CONCISE CHINESE SUMMARY:"""
prompt = PromptTemplate(template=prompt_template, input_variables=["text"])
llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo-1106")
chain = load_summarize_chain(llm, chain_type="map_reduce", map_prompt=prompt, combine_prompt=prompt,token_max = 10000)
result = chain.run(texts)
我們也可以使用MapReduceDocumentsChain
:
# 寫法2,使用 MapReduceDocumentsChain,自定義map_prompt和reduce_prompt
# map chian
map_template = """
Write a summary in chinese of this chunk of text that includes the main points and any important details.
{texts}
"""
map_prompt = PromptTemplate.from_template(map_template)
map_chain = LLMChain(llm=llm, prompt=map_prompt)
# reduce chain
reduce_template = """The following is set of summaries in Chinese:
{texts}
Take these and distill it into a final, consolidated summary in chinese.
CHINESE ANSWER:"""
reduce_prompt = PromptTemplate.from_template(reduce_template)
reduce_chain = LLMChain(llm=llm, prompt=reduce_prompt)
# combine chain
combine_documents_chain = StuffDocumentsChain(
llm_chain=reduce_chain, document_variable_name="texts"
)
# reduce chain
reduce_documents_chain = ReduceDocumentsChain(
combine_documents_chain=combine_documents_chain,
collapse_documents_chain=combine_documents_chain,
token_max = 10000,
)
# map-reduce chain
map_reduce_chain = MapReduceDocumentsChain(
llm_chain=map_chain,
reduce_documents_chain=reduce_documents_chain,
document_variable_name="texts",
return_intermediate_steps=True,
)
result = map_reduce_chain.invoke(texts)
print(result['output_text'])
輸出如下:
哈利、羅恩和赫敏在準備第三項任務時,討論了鄧布利多對斯內普的信任、麗塔·斯基特的報道、以及對巴格曼和馬德琳·馬克西姆的猜測。他們在黑魔法防御課上練習咒語,準備迎接挑戰。在任務當天,哈利和塞德里克一起抓住了三強杯,但被傳送到了一個墓地。伏地魔出現,宣布要殺死哈利,展示自己的力量。任務結束后,哈利發現自己被綁在伏地魔父親的墓碑上,面對著伏地魔的威脅。在最后一刻,哈利使用驅魔術成功反擊了伏地魔,展現出了勇敢和決心。鄧布利多在離別宴會上向全校師生宣布了塞德里克·迪戈里被伏地魔謀殺的消息,并表揚了哈利·波特的勇敢行為。他強調了團結的重要性,呼吁大家共同對抗伏地魔的黑暗勢力。同時,海格和馬德姆·馬克西姆被派去執行一項秘密任務。整個學校都在為即將到來的黑暗和困難時期做準備。
Map-Reduce雖然可以處理任意長度的文本,但仍有改進空間。因為reduce階段只是機械地將各段落摘要拼接,再做一次總結,并沒有考慮上下文銜接、冗余去重等。
Refine是一種迭代式總結方法:
與Map-Reduce相比,Refine的特點是每一步迭代都會將上一步的中間摘要結果作為上下文,與當前文本片段一起輸入,不斷細化總結結果。LangChain對Refine方法也提供了支持:
prompt_template = """Write a concise summary of the following:
{text}
CONCISE SUMMARY:"""
prompt = PromptTemplate.from_template(prompt_template)
refine_template = (
"Your job is to produce a final comprehensive summary in Chinese, considering all the context provided so far, including: {existing_answer}\\n"
"We have the opportunity to further refine and build upon the existing summary"
"with some more context below.\\n"
"------------\\n"
"{text}\\n"
"------------\\n"
"Given the new context, refine and original summary comprehensively and concisely in Chinese, making sure to cover important details from the entire context."
)
refine_prompt = PromptTemplate.from_template(template=refine_template)
chain = load_summarize_chain(
llm=llm,
chain_type="refine",
question_prompt=prompt,
refine_prompt=refine_prompt,
return_intermediate_steps=True,
input_key="input_documents",
output_key="output_text",
)
result = chain({"input_documents": texts}, return_only_outputs=True)
輸出:
哈利、羅恩和赫敏討論伏地魔的威脅不斷增長,以及鄧布利多對斯內普的信任。他們還為三巫魔杯比賽的第三項任務做準備,哈利練習咒語和法術。比賽當天,哈利和其他冠軍進入了一個充滿障礙和生物的迷宮。哈利遇到了一位攝魂怪、一只爆裂蠕蟲和一只獅身人面獸,最終與克魯姆對峙,克魯姆對塞德里克使用了不可饒恕的咒語。哈利和塞德里克分道揚鑣,哈利遇到了獅身人面獸,獅身人面獸用謎語向他挑戰。在迷宮中,哈利和塞德里克一起觸摸了三巫魔杯,結果被傳送到了一個墓地。伏地魔重獲人形,向死噴火,準備殺死哈利。在墓地上,哈利和伏地魔進行了一場激烈的對決,最終哈利成功利用三巫魔杯的力量逃脫,帶著塞德里克的尸體回到霍格沃茨。接著,哈利被發現了一個隱藏的真相,原來一直以來的“麻瓜”魔法師是偽裝的,實際上是巴蒂·克勞奇,他被伏地魔派來執行一系列陰謀。同時,鄧布利多與斯內普和西里斯進行了重要的對話,以及與魔法部長福吉的交涉。整個故事充滿了悲傷和挑戰,但也展現了哈利的勇氣和決心。最后,哈利在回到普里懷特大街之前,與弗雷德和喬治分別,將三巫魔杯的獎金交給了他們,以支持他們的玩笑商店。
在運行上述代碼時,我感覺至少調整了幾十次prompt的設計……map_reduce
和refine
似乎都對prompt異常敏感,哪怕只是prompt微小的改動,最終結果也會有很大出入。
另外,refine
似乎很容易遺忘最初的一些關鍵信息,尤其是在概括小說情節時,它好像會忽視前期文本總結,而過于側重后續的文本。再加上我一直要求模型從英文原文中提煉出中文摘要,在翻譯過程中也會逐步累積一些小錯誤,導致經過多次迭代后,最終生成的結果顯得有點奇怪。
總之,我想說的是,對于這類任務,根據具體文本內容對prompt進行定制化設計是至關重要的,尤其是在使用map_reduce和refine這種需要編寫多個prompt的情況下。而上述代碼只是提供了一種可能的思路,直接照搬可能會存在一些問題,需要自己多煉煉。
此外,LangChain官方文檔對于使用load_summarize_chain
(寫法1)和直接加載MapReduceDocumentsChain
、ReduceDocumentsChain
、RefineDocumentsChain
(寫法2)的區別解釋得不夠清晰,給出的示例代碼運行起來也存在一些問題。我個人猜測可能直接加載load_summarize_chain有內置的prompt,比較快速,而加載不同的鏈則側重于更多自定義的細節。
本文章轉載微信公眾號@TextMatters