GPT 架構採用 Transformer 模型的解碼器部分,以單向、從左到右的方式處理文字,使其在文字生成任務中表現出色。模型的龐大規模,例如 GPT-3 擁有的 1750 億個引數,也貢獻了其強大的生成能力。雖然 GPT 的訓練目標是預測下一個詞,但它展現出一些新興行為,例如翻譯,這並非透過特定訓練目標達成,而是來自模型對大量多語言資料的學習。建構大語言模型包含資料準備與取樣、預訓練和微調三個階段。資料準備階段涉及資料取樣和注意力機制;預訓練階段在未標記資料上訓練基礎模型;微調階段則針對特定任務,例如文字分類別或個人助理,進行模型調整。理解詞嵌入是處理文字資料的關鍵,它將離散的文字資料轉換為連續的向量表示,以便神經網路處理。常用的詞嵌入演算法如 Word2Vec,透過預測目標詞的上下文來學習詞嵌入。在為 LLM 準備文字資料時,需要進行文字分詞,例如使用位元組對編碼(BPE)處理罕見詞彙,並透過滑動視窗方法取樣訓練樣本。最後,使用詞嵌入矩陣將分詞後的文字轉換為向量表示,以供 LLM 訓練使用。

探討 GPT 架構

GPT 架構的設計使其在文字生成和下一個詞預測任務中表現出色。與原始的 Transformer 模型相比,GPT 架構僅使用瞭解碼器部分,並且是為單向、從左到右的處理而設計的。這種設計使得 GPT 模型能夠以迭代的方式,一次一個詞地生成文字。

GPT 架構的特點

GPT 架構具有以下特點:

  • 單向處理:GPT 架構是為單向、從左到右的處理而設計的,這使得它非常適合用於文字生成和下一個詞預測任務。
  • 解碼器架構:GPT 架構僅使用了原始 Transformer 模型的解碼器部分,這簡化了架構並使其更適合於文字生成任務。
  • 龐大的模型規模:像 GPT-3 這樣的模型具有龐大的規模,擁有 96 個 Transformer 層和總共 1750 億個引數。

新興行為

GPT 模型的訓練目標是預測句子中的下一個詞,但它們卻表現出了一些新興行為,例如能夠執行翻譯任務。這些新興行為並不是透過特定的訓練目標來實作的,而是透過模型對大量多語言資料的接觸而自然產生的。

建構大語言模型

建構大語言模型需要經過三個階段:

  1. 資料準備和取樣:實作資料取樣和了解基本機制。
  2. 預訓練:在未標記的資料上預訓練 LLM,以獲得基礎模型供進一步微調。
  3. 微調:對預訓練的 LLM 進行微調,以建立分類別模型或個人助理。

第一階段:基礎模型

在第一階段,我們將學習基本的資料預處理步驟,並實作注意力機制,這是每個 LLM 的核心。

第二階段:預訓練

在第二階段,我們將學習如何編寫程式碼並預訓練一個類別似 GPT 的 LLM,使其能夠生成新的文字。我們還將討論評估 LLM 的基本原理,這對於開發有能力的 NLP 系統至關重要。

第三階段:微調

在第三階段,我們將對預訓練的 LLM 進行微調,使其能夠遵循指令,例如回答查詢或分類別文字,這是許多現實應用和研究中最常見的任務。

圖示解密:
  • 此圖示清晰地展示了建構 LLM 的三個階段及其之間的邏輯關係。
  • 每個階段都建立在前一階段的基礎上,最終形成了一個功能完備的個人助理或文字分類別器。

大語言模型訓練的文字資料處理

本章節將探討大語言模型(LLMs)訓練前的文字資料準備工作。我們將介紹如何將文字拆分為單詞和子詞(subword)符記(tokens),並使用進階的符記化(tokenization)技術,如位元組對編碼(byte pair encoding),將文字轉換為向量表示,以供LLMs使用。

理解詞嵌入(Word Embeddings)

深度神經網路模型,包括LLMs,無法直接處理原始文字資料。由於文字資料是類別型的,因此與實作和訓練神經網路所使用的數學運算不相容。因此,我們需要一種方法將單詞表示為連續值的向量。

