hankcs--hanlp
321 行
11 KiB
Python
321 行
11 KiB
Python
# -*- coding:utf-8 -*-
|
||
# Author: hankcs
|
||
# Date: 2020-11-25 16:14
|
||
import os
|
||
import shutil
|
||
import sys
|
||
from collections import defaultdict
|
||
from os import listdir
|
||
from os.path import join, isfile
|
||
from typing import List
|
||
|
||
from phrasetree.tree import Tree
|
||
|
||
from hanlp.components.parsers.conll import read_conll
|
||
from hanlp.utils.io_util import get_resource, get_exitcode_stdout_stderr, read_tsv_as_sents, run_cmd, pushd
|
||
from hanlp.utils.log_util import cprint
|
||
from hanlp.utils.time_util import CountdownTimer
|
||
|
||
|
||
# See Shao et al., 2017
|
||
# CTB9_ACADEMIA_SPLITS = {
|
||
# 'train': '''
|
||
# 0044-0143, 0170-0270, 0400-0899,
|
||
# 1001-1017, 1019, 1021-1035, 1037-
|
||
# 1043, 1045-1059, 1062-1071, 1073-
|
||
# 1117, 1120-1131, 1133-1140, 1143-
|
||
# 1147, 1149-1151, 2000-2915, 4051-
|
||
# 4099, 4112-4180, 4198-4368, 5000-
|
||
# 5446, 6000-6560, 7000-7013
|
||
# ''',
|
||
# 'dev': '''
|
||
# 0301-0326, 2916-3030, 4100-4106,
|
||
# 4181-4189, 4369-4390, 5447-5492,
|
||
# 6561-6630, 7013-7014
|
||
# ''',
|
||
# 'test': '''
|
||
# 0001-0043, 0144-0169, 0271-0301,
|
||
# 0900-0931, 1018, 1020, 1036, 1044,
|
||
# 1060, 1061, 1072, 1118, 1119, 1132,
|
||
# 1141, 1142, 1148, 3031-3145, 4107-
|
||
# 4111, 4190-4197, 4391-4411, 5493-
|
||
# 5558, 6631-6700, 7015-7017
|
||
# '''
|
||
# }
|
||
#
|
||
#
|
||
# def _make_splits(splits: Dict[str, str]):
|
||
# total = set()
|
||
# for part, text in list(splits.items()):
|
||
# if not isinstance(text, str):
|
||
# continue
|
||
# lines = text.replace('\n', '').split()
|
||
# cids = set()
|
||
# for line in lines:
|
||
# for each in line.split(','):
|
||
# each = each.strip()
|
||
# if not each:
|
||
# continue
|
||
# if '-' in each:
|
||
# start, end = each.split('-')
|
||
# start, end = map(lambda x: int(x), [start, end])
|
||
# cids.update(range(start, end + 1))
|
||
# # cids.update(map(lambda x: f'{x:04d}', range(start, end)))
|
||
# else:
|
||
# cids.add(int(each))
|
||
# cids = set(f'{x:04d}' for x in cids)
|
||
# assert len(cids & total) == 0, f'Overlap found in {part}'
|
||
# splits[part] = cids
|
||
#
|
||
# return splits
|
||
#
|
||
#
|
||
# _make_splits(CTB9_ACADEMIA_SPLITS)
|
||
|
||
|
||
def convert_to_dependency(src, dst, language='zh', version='3.3.0', conllx=True, ud=False):
|
||
cprint(f'Converting {os.path.basename(src)} to {os.path.basename(dst)} using Stanford Parser Version {version}. '
|
||
f'It might take a while [blink][yellow]...[/yellow][/blink]')
|
||
if version == '3.3.0':
|
||
sp_home = 'https://nlp.stanford.edu/software/stanford-parser-full-2013-11-12.zip'
|
||
elif version == '4.2.0':
|
||
sp_home = 'https://nlp.stanford.edu/software/stanford-parser-4.2.0.zip'
|
||
else:
|
||
raise ValueError(f'Unsupported version {version}')
|
||
sp_home = get_resource(sp_home)
|
||
# jar_path = get_resource(f'{sp_home}#stanford-parser.jar')
|
||
if ud:
|
||
jclass = 'edu.stanford.nlp.trees.international.pennchinese.UniversalChineseGrammaticalStructure' if language == 'zh' \
|
||
else 'edu.stanford.nlp.trees.ud.UniversalDependenciesConverter'
|
||
else:
|
||
jclass = 'edu.stanford.nlp.trees.international.pennchinese.ChineseGrammaticalStructure' if language == 'zh' \
|
||
else 'edu.stanford.nlp.trees.EnglishGrammaticalStructure'
|
||
cmd = f'java -cp {sp_home}/* {jclass} ' \
|
||
f'-treeFile {src}'
|
||
if conllx:
|
||
cmd += ' -conllx'
|
||
if not ud:
|
||
cmd += f' -basic -keepPunct'
|
||
code, out, err = get_exitcode_stdout_stderr(cmd)
|
||
with open(dst, 'w') as f:
|
||
f.write(out)
|
||
if code:
|
||
raise RuntimeError(f'Conversion failed with code {code} for {src}. The err message is:\n {err}\n'
|
||
f'Do you have java installed? Do you have enough memory?')
|
||
|
||
|
||
def clean_ctb_bracketed(ctb_root, out_root):
|
||
os.makedirs(out_root, exist_ok=True)
|
||
ctb_root = join(ctb_root, 'bracketed')
|
||
chtbs = _list_treebank_root(ctb_root)
|
||
timer = CountdownTimer(len(chtbs))
|
||
for f in chtbs:
|
||
with open(join(ctb_root, f), encoding='utf-8') as src, open(join(out_root, f + '.txt'), 'w',
|
||
encoding='utf-8') as out:
|
||
for line in src:
|
||
if not line.strip().startswith('<'):
|
||
out.write(line)
|
||
timer.log('Cleaning up CTB [blink][yellow]...[/yellow][/blink]', erase=False)
|
||
|
||
|
||
def _list_treebank_root(ctb_root):
|
||
chtbs = [f for f in listdir(ctb_root) if isfile(join(ctb_root, f)) and f.startswith('chtb')]
|
||
return sorted(chtbs)
|
||
|
||
|
||
def list_treebank(ctb_home):
|
||
ctb_home = get_resource(ctb_home)
|
||
cleaned_root = join(ctb_home, 'cleaned_bracket')
|
||
return _list_treebank_root(cleaned_root)
|
||
|
||
|
||
def load_bracketed_trees(chtbs) -> List[Tree]:
|
||
trees = []
|
||
for f in chtbs:
|
||
with open(f, encoding='utf-8') as src:
|
||
content = src.read()
|
||
trees = [x for x in content.split('\n\n') if x.strip()]
|
||
for tree in trees:
|
||
tree = Tree.fromstring(tree)
|
||
trees.append(tree)
|
||
return trees
|
||
|
||
|
||
def split_str_to_trees(text: str):
|
||
trees = []
|
||
buffer = []
|
||
for line in text.split('\n'):
|
||
if not line.strip():
|
||
continue
|
||
if line.startswith('('):
|
||
if buffer:
|
||
trees.append('\n'.join(buffer).strip())
|
||
buffer = []
|
||
buffer.append(line)
|
||
if buffer:
|
||
trees.append('\n'.join(buffer).strip())
|
||
return trees
|
||
|
||
|
||
def make_ctb_tasks(chtbs, out_root, part):
|
||
for task in ['cws', 'pos', 'par', 'dep']:
|
||
os.makedirs(join(out_root, task), exist_ok=True)
|
||
timer = CountdownTimer(len(chtbs))
|
||
par_path = join(out_root, 'par', f'{part}.txt')
|
||
with open(join(out_root, 'cws', f'{part}.txt'), 'w', encoding='utf-8') as cws, \
|
||
open(join(out_root, 'pos', f'{part}.tsv'), 'w', encoding='utf-8') as pos, \
|
||
open(par_path, 'w', encoding='utf-8') as par:
|
||
for f in chtbs:
|
||
with open(f, encoding='utf-8') as src:
|
||
content = src.read()
|
||
trees = split_str_to_trees(content)
|
||
for tree in trees:
|
||
try:
|
||
tree = Tree.fromstring(tree)
|
||
except ValueError:
|
||
print(tree)
|
||
exit(1)
|
||
words = []
|
||
for word, tag in tree.pos():
|
||
if tag == '-NONE-' or not tag:
|
||
continue
|
||
tag = tag.split('-')[0]
|
||
if tag == 'X': # 铜_NN 30_CD x_X 25_CD x_X 14_CD cm_NT 1999_NT
|
||
tag = 'FW'
|
||
pos.write('{}\t{}\n'.format(word, tag))
|
||
words.append(word)
|
||
cws.write(' '.join(words))
|
||
par.write(tree.pformat(margin=sys.maxsize))
|
||
for fp in cws, pos, par:
|
||
fp.write('\n')
|
||
timer.log(f'Preprocesing the [blue]{part}[/blue] set of CTB [blink][yellow]...[/yellow][/blink]',
|
||
erase=False)
|
||
remove_all_ec(par_path)
|
||
dep_path = join(out_root, 'dep', f'{part}.conllx')
|
||
convert_to_dependency(par_path, dep_path)
|
||
sents = list(read_conll(dep_path))
|
||
with open(dep_path, 'w') as out:
|
||
for sent in sents:
|
||
for i, cells in enumerate(sent):
|
||
tag = cells[3]
|
||
tag = tag.split('-')[0] # NT-SHORT ---> NT
|
||
if tag == 'X': # 铜_NN 30_CD x_X 25_CD x_X 14_CD cm_NT 1999_NT
|
||
tag = 'FW'
|
||
cells[3] = cells[4] = tag
|
||
out.write('\t'.join(str(x) for x in cells))
|
||
out.write('\n')
|
||
out.write('\n')
|
||
|
||
|
||
def reverse_splits(splits):
|
||
cid_domain = dict()
|
||
for domain, cids in splits.items():
|
||
for each in cids:
|
||
cid_domain[each] = domain
|
||
return cid_domain
|
||
|
||
|
||
def split_chtb(chtbs: List[str], splits=None):
|
||
train, dev, test = [], [], []
|
||
unused = []
|
||
for each in chtbs:
|
||
name, domain, ext = each.split('.', 2)
|
||
_, cid = name.split('_')
|
||
if splits:
|
||
if cid in splits['train']:
|
||
bin = train
|
||
elif cid in splits['dev']:
|
||
bin = dev
|
||
elif cid in splits['test']:
|
||
bin = test
|
||
else:
|
||
bin = unused
|
||
# raise IOError(f'{name} not in any splits')
|
||
else:
|
||
bin = train
|
||
if name.endswith('8'):
|
||
bin = dev
|
||
elif name.endswith('9'):
|
||
bin = test
|
||
bin.append(each)
|
||
return train, dev, test
|
||
|
||
|
||
def id_of_chtb(each: str):
|
||
return int(each.split('.')[0].split('_')[-1])
|
||
|
||
|
||
def make_ctb(ctb_home):
|
||
ctb_home = get_resource(ctb_home)
|
||
cleaned_root = join(ctb_home, 'cleaned_bracket')
|
||
if not os.path.isdir(cleaned_root):
|
||
clean_ctb_bracketed(ctb_home, cleaned_root)
|
||
tasks_root = join(ctb_home, 'tasks')
|
||
if not os.path.isdir(tasks_root):
|
||
try:
|
||
chtbs = _list_treebank_root(cleaned_root)
|
||
print(f'For the {len(chtbs)} files in CTB, we apply the following splits:')
|
||
train, dev, test = split_chtb(chtbs)
|
||
for part, name in zip([train, dev, test], ['train', 'dev', 'test']):
|
||
print(f'{name} = {[id_of_chtb(x) for x in part]}')
|
||
cprint('[yellow]Each file id ending with 8/9 is put into '
|
||
'dev/test respectively, the rest are put into train. '
|
||
'Our splits ensure files are evenly split across each genre, which is recommended '
|
||
'for production systems.[/yellow]')
|
||
for part, name in zip([train, dev, test], ['train', 'dev', 'test']):
|
||
make_ctb_tasks([join(cleaned_root, x) for x in part], tasks_root, name)
|
||
cprint('Done pre-processing CTB. Enjoy your research with [blue]HanLP[/blue]!')
|
||
except Exception as e:
|
||
shutil.rmtree(tasks_root, ignore_errors=True)
|
||
raise e
|
||
|
||
|
||
def load_domains(ctb_home):
|
||
"""
|
||
Load file ids from a Chinese treebank grouped by domains.
|
||
|
||
Args:
|
||
ctb_home: Root path to CTB.
|
||
|
||
Returns:
|
||
A dict of sets, each represents a domain.
|
||
"""
|
||
ctb_home = get_resource(ctb_home)
|
||
ctb_root = join(ctb_home, 'bracketed')
|
||
chtbs = _list_treebank_root(ctb_root)
|
||
domains = defaultdict(set)
|
||
for each in chtbs:
|
||
name, domain = each.split('.')
|
||
_, fid = name.split('_')
|
||
domains[domain].add(fid)
|
||
return domains
|
||
|
||
|
||
def ctb_pos_to_text_format(path, delimiter='_'):
|
||
"""
|
||
Convert ctb pos tagging corpus from tsv format to text format, where each word is followed by
|
||
its pos tag.
|
||
Args:
|
||
path: File to be converted.
|
||
delimiter: Delimiter between word and tag.
|
||
"""
|
||
path = get_resource(path)
|
||
name, ext = os.path.splitext(path)
|
||
with open(f'{name}.txt', 'w', encoding='utf-8') as out:
|
||
for sent in read_tsv_as_sents(path):
|
||
out.write(' '.join([delimiter.join(x) for x in sent]))
|
||
out.write('\n')
|
||
|
||
|
||
def remove_all_ec(path):
|
||
"""
|
||
Remove empty categories for all trees in this file and save them into a "noempty" file.
|
||
|
||
Args:
|
||
path: File path.
|
||
"""
|
||
script = get_resource('https://file.hankcs.com/bin/remove_ec.zip')
|
||
with pushd(script):
|
||
run_cmd(f'java -cp elit-ddr-0.0.5-SNAPSHOT.jar:elit-sdk-0.0.5-SNAPSHOT.jar:hanlp-1.7.8.jar:'
|
||
f'fastutil-8.1.1.jar:. demo.RemoveEmptyCategoriesTreebank {path}')
|