yasuokaの日記: 単語内のリンクにgoeswithを用いた日本語係り受け解析器の製作
6月4日の日記でリリースしたbert-large-japanese-char-extendedをもとに、日本語係り受け解析器を作ってみることにした。ただ、bert-large-japanese-char-extendedの単語長は1文字なので、UD_Japanese-GSDの単語長である国語研短単位とは、長さが合わない。多少、悩んだあげく、単語内のリンクにgoeswithを使っておいて、解析後にgoeswithを削り取ることで、単語組み上げをおこなうことにした。Google Colaboratory(GPU版)で、TransformersのAutoModelForTokenClassificationを使って、日本語係り受け解析器を作ってみよう。
!test -d UD_Japanese-GSD || git clone --depth=1 https://github.com/universaldependencies/UD_Japanese-GSD
!pip install transformers datasets
from transformers import AutoTokenizer,AutoConfig,AutoModelForTokenClassification,DataCollatorForTokenClassification,TrainingArguments,Trainer
from unicodedata import category
from datasets.arrow_dataset import Dataset
brt="KoichiYasuoka/bert-large-japanese-char-extended"
with open("UD_Japanese-GSD/ja_gsd-ud-train.conllu","r",encoding="utf-8") as f:
r=f.read()
tok,tag=[],[]
for u in r.strip().split("\n\n"):
w=[t for t in u.split("\n") if t.startswith("# text = ")][0]
s=[t.split("\t") for t in u.split("\n") if not t.startswith("#")]
for i,v in enumerate([v for v in w[9:] if category(v)!="Zs"]):
t=s[i]
x,t[0],t[1],t[2],t[8],t[9]=t[1],str(i+1),v,"_","_","_" if t[9].find("SpaceAfter=No")<0 else "SpaceAfter=No"
if v!=x:
s.insert(i+1,[str(i+2),x[1:],"_","X","_","_",t[6] if t[7]=="goeswith" else str(i+1),"goeswith","_",t[9]])
t[9]="SpaceAfter=No"
for t in [t for t in s if int(t[6])>i+1]:
t[6]=str(int(t[6])+1)
tok.append([t[1] for t in s])
tag.append(["\t".join(t[3:6]+["{:+}".format(int(t[6])-int(t[0])) if int(t[6]) else "0",t[7]]) for t in s])
lid={l:i for i,l in enumerate(set(sum(tag,[])))}
tkz=AutoTokenizer.from_pretrained(brt)
dts=Dataset.from_dict({"tokens":tok,"tags":tag,"input_ids":[tkz.convert_tokens_to_ids(s) for s in tok],"labels":[[lid[t] for t in s] for s in tag]})
cfg=AutoConfig.from_pretrained(brt,num_labels=len(lid),label2id=lid,id2label={i:l for l,i in lid.items()})
mdl=AutoModelForTokenClassification.from_pretrained(brt,config=cfg)
dcl=DataCollatorForTokenClassification(tokenizer=tkz)
arg=TrainingArguments(output_dir="/tmp",overwrite_output_dir=True,per_device_train_batch_size=4,save_total_limit=2)
trn=Trainer(model=mdl,args=arg,data_collator=dcl,train_dataset=dts)
trn.train()
trn.save_model("bert-large-japanese-char-ud-goeswith")
tkz.save_pretrained("bert-large-japanese-char-ud-goeswith")
GPU版なら45分程度で、bert-large-japanese-char-ud-goeswithモデルが出来上がるはずだ。「昨日は誰も来なかった」を係り受け解析してみよう。
!pip install transformers deplacy
import torch
from transformers import AutoTokenizer,AutoModelForTokenClassification
from unicodedata import category
tokenizer=AutoTokenizer.from_pretrained("bert-large-japanese-char-ud-goeswith")
model=AutoModelForTokenClassification.from_pretrained("bert-large-japanese-char-ud-goeswith")
def nlp(sentence):
s=[t for t in sentence if category(t)!="Zs"]
m=[i-j-1 for j,i in enumerate([i for i,t in enumerate(sentence) if category(t)=="Zs"])]
e=tokenizer.encode(s,return_tensors="pt",add_special_tokens=False)
for i,q in enumerate(torch.argmax(model(e)[0],dim=2)[0].tolist()):
t=model.config.id2label[q].split("\t")
t[3]=str(int(t[3])+i+1) if int(t[3]) else "0"
s[i]=[s[i],"_"]+t+["_","_" if i in m else "SpaceAfter=No"]
for i in [i for i in range(len(s)-1,0,-1) if s[i][6]=="goeswith"]:
t=s.pop(i)
s[i-1][0]+=t[0]
s[i-1][8]=t[8]
for t in [t for t in s if int(t[5])>i]:
t[5]=str(int(t[5])-1)
return "\n".join("\t".join([str(i+1)]+t) for i,t in enumerate(s))+"\n\n"
doc=nlp("昨日は誰も来なかった")
import deplacy
deplacy.render(doc,Japanese=True)
deplacy.serve(doc,port=None)
私(安岡孝一)の手元では、以下の結果になった。
昨日 NOUN ═╗<════╗ obl(斜格補語)
は ADP <╝ ║ case(格表示)
誰 PRON ═╗<══╗ ║ nsubj(主語)
も ADP <╝ ║ ║ case(格表示)
来 VERB ═╗═╗═╝═╝ root(親)
なかっ AUX <╝ ║ aux(動詞補助成分)
た AUX <══╝ aux(動詞補助成分)
1 昨日 _ NOUN 名詞-普通名詞-副詞可能 _ 5 obl _ SpaceAfter=No
2 は _ ADP 助詞-係助詞 _ 1 case _ SpaceAfter=No
3 誰 _ PRON 代名詞 _ 5 nsubj _ SpaceAfter=No
4 も _ ADP 助詞-係助詞 _ 3 case _ SpaceAfter=No
5 来 _ VERB 動詞-非自立可能 _ 0 root _ SpaceAfter=No
6 なかっ _ AUX 助動詞 Polarity=Neg 5 aux _ SpaceAfter=No
7 た _ AUX 助動詞 _ 5 aux _ SpaceAfter=No
SVGで可視化すると、こんな感じ。goeswithを削り取る前の状態も見てみよう。
!pip install transformers deplacy
import torch
from transformers import AutoTokenizer,AutoModelForTokenClassification
from unicodedata import category
tokenizer=AutoTokenizer.from_pretrained("bert-large-japanese-char-ud-goeswith")
model=AutoModelForTokenClassification.from_pretrained("bert-large-japanese-char-ud-goeswith")
def nlp(sentence):
s=[t for t in sentence if category(t)!="Zs"]
m=[i-j-1 for j,i in enumerate([i for i,t in enumerate(sentence) if category(t)=="Zs"])]
e=tokenizer.encode(s,return_tensors="pt",add_special_tokens=False)
for i,q in enumerate(torch.argmax(model(e)[0],dim=2)[0].tolist()):
t=model.config.id2label[q].split("\t")
t[3]=str(int(t[3])+i+1) if int(t[3]) else "0"
s[i]=[s[i],"_"]+t+["_","_" if i in m else "SpaceAfter=No"]
return "\n".join("\t".join([str(i+1)]+t) for i,t in enumerate(s))+"\n\n"
doc=nlp("昨日は誰も来なかった")
import deplacy
deplacy.render(doc,Japanese=True)
deplacy.serve(doc,port=None)
私の手元では、goeswithを削り取る前は以下の状態だった。
昨 NOUN ═╗═╗<══════╗ obl(斜格補語)
日 X <╝ ║ ║ goeswith(泣き別れ)
は ADP <══╝ ║ case(格表示)
誰 PRON ═╗<══════╗ ║ nsubj(主語)
も ADP <╝ ║ ║ case(格表示)
来 VERB ═════╗═╗═╝═╝ root(親)
な AUX ═╗═╗<╝ ║ aux(動詞補助成分)
か X <╝ ║ ║ goeswith(泣き別れ)
っ X <══╝ ║ goeswith(泣き別れ)
た AUX <══════╝ aux(動詞補助成分)
1 昨 _ NOUN 名詞-普通名詞-副詞可能 _ 6 obl _ SpaceAfter=No
2 日 _ X _ _ 1 goeswith _ SpaceAfter=No
3 は _ ADP 助詞-係助詞 _ 1 case _ SpaceAfter=No
4 誰 _ PRON 代名詞 _ 6 nsubj _ SpaceAfter=No
5 も _ ADP 助詞-係助詞 _ 4 case _ SpaceAfter=No
6 来 _ VERB 動詞-非自立可能 _ 0 root _ SpaceAfter=No
7 な _ AUX 助動詞 Polarity=Neg 6 aux _ SpaceAfter=No
8 か _ X _ _ 7 goeswith _ SpaceAfter=No
9 っ _ X _ _ 7 goeswith _ SpaceAfter=No
10 た _ AUX 助動詞 _ 6 aux _ SpaceAfter=No
形態素解析器(MeCabやfugashi)に頼らず、系列ラベリングだけで組み上げたにしては、まずまずの結果だ。さて、6月22日発表予定の『世界のUniversal Dependenciesと係り受け解析ツール群』に、このアイデアも使えるかな。
単語内のリンクにgoeswithを用いた日本語係り受け解析器の製作 More ログイン