言語処理100本ノック 2015年版 (40~45)
プログラムの量が多くなってきたので2回に分ける。
CaboChaのインストールで一苦労。
さて、何の指示もされていないがCaboChaはテキストをそのまま放り込むとツリー表示になる。このままでは役に立たない。
本問では「係り受け解析結果」を使用するので、-f1 -I0 -O4 オプションをつける。
これがおそらく100本ノックで想定されているneko.txt.cabochaである。
40. 係り受け解析結果の読み込み(形態素)
形態素を表すクラスMorphを実装せよ.このクラスは表層形(surface),基本形(base),品詞(pos),品詞細分類1(pos1)をメンバ変数に持つこととする.さらに,CaboChaの解析結果(neko.txt.cabocha)を読み込み,各文をMorphオブジェクトのリストとして表現し,3文目の形態素列を表示せよ.
とりあえずクラスを実装する。
メンバ変数とは何か?という問題があるが、ここでは問題文からpythonのインスタンス変数とする。
形態素列の表示形式が明示されていないので、メソッドを定義せずに属性値をそのまま取ってくるものとする。
#!/usr/bin/env python import codecs import re import copy class Morph(object): def __init__(self,line): l = re.sub(r"\t",",",line) l = re.split(",",l) self.surface = l[0] self.base = l[7] self.pos = l[1] self.pos1 = l[2] fin = codecs.open('neko.txt.cabocha', 'r', 'utf_8') string = [] src = [] if __name__ == "__main__": n=0 for line in fin: if n==50: break if re.search("EOS",line): if string: src.append(string.copy()) string = [] continue if re.search(r'^\*',line): continue b = Morph(line) string.append(copy.copy(b)) n = n + 1 for x in src[3]: print(x.__dict__)
・何も考えずに append すると同じインスタンスばかり追加されることになる。 copy が重要。
・文末は EOS(= End of String) で判定。
・構造としては src(全文) - string(文) - Morph(形態素)
・形態素列の表示は手を抜いて __dict__ を使用。
41. 係り受け解析結果の読み込み(文節・係り受け)
40に加えて,文節を表すクラスChunkを実装せよ.このクラスは形態素(Morphオブジェクト)のリスト(morphs),係り先文節インデックス番号(dst),係り元文節インデックス番号のリスト(srcs)をメンバ変数に持つこととする.さらに,入力テキストのCaboChaの解析結果を読み込み,1文をChunkオブジェクトのリストとして表現し,8文目の文節の文字列と係り先を表示せよ.第5章の残りの問題では,ここで作ったプログラムを活用せよ.
「文節とは何か」という定義が必要だが。ここではCaboChaがアスタリスク付きで追加した行を文節の開始とする。
40では一文ごとにMorphをリスト化したが、文節ごとにリストし、さらにその文節をリスト化しなければならない。
構造としては src(全文) - string(文) - Chunk(文節) - Morph(形態素) となる。
アスタリスク付きで追加した行はこのようになっている。
* 2 -1D 0/2 0.000000
便宜的にアスタリスクの次から項目1,項目2,項目3,項目4と呼称する。
項目1は文節番号である。この場合では3番目(0からはじまるため)となっている。
項目2は係り先番号である。この場合は-1となっており、係られているだけでどこにも係っていないことを示す。
項目3は主辞と機能語の位置を示している。この場合は文節内の1番目が主辞/3番目が機能語となっている。
項目4は係関係のスコアである。
ということで、使えるのは項目2のみである。個人的には文節番号をメンバ変数に入れておきたいが、番号は位置で分かるため必要とされてないと思われる。
dstはアスタリスク行を分解し、Dを削除すれば手に入る。ここはpythonらしく[:-1]でいきたい。
問題は係り元文節番号である。
これは、係り先番号を保存しておくことでできそう。前に係ることさえなければ。
1
3
3
5
5
6
7
- 1
となっていたとする。これを
[0,1],
[1,3],
[2,3],
[3,5],
[4,5],
[5,6],
[6,7],
[7,-1]
とリスト化する。第2項で第1項を抽出すれば良い。第4文節に係る文節は第2文節と第3文節であることが分かる([1,3],[2,3])。
#!/usr/bin/env python import codecs import re import copy morphlist=[] srcslist=[] #係り元番号回収用 class Chunk(object): def __init__(self,line): l = re.split(" ",line) self.dst = l[2] self.dst = int(self.dst[:-1]) #Dを削除 self.srcs =[] for x in srcslist: #srcslistに自分の文節番号があれば if (x[0] == l[1]): self.srcs.append(x[1]) #係り元番号を全て取り出す srcslist.append([self.dst,l[1]]) #srcslistに自分の係り先を追加 def morphsin(self,morphlist): self.morphs = morphlist class Morph(object): def __init__(self,line): l = re.sub(r"\t",",",line) l = re.split(",",l) self.surface = l[0] self.base = l[7] self.pos = l[1] self.pos1 = l[2] fin = codecs.open('neko.txt.cabocha', 'r', 'utf_8') string = [] src = [] if __name__ == "__main__": n=0 for line in fin: if n==1000: break if re.search("EOS",line): if string: string[-1].morphsin(morphlist) morphlist = [] src.append(string.copy()) #stringを追加 string = [] srcslist=[] continue if re.search(r'^\*',line): #最後のstringのChunkにmorphlistを追加 if string: string[-1].morphsin(morphlist) morphlist = [] c = Chunk(line) string.append(copy.copy(c)) #Chunkを追加 n = n + 1 continue b = Morph(line) morphlist.append(copy.copy(b)) for x in src[8]: for y in x.morphs: print(y.surface,end=" ") print(x.dst)
・ いきなり全行処理されると困るので作成中は処理量を担当する 変数n を少なくしておく。
結果
しかし 9 その 2 当時 は 5 何 という 4 考 も 5 なかっ た から 9 別段 7 恐し 9 いとも 9 思わ なかっ た 。 -1
となる。
42. 係り元と係り先の文節の表示
係り元の文節と係り先の文節のテキストをタブ区切り形式ですべて抽出せよ.ただし,句読点などの記号は出力しないようにせよ.
Chunkに自分の文節と係り先の情報が入っているのでそれを使えということか。
そこに句読点などの記号を出力させないという処理を追加して……と。
pos属性が記号の場合に表示しないようにすればいい。
残念ながら文頭の「ー」が記号扱いになっていないのでそれも処理。
#!/usr/bin/env python import codecs import re import copy morphlist=[] srcslist=[] printstring = "" class Chunk(object): def __init__(self,line): l = re.split(" ",line) self.dst = l[2] self.dst = int(self.dst[:-1]) l[1] = int(l[1]) #文節番号をint化 self.srcs =[] for x in srcslist: if (x[0] == l[1]): self.srcs.append(x[1]) srcslist.append([self.dst,l[1]]) #srcslistに追加 def morphsin(self,morphlist): self.morphs = morphlist def pdelsymbol(self): printx = "" for x in self.morphs: if x.pos != '記号': if x.surface != '一': printx = printx + x.surface if printx: return printx, self.dst class Morph(object): def __init__(self,line): l = re.sub(r"\t",",",line) l = re.split(",",l) self.surface = l[0] self.base = l[7] self.pos = l[1] self.pos1 = l[2] fin = codecs.open('neko.txt.cabocha', 'r', 'utf_8') string = [] src = [] if __name__ == "__main__": n=0 for line in fin: # if n==100000: if n==200: break if re.search("EOS",line): if string: string[-1].morphsin(morphlist) morphlist = [] src.append(string.copy()) #stringを追加 string = [] srcslist=[] continue if re.search(r'^\*',line): if string: string[-1].morphsin(morphlist) morphlist = [] c = Chunk(line) string.append(copy.copy(c)) #Chunkを追加 n = n + 1 continue b = Morph(line) morphlist.append(copy.copy(b)) for x in src: for y in x: if y.dst != -1: z1 = y.pdelsymbol() #記号処理 if z1: #pdelsymbolの結果、表示する文面がなくなることがあるため if z1[1] != -1: #最終節である場合は表示しない printstring = z1[0] + "\t" z2 = x[z1[1]].pdelsymbol() printstring = printstring + z2[0] print(printstring) printstring = ""
結果は以下のとおり。
吾輩は 猫である 名前は 無い まだ 無い どこで 生れたか 生れたか つかぬ とんと つかぬ 見当が つかぬ 何でも 薄暗い 薄暗い 所で じめじめした 所で (略)
43. 名詞を含む文節が動詞を含む文節に係るものを抽出
名詞を含む文節が,動詞を含む文節に係るとき,これらをタブ区切り形式で抽出せよ.ただし,句読点などの記号は出力しないようにせよ.
文節が名詞を含むか、動詞を含むかはMorph.posを見れば分かる。
名詞を含むか判定するメソッドexistnoun,動詞を含むか判定するメソッドexistverbを追加する。
#!/usr/bin/env python import codecs import re import copy morphlist=[] srcslist=[] printstring = "" class Chunk(object): def __init__(self,line): l = re.split(" ",line) self.dst = l[2] self.dst = int(self.dst[:-1]) l[1] = int(l[1]) #文節番号をint化 self.srcs =[] #もし、srcslistに自分の文節番号があれば for x in srcslist: if (x[0] == l[1]): self.srcs.append(x[1]) srcslist.append([self.dst,l[1]]) #srcslistに追加 def morphsin(self,morphlist): self.morphs = morphlist def pdelsymbol(self): printx = "" for x in self.morphs: if x.pos != '記号': if x.surface != '一': printx = printx + x.surface if printx: return printx, self.dst def existnoun(self): for x in self.morphs: if x.pos == '名詞': return x.pos def existverb(self): for x in self.morphs: if x.pos == '動詞': return x.pos class Morph(object): def __init__(self,line): l = re.sub(r"\t",",",line) l = re.split(",",l) self.surface = l[0] self.base = l[7] self.pos = l[1] self.pos1 = l[2] fin = codecs.open('neko.txt.cabocha', 'r', 'utf_8') string = [] src = [] if __name__ == "__main__": n=0 for line in fin: # if n==100000: if n==200: break if re.search("EOS",line): if string: string[-1].morphsin(morphlist) morphlist = [] src.append(string.copy()) #stringを追加 string = [] srcslist=[] continue if re.search(r'^\*',line): if string: string[-1].morphsin(morphlist) morphlist = [] c = Chunk(line) string.append(copy.copy(c)) #Chunkを追加 n = n + 1 continue b = Morph(line) morphlist.append(copy.copy(b)) for x in src: for y in x: if y.dst != -1: z1 = y.pdelsymbol() if z1: if z1[1] != -1: printstring = z1[0] + "\t" z2 = x[z1[1]].pdelsymbol() printstring = printstring + z2[0] if y.existnoun() and x[z1[1]].existverb(): print(printstring) printstring = ""
結果
どこで 生れたか 見当が つかぬ 所で 泣いていた ニャーニャー 泣いていた 事だけは 記憶している 吾輩は 見た ここで 始めて ものを 見た 我々を 捕えて 掌に 載せられて (略)
で、一番有名な「吾輩は猫である」は抽出されない。これは「で」「ある」がともに助動詞と判定されているためである。
44. 係り受け木の可視化
与えられた文の係り受け木を有向グラフとして可視化せよ.可視化には,係り受け木をDOT言語に変換し,Graphvizを用いるとよい.また,Pythonから有向グラフを直接的に可視化するには,pydotを使うとよい.
pydotのインストールの方が手強い。python3を使っている場合はpydot3をインストールすればよい。
有向グラフなので、
digraph test {
a -> b;
a -> c;
c -> d;
d -> a;
}
のようにDOT言語で表現するのだが実際はpydotに処理させるので、 graph_from_edges() に directed=True を指示するだけで済む。
「与えられた文の」というところが重要で、この縛りがないとトンでもない図形ができあがる。
引数を使って文を指定できるようにした。指示なしで起動すると「吾輩は 猫である」が描画される。
#!/usr/bin/env python import codecs import re import copy import pydot import sys morphlist=[] srcslist=[] printstring = "" edges = [] class Chunk(object): def __init__(self,line): l = re.split(" ",line) self.dst = l[2] self.dst = int(self.dst[:-1]) l[1] = int(l[1]) #文節番号をint化 self.srcs =[] for x in srcslist: if (x[0] == l[1]): self.srcs.append(x[1]) srcslist.append([self.dst,l[1]]) #srcslistに追加 def morphsin(self,morphlist): self.morphs = morphlist def pdelsymbol(self): printx = "" for x in self.morphs: if x.pos != '記号': if x.surface != '一': printx = printx + x.surface if printx: return printx, self.dst class Morph(object): def __init__(self,line): l = re.sub(r"\t",",",line) l = re.split(",",l) self.surface = l[0] self.base = l[7] self.pos = l[1] self.pos1 = l[2] fin = codecs.open('neko.txt.cabocha', 'r', 'utf_8') string = [] src = [] if __name__ == "__main__": param = sys.argv n=0 for line in fin: # if n==100000: if n==1000: break if re.search("EOS",line): if string: string[-1].morphsin(morphlist) morphlist = [] src.append(string.copy()) #stringを追加 string = [] srcslist=[] continue if re.search(r'^\*',line): if string: string[-1].morphsin(morphlist) morphlist = [] c = Chunk(line) string.append(copy.copy(c)) #Chunkを追加 n = n + 1 continue b = Morph(line) morphlist.append(copy.copy(b)) if len(param)>1: stringn = int(param[1]) else: stringn = 1 for y in src[stringn]: if y.dst != -1: z1 = y.pdelsymbol() if z1: if z1[1] != -1: printstring = z1[0] + "\t" z2 = src[stringn][z1[1]].pdelsymbol() printstring = printstring + z2[0] splitbytub = printstring.split('\t') edges.append(splitbytub) print(edges) printstring = "" g=pydot.graph_from_edges(edges, directed=True) g.write_jpeg('neko.jpg', prog='dot')
表示される図形例は以下のとおり。
45. 動詞の格パターンの抽出
今回用いている文章をコーパスと見なし,日本語の述語が取りうる格を調査したい. 動詞を述語,動詞に係っている文節の助詞を格と考え,述語と格をタブ区切り形式で出力せよ. ただし,出力は以下の仕様を満たすようにせよ.
動詞を含む文節において,最左の動詞の基本形を述語とする
述語に係る助詞を格とする
述語に係る助詞(文節)が複数あるときは,すべての助詞をスペース区切りで辞書順に並べる「吾輩はここで始めて人間というものを見た」という例文(neko.txt.cabochaの8文目)を考える. この文は「始める」と「見る」の2つの動詞を含み,「始める」に係る文節は「ここで」,「見る」に係る文節は「吾輩は」と「ものを」と解析された場合は,次のような出力になるはずである.
始める で
見る は をこのプログラムの出力をファイルに保存し,以下の事項をUNIXコマンドを用いて確認せよ.
コーパス中で頻出する述語と格パターンの組み合わせ
「する」「見る」「与える」という動詞の格パターン(コーパス中で出現頻度の高い順に並べよ)
43で作成したプログラムをベースに考えていく。名詞ではなくて助詞が含まれている文節を求める。
Chunkに係り元データが入っているのでこれを用いる。
動詞が入っているChunkの係り元データを並べる。助詞を並べたpariclelistを導入する。
#!/usr/bin/env python import codecs import re import copy morphlist=[] srcslist=[] printstring = "" class Chunk(object): def __init__(self,line): l = re.split(" ",line) self.dst = l[2] self.dst = int(self.dst[:-1]) l[1] = int(l[1]) #文節番号をint化 self.srcs =[] self.morphs = [] for x in srcslist: if (x[0] == l[1]): self.srcs.append(x[1]) srcslist.append([self.dst,l[1]]) #srcslistに追加 def morphsin(self,morphlist): self.morphs = morphlist def existparticle(self): for x in self.morphs: if x.pos == '助詞': return x.base def existverb(self): for x in self.morphs: if x.pos == '動詞': return x.base def pdelsymbol(self): printx = "" for x in self.morphs: if x.pos != '記号': if x.surface != '一': printx = printx + x.surface if printx: return printx, self.dst class Morph(object): def __init__(self,line): l = re.sub(r"\t",",",line) l = re.split(",",l) self.surface = l[0] self.base = l[7] self.pos = l[1] self.pos1 = l[2] fin = codecs.open('neko.txt.cabocha', 'r', 'utf_8') string = [] src = [] if __name__ == "__main__": n=0 for line in fin: # if n==100000: if n==50: break if re.search("EOS",line): if string: string[-1].morphsin(morphlist) morphlist = [] src.append(string.copy()) #stringを追加 string = [] srcslist=[] continue if re.search(r'^\*',line): if string: string[-1].morphsin(morphlist) morphlist = [] c = Chunk(line) string.append(copy.copy(c)) #Chunkを追加 n = n + 1 continue b = Morph(line) morphlist.append(copy.copy(b)) for stringx in src: for chunkx in stringx: if chunkx.existverb(): l = chunkx.pdelsymbol() printstring = chunkx.existverb() + "\t" particlelist = [] #助詞用のリストを作成 for cx in chunkx.srcs: if stringx[cx].existparticle(): l = stringx[cx].pdelsymbol() particlelist.append(stringx[cx].existparticle()) if len(particlelist)>=1: #リストにデータがなければ表示しない particlelist.sort() printx = ' '.join(particlelist) printstring = printstring + printx print(printstring) printx = "" particlelist = []
結果
生れる で つく か が 泣く で する だけ 始める で 見る は を 捕える を 煮る て 食う て (略)
「や否や」が抽出されたが、これはmecabによって接続助詞とみなされているからである。
続いて、吐き出したテキストを元にUNIXコマンドで処理していく。
「コーパス中で頻出する述語と格パターンの組み合わせ」を確認するということだが、「か が」「は を」は格パターンなのだろうか?今回は「か が」は「か」「が」パターンと同一ではないという前提で分析していく。
sort 45.txt | uniq -c | sort -nr 579 云う と 439 する を 258 思う と 209 なる に 196 ある が 186 する に 175 見る て 136 する と 120 する が 108 する に を 104 する て を (略)
このあたりが100を越えるパターン。
「する」「見る」「与える」という動詞の格パターン(コーパス中で出現頻度の高い順に並べよ)
sort 45.txt | grep ^する | uniq -c | sort -nr 439 する を 186 する に 136 する と 120 する が 108 する に を 104 する て を 91 する て 67 する は 60 する が を 52 する で を (略) sort 45.txt | grep ^見る | uniq -c | sort -nr 175 見る て 90 見る を 26 見る て て 20 見る から 17 見る て を 15 見る と 15 見る で 12 見る て は 12 見る から て (略) sort 45.txt | grep ^与える | uniq -c | sort -nr 4 与える に を 1 与える ば を 1 与える に に対して も 1 与える として を 1 与える て を 1 与える て は を 1 与える て に を 1 与える て に は を 1 与える て に に は を 1 与える て て と に は は を (略)
46以降は次回。