Skip to content

深入模型训练 Pytorch

约 5595 字大约 19 分钟

AI

2026-03-06

由 MNN 衍生发现想要运行MNN模型还是需要从依赖Pytorch/TF来训练。

总结MNN流程准备PyTorch模型 -> 使用 llmexport.py 导出为 MNN 格式 -> 编译 MNN 引擎 (启用 LLM) -> 配置 config.json -> 使用 llm_demo 进行推理

Pytorch 与 TF

都是目前深度学习领域的主流框架

Pytorch

pytorch 是基于 Python 构建的深度学习框架,意思是他是python的一个扩展库。
pytorch 的核心底层是用 c++/CUDA 实现的

上层是python api(torch模块)面对开发者,好调试。 底层是 C++/CUDA (张量库、自动求导引擎)为核心是实现的高性能跨平台

核心概念:
张量(Tensor)操作
自动求导(autograd)
构建模型(nn.Module)
数据加载(DataLoader + Dataset)
训练循环(前向传播、损失计算、反向传播、参数更新)

常规用法

常规用法是在python中用pytorch训练,调试模型;然后将训练好的模型导出,使用c++加载并推理;

当你够熟悉或者需要极致性能,可以直接用c++开发pytorch模型,无需python参与。

步骤:

  1. python端训练导出模型
	import torch
	import torch.nn as nn
	
	# 1. 定义一个简单的线性回归模型(
	class LinearModel(nn.Module):
	    def __init__(self):
	        super().__init__()
	        self.linear = nn.Linear(1, 1)  # 输入1维,输出1维
	
	    def forward(self, x):
	        return self.linear(x)
	
	# 2. 训练模型(简化版,仅演示导出)
	model = LinearModel()
	# 模拟训练(实际项目中需完整训练)
	x = torch.tensor([[1.0], [2.0], [3.0]])
	y = model(x)
	
	# 3. 导出为 TorchScript 格式(关键:桥接 Python 和 C++)
	# 方式1:追踪式(适合无动态控制流的模型)
	traced_model = torch.jit.trace(model, x)
	# 方式2:脚本式(适合有if/for等动态逻辑的模型)
	# scripted_model = torch.jit.script(model)
	
	# 保存模型文件
	traced_model.save("linear_model.pt")
	print("模型已导出为 linear_model.pt")
  1. C++端加载运行模型
	// 先安装LibTorch库 pytorch的c++库: https://pytorch.org/get-started/locally/
	
	#include <torch/torch.h>
	#include <iostream>
	int main() {
	    // 1. 加载 TorchScript 模型
	    std::string model_path = "linear_model.pt";
	    torch::jit::script::Module model;
	    try {
	        model = torch::jit::load(model_path);
	        std::cout << "模型加载成功!" << std::endl;
	    } catch (const c10::Error& e) {
	        std::cerr << "加载模型失败:" << e.what() << std::endl;
	        return -1;
	    }
	
	    // 2. 准备输入数据(C++ 张量,和 Python 张量对应)
	    // 创建 [1,1] 的张量,值为 5.0(对应 Python 的 torch.tensor([[5.0]]))
	    torch::Tensor input = torch::tensor({{5.0}}, torch::kFloat32);
	    
	    // 3. 构造输入列表(PyTorch C++ 要求输入封装为 vector)
	    std::vector<torch::jit::IValue> inputs;
	    inputs.push_back(input);
	
	    // 4. 执行推理(前向传播)
	    torch::Tensor output = model.forward(inputs).toTensor();
	
	    // 5. 输出结果
	    std::cout << "输入:5.0,预测结果:" << output.item<float>() << std::endl;
	
	    return 0;
	}

TensorFlow

TensorFlow有google开发的,核心优势是工业级的部署能力和完整的生态系统; 他也是由c++实现,上层使用python等语言

Code!

手写线性回归

线性回归(Linear Regression):

  • 线性:直线(单调的均匀变化的)
  • 回归:往回推,找规律。把杂乱的数据回归到他本来的趋势线上。

最简单的监督学习任务,本质上就是用数据拟合出一条线,预测未来的结果

比如带入场景

  • 学习时间 vs 考试成绩:一般来说,学的越久(X),分越高(Y)。线性回归就是找出多学 1 小时,大概能提多少分。
  • 广告费 vs 销售额:投的钱越多(X),卖得越多(Y)。线性回归就是算出每投 1 万广告费,大概能带来多少销售额。
  • 在线性回归的基础上深入一下 -> 逻辑回归可以做分类任务

掌握框架的核心流程:数据准备 → 模型定义 → 损失函数 / 优化器 → 训练循环 → 验证

import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt

# 1.生成数据集 (拿房子价格来举例)
x = torch.randn(200,1) # 生成100个样本,每个样本有1个特征 (选取100套房子的面积数)
y = 2 * x + 3 + 0.3 * torch.randn(200,1) # 基于x生成y,规律是y=2x+3, 真实数据没这么规律加少量噪声(房子的价格变化)

# 2.定义模型  单层线性层
model = nn.Linear(1,1) #一个输入特征数(房子面积),一个输出特征数 (预测价格)

# 3.定义损失函数和优化器
#均方误差,预测值与真实值的差的平方的平均值; 数值越小说明预测越准
criterion = nn.MSELoss() 
#优化器,负责调整模型的参数,让他越学越准
#SGD:随机梯度下降,最基础的优化算法
#lr=0.01 学习率:相当于迈的步子大小, 太大可能跨过头找不到最佳点  太小会学得慢 需要时间长
optimizer = torch.optim.SGD(model.parameters(),lr=0.02) #随机梯度下降,学习率0.1

# 4.循环训练
epochs = 200
loss_history = []

for epoch in range(epochs):
    y_pred = model(x) #应用当前模型来预测 前向传播 预测y

    #计算损失
    loss =  criterion(y_pred,y)         #计算预测得准不准                 *随便找个数
    loss_history.append(loss.item())    #记录损失值  损失的越多说明越不准!*看看跟本来的值差了多少

    #反向传播+优化
    optimizer.zero_grad()               #梯度清零                       *忘掉上一个值
    loss.backward()                     #反向传播计算梯度              *分析原因,看看预测的值多了还是少了
    optimizer.step()                    #更新模型参数                  *根据分析的结果调整一下预测方式

    #每10轮打印一次损失
    if(epoch+1) %10==0:
        print(f"Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}")

# 5.验证模型
print("\n训练后的模型参数:") #打印训练后的参数 理想值w≈2 b≈3
for name,param in  model.named_parameters():
    print(f"{name}: {param.data.item():.4f}")

#可视化结果
plt.figure(figsize=(10,4))

#子图1:损失变化        *看模型是不是越学越好(曲线应该下降)
plt.subplot(1,2,1)
plt.plot(loss_history)
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("Training Loss")

#子图2 真实值与预测值    *红线和蓝点越贴近,说明学得越好
plt.subplot(1,2,2)
plt.scatter(x.numpy(),y.numpy(),label="True Data")
plt.plot(x.numpy(),model(x).detach().numpy() ,color='red',label="Prediction")
plt.legend()
plt.title("True and prediction")

plt.show()

总结:

  • x特征张量(模型的输入),y标签张量(y)。二者一一对应,形状为:[样本数,特征/标签维度]
  • x和y是随机生成的模型数据,满足y=2x+3+噪声,目的是为了让模型学习找个线性关系
  • 真实场景中,x与y需要从文件中加载并转化成框架支持的张量格式,核心逻辑(特征 - 标签对应)。

深度学习的Hello World! 手写数字识别(MNIST 数据集)

MNIST:Modified National Institute of Standards and Technology(修改后的国家标准与技术研究所),一个包含了大量手写数字图片的数据集。

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets,transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt


device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

#训练参数
batch_size = 64       #批次大小  *每次训练多少张图, 太大可能显存/内存不够,太小训练慢。 批次太小→训练慢且不稳定,批次太大→显存不够且泛化能力差
learning_rate = 0.02  #学习率
epochs = 5           #训练轮数


# 1.加载数据集 自动下载
transform = transforms.ToTensor()
train_dataset = datasets.MNIST(root="./data",train=True,transform=transform,download=True)
test_dataset = datasets.MNIST(root="./data",train=False,transform=transform)
train_loader = DataLoader(train_dataset,batch_size=batch_size,shuffle=True)
test_loader = DataLoader(test_dataset,batch_size=batch_size,shuffle=False)


