Global vectors for word representation

Introduction

GloVe is an unsupervised learning algorithm for obtaining vector representations for words. Training is performed on aggregated global word-word co-occurrence statistics from a corpus, and the resulting representations showcase interesting linear substructures of the word vector space.

Getting started (Code download)

  • Download the latest latest code
    (licensed under the
    Apache License, Version 2.0).
    Look for «Clone or download»
  • Unpack the files:  unzip master.zip
  • Compile the source:  cd GloVe-master && make
  • Run the demo script: ./demo.sh
  • Consult the included README for further usage details, or ask a question

Download pre-trained word vectors

  • Pre-trained word vectors. This data is made available under the Public Domain Dedication
    and License v1.0 whose full text can be found at:
    http://www.opendatacommons.org/licenses/pddl/1.0/.

    • Wikipedia 2014 + Gigaword 5 (6B tokens, 400K vocab, uncased, 50d, 100d, 200d, & 300d vectors, 822 MB download): glove.6B.zip
    • Common Crawl (42B tokens, 1.9M vocab, uncased, 300d vectors, 1.75 GB download): glove.42B.300d.zip
    • Common Crawl (840B tokens, 2.2M vocab, cased, 300d vectors, 2.03 GB download): glove.840B.300d.zip
    • Twitter (2B tweets, 27B tokens, 1.2M vocab, uncased, 25d, 50d, 100d, & 200d vectors, 1.42 GB download): glove.twitter.27B.zip
  • Ruby script for preprocessing Twitter data

Citing GloVe

Jeffrey Pennington, Richard Socher, and Christopher D. Manning. 2014.
GloVe: Global Vectors for Word Representation.
[pdf] [bib]

Highlights

1.   Nearest neighbors

The Euclidean distance (or cosine similarity) between two word vectors provides an effective method for measuring the linguistic or semantic similarity of the corresponding words. Sometimes, the nearest neighbors according to this metric reveal rare but relevant words that lie outside an average human’s vocabulary. For example, here are the closest words to the target word frog:

  1. frog
  2. frogs
  3. toad
  4. litoria
  5. leptodactylidae
  6. rana
  7. lizard
  8. eleutherodactylus

3. litoria

4. leptodactylidae

5. rana

7. eleutherodactylus

2.   Linear substructures

The similarity metrics used for nearest neighbor evaluations produce a single scalar that quantifies the relatedness of two words. This simplicity can be problematic since two given words almost always exhibit more intricate relationships than can be captured by a single number. For example, man may be regarded as similar to woman in that both words describe human beings; on the other hand, the two words are often considered opposites since they highlight a primary axis along which humans differ from one another.

In order to capture in a quantitative way the nuance necessary to distinguish man from woman, it is necessary for a model to associate more than a single number to the word pair. A natural and simple candidate for an enlarged set of discriminative numbers is the vector difference between the two word vectors. GloVe is designed in order that such vector differences capture as much as possible the meaning specified by the juxtaposition of two words.

The underlying concept that distinguishes man from woman, i.e. sex or gender, may be equivalently specified by various other word pairs, such as king and queen or brother and sister. To state this observation mathematically, we might expect that the vector differences manwoman, kingqueen, and brothersister might all be roughly equal. This property and other interesting patterns can be observed in the above set of visualizations.

Training

The GloVe model is trained on the non-zero entries of a global word-word co-occurrence matrix, which tabulates how frequently words co-occur with one another in a given corpus. Populating this matrix requires a single pass through the entire corpus to collect the statistics. For large corpora, this pass can be computationally expensive, but it is a one-time up-front cost. Subsequent training iterations are much faster because the number of non-zero matrix entries is typically much smaller than the total number of words in the corpus.

The tools provided in this package automate the collection and preparation of co-occurrence statistics for input into the model. The core training code is separated from these preprocessing steps and can be executed independently.

Model Overview

GloVe is essentially a log-bilinear model with a weighted least-squares objective. The main intuition underlying the model is the simple observation that ratios of word-word co-occurrence probabilities have the potential for encoding some form of meaning. For example, consider the co-occurrence probabilities for target words ice and steam with various probe words from the vocabulary. Here are some actual probabilities from a 6 billion word corpus:

As one might expect, ice co-occurs more frequently with solid than it does with gas, whereas steam co-occurs more frequently with gas than it does with solid. Both words co-occur with their shared property water frequently, and both co-occur with the unrelated word fashion infrequently. Only in the ratio of probabilities does noise from non-discriminative words like water and fashion
cancel out, so that large values (much greater than 1) correlate well with properties specific to ice, and small values (much less than 1) correlate well with properties specific of steam. In this way, the ratio of probabilities encodes some crude form of meaning associated with the abstract concept of thermodynamic phase.

The training objective of GloVe is to learn word vectors such that their dot product equals the logarithm of the words’ probability of co-occurrence. Owing to the fact that the logarithm of a ratio equals the difference of logarithms, this objective associates (the logarithm of) ratios of co-occurrence probabilities with vector differences in the word vector space. Because these ratios can encode some form of meaning, this information gets encoded as vector differences as well. For this reason, the resulting word vectors perform very well on word analogy tasks, such as those examined in the word2vec package.

Visualization

GloVe produces word vectors with a marked banded structure that is evident upon visualization:

The horizontal bands result from the fact that the multiplicative interactions in the model occur component-wise. While there are additive interactions resulting from a dot product, in general there is little room for the individual dimensions to cross-pollinate.

The horizontal bands become more pronounced as the word frequency increases. Indeed, there are noticeable long-range trends as a function of word frequency, and they are unlikely to have a linguistic origin. This feature is not unique to GloVe — in fact, I’m unaware of any model for word vector learning that avoids this issue.

The vertical bands, such as the one around word 230k-233k, are due to local densities of related words (usually numbers) that happen to have similar frequencies.

Release history

  • GloVe v.1.2: Minor bug
    fixes in code (memory, off-by-one, errors). Eval code now also
    available in Python and Octave. UTF-8 encoding of largest data file
    fixed. Prepared by Russell Stewart and Christopher Manning. Oct 2015.
  • GloVe v.1.0: Original
    release. Prepared by Jeffrey Pennington. Aug 2014.

Bugs/Issues/Discussion

GitHub: GloVe is on
GitHub. For bug reports and patches, you’re best off using the
GitHub Issues and Pull requests features.

Google Group: The Google Group
globalvectors
can be used for questions and general discussion on GloVe.

Jeffrey Pennington
|
August 2014

Site design courtesy of Jason Chuang

GloVe: Global Vectors for Word Representation

nearest neighbors of
frog
Litoria Leptodactylidae Rana Eleutherodactylus
Pictures
Comparisons man -> woman city -> zip comparative -> superlative
GloVe Geometry

We provide an implementation of the GloVe model for learning word representations, and describe how to download web-dataset vectors or train your own. See the project page or the paper for more information on glove vectors.

Download pre-trained word vectors

The links below contain word vectors obtained from the respective corpora. If you want word vectors trained on massive web datasets, you need only download one of these text files! Pre-trained word vectors are made available under the Public Domain Dedication and License.

  • Common Crawl (42B tokens, 1.9M vocab, uncased, 300d vectors, 1.75 GB download): glove.42B.300d.zip [mirror]
  • Common Crawl (840B tokens, 2.2M vocab, cased, 300d vectors, 2.03 GB download): glove.840B.300d.zip [mirror]
  • Wikipedia 2014 + Gigaword 5 (6B tokens, 400K vocab, uncased, 300d vectors, 822 MB download): glove.6B.zip [mirror]
  • Twitter (2B tweets, 27B tokens, 1.2M vocab, uncased, 200d vectors, 1.42 GB download): glove.twitter.27B.zip [mirror]

Train word vectors on a new corpus

If the web datasets above don’t match the semantics of your end use case, you can train word vectors on your own corpus.

$ git clone https://github.com/stanfordnlp/glove
$ cd glove && make
$ ./demo.sh

Make sure you have the following prerequisites installed when running the steps above:

  • GNU Make
  • GCC (Clang pretending to be GCC is fine)
  • Python and NumPy

The demo.sh script downloads a small corpus, consisting of the first 100M characters of Wikipedia. It collects unigram counts, constructs and shuffles cooccurrence data, and trains a simple version of the GloVe model. It also runs a word analogy evaluation script in python to verify word vector quality. More details about training on your own corpus can be found by reading demo.sh or the src/README.md

