Todo lo que sabías sobre word2vec no es cierto

La explicación clásica de word2vec como una arquitectura Skip-gram de muestra negativa en el artículo científico original y en innumerables publicaciones de blog se ve así:

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) } 

De hecho, si buscas en Google [word2vec skipgram], lo que vemos:


Pero todas estas implementaciones están mal .

La implementación original de word2vec en C funciona de manera diferente y es fundamentalmente diferente de esto. Aquellos que implementan profesionalmente sistemas con incrustaciones de palabras de word2vec hacen uno de los siguientes:

  1. Llame directamente a la implementación original de C.
  2. Use la implementación gensim , que se transcribe desde la fuente C en la medida en que coincidan los nombres de las variables.

De hecho, gensim es la única implementación verdadera de C que conozco .

Implementación C


La implementación de C en realidad admite dos vectores para cada palabra . Un vector para la palabra está en foco, y el segundo para la palabra en contexto. (¿Parece familiar? Correcto, ¡los desarrolladores de GloVe tomaron una idea de word2vec sin mencionar este hecho!)

La implementación en el código C es excepcionalmente competente:

  • La matriz syn0 contiene la incrustación vectorial de la palabra si aparece como una palabra en foco. Aquí hay una inicialización aleatoria .

     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; } 
  • Otra matriz syn1neg contiene el vector de la palabra cuando aparece como una palabra de contexto. Aquí la inicialización es cero .
  • Durante el entrenamiento (Skip-gram, muestra negativa, aunque otros casos son casi iguales), primero seleccionamos la palabra de enfoque. Se mantiene durante toda la capacitación sobre ejemplos positivos y negativos. Los gradientes del vector de enfoque se acumulan en el búfer y se aplican a la palabra de enfoque después del entrenamiento en ejemplos positivos y negativos.

     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]; 

¿Por qué inicialización aleatoria y cero?


Una vez más, dado que esto no se explica en absoluto en los artículos originales y en cualquier lugar de Internet , solo puedo especular.

La hipótesis es que cuando las muestras negativas provienen de todo el texto y no se ponderan por frecuencia, puede elegir cualquier palabra , y con mayor frecuencia una palabra cuyo vector no esté entrenado en absoluto . Si este vector tiene un significado, cambiará aleatoriamente la palabra realmente importante en foco.

La conclusión es establecer todos los ejemplos negativos en cero, de modo que solo los vectores que ocurran con mayor o menor frecuencia afecten la presentación de otro vector.

Esto es realmente bastante complicado, y nunca había pensado en lo importantes que son las estrategias de inicialización.

¿Por qué estoy escribiendo esto?


Pasé dos meses de mi vida tratando de reproducir word2vec como se describe en la publicación científica original e innumerables artículos en Internet, pero fallé. No pude lograr los mismos resultados que word2vec, aunque hice mi mejor esfuerzo.

No podía imaginar que los autores de la publicación literalmente fabricaran un algoritmo que no funciona, mientras que la implementación hace algo completamente diferente.

Al final, decidí estudiar la fuente. Durante tres días estuve seguro de que entendí mal el código, ya que literalmente todos en Internet hablaron de una implementación diferente.

No tengo idea de por qué la publicación original y los artículos en Internet no dicen nada sobre el mecanismo real de word2vec, así que decidí publicar esta información yo mismo.

Esto también explica la elección radical de GloVe de establecer vectores separados para el contexto negativo: simplemente hicieron lo que hace word2vec, pero le dijeron a la gente al respecto :).

¿Es este un truco científico? No sé, una pregunta difícil. Pero para ser honesto, estoy increíblemente enojado. Probablemente, nunca más podré volver a tomarme en serio la explicación de los algoritmos en el aprendizaje automático: la próxima vez iré inmediatamente a ver las fuentes.

Source: https://habr.com/ru/post/454926/


All Articles