# 2.定义模型
#全连接模型 
class SimpleMLP(nn.Module):
    def __init__(self):
        super(SimpleMLP,self).__init__()
        self.model = nn.Sequential(
            nn.Flatten(), #将28x28的图像展平为784维的向量
            nn.Linear(784,128), #线性层,输入784维,输出128维
            nn.ReLU(),
            nn.Linear(128,10) #线性层,输入128维,输出10维(对应10个数字类别)
        )

    def forward(self,x):
        return self.model(x)
    
#CNN模型
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN,self).__init__()

        #卷积层1,输入1通道(灰度图一个通道),输出16通道(提取16个不同的特征器(卷积核)),卷积核大小3x3(3*3的窗口扫描图片,提取特征),步长1(卷积核每次向右/下移动1个像素。步长越大输出尺寸越小),填充1保持尺寸不变(在图片边缘补一圈0 保证尺寸)
        self.conv1 = nn.Conv2d(1,16,kernel_size=3,stride=1,padding=1)  #CNN的核心层,作用是提取图像的局部特征(比如数字的边缘,线条,拐角)

        self.relu=nn.ReLU() #激活函数 引入非线性,能拟合更复杂的特征; ReLU:`y = max(0, x)`只保留正数,把负数置 0 → 相当于 “筛选有用特征,丢弃无用特征”; 

        #池化层,尺寸减半(窗口大小2x2) MaxPool2d(2)表示使用2x2的窗口进行最大池化,即每4个像素取最大值,输出尺寸减半。
        self.pool = nn.MaxPool2d(2) #池化层的作用是降维(降低特征图的尺寸,减少参数数量和计算量,同时保留重要特征)。28×28 的特征图 → 池化后变成 14×14(尺寸减半,计算量减少 75%); 比如数字 “8” 的轮廓,池化后依然能保留 “两个圈” 的核心特征,但像素点更少了

        #第二个卷积层,输入16通道(上一层输出的特征图数量),输出32通道(提取32个不同的特征),卷积核大小3x3,步长1,填充1保持尺寸不变。池化后尺寸变成7x7,特征图数量变成32,所以全连接层输入维度是32*7*7=1568
        self.conv2 = nn.Conv2d(16,32,kernel_size=3,stride=1,padding=1) #提取更父杂的特征,比如数字的整体形状,笔画的连接方式等。第一层提取的是局部特征,第二层提取的是更抽象的全局特征。

        #全连接层1,输入1568 维(卷积 2 池化后的特征图尺寸是 32通道 × 7×7像素 → 展平后是 32×7×7=1568 维向量),输出128维
        self.fc1 = nn.Linear(32*7*7,128)  #把卷积提取的特征映射到高维特征空间,为最终分类做准备

        #全连接层2,输入128维(对应 fc1 的输出维度(必须匹配)),输出10维(对应10个数字类别 0-9的分类)
        self.fc2 = nn.Linear(128,10)  #最终分类,把 128 维特征映射到 10 个类别(0-9)。
    

    #前向传播:输入图像经过卷积层1、激活函数、池化层,卷积层2、激活函数、展平为向量,经过全连接层1、激活函数、全连接层2得到输出
    def forward(self,x):
        # 前向传播是数据在各个层里(网络)中的 “流动路径”

        #卷积层1 + 激活函数 + 池化层
        x=self.pool(self.relu(self.conv1(x))) #流动路径及维度变化:输入图像(64张图、1通道、28×28) → conv1 卷积层1提取局部特征((64, 16, 28, 28)通道变16,尺寸不变)) → ReLU激活函数((64, 16, 28, 28)只是数值变化) → pool 池化层降维保留重要特征(64, 16, 14, 14)尺寸减半)

        #卷积层2 + 激活;卷积层2提取更复杂的特征
        x=self.pool(self.relu(self.conv2(x))) #流动路径及维度变化:接收第一步的结果维度(64, 16, 14, 14) → conv2 卷积层2((64, 32, 14, 14)通道变32,尺寸不变) → ReLU激活函数(64, 32, 14, 14) → pool 池化层降维保留重要特征(64, 32, 7, 7)尺寸减半)

        #展平为向量,-1表示自动计算批次大小
        x=x.view(-1,32*7*7)  # x = x.flatten(1) 将二维的特征图转成一维向量,输入全连接层。  流动路径及维度变化:接收第二步的结果维度(64, 32, 7, 7) → 展平为向量(64, 1568) 32*7*7=1568 (64 个样本,每个样本 1568 维)

        #全连接层1 + 激活函数
        x=self.relu(self.fc1(x))  # 接上一步输入维度(64,1568) → fc1 全连接层1(64,128) → ReLU激活函数(64,128)

        #全连接层2
        x=self.fc2(x) #接上一步输入维度(64,128) → fc2 全连接层2(64,10) 输出10维对应10个类别的得分(logits),后续会通过损失函数计算概率并进行分类决策。
        return x




# 实例模型 全连接
# model = SimpleMLP().to(device)
# 实例模型 CNN
model = SimpleCNN().to(device)


#定义损失函数和优化器
criterion = nn.CrossEntropyLoss() #交叉熵损失函数,适用于多分类问题
optimizer = torch.optim.Adam(model.parameters(),lr=learning_rate) #Adam优化器,学习率0.001


# 训练模型
def train_model(model, train_loader, criterion, optimizer, epoch):
    model.train() #设置模型为训练模式
    train_loss = 0.0
    for batch_x,(data,target) in enumerate(train_loader):
        data,target = data.to(device),target.to(device) #将数据移到GPU

        optimizer.zero_grad() #清零梯度
        outputs = model(data) #前向传播
        loss = criterion(outputs,target) #计算损失
        loss.backward() #反向传播
        optimizer.step() #更新参数
        train_loss += loss.item() 

        if(batch_x+1) % 100 == 0: #每100个批次打印一次损失
            print(f"Epoch [{epoch+1}/{epochs}], Step [{batch_x+1}/{len(train_loader)}], Loss: {loss.item():.4f}")
            train_loss = 0.0 #重置损失值

def test(model, test_loader, criterion):
    model.eval() #设置模型为评估模式
    correct, total = 0,0
    with torch.no_grad(): #评估时不计算梯度
        for data,target in test_loader:
            data,target = data.to(device),target.to(device)
            outputs = model(data)
            test_loss = criterion(outputs,target).item() #计算测试损失
            pred = outputs.argmax(dim=1,keepdim=True) #取最大值的索引作为预测类别
            correct += pred.eq(target.view_as(pred)).sum().item() #统计正确预测的数量 
            total += target.size(0) #统计总的测试样本数量

    test_loss /= len(test_loader.dataset) #计算平均损失
    accuracy = 100 * correct / len(test_loader.dataset) #计算准确率,正确预测的数量除以总的测试样本数量
    print(f'\nTest set: Average loss: {test_loss:.4f}, Accuracy: {correct}/{len(test_loader.dataset)} ({accuracy:.2f}%)\n')
    return accuracy



if __name__ == "__main1__":
    best_accuracy = 0.0
    for epoch in range(epochs):
        train_model(model,train_loader,criterion,optimizer,epoch)
        curent_accuracy = test(model,test_loader,criterion)

        if curent_accuracy> best_accuracy:
            best_accuracy = curent_accuracy
            torch.save(model.state_dict(),'mnist_best_model.pth') #保存最优模型参数
    print(f'训练完成!最高测试准确率:{best_accuracy:.2f}%')
#============================== 训练结束 ===========================================
#===     MLP训练完成!最高测试准确率:95.39%     ========
#===     CNN训练完成!最高测试准确率:97.15%     ========