License

All work contained in this package is licensed under the Apache License, Version 2.0. See the include LICENSE file.

Начать стоит от печки, то есть с постановки задачи. Откуда берется сама задача word embedding?
Лирическое отступление: К сожалению, русскоязычное сообщество еще не выработало единого термина для этого понятия, поэтому мы будем использовать англоязычный.
Сам по себе embedding — это сопоставление произвольной сущности (например, узла в графе или кусочка картинки) некоторому вектору.

image

Сегодня мы говорим про слова и стоит обсудить, как делать такое сопоставление вектора слову.
Вернемся к предмету: вот у нас есть слова и есть компьютер, который должен с этими словами как-то работать. Вопрос — как компьютер будет работать со словами? Ведь компьютер не умеет читать, и вообще устроен сильно иначе, чем человек. Самая первая идея, приходящая в голову — просто закодировать слова цифрами по порядку следования в словаре. Идея очень продуктивна в своей простоте — натуральный ряд бесконечен и можно перенумеровать все слова, не опасаясь проблем. (На секунду забудем про ограничения типов, тем более, в 64-битное слово можно запихнуть числа от 0 до 2^64 — 1, что существенно больше количества всех слов всех известных языков.)

Но у этой идеи есть и существенный недостаток: слова в словаре следуют в алфавитном порядке, и при добавлении слова нужно перенумеровывать заново большую часть слов. Но даже это не является настолько важным, а важно то, буквенное написание слова никак не связано с его смыслом (эту гипотезу еще в конце XIX века высказал известный лингвист Фердинанд де Соссюр). В самом деле слова “петух”, “курица” и “цыпленок” имеют очень мало общего между собой и стоят в словаре далеко друг от друга, хотя очевидно обозначают самца, самку и детеныша одного вида птицы. То есть мы можем выделить два вида близости слов: лексический и семантический. Как мы видим на примере с курицей, эти близости не обязательно совпадают. Можно для наглядности привести обратный пример лексически близких, но семантически далеких слов — «зола» и «золото». (Если вы никогда не задумывались, то имя Золушка происходит именно от первого.)
Чтобы получить возможность представить семантическую близость, было предложено использовать embedding, то есть сопоставить слову некий вектор, отображающий его значение в “пространстве смыслов”.

Какой самый простой способ получить вектор из слова? Кажется, что естественно будет взять вектор длины нашего словаря и поставить только одну единицу в позиции, соответствующей номеру слова в словаре. Этот подход называется one-hot encoding (OHE). OHE все еще не обладает свойствами семантической близости:

Значит нам нужно найти другой способ преобразования слов в вектора, но OHE нам еще пригодится.

Отойдем немного назад — значение одного слова нам может быть и не так важно, т.к. речь (и устная, и письменная) состоит из наборов слов, которые мы называем текстами. Так что если мы захотим как-то представить тексты, то мы возьмем OHE-вектор каждого слова в тексте и сложим вместе. Т.е. на выходе получим просто подсчет количества различных слов в тексте в одном векторе. Такой подход называется “мешок слов” (bag of words, BoW), потому что мы теряем всю информацию о взаимном расположении слов внутри текста.

image

Но несмотря на потерю этой информации так тексты уже можно сравнивать. Например, с помощью косинусной меры.

image

Мы можем пойти дальше и представить наш корпус (набор текстов) в виде матрицы “слово-документ” (term-document). Стоит отметить, что в области информационного поиска (information retrieval) эта матрица носит название «обратного индекса» (inverted index), в том смысле, что обычный/прямой индекс выглядит как «документ-слово» и очень неудобен для быстрого поиска. Но это опять же выходит за рамки нашей статьи.

Эта матрица приводит нас к тематическим моделям, где матрицу “слово-документ” пытаются представить в виде произведения двух матриц “слово-тема” и “тема-документ”. В самом простом случае мы возьмем матрицу и с помощью SVD-разложения получим представление слов через темы и документов через темы:

image

Здесь $t_i$ — слова, $d_i$ — документы. Но это уже будет предметом другой статьи, а сейчас мы вернемся к нашей главной теме — векторному представлению слов.

Пусть у нас есть такой корпус:

s = ['Mars has an athmosphere', "Saturn 's moon Titan has its own athmosphere",
     'Mars has two moons', 'Saturn has many moons', 'Io has cryo-vulcanoes']

С помощью SVD-преобразования, выделим только первые две компоненты, и нарисуем:

Что интересного на этой картинке? То, что Титан и Ио — далеко друг от друга, хотя они оба являются спутниками Сатурна, но в нашем корпусе про это ничего нет. Слова «атмосфера» и «Сатурн» очень близко друг другу, хотя не являются синонимами. В то же время «два» и «много» стоят рядом, что логично. Но общий смысл этого примера в том, что результаты, которые вы получите очень сильно зависят от корпуса, с которым вы работаете. Весь код для получения картинки выше можно посмотреть здесь.

Логика повествования выводит на следующую модификацию матрицы term-document — формулу TF-IDF. Эта аббревиатура означает «term frequency — inverse document frequency».

$TF-IDF(w, d, C)=frac{count(w, d)}{count(d)}*log(frac{sum_{d' in C}{mathbb{1}(w,d')}}{|C|})$

Давайте попробуем разобраться, что это такое. Итак, TF — это частота слова $w$ в тексте $d$, здесь нет ничего сложного. А вот IDF — существенно более интересная вещь: это логарифм обратной частоты распространенности слова $w$ в корпусе $C$. Распространенностью называется отношение числа текстов, в которых встретилось искомое слово, к общему числу текстов в корпусе. С помощью TF-IDF тексты также можно сравнивать, и делать это можно с меньшей опаской, чем при использовании обычных частот.

Новая эпоха

Описанные выше подходы были (и остаются) хороши для времен (или областей), где количество текстов мало и словарь ограничен, хотя, как мы видели, там тоже есть свои сложности. Но с приходом в нашу жизнь интернета все стало одновременно и сложнее и проще: в доступе появилось великое множество текстов, и эти тексты с изменяющимся и расширяющимся словарем. С этим надо было что-то делать, а ранее известные модели не могли справиться с таким объемом текстов. Количество слов в английском языке очень грубо составляет миллион — матрица совместных встречаемостей только пар слов будет 10^6 x 10^6. Такая матрица даже сейчас не очень лезет в память компьютеров, а, скажем, 10 лет назад про такое можно было не мечтать. Конечно, были придуманы множество способов, упрощающих или распараллеливающих обработку таких матриц, но все это были паллиативные методы.

И тогда, как это часто бывает, был предложен выход по принципу “тот, кто нам мешает, тот нам поможет!” А именно, в 2013 году тогда мало кому известный чешский аспирант Томаш Миколов предложил свой подход к word embedding, который он назвал word2vec. Его подход основан на другой важной гипотезе, которую в науке принято называть гипотезой локальности — “слова, которые встречаются в одинаковых окружениях, имеют близкие значения”. Близость в данном случае понимается очень широко, как то, что рядом могут стоять только сочетающиеся слова. Например, для нас привычно словосочетание «заводной будильник». А сказать “заводной апельсин” мы не можем* — эти слова не сочетаются.

Основываясь на этой гипотезе Томаш Миколов предложил новый подход, который не страдал от больших объемов информации, а наоборот выигрывал [1].

image

Модель, предложенная Миколовым очень проста (и потому так хороша) — мы будем предсказывать вероятность слова по его окружению (контексту). То есть мы будем учить такие вектора слов, чтобы вероятность, присваиваемая моделью слову была близка к вероятности встретить это слово в этом окружении в реальном тексте.

$P(w_o| w_c)=frac{e^{s(w_o, w_c)}}{sum_{w_i in V} e^{s(w_i, w_c)}}$

Здесь $w_o$ — вектор целевого слова, $w_c$ — это некоторый вектор контекста, вычисленный (например, путем усреднения) из векторов окружающих нужное слово других слов. А $s(w_1, w_2)$ — это функция, которая двум векторам сопоставляет одно число. Например, это может быть упоминавшееся выше косинусное расстояние.

Приведенная формула называется softmax, то есть “мягкий максимум”, мягкий — в смысле дифференцируемый. Это нужно для того, чтобы наша модель могла обучиться с помощью backpropagation, то есть процесса обратного распространения ошибки.

Процесс тренировки устроен следующим образом: мы берем последовательно (2k+1) слов, слово в центре является тем словом, которое должно быть предсказано. А окружающие слова являются контекстом длины по k с каждой стороны. Каждому слову в нашей модели сопоставлен уникальный вектор, который мы меняем в процессе обучения нашей модели.

В целом, этот подход называется CBOW — continuous bag of words, continuous потому, что мы скармливаем нашей модели последовательно наборы слов из текста, a BoW потому что порядок слов в контексте не важен.

Также Миколовым сразу был предложен другой подход — прямо противоположный CBOW, который он назвал skip-gram, то есть “словосочетание с пропуском”. Мы пытаемся из данного нам слова угадать его контекст (точнее вектор контекста). В остальном модель не претерпевает изменений.

Что стоит отметить: хотя в модель не заложено явно никакой семантики, а только статистические свойства корпусов текстов, оказывается, что натренированная модель word2vec может улавливать некоторые семантические свойства слов. Классический пример из работы автора:

image

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

Более сложные вещи

На самом деле, за прошедшее время были предложены улучшения ставшей уже также классической модели Word2Vec. Два самых распространенных будут описаны ниже. Но этот раздел может быть пропущен без ущерба для понимания статьи в целом, если покажется слишком сложным.

Negative Sampling

В стандартной модели CBoW, рассмотренной выше, мы предсказываем вероятности слов и оптимизируем их. Функцией для оптимизации (минимизации в нашем случае) служит дивергенция Кульбака-Лейблера:

$KL(p||q) = int{p(x) logfrac{p(x)}{q(x)}} dx$

Здесь $p(x)$ — распределение вероятностей слов, которое мы берем из корпуса, $q(x)$ — распределение, которое порождает наша модель. Дивергенция — это буквально «расхождение», насколько одно распределение не похоже на другое. Т.к. наши распределения на словах, т.е. являются дискретными, мы можем заменить в этой формуле интеграл на сумму:

$KL(p||q) = sum_{x in V}{p(x) logfrac{p(x)}{q(x)}}$

Оказалось так, что оптимизировать эту формулу достаточно сложно. Прежде всего из-за того, что $q(x)$ рассчитывается с помощью softmax по всему словарю. (Как мы помним, в английском сейчас порядка миллиона слов.) Здесь стоит отметить, что многие слова вместе не встречаются, как мы уже отмечали выше, поэтому большая часть вычислений в softmax является избыточной. Был предложен элегантный обходной путь, который получил название Negative Sampling. Суть этого подхода заключается в том, что мы максимизируем вероятность встречи для нужного слова в типичном контексте (том, который часто встречается в нашем корпусе) и одновременно минимизируем вероятность встречи в нетипичном контексте (том, который редко или вообще не встречается). Формулой мысль выше записывается так:

$NegS(w_o) = sum_{i=1, x_i thicksim D}^{i=k}{-log(1 + e^{s(x_i, w_o)})} + sum_{j=1, x_j thicksim D'}^{j=l}{-log(1 + e^{-s(x_j, w_o)})}$

Здесь $s(x,w)$ — точно такой же, что и в оригинальной формуле, а вот остальное несколько отличается. Прежде всего стоит обратить внимание на то, что формуле теперь состоит из двух частей: позитивной ($s(x,w)$) и негативной ($-s(x,w)$). Позитивная часть отвечает за типичные контексты, и $D$ здесь — это распределение совместной встречаемости слова $w$ и остальных слов корпуса. Негативная часть — это, пожалуй, самое интересное — это набор слов, которые с нашим целевым словом встречаются редко. Этот набор порождается из распределения $D'$, которое на практике берется как равномерное по всем словам словаря корпуса. Было показано, что такая функция приводит при своей оптимизации к результату, аналогичному стандартному softmax [2].

Hierarchical SoftMax

Также люди зашли и с другой стороны — можно не менять исходную формулу, а попробовать посчитать сам softmax более эффективно. Например, используя бинарное дерево [3]. По всем словам в словаре строится дерево Хаффмана. В полученном дереве $V$ слов располагаются на листьях дерева.

image

На рисунке изображен пример такого бинарного дерева. Жирным выделен путь от корня до слова $w_2$. Длину пути обозначим $L(w)$, а $j$-ую вершину на пути к слову $w$ обозначим через $n(w,j)$. Можно доказать, что внутренних вершин (не листьев) $V − 1$.

С помощью иерархического softmax вектора $v_{n(w,j)}$ предсказывается для $V-1$ внутренних вершин. А вероятность того, что слово $w$ будет выходным словом (в зависимости от того, что мы предсказываем: слово из контекста или заданное слово по контексту) вычисляется по формуле:

$p(w=w_o)=prodlimits_{j=1}^{L(w)-1}sigma([n(w,j+1)=lch(n(w,j))] v_{n(w,j)}^T u)$

где $sigma(x)$ — функция softmax; $[true]=1,[false]=-1$; $lch(n)$ — левый сын вершины $n$; $u=v_{w_I}$, если используется метод skip-gram, $u=frac{1}{h} sumlimits_{k=1}^{h} v_{w_{I,k}}$, то есть, усредненный вектор контекста, если используется CBOW.

Формулу можно интуитивно понять, представив, что на каждом шаге мы можем пойти налево или направо с вероятностями:

$p(n,left)=sigma(v_n^T u)$
$p(n,right)=1-p(n,left)=1-sigma(v_n^T u)=sigma(-v_n^T u)$

Затем на каждом шаге вероятности перемножаются ($L(w)-1$ шагов) и получается искомая формула.

При использовании простого softmax для подсчета вероятности слова, приходилось вычислять нормирующую сумму по всем словам из словаря, требовалось $O(V)$ операций. Теперь же вероятность слова можно вычислить при помощи последовательных вычислений, которые требуют $O(log(V))$.

Другие модели

Помимо word2vec были, само собой, предложены и другие модели word embedding. Стоит отметить модель, предложенную лабораторией компьютерной лингвистики Стенфордского университета, под названием Global Vectors (GloVe), сочетающую в себе черты SVD разложения и word2vec [4].

Также надо упомянуть о том, что т.к. изначально все описанные модели были предложены для английского языка, то там не так остро стоит проблема словоизменения, характерная для синтетических языков (это — лингвистический термин), вроде русского. Везде выше по тексту неявно предполагалось, что мы либо считаем разные формы одного слова разными словами — и тогда надеяться, что нашего корпуса будет достаточно модели, чтобы выучить их синтаксическую близость, либо используем механизмы стеммирования или лемматизации. Стеммирование — это обрезание окончания слова, оставление только основы (например, “красного яблока” превратится в “красн яблок”). А лемматизация — замена слова его начальной формой (например, “мы бежим” превратится в “я бежать”). Но мы можем и не терять эту информацию, а использовать ее — закодировав OHE в новый вектор, и сконкатинировать его с вектором для основы или леммы.
Еще стоит сказать, что то, с чем мы начинали — буквенное представление слова — тоже не кануло в Лету: предложены модели по использованию буквенного представления слова для word embedding [5].

Практическое применение

Мы поговорили о теории, пришло время посмотреть, к чему все вышеописанное применимо на практике. Ведь любая самая красивая теория без практического применения — не более чем игра ума. Рассмотрим применение Word2Vec в двух задачах:
1) Задача классификации, необходимо по последовательности посещенных сайтов определять пользователя;
2) Задача регрессии, необходимо по тексту статьи определить ее рейтинг на Хабрахабре.

Классификация

# загрузим библиотеки и установим опции
from __future__ import division, print_function
# отключим всякие предупреждения Anaconda
import warnings
warnings.filterwarnings('ignore')
#%matplotlib inline
import numpy as np
import pandas as pd
from sklearn.metrics import roc_auc_score

Cкачать данные для первой задачи можно со страницы соревнования «Catch Me If You Can»

# загрузим обучающую и тестовую выборки
train_df = pd.read_csv('data/train_sessions.csv')#,index_col='session_id')
test_df = pd.read_csv('data/test_sessions.csv')#, index_col='session_id')

