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

Вы можете запустить этот блокнот на Kaggle.

Содержание:

  • Импорт
  • Подготовка данных
  • Исследуйте данные
  • Модель
  1. Simple Feed Forward NN (точность теста 96%)
  2. CNN на основе LeNet5 (точность теста 98%)
  • Подготовка тестовых данных
  • Глядя на прогноз
  • Экспорт данных
  • Ансамбли (точность теста 98%)
  • История коммитов

Импорт

# Basic Torch
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import torchvision
from torch.utils.data import TensorDataset
from torch.optim import Adam, SGD
# Basic Numeric Computation
import numpy as np
import pandas as pd
# Look at data
from matplotlib import pyplot
# Easy way to split train data
from sklearn.model_selection import train_test_split
# Looking at directory
import os
base_dir = "../input"
print(os.listdir(base_dir))
device = torch.device("cpu")# if torch.cuda.is_available() else torch.device("cpu")
device
epochs=10

Вывод:

['train.csv', 'sample_submission.csv', 'test.csv']

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

Извлечение нагрузки преобразования (ETL)

1. Извлечение

train = pd.read_csv(base_dir + '/train.csv')
test = pd.read_csv(base_dir + '/test.csv')
train.head()

2. Преобразование

# Convert Dataframe into format ready for training
def createImageData(raw: pd.DataFrame):
    y = raw['label'].values
    y.resize(y.shape[0],1)
    x = raw[[i for i in raw.columns if i != 'label']].values
    x = x.reshape([-1,1, 28, 28])
    y = y.astype(int).reshape(-1)
    x = x.astype(float)
    return x, y
## Convert to One Hot Encoding
def one_hot_embedding(labels, num_classes=10):
    y = torch.eye(num_classes) 
    return y[labels]
x_train, y_train = createImageData(train)
#x_train, x_val, y_train, y_val = train_test_split(x,y, test_size=0.02)
#x_train.shape, y_train.shape, x_val.shape, y_val.shape
x_train.shape, y_train.shape

Вне:

((42000, 1, 28, 28), (42000,))
# Normalization
mean = x_train.mean()
std = x_train.std()
x_train = (x_train-mean)/std
#x_val = (x_val-mean)/std
# Numpy to Torch Tensor
x_train = torch.from_numpy(np.float32(x_train)).to(device)
y_train = torch.from_numpy(y_train.astype(np.long)).to(device)
y_train = one_hot_embedding(y_train)
#x_val = torch.from_numpy(np.float32(x_val))
#y_val = torch.from_numpy(y_val.astype(np.long))

3. Загрузка

# Convert into Torch Dataset
train_ds = TensorDataset(x_train, y_train)
#val_ds = TensorDataset(x_val,y_val)

Вывод:

# Make Data Loader
train_dl = DataLoader(train_ds, batch_size=64)

Исследуйте данные

Всегда полезно просмотреть свои данные перед построением модели. Это помогает понять, с чем будет иметь дело модель, и устраняет предположения, которые вы могли неосознанно ввести в свою модель.

index = 1
pyplot.imshow(x_train.cpu()[index].reshape((28, 28)), cmap="gray")
print(y_train[index])

Вывод:

tensor([1., 0., 0., 0., 0., 0., 0., 0., 0., 0.])

Модель

Ниже приведены вспомогательные функции. Первоначально они были написаны как обычные функции Pytorch, но позже были абстрагированы, чтобы сделать код чистым и легко использовать повторно позже.

# Helper Functions
## Initialize weight with xavier_uniform
def init_weights(m):
    if type(m) == nn.Linear:
        torch.nn.init.xavier_uniform(m.weight)
        m.bias.data.fill_(0.01)
## Flatten Later
class Flatten(nn.Module):
    def forward(self, input):
        return input.view(input.size(0), -1)
# Train the network and print accuracy and loss overtime
def fit(train_dl, model, loss, optim, epochs=10):
    model = model.to(device)
    print('Epoch\tAccuracy\tLoss')
    accuracy_overtime = []
    loss_overtime = []
    for epoch in range(epochs):
        avg_loss = 0
        correct = 0
        total=0
        for x, y in train_dl: # Iterate over Data Loder
    
            # Forward pass
            yhat = model(x) 
            l = loss(y, yhat)
            
            #Metrics
            avg_loss+=l.item()
            
            # Backward pass
            optim.zero_grad()
            l.backward()
            optim.step()
            
            # Metrics
            _, original =  torch.max(y, 1)
            _, predicted = torch.max(yhat.data, 1)
            total += y.size(0)
            correct = correct + (original == predicted).sum().item()
            
        accuracy_overtime.append(correct/total)
        loss_overtime.append(avg_loss/len(train_dl))
        print(epoch,accuracy_overtime[-1], loss_overtime[-1], sep='\t')
    return accuracy_overtime, loss_overtime
# Plot Accuracy and Loss of Model
def plot_accuracy_loss(accuracy, loss):
    f = pyplot.figure(figsize=(15,5))
    ax1 = f.add_subplot(121)
    ax2 = f.add_subplot(122)
    ax1.title.set_text("Accuracy over epochs")
    ax2.title.set_text("Loss over epochs")
    ax1.plot(accuracy)
    ax2.plot(loss, 'r:')
# Take an array and show what model predicts 
def predict_for_index(array, model, index):
    testing = array[index].view(1,28,28)
    pyplot.imshow(x_train[index].reshape((28, 28)), cmap="gray")
    print(x_train[index].shape)
    a = model(testing.float())
    print('Prediction',torch.argmax(a,1))

Нейронная сеть прямого распространения с двумя скрытыми слоями

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

Кроме того, если вы увеличите эпоху за пределы 10, вы обнаружите, что модель быстро переобучается.

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

Я буду делать Convolutional NN позже, что потребует лучшей настройки гиперпараметров.

Вход в эту нейронную сеть с прямой связью составляет 784 функции. Целевые метки для прогнозирования — 10. Я интуитивно выбираю 100 как количество скрытых единиц в слое, потому что это золотая середина между 784 и 10.

(m, 784) -> (m, 100) -> (m,10)

m: количество изделий в мини-партии

# Define the model
ff_model = nn.Sequential(
    Flatten(),
    nn.Linear(28*28, 100),
    nn.ReLU(),
    nn.Linear(100, 10),
    nn.Softmax(1),
).to(device)
# Initialize model with xavier initialization which is recommended for ReLu
ff_model.apply(init_weights)

Выход[12]:

Sequential(
  (0): Flatten()
  (1): Linear(in_features=784, out_features=100, bias=True)
  (2): ReLU()
  (3): Linear(in_features=100, out_features=10, bias=True)
  (4): Softmax()
)

Определить гиперпараметры и обучить

optim = Adam(ff_model.parameters())
loss = nn.MSELoss()
output = fit(train_dl, ff_model, loss, optim, epochs)
plot_accuracy_loss(*output)

Вывод:

Epoch	Accuracy	Loss
0	0.9061666666666667	0.013730582292927031
1	0.9598333333333333	0.006338598204933814
2	0.9694047619047619	0.004766992041984232
3	0.9772857142857143	0.003731012495671476
4	0.9806666666666667	0.003179915646412032
5	0.9833571428571428	0.002766264982197474
6	0.9863571428571428	0.002329402380182783
7	0.9866428571428572	0.002256228744125365
8	0.9878333333333333	0.0020849015744137766
9	0.9884285714285714	0.0019292909375259308

Глядя на прогноз, который делает модель

index = 4
predict_for_index(x_train, ff_model, index)

Вывод:

torch.Size([1, 28, 28])
Prediction tensor([0])

Сверточный NN

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

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

Эта модель имеет точность 0,98942.

Исходники для создания модели:

# A too simple NN taken from pytorch.org/tutorials
class Mnist_CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1)
        self.conv2 = nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1)
        self.conv3 = nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1)
    def forward(self, xb):
        xb = xb.view(-1, 1, 28, 28)
        xb = F.relu(self.conv1(xb))
        xb = F.relu(self.conv2(xb))
        xb = F.relu(self.conv3(xb))
        xb = F.avg_pool2d(xb, 4)
        return xb.view(-1, xb.size(1))
class LeNet5(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5, stride=1)
        self.average1 = nn.AvgPool2d(2, stride=2)
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5, stride=1)
        self.average2 = nn.AvgPool2d(2, stride=2)
        self.conv3 = nn.Conv2d(16, 120, kernel_size=4, stride=1)
        
        self.flatten = Flatten()
        
        self.fc1 = nn.Linear(120, 82)
        self.fc2 = nn.Linear(82,10)
    def forward(self, xb):
        xb = xb.view(-1, 1, 28, 28)
        xb = F.tanh(self.conv1(xb))
        xb = self.average1(xb)
        xb = F.tanh(self.conv2(xb))
        xb = self.average2(xb)
        xb = F.tanh(self.conv3(xb))
        xb = xb.view(-1, xb.shape[1])
        xb = F.relu(self.fc1(xb))
        xb = F.relu(self.fc2(xb))
        return xb

In [17]:

conv_model = LeNet5()
conv_model.apply(init_weights)
loss = nn.MSELoss()
optim = SGD(conv_model.parameters(), lr=0.1, momentum=0.9)
plot_accuracy_loss(*fit(train_dl, conv_model,loss,optim,epochs)

Вывод:

Epoch	Accuracy	Loss
0	0.8568095238095238	0.022169289579246437
1	0.964	0.00809634799903891
2	0.9754285714285714	0.006067726472456468
3	0.980452380952381	0.00498412443012002
4	0.9839761904761904	0.004297092746283372
5	0.9863809523809524	0.0038011955834260347
6	0.9877619047619047	0.003426484330387182
7	0.9887142857142858	0.0031120370952284907
8	0.9897857142857143	0.0028682300103583785
9	0.9905952380952381	0.0026617846957140688

Подготовка тестовых данных

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

In [18]:

x_test = test.values
x_test = x_test.reshape([-1, 28, 28]).astype(float)
x_test = (x_test-mean)/std
x_test = torch.from_numpy(np.float32(x_test))
x_test.shape

Вышли[18]:

torch.Size([28000, 28, 28])

Глядя на прогноз

In [19]:

index = 7
predict_for_index(x_test, ff_model, index)
predict_for_index(x_test, conv_model, index)

Вне:

torch.Size([1, 28, 28])
Prediction tensor([3])
torch.Size([1, 28, 28])
Prediction tensor([3])

Экспорт данных для отправки

In [20]:

# Export data to CSV in format of submission
def export_csv(model_name, predictions, commit_no):
    df = pd.DataFrame(prediction.tolist(), columns=['Label'])
    df['ImageId'] = df.index + 1
    file_name = f'submission_{model_name}_v{commit_no}.csv'
    print('Saving ',file_name)
    df[['ImageId','Label']].to_csv(file_name, index = False)

In [21]:

test.head()

In [22]:

# just to make output easier to read
commit_no = 7

In [23]:

ff_test_yhat = ff_model(x_test.float())
prediction = torch.argmax(ff_test_yhat,1)
print('Prediction',prediction)
export_csv('ff_model',prediction, commit_no=commit_no)

Вне:

Prediction tensor([2, 0, 9,  ..., 3, 9, 2])
Saving  submission_ff_model_v7.csv

In [24]:

cn_train_yhat = conv_model(x_test)
prediction = torch.argmax(cn_train_yhat,1)
yo = torch.argmax(y_train,1)
export_csv('lenet_model',prediction, commit_no=commit_no)

Вне:

Saving  submission_lenet_model_v7.csv

Ансамбль

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

В этом конкретном случае кажется, что LeNet был намного лучше, чем сеть с прямой связью, поэтому точность ансамбля на тестовых данных такая же, как у LeNet5–0,98942

In [25]:

ensemble = ff_test_yhat + cn_train_yhat # Add probabilities of individual predictions
ensemble_one_hot = torch.argmax(y_train,1) # Find argmax
export_csv('ensemble',ensemble_one_hot, commit_no=commit_no)

Вне:

Saving  submission_ensemble_v7.csv

Вы можете запустить этот блокнот на Kaggle.

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