Создаём нейросеть на Python

Ну вот реально, очень мало в интернете статей на тему использования нейросетей. Теоретической информации — полно. «Напишем нейросеть в 9 строчек» — полно. А вот практических примеров с разжевыванием — единицы. Одна из хороших статей тут на хабре. Начало отличное, конец скомканный и до конца не раскрытый. Кроме того код во многих статьях датируемых до 20г уже не рабочий, т.к. используют Tensorflow версии меньше 2. Или ранние 2 версии. Посему на написание живого примера потратил довольно таки много времени.

Итак поставим себе задачу: узнать по фото, лето или зима отображены на ней.

Структура каталогов для датасета

Для начала создадим проект на python со следующей структурой каталогов:

, где папки:

  • predict (1) — сюда положим фотографии, по которым нейросеть будет давать ответ, лето там или зима
  • tarin (2) — с двумя подпапками summer и winter, в которые разместим фотографии лета и осени для обучения нейросети. На скриншоте их всего 4, но этого конечно очень мало. Как минимум нужно пару тысяч для хорошего предсказания.
  • validation (3) — валидационный набор фотографий — нужен для оценки качества обученной нейросети.

Необходимые для работы модули

Далее необходимо установить необходимые модули для Python для работы с нейросетью:

pip3 install tensorflow
pip3 install keras
pip3 install matplotlib
pip3 install numpy
pip3 install pillow
pip3 install image

Рыба главного файла main.py

Создадим файл main,py со следующим содержимым:

import os
import tensorflow as tf
from keras.preprocessing.image import ImageDataGenerator
import matplotlib.pyplot as plt
import numpy as np

base_dir = os.path.dirname(os.path.abspath(__file__))
train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')

train_summer_dir = os.path.join(train_dir, 'summer')
train_winter_dir = os.path.join(train_dir, 'winter')
validation_summer_dir = os.path.join(validation_dir, 'summer')
validation_winter_dir = os.path.join(validation_dir, 'winter')

print("1. Готовимся к запуску")

print(f"- текущий каталог : {base_dir}")
print(f"-- каталоги изображений для тренировки : {validation_summer_dir},{train_winter_dir}")
print(f"-- каталоги изображений для проверки : {validation_summer_dir},{validation_winter_dir}")

num_summer_tr = len(os.listdir(train_summer_dir))
num_winter_tr = len(os.listdir(train_winter_dir))

num_summer_val = len(os.listdir(validation_summer_dir))
num_winter_val = len(os.listdir(validation_winter_dir))

total_train = num_summer_tr + num_winter_tr
total_val = num_summer_val + num_summer_val

print(f"--- картинок для тренировки {total_train}")
print(f"--- картинок для проверки {total_val}")

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

Подготовка данных и установка параметров модели нейросети

Для того чтобы нейросеть могла обработать данные, их нужно привести к однообразному виду. Фоторафии бывают разного размера, разного цветового профиля и т.п. Для того чтобы привести их в примерно один и тот же формат и вид служит функция ImageDataGenerator |из пакета keras. Наиболее часто используемые параметры:

  • rescale=1./255 — преобразует тензор (матрицу) изображений из интервалов значений 0..255 до 0..1 Зачем и какой механизм? У меня четкого понимания. Пока просто принимаю как данность, что необходимо при работе с изображениями.
  • horizontal_flip=True — добавляет в тензор еще одно изображение, но отраженное по горизонтали
  • rotation_range=xx — поворачивает изображение на xx градусов
  • zoom_range=хх — увеличивает изображение

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

print("2. Устанавливаем параметры модели")

BATCH_SIZE = 100 # количество тренировочных изображений для обработки перед обновлением параметров модели
IMG_SHAPE = 150 # размерность 150x150 к которой будет преведено входное изображение

# приведем изображение к градациям цвета 0.255 и зазеркалим, чтобы увеличить количество изображений для тренировки
train_image_generator = ImageDataGenerator(rescale=1./255, horizontal_flip=True)
validation_image_generator = ImageDataGenerator(rescale=1./255)

