Кластеризация и визуализация текстовой информации

МЕНЮ


Искусственный интеллект. Новости
Поиск

ТЕМЫ


Новости ИИРабота разумаВнедрение ИИРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информации

RSS


RSS новости

Авторизация



Новостная лента форума ailab.ru

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

Большинство используемых библиотек уже есть в дистрибутиве Anaconda 3, поэтому советую использовать его. Недостающие модули/библиотеки можно установить стандартно через pip install «название пакета».
Подключаем следующие библиотеки:

import numpy as np import pandas as pd import nltk import re import os import codecs from sklearn import feature_extraction import mpld3 import matplotlib.pyplot as plt import matplotlib as mpl 

Для анализа можно взять любые данные. Мне на глаза тогда попала данная задача: Статистика поисковых запросов проекта Госзатраты. Им нужно было разбить данные на три группы: частные, государственные и коммерческие организации. Придумывать экстраординарное ничего не хотелось, поэтому решил проверить, как поведет кластеризация в данном случае (забегая наперед — не очень). Но можно выкачать данные из VK какого-нибудь паблика:

import vk #передаешь id сессии session = vk.Session(access_token='') # URL для получения access_token, вместо tvoi_id вставляете id созданного приложения Вк: # https://oauth.vk.com/authorize?client_id=tvoi_id&scope=friends,pages,groups,offline&redirect_uri=https://oauth.vk.com/blank.html&display=page&v=5.21&response_type=token  api = vk.API(session) poss=[] id_pab=-59229916 #id пабликов начинаются с минуса, id стены пользователя без минуса info=api.wall.get(owner_id=id_pab, offset=0, count=1) kolvo = (info[0]//100)+1  shag=100 sdvig=0 h=0  import time  while h<kolvo:     if(h>70):         print(h) #не обязательное условие, просто для контроля примерного окончания процесса     pubpost=api.wall.get(owner_id=id_pab, offset=sdvig, count=100)     i=1     while i < len(pubpost):         b=pubpost[i]['text']         poss.append(b)         i=i+1     h=h+1     sdvig=sdvig+shag     time.sleep(1) len(poss)  import io with io.open("public.txt", 'w', encoding='utf-8', errors='ignore') as file:     for line in poss:         file.write("%s " % line) file.close()  titles = open('public.txt', encoding='utf-8', errors='ignore').read().split(' ')  print(str(len(titles)) + ' постов считано')  import re posti=[]  #удалим все знаки препинания и цифры for line in titles:     chis = re.sub(r'(<(/?[^>]+)>)', ' ', line)     #chis = re.sub()     chis = re.sub('[^а-яА-Я ]', '', chis)     posti.append(chis) 

Я буду использовать данные поисковых запросов чтобы показать, как плохо кластеризуются короткие текстовые данные. Я заранее очистил от спецсимволов и знаков препинания текст плюс провел замену сокращений (например, ИП – индивидуальный предприниматель). Получился текст, где в каждой строке находился один поисковый запрос.

Считываем данные в массив и приступаем к нормализации – приведению слова к начальной форме. Это можно сделать несколькими способами, используя стеммер Портера, стеммер MyStem и PyMorphy2. Хочу предупредить – MyStem работает через wrapper, поэтому скорость выполнения операций очень медленная. Остановимся на стеммере Портера, хотя никто не мешает использовать другие и комбинировать их с друг другом (например, пройтись PyMorphy2, а после стеммером Портера).

titles = open('material4.csv', 'r', encoding='utf-8', errors='ignore').read().split(' ') print(str(len(titles)) + ' запросов считано')  from nltk.stem.snowball import SnowballStemmer stemmer = SnowballStemmer("russian")  def token_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('[а-яА-Я]', token):             filtered_tokens.append(token)     stems = [stemmer.stem(t) for t in filtered_tokens]     return stems  def token_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('[а-яА-Я]', token):             filtered_tokens.append(token)     return filtered_tokens  #Создаем словари (массивы) из полученных основ totalvocab_stem = [] totalvocab_token = [] for i in titles:     allwords_stemmed = token_and_stem(i)     #print(allwords_stemmed)     totalvocab_stem.extend(allwords_stemmed)          allwords_tokenized = token_only(i)     totalvocab_token.extend(allwords_tokenized)  

Pymorphy2

import pymorphy2 morph = pymorphy2.MorphAnalyzer()  G=[]  for i in titles:     h=i.split(' ')     #print(h)     s=''     for k in h:         #print(k)         p = morph.parse(k)[0].normal_form         #print(p)         s+=' '         s += p         #print(s)         #G.append(p)     #print(s)     G.append(s)  pymof = open('pymof_pod.txt', 'w', encoding='utf-8', errors='ignore') pymofcsv = open('pymofcsv_pod.csv', 'w', encoding='utf-8', errors='ignore') for item in G:     pymof.write("%s " % item)     pymofcsv.write("%s " % item) pymof.close() pymofcsv.close() 

pymystem3

Исполняемые файлы анализатора для текущей операционной системы будут автоматически загружены и установлены при первом использовании библиотеки.

from pymystem3 import Mystem m = Mystem() A = []  for i in titles:     #print(i)     lemmas = m.lemmatize(i)     A.append(lemmas)  #Этот массив можно сохранить в файл либо "забэкапить" import pickle with open("mystem.pkl", 'wb') as handle:                     pickle.dump(A, handle) 
Создадим матрицу весов TF-IDF. Будем считать каждый поисковой запрос за документ (так делают при анализе постов в Twitter, где каждый твит – это документ). tfidf_vectorizer мы возьмем из пакета sklearn, а стоп-слова мы возьмем из корпуса ntlk (изначально придется скачать через nltk.download()). Параметры можно подстроить как вы считаете нужным – от верхней и нижней границы до количества n-gram (в данном случае возьмем 3).
stopwords = nltk.corpus.stopwords.words('russian') #можно расширить список стоп-слов stopwords.extend(['что', 'это', 'так', 'вот', 'быть', 'как', 'в', 'к', 'на'])  from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer  n_featur=200000 tfidf_vectorizer = TfidfVectorizer(max_df=0.8, max_features=10000,                                  min_df=0.01, stop_words=stopwords,                                  use_idf=True, tokenizer=token_and_stem, ngram_range=(1,3)) get_ipython().magic('time tfidf_matrix = tfidf_vectorizer.fit_transform(titles)') print(tfidf_matrix.shape) 

Над полученной матрицей начинаем применять различные методы кластеризации:

num_clusters = 5  # Метод к-средних - KMeans from sklearn.cluster import KMeans  km = KMeans(n_clusters=num_clusters) get_ipython().magic('time km.fit(tfidf_matrix)') idx = km.fit(tfidf_matrix) clusters = km.labels_.tolist()  print(clusters) print (km.labels_)  # MiniBatchKMeans from sklearn.cluster import MiniBatchKMeans  mbk  = MiniBatchKMeans(init='random', n_clusters=num_clusters) #(init='k-means++', ‘random’ or an ndarray) mbk.fit_transform(tfidf_matrix) %time mbk.fit(tfidf_matrix) miniclusters = mbk.labels_.tolist() print (mbk.labels_)   # DBSCAN from sklearn.cluster import DBSCAN get_ipython().magic('time db = DBSCAN(eps=0.3, min_samples=10).fit(tfidf_matrix)') labels = db.labels_ labels.shape print(labels)  # Аггломеративная класстеризация from sklearn.cluster import AgglomerativeClustering  agglo1 = AgglomerativeClustering(n_clusters=num_clusters, affinity='euclidean') #affinity можно выбрать любое или попробовать все по очереди: cosine, l1, l2, manhattan get_ipython().magic('time answer = agglo1.fit_predict(tfidf_matrix.toarray())') answer.shape 

Полученные данные можно сгруппировать в dataframe и посчитать количество запросов, попавших в каждый кластер.

#k-means clusterkm = km.labels_.tolist() #minikmeans clustermbk = mbk.labels_.tolist() #dbscan clusters3 = labels  #agglo #clusters4 = answer.tolist()  frame = pd.DataFrame(titles, index = [clusterkm])  #k-means out = { 'title': titles, 'cluster': clusterkm } frame1 = pd.DataFrame(out, index = [clusterkm], columns = ['title', 'cluster'])  #mini out = { 'title': titles, 'cluster': clustermbk } frame_minik = pd.DataFrame(out, index = [clustermbk], columns = ['title', 'cluster'])  frame1['cluster'].value_counts() frame_minik['cluster'].value_counts() 

Из-за большого количества запросов не совсем удобно смотреть таблицы и хотелось бы больше интерактивности для понимания. Поэтому сделаем графики взаимного расположения запросов относительного друг друга.

Сначала необходимо вычислить расстояние между векторами. Для этого будет применяться косинусовое расстояние. В статьях предлагают использовать вычитание из единицы, чтобы не было отрицательных значений и находилось в пределах от 0 до 1, поэтому сделаем так же:

from sklearn.metrics.pairwise import cosine_similarity dist = 1 - cosine_similarity(tfidf_matrix) dist.shape 

Так как графики будут двух-, трехмерные, а исходная матрица расстояний n-мерная, то придется применять алгоритмы снижения размерности. На выбор есть много алгоритмов (MDS, PCA, t-SNE), но остановим выбор на Incremental PCA. Этот выбор сделан в следствии практического применения – я пробовал MDS и PCA, но оперативной памяти мне не хватало (8 гигабайт) и когда начинал использоваться файл подкачки, то можно было сразу уводить компьютер на перезагрузку.

Алгоритм Incremental PCA используется в качестве замены метода главных компонентов (PCA), когда набор данных, подлежащий разложению, слишком велик, чтобы разместиться в оперативной памяти. IPCA создает низкоуровневое приближение для входных данных, используя объем памяти, который не зависит от количества входных выборок данных.

# Метод главных компонент - PCA  from sklearn.decomposition import IncrementalPCA icpa = IncrementalPCA(n_components=2, batch_size=16) get_ipython().magic('time icpa.fit(dist) #demo =') get_ipython().magic('time demo2 = icpa.transform(dist)') xs, ys = demo2[:, 0], demo2[:, 1]  # PCA 3D from sklearn.decomposition import IncrementalPCA icpa = IncrementalPCA(n_components=3, batch_size=16) get_ipython().magic('time icpa.fit(dist) #demo =') get_ipython().magic('time ddd = icpa.transform(dist)') xs, ys, zs = ddd[:, 0], ddd[:, 1], ddd[:, 2]  #Можно сразу примерно посмотреть, что получится в итоге #from mpl_toolkits.mplot3d import Axes3D #fig = plt.figure() #ax = fig.add_subplot(111, projection='3d') #ax.scatter(xs, ys, zs) #ax.set_xlabel('X') #ax.set_ylabel('Y') #ax.set_zlabel('Z') #plt.show() 

Перейдем непосредственно к самой визуализации:

from matplotlib import rc #включаем русские символы на графике font = {'family' : 'Verdana'}#, 'weigth': 'normal'} rc('font', **font)  #можно сгенерировать цвета для кластеров import random def generate_colors(n):     color_list = []     for c in range(0,n):         r = lambda: random.randint(0,255)         color_list.append( '#%02X%02X%02X' % (r(),r(),r()) )     return color_list  #устанавливаем цвета cluster_colors = {0: '#ff0000', 1: '#ff0066', 2: '#ff0099',  3: '#ff00cc', 4: '#ff00ff',} #даем имена кластерам, но из-за рандома пусть будут просто 01234 cluster_names = {0: '0',  1: '1', 2: '2',  3: '3', 4: '4',} #matplotlib inline  #создаем data frame, который содержит координаты (из PCA) + номера кластеров и сами запросы df = pd.DataFrame(dict(x=xs, y=ys, label=clusterkm, title=titles))  #группируем по кластерам groups = df.groupby('label')  fig, ax = plt.subplots(figsize=(72, 36)) #figsize подбирается под ваш вкус  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.set_aspect('auto')     ax.tick_params(        axis= 'x',                   which='both',               bottom='off',               top='off',                  labelbottom='off')     ax.tick_params(        axis= 'y',                  which='both',              left='off',               top='off',                labelleft='off')      ax.legend(numpoints=1)  #показать легенду только 1 точки  #добавляем метки/названия в х,у позиции с поисковым запросом #for i in range(len(df)): #    ax.text(df.ix[i]['x'], df.ix[i]['y'], df.ix[i]['title'], size=6)    #показать график plt.show()  plt.close() 

Если раскомментировать строку с добавлением названий, то выглядеть это будет примерно так:

