Статьи

Обжигающий: Нахождение K ближайших соседей для удовольствия и прибыли


Представьте, что вы пользуетесь популярным сайтом электронной коммерции, посвященным модной и модной одежде для женщин, и у вас есть механизм рекомендаций по выбору, который рекомендует продукты на основе предыдущей истории покупок покупателя. Хотя механизм рекомендаций выполняет приличную работу, рекомендуя постоянным клиентам, он страдает от проблемы холодного запуска, когда новые или анонимные пользователи просматривают каталог сайта. В этом блоге мы попытаемся решить проблему «холодного старта», порекомендовав аналогичные продукты, которые просматривает покупатель, используя Scalding и несколько алгоритмов машинного обучения.
Scalding — это основанная на Scala абстракция над необработанным уменьшением карты из твиттера. Посмотрите
эту слайд-колоду от LinkedIn / Ebay, чтобы узнать о Scalding.

Прежде чем мы начнем проверять код, давайте разберемся с некоторыми бизнес-правилами.

       

  • Рекомендуемые товары должны быть в той же категории. Например, если кто-то смотрит вафельницу, и мы рекомендуем ему полотенце, это будет считаться плохой рекомендацией.
  •    

  • Рекомендуемые продукты должны быть в той же подкатегории. На нашем воображаемом сайте продаются двадцать различных типов джинсов. Если покупатель  выбирает джинсы Slimming Bootcut, Rinse Wash, мы должны попробовать другие джинсы bootcut.
  •    

  • Рекомендуемые товары должны быть в том же ценовом диапазоне. Например, если кто-то смотрит на туфли за 20 долларов, мы не будем рекомендовать ее за 100 долларов.

Для простоты мы будем предполагать, что наш каталог продуктов доступен в формате CSV, и наши продукты имеют пять атрибутов, а именно DEPARTMENT, SUB_DEPARTMENT, PRODUCT, DESCRIPTION, REG_PRICE, SALE_PRICE. Образец каталога продуктов доступен
здесь. Начнем с чтения каталога продуктов в режиме обжига и самостоятельного присоединения к DEPARTMENT, SUB_DEPARTMENT. Такое разделение каталога позаботится о первом бизнес-правиле рекомендации продуктов только из той же категории.

    /* * Schema of our product catalog */ 
    val inputSchema = ('DEPARTMENT, 'SUB_DEPARTMENT, 'PRODUCT, 'DESCRIPTION, 'REG_PRICE, 'SALE_PRICE)
    
    /* Duplicate schema used for self joining */ 
    val renameSchema = ('DEPARTMENT1, 'SUB_DEPARTMENT1, 'PRODUCT1, 'DESCRIPTION1, 'REG_PRICE1, 'SALE_PRICE1)
    
    /* Read in the catalog */  
    val productMatrix = Csv("input", separator = ",", fields = inputSchema, quote = "\"").read
    
    /* Read in the catalog a second time for joining */ 
    val productMatrixDuplicate = Csv("input", separator = ",", fields = inputSchema, quote = "\"").read.rename(inputSchema -> renameSchema)
    
    /** * Do a self join based on DEPARTMENT and SUB_DEPARTMENT */ 
    productMatrix.joinWithSmaller(('DEPARTMENT,'SUB_DEPARTMENT) -> ('DEPARTMENT1,'SUB_DEPARTMENT1), productMatrixDuplicate)
   

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

   
    "DEPARTMENT","SUB_DEPARTMENT","PRODUCT","DESCRIPTION","REG_PRICE","SALE_PRICE"
    "women","shoes","Marc Fisher Shoes","Pacca Pumps shoes","75.00","64.99"
    "women","shoes","Bandolino Shoes","Bayard Wedge Sandals shoes","59.00","49.99"
    "women","shoes","Nine West Shoes","Rocha Platform Pumps shoes","79.00","59.99"
    "women","shoes","MICHAEL Michael Kors Shoes","Fulton Moc Flats shoes","110.00","79.99"
   

Глядя на данные, мы можем использовать REG_PRICE и SALE_PRICE, чтобы рассчитать сходство на основе ценового диапазона. Мы рассчитаем Tanimoto Distance между продуктами, используя их REG_PRICE и SALE_PRICE. Обсуждение Tanimoto Distance выходит за рамки, но на практике, когда два товара имеют одинаковую цену и цену продажи, результат равен 0.0. Когда у них нет ничего общего, это 1.0.

Посмотрев на данные еще раз, мы увидим, что информация о стиле встроена в столбец ОПИСАНИЕ. К сожалению, мы не можем использовать Tanimoto Distance для описания сходства без преобразования и нормализации описания в числовые значения. Вместо этого мы рассчитаем расстояние NGram между описанием продукта. Хорошее описание расстояния NGram приведено здесь
pdf, но для нашего приложения расстояние NGram вернет значение от 1 до 0. Значение 1 означает, что указанные строки идентичны, а 0 означает, что строка максимально отличается. К счастью, в Apache Mahout и Lucene предусмотрены как вычисления Tanimoto Distance, так и NGram Distance, поэтому нам не нужно будет вручную проверять нашу собственную реализацию.

После того, как мы вычислили расстояние Tanimoto и NGram, мы можем объединить два расстояния, вычислив, как далеко они находятся от идеального соответствия, и сложив разницу. Простой способ убедиться, что мы правильно рассчитали все, — это рассчитать расстояние между одним и тем же продуктом и убедиться, что расстояние равно 0,0.

   
    /** * output Schema */ 
    val outputSChema = ('PRODUCT, 'PRODUCT1, 'Distance)
    
    /** * Map over the grouped fields and calculate distance  */
    .mapTo('* -> outputSChema) {
    in: (String, String, String, String, Double, Double, String, String, String, String, Double, Double) => calculateDistance(in)
    }
    
    /**
     * Calculates Tanimoto and NGram distance based on different product features and combine them together. 
     * @param in * @return 
     */ 
    def calculateDistance(in: (String, String, String, String, Double, Double, String, String, String, String, Double, Double)) = {
    val (_, _, p1_product, p1_description, p1_regPrice, p1_salePrice, _, _, p2_product, p2_description, p2_regPrice, p2_salePrice) = in
    
    val ngramDistance = 1 - MathUtils.round(ngram.getDistance(p1_description, p2_description).toDouble, SCALE)
    
    val p1_vector = new DenseVector(Array(p1_regPrice, p1_salePrice)) 
    val p2_vector = new DenseVector(Array(p2_regPrice, p2_salePrice)) 
    val tanimotoDistance = MathUtils.round(tanimotoDistanceMeasure.distance(p1_vector, p2_vector), SCALE) 
    
    val distance = MathUtils.round((tanimotoDistance + ngramDistance), SCALE) val result = (p1_product, p2_product, distance) result }
   

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

    
    .groupBy('PRODUCT) { g=> 
     g.sortBy('Distance).take(3) }
    
     .write(Csv("output", separator = ",", fields = outputSChema))




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

     
    //products
    "women","shoes","Rampage Shoes","Weatherly Booties shoes","59.99","35.99"
    "women","shoes","Marc Fisher Shoes","Pacca Pumps shoes","75.00","64.99"
    "women","shoes","Bandolino Shoes","Bayard Wedge Sandals shoes","59.00","49.99"
    "women","shoes","Nine West Shoes","Rocha Platform Pumps shoes","79.00","59.99"
    "women","shoes","MICHAEL Michael Kors Shoes","Fulton Moc Flats shoes","110.00","79.99"




    //top 3 recommendations for "Marc Fisher Shoes Pacca Pumps shoes"
     Nine West Shoes,0.48493
     Bandolino Shoes,0.73206
     MICHAEL Michael Kors Shoes,0.75641
  

    Как мы видим, главной рекомендацией для «
Marc Fisher Shoes Pacca Pumps » является «
Nine West Shoes Rocha Platform Pumps ». Эти две туфли не только находятся в одинаковом ценовом диапазоне, но и имеют одинаковый стиль обуви. Исходя из нашего результата, кажется, что мы можем достичь всех трех целей нашего бизнеса даже с очень небольшой выборкой данных. Полный исходный код для задания Рекомендации по продукту находится
здесь .