2015-08-06 4 views
14

A k skipgram - это ngram, которая является надмножеством всех ngrams и каждого (k-i) skipgram до (k-i) == 0 (который включает в себя 0 грамм пропуска). Итак, как эффективно вычислить эти skipgrams в python?Как вычислить skipgrams в python?

Ниже приводится код, который я попробовал, но это не делает, как и ожидалось:

<pre> 
    input_list = ['all', 'this', 'happened', 'more', 'or', 'less'] 
    def find_skipgrams(input_list, N,K): 
    bigram_list = [] 
    nlist=[] 

    K=1 
    for k in range(K+1): 
     for i in range(len(input_list)-1): 
      if i+k+1<len(input_list): 
       nlist=[] 
       for j in range(N+1): 
        if i+k+j+1<len(input_list): 
        nlist.append(input_list[i+k+j+1]) 

      bigram_list.append(nlist) 
    return bigram_list 

</pre> 

Приведенный выше код не делает правильно, но печать find_skipgrams(['all', 'this', 'happened', 'more', 'or', 'less'],2,1) дает следующий вывод

[[ «это» «больше», «больше», «больше», «больше», «больше», «0», « » или «меньше», «или», «меньше», «меньше»], ['произошло', 'больше', 'или'], ['больше', 'или', 'меньше'], ['или', 'less'], ['less'], ['less']]

код, перечисленные здесь также не дает правильный вывод: https://github.com/heaven00/skipgram/blob/master/skipgram.py