# ========================== 验证推理: 可视化预测结果 ===========================
def visualize_predictions():
    # 加载最优模型
    model.load_state_dict(torch.load('mnist_best_model.pth'))
    model.eval()
    
    # 取测试集前5张图片
    #关于推理的图片,由于训练使用的是28*28的灰度图,像素值0-1,背景黑/数字白的MNIST格式,所以如果随便上传图片进行推理,会有问题。
    #也需要转换成28*28的灰度图,像素值0-1,背景黑/数字白的MNIST格式,才能得到正确的预测结果。
    data_iter = iter(test_loader)
    images, labels = next(data_iter)
    images, labels = images.to(device), labels.to(device)
    
    # 预测
    outputs = model(images)
    preds = outputs.argmax(dim=1)


    # 可视化
    plt.figure(figsize=(10, 4))
    for i in range(11):
        plt.subplot(1, 11, i+1)
        # 转成CPU+numpy,恢复原始尺寸(去掉通道维度)
        img = images[i].cpu().squeeze().numpy()
        plt.imshow(img, cmap='gray')
        plt.title(f'Pred: {preds[i].item()}\nTrue: {labels[i].item()}')
        plt.axis('off')
    plt.tight_layout()
    plt.show()

# 运行可视化
visualize_predictions()

CNN层的概念

层 = 流水线工位

层类型核心作用维度变化(MNIST 示例)
卷积层(Conv2d)提取局部特征通道数增加,尺寸不变(靠 padding)
池化层(MaxPool2d)降维 + 保留关键特征尺寸减半,通道数不变
全连接层(Linear)特征映射 + 分类把高维特征转成类别得分
数据流动步骤经过的 “层”输入维度层的加工操作输出维度
第一步卷积层 1(64,1,28,28)提取 16 种局部特征(64,16,28,28)
第二步ReLU 层(64,16,28,28)筛选有用特征(非线性)(64,16,28,28)
第三步池化层(64,16,28,28)降维 + 保留关键特征(64,16,14,14)
第四步卷积层 2(64,16,14,14)提取 32 种更复杂特征(64,32,14,14)
第五步ReLU 层(64,32,14,14)筛选有用特征(64,32,14,14)
第六步池化层(64,32,14,14)再次降维(64,32,7,7)
第七步展平层(view/flatten)(64,32,7,7)转一维向量(64,1568)
第八步全连接层 1(64,1568)特征映射(64,128)
第九步ReLU 层(64,128)非线性变换(64,128)
第十步全连接层 2(64,128)最终分类(64,10)
  • 每一层的输出维度,就是下一层的输入维度(必须严格匹配,否则会报维度错误);
  • 前向传播的forward函数,就是按这个顺序把层 “串起来”
  • 网络的复杂度,本质就是 “层的数量 + 每层的参数数量”(比如 CNN 比全连接网络多了卷积层 / 池化层,能处理图像的空间特征)。

总结

  • 网络就是按特定顺序排列的层的集合,前向传播就是数据 “穿过每一层” 的过程;
  • 每一层的输入维度必须和上一层的输出维度匹配(这是维度报错的核心原因);
  • __init__定义 “有哪些层”,forward定义 “层的执行顺序”—— 这是 PyTorch 模型的核心逻辑。

LLM

import torch
import torch.nn as nn
import torch.nn.functional as F
import os

# 1. 模型定义
class MiniLLM(nn.Module):
    # 初始化模型,定义模型里有哪些层(词嵌入层、位置编码层、Transformer解码器层和输出层)
        # 参数(词汇表大小、嵌入维度、注意力头数、最大序列长度)
    def __init__(self, vocab_size, embed_dim, num_heads, max_seq_len):
        super().__init__()
        self.max_seq_len = max_seq_len
        self.embed_dim = embed_dim
        
        # 词嵌入层:把 单词ID → 向量
        # 输入:词汇表大小,输出:词向量维度
        self.token_embedding = nn.Embedding(vocab_size, embed_dim)

        # 位置嵌入层:给模型“知道单词在句子里的位置”
        # 因为 Transformer 不知道顺序,必须加位置信息
        self.pos_embedding = nn.Embedding(max_seq_len, embed_dim)
        
        # 定义一层 Transformer Decoder(语言模型核心)
        decoder_layer = nn.TransformerDecoderLayer(
            d_model=embed_dim,              # 模型维度 = 词向量维度
            nhead=num_heads,                # 多头注意力数量
            dim_feedforward=embed_dim*2,    # 前馈网络维度(通常是2倍)
            dropout=0.1,                    # 随机失活,防止过拟合
            batch_first=True,               # 输入形状优先:(批次, 序列长度, 维度)
            activation='gelu'               # 激活函数,比 relu 更平滑
        )
        # 把 N 层 Decoder 叠起来,这里用 2 层
        self.transformer_decoder = nn.TransformerDecoder(decoder_layer, num_layers=2)
        # 全连接层:把模型输出 → 映射到词汇表,预测下一个词
        self.fc = nn.Linear(embed_dim, vocab_size) 

    # 掩码(让模型只能看前面的词,不能偷看未来的词)
    def generate_causal_mask(self, seq_len, device):
        mask = torch.triu(torch.ones(seq_len, seq_len, device=device), diagonal=1)
        mask = mask.masked_fill(mask == 1, float('-inf'))
        return mask

    # 前向传播:模型真正的计算过程
    def forward(self, x):
        # B = 批次大小(一次训练多少句话), S = 序列长度(一句话多少个字)
        B, S = x.size()

        # 把单词ID → 词向量
        token_emb = self.token_embedding(x)
        # 生成位置索引 [0,1,2,...S-1]
        pos_idx = torch.arange(0, S, device=x.device).unsqueeze(0)
        # 位置索引 → 位置向量
        pos_emb = self.pos_embedding(pos_idx)
        # 词向量 + 位置向量 = 最终输入向量
        x = token_emb + pos_emb
        
        tgt_mask = self.generate_causal_mask(S, x.device)

        # 把向量送入 Transformer Decoder 计算
        x = self.transformer_decoder(x, x, tgt_mask=tgt_mask)
        # 全连接层输出 logits(未归一化的概率)
        logits = self.fc(x)
        return logits


# 2. 封装函数
def train_model(model, input_ids, target_ids, vocab_size, pad_id, epochs=100, lr=0.005, device='cpu'):
    """训练模型"""
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    loss_fn = nn.CrossEntropyLoss(ignore_index=pad_id)
    model.train()
    
    input_ids = input_ids.to(device)
    target_ids = target_ids.to(device)
    
    for epoch in range(epochs):
        optimizer.zero_grad()     # 清空上一步的梯度
        logits = model(input_ids) # 把数据喂给模型,得到预测结果

        # 计算损失:预测值 vs 真实值 .view(-1, vocab_size) 把形状展平,适配损失函数
        loss = loss_fn(logits.view(-1, vocab_size), target_ids.view(-1))

        # 反向传播:计算梯度
        loss.backward()
        # 更新模型参数
        optimizer.step()
        if (epoch + 1) % 10 == 0:
            print(f"Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}")
    return model

def save_model(model, path="./llm/llm_model.pt"):
    """保存模型"""
    os.makedirs(os.path.dirname(path), exist_ok=True)
    torch.save(model.state_dict(), path)
    print(f"模型已保存:{path}")

def load_model(model_class, vocab_size, embed_dim, num_heads, max_seq_len, path="./llm/llm_model.pt", device='cpu'):
    """加载模型"""
    # 创建一个空模型
    model = model_class(vocab_size, embed_dim, num_heads, max_seq_len).to(device)
    # 加载保存好的参数
    model.load_state_dict(torch.load(path, map_location=device, weights_only=False))
    model.eval()
    print(f"模型已加载:{path}")
    return model

def generate_text(model, start_tokens, idx2vocab, vocab, pad_id, max_new_tokens=10, temperature=1.0, device='cpu'):
    """生成文本(带温度采样)"""
    model.eval()
    generated = start_tokens.copy()
    
    with torch.no_grad():
        for _ in range(max_new_tokens):
            current_seq = torch.tensor([generated], device=device)
            logits = model(current_seq)
            
            # 温度缩放
            next_token_logits = logits[0, -1, :] / temperature
            
            # 概率采样
            probs = F.softmax(next_token_logits, dim=-1)
            next_token_id = torch.multinomial(probs, num_samples=1).item()

            # 防止越界
            next_token_id = min(next_token_id, len(idx2vocab) - 1)
            
            generated.append(next_token_id)
            if next_token_id == vocab.get("<EOS>", 0):
                break
    
    return ''.join([idx2vocab.get(idx, "?") for idx in generated if idx != pad_id])