Пример с 10 кластерами
image
Не совсем то, что хотелось бы ожидать. Воспользуемся mpld3 для перевода рисунка в интерактивный график.
# Plot  fig, ax = plt.subplots(figsize=(25,27))  ax.margins(0.03)   for name, group in groups_mbk:     points = ax.plot(group.x, group.y, marker='o', linestyle='', ms=12, #ms=18                      label=cluster_names[name], mec='none',                       color=cluster_colors[name])     ax.set_aspect('auto')     labels = [i for i in group.title]          tooltip = mpld3.plugins.PointHTMLTooltip(points[0], labels, voffset=10, hoffset=10, #css=css)     mpld3.plugins.connect(fig, tooltip) #   , TopToolbar()          ax.axes.get_xaxis().set_ticks([])     ax.axes.get_yaxis().set_ticks([])      #ax.axes.get_xaxis().set_visible(False)     #ax.axes.get_yaxis().set_visible(False)  ax.set_title("Mini K-Means", size=20) #groups_mbk      ax.legend(numpoints=1) mpld3.disable_notebook() #mpld3.display() mpld3.save_html(fig, "mbk.html")  mpld3.show() #mpld3.save_json(fig, "vivod.json") #mpld3.fig_to_html(fig)  fig, ax = plt.subplots(figsize=(51,25))  scatter = ax.scatter(np.random.normal(size=N),                      np.random.normal(size=N),                      c=np.random.random(size=N),                      s=1000 * np.random.random(size=N),                      alpha=0.3,                      cmap=plt.cm.jet) ax.grid(color='white', linestyle='solid')      ax.set_title("Кластеры", size=20)  fig, ax = plt.subplots(figsize=(51,25))  labels = ['point {0}'.format(i + 1) for i in range(N)] tooltip = mpld3.plugins.PointLabelTooltip(scatter, labels=labels) mpld3.plugins.connect(fig, tooltip)  mpld3.show()fig, ax = plt.subplots(figsize=(72,36)) for name, group in groups:     points = ax.plot(group.x, group.y, marker='o', linestyle='', ms=18,                       label=cluster_names[name], mec='none',                       color=cluster_colors[name])     ax.set_aspect('auto')     labels = [i for i in group.title]     tooltip = mpld3.plugins.PointLabelTooltip(points, labels=labels)     mpld3.plugins.connect(fig, tooltip)  ax.set_title("K-means", size=20)  mpld3.display() 

Теперь при наведении на любую точку графика всплывает текст с соотвествующим поисковым запросом. Пример готового html файла можно посмотреть здесь: Mini K-Means

Если хочется в 3D и с изменяемым масштабом, то существует сервис Plotly, который имеет плагин для Python.

Plotly 3D

#для примера просто 3D график из полученных значений import plotly plotly.__version__  import plotly.plotly as py import plotly.graph_objs as go  trace1 = go.Scatter3d(     x=xs,     y=ys,     z=zs,     mode='markers',     marker=dict(         size=12,         line=dict(             color='rgba(217, 217, 217, 0.14)',             width=0.5         ),         opacity=0.8     ) )  data = [trace1] layout = go.Layout(     margin=dict(         l=0,         r=0,         b=0,         t=0     ) ) fig = go.Figure(data=data, layout=layout) py.iplot(fig, filename='cluster-3d-plot') 

Результаты можно увидеть здесь: Пример

И заключительным пунктом выполним иерархическую (аггломеративную) кластеризацию по методу Уорда для создания дендограммы.

In [44]: from scipy.cluster.hierarchy import ward, dendrogram linkage_matrix = ward(dist)  fig, ax = plt.subplots(figsize=(15, 20)) ax = dendrogram(linkage_matrix, orientation="right", labels=titles);  plt.tick_params(     axis= 'x',     which='both',      bottom='off',     top='off',     labelbottom='off')  plt.tight_layout()   #сохраним рисунок plt.savefig('ward_clusters2.png', dpi=200) 

Выводы

К сожалению, в области исследования естественного языка очень много нерешённых вопросов и не все данные легко и просто сгруппировать в конкретные группы. Но надеюсь, что данное руководство усилит интерес к данной теме и даст базис для дальнейших экспериментов.

Источник: habrahabr.ru