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

Improve FastText documentation #2353

merged 40 commits into from
Jan 29, 2019

Conversation

mpenkov
Copy link
Collaborator

@mpenkov mpenkov commented Jan 25, 2019

  • Updated fasttext module docstring to include better examples, design documentation
  • Improved tutorial Jupyter notebook

doctest complains about vocabulary and training continuation
@menshikh-iv menshikh-iv self-requested a review January 25, 2019 12:19
... ['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.

... ]
>>> 'rocket' in model.wv
False
>>> model.train(new_sentences, total_examples=len(sentences), epochs=model.epochs)
Copy link
Owner

@piskvorky piskvorky Jan 25, 2019

Choose a reason for hiding this comment

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

Why total_example=len(sentences)? Even if correct, it looks strange… would be my first question as a user.
Let's provide the answer here in the docs why total_examples is needed, what is its role.

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 correct. I agree that is confusing. The docstring for the train function attempts to clarify the situation.

Personally, I think if neither total_examples and total_words are specified, we should try to determine sensible defaults by looking at e.g. len(sentences). WDYT @menshikh-iv ?

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.

Are you sure? I read the linked docs and still don't get why it's not len(new_sentences).

Please include some top-level intuition here. A short sentence on why this parameter is mandatory, and what should be its value, because it looks really strange and superfluous. +1 for sensible defaults.

Copy link
Collaborator

Choose a reason for hiding this comment

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

For train() to manage alpha correctly, and show meaningful progress estimates, it needs a good estimate of the size of the supplied-corpus - even when the corpus (as an iterable) may not self-report its length. In the typical case where the same corpus was just surveyed for its vocabulary, this value should be handy. (In the Word2Vec case, the count from the vocab-scan is cached inside the model for later consultation – unsure if the FT paths do this.) In the case where other/new data is being supplied, the caller should supply the right counts for the current data.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@gojomo If the corpus does self-report its length though, should we use that instead? If yes, which should we do:

  1. total_examples = len(corpus)
  2. total_words = len(corpus)

If the corpus does not self-report its length, then we could raise an exception with a heplful message. WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

If it's able to self-report its length, in count of texts, then yes, that would work as total_examples. But because streaming a corpus from an iterable is the motivating case for this interface, relying on that inside the method seems inappropriate to me. Leaving it the responsibility of the caller seems fine to me – and if they're lucky enough to have a corpus that reports its own length, they can supply it easily.

Copy link
Collaborator Author

@mpenkov mpenkov Jan 26, 2019

Choose a reason for hiding this comment

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

Also, while we're talking about simplifying the API, what do you think about removing the sentences and corpus_file parameters from the constructor? Currently, we have an inconsistency: in the constructor, we just pass sentences/corpus_file without total_examples and total_words parameters. In the train function, we include those additional parameters.

Instead of passing sentences in the constructor, the user can pass them in separately via the train function.

Pros:

  • Simpler constructor. There will be less parameters.
  • Easier to understand and consistent API.

Cons:

  • Getting a trained model now takes two steps instead of one (instantiate, train). Not sure how much of a con this is given that the train function is more powerful than the constructor anyway - it has parameters that the constructor doesn't.

@menshikh-iv @piskvorky @gojomo What do you think?

The same thing also applies to the callbacks parameters.

Copy link
Collaborator Author

@mpenkov mpenkov Jan 26, 2019

Choose a reason for hiding this comment

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

But because streaming a corpus from an iterable is the motivating case for this interface, relying on that inside the method seems inappropriate to me.

@gojomo Why do you think it is inappropriate? We could do something like this:

if total_examples or total_words:
    pass  # nothing to do here
elif sentences and hasattr(sentences, '__len__'):  # could also check for callable, if necessary
    total_examples = len(sentences)
elif data_corpus and hasattr(data_corpus, '__len__'):
    total_examples = len(data_corpus)
else:
    raise ValueError(
        'unable to infer total_examples or total_words from the training source, '
        'please pass one of them explicitly'
    )

It looks ugly, but it allows to user to do something like:

model.train(sentences)

instead of

model.train(sentences, num_examples=len(sentences))

I feel the former is more Pythonic.

Finally, I think having two separate keyword parameters for the input is confusing for the user. In my opinion, it would look a lot simpler if we unified the two parameters, and dealt with untangling them in the implementation.

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.

@mpenkov yes, I'd consider that "sensible" defaults. Thanks.

Agreed on unifying iter/epoch. How would that work? Keep both as acceptable input (internally unify to one), no deprecations? Deprecate one? Which one?

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.

@piskvorky OK. I think it's worth dealing with API refactoring in a separate PR, for two reasons:

  1. Such changes risk introducing regressions, and I'd rather not have them together with the relatively safe documentation changes.
  2. We've already spent much effort refactoring FastText, and it may be prudent to wait for things to stabilize (e.g. fix introduced regressions) before proceeding.

To answer your question, I think it makes sense to deprecate iter from the constructor. It's a poor name for a parameter, for three reasons:

  1. It masks the built-in iter function.
  2. It isn't obvious.
  3. It's inconsistent. We use epochs everywhere else outside of the constructor.

gensim/models/fasttext.py Show resolved Hide resolved
gensim/models/fasttext.py Outdated Show resolved Hide resolved
gensim/models/fasttext.py Outdated Show resolved Hide resolved
gensim/models/fasttext.py Outdated Show resolved Hide resolved
gensim/models/fasttext.py Outdated Show resolved Hide resolved
Copy link
Contributor

@menshikh-iv menshikh-iv left a comment

Choose a reason for hiding this comment

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

Good start overall, I note several issues with rendering
1.

:py:class:`Model`

should be

:class:`~gensim.models._fasttext_bin.Model`

please always use full path for any reference (class/method/function)
2.

:py:mod:`gensim.models.fasttext`

should be

:mod:`gensim.models.fasttext`
:py:class:`FastTextVocab`

should be

:class:`~gensim.models.FastTextVocab`

Also, you can always check how page rendered, just click "Details" near CircleCI and open "Artifacts" tab, this contains all rendered html, like https://circleci.com/gh/RaRe-Technologies/gensim/2108#artifacts/containers/0

@piskvorky
Copy link
Owner

piskvorky commented Jan 25, 2019

@mpenkov looks good, thanks! Do the docstring examples now cover all the confusion we saw from users (mailing list, github)? Any important workflows left, undocumented / under-documented?

@mpenkov
Copy link
Collaborator Author

mpenkov commented Jan 26, 2019

@mpenkov looks good, thanks! Do the docstring examples now cover all the confusion we saw from users (mailing list, github)? Any important workflows left, undocumented / under-documented?

It's hard for me to claim we've resolved all the confusion, because I haven't thoroughly read all the feedback from the users. Nevertheless, this PR addresses (or begins to address, owing to the ongoing discussions above) several key problems that we identified in the task description:

  1. Unnecessary parameters in the constructor (ongoing, plan to continue unti we reach consensus).
  2. IO problems (key, vec, etc).
  3. Lack of examples for training continuation.
  4. Lack of design documentation.

@menshikh-iv Have I missed anything?

gensim/models/_fasttext_bin.py Show resolved Hide resolved
gensim/models/keyedvectors.py Outdated Show resolved Hide resolved
gensim/models/fasttext.py Show resolved Hide resolved
gensim/models/fasttext.py Show resolved Hide resolved
gensim/models/fasttext.py Show resolved Hide resolved
gensim/models/fasttext.py Show resolved Hide resolved
gensim/models/fasttext.py Outdated Show resolved Hide resolved
gensim/models/fasttext.py Show resolved Hide resolved
gensim/models/fasttext.py Show resolved Hide resolved
@mpenkov mpenkov changed the title [WIP] Improve Fasttext documentation Improve Fasttext documentation Jan 27, 2019
@menshikh-iv
Copy link
Contributor

@piskvorky LGTM, wdyt?

>>> total_words = model3.corpus_total_words # number of words in the corpus
>>> model3.train(corpus_file=corpus_file, total_examples=total_examples, total_words=total_words, epochs=5)

The model needs the `total_examples` and `total_words` parameters in order to
Copy link
Owner

@piskvorky piskvorky Jan 28, 2019

Choose a reason for hiding this comment

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

Does the model really need both? IIRC, it only needs one of those, no?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You're right, I updated the example.

... def __iter__(self):
... with open(datapath('crime-and-punishment.txt')) as fin:
... for line in fin:
... yield line.lower().strip().split(" ")
Copy link
Owner

@piskvorky piskvorky Jan 28, 2019

Choose a reason for hiding this comment

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

Just split() (or we'll be left with newlines and all sorts of unicode "whitespace").

Also, I'd be careful with such examples, since people love to copy-paste (and this is really not a good way to tokenize -- even gensim.utils.tokenize is better).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

OK, I've improved the example with gensim.utils.tokenize


.. sourcecode:: pycon

>>> class MyIter:
Copy link
Owner

Choose a reason for hiding this comment

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

MyIter(object) (we still support Python 2).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

OK, done.

@piskvorky
Copy link
Owner

piskvorky commented Jan 28, 2019

This looks great! I wish all PRs were this meticulous (and useful).

I have some things I'd want to add to the docs, but it's nitpicking at this point. Probably best if we merge and I'll go over it myself here and there. Nothing critical.

@@ -769,9 +940,17 @@ def __contains__(self, word):
def load_fasttext_format(cls, model_file, encoding='utf8', full_model=True):
"""Load the input-hidden weight matrix from Facebook's native fasttext `.bin` and `.vec` output files.

By default, this function loads the full model.
A full model allows continuing training with more data, but also consumes more RAM and takes longer to load.
If you do not need to continue training and only wish the work with the already-trained embeddings, use `partial=False` for faster loading and to save RAM.
Copy link
Contributor

Choose a reason for hiding this comment

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

too long line (should be <120) + full_model=False, not partial=False @mpenkov

@menshikh-iv menshikh-iv changed the title Improve Fasttext documentation Improve FastText documentation Jan 29, 2019
@menshikh-iv
Copy link
Contributor

Awesome works, great @mpenkov 🔥

@menshikh-iv menshikh-iv merged commit 80406c2 into piskvorky:develop Jan 29, 2019
@mpenkov mpenkov deleted the doc-improv branch June 26, 2020 01:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants