Как отличать птиц от цветов. Или цветы от птиц
В качестве программы выходного дня мне захотелось поиграться с как бы «нейронной» сетью (спойлер — в ней нет нейронов). А чтобы потом не было мучительно больно за бесцельно прожитые годы часы, я подумал, что зря мы его кормим, пусть пользу приносит — пусть заодно эта сетка разберет домашний фотоархив и хотя бы разложит фотографии цветов в отдельную папку.
Самая простая сеть
Самая простая сеть нашлась в статье "Нейросеть в 11 строчек на Python" (это перевод от SLY_G статьи "A Neural Network in 11 lines of Python (Part 1)", вообще у автора есть еще продолжение "A Neural Network in 13 lines of Python (Part 2 — Gradient Descent)", но здесь достаточно первой статьи).
Краткое описание сетки — в этой сети есть ровно одна зависимость — NumPy.
Множество входов рассматривается как матрица , множество выходов — как вектор . В оригинальной статье сеть умножает входную матрицу, размерностью (4 x 3), на матрицу весов входов (3 x 4), к произведению применяет передаточную функцию, и получает матрицу слоя (4 x 4).
Далее слой умножается на матрицу весов выходов (4 x 1), также пропускается через функцию, и получается слой (4 x 1), который и есть результат работы сети.
Итого, опуская скалярную передаточную функцию, сеть реализует два матричных умножения:
Следствием этого, согласно правилам матричного умножения, получилось, что одна из размерностей в ходе работы сети не изменяется и получить на выходе единственное число невозможно.
Поэтому я немного доработал код из статьи, добавил транспонирование после умножения и работу с произвольным числом слоев в сетке. Это дало мне возможность получать любое сочетание размерностей входов и выходов.
Например, если нужно, чтобы было на входе матрица (3 x 4), а выход — единственное число, то добавляем две матрицы синапсов (4 x 1) и (3 x 1):
Или, скажем, можно преобразовать входную матрицу (10 x 8) на выход (4 x 5):
Загрузка фотографий
Так, сетка есть, теперь надо разобраться с загрузкой фоток. Фотографии лежат на диске, в основном в JPG, но встречаются и другие форматы. Размеры у них тоже разные, смотря чем снимали и как обрабатывали, от 3 Mpx до 16 Mpx.
Сначала я попробовал загружать фотографии через Qt, класс QImage, он умеет работать с разными форматами, обеспечивает конверсию и дает прямой доступ к данным картинки. Наверняка в Python существует способ проще, но зато с QImage мне не надо было разбираться. Чтобы сеть могла работать с картинкой, следует перевести в монохромное изображение и уменьшить до стандартного размера.
Для передачи в сетку нужно преобразовать изображение в матрицу numpy.ndarray. QImage.bits() дает указатель на данные изображения, где каждый байт соответствует пикселу. В NumPy нашлась функция recarray, способная сделать массив записей из буфера, а у него есть метод view, который нам сделает матрицу numpy.ndarray без копирования данных.
Сеть для изображений
Картинку, хоть и уменьшенную, непосредственно подавать на вход сети будет слишком накладно — я уже говорил, что сеть делает матричное умножение, поэтому даже один цикл обучения будет приводить к 400x400x400 = 64 млн. умножений. Знатоки рекомендуют использовать свертку. В Википедии есть замечательная иллюстрация ее работы:
На этой анимации видно, что размерность результата равна размерности исходной матрицы. Но я немного упрощу себе жизнь, буду двигаться не по пикселам, а разобью изображение на кусочки размером равным матрице входов, и применю сетку к ним поочередно. В матрицах вырезание кусочка делается достаточно просто:
Результат обработки кусочков сетью складывается в матрицу меньшего размера, эта матрица передается на вход общей сети. То есть будет две сети — первая работает с кусочками изображения, вторая — с результатом работы первой сети над кусочками.
Создание первичной сети:
Внутри создается self.net — собственно сеть, с заданным размером матрицы входов shape и c выходом в виде элементарной матрицы 1х1. Да, можно было наследоваться от класса сети NN, но был выходной, хотелось побыстрее получить результат, а архитектура еще не устоялась. Time to market бьется в наших сердцах!
Обсчет изображения первой сетью:
На выходе имеем матрицу resArr, с размерностью, равной количеству кусочков, на которые было разбито изображение. Эту матрицу передаем на вход второй сети, которая даcт конечный результат.
Тут вы должны меня спросить, откуда я взял первую строчку, и что она значит:
Это — ожидаемый результат сети в случае положительного ответа, т.е. если сеть считает, что на входе изображение цветка. Размерность выбрал из принципа «ни мало, ни много» — если брать размерность 1х1, то из одного получившегося числа трудно судить, насколько сеть «сомневается» в результате. Большую размерность задавать тоже смысла нет — она не даст больше информации. Равное количество нулей и единиц дает четкий ориентир — чем ближе к нему, тем больше совпадение. Если же взять все единицы или все нули, то у сети появится стимул к переобучению — увеличить все сомножители или, соответственно, обнулить их, чтобы получать нужный результат независимо от входных данных.
Как обучать сверточную сеть?
Обучающую выборку я сделал из своих же фотографий, попросту разложив их в два каталога: flowers
и noflowers
Пути к картинкам соберу в два массива
Обучать простые сети обычно, в том числе в оригинальной статье, предлагается традиционным методом — обратным распространением ошибки. Но чтобы этот метод применить к сверточной сети, состоящей из двух элементарных, нужно обеспечить сквозную передачу накопленной ошибки из второй сети в первую. Вообще для сверточных сетей есть и другие методы. Переделывать работающую сеть мне было лень, по крайней мере пока, поэтому решил обучить вторую сеть, а первую вообще не обучать, оставить забитой при создании случайными значениями, рассудив, что раз глазные нервы у человека не обучаются, то и мне нечего обучать первичную сеть, «смотрящую» на изображение.
В каждой эпохе сразу после обучения прогоняю через сеть всю выборку и смотрю, что получилось.
Если сеть обучилась правильно, то на ее выходе должно быть значение, близкое к заданному [[1,0,1,0]], если на входе цветок, и как можно более отличающееся от заданного, например [[0,1,0,1]], если на входе не цветок. Результат оценивается, эмпирически я принял отклонение от успешного результат не более 0,2 — это тоже успешный результат, и считается число ошибок. Из всех прогонов выбираем такую, где делается меньше всего ошибок, и сохраняем веса синапсов обоих сеток в файлы. Дальше эти файлы можно использовать для загрузки сеток.
Хоть розой назови её, хоть нет
С надеждой запускаю и… подождав. потом еще подождав. и еще… получаю полный бред — сетка не обучается:
Будучи носителем настоящих живых, а не искусственных нейронов, до меня дошло, что главным отличием цветов является цвет (да, кэп, спасибо, что ты всегда рядом, хотя зачастую опаздываешь со своими советами). Поэтому надо бы перевести его в какую-то цветовую модель, где цветовая составляющая будет выделена (HSV или HSL), и обучать сеть на цвете.
Но оказалось, что класс QImage не знает такие цветовые пространства. Пришлось отказаться от него и загружать фотки с помощью OpenCV, где такая возможность есть.
Правда, OpenCV наотрез отказался работать с русскими буквами в именах файлов, пришлось их переименовать.
Запустил — результат не порадовал, практически тот же.
Еще подумал, решил, что проблема в сильно случайных значениях в первой сетке, зря я понадеялся, что звезды сойдутся без моей помощи, поэтому добавил ей небольшое предобучение, всего 2 цикла на файл. Для образца положительного результата взял единичную матрицу.
Снова запустил — стало куда интереснее, цифры стали меняться, хотя идеала не достиг.
Дальше… А дальше выходной закончился, и мне пора было заниматься хозработами.
Что делать дальше?
Конечно, и эта сеть, и то, как я ее учил, и тестовый dataset очень мало соотносятся с реальными сетями и тем, чем занимаются data scientists. Это лишь игрушка для гимнастики ума, не возлагайте на нее больших надежд.
Можно наметить дальнейшие шаги, как добиться нужного результата (если он вам нужен):