如何在Hugging Face预训练LLM中获取特定注意力层与FFN层的梯度(类似CNN的Grad-CAM方式)?
如何在Hugging Face预训练LLM中获取特定注意力层与FFN层的梯度(类似CNN的Grad-CAM方式)?
当然可以做到!在Hugging Face的预训练LLM中获取特定注意力层或FFN层的梯度,进而实现类似CNN中Grad-CAM的效果,是完全可行的。下面我结合你提供的Phi-3代码,一步步给你讲清楚怎么做:
核心思路
要实现类似Grad-CAM的效果,我们需要:
- 开启PyTorch的梯度追踪模式(默认预训练模型在eval模式下会关闭梯度)
- 定位到目标层(特定注意力层/FFN层),用PyTorch的钩子机制捕获层的输出和梯度
- 定义一个明确的损失函数(梯度是基于损失反向传播得到的)
- 对梯度和层输出进行加权组合,得到类似热力图的结果
具体实现步骤(基于你的Phi-3代码)
1. 调整模型设置,开启梯度追踪
默认情况下,预训练模型加载后处于eval模式,梯度计算是关闭的。我们需要切换到train模式,但冻结所有参数(避免反向传播时不小心更新模型权重):
# 你的原有模型和tokenizer加载代码保持不变 tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-medium-4k-instruct") model = AutoModelForCausalLM.from_pretrained( "microsoft/Phi-3-medium-4k-instruct", device_map="auto", torch_dtype=torch.float16, trust_remote_code=True ) # 切换到train模式,开启梯度追踪 model.train() # 冻结所有参数,只追踪梯度不更新权重 for param in model.parameters(): param.requires_grad = False
2. 定位目标层并注册钩子
PyTorch的**钩子(Hook)**可以帮我们在模型前向/反向传播时,自动捕获目标层的输出和梯度。首先要确定你要关注的层的路径,Phi-3的每个Transformer层都包含:
- 注意力模块:
model.layers[N].self_attn(N是层索引,从0开始) - FFN(前馈网络)模块:
model.layers[N].mlp
注册钩子保存层输出和梯度:
import torch.nn as nn # 选择目标层:比如第5层的注意力层,你可以替换成任意层或FFN层 target_layer = model.layers[5].self_attn # 如果要选FFN层,替换成:target_layer = model.layers[5].mlp # 用于保存层输出和梯度的变量 layer_outputs = [] grads = [] # 前向钩子:保存层的输出特征 def forward_hook(module, input, output): # 注意力层的输出通常是元组(attn_output, attn_weights),我们取特征部分 if isinstance(output, tuple): layer_outputs.append(output[0].detach()) else: layer_outputs.append(output.detach()) # 反向钩子:保存层的梯度 def backward_hook(module, grad_input, grad_output): grads.append(grad_output[0].detach()) # 注册钩子 forward_handle = target_layer.register_forward_hook(forward_hook) backward_handle = target_layer.register_backward_hook(backward_hook)
3. 准备输入并计算损失
梯度的方向完全由损失函数决定,我们需要根据需求定义损失。这里提供两种常用方式:
# 准备输入(和你原来的代码一致) prompt = "Whats is the co-capital of Greece according to the country's public opinion?" inputs = tokenizer(prompt, return_tensors="pt").to("cuda:0") # 运行模型获取logits outputs = model(input_ids=inputs.input_ids, output_attentions=True) logits = outputs.logits # 方式1:用生成的token作为目标计算损失 # 先生成输出文本 generator = pipeline( "text-generation", model=model, tokenizer=tokenizer, return_full_text=False, max_new_tokens=100, do_sample=False ) generated_output = generator(prompt) generated_text = generated_output[0]["generated_text"] # 把生成的文本转成token generated_tokens = tokenizer(generated_text, return_tensors="pt").input_ids.to("cuda:0") # 模型输入的长度是input_seq_len,我们取对应生成部分的logits input_seq_len = inputs.input_ids.shape[1] generated_logits = logits[:, input_seq_len-1:-1, :] # 对应生成的每个token的logits # 计算交叉熵损失 loss = nn.CrossEntropyLoss()(generated_logits.reshape(-1, logits.shape[-1]), generated_tokens.reshape(-1)) # 方式2:如果不需要生成,也可以直接针对输入最后一个token的logits计算损失(比如已知目标token) # target_token = tokenizer.encode("Thessaloniki")[0] # target_token = torch.tensor([target_token], device="cuda:0") # last_logits = logits[:, -1, :] # loss = nn.CrossEntropyLoss()(last_logits, target_token)
4. 反向传播获取梯度并生成类似Grad-CAM的热力图
# 反向传播计算梯度 loss.backward() # 提取保存的梯度和层输出 grad = grads[0] layer_out = layer_outputs[0] # 类似Grad-CAM的处理:对梯度在序列维度做全局平均池化,得到权重alpha # Phi-3的注意力层输出形状是(batch, seq_len, hidden_dim),所以对seq_len维度做平均 alpha = grad.mean(dim=1, keepdim=True) # 用权重加权层输出,得到CAM热力图 cam = torch.sum(alpha * layer_out, dim=-1) # 归一化到0-1区间,方便后续可视化 cam = torch.relu(cam) cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8) # 加小值避免除零错误 # 现在可以把热力图和输入token对应起来可视化 input_tokens = tokenizer.convert_ids_to_tokens(inputs.input_ids[0]) cam_np = cam[0].cpu().numpy() # 转成numpy数组用于画图 # 移除钩子,避免影响后续模型操作 forward_handle.remove() backward_handle.remove()
关键注意事项
- 模型结构适配:不同LLM(比如Llama、GPT-2)的层结构命名可能略有不同,你可以通过
print(model)查看模型层级,找到正确的目标层路径。 - 梯度追踪开关:必须确保模型在允许梯度计算的模式下(
train模式或手动开启requires_grad),否则无法捕获梯度。 - 损失的选择:梯度的方向完全由损失函数决定,你需要根据自己的需求定义损失(比如针对生成的某个token、输入的某个位置等)。
- 钩子的移除:使用完钩子后记得移除,避免后续模型运行时重复捕获数据。
备注:内容来源于stack exchange,提问作者Jose Ramon
相关产品推荐
相关产品推荐

