介紹
在本次將學習另一個有着優秀表現的預訓練模型:GPT-2 模型,以及使用它進行文本生成任務實踐。
知識點
- GPT-2 的核心思想
- GPT-2 模型結構詳解
- GPT-2 進行文本生成
OpenAI 在論文 Improving Language Understanding by Generative Pre-Training 中提出了 GPT 模型。GPT 模型是由單向 Transformer 的解碼器構建的模型,OpenAI 團隊在一個非常大的書籍數據集 the Toronto Book Corpus 上對其進行了無監督預訓練。
而后,OpenAI 團隊又提出了 GPT-2 模型,GPT-2 模型是 GPT 模型的后繼,使用了更大的訓練集訓練,有更多的參數,是 GPT 模型的擴大版。GPT-2 的訓練集為數量有 8 百萬的網頁,由研究人員從網絡上爬取得到,大小共有 40 GB 的文本數據,訓練任務為給出上文,使模型預測下一個單詞。
由於 GPT 與 GPT-2 模型的差別就在於 GPT-2 使用了更多的訓練數據,增加了模型參數,在具體結構上並無較大差異。所以,下面我們主要介紹實際表現更優異的 GPT-2 模型。
GPT-2 核心思想
根據研究發現,語言有靈活的表達能力,即能夠將任務、輸入、輸出表示成一個字符串,並且模型可以用這種形式的字符串進行訓練,學習相應任務。例如,在翻譯任務中,一個訓練樣本可以被寫成
(translate to french, english text, french text)
同樣地,在閱讀理解任務中,一個訓練樣本可以被寫成
(answer the question, document, question, answer)
並且,人們可以用以上格式的多種任務的訓練樣本同時訓練一個模型,使得該模型獲得同時執行多種任務的能力。
於是 OpenAI 研究人員推測,一個具有足夠能力的語言模型將學習到推理和執行訓練樣本中所展示出的任務,以便更好地預測它們。如果一個語言模型能夠做到這一點,那么它實際上就是在進行無監督的多任務學習。於是研究人員決定通過分析語言模型在各種各樣的任務上的性能來測試這種情況是否屬實,這樣便有了 GPT-2 的產生。
而且正如他們所推測的,GPT-2 在多種任務上的性能表現很好,具體如下圖:

所以 GPT-2 對於其他預訓練模型的一個突破就在於,它能夠在未針對特定下游任務進行訓練的條件下,就在下游任務如:閱讀理解、機器翻譯、問答和文本概括上有很好的表現。這也表明了,在模型足夠大,訓練數據足夠充足時,無監督訓練技術也能訓練出在多種下游任務上有很好表現的模型。
因為監督學習需要大量的數據,並且需要被仔細清理過的數據,想要得到這樣的數據需要昂貴的人力成本。無監督學習可以克服這個缺點,因為它不需要人工標注,有大量現成的數據可以利用。這也表明了 GPT-2 模型研究的意義。
在了解了構建 GPT-2 模型的思想后,接下來我們將詳細了解一下 GPT-2 模型的結構。
GPT-2 模型結構
GPT-2 的整體結構如下圖,GPT-2 是以 Transformer 為基礎構建的, 使用字節對編碼的方法進行數據預處理,通過預測下一個詞任務進行預訓練的語言模型,下面我們從 GPT-2 的預處理方法出發,來一步步詳細解析一下 GPT-2。

字節對編碼
GPT-2 模型在數據預處理時使用了字節對編碼(Byte Pair Encoding,簡稱 BPE)方法,BPE 是一種能夠解決未登錄詞問題,並減小詞典大小的方法。它綜合利用了單詞層面編碼和字符層面編碼的優勢,舉例來說,我們要對下面的字符串編碼,
aaabdaaabac
字節對 aa 出現的次數最多,所以我們將它替換成一個沒在字符串中被用過的字符 Z ,
ZabdZabac
Z=aa
然后我們重復這個過程,用 Y 替換 ab ,
ZYdZYac
Y=ab
Z=aa
繼續,用 X 替換 ZY ,
XdXac
X=ZY
Y=ab
Z=aa
這個過程重復進行,直到沒有字節對出現超過一次。當需要解碼時,就將上述替換過程反向進行。
下面是一段 BPE 算法原文中對 BPE 算法的實現:
import re
import collections
def get_stats(vocab):
pairs = collections.defaultdict(int)
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols)-1):
pairs[symbols[i], symbols[i+1]] += freq # 計算字節對出現頻率
return pairs
def merge_vocab(pair, v_in):
v_out = {}
bigram = re.escape(' '.join(pair)) # 將字節對中可解釋為正則運算符的字符轉義
p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)') # 將要合並的字節對前后只能為空白字符
for word in v_in:
w_out = p.sub(''.join(pair), word) # 合並符合條件的字節對
v_out[w_out] = v_in[word]
return v_out
vocab = {'l o w </w>': 5, 'l o w e r </w>': 2,
'n e w e s t </w>': 6, 'w i d e s t </w>': 3}
num_merges = 10
for i in range(num_merges):
pairs = get_stats(vocab)
best = max(pairs, key=pairs.get) # 選擇頻率最大的字節對
vocab = merge_vocab(best, vocab)
print(best)
單向 Transformer 解碼器結構
GPT-2 模型由多層單向 Transformer 的解碼器部分構成,本質上是自回歸模型,自回歸的意思是指,每次產生新單詞后,將新單詞加到原輸入句后面,作為新的輸入句。其中 Transformer 解碼器結構如下圖:

GPT-2 模型中只使用了多個 Masked Self-Attention 和 Feed Forward Neural Network 兩個模塊。如下圖所示:

可以看到,GPT-2 模型會將語句輸入上圖所示的結構中,預測下一個詞,然后再將新單詞加入,作為新的輸入,繼續預測。損失函數會計算預測值與實際值之間的偏差。
從上一節我們了解到 BERT 是基於雙向 Transformer 結構構建,而 GPT-2 是基於單向 Transformer,這里的雙向與單向,是指在進行注意力計算時,BERT會同時考慮被遮蔽詞左右的詞對其的影響,而 GPT-2 只會考慮在待預測詞位置左側的詞對待預測詞的影響。
通過上述數據預處理方法和模型結構,以及大量的數據訓練出了 GPT-2 模型。OpenAI 團隊由於安全考慮,沒有開源全部訓練參數,而是提供了小型的預訓練模型,接下來我們將在 GPT-2 預訓練模型的基礎上進行。
GPT-2 文本生成
GPT-2 就是一個語言模型,能夠根據上文預測下一個單詞,所以它就可以利用預訓練已經學到的知識來生成文本,如生成新聞。也可以使用另一些數據進行微調,生成有特定格式或者主題的文本,如詩歌、戲劇。所以接下來,我們會用 GPT-2 模型進行一個文本生成。
預訓練模型生成新聞
想要直接運行一個預訓練好的 GPT-2 模型,最簡單的方法是讓它自由工作,即隨機生成文本。換句話說,在開始時,我們給它一點提示,即一個預定好的起始單詞,然后讓它自行地隨機生成后續的文本。
但這樣有時可能會出現問題,例如模型陷入一個循環,不斷生成同一個單詞。為了避免這種情況, GPT-2 設置了一個 top-k 參數,這樣模型就會從概率前 k 大的單詞中隨機選取一個單詞,作為下一個單詞。下面是選擇 top-k 的函數的實現,
import random
def select_top_k(predictions, k=10):
predicted_index = random.choice(
predictions[0, -1, :].sort(descending=True)[1][:10]).item()
return predicted_index
下面引入 GPT-2 模型,我們將使用在 PyTorch-Transformers 模型庫中封裝好的 GPT2Tokenizer() 和 GPT2LMHeadModel() 類來實際看一下 GPT-2 在預訓練后的對下一個詞預測的能力。首先,需要安裝 PyTorch-Transformers。
!pip install pytorch_transformers==1.0 # 安裝 PyTorch-Transformers
使用 PyTorch-Transformers 模型庫,先設置好准備輸入模型的例子,使用 GPT2Tokenizer() 建立分詞器對象對原句編碼。
import torch
from pytorch_transformers import GPT2Tokenizer
import logging
logging.basicConfig(level=logging.INFO)
# 載入預訓練模型的分詞器
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
# 使用 GPT2Tokenizer 對輸入進行編碼
text = "Yesterday, a man named Jack said he saw an alien,"
indexed_tokens = tokenizer.encode(text)
tokens_tensor = torch.tensor([indexed_tokens])
tokens_tensor.shape
接下來使用 GPT2LMHeadModel() 建立模型,並將模型模式設為驗證模式。由於預訓練模型參數體積很大,且托管在外網,所以本次先從網盤下載預訓練模型,本地無需此步驟。
from pytorch_transformers import GPT2LMHeadModel
# 讀取 GPT-2 預訓練模型
model = GPT2LMHeadModel.from_pretrained("./")
model.eval()
total_predicted_text = text
n = 100 # 預測過程的循環次數
for _ in range(n):
with torch.no_grad():
outputs = model(tokens_tensor)
predictions = outputs[0]
predicted_index = select_top_k(predictions, k=10)
predicted_text = tokenizer.decode(indexed_tokens + [predicted_index])
total_predicted_text += tokenizer.decode(predicted_index)
if '<|endoftext|>' in total_predicted_text:
# 如果出現文本結束標志,就結束文本生成
break
indexed_tokens += [predicted_index]
tokens_tensor = torch.tensor([indexed_tokens])
print(total_predicted_text)
運行結束后,我們觀察一下模型生成的文本,可以看到,大致感覺上這好像是一段正常的文本,不過,仔細看就會發現語句中的邏輯問題,這也是之后研究人員會繼續攻克的問題。
除了直接利用預訓練模型生成文本,我們還可以使用微調的方法使 GPT-2 模型生成有特定風格和格式的文本。
微調生成戲劇文本
接下來,我們將使用一些戲劇劇本對 GPT-2 進行微調。由於 OpenAI 團隊開源的 GPT-2 模型預訓練參數為使用英文數據集預訓練后得到的,雖然可以在微調時使用中文數據集,但需要大量數據和時間才會有好的效果,所以這里我們使用了英文數據集進行微調,從而更好地展現 GPT-2 模型的能力。
首先,下載訓練數據集,這里使用了莎士比亞的戲劇作品《羅密歐與朱麗葉》作為訓練樣本。數據集已經提前下載好並放在雲盤中,鏈接:https://pan.baidu.com/s/1LiTgiake1KC8qptjRncJ5w 提取碼:km06
with open('./romeo_and_juliet.txt', 'r') as f:
dataset = f.read()
len(dataset)
預處理訓練集,將訓練集編碼、分段。
indexed_text = tokenizer.encode(dataset)
del(dataset)
dataset_cut = []
for i in range(len(indexed_text)//512):
# 將字符串分段成長度為 512
dataset_cut.append(indexed_text[i*512:i*512+512])
del(indexed_text)
dataset_tensor = torch.tensor(dataset_cut)
dataset_tensor.shape
這里使用 PyTorch 提供的 DataLoader() 構建訓練集數據集表示,使用 TensorDataset() 構建訓練集數據迭代器。
from torch.utils.data import DataLoader, TensorDataset
# 構建數據集和數據迭代器,設定 batch_size 大小為 2
train_set = TensorDataset(dataset_tensor,
dataset_tensor) # 標簽與樣本數據相同
train_loader = DataLoader(dataset=train_set,
batch_size=2,
shuffle=False)
train_loader
檢查是否機器有 GPU,如果有就在 GPU 運行,否則就在 CPU 運行。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device
開始訓練。
from torch import nn
from torch.autograd import Variable
import time
pre = time.time()
epoch = 30 # 循環學習 30 次
model.to(device)
model.train()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-5) # 定義優化器
for i in range(epoch):
total_loss = 0
for batch_idx, (data, target) in enumerate(train_loader):
data, target = Variable(data).to(device), Variable(
target).to(device)
optimizer.zero_grad()
loss, logits, _ = model(data, labels=target)
total_loss += loss
loss.backward()
optimizer.step()
if batch_idx == len(train_loader)-1:
# 在每個 Epoch 的最后輸出一下結果
print('average loss:', total_loss/len(train_loader))
print('訓練時間:', time.time()-pre)
訓練結束后,可以使模型生成文本,觀察輸出。
text = "From fairest creatures we desire" # 這里也可以輸入不同的英文文本
indexed_tokens = tokenizer.encode(text)
tokens_tensor = torch.tensor([indexed_tokens])
model.eval()
total_predicted_text = text
# 使訓練后的模型進行 500 次預測
for _ in range(500):
tokens_tensor = tokens_tensor.to('cuda')
with torch.no_grad():
outputs = model(tokens_tensor)
predictions = outputs[0]
predicted_index = select_top_k(predictions, k=10)
predicted_text = tokenizer.decode(indexed_tokens + [predicted_index])
total_predicted_text += tokenizer.decode(predicted_index)
if '<|endoftext|>' in total_predicted_text:
# 如果出現文本結束標志,就結束文本生成
break
indexed_tokens += [predicted_index]
if len(indexed_tokens) > 1023:
# 模型最長輸入長度為1024,如果長度過長則截斷
indexed_tokens = indexed_tokens[-1023:]
tokens_tensor = torch.tensor([indexed_tokens])
print(total_predicted_text)
從生成結果可以看到,模型已經學習到了戲劇劇本的文本結構。但是仔細讀起來會發現缺少邏輯和關聯,這是因為由於時間和設備的限制,對模型的訓練比較有限。如果有條件可以用更多的數據,訓練更長的時間,這樣模型也會有更好的表現。
總結
本次中,我們首先學習了 GPT-2 的構建思想和它的結構,並觀察了預訓練結果,最后使用了 Fine-Tuning 方法應用 GPT-2 預訓練模型進行了戲劇文本生成任務。
相關鏈接
