Что делать, если быстрой и простой совместной фильтрации уже недостаточно.

Знай свою отправную точку

Важно помнить, что не следует начинать с сложных сценариев использования рекомендательных систем. После быстрой и простой реализации версии 1 ваш механизм рекомендаций становится максимально адаптированным к вашим бизнес-задачам, продуктам и пользователям. Чтобы построить такую ​​индивидуальную модель, вам понадобится цикл обратной связи и базовый план из версии 1, чтобы определить направления улучшений для вашего механизма рекомендаций. С индивидуальными решениями Agile - лучшая методология для достижения успеха. На практике это означает, что вы создаете версии 1.1, 1.2 и 1.3 задолго до того, как начнете работать над версией 2.

Для вашего механизма рекомендаций версии 1 у вас есть несколько вариантов в зависимости от вашей отправной точки:

  • Если вы знаете свои продукты или имеете обширные метаданные о своих продуктах, вы можете сначала создать простой механизм правил, захватывающий бизнес-логику. Помня о гибкости и прагматичности доставки, важно знать, когда машинное обучение не требуется.
  • Если вы также знаете своих пользователей, вы даже можете персонализировать свой механизм правил с использованием некоторой базовой сегментации пользователей, например RFM.
  • Но в большинстве случаев вы не знаете достаточно или даже ничего о своих пользователях и продуктах. Все, что у вас есть, - это данные о поведении пользователей при взаимодействии с вашим веб-сайтом или вашими продуктами. В этом случае вам следует рассмотреть возможность совместной фильтрации для создания своего механизма рекомендаций версии 1. Этот вариант использования подробно объясняется в:


ВАШ путь к версии 2

Поздравляем, после того, как вы развернули версию 1 своего механизма рекомендаций, пора отпраздновать свое достижение! У вас есть первая система рекомендаций, и она работает достаточно хорошо, чтобы заинтересованные стороны хотели, чтобы вы улучшили ее, а не отключили. 🎉

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

Проблемы, с которыми вы столкнетесь в версии 1:

  • Проблемы с холодным запуском нового контента и пользователей
  • Предвзятое отношение к содержанию клик-приманок: хорошие показатели отклика, за которыми следует резкое падение вниз по воронке.
  • Сложный компромисс между качеством и количеством ваших рекомендаций
  • Плохой UX
  • Дорогие счета за кластеры ETL и elasticsearch

Давайте посмотрим, что мы на самом деле создали в Части 1, и как это соотносится с более сложными системами рекомендаций:

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

Сделайте вашу модель отслеживающей состояние для лучшего скоринга модели

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

В Части 1 вы построили модель без состояния. Модели без сохранения состояния не запоминают и не заботятся о предшествующих событиях. Каждое взаимодействие с пользователем и запрос оценки полностью независимы. Следовательно, моделями без сохранения состояния действительно легко управлять, и они позволяют, например, горизонтальное масштабирование за простым балансировщиком нагрузки.

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

Однако вы не хотите добавлять состояние в саму модель. В идеале вы всегда отделяете диспетчер состояний от остальной модели. Диспетчер состояний должен быть независимой микрослужбой, которая обогащает входящий запрос без сохранения состояния, то есть от вашего веб-сайта или приложения, предыдущими взаимодействиями этого пользователя с использованием некоторой формы кеша. Управление состоянием как независимым микросервисом - отличный принцип дизайна, а также часть дизайна Rendezvous Architecture, о котором я писал ранее (в этом сообщении я назвал его Model Data Enricher):



Redis - отличное решение для обработки состояния системы рекомендаций. Это быстрый, легкий и полностью управляемый сервис в любом облаке, например AWS ElasticCache для Redis. В приведенном ниже коде используется скользящее окно TTL для хранения действий пользователя и автоматического удаления старых взаимодействий из кеша. Не стесняйтесь адаптировать его под свои нужды:



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

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

Добавляйте все больше и больше данных

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

  • Искать продукты
  • Просмотр сведений о продукте
  • Покупайте товары и возвращайте товары
  • Оцените и поделитесь продуктами
  • Участвуйте в маркетинге, используйте коды скидок

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

Самый простой вариант - добавить дополнительный тип документа к существующему кластеру Elasticsearch, следуя точно такой же логике, как и с первой метрикой. Типы документов в Elasticsearch сопоставимы с таблицами в RDB. Затем вы запускаете 2 параллельных запроса к каждому типу документа соответственно, которые можно независимо настраивать, отражая качество метрик. Этого можно достичь, настроив параметр min_doc_count в агрегации important_terms Elasticsearch или переключив статистику, используемую для определения важной рекомендации. Вы можете прочитать о доступных опциях в документации и посмотреть примеры кода в другой моей статье:



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

Оборотная сторона этого варианта: увеличение стоимости кластера Elasticsearch.

По мере того, как вы добавляете все больше и больше показателей в свою модель, вы заметите, что не только стоимость вашего кластера Elasticsearch выходит из-под контроля, но и время и ресурсы ЦП для обработки сигналов в вашей системе правил также резко возрастают. Ваша система правил, преобразующая сигналы в окончательные рекомендации, становится значительно сложнее. Обзор механизма правил, вероятно, сделает вывод, что вы обрабатываете сложные отношения между своими данными и для этих случаев есть гораздо лучшие решения: вероятно, пора рассмотреть базу данных графиков!

База данных Graph - идеальное решение для высокореляционных данных, например большие объемы отношений, а также сложные отношения. Вместо того, чтобы разрешать эти отношения на лету с каждым отдельным запросом, БД графа материализует эти отношения в виде ребер в вашем графе, чтобы значительно ускорить запросы на основе отношений между данными (узлами). Существует много поставщиков графических БД, и в этой статье не рассматриваются какие-либо рекомендации. Это может быть темой следующего сообщения в блоге.

Плюс этого варианта: может обрабатывать сложные отношения, быстро открывая сложные сценарии использования со сложной бизнес-логикой.

Оборотная сторона этого варианта: вам необходимо преобразовать логику рекомендаций в графические запросы.

Улучшите свое пользовательское определение

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

Как следствие, в матрице совместной встречаемости, построенной на основе истории взаимодействий с пользователем, будет гораздо меньше сигналов.

Это может иметь место, когда ваши пользователи используют несколько устройств и не имеют особых стимулов для создания учетной записи или входа в систему для использования ваших услуг. Если на вашем сайте большая часть анонимного трафика, стоит взглянуть на вероятностную модель, чтобы идентифицировать одного пользователя с течением времени и на разных устройствах.

Gartner назвал идентификацию между устройствами (XDID) новой технологией, за которой нужно следить, и наука о данных призвана помочь. Существуют различные варианты идентификации пользователей: вход в учетную запись, идентификация файлов cookie, снятие отпечатков пальцев с устройства и сопоставление IP-адресов.

Сопоставление IP-адресов - это более низкий вероятностный вариант выявления отдельных пользователей. Однако IP-адреса доступны для разных устройств. Есть проблемы с динамическими и общими IP-адресами, как в случае сетей 4G или общедоступного Wi-Fi. В любом случае IP-адреса являются наиболее приемлемым вариантом. Я написал целую статью на эту тему об использовании Графика IP-адресов и временных меток для сопоставления отдельных пользователей с движком Spark GraphX:



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

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

Пакетное вычисление сигналов

До сих пор мы всегда использовали Elasticsearch в качестве серверной части для вычисления сигналов для рекомендаций на лету. Это полезно на ранних этапах разработки дорожной карты рекомендаций, чтобы позволить гибкое экспериментирование, например с соотношением количества и качества. Обратной стороной использования Elasticsearch таким образом является то, что каждый запрос требует значительных затрат ЦП. Если совместная фильтрация подходит для ваших сценариев использования, то, вероятно, лучше выбрать лучший алгоритм совместной фильтрации и более эффективно вычислять рекомендательные сигналы в пакетном режиме, а не на лету. Таким образом, мы сохраняем Elasticsearch только в качестве уровня представления для предоставления рекомендаций пользователям.

Мы можем просто вычислить сигналы одновременного появления элемента-элемента в пакете. Это можно сделать с помощью реализации Mahout's spark-itemsimilarity, которая существует с помощью интерфейса командной строки. Это так же просто, как вызов программы Spark в библиотеке Mahout и предоставление ей путей к соответствующим данным. Вы можете найти подробную документацию на сайте Mahout. Вы также видите сходство архитектуры решения Mahout и нашей. Мы используем Elasticsearch, а не Solr и Redis в качестве нашего кеша для взаимодействия с пользователем. Остальное следует той же архитектуре решения.

