Рекомендательные системы: SVD на perl

в 15:29, , рубрики: surfingbird, Блог компании Surfingbird, коллаборативная фильтрация, рекомендательные системы, метки: , ,

В предыдущих сериях мы обсудили, что такое сингулярное разложение (SVD), и сформулировали модель сингулярного разложения с базовыми предикторами. В прошлый раз мы уже довели дело до конкретных формул апдейта. Сегодня я продемонстрирую очень простую реализацию очень простой модели, мы применим её к уже знакомой матрице рейтингов, а потом обсудим, какие получились результаты.
Рекомендательные системы: SVD на perl

Итак, SVD на Perl. Готовый скрипт, который можно запускать в виде

./svd.pl число_признаков dataset.csv

можно скачать отсюда. Я сейчас просто приведу его часть, которая делает собственно обучение.

# обучение SVD: обучаем, пока не сойдётся
while (abs($old_rmse - $rmse) > 0.00001 ) {
	$old_rmse = $rmse;
	$rmse = 0;
	foreach my $u ( keys %l ) {
		foreach my $v ( keys %{$l{$u}} ) {
			# считаем ошибку
			$err = $l{$u}{$v} - ($mu + $b_u[$u] + $b_v[$v] + dot($u_f[$u], $v_f[$v]) );
			# возводим в квадрат
			$rmse += $err * $err;
			# применяем правила апдейта для базовых предикторов...
			$mu += $eta * $err;
			$b_u[$u] += $eta * ($err - $lambda2 * $b_u[$u]);
			$b_v[$v] += $eta * ($err - $lambda2 * $b_v[$v]);
			# ...и для векторов признаков
			for (my $f=0; $f < $features; ++$f) {
				$u_f[$u][$f] += $eta * ($err * $v_f[$v][$f] - $lambda2 * $u_f[$u][$f]);
				$v_f[$v][$f] += $eta * ($err * $u_f[$u][$f] - $lambda2 * $v_f[$v][$f]);
			}
		}
	}
	++$iter_no;
	# нормируем суммарную ошибку, чтобы получить RMSE
	$rmse = sqrt($rmse / $total);
	print "Iteration $iter_no:tRMSE=" . $rmse . "n";
	# если RMSE меняется мало, нужно уменьшить скорость обучения
	if ($rmse > $old_rmse - $threshold) {
		$eta = $eta * 0.66;
		$threshold = $threshold * 0.5;
	}
}

Как видите, совершенно ничего страшного – нужно просто запустить процесс апдейта весов и продолжать его до сходимости. Сходимость определяется по RMSE (root mean squared error) – среднеквадратической ошибке, т.е. корню из сумме квадратов ошибок, поделённой на число тестовых примеров. Мы реализуем стохастический градиентный спуск: после каждого тестового примера мы сразу меняем параметры, а не собираем статистику об ошибке по всей базе.

Когда RMSE перестаёт заметно улучшаться, мы уменьшаем скорость обучения; при таком подходе рано или поздно процесс должен сойтись (т.е. RMSE должно перестать меняться). Что такое здесь λ – обсудим в следующий раз, сегодня она в наших примерах будет равна нулю.

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

Read 5 users and 5 urls.
Iteration 1:    RMSE=1.92845291697284
Iteration 2:    RMSE=1.28481736445124
Iteration 3:    RMSE=1.14159530044051
...
Iteration 808:	RMSE=0.0580161117835789
Iteration 809:	RMSE=0.0580061123741253
       mu:	2.54559533638261
User base:	0.7271	0.1626	0.7139	1.9097	-0.9677	
Item base:	0.8450	0.6593	0.2731	0.7328	0.0354	
User features
  user 0:	-0.5087	-0.8326	
  user 1:	1.0220	1.2826	
  user 2:	-0.9509	0.2792	
  user 3:	0.1031	-0.4814	
  user 4:	0.6095	0.0557	
Item features:
  item 0:	-0.8368	0.2511	
  item 1:	1.1101	0.4120	
  item 2:	-0.4159	-0.4073	
  item 3:	-0.3130	-0.9115	
  item 4:	0.6408	1.2205

Что получилось у нас в результате? Примерно то, чего мы и ожидали. Пользователи получили базовые предикторы в соответствии со своей добротой: Петрович оказался злобным зрителем, а остальные, наоборот, добрыми, особенно Жанночка. Фильмы получили базовые предикторы в соответствии с «общим качеством» (выраженным, естественно, через оценки).

Но самое интересное – мы получили разумное разложение оставшихся пользовательских предпочтений на два фактора. Пример, конечно, игрушечный, и в датасете изначально было заложено, что фильмы делятся на фильмы «про поросят» и «про тракторы»; некоторые пользователи больше любят мощную технику, а некоторые – свиной хрящик.

И действительно, видно, что первый фактор – это то, насколько фильм «про тракторы», а второй фактор – то, насколько он «про поросят»; конкретные значения глубокого смысла не имеют, важно только их соотношение. Кстати, датасет я специально не подгонял, и в данном случае произошёл забавный случай со знаками: SVD обучил отрицательные значения факторов «про тракторы» для двух пользователей, которым тракторы нравятся, Васи и Валерика. Но вместе с этим SVD обучил отрицательные значения для факторов «про тракторы» фильмов, которые действительно про тракторы – минус на минус даст плюс. А 1.0220 Петра как раз, наоборот, означает, что тракторы его по сравнению с поросятами совершенно не интересуют…

Мы теперь можем предсказать значение рейтинга Васи для «Трактористов» как

2.5456 + 0.7271 + 0.8450 + (-0.5087)*(-0.8368) + (-0.8326)*0.2511 = 4.3343143.

Что ж, действительно можно было предположить, что «Трактористы» Васе понравятся.

В следующий раз мы поговорим об оверфиттинге и регуляризации – что это такое, зачем это надо и как это внедрить в SVD.

Автор: snikolenko

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js