Чтобы начать серию расширений Neo4j для машинного обучения, я реализовал набор определяемых пользователем процедур, которые создают модель линейной регрессии в базе данных графов. В этом посте я продемонстрирую использование линейной регрессии в браузере Neo4j, чтобы предложить цены на краткосрочную аренду в Остине, штат Техас. Давайте посмотрим на пример использования:

Самый популярный район в Остине, штат Техас, определяется двумя последними цифрами его почтового индекса: «04». «04» с самыми модными клубами, ресторанами, магазинами и парками часто посещают туристы. Предположим, вы местный житель Остина, который уезжает в отпуск. Вы хотите нажиться на популярности своего района, сдавая дом в аренду на время вашего отсутствия. Однако вы не знаете, сколько будет взимать за ночь.

Здесь Уильям Лайон использует Neo4j для моделирования существующей краткосрочной аренды жилья в Остине, штат Техас. Помимо цены за ночь, каждый узел Listing содержит такие свойства, как количество спален, количество ванных комнат, количество размещенных гостей и доступность. В вашем доме четыре спальни, две ванные комнаты и могут разместиться пять человек. Как мы можем использовать данные о жилье, хранящиеся на графике Уилла, чтобы спрогнозировать подходящую ночную ставку для вашего объекта размещения?

Мы должны каким-то образом количественно определить взаимосвязь между неизвестной переменной (цена) и известными переменными (спальни, ванные комнаты и количество размещенных гостей). Многие специалисты по прогнозированию цен на рынке жилья используют общее количество комнат для прогнозирования цены на жилье. Они предполагают, что соотношение между ценой и количеством комнат линейное. Мы распространим эту идею на рынок краткосрочной аренды. Линейная регрессия - это инструмент, используемый для анализа взаимосвязи между переменными, и здесь ее можно использовать для количественной оценки взаимосвязи между стоимостью аренды за ночь и общим количеством комнат в объявлении.

Немного предыстории:

Регрессия - это линейный подход к моделированию взаимосвязи между двумя переменными. зависимая переменная «y» - это количество, которое мы хотели бы спрогнозировать (в данном случае стоимость аренды). Мы прогнозируем зависимую переменную, используя известное значение независимой переменной «x». Цель простой линейной регрессии - создать функцию, которая принимает независимую переменную в качестве входных данных и выводит прогноз для значения зависимой переменной. Помните уроки алгебры? Поскольку модель представляет собой линию, запись ее в форме y = a + b*x позволяет однозначно представить ее двумя параметрами:
наклоном (b) и пересечением (a).

Визуально есть график из нескольких (x, y) точек данных. Простая линейная регрессия находит линию, проходящую через точки, которые лучше всего подходят для данных. Чтобы определить линию наилучшего соответствия, мы используем метод наименьших квадратов, чтобы найти коэффициенты a и b, которые минимизируют сумму квадратов разностей между фактическое y -значение и его прогнозируемое y -значение в соответствии с линейной моделью y = a + b*x.

Есть несколько измерений, которые пытаются количественно оценить успех модели или то, насколько хорошо она «соответствует» данным. Например, коэффициент детерминации (R²) - это доля дисперсии зависимой переменной, которую можно спрогнозировать на основе независимой переменной. Коэффициент R² = 1 указывает, что дисперсия зависимой переменной полностью предсказуема по независимой переменной (и, таким образом, модель идеальна).

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

Подождите, это вообще проблема с графиком?

Метод наименьших квадратов восходит к началу 19 века. Он не полагается на тот факт, что данные могут храниться в виде графика. Зачем реализовывать линейную регрессию в Neo4j, если это не проблема с графом? Потому что в нашем случае интересующие нас данные уже живут на графике!

График - это логичный выбор для модели данных об аренде из-за неотъемлемой взаимосвязанности данных о краткосрочной аренде, смоделированных как
(:User)-[:WRITES]->(:Review)-[:REVIEWS]->(:Listing)
, и постоянных обновлений списков, пользователей и отзывов. Линейная регрессия не может быть проблемой графа, но набор данных в целом уверен. Прогнозирование цен - это лишь часть общего анализа, который мы хотели бы провести. Реализация процедуры регрессии в Neo4j позволяет нам избежать хлопот с экспортом данных в другое программное обеспечение. Таким образом, мы используем уникальные возможности графической базы данных, не забывая о более традиционных статистических методах, таких как линейная регрессия. И мы создаем еще один строительный блок для запуска более сложных конвейеров анализа данных нашего графика.

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

Давайте начнем

Установите процедуры

Создайте проект и базу данных в Neo4j Desktop. Загрузите jar-файл из последней версии линейной регрессии. Перетащите файл JAR в папку плагинов вашей базы данных. Добавьте содержимое пакета regression к распознанным процедурам в нижней части Управление- ›Настройки. Если у вас уже установлены APOC и Graph Algorithms, это будет выглядеть примерно так:

dbms.security.procedures.whitelist=algo.*,apoc.*,regression.*

Перезагрузите базу данных. Запустить

CALL dbms.procedures() YIELD name WHERE name CONTAINS 'regr' RETURN *

чтобы убедиться, что regression.linear.* процедуры готовы к использованию.

Импортировать данные

Откройте браузер Neo4j. Запустите :play http://guides.neo4j.com/listings из браузера Neo4j и следуйте запросам импорта, чтобы создать график списка краткосрочной аренды Уилла.

Добавьте наш список

Создайте новый узел Listing, чтобы представить дом, который мы хотели бы добавить на рынок краткосрочной аренды «04». На графике каждый Neighborhood можно идентифицировать по его почтовому индексу, хранящемуся в свойстве neighborhood_id.

MATCH (hood:Neighborhood {neighborhood_id:'78704'})
CREATE (list:Listing 
      {listing_id:'12345', bedrooms:4, bathrooms:2, accommodates:5}) 
MERGE (list)-[:IN_NEIGHBORHOOD]->(hood)

Разделение данных обучения и тестирования

Какое подмножество данных о жилье мы должны использовать для создания модели? Цены на жилье варьируются в зависимости от местоположения, поэтому давайте создадим модель, используя только объявления, расположенные в районе «04». Узлы листинга содержат свойства bedrooms и bathrooms, поэтому мы берем сумму этих двух значений как приблизительное общее количество комнат. Таким образом, наш набор данных - это все листинги в окрестности «04» с допустимыми свойствами price, bedrooms и bathrooms.

Мы могли бы использовать весь набор данных для обучения модели, но тогда мы не смогли бы протестировать производительность модели на невидимых данных. Поэтому мы разбиваем набор данных на выборку 75:25 (обучение: тестирование). Соберите идентификаторы узлов из всех подходящих узлов списка и используйте функцию regression.linear.split для случайного выбора 75% набора данных. Обозначьте это подмножество Train.

MATCH (list:Listing)-[:IN_NEIGHBORHOOD]->(:Neighborhood {neighborhood_id:'78704'}) 
WHERE exists(list.bedrooms) AND exists(list.bathrooms)
AND exists(list.price) 
WITH regression.linear.split(collect(id(list)), 0.75) AS trainingIDs
MATCH (list:Listing) WHERE id(list) in trainingIDs 
SET list:Train

Добавьте метку :Test к остальным узлам листинга в наборе данных.

MATCH (list:Listing)-[n:IN_NEIGHBORHOOD]->(hood:Neighborhood {neighborhood_id:'78704'})
WHERE exists(list.bedrooms) AND exists(list.bathrooms)
AND exists(list.price) AND NOT list:Train SET list:Test

Инициализировать модель

Если посмотреть на нашу create процедуру, у нее есть такая подпись:

create('model-name', 'model-type' = 'Simple',
       constantTerm? = true, noOfIndependentVars = 1)

Чтобы обеспечить гибкость для будущих реализаций множественной линейной регрессии с использованием той же create процедуры, вы должны ввести тип модели 'Simple'. Процедура создания также принимает в качестве аргументов, должна ли модель включать постоянный член и количество независимых переменных. По умолчанию они равны true и 1 соответственно. Исключайте постоянный член usingfalse только в том случае, если вы знаете, что ваши данные следуют линейной взаимосвязи на всем своем домене и должны проходить через источник.

CALL regression.linear.create('rental prices', 'Simple', true, 1)

Добавить данные для обучения

Найдите все Listing узлы, помеченные Train, и добавьте каждую соответствующую (bedrooms, price) точку данных. Наша модель будет менее точной, если мы случайно добавим данные из одного и того же листинга несколько раз. В качестве дополнительной меры предосторожности давайте также пометим Listing узлы меткой Seen, чтобы указать, что обучающие данные уже были добавлены в модель:

MATCH (list:Listing:Train) WHERE NOT list:Seen 
CALL regression.linear.add('rental prices',
     [list.bedrooms+list.bathrooms], list.price) 
SET list:Seen

Обратите внимание, что процедура regression.linear.add принимает независимые переменные типа List<Double>, хотя это простая линейная регрессия. Это оставляет гибкость для создания множественной линейной регрессии (с множеством независимых переменных) в будущем и продолжения использования той же процедуры.

Проверить модель

Обратите внимание, что в любой момент вы можете вызвать info процедуру. Он всегда будет возвращать поток информации о модели и будет заполнять карты trainInfo и testInfo статистикой данных обучения и тестирования, если состояние модели указывает, что эти поля действительны.

CALL regression.linear.info('rental prices')

Добавить данные тестирования

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

MATCH (list:Listing:Test) WHERE NOT list:Seen 
CALL regression.linear.add('rental prices', 
               [list.bedrooms + list.bathrooms], list.price, 'test') 
SET list:Seen

Обратите внимание, что для добавления тестовых данных мы используем ту же процедуру add, но вместо этого устанавливаем тип данных (который принимает значение по умолчанию 'train') на 'test'.

Провести тестовые расчеты

Когда мы завершим наши данные тестирования, вызовите test процедуру для анализа модели.

CALL regression.linear.test('rental prices')

В этом случае обучение R² = 0,363 и тестирование R² = 0,456. Модель предсказывает большую долю дисперсии, наблюдаемой в данных тестирования, чем при обучении! Хотя оба значения немного ниже, чем нам хотелось бы (в идеале R² составляет 0,6 или больше), эти числа указывают на то, что модель работает аналогичным образом с невидимыми данными. Для визуального понимания вы можете видеть, что данные тестирования расположены ближе к линии тренда, чем данные обучения. Это связано с тем, что было выбрано меньшее количество точек тестовых данных:

Делайте и сохраняйте прогнозы

Эти процедуры линейной регрессии построены на SimpleRegression из библиотеки Commons Math. Хотя процедура regression.linear.train существует и будет использоваться для будущих линейных моделей с несколькими независимыми переменными, для простой регрессии вызов процедуры train не требуется (при вызове она просто вернет информацию о модели). Модель может делать прогнозы всякий раз, когда в модели есть две или более точки данных. Вам как пользователю решать, проводить ли модель через этап тестирования, прежде чем делать прогнозы.

Теперь, когда мы протестировали модель, мы можем делать обоснованные прогнозы цен. Мы можем спрогнозировать цену для вашего объявления с 4 спальнями и 2 ванными комнатами (всего примерно 6 комнат) в категории «04» с помощью определяемой пользователем функции regression.linear.predict:

RETURN regression.linear.predict('rental prices', [6])

Или сделайте и сохраните на графике прогнозы для каждого списка «04» с неизвестной ценой:

MATCH (list:Listing)-[:IN_NEIGHBORHOOD]->
      (:Neighborhood {neighborhood_id:'78704'})
WHERE exists(list.bedrooms)and exists(list.bathrooms) 
  AND NOT exists(list.price)
WITH list, list.bedrooms + list.bathrooms as rooms
SET list.predicted_price = 
  regression.linear.predict('rental prices', [rooms])
RETURN DISTINCT rooms, list.predicted_price AS predictedPrice 
ORDER BY rooms

Теперь во всех объявлениях «04» хранятся рекомендованные цены, которые могут быть представлены хозяевам, чтобы помочь им сделать осознанный выбор цены.

Редактировать данные тестирования / обучения

Мы ничего не знаем о нашем наборе данных, за исключением того, что все записи расположены в «04» и содержат действительные данные bedrooms, bathrooms и price. Давайте немного уточним нашу модель, попытавшись исключить непопулярные объявления. Мы будем продолжать перечислять только те точки данных, которые являются частью отношения (:Review)-[:REVIEWS]->(:Listing), что указывает на то, что список имеет хотя бы один отзыв. Надеюсь, это устранит данные из новых для рынка данных (с потенциально ненадежной ценой).

Сначала создайте новую модель, скопировав данные обучения из модели «цены на аренду».

CALL regression.linear.create('popular rental prices', 'Simple')

CALL regression.linear.copy('rental prices', 
                            'popular rental prices')

Затем отредактируйте модель, удалив данные из объявлений без отзывов.

MATCH (list:Listing:Train) 
WHERE NOT (:Review)-[:REVIEWS]->(list) 
CALL regression.linear.remove('popular rental prices',  
       [list.bedrooms+list.bathrooms], list.price) 
REMOVE list:Train, list:Seen

Теперь мы должны удалить данные тестирования из объявлений без отзывов. По мере добавления данных тестирования в модель вычисления выполняются с использованием параметров модели, созданных во время обучения. Следовательно, если какие-либо данные обучения обновляются, модель изменяется, и все вычисления тестирования недействительны. Изменение данных обучения автоматически обнуляет данные тестирования. Следовательно, мы должны удалить метку Test из объявлений без отзывов, а затем заново добавить все оставшиеся тестовые данные.

Обратите внимание: если во время тестирования вашей модели вы хотите обновить данные тестирования, вы можете вызвать regression.linear.add или regression.linear.remove без повторного добавления всех данных тестирования. Это работает, пока остаются измененными данные обучения.

MATCH (list:Listing:Test) WHERE NOT (:Review)-[:REVIEWS]->(list) REMOVE list:Test

MATCH (list:Listing:Test) 
CALL regression.linear.add('popular rental prices', 
              [list.bedrooms + list.bathrooms], list.price, 'test') 
RETURN count(list)

CALL regression.linear.test('popular rental prices')

Вау, исключение «непопулярных» данных о листинге улучшило соответствие нашей модели! Теперь обучение R² = 0,500 и тестирование R² = 0,561. Оба увеличились, и R² тестирования снова выше обучения. Это указывает на то, что наша модель лучше объясняет расхождения в данных о ценах и хорошо работает с невидимыми данными. Обновим прогнозируемые цены на графике:

MATCH (list:Listing)-[:IN_NEIGHBORHOOD]->
      (:Neighborhood {neighborhood_id:’78704'})
WHERE exists(list.bedrooms)and exists(list.bathrooms) 
      AND NOT exists(list.price)
WITH list, list.bedrooms + list.bathrooms AS rooms
SET list.predicted_price = 
      regression.linear.predict('popular rental prices', [rooms])
RETURN DISTINCT rooms, list.predicted_price AS predictedPrice 
ORDER BY rooms

Сохраните, удалите и перезагрузите модель

Мы закончили анализ на сегодня! Сохраним только что созданную модель. Каждый именованный экземпляр SimpleRegression сохраняется как статическая переменная в пользовательской процедуре. Эти переменные очищаются каждый раз при завершении работы базы данных. Убедитесь, что перед выключением базы данных вы сохранили сериализованную версию любой модели, которую хотите сохранить на графике или во внешнем виде:

MERGE (m:ModelNode {model: 'popular rental prices'})
SET m.data = regression.linear.data('popular rental prices')

Удаляем модель:

CALL regression.linear.delete('popular rental prices')

При перезапуске базы данных загрузите модель из графика обратно в процедуру:

MATCH (m:ModelNode {model: 'popular rental prices'})
CALL regression.linear.load(m.model, m.data, 'Simple')
YIELD model, framework, hasConstant, numVars, state, nTrain, nTest, trainInfo, testInfo
RETURN *

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

Дополнительная функциональность

Обратите внимание, что существуют следующие процедуры, но они не были описаны в этом посте:

  • regression.linear.clear-удаляет все данные из модели по умолчанию или только данные тестирования с дополнительным аргументом 'test'
  • regression.linear.addM-Добавляет соответствующие точки данных из списков входных / выходных данных. Полезно при добавлении данных из внешних источников, таких как файл CSV.
  • regression.linear.removeM-Удаляет соответствующие точки данных из списков входных / выходных данных

Вопросов? Комментарии? Вы эксперт по обновлению формул для вычисления суммы квадратов регрессии (SSR) для тестовых данных (мне нужна помощь)? Пожалуйста, свяжитесь с LinkedIn или @ML_auren.

Особая благодарность Грейс Тенорио за помощь в этом проекте. Загляните в ее блог или поговорите с ней в Twitter :)