大模型入门到微调实战:上海交大教程+LoRA部署全路径
2026/9/20 18:28:36 网站建设 项目流程

大模型这个领域,最让人头疼的从来不是"要不要学",而是"从哪下手"。我见过太多人抱着《Attention Is All You Need》啃了三天,最后卡在多头注意力的维度变换上;也见过有人环境配了一周,连第一个token都没生成出来。上海交大那套《动手学大模型》教程能在GitHub上霸榜,本质上就是因为它把这条"从懵到能跑"的路给铺平了——不是那种只讲概念的PPT教程,而是每一章都让你真的动手敲代码、真的把模型跑起来。这篇我就结合这套教程的脉络,加上我自己踩过的坑,把大模型从入门到微调实战的完整路径拆开讲一遍。不管你是刚接触Transformer的学生,还是想把手头业务模型做领域适配的工程师,下面这些内容应该都能让你少走几天弯路。

1. 为什么这套教程值得花时间啃

1.1 它解决的是"知识断层"问题

大部分大模型学习资料有个通病:要么纯讲原理,公式推到天上去,但你看完不知道代码怎么写;要么纯讲调包,from transformers import AutoModel一贴,跑是跑通了,但模型里面发生了什么你完全没概念。《动手学大模型》这套教程的定位恰好卡在中间——它假设你有基本的Python和深度学习基础,但不假设你懂大模型。从Transformer的编码器结构开始,一层一层用手写代码的方式拆给你看,然后再过渡到用HuggingFace生态做实际任务。

这个顺序很关键。我自己的经验是,如果你跳过手写Transformer直接去调包,后面遇到微调不收敛、显存爆炸、推理结果诡异这些问题时,你根本没有排查能力。因为你不知道模型内部在干什么,就只能瞎试参数。而手写一遍之后,你至少知道hidden_sizenum_headsnum_layers这些参数各自控制什么,出了问题能定位到具体环节。

1.2 教程的章节脉络与学习节奏

这套教程的内容编排大致遵循这样一条线:Transformer原理与手写实现 → 预训练语言模型基础 → 大模型微调技术 → 模型部署与推理优化 → 实战项目。每个阶段都有配套的代码和练习,不是光看就完事。

我建议的学习节奏是这样的:Transformer手写部分不要赶,花三到五天,确保你能不看参考代码从零写出一个能跑通前向传播的Transformer。微调部分重点吃透LoRA,因为这是目前性价比最高的微调方式,单卡就能玩。部署部分至少跑通一次vLLM或Ollama的本地部署,感受一下推理框架对性能的影响。

注意:不要试图一次性把所有章节都看完再动手。这套教程的正确用法是"看一章、敲一章、跑一章",每章的代码都要自己复现一遍,哪怕照着抄也要抄一遍。

1.3 适合什么样的人学

这套教程不是零基础入门。如果你连Python的类和继承都用不熟,或者不知道什么是梯度下降,建议先去补基础。但如果你满足以下任意一条,这套教程会非常适合你:

  • 有深度学习基础,用过PyTorch写过简单的神经网络
  • 做过后端或算法开发,想转型到大模型方向
  • 手头有业务数据,想把开源大模型微调成领域专用模型
  • 已经会调OpenAI的API,但想理解底层原理并做本地部署

2. Transformer手写:绕不过去的第一道坎

2.1 从零实现一个Transformer编码器

Transformer的编码器部分,核心就是三个东西:多头自注意力、前馈网络、残差连接加层归一化。听起来简单,但手写的时候细节特别多。我当初写第一版的时候,光是维度对齐就调了两个小时。

先说输入的处理。假设你的输入是一个batch的token序列,经过embedding层之后得到形状为(batch_size, seq_len, d_model)的张量。这个d_model就是整个模型的隐藏维度,比如512或768。然后你需要加上位置编码,因为Transformer本身没有序列顺序的概念,位置信息全靠位置编码注入。

位置编码的实现有两种常见方式:一种是固定的正弦余弦编码,一种是可学习的位置嵌入。教程里两种都讲了,我建议先用正弦余弦版本,因为它不需要训练参数,而且能泛化到比训练时更长的序列。

import torch import torch.nn as nn import math class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=5000): super().__init__() pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0) self.register_buffer('pe', pe) def forward(self, x): return x + self.pe[:, :x.size(1), :]

这段代码里有个容易忽略的点:register_buffer。用这个方法注册的pe不会被当作模型参数更新,但会跟着模型一起保存和加载,而且会自动移到GPU上。如果你直接用普通属性存,模型搬到GPU的时候这个张量还在CPU上,就会报设备不匹配的错。

2.2 多头注意力:维度变换的坑最多

多头注意力是整个Transformer里最容易写错的部分。核心逻辑是:把输入投影到Q、K、V三个空间,然后分成多个头分别计算注意力,最后拼接起来再投影一次。

维度变换的流程是这样的:输入(batch, seq_len, d_model)经过线性层得到Q、K、V,形状都是(batch, seq_len, d_model)。然后reshape成(batch, seq_len, num_heads, head_dim),再transpose成(batch, num_heads, seq_len, head_dim)。这里head_dim = d_model // num_heads

class MultiHeadAttention(nn.Module): def __init__(self, d_model, num_heads): super().__init__() assert d_model % num_heads == 0 self.d_model = d_model self.num_heads = num_heads self.head_dim = d_model // num_heads self.q_linear = nn.Linear(d_model, d_model) self.k_linear = nn.Linear(d_model, d_model) self.v_linear = nn.Linear(d_model, d_model) self.out_linear = nn.Linear(d_model, d_model) def forward(self, x, mask=None): batch_size, seq_len, _ = x.shape q = self.q_linear(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) k = self.k_linear(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) v = self.v_linear(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim) if mask is not None: scores = scores.masked_fill(mask == 0, float('-inf')) attn = torch.softmax(scores, dim=-1) out = torch.matmul(attn, v) out = out.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model) return self.out_linear(out)

这里有几个实战中总结的要点。第一,math.sqrt(self.head_dim)这个缩放因子不能省,否则softmax之后的梯度会非常小,训练不动。第二,mask的处理要注意形状,通常是(batch, 1, 1, seq_len)或者(batch, 1, seq_len, seq_len),要能广播到scores的形状上。第三,transpose之后一定要.contiguous()view,否则会报内存不连续的错。

2.3 残差连接与层归一化的顺序问题

原始Transformer论文里用的是Post-LN,也就是先做注意力或前馈,再加残差,最后层归一化。但后来的实践发现Pre-LN(先归一化再进子层)训练更稳定,尤其是模型深了之后。现在大部分开源大模型用的都是Pre-LN或者RMSNorm。

如果你只是学习原理,按论文的Post-LN写就行。但如果要实际训练,建议直接上Pre-LN。这个细节在教程里可能不会特别强调,但你跑通之后做对比实验就能感受到差异。

3. 微调实战:LoRA为什么成了标配

3.1 全量微调 vs LoRA:显存账要算清楚

全量微调一个7B参数的模型,光是模型权重就要占14GB显存(FP16),加上优化器状态、梯度、激活值,没有个五六张A100根本跑不动。这对个人开发者和小团队来说基本不可行。

LoRA的思路很巧妙:我不动你原来的权重,我在旁边加两个小矩阵,训练的时候只更新这两个小矩阵。假设原始权重是W,形状(d, k),LoRA加上B @ A,其中A的形状是(r, k)B的形状是(d, r)r远小于dk。这样可训练参数就从d*k降到了r*(d+k)

举个例子,一个d=4096, k=4096的权重矩阵,全量微调要训练1600多万个参数。如果r=8,LoRA只需要训练8*(4096+4096)=65536个参数,差了240多倍。显存占用和训练时间都大幅下降。

3.2 LoRA微调Qwen模型的完整流程

以Qwen系列模型为例,用LoRA做指令微调的流程大致如下。首先安装依赖:

pip install transformers peft datasets accelerate bitsandbytes

然后加载模型和tokenizer。这里用4bit量化加载可以进一步省显存:

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig from peft import LoraConfig, get_peft_model, TaskType bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.float16 ) model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen2.5-7B-Instruct", quantization_config=bnb_config, device_map="auto" ) tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")

配置LoRA参数:

lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=8, lora_alpha=32, lora_dropout=0.1, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"] ) model = get_peft_model(model, lora_config) model.print_trainable_parameters()

target_modules的选择很关键。对于Qwen这类模型,注意力层的q_projk_projv_projo_proj是标配,有些实践还会加上前馈层的gate_projup_projdown_proj。加得越多,可训练参数越多,效果可能更好但显存也更高。我一般先用四个注意力投影层跑一版,效果不够再加。

lora_alphar的关系是alpha/r作为缩放系数。经验值是alpha设为r的两倍或四倍。r=8, alpha=32是比较稳的组合。

3.3 数据准备:格式比数量重要

LoRA微调的数据格式,最常见的是指令-回答对。每条数据包含instructioninput(可选)、output三部分。关键是你要把数据转成模型能理解的prompt格式。

以Qwen的chat template为例:

def format_data(sample): messages = [ {"role": "system", "content": "你是一个专业的助手。"}, {"role": "user", "content": sample["instruction"] + sample.get("input", "")}, {"role": "assistant", "content": sample["output"]} ] return tokenizer.apply_chat_template(messages, tokenize=False)

数据量方面,LoRA微调通常几百到几千条就能看到明显效果。但质量比数量重要得多。我试过用500条精心构造的数据,效果比5000条爬来的脏数据好很多。重点检查:回答是否准确、格式是否统一、有没有重复样本、有没有明显的错误标注。

3.4 训练参数设置与常见报错

训练用Trainer或者SFTTrainer都可以。关键参数:

参数推荐值说明
learning_rate1e-4 ~ 2e-4LoRA的学习率比全量微调大
batch_size1 ~ 4受显存限制,配合梯度累积
gradient_accumulation_steps4 ~ 16等效增大batch
num_epochs2 ~ 3LoRA容易过拟合,不宜多
warmup_ratio0.03 ~ 0.1预热比例
lr_schedulercosine余弦衰减比较稳
max_seq_length512 ~ 2048根据数据长度定

常见报错里,CUDA out of memory最常见。解决办法:减小batch_size、降低max_seq_length、开启gradient_checkpointing、用4bit量化加载。另一个常见问题是loss不下降,通常是学习率太小或者数据格式不对,先检查数据有没有正确tokenize。

4. 本地部署:把模型跑起来才算数

4.1 vLLM vs Ollama:场景决定选择

模型微调完之后,你得把它部署起来才能用。目前主流的本地部署方案有两个方向:vLLM和Ollama。

vLLM的核心优势是吞吐量。它用了PagedAttention技术,把KV Cache分成固定大小的块来管理,显存利用率比朴素实现高很多。如果你要做一个多人使用的API服务,vLLM基本是首选。启动命令很简单:

python -m vllm.entrypoints.openai.api_server \ --model /path/to/your/model \ --dtype float16 \ --max-model-len 4096 \ --gpu-memory-utilization 0.9

启动之后就是一个兼容OpenAI API格式的服务,可以直接用openai的Python库调用。

Ollama的定位更偏向个人使用和快速体验。它把模型下载、量化、运行都封装好了,一条命令就能跑:

ollama run qwen2.5:7b

Ollama适合快速验证和本地开发,但并发能力弱,不适合生产环境。vLLM适合做服务,但配置稍微复杂一些。我的建议是:开发阶段用Ollama快速迭代,上线用vLLM。

4.2 显存不够怎么办:量化方案对比

本地部署最大的门槛是显存。一个7B模型FP16需要约14GB显存,加上KV Cache,实际需要16GB以上。如果你的显卡只有8GB或12GB,就需要量化。

