embedding слой что это
Делаем прогноз слов рекуррентной сетью Embedding слой
На предыдущем занятии мы с вами построили экспериментальную рекуррентную НС для прогнозирования следующего символа. На этом занятии разовьем эту тему и построим сеть для оценки следующего слова. В целом, сеть будет реализована также как и ранее, а вот подготовка обучающей выборки будет выполняться несколько иначе. В самом простом варианте, нам следует сформировать трехмерный тензор (похожий на тензор из предыдущего занятия):
Из текста будем выделять слова целиком (а не отдельные символы, как ранее). Набор уникальных слов будут составлять наш словарь. Размер этого словаря обозначим через переменную
Каждое слово, затем, будет кодироваться one-hot вектором в соответствии с его номером в словаре:
Второй важный параметр – число слов, на основе которых строится прогноз, который определяется переменной:
Давайте теперь посмотрим, как можно сформировать такой тензор. Вначале загрузим тексты с отрицательными высказываниями из файла text:
Теперь нам нужно разбить эти высказывания на слова. Для этого воспользуемся уже знакомым из прошлого занятия инструментом Tokenizer и положим, что максимальное число слов будет равно 1000:
По идее, мы здесь могли бы и не задавать максимальное число слов, тогда эта величина была бы определена автоматически при парсинге текста. Но данный параметр имеет один существенный плюс: из всех найденных слов мы оставляем 999 наиболее часто встречаемых (то есть maxWordsCount-1), то есть, мы имеем возможность отбросить редкие слова, которые особо не нужны при обучении НС.
Конечно, в данном случае, останутся все найденные слова, т.к. их общее число меньше 1000. Вообще, этот параметр устанавливается с позиции «здравого смысла». Например, при большой обучающей выборке, скорее всего, мы будем иметь дело с большинством слов (и их форм) русского языка. Какой лексический запас слов у среднестатистического человека? Около 10 000. Значит, для большой выборки можно указать значение
и это будет хорошим выбором.
Итак, мы разбили текст на слова и для примера выведем их начальный список:
Здесь отображаются кортежи со словом и его частотой встречаемости в тексте.
Далее, мы преобразуем текст в последовательность чисел в соответствии с полученным словарем. Для этого используется специальный метод класса Tokenizer:
На выходе получим массив чисел объекта numpy:
Осталось закодировать числа массива data в one-hot векторы. Для этого мы воспользуемся методом to_categorical пакета Keras:
На выходе получим двумерную матрицу, состоящую из One-hot векторов:
Затем, из этой матрицы сформируем тензор обучающей выборки и соответствующий набор выходных значений. Для начала вычислим размер обучающего множества:
И, далее, сформируем входной тензор и прогнозные значения также, как мы это делали с символами:
Все, у нас есть обучающая выборка и требуемые выходные значения. Осталось создать модель рекуррентной сети. Мы ее возьмем из предыдущего занятия с числом нейронов скрытого слоя 128 и maxWordsCount нейронами на выходе с функцией активации softmax:
Готово. Запускаем процесс обучения:
И давайте теперь посмотрим, что у нас получилось. Запишем функцию для формирования текста из спрогнозированных слов:
И вызовем ее с тремя первыми словами:
Получим вот такой результат:
позитив добавляет годы счастье вашей жизни и двигаться их в вы держись в и мечты успеха свою жизни не меня за не в
Конечно, немного сумбурно, но в целом, что-то в этом есть. Такой результат еще связан с очень маленькой обучающей выборкой. По идее, здесь нужно взять какую-нибудь большую книгу и прогнать ее через сеть. Но цель этого занятия показать общий принцип использования рекуррентных сетей для прогнозирования слов в последовательности.
Embedding-слой
Однако в такой реализации есть один существенный недостаток: входной тензор, что мы получили, занимает в памяти очень много места. Представьте, если решается реальная задача с числом слов 20 000. Тогда тензора будет содержать:
элементов
и требовать значительный объем памяти. Поэтому специалисты по нейронным сетям предложили альтернативный подход – использование специального входного слоя, который получил название:
В чем его суть? Смотрите, когда мы подаем вектор с единицей на определенной позиции, то у нас, фактически, используются только связи для этого одного входа, остальные умножаются на 0 и формируют нулевые суммы на всех остальных нейронах скрытого слоя:
И отсюда хорошо видно, что если передавать на вход не такие расширенные векторы, а последовательность с порядковыми номерами слов в словаре:
То на входе НС можно реализовать простой алгоритм, который бы подавал 1 на нейрон с соответствующим номером этого слова, а остальные суммы приравнивались бы нулю. В итоге, мы существенно экономим память при хранении обучающей выборки, а результат получаем тот же самый. Именно такую операцию и выполняет Embedding слой. На выходах его слоя формируются выходные значения, равные весам связей для переданной 1:
Далее, эти значения весов подаются уже на следующий слой нейронной сети.
В Keras такой слой можно создать с помощью одноименного класса:
Есть и другие параметры, подробно о них можно почитать на странице документации по ссылке:
Этот слой можно создавать только как входной и в нашем случае мы его определим так:
Здесь 256 – это число выходов в Embedding-слое. В качестве входной обучающей выборки мы теперь можем использовать одномерный массив:
А выходные значения остаются прежними – двумерным массивом из One-hot векторов, так как у нас на выходе 1000 нейронов.
Далее, абсолютно также проводим обучение и немного модифицируем функцию buildPhrase:
И запускаем процесс прогнозирования слов. Как видите, использование Embedding слоя значительно упрощает и саму программу и размер используемой памяти.
Видео по теме
Нейронные сети: краткая история триумфа
Структура и принцип работы полносвязных нейронных сетей | #1 нейросети на Python
Ускорение обучения, начальные веса, стандартизация, подготовка выборки | #4 нейросети на Python
Функции активации, критерии качества работы НС | #6 нейросети на Python
Как нейронная сеть распознает цифры | #9 нейросети на Python
Оптимизаторы в Keras, формирование выборки валидации | #10 нейросети на Python
Batch Normalization (батч-нормализация) что это такое? | #12 нейросети на Python
Как работают сверточные нейронные сети | #13 нейросети на Python
Делаем сверточную нейронную сеть в Keras | #14 нейросети на Python
Примеры архитектур сверточных сетей VGG-16 и VGG-19 | #15 нейросети на Python
Теория стилизации изображений (Neural Style Transfer) | #16 нейросети на Python
Делаем перенос стилей изображений с помощью Keras и Tensorflow | #17 нейросети на Python
Как нейронная сеть раскрашивает изображения | #18 нейросети на Python
Введение в рекуррентные нейронные сети | #19 нейросети на Python
Как рекуррентная нейронная сеть прогнозирует символы | #20 нейросети на Python
Делаем прогноз слов рекуррентной сетью Embedding слой | #21 нейросети на Python
Как работают RNN. Глубокие рекуррентные нейросети | #22 нейросети на Python
Как делать сентимент-анализ рекуррентной LSTM сетью | #24 нейросети на Python
Рекуррентные блоки GRU. Пример их реализации в задаче сентимент-анализа | #25 нейросети на Python
Двунаправленные (bidirectional) рекуррентные нейронные сети | #26 нейросети на Python
Автоэнкодеры. Что это и как работают | #27 нейросети на Python
Вариационные автоэнкодеры (VAE). Что это такое? | #28 нейросети на Python
Делаем вариационный автоэнкодер (VAE) в Keras | #29 нейросети на Python
Расширенный вариационный автоэнкодер (CVAE) | #30 нейросети на Python
Что такое генеративно-состязательные сети (GAN) | #31 нейросети на Python
Делаем генеративно-состязательную сеть в Keras и Tensorflow | #32 нейросети на Python
© 2021 Частичное или полное копирование информации с данного сайта для распространения на других ресурсах, в том числе и бумажных, строго запрещено. Все тексты и изображения являются собственностью сайта
ML: Слой Embedding в Keras
Введение
В нейронных сетях существует специальный тип слоя Embedding, который на вход получает номера слов, а на выходе выдаёт их векторные представления (до начала обучения они случайные):
Выше VEC_DIM = 2 и у слоя три входа ( inputs = 3). У первого слова номер 0, у второго 2, а у третьего 1. Слой Embedding хранит матрицу формы (DIC_SIZE, VEC_DIM), из которой, при подаче на вход числа i, выдаёт i-ю строку.
Сопровождающий файл: NN_Embedding_Layer.ipynb. Общую теорию векторизации слов можно найти в этом документе. Слой Embedding в библиотеке PyTorch описан здесь.
Embedding в Keras
Форма входного и выходного тензоров слоя Embedding выглядят следующим образом: Слой всегда идёт первым, т.к. на его входе находится тензор с целыми числами: [0. VOC_SIZE-1].
Число входов, как и размер батча, можно не указывать (они автоматически определяться по входному тензору):
Например, ниже batch_size=1 и inputs=1, 2: Анлогично с batch_size=2 (список списков необходимо явно преобразовать в numpy-тензор!):
Матрица векторов
Можно загрузить готовую матрицу компонент векторов (например, обученную на другой задаче). Если необходимо, чтобы она далее не изменялась, надо указать trainable=False:
Регуляризация и ограничения
Компоненты векторов слоя Embedding являются обучаемыми параметрами. Для них (как и для любых параметров) можно установить ограничения значений и регуляризационные довески к ошибке.
Параметр embeddings_constraint ( None по умолчанию) задаёт ограничения. Например: будет контролировать, чтобы векторы были единичными.
Параметр embeddings_regularizer ( None по умолчанию) делает ограничение на компоненты векторов более мягкими. Для этого к функции ошибки добавляется, например, сумма квадратов компонент, умноженная на небольшую константу (ниже 0.01). Градиентный метод будет одновременно пытаться уменьшить ошибку предсказания модели и величину компонент, тем самым не давая им неконтролировано увеличиваться:
Взаимодействие с Dense и RNN
Слой Flatten параметров не имеет. Его задача сделать входящий тензор данных линейным. При этом он не затрагивает нулевую ось батча, т.е. при действии Flatten() на тензор (batch_size, size1. sizeN) получается тензор (batch_size, size1*. *sizeN):
Если снижение размерности Flatten не сделать, то модель:
свернёт выходы Embedding слоя и веса слоя Dense c units нейронами следующим образом:
Число параметров в слое Dense теперь будет равно VEC_DIM+1. Это означает, что к каждому вектору присоединяется слой с одними и теми же весами. Ниже эта архитектура нарисована в центре:
В отличии от слоя Dense, рекуррентные слои ожидают на своих входах векторы, поэтому Embedding к ним присоединяется непосредственно (выше третий рисунок): Рекуррентная сеть ( LSTM) по умолчанию имеет return_sequences=False, поэтому выше возвращается скрытое состояние (одномерное) только последней (третей) ячейки.
Маскирование входов
Рекомендуется первое слово в словаре (нулевой индекс) резервировать и не занимать значащим словом. Тогда нулевой индекс можно будет использовать как признак отсутствия входа. Это полезно при переменном числе входов, например в RNN. Для использования маскирования в Embedding надо указать mask_zero=True. Слой по-прежнему будет выдавать векторы по числу входов. Однако последующий RNN слой, вектор с нулевым индексом будет игнорировать, переходя к следующий ячейке:
Обычно маскированные входы с нулями идут в конце последовательности, «добивая» короткие предложения до максимальной длины нулями.
Маскирование учитывается также при вычислении ошибки, игнорируя ошибку от маскированных входов.
Чудесный мир Word Embeddings: какие они бывают и зачем нужны?
Начать стоит от печки, то есть с постановки задачи. Откуда берется сама задача word embedding?
Лирическое отступление: К сожалению, русскоязычное сообщество еще не выработало единого термина для этого понятия, поэтому мы будем использовать англоязычный.
Сам по себе embedding — это сопоставление произвольной сущности (например, узла в графе или кусочка картинки) некоторому вектору.
Сегодня мы говорим про слова и стоит обсудить, как делать такое сопоставление вектора слову.
Вернемся к предмету: вот у нас есть слова и есть компьютер, который должен с этими словами как-то работать. Вопрос — как компьютер будет работать со словами? Ведь компьютер не умеет читать, и вообще устроен сильно иначе, чем человек. Самая первая идея, приходящая в голову — просто закодировать слова цифрами по порядку следования в словаре. Идея очень продуктивна в своей простоте — натуральный ряд бесконечен и можно перенумеровать все слова, не опасаясь проблем. (На секунду забудем про ограничения типов, тем более, в 64-битное слово можно запихнуть числа от 0 до 2^64 — 1, что существенно больше количества всех слов всех известных языков.)
Но у этой идеи есть и существенный недостаток: слова в словаре следуют в алфавитном порядке, и при добавлении слова нужно перенумеровывать заново большую часть слов. Но даже это не является настолько важным, а важно то, буквенное написание слова никак не связано с его смыслом (эту гипотезу еще в конце XIX века высказал известный лингвист Фердинанд де Соссюр). В самом деле слова “петух”, “курица” и “цыпленок” имеют очень мало общего между собой и стоят в словаре далеко друг от друга, хотя очевидно обозначают самца, самку и детеныша одного вида птицы. То есть мы можем выделить два вида близости слов: лексический и семантический. Как мы видим на примере с курицей, эти близости не обязательно совпадают. Можно для наглядности привести обратный пример лексически близких, но семантически далеких слов — «зола» и «золото». (Если вы никогда не задумывались, то имя Золушка происходит именно от первого.)
Чтобы получить возможность представить семантическую близость, было предложено использовать embedding, то есть сопоставить слову некий вектор, отображающий его значение в “пространстве смыслов”.
Какой самый простой способ получить вектор из слова? Кажется, что естественно будет взять вектор длины нашего словаря и поставить только одну единицу в позиции, соответствующей номеру слова в словаре. Этот подход называется one-hot encoding (OHE). OHE все еще не обладает свойствами семантической близости:
Значит нам нужно найти другой способ преобразования слов в вектора, но OHE нам еще пригодится.
Отойдем немного назад — значение одного слова нам может быть и не так важно, т.к. речь (и устная, и письменная) состоит из наборов слов, которые мы называем текстами. Так что если мы захотим как-то представить тексты, то мы возьмем OHE-вектор каждого слова в тексте и сложим вместе. Т.е. на выходе получим просто подсчет количества различных слов в тексте в одном векторе. Такой подход называется “мешок слов” (bag of words, BoW), потому что мы теряем всю информацию о взаимном расположении слов внутри текста.
Но несмотря на потерю этой информации так тексты уже можно сравнивать. Например, с помощью косинусной меры.
Мы можем пойти дальше и представить наш корпус (набор текстов) в виде матрицы “слово-документ” (term-document). Стоит отметить, что в области информационного поиска (information retrieval) эта матрица носит название «обратного индекса» (inverted index), в том смысле, что обычный/прямой индекс выглядит как «документ-слово» и очень неудобен для быстрого поиска. Но это опять же выходит за рамки нашей статьи.
Эта матрица приводит нас к тематическим моделям, где матрицу “слово-документ” пытаются представить в виде произведения двух матриц “слово-тема” и “тема-документ”. В самом простом случае мы возьмем матрицу и с помощью SVD-разложения получим представление слов через темы и документов через темы:
Здесь — слова,
— документы. Но это уже будет предметом другой статьи, а сейчас мы вернемся к нашей главной теме — векторному представлению слов.
Пусть у нас есть такой корпус:
С помощью SVD-преобразования, выделим только первые две компоненты, и нарисуем:
Что интересного на этой картинке? То, что Титан и Ио — далеко друг от друга, хотя они оба являются спутниками Сатурна, но в нашем корпусе про это ничего нет. Слова «атмосфера» и «Сатурн» очень близко друг другу, хотя не являются синонимами. В то же время «два» и «много» стоят рядом, что логично. Но общий смысл этого примера в том, что результаты, которые вы получите очень сильно зависят от корпуса, с которым вы работаете. Весь код для получения картинки выше можно посмотреть здесь.
Логика повествования выводит на следующую модификацию матрицы term-document — формулу TF-IDF. Эта аббревиатура означает «term frequency — inverse document frequency».
Давайте попробуем разобраться, что это такое. Итак, TF — это частота слова в тексте
, здесь нет ничего сложного. А вот IDF — существенно более интересная вещь: это логарифм обратной частоты распространенности слова
в корпусе
. Распространенностью называется отношение числа текстов, в которых встретилось искомое слово, к общему числу текстов в корпусе. С помощью TF-IDF тексты также можно сравнивать, и делать это можно с меньшей опаской, чем при использовании обычных частот.
Новая эпоха
Описанные выше подходы были (и остаются) хороши для времен (или областей), где количество текстов мало и словарь ограничен, хотя, как мы видели, там тоже есть свои сложности. Но с приходом в нашу жизнь интернета все стало одновременно и сложнее и проще: в доступе появилось великое множество текстов, и эти тексты с изменяющимся и расширяющимся словарем. С этим надо было что-то делать, а ранее известные модели не могли справиться с таким объемом текстов. Количество слов в английском языке очень грубо составляет миллион — матрица совместных встречаемостей только пар слов будет 10^6 x 10^6. Такая матрица даже сейчас не очень лезет в память компьютеров, а, скажем, 10 лет назад про такое можно было не мечтать. Конечно, были придуманы множество способов, упрощающих или распараллеливающих обработку таких матриц, но все это были паллиативные методы.
И тогда, как это часто бывает, был предложен выход по принципу “тот, кто нам мешает, тот нам поможет!” А именно, в 2013 году тогда мало кому известный чешский аспирант Томаш Миколов предложил свой подход к word embedding, который он назвал word2vec. Его подход основан на другой важной гипотезе, которую в науке принято называть гипотезой локальности — “слова, которые встречаются в одинаковых окружениях, имеют близкие значения”. Близость в данном случае понимается очень широко, как то, что рядом могут стоять только сочетающиеся слова. Например, для нас привычно словосочетание «заводной будильник». А сказать “заводной апельсин” мы не можем* — эти слова не сочетаются.
Основываясь на этой гипотезе Томаш Миколов предложил новый подход, который не страдал от больших объемов информации, а наоборот выигрывал [1].
Модель, предложенная Миколовым очень проста (и потому так хороша) — мы будем предсказывать вероятность слова по его окружению (контексту). То есть мы будем учить такие вектора слов, чтобы вероятность, присваиваемая моделью слову была близка к вероятности встретить это слово в этом окружении в реальном тексте.
Здесь — вектор целевого слова,
— это некоторый вектор контекста, вычисленный (например, путем усреднения) из векторов окружающих нужное слово других слов. А
— это функция, которая двум векторам сопоставляет одно число. Например, это может быть упоминавшееся выше косинусное расстояние.
Приведенная формула называется softmax, то есть “мягкий максимум”, мягкий — в смысле дифференцируемый. Это нужно для того, чтобы наша модель могла обучиться с помощью backpropagation, то есть процесса обратного распространения ошибки.
Процесс тренировки устроен следующим образом: мы берем последовательно (2k+1) слов, слово в центре является тем словом, которое должно быть предсказано. А окружающие слова являются контекстом длины по k с каждой стороны. Каждому слову в нашей модели сопоставлен уникальный вектор, который мы меняем в процессе обучения нашей модели.
В целом, этот подход называется CBOW — continuous bag of words, continuous потому, что мы скармливаем нашей модели последовательно наборы слов из текста, a BoW потому что порядок слов в контексте не важен.
Также Миколовым сразу был предложен другой подход — прямо противоположный CBOW, который он назвал skip-gram, то есть “словосочетание с пропуском”. Мы пытаемся из данного нам слова угадать его контекст (точнее вектор контекста). В остальном модель не претерпевает изменений.
Что стоит отметить: хотя в модель не заложено явно никакой семантики, а только статистические свойства корпусов текстов, оказывается, что натренированная модель word2vec может улавливать некоторые семантические свойства слов. Классический пример из работы автора:
Слово «мужчина» относится к слову «женщина» так же, как слово «дядя» к слову «тётя», что для нас совершенно естественно и понятно, но в других моделям добиться такого же соотношения векторов можно только с помощью специальных ухищрений. Здесь же — это происходит естественно из самого корпуса текстов. Кстати, помимо семантических связей, улавливаются и синтаксические, справа показано соотношение единственного и множественного числа.
Более сложные вещи
На самом деле, за прошедшее время были предложены улучшения ставшей уже также классической модели Word2Vec. Два самых распространенных будут описаны ниже. Но этот раздел может быть пропущен без ущерба для понимания статьи в целом, если покажется слишком сложным.
Negative Sampling
В стандартной модели CBoW, рассмотренной выше, мы предсказываем вероятности слов и оптимизируем их. Функцией для оптимизации (минимизации в нашем случае) служит дивергенция Кульбака-Лейблера:
Здесь — распределение вероятностей слов, которое мы берем из корпуса,
— распределение, которое порождает наша модель. Дивергенция — это буквально «расхождение», насколько одно распределение не похоже на другое. Т.к. наши распределения на словах, т.е. являются дискретными, мы можем заменить в этой формуле интеграл на сумму:
Оказалось так, что оптимизировать эту формулу достаточно сложно. Прежде всего из-за того, что рассчитывается с помощью softmax по всему словарю. (Как мы помним, в английском сейчас порядка миллиона слов.) Здесь стоит отметить, что многие слова вместе не встречаются, как мы уже отмечали выше, поэтому большая часть вычислений в softmax является избыточной. Был предложен элегантный обходной путь, который получил название Negative Sampling. Суть этого подхода заключается в том, что мы максимизируем вероятность встречи для нужного слова в типичном контексте (том, который часто встречается в нашем корпусе) и одновременно минимизируем вероятность встречи в нетипичном контексте (том, который редко или вообще не встречается). Формулой мысль выше записывается так:
Здесь — точно такой же, что и в оригинальной формуле, а вот остальное несколько отличается. Прежде всего стоит обратить внимание на то, что формуле теперь состоит из двух частей: позитивной (
) и негативной (
). Позитивная часть отвечает за типичные контексты, и
здесь — это распределение совместной встречаемости слова
и остальных слов корпуса. Негативная часть — это, пожалуй, самое интересное — это набор слов, которые с нашим целевым словом встречаются редко. Этот набор порождается из распределения
, которое на практике берется как равномерное по всем словам словаря корпуса. Было показано, что такая функция приводит при своей оптимизации к результату, аналогичному стандартному softmax [2].
Hierarchical SoftMax
Также люди зашли и с другой стороны — можно не менять исходную формулу, а попробовать посчитать сам softmax более эффективно. Например, используя бинарное дерево [3]. По всем словам в словаре строится дерево Хаффмана. В полученном дереве слов располагаются на листьях дерева.
На рисунке изображен пример такого бинарного дерева. Жирным выделен путь от корня до слова . Длину пути обозначим
, а
-ую вершину на пути к слову
обозначим через
. Можно доказать, что внутренних вершин (не листьев)
.
С помощью иерархического softmax вектора предсказывается для
внутренних вершин. А вероятность того, что слово
будет выходным словом (в зависимости от того, что мы предсказываем: слово из контекста или заданное слово по контексту) вычисляется по формуле:
где — функция softmax;
;
— левый сын вершины
;
, если используется метод skip-gram,
, то есть, усредненный вектор контекста, если используется CBOW.
Формулу можно интуитивно понять, представив, что на каждом шаге мы можем пойти налево или направо с вероятностями:
Затем на каждом шаге вероятности перемножаются ( шагов) и получается искомая формула.
При использовании простого softmax для подсчета вероятности слова, приходилось вычислять нормирующую сумму по всем словам из словаря, требовалось операций. Теперь же вероятность слова можно вычислить при помощи последовательных вычислений, которые требуют
.
Другие модели
Помимо word2vec были, само собой, предложены и другие модели word embedding. Стоит отметить модель, предложенную лабораторией компьютерной лингвистики Стенфордского университета, под названием Global Vectors (GloVe), сочетающую в себе черты SVD разложения и word2vec [4].
Также надо упомянуть о том, что т.к. изначально все описанные модели были предложены для английского языка, то там не так остро стоит проблема словоизменения, характерная для синтетических языков (это — лингвистический термин), вроде русского. Везде выше по тексту неявно предполагалось, что мы либо считаем разные формы одного слова разными словами — и тогда надеяться, что нашего корпуса будет достаточно модели, чтобы выучить их синтаксическую близость, либо используем механизмы стеммирования или лемматизации. Стеммирование — это обрезание окончания слова, оставление только основы (например, “красного яблока” превратится в “красн яблок”). А лемматизация — замена слова его начальной формой (например, “мы бежим” превратится в “я бежать”). Но мы можем и не терять эту информацию, а использовать ее — закодировав OHE в новый вектор, и сконкатинировать его с вектором для основы или леммы.
Еще стоит сказать, что то, с чем мы начинали — буквенное представление слова — тоже не кануло в Лету: предложены модели по использованию буквенного представления слова для word embedding [5].
Практическое применение
Мы поговорили о теории, пришло время посмотреть, к чему все вышеописанное применимо на практике. Ведь любая самая красивая теория без практического применения — не более чем игра ума. Рассмотрим применение Word2Vec в двух задачах:
1) Задача классификации, необходимо по последовательности посещенных сайтов определять пользователя;
2) Задача регрессии, необходимо по тексту статьи определить ее рейтинг на Хабрахабре.
Классификация
Cкачать данные для первой задачи можно со страницы соревнования «Catch Me If You Can»
Т.к. сейчас мы каждому слову сопоставили вектор, то нужно решить, что сопоставить целому предложению из слов.
Один из возможных вариантов это просто усреднить все слова в предложении и получить некоторый смысл всего предложения (если слова нет в тексте, то берем нулевой вектор).
Т.к. мы получили distributed representation, то никакое число по отдельности ничего не значит, а значит лучше всего покажут себя линейные алгоритмы. Попробуем нейронные сети, LogisticRegression и проверим нелинейный метод XGBoost.
Получили неплохой результат. Значит Word2Vec смог выявить зависимости между сессиями.
Посмотрим, что произойдет с алгоритмом XGBoost.
Видим, что алгоритм сильно подстраивается под обучающую выборку, поэтому возможно наше предположение о необходимости использовать линейные алгоритмы подтверждено.
Посмотрим, что покажет обычный LogisticRegression.
Попробуем улучшить результаты.
Теперь вместо обычного среднего, чтобы учесть частоту с которой слово встречается в тексте, возьмем взвешенное среднее. В качестве весов возьмем IDF. Учёт IDF уменьшает вес широко употребительных слов и увеличивает вес более редких слов, которые могут достаточно точно указать на то, к какому классу относится текст. В нашем случае, кому принадлежит последовательность посещенных сайтов.
Проверим изменилось ли качество LogisticRegression.
видим прирост на 0.07, значит скорее всего взвешенное среднее помогает лучше отобразить смысл всего предложения через word2vec.
Предсказание популярности
Попробуем Word2Vec уже в текстовой задаче — предсказании популярности статьи на Хабрхабре.
Испробуем силы алгоритма непосредственно на текстовых данных статей Хабра. Мы преобразовали данные в csv таблицы. Скачать их вы можете здесь: train, test.
‘Доброго хабрадня!
\r\n
\r\nПерейду сразу к сути. С недавнего времени на меня возложилась задача развития контекстной сети текстовых объявлений. Задача возможно кому-то покажется простой, но есть несколько нюансов. Страна маленькая, 90% интернет-пользователей сконцентрировано в одном городе. С одной стороны легко охватить, с другой стороны некуда развиваться.
\r\n
\r\nТак как развитие интернет-проектов у нас слабое, и недоверие клиентов к местным проектам преобладает, то привлечь рекламодателей тяжело. Но самое страшное это привлечь площадки, которые знают и Бегун и AdSense, но абсолютно не знают нас. В целом проблема такая: площадки не регистрируются, потому что нет рекламодателей с деньгами, а рекламодатели не дают объявления, потому что список площадок слаб.
\r\n
\r\nКак выходят из такого положения Хабраспециалисты?’
Будем обучать модель на всем содержании статьи. Для этого совершим некоторые преобразования над текстом.
Напишем функцию, которая будет преобразовывать тестовую статью в лист из слов необходимый для обучения Word2Vec.
Функция получает строку, в которой содержится весь текстовый документ.
1) Сначала функция будет удалять все символы кроме букв верхнего и нижнего регистра;
2) Затем преобразовывает слова к нижнему регистру;
3) После чего удаляет стоп слова из текста, т.к. они не несут никакой информации о содержании;
4) Лемматизация, процесс приведения словоформы к лемме — её нормальной (словарной) форме.
Функция возвращает лист из слов.
Лемматизация занимает много времени, поэтому ее можно убрать в целях более быстрых подсчетов.
Будем обучаться на 2015 году, а валидироваться по первым 4 месяцам 2016, т.к. в нашей тестовой выборке представлены данные за первые 4 месяца 2017 года. Более правдивую валидацию можно сделать, идя по годам, увеличивая нашу обучающую выборку и смотря качество на первых четырех месяцах следующего года
Посмотрим чему выучилась модель:
[(‘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)]
Модель обучилась достаточно неплохо, посмотрим на результаты алгоритмов:
Попробуем нейронные сети.
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.
Литература
* Да, это специальная пасхалка для любителей творчества Энтони Бёрджеса.