第3章 句詞分類——句子Token都是類別
第3章 句詞分類——句子Token都是類別
??上一章我們介紹了相似匹配相關的基礎,以及使用相似匹配技術能夠實現(xiàn)的任務和應用。相似匹配以Embedding為核心,關注的是如何更好地表示文本。基于Embedding的表示往往是語義層面的,一般使用余弦相似度來衡量。我們也提到了,其實不光文本可以Embedding,其實任意對象都可以Embedding,這一技術廣泛應用在深度學習算法各個領域。
??本章我們關注NLP領域最常見的兩類任務:句子分類和Token分類,由于中文的字也是詞,所以也會叫作句詞分類。我們將首先介紹句子分類的基礎,包括相關的一些常見任務,以及如何對句子和Token進行分類。接下來就是ChatGPT相關接口的使用,其他廠商提供的類似接口用法也類似。我們會發(fā)現(xiàn)通過類似ChatGPT這樣的大模型接口其實可以做很多任務,句詞分類只是其中一部分。最后,依然是相關的應用,這些應用傳統(tǒng)方法也可以解決,但一般都會更加復雜、更加麻煩。相比較而言,基于大模型就簡單多了,而且效果也不錯。與上一章一樣,我們依然會重點關注最終目的和為達到目的使用的方法與流程。
3.1 句詞分類基礎
??自然語言理解(natural language understanding,NLU)與自然語言生成(natural language generation,NLG)任務并稱為NLP兩大主流任務。一般意義上的NLU常指與理解給定句子意思相關的情感分析、意圖識別、實體抽取、指代關系等任務,在智能對話中應用比較廣泛。具體來說,當用戶輸入一句話時,機器人一般會針對該句話(也可以把歷史記錄給附加上)進行全方面分析,包括但不限于以下內容。
- 情感分析。簡單來說,一般都會包括正向、中性、負向三種類型,也可以設計更多的類別,或更復雜的細粒度情感分析。我們著重說一下細粒度情感分析,它主要是針對其中某個實體或屬性的情感。比如電商購物的商品評論中,用戶可能會對商品價格、快遞、服務等一個或多個方面表達自己的觀點。這時候,我們更加需要的是對不同屬性的情感傾向,而不是整個評論的情感傾向。
- 意圖識別。一般都是分類模型,大部分時候都是多分類,但是也有可能是層級標簽分類或多標簽分類。多分類是指給定輸入文本,輸出為一個標簽,但標簽的總數(shù)有多個,比如對話文本的標簽可能包括詢問地址、詢問時間、詢問價格、閑聊等等。層級標簽分類是指給定輸入文本,輸出為層級的標簽,也就是從根節(jié)點到最終細粒度類別的路徑,比如詢問地址/詢問家庭地址、詢問地址/詢問公司地址等。多標簽分類是指給定輸入文本,輸出不定數(shù)量的標簽,也就是說每個文本可能有多個標簽,標簽之間是平級關系,比如投訴、建議(既有投訴同時提出了建議)。
- 實體和關系抽取。實體抽取就是提取出給定文本中的實體。實體一般指具有特定意義的實詞,如人名、地名、作品、品牌等等,大部分時候都是業(yè)務直接相關或需要重點關注的詞。關系抽取是指實體和實體之間的關系判斷。實體之間往往有一定的關系,比如“中國四大名著之首《紅樓夢》由清代作家曹雪芹編寫。”其中“曹雪芹”就是人名,“紅樓夢”是作品名,其中的關系就是“編寫”,一般會和實體作為三元組來表示:(曹雪芹,編寫,紅樓夢)。
??一般經(jīng)過上面這些分析后,機器人就可以對用戶的輸入有一個比較清晰的理解,便于接下來據(jù)此做出響應。另外值得一提的是,上面的過程并不一定只用在對話中,只要涉及到用戶輸入查詢(Query),需要系統(tǒng)給出響應的場景,都需要這個NLU的過程,一般也叫Query解析。
??上面的幾個分析,如果從算法的角度看,分為下面兩種。
- 句子級別的分類,如情感分析、意圖識別、關系抽取等。也就是給一個句子(也可能有其他一些信息),給出一個或多個標簽。
- Token級別的分類,如實體抽取、閱讀理解(就是給一段文本和一個問題,然后在文本中找出問題的答案)。也就是給一個句子,給出對應實體或答案的索引位置。
??Token級別的分類不太好理解,我們舉個例子,比如剛剛提到的這句話:“中國四大名著之首紅樓夢由清代作家曹雪芹編寫。”它在標注的時候是這樣的。
中 O
國 O
四 O
大 O
名 O
著 O
之 O
首 O
《 O
紅 B-WORK
樓 I-WORK
夢 I-WORK
《 O
由 O
清 O
代 O
作 O
家 O
曹 B-PERSON
雪 I-PERSON
芹 I-PERSON
編 O
寫 O
。 O
??在這個例子中,每個Token就是每個字,每個Token會對應一個標簽(當然也可以多個),標簽中的B表示開始(Begin),I表示內部(Internal),O表示其他(Other),也就是非實體。“紅樓夢”是作品,我們標注為WORK,”曹雪芹“是人名,我們標注為PERSON。當然,也可以根據(jù)實際需要決定是否標注”中國“、”清代“等實體。模型要做的就是學習這種對應關系,當給出新的文本時,能夠給出每個Token的標簽預測。
??可以看到,其實它們本質上都是分類任務,只是分類的位置或標準不一樣。當然了,實際應用中會有各種不同的變化和設計,但整個思路是差不多的,我們并不需要掌握其中的細節(jié),只需知道輸入、輸出和基本的邏輯就好了。
3.1.1 如何對一句話進行分類
??接下來,我們簡單介紹這些分類具體是怎么做的,先說句子級別的分類。回憶上一章的Embedding,那可以算是整個深度學習NLP的基石,我們這部分的內容也會用到Embedding,具體過程如下。
- 將給定句子或文本表征成Embedding。
- 將Embedding傳入一個神經(jīng)網(wǎng)絡,計算得到不同標簽的概率分布。
- 將上一步得到的標簽概率分布與真實的標簽做比較,并將誤差回傳,修改神經(jīng)網(wǎng)絡的參數(shù),即訓練。
- 得到訓練好的神經(jīng)網(wǎng)絡,即模型。
??舉個例子,簡單起見,我們假設Embedding維度為32維(上一章OpenAI返回的維度比較大),如下所示。
import numpy as np
np.random.seed(0)
emd = np.random.normal(0, 1, (1, 32))
??這里,我們隨機生成一個均值為0、標準差為1的1×32維的高斯分布作為Embedding數(shù)組,維度1表示我們詞表里只有一個Token。如果我們是三分類,那么最簡單的模型參數(shù)W就是32×3的大小,模型的預測過程如下所示。
W = np.random.random((32, 3))
z = emd @ W
z == array([[6.93930177, 5.96232449, 3.96168115]])
z.shape == (1, 3)
??這里得到的z一般被稱為logits,如果我們想要概率分布,則需要對其歸一化,也就是將logits變成0-1之間的概率值,并且每一行加起來為1(即100%),如下所示。
def norm(z):
exp = np.exp(z)
return exp / np.sum(exp)
y = norm(z)
y == array([[0.70059356, 0.26373654, 0.0356699 ]])
np.sum(y) == 0.9999999999999999
??根據(jù)給出的y,我們就知道預測的標簽是第0個位置的標簽,因為那個位置的概率最大(70.06%)。如果真實的標簽是第1個位置的標簽,那第1個位置的標簽實際就是1(100%),但它目前預測的概率只有26.37%,這個誤差就會回傳來調整W參數(shù)。下次計算時,第0個位置的概率就會變小,第1個位置的概率則會變大。這樣通過標注的數(shù)據(jù)樣本不斷循環(huán)迭代的過程其實就是模型訓練的過程,也就是通過標注數(shù)據(jù),讓模型W盡可能正確預測出標簽。
??實際中,W往往比較復雜,可以包含任意的數(shù)組,只要最后輸出變成1×3的大小即可,比如我們寫個稍微復雜點的。
w1 = np.random.random((32, 100))
w2 = np.random.random((100, 32))
w3 = np.random.random((32, 3))
y = norm(norm(norm(emd @ w1) @ w2) @ w3)
y == array([[0.32940147, 0.34281657, 0.32778196]])
??可以看到,現(xiàn)在有三個數(shù)組的模型參數(shù),形式上雖然復雜了些,但結果是一樣的,依然是一個1×3大小的數(shù)組。接下來的過程就和前面一樣了。
??稍微復雜點的是多標簽分類和層級標簽分類,這倆因為輸出的都是多個標簽,處理起來要麻煩一些,不過它們的處理方式是類似的。我們以多標簽分類來說明,假設有10個標簽,給定輸入文本,可能是其中任意多個標簽。這就意味著我們需要將10個標簽的概率分布都表示出來。可以針對每個標簽做個二分類,也就是說輸出的大小是10×2的,每一行表示”是否是該標簽“的概率分布,示例如下。
def norm(z):
axis = -1
exp = np.exp(z)
return exp / np.expand_dims(np.sum(exp, axis=axis), axis)
np.random.seed(42)
emd = np.random.normal(0, 1, (1, 32))
W = np.random.random((10, 32, 2))
y = norm(emd @ W)
y.shape == (10, 1, 2)
y == array([
[[0.66293305, 0.33706695]],
[[0.76852603, 0.23147397]],
[[0.59404023, 0.40595977]],
[[0.04682992, 0.95317008]],
[[0.84782999, 0.15217001]],
[[0.01194495, 0.98805505]],
[[0.96779413, 0.03220587]],
[[0.04782398, 0.95217602]],
[[0.41894957, 0.58105043]],
[[0.43668264, 0.56331736]]
])
??這里的輸出每一行有兩個值,分別表示”不是該標簽“和”是該標簽“的概率。比如第一行,預測結果顯示,不是該標簽的概率為66.29%,是該標簽的概率為33.71%。需要注意的是歸一化時,我們要指定維度求和,否則就變成所有的概率值加起來為1了,這就不對了(應該是每一行的概率和為1)。
??上面是句子級別分類(sequence classification)的邏輯,我們必須再次說明,實際場景會比這里的例子要復雜得多,但基本思路是這樣的。我們在大模型時代也并不需要自己去構建模型了,本章后面會講到如何使用大模型接口進行各類任務。
3.1.2 從句子分類到Token分類
??接下來看Token級別的分類,有了剛剛的基礎,這個就比較容易理解了。它最大的特點是:Embedding是針對每個Token的。也就是說,如果給定文本長度為10,假定維度依然是32,那Embedding的大小就為:(1, 10, 32)。比剛剛句子分類用到的(1, 32)多了個10。換句話說,這個文本的每一個Token都是一個32維的向量。
??下面我們假設標簽共有5個,和上面的例子對應,分別為:B-PERSON、I-PERSON、B-WORK、I-WORK和O。基本過程如下。
emd = np.random.normal(0, 1, (1, 10, 32))
W = np.random.random((32, 5))
z = emd @ W
y = norm(z)
y.shape == (1, 10, 5)
y == array([[
[0.23850186, 0.04651826, 0.12495322, 0.28764271, 0.30238396],
[0.06401011, 0.3422055 , 0.54911626, 0.01179874, 0.03286939],
[0.18309536, 0.62132479, 0.09037235, 0.06016401, 0.04504349],
[0.01570559, 0.0271437 , 0.20159052, 0.12386611, 0.63169408],
[0.1308541 , 0.06810165, 0.61293236, 0.00692553, 0.18118637],
[0.08011671, 0.04648297, 0.00200392, 0.02913598, 0.84226041],
[0.05143706, 0.09635837, 0.00115594, 0.83118412, 0.01986451],
[0.03721064, 0.14529403, 0.03049475, 0.76177941, 0.02522117],
[0.24154874, 0.28648044, 0.11024747, 0.35380566, 0.0079177 ],
[0.10965428, 0.00432547, 0.08823724, 0.00407713, 0.79370588]
]])
??注意看,每一行表示一個Token是某個標簽的概率分布(每一行加起來為1),比如第一行。
sum([0.23850186, 0.04651826, 0.12495322, 0.28764271, 0.30238396]) == 1.00000001
??具體的意思是,第一個Token是第0個位置標簽(假設標簽按上面給出的順序,那就是B-PERSON)的概率是23.85%,其他類似。根據(jù)這里預測的結果,第一個Token的標簽是O,那真實的標簽和這個預測的標簽之間就可能有誤差,通過誤差就可以更新參數(shù),從而使得之后預測時能預測到正確的標簽(也就是正確位置的概率最大)。不難看出,這個邏輯和前面的句子分類是類似的,其實就是對每一個Token做了個多分類。
??關于NLU常見問題的基本原理我們就介紹到這里,讀者如果對更多細節(jié)感興趣,可以閱讀NLP算法相關的書籍,從一個可直接上手的小項目開始一步一步構建自己的知識體系。
3.2 ChatGPT接口使用
3.2.1 基礎版GPT續(xù)寫
??這一小節(jié)我們介紹OpenAI的Completion接口,利用大模型的In-Context能力進行零樣本或少樣本的推理。這里有幾個重要概念第一章有過介紹,這里簡要回顧一下。
- In-Context:簡單來說就是一種上下文能力,也就是模型只要根據(jù)輸入的文本就可以自動給出對應的結果。這種能力是大模型在學習了非常多的文本后獲得的,可以看做是一種內在的理解能力。
- 零樣本:直接給模型文本,讓它給出我們要的標簽或輸出。
- 少樣本:給模型一些類似的樣例(輸入+輸出),再拼上一個新的沒有輸出的輸入,讓模型給出輸出。
??接下來,我們就可以用同一個接口,通過構造不同的輸入來完成不同的任務。換句話說,通過借助大模型的In-Context能力,我們只需要輸入的時候告訴模型我們的任務就行,讓我們看看具體的用法。
import openai
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
openai.api_key = OPENAI_API_KEY
def complete(prompt: str) -> str:
response = openai.Completion.create(
model="text-davinci-003",
prompt=prompt,
temperature=0,
max_tokens=64,
top_p=1.0,
frequency_penalty=0.0,
presence_penalty=0.0
)
ans = response.choices[0].text
return ans
??Completion接口不僅能幫助我們完成一段話或一篇文章的續(xù)寫,而且可以用來做各種各樣的任務,比如這章講到的句子和實體分類任務。相比上一章的Embedding接口,它的接口參數(shù)要復雜多了,我們對其中比較重要的參數(shù)進行說明。
-
model:指定的模型,text-davinci-003就是其中一個模型,我們可以根據(jù)自己的需要,參考官方文檔進行選擇,一般需要綜合價格和效果進行權衡。 -
prompt:提示詞,默認為<|endoftext|>,它是模型在訓練期間看到的文檔分隔符,因此如果未指定提示詞,模型將像從新文檔開始一樣。簡單來說,就是給模型的提示語。 -
max_tokens:生成的最大Token數(shù),默認為16。注意這里的Token數(shù)不一定是字數(shù)。提示詞+生成的文本,所有的Token長度不能超過模型的上下文長度。不同模型可支持的最大長度不同,可參考相應文檔。 -
temperature:溫度,默認為1。采樣溫度,介于0和2之間。較高的值(如0.8)將使輸出更加隨機,而較低的值(如0.2)將使其更加集中和確定。通常建議調整這個參數(shù)或下面的top_p,但不建議同時更改兩者。 -
top_p:下一個Token在累計概率為top_p的Token中采樣。默認為1,表示所有Token在采樣范圍內,0.8則意味著只選擇前80%概率的Token進行下一次采樣。 -
stop:停止的Token或序列,默認為null,最多4個,如果遇到該Token或序列就停止繼續(xù)生成。注意生成的結果中不包含stop。 -
presence_penalty:存在懲罰,默認為0,介于-2.0和2.0之間的數(shù)字。正值會根據(jù)新Token到目前為止是否出現(xiàn)在文本中來懲罰它們,從而增加模型討論新主題的可能性。太高可能會降低樣本質量。 -
frequency_penalty:頻次懲罰,默認為0,介于-2.0和2.0之間的數(shù)字。正值會根據(jù)新Token到目前為止在文本中的現(xiàn)有頻率來懲罰新Token,降低模型重復生成相同內容的可能性。太高可能會降低樣本質量。
??在大部分情況下,我們只需考慮上面這幾個參數(shù)即可,甚至只需要前兩個參數(shù),其他的用默認也行。不過熟悉上面的參數(shù)將幫助我們更好地使用接口。另外值得說明的是,雖然這里用的是OpenAI的接口,但其他廠商類似接口的參數(shù)也差不太多。熟悉這里的參數(shù),到時候切換起來也能更得心應手。
??下面我們先看幾個句子分類的例子,我們將分別展示怎么使用零樣本和少樣本。零樣本的例子如下所示。
# 零樣本,來自openai官方示例
prompt="""The following is a list of companies and the categories they fall into:
Apple, Facebook, Fedex
Apple
Category:
"""
ans = complete(prompt)
ans == """
Technology
Facebook
Category:
Social Media
Fedex
Category:
Logistics and Delivery
"""
可以看到,我們只是列出了公司名稱,和對應的格式,模型可以返回每個公司所屬的類別。下面是少樣本的例子。
# 少樣本
prompt = """今天真開心。-->正向
心情不太好。-->負向
我們是快樂的年輕人。-->
"""
ans = complete(prompt)
ans == """
正向
"""
這個例子中,我們先給了兩個樣例,然后給出一個新的句子,讓模型輸出其類別。可以看到,模型成功輸出”正向“。
??再看看Token分類(實體提取)的例子,依然是先看零樣本例子,如下所示。
# 零樣本,來自openai官方示例
prompt = """
From the text below, extract the following entities in the following format:
Companies: <comma-separated list of companies mentioned>
People & titles: <comma-separated list of people mentioned (with their titles or roles appended in parentheses)>
Text:
In March 1981, United States v. AT&T came to trial under Assistant Attorney General William Baxter. AT&T chairman Charles L. Brown thought the company would be gutted. He realized that AT&T would lose and, in December 1981, resumed negotiations with the Justice Department. Reaching an agreement less than a month later, Brown agreed to divestiture—the best and only realistic alternative. AT&T's decision allowed it to retain its research and manufacturing arms. The decree, titled the Modification of Final Judgment, was an adjustment of the Consent Decree of 14 January 1956. Judge Harold H. Greene was given the authority over the modified decree....
In 1982, the U.S. government announced that AT&T would cease to exist as a monopolistic entity. On 1 January 1984, it was split into seven smaller regional companies, Bell South, Bell Atlantic, NYNEX, American Information Technologies, Southwestern Bell, US West, and Pacific Telesis, to handle regional phone services in the U.S. AT&T retains control of its long distance services, but was no longer protected from competition.
"""
ans = complete(prompt)
ans == """
Companies: AT&T, Bell South, Bell Atlantic, NYNEX, American Information Technologies, Southwestern Bell, US West, Pacific Telesis
People & titles: William Baxter (Assistant Attorney General), Charles L. Brown (AT&T chairman), Harold H. Greene (Judge)
"""
??在官方這個例子中,要求模型從給定的文本中提取實體,并按要求的格式輸出。對”公司“實體,輸出用逗號分隔的公司列表;對”人物和頭銜“實體,輸出逗號分隔的人物列表(括號中是他們的頭銜或角色)。可以看到,模型很好地完成了任務。下面是少樣本的例子,我們把實體設置的稍微特殊一些,不使用常見的人名、公司、地址等,我們用音樂方面的一個關于和弦的小樂理知識。
# 少樣本
prompt = """
根據(jù)下面的格式抽取給定Text中的實體:
和弦: <實體用逗號分割>
Text:
增三和弦是大三度+大三度的增五度音,減三和弦是小三度+小三度的減五度音。
和弦:增三和弦,減三和弦
Text:
三和弦是由3個按照三度音程關系排列起來的一組音。大三和弦是大三度+小三度的純五度音,小三和弦是小三度+大三度的純五度音。
"""
ans = complete(prompt)
ans == "和弦:大三和弦,小三和弦"
??結果看起來很不錯,讀者可以嘗試如果不給這個例子,它會輸出什么。另外,也可以嘗試給一些其他例子看看效果如何。值得注意的是,隨著OpenAI模型的不斷升級,這一接口將逐漸廢棄。
3.2.2 進階版ChatGPT指令
??這一小節(jié)我們介紹ChatGPT接口,接口名是ChatCompletions,可以理解為對話,同時它也幾乎可以做任意的NLP任務。參數(shù)和Completion類似,我們依然介紹一下主要參數(shù)。
-
model:指定的模型,gpt-3.5-turbo就是ChatGPT,讀者可以根據(jù)實際情況參考官方文檔選擇合適的模型。 -
messages:會話消息,支持多輪,多輪就是多條。每一條消息為一個字典,包含role和content兩個字段,表示角色和消息內容。如:[{"role": "user", "content": "Hello!"}] -
temperature:和Completion接口含義一樣。 -
top_p:和Completion接口含義一樣。 -
stop:和Completion接口含義一樣。 -
max_tokens:默認無上限,其他和Completion接口含義一樣,也受限于模型能支持的最大上下文長度。 -
presence_penalty:和Completion接口含義一樣。 -
frequency_penalty:和Completion接口含義一樣。
??更多可以參考官方文檔,值得再次一提的是,接口支持多輪,而且多輪非常簡單,只需要把歷史會話加進去就可以了。
??接下來,我們采用ChatGPT方式來做上一小節(jié)做過的任務。與前面類似,首先寫一個通用的方法,如下所示。
import openai
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
openai.api_key = OPENAI_API_KEY
def ask(content: str) -> str:
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": content}]
)
ans = response.get("choices")[0].get("message").get("content")
return ans
??我們依次嘗試上面的例子,先來看第一個公司分類的例子,如下所示。
prompt="""The following is a list of companies and the categories they fall into:
Apple, Facebook, Fedex
Apple
Category:
"""
ans = ask(prompt)
ans == """
Technology/Electronics
Facebook
Category:
Technology/Social Media
Fedex
Category:
Logistics/Shipping
"""
??可以看到,我們在保持輸入和上一小節(jié)一樣時,最終得到的效果也是一樣的。不過,在ChatGPT這里我們的提示詞還可以更加靈活、自然一些,如下所示。
prompt="""please output the category of the following companies:
Apple, Facebook, Fedex
The output format should be:
<company>
Category:
<category>
"""
ans = ask(prompt)
ans == """
Apple
Category:
Technology
Facebook
Category:
Technology/Social Media
Fedex
Category:
Delivery/Logistics
"""
??不錯,依然很好地完成了任務。可以看到,ChatCompletion接口比前面的Completion接口更加”聰明“一些,交互更加自然。看起來有點像它理解了我們給出的指令,然后完成了任務,而不僅僅是續(xù)寫。不過,值得說明的是,Completion接口其實也能支持一定的指令,它是ChatCompletion的早期版本,相關技術是一脈相承的。
??由于提示詞可以非常靈活,這就導致了不同的寫法可能會得到不一樣的效果。于是,很快就催生了一個新的技術方向:提示詞工程,我們這里給出一些常見的關于提示詞的寫法建議。
- 清晰,切忌復雜或歧義,如果有術語,應定義清楚。
- 具體,描述語言應盡量具體,不要抽象或模棱兩可。
- 聚焦,問題避免太泛或開放。
- 簡潔,避免不必要的描述。
- 相關,主要指主題相關,而且是在整個對話期間。
??新手要特別注意以下容易忽略的地方。
- 沒有說明具體的輸出目標。特殊場景除外(比如就是漫無目的閑聊)。
- 在一次對話中混合多個主題。
- 讓語言模型做數(shù)學題。語言模型不太擅長處理數(shù)學問題。
- 沒有給出想要什么的示例樣本。有時候你需要給出一些示例,它才能更加明白你的意圖。比如上一小節(jié)我們構造的那個關于和弦的實體提取的例子。或者一些更加不通用的例子,更加應該給出幾個示例。
- 反向提示。也就是一些反面例子,就是不要讓它不做什么。模型對這類任務不太擅長。
- 要求他一次只做一件事。新手很容易走向另一個極端——把一個任務拆的特別碎,一次只問模型一小步。這時候建議的做法是將步驟捆綁在一起一次說清。
??我們繼續(xù),來試一下情感分類的例子,如下所示。
prompt = """請給出下面句子的情感傾向,情感傾向包括三種:正向、中性、負向。
句子:我們是快樂的年輕人。
"""
ans = ask(prompt)
ans == "情感傾向:正向"
沒有問題,結果與預期一致。對于這種比較通用的任務,一般情況下它都可以完成的很好。
??再來做一下實體的例子。
prompt = """
請抽取給定Text中的實體,實體包括Company和People&Title,對于People,請同時給出它們的Title或role,跟在實體后面,用括號括起。
Text:
In March 1981, United States v. AT&T came to trial under Assistant Attorney General William Baxter. AT&T chairman Charles L. Brown thought the company would be gutted. He realized that AT&T would lose and, in December 1981, resumed negotiations with the Justice Department. Reaching an agreement less than a month later, Brown agreed to divestiture—the best and only realistic alternative. AT&T's decision allowed it to retain its research and manufacturing arms. The decree, titled the Modification of Final Judgment, was an adjustment of the Consent Decree of 14 January 1956. Judge Harold H. Greene was given the authority over the modified decree....
In 1982, the U.S. government announced that AT&T would cease to exist as a monopolistic entity. On 1 January 1984, it was split into seven smaller regional companies, Bell South, Bell Atlantic, NYNEX, American Information Technologies, Southwestern Bell, US West, and Pacific Telesis, to handle regional phone services in the U.S. AT&T retains control of its long distance services, but was no longer protected from competition.
"""
ans = ask(prompt)
ans == """
實體抽取結果:
- Company: AT&T, Bell South, Bell Atlantic, NYNEX, American Information Technologies, Southwestern Bell, US West, Pacific Telesis
- People&Title: William Baxter (Assistant Attorney General), Charles L. Brown (AT&T chairman), Judge Harold H. Greene.
"""
??看起來還行,而且值得注意的是,我們剛剛使用中英文混合的輸入。最后,我們試一下上一小節(jié)的另一個實體提取的例子。
prompt = """
根據(jù)下面的格式抽取給定Text中的和弦實體,實體必須包括“和弦”兩個字。
Desired format:
和弦:<用逗號隔開>
Text:
三和弦是由3個按照三度音程關系排列起來的一組音。大三和弦是大三度+小三度的純五度音,小三和弦是小三度+大三度的純五度音。
"""
ans = ask(prompt)
ans == "和弦:大三和弦, 小三和弦"
??這里,我們也用了中英文混合,結果完全沒問題。讀者不妨多多嘗試不同的提示詞,總的來說,它并沒有標準答案,更多的是一種實踐經(jīng)驗。
3.3 相關任務與應用
3.3.1 文檔問答:給定文檔問問題
??文檔問答和上一章的QA有點類似,不過要稍微復雜一點。它會先用QA的方法召回一個相關的文檔,然后讓模型在這個文檔中找出問題的答案。一般的流程還是先召回相關文檔,然后做閱讀理解任務。閱讀理解和實體提取任務有些類似,但它預測的不是具體某個標簽,而是答案在原始文檔中的位置索引,即開始和結束的位置。
??舉個例子,假設我們的問題是:“北京奧運會舉辦于哪一年?”召回的文檔可能是含有北京奧運會舉辦的新聞,比如在下面這個文檔。其中,“2008年”這個答案在文檔中的索引就是標注數(shù)據(jù)時要標注的內容。
第29屆夏季奧林匹克運動會(Beijing 2008; Games of the XXIX Olympiad),又稱2008年北京奧運會,2008年8月8日晚上8時整在中國首都北京開幕。8月24日閉幕。
??當然,一個文檔里可能有不止一個問題,比如上面的文檔,還可以問:“北京奧運會什么時候開幕?”,“北京奧運會什么時候閉幕”,“北京奧運會是第幾屆奧運會”等問題。
??根據(jù)之前的NLP方法,這個任務實際做起來方案會比較多,也有一定的復雜度,不過總體來說還是語義匹配和Token分類任務。現(xiàn)在我們有了大語言模型,問題就變得簡單了。依然是兩步,如下所示。
- 召回相關文檔:與上一章的QA類似,這次召回的不是問題,而是文檔。其實就是給定問題與一批文檔計算相似度,從中選出相似度最高的那個文檔。
- 基于給定文檔回答問題:將召回來的文檔和問題以提示詞的方式提交給大語言模型接口(比如上節(jié)介紹的
Completion和ChatCompletion),直接讓大模型幫忙得出答案。
??第一步我們已經(jīng)比較熟悉了,對于第二步,我們分別用兩種不同的接口各舉一例。首先看看Completion接口,如下所示。
import openai
import os
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
openai.api_key = OPENAI_API_KEY
def complete(prompt: str) -> str:
response = openai.Completion.create(
prompt=prompt,
temperature=0,
max_tokens=300,
top_p=1,
frequency_penalty=0,
presence_penalty=0,
model="text-davinci-003"
)
ans = response["choices"][0]["text"].strip(" \n")
return ans
??假設第一步已經(jīng)完成,我們已經(jīng)獲得了一篇文檔。注意,這個文檔一般都會比較長,所以提示詞也會比較長,如下所示。
# 來自官方示例
prompt = """Answer the question as truthfully as possible using the provided text, and if the answer is not contained within the text below, say "I don't know"
Context:
The men's high jump event at the 2020 Summer Olympics took place between 30 July and 1 August 2021 at the Olympic Stadium. 33 athletes from 24 nations competed; the total possible number depended on how many nations would use universality places to enter athletes in addition to the 32 qualifying through mark or ranking (no universality places were used in 2021). Italian athlete Gianmarco Tamberi along with Qatari athlete Mutaz Essa Barshim emerged as joint winners of the event following a tie between both of them as they cleared 2.37m. Both Tamberi and Barshim agreed to share the gold medal in a rare instance where the athletes of different nations had agreed to share the same medal in the history of Olympics. Barshim in particular was heard to ask a competition official "Can we have two golds?" in response to being offered a 'jump off'. Maksim Nedasekau of Belarus took bronze. The medals were the first ever in the men's high jump for Italy and Belarus, the first gold in the men's high jump for Italy and Qatar, and the third consecutive medal in the men's high jump for Qatar (all by Barshim). Barshim became only the second man to earn three medals in high jump, joining Patrik Sj?berg of Sweden (1984 to 1992).
Q: Who won the 2020 Summer Olympics men's high jump?
A:"""
ans = complete(prompt)
ans == "Gianmarco Tamberi and Mutaz Essa Barshim emerged as joint winners of the event."
??上面的Context就是我們召回的文檔。可以看到,接口很好地給出了答案。另外,需要說明的是,我們在構造提示詞時其實還給出了一些限制,主要包括兩點:第一,要求根據(jù)給定的文本盡量真實地回答問題;第二,如果答案未包含在給定文本中,就回復“我不知道”。這些都是為了盡量保證輸出結果的準確性,減少模型胡言亂語的可能性。
??接下來再看ChatCompletion接口,我們選擇一個中文的例子,如下所示。
prompt = """請根據(jù)以下Context回答問題,直接輸出答案即可,不用附帶任何上下文。
Context:
諾曼人(諾曼人:Nourmands;法語:Normands;拉丁語:Normanni)是在10世紀和11世紀將名字命名為法國諾曼底的人。他們是北歐人的后裔(丹麥人,挪威人和挪威人)的海盜和海盜,他們在首相羅洛(Rollo)的領導下向西弗朗西亞國王查理三世宣誓效忠。經(jīng)過幾代人的同化,并與法蘭克和羅馬高盧人本地居民融合,他們的后代將逐漸與以西卡羅來納州為基礎的加洛林人文化融合。諾曼人獨特的文化和種族身份最初出現(xiàn)于10世紀上半葉,并在隨后的幾個世紀中持續(xù)發(fā)展。
問題:
諾曼底在哪個國家/地區(qū)?
"""
def ask(content: str) -> str:
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": content}]
)
ans = response.get("choices")[0].get("message").get("content")
return ans
ans = ask(prompt)
ans == "法國。"
??看起來沒什么問題。下面就以Completion接口為例把兩個步驟串起來。
??我們使用OpenAI提供的數(shù)據(jù)集:來自維基百科的關于2020年東京奧運會的數(shù)據(jù)。數(shù)據(jù)集可以參考OpenAI的openai-cookbook GitHub倉庫examples/fine-tuned_qa/獲取。下載后是一個csv文件,和上一章一樣,先加載并查看數(shù)據(jù)集。
import pandas as pd
df = pd.read_csv("./dataset/olympics_sections_text.csv")
df.shape == (3964, 4)
df.head()
??數(shù)據(jù)如表3-1所示,第一列是頁面標題,第二列是頁面內章節(jié)標題,第三列是章節(jié)內容,最后一列是Token數(shù)。
表3-1 2020東京奧運會數(shù)據(jù)集樣例
| title | heading | content | tokens | |
|---|---|---|---|---|
| 0 | 2020 Summer Olympics | Summary | The 2020 Summer Olympics (Japanese: 2020年夏季オリン... | 726 |
| 1 | 2020 Summer Olympics | Host city selection | The International Olympic Committee (IOC) vote... | 126 |
| 2 | 2020 Summer Olympics | Impact of the COVID-19 pandemic | In January 2020, concerns were raised about th... | 374 |
| 3 | 2020 Summer Olympics | Qualifying event cancellation and postponement | Concerns about the pandemic began to affect qu... | 298 |
| 4 | 2020 Summer Olympics | Effect on doping tests | Mandatory doping tests were being severely res... | 163 |
- 第一步:對每個文檔計算Embedding。
- 第二步:存儲Embedding,同時存儲內容及其他需要的信息(如
heading)。 - 第三步:從存儲的地方檢索最相關的文檔。
- 第四步:基于最相關的文檔回答給定問題。
??第一步依然是借助OpenAI的Embedding接口,但是第二步我們這次不用Redis,換一個向量搜索工具:Qdrant。Qdrant相比Redis更簡單易用、容易擴展。不過,實際中我們還是要根據(jù)實際情況選擇工具,工具沒有好壞,適合的就是最好的。我們真正要做的是將業(yè)務邏輯抽象,做到盡量不依賴任何工具,換工具最多只需要換一個適配器。
??和Redis一樣,我們依然使用Docker啟動服務。
docker run -p 6333:6333 -v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant`
??同樣,也需要安裝Python客戶端。
$ pip install qdrant-client
??安裝好后就可以使用Python和qdrant進行交互了。首先是第一步,生成Embedding,使用OpenAI的get_embedding接口,或者直接使用原生的Embedding.create接口,可以支持批量請求。
from openai.embeddings_utils import get_embedding, cosine_similarity
def get_embedding_direct(inputs: list):
embed_model = "text-embedding-ada-002"
res = openai.Embedding.create(
input=inputs, engine=embed_model
)
return res
??準備好數(shù)據(jù)后,批量獲取Embedding。
texts = [v.content for v in df.itertuples()]
len(texts) == 3964
import pnlp
emds = []
for idx, batch in enumerate(pnlp.generate_batches_by_size(texts, 200)):
response = get_embedding_direct(batch)
for v in response.data:
emds.append(v.embedding)
print(f"batch: {idx} done")
len(emds), len(emds[0]) == (3964, 1536)
??上面generate_batches_by_size方法可以將一個可迭代的對象(此處是列表)拆成批大小為200的多個批次。一次接口調用可以獲取200個文檔的Embedding表示。
??然后就是第二步,創(chuàng)建索引并入庫。在此之前先創(chuàng)建客戶端,如下所示。
from qdrant_client import QdrantClient
client = QdrantClient(host="localhost", port=6333)
??值得一提的是,qdrant還支持內存和文件庫,也就是說,可以直接將Embedding放在內存或硬盤里。
# client = QdrantClient(":memory:")
# 或
# client = QdrantClient(path="path/to/db")
??創(chuàng)建索引和Redis類似,不過在qdrant中是collection,如下所示。
from qdrant_client.models import Distance, VectorParams
client.recreate_collection(
collection_name="doc_qa",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)
??如果成功會返回True。刪除一個collection可以用下面的命令。
client.delete_collection("doc_qa")
??下面是把向量入庫代碼。
payload=[
{"content": v.content, "heading": v.heading, "title": v.title, "tokens": v.tokens} for v in df.itertuples()
]
client.upload_collection(
collection_name="doc_qa",
vectors=emds,
payload=payload
)
??接下來就是第三步,檢索相關文檔。這里會比Redis簡單很多,不需要構造復雜的查詢語句。
query = "Who won the 2020 Summer Olympics men's high jump?"
query_vector = get_embedding(query, engine="text-embedding-ada-002")
hits = client.search(
collection_name="doc_qa",
query_vector=query_vector,
limit=5
)
???獲取到5個最相關的文檔,第一個樣例如下所示。
ScoredPoint(id=236, version=3, score=0.90316474, payload={'content': '<CONTENT>', 'heading': 'Summary', 'title': "Athletics at the 2020 Summer Olympics – Men's high jump", 'tokens': 275}, vector=None)
??由于篇幅關系,我們把content給省略了,payload就是之前存進去的信息,我們可以在里面存儲需要的任何信息。score是相似度得分,表示給定的query和向量庫中存儲的文檔的相似度。
??接下來將這個過程和提示詞的構建合并在一起。
# 參考自官方示例
# 上下文的最大長度
MAX_SECTION_LEN = 500
# 召回多個文檔時,文檔與文檔之間的分隔符
SEPARATOR = "\n* "
separator_len = 3
def construct_prompt(question: str) -> str:
query_vector = get_embedding(question, engine="text-embedding-ada-002")
hits = client.search(
collection_name="doc_qa",
query_vector=query_vector,
limit=5
)
choose = []
length = 0
indexes = []
for hit in hits:
doc = hit.payload
length += doc["tokens"] + separator_len
if length > MAX_SECTION_LEN:
break
choose.append(SEPARATOR + doc["content"].replace("\n", " "))
indexes.append(doc["title"] + doc["heading"])
# 簡單的日志
print(f"Selected {len(choose)} document sections:")
print("\n".join(indexes))
header = """Answer the question as truthfully as possible using the provided context, and if the answer is not contained within the text below, say "I don't know."\n\nContext:\n"""
return header + "".join(choose) + "\n\n Q: " + question + "\n A:"
??來個例子試驗一下。
prompt = construct_prompt("Who won the 2020 Summer Olympics men's high jump?")
print("===\n", prompt)
"""
Selected 2 document sections:
Athletics at the 2020 Summer Olympics – Men's high jumpSummary
Athletics at the 2020 Summer Olympics – Men's long jumpSummary
===
Answer the question as truthfully as possible using the provided context, and if the answer is not contained within the text below, say "I don't know."
Context:
* <CONTENT 1>
* <CONTENT 2>
Q: Who won the 2020 Summer Olympics men's high jump?
A:
"""
??結果如上所示(考慮到篇幅關系,部分內容省略)。對于找到的5個相關文檔,由于有了長度限制(500),這里只使用了前2個。
??構造好提示詞后就是最后一步,基于給定文檔回答問題。
def complete(prompt: str) -> str:
response = openai.Completion.create(
prompt=prompt,
temperature=0,
max_tokens=300,
top_p=1,
frequency_penalty=0,
presence_penalty=0,
model="text-davinci-003"
)
ans = response["choices"][0]["text"].strip(" \n")
return ans
ans = complete(prompt)
ans == "Gianmarco Tamberi and Mutaz Essa Barshim emerged as joint winners of the event following a tie between both of them as they cleared 2.37m. Both Tamberi and Barshim agreed to share the gold medal."
??再試試ChatCompletion接口。
def ask(content: str) -> str:
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": content}]
)
ans = response.get("choices")[0].get("message").get("content")
return ans
ans = ask(prompt)
ans == "Gianmarco Tamberi and Mutaz Essa Barshim shared the gold medal in the men's high jump event at the 2020 Summer Olympics."
??可以看到,兩個接口都準確地回答了問題。下面我們再多看幾個例子。
query = "In the 2020 Summer Olympics, how many gold medals did the country which won the most medals win?"
prompt = construct_prompt(query)
answer = complete(prompt)
print(f"\nQ: {query}\nA: {answer}")
"""
Selected 2 document sections:
2020 Summer Olympics medal tableSummary
List of 2020 Summer Olympics medal winnersSummary
Q: In the 2020 Summer Olympics, how many gold medals did the country which won the most medals win?
A: The United States won the most medals overall, with 113, and the most gold medals, with 39.
"""
answer = ask(prompt)
print(f"\nQ: {query}\nA: {answer}")
"""
Q: In the 2020 Summer Olympics, how many gold medals did the country which won the most medals win?
A: The country that won the most medals at the 2020 Summer Olympics was the United States, with 113 medals, including 39 gold medals.
"""
??上面的問題是:”在2020年夏季奧運會上,獲得獎牌最多的國家獲得了多少枚金牌?“我們分別用兩個接口給出答案,結果差不多,但后者更具體一些。
query = "What is the tallest mountain in the world?"
prompt = construct_prompt(query)
answer = complete(prompt)
print(f"\nQ: {query}\nA: {answer}")
"""
Selected 3 document sections:
Sport climbing at the 2020 Summer Olympics – Men's combinedRoute-setting
Ski mountaineering at the 2020 Winter Youth Olympics – Boys' individualSummary
Ski mountaineering at the 2020 Winter Youth Olympics – Girls' individualSummary
Q: What is the tallest mountain in the world?
A: I don't know.
"""
answer = ask(prompt)
print(f"\nQ: {query}\nA: {answer}")
"""
Q: What is the tallest mountain in the world?
A: I don't know.
"""
??上面這個問題是“世界上最高的山是什么?”這個問題依然可以召回3個文檔,但其中并不包含答案。兩個接口都可以很好地按我們預設的要求回復。
??文檔問答是一個非常適合大模型的應用,它充分利用了大模型強大的理解能力。同時由于每個問題都有相關的文檔作為基礎,又最大限度地降低了模型胡亂發(fā)揮的可能性。而且,根據(jù)筆者實驗情況來看,這樣的用法即使在零樣本、不微調的情況下效果也不錯。讀者如果恰好有類似場景,不妨試試本方案。
3.3.2 模型微調:滿足個性化需要
??前面我們已經(jīng)介紹了各種分類和實體提取的用法。本節(jié)將介紹如何在自己的數(shù)據(jù)上進行微調,我們以主題分類任務為例。主題分類,簡單來說就是給定文本,判斷其屬于哪一類主題。
??我們使用今日頭條中文新聞分類數(shù)據(jù)集,該數(shù)據(jù)集共15個類別,分別為:科技、金融、娛樂、世界、汽車、運動、文化、軍事、旅游、游戲、教育、農(nóng)業(yè)、房產(chǎn)、社會、股票。
import pnlp
lines = pnlp.read_file_to_list_dict("./dataset/tnews.json")
len(lines) == 10000
??先讀取數(shù)據(jù)集,其中一條樣例數(shù)據(jù)如下所示。
lines[59] == {
"label": "101",
"label_desc": "news_culture",
"sentence": "上聯(lián):銀笛吹開云天月,下聯(lián)怎么對?",
"keywords": ""
}
??其中,label和label_desc分別是標簽ID和標簽描述,sentence是句子文本,keywords是關鍵詞,有可能為空(如上所示)。我們先看下標簽的分布情況。
from collections import Counter
ct = Counter([v["label_desc"] for v in lines])
ct.most_common() == [
('news_tech', 1089),
('news_finance', 956),
('news_entertainment', 910),
('news_world', 905),
('news_car', 791),
('news_sports', 767),
('news_culture', 736),
('news_military', 716),
('news_travel', 693),
('news_game', 659),
('news_edu', 646),
('news_agriculture', 494),
('news_house', 378),
('news_story', 215),
('news_stock', 45)
]
??根據(jù)統(tǒng)計情況,我們發(fā)現(xiàn)stock這個類別有點少。實際上,真實場景中,大部分時候各個標簽都是不均勻的。如果標簽很少的類型是我們要關注的,那就盡量再增加一些數(shù)據(jù)。否則,可以不做額外處理。
??我們用上面介紹過的接口來完成一下任務,先構建提示詞,如下所示。
def get_prompt(text: str) -> str:
prompt = f"""對給定文本進行分類,類別包括:科技、金融、娛樂、世界、汽車、運動、文化、軍事、旅游、游戲、教育、農(nóng)業(yè)、房產(chǎn)、社會、股票。
給定文本:
{text}
類別:
"""
return prompt
prompt = get_prompt(lines[0]["sentence"])
print(prompt)
"""
對給定文本進行分類,類別包括:科技、金融、娛樂、世界、汽車、運動、文化、軍事、旅游、游戲、教育、農(nóng)業(yè)、房產(chǎn)、社會、股票。
給定文本:
上聯(lián):銀笛吹開云天月,下聯(lián)怎么對?
類別:
"""
??這個提示詞就是把sentence當做給定文本,然后要求模型輸出對應的類別。注意,這些類別應該提供給模型。然后就是調用接口完成任務了。
import openai
import os
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
openai.api_key = OPENAI_API_KEY
def complete(prompt: str) -> str:
response = openai.Completion.create(
prompt=prompt,
temperature=0,
max_tokens=10,
top_p=1,
frequency_penalty=0,
presence_penalty=0,
model="text-davinci-003"
)
ans = response["choices"][0]["text"].strip(" \n")
return ans
def ask(content: str) -> str:
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": content}],
temperature=0,
max_tokens=10,
top_p=1,
frequency_penalty=0,
presence_penalty=0,
)
ans = response.get("choices")[0].get("message").get("content")
return ans
ans = complete(prompt)
ans == "文化"
ans = ask(prompt)
ans == "文化"
??可以看到,兩個接口均很好地完成了我們給的任務。我們再看一個識別不太理想的例子,數(shù)據(jù)如下所示。
lines[2] == {
"label": "104",
"label_desc": "news_finance",
"sentence": "出欄一頭豬虧損300元,究竟誰能笑到最后!",
"keywords": "商品豬,養(yǎng)豬,豬價,仔豬,飼料"
}
prompt = get_prompt(lines[2]["sentence"])
complete(prompt) == "社會"
ask(prompt) == "農(nóng)業(yè)"
??這個有點分歧,如果我們人工分析這句話,感覺“農(nóng)業(yè)”這個類別可能看起來更合適一些。不過,很遺憾,數(shù)據(jù)給出的標簽是“金融”。這種情況在實際場景中也比較常見,我們一般可以用下面的手段來解決。
- 少樣本。可以每次隨機從訓練數(shù)據(jù)集里抽幾條樣本(包括句子和標簽)出來作為提示詞的一部分。
- 微調。把我們自己的數(shù)據(jù)集按指定格式準備好,提交給微調接口,讓它幫我們微調一個在我們給的數(shù)據(jù)集上學習過的模型。
??少樣本最關鍵的是如何找到這個“樣本”,換句話說,我們拿什么樣例給模型當做參考樣本。對于類別標簽比較多的情況(實際工作場景中,成百上千種標簽是很常見的),即使每個標簽一個例子,這上下文長度也比較難以接受。這時候少樣本方案就有點不太方便了。當然,如果我們非要用也不是不行,還是最常用的策略:先召回幾個相似句,然后把相似句的內容和標簽作為少樣本的例子,讓接口來預測給定句子的類別。不過這樣做的話,和直接用QA方法做差不多了。
??此時,更好的方法就是在我們的數(shù)據(jù)集上微調模型,簡單來說就是讓模型“熟悉”我們獨特的數(shù)據(jù),進而讓其具備在類似數(shù)據(jù)上正確識別出相應標簽的能力。
??接下來,就讓我們看看具體怎么做,一般包括三個主要步驟。
- 第一步:準備數(shù)據(jù)。按接口要求的格式把數(shù)據(jù)準備好,這里的數(shù)據(jù)就是我們自己的數(shù)據(jù)集,至少包含一段文本和一個類別。
- 第二步:微調。使用微調接口將處理好的數(shù)據(jù)傳遞過去,由服務器自動完成微調,微調完成后可以得到一個新的模型ID。注意,這個模型ID只屬于你自己,不要將它公開給其他人。
- 第三步:使用新模型推理。這個很簡單,把原來接口里的
model參數(shù)內容換成剛剛得到的模型ID即可。
??注意,本書只介紹通過接口微調。接下來我們就來微調這個主題多分類模型,為了快速驗證結果,我們只取后500條數(shù)據(jù)作為訓練集。
import pandas as pd
train_lines = lines[-500:]
train = pd.DataFrame(train_lines)
train.shape == (500, 4)
train.head(3) # 只看前3條
??數(shù)據(jù)樣例如表3-2所示,各列的含義之前已經(jīng)解釋過了,此處不再贅述。需要說明的是,關鍵詞有點類似標簽,它們并不一定會出現(xiàn)在原文中。
表3-2 主題分類微調數(shù)據(jù)集樣例
| label | label_desc | sentence | keywords | |
|---|---|---|---|---|
| 0 | 103 | news_sports | 為什么斯凱奇與阿迪達斯腳感很相似,價格卻差了近一倍? | 達斯勒,阿迪達斯,FOAM,BOOST,斯凱奇 |
| 1 | 100 | news_story | 女兒日漸消瘦,父母發(fā)現(xiàn)有怪物,每天吃女兒一遍 | 大將軍,怪物 |
| 2 | 104 | news_finance | 另類逼空確認反彈,劍指3200點以上 | 股票,另類逼空,金融,創(chuàng)業(yè)板,快速放大 |
train.label_desc.value_counts()
"""
news_finance 48
news_tech 47
news_game 46
news_entertainment 46
news_travel 44
news_sports 42
news_military 40
news_world 38
news_car 36
news_culture 35
news_edu 27
news_agriculture 20
news_house 19
news_story 12
Name: label_desc, dtype: int64
"""
??需要說明的是,實際運行時,由于股票數(shù)據(jù)量太少,我們把這個類別去掉了,但是不影響整個流程。
??先是第一步,準備數(shù)據(jù)。要保證數(shù)據(jù)有兩列,分別是prompt和completion。當然不同服務商提供的接口可能不完全一樣,我們這里以OpenAI的接口為例。
df_train = train[["sentence", "label_desc"]]
df_train.columns = ["prompt", "completion"]
df_train.head()
??構造好的訓練數(shù)據(jù)樣例如表3-3所示。
表3-3 主題分類微調訓練數(shù)據(jù)樣例
| prompt | completion | |
|---|---|---|
| 0 | 為什么斯凱奇與阿迪達斯腳感很相似,價格卻差了近一倍? | news_sports |
| 1 | 女兒日漸消瘦,父母發(fā)現(xiàn)有怪物,每天吃女兒一遍 | news_story |
| 2 | 另類逼空確認反彈,劍指3200點以上 | news_finance |
| 3 | 老公在聚會上讓我向他的上司敬酒,現(xiàn)在老公哭了,我笑了 | news_story |
| 4 | 女孩上初中之后成績下降,如何才能提升成績? | news_edu |
df_train.to_json("dataset/tnews-finetuning.jsonl", orient="records", lines=True)
!openai tools fine_tunes.prepare_data -f dataset/tnews-finetuning.jsonl -q
??轉換后的數(shù)據(jù)樣例如下所示。
!head dataset/tnews-finetuning_prepared_train.jsonl
"""
{"prompt":"cf生存特訓:火箭彈狂野復仇,為兄弟報仇就要不死不休 ->","completion":" game"}
{"prompt":"哈爾濱 東北抗日聯(lián)軍博物館 ->","completion":" culture"}
{"prompt":"中國股市中,莊家為何如此猖獗?一文告訴你真相 ->","completion":" finance"}
{"prompt":"天府錦繡又重來 ->","completion":" agriculture"}
{"prompt":"生活,游戲,電影中有哪些詞匯稍加修改便可以成為一個非常霸氣的名字? ->","completion":" game"}
{"prompt":"法庭上,生父要爭奪孩子撫養(yǎng)權,小男孩的發(fā)言讓生父當場啞口無言 ->","completion":" entertainment"}
{"prompt":"如何才能選到好的深圳大數(shù)據(jù)培訓機構? ->","completion":" edu"}
{"prompt":"有哪些娛樂圈里面的明星追星? ->","completion":" entertainment"}
{"prompt":"東塢原生態(tài)野生茶 ->","completion":" culture"}
{"prompt":"亞冠:恒大不勝早有預示,全北失利命中注定 ->","completion":" sports"}
"""
??可以看到,轉換后最明顯的是給我們每一個prompt后面加了個 ->標記,除此之外還有下面一些調整。
- 小寫:將所有文本小寫,這個主要是英文,中文沒有這個說法。
- 去除標簽
news_前綴:注意看completion的字段值,前綴都不見了,處理后的結果是一個有意義的單詞,會更加合理。 - 在
completion字段值前面加空格:除了去掉前綴,還額外加了個空格。這也是英文下獨有的(英文用空格把單詞分開)。 - 切分為訓練和驗證集:訓練集用來微調模型,驗證集則用來評估模型性能和進行超參數(shù)調優(yōu)。
??這些調整會有相應的日志輸出,讀者注意閱讀轉換時的輸出日志。另外,它們也都是常見的、推薦的預處理做法。
??數(shù)據(jù)準備好后就到了第二步:微調。使用接口微調非常簡單,一般都是一行命令完成,或者甚至在頁面上用鼠標點一下就可以了。
import openai
import os
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
openai.api_key = OPENAI_API_KEY
!openai api fine_tunes.create \
-t "./dataset/tnews-finetuning_prepared_train.jsonl" \
-v "./dataset/tnews-finetuning_prepared_valid.jsonl" \
--compute_classification_metrics --classification_n_classes 14 \
-m davinci\
--no_check_if_files_exist
??其中,-t和-v分別指定訓練集和驗證集。接下來那行用來計算指標。-m則用來指定要微調的模型,可以微調的模型和價格可以在官方文檔獲取。最后一行是檢查文件是否存在,如果之前上傳過文件的話,這里可以復用。
??另外,值得一提的是目前只能只能微調Completion接口,ChatCompletion不支持微調。命令執(zhí)行后會得到一個任務ID,接下來可以用另一個接口和任務ID來獲取任務的實時狀態(tài),如下所示。
!openai api fine_tunes.get -i ft-QOkrWkHU0aleR6f5IQw1UpVL
??或者用下面的接口恢復數(shù)據(jù)流。
!openai api fine_tunes.follow -i ft-QOkrWkHU0aleR6f5IQw1UpVL
??注意,一個是follow,另一個是get。讀者可以通過openai api --help查看更多支持的命令。
??建議讀者過段時間通過get接口查看一下進度即可,不需要一直調用follow接口獲取數(shù)據(jù)流。這里可能要等一段時間,等排隊完成后進入訓練階段就很快了。查看進度時,主要看status是什么狀態(tài)。微調結束后,會獲取到一個新的模型ID,那就是我們此次調整后的模型。另外,我們也可以通過下面的命令查看本次微調的各項指標。
# -i 就是上面微調的任務ID
!openai api fine_tunes.results -i ft-QOkrWkHU0aleR6f5IQw1UpVL > metric.csv
metric = pd.read_csv('metric.csv')
metric[metric['classification/accuracy'].notnull()].tail(1)
??這里主要會輸出訓練后的損失、精度等。我們將精度繪制成圖,如圖3-1所示。
step_acc = metric[metric['classification/accuracy'].notnull()]['classification/accuracy']
import matplotlib.pyplot as plt
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10 ,6))
ax.plot(step_acc.index, step_acc.values, "k-", lw=1, alpha=1.0)
ax.set_xlabel("Step")
ax.set_ylabel("Accuracy");

圖3-1 微調精度
??這個精度其實是非常一般的,最高值在1200步(Step)左右,精度(Accuracy)達到64%左右。應該是我們給的語料太少的緣故。實際中,往往數(shù)據(jù)越多、數(shù)據(jù)質量越好,相應的效果也會越好。
??第三步:使用新模型推理。我們還是用剛剛的例子演示。
lines[2] == {
"label": "104",
"label_desc": "news_finance",
"sentence": "出欄一頭豬虧損300元,究竟誰能笑到最后!",
"keywords": "商品豬,養(yǎng)豬,豬價,仔豬,飼料"
}
prompt = get_prompt(lines[2]["sentence"])
prompt == """
對給定文本進行分類,類別包括:科技、金融、娛樂、世界、汽車、運動、文化、軍事、旅游、游戲、教育、農(nóng)業(yè)、房產(chǎn)、社會、股票。
給定文本:
出欄一頭豬虧損300元,究竟誰能笑到最后!
類別:
"""
??調用接口的代碼稍微調整一下,新增一個model參數(shù)。
def complete(prompt: str, model: str, max_tokens: int) -> str:
response = openai.Completion.create(
prompt=prompt,
temperature=0,
max_tokens=max_tokens,
top_p=1,
frequency_penalty=0,
presence_penalty=0,
model=model
)
ans = response["choices"][0]["text"].strip(" \n")
return ans
??調用微調前的模型和本節(jié)前面一樣,但是調用微調后的模型時,需要注意修改提示詞,如下所示。
# 原來微調之前的模型
complete(prompt, "text-davinci-003", 5) == "社會"
# 微調后的
prompt = lines[2]["sentence"] + " ->"
complete(prompt, "davinci:ft-personal-2023-04-04-14-51-29", 1) == "agriculture"
??微調后的模型返回了一個英文單詞,這是正常的,因為我們微調數(shù)據(jù)中的completion就是英文。我們這里是為了方便演示微調有效,讀者在實際使用時務必要統(tǒng)一。不過這個結果依然不是標注的“finance”,應該是這個句子本身和“agriculture”這個類別的訓練文本更加接近。對于這類比較特殊的樣例,務必要給模型提供一定數(shù)量類似的訓練樣本。
??上面我們介紹了主題分類的微調。實體抽取的微調也是類似的,它推薦的輸入格式如下。
{
"prompt":"<any text, for example news article>\n\n###\n\n",
"completion":" <list of entities, separated by a newline> END"
}
??示例如下所示。
{
"prompt":"Portugal will be removed from the UK's green travel list from Tuesday, amid rising coronavirus cases and concern over a \"Nepal mutation of the so-called Indian variant\". It will join the amber list, meaning holidaymakers should not visit and returnees must isolate for 10 days...\n\n###\n\n",
"completion":" Portugal\nUK\nNepal mutation\nIndian variant END"
}
??相信讀者應該很容易理解,不妨對一些專業(yè)領域的實體進行微調,對比一下微調前后的效果。
3.3.3 智能對話:大語言模型=自主控制的機器人
??智能對話,有時候也叫智能客服、對話機器人、聊天機器人等等。總之就是和用戶通過聊天方式進行交互的一種技術。傳統(tǒng)的對話機器人一般包括三個大的模塊。
- 自然語言理解模塊(NLU):負責對用戶輸入進行理解。我們在本章一開始已經(jīng)提到了,主要就是意圖分類+實體識別這兩種技術。實際中可能還有實體關系抽取、情感識別等組件。
- 對話管理模塊(dialogue management,DM):就是在獲得NLU的結果后,如何確定機器人的回復。也就是對話方向的控制。
- 回復生成模塊(natural language generation,NLG):就是生成最終要回復給用戶的輸出文本。
??對話機器人一般包括三種,不同類型的技術方案側重有所不同。
- 任務型機器人。主要用來完成特定的任務,比如訂機票、訂餐等,這一類機器人最關鍵的是要獲取完成任務所需的各種信息(專業(yè)術語叫:槽位)。整個對話過程其實可以看做是一個填槽過程,通過與用戶不斷對話獲取到需要的槽位信息。比如訂餐這個任務,就餐人數(shù)、就餐時間、聯(lián)系人電話等就是基本信息,機器人就要想辦法獲取到這些信息。這里NLU就是重頭,DM一般使用兩種方法:模型控制或流程圖控制。前者通過模型自動學習來實現(xiàn)流轉,后者則根據(jù)意圖類型進行流轉控制。
- 問答型機器人。主要用來回復用戶問題,和上一章介紹的QA有點類似,平時我們常見的客服機器人往往是這種類型。它們更重要的是問題匹配,DM相對弱一些。
- 閑聊型機器人。閑聊機器人一般沒什么實際作用。當然,也有一種情感陪伴型機器人,不在我們討論范圍內。
??以上是大致的分類,但真實場景中的對話機器人往往是多種功能的結合體。更加適合從主動發(fā)起/被動接受這個角度來劃分。
- 主動發(fā)起對話的機器人。一般是以外呼的方式進行,營銷、催款、通知等都是常見的場景。這種對話機器人一般不閑聊,電話費不允許。它們基本都是帶著特定任務或目的走流程,流程走完就掛斷結束。與用戶的互動更多是以QA的形式完成,因為主動權在機器人手里,所以流程一般都是固定控制的,甚至QA的數(shù)量、回答次數(shù)也會控制。
- 被動接受對話的機器人。一般是以網(wǎng)頁或客戶端的形式存在,絕大部分時候都是用戶找過來了,比如大部分公司網(wǎng)站或應用首頁的“智能客服”就是類似功能。它們以QA為主,輔以閑聊。稍微復雜點的是上面提到的任務型機器人,需要不斷收集槽位信息。
??大模型時代,智能對話機器人會有什么新變化嗎?接下來,我們探討一下這方面內容。
??首先,可以肯定的是類似ChatGPT這樣的大模型極大的擴展了對話機器人的邊界,大模型強大的In-Context能力不僅讓使用更加簡單(我們只需把歷史對話分角色放進去就好了),而且效果也更好。除了閑聊,問答型和任務型機器人它也可以很擅長,交互更加人性化。
??我們具體來展開說說它可以做什么以及怎么做,隨便舉幾個例子。
- 作為問答類產(chǎn)品,比如知識問答、情感咨詢、心理咨詢等等,完全稱得上諸事不決ChatGPT。比如問它編程概念,問它如何追求心儀的女孩子,問它怎么避免焦慮等等。它的大部分回答都能讓人眼前一亮。
- 作為智能客服,通過與企業(yè)知識庫結合勝任客服工作。相比之前的QA類的客服,它回答會更加個性化,效果也更好。
- 作為智能營銷機器人。智能客服更加偏向被動、為用戶答疑解惑的方向。營銷機器人則更加主動一些,它會根據(jù)已存儲的用戶信息,主動向用戶推薦相關產(chǎn)品,根據(jù)預設的目標向用戶發(fā)起對話。還可以同時負責維護客戶關系。
- 作為游戲中非玩家角色(non-player character,NPC)、陪聊機器人等休閑娛樂類產(chǎn)品。
- 作為教育、培訓的導師,可以進行一對一教學,尤其適合語言、編程類學習。
??這些都是它確定可以做的,市面上也有很多相關的應用了。為什么大模型能做這些?歸根結底還是其大規(guī)模參數(shù)所學到的知識和具備的理解力。尤其是強大的理解力,應該是決定性的(只有知識就是Google搜索引擎)。
??當然,并不是什么都要ChatGPT,我們要避免”手里有錘子,到處找釘子“的思維方式。某位哲人說過,一項新技術的出現(xiàn),短期內總是被高估,長期內總是被低估。ChatGPT引領的大模型是劃時代的,但也不意味著什么都要ChatGPT一下。比如,某些分類和實體抽取任務,之前的方法已經(jīng)能達到非常好的效果,這時候就完全不需要替換。我們知道很多實際任務它并不會隨著技術發(fā)展有太多變化,比如分類任務,難道出來個新技術,分類任務就不是分類任務了嗎。技術的更新會讓我們的效率得到提升,也就是說做同樣的任務更加簡單和高效了,可以做更難的任務了,但不等于任務也會發(fā)生變化。所以,一定要理清楚這里面的關鍵,明白手段和目的的區(qū)別。
??不過如果是新開發(fā)一個服務,或者不了解這方面的專業(yè)知識,那使用大模型接口反而可能是更好的策略。不過,實際上線前還是應該考慮清楚各種細節(jié),比如服務不可用怎么辦,并發(fā)大概多少,時延要求多少,用戶規(guī)模大概多少等等。我們技術方案的選型是和公司或自己的需求息息相關的,沒有絕對好的方案,只有當下是否適合的方案。
??同時,要盡可能多考慮幾步,但也不用太多(過度優(yōu)化是原罪)。比如日活只有不到幾百,上來就寫個分布式的設計方案就有點不合適。不過這并不妨礙我們在代碼和架構設計時考慮擴展性,比如數(shù)據(jù)庫,我們可能用SQLite,但代碼里并不直接和它耦合死,而是使用能同時支持其他數(shù)據(jù)庫、甚至分布式數(shù)據(jù)庫的ORM工具。這樣雖然寫起來稍微麻煩了一點點,但代碼會更加清晰,而且和可能會變化的東西解耦了。這樣即便日后規(guī)模增加了,數(shù)據(jù)庫可以隨便換,代碼基本不用更改。
??最后,我們也應該了解ChatGPT的一些局限,除了它本身的局限(后面有專門章節(jié)介紹),在工程上至少應該始終關注下面幾個話題:響應時間和穩(wěn)定性、并發(fā)和橫向可擴展性、可維護性和迭代、成本。只有當這些都能滿足我們的期望時,才應該選擇該方案。
??下面,我們使用ChatGPT來實現(xiàn)一個簡單的任務型對話機器人。設計階段需要考慮以下一些因素。
- 使用目的。首先,我們需要明確使用目的是什么,如上所言,不同的用途要考慮的因素也不一樣。簡單(但很實際)起見,我們以一個”訂餐機器人“為例。功能就是簡單的開場白,然后獲取用戶聯(lián)系方式、訂餐人數(shù)、用餐時間三個信息。
- 如何使用。使用也比較簡單,主要利用ChatGPT的多輪對話能力,這里的重點是控制上下文。不過由于任務簡單,我們不用對歷史記錄做召回再進行對話,直接在每一輪時把已經(jīng)獲取的信息告訴它,同時讓它繼續(xù)獲取其他信息,直到所有信息獲取完畢為止。另外,我們可以限制一下輸出Token的數(shù)量(控制輸出文本的長度)。
- 消息查詢、存儲。對于用戶的消息(以及機器人的回復),實際中往往需要存儲起來,用來做每一輪回復的歷史消息召回。而且這個日后還可能有其他用途,比如使用對話記錄對用戶進行畫像,或者當做訓練數(shù)據(jù)等等。存儲可以直接放到數(shù)據(jù)庫,或傳到類似ElasticSearch這樣的內部搜索引擎中。
- 消息解析。消息的解析可以實時進行(并不一定要用ChatGPT)或離線進行,本案例我們需要實時在線解析。這個過程我們可以讓ChatGPT在生成回復時順便一起完成。
- 實時干預。實時干預是應該要關注的,或者需要設計這樣的模塊。一方面是有時候即便做了限制,依然有可能被某些問法問到不太合適的答復;另一方面也不能排除部分惡意用戶對機器人進行攻擊。因此最好有干預機制的設計。這里,我們設計一個簡單策略:檢測用戶是否提問敏感類問題,如果發(fā)現(xiàn)此類問題直接返回預先設定好的文本,不再調用ChatGPT進行對話回復。
- 更新策略。更新策略主要是對企業(yè)知識庫的更新,這里由于我們使用的是In-Context能力,所以并不需要調整ChatGPT,可能需要調整Embedding接口。此案例暫不涉及。
??綜上,我們需要先對用戶輸入進行敏感性檢查,確認沒問題后開始對話。同時應存儲用戶消息,并在每輪對話時將用戶歷史消息傳遞給接口。
??先看一下敏感性檢查,這個接口比較多,國內很多廠商都有提供,OpenAI提供了一個相關的接口。這個接口本身是和對話無關的,我們以OpenAI的接口為例。
import openai
import os
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
openai.api_key = OPENAI_API_KEY
import requests
def check_risk(inp: str) -> bool:
safe_api = "https://api.openai.com/v1/moderations"
resp = requests.post(safe_api, json={"input": inp}, headers={"Authorization": f"Bearer {OPENAI_API_KEY}"})
data = resp.json()
return data["results"][0]["flagged"]
check_risk("good") == False
??接下來我們考慮如何構造接口的輸入,這里有兩個事情要做。第一,查詢歷史對話記錄作為上下文,簡單起見我們可以只考慮上一輪或把所有記錄都給它。由于對話輪次較少,我們采用后者。第二,計算輸入的Token數(shù),根據(jù)模型能接受最大Token長度和想輸出的最大長度,反推上下文的最大長度,并對歷史對話進行處理(如截斷)。確定好這個策略后,我們來設計數(shù)據(jù)結構,如下所示。
from dataclasses import dataclass, asdict
from typing import List, Dict
from datetime import datetime
import uuid
import json
import re
from sqlalchemy import insert
@dataclass
class User:
user_id: str
user_name: str
@dataclass
class ChatSession:
user_id: str
session_id: str
cellphone: str
people_number: int
meal_time: str
chat_at: datetime
@dataclass
class ChatRecord:
user_id: str
session_id: str
user_input: str
bot_output: str
chat_at: datetime
??上面除了用戶外,我們設計了兩個簡單的數(shù)據(jù)結構,一個是聊天信息,一個是聊天記錄,前者記錄聊天基本信息,后者記錄聊天記錄。其中,session_id主要用來區(qū)分每一次對話,當用戶點擊產(chǎn)品頁面的”開始對話“之類的按鈕后,就生成一個session_id;在下次對話時再生成一個新的。
??接下來,我們處理核心對話邏輯,這一塊主要是利用ChatGPT的能力,明確要求,把每一輪對話都喂給它。給出響應。
def ask(msg):
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
temperature=0.2,
max_tokens=100,
top_p=1,
frequency_penalty=0,
presence_penalty=0,
messages=msg
)
ans = response.get("choices")[0].get("message").get("content")
return ans
??然后就是把整個流程串起來。
class Chatbot:
def __init__(self):
self.system_inp = """現(xiàn)在你是一個訂餐機器人(角色是assistant),你的目的是向用戶獲取手機號碼、用餐人數(shù)量和用餐時間三個信息。你可以自由回復用戶消息,但牢記你的目的。每一輪你需要輸出給用戶的回復,以及獲取到的信息,信息應該以JSON方式存儲,包括三個key:cellphone表示手機號碼,people_number表示用餐人數(shù),meal_time表示用餐時間儲。
回復格式:
給用戶的回復:{回復給用戶的話}
獲取到的信息:{"cellphone": null, "people_number": null, "meal_time": null}
"""
self.max_round = 10
self.slot_labels = ["meal_time", "people_number", "cellphone"]
self.reg_msg = re.compile(r"\n+")
def check_over(self, slot_dict: dict):
for label in self.slot_labels:
if slot_dict.get(label) is None:
return False
return True
def send_msg(self, msg: str):
print(f"機器人:{msg}")
def chat(self, user_id: str):
sess_id = uuid.uuid4().hex
chat_at = datetime.now()
msg = [
{"role": "system", "content": self.system_inp},
]
n_round = 0
history = []
while True:
if n_round > self.max_round:
bot_msg = "非常感謝您對我們的支持,再見。"
self.send_msg(bot_msg)
break
try:
bot_inp = ask(msg)
except Exception as e:
bot_msg = "機器人出錯,稍后將由人工與您聯(lián)系,謝謝。"
self.send_msg(bot_msg)
break
tmp = self.reg_msg.split(bot_inp)
bot_msg = tmp[0].strip("給用戶的回復:")
self.send_msg(bot_msg)
if len(tmp) > 1:
slot_str = tmp[1].strip("獲取到的信息:")
slot = json.loads(slot_str)
print(f"\tslot: {slot}")
n_round += 1
if self.check_over(slot):
break
user_inp = input()
msg += [
{"role": "assistant", "content": bot_inp},
{"role": "user", "content": user_inp},
]
record = ChatRecord(user_id, sess_id, bot_inp, user_inp, datetime.now())
history.append(record)
if check_risk(user_inp):
break
chat_sess = ChatSession(user_id, sess_id, **slot, chat_at=chat_at)
self.store(history, chat_sess)
def store(self, history: List[ChatRecord], chat: ChatSession):
with SessionLocal.begin() as sess:
q = insert(
chat_record_table
).values(
[asdict(v) for v in history]
)
sess.execute(q)
with SessionLocal.begin() as sess:
q = insert(
chat_session_table
).values(
[asdict(chat)]
)
sess.execute(q)
??啟動前,需要把相關的數(shù)據(jù)庫表建好,這里略過。現(xiàn)在我們進行簡單的嘗試。
import pnlp
nick = "張三"
user = User(pnlp.generate_uuid(nick), nick)
chatbot = Chatbot()
chatbot.chat(user.user_id)
互動過程如下所示。
機器人:請問您的手機號碼是多少呢?
slot: {'cellphone': None, 'people_number': None, 'meal_time': None}
用戶:我的手機是13788889999
機器人:好的,您的手機號碼是13788889999,請問用餐人數(shù)是幾位呢?
slot: {'cellphone': '13788889999', 'people_number': None, 'meal_time': None}
用戶:我們一共五個人
機器人:好的,您們一共五個人,最后,請問您們的用餐時間是什么時候呢?
slot: {'cellphone': '13788889999', 'people_number': 5, 'meal_time': None}
用戶:稍等我問一下啊
機器人:好的,沒問題,我等您的消息。
用戶:好了,明天下午7點,謝謝
機器人:好的,您們的用餐時間是明天下午7點,我們已經(jīng)為您記錄好了,請問還有其他需要幫助的嗎?
slot: {'cellphone': '13788889999', 'people_number': 5, 'meal_time': '明天下午7點'}
??查表可以看到剛剛的記錄。
# chat_session_table 表
[(1,
'dc3be3b3516555d3b0b6a77a1d9c7e82',
'05a88a8e3db8490eacf14b8bb9800fcc',
'13788889999',
5,
'明天下午7點',
'2023-04-08 00:00:34.618232')]
# chat_record_table表
[(1,
'dc3be3b3516555d3b0b6a77a1d9c7e82',
'05a88a8e3db8490eacf14b8bb9800fcc',
'給用戶的回復:請問您的手機號碼是多少呢?\n獲取到的信息:{"cellphone": null, "people_number": null, "meal_time": null}',
'我的手機是13788889999',
'2023-04-08 00:00:47.498172'),
(2,
'dc3be3b3516555d3b0b6a77a1d9c7e82',
'05a88a8e3db8490eacf14b8bb9800fcc',
'給用戶的回復:好的,您的手機號碼是13788889999,請問用餐人數(shù)是幾位呢?\n獲取到的信息:{"cellphone": "13788889999", "people_number": null, "meal_time": null}',
'我們一共五個人',
'2023-04-08 00:01:18.694161'),
(3,
'dc3be3b3516555d3b0b6a77a1d9c7e82',
'05a88a8e3db8490eacf14b8bb9800fcc',
'給用戶的回復:好的,您們一共五個人,最后,請問您們的用餐時間是什么時候呢?\n獲取到的信息:{"cellphone": "13788889999", "people_number": 5, "meal_time": null}',
'稍等我問一下啊',
'2023-04-08 00:01:40.296970'),
(4,
'dc3be3b3516555d3b0b6a77a1d9c7e82',
'05a88a8e3db8490eacf14b8bb9800fcc',
'好的,沒問題,我等您的消息。',
'好了,明天下午7點,謝謝',
'2023-04-08 00:02:15.839735')]
??上面我們實現(xiàn)了一個非常簡陋的任務機器人,雖然沒有傳統(tǒng)機器人的NLU、DM和NLG三個模塊,但已經(jīng)可以工作了。唯一的不足可能是接口反應有點慢,不過這是另一個問題了。
??為了便于讀者更好地構建應用,我們需要對幾個地方進行重點強調。
??第一點,當要支持的對話輪次非常多時(比如培訓、面試這樣的場景),則需要實時將每一輪的對話索引起來,每一輪先召回所有歷史對話中相關的\(N\)輪作為上下文(正如我們在文檔問答中那樣)。然后讓ChatGPT根據(jù)這些上下文對用戶進行回復。這樣理論上我們是可以支持無限輪的。召回的過程其實就是一個回憶的過程,這里可以優(yōu)化的點或者說想象的空間很大。
??第二點,在傳遞message參數(shù)給ChatGPT時,由于有長度限制,有時候上下文中遇到特別長回復那種輪次,可能會導致只能傳幾輪(甚至一輪就耗光長度了)。根據(jù)ChatGPT自己的說法:當歷史記錄非常長時,我們確實可能只能利用其中的一小部分來生成回答。為了應對這種情況,通常會使用一些技術來選擇最相關的歷史記錄,以便在生成回答時使用。例如,可能會使用一些關鍵詞提取技術,識別出歷史記錄中最相關的信息,并將其與當前的輸入一起使用。還可能會使用一些摘要技術來對歷史記錄進行壓縮和精簡,以便在生成回答時只使用最重要的信息。此外,還可以使用一些記憶機制,如注意力機制,以便在歷史記錄中選擇最相關的信息。另外,根據(jù)ChatGPT的說法,在生成回復時,它也會使用一些技術來限制輸出長度,例如截斷輸出或者使用一些策略來生成更加簡潔的回答。當然,用戶也可以使用特定的輸入限制或規(guī)則來幫助縮短回答。總之,盡可能地在輸出長度和回答質量之間平衡。
??第三點,充分考慮安全性,根據(jù)實際情況合理設計架構。
??最后,值得一提的是,上面只是利用了ChatGPT的一小部分功能,讀者可以結合自己的業(yè)務,或者打開腦洞,開發(fā)更多有用、有趣的產(chǎn)品和應用。
3.4 本章小結
??句子和Token分類是NLP領域最基礎和常用的任務,本章我們首先簡單介紹了這兩種不同的任務,然后介紹了如何使用ChatGPT完成這些任務,最后介紹了幾個相關的應用。文檔問答基于給定上下文回答問題,相比傳統(tǒng)方法,基于大模型做起來非常容易且能獲得還不錯的效果。模型微調主要用于在垂直領域數(shù)據(jù)上適配,再好的大模型也有不擅長或學習不充分的地方,微調就是讓模型”進一步學習“。大模型背景下各類新型應用層出不窮,之前的應用也可以借力大模型提升效率。我們以一個任務機器人為例為讀者展示了如何利用大模型通過幾十行代碼完成一個簡單的應用。希望讀者可以打開腦洞,親自動手實現(xiàn)自己的想法,這也是本書的初衷之一。

【轉載】https://github.com/datawhalechina/hugging-llm
浙公網(wǎng)安備 33010602011771號