一、 数据预处理
数据预处理是深度学习建模预测流程中的第一步,它包括数据清洗、数据转换、特征选择等步骤。数据预处理的目标是将原始数据转换为神经网络可以接受的格式,并且尽可能地减少噪声和不相关的信息。
1.1 数据加载
torchvision.datasets.ImageFolder 是 PyTorch 中用于处理图像数据集的一个类,它主要用于加载将不同类别的图像分别放置在不同的子目录下的数据集结构。
root/class_1/image_1.jpg
root/class_1/image_2.jpg
...
root/class_2/image_1.jpg
root/class_2/image_2.jpg
...
ImageFolder 类会自动地将这种目录结构的图像数据加载并组织成 PyTorch 的 Dataset 对象。当创建了一个 ImageFolder 对象后,就可以通过索引的方式来获取每个图像的数据和对应的标签。
#导入 ImageFolder
from torchvision.datasets import ImageFolder
#创建 ImageFolder 对象,并指定数据集的路径
dataset = ImageFolder(filepath, transform=None)
#可以通过索引访问数据集中第一个样本的图像数据和标签
image, label = dataset[0]
1.2 数据划分与数据转换
torch.utils.data.random_split 用于随机分割一个数据集,生成训练集,验证集,测试集。
transforms 用于数据转换,即对图像进行预处理或增强。
# 随机分割数据集
train_size = int(0.8 * len(dataset))
test_size = len(dataset) - train_size
train_dataset, test_dataset = torch.utils.data.random_split(dataset, [train_size, test_size])
# 对训练集和测试集应用不同的转换
# 定义训练集的数据增强
train_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize([0.17666], [0.2539]).float(),
transforms.Resize([600, 600]),
transforms.RandomVerticalFlip(p=0.5),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomRotation(25),
transforms.RandomAutocontrast(p=0.8),
transforms.Grayscale(num_output_channels=3),])
# 定义测试集的数据转换(这里无需增强)
test_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Resize([600, 600]),
transforms.Grayscale(num_output_channels=3),])
train_dataset.dataset.transform = train_transform
test_dataset.dataset.transform = test_transform
1.3 数据访问
torch.utils.data import DataLoader 用于将数据集加载到批处理中。
#创建数据加载器,用于批量加载数据
train_dataloader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=8)
test_dataloader = DataLoader(test_dataset, batch_size=32, shuffle=True, num_workers=8)
二、模型训练
2.1 搭建模型
torch.nn 是 PyTorch 中用于构建神经网络的模块。它提供了一组类和函数,用于定义、训练和评估神经网络模型。torch.nn 模块的核心是 nn.Module 类,它是所有神经网络模型的基类。通过继承 nn.Module 类,并定义 forward 方法,您可以创建自己的神经网络模型,并定义模型的结构和操作。
2.1.1 使用 nn.Sequential
对于相对简单的网络,可以使用 torch.nn.Sequential 来快速定义一个连续的层序列。这种方法简洁明了,非常适合没有复杂分支或自定义操作的网络。 torch.nn.Sequential 的详细用法见链接torch.nn.Sequential 用法介绍
2.1.2 继承 nn.Module(可搭配nn.Sequential使用)
对于更复杂的网络,或者当需要更多的自定义行为时,可以通过继承 nn.Module,或是搭配nn.Sequential,并定义 forward 方法来创建自己的模型。这种方法提供了更多的灵活性。
注意:1.构造函数主要用于定义参数,forward函数表示层的前向传播逻辑。
2.注意forward()函数定义的是批数据的前向传播逻辑。只要在nn.Module的子类中定义了forward()函数,backward()函数就会被自动实现。
3.一般情况下,我们定义的参数是可以求导的,但是自定义操作如不可导,需要实现backward函数。
import torch.nn as nn
#定义模型
class CustomModule(nn.Module):
def __init__(self):
super(CustomModule, self).__init__()
self.Net = nn.Sequential(
nn.Conv2d(1, 20, 5),
nn.ReLU(),
nn.Conv2d(20, 64, 5),
nn.ReLU()
def forward(self, x):
return self.Net(x)
# __init__()函数只是用来定义层,但并没有将它们连接起来,forward()函数的作用就是将这些定义好的层连接成网络。
# 在 forward 方法中定义了数据如何通过网络流动。x = self.Net(x) 语句使得输入数据 x 通过之前定义的 nn.Sequential 中的所有层。这是模型的前向传播过程。
2.1.3 使用预训练模型
使用预训练的模型,如DenseNet,是深度学习中的一种常见做法,尤其是在计算机视觉领域。预训练模型是指已经在大型数据集(如 ImageNet)上训练过的模型,能够作为一个很好的特征提取器或作为新任务的起点。torchvision.models 提供了方便的接口来下载和使用这些预训练模型。
#定义一个继承自torch.nn.Module的自定义类 CustomDenseNet169
class CustomDenseNet169(nn.Module):
def __init__(self):
# 子类调用父类的构造函数
super(CustomDenseNet169, self).__init__()
# pretrained=True表示加载预训练参数,Fale表示只搭建模型框架随机初始化参数
self.densenet = models.densenet169(pretrained=True)
# 修改线性层
self.densenet.classifier = nn.Sequential(
nn.Linear(1664, 160),
nn.Linear(160, 1)
)
# 定义前向传播过程
def forward(self, x):
return self.densenet(x)
model = CustomDenseNet169().to(device)
#定义损失函数
loss_fn = nn.BCEWithLogitsLoss().to(device)
#定义优化器
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
2.2 训练模型
def model_train():
train_dataset, test_dataset = dataset_jpeg_INbreast.data_loader()
learning_rate = 1e-5
batch_size = 16#原32
epochs = 150
train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=8)
test_dataloader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True, num_workers=8)
model = CustomDenseNet169().to(device)
loss_fn = nn.BCEWithLogitsLoss().to(device)
# loss_fn = nn.CrossEntropyLoss().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
if not os.path.exists('./experiment_result'):
os.makedirs('./experiment_result')
for t in range(epochs):
print(f"Epoch {t + 1}\n-------------------------------")
train_loop(train_dataloader, model, loss_fn, optimizer)
test_loop(test_dataloader, model, loss_fn, t)
print("Done!")
2.2.2 搭建训练函数
训练函数主要包含以下几个部分:
optimizer.zero_grad()
该函数用于将梯度置零,确保每个batch的梯度计算是独立的,其避免由梯度累积所导致的参数更新不准确。因此,该函数要写在反向传播和梯度下降之前。
loss.backward()
该函数用于反向传播计算当前梯度。
optimizer.step()
该函数用于执行一次优化步骤,其利用loss.backward()计算得到的梯度,采用梯度下降法来更新参数的值。optimizer.step()放在每一个batch训练中,而不是epoch训练中,一次batch训练更新一次参数空间。
# 定义训练函数
def train_loop(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset)
train_loss = 0
num_batch = 0
for batch, (X, y) in enumerate(dataloader):
X, y = X.to(device), y.to(device)
y = y.unsqueeze(1)
pred = model(X)
loss = loss_fn(pred, y.float())
y = y.squeeze()
optimizer.zero_grad()
loss.backward()
optimizer.step()
loss, current = loss.item(), min(batch * dataloader.batch_size, size)
train_loss = train_loss + loss
print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]")
train_Loss_list.append(train_loss / size)
print(f"Train Error: \n Avg loss: {train_loss:>8f} \n")
2.2.3 搭建测试函数
def test_loop(dataloader, model, loss_fn, epoch):
size = len(dataloader.dataset)
num_batches = len(dataloader)
test_loss = 0
num_batch = 0
with torch.no_grad():
for X, y in dataloader:
X, y = X.to(device), y.to(device)
y = y.unsqueeze(1)
pred = model(X)
test_loss += loss_fn(pred, y.float()).item()
acc_y = y.cpu().detach().numpy()
acc_y = acc_y.astype(int)
acc_pred = pred.cpu().detach().numpy().round()
for i in range(len(acc_y)):
if acc_y[i] == acc_pred[i]:
correct = correct + 1
num_batch += 1
print(correct)
2.3 评估模型
图像分类是计算机视觉中的基础任务。对于单个标签分类的问题,模型评价指标主要有准确率 Accuracy,精确度 Precision,召回率 Recall,F-score,PR曲线,ROC和AUC。
在计算这些指标之前,需要先计算几个基本指标。这些指标是基于二分类的任务,也可以拓展到多分类。
TP(True Positive):被判定为正样本,事实上也是正样本,真阳。
TN(True Negative):被判定为负样本,事实上也是负样本,真阴。
FP(False Positive):被判定为正样本,但事实上是负样本,假阳。
FN(False Negative):被判定为负样本,但事实上是正样本,假阴。
判别是否为正例只需要设一个概率阈值T,预测概率大于阈值T的为正类,小于阈值T的为负类,默认就是0.5。
2.3.1 准确率 Accuracy
单标签分类任务中每一个样本都只有一个确定的类别,预测到该类别就是分类正确,没有预测到就是分类错误。因此,最直观的指标就是准确率
,
其表示所有样本都正确分类的概率。
2.3.2 精确度 Precision
如果只考虑正样本的指标,有两个很常用的指标,精确度和召回率。正样本精确率为
,
其表示被正确分类的阳性样本数与所有被分类为阳性的样本数之比。
2.3.3 召回率 Recall
,
其表示召回率表示被正确分类的阳性样本数与所有真实阳性样本数之比有多少样本被召回。通常召回率越高,精确度越低。根据不同的值可以绘制Recall-Precision曲线
其中,横轴就是Recall,纵轴就是Precision,曲线越接近右上角,说明其性能越好,可以用该曲线与坐标轴包围的面积来定量评估,值在0~1之间。
2.3.4 F1 score
有的时候关注的不仅仅是正样本的准确率,也关心其召回率,但是又不想用Accuracy来进行衡量,一个折中的指标是采用F1-score。F1-score是一个综合性能的指标,只有在召回率Recall和精确率Precision都高的情况下,F1-score才会很高此。
2.3.4 混淆矩阵
若想知道类别之间相互误分的情况,查看是否有特定的类别之间相互混淆,就可以用混淆矩阵画出分类的详细预测结果。对于包含多个类别的任务,混淆矩阵很清晰的反映出各类别之间的错分概率。对于一个包含20个类别的分类任务来说,混淆矩阵为20*20的矩阵,其中第i行第j列,表示第i类目标被分类为第j类的概率,越好的分类器对角线上的值更大,其他地方应该越小。
2.3.5 ROC曲线与AUC指标
2.3.1-2.3.4所介绍的准确率Accuracy,精确度Precision,召回率Recall,F1-score,混淆矩阵都只是一个单一的数值指标。如果想观察分类算法在不同的参数下的表现情况,可以使用ROC曲线,全称为receiver operating characteristic,即受试者工作特征曲线。
ROC曲线可以用于评价一个分类器在不同阈值下的表现情况。在ROC曲线中,每个点的横坐标是假阳性率 false positive rate(FPR),纵坐标是真阳性率 true positive rate(TPR),曲线描绘了分类器在True Positive和False Positive间的平衡,两个指标的计算如下:
,
其代表在所有真实的正样本中,预测结果为正的比例,其值越大越好。这个公式也正是Recall的公式
,
其代表在所有真实的负样本中,预测结果为正的比例,其值越小越好。
ROC曲线
(1)首先,明确计算AUC的时候,预测值y_pred一般都是[0, 1]的小数(代表预测为正样本的概率),真实值y_true为0或1。如果计算FPR和TPR,我们就需要知道预测的正负样本情况,但给的预测值是小数,如何划分预测的正负样本呢?答案是选取截断点。
(3)截断点是指区分正负预测结果的阈值。比如截断点=0.1,那就表示y_pred<0.1的为预测为负样本,y_pred>=0.1预测正样本。所以绘制AUC曲线需要不断移动“截断点”来得到所有的(FPR,TPR)点,然后把这些点用线段连起来就是ROC曲线了。
(4)截断点取值为 { +∞ , −∞ 和预测值的所有唯一值} 就够了。+∞表示所有都预测为负样本,(fpr, tpr) = (0, 0)。−∞表示所有都预测为正样本,(fpr, tpr) = (1, 1)。也就是这两个坐标点固定有,所以一般截断点默认取值就是预测值的所有唯一值。
注意:
点(0,0):FPR=TPR=0,分类器预测所有的样本都为负样本;
点(1,1):FPR=TPR=1,分类器预测所有的样本都为正样本;
点(0,1):FPR=0, TPR=1,此时FN=0且FP=0,所有的样本都正确分类;
点(1,0):FPR=1,TPR=0,此时TP=0且TN=0,最差分类器,避开了所有正确答案。
ROC曲线相对于PR曲线有个很好的特性。当测试集中的正负样本的分布变化的时候,ROC曲线能够保持不变,即对正负样本不均衡问题不敏感。比如负样本的数量增加到原来的10倍,TPR不受影响,FPR的各项也是成比例的增加,并不会有太大的变化。所以不均衡样本问题通常选用ROC作为评价标准。
ROC曲线越接近左上角,该分类器的性能越好,若一个分类器的ROC曲线完全包住另一个分类器,那么可以判断前者的性能更好。
AUC指标
如果想通过两条ROC曲线来定量评估两个分类器的性能,就可以使用AUC指标。AUC(Area Under Curve)为ROC曲线下的面积,它表示的就是一个概率,即随机挑选一个正样本和一个负样本,AUC表征的就是有多大的概率,分类器会对正样本给出的预测值高于负样本,当然前提是正样本的预测值的确应该高于负样本。AUC越大,说明分类器越可能把正样本排在前面,衡量的是一种排序的性能。
那么问题来了,ROC曲线下的面积怎么就能衡量分类器的排序能力?且听慢慢道来。
如果ROC面积越大,说明曲线越往左上角靠过去。那么对于任意截断点,(FPR,TPR)坐标点越往左上角(0,1)靠,说明FPR较小趋于0(根据定义得知,就是在所有真实负样本中,基本没有预测为正的样本),TRP较大趋于1(根据定义得知,也就是在所有真实正样本中,基本全都是预测为正的样本)。并且上述是对于任意截断点来说的,很明显,那就是分类器对正样本的打分基本要大于负样本的打分(一般预测值也叫打分),衡量的不就是排序能力嘛!