Дерево вероятностей для предложений в nltk с использованием зависимостей lookahead и backback
Предоставляет ли nltk или любой другой инструмент NLP построение деревьев вероятности на основе входных предложений, таким образом сохраняя языковую модель входного текста в дереве словаря, следующий пример дает мне приблизительную идею, но мне нужна такая же функциональность, что слово Wt не просто вероятностно смоделировано по прошлым входным словам (истории) Wt-n, но также и в виде таких слов, как Wt + m. Кроме того, счетчик обратного и ожидаемого слов также должен быть 2 или более, т.е. Bigrams или больше. Есть ли другие библиотеки на python, которые достигают этого?
from collections import defaultdict
import nltk
import math
ngram = defaultdict(lambda: defaultdict(int))
corpus = "The cat is cute. He jumps and he is happy."
for sentence in nltk.sent_tokenize(corpus):
tokens = map(str.lower, nltk.word_tokenize(sentence))
for token, next_token in zip(tokens, tokens[1:]):
ngram[token][next_token] += 1
for token in ngram:
total = math.log10(sum(ngram[token].values()))
ngram[token] = {nxt: math.log10(v) - total for nxt, v in ngram[token].items()}
решение требует как lookahead, так и backback, а специально подобранный словарь может помочь в решении этой проблемы. Можно также указать на соответствующие ресурсы, которые говорят о внедрении такой системы. Похоже, что nltk.models делает что-то подобное, но больше не доступно. Существуют ли какие-либо существующие шаблоны проектирования в НЛП, которые реализуют эту идею? пропущенные граммовые модели похожи на эту идею, но я считаю, что это должно было быть реализовано уже где-то.
Ответы
Ответ 1
Если я правильно понимаю ваш вопрос, вы ищете способ предсказать вероятность слова, учитывая его окружающий контекст (а не только обратный контекст, но и прямой контекст).
Одним быстрым взломом для вашей цели является подготовка двух разных языковых моделей. Один справа налево, а другой слева направо, а затем вероятность слова с учетом его контекста будет нормализованной суммой как прямого, так и обратного контекстов.
Расширение кода:
from collections import defaultdict
import nltk
from nltk.tokenize import word_tokenize
import numpy as np
ngram = defaultdict(lambda: defaultdict(int))
ngram_rev = defaultdict(lambda: defaultdict(int)) #reversed n-grams
corpus = "The cat is cute. He jumps and he is happy."
for sentence in nltk.sent_tokenize(corpus):
tokens = map(str.lower, nltk.word_tokenize(sentence))
for token, next_token in zip(tokens, tokens[1:]):
ngram[token][next_token] += 1
for token, rev_token in zip(tokens[1:], tokens):
ngram_rev[token][rev_token] += 1
for token in ngram:
total = np.log(np.sum(ngram[token].values()))
total_rev = np.log(np.sum(ngram_rev[token].values()))
ngram[token] = {nxt: np.log(v) - total
for nxt, v in ngram[token].items()}
ngram_rev[token] = {prv: np.log(v) - total_rev
for prv, v in ngram_rev[token].items()}
Теперь контекст находится как в ngram, так и в ngram_rev, которые соответственно удерживают передний и задний контексты.
Вы также должны учитывать сглаживание. То есть, если данная фраза не видна в вашем учебном корпусе, вы просто получите нулевые вероятности. Чтобы этого избежать, существует множество методов сглаживания, наиболее простым из которых является сглаживание add-on.
Ответ 2
Обычный алгоритм ngram традиционно работает только с предварительным контекстом, и по уважительной причине: теггер bigram принимает решения, рассматривая теги двух последних слов плюс текущее слово. Поэтому, если вы не отметили два прохода, тег следующего слова еще не известен. Но вас интересуют слова ngrams, а не тег ngrams, поэтому ничто не мешает вам тренировать теггер ngram, где ngram состоит из слов с обеих сторон. И вы действительно можете сделать это легко с помощью NLTK.
Агенты тегов NLTK ngram все делают тег ngrams, слева; но вы можете легко получить свой собственный теггер из своего абстрактного базового класса, ContextTagger
:
import nltk
from nltk.tag import ContextTagger
class TwoSidedTagger(ContextTagger):
left = 2
right = 1
def context(self, tokens, index, history):
left = self.left
right = self.right
tokens = tuple(t.lower() for t in tokens)
if index < left:
tokens = ("<start>",) * left + tokens
index += left
if index + right >= len(tokens):
tokens = tokens + ("<end>",) * right
return tokens[index-left:index+right+1]
Это определяет тегграмм теггера (2 + 1 + 1), где текущее слово является третьим в ngram, а не последним, как обычно. Затем вы можете инициализировать и обучать таггера, как обычные тегеры ngram (см. Главу 5 книги NLTK, особенно разделы 5.4ff). Давайте сначала посмотрим, как вы построите теггер части речи, используя часть тела Брауна в качестве обучающих данных:
data = list(nltk.corpus.brown.tagged_sents(categories="news"))
train_sents = data[400:]
test_sents = data[:400]
twosidedtagger = TwoSidedTagger({}, backoff=nltk.DefaultTagger('NN'))
twosidedtagger._train(train_sents)
Как и все маркеры ngram в NLTK, этот будет делегировать теггеру отсрочки, если его попросят пометить ngram, который он не видел во время обучения.
Для простоты я использовал простой "тег по умолчанию" в качестве тегатора backoff, но вам, вероятно, понадобится использовать что-то более мощное (см. Главу NLTK снова).
Затем вы можете использовать свой теггер для добавления нового текста или оценки его с уже отмеченным тестовым набором:
>>> print(twosidedtagger.tag("There were dogs everywhere .".split()))
>>> print(twosidedtagger.evaluate(test_sents))
Предсказание слов:
Вышеупомянутый теггер назначает тег POS, рассматривая близлежащие слова; но ваша цель - предсказать само слово, поэтому вам нужны разные данные обучения и другой тег по умолчанию. API NLTK ожидает данные обучения в форме (word, LABEL)
, где LABEL
- это значение, которое вы хотите сгенерировать. В вашем случае LABEL
- это только текущее слово; поэтому сделайте свои данные обучения следующим образом:
data = [ zip(s,s) for s in nltk.corpus.brown.sents(categories="news") ]
train_sents = data[400:]
test_sents = data[:400]
twosidedtagger = TwoSidedTagger({}, backoff=nltk.DefaultTagger('the')) # most common word
twosidedtagger._train(train_sents)
Не имеет смысла, чтобы целевое слово появлялось в "контексте" ngram, поэтому вы также должны изменить метод context()
, чтобы возвращенная ngram не включала его:
def context(self, tokens, index, history):
...
return tokens[index-left:index] + tokens[index+1:index+right+1]
Этот теггер использует триграммы, состоящие из двух слов слева и один справа от текущего слова.
С этими изменениями вы создадите теггер, который выводит наиболее вероятное слово в любой позиции. Попробуйте и как вам это нравится.
Прогноз:
Я ожидаю, что вам понадобится огромное количество данных для обучения, прежде чем вы сможете получить достойную производительность. Проблема в том, что тегеры ngram могут предлагать только тег для контекстов, которые они видели во время обучения.
Чтобы создать теггер, который обобщает, подумайте об использовании NLTK для обучения "последовательного классификатора". Вы можете использовать любые функции, которые хотите, включая слова до и после - конечно, насколько хорошо он будет работать, это ваша проблема. API-интерфейс классификатора NLTK аналогичен API-интерфейсу ContextTagger
, но функция контекста (функция функции) возвращает словарь, а не кортеж. Опять же, см. Книгу NLTK и исходный код.