Skip to content
陳鍾誠 edited this page Jun 28, 2024 · 7 revisions

GPT (Generative Pre-trained Transformer)

參考: https://songhuiming.github.io/pages/2023/05/28/gpt-1-gpt-2-gpt-3-instructgpt-chatgpt-and-gpt-4-summary/

2022 年底 ChatGPT 出現之後,讓大家開始注意到人工智慧技術,竟然已經進步到可以和人類對答如流的程度。

而且 ChatGPT 知識豐富,對大部分的領語都可以侃侃而談,像是《問答、翻譯、寫書、寫信、寫報告、角色扮演》等等,都幾乎和人類做得一樣好,而且絕對比人類快得多。

ChatGPT 背後是 GPT3/4 模型,但是 OpenAI 雖然號稱 Open ,不過卻沒有公開 GPT3/4 模型,因此我們只能用 GPT1/2 的模型去說明 GPT 是如何運作的。

GPT 的歷史

GPT(Generative Pre-trained Transformer)是由OpenAI開發的一種基於Transformer模型的自然語言生成模型。GPT系列模型的發展歷程如下:

  1. GPT-1:於2018年6月發布,是一種基於Transformer的單向語言模型,使用了1.5億個參數。在多個自然語言處理任務上取得了極好的表現。

  2. GPT-2:於2019年2月發布,使用了15億個參數。相較於GPT-1,GPT-2在多個自然語言生成任務上表現更好,例如生成文章、對話等。然而,由於GPT-2過於強大,OpenAI決定不公開釋出完整模型,只公開了部分模型和生成的文本樣例,以免被惡意濫用。

  3. GPT-3:於2020年6月發布,使用了1750億個參數。GPT-3是當時最大的語言模型,可用於多種自然語言處理任務,例如語言生成、語言理解等。GPT-3具有強大的通用性,可以完成許多不同的任務,而不需要進行任何特定任務的微調。它在自然語言生成方面的表現,甚至讓人感到模型似乎已經能夠理解語言的本質。

  4. GPT-4: 於2023年3月發布,使用一萬億個參數,訓練花費超過一億美元,能力比 GPT-3 更強大,而且能支援多模態 (除文字外,還包含影像,聲音等) 的輸入與輸出。

隨著GPT系列模型的不斷發展,它們的參數量越來越大,表現也越來越出色,並且在各種自然語言處理任務上取得了極好的表現。

GPT 模型

GPT 的訓練可以分為兩段,1. 預訓練 2. 微調。

在預訓練階段,GPT 通過大規模的無監督學習從海量的文本數據中學習語言模型,從而使得模型能夠對語言的規律和潛在含義進行建模。預訓練階段中的模型是單向的,只能使用前面的單詞來預測下一個單詞。

GPT1 使用 Transformer 的 Decoder 作為語言模型,同樣以 Multi-headed Self-Attention 為核心,其模型的數學公式如下:

$h_0 = U W_e + W_p$

$h_k = block(h_{k-1}) ;; \forall i \in [1,n]$

$P(u) = softmax(h_n W_e^T) $

以下是 GPT1 論文中的解說

在微調 (Fine-tune) 階段,GPT會進行《分類 (Classification) / 因果推理 (Entailment) / 相似度 (Similarity) / 多選 (Multiple Choice)》等微調訓練,然後再利用下列公式,將《微調與預訓練》兩者的損失函數融合為一,以下是 GPT1 論文中的說明。

minGPT

Karpathy 在 minGPT 裏,用 300 行左右的程式碼,實作了 GPT2 的模型核心,這也是我們接下來最主要的參考程式。

GPT 是一種 序列對序列 (seq2seq) 的技術,但以往 seq2seq 都是使用 RNN 模型 (包含 RNN/LSTM/GRU) 來預測下一個詞。

但在 2018 年 Google 的一篇革命性論文 Attention is all you need 出現後,大家發現使用 Attention/Transformer 的模型比使用 RNN 的模型更快又表現更好,於是 Attention/Transformer 就成了新一代 seq2seq 技術的主流。

所謂的 Transformer 模型,其架構如下圖所示,由左半邊的 Encoder 加上右半邊的 Decoder 組成:

這種 Encoder/Decoder 被使用在《降維+轉換》等領域 ...

例如 Word Embedding 技術就是用 Encoder/Decoder 將一個原本用 one-hot 編碼的詞彙 (可能是十萬維) 降低成 word vector (可能 300 維)。

而 seq2seq 則是標準的將 Encoder/Decoder 用在轉換領域的技術,例如機器翻譯應用中,要將《英文翻譯成中文》,就可以採用 seq2seq 的 Encoder/Decoder 模型。

2018 年開始, Google 繼續發展 Attention 技術,Google 取了 Transformer 的 encoder 部分,修改後設計了 BERT 等模型,而 OpenAI 則取了 Decoder 部分,修改後發展出了 GPT 模型,其演化過程如下圖所示。

要了解 Transformer / GPT 這些技術之前,我們最好先了解一下同樣用在 seq2seq 技術上的 RNN 語言模型。

RNN 循環神經網路

學過《數位電路/數位邏輯》的人,應該還記得《循序電路 (Sequential Logic) / 組合電路(Combinatorial Logic)》 這樣的區分。

其中的循序電路就是具有循環的電路,有了循環之後,我們就可以做出 Latch 去記憶一個位元。

但是沒有循環的《組合電路》,是沒有記憶的,因此只要輸入相同,輸出必然相同。(例如加法器,同樣的 a,b 必然會輸出同樣的 a+b)

傳統的神經網路模型,像是 MLP / CNN 等,就像《組合電路》一樣,對相同的輸入,會有相同的輸出,而不會記住你前面到底輸入了些甚麼?

而循環神經網路 RNN,就像《循序電路》一樣,會在《內部狀態》記住你之前輸入了甚麼,於是即使有同樣的輸入,未必有同樣的輸出,因為《內部狀態》的記憶可能不同。

RNN (Recurrent Neural Network) 循環神經網路透過加入循環,讓 RNN 可以記住過去的資訊在隱藏層 h 中 (如下圖左半部)

這種有循環的隱藏層,會有記憶效應,根據不同的內部狀態,就會有不同的輸出,即使輸入相同,輸出也有可能不同。

如果我們用 PyTorch 實作一個 RNN 循環神經網路,用來預測文章中的下一個詞,那麼就可以採用下列程式來實作。

class RNNLM(nn.Module):
    def __init__(self, method, vocab_size, embed_size, hidden_size, num_layers):
        super(RNNLM, self).__init__()
        method = method.upper()
        self.embed = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.RNN(embed_size, hidden_size, num_layers, batch_first=True) # RNN 也可以改為 GRU
        self.linear = nn.Linear(hidden_size, vocab_size)
        
    def forward(self, x, h):
        # Embed word ids to vectors
        x = self.embed(x)
        
        # Forward propagate 
        out, h = self.rnn(x, h)
        
        # Reshape output to (batch_size*seq_length, hidden_size)
        out = out.reshape(out.size(0)*out.size(1), out.size(2))
        
        # Decode hidden states of all time steps
        out = self.linear(out)
        return out, h

上述程式中的 h 就是隱藏層的狀態 ...

RNN 神經網路出現後,很多人拿來訓練語言模型,讓 RNN 學習如何產生一篇文章。(通常是像文字接龍一樣,如果你先給定幾句話,RNN 就會開始接龍)

MinGPT / MicroGrad 的作者 Karpathy 曾經寫過下列文章,展示了 RNN 的神奇效用

Karpathy 用 RNN 做了一些實驗,發現若讓 RNN 學《莎士比亞小說》,然後 RNN 寫出來就會很像《莎士比亞小說》,同樣的,學 LaTex 論文後,就會寫出看來很漂亮的論文,學 Linux 程式碼之後,RNN 就會寫很漂亮的 C 語言了。

但是 RNN 的記憶,常常衰退的很快,因此後來發展出了像 LSTM/GRU 等 RNN 的變形,於是 RNN 的記憶就變得更好了。

有了上述 RNN 的背景知識後,我們就能來看看 GPT 到底是甚麼了!

GPT

在語言模型上,GPT 和 RNN 相同,主要都是預測下一個詞,不過 GPT 的輸入是一整排的詞,這些詞被embed 層轉換成《詞向量》之後,和《位置》被 embed 層轉換後的《位置向量》相加,然後才傳送到 transformer_block 去。

在 minGPT 專案中,有個範例是讓 minGPT 去學習 0,1,2 這三個數字的排序,以下是該程式的片段

class SortDataset(Dataset):
    """ 
    Dataset for the Sort problem. E.g. for problem length 6:
    Input: 0 0 2 1 0 1 -> Output: 0 0 0 1 1 2
    Which will feed into the transformer concatenated as:
    input:  0 0 2 1 0 1 0 0 0 1 1
    output: I I I I I 0 0 0 1 1 2
    where I is "ignore", as the transformer is reading the input sequence
    """

您可以看到該範例中 GPT 的輸入是一整排的

input:  0 0 2 1 0 1 0 0 0 1 1
output: I I I I I 0 0 0 1 1 2

其中的 I 是 ignore ,也就是不重要,不計分的項目,接著你看到後半部,會發現 output 總是領先 input 一個數字,這也就是 GPT 真正想要訓練的事情,就是讓模型學會《下一個詞》到底應該輸出甚麼?

input:  0 0 2 1 0 1 後半 0 0 0 1 1
output: I I I I I 0 後半 0 0 1 1 2

理解了 GPT 的輸入與輸出之後,讓我們來看看 minGPT 模型的核心程式碼

class GPT(nn.Module):
    # ...
    def forward(self, idx, targets=None):
        device = idx.device
        b, t = idx.size()
        assert t <= self.block_size, f"Cannot forward sequence of length {t}, block size is only {self.block_size}"
        pos = torch.arange(0, t, dtype=torch.long, device=device).unsqueeze(0) # shape (1, t)

        # forward the GPT model itself
        tok_emb = self.transformer.wte(idx) # token embeddings of shape (b, t, n_embd)
        pos_emb = self.transformer.wpe(pos) # position embeddings of shape (1, t, n_embd)
        x = self.transformer.drop(tok_emb + pos_emb) # 詞向量 + 位置向量
        for block in self.transformer.h: # 經過 h 個 block (含 Attention) 之後
            x = block(x)
        x = self.transformer.ln_f(x) # 正規化
        logits = self.lm_head(x) # 從 embed_size 轉回 vocab_size 大小

        # if we are given some desired targets also calculate the loss
        loss = None
        if targets is not None: # 用 CrossEntropy 計算 loss
            loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-1)

        return logits, loss

而其中的 block 結構如下

class Block(nn.Module):
    """ an unassuming Transformer block """

    def __init__(self, config):
        super().__init__()
        self.ln_1 = nn.LayerNorm(config.n_embd)
        self.attn = CausalSelfAttention(config)
        self.ln_2 = nn.LayerNorm(config.n_embd)
        self.mlp = nn.ModuleDict(dict(
            c_fc    = nn.Linear(config.n_embd, 4 * config.n_embd),
            c_proj  = nn.Linear(4 * config.n_embd, config.n_embd),
            act     = NewGELU(),
            dropout = nn.Dropout(config.resid_pdrop),
        ))
        m = self.mlp
        self.mlpf = lambda x: m.dropout(m.c_proj(m.act(m.c_fc(x)))) # MLP forward

    def forward(self, x):
        x = x + self.attn(self.ln_1(x))
        x = x + self.mlpf(self.ln_2(x))
        return x

注意力機制的 CausalSelfAttention 層實作如下:

class CausalSelfAttention(nn.Module):
    """
    A vanilla multi-head masked self-attention layer with a projection at the end.
    It is possible to use torch.nn.MultiheadAttention here but I am including an
    explicit implementation here to show that there is nothing too scary here.
    """

    def __init__(self, config):
        super().__init__()
        assert config.n_embd % config.n_head == 0
        # key, query, value projections for all heads, but in a batch
        self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd)
        # output projection
        self.c_proj = nn.Linear(config.n_embd, config.n_embd)
        # regularization
        self.attn_dropout = nn.Dropout(config.attn_pdrop)
        self.resid_dropout = nn.Dropout(config.resid_pdrop)
        # causal mask to ensure that attention is only applied to the left in the input sequence
        self.register_buffer("bias", torch.tril(torch.ones(config.block_size, config.block_size))
                                     .view(1, 1, config.block_size, config.block_size))
        self.n_head = config.n_head
        self.n_embd = config.n_embd

    def forward(self, x):
        B, T, C = x.size() # batch size, sequence length, embedding dimensionality (n_embd)

        # calculate query, key, values for all heads in batch and move head forward to be the batch dim
        q, k ,v  = self.c_attn(x).split(self.n_embd, dim=2)
        k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)
        q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)
        v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)

        # causal self-attention; Self-attend: (B, nh, T, hs) x (B, nh, hs, T) -> (B, nh, T, T)
        att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
        att = att.masked_fill(self.bias[:,:,:T,:T] == 0, float('-inf'))
        att = F.softmax(att, dim=-1)
        att = self.attn_dropout(att)
        y = att @ v # (B, nh, T, T) x (B, nh, T, hs) -> (B, nh, T, hs)
        y = y.transpose(1, 2).contiguous().view(B, T, C) # re-assemble all head outputs side by side

        # output projection
        y = self.resid_dropout(self.c_proj(y))
        return y

注意力機制 Attention 的模型與數學式如下:

Attention(Q,K,V) 可以表示為:

$$ \text{Attention}(Q,K,V) = \text{softmax}(\frac{QK^T}{\sqrt{d_k}})V $$

其中,Q,K,V 分別表示查詢矩陣、鍵矩陣和值矩陣,softmax 表示對矩陣中每一行進行 softmax 歸一化, $sqrt(d_k)$ 是一個縮放因子, $d_k$ 表示鍵矩陣 K 的維度。注意,這里的 Q,K,V 通常是通過線性變換得到的。

必須注意的是,Attention 的輸入與輸出是一整排的《詞向量+位置》,通常比 RNN 單一的詞向量大得多。

問題是,Attention 到底是甚麼?為何表現那麼好呢?

Attention 的直覺意義

熟悉 JavaScript 的人一定知道 JSON

JSON 這樣的結構很有用,例如當作英翻中的字典

{
    dog:'狗',
    cat:'貓',
    a:'一隻',
    the:'這隻',
    eat:'吃',
    chase:'追'
}

或者記錄結構

{
    name:'陳鍾誠',
    age:53,
    gender:'男',
    friends: ['Snoopy', 'Tim'],
}

JSON 裡的結構都是像這樣

{
    key1:value1,
    key2:value2,
    ...
}

Attention 中的 key, value 也是這個意義,用 key 來提取出 value 。

但是現在到底應該提取那些 value 出來呢?這就是靠 query 決定了。

$$ \text{Attention}(Q,K,V) = \text{softmax}(\frac{QK^T}{\sqrt{d_k}})V $$

上述公式的意思,就是用 Q (query) 去找出相關的 K (key),然後再用這些 K (key) 取出對應的 V (value) 丟出去。

但要注意的是,Q, K, V 的實作是 Linear 全連接層,所以其實是像相關係數那樣的,兩兩之間都有相關,於是會有 W[q,k] 這樣的二維權重參數。

在 Attention / Transformer 機制被發展出來之後,研究者發現這個模型的效果超好,於是 Google 取了 Encoder 部分繼續發展出 BERT 等模型,而 OpenAI 則取了 Decoder 部分修改後發展出了 GPT 模型。

我們甚至無法清楚的理解為何 Attention / Transformer 的效果這麼好 ...

深度學習領域的研究人員,從簡單的神經網路開始,透過《直覺+實驗》,發現把 Sigmoid 改成 ReLU 後效果會更好,然後逐步引入了《CNN / RNN / LSTM / GRU / 殘差網路 / Attention / Transformer》等模型,持續地發現更新,更好,更強大的模型。

於是人工智慧的發展,因為這些模型而開啟了一個全新的時代!

如果你想更了解 GPT,請用 git clone 下載我們修改過的 minGPT 程式,像是 chargpt.py (以字元為單位,學習語言模型) / demo.py (學習排序) / generate.py (直接載入預訓練模型,開始文字接龍) 等等範例,這樣您才能對 GPT 的原理有更深一層的體會。

下載指令

git clone https://github.com/cccbook/py2gpt.git

然後切換到 04a-MinGpt 去執行 chargpt.py/demo.py/generate.py 等程式,執行方法請參考下列說明文件。

參考文獻

Clone this wiki locally