pwd
'/home/sergey/myvagrant'

В данной статье я рассматриваю пример реализации алгоритма Alternating Least Squares для выдачи рекомендаций методом Collaborative Filtering на Spark.

Входными данными служит «длинная» матрица, строки которой являются одной рекомендацией. Столбцы:

  • User ID: кто дает рекомендацию
  • Object ID: что рекомендуют
  • Рекомендация: численное значение рекомендации (может быть 0/1, может быть диапазон, например, от 0 до 10).

Другое название алгоритма — matrix factorization. Алгоритм предполагает, что как рекомендуемые предметы, так и пользователи, обладают некоторыми «скрытыми» факторами, оптимальные веса которых находятся на известных рекомендациях итеративным способом путем минимизации ошибок (отсюда и первое назвние: Alternating Least Squares).

Данные для данного упражнения можно закачать с сайта http://grouplens.org/datasets/movielens/ . Это 1’000’000 оценок 4’000 кинофильмов от 6’000 пользователей.

Формат данных:

  • 1-ое поле: User ID
  • 2-ое поле: Movie ID
  • 3-е поле: рэйтинг (оценка)
  • 4-ое поле: timestamp
!cat ./movielense/ratings.dat | head -3
1::1193::5::978300760
1::661::3::978302109
1::914::3::978301968
cat: write error: Broken pipe

Как обычно, первое что мы должны сделать — создать RDD и закачать данные в Hadoop.

from pyspark import SparkContext
sc = SparkContext()
ratingsRDD = sc.textFile('./movielense/ratings.dat', 2)

Чтобы представить данные в формате, который понимается алгоритмом, т.е. (User ID, Movie ID, Rating) необходима вспомогательная функция parse_ratings()

def parse_ratings(line):
    line = line.split('::')
    return (int(line[0]), int(line[1]), float(line[2]))

ratingsParsedRDD = ratingsRDD.map(lambda x : parse_ratings(x))

ratingsParsedRDD.take(3)
[(1, 1193, 5.0), (1, 661, 3.0), (1, 914, 3.0)]

Теперь, когда мы подготовили данные, мы можем перейти непосредственно к машинному обучению. Для этого, загрузим функцию ALS из модуля MLlib и создадим trainRDD, validateRDD, и testRDD

from pyspark.mllib.recommendation import ALS
trainRDD, validateRDD, testRDD = ratingsParsedRDD.randomSplit([.6,.2,.2], seed=1)
trainRDD = trainRDD.cache()

Перед тем, как мы приступим непосредственно к машинному обучению при помощи ALS.train(rdd, rank, iterations, lambda_, seed), нам необходимо задать параметры:

  • rank — количество скрытых фич (см. введение или что такое matrix factorization)
  • iterations — количество итераций
  • lambda_ — regularization parameter

Главный аргумент функции — это непосредственно наши данные rdd вида (User ID, Movie ID, Rating)

rank = 4
seed = 1
iteration = 5
regularizationParameter = 0.1

mod = ALS.train(trainRDD, rank, iterations = iteration,
                lambda_= regularizationParameter, seed=seed)

Предсказания модели получаются при помощи mod.predictAll(rdd), где rdd — это (User ID, Movie ID) БЕЗ рейтинга:

validatePrunedRDD = validateRDD.map(lambda x: (x[0],x[1])).cache()
predictionsRDD = mod.predictAll(validatePrunedRDD)
predictionsRDD.take(3)
[Rating(user=4904, product=2762, rating=5.10816721338027),
 Rating(user=4904, product=3000, rating=4.709048196080243),
 Rating(user=4904, product=2398, rating=4.632924725890961)]

Сама по-себе валидация не несет на себе никакого смысла. Валидация используется для подбора гиперпараметров.
Для подобора параметров определим функцию RMSE() : Root Mean Squared Error:

import numpy as np
def RMSE(trueRDD, predictRDD):
    trueFormattedRDD = trueRDD.map(lambda x: ((x[0], x[1]), x[2]))
    predictFormattedRDD = predictRDD.map(lambda x: ((x[0], x[1]), x[2]))
    count = predictFormattedRDD.count()
    error = (trueFormattedRDD
             .join(predictFormattedRDD)
             .map(lambda x: (x[1][0]-x[1][1])**2)
             .reduce(lambda a,b: a+b))
    return np.sqrt(error/count)

Теперь мы можем выбрать лучшую модель при помощи Cross Validation:

ranks = [4,8,12]

for rank in ranks:
    mod = ALS.train(trainRDD, rank = rank, iterations=iteration,
                lambda_= regularizationParameter, seed=seed)
    predictionsRDD = mod.predictAll(validatePrunedRDD)
    rmse = RMSE(validateRDD, predictionsRDD)
    print('rank is: {:2d} | RMS Error is: {:.4f}'.format(rank, rmse))
rank is:  4 | RMS Error is: 0.8920
rank is:  8 | RMS Error is: 0.8908
rank is: 12 | RMS Error is: 0.8963

Лучшей моделью является та, которая «в среднем» дает меньшую ошибку предсказания (в данном случае измеряется RMSE), т.е. модель с 8-ю скрытыми фичами.

Использование модели для выдачи рекомендаций

