Часть 1 Нейронная сеть Pytorch с нуля

Компьютерное зрение (CNN)

Компьютерное зрение относится к области искусственного интеллекта и информатики, которая направлена ​​на то, чтобы позволить компьютерам понимать и интерпретировать визуальные данные. Он включает в себя разработку алгоритмов и методов, которые позволяют компьютерам анализировать, обрабатывать и извлекать значимую информацию из изображений или видео. Конечная цель компьютерного зрения — позволить машинам воспринимать и понимать визуальный мир так же, как люди.

Введение:

Этот проект будет вращаться вокруг предоставления всеобъемлющего пошагового руководства по созданию нейронной сети с нуля. Основная цель — вооружить читателей глубоким пониманием фундаментальных процессов, связанных с построением нейронной сети. Как только проект будет завершен и наша цель по распространению знаний будет достигнута, мы перейдем к следующему этапу, который включает преобразование кода в модульный стиль. Это преобразование обеспечит повторное использование кода и его адаптацию для будущих проектов. Кроме того, мы сосредоточимся на повышении производительности модели за счет внедрения методов трансферного обучения, использования предварительно обученных моделей и их знаний для повышения общей точности и эффективности нашей нейронной сети.

Содержание:

  1. О наборе данных
  2. Подготовка данных
  3. Исследование данных
  4. Преобразование данных в тензоры
    4.1 torchvision.transforms
    4.2 torch.utils.data.Dataset.ImageFolder
    4.3 torch.utils.data.DataLoader
  5. Model_0 (базовая модель)
  6. Model_V1 (улучшение модели)
    6.1 TrivialAugmentation
    6.2 Матрица путаницы
  7. Сделайте прогноз в пользовательском изображении

1. О наборе данных

Для этого проекта мы будем использовать набор данных Food Images (Food-10) от Kaggle. Набор данных содержит ряд различных подмножеств полных данных о еде-101. Food101 предоставил нам метаданные как для тестов, так и для поездов в текстовом файле. Мы воспользуемся этой информацией и извлечем ее часть.



2. Подготовка данных

PyTorch предлагает множество встроенных функций загрузки данных для обработки распространенных типов данных. One useful function is ImageFolder, particularly when working with images in a standard image classification form



Этот проект был выполнен с использованием Google Colab.

Для начала данные получены из Kaggle по указанной выше ссылке и сохранены на моем Google Диске. Затем я подключаю свой Google Диск к Google Colab. Из загруженного набора данных извлекается подмножество 10% для построения модели и уменьшения ее размера. Этот подход способствует более быстрому обучению и позволяет многократно улучшать модель до тех пор, пока не будет достигнута желаемая производительность. Важно помнить, что машинное обучение включает в себя итеративный процесс, и рекомендуется начинать с меньшего подмножества. Как только желаемая производительность будет достигнута, модель можно масштабировать по мере необходимости.

Наш извлеченный набор данных должен быть организован в этом формате (стандартная форма классификации изображений)

Цель состоит в том, чтобы превратить эту структуру хранения данных в набор данных, который можно использовать с PyTorch.

Во-первых, давайте получим мои вспомогательные функции с моего Google Диска.

import shutil
from shutil import copy

# Specify the source and destination paths
source_file_path = "/content/drive/MyDrive/Deep_learning/helper_functions.py"
destination_file_path = "helper_functions.py"
# Copy the file
copy(source_file_path, destination_file_path)
# Print a success message
print("File copied successfully!")

# importing my helper_fucntions
from helper_functions import download_data_V2, walk_through_dir, plot_transformed_random_images, accuracy_fn, eval_model

Приведенная ниже функция кода извлекает 10% изображений для каждого из моих любимых продуктов: кексов, пончиков, картофеля фри и мороженого. Он извлекает образцы из указанной папки на Google Диске и сохраняет их во вновь созданных папках «train» и «test». Структура папок соответствует стандартному формату, требуемому набором данных ImageFolder PyTorch.

# test data
download_data_V2('/content/drive/MyDrive/Deep_learning/data2/meta/meta/test.txt',
                 '/content/drive/MyDrive/Deep_learning/data2/images',
                 'data/my_fav_foods/test',
                 percentage=0.1,
                 class_list=['cup_cakes', 'donuts', 'french_fries', 'ice_cream'])
# train data
download_data_V2('/content/drive/MyDrive/Deep_learning/data2/meta/meta/train.txt',
                 '/content/drive/MyDrive/Deep_learning/data2/images',
                 'data/my_fav_foods/train',
                 percentage=0.1,
                 class_list=['cup_cakes', 'donuts', 'french_fries', 'ice_cream'])

Создайте путь для каталога данных, обучения и тестирования

from pathlib import Path

# data preparation
# Setup path to data folder
data_path = Path("data/")
image_path = data_path / "my_fav_foods"

# Setup train and testing paths
train_dir = image_path / "train"
test_dir = image_path / "test"

Проверьте структуру папок

# check data folder structure
walk_through_dir(data_path)

Output:
There are 1 directories and 0 images in 'data'.
There are 2 directories and 0 images in 'data/my_fav_foods'.
There are 4 directories and 0 images in 'data/my_fav_foods/train'.
There are 0 directories and 75 images in 'data/my_fav_foods/train/ice_cream'.
There are 0 directories and 75 images in 'data/my_fav_foods/train/french_fries'.
There are 0 directories and 75 images in 'data/my_fav_foods/train/cup_cakes'.
There are 0 directories and 75 images in 'data/my_fav_foods/train/donuts'.
There are 4 directories and 0 images in 'data/my_fav_foods/test'.
There are 0 directories and 25 images in 'data/my_fav_foods/test/ice_cream'.
There are 0 directories and 25 images in 'data/my_fav_foods/test/french_fries'.
There are 0 directories and 25 images in 'data/my_fav_foods/test/cup_cakes'.
There are 0 directories and 25 images in 'data/my_fav_foods/test/donuts'.

