В этой записной книжке мы подробно рассмотрим, как построить простую модель глубокого обучения (ИНС) для прогнозирования меток рукописных цифр с учетом их изображения. Мы будем использовать набор данных MNIST от Kaggle для обучения модели (ссылка). Я сделаю все возможное, чтобы все было максимально просто, и объясню шаги и процесс, которым мы следуем по мере изучения этой записной книжки. Первым шагом является проверка набора данных, формата, в котором хранятся изображения, и соответствующих меток, связанных с одним и тем же файлом .

import numpy as np 
import pandas as pd 

import os
for dirname, _, filenames in os.walk('/kaggle/input'):
    for filename in filenames:
        print(os.path.join(dirname, filename))

/kaggle/input/digit-recognizer/sample_submission.csv
/kaggle/input/digit-recognizer/train.csv
/kaggle/input/digit-recognizer/test.csv

Заметим, что данные хранятся в виде csv. Мы можем быстро использовать pandas для чтения CSV в виде фрейма данных и просмотра его содержимого. Мы читаем файлы train и test, чтобы увидеть столбцы в каждом из них, и это даст нам представление о том, что такое целевая переменная.

train_df = pd.read_csv("/kaggle/input/digit-recognizer/train.csv")
test_df = pd.read_csv("/kaggle/input/digit-recognizer/test.csv")
print(train_df.shape , test_df.shape)
print("Train Cols : " , train_df.columns)
print("Test Cols : " , test_df.columns)
print(" Label -  " , [i for i in train_df.columns if i not in test_df.columns] )
(42000, 785) (28000, 784)
Train Cols :  Index(['label', 'pixel0', 'pixel1', 'pixel2', 'pixel3', 'pixel4', 'pixel5',
       'pixel6', 'pixel7', 'pixel8',
       ...
       'pixel774', 'pixel775', 'pixel776', 'pixel777', 'pixel778', 'pixel779',
       'pixel780', 'pixel781', 'pixel782', 'pixel783'],
      dtype='object', length=785)
Test Cols :  Index(['pixel0', 'pixel1', 'pixel2', 'pixel3', 'pixel4', 'pixel5', 'pixel6',
       'pixel7', 'pixel8', 'pixel9',
       ...
       'pixel774', 'pixel775', 'pixel776', 'pixel777', 'pixel778', 'pixel779',
       'pixel780', 'pixel781', 'pixel782', 'pixel783'],
      dtype='object', length=784)
 Label -   ['label']

В обучающем наборе данных 42 000 строк, а в test_dataset — 28 000 строк. Из названий столбцов делаем вывод, что картинки хранятся в виде строк, где столбец — pixel_1 указывает значение плотности пикселей в cell_no[1] в изображении . Поскольку есть значения до pixel_783, количество пикселей равно 784, что означает, что это изображение 28*28. Чтобы найти целевой столбец, мы просто проверяем столбцы в обучающем наборе данных, которые не являются частью тестового набора данных.

Создание класса набора данных для модели pytorch

Мы собираемся следовать рекомендациям pytorch по получению данных в виде набора данных, так как это упрощает нам жизнь при создании загрузчиков данных, которые впоследствии будут использоваться в процессах обучения. Здесь мы создаем собственный пользовательский класс, наследующий класс Dataset и указывающий, как получить доступ к элементам набора данных без использования метода getitem . В соответствии с этим методом мы считываем данные и сохраняем их в img_df, извлекаем изображение, преобразовываем их в массив 28 * 28 numpy, приводим значения в диапазон [0,1] и выполняем преобразования для обоих изображение и метка отдельно и возвращает их в виде тензоров X, y. Этот пользовательский класс принимает следующие аргументы

  • csv_name — Имя, под которым хранятся данные
  • img_dir — путь к каталогу, в котором хранится файл.
  • transform — Преобразования, которые необходимо выполнить в векторе изображения
  • target_transform — Преобразования, которые необходимо выполнить для целевой переменной.
import torch
from torch.utils.data import Dataset
from torchvision import datasets
from torchvision.transforms import ToTensor , Lambda

