パスワードを忘れた? アカウント作成

yasuokaさんのトモダチの日記みんなの日記も見てね。 スラドのストーリを選ぶための補助をお願いします。

13953573 journal
人工知能

yasuokaの日記: GiNZA v2.0.0で読む『吾輩は猫である』

日記 by yasuoka

日本語係り受け解析エンジンGiNZAのv2.0.0がリリースされたので、さっそく使ってみることにした。まずはインストール。

% pip3 install https://github.com/megagonlabs/ginza/releases/download/latest/ginza-latest.tar.gz

インストールがうまくいったら、5月20日の日記と同様、言語処理100本ノック2015の『吾輩は猫である』から、「ヴァイオリンを始める」文をGiNZAで探してみよう。

% python3
>>> import spacy,urllib.request
>>> ja=spacy.load("ja_ginza")
>>> with urllib.request.urlopen("http://www.cl.ecei.tohoku.ac.jp/nlp100/data/neko.txt") as r:
...   q=r.read()
...
>>> u=ja(q.decode("utf-8"))
>>> n={}
>>> for c in u.noun_chunks:
...   for i in range(c.start,c.end):
...     n[i]="NP_B" if i==c.start else "NP_I"
...
>>> from ginza.command_line import token_line
>>> for s in u.sents:
...   f=False
...   for w in s:
...     if w.lemma_=="バイオリン" and w.dep_=="obj":
...       if w.head.lemma_=="始める":
...         f=True
...   if f:
...     print("".join(token_line(w,n)+"\n" for w in s))
...
180931    「    「    PUNCT    補助記号-括弧開    _    180932    punct    _    BunsetuBILabel=B|BunsetuPositionType=CONT|SpaceAfter=No
180932    君    君    PRON    代名詞    _    180938    nsubj    _    BunsetuBILabel=I|BunsetuPositionType=SEM_HEAD|SpaceAfter=No|NP_B
180933    は    は    ADP    助詞-係助詞    _    180932    case    _    BunsetuBILabel=I|BunsetuPositionType=SYN_HEAD|SpaceAfter=No
180934    ヴァイオリン    バイオリン    NOUN    名詞-普通名詞-一般    _    180938    obj    _    BunsetuBILabel=B|BunsetuPositionType=SEM_HEAD|SpaceAfter=No|NP_B
180935    を    を    ADP    助詞-格助詞    _    180934    case    _    BunsetuBILabel=I|BunsetuPositionType=SYN_HEAD|SpaceAfter=No
180936    いつ頃    何時頃    NOUN    名詞-普通名詞-副詞可能    _    180938    nmod    _    BunsetuBILabel=B|BunsetuPositionType=SEM_HEAD|SpaceAfter=No|NP_B
180937    から    から    ADP    助詞-格助詞    _    180936    case    _    BunsetuBILabel=I|BunsetuPositionType=SYN_HEAD|SpaceAfter=No
180938    始め    始める    VERB    動詞-非自立可能    _    0    root    _    BunsetuBILabel=B|BunsetuPositionType=ROOT|SpaceAfter=No
180939    た    た    AUX    助動詞    _    180938    aux    _    BunsetuBILabel=I|BunsetuPositionType=FUNC|SpaceAfter=No
180940    の    の    SCONJ    助詞-準体助詞    _    180938    mark    _    BunsetuBILabel=I|BunsetuPositionType=FUNC|SpaceAfter=No
180941    か    か    PART    助詞-終助詞    _    180938    aux    _    BunsetuBILabel=I|BunsetuPositionType=FUNC|SpaceAfter=No
180942    い    い    PART    助詞-終助詞    _    180938    aux    _    BunsetuBILabel=I|BunsetuPositionType=SYN_HEAD|SpaceAfter=No
180943    。    。    PUNCT    補助記号-句点    _    180938    punct    _    BunsetuBILabel=I|BunsetuPositionType=CONT

IDを付け替えて「UDPipe Visualizer with Immediate Catena Tree」で可視化すると、 こんな感じ。「ヴァイオリンを始める」がうまく検索できていて、rootも正しく刺さっているようだ。素晴らしい。さて、このスゴイ技術を、漢文とか旧字旧かなとかに、うまく応用できないかしら。

13951200 journal
人工知能

yasuokaの日記: 「知を致すは物に格るに在り」の「に」はcliticかaffixか

日記 by yasuoka

一昨日のChamame2UD.pyを使って、漢文の読み下し文を係り受け解析していたところ、「知を致すは物に格るに在り」がうまく解析できないことに気づいた。

% python -i Chamame2UD.py
>>> qkana=Chamame2UD("qkana")
>>> s=qkana("知を致すは物に格るに在り")
>>> s.editor()
>>> print(s)
1    知    知    NOUN    名詞-普通名詞-一般    _    3    obj    _    SpaceAfter=No
2    を    を    ADP    助詞-格助詞    _    1    case    _    SpaceAfter=No
3    致す    致す    VERB    動詞-非自立可能    _    10    advcl    _    SpaceAfter=No
4    は    は    ADP    助詞-係助詞    _    3    case    _    SpaceAfter=No
5    物    物    NOUN    名詞-普通名詞-サ変可能    _    10    iobj    _    SpaceAfter=No
6    に    に    ADP    助詞-格助詞    _    5    case    _    SpaceAfter=No
7    格    格    NOUN    名詞-普通名詞-一般    _    10    iobj    _    SpaceAfter=No
8    る    り    AUX    助動詞    _    7    aux    _    SpaceAfter=No
9    に    に    ADP    助詞-格助詞    _    7    case    _    SpaceAfter=No
10    在り    有る    VERB    動詞-非自立可能    _    0    root    _    SpaceAfter=No

「CoNLL-U SVG Editor」で見ると、こんな感じ。どうやら、「格る」という動詞を旧仮名口語UniDicは知らないようだ。とりあえず、私(安岡孝一)なりに、このUniversal Dependenciesを手作業で修正しようとしたのだが、修正するにしても「短単位」を採用すべきか、「Syntactic Word」を採用すべきかで、迷ってしまった。「短単位」を採用するなら、「格」と「る」をまとめてVERBとみなし、

1    知    知    NOUN    名詞-普通名詞-一般    _    3    obj    _    SpaceAfter=No
2    を    を    ADP    助詞-格助詞    _    1    case    _    SpaceAfter=No
3    致す    致す    VERB    動詞-非自立可能    _    9    csubj    _    SpaceAfter=No
4    は    は    ADP    助詞-係助詞    _    3    case    _    SpaceAfter=No
5    物    物    NOUN    名詞-普通名詞-サ変可能    _    7    obj    _    SpaceAfter=No
6    に    に    ADP    助詞-格助詞    _    5    case    _    SpaceAfter=No
7    格る    格る    VERB    動詞-一般    _    9    advcl    _    SpaceAfter=No|Translit=イタル
8    に    に    ADP    助詞-格助詞    _    7    case    _    SpaceAfter=No
9    在り    有る    VERB    動詞-非自立可能    _    0    root    _    SpaceAfter=No

とすることになる。一方、「Syntactic Word」を採用するなら、「格るに」をひとまとめにするかどうかが議論の対象となる。この「に」をcliticと見るなら「短単位」と同じになるが、affixと見るなら以下のようにせざるを得ない。

1    知    知    NOUN    名詞-普通名詞-一般    _    3    obj    _    SpaceAfter=No
2    を    を    ADP    助詞-格助詞    _    1    case    _    SpaceAfter=No
3    致す    致す    VERB    動詞-非自立可能    _    8    csubj    _    SpaceAfter=No
4    は    は    ADP    助詞-係助詞    _    3    case    _    SpaceAfter=No
5    物    物    NOUN    名詞-普通名詞-サ変可能    _    7    obj    _    SpaceAfter=No
6    に    に    ADP    助詞-格助詞    _    5    case    _    SpaceAfter=No
7    格るに    格る    VERB    動詞-一般    _    8    advcl    _    SpaceAfter=No|Translit=イタルニ
8    在り    有る    VERB    動詞-非自立可能    _    0    root    _    SpaceAfter=No

でも、現時点の私には、この「に」がcliticなのかaffixなのか、ハッキリとは断言できない。6月27日の日記のアイデアも、今のところはCotoha2UD.py限定実装なので、旧字旧かなの読み下し文には適用できなさそうだ。さて、どうしたらいいかな。

13949759 journal
人工知能

yasuokaの日記: Web茶まめとUDPipe APIの両方にアクセスできるpythonラッパー

日記 by yasuoka

一昨日の日記で書いた「Chamame2UD.py」を、6月6日の日記で書いた「UDPipe2UD.py」と統合して、Web茶まめUDPipe APIの両方にアクセスできるpythonラッパーを書いてみた。python3のみならず、python2.7にも対応したので、さらに読みにくくなってしまったが、とりあえず公開する。

#! /usr/bin/python -i
# coding=utf-8
# "Chamame2UD.py" by 安岡孝一, July 3, 2019.

class UDPipeEntry(object):
  def __init__(self,result):
    if "\n" in result:
      t=[UDPipeEntry("0\t_\t_\t_\t_\t_\t0\t_\t_\t_")]
      for r in result.split("\n"):
        w=UDPipeEntry(r)
        if w.id>0:
          t.append(w)
      self._tokens=t
      for w in t:
        w._parent=self
        w.head=w._head
      self._result=result
    else:
      w=result.split("\t")
      try:
        w[0],w[6]=int(w[0]),int(w[6])
      except:
        w=[0]*10
      self.id,self.form,self.lemma,self.upos,self.xpos,self.feats,self._head,self.deprel,self.deps,self.misc=w if len(w)==10 else [0]*10
      self._result=""
  def __setattr__(self,name,value):
    v=value
    if name=="head":
      t=self._parent._tokens
      i=t.index(self)
      v=self if v==0 else t[i+v-self.id]
    if hasattr(self,name):
      if getattr(self,name)!=v:
        super(UDPipeEntry,self._parent).__setattr__("_result","")
        if name=="id":
          t=self._parent._tokens
          i=t.index(self)
          j=i+v-self.id
          super(UDPipeEntry,t[j]).__setattr__("id",t[i].id)
          t[i],t[j]=t[j],t[i]
    super(UDPipeEntry,self).__setattr__(name,v)
  def __repr__(self):
    if self._result!="":
      r=self._result
    elif hasattr(self,"_tokens"):
      r="".join(str(t)+"\n" for t in self._tokens[1:]).replace("\n1\t","\n\n1\t")
    else:
      r="\t".join([str(self.id),self.form,self.lemma,self.upos,self.xpos,self.feats,str(0 if self.head is self else self.head.id),self.deprel,self.deps,self.misc])
    return r if type(r) is str else r.encode("utf-8")
  def __getitem__(self,item):
    return self._tokens[item]
  def __len__(self):
    return len(self._tokens)
  def browse(self):
    self.editor(url="http://kanji.zinbun.kyoto-u.ac.jp/~yasuoka/kyodokenkyu/ud-kanbun/conllusvg/viewer.svg")
  def editor(self,url="http://kanji.zinbun.kyoto-u.ac.jp/~yasuoka/kyodokenkyu/ud-kanbun/conllusvg/editor.html"):
    import webbrowser
    try:
      import urllib.parse
      u=urllib.parse.quote(str(self))
    except:
      import urllib
      u=urllib.quote(str(self))
    webbrowser.open(url+"#"+u)

class UDPipe2UD(object):
  def __init__(self,lang="ja",option="tokenizer=presegmented&tagger&parser"):
    self.parseURL="http://lindat.mff.cuni.cz/services/udpipe/api/process?model="+lang+"&"+option
  def __call__(self,sentence):
    import json
    s=sentence if type(sentence) is str else sentence.encode("utf-8")
    try:
      import urllib.request,urllib.parse
      with urllib.request.urlopen(self.parseURL+"&data="+urllib.parse.quote(s)) as r:
        q=r.read()
    except:
      import urllib,urllib2
      r=urllib2.urlopen(self.parseURL+"&data="+urllib.quote(s))
      q=r.read().decode("utf-8")
    return UDPipeEntry(json.loads(q)["result"])