常见的量化方案:

  • GPTQ:训练后量化,4bit下7B模型约需4GB显存,质量损失较小
  • AWQ:激活感知量化,比GPTQ略好,但支持的模型少一些
  • GGUF:llama.cpp用的格式,CPU也能跑,但速度慢
  • bitsandbytes:在线量化,加载时量化,方便但速度稍慢

我实测下来,4bit的GPTQ或AWQ量化模型,在大多数任务上跟FP16的差距很小,但显存占用只有三分之一左右。对于个人开发者来说,量化基本是必选项。

4.3 推理参数调优:temperature和top_p怎么设

模型跑起来之后,生成质量很大程度上取决于推理参数。几个关键参数:

  • temperature:控制随机性。0.1以下适合确定性任务(如代码生成、数学题),0.7-1.0适合创意写作
  • top_p:核采样,只从累积概率前p的token中采样。通常设0.9-0.95
  • top_k:只从概率最高的k个token中采样。一般设40-50
  • repetition_penalty:重复惩罚,1.0表示不惩罚,1.1-1.2可以抑制重复

实际使用中,我一般temperature和top_p二选一调,不要同时大改。做知识问答时temperature=0.1, top_p=0.9比较稳;做文案生成时temperature=0.8, top_p=0.95更有创意。

5. 那些教程不会告诉你的踩坑经验

5.1 环境配置的隐形陷阱

大模型开发的环境配置,坑比想象中多。CUDA版本、PyTorch版本、transformers版本、peft版本之间的兼容性是个矩阵,稍不注意就冲突。

我的建议是用conda建独立环境,然后按这个顺序装:先确定CUDA版本(用nvidia-smi看),然后装对应CUDA版本的PyTorch,再装transformers和peft。不要用pip install一把梭,版本冲突了很难排查。

另一个坑是bitsandbytes在Windows上的支持问题。如果你用Windows,建议直接上WSL2,原生Windows下bitsandbytes的安装经常出问题。

5.2 微调效果不好的排查思路

LoRA微调之后效果不理想,按这个顺序排查:

第一,看loss曲线。如果loss完全不降,大概率是数据格式问题或者学习率太小。如果loss降了但验证集效果差,是过拟合,减少epoch或增大dropout。

第二,检查数据质量。随机抽几条训练数据,用tokenizer编码后decode出来看看,确认格式正确、没有截断。

第三,确认target_modules是否正确。不同模型的层命名不一样,用model.named_modules()打印出来确认。

第四,试试调大rlora_alpha。有时候模型容量不够,增大秩能改善。

5.3 模型合并与导出

LoRA训练完之后,推理时有两种方式:一种是加载基座模型再加载LoRA适配器,另一种是把LoRA权重合并到基座模型里导出成一个完整模型。

合并的代码:

from peft import PeftModel base_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B-Instruct") model = PeftModel.from_pretrained(base_model, "./lora_output") merged_model = model.merge_and_unload() merged_model.save_pretrained("./merged_model")

合并的好处是推理时不需要额外加载适配器,部署更简单。但合并后的模型文件跟基座一样大,而且不能再单独调整LoRA权重。如果要做多套LoRA切换,就不要合并。

5.4 从学习到产出的最短路径

最后说一个我自己的体会。大模型这个领域,看再多教程不如跑通一个完整流程。我的建议是给自己定一个具体目标,比如"用LoRA微调一个模型,让它能回答我所在领域的专业问题",然后围绕这个目标去学。遇到什么不会就查什么,比按部就班从头学到尾效率高得多。

具体路径可以这样:第一周手写Transformer并跑通;第二周跑通一个LoRA微调的demo;第三周准备自己的数据做一次真实微调;第四周部署成API服务。四周下来,你对大模型的理解会超过看三个月视频的人。

这套上海交大的教程最大的价值,就是给了你一条可以跟着走的路径。但路径归路径,路还是得自己走。代码要自己敲,bug要自己调,模型要自己跑。这个过程里积累的经验,才是真正属于你的东西。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询