- PVSM.RU - https://www.pvsm.ru -
Классическое объяснение word2vec как архитектуры Skip-gram с отрицательной выборкой в оригинальной научной статье и бесчисленных блог-постах выглядит так:
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], что мы видим:
Но все эти реализации ошибочны.
Оригинальная реализация word2vec на C работает иначе и кардинально отличается от этой. Те, кто профессионально внедряет системы с вложениями слов из word2vec, делают одно из следующих действий:
gensim
, которая транслитерируется из исходника C в той мере, в какой совпадают названия переменных.
Действительно, gensim
— единственная известная мне верная реализация на 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
, содержит вектор слова, когда оно встречается как контекстное слово. Здесь инициализация нулём.
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, но сказали людям об этом :).
Это научный обман? Не знаю, трудный вопрос. Но честно говоря, я невероятно зол. Наверное, я больше никогда не смогу серьёзно относиться к объяснению алгоритмов в машинном обучении: в следующий раз я сразу пойду смотреть исходники.
Автор: m1rko
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/algoritmy/319949
Ссылки в тексте:
[1] Страница Википедии, которая описывает алгоритм на высоком уровне: https://en.wikipedia.org/wiki/Word2vec#Training_algorithm
[2] Страница Tensorflow с тем же объяснением: https://www.tensorflow.org/tutorials/representation/word2vec
[3] Блог Towards Data Science c описанием того же алгоритма: https://towardsdatascience.com/word2vec-skip-gram-model-part-1-intuition-78614e4d6e0b
[4] Источник: https://habr.com/ru/post/454926/?utm_source=habrahabr&utm_medium=rss&utm_campaign=454926
Нажмите здесь для печати.