Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve FastText documentation #2353

Merged
merged 40 commits into from
Jan 29, 2019
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
18f5302
WIP: doco improvements
mpenkov Jan 24, 2019
b57a086
more doco
mpenkov Jan 25, 2019
5fa0c9f
Merge remote-tracking branch 'upstream/develop' into doc-improv
mpenkov Jan 25, 2019
d66f55c
flake8-docs updates
mpenkov Jan 25, 2019
9696657
adding fixmes
mpenkov Jan 25, 2019
3019bea
minor fixup
mpenkov Jan 26, 2019
74b740c
review response
mpenkov Jan 26, 2019
09ab630
Remove magic constant
mpenkov Jan 26, 2019
2e728cb
deprecate the iter parameter to the FastText constructor
mpenkov Jan 26, 2019
c435a8e
minor documentation fixes
mpenkov Jan 26, 2019
c688877
review response: use absolute references
mpenkov Jan 26, 2019
677679c
review response
mpenkov Jan 26, 2019
29c5210
fix unit test
mpenkov Jan 26, 2019
044d699
Revert "deprecate the iter parameter to the FastText constructor"
mpenkov Jan 26, 2019
f9df136
Revert "fix unit test"
mpenkov Jan 26, 2019
cdc727a
more documentation improvements
mpenkov Jan 27, 2019
4ea3f06
comment out pesky import
mpenkov Jan 27, 2019
e532d62
fix typo
mpenkov Jan 27, 2019
931d3d7
improve tutorial notebook
mpenkov Jan 27, 2019
177c712
minor documentation update
mpenkov Jan 27, 2019
bd83886
flake8-docs
mpenkov Jan 27, 2019
11fabca
more doco fixes
mpenkov Jan 27, 2019
2d490f0
fix example
mpenkov Jan 27, 2019
9b6f8bb
git rm docs/fasttext-notes.md
mpenkov Jan 27, 2019
9b5e161
review response: include _fasttext_bin in docs
mpenkov Jan 27, 2019
6aa013a
review response: make examples more readable
mpenkov Jan 27, 2019
7d2b562
review response: remove blank line
mpenkov Jan 27, 2019
25b24c7
review response: add emphasis
mpenkov Jan 27, 2019
b4e8405
review response: add comment
mpenkov Jan 27, 2019
1fc9bf2
review response: add example
mpenkov Jan 27, 2019
72ec312
review response: remove redundant line
mpenkov Jan 27, 2019
29c4faf
review response: update comment
mpenkov Jan 28, 2019
74410fc
Update gensim/models/fasttext.py
piskvorky Jan 28, 2019
a3456a4
review response: improve examples
mpenkov Jan 28, 2019
96eab08
clarify example
mpenkov Jan 28, 2019
ff72185
review response: improve example
mpenkov Jan 28, 2019
9140cf6
review response: improve tokenization in example
mpenkov Jan 28, 2019
31c79c3
flake8
mpenkov Jan 29, 2019
2f479ca
fix long lines
mpenkov Jan 29, 2019
c48a7f3
fixup: use correct parameter name
mpenkov Jan 29, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions gensim/models/_fasttext_bin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
"""Load models from the native binary format released by Facebook.

The main entry point is the :py:func:`load` function.
It returns a :py:class:`Model` namedtuple containing everything loaded from the binary.

Examples
--------

Expand Down
144 changes: 130 additions & 14 deletions gensim/models/fasttext.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
This module contains a fast native C implementation of Fasttext with Python interfaces. It is **not** only a wrapper
around Facebook's implementation.

This module supports loading models trained with Facebook's fastText implementation.
It also supports continuing training from such models.

For a tutorial see `this notebook
<https://github.com/RaRe-Technologies/gensim/blob/develop/docs/notebooks/FastText_Tutorial.ipynb>`_.

Expand All @@ -31,6 +34,15 @@
>>> from gensim.models import FastText
>>>
>>> model = FastText(common_texts, size=4, window=3, min_count=1, iter=10)
>>> sentences = [
... ['computer', 'artificial', 'intelligence'],
... ['artificial', 'trees'],
... ['human', 'intelligence'],
... ['artificial', 'graph'],
... ['intelligence'],
... ['artificial', 'intelligence', 'system']
... ]
>>> model.train(sentences, total_examples=len(sentences), epochs=model.epochs)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this model.epochs coming from? The model instantiation above shows no such variable.

Copy link
Collaborator Author

@mpenkov mpenkov Jan 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the epochs parameter is optional, so not included during instantiation.

Unfortunately, there is also some confusion about its name: the FastText constructor uses iter to specify the number of epochs, whereas the superclass uses the proper name epochs.

The presence of the epochs parameter to the train function (which seems to override the one set in the constructor) also complicates matters.

Copy link
Owner

@piskvorky piskvorky Jan 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. If it's optional, let's not use it in train. Or if we use it in train, let's instantiate it explicitly. This neither-here-nor-there example is confusing ("where is this value come from?").

Regarding iter / epochs -- can you please rename it to epochs, consistently? I remember some discussion around this (cc @menshikh-iv @gojomo ), but can't imagine why we'd want both. At most we could support iter for a while as an alias, but with a clear deprecation warning.

This is a perfect opportunity to clean up some of the API mess, rather than piling on.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree regarding the cleanup. My preference would be to leave epochs/iter out of the constructor. The model doesn't need that parameter until training time.

Copy link
Owner

@piskvorky piskvorky Jan 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Models in Gensim generally allow the trained_model = Constructor(params_including_training_params) pattern. So breaking that could be confusing to existing users (and a big change to backward incompatibility).

I'm not totally opposed though, especially if we still allow ctr params for a while with "deprecated" warnings. The API needs a clean up, and now is a good time.

Not a big priority though, and the documentation examples can already promote the instantiate then train, as 2 steps pattern.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not just training parameters you need to include in the constructor. It's also parameters for vocabulary creation. So you're managing at least 3 sets of separate parameters, 2 of which are duplicated by other methods of the class.

Copy link
Owner

@piskvorky piskvorky Jan 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we should promote them as separate steps in docs. Question is, do we deprecate (certainly not remove) them from ctr?

Copy link
Collaborator Author

@mpenkov mpenkov Jan 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand your motivation in not removing them (backward compatibility). Unfortunately, the current mess won't go away until we remove things like this.

I think the first step should be to deprecate them. After a while, we can remove them, perhaps in time for a major release.

If we want a one-liner way to instantiate and train, we can always write a pure function and promote that. That should make it easier for users to cut over to the cleaner API.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, deprecation is what I suggest.


Persist a model to disk with:

Expand All @@ -41,7 +53,51 @@
>>> fname = get_tmpfile("fasttext.model")
>>>
>>> model.save(fname)
>>> model = FastText.load(fname) # you can continue training with the loaded model!
>>> model = FastText.load(fname)

Once loaded, such models behave identically to those created from scratch.
For example, you can continue training the loaded model:

.. sourcecode:: pycon

>>> import numpy as np
>>> old_computer = np.copy(model.wv['computer']) # Grab the existing vector for this word
>>> new_sentences = [
... ['computers', 'expensive'],
... ['computer', 'chess', 'players', 'stronger', 'than', 'humans'],
... ['computers', 'are', 'everywhere'],
... ]
>>> model.train(new_sentences, total_examples=len(new_sentences), epochs=model.epochs)
>>> new_computer = model.wv['computer']
>>> # FIXME: why is this True??
mpenkov marked this conversation as resolved.
Show resolved Hide resolved
>>> np.allclose(old_computer, new_computer, atol=1e-4)
False

You can also load models trained with Facebook's fastText implementation:

.. sourcecode:: pycon

>>> from gensim.test.utils import datapath
>>> cap_path = datapath("crime-and-punishment.bin")
>>> # Partial model: loads quickly, uses less RAM, but cannot continue training
>>> fb_partial = FastText.load_fasttext_format(cap_path, full_model=False)
>>> # Full model: loads slowly, consumes RAM, but can continue training (see below)
>>> fb_full = FastText.load_fasttext_format(cap_path, full_model=True)

Once loaded, such models behave identically to those trained from scratch.
You may continue training them on new data:

.. sourcecode:: pycon

>>> 'computer' in fb_full.wv.vocab # New word, currently out of vocab
False
>>> old_computer = np.copy(fb_full.wv['computer']) # Calculate current vectors
>>> fb_full.train(sentences, total_examples=len(sentences), epochs=model.epochs)
>>> fb_full.train(new_sentences, total_examples=len(new_sentences), epochs=model.epochs)
piskvorky marked this conversation as resolved.
Show resolved Hide resolved
>>> new_computer = fb_full.wv['computer']
>>> # FIXME: why is this True??
>>> np.allclose(old_computer, new_computer, atol=1e-4) # Vector has changed, model has learnt something
False

Retrieve word-vector for vocab and out-of-vocab word:

Expand Down Expand Up @@ -85,6 +141,33 @@

>>> analogies_result = model.wv.evaluate_word_analogies(datapath('questions-words.txt'))

Implementation Notes
--------------------

These notes may help developers navigate our fastText implementation.
The implementation is split across several submodules:

- :py:mod:`gensim.models.fasttext`: This module. Contains FastText-specific functionality only.
- :py:mod:`gensim.models.keyedvectors`: Implements both generic and FastText-specific functionality.
- :py:mod:`gensim.models.word2vec`: Contains implementations for the vocabulary
and the trainables for FastText.
- :py:mod:`gensim.models.base_any2vec`: Contains implementations for the base
classes, including functionality such as callbacks, logging.
- :py:mod:`gensim.models.utils_any2vec`: Wrapper over Cython extensions.
- :py:mod:`gensim.utils`: Implements model I/O (loading and saving)

Our implementation relies heavily on inheritance.
It consists of several important classes:

- :py:class:`~gensim.models.word2vec.Word2VecVocab`: the vocabulary.
Keeps track of all the unique words, sometimes discarding the extremely rare ones.
This is sometimes called the Dictionary within Gensim.
- :py:class:`~gensim.models.keyedvectors.FastTextKeyedVectors`: the vectors.
Once training is complete, this class is sufficient for calculating embeddings.
- :py:class:`FastTextTrainables`: the underlying neural network. The implementation
uses this class to *learn* the word embeddings.
- :py:class:`FastText`: ties everything together.

"""