class CustomMNISTDataset(Dataset):
    def __init__(self, csv_name, img_dir, transform=None, target_transform=None , label_name = "label"):
        
        self.img_filename = csv_name
        self.img_dir = img_dir
        self.transform = transform
        self.target_transform = target_transform
        self.label_name = label_name
        
        img_path = os.path.join(self.img_dir, self.img_filename)
        self.img_df = pd.read_csv(img_path)

    def __len__(self):
        return len(self.img_df)

    def __getitem__(self, idx):
        
        # Extracting all the other columns except label_name
        img_cols = [ i for i in self.img_df.columns if i not in self.label_name]
        
        image = self.img_df.iloc[[idx]][img_cols].values

        # Reshaping the array from 1*784 to 28*28
        image = image.reshape(28,28)
        # image = image.astype(float)

        # Scaling the image so that the values only range between 0 and 1
        image = image/255.0
        
        
        if self.transform:
            image = self.transform(image)
    
        image = image.to(torch.float)    
        
        if self.label_name in self.img_df.columns:
            
            if self.target_transform:
                label = self.target_transform(label)
            label = int(self.img_df.iloc[[idx]][self.label_name].values)
            return image, label
        
        # Exceptions for test where labels are absent
        else :
            return image

Распределение целевых меток

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

train_df = pd.read_csv("/kaggle/input/digit-recognizer/train.csv")
train_df['label'].value_counts().sort_index()
0    4132
1    4684
2    4177
3    4351
4    4072
5    3795
6    4137
7    4401
8    4063
9    4188
Name: label, dtype: int64

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

Распределение меток как в поезде, так и в действительном наборе

Чтобы измерить фактическую производительность модели, необходимо создать обучающие и действительные подмножества из нашего набора training_data. Мы будем использовать только train_subset для обучения модели и проверим его производительность с действительным набором данных. Для создания действительного набора данных мы будем стратифицировать, используя метки y, чтобы гарантировать, что распределение как поезда, так и действительных подмножеств останется прежним.

## Illustration of creating a validation set 

from sklearn.model_selection import train_test_split
indices = list(range(len(train_df)))

train_indices , test_indices = train_test_split(indices, test_size=0.1, stratify=train_df['label'])
# train_indices , test_indices = train_test_split(indices, test_size=0.1)

len(train_indices) , len(test_indices) , len(train_df)

train_subset = train_df.loc[train_indices]
val_subset = train_df.loc[test_indices]

print("Distribution of target values in training dataset ; ")
print( train_subset['label'].value_counts().sort_index() / train_subset['label'].value_counts().sort_index().sum() )

print("Distribution of target values in validation dataset ; ")
print( val_subset['label'].value_counts().sort_index() / val_subset['label'].value_counts().sort_index().sum() )
Distribution of target values in training dataset ; 
0    0.098386
1    0.111534
2    0.099444
3    0.103598
4    0.096958
5    0.090344
6    0.098492
7    0.104788
8    0.096746
9    0.099709
Name: label, dtype: float64
Distribution of target values in validation dataset ; 
0    0.098333
1    0.111429
2    0.099524
3    0.103571
4    0.096905
5    0.090476
6    0.098571
7    0.104762
8    0.096667
9    0.099762
Name: label, dtype: float64

Создание набора данных поезда с использованием данных поезда

Мы продолжим и создадим набор данных train_dataset, используя класс, который мы определили выше. Что касается аргументов, мы предоставляем каталог и csv_name на основе значений, указанных ниже. Для преобразований мы используем функцию ToTensor(), которая преобразует массив numpy в тензор, и Normalize(mean, std), помогая нам нормализовать набор данных с заданным средним значением и стандартным отклонением, чтобы значения не превышали пропорцию pf. Мы сохраняем значение target_transform как None, так как ничего не нужно делать с целевой переменной.

from torchvision import transforms

# Crerating a temp dataset
train_csv_name = "train.csv"
test_csv_name = "test.csv"
img_dir = "/kaggle/input/digit-recognizer/"

# Converting X variables to Tensors
transforms = transforms.Compose( [transforms.ToTensor() , transforms.Normalize((0.5,), (0.5,)) , ] )

# Converting y-labels to one hot encoding
# target_transform = Lambda(lambda y: torch.zeros(
#     len(train_df['label'].unique()), dtype=torch.float).scatter_(dim=0, index=torch.tensor(y), value=1))
# This is not need since we are going to be using cross entropy loss function

label_name = "label"

train_dataset = CustomMNISTDataset(csv_name = train_csv_name , img_dir = img_dir , transform = transforms , target_transform = None , label_name = label_name)

# Inspecting the fist line item under dataset
x0 , y0 = train_dataset[0]
print(x0.shape , y0)
torch.Size([1, 28, 28]) 1

Построение точек данных train_dataset

# Ploting some of the datapoints in the dataset
import matplotlib.pyplot as plt

# sample_img , sample_lbl = temp_train_dataset[3]
figure = plt.figure(figsize=(8, 8))
cols, rows = 3, 3
figure.add_subplot(rows, cols, 1)
for i in range(1, cols * rows + 1):
    sample_idx = torch.randint(len(train_dataset), size=(1,)).item()
    sample_img , sample_lbl = train_dataset[sample_idx]
    figure.add_subplot(rows, cols, i)
    plt.title(sample_lbl)
    plt.axis("off")
    plt.imshow(sample_img.squeeze(), cmap="gray")
plt.show()

Проверяем, есть ли на устройстве графический процессор, если нет, мы выполняем весь процесс обучения на процессоре (недостаток в том, что он может быть очень медленным)

## Checking if the GPU is being used properly . 

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print('Using device:', device)
torch.cuda.is_available()
x0 = x0.to(device)
print("x0" , x0.is_cuda)
Using device: cpu
x0 False

Создание тестовых и действительных подмножеств с использованием стратифицированных выборок

Затем мы приступаем к разделению набора training_dataset на обучающие и действительные подмножества, как определено выше, с использованием стратифицированной выборки. Мы сохраняем действительный набор как 10% от training_dataset . Обратите внимание, что когда мы смотрим на содержимое train_dataloaders , размерность равна (torch.Size([64, 1, 28, 28]), torch.Size([64]) ), который отличается от содержимого train_dataset torch.Size([1, 28, 28]), int). По сути, это пакет, который обрабатывается во время обучения.

from torch.utils.data import SubsetRandomSampler
from sklearn.model_selection import train_test_split
indices = list(range(len(train_df)))
train_indices , valid_indices = train_test_split(indices, test_size=0.1, stratify=train_df['label'])

# Creating PT data samplers and loaders:
train_sampler = SubsetRandomSampler(train_indices)
valid_sampler = SubsetRandomSampler(test_indices)

train_dataloader = torch.utils.data.DataLoader(train_dataset , batch_size=64, sampler=train_sampler, num_workers=16)
valid_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=64, sampler=valid_sampler, num_workers=16)

x0 , y0 = next(iter(train_dataloader))
x0.shape , y0.shape
/opt/conda/lib/python3.7/site-packages/torch/utils/data/dataloader.py:490: UserWarning: This DataLoader will create 16 worker processes in total. Our suggested max number of worker in current system is 4, which is smaller than what this DataLoader is going to create. Please be aware that excessive worker creation might get DataLoader running slow or even freeze, lower the worker number to avoid potential slowness/freeze if necessary.
  cpuset_checked))





(torch.Size([64, 1, 28, 28]), torch.Size([64]))

Определение модели NN

Теперь, когда мы закончили определение загрузчиков данных, мы переходим к определению деталей нейронной сети, которые будут использоваться для прогнозов. Мы используем архитектуру с 2 скрытыми слоями и с выходным слоем, имеющим 10 узлов (поскольку существует 10 различных классов для предсказания). Мы определяем наш наш класс, наследующий nn.Module, чтобы определить нашу собственную функцию прямого распространения, поскольку это считается лучшей практикой при работе с pytorch.

Модель будет выглядеть следующим образом:

  • Мы сглаживаем наше изображение 28 * 28 в тензор длины 784 (что достигается с помощью nn.Flatten)
  • Затем мы вводим их в скрытый слой, содержащий 128 узлов, который затем подключается к другому скрытому слою с 64 узлами. Мы используем Relu в качестве функции активации между слоями.
  • Наконец, мы соединяем скрытый слой со слоем, имеющим 10 узлов (эквивалентно количеству меток).

Обратите внимание, что в конце NN нет слоя softmax. Это связано с тем, что nn.CrossEntropyLoss() автоматически применяет softmax из полученных выходных данных для расчета потерь . Однако, если мы используем nn.NLLLLoss() в качестве функции потери, нам придется включить nn.LogSoftmax в конце nn.Sequential(..)

from torch import nn

# Get cpu or gpu device for training.
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using {device} device")

class MyOwnNeuralNetwork(nn.Module):
    def __init__(self):
        super(MyOwnNeuralNetwork, self).__init__()
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(784, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 10)
            ## Softmax layer ignored since the loss function defined is nn.CrossEntropy()
        )

    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return  logits
    
    
model = MyOwnNeuralNetwork().to(device)
print(model)

# model = model.cuda()
# torch.backends.cudnn.benchmark=True
# torch.cuda.set_device(0)
Using cpu device
MyOwnNeuralNetwork(
  (flatten): Flatten(start_dim=1, end_dim=-1)
  (linear_relu_stack): Sequential(
    (0): Linear(in_features=784, out_features=128, bias=True)
    (1): ReLU()
    (2): Linear(in_features=128, out_features=64, bias=True)
    (3): ReLU()
    (4): Linear(in_features=64, out_features=10, bias=True)
  )
)

Определение оптимизаторов и функции потерь

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

## Defining optimizer and loss functions 

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=3e-3, momentum=0.9)

Определение цикла поезда

В рамках тренировочного цикла мы делаем следующие вещи.

  • Поместите модель в режим поезда
  • перечислить содержимое загрузчика данных, который дает нам X, y в виде пакетов
  • Для каждой партии
  1. Мы используем модель, чтобы делать прогнозы X
  2. Вычислите потери, используя предсказанные и фактические значения y
  3. Сбросьте значения градиента на 0 .
  4. Выполнить обратное распространение
  5. Обновите параметры, используя вычисленные градиенты обратного распространения.
  6. Вычислите точность обучения и потери с помощью предсказанного и фактического y .
from torch.autograd import Variable
from torch.utils.tensorboard import SummaryWriter
def train(dataloader, model, loss_fn, optimizer):
    
    # Total size of dataset for reference
    size = 0
    
    # places your model into training mode
    model.train()
    
    # loss batch
    batch_loss = {}
    batch_accuracy = {}
    
    correct = 0
    _correct = 0
    
    
    
    # Gives X , y for each batch
    for batch, (X, y) in enumerate(dataloader):
        
        # Converting device to cuda
        X, y = X.to(device), y.to(device)
        model.to(device)
        
        # Compute prediction error / loss
        # 1. Compute y_pred 
        # 2. Compute loss between y and y_pred using selectd loss function
        
        y_pred = model(X)
        loss = loss_fn(y_pred, y)

        # Backpropagation on optimizing for loss
        # 1. Sets gradients as 0 
        # 2. Compute the gradients using back_prop
        # 3. update the parameters using the gradients from step 2
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        _correct = (y_pred.argmax(1) == y).type(torch.float).sum().item()
        _batch_size = len(X)
        
        correct += _correct
        
        # Updating loss_batch and batch_accuracy
        batch_loss[batch] = loss.item()
        batch_accuracy[batch] = _correct/_batch_size
        
        size += _batch_size
        
        if batch % 100 == 0:
            loss, current = loss.item(), batch * len(X)
            print(f"loss: {loss:>7f}  [{current:>5d}]")
    
    correct/=size
    print(f"Train Accuracy: {(100*correct):>0.1f}%")
    
    return batch_loss , batch_accuracy

Определение допустимого/тестового цикла

Цикл проверки аналогичен циклу обучения, игнорируя обновления от обратного распространения. Следовательно, нам нужно установить модель в режим оценки, чтобы избежать обновления каких-либо параметров. Затем мы вычисляем потери и точность так же, как и в тренировочном цикле.

def validation(dataloader, model, loss_fn):
    
    # Total size of dataset for reference
    size = 0
    num_batches = len(dataloader)
    
    # Setting the model under evaluation mode.
    model.eval()

    test_loss, correct = 0, 0
    
    _correct = 0
    _batch_size = 0
    
    batch_loss = {}
    batch_accuracy = {}
    
    with torch.no_grad():
        
        # Gives X , y for each batch
        for batch , (X, y) in enumerate(dataloader):
            
            X, y = X.to(device), y.to(device)
            model.to(device)
            pred = model(X)
            
            batch_loss[batch] = loss_fn(pred, y).item()
            test_loss += batch_loss[batch]
            _batch_size = len(X)
            
            _correct = (pred.argmax(1) == y).type(torch.float).sum().item()
            correct += _correct
            
            size+=_batch_size
            batch_accuracy[batch] = _correct/_batch_size
            
            
            
    
    ## Calculating loss based on loss function defined
    test_loss /= num_batches
    
    ## Calculating Accuracy based on how many y match with y_pred
    correct /= size
    
    print(f"Valid Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
    
    return batch_loss , batch_accuracy

Триггеры обучения и проверки

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

train_batch_loss = []
train_batch_accuracy = []
valid_batch_accuracy = []
valid_batch_loss = []
train_epoch_no = []
valid_epoch_no = []

epochs = 10
for t in range(epochs):
    print(f"Epoch {t+1}\n-------------------------------")
    _train_batch_loss , _train_batch_accuracy = train(train_dataloader, model, loss_fn, optimizer)
    _valid_batch_loss , _valid_batch_accuracy = validation(valid_dataloader, model, loss_fn)
    for i in range(len(_train_batch_loss)):
        train_batch_loss.append(_train_batch_loss[i])
        train_batch_accuracy.append(_train_batch_accuracy[i])
        train_epoch_no.append( t + float((i+1)/len(_train_batch_loss)))     
    for i in range(len(_valid_batch_loss)):
        valid_batch_loss.append(_valid_batch_loss[i])
        valid_batch_accuracy.append(_valid_batch_accuracy[i])
        valid_epoch_no.append( t + float((i+1)/len(_valid_batch_loss)))     
print("Done!")
Epoch 1
-------------------------------


loss: 2.319053  [    0]
loss: 1.471163  [ 6400]
loss: 0.675529  [12800]
loss: 0.590934  [19200]
loss: 0.417268  [25600]
loss: 0.295707  [32000]
Train Accuracy: 78.2%
Valid Error: 
 Accuracy: 89.1%, Avg loss: 0.360749 

Epoch 2
-------------------------------
loss: 0.281806  [    0]
loss: 0.343148  [ 6400]
loss: 0.358678  [12800]
loss: 0.313208  [19200]
loss: 0.232435  [25600]
loss: 0.229005  [32000]
Train Accuracy: 90.4%
Valid Error: 
 Accuracy: 91.8%, Avg loss: 0.269360 

Epoch 3
-------------------------------
loss: 0.225520  [    0]
loss: 0.309443  [ 6400]
loss: 0.107599  [12800]
loss: 0.335566  [19200]
loss: 0.262280  [25600]
loss: 0.398198  [32000]
Train Accuracy: 92.0%
Valid Error: 
 Accuracy: 92.6%, Avg loss: 0.237691 

Epoch 4
-------------------------------
loss: 0.217512  [    0]
loss: 0.286153  [ 6400]
loss: 0.261390  [12800]
loss: 0.193834  [19200]
loss: 0.286631  [25600]
loss: 0.260825  [32000]
Train Accuracy: 93.1%
Valid Error: 
 Accuracy: 93.9%, Avg loss: 0.194569 

Epoch 5
-------------------------------
loss: 0.279033  [    0]
loss: 0.147660  [ 6400]
loss: 0.164245  [12800]
loss: 0.217309  [19200]
loss: 0.290423  [25600]
loss: 0.254665  [32000]
Train Accuracy: 93.9%
Valid Error: 
 Accuracy: 94.6%, Avg loss: 0.174962 

Epoch 6
-------------------------------
loss: 0.228728  [    0]
loss: 0.287362  [ 6400]
loss: 0.324856  [12800]
loss: 0.186807  [19200]
loss: 0.154871  [25600]
loss: 0.200961  [32000]
Train Accuracy: 94.8%
Valid Error: 
 Accuracy: 95.6%, Avg loss: 0.145897 

Epoch 7
-------------------------------
loss: 0.131066  [    0]
loss: 0.056263  [ 6400]
loss: 0.070663  [12800]
loss: 0.347360  [19200]
loss: 0.106663  [25600]
loss: 0.289104  [32000]
Train Accuracy: 95.4%
Valid Error: 
 Accuracy: 96.1%, Avg loss: 0.130608 

Epoch 8
-------------------------------
loss: 0.106794  [    0]
loss: 0.047672  [ 6400]
loss: 0.142277  [12800]
loss: 0.091740  [19200]
loss: 0.120352  [25600]
loss: 0.260128  [32000]
Train Accuracy: 95.9%
Valid Error: 
 Accuracy: 96.0%, Avg loss: 0.127156 

Epoch 9
-------------------------------
loss: 0.147196  [    0]
loss: 0.108950  [ 6400]
loss: 0.117143  [12800]
loss: 0.199887  [19200]
loss: 0.079983  [25600]
loss: 0.113003  [32000]
Train Accuracy: 96.3%
Valid Error: 
 Accuracy: 96.4%, Avg loss: 0.122171 

Epoch 10
-------------------------------
loss: 0.060340  [    0]
loss: 0.395733  [ 6400]
loss: 0.068480  [12800]
loss: 0.184037  [19200]
loss: 0.121428  [25600]
loss: 0.268083  [32000]
Train Accuracy: 96.7%
Valid Error: 
 Accuracy: 97.1%, Avg loss: 0.092871 

Done!

Мы видим приличную точность проверки выше 95% только с использованием нейронной сети из 2 скрытых слоев с 128 и 64 единицами. Для дальнейшего повышения точности модели можно использовать сверточную нейронную сеть, поскольку она работает намного лучше, когда речь идет о наборах данных изображений.

figure = plt.figure(figsize=(16, 16))


figure.add_subplot(2, 2, 1)
plt.plot(train_epoch_no , train_batch_accuracy)
plt.title("Train Batch Accuracy")
plt.xlabel("Epochs") 
plt.ylabel("Train Accuracy") 

figure.add_subplot(2, 2, 2)
plt.plot(train_epoch_no , train_batch_loss)
plt.title("Train Batch Loss")
plt.xlabel("Epochs") 
plt.ylabel("Train Loss") 

figure.add_subplot(2, 2, 3)
plt.plot(valid_epoch_no , valid_batch_accuracy)
plt.title("Valid Batch Accuracy")
plt.xlabel("Epochs") 
plt.ylabel("Train Accuracy") 

figure.add_subplot(2, 2, 4)
plt.plot(valid_epoch_no , valid_batch_loss)
plt.title("Valid Batch Loss")
plt.xlabel("Epochs") 
plt.ylabel("Train Loss") 


plt.show()

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

Загрузка тестовых данных для прогнозов

from torchvision import transforms

test_csv_name = "test.csv"
img_dir = "/kaggle/input/digit-recognizer/"

# Converting X variables to Tensors
transforms = transforms.Compose( [transforms.ToTensor() , transforms.Normalize((0.5,), (0.5,)) , ] )
label_name = "label"

test_dataset = CustomMNISTDataset(csv_name = test_csv_name , img_dir = img_dir , transform = transforms , target_transform = None , label_name = label_name)

Создание прогнозов на тестовых данных и их сохранение

probas = []
predictions = []
for i in range(len(test_dataset)):
    _probas = model(test_dataset[i])
    probas.append(_probas)
    predictions.append(_probas.argmax(1).item())
# having a look at the format of submission
pd.read_csv("/kaggle/input/digit-recognizer/sample_submission.csv")

# Saving the dataframe for submission
df = pd.DataFrame({'ImageId':range(1,len(predictions)+1),'Label':predictions})
df.to_csv("submission.csv",index=False)

Если вы дочитали до конца, то вот ссылка на мой блокнот на kaggle. Kaggle — замечательная платформа, на которой пользователи могут экспериментировать с различными наборами данных и пробовать новые вещи. Лучшее в kaggle то, что вам не нужно запускать свои модели машинного обучения на своей собственной машине. Kaggle предоставляет свои ядра для запуска наших блокнотов и отправки материалов непосредственно из них. Не стесняйтесь просматривать блокноты и запускать их на ядрах Kaggle шаг за шагом, чтобы лучше понять, что происходит на каждом этапе. если вам понравился контент, не забудьте поставить лайк :). Спасибо и до свидания до следующего раза.