Создаём нейросеть на Python
Ну вот реально, очень мало в интернете статей на тему использования нейросетей. Теоретической информации — полно. «Напишем нейросеть в 9 строчек» — полно. А вот практических примеров с разжевыванием — единицы. Одна из хороших статей тут на хабре. Начало отличное, конец скомканный и до конца не раскрытый. Кроме того код во многих статьях датируемых до 20г уже не рабочий, т.к. используют Tensorflow версии меньше 2. Или ранние 2 версии. Посему на написание живого примера потратил довольно таки много времени.
Итак поставим себе задачу: узнать по фото, лето или зима отображены на ней.
Структура каталогов для датасета
Для начала создадим проект на python со следующей структурой каталогов:
, где папки:
- predict (1) — сюда положим фотографии, по которым нейросеть будет давать ответ, лето там или зима
- tarin (2) — с двумя подпапками summer и winter, в которые разместим фотографии лета и осени для обучения нейросети. На скриншоте их всего 4, но этого конечно очень мало. Как минимум нужно пару тысяч для хорошего предсказания.
- validation (3) — валидационный набор фотографий — нужен для оценки качества обученной нейросети.
Необходимые для работы модули
Далее необходимо установить необходимые модули для Python для работы с нейросетью:
1 2 3 4 5 6 |
pip3 install tensorflow pip3 install keras pip3 install matplotlib pip3 install numpy pip3 install pillow pip3 install image |
Рыба главного файла main.py
Создадим файл main,py со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
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=хх — увеличивает изображение
Последние параметры нужны для того чтобы нейросеть включала в модель обучения и слегка измененные изображения
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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 получим массив нормализованых изображений для тренировки. Проверочные изображения (которые используются для оценки качества обучения нейросети, тоже нормализцуем:
1 2 3 4 5 6 7 |
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 загрузит изображения с диска, нормализует данные и изменит размер изображений — всего лишь одной строкой кода
При желании можно посмотреть, что у нас получилось:
1 2 3 4 5 6 7 8 9 |
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() <span style="color:#008000;font-weight:bold;">augmented_images = [train_data_gen[0][0][0] for i in range(5)]plotImages(augmented_images)</span> |
Добавим в модель слои
У модели должен быть обязательно входной слой, один или несколько промежуточных и один выходной слой. Кроме того необходимо задать количество нейронов сети, и количество классов на выходе.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 (rectified linear activation unit) — это кусочно-линейная функция, которая выводит входные данные без изменений, если они положительные, и ноль, если входные данные отрицательные. Она стала функцией активации по умолчанию для многих типов нейронных сетей, потому что модель, использующую ее, легче обучать, и она часто достигает лучших результатов
Подробнее можно почитать например здесь
Рассмотрим подробнее что означает каждая строчка:
1 |
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 (трехмерный сигнал).
Компиляция модели
После того как данные по модели подготовили, её нужно «скомпилировать», указав алгоритм будущего обучения, и прочие параметры:
1 2 3 4 5 6 7 8 |
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
управляют скоростью затухания этих скользящих средних
Его преимущества:
- Простая реализация.
- Вычислительная эффективность.
- Небольшие требования к памяти.
- Инвариант к диагональному масштабированию градиентов.
- Хорошо подходит для больших с точки зрения данных и параметров задач.
- Подходит для нестационарных целей.
- Подходит для задач с очень шумными или разреженными градиентами.
- Гиперпараметры имеют наглядную интерпретацию и обычно требуют небольшой настройки.
Тренируем модель
Для тренировки модели необходимо выполнить код:
1 2 3 4 5 6 7 8 9 |
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 — это количество циклов обучения модели. Чем больше — тем лучше результат обучения. Подбирается экспериментальным путём.
Результат тренировки можно посмотреть в виде графиков:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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() |
Проверка обученой модели
На этом шаге мы получили обученную модель. Теперь можно попробовать её попросить распознать, что же за изображение на картинке: Зима или лето? :
1 2 3 4 5 6 7 8 9 10 |
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) |
Вывод в консоль может быть примерно такой:
1 2 3 |
5. Узнаем по фото, что именно за картинка зима или лето.. 1/1 [==============================] - 0s 85ms/step [0] |
Результат будет или 0 или 1, в зависимости от того что на картинке — лето или зима. Т.е. в зависисмости от количества классов.
Исходный код нейросети можно скачать здесь
Уведомление: Сохранение весов модели нейросети — ЖЗГ