大模型原理与实践:第五章-自己搭建大模型_第3部分-预训练一个小型LLM

第五章-自己搭建大模型_第3部分-预训练一个小型LLM

总目录

  1. 第一章 NLP基础概念完整指南

    1. 第1部分-概念和发展历史
    2. 第2部分-各种任务(实体识别、关系抽取、文本摘要、机器翻译、自动问答)
    3. 第3部分-文本表示(词向量、语言模型、ELMo)
  2. 第二章 Transformer 架构原理

    1. 第1部分-注意力机制
    2. 第2部分Encoder-Decoder架构
    3. 第3部分-完整Transformer模型)
  3. 第三章 预训练语言模型

    1. 第1部分-Encoder-only(BERT、RoBERTa、ALBERT)
    2. 第2部分-Encoder-Decoder-T5
    3. 第3部分-Decoder-Only(GPT、LLama、GLM)
  4. 第四章 大语言模型

    1. 第1部分-发展历程、上下文、指令遵循、多模态
    2. 第2部分-LLM预训练、监督微调、强化学习
  5. 第五章 动手搭建大模型

    1. 第1部分-动手实现一个LLaMA2大模型
    2. 第2部分-自己训练 Tokenizer
    3. 第3部分-预训练一个小型LLM
  6. 第六章 大模型训练实践

    1. 第1部分-模型预训练
    2. 第2部分-模型有监督微调
    3. 第3部分-高效微调
  7. 第七章 大模型实战

    1. 第1部分-评测+RAG检索增强生成
    2. 第2部分-智能体Agent系统

目录

  1. 预训练一个小型LLM
    • 3.1 数据下载与处理
    • 3.2 构建 Dataset
    • 3.3 预训练流程
    • 3.4 SFT 微调
    • 3.5 模型推理与生成
  2. 总结与资源](#4-总结与资源)

3. 预训练一个小型LLM

现在进入实战环节:训练一个约2亿参数的小型语言模型。我们将经历完整的数据准备、预训练、监督微调和推理流程。

3.1 数据下载与处理

数据集选择

预训练数据:出门问问序列猴子数据集

  • 规模:约10B tokens
  • 来源:网页、百科、代码、书籍等
  • 特点:高质量中文语料

SFT数据:BelleGroup中文对话数据集

  • 规模:350万条对话
  • 格式:人机对话
  • 特点:覆盖多种任务场景
下载脚本
import os

# 下载预训练数据
os.system(
    "modelscope download --dataset ddzhu123/seq-monkey "
    "mobvoi_seq_monkey_general_open_corpus.jsonl.tar.bz2 "
    "--local_dir ./data"
)

# 解压
os.system("tar -xvf ./data/mobvoi_seq_monkey_general_open_corpus.jsonl.tar.bz2")

# 下载SFT数据
os.system(
    "huggingface-cli download --repo-type dataset "
    "BelleGroup/train_3.5M_CN --local-dir ./BelleGroup"
)
数据预处理
import json
from tqdm import tqdm

def split_text(text: str, chunk_size: int = 512) -> list:
    """将长文本切分为固定长度的块"""
    return [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]

# 处理预训练数据
print("处理预训练数据...")
with open('seq_monkey_processed.jsonl', 'w', encoding='utf-8') as out_f:
    with open('mobvoi_seq_monkey_general_open_corpus.jsonl', 'r', encoding='utf-8') as in_f:
        for line in tqdm(in_f):
            data = json.loads(line)
            text = data['text']
            
            # 切分长文本
            chunks = split_text(text, chunk_size=512)
            for chunk in chunks:
                out_f.write(json.dumps({'text': chunk}, ensure_ascii=False) + '\n')

# 处理SFT数据
print("处理SFT数据...")
def convert_to_messages(conversations: list) -> list:
    """转换为标准消息格式"""
    messages = [{"role": "system", "content": "你是一个AI助手"}]
    for conv in conversations:
        if conv['from'] == 'human':
            messages.append({'role': 'user', 'content': conv['value']})
        elif conv['from'] == 'assistant':
            messages.append({'role': 'assistant', 'content': conv['value']})
    return messages

with open('belle_processed.jsonl', 'w', encoding='utf-8') as out_f:
    with open('BelleGroup/train_3.5M_CN.json', 'r', encoding='utf-8') as in_f:
        for line in tqdm(in_f):
            data = json.loads(line)
            messages = convert_to_messages(data['conversations'])
            out_f.write(json.dumps(messages, ensure_ascii=False) + '\n')

print("数据处理完成!")

3.2 构建 Dataset

PretrainDataset(预训练数据集)

在这里插入图片描述

import torch
import numpy as np
from torch.utils.data import Dataset

class PretrainDataset(Dataset):
    """
    预训练数据集:自回归语言建模
    目标:学习预测下一个token
    """
    def __init__(self, data_path: str, tokenizer, max_length: int = 512):
        super().__init__()
        self.tokenizer = tokenizer
        self.max_length = max_length
        self.padding_id = 0
        
        # 加载所有数据到内存
        with open(data_path, 'r', encoding='utf-8') as f:
            self.data = f.readlines()

    def __len__(self):
        return len(self.data)

    def __getitem__(self, index: int):
        # 解析数据
        sample = json.loads(self.data[index])
        text = f"{self.tokenizer.bos_token}{sample['text']}"
        
        # Tokenize
        input_ids = self.tokenizer(text, add_special_tokens=False)['input_ids']
        input_ids = input_ids[:self.max_length]
        
        # Padding
        seq_len = len(input_ids)
        padding_len = self.max_length - seq_len
        input_ids = input_ids + [self.padding_id] * padding_len
        
        # Loss mask:标记哪些位置需要计算损失
        loss_mask = [1] * seq_len + [0] * padding_len
        
        # 构造输入和目标
        # X: [BOS, T1, T2, ..., Tn-1]
        # Y: [T1, T2, T3, ..., Tn]
        X = np.array(input_ids[:-1], dtype=np.int64)
        Y = np.array(input_ids[1:], dtype=np.int64)
        loss_mask = np.array(loss_mask[1:], dtype=np.int64)
        
        return torch.from_numpy(X), torch.from_numpy(Y), torch.from_numpy(loss_mask)

数据流示意

原始: [BOS, T1, T2, T3, T4, PAD, PAD, PAD]
X:    [BOS, T1, T2, T3, T4, PAD, PAD]
Y:    [T1,  T2, T3, T4, PAD, PAD, PAD]
Mask: [1,   1,  1,  1,  0,   0,   0  ]

模型学习:给定X的前k个token,预测第k+1个token。

SFTDataset(监督微调数据集)

在这里插入图片描述

class SFTDataset(Dataset):
    """
    SFT数据集:多轮对话
    目标:只在assistant回复部分计算损失
    """
    def __init__(self, data_path: str, tokenizer, max_length: int = 512):
        super().__init__()
        self.tokenizer = tokenizer
        self.max_length = max_length
        self.padding_id = 0
        
        with open(data_path, 'r', encoding='utf-8') as f:
            self.data = f.readlines()

    def __len__(self):
        return len(self.data)

    def generate_loss_mask(self, input_ids: list) -> list:
        """
        生成loss mask:只在assistant回复部分为1
        
        识别模式:<|im_start|>assistant\n ... <|im_end|>
        """
        mask = [0] * len(input_ids)
        
        # <|im_start|>assistant\n 的token IDs(需根据实际tokenizer调整)
        assistant_start = [3, 1074, 537, 500, 203]  # 示例IDs
        start_len = len(assistant_start)
        
        i = 0
        while i <= len(input_ids) - start_len:
            # 检查是否匹配assistant开始标记
            if input_ids[i:i+start_len] == assistant_start:
                # 找到对应的结束标记 <|im_end|> (ID=4)
                for j in range(i + start_len, len(input_ids)):
                    if input_ids[j] == 4:  # <|im_end|>
                        # 标记assistant回复区间
                        for pos in range(i + start_len, j + 1):
                            mask[pos] = 1
                        break
                i += start_len
            else:
                i += 1
        
        return mask

    def __getitem__(self, index: int):
        # 解析对话
        messages = json.loads(self.data[index])
        
        # 应用聊天模板
        text = self.tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=False
        )
        
        # Tokenize
        input_ids = self.tokenizer(text, add_special_tokens=False)['input_ids']
        input_ids = input_ids[:self.max_length]
        
        # Padding
        seq_len = len(input_ids)
        padding_len = self.max_length - seq_len
        input_ids = input_ids + [self.padding_id] * padding_len
        
        # 生成loss mask
        loss_mask = self.generate_loss_mask(input_ids)
        
        # 构造输入输出
        X = np.array(input_ids[:-1], dtype=np.int64)
        Y = np.array(input_ids[1:], dtype=np.int64)
        loss_mask = np.array(loss_mask[1:], dtype=np.int64)
        
        return torch.from_numpy(X), torch.from_numpy(Y), torch.from_numpy(loss_mask)

SFT数据流示意

对话:
<|im_start|>user\n你好<|im_end|>
<|im_start|>assistant\n你好!<|im_end|>

Mask:
[0, 0, ..., 0, 1, 1, 1, 1, 1, 0]
 └─user─┘  └─assistant─┘

只有assistant的回复参与损失计算,这样模型只学习生成回复,不学习生成用户输入。

3.3 预训练流程

完整的预训练代码包含学习率调度、梯度累积、混合精度等技术。

import argparse
import time
import math
from torch import optim
from torch.utils.data import DataLoader
from contextlib import nullcontext

def get_lr(current_step: int, total_steps: int, warmup_steps: int, max_lr: float) -> float:
    """
    余弦退火学习率调度
    包含warmup阶段
    """
    min_lr = max_lr / 10
    
    # Warmup阶段:线性增长
    if current_step < warmup_steps:
        return max_lr * current_step / warmup_steps
    
    # 训练结束后:保持最小学习率
    if current_step > total_steps:
        return min_lr
    
    # 余弦退火阶段
    progress = (current_step - warmup_steps) / (total_steps - warmup_steps)
    cosine_decay = 0.5 * (1.0 + math.cos(math.pi * progress))
    return min_lr + (max_lr - min_lr) * cosine_decay

def train_epoch(epoch: int, model, train_loader, optimizer, scaler, args):
    """训练一个epoch"""
    model.train()
    start_time = time.time()
    
    for step, (X, Y, loss_mask) in enumerate(train_loader):
        # 数据迁移到GPU
        X = X.to(args.device)
        Y = Y.to(args.device)
        loss_mask = loss_mask.to(args.device)

        # 动态学习率
        current_step = epoch * len(train_loader) + step
        total_steps = args.epochs * len(train_loader)
        lr = get_lr(current_step, total_steps, args.warmup_steps, args.learning_rate)
        
        for param_group in optimizer.param_groups:
            param_group['lr'] = lr

        # 前向传播(混合精度)
        with torch.cuda.amp.autocast(enabled=(args.dtype != 'float32')):
            output = model(X, Y)
            loss = output.last_loss / args.accumulation_steps
            
            # 应用loss mask
            loss_mask = loss_mask.view(-1)
            loss = torch.sum(loss * loss_mask) / (loss_mask.sum() + 1e-8)

        # 反向传播
        scaler.scale(loss).backward()

        # 梯度累积:每accumulation_steps步更新一次
        if (step + 1) % args.accumulation_steps == 0:
            # 梯度裁剪
            scaler.unscale_(optimizer)
            torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)

            # 更新参数
            scaler.step(optimizer)
            scaler.update()
            optimizer.zero_grad(set_to_none=True)

        # 日志
        if step % args.log_interval == 0:
            elapsed = time.time() - start_time
            eta = elapsed / (step + 1) * (len(train_loader) - step) / 60
            
            print(f'Epoch {epoch+1}/{args.epochs} | '
                  f'Step {step}/{len(train_loader)} | '
                  f'Loss {loss.item() * args.accumulation_steps:.4f} | '
                  f'LR {lr:.2e} | '
                  f'ETA {eta:.1f}min')

        # 定期保存
        if (step + 1) % args.save_interval == 0:
            save_checkpoint(model, args, step)

def save_checkpoint(model, args, step):
    """保存模型检查点"""
    model.eval()
    state_dict = model.module.state_dict() if hasattr(model, 'module') else model.state_dict()
    
    checkpoint_path = f'{args.output_dir}/checkpoint_step{step}.pth'
    torch.save(state_dict, checkpoint_path)
    print(f'✓ 已保存检查点: {checkpoint_path}')
    model.train()

# 主训练循环
if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--data_path", type=str, required=True)
    parser.add_argument("--output_dir", type=str, default="./output")
    parser.add_argument("--epochs", type=int, default=1)
    parser.add_argument("--batch_size", type=int, default=64)
    parser.add_argument("--learning_rate", type=float, default=2e-4)
    parser.add_argument("--warmup_steps", type=int, default=1000)
    parser.add_argument("--accumulation_steps", type=int, default=8)
    parser.add_argument("--grad_clip", type=float, default=1.0)
    parser.add_argument("--dtype", type=str, default="bfloat16")
    parser.add_argument("--num_workers", type=int, default=8)
    parser.add_argument("--log_interval", type=int, default=100)
    parser.add_argument("--save_interval", type=int, default=5000)
    args = parser.parse_args()

    # 设置设备
    args.device = "cuda" if torch.cuda.is_available() else "cpu"
    
    # 模型配置
    config = ModelConfig(dim=1024, n_layers=18, vocab_size=6144)
    
    # 初始化
    torch.manual_seed(42)
    model = Transformer(config).to(args.device)
    tokenizer = AutoTokenizer.from_pretrained('./tokenizer_k/')
    
    # 多GPU
    if torch.cuda.device_count() > 1:
        model = torch.nn.DataParallel(model)
        print(f"使用 {torch.cuda.device_count()} 个GPU训练")
    
    # 数据集
    train_dataset = PretrainDataset(args.data_path, tokenizer)
    train_loader = DataLoader(
        train_dataset,
        batch_size=args.batch_size,
        shuffle=True,
        num_workers=args.num_workers,
        pin_memory=True
    )
    
    # 优化器和混合精度
    optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
    scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype != 'float32'))
    
    # 开始训练
    print(f"\n{'='*50}")
    print(f"开始预训练")
    print(f"{'='*50}")
    print(f"模型参数: {sum(p.numel() for p in model.parameters())/1e6:.1f}M")
    print(f"训练样本: {len(train_dataset)}")
    print(f"Batch size: {args.batch_size}")
    print(f"Effective batch size: {args.batch_size * args.accumulation_steps}")
    print(f"{'='*50}\n")
    
    for epoch in range(args.epochs):
        train_epoch(epoch, model, train_loader, optimizer, scaler, args)

运行命令

python train_pretrain.py \
    --data_path ./seq_monkey_processed.jsonl \
    --output_dir ./pretrain_output \
    --batch_size 32 \
    --accumulation_steps 8 \
    --epochs 1 \
    --dtype bfloat16

训练技巧

  1. 梯度累积:模拟更大的batch size

    • 有效batch = batch_size × accumulation_steps
    • 例:32 × 8 = 256
  2. 混合精度:使用bfloat16加速训练

    • 节省约50%显存
    • 提速约2-3倍
  3. 学习率调度:warmup + 余弦退火

    • Warmup避免训练初期梯度过大
    • 余弦退火帮助收敛
  4. 梯度裁剪:防止梯度爆炸

    • 将梯度范数限制在1.0以内

资源估算

配置显存预计时间
单卡(batch=4)~7GB500h+
4卡(batch=32)~24GB120h
8卡(batch=64)~48GB50h

3.4 SFT 微调

SFT代码与预训练类似,主要区别:

  1. 使用SFTDataset
  2. 加载预训练权重
  3. 学习率通常更小(1e-5到5e-5)
# 加载预训练权重
def load_pretrained(model, checkpoint_path):
    """加载预训练权重"""
    state_dict = torch.load(checkpoint_path, map_location='cpu')
    
    # 清理可能的前缀
    unwanted_prefix = '_orig_mod.'
    for k in list(state_dict.keys()):
        if k.startswith(unwanted_prefix):
            state_dict[k[len(unwanted_prefix):]] = state_dict.pop(k)
    
    model.load_state_dict(state_dict, strict=False)
    print(f"✓ 已加载预训练权重: {checkpoint_path}")

# SFT训练主循环
if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--pretrain_checkpoint", type=str, required=True)
    parser.add_argument("--data_path", type=str, required=True)
    parser.add_argument("--output_dir", type=str, default="./sft_output")
    parser.add_argument("--learning_rate", type=float, default=2e-5)
    # ... 其他参数同预训练 ...
    args = parser.parse_args()
    
    # 初始化模型
    config = ModelConfig(dim=1024, n_layers=18, vocab_size=6144)
    model = Transformer(config).to(args.device)
    
    # 加载预训练权重
    load_pretrained(model, args.pretrain_checkpoint)
    
    # 使用SFT数据集
    tokenizer = AutoTokenizer.from_pretrained('./tokenizer_k/')
    train_dataset = SFTDataset(args.data_path, tokenizer)
    train_loader = DataLoader(train_dataset, batch_size=args.batch_size, shuffle=True)
    
    # 优化器(注意学习率更小)
    optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
    
    # 训练
    for epoch in range(args.epochs):
        train_epoch(epoch, model, train_loader, optimizer, scaler, args)