3. Исследование данных

Станьте единым целым с данными!

Теперь, когда у нас есть набор данных, давайте изучим их.

  • Случайная визуализация с использованием PIL
from PIL import Image
import random
random.seed(42)
# get all image path
all_image_pathlist = list(image_path.glob('*/*/*.jpg'))
# pick a random image from all_image_pathlist 
random_image = random.choice(all_image_pathlist)
# get the image class(the image class is the directory name where the image is stored)
image_class = random_image.parent.stem
# open image using PIL
img = Image.open(random_image)
# print metadata
print(f'image path: {random_image}')
print(f'image class: {image_class}')
print(f'image height: {img.height}')
print(f'image width: {img.width}')
img

output:
image path: data/my_fav_foods/test/french_fries/457709.jpg
image class: french_fries
image height: 384
image width: 512

4. Преобразование данных в тензоры

Чтобы подготовить данные изображения для глубокого обучения с помощью PyTorch, мы выполняем следующие ключевые шаги:

  • Преобразование изображений в тензоры с помощью torchvision.transforms: это преобразует данные изображения в числовые тензоры, позволяя алгоритмам машинного обучения обрабатывать данные. Мы также можем применять методы увеличения данных во время этого преобразования, чтобы повысить производительность модели.
  • Преобразование преобразованных данных в torch.utils.data.Dataset.ImageFolder: этот формат, специфичный для PyTorch, упрощает обработку и организацию данных. Он автоматически упорядочивает изображения в папках для конкретного класса для быстрого доступа во время обучения.
  • Создайте torch.utils.data.DataLoader: этот загрузчик генерирует пакеты выборок из набора данных, повышая эффективность обучения за счет одновременной обработки нескольких выборок. Это ускоряет обучение и повышает производительность модели.

4.1 torchvision.transforms

torchvision.transforms содержит множество готовых методов для форматирования изображений, превращения их в тензоры и даже манипулирования ими для увеличения данных.



Примечание. Обычно мы не выполняем манипуляции с данными в тестовом наборе. Однако нам нужно убедиться, что изображения нашего тестового набора преобразованы в тензоры. Мы приводим тестовые изображения к тому же размеру, что и наши тренировочные изображения.

import torch
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

# transform for train data
data_transform_train = transforms.Compose([
    # resize to 64x64 just like in tinyVGG architecture (For faster training time)
    transforms.Resize(size=(64,64)),
    # flip the images randomly on the horizontal
    transforms.RandomHorizontalFlip(p=0.5),
    # trivial augmentation
    # transforms.TrivialAugmentWide(num_magnitude_bins=5),
    # turn image into a torch.Tensor
    transforms.ToTensor()
])
# transform for test data 
data_transform_test = transforms.Compose([
    # resize to 64x64 just like in tinyVGG architecture (For faster training time)
    transforms.Resize(size=(64,64)),
    # turn image into a torch.Tensor
    transforms.ToTensor()
])

NOTE: После преобразования данных изображения его форма станет первым форматом цветового канала. (CHW) Такое расположение обычно используется в PyTorch.

# the img shape after transform
print(data_transform_train(img).shape)

output:
torch.Size([3, 64, 64])

Чтобы визуализировать наше преобразованное изображение с помощью matplotlib, нам нужно изменить форму изображения, чтобы она соответствовала формату, в котором цветовой канал является последним (HWC). Это можно сделать с помощью метода перестановки. Matlotlib принимает формат HWC.

import matplotlib.pyplot as plt
import numpy as np
plt.imshow(data_transform_train(img).permute(1,2,0)) 
plt.axis(False)

Теперь у нас есть набор преобразований, давайте вызовем нашу функцию, чтобы опробовать их на разных изображениях.

# NOTE: This function use matlotlib and has permuted the shape of each image for visulalization
# originaly the shape after transform is in CHW format
plot_transformed_random_images(all_image_pathlist, 
                        transform=data_transform_train, 
                        n=2)

4.2 torch.utils.data.Dataset.ImageFolder

Теперь пришло время преобразовать данные нашего изображения в набор данных, совместимый с PyTorch. Поскольку наши данные соответствуют стандартному формату классификации изображений, мы можем использовать класс torchvision.datasets.ImageFolder.



Указав путь к файлу целевого каталога изображений и указав желаемые преобразования, мы можем легко использовать этот класс. Давайте применим его к нашим папкам данных train_dir и test_dir.

from torchvision import datasets
# use train_data_transform
train_data = datasets.ImageFolder(root=train_dir, # target folder of images
                                  transform=data_transform_train, # transforms to perform on data (images)
                                  target_transform=None) # transforms to perform on labels (if necessary)
# use test_data_transform
test_data = datasets.ImageFolder(root=test_dir, 
                                 transform=data_transform_test)

print(f"Train data:\n{train_data}\nTest data:\n{test_data}")

output:
Train data:
Dataset ImageFolder
    Number of datapoints: 300
    Root location: data/my_fav_foods/train
    StandardTransform
Transform: Compose(
               Resize(size=(64, 64), interpolation=bilinear, max_size=None, antialias=warn)
               RandomHorizontalFlip(p=0.5)
               ToTensor()
           )
