Experiment for Baseline Model¶

In [2]:
import pandas as pd
import matplotlib.pyplot as plt
from PIL import Image
from sklearn.preprocessing import LabelEncoder
from sklearn.feature_selection import mutual_info_classif
import random
import os
import seaborn as sns

df = pd.read_csv("../data/HAM10000/HAM10000_metadata.csv")

image_dir1 = "../data/HAM10000/HAM10000_images_part_1"
image_dir2 = "../data/HAM10000/HAM10000_images_part_2"
Matplotlib is building the font cache; this may take a moment.
In [3]:
sample = df.sample(1)

row = sample.iloc[0]
print(row)


label_map = {
    "akiec":0,
    "bcc":1,
    "bkl":2,
    "df":3,
    "mel":4,
    "nv":5,
    "vasc":6
}

label = label_map[row["dx"]]

print("Disease Label:",label)
lesion_id        HAM_0004083
image_id        ISIC_0032652
dx                       bcc
dx_type                histo
age                     60.0
sex                   female
localization            face
dataset         vidir_modern
Name: 2618, dtype: object
Disease Label: 1
In [4]:
import torch
from torch.utils.data import Dataset

from torchvision import transforms

from PIL import Image

import os
In [5]:
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor()
])
In [6]:
label_map = {
    "akiec":0,
    "bcc":1,
    "bkl":2,
    "df":3,
    "mel":4,
    "nv":5,
    "vasc":6
}
In [7]:
class HAMDataset(Dataset):

    def __init__(
        self,
        dataframe,
        image_dir1,
        image_dir2,
        transform=None
    ):

        self.df = dataframe

        self.image_dir1 = image_dir1
        self.image_dir2 = image_dir2

        self.transform = transform

    def __len__(self):

        return len(self.df)

    def __getitem__(self, idx):

        row = self.df.iloc[idx]

        image_id = row["image_id"]

        filename = image_id + ".jpg"

        path1 = os.path.join(
            self.image_dir1,
            filename
        )

        path2 = os.path.join(
            self.image_dir2,
            filename
        )

        if os.path.exists(path1):
            image_path = path1
        else:
            image_path = path2

        image = Image.open(image_path).convert("RGB")

        if self.transform:
            image = self.transform(image)

        label = label_map[row["dx"]]

        return image, label
In [8]:
dataset = HAMDataset(
    df,
    image_dir1,
    image_dir2,
    transform
)
In [9]:
image, label = dataset[0]

print(image.shape)

print(label)#一次取一张图片
torch.Size([3, 224, 224])
2
In [10]:
from torch.utils.data import DataLoader

data_loader = DataLoader(
    dataset,
    batch_size=16,
    shuffle=True,
    num_workers=0
)
In [11]:
import matplotlib.pyplot as plt

images, labels = next(iter(data_loader))

print("Images shape:", images.shape)
print("Labels shape:", labels.shape)
print("Labels:", labels)

idx_to_class = {
    value: key
    for key, value in label_map.items()
}

fig, axes = plt.subplots(2, 4, figsize=(12, 6))

for i, ax in enumerate(axes.flatten()):
    image = images[i].permute(1, 2, 0)
    label_number = labels[i].item()
    disease_name = idx_to_class[label_number]

    ax.imshow(image)
    ax.set_title(disease_name)
    ax.axis("off")

plt.tight_layout()
plt.show()
Images shape: torch.Size([16, 3, 224, 224])
Labels shape: torch.Size([16])
Labels: tensor([5, 5, 5, 5, 5, 0, 5, 0, 5, 5, 5, 5, 0, 5, 5, 2])
No description has been provided for this image
In [12]:
print("总图片数:", len(df))

print("总病变数:", df["lesion_id"].nunique())
总图片数: 10015
总病变数: 7470
In [13]:
labels_per_lesion = df.groupby("lesion_id")["dx"].nunique()

print("一个病变对应多个疾病标签的数量:",
      (labels_per_lesion > 1).sum())
一个病变对应多个疾病标签的数量: 0
In [14]:
lesion_df = (
    df[["lesion_id", "dx"]]
    .drop_duplicates(subset="lesion_id")
    .reset_index(drop=True)
)

print(lesion_df.head())
print("独立病变数:", len(lesion_df))
     lesion_id   dx
0  HAM_0000118  bkl
1  HAM_0002730  bkl
2  HAM_0001466  bkl
3  HAM_0002761  bkl
4  HAM_0005132  bkl
独立病变数: 7470
In [15]:
from sklearn.model_selection import train_test_split

