一字一诗
LESSON 09 / 26

让模型知道字的先后

这一课完成什么

让模型能够区分一个字位于句子中的哪里。汉字表示表回答“是什么字”;位置表示表提供“处于第几个位置”的信息。本项目采用可学习的位置表,保持计算直接可见。

字告诉模型“是谁”,位置告诉它“在哪”

把字向量与位置向量逐格相加。交换两个字,会改变每个位置收到的组合;模型因此有条件区分先后顺序。

字告诉模型“是谁”,位置告诉它“在哪”
用文字逐步读这张图
  1. 字向量:“春” → [0.2, 0.4];同字先查同一行
  2. 位置 0:[0.1, −0.2];第一格位置
  3. 合在一起:[0.3, 0.2];逐项相加
  4. 位置 1:[−0.3, 0.2];换一个位置
  5. 同字放这里:[−0.1, 0.6];组合随位置变化
  6. 连成序列:字与位置共同决定;送入注意力的数字
为什么只提供一袋字不够?

一袋字丢掉了顺序,而同样几个字按不同顺序组合可以有不同意思。

两张表相加

若第一个位置是“明”,汉字向量假设为 [0.2,0.4],位置 0 的向量为 [0.1,-0.2],最终得到 [0.3,0.2]。第二个位置同样出现“明”,汉字向量相同,但加上位置 1 的另一行,因此输入表示不同。

python
positions = torch.arange(length, device=tokens.device)
hidden = self.characters(tokens) + self.positions(positions)

汉字部分尺寸 [B,T,C],位置部分尺寸 [T,C]。位置向量会在每条练习题中共用,对应相同的序列位置,这种自动适配外层维度叫广播。相加后尺寸仍为 [B,T,C],不会变为两倍宽度。

运行并交换顺序

bash
./poet lesson 9
.venv/bin/python lessons/09/experiment.py

实验比较编号 [1,2,3][2,1,3]。它们使用相同的三个位置编号,但前两个位置的汉字改变,最终每一行输入也随之改变。你可以打印第 3 行,确认没有变化。

如果把所有字向量简单平均,交换顺序后平均值会相同,会丢失大量顺序信息。完整 GPT 还有限制只能看前文的结构,因此它的顺序信息不只来自位置表;这一课关注显式位置表示如何工作,不把所有顺序效应归结于单一部件。

前文长度是什么限制

本项目位置表默认准备 64 行,表示一次模型调用最多处理 64 个位置,包括特殊标记、关键词和标点。四句五言诗有 20 个汉字、4 个标点;四句七言有 28 个汉字、4 个标点,还要留出条件区空间。

这个长度不同于“模型记住了多少首诗”。训练中见过许多作品被浓缩进参数,而某次生成只能直接利用本次传入的有限前文。增加前文长度还会扩大注意力分数表;长度翻倍,完整两两匹配表的格子数量会变为四倍。

验收与排错

小练习与答案

上下文长度为 64,表示宽度为 128,位置表有多少参数?为什么不必乘以批数量?

查看答案

共有 64×128=8192 项参数。所有诗共用这张位置表,批数量只是同时处理多少条题目,不创建独立参数。输入序列里关键词也占位置,因此正文第一个字的位置编号不一定总是同一个值。

动手补全一小段

先运行上面的完整实验,再复制本课起始文件为自己的练习。starter 有意留空 solve 函数;补全后运行文件,底部检查会告诉你是否符合本课要求。卡住时打开参考答案,比较每一步。

bash
cp lessons/09/starter.py lessons/09/my_exercise.py
# 编辑 my_exercise.py 中的 solve 函数,然后运行:
.venv/bin/python lessons/09/my_exercise.py
# 对照完整答案:
.venv/bin/python lessons/09/solution.py

下载起始代码 · 下载参考答案

展开本课补全练习的完整参考答案
python
"""第 09 课补全练习:加入位置信息。修改 solve,保持下方检查不变。"""
import math, io, argparse
import torch
from torch import nn
from torch.nn import functional as F
torch.set_num_threads(2)
torch.manual_seed(26)

def solve(words, positions):
    return words + positions

x=torch.zeros(2,3,4);p=torch.arange(12).view(3,4)
y=solve(x,p)
assert y.shape==(2,3,4) and torch.equal(y[0],y[1])
print("本课补全练习通过。")
展开本课完整、可独立运行的实验代码
python
"""本课独立实验;在仓库根目录执行 .venv/bin/python lessons/09/experiment.py。"""
import sys, json, math
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
import torch
from torch import nn
from torch.nn import functional as F
from poetry_gpt.common import ROOT, DEFAULT_DATA, read_json, read_jsonl
from poetry_gpt.model import ModelConfig, PoetryGPT, CausalAttention, Block
from poetry_gpt.data import Tokenizer, SPECIAL, clean_record, keywords_for
from poetry_gpt.labs import show
torch.set_num_threads(2)
torch.manual_seed(26)

e, p = (nn.Embedding(6, 4), nn.Embedding(4, 4))
a = torch.tensor([1, 2, 3])
b = torch.tensor([2, 1, 3])
pos = torch.arange(3)
show('原序列第一个位置', (e(a) + p(pos))[0].tolist())
show('换序列第一个位置', (e(b) + p(pos))[0].tolist())
show('位置编号', pos.tolist())
下载本课实验