Comparison of FastText and Word2Vec

Facebook Research open sourced a great project recently - fastText, a fast (no surprise) and effective method to learn word representations and perform text classification. I was curious about comparing these embeddings to other commonly used embeddings, so word2vec seemed like the obvious choice, especially considering fastText embeddings are an extension of word2vec.

I've used gensim to train the word2vec models, and the analogical reasoning task (described in Section 4.1 of [2]) for comparing the word2vec and fastText models. I've compared embeddings trained using the skipgram architecture.

Download data

In [1]:
import nltk
nltk.download('brown') 
# Only the brown corpus is needed in case you don't have it.

# Generate brown corpus text file
with open('brown_corp.txt', 'w+') as f:
    for word in nltk.corpus.brown.words():
        f.write('{word} '.format(word=word))

# Make sure you set FT_HOME to your fastText directory root
FT_HOME = 'fastText/'
# download the text8 corpus (a 100 MB sample of cleaned wikipedia text)
import os.path
if not os.path.isfile('text8'):
    !wget -c http://mattmahoney.net/dc/text8.zip
    !unzip text8.zip
# download and preprocess the text9 corpus
if not os.path.isfile('text9'):
  !wget -c http://mattmahoney.net/dc/enwik9.zip
  !unzip enwik9.zip
  !perl {FT_HOME}wikifil.pl enwik9 > text9
[nltk_data] Downloading package brown to /home/jayant/nltk_data...
[nltk_data]   Package brown is already up-to-date!
--2016-08-23 03:29:12--  http://mattmahoney.net/dc/text8.zip
Resolving mattmahoney.net (mattmahoney.net)... 98.139.135.129
Connecting to mattmahoney.net (mattmahoney.net)|98.139.135.129|:80... connected.
HTTP request sent, awaiting response... 416 Requested Range Not Satisfiable

    The file is already fully retrieved; nothing to do.

Archive:  text8.zip
  inflating: text8                   
--2016-08-23 03:29:15--  http://mattmahoney.net/dc/enwik9.zip
Resolving mattmahoney.net (mattmahoney.net)... 98.139.135.129
Connecting to mattmahoney.net (mattmahoney.net)|98.139.135.129|:80... connected.
HTTP request sent, awaiting response... 416 Requested Range Not Satisfiable

    The file is already fully retrieved; nothing to do.

Archive:  enwik9.zip
  inflating: enwik9                  

Train models

For training the models yourself, you'll need to have both Gensim and FastText set up on your machine.

In [2]:
MODELS_DIR = 'models/'
!mkdir -p {MODELS_DIR}

lr = 0.05
dim = 100
ws = 5
epoch = 5
minCount = 5
neg = 5
loss = 'ns'
t = 1e-4

from gensim.models import Word2Vec
from gensim.models.word2vec import Text8Corpus

# Same values as used for fastText training above
params = {
    'alpha': lr,
    'size': dim,
    'window': ws,
    'iter': epoch,
    'min_count': minCount,
    'sample': t,
    'sg': 1,
    'hs': 0,
    'negative': neg
}

def train_models(corpus_file, output_name):
    output_file = '{:s}_ft'.format(output_name)
    if not os.path.isfile(os.path.join(MODELS_DIR, '{:s}.vec'.format(output_file))):
        print('Training fasttext on {:s} corpus..'.format(corpus_file))
        %time !{FT_HOME}fasttext skipgram -input {corpus_file} -output {MODELS_DIR+output_file}  -lr {lr} -dim {dim} -ws {ws} -epoch {epoch} -minCount {minCount} -neg {neg} -loss {loss} -t {t}
    else:
        print('\nUsing existing model file {:s}.vec'.format(output_file))
        
    output_file = '{:s}_ft_no_ng'.format(output_name)
    if not os.path.isfile(os.path.join(MODELS_DIR, '{:s}.vec'.format(output_file))):
        print('\nTraining fasttext on {:s} corpus (without char n-grams)..'.format(corpus_file))
        %time !{FT_HOME}fasttext skipgram -input {corpus_file} -output {MODELS_DIR+output_file}  -lr {lr} -dim {dim} -ws {ws} -epoch {epoch} -minCount {minCount} -neg {neg} -loss {loss} -t {t} -maxn 0
    else:
        print('\nUsing existing model file {:s}.vec'.format(output_file))
        
    output_file = '{:s}_gs'.format(output_name)
    if not os.path.isfile(os.path.join(MODELS_DIR, '{:s}.vec'.format(output_file))):
        print('\nTraining word2vec on {:s} corpus..'.format(corpus_file))
        
        # Text8Corpus class for reading space-separated words file
        %time gs_model = Word2Vec(Text8Corpus(corpus_file), **params); gs_model
        # Direct local variable lookup doesn't work properly with magic statements (%time)
        locals()['gs_model'].save_word2vec_format(os.path.join(MODELS_DIR, '{:s}.vec'.format(output_file)))
        print('\nSaved gensim model as {:s}.vec'.format(output_file))
    else:
        print('\nUsing existing model file {:s}.vec'.format(output_file))

