张量与高维数组
深度学习中所有数据都以张量(多维数组)的形式存储和计算。一张 RGB 图片是 3 阶张量 (C,H,W)=(3,224,224);一组文本经 tokenizer 后变成 2 阶张量 (batch, seq_len);一个 mini-batch 的图片集合则是 4 阶张量 (B,C,H,W)=(32,3,224,224),约 480 万个浮点数在 GPU 上并行运算。Reshape 将 CNN 最后一层的 (B,512,7,7) 展平为 (B,25088) 送入全连接层;Transpose/Permute 在注意力多头切分 (B,L,H,D)↔(B,H,L,D) 以及图像通道转换 CHW↔HWC 时不可或缺;广播机制让 BatchNorm 的均值 (1,C,1,1) 能直接作用于整个 (B,C,H,W) 特征图,逐元素减均值除标准差。模型参数本身也是张量:GPT-3 拥有 1750 亿个参数张量,训练时每一步都要对等量的梯度张量做加法更新。理解张量形状变换和广播规则,是读懂和调试模型代码的第一步。
一、张量的定义与阶数
张量(Tensor)是标量、向量、矩阵的推广:
0 阶张量 = 标量(一个数字),如:损失值 L = 2.35
1 阶张量 = 向量(一行数字),如:偏置 b = [0.1, -0.2, 0.3]
2 阶张量 = 矩阵(一张表格),如:权重 W(3×4)
3 阶张量 = 三维数组,如:一张彩色图片(3×224×224)
4 阶张量 = 四维数组,如:一批图片(32×3×224×224)
5 阶张量 = 五维数组,如:一段视频(32×30×3×224×224)
张量的"阶数"(rank)= 需要几个下标来定位一个元素。
0 阶张量 = 标量(一个数字),如:损失值 L = 2.35
1 阶张量 = 向量(一行数字),如:偏置 b = [0.1, -0.2, 0.3]
2 阶张量 = 矩阵(一张表格),如:权重 W(3×4)
3 阶张量 = 三维数组,如:一张彩色图片(3×224×224)
4 阶张量 = 四维数组,如:一批图片(32×3×224×224)
5 阶张量 = 五维数组,如:一段视频(32×30×3×224×224)
张量的"阶数"(rank)= 需要几个下标来定位一个元素。
二、深度学习中常见的张量形状
| 数据类型 | 张量形状 | 每个维度含义 | 具体示例 |
|---|---|---|---|
| 灰度图片(单张) | (H, W) | 高度, 宽度 | MNIST: (28, 28),784 个像素值 0~255 |
| 彩色图片(单张) | (C, H, W) | 通道, 高度, 宽度 | ImageNet: (3, 224, 224),3=RGB 三通道 |
| 图片批次 | (B, C, H, W) | 批大小, 通道, 高, 宽 | 训练: (32, 3, 224, 224),32 张 224×224 彩色图 |
| 文本(一个句子) | (L, D) | 序列长度, 嵌入维度 | BERT: (512, 768),512 个 token 各 768 维 |
| 文本批次 | (B, L, D) | 批大小, 长度, 维度 | GPT: (16, 1024, 4096),16 句话 |
| 卷积核 | (Cout, Cin, Kh, Kw) | 输出通道, 输入通道, 核高, 核宽 | ResNet第1层: (64, 3, 7, 7) |
| 全连接权重 | (Dout, Din) | 输出维度, 输入维度 | 分类头: (1000, 2048),1000 个 ImageNet 类别 |
| 视频批次 | (B, T, C, H, W) | 批, 帧, 通道, 高, 宽 | (8, 30, 3, 112, 112),8 段 30 帧视频 |
三、真实数据的张量表示
示例1:一张 RGB 彩色图片 (3, 4, 4)
假设一张 4×4 像素的图片,3 个通道:
R通道(红色):
[[255, 200, 100, 0],
[230, 180, 80, 10],
[200, 150, 60, 20],
[180, 120, 40, 30]]
G通道(绿色):
[[0, 50, 100, 200],
[10, 60, 120, 210],
[20, 80, 150, 220],
[30, 100, 170, 240]]
B通道(蓝色):
[[0, 0, 50, 100],
[0, 10, 60, 110],
[10, 20, 70, 130],
[20, 30, 80, 150]]
左上角像素 (R=255, G=0, B=0):纯红色
右下角像素 (R=30, G=240, B=150):青绿色
整张图从左到右,红色渐弱,绿色渐强——颜色从红到绿的渐变。
假设一张 4×4 像素的图片,3 个通道:
R通道(红色):
[[255, 200, 100, 0],
[230, 180, 80, 10],
[200, 150, 60, 20],
[180, 120, 40, 30]]
G通道(绿色):
[[0, 50, 100, 200],
[10, 60, 120, 210],
[20, 80, 150, 220],
[30, 100, 170, 240]]
B通道(蓝色):
[[0, 0, 50, 100],
[0, 10, 60, 110],
[10, 20, 70, 130],
[20, 30, 80, 150]]
左上角像素 (R=255, G=0, B=0):纯红色
右下角像素 (R=30, G=240, B=150):青绿色
整张图从左到右,红色渐弱,绿色渐强——颜色从红到绿的渐变。
示例2:文本数据的张量化过程(从句子到张量)
原始文本:"猫 坐在 垫子 上"
第1步:分词(Tokenize)
→ ["猫", "坐在", "垫子", "上"](4 个 token)
第2步:建立词表并编号
{"猫":0, "坐在":1, "垫子":2, "上":3, "狗":4, ...}(词表大小=5000)
→ [0, 1, 2, 3](1 阶张量,形状 (4,))
第3步:词嵌入(Embedding Lookup)
嵌入矩阵 E 的形状是 (5000, 128),即 5000 个词各 128 维向量。
查表得到每个词的向量:
"猫" → E[0] = [0.12, -0.34, 0.56, ..., 0.78](128维)
"坐在" → E[1] = [-0.21, 0.45, -0.67, ..., 0.11](128维)
"垫子" → E[2] = [0.33, 0.28, -0.15, ..., -0.44](128维)
"上" → E[3] = [-0.09, 0.61, 0.23, ..., 0.55](128维)
结果:形状 (4, 128) 的 2 阶张量
第4步:组成 Batch
如果同时处理 8 个句子(padding 到最大长度 20):
→ 形状 (8, 20, 128) 的 3 阶张量
8 = batch_size, 20 = 最大序列长度, 128 = 嵌入维度
原始文本:"猫 坐在 垫子 上"
第1步:分词(Tokenize)
→ ["猫", "坐在", "垫子", "上"](4 个 token)
第2步:建立词表并编号
{"猫":0, "坐在":1, "垫子":2, "上":3, "狗":4, ...}(词表大小=5000)
→ [0, 1, 2, 3](1 阶张量,形状 (4,))
第3步:词嵌入(Embedding Lookup)
嵌入矩阵 E 的形状是 (5000, 128),即 5000 个词各 128 维向量。
查表得到每个词的向量:
"猫" → E[0] = [0.12, -0.34, 0.56, ..., 0.78](128维)
"坐在" → E[1] = [-0.21, 0.45, -0.67, ..., 0.11](128维)
"垫子" → E[2] = [0.33, 0.28, -0.15, ..., -0.44](128维)
"上" → E[3] = [-0.09, 0.61, 0.23, ..., 0.55](128维)
结果:形状 (4, 128) 的 2 阶张量
第4步:组成 Batch
如果同时处理 8 个句子(padding 到最大长度 20):
→ 形状 (8, 20, 128) 的 3 阶张量
8 = batch_size, 20 = 最大序列长度, 128 = 嵌入维度
四、张量的基本操作
4.1 形状变换(Reshape/View)
改变张量的形状但不改变数据。前提:元素总数不变。
原始张量 a,形状 (2, 3, 4),共 24 个元素
可以 reshape 为:
(24,) — 展平为 1 维向量
(6, 4) — 变为 6×4 矩阵
(2, 12) — 变为 2×12 矩阵
(4, 6) — 变为 4×6 矩阵
(2, 2, 6) — 变为 2×2×6 的 3 阶张量
不可以 reshape 为 (5, 5) 因为 5×5=25 ≠ 24
可以 reshape 为:
(24,) — 展平为 1 维向量
(6, 4) — 变为 6×4 矩阵
(2, 12) — 变为 2×12 矩阵
(4, 6) — 变为 4×6 矩阵
(2, 2, 6) — 变为 2×2×6 的 3 阶张量
不可以 reshape 为 (5, 5) 因为 5×5=25 ≠ 24
AI 应用——CNN 全连接层之前的 Flatten:
经过多层卷积后,特征图形状可能是 (batch=16, channels=512, h=7, w=7)
在进入全连接层前需要 flatten:
(16, 512, 7, 7) → reshape → (16, 512×7×7) = (16, 25088)
每个样本的 512 个 7×7 特征图被展平为 25088 维向量,
然后输入到全连接层 Linear(25088, 4096)。
VGG-16 的分类头就是这样:Conv → ... → (512,7,7) → Flatten → FC(25088→4096) → FC(4096→4096) → FC(4096→1000)
经过多层卷积后,特征图形状可能是 (batch=16, channels=512, h=7, w=7)
在进入全连接层前需要 flatten:
(16, 512, 7, 7) → reshape → (16, 512×7×7) = (16, 25088)
每个样本的 512 个 7×7 特征图被展平为 25088 维向量,
然后输入到全连接层 Linear(25088, 4096)。
VGG-16 的分类头就是这样:Conv → ... → (512,7,7) → Flatten → FC(25088→4096) → FC(4096→4096) → FC(4096→1000)
4.2 转置与维度置换(Transpose/Permute)
2D 转置:交换行列
A(3×4) → Aᵀ(4×3)
高维置换(permute):重新排列维度顺序
原始:(B, C, H, W) = (16, 3, 224, 224)
permute(0,2,3,1):(B, H, W, C) = (16, 224, 224, 3)
用途:不同的框架、不同的函数对维度顺序有不同要求:
PyTorch 卷积:(B, C, H, W) — "通道优先"
TensorFlow 卷积:(B, H, W, C) — "通道最后"
显示函数 matplotlib:(H, W, C) — 去掉 batch 维度
A(3×4) → Aᵀ(4×3)
高维置换(permute):重新排列维度顺序
原始:(B, C, H, W) = (16, 3, 224, 224)
permute(0,2,3,1):(B, H, W, C) = (16, 224, 224, 3)
用途:不同的框架、不同的函数对维度顺序有不同要求:
PyTorch 卷积:(B, C, H, W) — "通道优先"
TensorFlow 卷积:(B, H, W, C) — "通道最后"
显示函数 matplotlib:(H, W, C) — 去掉 batch 维度
4.3 广播机制(Broadcasting)
当两个不同形状的张量做逐元素运算时,自动扩展较小张量使其形状匹配。
广播规则:从最后一个维度向前逐一比较:
① 维度大小相同 → 直接运算
② 其中一个维度大小=1 → 自动复制扩展
③ 一个张量维度不够 → 在前面补维度1
④ 其他情况 → 报错
① 维度大小相同 → 直接运算
② 其中一个维度大小=1 → 自动复制扩展
③ 一个张量维度不够 → 在前面补维度1
④ 其他情况 → 报错
示例1:向量 + 标量
[1, 2, 3] + 10 = [1+10, 2+10, 3+10] = [11, 12, 13]
标量 10 被广播为 [10, 10, 10]
示例2:矩阵 + 行向量(批量加偏置)
X(3×4) + b(1×4) → b 沿第 0 维复制 3 次成 (3×4)
X = [[1,2,3,4], b = [0.1, 0.2, 0.3, 0.4]
[5,6,7,8],
[9,10,11,12]]
X + b = [[1.1, 2.2, 3.3, 4.4],
[5.1, 6.2, 7.3, 8.4],
[9.1, 10.2, 11.3, 12.4]]
示例3:列向量 × 行向量(外积)
a(3×1) × b(1×4) → 结果 (3×4)
a = [[2],[3],[4]] b = [1,2,3,4]
结果 = [[2,4,6,8],
[3,6,9,12],
[4,8,12,16]]
[1, 2, 3] + 10 = [1+10, 2+10, 3+10] = [11, 12, 13]
标量 10 被广播为 [10, 10, 10]
示例2:矩阵 + 行向量(批量加偏置)
X(3×4) + b(1×4) → b 沿第 0 维复制 3 次成 (3×4)
X = [[1,2,3,4], b = [0.1, 0.2, 0.3, 0.4]
[5,6,7,8],
[9,10,11,12]]
X + b = [[1.1, 2.2, 3.3, 4.4],
[5.1, 6.2, 7.3, 8.4],
[9.1, 10.2, 11.3, 12.4]]
示例3:列向量 × 行向量(外积)
a(3×1) × b(1×4) → 结果 (3×4)
a = [[2],[3],[4]] b = [1,2,3,4]
结果 = [[2,4,6,8],
[3,6,9,12],
[4,8,12,16]]
广播在 AI 中无处不在:
① 批量加偏置:z = Wx + b 中,b 形状 (1, n_out),要加到 (batch, n_out) 的矩阵上。
b 沿 batch 维度广播,等价于每个样本都加相同的偏置。
② BatchNorm:均值 μ 和方差 σ 形状 (1, C, 1, 1),要对 (B, C, H, W) 的特征图做标准化。
沿 B, H, W 三个维度广播,每个通道用相同的 μ 和 σ。
③ Attention mask:mask 形状 (1, 1, L, L),广播到 (B, num_heads, L, L)。
所有样本、所有注意力头共享同一个 mask。
① 批量加偏置:z = Wx + b 中,b 形状 (1, n_out),要加到 (batch, n_out) 的矩阵上。
b 沿 batch 维度广播,等价于每个样本都加相同的偏置。
② BatchNorm:均值 μ 和方差 σ 形状 (1, C, 1, 1),要对 (B, C, H, W) 的特征图做标准化。
沿 B, H, W 三个维度广播,每个通道用相同的 μ 和 σ。
③ Attention mask:mask 形状 (1, 1, L, L),广播到 (B, num_heads, L, L)。
所有样本、所有注意力头共享同一个 mask。
五、张量在实际模型中的流动
ResNet-50 数据流(形状追踪):
输入图片: (32, 3, 224, 224) — 32 张 224×224 RGB 图片
↓ Conv1(7×7, stride=2): (32, 64, 112, 112) — 64 个特征图
↓ MaxPool(3×3, stride=2): (32, 64, 56, 56)
↓ ResBlock×3(64通道): (32, 64, 56, 56) — 尺寸不变
↓ ResBlock×4(128通道): (32, 128, 28, 28) — 通道翻倍,尺寸减半
↓ ResBlock×6(256通道): (32, 256, 14, 14)
↓ ResBlock×3(512通道): (32, 512, 7, 7)
↓ AvgPool(7×7): (32, 512, 1, 1) — 全局平均池化
↓ Flatten: (32, 512)
↓ FC(512→1000): (32, 1000) — 1000 个 ImageNet 类别的 logits
↓ Softmax: (32, 1000) — 每个类别的概率
参数量分析:
Conv1: 3×64×7×7 + 64 = 9,472 个参数
最后的 FC: 512×1000 + 1000 = 513,000 个参数
整个 ResNet-50 总共约 25.6M 个参数
输入图片: (32, 3, 224, 224) — 32 张 224×224 RGB 图片
↓ Conv1(7×7, stride=2): (32, 64, 112, 112) — 64 个特征图
↓ MaxPool(3×3, stride=2): (32, 64, 56, 56)
↓ ResBlock×3(64通道): (32, 64, 56, 56) — 尺寸不变
↓ ResBlock×4(128通道): (32, 128, 28, 28) — 通道翻倍,尺寸减半
↓ ResBlock×6(256通道): (32, 256, 14, 14)
↓ ResBlock×3(512通道): (32, 512, 7, 7)
↓ AvgPool(7×7): (32, 512, 1, 1) — 全局平均池化
↓ Flatten: (32, 512)
↓ FC(512→1000): (32, 1000) — 1000 个 ImageNet 类别的 logits
↓ Softmax: (32, 1000) — 每个类别的概率
参数量分析:
Conv1: 3×64×7×7 + 64 = 9,472 个参数
最后的 FC: 512×1000 + 1000 = 513,000 个参数
整个 ResNet-50 总共约 25.6M 个参数
GPT-类模型数据流:
输入文本(token ids): (8, 512) — 8 个句子,每句 512 个 token
↓ Token Embedding: (8, 512, 768) — 768 维词向量
↓ Position Embedding: (8, 512, 768) — 加位置编码(广播)
↓ Transformer Block ×12:
Multi-Head Attention:
Q, K, V: 各 (8, 12, 512, 64) — 12 头, 每头 64 维
QKᵀ: (8, 12, 512, 512) — 注意力分数矩阵
softmax 后 × V: (8, 12, 512, 64)
concat 12头: (8, 512, 768)
FFN: (8, 512, 768) → (8, 512, 3072) → (8, 512, 768)
↓ LayerNorm: (8, 512, 768)
↓ Linear(768→vocab_size): (8, 512, 30000) — 每个位置预测下一个词
GPT-2 参数量:约 1.5 亿(GPT-3 约 1750 亿,GPT-4 据传约 1.8 万亿)
输入文本(token ids): (8, 512) — 8 个句子,每句 512 个 token
↓ Token Embedding: (8, 512, 768) — 768 维词向量
↓ Position Embedding: (8, 512, 768) — 加位置编码(广播)
↓ Transformer Block ×12:
Multi-Head Attention:
Q, K, V: 各 (8, 12, 512, 64) — 12 头, 每头 64 维
QKᵀ: (8, 12, 512, 512) — 注意力分数矩阵
softmax 后 × V: (8, 12, 512, 64)
concat 12头: (8, 512, 768)
FFN: (8, 512, 768) → (8, 512, 3072) → (8, 512, 768)
↓ LayerNorm: (8, 512, 768)
↓ Linear(768→vocab_size): (8, 512, 30000) — 每个位置预测下一个词
GPT-2 参数量:约 1.5 亿(GPT-3 约 1750 亿,GPT-4 据传约 1.8 万亿)
六、代码验证(C# / Rust)
C#(.NET 10)
// dotnet run 即可执行
// ===== 不同阶的张量 =====
float scalar = 3.14f; // 0阶
int[] vector = { 1, 2, 3 }; // 1阶, shape(3)
int[,] matrix = { { 1, 2 }, { 3, 4 } }; // 2阶, shape(2,2)
Console.WriteLine($"标量 shape: ()");
Console.WriteLine($"向量 shape: ({vector.Length},)");
Console.WriteLine($"矩阵 shape: ({matrix.GetLength(0)},{matrix.GetLength(1)})");
Console.WriteLine($"3阶张量 shape: (3,224,224) — 彩色图片");
Console.WriteLine($"4阶张量 shape: (32,3,224,224), 元素数: {32*3*224*224:N0}");
// ===== Reshape:索引映射 =====
int[] a = Enumerable.Range(0, 24).ToArray(); // (24,)
// reshape(2,3,4): a[i,j,k] = a[i*12+j*4+k]
Console.WriteLine($"\nreshape(2,3,4): a[1,2,3] = a[{1*12+2*4+3}] = {a[1*12+2*4+3]}");
Console.WriteLine($"reshape(6,4): a[5,3] = a[{5*4+3}] = {a[5*4+3]}");
Console.WriteLine($"reshape(-1,6): → ({24/6},6) 自动推断");
// ===== Flatten (CNN→FC) =====
Console.WriteLine($"\nFlatten: (16,512,7,7) → (16, {512*7*7})");
// ===== Transpose =====
int[,] m = { {1,2,3},{4,5,6} }; // (2,3)
Console.Write("\n原(2×3): ");
for (int r = 0; r < 2; r++) { Console.Write("["); for (int c = 0; c < 3; c++) Console.Write($"{m[r,c]} "); Console.Write("] "); }
Console.Write("\n转(3×2): ");
for (int c = 0; c < 3; c++) { Console.Write("["); for (int r = 0; r < 2; r++) Console.Write($"{m[r,c]} "); Console.Write("] "); }
Console.WriteLine($"\nPyTorch→显示: (3,224,224) C,H,W → (224,224,3) H,W,C");
// ===== 广播 + BatchNorm =====
double[] feat = { 2.0, 4.0, 6.0, 8.0 };
double mu = feat.Average();
double v = feat.Select(x => (x-mu)*(x-mu)).Average();
Console.Write($"\n广播: (32,100) + (100,) = (32,100)");
Console.Write($"\nBatchNorm: μ={mu}, σ²={v}\n 标准化: ");
foreach (var x in feat) Console.Write($"{(x-mu)/Math.Sqrt(v+1e-5):F3} ");
Console.Write($"\n 缩放(γ=2,β=1): ");
foreach (var x in feat) Console.Write($"{2*((x-mu)/Math.Sqrt(v+1e-5))+1:F3} ");
Console.WriteLine($"\nBatchNorm 广播: (16,64,28,28) - (1,64,1,1)");
Rust
fn main() {
// ===== 不同阶的张量 =====
let _scalar = 3.14_f32; // 0阶
let vector = [1, 2, 3]; // 1阶
let matrix = [[1, 2], [3, 4]]; // 2阶
println!("标量 shape: ()");
println!("向量 shape: ({},)", vector.len());
println!("矩阵 shape: ({},{})", matrix.len(), matrix[0].len());
println!("3阶张量 shape: (3,224,224) — 彩色图片");
println!("4阶张量 shape: (32,3,224,224), 元素数: {}", 32*3*224*224);
// ===== Reshape:索引映射 =====
let a: [i32; 24] = core::array::from_fn(|i| i as i32);
println!("\nreshape(2,3,4): a[1,2,3] = a[{}] = {}", 1*12+2*4+3, a[1*12+2*4+3]);
println!("reshape(6,4): a[5,3] = a[{}] = {}", 5*4+3, a[5*4+3]);
println!("reshape(-1,6): → ({},6) 自动推断", 24/6);
// ===== as_chunks (Rust 1.88+): 平坦数组 → 块结构 =====
let (rows, _) = a.as_chunks::<4>();
println!("as_chunks(4): 共{}行, 第2行={:?}", rows.len(), rows[1]);
// ===== Flatten =====
println!("\nFlatten: (16,512,7,7) → (16, {})", 512*7*7);
// ===== Transpose =====
let m = [[1,2,3],[4,5,6]];
print!("\n原(2×3): ");
for r in 0..2 { print!("["); for c in 0..3 { print!("{} ", m[r][c]); } print!("] "); }
print!("\n转(3×2): ");
for c in 0..3 { print!("["); for r in 0..2 { print!("{} ", m[r][c]); } print!("] "); }
println!("\nPyTorch→显示: (3,224,224) C,H,W → (224,224,3) H,W,C");
// ===== 广播 + BatchNorm =====
let feat = [2.0, 4.0, 6.0, 8.0_f64];
let mut mu = 0.0; for i in 0..4 { mu += feat[i]; } mu /= 4.0;
let mut v = 0.0; for i in 0..4 { v += (feat[i]-mu).powi(2); } v /= 4.0;
print!("\n广播: (32,100) + (100,) = (32,100)");
print!("\nBatchNorm: μ={mu}, σ²={v}\n 标准化: ");
for i in 0..4 { print!("{:.3} ", (feat[i]-mu)/(v+1e-5).sqrt()); }
print!("\n 缩放(γ=2,β=1): ");
for i in 0..4 { print!("{:.3} ", 2.0_f64.mul_add((feat[i]-mu)/(v+1e-5).sqrt(), 1.0)); }
println!("\nBatchNorm 广播: (16,64,28,28) - (1,64,1,1)");
}