2023-09-12 09:50:01 +03:00
|
|
|
# cython: infer_types=True
|
2017-10-27 20:45:57 +03:00
|
|
|
"""Implements the projectivize/deprojectivize mechanism in Nivre & Nilsson 2005
|
2017-05-22 12:48:02 +03:00
|
|
|
for doing pseudo-projective parsing implementation uses the HEAD decoration
|
|
|
|
scheme.
|
|
|
|
"""
|
2016-02-24 13:26:25 +03:00
|
|
|
from copy import copy
|
2023-06-14 18:48:41 +03:00
|
|
|
|
|
|
|
from cython.operator cimport dereference as deref
|
|
|
|
from cython.operator cimport preincrement as incr
|
2022-02-21 17:02:21 +03:00
|
|
|
from libc.limits cimport INT_MAX
|
|
|
|
from libc.stdlib cimport abs
|
|
|
|
from libcpp cimport bool
|
2022-06-08 20:34:11 +03:00
|
|
|
from libcpp.string cimport string, to_string
|
|
|
|
from libcpp.unordered_set cimport unordered_set
|
2023-06-14 18:48:41 +03:00
|
|
|
from libcpp.vector cimport vector
|
2016-02-22 16:40:40 +03:00
|
|
|
|
2020-07-31 00:30:54 +03:00
|
|
|
from ...tokens.doc cimport Doc, set_children_from_heads
|
2020-03-02 13:48:10 +03:00
|
|
|
|
2020-07-31 00:30:54 +03:00
|
|
|
from ...errors import Errors
|
2017-11-17 20:55:13 +03:00
|
|
|
|
2017-05-22 12:48:02 +03:00
|
|
|
DELIMITER = '||'
|
|
|
|
|
2016-03-01 12:09:08 +03:00
|
|
|
|
2016-02-24 13:26:25 +03:00
|
|
|
def ancestors(tokenid, heads):
|
2017-10-27 20:45:57 +03:00
|
|
|
# Returns all words going from the word up the path to the root. The path
|
|
|
|
# to root cannot be longer than the number of words in the sentence. This
|
|
|
|
# function ends after at most len(heads) steps, because it would otherwise
|
|
|
|
# loop indefinitely on cycles.
|
2016-02-24 13:26:25 +03:00
|
|
|
head = tokenid
|
2016-02-22 16:40:40 +03:00
|
|
|
cnt = 0
|
|
|
|
while heads[head] != head and cnt < len(heads):
|
|
|
|
head = heads[head]
|
|
|
|
cnt += 1
|
|
|
|
yield head
|
2017-10-27 20:45:57 +03:00
|
|
|
if head is None:
|
2016-02-22 16:40:40 +03:00
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
def contains_cycle(heads):
|
2017-10-27 20:45:57 +03:00
|
|
|
# in an acyclic tree, the path from each word following the head relation
|
|
|
|
# upwards always ends at the root node
|
2016-02-24 13:26:25 +03:00
|
|
|
for tokenid in range(len(heads)):
|
|
|
|
seen = set([tokenid])
|
2017-10-27 20:45:57 +03:00
|
|
|
for ancestor in ancestors(tokenid, heads):
|
2016-02-22 16:40:40 +03:00
|
|
|
if ancestor in seen:
|
|
|
|
return seen
|
|
|
|
seen.add(ancestor)
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2016-02-24 13:26:25 +03:00
|
|
|
def is_nonproj_arc(tokenid, heads):
|
2022-02-21 17:02:21 +03:00
|
|
|
cdef vector[int] c_heads = _heads_to_c(heads)
|
|
|
|
return _is_nonproj_arc(tokenid, c_heads)
|
|
|
|
|
|
|
|
|
2022-06-08 20:34:11 +03:00
|
|
|
cdef bool _is_nonproj_arc(int tokenid, const vector[int]& heads) nogil except *:
|
2016-02-22 16:40:40 +03:00
|
|
|
# definition (e.g. Havelka 2007): an arc h -> d, h < d is non-projective
|
2016-02-24 13:26:25 +03:00
|
|
|
# if there is a token k, h < k < d such that h is not
|
2016-02-22 16:40:40 +03:00
|
|
|
# an ancestor of k. Same for h -> d, h > d
|
2016-02-24 13:26:25 +03:00
|
|
|
head = heads[tokenid]
|
2017-10-27 20:45:57 +03:00
|
|
|
if head == tokenid: # root arcs cannot be non-projective
|
2016-02-22 16:40:40 +03:00
|
|
|
return False
|
2022-02-21 17:02:21 +03:00
|
|
|
elif head < 0: # unattached tokens cannot be non-projective
|
2016-02-22 16:40:40 +03:00
|
|
|
return False
|
2022-06-08 20:34:11 +03:00
|
|
|
|
2020-06-26 20:34:12 +03:00
|
|
|
cdef int start, end
|
|
|
|
if head < tokenid:
|
|
|
|
start, end = (head+1, tokenid)
|
|
|
|
else:
|
|
|
|
start, end = (tokenid+1, head)
|
2017-10-27 20:45:57 +03:00
|
|
|
for k in range(start, end):
|
2022-06-08 20:34:11 +03:00
|
|
|
if not _has_head_as_ancestor(k, head, heads):
|
2016-02-22 16:40:40 +03:00
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2022-06-08 20:34:11 +03:00
|
|
|
cdef bool _has_head_as_ancestor(int tokenid, int head, const vector[int]& heads) nogil except *:
|
2022-02-21 17:02:21 +03:00
|
|
|
ancestor = tokenid
|
2022-06-08 20:34:11 +03:00
|
|
|
cdef unordered_set[int] seen_tokens
|
|
|
|
seen_tokens.insert(ancestor)
|
|
|
|
while True:
|
|
|
|
# Reached the head or a disconnected node
|
2022-02-21 17:02:21 +03:00
|
|
|
if heads[ancestor] == head or heads[ancestor] < 0:
|
|
|
|
return True
|
2022-06-08 20:34:11 +03:00
|
|
|
# Reached the root
|
|
|
|
if heads[ancestor] == ancestor:
|
|
|
|
return False
|
2022-02-21 17:02:21 +03:00
|
|
|
ancestor = heads[ancestor]
|
2022-06-08 20:34:11 +03:00
|
|
|
result = seen_tokens.insert(ancestor)
|
|
|
|
# Found cycle
|
|
|
|
if not result.second:
|
|
|
|
raise_domain_error(heads_to_string(heads))
|
2022-02-21 17:02:21 +03:00
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2022-06-08 20:34:11 +03:00
|
|
|
cdef string heads_to_string(const vector[int]& heads) nogil:
|
|
|
|
cdef vector[int].const_iterator citer
|
|
|
|
cdef string cycle_str
|
|
|
|
|
|
|
|
cycle_str.append("Found cycle in dependency graph: [")
|
|
|
|
|
|
|
|
# FIXME: Rewrite using ostringstream when available in Cython.
|
|
|
|
citer = heads.const_begin()
|
|
|
|
while citer != heads.const_end():
|
|
|
|
if citer != heads.const_begin():
|
|
|
|
cycle_str.append(", ")
|
|
|
|
cycle_str.append(to_string(deref(citer)))
|
|
|
|
incr(citer)
|
|
|
|
cycle_str.append("]")
|
|
|
|
|
|
|
|
return cycle_str
|
|
|
|
|
|
|
|
|
2016-02-24 13:26:25 +03:00
|
|
|
def is_nonproj_tree(heads):
|
2022-02-21 17:02:21 +03:00
|
|
|
cdef vector[int] c_heads = _heads_to_c(heads)
|
2016-02-22 16:40:40 +03:00
|
|
|
# a tree is non-projective if at least one arc is non-projective
|
2022-02-21 17:02:21 +03:00
|
|
|
return any(_is_nonproj_arc(word, c_heads) for word in range(len(heads)))
|
2016-02-24 13:26:25 +03:00
|
|
|
|
|
|
|
|
2017-05-22 12:48:02 +03:00
|
|
|
def decompose(label):
|
|
|
|
return label.partition(DELIMITER)[::2]
|
|
|
|
|
|
|
|
|
|
|
|
def is_decorated(label):
|
2018-03-27 20:23:02 +03:00
|
|
|
return DELIMITER in label
|
|
|
|
|
2023-07-19 13:03:31 +03:00
|
|
|
|
2019-11-11 19:35:27 +03:00
|
|
|
def count_decorated_labels(gold_data):
|
2018-03-27 20:23:02 +03:00
|
|
|
freqs = {}
|
2019-11-11 19:35:27 +03:00
|
|
|
for example in gold_data:
|
2020-06-26 20:34:12 +03:00
|
|
|
proj_heads, deco_deps = projectivize(example.get_aligned("HEAD"),
|
|
|
|
example.get_aligned("DEP"))
|
2019-11-25 18:03:28 +03:00
|
|
|
# set the label to ROOT for each root dependent
|
2023-07-19 13:03:31 +03:00
|
|
|
deco_deps = [
|
|
|
|
'ROOT' if head == i else deco_deps[i]
|
|
|
|
for i, head in enumerate(proj_heads)
|
|
|
|
]
|
2019-11-25 18:03:28 +03:00
|
|
|
# count label frequencies
|
|
|
|
for label in deco_deps:
|
|
|
|
if is_decorated(label):
|
|
|
|
freqs[label] = freqs.get(label, 0) + 1
|
2018-03-27 20:23:02 +03:00
|
|
|
return freqs
|
2017-05-22 12:48:02 +03:00
|
|
|
|
|
|
|
|
|
|
|
def projectivize(heads, labels):
|
2017-10-27 20:45:57 +03:00
|
|
|
# Use the algorithm by Nivre & Nilsson 2005. Assumes heads to be a proper
|
|
|
|
# tree, i.e. connected and cycle-free. Returns a new pair (heads, labels)
|
|
|
|
# which encode a projective and decorated tree.
|
2017-05-22 12:48:02 +03:00
|
|
|
proj_heads = copy(heads)
|
2022-02-21 17:02:21 +03:00
|
|
|
|
|
|
|
cdef int new_head
|
|
|
|
cdef vector[int] c_proj_heads = _heads_to_c(proj_heads)
|
|
|
|
cdef int smallest_np_arc = _get_smallest_nonproj_arc(c_proj_heads)
|
|
|
|
if smallest_np_arc == -1: # this sentence is already projective
|
2017-05-22 12:48:02 +03:00
|
|
|
return proj_heads, copy(labels)
|
2022-02-21 17:02:21 +03:00
|
|
|
while smallest_np_arc != -1:
|
|
|
|
new_head = _lift(smallest_np_arc, proj_heads)
|
|
|
|
c_proj_heads[smallest_np_arc] = new_head
|
|
|
|
smallest_np_arc = _get_smallest_nonproj_arc(c_proj_heads)
|
2017-05-22 12:48:02 +03:00
|
|
|
deco_labels = _decorate(heads, proj_heads, labels)
|
|
|
|
return proj_heads, deco_labels
|
|
|
|
|
|
|
|
|
2022-02-21 17:02:21 +03:00
|
|
|
cdef vector[int] _heads_to_c(heads):
|
2023-07-19 13:03:31 +03:00
|
|
|
cdef vector[int] c_heads
|
2022-02-21 17:02:21 +03:00
|
|
|
for head in heads:
|
2023-07-19 13:03:31 +03:00
|
|
|
if head is None:
|
2022-02-21 17:02:21 +03:00
|
|
|
c_heads.push_back(-1)
|
|
|
|
else:
|
|
|
|
assert head < len(heads)
|
|
|
|
c_heads.push_back(head)
|
|
|
|
return c_heads
|
|
|
|
|
|
|
|
|
2017-11-17 20:55:13 +03:00
|
|
|
cpdef deprojectivize(Doc doc):
|
2017-10-27 20:45:57 +03:00
|
|
|
# Reattach arcs with decorated labels (following HEAD scheme). For each
|
|
|
|
# decorated arc X||Y, search top-down, left-to-right, breadth-first until
|
|
|
|
# hitting a Y then make this the new head.
|
2017-11-17 20:55:13 +03:00
|
|
|
for i in range(doc.length):
|
|
|
|
label = doc.vocab.strings[doc.c[i].dep]
|
|
|
|
if DELIMITER in label:
|
|
|
|
new_label, head_label = label.split(DELIMITER)
|
|
|
|
new_head = _find_new_head(doc[i], head_label)
|
2018-03-27 20:23:02 +03:00
|
|
|
doc.c[i].head = new_head.i - i
|
2024-09-09 14:49:41 +03:00
|
|
|
doc.c[i].dep = doc.vocab.strings.add(new_label, allow_transient=False)
|
2020-09-16 21:32:38 +03:00
|
|
|
set_children_from_heads(doc.c, 0, doc.length)
|
2017-11-17 20:55:13 +03:00
|
|
|
return doc
|
2017-05-22 12:48:02 +03:00
|
|
|
|
2017-10-27 20:45:57 +03:00
|
|
|
|
2017-05-22 12:48:02 +03:00
|
|
|
def _decorate(heads, proj_heads, labels):
|
|
|
|
# uses decoration scheme HEAD from Nivre & Nilsson 2005
|
2018-04-03 16:50:31 +03:00
|
|
|
if (len(heads) != len(proj_heads)) or (len(proj_heads) != len(labels)):
|
|
|
|
raise ValueError(Errors.E082.format(n_heads=len(heads),
|
|
|
|
n_proj_heads=len(proj_heads),
|
|
|
|
n_labels=len(labels)))
|
2017-05-22 12:48:02 +03:00
|
|
|
deco_labels = []
|
2017-10-27 20:45:57 +03:00
|
|
|
for tokenid, head in enumerate(heads):
|
2017-05-22 12:48:02 +03:00
|
|
|
if head != proj_heads[tokenid]:
|
2019-12-25 19:59:52 +03:00
|
|
|
deco_labels.append(f"{labels[tokenid]}{DELIMITER}{labels[head]}")
|
2017-05-22 12:48:02 +03:00
|
|
|
else:
|
|
|
|
deco_labels.append(labels[tokenid])
|
|
|
|
return deco_labels
|
|
|
|
|
2023-07-19 13:03:31 +03:00
|
|
|
|
2022-02-21 17:02:21 +03:00
|
|
|
def get_smallest_nonproj_arc_slow(heads):
|
|
|
|
cdef vector[int] c_heads = _heads_to_c(heads)
|
|
|
|
return _get_smallest_nonproj_arc(c_heads)
|
|
|
|
|
2017-05-22 12:48:02 +03:00
|
|
|
|
2022-06-08 20:34:11 +03:00
|
|
|
cdef int _get_smallest_nonproj_arc(const vector[int]& heads) nogil except -2:
|
2017-05-22 12:48:02 +03:00
|
|
|
# return the smallest non-proj arc or None
|
|
|
|
# where size is defined as the distance between dep and head
|
|
|
|
# and ties are broken left to right
|
2022-02-21 17:02:21 +03:00
|
|
|
cdef int smallest_size = INT_MAX
|
2022-06-08 20:34:11 +03:00
|
|
|
# -1 means its already projective.
|
2022-02-21 17:02:21 +03:00
|
|
|
cdef int smallest_np_arc = -1
|
|
|
|
cdef int size
|
|
|
|
cdef int tokenid
|
|
|
|
cdef int head
|
|
|
|
|
|
|
|
for tokenid in range(heads.size()):
|
|
|
|
head = heads[tokenid]
|
2017-05-22 12:48:02 +03:00
|
|
|
size = abs(tokenid-head)
|
2022-02-21 17:02:21 +03:00
|
|
|
if size < smallest_size and _is_nonproj_arc(tokenid, heads):
|
2017-05-22 12:48:02 +03:00
|
|
|
smallest_size = size
|
|
|
|
smallest_np_arc = tokenid
|
|
|
|
return smallest_np_arc
|
|
|
|
|
|
|
|
|
2022-02-21 17:02:21 +03:00
|
|
|
cpdef int _lift(tokenid, heads):
|
2017-05-22 12:48:02 +03:00
|
|
|
# reattaches a word to it's grandfather
|
|
|
|
head = heads[tokenid]
|
|
|
|
ghead = heads[head]
|
2022-02-21 17:02:21 +03:00
|
|
|
cdef int new_head = ghead if head != ghead else tokenid
|
2017-05-22 12:48:02 +03:00
|
|
|
# attach to ghead if head isn't attached to root else attach to root
|
2022-02-21 17:02:21 +03:00
|
|
|
heads[tokenid] = new_head
|
|
|
|
return new_head
|
2017-05-22 12:48:02 +03:00
|
|
|
|
|
|
|
|
|
|
|
def _find_new_head(token, headlabel):
|
|
|
|
# search through the tree starting from the head of the given token
|
|
|
|
# returns the id of the first descendant with the given label
|
|
|
|
# if there is none, return the current head (no change)
|
|
|
|
queue = [token.head]
|
|
|
|
while queue:
|
|
|
|
next_queue = []
|
|
|
|
for qtoken in queue:
|
|
|
|
for child in qtoken.children:
|
2017-10-27 20:45:57 +03:00
|
|
|
if child.is_space:
|
|
|
|
continue
|
|
|
|
if child == token:
|
|
|
|
continue
|
2017-05-22 12:48:02 +03:00
|
|
|
if child.dep_ == headlabel:
|
|
|
|
return child
|
|
|
|
next_queue.append(child)
|
|
|
|
queue = next_queue
|
|
|
|
return token.head
|