第J4周:ResNet与DenseNet结合探索

目标

根据ResNet50V2和DenseNet121的网络结构特点,组合成一个新的神经网络结构。然后进行训练和验证。

具体实现

(一)环境

语言环境:Python 3.10
编 译 器: PyCharm
框 架: Pytorch

(二)具体步骤
1. ResNet50V2特点
  • 使用残差连接(residual connections)跳过层,解决深层网络的梯度消失问题
  • 采用"pre-activation"结构,即先进行批量归一化和激活,再进行卷积
  • 使用瓶颈结构(bottleneck design)减少参数量
  • 擅长学习深层特征
2. DenseNet121特点
  • 密集连接(dense connections):每层都与前面所有层直接相连
  • 特征重用:后面的层可以直接使用前面层的特征
  • 减轻了梯度消失问题
  • 参数效率高:减少了冗余特征的学习
3. 创建一个结合两者优势的网络,称为ResDenseNet:
  • 使用ResNet50V2的深度和骨架结构
  • 在某些块中引入DenseNet的密集连接
  • 保留ResNet的残差学习能力
  • 利用DenseNet的特征重用机制
import os  
  
# 设置环境变量以允许重复的OpenMP库  
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'  
  
import torch  
import torch.nn as nn  
import torch.nn.functional as F  
import torch.optim as optim  
from torch.utils.data import DataLoader  
from torchvision import datasets, transforms  
import numpy as np  
import matplotlib.pyplot as plt  
from tqdm import tqdm  
  
# 设置随机种子,确保结果可复现  
torch.manual_seed(42)  
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  
print(f"使用设备: {device}")  
  
  
# ResNet50V2的残差块  
class ResidualBlock(nn.Module):  
    """ResNet50V2风格的残差块"""  
  
    def __init__(self, in_channels, out_channels, stride=1):  
        super(ResidualBlock, self).__init__()  
        self.expansion = 4  # 扩展因子  
        self.in_channels = in_channels  
        self.out_channels = out_channels * self.expansion  
  
        # 预激活结构的主路径  
        self.bn1 = nn.BatchNorm2d(in_channels)  
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)  
  
        self.bn2 = nn.BatchNorm2d(out_channels)  
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,  
                               stride=stride, padding=1, bias=False)  
  
        self.bn3 = nn.BatchNorm2d(out_channels)  
        self.conv3 = nn.Conv2d(out_channels, self.out_channels, kernel_size=1, bias=False)  
  
        # 快捷连接(shortcut)  
        self.shortcut = nn.Sequential()  
        if stride != 1 or in_channels != self.out_channels:  
            self.shortcut = nn.Conv2d(in_channels, self.out_channels,  
                                      kernel_size=1, stride=stride, bias=False)  
  
    def forward(self, x):  
        # 保存输入用于残差连接  
        identity = x  
  
        # 预激活  
        out = F.relu(self.bn1(x))  
  
        # 主路径  
        out = self.conv1(out)  
        out = F.relu(self.bn2(out))  
        out = self.conv2(out)  
        out = F.relu(self.bn3(out))  
        out = self.conv3(out)  
  
        # 残差连接  
        if self.in_channels != self.out_channels or hasattr(self.shortcut, 'conv1d'):  
            identity = self.shortcut(x)  
  
        out += identity  
        return out  
  
  
# DenseNet密集块中的单个层  
class DenseLayer(nn.Module):  
    """DenseNet中的基本层,使用瓶颈结构减少计算开销"""  
  
    def __init__(self, in_channels, growth_rate, bottleneck_ratio=4):  
        super(DenseLayer, self).__init__()  
        bottleneck_channels = growth_rate * bottleneck_ratio  
  
        # BN-ReLU-Conv(1x1) 瓶颈结构  
        self.bn1 = nn.BatchNorm2d(in_channels)  
        self.conv1 = nn.Conv2d(in_channels, bottleneck_channels, kernel_size=1, bias=False)  
  
        # BN-ReLU-Conv(3x3)  
        self.bn2 = nn.BatchNorm2d(bottleneck_channels)  
        self.conv2 = nn.Conv2d(bottleneck_channels, growth_rate, kernel_size=3,  
                               padding=1, bias=False)  
  
        # 加入dropout以防止过拟合  
        self.dropout = nn.Dropout(0.2)  
  
    def forward(self, x):  
        out = F.relu(self.bn1(x))  
        out = self.conv1(out)  
        out = F.relu(self.bn2(out))  
        out = self.conv2(out)  
        out = self.dropout(out)  
        return out  
  
  
# DenseNet密集块  
class DenseBlock(nn.Module):  
    """DenseNet的密集连接块"""  
  
    def __init__(self, in_channels, growth_rate, n_layers):  
        super(DenseBlock, self).__init__()  
        self.layers = nn.ModuleList()  
  
        # 添加n_layers个密集连接的层  
        for i in range(n_layers):  
            self.layers.append(DenseLayer(in_channels + i * growth_rate, growth_rate))  
  
    def forward(self, x):  
        features = [x]  
  
        for layer in self.layers:  
            new_feature = layer(torch.cat(features, dim=1))  
            features.append(new_feature)  
  
        return torch.cat(features, dim=1)  
  
  
# 过渡层  
class TransitionLayer(nn.Module):  
    """用于降低特征图尺寸和通道数的过渡层"""  
  
    def __init__(self, in_channels, reduction_factor=0.5):  
        super(TransitionLayer, self).__init__()  
        self.out_channels = int(in_channels * reduction_factor)  
  
        self.bn = nn.BatchNorm2d(in_channels)  
        self.conv = nn.Conv2d(in_channels, self.out_channels, kernel_size=1, bias=False)  
        self.pool = nn.AvgPool2d(kernel_size=2, stride=2)  
  
    def forward(self, x):  
        x = F.relu(self.bn(x))  
        x = self.conv(x)  
        x = self.pool(x)  
        return x  
  
  
# ResDenseNet主网络  
class ResDenseNet(nn.Module):  
    """结合ResNet50V2和DenseNet121的混合网络"""  
  
    def __init__(self, num_classes=10, growth_rate=32, input_size=32):  
        super(ResDenseNet, self).__init__()  
  
        # 根据输入尺寸选择初始卷积参数  
        if input_size < 64:  # 对于CIFAR等小尺寸数据集  
            # 使用更小的初始卷积层,不降低空间分辨率  
            self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)  
            self.bn1 = nn.BatchNorm2d(64)  
            self.use_maxpool = False  # 不使用最大池化层  
        else:  # 对于ImageNet等大尺寸数据集  
            # 使用标准ResNet的初始卷积层  
            self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)  
            self.bn1 = nn.BatchNorm2d(64)  
            self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)  
            self.use_maxpool = True  
  
        # 第一阶段:ResNet风格的残差块组  
        self.stage1 = nn.Sequential(  
            ResidualBlock(64, 64),  
            ResidualBlock(256, 64),  
            ResidualBlock(256, 64)  
        )  
  
        # 过渡层1: 256 -> 128  
        self.trans1 = TransitionLayer(256, reduction_factor=0.5)  
  
        # 第二阶段:DenseNet风格的密集连接块  
        self.stage2 = DenseBlock(128, growth_rate, n_layers=6)  
        # 密集块2输出通道: 128 + 6*32 = 320  
  
        # 过渡层2: 320 -> 160  
        self.trans2 = TransitionLayer(320, reduction_factor=0.5)  
  
        # 第三阶段:修改后的ResNet风格残差块组  
        # 为避免特征图尺寸过小,使用stride=1  
        self.stage3 = nn.Sequential(  
            ResidualBlock(160, 128, stride=1),  
            ResidualBlock(512, 128),  
            ResidualBlock(512, 128)  
        )  
  
        # 过渡层3: 512 -> 256  
        self.trans3 = TransitionLayer(512, reduction_factor=0.5)  
  
        # 第四阶段:DenseNet风格的密集连接块  
        self.stage4 = DenseBlock(256, growth_rate, n_layers=8)  
        # 密集块4输出通道: 256 + 8*32 = 512  
  
        # 分类器  
        self.bn_final = nn.BatchNorm2d(512)  
        self.global_pool = nn.AdaptiveAvgPool2d(1)  # 自适应池化到1x1  
        self.fc = nn.Linear(512, num_classes)  
  
        # 初始化权重  
        self._initialize_weights()  
  
    def _initialize_weights(self):  
        for m in self.modules():  
            if isinstance(m, nn.Conv2d):  
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')  
            elif isinstance(m, nn.BatchNorm2d):  
                nn.init.constant_(m.weight, 1)  
                nn.init.constant_(m.bias, 0)  
            elif isinstance(m, nn.Linear):  
                nn.init.normal_(m.weight, 0, 0.01)  
                nn.init.constant_(m.bias, 0)  
  
    def forward(self, x):  
        # 初始层  
        x = self.conv1(x)  
        x = F.relu(self.bn1(x))  
  
        if self.use_maxpool:  
            x = self.maxpool(x)  
  
        # 第一阶段:ResNet残差块  
        x = self.stage1(x)  
  
        # 第一个过渡层  
        x = self.trans1(x)  
  
        # 第二阶段:DenseNet密集块  
        x = self.stage2(x)  
  
        # 第二个过渡层  
        x = self.trans2(x)  
  
        # 第三阶段:ResNet残差块  
        x = self.stage3(x)  
  
        # 第三个过渡层  
        x = self.trans3(x)  
  
        # 第四阶段:DenseNet密集块  
        x = self.stage4(x)  
  
        # 最终处理  
        x = F.relu(self.bn_final(x))  
        x = self.global_pool(x)  
        x = torch.flatten(x, 1)  
        x = self.fc(x)  
  
        return x  
  
  
# 数据加载和预处理函数  
def load_data(batch_size=128):  
    """加载并预处理CIFAR-10数据集,使用增强的数据增强"""  
    transform_train = transforms.Compose([  
        transforms.RandomCrop(32, padding=4),  
        transforms.RandomHorizontalFlip(),  
        transforms.RandomRotation(15),  # 添加随机旋转  
        transforms.ColorJitter(brightness=0.2, contrast=0.2),  # 添加颜色抖动  
        transforms.ToTensor(),  
        transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),  
    ])  
  
    transform_test = transforms.Compose([  
        transforms.ToTensor(),  
        transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),  
    ])  
  
    train_dataset = datasets.CIFAR10(root='./data', train=True,  
                                     download=True, transform=transform_train)  
    test_dataset = datasets.CIFAR10(root='./data', train=False,  
                                    download=True, transform=transform_test)  
  
    train_loader = DataLoader(train_dataset, batch_size=batch_size,  
                              shuffle=True, num_workers=4, pin_memory=True)  
    test_loader = DataLoader(test_dataset, batch_size=batch_size,  
                             shuffle=False, num_workers=4, pin_memory=True)  
  
    return train_loader, test_loader  
  
  