spark-itemsimilarity Mahout 1.0
Usage: spark-itemsimilarity [options]
Input, output options
-i <value> | --input <value>
Input path, may be a filename, directory name, or comma delimited list of HDFS supported URIs (required)
-i2 <value> | --input2 <value>
Secondary input path for cross-similarity calculation, same restrictions as "--input" (optional). Default: empty.
-o <value> | --output <value>
Path for output, any local or HDFS supported URI (required)
Algorithm control options:
-mppu <value> | --maxPrefs <value>
Max number of preferences to consider per user (optional). Default: 500
-m <value> | --maxSimilaritiesPerItem <value>
Limit the number of similarities per item to this number (optional). Default: 100
Note: Only the Log Likelihood Ratio (LLR) is supported as a similarity measure.
Input text file schema options:
-id <value> | --inDelim <value>
Input delimiter character (optional). Default: "[,\t]"
-f1 <value> | --filter1 <value>
String (or regex) whose presence indicates a datum for the primary item set (optional). Default: no filter, all data is used
-f2 <value> | --filter2 <value>
String (or regex) whose presence indicates a datum for the secondary item set (optional). If not present no secondary dataset is collected
-rc <value> | --rowIDColumn <value>
Column number (0 based Int) containing the row ID string (optional). Default: 0
-ic <value> | --itemIDColumn <value>
Column number (0 based Int) containing the item ID string (optional). Default: 1
-fc <value> | --filterColumn <value>
Column number (0 based Int) containing the filter string (optional). Default: -1 for no filter
Using all defaults the input is expected of the form: "userID<tab>itemId" or "userID<tab>itemID<tab>any-text..." and all rows will be used
File discovery options:
-r | --recursive
Searched the -i path recursively for files that match --filenamePattern (optional), Default: false
-fp <value> | --filenamePattern <value>
Regex to match in determining input files (optional). Default: filename in the --input option or "^part-.*" if --input is a directory
Output text file schema options:
-rd <value> | --rowKeyDelim <value>
Separates the rowID key from the vector values list (optional). Default: "\t"
-cd <value> | --columnIdStrengthDelim <value>
Separates column IDs from their values in the vector values list (optional). Default: ":"
-td <value> | --elementDelim <value>
Separates vector element values in the values list (optional). Default: " "
-os | --omitStrength
Do not write the strength to the output files (optional), Default: false.
This option is used to output indexable data for creating a search engine recommender.
Default delimiters will produce output of the form: "itemID1<tab>itemID2:value2<space>itemID10:value10..."
Spark config options:
-ma <value> | --master <value>
Spark Master URL (optional). Default: "local". Note that you can specify the number of cores to get a performance improvement, for example "local[4]"
-sem <value> | --sparkExecutorMem <value>
Max Java heap available as "executor memory" on each node (optional). Default: 4g
-rs <value> | --randomSeed <value>
-h | --help
prints this usage text

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

Оборотная сторона этого варианта: невысокая гибкость и изощренность. Это позволит контролировать ваши расходы на Elasticsearch, но не повлияет на эффективность ваших рекомендаций, если бы это было проблемой для вашей реализации версии 1.

Улучшите свой выбор алгоритма с помощью латентных факторных моделей

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

Модель скрытых факторов позволила бы нам создать и (1) модель элемент-элемент, используя сходство факторов продукта, и (2) модель элемент-пользователь.

Хорошим решением для пакетной модели скрытых факторов является реализация ALS (альтернативный метод наименьших квадратов) в Spark, метод матричной факторизации для механизмов рекомендаций. Это называется альтернативным методом наименьших квадратов, потому что алгоритм чередует улучшения элемента и факторов пользователя соответственно: алгоритм сначала фиксирует факторы пользователя и запускает градиентный спуск на факторах элемента. Затем он переключает и исправляет факторы предметов, чтобы улучшить факторы пользователя. Алгоритм ALS очень масштабируем, может работать в распределенном режиме параллельно и очень эффективно обрабатывать большие наборы данных с помощью Spark ML.

Вы можете найти пример реализации ALS с Spark ML в документации. Мы по-прежнему используем ту же архитектуру решения, о которой говорилось ранее, и Spark ML предоставляет методы для создания рекомендаций для модели пользователь-элемент.

Однако мы должны написать некоторый собственный код для вычисления сходства факторов продукта для модели элемент-элемент. Вычисление схожести между продуктами по всем парам плохо подходит для больших каталогов продуктов. Растущее число комбинаций O (n ^ 2) очень быстро приводит к чрезмерно дорогостоящим операциям перемешивания и недопустимому времени вычислений. Spark предлагает решение с хешированием с учетом местоположения (LSH), гораздо более эффективным подходом к определению приблизительного ближайшего соседа. LSH использует специальную хеш-функцию, чтобы уменьшить размерность данных, одновременно увеличивая вероятность хеш-коллизии, чем больше похожи данные. Это означает, что похожие данные, скорее всего, окажутся в одном сегменте, а не в сегментах с разными данными. LSH является вероятностным приближением и обеспечивает компромисс между скоростью и точностью. Для нашей проблемы кластеризации скрытых факторов элемента для модели рекомендации элемент-элемент мы используем Случайная проекция в качестве нашей хеш-функции, которая аппроксимирует косинусное сходство для сегментирования нашего продукта. векторов.

Следующий код является примером использования Spark ML ALS для совместной фильтрации и LSH для создания модели элемент-элемент на основе сходства элементов:

import pyspark.sql.functions as F
from pyspark.ml.evaluation import RegressionEvaluator
from pyspark.ml.recommendation import ALS
from pyspark.sql import Row
# ALS Example from the Spark Documentation
lines = spark.read.text("sample_movielens_ratings.txt").rdd
parts = lines.map(lambda row: row.value.split("::"))
ratingsRDD = parts.map(
    lambda p: Row(
        userId=int(p[0]), movieId=int(p[1]),
        rating=float(p[2]), timestamp=int(p[3])
    )
)
ratings = spark.createDataFrame(ratingsRDD)
(training, test) = ratings.randomSplit([0.8, 0.2])

# Build the recommendation model using ALS on the training data
# Note we set cold start strategy to 'drop' to ensure we don't get NaN evaluation metrics
als = ALS(
    maxIter=5, regParam=0.01, userCol="userId", itemCol="movieId", 
    ratingCol="rating", coldStartStrategy="drop"
)
model = als.fit(training)
# Evaluate the model by computing the RMSE on the test data
predictions = model.transform(test)
evaluator = RegressionEvaluator(
    metricName="rmse", labelCol="rating", predictionCol="prediction"
)
rmse = evaluator.evaluate(predictions)
# Root-mean-square error = 1.7866152217057665
####################################################
# User-Item model predictions using latent factors #
####################################################
# Generate top 10 movie recommendations for each user
userRecs = model.recommendForAllUsers(10)
# Generate top 10 user recommendations for each movie
movieRecs = model.recommendForAllItems(10)
userRecs.show(1, False)
“””
+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|userId|recommendations                                                                                                                                                      |
+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|28    |[[12, 4.987671], [81, 4.968367], [92, 4.862609], [31, 4.0329857], [49, 4.024806], [2, 3.8403687], [82, 3.7117398], [62, 3.4866638], [61, 3.4003847], [24, 3.1223223]]|
+------+-------------------------------------------------------------
“””
#####################################################
# Item-Item Model based on latent factor similarity #
#####################################################
from pyspark.ml.feature import BucketedRandomProjectionLSH
from pyspark.ml.linalg import Vectors, VectorUDT
vector_udf = F.udf(lambda l: Vectors.dense(l), VectorUDT())
itemFactors = model.itemFactors.withColumn(
    'itemVector', vector_udf(F.col('features'))
)
brp = BucketedRandomProjectionLSH(
    inputCol="itemVector", outputCol="hashes", bucketLength=2.0,
    numHashTables=3
)
# Hashes of factors match with a probability proportional to their
# cosine similarity
brp_model = brp.fit(itemFactors)
recommendations = (
   brp_model
    .approxSimilarityJoin(itemFactors, itemFactors, 2.0,
         distCol="dist")
    .select(
        F.col("datasetA.id").alias("idA"),
        F.col("datasetB.id").alias("idB"),
        F.col("dist")
    )
    .filter('idA != idB')
    .withColumn('recommendation', F.concat_ws('-',
        F.when(F.col('idA') < F.col('idB'),
        F.col('idA')).otherwise(F.col('idB')),
        F.when(F.col('idA') < F.col('idB'),
        F.col('idB')).otherwise(F.col('idA')))
    )
    .select('recommendation', 'dist').distinct()
    .orderBy(F.col('dist'))
)
recommendations.show()
“””
+--------------+------------------+
|recommendation|              dist|
+--------------+------------------+
|         15-99|0.6752175108957209|
|         78-86| 0.766118452902565|
|         17-46|1.1002650472525193|
|         15-97|1.1036687784393326|
|         15-78|1.1089519518538236|
“””

Полный пример кода можно найти здесь:



