Ejemplo de word2vec con gensim

En la siguiente celda, importamos las librerías necesarias y configuramos los mensajes de los logs.


In [1]:
import gensim, logging, os
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

Entrenamiento de un modelo

Implemento una clase Corpus con un iterador sobre un directorio que contiene ficheros de texto. Utilizaré una instancia de Corpus para poder procesar de manera más eficiente una colección, sin necesidad de cargarlo previamente en memoria.


In [2]:
class Corpus(object):
    '''Clase Corpus que permite leer de manera secuencial un directorio de documentos de texto'''
    
    def __init__(self, directorio):
        self.directory = directorio

    def __iter__(self):
        for fichero in os.listdir(self.directory):
            for linea in open(os.path.join(self.directory, fichero)):
                yield linea.split()

CORPUSDIR contiene una colección de noticias en español (normalizada previamente a minúsculas y sin signos de puntuación) con alrededor de 150 millones de palabras. Entrenamos un modelo en un solo paso, ignorando aquellos tokens que aparecen menos de 10 veces, para descartar erratas.


In [3]:
CORPUSDIR = '/opt/textos/efe/txt/'
oraciones = Corpus(CORPUSDIR)
#model = gensim.models.Word2Vec(oraciones, min_count=10, size=150, workers=2)

# el modelo puede entrenarse en dos pasos sucesivos pero por separado
#model = gensim.models.Word2Vec() # modelo vacío
#model.build_vocab(oraciones)  # primera pasada para crear la lista de vocabulario
#model.train(other_sentences)  # segunda pasada para calcula vectores

Una vez completado el entrenamiento (después de casi 30 minutos), guardamos el modelo en disco.


In [ ]:
#model.save('/opt/textos/efe/efe.model.w2v')

En el futuro, podremos utilizar este modelo cargándolo en memoria con la instrucción:


In [4]:
model = gensim.models.Word2Vec.load('/opt/textos/efe/efe.model.w2v')

Probando nuestro modelo

El objeto model contiene una enorme matriz de números: una tabla, donde cada fila es uno de los términos del vocabulario reconocido y cada columna es una de las características que permiten modelar el significado de dicho término.

En nuestro modelo, tal y como está entrenado, tenemos más de 26 millones de términos:


In [5]:
print(model.corpus_count)


26522244

Cada término del vocabulario está representado como un vector con 150 dimensiones: 105 características. Podemos acceder al vector de un término concreto:


In [6]:
print(model['azul'], '\n')

print(model['verde'], '\n')

print(model['microsoft'])


[-0.17119461 -0.22743705  0.33344841  0.27989009  0.19492076 -0.1409657
 -0.0251887  -0.306355    0.10662927  0.17104091 -0.22948962  0.36396649
 -0.23878925 -0.21033089  0.19784033 -0.61557579 -0.21383885  0.44664291
 -0.64749044 -0.2872344  -0.55847257  0.1303734   0.03406142 -0.31790459
  0.21620856  0.24226433  0.27817437  0.26362386 -0.24987099  0.11778339
 -0.47454187 -0.23323169 -0.30103257 -0.03067073 -0.04180949 -0.32016489
 -0.18483758  0.08247121 -0.19331424  0.40855029 -0.47981584  0.16826837
 -0.41814953 -0.4381167  -0.16309766  0.0599881   0.0454194  -0.14878696
  0.47207984  0.26696566 -0.39615464  0.05608032  0.03534137 -0.07457943
  0.10052393 -0.30167556 -0.02699662  0.28504115 -0.404071   -0.44440794
 -0.36049092 -0.13775028  0.31180441  0.33074516 -0.17758727 -0.20152695
  0.18360205  0.36311045  0.11820537  0.17505574 -0.26084182  0.22973292
  0.14244203 -0.21329062  0.24214892 -0.05452183  0.53469735  0.4316895
  0.17438972  0.14840761  0.17512347 -0.06805158 -0.00998271  0.53148156
  0.05237582 -0.61710912  0.14314358 -0.04660515 -0.26403904  0.01032358
 -0.03842738 -0.0823352   0.15600698  0.15606904  0.4360964   0.47239709
 -0.27302223  0.01663303  0.1273931   0.23075445 -0.4847928   0.16276275
  0.30582228  0.10943583  0.1279446  -0.32940444  0.07694072  0.2292821
 -0.12622666  0.0733298   0.07129105 -0.45641646  0.13136545  0.28329578
  0.12077157 -0.10404336 -0.20772858 -0.00931035  0.29601341 -0.3436535
  0.15114224  0.4262369  -0.41771489 -0.1791162   0.33102775  0.04763839
  0.19161744  0.18149562 -0.51195669  0.32962632  0.16486904  0.08474743
  0.34230816  0.29315808 -0.44999346 -0.31663135  0.23556025 -0.05824243
 -0.08865268  0.29067162 -0.0396572   0.24919198  0.01579068  0.09618133
 -0.10783098 -0.22225887 -0.37826779 -0.18020371  0.26146412 -0.52031434] 

[  2.30957493e-01   1.76589355e-01   1.79953799e-01   1.23078786e-01
  -1.94133580e-01  -6.09562278e-01   2.63852388e-01  -1.70022380e-02
  -1.29560709e-01  -1.13064274e-02  -1.26384303e-01   2.06832543e-01
  -1.94057465e-01   9.61356834e-02   4.52292740e-01  -5.58245301e-01
  -1.87400118e-01   2.34033436e-01  -1.58626214e-01  -1.08615249e-01
  -1.67247057e-01  -4.08768624e-01   7.05183251e-03  -1.96276978e-01
   2.94465423e-01   2.76828438e-01   9.55004990e-02   5.25288135e-02
   2.80851144e-02  -1.00802615e-01  -2.14736909e-01   8.86443853e-02
  -2.60655403e-01  -1.99017361e-01   2.71034628e-01   9.35409889e-02
   1.07839346e-01   3.95288587e-01   7.92251155e-02   5.39270461e-01
  -1.14018798e-01   1.05657823e-01  -2.30260506e-01  -3.62149984e-01
  -4.64724749e-01  -2.49613866e-01   1.31628051e-01  -1.35565504e-01
   2.21864328e-01   2.51121849e-01  -1.63170382e-01   7.48877153e-02
  -1.68398455e-01   3.70264560e-01  -4.65207398e-01  -8.64806771e-02
   3.94436002e-01   2.91568656e-02   1.10389806e-01  -3.77948970e-01
  -8.06069002e-02   2.62984425e-01  -1.29976794e-01   2.92889208e-01
  -1.73737943e-01   3.87570143e-01   4.47416782e-01   8.74537751e-02
  -4.01846692e-02   3.45469378e-02  -4.39972609e-01   2.88376212e-01
  -3.49146962e-01  -2.65900582e-01   1.29171297e-01  -1.83937877e-01
   5.62119484e-01  -3.13232273e-01  -1.65914316e-02  -3.80431473e-01
   1.98906921e-02   1.99894547e-01  -7.93143511e-02   2.98175305e-01
   2.35787019e-01   1.01734838e-02  -1.08139291e-02   2.69624013e-02
  -3.77130136e-02   4.58080322e-02   4.75254096e-02  -1.75421521e-01
   1.13721803e-01   2.23769397e-01  -4.76955213e-02  -3.22787538e-02
   7.07692355e-02  -4.08826292e-01  -1.10958837e-01   5.19879222e-01
  -2.56664693e-01  -5.19150384e-02  -1.22189671e-01  -1.49979010e-01
   2.74152070e-01  -2.55965441e-01   3.27939004e-01   2.63560683e-01
  -3.11302356e-02   5.83389327e-02   4.87841703e-02   1.75534740e-01
   1.49735332e-01   3.69882911e-01  -2.41462648e-01   2.02506438e-01
  -2.04846472e-01  -9.59189981e-02  -2.36701399e-01  -2.01113150e-01
   7.47849122e-02   2.45368212e-01   2.01951489e-01  -1.61669380e-03
   1.47516490e-04   5.26641868e-02   3.73400897e-01   1.35177851e-01
  -4.62602794e-01   5.88251293e-01  -1.09317265e-01   7.43552148e-02
   2.44288892e-01   2.17024609e-01  -1.40864953e-01   4.01866473e-02
   3.27873707e-01  -3.76101248e-02  -1.67314544e-01   4.59715426e-02
   1.35031685e-01  -1.30689338e-01   1.65665939e-01   5.36308408e-01
  -3.69352221e-01   2.40254417e-01  -2.08341196e-01  -4.24397588e-01
   7.88010284e-02   2.89100800e-02] 

