Глубокое обучение на R, тренируем word2vec

от автора

Word2vec является практически единственным алгоритмом deep learning, который сравнительно легко можно запустить на обычном ПК (а не на видеокартах) и который строит распределенное представление слов за приемлемое время, по крайней мере так считают на Kaggle. Прочитав здесь про то, какие фокусы можно делать с тренированной моделью, я понял, что такую штуку просто обязан попробовать. Проблема только одна, я преимущественно работаю на языке R, а вот официальную реализацию word2vec под R мне найти не удалось, думаю её просто нет.


Зато есть исходники word2vec на C и описание на сайте Google, а в R есть возможность использовать внешние библиотеки на C, C++ и Fortran. Кстати, самые быстрые библиотеки R сделаны именно на C и С++. Еще есть R-обертка tmcn.word2vec, которая находится в стадии разработки. Её автор,
Jian Li (сайт на китайском) сделал что-то вроде демоверсии для китайского языка (с английским тоже работает, с русским пока не пробовал). Проблемы с этой версией следующие:

  • Во-первых, все параметры зашиты в C-коде;
  • Во-вторых, автор сделал только одну функцию для работы с обученной моделью – distance, которая оценивает сходство слов и выводит 20 вариантов с максимальным значением;
  • В-третьих, мне не удалось собрать пакет под x64 Windows. На win32 пакет ставится без проблем.

Оценив всё это «богатство», я решил сделать свой вариант R-интерфейса к word2vec. Сказать по правде, не очень хорошо знаю С, приходилось писать только простенькие программы, поэтому за основу я решил взять исходники Jian Li, потому что они точно компилируются под Windows, иначе бы не было пакета. Если что-то не будет работать, их всегда можно сверить с оригиналом.

Подготовка

Для того чтобы компилировать C-код для R под Windows нужно дополнительно установить Rtools. Этот набор инструментов содержит компилятор gcс, который запускается под Cygwin. После установки Rtools нужно проверить переменную PATH. Там должно быть что-то вроде:

 D:\Rtools\bin;D:\Rtools\gcc-4.6.3\bin;D:\R\bin 

Под OS X никаких Rtools не требуется. Нужен установленный компилятор, наличие которого проверяется командой gcc —version. Если его нет, нужно установить Xcode и через Xcode — Command Line Tools.

Про вызов С-библиотек из R нужно знать следующее:

  1. Все значения при вызове функции передаются в виде указателей и нужно позаботиться о том, чтобы в явном виде прописать их тип. Надежнее всего работает передача параметров типа char с последующим преобразованием в нужный тип уже в C;
  2. Вызываемая функция не возвращает значение, т.е. должна быть типа void;
  3. В C-код нужно добавить инструкцию #include <R.h>, а если есть сложная математика, то еще и #include <R.math>;
  4. Если нужно что-то вывести на консоль R, вместо printf() лучше использовать Rprintf(). Правда у меня printf() тоже работает.

Для начала я решил сделать что-то очень простое, типа Hello, World! Но так, чтобы туда передавалось какое-либо значение. Rstudio, которой я обычно пользуюсь, позволяет писать C и C++ код и всё правильно подсвечивает. Написав и сохранив код в hello.c я вызвал командную строку, перешел в нужный каталог и запустил компилятор следующей командой:

 > R --arch x64 CMD SHLIB hello.c 

Под win32 ключ архитектуры не нужен:

 > R CMD SHLIB hello.c 

В результате, в каталоге появилось два файла, hello.o (его можно смело удалить) и библиотека hello.dll. (На OS X вместо dll получится файл с расширением so). Вызов полученной функции hello в R осуществляется следующим кодом:

dyn.load("hello.dll") hellof <- function(n) {     .C("hello", as.integer(n)) } hellof(5) 

