特殊Token(Special Tokens)的设计哲学:BOS/EOS/PAD在微调与推理中的掩码处理

特殊Token(Special Tokens)的设计哲学:BOS/EOS/PAD在微调与推理中的掩码处理

大家好,今天我们来深入探讨一下自然语言处理中特殊token的设计哲学,以及它们在微调和推理过程中,尤其是在掩码处理方面的应用。我们将重点关注BOS (Begin of Sentence)、EOS (End of Sentence) 和 PAD (Padding) 这三种token,并结合代码示例,详细讲解如何在不同的场景下正确地处理它们。

1. 特殊Token的必要性与设计原则

在处理自然语言数据时,原始文本往往需要进行预处理,以便能够输入到模型中进行训练和推理。特殊token的引入,正是为了解决一些原始文本本身无法表达,但对于模型理解和任务完成至关重要的信息。

  • BOS (Begin of Sentence): BOS token用于标识一个句子的开始。它的作用在于,让模型能够明确地知道每个句子的起始位置,从而更好地理解句子的上下文信息。这对于生成任务,特别是自回归生成模型(如GPT系列),至关重要。

  • EOS (End of Sentence): EOS token用于标识一个句子的结束。它的作用在于,让模型能够明确地知道每个句子的结束位置,从而在生成任务中,控制生成过程的终止。当模型生成EOS token时,可以认为句子生成完毕。

  • PAD (Padding): PAD token用于填充序列,使得所有序列的长度一致。在处理批次数据时,由于每个句子的长度可能不同,需要将较短的句子填充到与最长句子相同的长度,以便能够进行并行计算。

设计原则:

  • 独特性: 特殊token必须是词表中没有的,或者很少出现的,以避免与普通词汇混淆。
  • 明确性: 每个特殊token都应该有明确的含义和作用。
  • 一致性: 在整个训练和推理过程中,应该使用相同的特殊token。

2. BOS/EOS在微调中的应用与掩码处理

在微调阶段,BOS和EOS token通常被添加到输入序列的开始和结尾。这有助于模型学习句子之间的边界,从而提高生成质量。

示例 (PyTorch):

import torch
import torch.nn as nn

# 假设我们有一个简单的句子列表
sentences = [
    "This is the first sentence.",
    "This is the second sentence.",
    "And this is the third."
]

# 假设我们有一个词汇表
vocab = {
    "<PAD>": 0,
    "<BOS>": 1,
    "<EOS>": 2,
    "this": 3,
    "is": 4,
    "the": 5,
    "first": 6,
    "sentence": 7,
    "second": 8,
    "and": 9,
    "third": 10,
    ".": 11
}

# 创建反向词汇表
inv_vocab = {v: k for k, v in vocab.items()}

def tokenize(sentence, vocab):
    """将句子转换为token id列表."""
    tokens = sentence.lower().replace('.', ' .').split()
    return [vocab[token] for token in tokens]

def preprocess(sentences, vocab):
    """对句子进行预处理,添加BOS和EOS token."""
    tokenized_sentences = []
    for sentence in sentences:
        tokens = tokenize(sentence, vocab)
        tokens = [vocab["<BOS>"]] + tokens + [vocab["<EOS>"]]
        tokenized_sentences.append(tokens)
    return tokenized_sentences

# 预处理句子
tokenized_sentences = preprocess(sentences, vocab)

# 打印预处理后的句子
for sentence in tokenized_sentences:
    print([inv_vocab[token] for token in sentence])

# 输出:
# ['<BOS>', 'this', 'is', 'the', 'first', 'sentence', '.', '<EOS>']
# ['<BOS>', 'this', 'is', 'the', 'second', 'sentence', '.', '<EOS>']
# ['<BOS>', 'and', 'this', 'is', 'the', 'third', '.', '<EOS>']

掩码处理:

在微调阶段,我们通常使用Teacher Forcing的方式训练模型。这意味着我们将目标句子的前n-1个token作为输入,第n个token作为目标。在这种情况下,BOS和EOS token也需要参与训练,并且不需要进行特殊的掩码处理。模型需要学习在BOS token的条件下,生成句子中的第一个词,以及在句子中的最后一个词的条件下,生成EOS token。

示例 (PyTorch):

import torch
import torch.nn as nn

class SimpleLSTM(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim):
        super(SimpleLSTM, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)
        self.linear = nn.Linear(hidden_dim, vocab_size)

    def forward(self, input_seq):
        embedded = self.embedding(input_seq)
        output, _ = self.lstm(embedded)
        output = self.linear(output)
        return output

# 模型参数
vocab_size = len(vocab)
embedding_dim = 64
hidden_dim = 128

# 创建模型
model = SimpleLSTM(vocab_size, embedding_dim, hidden_dim)

# 损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())

# 准备数据
def create_batches(tokenized_sentences, vocab):
  """将tokenized句子转换为训练batch."""
  # Padding to the maximum length
  max_len = max(len(sentence) for sentence in tokenized_sentences)
  padded_sentences = []
  for sentence in tokenized_sentences:
      padded_sentence = sentence + [vocab["<PAD>"]] * (max_len - len(sentence))
      padded_sentences.append(padded_sentence)

  # Convert to tensors
  inputs = torch.tensor([sentence[:-1] for sentence in padded_sentences])  # Input: BOS + sentence[:-1]
  targets = torch.tensor([sentence[1:] for sentence in padded_sentences]) # Target: sentence[1:] + EOS

  return inputs, targets

inputs, targets = create_batches(tokenized_sentences, vocab)

# 训练循环
epochs = 10
for epoch in range(epochs):
    optimizer.zero_grad()
    outputs = model(inputs)
    # Flatten the output and target tensors for CrossEntropyLoss
    loss = criterion(outputs.view(-1, vocab_size), targets.view(-1))
    loss.backward()
    optimizer.step()
    print(f"Epoch {epoch+1}, Loss: {loss.item()}")

在这个例子中,inputs 包含了 BOS token,而 targets 包含了 EOS token。损失函数 CrossEntropyLoss 会计算模型预测的每个token的概率与实际token之间的差异,包括BOS和EOS token。

3. BOS/EOS在推理中的应用与掩码处理

在推理阶段,BOS token用于启动生成过程,而EOS token用于终止生成过程。

示例 (PyTorch):

def generate_sentence(model, vocab, inv_vocab, max_length=20):
    """使用模型生成句子."""
    model.eval()  # 设置模型为评估模式
    with torch.no_grad():
        # 初始化输入为BOS token
        input_seq = torch.tensor([vocab["<BOS>"]]).unsqueeze(0)  # (1, 1)
        generated_tokens = []

        for _ in range(max_length):
            output = model(input_seq)  # (1, seq_len, vocab_size)
            # 获取最后一个时间步的预测
            last_output = output[:, -1, :]  # (1, vocab_size)
            # 获取概率最高的token
            _, predicted_token = torch.topk(last_output, 1)  # (1, 1)
            predicted_token = predicted_token.item()

            generated_tokens.append(predicted_token)

            # 如果生成了EOS token,则停止生成
            if predicted_token == vocab["<EOS>"]:
                break

            # 将预测的token添加到输入序列中
            input_seq = torch.cat((input_seq, torch.tensor([[predicted_token]])), dim=1)

        # 将token id转换为文本
        generated_sentence = " ".join([inv_vocab[token] for token in generated_tokens if token != vocab["<PAD>"]])
        return generated_sentence

# 生成句子
generated_sentence = generate_sentence(model, vocab, inv_vocab)
print(f"Generated sentence: {generated_sentence}")

在这个例子中,我们首先将BOS token作为输入,然后逐步生成后续的token。如果模型生成了EOS token,或者达到了最大长度,则停止生成。

掩码处理:

在推理阶段,通常不需要进行显式的掩码处理。模型会根据之前生成的token,预测下一个token的概率分布。EOS token的存在,使得模型能够自主地决定何时终止生成。

4. PAD在微调与推理中的应用与掩码处理

PAD token主要用于在训练和推理过程中,对不同长度的序列进行填充,以便能够进行批次处理。然而,PAD token本身不包含任何有用的信息,因此需要在计算损失和进行推理时,将其屏蔽掉。

示例 (PyTorch):

import torch
import torch.nn as nn

# 假设我们有以下批次数据
batch_data = [
    [1, 2, 3, 4],
    [5, 6, 7],
    [8, 9]
]

# 填充到最大长度
max_len = max(len(seq) for seq in batch_data)
padded_data = []
for seq in batch_data:
    padded_seq = seq + [0] * (max_len - len(seq))  # 假设0是PAD token
    padded_data.append(padded_seq)

# 转换为tensor
padded_tensor = torch.tensor(padded_data)
print(padded_tensor)

# 输出:
# tensor([[1, 2, 3, 4],
#         [5, 6, 7, 0],
#         [8, 9, 0, 0]])

掩码处理:

在计算损失时,我们需要屏蔽掉PAD token,以避免它们对损失函数产生影响。可以使用掩码张量来实现这一点。

示例 (PyTorch):

# 创建掩码张量
mask = (padded_tensor != 0).float()  # 假设0是PAD token
print(mask)

