数学与算法基础-线性代数部分

92 阅读6分钟

矩阵分解(SVD)

对矩阵进行SVD分解,通式如下:

A=UΣVT=(orthogonal)(diagonal)(orthogonal)A = U \Sigma V^{T} = (orthogonal)(diagonal)(orthogonal)

orthogonal:正交矩阵

dagonal:对角阵

U:m×m{U}: m × m ,该矩阵每一个列向量都是{AA^T}的特征向量

Σ:m×n{\Sigma}: m × n, 将AAT{AA^T}或者ATA{A^TA}的特征值开根号,得到的就是该矩阵主对角线上的元素,也就是{A}的奇异值

V:n×n{V}: n × n ,该矩阵的每一个列向量都是ATA{A^TA}的特征向量

  • 因为没学过研究生的矩阵课程,所以不太懂这个

  • 但是看来也很好理解。

  • 就是对一个m×n m × n 的矩阵AA进行分解。

  • 1.先通过 AATAA^T变成一个m×mm×m的方阵,然后算出来对应的mm个特征向量

    • m个特征向量组成左边的矩阵U
  • 2.然后通过ATA A^TA变成一个n×nn×n的方阵,然后算出来对应的nn个特征向量

    • n个特征向量组成右边的的矩阵V
  • 3.算A的奇异值只需要把 AA^T特征值开根号即可

  • 奇异值总是非负的(要取绝对值再开根号)
  • 奇异值通常按从大到小排序

张量运算(einsum)

Einstein Summation:变量下标重复出现时,即可省略繁琐的求和符号。

i=1naibi=a1b1+a2b2+...+anbn\sum_{i=1}^{n}a_ib_i = a_1b_1 + a_2b_2+...+a_nb_n

由于上面公式中a的下标和b的下标是重复出现的,所以可以记为:

aibi=i=1naibia_ib_i = \sum_{i=1}^{n}a_ib_i

eg1:向量点乘
 C = np.einsum('i, i->', a, b)

"i,i->" 表示 a[i] * b[i],然后对 i 进行求和,最终返回一个标量。C=iaibiC = \sum_ia_ib_i

c=iaibi[i,i>]c = \sum_i a_ib_i → [i,i->]

eg2:向量外积
 C = np.einsum("i,j->ij", a, b)
 # 等价于numpy的outer函数
 numpy_outer = np.outer(a, b)

"i,j->ij":表示 a[i] * b[j],不对 i, j 求和,返回一个 ixj 矩阵。

Cij=aibj[i,j>ij]C_{ij} = a_ib_j → [i,j -> ij]

eg3:矩阵转置
 B = np.einsum("ij->ji", A)
 # 等价于 B = A.T

Bji=Aij[ij>ji]B_{ji} = A_{ij} → [ij->ji]

eg4:矩阵乘法
 C = np.einsum("ij,jk->ik", A, B)  # 矩阵乘法
 #等价于 
 C = np.matmul(A, B)  # 或 A @ B

上述公式对应的计算公式为C_{ik} = \sum_jA_{ij}B_{jk},矩阵A与B相乘。

Cik=jAijBjk[ij,jk>ik]C_{ik} = \sum_j A_{ij}B_{jk} → [ij,jk->ik]

eg5:批量矩阵乘法
 A = np.random.rand(32, 3, 4)  # batch_size=32, shape (32, 3, 4)
 B = np.random.rand(32, 4, 5)  # shape (32, 4, 5)
 ​
 C = np.einsum("bij,bjk->bik", A, B)  # 批量矩阵乘法
 # 等价于
 C = np.matmul(A, B)  # 适用于批量计算

Cbik=jAbijBbjk[bij,bjk>bik]C_{bik} = \sum_j A_{bij}B_{bjk} → [bij,bjk->bik]

  • 批量处理数据 1.一次处理多个样本(batch_size=32) 2.提高计算效率 3.充分利用GPU并行计算能力
einsum 在 Transformer 自注意力机制中的应用

在Transformer 的 自注意力(Self-Attention) 计算中,我们常需要执行如下计算:

Attention(Q,K,V)=softmax(QKTdk)V\mathrm{Attention}(Q,K,V)=\mathrm{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V

einsum 表示:

 import torch
 import torch.nn.functional as F
 def dot_product_attention(q, K, V):
     """计算点积注意力"""
     # 计算 QK^T
     logits = torch.einsum("k,mk->m", q, K)
     # 归一化
     weights = F.softmax(logits, dim=0)
     # 计算 softmax(QK^T) * V
     return torch.einsum("m,mv->v", weights, V)
 # 创建输入张量
 q = torch.tensor([1.0, 2.0])  # shape (2,)
 K = torch.tensor([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])  # shape (3, 2)
 V = torch.tensor([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])  # shape (3, 2)
 # 计算注意力输出
 output = dot_product_attention(q, K, V)
 print(output.numpy())  # [4.0, 5.0]

感觉上述代码略微会误导人,K是 (3,2) 的形式,K^T就应该是 (2,3) ,所以应该是

 K_T = torch.einsum("mk->km",K)
 logits = torch.einsum("k,km->m", q, K_T)  

现在的代码就表示这Cm=qkKkmTC_m=q_kK{^T_{km}}

不过现在看来是我的思路出问题了,einsum会自动处理....

einsum的自动处理机制:

k,mk->m 这个表达式中:

  1. k 表示第一个输入q的维度
  2. mk 表示第二个输入K的维度
  3. ->m 表示输出的维度 当einsum看到这个表达式时,它会:
  4. 发现需要把 mk 中的 k 和第一个输入的 k 对齐
  5. 自动把K中的维度重新排列,相当于完成了转置操作
  6. 然后执行乘法运算

以后写代码,读代码,只需要关注对应的维度即可。不需要想着转置啥的能不能相乘啥的。直接写即可。

这就是einsum的强大之处:它让我们可以专注于维度的对应关系,而不用关心具体的实现细节。这样代码更简洁,也更容易理解。

归一化
  1. Min-Max归一化:

xnormalized=(xxmin)/(xmaxxmin)x_{normalized} = (x - x_{min}) / (x_{max} - x_{min})

  • 把数据缩放到[0,1]区间
  • 保持原始数据的分布形状
  1. Z-Score标准化:

xnormalized=(xmean)/stdx_{normalized} = (x - mean) / std

  • 转换后数据均值为0,标准差为1
  • 适用于数据大致呈正态分布的情况
  1. L2归一化:

xnormalized=x/sqrt(sum(x2))x_{normalized} = x / sqrt(sum(x²))

  • 向量的长度变为1
  • 保持向量方向不变
  1. Softmax:(tf.nn.softmax(logits))

xnormalized=ex/sum(ex)x_{normalized} = e^x / sum(e^x)

  • 转换为概率分布(和为1)
  • 强调相对大小差异
  • 常用于注意力机制和分类问题

梯度计算(链式法则)

eg1: 举个栗子
 import torch
 ​
 # 创建张量,并设置 requires_grad=True 以追踪其计算历史
 x = torch.tensor(2.0, requires_grad=True) # 标记需要计算梯度
 y = torch.sin(x)     # 前向传播第一步
 z = y ** 2          # 前向传播第二步
     
 # 计算图中 y 的梯度
 z.backward()  # # 反向传播,计算 z 对 x 的梯度
 print(x.grad)  # 输出 x 的梯度
 ​

前向传播

  • 数据从输入层流向输出层
  • 就像示例中的 x → sin(x) → sin²(x) (x→ y → z)

反向传播

  • 梯度从输出层反向流向输入层
  • 使用链式法则计算每一层的梯度
  • 通过 backward() 自动完成这个过程

梯度的作用

  • 指示参数需要调整的方向和大小
  • 帮助模型学习和优化
  • 最终目的是减小损失函数的值

torch.Tensor:

  • Tensor 是 PyTorch 中存储数据和定义计算图的基础数据结构。默认情况下,所有的张量(Tensor)都不会自动追踪计算的历史。
  • 如果要使张量参与计算图并能够进行自动求导,需要在创建张量时设置 requires_grad=True。

backward():

  • 调用张量的 backward() 方法,PyTorch 会自动计算该张量的所有依赖张量的梯度,并存储在各自的 .grad 属性中。
  • backward() 只接受标量张量(一个数值),如果不是标量张量,通常会传递一个与张量形状匹配的梯度参数。

torch.no_grad():

  • 在评估模型或推理时,我们不需要计算梯度,可以使用 torch.no_grad() 以节省内存和计算资源。

更多细节[来源blog.csdn.net/wahahaha116…]

  • 梯度累积与清零:每次调用 backward(),梯度会累积(即,累加到 .grad 属性中),因此在每次新的梯度计算之前通常需要清零现有的梯度,例如通过 optimizer.zero_grad()。
  • 多次反向传播:如果在同一个计算图上进行多次反向传播(例如在 RNN 中),需要设置 retain_graph=True,以防止计算图被释放。
eg2: 详细介绍

梯度求导的基本步骤

  • 定义计算图:

每当对 torch.Tensor 进行操作时,PyTorch 会动态地创建一个计算图来记录操作。

如果 Tensor 的 requires_grad 属性设置为 True,那么该张量会开始追踪其上的所有操作,这样就可以调用 backward() 来自动计算其梯度。

  • 前向传播(Forward Pass):

计算图的构建是在前向传播过程中完成的。

在前向传播过程中,输入数据通过神经网络的各层进行计算,最终生成输出。

  • 计算损失(Loss Calculation):

通常情况下,在前向传播结束后会计算损失函数(Loss),这是一个标量值,用于评估模型的输出与目标之间的差距。

  • 反向传播(Backward Pass):

调用损失张量的 backward() 方法。反向传播通过链式法则计算损失函数相对于每个叶子节点(即,所有具有 requires_grad=True 的张量)的梯度。

  • 更新参数(Parameter Update):

使用优化器(如 SGD、Adam 等)通过梯度下降或其他优化算法更新模型的参数。