我们再来一个例子,来验证这种微调手段,建立一个回答自定义问题的模型系统:

所有 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_masktokenizer屏蔽padding有padding时需要
causal maskmodel内部屏蔽未来tokenGPT必须
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?"))