evaluation_data = {}
train_models('brown_corp.txt', 'brown')
Training fasttext on brown_corp.txt corpus..
Read 1M words
Progress: 100.0%  words/sec/thread: 42250  lr: 0.000001  loss: 2.291523  eta: 0h0m 
Train time: 46.000000 sec
CPU times: user 1.03 s, sys: 152 ms, total: 1.18 s
Wall time: 54.3 s

Training fasttext on brown_corp.txt corpus (without char n-grams)..
Read 1M words
Progress: 100.0%  words/sec/thread: 78714  lr: 0.000001  loss: 2.360077  eta: 0h0m 
Train time: 25.000000 sec
CPU times: user 908 ms, sys: 124 ms, total: 1.03 s
Wall time: 32.5 s

Training word2vec on brown_corp.txt corpus..
CPU times: user 41.4 s, sys: 0 ns, total: 41.4 s
Wall time: 18 s

Saved gensim model as brown_gs.vec
In [3]:
train_models(corpus_file='text8', output_name='text8')
Training fasttext on text8 corpus..
Read 17M words
Progress: 100.0%  words/sec/thread: 33709  lr: 0.000001  loss: 1.833047  eta: 0h0m 
Train time: 926.000000 sec
CPU times: user 42.1 s, sys: 4.63 s, total: 46.8 s
Wall time: 15min 42s

Training fasttext on text8 corpus (without char n-grams)..
Read 17M words
Progress: 100.0%  words/sec/thread: 66780  lr: 0.000001  loss: 1.878257  eta: 0h0m 
Train time: 479.000000 sec
CPU times: user 29.8 s, sys: 3.46 s, total: 33.3 s
Wall time: 8min 16s

Training word2vec on text8 corpus..
CPU times: user 16min 16s, sys: 3.91 s, total: 16min 20s
Wall time: 6min 42s

Saved gensim model as text8_gs.vec
In [4]:
train_models(corpus_file='text9', output_name='text9')
Training fasttext on text9 corpus..
Read 124M words
Progress: 100.0%  words/sec/thread: 34687  lr: 0.000001  loss: 1.617828  eta: 0h0m 
Train time: 6519.000000 sec
CPU times: user 5min 38s, sys: 37.4 s, total: 6min 16s
Wall time: 1h 49min 49s

Training fasttext on text9 corpus (without char n-grams)..
Read 124M words
Progress: 100.0%  words/sec/thread: 67558  lr: 0.000001  loss: 1.645820  eta: 0h0m 
Train time: 3465.000000 sec
CPU times: user 3min 32s, sys: 23.4 s, total: 3min 56s
Wall time: 59min 10s

Training word2vec on text9 corpus..
CPU times: user 2h 6min 34s, sys: 36.6 s, total: 2h 7min 10s
Wall time: 53min 38s

Saved gensim model as text9_gs.vec

Comparisons

In [15]:
# download the file questions-words.txt to be used for comparing word embeddings
!wget https://raw.githubusercontent.com/tmikolov/word2vec/master/questions-words.txt
--2016-08-21 10:31:16--  https://raw.githubusercontent.com/tmikolov/word2vec/master/questions-words.txt
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 151.101.100.133
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|151.101.100.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 603955 (590K) [text/plain]
Saving to: ‘questions-words.txt’

100%[======================================>] 6,03,955     554KB/s   in 1.1s   

2016-08-21 10:31:17 (554 KB/s) - ‘questions-words.txt’ saved [603955/603955]

Once you have downloaded or trained the models and downloaded questions-words.txt, you're ready to run the comparison.

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

# Training times in seconds
evaluation_data['brown'] = [(18, 54.3, 32.5)]
evaluation_data['text8'] = [(402, 942, 496)]
evaluation_data['text9'] = [(3218, 6589, 3550)]

def print_accuracy(model, questions_file):
    print('Evaluating...\n')
    acc = model.accuracy(questions_file)

    sem_correct = sum((len(acc[i]['correct']) for i in range(5)))
    sem_total = sum((len(acc[i]['correct']) + len(acc[i]['incorrect'])) for i in range(5))
    sem_acc = 100*float(sem_correct)/sem_total
    print('\nSemantic: {:d}/{:d}, Accuracy: {:.2f}%'.format(sem_correct, sem_total, sem_acc))
    
    syn_correct = sum((len(acc[i]['correct']) for i in range(5, len(acc)-1)))
    syn_total = sum((len(acc[i]['correct']) + len(acc[i]['incorrect'])) for i in range(5,len(acc)-1))
    syn_acc = 100*float(syn_correct)/syn_total
    print('Syntactic: {:d}/{:d}, Accuracy: {:.2f}%\n'.format(syn_correct, syn_total, syn_acc))
    return (sem_acc, syn_acc)

word_analogies_file = 'questions-words.txt'
accuracies = []
print('\nLoading Gensim embeddings')
brown_gs = Word2Vec.load_word2vec_format(MODELS_DIR + 'brown_gs.vec')
print('Accuracy for Word2Vec:')
accuracies.append(print_accuracy(brown_gs, word_analogies_file))

print('\nLoading FastText embeddings')
brown_ft = Word2Vec.load_word2vec_format(MODELS_DIR + 'brown_ft.vec')
print('Accuracy for FastText (with n-grams):')
accuracies.append(print_accuracy(brown_ft, word_analogies_file))
2016-08-23 10:32:14,416 : INFO : loading projection weights from models/brown_gs.vec
Loading Gensim embeddings
2016-08-23 10:32:15,938 : INFO : loaded (15173, 100) matrix from models/brown_gs.vec
2016-08-23 10:32:16,060 : INFO : precomputing L2-norms of word weight vectors
Accuracy for Word2Vec:
Evaluating...

2016-08-23 10:32:16,746 : INFO : family: 20.3% (37/182)
2016-08-23 10:32:21,631 : INFO : gram1-adjective-to-adverb: 0.0% (0/702)
2016-08-23 10:32:22,421 : INFO : gram2-opposite: 0.0% (0/132)
2016-08-23 10:32:29,033 : INFO : gram3-comparative: 2.1% (22/1056)
2016-08-23 10:32:29,698 : INFO : gram4-superlative: 0.0% (0/210)
2016-08-23 10:32:33,248 : INFO : gram5-present-participle: 0.3% (2/650)
2016-08-23 10:32:37,647 : INFO : gram7-past-tense: 0.9% (11/1260)
2016-08-23 10:32:41,156 : INFO : gram8-plural: 5.4% (30/552)
2016-08-23 10:32:42,238 : INFO : gram9-plural-verbs: 0.9% (3/342)
2016-08-23 10:32:42,240 : INFO : total: 2.1% (105/5086)
2016-08-23 10:32:42,247 : INFO : loading projection weights from models/brown_ft.vec
Semantic: 37/182, Accuracy: 20.33%
Syntactic: 68/4904, Accuracy: 1.39%


Loading FastText embeddings
2016-08-23 10:32:45,542 : INFO : loaded (15173, 100) matrix from models/brown_ft.vec
2016-08-23 10:32:45,636 : INFO : precomputing L2-norms of word weight vectors
Accuracy for FastText (with n-grams):
Evaluating...

2016-08-23 10:32:46,240 : INFO : family: 15.4% (28/182)
2016-08-23 10:32:50,291 : INFO : gram1-adjective-to-adverb: 73.4% (515/702)
2016-08-23 10:32:50,752 : INFO : gram2-opposite: 81.1% (107/132)
2016-08-23 10:32:56,089 : INFO : gram3-comparative: 58.7% (620/1056)
2016-08-23 10:32:57,538 : INFO : gram4-superlative: 67.1% (141/210)
2016-08-23 10:33:00,715 : INFO : gram5-present-participle: 65.8% (428/650)
2016-08-23 10:33:06,537 : INFO : gram7-past-tense: 13.9% (175/1260)
2016-08-23 10:33:10,094 : INFO : gram8-plural: 55.4% (306/552)
2016-08-23 10:33:11,211 : INFO : gram9-plural-verbs: 74.0% (253/342)
2016-08-23 10:33:11,212 : INFO : total: 50.6% (2573/5086)
Semantic: 28/182, Accuracy: 15.38%
Syntactic: 2545/4904, Accuracy: 51.90%

The accuracy takes an optional parameter restrict_vocab, which limits the vocabulary of model considered for fast approximate evaluation (default is 30000).

Word2Vec embeddings seem to be slightly better than fastText embeddings at the semantic tasks, while the fastText embeddings do significantly better on the syntactic analogies. Makes sense, since fastText embeddings are trained for understanding morphological nuances, and most of the syntactic analogies are morphology based.

Let me explain that better.

According to the paper [1], embeddings for words are represented by the sum of their n-gram embeddings. This is meant to be useful for morphologically rich languages - so theoretically, the embedding for apparently would include information from both character n-grams apparent and ly (as well as other n-grams), and the n-grams would combine in a simple, linear manner. This is very similar to what most of our syntactic tasks look like.

Example analogy:

amazing amazingly calm calmly

This analogy is marked correct if:

embedding(amazing) - embedding(amazingly) = embedding(calm) - embedding(calmly)

Both these subtractions would result in a very similar set of remaining ngrams. No surprise the fastText embeddings do extremely well on this.

Let's do a small test to validate this hypothesis - fastText differs from word2vec only in that it uses char n-gram embeddings as well as the actual word embedding in the scoring function to calculate scores and then likelihoods for each word, given a context word. In case char n-gram embeddings are not present, this reduces (atleast theoretically) to the original word2vec model. This can be implemented by setting 0 for the max length of char n-grams for fastText.

In [15]:
print('Loading FastText embeddings')
brown_ft_no_ng = Word2Vec.load_word2vec_format(MODELS_DIR + 'brown_ft_no_ng.vec')
print('Accuracy for FastText (without n-grams):')
accuracies.append(print_accuracy(brown_ft_no_ng, word_analogies_file))
evaluation_data['brown'] += [[acc[0] for acc in accuracies], [acc[1] for acc in accuracies]]
2016-08-23 10:33:57,845 : INFO : loading projection weights from models/brown_ft_no_ng.vec
Loading FastText embeddings
2016-08-23 10:33:59,447 : INFO : loaded (15173, 100) matrix from models/brown_ft_no_ng.vec
2016-08-23 10:33:59,538 : INFO : precomputing L2-norms of word weight vectors
Accuracy for FastText (without n-grams):
Evaluating...

2016-08-23 10:34:00,435 : INFO : family: 17.0% (31/182)
2016-08-23 10:34:02,610 : INFO : gram1-adjective-to-adverb: 0.1% (1/702)
2016-08-23 10:34:03,012 : INFO : gram2-opposite: 0.8% (1/132)
2016-08-23 10:34:06,230 : INFO : gram3-comparative: 2.9% (31/1056)
2016-08-23 10:34:06,876 : INFO : gram4-superlative: 0.5% (1/210)
2016-08-23 10:34:09,941 : INFO : gram5-present-participle: 1.1% (7/650)
2016-08-23 10:34:14,521 : INFO : gram7-past-tense: 0.7% (9/1260)
2016-08-23 10:34:16,379 : INFO : gram8-plural: 5.3% (29/552)
2016-08-23 10:34:17,978 : INFO : gram9-plural-verbs: 2.0% (7/342)
2016-08-23 10:34:17,980 : INFO : total: 2.3% (117/5086)
Semantic: 31/182, Accuracy: 17.03%
Syntactic: 86/4904, Accuracy: 1.75%

A-ha! The results for FastText with no n-grams and Word2Vec look a lot more similar (as they should) - the differences could easily result from differences in implementation between fastText and Gensim, and randomization. Especially telling is that the semantic accuracy for FastText has improved slightly after removing n-grams, while the syntactic accuracy has taken a giant dive. Our hypothesis that the char n-grams result in better performance on syntactic analogies seems fair. It also seems possible that char n-grams hurt semantic accuracy a little. However, the brown corpus is too small to be able to draw any definite conclusions - the accuracies seem to vary significantly over different runs.

Let's try with a larger corpus now - text8 (collection of wiki articles). I'm also curious about the impact on semantic accuracy - for models trained on the brown corpus, the difference in the semantic accuracy and the accuracy values themselves are too small to be conclusive. Hopefully a larger corpus helps, and the text8 corpus likely has a lot more information about capitals, currencies, cities etc, which should be relevant to the semantic tasks.

In [16]:
accuracies = []
print('Loading Gensim embeddings')
text8_gs = Word2Vec.load_word2vec_format(MODELS_DIR + 'text8_gs.vec')
print('Accuracy for word2vec:')
accuracies.append(print_accuracy(text8_gs, word_analogies_file))

print('Loading FastText embeddings (with n-grams)')
text8_ft = Word2Vec.load_word2vec_format(MODELS_DIR + 'text8_ft.vec')
print('Accuracy for FastText (with n-grams):')
accuracies.append(print_accuracy(text8_ft, word_analogies_file))

print('Loading FastText embeddings')
text8_ft_no_ng = Word2Vec.load_word2vec_format(MODELS_DIR + 'text8_ft_no_ng.vec')
print('Accuracy for FastText (without n-grams):')
accuracies.append(print_accuracy(text8_ft_no_ng, word_analogies_file))

evaluation_data['text8'] += [[acc[0] for acc in accuracies], [acc[1] for acc in accuracies]]
2016-08-23 10:34:18,050 : INFO : loading projection weights from models/text8_gs.vec
Loading Gensim embeddings
2016-08-23 10:34:25,739 : INFO : loaded (71290, 100) matrix from models/text8_gs.vec
2016-08-23 10:34:25,927 : INFO : precomputing L2-norms of word weight vectors
Accuracy for word2vec:
Evaluating...

2016-08-23 10:34:28,601 : INFO : capital-common-countries: 67.0% (339/506)
2016-08-23 10:34:40,531 : INFO : capital-world: 45.3% (658/1452)
2016-08-23 10:34:42,060 : INFO : currency: 20.9% (56/268)
2016-08-23 10:34:53,593 : INFO : city-in-state: 23.2% (365/1571)
2016-08-23 10:34:57,068 : INFO : family: 59.2% (181/306)
2016-08-23 10:35:03,180 : INFO : gram1-adjective-to-adverb: 14.3% (108/756)
2016-08-23 10:35:06,024 : INFO : gram2-opposite: 11.4% (35/306)
2016-08-23 10:35:17,130 : INFO : gram3-comparative: 50.2% (633/1260)
2016-08-23 10:35:21,890 : INFO : gram4-superlative: 30.8% (156/506)
2016-08-23 10:35:29,381 : INFO : gram5-present-participle: 26.6% (264/992)
2016-08-23 10:35:40,566 : INFO : gram6-nationality-adjective: 76.9% (1054/1371)
2016-08-23 10:35:51,950 : INFO : gram7-past-tense: 29.2% (389/1332)
2016-08-23 10:36:01,483 : INFO : gram8-plural: 52.0% (516/992)
2016-08-23 10:36:07,185 : INFO : gram9-plural-verbs: 30.9% (201/650)
2016-08-23 10:36:07,187 : INFO : total: 40.4% (4955/12268)
2016-08-23 10:36:07,194 : INFO : loading projection weights from models/text8_ft.vec
Semantic: 1599/4103, Accuracy: 38.97%
Syntactic: 3356/8165, Accuracy: 41.10%

Loading FastText embeddings (with n-grams)
2016-08-23 10:36:15,551 : INFO : loaded (71290, 100) matrix from models/text8_ft.vec
2016-08-23 10:36:15,761 : INFO : precomputing L2-norms of word weight vectors
Accuracy for FastText (with n-grams):
Evaluating...

2016-08-23 10:36:20,631 : INFO : capital-common-countries: 57.5% (291/506)
2016-08-23 10:36:31,906 : INFO : capital-world: 44.6% (647/1452)
2016-08-23 10:36:34,651 : INFO : currency: 17.2% (46/268)
2016-08-23 10:36:49,641 : INFO : city-in-state: 18.0% (272/1511)
2016-08-23 10:36:51,402 : INFO : family: 51.0% (156/306)
2016-08-23 10:36:59,474 : INFO : gram1-adjective-to-adverb: 76.3% (577/756)
2016-08-23 10:37:01,244 : INFO : gram2-opposite: 62.7% (192/306)
2016-08-23 10:37:14,023 : INFO : gram3-comparative: 60.5% (762/1260)
2016-08-23 10:37:18,805 : INFO : gram4-superlative: 55.3% (280/506)
2016-08-23 10:37:28,822 : INFO : gram5-present-participle: 55.6% (552/992)
2016-08-23 10:37:39,523 : INFO : gram6-nationality-adjective: 93.1% (1276/1371)
2016-08-23 10:37:52,889 : INFO : gram7-past-tense: 38.0% (506/1332)
2016-08-23 10:38:01,988 : INFO : gram8-plural: 87.8% (871/992)
2016-08-23 10:38:07,999 : INFO : gram9-plural-verbs: 57.5% (374/650)
2016-08-23 10:38:08,013 : INFO : total: 55.7% (6802/12208)
2016-08-23 10:38:08,032 : INFO : loading projection weights from models/text8_ft_no_ng.vec
Semantic: 1412/4043, Accuracy: 34.92%
Syntactic: 5390/8165, Accuracy: 66.01%

Loading FastText embeddings
2016-08-23 10:38:16,212 : INFO : loaded (71290, 100) matrix from models/text8_ft_no_ng.vec
2016-08-23 10:38:16,423 : INFO : precomputing L2-norms of word weight vectors
Accuracy for FastText (without n-grams):
Evaluating...

2016-08-23 10:38:19,529 : INFO : capital-common-countries: 71.7% (363/506)
2016-08-23 10:38:31,840 : INFO : capital-world: 49.1% (713/1452)
2016-08-23 10:38:33,427 : INFO : currency: 25.0% (67/268)
2016-08-23 10:38:45,850 : INFO : city-in-state: 22.3% (337/1511)
2016-08-23 10:38:47,491 : INFO : family: 56.5% (173/306)
2016-08-23 10:38:55,203 : INFO : gram1-adjective-to-adverb: 19.0% (144/756)
2016-08-23 10:38:56,777 : INFO : gram2-opposite: 13.7% (42/306)
2016-08-23 10:39:07,522 : INFO : gram3-comparative: 45.1% (568/1260)
2016-08-23 10:39:12,079 : INFO : gram4-superlative: 28.3% (143/506)
2016-08-23 10:39:21,022 : INFO : gram5-present-participle: 22.3% (221/992)
2016-08-23 10:39:32,463 : INFO : gram6-nationality-adjective: 78.6% (1078/1371)
2016-08-23 10:39:45,108 : INFO : gram7-past-tense: 32.0% (426/1332)
2016-08-23 10:39:52,383 : INFO : gram8-plural: 54.6% (542/992)
2016-08-23 10:39:57,698 : INFO : gram9-plural-verbs: 23.2% (151/650)
2016-08-23 10:39:57,707 : INFO : total: 40.7% (4968/12208)
Semantic: 1653/4043, Accuracy: 40.89%
Syntactic: 3315/8165, Accuracy: 40.60%

With the text8 corpus, we observe a similar pattern. Semantic accuracy falls by a small but significant amount when n-grams are included in FastText, while FastText with n-grams performs far better on the syntactic analogies. FastText without n-grams are largely similar to Word2Vec.

My hypothesis for semantic accuracy being lower for the FastText-with-ngrams model is that most of the words in the semantic analogies are standalone words and are unrelated to their morphemes (eg: father, mother, France, Paris), hence inclusion of the char n-grams into the scoring function actually makes the embeddings worse.

This trend is observed in the original paper too where the performance of embeddings with n-grams is worse on semantic tasks than both word2vec cbow and skipgram models.

Let's do a quick comparison on an even larger corpus - text9

In [17]:
accuracies = []
print('Loading Gensim embeddings')
text9_gs = Word2Vec.load_word2vec_format(MODELS_DIR + 'text9_gs.vec')
print('Accuracy for word2vec:')
accuracies.append(print_accuracy(text9_gs, word_analogies_file))

print('Loading FastText embeddings (with n-grams)')
text9_ft = Word2Vec.load_word2vec_format(MODELS_DIR + 'text9_ft.vec')
print('Accuracy for FastText (with n-grams):')
accuracies.append(print_accuracy(text9_ft, word_analogies_file))

print('Loading FastText embeddings')
text9_ft_no_ng = Word2Vec.load_word2vec_format(MODELS_DIR + 'text9_ft_no_ng.vec')
print('Accuracy for FastText (without n-grams):')
accuracies.append(print_accuracy(text9_ft_no_ng, word_analogies_file))

evaluation_data['text9'] += [[acc[0] for acc in accuracies], [acc[1] for acc in accuracies]]
2016-08-23 10:40:04,786 : INFO : loading projection weights from models/text9_gs.vec
Loading Gensim embeddings
2016-08-23 10:40:30,884 : INFO : loaded (218316, 100) matrix from models/text9_gs.vec
Accuracy for word2vec:
Evaluating...

