В этом посте я собираюсь перейти к Word2Vec. Я расскажу о некоторой предыстории того, что такое алгоритм, и о том, как его можно использовать для генерации векторов Word. Затем я рассмотрю код для обучения модели Word2Vec в наборе данных комментариев Reddit и изучу полученные результаты.

Что такое Word2Vec?

Word2Vec - это алгоритм, который генерирует векторизованные представления слов на основе их лингвистического контекста. Эти векторы слов получаются путем обучения мелкой нейронной сети (одиночный скрытый слой) на отдельных словах в тексте и задании окружающих слов в качестве метки для прогнозирования. Обучая нейронную сеть на данных, мы обновляем веса скрытого слоя, который мы извлекаем в конце как векторы слов.

Чтобы понять Word2Vec, давайте рассмотрим его шаг за шагом. На вход алгоритма поступают текстовые данные, поэтому для строки предложений обучающие данные будут состоять из отдельных предложений, соединенных с контекстными словами, которые мы пытаемся предсказать. Давайте посмотрим на следующую диаграмму (из блога Криса МакКормика):

Мы используем скользящее окно по тексту, и для каждого «целевого слова» в наборе мы соединяем его со смежным словом, чтобы получить пару x-y. В этом случае размер окна (обозначенный как C) равен 4, поэтому с каждой стороны по 2 слова, за исключением краевых слов. Затем входные слова преобразуются в горячие векторы. Горячие векторы - это векторное представление данных, в котором мы используем большой вектор нулей, который соответствует каждому слову в словаре, и устанавливаем позицию, соответствующую целевому слову, равной 1. В этом случае у нас в общей сложности есть V слов, поэтому каждый вектор будет иметь длину V и будет иметь индекс, соответствующий его положению. на 1. В приведенном выше примере вектор, соответствующий последнему предложению, будет 0 0 0 1 0 0 0 0, потому что слово «лиса» является 4-м словом в словаре, и есть 8 слов (считая «один раз»).

После получения входных данных следующим шагом будет проектирование нейронной сети. Word2Vec - это неглубокая сеть, что означает, что в ней есть единственный скрытый слой. Схема традиционной нейронной сети выглядит так:

На диаграмме выше входные данные передаются через нейронную сеть с применением весовых векторов и единиц смещения. Это означает, что для каждого x мы получаем вывод как:

Нейронные сети обучаются итеративно, что означает, что после выполнения прямого вычисления y мы обновляем векторы веса w и единицы смещения b с помощью обратный расчет того, насколько они изменяются относительно ошибка предсказания. Вот уравнения обновления, хотя я не буду вдаваться в фактический вывод обратного распространения ошибки в этом посте:

Давайте теперь посмотрим на схему нейронной сети Word2Vec:

На приведенной выше диаграмме хорошо представлена ​​модель Word2Vec. Мы берем в качестве входных данных одноразовые векторизованные представления слов, применяя W1b1) для получения представлений скрытого слоя, а затем снова вводим их через W2 и b2 и применение активации SoftMax для получения вероятностей целевого слова и каждого из связанных с ними значений y. Однако нам нужен Word2Vec для получения векторных представлений входных слов. Обучая модель целевым словам и окружающим их меткам, мы итеративно обновляем значения скрытого слоя до точки, где стоимость (или разница между предсказанием и фактической меткой) минимальна. После обучения модели мы извлекаем это скрытое представление как вектор слова слова, поскольку между ними существует соответствие 1: 1. Размер скрытых единиц h здесь также является важным фактором, поскольку он определяет длину представления скрытого слоя для каждого слова.

Обучение модели Word2Vec

Сейчас я покажу, как обучить модель Word2Vec. Для этого проекта я буду использовать Reddit May 2015 Dataset, доступный на Kaggle. Хотя я не буду реализовывать модель нейронной сети для Word2Vec с использованием библиотеки глубокого обучения (например, TensorFlow), ее нетрудно реализовать, поскольку схема сети легко очерчивается.

Сначала мы начнем с необходимого импорта. Для этого проекта нам понадобятся NLTK (для nlp), Gensim (для Word2Vec), SkLearn (для алгоритма кластеризации), Pandas и Numby (для структур данных и обработки).

%matplotlib inline
import nltk.data;
from gensim.models import word2vec;
from sklearn.cluster import KMeans;
from sklearn.neighbors import KDTree;
import pandas as pd;
import numpy as np;
import os;
import re;
import logging;
import sqlite3;
import time;
import sys;
import multiprocessing;
from wordcloud import WordCloud, ImageColorGenerator
import matplotlib.pyplot as plt;
from itertools import cycle;

Из NLTK нам нужно скачать пакет «Punkt», который содержит модуль для получения предложений из текста. Пакет необходимо сначала загрузить.

nltk.download('punkt')

Набор данных, который я использую, доступен на Kaggle здесь: http://kaggle.com/reddit/reddit-comments-may-2015. Его необходимо загрузить и распаковать в локальном месте назначения.

Поскольку данные находятся в формате .sqlite, мы откроем соединение sql для их чтения.

sql_con = sqlite3.connect('/mnt/big/data/database.sqlite')

Следует отметить, что набор данных очень большой по размеру (8 ГБ со сжатием / 30 ГБ без сжатия). Я предлагаю вам использовать машину с достаточным объемом оперативной памяти для обработки. Для моей реализации я запустил ноутбук на экземпляре AWS P4.2xLarge с 60 ГБ оперативной памяти.

start = time.time()
sql_data = pd.read_sql("SELECT body FROM May2015", sql_con);
print('Total time: ' + str((time.time() - start)) + ' secs')

Моей машине AWS потребовалось около 1,5 минут, чтобы загрузить все.

Total time: 82.60828137397766 secs

Проверка длины фрейма данных должна показать, что в этом наборе данных около 55 000 000 отдельных комментариев.

Используя пакет Punkt от NLTK, мы получаем токенизатор String. Токенизатор позволяет нам кормить его комментариями и получать в нем отдельные предложения. Он будет использоваться как часть предварительной обработки.

tokenizer = nltk.data.load('tokenizers/punkt/english.pickle');

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

1. Удалите все escape-символы табуляции и escape-символы новой строки
2. Удалите все символы, не являющиеся символами (кроме точки).
3. Преобразуйте пробелы в один символ
4. Удалите начальные и конечные символы. пробелы
5. Преобразование текста в предложения

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

def clean_text(all_comments, out_name):
    
    out_file = open(out_name, 'w');
    
    for pos in range(len(all_comments)):
    
        #Get the comment
        val = all_comments.iloc[pos]['body'];
        
        #Normalize tabs and remove newlines
        no_tabs = str(val).replace('\t', ' ').replace('\n', '');
        
        #Remove all characters except A-Z and a dot.
        alphas_only = re.sub("[^a-zA-Z\.]", " ", no_tabs);
        
        #Normalize spaces to 1
        multi_spaces = re.sub(" +", " ", alphas_only);
        
        #Strip trailing and leading spaces
        no_spaces = multi_spaces.strip();
        
        #Normalize all charachters to lowercase
        clean_text = no_spaces.lower();
        
        #Get sentences from the tokenizer, remove the dot in each.
        sentences = tokenizer.tokenize(clean_text);
        sentences = [re.sub("[\.]", "", sentence) for sentence in sentences];
        
        #If the text has more than one space (removing single word comments) and one character, write it to the file.
        if len(clean_text) > 0 and clean_text.count(' ') > 0:
            for sentence in sentences:
                out_file.write("%s\n" % sentence)
                print(sentence);
                
        #Simple logging. At every 50000th step,
        #print the total number of rows processed and time taken so far, and flush the file.
        if pos % 50000 == 0:
            total_time = time.time() - start;
            sys.stdout.write('Completed ' + str(round(100 * (pos / total_rows), 2)) + '% - ' + str(pos) + ' rows in time ' + str(round(total_time / 60, 0)) + ' min & ' + str(round(total_time % 60, 2)) + ' secs\r');
            out_file.flush();
            break;
        
    out_file.close();

(Если указанную выше функцию трудно читать, обратитесь к ссылке GitHub в конце сообщения)

start = time.time();
clean_comments = clean_text(sql_data, '/mnt/big/out_full')
print('Total time: ' + str((time.time() - start)) + ' secs')

На очистку всего набора данных ушло около 5 часов. После завершения предварительной обработки выходной файл содержал чистые предложения без символов, прописных букв, начальных, конечных или нескольких пробелов и escape-символов.

Total time: 16183.129625082016 secs time 270.0 min & 41.12 secs

Теперь мы обучим модель Word2Vec очищенным предложениям.

start = time.time();
#Set the logging format to get some basic updates.
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s',\
    level=logging.INFO)
# Set values for various parameters
num_features = 100;    # Dimensionality of the hidden layer representation
min_word_count = 40;   # Minimum word count to keep a word in the vocabulary
num_workers = multiprocessing.cpu_count();       # Number of threads to run in parallel set to total number of cpus.
context = 5          # Context window size (on each side)                                                       
downsampling = 1e-3   # Downsample setting for frequent words
# Initialize and train the model. 
#The LineSentence object allows us to pass in a file name directly as input to Word2Vec,
#instead of having to read it into memory first.
print("Training model...");
model = word2vec.Word2Vec(LineSentence('/mnt/big/out_full_clean'), workers=num_workers, \
            size=num_features, min_count = min_word_count, \
            window = context, sample = downsampling);
# We don't plan on training the model any further, so calling 
# init_sims will make the model more memory efficient by normalizing the vectors in-place.
model.init_sims(replace=True);
# Save the model
model_name = "model_full_reddit";
model.save(model_name);
print('Total time: ' + str((time.time() - start)) + ' secs')

Затем мы получаем векторы слов для каждого слова в словаре, хранящиеся в переменной с именем «syn0»:

Z = model.wv.syn0;
print(Z[0].shape)
Z[0]

Глядя на вектор слов для первого слова, мы видим вектор из 100 элементов со значениями, обновленными после обучения модели нейронной сети.

(100,)
array([-0.11665151, -0.049594  ,  0.11327834,  0.07592423, -0.04993806,
        0.1568293 , -0.1132786 ,  0.22942989,  0.00898544, -0.28502461 
. . .
        0.0221282 ,  0.03846532, -0.05099594,  0.00453909,  0.10295779,
        0.10701912, -0.00672292,  0.12998071,  0.10565597,  0.16730358,
        0.08564204, -0.0385814 , -0.0275824 ,  0.08518873, -0.01272774,
        0.14785041,  0.04440513, -0.09262343,  0.23331712, -0.05708617,
        0.03630534,  0.11807019, -0.11764669,  0.01931123, -0.03500355,
        0.00498019,  0.07433683,  0.09522536,  0.08134035,  0.18196103], dtype=float32)

Теперь мы проанализируем результаты алгоритмов по-разному, чтобы увидеть, что мы можем делать с Word2Vec. Первое, что мы сделаем, это сгруппируем слова с помощью KMeans. Поскольку слова представлены в виде векторов, применить KMeans легко, поскольку алгоритм кластеризации просто смотрит на различия между векторами (и центрами).

def clustering_on_wordvecs(word_vectors, num_clusters):
    # Initalize a k-means object and use it to extract centroids
    kmeans_clustering = KMeans(n_clusters = num_clusters, init='k-means++');
    idx = kmeans_clustering.fit_predict(word_vectors);
    
    return kmeans_clustering.cluster_centers_, idx;

Я запускаю KMeans на 50 кластерах, поскольку набор данных очень разнообразен. Большее количество кластеров может быть даже лучше для получения более интересных тем.

centers, clusters = clustering_on_wordvecs(Z, 50);
centroid_map = dict(zip(model.wv.index2word, clusters));

Затем мы получаем слова в каждом кластере, которые находятся ближе всего к центру кластера. Для этого мы инициализируем KDTree на векторах слов и запрашиваем у него первые K слов в каждом центре кластера. Используя словарь слов Index 2, мы затем сравниваем каждый вектор слова с его исходным представлением слова и добавляем их во фрейм данных для облегчения печати.

def get_top_words(index2word, k, centers, wordvecs):
    tree = KDTree(wordvecs);
#Closest points for each Cluster center is used to query the closest 20 points to it.
    closest_points = [tree.query(np.reshape(x, (1, -1)), k=k) for x in centers];
    closest_words_idxs = [x[1] for x in closest_points];
#Word Index is queried for each position in the above array, and added to a Dictionary.
    closest_words = {};
    for i in range(0, len(closest_words_idxs)):
        closest_words['Cluster #' + str(i)] = [index2word[j] for j in closest_words_idxs[i][0]]
#A DataFrame is generated from the dictionary.
    df = pd.DataFrame(closest_words);
    df.index = df.index+1
return df;

Давайте возьмем верхние слова и напечатаем первые 20 в каждом кластере:

top_words = get_top_words(model.wv.index2word, 5000, centers, Z);

Хотя мы можем распечатать самые популярные слова из кластера, их может быть проще визуализировать в WordCloud. Следующая функция создаст облако слов со словами кластера, распечатает и сохранит их.

def display_cloud(cluster_num, cmap):
    wc = WordCloud(background_color="black", max_words=2000, max_font_size=80, colormap=cmap);
    wordcloud = wc.generate(' '.join([word for word in top_words['Cluster #' + str(cluster_num).zfill(2)]]))
plt.imshow(wordcloud, interpolation='bilinear')
    plt.axis("off")
    plt.savefig('cluster_' + str(cluster_num), bbox_inches='tight')

Мы будем называть их в каждом кластере и передавать разные цветовые схемы на каждой итерации, чтобы различать их.

cmaps = cycle([
            'flag', 'prism', 'ocean', 'gist_earth', 'terrain', 'gist_stern',
            'gnuplot', 'gnuplot2', 'CMRmap', 'cubehelix', 'brg', 'hsv',
            'gist_rainbow', 'rainbow', 'jet', 'nipy_spectral', 'gist_ncar'])
for i in range(50):
    col = next(cmaps);
    display_cloud(i+1, col)

После сохранения облака слов получаем следующее:

Я удалил несколько кластеров с непристойными словами (и из-за того, как работает Word2Vec, большинство слов в них были исключительно непристойными). На приведенной выше диаграмме мы можем видеть, насколько хорошо некоторые кластеры смогли идентифицировать похожие темы.

Во 2-м кластере в первом ряду есть восклицательные слова, такие как «тааааанк», «яааааа», «ахахахахахахаха», «айыыыыы» и «ааав».

В 1-м кластере 7-го ряда мы видим такие слова, как «знакомый», «друг», «товарищ по работе».

В 5-м кластере во 2-м ряду мы видим «Ринго», «Спрингстин», «Трамп», «Джоэл» и «Оптед», все музыкальные термины и исполнители.

Во 2-м кластере 4-го ряда есть слова «chromecast», «iPhone», «сервопривод», «маршрутизатор», «донгл», «dell» и «сенсорный экран», которые относятся к электронным устройствам и аксессуарам.

Другой кластер имеет только такие названия субреддитов, как «thingsjonsnowknows», «noisygifs», «peoplebeingbros» и «theydidthemonstermath».

Что еще мы можем делать с векторами Word? Gensim предоставляет нам несколько встроенных функций, с которыми мы можем поиграть. Мы можем использовать аналогии, чтобы увидеть словесные ассоциации. Например, король для женщины такой же, как королева для _, мы получаем:

def print_word_table(table, key):
    return pd.DataFrame(table, columns=[key, 'similarity'])
print_word_table(model.wv.most_similar_cosmul(positive=['king', 'woman'], negative=['queen']), 'Analogy')

Хотя «мужчина» здесь не первое ключевое слово, некоторые другие слова также попадают в ту же категорию.

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

model.wv.doesnt_match("apple microsoft samsung tesla".split())

Получаем: тесла

model.wv.doesnt_match("trump clinton sanders obama".split())

Получаем: козырь

model.wv.doesnt_match("joffrey cersei tywin lannister jon".split())

Получаем: Джон

model.wv.doesnt_match("daenerys rhaegar viserion aemon aegon jon targaryen".split())

Получаем: viserion

Эти примеры показывают, что Reddit думает о разных словах: Tesla - это другая компания из четырех других (другой тип технологий и то, что они делают), а Трамп - другой политик (только республиканец).

В наборе имен Ланстера мы получаем Джона Сноу, и, что еще более удивительно, в наборе имен Таргариенов Джон не выделяется, а Визерион выделяется (возможно, потому, что он более жестокий).

Наконец, мы можем использовать Word Vectors, чтобы найти слова, наиболее близкие к цели по сходству.

keys = ['musk', 'modi', 'hodor', 'martell', 'apple', 'neutrality', 'snowden', 'batman', 'hulk', 'warriors', 'falcons', 'pizza', ];
tables = [];
for key in keys:
    tables.append(get_word_table(model.wv.similar_by_word(key), key, show_sim=False))
pd.concat(tables, axis=1)

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

Эти результаты показывают, насколько эффективны векторы слов для понимания контекста между словами. Мы видим, что модель легко может найти имена других людей, обычно связанных с «Илоном Маском» в первом столбце или тех, кто связан с премьер-министром Индии Моди во 2-м столбце.

В соответствии с темой Игры престолов, когда я прохожу через «Ходор», мы видим имена других людей с севера, такие как Бенджен, Бран, Мира и Крастер, но переход в «Мартелл» дает названия другим домам в Вестеросе. вместо этого как Мормонт и Талли.

Слово «нейтралитет» показывает некоторые интересные результаты, слова, которые описывают, как Reddit относится к сетевому нейтралитету, например, «приватизация», «цензура» и «ttip» (Трансатлантическое торгово-инвестиционное партнерство), а «Сноуден» имеет слова как разоблачитель, Ассанж и НСА.

Просто ради забавы, мы видим имена других супергероев, когда передаем «Бэтмен» и «Халк», хотя в первом списке больше совпадений героев Marvel, чем героев DC во втором.

Передача имени команды НБА и команды НФЛ возвращает другие команды в лигах, а «Пицца» просто возвращает другие названия блюд.

использованная литература

Чтобы просмотреть дополнительные материалы по Word Vectors, я рекомендую прочитать несколько сообщений.

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

Учебное пособие по Word2Vec - Модель скип-грамма Криса МакКормика. Этот пост представляет собой очень хорошо написанное руководство по модели Skip Gram, которая также используется в моем посте. В блоге также есть много других источников, которые предоставляют более подробную информацию об алгоритме.

Удивительная сила векторов слов. Правильно подробное сообщение в блоге, объясняющее интуицию, лежащую в основе векторов Word, и то, как их можно использовать после того, как мы получим их из модели Word2Vec.

Эффективное оценивание представлений слов в векторном пространстве. Оригинальная статья Google по модели Word2Vec. Этот документ очень прост для чтения и понимания и объясняет преимущество модели неглубокой нейронной сети перед другими нейронными сетями NLP, которые стремятся изучить тот же тип информации.

Полный код

Окончательный код можно посмотреть в моем репозитории GitHub здесь:

Https://github.com/ravishchawla/word_2_vec

или просмотреть в Gist ниже:

Https://gist.github.com/ravishchawla/91994122e1820e976daa41c7aa8f4998