日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > windows >内容正文

windows

【大语言模型基础】60行Numpy教你实现GPT-原理与代码详解

發(fā)布時(shí)間:2023/12/29 windows 29 coder
生活随笔 收集整理的這篇文章主要介紹了 【大语言模型基础】60行Numpy教你实现GPT-原理与代码详解 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

寫在前面

本文主要是對博客 https://jaykmody.com/blog/gpt-from-scratch/ 的精簡整理,并加入了自己的理解。
中文翻譯:https://jiqihumanr.github.io/2023/04/13/gpt-from-scratch/#circle=on
項(xiàng)目地址:https://github.com/jaymody/picoGPT

本文最終將用60行代碼實(shí)現(xiàn)一個(gè)GPT。它可以加載OpenAI預(yù)訓(xùn)練的GPT-2模型權(quán)重,并生成一些文本。 注:本文僅實(shí)現(xiàn)了GPT模型的推理(無batch,不能訓(xùn)練)

一、GPT簡介

GPT(Generative Pre-trained Transformer)基于Transformer解碼器自回歸地預(yù)測下一個(gè)Token,從而進(jìn)行了語言模型的建模。

只要能夠足夠好地預(yù)測下一個(gè)Token,語言模型便可能具備足夠地潛力,從而實(shí)現(xiàn)人工智能。


以上就是關(guān)于GPT和它的能力的一個(gè)高層次概述。讓我們深入了解更多具體細(xì)節(jié)。

輸入 / 輸出

GPT的函數(shù)簽名大致如下:

def gpt(inputs: list[int]) -> list[list[float]]:
	""" GPT代碼,實(shí)現(xiàn)預(yù)測下一個(gè)token
	inputs:List[int], shape為[n_seq],輸入文本序列的token id的列表
	output:List[List[int]], shape為[n_seq, n_vocab],預(yù)測輸出的logits列表
	"""
    output = # 需要實(shí)現(xiàn)的GPT內(nèi)部計(jì)算邏輯 
    return output
輸入

輸入是一些由整數(shù)表示的文本序列,每個(gè)整數(shù)都與文本中的token一一對應(yīng)。例如:

text  = "robot must obey orders"
tokens = ["robot", "must", "obey", "orders"]
inputs = [1, 0, 2, 4]

token, 即詞元,是文本的子片段,使用某種分詞器生成。

分詞器將文本分割為不可分割的詞元單位,實(shí)現(xiàn)文本的高效表示,且方便模型學(xué)習(xí)文本的結(jié)構(gòu)和語義。

分詞器對應(yīng)一個(gè)詞匯表,我們可用詞匯表將token映射為整數(shù):

# 詞匯表中的token索引表示該token的整數(shù)ID
# 例如,"robot"的整數(shù)ID為1,因?yàn)関ocab[1] = "robot"
vocab = ["must", "robot", "obey", "the", "orders", "."]

# 一個(gè)根據(jù)空格進(jìn)行分詞的分詞器tokenizer
tokenizer = WhitespaceTokenizer(vocab)

# encode()方法將str字符串轉(zhuǎn)換為list[int]
ids = tokenizer.encode("robot must obey orders") # ids = [1, 0, 2, 4]

# 通過詞匯表映射,可以看到實(shí)際的token是什么
tokens = [tokenizer.vocab[i] for i in ids] # tokens = ["robot", "must", "obey", "orders"]

# decode()方法將list[int] 轉(zhuǎn)換回str
text = tokenizer.decode(ids) # text = "robot must obey orders"

簡而言之:

  • 通過語料數(shù)據(jù)集和分詞器tokenizer可以構(gòu)造一個(gè)包含文本中的所有token的詞匯表vocab。
  • 使用tokenizer將文本text分割為token序列,再使用詞匯表vocab將token映射為token id整數(shù),從而得到輸入文本token序列。

最后,可以通過vocab將token id序列再轉(zhuǎn)換回文本。

輸出

output是一個(gè)二維數(shù)組,其中output[i][j]表示文本序列的第i個(gè)位置的token(inputs[i])是詞匯表的第j個(gè)token(vocab[j])的概率(實(shí)際為未歸一化的logits得分)。例如:

inputs = [1, 0, 2, 4]  # "robot" "must" "obey" "orders"
vocab = ["must", "robot", "obey", "the", "orders", "."]
output = gpt(inputs)

# output[0] = [0.75, 0.1, 0.15, 0.0, 0.0, 0.0]
# 給定 "robot",模型預(yù)測 "must" 的概率最高

# output[1] = [0.0, 0.0, 0.8, 0.1, 0.0, 0.1]
# 給定序列 ["robot", "must"],模型預(yù)測 "obey" 的概率最高

# output[-1] = [0.0, 0.0, 0.1, 0.0, 0.85, 0.05]
# 給定整個(gè)序列["robot", "must", "obey"],模型預(yù)測 "orders" 的概率最高
next_token_id = np.argmax(output[-1])  # next_token_id = 4
next_token = vocab[next_token_id]      # next_token = "orders"

在上述例子中,輸入序列為["robot", "must", "obey"],GPT模型根據(jù)輸入,預(yù)測序列的下一個(gè)token是 "output",因?yàn)?output[-1][4]的值為0.85,是詞表中最高的一個(gè)。

  • output[0] 表示給定輸入token "robot",模型預(yù)測下一個(gè)token可能性最高的是"must",為0.75。
  • output[-1] 表示給定整個(gè)輸入序列 ["robot", "must", "obey"],模型預(yù)測下一個(gè)token是"orders"的可能性最高,為0.85。

為預(yù)測序列的下一個(gè)token,只需在output的最后一個(gè)位置中選擇可能性最高的token。那么,通過迭代地將上一輪的輸出拼接到輸入,并送入模型,從而持續(xù)地生成token。

這種生成方式稱為貪心采樣。實(shí)際可以對類別分布用溫度系數(shù)T進(jìn)行蒸餾(放大或減小分布的不確定性),并截?cái)囝悇e分布的按top-k,再進(jìn)行類別分布采樣。

具體地,在每次迭代中,將上一輪預(yù)測出的token添加到輸入末尾,然后預(yù)測下一個(gè)位置的值,如此往復(fù),就是整個(gè)自回歸的預(yù)測過程:

def generate(inputs, n_tokens_to_generate):
	""" GPT生成代碼
	inputs: list[int], 輸入文本的token ids列表
	n_tokens_to_generate:int, 需要生成的token數(shù)量
	"""
    # 自回歸式解碼循環(huán)
    for _ in range(n_tokens_to_generate): 
        output = gpt(inputs)            # 模型前向推理,輸出預(yù)測詞表大小的logits列表
        next_id = np.argmax(output[-1]) # 貪心采樣
        inputs.append(int(next_id))     # 將預(yù)測添加回輸入
    return inputs[len(inputs) - n_tokens_to_generate :]  # 只返回生成的ids

# 隨便舉例
input_ids = [1, 0, 2]                          # ["robot", "must", "obey"]
output_ids = generate(input_ids, 1)            #  output_ids = [1, 0, 2, 4]
output_tokens = [vocab[i] for i in output_ids] # ["robot", "must", "obey", "orders"]

二、GPT結(jié)構(gòu)與實(shí)現(xiàn)

2.1 基本組成部分

首先,導(dǎo)入相關(guān)可視化函數(shù)

import random
import numpy as np
import matplotlib.pyplot as plt

def plot(x, y, x_axis=None, y_axis=None):
    plt.plot(x, y) 
    if x_axis and isinstance(x_axis, tuple):    
        plt.xlim(x_axis[0], x_axis[1])
    if y_axis and isinstance(y_axis, tuple): 
        plt.ylim(y_axis[0], y_axis[1])
    plt.show()

def plotHot(w):
    plt.figure()
    plt.imshow(w, cmap='hot', interpolation='nearest')
    plt.show()
GELU

GPT-2選擇的FFN中的非線性激活函數(shù)是GELU(高斯誤差線性單元),是ReLU的對比的一種替代方法。它由以下函數(shù)近似表示:

def gelu(x):
    return 0.5 * x * (1 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * x**3)))

def relu(x):
    return np.maximum(0, x)

GELU與ReLU的對比

print(gelu(np.array([1, 2, -2, 0.5])))
print(relu(np.array([1, 2, -2, 0.5])))

x = np.linspace(-4, 4, 100) 
plot(x, np.array([gelu(x), relu(x)]).transpose())

Softmax

原始Softmax公式:$$\text{softmax}(x)_i = \frac{e^{x_i}}{\sum_j e^{x_j}}$$

相比原始Softmax, 這里使用了減去最大值max(x)技巧來保持?jǐn)?shù)值穩(wěn)定性。

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)

def rawSoftmax(x):
    exp_x = np.exp(x)
    return exp_x / np.sum(exp_x)
num = 100  # 生成不重復(fù)的隨機(jī)數(shù),比較 原始值、原始softmax和修正后的softmax
numbers = []
for i in range(num):
    number = random.uniform(1, 3)
    while number in numbers:
        number = random.uniform(1, 3)
    numbers.append(number)
plot(np.array(range(num)), np.array([numbers, rawSoftmax(numbers), softmax(numbers)]).transpose()) 


在輸入在合理范圍時(shí),兩者輸出基本相同。

raw_x = np.array([[-200, 100, -300, 0, 70000000]])
x1 = softmax(raw_x)
x2 = rawSoftmax(np.array(raw_x))
print(x1, x1.sum(axis=-1), softmax(x1))
print(x2, x2.sum(axis=-1), softmax(x2))

在輸入存在異常值時(shí),輸出結(jié)果比較(原始softmax出現(xiàn)nan)

[[0. 0. 0. 0. 1.]] [1.] [[0.14884758 0.14884758 0.14884758 0.14884758 0.40460968]]
[[ 0.  0.  0.  0. nan]] [nan] [[nan nan nan nan nan]]
tmp.py:7: RuntimeWarning: overflow encountered in exp exp_x = np.exp(x)
tmp.py:8: RuntimeWarning: invalid value encountered in divide return exp_x / np.sum(exp_x)
層歸一化

層歸一化(Layer Normalization)是基于特征維度將數(shù)據(jù)進(jìn)行標(biāo)準(zhǔn)化(均值為0方差為1),同時(shí)乘以縮放系數(shù)、加上平移系數(shù),保留其非線性能力:

\[\text{LayerNorm}(x) = \gamma \cdot \frac{x - \mu}{{\sigma}} + \beta \]

層歸一化可以有效地緩解優(yōu)化過程中潛在的不穩(wěn)定、收斂速度慢等問題。

def layer_norm(x, g, b, eps: float = 1e-5):
    """ 層歸一化操作
    x: np.array, 輸入
    g: float, 可學(xué)習(xí)的縮放參數(shù) gamma
    b: float, 可學(xué)習(xí)的平移參數(shù) beta
	eps: float, 避免方差為0從而除零的極小值
    """
    mean = np.mean(x, axis=-1, keepdims=True)
    variance = np.var(x, axis=-1, keepdims=True)
    x = (x - mean) / np.sqrt(variance + eps)  # 將x沿著最后一個(gè)軸,進(jìn)行標(biāo)準(zhǔn)化
    return g * x + b                          # 將標(biāo)準(zhǔn)化后的x進(jìn)行重新縮放和平移

可視化例子

num, dim = 5, 5
x = np.array([[random.randint(-10, 10) for _ in range(dim)] for _ in range(num)] )
g, b = 1, 0 # 不縮放和平移
x_norm = layer_norm(x, g, b)
print(x)
print(x_norm)
plotHot(x)
plotHot(x_norm)

輸出結(jié)果

# 層歸一化前
[[ -9   3  -2  -6  -6]
 [-10  -6 -10   8   4]
 [ -1   5  -4  -3  -5]
 [  8   7  -5  -5   9]
 [ 10  -1  -5   3   9]]
 
# 層歸一化后
[[-1.2056067   1.68784939  0.48224268 -0.48224268 -0.48224268]
 [-0.96768591 -0.43008263 -0.96768591  1.45152886  0.91392558]
 [ 0.16876312  1.8563943  -0.67505247 -0.39378061 -0.95632434]
 [ 0.8124999   0.65624992 -1.21874985 -1.21874985  0.96874988]
 [ 1.18444594 -0.73156955 -1.42830246 -0.03483665  1.01026272]]

層歸一化前

層歸一化后(每行數(shù)據(jù)經(jīng)過標(biāo)準(zhǔn)化后,分布差異變小了,從而輸入網(wǎng)絡(luò)的數(shù)據(jù)的分布得到了限制)

通過折線圖可視化(每條折線代表一個(gè)行向量),可以更明顯地看到變化:

axis = np.array(range(x.shape[0]))
plot(axis, x)
plot(axis, x_norm)

層歸一化前

層歸一化后

線性(仿射變換)層

標(biāo)準(zhǔn)的矩陣乘法+偏置:

def linear(x, w, b):  # [m, in], [in, out], [out] -> [m, out]
    return x @ w + b

例子

n_num = 3
in_dim, hid_dim = 4, 4
x = np.random.normal(size=(n_num, in_dim))
w = np.random.normal(size=(in_dim, hid_dim))
b = np.random.normal(size=(hid_dim,))
h = linear(x, w, b)
print(f"shape of w: {w.shape}")
print(f"input shape: {x.shape}, output shape: {h.shape}")
plotHot(w)

shape of w: (4, 4)
input shape: (3, 4), output shape: (3, 4)
權(quán)重可視化

2.2 GPT架構(gòu)

從整體上來看,GPT架構(gòu)分為三個(gè)部分:

  • 嵌入表示層:文本詞元嵌入(token embeddings) + 位置嵌入(positional embeddings)
  • transformer解碼器堆棧:多層decoder block堆疊
  • 預(yù)測:輸出投影回詞匯表(projection to vocab)

代碼層GPT實(shí)現(xiàn)

def gpt2(inputs, wte, wpe, blocks, ln_f, n_head):  
    """ GPT2模型實(shí)現(xiàn)
        輸入輸出tensor形狀: [n_seq] -> [n_seq, n_vocab]
        n_vocab, 詞表大小
        n_seq, 輸入token序列長度
        n_layer, 自注意力編碼器的層數(shù)
        n_embd, 詞表的詞元嵌入大小
        n_ctx, 輸入最大序列長度(位置編碼支持的長度,可用ROPE旋轉(zhuǎn)位置編碼提升外推長度) 
    params:
        inputs: List[int], token ids, 輸入token ids
        wte: np.ndarray[n_vocab, n_embd], token嵌入矩陣 (與輸出分類器共享參數(shù))
        wpe: np.ndarray[n_ctx, n_embd], 位置編碼嵌入矩陣
        blocks:object, n_layer層因果自注意力編碼器
        ln_f:tuple[float], 層歸一化參數(shù)
        n_head:int, 注意力頭數(shù)
    """
    # 1、在詞元嵌入中添加位置編碼信息:token + positional embeddings
    x = wte[inputs] + wpe[range(len(inputs))]  # [n_seq] -> [n_seq, n_embd]

    # 2、前向傳播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]

    # 3、Transformer編碼器塊的輸出投影到詞匯表概率分布上
    # 預(yù)測下個(gè)詞在詞表上的概率分布[ 輸出語言模型的建模的條件概率分布p(x_t|x_t-1 ... x_1) ]
    x = layer_norm(x, **ln_f)  # [n_seq, n_embd] -> [n_seq, n_embd]
    # 就是和嵌入矩陣進(jìn)行內(nèi)積(編碼器塊的輸出相當(dāng)于預(yù)測值,內(nèi)積相當(dāng)于求相似度最大的詞匯)
    return x @ wte.T  # [n_seq, n_embd] -> [n_seq, n_vocab]
嵌入表示層

Token embeddings
wte是一個(gè)[n_vocab, n_embd]可學(xué)習(xí)參數(shù)矩陣,它充當(dāng)一個(gè)token嵌入查找表,其中矩陣的第\(i\)
行對應(yīng)于我們詞匯表中第 \(i\)個(gè)token的embedding。
wte[inputs] 使用整數(shù)數(shù)組索引來檢索與輸入中每個(gè)token對應(yīng)的向量。

Positional embeddings
為了編碼序列的順序信息,通過在輸入表示中添加位置編碼(positional encoding)嵌入來注入位置信息。
位置編碼可以通過學(xué)習(xí)得到也可以直接固定得到。

大小為[n_ctx, n_embd]的wpe即可學(xué)習(xí)的位置嵌入矩陣,其中矩陣的第\(i\)行對應(yīng)輸入序列中第\(i\)個(gè)token的位置embedding,編碼了對應(yīng)的位置信息。

n_ctx代表最大序列長度,限制了模型外推的最大范圍。n_ctx代表最大序列長度,限制了模型外推的最大范圍。

在GPT中,位置嵌入矩陣wpe和token embeddings類似,先隨機(jī)初始化,后通過訓(xùn)練學(xué)習(xí)得到。wpe[inputs] 使用整數(shù)數(shù)組索引inputs來檢索與輸入中每個(gè)token對應(yīng)的位置嵌入。

將token嵌入與位置嵌入聯(lián)合為一個(gè)組合嵌入,這個(gè)嵌入將token信息和位置信息都編碼進(jìn)來了。

Token + Positional embeddings
將Tokene mbeddings與位置嵌入拼接后的嵌入,將token信息和位置信息都編碼進(jìn)來了,它將作為transoformer decoder blocks的實(shí)際輸入。

x = wte[inputs] + wpe[range(len(inputs))]  # [n_seq] -> [n_seq, n_embd]

解碼層

transformer解碼器模塊由兩個(gè)子層組成:

  • 多頭因果自注意力(Multi-head causal self attention)
  • 逐位置前饋神經(jīng)網(wǎng)絡(luò)(Position-wise feed forward neural network)

transformer解碼器中,堆疊了num_layers個(gè)如下的transformer_block:

def transformer_block(x, mlp, attn, ln_1, ln_2, n_head):  
    """ 自注意力編碼器層實(shí)現(xiàn) (只實(shí)現(xiàn)邏輯,各個(gè)子模塊參數(shù)需傳入)
        輸入輸出tensor形狀: [n_seq, n_embd] -> [n_seq, n_embd]
        n_seq, 輸入token序列長度
        n_embd, 詞表的詞元嵌入大小
    params:
        x: np.ndarray[n_seq, n_embd], 輸入token嵌入序列
        mlp: object, 前饋神經(jīng)網(wǎng)絡(luò)
        attn: object, 注意力編碼器層
        ln1: object, 線性層1
        ln2: object, 線性層2
        n_head:int, 注意力頭數(shù)
    """
    # Multi-head Causal Self-Attention (層歸一化 + 多頭自注意力 + 殘差連接 )
    x = x + mha(layer_norm(x, **ln_1), **attn, n_head=n_head)  # [n_seq, n_embd] -> [n_seq, n_embd]

    # Position-wise Feed Forward Network
    x = x + ffn(layer_norm(x, **ln_2), **mlp)  # [n_seq, n_embd] -> [n_seq, n_embd]

    return x

Self-Attention中的層規(guī)一化和殘差連接用于提升訓(xùn)練的穩(wěn)定性。

殘差連接
殘差連接引入輸入直接到輸出的通路,便于梯度回傳從而緩解在優(yōu)化過程中由于網(wǎng)絡(luò)過深引起的梯度消失問題。

\[\mathbf{x}^{l+1} = f(\mathbf{x}^l) + \mathbf{x}^l \]

位置感知的前饋網(wǎng)絡(luò)
對序列中的所有位置的表示進(jìn)行變換時(shí)使用的是同一個(gè)2層隱藏層的MLP,故稱其為position-wise的前饋網(wǎng)絡(luò)(Position-wise Feed Forward Network)。

\[{FFN}(\mathbf x) = Gelu(\mathbf{x} \mathbf{W}_1 + \mathbf{b}_1)\mathbf{W}_2 + \mathbf{b}_2 \]

def ffn(x, c_fc, c_proj):  
    """ 2層前饋神經(jīng)網(wǎng)絡(luò)實(shí)現(xiàn) (只實(shí)現(xiàn)邏輯,各個(gè)子模塊參數(shù)需傳入)
        輸入輸出tensor形狀: [n_seq, n_embd] -> [n_seq, n_embd]
        n_seq, 輸入token序列長度
        n_embd, 詞表的詞元嵌入大小
        n_hid, 隱藏維度
    params:
        x: np.ndarray[n_seq, n_embd], 輸入token嵌入序列
        c_fc: np.ndarray[n_embd, n_hid], 升維投影層參數(shù), 默認(rèn):4*n_embd
        c_proj: np.ndarray[n_hid, n_embd], 降維投影層參數(shù)
    """
    # project up:將n_embd投影到一個(gè)更高的維度 4*n_embd
    a = gelu(linear(x, **c_fc))  # [n_seq, n_embd] -> [n_seq, 4*n_embd]

    # project back down:投影回n_embd
    x = linear(a, **c_proj)  # [n_seq, 4*n_embd] -> [n_seq, n_embd]

    return x

這里僅僅是升維再降維,具體地將n_embd投影到一個(gè)更高的維度4*n_embd,然后再將其投影回n_embd。

多頭因果自注意力
這里將通過分別解釋“多頭因果自注意力”的每個(gè)詞,來一步步理解“多頭因果自注意力”:

  • 注意力(Attention)
  • 自(Self)
  • 因果(Causal)
  • 多頭(Multi-Head)

縮放點(diǎn)積注意力(scaled dot-product attention)

\[\mathbf{H} = \mathrm{softmax}\left(\frac{\mathbf Q \mathbf K^\top }{\sqrtozvdkddzhkzd}\right) \mathbf V \in \mathbb{R}^{T\times d} \]

其中,查詢向量\(\mathbf Q\in\mathbb R^{T\times d}\)、 鍵向量\(\mathbf K \in\mathbb R^{T\times d}\)、值向量\(\mathbf V\in\mathbb R^{T\times d}\)\(T\)為序列長度。

注意力得分除以\(\sqrtozvdkddzhkzd\)進(jìn)行縮放, 是考慮到在\(d\)過大時(shí),點(diǎn)積值較大會(huì)使得后續(xù)Softmax操作溢出導(dǎo)致梯度爆炸,不利于模型優(yōu)化。

def attention_raw(q, k, v):  
    """ 原始縮放點(diǎn)積注意力實(shí)現(xiàn)
        輸入輸出tensor形狀: [n_q, d_k], [n_k, d_k], [n_k, d_v] -> [n_q, d_v]
    params:
        q: np.ndarray[n_seq, n_embd], 查詢向量
        k: np.ndarray[n_seq, n_embd], 鍵向量
        v: np.ndarray[n_seq, n_embd], 值向量
    """
    return softmax(q @ k.T / np.sqrt(q.shape[-1])) @ v

# 以通過對q、k、v進(jìn)行投影變換來增強(qiáng)自注意效果
def self_attention_raw(x, w_k, w_q, w_v, w_proj): 
    """ 自注意力原始實(shí)現(xiàn)
        輸入輸出tensor形狀: [n_seq, n_embd] -> [n_seq, n_embd]
    params:
        x: np.ndarray[n_seq, n_embd], 輸入token嵌入序列
        w_k: np.ndarray[n_embd, n_embd], 查詢向量投影層參數(shù)
        w_q: np.ndarray[n_embd, n_embd], 鍵向量投影層參數(shù)
        w_v: np.ndarray[n_embd, n_embd], 值向量投影層參數(shù)
        w_proj: np.ndarray[n_embd, n_embd], 自注意力輸出投影層參數(shù)
    """
    # qkv projections
    q = x @ w_k # [n_seq, n_embd] @ [n_embd, n_embd] -> [n_seq, n_embd]
    k = x @ w_q # [n_seq, n_embd] @ [n_embd, n_embd] -> [n_seq, n_embd]
    v = x @ w_v # [n_seq, n_embd] @ [n_embd, n_embd] -> [n_seq, n_embd]

    # perform self attention
    x = attention(q, k, v) # [n_seq, n_embd] -> [n_seq, n_embd]

    # out projection
    x = x @ w_proj # [n_seq, n_embd] @ [n_embd, n_embd] -> [n_seq, n_embd]

    return x

# 將w_q、w_k和w_v組合成一個(gè)單獨(dú)的矩陣w_fc,執(zhí)行投影操作,然后拆分結(jié)果,我們就可以將矩陣乘法的數(shù)量從4個(gè)減少到2個(gè)
def self_attention(x, c_attn, c_proj): 
    """ 自注意力優(yōu)化后實(shí)現(xiàn)(w_q 、w_k 、w_v合并成一個(gè)矩陣w_fc進(jìn)行投影,再拆分結(jié)果)
        同時(shí)GPT-2的實(shí)現(xiàn):加入偏置項(xiàng)參數(shù)(所以使用線性層,進(jìn)行仿射變換)
        輸入輸出tensor形狀: [n_seq, n_embd] -> [n_seq, n_embd]
    params:
        x: np.ndarray[n_seq, n_embd], 輸入token嵌入序列
        w_fc: np.ndarray[n_embd, 3*n_embd], 查詢向量投影層參數(shù)
        w_proj: np.ndarray[n_embd, n_embd], 自注意力輸出投影層參數(shù)
    """
    # qkv projections
    x = linear(x, **c_attn) # [n_seq, n_embd] -> [n_seq, 3*n_embd]

    # split into qkv
    q, k, v = np.split(x, 3, axis=-1) # [n_seq, 3*n_embd] -> 3 of [n_seq, n_embd]

    # perform self attention
    x = attention(q, k, v) # [n_seq, n_embd] -> [n_seq, n_embd]

    # out projection
    x = linear(x, **c_proj) # [n_seq, n_embd] @ [n_embd, n_embd] = [n_seq, n_embd]

    return x