Достоинства этого варианта: более дешевая и масштабируемая пакетная модель, обеспечивающая модели «пользователь-элемент» и «элемент-элемент».

Оборотная сторона этого варианта: более сложная модель с большим количеством гиперпараметров для настройки на ALS и LSH.

Найдите больше приложений для ваших данных и моделей

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

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



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

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

Замените совместную фильтрацию своим алгоритмом

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

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

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

Обычно проще создавать функции, описывающие продукты, чем пользователей:

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

Я опубликовал предыдущий пост в блоге о прекрасном примере создания встраиваемых географических областей с использованием встраиваний поиска:



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

Мы используем сиамскую нейронную сеть, чтобы изучить отображение продукта или пользовательских функций из области для встраивания элемента в область для встраивания рекомендаций. Встраивание элементов рекомендаций или пользователей имеет гораздо более низкую размерность, чем вход NN, и желаемое поведение подобия, измеряемое по их косинусному сходству в пространстве встраивания рекомендаций. Изюминка сиамских нейронных сетей заключается в том, что веса обеих сетей идентичны, а градиентный спуск обеих нейронных сетей связан вместе. На практике это достигается за счет использования одной нейронной сети и последовательной оценки двух входных данных:

Мы используем косинусное подобие как результат нашей модели сиамской нейронной сети:

@staticmethod
def pairwise_cosine_sim(y1, y2):
    """Calculates the pairwise cosine similarity.
    """
    y1_norm = tf.nn.l2_normalize(y1,1)        
    y2_norm = tf.nn.l2_normalize(y2,1)
    return tf.matmul(y1_norm, y2_norm, transpose_b=True)

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

def contrastive_loss(cos_similarity, label, margin=1.0):
    """Contrastive loss function
    """
    distance = 1.0 - cos_similarity
    similarity = label * distance                                           
    dissimilarity = (1 - label) * tf.square(tf.maximum((margin - distance), 0))
    return 0.5 * tf.reduce_mean(dissimilarity + similarity)

Запас (от 0 до 2) можно использовать в качестве веса, определяющего, насколько тренировка должна быть направлена ​​на улучшение несходства по сравнению с сходством пар.

Нам также нужны маркированные данные для обучения. Мы можем получить метки из нашего кластера Elasticsearch, который мы использовали для версии 1. Элементы на переднем плане, отвечающие нашим требованиям к качеству, представляют собой положительные пары, тогда как другие элементы могут действовать как отрицательные пары.

Цикл обучения сиамской нейронной сети прост:

epochs = 10
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
for epoch in range(epochs):
    accuracy = tf.keras.metrics.BinaryAccuracy()
    epoch_loss = tf.keras.metrics.Mean()
    for item_pairs, labels in ds_train:
        with tf.GradientTape() as tape:
            item_similarities = model(np.array(item_pairs[0]), np.array(item_pairs[1]))
            loss = contrastive_loss(item_similarities, tf.reshape(labels, (1,-1)), margin=1.0)
            grad = tape.gradient(loss, model.trainable_variables)
        optimizer.apply_gradients(zip(grad, model.trainable_variables))
        epoch_loss.update_state(loss)
        accuracy.update_state(tf.reshape(labels, (1,-1)), item_similarities)
        
    print(
        "Epoch {:03d}: Loss: {:.3f} Accuraccy: {:.3f}".format(
            epoch,
            epoch_loss.result(),
            accuracy.result()
        )    
    )

На последнем графике показана обученная 2D-рекомендация, вмещающая пространство похожих фильмов из набора данных об объективах для фильмов Spark:

Плюс этого варианта: больше нет проблем с холодным запуском!

Недостаток этого варианта: вам понадобится широкий спектр полезных встраиваний элементов в качестве входных данных для сиамской нейронной сети. Определение обучающих этикеток требует экспериментов: для решения вашей бизнес-задачи, что определяет хорошую положительную или отрицательную пару элементов? Функция контрастных потерь проста в реализации, но дисбаланс пар положительных и отрицательных элементов может потребовать переключения на более сложную функцию потерь, например Тройной проигрыш.

Ян является успешным идейным лидером и консультантом в области преобразования данных в компаниях и имеет опыт масштабного внедрения науки о данных в коммерческое производство. Недавно dataIQ признал его одним из 100 самых влиятельных практиков в области данных и аналитики в Великобритании.

Подключитесь к LinkedIn: https://www.linkedin.com/in/janteichmann/

Прочтите другие статьи: https://medium.com/@jan.teichmann