<jupyter_start><jupyter_text>Семинар 2 SVD и приложения  Напоминание- Любую матрицу можно представить в виде $$A = U\Sigma V^*,$$где $U$ и $V$ – унитарны, а $\Sigma$ – диагональная с неотрицательными числами на диагонали, которые называются сингулярными числами  Приложение 1: тематическое моделирование и латентный семантический поиск- Дан набор документов- Необходимо разбить его на темы- Модель мешка слов- Для обеих задач нужно некоторое вложение документов в $\mathbb{R}^n$  Модель мешка слов ("bag-of-words")- Документ – это набор слов, порядок которых неважен- Тема – это набор слов, наиболее характерных для неё  Матрица документов-слов $A$- Матрица размера $N_{docs} \times N_{terms}$- Чем её заполнить?  Возможные варианты- Элемент $(i, j)$ – число вхождений слова $j$ в документ $i$- Элемент $(i, j)$ - tf-idf для слова $j$ и документа $i$Tf-Idf – произведение - tf = частота встречаемости слова в документе и - idf = $\log \frac{|D|}{|\{ d \in D \mid t \in D \}|}$, то есть логарифм отношения числа всех документов к числу документов, в которых есть слово, для которого вычисляется tf-idf. **Q:** какими свойствами будет обладать матрица $A$?  Как из SVD получить требуемое вложение?- Пусть дана матрица $A$- Для получения вложений в $\mathbb{R}^n$ вычислим SVD для некоторого ранга $d$: $A = U\Sigma V^*$- Матрица $U$ размера $N_{docs} \times d$, то есть каждый документ описывается некоторым вектором в $\mathbb{R}^d$- Матрица $V$ размера $N_{terms} \times d$, то есть каждое слово также описывается некоторым вектором в $\mathbb{R}^d$   Другая интерпретация- Если ранг $d$ – это число тем в коллекции документов, то получаем некоторое распределение слов по темам и документов по темам- Можно получить самые релевантные слова для каждой темы и самые характерные документы- Также эти вложения можно использовать как входные данные для более продвинутых алгоритмов кластеризации/классификации- Подробности будут освещены в курсе про машинное обучение  Давайте посмотрим, как это всё работает!<jupyter_code>import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scipy.sparse.linalg as spsplin
import scipy.sparse as spsp<jupyter_output><empty_output><jupyter_text>Загрузим данные<jupyter_code>documents = []
with open("./test_articles.txt","r") as fin:
    for line in fin.readlines():
        text = line.strip()
        documents.append(text)
print("Total number of Documents:",len(documents))<jupyter_output>Total number of Documents: 4551<jupyter_text>Пример документа<jupyter_code>documents[0]<jupyter_output><empty_output><jupyter_text>Необходимый препроцессинг текста<jupyter_code>news_df = pd.DataFrame({'document': documents})

# removing everything except alphabets`
news_df['clean_doc'] = news_df['document'].str.replace("[^a-zA-Z#]", " ")

# removing short words
news_df['clean_doc'] = news_df['clean_doc'].apply(lambda x: ' '.join([w for w in x.split() if len(w)>3]))

# make all text lowercase
news_df['clean_doc'] = news_df['clean_doc'].apply(lambda x: x.lower())
from nltk.corpus import stopwords
stop_words = stopwords.words('english')

# tokenization
tokenized_doc = news_df['clean_doc'].apply(lambda x: x.split())

# remove stop-words
tokenized_doc = tokenized_doc.apply(lambda x: [item for item in x if item not in stop_words])

# de-tokenization
detokenized_doc = []
for i in range(len(news_df)):
    t = ' '.join(tokenized_doc[i])
    detokenized_doc.append(t)

news_df['clean_doc'] = detokenized_doc<jupyter_output><empty_output><jupyter_text>До vs. после<jupyter_code>news_df.head()<jupyter_output><empty_output><jupyter_text>Построим матрицу $A$<jupyter_code>from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_extraction.text import CountVectorizer

vectorizer = TfidfVectorizer(stop_words='english', 
max_features= 1000, # keep top 1000 terms 
)

# vectorizer = CountVectorizer(stop_words="english", max_features=100)

X = vectorizer.fit_transform(news_df['clean_doc'])

X.shape
X, X.nnz<jupyter_output><empty_output><jupyter_text>Специальное SVD для разреженных матриц!- Как вычислять SVD для плотных матриц было показано на лекции- Однако у нас матрица разреженная!- Для таких матриц обычно нужны не все сингулярные числа и вектора, а только первые $k \ll n$- Для этого используются специальные алгоритмы!- Пока же просто вызовём функцию из модуля ```scipy.sparse.linalg```<jupyter_code>u, sigma, v = spsplin.svds(X.asfptype(), k=7)<jupyter_output><empty_output><jupyter_text>Посмотрим на убывание сингулярных чисел<jupyter_code>plt.plot(sigma[::-1]/sigma[-1])
terms = vectorizer.get_feature_names()
print(len(terms))
print(terms[:10])<jupyter_output>1000
['ability', 'able', 'abortion', 'absolutely', 'abuse', 'accept', 'access', 'according', 'account', 'accounts']<jupyter_text>Темы и "главные" слова для них<jupyter_code>for i, comp in enumerate(v):
    terms_comp = zip(terms, comp)
    sorted_terms = sorted(terms_comp, key= lambda x:x[1], reverse=True)[:10]
    print("Topic "+str(i)+": ")
    for t in sorted_terms:
        print("Word: {}, score: {}".format(t[0], t[1]))<jupyter_output>Topic 0: 
Word: health, score: 0.40862330747376874
Word: mental, score: 0.22240931303128725
Word: care, score: 0.22169596123074495
Word: patients, score: 0.20331924410163693
Word: people, score: 0.15992102366539657
Word: doctors, score: 0.14215598718119746
Word: hospital, score: 0.1325782197669305
Word: social, score: 0.11089865675449893
Word: says, score: 0.10809725354632962
Word: children, score: 0.10648923164782348
Topic 1: 
Word: film, score: 0.48038693031676405
Word: films, score: 0.12755665655401316
Word: movie, score: 0.1249785732242679
Word: director, score: 0.0846370876408235
Word: hollywood, score: 0.07996289850111983
Word: women, score: 0.07789731411556998
Word: health, score: 0.07000273851282515
Word: actor, score: 0.06921527165008458
Word: said, score: 0.055688664842670044
Word: drama, score: 0.055478115562781365
Topic 2: 
Word: bank, score: 0.4979364782266319
Word: banks, score: 0.23519917763522913
Word: film, score: 0.2138460671932756
Word: banking, score: 0.162724779306[...]<jupyter_text>Темы и "главные" документы для них<jupyter_code>top_docs = np.argmax(u, axis=0)
print(top_docs)
for i, j in enumerate(top_docs):
    print("Topic {}".format(i))
    print(documents[j])
    print(" ")<jupyter_output>[3116 2806 2414 2806 1057  464  658]
Topic 0
Public Service Awards 2016 health and wellbeing winner: Derventio Housing Trust “Quite often people are in hospital and they’ve got nothing: no food, no clothes, no toiletries,” says <NAME>, Derventio Housing Trust’s strategic lead for its Healthy Futures initiative. “We get all that sorted out, so people at least have a bit of dignity when they are discharged.” That’s just the start of the scheme’s work with homeless people due to leave hospital. Many have multiple, complex needs, such as mental health problems and addictions, and are trapped in a vicious cycle of ongoing health issues and repeat admissions. Over a 12-week period, staff work intensively to find housing for patients, settle them into their new homes and help them live independently – while making better use of primary care, rather than relying disproportionately on acute services. “Sometimes it’s because they don’t manage their health, so they actually get ill enough[...]<jupyter_text>Найдём по данному запросу ближайший документ<jupyter_code>new_request_original = '''There were four minutes remaining and none of the Newcastle United fans had left. 
Even with their team a man down and four goals down, they stayed. 
Even with another reminder of how their well-intentioned manager, <NAME>, 
struggles to build on the good work of <NAME>, 
they kept standing there, kept singing, defying the rain and the pain.'''

# removing everything except alphabets`
new_request = new_request_original.replace("[^a-zA-Z#]", " ")

# removing short words
new_request = ' '.join([w for w in new_request.split() if len(w)>3])

# make all text lowercase
new_request = new_request.lower()


# tokenization
tokenized_doc = new_request.split()

# remove stop-words
tokenized_doc = [item for item in tokenized_doc if item not in stop_words]

# de-tokenization
detokenized_request = ' '.join(tokenized_doc)

