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_loader 和 val_loader,但并未定义。在实际运行前,需要使用 torch.utils.data.DataLoader 和 torchvision.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