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])
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
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