[-0.03197898 -0.01570364 -0.3282325   0.08900949 -0.30093634  0.01832501
 -0.13503864 -0.19304295 -0.02079177 -0.22155227  0.08474499  0.06759185
 -0.06359272  0.1224151  -0.42976394 -0.20505999 -0.41154486  0.05751702
  0.29641381  0.09107774  0.25936633 -0.19533104  0.09884192 -0.05788413
 -0.06249953 -0.13323429 -0.05861594  0.10306133 -0.16049948  0.08856192
  0.27047008 -0.15098111 -0.21126767  0.48491111  0.15265165  0.07706715
  0.10325838  0.09815908 -0.05754314 -0.32591283 -0.02452205  0.18816504
 -0.2459964   0.20166932  0.30379188 -0.20653576  0.10118359 -0.04970498
 -0.31105918  0.28187343 -0.2056184   0.17799883 -0.36423597  0.02935517
  0.1066677   0.19146897  0.26359546  0.11294283  0.17230126 -0.16991299
 -0.09121415 -0.22017279  0.31777039  0.0981535  -0.18879151 -0.19472031
 -0.04327782 -0.1862593   0.23005775  0.03584678 -0.14436445  0.1811415
 -0.1911494  -0.01824404  0.38899192  0.26932925 -0.00555024  0.01413355
 -0.07528925 -0.05700698 -0.01515972  0.13451138 -0.02059917  0.0314442
  0.45566612 -0.0669797   0.08088095 -0.00164654 -0.14569741  0.10268459
  0.00076803 -0.1848142  -0.24874015  0.15364796  0.05178113  0.03680747
  0.12090363  0.03167083 -0.48286209  0.05833738 -0.48276404 -0.26606175
 -0.00903553  0.17854503  0.07405417 -0.13267273 -0.52087009  0.37045401
  0.36452699  0.16480219 -0.06983863 -0.10096778 -0.16836952  0.09662409
 -0.49067006 -0.08418     0.04454358 -0.02881731  0.29176784  0.14452544
  0.29302049  0.0193401   0.04088018 -0.29334545 -0.20482774 -0.16266401
  0.3165023  -0.07202946  0.22826849  0.01488546 -0.01429483 -0.33397943
 -0.01432972  0.2738508   0.48433411 -0.13700686 -0.21904209  0.34901419
 -0.09081372 -0.08691191  0.21047825  0.27449569 -0.2196137  -0.03813799
  0.0521419   0.38822809  0.20957901  0.09806318  0.32574233  0.42475697]

Estos vectores no nos dicen mucho, salvo que contienen números muy pequeños :-/

El mismo objeto model permite acceder a una serie de funcionalidades ya implementadas que nos van a permitir evaluar formal e informalmente el modelo. Por el momento, nos contentamos con los segundo: vamos a revisar visualmente los significados que nuestro modelo ha aprendido por su cuenta.

Podemos calcular la similitud semántica entre dos términos usando el método similarity, que nos devuelve un número entre 0 y 1:


In [7]:
print('hombre - mujer', model.similarity('hombre', 'mujer'))

print('madrid - parís', model.similarity('madrid', 'parís'))

print('perro - gato', model.similarity('perro', 'gato'))

print('gato - periódico', model.similarity('gato', 'periódico'))


hombre - mujer 0.473325617835
madrid - parís 0.649728730699
perro - gato 0.631910988958
gato - periódico 0.180082365746

Podemos seleccionar el término que no encaja a partir de una determinada lista de términos usando el método doesnt_match:


In [8]:
lista1 = 'madrid barcelona gonzález washington'.split()
print('en la lista', ' '.join(lista1), 'sobra:', model.doesnt_match(lista1))

lista2 = 'psoe pp ciu epi'.split()
print('en la lista', ' '.join(lista2), 'sobra:', model.doesnt_match(lista2))

lista3 = 'publicaron declararon soy negaron'.split()
print('en la lista', ' '.join(lista3), 'sobra:', model.doesnt_match(lista3))

lista3 = 'homero saturno cervantes shakespeare cela'.split()
print('en la lista', ' '.join(lista3), 'sobra:', model.doesnt_match(lista3))


en la lista madrid barcelona gonzález washington sobra: gonzález
en la lista psoe pp ciu epi sobra: epi
en la lista publicaron declararon soy negaron sobra: soy
en la lista homero saturno cervantes shakespeare cela sobra: saturno

Podemos buscar los términos más similares usando el método most_similar de nuestro modelo:


In [9]:
terminos = 'psoe chicago sevilla aznar podemos estuvieron'.split()

for t in terminos:
    print(t, '==>', model.most_similar(t), '\n')


psoe ==> [('pp', 0.8361572623252869), ('psc', 0.7607941031455994), ('psc-psoe', 0.7428343892097473), ('pnv', 0.7384518384933472), ('pse-ee', 0.7288982272148132), ('pp-', 0.7149612307548523), ('ciu', 0.7143486142158508), ('pce', 0.7000882625579834), ('iu', 0.6951525807380676), ('pse-psoe', 0.6903963685035706)] 

chicago ==> [('boston', 0.7728167772293091), ('filadelfia', 0.7359776496887207), ('dallas', 0.7138493061065674), ('detroit', 0.6884219646453857), ('houston', 0.6674472689628601), ('minneapolis', 0.6521533131599426), ('indianapolis', 0.6486193537712097), ('illinois', 0.6469548940658569), ('massachusetts', 0.6462246775627136), ('minnesota', 0.6423684358596802)] 

sevilla ==> [('valencia', 0.8517122268676758), ('oviedo', 0.828134298324585), ('valladolid', 0.8251886963844299), ('madrid', 0.8147549629211426), ('barcelona', 0.8145360946655273), ('mérida', 0.8092716932296753), ('toledo', 0.8011802434921265), ('badajoz', 0.791728675365448), ('albacete', 0.7854650616645813), ('málaga', 0.7728221416473389)] 

aznar ==> [('benegas', 0.8136860728263855), ('mendiluce', 0.7664819359779358), ('caneda', 0.7204908728599548), ('mohedano', 0.7103445529937744), ('zufiaur', 0.701751708984375), ('anguita', 0.7008182406425476), ('atutxa', 0.6707108616828918), ('gorordo', 0.6698802709579468), ('chiquillo', 0.6630854606628418), ('cuevas', 0.6615549325942993)] 

podemos ==> [('queremos', 0.8920885920524597), ('debemos', 0.8877831697463989), ('podremos', 0.8614301681518555), ('podíamos', 0.8506598472595215), ('deberíamos', 0.8323997855186462), ('podríamos', 0.8320119976997375), ('puedo', 0.8213134407997131), ('quiero', 0.8056972026824951), ('puedes', 0.7859168648719788), ('podamos', 0.7839317321777344)] 

estuvieron ==> [('estarán', 0.826420783996582), ('estaban', 0.7346822619438171), ('están', 0.6653903722763062), ('estén', 0.6633749604225159), ('estuvieran', 0.6207591891288757), ('estan', 0.6195564270019531), ('estuvo', 0.603593647480011), ('estarían', 0.595328688621521), ('estuvimos', 0.5647614598274231), ('estábamos', 0.5478390455245972)] 

Con el mismo método most_similar podemos combinar vectores de palabras tratando de jugar con los rasgos semánticos de cada una de ellas para descubrir nuevas relaciones.


In [10]:
print('==> alcalde + mujer - hombre')
most_similar = model.most_similar(positive=['alcalde', 'mujer'], negative=['hombre'], topn=3)
for item in most_similar:
    print(item)

print('==> madrid + filipinas - españa')
most_similar = model.most_similar(positive=['madrid', 'filipinas'], negative=['españa'], topn=3)
for item in most_similar:
    print(item)

print('==> michel + fútbol + argentina - españa')
most_similar = model.most_similar(positive=['michel', 'fútbol', 'argentina'], negative=['españa'], topn=3)
for item in most_similar:
    print(item)


==> alcalde + mujer - hombre
('alcaldesa', 0.5925700068473816)
('consellera', 0.5343092679977417)
('concejala', 0.5338941812515259)
==> madrid + filipinas - españa
('manila', 0.5159924030303955)
('pekín', 0.5065802335739136)
('hanoi', 0.49402812123298645)
==> michel + fútbol + argentina - españa
('platini', 0.529076099395752)
('ex-capitán', 0.5200508236885071)
('fpf', 0.4923384487628937)