hidden state 向量
当我们把一句话输入模型后,例如 "Hello world":
token IDs: [15496, 995]
经过 Embedding + Transformer 层后,会得到每个 token 的中间表示,形状为:
hidden_states: (batch_size, seq_len, hidden_dim) 比如: (1, 2, 768)
这是 Transformer 层的输出,即每个 token 的向量表示。
hidden state → logits:映射到词表空间
🔹 使用 输出投影矩阵(通常是 embedding 的转置)
为了从 hidden state 还原出词,我们需要得到它在词表上每个 token 的"分数",这叫 logits。实现方式如下:
logits = hidden_state @ W_out.T + b
其中:
W_out
是词嵌入矩阵(Embedding matrix),形状为(vocab_size, hidden_dim)
@
是矩阵乘法,hidden_state
形状是(seq_len, hidden_dim)
- 得到的
logits
形状是(seq_len, vocab_size)
所以,每个位置的 hidden state 都被映射成一个 词表大小的分布。
logits → token ID:选出最可能的 token
现在每个位置我们都有了一个 logits 向量,例如:
logits = [2.1, -0.5, 0.3, 6.9, ...] # 长度 = vocab_size
有几种选择方式:
方法 | 说明 |
---|---|
argmax(logits) |
选最大值,对应 greedy decoding |
softmax → sample |
转成概率分布后随机采样 |
top-k sampling |
从 top-k 个中采样,控制多样性 |
top-p (nucleus) |
从累计概率在 p 范围内采样 |
例如:
python
probs = softmax(logits)
token_id = torch.argmax(probs).item()
token ID → token 字符串片段
token ID 其实对应的是某个词表里的编号,比如:
python
tokenizer.convert_ids_to_tokens(50256) # 输出: <|endoftext|>
tokenizer.convert_ids_to_tokens(15496) # 输出: "Hello"
如果是多个 token ID,可以:
python
tokenizer.convert_ids_to_tokens([15496, 995]) # 输出: ["Hello", " world"]
tokens → 拼接成文本(decode)
tokens 是"子词"或"子字符",例如:
["Hel", "lo", " world", "!"]
通过 tokenizer.decode()
会自动合并它们为字符串:
python
tokenizer.decode([15496, 995]) # 输出: "Hello world"
它会处理空格、子词连接等细节,恢复为人类可读的句子。
多轮生成:把预测作为输入继续生成
在生成任务(如 GPT)中,模型是逐 token 生成的。
流程如下:
输入: "你好"
↓
tokenize → [token IDs]
↓
送入模型 → 得到下一个 token 的 logits
↓
选出 token ID → decode 成文字
↓
拼接到输入后,继续送入模型 → 下一轮生成
↓
...
直到生成 EOS(终止符)或达到最大长度
总结流程图:
(1) 输入文本 → tokenizer → token IDs
(2) token IDs → Embedding → hidden_states(中间层向量)
(3) hidden_states × W.T → logits(词表得分)
(4) logits → sampling → token ID
(5) token ID → token → decode → 文本
(6) 拼接文本 → 重复生成(自回归)
示例代码
python
"""
大语言模型解码过程详解
===========================
本示例展示了大语言模型如何将隐藏状态向量解码成文本输出
使用GPT-2模型作为演示,展示从输入文本到预测下一个token的完整流程
"""
import torch
import numpy as np
import matplotlib.pyplot as plt
from transformers import GPT2LMHeadModel, GPT2Tokenizer
# 设置随机种子,确保结果可复现
torch.manual_seed(42)
def display_token_probabilities(probabilities, tokens, top_k=5):
"""可视化展示token的概率分布(仅展示top_k个)"""
# 获取前k个最大概率及其索引
top_probs, top_indices = torch.topk(probabilities, top_k)
top_probs = top_probs.detach().numpy()
top_tokens = [tokens[idx] for idx in top_indices]
print(f"\n前{top_k}个最可能的下一个token:")
for token, prob in zip(top_tokens, top_probs):
print(f" {token:15s}: {prob:.6f} ({prob * 100:.2f}%)")
# 可视化概率分布
plt.figure(figsize=(10, 6))
plt.bar(top_tokens, top_probs)
plt.title(f"Top {top_k} The probability distribution of the next token")
plt.ylabel("probability")
plt.xlabel("Token")
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
def main():
print("Step 1: 加载预训练模型和分词器")
# 从Hugging Face加载预训练的GPT-2模型和分词器
tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
model = GPT2LMHeadModel.from_pretrained("gpt2")
model.eval() # 将模型设置为评估模式
print("\nStep 2: 准备输入文本")
input_text = "Artificial intelligence is"
print(f"输入文本: '{input_text}'")
# 将输入文本转换为模型需要的格式
inputs = tokenizer(input_text, return_tensors="pt")
input_ids = inputs["input_ids"]
attention_mask = inputs["attention_mask"]
# 展示分词结果
tokens = tokenizer.convert_ids_to_tokens(input_ids[0])
print(f"分词结果: {tokens}")
print(f"Token IDs: {input_ids[0].tolist()}")
print("\nStep 3: 运行模型前向传播")
# 使用torch.no_grad()避免计算梯度,节省内存
with torch.no_grad():
# output_hidden_states=True 让模型返回所有层的隐藏状态
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask,
output_hidden_states=True
)
# 获取最后一层的隐藏状态
# hidden_states的形状: [层数, batch_size, seq_len, hidden_dim]
last_layer_hidden_states = outputs.hidden_states[-1]
print(f"隐藏状态形状: {last_layer_hidden_states.shape}")
# 获取序列中最后一个token的隐藏状态
last_token_hidden_state = last_layer_hidden_states[0, -1, :]
print(f"最后一个token的隐藏状态形状: {last_token_hidden_state.shape}")
print(f"隐藏状态前5个值: {last_token_hidden_state[:5].tolist()}")
print("\nStep 4: 手动计算logits")
# 从模型中获取输出嵌入矩阵的权重
lm_head_weights = model.get_output_embeddings().weight # [vocab_size, hidden_dim]
print(f"语言模型输出嵌入矩阵形状: {lm_head_weights.shape}")
# 通过点积计算logits
# logits代表每个词汇表中token的分数
logits = torch.matmul(last_token_hidden_state, lm_head_weights.T) # [vocab_size]
print(f"Logits形状: {logits.shape}")
print(f"Logits值域: [{logits.min().item():.4f}, {logits.max().item():.4f}]")
print("\nStep 5: 应用softmax转换为概率")
# 使用softmax将logits转换为概率分布
probabilities = torch.softmax(logits, dim=0)
print(f"概率总和: {probabilities.sum().item():.4f}") # 应该接近1
# 找出概率最高的token
next_token_id = torch.argmax(probabilities).item()
next_token = tokenizer.decode([next_token_id])
print(f"预测的下一个token (ID: {next_token_id}): '{next_token}'")
# 展示完整的句子
complete_text = input_text + next_token
print(f"生成的文本: '{complete_text}'")
# 展示top-k的概率分布
display_token_probabilities(probabilities, tokenizer.convert_ids_to_tokens(range(len(probabilities))), top_k=10)
print("\nStep 6: 比较与模型内置解码结果")
# 获取模型内置的logits输出
model_outputs = model(input_ids=input_ids, attention_mask=attention_mask)
model_logits = model_outputs.logits
print(f"模型输出的logits形状: {model_logits.shape}")
# 获取最后一个token位置的logits
last_token_model_logits = model_logits[0, -1, :]
# 验证我们手动计算的logits与模型输出的logits是否一致
is_close = torch.allclose(logits, last_token_model_logits, rtol=1e-4)
print(f"手动计算的logits与模型输出的logits是否一致: {is_close}")
# 如果不一致,计算差异
if not is_close:
diff = torch.abs(logits - last_token_model_logits)
print(f"最大差异: {diff.max().item():.8f}")
print(f"平均差异: {diff.mean().item():.8f}")
print("\nStep 7: 使用模型进行文本生成")
# 使用模型的generate方法生成更多文本
# 生成时传递 attention_mask 和 pad_token_id
generated_ids = model.generate(
input_ids,
max_length=input_ids.shape[1] + 10, # 生成10个额外的token
temperature=1.0,
do_sample=True,
top_k=50,
top_p=0.95,
num_return_sequences=1,
attention_mask=attention_mask, # 添加 attention_mask
pad_token_id=tokenizer.eos_token_id # 明确设置 pad_token_id 为 eos_token_id
)
generated_text = tokenizer.decode(generated_ids[0], skip_special_tokens=True)
print(f"模型生成的文本:\n'{generated_text}'")
if __name__ == "__main__":
main()
Roberta代码案例
python
import torch
from transformers import RobertaTokenizer, RobertaForMaskedLM
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
# 设置中文字体显示(如果需要显示中文)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 1. 加载预训练的RoBERTa模型和tokenizer
tokenizer = RobertaTokenizer.from_pretrained('roberta-base')
model = RobertaForMaskedLM.from_pretrained('roberta-base')
# 2. 定义一个带有[MASK]标记的示例句子
text = f"The capital of France is {tokenizer.mask_token}."
print(f"原始文本: {text}")
# 3. 对文本进行编码,转换为模型的输入格式
inputs = tokenizer(text, return_tensors="pt")
print(f"\n标记化后的输入ID: {inputs['input_ids'][0].tolist()}")
print(f"对应的标记: {tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])}")
# 4. 找到[MASK]标记的位置
mask_token_index = torch.where(inputs["input_ids"] == tokenizer.mask_token_id)[1]
if mask_token_index.numel() == 0:
raise ValueError("没有找到[MASK]标记,请检查输入文本。")
print(f"\n[MASK]标记的位置: {mask_token_index.item()}")
# 5. 前向传播,获取预测结果(添加output_hidden_states=True)
with torch.no_grad():
outputs = model(**inputs, output_hidden_states=True)
# 6. 获取[MASK]位置的预测分数
logits = outputs.logits
mask_token_logits = logits[0, mask_token_index, :]
# 7. 找出前5个最可能的标记
top_5_tokens = torch.topk(mask_token_logits, 5, dim=1)
top_5_token_indices = top_5_tokens.indices[0].tolist()
top_5_token_scores = top_5_tokens.values[0].tolist()
print("\n预测结果:")
for i, (index, score) in enumerate(zip(top_5_token_indices, top_5_token_scores)):
token = tokenizer.decode([index])
probability = torch.softmax(mask_token_logits, dim=1)[0, index].item()
print(f" {i + 1}. '{token}' - 分数: {score:.2f}, 概率: {probability:.4f}")
# 8. 获取向量表示 - 确保获取到hidden_states
last_hidden_states = outputs.hidden_states[-1] # 现在这一行应该可以工作了
# 9. 可视化[MASK]位置的向量
def visualize_vector(vector, title):
plt.figure(figsize=(10, 6))
plt.bar(range(len(vector)), vector)
plt.title(title)
plt.xlabel('维度')
plt.ylabel('激活值')
plt.tight_layout()
plt.show()
# 10. 可视化解码过程
def visualize_decoding_process():
# 获取模型最终层的权重矩阵
decoder_weights = model.lm_head.decoder.weight.detach()
# 获取[MASK]位置的隐藏状态向量
mask_hidden_state = last_hidden_states[0, mask_token_index].squeeze()
# 计算点积得分
scores = torch.matmul(mask_hidden_state, decoder_weights.t())
# 获取前5个最高分词的索引和分数
top_indices = torch.topk(scores, 5).indices.tolist()
top_tokens = [tokenizer.decode([idx]) for idx in top_indices]
# 可视化注意力/解码过程
plt.figure(figsize=(12, 6))
# 可视化隐藏状态向量与词表中向量的相似度
plt.subplot(1, 2, 1)
sns.heatmap(scores.reshape(1, -1)[:, top_indices].detach().numpy(),
annot=True, fmt=".2f", cmap="YlGnBu",
xticklabels=top_tokens)
plt.title("词向量与词表的相似度分数")
plt.xlabel("候选词")
plt.ylabel("点积分数")
# 可视化softmax后的概率分布
plt.subplot(1, 2, 2)
probabilities = torch.softmax(scores, dim=0)[top_indices].detach().numpy()
plt.bar(top_tokens, probabilities)
plt.title("解码后的概率分布")
plt.xlabel("候选词")
plt.ylabel("概率")
plt.tight_layout()
plt.show()
# 可视化[MASK]位置的向量
mask_vector = last_hidden_states[0, mask_token_index].squeeze().detach().numpy()
visualize_vector(mask_vector[:50], "MASK位置的词向量表示(仅显示前50维)")
# 显示解码过程
visualize_decoding_process()
# 12. 显示最终预测结果
predicted_token_id = top_5_token_indices[0]
predicted_token = tokenizer.decode([predicted_token_id])
print(f"\n最终预测结果: '{predicted_token}'")
print(f"完整句子: {text.replace(tokenizer.mask_token, predicted_token)}")
更详细的代码案例
python
"""
大语言模型向量解码过程详解 - 使用BERT模型
=============================================
本示例展示了大语言模型如何将隐藏状态向量解码成文本输出
"""
import time
from datetime import datetime
import torch
import numpy as np
import matplotlib.pyplot as plt
from transformers import BertTokenizer, BertForMaskedLM
from typing import List, Tuple, Dict
# 设置随机种子,确保结果可复现
torch.manual_seed(42)
class DecodingVisualizer:
"""用于可视化大语言模型解码过程的工具类"""
def __init__(self, model_name: str = "bert-base-uncased", use_cuda: bool = True):
"""初始化模型和分词器"""
print(f"正在加载 {model_name} 模型和分词器...")
# 添加设备自动检测
self.device = torch.device("cuda" if torch.cuda.is_available() and use_cuda else "cpu")
# 注意:这里需要根据use_cuda参数决定使用哪个设备,而不是只检查可用性
# 添加低内存加载选项
self.tokenizer = BertTokenizer.from_pretrained(model_name)
print(f"正在加载 {model_name} (设备: {self.device})...")
start_time = time.time()
self.model = BertForMaskedLM.from_pretrained(
model_name,
low_cpu_mem_usage=True,
torch_dtype=torch.float16 if self.device.type == "cuda" else torch.float32
).to(self.device) # 确保这里没有遗漏.to(self.device)
self.model.eval() # 将模型设置为评估模式
# 获取模型配置
self.hidden_size = self.model.config.hidden_size
self.vocab_size = self.model.config.vocab_size
# 获取MASK token ID
self.mask_token_id = self.tokenizer.mask_token_id
self.mask_token = self.tokenizer.mask_token
load_time = time.time() - start_time
print(f"加载完成! 耗时: {load_time:.2f}s")
print(f"模型加载完成! 隐藏层维度: {self.hidden_size}, 词表大小: {self.vocab_size}")
print(f"MASK token: '{self.mask_token}', ID: {self.mask_token_id}")
def prepare_masked_input(self, text: str) -> Dict[str, torch.Tensor]:
"""掩码输入准备函数"""
# 更智能的掩码位置选择
words = text.split()
if not words:
return {
"inputs": self.tokenizer(text, return_tensors="pt").to(self.device), # 注意:这里需要将inputs张量移动到设备上
"original_text": text,
"masked_text": text,
"original_word": "",
"masked_position": 0
}
# 选择内容词进行掩码(避免掩码停用词)
content_pos = []
stopwords = {"the", "a", "an", "is", "are", "of", "to"}
for i, word in enumerate(words):
if word.lower() not in stopwords:
content_pos.append(i)
# 如果没有内容词,则选择最后一个词
masked_pos = content_pos[-1] if content_pos else len(words) - 1
original_word = words[masked_pos]
words[masked_pos] = self.mask_token
masked_text = " ".join(words)
inputs = self.tokenizer(
masked_text,
return_tensors="pt",
max_length=512,
truncation=True,
padding="max_length" # 固定长度便于批处理
).to(self.device)
return {
"inputs": inputs,
"original_text": text,
"masked_text": masked_text,
"original_word": original_word,
"masked_position": masked_pos + 1 # 考虑[CLS] token
}
def decode_step_by_step(self, text: str) -> None:
"""
详细展示BERT模型解码过程的每个步骤
参数:
text: 要处理的输入文本
verbose: 是否打印详细过程
返回:
包含完整解码信息的字典:
{
"input_text": str,
"masked_text": str,
"hidden_state": torch.Tensor,
"logits": torch.Tensor,
"predictions": List[Tuple[str, float]],
"top_k_predictions": List[Tuple[str, float]]
}
"""
print("\n" + "="*60)
print("BERT模型解码过程演示")
print("="*60)
# 准备带掩码的输入
masked_data = self.prepare_masked_input(text)
inputs = masked_data["inputs"]
original_text = masked_data["original_text"]
masked_text = masked_data["masked_text"]
original_word = masked_data["original_word"]
print(f"原始文本: '{original_text}'")
print(f"掩码文本: '{masked_text}'")
print(f"被掩码的词: '{original_word}'")
# 分词结果
input_ids = inputs["input_ids"]
token_type_ids = inputs["token_type_ids"]
attention_mask = inputs["attention_mask"]
tokens = self.tokenizer.convert_ids_to_tokens(input_ids[0])
print(f"\n分词结果: {tokens}")
print(f"Token IDs: {input_ids[0].tolist()}")
# 查找[MASK]的位置
mask_positions = [i for i, id in enumerate(input_ids[0]) if id == self.mask_token_id]
if mask_positions:
mask_position = mask_positions[0]
print(f"[MASK]的位置: {mask_position}, Token: '{tokens[mask_position]}'")
else:
print("未找到[MASK]标记,使用最后一个token作为示例")
mask_position = len(tokens) - 2 # 避免[SEP]标记
# Step 1: 运行模型前向传播
print("\n【Step 1: 运行模型前向传播】")
with torch.no_grad():
outputs = self.model(
input_ids=input_ids,
token_type_ids=token_type_ids,
attention_mask=attention_mask,
output_hidden_states=True
)
# 获取最后一层的隐藏状态
last_hidden_states = outputs.hidden_states[-1]
print(f"隐藏状态形状: {last_hidden_states.shape}")
# 获取[MASK]位置的隐藏状态
mask_hidden_state = last_hidden_states[0, mask_position, :]
print(f"[MASK]位置的隐藏状态形状: {mask_hidden_state.shape}")
print(f"隐藏状态前5个值: {mask_hidden_state[:5].tolist()}")
# Step 2优化: 添加详细解释和性能优化
print("\n【Step 2: 解码向量生成logits(解码过程的核心)】")
print("解码过程实质上是将隐藏状态向量映射到词表空间的一个线性变换")
print(f"数学表达式: logits = hidden_state × W^T + b")
# 使用更高效的矩阵运算
with torch.no_grad():
cls_weights = self.model.cls.predictions.decoder.weight
cls_bias = self.model.cls.predictions.decoder.bias
print(f"解码器权重矩阵形状: {cls_weights.shape}")
print(f"解码器偏置向量形状: {cls_bias.shape}")
# 使用einsum进行高效矩阵乘法
manual_logits = torch.einsum(
"d,vd->v",
mask_hidden_state,
cls_weights
) + cls_bias
# 添加温度系数调节
temperature = 1.0 # 可调节参数
tempered_logits = manual_logits / temperature
# 验证一致性时添加容差说明
model_logits = outputs.logits[0, mask_position, :]
is_close = torch.allclose(
manual_logits,
model_logits,
rtol=1e-3,
atol=1e-5
)
print(f"\n手动计算的logits与模型输出是否一致: {is_close}")
if not is_close:
diff = torch.abs(manual_logits - model_logits)
print(f"最大差异: {diff.max().item():.8f}")
print(f"平均差异: {diff.mean().item():.8f}")
print("注: 小的数值差异可能是由于计算精度造成的")
# Step 3: 从logits到概率
print("\n【Step 3: 将logits转换为概率】")
with torch.no_grad():
# 使用softmax转换为概率分布
probabilities = torch.softmax(manual_logits, dim=0)
print(f"概率总和: {probabilities.sum().item():.4f}") # 应该接近1
# 找出概率最高的token
top_probs, top_indices = torch.topk(probabilities, 5)
predicted_token_id = top_indices[0].item()
predicted_token = self.tokenizer.convert_ids_to_tokens([predicted_token_id])[0]
predicted_word = self.tokenizer.decode([predicted_token_id])
print(f"\n预测的token (ID: {predicted_token_id}): '{predicted_token}'")
print(f"解码后的单词: '{predicted_word}'")
# 原始被遮蔽的词的概率
if original_word:
original_word_ids = self.tokenizer.encode(original_word, add_special_tokens=False)
if original_word_ids:
original_id = original_word_ids[0]
original_prob = probabilities[original_id].item()
print(f"原始单词 '{original_word}' (ID: {original_id}) 的概率: {original_prob:.6f} ({original_prob*100:.2f}%)")
# 展示前10个最可能的tokens
self._display_token_probabilities(probabilities, top_k=10)
def _display_token_probabilities(self, probabilities: torch.Tensor, top_k: int = 5) -> None:
# 获取前k个最大概率及其索引
top_probs, top_indices = torch.topk(probabilities, top_k)
top_tokens = [self.tokenizer.convert_ids_to_tokens([idx.item()])[0] for idx in top_indices]
top_words = [self.tokenizer.decode([idx.item()]) for idx in top_indices]
# 创建使用更精确比例的图形
fig, ax = plt.subplots(figsize=(16, 9), constrained_layout=True)
# 使用更适合数据对比的渐变色调
colors = plt.cm.Blues(np.linspace(0.6, 0.9, top_k))
# 绘制条形图,适当增加条形宽度以提高可读性
bars = ax.barh(range(top_k), top_probs.tolist(), color=colors, height=0.6)
# 自定义Y轴刻度,同时显示token和对应的实际内容
ax.set_yticks(range(top_k))
labels = [f"{w} ({t})" if t != w else w for t, w in zip(top_tokens, top_words)]
ax.set_yticklabels(labels, fontsize=12)
# 添加更突出的标题与标签
ax.set_title("Token Prediction Probabilities", fontsize=18, fontweight='bold', pad=20)
ax.set_xlabel("Probability", fontsize=15, fontweight='semibold', labelpad=12)
# 去掉多余的Y轴标签,因为标签已经在刻度上显示
ax.set_ylabel("")
# 动态设置X轴范围,确保最高概率条形图占据约80%的宽度
max_prob = top_probs[0].item()
ax.set_xlim(0, max(max_prob * 1.25, 0.05))
# 添加更清晰的数据标签
for i, (bar, prob) in enumerate(zip(bars, top_probs)):
width = bar.get_width()
ax.text(
width + 0.005,
i,
f"{prob:.4f} ({prob:.1%})",
ha='left',
va='center',
fontsize=13,
fontweight='bold',
color='#333333'
)
# 添加半透明的网格线以便于阅读
ax.grid(axis='x', linestyle='--', alpha=0.4, color='gray')
# 反转Y轴使最高概率在上方
ax.invert_yaxis()
# 美化图表边框和背景
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_linewidth(0.5)
ax.spines['bottom'].set_linewidth(0.5)
# 设置浅色背景以提高对比度
ax.set_facecolor('#f8f8f8')
# 添加概率条形图的圆角效果
for bar in bars:
bar.set_edgecolor('white')
bar.set_linewidth(1)
plt.show()
def visualize_linear_transformation(self, text: str) -> None:
"""可视化向量解码的线性变换过程"""
print("\n" + "=" * 60)
print("可视化向量解码的线性变换过程")
print("=" * 60)
# 准备带掩码的输入
masked_data = self.prepare_masked_input(text)
inputs = masked_data["inputs"]
# 寻找[MASK]位置
input_ids = inputs["input_ids"]
mask_positions = [i for i, id in enumerate(input_ids[0]) if id == self.mask_token_id]
if mask_positions:
mask_position = mask_positions[0]
else:
mask_position = len(input_ids[0]) - 2 # 避免[SEP]
# 运行模型获取隐藏状态
with torch.no_grad():
outputs = self.model(
**inputs,
output_hidden_states=True
)
last_hidden_states = outputs.hidden_states[-1]
mask_hidden_state = last_hidden_states[0, mask_position, :]
# 获取解码器权重
cls_weights = self.model.cls.predictions.decoder.weight
# 为了可视化,我们只取前2维隐藏状态和几个样本词
reduced_hidden = mask_hidden_state[:2].cpu().numpy() # 添加cpu()
# 选取几个常见词的权重向量
common_words = ["the", "is", "and", "of", "to", "a", "in", "for", "with"]
word_ids = []
for word in common_words:
word_ids.extend(self.tokenizer.encode(word, add_special_tokens=False))
# 确保我们有不重复的IDs
word_ids = list(set(word_ids))[:8] # 取前8个
word_tokens = [self.tokenizer.convert_ids_to_tokens([id])[0] for id in word_ids]
# 获取这些词的权重向量
word_vectors = cls_weights[word_ids, :2].cpu().numpy() # 添加cpu()
# 可视化
plt.figure(figsize=(10, 8))
# 绘制隐藏状态向量
plt.scatter(reduced_hidden[0], reduced_hidden[1], c='red', s=100, marker='*',
label='Hidden state vector')
# 绘制词向量
plt.scatter(word_vectors[:, 0], word_vectors[:, 1], c='blue', s=50)
# 添加词标签
for i, token in enumerate(word_tokens):
plt.annotate(token, (word_vectors[i, 0], word_vectors[i, 1]),
fontsize=10, alpha=0.8)
# 计算这些词的logits(向量点积)
logits = np.dot(reduced_hidden, word_vectors.T)
# 绘制从隐藏状态到各词向量的连线,线宽表示logit值
max_logit = np.max(np.abs(logits))
for i, token in enumerate(word_tokens):
# 归一化logit值作为线宽
width = 0.5 + 3.0 * (logits[i] + max_logit) / (2 * max_logit)
# 用颜色表示logit的正负
color = 'green' if logits[i] > 0 else 'red'
alpha = abs(logits[i]) / max_logit
plt.plot([reduced_hidden[0], word_vectors[i, 0]],
[reduced_hidden[1], word_vectors[i, 1]],
linewidth=width, alpha=alpha, color=color)
plt.title("The relationship between the hidden state vector and the word vector (2D projection)")
plt.xlabel("dimension1")
plt.ylabel("dimension2")
plt.grid(True, alpha=0.3)
plt.legend()
plt.tight_layout()
plt.show()
# 展示与这些词的点积(logits)
print("\n隐藏状态与词向量的点积(logits):")
for i, token in enumerate(word_tokens):
print(f" 与 '{token}' 的点积: {logits[i]:.4f}")
# 将logits转换为概率
probs = np.exp(logits) / np.sum(np.exp(logits))
print("\n转换为概率后:")
for i, token in enumerate(word_tokens):
print(f" '{token}' 的概率: {probs[i]:.4f} ({probs[i]*100:.2f}%)")
def demonstrate_bert_mlm(self, text: str, positions_to_mask=None) -> None:
"""演示BERT掩码语言模型的完整预测过程"""
print("\n" + "=" * 60)
print("BERT掩码语言模型演示")
print("=" * 60)
# 分词
inputs = self.tokenizer(
text,
return_tensors="pt",
padding=True,
truncation=True
).to(self.device) # 添加.to(self.device)将输入移动到正确的设备
input_ids = inputs["input_ids"]
tokens = self.tokenizer.convert_ids_to_tokens(input_ids[0])
print(f"原始文本: '{text}'")
print(f"分词结果: {tokens}")
# 如果没有指定要掩码的位置,则随机选择
if positions_to_mask is None:
# 排除[CLS]和[SEP]标记
valid_positions = list(range(1, len(tokens) - 1))
# 随机选择15%的token进行掩码
num_to_mask = max(1, int(len(valid_positions) * 0.15))
positions_to_mask = np.random.choice(valid_positions, num_to_mask, replace=False)
# 应用掩码
masked_input_ids = input_ids.clone()
for pos in positions_to_mask:
if pos < len(tokens):
original_token = tokens[pos]
original_id = input_ids[0, pos].item()
masked_input_ids[0, pos] = self.mask_token_id
print(f"位置 {pos}: 将 '{original_token}' (ID: {original_id}) 替换为 '{self.mask_token}'")
# 运行模型
with torch.no_grad():
outputs = self.model(
input_ids=masked_input_ids,
token_type_ids=inputs["token_type_ids"],
attention_mask=inputs["attention_mask"]
)
predictions = outputs.logits
# 对每个掩码位置进行预测
print("\n预测结果:")
for pos in positions_to_mask:
if pos < len(tokens):
# 获取该位置的logits
logits = predictions[0, pos, :]
# 应用softmax获取概率
probs = torch.softmax(logits, dim=0)
# 获取概率最高的token
top_probs, top_indices = torch.topk(probs, 5)
# 显示预测结果
original_token = tokens[pos]
original_id = input_ids[0, pos].item()
print(f"\n位置 {pos} 原始token: '{original_token}' (ID: {original_id})")
print("Top 5预测:")
for i, (index, prob) in enumerate(zip(top_indices, top_probs)):
predicted_token = self.tokenizer.convert_ids_to_tokens([index])[0]
print(f" {i + 1}. '{predicted_token}': {prob:.6f} ({prob * 100:.2f}%)")
# 检查原始token的排名和概率
original_prob = probs[original_id].item()
original_rank = torch.where(torch.argsort(probs, descending=True) == original_id)[0].item() + 1
print(
f" 原始token '{original_token}' 排名: #{original_rank}, 概率: {original_prob:.6f} ({original_prob * 100:.2f}%)")
# 恢复掩码后的文本
predicted_ids = torch.argmax(outputs.logits, dim=-1)
predicted_tokens = []
for i in range(len(tokens)):
if i in positions_to_mask:
# 使用预测的token
predicted_token = self.tokenizer.convert_ids_to_tokens([predicted_ids[0, i].item()])[0]
predicted_tokens.append(predicted_token)
else:
predicted_tokens.append(tokens[i])
# 解码回文本
predicted_text = self.tokenizer.convert_tokens_to_string(predicted_tokens)
print(f"\n恢复后的文本: '{predicted_text}'")
def _nucleus_sampling(self, logits: torch.Tensor, p: float = 0.9) -> torch.Tensor:
"""
实现nucleus sampling (也称为top-p sampling)
Args:
logits: 模型输出的logits
p: 概率质量阈值(默认0.9)
Returns:
采样得到的token ID
"""
# 计算softmax概率
probs = torch.softmax(logits, dim=-1)
# 按概率从大到小排序
sorted_probs, sorted_indices = torch.sort(probs, descending=True)
# 计算累积概率
cumulative_probs = torch.cumsum(sorted_probs, dim=-1)
# 找到累积概率超过p的位置
nucleus = cumulative_probs < p
# 确保至少选择一个token(如果所有nucleus都是False)
if not nucleus.any():
nucleus[0] = True
# 将概率低于阈值的token概率设为0
nucleus_probs = torch.zeros_like(probs)
nucleus_probs[sorted_indices[nucleus]] = sorted_probs[nucleus]
# 重新归一化概率
if nucleus_probs.sum() > 0:
nucleus_probs = nucleus_probs / nucleus_probs.sum()
else:
# 如果所有概率都为0,则使用原始概率的top-1
nucleus_probs[sorted_indices[0]] = 1.0
# 采样
return torch.multinomial(nucleus_probs, num_samples=1)
def compare_decoding_strategies(self, text: str = None):
"""比较不同解码策略的结果"""
if text is None:
# 使用更具歧义性的例子,让不同策略有可能生成不同结果
text = "The scientist made a [MASK] discovery that changed the field."
print(f"使用示例文本: '{text}'")
# 扩展策略集合,使用多种参数
strategies = {
"贪婪解码": lambda logits: torch.argmax(logits, dim=-1),
"Top-K=3": lambda logits: torch.multinomial(
self._top_k_sampling(logits, k=3),
num_samples=1
),
"Top-K=10": lambda logits: torch.multinomial(
self._top_k_sampling(logits, k=10),
num_samples=1
),
"Top-P=0.5": lambda logits: self._nucleus_sampling(logits, 0.5),
"Top-P=0.9": lambda logits: self._nucleus_sampling(logits, 0.9)
}
# Top-K采样函数
def _top_k_sampling(self, logits, k=5):
values, _ = torch.topk(logits, k)
min_value = values[-1]
# 创建一个掩码,保留top-k的值
mask = logits >= min_value
filtered_logits = logits.clone()
# 将非top-k的logits设为负无穷
filtered_logits[~mask] = float('-inf')
# 应用softmax获取概率分布
probs = torch.softmax(filtered_logits, dim=-1)
return probs
# 为类添加辅助方法
self._top_k_sampling = _top_k_sampling.__get__(self, type(self))
masked_data = self.prepare_masked_input(text)
inputs = masked_data["inputs"]
# 查找[MASK]的位置
input_ids = inputs["input_ids"]
mask_positions = [i for i, id in enumerate(input_ids[0]) if id == self.mask_token_id]
if mask_positions:
mask_position = mask_positions[0]
else:
# 如果没有找到MASK标记,使用默认位置
mask_position = masked_data["masked_position"]
print(f"\n掩码词: '{self.mask_token}'")
print(f"掩码文本: '{masked_data['masked_text']}'")
with torch.no_grad():
outputs = self.model(**inputs)
logits = outputs.logits[0, mask_position]
probs = torch.softmax(logits, dim=-1)
# 获取原始单词的概率和排名
if masked_data["original_word"]:
original_word_tokens = self.tokenizer.encode(
masked_data["original_word"],
add_special_tokens=False
)
if original_word_tokens:
original_id = original_word_tokens[0]
original_prob = probs[original_id].item()
original_rank = torch.where(torch.argsort(probs, descending=True) == original_id)[0].item() + 1
print(
f"原始词: '{masked_data['original_word']}', 概率: {original_prob:.4f}, 在词表中排名: #{original_rank}")
# 获取总体词汇表预测的Top 10
top_probs, top_indices = torch.topk(probs, 10)
print("\n模型Top-10预测词:")
for i, (index, prob) in enumerate(zip(top_indices, top_probs)):
token = self.tokenizer.decode([index.item()])
print(f" {i + 1}. '{token}': {prob:.4f}")
print("\n不同解码策略比较结果:")
results = {}
for name, strategy in strategies.items():
# 对每个策略运行5次,查看随机性效果
strategy_results = []
for i in range(5 if "Top" in name else 1): # 贪婪解码是确定性的,只需运行一次
pred_id = strategy(logits).item()
pred_token = self.tokenizer.decode([pred_id])
pred_prob = probs[pred_id].item()
strategy_results.append((pred_token, pred_prob))
results[name] = strategy_results
# 显示结果
for name, strategy_results in results.items():
print(f"\n{name}:")
if len(strategy_results) == 1:
token, prob = strategy_results[0]
print(f" 预测词: '{token}', 概率: {prob:.4f}")
else:
# 对于随机策略,分析多次运行的结果
tokens = [r[0] for r in strategy_results]
# 显示唯一词及其出现次数
unique_tokens = {}
for token in tokens:
if token not in unique_tokens:
unique_tokens[token] = 0
unique_tokens[token] += 1
# 显示结果
print(f" 5次采样结果:")
for token, count in unique_tokens.items():
prob = next(p for t, p in strategy_results if t == token)
print(f" '{token}': {count}/5次, 概率: {prob:.4f}")
# 额外尝试几个更具歧义的文本
if text == "The scientist made a [MASK] discovery that changed the field.":
extra_examples = [
"The weather forecast for tomorrow is [MASK].",
"She felt [MASK] after hearing the unexpected news.",
"The movie was both entertaining and [MASK]."
]
print("\n\n更多测试示例:")
for example in extra_examples:
print(f"\n文本: '{example}'")
# 使用top-p和贪婪解码做对比
masked_data = self.prepare_masked_input(example)
inputs = masked_data["inputs"]
# 查找[MASK]的位置
input_ids = inputs["input_ids"]
mask_positions = [i for i, id in enumerate(input_ids[0]) if id == self.mask_token_id]
if mask_positions:
mask_position = mask_positions[0]
else:
mask_position = masked_data["masked_position"]
with torch.no_grad():
outputs = self.model(**inputs)
logits = outputs.logits[0, mask_position]
# 使用贪婪解码
greedy_id = torch.argmax(logits, dim=-1).item()
greedy_token = self.tokenizer.decode([greedy_id])
# 使用top-p解码
topp_results = []
for _ in range(5):
topp_id = self._nucleus_sampling(logits, 0.7).item()
topp_token = self.tokenizer.decode([topp_id])
topp_results.append(topp_token)
print(f" 贪婪解码: '{greedy_token}'")
print(f" Top-P=0.7 采样 (5次): {', '.join([f'\'{r}\'' for r in topp_results])}")
def main():
"""主函数"""
print("初始化BERT模型解码可视化器...")
# 您可以指定不同的预训练模型,如"bert-large-uncased"或"bert-base-chinese"等
visualizer = DecodingVisualizer("bert-base-uncased")
# 示例1: 基本解码过程
# 展示模型如何从隐藏状态向量解码出词汇表中的token
input_text = "The neural network transforms vectors into tokens through [MASK]."
visualizer.decode_step_by_step(input_text)
# 示例2: 可视化线性变换过程
# 展示隐藏状态和词向量之间的关系
input_text2 = "Language models convert hidden states to vocabulary logits by [MASK]."
visualizer.visualize_linear_transformation(input_text2)
# 示例3: 完整BERT掩码语言模型演示
# 展示BERT如何预测被掩码的多个位置
input_text3 = "Deep learning models perform vector transformations to process natural language."
# 指定具体位置进行掩码演示,这些索引对应分词后的位置
positions_to_mask = [4, 7, 11] # 对应某些单词位置
visualizer.demonstrate_bert_mlm(input_text3, positions_to_mask)
# 示例4: 比较不同解码策略
# 使用更具歧义性的文本能更好地展示不同解码策略的差异
print("\n" + "=" * 60)
print("不同解码策略比较")
print("=" * 60)
# 不传入参数,让函数使用默认的歧义性更强的文本
visualizer.compare_decoding_strategies()
# 可选:尝试其他歧义性文本来比较解码策略
# ambiguous_texts = [
# "The weather forecast for tomorrow is [MASK].",
# "She felt [MASK] after hearing the unexpected news."
# ]
# for text in ambiguous_texts:
# visualizer.compare_decoding_strategies(text)
if __name__ == "__main__":
main()