# 训练函数  
def train(model, train_loader, test_loader, epochs=30, lr=0.1):  
    """训练模型并评估性能,使用改进的训练策略"""  
    model = model.to(device)  
    criterion = nn.CrossEntropyLoss()  
  
    # 使用SGD优化器,添加了Nesterov动量  
    optimizer = optim.SGD(model.parameters(), lr=lr, momentum=0.9,  
                          weight_decay=5e-4, nesterov=True)  
  
    # 使用余弦退火学习率调度器  
    scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)  
  
    # 记录训练和验证性能  
    train_losses = []  
    test_losses = []  
    train_accs = []  
    test_accs = []  
  
    best_acc = 0.0  
  
    for epoch in range(epochs):  
        model.train()  
        train_loss = 0  
        correct = 0  
        total = 0  
  
        # 使用tqdm显示进度条  
        progress_bar = tqdm(train_loader, desc=f'Epoch {epoch + 1}/{epochs}')  
  
        for batch_idx, (inputs, targets) in enumerate(progress_bar):  
            inputs, targets = inputs.to(device), targets.to(device)  
  
            # 梯度清零  
            optimizer.zero_grad()  
  
            # 使用混合精度训练以加速(如果使用GPU)  
            if device.type == 'cuda':  
                with torch.cuda.amp.autocast():  
                    outputs = model(inputs)  
                    loss = criterion(outputs, targets)  
            else:  
                outputs = model(inputs)  
                loss = criterion(outputs, targets)  
  
            # 反向传播和优化  
            loss.backward()  
            optimizer.step()  
  
            # 统计  
            train_loss += loss.item()  
            _, predicted = outputs.max(1)  
            total += targets.size(0)  
            correct += predicted.eq(targets).sum().item()  
  
            # 更新进度条  
            progress_bar.set_postfix({  
                'Loss': train_loss / (batch_idx + 1),  
                'Acc': 100. * correct / total,  
                'LR': optimizer.param_groups[0]['lr']  
            })  
  
        # 计算训练集性能  
        train_loss /= len(train_loader)  
        train_acc = 100. * correct / total  
        train_losses.append(train_loss)  
        train_accs.append(train_acc)  
  
        # 评估验证集性能  
        model.eval()  
        test_loss = 0  
        correct = 0  
        total = 0  
  
        with torch.no_grad():  
            for inputs, targets in test_loader:  
                inputs, targets = inputs.to(device), targets.to(device)  
  
                # 使用混合精度  
                if device.type == 'cuda':  
                    with torch.cuda.amp.autocast():  
                        outputs = model(inputs)  
                        loss = criterion(outputs, targets)  
                else:  
                    outputs = model(inputs)  
                    loss = criterion(outputs, targets)  
  
                test_loss += loss.item()  
                _, predicted = outputs.max(1)  
                total += targets.size(0)  
                correct += predicted.eq(targets).sum().item()  
  
        # 计算验证集性能  
        test_loss /= len(test_loader)  
        test_acc = 100. * correct / total  
        test_losses.append(test_loss)  
        test_accs.append(test_acc)  
  
        # 保存最佳模型  
        if test_acc > best_acc:  
            best_acc = test_acc  
            torch.save({  
                'epoch': epoch,  
                'model_state_dict': model.state_dict(),  
                'optimizer_state_dict': optimizer.state_dict(),  
                'accuracy': best_acc,  
            }, 'resdensenet_best.pth')  
  
        print(f'Epoch {epoch + 1}/{epochs} - '              f'Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}% | '  
              f'Test Loss: {test_loss:.4f}, Test Acc: {test_acc:.2f}% | '  
              f'Best Acc: {best_acc:.2f}%')  
  
        # 学习率调整  
        scheduler.step()  
  
    # 加载最佳模型  
    checkpoint = torch.load('resdensenet_best.pth')  
    model.load_state_dict(checkpoint['model_state_dict'])  
    print(f"加载最佳模型(Epoch {checkpoint['epoch'] + 1},准确率: {checkpoint['accuracy']:.2f}%)")  
  
    return model, train_losses, test_losses, train_accs, test_accs  
  
  
# 绘制训练过程图  
def plot_learning_curves(train_losses, test_losses, train_accs, test_accs):  
    """绘制训练和验证的损失曲线和准确率曲线"""  
    plt.figure(figsize=(15, 6))  
  
    # 创建带有两个y轴的图表  
    ax1 = plt.subplot(1, 2, 1)  
    ax2 = plt.subplot(1, 2, 2)  
  
    # 绘制损失曲线  
    epochs = range(1, len(train_losses) + 1)  
    ax1.plot(epochs, train_losses, 'b-', label='Training Loss')  
    ax1.plot(epochs, test_losses, 'r-', label='Validation Loss')  
    ax1.set_title('Loss Curves')  
    ax1.set_xlabel('Epochs')  
    ax1.set_ylabel('Loss')  
    ax1.legend()  
    ax1.grid(True, linestyle='--', alpha=0.7)  
  
    # 绘制准确率曲线  
    ax2.plot(epochs, train_accs, 'b-', label='Training Accuracy')  
    ax2.plot(epochs, test_accs, 'r-', label='Validation Accuracy')  
    ax2.set_title('Accuracy Curves')  
    ax2.set_xlabel('Epochs')  
    ax2.set_ylabel('Accuracy (%)')  
    ax2.legend()  
    ax2.grid(True, linestyle='--', alpha=0.7)  
  
    plt.tight_layout()  
    plt.savefig('resdensenet_learning_curves.png', dpi=300)  
    plt.show()  
  
  
# 计算模型参数量与FLOPs  
def model_summary(model, input_size=(3, 32, 32)):  
    """计算模型的参数数量和FLOPs"""  
    try:  
        from thop import profile  
        input = torch.randn(1, *input_size).to(device)  
        model = model.to(device)  
        macs, params = profile(model, inputs=(input,))  
  
        print(f"模型参数量: {params / 1e6:.2f}M")  
        print(f"计算量(MACs): {macs / 1e9:.2f}G")  
        print(f"浮点运算量(FLOPs): {macs * 2 / 1e9:.2f}G")  
    except ImportError:  
        # 如果没有安装thop,则只计算参数量  
        params = sum(p.numel() for p in model.parameters() if p.requires_grad)  
        print(f"模型参数量: {params / 1e6:.2f}M")  
        print("若要计算FLOPs,请安装thop: pip install thop")  
  
  
# 分析混淆矩阵  
def confusion_matrix_analysis(model, test_loader, class_names):  
    """绘制混淆矩阵并分析模型表现"""  
    try:  
        import seaborn as sns  
        from sklearn.metrics import confusion_matrix, classification_report  
  
        model.eval()  
        all_preds = []  
        all_targets = []  
  
        with torch.no_grad():  
            for inputs, targets in tqdm(test_loader, desc="Evaluating"):  
                inputs, targets = inputs.to(device), targets.to(device)  
                outputs = model(inputs)  
                _, preds = outputs.max(1)  
  
                all_preds.extend(preds.cpu().numpy())  
                all_targets.extend(targets.cpu().numpy())  
  
        # 计算混淆矩阵  
        cm = confusion_matrix(all_targets, all_preds)  
  
        # 绘制混淆矩阵热图  
        plt.figure(figsize=(10, 8))  
        sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',  
                    xticklabels=class_names, yticklabels=class_names)  
        plt.xlabel('Predicted')  
        plt.ylabel('True')  
        plt.title('Confusion Matrix')  
        plt.savefig('confusion_matrix.png', dpi=300)  
        plt.show()  
  
        # 打印分类报告  
        print(classification_report(all_targets, all_preds, target_names=class_names))  
  
    except ImportError:  
        print("需要安装seaborn和scikit-learn以分析混淆矩阵: pip install seaborn scikit-learn")  
  
  
# 主函数  
def main(batch_size=128, epochs=50, lr=0.1):  
    print("训练ResDenseNet网络...")  
  
    # 加载数据  
    train_loader, test_loader = load_data(batch_size)  
  
    # 创建模型  
    model = ResDenseNet(num_classes=10, growth_rate=32, input_size=32)  
  
    # 显示模型信息  
    model_summary(model)  
  
    # 训练模型  
    model, train_losses, test_losses, train_accs, test_accs = train(  
        model, train_loader, test_loader, epochs=epochs, lr=lr  
    )  
  
    # 绘制学习曲线  
    plot_learning_curves(train_losses, test_losses, train_accs, test_accs)  
  
    # CIFAR-10类别名称  
    class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer',  
                   'dog', 'frog', 'horse', 'ship', 'truck']  
  
    # 分析混淆矩阵  
    confusion_matrix_analysis(model, test_loader, class_names)  
  
    # 最终评估  
    model.eval()  
    correct = 0  
    total = 0  
  
    with torch.no_grad():  
        for inputs, targets in test_loader:  
            inputs, targets = inputs.to(device), targets.to(device)  
            outputs = model(inputs)  
            _, predicted = outputs.max(1)  
            total += targets.size(0)  
            correct += predicted.eq(targets).sum().item()  
  
    print(f'最终测试集准确率: {100. * correct / total:.2f}%')  
  
    return model  
  
  
if __name__ == "__main__":  
    model = main(batch_size=128, epochs=50)

image.png
image.png
image.png

4. 使用乳腺肿瘤数据进行训练和预测
import os  
  
# 设置环境变量以允许重复的OpenMP库  
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'  
  
import torch  
import torch.nn as nn  
import torch.nn.functional as F  
import torch.optim as optim  
from torch.utils.data import DataLoader, Dataset  
from torchvision import transforms  
import numpy as np  
import matplotlib.pyplot as plt  
from tqdm import tqdm  
import cv2  
from PIL import Image  
import random  
from sklearn.model_selection import train_test_split  
from pathlib import Path  
  
  
# 设置随机种子,确保结果可复现  
def set_seed(seed=42):  
    random.seed(seed)  
    np.random.seed(seed)  
    torch.manual_seed(seed)  
    if torch.cuda.is_available():  
        torch.cuda.manual_seed(seed)  
        torch.cuda.manual_seed_all(seed)  
    torch.backends.cudnn.deterministic = True  
    torch.backends.cudnn.benchmark = False  
  
  
set_seed(42)  
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  
print(f"使用设备: {device}")  
  
  
# ResNet50V2的残差块  
class ResidualBlock(nn.Module):  
    """ResNet50V2风格的残差块"""  
  
    def __init__(self, in_channels, out_channels, stride=1):  
        super(ResidualBlock, self).__init__()  
        self.expansion = 4  # 扩展因子  
        self.in_channels = in_channels  
        self.out_channels = out_channels * self.expansion  
  
        # 预激活结构的主路径  
        self.bn1 = nn.BatchNorm2d(in_channels)  
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)  
  
        self.bn2 = nn.BatchNorm2d(out_channels)  
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,  
                               stride=stride, padding=1, bias=False)  
  
        self.bn3 = nn.BatchNorm2d(out_channels)  
        self.conv3 = nn.Conv2d(out_channels, self.out_channels, kernel_size=1, bias=False)  
  
        # 快捷连接(shortcut)  
        self.shortcut = nn.Sequential()  
        if stride != 1 or in_channels != self.out_channels:  
            self.shortcut = nn.Conv2d(in_channels, self.out_channels,  
                                      kernel_size=1, stride=stride, bias=False)  
  
    def forward(self, x):  
        # 保存输入用于残差连接  
        identity = x  
  
        # 预激活  
        out = F.relu(self.bn1(x))  
  
        # 主路径  
        out = self.conv1(out)  
        out = F.relu(self.bn2(out))  
        out = self.conv2(out)  
        out = F.relu(self.bn3(out))  
        out = self.conv3(out)  
  
        # 残差连接  
        if self.in_channels != self.out_channels or hasattr(self.shortcut, 'conv1d'):  
            identity = self.shortcut(x)  
  
        out += identity  
        return out  
  
  
# DenseNet密集块中的单个层  
class DenseLayer(nn.Module):  
    """DenseNet中的基本层,使用瓶颈结构减少计算开销"""  
  
    def __init__(self, in_channels, growth_rate, bottleneck_ratio=4):  
        super(DenseLayer, self).__init__()  
        bottleneck_channels = growth_rate * bottleneck_ratio  
  
        # BN-ReLU-Conv(1x1) 瓶颈结构  
        self.bn1 = nn.BatchNorm2d(in_channels)  
        self.conv1 = nn.Conv2d(in_channels, bottleneck_channels, kernel_size=1, bias=False)  
  
        # BN-ReLU-Conv(3x3)  
        self.bn2 = nn.BatchNorm2d(bottleneck_channels)  
        self.conv2 = nn.Conv2d(bottleneck_channels, growth_rate, kernel_size=3,  
                               padding=1, bias=False)  
  
        # 加入dropout以防止过拟合  
        self.dropout = nn.Dropout(0.2)  
  
    def forward(self, x):  
        out = F.relu(self.bn1(x))  
        out = self.conv1(out)  
        out = F.relu(self.bn2(out))  
        out = self.conv2(out)  
        out = self.dropout(out)  
        return out  
  
  
# DenseNet密集块  
class DenseBlock(nn.Module):  
    """DenseNet的密集连接块"""  
  
    def __init__(self, in_channels, growth_rate, n_layers):  
        super(DenseBlock, self).__init__()  
        self.layers = nn.ModuleList()  
  
        # 添加n_layers个密集连接的层  
        for i in range(n_layers):  
            self.layers.append(DenseLayer(in_channels + i * growth_rate, growth_rate))  
  
    def forward(self, x):  
        features = [x]  
  
        for layer in self.layers:  
            new_feature = layer(torch.cat(features, dim=1))  
            features.append(new_feature)  
  
        return torch.cat(features, dim=1)  
  
  
# 过渡层  
class TransitionLayer(nn.Module):  
    """用于降低特征图尺寸和通道数的过渡层"""  
  
    def __init__(self, in_channels, reduction_factor=0.5):  
        super(TransitionLayer, self).__init__()  
        self.out_channels = int(in_channels * reduction_factor)  
  
        self.bn = nn.BatchNorm2d(in_channels)  
        self.conv = nn.Conv2d(in_channels, self.out_channels, kernel_size=1, bias=False)  
        self.pool = nn.AvgPool2d(kernel_size=2, stride=2)  
  
    def forward(self, x):  
        x = F.relu(self.bn(x))  
        x = self.conv(x)  
        x = self.pool(x)  
        return x  
  
  
# ResDenseNet主网络  
class ResDenseNet(nn.Module):  
    """结合ResNet50V2和DenseNet121的混合网络"""  
  
    def __init__(self, num_classes=2, growth_rate=32, input_size=224):  
        super(ResDenseNet, self).__init__()  
  
        # 根据输入尺寸选择初始卷积参数  
        if input_size < 64:  # 对于小尺寸数据集  
            # 使用更小的初始卷积层,不降低空间分辨率  
            self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)  
            self.bn1 = nn.BatchNorm2d(64)  
            self.use_maxpool = False  # 不使用最大池化层  
        else:  # 对于大尺寸数据集  
            # 使用标准ResNet的初始卷积层  
            self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)  
            self.bn1 = nn.BatchNorm2d(64)  
            self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)  
            self.use_maxpool = True  
  
        # 第一阶段:ResNet风格的残差块组  
        self.stage1 = nn.Sequential(  
            ResidualBlock(64, 64),  
            ResidualBlock(256, 64),  
            ResidualBlock(256, 64)  
        )  
  
        # 过渡层1: 256 -> 128  
        self.trans1 = TransitionLayer(256, reduction_factor=0.5)  
  
        # 第二阶段:DenseNet风格的密集连接块  
        self.stage2 = DenseBlock(128, growth_rate, n_layers=6)  
        # 密集块2输出通道: 128 + 6*32 = 320  
  
        # 过渡层2: 320 -> 160  
        self.trans2 = TransitionLayer(320, reduction_factor=0.5)  
  
        # 第三阶段:修改后的ResNet风格残差块组  
        # 为避免特征图尺寸过小,使用stride=1  
        self.stage3 = nn.Sequential(  
            ResidualBlock(160, 128, stride=1),  
            ResidualBlock(512, 128),  
            ResidualBlock(512, 128)  
        )  
  
        # 过渡层3: 512 -> 256  
        self.trans3 = TransitionLayer(512, reduction_factor=0.5)  
  
        # 第四阶段:DenseNet风格的密集连接块  
        self.stage4 = DenseBlock(256, growth_rate, n_layers=8)  
        # 密集块4输出通道: 256 + 8*32 = 512  
  
        # 分类器  
        self.bn_final = nn.BatchNorm2d(512)  
        self.global_pool = nn.AdaptiveAvgPool2d(1)  # 自适应池化到1x1  
        self.fc = nn.Linear(512, num_classes)  
  
        # 初始化权重  
        self._initialize_weights()  
  
    def _initialize_weights(self):  
        for m in self.modules():  
            if isinstance(m, nn.Conv2d):  
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')  
            elif isinstance(m, nn.BatchNorm2d):  
                nn.init.constant_(m.weight, 1)  
                nn.init.constant_(m.bias, 0)  
            elif isinstance(m, nn.Linear):  
                nn.init.normal_(m.weight, 0, 0.01)  
                nn.init.constant_(m.bias, 0)  
  
    def forward(self, x):  
        # 初始层  
        x = self.conv1(x)  
        x = F.relu(self.bn1(x))  
  
        if self.use_maxpool:  
            x = self.maxpool(x)  
  
        # 第一阶段:ResNet残差块  
        x = self.stage1(x)  
  
        # 第一个过渡层  
        x = self.trans1(x)  
  
        # 第二阶段:DenseNet密集块  
        x = self.stage2(x)  
  
        # 第二个过渡层  
        x = self.trans2(x)  
  
        # 第三阶段:ResNet残差块  
        x = self.stage3(x)  
  
        # 第三个过渡层  
        x = self.trans3(x)  
  
        # 第四阶段:DenseNet密集块  
        x = self.stage4(x)  
  
        # 最终处理  
        x = F.relu(self.bn_final(x))  
        x = self.global_pool(x)  
        x = torch.flatten(x, 1)  
        x = self.fc(x)  
  
        return x  
  
  
