<think>我们遇到了一个新的错误:`RuntimeError: only Tensors of floating point dtype can require gradients`。
这个错误发生在`compute_loss`函数中,当我们尝试设置`anchor_ids.requires_grad_()`时。因为`anchor_ids`是整数类型的张量(通常是long或int类型),而整数张量不能要求梯度。在PyTorch中,只有浮点类型的张量才能有梯度。
解决方案:
我们不应该对输入ID(整数)设置梯度,因为它们只是索引。我们需要梯度的是模型中的参数(通过LoRA适配器)和浮点类型的中间表示(如隐藏状态)。
因此,我们需要修改代码,移除对整数张量设置梯度的操作,同时确保模型中的可训练参数(LoRA参数)的梯度能够正常计算。
具体修改:
1. 在`compute_loss`函数中,移除对输入ID和注意力掩码设置`requires_grad_()`的操作,因为这些张量是整数类型,不需要梯度。
2. 确保在计算嵌入表示时,模型输出的隐藏状态是浮点类型并且需要梯度(这通常由模型内部自动处理,只要模型参数需要梯度,其输出就会需要梯度)。
3. 在`get_sequence_representation`中,我们返回的嵌入是浮点类型,所以我们可以对其设置`requires_grad_()`(但通常不需要显式设置,因为如果模型参数需要梯度,那么这些输出也会自动需要梯度)。
但是,我们之前显式设置嵌入为`requires_grad_()`可能是为了确保梯度,但在模型参数需要梯度的情况下,这实际上是多余的。因此,我们可以移除这些显式设置,以避免潜在的类型错误(尽管浮点张量设置`requires_grad_()`不会出错,但整数张量会)。
因此,我们修改如下:
- 删除在`compute_loss`中对整数张量(`anchor_ids`, `anchor_mask`等)设置`requires_grad_()`的代码。
- 在`get_sequence_representation`中,我们不再显式设置`embeddings.requires_grad_()`,因为模型输出的隐藏状态已经与模型参数关联,如果模型参数需要梯度,那么这些隐藏状态也会自动需要梯度。
另外,我们之前为了确保梯度传播,在`compute_contrastive_loss`中也对嵌入张量设置了`requires_grad_()`,但这是浮点张量,所以不会出错。但同样,如果模型参数需要梯度,这些嵌入张量也会自动需要梯度。所以我们可以移除这些设置,或者保留(因为它们不会导致错误)。
为了简化,我们移除所有不必要的`requires_grad_()`设置,只保留必要的部分(即模型参数本身需要梯度,这由LoRA和模型训练模式保证)。
修改后的`compute_loss`函数中关于输入处理的部分:
原错误代码:
anchor_ids = anchor_ids.requires_grad_()
... # 其他整数张量同样设置
修改为:直接使用这些输入张量,不设置梯度。
同时,在`get_sequence_representation`中,移除`embeddings = embeddings.requires_grad_()`。
在`compute_contrastive_loss`中,我们也可以移除显式设置梯度的代码。
但是,我们之前添加这些设置是因为出现了梯度为None的问题。因此,我们需要确保即使不显式设置,梯度也能正常传播。这取决于模型是否输出了需要梯度的张量。
由于我们使用了LoRA,并且模型的基础参数被冻结,只有LoRA参数是可训练的,所以模型输出的隐藏状态应该会带有梯度(只要它们是通过可训练层计算得到的)。
因此,我们进行如下调整:
1. 在`compute_loss`中,不再对整数输入设置`requires_grad_()`。
2. 在`get_sequence_representation`中,直接返回隐藏状态,不设置梯度。
3. 在`compute_contrastive_loss`中,不再显式设置嵌入张量的梯度。
同时,我们确保模型处于训练模式(在`compute_loss`开始时调用`model.train()`)。
此外,我们还需要注意:在计算对比损失时,我们使用的嵌入张量应该是通过模型计算得到的,这些张量应该会自动需要梯度。
修改后的代码段:
```python
def compute_loss(self, model, inputs, return_outputs=False):
"""内存优化的损失计算"""
# 确保模型处于训练模式
model.train()
# 提取输入(这些是整数张量,不需要设置梯度)
anchor_ids = inputs["anchor_input_ids"]
anchor_mask = inputs["anchor_attention_mask"]
positive_ids = inputs["positive_input_ids"]
positive_mask = inputs["positive_attention_mask"]
negative_ids = inputs["negative_input_ids"]
negative_mask = inputs["negative_attention_mask"]
# 前向传播获取隐藏状态
def get_embeddings(input_ids, attention_mask):
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask,
output_hidden_states=True,
return_dict=True
)
return self.get_sequence_representation(outputs, attention_mask)
# 获取三元组的嵌入表示
anchor_emb = get_embeddings(anchor_ids, anchor_mask)
pos_emb = get_embeddings(positive_ids, positive_mask)
neg_emb = get_embeddings(negative_ids, negative_mask)
# 计算对比损失
cl_loss = self.compute_contrastive_loss(anchor_emb, pos_emb, neg_emb)
cl_loss = cl_loss * self.contrastive_weight
# ... 其余部分保持不变
```
同时,修改`get_sequence_representation`:
```python
def get_sequence_representation(self, outputs, attention_mask):
"""获取序列表示(内存优化版)"""
# 只获取需要的隐藏状态层
hidden_states = outputs.hidden_states[self.repr_layer]
# 获取每个序列的最后一个非填充token
seq_lengths = attention_mask.sum(dim=1) - 1
batch_indices = torch.arange(hidden_states.size(0))
# 返回对应位置的隐藏状态
return hidden_states[batch_indices, seq_lengths]
```
以及`compute_contrastive_loss`中移除显式设置梯度的代码:
```python
def compute_contrastive_loss(self, anchor_emb, pos_emb, neg_emb):
"""计算对比损失"""
# 计算余弦相似度
pos_sim = F.cosine_similarity(anchor_emb, pos_emb)
neg_sim = F.cosine_similarity(anchor_emb, neg_emb)
# 计算InfoNCE损失
numerator = torch.exp(pos_sim / self.temperature)
denominator = numerator + torch.exp(neg_sim / self.temperature)
info_nce_loss = -torch.log(numerator / (denominator + 1e-8)).mean()
# 计算三元组损失
triplet_loss = F.relu(neg_sim - pos_sim + self.margin).mean()
return info_nce_loss + triplet_loss
```
这样修改后,我们避免了对整数张量设置梯度,同时依赖模型本身的梯度传播机制。
但是,我们还需要确保模型中的LoRA参数是可训练的,并且模型在训练模式下。在代码中,我们已经将模型用`get_peft_model`包装,并且训练器会将其置于训练模式。
另外,注意在训练参数中我们启用了梯度检查点(`gradient_checkpointing=True`),这有助于减少内存使用。
最后,关于`Trainer.tokenizer is now deprecated`的警告,我们可以忽略,因为我们已经传递了tokenizer给自定义的Trainer,并且我们在数据收集器中使用它。
现在,我们整合以上修改,提供完整的代码。</think>### 问题分析
错误信息表明:
```
RuntimeError: only Tensors of floating point dtype can require gradients
```
这个错误发生在尝试为整数类型的张量设置梯度需求时:
```python
anchor_ids = anchor_ids.requires_grad_()
```
根本原因是:
1. `anchor_ids` 是整数类型的张量(通常是 `torch.long`)
2. PyTorch 只允许浮点类型张量(如 `torch.float32` 或 `torch.float16`)设置梯度
3. 输入ID张量(包含词汇索引)必须是整数类型,不能直接设置为需要梯度
### 完整解决方案
以下是修复后的完整代码,主要修改点:
1. 移除对整数张量的梯度设置
2. 确保只对浮点张量设置梯度需求
3. 优化梯度传播路径
```python
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
PreTrainedTokenizerBase,
BitsAndBytesConfig
)
from transformers.tokenization_utils_base import PreTrainedTokenizerBase
from transformers.utils import PaddingStrategy
from datasets import load_dataset
from typing import Any, Dict, List, Optional, Tuple, Union
import logging
from dataclasses import dataclass
import os
import gc
from peft import LoraConfig, get_peft_model
# 设置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 内存优化工具函数
def clear_memory():
"""清除Python和CUDA缓存"""
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.reset_peak_memory_stats()
def print_memory_usage():
"""打印当前内存使用情况"""
if torch.cuda.is_available():
allocated = torch.cuda.memory_allocated() / (1024 ** 3)
reserved = torch.cuda.memory_reserved() / (1024 ** 3)
logger.info(f"GPU内存使用: 已分配 {allocated:.2f}GB, 保留 {reserved:.2f}GB")
else:
logger.info("未检测到GPU")
def tokenize_function(examples, tokenizer, max_length=256):
"""将文本转换为token IDs"""
tokenized = {}
# 对每个字段进行分词
for key in ['anchor', 'positive', 'negative']:
if key in examples:
# 使用分词器处理文本
result = tokenizer(
examples[key],
max_length=max_length,
truncation=True,
padding=False,
return_tensors=None
)
tokenized[f"{key}_input_ids"] = result["input_ids"]
return tokenized
@dataclass
class ContrastiveDataCollator:
"""内存优化的数据收集器"""
tokenizer: PreTrainedTokenizerBase
padding: Union[bool, str, PaddingStrategy] = True
max_length: Optional[int] = None
pad_to_multiple_of: Optional[int] = None
return_tensors: str = "pt"
def __call__(self, features: List[Dict[str, Any]]) -> Dict[str, torch.Tensor]:
# 分离出三元组的各个部分
anchor_features = [{"input_ids": f["anchor_input_ids"]} for f in features]
positive_features = [{"input_ids": f["positive_input_ids"]} for f in features]
negative_features = [{"input_ids": f["negative_input_ids"]} for f in features]
# 对每个部分分别进行填充
batch_anchor = self.tokenizer.pad(
anchor_features,
padding=self.padding,
max_length=self.max_length,
pad_to_multiple_of=self.pad_to_multiple_of,
return_tensors=self.return_tensors,
)
batch_positive = self.tokenizer.pad(
positive_features,
padding=self.padding,
max_length=self.max_length,
pad_to_multiple_of=self.pad_to_multiple_of,
return_tensors=self.return_tensors,
)
batch_negative = self.tokenizer.pad(
negative_features,
padding=self.padding,
max_length=self.max_length,
pad_to_multiple_of=self.pad_to_multiple_of,
return_tensors=self.return_tensors,
)
# 创建注意力掩码
def create_attention_mask(input_ids):
return (input_ids != self.tokenizer.pad_token_id).int()
# 释放中间变量内存
del anchor_features, positive_features, negative_features
clear_memory()
return {
"anchor_input_ids": batch_anchor["input_ids"],
"anchor_attention_mask": create_attention_mask(batch_anchor["input_ids"]),
"positive_input_ids": batch_positive["input_ids"],
"positive_attention_mask": create_attention_mask(batch_positive["input_ids"]),
"negative_input_ids": batch_negative["input_ids"],
"negative_attention_mask": create_attention_mask(batch_negative["input_ids"]),
}
class ContrastiveTrainer(Trainer):
"""内存优化的训练器"""
def __init__(self, tokenizer=None, *args, contrastive_config=None, **kwargs):
# 首先调用父类初始化
super().__init__(*args, **kwargs)
# 关键修复:设置tokenizer
self.tokenizer = tokenizer
if contrastive_config is None:
contrastive_config = {}
# 设置默认值
self.temperature = contrastive_config.get("temperature", 0.07)
self.margin = contrastive_config.get("margin", 0.3)
self.contrastive_weight = contrastive_config.get("weight", 0.8)
self.repr_layer = contrastive_config.get("repr_layer", -1)
# 验证必要参数
if not hasattr(self.model.config, "output_hidden_states") or not self.model.config.output_hidden_states:
raise ValueError("模型必须设置output_hidden_states=True")
self.cross_entropy = nn.CrossEntropyLoss()
def compute_contrastive_loss(self, anchor_emb, pos_emb, neg_emb):
"""计算对比损失"""
# 计算余弦相似度
pos_sim = F.cosine_similarity(anchor_emb, pos_emb)
neg_sim = F.cosine_similarity(anchor_emb, neg_emb)
# 计算InfoNCE损失
numerator = torch.exp(pos_sim / self.temperature)
denominator = numerator + torch.exp(neg_sim / self.temperature)
info_nce_loss = -torch.log(numerator / (denominator + 1e-8)).mean()
# 计算三元组损失
triplet_loss = F.relu(neg_sim - pos_sim + self.margin).mean()
return info_nce_loss + triplet_loss
def get_sequence_representation(self, outputs, attention_mask):
"""获取序列表示(内存优化版)"""
# 只获取需要的隐藏状态层
hidden_states = outputs.hidden_states[self.repr_layer]
# 获取每个序列的最后一个非填充token
seq_lengths = attention_mask.sum(dim=1) - 1
batch_indices = torch.arange(hidden_states.size(0))
# 返回对应位置的隐藏状态
return hidden_states[batch_indices, seq_lengths]
def compute_loss(self, model, inputs, return_outputs=False):
"""内存优化的损失计算"""
# 确保模型处于训练模式
model.train()
# 提取输入(整数张量,不需要梯度)
anchor_ids = inputs["anchor_input_ids"]
anchor_mask = inputs["anchor_attention_mask"]
positive_ids = inputs["positive_input_ids"]
positive_mask = inputs["positive_attention_mask"]
negative_ids = inputs["negative_input_ids"]
negative_mask = inputs["negative_attention_mask"]
# 前向传播获取隐藏状态
def get_embeddings(input_ids, attention_mask):
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask,
output_hidden_states=True,
return_dict=True
)
return self.get_sequence_representation(outputs, attention_mask)
# 获取三元组的嵌入表示(浮点张量)
anchor_emb = get_embeddings(anchor_ids, anchor_mask)
pos_emb = get_embeddings(positive_ids, positive_mask)
neg_emb = get_embeddings(negative_ids, negative_mask)
# 计算对比损失
cl_loss = self.compute_contrastive_loss(anchor_emb, pos_emb, neg_emb)
cl_loss = cl_loss * self.contrastive_weight
# 关键修复:确保tokenizer已设置
if self.tokenizer is None:
raise ValueError("Tokenizer未设置!")
# 计算语言建模损失
lm_labels = positive_ids.clone()
# 关键修复:使用tokenizer的pad_token_id
pad_token_id = self.tokenizer.pad_token_id
lm_labels[lm_labels == pad_token_id] = -100
# 计算语言建模损失
lm_outputs = model(
input_ids=positive_ids,
attention_mask=positive_mask,
labels=lm_labels
)
lm_loss = lm_outputs.loss
# 总损失 = LM损失 + 对比损失
total_loss = lm_loss + cl_loss
# 记录内存使用
print_memory_usage()
return (total_loss, lm_outputs) if return_outputs else total_loss
# ================ 主程序 ================ #
if __name__ == "__main__":
# 配置量化以减少内存使用
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # 使用4位量化
bnb_4bit_quant_type="nf4", # 使用NF4量化类型
bnb_4bit_use_double_quant=True, # 双重量化
bnb_4bit_compute_dtype=torch.float16 # 计算使用FP16
)
# 加载模型和分词器(使用量化)
model = AutoModelForCausalLM.from_pretrained(
"model/Qwen/Qwen1.5-1.8B",
quantization_config=bnb_config, # 应用量化配置
device_map="auto", # 自动选择设备
output_hidden_states=True, # 必须设置以获取隐藏状态
return_dict_in_generate=True,
use_cache=False # 禁用缓存以节省内存
)
tokenizer = AutoTokenizer.from_pretrained("model/Qwen/Qwen1.5-1.8B")
tokenizer.pad_token = tokenizer.eos_token # 设置填充token
# 为量化模型添加LoRA适配器
lora_config = LoraConfig(
r=8,
lora_alpha=32,
target_modules=["q_proj", "v_proj"], # 针对Qwen1.5-1.8B模型
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters() # 打印可训练参数数量
# 加载数据集
def load_and_tokenize_dataset(file_path, tokenizer):
"""加载数据集并进行分词处理"""
# 加载原始数据集
dataset_dict = load_dataset('json', data_files=file_path)
raw_dataset = dataset_dict['train']
# 应用分词函数
tokenized_dataset = raw_dataset.map(
lambda ex: tokenize_function(ex, tokenizer, max_length=256),
batched=True,
batch_size=8, # 减小批处理大小
remove_columns=['anchor', 'positive', 'negative']
)
return tokenized_dataset
train_dataset = load_and_tokenize_dataset('data/processed/train_style_triplets.json', tokenizer)
val_dataset = load_and_tokenize_dataset('data/processed/val_style_triplets.json', tokenizer)
# 验证数据集格式
print("训练集样本示例:", train_dataset[0])
print("验证集样本示例:", val_dataset[0])
# 训练参数配置(内存优化)
training_args = TrainingArguments(
output_dir="./model/lora_adapter",
per_device_train_batch_size=1, # 减小批量大小
gradient_accumulation_steps=8, # 增加梯度累积步数
num_train_epochs=3,
learning_rate=2e-4,
logging_steps=10, # 更频繁的日志记录以监控内存
save_steps=500,
fp16=True,
report_to="none",
remove_unused_columns=False,
gradient_checkpointing=True, # 启用梯度检查点
optim="adafactor", # 使用内存更少的优化器
)
# 对比学习配置
contrastive_config = {
"temperature": 0.07,
"margin": 0.3,
"weight": 0.8,
"repr_layer": -1
}
# 初始化数据收集器
data_collator = ContrastiveDataCollator(
tokenizer=tokenizer,
max_length=256, # 减少最大长度
padding="max_length"
)
# 初始化训练器 - 关键修复:传递tokenizer
trainer = ContrastiveTrainer(
model=model,
args=training_args,
tokenizer=tokenizer, # 传递tokenizer
data_collator=data_collator,
train_dataset=train_dataset,
eval_dataset=val_dataset,
contrastive_config=contrastive_config
)
# 开始训练前打印内存状态
print_memory_usage()
# 开始训练
trainer.train()
# 保存LoRA适配器
model.save_pretrained("./model/lora_adapter")
# 评估模型
eval_results = trainer.evaluate()
print("评估结果:", eval_results)
```
### 关键修复点解释
1. **移除整数张量的梯度设置**:
```python
# 修改前(错误代码)
anchor_ids = anchor_ids.requires_grad_()
# 修改后(正确代码)
# 直接使用输入张量,不设置梯度
anchor_ids = inputs["anchor_input_ids"]
```
- 输入ID张量是整数类型(`torch.long`),不能设置梯度
- 只有模型输出的浮点张量(隐藏状态)才需要梯度
2. **简化梯度传播**:
```python
def get_sequence_representation(self, outputs, attention_mask):
# 修改前(多余代码)
embeddings = embeddings.requires_grad_()
# 修改后(正确代码)
return hidden_states[batch_indices, seq_lengths]
```
- 模型输出的隐藏状态已经是浮点类型
- 如果模型参数需要梯度,输出会自动保留梯度计算图
- 不需要显式设置 `requires_grad_()`
3. **优化对比损失计算**:
```python
def compute_contrastive_loss(self, anchor_emb, pos_emb, neg_emb):
# 修改前(多余代码)
anchor_emb = anchor_emb.requires_grad_()
# 修改后(正确代码)
# 直接使用输入张量计算损失
pos_sim = F.cosine_similarity(anchor_emb, pos_emb)
```
- 嵌入表示已经是浮点张量
- 损失函数会自动保留梯度计算图
### PyTorch梯度机制详解
1. **梯度传播规则**:
- 整数张量:不能有梯度,只能用于索引和离散值
- 浮点张量:可以有梯度,用于连续值计算
- 模型参数:默认需要梯度(`requires_grad=True`)
2. **计算图构建**:
```mermaid
graph LR
A[输入IDs] --> B(嵌入层)
B --> C[浮点嵌入]
C --> D[模型层]
D --> E[隐藏状态]
E --> F[损失函数]
F --> G[梯度]
G --> H[参数更新]
```
- 输入IDs(整数)→ 嵌入层 → 浮点嵌入(可梯度)→ 模型计算 → 损失函数
3. **LoRA训练的特殊性**:
- 基础模型参数被冻结(`requires_grad=False`)
- LoRA适配器参数需要梯度(`requires_grad=True`)
- 梯度只通过LoRA参数传播
### 性能优化建议
1. **使用更小的模型**:
- 如果内存仍然不足,考虑使用更小的预训练模型
- 如 Qwen-1.5-0.5B 或 Qwen-1.5-0.3B
2. **调整LoRA配置**:
```python
lora_config = LoraConfig(
r=4, # 减小秩
lora_alpha=16, # 减小alpha值
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
```
3. **优化批处理策略**:
```python
training_args = TrainingArguments(
per_device_train_batch_size=2, # 适当增加批大小
gradient_accumulation_steps=4, # 调整梯度累积步数
gradient_checkpointing=True, # 启用梯度检查点
fp16=True, # 使用半精度
optim="adafactor", # 内存高效优化器
)
```
4. **监控内存使用**:
```python
# 在关键位置添加内存监控
print_memory_usage()
```