關(guān)鍵詞:GPT
,Transformer
內(nèi)容摘要
- GPT的背景來源和發(fā)展簡述
- GPT的自回歸工作方式
- 圖解GPT-2的網(wǎng)絡(luò)結(jié)構(gòu)
- GPT的解碼采樣策略
- minGPT源碼分析和文本生成實戰(zhàn)
GPT的背景來源和發(fā)展簡述
GPT(Generative Pre-Trained Transformer,生成式預訓練Transformer模型),它是基于Transformer的Decoder解碼器在海量文本上訓練得到的預訓練模型。GPT采用自回歸的工作方式,能夠查看句子的一部分并且預測下一個單詞,不斷重復這個過程來生成連貫且適當上下文文本颓芭。
GPT的誕生早于Bert,相比于Bert針對的任務(wù)通常是文本分類柬赐、實體識別亡问、完型填空, GPT這種完全自行生成類似人類語言文本的任務(wù)更具挑戰(zhàn)性肛宋,是強人工智能的體現(xiàn)州藕,ChatGPT就是在GPT技術(shù)的基礎(chǔ)之上開發(fā)的交互式對話版本。
GPT的自回歸工作方式
給定一個前句酝陈,將它作為問題或者引導詞輸入給GPT床玻,GPT基于這個句子預測一個單詞,同時預測出的詞會加入到原始句子的結(jié)尾合并成一個新的句子作為形成下一次的輸入沉帮,這種工作方式被成為自回歸性锈死,如下圖所示
在訓練階段GPT采用對文本進行shifted-right右移一位的方式構(gòu)造出輸入和輸出,在預測推理階段采用自回歸的方式通過持續(xù)地更新上下文窗口中的輸入來源源不斷的預測下去穆壕。
圖解GPT-2的網(wǎng)絡(luò)結(jié)構(gòu)
GPT系列自O(shè)penAI在2018年發(fā)布的第一代GPT-1開始待牵,以及后的GPT-2,GPT-3喇勋,他們的區(qū)別主要在于預訓練數(shù)據(jù)和模型參數(shù)的規(guī)模在不斷擴大缨该,以及訓練策略優(yōu)化等等,而模型網(wǎng)絡(luò)結(jié)構(gòu)并沒有大的改變川背,都是以堆疊Transformer的解碼器Decoder作為基座贰拿。
模型 | 發(fā)布時間 | 層數(shù) | 頭數(shù) | 詞向量長度 | 參數(shù)量 | 預訓練數(shù)據(jù)量 |
---|---|---|---|---|---|---|
GPT-1 | 2018年6月 | 12 | 12 | 768 | 1.17億 | 5GB |
GPT-2 | 2019年2月 | 48 | 25 | 1600 | 15億 | 40GB |
GPT-3 | 2020年5月 | 96 | 96 | 12888 | 1750億 | 45TB |
本篇采用GPT-2作為GPT系列網(wǎng)路結(jié)構(gòu)的說明。
上圖所示左側(cè)為GPT-1結(jié)構(gòu)渗常,右側(cè)為GPT-2結(jié)構(gòu)壮不,堆疊了12層Transformer的Decoder解碼器汗盘,GPT-2略微修改了Layer Norm層歸一化的位置皱碘,輸入層對上下文窗口長度大小(block size)的文本做建模隐孽,使用token embedding+位置編碼癌椿,輸出層獲取解碼向量信息健蕊,最后一個block對應的向量為下一個詞的信息表征,可以對該向量做分類任務(wù)完成序列的生成踢俄。
GPT-2本質(zhì)上沿用了Transformer的解碼器Self Attention機制缩功,在堆疊的每一層悼粮,當前時刻信息的表征嗤堰,只能使用當前詞和當前詞之前的信息,不能像Bert那樣既能看到左側(cè)的詞又能看到右側(cè)的詞檬寂,因為GPT是文本生成任務(wù)琳钉,而Bert是給定了全文的基礎(chǔ)上進行語意理解势木,兩者區(qū)別如下圖所示
GPT的解碼采樣策略
GPT的堆疊Decoder結(jié)構(gòu)輸出的最后一個block的embedding,再結(jié)合Linear線性層和Softmax層映射到全部詞表歌懒,即可獲得下一個單詞的概率分布啦桌,GPT的解碼過程就是基于概率分布來確定下一個單詞是誰,顯然得分高的更應該被作為預測結(jié)果及皂。GPT常用的解碼策略有貪婪采樣和多項式采樣
- 貪婪采樣:在每一步選擇概率最高的單詞作為結(jié)果甫男。這種方法簡單高效,但是可能會導致生成的文本過于單調(diào)和重復
- 多項式采樣:在每一步根據(jù)概率分布作為權(quán)重验烧,隨機選擇單詞板驳。這種方法可以增加生成的多樣性,但是可能會導致生成的文本不連貫和無意義噪窘,出現(xiàn)比較離譜的生成結(jié)果
用PyTorch模擬一個基于多項式采樣的例子如下
>>> import torch
>>> weights = torch.Tensor([0, 1, 2, 3, 4, 5])
>>> cnt = {}
>>> for i in range(10000):
>>> res = torch.multinomial(weights, 1).item()
>>> cnt[res] = cnt.get(aaa, 0) + 1
>>> cnt
{5: 3302, 2: 1307, 3: 1994, 1: 707, 4: 2690}
采用torch.multinomial模擬10000次采樣笋庄,記錄下最終采樣的結(jié)果,結(jié)論是被采樣到的概率基本等于該單詞的權(quán)重占全部權(quán)重的比例倔监。
通常GPT采用多項式采樣結(jié)合Temperature溫度系數(shù)直砂,Top-K,Top-P來一起完成聯(lián)合采樣浩习。
Temperature溫度系數(shù)
溫度系數(shù)是一個大于0的值静暂,溫度系數(shù)會對概率得分分布做統(tǒng)一的縮放,具體做法是在softmax之前對原始得分除以溫度系數(shù)谱秽,公式如下
當溫度系數(shù)大于1時洽蛀,高得分單詞和低得分單詞的差距被縮小,導致多項式采樣的傾向更加均勻化疟赊,結(jié)果是生成的內(nèi)容多樣性更高郊供。當溫度系數(shù)小于1時,高得分單詞和低得分單詞的差距被放大近哟,導致多項式采樣的更加傾向于選擇得分高的單詞驮审,生成的內(nèi)容確定性更高。默認溫度系數(shù)等于1,此時等同于使用原始的概率分布不做任何縮放疯淫。地来。
使用PyTorch模擬溫度系數(shù)分別為2和0.5,結(jié)果如下
>>> torch.softmax(torch.tensor([1.0, 2.0, 3.0, 4.0]), -1)
tensor([0.0321, 0.0871, 0.2369, 0.6439])
>>> # 溫度系數(shù)為2
>>> torch.softmax(torch.tensor([1.0, 2.0, 3.0, 4.0]) / 2, -1)
tensor([0.1015, 0.1674, 0.2760, 0.4551])
>>> # 溫度系數(shù)為0.5
>>> torch.softmax(torch.tensor([1.0, 2.0, 3.0, 4.0]) / 0.5, -1)
tensor([0.0021, 0.0158, 0.1171, 0.8650])
顯然溫度系數(shù)越高為2時抽樣概率越平均熙掺,溫度系數(shù)越低為0.5時抽樣概率越極端未斑,對高分的頂部詞更加強化。
Top-K
Top-K指的是將詞表中所有單詞根據(jù)概率分布從高到低排序币绩,選取前K個作為采樣候選蜡秽,剩下的其他單詞永遠不會被采樣到,這樣做是可以將過于離譜的單詞直接排除在外缆镣,一定程度上提高了多項式采樣策略的準確性载城。同樣的k越大,生成的多樣性越高费就,k 越小诉瓦,生成的質(zhì)量一般越高。
Top-P
Top-P和Top-K類似力细,選取頭部單詞概率累加到閾值P截止睬澡,剩余的長尾單詞全部丟棄,例如P=95%眠蚂,單詞A的概率為0.85煞聪,單詞B的概率為0.11,頭部的兩個詞概率相加已經(jīng)達到95%逝慧,因此該步的采樣候選只有單詞A和單詞B昔脯,其他詞不會被采樣到。對位概率分布長尾的情況笛臣,Top-P相比于Top-K更加可以避免抽樣到一些概率過低的不相關(guān)的詞云稚。
minGPT源碼分析和文本生成實戰(zhàn)
minGPT項目是基于PyTorch實現(xiàn)的GPT-2,它包含GPT-2的訓練和推理沈堡,本篇以minGPT的chargpt例子作源碼分析静陈。chargpt是訓練用戶自定義的語料,訓練完成后基于用戶給到的文本可以實現(xiàn)自動續(xù)先诞丽,本例采用電視劇《狂飆》的部分章節(jié)作為語料灌給GPT-2進行訓練鲸拥。
模型參數(shù)概覽
定位到chargpt.py,首先作者定義了數(shù)據(jù)和模型相關(guān)的參數(shù)配置僧免,部分關(guān)鍵配置信息如下
data:
block_size: 128
model:
model_type: gpt-mini
n_layer: 6
n_head: 6
n_embd: 192
vocab_size: 2121
block_size: 128
embd_pdrop: 0.1
resid_pdrop: 0.1
attn_pdrop: 0.1
trainer:
num_workers: 4
max_iters: None
batch_size: 64
learning_rate: 0.0005
betas: (0.9, 0.95)
weight_decay: 0.1
grad_norm_clip: 1.0
- block_size:上下文窗口長度刑赶,也是輸入的最大文本長度為128
- model_type:gpt模型類型,這里采用gpt-mini懂衩,一個小規(guī)模參數(shù)的gpt撞叨,它包含6層Decoder呛伴,6個自注意力頭,詞向量的embedding維度為192
- num_workers:DataLoader加載數(shù)據(jù)的子進程數(shù)量谒所,可以加快訓練速度,對模型結(jié)果沒有影響
訓練數(shù)據(jù)構(gòu)造
需要用戶自己指定一個input.txt沛申,其內(nèi)容為合并了所有文本而成的一行字符串
text = open('input.txt', 'r').read()
train_dataset = CharDataset(config.data, text)
CharDataset基于PyTorch的Dataset構(gòu)造劣领,len和getitem實現(xiàn)如下
def __len__(self):
return len(self.data) - self.config.block_size
def __getitem__(self, idx):
# grab a chunk of (block_size + 1) characters from the data
chunk = self.data[idx:idx + self.config.block_size + 1]
# encode every character to an integer
dix = [self.stoi[s] for s in chunk]
# return as tensors
x = torch.tensor(dix[:-1], dtype=torch.long)
y = torch.tensor(dix[1:], dtype=torch.long)
return x, y
采用上下文窗口在原文本上從前滑動到最后,生成文本長度減去上下文窗口大小的實例作為len铁材,getitem部分實現(xiàn)訓練的輸入x和目標y尖淘,每次選取129個連續(xù)的字符作為一個塊,然后以前128個作為x著觉,后128個作為y村生,實現(xiàn)shifted right。
gpt模型構(gòu)建
作者將配置參數(shù)傳遞給GPT類實例化一個GPT網(wǎng)絡(luò)
model = GPT(config.model)
在GPT類中實現(xiàn)了網(wǎng)路的各個元素饼丘,包括token embedding趁桃,位置編碼,Decoder肄鸽,Decoder結(jié)束后的層歸一化卫病,以及最后的概率分布線性映射層
self.transformer = nn.ModuleDict(dict(
# TODO 輸入的embedding [10, 48]
wte=nn.Embedding(config.vocab_size, config.n_embd),
# TODO 位置編碼,block_size是上下文窗口 [6, 48]
wpe=nn.Embedding(config.block_size, config.n_embd),
drop=nn.Dropout(config.embd_pdrop), # 0.1
# TODO 隱藏層典徘,6層transformer
h=nn.ModuleList([Block(config) for _ in range(config.n_layer)]),
ln_f=nn.LayerNorm(config.n_embd),
))
# TODO 線性層輸出概率分布
self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
其中定義h為一個包含6層的堆疊Decoder蟀苛,Block實現(xiàn)了其中每一個Decoder單元,內(nèi)部包括點積注意力層逮诲,層歸一化帜平,前饋傳播層和殘差連接,實現(xiàn)如下
class Block(nn.Module):
def __init__(self, config):
super().__init__()
self.ln_1 = nn.LayerNorm(config.n_embd)
# TODO 自回歸模型中的自注意力層
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
# TODO feed forward層
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
forward中順序有所不同梅鹦,GPT-2中將層歸一化提前到了注意力操作之前裆甩。
核心的點積注意力在CausalSelfAttention中實現(xiàn),該類自己維護了下三角矩陣用于對后面的詞進行mask齐唆。
class CausalSelfAttention(nn.Module):
def __init__(self, config):
...
self.register_buffer("bias", torch.tril(torch.ones(config.block_size, config.block_size))
.view(1, 1, config.block_size, config.block_size))
def forward(self, x):
B, T, C = x.size()
...
att = att.masked_fill(self.bias[:, :, :T, :T] == 0, float('-inf'))
作者先定義了一個基于block_size全局最大128×128的下三角矩陣bias淑掌,而實際在使用時,根據(jù)輸入文本的實際長度T從左上角開始取對應T×T形狀蝶念,無論T多長bias始終是下三角矩陣抛腕,接著將注意力矩陣上三角部分全部置為-inf,從而使得所有后詞信息被隔離媒殉。這個下三角掩碼不論輸入句子長度多長担敌,并且在每一層堆疊的Decoder上都會生效。
在網(wǎng)絡(luò)搭建好后廷蓉,作者構(gòu)建輸入層全封,它是由token embedding和位置編碼相加而成
b, t = idx.size()
pos = torch.arange(0, t, dtype=torch.long, device=device).unsqueeze(0)
tok_emb = self.transformer.wte(idx) # token embeddings
pos_emb = self.transformer.wpe(pos) # position embeddings
x = self.transformer.drop(tok_emb + pos_emb)
緊接著輸入到由6層Decode組成的隱層h马昙,GPT-2在Decoder結(jié)束之后又加了層歸一化ln_f,最終到線性映射層lm_head輸出預測到詞表的得分分布
for block in self.transformer.h:
x = block(x)
x = self.transformer.ln_f(x)
logits = self.lm_head(x)
損失函數(shù)
損失采用每個預測位置的多分類交叉熵刹悴,一個批次下將所有樣本合并拉直行楞,最終所有位置的交叉熵取平均值作為最終損失
loss = None
if targets is not None:
# TODO logits => [batch_size * seq_len, 10], target => [batch_size * seq_len, ]
# TODO 該批次下 每條樣本土匀,每個位置下預測的多分類交叉熵損失 的平均值
loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-1)
模型推理
在模型推理階段使用generate類方法子房,該方法定義了最大推理步長,溫度系數(shù)等參數(shù)就轧。作者給了個例子证杭,輸入一句話,對其進行自回歸預測下一個單詞妒御,其中最大預測100步解愤,溫度系數(shù)為1.0,采用多項式采樣乎莉,限制Top-K為10
with torch.no_grad():
context = "高啟強被捕之后"
# TODO [seq_len, ] => [batch_size, seq_len]
x = torch.tensor([train_dataset.stoi[s] for s in context], dtype=torch.long)[None, ...].to(trainer.device)
y = model.generate(x, 100, temperature=1.0, do_sample=True, top_k=10)[0]
completion = ''.join([train_dataset.itos[int(i)] for i in y])
跟進generate方法查看源碼實現(xiàn)
def generate(self, idx, max_new_tokens, temperature=1.0, do_sample=False, top_k=None):
# TODO idx => [batch_size, 4]
for _ in range(max_new_tokens):
# TODO idx_cond不定長送讲,只要低于最大上下文窗口即可, 如果文本超過block上下文窗口(輸入序列最大長度),向前截取惋啃,取最后和block等長的
idx_cond = idx if idx.size(1) <= self.block_size else idx[:, -self.block_size:]
# TODO [batch_size, seq_len, 10]
logits, _ = self(idx_cond)
# TODO [batch_size, 10] 拿到最后一個block位置的作為下一個單詞的概率得分分布
# TODO temperature 溫度系數(shù), 默認為1,不對分數(shù)做任何縮放
# TODO 溫度系數(shù)越大李茫,得分差距縮短,多樣性更大肥橙,溫度系數(shù)越小魄宏,得分差距加大,多樣性更低
logits = logits[:, -1, :] / temperature
# TODO 只選取得分最大的topk個
if top_k is not None:
# TODO 輸出最大值和最大值的索引
# TODO v => [batch_size, topk]
v, _ = torch.topk(logits, top_k)
# TODO v[:, [-1]] => [batch_size, 1], topk的最小值, 將沒有進入topk的全部改為-inf
logits[logits < v[:, [-1]]] = -float('Inf')
# TODO [batch_size, 10] 經(jīng)過溫度系數(shù)和topk壓縮之后使用softmax轉(zhuǎn)化為概率
probs = F.softmax(logits, dim=-1)
if do_sample:
# TODO 根據(jù)概率作為權(quán)重采樣其中一個存筏,概率越大被采樣到的概率越大
# TODO 溫度系數(shù)的大小會直接影響這個地方的采樣權(quán)重,溫度系數(shù)決定了在topk范圍內(nèi)的采樣多樣性
# TODO topK會把沒有進入topk的softmax概率轉(zhuǎn)化為0,使得永遠不能被采樣到, topk決定了哪些會被采樣到
# TODO idx_next => [batch_size, 1]
idx_next = torch.multinomial(probs, num_samples=1)
else:
# TODO 貪婪搜索 [batch_size, 1]
_, idx_next = torch.topk(probs, k=1, dim=-1)
# TODO 將預測的新單詞拼接到原始輸入上作為新的輸入
idx = torch.cat((idx, idx_next), dim=1)
return idx
其中idx為自回歸維護的輸入宠互,每次預測完下一個單詞拼接到結(jié)尾覆蓋更新idx
idx = torch.cat((idx, idx_next), dim=1)
在每次推理過程中只使用上下文窗口中最后一個block的輸出作為下個單詞的推理依據(jù)
logits = logits[:, -1, :] / temperature
在采樣策略順序上,先進行溫度系數(shù)縮放椭坚,再進行top_k篩選予跌,最后通過softmax壓縮為概率,最終采用torch.multinomial進行多項式采樣善茎,top_k的實現(xiàn)是將所有非top_k的位置得分置為-inf券册,使得softmax壓縮為0,只要存在其他非0權(quán)重垂涯,多項式采樣永遠不會將0采樣出來烁焙,通過這種方法將非top_k在解碼中剔除。
模型訓練
基于《狂飆》語料灌入定義好的gpt-mini耕赘,每迭代500詞打印一次模型推理的結(jié)果骄蝇,我們給到例句“高啟強被捕之后”,查看隨著損失的收斂操骡,模型預測接下來50個詞的結(jié)果
迭代次數(shù) | 損失 | 生成結(jié)果 |
---|---|---|
0 | 7.696 | 高啟強被捕之后操”徐”九火,姓”赚窃,越,尬””越上”越徐噎曲噎噎的貓上建驗建岔激,姓”越拼越津噎勒极,”建,虑鼎,的辱匿,,震叙,上”上”越 |
1000 | 0.671 | 高啟強被捕之后,高啟強看著安靜地洗得很好的攤子散休。音在順道我們送禮物就去躲到了解決媒楼。門診大夫都準備,你再幫忙戚丸!”“不 |
2000 | 0.288 | 高啟強被捕之后的遺照片的下醫(yī)院常車牌划址,刑警隊員禮貌似乎跑斷。耳機里的現(xiàn)場館情不倫次限府,雖然快地想立即決定:將自己在將 |
3000 | 0.200 | 高啟強被捕之后夺颤,安欣也十分爽快地答應幫忙——只要高家兄弟拿出三萬元。正在高啟強一籌莫展的時候胁勺,唐小龍介紹了一個門道 |
7500 | 0.107 | 高啟強被捕之后世澜,高啟強終于弄明白了是怎么回事。原來唐家兄弟自從上次見到安欣和李響之后署穗,便四處打聽安欣的身份寥裂,確定了 |
8500 | 0.095 | 高啟強被捕之后隨即打開懷里的包,點出一本厚的法醫(yī)學專著擺在桌上案疲,得意地指了指封恰,“看看『址龋”安欣一怔:“你怎么看這個诺舔? |
隨著損失的收斂,生成結(jié)果逐漸變得連貫备畦,尤其最后一句從語法和語義上基本挑不出什么問題低飒,也預測出了訓練集中的部分原文。此處只是一個例子在給定文本上從頭訓練一個簡單的小規(guī)模gpt網(wǎng)絡(luò)懂盐,如果期望更好的效果需要基于更大規(guī)模數(shù)據(jù)的預訓練gpt模型逸嘀。