导语
结合之前介绍的各个子模块,这是一篇面向新手的从0到1训练小型 GPT 风格模型的实践文章。目标是用最小可行的代码与完整训练流水线,帮助你在不依赖复杂框架的情况下,理解并跑通“数据准备 → 模型搭建 → 训练验证 → 保存与推理”的关键环节。你无需深厚的数学背景,只要具备基础的 Python 使用经验即可上手。
你将收获什么
- • 训练脚本:一个极简 Decoder-only Transformer(类似 GPT),包含嵌入、位置编码、自注意力、前馈网络、残差与层归一化。
- • 预测脚本:支持加载已训练权重,进行贪心或温度采样文本生成,含交互式与批量测试两种模式。
- • 数据流水线:从原始中文文本开始,经过清洗、分词、词表构建,转换为可训练的样本对(X→Y)。
- • 训练工具:训练/验证划分、进度条、早停、最优模型保存、损失曲线可视化。
一图看懂训练流程
- 原始文本 → 2) 文本清洗与分词 → 3) 构建词表与索引映射 → 4) 划分训练/验证集并喂给 DataLoader → 5) 模型前向与交叉熵损失 → 6) 反向传播与优化 → 7) 验证监控与早停 → 8) 持久化模型与词表 → 9) 加载权重进行文本生成。
环境与数据建议
- • 硬件:CPU 即可跑通;若有 GPU(如消费级显卡),训练会更快。
- • 依赖:PyTorch、jieba、numpy、matplotlib 等(示例代码已导入,按需安装)。
- • 数据:建议选择干净、风格一致的中文长文本(示例用《西游记》)。数据越一致,模型越容易学到稳定分布。
最小实操清单(Checklist)
-
- 准备
data/你的文本.txt,更新训练脚本中文件路径。
- 准备
-
- 运行训练脚本,确认样本预览与 batch 形状打印正常。
-
- 观察进度与损失,等待早停或手动终止。
-
- 查看
saved_models/下生成的权重与配置。
- 查看
-
- 运行预测脚本,测试交互式与批量生成,尝试不同温度与长度。
关键超参数怎么选
- • block_size(上下文长度):越大上下文越长,但显存/计算更贵。示例取 32 适合入门。
- • embed_size(嵌入维度):越大表达力越强,但更易过拟合且训练变慢。示例取 32。
- • n_layers / heads:适度增大通常提升上限,但需配合数据量与预算。
- • batch_size / lr:影响收敛速度与稳定性。若 loss 不稳,可调小 lr 或增大 batch。

