From 97cef997032c3222645ebdc898c199a7b63e5395 Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Sat, 30 Apr 2022 08:22:46 +0900 Subject: [PATCH 01/34] bump timeout for wheel building step it seems to occasionally go over 10 minutes, e.g. https://github.com/RaRe-Technologies/gensim/runs/6231204549 --- .github/workflows/build-wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index cab6a16641..a9f0fd7ef2 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -37,7 +37,7 @@ jobs: - name: Check Sphinx Gallery cache run: python docs/src/check_gallery.py build: - timeout-minutes: 30 + timeout-minutes: 35 runs-on: ${{ matrix.os }} defaults: run: From 8e9e09ca32f177dfa0b7d0e6253f0b5374fb37ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radim=20=C5=98eh=C5=AF=C5=99ek?= Date: Sun, 1 May 2022 18:25:37 +0200 Subject: [PATCH 02/34] bump up dev version --- docs/src/conf.py | 2 +- gensim/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/conf.py b/docs/src/conf.py index 168d4cf58e..1789e7353f 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -63,7 +63,7 @@ # The short X.Y version. version = '4.2.0' # The full version, including alpha/beta/rc tags. -release = '4.2.0' +release = '4.2.1.dev0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/gensim/__init__.py b/gensim/__init__.py index b5f915a3ed..2ac8d16b66 100644 --- a/gensim/__init__.py +++ b/gensim/__init__.py @@ -4,7 +4,7 @@ """ -__version__ = '4.2.0' +__version__ = '4.2.1.dev0' import logging diff --git a/setup.py b/setup.py index e3ee0c3bdb..8091591fe0 100644 --- a/setup.py +++ b/setup.py @@ -334,7 +334,7 @@ def run(self): setup( name='gensim', - version='4.2.0', + version='4.2.1.dev0', description='Python framework for fast Vector Space Modelling', long_description=LONG_DESCRIPTION, From 9b316bcc113f34350fb8eb5f904b2d9ad9f29486 Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Mon, 2 May 2022 21:30:33 +0900 Subject: [PATCH 03/34] disable py3.6 builds in travis.yml --- .travis.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index f1c9f05e99..11e06f2f63 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,14 +33,6 @@ matrix: # See .github/workflows/build-wheels.yml for a discussion of why we # handle numpy versions explicitly. # - - os: linux - env: - - MB_PYTHON_VERSION=3.6 - # - # scipy 1.7.0 wheels not available for Py3.6, so we have to build using - # an older version. - # - - BUILD_DEPENDS="numpy==1.19.2 scipy==1.5.3" - os: linux env: - MB_PYTHON_VERSION=3.7 From 400d90654fb0dce55facf566354b91c0f5175731 Mon Sep 17 00:00:00 2001 From: Paul Wise Date: Thu, 5 May 2022 14:43:33 +0800 Subject: [PATCH 04/34] Allow overriding the Cython version requirement (#3323) Use an environment variable for this since it is often easier to set in a build wrapper rather than trying to override command-line options in the right layer of a multi-layer build wrapper and it also requires a lot less code to do the override. This will be useful for using alpha versions of Cython or old versions of Cython provided by the distros or specific versions that fix certain bugs. --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 8091591fe0..d41f668ee0 100644 --- a/setup.py +++ b/setup.py @@ -320,6 +320,9 @@ def run(self): # CYTHON_STR = 'Cython==0.29.28' +# Allow overriding the Cython version requirement +CYTHON_STR = os.environ.get('GENSIM_CYTHON_REQUIRES', CYTHON_STR) + install_requires = [ NUMPY_STR, 'scipy >= 0.18.1', From 5a19dd0407577001bcc177166582da7a5a4e5f70 Mon Sep 17 00:00:00 2001 From: Paul Wise Date: Thu, 5 May 2022 14:45:02 +0800 Subject: [PATCH 05/34] Update Python module MANIFEST (#3343) Drop COPYING.LESSER as it was moved to COPYING in 2016. Drop ez_setup.py as it was removed in 2018. Switch doc2vec_inner.c to doc2vec_inner.cpp, as it has always been using language=c++. Suggested-by: setup.py build Fixes: commit beb04ea1f8f9c438b0a40aa4cbbd955ea065f84f Fixes: commit 2891861d77f9eff2dc703214c099240ef227b7da Fixes: commit 1aa11bbaa7beba9b3068cb2e2d04a500ebd31f30 --- MANIFEST.in | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 8aa14d25b8..cc4323533f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,8 +2,6 @@ recursive-include gensim/test/test_data * include README.md include CHANGELOG.md include COPYING -include COPYING.LESSER -include ez_setup.py include gensim/models/voidptr.h include gensim/models/stdint_wrapper.h @@ -16,7 +14,7 @@ include gensim/models/word2vec_corpusfile.cpp include gensim/models/word2vec_corpusfile.pyx include gensim/models/word2vec_corpusfile.pxd -include gensim/models/doc2vec_inner.c +include gensim/models/doc2vec_inner.cpp include gensim/models/doc2vec_inner.pyx include gensim/models/doc2vec_inner.pxd include gensim/models/doc2vec_corpusfile.cpp From eeb7e8662d5350efe68fa14db08b02d273735af9 Mon Sep 17 00:00:00 2001 From: Paul Wise Date: Thu, 5 May 2022 21:41:54 +0800 Subject: [PATCH 06/34] Clean up references to `Morfessor`, `tox` and `gensim.models.wrappers` (#3345) * Drop reference to Morfessor It was dropped from usage in 2022. Fixes: commit acce8a21377d1f599f8b9ec56b10321c91d4109d * Drop references to tox Usage of tox was dropped in 2022. Where necessary, replace tox commands with the current equivalent. Fixes: commit 3ce81a44cc59b3d77edee043ee14050f611008df * Drop direct and indirect references to gensim.modules.wrapper(s) They were all removed in 2021. TestDtmModel escaped being noticed via test failures because all the tests it runs are skipped without the DTM_PATH environment variable being set. Fixes: commit a21d9cc768598640f38e4bd03d368f8712a9aa77 --- .gitignore | 1 - .travis.yml | 2 +- CONTRIBUTING.md | 8 ++-- gensim/models/callbacks.py | 18 ++++----- gensim/models/coherencemodel.py | 3 +- gensim/models/keyedvectors.py | 3 +- gensim/test/test_dtm.py | 70 --------------------------------- gensim/utils.py | 2 +- release/upload_docs.sh | 3 +- 9 files changed, 16 insertions(+), 94 deletions(-) delete mode 100644 gensim/test/test_dtm.py diff --git a/.gitignore b/.gitignore index 019e1812f7..8853bd683a 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,6 @@ Thumbs.db # Other # ######### -.tox/ .cache/ .project .pydevproject diff --git a/.travis.yml b/.travis.yml index 11e06f2f63..b0b952766d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ env: # them here for now. They'll get picked up by the multibuild stuff # running in multibuild/common_utils.sh. # - - TEST_DEPENDS="pytest mock cython nmslib pyemd testfixtures Morfessor==2.0.2a4 python-levenshtein==0.12.0 visdom==0.1.8.9 scikit-learn" + - TEST_DEPENDS="pytest mock cython nmslib pyemd testfixtures python-levenshtein==0.12.0 visdom==0.1.8.9 scikit-learn" matrix: # diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0eeb90591b..09f2f5a870 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,11 +20,9 @@ Also, please check the [Gensim FAQ](https://github.com/RaRe-Technologies/gensim/ - For windows: `pip install -e .[test-win]` 5. Implement your changes 6. Check that everything's OK in your branch: - - Check it for PEP8: `tox -e flake8` - - Build its documentation (works only for MacOS/Linux): `tox -e docs` (documentation stored in `docs/src/_build`) - - Run unit tests: `tox -e py{version}-{os}`, for example `tox -e py35-linux` or `tox -e py36-win` where - - `{version}` is one of `35`, `36` - - `{os}` is either `win` or `linux` + - Check it for PEP8: `flake8 --ignore E12,W503 --max-line-length 120 --show-source gensim` + - Build its documentation (works only for MacOS/Linux): `make -C docs/src html` (documentation stored in `docs/src/_build`) + - Run unit tests: `pytest -v gensim/test` 7. Add files, commit and push: `git add ... ; git commit -m "my commit message"; git push origin my-feature` 8. [Create a PR](https://help.github.com/articles/creating-a-pull-request/) on Github. Write a **clear description** for your PR, including all the context and relevant information, such as: - The issue that you fixed, e.g. `Fixes #123` diff --git a/gensim/models/callbacks.py b/gensim/models/callbacks.py index 42f250cb91..c5560441af 100644 --- a/gensim/models/callbacks.py +++ b/gensim/models/callbacks.py @@ -234,9 +234,7 @@ def get_value(self, **kwargs): Key word arguments to override the object's internal attributes. One of the following parameters are expected: - * `model` - pre-trained topic model of type :class:`~gensim.models.ldamodel.LdaModel`, or one - of its wrappers, such as :class:`~gensim.models.wrappers.ldamallet.LdaMallet` or - :class:`~gensim.models.wrappers.ldavowpalwabbit.LdaVowpalWabbit`. + * `model` - pre-trained topic model of type :class:`~gensim.models.ldamodel.LdaModel`. * `topics` - list of tokenized topics. Returns @@ -290,10 +288,8 @@ def get_value(self, **kwargs): ---------- **kwargs Key word arguments to override the object's internal attributes. - A trained topic model is expected using the 'model' key. This can be of type - :class:`~gensim.models.ldamodel.LdaModel`, or one of its wrappers, such as - :class:`~gensim.models.wrappers.ldamallet.LdaMallet` or - :class:`~gensim.models.wrapper.ldavowpalwabbit.LdaVowpalWabbit`. + A trained topic model is expected using the 'model' key. + This must be of type :class:`~gensim.models.ldamodel.LdaModel`. Returns ------- @@ -354,8 +350,8 @@ def get_value(self, **kwargs): ---------- **kwargs Key word arguments to override the object's internal attributes. - Two models of type :class:`~gensim.models.ldamodelLdaModel` or its wrappers are expected using the keys - `model` and `other_model`. + Two models of type :class:`~gensim.models.ldamodelLdaModel` + are expected using the keys `model` and `other_model`. Returns ------- @@ -424,8 +420,8 @@ def get_value(self, **kwargs): ---------- **kwargs Key word arguments to override the object's internal attributes. - Two models of type :class:`~gensim.models.ldamodel.LdaModel` or its wrappers are expected using the keys - `model` and `other_model`. + Two models of type :class:`~gensim.models.ldamodel.LdaModel` + are expected using the keys `model` and `other_model`. Returns ------- diff --git a/gensim/models/coherencemodel.py b/gensim/models/coherencemodel.py index b3c89640a7..d6df976153 100644 --- a/gensim/models/coherencemodel.py +++ b/gensim/models/coherencemodel.py @@ -132,8 +132,7 @@ def __init__(self, model=None, topics=None, texts=None, corpus=None, dictionary= model : :class:`~gensim.models.basemodel.BaseTopicModel`, optional Pre-trained topic model, should be provided if topics is not provided. Currently supports :class:`~gensim.models.ldamodel.LdaModel`, - :class:`~gensim.models.ldamulticore.LdaMulticore`, :class:`~gensim.models.wrappers.ldamallet.LdaMallet` and - :class:`~gensim.models.wrappers.ldavowpalwabbit.LdaVowpalWabbit`. + :class:`~gensim.models.ldamulticore.LdaMulticore`. Use `topics` parameter to plug in an as yet unsupported model. topics : list of list of str, optional List of tokenized topics, if this is preferred over model - dictionary should be provided. diff --git a/gensim/models/keyedvectors.py b/gensim/models/keyedvectors.py index 8f86b807f2..6fb9e329d8 100644 --- a/gensim/models/keyedvectors.py +++ b/gensim/models/keyedvectors.py @@ -9,8 +9,7 @@ and various similarity look-ups. Since trained word vectors are independent from the way they were trained (:class:`~gensim.models.word2vec.Word2Vec`, -:class:`~gensim.models.fasttext.FastText`, -:class:`~gensim.models.wrappers.varembed.VarEmbed` etc), they can be represented by a standalone structure, +:class:`~gensim.models.fasttext.FastText` etc), they can be represented by a standalone structure, as implemented in this module. The structure is called "KeyedVectors" and is essentially a mapping between *keys* diff --git a/gensim/test/test_dtm.py b/gensim/test/test_dtm.py deleted file mode 100644 index 0e57d15e7e..0000000000 --- a/gensim/test/test_dtm.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -Automated tests for DTM/DIM model -""" - - -import logging -from subprocess import CalledProcessError -import gensim -import os -import unittest -from gensim import corpora -from gensim.test.utils import datapath - - -class TestDtmModel(unittest.TestCase): - - def setUp(self): - self.time_slices = [3, 7] - self.corpus = corpora.mmcorpus.MmCorpus(datapath('dtm_test.mm')) - self.id2word = corpora.Dictionary.load(datapath('dtm_test.dict')) - # first you need to setup the environment variable $DTM_PATH for the dtm executable file - self.dtm_path = os.environ.get('DTM_PATH', None) - if not self.dtm_path: - self.skipTest("$DTM_PATH is not properly set up.") - - def test_dtm(self): - if self.dtm_path is not None: - model = gensim.models.wrappers.DtmModel( - self.dtm_path, self.corpus, self.time_slices, num_topics=2, - id2word=self.id2word, model='dtm', initialize_lda=True, - rng_seed=1 - ) - topics = model.show_topics(num_topics=2, times=2, num_words=10) - self.assertEqual(len(topics), 4) - - one_topic = model.show_topic(topicid=1, time=1, topn=10) - self.assertEqual(len(one_topic), 10) - self.assertEqual(one_topic[0][1], u'idexx') - - def test_dim(self): - if self.dtm_path is not None: - model = gensim.models.wrappers.DtmModel( - self.dtm_path, self.corpus, self.time_slices, num_topics=2, - id2word=self.id2word, model='fixed', initialize_lda=True, - rng_seed=1 - ) - topics = model.show_topics(num_topics=2, times=2, num_words=10) - self.assertEqual(len(topics), 4) - - one_topic = model.show_topic(topicid=1, time=1, topn=10) - self.assertEqual(len(one_topic), 10) - self.assertEqual(one_topic[0][1], u'skills') - - # In stderr expect "Error opening file /tmp/a65419_train_out/initial-lda-ss.dat. Failing." - def test_called_process_error(self): - if self.dtm_path is not None: - with self.assertRaises(CalledProcessError): - gensim.models.wrappers.DtmModel( - self.dtm_path, self.corpus, self.time_slices, num_topics=2, - id2word=self.id2word, model='dtm', initialize_lda=False, - rng_seed=1 - ) - - -if __name__ == '__main__': - logging.basicConfig(level=logging.DEBUG) - unittest.main() diff --git a/gensim/utils.py b/gensim/utils.py index 78d64b88e6..0619296888 100644 --- a/gensim/utils.py +++ b/gensim/utils.py @@ -1863,7 +1863,7 @@ def keep_vocab_item(word, count, min_count, trim_rule=None): def check_output(stdout=subprocess.PIPE, *popenargs, **kwargs): r"""Run OS command with the given arguments and return its output as a byte string. - Backported from Python 2.7 with a few minor modifications. Widely used for :mod:`gensim.models.wrappers`. + Backported from Python 2.7 with a few minor modifications. Used in word2vec/glove2word2vec tests. Behaves very similar to https://docs.python.org/2/library/subprocess.html#subprocess.check_output. Examples diff --git a/release/upload_docs.sh b/release/upload_docs.sh index d454eaa157..3bec935a8e 100644 --- a/release/upload_docs.sh +++ b/release/upload_docs.sh @@ -1,3 +1,4 @@ -tox -e compile,docs +python setup.py build_ext --inplace cd docs/src +make html make upload From ded78776284ad7b55b6626191eaa8dcea0dd3db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radim=20=C5=98eh=C5=AF=C5=99ek?= Date: Fri, 6 May 2022 02:53:28 +0200 Subject: [PATCH 07/34] Disable the Gensim 3=>4 warning in docs (#3346) * disable 3=>4 docs notification * replace migration docs by sponsorship link --- docs/src/sphinx_rtd_theme/notification.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/src/sphinx_rtd_theme/notification.html b/docs/src/sphinx_rtd_theme/notification.html index 4b0d7922e1..dd648b861f 100644 --- a/docs/src/sphinx_rtd_theme/notification.html +++ b/docs/src/sphinx_rtd_theme/notification.html @@ -1,6 +1,3 @@
- You're viewing documentation for Gensim 4.0.0. For Gensim 3.8.3, please visit the old Gensim 3.8.3 documentation and Migration Guide. - + Gensim relies on your donations for sustenance. If you like Gensim, please consider donating.
From 62669aef21ae8047c3105d89f0032df81e73b4fa Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Mon, 22 Aug 2022 00:15:45 +0900 Subject: [PATCH 08/34] pin sphinx versions, add explicit gallery_top label (#3383) * pin sphinx versions, add explicit gallery_top label * make flake8 happy again * make flake8 happy again * make flake8 happy again * make flake8 happy again --- docs/src/auto_examples/index.rst | 261 +++++++++++++------------------ docs/src/gallery/README.txt | 2 + docs/src/support.rst | 2 +- gensim/models/doc2vec.py | 3 +- gensim/models/keyedvectors.py | 4 +- gensim/models/ldamodel.py | 3 +- gensim/models/ldaseqmodel.py | 6 +- gensim/test/test_ldamodel.py | 2 +- setup.py | 14 +- 9 files changed, 131 insertions(+), 166 deletions(-) diff --git a/docs/src/auto_examples/index.rst b/docs/src/auto_examples/index.rst index 47819284b2..ca47a1738e 100644 --- a/docs/src/auto_examples/index.rst +++ b/docs/src/auto_examples/index.rst @@ -1,23 +1,23 @@ :orphan: - - -.. _sphx_glr_auto_examples: - Documentation ============= +.. _gallery_top: + We welcome contributions to our documentation via GitHub pull requests, whether it's fixing a typo or authoring an entirely new tutorial or guide. If you're thinking about contributing documentation, please see :ref:`sphx_glr_auto_examples_howtos_run_doc.py`. + .. raw:: html -
+
+.. raw:: html -.. _sphx_glr_auto_examples_core: +
Core Tutorials: New Users Start Here! ------------------------------------- @@ -27,96 +27,82 @@ Understanding this functionality is vital for using gensim effectively. +.. raw:: html + +
+ + .. raw:: html
.. only:: html - .. figure:: /auto_examples/core/images/thumb/sphx_glr_run_core_concepts_thumb.png - :alt: Core Concepts + .. image:: /auto_examples/core/images/thumb/sphx_glr_run_core_concepts_thumb.png + :alt: Core Concepts - :ref:`sphx_glr_auto_examples_core_run_core_concepts.py` + :ref:`sphx_glr_auto_examples_core_run_core_concepts.py` .. raw:: html +
Core Concepts
-.. toctree:: - :hidden: - - /auto_examples/core/run_core_concepts - .. raw:: html
.. only:: html - .. figure:: /auto_examples/core/images/thumb/sphx_glr_run_corpora_and_vector_spaces_thumb.png - :alt: Corpora and Vector Spaces + .. image:: /auto_examples/core/images/thumb/sphx_glr_run_corpora_and_vector_spaces_thumb.png + :alt: Corpora and Vector Spaces - :ref:`sphx_glr_auto_examples_core_run_corpora_and_vector_spaces.py` + :ref:`sphx_glr_auto_examples_core_run_corpora_and_vector_spaces.py` .. raw:: html +
Corpora and Vector Spaces
-.. toctree:: - :hidden: - - /auto_examples/core/run_corpora_and_vector_spaces - .. raw:: html
.. only:: html - .. figure:: /auto_examples/core/images/thumb/sphx_glr_run_topics_and_transformations_thumb.png - :alt: Topics and Transformations + .. image:: /auto_examples/core/images/thumb/sphx_glr_run_topics_and_transformations_thumb.png + :alt: Topics and Transformations - :ref:`sphx_glr_auto_examples_core_run_topics_and_transformations.py` + :ref:`sphx_glr_auto_examples_core_run_topics_and_transformations.py` .. raw:: html +
Topics and Transformations
-.. toctree:: - :hidden: - - /auto_examples/core/run_topics_and_transformations - .. raw:: html
.. only:: html - .. figure:: /auto_examples/core/images/thumb/sphx_glr_run_similarity_queries_thumb.png - :alt: Similarity Queries + .. image:: /auto_examples/core/images/thumb/sphx_glr_run_similarity_queries_thumb.png + :alt: Similarity Queries - :ref:`sphx_glr_auto_examples_core_run_similarity_queries.py` + :ref:`sphx_glr_auto_examples_core_run_similarity_queries.py` .. raw:: html +
Similarity Queries
-.. toctree:: - :hidden: - - /auto_examples/core/run_similarity_queries .. raw:: html -
- - - -.. _sphx_glr_auto_examples_tutorials: +
Tutorials: Learning Oriented Lessons ------------------------------------ @@ -125,180 +111,150 @@ Learning-oriented lessons that introduce a particular gensim feature, e.g. a mod +.. raw:: html + +
+ + .. raw:: html
.. only:: html - .. figure:: /auto_examples/tutorials/images/thumb/sphx_glr_run_word2vec_thumb.png - :alt: Word2Vec Model + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_word2vec_thumb.png + :alt: Word2Vec Model - :ref:`sphx_glr_auto_examples_tutorials_run_word2vec.py` + :ref:`sphx_glr_auto_examples_tutorials_run_word2vec.py` .. raw:: html +
Word2Vec Model
-.. toctree:: - :hidden: - - /auto_examples/tutorials/run_word2vec - .. raw:: html
.. only:: html - .. figure:: /auto_examples/tutorials/images/thumb/sphx_glr_run_doc2vec_lee_thumb.png - :alt: Doc2Vec Model + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_doc2vec_lee_thumb.png + :alt: Doc2Vec Model - :ref:`sphx_glr_auto_examples_tutorials_run_doc2vec_lee.py` + :ref:`sphx_glr_auto_examples_tutorials_run_doc2vec_lee.py` .. raw:: html +
Doc2Vec Model
-.. toctree:: - :hidden: - - /auto_examples/tutorials/run_doc2vec_lee - .. raw:: html
.. only:: html - .. figure:: /auto_examples/tutorials/images/thumb/sphx_glr_run_ensemblelda_thumb.png - :alt: Ensemble LDA + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_ensemblelda_thumb.png + :alt: Ensemble LDA - :ref:`sphx_glr_auto_examples_tutorials_run_ensemblelda.py` + :ref:`sphx_glr_auto_examples_tutorials_run_ensemblelda.py` .. raw:: html +
Ensemble LDA
-.. toctree:: - :hidden: - - /auto_examples/tutorials/run_ensemblelda - .. raw:: html
.. only:: html - .. figure:: /auto_examples/tutorials/images/thumb/sphx_glr_run_fasttext_thumb.png - :alt: FastText Model + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_fasttext_thumb.png + :alt: FastText Model - :ref:`sphx_glr_auto_examples_tutorials_run_fasttext.py` + :ref:`sphx_glr_auto_examples_tutorials_run_fasttext.py` .. raw:: html +
FastText Model
-.. toctree:: - :hidden: - - /auto_examples/tutorials/run_fasttext - .. raw:: html
.. only:: html - .. figure:: /auto_examples/tutorials/images/thumb/sphx_glr_run_annoy_thumb.png - :alt: Fast Similarity Queries with Annoy and Word2Vec + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_annoy_thumb.png + :alt: Fast Similarity Queries with Annoy and Word2Vec - :ref:`sphx_glr_auto_examples_tutorials_run_annoy.py` + :ref:`sphx_glr_auto_examples_tutorials_run_annoy.py` .. raw:: html +
Fast Similarity Queries with Annoy and Word2Vec
-.. toctree:: - :hidden: - - /auto_examples/tutorials/run_annoy - .. raw:: html
.. only:: html - .. figure:: /auto_examples/tutorials/images/thumb/sphx_glr_run_lda_thumb.png - :alt: LDA Model + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_lda_thumb.png + :alt: LDA Model - :ref:`sphx_glr_auto_examples_tutorials_run_lda.py` + :ref:`sphx_glr_auto_examples_tutorials_run_lda.py` .. raw:: html +
LDA Model
-.. toctree:: - :hidden: - - /auto_examples/tutorials/run_lda - .. raw:: html
.. only:: html - .. figure:: /auto_examples/tutorials/images/thumb/sphx_glr_run_wmd_thumb.png - :alt: Word Mover's Distance + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_wmd_thumb.png + :alt: Word Mover's Distance - :ref:`sphx_glr_auto_examples_tutorials_run_wmd.py` + :ref:`sphx_glr_auto_examples_tutorials_run_wmd.py` .. raw:: html +
Word Mover's Distance
-.. toctree:: - :hidden: - - /auto_examples/tutorials/run_wmd - .. raw:: html
.. only:: html - .. figure:: /auto_examples/tutorials/images/thumb/sphx_glr_run_scm_thumb.png - :alt: Soft Cosine Measure + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_scm_thumb.png + :alt: Soft Cosine Measure - :ref:`sphx_glr_auto_examples_tutorials_run_scm.py` + :ref:`sphx_glr_auto_examples_tutorials_run_scm.py` .. raw:: html +
Soft Cosine Measure
-.. toctree:: - :hidden: - - /auto_examples/tutorials/run_scm .. raw:: html -
- - - -.. _sphx_glr_auto_examples_howtos: +
How-to Guides: Solve a Problem ------------------------------ @@ -307,96 +263,82 @@ These **goal-oriented guides** demonstrate how to **solve a specific problem** u +.. raw:: html + +
+ + .. raw:: html
.. only:: html - .. figure:: /auto_examples/howtos/images/thumb/sphx_glr_run_downloader_api_thumb.png - :alt: How to download pre-trained models and corpora + .. image:: /auto_examples/howtos/images/thumb/sphx_glr_run_downloader_api_thumb.png + :alt: How to download pre-trained models and corpora - :ref:`sphx_glr_auto_examples_howtos_run_downloader_api.py` + :ref:`sphx_glr_auto_examples_howtos_run_downloader_api.py` .. raw:: html +
How to download pre-trained models and corpora
-.. toctree:: - :hidden: - - /auto_examples/howtos/run_downloader_api - .. raw:: html
.. only:: html - .. figure:: /auto_examples/howtos/images/thumb/sphx_glr_run_doc_thumb.png - :alt: How to Author Gensim Documentation + .. image:: /auto_examples/howtos/images/thumb/sphx_glr_run_doc_thumb.png + :alt: How to Author Gensim Documentation - :ref:`sphx_glr_auto_examples_howtos_run_doc.py` + :ref:`sphx_glr_auto_examples_howtos_run_doc.py` .. raw:: html +
How to Author Gensim Documentation
-.. toctree:: - :hidden: - - /auto_examples/howtos/run_doc - .. raw:: html
.. only:: html - .. figure:: /auto_examples/howtos/images/thumb/sphx_glr_run_doc2vec_imdb_thumb.png - :alt: How to reproduce the doc2vec 'Paragraph Vector' paper + .. image:: /auto_examples/howtos/images/thumb/sphx_glr_run_doc2vec_imdb_thumb.png + :alt: How to reproduce the doc2vec 'Paragraph Vector' paper - :ref:`sphx_glr_auto_examples_howtos_run_doc2vec_imdb.py` + :ref:`sphx_glr_auto_examples_howtos_run_doc2vec_imdb.py` .. raw:: html +
How to reproduce the doc2vec 'Paragraph Vector' paper
-.. toctree:: - :hidden: - - /auto_examples/howtos/run_doc2vec_imdb - .. raw:: html
.. only:: html - .. figure:: /auto_examples/howtos/images/thumb/sphx_glr_run_compare_lda_thumb.png - :alt: How to Compare LDA Models + .. image:: /auto_examples/howtos/images/thumb/sphx_glr_run_compare_lda_thumb.png + :alt: How to Compare LDA Models - :ref:`sphx_glr_auto_examples_howtos_run_compare_lda.py` + :ref:`sphx_glr_auto_examples_howtos_run_compare_lda.py` .. raw:: html +
How to Compare LDA Models
-.. toctree:: - :hidden: - - /auto_examples/howtos/run_compare_lda .. raw:: html -
- - - -.. _sphx_glr_auto_examples_other: +
Other Resources --------------- @@ -433,27 +375,38 @@ Blog posts, tutorial videos, hackathons and other useful Gensim resources, from - ? `Deep Inverse Regression with Yelp Reviews `__ (Document Classification using Bayesian Inversion and several word2vec models, one for each class) + .. raw:: html -
+
+ +.. raw:: html + +
-.. only :: html +.. toctree:: + :hidden: + :includehidden: - .. container:: sphx-glr-footer - :class: sphx-glr-footer-gallery + /auto_examples/core/index.rst + /auto_examples/tutorials/index.rst + /auto_examples/howtos/index.rst + /auto_examples/other/index.rst - .. container:: sphx-glr-download sphx-glr-download-python +.. only:: html - :download:`Download all examples in Python source code: auto_examples_python.zip ` + .. container:: sphx-glr-footer sphx-glr-footer-gallery + .. container:: sphx-glr-download sphx-glr-download-python + :download:`Download all examples in Python source code: auto_examples_python.zip ` - .. container:: sphx-glr-download sphx-glr-download-jupyter + .. container:: sphx-glr-download sphx-glr-download-jupyter - :download:`Download all examples in Jupyter notebooks: auto_examples_jupyter.zip ` + :download:`Download all examples in Jupyter notebooks: auto_examples_jupyter.zip ` .. only:: html diff --git a/docs/src/gallery/README.txt b/docs/src/gallery/README.txt index 80ab288c48..560c047c25 100644 --- a/docs/src/gallery/README.txt +++ b/docs/src/gallery/README.txt @@ -1,5 +1,7 @@ Documentation ============= +.. _gallery_top: + We welcome contributions to our documentation via GitHub pull requests, whether it's fixing a typo or authoring an entirely new tutorial or guide. If you're thinking about contributing documentation, please see :ref:`sphx_glr_auto_examples_howtos_run_doc.py`. diff --git a/docs/src/support.rst b/docs/src/support.rst index a28f1e0003..514b75b9b1 100644 --- a/docs/src/support.rst +++ b/docs/src/support.rst @@ -11,7 +11,7 @@ Open source support The main communication channel is the free `Gensim mailing list `_. -This is the preferred way to ask for help, report problems and share insights with the community. Newbie questions are perfectly fine, as long as you've read the :ref:`tutorials ` and `FAQ `_. +This is the preferred way to ask for help, report problems and share insights with the community. Newbie questions are perfectly fine, as long as you've read the :ref:`tutorials ` and `FAQ `_. FAQ and some useful snippets of code are maintained on GitHub: https://github.com/RARE-Technologies/gensim/wiki/Recipes-&-FAQ. diff --git a/gensim/models/doc2vec.py b/gensim/models/doc2vec.py index 20a739f64a..d76be81f9b 100644 --- a/gensim/models/doc2vec.py +++ b/gensim/models/doc2vec.py @@ -1137,7 +1137,8 @@ def __iter__(self): class TaggedLineDocument: def __init__(self, source): - """Iterate over a file that contains documents: one line = :class:`~gensim.models.doc2vec.TaggedDocument` object. + """Iterate over a file that contains documents: + one line = :class:`~gensim.models.doc2vec.TaggedDocument` object. Words are expected to be already preprocessed and separated by whitespace. Document tags are constructed automatically from the document line number (each document gets a unique integer tag). diff --git a/gensim/models/keyedvectors.py b/gensim/models/keyedvectors.py index 6fb9e329d8..019d2c2652 100644 --- a/gensim/models/keyedvectors.py +++ b/gensim/models/keyedvectors.py @@ -517,7 +517,7 @@ def get_mean_vector(self, keys, weights=None, pre_normalize=True, post_normalize elif not ignore_missing: raise KeyError(f"Key '{key}' not present in vocabulary") - if(total_weight > 0): + if total_weight > 0: mean = mean / total_weight if post_normalize: mean = matutils.unitvec(mean).astype(REAL) @@ -1252,7 +1252,7 @@ def n_similarity(self, ws1, ws2): Similarities between `ws1` and `ws2`. """ - if not(len(ws1) and len(ws2)): + if not (len(ws1) and len(ws2)): raise ZeroDivisionError('At least one of the passed list is empty.') mean1 = self.get_mean_vector(ws1, pre_normalize=False) mean2 = self.get_mean_vector(ws2, pre_normalize=False) diff --git a/gensim/models/ldamodel.py b/gensim/models/ldamodel.py index 10a0c60134..b5f8017f07 100755 --- a/gensim/models/ldamodel.py +++ b/gensim/models/ldamodel.py @@ -314,7 +314,8 @@ def load(cls, fname, *args, **kwargs): class LdaModel(interfaces.TransformationABC, basemodel.BaseTopicModel): - """Train and use Online Latent Dirichlet Allocation model as presented in `'Online Learning for LDA' by Hoffman et al.`_ + """Train and use Online Latent Dirichlet Allocation model as presented in + `'Online Learning for LDA' by Hoffman et al.`_ Examples ------- diff --git a/gensim/models/ldaseqmodel.py b/gensim/models/ldaseqmodel.py index 0f222c9c6c..8ffcb5eee6 100644 --- a/gensim/models/ldaseqmodel.py +++ b/gensim/models/ldaseqmodel.py @@ -4,7 +4,8 @@ # Licensed under the GNU LGPL v2.1 - http://www.gnu.org/licenses/lgpl.html # Based on Copyright (C) 2016 Radim Rehurek -"""Lda Sequence model, inspired by `David M. Blei, John D. Lafferty: "Dynamic Topic Models" +"""Lda Sequence model, inspired by +`David M. Blei, John D. Lafferty: "Dynamic Topic Models" `_. The original C/C++ implementation can be found on `blei-lab/dtm `_. @@ -744,7 +745,8 @@ def update_zeta(self): return self.zeta def compute_post_variance(self, word, chain_variance): - r"""Get the variance, based on the `Variational Kalman Filtering approach for Approximate Inference (section 3.1) + r"""Get the variance, based on the + `Variational Kalman Filtering approach for Approximate Inference (section 3.1) `_. This function accepts the word to compute variance for, along with the associated sslm class object, diff --git a/gensim/test/test_ldamodel.py b/gensim/test/test_ldamodel.py index 297006b75f..7ce675e337 100644 --- a/gensim/test/test_ldamodel.py +++ b/gensim/test/test_ldamodel.py @@ -33,7 +33,7 @@ def test_random_state(): testcases = [np.random.seed(0), None, np.random.RandomState(0), 0] for testcase in testcases: - assert(isinstance(utils.get_random_state(testcase), np.random.RandomState)) + assert isinstance(utils.get_random_state(testcase), np.random.RandomState) class TestLdaModel(unittest.TestCase, basetmtests.TestBaseTopicModel): diff --git a/setup.py b/setup.py index d41f668ee0..de9e0bfb36 100644 --- a/setup.py +++ b/setup.py @@ -297,11 +297,17 @@ def run(self): # https://packaging.python.org/discussions/install-requires-vs-requirements/ # +# +# We pin the Sphinx-related packages to specific versions here because we want +# our documentation builds to be reproducible. Different versions of Sphinx +# can generate slightly different output, and because we keep some of the output +# under version control, we want to keep these differences to a minimum. +# docs_testenv = core_testenv + distributed_env + visdom_req + [ - 'sphinx', - 'sphinx-gallery', - 'sphinxcontrib.programoutput', - 'sphinxcontrib-napoleon', + 'sphinx==5.1.1', + 'sphinx-gallery==0.11.1', + 'sphinxcontrib.programoutput==0.17', + 'sphinxcontrib-napoleon==0.7', 'matplotlib', # expected by sphinx-gallery 'memory_profiler', 'annoy', From 99e43c4b2da4fbfb03f5f8e5e1c6e65157e1ec48 Mon Sep 17 00:00:00 2001 From: hstk Date: Mon, 22 Aug 2022 09:45:35 +0800 Subject: [PATCH 09/34] fix: fix error 'for loop initial declaration' (#3378) Co-authored-by: Michael Penkov --- gensim/similarities/fastss.pyx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/gensim/similarities/fastss.pyx b/gensim/similarities/fastss.pyx index e47a5442b2..fe0366bb04 100644 --- a/gensim/similarities/fastss.pyx +++ b/gensim/similarities/fastss.pyx @@ -43,9 +43,11 @@ cdef extern from *: void * s1_data = PyUnicode_DATA(s1); void * s2_data = PyUnicode_DATA(s2); - for (WIDTH tmpi = 0; tmpi <= len_s1; tmpi++) row2[tmpi] = tmpi; + WIDTH tmpi; + for (tmpi = 0; tmpi <= len_s1; tmpi++) row2[tmpi] = tmpi; - for (WIDTH i2 = 0; i2 < len_s2; i2++) { + WIDTH i2; + for (i2 = 0; i2 < len_s2; i2++) { int all_bad = i2 >= maximum; const Py_UCS4 ch = PyUnicode_READ(kind2, s2_data, i2); row_flip = 1 - row_flip; @@ -56,7 +58,8 @@ cdef extern from *: } *pos_new = i2 + 1; - for (WIDTH i1 = 0; i1 < len_s1; i1++) { + WIDTH i1; + for (i1 = 0; i1 < len_s1; i1++) { WIDTH val = *(pos_old++); if (ch != PyUnicode_READ(kind1, s1_data, i1)) { const WIDTH _val1 = *pos_old; From 2350e9259b066126a9687cf6e04cb644f387a38a Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Mon, 22 Aug 2022 15:37:06 +0900 Subject: [PATCH 10/34] update release/hijack_pr.py --- release/hijack_pr.py | 80 ++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/release/hijack_pr.py b/release/hijack_pr.py index d109836d49..fb326fff7c 100755 --- a/release/hijack_pr.py +++ b/release/hijack_pr.py @@ -18,6 +18,14 @@ The above commands would check out the code for the PR, make changes to them, and push them back. Obviously, this requires the PR to be writable, but most gensim PRs are. If they aren't, then leave it up to the PR author to make the required changes. + +Sometimes, we'll make upstream changes that we want to merge into existing PRs. +This is particularly useful when some nagging build problem is affecting multiple PRs. +We can achieve this with: + + $ release/hijack_pr.py merge-upstream-into 1234 + +This hijacks the PR and merges upstream/develop into it. """ import json import subprocess @@ -25,47 +33,67 @@ import smart_open + def check_output(command): return subprocess.check_output(command).strip().decode('utf-8') -if sys.argv[1] == "push": +def push(): command = "git rev-parse --abbrev-ref HEAD@{upstream}".split() remote, remote_branch = check_output(command).split('/') current_branch = check_output(['git', 'branch', '--show-current']) - check_output(['git', 'push', remote, f'{current_branch}:{remote_branch}']) + subprocess.check_call(['git', 'push', remote, f'{current_branch}:{remote_branch}']) # # Cleanup to prevent remotes and branches from piling up # - check_output(['git', 'branch', '--delete', current_branch]) - check_output(['git', 'remote', 'remove', remote]) - sys.exit(0) + subprocess.check_call(['git', 'checkout', 'develop']) + subprocess.check_call(['git', 'branch', '--delete', current_branch]) + subprocess.check_call(['git', 'remote', 'remove', remote]) + + +def hijack(prid): + url = f"https://github.com/repos/RaRe-Technologies/gensim/pulls/{prid}" + with smart_open.open(url) as fin: + prinfo = json.load(fin) + + user = prinfo['head']['user']['login'] + ssh_url = prinfo['head']['repo']['ssh_url'] -prid = int(sys.argv[1]) -url = f"https://github.com/repos/RaRe-Technologies/gensim/pulls/{prid}" -with smart_open.open(url) as fin: - prinfo = json.load(fin) + remotes = check_output(['git', 'remote']).split('\n') + if user not in remotes: + subprocess.check_call(['git', 'remote', 'add', user, ssh_url]) -user = prinfo['head']['user']['login'] -ssh_url = prinfo['head']['repo']['ssh_url'] + subprocess.check_call(['git', 'fetch', user]) -remotes = check_output(['git', 'remote']).split('\n') -if user not in remotes: - subprocess.check_call(['git', 'remote', 'add', user, ssh_url]) + ref = prinfo['head']['ref'] + subprocess.check_call(['git', 'checkout', f'{user}/{ref}']) + + # + # Prefix the local branch name with the user to avoid naming clashes with + # existing branches, e.g. develop + # + subprocess.check_call(['git', 'switch', '-c', f'{user}_{ref}']) + + # + # Set the upstream so we can push back to it more easily + # + subprocess.check_call(['git', 'branch', '--set-upstream-to', f'{user}/{ref}']) -subprocess.check_call(['git', 'fetch', user]) -ref = prinfo['head']['ref'] -subprocess.check_call(['git', 'checkout', f'{user}/{ref}']) +def main(): + if sys.argv[1] == "push": + push() + elif sys.argv[1] == 'merge-upstream-into': + prid = int(sys.argv[2]) + hijack(prid) + subprocess.check_call(['git', 'fetch', 'upstream']) + subprocess.check_call(['git', 'merge', 'upstream/develop', '--no-edit']) + push() + else: + prid = int(sys.argv[1]) + hijack(prid) -# -# Prefix the local branch name with the user to avoid naming clashes with -# existing branches, e.g. develop -# -subprocess.check_call(['git', 'switch', '-c', f'{user}_{ref}']) -# -# Set the upstream so we can push back to it more easily -# -subprocess.check_call(['git', 'branch', '--set-upstream-to', f'{user}/{ref}']) +if __name__ == '__main__': + main() From 7f314ee10c9ad83816aa795c5ef6ebc378de3acf Mon Sep 17 00:00:00 2001 From: dymil <30931139+dymil@users.noreply.github.com> Date: Mon, 22 Aug 2022 08:48:56 -0400 Subject: [PATCH 11/34] Fix typo in word2vec and KeyedVectors docstrings (#3365) * Add missing word in word2vec docstring * Fix docstring typo in KeyedVectors distances() word_or_vector, not word_or_vectors --- gensim/models/keyedvectors.py | 2 +- gensim/models/word2vec.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gensim/models/keyedvectors.py b/gensim/models/keyedvectors.py index 019d2c2652..6b31496bb5 100644 --- a/gensim/models/keyedvectors.py +++ b/gensim/models/keyedvectors.py @@ -1167,7 +1167,7 @@ def cosine_similarities(vector_1, vectors_all): def distances(self, word_or_vector, other_words=()): """Compute cosine distances from given word or vector to all words in `other_words`. - If `other_words` is empty, return distance between `word_or_vectors` and all words in vocab. + If `other_words` is empty, return distance between `word_or_vector` and all words in vocab. Parameters ---------- diff --git a/gensim/models/word2vec.py b/gensim/models/word2vec.py index 061dcfc817..d20bb63b1f 100755 --- a/gensim/models/word2vec.py +++ b/gensim/models/word2vec.py @@ -68,7 +68,7 @@ >>> sims = model.wv.most_similar('computer', topn=10) # get other similar words The reason for separating the trained vectors into `KeyedVectors` is that if you don't -need the full model state any more (don't need to continue training), its state can discarded, +need the full model state any more (don't need to continue training), its state can be discarded, keeping just the vectors and their keys proper. This results in a much smaller and faster object that can be mmapped for lightning From 77c3a7ff5254346146d0e9eedf8e84fb6d577878 Mon Sep 17 00:00:00 2001 From: dymil <30931139+dymil@users.noreply.github.com> Date: Mon, 22 Aug 2022 08:54:57 -0400 Subject: [PATCH 12/34] Replace np.multiply with np.square and copyedit in translation_matrix.py (#3374) * Replace np.multiply with np.square and copyedit * Copyedit translation_matrix.py Co-authored-by: Michael Penkov --- gensim/models/translation_matrix.py | 40 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/gensim/models/translation_matrix.py b/gensim/models/translation_matrix.py index 528e3d6fa2..246482421f 100644 --- a/gensim/models/translation_matrix.py +++ b/gensim/models/translation_matrix.py @@ -5,7 +5,7 @@ a standard nearest neighbour method or a globally corrected neighbour retrieval method [1]_. This method can be used to augment the existing phrase tables with more candidate translations, or -filter out errors from the translation tables and known dictionaries [2]_. What's more, It also work +filter out errors from the translation tables and known dictionaries [2]_. What's more, it also works for any two sets of named-vectors where there are some paired-guideposts to learn the transformation. Examples @@ -14,7 +14,7 @@ How to make translation between two set of word-vectors ======================================================= -Initialize a word-vector models +Initialize two word-vector models .. sourcecode:: pycon @@ -24,7 +24,7 @@ >>> model_en = KeyedVectors.load_word2vec_format(datapath("EN.1-10.cbow1_wind5_hs0_neg10_size300_smpl1e-05.txt")) >>> model_it = KeyedVectors.load_word2vec_format(datapath("IT.1-10.cbow1_wind5_hs0_neg10_size300_smpl1e-05.txt")) -Define word pairs (that will be used for construction of translation matrix +Define word pairs (that will be used for construction of translation matrix) .. sourcecode:: pycon @@ -143,12 +143,12 @@ def build(cls, lang_vec, lexicon=None): Object that stored word-vectors """ - # `words` to store all the word that - # `mat` to store all the word vector for the word in 'words' list + # `words` to store all the words + # `mat` to store the word vector for each word in the 'words' list words = [] mat = [] if lexicon is not None: - # if the lexicon is not provided, using the all the Keyedvectors's words as default + # if the lexicon is not provided, using all the Keyedvectors's words as default for item in lexicon: words.append(item) mat.append(lang_vec.vectors[lang_vec.get_index(item)]) @@ -161,18 +161,18 @@ def build(cls, lang_vec, lexicon=None): return Space(mat, words) def normalize(self): - """Normalize the word vector's matrix.""" - self.mat = self.mat / np.sqrt(np.sum(np.multiply(self.mat, self.mat), axis=1, keepdims=True)) + """Normalize the word vectors matrix.""" + self.mat = self.mat / np.sqrt(np.sum(np.square(self.mat), axis=1, keepdims=True)) class TranslationMatrix(utils.SaveLoad): - """Objects of this class realize the translation matrix which map the source language to the target language. + """Objects of this class realize the translation matrix which maps the source language to the target language. The main methods are: We map it to the other language space by computing z = Wx, then return the word whose representation is close to z. - The details use seen the notebook [3]_ + For details on use, see the tutorial notebook [3]_ Examples -------- @@ -234,7 +234,7 @@ def __init__(self, source_lang_vec, target_lang_vec, word_pairs=None, random_sta self.train(word_pairs) def train(self, word_pairs): - """Build the translation matrix that mapping from source space to target space. + """Build the translation matrix to map from source space to target space. Parameters ---------- @@ -289,7 +289,7 @@ def translate(self, source_words, topn=5, gc=0, sample_num=None, source_lang_vec Define translation algorithm, if `gc == 0` - use standard NN retrieval, otherwise, use globally corrected neighbour retrieval method (as described in [1]_). sample_num : int, optional - Number of word to sample from the source lexicon, if `gc == 1`, then `sample_num` **must** be provided. + Number of words to sample from the source lexicon, if `gc == 1`, then `sample_num` **must** be provided. source_lang_vec : :class:`~gensim.models.keyedvectors.KeyedVectors`, optional New source language vectors for translation, by default, used the model's source language vector. target_lang_vec : :class:`~gensim.models.keyedvectors.KeyedVectors`, optional @@ -366,15 +366,15 @@ def translate(self, source_words, topn=5, gc=0, sample_num=None, source_lang_vec class BackMappingTranslationMatrix(utils.SaveLoad): - """Realize the BackMapping translation matrix which map the source model's document vector - to the target model's document vector(old model). + """Realize the BackMapping translation matrix which maps the source model's document vector + to the target model's document vector (old model). - BackMapping translation matrix is used to learn a mapping for two document vector space which we - specify as source document vector and target document vector. The target document vector are trained - on superset corpus of source document vector, we can incrementally increase the vector in + BackMapping translation matrix is used to learn a mapping for two document vector spaces which we + specify as source document vector and target document vector. The target document vectors are trained + on a superset corpus of source document vectors; we can incrementally increase the vector in the old model through the BackMapping translation matrix. - the details use seen the notebook [3]_. + For details on use, see the tutorial notebook [3]_. Examples -------- @@ -421,7 +421,7 @@ def __init__(self, source_lang_vec, target_lang_vec, tagged_docs=None, random_st self.train(tagged_docs) def train(self, tagged_docs): - """Build the translation matrix that mapping from the source model's vector to target model's vector + """Build the translation matrix to map from the source model's vectors to target model's vectors Parameters ---------- @@ -432,7 +432,7 @@ def train(self, tagged_docs): Returns ------- numpy.ndarray - Translation matrix that mapping from the source model's vector to target model's vector. + Translation matrix that maps from the source model's vectors to target model's vectors. """ m1 = [self.source_lang_vec.dv[item.tags].flatten() for item in tagged_docs] From ff3531bee156f69c8a7fe5809ce22df0176bcf98 Mon Sep 17 00:00:00 2001 From: dymil <30931139+dymil@users.noreply.github.com> Date: Mon, 22 Aug 2022 08:56:01 -0400 Subject: [PATCH 13/34] Copyedit and fix outdated statements (#3375) * print statements replaced with print function for Python 3 * gensim.utils.smart_open replaced with smart_open.open for newer gensim Co-authored-by: Michael Penkov --- docs/notebooks/translation_matrix.ipynb | 92 +++++++++++-------------- 1 file changed, 42 insertions(+), 50 deletions(-) diff --git a/docs/notebooks/translation_matrix.ipynb b/docs/notebooks/translation_matrix.ipynb index 8832f732e6..8e7eefdbbd 100644 --- a/docs/notebooks/translation_matrix.ipynb +++ b/docs/notebooks/translation_matrix.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Tranlation Matrix Tutorial" + "# Translation Matrix Tutorial" ] }, { @@ -34,14 +34,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Tomas Mikolov, Quoc V Le, Ilya Sutskever. 2013.[Exploiting Similarities among Languages for Machine Translation](https://arxiv.org/pdf/1309.4168.pdf)\n", + "Tomas Mikolov, Quoc V Le, Ilya Sutskever. 2013. [Exploiting Similarities among Languages for Machine Translation](https://arxiv.org/pdf/1309.4168.pdf)\n", "\n", - "Georgiana Dinu, Angelikie Lazaridou and Marco Baroni. 2014.[Improving zero-shot learning by mitigating the hubness problem](https://arxiv.org/pdf/1309.4168.pdf)" + "Georgiana Dinu, Angelikie Lazaridou and Marco Baroni. 2014. [Improving zero-shot learning by mitigating the hubness problem](https://arxiv.org/pdf/1309.4168.pdf)" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -49,7 +49,8 @@ "\n", "from gensim import utils\n", "from gensim.models import translation_matrix\n", - "from gensim.models import KeyedVectors" + "from gensim.models import KeyedVectors\n", + "import smart_open" ] }, { @@ -65,7 +66,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -74,30 +75,23 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 7, "metadata": {}, "outputs": [ { - "ename": "FileNotFoundError", - "evalue": "[Errno 2] No such file or directory: 'OPUS_en_it_europarl_train_5K.txt'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mtrain_file\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m\"OPUS_en_it_europarl_train_5K.txt\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0;32mwith\u001b[0m \u001b[0mutils\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msmart_open\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtrain_file\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"r\"\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4\u001b[0m \u001b[0mword_pair\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mtuple\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mutils\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mto_unicode\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mline\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstrip\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msplit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mline\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0mprint\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mword_pair\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;36m10\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/envs/gensim/lib/python3.7/site-packages/smart_open/smart_open_lib.py\u001b[0m in \u001b[0;36msmart_open\u001b[0;34m(uri, mode, **kw)\u001b[0m\n\u001b[1;32m 437\u001b[0m \u001b[0mtransport_params\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mkey\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 438\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 439\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mopen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0muri\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmode\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mignore_ext\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mignore_extension\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtransport_params\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mtransport_params\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mscrubbed_kwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 440\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 441\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/envs/gensim/lib/python3.7/site-packages/smart_open/smart_open_lib.py\u001b[0m in \u001b[0;36mopen\u001b[0;34m(uri, mode, buffering, encoding, errors, newline, closefd, opener, ignore_ext, transport_params)\u001b[0m\n\u001b[1;32m 305\u001b[0m \u001b[0mbuffering\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mbuffering\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 306\u001b[0m \u001b[0mencoding\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mencoding\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 307\u001b[0;31m \u001b[0merrors\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0merrors\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 308\u001b[0m )\n\u001b[1;32m 309\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mfobj\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/envs/gensim/lib/python3.7/site-packages/smart_open/smart_open_lib.py\u001b[0m in \u001b[0;36m_shortcut_open\u001b[0;34m(uri, mode, ignore_ext, buffering, encoding, errors)\u001b[0m\n\u001b[1;32m 496\u001b[0m \u001b[0;31m#\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 497\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0msix\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPY3\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 498\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0m_builtin_open\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mparsed_uri\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0muri_path\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmode\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbuffering\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mbuffering\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mopen_kwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 499\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mopen_kwargs\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 500\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0m_builtin_open\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mparsed_uri\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0muri_path\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmode\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbuffering\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mbuffering\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: 'OPUS_en_it_europarl_train_5K.txt'" + "name": "stdout", + "output_type": "stream", + "text": [ + "[('for', 'per'), ('that', 'che'), ('with', 'con'), ('are', 'are'), ('are', 'sono'), ('this', 'questa'), ('this', 'questo'), ('you', 'lei'), ('not', 'non'), ('which', 'che')]\n" ] } ], "source": [ "train_file = \"OPUS_en_it_europarl_train_5K.txt\"\n", "\n", - "with utils.smart_open(train_file, \"r\") as f:\n", + "with smart_open.open(train_file, \"r\") as f:\n", " word_pair = [tuple(utils.to_unicode(line).strip().split()) for line in f]\n", - "print (word_pair[:10])" + "print(word_pair[:10])" ] }, { @@ -151,14 +145,14 @@ "source": [ "transmat = translation_matrix.TranslationMatrix(source_word_vec, target_word_vec, word_pair)\n", "transmat.train(word_pair)\n", - "print (\"the shape of translation matrix is: \", transmat.translation_matrix.shape)" + "print(\"the shape of translation matrix is: \", transmat.translation_matrix.shape)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Prediction Time: For any given new word, we can map it to the other language space by coputing $z = Wx$, then we find the word whose representation is closet to z in the target language space, using consine similarity as the distance metric." + "Prediction Time: For any given new word, we can map it to the other language space by computing $z = Wx$, then we find the word whose representation is closet to z in the target language space, using cosine similarity as the distance metric." ] }, { @@ -190,7 +184,7 @@ "outputs": [], "source": [ "for k, v in translated_word.iteritems():\n", - " print (\"word \", k, \" and translated word\", v)" + " print(\"word \", k, \" and translated word\", v)" ] }, { @@ -211,7 +205,7 @@ "source_word, target_word = zip(*words)\n", "translated_word = transmat.translate(source_word, 5)\n", "for k, v in translated_word.iteritems():\n", - " print (\"word \", k, \" and translated word\", v)" + " print(\"word \", k, \" and translated word\", v)" ] }, { @@ -232,7 +226,7 @@ "source_word, target_word = zip(*words)\n", "translated_word = transmat.translate(source_word, 5)\n", "for k, v in translated_word.iteritems():\n", - " print (\"word \", k, \" and translated word\", v)" + " print(\"word \", k, \" and translated word\", v)" ] }, { @@ -246,7 +240,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Testing the creation time, we extracted more word pairs from a dictionary built from Europarl([Europara, en-it](http://opus.lingfil.uu.se/)). We obtain about 20K word pairs and their coresponding word vectors or you can download from this.[word_dict.pkl](https://pan.baidu.com/s/1dF8HUX7)" + "Testing the creation time, we extracted more word pairs from a dictionary built from Europarl([Europara, en-it](http://opus.lingfil.uu.se/)). We obtain about 20K word pairs and their corresponding word vectors or you can download from this: [word_dict.pkl](https://pan.baidu.com/s/1dF8HUX7)" ] }, { @@ -257,9 +251,9 @@ "source": [ "import pickle\n", "word_dict = \"word_dict.pkl\"\n", - "with utils.smart_open(word_dict, \"r\") as f:\n", + "with smart_open.open(word_dict, \"r\") as f:\n", " word_pair = pickle.load(f)\n", - "print (\"the length of word pair \", len(word_pair))" + "print(\"the length of word pair \", len(word_pair))" ] }, { @@ -423,7 +417,7 @@ "\n", "# Translate the English word five to Italian word\n", "translated_word = transmat.translate([en_words[4]], 3)\n", - "print \"translation of five: \", translated_word\n", + "print(\"translation of five: \", translated_word)\n", "\n", "# the translated words of five\n", "for item in translated_word[en_words[4]]:\n", @@ -518,7 +512,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Let's see some animal words, the figue shows that most of words are also share the similar geometric arrangements." + "Let's see some animal words, the figure shows that most of the words also have similar geometric arrangements." ] }, { @@ -593,7 +587,7 @@ "\n", "# Translate the English word birds to Italian word\n", "translated_word = transmat.translate([en_words[4]], 3)\n", - "print \"translation of birds: \", translated_word\n", + "print(\"translation of birds: \", translated_word)\n", "\n", "# the translated words of birds\n", "for item in translated_word[en_words[4]]:\n", @@ -700,7 +694,7 @@ "source": [ "As dicussion in this [PR](https://github.com/RaRe-Technologies/gensim/pull/1434), Translation Matrix not only can used to translate the words from one source language to another target lanuage, but also to translate new document vectors back to old model space.\n", "\n", - "For example, if we have trained 15k documents using doc2vec (we called this as model1), and we are going to train new 35k documents using doc2vec(we called this as model2). So we can include those 15k documents as reference documents into the new 35k documents. Then we can get 15k document vectors from model1 and 50k document vectors from model2, but both of the two models have vectors for those 15k documents. We can use those vectors to build a mapping from model1 to model2. Finally, with this relation, we can back-mapping the model2's vector to model1. Therefore, 35k document vectors are learned using this method." + "For example, if we have trained 15k documents using doc2vec (we called this as model1), and we are going to train new 35k documents using doc2vec (we called this as model2). So we can include those 15k documents as reference documents into the new 35k documents. Then we can get 15k document vectors from model1 and 50k document vectors from model2, but both of the two models have vectors for those 15k documents. We can use those vectors to build a mapping from model1 to model2. Finally, with this relation, we can back-map the model2's vector to model1. Therefore, 35k document vectors are learned using this method." ] }, { @@ -720,13 +714,13 @@ "from gensim.models.doc2vec import TaggedDocument\n", "from gensim.models import Doc2Vec\n", "from collections import namedtuple\n", - "from gensim import utils\n", + "import smart_open\n", "\n", "def read_sentimentDocs():\n", " SentimentDocument = namedtuple('SentimentDocument', 'words tags split sentiment')\n", "\n", " alldocs = [] # will hold all docs in original order\n", - " with utils.smart_open('aclImdb/alldata-id.txt', encoding='utf-8') as alldata:\n", + " with smart_open.open('aclImdb/alldata-id.txt', encoding='utf-8') as alldata:\n", " for line_no, line in enumerate(alldata):\n", " tokens = gensim.utils.to_unicode(line).split()\n", " words = tokens[1:]\n", @@ -748,14 +742,14 @@ "small_corpus = train_docs[:15000]\n", "large_corpus = train_docs + test_docs\n", "\n", - "print len(train_docs), len(test_docs), len(doc_list), len(small_corpus), len(large_corpus)" + "print(len(train_docs), len(test_docs), len(doc_list), len(small_corpus), len(large_corpus))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Here, we train two Doc2vec model, the parameters can be determined by yourself. We trained on 15k documents for the `model1` and 50k documents for the `model2`. But you should mixed some documents which from the 15k document in `model` to the `model2` as dicussed before. " + "Here, we train two Doc2vec model, the parameters can be determined by yourself. We trained on 15k documents for the `model1` and 50k documents for the `model2`. But you should mix some documents which from the 15k document in `model` to the `model2`, as discussed before. " ] }, { @@ -795,7 +789,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "For the IMDB training dataset, we train an classifier on the train data which has 25k documents with positive and negative label. Then using this classifier to predict the test data. To see what accuracy can the document vectors which learned by different method achieve." + "For the IMDB training dataset, we train an classifier on the train data which has 25k documents with positive and negative label. Then using this classifier to predict the test data, we see what accuracy can be achieved by the document vectors learned by different methods." ] }, { @@ -812,7 +806,7 @@ " classifier = LogisticRegression()\n", " classifier.fit(train, train_label)\n", " score = classifier.score(test, test_label)\n", - " print \"the classifier score :\", score\n", + " print(\"the classifier score :\", score)\n", " return score" ] }, @@ -855,7 +849,7 @@ " test_array[i + 12500] = m2[i + 37500]\n", " test_label[i + 12500] = 0\n", "\n", - "print \"The vectors are learned by doc2vec method\"\n", + "print(\"The vectors are learned by doc2vec method\")\n", "test_classifier_error(train_array, train_label, test_array, test_label)" ] }, @@ -910,7 +904,7 @@ " test_array[i + 12500] = m1[i + 37500]\n", " test_label[i + 12500] = 0\n", "\n", - "print \"The vectors are learned by back-mapping method\"\n", + "print(\"The vectors are learned by back-mapping method\")\n", "test_classifier_error(train_array, train_label, test_array, test_label)" ] }, @@ -918,7 +912,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As we can see that, the vectors learned by back-mapping method performed not bad but still need improved." + "As we can see that, the vectors learned by back-mapping method performed not bad but still need to be improved." ] }, { @@ -1026,18 +1020,11 @@ "source": [ "You probably will see kinds of colors point. One for the `model1`, the `sdoc0` to `sdoc4` document vector are learned by Doc2vec and `sdoc5` and `sdoc6` are learned by back-mapping. One for the `model2`, the `tdoc0` to `tdoc6` are learned by Doc2vec. We can see that some of points learned from the back-mapping method still have the relative position with the point learned by Doc2vec." ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3.10.2 64-bit", "language": "python", "name": "python3" }, @@ -1051,7 +1038,12 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.1" + "version": "3.10.2" + }, + "vscode": { + "interpreter": { + "hash": "901b79e026e03396fd1ffa7133844e9ea80e258ce34c66e1aabb5896bcb18463" + } } }, "nbformat": 4, From 5dbfb1e231e72a8f4ebc1bf099675116848f1d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20Novotn=C3=BD?= Date: Thu, 8 Sep 2022 02:50:27 +0200 Subject: [PATCH 14/34] Implement Okapi BM25 variants in Gensim (#3304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add and unit-test gensim.models.bm25model.OkapiBM25Model * Document gensim.models.bm25 * Add and unit-test gensim.models.bm25model.{Lucene,Atire}BM25Model * Add normalize_{queries,documents} params to gensim.similarities.docsim * Add example of BM25 to gensim.similarities.docsim.SparseMatrixSimilarity * Refresh stale gallery cache * Update gensim/models/bm25model.py Co-authored-by: Radim Řehůřek Co-authored-by: Radim Řehůřek --- ...glr_run_topics_and_transformations_001.png | Bin 69248 -> 120191 bytes ...r_run_topics_and_transformations_thumb.png | Bin 62435 -> 64089 bytes .../core/run_topics_and_transformations.ipynb | 12 +- .../core/run_topics_and_transformations.py | 14 + .../run_topics_and_transformations.py.md5 | 2 +- .../core/run_topics_and_transformations.rst | 150 ++++++- .../auto_examples/core/sg_execution_times.rst | 8 +- docs/src/auto_examples/index.rst | 4 +- .../core/run_topics_and_transformations.py | 14 + gensim/models/__init__.py | 1 + gensim/models/bm25model.py | 391 ++++++++++++++++++ gensim/similarities/docsim.py | 90 +++- gensim/test/test_bm25model.py | 264 ++++++++++++ 13 files changed, 907 insertions(+), 43 deletions(-) create mode 100644 gensim/models/bm25model.py create mode 100644 gensim/test/test_bm25model.py diff --git a/docs/src/auto_examples/core/images/sphx_glr_run_topics_and_transformations_001.png b/docs/src/auto_examples/core/images/sphx_glr_run_topics_and_transformations_001.png index 3dbf224404b34b76d60f5e91180daa785176a7ef..2b3ddc2b4fbd96caeb535b0fcf576bcfa1dcbe54 100644 GIT binary patch literal 120191 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 FYP!0UDALdNxM8> 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 E0QKl*g8%>k literal 69248 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 L$&1zs8wC9iVGki2 diff --git a/docs/src/auto_examples/core/run_topics_and_transformations.ipynb b/docs/src/auto_examples/core/run_topics_and_transformations.ipynb index c5a5fbb709..4f40de02be 100644 --- a/docs/src/auto_examples/core/run_topics_and_transformations.ipynb +++ b/docs/src/auto_examples/core/run_topics_and_transformations.ipynb @@ -15,7 +15,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "\nTopics and Transformations\n===========================\n\nIntroduces transformations and demonstrates their use on a toy corpus.\n\n" + "\n# Topics and Transformations\n\nIntroduces transformations and demonstrates their use on a toy corpus.\n" ] }, { @@ -33,7 +33,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In this tutorial, I will show how to transform documents from one vector representation\ninto another. This process serves two goals:\n\n1. To bring out hidden structure in the corpus, discover relationships between\n words and use them to describe the documents in a new and\n (hopefully) more semantic way.\n2. To make the document representation more compact. This both improves efficiency\n (new representation consumes less resources) and efficacy (marginal data\n trends are ignored, noise-reduction).\n\nCreating the Corpus\n-------------------\n\nFirst, we need to create a corpus to work with.\nThis step is the same as in the previous tutorial;\nif you completed it, feel free to skip to the next section.\n\n" + "In this tutorial, I will show how to transform documents from one vector representation\ninto another. This process serves two goals:\n\n1. To bring out hidden structure in the corpus, discover relationships between\n words and use them to describe the documents in a new and\n (hopefully) more semantic way.\n2. To make the document representation more compact. This both improves efficiency\n (new representation consumes less resources) and efficacy (marginal data\n trends are ignored, noise-reduction).\n\n## Creating the Corpus\n\nFirst, we need to create a corpus to work with.\nThis step is the same as in the previous tutorial;\nif you completed it, feel free to skip to the next section.\n\n" ] }, { @@ -51,7 +51,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Creating a transformation\n++++++++++++++++++++++++++\n\nThe transformations are standard Python objects, typically initialized by means of\na :dfn:`training corpus`:\n\n\n" + "### Creating a transformation\n\nThe transformations are standard Python objects, typically initialized by means of\na :dfn:`training corpus`:\n\n\n" ] }, { @@ -69,7 +69,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We used our old corpus from tutorial 1 to initialize (train) the transformation model. Different\ntransformations may require different initialization parameters; in case of TfIdf, the\n\"training\" consists simply of going through the supplied corpus once and computing document frequencies\nof all its features. Training other models, such as Latent Semantic Analysis or Latent Dirichlet\nAllocation, is much more involved and, consequently, takes much more time.\n\n

\n\n\nTransforming vectors\n+++++++++++++++++++++\n\nFrom now on, ``tfidf`` is treated as a read-only object that can be used to convert\nany vector from the old representation (bag-of-words integer counts) to the new representation\n(TfIdf real-valued weights):\n\n" + "We used our old corpus from tutorial 1 to initialize (train) the transformation model. Different\ntransformations may require different initialization parameters; in case of TfIdf, the\n\"training\" consists simply of going through the supplied corpus once and computing document frequencies\nof all its features. Training other models, such as Latent Semantic Analysis or Latent Dirichlet\nAllocation, is much more involved and, consequently, takes much more time.\n\n

Note

Transformations always convert between two specific vector\n spaces. The same vector space (= the same set of feature ids) must be used for training\n as well as for subsequent vector transformations. Failure to use the same input\n feature space, such as applying a different string preprocessing, using different\n feature ids, or using bag-of-words input vectors where TfIdf vectors are expected, will\n result in feature mismatch during transformation calls and consequently in either\n garbage output and/or runtime exceptions.

\n\n\n### Transforming vectors\n\nFrom now on, ``tfidf`` is treated as a read-only object that can be used to convert\nany vector from the old representation (bag-of-words integer counts) to the new representation\n(TfIdf real-valued weights):\n\n" ] }, { @@ -177,7 +177,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The next question might be: just how exactly similar are those documents to each other?\nIs there a way to formalize the similarity, so that for a given input document, we can\norder some other set of documents according to their similarity? Similarity queries\nare covered in the next tutorial (`sphx_glr_auto_examples_core_run_similarity_queries.py`).\n\n\nAvailable transformations\n--------------------------\n\nGensim implements several popular Vector Space Model algorithms:\n\n* `Term Frequency * Inverse Document Frequency, Tf-Idf `_\n expects a bag-of-words (integer values) training corpus during initialization.\n During transformation, it will take a vector and return another vector of the\n same dimensionality, except that features which were rare in the training corpus\n will have their value increased.\n It therefore converts integer-valued vectors into real-valued ones, while leaving\n the number of dimensions intact. It can also optionally normalize the resulting\n vectors to (Euclidean) unit length.\n\n .. sourcecode:: pycon\n\n model = models.TfidfModel(corpus, normalize=True)\n\n* `Latent Semantic Indexing, LSI (or sometimes LSA) `_\n transforms documents from either bag-of-words or (preferrably) TfIdf-weighted space into\n a latent space of a lower dimensionality. For the toy corpus above we used only\n 2 latent dimensions, but on real corpora, target dimensionality of 200--500 is recommended\n as a \"golden standard\" [1]_.\n\n .. sourcecode:: pycon\n\n model = models.LsiModel(tfidf_corpus, id2word=dictionary, num_topics=300)\n\n LSI training is unique in that we can continue \"training\" at any point, simply\n by providing more training documents. This is done by incremental updates to\n the underlying model, in a process called `online training`. Because of this feature, the\n input document stream may even be infinite -- just keep feeding LSI new documents\n as they arrive, while using the computed transformation model as read-only in the meanwhile!\n\n .. sourcecode:: pycon\n\n model.add_documents(another_tfidf_corpus) # now LSI has been trained on tfidf_corpus + another_tfidf_corpus\n lsi_vec = model[tfidf_vec] # convert some new document into the LSI space, without affecting the model\n\n model.add_documents(more_documents) # tfidf_corpus + another_tfidf_corpus + more_documents\n lsi_vec = model[tfidf_vec]\n\n See the :mod:`gensim.models.lsimodel` documentation for details on how to make\n LSI gradually \"forget\" old observations in infinite streams. If you want to get dirty,\n there are also parameters you can tweak that affect speed vs. memory footprint vs. numerical\n precision of the LSI algorithm.\n\n `gensim` uses a novel online incremental streamed distributed training algorithm (quite a mouthful!),\n which I published in [5]_. `gensim` also executes a stochastic multi-pass algorithm\n from Halko et al. [4]_ internally, to accelerate in-core part\n of the computations.\n See also `wiki` for further speed-ups by distributing the computation across\n a cluster of computers.\n\n* `Random Projections, RP `_ aim to\n reduce vector space dimensionality. This is a very efficient (both memory- and\n CPU-friendly) approach to approximating TfIdf distances between documents, by throwing in a little randomness.\n Recommended target dimensionality is again in the hundreds/thousands, depending on your dataset.\n\n .. sourcecode:: pycon\n\n model = models.RpModel(tfidf_corpus, num_topics=500)\n\n* `Latent Dirichlet Allocation, LDA `_\n is yet another transformation from bag-of-words counts into a topic space of lower\n dimensionality. LDA is a probabilistic extension of LSA (also called multinomial PCA),\n so LDA's topics can be interpreted as probability distributions over words. These distributions are,\n just like with LSA, inferred automatically from a training corpus. Documents\n are in turn interpreted as a (soft) mixture of these topics (again, just like with LSA).\n\n .. sourcecode:: pycon\n\n model = models.LdaModel(corpus, id2word=dictionary, num_topics=100)\n\n `gensim` uses a fast implementation of online LDA parameter estimation based on [2]_,\n modified to run in `distributed mode ` on a cluster of computers.\n\n* `Hierarchical Dirichlet Process, HDP `_\n is a non-parametric bayesian method (note the missing number of requested topics):\n\n .. sourcecode:: pycon\n\n model = models.HdpModel(corpus, id2word=dictionary)\n\n `gensim` uses a fast, online implementation based on [3]_.\n The HDP model is a new addition to `gensim`, and still rough around its academic edges -- use with care.\n\nAdding new :abbr:`VSM (Vector Space Model)` transformations (such as different weighting schemes) is rather trivial;\nsee the `apiref` or directly the `Python code `_\nfor more info and examples.\n\nIt is worth repeating that these are all unique, **incremental** implementations,\nwhich do not require the whole training corpus to be present in main memory all at once.\nWith memory taken care of, I am now improving `distributed`,\nto improve CPU efficiency, too.\nIf you feel you could contribute by testing, providing use-cases or code, see the `Gensim Developer guide `__.\n\nWhat Next?\n----------\n\nContinue on to the next tutorial on `sphx_glr_auto_examples_core_run_similarity_queries.py`.\n\nReferences\n----------\n\n.. [1] Bradford. 2008. An empirical study of required dimensionality for large-scale latent semantic indexing applications.\n\n.. [2] Hoffman, Blei, Bach. 2010. Online learning for Latent Dirichlet Allocation.\n\n.. [3] Wang, Paisley, Blei. 2011. Online variational inference for the hierarchical Dirichlet process.\n\n.. [4] Halko, Martinsson, Tropp. 2009. Finding structure with randomness.\n\n.. [5] \u0158eh\u016f\u0159ek. 2011. Subspace tracking for Latent Semantic Analysis.\n\n" + "The next question might be: just how exactly similar are those documents to each other?\nIs there a way to formalize the similarity, so that for a given input document, we can\norder some other set of documents according to their similarity? Similarity queries\nare covered in the next tutorial (`sphx_glr_auto_examples_core_run_similarity_queries.py`).\n\n\n## Available transformations\n\nGensim implements several popular Vector Space Model algorithms:\n\n* `Term Frequency * Inverse Document Frequency, Tf-Idf `_\n expects a bag-of-words (integer values) training corpus during initialization.\n During transformation, it will take a vector and return another vector of the\n same dimensionality, except that features which were rare in the training corpus\n will have their value increased.\n It therefore converts integer-valued vectors into real-valued ones, while leaving\n the number of dimensions intact. It can also optionally normalize the resulting\n vectors to (Euclidean) unit length.\n\n .. sourcecode:: pycon\n\n model = models.TfidfModel(corpus, normalize=True)\n\n* `Okapi Best Matching, Okapi BM25 `_\n expects a bag-of-words (integer values) training corpus during initialization.\n During transformation, it will take a vector and return another vector of the\n same dimensionality, except that features which were rare in the training corpus\n will have their value increased. It therefore converts integer-valued\n vectors into real-valued ones, while leaving the number of dimensions intact.\n\n Okapi BM25 is the standard ranking function used by search engines to estimate\n the relevance of documents to a given search query.\n\n .. sourcecode:: pycon\n\n model = models.OkapiBM25Model(corpus)\n\n* `Latent Semantic Indexing, LSI (or sometimes LSA) `_\n transforms documents from either bag-of-words or (preferrably) TfIdf-weighted space into\n a latent space of a lower dimensionality. For the toy corpus above we used only\n 2 latent dimensions, but on real corpora, target dimensionality of 200--500 is recommended\n as a \"golden standard\" [1]_.\n\n .. sourcecode:: pycon\n\n model = models.LsiModel(tfidf_corpus, id2word=dictionary, num_topics=300)\n\n LSI training is unique in that we can continue \"training\" at any point, simply\n by providing more training documents. This is done by incremental updates to\n the underlying model, in a process called `online training`. Because of this feature, the\n input document stream may even be infinite -- just keep feeding LSI new documents\n as they arrive, while using the computed transformation model as read-only in the meanwhile!\n\n .. sourcecode:: pycon\n\n model.add_documents(another_tfidf_corpus) # now LSI has been trained on tfidf_corpus + another_tfidf_corpus\n lsi_vec = model[tfidf_vec] # convert some new document into the LSI space, without affecting the model\n\n model.add_documents(more_documents) # tfidf_corpus + another_tfidf_corpus + more_documents\n lsi_vec = model[tfidf_vec]\n\n See the :mod:`gensim.models.lsimodel` documentation for details on how to make\n LSI gradually \"forget\" old observations in infinite streams. If you want to get dirty,\n there are also parameters you can tweak that affect speed vs. memory footprint vs. numerical\n precision of the LSI algorithm.\n\n `gensim` uses a novel online incremental streamed distributed training algorithm (quite a mouthful!),\n which I published in [5]_. `gensim` also executes a stochastic multi-pass algorithm\n from Halko et al. [4]_ internally, to accelerate in-core part\n of the computations.\n See also `wiki` for further speed-ups by distributing the computation across\n a cluster of computers.\n\n* `Random Projections, RP `_ aim to\n reduce vector space dimensionality. This is a very efficient (both memory- and\n CPU-friendly) approach to approximating TfIdf distances between documents, by throwing in a little randomness.\n Recommended target dimensionality is again in the hundreds/thousands, depending on your dataset.\n\n .. sourcecode:: pycon\n\n model = models.RpModel(tfidf_corpus, num_topics=500)\n\n* `Latent Dirichlet Allocation, LDA `_\n is yet another transformation from bag-of-words counts into a topic space of lower\n dimensionality. LDA is a probabilistic extension of LSA (also called multinomial PCA),\n so LDA's topics can be interpreted as probability distributions over words. These distributions are,\n just like with LSA, inferred automatically from a training corpus. Documents\n are in turn interpreted as a (soft) mixture of these topics (again, just like with LSA).\n\n .. sourcecode:: pycon\n\n model = models.LdaModel(corpus, id2word=dictionary, num_topics=100)\n\n `gensim` uses a fast implementation of online LDA parameter estimation based on [2]_,\n modified to run in `distributed mode ` on a cluster of computers.\n\n* `Hierarchical Dirichlet Process, HDP `_\n is a non-parametric bayesian method (note the missing number of requested topics):\n\n .. sourcecode:: pycon\n\n model = models.HdpModel(corpus, id2word=dictionary)\n\n `gensim` uses a fast, online implementation based on [3]_.\n The HDP model is a new addition to `gensim`, and still rough around its academic edges -- use with care.\n\nAdding new :abbr:`VSM (Vector Space Model)` transformations (such as different weighting schemes) is rather trivial;\nsee the `apiref` or directly the `Python code `_\nfor more info and examples.\n\nIt is worth repeating that these are all unique, **incremental** implementations,\nwhich do not require the whole training corpus to be present in main memory all at once.\nWith memory taken care of, I am now improving `distributed`,\nto improve CPU efficiency, too.\nIf you feel you could contribute by testing, providing use-cases or code, see the `Gensim Developer guide `__.\n\n## What Next?\n\nContinue on to the next tutorial on `sphx_glr_auto_examples_core_run_similarity_queries.py`.\n\n## References\n\n.. [1] Bradford. 2008. An empirical study of required dimensionality for large-scale latent semantic indexing applications.\n\n.. [2] Hoffman, Blei, Bach. 2010. Online learning for Latent Dirichlet Allocation.\n\n.. [3] Wang, Paisley, Blei. 2011. Online variational inference for the hierarchical Dirichlet process.\n\n.. [4] Halko, Martinsson, Tropp. 2009. Finding structure with randomness.\n\n.. [5] \u0158eh\u016f\u0159ek. 2011. Subspace tracking for Latent Semantic Analysis.\n\n" ] }, { @@ -208,7 +208,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.8.13" } }, "nbformat": 4, diff --git a/docs/src/auto_examples/core/run_topics_and_transformations.py b/docs/src/auto_examples/core/run_topics_and_transformations.py index 605584084d..45888505e0 100644 --- a/docs/src/auto_examples/core/run_topics_and_transformations.py +++ b/docs/src/auto_examples/core/run_topics_and_transformations.py @@ -188,6 +188,20 @@ # # model = models.TfidfModel(corpus, normalize=True) # +# * `Okapi Best Matching, Okapi BM25 `_ +# expects a bag-of-words (integer values) training corpus during initialization. +# During transformation, it will take a vector and return another vector of the +# same dimensionality, except that features which were rare in the training corpus +# will have their value increased. It therefore converts integer-valued +# vectors into real-valued ones, while leaving the number of dimensions intact. +# +# Okapi BM25 is the standard ranking function used by search engines to estimate +# the relevance of documents to a given search query. +# +# .. sourcecode:: pycon +# +# model = models.OkapiBM25Model(corpus) +# # * `Latent Semantic Indexing, LSI (or sometimes LSA) `_ # transforms documents from either bag-of-words or (preferrably) TfIdf-weighted space into # a latent space of a lower dimensionality. For the toy corpus above we used only diff --git a/docs/src/auto_examples/core/run_topics_and_transformations.py.md5 b/docs/src/auto_examples/core/run_topics_and_transformations.py.md5 index 4ea3bee39d..ce683c931f 100644 --- a/docs/src/auto_examples/core/run_topics_and_transformations.py.md5 +++ b/docs/src/auto_examples/core/run_topics_and_transformations.py.md5 @@ -1 +1 @@ -f49c3821bbacdeefdf3945d5dcb5ad01 \ No newline at end of file +226db24f9e807e4bbd2a6ef280a75510 \ No newline at end of file diff --git a/docs/src/auto_examples/core/run_topics_and_transformations.rst b/docs/src/auto_examples/core/run_topics_and_transformations.rst index a5056ee4e3..64c9675939 100644 --- a/docs/src/auto_examples/core/run_topics_and_transformations.rst +++ b/docs/src/auto_examples/core/run_topics_and_transformations.rst @@ -1,12 +1,21 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples/core/run_topics_and_transformations.py" +.. LINE NUMBERS ARE GIVEN BELOW. + .. only:: html .. note:: :class: sphx-glr-download-link-note - Click :ref:`here ` to download the full example code - .. rst-class:: sphx-glr-example-title + Click :ref:`here ` + to download the full example code + +.. rst-class:: sphx-glr-example-title - .. _sphx_glr_auto_examples_core_run_topics_and_transformations.py: +.. _sphx_glr_auto_examples_core_run_topics_and_transformations.py: Topics and Transformations @@ -14,6 +23,7 @@ Topics and Transformations Introduces transformations and demonstrates their use on a toy corpus. +.. GENERATED FROM PYTHON SOURCE LINES 7-11 .. code-block:: default @@ -28,6 +38,8 @@ Introduces transformations and demonstrates their use on a toy corpus. +.. GENERATED FROM PYTHON SOURCE LINES 12-28 + In this tutorial, I will show how to transform documents from one vector representation into another. This process serves two goals: @@ -45,6 +57,7 @@ First, we need to create a corpus to work with. This step is the same as in the previous tutorial; if you completed it, feel free to skip to the next section. +.. GENERATED FROM PYTHON SOURCE LINES 28-65 .. code-block:: default @@ -89,8 +102,20 @@ if you completed it, feel free to skip to the next section. +.. rst-class:: sphx-glr-script-out + + Out: + + .. code-block:: none + + 2022-07-18 19:59:38,851 : INFO : adding document #0 to Dictionary<0 unique tokens: []> + 2022-07-18 19:59:38,852 : INFO : built Dictionary<12 unique tokens: ['computer', 'human', 'interface', 'response', 'survey']...> from 9 documents (total 29 corpus positions) + 2022-07-18 19:59:38,853 : INFO : Dictionary lifecycle event {'msg': "built Dictionary<12 unique tokens: ['computer', 'human', 'interface', 'response', 'survey']...> from 9 documents (total 29 corpus positions)", 'datetime': '2022-07-18T19:59:38.852315', 'gensim': '4.2.1.dev0', 'python': '3.8.13 (default, Jul 12 2022, 12:32:46) \n[GCC 10.2.1 20210110]', 'platform': 'Linux-5.10.0-0.bpo.12-amd64-x86_64-with-glibc2.2.5', 'event': 'created'} + + +.. GENERATED FROM PYTHON SOURCE LINES 66-72 Creating a transformation ++++++++++++++++++++++++++ @@ -99,6 +124,7 @@ The transformations are standard Python objects, typically initialized by means a :dfn:`training corpus`: +.. GENERATED FROM PYTHON SOURCE LINES 73-77 .. code-block:: default @@ -110,9 +136,21 @@ a :dfn:`training corpus`: +.. rst-class:: sphx-glr-script-out + + Out: + + .. code-block:: none + + 2022-07-18 19:59:38,866 : INFO : collecting document frequencies + 2022-07-18 19:59:38,866 : INFO : PROGRESS: processing document #0 + 2022-07-18 19:59:38,868 : INFO : TfidfModel lifecycle event {'msg': 'calculated IDF weights for 9 documents and 12 features (28 matrix non-zeros)', 'datetime': '2022-07-18T19:59:38.868192', 'gensim': '4.2.1.dev0', 'python': '3.8.13 (default, Jul 12 2022, 12:32:46) \n[GCC 10.2.1 20210110]', 'platform': 'Linux-5.10.0-0.bpo.12-amd64-x86_64-with-glibc2.2.5', 'event': 'initialize'} + +.. GENERATED FROM PYTHON SOURCE LINES 78-100 + We used our old corpus from tutorial 1 to initialize (train) the transformation model. Different transformations may require different initialization parameters; in case of TfIdf, the "training" consists simply of going through the supplied corpus once and computing document frequencies @@ -136,6 +174,7 @@ From now on, ``tfidf`` is treated as a read-only object that can be used to conv any vector from the old representation (bag-of-words integer counts) to the new representation (TfIdf real-valued weights): +.. GENERATED FROM PYTHON SOURCE LINES 100-104 .. code-block:: default @@ -158,8 +197,11 @@ any vector from the old representation (bag-of-words integer counts) to the new +.. GENERATED FROM PYTHON SOURCE LINES 105-106 + Or to apply a transformation to a whole corpus: +.. GENERATED FROM PYTHON SOURCE LINES 106-111 .. code-block:: default @@ -191,6 +233,8 @@ Or to apply a transformation to a whole corpus: +.. GENERATED FROM PYTHON SOURCE LINES 112-128 + In this particular case, we are transforming the same corpus that we used for training, but this is only incidental. Once the transformation model has been initialized, it can be used on any vectors (provided they come from the same vector space, of course), @@ -208,6 +252,7 @@ folding-in for LSA, by topic inference for LDA etc. Transformations can also be serialized, one on top of another, in a sort of chain: +.. GENERATED FROM PYTHON SOURCE LINES 128-132 .. code-block:: default @@ -219,13 +264,36 @@ Transformations can also be serialized, one on top of another, in a sort of chai +.. rst-class:: sphx-glr-script-out + + Out: + + .. code-block:: none + + 2022-07-18 19:59:39,141 : INFO : using serial LSI version on this node + 2022-07-18 19:59:39,142 : INFO : updating model with new documents + 2022-07-18 19:59:39,143 : INFO : preparing a new chunk of documents + 2022-07-18 19:59:39,144 : INFO : using 100 extra samples and 2 power iterations + 2022-07-18 19:59:39,144 : INFO : 1st phase: constructing (12, 102) action matrix + 2022-07-18 19:59:39,146 : INFO : orthonormalizing (12, 102) action matrix + 2022-07-18 19:59:39,148 : INFO : 2nd phase: running dense svd on (12, 9) matrix + 2022-07-18 19:59:39,148 : INFO : computing the final decomposition + 2022-07-18 19:59:39,149 : INFO : keeping 2 factors (discarding 47.565% of energy spectrum) + 2022-07-18 19:59:39,150 : INFO : processed documents up to #9 + 2022-07-18 19:59:39,151 : INFO : topic #0(1.594): 0.703*"trees" + 0.538*"graph" + 0.402*"minors" + 0.187*"survey" + 0.061*"system" + 0.060*"time" + 0.060*"response" + 0.058*"user" + 0.049*"computer" + 0.035*"interface" + 2022-07-18 19:59:39,151 : INFO : topic #1(1.476): -0.460*"system" + -0.373*"user" + -0.332*"eps" + -0.328*"interface" + -0.320*"time" + -0.320*"response" + -0.293*"computer" + -0.280*"human" + -0.171*"survey" + 0.161*"trees" + 2022-07-18 19:59:39,151 : INFO : LsiModel lifecycle event {'msg': 'trained LsiModel in 0.01s', 'datetime': '2022-07-18T19:59:39.151911', 'gensim': '4.2.1.dev0', 'python': '3.8.13 (default, Jul 12 2022, 12:32:46) \n[GCC 10.2.1 20210110]', 'platform': 'Linux-5.10.0-0.bpo.12-amd64-x86_64-with-glibc2.2.5', 'event': 'created'} + + +.. GENERATED FROM PYTHON SOURCE LINES 133-136 Here we transformed our Tf-Idf corpus via `Latent Semantic Indexing `_ into a latent 2-D space (2-D because we set ``num_topics=2``). Now you're probably wondering: what do these two latent dimensions stand for? Let's inspect with :func:`models.LsiModel.print_topics`: +.. GENERATED FROM PYTHON SOURCE LINES 136-139 .. code-block:: default @@ -242,11 +310,15 @@ dimensions stand for? Let's inspect with :func:`models.LsiModel.print_topics`: .. code-block:: none + 2022-07-18 19:59:39,298 : INFO : topic #0(1.594): 0.703*"trees" + 0.538*"graph" + 0.402*"minors" + 0.187*"survey" + 0.061*"system" + 0.060*"time" + 0.060*"response" + 0.058*"user" + 0.049*"computer" + 0.035*"interface" + 2022-07-18 19:59:39,298 : INFO : topic #1(1.476): -0.460*"system" + -0.373*"user" + -0.332*"eps" + -0.328*"interface" + -0.320*"time" + -0.320*"response" + -0.293*"computer" + -0.280*"human" + -0.171*"survey" + 0.161*"trees" - [(0, '0.703*"trees" + 0.538*"graph" + 0.402*"minors" + 0.187*"survey" + 0.061*"system" + 0.060*"response" + 0.060*"time" + 0.058*"user" + 0.049*"computer" + 0.035*"interface"'), (1, '-0.460*"system" + -0.373*"user" + -0.332*"eps" + -0.328*"interface" + -0.320*"response" + -0.320*"time" + -0.293*"computer" + -0.280*"human" + -0.171*"survey" + 0.161*"trees"')] + [(0, '0.703*"trees" + 0.538*"graph" + 0.402*"minors" + 0.187*"survey" + 0.061*"system" + 0.060*"time" + 0.060*"response" + 0.058*"user" + 0.049*"computer" + 0.035*"interface"'), (1, '-0.460*"system" + -0.373*"user" + -0.332*"eps" + -0.328*"interface" + -0.320*"time" + -0.320*"response" + -0.293*"computer" + -0.280*"human" + -0.171*"survey" + 0.161*"trees"')] +.. GENERATED FROM PYTHON SOURCE LINES 140-148 + (the topics are printed to log -- see the note at the top of this page about activating logging) @@ -256,6 +328,7 @@ second topic practically concerns itself with all the other words. As expected, the first five documents are more strongly related to the second topic while the remaining four documents to the first topic: +.. GENERATED FROM PYTHON SOURCE LINES 148-153 .. code-block:: default @@ -274,21 +347,24 @@ remaining four documents to the first topic: .. code-block:: none - [(0, 0.06600783396090518), (1, -0.520070330636184)] Human machine interface for lab abc computer applications - [(0, 0.19667592859142694), (1, -0.7609563167700047)] A survey of user opinion of computer system response time - [(0, 0.08992639972446678), (1, -0.72418606267525)] The EPS user interface management system - [(0, 0.07585847652178407), (1, -0.6320551586003417)] System and human system engineering testing of EPS - [(0, 0.10150299184980252), (1, -0.5737308483002961)] Relation of user perceived response time to error measurement - [(0, 0.7032108939378307), (1, 0.16115180214025954)] The generation of random binary unordered trees - [(0, 0.8774787673119826), (1, 0.16758906864659615)] The intersection graph of paths in trees - [(0, 0.9098624686818572), (1, 0.14086553628719237)] Graph minors IV Widths of trees and well quasi ordering - [(0, 0.6165825350569278), (1, -0.05392907566389235)] Graph minors A survey + [(0, 0.06600783396090446), (1, -0.5200703306361851)] Human machine interface for lab abc computer applications + [(0, 0.19667592859142627), (1, -0.7609563167700037)] A survey of user opinion of computer system response time + [(0, 0.08992639972446514), (1, -0.724186062675251)] The EPS user interface management system + [(0, 0.07585847652178207), (1, -0.632055158600343)] System and human system engineering testing of EPS + [(0, 0.10150299184980262), (1, -0.5737308483002944)] Relation of user perceived response time to error measurement + [(0, 0.703210893937831), (1, 0.16115180214025884)] The generation of random binary unordered trees + [(0, 0.8774787673119828), (1, 0.16758906864659542)] The intersection graph of paths in trees + [(0, 0.9098624686818574), (1, 0.14086553628719167)] Graph minors IV Widths of trees and well quasi ordering + [(0, 0.6165825350569278), (1, -0.05392907566389242)] Graph minors A survey + +.. GENERATED FROM PYTHON SOURCE LINES 154-155 Model persistency is achieved with the :func:`save` and :func:`load` functions: +.. GENERATED FROM PYTHON SOURCE LINES 155-165 .. code-block:: default @@ -306,8 +382,30 @@ Model persistency is achieved with the :func:`save` and :func:`load` functions: +.. rst-class:: sphx-glr-script-out + + Out: + + .. code-block:: none + + 2022-07-18 19:59:39,441 : INFO : Projection lifecycle event {'fname_or_handle': '/tmp/model-rpai5uj5.lsi.projection', 'separately': 'None', 'sep_limit': 10485760, 'ignore': frozenset(), 'datetime': '2022-07-18T19:59:39.441811', 'gensim': '4.2.1.dev0', 'python': '3.8.13 (default, Jul 12 2022, 12:32:46) \n[GCC 10.2.1 20210110]', 'platform': 'Linux-5.10.0-0.bpo.12-amd64-x86_64-with-glibc2.2.5', 'event': 'saving'} + 2022-07-18 19:59:39,443 : INFO : saved /tmp/model-rpai5uj5.lsi.projection + 2022-07-18 19:59:39,443 : INFO : LsiModel lifecycle event {'fname_or_handle': '/tmp/model-rpai5uj5.lsi', 'separately': 'None', 'sep_limit': 10485760, 'ignore': ['projection', 'dispatcher'], 'datetime': '2022-07-18T19:59:39.443722', 'gensim': '4.2.1.dev0', 'python': '3.8.13 (default, Jul 12 2022, 12:32:46) \n[GCC 10.2.1 20210110]', 'platform': 'Linux-5.10.0-0.bpo.12-amd64-x86_64-with-glibc2.2.5', 'event': 'saving'} + 2022-07-18 19:59:39,444 : INFO : not storing attribute projection + 2022-07-18 19:59:39,444 : INFO : not storing attribute dispatcher + 2022-07-18 19:59:39,444 : INFO : saved /tmp/model-rpai5uj5.lsi + 2022-07-18 19:59:39,444 : INFO : loading LsiModel object from /tmp/model-rpai5uj5.lsi + 2022-07-18 19:59:39,445 : INFO : loading id2word recursively from /tmp/model-rpai5uj5.lsi.id2word.* with mmap=None + 2022-07-18 19:59:39,445 : INFO : setting ignored attribute projection to None + 2022-07-18 19:59:39,445 : INFO : setting ignored attribute dispatcher to None + 2022-07-18 19:59:39,445 : INFO : LsiModel lifecycle event {'fname': '/tmp/model-rpai5uj5.lsi', 'datetime': '2022-07-18T19:59:39.445641', 'gensim': '4.2.1.dev0', 'python': '3.8.13 (default, Jul 12 2022, 12:32:46) \n[GCC 10.2.1 20210110]', 'platform': 'Linux-5.10.0-0.bpo.12-amd64-x86_64-with-glibc2.2.5', 'event': 'loaded'} + 2022-07-18 19:59:39,445 : INFO : loading LsiModel object from /tmp/model-rpai5uj5.lsi.projection + 2022-07-18 19:59:39,446 : INFO : Projection lifecycle event {'fname': '/tmp/model-rpai5uj5.lsi.projection', 'datetime': '2022-07-18T19:59:39.446113', 'gensim': '4.2.1.dev0', 'python': '3.8.13 (default, Jul 12 2022, 12:32:46) \n[GCC 10.2.1 20210110]', 'platform': 'Linux-5.10.0-0.bpo.12-amd64-x86_64-with-glibc2.2.5', 'event': 'loaded'} + + +.. GENERATED FROM PYTHON SOURCE LINES 166-301 The next question might be: just how exactly similar are those documents to each other? Is there a way to formalize the similarity, so that for a given input document, we can @@ -334,6 +432,20 @@ Gensim implements several popular Vector Space Model algorithms: model = models.TfidfModel(corpus, normalize=True) +* `Okapi Best Matching, Okapi BM25 `_ + expects a bag-of-words (integer values) training corpus during initialization. + During transformation, it will take a vector and return another vector of the + same dimensionality, except that features which were rare in the training corpus + will have their value increased. It therefore converts integer-valued + vectors into real-valued ones, while leaving the number of dimensions intact. + + Okapi BM25 is the standard ranking function used by search engines to estimate + the relevance of documents to a given search query. + + .. sourcecode:: pycon + + model = models.OkapiBM25Model(corpus) + * `Latent Semantic Indexing, LSI (or sometimes LSA) `_ transforms documents from either bag-of-words or (preferrably) TfIdf-weighted space into a latent space of a lower dimensionality. For the toy corpus above we used only @@ -431,6 +543,7 @@ References .. [5] Řehůřek. 2011. Subspace tracking for Latent Semantic Analysis. +.. GENERATED FROM PYTHON SOURCE LINES 301-307 .. code-block:: default @@ -443,9 +556,10 @@ References -.. image:: /auto_examples/core/images/sphx_glr_run_topics_and_transformations_001.png - :alt: run topics and transformations - :class: sphx-glr-single-img +.. image-sg:: /auto_examples/core/images/sphx_glr_run_topics_and_transformations_001.png + :alt: run topics and transformations + :srcset: /auto_examples/core/images/sphx_glr_run_topics_and_transformations_001.png + :class: sphx-glr-single-img @@ -454,9 +568,9 @@ References .. rst-class:: sphx-glr-timing - **Total running time of the script:** ( 0 minutes 0.970 seconds) + **Total running time of the script:** ( 0 minutes 1.658 seconds) -**Estimated memory usage:** 7 MB +**Estimated memory usage:** 58 MB .. _sphx_glr_download_auto_examples_core_run_topics_and_transformations.py: diff --git a/docs/src/auto_examples/core/sg_execution_times.rst b/docs/src/auto_examples/core/sg_execution_times.rst index e206b6d636..26ebfe6747 100644 --- a/docs/src/auto_examples/core/sg_execution_times.rst +++ b/docs/src/auto_examples/core/sg_execution_times.rst @@ -5,14 +5,14 @@ Computation times ================= -**00:05.212** total execution time for **auto_examples_core** files: +**00:01.658** total execution time for **auto_examples_core** files: +--------------------------------------------------------------------------------------------------------------+-----------+---------+ -| :ref:`sphx_glr_auto_examples_core_run_corpora_and_vector_spaces.py` (``run_corpora_and_vector_spaces.py``) | 00:05.212 | 47.2 MB | +| :ref:`sphx_glr_auto_examples_core_run_topics_and_transformations.py` (``run_topics_and_transformations.py``) | 00:01.658 | 58.1 MB | +--------------------------------------------------------------------------------------------------------------+-----------+---------+ | :ref:`sphx_glr_auto_examples_core_run_core_concepts.py` (``run_core_concepts.py``) | 00:00.000 | 0.0 MB | +--------------------------------------------------------------------------------------------------------------+-----------+---------+ -| :ref:`sphx_glr_auto_examples_core_run_similarity_queries.py` (``run_similarity_queries.py``) | 00:00.000 | 0.0 MB | +| :ref:`sphx_glr_auto_examples_core_run_corpora_and_vector_spaces.py` (``run_corpora_and_vector_spaces.py``) | 00:00.000 | 0.0 MB | +--------------------------------------------------------------------------------------------------------------+-----------+---------+ -| :ref:`sphx_glr_auto_examples_core_run_topics_and_transformations.py` (``run_topics_and_transformations.py``) | 00:00.000 | 0.0 MB | +| :ref:`sphx_glr_auto_examples_core_run_similarity_queries.py` (``run_similarity_queries.py``) | 00:00.000 | 0.0 MB | +--------------------------------------------------------------------------------------------------------------+-----------+---------+ diff --git a/docs/src/auto_examples/index.rst b/docs/src/auto_examples/index.rst index ca47a1738e..a1f42f795e 100644 --- a/docs/src/auto_examples/index.rst +++ b/docs/src/auto_examples/index.rst @@ -220,7 +220,7 @@ Learning-oriented lessons that introduce a particular gensim feature, e.g. a mod .. raw:: html -
+
.. only:: html @@ -237,7 +237,7 @@ Learning-oriented lessons that introduce a particular gensim feature, e.g. a mod .. raw:: html -
+
.. only:: html diff --git a/docs/src/gallery/core/run_topics_and_transformations.py b/docs/src/gallery/core/run_topics_and_transformations.py index 605584084d..45888505e0 100644 --- a/docs/src/gallery/core/run_topics_and_transformations.py +++ b/docs/src/gallery/core/run_topics_and_transformations.py @@ -188,6 +188,20 @@ # # model = models.TfidfModel(corpus, normalize=True) # +# * `Okapi Best Matching, Okapi BM25 `_ +# expects a bag-of-words (integer values) training corpus during initialization. +# During transformation, it will take a vector and return another vector of the +# same dimensionality, except that features which were rare in the training corpus +# will have their value increased. It therefore converts integer-valued +# vectors into real-valued ones, while leaving the number of dimensions intact. +# +# Okapi BM25 is the standard ranking function used by search engines to estimate +# the relevance of documents to a given search query. +# +# .. sourcecode:: pycon +# +# model = models.OkapiBM25Model(corpus) +# # * `Latent Semantic Indexing, LSI (or sometimes LSA) `_ # transforms documents from either bag-of-words or (preferrably) TfIdf-weighted space into # a latent space of a lower dimensionality. For the toy corpus above we used only diff --git a/gensim/models/__init__.py b/gensim/models/__init__.py index ac08d1fdb4..d9a28ead34 100644 --- a/gensim/models/__init__.py +++ b/gensim/models/__init__.py @@ -9,6 +9,7 @@ from .ldamodel import LdaModel # noqa:F401 from .lsimodel import LsiModel # noqa:F401 from .tfidfmodel import TfidfModel # noqa:F401 +from .bm25model import OkapiBM25Model, LuceneBM25Model, AtireBM25Model # noqa:F401 from .rpmodel import RpModel # noqa:F401 from .logentropy_model import LogEntropyModel # noqa:F401 from .word2vec import Word2Vec, FAST_VERSION # noqa:F401 diff --git a/gensim/models/bm25model.py b/gensim/models/bm25model.py new file mode 100644 index 0000000000..265afb3af9 --- /dev/null +++ b/gensim/models/bm25model.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""This module implements functionality related to the `Okapi Best Matching +`_ class of bag-of-words vector space models. + +Robertson and Zaragoza [1]_ describe the original algorithm and its modifications. + +.. [1] Robertson S., Zaragoza H. (2015). `The Probabilistic Relevance Framework: BM25 and + Beyond, `_. + +""" + +from abc import ABCMeta, abstractmethod +from collections import defaultdict +import logging +import math + +from gensim import interfaces, utils +import numpy as np + + +logger = logging.getLogger(__name__) + + +class BM25ABC(interfaces.TransformationABC, metaclass=ABCMeta): + """Objects of this abstract class realize the transformation between word-document co-occurrence + matrix (int) into a BM25 matrix (positive floats). Concrete subclasses of this abstract class + implement different BM25 scoring functions. + + """ + def __init__(self, corpus=None, dictionary=None): + r"""Pre-compute the average length of a document and inverse term document frequencies, + which will be used to weight term frequencies for the documents. + + Parameters + ---------- + corpus : iterable of iterable of (int, int) or None, optional + An input corpus, which will be used to compute the average length of a document and + inverse term document frequencies. If None, then `dictionary` will be used to compute + the statistics. If both `corpus` and `dictionary` are None, the statistics will be left + unintialized. Default is None. + dictionary : :class:`~gensim.corpora.Dictionary` + An input dictionary, which will be used to compute the average length of a document and + inverse term document frequencies. If None, then `corpus` will be used to compute the + statistics. If both `corpus` and `dictionary` are None, the statistics will be left + unintialized. Default is None. + + Attributes + ---------- + avgdl : float + The average length of a document. + idfs : dict of (int, float) + A mapping from term ids to inverse term document frequencies. + + """ + self.avgdl, self.idfs = None, None + if dictionary: + if corpus: + logger.warning("constructor received both corpus and dictionary; ignoring the corpus") + num_tokens = sum(dictionary.cfs.values()) + self.avgdl = num_tokens / dictionary.num_docs + self.idfs = self.precompute_idfs(dictionary.dfs, dictionary.num_docs) + elif corpus: + dfs = defaultdict(lambda: 0) + num_tokens = 0 + num_docs = 0 + for bow in corpus: + num_tokens += len(bow) + for term_id in set(term_id for term_id, _ in bow): + dfs[term_id] += 1 + num_docs += 1 + self.avgdl = num_tokens / num_docs + self.idfs = self.precompute_idfs(dfs, num_docs) + else: + pass + + @abstractmethod + def precompute_idfs(self, dfs, num_docs): + """Precompute inverse term document frequencies, which will be used to weight term frequencies + for the documents. + + Parameters + ---------- + dfs : dict of (int, int) + A mapping from term ids to term document frequencies. + num_docs : int + The total number of documents in the training corpus. + + Returns + ------- + idfs : dict of (int, float) + A mapping from term ids to inverse term document frequencies. + + """ + pass + + @abstractmethod + def get_term_weights(self, num_tokens, term_frequencies, idfs): + """Compute vector space weights for a set of terms in a document. + + Parameters + ---------- + num_tokens : int + The number of tokens in the document. + term_frequencies : ndarray + 1D array of term frequencies. + idfs : ndarray + 1D array of inverse term document frequencies. + + Returns + ------- + term_weights : ndarray + 1D array of vector space weights. + + """ + pass + + def __getitem__(self, bow): + is_corpus, bow = utils.is_corpus(bow) + if is_corpus: + return self._apply(bow) + + num_tokens = sum(freq for term_id, freq in bow) + + term_ids, term_frequencies, idfs = [], [], [] + for term_id, term_frequency in bow: + term_ids.append(term_id) + term_frequencies.append(term_frequency) + idfs.append(self.idfs.get(term_id) or 0.0) + term_frequencies, idfs = np.array(term_frequencies), np.array(idfs) + + term_weights = self.get_term_weights(num_tokens, term_frequencies, idfs) + + vector = [ + (term_id, float(weight)) + for term_id, weight + in zip(term_ids, term_weights) + ] + return vector + + +class OkapiBM25Model(BM25ABC): + """The original Okapi BM25 scoring function of Robertson et al. [2]_. + + Examples + -------- + .. sourcecode:: pycon + + >>> from gensim.corpora import Dictionary + >>> from gensim.models import OkapiBM25Model + >>> from gensim.test.utils import common_texts + >>> + >>> dictionary = Dictionary(common_texts) # fit dictionary + >>> model = OkapiBM25Model(dictionary=dictionary) # fit model + >>> + >>> corpus = [dictionary.doc2bow(line) for line in common_texts] # convert corpus to BoW format + >>> vector = model[corpus[0]] # apply model to the first corpus document + + References + ---------- + .. [2] Robertson S. E., Walker S., Jones S., Hancock-Beaulieu M. M., Gatford M. (1995). + `Okapi at TREC-3 `_. + *NIST Special Publication 500-226*. + + """ + def __init__(self, corpus=None, dictionary=None, k1=1.5, b=0.75, epsilon=0.25): + r"""Pre-compute the average length of a document and inverse term document frequencies, + which will be used to weight term frequencies for the documents. + + Parameters + ---------- + corpus : iterable of iterable of (int, int) or None, optional + An input corpus, which will be used to compute the average length of a document and + inverse term document frequencies. If None, then `dictionary` will be used to compute + the statistics. If both `corpus` and `dictionary` are None, the statistics will be left + unintialized. Default is None. + dictionary : :class:`~gensim.corpora.Dictionary` + An input dictionary, which will be used to compute the average length of a document and + inverse term document frequencies. If None, then `corpus` will be used to compute the + statistics. If both `corpus` and `dictionary` are None, the statistics will be left + unintialized. Default is None. + k1 : float + A positive tuning parameter that determines the impact of the term frequency on its BM25 + weight. Singhal [5]_ suggests to set `k1` between 1.0 and 2.0. Default is 1.5. + b : float + A tuning parameter between 0.0 and 1.0 that determines the document length + normalization: 1.0 corresponds to full document normalization, while 0.0 corresponds to + no length normalization. Singhal [5]_ suggests to set `b` to 0.75, which is the default. + epsilon : float + A positive tuning parameter that lower-bounds an inverse document frequency. + Defaults to 0.25. + + Attributes + ---------- + k1 : float + A positive tuning parameter that determines the impact of the term frequency on its BM25 + weight. Singhal [3]_ suggests to set `k1` between 1.0 and 2.0. Default is 1.5. + b : float + A tuning parameter between 0.0 and 1.0 that determines the document length + normalization: 1.0 corresponds to full document normalization, while 0.0 corresponds to + no length normalization. Singhal [3]_ suggests to set `b` to 0.75, which is the default. + epsilon : float + A positive tuning parameter that lower-bounds an inverse document frequency. + Defaults to 0.25. + + References + ---------- + .. [3] Singhal, A. (2001). `Modern information retrieval: A brief overview + `_. *IEEE Data Eng. Bull.*, 24(4), 35–43. + + """ + self.k1, self.b, self.epsilon = k1, b, epsilon + super().__init__(corpus, dictionary) + + def precompute_idfs(self, dfs, num_docs): + idf_sum = 0 + idfs = dict() + negative_idfs = [] + for term_id, freq in dfs.items(): + idf = math.log(num_docs - freq + 0.5) - math.log(freq + 0.5) + idfs[term_id] = idf + idf_sum += idf + if idf < 0: + negative_idfs.append(term_id) + average_idf = idf_sum / len(idfs) + + eps = self.epsilon * average_idf + for term_id in negative_idfs: + idfs[term_id] = eps + + return idfs + + def get_term_weights(self, num_tokens, term_frequencies, idfs): + term_weights = idfs * (term_frequencies * (self.k1 + 1) + / (term_frequencies + self.k1 * (1 - self.b + self.b + * num_tokens / self.avgdl))) + return term_weights + + +class LuceneBM25Model(BM25ABC): + """The scoring function of Apache Lucene 8 [4]_. + + Examples + -------- + .. sourcecode:: pycon + + >>> from gensim.corpora import Dictionary + >>> from gensim.models import LuceneBM25Model + >>> from gensim.test.utils import common_texts + >>> + >>> dictionary = Dictionary(common_texts) # fit dictionary + >>> corpus = [dictionary.doc2bow(line) for line in common_texts] # convert corpus to BoW format + >>> + >>> model = LuceneBM25Model(dictionary=dictionary) # fit model + >>> vector = model[corpus[0]] # apply model to the first corpus document + + References + ---------- + .. [4] Kamphuis, C., de Vries, A. P., Boytsov, L., Lin, J. (2020). Which + BM25 Do You Mean? `A Large-Scale Reproducibility Study of Scoring Variants + `_. In: Advances in Information Retrieval. + 28–34. + + """ + def __init__(self, corpus=None, dictionary=None, k1=1.5, b=0.75): + r"""Pre-compute the average length of a document and inverse term document frequencies, + which will be used to weight term frequencies for the documents. + + Parameters + ---------- + corpus : iterable of iterable of (int, int) or None, optional + An input corpus, which will be used to compute the average length of a document and + inverse term document frequencies. If None, then `dictionary` will be used to compute + the statistics. If both `corpus` and `dictionary` are None, the statistics will be left + unintialized. Default is None. + dictionary : :class:`~gensim.corpora.Dictionary` + An input dictionary, which will be used to compute the average length of a document and + inverse term document frequencies. If None, then `corpus` will be used to compute the + statistics. If both `corpus` and `dictionary` are None, the statistics will be left + unintialized. Default is None. + k1 : float + A positive tuning parameter that determines the impact of the term frequency on its BM25 + weight. Singhal [5]_ suggests to set `k1` between 1.0 and 2.0. Default is 1.5. + b : float + A tuning parameter between 0.0 and 1.0 that determines the document length + normalization: 1.0 corresponds to full document normalization, while 0.0 corresponds to + no length normalization. Singhal [5]_ suggests to set `b` to 0.75, which is the default. + + Attributes + ---------- + k1 : float + A positive tuning parameter that determines the impact of the term frequency on its BM25 + weight. Singhal [3]_ suggests to set `k1` between 1.0 and 2.0. Default is 1.5. + b : float + A tuning parameter between 0.0 and 1.0 that determines the document length + normalization: 1.0 corresponds to full document normalization, while 0.0 corresponds to + no length normalization. Singhal [3]_ suggests to set `b` to 0.75, which is the default. + + """ + self.k1, self.b = k1, b + super().__init__(corpus, dictionary) + + def precompute_idfs(self, dfs, num_docs): + idfs = dict() + for term_id, freq in dfs.items(): + idf = math.log(num_docs + 1.0) - math.log(freq + 0.5) + idfs[term_id] = idf + return idfs + + def get_term_weights(self, num_tokens, term_frequencies, idfs): + term_weights = idfs * (term_frequencies + / (term_frequencies + self.k1 * (1 - self.b + self.b + * num_tokens / self.avgdl))) + return term_weights + + +class AtireBM25Model(BM25ABC): + """The scoring function of Trotman et al. [5]_. + + Examples + -------- + .. sourcecode:: pycon + + >>> from gensim.corpora import Dictionary + >>> from gensim.models import AtireBM25Model + >>> from gensim.test.utils import common_texts + >>> + >>> dictionary = Dictionary(common_texts) # fit dictionary + >>> corpus = [dictionary.doc2bow(line) for line in common_texts] # convert corpus to BoW format + >>> + >>> model = AtireBM25Model(dictionary=dictionary) # fit model + >>> vector = model[corpus[0]] # apply model to the first corpus document + + References + ---------- + .. [5] Trotman, A., Jia X., Crane M., `Towards an Efficient and Effective Search Engine + `_, + In: SIGIR 2012 Workshop on Open Source Information Retrieval. 40–47. + + """ + def __init__(self, corpus=None, dictionary=None, k1=1.5, b=0.75): + r"""Pre-compute the average length of a document and inverse term document frequencies, + which will be used to weight term frequencies for the documents. + + Parameters + ---------- + corpus : iterable of iterable of (int, int) or None, optional + An input corpus, which will be used to compute the average length of a document and + inverse term document frequencies. If None, then `dictionary` will be used to compute + the statistics. If both `corpus` and `dictionary` are None, the statistics will be left + unintialized. Default is None. + dictionary : :class:`~gensim.corpora.Dictionary` + An input dictionary, which will be used to compute the average length of a document and + inverse term document frequencies. If None, then `corpus` will be used to compute the + statistics. If both `corpus` and `dictionary` are None, the statistics will be left + unintialized. Default is None. + k1 : float + A positive tuning parameter that determines the impact of the term frequency on its BM25 + weight. Singhal [5]_ suggests to set `k1` between 1.0 and 2.0. Default is 1.5. + b : float + A tuning parameter between 0.0 and 1.0 that determines the document length + normalization: 1.0 corresponds to full document normalization, while 0.0 corresponds to + no length normalization. Singhal [5]_ suggests to set `b` to 0.75, which is the default. + + Attributes + ---------- + k1 : float + A positive tuning parameter that determines the impact of the term frequency on its BM25 + weight. Singhal [3]_ suggests to set `k1` between 1.0 and 2.0. Default is 1.5. + b : float + A tuning parameter between 0.0 and 1.0 that determines the document length + normalization: 1.0 corresponds to full document normalization, while 0.0 corresponds to + no length normalization. Singhal [3]_ suggests to set `b` to 0.75, which is the default. + + """ + self.k1, self.b = k1, b + super().__init__(corpus, dictionary) + + def precompute_idfs(self, dfs, num_docs): + idfs = dict() + for term_id, freq in dfs.items(): + idf = math.log(num_docs) - math.log(freq) + idfs[term_id] = idf + return idfs + + def get_term_weights(self, num_tokens, term_frequencies, idfs): + term_weights = idfs * (term_frequencies * (self.k1 + 1) + / (term_frequencies + self.k1 * (1 - self.b + self.b + * num_tokens / self.avgdl))) + return term_weights diff --git a/gensim/similarities/docsim.py b/gensim/similarities/docsim.py index cdb966547d..a6d5e21bce 100644 --- a/gensim/similarities/docsim.py +++ b/gensim/similarities/docsim.py @@ -73,6 +73,7 @@ import itertools import os import heapq +import warnings import numpy import scipy.sparse @@ -906,7 +907,8 @@ class SoftCosineSimilarity(interfaces.SimilarityABC): for more examples. """ - def __init__(self, corpus, similarity_matrix, num_best=None, chunksize=256, normalized=(True, True)): + def __init__(self, corpus, similarity_matrix, num_best=None, chunksize=256, normalized=None, + normalize_queries=True, normalize_documents=True): """ Parameters @@ -919,12 +921,19 @@ def __init__(self, corpus, similarity_matrix, num_best=None, chunksize=256, norm The number of results to retrieve for a query, if None - return similarities with all elements from corpus. chunksize: int, optional Size of one corpus chunk. - normalized : tuple of {True, False, 'maintain'}, optional - First/second value specifies whether the query/document vectors in the inner product - will be L2-normalized (True; corresponds to the soft cosine similarity measure; default), - maintain their L2-norm during change of basis ('maintain'; corresponds to query - expansion with partial membership), or kept as-is (False; - corresponds to query expansion). + normalized : tuple of {True, False, 'maintain', None}, optional + A deprecated alias for `(normalize_queries, normalize_documents)`. If None, use + `normalize_queries` and `normalize_documents`. Default is None. + normalize_queries : {True, False, 'maintain'}, optional + Whether the query vector in the inner product will be L2-normalized (True; corresponds + to the soft cosine similarity measure; default), maintain their L2-norm during change of + basis ('maintain'; corresponds to queryexpansion with partial membership), or kept as-is + (False; corresponds to query expansion). + normalize_documents : {True, False, 'maintain'}, optional + Whether the document vector in the inner product will be L2-normalized (True; corresponds + to the soft cosine similarity measure; default), maintain their L2-norm during change of + basis ('maintain'; corresponds to queryexpansion with partial membership), or kept as-is + (False; corresponds to query expansion). See Also -------- @@ -941,7 +950,14 @@ def __init__(self, corpus, similarity_matrix, num_best=None, chunksize=256, norm self.corpus = list(corpus) self.num_best = num_best self.chunksize = chunksize - self.normalized = normalized + if normalized is not None: + warnings.warn( + 'Parameter normalized will be removed in 5.0.0, use normalize_queries and normalize_documents instead', + category=DeprecationWarning, + ) + self.normalized = normalized + else: + self.normalized = (normalize_queries, normalize_documents) # Normalization of features is undesirable, since soft cosine similarity requires special # normalization using the similarity matrix. Therefore, we would just be normalizing twice, @@ -1102,6 +1118,50 @@ def __str__(self): class SparseMatrixSimilarity(interfaces.SimilarityABC): """Compute cosine similarity against a corpus of documents by storing the index matrix in memory. + Examples + -------- + Here is how you would index and query a corpus of documents in the bag-of-words format using the + cosine similarity: + + .. sourcecode:: pycon + + >>> from gensim.corpora import Dictionary + >>> from gensim.similarities import SparseMatrixSimilarity + >>> from gensim.test.utils import common_texts as corpus + >>> + >>> dictionary = Dictionary(corpus) # fit dictionary + >>> bow_corpus = [dictionary.doc2bow(line) for line in corpus] # convert corpus to BoW format + >>> index = SparseMatrixSimilarity(bm25_corpus, num_docs=len(corpus), num_terms=len(dictionary)) + >>> + >>> query = 'graph trees computer'.split() # make a query + >>> bow_query = dictionary.doc2bow(query) + >>> similarities = index[bow_query] # calculate similarity of query to each doc from bow_corpus + + Here is how you would index and query a corpus of documents using the Okapi BM25 scoring + function: + + .. sourcecode:: pycon + + >>> from gensim.corpora import Dictionary + >>> from gensim.models import TfidfModel, OkapiBM25Model + >>> from gensim.similarities import SparseMatrixSimilarity + >>> from gensim.test.utils import common_texts as corpus + >>> + >>> dictionary = Dictionary(corpus) # fit dictionary + >>> query_model = TfidfModel(dictionary=dictionary, smartirs='bnn') # enforce binary weights + >>> document_model = OkapiBM25Model(dictionary=dictionary) # fit bm25 model + >>> + >>> bow_corpus = [dictionary.doc2bow(line) for line in corpus] # convert corpus to BoW format + >>> bm25_corpus = document_model[bow_corpus] + >>> index = SparseMatrixSimilarity(bm25_corpus, num_docs=len(corpus), num_terms=len(dictionary), + ... normalize_queries=False, normalize_documents=False) + >>> + >>> + >>> query = 'graph trees computer'.split() # make a query + >>> bow_query = dictionary.doc2bow(query) + >>> bm25_query = query_model[bow_query] + >>> similarities = index[bm25_query] # calculate similarity of query to each doc from bow_corpus + Notes ----- Use this if your input corpus contains sparse vectors (such as TF-IDF documents) and fits into RAM. @@ -1122,7 +1182,8 @@ class SparseMatrixSimilarity(interfaces.SimilarityABC): """ def __init__(self, corpus, num_features=None, num_terms=None, num_docs=None, num_nnz=None, - num_best=None, chunksize=500, dtype=numpy.float32, maintain_sparsity=False): + num_best=None, chunksize=500, dtype=numpy.float32, maintain_sparsity=False, + normalize_queries=True, normalize_documents=True): """ Parameters @@ -1146,10 +1207,15 @@ def __init__(self, corpus, num_features=None, num_terms=None, num_docs=None, num Data type of the internal matrix. maintain_sparsity : bool, optional Return sparse arrays from :meth:`~gensim.similarities.docsim.SparseMatrixSimilarity.get_similarities`? - + normalize_queries : bool, optional + If queries are in bag-of-words (int, float) format, as opposed to a sparse or dense + 2D arrays, they will be L2-normalized. Default is True. + normalize_documents : bool, optional + If `corpus` is in bag-of-words (int, float) format, as opposed to a sparse or dense + 2D arrays, it will be L2-normalized. Default is True. """ self.num_best = num_best - self.normalize = True + self.normalize = normalize_queries self.chunksize = chunksize self.maintain_sparsity = maintain_sparsity @@ -1173,7 +1239,7 @@ def __init__(self, corpus, num_features=None, num_terms=None, num_docs=None, num raise ValueError("refusing to guess the number of sparse features: specify num_features explicitly") corpus = (matutils.scipy2sparse(v) if scipy.sparse.issparse(v) else (matutils.full2sparse(v) if isinstance(v, numpy.ndarray) else - matutils.unitvec(v)) for v in corpus) + matutils.unitvec(v) if normalize_documents else v) for v in corpus) self.index = matutils.corpus2csc( corpus, num_terms=num_terms, num_docs=num_docs, num_nnz=num_nnz, dtype=dtype, printprogress=10000, diff --git a/gensim/test/test_bm25model.py b/gensim/test/test_bm25model.py new file mode 100644 index 0000000000..4cb9ca49ee --- /dev/null +++ b/gensim/test/test_bm25model.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from collections import defaultdict +import math +import unittest + +from gensim.models.bm25model import BM25ABC +from gensim.models import OkapiBM25Model, LuceneBM25Model, AtireBM25Model + +from gensim.corpora import Dictionary + + +class BM25Stub(BM25ABC): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def precompute_idfs(self, dfs, num_docs): + return dict() + + def get_term_weights(self, num_tokens, term_frequencies, idfs): + return term_frequencies + + +class BM25ABCTest(unittest.TestCase): + def setUp(self): + self.documents = [['cat', 'dog', 'mouse'], ['cat', 'lion'], ['cat', 'lion']] + self.dictionary = Dictionary(self.documents) + + self.expected_avgdl = sum(map(len, self.documents)) / len(self.documents) + + def test_avgdl_from_corpus(self): + corpus = list(map(self.dictionary.doc2bow, self.documents)) + model = BM25Stub(corpus=corpus) + actual_avgdl = model.avgdl + self.assertAlmostEqual(self.expected_avgdl, actual_avgdl) + + def test_avgdl_from_dictionary(self): + model = BM25Stub(dictionary=self.dictionary) + actual_avgdl = model.avgdl + self.assertAlmostEqual(self.expected_avgdl, actual_avgdl) + + +class OkapiBM25ModelTest(unittest.TestCase): + def setUp(self): + self.documents = [['cat', 'dog', 'mouse'], ['cat', 'lion'], ['cat', 'lion']] + self.dictionary = Dictionary(self.documents) + self.k1, self.b, self.epsilon = 1.5, 0.75, 0.25 + + def get_idf(word): + frequency = sum(map(lambda document: word in document, self.documents)) + return math.log((len(self.documents) - frequency + 0.5) / (frequency + 0.5)) + + dog_idf = get_idf('dog') + cat_idf = get_idf('cat') + mouse_idf = get_idf('mouse') + lion_idf = get_idf('lion') + + average_idf = (dog_idf + cat_idf + mouse_idf + lion_idf) / len(self.dictionary) + eps = self.epsilon * average_idf + + self.expected_dog_idf = dog_idf if dog_idf > 0 else eps + self.expected_cat_idf = cat_idf if cat_idf > 0 else eps + self.expected_mouse_idf = mouse_idf if mouse_idf > 0 else eps + self.expected_lion_idf = lion_idf if lion_idf > 0 else eps + + def test_idfs_from_corpus(self): + corpus = list(map(self.dictionary.doc2bow, self.documents)) + model = OkapiBM25Model(corpus=corpus, k1=self.k1, b=self.b, epsilon=self.epsilon) + + actual_dog_idf = model.idfs[self.dictionary.token2id['dog']] + actual_cat_idf = model.idfs[self.dictionary.token2id['cat']] + actual_mouse_idf = model.idfs[self.dictionary.token2id['mouse']] + actual_lion_idf = model.idfs[self.dictionary.token2id['lion']] + + self.assertAlmostEqual(self.expected_dog_idf, actual_dog_idf) + self.assertAlmostEqual(self.expected_cat_idf, actual_cat_idf) + self.assertAlmostEqual(self.expected_mouse_idf, actual_mouse_idf) + self.assertAlmostEqual(self.expected_lion_idf, actual_lion_idf) + + def test_idfs_from_dictionary(self): + model = OkapiBM25Model(dictionary=self.dictionary, k1=self.k1, b=self.b, epsilon=self.epsilon) + + actual_dog_idf = model.idfs[self.dictionary.token2id['dog']] + actual_cat_idf = model.idfs[self.dictionary.token2id['cat']] + actual_mouse_idf = model.idfs[self.dictionary.token2id['mouse']] + actual_lion_idf = model.idfs[self.dictionary.token2id['lion']] + + self.assertAlmostEqual(self.expected_dog_idf, actual_dog_idf) + self.assertAlmostEqual(self.expected_cat_idf, actual_cat_idf) + self.assertAlmostEqual(self.expected_mouse_idf, actual_mouse_idf) + self.assertAlmostEqual(self.expected_lion_idf, actual_lion_idf) + + def test_score(self): + model = OkapiBM25Model(dictionary=self.dictionary, k1=self.k1, b=self.b, epsilon=self.epsilon) + + first_document = self.documents[0] + first_bow = self.dictionary.doc2bow(first_document) + weights = defaultdict(lambda: 0.0) + weights.update(model[first_bow]) + + actual_dog_weight = weights[self.dictionary.token2id['dog']] + actual_cat_weight = weights[self.dictionary.token2id['cat']] + actual_mouse_weight = weights[self.dictionary.token2id['mouse']] + actual_lion_weight = weights[self.dictionary.token2id['lion']] + + def get_expected_weight(word): + idf = model.idfs[self.dictionary.token2id[word]] + numerator = self.k1 + 1 + denominator = 1 + self.k1 * (1 - self.b + self.b * len(first_document) / model.avgdl) + return idf * numerator / denominator + + expected_dog_weight = get_expected_weight('dog') if 'dog' in first_document else 0.0 + expected_cat_weight = get_expected_weight('cat') if 'cat' in first_document else 0.0 + expected_mouse_weight = get_expected_weight('mouse') if 'mouse' in first_document else 0.0 + expected_lion_weight = get_expected_weight('lion') if 'lion' in first_document else 0.0 + + self.assertAlmostEqual(expected_dog_weight, actual_dog_weight) + self.assertAlmostEqual(expected_cat_weight, actual_cat_weight) + self.assertAlmostEqual(expected_mouse_weight, actual_mouse_weight) + self.assertAlmostEqual(expected_lion_weight, actual_lion_weight) + + +class LuceneBM25ModelTest(unittest.TestCase): + def setUp(self): + self.documents = [['cat', 'dog', 'mouse'], ['cat', 'lion'], ['cat', 'lion']] + self.dictionary = Dictionary(self.documents) + self.k1, self.b = 1.5, 0.75 + + def get_idf(word): + frequency = sum(map(lambda document: word in document, self.documents)) + return math.log(1.0 + (len(self.documents) - frequency + 0.5) / (frequency + 0.5)) + + self.expected_dog_idf = get_idf('dog') + self.expected_cat_idf = get_idf('cat') + self.expected_mouse_idf = get_idf('mouse') + self.expected_lion_idf = get_idf('lion') + + def test_idfs_from_corpus(self): + corpus = list(map(self.dictionary.doc2bow, self.documents)) + model = LuceneBM25Model(corpus=corpus, k1=self.k1, b=self.b) + + actual_dog_idf = model.idfs[self.dictionary.token2id['dog']] + actual_cat_idf = model.idfs[self.dictionary.token2id['cat']] + actual_mouse_idf = model.idfs[self.dictionary.token2id['mouse']] + actual_lion_idf = model.idfs[self.dictionary.token2id['lion']] + + self.assertAlmostEqual(self.expected_dog_idf, actual_dog_idf) + self.assertAlmostEqual(self.expected_cat_idf, actual_cat_idf) + self.assertAlmostEqual(self.expected_mouse_idf, actual_mouse_idf) + self.assertAlmostEqual(self.expected_lion_idf, actual_lion_idf) + + def test_idfs_from_dictionary(self): + model = LuceneBM25Model(dictionary=self.dictionary, k1=self.k1, b=self.b) + + actual_dog_idf = model.idfs[self.dictionary.token2id['dog']] + actual_cat_idf = model.idfs[self.dictionary.token2id['cat']] + actual_mouse_idf = model.idfs[self.dictionary.token2id['mouse']] + actual_lion_idf = model.idfs[self.dictionary.token2id['lion']] + + self.assertAlmostEqual(self.expected_dog_idf, actual_dog_idf) + self.assertAlmostEqual(self.expected_cat_idf, actual_cat_idf) + self.assertAlmostEqual(self.expected_mouse_idf, actual_mouse_idf) + self.assertAlmostEqual(self.expected_lion_idf, actual_lion_idf) + + def test_score(self): + model = LuceneBM25Model(dictionary=self.dictionary, k1=self.k1, b=self.b) + + first_document = self.documents[0] + first_bow = self.dictionary.doc2bow(first_document) + weights = defaultdict(lambda: 0.0) + weights.update(model[first_bow]) + + actual_dog_weight = weights[self.dictionary.token2id['dog']] + actual_cat_weight = weights[self.dictionary.token2id['cat']] + actual_mouse_weight = weights[self.dictionary.token2id['mouse']] + actual_lion_weight = weights[self.dictionary.token2id['lion']] + + def get_expected_weight(word): + idf = model.idfs[self.dictionary.token2id[word]] + denominator = 1 + self.k1 * (1 - self.b + self.b * len(first_document) / model.avgdl) + return idf / denominator + + expected_dog_weight = get_expected_weight('dog') if 'dog' in first_document else 0.0 + expected_cat_weight = get_expected_weight('cat') if 'cat' in first_document else 0.0 + expected_mouse_weight = get_expected_weight('mouse') if 'mouse' in first_document else 0.0 + expected_lion_weight = get_expected_weight('lion') if 'lion' in first_document else 0.0 + + self.assertAlmostEqual(expected_dog_weight, actual_dog_weight) + self.assertAlmostEqual(expected_cat_weight, actual_cat_weight) + self.assertAlmostEqual(expected_mouse_weight, actual_mouse_weight) + self.assertAlmostEqual(expected_lion_weight, actual_lion_weight) + + +class AtireBM25ModelTest(unittest.TestCase): + def setUp(self): + self.documents = [['cat', 'dog', 'mouse'], ['cat', 'lion'], ['cat', 'lion']] + self.dictionary = Dictionary(self.documents) + self.k1, self.b, self.epsilon = 1.5, 0.75, 0.25 + + def get_idf(word): + frequency = sum(map(lambda document: word in document, self.documents)) + return math.log(len(self.documents) / frequency) + + self.expected_dog_idf = get_idf('dog') + self.expected_cat_idf = get_idf('cat') + self.expected_mouse_idf = get_idf('mouse') + self.expected_lion_idf = get_idf('lion') + + def test_idfs_from_corpus(self): + corpus = list(map(self.dictionary.doc2bow, self.documents)) + model = AtireBM25Model(corpus=corpus, k1=self.k1, b=self.b) + + actual_dog_idf = model.idfs[self.dictionary.token2id['dog']] + actual_cat_idf = model.idfs[self.dictionary.token2id['cat']] + actual_mouse_idf = model.idfs[self.dictionary.token2id['mouse']] + actual_lion_idf = model.idfs[self.dictionary.token2id['lion']] + + self.assertAlmostEqual(self.expected_dog_idf, actual_dog_idf) + self.assertAlmostEqual(self.expected_cat_idf, actual_cat_idf) + self.assertAlmostEqual(self.expected_mouse_idf, actual_mouse_idf) + self.assertAlmostEqual(self.expected_lion_idf, actual_lion_idf) + + def test_idfs_from_dictionary(self): + model = AtireBM25Model(dictionary=self.dictionary, k1=self.k1, b=self.b) + + actual_dog_idf = model.idfs[self.dictionary.token2id['dog']] + actual_cat_idf = model.idfs[self.dictionary.token2id['cat']] + actual_mouse_idf = model.idfs[self.dictionary.token2id['mouse']] + actual_lion_idf = model.idfs[self.dictionary.token2id['lion']] + + self.assertAlmostEqual(self.expected_dog_idf, actual_dog_idf) + self.assertAlmostEqual(self.expected_cat_idf, actual_cat_idf) + self.assertAlmostEqual(self.expected_mouse_idf, actual_mouse_idf) + self.assertAlmostEqual(self.expected_lion_idf, actual_lion_idf) + + def test_score(self): + model = AtireBM25Model(dictionary=self.dictionary, k1=self.k1, b=self.b) + + first_document = self.documents[0] + first_bow = self.dictionary.doc2bow(first_document) + weights = defaultdict(lambda: 0.0) + weights.update(model[first_bow]) + + actual_dog_weight = weights[self.dictionary.token2id['dog']] + actual_cat_weight = weights[self.dictionary.token2id['cat']] + actual_mouse_weight = weights[self.dictionary.token2id['mouse']] + actual_lion_weight = weights[self.dictionary.token2id['lion']] + + def get_expected_weight(word): + idf = model.idfs[self.dictionary.token2id[word]] + numerator = self.k1 + 1 + denominator = 1 + self.k1 * (1 - self.b + self.b * len(first_document) / model.avgdl) + return idf * numerator / denominator + + expected_dog_weight = get_expected_weight('dog') if 'dog' in first_document else 0.0 + expected_cat_weight = get_expected_weight('cat') if 'cat' in first_document else 0.0 + expected_mouse_weight = get_expected_weight('mouse') if 'mouse' in first_document else 0.0 + expected_lion_weight = get_expected_weight('lion') if 'lion' in first_document else 0.0 + + self.assertAlmostEqual(expected_dog_weight, actual_dog_weight) + self.assertAlmostEqual(expected_cat_weight, actual_cat_weight) + self.assertAlmostEqual(expected_mouse_weight, actual_mouse_weight) + self.assertAlmostEqual(expected_lion_weight, actual_lion_weight) From a435f24fe25e17f473e71af3468660512c2606cb Mon Sep 17 00:00:00 2001 From: Tobi Date: Sun, 2 Oct 2022 17:05:30 +0200 Subject: [PATCH 15/34] Giving missing credit to Alex in docs (#3393) --- gensim/models/ensemblelda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gensim/models/ensemblelda.py b/gensim/models/ensemblelda.py index 39d7e06620..16b5c1c1bc 100644 --- a/gensim/models/ensemblelda.py +++ b/gensim/models/ensemblelda.py @@ -91,7 +91,7 @@ Citation -------- BRIGL, Tobias, 2019, Extracting Reliable Topics using Ensemble Latent Dirichlet Allocation [Bachelor Thesis]. -Technische Hochschule Ingolstadt. Munich: Data Reply GmbH. Available from: +Technische Hochschule Ingolstadt. Munich: Data Reply GmbH. Supervised by Alex Loosley. Available from: https://www.sezanzeb.de/machine_learning/ensemble_LDA/ """ From fdf40eb309f5b188b8ef173cff80c7727bfa36ff Mon Sep 17 00:00:00 2001 From: TLouf <31036680+TLouf@users.noreply.github.com> Date: Thu, 3 Nov 2022 14:44:56 +0100 Subject: [PATCH 16/34] PERF: pyemd to POT for EMD computation in `wmdistance` (#3327) * PERF: switch from pyemd to POT for EMD computation * Adapt citations * Adapt dependency * Adapt tests * Update cache for gallery Co-authored-by: TLouf --- .travis.yml | 2 +- docs/notebooks/WMD_tutorial.ipynb | 5 +- docs/notebooks/soft_cosine_tutorial.ipynb | 46 +- docs/src/auto_examples/core/index.rst | 98 ++++ docs/src/auto_examples/howtos/index.rst | 97 ++++ docs/src/auto_examples/index.rst | 24 +- docs/src/auto_examples/other/index.rst | 49 ++ .../images/sphx_glr_run_fasttext_001.png | Bin 8103 -> 19508 bytes .../tutorials/images/sphx_glr_run_wmd_001.png | Bin 31931 -> 31931 bytes .../thumb/sphx_glr_run_fasttext_thumb.png | Bin 12051 -> 13093 bytes .../images/thumb/sphx_glr_run_wmd_thumb.png | Bin 17172 -> 17211 bytes docs/src/auto_examples/tutorials/index.rst | 169 +++++++ .../tutorials/run_fasttext.ipynb | 24 +- .../auto_examples/tutorials/run_fasttext.py | 2 +- .../tutorials/run_fasttext.py.md5 | 2 +- .../auto_examples/tutorials/run_fasttext.rst | 421 +++++++++++------- .../tutorials/run_fasttext_codeobj.pickle | Bin 0 -> 9038 bytes .../src/auto_examples/tutorials/run_wmd.ipynb | 10 +- docs/src/auto_examples/tutorials/run_wmd.py | 9 +- .../auto_examples/tutorials/run_wmd.py.md5 | 2 +- docs/src/auto_examples/tutorials/run_wmd.rst | 132 +++--- .../tutorials/run_wmd_codeobj.pickle | Bin 0 -> 2602 bytes .../tutorials/sg_execution_times.rst | 36 +- docs/src/gallery/tutorials/run_fasttext.py | 2 +- docs/src/gallery/tutorials/run_wmd.py | 9 +- gensim/models/keyedvectors.py | 19 +- gensim/similarities/docsim.py | 6 +- gensim/test/test_fasttext.py | 8 +- gensim/test/test_similarities.py | 48 +- gensim/test/test_word2vec.py | 12 +- requirements_docs.txt | 2 +- setup.py | 2 +- 32 files changed, 866 insertions(+), 370 deletions(-) create mode 100644 docs/src/auto_examples/core/index.rst create mode 100644 docs/src/auto_examples/howtos/index.rst create mode 100644 docs/src/auto_examples/other/index.rst create mode 100644 docs/src/auto_examples/tutorials/index.rst create mode 100644 docs/src/auto_examples/tutorials/run_fasttext_codeobj.pickle create mode 100644 docs/src/auto_examples/tutorials/run_wmd_codeobj.pickle diff --git a/.travis.yml b/.travis.yml index b0b952766d..d27d176f58 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ env: # them here for now. They'll get picked up by the multibuild stuff # running in multibuild/common_utils.sh. # - - TEST_DEPENDS="pytest mock cython nmslib pyemd testfixtures python-levenshtein==0.12.0 visdom==0.1.8.9 scikit-learn" + - TEST_DEPENDS="pytest mock cython nmslib POT testfixtures python-levenshtein==0.12.0 visdom==0.1.8.9 scikit-learn" matrix: # diff --git a/docs/notebooks/WMD_tutorial.ipynb b/docs/notebooks/WMD_tutorial.ipynb index ff1f608dc5..9b051104d5 100644 --- a/docs/notebooks/WMD_tutorial.ipynb +++ b/docs/notebooks/WMD_tutorial.ipynb @@ -30,7 +30,7 @@ "\n", "## Running this notebook\n", "\n", - "You can download this [iPython Notebook](http://ipython.org/notebook.html), and run it on your own computer, provided you have installed Gensim, PyEMD, NLTK, and downloaded the necessary data.\n", + "You can download this [iPython Notebook](http://ipython.org/notebook.html), and run it on your own computer, provided you have installed Gensim, POT, NLTK, and downloaded the necessary data.\n", "\n", "The notebook was run on an Ubuntu machine with an Intel core i7-4770 CPU 3.40GHz (8 cores) and 32 GB memory. Running the entire notebook on this machine takes about 3 minutes.\n", "\n", @@ -524,8 +524,7 @@ "source": [ "## References\n", "\n", - "1. Ofir Pele and Michael Werman, *A linear time histogram metric for improved SIFT matching*, 2008.\n", - "* Ofir Pele and Michael Werman, *Fast and robust earth mover's distances*, 2009.\n", + "1. * Rémi Flamary et al. *POT: Python Optimal Transport*, 2021.\n", "* Matt Kusner et al. *From Embeddings To Document Distances*, 2015.\n", "* Thomas Mikolov et al. *Efficient Estimation of Word Representations in Vector Space*, 2013." ] diff --git a/docs/notebooks/soft_cosine_tutorial.ipynb b/docs/notebooks/soft_cosine_tutorial.ipynb index 4c7fceb1df..a8d1c41555 100644 --- a/docs/notebooks/soft_cosine_tutorial.ipynb +++ b/docs/notebooks/soft_cosine_tutorial.ipynb @@ -30,7 +30,7 @@ ">\n", "\n", "## Running this notebook\n", - "You can download this [Jupyter notebook](http://jupyter.org/), and run it on your own computer, provided you have installed the `gensim`, `jupyter`, `sklearn`, `pyemd`, and `wmd` Python packages.\n", + "You can download this [Jupyter notebook](http://jupyter.org/), and run it on your own computer, provided you have installed the `gensim`, `jupyter`, `sklearn`, `POT`, and `wmd` Python packages.\n", "\n", "The notebook was run on an Ubuntu machine with an Intel core i7-6700HQ CPU 3.10GHz (4 cores) and 16 GB memory. Assuming all resources required by the notebook have already been downloaded, running the entire notebook on this machine takes about 30 minutes." ] @@ -357,7 +357,7 @@ "metadata": {}, "outputs": [], "source": [ - "!pip install pyemd" + "!pip install POT" ] }, { @@ -404,7 +404,7 @@ " return similarities\n", "\n", "def wmd_gensim(query, documents):\n", - " # Compute Word Mover's Distance as implemented in PyEMD by William Mayner\n", + " # Compute Word Mover's Distance as implemented in POT\n", " # between the query and the documents.\n", " index = WmdSimilarity(documents, w2v_model)\n", " similarities = index[query]\n", @@ -532,26 +532,26 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Dataset | Strategy | MAP score | Elapsed time (sec)\n", - ":---|:---|:---|---:\n", - "2016-test|softcossim|78.52 ±11.18|6.00 ±0.79\n", - "2016-test|**Winner (UH-PRHLT-primary)**|76.70 ±0.00|\n", - "2016-test|cossim|76.45 ±10.40|0.64 ±0.08\n", - "2016-test|wmd-gensim|76.23 ±11.42|5.37 ±0.64\n", - "2016-test|**Baseline 1 (IR)**|74.75 ±0.00|\n", - "2016-test|wmd-relax|71.05 ±11.06|1.11 ±0.09\n", - "2016-test|**Baseline 2 (random)**|46.98 ±0.00|\n", - "\n", - "\n", - "Dataset | Strategy | MAP score | Elapsed time (sec)\n", - ":---|:---|:---|---:\n", - "2017-test|**Winner (SimBow-primary)**|47.22 ±0.00|\n", - "2017-test|softcossim|45.88 ±16.22|7.08 ±1.49\n", - "2017-test|cossim|44.38 ±14.71|0.74 ±0.10\n", - "2017-test|wmd-gensim|44.06 ±15.92|6.20 ±0.87\n", - "2017-test|wmd-relax|43.52 ±16.30|1.30 ±0.18\n", - "2017-test|**Baseline 1 (IR)**|41.85 ±0.00|\n", - "2017-test|**Baseline 2 (random)**|29.81 ±0.00|" + "Dataset | Strategy | MAP score | Elapsed time (sec)\n", + ":---|:---|:---|---:\n", + "2016-test|softcossim|78.52 ±11.18|6.00 ±0.79\n", + "2016-test|**Winner (UH-PRHLT-primary)**|76.70 ±0.00|\n", + "2016-test|cossim|76.45 ±10.40|0.64 ±0.08\n", + "2016-test|wmd-gensim|76.23 ±11.42|5.37 ±0.64\n", + "2016-test|**Baseline 1 (IR)**|74.75 ±0.00|\n", + "2016-test|wmd-relax|71.05 ±11.06|1.11 ±0.09\n", + "2016-test|**Baseline 2 (random)**|46.98 ±0.00|\n", + "\n", + "\n", + "Dataset | Strategy | MAP score | Elapsed time (sec)\n", + ":---|:---|:---|---:\n", + "2017-test|**Winner (SimBow-primary)**|47.22 ±0.00|\n", + "2017-test|softcossim|45.88 ±16.22|7.08 ±1.49\n", + "2017-test|cossim|44.38 ±14.71|0.74 ±0.10\n", + "2017-test|wmd-gensim|44.06 ±15.92|6.20 ±0.87\n", + "2017-test|wmd-relax|43.52 ±16.30|1.30 ±0.18\n", + "2017-test|**Baseline 1 (IR)**|41.85 ±0.00|\n", + "2017-test|**Baseline 2 (random)**|29.81 ±0.00|" ] }, { diff --git a/docs/src/auto_examples/core/index.rst b/docs/src/auto_examples/core/index.rst new file mode 100644 index 0000000000..03716729a9 --- /dev/null +++ b/docs/src/auto_examples/core/index.rst @@ -0,0 +1,98 @@ + + +.. _sphx_glr_auto_examples_core: + +Core Tutorials: New Users Start Here! +------------------------------------- + +If you're new to gensim, we recommend going through all core tutorials in order. +Understanding this functionality is vital for using gensim effectively. + + + +.. raw:: html + +
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/core/images/thumb/sphx_glr_run_core_concepts_thumb.png + :alt: Core Concepts + + :ref:`sphx_glr_auto_examples_core_run_core_concepts.py` + +.. raw:: html + +
Core Concepts
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/core/images/thumb/sphx_glr_run_corpora_and_vector_spaces_thumb.png + :alt: Corpora and Vector Spaces + + :ref:`sphx_glr_auto_examples_core_run_corpora_and_vector_spaces.py` + +.. raw:: html + +
Corpora and Vector Spaces
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/core/images/thumb/sphx_glr_run_topics_and_transformations_thumb.png + :alt: Topics and Transformations + + :ref:`sphx_glr_auto_examples_core_run_topics_and_transformations.py` + +.. raw:: html + +
Topics and Transformations
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/core/images/thumb/sphx_glr_run_similarity_queries_thumb.png + :alt: Similarity Queries + + :ref:`sphx_glr_auto_examples_core_run_similarity_queries.py` + +.. raw:: html + +
Similarity Queries
+
+ + +.. raw:: html + +
+ + +.. toctree:: + :hidden: + + /auto_examples/core/run_core_concepts + /auto_examples/core/run_corpora_and_vector_spaces + /auto_examples/core/run_topics_and_transformations + /auto_examples/core/run_similarity_queries + diff --git a/docs/src/auto_examples/howtos/index.rst b/docs/src/auto_examples/howtos/index.rst new file mode 100644 index 0000000000..a43341d306 --- /dev/null +++ b/docs/src/auto_examples/howtos/index.rst @@ -0,0 +1,97 @@ + + +.. _sphx_glr_auto_examples_howtos: + +How-to Guides: Solve a Problem +------------------------------ + +These **goal-oriented guides** demonstrate how to **solve a specific problem** using gensim. + + + +.. raw:: html + +
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/howtos/images/thumb/sphx_glr_run_downloader_api_thumb.png + :alt: How to download pre-trained models and corpora + + :ref:`sphx_glr_auto_examples_howtos_run_downloader_api.py` + +.. raw:: html + +
How to download pre-trained models and corpora
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/howtos/images/thumb/sphx_glr_run_doc_thumb.png + :alt: How to Author Gensim Documentation + + :ref:`sphx_glr_auto_examples_howtos_run_doc.py` + +.. raw:: html + +
How to Author Gensim Documentation
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/howtos/images/thumb/sphx_glr_run_doc2vec_imdb_thumb.png + :alt: How to reproduce the doc2vec 'Paragraph Vector' paper + + :ref:`sphx_glr_auto_examples_howtos_run_doc2vec_imdb.py` + +.. raw:: html + +
How to reproduce the doc2vec 'Paragraph Vector' paper
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/howtos/images/thumb/sphx_glr_run_compare_lda_thumb.png + :alt: How to Compare LDA Models + + :ref:`sphx_glr_auto_examples_howtos_run_compare_lda.py` + +.. raw:: html + +
How to Compare LDA Models
+
+ + +.. raw:: html + +
+ + +.. toctree:: + :hidden: + + /auto_examples/howtos/run_downloader_api + /auto_examples/howtos/run_doc + /auto_examples/howtos/run_doc2vec_imdb + /auto_examples/howtos/run_compare_lda + diff --git a/docs/src/auto_examples/index.rst b/docs/src/auto_examples/index.rst index a1f42f795e..2d2f133d0c 100644 --- a/docs/src/auto_examples/index.rst +++ b/docs/src/auto_examples/index.rst @@ -152,35 +152,35 @@ Learning-oriented lessons that introduce a particular gensim feature, e.g. a mod .. raw:: html -
+
.. only:: html - .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_ensemblelda_thumb.png - :alt: Ensemble LDA + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_fasttext_thumb.png + :alt: FastText Model - :ref:`sphx_glr_auto_examples_tutorials_run_ensemblelda.py` + :ref:`sphx_glr_auto_examples_tutorials_run_fasttext.py` .. raw:: html -
Ensemble LDA
+
FastText Model
.. raw:: html -
+
.. only:: html - .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_fasttext_thumb.png - :alt: FastText Model + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_ensemblelda_thumb.png + :alt: Ensemble LDA - :ref:`sphx_glr_auto_examples_tutorials_run_fasttext.py` + :ref:`sphx_glr_auto_examples_tutorials_run_ensemblelda.py` .. raw:: html -
FastText Model
+
Ensemble LDA
@@ -220,7 +220,7 @@ Learning-oriented lessons that introduce a particular gensim feature, e.g. a mod .. raw:: html -
+
.. only:: html @@ -237,7 +237,7 @@ Learning-oriented lessons that introduce a particular gensim feature, e.g. a mod .. raw:: html -
+
.. only:: html diff --git a/docs/src/auto_examples/other/index.rst b/docs/src/auto_examples/other/index.rst new file mode 100644 index 0000000000..c573b3902a --- /dev/null +++ b/docs/src/auto_examples/other/index.rst @@ -0,0 +1,49 @@ + + +.. _sphx_glr_auto_examples_other: + +Other Resources +--------------- + +Blog posts, tutorial videos, hackathons and other useful Gensim resources, from around the internet. + +- *Use FastText or Word2Vec?* Comparison of embedding quality and performance. `Jupyter Notebook `__ +- Multiword phrases extracted from *How I Met Your Mother*. `Blog post by Mark Needham `__ +- *Using Gensim LDA for hierarchical document clustering*. `Jupyter notebook by Brandon Rose `__ +- *Evolution of Voldemort topic through the 7 Harry Potter books*. `Blog post `__ +- *Movie plots by genre*: Document classification using various techniques: TF-IDF, word2vec averaging, Deep IR, Word Movers Distance and doc2vec. `Github repo `__ +- *Word2vec: Faster than Google? Optimization lessons in Python*, talk by Radim Řehůřek at PyData Berlin 2014. `Youtube video `__ +- *Word2vec & friends*, talk by Radim Řehůřek at MLMU.cz 7.1.2015. `Youtube video `__ + +.. + - ? `Making an Impact with NLP `__ -- Pycon 2016 Tutorial by Hobsons Lane + - ? `NLP with NLTK and Gensim `__ -- Pycon 2016 Tutorial by Tony Ojeda, Benjamin Bengfort, Laura Lorenz from District Data Labs + - ? `Word Embeddings for Fun and Profit `__ -- Talk at PyData London 2016 talk by Lev Konstantinovskiy. See accompanying `repo `__ + - ? English Wikipedia; TODO: convert to proper .py format + - ? `Colouring words by topic in a document, print words in a + topics `__ + - ? `Topic Coherence, a metric that correlates that human judgement on topic quality. `__ + - ? `America's Next Topic Model slides `__ + - How to choose your next topic model, presented at Pydata Berlin 10 August 2016 by Lev Konstantinovsky + - ? `Dynamic Topic Modeling and Dynamic Influence Model Tutorial `__ + - ? `Python Dynamic Topic Modelling Theory and Tutorial `__ + - ? `Word Movers Distance for Yelp Reviews tutorial `__ + - FIXME WMD superceded by soft cosine similarity = faster and better? any numbers / tutorials for that? + - ? `Great illustration of corpus preparation `__, `Code `__ + - ? `Alternative `__, + - ? `Alternative 2 `__ + - ? `Doc2Vec on customer reviews `__ + - ? `Doc2Vec on Airline Tweets Sentiment Analysis `__ + - ? `Deep Inverse Regression with Yelp Reviews `__ (Document Classification using Bayesian Inversion and several word2vec models, one for each class) + + + +.. raw:: html + +
+ + +.. raw:: html + +
+ diff --git a/docs/src/auto_examples/tutorials/images/sphx_glr_run_fasttext_001.png b/docs/src/auto_examples/tutorials/images/sphx_glr_run_fasttext_001.png index 9d95c4d8f04a2059385c9a1e342354fee11f61a4..9994f9312b39f15e4bad50a319bf0c26f960efe5 100644 GIT binary patch literal 19508 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 diff --git a/docs/src/auto_examples/tutorials/images/sphx_glr_run_wmd_001.png b/docs/src/auto_examples/tutorials/images/sphx_glr_run_wmd_001.png index 7d7ea7db56c5c8dc4d14f7739370a485d74c9814..d321d6e8ec87ed529003479d8698d80acb0ace58 100644 GIT binary patch delta 45 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 z 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 diff --git a/docs/src/auto_examples/tutorials/images/thumb/sphx_glr_run_wmd_thumb.png b/docs/src/auto_examples/tutorials/images/thumb/sphx_glr_run_wmd_thumb.png index 381d4a9d592148dda9d9dd0066687d998ad6059c..9732c8e0961aa871346cac0cbad00c4c9d05dad8 100644 GIT binary patch literal 17211 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 i|H1!Nfs8@Wy}ziMUBavUfPU=)Mp|4!tVYBr_ + + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_word2vec_thumb.png + :alt: Word2Vec Model + + :ref:`sphx_glr_auto_examples_tutorials_run_word2vec.py` + +.. raw:: html + +
Word2Vec Model
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_doc2vec_lee_thumb.png + :alt: Doc2Vec Model + + :ref:`sphx_glr_auto_examples_tutorials_run_doc2vec_lee.py` + +.. raw:: html + +
Doc2Vec Model
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_fasttext_thumb.png + :alt: FastText Model + + :ref:`sphx_glr_auto_examples_tutorials_run_fasttext.py` + +.. raw:: html + +
FastText Model
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_ensemblelda_thumb.png + :alt: Ensemble LDA + + :ref:`sphx_glr_auto_examples_tutorials_run_ensemblelda.py` + +.. raw:: html + +
Ensemble LDA
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_annoy_thumb.png + :alt: Fast Similarity Queries with Annoy and Word2Vec + + :ref:`sphx_glr_auto_examples_tutorials_run_annoy.py` + +.. raw:: html + +
Fast Similarity Queries with Annoy and Word2Vec
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_lda_thumb.png + :alt: LDA Model + + :ref:`sphx_glr_auto_examples_tutorials_run_lda.py` + +.. raw:: html + +
LDA Model
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_wmd_thumb.png + :alt: Word Mover's Distance + + :ref:`sphx_glr_auto_examples_tutorials_run_wmd.py` + +.. raw:: html + +
Word Mover's Distance
+
+ + +.. raw:: html + +
+ +.. only:: html + + .. image:: /auto_examples/tutorials/images/thumb/sphx_glr_run_scm_thumb.png + :alt: Soft Cosine Measure + + :ref:`sphx_glr_auto_examples_tutorials_run_scm.py` + +.. raw:: html + +
Soft Cosine Measure
+
+ + +.. raw:: html + +
+ + +.. toctree:: + :hidden: + + /auto_examples/tutorials/run_word2vec + /auto_examples/tutorials/run_doc2vec_lee + /auto_examples/tutorials/run_fasttext + /auto_examples/tutorials/run_ensemblelda + /auto_examples/tutorials/run_annoy + /auto_examples/tutorials/run_lda + /auto_examples/tutorials/run_wmd + /auto_examples/tutorials/run_scm + diff --git a/docs/src/auto_examples/tutorials/run_fasttext.ipynb b/docs/src/auto_examples/tutorials/run_fasttext.ipynb index 6506e1465b..0170d7fc78 100644 --- a/docs/src/auto_examples/tutorials/run_fasttext.ipynb +++ b/docs/src/auto_examples/tutorials/run_fasttext.ipynb @@ -15,7 +15,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "\nFastText Model\n==============\n\nIntroduces Gensim's fastText model and demonstrates its use on the Lee Corpus.\n\n" + "\n# FastText Model\n\nIntroduces Gensim's fastText model and demonstrates its use on the Lee Corpus.\n" ] }, { @@ -40,14 +40,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "When to use fastText?\n---------------------\n\nThe main principle behind `fastText `_ is that the\nmorphological structure of a word carries important information about the meaning of the word.\nSuch structure is not taken into account by traditional word embeddings like Word2Vec, which\ntrain a unique word embedding for every individual word.\nThis is especially significant for morphologically rich languages (German, Turkish) in which a\nsingle word can have a large number of morphological forms, each of which might occur rarely,\nthus making it hard to train good word embeddings.\n\n\nfastText attempts to solve this by treating each word as the aggregation of its subwords.\nFor the sake of simplicity and language-independence, subwords are taken to be the character ngrams\nof the word. The vector for a word is simply taken to be the sum of all vectors of its component char-ngrams.\n\n\nAccording to a detailed comparison of Word2Vec and fastText in\n`this notebook `__,\nfastText does significantly better on syntactic tasks as compared to the original Word2Vec,\nespecially when the size of the training corpus is small. Word2Vec slightly outperforms fastText\non semantic tasks though. The differences grow smaller as the size of the training corpus increases.\n\n\nfastText can obtain vectors even for out-of-vocabulary (OOV) words, by summing up vectors for its\ncomponent char-ngrams, provided at least one of the char-ngrams was present in the training data.\n\n\n" + "## When to use fastText?\n\nThe main principle behind [fastText](https://github.com/facebookresearch/fastText) is that the\nmorphological structure of a word carries important information about the meaning of the word.\nSuch structure is not taken into account by traditional word embeddings like Word2Vec, which\ntrain a unique word embedding for every individual word.\nThis is especially significant for morphologically rich languages (German, Turkish) in which a\nsingle word can have a large number of morphological forms, each of which might occur rarely,\nthus making it hard to train good word embeddings.\n\n\nfastText attempts to solve this by treating each word as the aggregation of its subwords.\nFor the sake of simplicity and language-independence, subwords are taken to be the character ngrams\nof the word. The vector for a word is simply taken to be the sum of all vectors of its component char-ngrams.\n\n\nAccording to a detailed comparison of Word2Vec and fastText in\n[this notebook](https://github.com/RaRe-Technologies/gensim/blob/develop/docs/notebooks/Word2Vec_FastText_Comparison.ipynb)_,\nfastText does significantly better on syntactic tasks as compared to the original Word2Vec,\nespecially when the size of the training corpus is small. Word2Vec slightly outperforms fastText\non semantic tasks though. The differences grow smaller as the size of the training corpus increases.\n\n\nfastText can obtain vectors even for out-of-vocabulary (OOV) words, by summing up vectors for its\ncomponent char-ngrams, provided at least one of the char-ngrams was present in the training data.\n\n\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Training models\n---------------\n\n\n" + "## Training models\n\n\n" ] }, { @@ -72,14 +72,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Training hyperparameters\n^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n" + "### Training hyperparameters\n\n\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Hyperparameters for training the model follow the same pattern as Word2Vec. FastText supports the following parameters from the original word2vec:\n\n- model: Training architecture. Allowed values: `cbow`, `skipgram` (Default `cbow`)\n- vector_size: Dimensionality of vector embeddings to be learnt (Default 100)\n- alpha: Initial learning rate (Default 0.025)\n- window: Context window size (Default 5)\n- min_count: Ignore words with number of occurrences below this (Default 5)\n- loss: Training objective. Allowed values: `ns`, `hs`, `softmax` (Default `ns`)\n- sample: Threshold for downsampling higher-frequency words (Default 0.001)\n- negative: Number of negative words to sample, for `ns` (Default 5)\n- epochs: Number of epochs (Default 5)\n- sorted_vocab: Sort vocab by descending frequency (Default 1)\n- threads: Number of threads to use (Default 12)\n\n\nIn addition, fastText has three additional parameters:\n\n- min_n: min length of char ngrams (Default 3)\n- max_n: max length of char ngrams (Default 6)\n- bucket: number of buckets used for hashing ngrams (Default 2000000)\n\n\nParameters ``min_n`` and ``max_n`` control the lengths of character ngrams that each word is broken down into while training and looking up embeddings. If ``max_n`` is set to 0, or to be lesser than ``min_n``\\ , no character ngrams are used, and the model effectively reduces to Word2Vec.\n\n\n\nTo bound the memory requirements of the model being trained, a hashing function is used that maps ngrams to integers in 1 to K. For hashing these character sequences, the `Fowler-Noll-Vo hashing function `_ (FNV-1a variant) is employed.\n\n\n" + "Hyperparameters for training the model follow the same pattern as Word2Vec. FastText supports the following parameters from the original word2vec:\n\n- model: Training architecture. Allowed values: `cbow`, `skipgram` (Default `cbow`)\n- vector_size: Dimensionality of vector embeddings to be learnt (Default 100)\n- alpha: Initial learning rate (Default 0.025)\n- window: Context window size (Default 5)\n- min_count: Ignore words with number of occurrences below this (Default 5)\n- loss: Training objective. Allowed values: `ns`, `hs`, `softmax` (Default `ns`)\n- sample: Threshold for downsampling higher-frequency words (Default 0.001)\n- negative: Number of negative words to sample, for `ns` (Default 5)\n- epochs: Number of epochs (Default 5)\n- sorted_vocab: Sort vocab by descending frequency (Default 1)\n- threads: Number of threads to use (Default 12)\n\n\nIn addition, fastText has three additional parameters:\n\n- min_n: min length of char ngrams (Default 3)\n- max_n: max length of char ngrams (Default 6)\n- bucket: number of buckets used for hashing ngrams (Default 2000000)\n\n\nParameters ``min_n`` and ``max_n`` control the lengths of character ngrams that each word is broken down into while training and looking up embeddings. If ``max_n`` is set to 0, or to be lesser than ``min_n``\\ , no character ngrams are used, and the model effectively reduces to Word2Vec.\n\n\n\nTo bound the memory requirements of the model being trained, a hashing function is used that maps ngrams to integers in 1 to K. For hashing these character sequences, the [Fowler-Noll-Vo hashing function](http://www.isthe.com/chongo/tech/comp/fnv) (FNV-1a variant) is employed.\n\n\n" ] }, { @@ -93,7 +93,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Saving/loading models\n---------------------\n\n\n" + "## Saving/loading models\n\n\n" ] }, { @@ -125,7 +125,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Word vector lookup\n------------------\n\n\nAll information necessary for looking up fastText words (incl. OOV words) is\ncontained in its ``model.wv`` attribute.\n\nIf you don't need to continue training your model, you can export & save this `.wv`\nattribute and discard `model`, to save space and RAM.\n\n\n" + "## Word vector lookup\n\n\nAll information necessary for looking up fastText words (incl. OOV words) is\ncontained in its ``model.wv`` attribute.\n\nIf you don't need to continue training your model, you can export & save this `.wv`\nattribute and discard `model`, to save space and RAM.\n\n\n" ] }, { @@ -176,7 +176,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Similarity operations\n---------------------\n\n\n" + "## Similarity operations\n\n\n" ] }, { @@ -223,14 +223,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Syntactically similar words generally have high similarity in fastText models, since a large number of the component char-ngrams will be the same. As a result, fastText generally does better at syntactic tasks than Word2Vec. A detailed comparison is provided `here `_.\n\n\n" + "Syntactically similar words generally have high similarity in fastText models, since a large number of the component char-ngrams will be the same. As a result, fastText generally does better at syntactic tasks than Word2Vec. A detailed comparison is provided [here](Word2Vec_FastText_Comparison.ipynb).\n\n\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Other similarity operations\n^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThe example training corpus is a toy corpus, results are not expected to be good, for proof-of-concept only\n\n" + "### Other similarity operations\n\nThe example training corpus is a toy corpus, results are not expected to be good, for proof-of-concept only\n\n" ] }, { @@ -292,7 +292,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Word Movers distance\n^^^^^^^^^^^^^^^^^^^^\n\nYou'll need the optional ``pyemd`` library for this section, ``pip install pyemd``.\n\nLet's start with two sentences:\n\n" + "### Word Movers distance\n\nYou'll need the optional ``POT`` library for this section, ``pip install POT``.\n\nLet's start with two sentences:\n\n" ] }, { @@ -377,7 +377,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.10.6" } }, "nbformat": 4, diff --git a/docs/src/auto_examples/tutorials/run_fasttext.py b/docs/src/auto_examples/tutorials/run_fasttext.py index 5a03b5d35e..ac3a1bc4c3 100644 --- a/docs/src/auto_examples/tutorials/run_fasttext.py +++ b/docs/src/auto_examples/tutorials/run_fasttext.py @@ -248,7 +248,7 @@ # Word Movers distance # ^^^^^^^^^^^^^^^^^^^^ # -# You'll need the optional ``pyemd`` library for this section, ``pip install pyemd``. +# You'll need the optional ``POT`` library for this section, ``pip install POT``. # # Let's start with two sentences: sentence_obama = 'Obama speaks to the media in Illinois'.lower().split() diff --git a/docs/src/auto_examples/tutorials/run_fasttext.py.md5 b/docs/src/auto_examples/tutorials/run_fasttext.py.md5 index 68b4e4ced3..e227559329 100644 --- a/docs/src/auto_examples/tutorials/run_fasttext.py.md5 +++ b/docs/src/auto_examples/tutorials/run_fasttext.py.md5 @@ -1 +1 @@ -afc57676cfaca4793066a78efb2996f7 \ No newline at end of file +5f5ac745a06ff512074def4c0eb15f79 \ No newline at end of file diff --git a/docs/src/auto_examples/tutorials/run_fasttext.rst b/docs/src/auto_examples/tutorials/run_fasttext.rst index 6d163b7910..539cab39e7 100644 --- a/docs/src/auto_examples/tutorials/run_fasttext.rst +++ b/docs/src/auto_examples/tutorials/run_fasttext.rst @@ -1,12 +1,21 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples/tutorials/run_fasttext.py" +.. LINE NUMBERS ARE GIVEN BELOW. + .. only:: html .. note:: :class: sphx-glr-download-link-note - Click :ref:`here ` to download the full example code - .. rst-class:: sphx-glr-example-title + Click :ref:`here ` + to download the full example code - .. _sphx_glr_auto_examples_tutorials_run_fasttext.py: +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_auto_examples_tutorials_run_fasttext.py: FastText Model @@ -14,6 +23,7 @@ FastText Model Introduces Gensim's fastText model and demonstrates its use on the Lee Corpus. +.. GENERATED FROM PYTHON SOURCE LINES 7-11 .. code-block:: default @@ -28,10 +38,14 @@ Introduces Gensim's fastText model and demonstrates its use on the Lee Corpus. +.. GENERATED FROM PYTHON SOURCE LINES 12-15 + Here, we'll learn to work with fastText library for training word-embedding models, saving & loading them and performing similarity operations & vector lookups analogous to Word2Vec. +.. GENERATED FROM PYTHON SOURCE LINES 18-45 + When to use fastText? --------------------- @@ -60,15 +74,20 @@ fastText can obtain vectors even for out-of-vocabulary (OOV) words, by summing u component char-ngrams, provided at least one of the char-ngrams was present in the training data. +.. GENERATED FROM PYTHON SOURCE LINES 49-52 + Training models --------------- +.. GENERATED FROM PYTHON SOURCE LINES 56-60 + For the following examples, we'll use the Lee Corpus (which you already have if you've installed Gensim) for training our model. +.. GENERATED FROM PYTHON SOURCE LINES 61-82 .. code-block:: default @@ -99,19 +118,44 @@ For the following examples, we'll use the Lee Corpus (which you already have if .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - - - - + 2022-10-23 11:05:20,779 : INFO : adding document #0 to Dictionary<0 unique tokens: []> + 2022-10-23 11:05:20,779 : INFO : built Dictionary<12 unique tokens: ['computer', 'human', 'interface', 'response', 'survey']...> from 9 documents (total 29 corpus positions) + 2022-10-23 11:05:20,782 : INFO : Dictionary lifecycle event {'msg': "built Dictionary<12 unique tokens: ['computer', 'human', 'interface', 'response', 'survey']...> from 9 documents (total 29 corpus positions)", 'datetime': '2022-10-23T11:05:20.780094', 'gensim': '4.2.1.dev0', 'python': '3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0]', 'platform': 'Linux-5.19.0-76051900-generic-x86_64-with-glibc2.35', 'event': 'created'} + 2022-10-23 11:05:20,858 : INFO : FastText lifecycle event {'params': 'FastText', 'datetime': '2022-10-23T11:05:20.858457', 'gensim': '4.2.1.dev0', 'python': '3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0]', 'platform': 'Linux-5.19.0-76051900-generic-x86_64-with-glibc2.35', 'event': 'created'} + 2022-10-23 11:05:20,858 : INFO : collecting all words and their counts + 2022-10-23 11:05:20,858 : INFO : PROGRESS: at sentence #0, processed 0 words, keeping 0 word types + 2022-10-23 11:05:20,874 : INFO : collected 10781 word types from a corpus of 59890 raw words and 300 sentences + 2022-10-23 11:05:20,874 : INFO : Creating a fresh vocabulary + 2022-10-23 11:05:20,882 : INFO : FastText lifecycle event {'msg': 'effective_min_count=5 retains 1762 unique words (16.34% of original 10781, drops 9019)', 'datetime': '2022-10-23T11:05:20.882842', 'gensim': '4.2.1.dev0', 'python': '3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0]', 'platform': 'Linux-5.19.0-76051900-generic-x86_64-with-glibc2.35', 'event': 'prepare_vocab'} + 2022-10-23 11:05:20,882 : INFO : FastText lifecycle event {'msg': 'effective_min_count=5 leaves 46084 word corpus (76.95% of original 59890, drops 13806)', 'datetime': '2022-10-23T11:05:20.882944', 'gensim': '4.2.1.dev0', 'python': '3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0]', 'platform': 'Linux-5.19.0-76051900-generic-x86_64-with-glibc2.35', 'event': 'prepare_vocab'} + 2022-10-23 11:05:20,892 : INFO : deleting the raw counts dictionary of 10781 items + 2022-10-23 11:05:20,892 : INFO : sample=0.001 downsamples 45 most-common words + 2022-10-23 11:05:20,893 : INFO : FastText lifecycle event {'msg': 'downsampling leaves estimated 32610.61883565215 word corpus (70.8%% of prior 46084)', 'datetime': '2022-10-23T11:05:20.893011', 'gensim': '4.2.1.dev0', 'python': '3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0]', 'platform': 'Linux-5.19.0-76051900-generic-x86_64-with-glibc2.35', 'event': 'prepare_vocab'} + 2022-10-23 11:05:20,927 : INFO : estimated required memory for 1762 words, 2000000 buckets and 100 dimensions: 802597824 bytes + 2022-10-23 11:05:20,927 : INFO : resetting layer weights + 2022-10-23 11:05:22,169 : INFO : FastText lifecycle event {'update': False, 'trim_rule': 'None', 'datetime': '2022-10-23T11:05:22.169699', 'gensim': '4.2.1.dev0', 'python': '3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0]', 'platform': 'Linux-5.19.0-76051900-generic-x86_64-with-glibc2.35', 'event': 'build_vocab'} + 2022-10-23 11:05:22,169 : INFO : FastText lifecycle event {'msg': 'training model with 3 workers on 1762 vocabulary and 100 features, using sg=0 hs=0 sample=0.001 negative=5 window=5 shrink_windows=True', 'datetime': '2022-10-23T11:05:22.169966', 'gensim': '4.2.1.dev0', 'python': '3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0]', 'platform': 'Linux-5.19.0-76051900-generic-x86_64-with-glibc2.35', 'event': 'train'} + 2022-10-23 11:05:22,273 : INFO : EPOCH 0: training on 60387 raw words (32958 effective words) took 0.1s, 355842 effective words/s + 2022-10-23 11:05:22,369 : INFO : EPOCH 1: training on 60387 raw words (32906 effective words) took 0.1s, 369792 effective words/s + 2022-10-23 11:05:22,466 : INFO : EPOCH 2: training on 60387 raw words (32863 effective words) took 0.1s, 361340 effective words/s + 2022-10-23 11:05:22,563 : INFO : EPOCH 3: training on 60387 raw words (32832 effective words) took 0.1s, 363904 effective words/s + 2022-10-23 11:05:22,662 : INFO : EPOCH 4: training on 60387 raw words (32827 effective words) took 0.1s, 355536 effective words/s + 2022-10-23 11:05:22,662 : INFO : FastText lifecycle event {'msg': 'training on 301935 raw words (164386 effective words) took 0.5s, 333704 effective words/s', 'datetime': '2022-10-23T11:05:22.662680', 'gensim': '4.2.1.dev0', 'python': '3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0]', 'platform': 'Linux-5.19.0-76051900-generic-x86_64-with-glibc2.35', 'event': 'train'} + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 83-86 Training hyperparameters ^^^^^^^^^^^^^^^^^^^^^^^^ +.. GENERATED FROM PYTHON SOURCE LINES 90-118 + Hyperparameters for training the model follow the same pattern as Word2Vec. FastText supports the following parameters from the original word2vec: - model: Training architecture. Allowed values: `cbow`, `skipgram` (Default `cbow`) @@ -141,17 +185,24 @@ Parameters ``min_n`` and ``max_n`` control the lengths of character ngrams that To bound the memory requirements of the model being trained, a hashing function is used that maps ngrams to integers in 1 to K. For hashing these character sequences, the `Fowler-Noll-Vo hashing function `_ (FNV-1a variant) is employed. +.. GENERATED FROM PYTHON SOURCE LINES 122-124 + **Note:** You can continue to train your model while using Gensim's native implementation of fastText. +.. GENERATED FROM PYTHON SOURCE LINES 128-131 + Saving/loading models --------------------- +.. GENERATED FROM PYTHON SOURCE LINES 135-138 + Models can be saved and loaded via the ``load`` and ``save`` methods, just like any other model in Gensim. +.. GENERATED FROM PYTHON SOURCE LINES 139-153 .. code-block:: default @@ -175,20 +226,35 @@ any other model in Gensim. .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - + 2022-10-23 11:05:22,826 : INFO : FastText lifecycle event {'fname_or_handle': '/tmp/saved_model_gensim-grsw1xyt', 'separately': '[]', 'sep_limit': 10485760, 'ignore': frozenset(), 'datetime': '2022-10-23T11:05:22.826086', 'gensim': '4.2.1.dev0', 'python': '3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0]', 'platform': 'Linux-5.19.0-76051900-generic-x86_64-with-glibc2.35', 'event': 'saving'} + 2022-10-23 11:05:22,827 : INFO : storing np array 'vectors_ngrams' to /tmp/saved_model_gensim-grsw1xyt.wv.vectors_ngrams.npy + 2022-10-23 11:05:24,259 : INFO : not storing attribute vectors + 2022-10-23 11:05:24,259 : INFO : not storing attribute buckets_word + 2022-10-23 11:05:24,260 : INFO : not storing attribute cum_table + 2022-10-23 11:05:24,289 : INFO : saved /tmp/saved_model_gensim-grsw1xyt + 2022-10-23 11:05:24,289 : INFO : loading FastText object from /tmp/saved_model_gensim-grsw1xyt + 2022-10-23 11:05:24,292 : INFO : loading wv recursively from /tmp/saved_model_gensim-grsw1xyt.wv.* with mmap=None + 2022-10-23 11:05:24,292 : INFO : loading vectors_ngrams from /tmp/saved_model_gensim-grsw1xyt.wv.vectors_ngrams.npy with mmap=None + 2022-10-23 11:05:24,594 : INFO : setting ignored attribute vectors to None + 2022-10-23 11:05:24,594 : INFO : setting ignored attribute buckets_word to None + 2022-10-23 11:05:24,673 : INFO : setting ignored attribute cum_table to None + 2022-10-23 11:05:24,689 : INFO : FastText lifecycle event {'fname': '/tmp/saved_model_gensim-grsw1xyt', 'datetime': '2022-10-23T11:05:24.689800', 'gensim': '4.2.1.dev0', 'python': '3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0]', 'platform': 'Linux-5.19.0-76051900-generic-x86_64-with-glibc2.35', 'event': 'loaded'} + + +.. GENERATED FROM PYTHON SOURCE LINES 154-158 The ``save_word2vec_format`` is also available for fastText models, but will cause all vectors for ngrams to be lost. As a result, a model loaded in this way will behave as a regular word2vec model. +.. GENERATED FROM PYTHON SOURCE LINES 162-172 + Word vector lookup ------------------ @@ -200,6 +266,7 @@ If you don't need to continue training your model, you can export & save this `. attribute and discard `model`, to save space and RAM. +.. GENERATED FROM PYTHON SOURCE LINES 173-181 .. code-block:: default @@ -217,16 +284,15 @@ attribute and discard `model`, to save space and RAM. .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - + True +.. GENERATED FROM PYTHON SOURCE LINES 183-185 .. code-block:: default @@ -238,8 +304,6 @@ attribute and discard `model`, to save space and RAM. .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none False @@ -247,6 +311,7 @@ attribute and discard `model`, to save space and RAM. +.. GENERATED FROM PYTHON SOURCE LINES 187-189 .. code-block:: default @@ -258,35 +323,34 @@ attribute and discard `model`, to save space and RAM. .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - array([ 0.12453239, -0.26018462, -0.04087191, 0.2563215 , 0.31401935, - 0.16155584, 0.39527607, 0.27404118, -0.45236284, 0.06942682, - 0.36584955, 0.51162827, -0.51161295, -0.192019 , -0.5068029 , - -0.07426998, -0.6276584 , 0.22271585, 0.19990133, 0.2582401 , - 0.14329399, -0.01959469, -0.45576197, -0.06447829, 0.1493489 , - 0.17261286, -0.13472046, 0.26546794, -0.34596932, 0.5626187 , - -0.7038802 , 0.15603925, -0.03104019, -0.06228801, -0.13480644, - -0.0684596 , 0.24728075, 0.55081636, 0.07330963, 0.32814154, - 0.1574982 , 0.56742406, -0.31233737, 0.14195296, 0.0540203 , - 0.01718009, 0.05519052, -0.04002226, 0.16157456, -0.5134223 , - -0.01033936, 0.05745083, -0.39208183, 0.52553374, -1.0542839 , - 0.2145304 , -0.15234643, -0.35197273, -0.6215585 , 0.01796502, - 0.21242104, 0.30762967, 0.2787644 , -0.19908747, 0.7144409 , - 0.45586124, -0.21344525, 0.26920903, -0.651759 , -0.37096855, - -0.16243419, -0.3085725 , -0.70485127, -0.04926324, -0.80278563, - -0.24352737, 0.6427129 , -0.3530421 , -0.29960123, 0.01466726, - -0.18253349, -0.2489397 , 0.00648343, 0.18057272, -0.11812428, - -0.49044088, 0.1847386 , -0.27946883, 0.3941279 , -0.39211616, - 0.26847798, 0.41468227, -0.3953728 , -0.25371104, 0.3390468 , - -0.16447693, -0.18722224, 0.2782088 , -0.0696249 , 0.4313547 ], + array([-0.19996722, 0.1813906 , -0.2631422 , -0.09450997, 0.0605551 , + 0.38595745, 0.30778143, 0.5067505 , 0.23698695, -0.23913051, + 0.02506454, -0.15320891, -0.2434152 , 0.52560467, -0.38980618, + -0.55800015, 0.19291814, -0.23110117, -0.43341738, -0.53108984, + -0.4688596 , -0.04782811, -0.46767992, -0.1137548 , -0.20153292, + -0.31324366, -0.6708753 , -0.10945056, -0.31843412, 0.26011363, + -0.32820454, 0.32238692, 0.8404276 , -0.2502807 , 0.19792764, + 0.37759355, 0.40180317, -0.09189364, -0.36985794, -0.33649284, + 0.46887243, -0.43174997, 0.04100857, -0.39025533, -0.51651365, + -0.32087606, -0.05997978, 0.14294061, 0.360094 , -0.02155857, + 0.37047735, -0.44327876, 0.28450134, -0.4054028 , -0.19731535, + -0.21376207, -0.1685454 , -0.12901361, 0.03528974, -0.35231775, + -0.35454988, -0.43326724, -0.21185161, 0.3519939 , -0.11108 , + 0.69391364, 0.05785353, 0.05663215, 0.42399758, 0.24977471, + -0.24918619, 0.3934391 , 0.5109367 , -0.6553013 , 0.33610865, + -0.09825795, 0.25878346, -0.03377685, 0.06902322, 0.37547323, + 0.17450804, -0.5030028 , -0.82190335, -0.15457787, -0.12070727, + -0.78729135, 0.49075758, 0.19234893, -0.01774574, -0.28116694, + -0.02472195, 0.40292844, -0.14185381, 0.07625303, -0.20744859, + 0.59728205, -0.2217386 , -0.29148448, -0.01873052, -0.2401561 ], dtype=float32) +.. GENERATED FROM PYTHON SOURCE LINES 191-194 .. code-block:: default @@ -299,42 +363,45 @@ attribute and discard `model`, to save space and RAM. .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - array([ 0.10586783, -0.22489995, -0.03636307, 0.22263278, 0.27037606, - 0.1394871 , 0.3411114 , 0.2369042 , -0.38989475, 0.05935 , - 0.31713557, 0.44301754, -0.44249156, -0.16652377, -0.4388366 , - -0.06266895, -0.5436303 , 0.19294666, 0.17363031, 0.22459263, - 0.12532061, -0.01866964, -0.3936521 , -0.05507145, 0.12905194, - 0.14942174, -0.11657442, 0.22935589, -0.29934618, 0.4859668 , - -0.6073519 , 0.13433163, -0.02491274, -0.05468523, -0.11884545, - -0.06117092, 0.21444008, 0.4775469 , 0.06227469, 0.28350767, - 0.13580805, 0.48993143, -0.27067345, 0.1252003 , 0.04606731, - 0.01598426, 0.04640368, -0.03456376, 0.14138013, -0.44429192, - -0.00865329, 0.05027836, -0.341311 , 0.45402458, -0.91097856, - 0.1868968 , -0.13116683, -0.30361563, -0.5364188 , 0.01603454, - 0.18146741, 0.26708448, 0.24074472, -0.17163375, 0.61906886, - 0.39530373, -0.18259627, 0.23319626, -0.5634787 , -0.31959867, - -0.13945322, -0.269441 , -0.60941464, -0.0403638 , -0.69563633, - -0.2098089 , 0.5569868 , -0.30320194, -0.25840232, 0.01436759, - -0.15632603, -0.21624804, 0.00434287, 0.15566474, -0.10228094, - -0.4249678 , 0.16197811, -0.24147548, 0.34205705, -0.3391568 , - 0.23235887, 0.35860622, -0.34247142, -0.21777524, 0.29318404, - -0.1407287 , -0.16115218, 0.24247572, -0.06217333, 0.37221798], + array([-0.17333212, 0.15747589, -0.22726758, -0.08140025, 0.05103909, + 0.33196837, 0.2670658 , 0.43939307, 0.205082 , -0.20810795, + 0.02336278, -0.13075203, -0.21126968, 0.45168898, -0.33789524, + -0.48235178, 0.16582203, -0.19900155, -0.3727986 , -0.4591713 , + -0.401847 , -0.04239817, -0.40366223, -0.09961417, -0.17264459, + -0.26896393, -0.57774097, -0.09225026, -0.27459562, 0.22605109, + -0.28136173, 0.27779424, 0.72365224, -0.21562205, 0.17094932, + 0.3253317 , 0.34816158, -0.07930711, -0.31941393, -0.29101238, + 0.40383977, -0.3717381 , 0.03487907, -0.33628452, -0.4465965 , + -0.27571818, -0.0488493 , 0.12399682, 0.31216368, -0.01752434, + 0.32131058, -0.38280696, 0.24619998, -0.34979105, -0.16987896, + -0.18326469, -0.14740779, -0.1095791 , 0.03177686, -0.30144197, + -0.30499157, -0.37426412, -0.18248272, 0.3032632 , -0.09528783, + 0.59990335, 0.05005969, 0.04626458, 0.36565247, 0.21673569, + -0.2155152 , 0.33764148, 0.4421136 , -0.56542957, 0.29158652, + -0.08375975, 0.22272962, -0.02998246, 0.05934277, 0.3240713 , + 0.1511237 , -0.43450487, -0.7087094 , -0.13446207, -0.10318276, + -0.6806781 , 0.42355484, 0.1661925 , -0.01327086, -0.2432955 , + -0.02126789, 0.34654808, -0.12292334, 0.06645596, -0.1795192 , + 0.5156855 , -0.19275527, -0.24794976, -0.01581961, -0.2081413 ], dtype=float32) +.. GENERATED FROM PYTHON SOURCE LINES 195-198 + Similarity operations --------------------- +.. GENERATED FROM PYTHON SOURCE LINES 202-204 + Similarity operations work the same way as word2vec. **Out-of-vocabulary words can also be used, provided they have at least one character ngram present in the training data.** +.. GENERATED FROM PYTHON SOURCE LINES 205-209 .. code-block:: default @@ -348,8 +415,6 @@ Similarity operations work the same way as word2vec. **Out-of-vocabulary words c .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none False @@ -357,6 +422,7 @@ Similarity operations work the same way as word2vec. **Out-of-vocabulary words c +.. GENERATED FROM PYTHON SOURCE LINES 211-213 .. code-block:: default @@ -368,8 +434,6 @@ Similarity operations work the same way as word2vec. **Out-of-vocabulary words c .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none True @@ -377,6 +441,7 @@ Similarity operations work the same way as word2vec. **Out-of-vocabulary words c +.. GENERATED FROM PYTHON SOURCE LINES 215-217 .. code-block:: default @@ -388,23 +453,26 @@ Similarity operations work the same way as word2vec. **Out-of-vocabulary words c .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - 0.9999929 + 0.9999918 + +.. GENERATED FROM PYTHON SOURCE LINES 218-220 Syntactically similar words generally have high similarity in fastText models, since a large number of the component char-ngrams will be the same. As a result, fastText generally does better at syntactic tasks than Word2Vec. A detailed comparison is provided `here `_. +.. GENERATED FROM PYTHON SOURCE LINES 224-228 + Other similarity operations ^^^^^^^^^^^^^^^^^^^^^^^^^^^ The example training corpus is a toy corpus, results are not expected to be good, for proof-of-concept only +.. GENERATED FROM PYTHON SOURCE LINES 229-231 .. code-block:: default @@ -416,24 +484,23 @@ The example training corpus is a toy corpus, results are not expected to be good .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - [('night', 0.9999929070472717), - ('night.', 0.9999895095825195), - ('flights', 0.999988853931427), - ('rights', 0.9999886751174927), - ('residents', 0.9999884366989136), - ('overnight', 0.9999883770942688), - ('commanders', 0.999988317489624), - ('reached', 0.9999881386756897), - ('commander', 0.9999880790710449), - ('leading', 0.999987781047821)] + [('night', 0.9999918341636658), + ('rights', 0.9999877214431763), + ('flights', 0.9999877214431763), + ('overnight', 0.999987006187439), + ('fighting', 0.9999857544898987), + ('fighters', 0.9999855160713196), + ('fight', 0.9999852180480957), + ('entered', 0.9999851584434509), + ('fighter', 0.999984860420227), + ('eight', 0.999984622001648)] +.. GENERATED FROM PYTHON SOURCE LINES 233-235 .. code-block:: default @@ -445,15 +512,14 @@ The example training corpus is a toy corpus, results are not expected to be good .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - 0.9999402 + 0.99993986 +.. GENERATED FROM PYTHON SOURCE LINES 237-239 .. code-block:: default @@ -465,8 +531,6 @@ The example training corpus is a toy corpus, results are not expected to be good .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none 'lunch' @@ -474,6 +538,7 @@ The example training corpus is a toy corpus, results are not expected to be good +.. GENERATED FROM PYTHON SOURCE LINES 241-243 .. code-block:: default @@ -485,24 +550,23 @@ The example training corpus is a toy corpus, results are not expected to be good .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - [('attempt', 0.999660074710846), - ('biggest', 0.9996545314788818), - ('again', 0.9996527433395386), - ('against', 0.9996523857116699), - ('doubles', 0.9996522068977356), - ('Royal', 0.9996512532234192), - ('Airlines', 0.9996494054794312), - ('forced', 0.9996494054794312), - ('arrest', 0.9996492266654968), - ('follows', 0.999649167060852)] + [('find', 0.9996394515037537), + ('capital,', 0.999639093875885), + ('findings', 0.9996339082717896), + ('seekers.', 0.9996323585510254), + ('field', 0.9996322393417358), + ('finding', 0.9996311664581299), + ('had', 0.9996305704116821), + ('abuse', 0.9996281862258911), + ('storm', 0.9996268153190613), + ('heading', 0.9996247291564941)] +.. GENERATED FROM PYTHON SOURCE LINES 245-247 .. code-block:: default @@ -514,11 +578,20 @@ The example training corpus is a toy corpus, results are not expected to be good .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - (0.24489795918367346, + 2022-10-23 11:05:26,790 : INFO : Evaluating word analogies for top 300000 words in the model on /home/thomas/Documents/FOSS/gensim-tlouf/gensim/test/test_data/questions-words.txt + 2022-10-23 11:05:26,814 : INFO : family: 0.0% (0/2) + 2022-10-23 11:05:26,822 : INFO : gram3-comparative: 8.3% (1/12) + 2022-10-23 11:05:26,827 : INFO : gram4-superlative: 33.3% (4/12) + 2022-10-23 11:05:26,832 : INFO : gram5-present-participle: 45.0% (9/20) + 2022-10-23 11:05:26,845 : INFO : gram6-nationality-adjective: 30.0% (6/20) + 2022-10-23 11:05:26,851 : INFO : gram7-past-tense: 5.0% (1/20) + 2022-10-23 11:05:26,856 : INFO : gram8-plural: 33.3% (4/12) + 2022-10-23 11:05:26,858 : INFO : Quadruplets with out-of-vocabulary words: 99.5% + 2022-10-23 11:05:26,859 : INFO : NB: analogies containing OOV words were skipped from evaluation! To change this behavior, use "dummy4unknown=True" + 2022-10-23 11:05:26,859 : INFO : Total accuracy: 25.5% (25/98) + (0.25510204081632654, [{'correct': [], 'incorrect': [], 'section': 'capital-common-countries'}, {'correct': [], 'incorrect': [], 'section': 'capital-world'}, {'correct': [], 'incorrect': [], 'section': 'currency'}, @@ -528,81 +601,80 @@ The example training corpus is a toy corpus, results are not expected to be good 'section': 'family'}, {'correct': [], 'incorrect': [], 'section': 'gram1-adjective-to-adverb'}, {'correct': [], 'incorrect': [], 'section': 'gram2-opposite'}, - {'correct': [('GOOD', 'BETTER', 'LOW', 'LOWER'), - ('GREAT', 'GREATER', 'LOW', 'LOWER'), - ('LONG', 'LONGER', 'LOW', 'LOWER')], + {'correct': [('LONG', 'LONGER', 'GREAT', 'GREATER')], 'incorrect': [('GOOD', 'BETTER', 'GREAT', 'GREATER'), ('GOOD', 'BETTER', 'LONG', 'LONGER'), + ('GOOD', 'BETTER', 'LOW', 'LOWER'), ('GREAT', 'GREATER', 'LONG', 'LONGER'), + ('GREAT', 'GREATER', 'LOW', 'LOWER'), ('GREAT', 'GREATER', 'GOOD', 'BETTER'), + ('LONG', 'LONGER', 'LOW', 'LOWER'), ('LONG', 'LONGER', 'GOOD', 'BETTER'), - ('LONG', 'LONGER', 'GREAT', 'GREATER'), ('LOW', 'LOWER', 'GOOD', 'BETTER'), ('LOW', 'LOWER', 'GREAT', 'GREATER'), ('LOW', 'LOWER', 'LONG', 'LONGER')], 'section': 'gram3-comparative'}, - {'correct': [('BIG', 'BIGGEST', 'LARGE', 'LARGEST'), - ('GOOD', 'BEST', 'LARGE', 'LARGEST'), - ('GREAT', 'GREATEST', 'LARGE', 'LARGEST')], + {'correct': [('GOOD', 'BEST', 'LARGE', 'LARGEST'), + ('GREAT', 'GREATEST', 'LARGE', 'LARGEST'), + ('GREAT', 'GREATEST', 'BIG', 'BIGGEST'), + ('LARGE', 'LARGEST', 'BIG', 'BIGGEST')], 'incorrect': [('BIG', 'BIGGEST', 'GOOD', 'BEST'), ('BIG', 'BIGGEST', 'GREAT', 'GREATEST'), + ('BIG', 'BIGGEST', 'LARGE', 'LARGEST'), ('GOOD', 'BEST', 'GREAT', 'GREATEST'), ('GOOD', 'BEST', 'BIG', 'BIGGEST'), - ('GREAT', 'GREATEST', 'BIG', 'BIGGEST'), ('GREAT', 'GREATEST', 'GOOD', 'BEST'), - ('LARGE', 'LARGEST', 'BIG', 'BIGGEST'), ('LARGE', 'LARGEST', 'GOOD', 'BEST'), ('LARGE', 'LARGEST', 'GREAT', 'GREATEST')], 'section': 'gram4-superlative'}, - {'correct': [('GO', 'GOING', 'SAY', 'SAYING'), - ('LOOK', 'LOOKING', 'PLAY', 'PLAYING'), + {'correct': [('GO', 'GOING', 'PLAY', 'PLAYING'), + ('GO', 'GOING', 'SAY', 'SAYING'), ('LOOK', 'LOOKING', 'SAY', 'SAYING'), ('LOOK', 'LOOKING', 'GO', 'GOING'), ('PLAY', 'PLAYING', 'SAY', 'SAYING'), ('PLAY', 'PLAYING', 'GO', 'GOING'), - ('SAY', 'SAYING', 'GO', 'GOING')], + ('PLAY', 'PLAYING', 'LOOK', 'LOOKING'), + ('SAY', 'SAYING', 'GO', 'GOING'), + ('SAY', 'SAYING', 'PLAY', 'PLAYING')], 'incorrect': [('GO', 'GOING', 'LOOK', 'LOOKING'), - ('GO', 'GOING', 'PLAY', 'PLAYING'), ('GO', 'GOING', 'RUN', 'RUNNING'), + ('LOOK', 'LOOKING', 'PLAY', 'PLAYING'), ('LOOK', 'LOOKING', 'RUN', 'RUNNING'), ('PLAY', 'PLAYING', 'RUN', 'RUNNING'), - ('PLAY', 'PLAYING', 'LOOK', 'LOOKING'), ('RUN', 'RUNNING', 'SAY', 'SAYING'), ('RUN', 'RUNNING', 'GO', 'GOING'), ('RUN', 'RUNNING', 'LOOK', 'LOOKING'), ('RUN', 'RUNNING', 'PLAY', 'PLAYING'), ('SAY', 'SAYING', 'LOOK', 'LOOKING'), - ('SAY', 'SAYING', 'PLAY', 'PLAYING'), ('SAY', 'SAYING', 'RUN', 'RUNNING')], 'section': 'gram5-present-participle'}, {'correct': [('AUSTRALIA', 'AUSTRALIAN', 'INDIA', 'INDIAN'), ('AUSTRALIA', 'AUSTRALIAN', 'ISRAEL', 'ISRAELI'), ('FRANCE', 'FRENCH', 'INDIA', 'INDIAN'), ('INDIA', 'INDIAN', 'ISRAEL', 'ISRAELI'), - ('ISRAEL', 'ISRAELI', 'INDIA', 'INDIAN'), - ('SWITZERLAND', 'SWISS', 'INDIA', 'INDIAN')], + ('INDIA', 'INDIAN', 'AUSTRALIA', 'AUSTRALIAN'), + ('ISRAEL', 'ISRAELI', 'INDIA', 'INDIAN')], 'incorrect': [('AUSTRALIA', 'AUSTRALIAN', 'FRANCE', 'FRENCH'), ('AUSTRALIA', 'AUSTRALIAN', 'SWITZERLAND', 'SWISS'), ('FRANCE', 'FRENCH', 'ISRAEL', 'ISRAELI'), ('FRANCE', 'FRENCH', 'SWITZERLAND', 'SWISS'), ('FRANCE', 'FRENCH', 'AUSTRALIA', 'AUSTRALIAN'), ('INDIA', 'INDIAN', 'SWITZERLAND', 'SWISS'), - ('INDIA', 'INDIAN', 'AUSTRALIA', 'AUSTRALIAN'), ('INDIA', 'INDIAN', 'FRANCE', 'FRENCH'), ('ISRAEL', 'ISRAELI', 'SWITZERLAND', 'SWISS'), ('ISRAEL', 'ISRAELI', 'AUSTRALIA', 'AUSTRALIAN'), ('ISRAEL', 'ISRAELI', 'FRANCE', 'FRENCH'), ('SWITZERLAND', 'SWISS', 'AUSTRALIA', 'AUSTRALIAN'), ('SWITZERLAND', 'SWISS', 'FRANCE', 'FRENCH'), + ('SWITZERLAND', 'SWISS', 'INDIA', 'INDIAN'), ('SWITZERLAND', 'SWISS', 'ISRAEL', 'ISRAELI')], 'section': 'gram6-nationality-adjective'}, - {'correct': [], + {'correct': [('PAYING', 'PAID', 'SAYING', 'SAID')], 'incorrect': [('GOING', 'WENT', 'PAYING', 'PAID'), ('GOING', 'WENT', 'PLAYING', 'PLAYED'), ('GOING', 'WENT', 'SAYING', 'SAID'), ('GOING', 'WENT', 'TAKING', 'TOOK'), ('PAYING', 'PAID', 'PLAYING', 'PLAYED'), - ('PAYING', 'PAID', 'SAYING', 'SAID'), ('PAYING', 'PAID', 'TAKING', 'TOOK'), ('PAYING', 'PAID', 'GOING', 'WENT'), ('PLAYING', 'PLAYED', 'SAYING', 'SAID'), @@ -618,76 +690,76 @@ The example training corpus is a toy corpus, results are not expected to be good ('TAKING', 'TOOK', 'PLAYING', 'PLAYED'), ('TAKING', 'TOOK', 'SAYING', 'SAID')], 'section': 'gram7-past-tense'}, - {'correct': [('BUILDING', 'BUILDINGS', 'CAR', 'CARS'), - ('BUILDING', 'BUILDINGS', 'CHILD', 'CHILDREN'), - ('CAR', 'CARS', 'BUILDING', 'BUILDINGS'), - ('CHILD', 'CHILDREN', 'CAR', 'CARS'), - ('MAN', 'MEN', 'CAR', 'CARS')], - 'incorrect': [('BUILDING', 'BUILDINGS', 'MAN', 'MEN'), - ('CAR', 'CARS', 'CHILD', 'CHILDREN'), + {'correct': [('BUILDING', 'BUILDINGS', 'CHILD', 'CHILDREN'), + ('CAR', 'CARS', 'CHILD', 'CHILDREN'), + ('MAN', 'MEN', 'BUILDING', 'BUILDINGS'), + ('MAN', 'MEN', 'CHILD', 'CHILDREN')], + 'incorrect': [('BUILDING', 'BUILDINGS', 'CAR', 'CARS'), + ('BUILDING', 'BUILDINGS', 'MAN', 'MEN'), ('CAR', 'CARS', 'MAN', 'MEN'), + ('CAR', 'CARS', 'BUILDING', 'BUILDINGS'), ('CHILD', 'CHILDREN', 'MAN', 'MEN'), ('CHILD', 'CHILDREN', 'BUILDING', 'BUILDINGS'), - ('MAN', 'MEN', 'BUILDING', 'BUILDINGS'), - ('MAN', 'MEN', 'CHILD', 'CHILDREN')], + ('CHILD', 'CHILDREN', 'CAR', 'CARS'), + ('MAN', 'MEN', 'CAR', 'CARS')], 'section': 'gram8-plural'}, {'correct': [], 'incorrect': [], 'section': 'gram9-plural-verbs'}, - {'correct': [('GOOD', 'BETTER', 'LOW', 'LOWER'), - ('GREAT', 'GREATER', 'LOW', 'LOWER'), - ('LONG', 'LONGER', 'LOW', 'LOWER'), - ('BIG', 'BIGGEST', 'LARGE', 'LARGEST'), + {'correct': [('LONG', 'LONGER', 'GREAT', 'GREATER'), ('GOOD', 'BEST', 'LARGE', 'LARGEST'), ('GREAT', 'GREATEST', 'LARGE', 'LARGEST'), + ('GREAT', 'GREATEST', 'BIG', 'BIGGEST'), + ('LARGE', 'LARGEST', 'BIG', 'BIGGEST'), + ('GO', 'GOING', 'PLAY', 'PLAYING'), ('GO', 'GOING', 'SAY', 'SAYING'), - ('LOOK', 'LOOKING', 'PLAY', 'PLAYING'), ('LOOK', 'LOOKING', 'SAY', 'SAYING'), ('LOOK', 'LOOKING', 'GO', 'GOING'), ('PLAY', 'PLAYING', 'SAY', 'SAYING'), ('PLAY', 'PLAYING', 'GO', 'GOING'), + ('PLAY', 'PLAYING', 'LOOK', 'LOOKING'), ('SAY', 'SAYING', 'GO', 'GOING'), + ('SAY', 'SAYING', 'PLAY', 'PLAYING'), ('AUSTRALIA', 'AUSTRALIAN', 'INDIA', 'INDIAN'), ('AUSTRALIA', 'AUSTRALIAN', 'ISRAEL', 'ISRAELI'), ('FRANCE', 'FRENCH', 'INDIA', 'INDIAN'), ('INDIA', 'INDIAN', 'ISRAEL', 'ISRAELI'), + ('INDIA', 'INDIAN', 'AUSTRALIA', 'AUSTRALIAN'), ('ISRAEL', 'ISRAELI', 'INDIA', 'INDIAN'), - ('SWITZERLAND', 'SWISS', 'INDIA', 'INDIAN'), - ('BUILDING', 'BUILDINGS', 'CAR', 'CARS'), + ('PAYING', 'PAID', 'SAYING', 'SAID'), ('BUILDING', 'BUILDINGS', 'CHILD', 'CHILDREN'), - ('CAR', 'CARS', 'BUILDING', 'BUILDINGS'), - ('CHILD', 'CHILDREN', 'CAR', 'CARS'), - ('MAN', 'MEN', 'CAR', 'CARS')], + ('CAR', 'CARS', 'CHILD', 'CHILDREN'), + ('MAN', 'MEN', 'BUILDING', 'BUILDINGS'), + ('MAN', 'MEN', 'CHILD', 'CHILDREN')], 'incorrect': [('HE', 'SHE', 'HIS', 'HER'), ('HIS', 'HER', 'HE', 'SHE'), ('GOOD', 'BETTER', 'GREAT', 'GREATER'), ('GOOD', 'BETTER', 'LONG', 'LONGER'), + ('GOOD', 'BETTER', 'LOW', 'LOWER'), ('GREAT', 'GREATER', 'LONG', 'LONGER'), + ('GREAT', 'GREATER', 'LOW', 'LOWER'), ('GREAT', 'GREATER', 'GOOD', 'BETTER'), + ('LONG', 'LONGER', 'LOW', 'LOWER'), ('LONG', 'LONGER', 'GOOD', 'BETTER'), - ('LONG', 'LONGER', 'GREAT', 'GREATER'), ('LOW', 'LOWER', 'GOOD', 'BETTER'), ('LOW', 'LOWER', 'GREAT', 'GREATER'), ('LOW', 'LOWER', 'LONG', 'LONGER'), ('BIG', 'BIGGEST', 'GOOD', 'BEST'), ('BIG', 'BIGGEST', 'GREAT', 'GREATEST'), + ('BIG', 'BIGGEST', 'LARGE', 'LARGEST'), ('GOOD', 'BEST', 'GREAT', 'GREATEST'), ('GOOD', 'BEST', 'BIG', 'BIGGEST'), - ('GREAT', 'GREATEST', 'BIG', 'BIGGEST'), ('GREAT', 'GREATEST', 'GOOD', 'BEST'), - ('LARGE', 'LARGEST', 'BIG', 'BIGGEST'), ('LARGE', 'LARGEST', 'GOOD', 'BEST'), ('LARGE', 'LARGEST', 'GREAT', 'GREATEST'), ('GO', 'GOING', 'LOOK', 'LOOKING'), - ('GO', 'GOING', 'PLAY', 'PLAYING'), ('GO', 'GOING', 'RUN', 'RUNNING'), + ('LOOK', 'LOOKING', 'PLAY', 'PLAYING'), ('LOOK', 'LOOKING', 'RUN', 'RUNNING'), ('PLAY', 'PLAYING', 'RUN', 'RUNNING'), - ('PLAY', 'PLAYING', 'LOOK', 'LOOKING'), ('RUN', 'RUNNING', 'SAY', 'SAYING'), ('RUN', 'RUNNING', 'GO', 'GOING'), ('RUN', 'RUNNING', 'LOOK', 'LOOKING'), ('RUN', 'RUNNING', 'PLAY', 'PLAYING'), ('SAY', 'SAYING', 'LOOK', 'LOOKING'), - ('SAY', 'SAYING', 'PLAY', 'PLAYING'), ('SAY', 'SAYING', 'RUN', 'RUNNING'), ('AUSTRALIA', 'AUSTRALIAN', 'FRANCE', 'FRENCH'), ('AUSTRALIA', 'AUSTRALIAN', 'SWITZERLAND', 'SWISS'), @@ -695,20 +767,19 @@ The example training corpus is a toy corpus, results are not expected to be good ('FRANCE', 'FRENCH', 'SWITZERLAND', 'SWISS'), ('FRANCE', 'FRENCH', 'AUSTRALIA', 'AUSTRALIAN'), ('INDIA', 'INDIAN', 'SWITZERLAND', 'SWISS'), - ('INDIA', 'INDIAN', 'AUSTRALIA', 'AUSTRALIAN'), ('INDIA', 'INDIAN', 'FRANCE', 'FRENCH'), ('ISRAEL', 'ISRAELI', 'SWITZERLAND', 'SWISS'), ('ISRAEL', 'ISRAELI', 'AUSTRALIA', 'AUSTRALIAN'), ('ISRAEL', 'ISRAELI', 'FRANCE', 'FRENCH'), ('SWITZERLAND', 'SWISS', 'AUSTRALIA', 'AUSTRALIAN'), ('SWITZERLAND', 'SWISS', 'FRANCE', 'FRENCH'), + ('SWITZERLAND', 'SWISS', 'INDIA', 'INDIAN'), ('SWITZERLAND', 'SWISS', 'ISRAEL', 'ISRAELI'), ('GOING', 'WENT', 'PAYING', 'PAID'), ('GOING', 'WENT', 'PLAYING', 'PLAYED'), ('GOING', 'WENT', 'SAYING', 'SAID'), ('GOING', 'WENT', 'TAKING', 'TOOK'), ('PAYING', 'PAID', 'PLAYING', 'PLAYED'), - ('PAYING', 'PAID', 'SAYING', 'SAID'), ('PAYING', 'PAID', 'TAKING', 'TOOK'), ('PAYING', 'PAID', 'GOING', 'WENT'), ('PLAYING', 'PLAYED', 'SAYING', 'SAID'), @@ -723,25 +794,29 @@ The example training corpus is a toy corpus, results are not expected to be good ('TAKING', 'TOOK', 'PAYING', 'PAID'), ('TAKING', 'TOOK', 'PLAYING', 'PLAYED'), ('TAKING', 'TOOK', 'SAYING', 'SAID'), + ('BUILDING', 'BUILDINGS', 'CAR', 'CARS'), ('BUILDING', 'BUILDINGS', 'MAN', 'MEN'), - ('CAR', 'CARS', 'CHILD', 'CHILDREN'), ('CAR', 'CARS', 'MAN', 'MEN'), + ('CAR', 'CARS', 'BUILDING', 'BUILDINGS'), ('CHILD', 'CHILDREN', 'MAN', 'MEN'), ('CHILD', 'CHILDREN', 'BUILDING', 'BUILDINGS'), - ('MAN', 'MEN', 'BUILDING', 'BUILDINGS'), - ('MAN', 'MEN', 'CHILD', 'CHILDREN')], + ('CHILD', 'CHILDREN', 'CAR', 'CARS'), + ('MAN', 'MEN', 'CAR', 'CARS')], 'section': 'Total accuracy'}]) +.. GENERATED FROM PYTHON SOURCE LINES 248-254 + Word Movers distance ^^^^^^^^^^^^^^^^^^^^ -You'll need the optional ``pyemd`` library for this section, ``pip install pyemd``. +You'll need the optional ``POT`` library for this section, ``pip install POT``. Let's start with two sentences: +.. GENERATED FROM PYTHON SOURCE LINES 254-258 .. code-block:: default @@ -756,9 +831,12 @@ Let's start with two sentences: +.. GENERATED FROM PYTHON SOURCE LINES 259-261 + Remove their stopwords. +.. GENERATED FROM PYTHON SOURCE LINES 261-265 .. code-block:: default @@ -773,8 +851,11 @@ Remove their stopwords. +.. GENERATED FROM PYTHON SOURCE LINES 266-267 + Compute the Word Movers Distance between the two sentences. +.. GENERATED FROM PYTHON SOURCE LINES 267-270 .. code-block:: default @@ -787,18 +868,22 @@ Compute the Word Movers Distance between the two sentences. .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - 'Word Movers Distance is 0.015923231075180694 (lower means closer)' + 2022-10-23 11:05:27,139 : INFO : adding document #0 to Dictionary<0 unique tokens: []> + 2022-10-23 11:05:27,140 : INFO : built Dictionary<8 unique tokens: ['illinois', 'media', 'obama', 'speaks', 'chicago']...> from 2 documents (total 8 corpus positions) + 2022-10-23 11:05:27,140 : INFO : Dictionary lifecycle event {'msg': "built Dictionary<8 unique tokens: ['illinois', 'media', 'obama', 'speaks', 'chicago']...> from 2 documents (total 8 corpus positions)", 'datetime': '2022-10-23T11:05:27.140129', 'gensim': '4.2.1.dev0', 'python': '3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0]', 'platform': 'Linux-5.19.0-76051900-generic-x86_64-with-glibc2.35', 'event': 'created'} + 'Word Movers Distance is 0.01600033861640832 (lower means closer)' +.. GENERATED FROM PYTHON SOURCE LINES 271-273 + That's all! You've made it to the end of this tutorial. +.. GENERATED FROM PYTHON SOURCE LINES 273-278 .. code-block:: default @@ -810,9 +895,10 @@ That's all! You've made it to the end of this tutorial. -.. image:: /auto_examples/tutorials/images/sphx_glr_run_fasttext_001.png - :alt: run fasttext - :class: sphx-glr-single-img +.. image-sg:: /auto_examples/tutorials/images/sphx_glr_run_fasttext_001.png + :alt: run fasttext + :srcset: /auto_examples/tutorials/images/sphx_glr_run_fasttext_001.png + :class: sphx-glr-single-img @@ -821,30 +907,25 @@ That's all! You've made it to the end of this tutorial. .. rst-class:: sphx-glr-timing - **Total running time of the script:** ( 0 minutes 28.645 seconds) + **Total running time of the script:** ( 0 minutes 7.208 seconds) -**Estimated memory usage:** 2975 MB +**Estimated memory usage:** 1619 MB .. _sphx_glr_download_auto_examples_tutorials_run_fasttext.py: +.. only:: html -.. only :: html - - .. container:: sphx-glr-footer - :class: sphx-glr-footer-example - - - - .. container:: sphx-glr-download sphx-glr-download-python + .. container:: sphx-glr-footer sphx-glr-footer-example - :download:`Download Python source code: run_fasttext.py ` + .. container:: sphx-glr-download sphx-glr-download-python + :download:`Download Python source code: run_fasttext.py ` - .. container:: sphx-glr-download sphx-glr-download-jupyter + .. container:: sphx-glr-download sphx-glr-download-jupyter - :download:`Download Jupyter notebook: run_fasttext.ipynb ` + :download:`Download Jupyter notebook: run_fasttext.ipynb ` .. only:: html diff --git a/docs/src/auto_examples/tutorials/run_fasttext_codeobj.pickle b/docs/src/auto_examples/tutorials/run_fasttext_codeobj.pickle new file mode 100644 index 0000000000000000000000000000000000000000..1be3e19b04597db39ff779f3d4ac6c72e7b74473 GIT binary patch literal 9038 zcmd5?O^6&t6wc31b~dxSnrsqH3`Q|x6zPM4;zf{zV2ETTiN=dIwbL~_J@y~nHT$C| z0WU5zh&Xx>L=n7r$z8m7Q4qW;UOebY$R*%KQSep&)vK!Rnx1R~_cYT}-}|fYy?U>D zuXjE?`_P{3FYWb1&!c_jhCwn(d#kZSW9po9`)u;THL0BeCl z>Lu0y5DXBLVXyh8#hu~O&bYmfB}oT}9v&OcD&s&dgM*Jt;Cl?tosbk;uk$c$xk5JR3n6io?OH568Z*~RcrYGm1w(osD*!ZI1pYbHv9qlg ztm&4lnV9LJMgd1plA$g%hf9JYyPvM??qvpBEGBLs%w8_dE`GRu<7*_%=0c4ChUxgj?7 zNV%yv$*1tLv7%vuHVW24HhfKTqY;RlA(bV-jzlcRhM7CQ(G=jJphe7{D{$(|lQ>yMVT`cDW*Z z!g4^>ECb+11vr%3$bWR~jj&JF1l_CBZQ-BI3|BX>_6$+wsUUO1(`Xjjgs%$+^MpTA z9n@w=+I9|&@@90T$t>i^cd{c@^;8_ui2Hpz9MbedfuLzO=fwZ5ulP}RsjeU3nSosT zxpIlubeLvr7TRv$(5))N<{UnFs)xZZ1(3ijN*IXYf zLaW}Ze)^VmXbCp_ONx0L8HgbNNMLz{l@;&e#50N2{4t()CP~7pQFrg2J(6Q$IF_AK ztH!9;7KyYi3k;Nw$YU1Sayj7&>NKXyXhk6NIz5*mdA2~~6?q}^+%+rO=AZa5SNF8c zmi?~3UM2N>;E*^b+dM%#!Pt-BQ*U%qsLPwYpqw(KOQ8D3Nm_dc$lgwiMvu|M!ihX) z?F6CyI-^RETRqj$T@c?aSo6(DeX}g&-jk|(qT5ql)9C+Hdo0rUj6l(JuU)aQ!^RQs z&*VX4^sHD;(!&W27=^1&JBn%II`FN+&v|eSs=?1Y(aRgrRa1J3Aq z0W7jD1A%$-g%qA77sV=?cCzC{$d#^|8f&__dy{0$#sLY6HsBR(^E?!StYB0HXyLgn znq2V7dbSsLqtfE$4|{3NL)^H^5)KLpJYocE7baHf#UpdsP-C(9uDCRoWT*>k;@t#A zK?^do@xrnj<&;x4*Vq_0$(YejtRC69t=U=jz}!wXh1CaA5HS#GxUOj^Tg#eA4A6bQ z>R@>mb)uNxFbD=THH@Pj!JRxO4@5Z@T&q6zMyXgv)8(pUglohnQ_98r0!m;YjUYQL z*N-W2+SvDzKo|BESg@}qXAFr%(}ggx#)0PsuM5p5_!t9kaO^Tdb{N|}Wy8?X3g4%i zErm3&Wnake_D7$KB{V&l_reLh7Z3`U%B{7s=KxihK7D`lKg{gf7@>-gUY@y!_h~Tix7u zek-sQt1~Rnp{~a?cb;M=`RgH}UwJ+w)lgB@1+~~SBOb-^rz{qDb+yBpJG$9Msu=~% zXtrhuGW%D}?20U<*;A>8ZPn07*YB#Xxl+X``E_YoJFK$E@S1c}kd3DImA7`*C3vOn zNSAwpInB(yC4}Y7JzOIPwyay~;THTMKr~$`JH|DVxc#$XFmU9&#B#emjYhvvL-R%`_ [4] vector embeddings of\nwords. It been shown to outperform many of the state-of-the-art methods in\nk-nearest neighbors classification [3].\n\nWMD is illustrated below for two very similar sentences (illustration taken\nfrom `Vlad Niculae's blog\n`_). The sentences\nhave no words in common, but by matching the relevant words, WMD is able to\naccurately measure the (dis)similarity between the two sentences. The method\nalso uses the bag-of-words representation of the documents (simply put, the\nword's frequencies in the documents), noted as $d$ in the figure below. The\nintuition behind the method is that we find the minimum \"traveling distance\"\nbetween documents, in other words the most efficient way to \"move\" the\ndistribution of document 1 to the distribution of document 2.\n\n\n" + "Word Mover's Distance (WMD) is a promising new tool in machine learning that\nallows us to submit a query and return the most relevant documents. This\ntutorial introduces WMD and shows how you can compute the WMD distance\nbetween two documents using ``wmdistance``.\n\n## WMD Basics\n\nWMD enables us to assess the \"distance\" between two documents in a meaningful\nway even when they have no words in common. It uses [word2vec](http://rare-technologies.com/word2vec-tutorial/) [4] vector embeddings of\nwords. It been shown to outperform many of the state-of-the-art methods in\nk-nearest neighbors classification [3].\n\nWMD is illustrated below for two very similar sentences (illustration taken\nfrom [Vlad Niculae's blog](http://vene.ro/blog/word-movers-distance-in-python.html)). The sentences\nhave no words in common, but by matching the relevant words, WMD is able to\naccurately measure the (dis)similarity between the two sentences. The method\nalso uses the bag-of-words representation of the documents (simply put, the\nword's frequencies in the documents), noted as $d$ in the figure below. The\nintuition behind the method is that we find the minimum \"traveling distance\"\nbetween documents, in other words the most efficient way to \"move\" the\ndistribution of document 1 to the distribution of document 2.\n\n\n" ] }, { @@ -40,7 +40,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This method was introduced in the article \"From Word Embeddings To Document\nDistances\" by Matt Kusner et al. (\\ `link to PDF\n`_\\ ). It is inspired\nby the \"Earth Mover's Distance\", and employs a solver of the \"transportation\nproblem\".\n\nIn this tutorial, we will learn how to use Gensim's WMD functionality, which\nconsists of the ``wmdistance`` method for distance computation, and the\n``WmdSimilarity`` class for corpus based similarity queries.\n\n.. Important::\n If you use Gensim's WMD functionality, please consider citing [1], [2] and [3].\n\nComputing the Word Mover's Distance\n-----------------------------------\n\nTo use WMD, you need some existing word embeddings.\nYou could train your own Word2Vec model, but that is beyond the scope of this tutorial\n(check out `sphx_glr_auto_examples_tutorials_run_word2vec.py` if you're interested).\nFor this tutorial, we'll be using an existing Word2Vec model.\n\nLet's take some sentences to compute the distance between.\n\n\n" + "This method was introduced in the article \"From Word Embeddings To Document\nDistances\" by Matt Kusner et al. (\\ [link to PDF](http://jmlr.org/proceedings/papers/v37/kusnerb15.pdf)\\ ). It is inspired\nby the \"Earth Mover's Distance\", and employs a solver of the \"transportation\nproblem\".\n\nIn this tutorial, we will learn how to use Gensim's WMD functionality, which\nconsists of the ``wmdistance`` method for distance computation, and the\n``WmdSimilarity`` class for corpus based similarity queries.\n\n.. Important::\n If you use Gensim's WMD functionality, please consider citing [1] and [2].\n\n## Computing the Word Mover's Distance\n\nTo use WMD, you need some existing word embeddings.\nYou could train your own Word2Vec model, but that is beyond the scope of this tutorial\n(check out `sphx_glr_auto_examples_tutorials_run_word2vec.py` if you're interested).\nFor this tutorial, we'll be using an existing Word2Vec model.\n\nLet's take some sentences to compute the distance between.\n\n\n" ] }, { @@ -130,7 +130,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "References\n----------\n\n1. Ofir Pele and Michael Werman, *A linear time histogram metric for improved SIFT matching*, 2008.\n2. Ofir Pele and Michael Werman, *Fast and robust earth mover's distances*, 2009.\n3. Matt Kusner et al. *From Embeddings To Document Distances*, 2015.\n4. Tom\u00e1\u0161 Mikolov et al. *Efficient Estimation of Word Representations in Vector Space*, 2013.\n\n\n" + "## References\n\n1. R\u00e9mi Flamary et al. *POT: Python Optimal Transport*, 2021.\n2. Matt Kusner et al. *From Embeddings To Document Distances*, 2015.\n3. Tom\u00e1\u0161 Mikolov et al. *Efficient Estimation of Word Representations in Vector Space*, 2013.\n\n\n" ] } ], @@ -150,7 +150,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.3" + "version": "3.10.6" } }, "nbformat": 4, diff --git a/docs/src/auto_examples/tutorials/run_wmd.py b/docs/src/auto_examples/tutorials/run_wmd.py index 06e263063a..c037ef697b 100644 --- a/docs/src/auto_examples/tutorials/run_wmd.py +++ b/docs/src/auto_examples/tutorials/run_wmd.py @@ -53,7 +53,7 @@ # ``WmdSimilarity`` class for corpus based similarity queries. # # .. Important:: -# If you use Gensim's WMD functionality, please consider citing [1], [2] and [3]. +# If you use Gensim's WMD functionality, please consider citing [1] and [2]. # # Computing the Word Mover's Distance # ----------------------------------- @@ -118,8 +118,7 @@ def preprocess(sentence): # References # ---------- # -# 1. Ofir Pele and Michael Werman, *A linear time histogram metric for improved SIFT matching*, 2008. -# 2. Ofir Pele and Michael Werman, *Fast and robust earth mover's distances*, 2009. -# 3. Matt Kusner et al. *From Embeddings To Document Distances*, 2015. -# 4. Tomáš Mikolov et al. *Efficient Estimation of Word Representations in Vector Space*, 2013. +# 1. Rémi Flamary et al. *POT: Python Optimal Transport*, 2021. +# 2. Matt Kusner et al. *From Embeddings To Document Distances*, 2015. +# 3. Tomáš Mikolov et al. *Efficient Estimation of Word Representations in Vector Space*, 2013. # diff --git a/docs/src/auto_examples/tutorials/run_wmd.py.md5 b/docs/src/auto_examples/tutorials/run_wmd.py.md5 index 382c5b9954..b6772e20fb 100644 --- a/docs/src/auto_examples/tutorials/run_wmd.py.md5 +++ b/docs/src/auto_examples/tutorials/run_wmd.py.md5 @@ -1 +1 @@ -45521a352637a0f53e62f3e19e61fc07 \ No newline at end of file +a087a5b43fbba9a3e71c2384ddc264af \ No newline at end of file diff --git a/docs/src/auto_examples/tutorials/run_wmd.rst b/docs/src/auto_examples/tutorials/run_wmd.rst index cc62e120dd..6a04bcc412 100644 --- a/docs/src/auto_examples/tutorials/run_wmd.rst +++ b/docs/src/auto_examples/tutorials/run_wmd.rst @@ -1,7 +1,18 @@ -.. note:: - :class: sphx-glr-download-link-note - Click :ref:`here ` to download the full example code +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples/tutorials/run_wmd.py" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. note:: + :class: sphx-glr-download-link-note + + Click :ref:`here ` + to download the full example code + .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_examples_tutorials_run_wmd.py: @@ -12,6 +23,8 @@ Word Mover's Distance Demonstrates using Gensim's implemenation of the WMD. +.. GENERATED FROM PYTHON SOURCE LINES 10-35 + Word Mover's Distance (WMD) is a promising new tool in machine learning that allows us to submit a query and return the most relevant documents. This tutorial introduces WMD and shows how you can compute the WMD distance @@ -38,6 +51,7 @@ between documents, in other words the most efficient way to "move" the distribution of document 1 to the distribution of document 2. +.. GENERATED FROM PYTHON SOURCE LINES 35-44 .. code-block:: default @@ -53,11 +67,16 @@ distribution of document 1 to the distribution of document 2. -.. image:: /auto_examples/tutorials/images/sphx_glr_run_wmd_001.png - :class: sphx-glr-single-img +.. image-sg:: /auto_examples/tutorials/images/sphx_glr_run_wmd_001.png + :alt: run wmd + :srcset: /auto_examples/tutorials/images/sphx_glr_run_wmd_001.png + :class: sphx-glr-single-img + + +.. GENERATED FROM PYTHON SOURCE LINES 45-68 This method was introduced in the article "From Word Embeddings To Document Distances" by Matt Kusner et al. (\ `link to PDF @@ -70,7 +89,7 @@ consists of the ``wmdistance`` method for distance computation, and the ``WmdSimilarity`` class for corpus based similarity queries. .. Important:: - If you use Gensim's WMD functionality, please consider citing [1], [2] and [3]. + If you use Gensim's WMD functionality, please consider citing [1] and [2]. Computing the Word Mover's Distance ----------------------------------- @@ -83,6 +102,7 @@ For this tutorial, we'll be using an existing Word2Vec model. Let's take some sentences to compute the distance between. +.. GENERATED FROM PYTHON SOURCE LINES 68-76 .. code-block:: default @@ -100,11 +120,15 @@ Let's take some sentences to compute the distance between. + +.. GENERATED FROM PYTHON SOURCE LINES 77-81 + These sentences have very similar content, and as such the WMD should be low. Before we compute the WMD, we want to remove stopwords ("the", "to", etc.), as these do not contribute a lot to the information in the sentences. +.. GENERATED FROM PYTHON SOURCE LINES 81-94 .. code-block:: default @@ -127,45 +151,16 @@ as these do not contribute a lot to the information in the sentences. .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - /home/witiko/.virtualenvs/gensim4/lib/python3.7/site-packages/sklearn/feature_extraction/image.py:167: DeprecationWarning: `np.int` is a deprecated alias for the builtin `int`. To silence this warning, use `int` by itself. Doing this will not modify any behavior and is safe. When replacing `np.int`, you may wish to use e.g. `np.int64` or `np.int32` to specify the precision. If you wish to review your current use, check the release note link for additional information. - Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations - dtype=np.int): - /home/witiko/.virtualenvs/gensim4/lib/python3.7/site-packages/sklearn/linear_model/least_angle.py:30: DeprecationWarning: `np.float` is a deprecated alias for the builtin `float`. To silence this warning, use `float` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.float64` here. - Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations - method='lar', copy_X=True, eps=np.finfo(np.float).eps, - /home/witiko/.virtualenvs/gensim4/lib/python3.7/site-packages/sklearn/linear_model/least_angle.py:167: DeprecationWarning: `np.float` is a deprecated alias for the builtin `float`. To silence this warning, use `float` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.float64` here. - Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations - method='lar', copy_X=True, eps=np.finfo(np.float).eps, - /home/witiko/.virtualenvs/gensim4/lib/python3.7/site-packages/sklearn/linear_model/least_angle.py:284: DeprecationWarning: `np.float` is a deprecated alias for the builtin `float`. To silence this warning, use `float` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.float64` here. - Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations - eps=np.finfo(np.float).eps, copy_Gram=True, verbose=0, - /home/witiko/.virtualenvs/gensim4/lib/python3.7/site-packages/sklearn/linear_model/least_angle.py:862: DeprecationWarning: `np.float` is a deprecated alias for the builtin `float`. To silence this warning, use `float` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.float64` here. - Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations - eps=np.finfo(np.float).eps, copy_X=True, fit_path=True, - /home/witiko/.virtualenvs/gensim4/lib/python3.7/site-packages/sklearn/linear_model/least_angle.py:1101: DeprecationWarning: `np.float` is a deprecated alias for the builtin `float`. To silence this warning, use `float` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.float64` here. - Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations - eps=np.finfo(np.float).eps, copy_X=True, fit_path=True, - /home/witiko/.virtualenvs/gensim4/lib/python3.7/site-packages/sklearn/linear_model/least_angle.py:1127: DeprecationWarning: `np.float` is a deprecated alias for the builtin `float`. To silence this warning, use `float` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.float64` here. - Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations - eps=np.finfo(np.float).eps, positive=False): - /home/witiko/.virtualenvs/gensim4/lib/python3.7/site-packages/sklearn/linear_model/least_angle.py:1362: DeprecationWarning: `np.float` is a deprecated alias for the builtin `float`. To silence this warning, use `float` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.float64` here. - Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations - max_n_alphas=1000, n_jobs=None, eps=np.finfo(np.float).eps, - /home/witiko/.virtualenvs/gensim4/lib/python3.7/site-packages/sklearn/linear_model/least_angle.py:1602: DeprecationWarning: `np.float` is a deprecated alias for the builtin `float`. To silence this warning, use `float` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.float64` here. - Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations - max_n_alphas=1000, n_jobs=None, eps=np.finfo(np.float).eps, - /home/witiko/.virtualenvs/gensim4/lib/python3.7/site-packages/sklearn/linear_model/least_angle.py:1738: DeprecationWarning: `np.float` is a deprecated alias for the builtin `float`. To silence this warning, use `float` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.float64` here. - Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations - eps=np.finfo(np.float).eps, copy_X=True, positive=False): - [nltk_data] Downloading package stopwords to /home/witiko/nltk_data... + [nltk_data] Downloading package stopwords to /home/thomas/nltk_data... [nltk_data] Package stopwords is already up-to-date! + +.. GENERATED FROM PYTHON SOURCE LINES 95-101 + Now, as mentioned earlier, we will be using some downloaded pre-trained embeddings. We load these into a Gensim Word2Vec model class. @@ -173,6 +168,7 @@ embeddings. We load these into a Gensim Word2Vec model class. The embeddings we have chosen here require a lot of memory. +.. GENERATED FROM PYTHON SOURCE LINES 101-104 .. code-block:: default @@ -183,11 +179,22 @@ embeddings. We load these into a Gensim Word2Vec model class. +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + 2022-10-23 11:18:41,292 : INFO : loading projection weights from /home/thomas/gensim-data/word2vec-google-news-300/word2vec-google-news-300.gz + 2022-10-23 11:19:12,793 : INFO : KeyedVectors lifecycle event {'msg': 'loaded (3000000, 300) matrix of type float32 from /home/thomas/gensim-data/word2vec-google-news-300/word2vec-google-news-300.gz', 'binary': True, 'encoding': 'utf8', 'datetime': '2022-10-23T11:19:12.755440', 'gensim': '4.2.1.dev0', 'python': '3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0]', 'platform': 'Linux-5.19.0-76051900-generic-x86_64-with-glibc2.35', 'event': 'load_word2vec_format'} + + + + +.. GENERATED FROM PYTHON SOURCE LINES 105-107 So let's compute WMD using the ``wmdistance`` method. +.. GENERATED FROM PYTHON SOURCE LINES 107-110 .. code-block:: default @@ -200,17 +207,22 @@ So let's compute WMD using the ``wmdistance`` method. .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none + 2022-10-23 11:19:12,860 : INFO : adding document #0 to Dictionary<0 unique tokens: []> + 2022-10-23 11:19:12,861 : INFO : built Dictionary<8 unique tokens: ['illinois', 'media', 'obama', 'speaks', 'chicago']...> from 2 documents (total 8 corpus positions) + 2022-10-23 11:19:12,861 : INFO : Dictionary lifecycle event {'msg': "built Dictionary<8 unique tokens: ['illinois', 'media', 'obama', 'speaks', 'chicago']...> from 2 documents (total 8 corpus positions)", 'datetime': '2022-10-23T11:19:12.861331', 'gensim': '4.2.1.dev0', 'python': '3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0]', 'platform': 'Linux-5.19.0-76051900-generic-x86_64-with-glibc2.35', 'event': 'created'} distance = 1.0175 + +.. GENERATED FROM PYTHON SOURCE LINES 111-113 + Let's try the same thing with two completely unrelated sentences. Notice that the distance is larger. +.. GENERATED FROM PYTHON SOURCE LINES 113-117 .. code-block:: default @@ -224,50 +236,48 @@ Let's try the same thing with two completely unrelated sentences. Notice that th .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - distance = 1.3663 + 2022-10-23 11:19:15,303 : INFO : adding document #0 to Dictionary<0 unique tokens: []> + 2022-10-23 11:19:15,304 : INFO : built Dictionary<7 unique tokens: ['illinois', 'media', 'obama', 'speaks', 'favorite']...> from 2 documents (total 7 corpus positions) + 2022-10-23 11:19:15,304 : INFO : Dictionary lifecycle event {'msg': "built Dictionary<7 unique tokens: ['illinois', 'media', 'obama', 'speaks', 'favorite']...> from 2 documents (total 7 corpus positions)", 'datetime': '2022-10-23T11:19:15.304338', 'gensim': '4.2.1.dev0', 'python': '3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0]', 'platform': 'Linux-5.19.0-76051900-generic-x86_64-with-glibc2.35', 'event': 'created'} + distance = 1.3664 + +.. GENERATED FROM PYTHON SOURCE LINES 118-125 + References ---------- -1. Ofir Pele and Michael Werman, *A linear time histogram metric for improved SIFT matching*, 2008. -2. Ofir Pele and Michael Werman, *Fast and robust earth mover's distances*, 2009. -3. Matt Kusner et al. *From Embeddings To Document Distances*, 2015. -4. Tomáš Mikolov et al. *Efficient Estimation of Word Representations in Vector Space*, 2013. +1. Rémi Flamary et al. *POT: Python Optimal Transport*, 2021. +2. Matt Kusner et al. *From Embeddings To Document Distances*, 2015. +3. Tomáš Mikolov et al. *Efficient Estimation of Word Representations in Vector Space*, 2013. .. rst-class:: sphx-glr-timing - **Total running time of the script:** ( 0 minutes 55.983 seconds) + **Total running time of the script:** ( 0 minutes 36.418 seconds) -**Estimated memory usage:** 7537 MB +**Estimated memory usage:** 7551 MB .. _sphx_glr_download_auto_examples_tutorials_run_wmd.py: +.. only:: html -.. only :: html - - .. container:: sphx-glr-footer - :class: sphx-glr-footer-example - - - - .. container:: sphx-glr-download + .. container:: sphx-glr-footer sphx-glr-footer-example - :download:`Download Python source code: run_wmd.py ` + .. container:: sphx-glr-download sphx-glr-download-python + :download:`Download Python source code: run_wmd.py ` - .. container:: sphx-glr-download + .. container:: sphx-glr-download sphx-glr-download-jupyter - :download:`Download Jupyter notebook: run_wmd.ipynb ` + :download:`Download Jupyter notebook: run_wmd.ipynb ` .. only:: html diff --git a/docs/src/auto_examples/tutorials/run_wmd_codeobj.pickle b/docs/src/auto_examples/tutorials/run_wmd_codeobj.pickle new file mode 100644 index 0000000000000000000000000000000000000000..9df7517582979fe33a256db7c95ad592c142ed0c GIT binary patch literal 2602 zcmbtWOK;RL5Ju|0y4lipE2vZsa4ZLM;L52gLaL}JYNaA1(8`;*u~jFI;-tGhRO*F8 zQEmhwA#vf#l?(rlvEw|vk9OPcDQnO7eKX_nj4zwtpLQRpUurp=G3>;Y2O>+YgUH1Z zc3)E`&c0>O53}v`p};|m0|yWI5cw$koL#_bBS1dR(gvd<&eE38-Gssa&M=`Yrjo0d zuMGs@QJj%(+M#0LFeF5Fm0IwP$04H*jkBu+rCky8a4_PLD{=+W*5qWl#!4*GC%r4O zqA($gZR&%-2r%2=7Jxb@Nua{XCV`t8nA^295UkdH6o-t*j1KMaTz+N5nz@=Q_~S z%!p-*Yts0Z@K&w`Ss*X85X3CAy=bmq8vZpD>~5Kss%T7QRor;OuS``1yOZjwtVUIg zc2SaAeCq?0=sukOE|0=Q9AX3)MjL_DlYqsiwxjm!sM=K51d7V4?VD$K1986w)Wot# z@62Zc??Kd%j>$@SZzR7zJ3o)7!?00ECH`m;{OmN4<3+F&A(M}bfXeVb8P?6bEXZ>D zs2)*SHZq`rM&NXVanGZHXAhB}&Mps5sJBQxyN0G!M}u-(vq~+m)atl=6hJR>D}k1e zW5V63=EMy83JBa~*t{feQ{Pj)vAl@OJ7X;NeB@zCy;kd@G+(r4E#^HmZ^9-+w_=bn zjZ5iXk-xonZw{)smS`~DaW9OZ8VWIytjm&kwVG`a3Lp+O+~$s+FB*u_B}hhT+zwn6 zMd(~+p&2B82wxWYS{kpZmTr&y+SJhPWV1d6>J~tCkAZL_7FAc!(ya?e8VEoH2Th+w;@&DVJhw`t$CiZ`!Oxq0r literal 0 HcmV?d00001 diff --git a/docs/src/auto_examples/tutorials/sg_execution_times.rst b/docs/src/auto_examples/tutorials/sg_execution_times.rst index 0dfaf2783f..0b10d0ae69 100644 --- a/docs/src/auto_examples/tutorials/sg_execution_times.rst +++ b/docs/src/auto_examples/tutorials/sg_execution_times.rst @@ -5,22 +5,22 @@ Computation times ================= -**04:13.971** total execution time for **auto_examples_tutorials** files: +**00:36.418** total execution time for **auto_examples_tutorials** files: -+-------------------------------------------------------------------------------------+-----------+----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_lda.py` (``run_lda.py``) | 04:13.971 | 664.3 MB | -+-------------------------------------------------------------------------------------+-----------+----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_annoy.py` (``run_annoy.py``) | 00:00.000 | 0.0 MB | -+-------------------------------------------------------------------------------------+-----------+----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_doc2vec_lee.py` (``run_doc2vec_lee.py``) | 00:00.000 | 0.0 MB | -+-------------------------------------------------------------------------------------+-----------+----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_ensemblelda.py` (``run_ensemblelda.py``) | 00:00.000 | 0.0 MB | -+-------------------------------------------------------------------------------------+-----------+----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_fasttext.py` (``run_fasttext.py``) | 00:00.000 | 0.0 MB | -+-------------------------------------------------------------------------------------+-----------+----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_scm.py` (``run_scm.py``) | 00:00.000 | 0.0 MB | -+-------------------------------------------------------------------------------------+-----------+----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_wmd.py` (``run_wmd.py``) | 00:00.000 | 0.0 MB | -+-------------------------------------------------------------------------------------+-----------+----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_word2vec.py` (``run_word2vec.py``) | 00:00.000 | 0.0 MB | -+-------------------------------------------------------------------------------------+-----------+----------+ ++-------------------------------------------------------------------------------------+-----------+-----------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_wmd.py` (``run_wmd.py``) | 00:36.418 | 7551.3 MB | ++-------------------------------------------------------------------------------------+-----------+-----------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_annoy.py` (``run_annoy.py``) | 00:00.000 | 0.0 MB | ++-------------------------------------------------------------------------------------+-----------+-----------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_doc2vec_lee.py` (``run_doc2vec_lee.py``) | 00:00.000 | 0.0 MB | ++-------------------------------------------------------------------------------------+-----------+-----------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_ensemblelda.py` (``run_ensemblelda.py``) | 00:00.000 | 0.0 MB | ++-------------------------------------------------------------------------------------+-----------+-----------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_fasttext.py` (``run_fasttext.py``) | 00:00.000 | 0.0 MB | ++-------------------------------------------------------------------------------------+-----------+-----------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_lda.py` (``run_lda.py``) | 00:00.000 | 0.0 MB | ++-------------------------------------------------------------------------------------+-----------+-----------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_scm.py` (``run_scm.py``) | 00:00.000 | 0.0 MB | ++-------------------------------------------------------------------------------------+-----------+-----------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_word2vec.py` (``run_word2vec.py``) | 00:00.000 | 0.0 MB | ++-------------------------------------------------------------------------------------+-----------+-----------+ diff --git a/docs/src/gallery/tutorials/run_fasttext.py b/docs/src/gallery/tutorials/run_fasttext.py index 5a03b5d35e..ac3a1bc4c3 100644 --- a/docs/src/gallery/tutorials/run_fasttext.py +++ b/docs/src/gallery/tutorials/run_fasttext.py @@ -248,7 +248,7 @@ # Word Movers distance # ^^^^^^^^^^^^^^^^^^^^ # -# You'll need the optional ``pyemd`` library for this section, ``pip install pyemd``. +# You'll need the optional ``POT`` library for this section, ``pip install POT``. # # Let's start with two sentences: sentence_obama = 'Obama speaks to the media in Illinois'.lower().split() diff --git a/docs/src/gallery/tutorials/run_wmd.py b/docs/src/gallery/tutorials/run_wmd.py index 06e263063a..c037ef697b 100644 --- a/docs/src/gallery/tutorials/run_wmd.py +++ b/docs/src/gallery/tutorials/run_wmd.py @@ -53,7 +53,7 @@ # ``WmdSimilarity`` class for corpus based similarity queries. # # .. Important:: -# If you use Gensim's WMD functionality, please consider citing [1], [2] and [3]. +# If you use Gensim's WMD functionality, please consider citing [1] and [2]. # # Computing the Word Mover's Distance # ----------------------------------- @@ -118,8 +118,7 @@ def preprocess(sentence): # References # ---------- # -# 1. Ofir Pele and Michael Werman, *A linear time histogram metric for improved SIFT matching*, 2008. -# 2. Ofir Pele and Michael Werman, *Fast and robust earth mover's distances*, 2009. -# 3. Matt Kusner et al. *From Embeddings To Document Distances*, 2015. -# 4. Tomáš Mikolov et al. *Efficient Estimation of Word Representations in Vector Space*, 2013. +# 1. Rémi Flamary et al. *POT: Python Optimal Transport*, 2021. +# 2. Matt Kusner et al. *From Embeddings To Document Distances*, 2015. +# 3. Tomáš Mikolov et al. *Efficient Estimation of Word Representations in Vector Space*, 2013. # diff --git a/gensim/models/keyedvectors.py b/gensim/models/keyedvectors.py index 6b31496bb5..e8045bcd97 100644 --- a/gensim/models/keyedvectors.py +++ b/gensim/models/keyedvectors.py @@ -918,10 +918,8 @@ def wmdistance(self, document1, document2, norm=True): When using this code, please consider citing the following papers: - * `Ofir Pele and Michael Werman "A linear time histogram metric for improved SIFT matching" - `_ - * `Ofir Pele and Michael Werman "Fast and robust earth mover's distances" - `_ + * `Rémi Flamary et al. "POT: Python Optimal Transport" + `_ * `Matt Kusner et al. "From Word Embeddings To Document Distances" `_. @@ -942,7 +940,7 @@ def wmdistance(self, document1, document2, norm=True): Warnings -------- - This method only works if `pyemd `_ is installed. + This method only works if `POT `_ is installed. If one of the documents have no words that exist in the vocab, `float('inf')` (i.e. infinity) will be returned. @@ -950,12 +948,11 @@ def wmdistance(self, document1, document2, norm=True): Raises ------ ImportError - If `pyemd `_ isn't installed. + If `POT `_ isn't installed. """ - # If pyemd C extension is available, import it. - # If pyemd is attempted to be used, but isn't installed, ImportError will be raised in wmdistance - from pyemd import emd + # If POT is attempted to be used, but isn't installed, ImportError will be raised in wmdistance + from ot import emd2 # Remove out-of-vocabulary words. len_pre_oov1 = len(document1) @@ -1002,12 +999,12 @@ def nbow(document): d[idx] = freq / float(doc_len) # Normalized word frequencies. return d - # Compute nBOW representation of documents. This is what pyemd expects on input. + # Compute nBOW representation of documents. This is what POT expects on input. d1 = nbow(document1) d2 = nbow(document2) # Compute WMD. - return emd(d1, d2, distance_matrix) + return emd2(d1, d2, distance_matrix) def most_similar_cosmul( self, positive=None, negative=None, topn=10, restrict_vocab=None diff --git a/gensim/similarities/docsim.py b/gensim/similarities/docsim.py index a6d5e21bce..2e46479f87 100644 --- a/gensim/similarities/docsim.py +++ b/gensim/similarities/docsim.py @@ -1014,10 +1014,8 @@ class WmdSimilarity(interfaces.SimilarityABC): When using this code, please consider citing the following papers: - * `Ofir Pele and Michael Werman, "A linear time histogram metric for improved SIFT matching" - `_ - * `Ofir Pele and Michael Werman, "Fast and robust earth mover's distances" - `_ + * `Rémi Flamary et al. "POT: Python Optimal Transport" + `_ * `Matt Kusner et al. "From Word Embeddings To Document Distances" `_ diff --git a/gensim/test/test_fasttext.py b/gensim/test/test_fasttext.py index ecc44a30e4..64bd636b3a 100644 --- a/gensim/test/test_fasttext.py +++ b/gensim/test/test_fasttext.py @@ -30,10 +30,10 @@ import gensim.models.fasttext try: - from pyemd import emd # noqa:F401 - PYEMD_EXT = True + from ot import emd2 # noqa:F401 + POT_EXT = True except (ImportError, ValueError): - PYEMD_EXT = False + POT_EXT = False logger = logging.getLogger(__name__) @@ -394,7 +394,7 @@ def test_contains(self): self.assertFalse('nights' in self.test_model.wv.key_to_index) self.assertTrue('nights' in self.test_model.wv) - @unittest.skipIf(PYEMD_EXT is False, "pyemd not installed") + @unittest.skipIf(POT_EXT is False, "POT not installed") def test_wm_distance(self): doc = ['night', 'payment'] oov_doc = ['nights', 'forests', 'payments'] diff --git a/gensim/test/test_similarities.py b/gensim/test/test_similarities.py index 0b917980d2..a7fdfdf7bc 100644 --- a/gensim/test/test_similarities.py +++ b/gensim/test/test_similarities.py @@ -36,10 +36,10 @@ from gensim.similarities.fastss import editdist try: - from pyemd import emd # noqa:F401 - PYEMD_EXT = True + from ot import emd2 # noqa:F401 + POT_EXT = True except (ImportError, ValueError): - PYEMD_EXT = False + POT_EXT = False SENTENCES = [doc2vec.TaggedDocument(words, [i]) for i, words in enumerate(TEXTS)] @@ -88,8 +88,8 @@ def test_full(self, num_best=None, shardsize=100): index.destroy() def test_num_best(self): - if self.cls == similarities.WmdSimilarity and not PYEMD_EXT: - self.skipTest("pyemd not installed") + if self.cls == similarities.WmdSimilarity and not POT_EXT: + self.skipTest("POT not installed") for num_best in [None, 0, 1, 9, 1000]: self.testFull(num_best=num_best) @@ -119,8 +119,8 @@ def test_scipy2scipy_clipped(self): def test_empty_query(self): index = self.factoryMethod() - if isinstance(index, similarities.WmdSimilarity) and not PYEMD_EXT: - self.skipTest("pyemd not installed") + if isinstance(index, similarities.WmdSimilarity) and not POT_EXT: + self.skipTest("POT not installed") query = [] try: @@ -177,8 +177,8 @@ def test_iter(self): index.destroy() def test_persistency(self): - if self.cls == similarities.WmdSimilarity and not PYEMD_EXT: - self.skipTest("pyemd not installed") + if self.cls == similarities.WmdSimilarity and not POT_EXT: + self.skipTest("POT not installed") fname = get_tmpfile('gensim_similarities.tst.pkl') index = self.factoryMethod() @@ -197,8 +197,8 @@ def test_persistency(self): self.assertEqual(index.num_best, index2.num_best) def test_persistency_compressed(self): - if self.cls == similarities.WmdSimilarity and not PYEMD_EXT: - self.skipTest("pyemd not installed") + if self.cls == similarities.WmdSimilarity and not POT_EXT: + self.skipTest("POT not installed") fname = get_tmpfile('gensim_similarities.tst.pkl.gz') index = self.factoryMethod() @@ -217,8 +217,8 @@ def test_persistency_compressed(self): self.assertEqual(index.num_best, index2.num_best) def test_large(self): - if self.cls == similarities.WmdSimilarity and not PYEMD_EXT: - self.skipTest("pyemd not installed") + if self.cls == similarities.WmdSimilarity and not POT_EXT: + self.skipTest("POT not installed") fname = get_tmpfile('gensim_similarities.tst.pkl') index = self.factoryMethod() @@ -239,8 +239,8 @@ def test_large(self): self.assertEqual(index.num_best, index2.num_best) def test_large_compressed(self): - if self.cls == similarities.WmdSimilarity and not PYEMD_EXT: - self.skipTest("pyemd not installed") + if self.cls == similarities.WmdSimilarity and not POT_EXT: + self.skipTest("POT not installed") fname = get_tmpfile('gensim_similarities.tst.pkl.gz') index = self.factoryMethod() @@ -261,8 +261,8 @@ def test_large_compressed(self): self.assertEqual(index.num_best, index2.num_best) def test_mmap(self): - if self.cls == similarities.WmdSimilarity and not PYEMD_EXT: - self.skipTest("pyemd not installed") + if self.cls == similarities.WmdSimilarity and not POT_EXT: + self.skipTest("POT not installed") fname = get_tmpfile('gensim_similarities.tst.pkl') index = self.factoryMethod() @@ -284,8 +284,8 @@ def test_mmap(self): self.assertEqual(index.num_best, index2.num_best) def test_mmap_compressed(self): - if self.cls == similarities.WmdSimilarity and not PYEMD_EXT: - self.skipTest("pyemd not installed") + if self.cls == similarities.WmdSimilarity and not POT_EXT: + self.skipTest("POT not installed") fname = get_tmpfile('gensim_similarities.tst.pkl.gz') index = self.factoryMethod() @@ -310,7 +310,7 @@ def factoryMethod(self): # Override factoryMethod. return self.cls(TEXTS, self.w2v_model) - @unittest.skipIf(PYEMD_EXT is False, "pyemd not installed") + @unittest.skipIf(POT_EXT is False, "POT not installed") def test_full(self, num_best=None): # Override testFull. @@ -329,7 +329,7 @@ def test_full(self, num_best=None): self.assertTrue(numpy.alltrue(sims[1:] > 0.0)) self.assertTrue(numpy.alltrue(sims[1:] < 1.0)) - @unittest.skipIf(PYEMD_EXT is False, "pyemd not installed") + @unittest.skipIf(POT_EXT is False, "POT not installed") def test_non_increasing(self): ''' Check that similarities are non-increasing when `num_best` is not `None`.''' @@ -345,7 +345,7 @@ def test_non_increasing(self): cond = sum(numpy.diff(sims2) < 0) == len(sims2) - 1 self.assertTrue(cond) - @unittest.skipIf(PYEMD_EXT is False, "pyemd not installed") + @unittest.skipIf(POT_EXT is False, "POT not installed") def test_chunking(self): # Override testChunking. @@ -364,7 +364,7 @@ def test_chunking(self): self.assertTrue(numpy.alltrue(sim > 0.0)) self.assertTrue(numpy.alltrue(sim <= 1.0)) - @unittest.skipIf(PYEMD_EXT is False, "pyemd not installed") + @unittest.skipIf(POT_EXT is False, "POT not installed") def test_iter(self): # Override testIter. @@ -373,7 +373,7 @@ def test_iter(self): self.assertTrue(numpy.alltrue(sims >= 0.0)) self.assertTrue(numpy.alltrue(sims <= 1.0)) - @unittest.skipIf(PYEMD_EXT is False, "pyemd not installed") + @unittest.skipIf(POT_EXT is False, "POT not installed") def test_str(self): index = self.cls(TEXTS, self.w2v_model) self.assertTrue(str(index)) diff --git a/gensim/test/test_word2vec.py b/gensim/test/test_word2vec.py index 7e58275208..d1749e4bfa 100644 --- a/gensim/test/test_word2vec.py +++ b/gensim/test/test_word2vec.py @@ -21,10 +21,10 @@ from testfixtures import log_capture try: - from pyemd import emd # noqa:F401 - PYEMD_EXT = True + from ot import emd2 # noqa:F401 + POT_EXT = True except (ImportError, ValueError): - PYEMD_EXT = False + POT_EXT = False from gensim import utils from gensim.models import word2vec, keyedvectors @@ -1084,7 +1084,7 @@ def test_negative_ns_exp(self): class TestWMD(unittest.TestCase): - @unittest.skipIf(PYEMD_EXT is False, "pyemd not installed") + @unittest.skipIf(POT_EXT is False, "POT not installed") def test_nonzero(self): '''Test basic functionality with a test sentence.''' @@ -1096,7 +1096,7 @@ def test_nonzero(self): # Check that distance is non-zero. self.assertFalse(distance == 0.0) - @unittest.skipIf(PYEMD_EXT is False, "pyemd not installed") + @unittest.skipIf(POT_EXT is False, "POT not installed") def test_symmetry(self): '''Check that distance is symmetric.''' @@ -1107,7 +1107,7 @@ def test_symmetry(self): distance2 = model.wv.wmdistance(sentence2, sentence1) self.assertTrue(np.allclose(distance1, distance2)) - @unittest.skipIf(PYEMD_EXT is False, "pyemd not installed") + @unittest.skipIf(POT_EXT is False, "POT not installed") def test_identical_sentences(self): '''Check that the distance from a sentence to itself is zero.''' diff --git a/requirements_docs.txt b/requirements_docs.txt index 3e41db0927..dc8b44b173 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -5,7 +5,7 @@ memory-profiler==0.55.0 nltk==3.4.5 nmslib==2.1.1 pandas==1.2.3 -pyemd==0.5.1 +POT==0.8.1 scikit-learn==0.24.1 sphinx-gallery==0.8.2 sphinxcontrib-napoleon==0.7 diff --git a/setup.py b/setup.py index de9e0bfb36..4c87887f3c 100644 --- a/setup.py +++ b/setup.py @@ -274,7 +274,7 @@ def run(self): if not (sys.platform.lower().startswith("win") and sys.version_info[:2] >= (3, 9)): core_testenv.extend([ - 'pyemd', + 'POT', 'nmslib', ]) From c93eb0b647ec9de7cfcb43c74c37f295f2944821 Mon Sep 17 00:00:00 2001 From: Tal Ifargan <65530510+TalIfargan@users.noreply.github.com> Date: Thu, 3 Nov 2022 15:49:57 +0200 Subject: [PATCH 17/34] Fixed bug in loss computation for Word2Vec with hierarchical softmax (#3397) * fixed loss computation for sg, hs * fixed loss computation for cbow, hs --- gensim/models/word2vec_inner.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gensim/models/word2vec_inner.pyx b/gensim/models/word2vec_inner.pyx index ffdc908b5c..3692e3e5b8 100755 --- a/gensim/models/word2vec_inner.pyx +++ b/gensim/models/word2vec_inner.pyx @@ -126,7 +126,7 @@ cdef void w2v_fast_sentence_sg_hs( if _compute_loss == 1: sgn = (-1)**word_code[b] # ch function: 0-> 1, 1 -> -1 - lprob = -1*sgn*f_dot + lprob = sgn*f_dot if lprob <= -MAX_EXP or lprob >= MAX_EXP: continue lprob = LOG_TABLE[((lprob + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))] @@ -326,7 +326,7 @@ cdef void w2v_fast_sentence_cbow_hs( if _compute_loss == 1: sgn = (-1)**word_code[b] # ch function: 0-> 1, 1 -> -1 - lprob = -1*sgn*f_dot + lprob = sgn*f_dot if lprob <= -MAX_EXP or lprob >= MAX_EXP: continue lprob = LOG_TABLE[((lprob + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))] From b17e6af9bb40f0bb5dfcf083f6253905336ecbd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Martinovi=C4=87?= <53435298+martino-vic@users.noreply.github.com> Date: Sat, 3 Dec 2022 14:49:18 +0100 Subject: [PATCH 18/34] fix deprecation warning from pytest (#3354) gensim/matutils.py:22: DeprecationWarning: Please use `triu` from the `scipy.linalg` namespace, the `scipy.linalg.special_matrices` namespace is deprecated. from scipy.linalg.special_matrices import triu --- gensim/matutils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gensim/matutils.py b/gensim/matutils.py index 4d4064acc0..fdd1a6b592 100644 --- a/gensim/matutils.py +++ b/gensim/matutils.py @@ -17,9 +17,8 @@ import numpy as np import scipy.sparse from scipy.stats import entropy -import scipy.linalg +from scipy.linalg import get_blas_funcs, triu from scipy.linalg.lapack import get_lapack_funcs -from scipy.linalg.special_matrices import triu from scipy.special import psi # gamma function utils @@ -42,7 +41,7 @@ def blas(name, ndarray): BLAS function for the needed operation on the given data type. """ - return scipy.linalg.get_blas_funcs((name,), (ndarray,))[0] + return get_blas_funcs((name,), (ndarray,))[0] def argsort(x, topn=None, reverse=False): From b6ea788bce74ad958476c2638e2b728641ec19fb Mon Sep 17 00:00:00 2001 From: Paul Wise Date: Sat, 3 Dec 2022 22:11:22 +0800 Subject: [PATCH 19/34] Switch to Cython language level 3 (#3344) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Switch to Cython language level 3 Python 2 is not supported by gensim, so switching to language level 3 should be fine. Silences a warning from Cython: FutureWarning: Cython directive 'language_level' not set, using 2 for now (Py2). This will change in a later release! * fix cythonization with language_level=3 Co-authored-by: Radim Řehůřek --- gensim/_matutils.pyx | 1 + gensim/corpora/_mmreader.pyx | 1 + gensim/models/doc2vec_corpusfile.pyx | 1 + gensim/models/doc2vec_inner.pxd | 3 ++- gensim/models/doc2vec_inner.pyx | 3 ++- gensim/models/fasttext_corpusfile.pyx | 1 + gensim/models/fasttext_inner.pxd | 3 ++- gensim/models/fasttext_inner.pyx | 3 ++- gensim/models/nmf_pgd.pyx | 1 + gensim/models/word2vec_corpusfile.pxd | 1 + gensim/models/word2vec_corpusfile.pyx | 1 + gensim/models/word2vec_inner.pxd | 1 + gensim/models/word2vec_inner.pyx | 8 ++----- setup.py | 33 ++++++++++++++------------- 14 files changed, 35 insertions(+), 26 deletions(-) diff --git a/gensim/_matutils.pyx b/gensim/_matutils.pyx index 6c79020832..0162202224 100644 --- a/gensim/_matutils.pyx +++ b/gensim/_matutils.pyx @@ -1,6 +1,7 @@ #!/usr/bin/env cython # coding: utf-8 # cython: embedsignature=True +# cython: language_level=3 from __future__ import division cimport cython diff --git a/gensim/corpora/_mmreader.pyx b/gensim/corpora/_mmreader.pyx index 3c32797de8..16edc070c8 100644 --- a/gensim/corpora/_mmreader.pyx +++ b/gensim/corpora/_mmreader.pyx @@ -1,5 +1,6 @@ # Copyright (C) 2018 Radim Rehurek # cython: embedsignature=True +# cython: language_level=3 """Reader for corpus in the Matrix Market format.""" import logging diff --git a/gensim/models/doc2vec_corpusfile.pyx b/gensim/models/doc2vec_corpusfile.pyx index 9216d13bd4..50a07fc3ab 100644 --- a/gensim/models/doc2vec_corpusfile.pyx +++ b/gensim/models/doc2vec_corpusfile.pyx @@ -1,4 +1,5 @@ #!/usr/bin/env cython +# cython: language_level=3 # cython: boundscheck=False # cython: wraparound=False # cython: cdivision=True diff --git a/gensim/models/doc2vec_inner.pxd b/gensim/models/doc2vec_inner.pxd index 77da86f449..41635b47a0 100644 --- a/gensim/models/doc2vec_inner.pxd +++ b/gensim/models/doc2vec_inner.pxd @@ -1,5 +1,6 @@ #!/usr/bin/env cython # distutils: language = c++ +# cython: language_level=3 # cython: boundscheck=False # cython: wraparound=False # cython: cdivision=True @@ -15,7 +16,7 @@ import numpy as np cimport numpy as np -from word2vec_inner cimport REAL_t +from gensim.models.word2vec_inner cimport REAL_t DEF MAX_DOCUMENT_LEN = 10000 diff --git a/gensim/models/doc2vec_inner.pyx b/gensim/models/doc2vec_inner.pyx index 1657c59787..804a3ac28d 100644 --- a/gensim/models/doc2vec_inner.pyx +++ b/gensim/models/doc2vec_inner.pyx @@ -1,4 +1,5 @@ #!/usr/bin/env cython +# cython: language_level=3 # cython: boundscheck=False # cython: wraparound=False # cython: cdivision=True @@ -24,7 +25,7 @@ except ImportError: # in scipy > 0.15, fblas function has been removed import scipy.linalg.blas as fblas -from word2vec_inner cimport bisect_left, random_int32, sscal, REAL_t, EXP_TABLE, our_dot, our_saxpy +from gensim.models.word2vec_inner cimport bisect_left, random_int32, sscal, REAL_t, EXP_TABLE, our_dot, our_saxpy DEF MAX_DOCUMENT_LEN = 10000 diff --git a/gensim/models/fasttext_corpusfile.pyx b/gensim/models/fasttext_corpusfile.pyx index 5d275b42b6..2b5344e2d5 100644 --- a/gensim/models/fasttext_corpusfile.pyx +++ b/gensim/models/fasttext_corpusfile.pyx @@ -1,5 +1,6 @@ #!/usr/bin/env cython # distutils: language = c++ +# cython: language_level=3 # cython: boundscheck=False # cython: wraparound=False # cython: cdivision=True diff --git a/gensim/models/fasttext_inner.pxd b/gensim/models/fasttext_inner.pxd index 31a1b1d35f..af7a531116 100644 --- a/gensim/models/fasttext_inner.pxd +++ b/gensim/models/fasttext_inner.pxd @@ -1,4 +1,5 @@ #!/usr/bin/env cython +# cython: language_level=3 # cython: boundscheck=False # cython: wraparound=False # cython: cdivision=True @@ -13,7 +14,7 @@ import numpy as np cimport numpy as np -from word2vec_inner cimport REAL_t +from gensim.models.word2vec_inner cimport REAL_t DEF MAX_SENTENCE_LEN = 10000 diff --git a/gensim/models/fasttext_inner.pyx b/gensim/models/fasttext_inner.pyx index e27bd62feb..6e246b3579 100644 --- a/gensim/models/fasttext_inner.pyx +++ b/gensim/models/fasttext_inner.pyx @@ -1,4 +1,5 @@ #!/usr/bin/env cython +# cython: language_level=3 # cython: boundscheck=False # cython: wraparound=False # cython: cdivision=True @@ -55,7 +56,7 @@ from libc.string cimport memset # # The versions are as chosen in word2vec_inner.pyx, and aliased to `our_` functions -from word2vec_inner cimport bisect_left, random_int32, scopy, sscal, \ +from gensim.models.word2vec_inner cimport bisect_left, random_int32, scopy, sscal, \ REAL_t, our_dot, our_saxpy DEF MAX_SENTENCE_LEN = 10000 diff --git a/gensim/models/nmf_pgd.pyx b/gensim/models/nmf_pgd.pyx index dff480cdb4..2419272e5b 100644 --- a/gensim/models/nmf_pgd.pyx +++ b/gensim/models/nmf_pgd.pyx @@ -1,5 +1,6 @@ # Author: Timofey Yefimov +# cython: language_level=3 # cython: cdivision=True # cython: boundscheck=False # cython: wraparound=False diff --git a/gensim/models/word2vec_corpusfile.pxd b/gensim/models/word2vec_corpusfile.pxd index 7030686916..56e8cb64ee 100644 --- a/gensim/models/word2vec_corpusfile.pxd +++ b/gensim/models/word2vec_corpusfile.pxd @@ -1,4 +1,5 @@ # distutils: language = c++ +# cython: language_level=3 # cython: boundscheck=False # cython: wraparound=False # cython: cdivision=True diff --git a/gensim/models/word2vec_corpusfile.pyx b/gensim/models/word2vec_corpusfile.pyx index 5d7f5004e4..a2b962aed6 100644 --- a/gensim/models/word2vec_corpusfile.pyx +++ b/gensim/models/word2vec_corpusfile.pyx @@ -1,5 +1,6 @@ #!/usr/bin/env cython # distutils: language = c++ +# cython: language_level=3 # cython: boundscheck=False # cython: wraparound=False # cython: cdivision=True diff --git a/gensim/models/word2vec_inner.pxd b/gensim/models/word2vec_inner.pxd index 82abad2f05..4b4523dc55 100644 --- a/gensim/models/word2vec_inner.pxd +++ b/gensim/models/word2vec_inner.pxd @@ -1,3 +1,4 @@ +# cython: language_level=3 # cython: boundscheck=False # cython: wraparound=False # cython: cdivision=True diff --git a/gensim/models/word2vec_inner.pyx b/gensim/models/word2vec_inner.pyx index 3692e3e5b8..1c0807ee0f 100755 --- a/gensim/models/word2vec_inner.pyx +++ b/gensim/models/word2vec_inner.pyx @@ -1,4 +1,5 @@ #!/usr/bin/env cython +# cython: language_level=3 # cython: boundscheck=False # cython: wraparound=False # cython: cdivision=True @@ -19,12 +20,7 @@ from libc.math cimport exp from libc.math cimport log from libc.string cimport memset -# scipy <= 0.15 -try: - from scipy.linalg.blas import fblas -except ImportError: - # in scipy > 0.15, fblas function has been removed - import scipy.linalg.blas as fblas +import scipy.linalg.blas as fblas REAL = np.float32 diff --git a/setup.py b/setup.py index 4c87887f3c..6d35665dc8 100644 --- a/setup.py +++ b/setup.py @@ -15,25 +15,26 @@ import platform import shutil import sys +from collections import OrderedDict from setuptools import Extension, find_packages, setup, distutils from setuptools.command.build_ext import build_ext -c_extensions = { - 'gensim.models.word2vec_inner': 'gensim/models/word2vec_inner.c', - 'gensim.corpora._mmreader': 'gensim/corpora/_mmreader.c', - 'gensim.models.fasttext_inner': 'gensim/models/fasttext_inner.c', - 'gensim._matutils': 'gensim/_matutils.c', - 'gensim.models.nmf_pgd': 'gensim/models/nmf_pgd.c', - 'gensim.similarities.fastss': 'gensim/similarities/fastss.c', -} +c_extensions = OrderedDict([ + ('gensim.models.word2vec_inner', 'gensim/models/word2vec_inner.c'), + ('gensim.corpora._mmreader', 'gensim/corpora/_mmreader.c'), + ('gensim.models.fasttext_inner', 'gensim/models/fasttext_inner.c'), + ('gensim._matutils', 'gensim/_matutils.c'), + ('gensim.models.nmf_pgd', 'gensim/models/nmf_pgd.c'), + ('gensim.similarities.fastss', 'gensim/similarities/fastss.c'), +]) -cpp_extensions = { - 'gensim.models.doc2vec_inner': 'gensim/models/doc2vec_inner.cpp', - 'gensim.models.word2vec_corpusfile': 'gensim/models/word2vec_corpusfile.cpp', - 'gensim.models.fasttext_corpusfile': 'gensim/models/fasttext_corpusfile.cpp', - 'gensim.models.doc2vec_corpusfile': 'gensim/models/doc2vec_corpusfile.cpp', -} +cpp_extensions = OrderedDict([ + ('gensim.models.doc2vec_inner', 'gensim/models/doc2vec_inner.cpp'), + ('gensim.models.word2vec_corpusfile', 'gensim/models/word2vec_corpusfile.cpp'), + ('gensim.models.fasttext_corpusfile', 'gensim/models/fasttext_corpusfile.cpp'), + ('gensim.models.doc2vec_corpusfile', 'gensim/models/doc2vec_corpusfile.cpp'), +]) def need_cython(): @@ -111,8 +112,8 @@ def finalize_options(self): if need_cython(): import Cython.Build - Cython.Build.cythonize(list(make_c_ext(use_cython=True))) - Cython.Build.cythonize(list(make_cpp_ext(use_cython=True))) + Cython.Build.cythonize(list(make_c_ext(use_cython=True)), language_level=3) + Cython.Build.cythonize(list(make_cpp_ext(use_cython=True)), language_level=3) class CleanExt(distutils.cmd.Command): From 3331b824d2e3f7a65f5df7903382a0c0a30dcc61 Mon Sep 17 00:00:00 2001 From: Jayme Gordon Date: Sat, 3 Dec 2022 08:34:57 -0700 Subject: [PATCH 20/34] Implement numpy hack in setup.py to enable install under Poetry (#3363) * Closes #3362: Install issue poetry * get rid of redundant exception handling this code can never raise an exception, so we shouldn't be expecting them Co-authored-by: Michael Penkov --- setup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 6d35665dc8..aeef499c8f 100644 --- a/setup.py +++ b/setup.py @@ -99,13 +99,14 @@ class CustomBuildExt(build_ext): importing them at module level, because they may not be available yet. """ # + # Prevent numpy from thinking it is still in its setup process # http://stackoverflow.com/questions/19919905/how-to-bootstrap-numpy-installation-in-setup-py # def finalize_options(self): build_ext.finalize_options(self) - # Prevent numpy from thinking it is still in its setup process: - # https://docs.python.org/2/library/__builtin__.html#module-__builtin__ - __builtins__.__NUMPY_SETUP__ = False + + import builtins + builtins.__NUMPY_SETUP__ = False import numpy self.include_dirs.append(numpy.get_include()) From cc70a6c31d6f01b9293999607129d349ed45a6b3 Mon Sep 17 00:00:00 2001 From: Aswin Shailajan <72661784+aswin2108@users.noreply.github.com> Date: Tue, 6 Dec 2022 05:37:26 +0530 Subject: [PATCH 21/34] Fixed the broken link in readme.md (#3409) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f1cb9f3ddd..b4bd542f65 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ Adopters | [Tailwind](https://www.tailwindapp.com/) | ![tailwind](docs/src/readme_images/tailwind.png) | Media | Post interesting and relevant content to Pinterest. | | [Issuu](https://issuu.com/) | ![issuu](docs/src/readme_images/issuu.png) | Media | Gensim's LDA module lies at the very core of the analysis we perform on each uploaded publication to figure out what it's all about. | | [Search Metrics](http://www.searchmetrics.com/) | ![search-metrics](docs/src/readme_images/search-metrics.png) | Content Marketing | Gensim word2vec used for entity disambiguation in Search Engine Optimisation. | -| [12K Research](https://12k.co/) | ![12k](docs/src/readme_images/12k.png)| Media | Document similarity analysis on media articles. | +| [12K Research](https://12k.com/) | ![12k](docs/src/readme_images/12k.png)| Media | Document similarity analysis on media articles. | | [Stillwater Supercomputing](http://www.stillwater-sc.com/) | ![stillwater](docs/src/readme_images/stillwater.png) | Hardware | Document comprehension and association with word2vec. | | [SiteGround](https://www.siteground.com/) | ![siteground](docs/src/readme_images/siteground.png) | Web hosting | An ensemble search engine which uses different embeddings models and similarities, including word2vec, WMD, and LDA. | | [Capital One](https://www.capitalone.com/) | ![capitalone](docs/src/readme_images/capitalone.png) | Finance | Topic modeling for customer complaints exploration. | From 3f536fbc394bab8345ca72ac57a9c4a5614eb71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Primo=C5=BE=20Godec?= Date: Tue, 6 Dec 2022 08:38:16 +0100 Subject: [PATCH 22/34] Coherence Model - work on documents without tokens (#3406) --- gensim/test/test_coherencemodel.py | 6 ++++++ gensim/topic_coherence/text_analysis.py | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/gensim/test/test_coherencemodel.py b/gensim/test/test_coherencemodel.py index 2b111f7306..9927851a93 100644 --- a/gensim/test/test_coherencemodel.py +++ b/gensim/test/test_coherencemodel.py @@ -305,6 +305,12 @@ def testCompareCoherenceForModels(self): self.assertAlmostEqual(np.mean(coherence_topics2), coherence2, 4) self.assertAlmostEqual(coherence1, coherence2, places=4) + def testEmptyList(self): + """Test if CoherenceModel works with document without tokens""" + texts = self.texts + [[]] + cm = CoherenceModel(model=self.ldamodel, texts=texts, coherence="c_v", processes=1) + cm.get_coherence() + if __name__ == '__main__': logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.DEBUG) diff --git a/gensim/topic_coherence/text_analysis.py b/gensim/topic_coherence/text_analysis.py index 2c06185a0b..58bdc2c35f 100644 --- a/gensim/topic_coherence/text_analysis.py +++ b/gensim/topic_coherence/text_analysis.py @@ -293,7 +293,8 @@ def accumulate(self, texts, window_size): relevant_texts, window_size, ignore_below_size=False, include_doc_num=True) for doc_num, virtual_document in windows: - self.analyze_text(virtual_document, doc_num) + if len(virtual_document) > 0: + self.analyze_text(virtual_document, doc_num) self.num_docs += 1 return self From a9b9714294ba61486849af940ea7b3465d66d0cc Mon Sep 17 00:00:00 2001 From: "Samsul Rahmadani (munggok)" <56231298+acul3@users.noreply.github.com> Date: Tue, 6 Dec 2022 20:24:42 +0800 Subject: [PATCH 23/34] Add support for Python 3.11 and drop support for Python 3.7 (#3402) * add support for python 3.11 * change to oldest version numpy and scipy that support 3.11 * add build for python3.11 * disable nmslib test for python 3.11 * fix formatting * fix formatting * change build for 3.8 * drop Py3.7 support and builds Co-authored-by: Michael Penkov --- .github/workflows/build-wheels.yml | 48 ++++++++++++++---------------- .github/workflows/tests.yml | 21 +++++-------- .gitmodules | 2 +- .travis.yml | 12 +++++--- multibuild | 2 +- setup.py | 16 +++++----- 6 files changed, 49 insertions(+), 52 deletions(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index a9f0fd7ef2..cf8016e9ab 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -15,10 +15,10 @@ jobs: linters: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} @@ -48,7 +48,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10', '3.11'] os: [ubuntu-latest, macos-latest, windows-latest] platform: [x64] include: @@ -74,11 +74,6 @@ jobs: # https://github.com/scipy/oldest-supported-numpy/blob/master/setup.cfg # with the exception that we enforce the minimum version to be 1.17.0. # - - os: ubuntu-latest - manylinux-version: 2010 - python-version: 3.7 - build-depends: numpy==1.17.0 - - os: ubuntu-latest manylinux-version: 2010 python-version: 3.8 @@ -93,12 +88,11 @@ jobs: manylinux-version: 2014 python-version: "3.10" build-depends: numpy==1.22.2 scipy==1.8.0 - - - os: macos-latest - travis-os-name: osx - manylinux-version: 1 - python-version: 3.7 - build-depends: numpy==1.17.0 + + - os: ubuntu-latest + manylinux-version: 2014 + python-version: "3.11" + build-depends: numpy==1.23.2 scipy==1.9.2 - os: macos-latest travis-os-name: osx @@ -117,11 +111,12 @@ jobs: manylinux-version: 1 python-version: "3.10" build-depends: numpy==1.22.2 scipy==1.8.0 - - - os: windows-latest - manylinux-version: 2010 - python-version: 3.7 - build-depends: numpy==1.17.0 + + - os: macos-latest + travis-os-name: osx + manylinux-version: 1 + python-version: "3.11" + build-depends: numpy==1.23.2 scipy==1.9.2 - os: windows-latest manylinux-version: 2010 @@ -137,6 +132,11 @@ jobs: manylinux-version: 2010 python-version: "3.10" build-depends: numpy==1.22.2 scipy==1.8.0 + + - os: windows-latest + manylinux-version: 2010 + python-version: "3.11" + build-depends: numpy==1.23.2 scipy==1.9.2 env: PKG_NAME: gensim @@ -155,7 +155,7 @@ jobs: BUILD_DEPENDS: ${{ matrix.build-depends }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: submodules: recursive fetch-depth: 0 @@ -168,7 +168,7 @@ jobs: echo "TRAVIS_OS_NAME: ${TRAVIS_OS_NAME}" echo "SKIP_NETWORK_TESTS: ${SKIP_NETWORK_TESTS}" - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -202,19 +202,15 @@ jobs: echo ::group::Set up dependencies python --version python -c "import struct; print(struct.calcsize('P') * 8)" - python -m pip install -U pip setuptools wheel wheelhouse_uploader ${{ env.BUILD_DEPENDS }} echo ::endgroup:: - echo ::group::Build wheel python setup.py bdist_wheel echo ::endgroup - echo ::group::Install run ls dist python continuous_integration/install_wheel.py echo ::endgroup:: - # # For consistency with the multibuild step. # @@ -240,6 +236,7 @@ jobs: if: matrix.os != 'windows-latest' run: | . test_environment/bin/activate + python -m pip install --upgrade pip pip install pytest testfixtures mock pip install wheelhouse/*.whl cd test_environment @@ -258,6 +255,7 @@ jobs: if: matrix.os == 'windows-latest' run: | test_environment/Scripts/activate.bat + python -m pip install --upgrade pip pip install pytest testfixtures mock pip install wheelhouse/*.whl cd test_environment diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3cb54fe8be..9d52759538 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,10 +9,10 @@ jobs: linters: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} @@ -47,9 +47,9 @@ jobs: needs: [linters] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: # # We use Py3.8 here for historical reasons. @@ -65,7 +65,6 @@ jobs: sudo apt-get -yq remove texlive-binaries --purge sudo apt-get -yq --no-install-suggests --no-install-recommends --force-yes install dvipng texlive-latex-base texlive-latex-extra texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended latexmk sudo apt-get -yq install build-essential python3.8-dev - - name: Install gensim and its dependencies run: pip install -e .[docs] @@ -73,7 +72,6 @@ jobs: run: | python setup.py build_ext --inplace make -C docs/src clean html - # # FIXME: do we want to store the built documentation somewhere, or is # knowing that the docs built successfully enough? @@ -90,15 +88,15 @@ jobs: fail-fast: false matrix: include: - - {python: 3.7, os: ubuntu-20.04} - {python: 3.8, os: ubuntu-20.04} - {python: 3.9, os: ubuntu-20.04} - {python: '3.10', os: ubuntu-20.04} + - {python: '3.11', os: ubuntu-20.04} - - {python: 3.7, os: windows-2019} - {python: 3.8, os: windows-2019} - {python: 3.9, os: windows-2019} - {python: '3.10', os: windows-2019} + - {python: '3.11', os: windows-2019} # # Don't run this job unless the linters have succeeded. @@ -108,9 +106,9 @@ jobs: needs: [linters] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - name: Update pip @@ -129,14 +127,12 @@ jobs: curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x2EE0EA64E40A89B84B2DF73499E82A75642AC823" | sudo apt-key add sudo apt-get update -y sudo apt-get install -y sbt - - name: Install GDB & enable core dumps if: matrix.os == 'ubuntu-20.04' run: | sudo apt-get update -y sudo apt-get install -y gdb ulimit -c unlimited -S # enable core dumps - - name: Install gensim and its dependencies if: matrix.os != 'windows' run: pip install -e .[test] @@ -150,7 +146,6 @@ jobs: python --version pip --version python setup.py build_ext --inplace - # # Some of our tests are hanging, and I strongly suspect it's because of the coverage plugin. # diff --git a/.gitmodules b/.gitmodules index 347fe93043..52a1b1716c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "multibuild"] path = multibuild - url = https://github.com/matthew-brett/multibuild.git + url = https://github.com/multi-build/multibuild diff --git a/.travis.yml b/.travis.yml index d27d176f58..8937c0c74c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,10 +33,6 @@ matrix: # See .github/workflows/build-wheels.yml for a discussion of why we # handle numpy versions explicitly. # - - os: linux - env: - - MB_PYTHON_VERSION=3.7 - - BUILD_DEPENDS="numpy==1.19.2 scipy==1.7.0" - os: linux env: - MB_PYTHON_VERSION=3.8 @@ -50,6 +46,14 @@ matrix: # this numpy release are available via PyPI. # - BUILD_DEPENDS="numpy==1.19.3 scipy==1.7.0" + - os: linux + env: + - MB_PYTHON_VERSION=3.10 + - BUILD_DEPENDS="numpy==1.19.3 scipy==1.7.0" + - os: linux + env: + - MB_PYTHON_VERSION=3.11 + - BUILD_DEPENDS="numpy==1.19.3 scipy==1.7.0" before_install: - source multibuild/common_utils.sh diff --git a/multibuild b/multibuild index 4e30a05764..d4b02ce8c7 160000 --- a/multibuild +++ b/multibuild @@ -1 +1 @@ -Subproject commit 4e30a057646d9e70d5e6e7c354e85cfb740d2ca1 +Subproject commit d4b02ce8c707c8a2bcc721e29f2385149f994b1f diff --git a/setup.py b/setup.py index aeef499c8f..485cf08294 100644 --- a/setup.py +++ b/setup.py @@ -274,7 +274,7 @@ def run(self): 'testfixtures', ] -if not (sys.platform.lower().startswith("win") and sys.version_info[:2] >= (3, 9)): +if not sys.platform.lower().startswith("win") and sys.version_info[:2] < (3, 11): core_testenv.extend([ 'POT', 'nmslib', @@ -282,7 +282,6 @@ def run(self): # Add additional requirements for testing on Linux that are skipped on Windows. linux_testenv = core_testenv[:] + visdom_req - # Skip problematic/uninstallable packages (& thus related conditional tests) in Windows builds. # We still test them in Linux via Travis, see linux_testenv above. # See https://github.com/RaRe-Technologies/gensim/pull/2814 @@ -320,20 +319,20 @@ def run(self): 'pandas', ] -NUMPY_STR = 'numpy >= 1.17.0' +NUMPY_STR = 'numpy >= 1.18.5' # # We pin the Cython version for reproducibility. We expect our extensions # to build with any sane version of Cython, so we should update this pin # periodically. # -CYTHON_STR = 'Cython==0.29.28' +CYTHON_STR = 'Cython==0.29.32' # Allow overriding the Cython version requirement CYTHON_STR = os.environ.get('GENSIM_CYTHON_REQUIRES', CYTHON_STR) install_requires = [ NUMPY_STR, - 'scipy >= 0.18.1', + 'scipy >= 1.7.0', 'smart_open >= 1.8.1', ] @@ -378,9 +377,10 @@ def run(self): 'Environment :: Console', 'Intended Audience :: Science/Research', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3 :: Only', 'Topic :: Scientific/Engineering :: Artificial Intelligence', 'Topic :: Scientific/Engineering :: Information Analysis', @@ -388,7 +388,7 @@ def run(self): ], test_suite="gensim.test", - python_requires='>=3.6', + python_requires='>=3.8', setup_requires=setup_requires, install_requires=install_requires, tests_require=linux_testenv, From 985a3bd06e68ed649016c791df2763f5c5a62ad1 Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Tue, 6 Dec 2022 23:13:29 +0900 Subject: [PATCH 24/34] fix pip invokation Seems to consistently fail for MacOS Py3.11 --- .github/workflows/build-wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index cf8016e9ab..cd0548fe83 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -274,6 +274,6 @@ jobs: # if: ${{ always() && env.WHEELHOUSE_UPLOADER_USERNAME && env.WHEELHOUSE_UPLOADER_SECRET }} run: | - pip install wheelhouse-uploader + python -m pip install wheelhouse-uploader ls wheelhouse/*.whl python -m wheelhouse_uploader upload --local-folder wheelhouse/ --no-ssl-check gensim-wheels --provider S3 --no-enable-cdn From 68fdbf9cd66895e200f618b3bd2a6629608971df Mon Sep 17 00:00:00 2001 From: Gordon Mohr Date: Tue, 6 Dec 2022 22:50:22 -0400 Subject: [PATCH 25/34] clarify runtime expectations (#3381) * clarify runtime expectations * update doc files made stale by this PR Co-authored-by: Michael Penkov --- .../tutorials/run_doc2vec_lee.ipynb | 18 +- .../tutorials/run_doc2vec_lee.py | 12 +- .../tutorials/run_doc2vec_lee.py.md5 | 2 +- .../tutorials/run_doc2vec_lee.rst | 398 +++++++----------- .../tutorials/sg_execution_times.rst | 35 +- docs/src/gallery/tutorials/run_doc2vec_lee.py | 12 +- 6 files changed, 206 insertions(+), 271 deletions(-) diff --git a/docs/src/auto_examples/tutorials/run_doc2vec_lee.ipynb b/docs/src/auto_examples/tutorials/run_doc2vec_lee.ipynb index 5314bea335..a886f2f526 100644 --- a/docs/src/auto_examples/tutorials/run_doc2vec_lee.ipynb +++ b/docs/src/auto_examples/tutorials/run_doc2vec_lee.ipynb @@ -15,7 +15,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "\nDoc2Vec Model\n=============\n\nIntroduces Gensim's Doc2Vec model and demonstrates its use on the\n`Lee Corpus `__.\n\n\n" + "\n# Doc2Vec Model\n\nIntroduces Gensim's Doc2Vec model and demonstrates its use on the\n[Lee Corpus](https://hekyll.services.adelaide.edu.au/dspace/bitstream/2440/28910/1/hdl_28910.pdf)_.\n" ] }, { @@ -33,7 +33,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Doc2Vec is a `core_concepts_model` that represents each\n`core_concepts_document` as a `core_concepts_vector`. This\ntutorial introduces the model and demonstrates how to train and assess it.\n\nHere's a list of what we'll be doing:\n\n0. Review the relevant models: bag-of-words, Word2Vec, Doc2Vec\n1. Load and preprocess the training and test corpora (see `core_concepts_corpus`)\n2. Train a Doc2Vec `core_concepts_model` model using the training corpus\n3. Demonstrate how the trained model can be used to infer a `core_concepts_vector`\n4. Assess the model\n5. Test the model on the test corpus\n\nReview: Bag-of-words\n--------------------\n\n.. Note:: Feel free to skip these review sections if you're already familiar with the models.\n\nYou may be familiar with the `bag-of-words model\n`_ from the\n`core_concepts_vector` section.\nThis model transforms each document to a fixed-length vector of integers.\nFor example, given the sentences:\n\n- ``John likes to watch movies. Mary likes movies too.``\n- ``John also likes to watch football games. Mary hates football.``\n\nThe model outputs the vectors:\n\n- ``[1, 2, 1, 1, 2, 1, 1, 0, 0, 0, 0]``\n- ``[1, 1, 1, 1, 0, 1, 0, 1, 2, 1, 1]``\n\nEach vector has 10 elements, where each element counts the number of times a\nparticular word occurred in the document.\nThe order of elements is arbitrary.\nIn the example above, the order of the elements corresponds to the words:\n``[\"John\", \"likes\", \"to\", \"watch\", \"movies\", \"Mary\", \"too\", \"also\", \"football\", \"games\", \"hates\"]``.\n\nBag-of-words models are surprisingly effective, but have several weaknesses.\n\nFirst, they lose all information about word order: \"John likes Mary\" and\n\"Mary likes John\" correspond to identical vectors. There is a solution: bag\nof `n-grams `__\nmodels consider word phrases of length n to represent documents as\nfixed-length vectors to capture local word order but suffer from data\nsparsity and high dimensionality.\n\nSecond, the model does not attempt to learn the meaning of the underlying\nwords, and as a consequence, the distance between vectors doesn't always\nreflect the difference in meaning. The ``Word2Vec`` model addresses this\nsecond problem.\n\nReview: ``Word2Vec`` Model\n--------------------------\n\n``Word2Vec`` is a more recent model that embeds words in a lower-dimensional\nvector space using a shallow neural network. The result is a set of\nword-vectors where vectors close together in vector space have similar\nmeanings based on context, and word-vectors distant to each other have\ndiffering meanings. For example, ``strong`` and ``powerful`` would be close\ntogether and ``strong`` and ``Paris`` would be relatively far.\n\nGensim's :py:class:`~gensim.models.word2vec.Word2Vec` class implements this model.\n\nWith the ``Word2Vec`` model, we can calculate the vectors for each **word** in a document.\nBut what if we want to calculate a vector for the **entire document**\\ ?\nWe could average the vectors for each word in the document - while this is quick and crude, it can often be useful.\nHowever, there is a better way...\n\nIntroducing: Paragraph Vector\n-----------------------------\n\n.. Important:: In Gensim, we refer to the Paragraph Vector model as ``Doc2Vec``.\n\nLe and Mikolov in 2014 introduced the `Doc2Vec algorithm `__,\nwhich usually outperforms such simple-averaging of ``Word2Vec`` vectors.\n\nThe basic idea is: act as if a document has another floating word-like\nvector, which contributes to all training predictions, and is updated like\nother word-vectors, but we will call it a doc-vector. Gensim's\n:py:class:`~gensim.models.doc2vec.Doc2Vec` class implements this algorithm.\n\nThere are two implementations:\n\n1. Paragraph Vector - Distributed Memory (PV-DM)\n2. Paragraph Vector - Distributed Bag of Words (PV-DBOW)\n\n.. Important::\n Don't let the implementation details below scare you.\n They're advanced material: if it's too much, then move on to the next section.\n\nPV-DM is analogous to Word2Vec CBOW. The doc-vectors are obtained by training\na neural network on the synthetic task of predicting a center word based an\naverage of both context word-vectors and the full document's doc-vector.\n\nPV-DBOW is analogous to Word2Vec SG. The doc-vectors are obtained by training\na neural network on the synthetic task of predicting a target word just from\nthe full document's doc-vector. (It is also common to combine this with\nskip-gram testing, using both the doc-vector and nearby word-vectors to\npredict a single target word, but only one at a time.)\n\nPrepare the Training and Test Data\n----------------------------------\n\nFor this tutorial, we'll be training our model using the `Lee Background\nCorpus\n`_\nincluded in gensim. This corpus contains 314 documents selected from the\nAustralian Broadcasting Corporation\u2019s news mail service, which provides text\ne-mails of headline stories and covers a number of broad topics.\n\nAnd we'll test our model by eye using the much shorter `Lee Corpus\n`_\nwhich contains 50 documents.\n\n\n" + "Doc2Vec is a `core_concepts_model` that represents each\n`core_concepts_document` as a `core_concepts_vector`. This\ntutorial introduces the model and demonstrates how to train and assess it.\n\nHere's a list of what we'll be doing:\n\n0. Review the relevant models: bag-of-words, Word2Vec, Doc2Vec\n1. Load and preprocess the training and test corpora (see `core_concepts_corpus`)\n2. Train a Doc2Vec `core_concepts_model` model using the training corpus\n3. Demonstrate how the trained model can be used to infer a `core_concepts_vector`\n4. Assess the model\n5. Test the model on the test corpus\n\n## Review: Bag-of-words\n\n.. Note:: Feel free to skip these review sections if you're already familiar with the models.\n\nYou may be familiar with the [bag-of-words model](https://en.wikipedia.org/wiki/Bag-of-words_model) from the\n`core_concepts_vector` section.\nThis model transforms each document to a fixed-length vector of integers.\nFor example, given the sentences:\n\n- ``John likes to watch movies. Mary likes movies too.``\n- ``John also likes to watch football games. Mary hates football.``\n\nThe model outputs the vectors:\n\n- ``[1, 2, 1, 1, 2, 1, 1, 0, 0, 0, 0]``\n- ``[1, 1, 1, 1, 0, 1, 0, 1, 2, 1, 1]``\n\nEach vector has 10 elements, where each element counts the number of times a\nparticular word occurred in the document.\nThe order of elements is arbitrary.\nIn the example above, the order of the elements corresponds to the words:\n``[\"John\", \"likes\", \"to\", \"watch\", \"movies\", \"Mary\", \"too\", \"also\", \"football\", \"games\", \"hates\"]``.\n\nBag-of-words models are surprisingly effective, but have several weaknesses.\n\nFirst, they lose all information about word order: \"John likes Mary\" and\n\"Mary likes John\" correspond to identical vectors. There is a solution: bag\nof [n-grams](https://en.wikipedia.org/wiki/N-gram)_\nmodels consider word phrases of length n to represent documents as\nfixed-length vectors to capture local word order but suffer from data\nsparsity and high dimensionality.\n\nSecond, the model does not attempt to learn the meaning of the underlying\nwords, and as a consequence, the distance between vectors doesn't always\nreflect the difference in meaning. The ``Word2Vec`` model addresses this\nsecond problem.\n\n## Review: ``Word2Vec`` Model\n\n``Word2Vec`` is a more recent model that embeds words in a lower-dimensional\nvector space using a shallow neural network. The result is a set of\nword-vectors where vectors close together in vector space have similar\nmeanings based on context, and word-vectors distant to each other have\ndiffering meanings. For example, ``strong`` and ``powerful`` would be close\ntogether and ``strong`` and ``Paris`` would be relatively far.\n\nGensim's :py:class:`~gensim.models.word2vec.Word2Vec` class implements this model.\n\nWith the ``Word2Vec`` model, we can calculate the vectors for each **word** in a document.\nBut what if we want to calculate a vector for the **entire document**\\ ?\nWe could average the vectors for each word in the document - while this is quick and crude, it can often be useful.\nHowever, there is a better way...\n\n## Introducing: Paragraph Vector\n\n.. Important:: In Gensim, we refer to the Paragraph Vector model as ``Doc2Vec``.\n\nLe and Mikolov in 2014 introduced the [Doc2Vec algorithm](https://cs.stanford.edu/~quocle/paragraph_vector.pdf)_,\nwhich usually outperforms such simple-averaging of ``Word2Vec`` vectors.\n\nThe basic idea is: act as if a document has another floating word-like\nvector, which contributes to all training predictions, and is updated like\nother word-vectors, but we will call it a doc-vector. Gensim's\n:py:class:`~gensim.models.doc2vec.Doc2Vec` class implements this algorithm.\n\nThere are two implementations:\n\n1. Paragraph Vector - Distributed Memory (PV-DM)\n2. Paragraph Vector - Distributed Bag of Words (PV-DBOW)\n\n.. Important::\n Don't let the implementation details below scare you.\n They're advanced material: if it's too much, then move on to the next section.\n\nPV-DM is analogous to Word2Vec CBOW. The doc-vectors are obtained by training\na neural network on the synthetic task of predicting a center word based an\naverage of both context word-vectors and the full document's doc-vector.\n\nPV-DBOW is analogous to Word2Vec SG. The doc-vectors are obtained by training\na neural network on the synthetic task of predicting a target word just from\nthe full document's doc-vector. (It is also common to combine this with\nskip-gram testing, using both the doc-vector and nearby word-vectors to\npredict a single target word, but only one at a time.)\n\n## Prepare the Training and Test Data\n\nFor this tutorial, we'll be training our model using the [Lee Background\nCorpus](https://hekyll.services.adelaide.edu.au/dspace/bitstream/2440/28910/1/hdl_28910.pdf)\nincluded in gensim. This corpus contains 314 documents selected from the\nAustralian Broadcasting Corporation\u2019s news mail service, which provides text\ne-mails of headline stories and covers a number of broad topics.\n\nAnd we'll test our model by eye using the much shorter [Lee Corpus](https://hekyll.services.adelaide.edu.au/dspace/bitstream/2440/28910/1/hdl_28910.pdf)\nwhich contains 50 documents.\n\n\n" ] }, { @@ -51,7 +51,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Define a Function to Read and Preprocess Text\n---------------------------------------------\n\nBelow, we define a function to:\n\n- open the train/test file (with latin encoding)\n- read the file line-by-line\n- pre-process each line (tokenize text into individual words, remove punctuation, set to lowercase, etc)\n\nThe file we're reading is a **corpus**.\nEach line of the file is a **document**.\n\n.. Important::\n To train the model, we'll need to associate a tag/number with each document\n of the training corpus. In our case, the tag is simply the zero-based line\n number.\n\n\n" + "## Define a Function to Read and Preprocess Text\n\nBelow, we define a function to:\n\n- open the train/test file (with latin encoding)\n- read the file line-by-line\n- pre-process each line (tokenize text into individual words, remove punctuation, set to lowercase, etc)\n\nThe file we're reading is a **corpus**.\nEach line of the file is a **document**.\n\n.. Important::\n To train the model, we'll need to associate a tag/number with each document\n of the training corpus. In our case, the tag is simply the zero-based line\n number.\n\n\n" ] }, { @@ -112,7 +112,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Training the Model\n------------------\n\nNow, we'll instantiate a Doc2Vec model with a vector size with 50 dimensions and\niterating over the training corpus 40 times. We set the minimum word count to\n2 in order to discard words with very few occurrences. (Without a variety of\nrepresentative examples, retaining such infrequent words can often make a\nmodel worse!) Typical iteration counts in the published `Paragraph Vector paper `__\nresults, using 10s-of-thousands to millions of docs, are 10-20. More\niterations take more time and eventually reach a point of diminishing\nreturns.\n\nHowever, this is a very very small dataset (300 documents) with shortish\ndocuments (a few hundred words). Adding training passes can sometimes help\nwith such small datasets.\n\n\n" + "## Training the Model\n\nNow, we'll instantiate a Doc2Vec model with a vector size with 50 dimensions and\niterating over the training corpus 40 times. We set the minimum word count to\n2 in order to discard words with very few occurrences. (Without a variety of\nrepresentative examples, retaining such infrequent words can often make a\nmodel worse!) Typical iteration counts in the published [Paragraph Vector paper](https://cs.stanford.edu/~quocle/paragraph_vector.pdf)_\nresults, using 10s-of-thousands to millions of docs, are 10-20. More\niterations take more time and eventually reach a point of diminishing\nreturns.\n\nHowever, this is a very very small dataset (300 documents) with shortish\ndocuments (a few hundred words). Adding training passes can sometimes help\nwith such small datasets.\n\n\n" ] }, { @@ -166,7 +166,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Next, train the model on the corpus.\nIf optimized Gensim (with BLAS library) is being used, this should take no more than 3 seconds.\nIf the BLAS library is not being used, this should take no more than 2\nminutes, so use optimized Gensim with BLAS if you value your time.\n\n\n" + "Next, train the model on the corpus.\nIn the usual case, where Gensim installation found a BLAS library for optimized\nbulk vector operations, this training on this tiny 300 document, ~60k word corpus \nshould take just a few seconds. (More realistic datasets of tens-of-millions\nof words or more take proportionately longer.) If for some reason a BLAS library \nisn't available, training uses a fallback approach that takes 60x-120x longer, \nso even this tiny training will take minutes rather than seconds. (And, in that \ncase, you should also notice a warning in the logging letting you know there's \nsomething worth fixing.) So, be sure your installation uses the BLAS-optimized \nGensim if you value your time.\n\n\n" ] }, { @@ -209,7 +209,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Assessing the Model\n-------------------\n\nTo assess our new model, we'll first infer new vectors for each document of\nthe training corpus, compare the inferred vectors with the training corpus,\nand then returning the rank of the document based on self-similarity.\nBasically, we're pretending as if the training corpus is some new unseen data\nand then seeing how they compare with the trained model. The expectation is\nthat we've likely overfit our model (i.e., all of the ranks will be less than\n2) and so we should be able to find similar documents very easily.\nAdditionally, we'll keep track of the second ranks for a comparison of less\nsimilar documents.\n\n\n" + "## Assessing the Model\n\nTo assess our new model, we'll first infer new vectors for each document of\nthe training corpus, compare the inferred vectors with the training corpus,\nand then returning the rank of the document based on self-similarity.\nBasically, we're pretending as if the training corpus is some new unseen data\nand then seeing how they compare with the trained model. The expectation is\nthat we've likely overfit our model (i.e., all of the ranks will be less than\n2) and so we should be able to find similar documents very easily.\nAdditionally, we'll keep track of the second ranks for a comparison of less\nsimilar documents.\n\n\n" ] }, { @@ -281,7 +281,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Testing the Model\n-----------------\n\nUsing the same approach above, we'll infer the vector for a randomly chosen\ntest document, and compare the document to our model by eye.\n\n\n" + "## Testing the Model\n\nUsing the same approach above, we'll infer the vector for a randomly chosen\ntest document, and compare the document to our model by eye.\n\n\n" ] }, { @@ -299,7 +299,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Conclusion\n----------\n\nLet's review what we've seen in this tutorial:\n\n0. Review the relevant models: bag-of-words, Word2Vec, Doc2Vec\n1. Load and preprocess the training and test corpora (see `core_concepts_corpus`)\n2. Train a Doc2Vec `core_concepts_model` model using the training corpus\n3. Demonstrate how the trained model can be used to infer a `core_concepts_vector`\n4. Assess the model\n5. Test the model on the test corpus\n\nThat's it! Doc2Vec is a great way to explore relationships between documents.\n\nAdditional Resources\n--------------------\n\nIf you'd like to know more about the subject matter of this tutorial, check out the links below.\n\n* `Word2Vec Paper `_\n* `Doc2Vec Paper `_\n* `Dr. Michael D. Lee's Website `_\n* `Lee Corpus `__\n* `IMDB Doc2Vec Tutorial `_\n\n\n" + "## Conclusion\n\nLet's review what we've seen in this tutorial:\n\n0. Review the relevant models: bag-of-words, Word2Vec, Doc2Vec\n1. Load and preprocess the training and test corpora (see `core_concepts_corpus`)\n2. Train a Doc2Vec `core_concepts_model` model using the training corpus\n3. Demonstrate how the trained model can be used to infer a `core_concepts_vector`\n4. Assess the model\n5. Test the model on the test corpus\n\nThat's it! Doc2Vec is a great way to explore relationships between documents.\n\n## Additional Resources\n\nIf you'd like to know more about the subject matter of this tutorial, check out the links below.\n\n* [Word2Vec Paper](https://papers.nips.cc/paper/5021-distributed-representations-of-words-and-phrases-and-their-compositionality.pdf)\n* [Doc2Vec Paper](https://cs.stanford.edu/~quocle/paragraph_vector.pdf)\n* [Dr. Michael D. Lee's Website](http://faculty.sites.uci.edu/mdlee)\n* [Lee Corpus](http://faculty.sites.uci.edu/mdlee/similarity-data/)_\n* [IMDB Doc2Vec Tutorial](doc2vec-IMDB.ipynb)\n\n\n" ] } ], @@ -319,7 +319,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.8.10" } }, "nbformat": 4, diff --git a/docs/src/auto_examples/tutorials/run_doc2vec_lee.py b/docs/src/auto_examples/tutorials/run_doc2vec_lee.py index 7012d38f66..18f4ee7b16 100644 --- a/docs/src/auto_examples/tutorials/run_doc2vec_lee.py +++ b/docs/src/auto_examples/tutorials/run_doc2vec_lee.py @@ -215,9 +215,15 @@ def read_corpus(fname, tokens_only=False): ############################################################################### # Next, train the model on the corpus. -# If optimized Gensim (with BLAS library) is being used, this should take no more than 3 seconds. -# If the BLAS library is not being used, this should take no more than 2 -# minutes, so use optimized Gensim with BLAS if you value your time. +# In the usual case, where Gensim installation found a BLAS library for optimized +# bulk vector operations, this training on this tiny 300 document, ~60k word corpus +# should take just a few seconds. (More realistic datasets of tens-of-millions +# of words or more take proportionately longer.) If for some reason a BLAS library +# isn't available, training uses a fallback approach that takes 60x-120x longer, +# so even this tiny training will take minutes rather than seconds. (And, in that +# case, you should also notice a warning in the logging letting you know there's +# something worth fixing.) So, be sure your installation uses the BLAS-optimized +# Gensim if you value your time. # model.train(train_corpus, total_examples=model.corpus_count, epochs=model.epochs) diff --git a/docs/src/auto_examples/tutorials/run_doc2vec_lee.py.md5 b/docs/src/auto_examples/tutorials/run_doc2vec_lee.py.md5 index f1b58e756c..5c0d021557 100644 --- a/docs/src/auto_examples/tutorials/run_doc2vec_lee.py.md5 +++ b/docs/src/auto_examples/tutorials/run_doc2vec_lee.py.md5 @@ -1 +1 @@ -7d0ee86f6eb9d1e2f55b9f295eec3060 \ No newline at end of file +581caa67e8496a210a030c2886fb8bbc \ No newline at end of file diff --git a/docs/src/auto_examples/tutorials/run_doc2vec_lee.rst b/docs/src/auto_examples/tutorials/run_doc2vec_lee.rst index 6e99a47a13..68a6fc7d3f 100644 --- a/docs/src/auto_examples/tutorials/run_doc2vec_lee.rst +++ b/docs/src/auto_examples/tutorials/run_doc2vec_lee.rst @@ -1,12 +1,21 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples/tutorials/run_doc2vec_lee.py" +.. LINE NUMBERS ARE GIVEN BELOW. + .. only:: html .. note:: :class: sphx-glr-download-link-note - Click :ref:`here ` to download the full example code - .. rst-class:: sphx-glr-example-title + Click :ref:`here ` + to download the full example code + +.. rst-class:: sphx-glr-example-title - .. _sphx_glr_auto_examples_tutorials_run_doc2vec_lee.py: +.. _sphx_glr_auto_examples_tutorials_run_doc2vec_lee.py: Doc2Vec Model @@ -15,7 +24,7 @@ Doc2Vec Model Introduces Gensim's Doc2Vec model and demonstrates its use on the `Lee Corpus `__. - +.. GENERATED FROM PYTHON SOURCE LINES 9-13 .. code-block:: default @@ -30,6 +39,8 @@ Introduces Gensim's Doc2Vec model and demonstrates its use on the +.. GENERATED FROM PYTHON SOURCE LINES 14-129 + Doc2Vec is a :ref:`core_concepts_model` that represents each :ref:`core_concepts_document` as a :ref:`core_concepts_vector`. This tutorial introduces the model and demonstrates how to train and assess it. @@ -146,6 +157,7 @@ And we'll test our model by eye using the much shorter `Lee Corpus which contains 50 documents. +.. GENERATED FROM PYTHON SOURCE LINES 129-137 .. code-block:: default @@ -164,6 +176,8 @@ which contains 50 documents. +.. GENERATED FROM PYTHON SOURCE LINES 138-155 + Define a Function to Read and Preprocess Text --------------------------------------------- @@ -182,6 +196,7 @@ Each line of the file is a **document**. number. +.. GENERATED FROM PYTHON SOURCE LINES 155-170 .. code-block:: default @@ -207,9 +222,12 @@ Each line of the file is a **document**. +.. GENERATED FROM PYTHON SOURCE LINES 171-173 + Let's take a look at the training corpus +.. GENERATED FROM PYTHON SOURCE LINES 173-175 .. code-block:: default @@ -221,8 +239,6 @@ Let's take a look at the training corpus .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none [TaggedDocument(words=['hundreds', 'of', 'people', 'have', 'been', 'forced', 'to', 'vacate', 'their', 'homes', 'in', 'the', 'southern', 'highlands', 'of', 'new', 'south', 'wales', 'as', 'strong', 'winds', 'today', 'pushed', 'huge', 'bushfire', 'towards', 'the', 'town', 'of', 'hill', 'top', 'new', 'blaze', 'near', 'goulburn', 'south', 'west', 'of', 'sydney', 'has', 'forced', 'the', 'closure', 'of', 'the', 'hume', 'highway', 'at', 'about', 'pm', 'aedt', 'marked', 'deterioration', 'in', 'the', 'weather', 'as', 'storm', 'cell', 'moved', 'east', 'across', 'the', 'blue', 'mountains', 'forced', 'authorities', 'to', 'make', 'decision', 'to', 'evacuate', 'people', 'from', 'homes', 'in', 'outlying', 'streets', 'at', 'hill', 'top', 'in', 'the', 'new', 'south', 'wales', 'southern', 'highlands', 'an', 'estimated', 'residents', 'have', 'left', 'their', 'homes', 'for', 'nearby', 'mittagong', 'the', 'new', 'south', 'wales', 'rural', 'fire', 'service', 'says', 'the', 'weather', 'conditions', 'which', 'caused', 'the', 'fire', 'to', 'burn', 'in', 'finger', 'formation', 'have', 'now', 'eased', 'and', 'about', 'fire', 'units', 'in', 'and', 'around', 'hill', 'top', 'are', 'optimistic', 'of', 'defending', 'all', 'properties', 'as', 'more', 'than', 'blazes', 'burn', 'on', 'new', 'year', 'eve', 'in', 'new', 'south', 'wales', 'fire', 'crews', 'have', 'been', 'called', 'to', 'new', 'fire', 'at', 'gunning', 'south', 'of', 'goulburn', 'while', 'few', 'details', 'are', 'available', 'at', 'this', 'stage', 'fire', 'authorities', 'says', 'it', 'has', 'closed', 'the', 'hume', 'highway', 'in', 'both', 'directions', 'meanwhile', 'new', 'fire', 'in', 'sydney', 'west', 'is', 'no', 'longer', 'threatening', 'properties', 'in', 'the', 'cranebrook', 'area', 'rain', 'has', 'fallen', 'in', 'some', 'parts', 'of', 'the', 'illawarra', 'sydney', 'the', 'hunter', 'valley', 'and', 'the', 'north', 'coast', 'but', 'the', 'bureau', 'of', 'meteorology', 'claire', 'richards', 'says', 'the', 'rain', 'has', 'done', 'little', 'to', 'ease', 'any', 'of', 'the', 'hundred', 'fires', 'still', 'burning', 'across', 'the', 'state', 'the', 'falls', 'have', 'been', 'quite', 'isolated', 'in', 'those', 'areas', 'and', 'generally', 'the', 'falls', 'have', 'been', 'less', 'than', 'about', 'five', 'millimetres', 'she', 'said', 'in', 'some', 'places', 'really', 'not', 'significant', 'at', 'all', 'less', 'than', 'millimetre', 'so', 'there', 'hasn', 'been', 'much', 'relief', 'as', 'far', 'as', 'rain', 'is', 'concerned', 'in', 'fact', 'they', 've', 'probably', 'hampered', 'the', 'efforts', 'of', 'the', 'firefighters', 'more', 'because', 'of', 'the', 'wind', 'gusts', 'that', 'are', 'associated', 'with', 'those', 'thunderstorms'], tags=[0]), TaggedDocument(words=['indian', 'security', 'forces', 'have', 'shot', 'dead', 'eight', 'suspected', 'militants', 'in', 'night', 'long', 'encounter', 'in', 'southern', 'kashmir', 'the', 'shootout', 'took', 'place', 'at', 'dora', 'village', 'some', 'kilometers', 'south', 'of', 'the', 'kashmiri', 'summer', 'capital', 'srinagar', 'the', 'deaths', 'came', 'as', 'pakistani', 'police', 'arrested', 'more', 'than', 'two', 'dozen', 'militants', 'from', 'extremist', 'groups', 'accused', 'of', 'staging', 'an', 'attack', 'on', 'india', 'parliament', 'india', 'has', 'accused', 'pakistan', 'based', 'lashkar', 'taiba', 'and', 'jaish', 'mohammad', 'of', 'carrying', 'out', 'the', 'attack', 'on', 'december', 'at', 'the', 'behest', 'of', 'pakistani', 'military', 'intelligence', 'military', 'tensions', 'have', 'soared', 'since', 'the', 'raid', 'with', 'both', 'sides', 'massing', 'troops', 'along', 'their', 'border', 'and', 'trading', 'tit', 'for', 'tat', 'diplomatic', 'sanctions', 'yesterday', 'pakistan', 'announced', 'it', 'had', 'arrested', 'lashkar', 'taiba', 'chief', 'hafiz', 'mohammed', 'saeed', 'police', 'in', 'karachi', 'say', 'it', 'is', 'likely', 'more', 'raids', 'will', 'be', 'launched', 'against', 'the', 'two', 'groups', 'as', 'well', 'as', 'other', 'militant', 'organisations', 'accused', 'of', 'targetting', 'india', 'military', 'tensions', 'between', 'india', 'and', 'pakistan', 'have', 'escalated', 'to', 'level', 'not', 'seen', 'since', 'their', 'war'], tags=[1])] @@ -230,9 +246,12 @@ Let's take a look at the training corpus +.. GENERATED FROM PYTHON SOURCE LINES 176-178 + And the testing corpus looks like this: +.. GENERATED FROM PYTHON SOURCE LINES 178-180 .. code-block:: default @@ -244,8 +263,6 @@ And the testing corpus looks like this: .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none [['the', 'national', 'executive', 'of', 'the', 'strife', 'torn', 'democrats', 'last', 'night', 'appointed', 'little', 'known', 'west', 'australian', 'senator', 'brian', 'greig', 'as', 'interim', 'leader', 'shock', 'move', 'likely', 'to', 'provoke', 'further', 'conflict', 'between', 'the', 'party', 'senators', 'and', 'its', 'organisation', 'in', 'move', 'to', 'reassert', 'control', 'over', 'the', 'party', 'seven', 'senators', 'the', 'national', 'executive', 'last', 'night', 'rejected', 'aden', 'ridgeway', 'bid', 'to', 'become', 'interim', 'leader', 'in', 'favour', 'of', 'senator', 'greig', 'supporter', 'of', 'deposed', 'leader', 'natasha', 'stott', 'despoja', 'and', 'an', 'outspoken', 'gay', 'rights', 'activist'], ['cash', 'strapped', 'financial', 'services', 'group', 'amp', 'has', 'shelved', 'million', 'plan', 'to', 'buy', 'shares', 'back', 'from', 'investors', 'and', 'will', 'raise', 'million', 'in', 'fresh', 'capital', 'after', 'profits', 'crashed', 'in', 'the', 'six', 'months', 'to', 'june', 'chief', 'executive', 'paul', 'batchelor', 'said', 'the', 'result', 'was', 'solid', 'in', 'what', 'he', 'described', 'as', 'the', 'worst', 'conditions', 'for', 'stock', 'markets', 'in', 'years', 'amp', 'half', 'year', 'profit', 'sank', 'per', 'cent', 'to', 'million', 'or', 'share', 'as', 'australia', 'largest', 'investor', 'and', 'fund', 'manager', 'failed', 'to', 'hit', 'projected', 'per', 'cent', 'earnings', 'growth', 'targets', 'and', 'was', 'battered', 'by', 'falling', 'returns', 'on', 'share', 'markets']] @@ -253,10 +270,14 @@ And the testing corpus looks like this: +.. GENERATED FROM PYTHON SOURCE LINES 181-184 + Notice that the testing corpus is just a list of lists and does not contain any tags. +.. GENERATED FROM PYTHON SOURCE LINES 186-202 + Training the Model ------------------ @@ -274,6 +295,7 @@ documents (a few hundred words). Adding training passes can sometimes help with such small datasets. +.. GENERATED FROM PYTHON SOURCE LINES 202-204 .. code-block:: default @@ -283,11 +305,20 @@ with such small datasets. +.. rst-class:: sphx-glr-script-out + + .. code-block:: none + + 2022-12-07 10:59:00,578 : INFO : Doc2Vec lifecycle event {'params': 'Doc2Vec', 'datetime': '2022-12-07T10:59:00.540082', 'gensim': '4.2.1.dev0', 'python': '3.8.10 (default, Jun 22 2022, 20:18:18) \n[GCC 9.4.0]', 'platform': 'Linux-5.4.0-135-generic-x86_64-with-glibc2.29', 'event': 'created'} + +.. GENERATED FROM PYTHON SOURCE LINES 205-206 + Build a vocabulary +.. GENERATED FROM PYTHON SOURCE LINES 206-208 .. code-block:: default @@ -299,24 +330,24 @@ Build a vocabulary .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - 2020-09-30 21:08:55,026 : INFO : collecting all words and their counts - 2020-09-30 21:08:55,027 : INFO : PROGRESS: at example #0, processed 0 words (0/s), 0 word types, 0 tags - 2020-09-30 21:08:55,043 : INFO : collected 6981 word types and 300 unique tags from a corpus of 300 examples and 58152 words - 2020-09-30 21:08:55,043 : INFO : Loading a fresh vocabulary - 2020-09-30 21:08:55,064 : INFO : effective_min_count=2 retains 3955 unique words (56% of original 6981, drops 3026) - 2020-09-30 21:08:55,064 : INFO : effective_min_count=2 leaves 55126 word corpus (94% of original 58152, drops 3026) - 2020-09-30 21:08:55,098 : INFO : deleting the raw counts dictionary of 6981 items - 2020-09-30 21:08:55,100 : INFO : sample=0.001 downsamples 46 most-common words - 2020-09-30 21:08:55,100 : INFO : downsampling leaves estimated 42390 word corpus (76.9% of prior 55126) - 2020-09-30 21:08:55,149 : INFO : estimated required memory for 3955 words and 50 dimensions: 3679500 bytes - 2020-09-30 21:08:55,149 : INFO : resetting layer weights + 2022-12-07 10:59:00,806 : INFO : collecting all words and their counts + 2022-12-07 10:59:00,808 : INFO : PROGRESS: at example #0, processed 0 words (0 words/s), 0 word types, 0 tags + 2022-12-07 10:59:00,850 : INFO : collected 6981 word types and 300 unique tags from a corpus of 300 examples and 58152 words + 2022-12-07 10:59:00,850 : INFO : Creating a fresh vocabulary + 2022-12-07 10:59:00,887 : INFO : Doc2Vec lifecycle event {'msg': 'effective_min_count=2 retains 3955 unique words (56.65% of original 6981, drops 3026)', 'datetime': '2022-12-07T10:59:00.886953', 'gensim': '4.2.1.dev0', 'python': '3.8.10 (default, Jun 22 2022, 20:18:18) \n[GCC 9.4.0]', 'platform': 'Linux-5.4.0-135-generic-x86_64-with-glibc2.29', 'event': 'prepare_vocab'} + 2022-12-07 10:59:00,887 : INFO : Doc2Vec lifecycle event {'msg': 'effective_min_count=2 leaves 55126 word corpus (94.80% of original 58152, drops 3026)', 'datetime': '2022-12-07T10:59:00.887466', 'gensim': '4.2.1.dev0', 'python': '3.8.10 (default, Jun 22 2022, 20:18:18) \n[GCC 9.4.0]', 'platform': 'Linux-5.4.0-135-generic-x86_64-with-glibc2.29', 'event': 'prepare_vocab'} + 2022-12-07 10:59:00,917 : INFO : deleting the raw counts dictionary of 6981 items + 2022-12-07 10:59:00,918 : INFO : sample=0.001 downsamples 46 most-common words + 2022-12-07 10:59:00,918 : INFO : Doc2Vec lifecycle event {'msg': 'downsampling leaves estimated 42390.98914085061 word corpus (76.9%% of prior 55126)', 'datetime': '2022-12-07T10:59:00.918276', 'gensim': '4.2.1.dev0', 'python': '3.8.10 (default, Jun 22 2022, 20:18:18) \n[GCC 9.4.0]', 'platform': 'Linux-5.4.0-135-generic-x86_64-with-glibc2.29', 'event': 'prepare_vocab'} + 2022-12-07 10:59:00,965 : INFO : estimated required memory for 3955 words and 50 dimensions: 3679500 bytes + 2022-12-07 10:59:00,965 : INFO : resetting layer weights + +.. GENERATED FROM PYTHON SOURCE LINES 209-214 Essentially, the vocabulary is a list (accessible via ``model.wv.index_to_key``) of all of the unique words extracted from the training corpus. @@ -324,6 +355,7 @@ Additional attributes for each word are available using the ``model.wv.get_vecat For example, to see how many times ``penalty`` appeared in the training corpus: +.. GENERATED FROM PYTHON SOURCE LINES 214-216 .. code-block:: default @@ -335,8 +367,6 @@ For example, to see how many times ``penalty`` appeared in the training corpus: .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none Word 'penalty' appeared 4 times in the training corpus. @@ -344,12 +374,21 @@ For example, to see how many times ``penalty`` appeared in the training corpus: +.. GENERATED FROM PYTHON SOURCE LINES 217-228 + Next, train the model on the corpus. -If optimized Gensim (with BLAS library) is being used, this should take no more than 3 seconds. -If the BLAS library is not being used, this should take no more than 2 -minutes, so use optimized Gensim with BLAS if you value your time. +In the usual case, where Gensim installation found a BLAS library for optimized +bulk vector operations, this training on this tiny 300 document, ~60k word corpus +should take just a few seconds. (More realistic datasets of tens-of-millions +of words or more take proportionately longer.) If for some reason a BLAS library +isn't available, training uses a fallback approach that takes 60x-120x longer, +so even this tiny training will take minutes rather than seconds. (And, in that +case, you should also notice a warning in the logging letting you know there's +something worth fixing.) So, be sure your installation uses the BLAS-optimized +Gensim if you value your time. +.. GENERATED FROM PYTHON SOURCE LINES 228-230 .. code-block:: default @@ -361,181 +400,62 @@ minutes, so use optimized Gensim with BLAS if you value your time. .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - 2020-09-30 21:08:55,553 : INFO : training model with 3 workers on 3955 vocabulary and 50 features, using sg=0 hs=0 sample=0.001 negative=5 window=5 - 2020-09-30 21:08:55,613 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:55,614 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:55,614 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:55,614 : INFO : EPOCH - 1 : training on 58152 raw words (42784 effective words) took 0.1s, 751479 effective words/s - 2020-09-30 21:08:55,664 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:55,666 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:55,666 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:55,666 : INFO : EPOCH - 2 : training on 58152 raw words (42745 effective words) took 0.1s, 845101 effective words/s - 2020-09-30 21:08:55,718 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:55,719 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:55,720 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:55,720 : INFO : EPOCH - 3 : training on 58152 raw words (42605 effective words) took 0.1s, 810845 effective words/s - 2020-09-30 21:08:55,781 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:55,783 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:55,784 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:55,784 : INFO : EPOCH - 4 : training on 58152 raw words (42723 effective words) took 0.1s, 677810 effective words/s - 2020-09-30 21:08:55,846 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:55,847 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:55,848 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:55,848 : INFO : EPOCH - 5 : training on 58152 raw words (42641 effective words) took 0.1s, 682513 effective words/s - 2020-09-30 21:08:55,903 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:55,905 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:55,905 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:55,905 : INFO : EPOCH - 6 : training on 58152 raw words (42654 effective words) took 0.1s, 760381 effective words/s - 2020-09-30 21:08:55,960 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:55,962 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:55,964 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:55,964 : INFO : EPOCH - 7 : training on 58152 raw words (42751 effective words) took 0.1s, 741994 effective words/s - 2020-09-30 21:08:56,018 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,020 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,020 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,020 : INFO : EPOCH - 8 : training on 58152 raw words (42692 effective words) took 0.1s, 773631 effective words/s - 2020-09-30 21:08:56,076 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,078 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,081 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,081 : INFO : EPOCH - 9 : training on 58152 raw words (42745 effective words) took 0.1s, 719453 effective words/s - 2020-09-30 21:08:56,137 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,137 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,137 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,138 : INFO : EPOCH - 10 : training on 58152 raw words (42733 effective words) took 0.1s, 770082 effective words/s - 2020-09-30 21:08:56,195 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,196 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,197 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,197 : INFO : EPOCH - 11 : training on 58152 raw words (42791 effective words) took 0.1s, 734171 effective words/s - 2020-09-30 21:08:56,253 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,255 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,255 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,255 : INFO : EPOCH - 12 : training on 58152 raw words (42773 effective words) took 0.1s, 745248 effective words/s - 2020-09-30 21:08:56,316 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,318 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,318 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,318 : INFO : EPOCH - 13 : training on 58152 raw words (42793 effective words) took 0.1s, 702300 effective words/s - 2020-09-30 21:08:56,369 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,371 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,373 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,373 : INFO : EPOCH - 14 : training on 58152 raw words (42637 effective words) took 0.1s, 802259 effective words/s - 2020-09-30 21:08:56,421 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,425 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,426 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,426 : INFO : EPOCH - 15 : training on 58152 raw words (42686 effective words) took 0.1s, 820787 effective words/s - 2020-09-30 21:08:56,475 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,478 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,479 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,479 : INFO : EPOCH - 16 : training on 58152 raw words (42799 effective words) took 0.1s, 829690 effective words/s - 2020-09-30 21:08:56,530 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,530 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,533 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,534 : INFO : EPOCH - 17 : training on 58152 raw words (42733 effective words) took 0.1s, 794744 effective words/s - 2020-09-30 21:08:56,583 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,585 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,587 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,587 : INFO : EPOCH - 18 : training on 58152 raw words (42703 effective words) took 0.1s, 813146 effective words/s - 2020-09-30 21:08:56,638 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,640 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,640 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,641 : INFO : EPOCH - 19 : training on 58152 raw words (42763 effective words) took 0.1s, 822300 effective words/s - 2020-09-30 21:08:56,696 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,700 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,700 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,700 : INFO : EPOCH - 20 : training on 58152 raw words (42649 effective words) took 0.1s, 733047 effective words/s - 2020-09-30 21:08:56,752 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,753 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,754 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,754 : INFO : EPOCH - 21 : training on 58152 raw words (42701 effective words) took 0.1s, 822006 effective words/s - 2020-09-30 21:08:56,803 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,805 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,805 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,805 : INFO : EPOCH - 22 : training on 58152 raw words (42714 effective words) took 0.1s, 848390 effective words/s - 2020-09-30 21:08:56,857 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,857 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,859 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,860 : INFO : EPOCH - 23 : training on 58152 raw words (42740 effective words) took 0.1s, 811758 effective words/s - 2020-09-30 21:08:56,907 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,909 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,910 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,910 : INFO : EPOCH - 24 : training on 58152 raw words (42754 effective words) took 0.0s, 873741 effective words/s - 2020-09-30 21:08:56,959 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:56,960 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:56,960 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:56,960 : INFO : EPOCH - 25 : training on 58152 raw words (42704 effective words) took 0.0s, 862291 effective words/s - 2020-09-30 21:08:57,009 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,010 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,011 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,011 : INFO : EPOCH - 26 : training on 58152 raw words (42741 effective words) took 0.0s, 868076 effective words/s - 2020-09-30 21:08:57,059 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,062 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,063 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,063 : INFO : EPOCH - 27 : training on 58152 raw words (42610 effective words) took 0.1s, 830699 effective words/s - 2020-09-30 21:08:57,112 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,114 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,115 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,116 : INFO : EPOCH - 28 : training on 58152 raw words (42747 effective words) took 0.1s, 835959 effective words/s - 2020-09-30 21:08:57,164 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,169 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,170 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,170 : INFO : EPOCH - 29 : training on 58152 raw words (42755 effective words) took 0.1s, 804348 effective words/s - 2020-09-30 21:08:57,219 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,222 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,224 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,224 : INFO : EPOCH - 30 : training on 58152 raw words (42760 effective words) took 0.1s, 808636 effective words/s - 2020-09-30 21:08:57,271 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,273 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,273 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,273 : INFO : EPOCH - 31 : training on 58152 raw words (42727 effective words) took 0.0s, 889118 effective words/s - 2020-09-30 21:08:57,323 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,326 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,327 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,327 : INFO : EPOCH - 32 : training on 58152 raw words (42786 effective words) took 0.1s, 819149 effective words/s - 2020-09-30 21:08:57,377 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,378 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,379 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,379 : INFO : EPOCH - 33 : training on 58152 raw words (42614 effective words) took 0.1s, 828217 effective words/s - 2020-09-30 21:08:57,427 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,430 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,431 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,431 : INFO : EPOCH - 34 : training on 58152 raw words (42757 effective words) took 0.1s, 848700 effective words/s - 2020-09-30 21:08:57,476 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,479 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,481 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,481 : INFO : EPOCH - 35 : training on 58152 raw words (42713 effective words) took 0.0s, 881912 effective words/s - 2020-09-30 21:08:57,530 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,530 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,532 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,532 : INFO : EPOCH - 36 : training on 58152 raw words (42632 effective words) took 0.1s, 843930 effective words/s - 2020-09-30 21:08:57,580 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,583 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,584 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,584 : INFO : EPOCH - 37 : training on 58152 raw words (42691 effective words) took 0.1s, 851268 effective words/s - 2020-09-30 21:08:57,632 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,634 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,635 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,635 : INFO : EPOCH - 38 : training on 58152 raw words (42667 effective words) took 0.1s, 850589 effective words/s - 2020-09-30 21:08:57,685 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,686 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,687 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,687 : INFO : EPOCH - 39 : training on 58152 raw words (42641 effective words) took 0.1s, 843857 effective words/s - 2020-09-30 21:08:57,736 : INFO : worker thread finished; awaiting finish of 2 more threads - 2020-09-30 21:08:57,737 : INFO : worker thread finished; awaiting finish of 1 more threads - 2020-09-30 21:08:57,741 : INFO : worker thread finished; awaiting finish of 0 more threads - 2020-09-30 21:08:57,741 : INFO : EPOCH - 40 : training on 58152 raw words (42721 effective words) took 0.1s, 807691 effective words/s - 2020-09-30 21:08:57,741 : INFO : training on a 2326080 raw words (1708575 effective words) took 2.2s, 781245 effective words/s - - - + 2022-12-07 10:59:01,272 : INFO : Doc2Vec lifecycle event {'msg': 'training model with 3 workers on 3955 vocabulary and 50 features, using sg=0 hs=0 sample=0.001 negative=5 window=5 shrink_windows=True', 'datetime': '2022-12-07T10:59:01.271863', 'gensim': '4.2.1.dev0', 'python': '3.8.10 (default, Jun 22 2022, 20:18:18) \n[GCC 9.4.0]', 'platform': 'Linux-5.4.0-135-generic-x86_64-with-glibc2.29', 'event': 'train'} + 2022-12-07 10:59:01,408 : INFO : EPOCH 0: training on 58152 raw words (42665 effective words) took 0.1s, 335294 effective words/s + 2022-12-07 10:59:01,462 : INFO : EPOCH 1: training on 58152 raw words (42755 effective words) took 0.1s, 816420 effective words/s + 2022-12-07 10:59:01,521 : INFO : EPOCH 2: training on 58152 raw words (42692 effective words) took 0.1s, 745004 effective words/s + 2022-12-07 10:59:01,573 : INFO : EPOCH 3: training on 58152 raw words (42670 effective words) took 0.1s, 841368 effective words/s + 2022-12-07 10:59:01,627 : INFO : EPOCH 4: training on 58152 raw words (42685 effective words) took 0.1s, 815442 effective words/s + 2022-12-07 10:59:01,703 : INFO : EPOCH 5: training on 58152 raw words (42709 effective words) took 0.1s, 578402 effective words/s + 2022-12-07 10:59:01,753 : INFO : EPOCH 6: training on 58152 raw words (42594 effective words) took 0.0s, 864899 effective words/s + 2022-12-07 10:59:01,804 : INFO : EPOCH 7: training on 58152 raw words (42721 effective words) took 0.0s, 864073 effective words/s + 2022-12-07 10:59:01,881 : INFO : EPOCH 8: training on 58152 raw words (42622 effective words) took 0.1s, 566867 effective words/s + 2022-12-07 10:59:01,932 : INFO : EPOCH 9: training on 58152 raw words (42770 effective words) took 0.0s, 862066 effective words/s + 2022-12-07 10:59:02,006 : INFO : EPOCH 10: training on 58152 raw words (42739 effective words) took 0.1s, 587035 effective words/s + 2022-12-07 10:59:02,058 : INFO : EPOCH 11: training on 58152 raw words (42612 effective words) took 0.1s, 850879 effective words/s + 2022-12-07 10:59:02,135 : INFO : EPOCH 12: training on 58152 raw words (42655 effective words) took 0.1s, 566216 effective words/s + 2022-12-07 10:59:02,187 : INFO : EPOCH 13: training on 58152 raw words (42749 effective words) took 0.1s, 844125 effective words/s + 2022-12-07 10:59:02,265 : INFO : EPOCH 14: training on 58152 raw words (42748 effective words) took 0.1s, 556136 effective words/s + 2022-12-07 10:59:02,347 : INFO : EPOCH 15: training on 58152 raw words (42748 effective words) took 0.1s, 530528 effective words/s + 2022-12-07 10:59:02,398 : INFO : EPOCH 16: training on 58152 raw words (42737 effective words) took 0.0s, 871200 effective words/s + 2022-12-07 10:59:02,485 : INFO : EPOCH 17: training on 58152 raw words (42697 effective words) took 0.1s, 499981 effective words/s + 2022-12-07 10:59:02,584 : INFO : EPOCH 18: training on 58152 raw words (42747 effective words) took 0.1s, 440730 effective words/s + 2022-12-07 10:59:02,672 : INFO : EPOCH 19: training on 58152 raw words (42739 effective words) took 0.1s, 497651 effective words/s + 2022-12-07 10:59:02,761 : INFO : EPOCH 20: training on 58152 raw words (42782 effective words) took 0.1s, 499103 effective words/s + 2022-12-07 10:59:02,851 : INFO : EPOCH 21: training on 58152 raw words (42580 effective words) took 0.1s, 489515 effective words/s + 2022-12-07 10:59:02,939 : INFO : EPOCH 22: training on 58152 raw words (42687 effective words) took 0.1s, 496560 effective words/s + 2022-12-07 10:59:03,023 : INFO : EPOCH 23: training on 58152 raw words (42667 effective words) took 0.1s, 517527 effective words/s + 2022-12-07 10:59:03,156 : INFO : EPOCH 24: training on 58152 raw words (42678 effective words) took 0.1s, 328575 effective words/s + 2022-12-07 10:59:03,322 : INFO : EPOCH 25: training on 58152 raw words (42743 effective words) took 0.2s, 261440 effective words/s + 2022-12-07 10:59:03,486 : INFO : EPOCH 26: training on 58152 raw words (42692 effective words) took 0.2s, 266564 effective words/s + 2022-12-07 10:59:03,627 : INFO : EPOCH 27: training on 58152 raw words (42774 effective words) took 0.1s, 310530 effective words/s + 2022-12-07 10:59:03,770 : INFO : EPOCH 28: training on 58152 raw words (42706 effective words) took 0.1s, 305665 effective words/s + 2022-12-07 10:59:03,901 : INFO : EPOCH 29: training on 58152 raw words (42658 effective words) took 0.1s, 334228 effective words/s + 2022-12-07 10:59:04,028 : INFO : EPOCH 30: training on 58152 raw words (42746 effective words) took 0.1s, 344379 effective words/s + 2022-12-07 10:59:04,159 : INFO : EPOCH 31: training on 58152 raw words (42676 effective words) took 0.1s, 334291 effective words/s + 2022-12-07 10:59:04,295 : INFO : EPOCH 32: training on 58152 raw words (42763 effective words) took 0.1s, 322886 effective words/s + 2022-12-07 10:59:04,488 : INFO : EPOCH 33: training on 58152 raw words (42647 effective words) took 0.2s, 224522 effective words/s + 2022-12-07 10:59:04,629 : INFO : EPOCH 34: training on 58152 raw words (42720 effective words) took 0.1s, 310616 effective words/s + 2022-12-07 10:59:04,764 : INFO : EPOCH 35: training on 58152 raw words (42775 effective words) took 0.1s, 323299 effective words/s + 2022-12-07 10:59:04,899 : INFO : EPOCH 36: training on 58152 raw words (42662 effective words) took 0.1s, 322458 effective words/s + 2022-12-07 10:59:05,032 : INFO : EPOCH 37: training on 58152 raw words (42656 effective words) took 0.1s, 329126 effective words/s + 2022-12-07 10:59:05,162 : INFO : EPOCH 38: training on 58152 raw words (42720 effective words) took 0.1s, 337238 effective words/s + 2022-12-07 10:59:05,308 : INFO : EPOCH 39: training on 58152 raw words (42688 effective words) took 0.1s, 299620 effective words/s + 2022-12-07 10:59:05,308 : INFO : Doc2Vec lifecycle event {'msg': 'training on 2326080 raw words (1708074 effective words) took 4.0s, 423332 effective words/s', 'datetime': '2022-12-07T10:59:05.308684', 'gensim': '4.2.1.dev0', 'python': '3.8.10 (default, Jun 22 2022, 20:18:18) \n[GCC 9.4.0]', 'platform': 'Linux-5.4.0-135-generic-x86_64-with-glibc2.29', 'event': 'train'} + + + + +.. GENERATED FROM PYTHON SOURCE LINES 231-235 Now, we can use the trained model to infer a vector for any piece of text by passing a list of words to the ``model.infer_vector`` function. This vector can then be compared with other vectors via cosine similarity. +.. GENERATED FROM PYTHON SOURCE LINES 235-238 .. code-block:: default @@ -548,22 +468,22 @@ vector can then be compared with other vectors via cosine similarity. .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - [-0.08478509 0.05011684 0.0675064 -0.19926868 -0.1235586 0.01768214 - -0.12645927 0.01062329 0.06113973 0.35424358 0.01320948 0.07561274 - -0.01645093 0.0692549 0.08346193 -0.01599065 0.08287009 -0.0139379 - -0.17772709 -0.26271465 0.0442089 -0.04659882 -0.12873884 0.28799203 - -0.13040264 0.12478471 -0.14091878 -0.09698066 -0.07903259 -0.10124907 - -0.28239366 0.13270256 0.04445919 -0.24210942 -0.1907376 -0.07264525 - -0.14167067 -0.22816683 -0.00663796 0.23165748 -0.10436232 -0.01028251 - -0.04064698 0.08813146 0.01072008 -0.149789 0.05923386 0.16301566 - 0.05815683 0.1258063 ] + [-0.10196274 -0.36020595 -0.10973375 0.28432116 -0.00792601 0.01950991 + 0.01309869 0.1045896 -0.2011485 -0.12135196 0.15298457 0.05421316 + -0.06486023 -0.00131951 -0.2237759 -0.08489189 0.05889525 0.27961093 + 0.08121023 -0.06200862 -0.00651888 -0.06831821 0.13001564 0.04539844 + -0.01659351 -0.02359444 -0.22276032 0.06692155 -0.11293832 -0.08056813 + 0.38737044 0.05470002 0.19902836 0.19122775 0.17020799 0.10668964 + 0.01216549 -0.3049222 -0.05198798 0.00130251 0.04994885 -0.0069596 + -0.06367141 -0.11740001 0.14623125 0.10109582 -0.06466878 -0.06512908 + 0.17817481 -0.00934212] + +.. GENERATED FROM PYTHON SOURCE LINES 239-247 Note that ``infer_vector()`` does *not* take a string, but rather a list of string tokens, which should have already been tokenized the same way as the @@ -574,6 +494,8 @@ iterative approximation problem that makes use of internal randomization, repeated inferences of the same text will return slightly different vectors. +.. GENERATED FROM PYTHON SOURCE LINES 249-262 + Assessing the Model ------------------- @@ -588,6 +510,7 @@ Additionally, we'll keep track of the second ranks for a comparison of less similar documents. +.. GENERATED FROM PYTHON SOURCE LINES 262-272 .. code-block:: default @@ -608,10 +531,13 @@ similar documents. +.. GENERATED FROM PYTHON SOURCE LINES 273-276 + Let's count how each document ranks with respect to the training corpus NB. Results vary between runs due to random seeding and very small corpus +.. GENERATED FROM PYTHON SOURCE LINES 276-281 .. code-block:: default @@ -626,8 +552,6 @@ NB. Results vary between runs due to random seeding and very small corpus .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none Counter({0: 292, 1: 8}) @@ -635,6 +559,8 @@ NB. Results vary between runs due to random seeding and very small corpus +.. GENERATED FROM PYTHON SOURCE LINES 282-290 + Basically, greater than 95% of the inferred documents are found to be most similar to itself and about 5% of the time it is mistakenly most similar to another document. Checking the inferred-vector against a @@ -644,6 +570,7 @@ behaving in a usefully consistent manner, though not a real 'accuracy' value. This is great and not entirely surprising. We can take a look at an example: +.. GENERATED FROM PYTHON SOURCE LINES 290-295 .. code-block:: default @@ -658,26 +585,26 @@ This is great and not entirely surprising. We can take a look at an example: .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none Document (299): «australia will take on france in the doubles rubber of the davis cup tennis final today with the tie levelled at wayne arthurs and todd woodbridge are scheduled to lead australia in the doubles against cedric pioline and fabrice santoro however changes can be made to the line up up to an hour before the match and australian team captain john fitzgerald suggested he might do just that we ll make team appraisal of the whole situation go over the pros and cons and make decision french team captain guy forget says he will not make changes but does not know what to expect from australia todd is the best doubles player in the world right now so expect him to play he said would probably use wayne arthurs but don know what to expect really pat rafter salvaged australia davis cup campaign yesterday with win in the second singles match rafter overcame an arm injury to defeat french number one sebastien grosjean in three sets the australian says he is happy with his form it not very pretty tennis there isn too many consistent bounces you are playing like said bit of classic old grass court rafter said rafter levelled the score after lleyton hewitt shock five set loss to nicholas escude in the first singles rubber but rafter says he felt no added pressure after hewitt defeat knew had good team to back me up even if we were down he said knew could win on the last day know the boys can win doubles so even if we were down still feel we are good enough team to win and vice versa they are good enough team to beat us as well» - SIMILAR/DISSIMILAR DOCS PER MODEL Doc2Vec(dm/m,d50,n5,w5,mc2,s0.001,t3): + SIMILAR/DISSIMILAR DOCS PER MODEL Doc2Vec: - MOST (299, 0.9482713341712952): «australia will take on france in the doubles rubber of the davis cup tennis final today with the tie levelled at wayne arthurs and todd woodbridge are scheduled to lead australia in the doubles against cedric pioline and fabrice santoro however changes can be made to the line up up to an hour before the match and australian team captain john fitzgerald suggested he might do just that we ll make team appraisal of the whole situation go over the pros and cons and make decision french team captain guy forget says he will not make changes but does not know what to expect from australia todd is the best doubles player in the world right now so expect him to play he said would probably use wayne arthurs but don know what to expect really pat rafter salvaged australia davis cup campaign yesterday with win in the second singles match rafter overcame an arm injury to defeat french number one sebastien grosjean in three sets the australian says he is happy with his form it not very pretty tennis there isn too many consistent bounces you are playing like said bit of classic old grass court rafter said rafter levelled the score after lleyton hewitt shock five set loss to nicholas escude in the first singles rubber but rafter says he felt no added pressure after hewitt defeat knew had good team to back me up even if we were down he said knew could win on the last day know the boys can win doubles so even if we were down still feel we are good enough team to win and vice versa they are good enough team to beat us as well» + MOST (299, 0.9564058780670166): «australia will take on france in the doubles rubber of the davis cup tennis final today with the tie levelled at wayne arthurs and todd woodbridge are scheduled to lead australia in the doubles against cedric pioline and fabrice santoro however changes can be made to the line up up to an hour before the match and australian team captain john fitzgerald suggested he might do just that we ll make team appraisal of the whole situation go over the pros and cons and make decision french team captain guy forget says he will not make changes but does not know what to expect from australia todd is the best doubles player in the world right now so expect him to play he said would probably use wayne arthurs but don know what to expect really pat rafter salvaged australia davis cup campaign yesterday with win in the second singles match rafter overcame an arm injury to defeat french number one sebastien grosjean in three sets the australian says he is happy with his form it not very pretty tennis there isn too many consistent bounces you are playing like said bit of classic old grass court rafter said rafter levelled the score after lleyton hewitt shock five set loss to nicholas escude in the first singles rubber but rafter says he felt no added pressure after hewitt defeat knew had good team to back me up even if we were down he said knew could win on the last day know the boys can win doubles so even if we were down still feel we are good enough team to win and vice versa they are good enough team to beat us as well» - SECOND-MOST (104, 0.8029672503471375): «australian cricket captain steve waugh has supported fast bowler brett lee after criticism of his intimidatory bowling to the south african tailenders in the first test in adelaide earlier this month lee was fined for giving new zealand tailender shane bond an unsportsmanlike send off during the third test in perth waugh says tailenders should not be protected from short pitched bowling these days you re earning big money you ve got responsibility to learn how to bat he said mean there no times like years ago when it was not professional and sort of bowlers code these days you re professional our batsmen work very hard at their batting and expect other tailenders to do likewise meanwhile waugh says his side will need to guard against complacency after convincingly winning the first test by runs waugh says despite the dominance of his side in the first test south africa can never be taken lightly it only one test match out of three or six whichever way you want to look at it so there lot of work to go he said but it nice to win the first battle definitely it gives us lot of confidence going into melbourne you know the big crowd there we love playing in front of the boxing day crowd so that will be to our advantage as well south africa begins four day match against new south wales in sydney on thursday in the lead up to the boxing day test veteran fast bowler allan donald will play in the warm up match and is likely to take his place in the team for the second test south african captain shaun pollock expects much better performance from his side in the melbourne test we still believe that we didn play to our full potential so if we can improve on our aspects the output we put out on the field will be lot better and we still believe we have side that is good enough to beat australia on our day he said» + SECOND-MOST (104, 0.7868924140930176): «australian cricket captain steve waugh has supported fast bowler brett lee after criticism of his intimidatory bowling to the south african tailenders in the first test in adelaide earlier this month lee was fined for giving new zealand tailender shane bond an unsportsmanlike send off during the third test in perth waugh says tailenders should not be protected from short pitched bowling these days you re earning big money you ve got responsibility to learn how to bat he said mean there no times like years ago when it was not professional and sort of bowlers code these days you re professional our batsmen work very hard at their batting and expect other tailenders to do likewise meanwhile waugh says his side will need to guard against complacency after convincingly winning the first test by runs waugh says despite the dominance of his side in the first test south africa can never be taken lightly it only one test match out of three or six whichever way you want to look at it so there lot of work to go he said but it nice to win the first battle definitely it gives us lot of confidence going into melbourne you know the big crowd there we love playing in front of the boxing day crowd so that will be to our advantage as well south africa begins four day match against new south wales in sydney on thursday in the lead up to the boxing day test veteran fast bowler allan donald will play in the warm up match and is likely to take his place in the team for the second test south african captain shaun pollock expects much better performance from his side in the melbourne test we still believe that we didn play to our full potential so if we can improve on our aspects the output we put out on the field will be lot better and we still believe we have side that is good enough to beat australia on our day he said» - MEDIAN (238, 0.2635717988014221): «centrelink is urging people affected by job cuts at regional pay tv operator austar and travel company traveland to seek information about their income support options traveland has announced it is shedding more than jobs around australia and austar is letting employees go centrelink finance information officer peter murray says those facing uncertain futures should head to centrelink in the next few days centrelink is the shopfront now for commonwealth services for income support and the employment network so that it is important if people haven been to us before they might get pleasant surprise at the range of services that we do offer to try and help them through situations where things might have changed for them mr murray said» + MEDIAN (119, 0.24808582663536072): «australia is continuing to negotiate with the united states government in an effort to interview the australian david hicks who was captured fighting alongside taliban forces in afghanistan mr hicks is being held by the united states on board ship in the afghanistan region where the australian federal police and australian security intelligence organisation asio officials are trying to gain access foreign affairs minister alexander downer has also confirmed that the australian government is investigating reports that another australian has been fighting for taliban forces in afghanistan we often get reports of people going to different parts of the world and asking us to investigate them he said we always investigate sometimes it is impossible to find out we just don know in this case but it is not to say that we think there are lot of australians in afghanistan the only case we know is hicks mr downer says it is unclear when mr hicks will be back on australian soil but he is hopeful the americans will facilitate australian authorities interviewing him» - LEAST (243, -0.13247375190258026): «four afghan factions have reached agreement on an interim cabinet during talks in germany the united nations says the administration which will take over from december will be headed by the royalist anti taliban commander hamed karzai it concludes more than week of negotiations outside bonn and is aimed at restoring peace and stability to the war ravaged country the year old former deputy foreign minister who is currently battling the taliban around the southern city of kandahar is an ally of the exiled afghan king mohammed zahir shah he will serve as chairman of an interim authority that will govern afghanistan for six month period before loya jirga or grand traditional assembly of elders in turn appoints an month transitional government meanwhile united states marines are now reported to have been deployed in eastern afghanistan where opposition forces are closing in on al qaeda soldiers reports from the area say there has been gun battle between the opposition and al qaeda close to the tora bora cave complex where osama bin laden is thought to be hiding in the south of the country american marines are taking part in patrols around the air base they have secured near kandahar but are unlikely to take part in any assault on the city however the chairman of the joint chiefs of staff general richard myers says they are prepared for anything they are prepared for engagements they re robust fighting force and they re absolutely ready to engage if that required he said» + LEAST (216, -0.11085141450166702): «senior taliban official confirmed the islamic militia would begin handing over its last bastion of kandahar to pashtun tribal leaders on friday this agreement was that taliban should surrender kandahar peacefully to the elders of these areas and we should guarantee the lives and the safety of taliban authorities and all the taliban from tomorrow should start this program former taliban ambassador to pakistan abdul salam zaeef told cnn in telephone interview he insisted that the taliban would not surrender to hamid karzai the new afghan interim leader and pashtun elder who has been cooperating with the united states to calm unrest among the southern tribes the taliban will surrender to elders not to karzai karzai and other persons which they want to enter kandahar by the support of america they don allow to enter kandahar city he said the taliban will surrender the weapons the ammunition to elders» +.. GENERATED FROM PYTHON SOURCE LINES 296-305 + Notice above that the most similar document (usually the same text) is has a similarity score approaching 1.0. However, the similarity score for the second-ranked documents should be significantly lower (assuming the documents @@ -688,6 +615,7 @@ We can run the next cell repeatedly to see a sampling other target-document comparisons. +.. GENERATED FROM PYTHON SOURCE LINES 305-315 .. code-block:: default @@ -707,17 +635,17 @@ comparisons. .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - Train Document (292): «rival afghan factions are deadlocked over the shape of future government the northern alliance has demanded day adjournment of power sharing talks in germany after its president burhanuddin rabbani objected to the appointment system for an interim administration president rabbani has objected to the plans for an interim government to be drawn up by appointment as discussed in bonn saying the interim leaders should be voted in by afghans themselves he also says there is no real need for sizeable international security force president rabbani says he would prefer local afghan factions drew up their own internal security forces of around personnel but if the world insisted there should be an international security presence there should be no more than or personnel in their security forces he says president rabbani objections are likely to cast doubt on his delegation ability to commit the northern alliance to any course of action decided upon in bonn he now threatens to undermine the very process he claims to support in the quest for stable government in afghanistan» + Train Document (198): «authorities are trying to track down the crew of vessel that landed undetected at cocos islands carrying asylum seekers the group of sri lankan men was found aboard their boat moored to the south of the islands yesterday afternoon shire president ron grant says investigations are underway as to the whereabouts of the crew after the asylum seekers told authorities they had left in another boat after dropping them off unfortunately for them there two aircraft the royal australian air force here at the moment and one getting prepared to fly off and obviously they will be looking to see if there is another boat he said mr grant says the sri lankans have not yet been brought ashore» + + Similar Document (89, 0.7137947082519531): «after the torching of more than buildings over the past three days the situation at the woomera detention centre overnight appeared relatively calm there was however tension inside the south australian facility with up to detainees breaking into prohibited zone the group became problem for staff after breaching fence within the centre at one point staff considered using water cannon to control the detainees it is not known if they actually resorted to any tough action but group of men wearing riot gear possibly star force police officers brought in on standby could be seen in one of the compounds late yesterday government authorities confirmed that two detainees had committed acts of self harm one of them needed stitches and is believed to have been taken away in an ambulance no other details have been released» - Similar Document (13, 0.7867921590805054): «talks between afghan and british officials in kabul have ended without final agreement on the deployment of international security force the lack of suitable translation of the document meant further delay authorities in kabul have been giving conflicting signals for weeks now over the number of peacekeepers they would allow and the role the international force would play the foreign minister dr abdullah appeared to be ending the confusion saying an agreement was about to be signed there is already the agreement so it was finalised he said but spokesman for the interior minister yunis kanooni emerged soon after to say there was no agreement and nothing to sign scores of british peacekeepers are already patrolling the streets of kabul in tandem with afghan police but proposals to enlarge the force to as many as international peacekeepers have been criticised by some commanders as tantamount to foreign occupation» +.. GENERATED FROM PYTHON SOURCE LINES 316-322 Testing the Model ----------------- @@ -726,6 +654,7 @@ Using the same approach above, we'll infer the vector for a randomly chosen test document, and compare the document to our model by eye. +.. GENERATED FROM PYTHON SOURCE LINES 322-334 .. code-block:: default @@ -747,23 +676,23 @@ test document, and compare the document to our model by eye. .. rst-class:: sphx-glr-script-out - Out: - .. code-block:: none - Test Document (49): «labor needed to distinguish itself from the government on the issue of asylum seekers greens leader bob brown has said his senate colleague kerry nettle intends to move motion today on the first anniversary of the tampa crisis condemning the government over its refugee policy and calling for an end to mandatory detention we greens want to bring the government to book over its serial breach of international obligations as far as asylum seekers in this country are concerned senator brown said today» + Test Document (17): «the united nations world food program estimates that up to million people in seven countries malawi mozambique zambia angola swaziland lesotho and zimbabwe face death by starvation unless there is massive international response in malawi as many as people may have already died the signs of malnutrition swollen stomachs stick thin arms light coloured hair are everywhere» + + SIMILAR/DISSIMILAR DOCS PER MODEL Doc2Vec: - SIMILAR/DISSIMILAR DOCS PER MODEL Doc2Vec(dm/m,d50,n5,w5,mc2,s0.001,t3): + MOST (86, 0.8239533305168152): «argentina economy minister domingo cavallo is reported to have resigned in the face of mounting unrest over the country crumbling economy the reports in number of local media outlets could not be officially confirmed the news comes as police used teargas to disperse tens of thousands of people who had massed near the presidential palace in buenos aires and in other parts of the city to protest against the declaration of state of emergency it was declared after mounting popular discontent and widespread looting in the past few days with people over the state of the economy which has been in recession for four years» - MOST (218, 0.8016394376754761): «refugee support groups are strongly critical of federal government claims that the pacific solution program is working well the immigration minister philip ruddock says he is pleased with the program which uses pacific island nations to process asylum seekers wanting to come to australia president of the hazara ethnic society of australia hassan ghulam says the australian government is bullying smaller nations into accepting asylum seekers if the pacific countries wanted refugees they can clearly raise their voice in the united nations and say yes we are accepting refugees and why australia who gives this authority to the australian government to force the pacific countries to accept refugees in this form or in the other form he asked» + MEDIAN (221, 0.40627941489219666): «reserve bank governor ian macfarlane says he is confident australia will ride through the current world economic slump largely brought on by the united states mr macfarlane told gathering in sydney last night australia growth is remarkably good by world standards and inflation should come down in the next months he predicts the united states economy will show signs of recovery from mid year and that as result it is highly unlikely that the reserve bank will raise interest rates in the next six months calendar year has been difficult one for the world economy and the first half of looks like remaining weak before recovery gets underway therefore this period will be classified as world recession like those of the mid the early and the early mr macfarlane said the australian economy has got through the first half of it in reasonably good shape» - MEDIAN (204, 0.3319269120693207): «an iraqi doctor being held at sydney villawood detention centre claims he was prevented from receiving human rights award dr aamer sultan had been awarded special commendation at yesterday human rights and equal opportunity commission awards in sydney but was not able to receive the honour in person dr sultan says he had been hoping to attend the ceremony but says the management at villawood stopped him from going submitted formal request to the centre manager who promised me that he will present the matter to migration management here who are the main authority here they also came back that unfortunately we can not fulfill this request for you but they didn give any explanation dr sultan says he was disappointed by the decision the immigration minister philip ruddock has written letter of complaint to the medical journal of australia about an article penned by dr sultan on the psychological state of detainees at villawood the journal has published research dr sultan conducted with former visiting psychologist to the centre kevin sullivan their survey of detainees over nine months found all but one displayed symptoms of psychological distress at some time the article says per cent acknowledged chronic depressive symptoms and close to half of the group had reached severe stages of depression» + LEAST (37, -0.06813289225101471): «australia quicks and opening batsmen have put the side in dominant position going into day three of the boxing day test match against south africa at the mcg australia is no wicket for only runs shy of south africa after andy bichel earlier starred as the tourists fell for when play was abandoned due to rain few overs short of scheduled stumps yesterday justin langer was not out and matthew hayden the openers went on the attack from the start with langer innings including six fours and hayden eight earlier shaun pollock and nantie haywood launched vital rearguard action to help south africa to respectable first innings total the pair put on runs for the final wicket to help the tourists to the south africans had slumped to for through combination of australia good bowling good fielding and good luck after resuming at for yesterday morning the tourists looked to be cruising as jacques kallis and neil mckenzie added without loss but then bichel suddenly had them reeling after snatching two wickets in two balls first he had jacques kallis caught behind for although kallis could consider himself very unlucky as replays showed his bat was long way from the ball on the next ball bichel snatched sharp return catch to dismiss lance klusener first ball and have shot at hat trick bichel missed out on the hat trick and mark boucher and neil mckenzie again steadied the south african innings adding before the introduction of part timer mark waugh to the attack paid off for australia waugh removed boucher for caught by bichel brett lee then chipped in trapping mckenzie leg before for with perfect inswinger bichel continued his good day in the field running out claude henderson for with direct hit from the in field lee roared in to allan donald bouncing him and then catching the edge with rising delivery which ricky ponting happily swallowed at third slip to remove the returning paceman for duck bichel did not get his hat trick but ended with the best figures of the australian bowlers after also picking up the final wicket of nantie haywood for lee took for and glenn mcgrath for» - LEAST (157, -0.10524928569793701): «british man has been found guilty by unanimous verdict of the kidnap and murder of an eight year old schoolgirl whose death in july shocked britain and set off rampage of anti paedophile vigilantes roy whiting was sentenced to life imprisonment for the abduction and murder of eight year old sarah payne with recommendation by trial judge justice richard curtis that he never be released you are indeed an evil man you are in no way mentally unwell have seen you for month and in my view you are glib and cunning liar justice curtis said there were cheers of delight as the verdicts were read out by the foreman at lewes crown court the jury of nine men and three women had been deliberating for nine hours as soon as the verdicts were declared the court heard details of whiting previous conviction for the kidnap and indecent assault of nine year old girl in prosecutor timothy langdale told the jury how the defendant threw the child into the back of his dirty red ford sierra and locked the doors he had driven her somewhere she didn know where when she asked where they were going he said shut up because he had knife mr langdale said the defendant told the girl to take off her clothes when she refused he produced rope from his pocket and threatened to tie her up what he actually threatened was that he would tie her mouth up she took her clothes off as he had ordered her to do mr langdale then gave graphic details of the abuse to which whiting subjected the terrified child whiting was given four year jail sentence in june after admitting carrying out the attack in march that year but he was released in november despite warnings from probation officers who were convinced there was danger he would attack another child they set out their warnings in pre sentence report prepared after the first assault and in the parole report before he was released from prison he was kept under supervision for four months after his release but was not being monitored by july last year when eight year old sarah was abducted and killed whiting has been arrested three times in connection with the case but the first and second times was released without being charged sarah disappeared on july last year prompting massive police search her partially buried naked body was found days later in field and police believe she was strangled or suffocated» +.. GENERATED FROM PYTHON SOURCE LINES 335-360 Conclusion ---------- @@ -794,30 +723,25 @@ If you'd like to know more about the subject matter of this tutorial, check out .. rst-class:: sphx-glr-timing - **Total running time of the script:** ( 0 minutes 7.863 seconds) + **Total running time of the script:** ( 0 minutes 16.509 seconds) -**Estimated memory usage:** 37 MB +**Estimated memory usage:** 48 MB .. _sphx_glr_download_auto_examples_tutorials_run_doc2vec_lee.py: +.. only:: html -.. only :: html - - .. container:: sphx-glr-footer - :class: sphx-glr-footer-example - - - - .. container:: sphx-glr-download sphx-glr-download-python + .. container:: sphx-glr-footer sphx-glr-footer-example - :download:`Download Python source code: run_doc2vec_lee.py ` + .. container:: sphx-glr-download sphx-glr-download-python + :download:`Download Python source code: run_doc2vec_lee.py ` - .. container:: sphx-glr-download sphx-glr-download-jupyter + .. container:: sphx-glr-download sphx-glr-download-jupyter - :download:`Download Jupyter notebook: run_doc2vec_lee.ipynb ` + :download:`Download Jupyter notebook: run_doc2vec_lee.ipynb ` .. only:: html diff --git a/docs/src/auto_examples/tutorials/sg_execution_times.rst b/docs/src/auto_examples/tutorials/sg_execution_times.rst index 0b10d0ae69..eb3d2c3c8c 100644 --- a/docs/src/auto_examples/tutorials/sg_execution_times.rst +++ b/docs/src/auto_examples/tutorials/sg_execution_times.rst @@ -5,22 +5,21 @@ Computation times ================= -**00:36.418** total execution time for **auto_examples_tutorials** files: -+-------------------------------------------------------------------------------------+-----------+-----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_wmd.py` (``run_wmd.py``) | 00:36.418 | 7551.3 MB | -+-------------------------------------------------------------------------------------+-----------+-----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_annoy.py` (``run_annoy.py``) | 00:00.000 | 0.0 MB | -+-------------------------------------------------------------------------------------+-----------+-----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_doc2vec_lee.py` (``run_doc2vec_lee.py``) | 00:00.000 | 0.0 MB | -+-------------------------------------------------------------------------------------+-----------+-----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_ensemblelda.py` (``run_ensemblelda.py``) | 00:00.000 | 0.0 MB | -+-------------------------------------------------------------------------------------+-----------+-----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_fasttext.py` (``run_fasttext.py``) | 00:00.000 | 0.0 MB | -+-------------------------------------------------------------------------------------+-----------+-----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_lda.py` (``run_lda.py``) | 00:00.000 | 0.0 MB | -+-------------------------------------------------------------------------------------+-----------+-----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_scm.py` (``run_scm.py``) | 00:00.000 | 0.0 MB | -+-------------------------------------------------------------------------------------+-----------+-----------+ -| :ref:`sphx_glr_auto_examples_tutorials_run_word2vec.py` (``run_word2vec.py``) | 00:00.000 | 0.0 MB | -+-------------------------------------------------------------------------------------+-----------+-----------+ ++-------------------------------------------------------------------------------------+-----------+---------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_doc2vec_lee.py` (``run_doc2vec_lee.py``) | 00:16.509 | 48.4 MB | ++-------------------------------------------------------------------------------------+-----------+---------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_annoy.py` (``run_annoy.py``) | 00:00.000 | 0.0 MB | ++-------------------------------------------------------------------------------------+-----------+---------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_ensemblelda.py` (``run_ensemblelda.py``) | 00:00.000 | 0.0 MB | ++-------------------------------------------------------------------------------------+-----------+---------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_fasttext.py` (``run_fasttext.py``) | 00:00.000 | 0.0 MB | ++-------------------------------------------------------------------------------------+-----------+---------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_lda.py` (``run_lda.py``) | 00:00.000 | 0.0 MB | ++-------------------------------------------------------------------------------------+-----------+---------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_scm.py` (``run_scm.py``) | 00:00.000 | 0.0 MB | ++-------------------------------------------------------------------------------------+-----------+---------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_wmd.py` (``run_wmd.py``) | 00:00.000 | 0.0 MB | ++-------------------------------------------------------------------------------------+-----------+---------+ +| :ref:`sphx_glr_auto_examples_tutorials_run_word2vec.py` (``run_word2vec.py``) | 00:00.000 | 0.0 MB | ++-------------------------------------------------------------------------------------+-----------+---------+ diff --git a/docs/src/gallery/tutorials/run_doc2vec_lee.py b/docs/src/gallery/tutorials/run_doc2vec_lee.py index 7012d38f66..18f4ee7b16 100644 --- a/docs/src/gallery/tutorials/run_doc2vec_lee.py +++ b/docs/src/gallery/tutorials/run_doc2vec_lee.py @@ -215,9 +215,15 @@ def read_corpus(fname, tokens_only=False): ############################################################################### # Next, train the model on the corpus. -# If optimized Gensim (with BLAS library) is being used, this should take no more than 3 seconds. -# If the BLAS library is not being used, this should take no more than 2 -# minutes, so use optimized Gensim with BLAS if you value your time. +# In the usual case, where Gensim installation found a BLAS library for optimized +# bulk vector operations, this training on this tiny 300 document, ~60k word corpus +# should take just a few seconds. (More realistic datasets of tens-of-millions +# of words or more take proportionately longer.) If for some reason a BLAS library +# isn't available, training uses a fallback approach that takes 60x-120x longer, +# so even this tiny training will take minutes rather than seconds. (And, in that +# case, you should also notice a warning in the logging letting you know there's +# something worth fixing.) So, be sure your installation uses the BLAS-optimized +# Gensim if you value your time. # model.train(train_corpus, total_examples=model.corpus_count, epochs=model.epochs) From e656d77003c8fe36c1a217f9c5ec5661ed165a21 Mon Sep 17 00:00:00 2001 From: funasshi <54014411+funasshi@users.noreply.github.com> Date: Wed, 7 Dec 2022 11:50:47 +0900 Subject: [PATCH 26/34] Fix bug that prevents loading old models (#3359) * fix loading old gensim model by new model * Revert "fix loading old gensim model by new model" This reverts commit d398e92291e3de4d6d2a73016150cd9aa2e52714. * fix loading old gensim model by new model * Update word2vec.py * Update word2vec.py * Update word2vec.py Co-authored-by: Michael Penkov --- gensim/models/word2vec.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gensim/models/word2vec.py b/gensim/models/word2vec.py index d20bb63b1f..b172f57c8a 100755 --- a/gensim/models/word2vec.py +++ b/gensim/models/word2vec.py @@ -200,6 +200,9 @@ from gensim.models.keyedvectors import KeyedVectors, pseudorandom_weak_vector from gensim import utils, matutils +# This import is required by pickle to load models stored by Gensim < 4.0, such as Gensim 3.8.3. +from gensim.models.keyedvectors import Vocab # noqa + from smart_open.compression import get_supported_extensions logger = logging.getLogger(__name__) From ca8e4e8378bc489b0dda087dd9c0ed8f933ca3e2 Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Wed, 7 Dec 2022 12:09:49 +0900 Subject: [PATCH 27/34] refactor wheel building and testing workflow (#3410) handle windows separately - it's cleaner that way --- .github/workflows/build-wheels.yml | 195 +++++++++++++++++++---------- 1 file changed, 126 insertions(+), 69 deletions(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index cd0548fe83..dab4957b2f 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -36,7 +36,8 @@ jobs: - name: Check Sphinx Gallery cache run: python docs/src/check_gallery.py - build: + + multibuild: timeout-minutes: 35 runs-on: ${{ matrix.os }} defaults: @@ -48,9 +49,6 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] - os: [ubuntu-latest, macos-latest, windows-latest] - platform: [x64] include: # # We want the _oldest_ possible manylinux version to ensure our @@ -76,12 +74,12 @@ jobs: # - os: ubuntu-latest manylinux-version: 2010 - python-version: 3.8 + python-version: "3.8" build-depends: numpy==1.17.3 - os: ubuntu-latest manylinux-version: 2010 - python-version: 3.9 + python-version: "3.9" build-depends: numpy==1.19.3 - os: ubuntu-latest @@ -97,13 +95,13 @@ jobs: - os: macos-latest travis-os-name: osx manylinux-version: 1 - python-version: 3.8 + python-version: "3.8" build-depends: numpy==1.17.3 - os: macos-latest travis-os-name: osx manylinux-version: 1 - python-version: 3.9 + python-version: "3.9" build-depends: numpy==1.19.3 - os: macos-latest @@ -118,41 +116,23 @@ jobs: python-version: "3.11" build-depends: numpy==1.23.2 scipy==1.9.2 - - os: windows-latest - manylinux-version: 2010 - python-version: 3.8 - build-depends: numpy==1.17.3 - - - os: windows-latest - manylinux-version: 2010 - python-version: 3.9 - build-depends: numpy==1.19.3 - - - os: windows-latest - manylinux-version: 2010 - python-version: "3.10" - build-depends: numpy==1.22.2 scipy==1.8.0 - - - os: windows-latest - manylinux-version: 2010 - python-version: "3.11" - build-depends: numpy==1.23.2 scipy==1.9.2 - env: - PKG_NAME: gensim - REPO_DIR: gensim - BUILD_COMMIT: HEAD - PLAT: x86_64 - UNICODE_WIDTH: 32 - MB_PYTHON_VERSION: ${{ matrix.python-version }} # MB_PYTHON_VERSION is needed by Multibuild + SKIP_NETWORK_TESTS: 1 TEST_DEPENDS: pytest mock testfixtures + BUILD_DEPENDS: ${{ matrix.build-depends }} + + # + # For multibuild + # + BUILD_COMMIT: HEAD DOCKER_TEST_IMAGE: multibuild/xenial_x86_64 - TRAVIS_OS_NAME: ${{ matrix.travis-os-name }} - SKIP_NETWORK_TESTS: 1 MB_ML_VER: ${{ matrix.manylinux-version }} - WHEELHOUSE_UPLOADER_USERNAME: ${{ secrets.AWS_ACCESS_KEY_ID }} - WHEELHOUSE_UPLOADER_SECRET: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - BUILD_DEPENDS: ${{ matrix.build-depends }} + MB_PYTHON_VERSION: ${{ matrix.python-version }} # MB_PYTHON_VERSION is needed by Multibuild + PKG_NAME: gensim + PLAT: x86_64 + REPO_DIR: gensim + TRAVIS_OS_NAME: ${{ matrix.travis-os-name }} + UNICODE_WIDTH: 32 steps: - uses: actions/checkout@v3 @@ -175,8 +155,7 @@ jobs: run: | python -m pip install --upgrade pip pip install virtualenv - - name: Build Wheel (Multibuild) - if: matrix.os != 'windows-latest' + - name: Build Wheel run: | echo ::group::Set up Multibuild source multibuild/common_utils.sh @@ -191,31 +170,6 @@ jobs: build_wheel $REPO_DIR ${{ matrix.PLAT }} echo ::endgroup:: - # - # We can't use multibuild on Windows, so we have to roll our own build script. - # Adapted from - # https://github.com/RaRe-Technologies/gensim-wheels/commit/084b863390edee05bbe15d4ec05d1ab726e52202 - # - - name: Build Wheel (Windows) - if: matrix.os == 'windows-latest' - run: | - echo ::group::Set up dependencies - python --version - python -c "import struct; print(struct.calcsize('P') * 8)" - python -m pip install -U pip setuptools wheel wheelhouse_uploader ${{ env.BUILD_DEPENDS }} - echo ::endgroup:: - echo ::group::Build wheel - python setup.py bdist_wheel - echo ::endgroup - echo ::group::Install run - ls dist - python continuous_integration/install_wheel.py - echo ::endgroup:: - # - # For consistency with the multibuild step. - # - mv dist wheelhouse - - name: Prepare for testing run: | # @@ -232,8 +186,7 @@ jobs: # It also does not work under Windows. # So, we create our own simple test step here. # - - name: Install and Test Wheel (Linux, MacOS) - if: matrix.os != 'windows-latest' + - name: Install and Test Wheel run: | . test_environment/bin/activate python -m pip install --upgrade pip @@ -247,12 +200,113 @@ jobs: # pytest -rfxEXs --durations=20 --disable-warnings --showlocals --pyargs gensim + - name: Upload wheels to s3://gensim-wheels + # + # Only do this if the credentials are set. + # This means that PRs will still build wheels, but not upload them. + # (PRs do not have access to secrets). + # + # The always() ensures this step runs even if a previous step fails. + # We want to upload wheels whenever possible (even if e.g. tests failed) + # because we don't want an innocuous test failure from blocking a release. + # + if: ${{ always() && env.WHEELHOUSE_UPLOADER_USERNAME && env.WHEELHOUSE_UPLOADER_SECRET }} + run: | + python -m pip install wheelhouse-uploader + ls wheelhouse/*.whl + python -m wheelhouse_uploader upload --local-folder wheelhouse/ --no-ssl-check gensim-wheels --provider S3 --no-enable-cdn + env: + WHEELHOUSE_UPLOADER_USERNAME: ${{ secrets.AWS_ACCESS_KEY_ID }} + WHEELHOUSE_UPLOADER_SECRET: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + + # + # The build process for windows is different to that of Linux and MacOS. + # First, we cannot use multibuild (it does not support Windows). + # This means we have to write our own building and testing steps, but in a + # way it's simpler, because we don't need to care about configuring + # multibuild ourselves. + # Second, the syntax to enable virtual environments, etc. is different. + # + build_windows: + timeout-minutes: 35 + runs-on: windows-latest + defaults: + run: + shell: bash + + needs: [linters] + + strategy: + fail-fast: false + matrix: + include: + - python-version: "3.8" + build-depends: numpy==1.17.3 + + - python-version: "3.9" + build-depends: numpy==1.19.3 + + - python-version: "3.10" + build-depends: numpy==1.22.2 scipy==1.8.0 + + - python-version: "3.11" + build-depends: numpy==1.23.2 scipy==1.9.2 + + env: + SKIP_NETWORK_TESTS: 1 + TEST_DEPENDS: pytest mock testfixtures + BUILD_DEPENDS: ${{ matrix.build-depends }} + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install virtualenv + + - name: Build Wheel + run: | + echo ::group::Set up dependencies + python --version + python -c "import struct; print(struct.calcsize('P') * 8)" + python -m pip install -U pip setuptools wheel wheelhouse_uploader ${{ env.BUILD_DEPENDS }} + echo ::endgroup:: + echo ::group::Build wheel + python setup.py bdist_wheel + echo ::endgroup + echo ::group::Install run + ls dist + python continuous_integration/install_wheel.py + echo ::endgroup:: + # + # For consistency with the multibuild step. The wheel uploader expects + # the wheels to be under wheelhouse. + # + mv dist wheelhouse + + - name: Prepare for testing + run: | + # + # FIXME: Why are these eggs here? + # + # These eggs prevent the wheel from building and running on Py3.10 + # + find . -type f -name "*.egg" -exec rm -v {} \; + python -m venv test_environment + # # We need a separate testing step for windows because the command for # activating the virtual environment is slightly different # - name: Install and Test Wheel (Windows) - if: matrix.os == 'windows-latest' run: | test_environment/Scripts/activate.bat python -m pip install --upgrade pip @@ -277,3 +331,6 @@ jobs: python -m pip install wheelhouse-uploader ls wheelhouse/*.whl python -m wheelhouse_uploader upload --local-folder wheelhouse/ --no-ssl-check gensim-wheels --provider S3 --no-enable-cdn + env: + WHEELHOUSE_UPLOADER_USERNAME: ${{ secrets.AWS_ACCESS_KEY_ID }} + WHEELHOUSE_UPLOADER_SECRET: ${{ secrets.AWS_SECRET_ACCESS_KEY }} From 50a9e6b4cbba13fb08a8ec470a0297614c16d802 Mon Sep 17 00:00:00 2001 From: globba <114566111+globba@users.noreply.github.com> Date: Mon, 12 Dec 2022 14:40:02 +0100 Subject: [PATCH 28/34] Fixed issue when using add_vector with FastTextKeyedVectors (#3389) Since Gensim 4.0, 'key' in FastTextKeyedVectors always returns True by design. The proper way to check if a key already exists is with 'key' in FastTextKeyedVectors.key_to_index. Co-authored-by: dcarron --- gensim/models/keyedvectors.py | 2 +- gensim/test/test_fasttext.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/gensim/models/keyedvectors.py b/gensim/models/keyedvectors.py index e8045bcd97..b09f440f92 100644 --- a/gensim/models/keyedvectors.py +++ b/gensim/models/keyedvectors.py @@ -592,7 +592,7 @@ def add_vectors(self, keys, weights, extras=None, replace=False): in_vocab_mask = np.zeros(len(keys), dtype=bool) for idx, key in enumerate(keys): - if key in self: + if key in self.key_to_index: in_vocab_mask[idx] = True # add new entities to the vocab diff --git a/gensim/test/test_fasttext.py b/gensim/test/test_fasttext.py index 64bd636b3a..e07eaab9a1 100644 --- a/gensim/test/test_fasttext.py +++ b/gensim/test/test_fasttext.py @@ -1782,6 +1782,28 @@ def test_identity(self): self.assertTrue(np.all(np.array([6, 7, 8]) == n[2])) +class FastTextKeyedVectorsTest(unittest.TestCase): + def test_add_vector(self): + wv = FastTextKeyedVectors(vector_size=2, min_n=3, max_n=6, bucket=2000000) + wv.add_vector("test_key", np.array([0, 0])) + + self.assertEqual(wv.key_to_index["test_key"], 0) + self.assertEqual(wv.index_to_key[0], "test_key") + self.assertTrue(np.all(wv.vectors[0] == np.array([0, 0]))) + + def test_add_vectors(self): + wv = FastTextKeyedVectors(vector_size=2, min_n=3, max_n=6, bucket=2000000) + wv.add_vectors(["test_key1", "test_key2"], np.array([[0, 0], [1, 1]])) + + self.assertEqual(wv.key_to_index["test_key1"], 0) + self.assertEqual(wv.index_to_key[0], "test_key1") + self.assertTrue(np.all(wv.vectors[0] == np.array([0, 0]))) + + self.assertEqual(wv.key_to_index["test_key2"], 1) + self.assertEqual(wv.index_to_key[1], "test_key2") + self.assertTrue(np.all(wv.vectors[1] == np.array([1, 1]))) + + if __name__ == '__main__': logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.DEBUG) unittest.main() From 45d35eee1e7d0c9eb69ae76a99b2ed7cc35a1c0b Mon Sep 17 00:00:00 2001 From: Emil Rijcken <39989052+ERijck@users.noreply.github.com> Date: Mon, 12 Dec 2022 14:46:40 +0100 Subject: [PATCH 29/34] Flsamodel (#3398) * added flsamodel * added FuzzyTM to dependencies * flake8 * less dependencies and not imported from FuzzyTM * added flsamodel Co-authored-by: Michael Penkov --- gensim/models/flsamodel.py | 1696 ++++++++++++++++++++++++++++++++++++ setup.py | 1 + 2 files changed, 1697 insertions(+) create mode 100644 gensim/models/flsamodel.py diff --git a/gensim/models/flsamodel.py b/gensim/models/flsamodel.py new file mode 100644 index 0000000000..9ed815abe1 --- /dev/null +++ b/gensim/models/flsamodel.py @@ -0,0 +1,1696 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Oct 27 11:04:27 2022 + +@author: 20200016 +""" + +import math +from collections import Counter +import warnings +import pickle +import itertools +import numpy as np +from scipy.sparse.linalg import svds +from scipy.sparse import dok_matrix +from pyfume import Clustering +import gensim.corpora as corpora +from gensim.models.coherencemodel import CoherenceModel +from gensim.models import Word2Vec + + +class FlsaModel(): + """ + Class to initialize and train fuzzy topic models with methods similar + to Gensim's LdaModel' + + Parameters + ---------- + corpus : The input corpus. + either: list of list of str. + or: list of list of tuples (int, int) (bow). + + num_topics: int + The number of topics to be trained. + + algorithm: str ['flsa', 'flsa-w', 'flsa-e'] + The algorithm to train. + + id2word: gensim.corpora.dictionary.Dictionary + Object to map id's to words + (only used when the corpus is passed into the object as a bow). + + word_weighting: str ['normal', 'idf', 'probidf', 'entropy'] + Global term weighting mechanism. + + cluster_method: str ['fcm', 'gk', 'fst-pso'] + Fuzzy clustering method. + + svd_factors: int + The number of singular values to use. + """ + def __init__( + self, + corpus, + num_topics, + algorithm, + num_words=20, + word_weighting='normal', + cluster_method='fcm', + svd_factors=2, + id2word=None, + min_count=None, + window=None, + vector_size=None, + workers=None, + ): + self.corpus = self._set_corpus(corpus, id2word) + self.num_topics = num_topics + self.algorithm = algorithm + self.num_topics = num_topics + self.num_words = num_words + self.word_weighting = word_weighting + self.cluster_method = cluster_method + self.svd_factors = svd_factors + self.min_count = min_count + self.window = window + self.vector_size = vector_size + self.workers = workers + self._check_variables() + self._vocabulary, self._vocabulary_size = self._create_vocabulary(self.corpus) + self._word_to_index, self._index_to_word = self._create_index_dicts(self._vocabulary) + self._sum_words = self._create_sum_words(self.corpus) + self._prob_word_i = None + self._prob_document_j = None + self._prob_topic_k = None + self._prob_word_given_topic = None + self._prob_word_given_document = None + self.coherence_score = None + self.diversity_score = None + self.pwgt, self.ptgd = self._get_matrices() + + def _set_corpus( + self, + corpus, + id2word, + ): + """ + Method that sets the corpus to FuzzyTM's required input format. + If a list of list of str is passed into the method for corpus, then + it returns the same corpus. If a bow (list of list of tuples) is passed + into the class, it transforms this into a list of list of str. + + Parameters + ---------- + corpus : either: list of list of str (tokens). or: list of list of tuples (int, int). + The input corpus. + id2word: gensim.corpora.dictionary.Dictionary + Object to map id's to words + (only used when the corpus is passed into the object as a bow) + + Returns + ------- + list of list of str + The corpus in FuzzyTM's required input format. + """ + if self._check_bow(corpus): + if not isinstance(id2word, corpora.dictionary.Dictionary): + raise ValueError("Please pass 'id2word' when using a bow for 'corpus'.") + return self._convert_bow(corpus, id2word) + return corpus + + @staticmethod + def _check_bow( + corpus, + ): + """ + Method to check if the input format has the bow format. + + Parameters + ---------- + corpus : either: list of list of str (tokens). or: list of list of tuples (int, int). + The input corpus. + + Returns + ------- + bool + True if bow format + """ + if not isinstance(corpus, list): + return False + for doc in corpus: + if not isinstance(doc, list): + return False + for tup in doc: + if not isinstance(tup, tuple): + return False + if not isinstance(tup[0], int) or not isinstance(tup[1], int): + return False + return True + + @staticmethod + def _convert_bow( + corpus, + id2word, + ): + """ + Method to convert the bow format into a list of list of str. + + Parameters + ---------- + corpus : The input corpus. + either: list of list of str (tokens). + or: list of list of tuples (int, int). + + id2word: gensim.corpora.dictionary.Dictionary + Object to map id's to words + + Returns + ------- + list of list of str + The corpus in FuzzyTM's required input format. + """ + id2token = {v: k for k, v in id2word.token2id.items()} + data_list = [] + for doc in corpus: + doc_list = [] + for tup in doc: + for _ in itertools.repeat(None, tup[1]): + doc_list.append(id2token[tup[0]]) + data_list.append(doc_list) + return data_list + + def _check_variables(self): + """ + Check whether the input data has the right format. + + Correct format: list of list of str (tokens) + The function raises an error if the format is incorrect. + """ + for i, doc in enumerate(self.corpus): + if not isinstance(doc, list): + raise TypeError("corpus variable at index ", + str(i), + " is not a list") + if not len(doc) > 0: + raise ValueError( + "The corpus has an empty list at index ", + str(i), + " and should contain at least one str value") + for j, word in enumerate(doc): + if not isinstance(word, str): + raise TypeError(f"Word {j} of document {i} is not a str") + if not isinstance(self.num_topics, int) or self.num_topics < 1: + raise ValueError("Please use a positive int for num_topics") + if not isinstance(self.num_words, int) or self.num_words < 1: + raise ValueError("Please use a positive int for num_words") + if self.algorithm in [ + "flsa", + "flsa-w", + ] and self.word_weighting not in [ + "entropy", + "idf", + "normal", + "probidf", + ]: + warning = ["Invalid word weighting method", + "Please choose between:", + "'entropy', 'idf', 'normal' and'probidf'", + ] + raise ValueError(' '.join(warning)) + if self.cluster_method not in [ + "fcm", + "fst-pso", + "gk", + ]: + raise ValueError( + "Invalid 'cluster_method. Please choose: 'fcm', 'fst-pso' or 'gk'") + if not isinstance(self.svd_factors, int) and self.svd_factors > 0: + raise ValueError("Please use a positive int for svd_factors") + if self.algorithm not in [ + 'flsa', + 'flsa-w', + 'flsa-e', + ]: + raise ValueError('Please select a correct "algoritm"') + + @staticmethod + def _create_vocabulary(corpus): + """ + Create the vocabulary from 'corpus'. + + Parameters + ---------- + corpus : list of lists of str + The input file used to initialize the model. + + Returns + ------- + set of str + All the vocabulary words. + """ + vocabulary = set(el for lis in corpus for el in lis) + return vocabulary, len(vocabulary) + + @staticmethod + def _create_index_dicts(vocabulary): + """ + Create the dictionaries with mappings between words and indices. + + Parameters + ---------- + vocabulary : set of str + All the words in the corpus. + + Returns + ------- + dict of {str : int} + Dictionary that maps a vocabulary word to and index number. + dict of {int : str} + Dictionary that maps an index number to each vocabulary word. + """ + if not isinstance(vocabulary, set): + raise ValueError("Please use a 'set' type for 'vocabulary'.") + word_to_index = dict() + index_to_word = dict() + for i, word in enumerate(vocabulary): + word_to_index[word] = i + index_to_word[i] = word + return word_to_index, index_to_word + + @staticmethod + def _create_sum_words(corpus): + """ + Creates a Counter object that stores the count of each word in the corpus (corpus). + + Parameters + ---------- + corpus : list of lists of str + The input file used to initialize the model. + + Returns + ------- + collections.Counter {str : int} + The count of each word in the corpus. + """ + sum_words = Counter() + for document in corpus: + sum_words.update(Counter(document)) + return sum_words + + @staticmethod + def _create_sparse_local_term_weights( + corpus, + vocabulary_size, + word_to_index, + ): + """ + Creates a sparse matrix showing the frequency of each words in documents. + + (See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.dok_matrix.html) + Axes: + rows: documents (size: number of documents in corpus) + columns: words (size: vocabulary length) + + Parameters + ---------- + corpus : list of lists of str + The input file used to initialize the model. + vocabulary_size : int + Number of unique words in the corpus. + word_to_index: dict {str : int} + Maps each unique vocabulary word to a unique index number. + + Returns + ------- + scipy.sparse.dok_matrix + sparse matrix representation of the local term weights. + """ + sparse_local_term_weights = dok_matrix( + (len(corpus), + vocabulary_size), + dtype=np.float32, + ) + for document_index, document in enumerate(corpus): + document_counter = Counter(document) + for word in document_counter.keys(): + sparse_local_term_weights[ + document_index, word_to_index[word], + ] = document_counter[word] + return sparse_local_term_weights + + def _create_sparse_global_term_weights( + self, + corpus, + word_weighting, + vocabulary_size=None, + sparse_local_term_weights=None, + index_to_word=None, + word_to_index=None, + sum_words=None, + ): + """ + Apply a word_weighting method on the sparse_local_term_weights + to create sparse_global_term_weights. + (See: https://link.springer.com/article/10.1007/s40815-017-0327-9) + + Parameters + ---------- + corpus : list of lists of str + The input file used to initialize the model. + word_weighting : str + Indicates the method used for word_weighting. Choose from: + - entropy + - normal + - idf + - probidf + vocabulary_size : int + Number of unique words in the corpus. + sparse_local_term_weights : scipy.sparse.dok_matrix + A sparse matrix showing the frequency of each words in documents. + word_to_index : dict {str : int} + Maps each unique vocabulary word to a unique index number. + index_to_word : dict {int : str} + Maps each unique index number to a unique vocabulary word. + sum_words : collections.Counter {str : int} + The count of each word in the corpus. + + Returns + ------- + scipy.sparse.dok_matrix + sparse matrix representation of the global term weights. + """ + num_documents = len(corpus) + if word_weighting in ['entropy', 'normal']: + if sparse_local_term_weights is None: + raise ValueError("Please feed the algorithm 'sparse_local_term_weights'") + if word_weighting in ['entropy']: + if index_to_word is None: + raise ValueError("Please feed the algorithm 'index_to_word'") + if sum_words is None: + raise ValueError("Please feed the algorithm 'sum_words'") + if word_weighting in ['entropy', 'idf', 'probidf']: + if vocabulary_size is None: + raise ValueError("Please feed the algorithm 'vocabulary_size'") + if word_weighting in ['idf', 'probidf']: + if word_to_index is None: + raise ValueError("Please feed the algorithm 'word_to_index'") + if word_weighting == 'entropy': + global_term_weights = self._calculate_entropy( + num_documents, + vocabulary_size, + sparse_local_term_weights, + index_to_word, sum_words, + ) + elif word_weighting == 'idf': + global_term_weights = self._calculate_idf( + num_documents, + vocabulary_size, + corpus, + word_to_index, + ) + elif word_weighting == 'normal': + global_term_weights = self._calculate_normal(sparse_local_term_weights) + elif word_weighting == 'probidf': + global_term_weights = self._calculate_probidf( + num_documents, + vocabulary_size, + corpus, + word_to_index, + ) + else: + raise ValueError('Invalid word weighting method') + return sparse_local_term_weights.multiply(global_term_weights).tocsc() + + def _calculate_entropy( + self, + num_documents, + vocabulary_size, + sparse_local_term_weights, + index_to_word, + sum_words, + ): + """ + Use the entropy word weighting method. + + (See: https://link.springer.com/article/10.1007/s40815-017-0327-9) + + Parameters + ---------- + num_documents : int + The number of documents in the corpus. + vocabulary_size : int + Number of unique words in the corpus. + sparse_local_term_weights : scipy.sparse.dok_matrix + A sparse matrix showing the frequency of each words in documents. + index_to_word : dict {int : str} + Maps each unique index number to a unique vocabulary word. + sum_words : collections.Counter {str : int} + The count of each word in the corpus. + + Returns + ------- + numpy.array : float + """ + p_log_p_ij = self._create_p_log_p_ij( + num_documents, + vocabulary_size, + sparse_local_term_weights, + index_to_word, + sum_words, + ) + summed_p_log_p = p_log_p_ij.sum(0).tolist()[0] + return np.array([1 + summed_p_log_p_i / np.log2(num_documents) for summed_p_log_p_i in summed_p_log_p]) + + def _calculate_idf( + self, + num_documents, + vocabulary_size, + corpus, + word_to_index, + ): + """ + Use the idf word weightingg method. + + (See: https://link.springer.com/article/10.1007/s40815-017-0327-9) + + Parameters + ---------- + num_documents : int + The number of documents in the corpus. + vocabulary_size : int + Number of unique words in the corpus. + corpus : list of lists of str + The input file used to initialize the model. + word_to_index: dict {str : int} + Maps each unique vocabulary word to a unique index number. + + Returns + ------- + numpy.array : float + """ + binary_sparse_dtm = self._create_sparse_binary_dtm( + num_documents, + vocabulary_size, + corpus, + word_to_index, + ) + summed_words = binary_sparse_dtm.sum(0).tolist()[0] + return np.array([np.log2(num_documents / word_count) for word_count in summed_words]) + + @staticmethod + def _calculate_normal( + sparse_local_term_weights, + ): + """ + Use the normal word weightingg method. + + (See: https://link.springer.com/article/10.1007/s40815-017-0327-9) + + Parameters + ---------- + sparse_local_term_weights : scipy.sparse.dok_matrix + A sparse matrix showing the frequency of each words in documents. + + Returns + ------- + numpy.array : float + """ + squared_dtm = sparse_local_term_weights.multiply(sparse_local_term_weights) + summed_words = squared_dtm.sum(0).tolist()[0] + return np.array([1 / (math.sqrt(word_count)) for word_count in summed_words]) + + def _calculate_probidf( + self, + num_documents, + vocabulary_size, + corpus, + word_to_index, + ): + """ + Use the probidf word weightingg method. + + (See: https://link.springer.com/article/10.1007/s40815-017-0327-9) + + Parameters + ---------- + num_documents : int + The number of documents in the corpus. + vocabulary_size : int + Number of unique words in the corpus. + corpus : list of lists of str + The input file used to initialize the model. + word_to_index: dict {str : int} + Maps each unique vocabulary word to a unique index number. + + Returns + ------- + numpy.array : float + """ + binary_sparse_dtm = self._create_sparse_binary_dtm( + num_documents, + vocabulary_size, + corpus, + word_to_index, + ) + summed_binary_words_list = binary_sparse_dtm.sum(0).tolist()[0] + + return np.array([np.log2((num_documents - binary_word_count) / binary_word_count) + for binary_word_count in summed_binary_words_list]) + + @staticmethod + def _create_p_log_p_ij( + num_documents, + vocabulary_size, + sparse_local_term_weights, + index_to_word, + sum_words, + ): + """ + Create probability of word i in document j, multiplied by its base-2 logarithm. + + (See: https://link.springer.com/article/10.1007/s40815-017-0327-9) + + Parameters + ---------- + num_documents : int + The number of documents in the corpus. + vocabulary_size : int + Number of unique words in the corpus. + sparse_local_term_weights : scipy.sparse.dok_matrix + A sparse matrix showing the frequency of each words in documents. + index_to_word : dict {int : str} + Maps each unique index number to a unique vocabulary word. + sum_words : collections.Counter {str : int} + The count of each word in the corpus. + + Returns + ------- + scipy.sparse.dok_matrix + """ + p_log_p_ij = dok_matrix( + (num_documents, vocabulary_size), dtype=np.float32, + ) + for j in range(num_documents): + row_counts = sparse_local_term_weights.getrow(j).toarray()[0] + word_index = row_counts.nonzero()[0] + non_zero_row_counts = row_counts[row_counts != 0] + for i, count in enumerate(non_zero_row_counts): + word = index_to_word[word_index[i]] + prob_ij = count / sum_words[word] + p_log_p_ij[j, word_index[i]] = prob_ij * np.log2(prob_ij) + return p_log_p_ij + + @staticmethod + def _create_sparse_binary_dtm( + num_documents, + vocabulary_size, + corpus, + word_to_index, + ): + """ + Create a binary sparse document-term-matrix (used for idf and probidf). + + (See: https://link.springer.com/article/10.1007/s40815-017-0327-9) + + Parameters + ---------- + num_documents : int + The number of documents in the corpus. + vocabulary_size : int + Number of unique words in the corpus. + corpus : list of lists of str + The input file used to initialize the model. + word_to_index: dict {str : int} + Maps each unique vocabulary word to a unique index number. + + Returns + ------- + scipy.sparse.dok_matrix + """ + binary_sparse_dtm = dok_matrix( + (num_documents, vocabulary_size), dtype=np.float32, + ) + for doc_index, document in enumerate(corpus): + binary_document_counter = dict.fromkeys(document, 1) + for word in set(document): + binary_sparse_dtm[doc_index, + word_to_index[word]] = binary_document_counter[word] + return binary_sparse_dtm + + @staticmethod + def _create_projected_data( + algorithm, + sparse_weighted_matrix, + svd_factors, + ): + """ + Perform singular decomposition for dimensionality reduction. + + (See: https://web.mit.edu/be.400/www/SVD/Singular_Value_Decomposition.htm) + For SVD on a sparse matrix, the sparsesvd package is used + (https://pypi.org/project/sparsesvd/) + + Parameters + ---------- + algorithm : str + Indicator for which algorithm is being trained ('flsa' or 'flsa-w'). + sparse_weighted_matrix : scipy.sparse.dok_matrix + Sparse global term matrix. + svd_factors : int + The number of singular values to include. + + Returns + ------- + numpy.array : float + """ + svd_u, _, svd_v = svds( + sparse_weighted_matrix, + svd_factors, + ) + if algorithm in ['flsa']: + return svd_u + if algorithm in ['flsa-w']: + return svd_v.T + raise ValueError('Invalid algorithm selected.', + 'Only "flsa" ans "flsa-w" are currently supported.') + + @staticmethod + def _create_partition_matrix( + data, + number_of_clusters, + method='fcm', + ): + """ + Perform clustering on the projected data. + + The pyFUME package is used for clustering: + (https://pyfume.readthedocs.io/en/latest/Clustering.html) + + Parameters + ---------- + data: numpy.array + The output from self._create_projected_data(). + number_of_clusters : int + The number of clusters (topics). + method : str + The cluster method, choose from: 'fcm', 'gk', 'fst-pso'. + Returns + ------- + numpy.array : float + """ + clusterer = Clustering.Clusterer( + nr_clus=number_of_clusters, + data=data, + ) + _, partition_matrix, _ = clusterer.cluster(method=method) + return partition_matrix + + @staticmethod + def _create_prob_document_j(sparse_matrix): + """ + Get the probability of document j. + + Parameters + ---------- + sparse_matrix : scipy.sparse.dok_matrix + A sparse matrix representation of the global term weights. + Returns + ------- + numpy.array : float + (shape: number of documents x 1) + """ + # Vector with the length of num_document, + # each cell represents the sum of all weights of a document + document_sum = np.array([doc[0] for doc in sparse_matrix.sum(1).tolist()]) + # sum of all the elements in the weighted matrix + total_sum_d = sum(sparse_matrix.sum(0).tolist()[0]) + return document_sum / total_sum_d # normalized probability + + @staticmethod + def _create_prob_word_i(sparse_matrix): + """ + Get the probability of word i. + + Parameters + ---------- + sparse_matrix : scipy.sparse.dok_matrix + A sparse matrix representation of the global term weights. + + Returns + ------- + numpy.array : float + (shape: vocabulary_size x 1) + """ + word_sum = np.array(sparse_matrix.sum(0).tolist()) + # Sum of all the elements in the weighted matrix + total_sum_w = sum(sparse_matrix.sum(0).tolist()[0]) + return (word_sum / total_sum_w)[0] # normalized probability + + @staticmethod + def _create_prob_topic_k( + prob_topic_given_word_transpose, + prob_word_i, + ): + """ + Get the probability of topic k. + + Parameters + ---------- + prob_topic_given_word_transpose : numpy.array : float + The output from self._create_partition_matrix(). + prob_word_i : numpy.array : float + The output from self._create_prob_word_i(). + + Returns + ------- + numpy.array : float + (shape: 1 x number of topics) + """ + return np.matmul(prob_topic_given_word_transpose.T, prob_word_i) + + @staticmethod + def _check_passed_variables( + algorithm, + prob_topic_given_document_transpose, + prob_topic_given_word_transpose, + local_term_weights, + global_term_weights, + ): + """ + Check whether the algorithms are being fed the right attributes. + """ + if algorithm in ['flsa']: + if prob_topic_given_document_transpose is None: + raise ValueError("Please feed the method", + "'prob_topic_given_document_transpose' to run flsa") + if global_term_weights is None: + raise ValueError("Please feed the method 'global_term_weights', to run flsa") + elif algorithm in ['flsa-w']: + if prob_topic_given_word_transpose is None: + raise ValueError("Please feed the method", + "'prob_topic_given_word_transpose' to run flsa-w") + if global_term_weights is None: + raise ValueError("Please feed the method 'global_term_weights'", + " to run flsa-w") + elif algorithm in [ + 'flsa-e', + ]: + if prob_topic_given_word_transpose is None: + raise ValueError("Please feed the method", + "'prob_topic_given_word_transpose' to run model") + if local_term_weights is None: + raise ValueError("Please feed the method 'local_term_weights', to run model") + + else: + raise ValueError('Your algorithm is currently not supported') + + def _create_probability_matrices( + self, + algorithm, + prob_topic_given_document_transpose=None, + prob_topic_given_word_transpose=None, + local_term_weights=None, + global_term_weights=None, + ): + """ + Method that performs matrix multiplications to obtain the output matrices. + + The 'algorithm' parameter is generic and the other ones depend on the selected algorithm. + The other parameters passed into this method depend on the used algorithm. + + Parameters + ---------- + algorithm : str + Indicator for which algorithm is being trained ('flsa' or 'flsa-w'). + global_term_weights : scipy.sparse.dok_matrix + The output from self._create_partition_matrix(). + prob_topic_given_document_transpose : numpy.array : float + The output from self._create_partition_matrix() (flsa) + prob_topic_given_word_transpose : numpy.array : float + (flsa-w) + + Returns + ------- + numpy.array : float + The prbability of a word given a topic. + numpy.array : float + The prbability of a topic given a document. + """ + # Check whether the right variable are passed into the method. + self._check_passed_variables( + algorithm, + prob_topic_given_document_transpose, + prob_topic_given_word_transpose, + local_term_weights, + global_term_weights, + ) + + # Calculate the initial probabilities + if algorithm in [ + 'flsa', + 'flsa-w', + ]: + self._prob_word_i = self._create_prob_word_i(global_term_weights) + self._prob_document_j = self._create_prob_document_j(global_term_weights) + if algorithm in ['flsa-w']: + self._prob_topic_k = self._create_prob_topic_k( + prob_topic_given_word_transpose, + self._prob_word_i, + ) + elif algorithm in [ + 'flsa-e', + ]: + self._prob_word_i = self._create_prob_word_i(local_term_weights) + self._prob_document_j = self._create_prob_document_j(local_term_weights) + self._prob_topic_k = self._create_prob_topic_k( + prob_topic_given_word_transpose, self._prob_word_i, + ) + if algorithm in ['flsa']: + prob_document_and_topic = (prob_topic_given_document_transpose.T * self._prob_document_j).T + prob_document_given_topic = prob_document_and_topic / prob_document_and_topic.sum(axis=0) + self._prob_word_given_document = np.asarray(global_term_weights / global_term_weights.sum(1)) + self._prob_word_given_topic = np.matmul( + self._prob_word_given_document.T, + prob_document_given_topic, + ) + prob_topic_given_document = prob_topic_given_document_transpose.T + return self._prob_word_given_topic, prob_topic_given_document + + elif algorithm in [ + 'flsa-w', + 'flsa-e' + ]: + prob_word_and_topic = (prob_topic_given_word_transpose.T * self._prob_word_i).T + self._prob_word_given_topic = prob_word_and_topic / prob_word_and_topic.sum(axis=0) + if algorithm in ['flsa-w']: + self._prob_word_given_document = np.asarray(global_term_weights / global_term_weights.sum(1)).T + elif algorithm in [ + 'flsa-e', + ]: + self._prob_word_given_document = np.asarray(local_term_weights / local_term_weights.sum(1)).T + prob_document_given_word = ((self._prob_word_given_document * self._prob_document_j).T + / np.array(self._prob_word_i)) + prob_document_given_topic = np.matmul( + prob_document_given_word, + self._prob_word_given_topic, + ) + prob_topic_given_document = ((prob_document_given_topic * self._prob_topic_k).T + / self._prob_document_j) + return self._prob_word_given_topic, prob_topic_given_document + raise ValueError('"algorithm" is unknown.') + + @staticmethod + def _create_dictlist_topn( + topn, + prob_word_given_topic, + index_to_word, + ): + """ + Creates a list with dictionaries of word probabilities + per topic based on the top-n words. + + Parameters + ---------- + topn : int + The top-n words to include + (needs only to be used when 'method=topn'). + prob_word_given_topic : numpy.array : float + Matrix that gives the probability of a word given a topic. + index_to_word : dict {int : str} + Maps each unique index number to a unique vocabulary word. + + Returns + ------- + list of dicts {int : float} + Keys: all the indices of words from prob_word_given_topic + who's weight's are amongst the top percentage. + Values: the probability associated to a word. + """ + if not isinstance(topn, int) and topn > 0: + raise ValueError("Please choose a positive integer for 'topn'") + top_dictionaries = [] + for topic_index in range(prob_word_given_topic.shape[1]): + new_dict = dict() + highest_weight_indices = prob_word_given_topic[:, topic_index].argsort()[-topn:] + for word_index in highest_weight_indices: + new_dict[index_to_word[word_index]] = prob_word_given_topic[ + word_index, topic_index, + ] + top_dictionaries.append(new_dict) + return top_dictionaries + + @staticmethod + def _create_dictlist_percentile( + perc, + prob_word_given_topic, + index_to_word, + ): + """ + Create a list with dictionaries of word probabilities + per topic based on the percentile. + - Keys: all the indices of words from prob_word_given_topic + who's weight's are amongst the top percentage. + - Values: the probability associated to a word. + + Parameters + ---------- + perc : float + The top percentile words to include + (needs only to be used when 'method=percentile'). + prob_word_given_topic : numpy.array : float + Matrix that gives the probability of a word given a topic. + index_to_word : dict {int : str} + Maps each unique index number to a unique vocabulary word. + + Returns + ------- + list of dicts {int : float} + Keys: all the indices of words from prob_word_given_topic + who's weight's are amongst the top percentage. + Values: the probability associated to a word. + """ + if not isinstance(perc, float) and 0 <= perc <= 1: + raise ValueError("Please choose a number between 0 and 1 for 'perc'") + top_list = [] + for top in range(prob_word_given_topic.shape[1]): + new_dict = dict() + count = 0 + i = 0 + weights = np.sort(prob_word_given_topic[:, top])[::-1] + word_indices = np.argsort(prob_word_given_topic[:, top])[::-1] + while count < perc: + new_dict[index_to_word[word_indices[i]]] = weights[i] + count += weights[i] + i += 1 + top_list.append(new_dict) + return top_list + + def show_topics( + self, + formatted=True, + prob_word_given_topic=None, + num_words=-1, + index_to_word=None, + ): + """ + Get a representation for the topics. + + Parameters + ---------- + formatted : bool + Whether the topic representations should be formatted as strings. + If False, they are returned as 2 tuples of (word, probability). + prob_word_given_topic : numpy.array : float + Matrix that gives the probability of a word given a topic. + num_words : int + Indicates how many words per topic should be shown. + index_to_word : dict {int : str} + Maps each unique index number to a unique vocabulary word. + + Returns + ------- + list of tuples (int, str) + The produced topics. + """ + if prob_word_given_topic is None: + prob_word_given_topic = self._prob_word_given_topic + + if num_words < 0: + num_words = self.num_words + if index_to_word is None: + index_to_word = self._index_to_word + if not isinstance(prob_word_given_topic, np.ndarray): + raise TypeError("Please feed the algorithm 'prob_word_given_topic' as a np.ndarray") + if not isinstance(index_to_word, dict): + raise TypeError("Please feed the algorithm 'index_to_word' as a dict") + if not isinstance(num_words, int) or num_words <= 0: + raise TypeError("Please use a positive int for 'num_words'.") + if prob_word_given_topic.shape[0] < prob_word_given_topic.shape[1]: + raise ValueError("'prob_word_given_topic' has more columns then rows,", + " probably you need to take the transpose.") + warning = ["It seems like 'prob_word_given_topic' and 'index_to_word", + "are not aligned. The number of vocabulary words in", + "'prob_word_given_topic' deviate from the ", + "number of words in 'index_to_word'."] + if prob_word_given_topic.shape[0] != len(index_to_word.keys()): + warnings.warn(' '.join(warning)) + if not isinstance(formatted, bool): + raise ValueError('Please choose a boolean for "formatted"') + topic_list = [] + if not formatted: + for topic_index in range(prob_word_given_topic.shape[1]): + weight_words = "" + sorted_highest_weight_indices = prob_word_given_topic[:, topic_index].argsort()[-num_words:][::-1] + for word_index in sorted_highest_weight_indices: + weight_words += (str(round(prob_word_given_topic[word_index, topic_index], 4)) + + '*"' + index_to_word[word_index] + '" + ') + topic_list.append((topic_index, weight_words[:-3])) + return topic_list + else: + for topic_index in range(prob_word_given_topic.shape[1]): + word_list = [] + sorted_highest_weight_indices = prob_word_given_topic[:, topic_index].argsort()[-num_words:][::-1] + for word_index in sorted_highest_weight_indices: + word_list.append(index_to_word[word_index]) + topic_list.append(word_list) + return topic_list + + def get_topic_embedding( + self, + corpus, + prob_word_given_topic=None, + method='topn', + topn=20, + perc=0.05, + ): + """ + Create a topic embedding for each input document, + to be used as input to predictive models. + + Parameters + ---------- + corpus : list of lists of str + The input file used to initialize the model. + prob_word_given_topic : numpy.array : float + Matrix that gives the probability of a word given a topic. + method : str + Method to select words to be included in the embedding. + (choose from 'topn', 'percentile'): + - topn: for each topic the top n words with the highest + probability are included. + - percentile: for each topic all words with highest + probabilities are assigned while the cumulative + probability is lower than the percentile. + topn : int + The top-n words to include + (needs only to be used when 'method=topn'). + perc: float + The benchmark percentile until which words need to be added + (between 0 and 1). + + Returns + ------- + numpy.array : float + Array in which each row gives the topic embedding for + the associated document. + """ + self._check_variables() + if prob_word_given_topic is None: + prob_word_given_topic = self._prob_word_given_topic + top_dist = [] + if method not in ['topn', 'percentile']: + raise ValueError(method, "is not a valid option for 'method'.", + " Choose either 'topn' or 'percentile'") + if method == 'topn': + dictlist = self._create_dictlist_topn( + topn, prob_word_given_topic, self._index_to_word, + ) + else: + dictlist = self._create_dictlist_percentile( + perc, prob_word_given_topic, self._index_to_word, + ) + for doc in corpus: + topic_weights = [0] * prob_word_given_topic.shape[1] + for word in doc: + for i in range(prob_word_given_topic.shape[1]): + topic_weights[i] += dictlist[i].get(word, 0) + top_dist.append(topic_weights) + return np.array(top_dist) + + def get_coherence_score( + self, + corpus=None, + topics=None, + coherence='c_v', + ): + """ + Calculate the coherence score for the generated topic. + + Parameters + ---------- + corpus : list of lists of str + The input file used to initialize the model. + topics : list of lists of str + The words per topics, + equivalent to self.show_topics(formatted=True). + coherence : str + The type of coherence to be calculated. + Choose from: 'u_mass', 'c_v', 'c_uci', 'c_npmi'. + + Returns + ------- + float + The coherence score. + """ + if corpus is None and topics is None: + corpus = self.corpus + topics = self.show_topics(formatted=True) + + id2word = corpora.Dictionary(corpus) + corpus_bow = [id2word.doc2bow(text) for text in corpus] + self.coherence_score = CoherenceModel( + topics=topics, + texts=corpus, + corpus=corpus_bow, + dictionary=id2word, + coherence=coherence, + topn=len(topics[0]), + ).get_coherence() + return self.coherence_score + + def get_diversity_score( + self, + topics=None, + ): + """'' + Calculate the diversity score for the generated topic. + + Diversity = number of unique words / number of total words. + See: https://tinyurl.com/2bs84zd8 + + Parameters + ---------- + topics : list of lists of str + The words per topics, + equivalent to self.show_topics(formatted=True). + + Returns + ------- + float + The diversity score. + """ + if topics is None: + topics = self.show_topics(formatted=True) + unique_words = set() + total_words = 0 + for top in topics: + unique_words.update(top) + total_words += len(top) + self.diversity_score = len(unique_words) / total_words + return self.diversity_score + + def get_interpretability_score( + self, + corpus=None, + topics=None, + coherence='c_v', + ): + """'' + Calculate the interpretability score for the generated topics. + + Interpretability = coherence * diversity. + (see: https://tinyurl.com/2bs84zd8) + + Parameters + ---------- + corpus : list of lists of str + The input file used to initialize the model. + topics : list of lists of str + The words per topics, equivalent to + self.show_topics(formatted=True). + coherence : str + The type of coherence to be calculated. + Choose from: 'u_mass', 'c_v', 'c_uci', 'c_npmi'. + + Returns + ------- + float + The interpretability score. + """ + if corpus is None and topics is None: + corpus = self.corpus + topics = self.show_topics(formatted=True) + if self.coherence_score is None: + self.coherence_score = self.get_coherence_score( + corpus, + topics, + coherence, + ) + if self.diversity_score is None: + self.diversity_score = self.get_diversity_score(topics) + return self.coherence_score * self.diversity_score + + def get_vocabulary(self): + """ + Returns a set of all the words in the corpus + + Example: + After initializing an instance of the flsamodel models as 'model' + + corpus = [['this', 'is', 'the', 'first', 'file'], + ['and', 'this', 'is', 'second', 'file']] + + model.get_vocabulary() + + >>> {'this', 'is', 'the', 'first', 'file', 'and', 'second'} + """ + return self._vocabulary + + def get_topics(self): + """ + Get the term-topic matrix. + + Returns + ------- + numpy.ndarray + The probability for each word in each topic, + shape (num_topics, vocabulary_size). + """ + return self.pwgt + + def get_vocabulary_size(self): + """ + Returns the number of words in the vocabulary + + Example: + After initializing an instance of the flsamodel models as 'model' + + corpus = [['this', 'is', 'the', 'first', 'file'], + ['and', 'this', 'is', 'second', 'file']] + + model.get_vocabulary_size() + + >>> 7 + """ + return self._vocabulary_size + + def get_word_to_index(self): + """ + Obtain a dictionary that maps each vocabulary word to an index. + + Returns + ------- + dict of {str : int} + word to int mapping. + """ + return self._word_to_index + + def get_index_to_word(self): + """ + Obtain a dictionary that maps index numbers to vocabulary words. + + Returns + ------- + dict of {int : str} + int to word mapping. + """ + return self._index_to_word + + def get_corpus(self): + """ + Return the input file. + + Returns + ------- + list of list of str + The input file 'corpus'. + """ + return self.corpus + + def get_prob_word_i(self): + """ + Return the probabilities per word. + + Returns + ------- + np.array of float + The probabilities per word. + """ + return self._prob_word_i + + def get_prob_document_j(self): + """ + Return the probabilities per document. + + Returns + ------- + np.array of float + The probabilities per document. + """ + return self._prob_document_j + + def get_prob_topic_k(self): + """ + Return the probabilities per topic. + + Returns + ------- + np.array of float + The probabilities per topic. + """ + return self._prob_topic_k + + def save( + self, + filepath, + ): + """'' + Saves the object to the drive, using the pickle library. + + Parameters + ---------- + filepath : str + The directory in which the file should be stored, + either with or without the file name. + + Returns + ------- + float + The interpretability score. + """ + if not isinstance(filepath, str): + raise ValueError('Make sure that "filepath" has type "str"') + if filepath.endswith('.pickle'): + pickle_out = open(filepath, 'wb') + elif filepath.endswith('/'): + pickle_out = open(filepath + 'model.pickle', 'wb') + else: + pickle_out = open(filepath + '.pickle', 'wb') + pickle.dump(self, pickle_out) + pickle_out.close() + + def load( + self, + filepath, + ): + """'' + Loads the object from the drive, using the pickle library. + + Parameters + ---------- + filepath : str + The directory in which the file should be stored, + either with or without the file name. + + Returns + ------- + float + The interpretability score. + """ + if not isinstance(filepath, str): + raise ValueError('Make sure that "filepath" has type "str"') + if not filepath.endswith('.pickle'): + if filepath.endswith('/'): + filepath += 'model.pickle' + else: + filepath += '/model.pickle' + infile = open(filepath, 'rb') + self.__dict__ = pickle.load(infile).__dict__ + infile.close() + + +class Flsa(FlsaModel): + """ + Class to run the FLSA algorithm (see: https://tinyurl.com/mskjaeuu). + + Parameters + ---------- + corpus : list of lists of str + The input file used to initialize the model. + num_topics : int + The number of topics that the model should train. + num_words : int + Indicates how many words per topic should be shown. + word_weighting : str + Indicates the method used for word_weighting. Choose from: + - entropy + - normal + - idf + - probidf + svd_factors : int + The number of singular values to include. + cluster_method : str + The cluster algorithm to be used ('fcm', 'gk', 'fst-pso'). + """ + def __init__( + self, + corpus, + num_topics, + num_words=10, + word_weighting='normal', + svd_factors=2, + cluster_method='fcm', + ): + super().__init__( + algorithm='flsa', + corpus=corpus, + num_topics=num_topics, + num_words=num_words, + word_weighting=word_weighting, + cluster_method=cluster_method, + svd_factors=svd_factors, + ) + + def _get_matrices(self): + """ + Method to obtain the matrices after the model has been initialized. + + Returns + ------- + numpy.array : float + The prbability of a word given a topic. + numpy.array : float + The prbability of a topic given a document. + """ + sparse_document_term_matrix = self._create_sparse_local_term_weights( + self.corpus, + self._vocabulary_size, + self._word_to_index, + ) + sparse_global_term_weighting = self._create_sparse_global_term_weights( + corpus=self.corpus, + word_weighting=self.word_weighting, + vocabulary_size=self._vocabulary_size, + sparse_local_term_weights=sparse_document_term_matrix, + index_to_word=self._index_to_word, + word_to_index=self._word_to_index, + sum_words=self._sum_words, + ) + projected_data = self._create_projected_data( + algorithm='flsa', + sparse_weighted_matrix=sparse_global_term_weighting, + svd_factors=self.svd_factors, + ) + partition_matrix = self._create_partition_matrix( + data=projected_data, + number_of_clusters=self.num_topics, + method=self.cluster_method + ) + return self._create_probability_matrices( + algorithm='flsa', + prob_topic_given_document_transpose=partition_matrix, + global_term_weights=sparse_global_term_weighting, + ) + + +class FlsaW(FlsaModel): + """ + Class to train the FLSA-W algorithm. + + See: https://ieeexplore.ieee.org/abstract/document/9660139 + + Parameters + ---------- + corpus : list of lists of str + The input file used to initialize the model. + num_topics : int + The number of topics that the model should train. + num_words : int + Indicates how many words per topic should be shown. + word_weighting : str + Indicates the method used for word_weighting. Choose from: + - entropy + - normal + - idf + - probidf + svd_factors : int + The number of singular values to include. + cluster_method : str + The cluster algorithm to be used ('fcm', 'gk', 'fst-pso'). + """ + def __init__( + self, + corpus, + num_topics, + num_words=10, + word_weighting='normal', + svd_factors=2, + cluster_method='fcm', + ): + + super().__init__( + algorithm='flsa-w', + corpus=corpus, + num_topics=num_topics, + num_words=num_words, + word_weighting=word_weighting, + cluster_method=cluster_method, + svd_factors=svd_factors, + ) + + def _get_matrices(self): + """ + Method to obtain the matrices after the model has been initialized. + + Returns + ------- + numpy.array : float + The prbability of a word given a topic. + numpy.array : float + The prbability of a topic given a document. + """ + sparse_document_term_matrix = self._create_sparse_local_term_weights( + self.corpus, + self._vocabulary_size, + self._word_to_index, + ) + sparse_global_term_weighting = self._create_sparse_global_term_weights( + corpus=self.corpus, + word_weighting=self.word_weighting, + vocabulary_size=self._vocabulary_size, + sparse_local_term_weights=sparse_document_term_matrix, + index_to_word=self._index_to_word, + word_to_index=self._word_to_index, + sum_words=self._sum_words, + ) + projected_data = self._create_projected_data( + algorithm='flsa-w', + sparse_weighted_matrix=sparse_global_term_weighting, + svd_factors=self.svd_factors, + ) + partition_matrix = self._create_partition_matrix( + data=projected_data, + number_of_clusters=self.num_topics, + method=self.cluster_method, + ) + return self._create_probability_matrices( + algorithm='flsa-w', + prob_topic_given_word_transpose=partition_matrix, + global_term_weights=sparse_global_term_weighting, + ) + + +class FlsaE(FlsaModel): + """ + Class to train the FLSA-E algorithm. See: https://tinyurl.com/5n8utppk + + Parameters + ---------- + corpus : list of lists of str + The input file used to initialize the model. + num_topics : int + The number of topics that the model should train. + num_words : int + Indicates how many words per topic should be shown. + cluster_method : str + The cluster algorithm to be used ('fcm', 'gk', 'fst-pso'). + min_count : int + Ignores all words with total frequency lower than this. + window : int + Maximum distance between the current and predicted word within a sentence. + vector_size : int + Dimensionality of the word vectors. + workers : int + Use these many worker threads to train the model + ( = faster training with multicore machines). + """ + + def __init__( + self, + corpus, + num_topics, + num_words=10, + cluster_method='fcm', + min_count=1, + window=5, + vector_size=20, + workers=4, + ): + + self.model = ... + self.word_embedding = ... + + super().__init__( + algorithm='flsa-e', + corpus=corpus, + num_topics=num_topics, + num_words=num_words, + cluster_method=cluster_method, + min_count=min_count, + window=window, + vector_size=vector_size, + workers=workers, + ) + + def get_word_embedding( + self, + data, + vector_size, + window, + min_count, + workers, + ): + """ + Method to train a word embedding on the corpus. + + Parameters + ---------- + data : list of lists of str + The input file used to initialize the model. + min_count : int + Ignores all words with total frequency lower than this. + window : int + Maximum distance between the current and predicted word within a sentence. + vector_size : int + Dimensionality of the word vectors. + workers : int + Use these many worker threads to train the model + ( = faster training with multicore machines). + """ + + self.model = Word2Vec( + sentences=data, + vector_size=vector_size, + window=window, + min_count=min_count, + workers=workers, + ) + + return self.model.wv.vectors + + def _get_matrices( + self, + ): + ''' + Method to run after the FLSA_E class has been initialized to obtain the output matrices. + + Returns: + - Numpy array: prob_word_given_topic + - Numpy array: prob_topic_given_document + ''' + sparse_document_term_matrix = self._create_sparse_local_term_weights( + self.corpus, + self._vocabulary_size, + self._word_to_index, + ) + + self.word_embedding = self.get_word_embedding( + data=self.corpus, + min_count=self.min_count, + vector_size=self.vector_size, + window=self.window, + workers=self.workers, + ) + + partition_matrix = self._create_partition_matrix( + data=self.word_embedding, + number_of_clusters=self.num_topics, + method=self.cluster_method, + ) + + return self._create_probability_matrices( + algorithm='flsa-e', + prob_topic_given_word_transpose=partition_matrix, + local_term_weights=sparse_document_term_matrix, + ) diff --git a/setup.py b/setup.py index 485cf08294..c19e0d3056 100644 --- a/setup.py +++ b/setup.py @@ -334,6 +334,7 @@ def run(self): NUMPY_STR, 'scipy >= 1.7.0', 'smart_open >= 1.8.1', + 'FuzzyTM >= 0.4.0' ] setup_requires = [NUMPY_STR] From db90a6adda2f814d74b22cfc92a427886ce8c1c4 Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Fri, 16 Dec 2022 22:12:45 +0900 Subject: [PATCH 30/34] Fix backwards compatibility bug in Word2Vec (#3415) * add missing model file * update test --- gensim/models/word2vec.py | 3 --- .../test/test_data/model-from-gensim-3.8.0.w2v | Bin 0 -> 18926 bytes gensim/test/test_word2vec.py | 7 +++++++ 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 gensim/test/test_data/model-from-gensim-3.8.0.w2v diff --git a/gensim/models/word2vec.py b/gensim/models/word2vec.py index b172f57c8a..d4a4ba992e 100755 --- a/gensim/models/word2vec.py +++ b/gensim/models/word2vec.py @@ -1986,9 +1986,6 @@ def _load_specials(self, *args, **kwargs): for a in ('hashfxn', 'layer1_size', 'seed', 'syn1neg', 'syn1'): if hasattr(self.trainables, a): setattr(self, a, getattr(self.trainables, a)) - if hasattr(self, 'syn1'): - self.syn1 = self.syn1 - del self.syn1 del self.trainables if not hasattr(self, 'shrink_windows'): self.shrink_windows = True diff --git a/gensim/test/test_data/model-from-gensim-3.8.0.w2v b/gensim/test/test_data/model-from-gensim-3.8.0.w2v new file mode 100644 index 0000000000000000000000000000000000000000..40f7e22e32741c0e2f220dca87c6b3948dfab2aa GIT binary patch literal 18926 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 Cs`@Gb literal 0 HcmV?d00001 diff --git a/gensim/test/test_word2vec.py b/gensim/test/test_word2vec.py index d1749e4bfa..a07cf08b10 100644 --- a/gensim/test/test_word2vec.py +++ b/gensim/test/test_word2vec.py @@ -275,6 +275,13 @@ def test_persistence(self): self.assertTrue(np.allclose(wv.vectors, loaded_wv.vectors)) self.assertEqual(len(wv), len(loaded_wv)) + def test_persistence_backwards_compatible(self): + """Can we still load a model created with an older gensim version?""" + path = datapath('model-from-gensim-3.8.0.w2v') + model = word2vec.Word2Vec.load(path) + x = model.score(['test']) + assert x is not None + def test_persistence_from_file(self): """Test storing/loading the entire model trained with corpus_file argument.""" with temporary_file(get_tmpfile('gensim_word2vec.tst')) as corpus_file: From 2fd3e89ca42a7812a71c608572aba2e858377c8c Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Fri, 16 Dec 2022 22:28:27 +0900 Subject: [PATCH 31/34] fix numpy hack in setup.py fix #3225 --- setup.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index c19e0d3056..41d9a9c919 100644 --- a/setup.py +++ b/setup.py @@ -96,19 +96,26 @@ class CustomBuildExt(build_ext): """Custom build_ext action with bootstrapping. We need this in order to use numpy and Cython in this script without - importing them at module level, because they may not be available yet. + importing them at module level, because they may not be available at that time. """ - # - # Prevent numpy from thinking it is still in its setup process - # http://stackoverflow.com/questions/19919905/how-to-bootstrap-numpy-installation-in-setup-py - # def finalize_options(self): build_ext.finalize_options(self) import builtins - builtins.__NUMPY_SETUP__ = False - import numpy + + # + # Prevent numpy from thinking it is still in its setup process + # http://stackoverflow.com/questions/19919905/how-to-bootstrap-numpy-installation-in-setup-py + # + # Newer numpy versions don't support this hack, nor do they need it. + # https://github.com/pyvista/pyacvd/pull/23#issue-1298467701 + # + try: + builtins.__NUMPY_SETUP__ = False + except Exception as ex: + print(f'could not use __NUMPY_SETUP__ hack (numpy version: {numpy.__version__}): {ex}') + self.include_dirs.append(numpy.get_include()) if need_cython(): From f571f337dcaf5fe1a30f6872f267da616cc171e3 Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Sat, 17 Dec 2022 22:30:44 +0900 Subject: [PATCH 32/34] updated changelog for next release (#3412) * updated changelog for next release * updated changelog --- CHANGELOG.md | 39 ++++++++++++++++++++++++++++++++++++++- multibuild | 2 +- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9718b90b64..a1fb190013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,45 @@ Changes ======= -## Unreleased +## 4.3.0, 2022-12-17 +### :star2: New Features + +* Added support for Python 3.11 and drop support for Python 3.7 (__[acul3](https://github.com/acul3)__, [#3402](https://github.com/RaRe-Technologies/gensim/pull/3402)) +* Added a new model: Flsamodel (__[ERijck](https://github.com/ERijck)__, [#3398](https://github.com/RaRe-Technologies/gensim/pull/3398)) + +### :red_circle: Bug fixes + +* Fixed bug in loss computation for Word2Vec with hierarchical softmax (__[TalIfargan](https://github.com/TalIfargan)__, [#3397](https://github.com/RaRe-Technologies/gensim/pull/3397)) +* Patch Coherence Model to correctly handle empty documents (__[PrimozGodec](https://github.com/PrimozGodec)__, [#3406](https://github.com/RaRe-Technologies/gensim/pull/3406)) +* Fixed bug that prevents loading old models (__[funasshi](https://github.com/funasshi)__, [#3359](https://github.com/RaRe-Technologies/gensim/pull/3359)) +* Fixed deprecation warning from pytest (__[martino-vic](https://github.com/martino-vic)__, [#3354](https://github.com/RaRe-Technologies/gensim/pull/3354)) +* Fixed FastTextKeyedVectors handling in add_vector (__[globba](https://github.com/globba)__, [#3389](https://github.com/RaRe-Technologies/gensim/pull/3389)) +* Fixed typo in word2vec and KeyedVectors docstrings (__[dymil](https://github.com/dymil)__, [#3365](https://github.com/RaRe-Technologies/gensim/pull/3365)) +* Fix backwards compatibility bug in Word2Vec, (**[@mpenkov](https://github.com/mpenkov)**, [#3415](https://github.com/RaRe-Technologies/gensim/pull/3415)) +* Fix numpy hack in setup.py, by (**[@mpenkov](https://github.com/mpenkov)**, [#3416](https://github.com/RaRe-Technologies/gensim/pull/3416)) + +### :books: Tutorial and doc improvements + +* Clarified runtime expectations (__[gojomo](https://github.com/gojomo)__, [#3381](https://github.com/RaRe-Technologies/gensim/pull/3381)) +* Copyedit and fix outdated statements in translation matrix tutorial (__[dymil](https://github.com/dymil)__, [#3375](https://github.com/RaRe-Technologies/gensim/pull/3375)) +* Disabled the Gensim 3=>4 warning in docs (__[piskvorky](https://github.com/piskvorky)__, [#3346](https://github.com/RaRe-Technologies/gensim/pull/3346)) +* Fixed the broken link in readme.md (__[aswin2108](https://github.com/aswin2108)__, [#3409](https://github.com/RaRe-Technologies/gensim/pull/3409)) +* Giving missing credit in EnsembleLDA to Alex in docs (__[sezanzeb](https://github.com/sezanzeb)__, [#3393](https://github.com/RaRe-Technologies/gensim/pull/3393)) + +### :+1: Improvements + +* Switched to Cython language level 3 (__[pabs3](https://github.com/pabs3)__, [#3344](https://github.com/RaRe-Technologies/gensim/pull/3344)) +* Declare variables prior to for loop in fastss.pyx for ANSI C compatibility (__[hstk30](https://github.com/hstk30)__, [#3378](https://github.com/RaRe-Technologies/gensim/pull/3378)) +* Implement numpy hack in setup.py to enable install under Poetry (__[jaymegordo](https://github.com/jaymegordo)__, [#3363](https://github.com/RaRe-Technologies/gensim/pull/3363)) +* Replaceed np.multiply with np.square and copyedit in translation_matrix.py (__[dymil](https://github.com/dymil)__, [#3374](https://github.com/RaRe-Technologies/gensim/pull/3374)) + +### 🔮 Testing, CI, housekeeping + +* Clean up references to `Morfessor`, `tox` and `gensim.models.wrappers` (__[pabs3](https://github.com/pabs3)__, [#3345](https://github.com/RaRe-Technologies/gensim/pull/3345)) +* Pinned sphinx versions, add explicit gallery_top label (__[mpenkov](https://github.com/mpenkov)__, [#3383](https://github.com/RaRe-Technologies/gensim/pull/3383)) +* Updated Python module MANIFEST (__[pabs3](https://github.com/pabs3)__, [#3343](https://github.com/RaRe-Technologies/gensim/pull/3343)) +* Refactored wheel building and testing workflow (__[mpenkov](https://github.com/mpenkov)__, [#3410](https://github.com/RaRe-Technologies/gensim/pull/3410)) ## 4.2.0, 2022-04-29 diff --git a/multibuild b/multibuild index d4b02ce8c7..4e30a05764 160000 --- a/multibuild +++ b/multibuild @@ -1 +1 @@ -Subproject commit d4b02ce8c707c8a2bcc721e29f2385149f994b1f +Subproject commit 4e30a057646d9e70d5e6e7c354e85cfb740d2ca1 From 1a5ee4b403acf0d11d77ee7cad534e5f46f7445b Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Sun, 18 Dec 2022 00:02:10 +0900 Subject: [PATCH 33/34] bumped version to 4.3.0 --- docs/src/conf.py | 2 +- gensim/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/conf.py b/docs/src/conf.py index 1789e7353f..75981881de 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -63,7 +63,7 @@ # The short X.Y version. version = '4.2.0' # The full version, including alpha/beta/rc tags. -release = '4.2.1.dev0' +release = '4.3.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/gensim/__init__.py b/gensim/__init__.py index 2ac8d16b66..e7c59b6bd6 100644 --- a/gensim/__init__.py +++ b/gensim/__init__.py @@ -4,7 +4,7 @@ """ -__version__ = '4.2.1.dev0' +__version__ = '4.3.0' import logging diff --git a/setup.py b/setup.py index 41d9a9c919..deace40c59 100644 --- a/setup.py +++ b/setup.py @@ -352,7 +352,7 @@ def run(self): setup( name='gensim', - version='4.2.1.dev0', + version='4.3.0', description='Python framework for fast Vector Space Modelling', long_description=LONG_DESCRIPTION, From 99c6c7baa4dd9681b197dd82b3cf1a871b1a168b Mon Sep 17 00:00:00 2001 From: Michael Penkov Date: Sun, 18 Dec 2022 00:06:05 +0900 Subject: [PATCH 34/34] bump version in conf.py --- docs/src/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/conf.py b/docs/src/conf.py index 75981881de..f6483f8544 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -61,7 +61,7 @@ # built documents. # # The short X.Y version. -version = '4.2.0' +version = '4.3' # The full version, including alpha/beta/rc tags. release = '4.3.0'

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 YGxoYUsdJR@WiSANr>mdKI;Vst06nQ7hX4Qo diff --git a/docs/src/auto_examples/core/images/thumb/sphx_glr_run_topics_and_transformations_thumb.png b/docs/src/auto_examples/core/images/thumb/sphx_glr_run_topics_and_transformations_thumb.png index eebecf5b78e66e2e6b1e1185f05c2f51740e394d..81f90665d13eaab0e268aa1f66fc4dcae546952c 100644 GIT binary patch literal 64089 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 Q1_OR%C6y#<#J&ap58RCY6#xJL literal 62435 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