从零实现大模型-GPT2任务微调 原创

发布于 2024-7-2 11:26
浏览
0收藏

​从零实现大模型-BERT预训练​

​从零实现大模型-BERT微调​

我们在BERT微调那篇文章中提到,许多NLP任务(如情感分析和问答)都依赖于上下文理解能力。而像BERT这种双向模型具有较强的上下文理解能力,因此非常适合用于任务微调,即针对某个具体任务进行微调。

​从零实现大模型-GPT2预训练​

​从零实现大模型-GPT2指令微调​

​从零实现大模型-GPT2 RLHF​

而像GPT这种自回归模型,在预训练完成后会进行一个指令微调过程,用于跟随人类指令,然后通过指令去完成不同的任务(翻译,总结)。

那GPT能否像BERT那样,直接微调用于完成某个具体任务呢?虽然BERT更适合,但GPT确实也可以。

今天我们就基于之前的GPT2预训练模型,使用一个垃圾邮件数据集,来微调一个邮件分类模型。

1.垃圾邮件识别

传统ML方法

当然,也可以通过传统的机器学习方法实现垃圾邮件分类,例如,贝叶斯分类,或者基于统计学,事先设定一些敏感词汇,如果邮件中出现了这些敏感词汇,就认为是垃圾邮件。

Embedding

但传统机器学习方法依赖人类经验,既然我们正在写大模型的文章,就得入乡随俗。

我们通过Embedding实现文本分类,具体来说就是计算邮件内容的Embedding,然后计算“spam”和“none spam”的Embedding,最后通过计算余弦相似度来判断邮件类型。

从零实现大模型-GPT2任务微调-AI.x社区

指令微调模型

其实,还有比计算Embedding更简单的方法,如果大模型已经经过预训练、指令微调以及RLHF过程,那么就可以直接利用这种指令跟随能力来实现垃圾邮件分类。

例如,我们构造下面的prompt输入给chatGPT。

"Is the following text 'spam'? Answer with 'yes' or 'no':"
    " 'You are a winner you have been specially"
    " selected to receive $1000 cash or a $2000 award.'"

以下是GPT4-o给出的答案,不仅准确识别出了垃圾邮件,还遵循了人类指令输出了“yes”。

从零实现大模型-GPT2任务微调-AI.x社区

如果只使用预训练模型,没有经过指令微调,前面我们也测试过,模型虽然有输出,但输出只是简单的拷贝输入。

输入:
Is the following text 'spam'? Answer with 'yes' or 'no': 'You are a winner you have been specially selected to receive $1000 cash or a $2000 award.'
输出:
The following text 'spam'? Answer with 'yes' or 'no': 'You are a winner

2.任务微调

本文完整代码如下,建议结合代码阅读文本。

https://github.com/AIDajiangtang/LLM-from-scratch/blob/main/GPT2_fine-tune_spam_classifier_from_scratch.ipynb

在通过Embedding实现文本分类时,Embedding是通过调用openAI的API得到的,其实,这个Embedding也可以看作是GPT模型输出隐状态的一部分。

除了用Embedding计算余弦相似度来实现文分类外,还可以基于隐状态实现一个分类模型,也就是在模型的输出端加一个分类头。

从零实现大模型-GPT2任务微调-AI.x社区

准备数据

训练数据来自公开的垃圾邮件数据集,包括文本和标签两列,标签列中spam代表是垃圾邮件。

从零实现大模型-GPT2任务微调-AI.x社区

因为数据集中垃圾邮件数量少于正常邮件,所以要平衡正负样本数量。

# Examine class distributions
print(df["Label"].value_counts())

Label
ham     4825
spam     747
Name: count, dtype: int64


def create_balanced_dataset(df):
    num_spam = df[df["Label"] == "spam"].shape[0]
    ham_subset = df[df["Label"] == "ham"].sample(num_spam, random_state=123)
    balanced_df = pd.concat([ham_subset, df[df["Label"] == "spam"]])
    return balanced_df


balanced_df = create_balanced_dataset(df)
print(balanced_df["Label"].value_counts())

Label
ham     747
spam    747
Name: count, dtype: int64


接下来划分训练集,验证机和测试集。

接下来是tokenization,padding或者截断到最大长度。

最后构造Dataloader。

加载预训练模型

加载gpt2预训练模型。

CHOOSE_MODEL = "gpt2-small (124M)"
INPUT_PROMPT = "Every effort moves"
BASE_CONFIG = {
    "vocab_size": 50257,     # Vocabulary size
    "context_length": 1024,  # Context length
    "drop_rate": 0.0,        # Dropout rate
    "qkv_bias": True         # Query-key-value bias
}
model_configs = {
    "gpt2-small (124M)": {"emb_dim": 768, "n_layers": 12, "n_heads": 12},
    "gpt2-medium (355M)": {"emb_dim": 1024, "n_layers": 24, "n_heads": 16},
    "gpt2-large (774M)": {"emb_dim": 1280, "n_layers": 36, "n_heads": 20},
    "gpt2-xl (1558M)": {"emb_dim": 1600, "n_layers": 48, "n_heads": 25},
}
BASE_CONFIG.update(model_configs[CHOOSE_MODEL])


assert train_dataset.max_length <= BASE_CONFIG["context_length"], (
    f"Dataset length {train_dataset.max_length} exceeds model's context "
    f"length {BASE_CONFIG['context_length']}. Reinitialize data sets with "
    f"`max_length={BASE_CONFIG['context_length']}`"
)


from gpt_download import download_and_load_gpt2


model_size = CHOOSE_MODEL.split(" ")[-1].lstrip("(").rstrip(")")
settings, params = download_and_load_gpt2(model_size=model_size, models_dir="gpt2")


model = GPTModel(BASE_CONFIG)
load_weights_into_gpt(model, params)
model.eval()

再微调过程中,可以冻结大部分预训练模型参数。

# Freeze all model layers first
for param in model.parameters():
  param.requires_grad = False

添加分类头

在预训练模型的输出端加一个二分类分类头。

# Add a classification head
torch.manual_seed(123)
num_classes = 2
model.out_head = torch.nn.Linear(
    in_features=BASE_CONFIG["emb_dim"], # 768
    out_features=num_classes,           # 2 (spam or not spam)
)


构造损失函数

因为是二分类,所以构造一个交叉熵损失函数。

def calculate_loss_batch(input_batch, target_batch, model, device):
  input_batch, target_batch = input_batch.to(device), target_batch.to(device)
  logits = model(input_batch)[:, -1, :] # Grab logits of last output token only!
  loss = torch.nn.functional.cross_entropy(logits, target_batch)
  return loss


然后开始训练,训练过程与指令微调过程基本一致。

import time


start_time = time.time()
torch.manual_seed(123)


# Create optimizer
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5, weight_decay=0.1)


# Set training epochs
num_epochs = 5


# Train the model
train_losses, val_losses, train_accs, val_accs, examples_seen = train_classifier(
    model=model,
    train_loader=train_loader,
    val_loader=val_loader,
    optimizer=optimizer,
    device=device,
    num_epochs=num_epochs,
    eval_freq=50,
    eval_iter=5,
    tokenizer=tokenizer,
)


end_time = time.time()
execution_time_minutes = (end_time - start_time) / 60
print(f"Training completed in {execution_time_minutes:.2f} minutes.")


本文转载自公众号人工智能大讲堂 

原文链接:​​https://mp.weixin.qq.com/s/n1h9JeCxV3Kq_yj0-_RS5A​



©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
标签
收藏
回复
举报
回复
相关推荐