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
4 Comments
Ilya
19.12.2016
Замечательная статья, спасибо!
Однако если я ничего не перепутал, нельзя назвать тот метод подбора гиперпараметров «используя кросс-валидацию», так как при кросс-валидации вы должны делать примерно следующее:
1. Разбиваете сет на N частей
2. 1. Откладываете первую, на остальных учитесь, смотрите скор на первой
2. Откладываете вторую на остальных учитесь, смотрите скор на второй
…
3. Потом каким либо образом оценивает модель по сборам на всех отложенных выборках
admin
20.12.2016
Да, отчасти, Вы правы. Классическая кросс-валидация (ключевое слово — «кросс») — это именно тот процесс, который Вы описываете (пример — реализация «кросс-валидации» в `R` в пакете `caret` или в `Python` в пакете `sklearn`). Здесь же приведен упрощенный пример, который можно назвать `1-fold cross-validation` (в терминологии классической книги по машинному обучению Applied Predictive Modelling by Max Kuhn или Statistical Learning by Gareth etc). Так что с приведенной оговоркой, я бы все таки не стал лишать вышеописанный метод названия «cross-validation» )))
Veaceslav Kunitki
10.05.2018
1. Зачем делать перетренировку модели перед получением предсказания?
Можно ли без этого?
2. Какого типа предсказания мы получаем в итоге?
Предсказания типа: какую оценку бы поставил Вася такому-то фильму?
А если я хочу ему просто порекомендовать фильм, что мне делать?
Сортировать рекомендованные фильмы?
Veaceslav Kunitki
10.05.2018
Подскажите, как решить проблему:
для предсказания надо добавить данные в датасет и перетренировать модель.
А модель тренируется долго. От 10 секунд на вашем примере.
Реалтайм так не построить ;0(
Write a comment: