Глубокое обучение на 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 нужно знать следующее:
Все значения при вызове функции передаются в виде указателей и нужно позаботиться о том, чтобы в явном виде прописать их тип. Надежнее всего работает передача параметров типа char с последующим преобразованием в нужный тип уже в C;
Вызываемая функция не возвращает значение, т.е. должна быть типа void;
В C-код нужно добавить инструкцию #include
Модель В варианте 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 в моем репозитарии.