2 张量思维
“我试图想象信息在计算机中穿行的样子。”
—— 《创:战纪》 (2010)
主角萨姆第一次进入“电子世界”(The Grid)。在这里,没有任何实体,目之所及皆是流动的光轨、几何体和飞驰的光之摩托。这就是计算机内部的真实景象——纯粹的数据流动。
2.1 从标量到张量
在数学和物理学中,张量(Tensor)是对标量、向量和矩阵的广义化(Generalization)。我们可以根据张量的阶(Rank/Order)——即数据拥有多少个维度(轴)——来对它们进行分类。
为了便于直观理解,我们采用“数学定义 + R torch 代码”对照的方式来学习:
\[ \begin{array}{cccc} \text{Scalar} & \text{Vector} & \text{Matrix} & \text{Tensor} \\[1em] 1 & \begin{bmatrix} 1 \\ 2 \end{bmatrix} & \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} & \begin{bmatrix} [1, 2] & [3, 2] \\ [1, 7] & [5, 4] \end{bmatrix} \end{array} \]
标量 (Scalar)
数学定义:一个独立的数值,只有大小,没有方向。是一个零阶张量。
- 符号表示:\(x \in \mathbb{R}\)
- 阶数(Rank):0
- 形状(Shape):
()(空)
在 R 语言原生环境中,其实不存在严格意义上的“标量”(即使是 x <- 5,也是一个长度为 1 的向量)。但在 torch 中,标量是真实存在的。
# 创建一个 0 阶张量 (标量)
scalar <- torch_tensor(5)
print(scalar)
# 输出: torch_tensor
# 5
# [ CPUFloatType{} ]
# 查看形状
scalar$size()
# 输出: list() <-- 注意这里是空的,表示没有维度向量 (Vector)
数学定义:一列有序排列的数。是一个一阶张量。
- 符号表示:\(\mathbf{x} \in \mathbb{R}^n\)
- 阶数(Rank):1
- 形状(Shape):
(n) - 应用场景:偏置(Bias)、一维时间序列。
# 创建一个 1 阶张量 (向量)
vector <- torch_tensor(c(1, 2, 3))
vector$size()
# 输出: [[1]] 3 <-- 只有一个轴,长度为 3矩阵 (Matrix)
数学定义:由行和列组成的二维数组。是一个二阶张量。
- 符号表示:\(\mathbf{A} \in \mathbb{R}^{m \times n}\)
- 阶数(Rank):2
- 形状(Shape):
(m, n) - 应用场景:结构化表格数据、线性层的权重矩阵。
matrix_r <- matrix(1:6, nrow = 2, ncol = 3)
matrix_tensor <- torch_tensor(matrix_r)
matrix_tensor$shape
# 输出: [1] 2 3高阶张量
数学定义:矩阵向更高维度的扩展。可以将 3 阶张量想象为“立方体”或“多个矩阵摞在一起”。
- 符号表示:\(\mathcal{T} \in \mathbb{R}^{d_1 \times d_2 \times \dots \times d_n}\)
- 阶数(Rank):\(n\) (\(n \ge 3\))
- 应用场景:
- 3阶:彩色图像 (通道数 \(C \times\) 高 \(H \times\) 宽 \(W\))
- 4阶:图像批次 (批大小 \(N \times C \times H \times W\))
- 5阶:视频数据 (批大小 \(N \times\) 帧数 \(T \times C \times H \times W\))
这也是 R 用户最容易感到困惑的地方。R 的 array 是列优先(Column-major),而 torch 也是兼容的,但深度学习通常遵循特定的维度约定(如 NCHW)。
# 创建一个随机的 3 阶张量 (模拟一张 224x224 的 RGB 彩色图片)
# 维度顺序通常为: (Channels, Height, Width)
image_tensor <- torch_randn(3, 224, 224)
print(image_tensor$shape) # $shape 是 $size() 的别名
# 输出: [3, 224, 224]Rank 与 Shape 的防混淆指南:
- Rank (阶):指张量有多少个轴(Axis)。即,你需要几个索引才能定位到一个具体的数字。
image_tensor[1, 10, 10]需要 3 个索引,所以 Rank = 3。 - Shape (形状):指每个轴上具体的长度。例如上图的 Shape 是 (3, 224, 224)。
2.2 张量的核心操作
如果说张量是深度学习的原子,那么张量操作就是我们在微观世界中操纵物质的手段。对于熟悉 R 语言的读者来说,好消息是 R torch 包在设计上极力模仿了 R 原生数组(Array)和矩阵(Matrix)的行为逻辑;坏消息是,为了满足高性能计算和自动微分的需求,它引入了一些必须遵守的特定规则。
一、创建与数据类型 (Dtype)
在 R 中,numeric 默认是双精度浮点数(Float64)。但在深度学习中,为了节省显存和加速计算,标准类型通常是单精度(Float32)或整数(Int64)。
# 从 R 向量创建,默认转为 Float32
v <- c(1, 2, 3)
t1 <- torch_tensor(v)
# 工厂函数生成特定张量
x_rand <- torch_randn(3, 3) # 标准正态分布
x_zeros <- torch_zeros(2, 5) # 全 0
x_ones <- torch_ones(2, 5) # 全 1
x_seq <- torch_arange(start = 1, end = 10, step = 1) # 序列
x_eye <- torch_eye(3) # 生成 3x3 的单位矩阵
x_diag <- torch_diag(torch_tensor(1:3)) # 生成对角矩阵
# 类型转换 (Casting):实战高频操作
x_int <- t1$to(dtype = torch_int64())
x_bool <- t1$to(dtype = torch_bool())二、内存引用 vs 写时复制
这是 R 用户最容易写出 Bug 的地方。R 语言采用写时复制(Copy-on-modify),而 Torch 张量默认是内存引用(Reference)。
| 操作 | a <- c(1,2) |
a <- torch_tensor(c(1,2)) |
|---|---|---|
| 赋值给新变量 | b <- a |
b <- a |
| 修改新变量 | b[1] <- 99 |
b[1] <- 99 |
| 原变量结果 | a 保持不变,仍为 c(1, 2) |
a 也会变成 c(99, 2) |
如果你需要一个完全独立的副本,必须使用 $clone()。
b <- a$clone() # 此时修改 b 不会影响 a三、索引、切片与条件查找
好消息是,R torch 保留了 R 语言从 1 开始索引(1-based indexing)的习惯。你可以无缝迁移 matrix 的操作经验。
x <- torch_randn(4, 4)
row_1 <- x[1, ] # 选取第 1 行
subset <- x[1:2, 3] # 选取第 1 到 2 行,第 3 列
rest <- x[-1, ] # 负索引:排除第 1 行
# 逻辑索引 (Masking)
positive_vals <- x[x > 0]
# 条件查找 (类似 R 的 ifelse)
# torch_where(条件, 为真时返回, 为假时返回)
x_clipped <- torch_where(x > 0, x, torch_tensor(0))2.3 形状与维度操作
在搭建神经网络时,数据的维度在不同层之间不断重塑。掌握维度操作是写出顺畅代码的关键。
一、View 与 Reshape
- view():要求张量在内存中连续,效率极高,不复制数据。
- reshape():更通用,如果内存不连续会自动克隆数据。
x <- torch_arange(1, 12)
# 变为 3x4 矩阵
m <- x$view(c(3, 4))
# 维度推断:使用 -1 (表示"行数我定了,列数你自己算")
m_auto <- x$view(c(3, -1))二、维度交换
R 基础包使用 aperm() 交换数组维度,在 torch 中使用 permute()。图像处理中经常需要将 R 习惯的 (Height, Width, Channels) 转换为 PyTorch 模型需要的 (Channels, Height, Width)。
img <- torch_randn(256, 256, 3) # (H, W, C)
# 将第 3 维提到第 1 维,其余顺延
img_permuted <- img$permute(c(3, 1, 2)) # 新形状: (3, 256, 256)三、挤压、扩张与拼接
在批处理(Batch)运算中,维度对齐至关重要。有时我们需要人为地增加或减少一个维度。
unsqueeze(dim):在指定位置插入一个长度为 1 的维度。squeeze(dim):移除指定位置长度为 1 的维度。
x <- torch_tensor(c(1, 2, 3)) # 形状: (3)
# 增加 Batch 维度,使其变成 "Batch Size 为 1 的向量"
x_batch <- x$unsqueeze(1) # 形状: (1, 3)
# 恢复原状
x_original <- x_batch$squeeze(1) # 形状: (3)当你把一张图片输入模型预测时,模型通常期待
(Batch, Channel, Height, Width)。如果你只读入了一张图(C, H, W),必须使用unsqueeze(1)变成(1, C, H, W)才能被模型接受。
torch_cat() vs torch_stack():拼接数据的两把利器。
- cat:在现有维度拼接(不改变 Rank)。
- stack:沿着新维度堆叠(Rank + 1)。
t1 <- torch_randn(3, 4)
t2 <- torch_randn(3, 4)
# 拼接:形状变为 (6, 4) -> 依然是 2 阶矩阵
c_tensor <- torch_cat(list(t1, t2), dim = 1)
# 堆叠:形状变为 (2, 3, 4) -> 变成了 3 阶张量
s_tensor <- torch_stack(list(t1, t2), dim = 1)2.4 线性代数运算实战
如果张量是静止的数据,线性代数就是驱动神经网络运转的引擎。
2.4.1 基础算数
我们可以对张量进行所有通常的数学运算:加、减、除……这些操作可以作为函数(以 torch_ 开头)以及对象的方法(用$-syntax调用)。例如,以下是等价的:
> t1 <- torch_tensor(c(1, 2))
> t2 <- torch_tensor(c(3, 4))
> t1 + t2
torch_tensor
4
6
[ CPUFloatType{2} ]
> torch_add(t1, t2)
torch_tensor
4
6
[ CPUFloatType{2} ]
> t1$add(t2)
torch_tensor
4
6
[ CPUFloatType{2} ]同理还有 torch_sub(), torch_mul(), torch_div() 等函数,以及 $sub(), $mul(), $div() 等方法。这里有个小知识点需要注意,原地操作:
- 所有以
_结尾的方法都是原地操作,会直接修改调用它的张量,而不是返回新的张量。 - 例如,
$add_()会将调用它的张量的值加上传入的参数,而不是返回一个新的张量。
x <- torch_tensor(c(1, 2, 3))
y <- x$sub(1) # 非原地:创建新张量 [0, 1, 2],x 还是 [1, 2, 3]
x$sub_(1) # 原地:直接修改 x 为 [0, 1, 2]点积(Dot Product)是这样计算的:
> t1 <- torch_tensor(c(1, 2))
> t2 <- torch_tensor(c(3, 4))
> torch_dot(t1, t2)
torch_tensor
11
[ CPUFloatType{1} ]
> t1$dot(t2)
torch_tensor
11
[ CPUFloatType{1} ]这是最基础的运算,对应于 R 语言中的标准算术操作。当两个张量形状完全一致时,加减乘除都是发生在对应位置的元素之间。
- Hadamard 积:即逐元素相乘。在 R 中我们使用
*,在 torch 中也是*(或torch_mul)。 - 数学表示:\(\mathbf{C} = \mathbf{A} \odot \mathbf{B}\),即 \(c_{ij} = a_{ij} \times b_{ij}\)。
library(torch)
a <- torch_tensor(matrix(c(1, 2, 3, 4), ncol = 2))
b <- torch_tensor(matrix(c(1, 0, 1, 0), ncol = 2))
# 逐元素相乘
# 等价为 torch_multiply(a, b)
# 结果:
# 1 3
# 0 0
res <- a * b注意:这不是矩阵乘法。注意区别
*和matmul。
2.4.2 矩阵乘法
矩阵乘法(Matrix Multiplication)是几乎所有神经网络层的计算核心。
- R 的习惯:我们习惯使用
%*%算符。 - Torch 的方式:使用
$matmul()方法或torch_matmul()函数。
1. 基础矩阵乘法
规则:\((M \times K)\) 的矩阵乘以 \((K \times N)\) 的矩阵,得到 \((M \times N)\) 的结果。
# 形状: (2, 3)
A <- torch_randn(2, 3)
# 形状: (3, 4)
B <- torch_randn(3, 4)
# 结果形状: (2, 4)
C <- A$matmul(B)2. 批量矩阵乘法 (Batch Matmul)
这是 Torch 强大的地方。假设你有一个 Batch 的数据,形状为 \((B, N, M)\),你需要对每个样本都乘以同一个权重矩阵 \((M, K)\)。Torch 会自动识别并处理前面的 Batch 维度,这被称为“批次感知”。
# Batch size=10, 序列长度=20, 特征维度=5
inputs <- torch_randn(10, 20, 5)
# 线性层权重: 5 -> 8
weights <- torch_randn(5, 8)
# 自动处理 Batch 维度
# 结果形状: (10, 20, 8)
outputs <- inputs$matmul(weights)2.4.3 广播机制
在 R 语言的基础操作中,如果你把长度为 3 的向量加到长度为 10 的向量上,R 会尝试“循环补齐(Recycle)”,如果不整除则会给出一个 Warning。
Torch 的规则完全不同且更加严格。广播机制(Broadcasting)允许我们在不同形状的张量之间进行算术运算,但必须满足特定的几何条件。
规则:从两个张量的最右边(最后一个维度)开始向左匹配,如果满足以下任一条件,则认为该维度是兼容的:
- 两个维度的大小相等。
- 其中一个维度的大小为 1。
如果维度为 1,Torch 就会在这个维度上将数据“复制”或“拉伸”,直到与另一个张量匹配。
实战:图像归一化
假设我们有一批 RGB 图像,形状为 (32, 3, 256, 256) —— (Batch, Channel, Height, Width)。 我们要对每个通道减去均值 mean = c(0.5, 0.5, 0.5)。
mean 张量的原始形状是 (3)。直接相减会报错,因为从右边对齐时,256 和 3 不匹配。我们需要将 mean 变形为 (3, 1, 1)。
# 模拟图像数据
images <- torch_randn(32, 3, 256, 256)
# 均值向量
means <- torch_tensor(c(0.5, 0.5, 0.5))
# 正确写法:利用 view 调整维度以满足广播规则
# 变成 (1, 3, 1, 1) 或 (3, 1, 1),从右向左匹配
means_broadcastable <- means$view(c(3, 1, 1))
# 广播过程:
# means (3, 1, 1) 在第 2, 3 维被拉伸成 (3, 256, 256)
# 然后在第 0 维(Batch)被逻辑复制 32 次
normalized <- images - means_broadcastable广播就像是只有当一个维度是“1”时,可以变成任意数字;否则它必须和对方严丝合缝。
2.4.4 聚合操作
在计算损失函数(如 MSE 或 CrossEntropy)时,我们经常需要将高维张量压缩成一个标量或低维张量。
sum(): 求和mean(): 求平均max()/argmax(): 求最大值及其索引
关键参数:dim 和 keepdim
R 用户习惯 colMeans 或 rowMeans,但在 Torch 中,我们统一使用 dim 参数指定“要消灭的维度”。
x <- torch_tensor(matrix(c(1, 2, 3, 4), ncol = 2))
# 全局平均
avg_all <- x$mean()
# 按行求平均(消灭列维度,即第 2 维)
# 结果形状: (2)
row_means <- x$mean(dim = 2)
# 保持维度 (Keepdim)
# 结果形状: (2, 1) —— 这是一个列向量形状
# 这对于后续的广播运算非常重要!
row_means_keep <- x$mean(dim = 2, keepdim = TRUE)2.4.5 爱因斯坦求和约定
在实现复杂的现代网络(如 Transformer 中的多头注意力)时,维度的 permute 和 matmul 组合会让代码变得晦涩难懂。函数 torch_einsum 提供了一种用字符串描述运算的优雅方式。它允许你用一个简洁的字符串公式来描述复杂的张量运算,比如矩阵乘法、转置、张量收缩、加权求和等。
假设我们要计算 \(C = \sum_k A_{ik} B_{kj}\) (矩阵乘法):
A <- torch_randn(2, 3)
B <- torch_randn(3, 4)
# 这里的 "ik,kj->ij" 像写公式一样描述了运算逻辑
# 此表达式等价于 A$matmul(B)
C <- torch_einsum("ik,kj->ij", list(A, B))图像加权灰度化:
img <- torch_randn(c(3,5,5)) # RGB图像
w <- torch_tensor(c(0.3,0.6,0.1))
gray <- torch_einsum("chw,c->hw", list(img, w))
# 输出 5x5 灰度图虽然初看起来抽象,但在处理高维张量逻辑时,它是最清晰的文档。
2.5 计算设备切换
张量操作的最后一步,通常是将数据移动到计算设备上。还记得上一章最后演示矩阵乘法在不同环境下的差别吗?这是 torch 区别于普通 R 矩阵的杀手级功能。
# 检查 GPU 是否可用
device <- if (backends_mps_is_available()) {
torch_device("mps") # Mac M1/M2/M3
} else if (cuda_is_available()) {
torch_device("cuda") # NVIDIA GPU
} else {
torch_device("cpu")
}
x <- torch_randn(100, 100)
# 将张量移动到 GPU
x_gpu <- x$to(device = device)
# 在 GPU 上进行运算
result <- x_gpu$matmul(x_gpu)
# 将结果移回 CPU(例如为了绘图或保存)
result_cpu <- result$to(device = "cpu")至此,所有的准备工作就绪。在下一章,我们将赋予这些数学公式以“智能”——探索自动微分(Autograd)的奥秘,看看机器是如何完成学习的。