Допустим, у нас есть клиент с "Client ID" = 0 и 10 «условными» рекомендациями вида (Movie ID, Recommendation):

clientID0 = [
    (1,3),
    (37,4),
    (11,5),
    (567,2),
    (38,5),
    (245,5),
    (589,4),
    (76, 1),
    (3563,5),
    (1001,4)    
]

Какие top10 фильмов мы еще можем порекомендовать данному клиенту, на основе его выбора?

Чтобы ответить на этот вопрос:

1. Добавим данного клиента в базу данных.

clientID0RDD = sc.parallelize(clientID0,2).map(lambda x: (0, x[0], x[1]))
totalRDD = testRDD.union(clientID0RDD)

2. Перетренируем модель (с учетом найденных оптимальных гиперпараметров).

mod0 = ALS.train(totalRDD, rank=8, iterations=5, lambda_=.1, seed=seed)

3. Предскажем top10 фильмов, которые будут интересны User ID=0

top10 = mod0.recommendProducts(0,10)
top10 = {k:v for (_,k,v) in top10}
top10
{119: 7.326464711645337,
 213: 5.6742506040069545,
 1014: 5.366592013406946,
 1117: 5.584937403439358,
 1545: 5.82351913905598,
 1570: 5.76793720538722,
 2843: 5.477880077644354,
 2923: 5.439076322729706,
 3449: 5.285041773719348,
 3612: 5.2848412947840435}

4. Сделаем «красивый» вывод: (Фильм | ID | Рейтинг )

!cat ./movielense/movies.dat | head -3
1::Toy Story (1995)::Animation|Children's|Comedy
2::Jumanji (1995)::Adventure|Children's|Fantasy
3::Grumpier Old Men (1995)::Comedy|Romance
cat: write error: Broken pipe
def parse_movies(line):
    line=line.split('::')
    return (int(line[0]), line[1])

allMoviesRDD = (sc.textFile('./movielense/movies.dat', 2)
                .map(lambda x: parse_movies(x)))

preds = allMoviesRDD.filter(lambda x: x[0] in top10).collect()

for item in preds:
    print('Фильм: {:52s} || ID: {:4d} || Рекомендация: {:.2f}'.format(item[1], item[0], top10[item[0]]))
Фильм: Steal Big, Steal Little (1995)                       || ID:  119 || Рекомендация: 7.33
Фильм: Burnt By the Sun (Utomlyonnye solntsem) (1994)       || ID:  213 || Рекомендация: 5.67
Фильм: Pollyanna (1960)                                     || ID: 1014 || Рекомендация: 5.37
Фильм: Eighth Day, The (Le Huiti�me jour ) (1996)           || ID: 1117 || Рекомендация: 5.58
Фильм: Ponette (1996)                                       || ID: 1545 || Рекомендация: 5.82
Фильм: Tetsuo II: Body Hammer (1992)                        || ID: 1570 || Рекомендация: 5.77
Фильм: Black Cat, White Cat (Crna macka, beli macor) (1998) || ID: 2843 || Рекомендация: 5.48
Фильм: Citizen's Band (a.k.a. Handle with Care) (1977)      || ID: 2923 || Рекомендация: 5.44
Фильм: Good Mother, The (1988)                              || ID: 3449 || Рекомендация: 5.29
Фильм: Slipper and the Rose, The (1976)                     || ID: 3612 || Рекомендация: 5.28
  1. 19.12.2016

    Замечательная статья, спасибо!

    Однако если я ничего не перепутал, нельзя назвать тот метод подбора гиперпараметров «используя кросс-валидацию», так как при кросс-валидации вы должны делать примерно следующее:
    1. Разбиваете сет на N частей
    2. 1. Откладываете первую, на остальных учитесь, смотрите скор на первой
    2. Откладываете вторую на остальных учитесь, смотрите скор на второй

    3. Потом каким либо образом оценивает модель по сборам на всех отложенных выборках

    • 20.12.2016

      Да, отчасти, Вы правы. Классическая кросс-валидация (ключевое слово — «кросс») — это именно тот процесс, который Вы описываете (пример — реализация «кросс-валидации» в `R` в пакете `caret` или в `Python` в пакете `sklearn`). Здесь же приведен упрощенный пример, который можно назвать `1-fold cross-validation` (в терминологии классической книги по машинному обучению Applied Predictive Modelling by Max Kuhn или Statistical Learning by Gareth etc). Так что с приведенной оговоркой, я бы все таки не стал лишать вышеописанный метод названия «cross-validation» )))

  2. 10.05.2018

    1. Зачем делать перетренировку модели перед получением предсказания?
    Можно ли без этого?
    2. Какого типа предсказания мы получаем в итоге?
    Предсказания типа: какую оценку бы поставил Вася такому-то фильму?
    А если я хочу ему просто порекомендовать фильм, что мне делать?
    Сортировать рекомендованные фильмы?

  3. 10.05.2018

    Подскажите, как решить проблему:
    для предсказания надо добавить данные в датасет и перетренировать модель.
    А модель тренируется долго. От 10 секунд на вашем примере.
    Реалтайм так не построить ;0(

Write a comment:

*

Your email address will not be published.

© 2014 In R we trust.
Top
Follow us: