構文解析 - 静かなる名辞 https://www.haya-programming.com/archive/category/%E6%A7%8B%E6%96%87%E8%A7%A3%E6%9E%90 pythonとプログラミングのこと Thu, 07 May 2020 20:42:34 +0900 http://blogs.law.harvard.edu/tech/rss Hatena::Blog 【python】cabochaのpythonバインディングの変な挙動 https://www.haya-programming.com/entry/2018/03/23/075140 <div class="section"> <h3>環境</h3> <p> ubuntu 14.04<br />  cabocha 0.69<br />  cabocha-python 0.69</p> </div> <div class="section"> <h3>問題の概要</h3> <p> 変な挙動だった。というか率直に言ってバグなのでは?</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; <span class="synPreProc">import</span> CaboCha &gt;&gt;&gt; cparser = CaboCha.Parser() &gt;&gt;&gt; tree1 = cparser.parse(<span class="synConstant">&quot;吾輩は猫である。&quot;</span>) &gt;&gt;&gt; <span class="synIdentifier">print</span>(tree1.toString(CaboCha.FORMAT_TREE)) 吾輩は-D 猫である。 EOS &gt;&gt;&gt; tree2 = cparser.parse(<span class="synConstant">&quot;吾輩は猫ではない。&quot;</span>) &gt;&gt;&gt; <span class="synIdentifier">print</span>(tree1.toString(CaboCha.FORMAT_TREE)) 吾輩は-D 猫ではない。 EOS </pre><p> これはおかしい。CaboChaはこわれている。</p><p> いや、「これで仕様通り動いてる。おまえの使い方が間違ってるんだ」て言われたら反論できないけど。詳細なドキュメントを見かけたことがないので、もしかしたらアホなこと(Parserのインスタンスの使い回し)をやっているのかもしれない。</p> </div> <div class="section"> <h3>回避するために試したこと</h3> <ul> <li>CaboCha.Parser("")する(コンストラクタに空文字列を渡す)</li> </ul><p> 効果なし。</p> <ul> <li>一度空文字列に対してparseを呼ぶ</li> </ul><p> ここを参考に「もしかしたら効くかも」と思ってやってみた。<br /> <a href="http://kenichia.hatenablog.com/entry/2016/07/28/142633">MeCab&#x306E;parseToNode&#x306E;&#x3072;&#x3069;&#x3044;&#x30D0;&#x30B0; - &#x5317;&#x91CE;&#x5742;&#x5099;&#x5FD8;&#x9332;</a><br />  効果なし。</p> <ul> <li>文字列を変数に入れる、encodeする</li> </ul><p> ここを参考に(以下略)。<br /> <a href="https://shogo82148.github.io/blog/2012/12/15/mecab-python/">MeCab&#x3092;Python&#x304B;&#x3089;&#x4F7F;&#x3046;&#x6CE8;&#x610F;&#x70B9;&#x3068;&#x304B;</a><br />  効果なし。encodeに至ってはやったら落ちた。</p> <ul> <li>仕方がないのでCaboCha.Parser()を毎回作る</li> </ul><pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; tree = CaboCha.Parser().parse(<span class="synConstant">&quot;吾輩は猫である。&quot;</span>) &gt;&gt;&gt; tree.toString(CaboCha.FORMAT_TREE) Segmentation fault (コアダンプ) </pre><p> たぶん本体のメモリ管理とpythonの接合が上手く行っていないのだろうけど、さて困った。</p> <pre class="code lang-python" data-lang="python" data-unlink>&gt;&gt;&gt; parser1 = CaboCha.Parser() &gt;&gt;&gt; parser2 = CaboCha.Parser() &gt;&gt;&gt; tree1 = parser1.parse(<span class="synConstant">&quot;吾輩は猫である。&quot;</span>) &gt;&gt;&gt; tree2 = parser2.parse(<span class="synConstant">&quot;吾輩は猫ではない。&quot;</span>) &gt;&gt;&gt; tree1.toString(CaboCha.FORMAT_TREE) <span class="synConstant">' 吾輩は-D</span><span class="synSpecial">\n</span><span class="synConstant"> 猫である。</span><span class="synSpecial">\n</span><span class="synConstant">EOS</span><span class="synSpecial">\n</span><span class="synConstant">'</span> </pre><p> 一応回避できることはわかった。これで書くと極めて非python的なプログラミングを強いられるという問題はあるが、たぶんなんとかなる。</p><p> ちなみに、ParserのインスタンスがGCに回収されると treeだけ残っててもtoStringできないようです(Segmentation faultを吐いてくれる)。どんな作りになってるのかなんとなくわかってきたけど、率直に言って○○。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synStatement">def</span> <span class="synIdentifier">parse</span>(sentences): <span class="synConstant">&quot;&quot;&quot;</span> <span class="synConstant"> sentencesは一文ずつに区切られた文のリストとして扱う</span> <span class="synConstant"> &quot;&quot;&quot;</span> trees = [] plist = [] <span class="synStatement">for</span> s <span class="synStatement">in</span> sentences: parser = CaboCha.Parser() trees.append(parser.parse(s)) plist.append(parser) </pre><p> このようなものを書いてあげれば、後からtreeを使うことができることがわかった。率直に言ってまったく嬉しくない。</p> </div> <div class="section"> <h3>問題原因の切り分け</h3> <p> は、できてないです。<br />  </p> <ul> <li>うちの環境固有の問題</li> <li>cabocha-pythonの特定のバージョンの問題</li> <li>cabocha-python固有の問題</li> <li>cabocha固有の問題</li> </ul><p> とりあえず逃げれることはわかったので、僕はやらない(明言)。</p> </div> <div class="section"> <h3>対策</h3> <p> たぶん解析結果のtreeオブジェクトを使いまわそうという発想が間違っていて、cabochaのtreeオブジェクトを使わないで初手でXMLか何かに変換して取り扱うのが楽だと思います。そんなことを強いるバインディングって何よ? って気がしますが。</p><p> もう面倒くさいから、JUMAN/KNPに鞍替えしようかなと思う今日この頃。</p> </div> Fri, 23 Mar 2018 07:51:40 +0900 hatenablog://entry/17391345971628459573 python 自然言語処理 構文解析 日本語モダリティ解析器 Zundaを試す https://www.haya-programming.com/entry/2018/03/22/133436 <p> 日本語のモダリティを解析できるらしい。「文中のイベント(動詞や形容詞など)に対して、その真偽判断(イベントが起こったかどうか)、仮想性(仮定の話かどうか)などを解析します」とのこと。</p><p> 公式ページはたぶんここ。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fjmizuno.github.io%2Fzunda%2F" title="zunda" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://jmizuno.github.io/zunda/">jmizuno.github.io</a></cite><br /> </p> <div class="section"> <h3>環境</h3> <p> ubuntu14.04</p> </div> <div class="section"> <h3>インストール</h3> <p> CaboCha (>=0.60)と、Boost Library (>=1.41)を予め入れおく必要がある。CaboChaは入ってたけどBoost Libraryはなかったので、apt-getで入れた。</p> <pre class="code" data-lang="" data-unlink>$ sudo apt-get install libboost-all-dev</pre><p> 後はtarballを落としてきてmakeで入れろと公式に書いてある。どんなエラーが出てくるかとびくびくしながらやったけど、まったく問題なく入った。</p> <pre class="code" data-lang="" data-unlink>$ ./configure $ make $ sudo make install</pre> </div> <div class="section"> <h3>試してみる</h3> <p> こうやって使うらしい。</p> <pre class="code" data-lang="" data-unlink>$ echo -e &#34;次郎は大阪に行った。\n太郎は東京には行かず地元に残ろうとした&#34; | zunda #EVENT0 4 wr:筆者 非未来 0 叙述 成立 0 0 * 0 2D 0/1 -2.249829 次郎 名詞,固有名詞,人名,名,*,*,次郎,ジロウ,ジロー は 助詞,係助詞,*,*,*,*,は,ハ,ワ * 1 2D 0/1 -2.249829 大阪 名詞,固有名詞,地域,一般,*,*,大阪,オオサカ,オーサカ に 助詞,格助詞,一般,*,*,*,に,ニ,ニ * 2 -1D 0/1 0.000000 行っ 動詞,自立,*,*,五段・カ行促音便,連用タ接続,行く,イッ,イッ た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ 。 記号,句点,*,*,*,*,。,。,。 EOS #EVENT0 5 wr:筆者 未来 0 叙述 不成立 0 0 #EVENT1 9 wr:筆者 未来 0 意志 高確率 ポジティブ 0 #EVENT2 12 wr:筆者 非未来 0 叙述 成立 0 0 * 0 4D 0/1 -1.650377 太郎 名詞,固有名詞,人名,名,*,*,太郎,タロウ,タロー は 助詞,係助詞,*,*,*,*,は,ハ,ワ * 1 2D 0/2 0.320510 東京 名詞,固有名詞,地域,一般,*,*,東京,トウキョウ,トーキョー に 助詞,格助詞,一般,*,*,*,に,ニ,ニ は 助詞,係助詞,*,*,*,*,は,ハ,ワ * 2 4D 0/1 -1.650377 行か 動詞,自立,*,*,五段・カ行促音便,未然形,行く,イカ,イカ ず 助動詞,*,*,*,特殊・ヌ,連用ニ接続,ぬ,ズ,ズ * 3 4D 0/1 -1.650377 地元 名詞,一般,*,*,*,*,地元,ジモト,ジモト に 助詞,格助詞,一般,*,*,*,に,ニ,ニ * 4 -1D 3/4 0.000000 残ろ 動詞,自立,*,*,五段・ラ行,未然ウ接続,残る,ノコロ,ノコロ う 助動詞,*,*,*,不変化型,基本形,う,ウ,ウ と 助詞,格助詞,引用,*,*,*,と,ト,ト し 動詞,自立,*,*,サ変・スル,連用形,する,シ,シ た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ EOS</pre><p> 公式ページには改行すれば別の文として扱われると書いてあるが、echoに-eオプションを渡さないと改行文字は改行として出力されないので注意。</p><p> それはそうと、この結果の見方だが、</p> <pre class="code" data-lang="" data-unlink>#EVENT0 4 wr:筆者 非未来 0 叙述 成立 0 0」</pre><p> という結果は4番目の形態素がどんなモダリティなのかを表す。つまり「行っ 動詞,自立,*,*,五段・カ行促音便,連用タ接続,行く,イッ,イッ」に対応する。単純だけどわかりやすいかどうかは微妙かもしれない。</p> </div> <div class="section"> <h3>色々なことを試す</h3> <p> とりあえず、もうちょっと色々な文を入れてみる。</p> <pre class="code" data-lang="" data-unlink>$ echo &#34;遊びに行きたいな&#34; | zunda #EVENT0 2 wr:筆者 未来 0 欲求 0 ポジティブ 0 * 0 1D 0/1 0.000000 遊び 名詞,一般,*,*,*,*,遊び,アソビ,アソビ に 助詞,格助詞,一般,*,*,*,に,ニ,ニ * 1 -1D 0/2 0.000000 行き 動詞,自立,*,*,五段・カ行促音便,連用形,行く,イキ,イキ たい 助動詞,*,*,*,特殊・タイ,基本形,たい,タイ,タイ な 助詞,終助詞,*,*,*,*,な,ナ,ナ EOS</pre><p> なるほど。</p> <pre class="code" data-lang="" data-unlink>$ echo -e &#34;吾輩は猫である。\n名前はまだない。&#34; | zunda * 0 1D 0/1 0.000000 吾輩 名詞,代名詞,一般,*,*,*,吾輩,ワガハイ,ワガハイ は 助詞,係助詞,*,*,*,*,は,ハ,ワ * 1 -1D 0/2 0.000000 猫 名詞,一般,*,*,*,*,猫,ネコ,ネコ で 助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ ある 助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル 。 記号,句点,*,*,*,*,。,。,。 EOS #EVENT0 3 wr:筆者 非未来 0 叙述 成立 0 0 * 0 2D 0/1 -2.377508 名前 名詞,一般,*,*,*,*,名前,ナマエ,ナマエ は 助詞,係助詞,*,*,*,*,は,ハ,ワ * 1 2D 0/0 -2.377508 まだ 副詞,助詞類接続,*,*,*,*,まだ,マダ,マダ * 2 -1D 0/0 0.000000 ない 形容詞,自立,*,*,形容詞・アウオ段,基本形,ない,ナイ,ナイ 。 記号,句点,*,*,*,*,。,。,。 EOS</pre><p> ふーん。</p><p> まあ、勝手はなんとなくわかった。それはそうと、zundaにはpythonバインディングなんて親切なものはないらしい。zundaオリジナルなのは#から始まる行だけで、その下はcabochaの-f 1フォーマットがそのまま出てるだけなので、プログラミングで使う側としてはテキスト処理でゴリ押して使うことになるだろう。</p><p> その際は、コマンドラインオプションで何百kbも受け渡しするのは流石にアレなので、まとめてファイルを処理させるか、立ち上げておいて標準入出力でやりとりするかのどちらかになる。</p> </div> Thu, 22 Mar 2018 13:34:36 +0900 hatenablog://entry/17391345971628218541 自然言語処理 構文解析 【python】CKY法をpythonで実装 https://www.haya-programming.com/entry/2018/02/19/044452 <p> 構文解析アルゴリズムのCKY法の実装について説明する。参考にしたテキストはこれ。</p><p><div class="hatena-asin-detail"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4339024511/hatena-blog-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/41t6quv04oL._SL160_.jpg" class="hatena-asin-detail-image" alt="自然言語処理の基礎" title="自然言語処理の基礎"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4339024511/hatena-blog-22/">自然言語処理の基礎</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span> 奥村学</li><li><span class="hatena-asin-detail-label">出版社/メーカー:</span> コロナ社</li><li><span class="hatena-asin-detail-label">発売日:</span> 2010/10/15</li><li><span class="hatena-asin-detail-label">メディア:</span> 単行本(ソフトカバー)</li><li><span class="hatena-asin-detail-label">購入</span>: 8人 <span class="hatena-asin-detail-label">クリック</span>: 379回</li><li><a href="http://d.hatena.ne.jp/asin/4339024511/hatena-blog-22" target="_blank">この商品を含むブログ (11件) を見る</a></li></ul></div><div class="hatena-asin-detail-foot"></div></div></p><p> 目次</p> <ul class="table-of-contents"> <li><a href="#理論">理論</a></li> <li><a href="#問題設定">問題設定</a></li> <li><a href="#実装">実装</a></li> <li><a href="#結果">結果</a></li> <li><a href="#感想">感想</a></li> <li><a href="#付録ソースコード">付録 ソースコード</a></li> </ul><p><span style="font-size: 80%">スポンサーリンク</span><br /> <script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script></p> <p><ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-6261827798827777" data-ad-slot="1744230936" data-ad-format="auto" data-full-width-responsive="true"></ins><br /> <script> (adsbygoogle = window.adsbygoogle || []).push({}); </script></p><br /> <p></p> <div class="section"> <h3 id="理論">理論</h3> <p> 教科書読めばぜんぶ書いてあります(ちゃんと解説しようとすると大変なので、自分で説明したくない)。</p><p> ネット上の解説としては、</p> <ul> <li><a href="https://ja.wikipedia.org/wiki/CYK%E6%B3%95">CYK&#x6CD5; - Wikipedia</a></li> <li><a href="http://www.sist.ac.jp/~kanakubo/research/natural_language_processing/syntactic_analysis.html">&#x69CB;&#x6587;&#x89E3;&#x6790;&#x306E;&#x65B9;&#x6CD5;</a></li> <li><a href="http://www.jaist.ac.jp/~kshirai/lec/i223/04a.pdf">http://www.jaist.ac.jp/~kshirai/lec/i223/04a.pdf</a></li> </ul><p> この3つを読めば理解できると思います。プログラムとして実装する前に、紙とペンで一回やってみるべきです。</p><p> CKY法は理屈が簡単な割に、プログラムに書き起こすのが面倒くさいタイプのアルゴリズムです(だと思う)。頭でちゃんと理解してから挑むことが望ましい。</p> </div> <div class="section"> <h3 id="問題設定">問題設定</h3> <p> 教科書のToy problemで行きます。次の文を構文解析するというものです。</p> <pre class="code" data-lang="" data-unlink>astronomers saw stars with ears</pre><p> 文法は次のように与えられています。</p> <pre class="code" data-lang="" data-unlink>S→NP VP:1.0 PP→P NP:1.0 VP→V NP:0.7 VP→VP PP:0.3 NP→NP PP:0.4 P→with:1.0 V→saw:1.0 NP→astronomers:0.1 NP→ears:0.18 NP→saw:0.04 NP→stars:0.18 NP→telescope:0.1</pre><p> コロンの後の数字は文法規則が適用される確率です。今回の例文には多義性があるので(複数の構文解析結果が出て来る)、この確率を使ってもっともらしい結果を選ぼうという訳です。</p> </div> <div class="section"> <h3 id="実装">実装</h3> <p> 書いたソースコードは記事の最後に丸ごと載せてます。以下では実装方法を簡単に解説します。</p><p> とりあえず何も考えず、上記例文と文法をグローバル変数として定義。</p> <pre class="code lang-python" data-lang="python" data-unlink>example_sentence = <span class="synConstant">&quot;astronomers saw stars with ears&quot;</span> grammar_text = <span class="synConstant">&quot;&quot;&quot;S→NP VP:1.0</span> <span class="synConstant">PP→P NP:1.0</span> <span class="synConstant">VP→V NP:0.7</span> <span class="synConstant">VP→VP PP:0.3</span> <span class="synConstant">NP→NP PP:0.4</span> <span class="synConstant">P→with:1.0</span> <span class="synConstant">V→saw:1.0</span> <span class="synConstant">NP→astronomers:0.1</span> <span class="synConstant">NP→ears:0.18</span> <span class="synConstant">NP→saw:0.04</span> <span class="synConstant">NP→stars:0.18</span> <span class="synConstant">NP→telescope:0.1&quot;&quot;&quot;</span> </pre><p> CKYクラスを作ることにする。CKYクラスインスタンスのparseメソッドを呼べば、然るべき型で結果を返してくれるように作る方針で行こう。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synStatement">class</span> <span class="synIdentifier">CKY</span>: <span class="synStatement">def</span> <span class="synIdentifier">__init__</span>(self, grammar_text): self.grammar_dict = defaultdict(<span class="synIdentifier">set</span>) <span class="synStatement">for</span> line <span class="synStatement">in</span> grammar_text.split(<span class="synConstant">&quot;</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>): rule, p = line.split(<span class="synConstant">&quot;:&quot;</span>) l, r = rule.split(<span class="synConstant">&quot;→&quot;</span>) self.grammar_dict[r].add((l, <span class="synIdentifier">float</span>(p))) self.cky_array = <span class="synIdentifier">None</span> </pre><p> 文法の情報はクラス内で持ってないと困るので、defaultdict(set)で格納。解析の過程を考えると、文法の右側の要素から左側の要素(あと確率)が取り出せると嬉しいので、そうする。今回、keyは"astronomers"みたいな終端記号を表す文字列か、"P NP"みたいな非終端記号のペアを表す文字列にしている。非終端記号のペアはtupleにして……とか考えるとかえって面倒くさい。</p><p> なんでsetにするのか? 右が同じだけど左が異なるパターンがあるから。「V→saw:1.0」と「NP→saw:0.04」とかですね。</p><p> self.cky_arrayはとりあえずNoneにしておく。文の長さが決まらないとinitializeできない。ということは、initializeするメソッドも作っておく必要がある(別にメソッドにしないでparseメソッド内でやっても良いんだが)。</p><p> このcky_array、CKYテーブル、詰まるところ三角行列をどう実装するかは悩みどころで、適当に作るとインデックスでエラく苦労する。とりあえず、今回は多義性がある文を解析するので、行列の一つのセルに複数の要素が入るので、三重リストみたいなものにしないといけない。</p><p> という訳で、単語数*単語数*空リストの三重リストとして実装する。こうすると下半分が無駄にメモリを食うけど、大した実害はない。ちょっと無駄っぽいけど。</p> <pre class="code lang-python" data-lang="python" data-unlink> <span class="synStatement">def</span> <span class="synIdentifier">_init_cky_array</span>(self, length): self.cky_array = [[[] <span class="synStatement">for</span> _ <span class="synStatement">in</span> <span class="synIdentifier">range</span>(length)] <span class="synStatement">for</span> i <span class="synStatement">in</span> <span class="synIdentifier">range</span>(length)] <span class="synStatement">return</span> self.cky_array </pre><p> あとは空リストに適当に値を突っ込んでいけば、CKYテーブルは作れる。適当に、と書いたけど、ここが一番つらい。とりあえずparseメソッドを書き始める。</p> <pre class="code lang-python" data-lang="python" data-unlink> <span class="synStatement">def</span> <span class="synIdentifier">parse</span>(self, text): words = text.split() self.length = <span class="synIdentifier">len</span>(words) self._init_cky_array(self.length) </pre><p> まず行列の対角成分(NT→Tの文法の部分)を埋める。</p> <pre class="code lang-python" data-lang="python" data-unlink> <span class="synStatement">for</span> i, word <span class="synStatement">in</span> <span class="synIdentifier">enumerate</span>(words): <span class="synStatement">for</span> l, p <span class="synStatement">in</span> self.grammar_dict[word]: self.cky_array[i][i].append((l, word, p)) </pre><p> テーブルのセルに入れる値は、(左辺値(str), 単語(str), 確率(float))の形のtuple。対角成分以外では、(左辺値(str), (右辺の左のindex(tuple), 右辺の右のindex(tuple)), 確率(float))とする方針。こういうところに独自定義のオブジェクトを入れたがる人がたまにいるが、経験上かえって面倒くさくなることが多い。CKY以外のクラスは定義しないで書く。</p><p> そして謎のfor文で一気にCKY配列を埋める。コメントを書いたので頑張って理解して。</p> <pre class="code lang-python" data-lang="python" data-unlink> <span class="synComment"># 対角成分の1つ右,2つ右,...と処理を回すループ</span> <span class="synStatement">for</span> d <span class="synStatement">in</span> <span class="synIdentifier">range</span>(<span class="synConstant">1</span>, self.length): <span class="synComment"># 斜め下に進んでいくループ</span> <span class="synComment"># i,jでどのセルを処理対象とするか決める</span> <span class="synStatement">for</span> i <span class="synStatement">in</span> <span class="synIdentifier">range</span>(self.length - d): j = i + d <span class="synComment"># セルの中身を埋めるループ</span> <span class="synStatement">for</span> k <span class="synStatement">in</span> <span class="synIdentifier">range</span>(i, j): <span class="synComment"># 右辺の可能な組み合わせを列挙してる</span> <span class="synStatement">for</span> a, b <span class="synStatement">in</span> product( <span class="synIdentifier">range</span>(<span class="synIdentifier">len</span>(self.cky_array[i][k])), <span class="synIdentifier">range</span>(<span class="synIdentifier">len</span>(self.cky_array[k+<span class="synConstant">1</span>][j]))): <span class="synComment"># 辞書のキーを作る</span> s = <span class="synConstant">&quot;{0} {1}&quot;</span>.<span class="synIdentifier">format</span>( self.cky_array[i][k][a][<span class="synConstant">0</span>], self.cky_array[k+<span class="synConstant">1</span>][j][b][<span class="synConstant">0</span>]) <span class="synComment"># キーに合致する文法をぜんぶ出す</span> <span class="synStatement">for</span> l,p <span class="synStatement">in</span> self.grammar_dict[s]: <span class="synComment"># セルに中身を入れる</span> self.cky_array[i][j].append( (l, ((i,k,a), (k+<span class="synConstant">1</span>,j,b)), p)) </pre><p> なんとforループが5つもある。<span style="font-size: 80%"><span style="color: #cccccc">五重のforと名付けよう。</span></span>なお、CKY法は<img src="https://chart.apis.google.com/chart?cht=tx&chl=%20O%28n%5E3%29" alt=" O(n^3)"/>のアルゴリズムである。一番内側の2つのループは基本的に定数項で、計算量には効かない。</p><p> このforループが終わると、CKYテーブルはすでに完成している。後は、これを辿って構文木を出力するだけだ。セルにindexを入れたことがここで効いてくる。なお、紙とペンでやるときはNP1とか通し番号を振り、NP1(astronomers)とかPP1(P1, NP2)みたいに書くと混乱が少ない。<br />  <br />  構文木を辿る方法は、当然再帰である。indexを見て次のセルに飛べば良い。indexを表現するtupleではなく、終端記号を表現するstrが格納されていたら、再帰の終了条件を満たしたとみなす。</p><p> 構文木の出力形式は、XMLで行く。僕はlxmlを使って処理するのに慣れているので、今回も使うことにする。</p><p> 以上の方針を決めた上で、次のコードを書き足す。</p> <pre class="code lang-python" data-lang="python" data-unlink> <span class="synComment"># parseの最後</span> <span class="synStatement">return</span> self._gen_xml_etree_list() <span class="synStatement">def</span> <span class="synIdentifier">_traverse_tree</span>(self, index=(<span class="synConstant">0</span>,<span class="synConstant">0</span>,<span class="synConstant">0</span>)): <span class="synComment"># 構文木を辿る</span> i,j,k = index node = self.cky_array[i][j][k] elem = etree.Element(node[<span class="synConstant">0</span>]) child = node[<span class="synConstant">1</span>] p = node[<span class="synConstant">2</span>] elem.attrib[<span class="synConstant">&quot;p&quot;</span>] = <span class="synIdentifier">str</span>(p) <span class="synStatement">if</span> <span class="synIdentifier">type</span>(child) == <span class="synIdentifier">str</span>: elem.text = child <span class="synStatement">return</span> elem <span class="synStatement">else</span>: l, r = child elem.append(self._traverse_tree(index=l)) elem.append(self._traverse_tree(index=r)) <span class="synStatement">return</span> elem <span class="synStatement">def</span> <span class="synIdentifier">_gen_xml_etree_list</span>(self): <span class="synComment"># 再帰呼出しを開始する</span> lst = [] <span class="synStatement">for</span> i, s <span class="synStatement">in</span> <span class="synIdentifier">enumerate</span>(self.cky_array[<span class="synConstant">0</span>][self.length - <span class="synConstant">1</span>]): <span class="synStatement">if</span> s[<span class="synConstant">0</span>] != <span class="synConstant">&quot;S&quot;</span>: <span class="synStatement">pass</span> <span class="synStatement">else</span>: <span class="synComment"># etreeのまま返すことにしよう...</span> lst.append(self._traverse_tree((<span class="synConstant">0</span>,<span class="synConstant">4</span>,i))) <span class="synStatement">return</span> lst </pre><p> お疲れ様でした。これでCKYクラスの実装はおしまいです。あとはmainを書くだけです。mainは確率の総乗を計算し、またetreeを文字列に変換して表示します。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synStatement">def</span> <span class="synIdentifier">main</span>(): cky = CKY(grammar_text) lst = cky.parse(example_sentence) <span class="synStatement">for</span> xml_tree <span class="synStatement">in</span> lst: p = <span class="synConstant">1</span> <span class="synStatement">for</span> elem <span class="synStatement">in</span> xml_tree.<span class="synIdentifier">iter</span>(): p *= <span class="synIdentifier">float</span>(elem.attrib[<span class="synConstant">&quot;p&quot;</span>]) <span class="synIdentifier">print</span>(p) <span class="synIdentifier">print</span>( etree.tostring(xml_tree, pretty_print=<span class="synIdentifier">True</span>).decode()) </pre><p> あとはmainの呼び出しを書けば終了です。import等はここでは省略しました。記事末尾のソースコードには載せています。</p> </div> <div class="section"> <h3 id="結果">結果</h3> <p> 実行結果を見せます。</p> <pre class="code" data-lang="" data-unlink>0.0009071999999999998 &lt;S p=&#34;1.0&#34;&gt; &lt;NP p=&#34;0.1&#34;&gt;astronomers&lt;/NP&gt; &lt;VP p=&#34;0.7&#34;&gt; &lt;V p=&#34;1.0&#34;&gt;saw&lt;/V&gt; &lt;NP p=&#34;0.4&#34;&gt; &lt;NP p=&#34;0.18&#34;&gt;stars&lt;/NP&gt; &lt;PP p=&#34;1.0&#34;&gt; &lt;P p=&#34;1.0&#34;&gt;with&lt;/P&gt; &lt;NP p=&#34;0.18&#34;&gt;ears&lt;/NP&gt; &lt;/PP&gt; &lt;/NP&gt; &lt;/VP&gt; &lt;/S&gt; 0.0006803999999999998 &lt;S p=&#34;1.0&#34;&gt; &lt;NP p=&#34;0.1&#34;&gt;astronomers&lt;/NP&gt; &lt;VP p=&#34;0.3&#34;&gt; &lt;VP p=&#34;0.7&#34;&gt; &lt;V p=&#34;1.0&#34;&gt;saw&lt;/V&gt; &lt;NP p=&#34;0.18&#34;&gt;stars&lt;/NP&gt; &lt;/VP&gt; &lt;PP p=&#34;1.0&#34;&gt; &lt;P p=&#34;1.0&#34;&gt;with&lt;/P&gt; &lt;NP p=&#34;0.18&#34;&gt;ears&lt;/NP&gt; &lt;/PP&gt; &lt;/VP&gt; &lt;/S&gt; </pre><p> まあ、良いのでは。「天文学者は耳と一緒の星を見た」と「天文学者は耳で星を見た」の二通りの解析結果があり、前者の方が良い感じ、みたいな結果・・・だと思います。</p> </div> <div class="section"> <h3 id="感想">感想</h3> <p> やっぱりアルゴリズムが簡単な割に書くのが大変だった。特にindexの範囲をミスると簡単に死ねるので、自分で実装するときはindexを随時printして(あるいはデバッガで確認して)正しい値が出ているか確認しながらやるのが良いです。</p> </div> <div class="section"> <h3 id="付録ソースコード">付録 ソースコード</h3> <p><div onclick="obj=document.getElementById('oritatami_part').style; obj.display=(obj.display=='none')?'block':'none';"><br /> <a style="cursor:pointer;">▶ソースコード全体(クリックで展開)</a><br /> </div><div id="oritatami_part" style="display:none;clear:both;"></p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment"># coding: UTF-8</span> <span class="synPreProc">from</span> collections <span class="synPreProc">import</span> defaultdict <span class="synPreProc">from</span> itertools <span class="synPreProc">import</span> product <span class="synPreProc">from</span> lxml <span class="synPreProc">import</span> etree example_sentence = <span class="synConstant">&quot;astronomers saw stars with ears&quot;</span> grammar_text = <span class="synConstant">&quot;&quot;&quot;S→NP VP:1.0</span> <span class="synConstant">PP→P NP:1.0</span> <span class="synConstant">VP→V NP:0.7</span> <span class="synConstant">VP→VP PP:0.3</span> <span class="synConstant">NP→NP PP:0.4</span> <span class="synConstant">P→with:1.0</span> <span class="synConstant">V→saw:1.0</span> <span class="synConstant">NP→astronomers:0.1</span> <span class="synConstant">NP→ears:0.18</span> <span class="synConstant">NP→saw:0.04</span> <span class="synConstant">NP→stars:0.18</span> <span class="synConstant">NP→telescope:0.1&quot;&quot;&quot;</span> <span class="synStatement">class</span> <span class="synIdentifier">CKY</span>: <span class="synStatement">def</span> <span class="synIdentifier">__init__</span>(self, grammar_text): self.grammar_dict = defaultdict(<span class="synIdentifier">set</span>) <span class="synStatement">for</span> line <span class="synStatement">in</span> grammar_text.split(<span class="synConstant">&quot;</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>): rule, p = line.split(<span class="synConstant">&quot;:&quot;</span>) l, r = rule.split(<span class="synConstant">&quot;→&quot;</span>) self.grammar_dict[r].add((l, <span class="synIdentifier">float</span>(p))) self.cky_array = <span class="synIdentifier">None</span> <span class="synStatement">def</span> <span class="synIdentifier">_init_cky_array</span>(self, length): self.cky_array = [[[] <span class="synStatement">for</span> _ <span class="synStatement">in</span> <span class="synIdentifier">range</span>(length)] <span class="synStatement">for</span> i <span class="synStatement">in</span> <span class="synIdentifier">range</span>(length)] <span class="synStatement">return</span> self.cky_array <span class="synStatement">def</span> <span class="synIdentifier">parse</span>(self, text): words = text.split() self.length = <span class="synIdentifier">len</span>(words) self._init_cky_array(self.length) <span class="synStatement">for</span> i, word <span class="synStatement">in</span> <span class="synIdentifier">enumerate</span>(words): <span class="synStatement">for</span> l, p <span class="synStatement">in</span> self.grammar_dict[word]: self.cky_array[i][i].append((l, word, p)) <span class="synComment"># 対角成分の1つ右,2つ右,...と処理を回すループ</span> <span class="synStatement">for</span> d <span class="synStatement">in</span> <span class="synIdentifier">range</span>(<span class="synConstant">1</span>, self.length): <span class="synComment"># 斜め下に進んでいくループ</span> <span class="synComment"># i,jでどのセルを処理対象とするか決める</span> <span class="synStatement">for</span> i <span class="synStatement">in</span> <span class="synIdentifier">range</span>(self.length - d): j = i + d <span class="synComment"># セルの中身を埋めるループ</span> <span class="synStatement">for</span> k <span class="synStatement">in</span> <span class="synIdentifier">range</span>(i, j): <span class="synComment"># 右辺の可能な組み合わせを列挙してる</span> <span class="synStatement">for</span> a, b <span class="synStatement">in</span> product( <span class="synIdentifier">range</span>(<span class="synIdentifier">len</span>(self.cky_array[i][k])), <span class="synIdentifier">range</span>(<span class="synIdentifier">len</span>(self.cky_array[k+<span class="synConstant">1</span>][j]))): <span class="synComment"># 辞書のキーを作る</span> s = <span class="synConstant">&quot;{0} {1}&quot;</span>.<span class="synIdentifier">format</span>( self.cky_array[i][k][a][<span class="synConstant">0</span>], self.cky_array[k+<span class="synConstant">1</span>][j][b][<span class="synConstant">0</span>]) <span class="synComment"># キーに合致する文法をぜんぶ出す</span> <span class="synStatement">for</span> l,p <span class="synStatement">in</span> self.grammar_dict[s]: <span class="synComment"># セルに中身を入れる</span> self.cky_array[i][j].append( (l, ((i,k,a), (k+<span class="synConstant">1</span>,j,b)), p)) <span class="synComment"># parseの最後</span> <span class="synStatement">return</span> self._gen_xml_etree_list() <span class="synStatement">def</span> <span class="synIdentifier">_traverse_tree</span>(self, index=(<span class="synConstant">0</span>,<span class="synConstant">0</span>,<span class="synConstant">0</span>)): <span class="synComment"># 構文木を辿る</span> i,j,k = index node = self.cky_array[i][j][k] elem = etree.Element(node[<span class="synConstant">0</span>]) child = node[<span class="synConstant">1</span>] p = node[<span class="synConstant">2</span>] elem.attrib[<span class="synConstant">&quot;p&quot;</span>] = <span class="synIdentifier">str</span>(p) <span class="synStatement">if</span> <span class="synIdentifier">type</span>(child) == <span class="synIdentifier">str</span>: elem.text = child <span class="synStatement">return</span> elem <span class="synStatement">else</span>: l, r = child elem.append(self._traverse_tree(index=l)) elem.append(self._traverse_tree(index=r)) <span class="synStatement">return</span> elem <span class="synStatement">def</span> <span class="synIdentifier">_gen_xml_etree_list</span>(self): <span class="synComment"># 再帰呼出しを開始する</span> lst = [] <span class="synStatement">for</span> i, s <span class="synStatement">in</span> <span class="synIdentifier">enumerate</span>(self.cky_array[<span class="synConstant">0</span>][self.length - <span class="synConstant">1</span>]): <span class="synStatement">if</span> s[<span class="synConstant">0</span>] != <span class="synConstant">&quot;S&quot;</span>: <span class="synStatement">pass</span> <span class="synStatement">else</span>: <span class="synComment"># etreeのまま返すことにしよう...</span> lst.append(self._traverse_tree((<span class="synConstant">0</span>,<span class="synConstant">4</span>,i))) <span class="synStatement">return</span> lst <span class="synStatement">def</span> <span class="synIdentifier">main</span>(): cky = CKY(grammar_text) lst = cky.parse(example_sentence) <span class="synStatement">for</span> xml_tree <span class="synStatement">in</span> lst: p = <span class="synConstant">1</span> <span class="synStatement">for</span> elem <span class="synStatement">in</span> xml_tree.<span class="synIdentifier">iter</span>(): p *= <span class="synIdentifier">float</span>(elem.attrib[<span class="synConstant">&quot;p&quot;</span>]) <span class="synIdentifier">print</span>(p) <span class="synIdentifier">print</span>( etree.tostring(xml_tree, pretty_print=<span class="synIdentifier">True</span>).decode()) <span class="synStatement">if</span> __name__ == <span class="synConstant">&quot;__main__&quot;</span>: main() </pre><p></div></p> </div> Mon, 19 Feb 2018 04:44:52 +0900 hatenablog://entry/17391345971617705447 python 自然言語処理 構文解析