-
Notifications
You must be signed in to change notification settings - Fork 16
gpt
2022 年底 ChatGPT 出現之後,讓大家開始注意到人工智慧技術,竟然已經進步到可以和人類對答如流的程度。
而且 ChatGPT 知識豐富,對大部分的領語都可以侃侃而談,像是《問答、翻譯、寫書、寫信、寫報告、角色扮演》等等,都幾乎和人類做得一樣好,而且絕對比人類快得多。
ChatGPT 背後是 GPT3/4 模型,但是 OpenAI 雖然號稱 Open ,不過卻沒有公開 GPT3/4 模型,因此我們只能用 GPT1/2 的模型去說明 GPT 是如何運作的。
GPT(Generative Pre-trained Transformer)是由OpenAI開發的一種基於Transformer模型的自然語言生成模型。GPT系列模型的發展歷程如下:
-
GPT-1:於2018年6月發布,是一種基於Transformer的單向語言模型,使用了1.5億個參數。在多個自然語言處理任務上取得了極好的表現。
-
GPT-2:於2019年2月發布,使用了15億個參數。相較於GPT-1,GPT-2在多個自然語言生成任務上表現更好,例如生成文章、對話等。然而,由於GPT-2過於強大,OpenAI決定不公開釋出完整模型,只公開了部分模型和生成的文本樣例,以免被惡意濫用。
-
GPT-3:於2020年6月發布,使用了1750億個參數。GPT-3是當時最大的語言模型,可用於多種自然語言處理任務,例如語言生成、語言理解等。GPT-3具有強大的通用性,可以完成許多不同的任務,而不需要進行任何特定任務的微調。它在自然語言生成方面的表現,甚至讓人感到模型似乎已經能夠理解語言的本質。
-
GPT-4: 於2023年3月發布,使用一萬億個參數,訓練花費超過一億美元,能力比 GPT-3 更強大,而且能支援多模態 (除文字外,還包含影像,聲音等) 的輸入與輸出。
隨著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 論文中的說明。

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 語言模型。
學過《數位電路/數位邏輯》的人,應該還記得《循序電路 (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 和 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) 可以表示為:
其中,Q,K,V 分別表示查詢矩陣、鍵矩陣和值矩陣,softmax 表示對矩陣中每一行進行 softmax 歸一化,
必須注意的是,Attention 的輸入與輸出是一整排的《詞向量+位置》,通常比 RNN 單一的詞向量大得多。
問題是,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 決定了。
上述公式的意思,就是用 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 等程式,執行方法請參考下列說明文件。