Учебник по классификации изображений 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, он автоматически назначит cuda в качестве содержимого переменной device. Имейте в виду, что, делая это, мы на самом деле не используем графический процессор, а просто хотим его обнаружить.

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

2. Загрузите изображения и создайте ярлыки

Как я упоминал ранее, мы собираемся использовать Cat and Dog Dataset, который общедоступен на Kaggle. Вот как выглядит структура набора данных.

Что нам нужно сделать, так это загрузить изображения из папок с именами 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, который будет превращаться в 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 в приведенном выше индикаторе выполнения на самом деле является количеством итераций, которые мы делаем при доступе ко всем файлам в папке. Но нам нужно вычесть его на 1, чтобы получить фактическое количество изображений благодаря файлу _DS_Store, который мы пропустили.

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

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

Далее нам нужно поместить все данные обучения и тестирования в один и тот же массив, чего можно просто добиться с помощью функции np.append(). Коды, написанные ниже в Codeblock 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)

Создание этикеток

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

Способ сделать это довольно просто. Помните, что первые 4000 изображений в X_train — это кошки, а остальные 4005 — собаки. Зная эту структуру, мы можем создать массив нулей и единиц с этой длиной, которые затем объединяются так же, как мы сделали с 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)

До этого момента мы успешно создавали метки для всех изображений в нашем наборе данных.

Отображение нескольких изображений

В качестве побочной задачи я также создам функцию для отображения нескольких изображений в нашем наборе данных. Функция, которую я назвал 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)

В Codeblock 10 ниже я установил start_index на 4000, так как я хочу увидеть первые 32 изображения собак.

# Codeblock 10
show_images(X_train, y_train, 4000)

3. Предварительная обработка и увеличение данных

Предварительная обработка будет выполняться как для изображений, так и для этикеток. Сначала поговорим о последнем. Метки, которые мы создали ранее, не имеют формы, приемлемой для Pytorch, когда дело доходит до обучения модели. Ниже показано, как выглядят первые 10 ярлыков в текущем y_train.

# Codeblock 11
print(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])

Предварительная обработка и увеличение изображения

Удобство работы с Pytorch заключается в том, что мы можем выполнять предварительную обработку и увеличение изображений с помощью одной функции, а именно transforms.Compose(). Что мы делаем с кодовым блоком 13 ниже, так это то, что мы преобразуем изображения (которые изначально были в виде массива Numpy) в тензор Pytorch. Значения интенсивности пикселей на изображениях также сжимаются до -1 и 1 только с использованием transforms.Normalize(). Важно знать, что я повторяю значение среднего значения и стандартного отклонения три раза, потому что исходное изображение имеет цветовые каналы 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. Детали модели можно увидеть в Codeblock 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].

После достижения последнего максимального объединяющего слоя следующим шагом будет сглаживание тензора. Полученный тензор затем пропускается через слой отсева со скоростью отбрасывания 50%. Затем этот выпадающий слой соединяется с двумя последовательными скрытыми слоями с 32 и 64 нейронами соответственно. Наконец, мы соединим выходной слой с одним нейроном. Возможно, стоит отметить, что два скрытых слоя используют функцию активации ReLU, а выходной слой использует сигмоид.

После того, как класс CNN был создан, теперь мы можем фактически инициализировать модель. Мы выполним инициализацию, используя следующий код. Не забудьте написать to(device), чтобы за нас работал GPU.

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

Если хотите, вы можете распечатать детали модели, используя функцию summary(), взятую из модуля torchinfo.

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

6. Обучение модели

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

# 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() до прямого распространения. Одной из причин этого является повторное соединение случайно разъединенных нейронов в отсевающем слое, чтобы сеть достигла максимальной производительности. Затем модель вернется в режим обучения с помощью model.train() после завершения всего процесса прогнозирования.

Цикл обучения

Поскольку функция predict_test_data() создана, теперь мы собираемся работать с обучающим циклом. Этот процесс на самом деле довольно стандартный. Будет два цикла for, один из которых повторяется для каждой эпохи, а другой — для каждой партии. И мы будем выполнять следующие операции в каждом отдельном пакете: прямое распространение, обратное распространение и градиентный спуск. Между ними также будет несколько других операций, касающихся расчета стоимости потерь и оценки точности.

В этом случае я решил запустить тренировку на 100 эпох, что заняло у меня около 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)}')

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

7. Оценка

Производительность этой модели будет оцениваться путем распечатки истории значений потерь и точности по мере продвижения эпохи. Кодовый блок 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.

Это все для этой статьи. Спасибо за прочтение!

Полностью рабочий код этого блокнота доступен по этой ссылке.

Рекомендации

[1] Тяньюй Ци, Чжэнь Лю, Ин Ван. Оценка степени повреждения здания, вызванного использованием сверточных нейронных сетей в сочетании с порядковой регрессией. Дистанционное зондирование. https://www.researchgate.net/publication/337693500_Assessment_of_the_Degree_of_Building_Damage_Caused_by_Disaster_Using_Convolutional_Neural_Networks_in_Combination_with_Ordinal_Regression [По состоянию на 28 января 2023 г.].

[2] Garikoitz Lerma Usabiaga et al., Ретроспективная оценка движения головы при структурной МРТ головного мозга с использованием 3D CNN. Исследовательские ворота. https://www.researchgate.net/publication/316974453_Retrospective_Head_Motion_Estimation_in_Structural_Brain_MRI_with_3D_CNN [По состоянию на 28 января 2023 г.].

[3] Хао Сунь, Сянтао Чжэн и Сяоцян Лу. Контролируемая сеть сегментации для классификации гиперспектральных изображений. Транзакции IEEE при обработке изображений. https://www.researchgate.net/publication/349061100_A_Supervised_Segmentation_Network_for_Hyperspectral_Image_Classification [По состоянию на 28 января 2023 г.].

[4] Удалите смещение из свертки, если за сверткой следует слой нормализации. Переполнение стека. https://stackoverflow.com/questions/64865720/remove-bias-from-the-convolution-if-the-convolution-is-followed-by-a-normalizati [По состоянию на 29 января 2023 г.].

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .

Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.