Browse Source

上传文件至 ''

Gorilla 2 months ago
parent
commit
3bf5080ead
5 changed files with 984 additions and 0 deletions
  1. 67 0
      CBAM.py
  2. 335 0
      ChexnetTrainer(2)(4).py
  3. 354 0
      ChexnetTrainer.py
  4. 60 0
      DatasetGenerator(2).py
  5. 168 0
      dataset.py

+ 67 - 0
CBAM.py

@@ -0,0 +1,67 @@
+# CBAM 模块
+import torch
+import torch.nn as nn
+
+# 通道注意力模块,用于学习输入特征在通道维度上的重要性
+class ChannelAttention(nn.Module):
+    def __init__(self, in_channels, reduction=16):
+        super(ChannelAttention, self).__init__()
+        # in_channels:输入的通道数,表示特征图有多少个通道
+        # reduction:缩减率,用于减少通道的维度,通常设置为16,表示将通道数缩小 16 倍后再扩展回来
+
+        self.avg_pool = nn.AdaptiveAvgPool2d(1)  # 自适应平均池化到 (1, 1),用于生成全局通道信息
+        self.max_pool = nn.AdaptiveMaxPool2d(1)  # 自适应最大池化到 (1, 1),与平均池化结合使用
+
+        # 全连接层使用1x1卷积替代,全连接层的作用是通过线性变换来学习不同通道的重要性
+        # Conv2d(in_channels, in_channels // reduction, 1, bias=False):
+        # 将输入通道数减小到原来的 1/reduction(即 1/16),用来降低计算复杂度
+        self.fc = nn.Sequential(
+            nn.Conv2d(in_channels, in_channels // reduction, 1, bias=False),
+            nn.ReLU(inplace=True),  # 激活函数,用于增加网络的非线性特性
+            nn.Conv2d(in_channels // reduction, in_channels, 1, bias=False)  # 将通道数还原为原始大小
+        )
+        self.sigmoid = nn.Sigmoid()  # Sigmoid 激活函数,用于将输出压缩到 (0, 1) 之间
+
+    def forward(self, x):
+        # 使用平均池化和最大池化得到两个特征图
+        avg_out = self.fc(self.avg_pool(x))  # 对平均池化后的特征图应用全连接层
+        max_out = self.fc(self.max_pool(x))  # 对最大池化后的特征图应用全连接层
+        out = avg_out + max_out  # 将两个特征图相加,融合两种不同池化方式的信息
+        return self.sigmoid(out) * x  # 使用 sigmoid 将结果压缩到 (0, 1) 之间,并乘以输入特征图得到加权后的输出
+
+
+# 空间注意力模块,用于学习输入特征在空间维度(H 和 W)上的重要性
+class SpatialAttention(nn.Module):
+    def __init__(self, kernel_size=7):
+        super(SpatialAttention, self).__init__()
+        # kernel_size:卷积核大小,通常为 3 或 7,用于控制注意力机制的感受野
+        assert kernel_size in (3, 7), 'kernel size must be 3 or 7'  # 检查 kernel_size 的合法性
+        padding = (kernel_size - 1) // 2  # 计算 padding 大小,以保持卷积前后特征图的大小一致
+        self.conv = nn.Conv2d(2, 1, kernel_size, padding=padding, bias=False)  # 卷积层,输入通道为2,输出通道为1
+        self.sigmoid = nn.Sigmoid()  # Sigmoid 激活函数,用于将输出压缩到 (0, 1) 之间
+
+    def forward(self, x):
+        # 平均池化和最大池化在通道维度上进行,得到两个单通道特征图
+        avg_out = torch.mean(x, dim=1, keepdim=True)  # 对输入的特征图在通道维度取平均值
+        max_out, _ = torch.max(x, dim=1, keepdim=True)  # 对输入的特征图在通道维度取最大值
+        # 将平均池化和最大池化的结果拼接在一起,得到形状为 (batch_size, 2, H, W) 的张量
+        mask = torch.cat([avg_out, max_out], dim=1)
+        # 通过卷积层生成空间注意力权重
+        mask = self.conv(mask)
+        mask = self.sigmoid(mask)  # 使用 sigmoid 将结果压缩到 (0, 1) 之间
+        return mask * x  # 使用注意力权重与输入特征图相乘,得到加权后的输出
+
+
+# CBAM 模块,结合通道注意力和空间注意力
+class CBAM(nn.Module):
+    def __init__(self, in_channels, reduction=16, kernel_size=7):
+        super(CBAM, self).__init__()
+        # 通道注意力模块,首先对通道维度进行加权
+        self.channel_attention = ChannelAttention(in_channels, reduction)
+        # 空间注意力模块,然后对空间维度进行加权
+        self.spatial_attention = SpatialAttention(kernel_size)
+
+    def forward(self, x):
+        x = self.channel_attention(x)  # 先通过通道注意力模块
+        x = self.spatial_attention(x)  # 再通过空间注意力模块
+        return x  # 返回经过 CBAM 处理的特征图

+ 335 - 0
ChexnetTrainer(2)(4).py

@@ -0,0 +1,335 @@
+import os
+import numpy as np
+import time
+import sys
+import matplotlib.pyplot as plt
+import torch
+import torch.nn as nn
+import torch.backends.cudnn as cudnn
+import torchvision
+import torchvision.transforms as transforms
+import torch.optim as optim
+import torch.nn.functional as tfunc
+import tqdm
+from torch.utils.data import DataLoader
+from torch.optim.lr_scheduler import ReduceLROnPlateau
+import torch.nn.functional as func
+from tqdm import tqdm
+from sklearn.metrics import roc_auc_score,roc_curve,auc,f1_score
+from DensenetModels import DenseNet121, DenseNet169, \
+    DenseNet201,ResNet50,FocalLoss  # 引入不同版本的DenseNet
+from DatasetGenerator import DatasetGenerator  # 引入自定义的数据集生成器
+
+
+# 定义一个用于训练、验证、测试DenseNet模型的类
+class ChexnetTrainer():
+    # 训练网络的主函数
+    def train(pathDirData, pathFileTrain, pathFileVal, nnArchitecture,
+              nnIsTrained, nnClassCount, trBatchSize, trMaxEpoch, transResize,
+              transCrop, launchTimestamp, checkpoint):
+
+        # 根据选择的模型架构来初始化不同的DenseNet模型
+        if nnArchitecture == 'DENSE-NET-121':
+            model = DenseNet121(nnClassCount,
+                                nnIsTrained).cuda()  # 初始化DenseNet121
+        elif nnArchitecture == 'DENSE-NET-169':
+            model = DenseNet169(nnClassCount,
+                                nnIsTrained).cuda()  # 初始化DenseNet169
+        elif nnArchitecture == 'DENSE-NET-201':
+            model = DenseNet201(nnClassCount,
+                             nnIsTrained).cuda()  # 初始化DenseNet201
+        elif nnArchitecture == 'RESNET-50':
+            model = ResNet50(nnClassCount, nnIsTrained).cuda()
+        else:
+            raise ValueError(
+                f"Unknown architecture: {nnArchitecture}. Please choose from 'DENSE-NET-121', 'DENSE-NET-169', 'DENSE-NET-201', 'RESNET-50'.")
+        model = model.cuda()  # 将模型加载到GPU上
+
+        # 数据预处理,包含随机裁剪、水平翻转、归一化等
+        normalize = transforms.Normalize([0.485, 0.456, 0.406],
+                                         [0.229, 0.224, 0.225])
+        transformList = [
+            transforms.RandomResizedCrop(transCrop),  # 随机裁剪到指定大小
+            transforms.RandomHorizontalFlip(),  # 随机水平翻转
+            transforms.ToTensor(),  # 转换为张量
+            normalize  # 归一化
+        ]
+        transformSequence = transforms.Compose(transformList)  # 将这些变换组成序列
+
+        # 创建训练和验证集的数据加载器
+        datasetTrain = DatasetGenerator(pathImageDirectory=pathDirData,
+                                        pathDatasetFile=pathFileTrain,
+                                        transform=transformSequence)
+        #pos_weight = datasetTrain.calculate_pos_weights()  # 计算 pos_weight
+        datasetVal = DatasetGenerator(pathImageDirectory=pathDirData,
+                                      pathDatasetFile=pathFileVal,
+                                      transform=transformSequence)
+        dataLoaderTrain = DataLoader(dataset=datasetTrain,
+                                     batch_size=trBatchSize, shuffle=True,
+                                     num_workers=8, pin_memory=True)  # 训练集
+        dataLoaderVal = DataLoader(dataset=datasetVal, batch_size=trBatchSize,
+                                   shuffle=False, num_workers=8,
+                                   pin_memory=True)  # 验证集
+
+        # 设置优化器和学习率调度器
+        optimizer = optim.Adam(model.parameters(), lr=0.0001,
+                               betas=(0.9, 0.999), eps=1e-08, weight_decay=1e-5)
+        scheduler = ReduceLROnPlateau(optimizer, factor=0.1, patience=5,
+                                      mode='min')  # 当损失不再下降时,减少学习率
+
+        # 使用多标签分类的损失函数
+        # 设置每个类别的权重,基于提供的AUC-ROC曲线
+        # class_weights = torch.tensor(
+        #     [1.39, 0.73, 1.33, 1.62, 1.32, 1.41, 1.54, 1.27, 1.59, 1.25, 1.28,
+        #      1.27, 1.48, 1.18], dtype=torch.float).cuda()
+        # 使用加权的 BCEWithLogitsLoss 作为损失函数
+        #loss = torch.nn.BCEWithLogitsLoss(pos_weight=class_weights)
+        #loss = torch.nn.MultiLabelSoftMarginLoss()
+        # 使用Focal Loss作为损失函数
+        # loss = FocalLoss(alpha=1, gamma=2, logits=True)
+        # launchTimestamp += str(loss)
+        # launchTimestamp += 'delete'
+        class_weights = torch.tensor([
+    1.1762,  # Atelectasis
+    0.6735,  # Cardiomegaly
+    0.9410,  # Effusion
+    1.6680,  # Infiltration
+    0.9699,  # Mass
+    1.1950,  # Nodule
+    1.7584,  # Pneumonia
+    0.6859,  # Pneumothorax
+    1.6683,  # Consolidation
+    0.7744,  # Edema
+    0.4625,  # Emphysema
+    0.7385,  # Fibrosis
+    1.3764,  # Pleural_Thickening
+    0.1758   # Hernia
+], dtype=torch.float32).cuda()
+        loss = FocalLoss(alpha=class_weights, gamma=2, logits=True)
+
+
+        # 加载检查点文件(如果存在),继续训练
+        #checkpoint = 'm-29102024-093913MultiLabelSoftMarginLoss()delete.pth.tar'  # 测试时写死的文件名
+        # if checkpoint != None:
+        #     modelCheckpoint = torch.load(checkpoint)
+        #     model.load_state_dict(modelCheckpoint['state_dict'])
+        #     optimizer.load_state_dict(modelCheckpoint['optimizer'])
+
+        lossMIN = 100000  # 记录最小损失值
+        train_f1_scores, val_f1_scores = [], []
+        # 训练循环,遍历指定的epoch数
+        for epochID in range(trMaxEpoch):
+
+            # 获取当前时间戳,记录每个epoch的开始时间
+            timestampTime = time.strftime("%H%M%S")
+            timestampDate = time.strftime("%d%m%Y")
+            timestampSTART = timestampDate + '-' + timestampTime
+
+            # 训练一个epoch
+            ChexnetTrainer.epochTrain(model, dataLoaderTrain, optimizer,
+                                      scheduler, trMaxEpoch, nnClassCount, loss,train_f1_scores)
+            # 验证一个epoch
+            lossVal, losstensor,val_f1 = ChexnetTrainer.epochVal(model, dataLoaderVal,optimizer, scheduler,
+                                                          trMaxEpoch,nnClassCount, loss)
+            val_f1_scores.append(val_f1)
+            # 获取每个epoch结束时的时间戳
+            timestampTime = time.strftime("%H%M%S")
+            timestampDate = time.strftime("%d%m%Y")
+            timestampEND = timestampDate + '-' + timestampTime
+
+            # 使用调度器调整学习率
+            scheduler.step(losstensor.item())
+
+            # 保存当前最佳模型
+            if lossVal < lossMIN:
+                lossMIN = lossVal
+                torch.save(
+                    {'epoch': epochID + 1, 'state_dict': model.state_dict(),
+                     'best_loss': lossMIN, 'optimizer': optimizer.state_dict()},
+                    'm-' + launchTimestamp + '.pth.tar')
+                print('Epoch [' + str(
+                    epochID + 1) + '] [save] [' + timestampEND + '] loss= ' + str(
+                    lossVal))
+            else:
+                print('Epoch [' + str(
+                    epochID + 1) + '] [----] [' + timestampEND + '] loss= ' + str(
+                    lossVal))
+        plt.plot(train_f1_scores, label="Train F1-Score")
+        plt.plot(val_f1_scores, label="Val F1-Score")
+        plt.xlabel("Epoch")
+        plt.ylabel("F1 Score")
+        plt.title("F1 Score per Epoch")
+        plt.legend()
+        #plt.savefig("f1score.png")
+        #plt.show()
+    # 训练每个epoch的具体过程
+    def epochTrain(model, dataLoader, optimizer, scheduler, epochMax,
+                   classCount, loss,f1_scores):
+        model.train()
+        all_targets = []
+        all_preds = []
+
+        for batchID, (input, target) in enumerate(tqdm(dataLoader)):
+            target = target.cuda()
+            input = input.cuda()
+
+            varInput = torch.autograd.Variable(input)
+            varTarget = torch.autograd.Variable(target)
+            varOutput = model(varInput)
+
+            lossvalue = loss(varOutput, varTarget)
+
+            optimizer.zero_grad()
+            lossvalue.backward()
+            optimizer.step()
+
+            pred = torch.sigmoid(varOutput).cpu().data.numpy() > 0.5
+            all_targets.extend(target.cpu().numpy())
+            all_preds.extend(pred)
+
+
+        f1 = f1_score(np.array(all_targets), np.array(all_preds), average="macro")
+        f1_scores.append(f1)
+
+
+    # 验证每个epoch的具体过程
+    def epochVal(model, dataLoader, optimizer, scheduler, epochMax, classCount,
+                 loss):
+        model.eval()  # 设置模型为评估模式
+
+        lossVal = 0
+        lossValNorm = 0
+        losstensorMean = 0
+        all_targets = []
+        all_preds = []
+
+        for i, (input, target) in enumerate(dataLoader):  # 遍历每个批次
+            target = target.cuda()
+            input = input.cuda()
+            with torch.no_grad():  # 禁用梯度计算,节省内存
+                varInput = torch.autograd.Variable(input)
+                varTarget = torch.autograd.Variable(target)
+                varOutput = model(varInput)  # 前向传播
+
+            losstensor = loss(varOutput, varTarget)  # 计算损失
+            losstensorMean += losstensor  # 累积损失
+            lossVal += losstensor.item()  # 累积损失值
+            lossValNorm += 1  # 记录批次数量
+
+            pred = torch.sigmoid(varOutput).cpu().data.numpy() > 0.5
+            all_targets.extend(target.cpu().numpy())
+            all_preds.extend(pred)
+        f1 = f1_score(np.array(all_targets), np.array(all_preds),
+                      average="macro")
+        outLoss = lossVal / lossValNorm  # 计算平均损失
+        losstensorMean = losstensorMean / lossValNorm  # 平均损失张量
+
+        return outLoss, losstensorMean,f1  # 返回验证损失和损失张量均值
+
+    # 计算AUROC(AUC-ROC曲线下的面积)
+    def computeAUROC(dataGT, dataPRED, classCount, plot_roc_curve=False,
+                     class_names=None):
+        outAUROC = []
+        datanpGT = dataGT.cpu().numpy()  # 将ground truth转换为numpy格式
+        datanpPRED = dataPRED.cpu().numpy()  # 将预测结果转换为numpy格式
+
+        if plot_roc_curve and class_names is None:
+            class_names = [f"Class {i + 1}" for i in range(classCount)]
+
+        # 针对每个类别计算ROC AUC分数
+        plt.figure(figsize=(12, 8))
+        for i in range(classCount):
+            # 计算当前类别的ROC AUC分数
+            outAUROC.append(roc_auc_score(datanpGT[:, i], datanpPRED[:, i]))
+            if plot_roc_curve:
+                # 计算 ROC 曲线的点
+                fpr, tpr, _ = roc_curve(datanpGT[:, i], datanpPRED[:, i])
+                roc_auc = auc(fpr, tpr)
+                plt.plot(fpr, tpr, lw=2,
+                         label=f'{class_names[i]} (area = {roc_auc:.2f})')
+
+        if plot_roc_curve:
+            plt.plot([0, 1], [0, 1], color='navy', linestyle='--')  # 绘制随机猜测线
+            plt.xlim([0.0, 1.0])
+            plt.ylim([0.0, 1.05])
+            plt.xlabel('False Positive Rate')
+            plt.ylabel('True Positive Rate')
+            plt.title('Receiver Operating Characteristic (ROC) Curves')
+            plt.legend(loc="lower right")
+            plt.savefig("aucroc.png")
+            #plt.show()
+
+        return outAUROC  # 返回每个类别的AUROC值
+
+    # 测试模型
+    def test(pathDirData, pathFileTest, pathModel, nnArchitecture, nnClassCount,
+             nnIsTrained, trBatchSize, transResize, transCrop, launchTimeStamp):
+        CLASS_NAMES = ['Atelectasis', 'Cardiomegaly', 'Effusion',
+                       'Infiltration', 'Mass', 'Nodule', 'Pneumonia',
+                       'Pneumothorax', 'Consolidation', 'Edema', 'Emphysema',
+                       'Fibrosis', 'Pleural_Thickening', 'Hernia']
+
+        cudnn.benchmark = True  # 加速卷积操作
+
+        # 根据架构选择相应的DenseNet模型
+        if nnArchitecture == 'DENSE-NET-121':
+            model = DenseNet121(nnClassCount, nnIsTrained).cuda()
+        elif nnArchitecture == 'DENSE-NET-169':
+            model = DenseNet169(nnClassCount, nnIsTrained).cuda()
+        elif nnArchitecture == 'DENSE-NET-201':
+            model = DenseNet201(nnClassCount, nnIsTrained).cuda()
+        elif nnArchitecture == 'RESNET-50':
+            model = ResNet50(nnClassCount, nnIsTrained).cuda()
+
+        modelCheckpoint = torch.load(pathModel)  # 加载模型
+        model.load_state_dict(modelCheckpoint['state_dict'])  # 载入训练好的参数
+
+        model = model.cuda()  # 将模型加载到GPU上
+        model.eval()  # 设置为评估模式
+
+        # 定义数据预处理
+        normalize = transforms.Normalize([0.485, 0.456, 0.406],
+                                         [0.229, 0.224, 0.225])
+        transformList = [
+            transforms.Resize(transResize),  # 调整大小
+            transforms.CenterCrop(transCrop),  # 中心裁剪
+            transforms.ToTensor(),  # 转换为张量
+            normalize  # 归一化
+        ]
+        transformSequence = transforms.Compose(transformList)
+
+        # 创建测试集的数据加载器
+        datasetTest = DatasetGenerator(pathImageDirectory=pathDirData,
+                                       pathDatasetFile=pathFileTest,
+                                       transform=transformSequence)
+        dataLoaderTest = DataLoader(dataset=datasetTest, batch_size=trBatchSize,
+                                    shuffle=False, num_workers=8,
+                                    pin_memory=True)
+
+        # 初始化张量来存储ground truth和预测结果
+        outGT = torch.FloatTensor().cuda()
+        outPRED = torch.FloatTensor().cuda()
+
+        # 遍历测试集
+        for i, (input, target) in enumerate(dataLoaderTest):
+            target = target.cuda()
+            input = input.cuda()
+
+            with torch.no_grad():
+                varInput = torch.autograd.Variable(input)
+                out = model(varInput)  # 前向传播
+                outPRED = torch.cat((outPRED, out), 0)  # 将输出结果连接起来
+                outGT = torch.cat((outGT, target), 0)  # 将ground truth连接起来
+
+        # 计算AUROC值
+        aurocIndividual = ChexnetTrainer.computeAUROC(outGT, outPRED,
+                                                      nnClassCount,
+                                                      plot_roc_curve=True,
+                                                      class_names=CLASS_NAMES)
+        aurocMean = np.array(aurocIndividual).mean()  # 计算平均AUROC值
+
+        # 输出每个类别的AUROC
+        for i in range(len(aurocIndividual)):
+            print(f'{CLASS_NAMES[i]}: {aurocIndividual[i]}')
+
+        print(f'MEAN: {aurocMean}')

+ 354 - 0
ChexnetTrainer.py

@@ -0,0 +1,354 @@
+import os
+import numpy as np
+import torch
+import torch.nn as nn
+import torch.optim as optim
+from torchvision import transforms
+import matplotlib.pyplot as plt
+from torch.utils.data import DataLoader
+from sklearn.ensemble import RandomForestClassifier
+from sklearn.model_selection import train_test_split
+from sklearn.metrics import accuracy_score, f1_score
+from tqdm import tqdm
+import joblib  # 导入joblib库用于保存模型
+from DatasetGenerator import DatasetGenerator
+from DensenetModels import DenseNet121, DenseNet169, DenseNet201, ResNet50, FocalLoss
+from sklearn.multioutput import MultiOutputClassifier
+from sklearn.metrics import roc_curve, auc
+
+
+class ChexnetTrainer:
+
+
+    # 在 ChexnetTrainer 类中添加以下方法
+
+    @staticmethod
+    def train(pathDirData, pathFileTrain, pathFileVal, nnArchitecture,
+              nnIsTrained, nnClassCount, trBatchSize, trMaxEpoch, transResize,
+              transCrop, launchTimestamp, checkpoint=None):
+        """训练函数"""
+
+        # 自动选择设备
+        device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+
+        # 获取模型并迁移到设备
+        model = ChexnetTrainer._get_model(nnArchitecture, nnClassCount, nnIsTrained).to(device)
+
+        # 设置数据预处理和数据加载器
+        transformSequence = ChexnetTrainer._get_transform(transCrop)
+        datasetTrain = DatasetGenerator(pathImageDirectory=pathDirData, pathDatasetFile=pathFileTrain,
+                                        transform=transformSequence, model=model)
+        datasetVal = DatasetGenerator(pathImageDirectory=pathDirData, pathDatasetFile=pathFileVal,
+                                      transform=transformSequence, model=model)
+
+        dataLoaderTrain = DataLoader(dataset=datasetTrain, batch_size=trBatchSize, shuffle=True, num_workers=4,
+                                     pin_memory=True)
+        dataLoaderVal = DataLoader(dataset=datasetVal, batch_size=trBatchSize, shuffle=False, num_workers=4,
+                                   pin_memory=True)
+
+        optimizer = optim.Adam(model.parameters(), lr=0.0001, betas=(0.9, 0.999), eps=1e-08, weight_decay=1e-5)
+
+        # 替换 ReduceLROnPlateau 为 StepLR
+        scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)
+
+        # 迁移class_weights到对应设备
+        class_weights = torch.tensor(
+            [1.1762, 0.6735, 0.9410, 1.6680, 0.9699, 1.1950, 2.2584, 0.6859, 1.6683, 0.7744, 0.4625, 0.7385, 1.3764,
+             0.1758], dtype=torch.float32).to(device)
+
+        loss = FocalLoss(alpha=class_weights, gamma=2, logits=True)
+
+        lossMIN = 100000
+        train_f1_scores, val_f1_scores = [], []
+        all_val_targets = []
+        all_val_scores = []
+
+        # 训练循环
+        for epochID in range(trMaxEpoch):
+            ChexnetTrainer.epochTrain(model, dataLoaderTrain, optimizer, scheduler, trMaxEpoch, nnClassCount, loss,
+                                      train_f1_scores, device)
+
+            lossVal, losstensor, val_f1, val_targets, val_scores = ChexnetTrainer.epochVal(
+                model, dataLoaderVal, optimizer, scheduler,
+                trMaxEpoch, nnClassCount, loss, val_f1_scores, device)
+
+            all_val_targets.append(val_targets)
+            all_val_scores.append(val_scores)
+
+            # 更新学习率
+            scheduler.step()
+
+            # 保存最佳模型
+            if lossVal < lossMIN:
+                lossMIN = lossVal
+                torch.save({'epoch': epochID + 1, 'state_dict': model.state_dict(), 'best_loss': lossMIN,
+                            'optimizer': optimizer.state_dict()},
+                           'm-' + launchTimestamp + '.pth.tar')
+
+        # 合并所有验证集的标签和预测
+        all_val_targets = np.vstack(all_val_targets)
+        all_val_scores = np.vstack(all_val_scores)
+
+        # 绘制F1分数图
+        plt.figure()
+        plt.plot(train_f1_scores, label="Train F1-Score")
+        plt.plot(val_f1_scores, label="Val F1-Score")
+        plt.xlabel("Epoch")
+        plt.ylabel("F1 Score")
+        plt.title("F1 Score per Epoch")
+        plt.legend()
+        plt.savefig(os.path.join('images', 'f1_scores.png'))
+        plt.close()
+
+        # 计算并绘制AUC-ROC曲线
+        CLASS_NAMES = ['Atelectasis', 'Cardiomegaly', 'Effusion',
+                       'Infiltration', 'Mass', 'Nodule', 'Pneumonia',
+                       'Pneumothorax', 'Consolidation', 'Edema', 'Emphysema',
+                       'Fibrosis', 'Pleural_Thickening',
+                       'Hernia']
+
+        ChexnetTrainer.plot_auc_roc(all_val_targets, all_val_scores, CLASS_NAMES,
+                                    os.path.join('images', 'auc_roc_curve.png'))
+        print("AUC-ROC曲线已保存到 images/auc_roc_curve.png")
+
+        # 提取特征并训练随机森林
+        rf_classifier = ChexnetTrainer.train_random_forest(datasetTrain, datasetVal, model, device)
+
+        # 保存随机森林模型
+        joblib.dump(rf_classifier, os.path.join('images', 'random_forest_model.pkl'))
+        # 保存随机森林分类器
+
+    @staticmethod
+    def epochTrain(model, dataLoader, optimizer, scheduler, epochMax, classCount, loss, f1_scores, device):
+        model.train()
+        all_targets = []
+        all_preds = []
+
+        # 创建进度条
+        pbar = tqdm(total=len(dataLoader), desc="Training", leave=True)
+
+        for batchID, (input, target) in enumerate(dataLoader):
+            target = target.to(device)
+            input = input.to(device)
+
+            varInput = torch.autograd.Variable(input)
+            varTarget = torch.autograd.Variable(target)
+            varOutput = model(varInput)
+
+            lossvalue = loss(varOutput, varTarget)
+            optimizer.zero_grad()
+            lossvalue.backward()
+            optimizer.step()
+
+            pred = torch.sigmoid(varOutput).cpu().data.numpy() > 0.5
+            all_targets.extend(target.cpu().numpy())
+            all_preds.extend(pred)
+
+            # 更新进度条
+            pbar.update(1)
+        pbar.close()
+
+        f1 = f1_score(np.array(all_targets), np.array(all_preds), average="macro")
+
+        f1_scores.append(f1)
+        # 在每个epoch结束时打印当前的F1-score
+        print(f"Epoch completed. F1 Score (Macro): {f1:.4f}")
+
+    @staticmethod
+    def epochVal(model, dataLoader, optimizer, scheduler, epochMax, classCount, loss, f1_scores, device):
+        model.eval()
+        lossVal = 0
+        lossValNorm = 0
+        losstensorMean = 0
+        all_targets = []
+        all_preds = []
+        all_scores = []  # 收集预测概率
+
+        # 创建进度条
+        pbar = tqdm(total=len(dataLoader), desc="Validation", leave=True)
+
+        for i, (input, target) in enumerate(dataLoader):
+            target = target.to(device)
+            input = input.to(device)
+            with torch.no_grad():
+                varInput = torch.autograd.Variable(input)
+                varTarget = torch.autograd.Variable(target)
+                varOutput = model(varInput)
+
+            losstensor = loss(varOutput, varTarget)
+            losstensorMean += losstensor
+            lossVal += losstensor.item()
+            lossValNorm += 1
+
+            scores = torch.sigmoid(varOutput).cpu().data.numpy()
+            all_scores.extend(scores)
+            pred = scores > 0.5
+            all_targets.extend(target.cpu().numpy())
+            all_preds.extend(pred)
+
+            # 更新进度条
+            pbar.update(1)
+        pbar.close()
+
+        f1 = f1_score(np.array(all_targets), np.array(all_preds), average="macro")
+        f1_scores.append(f1)
+        # 在每个epoch结束时打印当前的F1-score
+        print(f"Epoch completed. F1 Score (Macro): {f1:.4f}")
+
+        return lossVal / lossValNorm, losstensorMean / lossValNorm, f1, np.array(all_targets), np.array(all_scores)
+
+    @staticmethod
+    def train_random_forest(datasetTrain, datasetVal, model, device):
+        """训练随机森林分类器,适应多标签分类"""
+
+        print("正在提取训练数据的特征。")
+        # 提取训练数据的特征
+        train_features, train_labels = ChexnetTrainer.extract_features(datasetTrain, model, device)
+        val_features, val_labels = ChexnetTrainer.extract_features(datasetVal, model, device)
+
+        print("正在训练随机森林分类器。")
+        # 使用MultiOutputClassifier来处理多标签问题
+        rf_classifier = MultiOutputClassifier(RandomForestClassifier(n_estimators=100))
+        rf_classifier.fit(train_features, train_labels)
+
+        print("正在评估随机森林分类器。")
+        # 在验证集上评估随机森林
+        val_preds = rf_classifier.predict(val_features)
+        val_f1 = f1_score(val_labels, val_preds, average='macro')
+
+        print(f"Random Forest F1 Score on validation set: {val_f1}")
+
+        return rf_classifier
+
+    @staticmethod
+    def extract_features(dataset, model, device):
+        """提取数据集的特征"""
+        features = []
+        labels = []
+
+        print("正在提取数据集的特征。")
+        model.eval()
+        dataLoader = DataLoader(dataset=dataset, batch_size=32, shuffle=False, num_workers=4, pin_memory=True)
+        for input, target in tqdm(dataLoader, desc="Extracting Features"):
+            input = input.to(device)
+            target = target.to(device)
+
+            with torch.no_grad():
+                varInput = torch.autograd.Variable(input)
+                varOutput = model(varInput)
+
+            # 这里假设 varOutput 是一个二维张量,我们需要把它展平成一维向量
+            features.append(varOutput.view(varOutput.size(0), -1).cpu().data.numpy())  # 展平特征
+            labels.append(target.cpu().data.numpy())  # 假设 target 是多标签格式的
+
+        # 使用 np.vstack 将特征和标签拼接成数组
+        return np.vstack(features), np.vstack(labels)  # 确保标签是二维矩阵,每一行对应一个样本的标签
+
+    @staticmethod
+    def plot_auc_roc(y_true, y_scores, class_names, save_path):
+        """绘制AUC-ROC曲线并保存"""
+        plt.figure(figsize=(20, 15))
+        for i, class_name in enumerate(class_names):
+            fpr, tpr, _ = roc_curve(y_true[:, i], y_scores[:, i])
+            roc_auc = auc(fpr, tpr)
+            plt.plot(fpr, tpr, lw=2, label=f'ROC curve of class {class_name} (area = {roc_auc:.2f})')
+
+        plt.plot([0, 1], [0, 1], 'k--', lw=2)
+        plt.xlim([0.0, 1.0])
+        plt.ylim([0.0, 1.05])
+        plt.xlabel('False Positive Rate')
+        plt.ylabel('True Positive Rate')
+        plt.title('Receiver Operating Characteristic (ROC) Curves')
+        plt.legend(loc="lower right")
+        plt.savefig(save_path)
+        plt.close()
+
+    @staticmethod
+    def test(pathDirData, pathFileTest, pathModel, pathRfModel, nnArchitecture, nnClassCount, nnIsTrained,
+             trBatchSize, imgtransResize, imgtransCrop, timestampLaunch):
+        """测试函数,支持深度学习模型和随机森林模型"""
+        # 加载深度学习模型
+        device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+        model = ChexnetTrainer._get_model(nnArchitecture, nnClassCount, nnIsTrained).to(device)
+        checkpoint = torch.load(pathModel, map_location=device)
+        model.load_state_dict(checkpoint['state_dict'])
+        model.eval()  # 确保模型处于评估模式
+
+        # 加载随机森林模型
+        rf_classifier = joblib.load(pathRfModel)
+
+        # 获取 transform
+        transformSequence = ChexnetTrainer._get_transform(imgtransCrop)
+        datasetTest = DatasetGenerator(pathImageDirectory=pathDirData, pathDatasetFile=pathFileTest,
+                                       transform=transformSequence, model=model)
+
+        # 使用 extract_features 提取特征和标签
+        features, labels = ChexnetTrainer.extract_features(datasetTest, model, device)
+
+        # 使用随机森林模型进行预测
+        rf_preds = rf_classifier.predict(features)
+
+        # 计算多标签分类的 F1 分数
+        rf_f1 = f1_score(labels, rf_preds, average="macro")
+        print(f"Random Forest Multi-label F1 Score on test set: {rf_f1}")
+
+        # 计算AUC-ROC
+        rf_scores = []
+        if hasattr(rf_classifier, "predict_proba"):
+            rf_scores = rf_classifier.predict_proba(features)
+            # 将列表转换为数组,并选择正类的概率
+            rf_scores = np.array([rf_scores[i][:, 1] for i in range(len(rf_scores))]).reshape(labels.shape)
+        else:
+            # 如果没有predict_proba方法,则无法计算AUC
+            print("随机森林模型不支持predict_proba方法,无法计算AUC-ROC。")
+            rf_scores = np.zeros_like(labels)
+
+        # 绘制AUC-ROC曲线
+        CLASS_NAMES = ['Atelectasis', 'Cardiomegaly', 'Effusion',
+                       'Infiltration', 'Mass', 'Nodule', 'Pneumonia',
+                       'Pneumothorax', 'Consolidation', 'Edema', 'Emphysema',
+                       'Fibrosis', 'Pleural_Thickening',
+                       'Hernia']
+
+        ChexnetTrainer.plot_auc_roc(labels, rf_scores, CLASS_NAMES,
+                                    os.path.join('images', 'test_auc_roc_curve.png'))
+        print("测试集的AUC-ROC曲线已保存到 images/test_auc_roc_curve.png")
+
+        # 输出深度学习模型和随机森林模型的 F1 分数
+        print(f"Testing completed. Random Forest F1 Score: {rf_f1}")
+
+        return labels, rf_preds  # 返回真实标签和预测结果
+
+    # 其他方法保持不变...
+
+    @staticmethod
+    def _get_model(nnArchitecture, nnClassCount, nnIsTrained):
+        """根据选择的模型架构返回对应的模型"""
+        device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+
+        # 根据模型架构选择模型
+        if nnArchitecture == 'DENSE-NET-121':
+            model = DenseNet121(nnClassCount, nnIsTrained).to(device)
+        elif nnArchitecture == 'DENSE-NET-169':
+            model = DenseNet169(nnClassCount, nnIsTrained).to(device)
+        elif nnArchitecture == 'DENSE-NET-201':
+            model = DenseNet201(nnClassCount, nnIsTrained).to(device)
+        elif nnArchitecture == 'RESNET-50':
+            model = ResNet50(nnClassCount, nnIsTrained).to(device)
+        else:
+            raise ValueError(
+                f"Unknown architecture: {nnArchitecture}. Please choose from 'DENSE-NET-121', 'DENSE-NET-169', 'DENSE-NET-201', 'RESNET-50'.")
+
+        return model
+
+    @staticmethod
+    def _get_transform(transCrop):
+        """返回图像预处理的转换"""
+        normalize = transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
+        transformList = [
+            transforms.RandomResizedCrop(transCrop),
+            transforms.RandomHorizontalFlip(),
+            transforms.ToTensor(),
+            normalize
+        ]
+        return transforms.Compose(transformList)

+ 60 - 0
DatasetGenerator(2).py

@@ -0,0 +1,60 @@
+import os
+import numpy as np
+from PIL import Image
+
+import torch
+from torch.utils.data import Dataset
+
+# --------------------------------------------------------------------------------
+
+# 定义一个数据集类 DatasetGenerator,继承自 PyTorch 的 Dataset 类
+class DatasetGenerator(Dataset):
+
+    # --------------------------------------------------------------------------------
+
+    # 初始化函数,传入图像目录路径、数据集文件路径和图像预处理变换
+    def __init__(self, pathImageDirectory, pathDatasetFile, transform):
+        self.listImagePaths = []  # 用于存储图像路径的列表
+        self.listImageLabels = []  # 用于存储标签的列表
+        self.transform = transform  # 图像的预处理方法
+
+        # ---- 打开文件,获取图像路径和标签
+        with open(pathDatasetFile, "r") as fileDescriptor:
+            for line in fileDescriptor:
+                lineItems = line.strip().split()
+                imagePath = os.path.join(pathImageDirectory, lineItems[0])  # 获取图像文件的完整路径
+                imageLabel = lineItems[1:]  # 获取对应的标签(位于行的第二部分)
+
+                # 将标签转换为整数列表,并确保每个标签是整数(有可能是浮点数的情况需要处理)
+                imageLabel = [int(float(i)) for i in imageLabel]
+
+                # 如果标签数组中至少有一个值为1(即图片至少有一个分类标签)
+                if np.array(imageLabel).sum() >= 1:
+                    self.listImagePaths.append(imagePath)  # 将图像路径加入列表
+                    self.listImageLabels.append(imageLabel)  # 将图像标签加入列表
+
+    # --------------------------------------------------------------------------------
+
+    # 获取数据集中特定索引的图像及其标签
+    def __getitem__(self, index):
+        # 根据索引获取图像路径
+        imagePath = self.listImagePaths[index]
+
+        # 打开图像文件,并将图像转换为 RGB 模式
+        imageData = Image.open(imagePath).convert('RGB')
+
+        # 将对应的标签转换为 PyTorch 的 FloatTensor(浮点数张量)
+        imageLabel = torch.FloatTensor(self.listImageLabels[index])
+
+        # 如果有图像预处理操作,应用预处理
+        if self.transform is not None:
+            imageData = self.transform(imageData)
+
+        # 返回图像数据和其对应的标签
+        return imageData, imageLabel
+
+    # --------------------------------------------------------------------------------
+
+    # 返回数据集的样本数量
+    def __len__(self):
+        return len(self.listImagePaths)

+ 168 - 0
dataset.py

@@ -0,0 +1,168 @@
+import numpy as np
+import pandas as pd
+import cv2
+from PIL import Image
+from sklearn.model_selection import train_test_split
+from torch.utils.data import Dataset
+import os
+from torchvision import transforms
+
+
+# 定义图像预处理的函数,参数包括是否为训练模式以及自定义参数(args)
+def build_transform(train, args):
+    if train:
+        # 如果是训练模式,进行一系列数据增强和归一化处理
+        transform = transforms.Compose((
+            transforms.RandomResizedCrop(int(args.img_size / 0.875),
+                                         scale=(0.8, 1.0)),  # 随机裁剪图像
+            transforms.RandomRotation(7),  # 随机旋转图像
+            transforms.RandomHorizontalFlip(),  # 随机水平翻转
+            transforms.CenterCrop(args.img_size),  # 中心裁剪
+            transforms.ToTensor(),  # 转换为张量
+            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
+        # 使用Imagenet的均值和方差进行归一化
+        ))
+    else:
+        # 如果是验证或测试模式,只进行裁剪和归一化处理
+        transform = transforms.Compose((
+            transforms.Resize(int(args.img_size / 0.875)),  # 调整大小
+            transforms.CenterCrop(args.img_size),  # 中心裁剪
+            transforms.ToTensor(),  # 转换为张量
+            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
+        # 归一化
+        ))
+    return transform
+
+
+# 定义数据集类 photodatatest,继承自 PyTorch 的 Dataset 类
+class ChestXray14Dataset(Dataset):
+    def __init__(self,
+                 data_root,  # 数据集路径
+                 classes=['Atelectasis', 'Cardiomegaly', 'Effusion',
+                          'Infiltration', 'Mass', 'Nodule', 'Pneumonia',
+                          'Pneumothorax', 'Consolidation', 'Edema', 'Emphysema',
+                          'Fibrosis', 'Pleural_Thickening', 'Hernia'],
+                 mode='train',  # 模式:'train', 'valid', 或 'test'
+                 split='official',  # 数据划分方式:'official' 或 'non-official'
+                 has_val_set=True,  # 是否包含验证集
+                 transform=None):  # 图像预处理方法
+        super().__init__()
+        self.data_root = data_root  # 数据集根目录
+        self.classes = classes  # 多标签类别
+        self.num_classes = len(self.classes)  # 类别数
+        self.mode = mode  # 当前数据集模式
+
+        # 根据split的类型选择加载方式
+        if split == 'official':
+            # 使用官方划分方式加载数据
+            self.dataframe, self.num_patients = self.load_split_file(
+                self.data_root, self.mode, has_val_set)
+        else:
+            # 使用非官方划分方式加载数据
+            self.dataframe, self.num_patients = self.load_split_file_non_official(
+                self.data_root, self.mode)
+
+        self.transform = transform  # 图像预处理方法
+        self.num_samples = len(self.dataframe)  # 样本数量
+
+    # 加载官方划分文件的方法,根据mode加载不同的数据
+    def load_split_file(self, folder, mode, has_val=True):
+        df = pd.read_csv(
+            os.path.join(
+                'F:\chexnet\chexnet-master\photodatatest',
+                'Data_Entry_2017.csv'))  # 使用绝对路径
+
+        # 如果模式为训练或验证
+        if mode in ['train', 'valid']:
+            # 加载训练和验证数据文件
+            file_name = os.path.join(folder, 'train_val_list.txt')
+            with open(file_name, 'r') as f:
+                lines = f.read().splitlines()  # 读取所有图像文件名
+            df_train_val = df[df['Image Index'].isin(lines)]  # 过滤出对应的图像
+
+            # 如果需要验证集,将患者ID拆分为训练和验证集
+            if has_val:
+                patient_ids = df_train_val['Patient ID'].unique()  # 获取所有患者ID
+                train_ids, val_ids = train_test_split(patient_ids,
+                                                      test_size=1 - 0.7 / 0.8,
+                                                      random_state=0,
+                                                      shuffle=True)
+                target_ids = train_ids if mode == 'train' else val_ids  # 根据模式选择训练或验证集的患者ID
+                df = df_train_val[
+                    df_train_val['Patient ID'].isin(target_ids)]  # 根据ID过滤数据
+            else:
+                df = df_train_val
+        elif mode == 'test':
+            # 如果模式为测试,加载测试数据文件
+            file_name = os.path.join(folder, 'test_list.txt')
+            with open(file_name, 'r') as f:
+                target_files = f.read().splitlines()  # 读取测试集文件名
+            df = df[df['Image Index'].isin(target_files)]  # 过滤测试数据
+        else:
+            raise NotImplementedError(f'Unidentified split: {mode}')  # 未识别的模式报错
+
+        num_patients = len(df['Patient ID'].unique())  # 统计患者数
+        return df, num_patients
+
+    # 非官方划分文件的加载方法,根据比例拆分数据集
+    def load_split_file_non_official(self, folder, mode):
+        train_rt, val_rt, test_rt = 0.7, 0.1, 0.2  # 定义训练、验证和测试的比例
+        df = pd.read_csv(
+            os.path.join(folder, 'Data_Entry_2017.csv'))  # 加载数据标签文件
+        patient_ids = df['Patient ID'].unique()  # 获取所有患者ID
+
+        # 先划分出测试集,然后在剩余数据中划分出验证集和训练集
+        train_val_ids, test_ids = train_test_split(patient_ids,
+                                                   test_size=test_rt,
+                                                   random_state=0, shuffle=True)
+        train_ids, val_ids = train_test_split(train_val_ids,
+                                              test_size=val_rt / (
+                                                          train_rt + val_rt),
+                                              random_state=0, shuffle=True)
+
+        # 根据模式选择目标ID
+        target_ids = {'train': train_ids, 'valid': val_ids, 'test': test_ids}[
+            mode]
+        df = df[df['Patient ID'].isin(target_ids)]  # 根据ID过滤数据
+        num_patients = len(target_ids)  # 统计患者数
+        return df, num_patients
+
+    # 将疾病标签转换为多标签编码
+    def encode_label(self, label):
+        encoded_label = np.zeros(self.num_classes,
+                                 dtype=np.float32)  # 初始化全0的标签数组
+        if label != 'No Finding':  # 如果标签不为"No Finding"
+            for l in label.split('|'):  # 对每个疾病标签进行处理
+                encoded_label[self.classes.index(l)] = 1  # 将对应疾病的索引位置置1
+        return encoded_label
+
+    # 图像预处理函数,调整图像尺寸
+    def pre_process(self, img):
+        h, w = img.shape
+        img = cv2.resize(img, dsize=(max(h, w), max(h, w)))  # 将图像调整为正方形
+        return img
+
+    # 统计每个类别的样本数量
+    def count_class_dist(self):
+        class_counts = np.zeros(self.num_classes)  # 初始化类别计数
+        for index, row in self.dataframe.iterrows():  # 遍历数据集的每一行
+            class_counts += self.encode_label(
+                row['Finding Labels'])  # 将标签编码加到计数器中
+        return self.num_samples, class_counts
+
+    # 返回数据集的样本数量
+    def __len__(self):
+        return self.num_samples
+
+    # 获取指定索引的数据
+    def __getitem__(self, idx):
+        row = self.dataframe.iloc[idx]  # 获取对应行的数据
+        img_file, label = row['Image Index'], row[
+            'Finding Labels']  # 获取图像文件名和标签
+        img = cv2.imread(
+            os.path.join(self.data_root, 'images', img_file))  # 读取图像
+        img = Image.fromarray(img)  # 转换为PIL图像
+        if self.transform is not None:
+            img = self.transform(img)  # 应用预处理
+        label = self.encode_label(label)  # 编码标签
+        return img, label  # 返回图像和标签