class Chamame2UD(object):
  def __init__(self,dict="gendai"):
    self.chamURL="https://unidic.ninjal.ac.jp/chamame/chamamebin/webchamame.php"
    self.udpipe=UDPipe2UD(lang="ja",option="parser")
    d={ "gendai":"dic1", "spoken":"dic2", "qkana":"dic3", "kindai":"dic4", "kinsei":"dic5", "kyogen":"dic6", "wakan":"dic7", "wabun":"dic8", "manyo":"dic9" }
    self.dictkey=d[dict]
    self.dict="UniDic-"+dict
  def __call__(self,sentence):
    import random,json
    s=sentence if type(sentence) is str else sentence.encode("utf-8")
    f={ self.dictkey:self.dict, "st":s+"\n\n", "f1":"1", "f3":"1", "out-e":"csv", "c-code":"utf-8" }
    b="".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for i in range(10))
    d="\n".join("--"+b+"\nContent-Disposition:form-data;name="+k+"\n\n"+v for k,v in f.items())+"\n--"+b+"--\n"
    h={ "Content-Type":"multipart/form-data;charset=utf-8;boundary="+b }
    try:
      import urllib.request
      u=urllib.request.Request(self.chamURL,d.encode(),h)
      with urllib.request.urlopen(u) as r:
        q=r.read()
    except:
      import urllib2
      u=urllib2.Request(self.chamURL,d.decode("utf-8").encode("utf-8"),h)
      r=urllib2.urlopen(u)
      q=r.read()
    f={ u"接頭辞":"NOUN", u"代名詞":"PRON", u"連体詞":"DET", u"動詞":"VERB", u"形容詞":"ADJ", u"形状詞":"ADJ", u"副詞":"ADV", u"感動詞":"INTJ", u"助動詞":"AUX", u"接続詞":"CCONJ", u"補助記号":"PUNCT", u"記号":"SYM", u"空白":"SYM" }
    t=""
    for s in q.decode("utf-8").replace("\r","").split("\n"):
      w=s.split(",")
      if len(w)<5:
        continue
      elif w[1]=="B":
        n=1
        t+="\n"
      elif w[1]=="I":
        n+=1
      else:
        continue
      u="X"
      x=(w[4]+u"-").replace(u" ","").split(u"-")
      if x[0]==u"名詞":
        u="PROPN" if x[1]==u"固有名詞" else "NUM" if x[1]==u"数詞" else "NOUN"
      elif x[0]==u"助詞":
        u="ADP"
        if x[1]==u"接続助詞":
          u="SCONJ" if w[3]==u"て" else "CCONJ"
        else:
          u="PART" if x[1]==u"終助詞" else "ADP"
      elif x[0]==u"接尾辞":
        u="NOUN" if x[1]==u"名詞的" else "PART"
      elif x[0] in f:
        u=f[x[0]]
      t+="\t".join([str(n),w[2],w[2] if w[3]=="" else w[3],u,w[4],"_","_","_","_","SpaceAfter=No"])+"\n"
    return self.udpipe(t)

上のプログラムを「Chamame2UD.py」に保存したら、「勞心者治人」と「心を勞する者は人を治める」の両方を、係り受け解析してみよう。

% python -i Chamame2UD.py
>>> lzh=UDPipe2UD(lang="lzh")
>>> qkana=Chamame2UD(dict="qkana")
>>> s1=lzh("勞心者治人")
>>> s2=qkana("心を勞する者は人を治める")
>>> print(str(s1)+str(s2))
# newdoc
# newpar
# sent_id = 1
# text = 勞心者治人
1    勞    勞    VERB    v,動詞,描写,境遇    Degree=Pos    3    acl    _    SpaceAfter=No
2    心    心    NOUN    n,名詞,不可譲,身体    _    1    obj    _    SpaceAfter=No
3    者    者    PART    p,助詞,提示,*    _    4    nsubj    _    SpaceAfter=No
4    治    治    VERB    v,動詞,行為,動作    _    0    root    _    SpaceAfter=No
5    人    人    NOUN    n,名詞,人,人    _    4    obj    _    SpaceAfter=No

1    心    心    NOUN    名詞-普通名詞-サ変可能    _    3    obj    _    SpaceAfter=No
2    を    を    ADP    助詞-格助詞    _    1    case    _    SpaceAfter=No
3    勞する    労する    VERB    動詞-一般    _    4    acl    _    SpaceAfter=No
4    者    者    NOUN    名詞-普通名詞-一般    _    8    nsubj    _    SpaceAfter=No
5    は    は    ADP    助詞-係助詞    _    4    case    _    SpaceAfter=No
6    人    人    NOUN    名詞-普通名詞-一般    _    8    obj    _    SpaceAfter=No
7    を    を    ADP    助詞-格助詞    _    6    case    _    SpaceAfter=No
8    治める    収める    VERB    動詞-一般    _    0    root    _    SpaceAfter=No

上の例では、「勞心者治人」の解析にUDPipeの古典中国語モデルclassical_chinese-kyoto-ud-2.4を、「心を勞する者は人を治める」の解析に旧仮名口語UniDicとUDPipeの日本語モデルjapanese-gsd-ud-2.4を、それぞれ使用している。これらのUniversal Dependenciesを両方合わせて、SVGで可視化するには、上の手順に続いて以下を実行すればOKだ。

>>> t=UDPipeEntry(str(s1)+str(s2))
>>> t.browse()

うまくいけば、こんな感じのブラウザが立ち上がってくる。これで、古典中国語(漢文)と旧かな日本語(読み下し文)の平行コーパスを作る準備ができたのだけど、さて、そういうのが必要な人たちって、世の中にいるのかしら。

13947661 journal
人工知能

yasuokaの日記: Web茶まめとUDPipeの組み合わせによる旧字旧かな係り受け解析の改良

日記 by yasuoka

昨日の日記で書いた「Chamame2UD.py」だが、接尾辞を処理し忘れていたのに気づいたので、ざっと書き直してみた。ついでにpython2.7にも対応したので、かなり読みにくくなってしまった。旧字旧かなで書かれた文を係り受け解析したい人々が、どのくらい読者にいるのか不明なのだが、それでも公開することにする。

#! /usr/bin/python -i
# coding=utf-8
# "Chamame2UD.py" by 安岡孝一, July 1, 2019.

class ChamameEntry:
  def __init__(self,result):
    self.result=result
    if "\n" in result:
      t=[]
      for r in result.split("\n"):
        w=ChamameEntry(r)
        if w.id>0:
          t.append(w)
      for i,w in enumerate(t):
        w.head=w if w.head==0 else t[i+w.head-w.id]
      self.tokens=t
    else:
      w=result.split("\t")
      try:
        w[0],w[6]=int(w[0]),int(w[6])
      except:
        w=[0]*10
      self.id,self.form,self.lemma,self.upos,self.xpos,self.feats,self.head,self.deprel,self.deps,self.misc=w if len(w)==10 else [0]*10
  def __repr__(self):
    if type(self.result) is str:
      return self.result
    return self.result.encode("utf-8")
  def __getitem__(self,item):
    return self.tokens[item]
  def __len__(self):
    return len(self.tokens)
  def browse(self):
    self.editor(url="http://kanji.zinbun.kyoto-u.ac.jp/~yasuoka/kyodokenkyu/ud-kanbun/conllusvg/viewer.svg")
  def editor(self,url="http://kanji.zinbun.kyoto-u.ac.jp/~yasuoka/kyodokenkyu/ud-kanbun/conllusvg/editor.html"):
    import webbrowser
    try:
      import urllib.parse
      u=urllib.parse.quote(str(self))
    except:
      import urllib
      u=urllib.quote(str(self))
    webbrowser.open(url+"#"+u)

class Chamame2UD:
  def __init__(self,dict="gendai"):
    self.chamURL="https://unidic.ninjal.ac.jp/chamame/chamamebin/webchamame.php"
    self.parseURL="http://lindat.mff.cuni.cz/services/udpipe/api/process"
    d={ "gendai":"dic1", "spoken":"dic2", "qkana":"dic3", "kindai":"dic4", "kinsei":"dic5", "kyogen":"dic6", "wakan":"dic7", "wabun":"dic8", "manyo":"dic9" }
    self.dictkey=d[dict]
    self.dict="UniDic-"+dict
  def __call__(self,sentence):
    import random,json
    s=sentence if type(sentence) is str else sentence.encode("utf-8")
    f={ self.dictkey:self.dict, "st":s+"\n\n", "f1":"1", "f3":"1", "out-e":"csv", "c-code":"utf-8" }
    b="".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for i in range(10))
    d="\n".join("--"+b+"\nContent-Disposition:form-data;name="+k+"\n\n"+v for k,v in f.items())+"\n--"+b+"--\n"
    h={ "Content-Type":"multipart/form-data;charset=utf-8;boundary="+b }
    try:
      import urllib.request
      u=urllib.request.Request(self.chamURL,d.encode(),h)
      with urllib.request.urlopen(u) as r:
        q=r.read()
    except:
      import urllib2
      u=urllib2.Request(self.chamURL,d.decode("utf-8").encode("utf-8"),h)
      r=urllib2.urlopen(u)
      q=r.read()
    f={ u"接頭辞":"NOUN", u"代名詞":"PRON", u"連体詞":"PRON", u"動詞":"VERB", u"形容詞":"ADJ", u"形状詞":"ADJ", u"副詞":"ADV", u"感動詞":"INTJ", u"助動詞":"AUX", u"接続詞":"CCONJ", u"補助記号":"PUNCT", u"記号":"SYM", u"空白":"SYM" }
    t=""
    for s in q.decode("utf-8").replace("\r","").split("\n"):
      w=s.split(",")
      if(len(w)<5):
        continue
      elif(w[1]=="B"):
        n=1
        t+="\n"
      elif(w[1]=="I"):
        n+=1
      else:
        continue
      u="X"
      x=(w[4]+u"-").replace(u" ","").split(u"-")
      if x[0]==u"名詞":
        u="PROPN" if x[1]==u"固有名詞" else "NUM" if x[1]==u"数詞" else "NOUN"
      elif x[0]==u"助詞":
        u="ADP"
        if x[1]==u"接続助詞":
          u="SCONJ" if w[3]==u"て" else "CCONJ"
        else:
          u="PART" if x[1]==u"終助詞" else "ADP"
      elif x[0]==u"接尾辞":
        u="NOUN" if x[1]==u"名詞的" else "PART"
      elif x[0] in f:
        u=f[x[0]]
      t+="\t".join([str(n),w[2],w[2] if w[3]=="" else w[3],u,w[4],"_","_","_","_","SpaceAfter=No"])+"\n"
    try:
      import urllib.parse
      with urllib.request.urlopen(self.parseURL+"?model=ja&parser&data="+urllib.parse.quote(t)) as r:
        q=r.read()
    except:
      import urllib
      r=urllib2.urlopen(self.parseURL+"?model=ja&parser&data="+urllib.quote(t.encode("utf-8")))
      q=r.read().decode("utf-8")
    return(ChamameEntry(json.loads(q)["result"]))

上のプログラムを「Chamame2UD.py」に保存したら、「澤山居つた兄弟が一疋も見えぬ」を係り受け解析してみよう。

% python -i Chamame2UD.py
>>> qkana=Chamame2UD("qkana")
>>> s=qkana("澤山居つた兄弟が一疋も見えぬ")
>>> s.browse()
>>> print(s)
1    澤山    沢山    ADV    副詞    _    2    advmod    _    SpaceAfter=No
2    居つ    居る    VERB    動詞-非自立可能    _    4    acl    _    SpaceAfter=No
3    た    た    AUX    助動詞    _    2    aux    _    SpaceAfter=No
4    兄弟    兄弟    NOUN    名詞-普通名詞-一般    _    9    nsubj    _    SpaceAfter=No
5    が    が    ADP    助詞-格助詞    _    4    case    _    SpaceAfter=No
6    一    一    NUM    名詞-数詞    _    7    compound    _    SpaceAfter=No
7    疋    匹    NOUN    接尾辞-名詞的-助数詞    _    9    obl    _    SpaceAfter=No
8    も    も    ADP    助詞-係助詞    _    7    case    _    SpaceAfter=No
9    見え    見える    VERB    動詞-一般    _    0    root    _    SpaceAfter=No
10    ぬ    ず    AUX    助動詞    _    9    aux    _    SpaceAfter=No

うまく行けば、こんな感じのブラウザが立ち上がってきて、↑のUniversal Dependenciesが出力される。ちなみに、この「Chamame2UD.py」で使えるUniDicは、以下の通り。

それぞれに癖のあるUniDicだが、UPOSをうまく合わせることで、UDPipeの日本語モデルjapanese-gsd-ud-2.4の係り受け解析に、何とか載ってくれるようである。よければ、色々な文で試してみてほしい。

13946844 journal
人工知能

yasuokaの日記: Web茶まめとUDPipeの組み合わせによる旧字旧かな係り受け解析 1

日記 by yasuoka

旧字旧かなで書かれた文を係り受け解析すべく、旧仮名口語UniDicと、UDPipeの日本語モデルを組み合わせてみることにした。具体的には、国立国語研究所のWeb茶まめで形態素解析をおこなったのち、LINDAT/CLARINのUDPipe APIで依存文法解析をおこなうpython3ラッパー「Chamame2UD.py」を書いてみた。

#! /usr/bin/python3 -i
# "Chamame2UD.py" by 安岡孝一, June 30, 2019.

class ChamameEntry:
  def __init__(self,result):
    self.result=result
    if "\n" in result:
      t=[]
      for r in result.split("\n"):
        w=ChamameEntry(r)
        if w.id>0:
          t.append(w)
      for i,w in enumerate(t):
        w.head=w if w.head==0 else t[i+w.head-w.id]
      self.tokens=t
    else:
      w=result.split("\t")
      try:
        w[0],w[6]=int(w[0]),int(w[6])
      except:
        w=[0]*10
      self.id,self.form,self.lemma,self.upos,self.xpos,self.feats,self.head,self.deprel,self.deps,self.misc=w if len(w)==10 else [0]*10
  def __repr__(self):
    if type(self.result) is str:
      return self.result
    return self.result.encode('utf-8')
  def __getitem__(self,item):
    return self.tokens[item]
  def __len__(self):
    return len(self.tokens)
  def browse(self):
    self.editor(url="http://kanji.zinbun.kyoto-u.ac.jp/~yasuoka/kyodokenkyu/ud-kanbun/conllusvg/viewer.svg")
  def editor(self,url="http://kanji.zinbun.kyoto-u.ac.jp/~yasuoka/kyodokenkyu/ud-kanbun/conllusvg/editor.html"):
    import webbrowser,urllib.parse
    u=urllib.parse.quote(str(self))
    webbrowser.open(url+"#"+u)

class Chamame2UD:
  def __init__(self,dict="gendai"):
    self.chamURL="https://unidic.ninjal.ac.jp/chamame/chamamebin/webchamame.php"
    self.parseURL="http://lindat.mff.cuni.cz/services/udpipe/api/process"
    d={ "gendai":"dic1", "spoken":"dic2", "qkana":"dic3", "kindai":"dic4", "kinsei":"dic5", "kyogen":"dic6", "wakan":"dic7", "wabun":"dic8", "manyo":"dic9" }
    self.dictkey=d[dict]
    self.dict="UniDic-"+dict
  def __call__(self,sentence):
    import random,urllib.request,urllib.parse,json
    f={ self.dictkey:self.dict, "st":sentence+"\n\n", "f1":"1", "f3":"1", "out-e":"csv", "c-code":"utf-8" }
    b="".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for i in range(10))
    d="\n".join("--"+b+"\nContent-Disposition:form-data;name="+k+"\n\n"+v for k,v in f.items())+"\n--"+b+"--\n"
    h={ "Content-Type":"multipart/form-data;charset=utf-8;boundary="+b }
    u=urllib.request.Request(self.chamURL,d.encode(),h)
    with urllib.request.urlopen(u) as r:
      q=r.read()
    f={ "接頭辞":"NOUN", "代名詞":"PRON", "連体詞":"PRON", "動詞":"VERB", "形容詞":"ADJ", "形状詞":"ADJ", "副詞":"ADV", "感動詞":"INTJ", "助動詞":"AUX", "接続詞":"CCONJ", "補助記号":"PUNCT", "記号":"SYM" }
    t=""
    for s in q.decode("utf-8").replace("\r","").split("\n"):
      w=s.split(",")
      if(len(w)<5):
        continue
      elif(w[1]=="B"):
        n=1
        t+="\n"
      elif(w[1]=="I"):
        n+=1
      else:
        continue
      u="X"
      x=(w[4]+"-").replace(" ","").split("-")
      if x[0]=="名詞":
        u="PROPN" if x[1]=="固有名詞" else "NUM" if x[1]=="数詞" else "NOUN"
      elif x[0]=="助詞":
        if x[1]=="接続助詞":
          u="SCONJ" if w[3]=="て" else "CCONJ"
        else:
          u="PART" if x[1]=="終助詞" else "ADP"
      elif x[0] in f:
        u=f[x[0]]
      t+="\t".join([str(n),w[2],w[2] if w[3]=="" else w[3],u,w[4],"_","_","_","_","SpaceAfter=No"])+"\n"
    with urllib.request.urlopen(self.parseURL+"?model=ja&parser&data="+urllib.parse.quote(t)) as r:
      q=r.read()
    return(ChamameEntry(json.loads(q)["result"]))

かなり長いし、まだバグが潜んでる気もするのだが、上のプログラムを「Chamame2UD.py」に保存したら、「喜怒哀樂の未だ發せざる之を中と謂ふ」を係り受け解析してみよう。

% python3 -i Chamame2UD.py
>>> qkana=Chamame2UD("qkana")
>>> s=qkana("喜怒哀樂の未だ發せざる之を中と謂ふ")
>>> s.browse()
>>> print(s)
1    喜怒    喜怒    NOUN    名詞-普通名詞-一般    _    2    compound    _    SpaceAfter=No
2    哀樂    哀楽    NOUN    名詞-普通名詞-一般    _    5    obl    _    SpaceAfter=No
3    の    の    ADP    助詞-格助詞    _    2    case    _    SpaceAfter=No
4    未だ    未だ    ADV    副詞    _    5    advmod    _    SpaceAfter=No
5    發せ    発する    VERB    動詞-一般    _    7    acl    _    SpaceAfter=No
6    ざる    ず    AUX    助動詞    _    5    aux    _    SpaceAfter=No
7    之    此れ    PRON    代名詞    _    11    obj    _    SpaceAfter=No
8    を    を    ADP    助詞-格助詞    _    7    case    _    SpaceAfter=No
9    中    中    NOUN    名詞-普通名詞-副詞可能    _    11    obl    _    SpaceAfter=No
10    と    と    ADP    助詞-格助詞    _    9    case    _    SpaceAfter=No
11    謂ふ    言う    VERB    動詞-一般    _    0    root    _    SpaceAfter=No

うまく行けば、こんな感じのブラウザが立ち上がってきて、↑のUniversal Dependenciesが出力される。形態素解析にqkana(旧仮名口語UniDic)を、依存文法解析にjapanese-gsd-ud-2.4-190531を使っているのだが、まあまあの出来に見える。なお、この「Chamame2UD.py」では、qkanaの代わりにkindaiを指定すれば近代文語UniDicが使えるし、wakan(中世文語UniDic)やwabun(中古和文UniDic)やmanyo(万葉集UniDic)も、たぶん使えるはずだ。ただ、係り受け解析の部分だけは、あくまで現代日本語wikipedia由来なので、そこは注意して使ってみてほしい。

13945493 journal
人工知能

yasuokaの日記: COTOHA APIのUniversal Dependencies向けpythonラッパー

日記 by yasuoka

昨日の日記のアイデアに沿って、COTOHA API構文解析v1のUniversal Dependencies向けpythonラッパー「Cotoha2UD.py」を書き直してみた。python3のみならず、python2.7にも対応したため、かなり長くなってしまった。

#! /usr/bin/python -i
# coding=utf-8
# "Cotoha2UD.py" by 安岡孝一, June 28, 2019.

class CotohaEntry:
  def __init__(self,response,option):
    import json
    self.response=response
    self.result=json.loads(response)["result"]
    self.option=option
    self.tokens=[]
    for b in self.result:
      for w in b["tokens"]:
        w["xpos"]=w["pos"] if w["features"]==[] else w["pos"]+"["+",".join(w["features"])+"]"
        w["feats"]="_"
        w["head"]=w
        w["deprel"]="root"
        w["deps"]="_"
        w["misc"]="SpaceAfter=No"
        self.tokens.insert(w["id"],w)
    for w in self.tokens:
      if "dependency_labels" in w:
        for r in w["dependency_labels"]:
          t=self.tokens[r["token_id"]]
          t["head"]=w
          if r["label"]=="neg":
            t["deprel"]="aux"
            t["feats"]="Polarity=Neg"
          else:
            t["deprel"]=r["label"].replace("dobj","obj").replace("name","flat").replace("pass",":pass")
    p={ u"名詞接尾辞":"NOUN", u"冠名詞":"NOUN", u"補助名詞":"NOUN",
        u"動詞語幹": "VERB",
        u"冠動詞":"ADV", u"冠形容詞":"ADV", u"連用詞":"ADV",
        u"形容詞語幹":"ADJ",
        u"連体詞":"DET",
        u"接続詞":"CCONJ",
        u"独立詞":"INTJ",
        u"括弧":"PUNCT", u"句点":"PUNCT", u"読点":"PUNCT", u"空白":"PUNCT",
        u"Symbol":"SYM",
        u"Number":"NUM" }
    for w in self.tokens:
      if w["pos"]==u"名詞":
        w["upos"]="NOUN"
        if u"代名詞" in w["xpos"] or u"指示" in w["xpos"]:
          w["upos"]="PRON"
        if u"固有" in w["xpos"]:
          w["upos"]="PROPN"
      else:
        w["upos"]=p[w["pos"]] if w["pos"] in p else "PART"
      if w["deprel"]=="case":
        w["upos"]="ADP"
      elif w["deprel"]=="cop" or w["deprel"].startswith("aux"):
        w["upos"]="AUX"
    self.tokens[0]["idx"]=i=1
    for w in self.tokens[1:]:
      if u"接尾辞" in w["xpos"] or u"活用語尾" in w["xpos"]:
        w["idx"]=0
        if u"[連用]" in w["xpos"]:
          w["feats"]=("VerbForm=Part|"+w["feats"]).replace("|_","")
        if u"[終止]" in w["xpos"]:
          w["feats"]=("VerbForm=Fin|"+w["feats"]).replace("|_","")
        if u"[連体]" in w["xpos"]:
          w["feats"]=("VerbForm=Conv|"+w["feats"]).replace("|_","")
      else:
        i+=1
        w["idx"]=i
    for w in self.tokens:
      if w["idx"]>0:
        w["deprelx"]=w["deprel"]
        if w["head"]["idx"]==0:
          i=w["head"]["id"]
          while self.tokens[i]["idx"]==0:
            i-=1
          if self.tokens[i] is w:
            i=w["head"]["id"]
            w["deprelx"]=self.tokens[i]["deprel"]
            w["headx"]=w if w["deprelx"]=="root" else self.tokens[i]["head"]
          else:
            w["headx"]=self.tokens[i]
        else:
          w["headx"]=w["head"]
    i=j=""
    k="_"
    for w in reversed(self.tokens):
      k=w["feats"]+"|"+k
      if w["idx"]>0:
        w["formx"]=w["form"]+i
        w["xposx"]=w["xpos"]+j
        w["featsx"]=k.replace("|_","").replace("_|","")
        i=j=""
        k="_"
      else:
        i=w["form"]+i
        j="+"+w["xpos"]+j
  def __repr__(self):
    if "concatSuffix" in self.option:
      r="".join("\t".join([str(t["idx"]),t["formx"],t["lemma"],t["upos"],t["xposx"],t["featsx"],str(0 if t["headx"] is t else t["headx"]["idx"]),t["deprelx"],t["deps"],t["misc"]])+"\n" if t["idx"]>0 else "" for t in self.tokens)
    else:
      r="".join("\t".join([str(t["id"]+1),t["form"],t["lemma"],t["upos"],t["xpos"],t["feats"],str(0 if t["head"] is t else t["head"]["id"]+1),t["deprel"],t["deps"],t["misc"]])+"\n" for t in self.tokens)
    if type(r) is str:
      return r
    return r.encode("utf-8")
  def editor(self,url="http://kanji.zinbun.kyoto-u.ac.jp/~yasuoka/kyodokenkyu/ud-kanbun/conllusvg/editor.html"):
    import webbrowser
    try:
      import urllib.parse
      u=urllib.parse.quote(str(self))
    except:
      import urllib
      u=urllib.quote(str(self))
    webbrowser.open(url+"#"+u)
  def browse(self):
    self.editor(url="http://kanji.zinbun.kyoto-u.ac.jp/~yasuoka/kyodokenkyu/ud-kanbun/conllusvg/viewer.svg")

class Cotoha2UD:
  def __init__(self,accessToken,option=""):
    self.parseURL="https://api.ce-cotoha.com/api/dev/nlp/v1/parse"
    self.accessToken=accessToken
    self.option=option
  def __call__(self,sentence):
    import json
    h={ "Content-Type":"application/json;charset=UTF-8",
        "Authorization":"Bearer "+self.accessToken }
    d={ "sentence":sentence }
    try:
      import urllib.request
      u=urllib.request.Request(self.parseURL,json.dumps(d).encode(),h)
      with urllib.request.urlopen(u) as r:
        q=r.read()
    except:
      import urllib2
      u=urllib2.Request(self.parseURL,json.dumps(d).encode(),h)
      r=urllib2.urlopen(u)
      q=r.read().decode("utf-8")
    return CotohaEntry(q,self.option)

上のプログラムを「Cotoha2UD.py」に保存したら、ちょっと面倒くさい手順で「アクセストークン」を取得する。そこまでが出来たら、とりあえず「着たくない服は着ない」を、新しいCotoha2UDで係り受け解析してみよう。

% python -i Cotoha2UD.py
>>> ja=Cotoha2UD(accessToken="アクセストークン",option="concatSuffix")
>>> s=ja("着たくない服は着ない")
>>> s.browse()
>>> print(s)
1    着たく    着る    VERB    動詞語幹[A]+動詞接尾辞[形容詞語幹]+形容詞接尾辞[連用]    VerbForm=Part    3    acl    _    SpaceAfter=No
2    ない    ない    AUX    形容詞語幹[アウオ段]+形容詞接尾辞[連体]    Polarity=Neg|VerbForm=Conv    1    aux    _    SpaceAfter=No
3    服    服    NOUN    名詞    _    5    obj    _    SpaceAfter=No
4    は    は    ADP    連用助詞    _    3    case    _    SpaceAfter=No
5    着ない    着る    VERB    動詞語幹[A]+動詞接尾辞[終止]    VerbForm=Fin|Polarity=Neg    0    root    _    SpaceAfter=No

うまく行けば、こんな感じのブラウザが立ち上がってきて、↑のUniversal Dependenciesが出力される。確かに「超短単位」や「短単位」よりは、Universal Dependenciesの「Syntactic Word」に近い気がする。さて、技術的には何とかなりそうなんだけど、日本語Universal Dependenciesが「Syntactic Word」に乗り換えるって、実際のところ可能なのかしら。

13944375 journal
人工知能

yasuokaの日記: 「着たくない服は着ない」の「ない」はcliticかaffixか

日記 by yasuoka

村脇有吾の「On the Definition of Japanese Word」(arXiv、2019年6月24日)を読んでいたところ、ふっとCOTOHA APIに思い至った。COTOHA API構文解析v1は、いわゆる「超短単位」なので、そのままUniversal Dependenciesに変換すると、単語が短くなりすぎるのだ。以前、私(安岡孝一)が作ったCotoha2UD.pyで、「着たくない服は着ない」という文を解析してみよう。

% python3 -i Cotoha2UD.py
>>> ja=Cotoha2UD(accessToken="アクセストークン")
>>> s=ja("着たくない服は着ない")
>>> print(s)
1    着    着る    VERB    動詞語幹[A]    _    6    acl    _    SpaceAfter=No
2    た    たい    AUX    動詞接尾辞[形容詞語幹]    _    1    aux    _    SpaceAfter=No
3    く    く    AUX    形容詞接尾辞[連用]    _    1    aux    _    SpaceAfter=No
4    な    ない    AUX    形容詞語幹[アウオ段]    Polarity=Neg    1    aux    _    SpaceAfter=No
5    い    い    AUX    形容詞接尾辞[連体]    _    1    aux    _    SpaceAfter=No
6    服    服    NOUN    名詞    _    8    obj    _    SpaceAfter=No
7    は    は    ADP    連用助詞    _    6    case    _    SpaceAfter=No
8    着    着る    VERB    動詞語幹[A]    _    0    root    _    SpaceAfter=No
9    ない    ない    AUX    動詞接尾辞[終止]    Polarity=Neg    8    aux    _    SpaceAfter=No

SVGで可視化すると、こんな感じ。接尾辞がバラバラになっているので、Universal Dependenciesとしてはマズイことになっている。これを、上記論文のアイデアに従って、affix(接頭辞とか接尾辞)を周りの単語にくっつけると、たとえば以下のようになる。

1    着たく    着る    VERB    動詞語幹[A]+動詞接尾辞[形容詞語幹]+形容詞接尾辞[連用]    _    3    acl    _    SpaceAfter=No
2    ない    ない    AUX    形容詞語幹[アウオ段]+形容詞接尾辞[連体]    Polarity=Neg    1    aux    _    SpaceAfter=No
3    服    服    NOUN    名詞    _    5    obj    _    SpaceAfter=No
4    は    は    ADP    連用助詞    _    3    case    _    SpaceAfter=No
5    着ない    着る    VERB    動詞語幹[A]+動詞接尾辞[終止]    Polarity=Neg    0    root    _    SpaceAfter=No

「着たくない」の「ない」はclitic(接語)なので、「着たく」と「ない」で分ける。「服は」の「は」もcliticなので、「服」と「は」をくっつけない。一方、「着ない」の「ない」はaffixなので、くっつける。SVGで可視化すると、こんな感じ。ふーむ、接尾辞と活用語尾なら、うまく自動でくっつけられる気がするのだけど、さてそれは、ちゃんとcliticとaffixを見分け切れるかしら。

13943661 journal
中国

yasuokaの日記: 戸籍と在留カードにおける「陳」と「陣」

日記 by yasuoka

『戸籍時報』の今月号を読んでいたところ、堀田百合の「届書及び添付書類に記載されている中国人の氏名の漢字が簡体字で記載されている場合の取扱いについて」(pp.74-78)という記事に出くわした。戸籍における「正字」と、在留カードにおける「正字」が、どう異なっているかを解説した記事で、その点では面白いものだったが、残念ながら「問」の立て方が甘くて、内容としてはイマイチだった。

問 日本人男と中国人女(上海市生まれで現在は日本に在住)の創設的婚姻届が提出されました。届書の氏名欄には「陳家宝」と記載されています。
添付された出生証明書,パスポート,在留カード等(以下「添付書類」という。)に中国人女の氏名が次のように記載されていた場合に日本人男の戸籍の婚姻事項中「配偶者氏名」欄には,どのように記載すべきでしょうか。
(1)「陳家寶」と記載されていた場合
(2)「陳家宝」と記載されていた場合

この2つのケースであれば、話はそんなに難しくない。「上海市生まれ」で実際にヤヤコシイのは、出生証明書やパスポートに以下のような氏名が記載されていた場合だ。

(3)「陈家宝」と記載されていた場合
(4)「阵家宝」と記載されていた場合

記事のタイトルに「中国人の氏名の漢字が簡体字で記載されている場合の取扱い」と謳っているのだから、(3)と(4)は当然、考慮すべきケースのはずだ。というのも、『在留カード等に係る漢字氏名の表記等に関する告示』(平成23年12月26日法務省告示第582号)では、恐ろしいことに「陈」と「阵」を同一視しており、在留カードでは「陳」と「陣」のどちらを選んでもよい。すなわち、出生証明書やパスポートが「阵家宝」なのに、在留カードは「陳家宝」という選択が可能となっているのである。逆に、出生証明書やパスポートが「陈家宝」なのに、在留カードは「陣家宝」というケースもあるわけだ。しかし、堀田百合の記事では、これらのケースを考慮した形跡がない。

(3)' パスポートに「陈家宝」、在留カードに「陣家宝」と記載されていた場合
(4)' パスポートに「阵家宝」、在留カードに「陳家宝」と記載されていた場合

このような場合に、氏名を「陳家宝」とする婚姻届が提出されたら、現場としてはどうするべきなのだろう。私(安岡孝一)個人としては、仕方ないので(3)'(4)'いずれも「陳家宝」のまま受理するしかないと思うのだが、さて、堀田百合の意見はどうなのだろう。

13941283 journal
アメリカ合衆国

yasuokaの日記: タイプライターの名づけ親は誰なのか

日記 by yasuoka

ネットサーフィンしていたところ、講談社ブルーバックスのサイトで「6月23日 世界初のタイプライター(1868年)」(サイエンス365days、2019年6月23日)という記事を見つけた。

この日、アメリカの新聞記者で、発明家のクリストファー・レイサム・ショールズ(Christopher Latham Sholes、1819-1890)が取得した、世界初の実用的なタイプライターの特許が発効されました。

『タイプライターに魅せられた男たち』にも書いたが、クリストファー・レイサム・ショールズ/カルロス・グリデン/サミュエル・ウィラード・ソレーのアメリカ特許No.79265(1868年5月1日署名、1868年6月23日成立)は、さすがに「実用的なタイプライター」にはほど遠い。特許申請書を見ればわかるとおり、キーが21個しかない。しかも実際に製作されたモデルでは、キーを11個に減らして、ようやく特許を成立させたのである(『オフィス機器としてのQWERTYキーボード』写1参照)。アルファベット26字・数字8字(1と0はIとOを流用)・記号4字(ピリオド、コンマ、ハイフン、疑問符)を搭載した38キーの「実用的なタイプライター」は、私(安岡孝一)の知る限り、1870年4月まで遅れることになる。

なお、タイプライターという名前も、ショールズが試作機に命名したものです。

私の調べた限り「The American Type Writer」という名称は、ジェームズ・デンスモアの発案によると考えられる(cf. E. Payson Porter: "Porter's Telegraph College", Saint Joseph Herald, Vol.3, No.29 (1868年11月21日), p.3)。ただし、デンスモアもいきなりこの名称を思いついたわけではなく、ジョン・プラットの「Type Writing Machine」(Scientific American, Vol.17, No.1 (1867年7月6日), p.3)あたりがヒントとなっている可能性が高い。なお、ショールズ自身は、E.レミントン&サンズ社の初期モデルに「Sholes & Glidden Type-Writer」と名づけており(cf. "Ilion", Milwaukee Daily Sentinel, Vol.30, No.142 (1873年6月14日), p.2)、その意味では「試作機に命名」とムリヤリ言えなくもないが、それは上記の特許成立から5年後の話である。

まあ、グリデンもソレーもデンスモアも無視して、ショールズだけに注目したい気持ちはわからなくもないが、少なくともグリデンは、それを許さないと思う(cf. Carlos Glidden: "The New Type Writer", Scientific American, Vol.27, No.9 (1872年8月31日), p.132)。講談社ブルーバックスって、もう2000冊以上でてるはずなんだけど、これまでタイプライターをマトモに扱ったことがないのかしら。

13940582 journal
アメリカ合衆国

yasuokaの日記: Universal Dependenciesの拡張による「They shut the station down」の直接構成鎖解析

日記 by yasuoka

私(安岡孝一)の2月26日の日記で議論した「They shut the station down」だが、どうやらcompound:prt(あるいは全てのcompound)を特別扱いすることで、構成鎖不可分性を自動抽出できそうな気配になってきた。具体的には、以下のようなcatena_inseparability付きUniversal Dependenciesが、抽出できればいいということになる。

# text = They shut the station down
# catena_inseparability = 2<1<4<5<3
1    They    they    PRON    PRP    Case=Nom|Number=Plur|Person=3|PronType=Prs    2    nsubj    _    _
2    shut    shut    VERB    VBD    Mood=Ind|Tense=Past|VerbForm=Fin    0    root    _    _
3    the    the    DET    DT    Definite=Def|PronType=Art    4    det    _    _
4    station    station    NOUN    NN    Number=Sing    2    obj    _    _
5    down    down    ADP    RP    _    2    compound:prt    _    _

「UDPipe Visualizer with Immediate Catena Tree」で可視化してみると、こんな感じ。「shut down」と「the station」の間で、構成鎖解析木の枝が交差してしまうが、それは仕方ないだろう。UDPipeのenglish-gum-ud-2.4-190531モデルを使えば、現状でも何とか自動抽出できそうな気配だが、他のモデルや例文も、もう少し調べてみる必要がありそうだ。さて、どうなるかな。

typodupeerror

身近な人の偉大さは半減する -- あるアレゲ人

読み込み中...