печати skipgram_ndarray ("Как тебя зовут") дает: [ 'Что,', 'есть твой', «Ваш , name ',' name, ',' What, your ',' is, name ']

имя - это униграмма!

+2

Что вы пытаетесь предпринять? – msw

+0

@msw обновил вопрос !! – stackit

ответ

13

Из paper, что OP ссылки, следующая строка:

Боевики убили в непрекращающихся боевых

Доходность:

2-скип-би-г = {Повстанцы убили мятежник в мятежники продолжающихся, убито в, убита продолжающейся, убитую борьбе, в текущем, в боевых действиях, продолжающиеся боевые действия}

2-скипа -tri-grams = {повстанцы убиты, повстанцы убиты, повстанцы убили боевые действия, повстанцы в продолжающихся боевиках в боевые действия, повстанцы продолжали боевые действия, убитые в ходе, убитые в боевых действиях, убивали продолжающиеся боевые действия в ходе продолжающихся боевых действий}.

С небольшими изменениями в ngrams код NLTK (в https://github.com/nltk/nltk/blob/develop/nltk/util.py#L383):

from itertools import chain, combinations 
import copy 
from nltk.util import ngrams 

def pad_sequence(sequence, n, pad_left=False, pad_right=False, pad_symbol=None): 
    if pad_left: 
     sequence = chain((pad_symbol,) * (n-1), sequence) 
    if pad_right: 
     sequence = chain(sequence, (pad_symbol,) * (n-1)) 
    return sequence 

def skipgrams(sequence, n, k, pad_left=False, pad_right=False, pad_symbol=None): 
    sequence_length = len(sequence) 
    sequence = iter(sequence) 
    sequence = pad_sequence(sequence, n, pad_left, pad_right, pad_symbol) 

    if sequence_length + pad_left + pad_right < k: 
     raise Exception("The length of sentence + padding(s) < skip") 

    if n < k: 
     raise Exception("Degree of Ngrams (n) needs to be bigger than skip (k)")  

    history = [] 
    nk = n+k 

    # Return point for recursion. 
    if nk < 1: 
     return 
    # If n+k longer than sequence, reduce k by 1 and recur 
    elif nk > sequence_length: 
     for ng in skipgrams(list(sequence), n, k-1): 
      yield ng 

    while nk > 1: # Collects the first instance of n+k length history 
     history.append(next(sequence)) 
     nk -= 1 

    # Iterative drop first item in history and picks up the next 
    # while yielding skipgrams for each iteration. 
    for item in sequence: 
     history.append(item) 
     current_token = history.pop(0)  
     # Iterates through the rest of the history and 
     # pick out all combinations the n-1grams 
     for idx in list(combinations(range(len(history)), n-1)): 
      ng = [current_token] 
      for _id in idx: 
       ng.append(history[_id]) 
      yield tuple(ng) 

    # Recursively yield the skigrams for the rest of seqeunce where 
    # len(sequence) < n+k 
    for ng in list(skipgrams(history, n, k-1)): 
     yield ng 

Давайте делать некоторые doctest, чтобы соответствовать примеру в статье:

>>> two_skip_bigrams = list(skipgrams(text, n=2, k=2)) 
[('Insurgents', 'killed'), ('Insurgents', 'in'), ('Insurgents', 'ongoing'), ('killed', 'in'), ('killed', 'ongoing'), ('killed', 'fighting'), ('in', 'ongoing'), ('in', 'fighting'), ('ongoing', 'fighting')] 
>>> two_skip_trigrams = list(skipgrams(text, n=3, k=2)) 
[('Insurgents', 'killed', 'in'), ('Insurgents', 'killed', 'ongoing'), ('Insurgents', 'killed', 'fighting'), ('Insurgents', 'in', 'ongoing'), ('Insurgents', 'in', 'fighting'), ('Insurgents', 'ongoing', 'fighting'), ('killed', 'in', 'ongoing'), ('killed', 'in', 'fighting'), ('killed', 'ongoing', 'fighting'), ('in', 'ongoing', 'fighting')] 

Но обратите внимание, что если n+k > len(sequence), он будет давать те же эффекты, что и skipgrams(sequence, n, k-1) (это не ошибка, это отказоустойчивая функция), например

>>> three_skip_trigrams = list(skipgrams(text, n=3, k=3)) 
>>> three_skip_fourgrams = list(skipgrams(text, n=4, k=3)) 
>>> four_skip_fourgrams = list(skipgrams(text, n=4, k=4)) 
>>> four_skip_fivegrams = list(skipgrams(text, n=5, k=4)) 
>>> 
>>> print len(three_skip_trigrams), three_skip_trigrams 
10 [('Insurgents', 'killed', 'in'), ('Insurgents', 'killed', 'ongoing'), ('Insurgents', 'killed', 'fighting'), ('Insurgents', 'in', 'ongoing'), ('Insurgents', 'in', 'fighting'), ('Insurgents', 'ongoing', 'fighting'), ('killed', 'in', 'ongoing'), ('killed', 'in', 'fighting'), ('killed', 'ongoing', 'fighting'), ('in', 'ongoing', 'fighting')] 
>>> print len(three_skip_fourgrams), three_skip_fourgrams 
5 [('Insurgents', 'killed', 'in', 'ongoing'), ('Insurgents', 'killed', 'in', 'fighting'), ('Insurgents', 'killed', 'ongoing', 'fighting'), ('Insurgents', 'in', 'ongoing', 'fighting'), ('killed', 'in', 'ongoing', 'fighting')] 
>>> print len(four_skip_fourgrams), four_skip_fourgrams 
5 [('Insurgents', 'killed', 'in', 'ongoing'), ('Insurgents', 'killed', 'in', 'fighting'), ('Insurgents', 'killed', 'ongoing', 'fighting'), ('Insurgents', 'in', 'ongoing', 'fighting'), ('killed', 'in', 'ongoing', 'fighting')] 
>>> print len(four_skip_fivegrams), four_skip_fivegrams 
1 [('Insurgents', 'killed', 'in', 'ongoing', 'fighting')] 

Это позволяет n == k но запретить n > k, как показано в строках:

if n < k: 
     raise Exception("Degree of Ngrams (n) needs to be bigger than skip (k)")  

Для понимания ради, давайте попробуем понять "мистическое" линия:

for idx in list(combinations(range(len(history)), n-1)): 
    pass # Do something 

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

>>> from itertools import combinations 
>>> x = [0,1,2,3,4,5] 
>>> list(combinations(x,2)) 
[(0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5)] 

И поскольку индексы списка жетонов всегда уникальны, например.

>>> sent = ['this', 'is', 'a', 'foo', 'bar'] 
>>> current_token = sent.pop(0) # i.e. 'this' 
>>> range(len(sent)) 
[0,1,2,3] 

Это можно вычислить возможную combinations (without replacement) диапазона:

>>> n = 3 
>>> list(combinations(range(len(sent)), n-1)) 
[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] 

Если сопоставить показатели вернуться к списку лексем:

>>> [tuple(sent[id] for id in idx) for idx in combinations(range(len(sent)), 2) 
[('is', 'a'), ('is', 'foo'), ('is', 'bar'), ('a', 'foo'), ('a', 'bar'), ('foo', 'bar')] 

Тогда мы сцепить с current_token , мы получаем skipgrams для текущего токена и контекста + окно пропуска:

>>> [tuple([current_token]) + tuple(sent[id] for id in idx) for idx in combinations(range(len(sent)), 2)] 
[('this', 'is', 'a'), ('this', 'is', 'foo'), ('this', 'is', 'bar'), ('this', 'a', 'foo'), ('this', 'a', 'bar'), ('this', 'foo', 'bar')] 

После этого мы переходим к следующему слову.

+0

Кроме того, проблема поднята: https://github.com/nltk/nltk/issues/1070 – alvas

+0

хорошая работа, но я бы хотел, чтобы она вернула само предложение, если длина превышает – stackit

+0

, можете ли вы ответить на это: http: // stackoverflow .com/questions/31827756/static-sentence-suggestion-model-like-spell-проверка – stackit

0

Как об использовании чужой реализации https://github.com/heaven00/skipgram/blob/master/skipgram.py, где k = skip_size и n=ngram_order:

def skipgram_ndarray(sent, k=1, n=2): 
    """ 
    This is not exactly a vectorized version, because we are still 
    using a for loop 
    """ 
    tokens = sent.split() 
    if len(tokens) < k + 2: 
     raise Exception("REQ: length of sentence > skip + 2") 
    matrix = np.zeros((len(tokens), k + 2), dtype=object) 
    matrix[:, 0] = tokens 
    matrix[:, 1] = tokens[1:] + [''] 
    result = [] 
    for skip in range(1, k + 1): 
     matrix[:, skip + 1] = tokens[skip + 1:] + [''] * (skip + 1) 
    for index in range(1, k + 2): 
     temp = matrix[:, 0] + ',' + matrix[:, index] 
     map(result.append, temp.tolist()) 
    limit = (((k + 1) * (k + 2))/6) * ((3 * n) - (2 * k) - 6) 
    return result[:limit] 

def skipgram_list(sent, k=1, n=2): 
    """ 
    Form skipgram features using list comprehensions 
    """ 
    tokens = sent.split() 
    tokens_n = ['''tokens[index + j + {0}]'''.format(index) 
       for index in range(n - 1)] 
    x = '(tokens[index], ' + ', '.join(tokens_n) + ')' 
    query_part1 = 'result = [' + x + ' for index in range(len(tokens))' 
    query_part2 = ' for j in range(1, k+2) if index + j + n < len(tokens)]' 
    exec(query_part1 + query_part2) 
    return result 
+0

Нет, это не работает, print skipgram_ndarray («Как ваше имя») дает: ['What, is', 'is, your', 'your, name', 'name,', 'What, your', ' is, name '] имя является униграммой, а другая функция является еще более неправильной – stackit

+1

, и она не работает для k = 3 – stackit

+0

Эта реализация жестко закодирована для 'k <3'. Он работает, он просто не масштабируется (а также с большим количеством хаков ... 'exec (...)' смешно). – alvas

5

EDITED

последняя NLTK версия 3.2.5 имеет skipgrams реализован.

Вот очиститель реализации от @jnothman из репо NLTK: https://github.com/nltk/nltk/blob/develop/nltk/util.py#L538

def skipgrams(sequence, n, k, **kwargs): 
    """ 
    Returns all possible skipgrams generated from a sequence of items, as an iterator. 
    Skipgrams are ngrams that allows tokens to be skipped. 
    Refer to http://homepages.inf.ed.ac.uk/ballison/pdf/lrec_skipgrams.pdf 

    :param sequence: the source data to be converted into trigrams 
    :type sequence: sequence or iter 
    :param n: the degree of the ngrams 
    :type n: int 
    :param k: the skip distance 
    :type k: int 
    :rtype: iter(tuple) 
    """ 

    # Pads the sequence as desired by **kwargs. 
    if 'pad_left' in kwargs or 'pad_right' in kwargs: 
    sequence = pad_sequence(sequence, n, **kwargs) 

    # Note when iterating through the ngrams, the pad_right here is not 
    # the **kwargs padding, it's for the algorithm to detect the SENTINEL 
    # object on the right pad to stop inner loop. 
    SENTINEL = object() 
    for ngram in ngrams(sequence, n + k, pad_right=True, right_pad_symbol=SENTINEL): 
    head = ngram[:1] 
    tail = ngram[1:] 
    for skip_tail in combinations(tail, n - 1): 
     if skip_tail[-1] is SENTINEL: 
      continue 
     yield head + skip_tail 

[выход]:

>>> from nltk.util import skipgrams 
>>> sent = "Insurgents killed in ongoing fighting".split() 
>>> list(skipgrams(sent, 2, 2)) 
[('Insurgents', 'killed'), ('Insurgents', 'in'), ('Insurgents', 'ongoing'), ('killed', 'in'), ('killed', 'ongoing'), ('killed', 'fighting'), ('in', 'ongoing'), ('in', 'fighting'), ('ongoing', 'fighting')] 
>>> list(skipgrams(sent, 3, 2)) 
[('Insurgents', 'killed', 'in'), ('Insurgents', 'killed', 'ongoing'), ('Insurgents', 'killed', 'fighting'), ('Insurgents', 'in', 'ongoing'), ('Insurgents', 'in', 'fighting'), ('Insurgents', 'ongoing', 'fighting'), ('killed', 'in', 'ongoing'), ('killed', 'in', 'fighting'), ('killed', 'ongoing', 'fighting'), ('in', 'ongoing', 'fighting')] 
+0

COOL, где ссылка? – stackit

+0

https://github.com/nltk/nltk/issues/1070 – alvas

4

Хотя это расстанутся полностью из кода и перенести его на внешнюю библиотеку ; вы можете использовать Colibri Core (https://proycon.github.io/colibri-core) для экстракции skipgram. Это библиотека, написанная специально для эффективного извлечения n-грамма и пропусков из больших текстовых корпусов. База кода находится в C++ (для скорости/эффективности), но доступна привязка Python.

Вы по праву отметили эффективность, так как извлечение пропусков быстро показывает экспоненциальную сложность, что может быть не большой проблемой, если вы только передадите предложение, как в своем input_list, но становится проблематичным, если вы отпустите его на большие данные корпуса. Чтобы смягчить это, вы можете установить такие параметры, как порог возникновения, или потребовать, чтобы каждый пропуск пропускаемого файла был заполнен не менее x отдельных n-граммов.

import colibricore 

#Prepare corpus data (will be encoded for efficiency) 
corpusfile_plaintext = "somecorpus.txt" #input, one sentence per line 
encoder = colibricore.ClassEncoder() 
encoder.build(corpusfile_plaintext) 
corpusfile = "somecorpus.colibri.dat" #corpus output 
classfile = "somecorpus.colibri.cls" #class encoding output 
encoder.encodefile(corpusfile_plaintext,corpusfile) 
encoder.save(classfile) 

#Set options for skipgram extraction (mintokens is the occurrence threshold, maxlength maximum ngram/skipgram length) 
colibricore.PatternModelOptions(mintokens=2,maxlength=8,doskipgrams=True) 

#Instantiate an empty pattern model 
model = colibricore.UnindexedPatternModel() 

#Train the model on the encoded corpus file (this does the skipgram extraction) 
model.train(corpusfile, options) 

#Load a decoder so we can view the output 
decoder = colibricore.ClassDecoder(classfile) 

#Output all skipgrams 
for pattern in model: 
    if pattern.category() == colibricore.Category.SKIPGRAM: 
     print(pattern.tostring(decoder)) 

Существует более обширный учебник Python обо всем этом на веб-сайте.

Отказ от ответственности: Я автор Colibri Ядра

+0

ya Я пробовал это, прежде чем писать этот вопрос, но не смог установить colibri на ubuntu – stackit

+0

Я улучшил процедуру установки и инструкции на прошлой неделе, я надеюсь, что она установит с меньшими проблемами теперь. – proycon

+0

@proycon, можно ли создавать типы уток в оболочках python colibri, чтобы интерфейс выглядел как «NLTK», например. 'colibri.ngrams (текст, n = 3)' или 'colibri.skipgram (текст, n = 3, k = 2)'? Или проще перекомпилировать некоторые куски обложек 'colibri' в репозитории NLTK? – alvas

1

См this для полной информации.

Приведенный ниже пример уже упоминался в нем о его использовании и работает как шарм!

>>>sent = "Insurgents killed in ongoing fighting".split() 
>>>list(skipgrams(sent, 2, 2)) 
[('Insurgents', 'killed'), ('Insurgents', 'in'), ('Insurgents', 'ongoing'), ('killed', 'in'), ('killed', 'ongoing'), ('killed', 'fighting'), ('in', 'ongoing'), ('in', 'fighting'), ('ongoing', 'fighting')] 
+1

Функция skipgram была создана из-за этого старого вопроса в nltk после того, как она была запрошена на их форуме – stackit