import logging
Expand Down Expand Up @@ -535,7 +618,7 @@ def build_vocab(self, sentences=None, corpus_file=None, update=False, progress_p
def _set_train_params(self, **kwargs):
#
# We need the wv.buckets_word member to be initialized in order to
# continue training. The _clear_post_train method destroys this
# continue training. The _clear_post_train method destroys this
# variable, so we reinitialize it here, if needed.
#
# The .old_vocab_len and .old_hash2index_len members are set only to
Expand Down Expand Up @@ -771,7 +854,11 @@ def load_fasttext_format(cls, model_file, encoding='utf8', full_model=True):

Notes
------
Due to limitations in the FastText API, you cannot continue training with a model loaded this way.
Facebook provides both `.vec` and `.bin` files with their modules.
menshikh-iv marked this conversation as resolved.
Show resolved Hide resolved
The former contains human-readable vectors.
The latter contains machine-readable vectors along with other model parameters.
This function effectively ignores `.vec` output file, since that file is redundant.
It only needs the `.bin` file.

Parameters
----------
Expand All @@ -783,12 +870,12 @@ def load_fasttext_format(cls, model_file, encoding='utf8', full_model=True):
encoding : str, optional
Specifies the file encoding.
full_model : boolean, optional
If False, skips loading the hidden output matrix. This saves a fair bit
If False, skips loading the hidden output matrix. This saves a fair bit
of CPU time and RAM, but prevents training continuation.

Returns
-------
:class: `~gensim.models.fasttext.FastText`
gensim.models.fasttext.FastText
The loaded model.

"""
Expand Down Expand Up @@ -856,7 +943,7 @@ def load(cls, *args, **kwargs):

if not hasattr(model.wv, 'compatible_hash'):
logger.warning(
"This older model was trained with a buggy hash function. "
"This older model was trained with a buggy hash function. "
"The model will continue to work, but consider training it "
"from scratch."
)
Expand All @@ -877,15 +964,44 @@ def accuracy(self, questions, restrict_vocab=30000, most_similar=None, case_inse
return self.wv.accuracy(questions, restrict_vocab, most_similar, case_insensitive)


#
# Keep for backward compatibility.
#
class FastTextVocab(Word2VecVocab):
"""This is a redundant class. It exists only to maintain backwards compatibility
with older gensim versions."""
pass


class FastTextTrainables(Word2VecTrainables):
"""Represents the inner shallow neural network used to train :class:`~gensim.models.fasttext.FastText`."""
"""Represents the inner shallow neural network used to train :class:`~gensim.models.fasttext.FastText`.

Mostly inherits from its parent (:py:class:`gensim.models.word2vec.Word2VecTrainables`).
Adds logic for calculating and maintaining ngram weights.

Attributes
----------

menshikh-iv marked this conversation as resolved.
Show resolved Hide resolved
hashfxn : function
Used for randomly initializing weights. Defaults to the built-in hash()
layer1_size : int
The size of the inner layer of the NN. Equal to the vector dimensionality. Set in the :py:class:`gensim.models.word2vec.Word2VecTrainables` constructor.
seed : float
The random generator seed used in reset_weights and update_weights
syn1 : numpy.array
The inner layer of the NN. Each row corresponds to a term in the vocabulary. Columns correspond to weights of the inner layer. There are layer1_size such weights. Set in the reset_weights and update_weights methods, only if hierarchical sampling is used.
syn1neg : numpy.array
Similar to syn1, but only set if negative sampling is used.
vectors_lockf : numpy.array
A one-dimensional array with one element for each term in the vocab. Set in reset_weights to an array of ones.
vectors_vocab_lockf : numpy.array
Similar to vectors_vocab_lockf, ones(len(model.trainables.vectors), dtype=REAL)
vectors_ngrams_lockf : numpy.array
np.ones((self.bucket, wv.vector_size), dtype=REAL)

Notes
-----

The lockf stuff looks like it gets used by the fast C implementation.
piskvorky marked this conversation as resolved.
Show resolved Hide resolved

"""
def __init__(self, vector_size=100, seed=1, hashfxn=hash, bucket=2000000):
super(FastTextTrainables, self).__init__(
vector_size=vector_size, seed=seed, hashfxn=hashfxn)
Expand All @@ -899,17 +1015,17 @@ def __init__(self, vector_size=100, seed=1, hashfxn=hash, bucket=2000000):
# 2. vectors_ngrams_lockf
#
# These are both 2D matrices of shapes equal to the shapes of
# wv.vectors_vocab and wv.vectors_ngrams. So, each row corresponds to
# wv.vectors_vocab and wv.vectors_ngrams. So, each row corresponds to
# a vector, and each column corresponds to a dimension within that
# vector.
#
# Lockf stands for "lock factor": zero values suppress learning, one
# values enable it. Interestingly, the vectors_vocab_lockf and
# values enable it. Interestingly, the vectors_vocab_lockf and
# vectors_ngrams_lockf seem to be used only by the C code in
# fasttext_inner.pyx.
#
# The word2vec implementation also uses vectors_lockf: in that case,
# it's a 1D array, with a real number for each vector. The FastText
# it's a 1D array, with a real number for each vector. The FastText
# implementation inherits this vectors_lockf attribute but doesn't
# appear to use it.
#
Expand Down Expand Up @@ -987,7 +1103,7 @@ def _load_fasttext_format(model_file, encoding='utf-8', full_model=True):
encoding : str, optional
Specifies the file encoding.
full_model : boolean, optional
If False, skips loading the hidden output matrix. This saves a fair bit
If False, skips loading the hidden output matrix. This saves a fair bit
of CPU time and RAM, but prevents training continuation.
menshikh-iv marked this conversation as resolved.
Show resolved Hide resolved

Returns
Expand Down
10 changes: 8 additions & 2 deletions gensim/models/keyedvectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -1943,14 +1943,19 @@ class FastTextKeyedVectors(WordEmbeddingsKeyedVectors):
If True, uses the Facebook-compatible hash function instead of the
Gensim backwards-compatible hash function.

Some important attributes:
menshikh-iv marked this conversation as resolved.
Show resolved Hide resolved

Attributes
----------
vectors_vocab : np.array
A vector for each entity in the vocabulary.
Each row corresponds to a vector for an entity in the vocabulary.
Columns correspond to vector dimensions.
vectors_vocab_norm : np.array
Same as vectors_vocab, but the vectors are L2 normalized.
vectors_ngrams : np.array
A vector for each ngram across all entities in the vocabulary.
Each row is a vector that corresponds to a bucket.
Columns correspond to vector dimensions.
vectors_ngrams_norm : np.array
Same as vectors_ngrams, but the vectors are L2 normalized.
Under some conditions, may actually be the same matrix as
Expand All @@ -1964,7 +1969,8 @@ class FastTextKeyedVectors(WordEmbeddingsKeyedVectors):
bucket to an index, and then indexing into vectors_ngrams (in other
words, vectors_ngrams[hash2index[hash_fn(ngram) % bucket]].
num_ngram_vectors : int
TODO
The number of vectors that correspond to ngrams, as opposed to terms
(full words).

"""
def __init__(self, vector_size, min_n, max_n, bucket, compatible_hash):
Expand Down