Рекомендация фильмов по текстовому описанию¶
¶
Извлечение информацию из текста¶
- Тексовые данные из себя представляют набор неструктуированных данных
- Существуют разные подходы для извлечения нужной нам инофрмации которые дадут нам возможность построить рекоммендации на основе текстовых данных
NLP подходы для создания фичей¶
- Первых подход который мы рассмотрим для каждого текста создает набор фичей которые он встречает в тексте, извлекая нужную информацию из всех текстов, мы фиксируем количество колонок. Количнство колонок пропорциональна количеству уникальных слов во всех документах;
Bag of Words
(BoW) подходы - Второй подход так же создает набор фичей, но мы заранее определяем эго размер. Обучив эти фичи у нас для конкретного слова есть свой вектор представлений в многовекторном пространстве. Эти данные хранятся в самой модели и их можно извлекать при необходимости, и использовать на новых докумаетах. Подавая новые документы в модель мы обычно выбираем арифмическое среднее векторов всех слов в документе, таким образом получая для каждого документа фиксированный веркор;
эмбеддинговый
подход
Оценка схожости¶
- Как и раньше, получив некоторую матрицу представлений для каждого документа, нам нужно посчитать оценку схожости, это может быть
cosine_similarity
и тд. - Мы будем использовать упрощенный вариант
linear_kernel
¶
Давайте теперь загрузим данные. Для этой задачи мы воспользуемся данные кратких описании разных фильмов. И на основе этих текстовых данных будем строить реком
import pandas as pd
import numpy as np
import warnings; warnings.filterwarnings('ignore')
movies = pd.read_csv('/kaggle/input/tmdb-movie-metadata/tmdb_5000_movies.csv')
Датасет содержит следующие характеристики:
budget
- бюджет, на который был снят фильм.genre
- Жанр фильма, боевик, комедия, триллер и т.д.homepage
- Ссылка на домашнюю страницу фильма.id
- Фактически это идентификатор фильма.keywords
- Ключевые слова или теги, связанные с фильмом.original_language
- Язык, на котором был снят фильм.original_title
- Название фильма до перевода или адаптации.overview
- Краткое описание фильма.popularity
- Числовое значение, указывающее на популярность фильма.production_companies
- Производственная компания фильма.production_countries
- Страна, в которой был снят фильм.release_date
- Дата выхода фильма в прокат.revenue
- Мировой доход, полученный фильмом.runtime
- Время работы фильма в минутах.status
- "Released" или "Rumored".tagline
- Заголовок фильма.title
- Название фильма.vote_average
- средний рейтинг, полученный фильмом.vote_count
- количество набранных голосов.
Посмотрим на сами данные
movies.head(5)
budget | genres | homepage | id | keywords | original_language | original_title | overview | popularity | production_companies | production_countries | release_date | revenue | runtime | spoken_languages | status | tagline | title | vote_average | vote_count | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 237000000 | [{"id": 28, "name": "Action"}, {"id": 12, "nam... | http://www.avatarmovie.com/ | 19995 | [{"id": 1463, "name": "culture clash"}, {"id":... | en | Avatar | In the 22nd century, a paraplegic Marine is di... | 150.437577 | [{"name": "Ingenious Film Partners", "id": 289... | [{"iso_3166_1": "US", "name": "United States o... | 2009-12-10 | 2787965087 | 162.0 | [{"iso_639_1": "en", "name": "English"}, {"iso... | Released | Enter the World of Pandora. | Avatar | 7.2 | 11800 |
1 | 300000000 | [{"id": 12, "name": "Adventure"}, {"id": 14, "... | http://disney.go.com/disneypictures/pirates/ | 285 | [{"id": 270, "name": "ocean"}, {"id": 726, "na... | en | Pirates of the Caribbean: At World's End | Captain Barbossa, long believed to be dead, ha... | 139.082615 | [{"name": "Walt Disney Pictures", "id": 2}, {"... | [{"iso_3166_1": "US", "name": "United States o... | 2007-05-19 | 961000000 | 169.0 | [{"iso_639_1": "en", "name": "English"}] | Released | At the end of the world, the adventure begins. | Pirates of the Caribbean: At World's End | 6.9 | 4500 |
2 | 245000000 | [{"id": 28, "name": "Action"}, {"id": 12, "nam... | http://www.sonypictures.com/movies/spectre/ | 206647 | [{"id": 470, "name": "spy"}, {"id": 818, "name... | en | Spectre | A cryptic message from Bond’s past sends him o... | 107.376788 | [{"name": "Columbia Pictures", "id": 5}, {"nam... | [{"iso_3166_1": "GB", "name": "United Kingdom"... | 2015-10-26 | 880674609 | 148.0 | [{"iso_639_1": "fr", "name": "Fran\u00e7ais"},... | Released | A Plan No One Escapes | Spectre | 6.3 | 4466 |
3 | 250000000 | [{"id": 28, "name": "Action"}, {"id": 80, "nam... | http://www.thedarkknightrises.com/ | 49026 | [{"id": 849, "name": "dc comics"}, {"id": 853,... | en | The Dark Knight Rises | Following the death of District Attorney Harve... | 112.312950 | [{"name": "Legendary Pictures", "id": 923}, {"... | [{"iso_3166_1": "US", "name": "United States o... | 2012-07-16 | 1084939099 | 165.0 | [{"iso_639_1": "en", "name": "English"}] | Released | The Legend Ends | The Dark Knight Rises | 7.6 | 9106 |
4 | 260000000 | [{"id": 28, "name": "Action"}, {"id": 12, "nam... | http://movies.disney.com/john-carter | 49529 | [{"id": 818, "name": "based on novel"}, {"id":... | en | John Carter | John Carter is a war-weary, former military ca... | 43.926995 | [{"name": "Walt Disney Pictures", "id": 2}] | [{"iso_3166_1": "US", "name": "United States o... | 2012-03-07 | 284139100 | 132.0 | [{"iso_639_1": "en", "name": "English"}] | Released | Lost in our world, found in another. | John Carter | 6.1 | 2124 |
¶
Не забигая вперед, вспомним как мы можем создать некий беислайн для рекомендации;
- Cоздадим для всех пользователей одну и ту же рекомендацию
Фильмов в данных много, но не все мы хотели бы рекоммендовать, один из простейших подходов это фильтрация на остнове каких то метрик
- Прежде чем приступить к работе, нам нужно получить метрику для оценки фильма
- Рассчитать оценку для каждого фильма, отсортировать оценки и рекомендовать пользователям фильмы с наилучшим рейтингом
- Мы можем использовать
средний рейтинг
фильма в качестве оценки, но это будет не совсем корректно, так как фильм со средним рейтингом 8,9 и всего 3 голосами не может считаться лучшим, чем фильм со средним рейтингом 7,8, но 40 голосами - Поэтому в качестве оценки мы будем использовать
взвешенный рейтинг
IMDB (wr)
C= movies['vote_average'].mean()
C.round(2)
6.09
- Итак, средняя оценка всех фильмов составляет около 6 по 10-балльной шкале
- Следующий шаг - определение подходящего значения m (минимального количества голосов, необходимого для включения в таблицу)
- Мы будем использовать 90-й процентиль в качестве отсечки. Другими словами, чтобы фильм попал в чарт, он должен набрать больше голосов, чем хотя бы 90 % фильмов в списке.
m= movies['vote_count'].quantile(0.9)
m.round(2)
1838.4
Теперь мы можем отфильтровать фильмы, которые попадают в таблицу
q_movies = movies.copy().loc[movies['vote_count'] >= m]
q_movies.shape
(481, 20)
- Мы видим, что в этом списке 481 фильм.
- Теперь нам нужно рассчитать нашу метрику для каждого фильма, отвечающего требованиям. Для этого мы определим функцию weighted_rating() и определим новую функцию score
def weighted_rating(x, m=m, C=C):
v = x['vote_count']
R = x['vote_average']
# Расчет по формуле IMDB
return (v/(v+m) * R) + (m/(m+v) * C)
# Определим новую функцию 'score' и рассчитаем ее значение с помощью `weighted_rating()`.
q_movies['score'] = q_movies.apply(weighted_rating, axis=1)
- Наконец, отсортируем DataFrame по признаку оценки и выведем название, количество голосов, среднее количество голосов и взвешенный рейтинг или оценку 10 лучших фильмов
#Сортировка фильмов на основе рейтинга, рассчитанного выше
q_movies = q_movies.sort_values('score', ascending=False)
#Вывод 15 лучших фильмов
q_movies[['title', 'vote_count', 'vote_average', 'score']].head(10)
title | vote_count | vote_average | score | |
---|---|---|---|---|
1881 | The Shawshank Redemption | 8205 | 8.5 | 8.059258 |
662 | Fight Club | 9413 | 8.3 | 7.939256 |
65 | The Dark Knight | 12002 | 8.2 | 7.920020 |
3232 | Pulp Fiction | 8428 | 8.3 | 7.904645 |
96 | Inception | 13752 | 8.1 | 7.863239 |
3337 | The Godfather | 5893 | 8.4 | 7.851236 |
95 | Interstellar | 10867 | 8.1 | 7.809479 |
809 | Forrest Gump | 7927 | 8.2 | 7.803188 |
329 | The Lord of the Rings: The Return of the King | 8064 | 8.1 | 7.727243 |
1990 | The Empire Strikes Back | 5879 | 8.2 | 7.697884 |
pop = movies.sort_values('popularity', ascending=False)
ldf = pop[['title','popularity']].head(6)
ldf.style\
.bar(align='mid',
color=['#d65f5f','#d65f5f'])
title | popularity | |
---|---|---|
546 | Minions | 875.581305 |
95 | Interstellar | 724.247784 |
788 | Deadpool | 514.569956 |
94 | Guardians of the Galaxy | 481.098624 |
127 | Mad Max: Fury Road | 434.278564 |
28 | Jurassic World | 418.708552 |
- Следует иметь в виду, что эти демографические рекомендательные системы предоставляют общую таблицу рекомендуемых фильмов для всех пользователей.
- Они не учитывают интересы и вкусы конкретного пользователя
- Далее мы рассмотрим варианты NLP рекомендовательных систем, представим что пользователь только что посмотрел фильм
The Dark Knight Rises
, и теперь мы хотим посоветовать походие фильмы этому пользователю. Имея информацию об обзорах фильмовoverview
, мы можем найти походие фильмы по описанию, те. если в векторном пространстве вектора будут похожими, мы можем их рекемендовать
¶
- Мы рассчитаем попарные оценки сходства для всех фильмов на основе описания их сюжета и будем рекомендовать фильмы на основе этих оценок сходства.
- Описание сюжета приведено в
overview
фиче нашего набора данных. Давайте посмотрим на данные.
movies['overview'].head(5)
0 In the 22nd century, a paraplegic Marine is di... 1 Captain Barbossa, long believed to be dead, ha... 2 A cryptic message from Bond’s past sends him o... 3 Following the death of District Attorney Harve... 4 John Carter is a war-weary, former military ca... Name: overview, dtype: object
- Теперь мы вычислим векторы Term Frequency-Inverse Document Frequency (TF-IDF) для каждого обзора
- Суть подхода в том что мы пробигая по всем документам, сохраняем все уникальные слова/токены и считаем их количество во всех документах;
TF
- Особенность этого метода это дополнительный термин который оценивает важность слова (в каких документах встречается относительно всех документах);
IDF
from sklearn.feature_extraction.text import TfidfVectorizer
#Определим объект векторизатора TF-IDF. Удалим все английские стоп-слова, такие как 'the', 'a'
tfidf = TfidfVectorizer(stop_words='english')
#Заменим NaN пустой строкой
movies['overview'] = movies['overview'].fillna('')
#Построим требуемую матрицу TF-IDF путем подгонки и преобразования данных
tfidf_matrix = tfidf.fit_transform(movies['overview'])
print('tfidf matrix size:',tfidf_matrix.shape)
tfidf matrix size: (4803, 20978)
Мы видим, что для описания 4800 фильмов в нашем наборе данных было использовано более 20 000 различных слов.
Теперь, имея на руках эти вектора вредставлений всех документов, мы можем вычислить оценку сходства. Для этого можно использовать различные метрики, такие как Евклидова, Пирсона или косинусная метрика. Разные метрики хорошо работают в разных сценариях, и часто бывает полезно поэкспериментировать.
Мы будем использовать косинусоидальное сходство
между двумя фильмами. Мы используем косинусоидальную метрику, поскольку оно не зависит от амплитуды и относительно легко и быстро вычисляется.
Поскольку мы использовали векторизатор TF-IDF, вычисление точечного произведения напрямую даст нам оценку косинусного сходства. Поэтому мы будем использовать linear_kernel
от sklearn вместо cosine_similarity
, так как это быстрее.
from sklearn.metrics.pairwise import linear_kernel
cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)
indices = pd.Series(movies.index,
index=movies['title']).drop_duplicates()
def get_recommendations(title, cosine_sim=cosine_sim, scores=cosine_sim):
# Получение индекса фильма, соответствующего названию
idx = indices[title]
# Получение оценок парного сходства всех фильмов с этим фильмом
sim_scores = list(enumerate(cosine_sim[idx]))
# Сортировка фильмов по степени сходства
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
# 10 самых похожих фильмов
sim_scores = sim_scores[1:11]
idx = []; vals = []
for i in sim_scores:
idx.append(i[0])
vals.append(i[1])
pd_scores = pd.Series(vals,index=idx,name='similarity')
# Get the movie indices
movie_indices = [i[0] for i in sim_scores]
merged = pd.concat([movies['title'].iloc[movie_indices],pd_scores],axis=1)
# Return the top 10 most similar movies
return merged.style\
.bar(align='mid',
color=['#d65f5f','#d65f5f'])
get_recommendations('The Dark Knight Rises')
title | similarity | |
---|---|---|
65 | The Dark Knight | 0.301512 |
299 | Batman Forever | 0.298570 |
428 | Batman Returns | 0.287851 |
1359 | Batman | 0.264461 |
3854 | Batman: The Dark Knight Returns, Part 2 | 0.185450 |
119 | Batman Begins | 0.167996 |
2507 | Slow Burn | 0.166829 |
9 | Batman v Superman: Dawn of Justice | 0.133740 |
1181 | JFK | 0.132197 |
210 | Batman & Robin | 0.130455 |
get_recommendations('The Avengers')
title | similarity | |
---|---|---|
7 | Avengers: Age of Ultron | 0.146374 |
3144 | Plastic | 0.122791 |
1715 | Timecop | 0.110385 |
4124 | This Thing of Ours | 0.107529 |
3311 | Thank You for Smoking | 0.106203 |
3033 | The Corruptor | 0.097598 |
588 | Wall Street: Money Never Sleeps | 0.094084 |
2136 | Team America: World Police | 0.092244 |
1468 | The Fountain | 0.086643 |
1286 | Snowpiercer | 0.086189 |
¶
- Даллее посмотрим вариант уже с
эмбеддингами
- Мы можем использовать язоковые модели для того чтобы сопаставить словам (из документов) некий эмбеддинг вектор и усреднить, получив среднее значение для документов в фиче
overview
- Одна из моделей которую мы можем использовать это моделт
LaBSE
, в модели используются эмбелддинги слов размера 768 - Воспользуемся библиотекой
transformers
для быстрой подгрузки модели и эго токенизатора. Токенищатор нам нужен будет для того чтобы разбить документы на части, создав вводные данные для моделиinputs
, мы так же в условии токенизации будем использоватьtruncate
и максммальную длину 512
from transformers import AutoTokenizer, AutoModel
import torch
import numpy as np
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
# Load pre-trained LaBSE model and tokenizer
tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/LaBSE")
model = AutoModel.from_pretrained("sentence-transformers/LaBSE").to(device)
Using device: cuda
get_labse_vectors_batch
для каждого документа вoverview
сопоставляет токену соответсвующий вектор представления из одного из предобученнвх слоев модели. Для каждого документа мы усредняем значение всех слов которые есть в словаре.- Размерность эмбеддингов в этой модели 768
import numpy as np
import pandas as pd
from tqdm import tqdm
def get_labse_vectors_batch(texts, model, tokenizer, device, batch_size=32):
embeddings = []
num_batches = (len(texts) + batch_size - 1) // batch_size
for i in tqdm(range(0, len(texts), batch_size), total=num_batches, desc="Generating LaBSE vectors"):
batch = texts[i:i+batch_size]
inputs = tokenizer(batch, return_tensors="pt", padding=True, truncation=True, max_length=512)
inputs = {k: v.to(device) for k, v in inputs.items()}
with torch.no_grad():
outputs = model(**inputs)
batch_embeddings = outputs.last_hidden_state[:, 0, :].cpu().numpy()
embeddings.append(batch_embeddings)
return np.vstack(embeddings)
movies['overview'] = movies['overview'].fillna('')
overviews = movies['overview'].tolist()
labse_vectors = get_labse_vectors_batch(overviews, model, tokenizer, device)
labse_vectors.shape
Generating LaBSE vectors: 100%|██████████| 151/151 [00:24<00:00, 6.08it/s]
(4803, 768)
labse_vectors.shape
(4803, 768)
from sklearn.metrics.pairwise import linear_kernel
labse_cosine_sim = linear_kernel(labse_vectors, labse_vectors)
indices = pd.Series(movies.index, index=movies['title']).drop_duplicates()
get_recommendations('The Dark Knight Rises', labse_cosine_sim)
title | similarity | |
---|---|---|
1253 | Kiss of Death | 480.728943 |
65 | The Dark Knight | 458.324799 |
1422 | The X Files: I Want to Believe | 455.618408 |
600 | Killer Elite | 453.634796 |
1209 | The Rainmaker | 450.117798 |
982 | Run All Night | 446.992432 |
1830 | Ride Along | 446.656616 |
987 | Dream House | 446.387756 |
3805 | Purple Violets | 446.120972 |
210 | Batman & Robin | 444.635986 |
¶
- Последний подход мы попробуем это подход предобученных
fasttext
эмбеддингов. - Решая задачу предсказания контекста на остнове стреднего слова и на остнове среднего предсказания контекстных слов используя нейросеть Fasttext позволяет нам обучиь эмбединговый слой.
- Качество эмбединговых векторов напрямую зависит от корпуса документов на которой мы их обучили
- В данном примере используем эмбединги обуены на корпусе новостей
fasttext-wiki-news-subwords-300
в котором вектора слов имеют размер 300 пространств
from gensim.models import FastText
import gensim.downloader as api
import numpy as np
model = api.load('fasttext-wiki-news-subwords-300')
[==================================================] 100.0% 958.5/958.4MB downloaded
get_weighted_fasttext_vectors_batch
извлекает среднее значение всех слов для каждого документа, также часто мы совмещаем эмбединги свесте с весами TFIDF для каждого слова
def get_weighted_fasttext_vectors_batch(texts, model, tfidf_vectorizer, tfidf_matrix, batch_size=1000):
embeddings = []
num_batches = (len(texts) + batch_size - 1) // batch_size
for i in tqdm(range(0, len(texts), batch_size), total=num_batches, desc="Generating weighted FastText vectors"):
batch = texts[i:i+batch_size]
batch_embeddings = []
for j, text in enumerate(batch):
words = text.split()
word_vectors = []
weights = []
for word in words:
if word in model and word in tfidf_vectorizer.vocabulary_:
word_vectors.append(model[word])
tfidf_index = tfidf_vectorizer.vocabulary_[word]
weight = tfidf_matrix[i+j, tfidf_index]
weights.append(weight)
if word_vectors:
weighted_vectors = np.array(word_vectors) * np.array(weights)[:, np.newaxis]
avg_vector = np.sum(weighted_vectors, axis=0) / np.sum(weights)
else:
avg_vector = np.zeros(model.vector_size)
batch_embeddings.append(avg_vector)
embeddings.extend(batch_embeddings)
return np.array(embeddings)
movies['overview'] = movies['overview'].fillna('')
overviews = movies['overview'].tolist()
tfidf_vectorizer = TfidfVectorizer(stop_words='english')
tfidf_matrix = tfidf_vectorizer.fit_transform(overviews)
fasttext_vectors = get_weighted_fasttext_vectors_batch(overviews, model, tfidf_vectorizer, tfidf_matrix)
fasttext_vectors.shape
Generating weighted FastText vectors: 100%|██████████| 5/5 [00:03<00:00, 1.53it/s]
(4803, 300)
Имея фичу матрицу fasttext_vectors
построем матрицу схожости, как мы делали и раньше fasttext_cosine_sim
from sklearn.metrics.pairwise import linear_kernel
fasttext_cosine_sim = linear_kernel(fasttext_vectors, fasttext_vectors)
indices = pd.Series(movies.index, index=movies['title']).drop_duplicates()
Делаем рекомендации для фильма The Dark Knight Rise
get_recommendations('The Dark Knight Rises', fasttext_cosine_sim)
title | similarity | |
---|---|---|
2435 | Running Scared | 0.346712 |
2910 | A Tale of Three Cities | 0.344737 |
4367 | The Broken Hearts Club: A Romantic Comedy | 0.341655 |
176 | The Revenant | 0.339179 |
3475 | Casa De Mi Padre | 0.337685 |
3374 | Veer-Zaara | 0.336817 |
2807 | The Perfect Game | 0.336533 |
935 | Herbie Fully Loaded | 0.334470 |
2850 | Tales from the Crypt: Demon Knight | 0.332519 |
1293 | Frankenweenie | 0.331367 |
- Несмотря на то, что наша система неплохо справляется с поиском фильмов с похожим сюжетом, качество рекомендаций оставляет желать лучшего.
- "Тёмный рыцарь: Возрождение легенды" возвращает все фильмы о Бэтмене, в то время как люди, которым понравился этот фильм, скорее всего, больше склонны любить другие фильмы Кристофера Нолана. Это то, что не может уловить текущая система.