一、概述
T5 使用常规交叉熵损失(与任何语言模型一样)。
假设您正在微调 T5 以进行翻译,并且您有以下训练示例:
* source sentence: "hello how are you"
* target sentence: "salut comment ça-va"
首先,需要使用 对模型的句子进行标记。假设每个单词都被标记化为一个标记,并且我们还添加了 T5 的特殊标记(即 - 它表示序列的结束),我们为模型提供以下输入:T5Tokenizer
* input tokens = [hello, how, are, you, ]
* label tokens = [salut, comment, ça, -, va, ]
当然,我们不会将这些标记作为文本提供给模型,而是以整数 ID 的形式提供,这些 ID 指的是嵌入矩阵中的行索引,因此实际输入将如下所示:
* input_ids = [21820, 149, 33, 25, 1]
* labels = [20239, 1670, 3664, 18, 900, 1]
在这种情况下,您首先向 T5 的编码器提供,该编码器会将其转换为形状的张量。接下来,T5 的解码器将为目标序列的每个标记预测正确的下一个标记。具体如下:input_ids(batch_size, seq_len, hidden_size)
salut comment ça - va => label tokens
20239 1670 3664 18 900 1 => labels
----------------------------------------------------------------------------------------------
DECODER
----------------------------------------------------------------------------------------------
0 20239 1670 3664 18 900 => decoder_input_ids
decoder_start_token salut comment ça - va => decoder input tokens
换句话说,我们用一个特殊的标记(解码器开始标记 - 对于 T5 来说是填充标记,索引为 0)在解码器输入前面,然后解码器需要(并行)预测:
解码器启动令牌后面的令牌是“Salut”。在这里,我们计算模型预测与目标标记(即“致敬”)之间的交叉熵损失。
“Salut”后面的标记是“comment”。在这里,我们计算模型预测和目标标记(即“注释”)之间的交叉熵损失。
“comment”后面的标记是“ça”。在这里,我们计算模型预测和目标标记(即“ça”)之间的交叉熵损失。
等。
“va”后面的令牌是“”(意思是序列末尾或EOS令牌)。在这里,我们计算模型预测与目标标记(“”)之间的交叉熵损失。
在法典 389,这是一次性完成的,即通过将模型的对数(形状为 batch_size、seq_len、vocab_size))与地面真值标签进行比较:
loss = loss_fct(lm_logits.view(-1, lm_logits.size(-1)), labels.view(-1))
引用: What is loss function for T5 - Models - Hugging Face Forums
二、详细讲解
import torch
from torch import nn
from transformers import T5ForConditionalGeneration, T5Tokenizer
# 修改后的自定义损失函数
class CustomLoss(nn.Module):
def __init__(self):
super(CustomLoss, self).__init__()
self.loss_fn = nn.CrossEntropyLoss(reduction='none')
def forward(self, outputs, labels):
logits = outputs.logits
batch_size, seq_len, vocab_size = logits.size()
# 获取每个标签序列的实际长度(去掉pad)
label_lengths = (labels != tokenizer.pad_token_id).sum(dim=1)
# 计算权重
weights = torch.zeros_like(labels, dtype=torch.float)
for i, length in enumerate(label_lengths):
length = length.item()
weights[i, :length] = torch.arange(length + 1, 1, -1, dtype=torch.float)
# 计算损失
loss = self.loss_fn(logits.view(-1, vocab_size), labels.view(-1))
loss = loss.view(batch_size, seq_len)
# 应用权重
weighted_loss = loss * weights
# 计算平均损失
weighted_loss = weighted_loss.sum() / weights.sum()
return weighted_loss
# 加载模型和分词器
model_name = 't5-small'
model = T5ForConditionalGeneration.from_pretrained(model_name)
tokenizer = T5Tokenizer.from_pretrained(model_name)
# 示例训练数据
train_data = [
{'input_text': 'translate English to French: Hello, how are you?', 'target_text': 'Bonjour, comment ça va?'},
# 添加更多训练数据...
]
# 训练函数
def train(model, tokenizer, custom_loss_fn, train_data, epochs=3, lr=1e-4):
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
for epoch in range(epochs):
model.train()
total_loss = 0
for batch in train_data:
inputs = tokenizer(batch['input_text'], return_tensors='pt', padding=True, truncation=True).input_ids
labels = tokenizer(batch['target_text'], return_tensors='pt', padding=True, truncation=True).input_ids
outputs = model(input_ids=inputs, labels=labels)
loss = custom_loss_fn(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
avg_loss = total_loss / len(train_data)
print(f'Epoch {epoch + 1}, Loss: {avg_loss}')
# 初始化自定义损失函数
custom_loss_fn = CustomLoss()
# 训练模型
train(model, tokenizer, custom_loss_fn, train_data)
模型输出 logits
在 T5 模型中,输出的 logits 张量通常有以下形状:
logits.shape -> (batch_size, seq_len, vocab_size)
batch_size:一个批次中的样本数量。
seq_len:每个样本的序列长度。
vocab_size:词汇表的大小,表示可能的词汇数。
真实标签 labels
真实标签 labels 张量的形状为:
labels.shape -> (batch_size, seq_len)
batch_size:一个批次中的样本数量。
seq_len:每个样本的序列长度。
计算交叉熵损失时的维度转换
为了计算交叉熵损失,我们需要将 logits 和 labels 转换为合适的形状。具体步骤如下:
转换 logits 的形状:
交叉熵损失函数期望 logits 的形状为 (N, C),其中 N 是样本数,C 是每个样本的类别数。对于序列任务,这里的 N 等于 batch_size * seq_len,C 等于 vocab_size。
因此,我们需要将 logits 的形状从 (batch_size, seq_len, vocab_size) 转换为 (batch_size * seq_len, vocab_size)。
logits = logits.view(-1, vocab_size)
转换 labels 的形状:
交叉熵损失函数期望 labels 的形状为 (N),其中 N 是样本数。对于序列任务,这里的 N 也是 batch_size * seq_len。
因此,我们需要将 labels 的形状从 (batch_size, seq_len) 转换为 (batch_size * seq_len)。
labels = labels.view(-1)
计算交叉熵损失
在转换形状后,我们可以计算交叉熵损失。nn.CrossEntropyLoss 函数会自动将 logits 传递到 softmax 函数中,因此我们只需传入转换后的 logits 和 labels。
loss = self.loss_fn(logits.view(-1, vocab_size), labels.view(-1))
代码解析
提取模型输出的 logits 并获取其形状:
logits = outputs.logits batch_size, seq_len, vocab_size = logits.size()
计算每个标签序列的实际长度(去掉 pad):
label_lengths = (labels != tokenizer.pad_token_id).sum(dim=1)
初始化权重张量并为每个标签序列计算权重:
weights = torch.zeros_like(labels, dtype=torch.float) for i, length in enumerate(label_lengths): length = length.item() weights[i, :length] = torch.arange(length + 1, 1, -1, dtype=torch.float)
将 logits 和 labels 转换为合适的形状并计算交叉熵损失:
loss = self.loss_fn(logits.view(-1, vocab_size), labels.view(-1)) loss = loss.view(batch_size, seq_len)
应用权重并计算加权平均损失:
weighted_loss = loss * weights weighted_loss = weighted_loss.sum() / weights.sum()
通过这种方式,我们可以为每个标签位置应用不同的权重,从而计算加权的交叉熵损失。