Daily AI 20251217 (PyTorch基础回顾2)

AgenticCoding·十二月创作之星挑战赛 10w+人浏览 422人参与

CNN

  • 输入图像:网络接收的原始图像数据。
  • 卷积(Convolution):使用卷积核(Kernel)在输入图像上滑动,提取特征,生成特征图(Feature Maps)。
  • 池化(Pooling):通常在卷积层之后,通过最大池化或平均池化减少特征图的尺寸,同时保留重要特征,生成池化特征图(Pooled Feature Maps)。
  • 特征提取(Feature Extraction):通过多个卷积和池化层的组合,逐步提取图像的高级特征。
  • 展平层(Flatten Layer):将多维的特征图转换为一维向量,以便输入到全连接层。
  • 全连接层(Fully Connected Layer):类似于传统的神经网络层,用于将提取的特征映射到输出类别。
  • 分类(Classification):网络的输出层,根据全连接层的输出进行分类。
  • 概率分布(Probabilistic Distribution):输出层给出每个类别的概率,表示输入图像属于各个类别的可能性。

输入层-卷积层-非线性激活-池化-归一化层-全连接(将提取的特征图展平为一维向量)-输出层

Normalization Layer and Batch Normalization (BN)

随着网络层数的加深,参数不断更新,每一层输入数据的分布(均值、方差)都在不断剧烈变化。会导致:

  • 训练慢:后一层网络必须不断“适应”前一层传来的新分布,导致学习率必须设得很小。
  • 梯度消失/爆炸:如果数据分布跑到了激活函数(如Sigmoid/Tanh)的饱和区,梯度会趋近于0,导致网络无法训练;即使是ReLU,数据分布过大也会导致权重更新不稳定。
  • 对初始化敏感:如果初始权重没随机好,网络很容易崩。

归一化的作用就是:强行把每一层的输入拉回到一个标准的分布(通常是均值为0,方差为1),让网络在一个稳定的“舒适区”里训练。

能使用大batch_size训练时,适合用Batch Normalization;
当batch_size很小时,适合用Group Normalization;
做 NLP (Transformer),适合用Layer Normalization;
做风格迁移/生成图像,适合用 Instance Normalization。
Batch Normalization (BN) 的数学原理

在 CNN 中,BN 的核心特征是:Channel-wise(按通道独立计算) \color{red}\text{在 CNN 中,BN 的核心特征是:Channel-wise(按通道独立计算)}  CNN 中,BN 的核心特征是:Channelwise(按通道独立计算)
它在 Batch 维度(N)和空间维度( H, W )上进行聚合,为每一个通道 c 计算一组统计量。 \color{red}\text{它在 Batch 维度(N)和空间维度( H, W )上进行聚合,为每一个通道 c 计算一组统计量。} 它在 Batch 维度(N)和空间维度( H, W )上进行聚合,为每一个通道 c 计算一组统计量。

设 CNN 某一层的输入张量为 X X X ,输出张量为 Y Y Y 。它们的形状均为 ( N , C , H , W ) (N, C, H, W) (N,C,H,W) ,其中:

  • N N N :Batch Size(批量大小)
  • C C C :Channels(通道数/特征图数量)
  • H , W H, W H,W :Height,Width(特征图的高和宽)

使用索引 ( n , c , h , w ) (n, c, h, w) (n,c,h,w) 来定位张量中的某一个元素值 x n , c , h , w x_{n, c, h, w} xn,c,h,w
n ∈ { 1 , … , N } n \in\{1, \ldots, N\} n{1,,N}
c ∈ { 1 , … , C } c \in\{1, \ldots, C\} c{1,,C}
h ∈ { 1 , … , H } h \in\{1, \ldots, H\} h{1,,H}
w ∈ { 1 , … , W } w \in\{1, \ldots, W\} w{1,,W}

若在一个卷积层之后、激活函数之前插入BN层:

  • 对于每一个通道c,计算 batch中所有图像的 对应的通道c中 所有像素值的均值;集合 B c \mathcal{B}_c Bc 包含的元素个数为 m = N × H × W m=N \times H \times W m=N×H×W
  • 计算通道均值与方差:在 ( N , H , W ) (N, H, W) (N,H,W) 三个维度上求均值与方差:
    μ c = 1 N ⋅ H ⋅ W ∑ n = 1 N ∑ h = 1 H ∑ w = 1 W x n , c , h , w σ c 2 = 1 N ⋅ H ⋅ W ∑ n = 1 N ∑ h = 1 H ∑ w = 1 W ( x n , c , h , w − μ c ) 2 \mu_c=\frac{1}{N \cdot H \cdot W} \sum_{n=1}^N \sum_{h=1}^H \sum_{w=1}^W x_{n, c, h, w} \\ \sigma_c^2=\frac{1}{N \cdot H \cdot W} \sum_{n=1}^N \sum_{h=1}^H \sum_{w=1}^W\left(x_{n, c, h, w}-\mu_c\right)^2 μc=NHW1n=1Nh=1Hw=1Wxn,c,h,wσc2=NHW1n=1Nh=1Hw=1W(xn,c,h,wμc)2
  • 归一化:对该通道内的每一个元素进行标准化,使其服从 N ( 0 , 1 ) N(0,1) N(0,1) 分布。
    x ^ n , c , h , w = x n , c , h , w − μ c σ c 2 + ϵ \hat{x}_{n, c, h, w}=\frac{x_{n, c, h, w}-\mu_c}{\sqrt{\sigma_c^2+\epsilon}} x^n,c,h,w=σc2+ϵ xn,c,h,wμc
  • 缩放与平移:引入可学习的参数 γ c \gamma_c γc β c \beta_c βc 。注意, γ \gamma γ β \beta β 是向量,长度为 C C C ,即每个通道拥有自己独特的一对 γ c , β c \gamma_c, \beta_c γc,βc
    y n , c , h , w = γ c ⋅ x ^ n , c , h , w + β c y_{n, c, h, w}=\gamma_c \cdot \hat{x}_{n, c, h, w}+\beta_c yn,c,h,w=γcx^n,c,h,w+βc
  • 参数数量:因为有 C C C 个通道,所以最终网络需要学习 C C C γ \gamma γ 值 和 C C C β \beta β,以及在推理阶段使用的 C C C 个全局 running_mean 和 C C C 个全局 running_var 。
Batch Normalization (BN) 训练与推理

训练时

  • 使用当前Mini-batch的均值 μ B \mu_{\mathcal{B}} μB 和方差 σ B 2 \sigma_{\mathcal{B}}^2 σB2 进行归一化。
  • 同时,BN层会维护一对全局统计量(Global Statistics):running_mean 和 running_var。它们通过移动平均(Moving Average)的方式不断更新,记录整个数据集的分布特征。
     running-mean  = ( 1 −  momentum  ) ×  running-mean  +  momentum  × μ B \text { running-mean }=(1-\text { momentum }) \times \text { running-mean }+ \text { momentum } \times \mu_{\mathcal{B}}  running-mean =(1 momentum )× running-mean + momentum ×μB

推理时

  • 直接使用训练期间计算好的 running_mean 和 running_var 以及学习到的 γ , β \gamma, \beta γ,β 来处理数据。
Batch Normalization (BN) 实现
import torch
import torch.nn as nn

# 假设输入是一个 mini-batch: N=2, C=3, H=2, W=2
input_tensor = torch.randn(2, 3, 2, 2)

# 定义 BN 层,num_features 必须等于输入的通道数 C
# track_running_stats=True 表示会追踪全局均值和方差用于推理
bn_layer = nn.BatchNorm2d(num_features=3, affine=True, track_running_stats=True)

# --- 训练阶段 ---
bn_layer.train() # 开启训练模式
output = bn_layer(input_tensor)

