探秘Transformer系列之(15)--- 采样和输出
目录
- 探秘Transformer系列之(15)--- 采样和输出
- 0x00 概述
- 0x01 Generator
- 1.1 Linear
- 1.2 softmax
- 1.3 实现
- 1.4 使用
- 0x02 采样
- 2.1 采样方法
- 2.2 贪心解码
- 2.3 Beam(束搜索)
- 2.4 top-k
- 2.5 top-p
- 2.6 性能
- 0x03 采样参数
- 3.1 temperature
- 概念
- 动态温度系数
- KL-Divergence Guided Temperature Sampling
- Hot or Cold
- EDT
- 3.2 repetition_penalty
- 0x04 logits分析
- 4.1 压缩信息
- 4.2 变化
- 4.3 预处理logits
- 4.2 隐式思维链
- pause tokens/Filler Token
- CoT
- coconut
- 4.3 基于熵的采样
- 0x05 权重共享
- 5.1 vanilla Transformer
- 5.2 共享词表权重
- 5.2 FC和embedding共享
- 0xFF 参考
0x00 概述
解码器包括很多Transformer层,每一层的最后部分是"Add & Norm",其实也就是说,编码器的最后一层的最后一个模块是一个"Add & Norm"。该模块的输出是一个代表语义的浮点型向量。我们目前遇到的问题是:如何把这个浮点向量转换成一个词?这就是采样和输出部分所做的工作。简要来说,在预测阶段,采样和输出部分会执行下面三步:
- 计算概率。在解码器层输出结果后,需要经过Generator线性层进行最后的预测,Generator线性层就是个标准的分类网络。Generator线性层会把最后一个token对应的特征向量通过一个线性层升维到词表维度,并且把升维后的新向量通过softmax进行归一化,最终输出一个概率分布(每个概率对应词汇表中的一个token)。该分布表示词表中每个词匹配这个特征向量的概率,或者说是表示词表中每个 token 作为下一个 token 的概率。该部分实际是一个分类网络。
- 采样。根据这个概率分布来指导采样,找出最大概率对应的词表index。
- 生词。依据index从词表中选择下一个 token 作为最终输出。
下图展示了上述流程:从底部以解码器组件产生的输出向量开始,最终转化出一个输出单词。
0x01 Generator
Transformer为代表的深度神经网络是万能函数逼近器,所做的事情是学习外部世界信息的概率分布,将其压缩或提取,构建内部概率模型。编码器-解码器处理之后的输出依然是一个实数向量,该向量是一个高维概率向量,其代表了Transformer视角下的编织起来的事物之间的各种复杂的关系。我们需要对此向量进行分类训练,才能从复杂关系中确认下一个token。
Generator就完成了此分类功能。Generator的输入是词向量序列,输出是每个位置上单词的概率分布。其主要包括两部分:
- Linear:将输出扩展至Vocabulary Size,或者说把词映射到词典。 Linear的输入是经过编码器-解码器处理后的词向量(在推理时,Generator使用的并不是解码器的所有输出token,而是最后一个token),输出是logits(对数几率/词表特征)。
- Softmax:Softmax将输入的logits转换为概率,输出就是最后一个token对应词表中单词的概率分布。后续会选取概率最高的作为预测结果。
1.1 Linear
线性层主要起到转换维度的作用,这一步的目的是将解码器生成的向量映射到预先定义的词典大小,从而准备进行词预测。其相关特点如下:
- 线性层(通常被称为 language model head 或 LM head)是一个简单的全连接神经网络,它可以把解码器产生的向量投射到一个比它大得多的,被称作logits的向量上。
- 也可以把线性层认为是 Token Embeddings 矩阵,其行数为模型词表中 Token 的个数,列数为 embedding 的维度,也就是每个 Token 对应一个 embedding 向量。解码器产生的向量和Token Embeddings 矩阵相乘(也就是和每个 Token 对应的 embedding 向量做内积),得到和词表中每个 Token 的相似性得分(logits),生成一个与模型词汇表中每个词作为下一词出现的可能性相关的数值列表(即logits)。
- logits的大小是 vocab_size ,对应了词汇表的大小。假设我们的模型词汇表是10000个词,那logits向量维数也是10000,词表中每个词对应logits向量中的一个logit。如果编码器输出的形状是(batch size,100,512),则把其中每个序列最后一个token取出,经过大小为[512, 10000]的线性层后,就得到形状为(batch size, 1,10000)的logits列表。
- logits向量包含每个单词成为序列中下一个单词的概率,或者说是候选 Token 的得分向量。logits的每一个维度都代表目标语言单词库中的一个单词,具体对应这个单词的分数(Word Scores)或者是分数权重,表示某个特定词元是“正确”下一个词元的概率。logit值越高,表示相应词元是“正确”词元的可能性越大。具体而言,假设我们预测第i个位置的单词,目标词汇表中的每个单词在第i个位置都有一个分数值,分数值表示词汇表中的每个词在第i个位置出现的可能性分数。向量中某维度的值越大,代表此单词是第i个位置上单词的概率越大。因此线性层就充当了分类头(词表中的每一个词当作一个类别)的作用,只是这个分类头的类别比较大。
- 后续要从这些单词中找出最大概率生成的词是哪几个?为何要是几个单词?这是因为要通过类似top_k的采样算法来调整模型的表现力,否则,你对模型说我爱你,模型回复的答案永远是我也爱你。
比如针对上下文”I am sleepy. I start a pot of",下图给出了预测下一个token时,词表中每个词的概率分布(按降序排列)。
下图给出了预测头的数学表示,\(W_U\)就是线性层(unembedding matrix),有时还会有偏置。最后一个残差流状态通过该线性映射进行转换,将表示转换为基于logits的下一个token分布,该分布通过softmax函数转换为概率分布。
下图则对Transformer的前向传播过程进行分解,图中方程式的四项的特点如下:
- 第一项是direct path(直接路径),该路径把输入embedding和 unembedding matrix 连接起来,对应图中上方最左侧的红色路线。
- 第二项和第四项被称为full OV circuits,该路径流经单个OV矩阵,对应图中上方的黄色路线。
- 第三项被称为虚拟注意头(virtual attention heads)。因为该部分两个注意头的顺序读写,因此也被称为V-composition(虚拟组合)。
1.2 softmax
线性层输出的 logits难以解释,因此我们接下来会把logits经过 softmax 转换为概率,即把向量中最后一维的数字缩放到0-1的概率值域内,并确保这些数字的和为1。这在多分类问题中尤为重要,因为模型预测的结果可以解释为每个类别的概率(每个位置上单词的概率分布)。后续会按照概率分布采样。
注意:Generator 返回的是 softmax 的 log 值。这里使用的是log_softmax而非softmax。虽然其效果应该是一样的。但是log_softmax能够解决溢出问题,加快运算速度,提高数据稳定性。
1.3 实现
本章第一个图的蓝圈对应下面的Generator类。Generator类包括Linear层和Softmax层。从直观的角度看,
- 线性层的作用就是把词映射到词典。
- Softmax层的作用就是选择概率最大的词。
Generator类的构建参数为:
- d_model:Decoder输出的大小,即词向量的维度。
- vocab:词典的大小。
具体代码如下。- # nn.functional工具包装载了网络层中那些只进行计算, 而没有参数的层
- import torch.nn.functional as F
- # 定义一个基于 nn.Module 的生成器类,其将线性层和softmax计算层一起实现, 因为二者的共同目标是生成最后的结构,因此把此类的名字叫做Generator
- class Generator(nn.Module):
- "Define standard linear + softmax generation step."
-
- # 初始化方法,接收模型维度(d_model)和词汇表大小(vocab)作为参数
- def __init__(self, d_model, vocab):
- """初始化函数的输入参数有两个, d_model代表词嵌入维度, vocab_size代表词表大小."""
- super(Generator, self).__init__() # 调用 nn.Module 的初始化方法
- # 这个线性层的参数有两个, 就是初始化函数传进来的两个参数: d_model, vocab_size
- self.proj = nn.Linear(d_model, vocab) # 定义一个线性层,将向量从模型的输出维度映射到词汇表大小
- # 前向传播方法,输入x是Decoder的输出,x的形状是[1, d_model],因为x是序列中最后一个token对应的向量
- def forward(self, x):
- # 将输入 x 传入线性层,然后对输出应用 log-softmax 激活函数(在最后一个维度上)
-
- # 在函数中, 首先使用self.proj对x在最后一个维度上进行线性变化,
- # 然后使用F中已经实现的log_softmax进行的softmax处理.
- # log_softmax就是对softmax的结果又取了对数, 因为对数函数是单调递增函数, 因此对最终我们取最大的概率值没有影响. 最后返回结果即可
- return log_softmax(self.proj(x), dim=-1)
复制代码 1.4 使用
如何使用Generator类?以及如何使用生成的概率?
推理
在推理时,只需要拿Decoder输出的最后一个token对应的张量送给Generator,得到一个词的概率分布。以下是推理代码。- def inference_test():
- test_model = make_model(11, 11, 2)
- test_model.eval()
- src = torch.LongTensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
- src_mask = torch.ones(1, 1, 10)
- memory = test_model.encode(src, src_mask)
- ys = torch.zeros(1, 1).type_as(src)
- for i in range(9):
- out = test_model.decode(
- memory, src_mask, ys, subsequent_mask(ys.size(1)).type_as(src.data)
- )
- prob = test_model.generator(out[:, -1])
- _, next_word = torch.max(prob, dim=1)
- next_word = next_word.data[0]
- ys = torch.cat(
- [ys, torch.empty(1, 1).type_as(src.data).fill_(next_word)], dim=1
- )
- print("Example Untrained Model Prediction:", ys)
复制代码 torch.max(prob, dim=1)实际上就是下面要学习的贪心解码。next_token = vocabulary[np.argmax(probs)] 便可以获取词表中的token。
训练
在训练时,需要将Decoder的所有输出送给Generator,然后对于输出的每个词,都会得到一个词的概率分布。在每个位置,我们先找到概率最高的单词索引(贪婪搜索),然后将该索引映射到词汇表中的相应单词。这些词就构成了 Transformer 的输出序列。
具体示例代码如下。- def example_simple_model():
- V = 11
- criterion = LabelSmoothing(size=V, padding_idx=0, smoothing=0.0)
- model = make_model(V, V, N=2)
- optimizer = torch.optim.Adam(
- model.parameters(), lr=0.5, betas=(0.9, 0.98), eps=1e-9
- )
- lr_scheduler = LambdaLR(
- optimizer=optimizer,
- lr_lambda=lambda step: rate(
- step, model_size=model.src_embed[0].d_model, factor=1.0, warmup=400
- ),
- )
- batch_size = 80
- for epoch in range(20):
- model.train()
- run_epoch(
- data_gen(V, batch_size, 20),
- model,
- SimpleLossCompute(model.generator, criterion), # 调用Generator类的实例
- optimizer,
- lr_scheduler,
- mode="train",
- )
- model.eval()
- run_epoch(
- data_gen(V, batch_size, 5),
- model,
- SimpleLossCompute(model.generator, criterion), # 调用Generator类的实例
- DummyOptimizer(),
- DummyScheduler(),
- mode="eval",
- )[0]
- model.eval()
- src = torch.LongTensor([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])
- max_len = src.shape[1]
- src_mask = torch.ones(1, 1, max_len)
- print(greedy_decode(model, src, src_mask, max_len=max_len, start_symbol=0))
- # execute_example(example_simple_model)
复制代码 具体是在计算损失里面调用了model.generator进行预测。假设batch size是2,序列长度是100,代码中会对最后一个维度进行softmax操作,得到bx100个单词的概率分布,在训练过程中bx100个单词是知道真值的,故可以直接采用损失函数进行训练。- class SimpleLossCompute:
- "A simple loss compute and train function."
- def __init__(self, generator, criterion):
- self.generator = generator
- self.criterion = criterion
- def __call__(self, x, y, norm):
- x = self.generator(x)
- sloss = (
- self.criterion(
- x.contiguous().view(-1, x.size(-1)), y.contiguous().view(-1)
- )
- / norm
- )
- return sloss.data * norm, sloss
复制代码 0x02 采样
拿到logits后,下一步是根据它来选择下一个词元。这个过程称为采样。通过 logits 生成的概率来提取 tokens 的过程是通过一种被称为采样方法、搜索策略(search strategy)、生成策略(generation strategy)或解码策略(decoding strategy)的启发式方法来完成的。由于语言的顺序结构,token不仅要在上下文中合适,而且要自然地流动以创建连贯的句子和段落。采样方法有助于选择遵循语言模式和结构的token。此外,采样方法有助于在确定性输出和创造性、多样化响应之间取得平衡。
所有采样方法的基本原理是设定一个概率阈值 \(
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |