☰
动手学大语言模型:从原理到LoRA微调与量化部署的实践指南
2026/9/29 15:12:08 网站建设 项目流程

简介:《动手学大语言模型》(Hands-On Large Language Models,又被称为“袋鼠书”)由 Jay Alammar 与 Maarten Grootendorst 合著,是一本以高度视觉化方式讲解大型语言模型的实践指南,适合正在入门或希望系统提升生成式 AI 能力的学生、研究者与工程技术人员。全书围绕模型的生成、表示与检索三大应用展开,详细讲解 Transformer 架构、标记器、语义搜索、RAG、文本与视觉嵌入融合等关键主题,并通过大量插图、时间线、经典论文引用和可运行代码实验室,把抽象概念转化为可操作的工程方法。配套下载的压缩包共 1 个 PDF 文件,大小约 21.37MB,PDF 格式便于跨设备阅读、检索重点章节和做批注。该资源已有 899 人学习下载;书中还包含多位业内专家的推荐语,从算法演进、理论严谨性与工程实用性三个维度提供了完整参考,既能辅助读者快速建立 LLM 知识框架,也能作为日常开发与学习的案头手册。

1. 袋鼠书不是来给你讲概念的:它把大语言模型拆成了能上手改的模块

书名里的 Hands-On 不是营销词。Jay Alammar 这本常被叫作“袋鼠书”的《动手学大语言模型》,从头到尾都在逼你做同一件事:把 Large Language Models 从“能聊天的黑匣子”拆成“能改代码的零件箱”。书里没有大段抄论文,而是用大量示意图和可运行 notebook 带你从 token 切分一路走到微调与评估。适合两类人:一类是已经调过 API、但不知道模型内部到底发生了什么的应用开发者,另一类是准备做本地部署大语言模型、却被显存和训练成本卡住的研究生。你不需要数学博士背景,但最好有 Python 基础。读完再回来看模型榜单,你会明显感觉到自己看问题的层级变了——不再问“哪个模型强”,而是问“这个模型在哪一层、用了什么策略变强”。

2. 动手前先还清三笔认知债:token、嵌入与注意力机制的工程视角

袋鼠书前面的章节讲得很慢,但慢得有道理。大语言模型的输入输出链路可以压缩成一句话:文本 → 切分 → 索引 → 嵌入 → 多层注意力变换 → 概率输出。任何一步理解偏了,后面调参都会变成玄学。

2.1 分词粒度决定中文效果:从 BPE 到 SentencePiece

Tokenizer 是模型真正“看到”的东西。英文按空格切词看着合理,中文按字切又会把“机器学习”拆成“机 / 器 / 学 / 习”四个独立片段,模型初期很难学到“机器学习”是一个稳定概念。常见做法是用字节级 BPE 或 Unigram 算法, SentencePiece 则是把语言无关的字节片段作为原子单位。

from transformers import AutoTokenizer tok = AutoTokenizer.from_pretrained("bert-base-chinese") text = "机器学习是人工智能的核心" encoded = tok(text, return_tensors="pt") print(encoded["input_ids"]) # 把 token id 还原成可见 token print(tok.convert_ids_to_tokens(encoded["input_ids"][0]))

这行代码能让你直观看到切分结果:中文 BERT 多半会把“机器学习”切成["机", "器", "学", "习"]四个 token,而一个用中文语料训练过的 BPE 词表可能切出["机器学习", "是", "人工智能", "的", "核心"]。后者对后续注意力计算更友好,因为语义单元更完整。实际项目里换 tokenizer 会直接改变模型能处理的最大序列长度和训练收敛速度,这不是一个能“顺手换掉”的组件。

2.2 嵌入层与位置编码:模型不是真的“读懂”文本

嵌入层做的事情是把 token id 映射成稠密向量。这个向量初始是随机的,经过预训练之后,语义相近的词在向量空间里的距离会拉近。“国王 - 男性 + 女性 ≈ 女王”这种经典关系不是手工标出来的,而是语言建模目标逼出来的副产品。

但嵌入层解决不了词序问题。Transformer 结构本身没有循环,对位置不敏感,于是模型设计者加入了位置编码。这本书里对绝对位置编码和旋转位置编码的区别讲得比较清楚:绝对位置打一个固定标签,RoPE 则通过旋转矩阵把位置信息揉进注意力计算里,这也是 LLaMA 等模型能外推更长上下文的关键设计。