print("Scale (gamma):", bn_layer.weight.data) # 初始通常为全1
print("Shift (beta):", bn_layer.bias.data)    # 初始通常为全0
print("Running Mean:", bn_layer.running_mean) # 初始为0,随训练更新
print("Running Var:", bn_layer.running_var)   # 初始为1,随训练更新

# --- 推理阶段 ---
bn_layer.eval() # 开启评估模式
# 此时 forward 过程不再计算 batch 均值,而是用 running_mean/var
test_output = bn_layer(input_tensor)

Layer Normalization

对每一个样本的所有通道和空间信息进行归一化。
对于输入维度(N-C-H-W);对于单个样本(此处是单个图片),在(C-H-W)维度上聚合:

  • 对于第 n n n 个样本,计算均值:
    μ n = 1 C ⋅ H ⋅ W ∑ c = 1 C ∑ h = 1 H ∑ w = 1 W x n , c , h , w \mu_n=\frac{1}{C \cdot H \cdot W} \sum_{c=1}^C \sum_{h=1}^H \sum_{w=1}^W x_{n, c, h, w} μn=CHW1c=1Ch=1Hw=1Wxn,c,h,w
  • 计算方差
    σ n 2 = 1 C ⋅ H ⋅ W ∑ c = 1 C ∑ h = 1 H ∑ w = 1 W ( x n , c , h , w − μ n ) 2 \sigma_n^2=\frac{1}{C \cdot H \cdot W} \sum_{c=1}^C \sum_{h=1}^H \sum_{w=1}^W\left(x_{n, c, h, w}-\mu_n\right)^2 σn2=CHW1c=1Ch=1Hw=1W(xn,c,h,wμn)2
  • 归一化 (这里的 μ n \mu_n μn σ n \sigma_n σn 对同一个样本内的所有 C C C 个通道都是一样的)
    x ^ n , c , h , w = x n , c , h , w − μ n σ n 2 + ϵ \hat{x}_{n, c, h, w}=\frac{x_{n, c, h, w}-\mu_n}{\sqrt{\sigma_n^2+\epsilon}} x^n,c,h,w=σn2+ϵ xn,c,h,wμn
  • 缩放与平移
    y n , c , h , w = γ c ⋅ x ^ n , c , h , w + β c y_{n, c, h, w}=\gamma_c \cdot \hat{x}_{n, c, h, w}+\beta_c yn,c,h,w=γcx^n,c,h,w+βc

Group Normalization

GN 是 LN 和 IN 的折中方案,为解决 BN 在小 Batch Size 下性能崩塌的问题

由于CNN 的通道特征通常不是完全独立的,而是分组的(例如,一组通道提取形状,另一组提取纹理)。GN 顺应了这种“群组特征”。

  • 预处理:将通道数 C C C 分成 G G G 个组,每组包含 M = C / G M=C / G M=C/G 个通道。需要计算当前通道 c c c 属于哪一个组,记组索引为 g c = ⌊ c M ⌋ g_c=\left\lfloor\frac{c}{M}\right\rfloor gc=Mc
  • 计算范围:对于固定的样本 n n n 和固定的组 g g g ,在 ( M , H , W ) (M, H, W) (M,H,W) 维度上聚合。即"一部分通道+所有空间像素"。
  • 计算均值 对于第 n n n 个样本的第 g g g 个组(该组包含的通道集合记为 C g \mathcal{C}_g Cg ):
    μ n , g = 1 M ⋅ H ⋅ W ∑ c ∈ C g ∑ h = 1 H ∑ w = 1 W x n , c , h , w \mu_{n, g}=\frac{1}{M \cdot H \cdot W} \sum_{c \in \mathcal{C}_g} \sum_{h=1}^H \sum_{w=1}^W x_{n, c, h, w} μn,g=MHW1cCgh=1Hw=1Wxn,c,h,w
  • 计算方差
    σ n , g 2 = 1 M ⋅ H ⋅ W ∑ c ∈ C g ∑ h = 1 H ∑ w = 1 W ( x n , c , h , w − μ n , g ) 2 \sigma_{n, g}^2=\frac{1}{M \cdot H \cdot W} \sum_{c \in \mathcal{C}_g} \sum_{h=1}^H \sum_{w=1}^W\left(x_{n, c, h, w}-\mu_{n, g}\right)^2 σn,g2=MHW1cCgh=1Hw=1W(xn,c,h,wμn,g)2
  • 归一化(同一个 Group 内的所有通道共用这一个均值和方差)
    x ^ n , c , h , w = x n , c , h , w − μ n , g σ n , g 2 + ϵ \hat{x}_{n, c, h, w}=\frac{x_{n, c, h, w}-\mu_{n, g}}{\sqrt{\sigma_{n, g}^2+\epsilon}} x^n,c,h,w=σn,g2+ϵ xn,c,h,wμn,g
  • 缩放平移( γ \gamma γ β \beta β 依然是 Channel-wise 的,每个通道都有自己独立的缩放参数)
    y n , c , h , w = γ c ⋅ x ^ n , c , h , w + β c y_{n, c, h, w}=\gamma_c \cdot \hat{x}_{n, c, h, w}+\beta_c yn,c,h,w=γcx^n,c,h,w+βc

Instance Normalization

把每个样本的每个通道都看作独立的个体;对于固定的样本 n n n 和固定的通道 c c c,我们只在 ( H , W ) (H, W) (H,W) 空间维度上聚合

为什么 Transformer 需要用 LN 而不是 BN

CNN由于平移不变性(如果在图片的左上角检测到了“边缘”,和在右下角检测到“边缘”,它们应该被同等对待),因此,对于batch所有样本的同一个通道 c c c,无论像素在哪个位置 ( h , w ) (h, w) (h,w),都希望用同一组缩放系数 ( γ c , β c ) (\gamma_c, \beta_c) (γc,βc) 来调整它。

对于NLP-Transformer,输入形状 (batch_size N - sequence length L - embedding dimension D);需针对 Embedding 向量的每一个元素(Element)都有独立的参数。

  • 计算均值:针对第 n n n 个句子的第 l l l 个单词,计算其 D D D 个特征值的平均值:
    μ n , l = 1 D ∑ d = 1 D x n , l , d \mu_{n, l}=\frac{1}{D} \sum_{d=1}^D x_{n, l, d} μn,l=D1d=1Dxn,l,d
    结果 μ n , l \mu_{n, l} μn,l 是一个标量,它只与当前单词有关,不依赖其他单词,也不依赖 Batch里的其他句子。
  • 计算方差
    σ n , l 2 = 1 D ∑ d = 1 D ( x n , l , d − μ n , l ) 2 \sigma_{n, l}^2=\frac{1}{D} \sum_{d=1}^D\left(x_{n, l, d}-\mu_{n, l}\right)^2 σn,l2=D1d=1D(xn,l,dμn,l)2
  • 归一化:对该单词向量的每一个特征 d d d 进行标准化:
    x ^ n , l , d = x n , l , d − μ n , l σ n , l 2 + ϵ \hat{x}_{n, l, d}=\frac{x_{n, l, d}-\mu_{n, l}}{\sqrt{\sigma_{n, l}^2+\epsilon}} x^n,l,d=σn,l2+ϵ xn,l,dμn,l
  • 缩放与平移:使用可学习参数 γ \gamma γ β \beta β 进行仿射变换。
    y n , l , d = γ d ⋅ x ^ n , l , d + β d , ∀ d = 0 , ⋯   , D − 1 y_{n, l, d}=\gamma_d \cdot \hat{x}_{n, l, d}+\beta_d, \forall d = 0,\cdots,D-1 yn,l,d=γdx^n,l,d+βd,d=0,,D1

在这里插入图片描述

  • 不定长序列与 Padding 问题
    BN的逻辑:BN 需要在 Batch 维度上求均值。而NLP 一个 Batch 里的句子长度不一样,句子 A 有 5 个词,句子 B 有 100 个词。为了凑成 Tensor,短句子必须补 0 。
    如果用 BN,那些大量的Padding 0 会被算进均值和方差里,导致统计量严重偏移,不仅本身算不准,还会把这种偏差"污染"给长句子中的有效词。
    LN 只算当前这个单词自己的 D D D 维向量的均值方差,句子长短、Padding 多少,完全不影响当前这个有效单词的归一化计算。
  • Training 与 Inference 的一致性
    在 Transformer-GPT中,推理是自回归的。需通过 t t t 时刻生成的词,去预测 t + 1 t+1 t+1 时刻。如果使用 BN,推理时需要一个全局统计量。但在 NLP 中,不同长度、不同语义的句子统计特性差异巨大,很难用一个全局统计量概括所有句子的分布。
  • Batch Size 的限制
    Transformer 模型通显存占用高。导致训练时的 Batch Size 往往很小;当 Batch Size 很小时,BN 的统计量极其不稳定,会导致模型无法收敛。

默写CNN-MNIST–版本1:

# 导入库
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

from torchvision import datasets, transforms

# import matplotlib.pyplot as plt

'数据加载与预处理'
# data processing
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))
                                ])

train_dataset = datasets.MNIST(root='.\data\mnist',
                               train=True, transform=transform, download=False)
test_dataset = datasets.MNIST(root='.\data\mnist',
                              train=False, transform=transform, download=False)

# data loading
train_loader = torch.utils.data.DataLoader(
    dataset=train_dataset, batch_size=32, shuffle=True)

test_loader = torch.utils.data.DataLoader(
    dataset=test_dataset, batch_size=32, shuffle=False)


'模型构建'
class simpleCNN(nn.Module):

    _n_cls = 10

    def __init__(self):
        # py3 特性 自动继承父类
        super().__init__()
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
        self.pooling1 = nn.MaxPool2d(kernel_size=2)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
        self.pooling2 = nn.MaxPool2d(kernel_size=2)
        self.linear1 = nn.Linear(64*7*7, 256)
        self.linear2 = nn.Linear(256, self._n_cls)

    def forward(self, x):

        x = self.conv1(x)
        x = F.relu(x)
        x = self.pooling1(x)
        x = self.conv2(x)
        x = F.relu(x)
        x = self.pooling2(x)
        x = x.view(-1, 64*7*7)
        x = self.linear1(x)
        x = F.relu(x)
        x = self.linear2(x)

        return x

# model initialization
model = simpleCNN()
model.train()

# 定义损失函数与优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=1e-2, momentum=0)

# 训练模型
acc_lis = []
epoch = 5
for ep in range(epoch):
    total_loss = 0
    # count = 0
    for idx, (img, lab) in enumerate(train_loader):

        # forward to generate
        out = model(img)
        loss = criterion(out, lab)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()

        # count += 1
        # if count >= 10:
        #     break

    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for img, lab in test_loader:
            test_out = model(img)
            _, predicted = torch.max(test_out, 1)
            total += lab.size(0)
            correct += (predicted == lab).sum().item()

    acc = 100 * correct / total
    acc_lis.append(acc)
    print(f'round: {ep}, test accuracy = {acc}%')

Hints 1

nn.Conv2d()
self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, stride=1, padding=1)

in_channels (输入通道数): (MNIST灰度图 故是1)
out_channels (输出通道数)
kernel_size (卷积核大小):滑动窗口的大小
stride (步长):卷积核每次滑动的距离
padding (填充):在输入图像周围填充 0 的圈数

nn.MaxPool2d()

用于下采样,减少计算量,并提取最显著的特征(即最大值)

self.pooling1 = nn.MaxPool2d(kernel_size=2, stride=2)

kernel_size: 池化窗口的大小

  • 在 MaxPool 中,如果不指定 stride,默认值等于 kernel_size。这意味着窗口之间不重叠。
  • 2 × 2 2 \times 2 2×2 的池化会让图像的长和宽直接减半
nn.Linear()

用于将提取出的特征矩阵展平并进行分类。

self.linear1 = nn.Linear(in_features=64*7*7, out_features=256)
  • in_features: 输入向量的长度,须与上一层输出并Flatten后的元素总数严格匹配。
  • out_features: 输出向量的长度。
维度推演

初始输入
-形状:(1,28,28)
1.self.conv1(x)

  • 参数: K = 3 , S = 1 , P = 1 K=3, S=1, P=1 K=3,S=1,P=1
  • 计算: H out  = 28 − 3 + 2 × 1 1 + 1 = 28 H_{\text {out }}=\frac{28-3+2 \times 1}{1}+1=28 Hout =1283+2×1+1=28
  • 通道数变为了 out_channels=32。
  • 输出形状: ( 32 , 28 , 28 ) (32,28,28) (32,28,28)

2.self.pooling1( x )

  • 参数: K = 2 K=2 K=2 ,默认 S = 2 S=2 S=2
  • 计算: H out  = 28 2 = 14 H_{\text {out }}=\frac{28}{2}=14 Hout =228=14
  • 通道数不变。
  • 输出形状:( 32 , 14 , 14 32,14,14 32,14,14

3.self.conv2(x)

  • 输入通道必须是 32 (匹配上一层)。
  • 参数: K = 3 , S = 1 , P = 1 K=3, S=1, P=1 K=3,S=1,P=1(注意这里又是 Padding=1,Stride=1,所以长宽不变)
  • 计算: H out  = 14 − 3 + 2 × 1 1 + 1 = 14 H_{\text {out }}=\frac{14-3+2 \times 1}{1}+1=14 Hout =1143+2×1+1=14
  • 通道数变为了 out_channels = 64 =64 =64
  • 输出形状:( 64 , 14 , 14 64,14,14 64,14,14

4.self.pooling2(x)

  • 参数: K = 2 , S = 2 K=2, S=2 K=2,S=2
  • 计算: H out  = 14 2 = 7 H_{\text {out }}=\frac{14}{2}=7 Hout =214=7
  • 通道数不变。
  • 输出形状:( 64 , 7 , 7 64,7,7 64,7,7

5. x . view ⁡ ( − 1 ,   64 ∗ 7 ∗ 7 ) x . \operatorname{view}(-1, ~ 64 * 7 * 7) x.view(1, 6477)

  • 这里是卷积层到全连接层的过渡。
  • 此时的数据形状是(Batch,64,7,7),是一个三维的立方体数据。
  • 全连接层只接受一维向量(除了 Batch 维)。
  • 需要把 64 个 7 × 7 7 \times 7 7×7 的特征图全部拉直。
  • 总元素数量 = 64 × 7 × 7 = 3136 =64 \times 7 \times 7=3136 =64×7×7=3136
维度匹配

对于卷积层,输出尺寸 O O O 的计算公式为:
O = I − K + 2 P S + 1 O=\frac{I-K+2 P}{S}+1 O=SIK+2P+1
其中: I = I= I= 输入尺寸, K = K= K= 卷积核大小, P = P= P= Padding, S = S= S= Stride。

k e r n e l _ s i z e = 3 kernel\_size=3 kernel_size=3 s t r i d e = 1 stride=1 stride=1 时,设置 p a d d i n g = 1 padding=1 padding=1
可以保持输入和输出的长宽尺寸不变(只有深度变了)
代入公式:O = ( I - 3 + 2*1 ) / 1 + 1 = I
在这里插入图片描述

x.view(-1, 6477)

由于x目前的形状为: ( Batch_Size , Channel , Height , Width ) (\text{Batch\_Size}, \text{Channel}, \text{Height}, \text{Width}) (Batch_Size,Channel,Height,Width)
-1: 代表 “自动计算未知的维度”,PyTorch 会根据元素总数保持不变的原则,自动计算这一维应该是多少,此处,-1 就会自动变成 Batch_Size: ( B a t c h _ S i z e , 3136 ) (Batch\_Size, 3136) (Batch_Size,3136)

  • .view() 的用法与机制
    核心规则:元素总数必须守恒 变换前的形状乘积,必须等于变换后的形状乘积。 核心规则:元素总数必须守恒 \\ 变换前的形状乘积,必须等于变换后的形状乘积。 核心规则:元素总数必须守恒变换前的形状乘积,必须等于变换后的形状乘积。

  • 具体示例

x = torch.zeros([2,3,4])

x1 = x.view(-1,24)
'torch.Size([1, 24])'   

# 我想要变成 1 行,长度自动算
x2 = x.view(-1)
'torch.Size([24])'

# 我想要保持第0维(2)不变,后面全部拉直
x3 = x.view(2, -1)
'torch.Size([2, 12])'

# 我想要变成 (4, 6) 的形状
x4 = x.view(4, 6)
'torch.Size([4, 6])'

print(x1.size(),x2.size(),x3.size(),x4.size())
  • view(): 要求数据在内存中是连续的 (Contiguous)。如果之前对张量做过 transpose 或 permute(转置/换位)操作,数据在内存里的顺序就乱了,直接用 view 会报错。
  • 解决方法: x.contiguous().view(…)
  • reshape(): 如果数据不连续,它会自动复制一份新的数据使其连续,然后再变形

Hints 2

model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for img, lab in test_loader:
            test_out = model(img)
            # print(test_out)
            _, predicted = torch.max(test_out, 1)
            total += lab.size(0)
            correct += (predicted == lab).sum().item()

    acc = 100 * correct / total
    acc_lis.append(acc)
    print(f'round: {ep}, test accuracy = {acc}%')
model.eval()

Dropout 层在训练时会随机丢弃神经元,但在测试时必须停止丢弃;Batch Normalization 层在训练时使用当前 batch 的均值和方差,而在测试时使用全局统计的均值和方差。

with torch.no_grad():

创建一个上下文环境,在这个缩进块内的所有计算都不会计算梯度;测试阶段只需要前向传播,不需要反向传播,关闭梯度可以大幅提升速度。

for img, lab in test_loader:

从 test_loader 中逐个batch提取数据:32张图,每张图对应 0-9 共 10 个类别的分数)。分数越高的类别,代表模型认为越可能是该数字

img.size(): torch.Size([32, 1, 28, 28])
lab.size(): torch.Size([32])
lab: tensor([7, 2, 1, 0, 4, 1, 4, 9, 5, 9, 0, 6, 9, 0, 1, 5, 9, 7, 3, 4, 9, 6, 6, 5, 4, 0, 7, 4, 0, 1, 3, 1])
test_out = model(img)

输出batch_size个logits向量:

test_out.size(): torch.Size([32, 10])
_, predicted = torch.max(test_out, 1)

找出每张图中分数最高的那个类别的索引。

torch.max(tensor, dim)

torch.max(tensor, dim) 会返回两个值:第一个是最大值本身(用 _ 忽略),第二个是最大值的索引(即模型预测的数字)。

' _ 是一个惯用的占位符变量名,表示“不关心此返回值,后续也不会使用” '

参数 1 表示在第 1 维度(横向,即类别维度)上找最大值。

predicted 就是模型给出的最终答案,例如 [7, 2, 1, …]。

print(predicted)
'tensor([7, 2, 1, 0, 4, 1, 7, 4, 0, 7, 0, 0, 4, 0, 1, 3, 7, 7, 3, 4, 8, 6, 6, 5, 4, 0, 7, 4, 0, 1, 3, 1])'
total += lab.size(0)

累加这一批次的样本数量

lab.size()
'''
返回值:torch.Size 对象: torch.Size([32])
'''
correct += (predicted == lab).sum().item()
  • predicted == lab:逐元素比较,生成一个布尔张量(例如 [True, False, True, …])。
  • .sum():将布尔值求和(True 为 1,False 为 0),得到这一批次猜对的个数(例如 tensor(28))。
  • .item():将 PyTorch 的 0 维张量(Tensor)转换为标准的 Python 数字(int/float)。如果不加 .item(),累加的将是 Tensor 对象,会导致显存泄露。.item()只适用于包含一个元素的Tensor
'''
当对 Tensor 进行运算(如 sum())时,PyTorch 会默认构建计算图以便进行反向传播。生成的 Tensor 即使只是一个标量,其也可能挂载 grad_fn

加上 .item(): 把 Tensor 里的数值取出来,变成一个普通的 Python 数字,不包含梯度信息,从而切断了计算图,释放显存,防止多轮训练出现显存溢出
'''
tensor维度定义

T e n s o r 的维度(从左到右, D i m 0 , D i m 1... )代表了从宏观到微观的“层级”。 \color{red}Tensor 的维度(从左到右,Dim 0, Dim 1...)代表了从宏观到微观的“层级”。 Tensor的维度(从左到右,Dim0,Dim1...)代表了从宏观到微观的层级

对于4D [B, C, H, W]
Dim 0 ( B ): Batch Size 图索引
Dim 1 ( C ): Channels 颜色通道(RGB)。
Dim 2 ( H ): Height 图片高
Dim 3 ( W ): Width 图片宽
在这里插入图片描述
可视化代码(Gemini):

import torch
import matplotlib.pyplot as plt
import numpy as np

def visualize_4d_tensor():
    # 1. 定义维度参数
    B, C, H, W = 4, 3, 32, 32
    # Batch=4 (4张图)
    # Channel=3 (RGB 3通道)
    # Height=32, Width=32 (像素大小)

    print(f"创建 4D Tensor,形状为: [{B}, {C}, {H}, {W}]")
    print("-" * 30)

    # 2. 创建模拟数据
    # 为了演示,我们生成随机噪音图
    tensor_4d = torch.rand(B, C, H, W)
    
    # 3. 可视化逻辑
    # 我们将展示每一张图(Dim 0),以及每一张图的每一个通道(Dim 1)
    
    fig, axes = plt.subplots(B, C + 1, figsize=(12, 10))
    # 每一行代表一个 Batch (Dim 0)
    # 前三列代表 R, G, B 通道 (Dim 1)
    # 最后一列代表 合成的彩色图

    for b in range(B): # 遍历 Dim 0 (批次)
        
        # --- 拿出一张完整的图 ---
        single_img_tensor = tensor_4d[b] # 形状变成 [3, 32, 32]
        
        # 这里的技巧:Matplotlib 需要 [H, W, C] 格式,而 PyTorch 是 [C, H, W]
        # 所以我们需要 .permute(1, 2, 0) 交换维度
        img_for_plot = single_img_tensor.permute(1, 2, 0).numpy()
        
        # 显示合成图
        axes[b, C].imshow(img_for_plot)
        axes[b, C].set_title(f"Batch {b}\n(Full Color)", fontsize=10, color='red')
        axes[b, C].axis('off')
        
        # --- 遍历该图的 3 个通道 ---
        for c in range(C): # 遍历 Dim 1 (通道)
            
            # 取出单通道数据,形状 [32, 32]
            channel_img = tensor_4d[b, c, :, :] 
            
            # 显示灰度图(因为单通道本质就是灰度矩阵)
            axes[b, c].imshow(channel_img, cmap='gray')
            axes[b, c].set_title(f"Batch {b}\nChannel {c}", fontsize=10)
            axes[b, c].axis('off')

    plt.tight_layout()
    plt.show()

# 运行可视化
visualize_4d_tensor()
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

idkmn_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值