# приведем колонки time1, ..., time10 к временному формату
times = ['time%s' % i for i in range(1, 11)]
train_df[times] = train_df[times].apply(pd.to_datetime)
test_df[times] = test_df[times].apply(pd.to_datetime)

# отсортируем данные по времени
train_df = train_df.sort_values(by='time1')

# посмотрим на заголовок обучающей выборки
train_df.head()

image

sites = ['site%s' % i for i in range(1, 11)]
#заменим nan на 0
train_df[sites] = train_df[sites].fillna(0).astype('int').astype('str')
test_df[sites] = test_df[sites].fillna(0).astype('int').astype('str')
#создадим тексты необходимые для обучения word2vec
train_df['list'] = train_df['site1']
test_df['list'] = test_df['site1']
for s in sites[1:]:
    train_df['list'] = train_df['list']+","+train_df[s]
    test_df['list'] = test_df['list']+","+test_df[s]
train_df['list_w'] = train_df['list'].apply(lambda x: x.split(','))
test_df['list_w'] = test_df['list'].apply(lambda x: x.split(','))

#В нашем случае предложение это набор сайтов, которые посещал пользователь
#нам необязательно переводить цифры в названия сайтов, т.к. алгоритм будем выявлять взаимосвязь их друг с другом.
train_df['list_w'][10]

['229', '1500', '33', '1500', '391', '35', '29', '2276', '40305', '23']

# подключим word2vec
from gensim.models import word2vec

#объединим обучающую и тестовую выборки и обучим нашу модель на всех данных 
#с размером окна в 6=3*2 (длина предложения 10 слов) и итоговыми векторами размерности 300, параметр workers отвечает за количество ядер
test_df['target'] = -1
data = pd.concat([train_df,test_df], axis=0)

model = word2vec.Word2Vec(data['list_w'], size=300, window=3, workers=4)
#создадим словарь со словами и соответсвующими им векторами
w2v = dict(zip(model.wv.index2word, model.wv.syn0))

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

class mean_vectorizer(object):
    def __init__(self, word2vec):
        self.word2vec = word2vec
        self.dim = len(next(iter(w2v.values())))

    def fit(self, X):
        return self 

    def transform(self, X):
        return np.array([
            np.mean([self.word2vec[w] for w in words if w in self.word2vec] 
                    or [np.zeros(self.dim)], axis=0)
            for words in X
        ])

data_mean=mean_vectorizer(w2v).fit(train_df['list_w']).transform(train_df['list_w'])
data_mean.shape

(253561, 300)

Т.к. мы получили distributed representation, то никакое число по отдельности ничего не значит, а значит лучше всего покажут себя линейные алгоритмы. Попробуем нейронные сети, LogisticRegression и проверим нелинейный метод XGBoost.

# Воспользуемся валидацией
def split(train,y,ratio):
    idx = round(train.shape[0] * ratio)
    return train[:idx, :], train[idx:, :], y[:idx], y[idx:]
y = train_df['target']
Xtr, Xval, ytr, yval = split(data_mean, y,0.8)
Xtr.shape,Xval.shape,ytr.mean(),yval.mean()

((202849, 300), (50712, 300), 0.009726446765820882, 0.006389020350212968)

# подключим библиотеки keras 
from keras.models import Sequential, Model
from keras.layers import Dense, Dropout, Activation, Input
from keras.preprocessing.text import Tokenizer
from keras import regularizers

# опишем нейронную сеть
model = Sequential()
model.add(Dense(128, input_dim=(Xtr.shape[1])))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(1))
model.add(Activation('sigmoid'))
model.compile(loss='binary_crossentropy',
              optimizer='adam',
              metrics=['binary_accuracy'])

history = model.fit(Xtr, ytr,
                    batch_size=128,
                    epochs=10,
                    validation_data=(Xval, yval),
                    class_weight='auto',
                    verbose=0)

classes = model.predict(Xval, batch_size=128)
roc_auc_score(yval, classes)

0.91892341356995644

Получили неплохой результат. Значит Word2Vec смог выявить зависимости между сессиями.
Посмотрим, что произойдет с алгоритмом XGBoost.

import xgboost as xgb

dtr = xgb.DMatrix(Xtr, label= ytr,missing = np.nan)
dval = xgb.DMatrix(Xval, label= yval,missing = np.nan)
watchlist = [(dtr, 'train'), (dval, 'eval')]
history = dict()

params = {
    'max_depth': 26,
    'eta': 0.025,
    'nthread': 4,
    'gamma' : 1,
    'alpha' : 1,
    'subsample': 0.85,
    'eval_metric': ['auc'],
    'objective': 'binary:logistic',
    'colsample_bytree': 0.9,
    'min_child_weight': 100,
    'scale_pos_weight':(1)/y.mean(),
    'seed':7
}

model_new = xgb.train(params, dtr, num_boost_round=200, evals=watchlist, evals_result=history, verbose_eval=20)

Обучение

[0] train-auc:0.954886  eval-auc:0.85383
[20]    train-auc:0.989848  eval-auc:0.910808
[40]    train-auc:0.992086  eval-auc:0.916371
[60]    train-auc:0.993658  eval-auc:0.917753
[80]    train-auc:0.994874  eval-auc:0.918254
[100]   train-auc:0.995743  eval-auc:0.917947
[120]   train-auc:0.996396  eval-auc:0.917735
[140]   train-auc:0.996964  eval-auc:0.918503
[160]   train-auc:0.997368  eval-auc:0.919341
[180]   train-auc:0.997682  eval-auc:0.920183

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

from sklearn.linear_model import LogisticRegression
def get_auc_lr_valid(X, y, C=1, seed=7, ratio = 0.8):
    # разделим выборку на обучающую и валидационную
    idx = round(X.shape[0] * ratio)
    # обучение классификатора
    lr = LogisticRegression(C=C, random_state=seed, n_jobs=-1).fit(X[:idx], y[:idx])
    # прогноз для валидационной выборки
    y_pred = lr.predict_proba(X[idx:, :])[:, 1]
    # считаем качество
    score = roc_auc_score(y[idx:], y_pred)

    return score

get_auc_lr_valid(data_mean, y, C=1, seed=7, ratio = 0.8)

0.90037148150108237

Попробуем улучшить результаты.

Теперь вместо обычного среднего, чтобы учесть частоту с которой слово встречается в тексте, возьмем взвешенное среднее. В качестве весов возьмем IDF. Учёт IDF уменьшает вес широко употребительных слов и увеличивает вес более редких слов, которые могут достаточно точно указать на то, к какому классу относится текст. В нашем случае, кому принадлежит последовательность посещенных сайтов.

#пропишем класс выполняющий tfidf преобразование.
from sklearn.feature_extraction.text import TfidfVectorizer
from collections import defaultdict

class tfidf_vectorizer(object):
    def __init__(self, word2vec):
        self.word2vec = word2vec
        self.word2weight = None
        self.dim = len(next(iter(w2v.values())))

    def fit(self, X):
        tfidf = TfidfVectorizer(analyzer=lambda x: x)
        tfidf.fit(X)
        max_idf = max(tfidf.idf_)
        self.word2weight = defaultdict(
            lambda: max_idf,
            [(w, tfidf.idf_[i]) for w, i in tfidf.vocabulary_.items()])

        return self

    def transform(self, X):
        return np.array([
                np.mean([self.word2vec[w] * self.word2weight[w]
                         for w in words if w in self.word2vec] or
                        [np.zeros(self.dim)], axis=0)
                for words in X
            ])

data_mean = tfidf_vectorizer(w2v).fit(train_df['list_w']).transform(train_df['list_w'])

Проверим изменилось ли качество LogisticRegression.

get_auc_lr_valid(data_mean, y, C=1, seed=7, ratio = 0.8)

0.90738924587178804

видим прирост на 0.07, значит скорее всего взвешенное среднее помогает лучше отобразить смысл всего предложения через word2vec.

Предсказание популярности

Попробуем Word2Vec уже в текстовой задаче — предсказании популярности статьи на Хабрхабре.

Испробуем силы алгоритма непосредственно на текстовых данных статей Хабра. Мы преобразовали данные в csv таблицы. Скачать их вы можете здесь: train, test.

Xtrain = pd.read_csv('data/train_content.csv')
Xtest = pd.read_csv('data/test_content.csv')
print(Xtrain.shape,Xtest.shape)
Xtrain.head()

image

Пример текста

‘Доброго хабрадня!
rn
rnПерейду сразу к сути. С недавнего времени на меня возложилась задача развития контекстной сети текстовых объявлений. Задача возможно кому-то покажется простой, но есть несколько нюансов. Страна маленькая, 90% интернет-пользователей сконцентрировано в одном городе. С одной стороны легко охватить, с другой стороны некуда развиваться.
rn
rnТак как развитие интернет-проектов у нас слабое, и недоверие клиентов к местным проектам преобладает, то привлечь рекламодателей тяжело. Но самое страшное это привлечь площадки, которые знают и Бегун и AdSense, но абсолютно не знают нас. В целом проблема такая: площадки не регистрируются, потому что нет рекламодателей с деньгами, а рекламодатели не дают объявления, потому что список площадок слаб.
rn
rnКак выходят из такого положения Хабраспециалисты?’

Будем обучать модель на всем содержании статьи. Для этого совершим некоторые преобразования над текстом.

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

1) Сначала функция будет удалять все символы кроме букв верхнего и нижнего регистра;

2) Затем преобразовывает слова к нижнему регистру;

3) После чего удаляет стоп слова из текста, т.к. они не несут никакой информации о содержании;

4) Лемматизация, процесс приведения словоформы к лемме — её нормальной (словарной) форме.

Функция возвращает лист из слов.


# подключим необходимые библиотеки
from sklearn.metrics import mean_squared_error
import re
from nltk.corpus import stopwords
import pymorphy2
morph = pymorphy2.MorphAnalyzer()

stops = set(stopwords.words("english")) | set(stopwords.words("russian"))
def review_to_wordlist(review):
    #1)
    review_text = re.sub("[^а-яА-Яa-zA-Z]"," ", review)
    #2)
    words = review_text.lower().split()
    #3)
    words = [w for w in words if not w in stops]
    #4)
    words = [morph.parse(w)[0].normal_form for w in words ]
    return(words)

Лемматизация занимает много времени, поэтому ее можно убрать в целях более быстрых подсчетов.

# Преобразуем время
Xtrain['date'] = Xtrain['date'].apply(pd.to_datetime)
Xtrain['year'] = Xtrain['date'].apply(lambda x: x.year)
Xtrain['month'] = Xtrain['date'].apply(lambda x: x.month)

Будем обучаться на 2015 году, а валидироваться по первым 4 месяцам 2016, т.к. в нашей тестовой выборке представлены данные за первые 4 месяца 2017 года. Более правдивую валидацию можно сделать, идя по годам, увеличивая нашу обучающую выборку и смотря качество на первых четырех месяцах следующего года

Xtr = Xtrain[Xtrain['year']==2015]
Xval = Xtrain[(Xtrain['year']==2016)& (Xtrain['month']<=4)]
ytr = Xtr['favs_lognorm']
yval = Xval['favs_lognorm']
Xtr.shape,Xval.shape,ytr.mean(),yval.mean()

((23425, 15), (7556, 15), 3.4046228249071526, 3.304679829935242)

data = pd.concat([Xtr,Xval],axis = 0,ignore_index = True)

#у нас есть nan, поэтому преобразуем их к строке
data['content_clear'] = data['content'].apply(str)

%%time
data['content_clear'] = data['content_clear'].apply(review_to_wordlist)

model = word2vec.Word2Vec(data['content_clear'], size=300, window=10, workers=4)
w2v = dict(zip(model.wv.index2word, model.wv.syn0))

Посмотрим чему выучилась модель:

model.wv.most_similar(positive=['open', 'data','science','best'])

Результат

[(‘massive’, 0.6958945393562317),
(‘mining’, 0.6796239018440247),
(‘scientist’, 0.6742461919784546),
(‘visualization’, 0.6403135061264038),
(‘centers’, 0.6386666297912598),
(‘big’, 0.6237790584564209),
(‘engineering’, 0.6209672689437866),
(‘structures’, 0.609510600566864),
(‘knowledge’, 0.6094595193862915),
(‘scientists’, 0.6050446629524231)]

Модель обучилась достаточно неплохо, посмотрим на результаты алгоритмов:

data_mean = mean_vectorizer(w2v).fit(data['content_clear']).transform(data['content_clear'])
data_mean.shape

def split(train,y,ratio):
    idx = ratio
    return train[:idx, :], train[idx:, :], y[:idx], y[idx:]
y = data['favs_lognorm']
Xtr, Xval, ytr, yval = split(data_mean, y,23425)
Xtr.shape,Xval.shape,ytr.mean(),yval.mean()

((23425, 300), (7556, 300), 3.4046228249071526, 3.304679829935242)

from sklearn.linear_model import Ridge
from sklearn.metrics import mean_squared_error
model = Ridge(alpha = 1,random_state=7)
model.fit(Xtr, ytr)
train_preds = model.predict(Xtr)
valid_preds = model.predict(Xval)
ymed = np.ones(len(valid_preds))*ytr.median()
print('Ошибка на трейне',mean_squared_error(ytr, train_preds))
print('Ошибка на валидации',mean_squared_error(yval, valid_preds))
print('Ошибка на валидации предсказываем медиану',mean_squared_error(yval, ymed))

Ошибка на трейне 0.734248488422
Ошибка на валидации 0.665592676973
Ошибка на валидации предсказываем медиану 1.44601638512

data_mean_tfidf = tfidf_vectorizer(w2v).fit(data['content_clear']).transform(data['content_clear'])

y = data['favs_lognorm']
Xtr, Xval, ytr, yval = split(data_mean_tfidf, y,23425)
Xtr.shape,Xval.shape,ytr.mean(),yval.mean()

((23425, 300), (7556, 300), 3.4046228249071526, 3.304679829935242)

model = Ridge(alpha = 1,random_state=7)
model.fit(Xtr, ytr)
train_preds = model.predict(Xtr)
valid_preds = model.predict(Xval)
ymed = np.ones(len(valid_preds))*ytr.median()
print('Ошибка на трейне',mean_squared_error(ytr, train_preds))
print('Ошибка на валидации',mean_squared_error(yval, valid_preds))
print('Ошибка на валидации предсказываем медиану',mean_squared_error(yval, ymed))

Ошибка на трейне 0.743623730976
Ошибка на валидации 0.675584372744
Ошибка на валидации предсказываем медиану 1.44601638512

Попробуем нейронные сети.

# подключим библиотеки keras 
from keras.models import Sequential, Model
from keras.layers import Dense, Dropout, Activation, Input
from keras.preprocessing.text import Tokenizer
from keras import regularizers
from keras.wrappers.scikit_learn import KerasRegressor

# Опишем нашу сеть.
def baseline_model():
    model = Sequential()
    model.add(Dense(128, input_dim=Xtr.shape[1], kernel_initializer='normal', activation='relu'))
    model.add(Dropout(0.2))
    model.add(Dense(64, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(1, kernel_initializer='normal'))

    model.compile(loss='mean_squared_error', optimizer='adam')
    return model
estimator = KerasRegressor(build_fn=baseline_model,epochs=20, nb_epoch=20, batch_size=64,validation_data=(Xval, yval), verbose=2)

estimator.fit(Xtr, ytr)

Обучение

Train on 23425 samples, validate on 7556 samples
Epoch 1/20
1s — loss: 1.7292 — val_loss: 0.7336
Epoch 2/20
0s — loss: 1.2382 — val_loss: 0.6738
Epoch 3/20
0s — loss: 1.1379 — val_loss: 0.6916
Epoch 4/20
0s — loss: 1.0785 — val_loss: 0.6963
Epoch 5/20
0s — loss: 1.0362 — val_loss: 0.6256
Epoch 6/20
0s — loss: 0.9858 — val_loss: 0.6393
Epoch 7/20
0s — loss: 0.9508 — val_loss: 0.6424
Epoch 8/20
0s — loss: 0.9066 — val_loss: 0.6231
Epoch 9/20
0s — loss: 0.8819 — val_loss: 0.6207
Epoch 10/20
0s — loss: 0.8634 — val_loss: 0.5993
Epoch 11/20
1s — loss: 0.8401 — val_loss: 0.6093
Epoch 12/20
1s — loss: 0.8152 — val_loss: 0.6006
Epoch 13/20
0s — loss: 0.8005 — val_loss: 0.5931
Epoch 14/20
0s — loss: 0.7736 — val_loss: 0.6245
Epoch 15/20
0s — loss: 0.7599 — val_loss: 0.5978
Epoch 16/20
1s — loss: 0.7407 — val_loss: 0.6593
Epoch 17/20
1s — loss: 0.7339 — val_loss: 0.5906
Epoch 18/20
1s — loss: 0.7256 — val_loss: 0.5878
Epoch 19/20
1s — loss: 0.7117 — val_loss: 0.6123
Epoch 20/20
0s — loss: 0.7069 — val_loss: 0.5948

Получили более хороший результат по сравнению с гребневой регрессией.

Заключение

Word2Vec показал свою пользу на практических задачах анализа текстов, все-таки не зря на текущий момент на практике используется в основном именно он и — гораздо менее популярный — GloVe. Тем не менее, может быть в вашей конкретной задаче, вам пригодятся подходы, которым для эффективной работы не требуются такие объемы данных, как для word2vec.

Код ноутбуков с примерами можно взять здесь. Код практического применения — вот тут.

Пост написан совместно с demonzheg.

Литература

  1. Tomas Mikolov, Kai Chen, Greg Corrado, and Jeffrey Dean. Efficient estimation
    of word representations in vector space.
    CoRR, abs/1301.3781,
  2. Tomas Mikolov, Ilya Sutskever, Kai Chen, Gregory S. Corrado, and Jeffrey Dean. Distributed representations of words and phrases and their compositionality. In Advances in Neural Information Processing Systems 26: 27th Annual Conference on Neural Information Processing Systems 2013. Proceedings of a meeting held December 5-8, 2013, Lake Tahoe, Nevada, United States, pages 3111–3119, 2013.
  3. Morin, F., & Bengio, Y. Hierarchical Probabilistic Neural Network Language Model. Aistats, 5, 2005.
  4. Jeffrey Pennington, Richard Socher, and Christopher D. Manning. GloVe: Global Vectors for Word Representation. 2014.
  5. Piotr Bojanowski, Edouard Grave, Armand Joulin, and Tomas Mikolov. Enriching word vectors
    with subword information.
    arXiv preprint arXiv:1607.04606, 2016.

* Да, это специальная пасхалка для любителей творчества Энтони Бёрджеса.

GloVe: Global Vectors for Word Representation

Что такое перчатка?

Так какGloVe: Global Vectors for Word RepresentationС точки зрения статьи, полное название GloVe — «Глобальные векторы для представления в Word», основанное наГлобальная статистика частоты словИнструмент для представления слов (основанный на подсчете и общей статистике), который может выразить слово как вектор действительных чисел, эти векторы отражают некоторые семантические характеристики между словами, такие как сходство (подобие), аналогия (Аналогия) и тд. Мы можем вычислить семантическое сходство между двумя словами с помощью операций над векторами, такими как евклидово расстояние или косинусное сходство.

  • Цель модели — выполнить векторизованное представление слов так, чтобы как можно больше семантической и грамматической информации содержалось между векторами.

  • Вход: корпус

  • Вывод: слово вектор

  • Обзор метода: сначала строится матрица совпадений слов на основе корпуса, а затем изучается вектор слов на основе матрицы совпадений и модели GloVe.

Статистическая матрица совпадений

Пусть матрица совместного вхождения будет

X

X

Чьи элементы

X

i

,

j

X_{i,j}


X

i

,

j

X_{i,j}

Значение: количество раз, когда слово i и слово j появляются вместе в окне во всем корпусе.

например:
имеет корпус:

i love you but you love him i am sad

Этот небольшой корпус состоит только из одного предложения и состоит из 7 слов: я люблю тебя, но он, я и грустный.
Если мы используем статистическое окно с шириной окна 5 (длина левой и правой стороны равна 2), то у нас будет следующее содержимое окна:

Длина окон 0 и 1 меньше 5, потому что содержимое на левой стороне заголовка слова меньше 2, а длина окон 8 и 9 также меньше 5.
Возьмем окно 5 в качестве примера, чтобы проиллюстрировать, как построить матрицу совместного использования:
Центральное слово — любовь, а слова контекста — но, вы, он, я; затем выполните:

X

l

o

v

e

,

b

u

t

+

=

1

X_{love,but} +=1


X

l

o

v

e

,

y

o

u

+

=

1

X_{love,you} +=1


X

l

o

v

e

,

h

i

m

+

=

1

X_{love,him} +=1


X

l

o

v

e

,

i

+

=

1

X_{love,i} +=1

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

X

X

Как реализован GloVe?

Реализация GloVe разделена на следующие три этапа:

  • Построить матрицу совместного вхождения (корпус) в соответствии с корпусом

    X

    X

    (Что такое матрица совместного появления?),Каждый элемент в матрице

    X

    i

    j

    X_{ij}

    Репрезентативные слова и контекстные слова

    j

    j

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

    d

    d

    Предложена функция затухания (уменьшение веса):

    d

    e

    a

    c

    y

    =

    1

    /

    d

    deacy=1/d

    Используется для расчета веса, то естьЧем больше вес общего количества двух слов, которые находятся дальше, тем меньше

In all cases we use a decreasing weighting function, so that word pairs that are d words apart contribute 1/d to the total count.
  • Для построения приблизительной взаимосвязи между Word Vector и матрицей сопутствующих явлений автор статьи предлагает следующую формулу для аппроксимации взаимосвязи между ними:

w

i

T

w

~

j

+

b

i

+

b

j

=

l

o

g

(

X

i

j

)

w^T_{i}tilde{w}_j+b_i+b_j=log(X_{ij})

(1)

среди них,

w

i

T

w^T_{i}

с участием

w

j

~

tilde{w_j}

Является ли слово вектор, который мы наконец просим

b

i

b_{i}

с участием

b

j

~

tilde{b_j}

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

w

i

T

w^T_{i}

с участием

w

j

~

tilde{w_j}

? Мы представим их подробно ниже.

  • С Формулой 1 мы можем построить функцию потерь:

J

=

J=

i

,

j

=

1

V

sum_{i,j=1}^V

f

(

X

i

j

)

(

w

i

T

w

~

j

+

b

i

+

b

j

l

o

g

(

X

i

j

)

)

2

f(X_{ij})(w^T_{i}tilde{w}_j+b_i+b_j-log(X_{ij}))^2

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

f

(

X

i

j

)

f(X_{ij})

, Так какую роль играет эта функция, почему мы должны добавить эту функцию? Мы знаем, что в корпусе должно быть много слов, которые часто встречаются вместе (частые совпадения), тогда мы надеемся:

  • 1. Вес этих слов более важен, чем слова, которые редко появляются вместе (редкие совпадения), поэтому, если эта функция является неубывающей (неубывающей);
  • 2. Но мы также не хотим, чтобы этот вес был завышенным и не должен увеличиваться после достижения определенного уровня;
  • 3. Если два слова не появляются вместе, то есть

    X

    i

    j

    =

    0

    X_{ij}=0

    Тогда они не должны участвовать в расчете функции потерь, то есть

    f

    (

    x

    )

    f(x)

    Удовлетворить

    f

    (

    0

    )

    =

    0

    f(0)=0

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

f

(

x

)

=

{

(

x

/

x

m

a

x

)

α

,

if 

x

<

x

m

a

x

 

1

,

otherwise

f(x)=begin{cases} (x/x_{max})^α,&text{if $x<x_{max}$ }\1,&text{otherwise}end{cases}

Изображение функции показано ниже:

Во всех экспериментах в этой работе значение α составляет 0,75, и

x

m

a

x

x_{max}

Значение равно 100. Выше приведены детали реализации GloVe, так как же тренируется GloVe?

Как тренируется GloVe?

Хотя многие люди утверждают, что GloVe является методом обучения без надзора (поскольку он не требует ручной маркировки), у него все еще есть метка. Эта метка является журналом (в формуле 2).

X

i

j

X_{ij}

) и вектор в уравнении 2

w

w

с участием

w

~

tilde{w}

Он заключается в постоянном обновлении / изучении параметров, поэтому, по сути, его метод обучения ничем не отличается от контролируемого метода обучения, основанного на градиентном спуске. В частности, эксперимент в этой статье делается следующим образом:Используется алгоритм градиентного спуска Адаграда

X

X

Все ненулевые элементы в случайной выборке, кривизна обучения установлена ​​на 0,05 и повторяется 50 раз, когда размер вектора меньше 300, и повторяется 100 раз на векторах других размеров до сходимости, Окончательное изучение состоит в том, что два вектора

w

w

с участием

w

~

tilde{w}

,так как

X

X

Симметрично, так что в принципе

w

w

с участием

w

~

tilde{w}

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

w

w

+

w

~

tilde{w}

Как конечный вектор (различная инициализация этих двух эквивалентна добавлению разных случайных шумов, поэтому это может улучшить устойчивость), После обучения корпуса из 40 миллиардов токенов полученные экспериментальные результаты показаны ниже:

На этом графике используются три показателя: семантическая точность, грамматическая точность и общая точность. Тогда нетрудно обнаружить, что Vector Dimension может достичь наилучшего результата при 300, а размер контекстного окна составляет примерно от 6 до 10.

Сравнение Glove и LSA, word2vec

LSA (скрытый семантический анализ) — это более ранний инструмент представления вектора слов на основе подсчета, который также основан на матрице сопутствующих явлений, но использует технологию разложения матриц, основанную на разложении по сингулярным значениям (SVD), для уменьшения больших матриц. Размерность, и мы знаем, что сложность SVD очень высока, поэтому его вычислительные затраты относительно высоки. Другое дело, что его статистический вес для всех слов постоянен. Эти недостатки преодолеваются один за другим в GloVe. Самым большим недостатком word2vec является то, что он не в полной мере использует весь корпус, поэтому GloVe фактически сочетает в себе преимущества обоих. Судя по экспериментальным результатам, приведенным в этой статье, производительность GloVe намного превосходит LSA и word2vec, но некоторые люди в Интернете также говорят, что фактическая производительность GloVe и word2vec на самом деле похожа.

Формула Деривация

На этом содержание GloVe в основном закончено. Единственное сомнение в том, как появилась Формула 1? Если вы заинтересованы, вы можете продолжить чтение, если нет, вы можете закрыть окно браузера. Чтобы прояснить эту проблему, мы сначала определим некоторые переменные:

  • X

    i

    j

    X_{ij}

    слово

    j

    j

    Появляются в словах

    i

    i

    Времена в контексте

  • X

    i

    X_{i}

    слово

    i

    i

    Общее количество вхождений всех слов в контексте, т.е.

    X

    i

    =

    k

    X

    i

    k

    X_{i}=sum^kX_{ik}

    ;

  • P

    i

    j

    =

    P

    (

    j

    i

    )

    =

    X

    i

    j

    /

    X

    i

    P_{ij}=P(j|i)=X_{ij}/X_{i}

    слово

    j

    j

    Появляются в словах

    i

    i

    Вероятность в контексте

С этими определениями, давайте посмотрим на таблицу:

Ключом к пониманию этой таблицы является последняя строка, которая представляет отношение двух вероятностей (отношение),Мы можем использовать это, чтобы соблюдать два слова

i

i

с участием

j

j

По отношению к словам

k

k

Что более актуально (уместно), Например, лед и твердое тело более связаны, но поток и твердое тело явно не связаны, поэтому мы найдем

P

(

s

o

l

i

d

i

c

e

)

/

P

(

s

o

l

i

d

s

t

e

a

m

)

P(solid|ice)/P(solid|steam)

Гораздо больше, чем 1. Тот же газ и пар более связаны, но не связаны со льдом, тогда

P

(

s

o

l

i

d

i

c

e

)

/

P

(

s

o

l

i

d

s

t

e

a

m

)

P(solid|ice)/P(solid|steam)

Это намного меньше 1, когда все это связано (например, вода) или вообще отсутствует, соотношение между ними близко к 1, это очень интуитивно понятно. следовательно,Приведенный выше вывод может объяснить, что это может быть более подходящий метод для изучения векторов слов по отношению вероятности, а не самой вероятности.Таким образом, все содержание ниже находится вокруг этой точки.

Таким образом, чтобы отразить коэффициент вероятности, упомянутый выше, мы можем построить следующую функцию:

F

(

w

i

,

w

j

,

w

~

k

)

=

P

i

k

/

P

j

k

F(w_{i},w_j,tilde{w}_k)=P_{ik}/P_{jk}

Среди них функция

F

F

Параметры и конкретная форма не определены, он имеет три параметра

w

i

,

w

j

с участием

w

~

k

w_ {i}, w_j и tilde {w} _k

w

с участием

w

~

w и tilde {w}

Есть разные векторы;
Поскольку векторное пространство является линейным, самый простой способ выразить пропорциональную разницу между двумя вероятностями — это сделать разницу, поэтому мы получаем:

F

(

w

i

w

j

,

w

~

k

)

=

P

i

k

P

j

k

F(w_{i}-w_j,tilde{w}_k)=frac{P_{ik}}{P_{jk}}

В это время мы обнаружили, что правая часть формулы 5 является величиной, а левая сторона — вектором, поэтому мы преобразуем левую сторону во внутреннюю форму произведения двух векторов:

F

(

(

w

i

w

j

)

T

w

~

k

)

=

P

i

k

P

j

k

F((w_{i}-w_j)^Ttilde{w}_k)=frac{P_{ik}}{P_{jk}}

(6)

мы знаем

X

X

Это симметричная матрица, и слова и контекстные слова на самом деле являются относительными, то есть если мы обмениваемся следующим образом:

w

w

leftrightarrow

w

~

k

tilde{w}_k

X

X

leftrightarrow

X

T

X^T

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

F

F

Чтобы удовлетворить гомоморфизм:

F

(

(

w

i

w

j

)

T

w

~

k

)

=

F

(

w

i

T

w

~

k

)

F

(

w

j

T

w

~

k

)

F((w_{i}-w_j)^Ttilde{w}_k)=frac{F(w^T_{i}tilde{w}_k)}{F(w^T_{j}tilde{w}_k)}

В сочетании с формулой 6 мы можем получить:

F

(

w

i

T

w

~

k

)

=

P

i

k

=

X

i

k

X

i

F(w^T_{i}tilde{w}_k)=P_{ik}=frac{X_{ik}}{X_i}

Тогда пусть F = exp, поэтому мы имеем:

w

i

T

w

~

k

=

l

o

g

(

P

i

k

)

=

l

o

g

(

X

i

k

)

l

o

g

(

X

i

)

w^T_{i}tilde{w}_k=log(P_{ik})=log(X_{ik})-log(X_i)

(9)

Потому что правая сторона знака равенства

l

o

g

(

X

i

)

log(X_i)

Существование формулы 9 не удовлетворяет симметрии, и это

l

o

g

(

X

i

)

log(X_i)

Фактически

k

k

Независимо, это только следует

i

i

Связанные, поэтому мы можем целиться

w

i

w_i

Добавить термин смещения

b

i

b_i

Замените его, поэтому мы имеем:

w

i

T

w

~

k

+

b

i

=

l

o

g

(

X

i

k

)

w^T_{i}tilde{w}_k+b_i=log(X_{ik})

(10)

Но формула 10 все еще не удовлетворяет симметрии, поэтому мы стремимся к

w

k

w_k

Добавить термин смещения

b

k

b_k

Чтобы получить формула формулы 1:

w

i

T

w

~

k

+

b

i

+

b

k

=

l

o

g

(

X

i

k

)

w^T_{i}tilde{w}_k+b_i+b_k=log(X_{ik})

(1)

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

ссылка:

1. Тезис:GloVe: Global Vectors for Word Representation。
2.GloVe объяснил
3.Понимать модель GloVe (Глобальные векторы для представления слов)

The overall structure of the paper:

1. Summary

Mainly propose a new word vector learning method glove, which uses global statistical information and local context information to learn

1. The current word vector learning model can capture the grammatical and semantic rules through the arithmetic calculation of the vector, but the rules behind it are still unexplainable

2. After careful analysis, I found a feature that contributes to this washing vector law, and proposed a new logarithmic bilinear regression model based on words, which uses matrix decomposition and the advantages of local context to learn Word vector

3. The model achieves efficient training by training only in the non-zero position of the co-occurrence matrix

4. The model has 75% accuracy on word pair reasoning tasks and obtains optimal results on multiple tasks

Two, Introduction

Explain the previous related methods-matrix decomposition and word2vec have their own advantages

Matrix Factorization Methods (Matrix Factorization Methods)

Word co-occurrence matrix-window window=1

           1.  I enjoy flying

           2.  I like NLP.

           3. I like deep learning.

           I   like  enjoy  deep  learning  NLP  flying   .

        I    0    2    1      0      0      0      0      0 

    like   2    0    0      1      0      1      0      0

  enjoy  1    0    0      0      0      0      1      0 

   deep  0    1    0      0      1      0      0      0 

learning  0    0    0     1      0      0      0      1

    NLP   0    1    0     0      0      0      0      1 

    flying  0    0    1     0      0      0      0      1

       .      0    0     0     0     1      1      1      0

Vocabulary size |V|, x.size = |v| * |v|

Disadvantages: The inference effect of words is relatively poor, and no semantic information is learned

Context-based vector learning methods (shallow window-based methods)

Disadvantages of word2vec: Only use context information in the window, not global statistics

3. Related work

Introduce matrix decomposition and word2vec related methods

Four, glove model

Introduce the derivation process of glove, connect glove with other models, and analyze the complexity

Note: i, j are equivalent to the index of the co-occurrence matrix, wi, wj represent the word vector, this parameter is learned, bi, bj are equivalent to the offset of i and j, and the target is the number of occurrences at (i, j) The logarithm of is equivalent to a regression problem. The equation is constructed. The loss function is equivalent to the difference between. The process of model training is the process of loss reduction.

Five, experimental analysis

Result of model experiment and hyperparameter analysis

Result of word pair inference experiment

Result of named entity recognition experiment

Super parameter analysis of vector length and window size

Analysis of corpus size over parameters

Word2vec comparison experiment

Six, summary

key point:

1. Word vector learning method of matrix decomposition

2. Context-based word vector learning method

3. Pre-training word vector

Innovation:

1. Propose a new word vector training model-Glove

2. Achieve the best results on multiple tasks

Announced a series of pre-trained word vectors

Inspiration points:

1. Relative to the original probability, the ratio of the probability is more able to distinguish between related words and unrelated words and can distinguish two related words

2. Propose a new log bilinear regression model, which combines the advantages of global matrix factorization and local context

Seven, code implementation

# ****** Data Processing Section ******

# encoding = 'utf-8'

from torch.utils import data
import os
import numpy as np
import pickle

 min_count = 50 # Set the minimum word frequency

data = open("./data/text8.txt").read()
data = data.split()


 # Construct word frequency to remove low frequency words

word2freq = {}

for word in data:
    if word2freq.get(word)!=None:
        word2freq[word] += 1
    else:
        word2freq[word] = 1
        
word2id = {}
for word in word2freq:
    if word2freq[word] < min_count:
        continue
    else:
        if word2id.get(word) == None:
            word2id[word] = len(word2id)

 # Build a co-occurrence matrix

vocab_size = len(word2id)
comat = np.zeros((vocab_size,vocab_size))

 window_size = 2 # Set the size of the sliding window

for i in range(len(data)):
    if i%1000000==0:
        print(i,len(data))
    
    if word2id.get(data[i]) == None:
        continue
    
    w_index = word2id[data[i]]
    
    for j in range(max(0,i-window_size),min(len(data),i+window_size+1)):
        if word2id.get(data[j]) == None or i==j:
            continue
        u_index = word2id[data[j]]
        comat[w_index][u_index] += 1

 coocs = np.transpose(np.nonzero(comat)) # Extract non-zero data

 # Generate training set

labels = []

for i in range(len(coocs)):
    if i%1000000==0:
        print(i,len(coocs))
        
    labels.append(comat[coocs[i][0]][coocs[i][1]])

labels = np.array(labels)


np.save("./data/data.npy",coocs)
np.save('./data/label.npy',labels)
pickle.dump(word2id,open("./data/word2id",'wb'))

# ***** Model building part *****


import torch
import torch.nn as nn


class glove_model(nn.Module):
    
    def __init__(self, vocab_size, embed_size, x_max, alpha):
        
        super(glove_model, self).__init__()
        self.vocab_size = vocab_size
        self.embed_size = embed_size
        self.x_max = x_max
        self.alpha = alpha
                 self.w_embed = nn.Embedding(self.vocab_size,self.embed_size).type(torch.float64) # central word vector
                 self.w_bias = nn.Embedding(self.vocab_size,1).type(torch.float64) # central word bias
        
                 self.v_embed = nn.Embedding(self.vocab_size,self.embed_size).type(torch.float64) # surrounding word vector
                 self.v_bias = nn.Embedding(self.vocab_size,1).type(torch.float64) # surrounding word bias
        
    def forward(self, w_data, v_data, labels):
        
        w_data_embed = self.w_embed(w_data)
        w_data_bias = self.w_bias(w_data)
        v_data_embed = self.v_embed(v_data)
        v_data_bias = self.v_bias(v_data)
        
                 weights = torch.pow(labels/self.x_max,self.alpha) # weight generation
        weights[weights>1] = 1
        
        loss = torch.mean(weights*torch.pow(torch.sum(w_data_embed * v_data_embed,1) + w_data_bias + v_data_bias - torch.log(labels),2))
        
        return loss
    
    
    def save_embedding(self, word2id, file_name):
        
        embedding_1 = self.w_embed.weight.data.cpu().numpy()
        embedding_2 = self.v_embed.weight.data.cpu().numpy()
        embedding =  (embedding_1 + embedding_2)/2
        
        fout  = open(file_name, 'w')
        fout.write("%d %dn" %(len(word2id),self.embed_size))
        for w,wid in word2id.items():
            e = embedding[wid]
            w = ' '.join(map(lambda x:str(x), e))
            fout.write('%s %sn' %(w,e))


model = glove_model(100,100,100,0.75)
word2id = dict()

for i in range(100):
    word2id[str(i)] = i
    
w_data = torch.Tensor([0,0,1,1,1]).long()
v_data = torch.Tensor([1,2,0,2,3]).long()
labels = torch.Tensor([1,2,3,4,5])
model.forward(w_data,v_data,labels)

embedding_1 = model.w_embed.weight.data.cpu().numpy()




# ***** Model training part *****

from data import Wiki_Dataset
from model import glove_model
import torch
import numpy as np
import torch.optim as optim
from tqdm import tqdm
import config as argumentparser

config = argumentparser.ArgumentParser()


if config.cuda and torch.cuda.is_available():
    torch.cuda.set_device(config.gpu)

torch.cuda.is_available()

wiki_dataset = Wiki_Dataset(min_count=config.min_count,window_size = config.window_size)

training_iter = torch.utils.data.DataLoader(dataset=wiki_dataset,batch_size=config.batch_size,shuffle=True,num_workers=2)


model = glove_model(len(wiki_dataset.word2id),config.embed_size,config.x_max,config.alpha)


if config.cuda and torch.cuda.is_available():
    torch.cuda.set_decvice(config.gpu)
    model.cuda()
    
optimizer = optim.Adam(model.parameters(),lr=config.learning_rate)


loss = -1

for epoch in range(config.epoch):
    process_bar = tqdm(training_iter)
    print(process_bar)
    for data,label in process_bar:
        w_data = torch.Tensor(np.array([sample[0] for sample in data])).long()
        v_data = torch.Tensor(np.array([sample[1] for sample in data])).long()
        
        if config.cuda and torch.cuda.is_available():
            w_data = w_data.cuda()
            v_data = v_data.cuda()
            label = label.cuda()
            
        loss_now = model(w_data,v_data,label)
        
        if loss == -1:
            loss = loss_now.data.item()
        else:
            loss = 0.95*loss + 0.05 *loss_now.data.item()
            
        process_bar.set_postfix(loss=loss)
        process_bar.update()
        optimizer.zero_grad()
        loss_now.backward()
        optimizer.step()
        
model.save_embedding()

See the specific code:https://github.com/wangtao666666/NLP/tree/master/Glove

Понравилась статья? Поделить с друзьями:
  • Global replace in word
  • Glad you came word
  • Give the missing part of the word 5 класс
  • Give the meaning of word processing
  • Give the meaning of the word stress