использование памяти вероятностным парсером - PullRequest
4 голосов
/ 21 марта 2011

Я пишу CKY-анализатор для грамматики конкатенации диапазонов.Я хочу использовать банк деревьев в качестве грамматики, поэтому грамматика будет большой.Я написал прототип 1 на Python, и он, кажется, хорошо работает, когда я моделирую банк деревьев из нескольких десятков предложений, но использование памяти недопустимо.Я пытался написать его на C ++, но пока это было очень сложно, так как я никогда не использовал C ++ раньше.Вот некоторые данные (n - количество предложений, на которых основана грамматика):

n    mem
9    173M
18   486M
36   836M

Эта модель роста - это то, что следует ожидать при использовании алгоритма лучший первый, но количество служебных данных - это то, что меня беспокоит,Использование памяти по данным heapy в десять раз меньше, чем эти цифры, сообщил Вальгринд нечто подобное.В чем причина этого несоответствия и могу ли я что-то с этим сделать в Python (или Cython)?Возможно это из-за фрагментации?Или, может быть, это накладные расходы на словари python?

Некоторый фон: две важные структуры данных - это план отображения краев на вероятности и диаграмма, представляющая собой словарь, отображающий нетерминалы и позиции на ребра.Повестка дня реализуется с помощью heapdict (который внутренне использует dict и список heapq), диаграммой со словарем, отображающим нетерминалы и позиции по краям.Повестка дня часто вставляется и удаляется из, диаграмма получает только вставки и поиски.Я представляю ребра с кортежами так:

(("S", 111), ("NP", 010), ("VP", 100, 001))

Строки - это нетерминальные метки из грамматики, позиции кодируются как битовая маска.Может быть несколько позиций, когда составляющая является прерывистой.Таким образом, это ребро может представлять собой анализ «Счастливы ли Мэри», где «есть» и «счастливы» оба принадлежат VP. Словарь диаграммы индексируется первым элементом этого ребра, («S», 111) в этомВ новой версии я попытался транспонировать это представление в надежде, что оно сэкономит память за счет повторного использования:

(("S", "NP", "VP), (111, 100, 011))

Я подумал, что Python сохранит первую часть только один раз, если это произойдет в сочетании сразные позиции, хотя на самом деле я не уверен, что это правда. В любом случае, это, похоже, не имеет никакого значения.

Так что в основном меня интересует, стоит ли преследовать мою реализацию Python.далее, в том числе выполнение операций с Cython и различными структурами данных, или единственно приемлемым вариантом является написание его с нуля на C ++.

ОБНОВЛЕНИЕ : после некоторых улучшений у меня больше не возникает проблем сиспользование памяти. Я работаю над оптимизированной версией Cython. Я награжу награду самым полезным предложением для увеличенияэффективность кода.Есть аннотированная версия в http://student.science.uva.nl/~acranenb/plcfrs_cython.html

1 https://github.com/andreasvc/disco-dop/ - запустите test.py, чтобы разобрать некоторые предложения.Требуется Python 2.6, nltk и heapdict

Ответы [ 3 ]

2 голосов
/ 27 марта 2011

Вы пытались запустить свое приложение с PyPy, а не с CPython?

PyPy на лот умнее, чем CPython, замечать общие черты и избегать лишних затрат памяти, связанных с ненужным дублированием.

В любом случае стоит попробовать: http://pypy.org/

2 голосов
/ 21 марта 2011

Я полагал, что Python сохранит первую часть только один раз, если это произойдет в сочетании с различными позициями

Не обязательно:

>>> ("S", "NP", "VP") is ("S", "NP", "VP")
False

Возможно, вы захотите intern всех строк, относящихся к нетерминалам, так как вы, кажется, создаете много из них в rcgrules.py. Если вы хотите intern кортеж, то сначала превратите его в строку:

>>> intern("S NP VP") is intern(' '.join('S', 'NP', 'VP'))
True

В противном случае вам придется «копировать» кортежи, а не создавать их заново.

(Если вы новичок в C ++, то переписывание такого алгоритма в нем вряд ли даст большую выгоду памяти. Сначала вам нужно будет оценить различные реализации хеш-таблиц и узнать о поведении копирования в его контейнерах. Я нашел boost::unordered_map довольно расточительным с большим количеством маленьких хеш-таблиц.)

1 голос
/ 30 марта 2011

Первое, что нужно сделать в этих случаях - это всегда профиль:

15147/297    0.032    0.000    0.041    0.000 tree.py:102(__eq__)
15400/200    0.031    0.000    0.106    0.001 tree.py:399(convert)
        1    0.023    0.023    0.129    0.129 plcfrs_cython.pyx:52(parse)
6701/1143    0.022    0.000    0.043    0.000 heapdict.py:45(_min_heapify)
    18212    0.017    0.000    0.023    0.000 plcfrs_cython.pyx:38(__richcmp__)
10975/10875    0.017    0.000    0.035    0.000 tree.py:75(__init__)
     5772    0.016    0.000    0.050    0.000 tree.py:665(__init__)
      960    0.016    0.000    0.025    0.000 plcfrs_cython.pyx:118(deduced_from)
    46938    0.014    0.000    0.014    0.000 tree.py:708(_get_node)
25220/2190    0.014    0.000    0.016    0.000 tree.py:231(subtrees)
    10975    0.013    0.000    0.023    0.000 tree.py:60(__new__)
    49441    0.013    0.000    0.013    0.000 {isinstance}
    16748    0.008    0.000    0.015    0.000 {hasattr}

Первое, что я заметил, это то, что очень мало функций из самого модуля Cython. Большинство из них происходят из модуля tree.py и, возможно, это узкое место.

Сосредоточив внимание на стороне Cython, я вижу функцию richcmp :

мы можем оптимизировать его, просто добавив тип значений в объявление метода

def __richcmp__(ChartItem self, ChartItem other, int op):
        ....

Это снижает значение

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
....
18212    0.011    0.000    0.015    0.000 plcfrs_cython.pyx:38(__richcmp__)

Добавление синтаксиса elif вместо единственного if включит оптимизацию переключения cython

    if op == 0: return self.label < other.label or self.vec < other.vec
    elif op == 1: return self.label <= other.label or self.vec <= other.vec
    elif op == 2: return self.label == other.label and self.vec == other.vec
    elif op == 3: return self.label != other.label or self.vec != other.vec
    elif op == 4: return self.label > other.label or self.vec > other.vec
    elif op == 5: return self.label >= other.label or self.vec >= other.vec

получение:

17963    0.002    0.000    0.002    0.000 plcfrs_cython.pyx:38(__richcmp__)

пытаясь выяснить, откуда происходит преобразование tree.py:399, я обнаружил, что эта функция внутри dopg.py занимает все это время

  def removeids(tree):
""" remove unique IDs introduced by the Goodman reduction """
result = Tree.convert(tree)
for a in result.subtrees(lambda t: '@' in t.node):
    a.node = a.node.rsplit('@', 1)[0]
if isinstance(tree, ImmutableTree): return result.freeze()
return result

Теперь я не уверен, является ли каждый узел в дереве ChartItem и значение getitem используется где-то еще, но добавляет эти изменения:

cdef class ChartItem:
cdef public str label
cdef public str root
cdef public long vec
cdef int _hash
__slots__ = ("label", "vec", "_hash")
def __init__(ChartItem self, label, int vec):
    self.label = intern(label) #.rsplit('@', 1)[0])
    self.root = intern(label.rsplit('@', 1)[0])
    self.vec = vec
    self._hash = hash((self.label, self.vec))
def __hash__(self):
    return self._hash
def __richcmp__(ChartItem self, ChartItem other, int op):
    if op == 0: return self.label < other.label or self.vec < other.vec
    elif op == 1: return self.label <= other.label or self.vec <= other.vec
    elif op == 2: return self.label == other.label and self.vec == other.vec
    elif op == 3: return self.label != other.label or self.vec != other.vec
    elif op == 4: return self.label > other.label or self.vec > other.vec
    elif op == 5: return self.label >= other.label or self.vec >= other.vec
def __getitem__(ChartItem self, int n):
    if n == 0: return self.root
    elif n == 1: return self.vec
def __repr__(self):
    #would need bitlen for proper padding
    return "%s[%s]" % (self.label, bin(self.vec)[2:][::-1]) 

и внутри наиболее вероятного разреза:

from libc cimport pow
def mostprobableparse...
            ...
    cdef dict parsetrees = <dict>defaultdict(float)
    cdef float prob
    m = 0
    for n,(a,prob) in enumerate(derivations):
        parsetrees[a] += pow(e,prob)
        m += 1

Я получаю:

         189345 function calls (173785 primitive calls) in 0.162 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
6701/1143    0.025    0.000    0.037    0.000 heapdict.py:45(_min_heapify)
        1    0.023    0.023    0.120    0.120 plcfrs_cython.pyx:54(parse)
      960    0.018    0.000    0.030    0.000 plcfrs_cython.pyx:122(deduced_from)
 5190/198    0.011    0.000    0.015    0.000 tree.py:102(__eq__)
     6619    0.006    0.000    0.006    0.000 heapdict.py:67(_swap)
     9678    0.006    0.000    0.008    0.000 plcfrs_cython.pyx:137(concat)

, поэтому следующие шаги должны оптимизировать heapify и deduced_from

deduce_from можно оптимизировать немного больше:

cdef inline deduced_from(ChartItem Ih, double x, pyCx, pyunary, pylbinary, pyrbinary, int bitlen):
cdef str I = Ih.label
cdef int Ir = Ih.vec
cdef list result = []
cdef dict Cx = <dict>pyCx
cdef dict unary = <dict>pyunary
cdef dict lbinary = <dict>pylbinary
cdef dict rbinary = <dict>pyrbinary
cdef ChartItem Ilh
cdef double z
cdef double y
cdef ChartItem I1h 
for rule, z in unary[I]:
    result.append((ChartItem(rule[0][0], Ir), ((x+z,z), (Ih,))))
for rule, z in lbinary[I]:
    for I1h, y in Cx[rule[0][2]].items():
        if concat(rule[1], Ir, I1h.vec, bitlen):
            result.append((ChartItem(rule[0][0], Ir ^ I1h.vec), ((x+y+z, z), (Ih, I1h))))
for rule, z in rbinary[I]:
    for I1h, y in Cx[rule[0][1]].items():
        if concat(rule[1], I1h.vec, Ir, bitlen):
            result.append((ChartItem(rule[0][0], I1h.vec ^ Ir), ((x+y+z, z), (I1h, Ih))))
return result

Я на этом остановлюсь, хотя я уверен, что мы сможем продолжить оптимизацию по мере того, как будет получено больше понимания проблемы.

Серия юнит-тестов была бы полезна, чтобы утверждать, что каждая оптимизация не вносит какой-либо тонкой ошибки.

Примечание: попробуйте использовать пробелы вместо вкладок.

...