2016-08-23 10:40:31,775 : INFO : precomputing L2-norms of word weight vectors
2016-08-23 10:40:34,765 : INFO : capital-common-countries: 84.2% (426/506)
2016-08-23 10:40:48,132 : INFO : capital-world: 74.6% (1221/1636)
2016-08-23 10:40:49,018 : INFO : currency: 21.9% (39/178)
2016-08-23 10:41:06,134 : INFO : city-in-state: 41.8% (891/2133)
2016-08-23 10:41:10,016 : INFO : family: 67.1% (255/380)
2016-08-23 10:41:15,973 : INFO : gram1-adjective-to-adverb: 28.7% (233/812)
2016-08-23 10:41:19,624 : INFO : gram2-opposite: 33.3% (114/342)
2016-08-23 10:41:30,799 : INFO : gram3-comparative: 67.1% (846/1260)
2016-08-23 10:41:35,522 : INFO : gram4-superlative: 51.8% (262/506)
2016-08-23 10:41:42,860 : INFO : gram5-present-participle: 51.8% (482/930)
2016-08-23 10:41:58,254 : INFO : gram6-nationality-adjective: 91.2% (1250/1371)
2016-08-23 10:42:11,573 : INFO : gram7-past-tense: 49.3% (657/1332)
2016-08-23 10:42:21,335 : INFO : gram8-plural: 75.2% (794/1056)
2016-08-23 10:42:27,170 : INFO : gram9-plural-verbs: 47.4% (333/702)
2016-08-23 10:42:27,172 : INFO : total: 59.4% (7803/13144)
2016-08-23 10:42:27,179 : INFO : loading projection weights from models/text9_ft.vec
Semantic: 2832/4833, Accuracy: 58.60%
Syntactic: 4971/8311, Accuracy: 59.81%

Loading FastText embeddings (with n-grams)
2016-08-23 10:42:52,360 : INFO : loaded (218316, 100) matrix from models/text9_ft.vec
Accuracy for FastText (with n-grams):
Evaluating...

2016-08-23 10:42:53,500 : INFO : precomputing L2-norms of word weight vectors
2016-08-23 10:42:58,763 : INFO : capital-common-countries: 84.6% (428/506)
2016-08-23 10:43:14,010 : INFO : capital-world: 71.8% (1174/1636)
2016-08-23 10:43:16,987 : INFO : currency: 22.5% (40/178)
2016-08-23 10:43:38,323 : INFO : city-in-state: 33.0% (703/2133)
2016-08-23 10:43:41,400 : INFO : family: 59.5% (226/380)
2016-08-23 10:43:49,864 : INFO : gram1-adjective-to-adverb: 55.5% (451/812)
2016-08-23 10:43:53,560 : INFO : gram2-opposite: 50.6% (173/342)
2016-08-23 10:44:04,678 : INFO : gram3-comparative: 72.9% (919/1260)
2016-08-23 10:44:09,314 : INFO : gram4-superlative: 58.1% (294/506)
2016-08-23 10:44:16,302 : INFO : gram5-present-participle: 55.7% (518/930)
2016-08-23 10:44:29,878 : INFO : gram6-nationality-adjective: 94.5% (1296/1371)
2016-08-23 10:44:42,831 : INFO : gram7-past-tense: 48.0% (640/1332)
2016-08-23 10:44:52,464 : INFO : gram8-plural: 83.5% (882/1056)
2016-08-23 10:44:58,566 : INFO : gram9-plural-verbs: 55.7% (391/702)
2016-08-23 10:44:58,576 : INFO : total: 61.9% (8135/13144)
2016-08-23 10:44:58,589 : INFO : loading projection weights from models/text9_ft_no_ng.vec
Semantic: 2571/4833, Accuracy: 53.20%
Syntactic: 5564/8311, Accuracy: 66.95%

Loading FastText embeddings
2016-08-23 10:45:24,650 : INFO : loaded (218316, 100) matrix from models/text9_ft_no_ng.vec
Accuracy for FastText (without n-grams):
Evaluating...

2016-08-23 10:45:25,641 : INFO : precomputing L2-norms of word weight vectors
2016-08-23 10:45:29,053 : INFO : capital-common-countries: 87.7% (444/506)
2016-08-23 10:45:44,191 : INFO : capital-world: 77.3% (1264/1636)
2016-08-23 10:45:45,169 : INFO : currency: 19.7% (35/178)
2016-08-23 10:46:03,906 : INFO : city-in-state: 39.2% (836/2133)
2016-08-23 10:46:08,098 : INFO : family: 62.9% (239/380)
2016-08-23 10:46:14,351 : INFO : gram1-adjective-to-adverb: 30.7% (249/812)
2016-08-23 10:46:18,093 : INFO : gram2-opposite: 32.7% (112/342)
2016-08-23 10:46:28,567 : INFO : gram3-comparative: 70.2% (885/1260)
2016-08-23 10:46:33,014 : INFO : gram4-superlative: 49.4% (250/506)
2016-08-23 10:46:39,864 : INFO : gram5-present-participle: 48.1% (447/930)
2016-08-23 10:46:52,864 : INFO : gram6-nationality-adjective: 93.1% (1277/1371)
2016-08-23 10:47:03,413 : INFO : gram7-past-tense: 47.2% (629/1332)
2016-08-23 10:47:15,591 : INFO : gram8-plural: 78.3% (827/1056)
2016-08-23 10:47:21,488 : INFO : gram9-plural-verbs: 43.0% (302/702)
2016-08-23 10:47:21,491 : INFO : total: 59.3% (7796/13144)
Semantic: 2818/4833, Accuracy: 58.31%
Syntactic: 4978/8311, Accuracy: 59.90%