Test data:
Dataset ImageFolder
    Number of datapoints: 100
    Root location: data/my_fav_foods/test
    StandardTransform
Transform: Compose(
               Resize(size=(64, 64), interpolation=bilinear, max_size=None, antialias=warn)
               ToTensor()
           )

Исследование и извлечение наиболее распространенных атрибутов нашего преобразованного набора данных.

# Check the lengths
print(len(train_data), len(test_data))

# Get class names as a list
class_names = train_data.classes
print(class_names)

# Can also get class names as a dict
class_dict = train_data.class_to_idx
print(class_dict)

output:
300 100
['cup_cakes', 'donuts', 'french_fries', 'ice_cream']
{'cup_cakes': 0, 'donuts': 1, 'french_fries': 2, 'ice_cream': 3}

Проверка метаданных первого изображения

img, label = train_data[0][0], train_data[0][1] # indexing the first img and label
print(f"Image tensor:\n{img}")
print(f"Image shape: {img.shape}")
print(f"Image datatype: {img.dtype}")
print(f"Image label: {label}")
print(f"Label datatype: {type(label)}")

output:
Image tensor:
tensor([[[0.3216, 0.2314, 0.1647,  ..., 0.9020, 0.9059, 0.9059],
         [0.5647, 0.5373, 0.4471,  ..., 0.4353, 0.4471, 0.4471],
         [0.5216, 0.5608, 0.6039,  ..., 0.6275, 0.6275, 0.6235],
         ...,
         [0.5255, 0.5255, 0.5216,  ..., 0.4784, 0.4588, 0.4510],
         [0.5098, 0.5098, 0.5059,  ..., 0.4667, 0.4510, 0.4353],
         [0.4941, 0.4980, 0.4941,  ..., 0.4471, 0.4314, 0.4196]],

        [[0.3020, 0.2196, 0.1608,  ..., 0.9569, 0.9608, 0.9608],
         [0.5412, 0.4941, 0.4235,  ..., 0.6353, 0.6471, 0.6431],
         [0.5176, 0.5451, 0.5843,  ..., 0.8000, 0.8000, 0.7961],
         ...,
         [0.4000, 0.3961, 0.3922,  ..., 0.3451, 0.3255, 0.3176],
         [0.3765, 0.3765, 0.3725,  ..., 0.3294, 0.3137, 0.3020],
         [0.3608, 0.3647, 0.3608,  ..., 0.3098, 0.2941, 0.2863]],

        [[0.2431, 0.1647, 0.1098,  ..., 0.9725, 0.9725, 0.9725],
         [0.4549, 0.4196, 0.3451,  ..., 0.8745, 0.8863, 0.8863],
         [0.4667, 0.4863, 0.4784,  ..., 0.9451, 0.9412, 0.9373],
         ...,
         [0.2510, 0.2471, 0.2431,  ..., 0.3059, 0.2941, 0.2824],
         [0.2588, 0.2627, 0.2627,  ..., 0.3059, 0.2863, 0.2667],
         [0.2549, 0.2588, 0.2627,  ..., 0.2863, 0.2667, 0.2510]]])
Image shape: torch.Size([3, 64, 64])
Image datatype: torch.float32
Image label: 0
Label datatype: <class 'int'>

Визуализировать в Matplotlib

# Rearrange the order of dimensions
img_permute = img.permute(1, 2, 0)

# Print out different shapes (before and after permute)
print(f"Original shape: {img.shape} -> [color_channels, height, width]")
print(f"Image permute shape: {img_permute.shape} -> [height, width, color_channels]")

# Plot the image
plt.figure(figsize=(5, 3))
plt.imshow(img_permute)
plt.axis("off")
plt.title(class_names[label], fontsize=14);

output:
Original shape: torch.Size([3, 64, 64]) -> [color_channels, height, width]
Image permute shape: torch.Size([64, 64, 3]) -> [height, width, color_channels]

4.3 torch.utils.data.DataLoader

Превращение нашего набора данных в DataLoader делает их итерируемыми (в пакетах), чтобы модель могла пройти и изучить отношения между образцами и целями.



import multiprocessing
# Turn train and test Datasets into DataLoaders
from torch.utils.data import DataLoader

BATCHSIZE = 32
NUM_WORKERS = multiprocessing.cpu_count()

train_dataloader = DataLoader(dataset=train_data, 
                              batch_size=BATCHSIZE, # how many samples per batch?
                              num_workers=NUM_WORKERS, # how many subprocesses to use for data loading? (higher = more)
                              shuffle=True) # shuffle the data?

test_dataloader = DataLoader(dataset=test_data, 
                             batch_size=BATCHSIZE, 
                             num_workers=NUM_WORKERS, 
                             shuffle=False) # don't usually need to shuffle testing data

train_dataloader, test_dataloader

output:
(<torch.utils.data.dataloader.DataLoader at 0x7fd16c66fbe0>,
 <torch.utils.data.dataloader.DataLoader at 0x7fd16c66e3e0>)

Проверьте форму

img, label = next(iter(train_dataloader))

print(f"Image shape: {img.shape} -> [batch_size, color_channels, height, width]")
print(f"Label shape: {label.shape}")

output:
Image shape: torch.Size([32, 3, 64, 64]) -> [batch_size, color_channels, height, width]
Label shape: torch.Size([32])

5. Model_0 (базовая модель)

В нейронных сетях построение базовой модели имеет решающее значение для исследования архитектуры, настройки гиперпараметров, использования предварительного обучения или передачи обучения, обнаружения переобучения и сравнения/выбора моделей. Он служит отправной точкой для создания более продвинутых и оптимизированных моделей нейронных сетей.

5.1 Архитектура построения модели, копирующая TinyVGG



from torch import nn
class tinyVGG(nn.Module):
    """
    Model architecture copying TinyVGG from: 
    https://poloclub.github.io/cnn-explainer/
    """

    def __init__(self, input_shape: int, hidden_units: int, output_shape: int):
        super().__init__()
        # Convolutional blocks 1
        self.block_1 = nn.Sequential(
            nn.Conv2d(in_channels=input_shape, out_channels=hidden_units, kernel_size=3, stride=1, padding=0),
            nn.ReLU(),
            nn.Conv2d(in_channels=hidden_units, out_channels=hidden_units, kernel_size=3, stride=1, padding=0),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2) # compresses our images i.e 60x60 image pass through maxpol kernel:2 = 30x30 shape, for kernel of 4 = 15x15 shape
        )

        # Convolutional blocks 2
        self.block_2 = nn.Sequential(
            nn.Conv2d(in_channels=hidden_units, out_channels=hidden_units, kernel_size=3, stride=1, padding=0),
            nn.ReLU(),
            nn.Conv2d(in_channels=hidden_units, out_channels=hidden_units, kernel_size=3, stride=1, padding=0),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2)
        )

        # Classifier
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(in_features=hidden_units*13*13, # each layer of our network compresses and changes the shape of our inputs data
                      out_features=output_shape)
        )

    def forward(self, x):
        x = self.block_1(x)
        # print(f'shape after convolutional block 1: {x.shape}')
        x = self.block_2(x)
        # print(f'shape after convolutional block 2: {x.shape}')
        x = self.classifier(x)
        # print(f'shape after classifier: {x.shape}')
        return x

Создайте экземпляр модели

model_0 = tinyVGG(input_shape=3, 
                  hidden_units=10,
                  output_shape=len(class_names))

Общие сведения о моделях

# Install torchinfo if it's not available, import it if it is
try: 
    import torchinfo
except:
    !pip install torchinfo
    import torchinfo
    
from torchinfo import summary
summary(model_0, input_size=[3, 3, 64, 64]) # do a test pass through of an example input size 

output:
Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/
Collecting torchinfo
  Downloading torchinfo-1.8.0-py3-none-any.whl (23 kB)
Installing collected packages: torchinfo
Successfully installed torchinfo-1.8.0
==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
tinyVGG                                  [3, 4]                    --
├─Sequential: 1-1                        [3, 10, 30, 30]           --
│    └─Conv2d: 2-1                       [3, 10, 62, 62]           280
│    └─ReLU: 2-2                         [3, 10, 62, 62]           --
│    └─Conv2d: 2-3                       [3, 10, 60, 60]           910
│    └─ReLU: 2-4                         [3, 10, 60, 60]           --
│    └─MaxPool2d: 2-5                    [3, 10, 30, 30]           --
├─Sequential: 1-2                        [3, 10, 13, 13]           --
│    └─Conv2d: 2-6                       [3, 10, 28, 28]           910
│    └─ReLU: 2-7                         [3, 10, 28, 28]           --
│    └─Conv2d: 2-8                       [3, 10, 26, 26]           910
│    └─ReLU: 2-9                         [3, 10, 26, 26]           --
│    └─MaxPool2d: 2-10                   [3, 10, 13, 13]           --
├─Sequential: 1-3                        [3, 4]                    --
│    └─Flatten: 2-11                     [3, 1690]                 --
│    └─Linear: 2-12                      [3, 4]                    6,764
==========================================================================================
Total params: 9,774
Trainable params: 9,774
Non-trainable params: 0
Total mult-adds (M): 17.06
==========================================================================================
Input size (MB): 0.15
Forward/backward pass size (MB): 2.14
Params size (MB): 0.04
Estimated Total Size (MB): 2.32
==========================================================================================

Настройте функцию потерь и оптимизатор

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(params=model_0.parameters(), lr=0.001)

Нам снова понадобится наша вспомогательная функция для этапа обучения и визуализации производительности модели.

from helper_functions import train_step, test_step, train, plot_loss_curves, pred_and_plot_image, eval_model, plot_model_performance

Обучение начинается здесь…

### START TRAINING ###

# Set random seeds
torch.manual_seed(42)

NUM_EPOCHS = 10
from timeit import default_timer as timer 
start_time = timer()

# Train model_0 
model_0_results = train(model=model_0, 
                        train_dataloader=train_dataloader,
                        test_dataloader=test_dataloader,
                        optimizer=optimizer,
                        loss_fn=loss_fn, 
                        epochs=NUM_EPOCHS)

# End the timer and print out how long it took
end_time = timer()
print(f"Total training time: {end_time-start_time:.3f} seconds")

output:
100%
10/10 [00:48<00:00, 3.95s/it]
Epoch: 1 | train_loss: 1.3883 | train_acc: 0.2094 | test_loss: 1.3810 | test_acc: 0.2422
Epoch: 2 | train_loss: 1.3679 | train_acc: 0.3302 | test_loss: 1.3757 | test_acc: 0.2812
Epoch: 3 | train_loss: 1.3302 | train_acc: 0.3312 | test_loss: 1.3318 | test_acc: 0.4062
Epoch: 4 | train_loss: 1.2576 | train_acc: 0.3521 | test_loss: 1.3115 | test_acc: 0.3984
Epoch: 5 | train_loss: 1.2249 | train_acc: 0.4125 | test_loss: 1.4605 | test_acc: 0.3750
Epoch: 6 | train_loss: 1.2448 | train_acc: 0.4198 | test_loss: 1.3601 | test_acc: 0.3516
Epoch: 7 | train_loss: 1.2283 | train_acc: 0.4385 | test_loss: 1.3194 | test_acc: 0.3594
Epoch: 8 | train_loss: 1.2523 | train_acc: 0.3667 | test_loss: 1.3298 | test_acc: 0.3906
Epoch: 9 | train_loss: 1.1973 | train_acc: 0.4552 | test_loss: 1.3669 | test_acc: 0.3672
Epoch: 10 | train_loss: 1.1929 | train_acc: 0.4500 | test_loss: 1.3539 | test_acc: 0.3984
Total training time: 48.788 seconds
print(model_0_results.keys())
model_0_results['test_acc'][-1]

