AI 数学课程 · 第 5 课,共 180

面向 AI 的张量索引:读取样本、词元与特征

学习读取张量下标,将批次-词元-特征地址转换为 NumPy,并发现 AI 代码中看似有效的轴错误。

分享这篇文章

一个 AI 程序读取 X[1, 2, 0] 并返回 0.8。附近的表达式 X[1, 0, 2] 返回 0.3。两个地址都有效,但只有一个表示“第二个示例中的最后一个词元的第一个特征”。

差异在于索引:索引是用来定位有序结构中某个值的位置。索引是很小的记号,但它决定了一次计算实际使用的是哪个示例、词元、特征、像素或模型输出。因此,错误的索引可能产生一个看似可信的数字,而不是有帮助的错误提示。

学完本课后,你将能够读取带索引的样本和坐标,跟踪批次-词元-特征张量地址,将数学下标转换为 NumPy 表达式,并发现一个数字合法但轴含义错误的地址。本课为第 4 课中的数据集集合增加了顺序。官方的 Mathematics for Machine Learning 配套网站把这套记号放在从数值数据通向课程后续使用的向量、矩阵和模型的路径上。

索引回答“哪个位置?”

假设一个有序数据集包含三个示例:

D=(x1,x2,x3).D=(x_1,x_2,x_3).

x2x_2 中的下标表示“选择位置 2 的示例”。它并不表示该示例的值是 2。如果第二个示例是数对 x2=(1.7,0.4)x_2=(1.7,-0.4),那么 x2x_2 指的是整个数对。

再增加一个下标,就可以选择该示例内部的一个坐标:

x2,1=1.7andx2,2=0.4.x_{2,1}=1.7 \qquad\text{and}\qquad x_{2,2}=-0.4.

从左到右读取 x2,1x_{2,1}:先选择示例 2,再选择坐标 1。索引组成一个地址;存储在该地址中的数字才是值。

这也把位置与身份区分开来。把一个样本从位置 2 移到位置 7,会改变它的索引,即使它的稳定 ID、图像像素和标签都不变。索引是有用的地址,而不是现实世界对象的永久名称。

每个下标选择一个轴

是数组组织数据的一个方向。表格有行轴和列轴。在这里,张量是一个多维数组:它可以有这两个轴,也可以为批次和词元等概念增加更多轴。

考虑一个激活张量

XRB×T×F,X\in\mathbb{R}^{B\times T\times F},

其中:

  • BB 是批次中的示例数量;
  • TT 是每个示例的词元位置数量;
  • FF 是每个词元存储的特征数量。

记号 RB×T×F\mathbb{R}^{B\times T\times F} 表示每个条目都是实数,并且张量的形状是 B×T×FB\times T\times F。一个标量条目是

Xb,t,f,X_{b,t,f},

它表示按这个顺序选择批次位置 bb、词元位置 tt 和特征位置 ff

对于形状为 2×3×42\times3\times4 的张量,采用从 1 开始的数学约定时,可以有

1b2,1t3,1f4.1\le b\le2, \qquad 1\le t\le3, \qquad 1\le f\le4.

形状提供每个索引的范围,轴名称提供含义。两者缺一不可。

一个地址:完整地计算到一个值

使用下面这个两批次张量。每个批次项包含三行词元,每个词元有四个特征值。

沿着地址 X2,3,1 查找:先选择批次项 2,再选择词元 3,最后选择特征 1。选中的单元格中的值为 0.8。
批次项 1 张量网格 批次项 1 包含三行词元和四列特征。批次项 1特征索引 →词元索引 →f1f2f3f4词元 10.20.8-0.10.4词元 21.1-0.30.50.0词元 30.70.60.2-0.4 批次项 2 张量网格 批次项 2 包含三行词元和四列特征。已选中词元 3、特征 1,其值为 0.8。批次项 2特征索引 →词元索引 →f1f2f3f4词元 1-0.20.90.30.1词元 20.4-0.71.20.5词元 30.8 ★0.1-0.61.5★ X₂,₃,₁ = 0.8
批次项 1:词元行与特征列
词元特征 1特征 2特征 3特征 4
10.20.8-0.10.4
21.1-0.30.50.0
30.70.60.2-0.4
批次项 2:词元行与特征列
词元特征 1特征 2特征 3特征 4
1-0.20.90.30.1
20.4-0.71.20.5
30.80.1-0.61.5

