Repository: piskvorky/gensim

Issue:
Better support for evaluating threshold settings in models.phrases.Phrases
One of the challenges when learning bigrams from a new corpus is determining the right threshold for your scoring to accept or reject a potential bigram. Standard approaches involve taking a list of gold-labeled bigrams created by humans, ranking all bigrams in your corpus by their score, and determining a score threshold based on a comparison to the gold labels. (for one example, see [this paper](https://svn.spraakdata.gu.se/repos/gerlof/pub/www/Docs/npmi-pfd.pdf)

Right now, doing this with built in functionality in models.Phrases requires running export_phrases on your whole corpus. For an especially large corpus, this could mean a lot of wasted time waiting for export_phrases to run. It also means getting some strange results--if you set your threshold very low, a strong bigram in your corpus may not be output by export_phrases if the first word in the strong bigram is especially rare and the strong bigram is proceeded by a limited set of words (you'd get a bunch of lower scoring bigrams with the first word of the strong bigram as the second word of some weaker bigrams).

There should be a method that only traverses the vocab dictionary and returns something that shows the scores for the bigrams in the corpus. This would be both faster than export_phrases and would ensure that all bigrams (that exceed some threshold) have their score output. I have code to do something like this, and I'm happy to contribute it. (Although it might make sense to wait until after #1464 and maybe #1446 are finalized.)


Discussion:
Sounds useful!
@gojomo Thanks. I'll add after the scoring PR #1464 is accepted (or rejected) since that could affect how this method works, as it depends on the scoring. Right now export_phrases is the only method in phrases that actually calculates scores, this will have to calculate scores as well. 

Resolving patch:
diff --git a/gensim/models/phrases.py b/gensim/models/phrases.py
--- a/gensim/models/phrases.py
+++ b/gensim/models/phrases.py
@@ -21,7 +21,7 @@
 
     >>> from gensim.test.utils import datapath
     >>> from gensim.models.word2vec import Text8Corpus
-    >>> from gensim.models.phrases import Phrases
+    >>> from gensim.models.phrases import Phrases, ENGLISH_CONNECTOR_WORDS
     >>>
     >>> # Create training corpus. Must be a sequence of sentences (e.g. an iterable or a generator).
     >>> sentences = Text8Corpus(datapath('testcorpus.txt'))
@@ -31,7 +31,7 @@
     ['computer', 'human', 'interface', 'computer', 'response', 'survey', 'system', 'time', 'user', 'interface']
     >>>
     >>> # Train a toy phrase model on our training corpus.
-    >>> phrase_model = Phrases(sentences, delimiter='_', min_count=1, threshold=1)
+    >>> phrase_model = Phrases(sentences, min_count=1, threshold=1, connector_words=ENGLISH_CONNECTOR_WORDS)
     >>>
     >>> # Apply the trained phrases model to a new, unseen sentence.
     >>> new_sentence = ['trees', 'graph', 'minors']
@@ -61,8 +61,6 @@
 
 """
 
-import sys
-import os
 import logging
 from collections import defaultdict
 import itertools
@@ -77,6 +75,16 @@
 
 NEGATIVE_INFINITY = float('-inf')
 
+# Words from this set are "ignored" during phrase detection:
+# 1) Phrases may not start nor end with these words.
+# 2) Phrases may include any number of these words inside.
+ENGLISH_CONNECTOR_WORDS = frozenset(
+    " a an the "  # articles; we never care about these in MWEs
+    " for of with without at from to in on by "  # prepositions; incomplete on purpose, to minimize FNs
+    " and or "  # conjunctions; incomplete on purpose, to minimize FNs
+    .split()
+)
+
 
 def original_scorer(worda_count, wordb_count, bigram_count, len_vocab, min_count, corpus_word_count):
     r"""Bigram scoring function, based on the original `Mikolov, et. al: "Distributed Representations
@@ -100,7 +108,7 @@ def original_scorer(worda_count, wordb_count, bigram_count, len_vocab, min_count
     Returns
     -------
     float
-        Score for given bi-gram, greater than or equal to 0.
+        Score for given phrase. Can be negative.
 
     Notes
     -----
@@ -201,8 +209,8 @@ class _PhrasesTransformation(interfaces.TransformationABC):
     :class:`~gensim.models.phrases.FrozenPhrases`.
 
     """
-    def __init__(self, common_terms):
-        self.common_terms = frozenset(common_terms)
+    def __init__(self, connector_words):
+        self.connector_words = frozenset(connector_words)
 
     def score_candidate(self, word_a, word_b, in_between):
         """Score a single phrase candidate.
@@ -225,7 +233,7 @@ def analyze_sentence(self, sentence):
 
         Yields
         ------
-        (str, score)
+        (str, {float, None})
             Iterate through the input sentence tokens and yield 2-tuples of:
             - ``(concatenated_phrase_tokens, score)`` for token sequences that form a phrase.
             - ``(word, None)`` if the token is not a part of a phrase.
@@ -233,8 +241,8 @@ def analyze_sentence(self, sentence):
         """
         start_token, in_between = None, []
         for word in sentence:
-            if word not in self.common_terms:
-                # The current word is a normal token, not a stop word, which means it's a potential
+            if word not in self.connector_words:
+                # The current word is a normal token, not a connector word, which means it's a potential
                 # beginning (or end) of a phrase.
                 if start_token:
                     # We're inside a potential phrase, of which this word is the end.
@@ -250,14 +258,14 @@ def analyze_sentence(self, sentence):
                             yield w, None
                         start_token, in_between = word, []  # new potential phrase starts here
                 else:
-                    # Not inside a potential bigram yet; start a new potential bigram here.
+                    # Not inside a phrase yet; start a new phrase candidate here.
                     start_token, in_between = word, []
-            else:  # We're a stop word.
+            else:  # We're a connector word.
                 if start_token:
-                    # We're inside a potential bigram: add the stopword and keep growing the phrase.
+                    # We're inside a potential phrase: add the connector word and keep growing the phrase.
                     in_between.append(word)
                 else:
-                    # Not inside a bigram: emit the stopword and move on. Phrases never begin with a stopword.
+                    # Not inside a phrase: emit the connector word and move on.
                     yield word, None
         # Emit any non-phrase tokens at the end.
         if start_token:
@@ -266,7 +274,7 @@ def analyze_sentence(self, sentence):
                 yield w, None
 
     def __getitem__(self, sentence):
-        """Convert the input sequence of tokens `sentence` into a sequence of tokens where adjacent
+        """Convert the input sequence of tokens ``sentence`` into a sequence of tokens where adjacent
         tokens are replaced by a single token if they form a bigram collocation.
 
         If `sentence` is an entire corpus (iterable of sentences rather than a single
@@ -281,10 +289,10 @@ def __getitem__(self, sentence):
         Return
         ------
         {list of str, iterable of list of str}
-            Sentence with phrase tokens joined by `self.delimiter` character, if input was a single sentence.
-            A generator of such joined sentences if input was a corpus.
+            Sentence with phrase tokens joined by ``self.delimiter``, if input was a single sentence.
+            A generator of such sentences if input was a corpus.
 
-        """
+s        """
         is_single, sentence = _is_single(sentence)
         if not is_single:
             # If the input is an entire corpus (rather than a single sentence),
@@ -293,7 +301,7 @@ def __getitem__(self, sentence):
 
         return [token for token, _ in self.analyze_sentence(sentence)]
 
-    def export_phrases(self, sentences):
+    def find_phrases(self, sentences):
         """Get all unique phrases (multi-word expressions) that appear in ``sentences``, and their scores.
 
         Parameters
@@ -304,7 +312,7 @@ def export_phrases(self, sentences):
         Returns
         -------
         dict(str, float)
-           Unique phrases mapped to their scores.
+           Unique phrases found in ``sentences``, mapped to their scores.
 
         Example
         -------
@@ -312,12 +320,12 @@ def export_phrases(self, sentences):
 
             >>> from gensim.test.utils import datapath
             >>> from gensim.models.word2vec import Text8Corpus
-            >>> from gensim.models.phrases import Phrases
+            >>> from gensim.models.phrases import Phrases, ENGLISH_CONNECTOR_WORDS
             >>>
             >>> sentences = Text8Corpus(datapath('testcorpus.txt'))
-            >>> phrases = Phrases(sentences, min_count=1, threshold=0.1)
+            >>> phrases = Phrases(sentences, min_count=1, threshold=0.1, connector_words=ENGLISH_CONNECTOR_WORDS)
             >>>
-            >>> for phrase, score in phrases.export_phrases(sentences).items():
+            >>> for phrase, score in phrases.find_phrases(sentences).items():
             ...     print(phrase, score)
         """
         result = {}
@@ -381,13 +389,17 @@ def load(cls, *args, **kwargs):
                     model.scoring = npmi_scorer
                 else:
                     raise ValueError(f'failed to load {cls.__name__} model, unknown scoring "{model.scoring}"')
-        # Initialize new attributes to default values.
-        if not hasattr(model, "common_terms"):
+
+        # common_terms didn't exist pre-3.?, and was renamed to connector in 4.0.0.
+        if hasattr(model, "common_terms"):
+            model.connector_words = model.common_terms
+            del model.common_terms
+        else:
             logger.warning(
-                'older version of %s loaded without common_terms attribute, setting it to empty set',
+                'older version of %s loaded without common_terms attribute, setting connector_words to an empty set',
                 cls.__name__,
             )
-            model.common_terms = frozenset()
+            model.connector_words = frozenset()
 
         if not hasattr(model, 'corpus_word_count'):
             logger.warning('older version of %s loaded without corpus_word_count', cls.__name__)
@@ -415,7 +427,7 @@ class Phrases(_PhrasesTransformation):
     def __init__(
             self, sentences=None, min_count=5, threshold=10.0,
             max_vocab_size=40000000, delimiter='_', progress_per=10000,
-            scoring='default', common_terms=frozenset(),
+            scoring='default', connector_words=frozenset(),
         ):
         """
 
@@ -445,41 +457,30 @@ def __init__(
 
             #. "default" - :func:`~gensim.models.phrases.original_scorer`.
             #. "npmi" - :func:`~gensim.models.phrases.npmi_scorer`.
-        common_terms : set of str, optional
-            List of "stop words" that won't affect frequency count of expressions containing them.
-            Allow to detect expressions like "bank_of_america" or "eye_of_the_beholder".
+        connector_words : set of str, optional
+            Set of words that may be included within a phrase, without affecting its scoring.
+            No phrase can start nor end with a connector word; a phrase may contain any number of
+            connector words in the middle.
 
-        Notes
-        -----
-        'npmi' is more robust when dealing with common words that form part of common bigrams, and
-        ranges from -1 to 1, but is slower to calculate than the default. The default is the PMI-like scoring
-        as described by `Mikolov, et. al: "Distributed Representations of Words and Phrases and their Compositionality"
-        <https://arxiv.org/abs/1310.4546>`_.
-
-        To use a custom scoring function, pass in a function with the following signature:
+            **If your texts are in English, set** ``connector_words=phrases.ENGLISH_CONNECTOR_WORDS``.
 
-        * worda_count - number of corpus occurrences in `sentences` of the first token in the bigram being scored
-        * wordb_count - number of corpus occurrences in `sentences` of the second token in the bigram being scored
-        * bigram_count - number of occurrences in `sentences` of the whole bigram
-        * len_vocab - the number of unique tokens in `sentences`
-        * min_count - the `min_count` setting of the Phrases class
-        * corpus_word_count - the total number of tokens (non-unique) in `sentences`
+            This will cause phrases to include common English articles, prepositions and
+            conjuctions, such as `bank_of_america` or `eye_of_the_beholder`.
 
-        The scoring function **must accept all these parameters**, even if it doesn't use them in its scoring.
-
-        The scoring function **must be pickleable**.
+            For other languages or specific applications domains, use custom ``connector_words``
+            that make sense there: ``connector_words=frozenset("der die das".split())`` etc.
 
         Examples
-        ----------
+        --------
         .. sourcecode:: pycon
 
             >>> from gensim.test.utils import datapath
             >>> from gensim.models.word2vec import Text8Corpus
-            >>> from gensim.models.phrases import Phrases
+            >>> from gensim.models.phrases import Phrases, ENGLISH_CONNECTOR_WORDS
             >>>
             >>> # Load corpus and train a model.
             >>> sentences = Text8Corpus(datapath('testcorpus.txt'))
-            >>> phrases = Phrases(sentences, min_count=1, threshold=1)
+            >>> phrases = Phrases(sentences, min_count=1, threshold=1, connector_words=ENGLISH_CONNECTOR_WORDS)
             >>>
             >>> # Use the model to detect phrases in a new sentence.
             >>> sent = [u'trees', u'graph', u'minors']
@@ -488,7 +489,7 @@ def __init__(
             >>>
             >>> # Or transform multiple sentences at once.
             >>> sents = [[u'trees', u'graph', u'minors'], [u'graph', u'minors']]
-            >>> for phrase in frozen_phrases[sents]:
+            >>> for phrase in phrases[sents]:
             ...     print(phrase)
             [u'trees_graph', u'minors']
             [u'graph_minors']
@@ -498,8 +499,29 @@ def __init__(
             >>> print(frozen_phrases[sent])
             [u'trees_graph', u'minors']
 
+        Notes
+        -----
+
+        The ``scoring="npmi"`` is more robust when dealing with common words that form part of common bigrams, and
+        ranges from -1 to 1, but is slower to calculate than the default ``scoring="default"``.
+        The default is the PMI-like scoring as described in `Mikolov, et. al: "Distributed
+        Representations of Words and Phrases and their Compositionality" <https://arxiv.org/abs/1310.4546>`_.
+
+        To use your own custom ``scoring`` function, pass in a function with the following signature:
+
+        * ``worda_count`` - number of corpus occurrences in `sentences` of the first token in the bigram being scored
+        * ``wordb_count`` - number of corpus occurrences in `sentences` of the second token in the bigram being scored
+        * ``bigram_count`` - number of occurrences in `sentences` of the whole bigram
+        * ``len_vocab`` - the number of unique tokens in `sentences`
+        * ``min_count`` - the `min_count` setting of the Phrases class
+        * ``corpus_word_count`` - the total number of tokens (non-unique) in `sentences`
+
+        The scoring function must accept all these parameters, even if it doesn't use them in its scoring.
+
+        The scoring function **must be pickleable**.
+
         """
-        super().__init__(common_terms=common_terms)
+        super().__init__(connector_words=connector_words)
         if min_count <= 0:
             raise ValueError("min_count should be at least 1")
 
@@ -554,36 +576,8 @@ def __str__(self):
         )
 
     @staticmethod
-    def _learn_vocab(
-            sentences, max_vocab_size, delimiter='_', common_terms=frozenset(), progress_per=10000,
-        ):
-        """Collect unigram and bigram counts from the `sentences` iterable.
-
-        Parameters
-        ----------
-        sentences : iterable of list of str
-            The `sentences` iterable can be simply a list, but for larger corpora, consider a generator that streams
-            the sentences directly from disk/network, See :class:`~gensim.models.word2vec.BrownCorpus`,
-            :class:`~gensim.models.word2vec.Text8Corpus` or :class:`~gensim.models.word2vec.LineSentence`
-            for such examples.
-        max_vocab_size : int
-            Maximum size (number of tokens) of the vocabulary. Used to control pruning of less common words,
-            to keep memory under control. 40M needs about 3.6GB of RAM. Increase/decrease
-            `max_vocab_size` depending on how much available memory you have.
-        delimiter : str, optional
-            Glue character used to join collocation tokens.
-        common_terms : set of str, optional
-            List of "stop words" that won't affect frequency count of phrases containing them.
-            Allow to detect phrases like "bank_of_america" or "eye_of_the_beholder".
-        progress_per : int
-            Log progress once every `progress_per` sentences.
-
-        Return
-        ------
-        (int, dict of (str, int), int)
-            Number of pruned words, counters for each word/bi-gram, and total number of words.
-
-        """
+    def _learn_vocab(sentences, max_vocab_size, delimiter, connector_words, progress_per):
+        """Collect unigram and bigram counts from the `sentences` iterable."""
         sentence_no, total_words, min_reduce = -1, 0, 1
         vocab = defaultdict(int)
         logger.info("collecting all words and their counts")
@@ -595,7 +589,7 @@ def _learn_vocab(
                 )
             start_token, in_between = None, []
             for word in sentence:
-                if word not in common_terms:
+                if word not in connector_words:
                     vocab[word] += 1
                     if start_token is not None:
                         phrase_tokens = itertools.chain([start_token], in_between, [word])
@@ -621,7 +615,7 @@ def add_vocab(self, sentences):
         Parameters
         ----------
         sentences : iterable of list of str
-            Text corpus.
+            Text corpus to update this model's parameters from.
 
         Example
         -------
@@ -629,16 +623,16 @@ def add_vocab(self, sentences):
 
             >>> from gensim.test.utils import datapath
             >>> from gensim.models.word2vec import Text8Corpus
-            >>> from gensim.models.phrases import Phrases
+            >>> from gensim.models.phrases import Phrases, ENGLISH_CONNECTOR_WORDS
             >>>
             >>> # Train a phrase detector from a text corpus.
             >>> sentences = Text8Corpus(datapath('testcorpus.txt'))
-            >>> phrases = Phrases(sentences)  # train model
+            >>> phrases = Phrases(sentences, connector_words=ENGLISH_CONNECTOR_WORDS)  # train model
             >>> assert len(phrases.vocab) == 37
             >>>
             >>> more_sentences = [
             ...     [u'the', u'mayor', u'of', u'new', u'york', u'was', u'there'],
-            ...     [u'machine', u'learning', u'can', u'be', u'new', u'york', u'sometimes']
+            ...     [u'machine', u'learning', u'can', u'be', u'new', u'york', u'sometimes'],
             ... ]
             >>>
             >>> phrases.add_vocab(more_sentences)  # add new sentences to model
@@ -652,7 +646,7 @@ def add_vocab(self, sentences):
         # counts collected in previous learn_vocab runs.
         min_reduce, vocab, total_words = self._learn_vocab(
             sentences, max_vocab_size=self.max_vocab_size, delimiter=self.delimiter,
-            progress_per=self.progress_per, common_terms=self.common_terms,
+            progress_per=self.progress_per, connector_words=self.connector_words,
         )
 
         self.corpus_word_count += total_words
@@ -711,9 +705,28 @@ def freeze(self):
         """
         return FrozenPhrases(self)
 
+    def export_phrases(self):
+        """Extract all found phrases.
+
+        Returns
+        ------
+        dict(str, float)
+            Mapping between phrases and their scores.
+
+        """
+        result, source_vocab = {}, self.vocab
+        for token in source_vocab:
+            unigrams = token.split(self.delimiter)
+            if len(unigrams) < 2:
+                continue  # no phrases here
+            phrase, score = self.score_candidate(unigrams[0], unigrams[-1], unigrams[1:-1])
+            if score is not None:
+                result[phrase] = score
+        return result
+
 
 class FrozenPhrases(_PhrasesTransformation):
-    """Minimal state & functionality exported from :class:`~gensim.models.phrases.Phrases`.
+    """Minimal state & functionality exported from a trained :class:`~gensim.models.phrases.Phrases` model.
 
     The goal of this class is to cut down memory consumption of `Phrases`, by discarding model state
     not strictly needed for the phrase detection task.
@@ -741,11 +754,11 @@ def __init__(self, phrases_model):
 
             >>> from gensim.test.utils import datapath
             >>> from gensim.models.word2vec import Text8Corpus
-            >>> from gensim.models.phrases import Phrases
+            >>> from gensim.models.phrases import Phrases, ENGLISH_CONNECTOR_WORDS
             >>>
             >>> # Load corpus and train a model.
             >>> sentences = Text8Corpus(datapath('testcorpus.txt'))
-            >>> phrases = Phrases(sentences, min_count=1, threshold=1)
+            >>> phrases = Phrases(sentences, min_count=1, threshold=1, connector_words=ENGLISH_CONNECTOR_WORDS)
             >>>
             >>> # Export a FrozenPhrases object that is more efficient but doesn't allow further training.
             >>> frozen_phrases = phrases.freeze()
@@ -757,9 +770,9 @@ def __init__(self, phrases_model):
         self.min_count = phrases_model.min_count
         self.delimiter = phrases_model.delimiter
         self.scoring = phrases_model.scoring
-        self.common_terms = phrases_model.common_terms
+        self.connector_words = phrases_model.connector_words
         logger.info('exporting phrases from %s', phrases_model)
-        self.phrasegrams = self._import_phrases(phrases_model)
+        self.phrasegrams = phrases_model.export_phrases()
         logger.info('exported %s', self)
 
     def __str__(self):
@@ -767,25 +780,6 @@ def __str__(self):
             self.__class__.__name__, len(self.phrasegrams), self.min_count, self.threshold,
         )
 
-    def _import_phrases(self, phrases_model):
-        """Extract all phrases that pass the threshold out of `phrases_model`.
-
-        Returns
-        ------
-        dict[str, float]
-            Mapping between phrases and their scores.
-
-        """
-        result, source_vocab = {}, phrases_model.vocab
-        for token in source_vocab:
-            unigrams = token.split(self.delimiter)
-            if len(unigrams) < 2:
-                continue  # no phrases here
-            phrase, score = phrases_model.score_candidate(unigrams[0], unigrams[-1], unigrams[1:-1])
-            if score is not None:
-                result[phrase] = score
-        return result
-
     def score_candidate(self, word_a, word_b, in_between):
         phrase = self.delimiter.join([word_a] + in_between + [word_b])
         score = self.phrasegrams.get(phrase, NEGATIVE_INFINITY)
@@ -795,23 +789,3 @@ def score_candidate(self, word_a, word_b, in_between):
 
 
 Phraser = FrozenPhrases  # alias for backward compatibility
-
-
-if __name__ == '__main__':
-    logging.basicConfig(format='%(asctime)s : %(threadName)s : %(levelname)s : %(message)s', level=logging.INFO)
-    logging.info("running %s", " ".join(sys.argv))
-
-    # check and process cmdline input
-    program = os.path.basename(sys.argv[0])
-    if len(sys.argv) < 2:
-        print(globals()['__doc__'] % locals())
-        sys.exit(1)
-    infile = sys.argv[1]
-
-    from gensim.models import Phrases  # noqa:F811 for pickle
-    from gensim.models.word2vec import Text8Corpus
-    sentences = Text8Corpus(infile)
-
-    bigram = Phrases(sentences, min_count=5, threshold=100)
-    for s in bigram[sentences]:
-        print(u' '.join(s))
diff --git a/gensim/sklearn_api/phrases.py b/gensim/sklearn_api/phrases.py
--- a/gensim/sklearn_api/phrases.py
+++ b/gensim/sklearn_api/phrases.py
@@ -32,7 +32,7 @@
 from sklearn.exceptions import NotFittedError
 
 from gensim import models
-from gensim.models.phrases import FrozenPhrases
+from gensim.models.phrases import FrozenPhrases, ENGLISH_CONNECTOR_WORDS  # noqa:F401
 
 
 class PhrasesTransformer(TransformerMixin, BaseEstimator):
@@ -46,7 +46,7 @@ class PhrasesTransformer(TransformerMixin, BaseEstimator):
     """
     def __init__(
             self, min_count=5, threshold=10.0, max_vocab_size=40000000,
-            delimiter='_', progress_per=10000, scoring='default', common_terms=frozenset(),
+            delimiter='_', progress_per=10000, scoring='default', connector_words=frozenset(),
         ):
         """
 
@@ -90,9 +90,17 @@ def __init__(
 
             A scoring function without any of these parameters (even if the parameters are not used) will
             raise a ValueError on initialization of the Phrases class. The scoring function must be pickleable.
-        common_terms : set of str, optional
-            List of "stop words" that won't affect frequency count of expressions containing them.
-            Allow to detect expressions like "bank_of_america" or "eye_of_the_beholder".
+        connector_words : set of str, optional
+            Set of words that may be included within a phrase, without affecting its scoring.
+            No phrase can start nor end with a connector word; a phrase may contain any number of
+            connector words in the middle.
+
+            **If your texts are in English, set ``connector_words=phrases.ENGLISH_CONNECTOR_WORDS``.**
+            This will cause phrases to include common English articles, prepositions and
+            conjuctions, such as `bank_of_america` or `eye_of_the_beholder`.
+
+            For other languages or specific applications domains, use custom ``connector_words``
+            that make sense there: ``connector_words=frozenset("der die das".split())`` etc.
 
         """
         self.gensim_model = None
@@ -103,11 +111,11 @@ def __init__(
         self.delimiter = delimiter
         self.progress_per = progress_per
         self.scoring = scoring
-        self.common_terms = common_terms
+        self.connector_words = connector_words
 
     def __setstate__(self, state):
         self.__dict__ = state
-        self.common_terms = frozenset()
+        self.connector_words = frozenset()
         self.phraser = None
 
     def fit(self, X, y=None):
@@ -127,7 +135,7 @@ def fit(self, X, y=None):
         self.gensim_model = models.Phrases(
             sentences=X, min_count=self.min_count, threshold=self.threshold,
             max_vocab_size=self.max_vocab_size, delimiter=self.delimiter,
-            progress_per=self.progress_per, scoring=self.scoring, common_terms=self.common_terms
+            progress_per=self.progress_per, scoring=self.scoring, connector_words=self.connector_words,
         )
         self.phraser = FrozenPhrases(self.gensim_model)
         return self
@@ -184,7 +192,7 @@ def partial_fit(self, X):
             self.gensim_model = models.Phrases(
                 sentences=X, min_count=self.min_count, threshold=self.threshold,
                 max_vocab_size=self.max_vocab_size, delimiter=self.delimiter,
-                progress_per=self.progress_per, scoring=self.scoring, common_terms=self.common_terms
+                progress_per=self.progress_per, scoring=self.scoring, connector_words=self.connector_words,
             )
 
         self.gensim_model.add_vocab(X)
diff --git a/release/bump_version.py b/release/bump_version.py
--- a/release/bump_version.py
+++ b/release/bump_version.py
@@ -29,7 +29,7 @@ def bump_docs_src_conf_py(root, previous_version, new_version):
     path = os.path.join(root, 'docs', 'src', 'conf.py')
 
     short_previous_version = '.'.join(previous_version.split('.')[:2])
-    short_new_version = '.'.join(new_version.split('.')[:2])
+    short_new_version = new_version  # '.'.join(new_version.split('.')[:2])
     pattern = re.compile("^version = '%s'$" % short_previous_version, re.MULTILINE)
     repl = "version = '%s'" % short_new_version
     bump(path, pattern, repl, check=False)  # short version won't always change


Test patch:
diff --git a/gensim/test/test_phrases.py b/gensim/test/test_phrases.py
--- a/gensim/test/test_phrases.py
+++ b/gensim/test/test_phrases.py
@@ -22,7 +22,7 @@ class TestPhraseAnalysis(unittest.TestCase):
     class AnalysisTester(_PhrasesTransformation):
 
         def __init__(self, scores, threshold):
-            super().__init__(common_terms={"a", "the", "with", "of"})
+            super().__init__(connector_words={"a", "the", "with", "of"})
             self.scores = scores
             self.threshold = threshold
 
@@ -59,7 +59,7 @@ def test_analysis_bigrams(self):
         result = self.AnalysisTester(scores, threshold=1)[sentence]
         self.assertEqual(result, sentence)
 
-    def test_analysis_common_terms(self):
+    def test_analysis_connector_words(self):
         scores = {
             "simple_sentence": 2, "sentence_many": 2,
             "many_possible": 2, "possible_bigrams": 2,
@@ -75,7 +75,7 @@ def test_analysis_common_terms(self):
             ["simple", "the", "sentence", "and", "many_possible", "bigrams", "with", "a"],
         )
 
-    def test_analysis_common_terms_in_between(self):
+    def test_analysis_connector_words_in_between(self):
         scores = {
             "simple_sentence": 2, "sentence_with_many": 2,
             "many_possible": 2, "many_of_the_possible": 2, "possible_bigrams": 2,
@@ -95,7 +95,7 @@ class PhrasesData:
     sentences = common_texts + [
         ['graph', 'minors', 'survey', 'human', 'interface'],
     ]
-    common_terms = frozenset()
+    connector_words = frozenset()
 
     bigram1 = u'response_time'
     bigram2 = u'graph_minors'
@@ -109,8 +109,8 @@ class PhrasesCommon(PhrasesData):
     """Tests for both Phrases and FrozenPhrases classes."""
 
     def setUp(self):
-        self.bigram = Phrases(self.sentences, min_count=1, threshold=1, common_terms=self.common_terms)
-        self.bigram_default = Phrases(self.sentences, common_terms=self.common_terms)
+        self.bigram = Phrases(self.sentences, min_count=1, threshold=1, connector_words=self.connector_words)
+        self.bigram_default = Phrases(self.sentences, connector_words=self.connector_words)
 
     def testEmptyPhrasifiedSentencesIterator(self):
         bigram_phrases = Phrases(self.sentences)
@@ -215,7 +215,7 @@ class TestPhrasesModel(PhrasesCommon, unittest.TestCase):
     def testExportPhrases(self):
         """Test Phrases bigram export phrases."""
         bigram = Phrases(self.sentences, min_count=1, threshold=1, delimiter=' ')
-        seen_bigrams = set(bigram.export_phrases(self.sentences).keys())
+        seen_bigrams = set(bigram.find_phrases(self.sentences).keys())
 
         assert seen_bigrams == {
             'response time',
@@ -227,7 +227,7 @@ def testMultipleBigramsSingleEntry(self):
         """Test a single entry produces multiple bigrams."""
         bigram = Phrases(self.sentences, min_count=1, threshold=1, delimiter=' ')
         test_sentences = [['graph', 'minors', 'survey', 'human', 'interface']]
-        seen_bigrams = set(bigram.export_phrases(test_sentences).keys())
+        seen_bigrams = set(bigram.find_phrases(test_sentences).keys())
 
         assert seen_bigrams == {'graph minors', 'human interface'}
 
@@ -235,7 +235,7 @@ def testScoringDefault(self):
         """Test the default scoring, from the mikolov word2vec paper."""
         bigram = Phrases(self.sentences, min_count=1, threshold=1, delimiter=' ')
         test_sentences = [['graph', 'minors', 'survey', 'human', 'interface']]
-        seen_scores = set(round(score, 3) for score in bigram.export_phrases(test_sentences).values())
+        seen_scores = set(round(score, 3) for score in bigram.find_phrases(test_sentences).values())
 
         assert seen_scores == {
             5.167,  # score for graph minors
@@ -254,7 +254,7 @@ def testScoringNpmi(self):
         """Test normalized pointwise mutual information scoring."""
         bigram = Phrases(self.sentences, min_count=1, threshold=.5, scoring='npmi')
         test_sentences = [['graph', 'minors', 'survey', 'human', 'interface']]
-        seen_scores = set(round(score, 3) for score in bigram.export_phrases(test_sentences).values())
+        seen_scores = set(round(score, 3) for score in bigram.find_phrases(test_sentences).values())
 
         assert seen_scores == {
             .882,  # score for graph minors
@@ -265,7 +265,7 @@ def testCustomScorer(self):
         """Test using a custom scoring function."""
         bigram = Phrases(self.sentences, min_count=1, threshold=.001, scoring=dumb_scorer)
         test_sentences = [['graph', 'minors', 'survey', 'human', 'interface', 'system']]
-        seen_scores = list(bigram.export_phrases(test_sentences).values())
+        seen_scores = list(bigram.find_phrases(test_sentences).values())
 
         assert all(score == 1 for score in seen_scores)
         assert len(seen_scores) == 3  # 'graph minors' and 'survey human' and 'interface system'
@@ -294,7 +294,7 @@ def testSaveLoadCustomScorer(self):
             bigram.save(fpath)
             bigram_loaded = Phrases.load(fpath)
             test_sentences = [['graph', 'minors', 'survey', 'human', 'interface', 'system']]
-            seen_scores = list(bigram_loaded.export_phrases(test_sentences).values())
+            seen_scores = list(bigram_loaded.find_phrases(test_sentences).values())
 
             assert all(score == 1 for score in seen_scores)
             assert len(seen_scores) == 3  # 'graph minors' and 'survey human' and 'interface system'
@@ -306,7 +306,7 @@ def testSaveLoad(self):
             bigram.save(fpath)
             bigram_loaded = Phrases.load(fpath)
             test_sentences = [['graph', 'minors', 'survey', 'human', 'interface', 'system']]
-            seen_scores = set(round(score, 3) for score in bigram_loaded.export_phrases(test_sentences).values())
+            seen_scores = set(round(score, 3) for score in bigram_loaded.find_phrases(test_sentences).values())
 
             assert seen_scores == set([
                 5.167,  # score for graph minors
@@ -317,7 +317,7 @@ def testSaveLoadStringScoring(self):
         """Test backwards compatibility with a previous version of Phrases with custom scoring."""
         bigram_loaded = Phrases.load(datapath("phrases-scoring-str.pkl"))
         test_sentences = [['graph', 'minors', 'survey', 'human', 'interface', 'system']]
-        seen_scores = set(round(score, 3) for score in bigram_loaded.export_phrases(test_sentences).values())
+        seen_scores = set(round(score, 3) for score in bigram_loaded.find_phrases(test_sentences).values())
 
         assert seen_scores == set([
             5.167,  # score for graph minors
@@ -328,7 +328,7 @@ def testSaveLoadNoScoring(self):
         """Test backwards compatibility with old versions of Phrases with no scoring parameter."""
         bigram_loaded = Phrases.load(datapath("phrases-no-scoring.pkl"))
         test_sentences = [['graph', 'minors', 'survey', 'human', 'interface', 'system']]
-        seen_scores = set(round(score, 3) for score in bigram_loaded.export_phrases(test_sentences).values())
+        seen_scores = set(round(score, 3) for score in bigram_loaded.find_phrases(test_sentences).values())
 
         assert seen_scores == set([
             5.167,  # score for graph minors
@@ -336,9 +336,9 @@ def testSaveLoadNoScoring(self):
         ])
 
     def testSaveLoadNoCommonTerms(self):
-        """ Ensure backwards compatibility with old versions of Phrases, before common_terms"""
+        """Ensure backwards compatibility with old versions of Phrases, before connector_words."""
         bigram_loaded = Phrases.load(datapath("phrases-no-common-terms.pkl"))
-        self.assertEqual(bigram_loaded.common_terms, frozenset())
+        self.assertEqual(bigram_loaded.connector_words, frozenset())
         # can make a phraser, cf #1751
         phraser = FrozenPhrases(bigram_loaded)  # does not raise
         phraser[["human", "interface", "survey"]]  # does not raise
@@ -381,9 +381,9 @@ def testSaveLoadNoScoring(self):
         self.assertEqual(bigram_loaded.scoring, original_scorer)
 
     def testSaveLoadNoCommonTerms(self):
-        """Ensure backwards compatibility with old versions of FrozenPhrases, before common_terms."""
+        """Ensure backwards compatibility with old versions of FrozenPhrases, before connector_words."""
         bigram_loaded = FrozenPhrases.load(datapath("phraser-no-common-terms.pkl"))
-        self.assertEqual(bigram_loaded.common_terms, frozenset())
+        self.assertEqual(bigram_loaded.connector_words, frozenset())
 
 
 class TestFrozenPhrasesModel(PhrasesCommon, unittest.TestCase):
@@ -392,15 +392,15 @@ class TestFrozenPhrasesModel(PhrasesCommon, unittest.TestCase):
     def setUp(self):
         """Set up FrozenPhrases models for the tests."""
         bigram_phrases = Phrases(
-            self.sentences, min_count=1, threshold=1, common_terms=self.common_terms)
+            self.sentences, min_count=1, threshold=1, connector_words=self.connector_words)
         self.bigram = FrozenPhrases(bigram_phrases)
 
-        bigram_default_phrases = Phrases(self.sentences, common_terms=self.common_terms)
+        bigram_default_phrases = Phrases(self.sentences, connector_words=self.connector_words)
         self.bigram_default = FrozenPhrases(bigram_default_phrases)
 
 
 class CommonTermsPhrasesData:
-    """This mixin permits to reuse tests with the common_terms option."""
+    """This mixin permits to reuse tests with the connector_words option."""
 
     sentences = [
         ['human', 'interface', 'with', 'computer'],
@@ -414,7 +414,7 @@ class CommonTermsPhrasesData:
         ['data', 'and', 'graph', 'survey'],
         ['data', 'and', 'graph', 'survey', 'for', 'human', 'interface']  # test bigrams within same sentence
     ]
-    common_terms = ['of', 'and', 'for']
+    connector_words = ['of', 'and', 'for']
 
     bigram1 = u'lack_of_interest'
     bigram2 = u'data_and_graph'
@@ -428,13 +428,13 @@ def gen_sentences(self):
 
 
 class TestPhrasesModelCommonTerms(CommonTermsPhrasesData, TestPhrasesModel):
-    """Test Phrases models with common terms"""
+    """Test Phrases models with connector words."""
 
     def testMultipleBigramsSingleEntry(self):
         """Test a single entry produces multiple bigrams."""
-        bigram = Phrases(self.sentences, min_count=1, threshold=1, common_terms=self.common_terms, delimiter=' ')
+        bigram = Phrases(self.sentences, min_count=1, threshold=1, connector_words=self.connector_words, delimiter=' ')
         test_sentences = [['data', 'and', 'graph', 'survey', 'for', 'human', 'interface']]
-        seen_bigrams = set(bigram.export_phrases(test_sentences).keys())
+        seen_bigrams = set(bigram.find_phrases(test_sentences).keys())
 
         assert seen_bigrams == set([
             'data and graph',
@@ -443,8 +443,8 @@ def testMultipleBigramsSingleEntry(self):
 
     def testExportPhrases(self):
         """Test Phrases bigram export phrases."""
-        bigram = Phrases(self.sentences, min_count=1, threshold=1, common_terms=self.common_terms, delimiter=' ')
-        seen_bigrams = set(bigram.export_phrases(self.sentences).keys())
+        bigram = Phrases(self.sentences, min_count=1, threshold=1, connector_words=self.connector_words, delimiter=' ')
+        seen_bigrams = set(bigram.find_phrases(self.sentences).keys())
 
         assert seen_bigrams == set([
             'human interface',
@@ -455,9 +455,9 @@ def testExportPhrases(self):
 
     def testScoringDefault(self):
         """ test the default scoring, from the mikolov word2vec paper """
-        bigram = Phrases(self.sentences, min_count=1, threshold=1, common_terms=self.common_terms)
+        bigram = Phrases(self.sentences, min_count=1, threshold=1, connector_words=self.connector_words)
         test_sentences = [['data', 'and', 'graph', 'survey', 'for', 'human', 'interface']]
-        seen_scores = set(round(score, 3) for score in bigram.export_phrases(test_sentences).values())
+        seen_scores = set(round(score, 3) for score in bigram.find_phrases(test_sentences).values())
 
         min_count = float(bigram.min_count)
         len_vocab = float(len(bigram.vocab))
@@ -479,10 +479,10 @@ def testScoringNpmi(self):
         """Test normalized pointwise mutual information scoring."""
         bigram = Phrases(
             self.sentences, min_count=1, threshold=.5,
-            scoring='npmi', common_terms=self.common_terms,
+            scoring='npmi', connector_words=self.connector_words,
         )
         test_sentences = [['data', 'and', 'graph', 'survey', 'for', 'human', 'interface']]
-        seen_scores = set(round(score, 3) for score in bigram.export_phrases(test_sentences).values())
+        seen_scores = set(round(score, 3) for score in bigram.find_phrases(test_sentences).values())
 
         assert seen_scores == set([
             .74,  # score for data and graph
@@ -493,17 +493,17 @@ def testCustomScorer(self):
         """Test using a custom scoring function."""
         bigram = Phrases(
             self.sentences, min_count=1, threshold=.001,
-            scoring=dumb_scorer, common_terms=self.common_terms,
+            scoring=dumb_scorer, connector_words=self.connector_words,
         )
         test_sentences = [['data', 'and', 'graph', 'survey', 'for', 'human', 'interface']]
-        seen_scores = list(bigram.export_phrases(test_sentences).values())
+        seen_scores = list(bigram.find_phrases(test_sentences).values())
 
         assert all(seen_scores)  # all scores 1
         assert len(seen_scores) == 2  # 'data and graph' 'survey for human'
 
     def test__getitem__(self):
         """Test Phrases[sentences] with a single sentence."""
-        bigram = Phrases(self.sentences, min_count=1, threshold=1, common_terms=self.common_terms)
+        bigram = Phrases(self.sentences, min_count=1, threshold=1, connector_words=self.connector_words)
         test_sentences = [['data', 'and', 'graph', 'survey', 'for', 'human', 'interface']]
         phrased_sentence = next(bigram[test_sentences].__iter__())
 
diff --git a/gensim/test/test_sklearn_api.py b/gensim/test/test_sklearn_api.py
--- a/gensim/test/test_sklearn_api.py
+++ b/gensim/test/test_sklearn_api.py
@@ -280,7 +280,7 @@
      'technologies', 'that', 'arise', 'from', 'theoretical', 'breakthroughs'],
     ['advances', 'in', 'the', 'understanding', 'of', 'electromagnetism', 'or', 'nuclear', 'physics',
      'led', 'directly', 'to', 'the', 'development', 'of', 'new', 'products', 'that', 'have', 'dramatically',
-     'transformed', 'modern', 'day', 'society']
+     'transformed', 'modern', 'day', 'society'],
 ]
 
 d2v_sentences = [models.doc2vec.TaggedDocument(words, [i]) for i, words in enumerate(w2v_texts)]
@@ -288,15 +288,15 @@
 dict_texts = [' '.join(text) for text in common_texts]
 
 phrases_sentences = common_texts + [
-    ['graph', 'minors', 'survey', 'human', 'interface']
+    ['graph', 'minors', 'survey', 'human', 'interface'],
 ]
 
-common_terms = ["of", "the", "was", "are"]
-phrases_w_common_terms = [
+connector_words = ["of", "the", "was", "are"]
+phrases_w_connector_words = [
     [u'the', u'mayor', u'of', u'new', u'york', u'was', u'there'],
     [u'the', u'mayor', u'of', u'new', u'orleans', u'was', u'there'],
     [u'the', u'bank', u'of', u'america', u'offices', u'are', u'open'],
-    [u'the', u'bank', u'of', u'america', u'offices', u'are', u'closed']
+    [u'the', u'bank', u'of', u'america', u'offices', u'are', u'closed'],
 ]
 
 
@@ -1174,7 +1174,7 @@ def testModelNotFitted(self):
 
 class TestPhrasesTransformerCommonTerms(unittest.TestCase):
     def setUp(self):
-        self.model = PhrasesTransformer(min_count=1, threshold=1, common_terms=common_terms)
+        self.model = PhrasesTransformer(min_count=1, threshold=1, connector_words=connector_words)
         self.expected_transformations = [
             [u'the', u'mayor_of_new', u'york', u'was', u'there'],
             [u'the', u'mayor_of_new', u'orleans', u'was', u'there'],
@@ -1183,18 +1183,18 @@ def setUp(self):
         ]
 
     def testFitAndTransform(self):
-        self.model.fit(phrases_w_common_terms)
+        self.model.fit(phrases_w_connector_words)
 
-        transformed = self.model.transform(phrases_w_common_terms)
+        transformed = self.model.transform(phrases_w_connector_words)
         self.assertEqual(transformed, self.expected_transformations)
 
     def testFitTransform(self):
-        transformed = self.model.fit_transform(phrases_w_common_terms)
+        transformed = self.model.fit_transform(phrases_w_connector_words)
         self.assertEqual(transformed, self.expected_transformations)
 
     def testPartialFit(self):
         # fit half of the sentences
-        self.model.fit(phrases_w_common_terms[:2])
+        self.model.fit(phrases_w_connector_words[:2])
 
         expected_transformations_0 = [
             [u'the', u'mayor_of_new', u'york', u'was', u'there'],
@@ -1203,12 +1203,12 @@ def testPartialFit(self):
             [u'the', u'bank', u'of', u'america', u'offices', u'are', u'closed']
         ]
         # transform all sentences, second half should be same as original
-        transformed_0 = self.model.transform(phrases_w_common_terms)
+        transformed_0 = self.model.transform(phrases_w_connector_words)
         self.assertEqual(transformed_0, expected_transformations_0)
 
         # fit remaining sentences, result should be the same as in the other tests
-        self.model.partial_fit(phrases_w_common_terms[2:])
-        transformed_1 = self.model.fit_transform(phrases_w_common_terms)
+        self.model.partial_fit(phrases_w_connector_words[2:])
+        transformed_1 = self.model.fit_transform(phrases_w_connector_words)
         self.assertEqual(transformed_1, self.expected_transformations)
 
         new_phrases = [[u'offices', u'are', u'open'], [u'offices', u'are', u'closed']]
@@ -1219,7 +1219,7 @@ def testPartialFit(self):
             [u'the', u'bank_of_america', u'offices_are_open'],
             [u'the', u'bank_of_america', u'offices_are_closed']
         ]
-        transformed_2 = self.model.transform(phrases_w_common_terms)
+        transformed_2 = self.model.transform(phrases_w_connector_words)
         self.assertEqual(transformed_2, expected_transformations_2)