因果
為了防止序列建模時(shí)出現(xiàn)信息泄露,需要修改注意力矩陣(增加Mask)以隱藏或屏蔽我們的輸入,從而避免模型在訓(xùn)練階段直接看到后續(xù)的文本序列(信息泄露)進(jìn)而無法得到有效地訓(xùn)練。

# 輸入是 ["not", "all", "heroes", "wear", "capes"] 

# 原始自注意力
        not    all   heroes  wear  capes
   not 0.116  0.159  0.055  0.226  0.443
   all 0.180  0.397  0.142  0.106  0.175
heroes 0.156  0.453  0.028  0.129  0.234
  wear 0.499  0.055  0.133  0.017  0.295
 capes 0.089  0.290  0.240  0.228  0.153

 # 因果自注意力 (行為j, 列為i)
 # 為防止輸入的所有查詢都能預(yù)測未來,需要將所有j>i位置設(shè)置為0 :
        not    all   heroes  wear  capes
   not 0.116  0.     0.     0.     0.
   all 0.180  0.397  0.     0.     0.
heroes 0.156  0.453  0.028  0.     0.
  wear 0.499  0.055  0.133  0.017  0.
 capes 0.089  0.290  0.240  0.228  0.153

 # 在應(yīng)用 softmax 之前,我們需要修改我們的注意力矩陣,得到掩碼自注意力
 # 即,在softmax之前將要屏蔽項(xiàng)的注意力得分設(shè)置為 ?∞(歸一化系數(shù)為0)
 # mask掩碼矩陣
 0 -1e10 -1e10 -1e10 -1e10
 0   0   -1e10 -1e10 -1e10
 0   0     0   -1e10 -1e10
 0   0     0     0   -1e10
 0   0     0     0     0

 使用 -1e10 而不是 -np.inf ,因?yàn)?-np.inf 可能會(huì)導(dǎo)致 nans

加入掩碼矩陣的注意力實(shí)現(xiàn):

def attention(q, k, v, mask):  
    """ 縮放點(diǎn)積注意力實(shí)現(xiàn)
        輸入輸出tensor形狀: [n_q, d_k], [n_k, d_k], [n_k, d_v] -> [n_q, d_v]
    params:
        q: np.ndarray[n_seq, n_embd], 查詢向量
        k: np.ndarray[n_seq, n_embd], 鍵向量
        v: np.ndarray[n_seq, n_embd], 值向量
        mask: np.ndarray[n_seq, n_seq], 注意力掩碼矩陣
    """
    return softmax(q @ k.T / np.sqrt(q.shape[-1]) + mask) @ v

因果注意力掩碼矩陣可視化

x = np.array([1, 1, 1, 1, 1])
causal_mask = (1 - np.tri(x.shape[0], dtype=x.dtype))* -1e10   
print(causal_mask)
plotHot(causal_mask) 
[[-0.e+00 -1.e+10 -1.e+10 -1.e+10 -1.e+10]
 [-0.e+00 -0.e+00 -1.e+10 -1.e+10 -1.e+10]
 [-0.e+00 -0.e+00 -0.e+00 -1.e+10 -1.e+10]
 [-0.e+00 -0.e+00 -0.e+00 -0.e+00 -1.e+10]
 [-0.e+00 -0.e+00 -0.e+00 -0.e+00 -0.e+00]]

注意力可視化

