我正在为Range Concatenation Grammar编写一个CKY解析器。我想使用树库作为语法,所以语法会很大。我在Python中编写了一个原型1,当我模拟几十个句子的树库时它看起来效果很好,但是内存使用是不可接受的。我尝试用C ++编写它,但到目前为止,由于我之前从未使用过C ++,因此非常令人沮丧。这里有一些数据(n是语法所依据的句子数):
n mem
9 173M
18 486M
36 836M
这种增长模式是给出最佳优先算法的预期,但开销量是我所关心的。根据heapy的内存使用量比这些数字小十倍,valgrind报告了类似的内容。导致这种差异的原因是什么,我可以用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
答案 0 :(得分:2)
我认为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 :(得分:2)
答案 2 :(得分:1)
在这些情况下,首先要做的是:
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将启用the switch optimization of 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])
并且在mostprobableparse里面:
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
我会在此停留,但我相信我们可以继续优化,因为我们可以更深入地了解问题。
一系列单元测试可用于断言每个优化都不会引入任何细微的错误。
旁注,尝试使用空格而不是标签。