output:
dict_keys(['train_loss', 'train_acc', 'test_loss', 'test_acc'])
0.3984375

Визуализируйте производительность model_0

plot_loss_curves(model_0_results)

Основываясь на данных настройках модели и гиперпараметрах, кажется, что model_0 не достигает удовлетворительной точности. Визуализация производительности показывает, что модель хорошо работает на данных обучения, но хуже на данных тестирования, что указывает на явный случай переобучения из-за заметного разрыва между потерями при обучении и тестировании.

Мы можем обратиться к следующей ссылке для руководства по решению проблемы:



Кроме того, мы можем:
Увеличение количества эпох на основе тенденции графика указывает на потенциал для улучшения. Кроме того, мы можем поэкспериментировать с преобразованием размера входных данных (например, увеличить масштаб с 64x64), хотя это может потребовать больше времени на обучение. Сбор большего количества образцов для нейронной сети также может привести к лучшим результатам.

Вот некоторые общие гиперпараметры, используемые в PyTorch:

  1. Скорость обучения: определяет размер шага для обновления весов модели во время обучения.
  2. Размер партии: количество образцов, обработанных на каждой итерации во время обучения.
  3. Количество эпох: общее время прохождения всего набора данных через модель во время обучения.
  4. Функция потерь: вычисляет ошибку между прогнозируемыми и целевыми значениями.
  5. Оптимизатор: алгоритм, используемый для обновления весов моделей на основе рассчитанных потерь.
  6. Импульс: параметр, который накапливает долю прошлых градиентов для ускорения сходимости.
  7. Затухание веса: термин регуляризации, который наказывает большие веса для предотвращения переобучения.
  8. Dropout: метод регуляризации, случайным образом удаляющий часть единиц нейронной сети во время обучения.
  9. Функция активации: нелинейная функция, применяемая к выходным данным слоя нейронной сети.
  10. Размер скрытого слоя: количество нейронов в каждом скрытом слое нейронной сети.
  11. kernel_size, шаг и отступы

Эти гиперпараметры существенно влияют на производительность модели и обучение в PyTorch. Правильная их настройка имеет решающее значение для достижения оптимальных результатов.

5.2 Баланс между переоснащением и недообучением

Ни один из рассмотренных выше методов не является серебряной пулей, то есть они не всегда работают.

И предотвращение переобучения и недообучения, возможно, является наиболее активной областью исследований в области машинного обучения.

Поскольку все хотят, чтобы их модели подходили лучше (меньше недообучения), но не настолько хорошо, они плохо обобщают и работают в реальном мире (меньше переобучения).

Между переоснащением и недообучением существует тонкая грань.

Потому что слишком много одного может вызвать другое.

Трансферное обучение, возможно, является одним из самых мощных методов, когда речь идет о том, чтобы справиться как с переоснащением, так и с недостаточным приспособлением к вашим собственным проблемам.

6. Model_V1 (улучшение модели)

Теперь, когда мы получили результаты нашей базовой модели и выявили переоснащение, мы планируем улучшить нашу модель (model_V1), включив тривиальные методы расширения.

6.1 Тривиальная аугментация

Увеличение данных — это метод, используемый для увеличения размера и разнообразия обучающего набора данных. Он включает в себя применение преобразований к существующим данным и создание новых образцов, которые похожи, но немного отличаются. Это помогает повысить производительность и обобщить модели машинного обучения, предоставляя им более широкий и разнообразный набор примеров. Общие преобразования включают геометрические изменения, корректировку цвета, искажение и рандомизацию. Увеличение данных применяется во время обучения, чтобы предотвратить переоснащение и повысить способность модели обрабатывать реальные изменения.

Внедряя эту модификацию, мы стремимся усложнить понимание данных моделью, одновременно снижая риск переобучения.

data_transform_train_V1 = transforms.Compose([
    # resize to 64x64 just like in tinyVGG architecture
    transforms.Resize(size=(64,64)),
    # flip the images randomly on the horizontal
    transforms.RandomHorizontalFlip(p=0.5),
    # trivial augmentation
    transforms.TrivialAugmentWide(num_magnitude_bins=5),
    # turn image into a torch.Tensor
    transforms.ToTensor()
])

data_transform_test_V1 = transforms.Compose([
    # resize to 64x64 just like in tinyVGG architecture
    transforms.Resize(size=(64,64)),
    # turn image into a torch.Tensor
    transforms.ToTensor()
])
# use data_transform_train_V1 here
train_data_V1 = datasets.ImageFolder(root=train_dir, # target folder of images
                                  transform=data_transform_train_V1, # transforms to perform on data (images)
                                  target_transform=None) # transforms to perform on labels (if necessary)

# use data_transform_test_V1 here
test_data_V1 = datasets.ImageFolder(root=test_dir, 
                                 transform=data_transform_test_V1)
BATCHSIZE = 32
NUM_WORKERS = multiprocessing.cpu_count()

