Кластеризация документов — это задача классификации документов по разным группам на основе их текстового и семантического контекста. Это неконтролируемый метод, поскольку у нас нет меток для документов, и он применяется в информационном поиске и поисковых системах.
Давайте начнем…
Чтобы классифицировать документы на основе их содержания, я решил использовать алгоритм К-средних. Из-за того, что элементы не помечены, это явно проблема обучения без учителя, и одним из лучших решений должно быть K-Means. Конечно, мы можем использовать другой алгоритм, например смешанные модели Гаусса, или даже методы глубокого обучения, такие как автоэнкодеры. Я буду использовать python с блокнотом Jupyter, чтобы объединить код и результаты с документацией.
Я разрабатываю код в Анаконда среду, и я использую следующие зависимости:
Панды для передачи данных
Склеарн для машинного обучения и предварительной обработки
Матплотлиб для построения
нтлк для алгоритмов естественного языка
КрасивыйСуп разобрать текст из xml файла и избавиться от категорий
Анализ данных
Функция parseXML использует xml.etree.ElementTree для анализа данных. Я решил использовать для кластеризации только название и описание элементов, наиболее релевантных семасиологии. Из-за того, что описание не является raw tex, мы извлекаем текст библиотекой BeautifulSoup, как я уже упоминал. Также мы отбрасываем элементы с очень маленьким описанием, потому что они влияют на окончательную кластеризацию. Можно считать, что все они принадлежат дополнительному кластеру. Конечно, есть способы их включить, но я пока ими не пользуюсь.
import xml.etree.ElementTree as ET
import pandas as pd
import nltk
from sklearn.cluster import KMeans
from sklearn.externals import joblib
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
nltk.download('punkt')
from bs4 import BeautifulSoup
from nltk import SnowballStemmer
import re
def parseXML(xmlfile):
tree = ET.parse(xmlfile)
root = tree.getroot()
titles=()
descriptions=()
for item in root.findall('./channel/item'):
for child in item:
if(child.tag=='title' ):
titles.append(child.text)
if (child.tag == 'description' ):
soup = BeautifulSoup(str(child.text).encode('utf8','ignore'), "lxml")
strtext=soup.text.replace(u'\xa0', u' ').replace('\n',' ')
descriptions.append(strtext)
return titles,descriptions
bef_titles,bef_descriptions = parseXML('data.source.rss-feeds.xml')
print('Count of items before dropping:' ,len(bef_titles))
titles=()
descriptions=()
for i in range(len(bef_titles)):
if ( len(bef_descriptions(i)) > 500):
titles.append(bef_titles(i))
descriptions.append(bef_descriptions(i))
print('Count of items after:' ,len(titles))
(nltk_data) Downloading package punkt to
(nltk_data) C:\Users\sergi\AppData\Roaming\nltk_data...
(nltk_data) Package punkt is already up-to-date!
Count of items before dropping: 1662
Count of items after: 1130
Токенизация и стемминг
Следующим шагом является разбиение текста на слова, удаление всех морфологических аффиксов и удаление общих слов, таких как артикли и предлоги. Это можно сделать с помощью встроенных функций ntlk. В конце концов, мы получаем два разных словаря (один токенизированный и базовый и один только токенизированный), и мы объединяем их в кадр данных pandas.
def tokenize_and_stem(text):
tokens = (word for sent in nltk.sent_tokenize(text) for word in nltk.word_tokenize(sent))
filtered_tokens = ()
for token in tokens:
if re.search('(a-zA-Z)', token):
filtered_tokens.append(token)
stems = (stemmer.stem(t) for t in filtered_tokens)
return stems
def tokenize_only(text):
tokens = (word.lower() for sent in nltk.sent_tokenize(text) for word in nltk.word_tokenize(sent))
filtered_tokens = ()
for token in tokens:
if re.search('(a-zA-Z)', token):
filtered_tokens.append(token)
return filtered_tokens
stemmer = SnowballStemmer("english")
totalvocab_stemmed = ()
totalvocab_tokenized = ()
for i in descriptions:
allwords_stemmed = tokenize_and_stem(i)
totalvocab_stemmed.extend(allwords_stemmed)
allwords_tokenized = tokenize_only(i)
totalvocab_tokenized.extend(allwords_tokenized)
vocab_frame = pd.DataFrame({'words': totalvocab_tokenized}, index=totalvocab_stemmed)
print('there are ' + str(vocab_frame.shape(0)) + ' items in vocab_frame')
there are 481437 items in vocab_frame
Векторизация и стемминг
Прежде чем мы загрузим данные в алгоритм K-средних, необходимо их векторизовать. Самый популярный метод — векторизатор Tdidf, который создает матрицу на основе частоты слов в документах, и именно его мы собираемся использовать. По сути, это показывает, насколько важно слово для документа. Стоит упомянуть об этом в качестве будущей работы. слово2век и doc2vec может быть гораздо более эффективным для представления отношений между элементами.
tfidf_vectorizer = TfidfVectorizer(max_df=0.8, max_features=200000,min_df=0.2, stop_words='english',
use_idf=True, tokenizer=tokenize_and_stem, ngram_range=(1,3))
tfidf_matrix = tfidf_vectorizer.fit_transform(descriptions)
print('Td idf Matrix shape: ',tfidf_matrix.shape)
terms = tfidf_vectorizer.get_feature_names()
dist = 1 - cosine_similarity(tfidf_matrix)
Td idf Matrix shape: (1130, 74)
К означает
Здесь происходит фактическая кластеризация, где K означает создание 5 кластеров на основе матрицы Td-idf. Мы можем легко предсказать, что это не будет оптимальным решением, поскольку оно учитывает только частоту каждого слова в документе.
num_clusters = 5
km = KMeans(n_clusters=num_clusters)
km.fit(tfidf_matrix)
clusters = km.labels_.tolist()
Чтобы представить кластер, я создаю кадр данных pandas, индексированный кластерами. Ниже представлены первые 6 слов каждого кластера. Мы замечаем, что кластеризация далека от совершенства, поскольку некоторые слова входят более чем в одну группу. Также нет четкого разграничения смыслового содержания кластеров. Мы легко можем видеть, что термины, связанные с работой, включают более одного кластера.
items = { 'title': titles, 'description': descriptions}
frame = pd.DataFrame(items, index = (clusters) , columns = ( 'title','cluster'))
print("Top terms per cluster:")
order_centroids = km.cluster_centers_.argsort()(:, ::-1)
for i in range(num_clusters):
print("Cluster %d words:" % i, end='')
for ind in order_centroids(i, :6):
print(' %s' % vocab_frame.ix(terms(ind).split(' ')).values.tolist()(0)(0), end=',')
print()
Top terms per cluster:
Cluster 0 words: labour, employability, european, social, work, eu,
Cluster 1 words: occupational, sectors, skill, employability, services, workers,
Cluster 2 words: skill, job, labour, develop, market, cedefop,
Cluster 3 words: education, training, learning, vocational, education, cedefop,
Cluster 4 words: rates, unemployment, area, employability, increasingly, stated,
Визуализация
Чтобы визуализировать кластеризацию, мы должны сначала уменьшить их размерность. Мы достигли этого с помощью t-SNE (t-Distributed Stochastic Neighbor Embedding) из библиотеки sklearn.manifold. Другой способ использовать СПС или многомерное масштабирование (MDS).
Графика выполняется с помощью библиотеки matplotlib.
import os
import matplotlib.pyplot as plt
import matplotlib as mpl
from sklearn.manifold import TSNE
tsne = TSNE(n_components=2, verbose=1, perplexity=40, n_iter=300)
pos = tsne.fit_transform(dist)
xs, ys = pos(:, 0), pos(:, 1)
cluster_colors = {0: '#1b9e77', 1: '#d95f02', 2: '#7570b3', 3: '#e7298a', 4: '#66a61e'}
cluster_names = {0: 'A',1: 'B', 2: 'C', 3: 'D', 4: 'E'}
(t-SNE) Computing pairwise distances...
(t-SNE) Computing 121 nearest neighbors...
(t-SNE) Computed conditional probabilities for sample 1000 / 1130
(t-SNE) Computed conditional probabilities for sample 1130 / 1130
(t-SNE) Mean sigma: 1.785805
(t-SNE) KL divergence after 100 iterations with early exaggeration: 0.947952
(t-SNE) Error after 125 iterations: 0.947952
%matplotlib inline
df = pd.DataFrame(dict(x=xs, y=ys, label=clusters, title=titles))
groups = df.groupby('label')
fig, ax = plt.subplots(figsize=(16,8) )
for name, group in groups:
ax.plot(group.x, group.y, marker='o', linestyle='', ms=12,
label=cluster_names(name), color=cluster_colors(name), mec='none')
ax.legend(numpoints=1)
plt.show()
Мы видим, что результаты не так плохи, как мы думали изначально. Хотя есть некоторое частичное совпадение, группы весьма различаются. Однако нет никаких сомнений в том, что мы можем оптимизировать их гораздо дальше.
Следует отметить, что элементы с несколькими словами не представлены на графике. Я также заметил, что некоторые элементы написаны на языке, отличном от английского. В настоящее время мы не обрабатываем их, и поэтому их классификация фактически случайна. На диаграмме несколько неуместных точек.
Кроме того, предстоит еще много работы по очистке и предварительной обработке данных.
Один из способов — оптимизировать параметры векторизации tdidf, используя doc2vec для векторизации. Или мы можем использовать другой метод, такой как Affinity Propagation, Spectra Clustering или более современные методы, такие как HDBSCAN и Вариационные автоэнкодерыкоторый я хотел бы развивать.
PS: Чтобы запустить код, вы можете сделать это прямо из jupyter, если установлены необходимые зависимости, или вы можете экспортировать его в виде файла .py и запустить с помощью ide или напрямую через консоль.
* Раскрытие информации: Обратите внимание, что некоторые из приведенных выше ссылок могут быть партнерскими ссылками, и без дополнительной оплаты для вас мы будем получать комиссию, если вы решите совершить покупку после перехода по ссылке.