Chapter5.3 Decoding strategies to control randomness
文章目录
- 5 Pretraining on Unlabeled Data
- 5.3 Decoding strategies to control randomness
- 5.3.1 Temperature scaling
- 5.3.2 Top-k sampling
- 5.3.3 Modifying the text generation function
5 Pretraining on Unlabeled Data
5.3 Decoding strategies to control randomness
-
以下是重新组织并精简后的内容:
在本节中,我们将介绍文本生成策略(也称为解码策略)以生成更具原创性的文本。首先,简要回顾前一章的
generate_text_simple
函数,该函数之前用于generate_and_print_sample
中。接着,介绍两种改进该函数的技术:- 温度调节(temperature scaling)
- top-k 采样(top-k sampling).
-
我们首先将模型从 GPU 转移回 CPU,因为对于相对较小的模型来说,推理(inference)不需要 GPU。此外,在训练结束后,我们将模型设置为评估模式(evaluation mode),以关闭随机组件,例如 dropout。接下来,我们将 GPTModel 实例(模型)插入到generate_text_simple函数中,该函数使用 LLM 一次生成一个token:
model.to("cpu") model.eval() tokenizer = tiktoken.get_encoding("gpt2") token_ids = generate_text_simple( model=model, idx=text_to_token_ids("Every effort moves you", tokenizer), max_new_tokens=25, context_size=GPT_CONFIG_124M["context_length"] ) print("Output text:\n", token_ids_to_text(token_ids, tokenizer)) """输出""" Output text: Every effort moves you?" "Yes--quite insensible to the irony. She wanted him vindicated--and by me!"
我们已知,在每个生成步骤中选择生成的标记,对应于词汇表中所有标记中的最大概率分数。这意味着即使我们在同一台计算机上多次运行上面的
generate_text_simple
函数,大语言模型(LLM)也将始终生成相同的输出。 因此,后续两个小节介绍的两个概念,用于控制生成文本的随机性和多样性:温度调节(temperature scaling)和 top-k 采样(top-k sampling)。
5.3.1 Temperature scaling
-
此前,在generate_text_simple函数中,我们总是使用torch.argmax采样概率最高的token作为下一个token,这种方式也被称为greedy decoding。为了生成更多样化的文本,我们可以将argmax替换为一个从概率分布中采样的函数
torch.multinomial(probs, num_samples=1)(
这里的概率分布是指LLM在每一步token生成时为词汇表中每个条目生成的概率分数)。为了通过具体示例说明概率采样,我们简要讨论下一个标记生成过程,并使用一个非常小的词汇表进行说明
vocab = { "closer": 0, "every": 1, "effort": 2, "forward": 3, "inches": 4, "moves": 5, "pizza": 6, "toward": 7, "you": 8, } inverse_vocab = {v: k for k, v in vocab.items()} # 假设 LLM 被赋予起始上下文“every effort moves you”,并生成以下下一个令牌逻辑 next_token_logits = torch.tensor( [4.51, 0.89, -1.90, 6.75, 1.63, -1.62, -1.89, 6.28, 1.79] ) probas = torch.softmax(next_token_logits, dim=0) next_token_id = torch.argmax(probas).item() # The next generated token is then as follows: print(inverse_vocab[next_token_id]) """输出""" forward
由于最大的 logit 值以及相应的最大的 softmax 概率得分位于第四个位置(索引位置 3,因为 Python 使用 0 索引),因此生成的单词是“forward”。
为了实现概率采样过程,我们现在可以替换argmax 与 PyTorch 中的multinomial函数:
torch.manual_seed(123) next_token_id = torch.multinomial(probas, num_samples=1).item() print(inverse_vocab[next_token_id]) """输出""" toward
我们使用
torch.multinomial(probas, num_samples=1)
从softmax分布中采样确定最可能的token,而非直接使用torch.argmax
。为便于说明,观察使用原始softmax概率对下一个token进行1,000次采样的结果def print_sampled_tokens(probas): torch.manual_seed(123) # Manual seed for reproducibility sample = [torch.multinomial(probas, num_samples=1).item() for i in range(1_000)] sampled_ids = torch.bincount(torch.tensor(sample)) for i, freq in enumerate(sampled_ids): print(f"{inverse_vocab[i]}\t x \t {freq}") print_sampled_tokens(probas) """输出""" closer x 71 every x 2 effort x 0 forward x 544 inches x 2 moves x 1 pizza x 0 toward x 376 you x 4
从输出中可以看出,单词“forward”被采样的次数最多(1000次中有544次),但是其他token如closer,toward偶尔也会被采样。这意味着,如果我们在generate_and_print_sample函数中用multinomial函数替换argmax函数,LLM有时会生成诸如“every effort moves you toward”“every effort moves you inches”和“every effort moves you closer”的文本,而不仅仅是“every effort moves you forward”。
-
我们可以通过一种称为“temperature scaling”的概念进一步控制分布和选择过程,其中“temperature scaling”只是一个花哨的描述,指的是将logits除以一个大于0的数:
def softmax_with_temperature(logits, temperature): scaled_logits = logits / temperature return torch.softmax(scaled_logits, dim=0) # Temperature values temperatures = [1, 0.1, 5] # Original, higher confidence, and lower confidence # Calculate scaled probabilities scaled_probas = [softmax_with_temperature(next_token_logits, T) for T in temperatures] # Plotting x = torch.arange(len(vocab)) bar_width = 0.15 fig, ax = plt.subplots(figsize=(5, 3)) for i, T in enumerate(temperatures): rects = ax.bar(x + i * bar_width, scaled_probas[i], bar_width, label=f'Temperature = {T}') ax.set_ylabel('Probability') ax.set_xticks(x) ax.set_xticklabels(vocab.keys(), rotation=90) ax.legend() plt.tight_layout() plt.savefig("temperature-plot.pdf") plt.show()
温度值大于1会导致token概率分布更加均匀,而温度值小于1则会导致分布更加集中(更尖锐或更突出)我们通过绘制原始概率与不同温度值缩放后的概率来直观说明这一点:
我们可以看到
-
温度值为1时等同于无缩放,token选择概率与原始softmax一致;
print_sampled_tokens(scaled_probas[0]) # temperature=1 """输出""" closer x 71 every x 2 effort x 0 forward x 544 inches x 2 moves x 1 pizza x 0 toward x 376 you x 4
-
温度值小于1(如0.1)时分布更尖锐,接近
torch.argmax
的效果,几乎总是选择最可能的token;print_sampled_tokens(scaled_probas[1]) # temperature=0.1 """输出""" closer x 0 every x 0 effort x 0 forward x 992 inches x 0 moves x 0 pizza x 0 toward x 8
-
温度值大于1(如5)时分布更均匀,增加文本多样性但可能导致无意义内容,如
every effort moves you pizza(pizza出现了43次,概率为4.3%)
print_sampled_tokens(scaled_probas[2]) # temperature=5 """输出""" closer x 153 every x 68 effort x 55 forward x 223 inches x 102 moves x 50 pizza x 43 toward x 218 you x 88
-
5.3.2 Top-k sampling
-
在上一节中,我们通过结合温度缩放的概率采样方法增加了输出多样性,较高温度值使token分布更均匀,减少重复选择最可能token的情况,从而探索更具创造性的路径,但这种方法有时会导致语法错误或完全无意义的输出(如“every effort moves you pizza”)。
为了能够使用较高的温度值来增加输出多样性并减少无意义句子的概率,我们可以将采样的token限制在top-k最可能的token中:
使用 k=3 的 top-k 采样,我们关注与最高 logits 相关的 3 个标记,并在应用 softmax 函数之前屏蔽掉所有其他具有负无穷大 (-inf) 的标记。这会导致概率分布的概率值为 0 分配给所有非前 k 个标记。
-
在代码中实现上图中概述的 top-k 过程
首先选择具有最大 logit 值的标记:
top_k = 3 top_logits, top_pos = torch.topk(next_token_logits, top_k) print("Top logits:", top_logits) print("Top positions:", top_pos) """输出""" Top logits: tensor([6.7500, 6.2800, 4.5100]) Top positions: tensor([3, 7, 0])
随后,我们应用 PyTorch 的 where 函数将低于 top-3 选择中最低 logit 值的标记的 logit 值设置为负无穷大 (-inf)
new_logits = torch.where( condition=next_token_logits < top_logits[-1], input=torch.tensor(float("-inf")), other=next_token_logits ) # 或者 # new_logits = torch.full_like( # 创建一个包含-inf值的张量 # next_token_logits, -torch.inf # ) # new_logits[top_pos] = next_token_logits[top_pos] # 将top k值复制到-inf张量中 print(new_logits) """输出""" tensor([4.5100, -inf, -inf, 6.7500, -inf, -inf, -inf, 6.2800, -inf])
最后,让我们应用 softmax 函数将它们转换为下一个标记的概率:
topk_probas = torch.softmax(new_logits, dim=0) print(topk_probas) """输出""" tensor([0.0615, 0.00, 0.00, 0.5775, 0.00, 0.00, 0.00, 0.3610, 0.00])
5.3.3 Modifying the text generation function
-
前两小节介绍了温度采样(temperature sampling)和top-k采样(top-k sampling),我们将利用这两个概念修改之前用于通过LLM生成文本的
generate_simple
函数,创建一个新的generate
函数。def generate(model, idx, max_new_tokens, context_size, temperature=0.0, top_k=None, eos_id=None): # For-loop is the same as before: Get logits, and only focus on last time step for _ in range(max_new_tokens): idx_cond = idx[:, -context_size:] with torch.no_grad(): logits = model(idx_cond) logits = logits[:, -1, :] # New: Filter logits with top_k sampling if top_k is not None: # Keep only top_k values top_logits, _ = torch.topk(logits, top_k) min_val = top_logits[:, -1] logits = torch.where(logits < min_val, torch.tensor(float("-inf")).to(logits.device), logits) # New: Apply temperature scaling if temperature > 0.0: logits = logits / temperature # Apply softmax to get probabilities probs = torch.softmax(logits, dim=-1) # (batch_size, context_len) # Sample from the distribution idx_next = torch.multinomial(probs, num_samples=1) # (batch_size, 1) # Otherwise same as before: get idx of the vocab entry with the highest logits value else: idx_next = torch.argmax(logits, dim=-1, keepdim=True) # (batch_size, 1) if idx_next == eos_id: # Stop generating early if end-of-sequence token is encountered and eos_id is specified break # Same as before: append sampled index to the running sequence idx = torch.cat((idx, idx_next), dim=1) # (batch_size, num_tokens+1) return idx
更改完成后,看看这个新生成函数的实际效果
torch.manual_seed(123) token_ids = generate( model=model, idx=text_to_token_ids("Every effort moves you", tokenizer), max_new_tokens=15, context_size=GPT_CONFIG_124M["context_length"], top_k=25, temperature=1.4 ) print("Output text:\n", token_ids_to_text(token_ids, tokenizer)) """输出""" Output text: Every effort moves you know began to my surprise, poor StI was such a good; and
下面的是我们先前生成的output
Every effort moves you?" "Yes--quite insensible to the irony. She wanted him vindicated--and by me!"
可以对比一下,已经有很大的不同了。