Определяем спелость арбуза с помощью Keras: полный цикл, от идеи до программы на Google Play +62


С чего все началось


Все началось с Эппл Маркета — я обнаружил, что у них есть программа, позволяющая определить спелость арбуза. Программа… странная. Чего стоит, хотя бы, предложение постучать по арбузу не костяшками пальцев, а… телефоном! Тем не менее, мне захотелось повторить это достижение на более привычной платформе Андроид.

Выбор инструментов


Задача наша решается несколькими способами, и если честно, мне пришлось приложить немалые усилия, чтобы не пойти «простым» путем. То есть, взять преобразования Фурье, вейвлеты и редактор сигналов. Однако, я хотел получить опыт работы с нейросетями, так что пусть сети и занимаются анализом данных.

В качестве библиотеки для создания и обучения нейросетей был выбран Керас — гугловская надстройка над TensorFlow и Theano. Вообще, если вы только начинаете работу с сетями глубокого обучения, лучше инструмента вам не найти. С одной стороны, Керас — мощный инструмент, оптимизированный по скорости, памяти и железу (да, он умеет работать на видеокартах и их кластерах). С другой — всё, что можно «спрятать» от пользователя, там спрятано, так что вам не придется ломать голову над стыковкой слоев нейросети, например. Очень удобно.

Как Керас, так и нейросети вообще, требуют знания Питона — этот язык, подобно змее обвил… извините, наболело. Короче, без Питона в современный Deep Learning соваться не стоит. К счастью, Питон можно изучить за две недели, в крайнем случае — за месяц.

К Питону вам потребуются еще некоторые библиотеки, но это уже мелочи — я имею в виду, если уж вы справились с самим Питоном. Потребуется знакомство (весьма поверхностное) с NumPy, PyPlot и возможно, еще с парой библиотек, из которых мы возьмем буквально по паре функций. Не сложно. Правда.

Ну и в завершение замечу, что упоминавшиеся выше кластеры видеокарт нам не потребуются — наша задача нормально решается с помощью компьютерного CPU — медленно, но не критично медленно.

План работы


Сначала нужно создать нейросеть — на Питоне и Керасе, под Убунту. Можно — на эмуляторе Убунты. Можно — под Виндоуз, но потраченного дополнительно времени вам хватит, чтобы упомянутую Убунту изучить, и далее работать под ней.

Следующий шаг — написание программы. Я планирую сделать это на Java под Андроид. Это будет прототип программы, в том смысле, что пользовательский интерфейс у нее будет, а вот нейросети пока что нет.

В чем же смысл написания «пустышки», спросите вы. А вот в чем: любая задача, связанная с анализом данных, рано или поздно упирается в поиск данных — для обучения нашей программы. В самом деле, сколько арбузов надо обстучать и попробовать на вкус, чтобы нейросеть смогла на этих данных построить достоверную модель? Сотню? Больше?

Здесь нам и поможет наша программа: заливаем ее на Google Play, раздаем (ладно, навязываем, выкручивая руки) всем друзьям, которым не повезло иметь телефон с Андроидом, и данные, тонюсеньким ручейком, начинают стекаться… а кстати, куда?

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

Наконец, данных достаточно. Обучаем нейросеть.

Портируем нейросеть на Java и выпускаем обновление нашей программы.

Профит. Хотя нет. Программа была бесплатной. Только опыт и набитые шишки.

Создание нейросети


Работа с аудио, каковым, безусловно, является постукивание по арбузу, это либо рекуррентные нейросети, либо так называемая одномерная конволюционная сеть. Причем, в последнее время конволюционные сети однозначно лидируют, вытесняя рекуррентные. Идея конволюционной сети заключается в том, что по массиву данных — графику «интерсивность звука — время» — скользит окошко, и вместо анализа сотен тысяч сэмплов, мы работаем только с тем, что в окошко попадает. Следующие слои объединяют и анализируют результаты работы этого слоя.

Чтобы было понятнее, представьте, что вам надо найти на фото морского пейзажа чайку. Вы сканируете картину — «окошко» вашего внимания движется вдоль воображаемых строк и столбцов, в поисках белой галочки. Именно так работает конволюционная 2D сеть, одномерная же сканирует вдоль одной координаты — оптимальный выбор, если мы имеем дело со звуковым сигналом.