# 自定义数据集类  
class BreastCancerDataset(Dataset):  
    def __init__(self, image_paths, labels, transform=None):  
        self.image_paths = image_paths  
        self.labels = labels  
        self.transform = transform  
  
    def __len__(self):  
        return len(self.image_paths)  
  
    def __getitem__(self, idx):  
        img_path = self.image_paths[idx]  
        image = Image.open(img_path).convert('RGB')  
        label = self.labels[idx]  
  
        if self.transform:  
            image = self.transform(image)  
  
        return image, label  
  
  
# 数据加载和预处理函数  
def load_breast_cancer_data(data_dir='./data/J3-data/', batch_size=32, img_size=224, test_size=0.2):  
    """加载并预处理乳腺癌数据集"""  
    data_dir = Path(data_dir)  
  
    # 数据增强和标准化  
    transform_train = transforms.Compose([  
        transforms.Resize((img_size, img_size)),  
        transforms.RandomHorizontalFlip(),  
        transforms.RandomVerticalFlip(),  
        transforms.RandomRotation(20),  
        transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1),  
        transforms.ToTensor(),  
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])  
    ])  
  
    transform_test = transforms.Compose([  
        transforms.Resize((img_size, img_size)),  
        transforms.ToTensor(),  
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])  
    ])  
  
    # 收集所有图像路径和标签  
    normal_dir = data_dir / 'normal'  
    cancer_dir = data_dir / 'breast cancer'  
  
    normal_images = list(normal_dir.glob('*.*'))  
    cancer_images = list(cancer_dir.glob('*.*'))  
  
    # 支持的图像格式  
    valid_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff'}  
    normal_images = [img for img in normal_images if img.suffix.lower() in valid_extensions]  
    cancer_images = [img for img in cancer_images if img.suffix.lower() in valid_extensions]  
  
    # 确保图像路径存在和可读  
    normal_images = [img for img in normal_images if img.exists()]  
    cancer_images = [img for img in cancer_images if img.exists()]  
  
    print(f"正常乳腺图像数量: {len(normal_images)}")  
    print(f"乳腺癌图像数量: {len(cancer_images)}")  
  
    all_images = normal_images + cancer_images  
    all_labels = [0] * len(normal_images) + [1] * len(cancer_images)  # 0:正常, 1:乳腺癌  
  
    # 划分训练集和测试集  
    X_train, X_test, y_train, y_test = train_test_split(  
        all_images, all_labels, test_size=test_size, random_state=42, stratify=all_labels  
    )  
  
    # 创建数据集  
    train_dataset = BreastCancerDataset(X_train, y_train, transform=transform_train)  
    test_dataset = BreastCancerDataset(X_test, y_test, transform=transform_test)  
  
    # 创建数据加载器  
    train_loader = DataLoader(train_dataset, batch_size=batch_size,  
                              shuffle=True, num_workers=4, pin_memory=True)  
    test_loader = DataLoader(test_dataset, batch_size=batch_size,  
                             shuffle=False, num_workers=4, pin_memory=True)  
  
    class_names = ['正常', '乳腺癌']  
  
    return train_loader, test_loader, class_names  
  
  
# 训练函数  
def train_model(model, train_loader, test_loader, epochs=50, lr=0.001, class_names=None):  
    """训练模型并评估性能"""  
    model = model.to(device)  
  
    # 添加类别权重以处理可能的数据不平衡  
    if class_names:  
        # 计算类别权重  
        num_samples = [0] * len(class_names)  
        for _, labels in train_loader:  
            for label in labels:  
                num_samples[label.item()] += 1  
  
        total_samples = sum(num_samples)  
        weight = [total_samples / (len(class_names) * count) if count > 0 else 1.0 for count in num_samples]  
        class_weights = torch.FloatTensor(weight).to(device)  
        print(f"类别权重: {weight}")  
        criterion = nn.CrossEntropyLoss(weight=class_weights)  
    else:  
        criterion = nn.CrossEntropyLoss()  
  
    # 使用AdamW优化器,通常在图像分类任务中表现更好  
    optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)  
  
    # 学习率调度器:余弦退火  
    scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs, eta_min=1e-6)  
  
    # 记录训练和验证性能  
    train_losses = []  
    test_losses = []  
    train_accs = []  
    test_accs = []  
  
    best_acc = 0.0  
  
    for epoch in range(epochs):  
        model.train()  
        train_loss = 0  
        correct = 0  
        total = 0  
  
        # 使用tqdm显示进度条  
        progress_bar = tqdm(train_loader, desc=f'Epoch {epoch + 1}/{epochs}')  
  
        for batch_idx, (inputs, targets) in enumerate(progress_bar):  
            inputs, targets = inputs.to(device), targets.to(device)  
  
            # 梯度清零  
            optimizer.zero_grad()  
  
            # 前向传播  
            outputs = model(inputs)  
            loss = criterion(outputs, targets)  
  
            # 反向传播和优化  
            loss.backward()  
            optimizer.step()  
  
            # 统计  
            train_loss += loss.item()  
            _, predicted = outputs.max(1)  
            total += targets.size(0)  
            correct += predicted.eq(targets).sum().item()  
  
            # 更新进度条  
            progress_bar.set_postfix({  
                'Loss': train_loss / (batch_idx + 1),  
                'Acc': 100. * correct / total,  
                'LR': optimizer.param_groups[0]['lr']  
            })  
  
        # 计算训练集性能  
        train_loss /= len(train_loader)  
        train_acc = 100. * correct / total  
        train_losses.append(train_loss)  
        train_accs.append(train_acc)  
  
        # 评估验证集性能  
        test_loss, test_acc = evaluate_model(model, test_loader, criterion)  
        test_losses.append(test_loss)  
        test_accs.append(test_acc)  
  
        # 保存最佳模型  
        if test_acc > best_acc:  
            best_acc = test_acc  
            torch.save({  
                'epoch': epoch,  
                'model_state_dict': model.state_dict(),  
                'optimizer_state_dict': optimizer.state_dict(),  
                'accuracy': best_acc,  
            }, 'resdensenet_breast_best.pth')  
  
        print(f'Epoch {epoch + 1}/{epochs} - '              f'Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}% | '  
              f'Test Loss: {test_loss:.4f}, Test Acc: {test_acc:.2f}% | '  
              f'Best Acc: {best_acc:.2f}%')  
  
        # 学习率调整  
        scheduler.step()  
  
    # 加载最佳模型  
    checkpoint = torch.load('resdensenet_breast_best.pth')  
    model.load_state_dict(checkpoint['model_state_dict'])  
    print(f"加载最佳模型(Epoch {checkpoint['epoch'] + 1},准确率: {checkpoint['accuracy']:.2f}%)")  
  
    return model, train_losses, test_losses, train_accs, test_accs  
  
  
# 评估函数  
def evaluate_model(model, data_loader, criterion=None):  
    """评估模型在数据集上的性能"""  
    if criterion is None:  
        criterion = nn.CrossEntropyLoss()  
  
    model.eval()  
    test_loss = 0  
    correct = 0  
    total = 0  
  
    with torch.no_grad():  
        for inputs, targets in data_loader:  
            inputs, targets = inputs.to(device), targets.to(device)  
            outputs = model(inputs)  
            loss = criterion(outputs, targets)  
  
            test_loss += loss.item()  
            _, predicted = outputs.max(1)  
            total += targets.size(0)  
            correct += predicted.eq(targets).sum().item()  
  
    test_loss /= len(data_loader)  
    test_acc = 100. * correct / total  
  
    return test_loss, test_acc  
  
  
# 绘制训练过程图  
def plot_learning_curves(train_losses, test_losses, train_accs, test_accs):  
    """绘制训练和验证的损失曲线和准确率曲线"""  
    plt.figure(figsize=(15, 6))  
  
    # 创建带有两个y轴的图表  
    ax1 = plt.subplot(1, 2, 1)  
    ax2 = plt.subplot(1, 2, 2)  
  
    # 绘制损失曲线  
    epochs = range(1, len(train_losses) + 1)  
    ax1.plot(epochs, train_losses, 'b-', label='Training Loss')  
    ax1.plot(epochs, test_losses, 'r-', label='Validation Loss')  
    ax1.set_title('Loss Curves')  
    ax1.set_xlabel('Epochs')  
    ax1.set_ylabel('Loss')  
    ax1.legend()  
    ax1.grid(True, linestyle='--', alpha=0.7)  
  
    # 绘制准确率曲线  
    ax2.plot(epochs, train_accs, 'b-', label='Training Accuracy')  
    ax2.plot(epochs, test_accs, 'r-', label='Validation Accuracy')  
    ax2.set_title('Accuracy Curves')  
    ax2.set_xlabel('Epochs')  
    ax2.set_ylabel('Accuracy (%)')  
    ax2.legend()  
    ax2.grid(True, linestyle='--', alpha=0.7)  
  
    plt.tight_layout()  
    plt.savefig('breast_cancer_learning_curves.png', dpi=300)  
    plt.show()  
  
  
