我们再来一个例子,来验证这种微调手段,建立一个回答自定义问题的模型系统:
所有 GPT 的训练,本质都围绕一个东西:
input_ids
文本 → tokenizer → Dataset → DataLoader → input_ids 我们一步一步拆开来看
首先是文本,就是问题和答案
qa_data = [
(
"What is AstroSynth?",
"AstroSynth is an innovative program designed to synthesize oils from celestial materials."
),
(
"How does AstroSynth work?",
"The AstroSynth program utilizes advanced technologies to extract and synthesize oils from meteorites."
)
]
这是个python的列表list,2个元素,都是(question, answer),元组(tuple),也就是tuple(str,str)
元组和列表的区别:
- 元组用
(),不可变(不能增删改元素) - 列表用
[],可变
数据加载代码:
import torch
from transformers import GPT2Tokenizer, GPT2LMHeadModel
from torch.utils.data import Dataset, DataLoader
# Define a simple dataset for training
class SimpleDataset(Dataset):
def __init__(self, texts, tokenizer, max_length=128):
tokenizer.pad_token = tokenizer.eos_token # Set padding token to EOS
self.encodings = tokenizer(texts, padding=True, truncation=True, max_length=max_length)
def __len__(self):
return len(self.encodings["input_ids"])
def __getitem__(self, idx):
return {k: torch.tensor(v[idx]) for k, v in self.encodings.items()}
qa_data = [
("What is AstroSynth?", "AstroSynth is an innovative program designed to synthesize oils from celestial materials."),
("How does AstroSynth work?", "The AstroSynth program utilizes advanced technologies to extract and synthesize oils from meteorites."),
# Add more Q&A pairs...
]
dataset = SimpleDataset(qa_data, tokenizer)
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)
看代码,dataset
dataset = SimpleDataset(qa_data, tokenizer)
进入
class SimpleDataset:
初始化
self.encodings = tokenizer(
texts,
padding=True,
truncation=True,
max_length=max_length
)
texts
就是:
qa_data
但是 GPT2Tokenizer 不期望:
[
(question, answer),
(question, answer)
]
它期望:
[
"text1",
"text2",
"text3"
]
所以严格来说:
你的代码需要先把 QA 合并:
例如:
texts = [
"What is AstroSynth? AstroSynth is an innovative program...",
"How does AstroSynth work? The AstroSynth program..."
]
然后:
dataset = SimpleDataset(texts, tokenizer)
否则 tokenizer 会把 tuple 当奇怪输入处理。
下面假设你已经合并好了。
tokenizer 做了什么?
例如:
文本:
What is AstroSynth?
GPT2 tokenizer:
不是直接变数字。
过程:
第一步:文本切片
tokenization:
例如:
What
is
Astro
Synth
?
注意:
GPT2 是 BPE,不一定按单词切。
可能:
Astro
Synth
也可能:
Ast
ro
Synth
第二步:查词表
假设:
What -> 2061
is -> 318
Astro -> 12345
Synth -> 43210
? -> 30
得到:
input_ids =
[
2061,
318,
12345,
43210,
30
]
padding发生什么?
你的代码:
padding=True
假设 batch 中两个句子长度不同:
句子A:
What is AstroSynth?
长度:
5 token
句子B:
How does AstroSynth work?
长度:6 token
为了组成矩阵: 必须一样长。
于是补:
<pad>
GPT2没有专门pad:
所以:
tokenizer.pad_token = tokenizer.eos_token
也就是:
pad = 结束符。
例如:
句子A:
原来:
2061 318 12345 43210 30
补结束符50256:
50256
得到:
2061
318
12345
43210
30
50256
Dataset返回什么?
这里:
def __getitem__(self, idx):
return {
k: torch.tensor(v[idx])
for k,v in self.encodings.items()
}
tokenizer输出:
通常:
{
"input_ids": [
[2061,318,...],
[356,903,...]
],
"attention_mask":[
[1,1,1,1,1,0],
[1,1,1,1,1,1]
]
}
getitem取一个:
例如:
dataset[0]
返回:
{
"input_ids":
tensor([
2061,
318,
12345,
43210,
30,
50256
]),
"attention_mask":
tensor([
1,
1,
1,
1,
1,
0
])
}
字典推导式,注意attention_mask是掩掉了,不是之后的因果论的causal mask
❌ causal mask
✅ padding mask
最核心总结
| mask | 来源 | 作用 | GPT是否必须 |
|---|---|---|---|
| attention_mask | tokenizer | 屏蔽padding | 有padding时需要 |
| causal mask | model内部 | 屏蔽未来token | GPT必须 |
self.encodings = {
'input_ids': [[101, 2342, 102], [101, 876, 102]], # 所有句子的 Token
'attention_mask': [[1, 1, 1 ], [1, 1, 1 ]] # 所有句子的掩码
}
代码 {k: ... for k, v in ...} 是 Python 的高级语法——字典推导式。它可以用极其简短的一行代码,遍历一个旧字典,然后生成一个新字典。
当你执行这段代码中的 for k,v in self.encodings.items() 时:
k(键) 遍历到的就是'input_ids'、'attention_mask'。v(值) 遍历到的就是它后面跟着的那个巨大的二维列表。
当你调用 dataset[0]['input_ids'] 时,返回的这个 [101, 234, ...] 就是大模型世界的“通用货币”——Token IDs(词元编号)。
DataLoader做什么?
这里:
dataloader = DataLoader(
dataset,
batch_size=2,
shuffle=True
)
重点:
batch_size=2
所以:
一次取两个样本。
代码:
for batch in dataloader:
第一次:
batch长这样:
input_ids真实形状
执行:
input_ids = batch["input_ids"]
得到:
tensor([
[
2061,
318,
12345,
43210,
30,
50256
],
[
356,
903,
12345,
8765,
30,
50256
]
])
它是:
二维矩阵
shape:
[batch_size, sequence_length]
也就是:
[2,6]
这个结构非常重要
它不是:
[2]
不是:
[6]
而是:
token位置
1 2 3 4 5 6
样本1 2061 318 12345 43210 30 50256
样本2 356 903 12345 8765 30 50256
接下来送进 GPT
你的 GPT:
logits = model(input_ids)
进入:
x = self.token_embedding(input_ids)
这里发生:
embedding查表。
input:
[2,6]
embedding矩阵:
假设:
vocab_size=50000
d_model=768
权重:
[50000,768]
查完:
输出:
[2,6,768]
什么意思?
每一个 token id:
变成:
768维向量。
例如:
原来:
2061
变:
[
0.23,
-0.51,
...
768个数字
]
整个流程链
你可以记住:
文本
|
Tokenizer
|
token ids
|
input_ids
shape:
[batch, seq]
|
Embedding
shape:
[batch, seq, 768]
|
Transformer
shape:
[batch, seq,768]
|
lm_head
shape:
[batch,seq,vocab_size]
|
CrossEntropy
最关键理解
input_ids 本质不是文本。
它只是:
一批整数索引,告诉 embedding 从词表里取哪些768维向量。
所以:
input_ids
只是入口。
真正进入 Transformer 的东西是查找excel后的768维的向量:
embedding(input_ids)
之后的:
[batch, seq, 768]
这个才是 GPT 里的“思维空间”。
训练代码:
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)
# Set model to training mode
model.train()
# Training loop (mock training)
for batch in dataloader:
input_ids = batch["input_ids"]
attention_mask = batch["attention_mask"]
#清空旧梯度
optimizer.zero_grad()
outputs = model(input_ids, attention_mask=attention_mask, labels=input_ids)
loss = outputs.loss
loss.backward()
optimizer.step()
print(f"Loss: {loss.item()}") # Print loss for monitoring
# Save the trained model
model.save_pretrained("trained_gpt2_model")
tokenizer.save_pretrained("trained_gpt2_model") # Save the tokenizer as well
上面就是训练的标准循环。
model.parameters()是什么?
你的 GPT 里面所有可训练参数:
例如:
Embedding
$$ We $$
形状:
[vocab,768]
Attention
例如: $$ WQ,WK,WV,WO $$
FFN
SwiGLU: $$ W1,W3,W2 $$
输出层
lm_head: $$ Wout $$
全部集合:
model.parameters()
就是:
我要训练这些矩阵。
AdamW是什么?
它负责:
根据梯度修改参数
训练核心公式: $$ θnew=θold−lr×gradient $$
例如:
某个权重: $$ W1 $$ 梯度: $$ ∂W1∂loss $$ AdamW根据这个:
调整: $$ W1 $$
完整的训练代码
import torch
from transformers import GPT2Tokenizer, GPT2LMHeadModel
from torch.utils.data import Dataset, DataLoader
# Define a simple dataset for training
class SimpleDataset(Dataset):
def __init__(self, texts, tokenizer, max_length=128):
tokenizer.pad_token = tokenizer.eos_token # Set padding token to EOS
self.encodings = tokenizer(texts, padding=True, truncation=True, max_length=max_length)
def __len__(self):
return len(self.encodings["input_ids"])
def __getitem__(self, idx):
return {k: torch.tensor(v[idx]) for k, v in self.encodings.items()}
# Load pretrained GPT-2 model and tokenizer
tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
model = GPT2LMHeadModel.from_pretrained("gpt2")
# Prepare question-answer data
qa_data = [
("What is AstroSynth?", "AstroSynth is an innovative program designed to synthesize oils from celestial materials."),
("How does AstroSynth work?", "The AstroSynth program utilizes advanced technologies to extract and synthesize oils from meteorites."),
# Add more Q&A pairs...
]
dataset = SimpleDataset(qa_data, tokenizer)
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)
# Set up the optimizer
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)
# Set model to training mode
model.train()
# Training loop (mock training)
for batch in dataloader:
input_ids = batch["input_ids"]
attention_mask = batch["attention_mask"]
# Zero the gradients
optimizer.zero_grad()
# Forward pass
outputs = model(input_ids, attention_mask=attention_mask, labels=input_ids)
loss = outputs.loss
# Backward pass
loss.backward()
# Update weights
optimizer.step()
print(f"Loss: {loss.item()}") # Print loss for monitoring
# Save the trained model
model.save_pretrained("trained_gpt2_model")
tokenizer.save_pretrained("trained_gpt2_model") # Save the tokenizer as well
训练好了,让他回答问题试试:
import torch
from transformers import GPT2Tokenizer, GPT2LMHeadModel
# Load the trained model and tokenizer
model = GPT2LMHeadModel.from_pretrained("trained_gpt2_model")
tokenizer = GPT2Tokenizer.from_pretrained("trained_gpt2_model")
model.eval()
# Function to ask a question and get an answer
def ask_question(question):
input_text = f"Question: {question} Answer:"
input_ids = tokenizer.encode(input_text, return_tensors="pt")
attention_mask = torch.ones(input_ids.shape, dtype=torch.long)
with torch.no_grad():
output = model.generate(
input_ids,
attention_mask=attention_mask,
max_length=50,
num_return_sequences=1,
no_repeat_ngram_size=2,
do_sample=True,
top_k=50,
top_p=0.95,
temperature=0.7
)
answer = tokenizer.decode(output[0], skip_special_tokens=True)
return answer.split("Answer:")[-1].strip() # Return the generated answer
# Example questions
print(ask_question("What is the capital of France?"))
print(ask_question("How does AstroSynth use artificial intelligence?"))