<think>根据错误信息,评估过程中出现了KeyError: 'anchor_input_ids'。这是因为在评估阶段,我们使用了评估专用的数据收集器EvalDataCollator,它返回的字典中只包含'input_ids', 'attention_mask', 'labels'三个键。但是在compute_loss方法中,我们仍然试图从inputs中获取'anchor_input_ids'等键,这导致了KeyError。
解决方案:
我们需要修改ContrastiveTrainer类,使其在评估阶段使用不同的损失计算方式。具体来说:
1. 在评估阶段,我们只计算语言建模损失(LM loss),不计算对比损失。
2. 在compute_loss方法中,我们需要判断当前是训练阶段还是评估阶段。如果是评估阶段,则只计算LM损失;如果是训练阶段,则计算总损失(LM损失+对比损失)。
修改步骤:
在compute_loss方法中,我们首先检查输入中是否包含'anchor_input_ids'。如果包含,说明是训练阶段,执行三元组的对比损失计算。如果不包含,则说明是评估阶段,只计算语言建模损失。
具体代码修改如下:</think>### 问题分析
根据错误日志,评估过程中出现了一个关键错误:
```
KeyError: 'anchor_input_ids'
Traceback (most recent call last):
File "Contrastive_Training_LM.py", line 224, in compute_loss
anchor_ids = inputs["anchor_input_ids"]
```
这个错误发生在评估阶段,具体位置在:
```python
def compute_loss(self, model, inputs, return_outputs=False):
anchor_ids = inputs["anchor_input_ids"] # 这里出现KeyError
```
### 错误原因分析
1. **问题根源**:
- 在评估阶段,我们使用了专门的数据收集器`EvalDataCollator`
- 这个收集器返回的数据格式是`{"input_ids": ..., "attention_mask": ..., "labels": ...}`
- 但`compute_loss`方法期望的是训练阶段的数据格式`{"anchor_input_ids": ..., ...}`
2. **具体原因**:
- 评估阶段使用了不同的数据格式(单一样本 vs 三元组)
- `compute_loss`方法没有区分训练和评估阶段的数据处理
- 方法试图访问评估数据中不存在的`anchor_input_ids`字段
### 解决方案
我们需要修改`compute_loss`方法,使其能够正确处理两种不同的数据格式:
```python
class ContrastiveTrainer(Trainer):
# ... [其他方法保持不变] ...
def compute_loss(self, model, inputs, return_outputs=False):
"""改进的损失计算,兼容训练和评估两种模式"""
# 检查输入数据格式
if "anchor_input_ids" in inputs:
# 训练模式:处理三元组数据
return self._compute_training_loss(model, inputs, return_outputs)
else:
# 评估模式:处理单一样本数据
return self._compute_evaluation_loss(model, inputs, return_outputs)
def _compute_training_loss(self, model, inputs, return_outputs=False):
"""训练阶段的损失计算(处理三元组)"""
# 提取输入
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"]
# ... [原有训练损失计算代码] ...
# 返回训练损失
return (total_loss, lm_outputs) if return_outputs else total_loss
def _compute_evaluation_loss(self, model, inputs, return_outputs=False):
"""评估阶段的损失计算(处理单一样本)"""
# 提取评估输入
input_ids = inputs["input_ids"]
attention_mask = inputs["attention_mask"]
labels = inputs["labels"]
# 计算语言建模损失
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask,
labels=labels
)
loss = outputs.loss
# 记录内存使用
print_memory_usage()
return (loss, outputs) if return_outputs else loss
```
### 完整修改代码
以下是完整的修改方案,包括上述修复和必要的优化:
```python
class ContrastiveTrainer(Trainer):
"""内存优化的训练器"""
def __init__(self, tokenizer=None, *args, contrastive_config=None, **kwargs):
# ... [初始化代码保持不变] ...
def compute_contrastive_loss(self, anchor_emb, pos_emb, neg_emb):
# ... [保持不变] ...
def get_sequence_representation(self, outputs, attention_mask):
# ... [保持不变] ...
def compute_loss(self, model, inputs, return_outputs=False):
"""改进的损失计算,兼容训练和评估两种模式"""
# 检查输入数据格式
if "anchor_input_ids" in inputs:
# 训练模式:处理三元组数据
return self._compute_training_loss(model, inputs, return_outputs)
else:
# 评估模式:处理单一样本数据
return self._compute_evaluation_loss(model, inputs, return_outputs)
def _compute_training_loss(self, model, inputs, return_outputs=False):
"""训练阶段的损失计算(处理三元组)"""
# 提取输入
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
def _compute_evaluation_loss(self, model, inputs, return_outputs=False):
"""评估阶段的损失计算(处理单一样本)"""
# 提取评估输入
input_ids = inputs["input_ids"]
attention_mask = inputs["attention_mask"]
labels = inputs["labels"]
# 计算语言建模损失
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask,
labels=labels
)
loss = outputs.loss
# 记录内存使用
print_memory_usage()
return (loss, outputs) if return_outputs else loss
def evaluate(
self,
eval_dataset: Optional[Dataset] = None,
ignore_keys: Optional[List[str]] = None,
metric_key_prefix: str = "eval",
) -> Dict[str, float]:
"""重写评估方法以使用专用的数据收集器"""
# 创建评估专用的数据收集器
eval_data_collator = EvalDataCollator(
tokenizer=self.tokenizer,
max_length=256,
padding="max_length"
)
# 临时保存原始数据收集器
original_collator = self.data_collator
try:
# 使用评估专用的数据收集器
self.data_collator = eval_data_collator
# 调用父类的评估方法
return super().evaluate(
eval_dataset=eval_dataset,
ignore_keys=ignore_keys,
metric_key_prefix=metric_key_prefix
)
finally:
# 恢复原始数据收集器
self.data_collator = original_collator
```
### 关键修改解释
1. **添加数据格式检测**:
```python
def compute_loss(self, model, inputs, return_outputs=False):
if "anchor_input_ids" in inputs:
return self._compute_training_loss(...)
else:
return self._compute_evaluation_loss(...)
```
- 根据输入数据中是否存在`anchor_input_ids`字段判断模式
- 训练模式使用三元组损失计算
- 评估模式使用标准语言建模损失
2. **分离损失计算逻辑**:
```python
def _compute_training_loss(self, model, inputs, return_outputs=False):
# 训练阶段的三元组损失计算
# ... [原有代码] ...
def _compute_evaluation_loss(self, model, inputs, return_outputs=False):
# 评估阶段的标准LM损失计算
outputs = model(input_ids=input_ids, ...)
loss = outputs.loss
return loss
```
- 将训练和评估的逻辑完全分离
- 评估阶段只计算语言建模损失,不涉及对比学习
3. **保持评估流程不变**:
```python
def evaluate(self, ...):
# 使用专用数据收集器
eval_data_collator = EvalDataCollator(...)
self.data_collator = eval_data_collator
return super().evaluate(...)
```
- 评估阶段仍然使用专门的数据收集器
- 确保输入数据格式与评估损失计算匹配
### 评估流程优化
1. **评估数据流**:
```mermaid
graph LR
A[评估数据集] --> B[EvalDataCollator]
B --> C[生成 input_ids, attention_mask, labels]
C --> D[模型前向传播]
D --> E[计算LM损失]
```
2. **评估阶段特点**:
- 不计算对比损失,只关注语言建模能力
- 使用正样本作为评估输入(代表目标风格)
- 计算标准困惑度(perplexity)指标
3. **为什么评估不使用对比损失**:
- 评估阶段需要标准化的评估指标
- 语言建模损失是广泛认可的基准指标
- 对比损失更适合训练阶段的风格学习
### 进一步优化建议
1. **添加评估指标记录**:
```python
def _compute_evaluation_loss(self, model, inputs, return_outputs=False):
# ... [损失计算] ...
# 记录额外指标
logits = outputs.logits
preds = torch.argmax(logits, dim=-1)
accuracy = (preds == labels).float().mean()
# 返回包含指标的字典
if return_outputs:
return loss, {
"loss": loss,
"accuracy": accuracy,
"outputs": outputs
}
else:
return loss
```
2. **自定义评估回调**:
```python
from transformers import TrainerCallback
class ContrastiveEvalCallback(TrainerCallback):
def on_evaluate(self, args, state, control, **kwargs):
# 计算风格相似度指标
style_sim = self.calculate_style_similarity(kwargs['model'], kwargs['eval_dataset'])
# 添加到评估结果
kwargs['metrics'].update({"eval_style_similarity": style_sim})
def calculate_style_similarity(self, model, dataset):
# 实现风格相似度计算
return similarity_score
```
3. **优化评估内存使用**:
```python
def _compute_evaluation_loss(self, model, inputs, return_outputs=False):
with torch.no_grad(): # 禁用梯度计算
with torch.cuda.amp.autocast(): # 混合精度
outputs = model(...)
# ... [其他计算] ...
```