常见算子

中间层 (Intermediate Layers)

Conv (卷积层)

卷积层是计算机视觉中的核心算子,用于提取图像特征。常用的是二维卷积 nn.Conv2d。

原理

通过一个可学习的卷积核(Kernel/Filter)在输入特征图上滑动,进行点积求和(互相关运算),从而提取局部特征。

PyTorch 实现

torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros', device=None, dtype=None)

参数详解

  • in_channels (int): 输入特征图的通道数。例如 RGB 图像为 3。
  • out_channels (int): 输出特征图的通道数(即卷积核的个数)。
  • kernel_size (int or tuple): 卷积核的大小。如 3 代表 3x3,(3, 5) 代表 3x5。
  • stride (int or tuple): 卷积核滑动的步长。默认为 1。步长 > 1 会导致输出尺寸减小(下采样)。
  • padding (int, tuple or str): 输入周边的填充层数。常用于保持输出尺寸不变(如 same padding)。
    • 默认为 0。
    • 'valid': 不填充。
    • 'same': 填充使得输出尺寸与输入尺寸(在 stride=1 时)一致。
  • dilation (int or tuple): 空洞卷积(Dilated Convolution)的膨胀率。默认为 1(普通卷积)。用于扩大感受野。
  • groups (int): 分组卷积。默认为 1。
    • groups=1: 普通卷积。
    • groups=in_channels 且 out_channels=in_channels: 深度卷积 (Depthwise Convolution)。
  • bias (bool): 是否添加偏置参数。默认为 True。如果卷积后接 BatchNorm,通常设为 False(因为 BN 有 affine 参数,Bias 冗余)。

输出尺寸计算公式

设输入尺寸 $(H_{in}, W_{in})$,输出尺寸 $(H_{out}, W_{out})$:

$$ H_{out} = \left\lfloor \frac{H_{in} + 2 \times \text{padding}[0] - \text{dilation}[0] \times (\text{kernel\_size}[0] - 1) - 1}{\text{stride}[0]} + 1 \right\rfloor $$

Example

import torch
import torch.nn as nn

# 1. 基础卷积: 保持尺寸不变 (Same Padding)
# Input: [Batch, 3, 32, 32] -> Output: [Batch, 16, 32, 32]
# Padding = (Kernel - 1) / 2 = 1
conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1)
x = torch.randn(1, 3, 32, 32)
out = conv(x)
print(f"Shape: {out.shape}") # torch.Size([1, 16, 32, 32])

# 2. 下采样 (Stride=2)
# Input: [Batch, 16, 32, 32] -> Output: [Batch, 32, 16, 16]
conv_down = nn.Conv2d(16, 32, kernel_size=3, stride=2, padding=1)
out_down = conv_down(out)
print(f"Downsampled Shape: {out_down.shape}") # torch.Size([1, 32, 16, 16])

# 3. 深度可分离卷积 (Depthwise Separable Conv)
# 分两步:Depthwise (groups=in_channels) + Pointwise (1x1 conv)
dw_conv = nn.Conv2d(16, 16, kernel_size=3, groups=16) # Depthwise
pw_conv = nn.Conv2d(16, 32, kernel_size=1)             # Pointwise

FLOPs 与 MACs

评估一个卷积层「有多贵」时,最常用的两个量是 MACs 和 FLOPs。它们的定义不同,倍数差 2 倍,是经典且容易混淆的概念。

公式为什么是 $H_o \times W_o \times C_{out} \times C_{in} \times k_h \times k_w$?

该公式计算的实际上是 MACs (Multiply-Accumulate Operations, 乘加次数),不是 FLOPs。可以用"自底向上"拆解:

1) 算输出特征图上「一个像素、一个通道」的值 在输出特征图第 $c_{out}$ 通道、位置 $(h_o, w_o)$ 处,需要把卷积核与输入特征图对应的滑动窗口做点积:

  • 卷积核局部空间大小:$k_h \times k_w$
  • 对应输入通道数:$C_{in}$

局部 3D 窗口内共有 $C_{in} \times k_h \times k_w$ 个元素。要算出这一个输出值:

  • 乘法:$C_{in} \times k_h \times k_w$ 次(权重 × 输入像素)
  • 加法:$C_{in} \times k_h \times k_w - 1$ 次(乘积累加);若考虑 Bias 再 +1,则加法次数恰好也是 $C_{in} \times k_h \times k_w$(详见「带 Bias 的细节」)

一个"乘法 + 加法"组合 = 一个 MAC。所以:

$$ \text{单个输出元素的 MACs} = C_{in} \times k_h \times k_w $$

2) 推广到整张输出特征图 输出形状 $[C_{out}, H_o, W_o]$,总元素个数 = $H_o \times W_o \times C_{out}$,乘上去得整层 MACs:

$$ \boxed{\text{Total MACs} = H_o \times W_o \times C_{out} \times C_{in} \times k_h \times k_w} $$

带 Bias 的细节:求和需要 $C_{in} k_h k_w - 1$ 次加法,加上 Bias 的 1 次加法 = $C_{in} k_h k_w$ 次加法,与乘法次数相同。所以 1 元素 = 1 MAC,无 Bias 时则略少于 1 MAC(差一次加法),工程上常忽略这个 -1。

MAC vs FLOP:1 MAC = 1 FLOP 还是 2 FLOPs?

两个概念的定义:

  • FLOP (Floating Point Operation):任意一次浮点运算(加/减/乘/除),单位是 “次”。
  • MAC / MACC (Multiply-Accumulate):一次乘加,$y = a \cdot b + c$,包含 1 次乘法 + 1 次加法 = 2 次 FLOPs。

按此严格定义:

$$ \boxed{\text{Total FLOPs} = 2 \times H_o \times W_o \times C_{out} \times C_{in} \times k_h \times k_w} $$

主流深度学习工具(PyTorch fvcore、DeepSpeed、thop 等)都采用这个标准,统计时会在 MACs 基础上 × 2。

那为什么有人会"1 MAC = 1 FLOP"?

主要有三个原因:

  1. 硬件的 FMA 指令 (Fused Multiply-Add, 融合乘加) 现代 GPU/TPU 与支持 AVX-512、AMX 的 CPU 都有专用 FMA 执行单元,单条指令、单个周期内同时完成"乘 + 加"。从硬件视角看,1 MAC 是一条不可分割的指令,因此在系统/硬件层面常被"宽松地"记作 1 次操作。

  2. 历史研究习惯 早期 CPU 上乘法比加法慢得多,乘法是瓶颈,研究者更关心乘法次数。论文里直接用 MACs 当 FLOPs 粗估——只要全文口径一致,相对量级是对的,不影响模型间复杂度比较。

  3. 标准分歧 + 商业考量 算法工程师写代码时常图省事,把代码里数出的 MAC 数直接冠以 FLOPs 之名;硬件厂商宣传 TFLOPS 时又倾向用 1 MAC = 2 FLOPs 的标准(这样显卡算力数字能翻倍,更好看)。

总结

项公式 / 定义
Conv2d 单层 MACs$H_o W_o C_{out} C_{in} k_h k_w$
Conv2d 单层 FLOPs (严格)$2 \times H_o W_o C_{out} C_{in} k_h k_w$
换算 (标准)1 MAC = 2 FLOPs
工具 (fvcore / thop / DeepSpeed)报 FLOPs 时默认 × 2

汇报建议:需要严谨的浮点运算量时,把 MACs × 2 作为 FLOPs。引用别人论文时先看清对方用的是 MAC 还是 FLOP(差 2 倍常数)。

Example:用 thop 实测

import torch
import torch.nn as nn
from thop import profile

conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1)
x = torch.randn(1, 3, 32, 32)

# profile 返回: (FLOPs, params)
macs, params = profile(conv, inputs=(x,), verbose=False)
flops = 2 * macs  # thop 实际返回的就是 MACs,乘 2 得严格 FLOPs
print(f"MACs  = {macs:,}   ({macs/1e6:.2f} M)")
print(f"FLOPs = {flops:,}   ({flops/1e6:.2f} M)")
print(f"Params= {params:,}")

# 手算: 32 * 32 * 16 * 3 * 3 * 3 = 1,327,104 MACs

Matmul / Linear (全连接层 & 矩阵乘法)

用于特征的线性变换或分类头。

1. torch.matmul

  • 原理: 矩阵乘法。支持广播机制(Broadcasting)。
  • 用法: torch.matmul(input, other) 或 @ 运算符。

Example:

import torch
# 2D 矩阵乘法
# [B, N] x [N, M] -> [B, M]
A = torch.randn(10, 20)
B = torch.randn(20, 30)
C = torch.matmul(A, B) # Size: [10, 30]

