С помощью Python, scipy, sklearn и nltk можно легко построить волне рабочую систему рекомендаций. Даже с учётом русской морфологии.
Здесь, чтобы не быть голословным и не ходить далеко за примерами, я покажу, как работает система рекомендаций на примере этого сайта.
Исходные тексты сайта у меня живут в markdown-формате.
То есть, уже текстовые. Поэтому я просто создал (с помощью
утилиты find
) список всех файлов, подлежащих анализу
в файле LIST
.
На понадобятся scipy
, sklearn
и nltk
>>> import scipy, sklearn, nltk
>>> scipy.version.version
'0.13.3'
>>> sklearn.__version__
'0.14.1'
>>> nltk.__version__
'2.0b9'
Поддержка русского языка тут уже есть «из коробки».
#!/usr/bin/python
# coding: utf8
import nltk
from sklearn.feature_extraction.text import CountVectorizer
import scipy as sp
class StemmedCountVectorizer(CountVectorizer):
def __init__(self, **kv):
super(StemmedCountVectorizer, self).__init__(**kv)
self._stemmer = nltk.stem.snowball.RussianStemmer(
'russian',
ignore_stopwords=False
)
def build_analyzer(self):
analyzer = super(StemmedCountVectorizer, self).build_analyzer()
return lambda doc: (self._stemmer.stem(w) for w in analyzer(doc))
def euclidean_distance(v1, v2):
# Строго говоря, эту нормолизацию можно было сделать
# эффективней, если использовать параметр axis в la.norm()
delta = v1/sp.linalg.norm(v1.toarray()) - v2/sp.linalg.norm(v2.toarray())
return sp.linalg.norm(delta.toarray())
def main():
files = [x.strip() for x in open('LIST', 'r')]
texts = [open(n, 'r').read().decode('utf8') for n in files]
vectorizer = StemmedCountVectorizer(
min_df=1,
token_pattern=ur'[ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё]{4,}'
)
x = vectorizer.fit_transform(texts)
#print ', '.join(vectorizer.get_feature_names())
#print x.toarray().transpose()
n_samples, n_features = x.shape
summary = []
target = 3 # 96 # 6
for i in range(0, n_samples):
summary.append([files[i], euclidean_distance(x[target], x[i])])
summary.sort(key = lambda x: x[1])
for t in summary:
print t
main()
Собственно, код на столько прост, что комментировать особо
нечего. sklearn.feature_extraction.text
предоставляет
готовый инструмент для подсчёта количества слов в тексте —
CountVectorizer
. Он позволяет загрузить много документов,
создаёт единый словарь из всех найденных слов и для
каждого слова и документа создаёт счётчик. Таким образом,
на выходе мы получаем список всех слов (их называют факторами),
и матрицу из счётчиков. Вы можете раскомментирвоать два
print-а и посмотреть на результат.
Хитрость тут только в том, что мы добавили преобразование слов в корни. Суть очень проста:
>>> import nltk
>>> stemmer = nltk.stem.snowball.RussianStemmer('russian')
>>> print stemmer.stem(u'лошадь')
лошад
>>> print stemmer.stem(u'Лошадью')
лошад
>>> print stemmer.stem(u'ЛОШАДЕЙ')
лошад
Мы отнаследовались от класса CountVectorizer
и добавили
функциональность выделения корней.
Как видите, расстояние между текстами мы считаем, как Евклидово
расстояние, между соответствующими точками. (Да-да:
scipy.linalg.norm
считает обычный корень из суммы квадратов.
Единственное, это можно было сделать эффективней. Я оставил
более наглядное решение, чтобы вам легче было играться с кодом.)
Нормализация очень важна. Она позволяет сделать вклады разных факторов соразмерными. В нашем случае, она позволяет нивелировать разницу в длинах документов. Попробуйте её устранить и вы сразу увидите разницу.
Мы выбираем один документ (target). Считаем расстояния от него до остальных, сортируем по расстоянию и печатаем результат.
Давайте найдём документы похожие на «Пример обучения нейрона».
Мы получим такой top с такими весами:
Я бы сказал, — не плохо.
Попробуем рассмотреть документ, посвящённый нитям в Python: «Нити в Python»:
Как и раньше, мы оставили только те документы, расстояние до которых меньше 1.2.
Давайте рассмотрим ещё один пример: «Байесовское машинное обучение»:
Думаю, достаточно, чтобы вы могли походить по страницам и оценить качество поиска.
Естественно, результаты приведены на момент выполнения скрипта. Что-то могло измениться, дополниться… Эта заметка, естественно, в анализе не участвовала.
Хотя эта система рекомендаций выдаёт вполне вменяемый результат (я даже подумываю, не прикрутить ли эти рекомендации к каждой странице сайта), всё же, она не лишена недостатков.
В реальной жизни надо, как минимум:
Это тот минимум, который абсолютно необходим. Но в зависимости от конкретной ситуации, у вас могут быть другие дополнительные факторы, которые полезно использовать. Всегда старайтесь смотреть шире, искать дополнительные источники информации для обучения и анализа; используйте их.
Если ваш массив документов побольше (желательно, наз в 100 :-)), то вы можете поиграться с кластеризацией. Это очень просто. Начать можно так:
from sklearn.cluster import KMeans
…
km = KMeans(
n_clusters=5,
init='random',
n_init=1,
verbose=1
)
km.fit(z)
Не забывайте, что z лучше нормализовать. Нормализовать
отдельные вектора можно либо, используя аргумент axis
,
либо через apply_along_axis
:
np.apply_along_axis(sp.linalg.norm, 1, mtx)
Зависит от версии.
После km.fit()
вы можете получить массив с номерами групп через
km.labels_
. Длинна этого массива как раз равна количеству
ваших файлов. Дальше zip
, sort
… Полный вперёд.
Кстати, тут интересно будет приглядеться к результатам и убедиться, что группировка текстов по однозначным группами — это, обычно, не самое хорошее решение. Например, на моей странице есть заметка про Байесовское машинное обучение. Её, видимо, разумно отнести к группе «машинное обучение», он ведь её хоршо бы видеть и в группе «теория вероятностей»? Очень часто, текст нельзя однозначно отнести к одной группе.
В этой связи, уместно вспомнить про
LDA.
По иронии судьбы, модуль sklearn.lda
не имеет никакого
отношения к latent Dirichlet allocation, а выполняет
linear discriminant analysis. Создание тематических
моделей, это отдельная история, которой я, пожалуй,
не буду тут касаться.