# 3. 配置 & 数据
vocab = {
    "我":0, "今":1, "天":2, "很":3, "开":4, "心":5, 
    "你":6, "好":7, "爱":8, "他":9, "去":10, "学":11, 
    "校":12, "吃":13, "饭":14, "快":15, "乐":16, 
    "<PAD>":17, "<EOS>":18
}
idx2vocab = {idx: word for word, idx in vocab.items()}
vocab_size = len(vocab)
pad_id = vocab["<PAD>"]   # 填充符号 ID(句子长度不够时用来补齐)

embed_dim = 16
num_heads = 2
max_seq_len = 10

# 训练数据:(输入句子, 目标句子)
training_data = [
    ([0, 1, 2, 3, 4, 5],    [1, 2, 3, 4, 5, 18]), # 我今天很开心 → 今天很开心<EOS>
    ([0, 8, 6, 7],          [8, 6, 7, 18]),     # 我爱你好 → 爱你好<EOS>
    ([9, 10, 11, 12],       [10, 11, 12, 18]),  # 他去学校 → 去学校<EOS>
    ([0, 13, 14],           [13, 14, 18]),      # 我吃饭 → 吃饭<EOS>
    ([0, 3, 15, 16],        [3, 15, 16, 18]),   # 我很快乐 → 很快乐<EOS>
    ([6, 3, 4, 5],          [3, 4, 5, 18]),     # 你很开心 → 很开心<EOS>
]

# 把所有输入句子补齐到最大长度,不足补 <PAD>
input_ids = torch.tensor([s[0] + [pad_id]*(max_seq_len-len(s[0])) for s in training_data])
# 把所有目标句子补齐到最大长度,不足补 <PAD>
target_ids = torch.tensor([s[1] + [pad_id]*(max_seq_len-len(s[1])) for s in training_data])

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"device: {device}\n")

# 4. 调用
if __name__ == "__main__":
    # # 训练
    # print("==== 开始训练 ====")
    # model = MiniLLM(vocab_size, embed_dim, num_heads, max_seq_len).to(device)
    # model = train_model(model, input_ids, target_ids, vocab_size, pad_id, epochs=200, lr=0.005, device=device)
    # print("==== 训练完成 ====\n")
    
    # # 保存
    # save_model(model, "./llm/llm_model.pt")
    


    # 加载
    print("==== 加载模型 ====")
    model = load_model(MiniLLM, vocab_size, embed_dim, num_heads, max_seq_len, "./llm/llm_model.pt", device)
    
    # 生成测试:不同温度
    print("==== 不同温度生成测试 ====")
    for temp in [0.3, 0.8, 1.0, 1.5]:
        result = generate_text(model, [0], idx2vocab, vocab, pad_id, max_new_tokens=10, temperature=temp, device=device)
        print(f"{temp}: {result}")
    
    # 不同开头测试
    print("\n==== 不同开头生成测试 (T=0.8) ====")
    for start in [[0], [0,1], [0,3]]:
        start_words = ''.join([idx2vocab[i] for i in start])
        result = generate_text(model, start, idx2vocab, vocab, pad_id, max_new_tokens=10, temperature=0.8, device=device)
        print(f"输入 '{start_words}' → {result}")

流程:(词嵌入)词嵌入:把汉字变成数字向量 -> (位置嵌入)告诉模型字的顺序 -> (Transformer Decoder)靠 “注意力” 看前面的字,预测下一个字 -> (全连接层)输出概率,选概率最大的字

通俗点:把汉字变成数字向量 -> 喂给模型训练 - > 模型学会了汉字之间的关系 - > 给一个开头,模型能预测下一个字 - > 不断循环,生成一段文本

总结

这就是一个模型微型版的预训练过程

预训练

预训练 = 让模型不停做“猜下一个字”的游戏,猜错了就改,猜对了就记住规律。

  • ([0,1,2,3,4,5], [1,2,3,4,5,18]) => 我今天很开心 → 今天很开心<EOS>
    • 模型看到:我 → 今 → 天 → 很 → 开,必须预测下一个字是:
      • 模型预测错 → 反向传播改参数
      • 模型预测对 → 强化这个规律
  • 模型不是死记硬背句子,是在学习语言规律、语法、搭配、逻辑
    • 学习之后理解规律语法之类的,就能输出你没喂过她的句子

实际的大模型训练 数据量超大(万亿级别),模型超大 层数过百,训练时间也长。