python

Распознаем номера автомобилей. Разработка multihead-модели в Catalyst

  • суббота, 12 июня 2021 г. в 00:55:51
https://habr.com/ru/company/simbirsoft/blog/561866/
  • Блог компании SimbirSoft
  • Python
  • Алгоритмы
  • Обработка изображений
  • Машинное обучение


Фиксация различных нарушений, контроль доступа, розыск и отслеживание автомобилей – лишь часть задач, для которых требуется по фотографии определить номер автомобиля (государственный регистрационный знак или ГРЗ). 

В этой статье мы рассмотрим создание модели для распознавания с помощью Catalyst – одного из самых популярных высокоуровневых фреймворков для Pytorch. Он позволяет избавиться от большого количества повторяющегося из проекта в проект кода – цикла обучения, расчёта метрик, создания чек-поинтов моделей и другого – и сосредоточиться непосредственно на эксперименте.

Сделать модель для распознавания можно с помощью разных подходов, например, путем поиска и определения отдельных символов, или в виде задачи image-to-text. Мы рассмотрим модель с несколькими выходами (multihead-модель). В качестве датасета возьмём датасет с российскими номерами от проекта Nomeroff Net. Примеры изображений из датасета представлены на рис. 1.

Рис. 1. Примеры изображений из датасета

Общий подход к решению задачи

Необходимо разработать модель, которая на входе будет принимать изображение ГРЗ, а на выходе отдавать строку распознанных символов. Модель будет состоять из экстрактора фичей и нескольких классификационных “голов”. В датасете представлены ГРЗ из 8 и 9 символов, поэтому голов будет девять. Каждая голова будет предсказывать один символ из алфавита “1234567890ABEKMHOPCTYX”, плюс специальный символ “-” (дефис) для обозначения отсутствия девятого символа в восьмизначных ГРЗ. Архитектура схематично представлена на рис. 2.

Рис. 2. Архитектура модели

В качестве loss-функции возьмём стандартную кросс-энтропию. Будем применять её к каждой голове в отдельности, а затем просуммируем полученные значения для получения общего лосса модели. Оптимизатор – Adam. Используем также OneCycleLRWithWarmup как планировщик leraning rate. Размер батча – 128. Длительность обучения установим в 10 эпох. 

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

Кодирование

Далее рассмотрим основные моменты кода. Класс датасета (листинг 1) в общем обычный для CV-задач на Pytorch. Обратить внимание стоит лишь на то, как мы возвращаем список кодов символов в качестве таргета. В параметре label_encoder передаётся служебный класс, который умеет преобразовывать символы алфавита в их коды и обратно.

class NpOcrDataset(Dataset):
   def __init__(self, data_path, transform, label_encoder):
       super().__init__()
       self.data_path = data_path
       self.image_fnames = glob.glob(os.path.join(data_path, "img", "*.png"))
       self.transform = transform
       self.label_encoder = label_encoder
 
   def __len__(self):
       return len(self.image_fnames)
 
   def __getitem__(self, idx):
       img_fname = self.image_fnames[idx]
       img = cv2.imread(img_fname)
       if self.transform:
           transformed = self.transform(image=img)
           img = transformed["image"]
       img = img.transpose(2, 0, 1)
      
       label_fname = os.path.join(self.data_path, "ann",
                                  os.path.basename(img_fname).replace(".png", ".json"))
       with open(label_fname, "rt") as label_file:
           label_struct = json.load(label_file)
           label = label_struct["description"]
       label = self.label_encoder.encode(label)
 
       return img, [c for c in label]

Листинг 1. Класс датасета

В классе модели (листинг 2) мы используем библиотеку PyTorch Image Models для создания экстрактора фичей. Каждую из классификационных голов модели мы добавляем в ModuleList, чтобы их параметры были доступны оптимизатору. Логиты с выхода каждой из голов возвращаются списком.

class MultiheadClassifier(nn.Module):
   def __init__(self, backbone_name, backbone_pretrained, input_size, num_heads, num_classes):
       super().__init__()
 
       self.backbone = timm.create_model(backbone_name, backbone_pretrained, num_classes=0)
       backbone_out_features_num = self.backbone(torch.randn(1, 3, input_size[1], input_size[0])).size(1)
 
       self.heads = nn.ModuleList([
           nn.Linear(backbone_out_features_num, num_classes) for _ in range(num_heads)
       ])
  
   def forward(self, x):
       features = self.backbone(x)
       logits = [head(features) for head in self.heads]
       return logits

Листинг 2. Класс модели

Центральным звеном, связывающим все компоненты и обеспечивающим обучение модели, является Runner. Он представляет абстракцию над циклом обучения-валидации модели и отдельными его компонентами. В случае обучения multihead-модели нас будет интересовать реализация метода handle_batch и набор колбэков.

Метод handle_batch, как следует из названия, отвечает за обработку батча данных. Мы в нём будем только вызывать модель с данными батча, а обработку полученных результатов – расчёт лосса, метрик и т.д. – мы реализуем с помощью колбэков. Код метода представлен в листинге 3.

class MultiheadClassificationRunner(dl.Runner):
   def __init__(self, num_heads, *args, **kwargs):
       super().__init__(*args, **kwargs)
       self.num_heads = num_heads
 
   def handle_batch(self, batch):
       x, targets = batch
       logits = self.model(x)
      
       batch_dict = { "features": x }
       for i in range(self.num_heads):
           batch_dict[f"targets{i}"] = targets[i]
       for i in range(self.num_heads):
           batch_dict[f"logits{i}"] = logits[i]
      
       self.batch = batch_dict

Листинг 3. Реализация runner’а

Колбэки мы будем использовать следующие:

  • CriterionCallback – для расчёта лосса. Нам потребуется по отдельному экземпляру для каждой из голов модели.

  • MetricAggregationCallback – для агрегации лоссов отдельных голов в единый лосс модели.

  • OptimizerCallback – чтобы запускать оптимизатор и обновлять веса модели.

  • SchedulerCallback – для запуска LR Scheduler’а.

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

  • CheckpointCallback – чтобы сохранять лучшие веса модели.

Код, формирующий список колбэков, представлен в листинге 4.

def get_runner_callbacks(num_heads, num_classes_per_head, class_names, logdir):
   cbs = [
       *[
           dl.CriterionCallback(
               metric_key=f"loss{i}",
               input_key=f"logits{i}",
               target_key=f"targets{i}"
           )
           for i in range(num_heads)
       ],
       dl.MetricAggregationCallback(
           metric_key="loss",
           metrics=[f"loss{i}" for i in range(num_heads)],
           mode="mean"
       ),
       dl.OptimizerCallback(metric_key="loss"),
       dl.SchedulerCallback(),
       *[
           dl.AccuracyCallback(
               input_key=f"logits{i}",
               target_key=f"targets{i}",
               num_classes=num_classes_per_head,
               suffix=f"{i}"
           )
           for i in range(num_heads)
       ],
       dl.CheckpointCallback(
           logdir=os.path.join(logdir, "checkpoints"),
           loader_key="valid",
           metric_key="loss",
           minimize=True,
           save_n_best=1
       )
   ]
  
   return cbs

Листинг 4. Код получения колбэков

Остальные части кода являются тривиальными для Pytorch и Catalyst, поэтому мы не станем приводить их здесь. Полный код к статье доступен на GitHub.

Результаты эксперимента

Рис. 3. График лосс-функции модели в процессе обучения. Оранжевая линия – train loss, синяя – valid loss

В списке ниже перечислены некоторые ошибки, которые модель допустила на тест-сете:

  • Incorrect prediction: T970XT23- instead of T970XO123

  • Incorrect prediction: X399KT161 instead of X359KT163

  • Incorrect prediction: E166EP133 instead of E166EP123

  • Incorrect prediction: X225YY96- instead of X222BY96-

  • Incorrect prediction: X125KX11- instead of X125KX14-

  • Incorrect prediction: X365PC17- instead of X365PC178

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

Заключение

В статье мы рассмотрели способ реализации multihead-модели для распознавания ГРЗ автомобилей с помощью фреймворка Catalyst. Основными компонентами явились собственно модель, а также раннер и набор колбэков для него. Модель успешно обучилась и показала высокую точность на тестовой выборке.

Спасибо за внимание! Надеемся, что наш опыт был вам полезен. 

Больше наших статей по машинному обучению и обработке изображений: