時は数年前にさかのぼり、日本語プログラミング言語でなんか遊んでみたいなという雑談をしていたころ。
当時、最もまともだった(ように見えた)日本語プログラミング言語は「なでしこ」で、それを使ってちょっと遊んでみた。
一通りのことはできるので、大きめのプログラムを書いてみようか…と思ったタイミングで、「なでしこ3」が開発中だということに気づいた。
それから数年、久しぶりになでしこ3を思い出してサイトを見てみたら、かなりいい感じになっていた。
なでしこがWindows専用だったのに対し、なでしこ3はMacOSやLinuxに対応してるほか、ウェブで気軽に試すことができる。
何ならスマホからでも簡易エディタにアクセスしてプログラミングができる。
すっかり書き方を忘れてたけど、某氏にはなでしこでなんか作るわって言ったまま言ったきりだし、ちょっと自然言語処理をしてみようと思う。
さしあたり、バイグラム言語モデルとかを実装すれば仕様も一通り見れるし、練習になるんじゃなかろうか。これで許してくれ。
ちなみにバイグラム(bigram、2-gram)とは一般的に連続する二つの単語列を指す。
バイグラム言語モデルとは、あるコーパスにおいて、ある単語の次にどの単語がどれだけの割合で出現するか、をモデル化したもの。
なでしこ3の準備
ウェブの簡易エディタでもなでしこ3で遊べるが、手元のテキストファイルを読み込んだりするためにはローカルになでしこ3を招き入れないといけない。
公式のダウンロードページから各自てきとうにいい感じにしてほしい。
エディタは付属してる「nakopad.vbs」をつかうとよさげ。
まずはテキストの読み込み
自然言語処理なので自然言語を使ったほうがテンションが上がる。
ということで、Project Gutenberg大先生の懐からアリスをぶっこぬこう。
ここからテキストをコピーして、「alice.txt」として保存しておく。
このとき、UTF-8形式で保存することを忘れずに。
テキストファイルと同じ場所に「bigramlm.nako3」を作成して、ここに呪文を書いていく。
これは個人的な好みだが、nakoという拡張子が可愛くて好き。
まずはテキストを読み込んで、改行で区切ったものをリストにする。
「alice.txt」をパスに代入
パスを開いて「{CR}{LF}」で区切って文書に代入
特に意味はないが、せっかくなので変数名はすべて日本語にする縛りで書いてみようと思う。
{CR}と{LF}はそれぞれ\rと\nを表す。{LF}は「改行」という予約語からも呼べるので、Linux環境とかなら「パスを開いて改行で区切って文書に代入」でいける。
実はこの方法でのテキスト読み込みをWindowsでやると、BOM~ってなるんだけど、これも無視しておく。
スペースで区切って単語列にする
今度は文を単語で区切る。
本当はいろいろ前処理が必要だが、面倒なのでスペースで区切るだけで前処理とさせてほしい。
単語で区切ったついでに、文の先頭と末尾を表す特殊なトークン「<BOS>」「<EOS>」を単語列の前後にそれぞれ加えておく。
改行で区切った文を順番に見ていって、スペースで区切って特殊なトークンを付与し、整形済文書という変数(配列)に追加していく。
整形済文書は空配列
番号=0
(番号<(文書の要素数))の間
文書@番号を「 」で区切って文に代入
// 長さが0の単語を削除
整形済文は空配列
文を反復
もし(対象の文字数)が0でなければ
整形済文に対象を配列追加
ここまで
ここまで
もし(整形済文の要素数)が0でなければ
// BOS、EOSの追加
整形済文の0に「<BOS>」を配列挿入
整形済文に「<EOS>」を配列追加
// 文書に追加
整形済文書に整形済文を配列追加
ここまで
(番号+1)を番号に代入
ここまで
配列と言いつつ、挙動はリストっぽいのでてきとうに挿入や追加ができて良い。
ちなみに「~の間」はwhile文で、for文なら番号を1から((文書の要素数)-1)まで繰り返す
と書ける。
今回はなんとなくwhileにしてみた。
配列のi番目の要素を抜き出す処理は、ほかの言語のようにarr[i]
とも書けるけど、なでしこ3ではarr@i
とも書ける。これは割と気に入ってる。
ちなみになぜ@かというと、ひらがなの「の」に似てるかららしい。良い。
変数への代入は、普通のプログラミング言語みたいにイコールで書くこともできるけど、なでしこらしく「AをBに代入」を極力使っていく。
二重ハッシュでバイグラム辞書
バイグラムをカウント
バイグラムの管理は行列かハッシュで管理するのがいいと思うんだけど、なでしこ3で行列をサイズ指定で初期化する方法が分からなかったので、今回は二重ハッシュで作る。
どうせ行列も疎行列になるので、最初からハッシュで持っておいたほうがいいのかもしれない。
「ある単語の次に、どの単語が何回出現するか」というカウントを二重辞書で持っておく必要があるので、整形済文書の各文について、単語を順番に見ていく。
例えば「スーパー」という単語の後に「マーケット」が2回、「サイヤ人」が4回出るようなコーパスなら、{スーパー: {マーケット:2, サイヤ人:4}}
みたいな辞書になる。
ここで、「スーパー」を「対象単語」、「マーケット」や「サイヤ人」を「次単語」と呼ぶことにする。
「反復」を使うと、javaでいうところの拡張forができる。
配列の要素が順番に「対象」という変数に代入される。
あるいは「それ」という変数を使うこともできる。
「それ」はなでしこ特有の面白い使い方があるので、使いこなせるようになりたい。
// バイグラムは二重ハッシュで管理
バイグラム辞書 = {}
// 整形済文書をまわしてバイグラム辞書を埋める
整形済文書を反復
それを単語列に代入
番号を0から((単語列の要素数)-1)まで繰り返す
単語列[番号]を対象単語に代入
単語列[番号+1]を次単語に代入
// 二重ハッシュとして追加していく
// 辞書に対象単語のハッシュを追加
対象キー番号 = (バイグラム辞書のハッシュキー列挙)の対象単語を配列検索
もし対象キー番号が(-1)なら
バイグラム辞書[対象単語] = {}
ここまで
// 辞書[対象単語][次単語]があれば+1、なければ1で初期化
次キー番号 = ((バイグラム辞書[対象単語])のハッシュキー列挙)の次単語を配列検索
もし次キー番号が(-1)なら
バイグラム辞書[対象単語][次単語] = 1
違えば
バイグラム辞書[対象単語][次単語] = バイグラム辞書[対象単語][次単語] + 1
ここまで
ここまで
ここまで
いよいよ何をやってるんだろう自分は、という気持ちになってくる。
英語ネイティブがpython書くときとか、こういう気分なんだろうか。
なんとなく変数を全部日本語にしてみたけど、見づらい事この上ない。
でも、変数を書くたびに英語日本語を切り替えるのも嫌だし、変数名は日本語の方がいいのかもしれない、ともおもう。
なでしこではほとんどの記号や数字が全角でも記述できるので、いつか全角縛りで何かを作ってみてもいいかもしれない。
バイグラム確率に置き換え
バイグラム確率は「ある単語の次にどの単語がどれくらいの割合で出現するか」という値なので、「対象単語」ごとに各「次単語」の出現する割合の和は1にならないといけない。
ということで、「次単語」の出現回数をその総和で割っておく。
// バイグラム確率を出す
バイグラム辞書のハッシュキー列挙を反復
// バイグラムの合計を計算
対象を対象単語に代入
合計=0
バイグラム辞書[対象単語]のハッシュキー列挙を反復
対象を次単語に代入
合計 = バイグラム辞書[対象単語][次単語] + 合計
ここまで
// 単語ごとに合計で割る
バイグラム辞書[対象単語]のハッシュキー列挙を反復
対象を次単語に代入
バイグラム辞書[対象単語][次単語] = バイグラム辞書[対象単語][次単語]/合計
ここまで
ここまで
ハッシュの反復には別の書き方があって、バイグラム辞書を反復
と書くことでkey, valueがそれぞれ「対象キー」と「対象」に代入される。
今回は二重ハッシュで、これをやると意味わからなくなるので愚直に書いた。
バイグラム確率を出してみる
バイグラム確率を使っていろいろ面白いことはできるけど、今回は確率を出すところまでで一度おしまい。
次はまたそのうち。
// 確率を見てみる
バイグラム辞書を表示
バイグラム辞書[「Alice」][「considered」]を表示
// 0.004464285714285714
つまり、ルイス・キャロルのアリスのなかで「Alice」という単語の後に「considered」が出現する確率は0.004くらいということ。
そうだねってかんじ。
追記:次は言語モデルを使って文生成。
日本語プログラミング言語「なでしこ3」で自然言語処理#2(文生成)
感想と展望
正直言ってまだつらい。
日本語で書けるのは面白いし、縛りプレイとして休日に遊ぶのにはちょうどいいと思う。
これで結構巨大なプログラムを書いてみようというたくらみもあるんだけど、なでしこ3がまだ完成しきってないっぽいので、もうちょっと様子を見ようと思う。
なでしこ1にあった名前空間の指定とか、クラスにあたるグループ機能とかがまだ無いようなので、大きい何かを作るのはまだちょっとつらそう。
ちゃんと調べてるわけじゃないので、もしすでにこれらに対応する機能があるようなら、ぜひ教えてください。