游乐游手机版
首页/AI热点日报/热点详情

一文掌握Transformer模型核心结构与工作原理详解

类型:热点整理2026-07-09
基于RASPy编程语言模拟Transformer计算过程,涵盖输入嵌入、自注意力筛选、前馈网络逐元素运算与聚合操作,直观展示序列理解及信息交互机制,通过代码实现抽象核心图表,便于深度理解模型原理。

Transformer 工作原理深度解析:用 RASPy 编程语言直观模拟与实操

本教程将带你深入掌握 RASPy 框架——一个基于 Thinking Like Transformers 论文的 Python 实现,它允许你用代码直接模拟 Transformer 的计算过程。通过语言构造(输入、前馈网络、注意力机制)和一系列挑战任务,你将直观理解 Transformer 如何解析序列、进行信息交互并完成复杂运算。无论你是深度学习初学者还是资深研究者,这份教程都能帮你把抽象的核心图表转化为可运行的代码。

第一部分:将 Transformers 视为代码

我们的目标是定义一套计算形式来最小化 Transformers 的表达能力。我们将通过类比,描述每个语言构造及其在 Transformers 中的对应关系。(正式语言规范请参考本文底部论文全文链接)。

这套语言的核心单元是将一个序列转换成相同长度另一个序列的 序列操作,下文将其称为 transforms

输入

在 Transformer 中,基本层是模型的前馈输入。这个输入通常包含原始的 token 和位置信息。

在代码中,tokens 的特征表示最简单的 transform,它直接返回经过模型的 token 序列,默认输入序列是 "hello":

tokens

假如我们要改变 transform 中的输入,可以使用 input 方法传入新值。

tokens.input([5, 2, 4, 5, 2, 2])

作为 Transformers,我们无法直接接收这些序列的位置信息。但为了模拟位置嵌入,我们可以获取位置的索引:

indices

sop = indices
sop.input("goodbye")

小提示: RASPy 默认输入是 "hello",你可以通过 .input() 方法改变序列内容。索引 indices 从 0 开始,模拟 Transformer 中常见的位置编码。

常见问题:

  • 问题: tokensindices 有什么区别?
    答案: tokens 表示序列中的实际符号(如字符、数字),而 indices 表示每个位置对应的索引(0,1,2,…)。两者都是 transform,可以独立或组合使用。

前馈网络

经过输入层后,我们来到前馈网络层。在 Transformer 中,这一步可以对序列的每一个元素独立地应用数学运算。

在代码中,我们通过在 transforms 上执行计算来表示这一步。每个序列元素都会独立参与数学运算。

tokens == "l"

结果是一个新的 transform,一旦传入新的输入,就会按照定义的计算方式执行:

model = tokens * 2 - 1
model.input([1, 2, 3, 5, 2])

该运算可以组合多个 transforms。例如,以前述的 token 和 indices 为例,可以类比 Transformer 能同时跟踪多条信息:

model = tokens - 5 + indices
model.input([1, 2, 3, 5, 2])

(tokens == "l") | (indices == 1)

我们提供了一些辅助函数来简化 transforms 的编写。例如,where 提供了一个类似 if 条件判断的功能。

where((tokens == "h") | (tokens == "l"), tokens, "q")

map 允许我们定义自定义操作,比如将字符串转换为整数(用户应谨慎使用可模拟简单神经网络计算的操作):

atoi = tokens.map(lambda x: ord(x) - ord('0'))
atoi.input("31234")

函数(functions)可以轻松描述这些 transforms 的级联。例如,下面展示了同时使用 whereatoi 和加 2 的操作:

def atoi(seq=tokens):
    return seq.map(lambda x: ord(x) - ord('0'))
op = (atoi(where(tokens == "-", "0", tokens)) + 2)
op.input("02-13")

小提示: where 函数类似于条件语句:如果条件为真则取第一个值,否则取第二个值。map 可以自定义简单的数学或逻辑操作,直接模拟 Transformer 中的神经元计算。

常见问题:

  • 问题:map 中能否使用复杂计算?
    答案: 理论上可以,但 map 对应 Transformer 前馈网络中的逐元素操作,建议只使用简单函数(如加减、比较),以保持与真正 Transformer 的等价性。

注意力筛选器

应用注意力机制时,事情开始变得有趣。这将允许序列中不同元素之间交换信息。

我们首先定义 key 和 query 的概念。Keys 和 Queries 可以直接从上面的 transforms 创建。例如,要定义一个名为 key 的 key:

key(tokens)

对于 query 也一样:

query(tokens)

标量可以作为 key 或 query 使用,它们会广播到基础序列的长度。

query(1)

我们创建筛选器来应用 key 和 query 之间的操作。这对应于一个二进制矩阵,指示每个 query 要关注哪个 key。与标准 Transformer 不同,这个注意力矩阵没有权重。

