##  Pytorch 图像分类教程。

在实践中,对猫和狗进行分类可能有些不必要。但对我来说,它实际上是学习神经网络的一个很好的起点。在本文中,我将分享我执行分类任务的方法。可以通过访问要使用的数据集。

以下是这篇文章的大纲:

  1. 导入模块和设置设备
  2. 加载图像和创建标签
  3. 预处理和数据扩充
  4. 自定义数据集类和数据加载器
  5. 创建 CNN 模型
  6. 模型训练
  7. 评估

事不宜迟,让我们动手写一些代码吧!

#  1.导入模块和设置设备

让我们通过导入所需的模块来启动这个项目。我将在阅读本文时对它们进行全部解释。

    
    
 

# Codeblock 1  
     import os  
     import cv2  
     import numpy as np  
     import matplotlib.pyplot as plt  
     import torch  
     import torch.nn as nn  
     import torch.optim as optim  
     import torch.nn.functional as F  
     import torchvision.transforms as transforms  
       
     from tqdm import tqdm  
     from torchinfo import summary  
     from torch.utils.data import DataLoader

成功导入所有模块后,现在我们可以初始化`device` ,它本质上只是一个`cuda`或`cpu`的字符串。如果您的代码检测到您的机器上安装了 Nvidia
GPU,那么它会自动将`cuda`分配为`device`变量的内容。请记住,通过这样做,我们实际上并没有使用 GPU,而只是想检测它。

    
    
     # Codeblock 2  
    device = 'cuda' if torch.cuda.is_available() else 'cpu' 

#  2.加载图像和创建标签

正如我之前提到的,我们将利用 Kaggle 上公开可用的内容。这是数据集的结构。

所有图像都存储在名为 **cats** 和 **dogs** 的单独文件夹中。

我们需要做的是从名为`cats`和`dogs`的文件夹中加载图像,这些文件夹来自`test_set`和`training_set`
。这基本上意味着我们将做四次完全相同的事情。为了简单起见,我决定为我命名为`load_images()`函数创建一个函数。该函数显示在下面的代码块 3 中。

    
    
 

# Codeblock 3  
     def load_images(path):  
       
         images = []  
         filenames = os.listdir(path)  
           
         for filename in tqdm(filenames):   
             if filename == '_DS_Store':  
                 continue  
             image = cv2.imread(os.path.join(path, filename))  
             image = cv2.resize(image, dsize=(100,100))  
             image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)  
             images.append(image)  
           
         return np.array(images) `load_images()`函数相当简单。它通过接受存储图像的目录地址( `path`
 )来工作。文件夹中的每个文件都将使用`cv2.imread()`加载。然后将这些图像调整为 100×100 并转换为 RGB(请记住,默认情况下
 OpenCV 在 BGR 中加载图像)。

如果仔细查看上面的代码,您会发现我使用了一个`if`语句,只要访问名为`_DS_Store`的文件,该语句就会变为`True`
。我们将摆脱它。老实说,我不确定它到底是什么文件,但它出现在我们正在使用的所有文件夹中。

**__DS_Store_** 文件

由于已经创建了`load_images()`函数,现在我们将使用它来实际加载图像。我们可以看到下面的代码,我将图像存储在`cats_train` 、
`dogs_train` 、 `cats_test`和`dogs_test`中,我认为这些数组的名称是不言自明的。

    
    

# Codeblock 4  
     cats_train = load_images('/kaggle/input/cat-and-dog/training_set/training_set/cats')  
     dogs_train = load_images('/kaggle/input/cat-and-dog/training_set/training_set/dogs')  
       
     cats_test = load_images('/kaggle/input/cat-and-dog/test_set/test_set/cats')  
     dogs_test = load_images('/kaggle/input/cat-and-dog/test_set/test_set/dogs')

图像加载进度条。

加载所有训练和测试数据分别需要大约 65 秒和 13 秒。我们还可以在上面的进度条中看到,训练集中的猫图像数量为 4000,而狗图像数量为 4005。

> 上面进度条中4001和4006的值其实就是我们访问文件夹中所有文件时的迭代次数。但由于`_DS_Store`我们需要将其减去 1 以获得实际的图像数量
> __ 我们跳过的文件。

为了确保我们已正确加载这些图像,我们将通过运行下面的代码块 5 来检查这些数组的形状。在这里我们可以看到所有图像都已成功加载并重新整形为我们之前指定的尺寸。

    
    

# Codeblock 5  
     print(cats_train.shape)  
     print(dogs_train.shape)  
     print(cats_test.shape)  
     print(dogs_test.shape)

所有数组的形状。

接下来我们需要做的是将所有训练数据和测试数据放入同一个数组中,这可以通过使用`np.append()`函数简单地实现。下面代码块 6
中编写的代码利用此函数沿第 0 轴连接数组。通过这样做,现在我们已经将所有训练和测试图像分别存储在`X_train`和`X_test`中。

    
    

# Codeblock 6  
     X_train = np.append(cats_train, dogs_train, axis=0)  
     X_test  = np.append(cats_test, dogs_test, axis=0)  
       
     print(X_train.shape)  
     print(X_test.shape) **X_train** 和 **X_test** 的形状。

## 创建标签

这就是所有的图像。接下来要完成的步骤是为每个图像创建一个标签。这部分的想法是用0标记猫,用1标记狗。

方法很简单。请记住, `X_train`中的前 4000 张图像是猫,其余 4005 张是狗。通过了解这种结构,我们可以创建一个由这些长度的 0 和 1
组成的数组,然后将它们以与我们对`X_train`所做的相同的方式连接起来。我们将做完全相同的事情来为测试数据创建标签。

    
    

# Codeblock 7  
     y_train = np.array([0] * len(cats_train) + [1] * len(dogs_train))  
     y_test = np.array([0] * len(cats_test) + [1] * len(dogs_test))  
       
     print(y_train.shape)  
     print(y_test.shape) **y_train** 和 **y_test** 的形状。

到目前为止,我们已经成功地为数据集中的所有图像创建了标签。

## 显示多个图像

作为附带任务,我还将创建一个函数来显示我们数据集中的多个图像。我命名为`show_images()`的函数接受 3 个参数: `images` 、
`labels`和`start_index` 。前两个基本上是一组图像和标签——非常简单。而`start_index` __
表示我们要首先显示的`images`的索引。下面的代码块 8 显示了函数的外观。

    
 

# Codeblock 8  
     def show_images(images, labels, start_index):  
         fig, axes = plt.subplots(nrows=4, ncols=8, figsize=(20,12))  
       
         counter = start_index  
       
         for i in range(4):  
             for j in range(8):  
                 axes[i,j].set_title(labels[counter].item())  
                 axes[i,j].imshow(images[counter], cmap='gray')  
                 axes[i,j].get_xaxis().set_visible(False)  
                 axes[i,j].get_yaxis().set_visible(False)  
                 counter += 1  
         plt.show()

一旦函数被初始化,现在我们可以尝试使用它了。

    

# Codeblock 9  
     show_images(X_train, y_train, 0)

来自 **cat** 类的前 32 张图像。

在下面的代码块 10 中,我将`start_index`设置为 4000,因为我想查看前 32 个狗图像。

    
    

# Codeblock 10  
     show_images(X_train, y_train, 4000)

来自类狗的前 32 个图像。

#  3.预处理和数据扩充

将对图像和标签进行预处理。先说后者。在训练模型时,我们之前创建的标签不是 Pytorch 可以接受的形状。下面是当前`y_train`中前 10
个标签的样子。

    
    

# Codeblock 11  
     print(y_train[:10]) **y_train** 中的前 10 个标签。

现在我们需要使用以下代码将其放入二维数组。之后我们还将其转换为 Pytorch 张量。

    
    
 

# Codeblock 12  
     y_train = torch.from_numpy(y_train.reshape(len(y_train),1))  
     y_test = torch.from_numpy(y_test.reshape(len(y_test),1))  
       
     print(y_train[:10]) 处理后的 **y_train** 数组。

## 图像预处理和增强

使用 Pytorch 的便利部分是我们可以使用单个函数(即`transforms.Compose()`进行图像预处理和增强。我们对下面的代码块 13
所做的是将图像(最初是 Numpy 数组的形式)转换为 Pytorch
张量。图像中的像素强度值也仅使用`transforms.Normalize()`压缩为 -1 和
1。重要的是要知道我重复了三次平均值和标准偏差的值,因为原始图像具有 RGB 颜色通道。所以基本上,列表中的每个元素都对应一个通道。

    
    

# Codeblock 13  
     transforms_train = transforms.Compose([transforms.ToTensor(), # convert to tensor  
                                            transforms.RandomRotation(degrees=20),   
                                            transforms.RandomHorizontalFlip(p=0.5),   
                                            transforms.RandomVerticalFlip(p=0.005),   
                                            transforms.RandomGrayscale(p=0.2),   
                                            transforms.Normalize(mean=[0.5,0.5,0.5], std=[0.5,0.5,0.5]) # squeeze to -1 and 1  
                                           ])

对于增强,我们将执行随机旋转、随机水平翻转、随机垂直翻转和随机灰度。
`p`参数表示将变换函数应用于图像的概率。我们可以在上面的代码中看到,图像垂直翻转的概率很小(0.005)。这本质上是因为我们假设我们测试集中的大多数猫狗都不会倒着看,但如果有的话,我们的模型有望能够正确预测。

图像数据增强只应在训练数据上执行。测试集中的图像应保持不变,仅用于预处理目的除外。这基本上是因为我们假设这些图像是我们将在真实世界的应用程序中自然看到的图像。下面的代码块
14 显示了用于测试数据的转换。

    

# Codeblock 14  
     transforms_test = transforms.Compose([transforms.ToTensor(),   
                                          transforms.Normalize([0.5,0.5,0.5], [0.5,0.5,0.5])])

#  4.自定义数据集类和数据加载器

下一步要做的是创建一个类来存储这对图像和标签。其实我们其实可以用`TensorDataset` __ Pytorch
模块中已有的类。但是,我们将创建一个自定义的,因为`TensorDataset`不允许传递`transforms.Compose`对象,这导致我们无法进行预处理和扩充。下面的代码块
15 显示了自定义类`Cat_Dog_Dataset`的样子。

    

# Codeblock 15  
     class Cat_Dog_Dataset():  
         def __init__(self, images, labels, transform=None):  
             self.images = images  
             self.labels = labels  
             self.transform = transform  
           
         def __len__(self):  
             return len(self.images)  
           
         def __getitem__(self, index):  
             image = self.images[index]  
             label = self.labels[index]  
               
             if self.transform:  
                 image = self.transform(image)  
               
             return (image, label)

在创建自定义数据集类时,需要定义三种方法: `__init__()` _、_ `__len__()` _和_`__getitem__()`
。第一个用于初始化所有属性。其次, `__len__()`允许将`Cat_Dog_Dataset`传递给`len()`函数,以便找出数据集中的样本数。最后,
`__getitem__()`使此类的对象可以被索引。 `DataLoader`实际上需要所有这些功能 __ (我稍后会解释)以便它正常工作。

由于定义了`Cat_Dog_Dataset`类,我们现在可以用它包装我们的`X_train`和`y_train`
。不要忘记传递`transformations_train` ,因为它可以让我们的图像在索引时进行转换。我们还将对测试数据做类似的事情。

    
    
 

# Codeblock 16  
     train_dataset = Cat_Dog_Dataset(images=X_train, labels=y_train, transform=transforms_train)  
     test_dataset  = Cat_Dog_Dataset(images=X_test, labels=y_test, transform=transforms_test)

## 数据加载器

初始化`train_dataset`和`test_dataset`后,我们需要为两者创建`DataLoader`以确定如何加载数据。在我们的例子中,我决定训练一个批量大小为
32 的模型。我还将`drop_last`参数设置为 True,以避免最后一批在包含少于 32 张图像时被送入模型。

    
    

# Codeblock 17  
     train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, drop_last=True)  
     test_loader  = DataLoader(test_dataset, batch_size=32, shuffle=True, drop_last=True)

## 可视化一些增强图像

下面是显示增强图像的代码,以防您想知道我们的图像在随机变换后的样子。要记住的一件重要事情是,在使用前面的转换函数处理数据之后,图像数组的形状会自动从 _(_

`no_of_images` _,_ `height` _,_ `width` _,_ `no_of_channels` _)_ 变为 _(_
 `no_of_images` _,_ `no_of_channels` _,_ `height` _,_ `width` _)_ 。    
     
      # Codeblock 18  
     iterator = iter(train_loader)  
     image_batch, label_batch = next(iterator)  
       
     print(image_batch.shape)

使用我们的转换函数处理后图像数组的形状。

另一方面,当涉及到显示图像时,我们需要将数组的形状转换回初始维度。这可以通过使用`permute()`方法来实现。完成后,我们现在可以将图像数组 (

`image_batch_permuted` ) 提供给`show_images()`函数。
    
     
      # Codeblock 19  
     image_batch_permuted = image_batch.permute(0, 2, 3, 1)  
       
     print(image_batch_permuted.shape)  
       
     show_images(image_batch_permuted, label_batch, 0)

图像增强结果。

这是转换后的图像的样子。事实上,我们无法确定水平翻转图像的外观,因为我们不熟悉原始的、未翻转的版本。然而,我们可以在这里看到一些图像轻微旋转,其中一些也被转换为灰度(尽管仍然有
3 个通道)。此外,由于 -1 到 1 值范围内的像素缩放,图像看起来比原始图像更暗。

#  5\. 创建 CNN 模型

至此,我们已经准备好数据集。不仅如此,我们还确保增强功能按预期工作。因此,下一步要做的是创建 CNN 模型。该模型的详细信息可以在下面的代码块 20
中看到。

    

# Codeblock 20  
     class CNN(nn.Module):  
         def __init__(self):  
             super().__init__()  
             self.conv0 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=(3,3), stride=(1,1), padding=(1,1), bias=False)  
             self.bn0 = nn.BatchNorm2d(num_features=16)  
             self.maxpool = nn.MaxPool2d(kernel_size=(2,2), stride=(2,2))  
               
             self.conv1 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=(3,3), stride=(1,1), padding=(1,1), bias=False)  
             self.bn1 = nn.BatchNorm2d(num_features=32)  
             # self.maxpool  
               
             self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=(3,3), stride=(1,1), padding=(1,1), bias=False)  
             self.bn2 = nn.BatchNorm2d(num_features=64)  
             # self.maxpool  
               
             self.conv3 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=(3,3), stride=(1,1), padding=(1,1), bias=False)  
             self.bn3 = nn.BatchNorm2d(num_features=128)  
             # self.maxpool  
               
             self.dropout = nn.Dropout(p=0.5)  
             self.fc0 = nn.Linear(in_features=128*6*6, out_features=64)  
             self.fc1 = nn.Linear(in_features=64, out_features=32)  
             self.fc2 = nn.Linear(in_features=32, out_features=1)  
               
         def forward(self, x):  
             x = F.relu(self.bn0(self.conv0(x)))  
             x = self.maxpool(x)  
               
             x = F.relu(self.bn1(self.conv1(x)))  
             x = self.maxpool(x)  
               
             x = F.relu(self.bn2(self.conv2(x)))  
             x = self.maxpool(x)  
               
             x = F.relu(self.bn3(self.conv3(x)))  
             x = self.maxpool(x)  
       
             x = x.reshape(x.shape[0], -1)  
               
             x = self.dropout(x)  
             x = F.relu(self.fc0(x))  
             x = F.relu(self.fc1(x))  
             x = F.sigmoid(self.fc2(x))  
               
             return x

让我解释一下架构。

我们要创建的卷积神经网络模型非常简单。将有 4 个卷积层,其中所有卷积层都使用 3×3 内核、1 个步幅和 1
个填充。这种卷积层配置保留了输入张量的空间维度。四个`Conv2d`层的不同之处在于内核的数量。第一个——直接连接到 CNN 的输入层——由 16
个内核组成。这个数字在后续的卷积层中加倍,剩下的分别由 32、64 和 128 个内核组成。

内核数量的增加导致输出通道的数量也增加。尽管空间维度完全相同,但这也导致了较大的输出维度。我们不希望这种情况发生,因为我们的模型可能会遇到维数灾难问题。为了解决这个问题,我们将在卷积层之后应用最大池化层。池化大小和步幅为
2 时,生成的空间输出维度将减少两倍。在这种情况下,由于我们有 4 个池化层,因此 100×100 的输入大小将分别减少到
50×50、25×25、12×12 和 6×6。

> 请注意,图像尺寸从 25x25 减小到 12x12 是通过将高度和宽度尺寸各减少 1 个像素来实现的。

批量归一化层也将在该网络中实现。该层将放置在卷积层和 ReLU(整流线性单元)之间。使用这种 Conv-BN-ReLU 结构的一些研究论文是 [1]、[2]
和 [3]。此外,您可能会注意到我在`Conv2d`层中将`bias`参数设置为`False` 。原因是在卷积层之后存在批量归一化层导致卷积中的偏差有些无用
[4]。

Conv-BN-ReLU结构示例(整体架构与我们的不同)[4]。

到达最后一个最大池化层后,下一步要做的是展平张量。然后将生成的张量以 50% 的丢弃率传递给丢弃层。然后将这个 dropout
层连接到两个连续的隐藏层,分别有 32 和 64 个神经元。最后,我们将连接一个输出层和一个神经元。可能值得注意的是,两个隐藏层使用的是 ReLU
激活函数,而输出层使用的是 sigmoid。

创建 CNN 类后,现在我们可以实际初始化模型了。我们将使用以下代码进行初始化。不要忘记写入`to(device)`以便让 GPU 完成我们的工作。

    
    
     # Codeblock 21  
    model = CNN().to(device) 

如果需要,您实际上可以使用从`torchinfo`模块获取的`summary()`函数打印出模型的详细信息。

    
    
     # Codeblock 22  
    summary(model, input_size=(4,3,100,100)) 

CNN模型的细节。

#  6.模型训练

在训练模型之前,我们需要指定一个损失函数和一个要使用的优化器。在这种情况下,我将分别对两者使用二元交叉熵 ( `BCELoss` ) 和 Adam。

    
    
     # Codeblock 23  
    loss_function = nn.BCELoss()  
    optimizer = optim.Adam(model.parameters(), lr=0.001) 

## 预测测试数据的函数

我希望能够读取每个时期的测试数据的所有评估指标。为了简单起见,我想将这些过程包装在一个我命名为`predict_test_data()`函数中,我们将在每个时期结束时调用该函数。

    

# Codeblock 24  
     def predict_test_data(model, test_loader):  
           
         num_correct = 0  
         num_samples = 0  
           
         model.eval()  
           
         with torch.no_grad():  
             for batch, (X_test, y_test) in enumerate(test_loader):  
                 X_test = X_test.float().to(device)  
                 y_test = y_test.float().to(device)  
       
                 # Calculate loss (forward propagation)  
                 test_preds = model(X_test)  
                 test_loss  = loss_function(test_preds, y_test)  
                   
                 # Calculate accuracy  
                 rounded_test_preds = torch.round(test_preds)  
                 num_correct += torch.sum(rounded_test_preds == y_test)  
                 num_samples += len(y_test)  
               
         model.train()  
           
         test_acc = num_correct/num_samples  
           
         return test_loss, test_acc

该函数通过接受两个输入参数来工作:一个模型和一个包含来自测试集的样本的数据加载器。函数内部要完成的过程只是预测那些测试数据(前向传播)。然后计算准确度分数。

可能被认为重要的一件事是我们需要在前向传播之前使用`model.eval()`将我们的模型置于评估模式。这样做的原因之一是重新连接 dropout
层中随机断开的神经元,以使网络获得最佳性能。在整个预测过程完成后,模型将使用`model.train()`返回训练模式。

## 训练循环

由于已创建`predict_test_data()`函数,我们现在将开始训练循环。这个过程实际上是非常标准的。将有两个`for`循环,一个针对每个时期进行迭代,另一个针对每个批次进行迭代。我们将在每个批次中执行以下操作:前向传播、反向传播和梯度下降。中间还会有其他几个关于损失值和准确度分数计算的操作。

