面向 AI 的张量索引:读取样本、词元与特征
学习读取张量下标,将批次-词元-特征地址转换为 NumPy,并发现 AI 代码中看似有效的轴错误。
一个 AI 程序读取 X[1, 2, 0] 并返回 0.8。附近的表达式 X[1, 0, 2] 返回 0.3。两个地址都有效,但只有一个表示“第二个示例中的最后一个词元的第一个特征”。
差异在于索引:索引是用来定位有序结构中某个值的位置。索引是很小的记号,但它决定了一次计算实际使用的是哪个示例、词元、特征、像素或模型输出。因此,错误的索引可能产生一个看似可信的数字,而不是有帮助的错误提示。
学完本课后,你将能够读取带索引的样本和坐标,跟踪批次-词元-特征张量地址,将数学下标转换为 NumPy 表达式,并发现一个数字合法但轴含义错误的地址。本课为第 4 课中的数据集集合增加了顺序。官方的 Mathematics for Machine Learning 配套网站把这套记号放在从数值数据通向课程后续使用的向量、矩阵和模型的路径上。
索引回答“哪个位置?”
假设一个有序数据集包含三个示例:
中的下标表示“选择位置 2 的示例”。它并不表示该示例的值是 2。如果第二个示例是数对 ,那么 指的是整个数对。
再增加一个下标,就可以选择该示例内部的一个坐标:
从左到右读取 :先选择示例 2,再选择坐标 1。索引组成一个地址;存储在该地址中的数字才是值。
这也把位置与身份区分开来。把一个样本从位置 2 移到位置 7,会改变它的索引,即使它的稳定 ID、图像像素和标签都不变。索引是有用的地址,而不是现实世界对象的永久名称。
每个下标选择一个轴
轴是数组组织数据的一个方向。表格有行轴和列轴。在这里,张量是一个多维数组:它可以有这两个轴,也可以为批次和词元等概念增加更多轴。
考虑一个激活张量
其中:
- 是批次中的示例数量;
- 是每个示例的词元位置数量;
- 是每个词元存储的特征数量。
记号 表示每个条目都是实数,并且张量的形状是 。一个标量条目是
它表示按这个顺序选择批次位置 、词元位置 和特征位置 。
对于形状为 的张量,采用从 1 开始的数学约定时,可以有
形状提供每个索引的范围,轴名称提供含义。两者缺一不可。
一个地址:完整地计算到一个值
使用下面这个两批次张量。每个批次项包含三行词元,每个词元有四个特征值。
| 词元 | 特征 1 | 特征 2 | 特征 3 | 特征 4 |
|---|---|---|---|---|
| 1 | 0.2 | 0.8 | -0.1 | 0.4 |
| 2 | 1.1 | -0.3 | 0.5 | 0.0 |
| 3 | 0.7 | 0.6 | 0.2 | -0.4 |
| 词元 | 特征 1 | 特征 2 | 特征 3 | 特征 4 |
|---|---|---|---|---|
| 1 | -0.2 | 0.9 | 0.3 | 0.1 |
| 2 | 0.4 | -0.7 | 1.2 | 0.5 |
| 3 | 0.8 | 0.1 | -0.6 | 1.5 |
这种逐步读取比记住字母顺序更重要。另一个项目可能把词元存储在批次之前,或者对图像使用高度、宽度和通道轴。代码和文档必须说明这份契约。
NumPy 从零开始
许多数学文本从 1 开始标记位置。Python 和 NumPy 使用从零开始的索引,因此第一个位置是 0。NumPy 的官方索引指南也确认,每个维度使用一个整数就能选择一个元素,并且大小为 的轴上的有效非负索引满足 。
| 目标位置 | 从 1 开始的数学索引 | 从零开始的 NumPy 索引 |
|---|---|---|
| 批次项 2 | b = 1 | |
| 词元 3 | t = 2 | |
| 特征 1 | f = 0 | |
| 完整地址 | 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 开始的数学下标。只要这种区别重要,就要说明所采用的约定。
下一课会在求和中使用这些地址。一旦 清楚地表示第 个示例,而 表示它的损失,诸如 的符号就会成为访问每个带索引损失并合并这些值的简洁指令。
检查你的理解
问题 1
如果 x₄ = (2.5, −1.0, 0.3),x₄ 和 x₄,₂ 分别指什么?
显示分步解答
先读取下标的数量。
- 有一个索引,因此选择位置 4 的整个示例。
- 给定示例是三坐标值 。
- 增加了一个坐标索引,因此选择该示例中的坐标 2。
因此,
下标 4 是一个地址,而不是存储的值。
问题 2
用图示计算 X₂,₂,₃。展示每个轴的选择。
显示分步解答
按照已声明的批次-词元-特征顺序跟随这些索引。
- 选择批次项 2。
- 选择其中的第二行词元:。
- 选择该行的第三个坐标。
所以,
在词元 2 之前选择特征 3 会改变问题,因为轴是有顺序的。
问题 3
将从 1 开始的数学地址 X₂,₃,₄ 转换为从零开始的 NumPy 地址,并找出其值。
显示分步解答
从所声明的从 1 开始的约定转换到 NumPy 时,将每个位置减去 1:
因此代码地址是 X[1, 2, 3]。批次项 2 的词元 3 是 ,特征 4 是 。所以
问题 4
对于形状为 (2, 3, 4) 的从零开始的张量,将 X[0, 2, 3]、X[1, 3, 0] 和 X[2, 0, 0] 分类为在范围内或超出范围。
显示分步解答
将形状转换成有效的非负范围:
X[0, 2, 3]在范围内,因为每个索引都属于自己的范围。X[1, 3, 0]超出范围,因为词元索引 3 不在 中。X[2, 0, 0]超出范围,因为批次索引 2 不在 中。
第一个失败的轴就足以让 NumPy 拒绝整个地址。
问题 5
开发者想要批次 2、词元 3、特征 1,却写成了 X[1, 0, 2]。为什么不会出现范围错误,该如何修复这一行?
显示分步解答
对于形状 (2, 3, 4),索引 (1, 0, 2) 在机械上是合法的:
因此 NumPy 返回批次 2、词元 1、特征 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)。因此,
其含义取决于当前的形状和顺序。如果再添加一个批次项或词元,-1 就会指向别处。它是相对地址,而不是附着在存储值上的稳定 ID。