前面十章,我们完成了 GPT 模型的预训练(Pre-training):模型学会了语言的统计规律,可以续写莎士比亚风格的文本。
但是预训练还有一个遗憾:它只教会了模型自言自语,却不太会与人交流。你问它:“您好!”,它可能顺着你的话续写:“今天天气不错!”,而不是回答:“您好!有什么我可以帮忙的吗?”。
实践中,研究者们发现只需要提供少量的对话模板让模型继续学习,它就可以非常轻松地从续写模式转换成对话模式。这个步骤被称为微调(Fine-tuning),也成了 GPT 模型训练的标准流程。
监督微调¶
监督微调(Supervised Fine-tuning,SFT)是最基本的微调手段,也是大型 GPT 模型微调的第一步:
预训练:自监督学习,从海量文本中学习语言规律,数据不需要人工标注标签;
监督微调:监督学习,从(提示词,回答)数据对中学习对话模式,数据需要人工构建。
数据格式¶
监督微调的数据是(提示词,回答)数据对(Prompt-Response):
提示词(Prompt):用户的提问或者命令;
回答(Response):模型回复的理想答案。
例如:
提示词:ROMEO:
Hello, Juliet!
回答:JULIET:
Hello, Romeo!数据来源¶
监督微调阶段的语料应该是全新的、特定于目标任务或领域的、在预训练阶段没有使用过的标注数据。
我们将从微型莎士比亚剧本(tinyshakespeare)数据集的测试集中提取对话样本,来保证模型没有在预训练阶段接触过这些数据。
💡 提示:为了方便教学,微调样本取自和预训练相同的莎士比亚剧本,只是选用了预训练时没有使用的测试集部分,这样就不用再准备新的数据集。实际项目中,微调数据应该是针对目标任务专门收集的,来源和风格通常都和预训练语料不同。
剧本的格式是:
SPEAKER A:
CONTENT...
SPEAKER B:
CONTENT...我们把相邻的两段台词配对,前一个作为提示词,后一个作为回答,就构建出了 SFT 样本。
import pickle
import re
from itertools import pairwise
import numpy as np
from src.gpt import *np.random.seed(42)数据提取¶
提取函数 extract_turns 从莎士比亚剧本中提取测试集(最后 10% 的台词段,位于预训练的测试集之内)的所有台词:
分块:用空行把剧本分成台词段;
识别角色:每段台词的第一行匹配
SPEAKER:格式,提取角色;提取台词:剩余内容整行合并成台词内容,总长度不超过
max_turn_chars(100 个字符)。
提取函数的返回值是一个列表,其中每个元素都是 (角色,台词) 对。
def extract_turns(filename, split=0.9, max_turn_chars=100):
with open(filename, encoding="utf-8") as f:
text = f.read()
# 按空行分块,每块是一个角色的一段台词
blocks = re.split(r"\n\s*\n", text.strip())
turns = []
# 截取测试集
split = int(len(blocks) * split)
for block in blocks[split:]:
lines = block.strip("\n").split("\n")
if not lines:
continue
# 格式验证:第一行必须是 “SPEAKER:”,否则不是台词段
m = re.compile(r"^([A-Z][A-Za-z' ]{0,30}):\s*$").match(lines[0].strip())
if not m:
continue
speaker = m.group(1).strip()
# 保持原文的分行格式,整行截取
content_lines = []
length = 0
for line in lines[1:]:
line = line.strip()
if not line:
continue
if length + len(line) + 1 > max_turn_chars:
break
content_lines.append(line)
length += len(line) + 1
if not content_lines:
continue
turns.append((speaker, "\n".join(content_lines)))
return turns超参数¶
我们只用到一个超参数:上下文尺寸,需要保持和预训练一致。
CONTEXT_SIZE = 256DATA_FILE = "../../../../data/tinyshakespeare.txt"
dataset = CharDataset(DATA_FILE, 1, CONTEXT_SIZE)提取样本¶
从数据集的测试集构建(提示词,回答)数据对:
配对:用
itertools.pairwise()把相邻的台词配对,前一个作为提示词,后一个作为回答;随机:随机打乱样本顺序;
截取:取前 512 个样本;
编码:把提示词和回答分别编码成词元序列。
# 相邻台词两两配对
pairs = list(pairwise(extract_turns(DATA_FILE)))
# 随机选取 512 对
np.random.shuffle(pairs)
pairs = pairs[:512]
samples = []
for (speaker_a, content_a), (speaker_b, content_b) in pairs:
# 提示
prompt = f"{speaker_a}:\n{content_a}\n\n"
# 回答
response = f"{speaker_b}:\n{content_b}\n\n"
samples.append((dataset.encode(prompt), dataset.encode(response)))保存样本¶
用 pickle.dump() 保存到文件。
SFT_SAMPLES = "../../../../data/sft-samples.pkl"
with open(SFT_SAMPLES, "wb") as f:
pickle.dump(samples, f)
print(f"saved {len(samples)} SFT samples to {SFT_SAMPLES}")saved 512 SFT samples to ../../../../data/sft-samples.pkl
验证样本¶
查看第一个样本,确认格式正确。
sample = samples[0]
print(f"{dataset.decode(sample[0])}\n{dataset.decode(sample[1])}")Pedant:
Away, away, mad ass! his name is Lucentio and he is
VINCENTIO:
Lucentio! O, he hath murdered his master! Lay hold
on him, I charge you, in the duke's name. O, my
本章,我们从微型莎士比亚剧本数据集的测试集中提取了 512 个对话样本,保存为 SFT 训练数据。每个样本均为(提示词,回答)数据对。
下一章,我们将利用这些样本对预训练后的模型进行监督微调,让模型学会按照对话的格式,针对提示词给出回答。
课后练习¶
对比预训练和监督微调阶段数据对(特征:标签)有什么不同?为什么?