詞嵌入是一種將資料轉換為向量格式的方法。透過使用特定的神經網路層或其他預訓練的神經網路模型,我們可以嵌入不同的資料型別,例如影片、音訊和文字。然而,不同的資料格式需要不同的嵌入模型。

詞嵌入的核心概念

詞嵌入是一種將離散物件(如單詞、影像或整個檔案)對映到連續向量空間中的方法。其主要目的是將非數值資料轉換為神經網路可以處理的格式。

雖然詞嵌入是最常見的文字嵌入形式,但也有句子、段落或整個檔案的嵌入。句子或段落嵌入是檢索增強生成(retrieval-augmented generation)的流行選擇。檢索增強生成結合了生成(如生成文字)和檢索(如搜尋外部知識函式庫),以在生成文字時提取相關資訊。

Word2Vec:一個流行的詞嵌入演算法

Word2Vec是一種訓練神經網路架構以生成詞嵌入的演算法,透過預測目標單詞的上下文或反之亦然。其主要思想是,出現相似上下文中的單詞往往具有相似的含義。因此,當投影到二維詞嵌入中進行視覺化時,相似的術語會聚集在一起。

為LLMs準備文字資料

在訓練LLMs之前,我們需要準備訓練資料集。這涉及將文字拆分為單詞和子詞符記,並將其編碼為向量表示,以供LLM使用。

文字元記化(Text Tokenization)

文字元記化是將文字拆分為單詞或子詞符記的過程。我們可以使用不同的符記化方案,如根據空格的符記化或更進階的位元組對編碼(BPE)。

位元組對編碼(Byte Pair Encoding)

BPE是一種進階的符記化技術,透過迭代合併最頻繁的位元組對來構建符記。這種方法可以有效地處理罕見單詞和新穎單詞。

取樣訓練示例(Sampling Training Examples)

我們可以使用滑動視窗(sliding window)方法從文字資料中取樣訓練示例。這涉及從原始文字中提取固定長度的序列,並將其用作LLM的輸入-輸出對。

將符記轉換為向量表示

一旦我們有了符記化的文字資料,我們就需要將其轉換為向量表示,以供LLM使用。這涉及使用詞嵌入矩陣將每個符記對映到一個連續向量空間中。

詞嵌入矩陣

詞嵌入矩陣是一種將每個符記對映到一個連續向量空間中的方法。我們可以使用不同的詞嵌入演算法,如Word2Vec或GloVe,來學習詞嵌入矩陣。

內容解密:
  1. 匯入必要的函式庫:首先,我們匯入numpy函式庫,用於數值計算。
  2. 定義詞嵌入矩陣:我們定義了一個隨機的詞嵌入矩陣,大小為100x128,代表100個符記,每個符記被對映到一個128維的向量空間中。
  3. 定義符記列表:我們定義了一個包含6個符記的列表。
  4. 將符記轉換為向量表示:我們使用列表推導式,將每個符記對映到其對應的向量表示。
  5. 列印向量表示:最後,我們列印每個符記及其對應的向量表示。

這個例子展示瞭如何使用詞嵌入矩陣將符記轉換為向量表示,這是LLMs訓練中的一個關鍵步驟。

處理文字資料

雖然我們可以使用像Word2Vec這樣的預訓練模型為機器學習模型生成嵌入向量,但大語言模型(LLM)通常會產生自己的嵌入向量,這些向量是輸入層的一部分,並在訓練過程中進行更新。最佳化嵌入向量作為LLM訓練的一部分,而不是使用Word2Vec的好處是,嵌入向量針對特定的任務和資料進行了最佳化。在本章的後面部分,我們將實作這樣的嵌入層。

不幸的是,高維度的嵌入向量對於視覺化來說是一個挑戰,因為我們的感官感知和常見的圖形表示本質上僅限於三個或更少的維度,這就是為什麼圖2.3顯示的是二維嵌入向量的二維散點圖。然而,在使用LLM時,我們通常使用具有更高維度的嵌入向量。對於GPT-2和GPT-3來說,嵌入向量的大小(通常稱為模型隱藏狀態的維度)根據特定的模型變體和大小而有所不同。這是在效能和效率之間的權衡。最小的GPT-2模型(1.17億和1.25億引數)使用768維的嵌入向量來提供具體的例子。最大的GPT-3模型(1750億引數)使用12,288維的嵌入向量。

接下來,我們將逐步介紹準備LLM使用的嵌入向量所需的步驟,包括將文字分成單詞,將單詞轉換成標記,以及將標記轉換成嵌入向量。

將文字標記化

讓我們討論如何將輸入文字分成單個標記,這是為LLM建立嵌入向量所需的預處理步驟。這些標記可以是單個單詞或特殊字元,包括標點符號,如圖2.4所示。

載入文字資料

我們將用來訓練LLM的文字是伊迪絲·華頓(Edith Wharton)的短篇小說《判決》(“The Verdict”),該小說已經進入公眾領域,因此允許用於LLM訓練任務。文字可在Wikisource上獲得,網址為https://en.wikisource.org/wiki/The_Verdict,您可以將其複製並貼上到文字檔案中。我已將其複製到名為"the-verdict.txt"的文字檔案中。

或者,您可以在本文的GitHub儲存函式庫中找到"the-verdict.txt"檔案,網址為https://mng.bz/Adng。您可以使用以下Python程式碼下載該檔案:

import urllib.request
url = ("https://raw.githubusercontent.com/rasbt/"
       "LLMs-from-scratch/main/ch02/01_main-chapter-code/"
       "the-verdict.txt")
file_path = "the-verdict.txt"
urllib.request.urlretrieve(url, file_path)

接下來,我們可以使用Python的標準檔案讀取工具載入"the-verdict.txt"檔案。

with open("the-verdict.txt", "r", encoding="utf-8") as f:
    raw_text = f.read()
print("總字元數:", len(raw_text))
print(raw_text[:99])

文字標記化

我們的目標是將這篇包含20,479個字元的短篇小說標記化為單個單詞和特殊字元,然後將其轉換成嵌入向量,用於LLM訓練。

為了說明文字標記化的過程,我們可以使用Python的正規表示式函式庫re。使用一些簡單的示例文字,我們可以使用re.split命令按照以下語法在空白字元上分割文字:

import re
text = "Hello, world. This, is a test."
result = re.split(r'(\s)', text)
print(result)

結果是一個包含單個單詞、空白字元和標點符號的列表:

['Hello,', ' ', 'world.', ' ', 'This,', ' ', 'is', ' ', 'a', ' ', 'test.']

內容解密:

  1. 載入文字資料:首先,我們需要載入要使用的文字資料。在這個例子中,我們使用了伊迪絲·華頓的短篇小說《判決》,並透過Python程式碼下載了該文字檔案。
  2. 文字標記化:接下來,我們需要將載入的文字資料標記化,即將其分成單個的單詞或特殊字元。這一步驟是建立嵌入向量所必需的預處理步驟。
  3. 使用正規表示式進行文字分割:為了說明文字標記化的過程,我們使用了Python的正規表示式函式庫re,並演示瞭如何使用re.split命令在空白字元上分割文字。

為什麼需要高維度的嵌入向量?

高維度的嵌入向量可以捕捉單詞之間更豐富的語義關係和上下文資訊,這對於大語言模型的效能至關重要。

標記化的重要性

標記化是自然語言處理中的一個關鍵步驟,它使得機器能夠理解和處理人類語言。透過將文字分成單個的標記,我們可以將其轉換成機器可理解的格式,從而進行進一步的處理和分析。

2.2 文字分詞處理

簡單的分詞方案能夠有效地將示例文字分割成單獨的詞彙單位,但某些詞彙仍與標點符號相連,我們希望將這些符號視為獨立的元素進行處理。同時,我們避免將所有文字轉換為小寫,因為大小寫有助於大語言模型(LLM)區分專有名詞和普通名詞,理解句子結構,並學習生成具有正確大小寫的文字。

讓我們修改正規表示式,使其能夠根據空白字元(\s)、逗號和句點([,.])進行分割:

result = re.split(r'([,.]|\s)', text)
print(result)

現在,詞彙和標點符號被正確地分割成獨立的列表元素,如下所示: [‘Hello’, ‘,’, ‘’, ’ ‘, ‘world’, ‘.’, ‘’, ’ ‘, ‘This’, ‘,’, ‘’, ’ ‘, ‘is’, ’ ‘, ‘a’, ’ ‘, ’test’, ‘.’, ‘’] 一個小問題是列表中仍然包含空白字元。我們可以透過以下方式安全地移除這些冗餘字元:

