前面我们一直在"用"模型:下载、运行、调用、挑选。从这一章开始,我们要亲手造一个。
造之前,先把一个现成的拆开看看。
这一节用上一章下载好的 Qwen3.5-0.8B 做标本,看清一个语言模型由哪几部分组成。
学完你能做到:说清楚一句话从输入到输出经过了哪些步骤,知道参数量是怎么算出来的,并且理解"训练"到底在调整什么。
📌 这一章起的代码只有 Python 版。
训练要用 PyTorch,它和 Transformers、分词器这些库都是 Python 的。
需要的环境是 Python 3.11 以上,再加 PyTorch 和 transformers 两个包。
进入上一章建好的 llm-course 目录,运行:
Window命令
uv pip install torch --torch-backend=auto --reinstall-package torch--torch-backend=auto 让 uv 根据你的 CUDA 驱动选择合适的版本。torch 上一步已经装过了,不加 --reinstall-package torch 就不会换。
Linux,Mac 命令
uv pip install torch transformersLinux 上这样装出来的 PyTorch 已经带 CUDA,有 NVIDIA 显卡就能直接用;
Mac 上自带苹果芯片的加速。
装完确认一下:
uv run python -c "import torch, transformers; print(torch.__version__, transformers.__version__, 'cuda' if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu')"输出的前两项是版本号,最后一项是后面训练要用的设备:
NVIDIA 显卡是 cuda,苹果芯片是 mps。
有 NVIDIA 显卡却显示 cpu,说明 PyTorch 还是 CPU 版,版本号后面通常带 +cpu。这样也能跑,只是默认配置要训练十几个小时。Windows 上照着上面那条 GPU 命令重装一遍再确认。
还是显示 cpu 的话,先运行 nvidia-smi 看驱动是否正常。驱动没问题,就到 PyTorch 官网选好系统和 CUDA 版本,照它给的命令装:把开头的 pip3 换成 uv pip,结尾加上 --reinstall-package torch。
没有 NVIDIA 显卡、也不是苹果芯片的电脑,显示 cpu 是正常的。第三节准备了缩小版,一样能走完流程。
这一章的命令都写成 uv run python。uv run 会用当前目录下的 .venv,不用先激活虚拟环境,换一个终端也不受影响。
只写 python 的话,没激活时用的是系统里的 Python,那里没有这两个包,会报 ModuleNotFoundError。
一、一句话是怎么被接下去的
语言模型只会做一件事:给定前面的内容,算出下一个 token 是词表里每一个 token 的概率。 我们看到它写出一整段话,其实是这件事重复了几百次。
"今天天气很好,我们一起去"
│
① 分词器:切成 token,换成编号
│ [今天, 天气, 很好, ,, 我们, 一起, 去] → [97375, 101618, ...]
│
② 词嵌入:每个编号换成一串数字(Qwen3.5-0.8B 里是 1024 个)
│
③ 一层层 Transformer:每层都把这些数字加工一遍(Qwen3.5-0.8B 有 24 层)
│
④ 输出层:得到词表里 24.8 万个 token 各自作为"下一个"的分数,换算成概率
│ 公园 11%、吃 7%、海边 4.5% ……
│
⑤ 选一个,比如"公园",接到末尾,回到第 ① 步这一节按顺序把这几块拆开。
第 ② ③ ④ 步都要用到一大堆固定不变的数字:查表用的大表、每一层加工用的参数、输出层打分用的参数。这些数字就是模型的权重。
二、分词器:模型的词典
模型只认数字,不认文字。分词器负责两件事:把文字切成一个个 token,再查词表把每个 token 换成编号;反过来,把模型输出的编号换回文字。
Qwen3.5 的分词器把"今天天气不错,我们去公园散步吧。"这 16 个字符切成了 9 个 token,本节最后的程序会真的跑出这个结果:
['今天', '天气', '不错', ',', '我们去', '公园', '散步', '吧', '。']可以看到,一个 token 可能是一个字、一个词,甚至"我们去"这样的短语。
最极端的情况下,一个字会被拆成几个字节,所以任何文字都能编码,不会出现"不认识的字"。
为什么不直接一个字一个 token?
因为 token 越少,模型要处理的序列就越短,算得越快,同样长度的上下文也能装下更多内容。
这也是为什么 API 按 token 计费、上下文长度按 token 计算。
分词器的核心是一张词表,Qwen3.5 的词表有 24.8 万个 token。
词表是怎么来的、该开多大,下一节训练自己的分词器时会细讲。
词表里还有一些特殊 token,比如 <|im_start|>、<|im_end|>,用来标记一条消息的开始和结束。它们不对应任何文字,教模型对话时会用到。
三、词嵌入:把编号换成一串数字
分词之后,每个 token 编号先查一张大表,换成一串数字,这一步叫词嵌入。
Qwen3.5-0.8B 里每个 token 换成 1024 个数字,可以理解成这个 token 在 1024 个维度上的"特征"。

这张大表叫词嵌入矩阵:词表里有多少个 token,它就有多少行,每行 1024 个数字。
Qwen3.5-0.8B 的这张表是 248320 行 × 1024 列。
编号在这里就是"行号"。分词器说"公园"是 99192 号,模型就去第 99192 行,把这一行的 1024 个数字整行取出来,代替"公园"往下传。
那这 1024 个数字是什么意思?可以把它想成一张游戏角色卡。
角色卡上用几项分数描述一个角色:力量 8、速度 3、智力 6……每一项就是一个"维度"。
词嵌入也是给每个 token 打分,只不过一共打 1024 项。
打分的好处是,意思相近的词,分数也相近。
假如只用"是不是动物""能不能吃"两项打分,就能把词画在一张图上:

"猫""狗""兔子"挤在一起,"苹果""香蕉""西瓜"挤在一起,"汽车"离它们都很远。有了这样的数字,后面的计算才能看出哪些词意思相近。
为什么不直接拿编号去算?因为编号只是排队的顺序,就像学号:99192 号和 99193 号挨着,不代表这两个词意思接近。
要说明的是,图里的"是不是动物""能不能吃"是为了好懂编出来的。真实模型的 1024 项没有名字,也不是人填的:一开始全是随机数,训练时和其他权重一起被慢慢调出来,最后每一项代表什么,连研究者也说不太清。
还有一点:同一个 token 不管出现在哪句话里,查表拿到的永远是同一行。"苹果"到底是水果还是手机品牌,查表这一步分不出来,要靠下面的注意力结合上下文去分辨。
四、Transformer 层:一层层加工
取出来的这些数字,接下来依次经过 24 层 Transformer。每一层都由注意力和前馈网络两部分组成,下面一个一个看。
1. 注意力:回头看前文
一个词是什么意思,往往取决于上下文。
"苹果"在"吃了一个苹果"和"苹果发布了新手机"里意思完全不同。
注意力机制就是让每个位置回头看前面所有位置,决定该从哪些地方获取信息。
它为每个 token 算出三样东西:
| 名字 | 比方 | 作用 |
|---|---|---|
| Query(查询) | 你在图书馆想找什么书 | "我需要什么信息" |
| Key(键) | 每本书书脊上的标签 | "我这里有什么信息" |
| Value(值) | 书的内容 | "真正被取走的信息" |
每个位置拿自己的 Query 和前面所有位置的 Key 比一比,越匹配的,就从它的 Value 里取越多信息。
注意,只能看前面,不能看后面,因为生成时后面的内容还不存在。
用两句话具体看一下:

第一句里,"苹果"回头一看,发现"吃了"和自己最匹配,就从"吃了"那里取走最多的信息,于是它的数字往"水果"的意思靠。
第二句里,最匹配的是"手机",它的数字就往"手机品牌"的意思靠。
图里的百分比就是"取多少":最匹配的位置占 60%,其他位置分剩下的。把各个位置的 Value 按这个比例混在一起,就是这一步取到的信息,它会被加进"苹果"自己的那串数字里。
也因为只能看前面,"苹果发布了新手机"里的"苹果"排在最前面,自己还看不到后面的"手机"。要等写到"手机"时,"手机"回头看见"苹果",整句话的意思才拼起来。
注意力还牵出几个常听到的词:KV Cache、头、GQA 和位置编码。上一章读模型卡时,我们按 KV Cache 公式算过显存,这里看看它们是从哪来的。