如何评估与调试
- • 观察训练/验证损失是否同步下降;若训练降而验证不降,可能过拟合。
- • 用
plot_training_curves看损失曲线,关注是否震荡或发散。 - • 训练自动保存“最佳模型”(按验证集指标),即使早停也能得到最好权重。
- • 推理多试不同种子与温度(temperature)。温度 > 1 更发散,< 1 更保守。
核心代码-训练
# -*- coding: utf-8 -*-
"""
从零开始训练一个简单的Transformer模型来生成文本
这个脚本展示了如何用PyTorch构建和训练一个最小化的GPT风格的模型
适合初学者理解大语言模型的基本原理
"""
# 导入必要的库
import os # 文件系统操作
import re # 正则表达式,用于文本处理
import torch # PyTorch深度学习框架
import jieba # 中文分词工具
import random # 随机数生成
import math # 数学函数
import matplotlib.pyplot as plt # 绘图库,用于可视化
import numpy as np # 数值计算库
from datetime import datetime # 时间处理,用于生成文件名
import time # 计时,用于显示进度与ETA
import torch.nn as nn # 神经网络模块
import torch.nn.functional as F # 神经网络函数
from torch.utils.data import Dataset, DataLoader # 数据集和数据加载器
# ------------------------------
# 1. 数据准备相关函数
# ------------------------------
def load_raw_text(novel_path: str) -> str:
"""
读取原始文本文件(UTF-8编码)。
参数:
novel_path: 文本文件的完整路径。
返回:
原始字符串文本内容。
注意:
函数内包含存在性断言,路径错误会直接抛错,便于新手快速定位问题。
"""
assert os.path.exists(novel_path), "请先放置小说文本文件在 novel.txt"
with open(novel_path, 'r', encoding='utf-8') as f:
return f.read()
# ------------------------------
# 2. 文本预处理:清洗和标准化
# ------------------------------
def clean_text(text):
"""
清洗文本数据,去除不必要的字符和格式
这是训练前的必要步骤,因为原始文本可能包含各种格式问题
"""
# 使用正则表达式将多个连续的空格、换行符、制表符等替换为单个空格
# \s+ 表示一个或多个空白字符(空格、换行、制表符等)
text = re.sub(r'\s+', ' ', text)
# 只保留中文汉字、英文字母、数字和常用标点符号
# [^一-龥a-zA-Z0-9,。!?;,.!?;] 表示除了这些字符外的所有字符
# 一-龥 是Unicode中中文字符的范围
text = re.sub(r'[^一-龥a-zA-Z0-9,。!?;,.!?;]', '', text)
return text
def preprocess_text(raw_text: str) -> str:
"""
文本预处理(清洗与标准化)。
参数:
raw_text: 原始文本。
返回:
清洗后的文本,仅保留常用字符并合并多余空白。
"""
text = clean_text(raw_text) # 复用已实现的 clean_text()
print(f"清洗后文本长度: {len(text)} 字符")
return text
# ------------------------------
# 3. 分词处理:将文本切分成词语
# ------------------------------
def tokenize_text(cleaned_text: str) -> str:
"""
中文分词并拼接为连续字符串(字/词级均可简单兼容)。
参数:
cleaned_text: 已清洗的文本。
返回:
token_text: 分词后拼接的字符串(本示例默认不保留空格)。
说明:
初学者可以将此处改为保留空格,尝试词级建模的效果差异。
"""
words = jieba.lcut(cleaned_text) # 使用 jieba 进行中文分词
token_text = ''.join(words) # 这里选择不保留空格,做字符/字粒度建模
print(f"分词后文本长度: {len(token_text)} 字符")
return token_text
# ------------------------------
# 4. 构建词汇表:将字符转换为数字
# ------------------------------
def build_vocab(token_text: str):
"""
构建字符级词汇表与互逆映射。
参数:
token_text: 分词/拼接后的文本。
返回:
chars: 排序后的唯一字符列表
stoi: 字符到索引(dict)
itos: 索引到字符(dict)
vocab_size: 词表大小
"""
chars = sorted(list(set(token_text)))
stoi = {ch: i for i, ch in enumerate(chars)}
itos = {i: ch for i, ch in enumerate(chars)}
vocab_size = len(chars)
print(f"词汇表大小: {vocab_size} 个不同字符")
print(f"前10个字符: {chars[:10]}")
return chars, stoi, itos, vocab_size
# ------------------------------
# 5. 数据集构建:将文本转换为训练数据
# ------------------------------
class TextDataset(Dataset):
"""
自定义数据集类,用于将文本转换为模型可以训练的数据格式
继承自PyTorch的Dataset类,需要实现__len__和__getitem__方法
"""
def __init__(self, text, block_size=32, stoi=None):
"""
初始化数据集
text: 输入文本
block_size: 每个训练样本的长度(上下文窗口大小)
"""
self.text = text
self.block_size = block_size # 每个样本包含32个字符
self.stoi = stoi # 字符到索引映射,避免依赖全局变量
def __len__(self):
"""
返回数据集的大小
总样本数 = 文本长度 - 块大小
因为我们需要为每个位置预测下一个字符
"""
return len(self.text) - self.block_size
def __getitem__(self, idx):
"""
根据索引返回一个训练样本
idx: 样本索引
返回: (输入序列, 目标序列)
"""
# 输入序列:从位置idx开始的block_size个字符
x = torch.tensor([self.stoi[ch] for ch in self.text[idx:idx+self.block_size]], dtype=torch.long)
# 目标序列:从位置idx+1开始的block_size个字符(比输入序列向右偏移1位)
# 这样模型学习的是:给定前32个字符,预测第33个字符
y = torch.tensor([self.stoi[ch] for ch in self.text[idx+1:idx+self.block_size+1]], dtype=torch.long)
return x, y
def create_dataloaders(token_text: str, block_size=32, batch_size=16, val_ratio=0.1, stoi=None):
"""
构建数据集与训练/验证 DataLoader。
参数:
token_text: 训练语料字符串。
block_size: 上下文长度(窗口大小)。
batch_size: 批大小。
val_ratio: 验证集比例。
返回:
dataset, train_loader, val_loader, train_dataset, val_dataset
说明:
使用固定随机种子划分,保证可复现性。
"""
dataset = TextDataset(token_text, block_size=block_size, stoi=stoi)
total_size = len(dataset)
val_size = max(1, int(total_size * val_ratio))
train_size = total_size - val_size
train_dataset, val_dataset = torch.utils.data.random_split(
dataset, [train_size, val_size], generator=torch.Generator().manual_seed(42)
)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
print(f"样本总数: {total_size}")
print(f"训练集: {train_size},验证集: {val_size}")
print(f"每批样本数: {batch_size}")
return dataset, train_loader, val_loader, train_dataset, val_dataset
# ------------------------------
# 5.1. 数据样例展示:让初学者理解训练数据格式
# ------------------------------
def show_samples(dataset: Dataset, itos: dict):
"""
打印展示若干条样例(输入/目标),帮助新手理解训练对齐关系。
参数:
dataset: 文本数据集。
itos: 索引到字符的映射,用于可读化。
"""
print("\n" + "="*60)
print("训练数据样例展示")
print("="*60)
for i in range(2):
sample_x, sample_y = dataset[i]
input_text = ''.join([itos[int(idx)] for idx in sample_x])
target_text = ''.join([itos[int(idx)] for idx in sample_y])
print(f"\n样例 {i+1}:")
print(f"输入序列 (X): {input_text}")
print(f"目标序列 (Y): {target_text}")
print(f"输入长度: {len(input_text)} 字符")
print(f"目标长度: {len(target_text)} 字符")
print("字符到数字映射示例:")
for j in range(min(5, len(input_text))):
char = input_text[j]
char_id = sample_x[j].item()
print(f" '{char}' -> {char_id}")
def describe_format_and_batch(train_loader: DataLoader, itos: dict):
"""
输出数据格式说明,并展示一个批次的形状及首条样本。
参数:
train_loader: 训练数据加载器。
itos: 索引到字符的映射。
"""
print("\n" + "="*60)
print("数据格式说明")
print("="*60)
print("• 输入序列(X): 模型看到的字符序列")
print("• 目标序列(Y): 模型要预测的下一个字符序列")
print("• 目标序列比输入序列向右偏移1位")
print("• 模型学习: 给定前32个字符,预测第33个字符")
print("• 每个字符都有唯一的数字ID")
print("="*60)
print("\n批次数据样例:")
print("-" * 40)
batch_x, batch_y = next(iter(train_loader))
print(f"批次形状: X={batch_x.shape}, Y={batch_y.shape}")
print(f"批次大小: {batch_x.shape[0]} 个样本")
print(f"序列长度: {batch_x.shape[1]} 个字符")
first_sample_x = batch_x[0]
first_sample_y = batch_y[0]
first_input = ''.join([itos[int(idx)] for idx in first_sample_x])
first_target = ''.join([itos[int(idx)] for idx in first_sample_y])
print(f"\n批次中第一个样本:")
print(f"输入: {first_input}")
print(f"目标: {first_target}")
print("\n" + "="*60)
# ------------------------------
# 6. 模型架构:构建Transformer模型
# ------------------------------
class SelfAttention(nn.Module):
"""
自注意力机制:Transformer的核心组件
让模型能够关注输入序列中的不同位置,学习字符之间的关系
"""
def __init__(self, embed_size, heads):
"""
初始化自注意力层
embed_size: 嵌入维度(每个字符用多少维向量表示)
heads: 注意力头的数量(多头注意力)
"""
super().__init__()
self.heads = heads # 注意力头数
self.head_dim = embed_size // heads # 每个头的维度
# 线性层:将输入映射为Query、Key、Value三个矩阵
# 输出维度是embed_size*3,然后分割成Q、K、V
self.qkv = nn.Linear(embed_size, embed_size*3)
# 输出投影层:将多头注意力的结果合并
self.fc_out = nn.Linear(embed_size, embed_size)
def forward(self, x):
"""
前向传播
x: 输入张量,形状为 (batch_size, sequence_length, embed_size)
"""
N, T, C = x.shape # N=批次大小, T=序列长度, C=嵌入维度
# 计算Q、K、V矩阵
qkv = self.qkv(x).reshape(N, T, 3, self.heads, self.head_dim)
q, k, v = qkv[:, :, 0], qkv[:, :, 1], qkv[:, :, 2] # 分割成Q、K、V
# 计算注意力分数:Q和K的点积,然后除以sqrt(head_dim)进行缩放
scores = torch.einsum("nthe,nshe->nths", q, k) / math.sqrt(self.head_dim)
# 创建因果掩码:防止模型看到未来的字符(GPT是自回归模型)
# 下三角矩阵,上三角部分为0,下三角部分为1
mask = torch.tril(torch.ones(T, T, device=x.device)).unsqueeze(0).unsqueeze(2)
scores = scores.masked_fill(mask == 0, float("-inf")) # 将上三角部分设为负无穷
# 应用softmax得到注意力权重
attn = F.softmax(scores, dim=-1)
# 用注意力权重对V进行加权求和
out = torch.einsum("nths,nshe->nthe", attn, v)
# 将多头维度合并回嵌入维度 (heads * head_dim = embed_size)
out = out.reshape(N, T, C)
# 通过输出投影层
return self.fc_out(out)
class Block(nn.Module):
"""
Transformer块:包含自注意力层和前馈网络层
这是Transformer的基本构建单元,可以堆叠多个这样的块
"""
def __init__(self, embed_size, heads):
"""
初始化Transformer块
embed_size: 嵌入维度
heads: 注意力头数
"""
super().__init__()
# 自注意力层
self.attn = SelfAttention(embed_size, heads)
# 层归一化:稳定训练过程,加速收敛
self.norm1 = nn.LayerNorm(embed_size)
# 前馈网络:两层全连接层,中间有ReLU激活函数
# 第一层将维度扩大2倍,第二层恢复到原始维度
self.ff = nn.Sequential(
nn.Linear(embed_size, embed_size*2), # 扩展维度
nn.ReLU(), # 激活函数
nn.Linear(embed_size*2, embed_size) # 恢复维度
)
# 第二个层归一化
self.norm2 = nn.LayerNorm(embed_size)
def forward(self, x):
"""
前向传播:残差连接 + 层归一化
"""
# 第一个子层:自注意力 + 残差连接 + 层归一化
# 残差连接:x + self.attn(x),帮助梯度传播
x = self.norm1(x + self.attn(x))
# 第二个子层:前馈网络 + 残差连接 + 层归一化
x = self.norm2(x + self.ff(x))
return x
class TinyDecoderTransformer(nn.Module):
"""
完整的Transformer模型:一个简化版的GPT
包含词嵌入、位置嵌入、多个Transformer块和输出层
"""
def __init__(self, vocab_size, embed_size=32, n_layers=2, heads=2, block_size=32):
"""
初始化模型
vocab_size: 词汇表大小
embed_size: 嵌入维度(每个字符用32维向量表示)
n_layers: Transformer层数(2层)
heads: 注意力头数(2个头)
block_size: 最大序列长度(32个字符)
"""
super().__init__()
# 词嵌入层:将字符ID转换为向量表示
# 每个字符对应一个32维的向量
self.token_emb = nn.Embedding(vocab_size, embed_size)
# 位置嵌入层:为每个位置学习一个向量
# 让模型知道字符在序列中的位置
self.pos_emb = nn.Embedding(block_size, embed_size)
# 堆叠多个Transformer块
# 这里使用2层,每层都有自注意力和前馈网络
self.layers = nn.ModuleList([Block(embed_size, heads) for _ in range(n_layers)])
# 最终的层归一化
self.ln = nn.LayerNorm(embed_size)
# 输出头:将隐藏状态映射回词汇表
# 输出每个字符成为下一个字符的概率
self.head = nn.Linear(embed_size, vocab_size)
self.block_size = block_size
def forward(self, x):
"""
前向传播
x: 输入字符ID序列,形状为 (batch_size, sequence_length)
"""
N, T = x.shape # N=批次大小, T=序列长度
# 生成位置索引:0, 1, 2, ..., T-1
positions = torch.arange(T, device=x.device).unsqueeze(0)
# 词嵌入 + 位置嵌入
# 每个字符的最终表示 = 词嵌入 + 位置嵌入
x = self.token_emb(x) + self.pos_emb(positions)
# 通过所有Transformer层
for layer in self.layers:
x = layer(x)
# 最终层归一化
x = self.ln(x)
# 输出层:预测下一个字符的概率分布
return self.head(x)
def build_training_components(vocab_size: int, lr=0.001):
"""
构建训练所需的设备、模型、优化器、损失函数。
参数:
vocab_size: 词表大小。
lr: 学习率。
返回:
device, model, optimizer, criterion
"""
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"使用设备: {device}")
model = TinyDecoderTransformer(vocab_size).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
print(f"模型参数数量: {sum(p.numel() for p in model.parameters()):,}")
return device, model, optimizer, criterion
# ------------------------------
# 7.1. 模型保存函数:保存训练好的模型
# ------------------------------
def save_model(model, vocab_size, chars, stoi, itos, train_losses, model_name=None):
"""
保存训练好的模型和相关数据
model: 训练好的模型
vocab_size: 词汇表大小
chars: 字符列表
stoi: 字符到索引的映射
itos: 索引到字符的映射
train_losses: 训练损失历史
model_name: 模型名称(可选)
"""
# 生成时间戳
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 如果没有指定模型名称,使用时间戳
if model_name is None:
model_name = f"transformer_model_{timestamp}"
# 创建保存目录
save_dir = "saved_models"
os.makedirs(save_dir, exist_ok=True)
# 准备保存的数据
save_data = {
'model_state_dict': model.state_dict(), # 模型参数
'model_config': {
'vocab_size': vocab_size,
'embed_size': 32,
'n_layers': 2,
'heads': 2,
'block_size': 32
},
'vocab_data': {
'chars': chars,
'stoi': stoi,
'itos': itos
},
'training_info': {
'train_losses': train_losses,
'final_loss': train_losses[-1] if train_losses else None,
'epochs_trained': len(train_losses),
'timestamp': timestamp
}
}
# 保存模型文件
model_path = os.path.join(save_dir, f"{model_name}.pth")
torch.save(save_data, model_path)
print(f"模型已保存到: {model_path}")
print(f"模型配置: {save_data['model_config']}")
print(f"训练信息: 共训练 {len(train_losses)} 轮,最终损失: {train_losses[-1]:.4f}")
return model_path
# ------------------------------
# 8. 训练过程:让模型学习文本模式
# ------------------------------
def _progress_bar(current, total, bar_len=30):
"""
简单文本进度条。
参数:
current: 当前进度(批次)。
total: 总批次数。
bar_len: 进度条长度。
返回:
(bar_str, ratio)
"""
ratio = current / total
filled = int(bar_len * ratio)
bar = '█' * filled + '-' * (bar_len - filled)
return bar, ratio
def train_model(model, train_loader, val_loader, criterion, optimizer, stoi, itos, chars, vocab_size,
epochs=1, patience=3):
"""
训练主循环(含验证、最佳保存、早停、控制台进度)。
参数:
model, train_loader, val_loader, criterion, optimizer: 训练组件。
stoi, itos, chars, vocab_size: 词表相关信息(用于保存)。
epochs: 训练轮数。
patience: 验证集无提升的容忍轮数(早停)。
返回:
train_losses: 每个 epoch 的平均损失。
batch_losses: 每个 batch 的损失记录。
"""
device = next(model.parameters()).device
print(f"开始训练,共 {epochs} 轮...")
train_losses = []
batch_losses = []
best_val_loss = float('inf')
no_improve_epochs = 0
for epoch in range(epochs):
epoch_start = time.time()
epoch_losses = []
num_batches = len(train_loader)
model.train()
for batch_idx, (x, y) in enumerate(train_loader, start=1):
x, y = x.to(device), y.to(device)
optimizer.zero_grad()
logits = model(x)
loss = criterion(logits.view(-1, vocab_size), y.view(-1))
loss.backward()
optimizer.step()
loss_val = loss.item()
batch_losses.append(loss_val)
epoch_losses.append(loss_val)
bar, ratio = _progress_bar(batch_idx, num_batches)
elapsed = time.time() - epoch_start
avg_time_per_batch = elapsed / batch_idx
eta = (num_batches - batch_idx) * avg_time_per_batch
avg_loss_so_far = np.mean(epoch_losses)
print(f"\rEpoch {epoch+1}/{epochs} [{bar}] {batch_idx}/{num_batches} loss={avg_loss_so_far:.4f} ETA={eta:5.1f}s", end="", flush=True)
avg_epoch_loss = np.mean(epoch_losses)
train_losses.append(avg_epoch_loss)
model.eval()
val_losses = []
with torch.no_grad():
for x_val, y_val in val_loader:
x_val, y_val = x_val.to(device), y_val.to(device)
logits_val = model(x_val)
val_loss = criterion(logits_val.view(-1, vocab_size), y_val.view(-1)).item()
val_losses.append(val_loss)
avg_val_loss = float(np.mean(val_losses)) if val_losses else float('inf')
epoch_time = time.time() - epoch_start
print(f"\rEpoch {epoch+1}/{epochs} [██████████████████████████████] {num_batches}/{num_batches} train_loss={avg_epoch_loss:.4f} val_loss={avg_val_loss:.4f} time={epoch_time:5.1f}s")
if avg_val_loss < best_val_loss - 1e-6:
best_val_loss = avg_val_loss
no_improve_epochs = 0
best_model_path = save_model(model, vocab_size, chars, stoi, itos, train_losses, model_name=f"best_epoch_{epoch+1}")
print(f"验证loss改善,已保存最佳模型: {best_model_path}")
else:
no_improve_epochs += 1
print(f"验证loss未改善(连续 {no_improve_epochs}/{patience} 次)")
if no_improve_epochs >= patience:
print("触发早停,结束训练。")
break
print("训练完成!")
return train_losses, batch_losses
# ------------------------------
# 8.1. 训练过程可视化
# ------------------------------
def plot_training_curves(train_losses, batch_losses):
"""
绘制训练损失曲线(按 epoch 与按 batch)。
参数:
train_losses: 每个 epoch 的平均损失。
batch_losses: 每个 batch 的损失。
"""
# 创建子图
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
# 绘制epoch损失曲线
ax1.plot(train_losses, 'b-', linewidth=2, marker='o')
ax1.set_title('训练损失曲线 (按Epoch)', fontsize=14, fontweight='bold')
ax1.set_xlabel('Epoch')
ax1.set_ylabel('平均损失')
ax1.grid(True, alpha=0.3)
ax1.set_yscale('log') # 使用对数坐标,更好地显示损失下降
# 绘制batch损失曲线(更详细的训练过程)
ax2.plot(batch_losses, 'r-', alpha=0.7, linewidth=1)
ax2.set_title('训练损失曲线 (按Batch)', fontsize=14, fontweight='bold')
ax2.set_xlabel('Batch')
ax2.set_ylabel('损失')
ax2.grid(True, alpha=0.3)
ax2.set_yscale('log')
# 调整布局
plt.tight_layout()
# 保存图片
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
plt.savefig(f'training_curves_{timestamp}.png', dpi=300, bbox_inches='tight')
print(f"训练曲线已保存为: training_curves_{timestamp}.png")
# 显示图片
plt.show()
def main():
"""
端到端训练管线:
1) 加载原始文本 → 2) 预处理 → 3) 分词
4) 构建词表 → 5) 构建数据集/加载器并展示样例
6) 构建训练组件 → 7) 训练(带验证/保存/早停)
8) 可视化并最终保存 → 9) 打印使用说明
"""
# 路径与原始文本
novel_path = os.path.join(os.path.dirname(__file__), 'data', '西游记.txt')
raw_text = load_raw_text(novel_path)
# 预处理与分词
cleaned_text = preprocess_text(raw_text)
token_text = tokenize_text(cleaned_text)
# 词表
chars, stoi, itos, vocab_size = build_vocab(token_text)
# 数据与样例
dataset, train_loader, val_loader, train_dataset, val_dataset = create_dataloaders(
token_text, block_size=32, batch_size=16, val_ratio=0.1, stoi=stoi
)
show_samples(dataset, itos)
describe_format_and_batch(train_loader, itos)
# 组件
device, model, optimizer, criterion = build_training_components(vocab_size, lr=0.001)
# 训练
train_losses, batch_losses = train_model(
model, train_loader, val_loader, criterion, optimizer,
stoi, itos, chars, vocab_size, epochs=1, patience=3
)
# 可视化与保存
plot_training_curves(train_losses, batch_losses)
model_path = save_model(model, vocab_size, chars, stoi, itos, train_losses)
# 完成提示
print("\n" + "="*60)
print("训练完成!模型使用说明")
print("="*60)
print("1. 模型已保存,可以重复使用")
print(f" 最终模型路径: {model_path}")
print("2. 要使用模型进行文本生成,请运行:")
print(" python predict_transformer.py")
print("3. 或者运行批量测试:")
print(" python predict_transformer.py test")
print("4. 训练曲线图已保存,可以查看训练过程")
print("5. 最佳模型已自动保存到 saved_models/ 目录")
print("="*60)
if __name__ == "__main__":
main()
核心代码-预测
# -*- coding: utf-8 -*-
"""
Transformer模型预测脚本
这个脚本用于加载已训练的模型并进行文本生成
可以独立运行,不需要重新训练模型
"""
import os
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
from datetime import datetime
# 在预测脚本内本地定义模型类,避免导入训练脚本触发训练
class SelfAttention(nn.Module):
"""
自注意力层:多头注意力的最小实现,用于捕获序列中位置间依赖。
"""
def __init__(self, embed_size, heads):
super().__init__()
self.heads = heads
self.head_dim = embed_size // heads
self.qkv = nn.Linear(embed_size, embed_size * 3)
self.fc_out = nn.Linear(embed_size, embed_size)
def forward(self, x):
N, T, C = x.shape
qkv = self.qkv(x).reshape(N, T, 3, self.heads, self.head_dim)
q, k, v = qkv[:, :, 0], qkv[:, :, 1], qkv[:, :, 2]
scores = torch.einsum("nthe,nshe->nths", q, k) / math.sqrt(self.head_dim)
mask = torch.tril(torch.ones(T, T, device=x.device)).unsqueeze(0).unsqueeze(2)
scores = scores.masked_fill(mask == 0, float("-inf"))
attn = F.softmax(scores, dim=-1)
out = torch.einsum("nths,nshe->nthe", attn, v)
out = out.reshape(N, T, C)
return self.fc_out(out)
class Block(nn.Module):
"""
Transformer 基本块:自注意力 + 前馈网络 + 残差 + 层归一化。
"""
def __init__(self, embed_size, heads):
super().__init__()
self.attn = SelfAttention(embed_size, heads)
self.norm1 = nn.LayerNorm(embed_size)
self.ff = nn.Sequential(
nn.Linear(embed_size, embed_size * 2),
nn.ReLU(),
nn.Linear(embed_size * 2, embed_size),
)
self.norm2 = nn.LayerNorm(embed_size)
def forward(self, x):
x = self.norm1(x + self.attn(x))
x = self.norm2(x + self.ff(x))
return x
class TinyDecoderTransformer(nn.Module):
"""
极简 Decoder-only Transformer(GPT 结构的精简版)。
组件:词嵌入、位置嵌入、若干 Block、层归一化与输出头。
"""
def __init__(self, vocab_size, embed_size=32, n_layers=2, heads=2, block_size=32):
super().__init__()
self.token_emb = nn.Embedding(vocab_size, embed_size)
self.pos_emb = nn.Embedding(block_size, embed_size)
self.layers = nn.ModuleList([Block(embed_size, heads) for _ in range(n_layers)])
self.ln = nn.LayerNorm(embed_size)
self.head = nn.Linear(embed_size, vocab_size)
self.block_size = block_size
def forward(self, x):
N, T = x.shape
positions = torch.arange(T, device=x.device).unsqueeze(0)
x = self.token_emb(x) + self.pos_emb(positions)
for layer in self.layers:
x = layer(x)
x = self.ln(x)
return self.head(x)
# ------------------------------
# 模型加载函数
# ------------------------------
def load_model(model_path, device="cpu"):
"""
加载已保存的 Transformer 模型(含词表与训练信息)。
参数:
model_path: .pth 模型文件路径
device: 加载到的设备('cpu' 或 'cuda')
返回:
model: 构建并加载权重后的模型
vocab_data: { 'stoi':..., 'itos':..., 'chars':... }
training_info: { 'train_losses':..., 'final_loss':..., ... }
"""
print(f"正在加载模型: {model_path}")
# 检查文件是否存在
if not os.path.exists(model_path):
raise FileNotFoundError(f"模型文件不存在: {model_path}")
# 加载保存的数据
save_data = torch.load(model_path, map_location=device)
# 提取模型配置
model_config = save_data['model_config']
vocab_data = save_data['vocab_data']
training_info = save_data['training_info']
# 创建新的模型实例
model = TinyDecoderTransformer(
vocab_size=model_config['vocab_size'],
embed_size=model_config['embed_size'],
n_layers=model_config['n_layers'],
heads=model_config['heads'],
block_size=model_config['block_size']
)
# 加载模型参数
model.load_state_dict(save_data['model_state_dict'])
model.to(device)
# 设置为评估模式
model.eval()
print(f"模型加载成功!")
print(f"模型配置: {model_config}")
print(f"训练信息: 共训练 {training_info['epochs_trained']} 轮,最终损失: {training_info['final_loss']:.4f}")
print(f"训练时间: {training_info['timestamp']}")
return model, vocab_data, training_info
# ------------------------------
# 文本生成函数
# ------------------------------
def generate_text(model, stoi, itos, seed_text, length=100, block_size=32, device="cpu"):
"""
基于贪心策略的文本生成(每步选概率最高的下一个字符)。
参数:
model, stoi, itos: 模型与词表
seed_text: 起始文本
length: 生成字符数量
block_size: 模型上下文窗口
device: 设备
返回:
生成的完整文本(包含种子与新增字符)
"""
# 将模型设置为评估模式(关闭dropout等)
model.eval()
# 将种子文本转换为字符ID序列
input_ids = torch.tensor([stoi[ch] for ch in seed_text], dtype=torch.long).unsqueeze(0).to(device)
generated = input_ids # 存储生成的序列
# 逐个生成字符
for _ in range(length):
# 只使用最后block_size个字符作为输入(保持序列长度不变)
logits = model(generated[:, -block_size:])
# 选择概率最高的字符作为下一个字符(贪心策略)
next_token = torch.argmax(logits[:, -1, :], dim=-1, keepdim=True)
# 将新生成的字符添加到序列中
generated = torch.cat([generated, next_token], dim=1)
# 将生成的字符ID转换回文本
generated_text = ''.join([itos[int(i)] for i in generated[0]])
return generated_text
def generate_text_with_temperature(model, stoi, itos, seed_text, length=100, block_size=32, temperature=1.0, device="cpu"):
"""
使用温度采样的文本生成(更具多样性)。
参数:
temperature: 温度系数,>1 更随机,<1 更保守,=1 等价原分布
说明:
先对 logits 除以温度,再用 softmax 得到分布进行抽样。
"""
model.eval()
input_ids = torch.tensor([stoi[ch] for ch in seed_text], dtype=torch.long).unsqueeze(0).to(device)
generated = input_ids
for _ in range(length):
logits = model(generated[:, -block_size:])
# 应用温度缩放
logits = logits[:, -1, :] / temperature
# 使用softmax采样
probs = F.softmax(logits, dim=-1)
next_token = torch.multinomial(probs, num_samples=1)
generated = torch.cat([generated, next_token], dim=1)
generated_text = ''.join([itos[int(i)] for i in generated[0]])
return generated_text
# ------------------------------
# 交互式预测
# ------------------------------
def interactive_prediction():
"""
交互式预测:从最新的模型文件加载,用户输入种子实时生成文本。
支持贪心与温度采样两种策略。
"""
print("="*60)
print("Transformer模型文本生成器")
print("="*60)
# 选择设备
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"使用设备: {device}")
# 查找最新的模型文件
save_dir = "saved_models"
if not os.path.exists(save_dir):
print(f"错误: 保存目录 {save_dir} 不存在")
print("请先运行 train_transformer.py 训练模型")
return
# 获取所有模型文件
model_files = [f for f in os.listdir(save_dir) if f.endswith('.pth')]
if not model_files:
print(f"错误: 在 {save_dir} 中没有找到模型文件")
print("请先运行 train_transformer.py 训练模型")
return
# 选择最新的模型文件
latest_model = max(model_files, key=lambda x: os.path.getctime(os.path.join(save_dir, x)))
model_path = os.path.join(save_dir, latest_model)
print(f"找到模型文件: {model_path}")
try:
# 加载模型
model, vocab_data, training_info = load_model(model_path, device)
# 获取词汇表数据
stoi = vocab_data['stoi']
itos = vocab_data['itos']
block_size = model.block_size
print("\n" + "="*50)
print("开始交互式文本生成")
print("输入 'quit' 退出,输入 'help' 查看帮助")
print("="*50)
while True:
try:
user_input = input("\n请输入种子文本: ").strip()
if user_input.lower() == 'quit':
print("退出程序")
break
elif user_input.lower() == 'help':
print("\n帮助信息:")
print("- 输入任意文本作为种子,模型会继续生成")
print("- 输入 'quit' 退出")
print("- 输入 'help' 显示此帮助")
print("- 种子文本中的字符必须在训练词汇表中")
continue
if not user_input:
print("请输入有效的种子文本")
continue
# 检查字符是否在词汇表中
valid_seed = ""
invalid_chars = []
for char in user_input:
if char in stoi:
valid_seed += char
else:
invalid_chars.append(char)
if invalid_chars:
print(f"警告: 以下字符不在词汇表中,将被忽略: {invalid_chars}")
if not valid_seed:
print("种子文本中没有有效字符,请重新输入")
continue
# 获取生成长度
length_input = input("请输入生成长度 (默认100): ").strip()
try:
length = int(length_input) if length_input else 100
except ValueError:
length = 100
# 选择生成策略
strategy = input("选择生成策略 (1=贪心, 2=温度采样, 默认1): ").strip()
print(f"\n使用种子: '{valid_seed}'")
print("生成中...")
if strategy == '2':
temp_input = input("请输入温度参数 (默认1.0): ").strip()
try:
temperature = float(temp_input) if temp_input else 1.0
except ValueError:
temperature = 1.0
generated = generate_text_with_temperature(
model, stoi, itos, valid_seed, length, block_size, temperature, device
)
print(f"生成结果 (温度={temperature}): {generated}")
else:
generated = generate_text(
model, stoi, itos, valid_seed, length, block_size, device
)
print(f"生成结果: {generated}")
except KeyboardInterrupt:
print("\n\n退出交互模式")
break
except Exception as e:
print(f"生成文本时出错: {e}")
except Exception as e:
print(f"加载模型时出错: {e}")
# ------------------------------
# 批量测试
# ------------------------------
def batch_test():
"""
批量测试:使用一组预置的中文开头作为种子,快速观察模型效果。
"""
print("="*60)
print("批量文本生成测试")
print("="*60)
device = "cuda" if torch.cuda.is_available() else "cpu"
save_dir = "saved_models"
if not os.path.exists(save_dir):
print(f"错误: 保存目录 {save_dir} 不存在")
return
model_files = [f for f in os.listdir(save_dir) if f.endswith('.pth')]
if not model_files:
print(f"错误: 在 {save_dir} 中没有找到模型文件")
return
latest_model = max(model_files, key=lambda x: os.path.getctime(os.path.join(save_dir, x)))
model_path = os.path.join(save_dir, latest_model)
try:
model, vocab_data, training_info = load_model(model_path, device)
stoi = vocab_data['stoi']
itos = vocab_data['itos']
block_size = model.block_size
# 测试不同的种子文本
test_seeds = [
"从前有", # 故事开头
"他说道", # 对话开头
"春天来", # 描述开头
"突然", # 转折词
"最后" # 结尾词
]
for i, seed in enumerate(test_seeds, 1):
print(f"\n测试 {i}: 种子文本 = '{seed}'")
print("-" * 40)
# 检查种子文本中的字符是否在词汇表中
valid_seed = ""
for char in seed:
if char in stoi:
valid_seed += char
else:
print(f"警告: 字符 '{char}' 不在词汇表中,跳过")
if valid_seed:
generated = generate_text(model, stoi, itos, valid_seed, length=100, block_size=block_size, device=device)
print(f"生成文本: {generated}")
else:
print("种子文本无效,跳过生成")
print("\n" + "="*50)
print("批量测试完成!")
print("="*50)
except Exception as e:
print(f"批量测试时出错: {e}")
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "test":
# 运行批量测试
batch_test()
else:
# 运行交互式预测
interactive_prediction()
常见问题(FAQ)
- • 训练很慢?入门阶段以“跑通”为主,可在 CPU 小规模实验,减少轮数与数据量;条件允许再用 GPU。
- • 生成重复?尝试提高温度、增大上下文、或增加训练数据的多样性与清洁度。
- • 中文需要分词吗?本示例等价于“字级建模”(分词后去空格),入门友好;后续可改词级/子词级。
- • 模型保存后加载报路径错误?确保加载路径与保存一致,优先使用绝对路径。
进阶方向与可扩展点
- • 位置编码:尝试 RoPE、ALiBi、YaRN 等,改动小、收益直观。
- • 优化器/调度器:AdamW,改善收敛与泛化。
- • 正则化:加入 Dropout、权重衰减,缓解过拟合。
- • 模型规模:逐步增大
embed_size、n_layers、heads,并放大与清洗数据。 - • 数据工程:去重去噪、提升数据质量。
结语
当你亲手跑通一次最小 GPT 的训练与推理,就完成了从“理论理解”到“工程实现”的关键跨越。之后的升级,本质上是把每个模块做得更强、更稳、更高效。祝你玩得开心,也欢迎在此基础上继续扩展,例如替换位置编码、引入更强优化器,或将字符级改为词级/子词级建模。
普通人如何抓住AI大模型的风口?
领取方式在文末
为什么要学习大模型?
目前AI大模型的技术岗位与能力培养随着人工智能技术的迅速发展和应用 , 大模型作为其中的重要组成部分 , 正逐渐成为推动人工智能发展的重要引擎 。大模型以其强大的数据处理和模式识别能力, 广泛应用于自然语言处理 、计算机视觉 、 智能推荐等领域 ,为各行各业带来了革命性的改变和机遇 。
目前,开源人工智能大模型已应用于医疗、政务、法律、汽车、娱乐、金融、互联网、教育、制造业、企业服务等多个场景,其中,应用于金融、企业服务、制造业和法律领域的大模型在本次调研中占比超过 30%。