result = [item for item in result if item.strip()]
print(result)

經過處理後,輸出結果不再包含空白字元,如下所示: [‘Hello’, ‘,’, ‘world’, ‘.’, ‘This’, ‘,’, ‘is’, ‘a’, ’test’, ‘.’]

內容解密:

  1. 使用正規表示式 r'([,.]|\s)' 對文字進行分割,能夠有效地將詞彙和標點符號分開。
  2. 分割後的結果中包含空白字元,透過列表推導式 [item for item in result if item.strip()] 移除這些字元。
  3. 這種處理方式簡化了分詞結果,但也取決於具體應用需求。在某些情況下,保留空白字元可能有助於模型理解文字的精確結構。

進一步擴充套件這個分詞方案,使其能夠處理其他型別的標點符號,如問號、引號和雙破折號等特殊字元:

text = "Hello, world. Is this-- a test?"
result = re.split(r'([,.:;?_!"()\']|--|\s)', text)
result = [item.strip() for item in result if item.strip()]
print(result)

輸出結果如下: [‘Hello’, ‘,’, ‘world’, ‘.’, ‘Is’, ’this’, ‘–’, ‘a’, ’test’, ‘?’]

內容解密:

  1. 擴充套件正規表示式 r'([,.:;?_!"()\']|--|\s)' 以處理更多型別的標點符號和特殊字元。
  2. 新的分詞方案能夠成功地將包含多種標點符號的文字分割成獨立的詞彙單位。
  3. 這種靈活的分詞方法有助於處理多樣化的文字資料。

現在,將這個分詞方案應用於 Edith Wharton 的整個短篇小說:

preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', raw_text)
preprocessed = [item.strip() for item in preprocessed if item.strip()]
print(len(preprocessed))

輸出結果為 4690,這是文字中的詞彙單位數量(不包含空白字元)。檢查前 30 個詞彙單位以驗證分詞效果:

print(preprocessed[:30])

輸出結果如下: [‘I’, ‘HAD’, ‘always’, ’thought’, ‘Jack’, ‘Gisburn’, ‘rather’, ‘a’, ‘cheap’, ‘genius’, ‘–’, ’though’, ‘a’, ‘good’, ‘fellow’, ’enough’, ‘–’, ‘so’, ‘it’, ‘was’, ’no’, ‘great’, ‘surprise’, ’to’, ‘me’, ’to’, ‘hear’, ’that’, ‘,’, ‘in’]

內容解密:

  1. 將分詞方案應用於整個短篇小說,得到總共 4690 個詞彙單位。
  2. 檢查前 30 個詞彙單位,確認分詞方案能夠正確地處理文字。

2.3 將詞彙單位轉換為詞彙ID

接下來,將這些詞彙單位從 Python 字串轉換為整數表示形式,以生成詞彙 ID。這一轉換是將詞彙 ID 進一步轉換為嵌入向量的中間步驟。

首先,建立一個詞彙表來定義每個獨特詞彙和特殊字元到唯一整數的對映關係,如圖 2.6 所示。

建立詞彙表

對預處理後的文字進行排序和去重,得到所有獨特詞彙的列表,並確定詞彙表的大小:

all_words = sorted(set(preprocessed))
vocab_size = len(all_words)
print(vocab_size)

結果表明,詞彙表的大小為 1,130。接下來,建立詞彙表並列印前 51 個條目以進行說明:

vocab = {token: integer for integer, token in enumerate(all_words)}
for i, item in enumerate(vocab.items()):
    print(item)
    if i >= 50:
        break

內容解密:

  1. 使用 sorted(set(preprocessed)) 得到所有獨特詞彙的列表,並確定詞彙表的大小。
  2. 透過字典推導式 {token: integer for integer, token in enumerate(all_words)} 建立詞彙表,將每個獨特詞彙對映到一個唯一的整數。
  3. 詞彙表的建立是將文字資料轉換為模型可處理的數值表示的關鍵步驟。