# 分析混淆矩阵  
def confusion_matrix_analysis(model, test_loader, class_names):  
    """绘制混淆矩阵并分析模型表现"""  
    try:  
        import seaborn as sns  
        from sklearn.metrics import confusion_matrix, classification_report  
  
        model.eval()  
        all_preds = []  
        all_targets = []  
  
        with torch.no_grad():  
            for inputs, targets in tqdm(test_loader, desc="Evaluating"):  
                inputs, targets = inputs.to(device), targets.to(device)  
                outputs = model(inputs)  
                _, preds = outputs.max(1)  
  
                all_preds.extend(preds.cpu().numpy())  
                all_targets.extend(targets.cpu().numpy())  
  
        # 计算混淆矩阵  
        cm = confusion_matrix(all_targets, all_preds)  
  
        # 绘制混淆矩阵热图  
        plt.figure(figsize=(10, 8))  
        sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',  
                    xticklabels=class_names, yticklabels=class_names)  
        plt.xlabel('预测')  
        plt.ylabel('真实')  
        plt.title('混淆矩阵')  
        plt.savefig('breast_cancer_confusion_matrix.png', dpi=300)  
        plt.show()  
  
        # 打印分类报告  
        report = classification_report(all_targets, all_preds, target_names=class_names)  
        print("分类报告:")  
        print(report)  
  
        # 计算特定指标  
        tn, fp, fn, tp = cm.ravel()  
        sensitivity = tp / (tp + fn) if (tp + fn) > 0 else 0  # 敏感度 (召回率)  
        specificity = tn / (tn + fp) if (tn + fp) > 0 else 0  # 特异度  
        precision = tp / (tp + fp) if (tp + fp) > 0 else 0  # 精确率  
        f1_score = 2 * (precision * sensitivity) / (precision + sensitivity) if (precision + sensitivity) > 0 else 0  
  
        print(f"敏感度(Sensitivity/Recall): {sensitivity:.4f}")  
        print(f"特异度(Specificity): {specificity:.4f}")  
        print(f"精确率(Precision): {precision:.4f}")  
        print(f"F1分数: {f1_score:.4f}")  
  
        return cm, report  
  
    except ImportError:  
        print("需要安装seaborn和scikit-learn以分析混淆矩阵: pip install seaborn scikit-learn")  
        return None, None  
  
  
# 可视化预测结果  
def visualize_predictions(model, test_loader, class_names, num_samples=10):  
    """可视化一些测试样本的预测结果"""  
    model.eval()  
  
    # 收集一些预测样本  
    samples = []  
    with torch.no_grad():  
        for inputs, targets in test_loader:  
            inputs, targets = inputs.to(device), targets.to(device)  
            outputs = model(inputs)  
            probs = F.softmax(outputs, dim=1)  
            _, preds = outputs.max(1)  
  
            for i in range(inputs.size(0)):  
                if len(samples) < num_samples:  
                    samples.append({  
                        'image': inputs[i].cpu(),  
                        'true': targets[i].item(),  
                        'pred': preds[i].item(),  
                        'prob': probs[i, preds[i]].item()  
                    })  
                else:  
                    break  
  
            if len(samples) >= num_samples:  
                break  
  
    # 显示预测结果  
    fig, axes = plt.subplots(2, 5, figsize=(15, 6))  
    axes = axes.flatten()  
  
    # 反标准化函数  
    mean = torch.tensor([0.485, 0.456, 0.406])  
    std = torch.tensor([0.229, 0.224, 0.225])  
  
    def denormalize(x):  
        x = x.clone()  
        for i in range(3):  
            x[i] = x[i] * std[i] + mean[i]  
        return x.clamp(0, 1)  
  
    for i, sample in enumerate(samples):  
        # 反标准化图像  
        img = denormalize(sample['image'])  
        # 转换为numpy并调整通道顺序  
        img = img.permute(1, 2, 0).numpy()  
  
        axes[i].imshow(img)  
        color = 'green' if sample['true'] == sample['pred'] else 'red'  
        label = f"真实: {class_names[sample['true']]}\n预测: {class_names[sample['pred']]}\n概率: {sample['prob']:.2f}"  
        axes[i].set_title(label, color=color)  
        axes[i].axis('off')  
  
    plt.tight_layout()  
    plt.savefig('breast_cancer_predictions.png', dpi=300)  
    plt.show()  
  
  
# 主训练函数  
def main(batch_size=32, epochs=50, lr=0.001, img_size=224):  
    print("训练乳腺癌分类ResDenseNet网络...")  
  
    # 加载数据  
    train_loader, test_loader, class_names = load_breast_cancer_data(  
        data_dir='./data/J3-data/',  
        batch_size=batch_size,  
        img_size=img_size  
    )  
  
    # 创建模型  
    model = ResDenseNet(num_classes=len(class_names), growth_rate=32, input_size=img_size)  
    print(f"模型创建完成,将训练{epochs}个epoch。")  
  
    # 显示模型参数数量  
    params = sum(p.numel() for p in model.parameters() if p.requires_grad)  
    print(f"模型参数数量: {params:,}")  
  
    # 训练模型  
    model, train_losses, test_losses, train_accs, test_accs = train_model(  
        model, train_loader, test_loader, epochs=epochs, lr=lr, class_names=class_names  
    )  
  
    # 绘制学习曲线  
    plot_learning_curves(train_losses, test_losses, train_accs, test_accs)  
  
    # 分析混淆矩阵  
    cm, report = confusion_matrix_analysis(model, test_loader, class_names)  
  
    # 可视化预测结果  
    visualize_predictions(model, test_loader, class_names)  
  
    # 最终评估  
    final_loss, final_acc = evaluate_model(model, test_loader)  
    print(f'最终测试集准确率: {final_acc:.2f}%')  
  
    return model  
  
  
if __name__ == "__main__":  
    model = main(batch_size=32, epochs=50, lr=0.001, img_size=224)

image.png

注:代码优化参考了第三方优化工具给出的意见,给了我很多启发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值