张量与高维数组

深度学习中所有数据都以张量(多维数组)的形式存储和计算。一张 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)= 需要几个下标来定位一个元素。

二、深度学习中常见的张量形状

数据类型张量形状每个维度含义具体示例
灰度图片(单张)(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):青绿色
整张图从左到右,红色渐弱,绿色渐强——颜色从红到绿的渐变。
示例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 = 嵌入维度

四、张量的基本操作

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
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)

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 维度

4.3 广播机制(Broadcasting)

当两个不同形状的张量做逐元素运算时,自动扩展较小张量使其形状匹配。

广播规则:从最后一个维度向前逐一比较:
① 维度大小相同 → 直接运算
② 其中一个维度大小=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]]
广播在 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。

五、张量在实际模型中的流动

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 个参数
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 万亿)

六、代码验证(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)");
}

已阅读当前小节,可返回首页继续浏览其它主题。