y = vectorizer.transform([detokenized_request]).toarray().T
y.shape, v.shape
score = u @ (sigma[:, np.newaxis] * v.dot(y))
print(score.shape)
idx_best = np.argmax(score)
print(documents[idx_best])
print(sorted(score.ravel())[::-1][:10])<jupyter_output>[0.1926413029576342, 0.18637279384967628, 0.18464519140512234, 0.1829021315603602, 0.18210910662170277, 0.17802423126548572, 0.1774632399857713, 0.1771421467788238, 0.17574163035083412, 0.17550382215861288]<jupyter_text>Приложение 2: рекомендательные системы  Что дано?- Матрица пользователь-продукт- Элементы матрицы – рейтинги, если есть- Неполное заполнение  Как использовать SVD?- Оригинальная работа [*Performance of recommender algorithms on top-n recommendation tasks*](https://dl.acm.org/citation.cfm?id=1864721), <NAME>., <NAME>., and <NAME>- Построим вложение пользователей и продуктов в некоторое подпространство $\mathbb{R}^n$- Для известных предпочтений найдём наиболее близкие продукты в этом подпространстве- Как это вычислительно сделать?  Датасет Movielens с рейтингами фильмов<jupyter_code>movies = pd.read_csv('movies.gz', index_col='movieid', header=0, encoding='unicode-escape')[['movienm', 'genreid']]
ratings = pd.read_csv('ratings.gz', header=0)<jupyter_output><empty_output><jupyter_text>Пример записей<jupyter_code>movies.head()
ratings.head()
ratings[['userid', 'movieid']].apply(pd.Series.nunique)<jupyter_output><empty_output><jupyter_text>Зададим список предпочтений<jupyter_code>best_movies = "matrix|die hard"
target_movies = movies.loc[movies.movienm.str.contains(best_movies, flags=2)]
target_movies<jupyter_output><empty_output><jupyter_text>Выделим индексы выбранных фильмов<jupyter_code>favorite_movies_ids = target_movies.index
print(favorite_movies_ids)<jupyter_output>Int64Index([165, 1036, 1370, 2571], dtype='int64', name='movieid')<jupyter_text>Создадим матрицу с рейтингами<jupyter_code>data_matrix = spsp.csr_matrix((ratings.rating.values.astype('f8'), 
                               (ratings.userid.values, ratings.movieid.values)))
print(data_matrix.shape)<jupyter_output>(6041, 3953)<jupyter_text>Вычислим SVD<jupyter_code>rank = 30
_, S, Vt = spsplin.svds(data_matrix, k=rank, return_singular_vectors='vh')<jupyter_output><empty_output><jupyter_text>Посмотрим на убывание сингулярных чисел<jupyter_code>plt.plot(S[::-1] / S[-1])<jupyter_output><empty_output><jupyter_text>Получим корреляции<jupyter_code>print(Vt[:, favorite_movies_ids].shape)
correlation = Vt.T @ Vt[:, favorite_movies_ids]
print(correlation.shape)
score = correlation.sum(axis=1)
print(score.shape)
# movies.loc[np.argsort(-Vt.T @ Vt[:, favorite_movies_ids].sum(axis=1))[:15]] # assuming binary preference vector<jupyter_output>(30, 4)
(3953, 4)
(3953,)<jupyter_text>Вычислим итоговые рекомендации<jupyter_code>top_n = 10
print(np.sort(score)[::-1][:top_n])
movies.loc[np.argsort(-score)[:top_n]]<jupyter_output>[0.15287975 0.15033298 0.14251449 0.12089341 0.11414237 0.10847293
 0.10257672 0.09429381 0.09029753 0.0862588 ]
{'fraction_non_alphanumeric': 0.11434678065722682, 'fraction_numerical': 0.0734704782260219, 'mean_word_length': 6.330287206266319, 'pattern_counts': {'":': 1, '<': 78, '<?xml version=': 0, '>': 80, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '20', 'dir': 'jupyter-structured-clean-dedup', 'id': '598052', 'license': 'MIT', 'n_tokens_mistral': 4772, 'n_tokens_neox': 4492, 'n_words': 1062, 'path': 'seminars/seminar2/seminar2.ipynb', 'repo_name': 'anastasiya-zhiboedova/nla2019_ozon', 'size': '11259'}