卷积神经网络(CNN)图像二分类模型

Admin
发布于 2026-05-27 / 0 阅读
0
0
import torch
import torch.nn as nn
import torch.optim as optim

# ==========================================
# 1. 定义 HorsesHumansCNN 模型
# ==========================================
class HorsesHumansCNN(nn.Module):
    def __init__(self):
        super(HorsesHumansCNN, self).__init__()
        # 假设输入图像为 3通道 (RGB),尺寸为 150x150
        self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
        self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
        self.conv3 = nn.Conv2d(32, 64, 3, padding=1)
        self.pool = nn.MaxPool2d(2, 2)
        
        # 经过3次池化 (150 / 2 / 2 / 2 = 18.75,取整为18)
        # 64通道 * 18 * 18
        self.fc1 = nn.Linear(64 * 18 * 18, 512)
        self.fc2 = nn.Linear(512, 1)  # 二分类输出1个节点
        self.dropout = nn.Dropout(0.5)

    def forward(self, x):
        x = self.pool(torch.relu(self.conv1(x)))
        x = self.pool(torch.relu(self.conv2(x)))
        x = self.pool(torch.relu(self.conv3(x)))
        
        # 展平特征图
        x = x.view(x.size(0), -1)
        x = self.dropout(torch.relu(self.fc1(x)))
        x = self.fc2(x)
        return x

# ==========================================
# 2. 训练与评估逻辑
# ==========================================
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

model = HorsesHumansCNN().to(device)

criterion = nn.BCEWithLogitsLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

def evaluate(model, data_loader, device):
    """封装评估函数,计算给定数据集上的准确率"""
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for images, labels in data_loader:
            images, labels = images.to(device), labels.to(device).float()
            outputs = model(images).view(-1)
            predicted = outputs > 0.0  
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
    return 100 * correct / total

def train_model(max_epochs, target_accuracy):
    for epoch in range(max_epochs):
        model.train()
        running_loss = 0.0
        
        for images, labels in train_loader:
            images, labels = images.to(device), labels.to(device).float()
            
            optimizer.zero_grad()
            outputs = model(images).view(-1)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
            
            running_loss += loss.item()

        avg_loss = running_loss / len(train_loader)
        val_accuracy = evaluate(model, val_loader, device)
        
        print(f'Epoch {epoch + 1}/{max_epochs}, Loss: {avg_loss:.4f}, Validation Accuracy: {val_accuracy:.2f}%')

        # ==========================================
        # 核心修改:判断停止条件
        # ==========================================
        current_epoch = epoch + 1
        
        # 条件1:准确率达标
        if val_accuracy >= target_accuracy:
            print(f'\n🎉 恭喜!验证准确率 {val_accuracy:.2f}% 已达到目标 {target_accuracy}%,停止训练!')
            # 保存最佳模型
            torch.save(model.state_dict(), 'best_model.pth')
            print('模型权重已保存至 best_model.pth')
            break  # 跳出训练循环
            
        # 条件2:训练次数超过10次 (即第11次)
        if current_epoch > 15:
            print(f'\n⏹️ 训练次数已达 {current_epoch} 次(超过10次上限),停止训练!')
            # 即使没达标,也保存一下目前的模型,以防万一
            torch.save(model.state_dict(), 'last_model.pth')
            print('模型权重已保存至 last_model.pth')
            break  # 跳出训练循环

# 设置最大训练次数为20(作为上限),目标准确率为75%
# 确保在调用前,train_loader 和 val_loader 已经定义
train_model(max_epochs=20, target_accuracy=75.0)

下面我将为你详细解释这段代码的含义、实现原理、用途以及需要注意的事项。

### 一、 代码含义与用途

这段代码是用 PyTorch 实现的一个**卷积神经网络(CNN)图像二分类模型**,主要用于解决“马与人”的图像识别问题。代码不仅定义了模型结构,还包含了完整的模型训练、验证逻辑,并实现了一个**自定义的早停机制**:当验证集准确率达到目标值(75%)或训练轮次超过上限(15次)时,自动停止训练并保存模型。

---

### 二、 实现原理详解

代码主要分为两个部分:模型定义和训练评估逻辑。

#### 1. HorsesHumansCNN 模型定义

* **网络结构**:这是一个经典的串联式卷积神经网络,由3个卷积块和2个全连接层组成。

* **卷积层 Conv2d)**:提取图像特征。通道数从 3(RGB输入)逐层提取到 16 -> 32 -> 64。使用 padding=1 配合 kernel_size=3 保证卷积前后特征图尺寸不变。

* **池化层 MaxPool2d)**:进行下采样,缩小特征图尺寸,减少计算量并增加平移不变性。使用 2x2 的池化核,每次将图像宽高减半。

* **全连接层 Linear)**:将三维的特征图展平为一维向量,进行分类映射。第一层将特征映射到 512 维,第二层映射到 **1 维**(因为是二分类问题,输出一个 logits 值即可)。

* **Dropout 层**:在第一个全连接层后以 50% 的概率随机丢弃神经元,防止模型过拟合。

* **前向传播 forward)**:数据依次经过 卷积 -> ReLU激活 -> 池化,然后通过 x.view(x.size(0), -1) 将多维特征展平,再经过带有 Dropout 的全连接层,最后输出预测值。

#### 2. 训练与评估逻辑

* **设备选择**:自动检测是否有 GPUcuda),有则用 GPU 加速,无则用 CPU。

* **损失函数 BCEWithLogitsLoss)**:**二分类交叉熵损失函数**。它内部集成了 Sigmoid 操作,直接接收模型输出的 logits(未经Sigmoid的原始值),相比于先 Sigmoid 再计算损失,数值稳定性更好。

* **优化器 Adam)**:自适应学习率优化器,初始学习率设为 0.001。

* **评估函数 evaluate)**:

* 使用 model.eval() 切换到评估模式,关闭 Dropout 和 BatchNorm 的训练行为。

* 使用 torch.no_grad() 禁止计算梯度,节省内存和算力。

* **关键点**:由于模型输出没有加 Sigmoid,输出值的阈值判断为 > 0.0(0.0相当于概率0.5的分界线)。

* **自定义早停训练 train_model)**:

* 每个 Epoch 结束后计算验证集准确率。

* **条件1**:准确率 $\ge$ 75%,保存模型为 best_model.pth 并停止。

* **条件2**:训练轮次 $> 15$(代码注释写的是超过10次,但实际代码逻辑是第16次触发),保存模型为 last_model.pth 并停止。

---

### 三、 注意事项与改进建议

1. **特征图尺寸计算错误(严重隐患)**:

代码注释中写道150 / 2 / 2 / 2 = 18.75,取整为18,并以此设置了 self.fc1 = nn.Linear(64 18 * 18, 512)

* **但实际上,PyTorch 的卷积和池化默认是向下取整**。$150 \div 2 = 75$,$75 \div 2 = 37$(不是18),$37 \div 2 = 18$。

因此,正确的特征图大小是 $64 \times 37 \times 37$(如果输入是150),全连接层的输入应为 64 37 37。如果按原代码运行,会在 x.view()fc1 报维度不匹配的错误。*建议将输入图像尺寸调整为 150x150,并将 fc1 修改为 nn.Linear(64 18 18, 512) 的前提是输入尺寸为 $150 \times 150$ 且池化前有额外的裁剪,或者直接将输入尺寸改为 144(144/8=18)。**

2. **逻辑与注释不符**:

* 注释写着“训练次数超过10次 (即第11次)”,但代码逻辑是 if current_epoch > 15:,这会在第16个 Epoch 才触发。建议将代码改为 current_epoch > 10 或修改注释以保持一致。

3. **缺少数据加载器定义**:

* 代码末尾调用了 train_loaderval_loader,但并未定义。在实际运行前,需要使用 torch.utils.data.DataLoadertorchvision.datasets 来加载“马与人”的数据集,并做好归一化处理。

4. **Sigmoid 与 阈值判断**:

* 评估函数中 predicted = outputs > 0.0 是正确的,因为 BCEWithLogitsLoss 包含了 Sigmoid。当 Logits > 0 时,Sigmoid 后的概率 > 0.5,判为正类。但阅读代码时容易让人误解,建议在评估时显式加上 torch.sigmoid(outputs),然后 predicted = sigmoid_outputs > 0.5,这样代码可读性更强。

5. **模型保存位置**:

* 早停条件1保存的是 best_model.pth,条件2保存的是 last_model.pth。在实际工程中,建议在每个 Epoch 达到历史最高准确率时都覆盖保存 best_model.pth,而不仅仅是在触发停止条件时保存。

**修正后的 fc1 计算示例(假设输入严格为 150x150):**

```python

# 经过3次卷积(padding=1)尺寸不变,3次池化尺寸减半:

# 150 -> 池化 -> 75 -> 池化 -> 37 -> 池化 -> 18

# 注意:75/2 = 37.5,PyTorch MaxPool 默认向下取整为 37

# 37/2 = 18.5,向下取整为 18

# 所以最终特征图尺寸为 64 18 18,原代码的 18*18 结论是正确的,但中间过程 75 写成了 18

self.fc1 = nn.Linear(64 18 18, 512) # 该行代码本身可以执行,但注释有误导性

Epoch 1/20, Loss: 0.6269, Validation Accuracy: 41.80%
Epoch 2/20, Loss: 0.5294, Validation Accuracy: 44.14%
Epoch 3/20, Loss: 0.5449, Validation Accuracy: 47.66%
Epoch 4/20, Loss: 0.4272, Validation Accuracy: 57.03%
Epoch 5/20, Loss: 0.3867, Validation Accuracy: 75.00%

🎉 恭喜!验证准确率 75.00% 已达到目标 75.0%,停止训练!
模型权重已保存至 best_model.pth


评论