Отмечу, однако, что зацикливаться на 1D сетях не обязательно. В качестве упражнения, я построил график звука и анализировал полученный битмап как картинку — с помощью 2d конволюционной сети. К моему удивлению, результат получился не хуже, чем при анализе «сырых одномерных» данных.

Используемая сеть имела следующую структуру:

model = Sequential()
model.add(Conv1D(filters=32, kernel_size=512, strides=3, 
	padding='valid', use_bias=False, input_shape=(nSampleSize, 1), name='c1d', 
	activation='relu'))
model.add(Activation('relu', input_shape=(nSampleSize, 1)))
model.add(MaxPooling1D(pool_size=(2)))

model.add(Conv1D(32, (3)))
model.add(Activation('relu'))
model.add(MaxPooling1D(pool_size=(2)))

model.add(Conv1D(64, (3)))
model.add(Activation('relu'))
model.add(MaxPooling1D(pool_size=(2)))

model.add(Flatten())
model.add(Dense(64))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(nNumOfOutputs)) #1))
model.add(Activation('sigmoid'))

model.compile(loss='mean_squared_error',
	optimizer='adam',
	metrics=['accuracy'])

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

Оценки для тестовой выборки выставляются человеком, как именно — мы поговорим в разделе, посвященном программе для Андроид. Задача нейросети — предсказать, какую для данного арбуза (по записи постукивания) оценку поставит человек.

Написание программы


Я уже упоминал, что программа должна выйти в виде двух версий. Первая, предварительная, честно предупреждает пользователя, что ее предсказания — полный бред. Зато она позволяет пользователю записать стук по арбузу, выставить оценку вкусовых качеств этого арбуза и переслать по Интернет автору программы. То есть, первая версия просто собирает данные.

Вот страничка программы на Google Play, разумеется, программа бесплатна.

Что она делает:

1. Нажимаем кнопку с микрофоном и начинается запись. У вас есть пять секунд, чтобы три раза стукнуть по арбузу — тук-тук-тук. Кнопка с арбузом делает «предсказание», и ее мы пока не трогаем.

Примечание — если на Гугле старая версия, то запись и предсказание совмещены в кнопке с арбузом, а кнопки с микрофоном нету.

image

2. Сохраненный файл — временный, и будет перезаписан при следующем нажатии кнопки записи. Это позволяет повторить постукивание, если кто-то говорит под руку (вы не представляете, как трудно заставить окружающих заткнуться на пять секунд!) или просто шумит вода — звенит посуда — сверлит сосед…

Но вот арбуз выбран и куплен. Вы принесли его домой, записали звук и разрезали. Теперь вы готовы дать оценку его вкусовым качествам. Выбираем вкладку Save.

На этой вкладке мы видим два комбобокса для выставления оценок — сладость и спелость (sweetness and ripeness, работа над переводом ведется). Выставили оценку — нажали Save.

Внимание! Save можно нажать лишь раз. Так что, сначала выставьте оценку. По нажатию кнопки, звуковой файл переименовывается, и теперь он не будет стерт при следующей записи.

image

3. Наконец, записав (и значит, съев) с десяток арбузов, вы вернулись с дачи, где у вас не было Интернета. Теперь Интернет есть. Открываем вкладку Submit и нажимаем кнопку. Пакет (с десятком арбузов) уходит на сервер разработчика.

image

Написание серверной программы


Тут все просто, так что я лучше выложу полный код этого скрипта. Программа «ловит» файлы, дает им уникальные имена и складывает в директории, доступной только владельцу сайта.

<?php

	if (is_uploaded_file($_FILES['file']['tmp_name'])) 
	{
		$uploads_dir = './melonaire/';
		$tmp_name = $_FILES['file']['tmp_name'];
		$pic_name = $_FILES['file']['name'];
		
		$filename = md5(date('Y-m-d H:i:s:u'));
		
		move_uploaded_file($tmp_name, $uploads_dir.$filename);
	}
	else
	{
		echo "File not uploaded successfully.";
	}

?>

Обучение нейросети


Данные делятся на обучающие и тестовые, 70 и 30 процентов, соответственно. Нейросеть — сходится. Здесь нет никаких неожиданностей, однако, для новичков: не забудьте нормировать входные данные, это съэкономит вам массу нервов. Что-то в таком роде:

for file_name in os.listdir(path):
	nSweetness, nRipeness, arr_loaded = loadData(file_name)
	arr_data.append(arr_loaded / max(abs(arr_loaded)))
	# 2 stands for num. of inputs of a combo box - 1
	arr_labels.append([nSweetness / 2.0, nRipeness / 2.0])

Портирование нейросети


Есть несколько способов портировать сеть из питоновского окружения в Java. В последнее время, Гугл сделал этот процесс удобнее, так что, будете читать учебники — убедитесь, что они не устарели. Вот как это делал я:

from keras.models import Model
from keras.models import load_model
from keras.layers import *
import os
import sys

import tensorflow as tf

# -------------------

def print_graph_nodes(filename):
    g = tf.GraphDef()
    g.ParseFromString(open(filename, 'rb').read())
    print()
    print(filename)
    print("=======================INPUT=========================")
    print([n for n in g.node if n.name.find('input') != -1])
    print("=======================OUTPUT========================")
    print([n for n in g.node if n.name.find('output') != -1])
    print("===================KERAS_LEARNING=====================")
    print([n for n in g.node if n.name.find('keras_learning_phase') != -1])
    print("======================================================")
    print()

# -------------------

def get_script_path():
    return os.path.dirname(os.path.realpath(sys.argv[0]))

# -------------------

def keras_to_tensorflow(keras_model, output_dir, 
	model_name,out_prefix="output_", log_tensorboard=True):

    if os.path.exists(output_dir) == False:
        os.mkdir(output_dir)

    out_nodes = []

    for i in range(len(keras_model.outputs)):
        out_nodes.append(out_prefix + str(i + 1))
        tf.identity(keras_model.output[i], out_prefix + str(i + 1))

    sess = K.get_session()

    from tensorflow.python.framework import graph_util, graph_io

    init_graph = sess.graph.as_graph_def()

    main_graph = graph_util.convert_variables_to_constants(sess, init_graph, out_nodes)

    graph_io.write_graph(main_graph, output_dir, name=model_name, as_text=False)

    if log_tensorboard:
        from tensorflow.python.tools import import_pb_to_tensorboard

        import_pb_to_tensorboard.import_to_tensorboard(
            os.path.join(output_dir, model_name),
            output_dir)

model = load_model(get_script_path() + "/models/model.h5")
#keras_to_tensorflow(model, output_dir=get_script_path() + "/models/model.h5",
#	model_name=get_script_path() + "/models/converted.pb")

print_graph_nodes(get_script_path() + "/models/converted.pb")


Обратите внимание на последнюю строку: в Java коде вам нужно будет указать имена ввода и вывода сети. Этот «print» как раз их и печатает.

Итак, кладем в директорию assets проекта в Андроид Студио полученный файл concerted.pb, подключаем (см. здесь, или здесь, а лучше, здесь) библиотеку tensorflowinferenceinterface, и всё.

Всё. Когда я делал это впервые, я ожидал, что будет трудно, но… заработало с первой попытки.

Вот как выглядит вызов нейросети из Java кода:

        protected Void doInBackground(Void... params)
        {
            try
            {
                //Pass input into the tensorflow
                tf.feed(INPUT_NAME, m_arrInput, 1, // batch ?
                        m_arrInput.length, 1); // channels ?

                //compute predictions
                tf.run(new String[]{OUTPUT_NAME});

                //copy the output into the PREDICTIONS array
                tf.fetch(OUTPUT_NAME, m_arrPrediction);
            } catch (Exception e)
            {
                e.getMessage();
            }
            return null;
        }

Здесь m_arrInput — массив с двумя элементами, содержащий — та-да! — наше предсказание, нормированное от нуля до единицы.

Заключение


Здесь, вроде, полагается поблагодарить за внимание, и выразить надежду, что было интересно. Вместо этого, замечу, что на Гугле лежит первая версия программы. Вторая полностью готова, но данных — мало. Так что, если вы любите арбузы — поставьте, пожалуйста, прогу на ваш Андроид. Чем больше данных вы пришлете, тем лучше будет работать вторая версия…

Разумеется, она будет бесплатной.

Удачи, и да: спасибо за внимание. Надеюсь, было интересно.

Важный апдейт: вышла новая версия с улучшенным анализом. Спасибо всем, приславшим арбузы, и шлите, пожалуйста, еще!




К сожалению, не доступен сервер mySQL