Тест показал, что всё работает правильно и для экспериментов с word2vec осталось подготовить данные. Я решил взять их на Kaggle из задачи «Bag of Words Meets Bags of Popcorn». Там есть обучающая, тестовая и неразмеченная выборки, которые в сумме содержат сто тысяч ревю фильмов из IMDB. Загрузив эти файлы, я убрал из них HTML-теги, специальные символы, цифры, знаки препинания, стоп-слова и токенизировал. Подробности обработки опускаю, я про них уже писал.

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

Модель

В варианте Jian Li — это два файла word2vec.h и word2vec.c. В первом содержится основной код, который в главном совпадает с оригинальным word2vec.c. Во втором — обертка для вызова функции TrainModel(). Первое, что я решил сделать — вытащить все параметры модели в R-код. Нужно было отредактировать R-скрипт и обертку в word2vec.c, получилась вот такая конструкция:

dyn.load("word2vec.dll") word2vec <- function(train_file, output_file,                       binary,                      cbow,                      num_threads,                      num_features,                      window,                      min_count,                      sample) { 	//...здесь вспомогательный код и проверки...          OUT <- .C("CWrapper_word2vec",                train_file = as.character(train_file),                output_file = as.character(output_file),               binary = as.character(binary), //... аналогично другие параметры               )  	//...здесь вывод диагностики из выходного потока OUT... } word2vec("train_data.txt", "model.bin",           binary=1, # output format, 1-binary, 0-txt          cbow=0, # skip-gram (0) or continuous bag of words (1)          num_threads = 1, # num of workers          num_features = 300, # word vector dimensionality          window = 10, # context / window size          min_count = 40, # minimum word count          sample = 1e-3 # downsampling of frequent words          ) 

Несколько слов про параметры:
binary — выходной формат модели;
cbow — какой алгоритм использовать для обучения skip-gram или мешок слов (cbow). Skip-gram работает медленнее, но дает лучший результат на редких словах;
num_threads — количество потоков процессора, задействованных при построении модели;
num_features — размерность пространства слов (или вектора для каждого слова), рекомендуется от десятков до сотен;
window — как много слов из контекста обучающий алгоритм должен принимать во внимание;
min_count — ограничивает размер словаря для значимых слов. Слова, которые не встречаются в тексте больше указанного количества, игнорируются. Рекомендованное значение — от десяти до ста;
sample — нижняя граница частоты встречаемости слов в тексте, рекомендуется от .00001 до .01.

Компилировал следующей командой с рекомендованными в makefile ключами:

 >R --arch x64 CMD SHLIB -lm -pthread -O3 -march=native -Wall -funroll-loops -Wno-unused-result word2vec.c 

Компилятор выдал некоторое количество предупреждений, но ничего серьезного, заветная word2vec.dll появилась в рабочем каталоге. Без проблем загрузил её в R функцией dyn.load(«word2vec.dll») и запустил одноименную функцию. Думаю, полезным является только ключ pthread. Без остальных можно обойтись (часть из них прописана в конфигурации Rtools).

Результат:
Всего в моем файле оказалось 11.5 млн. слов, словарь — 19133 слова, время построения модели 6 минут на компьютере с Intel Core i7. Чтобы проверить, работают ли мои параметры, я поменял значение num_threads с единицы на шесть. Можно было бы и не смотреть на мониторинг ресурсов, время построения модели сократилось до полутора минут. То есть эта штука умеет обрабатывать одиннадцать миллионов слов за минуты.

Оценка сходства

В distance я практически ничего менять не стал, только вытащил параметр количества возвращаемых значений. Затем скомпилировал библиотеку, загрузил её в R и проверил на двух словах «bad» и «good», учитывая, что имею дело с положительными и отрицательными ревю:

 Word: bad  Position in vocabulary: 15          Word   CosDist 1    terrible 0.5778409 2    horrible 0.5541780 3       lousy 0.5527389 4       awful 0.5206609 5   laughably 0.4910716 6   atrocious 0.4841466 7      horrid 0.4808238 8        good 0.4805901 9       worse 0.4726501 10 horrendous 0.4579800  Word: good  Position in vocabulary: 6         Word   CosDist 1     decent 0.5678578 2       nice 0.5364762 3      great 0.5197815 4        bad 0.4805902 5  excellent 0.4554003 6         ok 0.4365533 7    alright 0.4361723 8     really 0.4153538 9      liked 0.4061105 10      fine 0.4004776 

Всё снова получилось. Интересно, что от bad до good дистанция больше чем от good до bad если считать в словах. Ну, как говорится «от любви до ненависти…» ближе чем наоборот. Алгоритм рассчитывает сходство как косинус угла между векторами по следующей формуле (картинка из вики):

А значит, имея обученную модель, можно рассчитать дистанцию без С, и вместо сходства оценить, например, различия. Для этого нужно построить модель в текстовом формате (binary=0), загрузить её в R при помощи read.table() и написать некоторое количество кода, что я и сделал. Код без обработки исключений:

similarity <- function(word1, word2, model) {     size <- ncol(model)-1     vec1 <- model[model$word==word1,2:size]     vec2 <- model[model$word==word2,2:size]     sim <- sum(vec1 * vec2)     sim <- sim/(sqrt(sum(vec1^2))*sqrt(sum(vec2^2)))     return(sim) } difference <- function(string, model) {     words <- tokenize(string)     num_words <- length(words)     diff_mx <- matrix(rep(0,num_words^2), nrow=num_words, ncol=num_words)     for (i in 1:num_words) {         for (j in 1:num_words) {             sim <- similarity(words[i],words[j],model)             if(i!=j) {                 diff_mx[i,j]=sim             }         }     }     return(words[which.min(rowSums(diff_mx))]) } 

Здесь строится квадратная матрица размером количество слов в запросе на количество слов. Дальше для каждой пары несовпадающих слов рассчитывается сходство. Потом значения суммируются по строкам, находится строка с минимальной суммой. Номер строки соответствует позиции «лишнего» слова в запросе. Работу можно ускорить, если считать только половину матрицы. Пара примеров:

 > difference("squirrel deer human dog cat", model) [1] "human" > difference("bad red good nice awful", model) [1] "red" 

Аналогии

Поиск аналогий позволяет решать задачки типа «мужчина относится к женщина как король относится к ?». Специальная функция word-analogy есть только в оригинальном коде Google, поэтому с ней пришлось повозиться. Я написал обертку для вызова функции из R, убрал из кода бесконечный цикл и заменил стандартные потоки ввода-вывода на передачу параметров. Затем скомпилировал в библиотеку и сделал несколько экспериментов. Штука с королем-королевой у меня не получилась, видимо одиннадцати миллионов слов маловато (авторы word2vec рекомендуют в районе миллиарда). Несколько удачных примеров:

 > analogy("model300.bin", "man woman king", 3)       Word   CosDist 1   throne 0.4466286 2     lear 0.4268206 3 princess 0.4251665  > analogy("model300.bin", "man woman husband", 3)         Word   CosDist 1       wife 0.6323696 2 unfaithful 0.5626401 3    married 0.5268299  > analogy("model300.bin", "man woman boy", 3)      Word   CosDist 1    girl 0.6313665 2  mother 0.4309490 3 teenage 0.4272232 

Кластеризация

Почитав документацию я понял, что оказывается в word2vec есть встроенная K-Means кластеризация. И чтобы ей воспользоваться достаточно «вытащить» в R еще один параметр — classes. Это количество кластеров, если оно больше нуля, word2vec выдаст текстовый файл формата слово — номер кластера. Триста кластеров оказалось мало чтобы получить что-то вменяемое. Эвристика от разработчиков: размер словаря поделенный на 5. Соответственно выбрал 3000. Приведу несколько удачных кластеров (удачных в том смысле, что я понимаю, почему эти слова рядом):

            word   id 335       humor 2952 489     serious 2952 872      clever 2952 1035     humour 2952 1796 references 2952 1916     satire 2952 2061  slapstick 2952 2367     quirky 2952 2810      crude 2952 2953      irony 2952 3125 outrageous 2952 3296      farce 2952 3594      broad 2952 4870  silliness 2952 4979       edgy 2952          word  id 1025     cat 241 3242   mouse 241 11189 minnie 241             word  id 1089       army 322 1127   military 322 1556    mission 322 1558    soldier 322 3254       navy 322 3323     combat 322 3902    command 322 3975       unit 322 4270    colonel 322 4277  commander 322 7821    platoon 322 7853    marines 322 8691      naval 322 9762        pow 322 10391        gi 322 12452     corps 322 15839  infantry 322 16697     diver 322 

С помощью кластеризации нетрудно сделать сентимент-анализ. Для этого нужно построить «мешок кластеров» — матрицу размером количество ревю на максимальное количество кластеров. В каждой ячейки такой матрицы должно быть количество попаданий слов из ревю в заданный кластер. Я не пробовал, но проблем здесь не вижу. Говорят, что точность для ревю из IMDB получается такой же или немного меньше, чем если это делать через «Мешок слов».

Фразы

Word2vec умеет работать с фразами, вернее с устойчивыми сочетаниями слов. Для этого в оригинальном коде есть процедура word2phrase. Её задача – найти часто встречающиеся сочетания слов и заменить пробел между ними на нижнее подчеркивание. Файл, который получается после первого прохода содержит двойки слов. Если его снова отправить в word2phrase, появятся тройки и четверки. Результат потом можно использовать для тренировки word2vec.
Сделал вызов этой процедуры из R по аналогии с word2vec:

word2phrase("train_data.txt",              "train_phrase.txt",             min_count=5,                threshold=100) 

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

 [5887] "works_perfectly"                     "four_year_old"                       "multi_million_dollar"                [5890] "fresh_faced"                         "return_living_dead"                  "seemed_forced"                       [5893] "freddie_prinze_jr"                   "re_lucky"                            "puerto_rico"                         [5896] "every_sentence"                      "living_hell"                         "went_straight"                       [5899] "supporting_cast_including"           "action_set_pieces"                   "space_shuttle"      

Выбрал несколько фраз для distance():

 > distance("p_model300_2.bin", "crouching_tiger_hidden_dragon", 10) Word: crouching_tiger_hidden_dragon  Position in vocabulary: 15492                  Word   CosDist 1           tsui_hark 0.6041993 2             ang_lee 0.5996884 3  martial_arts_films 0.5541546 4      kung_fu_hustle 0.5381692 5        blockbusters 0.5305687 6           kill_bill 0.5279162 7          grindhouse 0.5242150 8             churned 0.5224440 9             budgets 0.5141657 10           john_woo 0.5046486  > distance("p_model300_2.bin", "academy_award_winning", 10) Word: academy_award_winning  Position in vocabulary: 15780                    Word   CosDist 1           nominations 0.4570983 2         ever_produced 0.4558123 3  francis_ford_coppola 0.4547777 4     producer_director 0.4545878 5          set_standard 0.4512480 6         participation 0.4503479 7     won_academy_award 0.4477891 8          michael_mann 0.4464636 9           huge_budget 0.4424854 10    directorial_debut 0.4406852 

На этом я эксперименты пока завершил. Одно важное замечание, word2vec «общается» с памятью напрямую, в результате R может работать нестабильно и аварийно завершать сессию. Иногда это связано с выводом диагностических сообщений от ОС, которые R не может корректно обработать. Если ошибок в коде нет, то помогает перезапустить интерпретатор или Rstudio.

R-код, исходники на C и скомпилированные под x64 Windows dll в моем репозитарии.

ссылка на оригинал статьи http://habrahabr.ru/post/258983/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *