來收聽極客頭條音頻版吧,智能播報由標貝科技提供技術支持。
「CSDN 極客頭條」,是從 CSDN 網站延伸至官方微信公眾號的特別欄目,專注于一天業界事報道。風里雨里,我們將每天為朋友們,播報最新鮮有料的新聞資訊,讓所有技術人,時刻緊跟業界潮流。
整理 | 胡巍巍
責編 | 郭芮
快訊速知
綠洲因涉嫌抄襲致歉:已臨時下架,啟動內部設計流程核查工作
ZAO回應工信部約談:將加強內容管理,完善各項管理機制
聯通移動回應“三大運營商停售達量限速套餐”:尚未接到通知
有蘋果開發者本月收入翻7倍,人民幣結算變美元或因操作疏忽
蘋果新旗艦名稱再泄露:iPhone 11/11 Pro/11 Pro Max
SK電訊將攜手微軟推出5G云游戲平臺 ,在手機端玩Xbox游戲
Facebook用戶電話號碼數據庫曝光,數據超過4.19億條
Go 1.13發布
Kali Linux 2019.3發布
綠洲因涉嫌抄襲致歉:已臨時下架,啟動內部設計流程核查工作
據報道,“綠洲”App通過官方微博表示,剛了解到有朋友反饋綠洲相關元素設計與韓國某設計團隊作品相似,向設計方表示歉意,也向所有關注綠洲的朋友致歉,并感謝反饋者。
綠洲方面稱,前期設計師確實借鑒了網上公開素材的類似設計元素,為保護版權,現在綠洲已進行臨時下架處理,同時啟動內部設計流程核查工作。(36氪)
華為:搭載鴻蒙OS的筆記本電腦和智能手表將在海外上市
上個月,華為公布了自研操作系統HarmonyOS(鴻蒙OS)。日前,華為在英國倫敦舉辦媒體活動,對外介紹了鴻蒙OS。
華為認為鴻蒙OS操作系統應該需要5到10年才能達到成熟,華為高級全球產品經理Peter Gauden透露,鴻蒙OS在榮耀智慧屏上首發后,登陸海外市場的首款產品預計包括智能手表和筆記本電腦。關于鴻蒙OS筆記本電腦更詳細的內容,Gauden沒有透露更多,但是預計將是采用移動SoC(如高通驍龍)的輕量級產品,能夠與搭載EMUI的華為手機實現很好的交互性。
至于智能手機,華為曾多次表示,雖然HarmonyOS也可以在移動設備上無縫運行,但華為希望堅持使用Android的智能手機。當然,如果華為別無選擇的話,將會在智能手機上搭載HarmonyOS系統。(IT之家)
ZAO回應工信部約談:將加強內容管理,完善各項管理機制
針對工信部約談陌陌,要求其對ZAO App數據安全問題自查整改的消息,ZAO運營團隊回應稱:ZAO上線以來,我們和各主管部門保持著積極順暢的溝通。
非常感謝大家對于新技術新應用的關注,我們也在第一時間回應了大家的核心關切,將嚴格按照法律法規和各主管部門的要求,按照更加嚴格的標準,全面加強內容管理、完善各項管理機制,確保用戶個人信息安全和數據安全。
聯通移動回應“三大運營商停售達量限速套餐”:尚未接到通知
近日,有消息稱,三大電信運營商將于9月1日起暫停達量限速套餐。9月4日,從上海聯通客服處獲悉,目前此類套餐還在正常銷售,未接到停售的通知。另外,上海移動的工作人員也表示,暫時沒有聽說停止銷售該類套餐。目前三大運營商中,只有中國電信明確將停售達量限速套餐。(澎湃)
有蘋果開發者本月收入翻7倍,人民幣結算變美元或因操作疏忽
9月4日上午,有App Store開發者在社交平臺上發文稱,蘋果公司今早在做每月結算時,誤將人民幣金額按照美元幣種匯款,導致他的月收入直接翻了七倍。雖然這筆外匯已經可以申報,但他表示自己并“不敢動”。也有開發者已提款到自己的國內賬戶。
蘋果給國外開發者進行結算時會采用美元計量單位,而在給國內開發者結算時則轉換為人民幣,此次事件應該出于財務轉換幣種時的操作疏忽。(界面)
蘋果新旗艦名稱再泄露:iPhone 11/11 Pro/11 Pro Max
據 macx 報道,一份名為 Apple Software Development Resources 的文件,基本上把秋季新品全都公布了,這份文件原由蘋果員工和工程團隊使用。
據內部文件截圖,蘋果秋季將發布 3 款新 iPhone,分別命名為 iPhone 11、iPhone 11 Pro 和 iPhone 11 Pro Max,搭載 iOS 13.1.0 系統;四款 Apple Watch 新機型將在 9 月 10 日召開的特殊媒體發布會上亮相,均搭載 WatchOS 6 操作系統,watchOS 6.1 或將于 10 月發布;最后是兩款全新 iPad 也將于 10 月發布,兩款 iPad 都會預裝 iPadOS 13。(愛范兒)
谷歌將就YouTube侵犯兒童隱私指控支付1.7億美元罰金
美國聯邦貿易委員會(FTC)周三表示,谷歌旗下YouTube視頻服務將支付1.7億美元罰金就其涉嫌違反兒童隱私法的指控達成和解。FTC稱,這筆罰金是自1998年國會通過《兒童在線隱私保護法》以來,涉案金額最高的罰款。YouTube表示,將完全停止針對兒童內容的個性化廣告服務,并將禁用這些視頻的評論和通知功能。
除了罰款,和解協議還要求谷歌和YouTube“開發、實施和維護一個系統,允許頻道所有者在YouTube平臺上識別他們的兒童導向內容”,以去確保YouTube能夠遵守兒童隱私保護相關的法律。(新浪美股)
SK電訊將攜手微軟推出5G云游戲平臺 ,在手機端玩Xbox游戲
據韓聯社報道,本周三,韓國電信運營商SK電訊表示,該公司將攜手微軟利用5G網絡進行一個新的云游戲平臺的試運行。
這一項目將使游戲玩家今后只要連接到5G網絡,就能夠在無需預先下載內容的情況下實現高質量的游戲體驗。微軟計劃于10月與SK電訊合作推出名為Project xCloud的新平臺的試運行。這一新平臺將使用戶能夠通過智能手機App運行微軟廣受歡迎的Xbox游戲。(品玩)
Facebook用戶電話號碼數據庫曝光,數據超過4.19億條
一個存儲了數以億條與Facebook帳戶關聯的電話號碼數據庫在網上泄露出來,令用戶隱私面臨風險。
這些數據總數超過4.19億條記錄,涵蓋多個地區,其中包括美國Facebook用戶的1.33億條記錄、英國的1800萬用戶記錄以及越南用戶的5000多萬條記錄。由于存儲這些數據的服務器沒有受密碼保護,導致任何人都可以找到并訪問該數據庫,使他們面臨垃圾電話和SIM交換攻擊的風險。這些攻擊通過欺騙手機運營商來獲得一個人的電話號碼,之后便可使用這個電話號碼,強制重置與該號碼關聯的任何互聯網帳戶的密碼。
Facebook發言人表示,在Facebook切斷對用戶電話號碼的訪問之前,數據已被處理。(新浪科技)
Go 1.13發布
主要更新:
數字文法的改進。
錯誤封裝改進
Kali Linux 2019.3發布
主要更新:
Burp Suite、HostAPd-WPE、Hyperion、Kismet 與 Nmap 等經典工具都更新進去了;
proxmark3 客戶端支持開箱即用的 RDV4。
【END】
60行代碼,從頭開始構建GPT?
最近,一位開發者做了一個實踐指南,用Numpy代碼從頭開始實現GPT。
你還可以將 OpenAI發布的GPT-2模型權重加載到構建的GPT中,并生成一些文本。
話不多說,直接開始構建GPT。
什么是GPT?
GPT代表生成式預訓練Transformer,是一種基于Transformer的神經網絡結構。
- 生成式(Generative):GPT生成文本。
- 預訓練(Pre-trained):GPT是根據書本、互聯網等中的大量文本進行訓練的。
- Transformer:GPT是一種僅用于解碼器的Transformer神經網絡。
大模型,如OpenAI的GPT-3、谷歌的LaMDA,以及Cohere的Command XLarge,背后都是GPT。它們的特別之處在于, 1) 非常大(擁有數十億個參數),2) 受過大量數據(數百GB的文本)的訓練。
直白講,GPT會在提示符下生成文本。
即便使用非常簡單的API(輸入=文本,輸出=文本),一個訓練有素的GPT也可以做一些非常棒的事情,比如寫郵件,總結一本書,為Instagram發帖提供想法,給5歲的孩子解釋黑洞,用SQL編寫代碼,甚至寫遺囑。
以上就是 GPT 及其功能的高級概述。讓我們深入了解更多細節。
輸入/輸出
GPT定義輸入和輸出的格式大致如下所示:
def gpt(inputs: list[int]) -> list[list[float]]:
# inputs has shape [n_seq]
# output has shape [n_seq, n_vocab]
output=# beep boop neural network magic
return output
輸入是由映射到文本中的token的一系列整數表示的一些文本:
# integers represent tokens in our text, for example:# text="not all heroes wear capes":# tokens="not" "all" "heroes" "wear" "capes"
inputs=[1, 0, 2, 4, 6]
Token是文本的子片段,使用分詞器生成。我們可以使用詞匯表將token映射到整數:
# the index of a token in the vocab represents the integer id for that token# i.e. the integer id for "heroes" would be 2, since vocab[2]="heroes"
vocab=["all", "not", "heroes", "the", "wear", ".", "capes"]
# a pretend tokenizer that tokenizes on whitespace
tokenizer=WhitespaceTokenizer(vocab)
# the encode() method converts a str -> list[int]
ids=tokenizer.encode("not all heroes wear") # ids=[1, 0, 2, 4]# we can see what the actual tokens are via our vocab mapping
tokens=[tokenizer.vocab[i] for i in ids] # tokens=["not", "all", "heroes", "wear"]# the decode() method converts back a list[int] -> str
text=tokenizer.decode(ids) # text="not all heroes wear"
簡而言之:
- 有一個字符串。
- 使用分詞器將其分解成稱為token的小塊。
- 使用詞匯表將這些token映射為整數。
在實踐中,我們會使用更先進的分詞方法,而不是簡單地用空白來分割,比如字節對編碼(BPE)或WordPiece,但原理是一樣的:
vocab將字符串token映射為整數索引
encode方法,可以轉換str -> list[int]
decode 方法,可以轉換 list[int] -> str ([2])
輸出
輸出是一個二維數組,其中 output[i][j] 是模型預測的概率,即 vocab[j] 處的token是下一個tokeninputs[i+1] 。例如:
vocab=["all", "not", "heroes", "the", "wear", ".", "capes"]
inputs=[1, 0, 2, 4] # "not" "all" "heroes" "wear"
output=gpt(inputs)
# ["all", "not", "heroes", "the", "wear", ".", "capes"]
# output[0]=[0.75 0.1 0.0 0.15 0.0 0.0 0.0 ]
# given just "not", the model predicts the word "all" with the highest probability
# ["all", "not", "heroes", "the", "wear", ".", "capes"]
# output[1]=[0.0 0.0 0.8 0.1 0.0 0.0 0.1 ]
# given the sequence ["not", "all"], the model predicts the word "heroes" with the highest probability
# ["all", "not", "heroes", "the", "wear", ".", "capes"]
# output[-1]=[0.0 0.0 0.0 0.1 0.0 0.05 0.85 ]
# given the whole sequence ["not", "all", "heroes", "wear"], the model predicts the word "capes" with the highest probability
要獲得整個序列的下一個token預測,我們只需獲取 output[-1] 中概率最高的token:
vocab=["all", "not", "heroes", "the", "wear", ".", "capes"]
inputs=[1, 0, 2, 4] # "not" "all" "heroes" "wear"
output=gpt(inputs)
next_token_id=np.argmax(output[-1]) # next_token_id=6
next_token=vocab[next_token_id] # next_token="capes"
將概率最高的token作為我們的預測,稱為貪婪解碼(Greedy Decoding)或貪婪采樣(greedy sampling)。
預測序列中的下一個邏輯詞的任務稱為語言建模。因此,我們可以將GPT稱為語言模型。
生成一個單詞很酷,但整個句子、段落等又如何呢?
生成文本
自回歸
我們可以通過迭代從模型中獲得下一個token預測來生成完整的句子。在每次迭代中,我們將預測的token追加回輸入:
def generate(inputs, n_tokens_to_generate):
for _ in range(n_tokens_to_generate): # auto-regressive decode loop
output=gpt(inputs) # model forward pass
next_id=np.argmax(output[-1]) # greedy sampling
inputs.append(int(next_id)) # append prediction to input
return inputs[len(inputs) - n_tokens_to_generate :] # only return generated ids
input_ids=[1, 0] # "not" "all"
output_ids=generate(input_ids, 3) # output_ids=[2, 4, 6]
output_tokens=[vocab[i] for i in output_ids] # "heroes" "wear" "capes"
這個預測未來值(回歸)并將其添加回輸入(自)的過程,就是為什么你可能會看到GPT被描述為自回歸的原因。
采樣
我們可以從概率分布中采樣,而不是貪婪采樣,從而為生成的引入一些隨機性:
inputs=[1, 0, 2, 4] # "not" "all" "heroes" "wear"
output=gpt(inputs)
np.random.choice(np.arange(vocab_size), p=output[-1]) # capes
np.random.choice(np.arange(vocab_size), p=output[-1]) # hats
np.random.choice(np.arange(vocab_size), p=output[-1]) # capes
np.random.choice(np.arange(vocab_size), p=output[-1]) # capes
np.random.choice(np.arange(vocab_size), p=output[-1]) # pants
這樣,我們就能在輸入相同內容的情況下生成不同的句子。
如果與top-k、top-p和溫度等在采樣前修改分布的技術相結合,我們的輸出質量就會大大提高。
這些技術還引入了一些超參數,我們可以利用它們來獲得不同的生成行為(例如,提高溫度會讓我們的模型承擔更多風險,從而更具「創造性」)。
訓練
我們可以像訓練其他神經網絡一樣,使用梯度下降法訓練GPT,并計算損失函數。對于GPT,我們采用語言建模任務的交叉熵損失:
def lm_loss(inputs: list[int], params) -> float:
# the labels y are just the input shifted 1 to the left
#
# inputs=[not, all, heros, wear, capes]
# x=[not, all, heroes, wear]
# y=[all, heroes, wear, capes]
#
# of course, we don't have a label for inputs[-1], so we exclude it from x
#
# as such, for N inputs, we have N - 1 langauge modeling example pairs
x, y=inputs[:-1], inputs[1:]
# forward pass
# all the predicted next token probability distributions at each position
output=gpt(x, params)
# cross entropy loss
# we take the average over all N-1 examples
loss=np.mean(-np.log(output[y]))
return loss
def train(texts: list[list[str]], params) -> float:
for text in texts:
inputs=tokenizer.encode(text)
loss=lm_loss(inputs, params)
gradients=compute_gradients_via_backpropagation(loss, params)
params=gradient_descent_update_step(gradients, params)
return params
這是一個經過大量簡化的訓練設置,但可以說明問題。
請注意,我們在gpt函數簽名中添加了params (為了簡單起見,我們在前面的章節中沒有添加)。在訓練循環的每一次迭代期間:
- 對于給定的輸入文本實例,計算了語言建模損失
- 損失決定了我們通過反向傳播計算的梯度
- 我們使用梯度來更新我們的模型參數,以使損失最小化(梯度下降)
請注意,我們不使用顯式標記的數據。相反,我們能夠僅從原始文本本身生成輸入/標簽對。這被稱為自監督學習。
自監督使我們能夠大規模擴展訓練數據,只需獲得盡可能多的原始文本并將其投放到模型中。例如,GPT-3接受了來自互聯網和書籍的3000億個文本token的訓練:
當然,你需要一個足夠大的模型才能從所有這些數據中學習,這就是為什么GPT-3有1750億個參數,訓練的計算成本可能在100萬至1000萬美元之間。
這個自監督的訓練步驟被稱為預訓練,因為我們可以重復使用「預訓練」的模型權重來進一步訓練模型的下游任務。預訓練的模型有時也稱為「基礎模型」。
在下游任務上訓練模型稱為微調,因為模型權重已經經過了理解語言的預訓練,只是針對手頭的特定任務進行了微調。
「一般任務的前期訓練+特定任務的微調」策略被稱為遷移學習。
提示
原則上,最初的GPT論文只是關于預訓練Transformer模型用于遷移學習的好處。
論文表明,當對標記數據集進行微調時,預訓練的117M GPT在各種自然語言處理任務中獲得了最先進的性能。
直到GPT-2和GPT-3論文發表后,我們才意識到,基于足夠的數據和參數預訓練的GPT模型,本身能夠執行任何任務,不需要微調。
只需提示模型,執行自回歸語言建模,然后模型就會神奇地給出適當的響應。這就是所謂的「上下文學習」(in-context learning),因為模型只是利用提示的上下文來完成任務。
語境中學習可以是0次、一次或多次。
在給定提示的情況下生成文本也稱為條件生成,因為我們的模型是根據某些輸入生成一些輸出的。
GPT并不局限于NLP任務。
你可以根據你想要的任何條件來微調這個模型。比如,你可以將GPT轉換為聊天機器人(如ChatGPT),方法是以對話歷史為條件。
說到這里,讓我們最后來看看實際的實現。
設置
克隆本教程的存儲庫:
git clone https://github.com/jaymody/picoGPT
cd picoGPT
然后安裝依賴項:
pip install -r requirements.txt
注意:這段代碼是用Python 3.9.10測試的。
每個文件的簡單分類:
- encoder.py包含OpenAI的BPE分詞器的代碼,這些代碼直接取自gpt-2 repo。
- utils.py包含下載和加載GPT-2模型權重、分詞器和超參數的代碼。- gpt2.py包含實際的GPT模型和生成代碼,我們可以將其作為python腳本運行。- gpt2_pico.py與gpt2.py相同,但代碼行數更少。
我們將從頭開始重新實現gpt2.py ,所以讓我們刪除它并將其重新創建為一個空文件:
rm gpt2.py
touch gpt2.py
首先,將以下代碼粘貼到gpt2.py中:
import numpy as np
def gpt2(inputs, wte, wpe, blocks, ln_f, n_head):
pass # TODO: implement this
def generate(inputs, params, n_head, n_tokens_to_generate):
from tqdm import tqdm
for _ in tqdm(range(n_tokens_to_generate), "generating"): # auto-regressive decode loop
logits=gpt2(inputs, **params, n_head=n_head) # model forward pass
next_id=np.argmax(logits[-1]) # greedy sampling
inputs.append(int(next_id)) # append prediction to input
return inputs[len(inputs) - n_tokens_to_generate :] # only return generated ids
def main(prompt: str, n_tokens_to_generate: int=40, model_size: str="124M", models_dir: str="models"):
from utils import load_encoder_hparams_and_params
# load encoder, hparams, and params from the released open-ai gpt-2 files
encoder, hparams, params=load_encoder_hparams_and_params(model_size, models_dir)
# encode the input string using the BPE tokenizer
input_ids=encoder.encode(prompt)
# make sure we are not surpassing the max sequence length of our model
assert len(input_ids) + n_tokens_to_generate < hparams["n_ctx"]
# generate output ids
output_ids=generate(input_ids, params, hparams["n_head"], n_tokens_to_generate)
# decode the ids back into a string
output_text=encoder.decode(output_ids)
return output_text
if __name__=="__main__":
import fire
fire.Fire(main)
將4個部分分別分解為:
- gpt2函數是我們將要實現的實際GPT代碼。你會注意到,除了inputs之外,函數簽名還包括一些額外的內容:
wte、 wpe、 blocks和ln_f是我們模型的參數。
n_head是前向傳遞過程中需要的超參數。
- generate函數是我們前面看到的自回歸解碼算法。為了簡單起見,我們使用貪婪抽樣。tqdm是一個進度條,幫助我們可視化解碼過程,因為它一次生成一個token。
- main函數處理:
加載分詞器(encoder)、模型權重(params)和超參數(hparams)
使用分詞器將輸入提示編碼為token ID
調用生成函數
將輸出ID解碼為字符串
fire.Fire(main)只是將我們的文件轉換為CLI應用程序,因此我們最終可以使用python gpt2.py "some prompt here"運行代碼
讓我們更詳細地了解一下筆記本中的encoder 、 hparams和params,或者在交互式的Python會話中,運行:
from utils import load_encoder_hparams_and_params
encoder, hparams, params=load_encoder_hparams_and_params("124M", "models")
這將把必要的模型和分詞器文件下載到models/124M ,并將encoder、 hparams和params加載到我們的代碼中。
編碼器
encoder是GPT-2使用的BPE分詞器:
ids=encoder.encode("Not all heroes wear capes.")
ids
[3673, 477, 10281, 5806, 1451, 274, 13]
encoder.decode(ids)
"Not all heroes wear capes."
使用分詞器的詞匯表(存儲在encoder.decoder中),我們可以看到實際的token是什么樣子的:
[encoder.decoder[i] for i in ids]
['Not', '?all', '?heroes', '?wear', '?cap', 'es', '.']
請注意,我們的token有時是單詞(例如Not),有時是單詞但前面有空格(例如?all,?表示空格),有時是單詞的一部分(例如Capes分為?cap和es),有時是標點符號(例如.)。
BPE的一個優點是它可以對任意字符串進行編碼。如果它遇到詞匯表中沒有的內容,它只會將其分解為它能夠理解的子字符串:
[encoder.decoder[i] for i in encoder.encode("zjqfl")]
['z', 'j', 'q', 'fl']
我們還可以檢查詞匯表的大小:
len(encoder.decoder)
50257
詞匯表以及確定如何拆分字符串的字節對合并是通過訓練分詞器獲得的。
當我們加載分詞器時,我們從一些文件加載已經訓練好的單詞和字節對合并,當我們運行load_encoder_hparams_and_params時,這些文件與模型文件一起下載。
超參數
hparams是一個包含我們模型的超參數的詞典:
>>> hparams
{
"n_vocab": 50257, # number of tokens in our vocabulary
"n_ctx": 1024, # maximum possible sequence length of the input
"n_embd": 768, # embedding dimension (determines the "width" of the network)
"n_head": 12, # number of attention heads (n_embd must be divisible by n_head)
"n_layer": 12 # number of layers (determines the "depth" of the network)
}
我們將在代碼的注釋中使用這些符號來顯示事物的基本形狀。我們還將使用n_seq表示輸入序列的長度(即n_seq=len(inputs))。
參數
params是一個嵌套的json字典,它保存我們模型的訓練權重。Json的葉節點是NumPy數組。我們會得到:
>>> import numpy as np
>>> def shape_tree(d):
>>> if isinstance(d, np.ndarray):
>>> return list(d.shape)
>>> elif isinstance(d, list):
>>> return [shape_tree(v) for v in d]
>>> elif isinstance(d, dict):
>>> return {k: shape_tree(v) for k, v in d.items()}
>>> else:
>>> ValueError("uh oh")
>>>
>>> print(shape_tree(params))
{
"wpe": [1024, 768],
"wte": [50257, 768],
"ln_f": {"b": [768], "g": [768]},
"blocks": [
{
"attn": {
"c_attn": {"b": [2304], "w": [768, 2304]},
"c_proj": {"b": [768], "w": [768, 768]},
},
"ln_1": {"b": [768], "g": [768]},
"ln_2": {"b": [768], "g": [768]},
"mlp": {
"c_fc": {"b": [3072], "w": [768, 3072]},
"c_proj": {"b": [768], "w": [3072, 768]},
},
},
... # repeat for n_layers
]
}
這些是從原始OpenAI TensorFlow檢查點加載的:
import tensorflow as tf
tf_ckpt_path=tf.train.latest_checkpoint("models/124M")
for name, _ in tf.train.list_variables(tf_ckpt_path):
arr=tf.train.load_variable(tf_ckpt_path, name).squeeze()
print(f"{name}: {arr.shape}")
model/h0/attn/c_attn/b: (2304,)
model/h0/attn/c_attn/w: (768, 2304)
model/h0/attn/c_proj/b: (768,)
model/h0/attn/c_proj/w: (768, 768)
model/h0/ln_1/b: (768,)
model/h0/ln_1/g: (768,)
model/h0/ln_2/b: (768,)
model/h0/ln_2/g: (768,)
model/h0/mlp/c_fc/b: (3072,)
model/h0/mlp/c_fc/w: (768, 3072)
model/h0/mlp/c_proj/b: (768,)
model/h0/mlp/c_proj/w: (3072, 768)
model/h1/attn/c_attn/b: (2304,)
model/h1/attn/c_attn/w: (768, 2304)
...
model/h9/mlp/c_proj/b: (768,)
model/h9/mlp/c_proj/w: (3072, 768)
model/ln_f/b: (768,)
model/ln_f/g: (768,)
model/wpe: (1024, 768)
model/wte: (50257, 768)
下面的代碼將上述TensorFlow變量轉換為我們的params詞典。
作為參考,以下是params的形狀,但用它們所代表的hparams替換了數字:
>>> import tensorflow as tf
>>> tf_ckpt_path=tf.train.latest_checkpoint("models/124M")
>>> for name, _ in tf.train.list_variables(tf_ckpt_path):
>>> arr=tf.train.load_variable(tf_ckpt_path, name).squeeze()
>>> print(f"{name}: {arr.shape}")
model/h0/attn/c_attn/b: (2304,)
model/h0/attn/c_attn/w: (768, 2304)
model/h0/attn/c_proj/b: (768,)
model/h0/attn/c_proj/w: (768, 768)
model/h0/ln_1/b: (768,)
model/h0/ln_1/g: (768,)
model/h0/ln_2/b: (768,)
model/h0/ln_2/g: (768,)
model/h0/mlp/c_fc/b: (3072,)
model/h0/mlp/c_fc/w: (768, 3072)
model/h0/mlp/c_proj/b: (768,)
model/h0/mlp/c_proj/w: (3072, 768)
model/h1/attn/c_attn/b: (2304,)
model/h1/attn/c_attn/w: (768, 2304)
...
model/h9/mlp/c_proj/b: (768,)
model/h9/mlp/c_proj/w: (3072, 768)
model/ln_f/b: (768,)
model/ln_f/g: (768,)
model/wpe: (1024, 768)
model/wte: (50257, 768)
基本層
在我們進入實際的GPT體系結構本身之前,最后一件事是,讓我們實現一些非特定于GPT的更基本的神經網絡層。
GELU
GPT-2選擇的非線性(激活函數)是GELU(高斯誤差線性單元),它是REU的替代方案:
它由以下函數近似表示:
def gelu(x):
return 0.5 * x * (1 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * x**3)))
與RELU類似,Gelu在輸入上按元素操作:
gelu(np.array([[1, 2], [-2, 0.5]]))
array([[ 0.84119, 1.9546 ],
[-0.0454 , 0.34571]])
Softmax
Good ole softmax:
def softmax(x):
exp_x=np.exp(x - np.max(x, axis=-1, keepdims=True))
return exp_x / np.sum(exp_x, axis=-1, keepdims=True)
我們使用max(x)技巧來保證數值穩定性。
SoftMax用于將一組實數(介于?∞和∞之間)轉換為概率(介于0和1之間,所有數字的總和為1)。我們在輸入的最后一個軸上應用softmax 。
x=softmax(np.array([[2, 100], [-5, 0]]))
x
array([[0.00034, 0.99966],
[0.26894, 0.73106]])
x.sum(axis=-1)
array([1., 1.])
層歸一化
層歸一化將值標準化,使其平均值為0,方差為1:
def layer_norm(x, g, b, eps: float=1e-5):
mean=np.mean(x, axis=-1, keepdims=True)
variance=np.var(x, axis=-1, keepdims=True)
x=(x - mean) / np.sqrt(variance + eps) # normalize x to have mean=0 and var=1 over last axisreturn g * x + b # scale and offset with gamma/beta params
層歸一化確保每一層的輸入始終在一致的范圍內,這會加快和穩定訓練過程。
與批處理歸一化一樣,歸一化輸出隨后被縮放,并使用兩個可學習向量gamma和beta進行偏移。分母中的小epsilon項用于避免除以零的誤差。
由于種種原因,Transformer采用分層定額代替批量定額。
我們在輸入的最后一個軸上應用層歸一化。
>>> x=np.array([[2, 2, 3], [-5, 0, 1]])
>>> x=layer_norm(x, g=np.ones(x.shape[-1]), b=np.zeros(x.shape[-1]))
>>> x
array([[-0.70709, -0.70709, 1.41418],
[-1.397 , 0.508 , 0.889 ]])
>>> x.var(axis=-1)
array([0.99996, 1. ]) # floating point shenanigans
>>> x.mean(axis=-1)
array([-0., -0.])
Linear
你的標準矩陣乘法+偏差:
def linear(x, w, b): # [m, in], [in, out], [out] -> [m, out]
return x @ w + b
線性層通常稱為映射(因為它們從一個向量空間映射到另一個向量空間)。
>>> x=np.random.normal(size=(64, 784)) # input dim=784, batch/sequence dim=64
>>> w=np.random.normal(size=(784, 10)) # output dim=10
>>> b=np.random.normal(size=(10,))
>>> x.shape # shape before linear projection
(64, 784)
>>> linear(x, w, b).shape # shape after linear projection
(64, 10)
GPT架構
GPT架構遵循Transformer的架構:
從高層次上講,GPT體系結構有三個部分:
文本+位置嵌入
一種transformer解碼器堆棧
向單詞步驟的映射
在代碼中,它如下所示:
def gpt2(inputs, wte, wpe, blocks, ln_f, n_head): # [n_seq] -> [n_seq, n_vocab]
# token + positional embeddings
x=wte[inputs] + wpe[range(len(inputs))] # [n_seq] -> [n_seq, n_embd]
# forward pass through n_layer transformer blocks
for block in blocks:
x=transformer_block(x, **block, n_head=n_head) # [n_seq, n_embd] -> [n_seq, n_embd]
# projection to vocab
x=layer_norm(x, **ln_f) # [n_seq, n_embd] -> [n_seq, n_embd]
return x @ wte.T # [n_seq, n_embd] -> [n_seq, n_vocab]
把所有放在一起
把所有這些放在一起,我們得到了gpt2.py,它總共只有120行代碼(如果刪除注釋和空格,則為60行)。
我們可以通過以下方式測試我們的實施:
python gpt2.py \"Alan Turing theorized that computers would one day become" \
--n_tokens_to_generate 8
它給出了輸出:
the most powerful machines on the planet.
它成功了!
我們可以使用下面的Dockerfile測試我們的實現與OpenAI官方GPT-2 repo的結果是否一致。
docker build -t "openai-gpt-2" "https://gist.githubusercontent.com/jaymody/9054ca64eeea7fad1b58a185696bb518/raw/Dockerfile"
docker run -dt "openai-gpt-2" --name "openai-gpt-2-app"
docker exec -it "openai-gpt-2-app" /bin/bash -c 'python3 src/interactive_conditional_samples.py --length 8 --model_type 124M --top_k 1'
# paste "Alan Turing theorized that computers would one day become" when prompted
這應該會產生相同的結果:
the most powerful machines on the planet.
下一步呢?
這個實現很酷,但它缺少很多花哨的東西:
將NumPy替換為JAX:
import jax.numpy as np
你現在可以使用代碼與GPU,甚至TPU!只需確保正確安裝了JAX即可。
反向傳播
同樣,如果我們用JAX替換NumPy:
import jax.numpy as np
然后,計算梯度就像以下操作一樣簡單:
def lm_loss(params, inputs, n_head) -> float:
x, y=inputs[:-1], inputs[1:]
output=gpt2(x, **params, n_head=n_head)
loss=np.mean(-np.log(output[y]))return loss
grads=jax.grad(lm_loss)(params, inputs, n_head)
Batching
再一次,如果我們用JAX替換NumPy:
import jax.numpy as np
然后,對gpt2函數進行批處理非常簡單:
gpt2_batched=jax.vmap(gpt2, in_axes=[0, None, None, None, None, None])
gpt2_batched(batched_inputs) # [batch, seq_len] -> [batch, seq_len, vocab]
推理優化
我們的實現效率相當低。你可以進行的最快、最有效的優化(在GPU+批處理支持之外)將是實現KV緩存。
訓練
訓練GPT對于神經網絡來說是相當標準的(梯度下降是損失函數)。
當然,在訓練GPT時,你還需要使用標準的技巧包(例如,使用ADAM優化器、找到最佳學習率、通過輟學和/或權重衰減進行正則化、使用學習率調度器、使用正確的權重初始化、批處理等)。
訓練一個好的GPT模型的真正秘訣是調整數據和模型的能力,這才是真正的挑戰所在。
對于縮放數據,你需要一個大、高質量和多樣化的文本語料庫。
- 大意味著數十億個token(TB級的數據)。
- 高質量意味著您想要過濾掉重復的示例、未格式化的文本、不連貫的文本、垃圾文本等。
- 多樣性意味著不同的序列長度,關于許多不同的主題,來自不同的來源,具有不同的視角等等。
評估
如何評價一個LLM,這是一個很難的問題。
停止生成
當前的實現要求我們提前指定要生成的token的確切數量。這并不是一個好方法,因為我們生成的token最終會過長、過短或在句子中途中斷。
為了解決這個問題,我們可以引入一個特殊的句尾(EOS)標記。
在預訓練期間,我們將EOS token附加到輸入的末尾(即tokens=["not", "all", "heroes", "wear", "capes", ".", "<|EOS|>"])。
在生成期間,只要我們遇到EOS token(或者如果我們達到了某個最大序列長度),就會停止:
def generate(inputs, eos_id, max_seq_len):
prompt_len=len(inputs)while inputs[-1] !=eos_id and len(inputs) < max_seq_len:
output=gpt(inputs)
next_id=np.argmax(output[-1])
inputs.append(int(next_id))return inputs[prompt_len:]
GPT-2沒有預訓練EOS token,所以我們不能在我們的代碼中使用這種方法。
無條件生成
使用我們的模型生成文本需要我們使用提示符對其進行條件調整。
但是,我們也可以讓我們的模型執行無條件生成,即模型在沒有任何輸入提示的情況下生成文本。
這是通過在預訓練期間將特殊的句子開始(BOS)標記附加到輸入開始(即tokens=["<|BOS|>", "not", "all", "heroes", "wear", "capes", "."])來實現的。
然后,要無條件地生成文本,我們輸入一個只包含BOS token的列表:
def generate_unconditioned(bos_id, n_tokens_to_generate):
inputs=[bos_id]for _ in range(n_tokens_to_generate):
output=gpt(inputs)
next_id=np.argmax(output[-1])
inputs.append(int(next_id))return inputs[1:]
GPT-2預訓練了一個BOS token(名稱為<|endoftext|>),因此使用我們的實現無條件生成非常簡單,只需將以下行更改為:
input_ids=encoder.encode(prompt) if prompt else [encoder.encoder["<|endoftext|>"]]
然后運行:
python gpt2.py ""
這將生成:
The first time I saw the new version of the game, I was so excited. I was so excited to see the new version of the game, I was so excited to see the new version
因為我們使用的是貪婪采樣,所以輸出不是很好(重復),而且是確定性的(即,每次我們運行代碼時都是相同的輸出)。為了得到質量更高且不確定的生成,我們需要直接從分布中抽樣(理想情況下,在應用類似top-p的方法之后)。
無條件生成并不是特別有用,但它是展示GPT能力的一種有趣的方式。
微調
我們在訓練部分簡要介紹了微調。回想一下,微調是指當我們重新使用預訓練的權重來訓練模型執行一些下游任務時。我們稱這一過程為遷移學習。
從理論上講,我們可以使用零樣本或少樣本提示,來讓模型完成我們的任務,
然而,如果你可以訪問token的數據集,微調GPT將產生更好的結果(在給定更多數據和更高質量的數據的情況下,結果可以擴展)。
有幾個與微調相關的不同主題,我將它們細分如下:
分類微調
在分類微調中,我們給模型一些文本,并要求它預測它屬于哪一類。
例如,以IMDB數據集為例,它包含將電影評為好或差的電影評論:
--- Example 1 ---
Text: I wouldn't rent this one even on dollar rental night.
Label: Bad
--- Example 2 ---
Text: I don't know why I like this movie so well, but I never get tired of watching it.
Label: Good
--- Example 3 ---
...
為了微調我們的模型,我們將語言建模頭替換為分類頭,并將其應用于最后一個token輸出:
def gpt2(inputs, wte, wpe, blocks, ln_f, cls_head, n_head):
x=wte[inputs] + wpe[range(len(inputs))]
for block in blocks:
x=transformer_block(x, **block, n_head=n_head)
x=layer_norm(x, **ln_f)
# project to n_classes
# [n_embd] @ [n_embd, n_classes] -> [n_classes]
return x[-1] @ cls_head
我們只使用最后一個token輸出x[-1],因為我們只需要為整個輸入生成單一的概率分布,而不是語言建模中的n_seq分布。
尤其,我們采用最后一個token,因為最后一個token是唯一被允許關注整個序列的token,因此具有關于整個輸入文本的信息。
像往常一樣,我們優化了w.r.t.交叉熵損失:
def singe_example_loss_fn(inputs: list[int], label: int, params) -> float:
logits=gpt(inputs, **params)
probs=softmax(logits)
loss=-np.log(probs[label]) # cross entropy loss
return loss
我們還可以通過應用sigmoid而不是softmax來執行多標簽分類,并獲取關于每個類別的二進制交叉熵損失。
生成式微調
有些任務不能被整齊地歸類。例如,總結這項任務。
我們只需對輸入和標簽進行語言建模,就能對這類任務進行微調。例如,下面是一個總結訓練樣本:
--- Article ---
This is an article I would like to summarize.
--- Summary ---
This is the summary.
我們像在預訓練中一樣訓練模型(優化w.r.t語言建模損失)。
在預測時間,我們向模型提供直到--- Summary ---的所有內容,然后執行自回歸語言建模以生成摘要。
分隔符--- Article ---和--- Summary ---的選擇是任意的。如何選擇文本的格式由你自己決定,只要它在訓練和推理之間保持一致。
注意,我們還可以將分類任務制定為生成式任務(例如使用IMDB):
--- Text ---
I wouldn't rent this one even on dollar rental night.
--- Label ---
Bad
指令微調
如今,大多數最先進的大模型在經過預尋來你后,還會經歷額外的指令微調。
在這一步中,模型對數千個人類標記的指令提示+完成對進行了微調(生成)。指令微調也可以稱為有監督的微調,因為數據是人為標記的。
那么,指令微調有什么好處呢?
雖然預測維基百科文章中的下一個單詞能讓模型擅長續寫句子,但這并不能讓它特別擅長遵循指令、進行對話或總結文檔(我們希望GPT能做的所有事情)。
在人類標注的指令+完成對上對其進行微調,是一種教模型如何變得更有用,并使其更易于交互的方法。
這就是所謂的AI對齊,因為我們正在對模型進行對齊,使其按照我們的意愿行事。
參數高效微調
當我們在上述章節中談到微調時,假定我們正在更新所有模型參數。
雖然這能產生最佳性能,但在計算(需要對整個模型進行反向傳播)和存儲(每個微調模型都需要存儲一份全新的參數副本)方面成本高昂。
解決這個問題最簡單的方法就是只更新頭部,凍結(即無法訓練)模型的其他部分。
雖然這可以加快訓練速度,并大大減少新參數的數量,但效果并不是特別好,因為我們失去了深度學習的深度。
相反,我們可以選擇性地凍結特定層,這將有助于恢復深度。這樣做的結果是,效果會好很多,但我們的參數效率會降低很多,也會失去一些訓練速度的提升。
值得一提的是,我們還可以利用參數高效的微調方法。
以Adapters 一文為例。在這種方法中,我們在transformer塊中的FFN和MHA層之后添加一個額外的「適配器」層。
適配層只是一個簡單的兩層全連接神經網絡,輸入輸出維度為 n_embd ,隱含維度小于 n_embd :
隱藏維度的大小是一個超參數,我們可以對其進行設置,從而在參數與性能之間進行權衡。
論文顯示,對于BERT模型,使用這種方法可以將訓練參數的數量減少到2%,而與完全微調相比,性能只受到很小的影響(<1%)。