ResNet深度解析:从退化问题到残差块实现,掌握深度学习核心架构
1. 从“堆层数”到“残差学习”ResNet到底在解决什么问题如果你在2015年之前尝试过训练一个超过20层的卷积神经网络大概率会碰到一个非常反直觉的现象训练误差不降反升。这不是过拟合——过拟合的表现是训练误差持续下降而验证误差上升但当时的情况是训练集上的误差本身就比浅层网络更高。换句话说网络更深了连“死记硬背”训练数据的能力都变差了。这个问题在当时困扰了很多人因为按照直觉更深的网络表达能力应该更强才对。ResNet深度残差网络就是冲着这个问题去的。它的核心贡献不是“发明了更深的网络”而是提出了一种让深层网络变得可训练的结构设计——残差连接。这个设计后来几乎成了深度学习领域的标配不管你是做图像分类、目标检测、语义分割还是做自然语言处理、语音识别残差连接无处不在。这篇文章适合谁看如果你正在学深度学习已经了解卷积神经网络的基本结构卷积层、池化层、全连接层但对ResNet的理解还停留在“它很深、它很强”这个层面那这篇内容就是为你准备的。我会从“为什么深层网络难训练”这个根问题讲起把残差块的设计逻辑、两种残差块的区别、网络结构的演进、以及实际训练中的关键细节都拆开讲清楚。读完之后你不仅能理解ResNet的设计精髓还能自己动手搭一个残差网络并且知道训练时该注意什么。关键词方面这篇文章会围绕深度残差网络、ResNet、残差块、跳跃连接、退化问题、Batch Normalization、瓶颈结构、迁移学习这些核心概念展开每个概念都会结合实际操作和代码来讲解不做纯理论堆砌。2. 退化问题的本质为什么加深网络反而更难训练2.1 退化问题不是过拟合也不是梯度消失那么简单很多人第一次接触ResNet时会把它解决的问题简单归结为“梯度消失”。这个理解不算错但不够准确。梯度消失确实是深层网络的经典难题——反向传播时梯度逐层相乘如果每层的梯度都小于1经过几十层之后梯度就趋近于零浅层的参数几乎不更新。但ResNet论文中揭示的“退化问题”比梯度消失更微妙。退化问题的核心现象是一个56层的网络在训练集上的表现竟然不如一个20层的网络。注意这里说的不是验证集是训练集。这意味着深层网络连拟合训练数据的能力都退化了。如果仅仅是梯度消失导致的参数不更新那至少浅层网络能学到的特征深层网络也应该能学到——毕竟深层网络可以看作是在浅层网络后面叠加了更多层最差的情况是后面那些层学成恒等映射输入等于输出这样深层网络的性能至少不应该比浅层差。但实际训练中网络学不会恒等映射。这就是退化问题的本质深层网络难以学习恒等映射。随机初始化的额外层会把原本学好的特征破坏掉而让这些层学到“什么都不做”这件事对于非线性层堆叠的结构来说并不容易。2.2 恒等映射为什么这么难学你可以这样理解假设有一个20层的网络已经训练得不错了现在你想把它加深到56层。理想情况下新增的36层应该学会恒等映射——输入什么就输出什么这样56层网络的性能至少和20层一样。但问题是这36层里每一层都包含卷积、批归一化、激活函数这些操作叠加起来要让最终输出严格等于输入需要权重和偏置精确配合。随机梯度下降在这么大的参数空间里搜索很难恰好找到这组“恒等映射”的解。打个比方你让一个人闭着眼睛在一张纸上画一条直线要求他画完后笔尖回到起点。如果他只画一小段浅层网络很容易控制但如果让他画很长一段再回来深层网络偏差就会累积很难精确回到原点。恒等映射对于多层非线性变换的堆叠来说就是那个“精确回到原点”的要求。ResNet的解决方案非常巧妙既然让网络学会“恒等映射”很难那我直接把输入加到输出上让网络只需要学习“残差”——也就是输入和期望输出之间的差值。如果最优的输出就是输入本身那网络只需要把残差学成零就行了这比学一个恒等映射简单得多。2.3 残差连接的数学表达与直觉理解残差块的基本形式可以写成y F(x, {Wi}) x其中x是输入F(x, {Wi})是残差函数也就是需要学习的部分y是输出。当F(x) 0时y x这就是恒等映射。网络只需要把F(x)推向零而不是从头学习一个恒等映射。这个设计的精妙之处在于残差连接提供了一条“高速公路”让梯度可以直接从深层回传到浅层。反向传播时梯度经过残差连接时相当于乘以1不会像经过多层非线性变换那样逐层衰减。这就同时缓解了梯度消失和退化问题。从另一个角度看残差连接让网络变成了一个“集成模型”——你可以把ResNet看作是多条不同深度路径的集成浅层路径负责捕捉低级特征深层路径负责高级语义残差连接让信息可以在不同深度之间自由流动。3. 残差块的两副面孔基础块与瓶颈块的设计取舍3.1 基础残差块两层3x3卷积的经典组合ResNet-18和ResNet-34使用的是基础残差块Basic Block结构很简单两个3x3卷积层每个卷积后面接批归一化和ReLU激活最后把输入加到输出上再过一次ReLU。具体流程是这样的输入x经过第一个3x3卷积输出通道数不变经过Batch Normalization和ReLU再经过第二个3x3卷积输出通道数不变经过Batch Normalization将原始输入x与步骤4的输出相加最后经过ReLU激活这里有一个关键细节第二个卷积之后、相加之前不能加ReLU。因为如果加了ReLU输出就变成非负的了而残差连接要求输出可以取任意值包括负值否则相加之后的信息会被截断。这个细节在实际写代码时很容易搞错我见过不少人在第二个卷积后面顺手加了ReLU结果训练效果大打折扣。基础块适用于较浅的ResNet18层、34层因为它的参数量和计算量相对较小在浅层网络中已经足够表达。3.2 瓶颈块用1x1卷积压缩再扩张当网络加深到50层、101层、152层时如果还用基础块参数量和计算量会急剧膨胀。这时候就需要瓶颈块Bottleneck Block出场了。瓶颈块的设计思路是先用1x1卷积把通道数压缩再用3x3卷积在低维空间做特征提取最后再用1x1卷积把通道数扩张回来。典型的压缩比例是4倍——比如输入256通道先压到64通道3x3卷积在64通道上操作再扩回256通道。为什么这样设计因为3x3卷积的计算量是跟通道数的平方成正比的。如果直接在256通道上做3x3卷积计算量是256×256×9而先压到64通道再扩回来3x3卷积的计算量变成64×64×9加上两个1x1卷积的计算量总体算下来比直接做3x3卷积少了将近一半。这就是瓶颈结构的价值——在保持表达能力的同时大幅降低计算成本。瓶颈块还有一个细节当输入输出通道数不一致或者特征图尺寸需要下采样时残差连接上需要加一个1x1卷积来调整维度和尺寸这个叫做“投影捷径”projection shortcut。如果输入输出维度一致就直接用恒等映射。3.3 两种块的对比与选型建议对比维度基础残差块瓶颈残差块适用网络深度ResNet-18/34ResNet-50/101/152核心结构两个3x3卷积1x1降维 3x3卷积 1x1升维参数量较大相同通道数下较小通过压缩通道计算量较高较低残差连接处理维度一致时直接相加维度不一致时用1x1卷积投影实际使用场景小数据集、轻量级任务大数据集、深层网络选型建议很直接如果你的任务比较简单、数据集不大用ResNet-18或34就够了基础块的结构更简单训练也更容易收敛。如果你需要更强的表达能力、有足够的计算资源那就上ResNet-50或更深瓶颈块能帮你省下大量显存和计算时间。4. 从18层到152层ResNet家族的结构演进与设计逻辑4.1 整体架构阶段划分与下采样策略ResNet的整体架构可以分成几个阶段首先是7x7的大卷积核加最大池化做初步特征提取和下采样然后进入四个阶段stage每个阶段由若干个残差块堆叠而成阶段之间通过步长为2的卷积做下采样特征图尺寸减半、通道数翻倍。以ResNet-50为例四个阶段的残差块数量分别是3、4、6、3通道数分别是256、512、1024、2048。这个“3-4-6-3”的配置不是随便定的它遵循了一个经验规律随着特征图尺寸减小通道数增加每个阶段的残差块数量也相应增加。这是因为浅层阶段处理的是高分辨率、低语义的特征计算量大但每个特征的信息量少深层阶段处理的是低分辨率、高语义的特征需要更多的通道来编码丰富的语义信息。4.2 下采样时残差连接怎么处理当特征图尺寸减半、通道数翻倍时残差连接上的恒等映射就对不上了——输入是56x56x64输出是28x28x128直接相加会报错。ResNet的处理方式是在捷径路径上加一个1x1卷积步长为2输出通道数设为与主路径一致。这样既完成了下采样又对齐了通道数。这里有一个实操中容易忽略的点这个1x1投影卷积后面通常不加Batch Normalization。虽然有些实现会加但原始论文和主流实现都不加。原因是这个投影卷积的作用只是维度对齐加BN反而会增加不必要的复杂度和计算量。4.3 各版本ResNet的参数量与计算量对比网络版本残差块类型残差块配置参数量约FLOPs约Top-1准确率ImageNetResNet-18基础块[2,2,2,2]11.7M1.8G69.8%ResNet-34基础块[3,4,6,3]21.8M3.6G73.3%ResNet-50瓶颈块[3,4,6,3]25.6M3.8G76.1%ResNet-101瓶颈块[3,4,23,3]44.5M7.6G77.4%ResNet-152瓶颈块[3,8,36,3]60.2M11.3G78.3%从表中可以看出ResNet-50的参数量只比ResNet-34多了不到4M但准确率提升了近3个百分点这就是瓶颈结构的威力。而ResNet-101和152虽然参数量和计算量大幅增加但准确率提升的边际效益在递减。实际选型时ResNet-50通常是性价比最高的选择。5. 动手搭建一个ResNet从零实现的完整路径5.1 环境准备与依赖确认在开始写代码之前先确认你的环境。我推荐用PyTorch因为它的动态图机制对调试残差连接这种结构非常友好。需要的依赖很简单pip install torch torchvision如果你有GPU确认CUDA可用import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))CPU也能跑但训练ResNet-50这种规模的网络CPU会慢到让你怀疑人生。建议至少有一块显存8G以上的GPU。5.2 基础残差块的代码实现与关键注释先实现基础残差块import torch import torch.nn as nn class BasicBlock(nn.Module): expansion 1 # 输出通道数相对于中间通道数的倍数 def __init__(self, in_channels, out_channels, stride1, downsampleNone): super(BasicBlock, self).__init__() # 第一个3x3卷积 self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) # 第二个3x3卷积 self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) # 下采样捷径当维度不匹配时使用 self.downsample downsample def forward(self, x): identity x # 保存输入用于残差连接 out self.conv1(x) out self.bn1(out) out self.relu(out) out self.conv2(out) out self.bn2(out) # 注意这里不加ReLU直接与identity相加 if self.downsample is not None: identity self.downsample(x) out identity # 残差连接 out self.relu(out) # 相加后再激活 return out这段代码里有几个关键点需要强调。第一biasFalse——因为后面接了Batch Normalization卷积层的偏置会被BN的均值减法抵消掉加上去纯属浪费参数。第二第二个卷积后面不加ReLU原因前面已经解释过了。第三downsample是一个独立的模块通常是一个1x1卷积加BN用来在维度不匹配时调整identity的维度。5.3 瓶颈块的实现与通道数变化追踪瓶颈块的实现稍微复杂一点因为涉及通道数的压缩和扩张class Bottleneck(nn.Module): expansion 4 # 输出通道数是中间通道数的4倍 def __init__(self, in_channels, mid_channels, stride1, downsampleNone): super(Bottleneck, self).__init__() # 1x1卷积降维 self.conv1 nn.Conv2d(in_channels, mid_channels, kernel_size1, biasFalse) self.bn1 nn.BatchNorm2d(mid_channels) # 3x3卷积提取特征 self.conv2 nn.Conv2d(mid_channels, mid_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn2 nn.BatchNorm2d(mid_channels) # 1x1卷积升维 self.conv3 nn.Conv2d(mid_channels, mid_channels * self.expansion, kernel_size1, biasFalse) self.bn3 nn.BatchNorm2d(mid_channels * self.expansion) self.relu nn.ReLU(inplaceTrue) self.downsample downsample def forward(self, x): identity x out self.conv1(x) out self.bn1(out) out self.relu(out) out self.conv2(out) out self.bn2(out) out self.relu(out) out self.conv3(out) out self.bn3(out) if self.downsample is not None: identity self.downsample(x) out identity out self.relu(out) return out注意expansion4这个类属性它表示瓶颈块的输出通道数是中间通道数的4倍。在构建网络时这个属性用来计算每个阶段的输出通道数。比如ResNet-50的第一个阶段中间通道数是64输出通道数就是64×4256。5.4 组装完整的ResNet网络有了残差块之后组装网络就相对直接了class ResNet(nn.Module): def __init__(self, block, layers, num_classes1000): super(ResNet, self).__init__() self.in_channels 64 # 初始卷积层 self.conv1 nn.Conv2d(3, 64, kernel_size7, stride2, padding3, biasFalse) self.bn1 nn.BatchNorm2d(64) self.relu nn.ReLU(inplaceTrue) self.maxpool nn.MaxPool2d(kernel_size3, stride2, padding1) # 四个阶段 self.layer1 self._make_layer(block, 64, layers[0], stride1) self.layer2 self._make_layer(block, 128, layers[1], stride2) self.layer3 self._make_layer(block, 256, layers[2], stride2) self.layer4 self._make_layer(block, 512, layers[3], stride2) # 全局平均池化和全连接层 self.avgpool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear(512 * block.expansion, num_classes) def _make_layer(self, block, out_channels, num_blocks, stride): downsample None # 当stride不为1或通道数不匹配时需要下采样捷径 if stride ! 1 or self.in_channels ! out_channels * block.expansion: downsample nn.Sequential( nn.Conv2d(self.in_channels, out_channels * block.expansion, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels * block.expansion), ) layers [] # 第一个块可能需要下采样 if block Bottleneck: layers.append(block(self.in_channels, out_channels, stride, downsample)) else: layers.append(block(self.in_channels, out_channels, stride, downsample)) self.in_channels out_channels * block.expansion # 后续块不需要下采样 for _ in range(1, num_blocks): if block Bottleneck: layers.append(block(self.in_channels, out_channels)) else: layers.append(block(self.in_channels, out_channels)) return nn.Sequential(*layers) def forward(self, x): x self.conv1(x) x self.bn1(x) x self.relu(x) x self.maxpool(x) x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.layer4(x) x self.avgpool(x) x torch.flatten(x, 1) x self.fc(x) return x组装网络时最容易出错的地方是_make_layer里的通道数追踪。self.in_channels需要在每个阶段结束后更新为当前阶段的输出通道数这样下一个阶段才能正确判断是否需要下采样捷径。我建议在写完之后打印一下每一层的输出形状确认维度变化符合预期。6. 训练ResNet时那些文档不会告诉你的实操细节6.1 学习率预热与衰减策略ResNet论文中使用的学习率策略是初始学习率0.1每30个epoch除以10。但这是针对ImageNet这种大规模数据集的。如果你在自己的数据集上训练尤其是数据集比较小的时候直接上0.1的学习率很可能导致训练初期loss爆炸。我的经验是先用一个较小的学习率比如0.01跑几个epoch观察loss下降是否平稳再决定是否调大。另外学习率预热warmup在训练ResNet时非常有用——前5个epoch把学习率从0线性增加到初始值可以让网络在初期稳定地找到下降方向避免一开始就震荡。# 学习率预热示例 def adjust_learning_rate(optimizer, epoch, base_lr0.1, warmup_epochs5): if epoch warmup_epochs: lr base_lr * (epoch 1) / warmup_epochs else: lr base_lr * (0.1 ** ((epoch - warmup_epochs) // 30)) for param_group in optimizer.param_groups: param_group[lr] lr6.2 Batch Normalization与权重初始化的配合ResNet大量使用了Batch Normalization这带来一个好处对权重初始化的敏感度降低了。但也不是随便初始化就行。PyTorch默认的Kaiming初始化对于ResNet来说已经够用但如果你自己实现卷积层记得用Kaiming正态初始化fan_out模式这样可以让每层输出的方差保持一致。另外一个细节BN层的weight初始化为1bias初始化为0。这是PyTorch的默认行为但如果你手动改了初始化记得把BN层恢复成这个配置。BN的weight如果初始化太小相当于把特征压缩了会影响残差连接的信息流动。6.3 数据增强与正则化的实际效果在ImageNet上训练ResNet时标准的数据增强是随机裁剪从256x256裁剪到224x224加随机水平翻转。如果你在自己的数据集上训练数据量不够大的话建议加上颜色抖动Color Jitter和随机旋转。但要注意旋转角度不要太大尤其是对于方向敏感的任务比如数字识别过度旋转会引入噪声。权重衰减方面ResNet论文用的是1e-4。这个值在大多数场景下都适用。如果你发现训练集准确率很高但验证集准确率上不去可以适当加大权重衰减到5e-4或1e-3。Dropout在ResNet中通常不用因为BN已经起到了正则化的作用再加Dropout反而可能拖慢收敛。6.4 迁移学习加载预训练权重的正确姿势实际项目中很少有人从零训练ResNet大多数情况是加载预训练权重做微调。这里有几个坑需要注意第一替换全连接层时要注意输入维度。ResNet-50的全连接层输入是2048512×4ResNet-18是512。如果你把num_classes改了只替换最后一层就行前面的层不用动。第二冻结策略要分阶段。如果目标数据集和预训练数据集差异很大比如从自然图像迁移到医学图像建议先冻结前面的层只训练后面的层和全连接层跑几个epoch之后再解冻全部层做微调。如果差异不大可以直接全部解冻。第三学习率要调小。微调时的学习率通常设为从头训练的十分之一甚至更小比如1e-3或1e-4。因为预训练权重已经在一个很好的位置了学习率太大会把学到的特征破坏掉。# 加载预训练权重并替换分类头 model torchvision.models.resnet50(pretrainedTrue) num_features model.fc.in_features model.fc nn.Linear(num_features, num_classes) # 替换为你的类别数 # 冻结前面的层 for name, param in model.named_parameters(): if fc not in name: param.requires_grad False # 只优化全连接层 optimizer torch.optim.SGD(model.fc.parameters(), lr1e-3, momentum0.9)7. 残差连接之后它如何影响了后续的网络设计7.1 残差思想在目标检测与分割中的延伸ResNet本身是一个分类网络但它的残差结构被广泛移植到了其他任务中。目标检测领域的Faster R-CNN、YOLO系列语义分割领域的FCN、U-Net都大量使用了ResNet作为骨干网络。原因很简单残差连接让深层骨干网络变得可训练而深层网络提取的特征语义更强对小目标和复杂场景的识别能力更好。在U-Net中跳跃连接skip connection和ResNet的残差连接有异曲同工之妙但目的不完全一样。U-Net的跳跃连接是把编码器的特征图直接拼接到解码器上目的是保留空间细节ResNet的残差连接是把输入加到输出上目的是让梯度更好地流动。两者可以结合使用比如在U-Net的每个卷积块里加入残差连接。7.2 DenseNet与ResNeXt残差连接的两个进化方向DenseNet把残差连接推到了极致每一层的输入都来自前面所有层的输出拼接。这样做的好处是特征复用更充分参数量更少缺点是显存占用大因为每一层的输出都要保存下来用于后续层的拼接。ResNeXt则是在残差块内部做文章把3x3卷积拆分成多个并行的组卷积每个组独立计算最后拼接。这种“分组卷积残差连接”的设计在参数量相近的情况下准确率比ResNet更高。它的核心思想是增加“基数”cardinality——也就是并行路径的数量而不是单纯增加深度或宽度。7.3 实际项目中的选型建议如果你现在要做一个图像分类项目我的建议是数据量小于1万张用ResNet-18或34配合强数据增强和迁移学习。不要上太深的网络容易过拟合。数据量在1万到10万张ResNet-50是首选瓶颈结构在参数量和准确率之间取得了很好的平衡。数据量大于10万张可以考虑ResNet-101或152但要注意计算资源。如果资源有限ResNeXt-10132x4d是比ResNet-152更好的选择。移动端或嵌入式部署考虑ResNet-18配合通道剪枝或量化或者直接用MobileNet系列。还有一个经验不要盲目追求准确率的小数点后几位。在实际业务中推理速度、模型大小、部署难度往往比那0.5%的准确率提升更重要。ResNet-50在大多数场景下已经足够好把精力花在数据质量和后处理上收益往往比换更深的网络更大。8. 我踩过的那些坑ResNet实操中的常见问题与排查思路8.1 训练loss不下降或震荡剧烈这是最常见的问题。排查顺序应该是先检查数据预处理有没有问题比如归一化的均值和方差是否匹配再检查学习率是不是太大然后检查Batch Normalization的momentum参数。BN的默认momentum是0.1如果batch size很小比如小于16BN的统计量会很不稳定这时候可以考虑用Group Normalization替代或者增大batch size。还有一个容易被忽略的点残差连接相加之后的值域。如果残差函数的输出方差很大加上identity之后会导致数值不稳定。这时候可以在残差函数的最后一个BN层之前加一个缩放系数比如0.1但这属于比较进阶的技巧一般用标准的BN就能解决。8.2 验证集准确率远低于训练集如果训练集准确率95%而验证集只有70%大概率是过拟合。解决方案按优先级排序加数据增强、加权重衰减、加Dropout虽然ResNet不常用但可以试试、减小模型规模。如果训练集准确率本身就不高比如80%那可能是欠拟合需要增大模型或延长训练时间。还有一个特殊情况验证集的预处理和训练集不一致。比如训练时用了随机裁剪验证时用了中心裁剪这本身没问题但如果归一化参数不一致就会导致验证集准确率异常低。检查一下transforms.Normalize的均值和方差是否在训练和验证时保持一致。8.3 显存不够用的优化策略ResNet-50在batch size为32、输入224x224的情况下大约需要8G显存。如果显存不够可以尝试以下方法减小batch size但不要小于8否则BN不稳定使用混合精度训练AMP可以节省约40%显存使用梯度累积用小的batch size模拟大的batch size使用梯度检查点gradient checkpointing用计算时间换显存混合精度训练现在在PyTorch里已经很简单了from torch.cuda.amp import autocast, GradScaler scaler GradScaler() for inputs, labels in dataloader: optimizer.zero_grad() with autocast(): outputs model(inputs) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()8.4 模型推理速度慢的排查方向如果推理速度不达预期先确认是不是用了GPU。然后检查输入尺寸——224x224和320x320的推理时间差很多。再检查是不是用了太大的batch size导致显存带宽瓶颈。最后如果部署在边缘设备上考虑用TensorRT或ONNX Runtime做推理优化通常能提速2到3倍。还有一个容易忽略的点PyTorch的默认卷积算法在推理时不一定是最优的。可以设置torch.backends.cudnn.benchmark True让cuDNN自动搜索最快的卷积算法。但这个设置只适合输入尺寸固定的场景如果输入尺寸变化频繁反而会拖慢速度。9. 关于ResNet我个人的几点体会ResNet从2015年提出到现在已经快十年了但它依然是深度学习领域最经典、最常用的网络架构之一。我自己的体会是ResNet的价值不仅在于它解决了深层网络的训练问题更在于它提供了一种通用的设计思路当直接学习目标映射很难时转而学习残差。这个思路后来被用在了很多地方比如Transformer里的残差连接、扩散模型里的残差预测本质上都是同一个思想。另外ResNet的代码实现虽然不复杂但细节很多。我建议每个学深度学习的人都自己从零实现一遍ResNet不要直接调torchvision.models.resnet50()。自己写一遍你才会真正理解downsample的作用、expansion的含义、以及为什么第二个卷积后面不能加ReLU。这些细节在调参和排错时非常有用。最后分享一个小技巧如果你在训练ResNet时发现某个阶段的loss突然跳变可以检查一下那个阶段的残差块里下采样捷径的步长和主路径的步长是否一致。我遇到过好几次因为步长不匹配导致特征图尺寸对不上最后相加时报错的情况。写代码时多打印形状能省下大量调试时间。

相关新闻

数据在内存中的存储:从字节序到内存对齐的底层原理与实战

数据在内存中的存储:从字节序到内存对齐的底层原理与实战

我当年第一次认认真真研究“数据在内存中的存储”这个问题,不是因为上课听懂了,而是因为一次线上事故:某个模块在用户量上来以后,偶发性出现计算结果错乱。排查了两天没头绪,最后发现是不同机器之间数据字节序不一致&a…

2026/10/16 11:33:11 阅读更多 →
OpenHarmony上Flutter视频播放器桥接:纹理渲染与状态同步实践

OpenHarmony上Flutter视频播放器桥接:纹理渲染与状态同步实践

我最近在 OpenHarmony 设备上做了一个“软件开发助手”App,核心功能是把视频教程、文档资料和常用命令手册整合到同一个工具里。选型阶段几乎没有犹豫,直接用 Flutter 干活,但真正动到视频播放器时才发现,把一套团队熟悉的 Flutte…

2026/10/16 11:33:11 阅读更多 →
YOLOv4 PyTorch源码深度解析:从CSPNet到NMS的可验证实现

YOLOv4 PyTorch源码深度解析:从CSPNet到NMS的可验证实现

1. 这不是“又一个YOLO教程”,而是一份给真正想搞懂YOLOv4 PyTorch实现的工程师的代码解剖报告你点开这个标题,大概率不是为了找一个能跑通的GitHub仓库链接,也不是想抄几行model YOLOv4()就去交差。你可能刚在论文里读到“CSPNet结构显著缓…

2026/10/16 11:33:11 阅读更多 →

最新新闻

Java老兵AI Agent转型实战:无需Python的工程化落地指南

Java老兵AI Agent转型实战:无需Python的工程化落地指南

1. 一个Java老兵的AI Agent转型之路1.1 为什么我决定从Java转向AI Agent写了十三年代码,Spring全家桶闭着眼睛都能搭出一套微服务架构,JVM调优参数比自家WiFi密码还熟。但去年开始,身边越来越多项目开始提“智能体”“Agent”“自动化决策”这…

2026/10/16 13:26:52 阅读更多 →
17个Deeplink动作速查表:用Shortcuts、Alfred和Raycast一句话控制Itsyhome

17个Deeplink动作速查表:用Shortcuts、Alfred和Raycast一句话控制Itsyhome

【免费下载链接】itsyhome-macos A native macOS menu bar app for controlling your smart home devices. 项目地址: https://gitcode.com/gh_mirrors/it/itsyhome-macos 点击查看 免费下载 Itsyhome 是一款原生的 macOS 菜单栏智能家居 App,支持控制 …

2026/10/16 13:26:52 阅读更多 →
Spring AI扩展包停更不用慌:Java开发者的大模型迁移与破局

Spring AI扩展包停更不用慌:Java开发者的大模型迁移与破局

前阵子社区里都在议论某个国产 Spring AI 扩展包停更的事,说实话,刚看到这个消息的时候我也愣了一下。这个扩展包一度被看作 Java 接入大模型最顺手的路径之一:依赖一引、配置一写,就能把对话补全、向量检索、工具调用接到 Spring…

2026/10/16 13:26:52 阅读更多 →
mingw32 gcc 2.95 旧工具链实战:环境搭建与兼容性避坑指南

mingw32 gcc 2.95 旧工具链实战:环境搭建与兼容性避坑指南

简介:一套专为Windows平台设计的轻量级开源GNU工具集,即MingW32 2.95,面向需要在Windows下编译本地C/C程序的开发者,可调用Windows API直接构建命令行或图形应用。该版本以精简和稳定著称,适合配置老旧开发环境、维护历…

2026/10/16 13:26:52 阅读更多 →
SpringBoot+Vue林业产品推荐系统:从推荐算法到前后端部署全解析

SpringBoot+Vue林业产品推荐系统:从推荐算法到前后端部署全解析

做毕业设计或课程设计的同学,选“SpringBootVue”这套组合的非常多,但要真正落地一个“林业产品推荐系统管理平台”这种带垂直场景的项目,光会CRUD是不够的。标题里这几样东西——Java、MySQL、前后端分离、推荐系统,再加一个林业…

2026/10/16 13:26:52 阅读更多 →
Agent上下文压缩与记忆策略指南:守住Token成本,告别对话失忆

Agent上下文压缩与记忆策略指南:守住Token成本,告别对话失忆

做Agent应用的时间越久,越会发现一个真相:真正卡住Agent上限的往往不是模型能力,而是上下文窗口。我见过太多这样的场景——一个看起来逻辑很完整的多工具Agent,跑到第8轮调用时就开始“失忆”,把用户几分钟前明确提到…

2026/10/16 13:25:49 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →