PyTorch for Deep Learning & Machine Learning – Full Course(4) - NoTe0x009
原链接:
https://www.youtube.com/watch?v=V_xro1bcAuA
写在前面
这一系列是我在学习他人的优质课程(博客文章、视频课程等)时所做的学习笔记,根据我自身的水平来进行学习,同时会进行思维的发散,补充原文中没有提到或者有错误的地方。同时也会进行经常性的更新和整理,让它和我的当下的状态更加契合。
这次带来的是视频的Chapter3部分,也就是Computer Vision,我也不知道他怎么会直接跳到计算机视觉的,总之我们先来看看吧。
准备工作
Introduction to computer vision
pytorch中的计算机视觉库有下面这些:
torchvision,计算机视觉的基础库。
torchvision.datasets,提供计算机视觉数据集以及数据加载函数。
torchvision.models,提供预训练好的计算机视觉模型,可以直接用于解决实际问题。
torchvision.transforms,用于对视觉数据进行处理的函数,使其适合机器学习模型使用。
torch.utils.data.Dataset,pytorch的基础数据集类。
torch.utils.data.DataLoader,在数据集上创建Python可迭代对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import torch from torch import nn
import torchvision from torchvision import datasets from torchvision import transforms from torchvision.transforms import ToTensor
import matplotlib.pyplot as plt
torch.__version__
torchvision.__version__
|
在这个任务中,我们使用的是FashionMNIST数据集,一共有70000张图片,60000张数据集和10000张测试集,图片都是28*28像素的8位灰度图,同时有10个互斥的服装品类标签:0=T恤/上衣、1=裤子、2=套头毛衣、3=连衣裙、4=外套、5=凉鞋、6=衬衫、7=运动鞋、8=包、9=短靴。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| from torchvision import datasets train_data = datasets.FashionMNIST(root="data",train=True,download=True,transform=torchvision.transforms.ToTensor(),target_transform=None)
test_data = datasets.FashionMNIST(root="data",train=False,download=True,transform=torchvision.transforms.ToTensor(),target_transform=None) len(train_data), len(test_data)
image, label = train_data[0] image, label
train_data.classes
train_data.class_to_idx
train_data.targets
|
这个图到底长什么样呢,我们可视化一下:
1 2 3 4 5 6
| import matplotlib.pyplot as plt image, label = train_data[0] image.shape
plt.imshow(image.squeeze(),cmap="gray") plt.title(label);
|
这个图像也不大,也能看出来是什么商品
Mini-batches
目前我们的数据仍以pytorch dataset的形式存在,DataLoader能够将数据集转换为一个Python可迭代对象。具体来说,我们希望吧数据按照batch组织起来。首先能提高计算效率,因为你可能无法一次在内存中存储这么多图像,所以我们将其拆分成每次32张(batch size为32);其次能够让神经网络在每个训练周期内获得更多更新梯度的机会。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| from torch.utils.data import DataLoader BATCH_SIZE = 32 train_dataloader = DataLoader(dataset=train_data,batch_size=BATCH_SIZE,shuffle=True) test_dataloader = DataLoader(dataset=test_data,batch_size=BATCH_SIZE,shuffle=False) train_dataloader, test_dataloader
len(train_dataloader), len(test_dataloader)
train_features_batch, train_labels_batch = next(iter(train_dataloader)) train_features_batch.shape, train_labels_batch.shape
|
Model 0: Build a basline model
这个基线模型的思路是:把输入的1乘28乘28(颜色通道、宽和高)铺平成为1乘784的张量,然后用两层神经网络来做。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| from torch import nn
class Model0(nn.Module): def __init__(self, input_shape:int, hidden_units:int, output_shape: int): super().__init__() self.layers= nn.Sequential( nn.Flatten(), nn.Linear(in_features=input_shape, out_features=hidden_units), nn.Linear(in_features=hidden_units, out_features=output_shape) ) def forward(self, x): return self.layers(x)
torch.manual_seed(42) classes = train_data.classes model_0 = Model0(input_shape=28*28,hidden_units=10, output_shape=len(classes)) model_0.to("cpu")
|
接下来就该配置损失函数、优化器与评估指标了。我们这个是多分类任务,因此损失函数选择nn.CrossEntropyLoss(),优化器采用torch.optim.SGD(),对于多分类问题来说,我们使用准确率作为评估模型性能的指标。
在运行过程中,我们不光要关注模型性能,我们还要关注模型的运行速度,所以我们再写一个计算训练时间的函数。
1 2 3 4 5 6 7 8 9
| from helper_functions import accuracy_fn from timeit import default_timer as timer
loss_fn = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(params=model_0.parameters(),lr=0.1) def print_train_time(start:float, end: float, device: str): total_time = end - start print(f"Train {total_time:.1f} seconds on {device}")
|
加上了batch之后的训练循环变成了这样。由于我们是基于batch计算,那么optimizer会在每一个batch后更新一次模型参数,而不是一个epoch更新一次。
- 遍历epochs。
- 在每个epoch下遍历batchs,执行训练步骤,计算训练损失。
- 遍历测试的batchs,执行测试步骤,计算测试损失。
- 打印日志、记录整体耗时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| from tqdm.auto import tqdm
torch.manual_seed(42) train_time_start_on_cpu = timer()
epochs = 3 for epoch in tqdm(range(epochs)): print(f"Epoch: {epoch} \n") train_loss = 0 for batch ,(X,y) in enumerate(train_dataloader): model_0.train() y_pred = model_0(X) loss = loss_fn(y_pred,y) train_loss += loss optimizer.zero_grad() loss.backward() optimizer.step() if batch % 400 ==0: print(f"Processed {batch*len(X)} / {len(train_data)} samples\n") train_loss /= len(train_dataloader) test_loss, test_acc = 0,0 model_0.eval() with torch.inference_mode(): for X_test,y_test in test_dataloader: test_pred = model_0(X_test) test_loss += loss_fn(test_pred,y_test) test_acc += accuracy_fn(y_true=y_test, y_pred=test_pred.argmax(dim=1)) test_loss /=len(test_dataloader) test_acc /= len(test_dataloader) print(f"TrainLoss: {train_loss:.4f}, TestLoss: {test_loss:.4f}, TestAcc: {test_acc:.4f} \n")
train_time_end_on_cpu = timer() total_train_time_model_0 = print_train_time(start=train_time_start_on_cpu, end=train_time_end_on_cpu, device="cpu")
|
三个epoch下来,大概训练了6秒。最后看一下model_0的效果如何。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| torch.manual_seed(42) def eval_model( model: nn.Module, data_loader:torch.utils.data.DataLoader, loss_fn: nn.Module, accrancy_fn = accuracy_fn): loss, acc =0,0 model.eval() with torch.inference_mode(): for X, y in tqdm(data_loader): y_pred = model(X) loss += loss_fn(y_pred, y) acc+= accrancy_fn( y_true = y, y_pred = y_pred.argmax(dim=1) ) loss /= len(data_loader) acc /= len(data_loader) return { "model_name": model.__class__.__name__, "model_loss": loss, "model_acc": acc }
model_0_result = eval_model( model=model_0, data_loader=test_dataloader, loss_fn=loss_fn ) model_0_result
|
Model 1: Building a better model with non-linearity
接下来,我们在模型的每一层处理之后加入一个非线性函数,看看对模型效果的提升有多大,将其命名为model_1。损失函数我们依旧选择CrossEntropyLoss,优化器也依旧是SGD。直接看整个的从定义到训练再到测试的完整代码。为了后续调用方便,我们将训练过程和测试过程独立出来作为两个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| class Model1(nn.Module): def __init__( self, input_shape: int, hidden_units: int, output_shape: int): super().__init__() self.layers = nn.Sequential( nn.Flatten(), nn.Linear( in_features=input_shape, out_features=hidden_units ), nn.ReLU(), nn.Linear( in_features=hidden_units, out_features=output_shape ), nn.ReLU() ) def forward(self, x:torch.Tensor): return self.layers(x)
torch.manual_seed(42) model_1 = Model1( input_shape=28*28, hidden_units=10, output_shape= len(classes))
loss_fn = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(params=model_1.parameters(), lr=0.1)
def train_step( model: nn.Module, data_loader: torch.utils.data.DataLoader, loss_fn, optimizer, accuracy_fn = accuracy_fn ): train_loss, train_acc =0,0 model.train() for batch, (X,y) in enumerate(data_loader): y_pred = model(X) loss = loss_fn(y_pred,y) train_loss+= loss.item() train_acc += accuracy_fn( y_true=y, y_pred=y_pred.argmax(dim=1) ) optimizer.zero_grad() loss.backward() optimizer.step() train_loss/=len(data_loader) train_acc /=len(data_loader) print(f"Train loss: {train_loss:.5f} | Train acc: {train_acc:.2f}%")
def test_step( model: nn.Module, data_loader: torch.utils.data.DataLoader, loss_fn, accuracy_fn = accuracy_fn ): test_loss, test_acc =0,0 model.eval() with torch.inference_mode(): for X,y in data_loader: test_pred = model(X) test_loss += loss_fn(test_pred,y) test_acc += accuracy_fn( y_true=y, y_pred= test_pred.argmax(dim=1) ) test_loss /=len(data_loader) test_acc /= len(data_loader) print(f"Test Loss: {test_loss:.2f}, Test Accrancy: {test_acc:.2f}%\n")
torch.manual_seed(42) train_time_start_on_cpu = timer() epochs = 3
for epcoh in tqdm(range(epochs)): print(f"Epoch {epoch} \n") train_step( model=model_1, data_loader=train_dataloader, optimizer=optimizer, loss_fn=loss_fn ) test_step( model=model_1, data_loader=test_dataloader, loss_fn=loss_fn )
train_time_end_on_cpu = timer() total_train_time_model_1 = print_train_time( start=train_time_start_on_cpu, end=train_time_end_on_cpu, device="cpu" )
|
这里有一个很严重的问题,如果你在函数参数里面写了默认参数,这里的默认参数是在函数定义时求值的,也就是说如果你给optimizer定义默认参数的时候,它就已经是当时的值了,你后续再去更新optimizer就不会被更新。
最后的结果是Test Loss: 0.69, Test Accrancy: 74.98%,训练了5.5s。如果是使用很小的模型,那么在数据量不大的时候,频繁的在GPU和CPU之间板运输局,会导致系统卡在运输环节,反而GPU的效率就没有CPU高了。
Model 2: Building a Convolutional Neural Network (CNN)
CNN也就是卷积神经网络,和常规的单层神经网络不一样,卷积神经网络的神经元是3d的,能够抓取更多特征,存储更多信息。在torch代码中,主要体现的是不用nn.Linear层,改用nn.Conv2d层和nn.MaxPool2d来进行模型设计。
我们首先来讲解一下nn.Conv2d层。它接收的参数列表如下:
in_channels,输入通道数,例如灰度图为1,RGB为3。
out_channels,输出通道数,即卷积核个数,卷积核个数决定你提取多少种特征。
kernal_size,卷积核大小,例如3乘3,卷积核尺寸越大感受域也越大,但是参数也越多。
stride,步长,控制输出尺寸缩小程度。
padding,填充,保持空间尺寸不变。
dilation,空洞卷积,一般不常用。
groups,分组卷积。
bias,是否使用偏置。
我们再介绍一下nn.MaxPool2d,他一般紧跟在卷积层之后,核心作用是,在不增加参数的情况下,降低特征图的空间尺寸,同时保留最显著的特征。一个nn.MaxPool2d层一般有以下接收的参数列表。
kernel_size,池化窗口大小,如2乘2。
stride,步长,默认等于kernel_size。
padding,填充。
dilation,空洞池化,极少用。
ceil_mode,是否向上取整输出尺寸。
对于没有深度学习理论基础得我来说,这个看的非常云里雾里。所以我们直接看Model2类的写法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| class Model2(nn.Module): def __init__( self, input_shape:int, hidden_units:int, output_shape:int ): super().__init__() self.block1 = nn.Sequential( nn.Conv2d( in_channels=input_shape, out_channels= hidden_units, kernel_size=3, stride=1, padding=1 ), nn.ReLU(), nn.Conv2d( in_channels=hidden_units, out_channels=hidden_units, kernel_size=3, stride=1, padding=1 ), nn.ReLU(), nn.MaxPool2d(kernel_size=2) ) self.block2 = nn.Sequential( nn.Conv2d( in_channels=hidden_units, out_channels= hidden_units, kernel_size=3, stride=1, padding=1 ), nn.ReLU(), nn.Conv2d( in_channels= hidden_units, out_channels=hidden_units, kernel_size=3, stride=1, padding=1 ), nn.ReLU(), nn.MaxPool2d(kernel_size=2) ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(in_features=hidden_units*7*7,out_features=output_shape) ) def forward(self,x): x = self.block1(x) x = self.block2(x) x = self.classifier(x) return x
model_2 = Model2( input_shape=1, hidden_units=10, output_shape= len(classes) )
model_2
|
我们来详细解释一下代码。block1能够做到找线条和边,输出不是像素,而是边缘特征图,然后block2再把边缘拼成部件。最后用classifier做判断。经过池化操作后,每一层给出的大小都减半,最后到classifier之前的形状为[16,7,7],减小了尺寸大小,增加了通道大小。
我说实话对于这些东西我目前也看不太懂,在实践的基础上,如果能够理解里面的数学原理那就最好了,如果不理解的话那还是直接先暂缓吧,毕竟能手动开始跑深度学习我感觉就已经很强大了。
接下来,保持损失函数和优化器都不变,直接写训练过程和测试过程,利用到了刚才写的train_step和test_step两个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| torch.manual_seed(42) loss_fn = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(params=model_2.parameters(), lr=0.1) train_time_start_on_cpu = timer()
epochs =3 for epoch in tqdm(range(epochs)): print(f"Epoch{epoch}\n") train_step( model=model_2, data_loader=train_dataloader, optimizer=optimizer, loss_fn=loss_fn ) test_step( model=model_2, data_loader=test_dataloader, loss_fn=loss_fn )
train_time_end_on_cpu = timer() total_train_time_model_2 = print_train_time( start= train_time_start_on_cpu, end= train_time_end_on_cpu, device="cpu" ) model_2_results = eval_model( model=model_2, data_loader=test_dataloader, loss_fn=loss_fn, accrancy_fn=accuracy_fn, )
model_2_results
|
这个结果还是很令人满意的,准确率能达到88%。我们比较一下三者的结果看一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import pandas as pd compare_results = pd.DataFrame( [ model_0_result, model_1_result, model_2_results ] ) compare_results
|
接下来,我们取最好的model_2,看一下他的预测结果大概长什么样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| def make_predictions( model: nn.Module, data: list, decive: str = "cpu" ): pred_probs = [] model.eval() with torch.inference_mode(): for sample in data: sample = torch.unsqueeze(sample,dim=0) pred_logit: torch.Tensor = model(sample) pred_prob = torch.softmax(pred_logit.squeeze(),dim=0) pred_probs.append(pred_prob) return torch.stack(pred_probs)
import random test_samples = [] test_labels = [] for sample,label in random.sample(list(test_data),k=9): test_samples.append(sample) test_labels.append(label) plt.imshow(test_samples[0].squeeze(),cmap="gray") plt.title(classes[test_labels[0]])
|
我们随机选取一个测试用例看一下到底是什么样的。
因为没设随机种子,所以和原教程中的东西是不一样的
一个完整的预测流程,是将其
1 2 3 4 5 6 7 8 9 10 11
| pred_probs = make_predictions(model=model_2,data=test_samples) pred_probs[:2]
pred_classes = pred_probs.argmax(dim=1) pred_classes
test_labels
|
可以看到,只有第三个预测错误,剩下的预测全部是正确的。
Evaluating model predictions with a confusion matrix
接下来,我们可以构建混淆矩阵来深入评估模型预测效果,使用混淆矩阵画图需要先安装一个库mlxtend。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| y_preds = [] model_2.eval() with torch.inference_mode(): for X,y in tqdm(test_dataloader,desc="making predictions"): y_logit = model_2(X) y_pred = torch.softmax(y_logit.squeeze(),dim=0).argmax(dim=1) y_preds.append(y_pred) y_pred_tensor = torch.cat(y_preds) y_pred_tensor
from torchmetrics import ConfusionMatrix from mlxtend.plotting import plot_confusion_matrix confmat = ConfusionMatrix(num_classes=len(classes), task="multiclass") confmat_tensor = confmat(preds=y_pred_tensor, target = test_data.targets) fig,ax = plot_confusion_matrix(conf_mat=confmat_tensor.numpy(), class_names=classes,figsize=(10,7))
|
可以看到到底有多少个Shirt被判断成了TShirt,这个算是重灾区了
总结
在这一章中,我们主要学习了如何处理图像类的数据,同时我们也可以使用CNN卷积神经网络来建立更加复杂的模型,从而学习到更多的特征了。