import torch import torch.nn.functional as F def rope(x, seq_len, dim, base=10000): theta = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim)) pos = torch.arange(seq_len).float() angles = torch.outer(pos, theta) # [seq_len, dim/2] cos = torch.cos(angles) sin = torch.sin(angles) # 简化演示:仅对偶数维度做旋转 return x, cos, sin

需要理解的是,RoPE 并不是什么高深数学,它是在做向量旋转:把两个 token 的注意力分数乘上它们位置差对应的旋转角,让“距离越近”的 token 在位置维度上天然更相似。写代码时最常踩的坑是维度索引没配对,导致 rotate 之后 cos 和 sin 形状对不上。调试这类问题时别去抠线性代数,而是把每个中间矩阵的 shape 打印出来,一步一步对。

2.3 注意力机制是信息路由,不是搜索功能

很多人把注意力理解成“模型在文本里找关键词”,这个直觉在单层时勉强成立,但堆到几十层之后就完全失效了。注意力层做的事情是让每个 token 根据当前表示去“查询”其他 token,再把有价值的信息加权汇总回来。它不是搜索,更像是一个由数据学出来的路由系统——不同头负责不同关系,有的头盯语法依赖,有的头盯共指消解。

书里建议的实践方式是:拿一个预训练模型,用transformers库把特定层的 attention weight 抽出来可视化。

from transformers import AutoModel, AutoTokenizer model = AutoModel.from_pretrained("bert-base-uncased", output_attentions=True) tok = AutoTokenizer.from_pretrained("bert-base-uncased") inputs = tok("The cat sat on the mat because it was tired", return_tensors="pt") outputs = model(**inputs) att = outputs.attentions # 每一层都返回注意力矩阵 # att[layer] 的 shape: [batch, num_heads, seq_len, seq_len]

参数说明:output_attentions=True是这里的关键开关,不打开的话注意力矩阵根本不会返回。看到[batch, num_heads, seq_len, seq_len]这个结构后,你可以自己验证一个现象:高层的注意力远比底层稀疏,因为模型已经学会只路由关键信息。理解这一点后,你再去看模型压缩、蒸馏、稀疏注意力这些改进方案,就不会觉得它们是在“砍模型”,而是在尊重注意力结构本身。

3. 从零跑到第一次文本生成:环境选型与最小复现路径

原理看明白了就得动手。这一章解决的是“代码在我电脑上怎么跑起来”的问题,顺便回应一个高频诉求:本地部署大语言模型到底需要什么配置。

3.1 显存不是唯一瓶颈:理解算力约束下的资源搭配

很多人以为跑大模型就是拼显存,其实显存决定的是“能不能放下”,内存和带宽决定的是“每秒能生成多少字”。CPU 推理不是不能做,但词表投影层和注意力计算对内存带宽的需求非常高,普通台式机跑 7B 模型生成速度可能只有每秒 2 到 4 个 token,体验很痛苦。所以在动手之前,先想清楚你的目标:是验证代码逻辑、跑通训练流程,还是真的要做本地服务。

书里的代码大多数基于 PyTorch 和 HuggingFace Transformers,对 GPU 的要求不算苛刻。一张 8GB 显存的卡就能跑通 7B 模型的推理和 LoRA 微调。真正吃显存的是优化器状态和梯度,而不是模型权重本身。这也是我建议在读这本书时先跑小模型、再尝试大模型的原因——同样的代码逻辑,1.5B 和 7B 之间差了可能 5 倍显存占用,但你能学到的东西没有缩水。

3.2 最小推理脚本:从加载到文本生成

先把环境装好。以 Python 3.10 为例,创建虚拟环境后核心依赖只有两个库,其余按书里 notebook 逐步补充即可:

python -m venv llm-book source llm-book/bin/activate pip install transformers torch --index-url https://download.pytorch.org/whl/cu121

安装完成后,用一个 1B 左右的小模型验证链路:

from transformers import AutoModelForCausalLM, AutoTokenizer model_name = "Qwen/Qwen2.5-1.5B-Instruct" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained( model_name, torch_dtype="auto", device_map="auto" ) prompt = "用一句话解释什么是注意力机制。" messages = [{"role": "user", "content": prompt}] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, return_tensors="pt" ) outputs = model.generate( inputs.to(model.device), max_new_tokens=128, do_sample=True, temperature=0.7, top_p=0.9 ) print(tokenizer.decode(outputs[0], skip_special_tokens=True))

代码逻辑分三段:tokenizer 负责把对话模板转成 token id 序列,模型做前向推理,最后 decode 把概率分布还原成文本。需要留意device_map="auto",它能自动把权重均匀分配到多张卡或 CPU 与 GPU 之间,单卡机器上不要乱改。do_sample=True开启随机采样,配合temperature=0.7让输出有一定多样性;如果做的是确定性任务,比如分类或者抽取,建议关掉采样,否则结果每次不一样。

3.3 在 CPU 上也能跑:量化推理的三种选择

没有独显不代表不能动手。袋鼠书里提到的量化思路在本地部署大语言模型时是必须掌握的技能:把 FP16 权重压缩成 INT8 或 INT4,显存占用直接砍半甚至砍到四分之一。

from transformers import BitsAndBytesConfig bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype="float16", bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True ) model = AutoModelForCausalLM.from_pretrained( model_name, quantization_config=bnb_config, device_map="auto" )

load_in_4bit=True关键参数;nf4是 4bit 量化里质量最好的数据格式;use_double_quant会对量化常数再做一次量化,能额外省几个百分点显存。代价是推理变慢一点,但 7B 模型在 6GB 显存上跑起来,对学习项目来说是值得的。

4. 微调与对齐:真正花钱的部分怎么省着做

推理跑通只是开始。书中后半部分进入训练环节:从文本生成模型到指令跟随模型,中间隔着微调和偏好对齐。这一章是最容易劝退读者的地方,也是含金量最高的部分。

4.1 全量微调不是默认选项:冻结权重和参数高效微调

全量微调 7B 模型需要至少 60GB 显存,这个量级已经超过了大多数个人开发者的硬件上限。所以 LoRA 类方法成为主流。LoRA 的思路非常简单:冻结原始权重,在每一层并行插入一个低秩矩阵,训练时只更新这个小矩阵。

from peft import LoraConfig, get_peft_model from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-1.5B-Instruct") lora_cfg = LoraConfig( r=8, lora_alpha=16, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM" ) model = get_peft_model(model, lora_cfg) model.print_trainable_parameters() # 通常只有 0.5% 上下

r=8是低秩矩阵的秩,lora_alpha=16是缩放系数。这两个值之间有个经验配比:alpha 一般是 r 的 1 到 2 倍,过大的 alpha 会让微调后的输出偏离原模型分布。dropout=0.05防止小数据量过拟合。这种配置下,1.5B 参数模型的可训练参数可能不到 10M,一张 8GB 显卡就能完成微调,这正符合“算力约束下提升大语言模型能力的资源配置建模”的核心思路:不是资源越堆越好,而是把参数集中在最有效的低秩子空间里更新。

4.2 评估要趁早:避免只盯着 Loss 曲线

很多人在训练时只关注 loss,loss 降到 1.2 就觉得成了。可实际上,语言模型的 loss 和下游真实任务质量之间不是单调关系。书里强调要自己做一个小规模评测集,用精确匹配、ROUGE 或者人工抽测来验证。推荐的做法是训练过程中每隔固定步数做一次生成采样,把 prompt 和模型输出打印到日志里。

if step % 500 == 0: model.eval() with torch.no_grad(): sample = tokenizer( "把这句话翻成英文:模型正在学习。", return_tensors="pt" ).to(device) output = model.generate(**sample, max_new_tokens=64) print(f"step {step}: {tokenizer.decode(output[0])}") model.train()

这段代码的作用是建立“主观质量”与训练步数的对应关系。你很快会发现:某些步骤输出中文语病减少了,但回答变得非常啰嗦;再过几百步,输出又开始退化。这是因为微调数据里存在分布噪音,模型在某个局部区间过拟合了。提前建立采样机制,能够在模型彻底跑偏前及时保存 checkpoint,这就是训练流程里的后悔药。

4.3 偏好对齐:从 RLHF 到 DPO 的更轻路径

书中谈到 RLHF 时点明了一个现实:完整的 RLHF 需要训练奖励模型,加上 PPO 四个模型交互,显存和代码量都很大。容易上手的替代方案是 DPO——直接偏好优化。DPO 不需要单独训练奖励模型,只需要准备包含“偏好回答”和“拒绝回答”的数据对,就能用分类损失让模型学会选择更好的回答。

from trl import DPOTrainer, DPOConfig dpo_config = DPOConfig( output_dir="./dpo_model", per_device_train_batch_size=1, learning_rate=5e-6, max_length=512, max_prompt_length=256, num_train_epochs=3 ) dpo_trainer = DPOTrainer( model=model, train_dataset=dataset, tokenizer=tokenizer, args=dpo_config )

注意learning_rate=5e-6这个值比 SFT 低一个数量级。偏好对齐是在微调基础上的精调,学习率过高很容易让模型忘记原本学到的基础能力。DPO 不生成偏好样本,只负责学习偏好,数据质量直接决定对齐结果。自己构造数据对的时候,宁可每对只有一句话的差异,也不要写成长篇大论——差异太大,模型不知道该对齐哪一个维度。

5. 避坑:复现袋鼠书代码时最容易翻车的五个隐性错误

边读边跑的最大障碍不是概念难懂,而是代码能跑但结果不对,或者昨天能跑今天报错。这一章的素材来自大量实际踩坑记录。

5.1 现象:加载模型时爆显存

原因:默认加载 FP32 权重,7B 模型光权重就要 28GB,加上激活值随手突破显存上限。

解决:加载时立刻指定torch_dtype=torch.float16,或者直接用量化配置。如果你已经用了 FP16 还爆显存,再看max_memory参数,手动把部分层分配到 CPU:

from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained( model_name, torch_dtype="auto", device_map="auto", max_memory={0: "6GiB", "cpu": "16GiB"} )

5.2 现象:同一个 prompt 每次生成结果都不同

原因:do_sample=True时模型按概率采样,输出天然有随机性。

解决:设置seed或加temperature=0。如果你在写评测案例,把do_sample=False改成贪心解码。我在写复现实验时固定了torch.manual_seed(42)加torch.backends.cudnn.deterministic=True,这样才能保证两次实验结果可比较。

5.3 现象:检查 tokenizer 分词时,结果和书上截图不一样

原因:HuggingFace 的 tokenizer 文件更新过,或者你加载的是不同 checkpoint 附带的不同词表。

解决:不要手动对比 token id,对比 tokenizer 的vocab_size和词表 hash。书里的版本未必和你下载时一致,这是正常现象,重点看分词逻辑而不是具体 id 值。

5.4 现象:模型微调后 loss 一直在降,但生成文本全是重复话

原因:学习率太高,加上数据中包含大量重复模板,模型陷入局部最优,输出收敛到高频率 token 的循环。

解决:降低学习率到 2e-5 或 1e-5,检查训练数据里重复度——如果同一句话出现超过 10 次,先做去重或采样。

from collections import Counter counter = Counter(dataset["text"]) dups = [k for k, v in counter.items() if v > 10] print(f"重复样本数: {len(dups)}")

5.5 现象:评估分数比论文报告低很多

原因:评估时没有使用和论文完全一致的 prompt 模板和生成参数。比如论文用max_new_tokens=128,你用了 256,长输出中多余部分拉了平均分。

解决:先看论文或仓库里eval脚本的设置,逐项对齐。把评估脚本的参数单独抽成配置项,不要藏在代码里。评估脚本和训练脚本一样重要,需要纳入版本管理。

6. 最后一章:把书读完后的第一个自测项目:造一个能复述知识点的问答工具

读完书不代表掌握,动手设计一个小项目才算闭环。我推荐的自测路径是:选一个你熟悉的垂直领域(比如你正在用的开发框架),准备 500 到 1000 条问答样本,然后依次完成四件事:数据清洗、LoRA 微调、推理效果测试、与基线模型做对比。这四个动作会强制你用上整本书的核心知识。

验证方法有一个很实用的技巧:拿一个完全没见过的问题去问微调前后的模型。

base = "使用PyTorch实现一个带dropout的两层全连接网络" print("微调前:", generate(base)) print("微调后:", generate_lora(base))

对比输出时不要只看“是否提到 dropout”,而要看“概念解释的顺序是否合理”“代码是否真的能跑”。如果微调后的输出能把 torch 的nn.Module复现得基本正确,说明模型真的学到了领域结构,而不是在背答案。

我的习惯是给这个项目设置一个硬性交付物:一个包含“训练日志、评测样本、配置文件”的文件夹,提交到 Git 仓库。这样以后再看这本书,随时可以回放那段时间的学习状态。大语言模型的学习路径很容易飘——一会儿想做智能体,一会儿想做检索增强,但读完一本动手型书籍后做出的落地作品,才是抵御焦虑的锚点。别贪多,先做一个只需要跑在一张消费级显卡上的小项目。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询