Accéder à la page principale du cours

Séance 3 - Apprentissage spectral de représentations vectorielles des mots

Adrien Guille, Université Lyon 2

Pour cette séance, 4 packages sont nécessaires : text2vec pour manipuler le texte, Matrix pour manipuler les matrices creuses produites par text2vec, sparsesvd pour calculer efficacement la SVD tronquée et MASS pour sauvegarder les représentations apprises.

L’objectif est d’implémenter Skip-Gram with Negative Sampling via une décomposition en valeurs singulières tronquée de la matrice décrivant l’information mutuelle (ponctuelle diminuée positive) entre les mots.

library(text2vec)
library(Matrix)
library(sparsesvd)
library(tictoc)
library(MASS)

Chargement et préparation du corpus

On charge les \(10^9\) premiers octets du dump anglophone de Wikipedia du 3 mars 2006 (source : http://mattmahoney.net/dc/textdata.html; fichier utilisé ici : http://mediamining.univ-lyon2.fr/people/guille/word_embedding/text9.zip), on extrait le vocabulaire (on ne conserve que les 30 000 mots les plus fréquents) pour pouvoir vectoriser le texte ensuite :

tic("Chargement et préparation du corpus")
max_vocabulary_size <- 30000
corpus <- readLines('../Data/text9', n=1, warn=FALSE)
iterator <- itoken(corpus, tokenizer=space_tokenizer, progressbar=FALSE)
vocabulary <- create_vocabulary(iterator)
print(sum(vocabulary$term_count))
## [1] 124301826
pruned_vocabulary <- prune_vocabulary(vocabulary, vocab_term_max=max_vocabulary_size)
vectorizer <- vocab_vectorizer(pruned_vocabulary)
toc()
## Chargement et préparation du corpus: 51.74 sec elapsed

Le corpus comporte plus de 124 millions de tokens.

Création de la matrice de cooccurrence

On crée la matrice \(X \in \mathbb{N}^{n \times n}\) qui décrit la fréquence (absolue) de co-occurrence entre les mots, pour une fenêtre symétrique, de longueur \(l = 5\) :

tic("Création de la matrice de cooccurrence")
l <- 5
X <- create_tcm(iterator, vectorizer, skip_grams_window=l, weights=rep(1, l))
toc()
## Création de la matrice de cooccurrence: 159.243 sec elapsed
print(nnzero(X) / max_vocabulary_size**2)
## [1] 0.03822968

Un peu moins de 4% des coefficients de \(X\) sont non nuls.

Calcul de la matrice SPPMI

On calcule la matrice \(M^{\text{SPPMI}}\), pour \(k=5\) :

tic("Calcul de la matrice SPPMI diminuée")
k <- 5
total <- sum(X)
word_prob <- colSums(X) / total
M <- X / total # calcul des p_ij
p_i_p_j <- word_prob %*% t(word_prob) # calcul des p_i * p_j
M <- log(M * 1/p_i_p_j) - log(k) # calcul de la PMI diminuée de log(k)
M[is.na(M)] <- 0 # gestion des NA causé par les log(p_ij) quand p_ij = 0
M[M<0] <- 0 # seuillage à 0 pour obtenir l'information mutuelle diminuée positive
toc()
## Calcul de la matrice SPPMI diminuée: 166.137 sec elapsed
M <- drop0(M) # retrait des 0 explicites pour préserver une représentation creuse efficace
print(nnzero(M) / max_vocabulary_size**2)
## [1] 0.01178099

Environ 1% des coefficients de \(M\) sont non nuls.

Calcul des représentations des mots par décomposition spectrale

On calcule la décomposition de \(M\) en valeurs singulières tronquée de rang \(d=50\) :

tic("Calcul des représentations des mots par décomposition spectrale")
decomposition <- sparsesvd(M, rank=100)
vectors <- decomposition$u %*% sqrt(diag(decomposition$d))
toc()
## Calcul des représentations des mots par décomposition spectrale: 22.105 sec elapsed

Résolution d’analogies

On reprend les fonctions écrites durant la séance 2 (consulter le notebook), on calcule quelques similarités, on calcule quelques voisinages et on résout les mêmes analogies, en anglais cette fois :

words <- pruned_vocabulary$term

cosine_similarity <- function(v1, v2){
  dot_product <- v1 %*% v2
  norm_prod <- sqrt(sum(v1**2)) * sqrt(sum(v2**2))
  return(as.numeric(dot_product / norm_prod))
}

find_closest_words <- function(v, n=5){
  similarity <- numeric(nrow(vectors))
  for(i in 1:nrow(vectors)){
    similarity[i] <- cosine_similarity(v, vectors[i, ])
  }
  ordered_words <- words[order(-similarity)]
  return(ordered_words[1:n])
}

resolve_analogy <- function(word_a, word_b, word_c, n=1){
  word_d <- vectors[match(word_b, words), ] - vectors[match(word_a, words), ] + vectors[match(word_c, words), ]
  return(find_closest_words(word_d, n))
}

On mesure la similarité entre des paires de mots de moins en moins sémantiquement proches :

cosine_similarity(vectors[match('car', words), ], vectors[match('truck', words), ])
## [1] 0.8712034
cosine_similarity(vectors[match('car', words), ], vectors[match('bike', words), ])
## [1] 0.4996874
cosine_similarity(vectors[match('car', words), ], vectors[match('boat', words), ])
## [1] 0.1502971
cosine_similarity(vectors[match('car', words), ], vectors[match('table', words), ])
## [1] 0.04475195

On affiche les mots les plus proches de “car” :

find_closest_words(vectors[match('car', words), ])
## [1] "car"        "cars"       "automobile" "bmw"        "porsche"

On résout deux analogies. Par exemple, le modèle trouve que la capitale de l’Espagne est Madrid :

resolve_analogy('father','mother', 'son')
## [1] "daughter"
resolve_analogy('france','paris', 'spain')
## [1] "madrid"

Evaluation sur une tâche linguistique

On mesure la correlation de Spearman entre les scores donnés à des paires de mots par des évaluateurs humains (source : http://www.cs.technion.ac.il/~gabr/resources/data/wordsim353/ ; fichier utilisé ici : http://mediamining.univ-lyon2.fr/people/guille/word_embedding/ws353.tsv), et la similarité cosinus mesurées avec les vecteurs appris :

ws353 <- read.csv('../Data/ws353.tsv', sep='\t', stringsAsFactors=FALSE)
head(ws353, 5)
similarity <- numeric(nrow(ws353))
for(i in 1:nrow(ws353)){
  word_1 <- ws353[i, 1]
  word_2 <- ws353[i, 2]
  similarity[i] <- cosine_similarity(vectors[match(word_1, words), ], 
                                     vectors[match(word_2, words), ])
}
cor.test(ws353$score, similarity, method = "spearman")
## Warning in cor.test.default(ws353$score, similarity, method = "spearman"):
## Cannot compute exact p-value with ties
## 
##  Spearman's rank correlation rho
## 
## data:  ws353$score and similarity
## S = 1959000, p-value < 2.2e-16
## alternative hypothesis: true rho is not equal to 0
## sample estimates:
##       rho 
## 0.6669039

On obtient une corrélation similaire à celle mesurée dans l’article de Levy & Goldberg (légèrement supérieure à celle mesurée avec la formulation originale de Skip-Gram with Negative Sampling).

Sauvegarde des représentations

On exporte les vecteurs, les mots et les fréquences :

write.matrix(vectors, file='spectral_en.vectors')
write.csv(words, file='spectral_en.words')
frequency <- vocabulary$term_count
write.csv(frequency, file='spectral_en.frequency')