写在前面
本文主要是对博客 https://jaykmody.com/blog/gpt-from-scratch/ 的精简整理,并加入了自己的理解。
中文翻译:https://jiqihumanr.github.io/2023/04/13/gpt-from-scratch/#circle=on
项目地址:https://github.com/jaymody/picoGPT
本文最终将用60行代码实现一个GPT。它可以加载OpenAI预训练的GPT-2模型权重,并生成一些文本。 注:本文仅实现了GPT模型的推理(无batch,不能训练)
一、GPT简介
GPT(Generative Pre-trained Transformer)基于Transformer解码器自回归地预测下一个Token,从而进行了语言模型的建模。
只要能够足够好地预测下一个Token,语言模型便可能具备足够地潜力,从而实现人工智能。
以上就是关于GPT和它的能力的一个高层次概述。让我们深入了解更多具体细节。
输入 / 输出
GPT的函数签名大致如下:
def gpt(inputs: list[int]) -> list[list[float]]:
""" GPT代码,实现预测下一个token
inputs:List[int], shape为[n_seq],输入文本序列的token id的列表
output:List[List[int]], shape为[n_seq, n_vocab],预测输出的logits列表
"""
output = # 需要实现的GPT内部计算逻辑
return output
输入
输入是一些由整数表示的文本序列,每个整数都与文本中的token一一对应。例如:
text = "robot must obey orders"
tokens = ["robot", "must", "obey", "orders"]
inputs = [1, 0, 2, 4]
token, 即词元,是文本的子片段,使用某种分词器生成。
分词器将文本分割为不可分割的词元单位,实现文本的高效表示,且方便模型学习文本的结构和语义。
分词器对应一个词汇表,我们可用词汇表将token映射为整数:
# 词汇表中的token索引表示该token的整数ID
# 例如,"robot"的整数ID为1,因为vocab[1] = "robot"
vocab = ["must", "robot", "obey", "the", "orders", "."]
# 一个根据空格进行分词的分词器tokenizer
tokenizer = WhitespaceTokenizer(vocab)
# encode()方法将str字符串转换为list[int]
ids = tokenizer.encode("robot must obey orders") # ids = [1, 0, 2, 4]
# 通过词汇表映射,可以看到实际的token是什么
tokens = [tokenizer.vocab[i] for i in ids] # tokens = ["robot", "must", "obey", "orders"]
# decode()方法将list[int] 转换回str
text = tokenizer.decode(ids) # text = "robot must obey orders"
简而言之:
- 通过语料数据集和分词器tokenizer可以构造一个包含文本中的所有token的词汇表vocab。
- 使用tokenizer将文本text分割为token序列,再使用词汇表vocab将token映射为token id整数,从而得到输入文本token序列。
最后,可以通过vocab将token id序列再转换回文本。
输出
output是一个二维数组,其中output[i][j]表示文本序列的第i个位置的token(inputs[i])是词汇表的第j个token(vocab[j])的概率(实际为未归一化的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",模型预测 "must" 的概率最高
# output[1] = [0.0, 0.0, 0.8, 0.1, 0.0, 0.1]
# 给定序列 ["robot", "must"],模型预测 "obey" 的概率最高
# output[-1] = [0.0, 0.0, 0.1, 0.0, 0.85, 0.05]
# 给定整个序列["robot", "must", "obey"],模型预测 "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模型根据输入,预测序列的下一个token是 "output",因为 output[-1][4]的值为0.85,是词表中最高的一个。
- output[0] 表示给定输入token "robot",模型预测下一个token可能性最高的是"must",为0.75。
- output[-1] 表示给定整个输入序列 ["robot", "must", "obey"],模型预测下一个token是"orders"的可能性最高,为0.85。
为预测序列的下一个token,只需在output的最后一个位置中选择可能性最高的token。那么,通过迭代地将上一轮的输出拼接到输入,并送入模型,从而持续地生成token。
这种生成方式称为贪心采样。实际可以对类别分布用温度系数T进行蒸馏(放大或减小分布的不确定性),并截断类别分布的按top-k,再进行类别分布采样。
具体地,在每次迭代中,将上一轮预测出的token添加到输入末尾,然后预测下一个位置的值,如此往复,就是整个自回归的预测过程:
def generate(inputs, n_tokens_to_generate):
""" GPT生成代码
inputs: list[int], 输入文本的token ids列表
n_tokens_to_generate:int, 需要生成的token数量
"""
# 自回归式解码循环
for _ in range(n_tokens_to_generate):
output = gpt(inputs) # 模型前向推理,输出预测词表大小的logits列表
next_id = np.argmax(output[-1]) # 贪心采样
inputs.append(int(next_id)) # 将预测添加回输入
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结构与实现
2.1 基本组成部分
首先,导入相关可视化函数
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中的非线性激活函数是GELU(高斯误差线性单元),是ReLU的对比的一种替代方法。它由以下函数近似表示:
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)技巧来保持数值稳定性。
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 # 生成不重复的随机数,比较 原始值、原始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())
在输入在合理范围时,两者输出基本相同。
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))
在输入存在异常值时,输出结果比较(原始softmax出现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)是基于特征维度将数据进行标准化(均值为0方差为1),同时乘以缩放系数、加上平移系数,保留其非线性能力:
发表评论
侧栏公告
寄语
譬如朝露博客是一个分享前端知识的网站,联系方式11523518。
热评文章
标签列表
热门文章
友情链接