这种逐步读取比记住字母顺序更重要。另一个项目可能把词元存储在批次之前,或者对图像使用高度、宽度和通道轴。代码和文档必须说明这份契约。

NumPy 从零开始

许多数学文本从 1 开始标记位置。Python 和 NumPy 使用从零开始的索引,因此第一个位置是 0。NumPy 的官方索引指南也确认,每个维度使用一个整数就能选择一个元素,并且大小为 dd 的轴上的有效非负索引满足 0i<d0\le i<d

目标位置从 1 开始的数学索引从零开始的 NumPy 索引
批次项 2b=2b=2b = 1
词元 3t=3t=3t = 2
特征 1f=1f=1f = 0
完整地址X2,3,1X_{2,3,1}X[1, 2, 0]

下面是完整张量以及几个经过验证的选择:

import numpy as np

X = np.array([
    [[ 0.2,  0.8, -0.1,  0.4],
     [ 1.1, -0.3,  0.5,  0.0],
     [ 0.7,  0.6,  0.2, -0.4]],
    [[-0.2,  0.9,  0.3,  0.1],
     [ 0.4, -0.7,  1.2,  0.5],
     [ 0.8,  0.1, -0.6,  1.5]],
])

print("shape:", X.shape)
print("one activation:", X[1, 2, 0])
print("last token of example 2:", X[1, 2])
print("feature 1 of every last token:", X[:, 2, 0])
shape: (2, 3, 4)
one activation: 0.8
last token of example 2: [ 0.8  0.1 -0.6  1.5]
feature 1 of every last token: [0.7 0.8]

X[:, 2, 0] 中的冒号表示“保留该轴上的每个位置”。第一个轴保持不变,而词元和特征获得具体索引,因此结果包含两个批次项各自的一个值。

shape 属性报告 (2, 3, 4),但这个元组本身并不表示“批次、词元、特征”。这些名称来自程序的数据契约。

合法地址仍可能表达错误的问题

回到开头的表达式:

X[1, 2, 0]  # 0.8: batch 2, token 3, feature 1
X[1, 0, 2]  # 0.3: batch 2, token 1, feature 3

两个地址都符合形状。NumPy 不知道第二行交换了词元和特征的含义。这是一个语义索引错误:程序取回了一个真实条目,却不是计算所要的条目。

有用的防护措施包括在张量旁写出轴契约,在边界处检查形状,以及为索引变量赋予描述性名称:

batch_index = 1
token_index = 2
feature_index = 0

activation = X[batch_index, token_index, feature_index]
assert X.shape == (2, 3, 4)
assert activation == 0.8

断言检查了这个示例。描述性变量保留了得出该地址的推理过程。

范围错误比无声的轴错误更安全

对于从零开始的形状 (2, 3, 4),最后一个非负地址是 X[1, 2, 3]X[2, 0, 0] 无效,因为批次索引 2 会从只有两个条目的轴中请求第三个项。NumPy 会抛出 IndexError,而不是返回一个凭空生成的值。

负索引是 Python 的约定:-1 选择某个轴的最后一个位置,因此 X[-1, -1, -1] 是读取 X[1, 2, 3] 的另一种方式。这是有效代码,但不应被无声地转换成从 1 开始的数学下标。只要这种区别重要,就要说明所采用的约定。

下一课会在求和中使用这些地址。一旦 xix_i 清楚地表示第 ii 个示例,而 i\ell_i 表示它的损失,诸如 ii\sum_i\ell_i 的符号就会成为访问每个带索引损失并合并这些值的简洁指令。

检查你的理解

问题 1

如果 x₄ = (2.5, −1.0, 0.3),x₄ 和 x₄,₂ 分别指什么?

显示分步解答

先读取下标的数量。

  1. x4x_4 有一个索引,因此选择位置 4 的整个示例。
  2. 给定示例是三坐标值 (2.5,1.0,0.3)(2.5,-1.0,0.3)
  3. x4,2x_{4,2} 增加了一个坐标索引,因此选择该示例中的坐标 2。

因此,

x4=(2.5,1.0,0.3)andx4,2=1.0.x_4=(2.5,-1.0,0.3) \qquad\text{and}\qquad x_{4,2}=-1.0.

下标 4 是一个地址,而不是存储的值。

问题 2

用图示计算 X₂,₂,₃。展示每个轴的选择。

显示分步解答

按照已声明的批次-词元-特征顺序跟随这些索引。

  1. b=2b=2 选择批次项 2。
  2. t=2t=2 选择其中的第二行词元:(0.4,0.7,1.2,0.5)(0.4,-0.7,1.2,0.5)
  3. f=3f=3 选择该行的第三个坐标。

所以,

X2,2,3=1.2.X_{2,2,3}=1.2.

在词元 2 之前选择特征 3 会改变问题,因为轴是有顺序的。

问题 3

将从 1 开始的数学地址 X₂,₃,₄ 转换为从零开始的 NumPy 地址,并找出其值。

显示分步解答

从所声明的从 1 开始的约定转换到 NumPy 时,将每个位置减去 1:

(2,3,4)(1,2,3).(2,3,4)\longrightarrow(1,2,3).

因此代码地址是 X[1, 2, 3]。批次项 2 的词元 3 是 (0.8,0.1,0.6,1.5)(0.8,0.1,-0.6,1.5),特征 4 是 1.51.5。所以

X2,3,4=1.5.X_{2,3,4}=1.5.

问题 4

对于形状为 (2, 3, 4) 的从零开始的张量,将 X[0, 2, 3]、X[1, 3, 0] 和 X[2, 0, 0] 分类为在范围内或超出范围。

显示分步解答

将形状转换成有效的非负范围:

b{0,1},t{0,1,2},f{0,1,2,3}.b\in\{0,1\},\qquad t\in\{0,1,2\},\qquad f\in\{0,1,2,3\}.
  • X[0, 2, 3] 在范围内,因为每个索引都属于自己的范围。
  • X[1, 3, 0] 超出范围,因为词元索引 3 不在 {0,1,2}\{0,1,2\} 中。
  • X[2, 0, 0] 超出范围,因为批次索引 2 不在 {0,1}\{0,1\} 中。

第一个失败的轴就足以让 NumPy 拒绝整个地址。

问题 5

开发者想要批次 2、词元 3、特征 1,却写成了 X[1, 0, 2]。为什么不会出现范围错误,该如何修复这一行?

显示分步解答

对于形状 (2, 3, 4),索引 (1, 0, 2) 在机械上是合法的:

1<2,0<3,2<4.1<2,\qquad 0<3,\qquad 2<4.

因此 NumPy 返回批次 2、词元 1、特征 3 处的值,即 0.30.3。这个库无法推断预期的轴含义。

将预期的从 1 开始的位置 (2,3,1)(2,3,1) 转换成从零开始的位置 $(1,2,0)`. 修复后的代码行是:

activation = X[1, 2, 0]  # 0.8

这是语义错误而不是范围错误,因此描述性变量名和明确的轴契约是有用的防护措施。

问题 6

X[-1, -1, -1] 选择了什么,为什么 −1 不是该值的永久身份?

显示分步解答

在 NumPy 中,-1 表示“该轴上的最后一个位置”。对于形状 (2, 3, 4),最后的从零开始位置是 (1, 2, 3)。因此,

X[1,1,1]=X[1,2,3]=1.5.X[-1,-1,-1]=X[1,2,3]=1.5.

其含义取决于当前的形状和顺序。如果再添加一个批次项或词元,-1 就会指向别处。它是相对地址,而不是附着在存储值上的稳定 ID。

资料来源

  1. Mathematics for Machine Learning companion website
  2. Mathematics for Machine Learning book PDF
  3. NumPy documentation: indexing on ndarrays
  4. NumPy documentation: ndarray shape