Pytorch的坑之 为什么模型参数和输入不变,输出结果却变了(BN的坑)

引言

不知道小伙伴使用pytorch时是否遇到了下面的情况:我们加载训练好的参数,然后输入经过网络,但是不经过梯度下降训练,也就是说模型的参数没有更改,这是我们仍然会发现我们输出的指标(例如准确率)下降。

现象

最近跑的一个程序,加载好预训练模型

checkpoint = load_checkpoint(args.resume)
model.load_state_dict(checkpoint['state_dict'])

随后我们加载好预训练模型,然后执行下面一段代码: 模型设为train模式,然后经过网络得到输出,此时我们并没有对输出的结果进行训练,参数没有变。然后模式设为eval模式,我们得到的最终评价指标res。

self.model.train()
input_dict = self._parse_data(inputs)
output_dict = self._forward(input_dict)
self.model.eval()
res = evaluator.evaluate(test_loader,  dataset=test_dataset)

但是不同的res实验结果却不一样。

分析

经过查阅资料发现模型内部有一个”捣蛋鬼“。那就是Batch Normalization。Batch Normalization(简称为BN),中文翻译成批规范化,是在深度学习中普遍使用的一种技术,通常用于解决多层神经网络中间层的协方差偏移(Internal Covariate Shift)问题,类似于网络输入进行零均值化和方差归一化的操作,不过是在中间层的输入中操作而已,具体原理不累述了。
题的关键 BN层的统计数据更新是在每一次训练阶段model.train()后的forward()方法中自动实现的,而不是在梯度计算与反向传播中更新optim.step()中完成。
所以说即使不梯度计算,整个模型还是由于BN的变化更新了。

拓展

BN其实坑还是不少的,还有两个值得小伙伴们注意的坑。

第一个是我们如果加载一个与训练好包括BN的模型迁移到其他任务,例如想着直接在物体检测框架的模型上添加mask分支,冻结detection参数,只训练mask相关的参数。那么如果不冻结BN的话,detection相关的性能指标一直在变!如何冻结呢?
正确打开方式如下:

def fix_bn(m):
    classname = m.__class__.__name__
    if classname.find('BatchNorm') != -1:
        m.eval()

model = models.resnet50(pretrained=True)
model.cuda()
model.train()
model.apply(fix_bn) 

第二个坑就是当使用网络进行推理的时候,发现每次更改测试集的batch size大小竟然会导致推理结果不同,甚至产生错误结果,后来发现在网络中定义了BN层,BN层在训练过程中,会将一个Batch的中的数据转变成正太分布,在推理过程中使用训练过程中的参数对数据进行处理,然而网络并不知道你是在训练还是测试阶段,因此,需要手动的加上:

model.train()
model.eval()
<think>嗯,用户想用PyTorch实现一个一维的ResNet18模型,还要集成空间注意力机制(SAM)。首先,我需要回忆一下ResNet18的结构。ResNet18有四个基本的残差块,每个块包含两个卷积层,每个卷积层后面跟着BatchNormReLU激活函数。对于一维的情况,应该将二维卷积Conv2d替换成一维的Conv1d,同时调整BatchNorm为BatchNorm1d。 接下来是空间注意力机制(SAM)。根据引用中的信息,空间注意力通常关注特征图的空间位置重要性。比如,CBAM中的空间注意力模块使用全局平均池化最大池化,然后将结果拼接起来,通过卷积层生成注意力权重。所以在一维情况下,应该将二维池化操作换成一维的,比如AdaptiveAvgPool1dAdaptiveMaxPool1d,然后用一维卷积生成注意力图。 然后,如何将SAM集成到ResNet18中呢?通常在残差块之后添加SAM模块。比如,在每个残差块的第二个卷积层之后插入SAM,这样可以让注意力机制调整特征图的空间权重,再与残差连接相加。 现在,开始构建模型结构。首先定义ResNet18的一维基础结构,然后插入SAM模块。需要注意的是,每个残差块中的下采样可能需要处理维度匹配的情况,这时候需要使用1x1的卷积来调整通道数。 在PyTorch中实现SAM模块时,要确保张量的维度正确。例如,输入特征图的形状应该是(batch_size, channels, length),经过池化后得到两个(batch_size, 1, length)的特征,拼接后(batch_size, 2, length),然后用一个一维卷积(kernel_size=7,padding=3)处理,生成空间注意力权重,再通过sigmoid激活函数,最后与原特征相乘。 编写代码时,首先创建SAM类,然后修改ResNet的残差块,在forward函数中将SAM应用到特征上。测试时要注意输入数据的维度是否正确,比如输入模拟数据(batch_size, 1, 1024)是否符合预期。 还需要考虑训练时的参数初始化,可能使用Kaiming初始化,并选择合适的优化器。另外,注意力机制的位置可能需要根据具体任务调整,比如放在每个残差块之后,或者在特定阶段添加。 用户可能遇到的问题包括维度匹配,比如池化后的拼接是否正确,卷积核大小padding的设置是否保持长度不变,以及如何将SAM集成到现有的ResNet结构中。此外,可能还需要调整SAM的参数,比如卷积核的大小,或者是否结合通道注意力机制(如CBAM中的方法),但用户明确要求的是空间注意力,所以暂时需要通道部分。 最后,验证实现的正确性,通过前向传播测试,确保没有错误,并且SAM模块确实影响了特征图的权重分布。可能还需要参考相关的论文,如GAM或CBAM,确保实现符合空间注意力的设计原则。</think>以下是为您构建的基于PyTorch实现1D ResNet18集成空间注意力机制(SAM)的完整解决方案: ### 一、模型结构设计 1. **ResNet18基础结构** - 基础卷积块:$C_{in} \rightarrow Conv1D(k=7,s=2,p=3) \rightarrow BN \rightarrow ReLU \rightarrow MaxPool1d$ - 残差块结构(每个块含两个卷积层): $$x \rightarrow Conv1D(k=3) \rightarrow BN \rightarrow ReLU \rightarrow Conv1D(k=3) \rightarrow BN \rightarrow (+x) \rightarrow ReLU$$ 2. **空间注意力模块(SAM)** 参考CBAM空间注意力机制[^3],调整为一维实现: ```python class SpatialAttention1D(nn.Module): def __init__(self, kernel_size=7): super().__init__() self.conv = nn.Conv1d(2, 1, kernel_size, padding=kernel_size//2) def forward(self, x): avg_out = torch.mean(x, dim=1, keepdim=True) max_out, _ = torch.max(x, dim=1, keepdim=True) concat = torch.cat([avg_out, max_out], dim=1) att = torch.sigmoid(self.conv(concat)) return x * att ``` ### 二、完整实现代码 ```python import torch import torch.nn as nn class SAMResNet18(nn.Module): def __init__(self, input_channels=1, num_classes=10): super().__init__() # Initial Conv Block self.conv1 = nn.Conv1d(input_channels, 64, kernel_size=7, stride=2, padding=3) self.bn1 = nn.BatchNorm1d(64) self.relu = nn.ReLU() self.maxpool = nn.MaxPool1d(kernel_size=3, stride=2, padding=1) # Residual Blocks with SAM self.layer1 = self._make_layer(64, 64, blocks=2, stride=1) self.layer2 = self._make_layer(64, 128, blocks=2, stride=2) self.layer3 = self._make_layer(128, 256, blocks=2, stride=2) self.layer4 = self._make_layer(256, 512, blocks=2, stride=2) # Spatial Attention Module self.sam = SpatialAttention1D() # Classifier self.avgpool = nn.AdaptiveAvgPool1d(1) self.fc = nn.Linear(512, num_classes) def _make_layer(self, in_channels, out_channels, blocks, stride): layers = [] layers.append(ResBlock1D(in_channels, out_channels, stride)) for _ in range(1, blocks): layers.append(ResBlock1D(out_channels, out_channels, stride=1)) 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.sam(x) # Apply spatial attention x = self.layer3(x) x = self.sam(x) # Apply again x = self.layer4(x) x = self.avgpool(x) x = torch.flatten(x, 1) x = self.fc(x) return x class ResBlock1D(nn.Module): def __init__(self, in_channels, out_channels, stride=1): super().__init__() self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1) self.bn1 = nn.BatchNorm1d(out_channels) self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size=3, padding=1) self.bn2 = nn.BatchNorm1d(out_channels) self.shortcut = nn.Sequential() if stride != 1 or in_channels != out_channels: self.shortcut = nn.Sequential( nn.Conv1d(in_channels, out_channels, kernel_size=1, stride=stride), nn.BatchNorm1d(out_channels) ) def forward(self, x): residual = self.shortcut(x) x = self.conv1(x) x = self.bn1(x) x = self.relu(x) x = self.conv2(x) x = self.bn2(x) x += residual return self.relu(x) ``` ### 三、关键实现细节 1. **维度匹配**:通过`AdaptiveAvgPool1d`确保同长度的输入最终都能pool到相同维度 2. **注意力位置**:在第二、第三残差块后插入SAM模块,参考GAM的设计理念[^4] 3. **参数初始化**:建议使用He初始化方法 ```python def _initialize_weights(self): for m in self.modules(): if isinstance(m, nn.Conv1d): nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') ``` ### 四、模型使用示例 ```python # 初始化模型 model = SAMResNet18(input_channels=1, num_classes=10) # 测试输入 x = torch.randn(32, 1, 1024) # (batch, channels, length) output = model(x) print(output.shape) # torch.Size([32, 10]) ``` ### 五、性能优化建议 1. 使用混合精度训练:`torch.cuda.amp` 2. 添加通道注意力模块构建CBAM结构[^3] 3. 采用深度可分离卷积降低参数量
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值