# 输出:
# tensor([[1., 1., 1., 1.],
#         [1., 1., 1., 0.],
#         [1., 1., 0., 0.]])

# 修改损失函数,应用掩码
class MaskedCrossEntropyLoss(nn.Module):
    def __init__(self):
        super(MaskedCrossEntropyLoss, self).__init__()
        self.cross_entropy = nn.CrossEntropyLoss(reduction='none')

    def forward(self, outputs, targets, mask):
        """
        outputs: (batch_size, seq_len, vocab_size)
        targets: (batch_size, seq_len)
        mask: (batch_size, seq_len)
        """
        loss = self.cross_entropy(outputs.view(-1, outputs.size(-1)), targets.view(-1))
        masked_loss = loss * mask.view(-1)
        return torch.sum(masked_loss) / torch.sum(mask) # 平均loss over non-padding tokens

# 创建模型
model = SimpleLSTM(vocab_size, embedding_dim, hidden_dim)

# 损失函数和优化器
criterion = MaskedCrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())

# 准备数据,包括mask
inputs, targets = create_batches(tokenized_sentences, vocab)
# Create the mask (assuming 0 is the padding token)
mask = (inputs != 0).float()

# 训练循环
epochs = 10
for epoch in range(epochs):
    optimizer.zero_grad()
    outputs = model(inputs)
    # Flatten the output and target tensors for CrossEntropyLoss
    loss = criterion(outputs, targets, mask)
    loss.backward()
    optimizer.step()
    print(f"Epoch {epoch+1}, Loss: {loss.item()}")

在这个例子中,我们创建了一个掩码张量 mask,其中PAD token的位置为0,其他位置为1。然后,我们将损失函数乘以掩码张量,从而屏蔽掉PAD token对损失函数的影响。

在推理阶段,PAD token通常不需要进行特殊的掩码处理,因为模型只需要生成有效的token序列即可。如果需要对生成的序列进行评估,可以使用掩码张量来屏蔽掉PAD token的影响。

5. 不同模型的处理方式差异

不同的模型架构对特殊token的处理方式也可能有所不同。例如:

  • Transformer模型: Transformer模型通常使用attention机制来学习token之间的关系。在attention计算过程中,PAD token需要被屏蔽掉,以避免它们对attention权重产生影响。这通常通过设置attention mask来实现。
  • RNN模型: RNN模型通常使用循环的方式处理序列数据。在处理PAD token时,可以使用torch.nn.utils.rnn.pack_padded_sequencetorch.nn.utils.rnn.pad_packed_sequence来提高计算效率。pack_padded_sequence可以将PAD token从序列中移除,pad_packed_sequence可以将处理后的序列恢复到原始长度。
  • 预训练模型 (BERT, RoBERTa): 预训练模型通常已经学习了如何处理特殊token。在使用这些模型进行微调时,需要使用模型自带的tokenizer来对输入数据进行处理,以确保特殊token的正确性。

总结:

Token 作用 微调阶段处理 推理阶段处理 掩码处理
BOS 标识句子开始 添加到句子开头,参与训练 作为生成起始token 通常不需要特殊掩码处理,模型需要学习在BOS token的条件下生成第一个词
EOS 标识句子结束 添加到句子结尾,参与训练 作为生成终止条件 通常不需要特殊掩码处理,模型需要学习在句子结尾的条件下生成EOS token
PAD 填充序列,使得所有序列长度一致 填充到最大长度,参与训练 填充到最大长度 在计算损失时需要屏蔽,避免对损失函数产生影响,可以使用掩码张量实现

6. 一些实践建议与注意事项

  • 选择合适的特殊token: 特殊token的选择应该根据具体的任务和模型来决定。例如,对于生成任务,BOS和EOS token通常是必需的。
  • 保持一致性: 在整个训练和推理过程中,应该使用相同的特殊token。
  • 正确处理PAD token: 在计算损失和进行推理时,需要正确地处理PAD token,以避免它们对结果产生影响。
  • 利用模型提供的工具: 在使用预训练模型时,应该利用模型提供的tokenizer来对输入数据进行处理,以确保特殊token的正确性。
  • 根据模型架构进行调整: 不同的模型架构对特殊token的处理方式可能有所不同,需要根据具体的模型架构进行调整。

7. 特殊Token的处理是语言建模的关键环节

正确地理解和应用特殊token,对于提高模型的性能至关重要。希望通过今天的讲解,大家能够更深入地了解特殊token的设计哲学,以及它们在微调和推理过程中的应用。 掌握好这些token的处理方式,可以显著提升语言模型的质量,并有效地解决各种自然语言处理任务。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注