Классическое объяснение word2vec как архитектуры Skip-gram с отрицательной выборкой в оригинальной научной статье и бесчисленных блог-постах выглядит так:
Действительно, если погуглить [word2vec skipgram], что мы видим:
Но все эти реализации ошибочны.
Оригинальная реализация word2vec на C работает иначе и кардинально отличается от этой. Те, кто профессионально внедряет системы с вложениями слов из word2vec, делают одно из следующих действий:
Действительно,
Реализация на C фактически поддерживает два вектора для каждого слова. Один вектор для этого слова в фокусе, а второй для слова в контексте. (Кажется знакомым? Верно, разработчики GloVe позаимствовали идею из word2vec, не упомянув об этом факте!)
Реализация в коде C исключительно грамотная:
Ещё раз, поскольку это вообще не объясняется в оригинальных статьях и нигде в интернете, я могу только предполагать.
Гипотеза заключается в том, что когда отрицательные образцы поступают со всего текста и не взвешиваются по частоте, вы можете выбрать любое слово, и чаще всего слово, вектор которого вообще не обучен. Если у этого вектора есть значение, то оно случайным образом сместит действительно важное слово в фокусе.
Суть в том, чтобы установить все отрицательные примеры на ноль, так что на представление другого вектора повлияют только векторы, которые встречаются более-менее часто.
На самом деле, это довольно хитроумно, и я раньше никогда не задумывался, насколько важны стратегии инициализации.
Я потратил два месяца своей жизни, пытаясь воспроизвести word2vec по описанию в оригинальной научной публикации и бесчисленных статьях в интернете, но не получилось. Я не смог достичь тех же результатов, что и word2vec, хотя старался изо всех сил.
Я не мог представить, что авторы публикации буквально сфабриковали алгоритм, который не работает, в то время как реализация делает нечто совершенно иное.
В конце концов, я решил изучить исходники. Три дня я пребывал в уверенности, что неправильно понимаю код, поскольку буквально все в интернете говорили об иной реализации.
Понятия не имею, почему оригинальная публикация и статьи в интернете ничего не говорят о реальном механизме работы word2vec, поэтому решил сам опубликовать эту информацию.
Это также объясняет радикальный выбор GloVe установить отдельные векторы для отрицательного контекста — они просто сделали то, что делает word2vec, но сказали людям об этом :).
Это научный обман? Не знаю, трудный вопрос. Но честно говоря, я невероятно зол. Наверное, я больше никогда не смогу серьёзно относиться к объяснению алгоритмов в машинном обучении: в следующий раз я сразу пойду смотреть исходники.
while(1) {
1. vf = vector of focus word
2. vc = vector of focus word
3. train such that (vc . vf = 1)
4. for(0 <= i <= negative samples):
vneg = vector of word *not* in context
train such that (vf . vneg = 0)
}
Действительно, если погуглить [word2vec skipgram], что мы видим:
- Страница Википедии, которая описывает алгоритм на высоком уровне
- Страница Tensorflow с тем же объяснением
- Блог Towards Data Science c описанием того же алгоритма, и список продолжается.
Но все эти реализации ошибочны.
Оригинальная реализация word2vec на C работает иначе и кардинально отличается от этой. Те, кто профессионально внедряет системы с вложениями слов из word2vec, делают одно из следующих действий:
- Напрямую вызывают исходную реализацию C.
- Используют реализацию
gensim
, которая транслитерируется из исходника C в той мере, в какой совпадают названия переменных.
Действительно,
gensim
— единственная известная мне верная реализация на C.Реализация на C
Реализация на C фактически поддерживает два вектора для каждого слова. Один вектор для этого слова в фокусе, а второй для слова в контексте. (Кажется знакомым? Верно, разработчики GloVe позаимствовали идею из word2vec, не упомянув об этом факте!)
Реализация в коде C исключительно грамотная:
- Массив
syn0
содержит векторное вложение слова, если оно попадается как слово в фокусе. Здесь случайная инициализация.
https://github.com/tmikolov/word2vec/blob/20c129af10659f7c50e86e3be406df663beff438/word2vec.c#L369 for (a = 0; a < vocab_size; a++) for (b = 0; b < layer1_size; b++) { next_random = next_random * (unsigned long long)25214903917 + 11; syn0[a * layer1_size + b] = (((next_random & 0xFFFF) / (real)65536) - 0.5) / layer1_size; }
- Другой массив
syn1neg
, содержит вектор слова, когда оно встречается как контекстное слово. Здесь инициализация нулём.
- Во время обучения (Skip-gram, отрицательная выборка, хотя другие случаи примерно такие же) мы сначала выбираем слово фокуса. Оно сохраняется на протяжении всего обучения на положительных и отрицательных примерах. Градиенты вектора фокуса накапливаются в буфере и применяются к фокусному слову после обучения и на положительных, и на отрицательных примерах.
if (negative > 0) for (d = 0; d < negative + 1; d++) { // if we are performing negative sampling, in the 1st iteration, // pick a word from the context and set the dot product target to 1 if (d == 0) { target = word; label = 1; } else { // for all other iterations, pick a word randomly and set the dot //product target to 0 next_random = next_random * (unsigned long long)25214903917 + 11; target = table[(next_random >> 16) % table_size]; if (target == 0) target = next_random % (vocab_size - 1) + 1; if (target == word) continue; label = 0; } l2 = target * layer1_size; f = 0; // find dot product of original vector with negative sample vector // store in f for (c = 0; c < layer1_size; c++) f += syn0[c + l1] * syn1neg[c + l2]; // set g = sigmoid(f) (roughly, the actual formula is slightly more complex) if (f > MAX_EXP) g = (label - 1) * alpha; else if (f < -MAX_EXP) g = (label - 0) * alpha; else g = (label - expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha; // 1. update the vector syn1neg, // 2. DO NOT UPDATE syn0 // 3. STORE THE syn0 gradient in a temporary buffer neu1e for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1neg[c + l2]; for (c = 0; c < layer1_size; c++) syn1neg[c + l2] += g * syn0[c + l1]; } // Finally, after all samples, update syn1 from neu1e https://github.com/tmikolov/word2vec/blob/20c129af10659f7c50e86e3be406df663beff438/word2vec.c#L541 // Learn weights input -> hidden for (c = 0; c < layer1_size; c++) syn0[c + l1] += neu1e[c];
Почему случайная и нулевая инициализация?
Ещё раз, поскольку это вообще не объясняется в оригинальных статьях и нигде в интернете, я могу только предполагать.
Гипотеза заключается в том, что когда отрицательные образцы поступают со всего текста и не взвешиваются по частоте, вы можете выбрать любое слово, и чаще всего слово, вектор которого вообще не обучен. Если у этого вектора есть значение, то оно случайным образом сместит действительно важное слово в фокусе.
Суть в том, чтобы установить все отрицательные примеры на ноль, так что на представление другого вектора повлияют только векторы, которые встречаются более-менее часто.
На самом деле, это довольно хитроумно, и я раньше никогда не задумывался, насколько важны стратегии инициализации.
Почему я это пишу
Я потратил два месяца своей жизни, пытаясь воспроизвести word2vec по описанию в оригинальной научной публикации и бесчисленных статьях в интернете, но не получилось. Я не смог достичь тех же результатов, что и word2vec, хотя старался изо всех сил.
Я не мог представить, что авторы публикации буквально сфабриковали алгоритм, который не работает, в то время как реализация делает нечто совершенно иное.
В конце концов, я решил изучить исходники. Три дня я пребывал в уверенности, что неправильно понимаю код, поскольку буквально все в интернете говорили об иной реализации.
Понятия не имею, почему оригинальная публикация и статьи в интернете ничего не говорят о реальном механизме работы word2vec, поэтому решил сам опубликовать эту информацию.
Это также объясняет радикальный выбор GloVe установить отдельные векторы для отрицательного контекста — они просто сделали то, что делает word2vec, но сказали людям об этом :).
Это научный обман? Не знаю, трудный вопрос. Но честно говоря, я невероятно зол. Наверное, я больше никогда не смогу серьёзно относиться к объяснению алгоритмов в машинном обучении: в следующий раз я сразу пойду смотреть исходники.
Комментарии (5)
roryorangepants
05.06.2019 17:38с отрицательной выборкой
В оригинале другое.
с вложениями слов
Никто так не говорит.
той мере, в какой совпадают названия переменных
Действительно, gensim — единственная известная мне верная реализация на C.
В оригинале другое.
Один вектор для этого слова в фокусе, а второй для слова в контексте
Кому-нибудь понятно, что имел в виду переводчик?
Дальше читал в оригинале.maxood
06.06.2019 15:04+1с вложениями слов
Да, звучит смешно. Перевод оторванный от контекста (words embedding in a continuous vector space) и сделанный с ошибкой — embedding = встраивание. По-русски я б написал «векторное представление слов».
QtRoS
06.06.2019 08:55Я прекрасно понимаю автора, поскольку несколько раз пытался разобраться и воспроизвести, например в рамках курса по tensorflow (или про deep learning в целом на примере tensorflow) от Udacity. Да, что-то похожее получалось, но не то.
Но справедливости ради хочу заметить, что описание реализации с двумя векторами мне попадалось, если нужно будет — поищу пруф. Тоже тогда недоумевал, почему такие детали скрыты...
maxood
06.06.2019 11:422-3 года назад хотел досконально разобраться в алгоритме, в результате написал свой вариант — github.com/maxoodf/word2vec
perfect_genius
Леонардо да Винчи тоже оставлял «дефекты» в своих чертежах, нарочно. Может, авторы хотели, чтобы к ним обратились за разъяснением.