def causal_self_attention(x, c_attn, c_proj): 
    """ 因果自注意力優(yōu)化后實(shí)現(xiàn)(w_q 、w_k 、w_v合并成一個(gè)矩陣w_fc進(jìn)行投影,再拆分結(jié)果)
        同時(shí)GPT-2的實(shí)現(xiàn):加入偏置項(xiàng)參數(shù)(所以使用線性層,進(jìn)行仿射變換)
        輸入輸出tensor形狀: [n_seq, n_embd] -> [n_seq, n_embd]
    params:
        x: np.ndarray[n_seq, n_embd], 輸入token嵌入序列
        c_attn: np.ndarray[n_embd, 3*n_embd], 查詢向量投影層參數(shù)
        c_proj: np.ndarray[n_embd, n_embd], 自注意力輸出投影層參數(shù)
    """
    # qkv projections
    x = linear(x, **c_attn) # [n_seq, n_embd] -> [n_seq, 3*n_embd]

    # split into qkv
    q, k, v = np.split(x, 3, axis=-1) # [n_seq, 3*n_embd] -> 3 of [n_seq, n_embd]

    # causal mask to hide future inputs from being attended to
    causal_mask = (1 - np.tri(x.shape[0], dtype=x.dtype))* -1e10   # [n_seq, n_seq]

    # perform causal self attention
    x = attention(q, k, v, causal_mask) # [n_seq, n_embd] -> [n_seq, n_embd]

    # out projection
    x = linear(x, **c_proj) # [n_seq, n_embd] @ [n_embd, n_embd] = [n_seq, n_embd]

    return x

實(shí)際,用-1e10替換-np.inf, 因?yàn)?np.inf會(huì)導(dǎo)致nans錯(cuò)誤。

多頭自注意力(Multi-Head-self-Attention)

def mha(x, c_attn, c_proj, n_head):
    """ 多頭自注意力實(shí)現(xiàn)
        輸入輸出tensor形狀: [n_seq, n_embd] -> [n_seq, n_embd]
        每個(gè)注意力計(jì)算的維度從n_embd降低到 n_embd/n_head。
        通過降低維度,模型利用多個(gè)子空間進(jìn)行建模
    params:
        x: np.ndarray[n_seq, n_embd], 輸入token嵌入序列
        c_attn: np.ndarray[n_embd, 3*n_embd], 查詢向量投影層參數(shù)
        c_proj: np.ndarray[n_embd, n_embd], 自注意力輸出投影層參數(shù)
    """  
    # qkv投影變換
    x = linear(x, **c_attn)  # [n_seq, n_embd] -> [n_seq, 3*n_embd]

    # 劃分為qkv
    qkv = np.split(x, 3, axis=-1)  # [n_seq, 3*n_embd] -> [3, n_seq, n_embd]

    # 將n_embd繼續(xù)劃分為_head個(gè)注意力頭
    qkv_heads = list(map(lambda x: np.split(x, n_head, axis=-1), qkv))  # [3, n_seq, n_embd] -> [3, n_head, n_seq, n_embd/n_head]

    # 構(gòu)造causal mask矩陣
    causal_mask = (1 - np.tri(x.shape[0], dtype=x.dtype))* -1e10  # [n_seq, n_seq]

    # 單獨(dú)執(zhí)行每個(gè)頭的因果自注意力(可多核多線程并行執(zhí)行)
    out_heads = [attention(q, k, v, causal_mask) for q, k, v in zip(*qkv_heads)]  # [3, n_head, n_seq, n_embd/n_head] -> [n_head, n_seq, n_embd/n_head]

    # 合并多個(gè)heads的結(jié)果
    x = np.hstack(out_heads)  # [n_head, n_seq, n_embd/n_head] -> [n_seq, n_embd]

    # 多頭因果自注意力輸出projection
    x = linear(x, **c_proj)  # [n_seq, n_embd] -> [n_seq, n_embd]

    return x

將所有代碼組合起來
將所有代碼組合起來就得到了gpt2.py,總共的代碼只有120行(如果你移除注釋、空格之類的,那就只有60行)。

二、項(xiàng)目實(shí)戰(zhàn)

可以通過以下代碼測試:

python gpt2.py "Alan Turing theorized that computers would one day become" --n_tokens_to_generate 8

其輸出是:the most powerful machines on the planet.

ToDO

參考鏈接

【1】配圖部分來自,https://jalammar.github.io/illustrated-gpt2/

總結(jié)

以上是生活随笔為你收集整理的【大语言模型基础】60行Numpy教你实现GPT-原理与代码详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。