Автор статьи: Виктория Ляликова

Всем привет! Сегодня рассмотрим решение задачи многоклассовой класификации на основе датасета овощей и фруктов с помощью сверточной нейронной сети архитектуры 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}????")

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

def get_img_array(img_path, size):
   img = tf.keras.preprocessing.image.load_img(img_path, target_size=size)
   array = tf.keras.preprocessing.image.img_to_array(img)
   # расширяем размерность для преобразования массива в пакеты
   array = np.expand_dims(array, axis=0)
   return array

И создадим основную функцию, которая будет выполнять предсказание. Сначала загружаем нашу модель из файла с помощью метода load_model из пакета keras. Определяем массив меток для классификации. Используем метод mobilenet_v2.preprocess_input для преобразования входного изображения в формат, подходящий для нейронной сети. Выполняем предсказание с помощью метода

def predictions(img_path):
   img_size = (224, 224)
   classifier = load_model("****/fruit224mobile.h5")
class_labels = ['Яблоко', 'Банан', 'Свекла', 'Болгарский перец', 'Капуста', 'Стручковый перец', 'Морковь', 'Цветная капуста',
               'Перец чили', 'Кукуруза', 'Огурец', 'Баклажан', 'Чеснок', 'Имбирь', 'Виноград', 'Халапеньо', 'Киви',
               'Лимон', 'Латук', 'Манго', 'Лук', 'Апельсин', 'Паприка', 'Груша', 'Горох', 'Ананас', 'Гранат',
               'Картофель', 'Редька', 'Соевые бобы', 'Шпинат', 'Сладкая кукуруза', 'Батат', 'Помидор', 'Репа', 'Арбуз']

try:
   preprocess_input = tf.keras.applications.mobilenet_v2.preprocess_input
   img_array = preprocess_input(get_img_array(img_path, size=img_size))
   pred = np.argmax(classifier.predict(img_array), axis=1)
   predictions = class_labels[pred[0]]
   return predictions
except Exception:
   return "Проблемы с изображением"

Все, бот готов! Протестируем его сначала на тестовых изображениях

И на своих изображениях. Здесь скриншоты сделаны в версии Telegram Desktop.

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

На этом все. В завершение хочу порекомендовать бесплатный вебинар, на котором эксперты OTUS расскажут какие преимущества дают Байесовские A/B тесты по сравнению с обычными (интерпретируемость, эффективность и другие), как проводить Байесовские A/B тесты и как работать с Байесовскми моделями в PyMC3.

Комментарии (0)