train_dataloader_V1 = DataLoader(dataset=train_data_V1, 
                              batch_size=BATCHSIZE, # how many samples per batch?
                              num_workers=NUM_WORKERS, # how many subprocesses to use for data loading? (higher = more)
                              shuffle=True) # shuffle the data?

test_dataloader_V1 = DataLoader(dataset=test_data_V1, 
                             batch_size=BATCHSIZE, 
                             num_workers=NUM_WORKERS, 
                             shuffle=False) # don't usually need to shuffle testing data

# train_dataloader_V1, test_dataloader_V1
model_V1 = tinyVGG(
    input_shape=3,
    hidden_units=10,
    output_shape=len(train_data_V1.classes))
### START TRAINING ###

# Set random seeds
torch.manual_seed(42)

NUM_EPOCHS = 10
from timeit import default_timer as timer 
start_time = timer()

# Train model_1 
modelOne useful function is ImageFolder, particularly when working with images in a standard image classification formresults = train(model=model_V1, 
                        train_dataloader=train_dataloader_V1,
                        test_dataloader=test_dataloader_V1,
                        optimizer=optimizer,
                        loss_fn=loss_fn, 
                        epochs=NUM_EPOCHS)

# End the timer and print out how long it took
end_time = timer()
print(f"Total training time: {end_time-start_time:.3f} seconds")

output:
100%
10/10 [00:53<00:00, 4.52s/it]
Epoch: 1 | train_loss: 1.3873 | train_acc: 0.2500 | test_loss: 1.3707 | test_acc: 0.4141
Epoch: 2 | train_loss: 1.3887 | train_acc: 0.2396 | test_loss: 1.3707 | test_acc: 0.4141
Epoch: 3 | train_loss: 1.3893 | train_acc: 0.2396 | test_loss: 1.3707 | test_acc: 0.4141
Epoch: 4 | train_loss: 1.3869 | train_acc: 0.2552 | test_loss: 1.3707 | test_acc: 0.4141
Epoch: 5 | train_loss: 1.3877 | train_acc: 0.2500 | test_loss: 1.3707 | test_acc: 0.4141
Epoch: 6 | train_loss: 1.3886 | train_acc: 0.2396 | test_loss: 1.3707 | test_acc: 0.4141
Epoch: 7 | train_loss: 1.3879 | train_acc: 0.2500 | test_loss: 1.3707 | test_acc: 0.4141
Epoch: 8 | train_loss: 1.3873 | train_acc: 0.2500 | test_loss: 1.3707 | test_acc: 0.4141
Epoch: 9 | train_loss: 1.3866 | train_acc: 0.2604 | test_loss: 1.3707 | test_acc: 0.4141
Epoch: 10 | train_loss: 1.3867 | train_acc: 0.2552 | test_loss: 1.3707 | test_acc: 0.4141
Total training time: 53.044 seconds

Визуализация

import pandas as pd
model_0_df = pd.DataFrame(model_0_results)
modelOne useful function is ImageFolder, particularly when working with images in a standard image classification formdf = pd.DataFrame(modelOne useful function is ImageFolder, particularly when working with images in a standard image classification formresults)

plot_model_performance(model_0_df, modelOne useful function is ImageFolder, particularly when working with images in a standard image classification formdf)

Результаты model_V1 с бином TrivialAugmentWide, равным 5, показывают, что он изо всех сил пытается учиться на данных обучения, но хорошо работает на данных тестирования. Это говорит о том, что модель эффективно уменьшила переоснащение, поскольку лучше обобщает невидимые данные.

6.2 Матрица путаницы

# from torchmetrics import ConfusionMatrix
# from mlxtend.plotting import plot_confusion_matrix
from tqdm.auto import tqdm

def conft_matrix(model: torch.nn.Module,
                 dataset: torch.utils.data.dataset.Dataset,
                 dataloader: torch.utils.data.DataLoader,
                 ):


  y_preds = []
  model.eval()
  with torch.inference_mode():
    # evaluate per batch using test_dataloader
    for X_test, y_test in tqdm(dataloader, desc='Making predictions...'):
        
        y_logit = model(X_test)
        y_preds.append(torch.softmax(y_logit, dim=0).argmax(dim=1))



  # convert into i demensional tensor
  y_preds_tensor = torch.concat(y_preds)
  # retreive the truth label
  y_true_tensor = torch.tensor(dataset.targets)

  # create confusion matrix
  cm = ConfusionMatrix(task='multiclass', num_classes=len(class_names))
  cm = cm(y_preds_tensor, y_true_tensor)
  # Convert the confusion matrix to a NumPy array
  cm = cm.numpy()

  # Plot the confusion matrix
  fig, ax = plot_confusion_matrix(conf_mat=cm, colorbar=True)
  # Set the tick labels on the x and y axes
  tick_marks = np.arange(len(class_names))
  plt.xticks(tick_marks, class_names)
  plt.yticks(tick_marks, class_names)
  # Show the plot


  return y_preds_tensor,y_true_tensor
Confusion_Matrix_V1 = conft_matrix(model_V1,
                      test_data_V1,
                      test_dataloader_V1)

y_pred, y_true = Confusion_Matrix_V1
y_pred, y_true 

output:
(tensor([1, 2, 3, 0, 2, 2, 0, 1, 0, 3, 0, 2, 3, 3, 2, 3, 0, 3, 2, 1, 1, 0, 0, 2,
         0, 0, 1, 2, 2, 3, 1, 2, 1, 3, 0, 3, 3, 1, 3, 0, 2, 0, 2, 2, 0, 1, 3, 0,
         1, 0, 2, 3, 3, 0, 1, 1, 2, 2, 1, 2, 3, 3, 1, 0, 1, 3, 3, 1, 3, 3, 1, 2,
         2, 3, 0, 0, 2, 0, 0, 3, 2, 3, 0, 0, 1, 2, 1, 1, 2, 2, 2, 0, 3, 3, 2, 3,
         0, 1, 2, 3]),
 tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
         1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
         2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
         3, 3, 3, 3]))

Интерпретация:

  1. Из 100 выборок в test_data наша модель сделала 26 правильных прогнозов.
  2. В частности, он точно предсказал 8 из 25 кексов.
  3. Стоит сосредоточиться на случаях, когда модель делала существенные неверные прогнозы. Например, он неправильно пометил 9 образцов как мороженое, хотя их истинные этикетки были картофелем фри. Это может быть связано с изображениями, которые содержат как мороженое, так и картофель фри, неправильно помеченными образцами дизайна или другими факторами.

7. Сделайте прогноз на пользовательском изображении

Нам нужно будет преобразовать его в тот же формат, что и данные, на которых обучается наша модель, за исключением аугментации и случайного переворота.

Скопируйте файл изображения с моего диска Google

copy('/content/drive/MyDrive/Deep_learning/my_donut.jpg',
     '/content/data')
# Print a success message
print("File copied successfully!")

Визуализируйте изображение

img = Image.open('/content/data/my_donut.jpg')
img

Настройка пользовательского пути к изображению

custom_image_path = data_path / "my_donut.jpg"
custom_image_path

output:
PosixPath('data/my_donut.jpg')

Мы будем читать наше изображение, используя torchvision.io.read_image().

Этот метод считывает изображение в формате JPEG или PNG и превращает его в трехмерный RGB или полутоновый torch.Tensor со значениями типа данных uint8 в диапазоне [0, 255].

import torchvision
# Read in custom image
custom_image_uint8 = torchvision.io.read_image(str(custom_image_path))
# Print out image data
print(f"Custom image tensor:\n{custom_image_uint8}\n")
print(f"Custom image shape: {custom_image_uint8.shape}\n")
print(f"Custom image dtype: {custom_image_uint8.dtype}")

output:
Custom image tensor:
tensor([[[213, 214, 214,  ..., 250, 250, 250],
         [213, 214, 214,  ..., 250, 250, 250],
         [213, 214, 214,  ..., 250, 250, 250],
         ...,
         [ 38,  37,  37,  ...,  24,  12,  44],
         [ 39,  38,  38,  ...,  33,  14,  37],
         [ 41,  40,  39,  ...,  49,  27,  43]],

        [[181, 182, 182,  ..., 244, 244, 244],
         [181, 182, 182,  ..., 244, 244, 244],
         [181, 182, 182,  ..., 244, 244, 244],
         ...,
         [ 20,  19,  19,  ...,  10,   0,  30],
         [ 21,  20,  20,  ...,  15,   0,  19],
         [ 23,  22,  21,  ...,  29,   7,  23]],

        [[168, 169, 169,  ..., 244, 244, 244],
         [168, 169, 169,  ..., 244, 244, 244],
         [168, 169, 169,  ..., 244, 244, 244],
         ...,
         [  6,   5,   5,  ...,   0,   0,  17],
         [  7,   6,   6,  ...,   3,   0,   9],
         [  9,   8,   7,  ...,  18,   0,  14]]], dtype=torch.uint8)

Custom image shape: torch.Size([3, 1020, 680])

Custom image dtype: torch.uint8

Наш тензор custom_image имеет тип данных torch.uint8, и его значения находятся в диапазоне [0, 255].

Но наша модель использует тензоры изображений типа torch.float32 со значениями между [0, 1].

Поэтому, прежде чем мы будем использовать наше собственное изображение с нашей моделью, нам нужно преобразовать его в тот же формат, что и данные, на которых обучается наша модель.

# Load in custom image and convert the tensor values to float32
custom_image = torchvision.io.read_image(str(custom_image_path)).type(torch.float32)

# Divide the image pixel values by 255 to get them between [0, 1]
custom_image = custom_image / 255. 

# Print out image data
print(f"Custom image tensor:\n{custom_image}\n")
print(f"Custom image shape: {custom_image.shape}\n")
print(f"Custom image dtype: {custom_image.dtype}")

output:
Custom image tensor:
tensor([[[0.8353, 0.8392, 0.8392,  ..., 0.9804, 0.9804, 0.9804],
         [0.8353, 0.8392, 0.8392,  ..., 0.9804, 0.9804, 0.9804],
         [0.8353, 0.8392, 0.8392,  ..., 0.9804, 0.9804, 0.9804],
         ...,
         [0.1490, 0.1451, 0.1451,  ..., 0.0941, 0.0471, 0.1725],
         [0.1529, 0.1490, 0.1490,  ..., 0.1294, 0.0549, 0.1451],
         [0.1608, 0.1569, 0.1529,  ..., 0.1922, 0.1059, 0.1686]],

        [[0.7098, 0.7137, 0.7137,  ..., 0.9569, 0.9569, 0.9569],
         [0.7098, 0.7137, 0.7137,  ..., 0.9569, 0.9569, 0.9569],
         [0.7098, 0.7137, 0.7137,  ..., 0.9569, 0.9569, 0.9569],
         ...,
         [0.0784, 0.0745, 0.0745,  ..., 0.0392, 0.0000, 0.1176],
         [0.0824, 0.0784, 0.0784,  ..., 0.0588, 0.0000, 0.0745],
         [0.0902, 0.0863, 0.0824,  ..., 0.1137, 0.0275, 0.0902]],

        [[0.6588, 0.6627, 0.6627,  ..., 0.9569, 0.9569, 0.9569],
         [0.6588, 0.6627, 0.6627,  ..., 0.9569, 0.9569, 0.9569],
         [0.6588, 0.6627, 0.6627,  ..., 0.9569, 0.9569, 0.9569],
         ...,
         [0.0235, 0.0196, 0.0196,  ..., 0.0000, 0.0000, 0.0667],
         [0.0275, 0.0235, 0.0235,  ..., 0.0118, 0.0000, 0.0353],
         [0.0353, 0.0314, 0.0275,  ..., 0.0706, 0.0000, 0.0549]]])

Custom image shape: torch.Size([3, 1020, 680])

Custom image dtype: torch.float32

Преобразуйте наше пользовательское изображение в 64x64.

# Create transform pipleine to resize image
custom_image_transform = transforms.Compose([
    transforms.Resize((64, 64)),
])

# Transform target image to 61 x 64 and add batch size of 1 in index 0
custom_image_transformed = custom_image_transform(custom_image).unsqueeze(dim=0)

# add batch size to the shape
custom_image_transformed.shape

Прогноз

Наш вывод отслеживал градиентный спуск, который немного замедлил результаты. Мы планируем преобразовать этот код в функцию и перевести его в режим оценки (eval_mode) позже. Это позволит нам быстрее выполнять прогнозирование пользовательских изображений.

custom_image_pred = model_V1(custom_image_transformed)
# results will be in logits form (raw output)
custom_image_pred

output:
tensor([[-0.0307, -0.0029,  0.0206,  0.1004]], grad_fn=<AddmmBackward0>)

Преобразование из логитов -> вероятности прогноза -> метки прогноза.

# Print out prediction logits
print(f"Prediction logits: {custom_image_pred}")

# Convert logits -> prediction probabilities (using torch.softmax() for multi-class classification)
custom_image_pred_probs = torch.softmax(custom_image_pred, dim=1)
print(f"Prediction probabilities: {custom_image_pred_probs}")

# Convert prediction probabilities -> prediction labels
custom_image_pred_label = torch.argmax(custom_image_pred_probs, dim=1)
print(f'Class Distionary: {class_dict}')
print(f"Prediction label: {class_names[custom_image_pred_label]}")

output:
Prediction logits: tensor([[-0.0307, -0.0029,  0.0206,  0.1004]], grad_fn=<AddmmBackward0>)
Prediction probabilities: tensor([[0.2369, 0.2436, 0.2494, 0.2701]], grad_fn=<SoftmaxBackward0>)
Class Distionary: {'cup_cakes': 0, 'donuts': 1, 'french_fries': 2, 'ice_cream': 3}
Prediction label: ice_cream

Наша модель model_V1 ошибочно предсказывает пончик для ice_cream с очень низкой вероятностью. Проверяя custom_image_pred_probs, мы замечаем, что модель дает почти одинаковый вес (значения похожи) каждому классу.

Подобная вероятность предсказания может означать несколько вещей:

  1. Модель пытается предсказать все четыре класса одновременно (может быть изображение, содержащее все классы).
  2. Модель на самом деле не знает, что она хочет предсказать, и, в свою очередь, просто присваивает одинаковые значения каждому из классов.

Наш случай номер 2, так как наша модель плохо обучена, она в основном угадывает предсказание.

Функция прогнозирования

# Call out our helper function
pred_and_plot_image(model_V1,
                    custom_image_path,
                    class_names,
                    custom_image_transform)

Вкратце, процесс включает в себя следующие этапы:

1. Загрузка набора данных food101 с Kaggle и выполнение необходимой подготовки и исследования.
2. Преобразование набора данных в наборы для обучения и тестирования с помощью модуля torchvision.transforms для применения различных преобразований изображений и torch.utils.data.Dataset.ImageFolder для организации набора данных. Эти методы помогают предварительно обрабатывать и структурировать данные для обучения и оценки.
3. Создание пакетов данных с использованием класса torch.utils.data.DataLoader для облегчения эффективной загрузки и обработки данных во время обучение.
4. Разработка базовой модели в качестве отправной точки для создания более продвинутых и оптимизированных моделей нейронных сетей.
5. Решение проблем переобучения путем реализации тривиальных методов расширения.
6. Сравнение производительности базовой модели с улучшенной моделью с использованием Matplotlib для визуализации и анализа результатов.
7. Использование матрицы путаницы для более глубокого понимания производительности модели и потенциальные области для улучшения.
8. Прогнозирование пользовательских изображений с использованием обученной модели.

Эти этапы охватывают процесс работы с набором данных food101, подготовку данных, создание моделей, устранение переобучения, оценку производительности и прогнозирование.

Спасибо, что нашли время, чтобы прочитать мою статью. Надеюсь, вы нашли его познавательным и наводящим на размышления. Если вы хотите оставаться на связи и получать новости о моих будущих статьях, рассмотрите возможность подписаться на мою учетную запись. Кроме того, пожалуйста, не стесняйтесь, пишите мне сюда для любых запросов или дальнейших обсуждений. Я ценю вашу поддержку и с нетерпением жду возможности пообщаться с вами. Следите за следующей записью в блоге «Переход к модульному стилю».

А пока не стесняйтесь обращаться ко мне по адресу [email protected]
Ознакомьтесь с полным кодом этой статьи здесь: Github
Помощник по функциям можно увидеть здесь: Помощник по функциям