运行命令

python train_sft.py \
    --pretrain_checkpoint ./pretrain_output/checkpoint_step50000.pth \
    --data_path ./belle_processed.jsonl \
    --output_dir ./sft_output \
    --learning_rate 2e-5 \
    --batch_size 32 \
    --epochs 1

SFT注意事项

  1. 学习率:SFT阶段学习率应比预训练小5-10倍,避免破坏预训练知识
  2. Epoch数:通常1-3个epoch即可,过多会过拟合
  3. 数据质量:SFT数据质量比数量更重要

3.5 模型推理与生成

训练完成后,我们可以使用模型进行文本生成。

import torch
from transformers import AutoTokenizer
from k_model import Transformer, ModelConfig

class TextGenerator:
    """文本生成器"""
    def __init__(
        self,
        checkpoint_path: str,
        tokenizer_path: str = './tokenizer_k/',
        device: str = None
    ):
        self.device = device or ('cuda' if torch.cuda.is_available() else 'cpu')
        
        # 加载tokenizer
        self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
        
        # 加载模型
        config = ModelConfig(dim=1024, n_layers=18, vocab_size=6144)
        self.model = Transformer(config)
        
        state_dict = torch.load(checkpoint_path, map_location=self.device)
        unwanted_prefix = '_orig_mod.'
        for k in list(state_dict.keys()):
            if k.startswith(unwanted_prefix):
                state_dict[k[len(unwanted_prefix):]] = state_dict.pop(k)
        
        self.model.load_state_dict(state_dict, strict=False)
        self.model.eval()
        self.model.to(self.device)
        
        # 统计参数
        num_params = sum(p.numel() for p in self.model.parameters())
        print(f"模型参数量: {num_params/1e6:.1f}M")

    def generate(
        self,
        prompt: str,
        max_new_tokens: int = 256,
        temperature: float = 0.7,
        top_k: int = 50,
        use_chat_template: bool = True
    ) -> str:
        """
        生成文本
        
        Args:
            prompt: 输入提示
            max_new_tokens: 最多生成token数
            temperature: 采样温度(0=贪婪,1=完全随机)
            top_k: Top-K采样
            use_chat_template: 是否使用聊天模板
            
        Returns:
            生成的文本
        """
        # 应用聊天模板
        if use_chat_template:
            messages = [
                {"role": "system", "content": "你是一个AI助手"},
                {"role": "user", "content": prompt}
            ]
            text = self.tokenizer.apply_chat_template(
                messages,
                tokenize=False,
                add_generation_prompt=True
            )
        else:
            text = prompt
        
        # 编码
        input_ids = self.tokenizer(text, return_tensors='pt')['input_ids']
        input_ids = input_ids.to(self.device)
        
        # 生成
        with torch.no_grad():
            output_ids = self.model.generate(
                input_ids,
                stop_id=self.tokenizer.eos_token_id,
                max_new_tokens=max_new_tokens,
                temperature=temperature,
                top_k=top_k
            )
        
        # 解码
        generated_text = self.tokenizer.decode(output_ids[0], skip_special_tokens=True)
        return generated_text

# 使用示例
if __name__ == "__main__":
    print("="*60)
    print("LLaMA2 文本生成演示")
    print("="*60)
    
    # 初始化生成器
    generator = TextGenerator(
        checkpoint_path='./sft_output/checkpoint_final.pth'
    )
    
    # 测试问题
    test_prompts = [
        "你好,请介绍一下你自己",
        "什么是深度学习?",
        "中国的首都是哪里?",
        "请用Python写一个冒泡排序",
        "1+1等于几?"
    ]
    
    for i, prompt in enumerate(test_prompts, 1):
        print(f"\n{'='*60}")
        print(f"问题 {i}: {prompt}")
        print(f"{'-'*60}")
        
        response = generator.generate(
            prompt,
            max_new_tokens=200,
            temperature=0.7,
            top_k=50
        )
        
        print(f"回答: {response}")
预期输出:
============================================================
LLaMA2 文本生成演示
============================================================
模型参数量: 370.5M