随着AI大模型技术的迅速发展,相关岗位的需求也日益增加。大模型产业链催生了一批高薪新职业:

人工智能大潮已来,不加入就可能被淘汰。如果你是技术人,尤其是互联网从业者,现在就开始学习AI大模型技术,真的是给你的人生一个重要建议!
最后
只要你真心想学习AI大模型技术,这份精心整理的学习资料我愿意无偿分享给你,但是想学技术去乱搞的人别来找我!
在当前这个人工智能高速发展的时代,AI大模型正在深刻改变各行各业。我国对高水平AI人才的需求也日益增长,真正懂技术、能落地的人才依旧紧缺。我也希望通过这份资料,能够帮助更多有志于AI领域的朋友入门并深入学习。
真诚无偿分享!!!
vx扫描下方二维码即可
加上后会一个个给大家发

大模型全套学习资料展示
自我们与MoPaaS魔泊云合作以来,我们不断打磨课程体系与技术内容,在细节上精益求精,同时在技术层面也新增了许多前沿且实用的内容,力求为大家带来更系统、更实战、更落地的大模型学习体验。

希望这份系统、实用的大模型学习路径,能够帮助你从零入门,进阶到实战,真正掌握AI时代的核心技能!
01 教学内容

-
从零到精通完整闭环:【基础理论 →RAG开发 → Agent设计 → 模型微调与私有化部署调→热门技术】5大模块,内容比传统教材更贴近企业实战!
-
大量真实项目案例: 带你亲自上手搞数据清洗、模型调优这些硬核操作,把课本知识变成真本事!
02适学人群
应届毕业生: 无工作经验但想要系统学习AI大模型技术,期待通过实战项目掌握核心技术。
零基础转型: 非技术背景但关注AI应用场景,计划通过低代码工具实现“AI+行业”跨界。
业务赋能突破瓶颈: 传统开发者(Java/前端等)学习Transformer架构与LangChain框架,向AI全栈工程师转型。

vx扫描下方二维码即可

本教程比较珍贵,仅限大家自行学习,不要传播!更严禁商用!
03 入门到进阶学习路线图
大模型学习路线图,整体分为5个大的阶段:

04 视频和书籍PDF合集

从0到掌握主流大模型技术视频教程(涵盖模型训练、微调、RAG、LangChain、Agent开发等实战方向)

新手必备的大模型学习PDF书单来了!全是硬核知识,帮你少走弯路(不吹牛,真有用)

05 行业报告+白皮书合集
收集70+报告与白皮书,了解行业最新动态!

06 90+份面试题/经验
AI大模型岗位面试经验总结(谁学技术不是为了赚$呢,找个好的岗位很重要)

07 deepseek部署包+技巧大全

由于篇幅有限
只展示部分资料
并且还在持续更新中…
真诚无偿分享!!!
vx扫描下方二维码即可
加上后会一个个给大家发

5832

被折叠的 条评论
为什么被折叠?