train_lesions, temp_lesions = train_test_split(
    lesion_df,
    test_size=0.30,
    random_state=42,
    stratify=lesion_df["dx"]
)
In [16]:
val_lesions, test_lesions = train_test_split(
    temp_lesions,
    test_size=0.50,
    random_state=42,
    stratify=temp_lesions["dx"]
)
In [17]:
print("训练病变数:", len(train_lesions))
print("验证病变数:", len(val_lesions))
print("测试病变数:", len(test_lesions))
print("总计:", len(train_lesions) + len(val_lesions) + len(test_lesions))
训练病变数: 5229
验证病变数: 1120
测试病变数: 1121
总计: 7470
In [18]:
train_ids = set(train_lesions["lesion_id"])
val_ids = set(val_lesions["lesion_id"])
test_ids = set(test_lesions["lesion_id"])

train_df = df[df["lesion_id"].isin(train_ids)].reset_index(drop=True)
val_df = df[df["lesion_id"].isin(val_ids)].reset_index(drop=True)
test_df = df[df["lesion_id"].isin(test_ids)].reset_index(drop=True)

print("训练图片数:", len(train_df))
print("验证图片数:", len(val_df))
print("测试图片数:", len(test_df))
print("总图片数:", len(train_df) + len(val_df) + len(test_df))
训练图片数: 7002
验证图片数: 1532
测试图片数: 1481
总图片数: 10015
In [19]:
train_val_overlap = train_ids & val_ids
train_test_overlap = train_ids & test_ids
val_test_overlap = val_ids & test_ids

print("训练集与验证集重复病变数:", len(train_val_overlap))
print("训练集与测试集重复病变数:", len(train_test_overlap))
print("验证集与测试集重复病变数:", len(val_test_overlap))
训练集与验证集重复病变数: 0
训练集与测试集重复病变数: 0
验证集与测试集重复病变数: 0
In [20]:
def show_class_distribution(name, data):
    print(f"\n{name}")
    print(data["dx"].value_counts())
    print("\n比例:")
    print(data["dx"].value_counts(normalize=True).round(4))


show_class_distribution("训练集", train_df)
show_class_distribution("验证集", val_df)
show_class_distribution("测试集", test_df)
训练集
dx
nv       4679
mel       778
bkl       774
bcc       366
akiec     230
vasc       99
df         76
Name: count, dtype: int64

比例:
dx
nv       0.6682
mel      0.1111
bkl      0.1105
bcc      0.0523
akiec    0.0328
vasc     0.0141
df       0.0109
Name: proportion, dtype: float64

验证集
dx
nv       1034
mel       170
bkl       157
bcc        77
akiec      51
vasc       24
df         19
Name: count, dtype: int64

比例:
dx
nv       0.6749
mel      0.1110
bkl      0.1025
bcc      0.0503
akiec    0.0333
vasc     0.0157
df       0.0124
Name: proportion, dtype: float64

测试集
dx
nv       992
bkl      168
mel      165
bcc       71
akiec     46
df        20
vasc      19
Name: count, dtype: int64

比例:
dx
nv       0.6698
bkl      0.1134
mel      0.1114
bcc      0.0479
akiec    0.0311
df       0.0135
vasc     0.0128
Name: proportion, dtype: float64
In [21]:
import os

split_dir = "../data/HAM10000/splits"
os.makedirs(split_dir, exist_ok=True)

train_df.to_csv(
    os.path.join(split_dir, "train.csv"),
    index=False
)

val_df.to_csv(
    os.path.join(split_dir, "val.csv"),
    index=False
)

test_df.to_csv(
    os.path.join(split_dir, "test.csv"),
    index=False
)

print("数据划分已经保存。")
数据划分已经保存。
In [22]:
train_dataset = HAMDataset(
    train_df,
    image_dir1,
    image_dir2,
    transform
)

val_dataset = HAMDataset(
    val_df,
    image_dir1,
    image_dir2,
    transform
)

test_dataset = HAMDataset(
    test_df,
    image_dir1,
    image_dir2,
    transform
)

print(len(train_dataset))
print(len(val_dataset))
print(len(test_dataset))
7002
1532
1481
In [23]:
from torch.utils.data import DataLoader

train_loader = DataLoader(
    train_dataset,
    batch_size=16,
    shuffle=True,
    num_workers=0
)

val_loader = DataLoader(
    val_dataset,
    batch_size=16,
    shuffle=False,
    num_workers=0
)

test_loader = DataLoader(
    test_dataset,
    batch_size=16,
    shuffle=False,
    num_workers=0
)
In [24]:
images, labels = next(iter(train_loader))

print(images.shape)
print(labels.shape)
torch.Size([16, 3, 224, 224])
torch.Size([16])
In [25]:
import torch
import torch.nn as nn

from torchvision.models import resnet50
In [26]:
model = resnet50(weights="DEFAULT")
In [27]:
print(model.fc)
Linear(in_features=2048, out_features=1000, bias=True)
In [28]:
model.fc = nn.Linear(
    model.fc.in_features,
    7
)
print(model.fc)
Linear(in_features=2048, out_features=7, bias=True)
In [29]:
device = torch.device(
    "mps" if torch.mps.is_available()
    else "cpu"
)

print(device)
mps
In [30]:
model = model.to(device)
In [31]:
criterion = nn.CrossEntropyLoss()
In [32]:
optimizer = torch.optim.Adam(
    model.parameters(),
    lr=1e-4
)
In [33]:
images, labels = next(iter(train_loader))

images = images.to(device)
labels = labels.to(device)

outputs = model(images)

print(outputs.shape)
torch.Size([16, 7])
In [34]:
train_transform = transforms.Compose([
    transforms.Resize((224, 224)),

    # 训练时增加轻量随机增强
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomRotation(10),

    transforms.ToTensor(),

    transforms.Normalize(
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225]
    )
])

val_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),

    transforms.Normalize(
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225]
    )
])

train_dataset = HAMDataset(
    train_df,
    image_dir1,
    image_dir2,
    train_transform
)

val_dataset = HAMDataset(
    val_df,
    image_dir1,
    image_dir2,
    val_transform
)

test_dataset = HAMDataset(
    test_df,
    image_dir1,
    image_dir2,
    val_transform
)

train_loader = DataLoader(
    train_dataset,
    batch_size=8,
    shuffle=True,
    num_workers=0
)

val_loader = DataLoader(
    val_dataset,
    batch_size=8,
    shuffle=False,
    num_workers=0
)

test_loader = DataLoader(
    test_dataset,
    batch_size=8,
    shuffle=False,
    num_workers=0
)
In [35]:
for parameter in model.parameters():
    parameter.requires_grad = False
    
model.fc = nn.Linear(
    model.fc.in_features,
    7
)

model = model.to(device)
In [36]:
for name, parameter in model.named_parameters():
    if parameter.requires_grad:
        print(name)
fc.weight
fc.bias
In [37]:
import numpy as np
from sklearn.utils.class_weight import compute_class_weight

class_names = [
    "akiec",
    "bcc",
    "bkl",
    "df",
    "mel",
    "nv",
    "vasc"
]

class_weights = compute_class_weight(
    class_weight="balanced",
    classes=np.array(class_names),
    y=train_df["dx"]
)

class_weights = torch.tensor(
    class_weights,
    dtype=torch.float32
).to(device)

print(class_weights)
tensor([ 4.3491,  2.7330,  1.2924, 13.1617,  1.2857,  0.2138, 10.1039],
       device='mps:0')
In [38]:
criterion = nn.CrossEntropyLoss(
    weight=class_weights
)
In [40]:
optimizer = torch.optim.Adam(
    model.fc.parameters(),
    lr=1e-3
)
In [66]:
from tqdm.auto import tqdm

model.train()

running_loss = 0.0
correct = 0
total = 0

for images, labels in tqdm(
    train_loader,
    desc="Training"
):
    images = images.to(device)
    labels = labels.to(device)

    # 清空上一批数据留下的梯度
    optimizer.zero_grad()

    # 模型预测
    outputs = model(images)

    # 计算损失
    loss = criterion(outputs, labels)

    # 计算梯度
    loss.backward()

    # 更新最后一层参数
    optimizer.step()

    running_loss += loss.item() * images.size(0)

    predictions = outputs.argmax(dim=1)

    correct += (
        predictions == labels
    ).sum().item()

    total += labels.size(0)

train_loss = running_loss / total
train_accuracy = correct / total

print("Train Loss:", train_loss)
print("Train Accuracy:", train_accuracy)
Training:   0%|          | 0/876 [00:00<?, ?it/s]
Train Loss: 1.4447416241970514
Train Accuracy: 0.618966009711511
In [67]:
model.eval()

val_running_loss = 0.0
val_correct = 0
val_total = 0

with torch.no_grad():

    for images, labels in tqdm(
        val_loader,
        desc="Validation"
    ):
        images = images.to(device)
        labels = labels.to(device)

        outputs = model(images)
        loss = criterion(outputs, labels)

        val_running_loss += (
            loss.item() * images.size(0)
        )

        predictions = outputs.argmax(dim=1)

        val_correct += (
            predictions == labels
        ).sum().item()

        val_total += labels.size(0)

val_loss = val_running_loss / val_total
val_accuracy = val_correct / val_total

print("Validation Loss:", val_loss)
print("Validation Accuracy:", val_accuracy)
Validation:   0%|          | 0/192 [00:00<?, ?it/s]
Validation Loss: 1.1815234999276951
Validation Accuracy: 0.5535248041775457

The First Model was set up!¶

1 epoch!¶

5 epochs Start up!¶

In [47]:
from sklearn.metrics import (
    accuracy_score,
    balanced_accuracy_score,
    f1_score
)
import numpy as np
In [46]:
def evaluate_model(model, data_loader, criterion, device):
    model.eval()

    running_loss = 0.0
    all_labels = []
    all_predictions = []

    with torch.no_grad():
        for images, labels in data_loader:
            images = images.to(device)
            labels = labels.to(device)

            outputs = model(images)
            loss = criterion(outputs, labels)

            running_loss += loss.item() * images.size(0)

            predictions = outputs.argmax(dim=1)

            all_labels.extend(labels.cpu().numpy())
            all_predictions.extend(predictions.cpu().numpy())

    average_loss = running_loss / len(data_loader.dataset)

    accuracy = accuracy_score(
        all_labels,
        all_predictions
    )

    balanced_accuracy = balanced_accuracy_score(
        all_labels,
        all_predictions
    )

    macro_f1 = f1_score(
        all_labels,
        all_predictions,
        average="macro",
        zero_division=0
    )

    return {
        "loss": average_loss,
        "accuracy": accuracy,
        "balanced_accuracy": balanced_accuracy,
        "macro_f1": macro_f1
    }
In [45]:
def train_one_epoch(
    model,
    data_loader,
    criterion,
    optimizer,
    device
):
    model.train()

    running_loss = 0.0
    all_labels = []
    all_predictions = []

    for images, labels in data_loader:
        images = images.to(device)
        labels = labels.to(device)

        optimizer.zero_grad()

        outputs = model(images)
        loss = criterion(outputs, labels)

        loss.backward()
        optimizer.step()

        running_loss += loss.item() * images.size(0)

        predictions = outputs.argmax(dim=1)

        all_labels.extend(labels.detach().cpu().numpy())
        all_predictions.extend(
            predictions.detach().cpu().numpy()
        )

    average_loss = running_loss / len(data_loader.dataset)

    accuracy = accuracy_score(
        all_labels,
        all_predictions
    )

    balanced_accuracy = balanced_accuracy_score(
        all_labels,
        all_predictions
    )

    macro_f1 = f1_score(
        all_labels,
        all_predictions,
        average="macro",
        zero_division=0
    )

    return {
        "loss": average_loss,
        "accuracy": accuracy,
        "balanced_accuracy": balanced_accuracy,
        "macro_f1": macro_f1
    }
In [44]:
import os

checkpoint_dir = "../checkpoints"
os.makedirs(checkpoint_dir, exist_ok=True)

best_model_path = os.path.join(
    checkpoint_dir,
    "resnet50_image_only_best.pth"
)
In [43]:
from torchvision.models import (
    resnet50,
    ResNet50_Weights
)

weights = ResNet50_Weights.DEFAULT

model = resnet50(weights=weights)

for parameter in model.parameters():
    parameter.requires_grad = False

model.fc = nn.Linear(
    model.fc.in_features,
    7
)

model = model.to(device)

criterion = nn.CrossEntropyLoss(
    weight=class_weights
)

optimizer = torch.optim.Adam(
    model.fc.parameters(),
    lr=1e-3
)
In [73]:
num_epochs = 5
best_val_macro_f1 = -1.0

history = {
    "train_loss": [],
    "train_accuracy": [],
    "train_balanced_accuracy": [],
    "train_macro_f1": [],
    "val_loss": [],
    "val_accuracy": [],
    "val_balanced_accuracy": [],
    "val_macro_f1": []
}

for epoch in range(num_epochs):

    train_metrics = train_one_epoch(
        model,
        train_loader,
        criterion,
        optimizer,
        device
    )

    val_metrics = evaluate_model(
        model,
        val_loader,
        criterion,
        device
    )

    history["train_loss"].append(
        train_metrics["loss"]
    )
    history["train_accuracy"].append(
        train_metrics["accuracy"]
    )
    history["train_balanced_accuracy"].append(
        train_metrics["balanced_accuracy"]
    )
    history["train_macro_f1"].append(
        train_metrics["macro_f1"]
    )

    history["val_loss"].append(
        val_metrics["loss"]
    )
    history["val_accuracy"].append(
        val_metrics["accuracy"]
    )
    history["val_balanced_accuracy"].append(
        val_metrics["balanced_accuracy"]
    )
    history["val_macro_f1"].append(
        val_metrics["macro_f1"]
    )

    print(f"\nEpoch {epoch + 1}/{num_epochs}")

    print(
        f"Train Loss: {train_metrics['loss']:.4f} | "
        f"Accuracy: {train_metrics['accuracy']:.4f} | "
        f"Balanced Accuracy: "
        f"{train_metrics['balanced_accuracy']:.4f} | "
        f"Macro-F1: {train_metrics['macro_f1']:.4f}"
    )

    print(
        f"Val Loss: {val_metrics['loss']:.4f} | "
        f"Accuracy: {val_metrics['accuracy']:.4f} | "
        f"Balanced Accuracy: "
        f"{val_metrics['balanced_accuracy']:.4f} | "
        f"Macro-F1: {val_metrics['macro_f1']:.4f}"
    )

    if val_metrics["macro_f1"] > best_val_macro_f1:
        best_val_macro_f1 = val_metrics["macro_f1"]

        torch.save(
            model.state_dict(),
            best_model_path
        )

        print("已保存新的最佳模型。")

history_df = pd.DataFrame(history)

history_df.to_csv(
    "../outputs/resnet50_image_only_history.csv",
    index=False
)
Epoch 1/5
Train Loss: 1.4530 | Accuracy: 0.6101 | Balanced Accuracy: 0.3972 | Macro-F1: 0.3584
Val Loss: 0.8981 | Accuracy: 0.6645 | Balanced Accuracy: 0.4214 | Macro-F1: 0.3867
已保存新的最佳模型。

Epoch 2/5
Train Loss: 1.2129 | Accuracy: 0.6547 | Balanced Accuracy: 0.5150 | Macro-F1: 0.4477
Val Loss: 0.8713 | Accuracy: 0.6860 | Balanced Accuracy: 0.5173 | Macro-F1: 0.4561
已保存新的最佳模型。

Epoch 3/5
Train Loss: 1.1361 | Accuracy: 0.6672 | Balanced Accuracy: 0.5574 | Macro-F1: 0.4835
Val Loss: 0.9164 | Accuracy: 0.6527 | Balanced Accuracy: 0.5590 | Macro-F1: 0.4655
已保存新的最佳模型。

Epoch 4/5
Train Loss: 1.0502 | Accuracy: 0.6842 | Balanced Accuracy: 0.6096 | Macro-F1: 0.5187
Val Loss: 0.7969 | Accuracy: 0.7082 | Balanced Accuracy: 0.5303 | Macro-F1: 0.4929
已保存新的最佳模型。

Epoch 5/5
Train Loss: 0.9684 | Accuracy: 0.7047 | Balanced Accuracy: 0.6424 | Macro-F1: 0.5643
Val Loss: 0.7920 | Accuracy: 0.7063 | Balanced Accuracy: 0.5513 | Macro-F1: 0.5062
已保存新的最佳模型。

5 Epoches Model Was Set Done.¶

Test Part¶

In [48]:
model.load_state_dict(
    torch.load(
        best_model_path,
        map_location=device,
        weights_only=True
    )
)

model.eval()

print("最佳模型加载成功")
最佳模型加载成功
In [49]:
test_metrics = evaluate_model(
    model,
    test_loader,
    criterion,
    device
)

print("Test Loss:", test_metrics["loss"])
print("Test Accuracy:", test_metrics["accuracy"])
print(
    "Test Balanced Accuracy:",
    test_metrics["balanced_accuracy"]
)
print("Test Macro-F1:", test_metrics["macro_f1"])
Test Loss: 0.7729439726860277
Test Accuracy: 0.7137069547602971
Test Balanced Accuracy: 0.516905135913686
Test Macro-F1: 0.4624325854884104
In [50]:
all_test_labels = []
all_test_predictions = []
all_test_probabilities = []