============================================================
问题 1: 你好,请介绍一下你自己
------------------------------------------------------------
回答: <|im_start|>system
你是一个AI助手<|im_end|>
<|im_start|>user
你好,请介绍一下你自己<|im_end|>
<|im_start|>assistant
你好!我是一个AI助手,很高兴认识你。我可以帮助你回答问题、提供信息、进行对话交流等。我的目标是为用户提供有用、准确的帮助。有什么我可以帮助你的吗?<|im_end|>

============================================================
问题 2: 什么是深度学习?
------------------------------------------------------------
回答: <|im_start|>system
你是一个AI助手<|im_end|>
<|im_start|>user
什么是深度学习?<|im_end|>
<|im_start|>assistant
深度学习是机器学习的一个分支,它使用多层神经网络来学习数据的表示。深度学习模型可以自动从原始数据中提取特征,而不需要人工特征工程。常见的深度学习模型包括卷积神经网络(CNN)、循环神经网络(RNN)和Transformer等。<|im_end|>

============================================================
问题 3: 中国的首都是哪里?
------------------------------------------------------------
回答: <|im_start|>system
你是一个AI助手<|im_end|>
<|im_start|>user
中国的首都是哪里?<|im_end|>
<|im_start|>assistant
中国的首都是北京。<|im_end|>

============================================================
问题 4: 请用Python写一个冒泡排序
------------------------------------------------------------
回答: <|im_start|>system
你是一个AI助手<|im_end|>
<|im_start|>user
请用Python写一个冒泡排序<|im_end|>
<|im_start|>assistant
好的,这是Python实现的冒泡排序:
```python
def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(0, n-i-1):
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
    return arr

# 测试
arr = [64, 34, 25, 12, 22, 11, 90]
print(bubble_sort(arr))
```<|im_end|>

============================================================
问题 5: 1+1等于几?
------------------------------------------------------------
回答: <|im_start|>system
你是一个AI助手<|im_end|>
<|im_start|>user
1+1等于几?<|im_end|>
<|im_start|>assistant
1+1等于2。<|im_end|>

采样策略对比

策略TemperatureTop-K特点
贪婪解码0.0-确定性,可能重复
标准采样1.0-多样性高,可能不连贯
温度采样0.7-平衡多样性和连贯性
Top-K0.750避免低概率词,更流畅

4. 总结与资源

恭喜你完成了从零实现LLaMA2大模型的完整旅程!让我们回顾一下核心要点。

4.1 核心技术总结

模型架构创新

  1. RMSNorm:去除均值中心化,提升7-10%计算效率
  2. RoPE:旋转位置编码,更好的长度外推能力
  3. GQA:分组查询注意力,显存节省40%,推理提速2倍
  4. SwiGLU:改进的激活函数,提升模型表达能力

训练技巧

  1. 混合精度训练:节省50%显存,提速2-3倍
  2. 梯度累积:模拟大batch训练
  3. 学习率调度:Warmup + 余弦退火
  4. 梯度裁剪:稳定训练过程

数据处理

  1. BPE Tokenizer:平衡词表大小和分词粒度
  2. 预训练vs SFT:自回归建模 vs 对话对齐
  3. Loss Masking:精确控制学习目标

4.2 性能指标

我们训练的模型达到:

指标数值
参数量215M
词表大小6,144
最大序列长度512
训练数据10B tokens (预训练) + 3.5M对话 (SFT)
训练时长46h (预训练) + 24h (SFT)
硬件8×4090

4.3 进阶方向

模型优化

  1. 量化:INT8/INT4量化降低部署成本
  2. 剪枝:移除冗余参数
  3. 知识蒸馏:从大模型学习到小模型
  4. LoRA微调:低秩适配器高效微调

能力增强

  1. 多模态:扩展到视觉、音频
  2. 长文本:扩展上下文长度到32k+
  3. 工具调用:集成外部工具和API
  4. 思维链:增强推理能力

部署优化

  1. ONNX导出:跨平台部署
  2. TensorRT加速:GPU推理优化
  3. 量化部署:移动端部署
  4. 分布式推理:模型并行

4.4 常见问题

Q: 显存不足怎么办?
A:

  • 减小batch_size(最低可到2)
  • 增大accumulation_steps(保持有效batch不变)
  • 使用梯度检查点(gradient checkpointing)
  • 使用更小的模型配置

