BraTS2025(SWIN-UNETR)

安装

pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install -r environment.yml -i https://pypi.tuna.tsinghua.edu.cn/simple

环境

conda env create -f environment.yml
conda activate swin_unetr

Dataloader

# 导入必要的库和模块

import os

import json

import torch

from monai.data import CacheDataset, DataLoader, decollate_batch

from monai.transforms import (

    Compose,

    LoadImaged,

    EnsureChannelFirstd,

    EnsureTyped,

    Orientationd,

    Spacingd,

    RandSpatialCropd,

    RandFlipd,

    NormalizeIntensityd,

    RandScaleIntensityd,

    RandShiftIntensityd,

    Activationsd,

    AsDiscreted,

    Invertd,

    MapTransform,

    Resized

)

  
  

# 定义一个自定义转换,将单通道标签转换为多通道格式

class ConvertToMultiChannelBasedOnBrats25Classesd(MapTransform):

    """

    根据 Brats25 标签的类别,将单通道标签转换为多通道格式。

    - 标签 1:坏死和非增强肿瘤核心

    - 标签 2:肿瘤周围水肿

    - 标签 3:GD 增强肿瘤

    - 标签 4:其他

    输出通道对应:

    - 通道 0:标签 1

    - 通道 1:标签 2

    - 通道 2:标签 3

    - 通道 3:标签 4

    """

  

    def __call__(self, data):

        d = dict(data)

        for key in self.keys:

            result = []

            # 标签 1:坏死和非增强肿瘤核心

            result.append(d[key] == 1)

            # 标签 2:肿瘤周围水肿

            result.append(d[key] == 2)

            # 标签 3:GD 增强肿瘤

            result.append(d[key] == 3)

            # 标签 4:其他

            result.append(d[key] == 4)

            d[key] = torch.stack(result, axis=0).float()

        return d

  
  

# 定义一个函数,用于读取数据列表并转换路径

def datafold_read(datalist, basedir, key="training"):

    """

    读取数据列表,并将相对路径转换为绝对路径。

    :param datalist: 数据列表文件路径

    :param basedir: 数据基目录

    :param key: 数据类型(训练、验证或测试)

    :return: 转换后的数据列表

    """

    with open(datalist) as f:

        json_data = json.load(f)

  

    json_data = json_data[key]

  

    for d in json_data:

        for k, v in d.items():

            if isinstance(d[k], list):

                d[k] = [os.path.join(basedir, iv) for iv in d[k]]

            elif isinstance(d[k], str):

                d[k] = os.path.join(basedir, d[k]) if len(d[k]) > 0 else d[k]

  

    return json_data

  
  

# 定义一个函数,用于获取训练和验证数据加载器

def get_loader(args):

    """

    获取训练和验证数据加载器。

    :param args: 参数对象,包含数据目录、数据列表文件等信息

    :return: 训练和验证数据加载器

    """

    data_dir = args.data_dir

    datalist_json = args.json_list

  

    # 获取训练和验证数据

    train_files = datafold_read(datalist=datalist_json, basedir=data_dir, key="training")

    validation_files = datafold_read(datalist=datalist_json, basedir=data_dir, key="validation")

  

    # 定义训练和验证转换

    train_transform = Compose(

        [

            LoadImaged(keys=["image", "label"]),

            EnsureChannelFirstd(keys="image"),

            EnsureTyped(keys=["image", "label"]),

            ConvertToMultiChannelBasedOnBrats25Classesd(keys="label"),

            Orientationd(keys=["image", "label"], axcodes="RAS"),

            Spacingd(keys=["image", "label"], pixdim=(1.0, 1.0, 1.0), mode=("bilinear", "nearest")),

            RandSpatialCropd(keys=["image", "label"], roi_size=[args.roi_x, args.roi_y, args.roi_z], random_size=False),

            RandFlipd(keys=["image", "label"], prob=0.5, spatial_axis=0),

            RandFlipd(keys=["image", "label"], prob=0.5, spatial_axis=1),

            RandFlipd(keys=["image", "label"], prob=0.5, spatial_axis=2),

            NormalizeIntensityd(keys="image", nonzero=True, channel_wise=True),

            RandScaleIntensityd(keys="image", factors=0.1, prob=1.0),

            RandShiftIntensityd(keys="image", offsets=0.1, prob=1.0),

        ]

    )

    val_transform = Compose(

        [

            LoadImaged(keys=["image", "label"]),

            EnsureChannelFirstd(keys="image"),

            EnsureTyped(keys=["image", "label"]),

            ConvertToMultiChannelBasedOnBrats25Classesd(keys="label"),

            Orientationd(keys=["image", "label"], axcodes="RAS"),

            Spacingd(keys=["image", "label"], pixdim=(1.0, 1.0, 1.0), mode=("bilinear", "nearest")),

            NormalizeIntensityd(keys="image", nonzero=True, channel_wise=True),

        ]

    )

  

    # 使用 CacheDataset 并设置缓存比例为 1

    train_ds = CacheDataset(

        data=train_files,

        transform=train_transform,

        cache_rate=0.1,

        num_workers=8

    )

    val_ds = CacheDataset(

        data=validation_files,

        transform=val_transform,

        cache_rate=0.1,

        num_workers=8

    )

  

    # 创建数据加载器并启用 persistent_workers

    train_loader = DataLoader(

        train_ds,

        batch_size=args.batch_size,

        shuffle=True,

        num_workers=4,  # 设置为4个worker

        persistent_workers=True

    )

    val_loader = DataLoader(

        val_ds,

        batch_size=args.batch_size,

        shuffle=False,

        num_workers=4,  # 设置为4个worker

        persistent_workers=True

)

  

    print(f"Number of training files: {len(train_files)}")

    print(f"Number of validation files: {len(validation_files)}")

  

    return train_loader, val_loader

  
  

def get_test_loader(args):

    """

    获取测试数据加载器。

    :param args: 参数对象,包含数据目录、数据列表文件等信息

    :return: 测试数据加载器

    """

    data_dir = args.data_dir

    datalist_json = args.json_list

    test_files = datafold_read(datalist=datalist_json, basedir=data_dir, key="test")

  

    test_transform = Compose(

        [

            LoadImaged(keys=["image", "label"]),

            EnsureChannelFirstd(keys="image"),

            EnsureTyped(keys=["image", "label"]),

            ConvertToMultiChannelBasedOnBrats25Classesd(keys="label"),

            Orientationd(keys=["image", "label"], axcodes="RAS"),

            Spacingd(keys=["image", "label"], pixdim=(1.0, 1.0, 1.0), mode=("bilinear", "nearest")),

            NormalizeIntensityd(keys="image", nonzero=True, channel_wise=True),

        ]

    )

  

    test_ds = CacheDataset(

        data=test_files,

        transform=test_transform,

        cache_rate=1.0,

        num_workers=8

    )

    test_loader = DataLoader(

        test_ds,

        batch_size=args.batch_size,

        shuffle=False,

        num_workers=args.num_workers,

        persistent_workers=True

    )

  

    return test_loader

  
  

def get_post_transforms():

    """

    获取后处理转换。

    :return: 后处理转换

    """

    return Compose(

        [

            Activationsd(keys="pred", softmax=True),

            AsDiscreted(keys="pred", threshold=0.5),

        ]

    )

Main

# 导入必要的库和模块

import argparse

import os

import torch

from torch.cuda.amp import GradScaler

from torch.optim.lr_scheduler import CosineAnnealingLR

import time

from monai.losses import DiceLoss, DiceCELoss

from monai.inferers import sliding_window_inference

from monai.metrics import DiceMetric

from monai.networks.nets import SwinUNETR

import matplotlib.pyplot as plt

from monai.transforms import Compose, EnsureTyped, Activations, AsDiscrete, MapTransform

from monai.data import decollate_batch

  

from dataloader import get_loader, get_post_transforms

  
  

# 定义命令行参数解析器