print("- нормализуем все изображения для тренировки")
train_data_gen = train_image_generator.flow_from_directory(batch_size=BATCH_SIZE,
                                                          directory=train_dir,
                                                          shuffle=True,
                                                          target_size=(IMG_SHAPE,IMG_SHAPE),
                                                          class_mode='binary')
Спойлер

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

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

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

print("- нормализуем все изображения для проверки")
val_data_gen = validation_image_generator.flow_from_directory(batch_size=BATCH_SIZE,
                                                              directory=validation_dir,
                                                              shuffle=False,
                                                              target_size=(IMG_SHAPE,IMG_SHAPE),
                                                              class_mode='binary')

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

При желании можно посмотреть, что у нас получилось:

def plotImages(images_arr):
  fig, axes = plt.subplots(1, 5, figsize=(20, 20))
  axes = axes.flatten()
  for img, ax in zip(images_arr, axes):
    ax.imshow(img)
  plt.tight_layout()
  plt.show()

augmented_images = [train_data_gen[0][0][0] for i in range(5)]plotImages(augmented_images)

Добавим в модель слои

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

model = tf.keras.models.Sequential([

    tf.keras.layers.Conv2D(32, (3,3), activation='relu', input_shape=(IMG_SHAPE, IMG_SHAPE, 3)),  # входной слой
    tf.keras.layers.MaxPooling2D(2, 2),

    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2, 2),

    tf.keras.layers.Conv2D(128, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2, 2),

    tf.keras.layers.Conv2D(150, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2, 2),

    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(150, activation='relu'), # 150 нейронов
    tf.keras.layers.Dense(2, activation='softmax') # 2- количество классов зима/лето
])
Что такое активатор relu

Выпрямленная линейная функция активации или сокращенно ReLU (rectified linear activation unit) — это кусочно-линейная функция, которая выводит входные данные без изменений, если они положительные, и ноль, если входные данные отрицательные. Она стала функцией активации по умолчанию для многих типов нейронных сетей, потому что модель, использующую ее, легче обучать, и она часто достигает лучших результатов
Подробнее можно почитать например здесь

Рассмотрим подробнее что означает каждая строчка:

tf.keras.layers.Conv2D(32, (3,3), activation='relu', input_shape=(IMG_SHAPE, IMG_SHAPE, 3)),  # входной слой
  • input_shape=(IMG_SHAPE, IMG_SHAPE, 3) — на входе изображение с размерами IMG_SHAPE х IMG_SHAPE, с 3 размерностью цветов (RGB)
  • 32 — количество фильтров (каналов), фактически в моём понимании это количество признаков по которым могут отличаться изображения друг от друга
  • (3,3) — размер ядра (окна внутри матрицы) в каждом фильтре. Выбираем исходя из того, насколько чувствительную модель мы хотим получить

Как мы видим в итоге мы обьявили 4 слоя, с размерностью матрицы от 32х32 до 150х150. Т.е начинаем вычленять признаки на картинке уменьшеной до 32х32 и останавливаемся на вычленении признаков с размерами исходной картинки. Но можно и задать чуть больше, чтоб «бордюр» был.

Команда tf.keras.layers.MaxPooling2D(2, 2), говорит о том, что далее мы укрупняем масштаб полученых признаков. (2.2) — размер окна в котором выбирается максимальное значение

Команда tf.keras.layers.Dense(150, activation=’relu’), говорит что в работе будет задействовано 150 нейронов (по размеру матрицы)

Команда tf.keras.layers.Dense(2, activation=’softmax’), сообщает нейросети, что результат мы хотим получить в виде двух классов 0 — лето, 1 — зима (ну на входе для обучения у нас два вида картинок)

Какие бывают слои

В пакете Keras можно найти слои:
Conv1D, Conv2D, Conv3D
так как на практике используют свертки для анализа одномерного, двумерного и трехмерного сигналов. Например, для:

  • аудио – 1D (одномерный сигнал);
  • изображения – 2D (двумерный сигнал);
  • видео – 3D (трехмерный сигнал).

Компиляция модели

После того как данные по модели подготовили, её нужно «скомпилировать», указав алгоритм будущего обучения, и прочие параметры:

print("- скомпилируем модель, по алгоритму Адам")
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

print("- представление модели:")
model.summary()

Мы воспользуемся оптимизатором adam. В качестве функции потерь воспользуемся sparse_categorical_crossentropy. Так же мы хотим на каждой обучающей итерации следить за точностью модели, поэтому передаём значение accuracy в параметр metrics.

Функция потерь

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

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

Помимо adam, есть еще ряд оптимизаторов: RMSprop, SGD и прочее. Чуть подробнее можно прочитать тут. Обычно таки для обработки изобращений используют adam.

Adam — один из самых эффективных алгоритмов оптимизации в обучении нейронных сетей. Он сочетает в себе идеи RMSProp и оптимизатора импульса. Вместо того чтобы адаптировать скорость обучения параметров на основе среднего первого момента (среднего значения), как в RMSProp, Adam также использует среднее значение вторых моментов градиентов. В частности, алгоритм вычисляет экспоненциальное скользящее среднее градиента и квадратичный градиент, а параметры beta1 и beta2 управляют скоростью затухания этих скользящих средних

Зачем этот оптимизатор?

Его преимущества:

  • Простая реализация.
  • Вычислительная эффективность.
  • Небольшие требования к памяти.
  • Инвариант к диагональному масштабированию градиентов.
  • Хорошо подходит для больших с точки зрения данных и параметров задач.
  • Подходит для нестационарных целей.
  • Подходит для задач с очень шумными или разреженными градиентами.
  • Гиперпараметры имеют наглядную интерпретацию и обычно требуют небольшой настройки.

Тренируем модель

Для тренировки модели необходимо выполнить код:

print("3. Тренируем модель")
EPOCHS = 10
history = model.fit(
    train_data_gen,
    steps_per_epoch=int(np.ceil(total_train / float(BATCH_SIZE))),
    epochs=EPOCHS,
    validation_data=val_data_gen,
    validation_steps=int(np.ceil(total_val / float(BATCH_SIZE)))
)

EPOCHS — это количество циклов обучения модели. Чем больше — тем лучше результат обучения. Подбирается экспериментальным путём.

Результат тренировки можно посмотреть в виде графиков:

print("4. Результаты тренировки")

acc = history.history['accuracy']
val_acc = history.history['val_accuracy']

loss = history.history['loss']
val_loss = history.history['val_loss']

epochs_range = range(EPOCHS)

plt.figure(figsize=(8,8))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, acc, label='Точность на обучении')
plt.plot(epochs_range, val_acc, label='Точность на валидации')
plt.legend(loc='lower right')
plt.title('Точность на обучающих и валидационных данных')

plt.subplot(1, 2, 2)
plt.plot(epochs_range, loss, label='Потери на обучении')
plt.plot(epochs_range, val_loss, label='Потери на валидации')
plt.legend(loc='upper right')
plt.title('Потери на обучающих и валидационных данных')
plt.savefig('./foo.png')
plt.show()

Проверка обученой модели

На этом шаге мы получили обученную модель. Теперь можно попробовать её попросить распознать, что же за изображение на картинке: Зима или лето? :

print("5. Узнаем по фото, что именно за картинка зима или лето..")

image = tf.keras.preprocessing.image.load_img(f"{base_dir}\\predict\\summer1.jpg", target_size=(IMG_SHAPE, IMG_SHAPE))
input_arr = tf.keras.preprocessing.image.img_to_array(image)
input_arr = np.array([input_arr])

input_arr = input_arr.astype('float32') / 255.  # говорим что результат хотим число с плавающей запятой
predictions = model.predict(input_arr) # получаем результат проверки изображения в виде массива вероятностей
predicted_class = np.argmax(predictions, axis=-1)  # вычленяем наиболее вероятный результат
print(predicted_class)

Вывод в консоль может быть примерно такой:

5. Узнаем по фото, что именно за картинка зима или лето..
1/1 [==============================] - 0s 85ms/step
[0]

Результат будет или 0 или 1, в зависимости от того что на картинке — лето или зима. Т.е. в зависисмости от количества классов.

Исходный код нейросети можно скачать здесь

Один комментарий

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.