在这种情况下,我决定运行 100 个 epoch 的训练,这需要我大约 20 分钟才能完成。下面的代码块 25 显示了我如何创建训练循环。

    
    

# Codeblock 25  
       
     train_losses = []    # Training and testing loss was calculated based on the last batch of each epoch.  
     test_losses  = []  
     train_accs = []  
     test_accs  = []  
       
     for epoch in range(100):  
           
         num_correct_train = 0  
         num_samples_train = 0  
         for batch, (X_train, y_train) in tqdm(enumerate(train_loader), total=len(train_loader)):  
             X_train = X_train.float().to(device)  
             y_train = y_train.float().to(device)  
               
             # Forward propagation  
             train_preds = model(X_train)  
             train_loss = loss_function(train_preds, y_train)  
               
             # Calculate train accuracy  
             with torch.no_grad():  
                 rounded_train_preds = torch.round(train_preds)  
                 num_correct_train += torch.sum(rounded_train_preds == y_train)  
                 num_samples_train += len(y_train)  
                   
             # Backward propagation  
             optimizer.zero_grad()  
             train_loss.backward()  
               
             # Gradient descent  
             optimizer.step()  
           
         train_acc = num_correct_train/num_samples_train  
         test_loss, test_acc = predict_test_data(model, test_loader)  
           
         train_losses.append(train_loss.item())  
         test_losses.append(test_loss.item())  
         train_accs.append(train_acc.item())  
         test_accs.append(test_acc.item())  
               
         print(f'Epoch: {epoch} \t|' \  
                 f' Train loss: {np.round(train_loss.item(),3)} \t|' \  
                 f' Test loss: {np.round(test_loss.item(),3)} \t|' \  
                 f' Train acc: {np.round(train_acc.item(),2)} \t|' \  
                 f' Test acc: {np.round(test_acc.item(),2)}') 当上面的代码运行时,我们的笔记本将输出如下所示的指标数据。在这里我决定只显示前几个时期,因为显示整个训练进度只是浪费空间。
培训进度如何。
# 七、评价
该模型的性能将通过随着时代的推移打印出损失值和准确性得分历史来评估。下面的代码块 27 和 28 展示了我是如何做到的。
    
     
      # Codeblock 26  
     plt.figure(figsize=(10,6))  
     plt.grid()  
     plt.plot(train_losses)  
     plt.plot(test_losses)  
     plt.legend(['train_losses', 'test_losses'])  
     plt.xlabel('epoch')  
     plt.ylabel('loss')  
     plt.show()

损失值历史记录。

上图显示训练进度看起来不错。这主要是因为尽管我们在第 70 个时期左右遇到波动,但随着时期的推移,损失值(针对训练和测试数据)有略微降低的趋势。

在下图中,我们可以观察到训练数据和测试数据的准确性都在逐个提高。很高兴看到在这种情况下我们达到了 94% 的最佳测试准确率。

# Codeblock 27  
     plt.figure(figsize=(10,6))  
     plt.grid()  
     plt.plot(train_accs)  
     plt.plot(test_accs)  
     plt.legend(['train_accs', 'test_accs'])  
     plt.xlabel('epoch')  
     plt.ylabel('accuracy')  
     plt.show()

准确度得分历史记录。

## 在测试集上预测图像

在测试集上预测图像的过程与我们在 Codeblock 18 和 19 中所做的几乎相同。这里的区别在于我们将预测结果作为每个图像的标题而不是基本事实传递。

    

# Codeblock 28  
       
     # Load test images  
     iter_test = iter(test_loader)  
     img_test, lbl_test = next(iter_test)  
       
     # Predict labels  
     preds_test = model(img_test.to(device))  
     img_test_permuted = img_test.permute(0, 2, 3, 1)  
     rounded_preds = preds_test.round()  
       
     # Show test images and the predicted labels  
     show_images(img_test_permuted, rounded_preds, 0)

对测试数据的预测。

好吧,根据上图,似乎所有的预测都是正确的。请记住,猫被标记为 0,而狗被标记为 1。

这就是本文的全部内容。谢谢阅读!

可以通过 访问此笔记本的完整代码。