Аналогичный метод из модуля nltk дает разные результаты на разных машинах. Зачем?

Я преподал несколько вводных классов для интеллектуального анализа текста с помощью Python, и класс попробовал аналогичный метод с предоставленными практическими текстами. Некоторые студенты получили разные результаты для text1.similar(), чем другие.

Все версии и т.д. были одинаковыми.

Кто-нибудь знает, почему произошли эти различия? Спасибо.

Код, используемый в командной строке.

python
>>> import nltk
>>> nltk.download() #here you use the pop-up window to download texts
>>> from nltk.book import *
*** Introductory Examples for the NLTK Book ***
Loading text1, ..., text9 and sent1, ..., sent9
Type the name of the text or sentence to view it.
Type: 'texts()' or 'sents()' to list the materials.
text1: Moby Dick by Herman Melville 1851
text2: Sense and Sensibility by Jane Austen 1811
text3: The Book of Genesis
text4: Inaugural Address Corpus
text5: Chat Corpus
text6: Monty Python and the Holy Grail
text7: Wall Street Journal
text8: Personals Corpus
text9: The Man Who Was Thursday by G . K . Chesterton 1908
>>>>>> text1.similar("monstrous")
mean part maddens doleful gamesome subtly uncommon careful untoward
exasperate loving passing mouldy christian few true mystifying
imperial modifies contemptible
>>> text2.similar("monstrous")
very heartily so exceedingly remarkably as vast a great amazingly
extremely good sweet

Эти списки терминов, возвращаемых аналогичным методом, отличаются от пользователя к пользователю, у них много общего, но они не идентичны. Все пользователи использовали одну и ту же ОС и те же версии python и nltk.

Надеюсь, этот вопрос станет более ясным. Спасибо.

Ответы

Ответ 1

В вашем примере есть еще 40 слов, которые имеют ровно один контекст вместе со словом 'monstrous'. В similar объект Counter используется для подсчета слов с похожими контекстами, а затем печатаются наиболее распространенные (по умолчанию 20). Поскольку все 40 имеют одинаковую частоту, порядок может отличаться.

Из doc Counter.most_common:

Элементы с равным числом отсчетов упорядочены произвольно


Я проверил частоту подобных слов с помощью этого кода (который по существу является копией соответствующей части кода функции):

from nltk.book import *
from nltk.util import tokenwrap
from nltk.compat import Counter

word = 'monstrous'
num = 20

text1.similar(word)

wci = text1._word_context_index._word_to_contexts

if word in wci.conditions():
            contexts = set(wci[word])
            fd = Counter(w for w in wci.conditions() for c in wci[w]
                          if c in contexts and not w == word)
            words = [w for w, _ in fd.most_common(num)]
            # print(tokenwrap(words))

print(fd)
print(len(fd))
print(fd.most_common(num))

Выход: (разные прогоны дают мне разные результаты)

Counter({'doleful': 1, 'curious': 1, 'delightfully': 1, 'careful': 1, 'uncommon': 1, 'mean': 1, 'perilous': 1, 'fearless': 1, 'imperial': 1, 'christian': 1, 'trustworthy': 1, 'untoward': 1, 'maddens': 1, 'true': 1, 'contemptible': 1, 'subtly': 1, 'wise': 1, 'lamentable': 1, 'tyrannical': 1, 'puzzled': 1, 'vexatious': 1, 'part': 1, 'gamesome': 1, 'determined': 1, 'reliable': 1, 'lazy': 1, 'passing': 1, 'modifies': 1, 'few': 1, 'horrible': 1, 'candid': 1, 'exasperate': 1, 'pitiable': 1, 'abundant': 1, 'mystifying': 1, 'mouldy': 1, 'loving': 1, 'domineering': 1, 'impalpable': 1, 'singular': 1})

Ответ 2

Короче:

Это связано с тем, как python3 хеширует ключи, когда функция similar() использует словарь Counter. См. http://pastebin.com/ysAF6p6h

См. Как и почему хеши словаря отличаются в python2 и python3?


В длинном:

Начнем с:

from nltk.book import *

Импорт здесь происходит из https://github.com/nltk/nltk/blob/develop/nltk/book.py, которые импортируют nltk.text.Text и прочитайте несколько тел в объект Text.

например. Вот как переменная text1 считывалась с nltk.book:

>>> import nltk.corpus
>>> from nltk.text import Text
>>> moby = Text(nltk.corpus.gutenberg.words('melville-moby_dick.txt'))

Теперь, если мы перейдем к коду для функции similar() в https://github.com/nltk/nltk/blob/develop/nltk/text.py#L377, мы увидим эту инициализацию, если она первый экземпляр доступа self._word_context_index:

def similar(self, word, num=20):
    """
    Distributional similarity: find other words which appear in the
    same contexts as the specified word; list most similar words first.
    :param word: The word used to seed the similarity search
    :type word: str
    :param num: The number of words to generate (default=20)
    :type num: int
    :seealso: ContextIndex.similar_words()
    """
    if '_word_context_index' not in self.__dict__:
        #print('Building word-context index...')
        self._word_context_index = ContextIndex(self.tokens, 
                                                filter=lambda x:x.isalpha(), 
                                                key=lambda s:s.lower())


    word = word.lower()
    wci = self._word_context_index._word_to_contexts
    if word in wci.conditions():
        contexts = set(wci[word])
        fd = Counter(w for w in wci.conditions() for c in wci[w]
                      if c in contexts and not w == word)
        words = [w for w, _ in fd.most_common(num)]
        print(tokenwrap(words))
    else:
        print("No matches")

Итак, это указывает на объект nltk.text.ContextIndex, который должен собирать все слова с похожим контекстным окном и хранить их, В докстерии говорится:

Двунаправленный индекс между словами и их "контекстами" в тексте. Контекст слова обычно определяется как слова, которые встречаются в фиксированное окно вокруг слова; но могут использоваться и другие определения путем предоставления настраиваемой контекстной функции.

По умолчанию, если вы вызываете функцию similar(), она инициализирует _word_context_index параметрами контекста по умолчанию, то есть левым и правым маркерами, см. https://github.com/nltk/nltk/blob/develop/nltk/text.py#L40

@staticmethod
def _default_context(tokens, i):
    """One left token and one right token, normalized to lowercase"""
    left = (tokens[i-1].lower() if i != 0 else '*START*')
    right = (tokens[i+1].lower() if i != len(tokens) - 1 else '*END*')
    return (left, right)

Из функции similar() мы видим, что она выполняет итерацию через слово в контексте, хранящемся в word_context_index, т.е. wci = self._word_context_index._word_to_contexts.

По существу, _word_to_contexts - это словарь, в котором ключи - это слова в корпусе, а значения - это левое и правое слова из https://github.com/nltk/nltk/blob/develop/nltk/text.py#L55:

    self._word_to_contexts = CFD((self._key(w), self._context_func(tokens, i))
                                 for i, w in enumerate(tokens))

И здесь мы видим, что это CFD, который является объектом nltk.probability.ConditionalFreqDist, который не включает сглаживание вероятности токена, см. полный код на https://github.com/nltk/nltk/blob/develop/nltk/probability.py#L1646.


Единственный возможный результат получения заключается в том, что функция similar() проходит через наиболее популярные слова в https://github.com/nltk/nltk/blob/develop/nltk/text.py#L402

Учитывая, что два ключа в объекте Counter имеют одинаковые подсчеты, слово с более ранним сортированным хешем будет печататься первым, а хэш ключа зависит от размера бит процессора, см. http://www.laurentluce.com/posts/python-dictionary-implementation/


Весь процесс поиска подобных слов сам по себе является детерминированным, поскольку:

  • корпус/вход фиксирован Text(gutenberg.words('melville-moby_dick.txt'))
  • контекст по умолчанию для каждого слова также фиксирован, т.е. self._word_context_index
  • вычисление условного распределения частот для _word_context_index._word_to_contexts является дискретным

За исключением случаев, когда функция выводит список most_common, который, когда есть связь в значениях Counter, выводит список ключей с учетом их хэшей.

В python2 нет причин получать другой вывод из разных экземпляров той же машины со следующим кодом:

$ python
>>> from nltk.book import *
>>> text1.similar('monstrous')
>>> exit()
$ python
>>> from nltk.book import *
>>> text1.similar('monstrous')
>>> exit()
$ python
>>> from nltk.book import *
>>> text1.similar('monstrous')
>>> exit()

Но в python3 каждый раз при запуске text1.similar('monstrous') он дает разные выходные данные, см. http://pastebin.com/ysAF6p6h


Вот простой эксперимент, чтобы доказать, что необычные различия хеширования между python2 и python3:

[email protected]:~$ python -c "from collections import Counter; x = Counter({'foo': 1, 'bar': 1, 'foobar': 1, 'barfoo': 1}); print(x.most_common())"
[('foobar', 1), ('foo', 1), ('bar', 1), ('barfoo', 1)]
[email protected]:~$ python -c "from collections import Counter; x = Counter({'foo': 1, 'bar': 1, 'foobar': 1, 'barfoo': 1}); print(x.most_common())"
[('foobar', 1), ('foo', 1), ('bar', 1), ('barfoo', 1)]
[email protected]:~$ python -c "from collections import Counter; x = Counter({'foo': 1, 'bar': 1, 'foobar': 1, 'barfoo': 1}); print(x.most_common())"
[('foobar', 1), ('foo', 1), ('bar', 1), ('barfoo', 1)]


[email protected]:~$ python3 -c "from collections import Counter; x = Counter({'foo': 1, 'bar': 1, 'foobar': 1, 'barfoo': 1}); print(x.most_common())"
[('barfoo', 1), ('foobar', 1), ('bar', 1), ('foo', 1)]
[email protected]:~$ python3 -c "from collections import Counter; x = Counter({'foo': 1, 'bar': 1, 'foobar': 1, 'barfoo': 1}); print(x.most_common())"
[('foo', 1), ('barfoo', 1), ('bar', 1), ('foobar', 1)]
[email protected]:~$ python3 -c "from collections import Counter; x = Counter({'foo': 1, 'bar': 1, 'foobar': 1, 'barfoo': 1}); print(x.most_common())"
[('bar', 1), ('barfoo', 1), ('foobar', 1), ('foo', 1)]