model.eval()

with torch.no_grad():
    for images, labels in test_loader:
        images = images.to(device)

        outputs = model(images)
        probabilities = torch.softmax(outputs, dim=1)
        predictions = outputs.argmax(dim=1)

        all_test_labels.extend(labels.numpy())
        all_test_predictions.extend(
            predictions.cpu().numpy()
        )
        all_test_probabilities.extend(
            probabilities.cpu().numpy()
        )

all_test_labels = np.array(all_test_labels)
all_test_predictions = np.array(all_test_predictions)
all_test_probabilities = np.array(
    all_test_probabilities
)
In [52]:
from sklearn.metrics import classification_report

report = classification_report(
    all_test_labels,
    all_test_predictions,
    target_names=class_names,
    digits=4,
    zero_division=0
)

print(report)
              precision    recall  f1-score   support

       akiec     0.3333    0.6087    0.4308        46
         bcc     0.3495    0.5070    0.4138        71
         bkl     0.6977    0.3571    0.4724       168
          df     0.2308    0.1500    0.1818        20
         mel     0.3688    0.5879    0.4533       165
          nv     0.9154    0.8286    0.8698       992
        vasc     0.3235    0.5789    0.4151        19

    accuracy                         0.7137      1481
   macro avg     0.4599    0.5169    0.4624      1481
weighted avg     0.7677    0.7137    0.7277      1481

In [53]:
model.load_state_dict(
    torch.load(
        best_model_path,
        map_location=device,
        weights_only=True
    )
)

model = model.to(device)
In [63]:
model.load_state_dict(
    torch.load(
        best_model_path,
        map_location=device,
        weights_only=True
    )
)

model = model.to(device)
In [68]:
for parameter in model.parameters():
    parameter.requires_grad = False

for parameter in model.fc.parameters():
    parameter.requires_grad = True
In [69]:
optimizer = torch.optim.Adam(
    model.fc.parameters(),
    lr=3e-4
)
In [70]:
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
    optimizer,
    mode="max",
    factor=0.5,
    patience=2
)
In [71]:
additional_epochs = 15
early_stopping_patience = 4

best_val_macro_f1 = 0.5062
epochs_without_improvement = 0
In [1]:
for epoch in range(additional_epochs):

    train_metrics = train_one_epoch(
        model,
        train_loader,
        criterion,
        optimizer,
        device
    )

    val_metrics = evaluate_model(
        model,
        val_loader,
        criterion,
        device
    )

    current_lr = optimizer.param_groups[0]["lr"]

    print(f"\nAdditional Epoch {epoch + 1}/{additional_epochs}")

    print(
        f"Train Loss: {train_metrics['loss']:.4f} | "
        f"Accuracy: {train_metrics['accuracy']:.4f} | "
        f"Balanced Accuracy: "
        f"{train_metrics['balanced_accuracy']:.4f} | "
        f"Macro-F1: {train_metrics['macro_f1']:.4f}"
    )

    print(
        f"Val Loss: {val_metrics['loss']:.4f} | "
        f"Accuracy: {val_metrics['accuracy']:.4f} | "
        f"Balanced Accuracy: "
        f"{val_metrics['balanced_accuracy']:.4f} | "
        f"Macro-F1: {val_metrics['macro_f1']:.4f}"
    )

    print(f"Learning Rate: {current_lr:.6f}")

    scheduler.step(
        val_metrics["macro_f1"]
    )

    if val_metrics["macro_f1"] > best_val_macro_f1:

        best_val_macro_f1 = val_metrics["macro_f1"]
        epochs_without_improvement = 0

        torch.save(
            model.state_dict(),
            best_model_path
        )

        print("已保存新的最佳模型。")

    else:
        epochs_without_improvement += 1

        print(
            "验证集 Macro-F1 未提升,"
            f"已连续 {epochs_without_improvement} 轮。"
        )

    if epochs_without_improvement >= early_stopping_patience:
        print("触发 Early Stopping,停止训练。")
        break
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[1], line 1
----> 1 for epoch in range(additional_epochs):
      3     train_metrics = train_one_epoch(
      4         model,
      5         train_loader,
   (...)
      8         device
      9     )
     11     val_metrics = evaluate_model(
     12         model,
     13         val_loader,
     14         criterion,
     15         device
     16     )

NameError: name 'additional_epochs' is not defined