parser = argparse.ArgumentParser(description="Training")

parser.add_argument("--data_dir", default="/data/coding/TEST", type=str, help="dataset directory")

parser.add_argument("--json_list", default="/data/coding/research-contributions-main/UNETR/BTCV/dataset_monai_cleaned.json", type=str, help="dataset json file")

parser.add_argument("--fold", default=0, type=int, help="fold number")

parser.add_argument("--batch_size", default=1, type=int, help="batch size")

parser.add_argument("--num_workers", default=4, type=int, help="number of workers")

parser.add_argument("--roi_x", default=128, type=int, help="roi size in x direction")

parser.add_argument("--roi_y", default=128, type=int, help="roi size in y direction")

parser.add_argument("--roi_z", default=128, type=int, help="roi size in z direction")

parser.add_argument("--in_channels", default=4, type=int, help="number of input channels")

parser.add_argument("--out_channels", default=4, type=int, help="number of output channels")

parser.add_argument("--optimizer", default="adamw", type=str, help="optimizer to use (adamw or adam)")

parser.add_argument("--loss", default="dicece", type=str, help="loss function to use (dicece or diceloss)")

parser.add_argument("--lr", default=2e-4, type=float, help="learning rate")

parser.add_argument("--weight_decay", default=1e-5, type=float, help="weight decay for optimizer")

parser.add_argument("--save_dir", default="/data/coding/TEST", type=str, help="directory to save checkpoints and plots")

parser.add_argument("--save_checkpoint_interval", default=10, type=int, help="interval to save checkpoints")

parser.add_argument("--max_epochs", default=100, type=int, help="maximum number of training epochs")

args = parser.parse_args()

  
  

# 定义训练函数

def train(args):

    """

    执行模型训练过程,包括数据加载、模型初始化、训练循环、验证和指标计算。

    """

    global model, optimizer, lr_scheduler, scaler, dice_metric, dice_metric_batch, post_trans, best_metric, best_metric_epoch, best_metrics_epochs_and_time, epoch_loss_values, metric_values, VAL_AMP, metric_values_tc, metric_values_wt, metric_values_et, metric_values_other

  

    # 初始化变量和对象

    max_epochs = args.max_epochs

    val_interval = 5

    VAL_AMP = False

  

    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

  

    # 定义 SwinUNETR 模型

    model = SwinUNETR(

        img_size=(args.roi_x, args.roi_y, args.roi_z),

        in_channels=args.in_channels,

        out_channels=args.out_channels,

        feature_size=48,

        use_checkpoint=False,

    ).to(device)

  

    # 初始化损失函数

    if args.loss == "dicece":

        loss_function = DiceCELoss(include_background=True, softmax=True)

    else:

        loss_function = DiceLoss(smooth_nr=0, smooth_dr=1e-5, squared_pred=True, to_onehot_y=False, softmax=True)

  

    # 初始化优化器

    if args.optimizer == "adamw":

        optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)

    else:

        optimizer = torch.optim.Adam(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)

  

    # 定义余弦退火学习率调度器

    lr_scheduler = CosineAnnealingLR(optimizer, T_max=max_epochs)

  

    from torch.amp import GradScaler

    scaler = GradScaler('cuda')

    torch.backends.cudnn.benchmark = True

  

    dice_metric = DiceMetric(include_background=True, reduction="mean")

    dice_metric_batch = DiceMetric(include_background=True, reduction="mean_batch")

  

    post_trans = Compose([Activations(softmax=True), AsDiscrete(to_onehot=None, threshold=0.5)])

  

    best_metric = -1

    best_metric_epoch = -1

    best_metrics_epochs_and_time = [[], [], []]

    epoch_loss_values = []

    metric_values = []

    metric_values_tc = []

    metric_values_wt = []

    metric_values_et = []

    metric_values_other = []

  

    train_loader, val_loader = get_loader(args)

  

    total_start = time.time()

    for epoch in range(max_epochs):

        epoch_start = time.time()

        print("-" * 10)

        print(f"epoch {epoch + 1}/{max_epochs}")

        model.train()

        epoch_loss = 0

        step = 0

        for batch_data in train_loader:

            step_start = time.time()

            step += 1

            inputs, labels = (

                batch_data["image"].to(device),

                batch_data["label"].to(device),

            )

            optimizer.zero_grad()

            with torch.autocast("cuda"):

                outputs = model(inputs)

                loss = loss_function(outputs, labels)

            scaler.scale(loss).backward()

            scaler.step(optimizer)

            scaler.update()

            epoch_loss += loss.item()

            print(

                f"{step}/{len(train_loader.dataset) // train_loader.batch_size} "

                f"train_loss: {loss.item():.4f} "

                f"step time: {(time.time() - step_start):.4f}"

            )

        lr_scheduler.step()

        epoch_loss /= step

        epoch_loss_values.append(epoch_loss)

        print(f"epoch {epoch + 1} average loss: {epoch_loss:.4f}")

  

        if (epoch + 1) % val_interval == 0:

            model.eval()

            with torch.no_grad():

                all_iou = []

                all_dice = []

                for val_data in val_loader:

                    val_inputs, val_labels = (

                        val_data["image"].to(device),

                        val_data["label"].to(device),

                    )

                    val_outputs = sliding_window_inference(

                        inputs=val_inputs,

                        roi_size=(args.roi_x, args.roi_y, args.roi_z),

                        sw_batch_size=1,

                        predictor=model,

                        overlap=0.5,

                    )

                    val_outputs = [post_trans(i) for i in decollate_batch(val_outputs)]

                    dice_metric(y_pred=val_outputs, y=val_labels)

                    dice_metric_batch(y_pred=val_outputs, y=val_labels)

  

                    # 计算自定义的 IOU 和 Dice

                    val_outputs = torch.cat(val_outputs, dim=0)

                    val_labels = torch.cat([val_label.unsqueeze(0) for val_label in val_labels], dim=0)

                    iou, dice = calculate_metrics(val_outputs, val_labels)

                    all_iou.append(iou)

                    all_dice.append(dice)

  

                # 计算平均 IOU 和 Dice

                avg_iou = torch.tensor(all_iou).mean(dim=0)

                avg_dice = torch.tensor(all_dice).mean(dim=0)

  

                # 处理类别不平衡问题

                first_non_zero = None

                for i in range(avg_iou.shape[0]):

                    if avg_iou[i] != 0:

                        first_non_zero = i

                        break

                if first_non_zero is not None:

                    for i in range(first_non_zero, avg_iou.shape[0]):

                        if avg_iou[i] == 0:

                            avg_iou[i] = 1.0

                            avg_dice[i] = 1.0

  

                metric = dice_metric.aggregate().item()

                metric_values.append(metric)

                metric_batch = dice_metric_batch.aggregate()

                metric_tc = metric_batch[0].item()

                metric_values_tc.append(metric_tc)

                metric_wt = metric_batch[1].item()

                metric_values_wt.append(metric_wt)

                metric_et = metric_batch[2].item()

                metric_values_et.append(metric_et)

                metric_other = metric_batch[3].item()

                metric_values_other.append(metric_other)

                dice_metric.reset()

                dice_metric_batch.reset()

  

                if metric > best_metric:

                    best_metric = metric

                    best_metric_epoch = epoch + 1

                    best_metrics_epochs_and_time[0].append(best_metric)

                    best_metrics_epochs_and_time[1].append(best_metric_epoch)

                    best_metrics_epochs_and_time[2].append(time.time() - total_start)

                    torch.save(

                        model.state_dict(),

                        os.path.join(args.save_dir, "best_metric_model.pth"),

                    )

                    print("saved new best metric model")

  

                if (epoch + 1) % args.save_checkpoint_interval == 0:

                    torch.save(

                        {

                            "epoch": epoch + 1,

                            "model_state_dict": model.state_dict(),

                            "optimizer_state_dict": optimizer.state_dict(),

                            "lr_scheduler_state_dict": lr_scheduler.state_dict(),

                            "best_metric": best_metric,

                            "best_metric_epoch": best_metric_epoch,

                        },

                        os.path.join(args.save_dir, f"checkpoint_epoch_{epoch + 1}.pth"),

                    )

                    print(f"saved checkpoint for epoch {epoch + 1}")

  

                print(

                    f"current epoch: {epoch + 1} current mean dice: {metric:.4f} "

                    f"tc: {metric_tc:.4f} wt: {metric_wt:.4f} et: {metric_et:.4f} other: {metric_other:.4f} "

                    f"\nbest mean dice: {best_metric:.4f} at epoch: {best_metric_epoch}"

                )

  

                print(f"custom iou metric: tensor({avg_iou.tolist()})")

                print(f"custom Dice metric: tensor({avg_dice.tolist()})")

  

        print(f"time consuming of epoch {epoch + 1} is: {(time.time() - epoch_start):.4f}")

  

    total_time = time.time() - total_start

  

    print(f"train completed, best_metric: {best_metric:.4f} at epoch: {best_metric_epoch}, total time: {total_time}.")

  

    torch.save(

        model.state_dict(),

        os.path.join(args.save_dir, "final_model.pth"),

    )

    print("saved final model")

  

    training_metrics = {

        "epoch_loss_values": epoch_loss_values,

        "metric_values": metric_values,

        "metric_values_tc": metric_values_tc,

        "metric_values_wt": metric_values_wt,

        "metric_values_et": metric_values_et,

        "metric_values_other": metric_values_other,

    }

    torch.save(

        training_metrics,

        os.path.join(args.save_dir, "training_metrics.pth"),

    )

    print("saved training metrics")

  

    try:

        plt.figure("train", (12, 6))

        plt.subplot(1, 2, 1)

        plt.title("Epoch Average Loss")

        x = [i + 1 for i in range(len(epoch_loss_values))]

        y = epoch_loss_values

        plt.xlabel("epoch")

        plt.plot(x, y, color="red")

        plt.subplot(1, 2, 2)

        plt.title("Val Mean Dice")

        x = [val_interval * (i + 1) for i in range(len(metric_values))]

        y = metric_values

        plt.xlabel("epoch")

        plt.plot(x, y, color="green")

        plt.savefig(os.path.join(args.save_dir, "training_curves.png"))

        plt.close()

  

        plt.figure("train", (18, 6))

        plt.subplot(1, 3, 1)

        plt.title("Val Mean Dice TC")

        x = [val_interval * (i + 1) for i in range(len(metric_values_tc))]

        y = metric_values_tc

        plt.xlabel("epoch")

        plt.plot(x, y, color="blue")

        plt.subplot(1, 3, 2)

        plt.title("Val Mean Dice WT")

        x = [val_interval * (i + 1) for i in range(len(metric_values_wt))]

        y = metric_values_wt

        plt.xlabel("epoch")

        plt.plot(x, y, color="brown")

        plt.subplot(1, 3, 3)

        plt.title("Val Mean Dice ET")

        x = [val_interval * (i + 1) for i in range(len(metric_values_et))]

        y = metric_values_et

        plt.xlabel("epoch")

        plt.plot(x, y, color="purple")

        plt.savefig(os.path.join(args.save_dir, "validation_curves.png"))

        plt.close()

    except Exception as e:

        print(f"Warning: Failed to save plots. Error: {e}")

  
  

# 定义指标计算函数

def calculate_metrics(y_pred, y):

    """

    计算每个标签的 IOU 和 Dice 指标。

    """

    epsilon = 1e-6

    iou = []

    dice = []

    for i in range(y.shape[1]):

        pred = y_pred[:, i, :, :, :]

        label = y[:, i, :, :, :]

        intersection = torch.sum(pred * label)

        union = torch.sum(pred) + torch.sum(label)

        iou.append((intersection / (union - intersection + epsilon)).item())

        dice.append((2. * intersection / (torch.sum(pred) + torch.sum(label) + epsilon)).item())

    return iou, dice

  
  

if __name__ == "__main__":

    train(args)

Evaluate

# 导入必要的库和模块

import argparse

import os

import torch

from monai.inferers import sliding_window_inference

from monai.metrics import DiceMetric

from monai.networks.nets import SwinUNETR  

from monai.transforms import Compose, EnsureTyped, from_engine

  

from dataloader import get_test_loader, get_post_transforms

  
  

# 定义一个函数,用于评估模型

def evaluate(args):

    """

    评估模型。

    :param args: 参数对象,包含数据目录、数据列表文件等信息

    """

    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

    # 初始化模型

    model = SwinUNETR(

        img_size=(args.roi_x, args.roi_y, args.roi_z),

        in_channels=args.in_channels,

        out_channels=args.out_channels,

        feature_size=48,

        use_checkpoint=False,

    ).to(device)

    # 加载模型权重

    model.load_state_dict(torch.load(os.path.join(args.data_dir, "best_metric_model.pth"), map_location=device))

    model.eval()

  

    # 获取测试数据加载器

    test_loader = get_test_loader(args)

  

    # 初始化Dice指标

    dice_metric = DiceMetric(include_background=True, reduction="mean")

    dice_metric_batch = DiceMetric(include_background=True, reduction="mean_batch")

  

    # 获取后处理转换

    post_trans = get_post_transforms()

  

    # 定义推理函数

    def inference(input):

        return sliding_window_inference(

            inputs=input,

            roi_size=(args.roi_x, args.roi_y, args.roi_z),

            sw_batch_size=1,

            predictor=model,

            overlap=0.5,

        )

  

    # 执行推理和评估

    with torch.no_grad():

        for test_data in test_loader:

            test_inputs = test_data["image"].to(device)

            test_data["pred"] = inference(test_inputs)

            test_data = [post_trans(i) for i in decollate_batch(test_data)]

            test_outputs, test_labels = from_engine(["pred", "label"])(test_data)

            dice_metric(y_pred=test_outputs, y=test_labels)

            dice_metric_batch(y_pred=test_outputs, y=test_labels)

  

        metric_org = dice_metric.aggregate().item()

        metric_batch_org = dice_metric_batch.aggregate()

  

        dice_metric.reset()

        dice_metric_batch.reset()

  

    # 假设 metric_batch_org 是一个长度为4的列表,分别对应四个类别的指标

    metric_tc = metric_batch_org[0].item()  # 类别1

    metric_wt = metric_batch_org[1].item()  # 类别2

    metric_et = metric_batch_org[2].item()  # 类别3

    metric_other = metric_batch_org[3].item()  # 类别4

  

    print("Metric on original image spacing: ", metric_org)

    print(f"metric_tc: {metric_tc:.4f}")

    print(f"metric_wt: {metric_wt:.4f}")

    print(f"metric_et: {metric_et:.4f}")

    print(f"metric_other: {metric_other:.4f}")

  
  

if __name__ == "__main__":

    # 定义命令行参数解析器

    parser = argparse.ArgumentParser(description="Evaluation")

    parser.add_argument("--data_dir", default="/data/coding/TEST", type=str, help="dataset directory")

    parser.add_argument("--json_list", default="/data/coding/research-contributions-main/UNETR/BTCV/dataset_monai_cleaned.json", type=str, help="dataset json file")

    parser.add_argument("--fold", default=0, type=int, help="fold number")

    parser.add_argument("--batch_size", default=1, type=int, help="batch size")

    parser.add_argument("--num_workers", default=4, type=int, help="number of workers")

    parser.add_argument("--roi_x", default=128, type=int, help="roi size in x direction")

    parser.add_argument("--roi_y", default=128, type=int, help="roi size in y direction")

    parser.add_argument("--roi_z", default=128, type=int, help="roi size in z direction")

    parser.add_argument("--in_channels", default=4, type=int, help="number of input channels")

    parser.add_argument("--out_channels", default=4, type=int, help="number of output channels")

    args = parser.parse_args()

  

    evaluate(args)