Создаем чат-бот для распознавания изображений на основе нейронной сети MobileNetV2

1b0a25288d723018e94d2ba64b701720.png

6419da07142b3d5e732aed94b669a81d.jpgАвтор статьи: Виктория Ляликова

Всем привет! Сегодня рассмотрим решение задачи многоклассовой класификации на основе датасета овощей и фруктов с помощью сверточной нейронной сети архитектуры MobileNetV2. А после этого еще создадим чат-бота, который будет классифицировать эти изображения.

Сегодня уже никого не удивишь чат-ботами, которые могут вести диалоги на естественном языке. В наши дни уже даже школьников учат создавать своих чат ботов. А почему бы тогда не создать бота, который может видеть. Рассмотрим процесс создания пока самого простого  бота, который будет в качестве сообщения принимать фото и отвечать, что на нем изображено. 

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

Датасет «Fruits and Vegetables Image Recognition» доступен на Kaggle.com и состоит из 3825 изображения таких фруктов и овощей как банан, яблоко, груша, виноград, апельсин, киви, арбуз, гранат, ананас, манго, огурцы, морковь, стручковый перец, лук, картофель, лимон, помидоры, редька, свекла, капуста, салат, шпинат, соевые бобы, цветная капуста, болгарский перец, перец чили, репа, кукуруза, сладкая кукуруза, сладкий картофель, паприка, халапеньо, имбирь, чеснок, горох, баклажан. Все данные содержат по 100 изображений для обучения, по 10 изображений для валидации и по 10 изображений для тестирования (в некоторых папках немного меньше).

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

import cv2
import pathlib
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Model
from tensorflow.keras.callbacks import Callback, ModelCheckpoint,EarlyStopping, ReduceLROnPlateau
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from keras.applications.mobilenet_v2 import MobileNetV2
from tensorflow.keras.applications import mobilenet_v2
from tensorflow.keras import layers

# директория обучения
train_dir = pathlib.Path("***/datasets/fruits1/train/")
# директория тестирования
test_dir = pathlib.Path("***/datasets/fruits1/test/")
# директория валидации
val_dir = pathlib.Path("***/datasets/fruits1/validation/")
img_height = 224
img_weigth = 224

Загружать изображения будем с помощью полезной утилиты image_dataset_from_directory библиотеки keras.preprocessing (вернет tf.data.Dataset), которая возвращает пакеты изображений из подкаталогов вместе с метками классов. Сохраним все классы овощей и фруктов в списке class_names.

train_ds = tf.keras.utils.image_dataset_from_directory(train_dir)
test_ds = tf.keras.utils.image_dataset_from_directory(test_dir)
val_ds = tf.keras.utils.image_dataset_from_directory(val_dir)
class_names = dict(zip(train_ds.class_names, range(len(train_ds.class_names))))
num_classes = len(class_names)

Всего 36 классов. Посмотрим на изображения

Для работы с изображениями очень хорошо подходят сверточные нейронные сети, поэтому за основу возьмем MobilenetV2, предобученную на большом количестве изображений из базы ImageNet. MobileNetV2 является облегченной глубокой нейронной сетью, которая использует сверточные блоки глубиной 53 слоя.

Сеть имеет 2 типа блоков: один остаточный блок с шагом 1 (на рисунке слева), другой блок с шагом 2 для уменьшения размера (на рисунке справа). 

Каждый блок имеет 3 различных слоя:

  • Свертка 1×1 имеет активационную функцию Relu6 (f (s)=max (0,6))

  • Глубинная свертка

  • Свертка 1×1 без линейной функции

Перед тем, как загружать изображения в нашу предобученную нейронную сеть, их необходимо преобразовать в формат, который принимает наша модель, то есть перевести в тензоры с плавающей точкой, а затем произвести нормализацию изображений из интервала от 0 до 255 к интервалу от 0 до 1.Это можно сделать с использованием класса ImageDataGenerator. Данный класс удобен и полезен тем, что берет изображения прямо из папок, а также позволяет расширить набор данных за счет создания копий изображений, изменяя различные свойства изображения (смещение, поворот, увеличение и т.д.). Такой подход позволяет модели лучше обобщать и извлекать различные признаки. Достаточно передать конструктору класса набор различных значений необходимых нам параметров и обо всем он позаботится сам.

train_generator = ImageDataGenerator(
preprocessing_function = mobilenet_v2.preprocess_input,
rotation_range = 32,
zoom_range = 0.2,
width_shift_range = 0.2,
height_shift_range = 0.2,
shear_range = 0.2,
horizontal_flip = True,
fill_mode = "nearest")

Так как используется предобученная нейронная сеть, тогда для перевода изображений в тензоры с плавающей точкой будем использовать готовый метод mobilenet_v2.preprocess_input

Теперь воспользуемся методом .flow_from_directory для применения полученного преобразования к нашим изображениям из обучающего и валидационного набора данных. 

train = train_generator.flow_from_directory(train_dir,
target_size = (img_height,img_width),
# изображение имеет 3 цветовых канала
color_mode = "rgb",
# создаем бинарные признаки меток класса 
class_mode = "categorical",
batch_size = 32,
shuffle = True,
seed = 123)

validation = train_generator.flow_from_directory(val_dir,
target_size = (img_height,img_width),
# изображение имеет 3 цветовых канала
color_mode = "rgb",
# создаем бинарные признаки меток класса 
class_mode = "categorical",
batch_size = 32,
shuffle = True,
seed = 123)

Здесь можно обратить внимание, что указан параметр shuffle = True, что говорит о том, что изображения из разных классов не будут перемешиваться. Сначала будут поступать изображения из первой папки, потом из второй и т.д, а затем из последней. Это необходимо, чтобы мы могли потом легко обращаться к меткам класса. 

Перейдем к построению модели. Загружаем предварительно обученную версию сети с размером входного изображения (224, 224, 3) с весами «imagenet».

mobilenet_ = MobileNetV2(
input_shape = (img_height,img_width,3),
include_top = False,
weights = 'imagenet',
pooling = 'avg')

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

mobilenet_.trainable = False

Создаем 2 обычных слоя с 128 нейронами и один последний слой классификации, с количеством нейронов равных количеству необходимых нам классов и активационной функцией softmax

inputs = mobilenet_.input
x = Dense(128, activation = 'relu')(mobilenet_.output)
x = Dense(128, activation = 'relu')(x)
outputs = Dense(num_classes , activation = 'softmax')(x)

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

mobilenet = Model(inputs = inputs, outputs = outputs)

Используем метод ModelCheckpoint для сохранения весов модели, основываясь на потерях на этапе проверки и метод EarlyStopping для ранней остановки обучения

early_stopping = EarlyStopping(
	monitor='val_loss',
	mode='min',
	patience = 2,
	verbose=1,
	restore_best_weights=True,
)
checkpoint =ModelCheckpoint('***/fruit224mobile.h5',
                        	monitor = 'val_loss',
                        	mode = 'min',
                       	save_best_only = True)

callbacks = [early_stopping, checkpoint]

Компилируем и подгоняем модель, используя оптимизатор Адама и категориальную перекрестную энтропию.

mobilenet.compile(optimizer=’’, loss ='categorical_crossentropy',metrics = ['accuracy'])

history = mobilenet.fit(
train, validation_data = validation,
batch_size = 32,
epochs = 20,
callbacks = callbacks)

Оценим модель на тестовых данных, используя метод «evaluate»

(eval_loss, eval_accuracy) = mobilenet.evaluate(test)

Посмотрим на точность на тестовых значениях

# получаем предсказанные значения от тестовых изображений
pred = mobilenet.predict(test)
# получаем номер класса с максимальным весом
pred = np.argmax(pred,axis=1)
# сопоставляем классы
labels = (train.class_indices)
labels = dict((v,k) for k,v in labels.items())
pred = [labels[k] for k in pred]
# получаем предсказанные классы
y_test = [labels[k] for k in test.classes]
# оцениваем точность
from sklearn.metrics import accuracy_score
acc = accuracy_score(y_test, pred)
print(f'Accuracy on the test set: {100*acc:.2f}%')

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

Переходим к работе с чат-ботом. Прежде чем начинать разработку, бота необходимо зарегистрировать и получить его уникальный id. При этом владельцем бота будет тот, с чьего аккаунта он создавался.  Для создания своего бота в Telegram есть специальный бот-@BotFather. Находим его и подтверждаем начало диалога, набрав команду /start. 

Далее вводим команду /newbot, вводим имя бота (MyTestbot) и username бота (RecFood_bot). Username должен обязательно заканчиваться на bot и быть уникальным.

Для работы с ботом будем использовать библиотеку aiogram. Она предоставляет множество удобных функций для управления ботом, обработки событий, работы с клавиатурами, интеграции с другими сервисами и т.д. Установим ее

pip install aiogram

Далее для работы с ботом нам понадобится Токен бота, который является его уникальным идентификатором, его предоставляет BotFather при создании бота.

Также для работы с ботом нам понадобятся классы Bot, Dispatcher, executor, types.

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

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

Создадим экземпляр класса Bot, передав в него токен, объект Disptcher и в него передадим нашего созданного бота и напишем 2 простых обработчика команды start и help. Для того, чтобы заставить бота работать, необходимо в конце программы вызвать метод start_polling класса executor, передав туда наш объект Dispatcher.

import logging
import os
from aiogram import Bot, Dispatcher, executor, types
from keras.models import load_model
from keras_preprocessing import image
from keras_preprocessing.image import img_to_array
import numpy as np
import tensorflow as tf
import cv2
#включаем логирование
logging.basicConfig(level=logging.INFO)
#объект бота
bot = Bot(token=API_TOKEN)
#диспетчер
dp = Dispatcher(bot)
#обработка команды старт
@dp.message_handler(commands=['start'])
async def echo(message: types.Message):
  await message.reply('Привет! Я бот, распознающий овощи и фрукты')
# обработка команды help
@dp.message_handler(commands=['help'])
async def echo(message: types.Message):
  await message.reply('Просто отправьте мне изображение, которое содержит овощ или фрукт')

if __name__ == '__main__':
#запуск пуллинга
  executor.start_polling(dp, skip_updates=True)

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

@dp.message_handler(content_types=[types.ContentType.PHOTO])
async def download_photo(message: types.Message):
# загружаем фото в папку по умолчанию
   await message.photo[-1].download()
# определяем путь к фото
   img_path = (await bot.get_file(message.photo[-1].file_id)).file_path
# получаем предсказание
   pred = predictions(img_path)
# Отправляем ответ пользователю
   await message.answer(f"Я думаю, что это {temp}
    
            

© Habrahabr.ru