# -*- coding:utf-8 -*- # Author: hankcs # Date: 2020-05-08 15:30 from abc import abstractmethod from collections import Counter from typing import Union, Tuple, Iterable, Any, Generator import numpy as np import tensorflow as tf from transformers import PreTrainedTokenizer, PretrainedConfig from hanlp_common.constant import ROOT from hanlp_common.structure import SerializableDict from hanlp.common.transform_tf import Transform from hanlp.common.vocab_tf import VocabTF from hanlp.components.parsers.alg_tf import tolist, kmeans, randperm, arange from hanlp.components.parsers.conll import read_conll from hanlp_common.conll import CoNLLWord, CoNLLUWord, CoNLLSentence from hanlp.layers.transformers.utils_tf import config_is, adjust_tokens_for_transformers, convert_examples_to_features from hanlp.utils.log_util import logger from hanlp.utils.string_util import ispunct from hanlp_common.util import merge_locals_kwargs class CoNLLTransform(Transform): def __init__(self, config: SerializableDict = None, map_x=True, map_y=True, lower=True, n_buckets=32, min_freq=2, use_pos=True, **kwargs) -> None: super().__init__(**merge_locals_kwargs(locals(), kwargs)) self.form_vocab: VocabTF = None if use_pos: self.cpos_vocab: VocabTF = None self.rel_vocab: VocabTF = None self.puncts: tf.Tensor = None @property def use_pos(self): return self.config.get('use_pos', True) def x_to_idx(self, x) -> Union[tf.Tensor, Tuple]: form, cpos = x return self.form_vocab.token_to_idx_table.lookup(form), self.cpos_vocab.token_to_idx_table.lookup(cpos) def y_to_idx(self, y): head, rel = y return head, self.rel_vocab.token_to_idx_table.lookup(rel) def X_to_inputs(self, X: Union[tf.Tensor, Tuple[tf.Tensor]]) -> Iterable: if len(X) == 2: form_batch, cposes_batch = X mask = tf.not_equal(form_batch, 0) elif len(X) == 3: form_batch, cposes_batch, mask = X else: raise ValueError(f'Expect X to be 2 or 3 elements but got {repr(X)}') sents = [] for form_sent, cposes_sent, length in zip(form_batch, cposes_batch, tf.math.count_nonzero(mask, axis=-1)): forms = tolist(form_sent)[1:length + 1] cposes = tolist(cposes_sent)[1:length + 1] sents.append([(self.form_vocab.idx_to_token[f], self.cpos_vocab.idx_to_token[c]) for f, c in zip(forms, cposes)]) return sents def lock_vocabs(self): super().lock_vocabs() self.puncts = tf.constant([i for s, i in self.form_vocab.token_to_idx.items() if ispunct(s)], dtype=tf.int64) def file_to_inputs(self, filepath: str, gold=True): assert gold, 'only support gold file for now' use_pos = self.use_pos conllu = filepath.endswith('.conllu') for sent in read_conll(filepath): for i, cell in enumerate(sent): form = cell[1] cpos = cell[3] head = cell[6] deprel = cell[7] # if conllu: # deps = cell[8] # deps = [x.split(':', 1) for x in deps.split('|')] # heads = [int(x[0]) for x in deps if '_' not in x[0] and '.' not in x[0]] # rels = [x[1] for x in deps if '_' not in x[0] and '.' not in x[0]] # if head in heads: # offset = heads.index(head) # if not self.rel_vocab or rels[offset] in self.rel_vocab: # deprel = rels[offset] sent[i] = [form, cpos, head, deprel] if use_pos else [form, head, deprel] yield sent @property def bos(self): if self.form_vocab.idx_to_token is None: return ROOT return self.form_vocab.idx_to_token[2] def input_is_single_sample(self, input: Any) -> bool: if self.use_pos: return isinstance(input[0][0], str) if len(input[0]) else False else: return isinstance(input[0], str) if len(input[0]) else False @abstractmethod def batched_inputs_to_batches(self, corpus, indices, shuffle): pass def len_of_sent(self, sent): return 1 + len(sent) # take ROOT into account def samples_to_dataset(self, samples: Generator, map_x=None, map_y=None, batch_size=5000, shuffle=None, repeat=None, drop_remainder=False, prefetch=1, cache=True) -> tf.data.Dataset: if shuffle: def generator(): # custom bucketing, load corpus into memory corpus = list(x for x in (samples() if callable(samples) else samples)) lengths = [self.len_of_sent(i) for i in corpus] if len(corpus) < 32: n_buckets = 1 else: n_buckets = min(self.config.n_buckets, len(corpus)) buckets = dict(zip(*kmeans(lengths, n_buckets))) sizes, buckets = zip(*[ (size, bucket) for size, bucket in buckets.items() ]) # the number of chunks in each bucket, which is clipped by # range [1, len(bucket)] chunks = [min(len(bucket), max(round(size * len(bucket) / batch_size), 1)) for size, bucket in zip(sizes, buckets)] range_fn = randperm if shuffle else arange max_samples_per_batch = self.config.get('max_samples_per_batch', None) for i in tolist(range_fn(len(buckets))): split_sizes = [(len(buckets[i]) - j - 1) // chunks[i] + 1 for j in range(chunks[i])] # how many sentences in each batch for batch_indices in tf.split(range_fn(len(buckets[i])), split_sizes): indices = [buckets[i][j] for j in tolist(batch_indices)] if max_samples_per_batch: for j in range(0, len(indices), max_samples_per_batch): yield from self.batched_inputs_to_batches(corpus, indices[j:j + max_samples_per_batch], shuffle) else: yield from self.batched_inputs_to_batches(corpus, indices, shuffle) else: def generator(): # custom bucketing, load corpus into memory corpus = list(x for x in (samples() if callable(samples) else samples)) n_tokens = 0 batch = [] for idx, sent in enumerate(corpus): sent_len = self.len_of_sent(sent) if n_tokens + sent_len > batch_size and batch: yield from self.batched_inputs_to_batches(corpus, batch, shuffle) n_tokens = 0 batch = [] n_tokens += sent_len batch.append(idx) if batch: yield from self.batched_inputs_to_batches(corpus, batch, shuffle) # next(generator()) return Transform.samples_to_dataset(self, generator, False, False, 0, False, repeat, drop_remainder, prefetch, cache) class CoNLL_DEP_Transform(CoNLLTransform): def __init__(self, config: SerializableDict = None, map_x=True, map_y=True, lower=True, n_buckets=32, min_freq=2, **kwargs) -> None: super().__init__(config, map_x, map_y, lower, n_buckets, min_freq, **kwargs) def batched_inputs_to_batches(self, corpus, indices, shuffle): """Convert batched inputs to batches of samples Args: corpus(list): A list of inputs indices(list): A list of indices, each list belongs to a batch shuffle: Returns: """ raw_batch = [[], [], [], []] for idx in indices: for b in raw_batch: b.append([]) for cells in corpus[idx]: for b, c, v in zip(raw_batch, cells, [self.form_vocab, self.cpos_vocab, None, self.rel_vocab]): b[-1].append(v.get_idx_without_add(c) if v else c) batch = [] for b, v in zip(raw_batch, [self.form_vocab, self.cpos_vocab, None, self.rel_vocab]): b = tf.keras.preprocessing.sequence.pad_sequences(b, padding='post', value=v.safe_pad_token_idx if v else 0, dtype='int64') batch.append(b) assert len(batch) == 4 yield (batch[0], batch[1]), (batch[2], batch[3]) def create_types_shapes_values(self) -> Tuple[Tuple, Tuple, Tuple]: types = (tf.int64, tf.int64), (tf.int64, tf.int64) shapes = ([None, None], [None, None]), ([None, None], [None, None]) values = (self.form_vocab.safe_pad_token_idx, self.cpos_vocab.safe_pad_token_idx), ( 0, self.rel_vocab.safe_pad_token_idx) return types, shapes, values def inputs_to_samples(self, inputs, gold=False): token_mapping: dict = self.config.get('token_mapping', None) use_pos = self.config.get('use_pos', True) for sent in inputs: sample = [] for i, cell in enumerate(sent): if isinstance(cell, tuple): cell = list(cell) elif isinstance(cell, str): cell = [cell] if token_mapping: cell[0] = token_mapping.get(cell[0], cell[0]) if self.config['lower']: cell[0] = cell[0].lower() if not gold: cell += [0, self.rel_vocab.safe_pad_token] sample.append(cell) # insert root word with arbitrary fields, anyway it will be masked # form, cpos, head, deprel = sample[0] sample.insert(0, [self.bos, self.bos, 0, self.bos] if use_pos else [self.bos, 0, self.bos]) yield sample def XY_to_inputs_outputs(self, X: Union[tf.Tensor, Tuple[tf.Tensor]], Y: Union[tf.Tensor, Tuple[tf.Tensor]], gold=False, inputs=None, conll=True, arc_scores=None, rel_scores=None) -> Iterable: (words, feats, mask), (arc_preds, rel_preds) = X, Y if inputs is None: inputs = self.X_to_inputs(X) ys = self.Y_to_outputs((arc_preds, rel_preds, mask), inputs=inputs) sents = [] for x, y in zip(inputs, ys): sent = CoNLLSentence() for idx, (cell, (head, deprel)) in enumerate(zip(x, y)): if self.use_pos and not self.config.get('joint_pos', None): form, cpos = cell else: form, cpos = cell, None if conll: sent.append( CoNLLWord(id=idx + 1, form=form, cpos=cpos, head=head, deprel=deprel) if conll == '.conll' else CoNLLUWord(id=idx + 1, form=form, upos=cpos, head=head, deprel=deprel)) else: sent.append([head, deprel]) sents.append(sent) return sents def fit(self, trn_path: str, **kwargs) -> int: use_pos = self.config.use_pos self.form_vocab = VocabTF() self.form_vocab.add(ROOT) # make root the 2ed elements while 0th is pad, 1st is unk if self.use_pos: self.cpos_vocab = VocabTF(pad_token=None, unk_token=None) self.rel_vocab = VocabTF(pad_token=None, unk_token=None) num_samples = 0 counter = Counter() for sent in self.file_to_samples(trn_path, gold=True): num_samples += 1 for idx, cell in enumerate(sent): if use_pos: form, cpos, head, deprel = cell else: form, head, deprel = cell if idx == 0: root = form else: counter[form] += 1 if use_pos: self.cpos_vocab.add(cpos) self.rel_vocab.add(deprel) for token in [token for token, freq in counter.items() if freq >= self.config.min_freq]: self.form_vocab.add(token) return num_samples @property def root_rel_idx(self): root_rel_idx = self.config.get('root_rel_idx', None) if root_rel_idx is None: for idx, rel in enumerate(self.rel_vocab.idx_to_token): if 'root' in rel.lower() and rel != self.bos: self.config['root_rel_idx'] = root_rel_idx = idx break return root_rel_idx def Y_to_outputs(self, Y: Union[tf.Tensor, Tuple[tf.Tensor]], gold=False, inputs=None, X=None) -> Iterable: arc_preds, rel_preds, mask = Y sents = [] for arc_sent, rel_sent, length in zip(arc_preds, rel_preds, tf.math.count_nonzero(mask, axis=-1)): arcs = tolist(arc_sent)[1:length + 1] rels = tolist(rel_sent)[1:length + 1] sents.append([(a, self.rel_vocab.idx_to_token[r]) for a, r in zip(arcs, rels)]) return sents class CoNLL_Transformer_Transform(CoNLL_DEP_Transform): def __init__(self, config: SerializableDict = None, map_x=True, map_y=True, lower=True, n_buckets=32, min_freq=0, max_seq_length=256, use_pos=False, mask_p=None, graph=False, topk=None, **kwargs) -> None: super().__init__(**merge_locals_kwargs(locals(), kwargs)) self.tokenizer: PreTrainedTokenizer = None self.transformer_config: PretrainedConfig = None if graph: self.orphan_relation = ROOT def lock_vocabs(self): super().lock_vocabs() if self.graph: CoNLL_SDP_Transform._find_orphan_relation(self) def fit(self, trn_path: str, **kwargs) -> int: if self.config.get('joint_pos', None): self.config.use_pos = True if self.graph: # noinspection PyCallByClass num = CoNLL_SDP_Transform.fit(self, trn_path, **kwargs) else: num = super().fit(trn_path, **kwargs) if self.config.get('topk', None): counter = Counter() for sent in self.file_to_samples(trn_path, gold=True): for idx, cell in enumerate(sent): form, head, deprel = cell counter[form] += 1 self.topk_vocab = VocabTF() for k, v in counter.most_common(self.config.topk): self.topk_vocab.add(k) return num def inputs_to_samples(self, inputs, gold=False): if self.graph: yield from CoNLL_SDP_Transform.inputs_to_samples(self, inputs, gold) else: yield from super().inputs_to_samples(inputs, gold) def file_to_inputs(self, filepath: str, gold=True): if self.graph: yield from CoNLL_SDP_Transform.file_to_inputs(self, filepath, gold) else: yield from super().file_to_inputs(filepath, gold) @property def mask_p(self) -> float: return self.config.get('mask_p', None) @property def graph(self): return self.config.get('graph', None) def create_types_shapes_values(self) -> Tuple[Tuple, Tuple, Tuple]: mask_p = self.mask_p types = (tf.int64, (tf.int64, tf.int64, tf.int64)), (tf.bool if self.graph else tf.int64, tf.int64, tf.int64) if mask_p else ( tf.bool if self.graph else tf.int64, tf.int64) if self.graph: shapes = ([None, None], ([None, None], [None, None], [None, None])), ( [None, None, None], [None, None, None], [None, None]) if mask_p else ( [None, None, None], [None, None, None]) else: shapes = ([None, None], ([None, None], [None, None], [None, None])), ( [None, None], [None, None], [None, None]) if mask_p else ([None, None], [None, None]) values = (self.form_vocab.safe_pad_token_idx, (0, 0, 0)), \ (0, self.rel_vocab.safe_pad_token_idx, 0) if mask_p else (0, self.rel_vocab.safe_pad_token_idx) types_shapes_values = types, shapes, values if self.use_pos: types_shapes_values = [((shapes[0][0], shapes[0][1] + (shapes[0][0],)), shapes[1]) for shapes in types_shapes_values] return types_shapes_values def X_to_inputs(self, X: Union[tf.Tensor, Tuple[tf.Tensor]]) -> Iterable: form_batch, feat, prefix_mask = X sents = [] for form_sent, length in zip(form_batch, tf.math.count_nonzero(prefix_mask, axis=-1)): forms = tolist(form_sent)[1:length + 1] sents.append([self.form_vocab.idx_to_token[f] for f in forms]) return sents def batched_inputs_to_batches(self, corpus, indices, shuffle): use_pos = self.use_pos if use_pos: raw_batch = [[], [], [], []] else: raw_batch = [[], [], []] if self.graph: max_len = len(max([corpus[i] for i in indices], key=len)) for idx in indices: arc = np.zeros((max_len, max_len), dtype=np.bool) rel = np.zeros((max_len, max_len), dtype=np.int64) for b in raw_batch[:2 if use_pos else 1]: b.append([]) for m, cells in enumerate(corpus[idx]): if use_pos: for b, c, v in zip(raw_batch, cells, [None, self.cpos_vocab]): b[-1].append(v.get_idx_without_add(c) if v else c) else: for b, c, v in zip(raw_batch, cells, [None]): b[-1].append(c) for n, r in zip(cells[-2], cells[-1]): arc[m, n] = True rid = self.rel_vocab.get_idx_without_add(r) if rid is None: logger.warning(f'Relation OOV: {r} not exists in train') continue rel[m, n] = rid raw_batch[-2].append(arc) raw_batch[-1].append(rel) else: for idx in indices: for s in raw_batch: s.append([]) for cells in corpus[idx]: if use_pos: for s, c, v in zip(raw_batch, cells, [None, self.cpos_vocab, None, self.rel_vocab]): s[-1].append(v.get_idx_without_add(c) if v else c) else: for s, c, v in zip(raw_batch, cells, [None, None, self.rel_vocab]): s[-1].append(v.get_idx_without_add(c) if v else c) # Transformer tokenizing config = self.transformer_config tokenizer = self.tokenizer xlnet = config_is(config, 'xlnet') roberta = config_is(config, 'roberta') pad_token = tokenizer.pad_token pad_token_id = tokenizer.convert_tokens_to_ids([pad_token])[0] cls_token = tokenizer.cls_token sep_token = tokenizer.sep_token max_seq_length = self.config.max_seq_length batch_forms = [] batch_input_ids = [] batch_input_mask = [] batch_prefix_offset = [] mask_p = self.mask_p if mask_p: batch_masked_offsets = [] mask_token_id = tokenizer.mask_token_id for sent_idx, sent in enumerate(raw_batch[0]): batch_forms.append([self.form_vocab.get_idx_without_add(token) for token in sent]) sent = adjust_tokens_for_transformers(sent) sent = sent[1:] # remove use [CLS] instead pad_label_idx = self.form_vocab.pad_idx input_ids, input_mask, segment_ids, prefix_mask = \ convert_examples_to_features(sent, max_seq_length, tokenizer, cls_token_at_end=xlnet, # xlnet has a cls token at the end cls_token=cls_token, cls_token_segment_id=2 if xlnet else 0, sep_token=sep_token, sep_token_extra=roberta, # roberta uses an extra separator b/w pairs of sentences, cf. github.com/pytorch/fairseq/commit/1684e166e3da03f5b600dbb7855cb98ddfcd0805 pad_on_left=xlnet, # pad on the left for xlnet pad_token_id=pad_token_id, pad_token_segment_id=4 if xlnet else 0, pad_token_label_id=pad_label_idx, do_padding=False) num_masks = sum(prefix_mask) # assert len(sent) == num_masks # each token has a True subtoken if num_masks < len(sent): # long sent gets truncated, +1 for root batch_forms[-1] = batch_forms[-1][:num_masks + 1] # form raw_batch[-1][sent_idx] = raw_batch[-1][sent_idx][:num_masks + 1] # head raw_batch[-2][sent_idx] = raw_batch[-2][sent_idx][:num_masks + 1] # rel raw_batch[-3][sent_idx] = raw_batch[-3][sent_idx][:num_masks + 1] # pos prefix_mask[0] = True # is now [CLS] prefix_offset = [idx for idx, m in enumerate(prefix_mask) if m] batch_input_ids.append(input_ids) batch_input_mask.append(input_mask) batch_prefix_offset.append(prefix_offset) if mask_p: if shuffle: size = int(np.ceil(mask_p * len(prefix_offset[1:]))) # never mask [CLS] mask_offsets = np.random.choice(np.arange(1, len(prefix_offset)), size, replace=False) for offset in sorted(mask_offsets): assert 0 < offset < len(input_ids) # mask_word = raw_batch[0][sent_idx][offset] # mask_prefix = tokenizer.convert_ids_to_tokens([input_ids[prefix_offset[offset]]])[0] # assert mask_word.startswith(mask_prefix) or mask_prefix.startswith( # mask_word) or mask_prefix == "'", \ # f'word {mask_word} prefix {mask_prefix} not match' # could vs couldn # mask_offsets.append(input_ids[offset]) # subword token # mask_offsets.append(offset) # form token input_ids[prefix_offset[offset]] = mask_token_id # mask prefix # whole word masking, mask the rest of the word for i in range(prefix_offset[offset] + 1, len(input_ids) - 1): if prefix_mask[i]: break input_ids[i] = mask_token_id batch_masked_offsets.append(sorted(mask_offsets)) else: batch_masked_offsets.append([0]) # No masking in prediction batch_forms = tf.keras.preprocessing.sequence.pad_sequences(batch_forms, padding='post', value=self.form_vocab.safe_pad_token_idx, dtype='int64') batch_input_ids = tf.keras.preprocessing.sequence.pad_sequences(batch_input_ids, padding='post', value=pad_token_id, dtype='int64') batch_input_mask = tf.keras.preprocessing.sequence.pad_sequences(batch_input_mask, padding='post', value=0, dtype='int64') batch_prefix_offset = tf.keras.preprocessing.sequence.pad_sequences(batch_prefix_offset, padding='post', value=0, dtype='int64') batch_heads = tf.keras.preprocessing.sequence.pad_sequences(raw_batch[-2], padding='post', value=0, dtype='int64') batch_rels = tf.keras.preprocessing.sequence.pad_sequences(raw_batch[-1], padding='post', value=self.rel_vocab.safe_pad_token_idx, dtype='int64') if mask_p: batch_masked_offsets = tf.keras.preprocessing.sequence.pad_sequences(batch_masked_offsets, padding='post', value=pad_token_id, dtype='int64') feats = (tf.constant(batch_input_ids, dtype='int64'), tf.constant(batch_input_mask, dtype='int64'), tf.constant(batch_prefix_offset)) if use_pos: batch_pos = tf.keras.preprocessing.sequence.pad_sequences(raw_batch[1], padding='post', value=self.cpos_vocab.safe_pad_token_idx, dtype='int64') feats += (batch_pos,) yield (batch_forms, feats), \ (batch_heads, batch_rels, batch_masked_offsets) if mask_p else (batch_heads, batch_rels) def len_of_sent(self, sent): # Transformer tokenizing config = self.transformer_config tokenizer = self.tokenizer xlnet = config_is(config, 'xlnet') roberta = config_is(config, 'roberta') pad_token = tokenizer.pad_token pad_token_id = tokenizer.convert_tokens_to_ids([pad_token])[0] cls_token = tokenizer.cls_token sep_token = tokenizer.sep_token max_seq_length = self.config.max_seq_length sent = sent[1:] # remove use [CLS] instead pad_label_idx = self.form_vocab.pad_idx sent = [x[0] for x in sent] sent = adjust_tokens_for_transformers(sent) input_ids, input_mask, segment_ids, prefix_mask = \ convert_examples_to_features(sent, max_seq_length, tokenizer, cls_token_at_end=xlnet, # xlnet has a cls token at the end cls_token=cls_token, cls_token_segment_id=2 if xlnet else 0, sep_token=sep_token, sep_token_extra=roberta, # roberta uses an extra separator b/w pairs of sentences, cf. github.com/pytorch/fairseq/commit/1684e166e3da03f5b600dbb7855cb98ddfcd0805 pad_on_left=xlnet, # pad on the left for xlnet pad_token_id=pad_token_id, pad_token_segment_id=4 if xlnet else 0, pad_token_label_id=pad_label_idx, do_padding=False) return len(input_ids) def samples_to_dataset(self, samples: Generator, map_x=None, map_y=None, batch_size=5000, shuffle=None, repeat=None, drop_remainder=False, prefetch=1, cache=True) -> tf.data.Dataset: if shuffle: return CoNLL_DEP_Transform.samples_to_dataset(self, samples, map_x, map_y, batch_size, shuffle, repeat, drop_remainder, prefetch, cache) def generator(): # custom bucketing, load corpus into memory corpus = list(x for x in (samples() if callable(samples) else samples)) n_tokens = 0 batch = [] for idx, sent in enumerate(corpus): sent_len = self.len_of_sent(sent) if n_tokens + sent_len > batch_size and batch: yield from self.batched_inputs_to_batches(corpus, batch, shuffle) n_tokens = 0 batch = [] n_tokens += sent_len batch.append(idx) if batch: yield from self.batched_inputs_to_batches(corpus, batch, shuffle) # debug for transformer # next(generator()) return Transform.samples_to_dataset(self, generator, False, False, 0, False, repeat, drop_remainder, prefetch, cache) def Y_to_outputs(self, Y: Union[tf.Tensor, Tuple[tf.Tensor]], gold=False, inputs=None, X=None) -> Iterable: if self.graph: ys = CoNLL_SDP_Transform.Y_to_outputs(self, Y, gold, inputs, X) ys = [[([t[0] for t in l], [t[1] for t in l]) for l in y] for y in ys] return ys return super().Y_to_outputs(Y, gold, inputs, X) class CoNLL_SDP_Transform(CoNLLTransform): def __init__(self, config: SerializableDict = None, map_x=True, map_y=True, lower=True, n_buckets=32, min_freq=2, use_pos=True, **kwargs) -> None: super().__init__(config, map_x, map_y, lower, n_buckets, min_freq, use_pos, **kwargs) self.orphan_relation = ROOT def lock_vocabs(self): super().lock_vocabs() # heuristic to find the orphan relation self._find_orphan_relation() def _find_orphan_relation(self): for rel in self.rel_vocab.idx_to_token: if 'root' in rel.lower(): self.orphan_relation = rel break def file_to_inputs(self, filepath: str, gold=True): assert gold, 'only support gold file for now' use_pos = self.use_pos conllu = filepath.endswith('.conllu') enhanced_only = self.config.get('enhanced_only', None) for i, sent in enumerate(read_conll(filepath)): parsed_sent = [] if conllu: for cell in sent: ID = cell[0] form = cell[1] cpos = cell[3] head = cell[6] deprel = cell[7] deps = cell[8] deps = [x.split(':', 1) for x in deps.split('|')] heads = [int(x[0]) for x in deps if x[0].isdigit()] rels = [x[1] for x in deps if x[0].isdigit()] if enhanced_only: if head in heads: offset = heads.index(head) heads.pop(offset) rels.pop(offset) else: if head not in heads: heads.append(head) rels.append(deprel) parsed_sent.append([form, cpos, heads, rels] if use_pos else [form, heads, rels]) else: prev_cells = None heads = [] rels = [] for j, cell in enumerate(sent): ID = cell[0] form = cell[1] cpos = cell[3] head = cell[6] deprel = cell[7] if prev_cells and ID != prev_cells[0]: # found end of token parsed_sent.append( [prev_cells[1], prev_cells[2], heads, rels] if use_pos else [prev_cells[1], heads, rels]) heads = [] rels = [] heads.append(head) rels.append(deprel) prev_cells = [ID, form, cpos, head, deprel] if use_pos else [ID, form, head, deprel] parsed_sent.append( [prev_cells[1], prev_cells[2], heads, rels] if use_pos else [prev_cells[1], heads, rels]) yield parsed_sent def fit(self, trn_path: str, **kwargs) -> int: self.form_vocab = VocabTF() self.form_vocab.add(ROOT) # make root the 2ed elements while 0th is pad, 1st is unk if self.use_pos: self.cpos_vocab = VocabTF(pad_token=None, unk_token=None) self.rel_vocab = VocabTF(pad_token=None, unk_token=None) num_samples = 0 counter = Counter() for sent in self.file_to_samples(trn_path, gold=True): num_samples += 1 for idx, cell in enumerate(sent): if len(cell) == 4: form, cpos, head, deprel = cell elif len(cell) == 3: if self.use_pos: form, cpos = cell[0] else: form = cell[0] head, deprel = cell[1:] else: raise ValueError('Unknown data arrangement') if idx == 0: root = form else: counter[form] += 1 if self.use_pos: self.cpos_vocab.add(cpos) self.rel_vocab.update(deprel) for token in [token for token, freq in counter.items() if freq >= self.config.min_freq]: self.form_vocab.add(token) return num_samples def inputs_to_samples(self, inputs, gold=False): use_pos = self.use_pos for sent in inputs: sample = [] for i, cell in enumerate(sent): if isinstance(cell, tuple): cell = list(cell) elif isinstance(cell, str): cell = [cell] if self.config['lower']: cell[0] = cell[0].lower() if not gold: cell += [[0], [self.rel_vocab.safe_pad_token]] sample.append(cell) # insert root word with arbitrary fields, anyway it will be masked if use_pos: form, cpos, head, deprel = sample[0] sample.insert(0, [self.bos, self.bos, [0], deprel]) else: form, head, deprel = sample[0] sample.insert(0, [self.bos, [0], deprel]) yield sample def batched_inputs_to_batches(self, corpus, indices, shuffle): use_pos = self.use_pos raw_batch = [[], [], [], []] if use_pos else [[], [], []] max_len = len(max([corpus[i] for i in indices], key=len)) for idx in indices: arc = np.zeros((max_len, max_len), dtype=bool) rel = np.zeros((max_len, max_len), dtype=np.int64) for b in raw_batch[:2]: b.append([]) for m, cells in enumerate(corpus[idx]): if use_pos: for b, c, v in zip(raw_batch, cells, [self.form_vocab, self.cpos_vocab]): b[-1].append(v.get_idx_without_add(c)) else: for b, c, v in zip(raw_batch, cells, [self.form_vocab]): b[-1].append(v.get_idx_without_add(c)) for n, r in zip(cells[-2], cells[-1]): arc[m, n] = True rid = self.rel_vocab.get_idx_without_add(r) if rid is None: logger.warning(f'Relation OOV: {r} not exists in train') continue rel[m, n] = rid raw_batch[-2].append(arc) raw_batch[-1].append(rel) batch = [] for b, v in zip(raw_batch, [self.form_vocab, self.cpos_vocab]): b = tf.keras.preprocessing.sequence.pad_sequences(b, padding='post', value=v.safe_pad_token_idx, dtype='int64') batch.append(b) batch += raw_batch[2:] assert len(batch) == 4 yield (batch[0], batch[1]), (batch[2], batch[3]) def create_types_shapes_values(self) -> Tuple[Tuple, Tuple, Tuple]: types = (tf.int64, tf.int64), (tf.bool, tf.int64) shapes = ([None, None], [None, None]), ([None, None, None], [None, None, None]) values = (self.form_vocab.safe_pad_token_idx, self.cpos_vocab.safe_pad_token_idx), ( False, self.rel_vocab.safe_pad_token_idx) return types, shapes, values def Y_to_outputs(self, Y: Union[tf.Tensor, Tuple[tf.Tensor]], gold=False, inputs=None, X=None) -> Iterable: arc_preds, rel_preds, mask = Y sents = [] for arc_sent, rel_sent, length in zip(arc_preds, rel_preds, tf.math.count_nonzero(mask, axis=-1)): sent = [] for arc, rel in zip(tolist(arc_sent[1:, 1:]), tolist(rel_sent[1:, 1:])): ar = [] for idx, (a, r) in enumerate(zip(arc, rel)): if a: ar.append((idx + 1, self.rel_vocab.idx_to_token[r])) if not ar: # orphan ar.append((0, self.orphan_relation)) sent.append(ar) sents.append(sent) return sents def XY_to_inputs_outputs(self, X: Union[tf.Tensor, Tuple[tf.Tensor]], Y: Union[tf.Tensor, Tuple[tf.Tensor]], gold=False, inputs=None, conll=True) -> Iterable: (words, feats, mask), (arc_preds, rel_preds) = X, Y xs = inputs ys = self.Y_to_outputs((arc_preds, rel_preds, mask)) sents = [] for x, y in zip(xs, ys): sent = CoNLLSentence() for idx, ((form, cpos), pred) in enumerate(zip(x, y)): head = [p[0] for p in pred] deprel = [p[1] for p in pred] if conll: sent.append(CoNLLWord(id=idx + 1, form=form, cpos=cpos, head=head, deprel=deprel)) else: sent.append([head, deprel]) sents.append(sent) return sents