paddlepaddle--paddlenlp
2549 行
90 KiB
Python
2549 行
90 KiB
Python
# coding:utf-8
|
|
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import contextlib
|
|
import copy
|
|
import csv
|
|
import json
|
|
import math
|
|
import os
|
|
import pickle
|
|
import re
|
|
import traceback
|
|
import warnings
|
|
from collections import OrderedDict, namedtuple
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from functools import cmp_to_key
|
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
|
|
import numpy as np
|
|
import paddle
|
|
import paddle.nn.functional as F
|
|
import six
|
|
from paddle.dataset.common import md5file
|
|
from PIL import Image
|
|
|
|
from ..transformers.tokenizer_utils_base import PaddingStrategy, PretrainedTokenizerBase
|
|
from ..utils.downloader import DownloaderCheck, get_path_from_url
|
|
from ..utils.image_utils import (
|
|
Bbox,
|
|
DecodeImage,
|
|
NormalizeImage,
|
|
PadBatch,
|
|
Permute,
|
|
ResizeImage,
|
|
check,
|
|
img2base64,
|
|
two_dimension_sort_layout,
|
|
)
|
|
from ..utils.log import logger
|
|
|
|
DOC_FORMAT = r"""
|
|
Examples:
|
|
.. code-block:: python
|
|
"""
|
|
DOWNLOAD_CHECK = False
|
|
|
|
|
|
def download_file(save_dir, filename, url, md5=None):
|
|
"""
|
|
Download the file from the url to specified directory.
|
|
Check md5 value when the file is exists, if the md5 value is the same as the existed file, just use
|
|
the older file, if not, will download the file from the url.
|
|
|
|
Args:
|
|
save_dir(string): The specified directory saving the file.
|
|
filename(string): The specified filename saving the file.
|
|
url(string): The url downling the file.
|
|
md5(string, optional): The md5 value that checking the version downloaded.
|
|
"""
|
|
fullname = os.path.join(save_dir, filename)
|
|
if os.path.exists(fullname):
|
|
if md5 and (not md5file(fullname) == md5):
|
|
logger.info("Updating {} from {}".format(filename, url))
|
|
logger.disable()
|
|
get_path_from_url(url, save_dir, md5)
|
|
else:
|
|
logger.info("Downloading {} from {}".format(filename, url))
|
|
logger.disable()
|
|
get_path_from_url(url, save_dir, md5)
|
|
logger.enable()
|
|
return fullname
|
|
|
|
|
|
def download_check(task):
|
|
"""
|
|
Check the resource status in the specified task.
|
|
|
|
Args:
|
|
task(string): The name of specified task.
|
|
"""
|
|
logger.disable()
|
|
global DOWNLOAD_CHECK
|
|
if not DOWNLOAD_CHECK:
|
|
DOWNLOAD_CHECK = True
|
|
checker = DownloaderCheck(task)
|
|
checker.start()
|
|
checker.join()
|
|
logger.enable()
|
|
|
|
|
|
def add_docstrings(*docstr):
|
|
"""
|
|
The function that add the doc string to doc of class.
|
|
"""
|
|
|
|
def docstring_decorator(fn):
|
|
fn.__doc__ = fn.__doc__ + "".join(DOC_FORMAT) + "".join(docstr)
|
|
return fn
|
|
|
|
return docstring_decorator
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def static_mode_guard():
|
|
paddle.enable_static()
|
|
yield
|
|
paddle.disable_static()
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def dygraph_mode_guard():
|
|
paddle.disable_static()
|
|
yield
|
|
|
|
|
|
def cut_chinese_sent(para):
|
|
"""
|
|
Cut the Chinese sentences more precisely, reference to "https://blog.csdn.net/blmoistawinde/article/details/82379256".
|
|
"""
|
|
para = re.sub(r"([。!?\?])([^”’])", r"\1\n\2", para)
|
|
para = re.sub(r"(\.{6})([^”’])", r"\1\n\2", para)
|
|
para = re.sub(r"(\…{2})([^”’])", r"\1\n\2", para)
|
|
para = re.sub(r"([。!?\?][”’])([^,。!?\?])", r"\1\n\2", para)
|
|
para = para.rstrip()
|
|
return para.split("\n")
|
|
|
|
|
|
class TermTreeNode(object):
|
|
"""Definition of term node. All members are protected, to keep rigorism of data struct.
|
|
|
|
Args:
|
|
sid (str): term id of node.
|
|
term (str): term, common name of this term.
|
|
base (str): `cb` indicates concept base, `eb` indicates entity base.
|
|
term_type (Optional[str], optional): type of this term, constructs hierarchical of `term` node. Defaults to None.
|
|
hyper (Optional[str], optional): parent type of a `type` node. Defaults to None.
|
|
node_type (str, optional): type statement of node, `type` or `term`. Defaults to "term".
|
|
alias (Optional[List[str]], optional): alias of this term. Defaults to None.
|
|
alias_ext (Optional[List[str]], optional): extended alias of this term, CANNOT be used in matching.
|
|
Defaults to None.
|
|
sub_type (Optional[List[str]], optional): grouped by some term. Defaults to None.
|
|
sub_term (Optional[List[str]], optional): some lower term. Defaults to None.
|
|
data (Optional[Dict[str, Any]], optional): to sore full information of a term. Defaults to None.
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
sid: str,
|
|
term: str,
|
|
base: str,
|
|
node_type: str = "term",
|
|
term_type: Optional[str] = None,
|
|
hyper: Optional[str] = None,
|
|
level: Optional[int] = None,
|
|
alias: Optional[List[str]] = None,
|
|
alias_ext: Optional[List[str]] = None,
|
|
sub_type: Optional[List[str]] = None,
|
|
sub_term: Optional[List[str]] = None,
|
|
data: Optional[Dict[str, Any]] = None,
|
|
):
|
|
self._sid = sid
|
|
self._term = term
|
|
self._base = base
|
|
self._term_type = term_type
|
|
self._hyper = hyper
|
|
self._sub_term = sub_term if sub_term is not None else []
|
|
self._sub_type = sub_type if sub_type is not None else []
|
|
self._alias = alias if alias is not None else []
|
|
self._alias_ext = alias_ext if alias_ext is not None else []
|
|
self._data = data
|
|
self._level = level
|
|
self._node_type = node_type
|
|
self._sons = set()
|
|
|
|
def __str__(self):
|
|
if self._data is not None:
|
|
return json.dumps(self._data, ensure_ascii=False)
|
|
else:
|
|
res = {
|
|
"termid": self._sid,
|
|
"term": self._term,
|
|
"src": self._base,
|
|
"alias": self._alias,
|
|
"alias_ext": self._alias_ext,
|
|
"termtype": self._term_type,
|
|
"subterms": self._sub_term,
|
|
"subtype": self._sub_type,
|
|
"links": [],
|
|
}
|
|
return json.dumps(res, ensure_ascii=False)
|
|
|
|
@property
|
|
def sid(self):
|
|
return self._sid
|
|
|
|
@property
|
|
def term(self):
|
|
return self._term
|
|
|
|
@property
|
|
def base(self):
|
|
return self._base
|
|
|
|
@property
|
|
def alias(self):
|
|
return self._alias
|
|
|
|
@property
|
|
def alias_ext(self):
|
|
return self._alias_ext
|
|
|
|
@property
|
|
def termtype(self):
|
|
return self._term_type
|
|
|
|
@property
|
|
def subtype(self):
|
|
return self._sub_type
|
|
|
|
@property
|
|
def subterm(self):
|
|
return self._sub_term
|
|
|
|
@property
|
|
def hyper(self):
|
|
return self._hyper
|
|
|
|
@property
|
|
def level(self):
|
|
return self._level
|
|
|
|
@property
|
|
def sons(self):
|
|
return self._sons
|
|
|
|
@property
|
|
def node_type(self):
|
|
return self._node_type
|
|
|
|
def add_son(self, son_name):
|
|
self._sons.add(son_name)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]):
|
|
"""Build a node from dictionary data.
|
|
|
|
Args:
|
|
data (Dict[str, Any]): Dictionary data contain all k-v data.
|
|
|
|
Returns:
|
|
[type]: TermTree node object.
|
|
"""
|
|
return cls(
|
|
sid=data["termid"],
|
|
term=data["term"],
|
|
base=data["src"],
|
|
term_type=data["termtype"],
|
|
sub_type=data["subtype"],
|
|
sub_term=data["subterms"],
|
|
alias=data["alias"],
|
|
alias_ext=data["alias_ext"],
|
|
data=data,
|
|
)
|
|
|
|
@classmethod
|
|
def from_json(cls, json_str: str):
|
|
"""Build a node from JSON string.
|
|
|
|
Args:
|
|
json_str (str): JSON string formatted by TermTree data.
|
|
|
|
Returns:
|
|
[type]: TermTree node object.
|
|
"""
|
|
dict_data = json.loads(json_str)
|
|
return cls.from_dict(dict_data)
|
|
|
|
|
|
class TermTree(object):
|
|
"""TermTree class."""
|
|
|
|
def __init__(self):
|
|
self._nodes: Dict[str, TermTreeNode] = {}
|
|
self._root = TermTreeNode(sid="root", term="root", base="cb", node_type="root", level=0)
|
|
self._nodes["root"] = self.root
|
|
self._index = {}
|
|
|
|
def __build_sons(self):
|
|
for node in self._nodes:
|
|
self.__build_son(self._nodes[node])
|
|
|
|
def __getitem__(self, item):
|
|
return self._nodes[item]
|
|
|
|
def __contains__(self, item):
|
|
return item in self._nodes
|
|
|
|
def __iter__(self):
|
|
return self._nodes.__iter__()
|
|
|
|
@property
|
|
def root(self):
|
|
return self._root
|
|
|
|
def __load_type(self, file_path: str):
|
|
with open(file_path, "rt", newline="", encoding="utf8") as csvfile:
|
|
file_handler = csv.DictReader(csvfile, delimiter="\t")
|
|
for row in file_handler:
|
|
if row["type-1"] not in self:
|
|
self.add_type(type_name=row["type-1"], hyper_type="root")
|
|
if row["type-2"] != "" and row["type-2"] not in self:
|
|
self.add_type(type_name=row["type-2"], hyper_type=row["type-1"])
|
|
if row["type-3"] != "" and row["type-3"] not in self:
|
|
self.add_type(type_name=row["type-3"], hyper_type=row["type-2"])
|
|
|
|
def __judge_term_node(self, node: TermTreeNode) -> bool:
|
|
if node.termtype not in self:
|
|
raise ValueError(f"Term type of new node {node.termtype} does not exists.")
|
|
if node.sid in self:
|
|
warnings.warn(f"{node.sid} exists, will be replaced by new node.")
|
|
|
|
def add_term(
|
|
self,
|
|
term: Optional[str] = None,
|
|
base: Optional[str] = None,
|
|
term_type: Optional[str] = None,
|
|
sub_type: Optional[List[str]] = None,
|
|
sub_term: Optional[List[str]] = None,
|
|
alias: Optional[List[str]] = None,
|
|
alias_ext: Optional[List[str]] = None,
|
|
data: Optional[Dict[str, Any]] = None,
|
|
):
|
|
"""Add a term into TermTree.
|
|
|
|
Args:
|
|
term (str): common name of name.
|
|
base (str): term is concept or entity.
|
|
term_type (str): term type of this term
|
|
sub_type (Optional[List[str]], optional): sub type of this term, must exists in TermTree. Defaults to None.
|
|
sub_terms (Optional[List[str]], optional): sub terms of this term. Defaults to None.
|
|
alias (Optional[List[str]], optional): alias of this term. Defaults to None.
|
|
alias_ext (Optional[List[str]], optional): . Defaults to None.
|
|
data (Optional[Dict[str, Any]], optional): [description]. Defaults to None.
|
|
"""
|
|
if data is not None:
|
|
new_node = TermTreeNode.from_dict(data)
|
|
else:
|
|
new_node = TermTreeNode(
|
|
sid=f"{term_type}_{base}_{term}",
|
|
term=term,
|
|
base=base,
|
|
term_type=term_type,
|
|
sub_term=sub_term,
|
|
sub_type=sub_type,
|
|
alias=alias,
|
|
alias_ext=alias_ext,
|
|
node_type="term",
|
|
)
|
|
self.__judge_term_node(new_node)
|
|
self._nodes[new_node.sid] = new_node
|
|
self.__build_index(new_node)
|
|
|
|
def add_type(self, type_name, hyper_type):
|
|
if type_name in self._nodes:
|
|
raise ValueError(f"Term Type {type_name} exists.")
|
|
if hyper_type not in self._nodes:
|
|
raise ValueError(f"Hyper type {hyper_type} does not exist, please add it first.")
|
|
if self._nodes[hyper_type].level == 3:
|
|
raise ValueError(
|
|
"Term type schema must be 3-LEVEL, 3rd level type node should not be a parent of type node."
|
|
)
|
|
self._nodes[type_name] = TermTreeNode(
|
|
sid=type_name,
|
|
term=type_name,
|
|
base=None,
|
|
hyper=hyper_type,
|
|
node_type="type",
|
|
level=self._nodes[hyper_type].level + 1,
|
|
)
|
|
self.__build_index(self._nodes[type_name])
|
|
|
|
def __load_file(self, file_path: str):
|
|
with open(file_path, encoding="utf-8") as fp:
|
|
for line in fp:
|
|
data = json.loads(line)
|
|
self.add_term(data=data)
|
|
|
|
def __build_son(self, node: TermTreeNode):
|
|
"""Build sons of a node
|
|
|
|
Args:
|
|
node (TermTreeNode): son node.
|
|
"""
|
|
type_node = None
|
|
if node.termtype is not None:
|
|
type_node = self._nodes[node.termtype]
|
|
elif node.hyper is not None:
|
|
type_node = self._nodes[node.hyper]
|
|
if type_node is not None:
|
|
type_node.add_son(node.sid)
|
|
for sub_type in node.subtype:
|
|
sub_type_node = self._nodes[sub_type]
|
|
sub_type_node.add_son(node.sid)
|
|
|
|
def build_son(self, node: str):
|
|
self.__build_son(self[node])
|
|
|
|
def __build_index(self, node: TermTreeNode):
|
|
if node.term not in self._index:
|
|
self._index[node.term] = []
|
|
self._index[node.term].append(node.sid)
|
|
for alia in node.alias:
|
|
if alia not in self._index:
|
|
self._index[alia] = []
|
|
self._index[alia].append(node.sid)
|
|
|
|
def __judge_hyper(self, source_id, target_id) -> bool:
|
|
queue = [source_id]
|
|
visited_node = {source_id}
|
|
while len(queue) > 0:
|
|
cur_id = queue.pop(0)
|
|
if cur_id == target_id:
|
|
return True
|
|
cur_node = self._nodes[cur_id]
|
|
edge = []
|
|
if cur_node.hyper is not None:
|
|
edge.append(cur_node.hyper)
|
|
if cur_node.termtype is not None:
|
|
edge.append(cur_node.termtype)
|
|
edge.extend(cur_node.subtype)
|
|
for next_id in edge:
|
|
if next_id not in visited_node:
|
|
queue.append(next_id)
|
|
visited_node.add(next_id)
|
|
return False
|
|
|
|
def find_term(self, term: str, term_type: Optional[str] = None) -> Tuple[bool, Union[List[str], None]]:
|
|
"""Find a term in Term Tree. If term not exists, return None.
|
|
If `term_type` is not None, will find term with this type.
|
|
|
|
Args:
|
|
term (str): term to look up.
|
|
term_type (Optional[str], optional): find term in this term_type. Defaults to None.
|
|
|
|
Returns:
|
|
Union[None, List[str]]: [description]
|
|
"""
|
|
if term not in self._index:
|
|
return False, None
|
|
else:
|
|
if term_type is None:
|
|
return True, self._index[term]
|
|
else:
|
|
out = []
|
|
for term_id in self._index[term]:
|
|
if self.__judge_hyper(term_id, term_type) is True:
|
|
out.append(term_id)
|
|
if len(out) > 0:
|
|
return True, out
|
|
else:
|
|
return False, None
|
|
|
|
def build_from_dir(self, term_schema_path, term_data_path, linking=True):
|
|
"""Build TermTree from a directory which should contain type schema and term data.
|
|
|
|
Args:
|
|
dir ([type]): [description]
|
|
"""
|
|
self.__load_type(term_schema_path)
|
|
if linking:
|
|
self.__load_file(term_data_path)
|
|
self.__build_sons()
|
|
|
|
@classmethod
|
|
def from_dir(cls, term_schema_path, term_data_path, linking) -> "TermTree":
|
|
"""Build TermTree from a directory which should contain type schema and term data.
|
|
|
|
Args:
|
|
source_dir ([type]): [description]
|
|
|
|
Returns:
|
|
TermTree: [description]
|
|
"""
|
|
term_tree = cls()
|
|
term_tree.build_from_dir(term_schema_path, term_data_path, linking)
|
|
return term_tree
|
|
|
|
def __dfs(self, cur_id: str, depth: int, path: Dict[str, str], writer: csv.DictWriter):
|
|
cur_node = self._nodes[cur_id]
|
|
if cur_node.node_type == "term":
|
|
return
|
|
if depth > 0:
|
|
path[f"type-{depth}"] = cur_id
|
|
if path["type-1"] != "":
|
|
writer.writerow(path)
|
|
for son in cur_node.sons:
|
|
self.__dfs(son, depth + 1, path, writer)
|
|
if depth > 0:
|
|
path[f"type-{depth}"] = ""
|
|
|
|
def save(self, save_dir):
|
|
"""Save term tree to directory `save_dir`
|
|
|
|
Args:
|
|
save_dir ([type]): Directory.
|
|
"""
|
|
if os.path.exists(save_dir) is False:
|
|
os.makedirs(save_dir, exist_ok=True)
|
|
out_path = {}
|
|
for i in range(1, 3):
|
|
out_path[f"type-{i}"] = ""
|
|
with open(f"{save_dir}/termtree_type.csv", "wt", encoding="utf-8", newline="") as fp:
|
|
fieldnames = ["type-1", "type-2", "type-3"]
|
|
csv_writer = csv.DictWriter(fp, delimiter="\t", fieldnames=fieldnames)
|
|
csv_writer.writeheader()
|
|
self.__dfs("root", 0, out_path, csv_writer)
|
|
with open(f"{save_dir}/termtree_data", "w", encoding="utf-8", newline="") as fp:
|
|
for nid in self:
|
|
node = self[nid]
|
|
if node.node_type == "term":
|
|
print(node, file=fp)
|
|
|
|
|
|
def levenstein_distance(s1: str, s2: str) -> int:
|
|
"""Calculate minimal Levenstein distance between s1 and s2.
|
|
|
|
Args:
|
|
s1 (str): string
|
|
s2 (str): string
|
|
|
|
Returns:
|
|
int: the minimal distance.
|
|
"""
|
|
m, n = len(s1) + 1, len(s2) + 1
|
|
|
|
# Initialize
|
|
dp = [[0] * n for i in range(m)]
|
|
dp[0][0] = 0
|
|
for i in range(1, m):
|
|
dp[i][0] = dp[i - 1][0] + 1
|
|
for j in range(1, n):
|
|
dp[0][j] = dp[0][j - 1] + 1
|
|
|
|
for i in range(1, m):
|
|
for j in range(1, n):
|
|
if s1[i - 1] != s2[j - 1]:
|
|
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1
|
|
else:
|
|
dp[i][j] = dp[i - 1][j - 1]
|
|
return dp[m - 1][n - 1]
|
|
|
|
|
|
class BurkhardKellerNode(object):
|
|
"""Node implementation for BK-Tree. A BK-Tree node stores the information of current word, and its approximate words calculated by levenstein distance.
|
|
|
|
Args:
|
|
word (str): word of current node.
|
|
"""
|
|
|
|
def __init__(self, word: str):
|
|
self.word = word
|
|
self.next = {}
|
|
|
|
|
|
class BurkhardKellerTree(object):
|
|
"""Implementation of BK-Tree"""
|
|
|
|
def __init__(self):
|
|
self.root = None
|
|
self.nodes = {}
|
|
|
|
def __add(self, cur_node: BurkhardKellerNode, word: str):
|
|
"""Insert a word into current tree. If tree is empty, set this word to root.
|
|
|
|
Args:
|
|
word (str): word to be inserted.
|
|
"""
|
|
if self.root is None:
|
|
self.root = BurkhardKellerNode(word)
|
|
return
|
|
if word in self.nodes:
|
|
return
|
|
dist = levenstein_distance(word, cur_node.word)
|
|
if dist not in cur_node.next:
|
|
self.nodes[word] = cur_node.next[dist] = BurkhardKellerNode(word)
|
|
else:
|
|
self.__add(cur_node.next[dist], word)
|
|
|
|
def add(self, word: str):
|
|
"""Insert a word into current tree. If tree is empty, set this word to root.
|
|
|
|
Args:
|
|
word (str): word to be inserted.
|
|
"""
|
|
return self.__add(self.root, word)
|
|
|
|
def __search_similar_word(self, cur_node: BurkhardKellerNode, s: str, threshold: int = 2) -> List[str]:
|
|
res = []
|
|
if cur_node is None:
|
|
return res
|
|
dist = levenstein_distance(cur_node.word, s)
|
|
if dist <= threshold:
|
|
res.append((cur_node.word, dist))
|
|
start = max(dist - threshold, 1)
|
|
while start < dist + threshold:
|
|
tmp_res = self.__search_similar_word(cur_node.next.get(start, None), s)[:]
|
|
res.extend(tmp_res)
|
|
start += 1
|
|
return res
|
|
|
|
def search_similar_word(self, word: str) -> List[str]:
|
|
"""Search the most similar (minimal levenstain distance) word between `s`.
|
|
|
|
Args:
|
|
s (str): target word
|
|
|
|
Returns:
|
|
List[str]: similar words.
|
|
"""
|
|
res = self.__search_similar_word(self.root, word)
|
|
|
|
def max_prefix(s1: str, s2: str) -> int:
|
|
res = 0
|
|
length = min(len(s1), len(s2))
|
|
for i in range(length):
|
|
if s1[i] == s2[i]:
|
|
res += 1
|
|
else:
|
|
break
|
|
return res
|
|
|
|
res.sort(key=lambda d: (d[1], -max_prefix(d[0], word)))
|
|
return res
|
|
|
|
|
|
class TriedTree(object):
|
|
"""Implementation of TriedTree"""
|
|
|
|
def __init__(self):
|
|
self.tree = {}
|
|
|
|
def add_word(self, word):
|
|
"""add single word into TriedTree"""
|
|
self.tree[word] = len(word)
|
|
for i in range(1, len(word)):
|
|
wfrag = word[:i]
|
|
self.tree[wfrag] = self.tree.get(wfrag, None)
|
|
|
|
def search(self, content):
|
|
"""Backward maximum matching
|
|
|
|
Args:
|
|
content (str): string to be searched
|
|
Returns:
|
|
List[Tuple]: list of maximum matching words, each element represents
|
|
the starting and ending position of the matching string.
|
|
"""
|
|
result = []
|
|
length = len(content)
|
|
for start in range(length):
|
|
for end in range(start + 1, length + 1):
|
|
pos = self.tree.get(content[start:end], -1)
|
|
if pos == -1:
|
|
break
|
|
if pos and (len(result) == 0 or end > result[-1][1]):
|
|
result.append((start, end))
|
|
return result
|
|
|
|
|
|
class Customization(object):
|
|
"""
|
|
User intervention based on Aho-Corasick automaton
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.dictitem = {}
|
|
self.ac = None
|
|
|
|
def load_customization(self, filename, sep=None):
|
|
"""Load the custom vocab"""
|
|
self.ac = TriedTree()
|
|
with open(filename, "r", encoding="utf8") as f:
|
|
for line in f:
|
|
if sep is None:
|
|
words = line.strip().split()
|
|
|
|
if len(words) == 0:
|
|
continue
|
|
|
|
phrase = ""
|
|
tags = []
|
|
offset = []
|
|
for word in words:
|
|
if word.rfind("/") < 1:
|
|
phrase += word
|
|
tags.append("")
|
|
else:
|
|
phrase += word[: word.rfind("/")]
|
|
tags.append(word[word.rfind("/") + 1 :])
|
|
offset.append(len(phrase))
|
|
|
|
if len(phrase) < 2 and tags[0] == "":
|
|
continue
|
|
|
|
self.dictitem[phrase] = (tags, offset)
|
|
self.ac.add_word(phrase)
|
|
|
|
def parse_customization(self, query, lac_tags, prefix=False):
|
|
"""Use custom vocab to modify the lac results"""
|
|
if not self.ac:
|
|
logger.warning("customization dict is not load")
|
|
return
|
|
ac_res = self.ac.search(query)
|
|
|
|
for begin, end in ac_res:
|
|
phrase = query[begin:end]
|
|
index = begin
|
|
|
|
tags, offsets = self.dictitem[phrase]
|
|
|
|
if prefix:
|
|
for tag, offset in zip(tags, offsets):
|
|
while index < begin + offset:
|
|
if len(tag) == 0:
|
|
lac_tags[index] = "I" + lac_tags[index][1:]
|
|
else:
|
|
lac_tags[index] = "I-" + tag
|
|
index += 1
|
|
lac_tags[begin] = "B" + lac_tags[begin][1:]
|
|
for offset in offsets:
|
|
index = begin + offset
|
|
if index < len(lac_tags):
|
|
lac_tags[index] = "B" + lac_tags[index][1:]
|
|
else:
|
|
for tag, offset in zip(tags, offsets):
|
|
while index < begin + offset:
|
|
if len(tag) == 0:
|
|
lac_tags[index] = lac_tags[index][:-1] + "I"
|
|
else:
|
|
lac_tags[index] = tag + "-I"
|
|
index += 1
|
|
lac_tags[begin] = lac_tags[begin][:-1] + "B"
|
|
for offset in offsets:
|
|
index = begin + offset
|
|
if index < len(lac_tags):
|
|
lac_tags[index] = lac_tags[index][:-1] + "B"
|
|
|
|
|
|
class SchemaTree(object):
|
|
"""
|
|
Implementation of SchemaTree
|
|
"""
|
|
|
|
def __init__(self, name="root", children=None):
|
|
self.name = name
|
|
self.children = []
|
|
self.prefix = None
|
|
self.parent_relations = None
|
|
self.parent = None
|
|
if children is not None:
|
|
for child in children:
|
|
self.add_child(child)
|
|
|
|
def __repr__(self):
|
|
return self.name
|
|
|
|
def add_child(self, node):
|
|
assert isinstance(node, SchemaTree), "The children of a node should be an instance of SchemaTree."
|
|
self.children.append(node)
|
|
|
|
|
|
def get_id_and_prob(span_set, offset_mapping):
|
|
"""
|
|
Return text id and probability of predicted spans
|
|
|
|
Args:
|
|
span_set (set): set of predicted spans.
|
|
offset_mapping (list[int]): list of pair preserving the
|
|
index of start and end char in original text pair (prompt + text) for each token.
|
|
Returns:
|
|
sentence_id (list[tuple]): index of start and end char in original text.
|
|
prob (list[float]): probabilities of predicted spans.
|
|
"""
|
|
prompt_end_token_id = offset_mapping[1:].index([0, 0])
|
|
bias = offset_mapping[prompt_end_token_id][1] + 1
|
|
for idx in range(1, prompt_end_token_id + 1):
|
|
offset_mapping[idx][0] -= bias
|
|
offset_mapping[idx][1] -= bias
|
|
|
|
sentence_id = []
|
|
prob = []
|
|
for start, end in span_set:
|
|
prob.append(start[1] * end[1])
|
|
start_id = offset_mapping[start[0]][0]
|
|
end_id = offset_mapping[end[0]][1]
|
|
sentence_id.append((start_id, end_id))
|
|
return sentence_id, prob
|
|
|
|
|
|
def dbc2sbc(s):
|
|
rs = ""
|
|
for char in s:
|
|
code = ord(char)
|
|
if code == 0x3000:
|
|
code = 0x0020
|
|
else:
|
|
code -= 0xFEE0
|
|
if not (0x0021 <= code and code <= 0x7E):
|
|
rs += char
|
|
continue
|
|
rs += chr(code)
|
|
return rs
|
|
|
|
|
|
class WordTagRelationExtractor(object):
|
|
"""Implement of information extractor."""
|
|
|
|
_chain_items = {"和", "与", "兼", "及", "以及", "还有", "并"}
|
|
_all_items = None
|
|
_jux_buf = []
|
|
|
|
def __init__(self, schema):
|
|
self._schema = schema
|
|
|
|
@property
|
|
def schema(self):
|
|
return self._schema
|
|
|
|
@classmethod
|
|
def from_dict(cls, config_dict):
|
|
"""Make an instance from a configuration dictionary.
|
|
|
|
Args:
|
|
config_dict (Dict[str, Any]): configuration dict.
|
|
"""
|
|
res = {}
|
|
|
|
for i, trip_config in enumerate(config_dict):
|
|
head_role_type = trip_config["head_role"]
|
|
if head_role_type not in res:
|
|
res[head_role_type] = {"trigger": {}, "g_t_map": {}, "rel_group": {}, "trig_word": {}}
|
|
group_name = trip_config["group"]
|
|
if "rel_group" in trip_config:
|
|
res[head_role_type]["rel_group"][group_name] = trip_config["rel_group"]
|
|
if group_name not in res[head_role_type]["trig_word"]:
|
|
res[head_role_type]["trig_word"][group_name] = set()
|
|
for trig_word in trip_config["trig_word"]:
|
|
res[head_role_type]["trigger"][trig_word] = {
|
|
"trigger_type": trip_config["trig_type"],
|
|
"group_name": group_name,
|
|
"rev_flag": trip_config["reverse"],
|
|
}
|
|
res[head_role_type]["trig_word"][group_name].add(trig_word)
|
|
res[head_role_type]["g_t_map"][group_name] = trip_config["tail_role"]
|
|
|
|
return cls(res)
|
|
|
|
@classmethod
|
|
def from_json(cls, json_str):
|
|
"""Implement an instance from JSON str."""
|
|
config_dict = json.loads(json_str)
|
|
return cls.from_dict(config_dict)
|
|
|
|
@classmethod
|
|
def from_pkl(cls, pkl_path):
|
|
"""Implement an instance from a serialized pickle package."""
|
|
with open(pkl_path, "rb") as fp:
|
|
schema = pickle.load(fp)
|
|
return cls(schema)
|
|
|
|
@classmethod
|
|
def from_config(cls, config_path):
|
|
"""Implement an instance from a configuration file."""
|
|
with open(config_path, encoding="utf-8") as fp:
|
|
config_json = json.load(fp)
|
|
return cls.from_dict(config_json)
|
|
|
|
def add_schema_from_dict(self, config_dict):
|
|
"""Add the schema from the dict."""
|
|
for i, trip_config in enumerate(config_dict):
|
|
head_role_type = trip_config["head_role"]
|
|
if head_role_type not in self._schema:
|
|
self._schema[head_role_type] = {"trigger": {}, "g_t_map": {}, "rel_group": {}, "trig_word": {}}
|
|
group_name = trip_config["group"]
|
|
if "rel_group" in self._schema:
|
|
self._schema[head_role_type]["rel_group"][group_name] = trip_config["rel_group"]
|
|
if group_name not in self._schema[head_role_type]["trig_word"]:
|
|
self._schema[head_role_type]["trig_word"][group_name] = set()
|
|
for trig_word in trip_config["trig_word"]:
|
|
self._schema[head_role_type]["trigger"][trig_word] = {
|
|
"trigger_type": trip_config["trig_type"],
|
|
"group_name": group_name,
|
|
"rev_flag": trip_config["reverse"],
|
|
}
|
|
self._schema[head_role_type]["trig_word"][group_name].add(trig_word)
|
|
self._schema[head_role_type]["g_t_map"][group_name] = trip_config["tail_role"]
|
|
|
|
def _judge_jux(self, wordtag_item):
|
|
"""Judge whether `wordtag_item` is a relevance component between two juxtaposed items.
|
|
|
|
Args:
|
|
wordtag_item (dict): input item.
|
|
|
|
Returns:
|
|
bool: [description]
|
|
"""
|
|
if wordtag_item["item"] in {"、", " ", "《", "》", "/"}:
|
|
return True
|
|
if wordtag_item["item"] in self._chain_items and wordtag_item["wordtag_label"] == "连词":
|
|
return True
|
|
return False
|
|
|
|
def _search_jux(self, cur_item, cur_pos=0, jux_type=None, jux_word=None, status_flag=None, search_list=None):
|
|
"""Find juxtaposed items with `cur_item` at `cur_pos` in `self._all_items`.
|
|
|
|
Args:
|
|
cur_item (Dict[str, Any]): the item current viewing.
|
|
cur_pos (int, optional): current position of viewing item. Defaults to 0.
|
|
jux_type (Set[str], optional): wordtag labels that can be considered as juxtaposed item. Defaults to None.
|
|
jux_word (Set[str], optional): words that can be considered as juxtaposed item. Defaults to None.
|
|
status_flag (bool, optional): if True, on the juxtaposed item, or on chain item. Defaults to None.
|
|
|
|
Returns:
|
|
int: end position of juxtable items.
|
|
"""
|
|
if search_list is None:
|
|
search_list = self._all_items
|
|
|
|
if jux_type is None and jux_word is None:
|
|
raise ValueError("`jux_type` and `jux_word` are both None.")
|
|
|
|
if status_flag is True:
|
|
self._jux_buf.append(cur_item)
|
|
|
|
if cur_pos >= len(search_list) - 1:
|
|
return cur_pos
|
|
|
|
next_item = search_list[cur_pos + 1]
|
|
|
|
if self._judge_jux(next_item) is True:
|
|
return self._search_jux(
|
|
cur_item=next_item,
|
|
cur_pos=cur_pos + 1,
|
|
jux_type=jux_type,
|
|
jux_word=jux_word,
|
|
status_flag=False,
|
|
search_list=search_list,
|
|
)
|
|
|
|
next_flag = True
|
|
if jux_type is not None:
|
|
next_flag = next_flag and self._match_item(next_item, jux_type)
|
|
if jux_word is not None:
|
|
next_flag = next_flag and (next_item["item"] in jux_word)
|
|
if next_flag is True:
|
|
return self._search_jux(
|
|
cur_item=next_item, cur_pos=cur_pos + 1, jux_type=jux_type, jux_word=jux_word, status_flag=True
|
|
)
|
|
if next_flag is not True:
|
|
while self._judge_jux(search_list[cur_pos]) is True:
|
|
cur_pos -= 1
|
|
return cur_pos
|
|
|
|
@staticmethod
|
|
def _match_item(item, type_can):
|
|
match_key = item["wordtag_label"].split("_")[0]
|
|
return match_key in type_can or item["wordtag_label"] in type_can
|
|
|
|
def _trig_handler(self, cur_item, head_conf):
|
|
"""Whether current item is a trigger, if True, return corresponding flag and configuration.
|
|
|
|
Args:
|
|
cur_item (Dict[str, Any]): current viewing ite,
|
|
st_conf (Dict[str, Any]): config
|
|
|
|
Returns:
|
|
Tuple[str, Union[None, dict]]: [description]
|
|
"""
|
|
trigger_conf = head_conf["trigger"]
|
|
if cur_item["item"] in trigger_conf:
|
|
# find a trigger, then judge whether it is a tail-trigger or a rel trigger.
|
|
if trigger_conf[cur_item["item"]]["trigger_type"] == "role":
|
|
# find a tail-trigger, then judge wordtag label.
|
|
group_name = trigger_conf[cur_item["item"]]["group_name"]
|
|
for tail_conf in head_conf["g_t_map"][group_name]:
|
|
if self._match_item(cur_item, tail_conf["main"]) is True:
|
|
return "trig_t", tail_conf
|
|
else:
|
|
return "un_trig", None
|
|
else:
|
|
return "trig_g", None
|
|
else:
|
|
return "un_trig", None
|
|
|
|
def _find_tail(self, search_range, sg_conf, head_hype):
|
|
"""Find tail role in `search_range`
|
|
|
|
Args:
|
|
search_range (List[int]): index range of `self._all_items`, items to be checked.
|
|
sg_conf (Dict[str, Any]): configuration of group.
|
|
head_type (str): wordtag label of head role item.
|
|
"""
|
|
for i in search_range:
|
|
item = self._all_items[i]
|
|
if item["item"] in {",", "?", "、", "。", ";"}:
|
|
return -2, None
|
|
for j, tail_conf in enumerate(sg_conf):
|
|
flag = self._match_item(item, tail_conf["main"])
|
|
if flag is True:
|
|
return i, tail_conf
|
|
if item["wordtag_label"].startswith(head_hype):
|
|
return -1, None
|
|
|
|
return -2, None
|
|
|
|
def _find_supp(self, search_range, search_type):
|
|
res = []
|
|
for i in search_range:
|
|
item = self._all_items[i]
|
|
if item["item"] == ",":
|
|
break
|
|
if any(item["wordtag_label"].startswith(sup_t) for sup_t in search_type):
|
|
res.append(item)
|
|
return res if len(res) > 0 else None
|
|
|
|
def _make_output(self, head_item, tail_item, group, source, support=None, trig_word=None, **kwargs):
|
|
"""Make formatted outputs of mined results.
|
|
|
|
Args:
|
|
head_item (Dict[str, Any]): [description]
|
|
head_index (int): [description]
|
|
tail_item (List[Dict[str, Any]]): [description]
|
|
tail_indices (List[int]): [description]
|
|
group (str): [description]
|
|
source (str): [description]
|
|
support (List[Dict[str, Any]], optional): [description]. Defaults to None.
|
|
support_indices (List[int], optional): [description]. Defaults to None.
|
|
trig_word (List[str], optional): [description]. Defaults to None.
|
|
trig_indices (List[int], optional): [description]. Defaults to None.
|
|
"""
|
|
res = {
|
|
"HEAD_ROLE": {
|
|
"item": head_item["item"],
|
|
"type": head_item["wordtag_label"],
|
|
"offset": head_item["offset"],
|
|
},
|
|
"TAIL_ROLE": [
|
|
{"item": ti["item"], "offset": ti["offset"], "type": ti["wordtag_label"]} for ti in tail_item
|
|
],
|
|
"GROUP": group,
|
|
"SRC": source,
|
|
}
|
|
if support is not None:
|
|
res["SUPPORT"] = [
|
|
{
|
|
"item": si["item"],
|
|
"offset": si["offset"],
|
|
"type": si["wordtag_label"],
|
|
}
|
|
for si in support
|
|
]
|
|
if trig_word is not None:
|
|
res["TRIG"] = [
|
|
{
|
|
"item": ti["item"],
|
|
"offset": ti["offset"],
|
|
}
|
|
for ti in trig_word
|
|
]
|
|
return res
|
|
|
|
def _reverse(self, res, group_name=None):
|
|
ret = []
|
|
for rev_head in res["TAIL_ROLE"]:
|
|
rev_tmp = {
|
|
"HEAD_ROLE": rev_head,
|
|
"TAIL_ROLE": [res["HEAD_ROLE"]],
|
|
"GROUP": group_name if group_name is not None else res["GROUP"],
|
|
}
|
|
if "SUPPORT" in res:
|
|
rev_tmp["SUPPORT"] = res["SUPPORT"]
|
|
if "TRIG" in res:
|
|
rev_tmp["TRIG"] = res["TRIG"]
|
|
rev_tmp["SRC"] = "REVERSE" if group_name is not None else res["SRC"]
|
|
ret.append(rev_tmp)
|
|
return ret
|
|
|
|
def extract_spo(self, all_items):
|
|
"""Pipeline of mining procedure.
|
|
|
|
Args:
|
|
all_items ([type]): [description]
|
|
"""
|
|
self._all_items = all_items
|
|
|
|
res_cand = []
|
|
|
|
# Match head role, and consider it as central, search others.
|
|
for i, head_cand in enumerate(self._all_items):
|
|
last_end = i
|
|
try:
|
|
datetime.strptime(head_cand["item"], "%Y年%m月%d日")
|
|
head_cand["wordtag_label"] = "时间类_具体时间"
|
|
except ValueError:
|
|
pass
|
|
|
|
if head_cand["wordtag_label"] in self._schema:
|
|
head_conf = self._schema[head_cand["wordtag_label"]]
|
|
head_type = head_cand["wordtag_label"]
|
|
else:
|
|
match_key = head_cand["wordtag_label"].split("_")[0]
|
|
if match_key in self._schema:
|
|
head_conf = self._schema[match_key]
|
|
head_type = match_key
|
|
else:
|
|
continue
|
|
|
|
trig_status = "un_trig"
|
|
|
|
# Consider `head_cand` as a start item, find trigger words behind.
|
|
# We suppose that minning strategy is directed, so only search items behinds head.
|
|
# If need, we can reverse constructed triples.
|
|
j = i + 1
|
|
while j < len(self._all_items):
|
|
cur_item = all_items[j]
|
|
cur_pos = j
|
|
j += 1
|
|
|
|
trig_status, trig_conf = self._trig_handler(cur_item, self._schema[head_type])
|
|
|
|
# Find a tail role, generate corresponding triple.
|
|
if trig_status == "trig_t":
|
|
trig_status = "un_trig"
|
|
tail_flag = True
|
|
for k in range(i + 1, j):
|
|
if self._all_items[k]["wordtag_label"] == head_cand["wordtag_label"]:
|
|
tail_flag = False
|
|
break
|
|
if tail_flag is False:
|
|
continue
|
|
|
|
group_name = head_conf["trigger"][cur_item["item"]]["group_name"]
|
|
del self._jux_buf[:]
|
|
idx = self._search_jux(
|
|
cur_item=cur_item, cur_pos=cur_pos, jux_type=trig_conf["main"], status_flag=True
|
|
)
|
|
supports = self._find_supp(search_range=range(j - 1, i, -1), search_type=trig_conf["support"])
|
|
|
|
tmp = self._make_output(
|
|
head_item=head_cand,
|
|
tail_item=self._jux_buf[:],
|
|
group=group_name,
|
|
support=supports,
|
|
source="TAIL",
|
|
)
|
|
|
|
# Reverse triple if group has relative.
|
|
if (
|
|
group_name in head_conf.get("rel_group", {})
|
|
or head_conf["trigger"][cur_item["item"]]["rev_flag"] is True
|
|
):
|
|
rev_tmp = self._reverse(tmp, head_conf.get("rel_group", {}).get(group_name, None))
|
|
res_cand.extend(rev_tmp[:])
|
|
if head_conf["trigger"][cur_item["item"]]["rev_flag"] is False:
|
|
res_cand.append(tmp.copy())
|
|
|
|
j = idx + 1
|
|
last_end = idx
|
|
continue
|
|
|
|
# Find a group trigger word, look for tail role items of current head role and group argument.
|
|
# Searching range is items behind group trigger and items between head rold and group trigger word.
|
|
if trig_status == "trig_g":
|
|
trig_status = "un_trig"
|
|
group_name = head_conf["trigger"][cur_item["item"]]["group_name"]
|
|
|
|
del self._jux_buf[:]
|
|
g_start_idx = j - 1
|
|
g_idx = self._search_jux(
|
|
cur_item=cur_item,
|
|
cur_pos=cur_pos,
|
|
jux_word=head_conf["trig_word"][group_name],
|
|
status_flag=True,
|
|
)
|
|
|
|
g_trig_words = self._jux_buf[:]
|
|
j = g_idx + 1
|
|
|
|
# Search right.
|
|
if j < len(self._all_items) - 1:
|
|
tail_idx, tail_conf = self._find_tail(
|
|
range(g_idx + 1, len(self._all_items)), head_conf["g_t_map"][group_name], head_type
|
|
)
|
|
|
|
if tail_idx > 0:
|
|
# Find a tail.
|
|
tail_item = self._all_items[tail_idx]
|
|
del self._jux_buf[:]
|
|
idx = self._search_jux(
|
|
cur_item=tail_item, cur_pos=tail_idx, status_flag=True, jux_type=tail_conf["main"]
|
|
)
|
|
tail_cand = self._jux_buf[:]
|
|
supports = self._find_supp(range(tail_idx - 1, i, -1), tail_conf["support"])
|
|
|
|
tmp = self._make_output(
|
|
head_item=head_cand,
|
|
tail_item=tail_cand,
|
|
group=group_name,
|
|
source="HGT",
|
|
support=supports,
|
|
trig_word=g_trig_words,
|
|
)
|
|
|
|
if (
|
|
group_name in head_conf.get("rel_group", {})
|
|
or head_conf["trigger"][cur_item["item"]]["rev_flag"] is True
|
|
):
|
|
rev_tmp = self._reverse(tmp, head_conf.get("rel_group", {}).get(group_name, None))
|
|
res_cand.extend(rev_tmp[:])
|
|
if head_conf["trigger"][cur_item["item"]]["rev_flag"] is False:
|
|
res_cand.append(tmp.copy())
|
|
|
|
j = idx + 1
|
|
last_end = idx
|
|
continue
|
|
|
|
# Search left
|
|
if g_idx - i > len(g_trig_words):
|
|
tail_idx, tail_conf = self._find_tail(
|
|
range(g_start_idx, last_end, -1), head_conf["g_t_map"][group_name], head_type
|
|
)
|
|
tail_item = self._all_items[tail_idx]
|
|
if tail_idx > 0:
|
|
del self._jux_buf[:]
|
|
_ = self._search_jux(
|
|
cur_item=tail_item,
|
|
cur_pos=0,
|
|
jux_type=tail_conf["main"],
|
|
status_flag=True,
|
|
search_list=self._all_items[i + 1 : tail_idx][::-1],
|
|
)
|
|
tail_cand = self._jux_buf[:]
|
|
supports = self._find_supp(range(g_idx - 1, last_end, -1), tail_conf["support"])
|
|
last_end = g_idx
|
|
|
|
tmp = self._make_output(
|
|
head_item=head_cand,
|
|
tail_item=tail_cand,
|
|
group=group_name,
|
|
trig_word=g_trig_words,
|
|
source="HTG",
|
|
support=supports,
|
|
)
|
|
|
|
if (
|
|
group_name in head_conf.get("rel_group", {})
|
|
or head_conf["trigger"][cur_item["item"]]["rev_flag"] is True
|
|
):
|
|
rev_tmp = self._reverse(tmp, head_conf.get("rel_group", {}).get(group_name, None))
|
|
res_cand.extend(rev_tmp[:])
|
|
if head_conf["trigger"][cur_item["item"]]["rev_flag"] is False:
|
|
res_cand.append(tmp.copy())
|
|
continue
|
|
return res_cand
|
|
|
|
|
|
@dataclass
|
|
class DataCollatorGP:
|
|
tokenizer: PretrainedTokenizerBase
|
|
padding: Union[bool, str, PaddingStrategy] = True
|
|
max_length: Optional[int] = None
|
|
label_maps: Optional[dict] = None
|
|
task_type: Optional[str] = None
|
|
|
|
def __call__(self, features: List[Dict[str, Union[List[int], paddle.Tensor]]]) -> Dict[str, paddle.Tensor]:
|
|
new_features = [{k: v for k, v in f.items() if k not in ["offset_mapping", "text"]} for f in features]
|
|
|
|
batch = self.tokenizer.pad(
|
|
new_features,
|
|
padding=self.padding,
|
|
)
|
|
|
|
batch = [paddle.to_tensor(batch[k]) for k in batch.keys()]
|
|
batch.append([feature["offset_mapping"] for feature in features])
|
|
batch.append([feature["text"] for feature in features])
|
|
return batch
|
|
|
|
|
|
@dataclass
|
|
class DataCollatorForErnieCtm:
|
|
tokenizer: PretrainedTokenizerBase
|
|
padding: Union[bool, str, PaddingStrategy] = True
|
|
model: Optional[str] = "wordtag"
|
|
|
|
def __call__(self, features: List[Dict[str, Union[List[int], paddle.Tensor]]]) -> Dict[str, paddle.Tensor]:
|
|
no_pad = "seq_len" if self.model == "wordtag" else "label_indices"
|
|
new_features = [{k: v for k, v in f.items() if k != no_pad} for f in features]
|
|
batch = self.tokenizer.pad(
|
|
new_features,
|
|
padding=self.padding,
|
|
)
|
|
|
|
batch = [paddle.to_tensor(batch[k]) for k in batch.keys()]
|
|
batch.append(paddle.to_tensor([f[no_pad] for f in features]))
|
|
return batch
|
|
|
|
|
|
def gp_decode(batch_outputs, offset_mappings, texts, label_maps, task_type="relation_extraction"):
|
|
if task_type == "entity_extraction":
|
|
batch_ent_results = []
|
|
for entity_output, offset_mapping, text in zip(batch_outputs[0].numpy(), offset_mappings, texts):
|
|
entity_output[:, [0, -1]] -= np.inf
|
|
entity_output[:, :, [0, -1]] -= np.inf
|
|
entity_probs = F.softmax(paddle.to_tensor(entity_output), axis=1).numpy()
|
|
ent_list = []
|
|
for l, start, end in zip(*np.where(entity_output > 0.0)):
|
|
ent_prob = entity_probs[l, start, end]
|
|
start, end = (offset_mapping[start][0], offset_mapping[end][-1])
|
|
ent = {
|
|
"text": text[start:end],
|
|
"type": label_maps["id2entity"][str(l)],
|
|
"start_index": start,
|
|
"probability": ent_prob,
|
|
}
|
|
ent_list.append(ent)
|
|
batch_ent_results.append(ent_list)
|
|
return batch_ent_results
|
|
else:
|
|
batch_ent_results = []
|
|
batch_rel_results = []
|
|
for entity_output, head_output, tail_output, offset_mapping, text in zip(
|
|
batch_outputs[0].numpy(),
|
|
batch_outputs[1].numpy(),
|
|
batch_outputs[2].numpy(),
|
|
offset_mappings,
|
|
texts,
|
|
):
|
|
entity_output[:, [0, -1]] -= np.inf
|
|
entity_output[:, :, [0, -1]] -= np.inf
|
|
entity_probs = F.softmax(paddle.to_tensor(entity_output), axis=1).numpy()
|
|
head_probs = F.softmax(paddle.to_tensor(head_output), axis=1).numpy()
|
|
tail_probs = F.softmax(paddle.to_tensor(tail_output), axis=1).numpy()
|
|
|
|
ents = set()
|
|
ent_list = []
|
|
for l, start, end in zip(*np.where(entity_output > 0.0)):
|
|
ent_prob = entity_probs[l, start, end]
|
|
ents.add((start, end))
|
|
start, end = (offset_mapping[start][0], offset_mapping[end][-1])
|
|
ent = {
|
|
"text": text[start:end],
|
|
"type": label_maps["id2entity"][str(l)],
|
|
"start_index": start,
|
|
"probability": ent_prob,
|
|
}
|
|
ent_list.append(ent)
|
|
batch_ent_results.append(ent_list)
|
|
|
|
rel_list = []
|
|
for sh, st in ents:
|
|
for oh, ot in ents:
|
|
p1s = np.where(head_output[:, sh, oh] > 0.0)[0]
|
|
p2s = np.where(tail_output[:, st, ot] > 0.0)[0]
|
|
ps = set(p1s) & set(p2s)
|
|
for p in ps:
|
|
rel_prob = head_probs[p, sh, oh] * tail_probs[p, st, ot]
|
|
if task_type == "relation_extraction":
|
|
rel = {
|
|
"subject": text[offset_mapping[sh][0] : offset_mapping[st][1]],
|
|
"predicate": label_maps["id2relation"][str(p)],
|
|
"object": text[offset_mapping[oh][0] : offset_mapping[ot][1]],
|
|
"subject_start_index": offset_mapping[sh][0],
|
|
"object_start_index": offset_mapping[oh][0],
|
|
"probability": rel_prob,
|
|
}
|
|
else:
|
|
rel = {
|
|
"aspect": text[offset_mapping[sh][0] : offset_mapping[st][1]],
|
|
"sentiment": label_maps["id2relation"][str(p)],
|
|
"opinion": text[offset_mapping[oh][0] : offset_mapping[ot][1]],
|
|
"aspect_start_index": offset_mapping[sh][0],
|
|
"opinion_start_index": offset_mapping[oh][0],
|
|
"probability": rel_prob,
|
|
}
|
|
rel_list.append(rel)
|
|
batch_rel_results.append(rel_list)
|
|
return (batch_ent_results, batch_rel_results)
|
|
|
|
|
|
DocSpan = namedtuple("DocSpan", ["start", "length"])
|
|
|
|
Example = namedtuple(
|
|
"Example",
|
|
[
|
|
"keys",
|
|
"key_labels",
|
|
"doc_tokens",
|
|
"text",
|
|
"qas_id",
|
|
"model_type",
|
|
"seq_labels",
|
|
"ori_boxes",
|
|
"boxes",
|
|
"segment_ids",
|
|
"symbol_ids",
|
|
"im_base64",
|
|
"image_rois",
|
|
],
|
|
)
|
|
|
|
Feature = namedtuple(
|
|
"Feature",
|
|
[
|
|
"unique_id",
|
|
"example_index",
|
|
"qas_id",
|
|
"doc_span_index",
|
|
"tokens",
|
|
"token_to_orig_map",
|
|
"token_is_max_context",
|
|
"token_ids",
|
|
"position_ids",
|
|
"text_type_ids",
|
|
"text_symbol_ids",
|
|
"overlaps",
|
|
"key_labels",
|
|
"seq_labels",
|
|
"se_seq_labels",
|
|
"bio_seq_labels",
|
|
"bioes_seq_labels",
|
|
"keys",
|
|
"model_type",
|
|
"doc_tokens",
|
|
"doc_labels",
|
|
"text",
|
|
"boxes",
|
|
"segment_ids",
|
|
"im_base64",
|
|
"image_rois",
|
|
],
|
|
)
|
|
|
|
|
|
class Compose(object):
|
|
"""compose"""
|
|
|
|
def __init__(self, transforms, ctx=None):
|
|
"""init"""
|
|
self.transforms = transforms
|
|
self.ctx = ctx
|
|
|
|
def __call__(self, data):
|
|
"""call"""
|
|
ctx = self.ctx if self.ctx else {}
|
|
for f in self.transforms:
|
|
try:
|
|
data = f(data, ctx)
|
|
except Exception as e:
|
|
stack_info = traceback.format_exc()
|
|
logger.warning("fail to map op [{}] with error: {} and stack:\n{}".format(f, e, str(stack_info)))
|
|
raise e
|
|
return data
|
|
|
|
|
|
def batch_arrange(batch_samples, fields):
|
|
def _segm(samples):
|
|
""""""
|
|
assert "gt_poly" in samples
|
|
segms = samples["gt_poly"]
|
|
if "is_crowd" in samples:
|
|
is_crowd = samples["is_crowd"]
|
|
if len(segms) != 0:
|
|
assert len(segms) == is_crowd.shape[0]
|
|
|
|
gt_masks = []
|
|
valid = True
|
|
for i in range(len(segms)):
|
|
segm = segms[i]
|
|
gt_segm = []
|
|
if "is_crowd" in samples and is_crowd[i]:
|
|
gt_segm.append([[0, 0]])
|
|
else:
|
|
for poly in segm:
|
|
if len(poly) == 0:
|
|
valid = False
|
|
break
|
|
gt_segm.append(np.array(poly).reshape(-1, 2))
|
|
if (not valid) or len(gt_segm) == 0:
|
|
break
|
|
gt_masks.append(gt_segm)
|
|
return gt_masks
|
|
|
|
def im_shape(samples, dim=3):
|
|
# hard code
|
|
assert "h" in samples
|
|
assert "w" in samples
|
|
if dim == 3: # RCNN, ..
|
|
return np.array((samples["h"], samples["w"], 1), dtype=np.float32)
|
|
else: # YOLOv3, ..
|
|
return np.array((samples["h"], samples["w"]), dtype=np.int32)
|
|
|
|
arrange_batch = []
|
|
for samples in batch_samples:
|
|
one_ins = ()
|
|
for i, field in enumerate(fields):
|
|
if field == "gt_mask":
|
|
one_ins += (_segm(samples),)
|
|
elif field == "im_shape":
|
|
one_ins += (im_shape(samples),)
|
|
elif field == "im_size":
|
|
one_ins += (im_shape(samples, 2),)
|
|
else:
|
|
if field == "is_difficult":
|
|
field = "difficult"
|
|
assert field in samples, "{} not in samples".format(field)
|
|
one_ins += (samples[field],)
|
|
arrange_batch.append(one_ins)
|
|
return arrange_batch
|
|
|
|
|
|
class ProcessReader(object):
|
|
"""
|
|
Args:
|
|
dataset (DataSet): DataSet object
|
|
sample_transforms (list of BaseOperator): a list of sample transforms
|
|
operators.
|
|
batch_transforms (list of BaseOperator): a list of batch transforms
|
|
operators.
|
|
batch_size (int): batch size.
|
|
shuffle (bool): whether shuffle dataset or not. Default False.
|
|
drop_last (bool): whether drop last batch or not. Default False.
|
|
drop_empty (bool): whether drop sample when it's gt is empty or not.
|
|
Default True.
|
|
mixup_epoch (int): mixup epoc number. Default is -1, meaning
|
|
not use mixup.
|
|
cutmix_epoch (int): cutmix epoc number. Default is -1, meaning
|
|
not use cutmix.
|
|
class_aware_sampling (bool): whether use class-aware sampling or not.
|
|
Default False.
|
|
worker_num (int): number of working threads/processes.
|
|
Default -1, meaning not use multi-threads/multi-processes.
|
|
use_process (bool): whether use multi-processes or not.
|
|
It only works when worker_num > 1. Default False.
|
|
bufsize (int): buffer size for multi-threads/multi-processes,
|
|
please note, one instance in buffer is one batch data.
|
|
memsize (str): size of shared memory used in result queue when
|
|
use_process is true. Default 3G.
|
|
inputs_def (dict): network input definition use to get input fields,
|
|
which is used to determine the order of returned data.
|
|
devices_num (int): number of devices.
|
|
num_trainers (int): number of trainers. Default 1.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
dataset=None,
|
|
sample_transforms=None,
|
|
batch_transforms=None,
|
|
batch_size=None,
|
|
shuffle=False,
|
|
drop_last=False,
|
|
drop_empty=True,
|
|
mixup_epoch=-1,
|
|
cutmix_epoch=-1,
|
|
class_aware_sampling=False,
|
|
use_process=False,
|
|
use_fine_grained_loss=False,
|
|
num_classes=80,
|
|
bufsize=-1,
|
|
memsize="3G",
|
|
inputs_def=None,
|
|
devices_num=1,
|
|
num_trainers=1,
|
|
):
|
|
""""""
|
|
self._fields = copy.deepcopy(inputs_def["fields"]) if inputs_def else None
|
|
|
|
# transform
|
|
self._sample_transforms = Compose(sample_transforms, {"fields": self._fields})
|
|
self._batch_transforms = None
|
|
|
|
if batch_transforms:
|
|
batch_transforms = [bt for bt in batch_transforms]
|
|
self._batch_transforms = Compose(batch_transforms, {"fields": self._fields})
|
|
|
|
self._batch_size = batch_size
|
|
self._shuffle = shuffle
|
|
self._drop_last = drop_last
|
|
self._drop_empty = drop_empty
|
|
|
|
# sampling
|
|
self._mixup_epoch = mixup_epoch // num_trainers
|
|
self._cutmix_epoch = cutmix_epoch // num_trainers
|
|
self._class_aware_sampling = class_aware_sampling
|
|
|
|
self._indexes = None
|
|
self._pos = -1
|
|
self._epoch = -1
|
|
self._curr_iter = 0
|
|
|
|
def process(self, dataset):
|
|
"""process"""
|
|
batch = self._load_batch(dataset)
|
|
res = self.worker(self._drop_empty, batch)
|
|
return res
|
|
|
|
def _load_batch(self, dataset):
|
|
batch = []
|
|
for data in dataset:
|
|
sample = copy.deepcopy(data)
|
|
batch.append(sample)
|
|
return batch
|
|
|
|
def worker(self, drop_empty=True, batch_samples=None):
|
|
"""
|
|
sample transform and batch transform.
|
|
"""
|
|
batch = []
|
|
for sample in batch_samples:
|
|
sample = self._sample_transforms(sample)
|
|
batch.append(sample)
|
|
if len(batch) > 0 and self._batch_transforms:
|
|
batch = self._batch_transforms(batch)
|
|
if len(batch) > 0 and self._fields:
|
|
batch = batch_arrange(batch, self._fields)
|
|
return batch
|
|
|
|
|
|
def pad_batch_data(
|
|
insts,
|
|
pad_idx=0,
|
|
max_seq_len=None,
|
|
return_pos=False,
|
|
return_input_mask=False,
|
|
return_max_len=False,
|
|
return_num_token=False,
|
|
return_seq_lens=False,
|
|
pad_2d_pos_ids=False,
|
|
pad_segment_id=False,
|
|
select=False,
|
|
extract=False,
|
|
):
|
|
"""
|
|
Pad the instances to the max sequence length in batch, and generate the
|
|
corresponding position data and attention bias.
|
|
"""
|
|
return_list = []
|
|
max_len = max(len(inst) for inst in insts) if max_seq_len is None else max_seq_len
|
|
# Any token included in dict can be used to pad, since the paddings' loss
|
|
# will be masked out by weights and make no effect on parameter gradients.
|
|
if pad_2d_pos_ids:
|
|
boxes = [x + [[0, 0, 0, 0]] * (max_len - len(x)) for x in insts]
|
|
boxes = np.array(boxes, dtype="int64")
|
|
return boxes
|
|
|
|
inst_data = np.array([inst + list([pad_idx] * (max_len - len(inst))) for inst in insts])
|
|
return_list += [inst_data.astype("int64").reshape([-1, max_len, 1])]
|
|
|
|
# position data
|
|
if return_pos:
|
|
inst_pos = np.array([list(range(0, len(inst))) + [pad_idx] * (max_len - len(inst)) for inst in insts])
|
|
|
|
return_list += [inst_pos.astype("int64").reshape([-1, max_len, 1])]
|
|
|
|
if return_input_mask:
|
|
# This is used to avoid attention on paddings.
|
|
input_mask_data = np.array([[1] * len(inst) + [0] * (max_len - len(inst)) for inst in insts])
|
|
input_mask_data = np.expand_dims(input_mask_data, axis=-1)
|
|
return_list += [input_mask_data.astype("float32")]
|
|
|
|
if return_max_len:
|
|
return_list += [max_len]
|
|
|
|
if return_num_token:
|
|
num_token = 0
|
|
for inst in insts:
|
|
num_token += len(inst)
|
|
return_list += [num_token]
|
|
|
|
if return_seq_lens:
|
|
seq_lens = np.array([len(inst) for inst in insts])
|
|
return_list += [seq_lens.astype("int64").reshape([-1, 1])]
|
|
|
|
return return_list if len(return_list) > 1 else return_list[0]
|
|
|
|
|
|
class ImageReader(object):
|
|
def __init__(
|
|
self,
|
|
super_rel_pos,
|
|
tokenizer,
|
|
max_key_len=16,
|
|
max_seq_len=512,
|
|
image_size=1024,
|
|
block_w=7,
|
|
block_h=7,
|
|
im_npos=224,
|
|
):
|
|
self.tokenizer = tokenizer
|
|
self.vocab = self.tokenizer.get_vocab()
|
|
|
|
self.pad_id = self.vocab["[PAD]"]
|
|
self.cls_id = self.vocab["[CLS]"]
|
|
self.sep_id = self.vocab["[SEP]"]
|
|
self.mask_id = self.vocab["[MASK]"]
|
|
self.pad = "[PAD]"
|
|
self.cls = "[CLS]"
|
|
self.sep = "[SEP]"
|
|
self.mask = "[MASK]"
|
|
|
|
self.super_rel_pos = super_rel_pos
|
|
self.max_key_len = max_key_len
|
|
self.max_seq_len = max_seq_len
|
|
self.doc_stride = 128
|
|
self.unique_id = 10000000
|
|
|
|
self.examples = {}
|
|
self.features = {}
|
|
|
|
self.image_size = image_size
|
|
self.block_w = block_w
|
|
self.block_h = block_h
|
|
self.im_npos = im_npos
|
|
self.image_rois = []
|
|
cut_width, cut_height = int(self.image_size / self.block_w), int(self.image_size / self.block_h)
|
|
for idh in range(self.block_h):
|
|
for idw in range(self.block_w):
|
|
self.image_rois.append([idw * cut_width, idh * cut_height, cut_width, cut_height])
|
|
|
|
sample_trans = [
|
|
DecodeImage(),
|
|
ResizeImage(target_size=self.im_npos, interp=1),
|
|
NormalizeImage(
|
|
is_channel_first=False,
|
|
mean=[103.530, 116.280, 123.675],
|
|
std=[57.375, 57.120, 58.395],
|
|
),
|
|
Permute(to_bgr=False),
|
|
]
|
|
|
|
batch_trans = [PadBatch(pad_to_stride=32, use_padded_im_info=True)]
|
|
|
|
inputs_def = {
|
|
"fields": ["image", "im_info", "im_id", "gt_bbox"],
|
|
}
|
|
self.data_loader = ProcessReader(
|
|
sample_transforms=sample_trans,
|
|
batch_transforms=batch_trans,
|
|
shuffle=False,
|
|
drop_empty=True,
|
|
inputs_def=inputs_def,
|
|
)
|
|
|
|
def ppocr2example(self, ocr_res, img_path, queries):
|
|
examples = []
|
|
segments = []
|
|
for rst in ocr_res:
|
|
left = min(rst[0][0][0], rst[0][3][0])
|
|
top = min(rst[0][0][-1], rst[0][1][-1])
|
|
width = max(rst[0][1][0], rst[0][2][0]) - min(rst[0][0][0], rst[0][3][0])
|
|
height = max(rst[0][2][-1], rst[0][3][-1]) - min(rst[0][0][-1], rst[0][1][-1])
|
|
segments.append({"bbox": Bbox(*[left, top, width, height]), "text": rst[-1][0]})
|
|
segments.sort(key=cmp_to_key(two_dimension_sort_layout))
|
|
# 2. im_base64
|
|
img_base64 = img2base64(img_path)
|
|
# 3. doc_tokens, doc_boxes, segment_ids
|
|
doc_tokens = []
|
|
doc_boxes = []
|
|
ori_boxes = []
|
|
doc_segment_ids = []
|
|
|
|
im_w_box = max([seg["bbox"].left + seg["bbox"].width for seg in segments]) + 20
|
|
im_h_box = max([seg["bbox"].top + seg["bbox"].height for seg in segments]) + 20
|
|
img = Image.open(img_path)
|
|
im_w, im_h = img.size
|
|
im_w, im_h = max(im_w, im_w_box), max(im_h, im_h_box)
|
|
|
|
scale_x = self.image_size / im_w
|
|
scale_y = self.image_size / im_h
|
|
for segment_id, segment in enumerate(segments):
|
|
bbox = segment["bbox"] # x, y, w, h
|
|
x1, y1, w, h = bbox.left, bbox.top, bbox.width, bbox.height
|
|
sc_w = int(min(w * scale_x, self.image_size - 1))
|
|
sc_h = int(min(h * scale_y, self.image_size - 1))
|
|
sc_y1 = int(max(0, min(y1 * scale_y, self.image_size - h - 1)))
|
|
sc_x1 = int(max(0, min(x1 * scale_x, self.image_size - w - 1)))
|
|
if w < 0:
|
|
raise ValueError("Incorrect bbox, please check the input word boxes.")
|
|
ori_bbox = Bbox(*[x1, y1, w, h])
|
|
sc_bbox = Bbox(*[sc_x1, sc_y1, sc_w, sc_h])
|
|
text = segment["text"]
|
|
char_num = []
|
|
eng_word = ""
|
|
for char in text:
|
|
if not check(char) and not eng_word:
|
|
doc_tokens.append([char])
|
|
doc_segment_ids.append([segment_id])
|
|
char_num.append(2)
|
|
elif not check(char) and eng_word:
|
|
doc_tokens.append([eng_word])
|
|
doc_segment_ids.append([segment_id])
|
|
char_num.append(len(eng_word))
|
|
eng_word = ""
|
|
doc_tokens.append([char])
|
|
doc_segment_ids.append([segment_id])
|
|
char_num.append(2)
|
|
else:
|
|
eng_word += char
|
|
if eng_word:
|
|
doc_tokens.append([eng_word])
|
|
doc_segment_ids.append([segment_id])
|
|
char_num.append(len(eng_word))
|
|
ori_char_width = round(ori_bbox.width / sum(char_num), 1)
|
|
sc_char_width = round(sc_bbox.width / sum(char_num), 1)
|
|
for chr_idx in range(len(char_num)):
|
|
if chr_idx == 0:
|
|
doc_boxes.append(
|
|
[Bbox(*[sc_bbox.left, sc_bbox.top, (sc_char_width * char_num[chr_idx]), sc_bbox.height])]
|
|
)
|
|
ori_boxes.append(
|
|
[Bbox(*[ori_bbox.left, ori_bbox.top, (ori_char_width * char_num[chr_idx]), ori_bbox.height])]
|
|
)
|
|
else:
|
|
doc_boxes.append(
|
|
[
|
|
Bbox(
|
|
*[
|
|
sc_bbox.left + (sc_char_width * sum(char_num[:chr_idx])),
|
|
sc_bbox.top,
|
|
(sc_char_width * char_num[chr_idx]),
|
|
sc_bbox.height,
|
|
]
|
|
)
|
|
]
|
|
)
|
|
ori_boxes.append(
|
|
[
|
|
Bbox(
|
|
*[
|
|
ori_bbox.left + (ori_char_width * sum(char_num[:chr_idx])),
|
|
ori_bbox.top,
|
|
(ori_char_width * char_num[chr_idx]),
|
|
ori_bbox.height,
|
|
]
|
|
)
|
|
]
|
|
)
|
|
|
|
qas_id = 0
|
|
for query in queries:
|
|
example = Example(
|
|
keys=[query],
|
|
key_labels=[0],
|
|
doc_tokens=doc_tokens,
|
|
seq_labels=[0 for one in doc_tokens],
|
|
text="",
|
|
qas_id="0_" + str(qas_id),
|
|
model_type=None,
|
|
ori_boxes=ori_boxes,
|
|
boxes=doc_boxes,
|
|
segment_ids=doc_segment_ids,
|
|
symbol_ids=None,
|
|
image_rois=self.image_rois,
|
|
im_base64=img_base64,
|
|
)
|
|
|
|
examples.append(example)
|
|
qas_id += 1
|
|
return examples
|
|
|
|
def box2example(self, ocr_res, img_path, queries):
|
|
"""
|
|
ocr_res = [[word_str, [x1, y1, x2, y2]], [word_str, [x1, y1, x2, y2]], ...]
|
|
"""
|
|
examples = []
|
|
doc_boxes = []
|
|
ori_boxes = []
|
|
boxes = [x[1] for x in ocr_res]
|
|
im_w_box = max([b[2] for b in boxes]) + 20
|
|
im_h_box = max([b[3] for b in boxes]) + 20
|
|
img = Image.open(img_path)
|
|
im_w, im_h = img.size
|
|
im_w, im_h = max(im_w, im_w_box), max(im_h, im_h_box)
|
|
|
|
scale_x = self.image_size / im_w
|
|
scale_y = self.image_size / im_h
|
|
for box in boxes:
|
|
x1, y1, x2, y2 = box
|
|
if x2 <= x1 or y2 <= y1:
|
|
raise ValueError("Invalid bbox format")
|
|
w = max(x1, x2) - min(x1, x2)
|
|
h = max(y1, y2) - min(y1, y2)
|
|
ori_boxes.append([Bbox(*[x1, y1, w, h])])
|
|
w = int(min(w * scale_x, self.image_size - 1))
|
|
h = int(min(h * scale_y, self.image_size - 1))
|
|
x1 = int(max(0, min(x1 * scale_x, self.image_size - w - 1)))
|
|
y1 = int(max(0, min(y1 * scale_y, self.image_size - h - 1)))
|
|
if w < 0:
|
|
raise ValueError("Invalid bbox format")
|
|
doc_boxes.append([Bbox(*[x1, y1, w, h])])
|
|
|
|
img_base64 = img2base64(img_path)
|
|
|
|
doc_tokens = [[x[0]] for x in ocr_res]
|
|
doc_segment_ids = [[0]] * len(doc_tokens)
|
|
|
|
qas_id = 0
|
|
for query in queries:
|
|
example = Example(
|
|
keys=[query],
|
|
key_labels=[0],
|
|
doc_tokens=doc_tokens,
|
|
seq_labels=[0 for one in doc_tokens],
|
|
text="",
|
|
qas_id=str(qas_id),
|
|
model_type=None,
|
|
ori_boxes=ori_boxes,
|
|
boxes=doc_boxes,
|
|
segment_ids=doc_segment_ids,
|
|
symbol_ids=None,
|
|
image_rois=self.image_rois,
|
|
im_base64=img_base64,
|
|
)
|
|
|
|
if not (len(example.doc_tokens) == len(example.boxes) == len(example.segment_ids)):
|
|
raise ValueError(
|
|
"Incorrect word_boxes, the format should be `List[str, Tuple[float, float, float, float]]`"
|
|
)
|
|
|
|
examples.append(example)
|
|
qas_id += 1
|
|
|
|
return examples
|
|
|
|
def example2feature(self, example, tokenizer, max_line_id=128):
|
|
features = []
|
|
all_doc_tokens = []
|
|
tok_to_orig_index = []
|
|
boxes = []
|
|
segment_ids = []
|
|
all_doc_labels = []
|
|
|
|
query_tokens = tokenizer.tokenize("&" + str(example.keys[0]))[1:][: self.max_key_len]
|
|
|
|
for i, (token_list, box_list, seg_list, l) in enumerate(
|
|
zip(example.doc_tokens, example.boxes, example.segment_ids, example.seq_labels)
|
|
):
|
|
assert len(token_list) == len(box_list) == len(seg_list)
|
|
for idt, (token, box, seg) in enumerate(zip(token_list, box_list, seg_list)):
|
|
sub_tokens = tokenizer.tokenize("&" + token)[1:]
|
|
for ii, sub_token in enumerate(sub_tokens):
|
|
width_split = box.width / len(sub_tokens)
|
|
boxes.append([box.left + ii * width_split, box.top, width_split, box.height])
|
|
segment_ids.append(seg)
|
|
tok_to_orig_index.append(i)
|
|
all_doc_tokens.append(sub_token)
|
|
all_doc_labels.extend([0])
|
|
|
|
max_tokens_for_doc = self.max_seq_len - len(query_tokens) - 4
|
|
doc_spans = []
|
|
start_offset = 0
|
|
while start_offset < len(all_doc_tokens):
|
|
length = len(all_doc_tokens) - start_offset
|
|
if length > max_tokens_for_doc:
|
|
length = max_tokens_for_doc
|
|
doc_spans.append(DocSpan(start=start_offset, length=length))
|
|
if start_offset + length == len(all_doc_tokens):
|
|
break
|
|
start_offset += min(length, self.doc_stride)
|
|
|
|
for (doc_span_index, doc_span) in enumerate(doc_spans):
|
|
tokens = []
|
|
labels = []
|
|
feature_segment_ids = []
|
|
feature_boxes = []
|
|
token_to_orig_map = {}
|
|
token_is_max_context = {}
|
|
text_type_ids = []
|
|
tokens.append(self.cls)
|
|
feature_boxes.append([0, 0, 0, 0])
|
|
labels.append(0)
|
|
text_type_ids.append(0)
|
|
feature_segment_ids.append(max_line_id - 1)
|
|
|
|
for i in range(doc_span.length):
|
|
split_token_index = doc_span.start + i
|
|
token_to_orig_map[len(tokens)] = tok_to_orig_index[split_token_index]
|
|
is_max_context = self._check_is_max_context(doc_spans, doc_span_index, split_token_index)
|
|
token_is_max_context[len(tokens)] = is_max_context
|
|
tokens.append(all_doc_tokens[split_token_index])
|
|
|
|
feature_boxes.append(boxes[split_token_index])
|
|
feature_segment_ids.append(segment_ids[split_token_index])
|
|
text_type_ids.append(0)
|
|
labels.append(all_doc_labels[split_token_index])
|
|
|
|
tokens.append(self.sep)
|
|
feature_boxes.append([0, 0, 0, 0])
|
|
text_type_ids.append(0)
|
|
feature_segment_ids.append(max_line_id - 1)
|
|
labels.append(0)
|
|
for token in query_tokens:
|
|
tokens.append(token)
|
|
feature_boxes.append([0, 0, 0, 0])
|
|
feature_segment_ids.append(max_line_id - 1)
|
|
text_type_ids.append(1)
|
|
labels.append(0)
|
|
|
|
tokens = tokens + [self.sep]
|
|
feature_boxes.extend([[0, 0, 0, 0]])
|
|
feature_segment_ids = feature_segment_ids + [max_line_id - 1]
|
|
text_type_ids = text_type_ids + [1]
|
|
labels.append(0)
|
|
|
|
position_ids = list(range(len(tokens)))
|
|
token_ids = tokenizer.convert_tokens_to_ids(tokens)
|
|
feature_segment_ids = [x % max_line_id for x in feature_segment_ids]
|
|
|
|
feature = Feature(
|
|
unique_id=self.unique_id,
|
|
example_index=0,
|
|
qas_id=example.qas_id,
|
|
doc_span_index=doc_span_index,
|
|
tokens=tokens,
|
|
token_to_orig_map=token_to_orig_map,
|
|
token_is_max_context=token_is_max_context,
|
|
token_ids=token_ids,
|
|
position_ids=position_ids,
|
|
text_type_ids=text_type_ids,
|
|
text_symbol_ids=None,
|
|
overlaps=None,
|
|
keys=example.keys,
|
|
seq_labels=labels,
|
|
se_seq_labels=None,
|
|
bio_seq_labels=None,
|
|
bioes_seq_labels=None,
|
|
key_labels=example.key_labels,
|
|
model_type=example.model_type,
|
|
doc_tokens=example.doc_tokens,
|
|
doc_labels=example.seq_labels,
|
|
text=example.text,
|
|
boxes=feature_boxes,
|
|
segment_ids=feature_segment_ids,
|
|
im_base64=example.im_base64,
|
|
image_rois=example.image_rois,
|
|
)
|
|
features.append(feature)
|
|
self.unique_id += 1
|
|
return features
|
|
|
|
def _pad_batch_records(self, batch_records, max_line_id=128, phase="infer"):
|
|
"""pad batch records"""
|
|
return_list = []
|
|
batch_token_ids = []
|
|
batch_sent_ids = []
|
|
batch_pos_ids = []
|
|
batch_2d_pos_ids = []
|
|
batch_segment_ids = []
|
|
batch_labels = []
|
|
batch_unique_id = []
|
|
batch_image_base64 = []
|
|
batch_image_rois = []
|
|
|
|
for i in range(len(batch_records)):
|
|
batch_token_ids.append(batch_records[i].token_ids)
|
|
batch_sent_ids.append(batch_records[i].text_type_ids)
|
|
batch_segment_ids.append(batch_records[i].segment_ids)
|
|
batch_labels.append(batch_records[i].seq_labels)
|
|
batch_unique_id.append(batch_records[i].unique_id)
|
|
batch_pos_ids.append(batch_records[i].position_ids)
|
|
batch_2d_pos_ids.append(batch_records[i].boxes)
|
|
batch_image_base64.append(batch_records[i].im_base64)
|
|
batch_image_rois.append(batch_records[i].image_rois)
|
|
|
|
padded_token_ids, _ = pad_batch_data(batch_token_ids, pad_idx=self.pad_id, return_input_mask=True)
|
|
padded_sent_ids = pad_batch_data(batch_sent_ids, pad_idx=self.pad_id)
|
|
padded_pos_ids = pad_batch_data(batch_pos_ids, pad_idx=self.pad_id)
|
|
new_padded_pos_ids = []
|
|
for idp, pos_ids in enumerate(padded_pos_ids):
|
|
new_padded_pos_ids.append(
|
|
np.concatenate((pos_ids, np.array([[x] for x in range(self.block_w * self.block_h)])), axis=0)
|
|
)
|
|
padded_pos_ids = np.array(new_padded_pos_ids)
|
|
padded_2d_pos_ids = pad_batch_data(batch_2d_pos_ids, pad_2d_pos_ids=True, select=False, extract=True)
|
|
new_padded_2d_pos_ids = []
|
|
for pos_ids_2d, batch_record in zip(padded_2d_pos_ids, batch_records):
|
|
new_padded_2d_pos_ids.append(np.concatenate((pos_ids_2d, np.array(batch_record.image_rois)), axis=0))
|
|
padded_2d_pos_ids = np.array(new_padded_2d_pos_ids)
|
|
padded_segment_ids = pad_batch_data(batch_segment_ids, pad_idx=max_line_id - 1)
|
|
|
|
input_mask_mat = self._build_input_mask(
|
|
np.array([list(x) + [[-1] for _ in range(self.block_w * self.block_h)] for x in padded_token_ids])
|
|
)
|
|
super_rel_pos = self._build_rel_pos(
|
|
np.array([list(x) + [[-1] for _ in range(self.block_w * self.block_h)] for x in padded_token_ids])
|
|
)
|
|
|
|
unique_id = np.array(batch_unique_id).astype("float32").reshape([-1, 1])
|
|
|
|
bsz, seq_len, _ = padded_token_ids.shape
|
|
task_ids = np.ones((bsz, seq_len, 1)).astype("int64")
|
|
for b in range(bsz):
|
|
if np.sum(padded_2d_pos_ids[b]) > 0:
|
|
task_ids[b, :, :] = 0
|
|
else:
|
|
task_ids[b, :, :] = 1
|
|
|
|
coco_data = self.generate_coco_data(
|
|
[""] * len(batch_image_base64),
|
|
batch_image_base64,
|
|
[self.image_size] * len(batch_image_base64),
|
|
[self.image_size] * len(batch_image_base64),
|
|
batch_image_rois,
|
|
)
|
|
|
|
image_data = self.im_make_batch(
|
|
self.data_loader.process(coco_data),
|
|
self.block_w * self.block_h,
|
|
len(batch_image_base64),
|
|
)
|
|
|
|
return_list = [
|
|
padded_token_ids,
|
|
padded_sent_ids,
|
|
padded_pos_ids,
|
|
padded_2d_pos_ids,
|
|
padded_segment_ids,
|
|
task_ids,
|
|
input_mask_mat,
|
|
super_rel_pos,
|
|
unique_id,
|
|
image_data,
|
|
]
|
|
return return_list
|
|
|
|
def data_generator(self, ocr_res, img_path, queries, batch_size, ocr_type="ppocr", phase="infer"):
|
|
if ocr_type == "ppocr":
|
|
self.examples[phase] = self.ppocr2example(ocr_res, img_path, queries)
|
|
elif ocr_type == "word_boxes":
|
|
self.examples[phase] = self.box2example(ocr_res, img_path, queries)
|
|
self.features[phase] = sum([self.example2feature(e, self.tokenizer) for e in self.examples[phase]], [])
|
|
for batch_data in self._prepare_batch_data(self.features[phase], batch_size, phase=phase):
|
|
yield self._pad_batch_records(batch_data)
|
|
|
|
def _prepare_batch_data(self, features, batch_size, phase=None):
|
|
"""generate batch records"""
|
|
batch_records = []
|
|
for feature in features:
|
|
to_append = len(batch_records) < batch_size
|
|
if to_append:
|
|
batch_records.append(feature)
|
|
else:
|
|
yield batch_records
|
|
batch_records = [feature]
|
|
|
|
if phase == "infer" and batch_records:
|
|
yield batch_records
|
|
|
|
def _build_input_mask(self, padded_token_ids):
|
|
"""build_input_mask"""
|
|
bsz, seq_len, _ = padded_token_ids.shape
|
|
return np.ones((bsz, seq_len, seq_len)).astype("float32")
|
|
|
|
def _build_rel_pos(self, padded_token_ids):
|
|
"""build relative position"""
|
|
bsz, seq_len, _ = padded_token_ids.shape
|
|
rel_pos = np.zeros((bsz, seq_len, seq_len)).astype("int64")
|
|
return rel_pos
|
|
|
|
def generate_coco_data(
|
|
self,
|
|
batch_image_path,
|
|
batch_image_base64,
|
|
batch_scaled_width,
|
|
batch_scaled_height,
|
|
batch_rois,
|
|
):
|
|
"""generator coco data"""
|
|
|
|
def transform(dataset):
|
|
roidbs = []
|
|
for i in dataset:
|
|
rvl_rec = {
|
|
"im_file": i[0],
|
|
"im_id": np.array([i[1]]),
|
|
"h": i[2],
|
|
"w": i[3],
|
|
"gt_bbox": i[4],
|
|
"cover_box": i[5],
|
|
"im_base64": i[6],
|
|
}
|
|
|
|
roidbs.append(rvl_rec)
|
|
return roidbs
|
|
|
|
result = []
|
|
for image_path, im_base64, width, height, roi in zip(
|
|
batch_image_path,
|
|
batch_image_base64,
|
|
batch_scaled_width,
|
|
batch_scaled_height,
|
|
batch_rois,
|
|
):
|
|
result.append((image_path, 0, height, width, roi, None, im_base64))
|
|
return transform(result)
|
|
|
|
def im_make_batch(self, dataset, image_boxes_nums, bsize):
|
|
"""make image batch"""
|
|
img_batch = np.array([i[0] for i in dataset], "float32")
|
|
return img_batch
|
|
|
|
def BIO2SPAN(self, BIO):
|
|
start_label, end_label = [], []
|
|
for seq in BIO:
|
|
first_one = True
|
|
start_pos = [1 if x == 2 else 0 for x in seq]
|
|
if sum(start_pos) == 0 and sum(seq) != 0:
|
|
start_pos = []
|
|
for idp, p in enumerate(seq):
|
|
if p == 1 and first_one:
|
|
start_pos.append(1)
|
|
first_one = False
|
|
else:
|
|
start_pos.append(0)
|
|
|
|
start_label.append(start_pos)
|
|
|
|
end_tmp = []
|
|
for index, s in enumerate(seq):
|
|
if s == -100 or s == 0:
|
|
end_tmp.append(s)
|
|
elif s == 2 and index + 1 < len(seq) and (seq[index + 1] == 0 or seq[index + 1] == 2):
|
|
end_tmp.append(1)
|
|
elif s == 2 and index + 1 < len(seq) and seq[index + 1] != 0:
|
|
end_tmp.append(0)
|
|
elif s == 2 and index + 1 == len(seq):
|
|
end_tmp.append(1)
|
|
elif s == 1 and (index + 1 == len(seq) or seq[index + 1] != 1):
|
|
end_tmp.append(1)
|
|
else:
|
|
end_tmp.append(0)
|
|
end_label.append(end_tmp)
|
|
|
|
return start_label, end_label
|
|
|
|
def _check_is_max_context(self, doc_spans, cur_span_index, position):
|
|
best_score = None
|
|
best_span_index = None
|
|
for (span_index, doc_span) in enumerate(doc_spans):
|
|
end = doc_span.start + doc_span.length - 1
|
|
if position < doc_span.start:
|
|
continue
|
|
if position > end:
|
|
continue
|
|
num_left_context = position - doc_span.start
|
|
num_right_context = end - position
|
|
score = min(num_left_context, num_right_context) + 0.01 * doc_span.length
|
|
if best_score is None or score > best_score:
|
|
best_score = score
|
|
best_span_index = span_index
|
|
return cur_span_index == best_span_index
|
|
|
|
|
|
def get_doc_pred(result, ans_pos, example, tokenizer, feature, do_lower_case, all_key_probs, example_index):
|
|
def _compute_softmax(scores):
|
|
"""Compute softmax probability over raw logits."""
|
|
if len(scores) == 0:
|
|
return []
|
|
|
|
max_score = None
|
|
for score in scores:
|
|
if max_score is None or score > max_score:
|
|
max_score = score
|
|
|
|
exp_scores = []
|
|
total_sum = 0.0
|
|
for score in scores:
|
|
x = math.exp(score - max_score)
|
|
exp_scores.append(x)
|
|
total_sum += x
|
|
|
|
probs = []
|
|
for score in exp_scores:
|
|
probs.append(score / total_sum)
|
|
return probs
|
|
|
|
preds = []
|
|
for start_index, end_index in ans_pos:
|
|
# process data
|
|
tok_tokens = feature.tokens[start_index : end_index + 1]
|
|
tok_text = " ".join(tok_tokens)
|
|
# De-tokenize WordPieces that have been split off.
|
|
tok_text = tok_text.replace(" ##", "")
|
|
tok_text = tok_text.replace("##", "")
|
|
tok_text = tok_text.strip()
|
|
tok_text = "".join(tok_text.split())
|
|
|
|
orig_doc_start = feature.token_to_orig_map[start_index]
|
|
orig_doc_end = feature.token_to_orig_map[end_index]
|
|
orig_tokens = example.doc_tokens[orig_doc_start : orig_doc_end + 1]
|
|
|
|
# Clean whitespace
|
|
orig_text = "".join(["".join(x) for x in orig_tokens])
|
|
final_text = get_final_text(tok_text, orig_text, tokenizer, do_lower_case)
|
|
|
|
probs = []
|
|
for idx, logit in enumerate(result.seq_logits[start_index : end_index + 1]):
|
|
if idx == 0:
|
|
# -1 is for B in OIB or I in OI
|
|
probs.append(_compute_softmax(logit)[-1])
|
|
else:
|
|
# 1 is for I in OIB or I in OI
|
|
probs.append(_compute_softmax(logit)[1])
|
|
avg_prob = sum(probs) / len(probs)
|
|
preds.append({"value": final_text, "prob": round(avg_prob, 2), "start": orig_doc_start, "end": orig_doc_end})
|
|
return preds
|
|
|
|
|
|
def get_final_text(pred_text, orig_text, tokenizer, do_lower_case):
|
|
"""Project the tokenized prediction back to the original text."""
|
|
|
|
def _strip_spaces(text):
|
|
ns_chars = []
|
|
ns_to_s_map = OrderedDict()
|
|
for (i, c) in enumerate(text):
|
|
if c == " ":
|
|
continue
|
|
ns_to_s_map[len(ns_chars)] = i
|
|
ns_chars.append(c)
|
|
ns_text = "".join(ns_chars)
|
|
return (ns_text, ns_to_s_map)
|
|
|
|
tok_text = " ".join(tokenizer.tokenize(orig_text))
|
|
|
|
start_position = tok_text.find(pred_text)
|
|
if start_position == -1:
|
|
return orig_text
|
|
end_position = start_position + len(pred_text) - 1
|
|
|
|
(orig_ns_text, orig_ns_to_s_map) = _strip_spaces(orig_text)
|
|
(tok_ns_text, tok_ns_to_s_map) = _strip_spaces(tok_text)
|
|
|
|
if len(orig_ns_text) != len(tok_ns_text):
|
|
return orig_text
|
|
|
|
# We then project the characters in `pred_text` back to `orig_text` using
|
|
# the character-to-character alignment.
|
|
tok_s_to_ns_map = {}
|
|
for (i, tok_index) in six.iteritems(tok_ns_to_s_map):
|
|
tok_s_to_ns_map[tok_index] = i
|
|
|
|
orig_start_position = None
|
|
if start_position in tok_s_to_ns_map:
|
|
ns_start_position = tok_s_to_ns_map[start_position]
|
|
if ns_start_position in orig_ns_to_s_map:
|
|
orig_start_position = orig_ns_to_s_map[ns_start_position]
|
|
|
|
if orig_start_position is None:
|
|
return orig_text
|
|
|
|
orig_end_position = None
|
|
if end_position in tok_s_to_ns_map:
|
|
ns_end_position = tok_s_to_ns_map[end_position]
|
|
if ns_end_position in orig_ns_to_s_map:
|
|
orig_end_position = orig_ns_to_s_map[ns_end_position]
|
|
|
|
if orig_end_position is None:
|
|
return orig_text
|
|
|
|
output_text = orig_text[orig_start_position : (orig_end_position + 1)]
|
|
return output_text
|
|
|
|
|
|
def find_bio_pos(label):
|
|
"""find answer position from BIO label"""
|
|
e = []
|
|
cand_ans = []
|
|
last_l = None
|
|
for idx, l in enumerate(label):
|
|
if l == "O":
|
|
if e:
|
|
cand_ans.append([e[0], e[-1]])
|
|
e = []
|
|
elif l.startswith("B"):
|
|
if last_l == "O" or last_l is None:
|
|
if len(e) != 0:
|
|
e = []
|
|
e.append(idx)
|
|
else: # I B
|
|
if e:
|
|
cand_ans.append([e[0], e[-1]])
|
|
e = []
|
|
e.append(idx)
|
|
elif l.startswith("I"):
|
|
if len(e) == 0:
|
|
continue
|
|
else:
|
|
e.append(idx)
|
|
last_l = l
|
|
if e:
|
|
cand_ans.append([e[0], e[-1]])
|
|
return cand_ans
|
|
|
|
|
|
def viterbi_decode(logits):
|
|
np_logits = np.array(logits) # shape: L * D
|
|
length, dim = np_logits.shape
|
|
f = np.zeros(np_logits.shape)
|
|
path = [["" for i in range(dim)] for j in range(length)]
|
|
label_scheme = "OIB"
|
|
# oib label 0:O, 1:I, 2:B
|
|
# illegal matrix: [O, I ,B, start, end] * [O, I, B, start, end]
|
|
illegal = np.array([[0, -1, 0, -1, 0], [0, 0, 0, -1, 0], [0, 0, 0, 0, 0], [0, -1, 0, 0, 0], [-1, -1, -1, -1, -1]])
|
|
illegal = illegal * 1000
|
|
|
|
f[0, :] = np_logits[0, :] + illegal[3, :3]
|
|
path[0] = [label_scheme[i] for i in range(dim)]
|
|
|
|
for step in range(1, length):
|
|
last_s = f[step - 1, :]
|
|
for d in range(dim):
|
|
cand_score = illegal[:3, d] + last_s + np_logits[step, d]
|
|
f[step, d] = np.max(cand_score)
|
|
path[step][d] = path[step - 1][np.argmax(cand_score)] + label_scheme[d]
|
|
final_path = path[-1][np.argmax(f[-1, :])]
|
|
return final_path
|
|
|
|
|
|
def find_answer_pos(logits, feature):
|
|
start_index = -1
|
|
end_index = -1
|
|
ans = []
|
|
cand_ans = []
|
|
|
|
best_path = viterbi_decode(logits)
|
|
cand_ans = find_bio_pos(best_path)
|
|
|
|
for start_index, end_index in cand_ans:
|
|
is_valid = True
|
|
if start_index not in feature.token_to_orig_map:
|
|
is_valid = False
|
|
if end_index not in feature.token_to_orig_map:
|
|
is_valid = False
|
|
if not feature.token_is_max_context.get(start_index, False):
|
|
is_valid = False
|
|
if end_index < start_index:
|
|
is_valid = False
|
|
if is_valid:
|
|
ans.append([start_index, end_index])
|
|
|
|
return ans
|
|
|
|
|
|
def calEuclidean(x_list, y_list):
|
|
"""
|
|
Calculate euclidean distance
|
|
"""
|
|
if x_list is None or y_list is None:
|
|
return None
|
|
else:
|
|
dist = np.sqrt(np.square(x_list[0] - y_list[0]) + np.square(x_list[1] - y_list[1]))
|
|
return dist
|
|
|
|
|
|
def longestCommonSequence(question_tokens, context_tokens):
|
|
"""
|
|
Longest common sequence
|
|
"""
|
|
max_index = -1
|
|
max_len = 0
|
|
m, n = len(question_tokens), len(context_tokens)
|
|
dp = [[0] * (n + 1) for _ in range(m + 1)]
|
|
for i in range(1, m + 1):
|
|
for j in range(1, n + 1):
|
|
if question_tokens[i - 1].lower() == context_tokens[j - 1][0].lower():
|
|
dp[i][j] = 1 + dp[i - 1][j - 1]
|
|
if dp[i][j] > max_len:
|
|
max_len = dp[i][j]
|
|
max_index = j - 1
|
|
return max_index, max_len
|
|
|
|
|
|
def sort_res(prompt, ans_list, context, boxes, lang="en"):
|
|
if len(ans_list) == 1:
|
|
return ans_list
|
|
else:
|
|
ans_val = []
|
|
for ans in ans_list:
|
|
ans_val.append(ans["value"])
|
|
if len(set(ans_val)) == len(ans_val):
|
|
sorted_ans_list = sorted(ans_list, key=lambda x: x["prob"], reverse=True)
|
|
return sorted_ans_list
|
|
else:
|
|
if lang == "en":
|
|
clean_prompt = [word for word in prompt.split(" ")]
|
|
else:
|
|
clean_prompt = [word for word in prompt]
|
|
|
|
max_index, max_len = longestCommonSequence(clean_prompt, context)
|
|
if max_index == -1:
|
|
sorted_ans_list = sorted(ans_list, key=lambda x: x["prob"], reverse=True)
|
|
return sorted_ans_list
|
|
else:
|
|
prompt_center = []
|
|
for idx in range(max_index - max_len + 1, max_index + 1):
|
|
box = boxes[idx][0]
|
|
x = box.left + box.width / 2
|
|
y = box.top + box.height / 2
|
|
prompt_center.append([x, y])
|
|
|
|
ans_center = []
|
|
ans_prob = []
|
|
for ans in ans_list:
|
|
ans_prob.append(ans["prob"])
|
|
cent_list = []
|
|
for idx in range(ans["start"], ans["end"] + 1):
|
|
box = boxes[idx][0]
|
|
x = box.left + box.width / 2
|
|
y = box.top + box.height / 2
|
|
cent_list.append([x, y])
|
|
ans_center.append(cent_list)
|
|
|
|
ans_odist = []
|
|
for ans_c in ans_center:
|
|
odist = 0
|
|
for a_c in ans_c:
|
|
for p_c in prompt_center:
|
|
odist += calEuclidean(a_c, p_c)
|
|
odist /= len(ans_c)
|
|
ans_odist.append(odist * (-1))
|
|
|
|
ans_score = np.sum([ans_prob, ans_odist], axis=0).tolist()
|
|
sorted_ans_list = sorted(ans_list, key=lambda x: ans_score[ans_list.index(x)], reverse=True)
|
|
return sorted_ans_list
|