In [23]:
%matplotlib inline
import matplotlib.pyplot as plt

def plot(ax, data, corpus_name='brown'):
    width = 0.25
    pos = [(i, i + width, i + 2*width) for i in range(len(data))]
    colors = ['#EE3224', '#F78F1E', '#FFC222']
    acc_ax = ax.twinx()
    # Training time
    ax.bar(pos[0],
            data[0],
            width,
            alpha=0.5,
            color=colors
            )
    # Semantic accuracy
    acc_ax.bar(pos[1],
            data[1],
            width,
            alpha=0.5,
            color=colors
            )

    # Syntactic accuracy
    acc_ax.bar(pos[2],
            data[2],
            width,
            alpha=0.5,
            color=colors
            )

    ax.set_ylabel('Training time (s)')
    acc_ax.set_ylabel('Accuracy (%)')
    ax.set_title(corpus_name)

    acc_ax.set_xticks([p[0] + 1.5 * width for p in pos])
    acc_ax.set_xticklabels(['Training Time', 'Semantic Accuracy', 'Syntactic Accuracy'])

    # Proxy plots for adding legend correctly
    proxies = [ax.bar([0], [0], width=0, color=c, alpha=0.5)[0] for c in colors]
    models = ('Gensim', 'FastText', 'FastText (no-ngrams)')
    ax.legend((proxies), models, loc='upper left')
    
    ax.set_xlim(pos[0][0]-width, pos[-1][0]+width*4)
    ax.set_ylim([0, max(data[0])*1.1] )
    acc_ax.set_ylim([0, max(data[1] + data[2])*1.1] )

    plt.grid()

# Plotting the bars
fig = plt.figure(figsize=(10,15))
for corpus, subplot in zip(sorted(evaluation_data.keys()), [311, 312, 313]):
    ax = fig.add_subplot(subplot)
    plot(ax, evaluation_data[corpus], corpus)

plt.show()

The results from text9 seem to confirm our hypotheses so far. Briefly summarising the main points -

  1. FastText models with n-grams do significantly better on syntactic tasks, because of the syntactic questions being related to morphology of the words
  2. Both Gensim word2vec and the fastText model with no n-grams do slightly better on the semantic tasks, presumably because words from the semantic questions are standalone words and unrelated to their char n-grams
  3. In general, the performance of the models seems to get closer with the increasing corpus size. However, this might possibly be due to the size of the model staying constant at 100, and a larger model size for large corpora might result in higher performance gains.
  4. The semantic accuracy for all models increases significantly with the increase in corpus size.
  5. However, the increase in syntactic accuracy from the increase in corpus size for the n-gram FastText model is lower (in both relative and absolute terms). This could possibly indicate that advantages gained by incorporating morphological information could be less significant in case of larger corpus sizes (the corpuses used in the original paper seem to indicate this too)
  6. Training times for gensim are slightly lower than the fastText no-ngram model, and significantly lower than the n-gram variant. This is quite impressive considering fastText is implemented in C++ and Gensim in Python (with calls to low-level BLAS routines for much of the heavy lifting). You could read this post for more details regarding word2vec optimisation in Gensim. Note that these times include importing any dependencies and serializing the models to disk, and not just the training times.

Conclusions

These preliminary results seem to indicate fastText embeddings are significantly better than word2vec at encoding syntactic information. This is expected, since most syntactic analogies are morphology based, and the char n-gram approach of fastText takes such information into account. The original word2vec model seems to perform better on semantic tasks, since words in semantic analogies are unrelated to their char n-grams, and the added information from irrelevant char n-grams worsens the embeddings. It'd be interesting to see how transferable these embeddings are for different kinds of tasks by comparing their performance in a downstream supervised task.

References