Tensor Operations —— 向量、矩阵与张量运算
一、前言:AI的“数据语言”
在深度学习的日常工作中,我们每天都在和张量打交道——输入是一批图片(四维张量),经过卷积层变成新的四维张量,经过全连接层压成二维矩阵,再通过激活函数做逐元素运算……可以说,张量运算是AI模型的“母语” 。
但很多初学者对张量的理解停留在“高维数组”的模糊印象上,对reshape和transpose的区别、@和*的不同用途、范数的几何含义、SVD为什么能压缩数据等问题缺乏系统认知。
本文将从五个方面,带你建立对向量、矩阵与张量运算的系统理解:
- 张量的维度与Shape变换
- 矩阵乘法(
@)与Hadamard积(*)的区别 - 范数(L1/L2/Frobenius)的几何意义
- 特征分解的直观理解
- 奇异值分解(SVD)的降维与数据压缩视角
二、张量的维度与Shape变换
2.1 什么是张量?
张量(Tensor)是深度学习中数据的基本容器。你可以把它理解为多维数组的统称:
- 0阶张量:标量(一个数),如
3.14 - 1阶张量:向量(一维数组),如
[1, 2, 3] - 2阶张量:矩阵(二维数组),如一张灰度图
- 3阶张量:如一张RGB彩色图(高×宽×通道)
- 4阶张量:如一个批次的RGB图片(批次×高×宽×通道)
张量的形状(Shape) 是一个元组,描述了每个维度的大小。例如 shape=(3, 4) 表示一个3行4列的矩阵。
1import torch2import numpy as np3
4# 创建各种张量5scalar = torch.tensor(3.14) # 0阶:标量6vector = torch.tensor([1, 2, 3]) # 1阶:向量7matrix = torch.tensor([[1, 2], [3, 4]]) # 2阶:矩阵8tensor_3d = torch.randn(2, 3, 4) # 3阶:形状(2,3,4)9
10print(f"标量形状: {scalar.shape}") # torch.Size([])11print(f"向量形状: {vector.shape}") # torch.Size([3])12print(f"矩阵形状: {matrix.shape}") # torch.Size([2, 2])13print(f"3D张量形状: {tensor_3d.shape}") # torch.Size([2, 3, 4])2.2 Shape变换的核心操作
形状变换是指在不改变张量数据内容的情况下,改变其维度和每个维度的大小。理解这些操作的关键,在于搞清楚存储(Storage) 与视图(View) 的区别:
- 存储:数据在内存中实际存放的连续区域
- 视图:我们“看待”这段内存的方式(通过shape和stride元数据来定义)
1# 创建一个连续存储的张量2x = torch.arange(12)3print(f"原始数据: {x}") # tensor([0, 1, 2, ..., 11])4
5# reshape:改变视图,数据在内存中的顺序不变6x_reshaped = x.reshape(3, 4)7print(f"reshape后:\n{x_reshaped}")8# tensor([[ 0, 1, 2, 3],9# [ 4, 5, 6, 7],10# [ 8, 9, 10, 11]])(1)reshape —— 重塑形状
reshape是最常用的形状变换函数,它不改变元素数量和元素值,只改变张量的逻辑形状。
1# 将24个元素重塑为不同形状2x = torch.arange(24)3
4# 变成 2×3×45a = x.reshape(2, 3, 4)6print(a.shape) # torch.Size([2, 3, 4])7
8# 使用 -1 让PyTorch自动计算该维度大小9b = x.reshape(2, 3, -1)10print(b.shape) # torch.Size([2, 3, 4]),-1自动算为411
12# 展平为一维13c = x.reshape(-1)14print(c.shape) # torch.Size([24])关键点:reshape要求新形状的元素总数与原张量相同。-1是一个便利写法,表示“自动计算这个维度的大小”。
(2)transpose 与 permute —— 交换维度
transpose用于交换两个指定的维度,permute可以重新排列任意数量的维度。
1# 创建一个3维张量2x = torch.randn(2, 3, 4) # shape: (2, 3, 4)3
4# transpose:交换维度0和维度15y = x.transpose(0, 1)6print(y.shape) # torch.Size([3, 2, 4])7
8# permute:重新排列所有维度9z = x.permute(2, 0, 1)10print(z.shape) # torch.Size([4, 2, 3])重要区别:reshape只是改变“看待数据的方式”,而transpose/permute会改变维度之间的逻辑关系。经过transpose后,张量在内存中变得不连续(non-contiguous) ,此时如果再调用reshape,PyTorch会先拷贝数据使其连续。
1x = torch.arange(12).reshape(3, 4)2print(x)3# tensor([[ 0, 1, 2, 3],4# [ 4, 5, 6, 7],5# [ 8, 9, 10, 11]])6
7# transpose后,内存布局改变了8x_t = x.transpose(0, 1)9print(x_t)10# tensor([[ 0, 4, 8],11# [ 1, 5, 9],12# [ 2, 6, 10],13# [ 3, 7, 11]])14
15print(x_t.is_contiguous()) # False(3)squeeze 与 unsqueeze —— 压缩与扩展维度
squeeze:删除所有大小为1的维度unsqueeze:在指定位置插入一个大小为1的维度
1x = torch.randn(1, 3, 1, 4)2
3# squeeze:删除所有尺寸为1的维度4y = x.squeeze()5print(y.shape) # torch.Size([3, 4])6
7# unsqueeze:在指定位置插入维度8z = torch.tensor([1, 2, 3])9w = z.unsqueeze(0) # 在第0维插入10print(w.shape) # torch.Size([1, 3])2.3 实战:图像数据的Shape变换
在计算机视觉中,图像数据经常需要在不同形状间转换:
1# 假设有一批RGB图像:batch_size=32, 通道数=3, 高=224, 宽=2242images = torch.randn(32, 3, 224, 224)3
4# 展平为特征向量(用于全连接层)5flattened = images.reshape(32, -1)6print(flattened.shape) # torch.Size([32, 150528])7
8# 交换通道维度和宽高维度(某些框架需要)9# 从 (B, C, H, W) 变为 (B, H, W, C)10images_chw = images.permute(0, 2, 3, 1)11print(images_chw.shape) # torch.Size([32, 224, 224, 3])三、矩阵乘法(@)与Hadamard积(*)的区别
3.1 Hadamard积(逐元素乘法)
Hadamard积(Hadamard Product) ,也叫逐元素乘法(Element-wise Multiplication) 或Schur积,是两个同型矩阵对应位置元素相乘:
(A∘B)ij=aij⋅bij
在Python中,*运算符默认执行的就是Hadamard积。
1import numpy as np2
3A = np.array([[1, 2], [3, 4]])4B = np.array([[5, 6], [7, 8]])5
6# Hadamard积(逐元素乘法)7C = A * B8print("Hadamard积 (A * B):")9print(C)10# [[ 5 12]11# [21 32]]12# 验证:1×5=5, 2×6=12, 3×7=21, 4×8=32Hadamard积具有交换律和结合律,这是它与普通矩阵乘法的关键区别之一。
3.2 矩阵乘法(普通乘积)
矩阵乘法(也叫点积、matmul)遵循“左行乘右列”的规则:
(AB)ij=∑k=1naik⋅bkj
要求:A的列数 = B的行数。结果的形状为 (A的行数, B的列数)。
1# 矩阵乘法(使用 @ 运算符)2D = A @ B3print("矩阵乘法 (A @ B):")4print(D)5# [[19 22]6# [43 50]]7# 验证:19 = 1×5 + 2×7, 22 = 1×6 + 2×88# 43 = 3×5 + 4×7, 50 = 3×6 + 4×89
10# 形状规则11A2 = np.ones((2, 3))12B2 = np.ones((3, 4))13C2 = A2 @ B214print(C2.shape) # (2, 4)在Python中,矩阵乘法可以通过三种方式实现:
@运算符(Python 3.5+引入,最推荐)np.matmul()函数np.dot()函数(对二维数组等同于矩阵乘法,但对高维数组行为不同)
3.3 关键区别总结
| 特性 | Hadamard积(*) | 矩阵乘法(@) |
|---|---|---|
| 运算规则 | 对应元素相乘 | 行×列求和 |
| 形状要求 | 必须完全相同 | A的列数 = B的行数 |
| 结果形状 | 与原矩阵相同 | (A的行数, B的列数) |
| 交换律 | ✅ 成立 | ❌ 不成立 |
| AI中的典型用途 | 注意力权重、门控机制 | 全连接层、卷积层 |
1# 直观对比2A = np.array([[1, 2], [3, 4]])3B = np.array([[5, 6], [7, 8]])4
5print("A * B (Hadamard):")6print(A * B)7# [[ 5 12]8# [21 32]]9
10print("A @ B (矩阵乘法):")11print(A @ B)12# [[19 22]13# [43 50]]在神经网络中,全连接层 y = Wx + b 使用的是矩阵乘法(@),而注意力机制中的权重计算、门控循环单元中的门控信号则常使用Hadamard积(*)。
四、范数(L1/L2/Frobenius)的几何意义
范数(Norm) 的本质是度量向量或矩阵“大小”的工具。不同的范数从不同角度衡量“大小”,在机器学习中各有用途。
4.1 L1范数:曼哈顿距离
L1范数定义为向量各元素绝对值之和:
∥x∥1=∑i=1n∣xi∣
几何意义:L1范数对应曼哈顿距离(Manhattan Distance) ——就像在曼哈顿街区行走,只能沿着街道直角转弯,距离是各段路程的绝对值之和。
1import torch2
3v = torch.tensor([3.0, -4.0])4
5# L1范数 = |3| + |-4| = 76l1_norm = torch.linalg.norm(v, ord=1)7print(f"L1范数: {l1_norm.item()}") # 7.0为什么L1范数能产生稀疏解? 在二维平面上,L1范数的“单位球”是一个菱形(顶点在坐标轴上)。当用L1范数做正则化时,优化问题的解更容易落在坐标轴上——这意味着某些维度的权重被压缩为0,从而实现特征选择。这就是Lasso回归使用L1正则化的原因。
4.2 L2范数:欧几里得距离
L2范数定义为向量各元素平方和的平方根:
∥x∥2=∑i=1nxi2
几何意义:L2范数就是欧几里得距离(Euclidean Distance) ——从原点出发到目标点的直线距离。
1# L2范数 = sqrt(3² + (-4)²) = 52l2_norm = torch.linalg.norm(v, ord=2)3print(f"L2范数: {l2_norm.item()}") # 5.0L2范数的平方有一个常用形式:∥x∥22=xTx。在机器学习中,L2正则化(岭回归/权重衰减)就是惩罚权重的L2范数平方,倾向于让所有权重都较小但非零,从而防止过拟合。
4.3 Frobenius范数:矩阵的“L2范数”
Frobenius范数是L2范数在矩阵上的推广——将矩阵所有元素的平方和开根号:
∥A∥F=∑i=1m∑j=1n∣aij∣2
从另一个角度看,如果把矩阵的所有列“堆叠”成一个长向量,Frobenius范数就是这个长向量的L2范数。
1M = torch.tensor([[1.0, 2.0], [3.0, 4.0]])2
3# Frobenius范数 = sqrt(1² + 2² + 3² + 4²) = sqrt(30) ≈ 5.4774fro_norm = torch.linalg.norm(M, ord='fro')5print(f"Frobenius范数: {fro_norm.item():.3f}") # 5.477Frobenius范数的几何意义:它衡量的是矩阵作为一个整体在“欧几里得空间”中的长度。在矩阵低秩近似中(如SVD截断),我们通常用Frobenius范数来衡量重建误差——保留的奇异值越多,重建误差(Frobenius范数)越小。
4.4 三种范数的直观对比
| 范数 | 公式 | 几何意义 | 机器学习用途 |
|---|---|---|---|
| L1 | $\sum | x_i | $ |
| L2 | ∑xi2 | 欧几里得距离 | 岭回归、权重衰减 |
| Frobenius | ∑aij2 | 矩阵的“长度” | 矩阵近似、SVD重建误差 |
五、特征分解(Eigenvalue Decomposition)的直观理解
5.1 特征向量与特征值:线性变换的“主轴”
特征分解(Eigendecomposition)是将一个方阵分解为特征向量和特征值的操作。
核心思想:一个矩阵 A 代表一个线性变换。绝大多数向量经过这个变换后,方向和长度都会改变。但存在一些特殊的向量,它们的方向在变换后保持不变,只是长度被拉伸或压缩了。
这些特殊的向量就是特征向量(Eigenvector) ,拉伸或压缩的倍数就是特征值(Eigenvalue) :
Av=λv
其中 v 是特征向量,λ 是特征值。
5.2 特征分解的公式
如果一个 n×n 的方阵 A 有 n 个线性无关的特征向量,它可以被分解为:
A=QΛQ−1
其中:
- Q 是特征向量组成的矩阵(每一列是一个特征向量)
- Λ 是对角矩阵,对角线上的元素是特征值
5.3 几何直觉:特征分解 = “换基 → 缩放 → 换回”
想象一个线性变换 A 作用于空间中的向量:
- 换基:用特征向量作为新的坐标系(乘以 Q−1)
- 缩放:在新坐标系下,变换只是沿各坐标轴做拉伸/压缩(乘以 Λ)
- 换回原基:将结果转回原来的坐标系(乘以 Q)
1import numpy as np2
3# 创建一个对称矩阵(保证可特征分解)4A = np.array([[4, 1], [1, 3]])5
6# 特征分解7eigenvalues, eigenvectors = np.linalg.eig(A)8
9print("特征值:", eigenvalues) # [4.618, 2.382]10print("特征向量:\n", eigenvectors)11# 每一列是一个特征向量12
13# 验证:A @ v = λ * v14v = eigenvectors[:, 0]15lam = eigenvalues[0]16print("A @ v:", A @ v)17print("λ * v:", lam * v)18# 两者应该相等特征值的大小表示“重要性” :特征值越大,对应的特征向量方向在变换中越重要。这就是为什么PCA(主成分分析)会选择最大特征值对应的特征向量作为主成分——它们代表了数据中方差最大的方向。
5.4 特征分解的局限
特征分解要求矩阵必须是方阵。但在实际数据中,我们经常遇到非方阵的数据矩阵(如 m 个样本 × n 个特征,m=n)。这时候就需要奇异值分解(SVD) 出场了。
六、奇异值分解(SVD):降维与数据压缩的利器
6.1 从特征分解到SVD
特征值分解的局限:只有方阵才能做特征分解。
SVD的突破:任意矩阵(无论是否方阵)都可以进行奇异值分解。
6.2 SVD的数学形式
任意 m×n 的矩阵 A 都可以分解为:
A=UΣVT
其中:
- U:m×m 的正交矩阵,列向量称为左奇异向量
- Σ:m×n 的对角矩阵,对角线上的值称为奇异值(按从大到小排列)
- V:n×n 的正交矩阵,列向量称为右奇异向量
1from scipy.linalg import svd2
3# 任意非方阵矩阵4A = np.array([[1, 2, 3],5 [4, 5, 6],6 [7, 8, 9],7 [10, 11, 12]]) # shape: (4, 3)8
9U, S, Vt = svd(A)10
11print("U的形状:", U.shape) # (4, 4)12print("S(奇异值):", S) # [25.46, 1.72, 0.00]13print("Vt的形状:", Vt.shape) # (3, 3)6.3 几何直觉:SVD = “旋转 → 缩放 → 再旋转”
从几何角度看,任何矩阵 A 的线性变换效果都可以分解为三个步骤:
- 旋转/反射(VT):在原始空间中旋转向量
- 沿坐标轴缩放(Σ):在各方向上做拉伸或压缩
- 旋转/反射(U):在目标空间中旋转
奇异值 σi 就是第 i 个方向上的缩放倍数。奇异值越大,说明这个方向在变换中越重要。
6.4 SVD的核心价值:低秩近似
SVD最强大的应用在于低秩近似(Low-Rank Approximation) ——用少数几个最大的奇异值及其对应的奇异向量来近似原始矩阵。
A≈Ak=∑i=1kσiuiviT
其中 k 远小于矩阵的秩。保留的奇异值越多,近似越精确;保留的越少,压缩率越高。
6.5 实战:用SVD压缩图像
这是理解SVD数据压缩能力最直观的例子。
1import numpy as np2import matplotlib.pyplot as plt3from scipy.linalg import svd4from PIL import Image5
6# 加载灰度图像并转为矩阵7# 这里用随机数据模拟,实际使用时替换为真实图像8# img = Image.open('image.jpg').convert('L')9# A = np.array(img, dtype=float)10
11# 为演示,创建一个有结构的矩阵(模拟图像)12np.random.seed(42)13A = np.random.randn(200, 200)14# 加一些结构使其更像图像15A = A @ A.T # 使其具有低秩结构16
17def svd_compress(A, k):18 """用SVD将矩阵A压缩到k个奇异值"""19 U, S, Vt = svd(A, full_matrices=False)20 # 只保留前k个奇异值21 S_k = np.diag(S[:k])22 U_k = U[:, :k]23 Vt_k = Vt[:k, :]24 # 重建矩阵25 A_k = U_k @ S_k @ Vt_k26 return A_k, S27
28# 分别用不同数量的奇异值重建29ks = [5, 20, 50, 100, 200]30fig, axes = plt.subplots(1, len(ks), figsize=(15, 3))31
32for i, k in enumerate(ks):33 A_k, S = svd_compress(A, k)34 axes[i].imshow(A_k, cmap='gray')35 axes[i].set_title(f'k={k}')36 axes[i].axis('off')37
38plt.tight_layout()39plt.show()40
41# 计算压缩比42def compression_ratio(m, n, k):43 """SVD压缩的存储压缩比"""44 # 原始存储: m*n45 # 压缩后存储: U(m*k) + Sigma(k) + Vt(k*n)46 compressed = m * k + k + k * n47 original = m * n48 return compressed / original49
50print(f"原始大小: {200*200} = 40000")51print(f"k=20时压缩后大小: {compression_ratio(200, 200, 20):.2%}")52# 输出类似: 19.90% —— 只需约20%的存储空间!SVD图像压缩的直观理解:
- 奇异值从大到小排列,前几个奇异值往往占了总能量的绝大部分
- 保留前 k 个奇异值重建图像,相当于用少数几个“特征模式”来近似整张图
- k 越小,压缩率越高,但图像越模糊;k 越大,图像越清晰,但存储越大
实际案例中,保留前100个奇异值(k=100)重建的图像与原图几乎无法区分,但存储空间只需要约12%。
6.6 SVD vs 特征分解:谁更通用?
| 特性 | 特征分解(EVD) | 奇异值分解(SVD) |
|---|---|---|
| 适用矩阵 | 仅限方阵 | 任意矩阵 |
| 分解结果 | A=QΛQ−1 | A=UΣVT |
| 正交性 | 特征向量不一定正交 | 奇异向量一定正交 |
| 数值稳定性 | 对病态矩阵敏感 | 数值稳定性更好 |
| PCA实现 | 对协方差矩阵做EVD | 直接对数据矩阵做SVD |
在实际工程中,PCA通常用SVD实现而非特征分解,因为SVD直接作用于数据矩阵,数值更稳定,且适用于大规模数据。
七、总结
回顾本文的核心脉络:
-
张量是AI数据的通用容器,理解
shape、reshape、transpose、squeeze等操作的本质——存储与视图的分离——是高效处理数据的基础。 -
矩阵乘法(
@) 与Hadamard积(*) 服务于完全不同的目的:前者是“特征混合”(全连接层),后者是“特征调制”(注意力机制)。 -
范数是衡量“大小”的尺子:L1是曼哈顿距离(产生稀疏),L2是欧几里得距离(权重衰减),Frobenius是矩阵的“L2长度”(衡量重建误差)。
-
特征分解将方阵分解为“方向×重要性”,是理解线性变换“主轴”的窗口。
-
SVD是特征分解的终极推广——适用于任意矩阵,是降维(PCA)、数据压缩(图像压缩)、推荐系统的数学基石。
这些概念不是孤立的数学公式,而是贯穿AI模型设计、训练、部署全流程的思维工具。理解它们,你就能真正“读懂”深度学习模型的每一个运算。
Some information may be outdated