輸出結果如下: (’!’, 0) (’"’, 1) ("’", 2) … (‘Her’, 49) (‘Hermia’, 50)

內容解密:

  1. 詞彙表中的每個條目都將一個獨特的詞彙或特殊字元對映到一個唯一的整數 ID。
  2. 這種對映關係使得文字資料能夠被模型處理和理解。

為了將 LLM 的輸出從數字轉換迴文字,需要建立一個逆向詞彙表,將詞彙 ID 映射回對應的文字詞彙單位,如圖 2.7 所示。

內容解密:

  1. 詞彙表的建立使得新文字能夠被轉換為詞彙 ID,而逆向詞彙表則使得模型輸出能夠被轉換回可讀的文字。
  2. 這種雙向對映關係是實作文字資料處理和生成的基礎。

2.3 將標記轉換為標記ID

本文將實作一個完整的分詞器(tokenizer)類別於Python中,包含一個encode方法,能夠將文字分割成多個標記(tokens),並透過詞彙表(vocabulary)將字串對映為整數ID,產生標記ID。此外,我們也會實作一個decode方法,能夠進行反向的整數到字串對映,將標記ID轉換迴文字。以下清單顯示了這個分詞器實作的程式碼。

class SimpleTokenizerV1:
    def __init__(self, vocab):
        self.str_to_int = vocab
        self.int_to_str = {i: s for s, i in vocab.items()}

    def encode(self, text):
        preprocessed = re.split(r'([,.?_!"()\']|--|\s)', text)
        preprocessed = [
            item.strip() for item in preprocessed if item.strip()
        ]
        ids = [self.str_to_int[s] for s in preprocessed]
        return ids

    def decode(self, ids):
        text = " ".join([self.int_to_str[i] for i in ids])
        text = re.sub(r'\s+([,.?!"()\'])', r'\1', text)
        return text

內容解密:

  1. __init__ 方法初始化分詞器,接受一個詞彙表 vocab,並建立兩個字典:str_to_int 用於將字串對映到整數ID,而 int_to_str 則是反向對映,將整數ID映射回字串。
  2. encode 方法處理輸入文字,將其分割成標記,並透過 str_to_int 字典將這些標記轉換為對應的整數ID。
  3. decode 方法執行反向操作,將整數ID列表轉換回原始文字。它首先使用 int_to_str 將ID映射回標記,然後將這些標記組合成文字,並移除特定標點符號前的空格。

使用 SimpleTokenizerV1 類別,我們可以透過現有的詞彙表例項化新的分詞器物件,然後用它來編碼和解碼文字,如圖2.8所示。

讓我們例項化一個新的分詞器物件,並對 Edith Wharton 的短篇小說中的一段文字進行分詞,以實際測試它:

tokenizer = SimpleTokenizerV1(vocab)
text = """"It's the last he painted, you know,"
Mrs. Gisburn said with pardonable pride."""
ids = tokenizer.encode(text)
print(ids)

上述程式碼列印出以下標記ID:

[1, 56, 2, 850, 988, 602, 533, 746, 5, 1126, 596, 5, 1, 67, 7, 38, 851, 1108, 754, 793, 7]

接下來,讓我們看看是否可以使用 decode 方法將這些標記ID轉換迴文字:

print(tokenizer.decode(ids))

輸出結果為:

'" It\' s the last he painted, you know," Mrs. Gisburn said with pardonable pride.'

根據輸出結果,我們可以看到 decode 方法成功地將標記ID轉換回原始文字。

到目前為止,一切順利。我們實作了一個能夠根據訓練集片段進行分詞和反分詞的分詞器。現在,讓我們將它應用到一個新的文字範例上,這個範例並不包含在訓練集中:

text = "Hello, do you like tea?"
print(tokenizer.encode(text))

執行這段程式碼將導致以下錯誤:

KeyError: 'Hello'

問題在於單字“Hello”並未在“The Verdict”短篇小說中使用,因此它並不包含在詞彙表中。這凸顯了在處理大語言模型(LLMs)時,需要考慮使用大型且多樣化的訓練集來擴充詞彙表的重要性。

圖示說明

此圖示展示了分詞器的實作方式,包括 encode 方法和 decode 方法。encode 方法接收樣本文字,將其分割成個別標記,並透過詞彙表將標記轉換為標記ID。decode 方法接收標記ID,將其轉換迴文字標記,並將這些文字標記組合成自然文字。