当前位置: 首页 > news >正文

【自定义一个简单的CNN模型】——深度学习.卷积神经网络

目录

1 自定义一个简单的CNN模型

1.1 语法结构

1.2 举个例子

1.3 使用nn.Sequential的好处

1.4 改进后的代码

1.5 注意事项

1.6 总结一句话

2 自定义CNN模型实现图片边缘检测案例

2.1 代码说明

2.2 技术要点

3 自定义CNN检测模型检测图片特征案例

3.1 思考一个问题

3.2 背后的机制

3.3 为什么这样设计比直接调用forward更好

3.3.1 会跳过nn.Module 的前置/后置处理

3.3.2 可能导致kooks不执行

3.3.4 梯度计算可能不正常

3.4 问题分析

3.5 更复杂的嵌套模块问题

3.6 总结:问什么永远不要调用forward()


1 自定义一个简单的CNN模型

# 使用pytorch自定义一个简单的CNN模型
import torch
import torch.nn as nnclass SimpleCNN(nn.Module):def __init__(self):super(SimpleCNN, self).__init__()self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)self.relu1 = nn.ReLU()self.pool1 = nn.MaxPool2d(2, 2)self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)self.relu2 = nn.ReLU()self.pool2 = nn.MaxPool2d(2, 2)def forward(self, x):x = self.pool1(self.relu1(self.conv1(x)))x = self.pool2(self.relu2(self.conv2(x)))return xif __name__ == '__main__':model = SimpleCNN()print("模型结构:\n", model)

运行效果:

模型结构:SimpleCNN((conv1): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))(relu1): ReLU()(pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)(conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))(relu2): ReLU()(pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)

上面代码虽然没有多大问题,但是看起来还是有点过于繁琐,推荐使用nn.Sequential 进行改进。

nn.Sequential 是 PyTorch 中的一个容器类模块,用于按顺序组织多个神经网络层(nn.Module 的子类),它会按照你添加的顺序依次执行这些层。

1.1 语法结构

torch.nn.Sequential(*args)

你可以将多个网络层作为参数传入 nn.Sequential,它们会按顺序构成一个序列化的网络结构。

1.2 举个例子

import torch.nn as nnmodel = nn.Sequential(nn.Linear(10, 50),nn.ReLU(),nn.Linear(50, 1)
)

这个 model 的执行顺序是:

  1. 输入通过 Linear(10 -> 50)

  2. 经过 ReLU 激活函数

  3. 再通过 Linear(50 -> 1)

等价于:

def forward(x):x = nn.Linear(10, 50)(x)x = nn.ReLU()(x)x = nn.Linear(50, 1)(x)return x

1.3 使用nn.Sequential的好处

  1. 代码简洁:不用手动写 forward 函数,每一层自动按顺序执行。

  2. 结构清晰:适合构建线性堆叠结构的网络(如 CNN 的卷积层、全连接层等)。

  3. 易于调试:可以像列表一样访问各个层,例如 model[0] 表示第一个层。

1.4 改进后的代码

import torch
import torch.nn as nnclass SimpleCNN(nn.Module):def __init__(self):super(SimpleCNN, self).__init__()self.features = nn.Sequential(nn.Conv2d(3, 16, kernel_size=3, padding=1),  # 输入3通道,输出16通道nn.ReLU(),nn.MaxPool2d(2, 2),nn.Conv2d(16, 32, kernel_size=3, padding=1),  # 输入16通道,输出32通道nn.ReLU(),)if __name__ == '__main__':model = SimpleCNN()print("模型结构:\n", model)

运行效果:

模型结构:SimpleCNN((features): Sequential((0): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))(1): ReLU()(2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)(3): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))(4): ReLU())
)

这段代码表示一个卷积特征提取器,输入一张图像后,会依次经过:

  • 第1层卷积 → ReLU → MaxPool

  • 第2层卷积 → ReLU → MaxPool

最终输出的是提取后的特征图(feature maps)。

1.5 注意事项

  • 除了按顺序执行,nn.Sequential 不提供其他功能。

  • 如果你的网络结构有分支、跳跃连接(如 ResNet 中的 shortcut)、多个输入输出等复杂结构,就不适合用 nn.Sequential,而应该自定义 forward 方法。

1.6 总结一句话

nn.Sequential 是一个按顺序执行的神经网络模块容器,适合构建线性堆叠结构的模型,可以简化代码并使结构更清晰。

2 自定义CNN模型实现图片边缘检测案例

实现代码如下:

import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as transforms
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np# 定义简单的CNN模型
class EdgeDetectionCNN(nn.Module):def __init__(self):super(EdgeDetectionCNN, self).__init__()# 使用固定的边缘检测卷积核self.conv1 = nn.Conv2d(1, 2, kernel_size=3, padding=1, bias=False)# 手动设置卷积核权重(水平和垂直边缘检测)sobel_x = torch.tensor([[[[-1, 0, 1],[-2, 0, 2],[-1, 0, 1]]]], dtype=torch.float32)sobel_y = torch.tensor([[[[-1, -2, -1],[0, 0, 0],[1, 2, 1]]]], dtype=torch.float32)# 组合两个卷积核edge_kernels = torch.cat([sobel_x, sobel_y], dim=0)self.conv1.weight = nn.Parameter(edge_kernels, requires_grad=False)def forward(self, x):# 应用边缘检测卷积edge_features = self.conv1(x)# 分离水平和垂直特征horizontal = edge_features[:, 0:1, :, :]vertical = edge_features[:, 1:2, :, :]# 计算边缘强度edge_magnitude = torch.sqrt(horizontal ** 2 + vertical ** 2)return edge_magnitude, horizontal, vertical# 图像预处理
def preprocess_image(image_path):# 打开图像并转换为灰度image = Image.open(image_path).convert('L')transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize(mean=[0.5], std=[0.5])])# 添加batch维度 [1, 1, H, W]image_tensor = transform(image).unsqueeze(0)return image_tensor, image# 可视化结果
def visualize_results(original, horizontal, vertical, magnitude):plt.figure(figsize=(12, 10))# 原始图像plt.subplot(2, 2, 1)plt.imshow(original, cmap='gray')plt.title('Original Image')plt.axis('off')# 水平边缘plt.subplot(2, 2, 2)plt.imshow(horizontal, cmap='gray')plt.title('Horizontal Edges')plt.axis('off')# 垂直边缘plt.subplot(2, 2, 3)plt.imshow(vertical, cmap='gray')plt.title('Vertical Edges')plt.axis('off')# 边缘强度plt.subplot(2, 2, 4)plt.imshow(magnitude, cmap='gray')plt.title('Edge Magnitude')plt.axis('off')plt.tight_layout()plt.savefig('edge_detection_result.png')plt.show()# 主流程
if __name__ == "__main__":# 1. 初始化模型model = EdgeDetectionCNN()# 2. 加载和预处理图像image_tensor, original_image = preprocess_image('./images/bird01.jpg')  # 替换为你的图片路径# 3. 提取边缘特征with torch.no_grad():edge_magnitude, horizontal, vertical = model(image_tensor)# 4. 转换为numpy并后处理horizontal_np = horizontal.squeeze().numpy()vertical_np = vertical.squeeze().numpy()magnitude_np = edge_magnitude.squeeze().numpy()# 5. 可视化结果visualize_results(original_image,horizontal_np,vertical_np,magnitude_np)

运行效果:

2.1 代码说明

  1. 模型架构

    • 自定义CNN包含单个卷积层

    • 使用两个固定的3x3卷积核(Sobel算子的水平和垂直方向)

    • 卷积层权重被冻结(不参与训练)

  2. 边缘检测原理

    • 水平卷积核检测垂直边缘

    • 垂直卷积核检测水平边缘

    • 边缘强度 = √(水平分量² + 垂直分量²)

  3. 处理流程

    • 输入图像转换为灰度图

    • 归一化到[-1, 1]范围

    • 通过卷积层提取特征

    • 分离水平和垂直边缘分量

    • 计算边缘强度图

  4. 输出可视化

    • 原始图像

    • 水平边缘特征图

    • 垂直边缘特征图

    • 边缘强度合成图

2.2 技术要点

  • 使用Sobel算子作为固定卷积核,是传统图像处理中经典的边缘检测方法

  • 通过torch.no_grad() 禁用梯度计算,提高推理效率

  • 特征图后处理包含绝对值强度计算,增强边缘可视化效果

  • 输出特征图与输入图像同分辨率(通过padding=1保持尺寸)

3 自定义CNN检测模型检测图片特征案例

import torch
import torch.nn as nn
import torchvision.transforms as transforms
from PIL import Image
import matplotlib.pyplot as plt# 定义简单的CNN模型
class SimpleCNN(nn.Module):def __init__(self):super(SimpleCNN, self).__init__()# 特征提取层self.features = nn.Sequential(nn.Conv2d(3, 16, kernel_size=3, padding=1),  # 输入3通道,输出16通道nn.ReLU(),nn.MaxPool2d(2, 2),  # 尺寸减半nn.Conv2d(16, 32, kernel_size=3, padding=1),nn.ReLU(),nn.MaxPool2d(2, 2),nn.Conv2d(32, 64, kernel_size=3, padding=1),nn.ReLU(),nn.MaxPool2d(2, 2))def forward(self, x):return self.features(x)# 图像预处理
def preprocess_image(image_path):transform = transforms.Compose([transforms.Resize((224, 224)),  # 调整图像尺寸transforms.ToTensor(),  # 转为Tensortransforms.Normalize(  # 标准化mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225])])image = Image.open(image_path).convert('RGB')return transform(image).unsqueeze(0)  # 增加batch维度# 可视化特征图
def visualize_feature_maps(feature_maps, layer_name):# 将特征图从GPU转到CPU并转为numpyfeatures = feature_maps.squeeze(0).detach().cpu().numpy()# 创建子图fig, axes = plt.subplots(8, 8, figsize=(12, 12))fig.suptitle(f'Feature Maps: {layer_name}', fontsize=16)# 绘制前64个特征图for i, ax in enumerate(axes.flat):if i < features.shape[0]:ax.imshow(features[i], cmap='viridis')ax.set_title(f'Ch {i + 1}')ax.axis('off')plt.tight_layout()plt.show()# 主程序
if __name__ == "__main__":# 1. 创建模型model = SimpleCNN()print("模型结构:\n", model)# 2. 加载并预处理图像image_tensor = preprocess_image('./images/cat.jpg')  # 替换为你的图片路径print("输入图像尺寸:", image_tensor.shape)# 3. 前向传播提取特征with torch.no_grad():feature_maps = model(image_tensor)# 4. 输出特征图信息print("\n提取的特征图尺寸:", feature_maps.shape)  # [batch, channels, height, width]print("特征图数量:", feature_maps.size(1))# 5. 可视化特征图visualize_feature_maps(feature_maps, "Final Conv Layer")

运行效果:

这里重点解释一下transforms.Compose模块。

transforms.Compose 是 PyTorch 中 torchvision.transforms 模块提供的一个非常有用的类,它允许你将多个图像变换组合成一个单一的变换。这对于在数据预处理阶段需要应用一系列转换操作(如调整大小、裁剪、翻转、归一化等)特别方便。当你使用 Compose 来创建一个转换列表时,输入的图像会按照列表中转换的顺序依次经过每个转换。

  • transforms.Resize((224, 224)): 这个转换操作会将输入图像的尺寸调整到指定的大小,这里是 224×224 像素。调整大小是很多深度学习模型的一个常见预处理步骤,因为大多数模型都需要固定的输入尺寸。

  • transforms.ToTensor(): 此操作将PIL Image或NumPy ndarray转换为FloatTensor,并且调整图像像素值的范围从[0, 255]到[0.0, 1.0]。这样做是为了使输入适合于许多深度学习框架中的计算要求。

  • transforms.Normalize(mean, std): 归一化操作。此步骤非常重要,因为它帮助模型更快地收敛。这里的参数mean和std分别表示用于每个通道的均值和标准差。对于RGB三通道图像,您提供了三个均值和三个标准差,对应于R、G、B三个通道。这个特定的均值和标准差集是基于ImageNet数据集计算得出的,常用于预训练模型的输入预处理。

这样,图像就会按顺序经过Resize、ToTensor和Normalize这三个转换步骤,最后得到一个可以输入到神经网络中的张量。

3.1 思考一个问题

这里让我思考另外一个问题,feature_maps = model(image_tensor) 这行代码为什么会自动调用forward方法呢?

3.2 背后的机制

  1. nn.Module基类的魔法方法

    • 当你调用model(input_tensor)时,实际上是在调用模型的__call__方法

    • PyTorch的nn.Module类重写了__call__方法

    • __call__方法内部会调用forward方法,并添加额外的处理(如钩子函数)

  2. 代码等价关系

    # 这两种写法是等价的
    output = model(image_tensor)      # 推荐写法
    output = model.forward(image_tensor)  # 等效写法(但不推荐)
  3. 为什么设计成这样

    • 语法更简洁自然(像函数调用一样使用模型)

    • 允许PyTorch在执行forward前后添加额外逻辑

    • 支持模型钩子(hooks)等高级功能

    • 保持API的一致性(所有模型都可以这样调用)

3.3 为什么这样设计比直接调用forward更好

方式优点缺点
model(input)自动处理hooks、梯度等-
model.forward(input)更"直接"可能绕过重要逻辑

在PyTorch中,永远不要直接调用forward(),因为:

  1. 会跳过nn.Module的前置/后置处理

  2. 可能导致hooks不执行

  3. 梯度计算可能不正常

我来详细解释这三个问题,并通过具体代码示例说明为什么应该避免直接调用 forward() 方法。这些问题是PyTorch框架设计中的关键点,理解它们对正确使用PyTorch至关重要。

3.3.1 会跳过nn.Module 的前置/后置处理

PyTorch 的 nn.Module 在调用 __call__ 方法(即当你使用 model(input) 时)时,会执行一系列重要的前置和后置处理操作。这些操作包括:

  • 设置模块的 training/eval 模式

  • 调用注册的 forward hooks

  • 管理模块调用堆栈

  • 处理递归子模块调用

代码示例:

import torch
import torch.nn as nnclass SimpleModel(nn.Module):def __init__(self):super().__init__()self.layer = nn.Linear(2, 2)def forward(self, x):print("执行 forward 方法")return self.layer(x)# 创建模型实例
model = SimpleModel()# 输入数据
input_data = torch.tensor([[1.0, 2.0]])print("=== 使用 model(input_data) 调用 ===")
output1 = model(input_data)  # 正确方式print("\n=== 使用 model.forward(input_data) 调用 ===")
output2 = model.forward(input_data)  # 错误方式

输出结果:

=== 使用 model(input_data) 调用 ===
执行 forward 方法=== 使用 model.forward(input_data) 调用 ===
执行 forward 方法

虽然看起来两者都调用了 forward,但当你使用 model(input_data) 时,PyTorch 会执行额外的处理:

# 伪代码展示 nn.Module 的 __call__ 方法内部逻辑
def __call__(self, *input, **kwargs):# 前置处理self._check_training_state()  # 检查训练/评估状态self._call_pre_hooks(input)   # 调用前向钩子# 实际执行前向传播result = self.forward(*input, **kwargs)# 后置处理self._call_post_hooks(result)  # 调用后向钩子self._update_module_stats()    # 更新模块统计信息return result

3.3.2 可能导致kooks不执行

Hooks 是 PyTorch 中强大的调试和扩展机制。直接调用 forward() 会绕过所有注册的 hooks。

代码示例:

# 创建模型
model = nn.Sequential(nn.Linear(2, 3), nn.ReLU(), nn.Linear(3, 1))# 注册一个前向钩子
def print_hook(module, input, output):print(f"钩子触发: {module.__class__.__name__} 输出形状: {output.shape}")return output# 为第一层注册钩子
model[0].register_forward_hook(print_hook)# 测试数据
x = torch.randn(1, 2)print("=== 使用 model(x) 调用 ===")
out1 = model(x)  # 会触发钩子print("\n=== 使用 model.forward(x) 调用 ===")
out2 = model.forward(x)  # 不会触发钩子

输出结果:

=== 使用 model(x) 调用 ===
钩子触发: Linear 输出形状: torch.Size([1, 3])=== 使用 model.forward(x) 调用 ===
# 没有钩子输出

3.3.4 梯度计算可能不正常

这是最严重的问题。PyTorch 的自动微分系统依赖于正确的计算图构建,直接调用 forward() 会破坏这个机制。

代码示例:

# 创建简单模型
class Net(nn.Module):def __init__(self):super().__init__()self.fc = nn.Linear(2, 1)def forward(self, x):return self.fc(x)model = Net()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)# 输入和目标数据
x = torch.tensor([[1.0, 2.0]], requires_grad=True)
y_true = torch.tensor([[3.0]])# 正确训练方式
def correct_training():optimizer.zero_grad()output = model(x)  # 正确调用loss = F.mse_loss(output, y_true)loss.backward()optimizer.step()print("正确训练 - 梯度:", model.fc.weight.grad)# 错误训练方式(直接调用forward)
def incorrect_training():optimizer.zero_grad()output = model.forward(x)  # 错误调用loss = F.mse_loss(output, y_true)loss.backward()optimizer.step()print("错误训练 - 梯度:", model.fc.weight.grad)# 运行两种方式
print("=== 正确训练方式 ===")
correct_training()print("\n=== 错误训练方式(直接调用forward)===")
incorrect_training()

输出结果:

=== 正确训练方式 ===
正确训练 - 梯度: tensor([[0.4000, 0.8000]])=== 错误训练方式(直接调用forward)===
错误训练 - 梯度: tensor([[0., 0.]])  # 梯度为零!

3.4 问题分析

当直接调用 forward() 时:

  1. 计算图断开:PyTorch 无法追踪从输入到输出的完整计算路径

  2. 梯度计算失败:自动微分系统无法确定如何计算参数的梯度

  3. 优化器无法更新参数:因为所有梯度都是零或未定义

3.5 更复杂的嵌套模块问题

当模型包含子模块时,直接调用 forward() 会引发更严重的问题:

class ParentModel(nn.Module):def __init__(self):super().__init__()self.child = nn.Linear(2, 2)def forward(self, x):# 正确方式:应该使用 self.child(x) 而不是 self.child.forward(x)return self.child(x) ** 2  # 平方操作model = ParentModel()
x = torch.tensor([[1.0, 2.0]], requires_grad=True)# 正确调用
y1 = model(x)
y1.backward()
print("正确调用 - x的梯度:", x.grad)  # 应为 [4, 8]# 重置梯度
x.grad = None# 错误方式:直接调用forward
y2 = model.forward(x)  # 这会破坏子模块的调用机制
try:y2.backward()print("错误调用 - x的梯度:", x.grad)
except Exception as e:print(f"错误调用失败: {str(e)}")

输出结果:

正确调用 - x的梯度: tensor([[4., 8.]])
错误调用失败: element 0 of tensors does not require grad and does not have a grad_fn

3.6 总结:问什么永远不要调用forward()

问题类型直接调用 forward() 的后果正确调用 model(input) 的优势
前置/后置处理跳过训练/评估模式切换,子模块初始化等自动处理所有模块状态管理
Hooks所有注册的钩子都不会执行保证所有钩子正确触发
梯度计算破坏计算图,导致梯度为零或错误保持完整的计算图,正确计算梯度
嵌套模块子模块的前向传播也可能被破坏递归正确处理所有子模块
框架兼容性可能导致与其他PyTorch功能不兼容保证与所有PyTorch特性兼容

在实际开发中,唯一应该直接使用 forward() 的情况是当你在重写 nn.Moduleforward 方法时,需要调用父类或其他模块的 forward 方法。即便如此,也应该使用 super().forward(x)self.child_module(x) 的形式,而不是直接调用 child_module.forward(x)

http://www.dtcms.com/a/292384.html

相关文章:

  • 大气能见度监测仪:洞察大气 “清晰度” 的科技之眼
  • 智慧教室:科技赋能,奏响个性化学习新乐章
  • MyBatis拦截器插件:实现敏感数据字段加解密
  • 中国科技信息杂志中国科技信息杂志社中国科技信息编辑部2025年第14期目录
  • 「芯生态」杰发科技AC7870携手IAR开发工具链,助推汽车电子全栈全域智能化落地
  • Vue中最简单的PDF引入方法及优缺点分析
  • docker build 和compose 学习笔记
  • CASB架构:了解正向代理、反向代理和API扫描
  • [转]Rust:过程宏
  • JMeter 实现 Protobuf 加密解密
  • AI 音频产品开发模板及流程(一)
  • 网络安全第三次作业搭建前端页面并解析
  • allegro 16.6配置CIS库报错 ORCIS-6129 ORCIS-6469
  • LeetCode 658.找到K个最接近的元素
  • .NET使用EPPlus导出EXCEL的接口中,文件流缺少文件名信息
  • Unity笔记——事件中心
  • 力扣-300.最长递增子序列
  • 以太坊网络发展分析:技术升级与市场动态的双重驱动
  • 快手开源 Kwaipilot-AutoThink 思考模型,有效解决过度思考问题
  • Cy3-COOH 花菁染料Cy3-羧基
  • linux-日志服务
  • Gitlab-CI实现组件自动推送
  • 常用 Flutter 命令大全:从开发到发布全流程总结
  • 检索增强型生成助力无人机精准数学推理!RAG-UAV:基于RAG的复杂算术推理方法
  • Lua语言
  • MybatisPlus-16.扩展功能-枚举处理器
  • ORACLE DATABASE 11.2.0.4 RAC Install
  • Vue-22-通过flask接口提供的数据使用plotly.js绘图(一)
  • Oracle定时清理归档日志
  • RAG(检索增强生成)里的文档管理