# 3D Batch Matmul (常见于 Attention)
# [B, H, W] x [B, W, K] -> [B, H, K]
x = torch.randn(2, 3, 4)
y = torch.randn(2, 4, 5)
z = torch.matmul(x, y) # Size: [2, 3, 5]

2. nn.Linear

  • 原理: 对输入进行线性变换 $y = xA^T + b$。
  • 参数详解:
    • in_features: 输入样本的大小。
    • out_features: 输出样本的大小。
    • bias: 是否包含偏置。默认为 True。

Example:

import torch.nn as nn
# Input: [Batch, 512] -> Output: [Batch, 10] (例如分类任务)
fc = nn.Linear(in_features=512, out_features=10)
x = torch.randn(32, 512)
out = fc(x)
print(out.shape) # torch.Size([32, 10])

Pooling (池化层)

用于降维,减少参数量,扩大感受野。

  • nn.MaxPool2d (最大池化): 取窗口内的最大值。保留最显著的特征(如纹理)。
  • nn.AvgPool2d (平均池化): 取窗口内的平均值。保留背景等整体特征。

参数详解

用于 nn.MaxPool2d(kernel_size, stride=None, padding=0, ...)

  • kernel_size: 窗口大小。
  • stride: 步长。默认等于 kernel_size。

Example:

# Input: [1, 64, 32, 32]
pool = nn.MaxPool2d(kernel_size=2, stride=2)
# Output: [1, 64, 16, 16] (尺寸减半)

输出层 (Output Layers)

Softmax

常规

将 N 个神经元的输出转化为预测概率分布。

原理

公式:

$$ \text{Softmax}(x_{i}) = \frac{\exp(x_i)}{\sum_j \exp(x_j)} $$

保证了所有输出值在 (0, 1) 之间,且总和为 1。

PyTorch 实现

torch.nn.Softmax(dim=None)

  • dim (int): 指定进行归一化的维度。
    • 对于二维输入 [Batch, Classes],通常 dim=1。
import torch
import torch.nn as nn

softmax = nn.Softmax(dim=1)
logits = torch.tensor([[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]])
probs = softmax(logits)

print(probs)
# tensor([[0.0900, 0.2447, 0.6652],
#         [0.0900, 0.2447, 0.6652]])
print(probs.sum(dim=1)) 
# tensor([1., 1.])

注意事项

  • 数值稳定性: 直接计算 $\exp(x)$ 容易溢出。PyTorch 的 CrossEntropyLoss 内部结合了 LogSoftmax 和 NLLLoss,因此训练分类模型时,模型的最后一层不需要加 Softmax,直接输出 logits 即可。

LLM中的Tempreature

原理

在 LLM (大语言模型) 的生成过程中 (Sampling),Temperature 是一个非常关键的超参数,它直接作用于 Softmax 之前的 logits。

公式变化为:

$$ p_i = \frac{\exp(x_i / T)}{\sum_j \exp(x_j / T)} $$

其中 $T$ 是温度系数 (Temperature)。

  • T > 1 (高温):
    • Logits 之间的差异被缩小 (除以大数)。
    • 分布变得更平坦 (接近均匀分布)。
    • 效果: 模型更倾向于选择非最高概率的词,生成结果更具随机性、多样性和创造力。
  • T < 1 (低温):
    • Logits 之间的差异被放大 (除以小数)。
    • 分布变得更尖锐 (强者更强)。
    • 效果: 模型更倾向于选择概率最高的词,生成结果更确定、保守和准确。
  • T = 1: 标准 Softmax。

PyTorch 实现

import torch
import torch.nn.functional as F

logits = torch.tensor([1.0, 2.0, 4.0])

# 1. T = 1.0 (Standard)
probs_std = F.softmax(logits / 1.0, dim=0)
print(f"T=1.0: {probs_std.numpy()}") 
# T=1.0: [0.042 0.114 0.844] (正常分布)

# 2. T = 0.5 (Low Temperature - More Deterministic)
probs_low = F.softmax(logits / 0.5, dim=0)
print(f"T=0.5: {probs_low.numpy()}")
# T=0.5: [0.002 0.018 0.980] (差异放大,几乎只选最大的)

# 3. T = 2.0 (High Temperature - More Random)
probs_high = F.softmax(logits / 2.0, dim=0)
print(f"T=2.0: {probs_high.numpy()}")
# T=2.0: [0.155 0.256 0.589] (差异缩小,概率更平均)