【必学收藏】从零开始构建GPT模型:超详细小白实践教程(含完整代码)

部署运行你感兴趣的模型镜像

导语

结合之前介绍的各个子模块,这是一篇面向新手的从0到1训练小型 GPT 风格模型的实践文章。目标是用最小可行的代码与完整训练流水线,帮助你在不依赖复杂框架的情况下,理解并跑通“数据准备 → 模型搭建 → 训练验证 → 保存与推理”的关键环节。你无需深厚的数学背景,只要具备基础的 Python 使用经验即可上手。

你将收获什么

  • • 训练脚本:一个极简 Decoder-only Transformer(类似 GPT),包含嵌入、位置编码、自注意力、前馈网络、残差与层归一化。
  • • 预测脚本:支持加载已训练权重,进行贪心或温度采样文本生成,含交互式与批量测试两种模式。
  • • 数据流水线:从原始中文文本开始,经过清洗、分词、词表构建,转换为可训练的样本对(X→Y)。
  • • 训练工具:训练/验证划分、进度条、早停、最优模型保存、损失曲线可视化。

一图看懂训练流程

  1. 原始文本 → 2) 文本清洗与分词 → 3) 构建词表与索引映射 → 4) 划分训练/验证集并喂给 DataLoader → 5) 模型前向与交叉熵损失 → 6) 反向传播与优化 → 7) 验证监控与早停 → 8) 持久化模型与词表 → 9) 加载权重进行文本生成。

环境与数据建议

  • • 硬件:CPU 即可跑通;若有 GPU(如消费级显卡),训练会更快。
  • • 依赖:PyTorch、jieba、numpy、matplotlib 等(示例代码已导入,按需安装)。
  • • 数据:建议选择干净、风格一致的中文长文本(示例用《西游记》)。数据越一致,模型越容易学到稳定分布。

最小实操清单(Checklist)

    1. 准备 data/你的文本.txt,更新训练脚本中文件路径。
    1. 运行训练脚本,确认样本预览与 batch 形状打印正常。
    1. 观察进度与损失,等待早停或手动终止。
    1. 查看 saved_models/ 下生成的权重与配置。
    1. 运行预测脚本,测试交互式与批量生成,尝试不同温度与长度。

关键超参数怎么选

  • • 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_sizen_layersheads,并放大与清洗数据。
  • • 数据工程:去重去噪、提升数据质量。

结语

当你亲手跑通一次最小 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全栈工程师转型‌。

image.png

vx扫描下方二维码即可
在这里插入图片描述

本教程比较珍贵,仅限大家自行学习,不要传播!更严禁商用!

03 入门到进阶学习路线图

大模型学习路线图,整体分为5个大的阶段:
图片

04 视频和书籍PDF合集

图片

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

图片

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

05 行业报告+白皮书合集

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

06 90+份面试题/经验

AI大模型岗位面试经验总结(谁学技术不是为了赚$呢,找个好的岗位很重要)图片
在这里插入图片描述

07 deepseek部署包+技巧大全

在这里插入图片描述

由于篇幅有限

只展示部分资料

并且还在持续更新中…

真诚无偿分享!!!
vx扫描下方二维码即可
加上后会一个个给大家发

在这里插入图片描述

您可能感兴趣的与本文相关的镜像

GPT-oss:20b

GPT-oss:20b

图文对话
Gpt-oss

GPT OSS 是OpenAI 推出的重量级开放模型,面向强推理、智能体任务以及多样化开发场景

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值