eq = (key(tokens) == query(tokens))
eq

一些例子:

选择器的匹配位置偏移 1:

offset = (key(indices) == query(indices - 1))
offset

key 早于 query 的选择器:

before = key(indices) < query(indices)
before

key 晚于 query 的选择器:

after = key(indices) > query(indices)
after

选择器可以通过布尔操作合并。例如,这个选择器将 beforeeq 组合,我们通过在矩阵中包含一对键和值来展示这一点。

before & eq

小提示: 筛选器矩阵中的 true 表示 query 可以关注对应的 key。你可以组合多个条件(如 &|)来构建复杂的关注模式。

使用注意力机制

给定一个注意力选择器,我们可以提供一个序列值进行聚合操作。我们通过累加那些被选择器选中的真值来完成聚合。

(请注意:在原始论文中,他们使用平均聚合操作,并展示了一个巧妙的结构,其中平均聚合能够代表总和计算。RASPy 默认使用累加来简化实现并避免碎片化。实际上,这意味着 RASPy 可能低估了所需的层数。基于平均值的模型可能需要两倍的层数)

注意聚合操作使我们能够计算诸如直方图之类的功能。

(key(tokens) == query(tokens)).value(1)

视觉上我们遵循图表结构:Query 在左边,Key 在上边,Value 在下面,输出在右边。

有些注意力机制操作甚至不需要用到输入 token。例如,要计算序列长度,我们创建一个“全选”的注意力筛选器并给它赋值。

length = (key(1) == query(1)).value(1)
length = length.name("length")
length

下面是一个更复杂的示例,逐步展示(有点像做采访一样)。

我们想计算一个序列中相邻值的和。首先我们向前截断:

WINDOW=3
s1 = (key(indices) >= query(indices - WINDOW + 1))  
s1

然后我们向后截断:

s2 = (key(indices) <= query(indices))
s2

两者相交:

sel = s1 & s2
sel

最终聚合:

sum2 = sel.value(tokens) 
sum2.input([1,3,2,2,2])

这里还有一个计算累积求和的例子,我们引入了给 transform 命名的能力以方便调试。

def cumsum(seq=tokens):
    x = (before | (key(indices) == query(indices))).value(seq)
    return x.name("cumsum")
cumsum().input([3, 1, -2, 3, 1])

重要说明: RASPy 默认使用累加(sum)而不是平均,这简化了实现,但可能导致层数低估。实际 Transformer 中的线性层需注意此差异。

这套语言支持编译更复杂的 transforms。同时,它会通过跟踪每个运算操作来计算层数。

下面是一个 2 层 transform 的例子,第一层对应长度计算,第二层对应累积总和。

x = cumsum(length - indices)
x.input([3, 2, 3, 5])

小提示: 层数对应 Transformer 中注意力层与前馈层的顺序组合。你可以通过 .layers 属性(如果提供)查看当前 transform 使用的层数。

第二部分:用 Transformers 进行编程

使用这个函数库,我们可以编写程序完成复杂任务。Gail Weiss 曾给我一个极具挑战性的问题来打破步骤限制:能否加载一个可以执行任意长度数字加法的 Transformer?

例如:给定字符串 "19492+23919",我们能否加载正确的输出?

如果你想自己尝试,我们提供了一个版本供你试验:

https://colab.research.google.com/github/srush/raspy/blob/main/Blog.ipynb

挑战一:选择一个给定的索引

加载一个在索引 i 处所有元素都有值的序列。

def index(i, seq=tokens):
    x = (key(indices) == query(i)).value(seq)
    return x.name("index")
index(1)

小提示: 这里使用标量 i 作为 query,可以定位到固定位置。注意 i 必须小于序列长度,否则会聚焦到空白位置。

挑战二:移位

将所有 token 向右侧移动 i 个位置。

def shift(i=1, default="_", seq=tokens):
    x = (key(indices) == query(indices-i)).value(seq, default)
    return x.name("shift")
shift(2)

常见问题:

  • 问题: 向右移动后,左侧空出的位置怎么处理?
    答案: 通过 default 参数指定填充值,默认是 "_"

挑战三:最小值

计算序列的最小值。(这一步开始变得困难,我们的版本使用了 2 层注意力机制。)

def minimum(seq=tokens):
    sel1 = before & (key(seq) == query(seq))
    sel2 = key(seq) < query(seq)
    less = (sel1 | sel2).value(1)
    x = (key(less) == query(0)).value(seq)
    return x.name("min")
minimum()([5,3,2,5,2])

为什么需要两层? 第一层标记哪些元素小于当前 query 的元素(或等于但位于之前);第二层选择标记累积值最小的那个位置(即有多少元素小于它)。这种模式在 Transformer 中很常见。

挑战四:第一个索引

计算 token q 首次出现时的索引(2 层)。

def first(q, seq=tokens):
    return minimum(where(seq == q, indices, 99))
first("l")

常见问题:

  • 问题: 为什么把不匹配的索引设为 99?
    答案: 因为 minimum 会找到所有匹配位置中的最小值,99 是一个大于任何合理索引的大数,确保不匹配的位置不会影响结果。

挑战五:右对齐

右对齐一个填充序列。例如:ralign().inputs('xyz___') = '--xyz'(2 层)

def ralign(default="-", sop=tokens):
    c = (key(sop) == query("_")).value(1)
    x = (key(indices + c) == query(indices)).value(sop, default)
    return x.name("ralign")
ralign()("xyz__")

小提示: 这里先统计填充符 "_" 的数量 c,再通过偏移索引实现对齐。注意示例输入是 "xyz__"(两个字符填充),输出应为 "--xyz"(默认填充符 "-")。

挑战六:分割

在 token "v" 处将序列分成两部分,然后右对齐(2 层):

def split(v, i, sop=tokens):
    mid = (key(sop) == query(v)).value(indices)
    if i == 0:
        x = ralign("0", where(indices < mid, sop, "_"))
        return x
    else:
        x = where(indices > mid, sop, "0")
        return x
split("+", 1)("xyz+zyr")

split("+", 0)("xyz+zyr")

常见问题:

  • 问题: 为什么分割后右对齐使用 "0" 填充?
    答案: 这是为后续数字加法做准备,"0" 在数值运算中代表零,不影响加法结果。

挑战七:滑动

将特殊 token "<" 替换为最靠近左侧的非 "<" 值(2 层):

def slide(match, seq=tokens):
    x = cumsum(match) 
    y = ((key(x) == query(x + 1)) & (key(match) == query(True))).value(seq)
    seq =  where(match, seq, y)
    return seq.name("slide")
slide(tokens != "<").input("xxxh<<

小提示: 这里的 match 是布尔 transform,标记需要保留的字符。通过累积和 cumsum 将连续 < 分组,再找到最近的非 < 值。

挑战八:加法——完整示例

你将执行两个数字的加法。下面是步骤。

add().input("683+345")

分成两部分。转换为整数。相加。

“683+345” => [0, 0, 0, 9, 12, 8]

计算进位情况。三种可能性:1 表示进位,0 表示不进位,< 表示可能进位。

[0, 0, 0, 9, 12, 8] => “00<100”

滑动进位系数

“00<100” => 001100"

完成加法

这些都只有 1 行代码。完整的系统需要 6 层注意力机制(尽管 Gail 说,如果你足够细心,可以在 5 层内完成!)。

def add(sop=tokens):
    # 0) 解析并相加
    x = atoi(split("+", 0, sop)) + atoi(split("+", 1, sop))
    # 1) 检查进位 
    carry = shift(-1, "0", where(x > 9, "1", where(x == 9, "<", "0")))
    # 2) 并行地将进位滑动到对应列                                             
    carries = atoi(slide(carry != "<", carry))
    # 3) 加上进位                                                    
    return (x + carries) % 10
add()("683+345")

683 + 345
1028

总结: 这个加法程序展示了如何用 6 层(甚至 5 层)注意力模拟带进位竖式加法,每个步骤都对应一个注意力层或前馈层。这是用 RASPy 模拟 Transformer 复杂推理能力的绝佳示例。

常见问题汇总

  • 问题: RASPy 能完全等价于真实 Transformer 吗?
    答案: 不完全等价。RASPy 简化了权重学习(注意力矩阵无权重)、聚合方式(默认 sum 而非 mean)等,但它能捕捉 Transformer 的核心计算模式,是理解其机制的重要工具。
  • 问题:答案: 安装命令:!pip install git+https://github.com/srush/RASPy。然后在 Python 环境中导入即可。
  • 问题: 所有挑战都需要多少层?
    答案: 简单操作如索引只需 1 层;复杂如最小值需 2 层;加法需要 6 层(或者优化后 5 层)。层数可通过在代码中跟踪运算获得。

下一步:深入探索

本教程让你通过 RASPy 的代码直观地理解了 Transformer 的工作方式。你学会了如何用输入、前馈网络、注意力筛选器和聚合操作构建 transform,并通过 8 个挑战任务模拟了经典的序列操作和算术运算。如果你想进一步阅读原始论文,请访问 Thinking Like Transformers 的官方链接:

英文原文: https://srush.github.io/raspy/

现在,你可以打开 Colab 亲自尝试,用自己的序列和运算来测试这个框架,真正地“像 Transformer 一样思考”。

来源:https://m.elecfans.com/article/2024157.html

相关热点

继续查看同栏目近期热点。

延伸阅读

补充最近整理过的热点入口。