Q: 训练loss不下降?
A:

  • 检查数据格式和loss_mask
  • 降低学习率
  • 增加warmup步数
  • 检查梯度范数(是否exploding/vanishing)

Q: 生成效果不好?
A:

  • 确保SFT数据质量
  • 调整采样参数(temperature, top_k)
  • 增加训练数据量
  • 尝试多轮对话微调

Q: 如何评估模型?
A:

  • 困惑度(Perplexity):衡量语言建模能力
  • BLEU/ROUGE:衡量生成质量
  • 人工评估:最终标准

4.5 资源链接

预训练模型下载

  • ModelScope: https://modelscope.cn/
  • HuggingFace: https://huggingface.co/

学习资源

  • Andrej Karpathy的视频教程
  • HuggingFace NLP课程
  • Stanford CS224N课程

开源项目

  • llama2.c: https://github.com/karpathy/llama2.c
  • minimind: https://github.com/jingyaogong/minimind
  • transformers: https://github.com/huggingface/transformers

4.6 致谢

本教程基于以下优秀开源项目:

  1. Meta的LLaMA2模型架构
  2. Andrej Karpathy的llama2.c项目
  3. HuggingFace的transformers库
  4. BelleGroup的中文对话数据集

感谢开源社区的无私贡献!


附录

A. 完整代码结构

project/
├── k_model.py              # 模型定义
├── dataset.py              # 数据集定义
├── train_tokenizer.py      # Tokenizer训练
├── train_pretrain.py       # 预训练脚本
├── train_sft.py            # SFT训练脚本
├── model_sample.py         # 推理脚本
├── tokenizer_k/            # Tokenizer文件
│   ├── tokenizer.json
│   ├── tokenizer_config.json
│   └── special_tokens_map.json
├── data/                   # 数据目录
│   ├── seq_monkey_processed.jsonl
│   └── belle_processed.jsonl
├── pretrain_output/        # 预训练输出
│   └── checkpoint_*.pth
└── sft_output/             # SFT输出
    └── checkpoint_*.pth

B. 环境配置

# 创建虚拟环境
conda create -n llm python=3.10
conda activate llm

# 安装PyTorch(根据CUDA版本选择)
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

# 安装其他依赖
pip install transformers tokenizers datasets
pip install numpy tqdm

# 可选:安装实验跟踪工具
pip install swanlab wandb

C. 调试技巧

# 1. 检查数据
dataset = PretrainDataset('data.jsonl', tokenizer)
X, Y, mask = dataset[0]
print(f"X shape: {X.shape}")
print(f"Y shape: {Y.shape}")
print(f"Mask sum: {mask.sum()}")  # 应该 > 0

# 2. 检查模型输出
model.eval()
with torch.no_grad():
    output = model(X.unsqueeze(0), Y.unsqueeze(0))
    print(f"Logits shape: {output.logits.shape}")
    print(f"Loss shape: {output.last_loss.shape if output.last_loss is not None else None}")

# 3. 检查梯度
for name, param in model.named_parameters():
    if param.grad is not None:
        print(f"{name}: grad_norm={param.grad.norm():.4f}")

# 4. 可视化注意力
# 在Attention.forward中添加:
# self.attention_weights = scores  # 保存注意力权重
# 然后可视化:
import matplotlib.pyplot as plt
plt.imshow(model.layers[0].attention.attention_weights[0, 0].cpu())
plt.colorbar()
plt.show()

最后的话

从零实现一个大模型是一段充满挑战但极具收获的旅程。你不仅学会了代码实现,更重要的是理解了大模型背后的原理和工程实践。

共勉:

  • 理论是基础,实践是关键
  • 遇到问题不要气馁,debug调试是学习的一部分
  • 保持好奇心,持续关注领域进展
  • 分享你的经验,帮助他人成长

祝你在AI的道路上越走越远!

参考文献

[1] Touvron, H., et al. (2023). LLaMA: Open and Efficient Foundation Language Models.

[2] Touvron, H., et al. (2023). LLaMA 2: Open Foundation and Fine-Tuned Chat Models.

[3] Vaswani, A., et al. (2017). Attention is All You Need.

[4] Su, J., et al. (2021). RoFormer: Enhanced Transformer with Rotary Position Embedding.

[5] Ainslie, J., et al. (2023). GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints.

[6] Shazeer, N. (2020). GLU Variants Improve Transformer.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁学文武

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值