ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

深度学习张量形状与维度变化:从反向传播到CNN的代码级指南

2026/10/8 8:04:40 拓冰建站 浏览量
深度学习张量形状与维度变化:从反向传播到CNN的代码级指南 上周调一个小型人脸识别网络又撞见了RuntimeError: mat1 and mat2 shapes cannot be multiplied。这种报错做深度学习这几年我见了不下几十次每次根源都一样对张量形状的变化轨迹心里没数。这篇是Python 神经网络与深度学习实用指南的第三篇。前两篇我们把环境、基础概念和第一个训练脚本跑通了这篇换个思路——从张量的形状与维度变化入手把前向传播、反向传播、卷积神经网络CNN这些看似高深的东西全部落到代码级的具体细节上。适合已经能跑通简单模型、但总觉得原理懂了却写不出代码的读者也适合那些训练效果忽好忽坏、想弄清背后原因的人。说白了这篇的目标就是帮你从会调库变成真懂网络。1. 为什么很多人的网络训练效果差先从张量形状说起1.1 一个报错背后隐藏的四个维度新手最常问的问题是图像到底是几维的数据答案其实不复杂——对 PyTorch 和 TensorFlow 这类框架来说一张彩色图片在进入神经网络前默认是四维张量形状写作(batch, channel, height, width)。以 64x64 的 RGB 图片为例batch一次喂进去多少张图比如 32channel颜色通道数RGB 就是 3灰度图是 1height、width图像的高和宽这里都是 64。所以一批数据张量的形状就是(32, 3, 64, 64)。我一直建议初学者先记住这个格式因为后面所有报错十有八九都是这四个数在某个环节对不上导致的。1.2 展平操作最容易被忽略的坑全连接层Fully Connected Layer简称 FC是神经网络里最古老的组成部分它要求输入是一个二维矩阵(batch, feature)也就是说图片必须先从三维(3, 64, 64)展平成一维122883×64×64变成一个(batch, 12288)的矩阵。问题来了很多人展平的时候只算了64*644096忘了把通道数乘进去。这就会导致输入维度和权重矩阵第一维对不上直接抛维度不匹配异常。提示遇到mat1 and mat2 shapes cannot be multiplied这类报错第一件事不是去搜代码而是把所有张量当前的 shape 都打印出来逐个环节检查。我经常在代码里临时加一行print(x.shape)来定位问题调完再删掉。这不是笨办法而是最快速的排查路径。2. 用 numpy 手写一个三层前馈网络从图像到高维向量2.1 为什么建议你手写一次前向传播很多人一上来就用nn.Sequential堆网络看着是挺方便但对网络内部的数据流动完全是黑盒。我强烈建议至少手写一次前向传播哪怕是很小的网络因为这会逼着你搞清楚每一层的输入输出到底是什么形状。这里拿 MNIST 手写数字识别举例。每张图是 28x28 的灰度图展平后是 784 维向量。我构建一个三层网络输入层784 个神经元隐藏层 1128 个神经元激活函数用 ReLU隐藏层 264 个神经元激活函数用 ReLU输出层10 个神经元对应 0~9 十个类别。2.2 矩阵乘法里的形状习惯行向量还是列向量写代码之前必须统一一个习惯权重矩阵到底怎么放。我推荐用W的形状为(当前层神经元数, 上一层神经元数)这样z W x b算出来的z形状是(当前层神经元数, batch_size)。不过 PyTorch 的nn.Linear(in_features, out_features)内部权重形状是(out_features, in_features)x形状是(batch, in_features)计算方式是x W.T b。两种写法都行但自己手写时一定要固定一种别混着来否则维度两天就对不上了。2.3 前向传播的完整实现用 numpy 实现一个简化版的 MNIST 前向传播代码如下import numpy as np def relu(x): return np.maximum(0, x) class ThreeLayerNet: def __init__(self, in_dim784, hidden1_dim128, hidden2_dim64, out_dim10): # 初始化权重和偏置 # 这里用 He 初始化适合 ReLU 激活函数 self.W1 np.random.randn(hidden1_dim, in_dim) * np.sqrt(2.0 / in_dim) self.b1 np.zeros((hidden1_dim, 1)) self.W2 np.random.randn(hidden2_dim, hidden1_dim) * np.sqrt(2.0 / hidden1_dim) self.b2 np.zeros((hidden2_dim, 1)) self.W3 np.random.randn(out_dim, hidden2_dim) * np.sqrt(2.0 / hidden2_dim) self.b3 np.zeros((out_dim, 1)) def forward(self, x): # x: (in_dim, batch_size) self.z1 self.W1 x self.b1 self.a1 relu(self.z1) self.z2 self.W2 self.a1 self.b2 self.a2 relu(self.z2) self.z3 self.W3 self.a2 self.b3 # 输出层先不接 softmax后面算损失时用 return self.z3 # 模拟一批 8 张 28x28 的图片 x_batch np.random.randn(784, 8) net ThreeLayerNet() out net.forward(x_batch) print(out.shape) # (10, 8)每列是一张图的分类得分2.4 从图像到高维向量到底是什么意思运行完上面的代码你会看到网络的输出形状是(10, 8)8 是批量大小10 是对应 10 个类别的得分。这就是热词里反复出现的人脸识别图像进入神经网络到输出高维度向量的过程。人脸识别里的做法其实一样只是输出维度不再是 10而是故意设计成一个高维向量比如 128 维、512 维。训练目标是让同一个人的两张人脸照片映射到高维空间中距离很近不同人的照片距离很远。这个思路叫度量学习核心就在于神经网络的最后一层特征向量而不是分类结果本身。3. 反向传播不是黑魔法两层网络逐步手算与代码对照3.1 链式法则是整个深度学习的地基反向传播的本质是链式法则。损失函数对某个权重求梯度需要从损失函数出发一层一层往回乘。很多人觉得它难是因为把符号推导搞复杂了实际上只要拿一个具体的小网络手算一遍就通了。我先用最简洁的例子说明。设损失函数是均方误差L 0.5 * (y_pred - y_true)^2最后一层是线性输出激活函数是 sigmoid隐藏层也是 sigmoid。那么损失对 W2 的梯度是dL/dW2 (y_pred - y_true) * sigmoid_derivative(z2) * a1这里y_pred - y_true是误差项sigmoid_derivative是激活函数的导数a1是上一层输出。所有反向传播代码本质上都是在这种链式乘法里打转。3.2 一个具体数字的完整传播过程我设计一个极小的网络输入层 2 个神经元隐藏层 2 个神经元输出层 1 个神经元。权重如下W1 [[0.5, -0.2], [0.3, 0.8]]W2 [[0.6, -0.4]]偏置都为 0输入 x [0.5, 0.1]真实值 y_true [1.0]先做前向传播z1 W1 x [0.25, 0.23]a1 sigmoid(z1) [0.5622, 0.5573]z2 W2 a1 0.3374 - 0.2229 0.1145y_pred sigmoid(z2) 0.5286L 0.5 * (0.5286 - 1)^2 0.1111再做反向传播dL/dz2 (y_pred - y_true) * sigmoid_derivative(z2) (0.5286-1) * 0.5286*(1-0.5286) ≈ -0.1177dL/dW2 dL/dz2 * a1 [-0.0662, -0.0656]dL/dz1 W2.T * sigmoid_derivative(z1) * dL/dz2得到关于两个神经元的梯度逐项更新权重完整的代码对照可以放进调试器里逐行验算。建议你用 pytest 写一个断言验证自己手算的结果和代码结果一致这个习惯对理解深刻程度有天壤之别。3.3 梯度消失的早期信号上面用了 sigmoid它有个致命问题导数最大值只有 0.25。多层反向传播时每一层都要乘以一个不超过 0.25 的数连乘三层就衰减到 0.0156五层之后就接近 0 了。这就是梯度消失——浅层权重几乎得不到更新。这解释了为什么现代网络都用 ReLUReLU 的导数在正半轴是 1连乘不会衰减。我第一次意识到这个问题时非常震撼原来一个激活函数的选择就能决定深度网络能不能训练起来。4. 从玩具模型到真实项目用 PyTorch 把流程工程化4.1 环境准备别再被 Python 版本坑了PyTorch 版本和 Python 版本有对应关系装错版本会出现DLL load failed或者No module named torch。我用 conda 管理的习惯是conda create -n dl python3.10 conda activate dl pip install numpy matplotlib # 根据你的 CUDA 版本安装对应 PyTorch去官网选对应命令 pip install torch torchvision这里有个常见问题很多人装 numpy 装失败多半是 pip 和 conda 混用导致的依赖冲突。我的原则是conda负责创建环境pip负责装 Python 包但不在同一个环境里反复交替使用两种安装方式。4.2 DataLoader 的两个关键参数batch_size 与 shuffle真实项目里数据量大不能一次性把全部数据喂进网络。PyTorch 的DataLoader就是干这个的from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_dataset datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransform ) train_loader DataLoader( train_dataset, batch_size64, shuffleTrue, num_workers4, pin_memoryTrue, )shuffleTrue在训练集里很重要它会打乱样本顺序避免模型学到样本排列的规律。num_workers是并行加载数据的进程数Windows 上有时得设为 0否则会因为多进程启动问题报错。4.3 训练循环的标准写法与断点续训一个标准的训练循环其实就五件事前向计算、算损失、梯度清零、反向传播、参数更新。import torch import torch.nn as nn import torch.optim as optim model Net() criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) for epoch in range(10): model.train() total_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() avg_loss total_loss / len(train_loader) print(fEpoch {epoch1}, Loss: {avg_loss:.4f}) # 每个 epoch 存一次 checkpoint便于断点续训 torch.save({ model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), epoch: epoch, loss: avg_loss, }, fcheckpoint_epoch_{epoch1}.pth)我踩过最大的坑是忘了加optimizer.zero_grad()。PyTorch 的梯度是累积的如果不每轮清零梯度会一直叠加导致 loss 忽高忽低训练完全乱套。新手看到 loss 震荡第一反应是调学习率但我第一个排查项永远是梯度有没有清零。断点续训的写法也很关键。重启训练时要同时加载模型参数和优化器状态还要把 epoch 数接上否则学习率调度器会从零开始算影响训练节奏checkpoint torch.load(checkpoint_epoch_5.pth) model.load_state_dict(checkpoint[model_state_dict]) optimizer.load_state_dict(checkpoint[optimizer_state_dict]) start_epoch checkpoint[epoch] 15. 全连接网络的天花板为什么人脸识别必须上 CNN5.1 全连接层处理图像的两个致命问题全连接层处理图像有两个硬伤。第一是参数爆炸一张 224x224 的 RGB 图片展平后是 15 万维第一层隐藏层如果有 1024 个神经元光这一层的参数量就是 1.5 亿单层就能把显卡显存吃爆。第二是丢失空间结构展平之后图片里左上角是眼睛右下角是嘴巴这种空间关系全没了模型需要重新从零学习位置信息样本效率极低。5.2 卷积层、池化层、特征图的维度变化全过程卷积的思路是用一个小的卷积核比如 3x3滑过整张图片每个位置做一次局部加权求和得到一张新的特征图。核很小参数少局部操作保留了空间关系同一张特征图的所有位置共享同一个核参数再次大幅减少。特征图尺寸的计算公式是output_size (input_size - kernel_size 2 * padding) / stride 1拿 MNIST 举例输入是 28x28 的单通道图完整走一遍卷积层 Conv1kernel3padding0stride1。尺寸计算(28-3)/1126。输出特征图 26x26通道数从 1 变成 16池化层 Pool12x2 最大池化尺寸减半变成 13x13卷积层 Conv2kernel3输出尺寸 (13-3)/1111通道数从 16 变成 32特征图变为 32 张 11x11池化层 Pool2再减半变成 32 张 5x5展平32×5×5800变成 800 维向量全连接层800 维映射到 10 个类别。这一番操作下来参数量比起直接用全连接网络少了几个数量级而准确率反而明显提升。5.3 一个人人都能敲出来的 CNN 实现用 PyTorch 实现上面的结构代码其实很短import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 16, kernel_size3), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(16, 32, kernel_size3), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Linear(32 * 5 * 5, 10) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) x self.classifier(x) return x训练 MNIST 时这种简单 CNN 用 CPU 跑几个 epoch 就能到 98% 以上的准确率。你把它和全连接网络对比一下就能直观感受到卷积在图像任务上的碾压优势。5.4 显存占用的粗略估算方法做项目时还得会估算模型占用多大的显存。模型显存大致由三块组成参数本身、梯度、优化器状态外加中间激活值。粗略估算时可以用torchinfo这个库from torchinfo import summary model SimpleCNN() summary(model, input_size(64, 1, 28, 28))它会直接给出参数量和每层的输出尺寸。实际训练时显存占用主要被中间变量激活值吃掉所以 batch size 设得太大、图片分辨率太高显存最容易爆。爆显存第一招是减小 batch size第二招是降低分辨率而不是随便换更大的显卡。6. 训练中那些看不见的坑正则化、学习率与多任务 loss6.1 L2 正则化的 PyTorch 代码与权重衰减的关系很多人不知道PyTorch 优化器里的weight_decay参数就是 L2 正则化。它做的事情是在损失函数里加一项所有权重的平方和抑制权重过大从而降低过拟合。用代码看更清楚。手动实现 L2 的损失是l2_lambda 1e-4 l2_norm sum(p.pow(2).sum() for p in model.parameters()) loss cross_entropy_loss l2_lambda * l2_norm对应到优化器上就是一句optimizer optim.Adam(model.parameters(), lr0.001, weight_decay1e-4)两者效果基本等价用内置的weight_decay就好。这里有个细节weight_decay对偏置bias一般不生效这对偏置来说其实是合理的因为偏置的主要作用是平移不让它参与正则化能更稳定。6.2 学习率过大震荡、过小绝望学习率是深度学习里最影响训练结果的一个超参数。设置太大loss 会剧烈震荡甚至发散设置太小训练半天 loss 纹丝不动。我的实用调参顺序是这样的先用固定学习率 0.001 跑 5 个 epoch观察 loss 曲线如果 loss 震荡除以 10比如降到 0.0001如果 loss 降得很慢乘以 3 或 5比如升到 0.003训练后期切换成余弦退火或按步长衰减。PyTorch 自带的调度器很好用比如每 10 个 epoch 学习率减半from torch.optim.lr_scheduler import StepLR scheduler StepLR(optimizer, step_size10, gamma0.5) # 每个 epoch 结束后调用 scheduler.step()6.3 多任务学习时如何调整多个 loss 的比例多任务学习里的经典问题模型同时做分类和回归两个 loss 都在反向传播比例怎么调无脑都是 1:1 往往导致一个任务压过另一个。我试过两种可行方案比较推荐第二种。第一种是手动加权先用固定权重跑观察两个 loss 的数量级差异。如果分类 loss 在 0.5 左右、回归 loss 在 2 左右直接对回归 loss 乘以一个 0.1 到 0.3 的系数把两者拉到相近量级。第二种是不确定性加权让网络自己学习每个任务的不确定性据此自动调整权重PyTorch 实现思路如下log_var_classification torch.nn.Parameter(torch.tensor(0.0)) log_var_regression torch.nn.Parameter(torch.tensor(0.0)) # 训练循环中 loss 0.5 * loss_cls / torch.exp(log_var_classification) \ 0.5 * loss_reg / torch.exp(log_var_regression) \ 0.5 * log_var_classification 0.5 * log_var_regression这套来自多任务深度学习的经典做法核心逻辑是一个任务的噪声大不确定性高就让它在总 loss 里占少一点。如果你还不太放心权重的初始值建议先锁定前 10 个 epoch 的权重后面再放开让参数自适应。7. 从这篇之后往哪走序列模型、图数据与边缘推理7.1 文本和时序数据LSTM 的输入格式与使用要点如果你的数据不是图像而是股价、录音、文本这类序列LSTM长短期记忆网络是经典入门选择。LSTM 的输入是三维张量形状为(sequence_length, batch_size, input_size)注意 PyTorch 的默认布局是seq_len放最前面很多人在这里反复踩坑。一个简单的用法import torch.nn as nn lstm nn.LSTM(input_size10, hidden_size64, num_layers2, batch_firstTrue) # 此时输入形状为 (batch, seq_len, input_size) x torch.randn(32, 20, 10) out, (h_n, c_n) lstm(x) # out 形状: (batch, seq_len, hidden_size) # h_n 形状: (num_layers, batch, hidden_size)LSTM 的门控机制让它能记住长期信息训练时比普通 RNN 稳定得多。不过要注意梯度裁剪梯度爆炸在序列模型里很常见torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)7.2 人脸表情识别与图神经网络为什么图上的卷积不同普通 CNN 处理的是规则网格数据但有些场景的数据天然是图结构比如社交网络关系、分子结构甚至人脸关键点构成的拓扑图。人脸表情识别里把脸部关键点建模成图用图卷积网络GCN能捕捉关键点之间的空间依赖关系效果比单纯用 CNN 更细腻。图神经网络的思路是每个节点通过聚合邻居节点的特征来更新自己的表示这其实和 CNN 的局部感受野概念很像只不过邻居关系由图的边结构定义不依赖规则网格。PyTorch 生态里常用torch_geometric实现 GCN这类高级方向不建议新手一上来就冲先把 CNN 和 LSTM 的基本功练扎实再往图神经网络爬。7.3 边缘计算与推理优化从训练好的模型到真正落地模型训练好了只是第一步真正落地部署才是项目的终点。边缘设备算力有限模型要经过一系列优化才能流畅跑起来。我现在最常做的优化链路是训练好的 PyTorch 模型导出成 ONNX再做量化。量化的核心是把 FP32 浮点数变成 INT8 整数参数量直接缩小四倍推理速度显著提升代价是精度略微下降。量化后的模型在小计算设备上可以跑到几十毫秒每帧这个速度对人脸识别这样的实时应用来说勉强够用。做这一块时建议先用自带脚本把模型导出来测量原始推理耗时和量化后的耗时对比再根据业务场景决定要不要用稀疏化或者知识蒸馏做进一步压缩。从手写前向传播到工程化训练再到模型部署这条路每一步都需要把维度变化和参数流动搞透。这篇涉及的代码量不算大但每一行都值得亲手敲一遍尤其是反向传播的手算对照和 CNN 的维度推算。把这些基础扎稳了后面不管碰强化学习、生成模型还是多智能体系统都不会再被底层的数据流搞到崩溃。