<jupyter_start><jupyter_text>**Simpsons classifier***By Vizgalov_Anton*---Разобрал пример из baseline, получил маленький score, решил взять предобученную Resnet-152 и сделать fine-tuning.Экспериментировал с аугментацией картинок через imgaug, но score это не улучшало, поэтому в финальной версии ее убрал.  *На Kaggle получил private score = 0.94092* Часть кода взята из https://pytorch.org/tutorials/beginner/transfer_learning_tutorial.html<jupyter_code># установить необходимые модули

!pip install -U torch numpy torchvision matplotlib pandas sklearn
# примонтировать Google Drive

from google.colab import drive
drive.mount('/content/drive')
# Для обучения используется расширенный датасет, в который были добавлены
# картинки для редких классов. Картинки получал путем image scraping-а по
# Google Images. На валидации результат улучшился значительно, но
# на тесте почти не изменился.
#
# Датасет скачивается отдельно.
!unzip -q /content/drive/My\ Drive/Simpsons/data/trainset.zip

# тестовый датасет без изменений
!unzip -q /content/drive/My\ Drive/simpsons/data/testset.zip -d test
# подключить необходимые модули

import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
import numpy as np
import torchvision
from torchvision import datasets, models, transforms
from torch.utils.data.sampler import SubsetRandomSampler
from sklearn.metrics import f1_score, classification_report, confusion_matrix
from sklearn.metrics import precision_recall_fscore_support as score
import warnings
import matplotlib.pyplot as plt
import pandas as pd
import time
import os
import copy
import random

plt.ion()   # interactive mode
# Приведение картинок к нужному размеру, нормализация и простейшая аугментация.

data_transforms = {
    'train': transforms.Compose([
        transforms.RandomResizedCrop(224),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'val': transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'test': transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])
}
# папка с датасетами
data_dir = '/content/drive/My Drive/Simpsons/data'

# прочитать и сохранить названия файлов из тестового датасета (пригодится для
# формирования таблички submission.csv)
files_list = []
for r, d, files in os.walk(data_dir + '/test'):
  files_list.extend(files)
sorted_files = sorted(files_list)

# вывести статистику по числу файлов на каждый класс (помогает узнать какие
# классы редкие)
file_num_info = [(len(files), r) for r, d, files in os.walk(data_dir + '/train')]
for num, file in sorted(file_num_info, key=lambda x: x[0]):
  print(num, '|', file.split('/')[-1])
# датасеты
train_dataset = datasets.ImageFolder(os.path.join(data_dir, 'train'),
                                     data_transforms['train'])
val_dataset = datasets.ImageFolder(os.path.join(data_dir, 'train'),
                                     data_transforms['val'])
test_dataset = datasets.ImageFolder(os.path.join(data_dir, 'test'),
                                     data_transforms['test'])

# перемешать индексы для того чтобы картинки брались в случайном порядке
indices = list(range(len(train_dataset)))
random.shuffle(indices)
train_indices, val_indices = indices[len(indices)//4:], indices[:len(indices)//4]

# сэмплеры
train_sampler = SubsetRandomSampler(train_indices)
val_sampler = SubsetRandomSampler(val_indices)

# индексы для тест-сета, уже без перемешивания
test_indices = list(range(len(test_dataset)))
test_sampler = SubsetRandomSampler(test_indices)

# лоадеры
# batch_size=32 оказался оптимальным
dataloaders = {'train': torch.utils.data.DataLoader(train_dataset,
                                                    sampler=train_sampler,
                                                    batch_size=32),
               'val': torch.utils.data.DataLoader(val_dataset,
                                                  sampler=val_sampler,
                                                  batch_size=32),
               'test': torch.utils.data.DataLoader(test_dataset,
                                                   sampler=test_sampler,
                                                   batch_size=1)
               }

# конечные размеры датасетов
dataset_sizes = {'train': len(train_indices),
                 'val': len(val_indices),
                 'test': len(test_indices)}

# названия классов и их количество
class_names = train_dataset.classes
num_classes = len(class_names)

# выбор устройства для запуска (предпочтительно GPU)
if torch.cuda.is_available():
  device = torch.device("cuda:0")
  print('Training on GPU')
else:
  device = torch.device("cpu")
  print('Training on CPU')
# функция обучения модели

def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
    since = time.time()

    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0

    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch + 1, num_epochs))
        print('-' * 10)

        # Each epoch has a training and validation phase
        for phase in ['train', 'val']:
            print('Start {} phase'.format(phase))
            if phase == 'train':
                model.train()  # Set model to training mode
            else:
                model.eval()   # Set model to evaluate mode

            running_loss = 0.0
            running_corrects = 0

            # Iterate over data.
            i = 1
            for inputs, labels in dataloaders[phase]:
                # показывает сколько батчей уже обработано
                print('Processing input #{} of {} [{:4f}%]' \
                  .format(i, len(dataloaders[phase]), i/len(dataloaders[phase])*100))
                i += 1

                inputs = inputs.to(device)
                labels = labels.to(device)

                # zero the parameter gradients
                optimizer.zero_grad()

                # forward
                # track history if only in train
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    _, preds = torch.max(outputs, 1)
                    loss = criterion(outputs, labels)

                    # backward + optimize only if in training phase
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                # statistics
                running_loss += loss.item() * inputs.size(0)
                running_corrects += torch.sum(preds == labels.data)
            if phase == 'train':
                scheduler.step()

            epoch_loss = running_loss / dataset_sizes[phase]
            epoch_acc = running_corrects.double() / dataset_sizes[phase]

            print('{} Loss: {:.4f} Acc: {:.4f}'.format(
                phase, epoch_loss, epoch_acc))

            # deep copy the model
            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())

        print()

    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))
    print('Best val Acc: {:4f}'.format(best_acc))

    # load best model weights
    model.load_state_dict(best_model_wts)
    return model
# используется предобученная Resnet-152
model_ft = models.resnet152(pretrained=True)
num_ftrs = model_ft.fc.in_features

# добавляется слой, соответствующий числу классов (у нас 42)
model_ft.fc = nn.Linear(num_ftrs, num_classes)

model_ft = model_ft.to(device)
criterion = nn.CrossEntropyLoss()

# в основном использовал lr=0.001, но lr=0.0001 в некоторых экспериментах
# помогал улучшить результат
optimizer_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9)

exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)
# Загрузить сохраненные веса.

model_ft.load_state_dict(torch.load("/content/drive/My Drive/Simpsons/model_weights_extdataset_12epoch.pth"))
model_ft.eval() # переключаем нейросеть в режим обучения
# обучение модели (оптимально 10-12 эпох)
model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler,
                       num_epochs=12)

# сохранение весов
model_weights = copy.deepcopy(model_ft.state_dict())
torch.save(model_weights, "model_weights_new.pth")<jupyter_output><empty_output><jupyter_text>**Calculate F1 score**<jupyter_code># функция для получения результата классификации

def predict(model, test_loader):
    with torch.no_grad():
        logits = []

        i = 1   
        for inputs in test_loader:
            # выводит информацию о прогрессе
            print('Predicting #{} of {}'.format(i, len(test_loader)))
            i += 1
            
            inputs = inputs.to(device)
            model.eval()
            outputs = model(inputs).cpu()
            logits.append(outputs)
            
    probs = nn.functional.softmax(torch.cat(logits), dim=-1).numpy()
    return probs
# берутся все картинки из валидационного сета и для них
# проводится классификация

imgs = [val_dataset[id][0].unsqueeze(0) for id in val_indices]
probs_ims = predict(model_ft, imgs)
# берется top-1 prediction для каждой картинки, затем каждому индексу класса
# сопоставляется слово - название класса

y_pred = np.argmax(probs_ims, -1)
actual_labels = [val_dataset[id][1] for id in val_indices]
preds_class = [class_names[i] for i in y_pred]
with warnings.catch_warnings():     # отключаются предупреждения о "нулевых"
  warnings.simplefilter("ignore")   # предсказаниях
  print('Score:', f1_score(actual_labels,                     # выводится f1-
                           y_pred,                            # score и отчет
                           labels=np.unique(actual_labels),   # по конкретным
                           average='macro'))                  # классам
  print(classification_report(actual_labels, y_pred))<jupyter_output>Score: 0.9599636662734239
              precision    recall  f1-score   support

           0       1.00      1.00      1.00       218
           1       1.00      0.75      0.86        12
           2       1.00      1.00      1.00       155
           3       1.00      0.93      0.96        29
           4       1.00      1.00      1.00       355
           5       1.00      1.00      1.00        21
           6       0.98      0.98      0.98       299
           7       0.98      1.00      0.99       232
           8       1.00      0.89      0.94         9
           9       0.97      0.99      0.98       127
          10       1.00      1.00      1.00        11
          11       0.97      0.99      0.98       109
          12       1.00      1.00      1.00         6
          13       1.00      0.20      0.33         5
          14       1.00      0.97      0.98        30
          15       0.99      0.99      0.99       548
          16       1.00      0.99      0.99       144
 [...]<jupyter_text>**Submit to Kaggle**<jupyter_code># загрузить картинки из тестового датасета

test_imgs = [test_dataset[id][0].unsqueeze(0)
             for id in range(len(test_dataset))]
# сделать классификацию для каждой картинки
probs = predict(model_ft, test_imgs)

# взять top-1 predictions
preds = [class_names[i] for i in np.argmax(probs, axis=1)]
# получить имена файлов картинок
test_filenames = [name for name in sorted_files]

# сопоставить имена файлов и результаты классификации
results = [(sorted_files[i], preds[i]) for i in range(len(sorted_files))]
results.sort(key=lambda x: int(x[0][3:-4]))

# сформировать итоговую таблицу и вывести ее первые несколько значений
my_submit = pd.DataFrame({'Id': [res[0] for res in results],
                          'Expected': [res[1] for res in results]})
my_submit.head()
# сохранить таблицу

my_submit.to_csv('drive/My Drive/Simpsons/new_submission.csv', index=False)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09873106835857552, 'fraction_numerical': 0.034302087597216535, 'mean_word_length': 2.275945293644409, 'pattern_counts': {'":': 0, '<': 12, '<?xml version=': 0, '>': 13, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 32, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '3', 'dir': 'jupyter-structured-clean-dedup', 'id': '357299', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 4198, 'n_tokens_neox': 4172, 'n_words': 926, 'path': 'Simpsons_classifier.ipynb', 'repo_name': 'AnthonyQuantum/Simpsons-classifier', 'size': '12215'}