转载自B站视频:https://www.bilibili.com/video/BV1wk4y1B77W/?spm_id_from=333.788
# PyTorch肺部感染识别1.0版本 作者源代码
# 导入必要的库
import torch
import torch.nn as nn
import numpy as np
import torch.optim as optim
from torchvision import transforms, datasets, models, utils
from torchsummary import summary # 可视化训练过程
from torch.utils.data import DataLoader
import time
import matplotlib.pyplot as plt
import os
import seaborn as sns
import pandas as pd
from mlxtend.plotting import plot_confusion_matrix
from sklearn.metrics import confusion_matrix
from PIL import Image
# 分为为train, val, test定义transform
image_transforms = {
'train': transforms.Compose([
transforms.RandomResizedCrop(size=300, scale=(0.8, 1.1)), # 功能:随机长宽比裁剪原始图片, 表示随机crop出来的图片会在的0.08倍至1.1倍之间
transforms.RandomRotation(degrees=10), # 功能:根据degrees随机旋转一定角度, 则表示在(-10,+10)度之间随机旋转
transforms.ColorJitter(0.4, 0.4, 0.4), # 功能:修改亮度、对比度和饱和度
transforms.RandomHorizontalFlip(), # 功能:水平翻转
transforms.CenterCrop(size=256), # 功能:根据给定的size从中心裁剪,size - 若为sequence,则为(h,w),若为int,则(size,size)
transforms.ToTensor(), # numpy --> tensor
# 功能:对数据按通道进行标准化(RGB),即先减均值,再除以标准差
transforms.Normalize([0.485, 0.456, 0.406], # mean
[0.229, 0.224, 0.225]) # std
]),
'val': transforms.Compose([
transforms.Resize(300),
transforms.CenterCrop(256),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], # mean
[0.229, 0.224, 0.225]) # std
]),
'test': transforms.Compose([
transforms.Resize(300),
transforms.CenterCrop(256),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], # mean
[0.229, 0.224, 0.225]) # std
])
}
# 加载数据集
# 数据集所在目录路径
data_dir = './chest_xray/'
# train路径
train_dir = data_dir + 'train/'
# val路径
val_dir = data_dir + 'val/'
# test路径
test_dir = data_dir + 'test/'
# 从文件中读取数据
datasets = {
'train' : datasets.ImageFolder(train_dir, transform=image_transforms['train']), # 读取train中的数据集,并transform
'val' : datasets.ImageFolder(val_dir, transform=image_transforms['val']), # 读取val中的数据集,并transform
'test' : datasets.ImageFolder(test_dir, transform=image_transforms['test']) # 读取test中的数据集,并transform
}
# 定义BATCH_SIZE
BATCH_SIZE = 128 # 每批读取128张图片
# DataLoader : 创建iterator, 按批读取数据
dataloaders = {
'train' : DataLoader(datasets['train'], batch_size=BATCH_SIZE, shuffle=True), # 训练集
'val' : DataLoader(datasets['val'], batch_size=BATCH_SIZE, shuffle=True), # 验证集
'test' : DataLoader(datasets['test'], batch_size=BATCH_SIZE, shuffle=True) # 测试集
}
# 创建label的键值对
LABEL = dict((v, k) for k, v in datasets['train'].class_to_idx.items())
LABEL
# train 简介
dataloaders['train'].dataset
dataloaders['train'].dataset.classes # train下的类别
dataloaders['train'].dataset.root # train的路径
# 肺部正常的图片
files_normal = os.listdir(os.path.join(str(dataloaders['train'].dataset.root), 'NORMAL'))
files_normal
# 肺部感染的图片
files_pneumonia = os.listdir(os.path.join(str(dataloaders['train'].dataset.root), 'PNEUMONIA'))
files_pneumonia
# val 简介
dataloaders['val'].dataset
# test 简介
dataloaders['test'].dataset
# 导入SummaryWriter
from torch.utils.tensorboard import SummaryWriter
# SummaryWriter() 向事件文件写入事件和概要
# 定义日志路径
log_path = 'logdir/'
# 定义函数:获取tensorboard writer
def tb_writer():
timestr = time.strftime("%Y%m%d_%H%M%S") # 时间格式
writer = SummaryWriter(log_path + timestr) # 写入日志
return writer
writer = tb_writer()
# 第1种方法:显示部分图片集
images, labels = next(iter(dataloaders['train'])) # 获取到一批数据
# 定义图片显示方法
def imshow(img):
img = img / 2 + 0.5 # 逆正则化
np_img = img.numpy() # tensor --> numpy
plt.imshow(np.transpose(np_img, (1, 2, 0))) # 改变通道顺序
plt.show()
grid = utils.make_grid(images) # make_grid的作用是将若干幅图像拼成一幅图像
imshow(grid) # 展示图片
# 在summary中添加图片数据
writer.add_image('X-Ray grid', grid,
0) # add_image(tag, img_tensor, global_step=None, walltime=None, dataformats='CHW')
writer.flush() # 把事件文件写入到磁盘
# 获取一张图片tensor
dataloaders['train'].dataset[4] # 返回:tensor, label
# 第2种方法:显示一张图片
def show_sample(img, label):
print("Label : ", dataloaders['train'].dataset.classes[label]) # 输出标签
img = img.numpy().transpose((1, 2, 0)) # 改变shape顺序
mean = np.array([0.485, 0.456, 0.406]) # 均值
std = np.array([0.229, 0.224, 0.225]) # 标准差
img = img * std + mean # 逆向复原
img = np.clip(img, 0, 1) # np.clip() 将inp中的元素值限制在(0,1)之间,最小值为0,最大值为1。小于min的等于min,大于max等于max
plt.imshow(img)
plt.axis('off') # 关闭坐标轴
show_sample(*dataloaders['train'].dataset[4]) # 显示第5张图片
# 第3种方法:显示一张图片
def show_image(img):
plt.figure(figsize=(8, 8)) # 显示大小
plt.imshow(img) # 显示图片
plt.axis('off') # 关闭坐标轴
plt.show()
# 读取图片
one_img = Image.open(dataloaders['train'].dataset.root + 'NORMAL/IM-0239-0001.jpeg')
# 调用函数
show_image(one_img)
# 记录错误分类的图片
def misclassified_images(pred, writer, target, images, output, epoch, count=10):
misclassified = (pred != target.data) # 判断是否一致
for index, image_tensor in enumerate(images[misclassified][:count]):
img_name = 'Epoch:{}-->Predict:{}-->Actual:{}'.format(epoch, LABEL[pred[misclassified].tolist()[index]],
LABEL[target.data[misclassified].tolist()[index]])
writer.add_image(img_name, image_tensor, epoch)
# 自定义池化层
class AdaptiveConcatPool2d(nn.Module):
def __init__(self, size=None):
super(AdaptiveConcatPool2d,self).__init__()
size = size or (1, 1) # kernel大小
# 自适应算法能够自动帮助我们计算核的大小和每次移动的步长。
self.avgPooling = nn.AdaptiveAvgPool2d(size) # 自适应平均池化
self.maxPooling = nn.AdaptiveMaxPool2d(size) # 最大池化
def forward(self, x):
# 拼接avg和max
return torch.cat([self.maxPooling(x), self.avgPooling(x)], dim=1)
# 迁移学习:获取预训练模型,并替换池化层和全连接层
def get_model():
# 获取欲训练模型 restnet50
model = models.resnet50(pretrained=True)
# 冻结模型参数
for param in model.parameters():
param.requires_grad = False
# 替换最后2层:池化层和全连接层
# 池化层
model.avgpool = AdaptiveConcatPool2d()
# 全连接层
model.fc = nn.Sequential(
nn.Flatten(), # 拉平
nn.BatchNorm1d(4096), # 加速神经网络的收敛过程,提高训练过程中的稳定性
nn.Dropout(0.5), # 丢掉部分神经元
nn.Linear(4096, 512), # 全连接层
nn.ReLU(), # 激活函数
nn.BatchNorm1d(512),
nn.Dropout(0.5),
nn.Linear(512, 2), # 2个输出
nn.LogSoftmax(dim=1) # 损失函数:将input转换成概率分布的形式,输出2个概率
)
return model
# 定义训练函数
def train_val(model, device, train_loader, val_loader, optimizer, criterion, epoch, writer):
model.train()
total_loss = 0.0
val_loss = 0.0
val_acc = 0
for batch_id, (images, labels) in enumerate(train_loader):
# 部署到device上
images, labels = images.to(device), labels.to(device)
# 梯度置0
optimizer.zero_grad()
# 模型输出
outputs = model(images)
# 计算损失
loss = criterion(outputs, labels)
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
# 累计损失
total_loss += loss.item() * images.size(0)
# 平均训练损失
train_loss = total_loss / len(train_loader.dataset)
# 写入到writer中
writer.add_scalar('Training Loss', train_loss, epoch)
# 写入到磁盘
writer.flush()
model.eval()
with torch.no_grad():
for images, labels in val_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images) # 前向传播输出
loss = criterion(outputs, labels) # 损失
val_loss += loss.item() * images.size(0) # 累计损失
_, pred = torch.max(outputs, dim=1) # 获取最大概率的索引
correct = pred.eq(labels.view_as(pred)) # 返回:tensor([ True,False,True,...,False])
accuracy = torch.mean(correct.type(torch.FloatTensor)) # 准确率
val_acc += accuracy.item() * images.size(0) # 累计准确率
# 平均验证损失
val_loss = val_loss / len(val_loader.dataset)
# 平均准确率
val_acc = val_acc / len(val_loader.dataset)
return train_loss, val_loss, val_acc
# 定义测试函数
def test(model, device, test_loader, criterion, epoch, writer):
model.eval()
total_loss = 0.0
correct = 0.0 # 正确数
with torch.no_grad():
for batch_id, (images, labels) in enumerate(test_loader):
images, labels = images.to(device), labels.to(device)
# 输出
outputs = model(images)
# 损失
loss = criterion(outputs, labels)
# 累计损失
total_loss += loss.item()
# 获取预测概率最大值的索引
_, predicted = torch.max(outputs, dim=1)
# 累计正确预测的数
correct += predicted.eq(labels.view_as(predicted)).sum().item()
# 错误分类的图片
misclassified_images(predicted, writer, labels, images, outputs, epoch)
# 平均损失
avg_loss = total_loss / len(test_loader.dataset)
# 计算正确率
accuracy = 100 * correct / len(test_loader.dataset)
# 将test的结果写入write
writer.add_scalar("Test Loss", total_loss, epoch)
writer.add_scalar("Accuracy", accuracy, epoch)
writer.flush()
return total_loss, accuracy
# 定义训练流程
# 是否有GPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(device.type)
# 模型部署到device
model = get_model().to(device)
# 损失函数
criterion = nn.NLLLoss()
# 优化器
optimizer = optim.SGD(model.parameters(), lr=0.001)
# 定义训练流程函数
def train_epochs(model, device, dataloaders, criterion, optimizer, epochs, writer):
# 输出信息
print("{0:>15} | {1:>15} | {2:>15} | {3:>15} | {4:>15} | {5:>15}".format('Epoch', 'Train Loss', 'val_loss', 'val_acc', 'Test Loss', 'Test_acc'))
# 初始最小的损失
best_loss = np.inf
# 开始训练、测试
for epoch in range(epochs):
# 训练,return: loss
train_loss, val_loss, val_acc = train_val(model, device, dataloaders['train'], dataloaders['val'], optimizer, criterion, epoch, writer)
# 测试,return: loss + accuracy
test_loss, test_acc = test(model, device, dataloaders['test'], criterion, epoch, writer)
# 判断损失是否最小
if test_loss < best_loss:
best_loss = test_loss # 保存最小损失
# 保存模型
torch.save(model.state_dict(), 'model.pth')
# 输出结果
print("{0:>15} | {1:>15} | {2:>15} | {3:>15} | {4:>15} | {5:>15}".format(epoch, train_loss, val_loss, val_acc, test_loss, test_acc))
writer.flush()
# 调用函数
epochs=10
train_epochs(model, device, dataloaders, criterion, optimizer, epochs, writer)
writer.close()
def plot_confusion(cm):
plt.figure()
plot_confusion_matrix(cm, figsize=(12, 8), cmap=plt.cm.Blues) # 参数设置
plt.xticks(range(2), ['Normal', 'Pneumonia'], fontsize=14)
plt.yticks(range(2), ['Normal', 'Pneumonia'], fontsize=14)
plt.xlabel('Predicted Label', fontsize=16)
plt.ylabel('True Label', fontsize=16)
plt.show()
def accuracy(outputs, labels):
# 计算正确率
_, preds = torch.max(outputs, dim=1)
correct = torch.tensor(torch.sum(preds == labels).item() / len(preds))
return correct
def metrics(outputs, labels):
_, preds = torch.max(outputs, dim=1)
# precision, recall, F1
# 混淆矩阵
cm = confusion_matrix(labels.cpu().numpy(), preds.cpu().numpy())
# 绘制混淆矩阵
plot_confusion(cm)
# 获取tn, fp, fn, tp
tn, fp, fn, tp = cm.ravel()
# 精准率
precision = tp / (tp + fp)
# 召回率
recall = tp / (tp + fn)
# f1 score
f1 = 2 * ((precision * recall) / (precision + recall))
return precision, recall, f1
# 计算testloader
precisions = []
recalls = []
f1s = []
accuracies = []
with torch.no_grad():
model.eval()
for datas, labels in dataloaders['test']:
datas, labels = datas.to(device), labels.to(device)
# 预测输出
outputs = model(datas)
# 计算metrics
precision, recall, f1 = metrics(outputs, labels)
acc = accuracy(outputs, labels)
# 保存结果
precisions.append(precision)
recalls.append(recall)
f1s.append(f1)
accuracies.append(acc.item())
['{:.2f}%'.format(pre*100) for pre in precisions]# 精准率 precision
['{:.2f}%'.format(r*100) for r in recalls]# 召回率 recall
['{:.2f}%'.format(f*100) for f in f1s]# f1
['{:.2f}%'.format(a*100) for a in accuracies]# 准确率 accuracy
# 1. 导入必要的库
import torch
import torch.nn as nn
import numpy as np
import torch.optim as optim
from torchvision import transforms, datasets, models, utils
from torchsummary import summary # 可视化训练过程
from torch.utils.data import DataLoader
import time
import matplotlib.pyplot as plt
import os
import seaborn as sns
import pandas as pd
from mlxtend.plotting import plot_confusion_matrix
from sklearn.metrics import confusion_matrix
from PIL import Image
from datetime import datetime
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
# 2. 分为train,val,test定义transform
image_transforms= {
'train':transforms.Compose([
transforms.RandomResizedCrop(size = 300, scale = (0.8, 1.1)), #随机长度比例裁剪原始图片,表示随机crop出来的图片会在0.8倍和1.1倍之间
transforms.RandomRotation(degrees = 10), # 根据degrees随机旋转一定角度,则表示在(-10,+10)度之间随机旋转
transforms.ColorJitter(0.4, 0.4, 0.4), # 修改亮度、对比度、饱和度
transforms.RandomHorizontalFlip(), # 水平翻转
transforms.CenterCrop(size = 256), # 根据给定的size从中心裁剪,size 若为sequence则为(h,w),若为int,则(size,size)
transforms.ToTensor(), # numpy-----tensor
# 对数据按通道进行标准化(RGB),即先减均值,再除以标准差
transforms.Normalize([0.485, 0.456, 0.406], # mean
[0.229, 0.224, 0.225]) # std
]),
'val':transforms.Compose([
transforms.Resize(300),
transforms.CenterCrop(256),
transforms.ToTensor(), # numpy-----tensor
# 对数据按通道进行标准化(RGB),即先减均值,再除以标准差
transforms.Normalize([0.485, 0.456, 0.406], # mean
[0.229, 0.224, 0.225]) # std
]),
'test':transforms.Compose([
transforms.Resize(300),
transforms.CenterCrop(256),
transforms.ToTensor(), # numpy-----tensor
# 对数据按通道进行标准化(RGB),即先减均值,再除以标准差
transforms.Normalize([0.485, 0.456, 0.406], # mean
[0.229, 0.224, 0.225]) # std
])
}
# 3. 加载数据集
# 数据集所在目录路径
data_dir = 'F:/Graduate students grade three/postgraduate studies/first year of postgraduate study/Project/PyTorch lung infection identification/one_version/chest_xray/'
# train路径
train_dir = data_dir + 'train/'
# val路径
val_dir = data_dir + 'val/'
# test路径
test_dir = data_dir + 'test/'
# 4. 从文件中读取数据
datas = {
'train': datasets.ImageFolder(train_dir, transform = image_transforms['train']), # 读取train中的数据集,并transform
'val': datasets.ImageFolder(val_dir, transform = image_transforms['val']), # 读取val中的数据集,并transform
'test': datasets.ImageFolder(test_dir, transform = image_transforms['test']), # 读取test中的数据集,并transform
}
# 5. 定义BATCH_SIZE
BATCH_SIZE = 16 # 每批读取16张图片
# 6. DataLoader: 创建iterator,按批读取数据
dataloaders = {
'train': DataLoader(datas['train'], batch_size = BATCH_SIZE,shuffle = True), # 训练集
'val': DataLoader(datas['val'], batch_size = BATCH_SIZE,shuffle = True), # 验证集
'test': DataLoader(datas['test'], batch_size = BATCH_SIZE,shuffle = True), # 测试集
}
# 7. 创建label的键值对
LABEL = dict((v, k) for k, v in datas['train'].class_to_idx.items())
print(LABEL)
# 8. train 简介
dataloaders['train'].dataset
print(dataloaders['train'].dataset)
# 9. train下的类别
dataloaders['train'].dataset.classes
print(dataloaders['train'].dataset.classes)
# 10. train路径
dataloaders['train'].dataset.root
print(dataloaders['train'].dataset.root)
# 11. 肺部正常图片
# listdir:把文件名字全部放在一个列表中,文件的名字就是列表的元素;os.path.join:路径的拼接,拼接str(dataloaders['train'].dataset.root)和NORMAL,最终组成一个完整的路径
file_normal = os.listdir(os.path.join(str(dataloaders['train'].dataset.root), 'NORMAL'))
print(file_normal)
# 12. 肺部感染图片
# listdir:把文件名字全部放在一个列表中,文件的名字就是列表的元素;os.path.join:路径的拼接,拼接str(dataloaders['train'].dataset.root)PNEUMONIA,最终组成一个完整的路径
file_pneumonia = os.listdir(os.path.join(str(dataloaders['train'].dataset.root), 'PNEUMONIA'))
print(file_pneumonia)
# 13. val简介 展示val的基本信息
dataloaders['val'].dataset
print(dataloaders['val'].dataset)
# 14. test简介 展示test的基本信息
dataloaders['test'].dataset
print(dataloaders['test'].dataset)
# 15. 导入SummaryWriter
from torch.utils.tensorboard import SummaryWriter
# SummaryWriter() 向事件写入事件和概要
# 16. 定义日志路径,logdir文件夹要事先在本地创建好
log_path = 'logdir/'
# 17. 定义函数:获取tensorboard writer
def tb_writer():
now = datetime.now() # current date and time
timestr = date_time = now.strftime("%Y-%m-%d, %H:%M:%S") # 时间格式,每份日志的时间
# writer = SummaryWriter(log_path + timestr) # 写入日志,在日志路径下面创建时间文件夹,把该时间点的日志,写在这个文件夹下面
writer = SummaryWriter(log_path, timestr) # 写入日志,在日志路径下面创建时间文件夹,把该时间点的日志,写在这个文件夹下面
return writer
writer = tb_writer()
# 18. 第1种方法: 显示部分图片集
images, labels = next(iter(dataloaders['train'])) # 获取到一批数据,也就是上面设置的BATCH_SIZE张图片
# 定义图片显示方法;整个过程图片都是以张量的形式存在
def imshow(img):
img = img /2 + 0.5 # 逆正则化
np_img = img.numpy() # tensor-----numpy
plt.imshow(np.transpose(np_img, (1, 2, 0))) # 改变通道顺序
plt.show()
grid = utils.make_grid(images) # make_grid的作用是将若干张图片拼成一张图片
imshow(grid) # 显示图片
# 在summary中添加图片数据
# 把运行结果的图片保存在日志中
writer.add_image('X-ray grid', grid, 0)
writer.flush() # 把事件文件写入到磁盘中
# 获取一张图片tensor
dataloaders['train'].dataset[4] # 返回的是训练集第五张图片的张量数据和标签
print(dataloaders['train'].dataset[4])
# 19. 第2种方法:显示图片
# 这种方法使用的也是图片的张量
def show_sample(img, label):
print('label : ', dataloaders['train'].dataset.classes[label]) # 输出标签
img = img.numpy().transpose((1, 2, 0)) # 改变shape顺序
mean = np.array([0.485, 0.456, 0.406]) # 均值
std = np.array([0.229, 0.224, 0.225]) # 方差
img = img * std + mean # 逆向复原
img =np.clip(img, 0, 1) # np.clip()将inp中的元素值限制在(0,1)之间,最小值为0,最大值为1,小于min的等于min,大于max等于max
plt.imshow(img)
plt.axis('off') # 关闭坐标轴
# 显示的是训练集中第5张图片
show_sample(*dataloaders['train'].dataset[4])
# 20. 第3种方法:显示一张图片
# 这种方法的限制是,文件保存的图片必须使安装jpg格式保存的
def show_image(img):
plt.figure(figsize = (8, 8)) # 显示大小
plt.imshow(img) # 显示图片
plt.axis('off') # 关闭坐标轴,由于在此次显示中坐标轴没什么作用,所以选择不显示,如果需要也可以设置为显示’on‘
plt.show()
# 读取图片
# 打开图片的函数,dataloaders['train'].dataset.root + 'NORMAL/IM-0239-001.jpeg'这里是一个路径拼接,前面是图片的位置,后面是图片的文件名
one_img = Image.open(dataloaders['train'].dataset.root + 'NORMAL/IM-0115-0001.jpeg')
# 调用函数
show_image(one_img)
# 21. 记录错误分离的图片
# epoch:那一轮预测错误
# count = 10:只展示前10张预测错误的图片
def misclassified_images(pred, writer, target, images, output, epoch, count = 10):
misclassified = (pred != target.data) # 判断是否一致
for index, image_tensor in enumerate(images[misclassified][:count]):
img_name = 'Epoch : {}---Predict: {} ---Actual : {}'.format(epoch, LABEL[pred[misclassified].tolist()[index]],
LABEL[target.data[misclassified].tolist()[index]])
writer.add_image(img_name, image_tensor, epoch)
# 22. 自定义池化层
class AdaptiveConcatPool2d(nn.Module):
# size:修改池化层的实质就是修改卷积核的大小
def __init__(self, size = None):
super(AdaptiveConcatPool2d, self).__init__()
size = size or (1, 1) # 池化层的卷积核的大小,默认值为(1,1)
# 自适应算法能够自动帮助我们计算核的大小和每次一到的步长
self.avgpooling = nn.AdaptiveAvgPool2d(size) # 自适应平均池化
self.maxpooling = nn.AdaptiveMaxPool2d(size) # 最大池化
def forward(self, x):
# torch.cat:将两个池化层拼接起来
# 1 :按照列的方向进行拼接
return torch.cat([self.maxpooling(x), self.avgpooling(x)], dim = 1)
# 23. 迁移学习:拿到一个成熟的模型,进行模型微调
def get_model():
model = models.resnet50(pretrained=True) # 获取预训练模型
# 冻结预训练模型中的所有参数(由于我们使用的是resnet50网络,它里面的很多参数已经很好了不需要我们再训练了,但是由于这个模型并不是针对肺部图片的模型,所以我们需要对这个模型做一些微调,改变一些参数或者层,所以需要先把所有参数都冻结起来,需要改那个参数,直接拿出来改就行)
for param in model.parameters():
param.requires_grad = False
# 微调模型:替换resnet最后的两层网络,返回一个新的网络模型。(本项目修改的是最后两层)
# model.avgpool:获取到了resnet最后的池化层
model.avgpool = AdaptiveConcatPool2d() # 池化层替换
# 改变全连接层
model.fc = nn.Sequential(
nn.Flatten(), # 所有维度拉平
nn.BatchNorm1d(4096), # 256 * 6 * 6 ----------------4096 正则化处理
nn.Dropout(0.5), # 丢掉一些神经元
nn.Linear(4096, 512),
nn.ReLU(), # 激活层
nn.BatchNorm1d(512), # 正则化处理
nn.Dropout(0.5), # 丢掉一些神经元
nn.Linear(512, 2), # 最后进行一个二分类,因为最后我们要判断这个肺部图片是正常的还是被感染的
nn.LogSoftmax(dim = 1), # 损失函数,dim = 1:维度为1
)
return model
# 24. 定义训练函数
# writer:将训练过程写进日志中,为后续了解训练过程做准备,比如写进每次训练的损失
# train_loader:训练数据集
# criterion: 损失函数
# optimizer:优化器
def train_val(model, device, train_loader, val_loader, optimizer, criterion, epoch, writer):
model.train()
total_loss = 0.0 # 总损失值初始化为0
val_loss = 0.0
val_acc = 0
# 循环读取训练数据集, 更新模型参数
# batch_id:获取正在运行的图片的批次,图片一个批次是8张肺部图片
for batch_id, (images, labels) in enumerate(train_loader):
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad() # 梯度度初始化为0
outputs = model(images) # 训练后的输出
loss =criterion(outputs, labels) # 计算损失
loss.backward() # 反向传播
optimizer.step() # 更新参数
total_loss += loss.item() + images.size(0) # 累计训练损失
# 平均损失
train_loss = total_loss / len(train_loader.dataset)
# 把每个批次的损失写进日志中,每个批次损失占总训练集的比列
writer.add_scalar('Training Loss', total_loss / len(train_loader.dataset), epoch)
writer.flush() # 每写一次,都需要刷新日志
model.eval()
# 循环读取数据
with torch.no_grad(): # 声明不进行梯度的更新和参数的更新
for images, labels in val_loader:
images, labels = images.to(device), labels.to(device)
# 预测输出
output = model(images)
# 计算损失
loss = criterion(output, labels)
val_loss = val_loss + loss.item() * images.size(0) # 累计损失
# 获取预存结果中每一行数据概率最大的下标
_, pred = torch.max(output, dim=1) # 获取最大概率的索引
# 判断预测是否正确,并把判断结果保存成tensor表,这个表示布尔类型的值(true和false),由于一次批处理128张图片,所以这里就有128个结果
correct = pred.eq(labels.view_as(pred)) # 返回:tensor([True,False......])
# 计算correct中true占128中的百分比,也就是正确率
accuracy = torch.mean(correct.type(torch.FloatTensor))
# 累计正确率
val_acc = val_acc + accuracy.item() * images.size(0)
# 平均验证损失
val_loss /= len(val_loader.dataset)
# 平均正确率
val_acc = val_acc / len(val_loader.dataset)
return train_loss, val_loss, val_acc
# 25. 定义测试函数
def test(model, device, test_loader, criterion, epoch, writer):
model.eval()
# 损失和正确个数
total_loss = 0.0
correct = 0.0
# 循环读取数据
with torch.no_grad():
for batch_id, (images, labels) in enumerate(test_loader):
images, labels = images.to(device), labels.to(device)
# 预测输出
outputs = model(images)
# 计算损失
loss = criterion(outputs, labels)
# 累计损失
total_loss = total_loss + loss.item()
# 获取预存结果中每一行数据概率最大的下标
_, predicted = torch.max(outputs, dim=1)
# 累计预测正确的个数
correct += predicted.eq(labels.view_as(predicted)).sum().item()
# 错误分类的图片
misclassified_images(predicted, writer, labels, images, outputs, epoch)
# 平均损失
avg_loss = total_loss / len(test_loader.dataset)
# 计算正确率
accuracy = 100 * correct / len(test_loader.dataset)
# 写进日志
writer.add_scalar('Test Loss', total_loss, epoch)
writer.add_scalar('Accuracy', accuracy, epoch)
# 刷新
writer.flush()
return total_loss, accuracy
# 26. 定义训练流程
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 选择使用GPU运行程序还是CPU运行程序
model = get_model().to(device)
# 27. 损失函数
criterion = nn.NLLLoss()
# 28. 优化器
optimizer = optim.SGD(model.parameters(), lr = 0.001)
# 29. 定义训练流程函数
def train_epochs(model, device, dataloaders, criterion, optimizer, epochs, writer):
"""
Returns:
返回一个训练过后最好的模型
"""
print("{0:>15} | {1:>15} | {2:>15} | {3:>15} |{4:>15} |{5:>15}".format('Epoch', 'Train Loss', 'val_loss', 'val_acc', 'Test Loss', 'Test_acc'))
best_score = np.inf # 假设最好的预测值,np.inf :表示正无穷
# 开始循环读取数据进行训练和验证
for epoch in range(epochs):
train_loss, val_loss, val_acc = train_val(model, device, dataloaders['train'], dataloaders['val'], optimizer, criterion, epoch, writer)
test_loss, test_acc = test(model, device, dataloaders['test'], criterion, epoch, writer)
if test_loss < best_score:
best_score = test_loss # 保留最小损失
torch.save(model.state_dict(), 'model.pth') # 保存模型 # state_dict变量存放训练过程中需要学习的权重和偏置系数
print("{0:>15} | {1:>15} | {2:>15} | {3:>15} |{4:>15} |{5:>15}".format(epoch, train_loss, val_loss, val_acc, test_loss, test_acc))
writer.flush()
# 30. 调用函数
epochs = 10
train_epochs(model, device, dataloaders, criterion, optimizer, epochs, writer)
writer.close()
# 31. 制作混淆矩阵
# cm:混淆矩阵的数值
def plot_confusion(cm):
plt.figure()
# figize = (12, 8):画布大小 cmap = plt.cm.Blues:颜色设置
plot_confusion_matrix(cm, figize = (12, 8), cmap = plt.cm.Blues) # 参数设置
# range(2):横轴的范围,两个类别,fontsize:字体大小
plt.xticks(range(2), ['Normal', 'Pneumonia'], fontsize = 14)
plt.yticks(range(2), ['Normal', 'Pneumonia'], fontsize=14)
plt.xlabel('Predicted Label', fontsize = 16)
plt.ylabel('True Label', fontsize = 16)
plt.show()
# 32. 计算正确率
def accuracy(outputs, labels):
_, preds = torch.max(outputs, dim=1)
correct = torch.tensor(torch.sum(preds == labels).item() / len(preds))
return correct
# 33.
def metrics(outputs, labels):
_, preds = torch.max(outputs, dim =1)
# precision, recall ,F1
# 混淆矩阵
cm = confusion_matrix(labels.cpu().numpy, preds.cpu().numpy())
# 绘制混淆矩阵
plot_confusion(cm)
# 获取 tn, fp, fn, tp
tn, fp, fn, tp = cm.ravel()
# 精准率
precision = tp / (tp + fp)
# 召回率
recall = tp / (tp + fn)
# f1 score
f1 = 2 * ((precision * recall) / (precision + recall))
return precision, recall, f1
# 34. 计算testloader
precisions = []
recalls = []
f1s = []
accuracies = []
with torch.no_grad():
model.eval()
for datas, labels in dataloaders['test']:
datas, labels =datas.to(device), labels.to(device)
# 预测输出
outputs = model(datas)
# 计算metrics
precision, recall, f1 = metrics(outputs, labels)
acc = accuracy(outputs, labels)
# 保存结果
precisions.append(precision)
recalls.append(recall)
f1s.append(f1)
accuracies.append(acc.item())
# 35. 精确率 precision
print(['{:.2f}%'.format(pre * 100) for pre in precisions])
# 36. 召回率 recall
print(['{:.2f}%'.format(r * 100) for r in recalls])
# 37. f1
print(['{:.2f}%'.format(f * 100) for f in f1s])
# 38. 准确率 accuracy
print(['{:.2f}%'.format(a * 100) for a in accuracies])