2017/08/14-08/20

祖父母と親戚に会うための家族旅行に同伴する。
毎年富山の祖父母家に集まるんだけど、祖父母ともに恒例になってきてみんなで旅行できるチャンスもそうないしってことで、今年は長野一泊弾丸旅行。

僕は運転手でした。

実は妹が数日前まで塾の合宿で長野にいて、僕は僕で一昨年に長野にゼミ合宿で行ってて、本当にアクセスがいいというかお手軽な観光地なんだなあ長野、と感じる。
数ヶ月ぶりにハンドル握って、善光寺あたりの狭い道を頑張って運転したので夜ご飯とかお風呂とか最高だった。
労働してこその大浴場。

翌日は軽井沢のアウトレットをぶらぶら。
あいにくの雨で、しかもうちの家族はウィンドウショッピングができない人ばっかりなのでつまらなかったらしい。
僕は大好きなタケオキクチで服買えたのでるんるんだったけど。

長野駅らへんから軽井沢の近くまで車で行ったんだけど、大雨で非常に怖かった。
悪天候の高速道路ってやだよねー、トラックの水しぶきで前見えないし風に煽られるし。
命が惜しいし家族を載せてたので80キロでのろのろ走ってた。
だいたいヤマトの後ろとかにくっついてれば安全運転ができる。


夏休みらしい夏休みは以上で、あとは普通にインターン先で仕事してた。
週末、なんかよくわからないけどC++を勉強しようと思い立って、適当に入門書を買って眺める。
想像以上に面白い言語だったので、食わず嫌いってよくないなと痛感する。
なんで今まで警戒してたんだろう。


日曜日、原宿から表参道、渋谷へとてくてく散歩。
牛カツのお店でお昼、牛タンのお店で夕飯というリッチで胃もたれする食生活を送る。
東京に来てから高いものばっかり食べてて財布は薄くなるし見た目は太くなるし、幸せなんだかわからない。

まあ、美味しいもの食べるのは幸せだからなんでもいいか。

2017/08/07-08/13

インターン先で論文読んだり手を動かしたりしてるうちに、なんとなく自分の研究にもフィードバックできる気がした一週間。
んで、つくってみたモデルが特にいい数値を出すわけでもなくなんなら収束すらしない雑魚っぷり。

頑張ろうな。


機械学習っぽい分野の内容でバズった「パンダの画像にノイズを入れるとテナガザルと判断される」というやつ、アテンションの逆っぽいやつだろうしまあそんなもんだろうなあって感じ。
人間の目にはそこまで違いを感じない範囲でアテンション(ぽいやつ)を調整したのがすごいところなんだと思うけど、その辺が分野外の人に伝わってないような気がする。
なんというか、機械を騙した!みたいなのがバズってんのかなあ。

機械学習に翻弄される人たちを見るのはちょっと面白いので、もっと遊ばれてほしいなと思う。



金曜日が休みだって知らずに一週間が始まって、あ、今週4日だけだ!って嬉しくなった。
大した働きをしてるわけでもないけど、人並みに仕事してるふりしてるのは面白いなあ、という所感。

お昼ご飯で毎日1000円くらい使ってるのも、なんかいい生活だよなあって感じ。
東京に来たら太る、というのは本当にその通りなんだと思う。東京出身だけど。
この調子で食べてたら、また70キロ近くなってしまう。よくない。

金曜日祝日、髪を切ってバッティングセンター行って充実。
奈良で髪を切るとみんな同じ髪型になって戻ってくるので、なんとなく嫌な予感がして東京まで我慢してた。
いつも切ってもらってる美容師さんにお願いしていい感じにしてもらった。

また12月くらいに戻ってくる予定があるので、次はそれくらいかなあ。

バッティングセンター、小学生くらいのお子様がたくさんいて夏休みを実感する。
小学4年生くらいの子達が100km/hの球で連中してる中、修士1年生は90km/hのブースでパカパカうって楽しんでた。
おっさんは速球を投げられないので、遅い球をミートできれば十分。


土曜日、高校の友人二人と焼肉してカラオケする。
なんかこの週間記録を読んでる人が何人かいるらしいって聞いてちょっと恥ずかしい気分になる。
書く内容がなくなったら本当にツイッターレベルの記事しか書かなくなりそう。

友人Mが傷心だったので、肉食べて酒飲んで雑にカラオケで騒いで遊んだ。
人ごとじゃないよなあという内容なので、自分も気をつけないといけない気がする。

久しぶりにカラオケで騒いで、喉がダメダメになった。
奈良で全然声出してなかったせいで、すぐに喉がダメになる。

数ヶ月ぶりに歌って、エモがこみ上げたので曲作りたくなった。
時間は作るものだけど、それでも作曲する時間がないようなあという感じ。
気力と体力を楽器に回してる余裕がない。

なんていってると、すぐにおっさんになってしまうんだろうなあ。
無理してでも弾くようにしないと。

来週はしょっぱなから家族の付き添いで長野に一泊旅行。
ドライバーとして徴収されてるので、美味しいもの食べて疲労を残さないようにする。

2017/07/31-08/06

東京に帰ってきたぞー!

奈良の寮を掃除して、冷蔵庫の中身を消費して、色々と準備をして晴れて帰京。
戻る前に、指導していただいてる先生にどんな感じで研究を進めるか相談する。
とりあえず色々モデル考えて弄ってうまく行くようにしたい。

東京にはインターンしに帰ってきました。
某ITのベンチャーにお邪魔して、いろいろ調べたりします。
学歴採用感が漂ってるけど、頑張っていきたい。

一応朝から夜までフルタイムで平日働くので、暇ってわけじゃない夏休み。
でも奈良の山奥に篭るよりは嬉しいかなあ。

デスクに座ると眠くなる病が発症してて、奈良じゃ研究もろくに手がつかなかった気がするし、心機一転こっちでいろいろやってみる。
とはいえ、実家だと研究のテンションも上がらないので適当にカフェをうろつこうとおもう。

そういえば前回戻ってきたときは深夜バスの弾丸だったのでかなり体力を消費した。
今回はちゃんと新幹線なので体調に問題なし。
やっぱり青春エコドリームしんどかったんだなあ。


土曜日、ちょっと用があって皇居あたりをうろうろする。
あの辺初めて行ったけど、趣があっていいねえ。
東京のど真ん中に、本当に何もないだだっぴろい空間があるってのは面白かった。

日曜日、母校の旧校舎が一部取り壊しになるらしく、最後に部室を惜しんでライブ開催すると聞いて駆けつける。
実は知り合いの高校生もそこに参加するってことで呼ばれてた。
久しぶりに高校生のサウンド聞いて、いいねえ若さって。としみじみしていた。
お節介でレビューをしてしまったけど、こういうのを老害って言うんだろうなあって感じてしまった。

各校の顧問たちが楽しそうに音楽してるのをみて、ああ、教員になればよかったかなあと少し後ろ髪を引かれた。
今の待遇じゃ絶対にならないけど、こういう人生もありだよなあと思った。
恩師は最前線で騒げない歳だし、アラフォーの先生方が最前線になるんだなあ、時間って流れるんだなあと感じる。

同期の部長と一緒だったんだけど、僕は終始彼に向かって「嫌だなあ、嫌だなあ」とぼやいていた。

夜はその彼と、もう一人教育実習経験者の同期とお酒を飲む。
高校時代の友人たちには博士課程に行けと言われ、期待してる言われ、頑張るしかねえよなあ、と背中を押される。

面識はないんだけど、高校の同期のうち二人がすでに亡くなっていると聞いた。
一人は急死、一人は病死だそうで、人ごとじゃないような気がして恐ろしい。
無理はしないようにしないと。

とりあえずバッティングセンターに通って体を動かし続ける。

17/07/24-07/30

試験勉強に尽力する一週間。
といっても、文系出身には数学数学した授業はつらく、早々にリタイヤした授業が多いのでそこまでキツくはない。

同期でとても数学ができる人がいるんだけど、彼が楽しそうに受けてる授業はヤバい、という判断基準で授業を選択している。
地に足についた勉強をしたいので(少なくともつま先くらいはついてるような勉強)、あんまりにも自分の能力に見合わないなと言うものはザクザク切ってる。
なんだかんだ言って単位はつらくないので、どうにかなると思う。

とりあえず研究に時間をガンぶりして、成果を残せたらなと思う。


いま取り組んでる研究について、指導教官に相談する。
先生にとてもまじめに考えていただけて、僕も頑張らないとなと思う。
この夏は、研究でやりたいタスクと似たようなことをインターンでさせてもらえるので、研究につながるようないろんな手法を試してみたい。
んで、夏明けには論文を書けるような状態にしたいね。
頑張ろう。


学部時代お世話になった教授がご結婚されるらしいと連絡が入る。
色々と衝撃的な連絡なので、今年の合宿でぜひお話を伺いたい。
夏の学部ゼミ合宿には、なんとゼミ生が五代も集まるらしく楽しみでしょうがない。
特に僕らはそのど真ん中の世代なので、一番楽しませてもらえるんじゃないかな。
先輩方がどんな生活を送ってるのかも気になるし、この夏が本当に楽しみ。


最近研究室で自分の机に向かうと突然眠気が襲ってくる。
完全に鬱というか、倦怠期と言うか、やる気の出ない周期にハマった。
このタイミングで東京に戻ってインターンできるのは、とてもよかったんじゃないかな。
心機一転、真面目に取り組もうと思う。

こういう風にならないためにも、研究環境を三つくらい用意しておいて、それを転々とすることでモチベ維持を図った方がいいのかなと思う。
といっても、研究室、カフェ、自室くらいしか候補はないんだけどね。

相変わらず野球をやってて、もう真っ黒。
東京に行ってインターン先で、野球やってるせいで黒くって。。。みたいな釈明をする必要があるレベル。
野球をやってるおかげで肩がこらなくなったので、東京でも壁打ちくらいはしたいなと思う。
グローブ持って帰らないと。


日曜日の夜、そういえば自分は言語学をずっとやっていたんだと思い出す。
いま煮詰まってる部分を、言語学者はどうとらえてるんだろうと思って、そのあたりを調べてみようと思う。

2017/07/17-07/23

予備実験の数値がうまく出ないし、なんか何したらいいのか分からないしで苦しい一週間だった。
結局予備実験の数値問題はコードのバグで、こんなしょーもないことに1週間つかったのか、と途方に暮れる。

論文の再実装をするのは目的が明確なのでモチベーションを保ちやすいんだけど、自分でモデルを考えるとなると兎角難しい。
考えてる時間の、脳内では進捗があるんだけど目に見える進捗が無い感じに慣れないといけないんだろうなあ。

研究室でBBQをする。
下っ端なのでセッティングとかいろいろしたんだけど、想像以上にたくさんの人が参加してくれてうれしく思う。
なんというか、面倒くさいことは嫌いだけど、自分らのセットした会場で人が楽しんでるのを見るとやってよかったなあと感じる。
先輩方には気苦労するタイプとか、自己を殺すタイプとか言われたけど、確かにその通りだなあ。

実は昔から根が真面目で、人に気を使って生活するのに疲れてたきらいがある。
学部3年くらいの頃にそれじゃあ自分がつぶれると思って、意識して自分勝手に生活するようにしてるんだけど、こういう当番制のイベントとかでうっかり素が出てしまうんだろうなあ。
もっと奔放な人間を模倣したい。

久しぶりにradikoを流して、色々曲を漁っていた。
というのも、デスクに向かってもあんまりやる気が出ず、ぼーっとツイッターを眺めたり、youtubeを眺めたりしてるだけだったので、音楽とかの収穫はかなりあった。
いちおう音楽を流すと、まあなんか論文でも読むかって気持ちになるのでまだモチベーションは無くなってない。

週の後半に先輩から聞いた噂で、指導していただいてる方が、僕を教育しがてらその内容で賞を狙ってる、みたいなことを聞く。
期待されてる、という感じ方は自意識過剰だけど、そんなに真面目に考えてくれてるんだなあと感動する。
モチベーション爆上げで予備実験もろもろ終わらせる。

がんばるぞー。


iOSアプリのグランドセフトオート、今までvice cityを入れて暇なときにストレス発散してたんだけど、さすがに飽きてきたので今度はSAを入れてみた。
マップ広くて超感動した。
我が家にはプレステ系がほぼなかったので、グラセフもオンタイムでやってない。
なので今更ながらグラセフを楽しむ22歳。
夜更かしが危険。

2017/07/10-07/16

進捗だめですって感じの一週間だった。

クラッチで深層学習書くのに疲れたので、chainerを使ってみることにした。
気になってたんだけどお金がなくて買えなかった二冊を購入し、読みふける一週間。
こういう週があってもいいよなと、自分に言い聞かせる。

Chainerによる実践深層学習

Chainerによる実践深層学習

深層学習による自然言語処理 (機械学習プロフェッショナルシリーズ)

深層学習による自然言語処理 (機械学習プロフェッショナルシリーズ)

久しぶりに技術書を真面目に読みふけったけど、やっぱり楽しいよね。
論文と違って伝えようとしてくれてるので読みやすいし、こういう技術書を書けるようになりたいなと思った。

というのもあって、このブログにそんな感じの記事を投稿した。
koguma-goya.hatenablog.com

この週の後半は、ほとんどこれを書くために使ってた気がする。
研究室の勉強会で簡単なbotを作って見せる機会があって、それの流用だけどね。
こうやって噛み砕いて説明する練習は、すればするほど説明がうまくなっていくと信じてる。

結果的にプレゼン能力の向上にもつながるだろうし、今後も続けていきたい。



8月に東京に戻るので、新幹線の切符を取りに行った。
多少は覚悟してたけど、奈良駅まで行かないとみどりの窓口が無いってのはかなり厳しい。
片道1時間以上かかるので、なんで新幹線の切符買うだけなのにこんなに遠出しないといけないんだ。。。とつらい気持ちになる。

JRのウェブ予約サービスを使ってみようとしたら、なんか新しくクレジットを作らないといけないとか言う謎仕様だったのであきらめた所存。
僕がちゃんと読んでないだけでクレジット登録でもできたのかなあ。

でも、予約したいときにすぐ予約できないあのシステムはちょっと微妙だと思う。
「あ、新幹線予約しないと」ってウェブ見たら「クレカの審査に一週間、電子切符の発行に三週間かかります!」とか書いててさすがに頭弱いんじゃないかって思った。
どんな人が新幹線の予約サイトを探すのかをもうちょっと考えた方がいいのでは、という感じ。


夏のインターン先の担当さんとお話させてもらう。
単語分割をやりたいんだ!と面接で我がまま言っていたが、なんと単語分割のタスクで勉強させてもらえることになった。
非常にモチベーションが上がったので、頑張りたい。



研究室ではプロコン勉強会が週に一回ある。
最近、毎週主催してた方が某大企業にインターンに行ってしまったので消滅しかけてたんだけど、強い中国人の先輩が引き継いでくれたおかげで無事勉強会が存続した。
動的計画法に強くなる、というコンセプトでやるらしく、とてもありがたい。
といっても、今週はすこしやってあとは3時間くらい「しにてえなぁー」って愚痴るだけの会だったけど。

その先輩によると、中国に旅行に行くなら今のうちだよ、とのこと。
政治的に色々あるらしくって、どこの国も大変だなあと思った。


今週、また言語の広がりシミュレータの動画がランキングに載ったらしい。
いまだにニコニコ動画のフォロー通知が来てすごいなとおもう。
我ながらニッチな動画だと思うんだけど、想像以上の人数に楽しんでもらえてうれしい。
より多くの人に言語学とか、そのへんの学問を楽しいと感じてもらえれば本望。

自然言語処理入門としての「botの作り方」


この前の記事とかツイートででぼやいてたことを、記事にして供養します。
本当は中学生くらいを対象にした教育動画として公開したかったんだけど、ちょっと作ってる余裕が無いのでとりあえず記事にします。

本記事は以下の内容で構成されています。


使用する言語はpythonです。
もしpythonを触ったことが無い、という方は以下のサイトを参考にすこしだけ勉強するとストレスなく読めると思います。

Python入門

0. 自己紹介

山奥にある研究室で自然言語処理学の勉強をしている熊です。
研究室の名前が偉大すぎて最近能力を過大評価されがちだけど、ぶっちゃけただの文系のポンコツ
学部では理論言語学を勉強していました。専門は統語論でした。英語の教員免許を一応持っています。
数学は苦手です。よろしくお願いします。


1. はじめに

この記事ではtwitterで散見されるbotを作ってみることで、言語処理の面白さを体験することを目的としています。

主に文系プログラマの卵をターゲットとしており、特に

・数学がちょっとよくわからない
・プログラミングが少しだけできる
・最近言語処理に興味を持って色々記事とか読んでる
・でも数式ばっかりでよくわからねえよ!!!

という方を対象にしています。そう、そこで頷いてるあなたです!

僕自身、学部2年のころに言語処理に興味を持ち、プログラミングを始めました。
でも言語処理系の記事の数式って謎なんですよね。最初の一年はつらい思いをしてました。
なにかとっかかりがあれば数式もイメージがしやすいのですが、最初のとっかかりってのは中々訪れません。
この記事が「よくわからないけどわかりたい」という人の役に立てばと思います。

数式や証明の類は一切載せませんので、数式のほうがわかりやすい!という方にはお勧めできない記事となっています。
上でも述べた通り、実装はpythonで行います。
4節まではプログラミングの話題に触れないので、もしプログラミング未経験の方は3節までを読み、あとは結果だけ見てもいいと思います。

ソースコードgithubに載せておいたので、みながら動かしてみてください。
https://github.com/mentaikoguma/bot_tutorial


なお、この記事ではツイッターとの連携部分については説明しません。
あくまでも、文生成についてのみ説明します。


2. botの分類

最初に、ここでいうbotとは何か、どのようなものがあるのかをまとめておきます。

ここでのbotとは、文章を出力するプログラムの事を指します。
例えばボタンを押すたびに何かしらの文が表示される、twitterで00分になると自動で文がツイートされるものなどをbotと呼ぶことにします。
そのうえで、botを二種類に分類します。

a. 記述型

作者があらかじめ用意した複数の文から、ランダム(あるいはルールに基づいて)ひとつを出力するものを、このように呼ぶことにします。
具体的には以下のbotなどが参考になります。

ワイト (@waightthinksso) | Twitter

リラックマfakebot (@rilakkuma_bot) | Twitter

これらは事前にセリフなどを用意しておき、それをランダムにひとつ選ぶことでツイートを行っています。
@waightthinkssoはテキストが一つしかない例、@rilakkuma_botリラックマの絵本の中のセリフを用意しているようです。
リラックマbot、いつまで生き延びるのかな)

また、記述型を応用することで対話っぽい事が出来ます。
「おはよう」「ねむいね」などのテキストを用意しておき、「おはよう」を含む文には「おはよう」と返すようプログラミングすれば、なんとなく会話をしている気分になります。
興味がある方は作ってみるといいと思います。テキストの用意がすごくつらいです。*1

ちなみに、記述型のtwitter botwebサービスを用いて簡単に作ることができます。
もしプログラミングなんてしたくないわ!って方は以下のサービスを利用してみてください。
(詳細な使い方はここでは行いません。)

twittbot - enjoy


b. 生成型

記述型と違い、作者が事前に文を用意しません。
例えばtwitterのタイムラインを1時間分溜めておき、それをもとに学習・文生成を行うものが挙げられます。
事前に文を用意しないので、同じ文が出力されることはほとんどありません。
「どのような文が生成されるのか」という点で非常に見ていて面白いbotとなります。

しゅうまい君 (@shuumai) | Twitter

最も有名なこの手のbotはしゅうまい君でしょう。
しゅうまい君はある一定時間のあいだ、フォローしているユーザーのツイートを分析、学習し、それをもとに文を作っています。
ご存知の方も多いと思いますが、非常に人間らしい文が生成されます。
この記事では、しゅうまい君と同じ挙動をするbotを作ることを目標とします。
詳細は次の節からお話します。


3. 文の生成

この節は、2節で紹介した生成型botがいかにして文を生成しているのかについて理解してもらうことを目標としています。
まず、しゅうまい君のホームページに書いてある挙動について紹介します。

しくみ
1.フォローしている人の発言を拾って形態素にばらして溜める
2.マルコフ連鎖で複数の短文を自動生成
3.適当な短文を選んでTwitterにつぶやく
(略)

…どうでしょうか。
この記事では自然言語処理の知識がほぼゼロ、という方を対象にしているので用語の説明をしっかりと行います。
(引用部分を見ただけで何を作ればいいのかわかったぜ!という方はもう読まなくて大丈夫です。)


形態素

言語の意味を持つ最小単位を形態素と呼びます。
例えば、「形態素」「と」「呼び」「ます」「。」などが形態素になります。
形態素にばらす」とは、単語分割を指しています。*2
というのも、日本語は英語と違って単語の切れ目がスペースによって明示されていないので、機械が処理するには少し不便なのです。
分割の例を以下に示します。

神様、僕は気づいてしまった*3
  ↓
「神様/、/僕/は/気づい/て/しまっ/た」

/によって形態素が区切られ、どれが単語なのかが分かるようになりました*4
これを単位として、言語処理を行います。
ちなみに、分割はpythonのパッケージをもちいて簡単に行えます。


マルコフ連鎖

これが生成部分を指す用語になります。自然言語処理では特に言語モデルとか、n-gramとかって言われることが多いです。
情報系であればよくご存じの単語だと思いますが、簡単に言うと

直前の数単語から次の単語を予測するという生成方法です。

この記事ではこの部分に重点を置いてしっかり解説したいと思います。
下線部の説明でもうわかったぜ!という方は、このまま実装の節まで進みましょう。


マルコフ連鎖(あるいはn-gram言語モデル)を体験してもらいます。
(数式を一切用いず説明するため、ここからの説明は冗長になります。)

Q. 以下の空欄を適当な形態素(単語)で埋めよ。ただし空欄以降も文が続く可能性がある。

私は(  )

空欄にはいろいろ入ると思います。
例えば「私は太郎」「私はペン」「私は食べ」などがあり得ますよね。
それぞれ、「私は太郎です」「私はペンを持っている」「私は食べることが好きです」のように派生する可能性があります。
ここでは「太郎」を入れたことにしてもう一度同じ問題を考えてもらいます。

Q. 以下の空欄を適当な形態素(単語)で埋めよ。ただし空欄以降も文が続く可能性がある。

私は太郎(  )

ここでの解答例は「私は太郎を」「私は太郎が」「私は太郎の」「私は太郎を」などになるでしょう。
間違っても「私は太郎ペン」や「私は太郎食べ」とはしませんよね。

カッコ内にどのような単語を入れるべきかは、直前の数単語に強く依存していることが分かると思います。
より一般化して、「次の状態は、直前の状態によって決定される」という状況をマルコフ性と呼びます。
これを繰り返すことで、一つの状態の遷移(ここでは単語列)が生成されます。この連鎖の事をマルコフ連鎖と呼びます。


実際に実装する際には、「ある数単語の後にどの単語が出現したか」の頻度辞書を作り、生成を行います。
上の形態素の例だと、以下のような辞書ができあがります。

「神様/、/僕/は/気づい/て/しまっ/た」

<BOS><BOS> 神様:1
<BOS>神様  、:1
神様、   僕:1
、僕    は:1
僕は    気づい:1
は気づい  て:1
気づいて  しまっ:1
てしまっ  た:1
しまった  <EOS>:1

ここで、<BOS>と<EOS>はbegin/end of sentenceで、文頭と文末を表す特殊記号として扱われるものです。
また:1はそれぞれの単語が、直前の二単語の次に1回現れたことを表しています。
すなわちこの辞書は、
文頭として「神様」が1回
「(文頭)-神様」に続いて「、」が一回、
「神様-、」に続いて「僕」が一回
...
という情報を表していることになります。

このように、直前の二単語の次にどの単語が来るかを数え上げるものを3-gram(tri-gram)言語モデルと呼びます。
(直前のn-1単語から次の語を予想する。)

少しはしっくり来たでしょうか。
次の節ではこれらを実際に実装してみましょう。


4. 実装

言語はpythonを用います。
記事の最初にある通り、一応ざーっと基本的な動作を確認してから実装してみることをお勧めします。
命名規則Java臭がありますが、大目に見てください。
また、エンジニアとしてのお作法が成ってないってのも見逃してください。

データセット

本当はツイートデータを使って実装できればいいのですが、ツイートのデータは二次配布ができないので青空文庫で我慢しましょう。
この記事の内容を実装してみて、さらにツイートでも同じことをやってみたいという方は、twitter developerとか使ってクロールしてください。
(詳細の説明は今回はしません。)

Welcome — Twitter Developers


今回使うデータセットは、夏目漱石の以下の文献にします。
・それから
吾輩は猫である
・こころ

上のテキストをすべてまとめたsoseki.txtをこちらからダウンロードして使ってください。
bot_tutorial/soseki.txt at master · mentaikoguma/bot_tutorial · GitHub

これを使って、今回は実装します。

Janome形態素解析ライブラリ)

前の節で単語分割についてお話しました。
pythonで単語分割をするにはJanomeというパッケージが必要です。

公式
Welcome to janome’s documentation! (Japanese) — Janome v0.3 documentation (ja)

解説
python で形態素解析。Janome が簡単。pip 一発でインストール | コード7区


基本的にはpipで入ります。

pip -install janome

windowsの場合はpipが使えないことがあるので、以下の記事を参照して頑張ってみてください。
windows環境のPython3.4でpipをつかってパッケージをインストールする - 意味悲鳴


学習部分を作る

これで準備が整いました。
あとはゴリゴリ書いていきましょう。
再掲しますが、githubにコードを載せてあります。
これを上から順になぞっていきます。
一応メソッドごとに区切って載せていきます。

import random
from collections import defaultdict
import janome
from janome.tokenizer import Tokenizer

インポートは上の通りです。ほかは使いません。
janomeは単語分割、defaultdictは辞書の初期化、randomは生成部分で使います。



●テキスト読み込み部分

def read(path):
  f = open(path, 'r')
  arr = []
  for line in f:
    arr.append(line.strip()) #改行文字を削除して配列に入れていく
  return arr

先ほど作ったsoseki.txtや、あとで作られる辞書ファイルを行ごとに読み込みます。
strip()では改行文字を削除しています。


●辞書作成部分

#readで読み込んだarrをdataとして受け取り、n-gram辞書を作る
def getDictOf(data, n):
  ngramDict = defaultdict(lambda:defaultdict(lambda:0)) #辞書の辞書を初期値0で設定
  t = Tokenizer() #Janomeをセット
  for sentence in data: #各文ごとに
    words = t.tokenize(sentence, wakati=True) #文を単語のリストにする
    for i in range(n-1): #<BOS> <EOS>を追加
      words.insert(0, '<BOS>')
    words.append('<EOS>')
      
    for indice in range((n-1), len(words)):
      biginOfPrev = indice - (n-1)
      prevWords = ''.join(words[biginOfPrev:indice]) #indice番目の単語の直前n-1単語の連結
      targetWord = words[indice] #indice番目の単語
      if prevWords.strip() and targetWord.strip(): #直前の単語、indice番目の単語がともに空白じゃないとき
        ngramDict[prevWords][targetWord] += 1 #辞書の値を++
  return ngramDict

生成に使う辞書を作る部分です。
上で説明した通り、n=3(3-gram / tri-gram)の時には直前2単語から次の単語を予測します。
つまり、ある二単語の次にどの単語が何回現れたかを辞書として記録すれば、確率としてあとで使うことができます。
defaultdictは初期値を設定するもので、よく使うメソッドです。
ここでは辞書のvalueを辞書にし、そのvalueを0として設定しています。
また、<BOS>をn-1個挿入することで、文頭のtri-gram確率を計算できるようにしています。
(先ほどの"<BOS><BOS> 神様:1"を思い出してください。)


●辞書保存部分

def write(path, data):
  f = open(path, 'w')
  for key1 in data:
    line = '%s\t' % key1
    for key2 in data[key1]:
      # key1[tab]key2/:/value/,/key2/;/value という形で保存する
      line += '%s/:/%d/,/' % (key2, data[key1][key2])
    line = line[:-3]
    f.write(line+'\r\n')

作成した辞書を保存します。
今回くらいの辞書であれば、毎回作ってもそこまでストレスではありません。
しかし、テキストが大きくなる場合は辞書を作るだけで数十秒、数分かかります。
そのため、一度作った辞書は外部に保存し、あとで読み込み直した方が便利です。
writeメソッドでは、辞書の直前n-1単語部分をkey1、直前単語が直後にとりうる単語をkey2としてforを回しています。
後でsplitして読み込みやすいよう、/:/、/,/という適当な記号で区切っておきます。*5

一度プログラムを動かしてみましょう。
ここまでのメソッドの下に以下のように記述し、プログラムを動かしてみてください。

n = 3
data = read('soseki.txt')
write('%d-gram.txt'%n,getDictOf(data, n),)

'%n-gram.txt'%nは、n-3のとき"3-gram.txt"という名前になります。
出来上がった辞書は以下のようになっているはずです。
(ソートをしていないため、作成するたびに辞書の見出しの並びは変わってしまいますが、大体一緒ならうまくいってるはずです。)

がなるべく	押し/:/1
「乱暴	だ/:/1
だけ感じ	た/:/1
という大丈夫	な/:/1
ず寐	る/:/1
真面目くさって	聞く/:/1/,/述べる/:/1
...
生成部分を作る

今度は作った辞書をもとに生成部分を作っていきます。

●辞書読み込み部分

def readDict(path):
  ngramDict = defaultdict(lambda:defaultdict(lambda:0))
  data = read(path)
  for line in data:
    key1, contents = line.split('\t')
    contents = contents.split('/,/')
    for content in contents:
      key2, value = content.split('/:/')
      ngramDict[key1][key2] = int(value)
  return ngramDict	

先ほどのwriteメソッドの逆の手続きで、辞書を読み込みます。


●生成部分

def generate(ngramDict, maxLength, n):
  sentence = ['<BOS>' for i in range(n-1)]  #最初の一単語を予測するためにn-1個の<BOS>で埋める
  while True:
    prevWords = ''.join(sentence[-(n-1):]) #現在の最後のn-1単語
    wordCands = [] #単語候補を入れるリスト
    for key2 in ngramDict[prevWords]:
      for i in range(ngramDict[prevWords][key2]):
        wordCands.append(key2) #辞書にある数字だけ単語候補を入れる
    if wordCands:
      random.shuffle(wordCands)		
      sentence.append(wordCands[0]) #シャッフルして一つ取り出し、文に追加
		
      if wordCands[0] == '<EOS>': #<EOS>を取り出したら文終了
        return ''.join(sentence[n-1:-1]) #<BOS><EOS>を取り除いて連結し、リターン
      elif len(sentence) > maxLength:
        return generate(ngramDict, maxLength, n) #指定した文字数を超えたら生成し直し
    else:
      return generate(ngramDict, maxLength, n) #単語候補が無い場合は生成し直し
  return none

いよいよ生成部分です。これさえかければもうbotは完成します。
上から順に追っていきましょう。
先ほど読み込んだ辞書を使って、文の生成を行います。

次に最初の一語を選ぶために、文を<BOS>で満たします。tri-gramの場合、<BOS><BOS>に続く単語が文頭になります。*6
辞書から<BOS><BOS>に続く単語を取り出し、wordCandsに追加していきます。
このとき、辞書にある出現回数ぶんだけ追加することが大切です。
文頭になりうる単語をすべてwordCandsに追加したとき、リスト内は「学習データでより多く観測された文頭の単語ほどたくさん入っている」という状態になっています。
これをシャッフルし、一つ取り出すことで、観測した出現頻度から作った確率分布のもとで抽選を行うことと等しくなります。

たとえば辞書の<BOS><BOS>の値が以下のようであったとします。

 私:4, 太郎:3, リンゴ:2, 見る:1

このとき、文頭になりうるwordCandsの中身は以下のようになります。

wordCands = [私,私,私,私,太郎,太郎,太郎,リンゴ,リンゴ,見る]

ここから、ランダムに一つ取り出すことで文頭を決めます。
必然的に、「私」を取り出す確率が高くなることは直感に従うでしょう。

これを繰り返し、文を生成していきます。
場合によっては無限に文が続いてしまうことがあるため、最大文字数は必ず設定するようにしましょう


あとは、これを適当に生成するようにプログラムを書くだけです。
例えば10個の文章を生成してほしければ、以下のように記述します。
maxLengthはツイートサイズと同じ140としました。

n = 3
maxLength = 140
ngramDict = readDict('%d-gram.txt'%n)
for i in range(10):
  print(generate(ngramDict, maxLength , n))


エンターキーを押すたびに生成するタイプならこうなります。

n = 3
maxLength = 140
ngramDict = readDict('%d-gram.txt'%n)
while True:
  s = input()
  if s == 'exit':
    break
  print(generate(ngramDict, maxLength , n))


生成結果の一例です。

「そりゃ無論さ。すると勝手の方にまだ這入って行く様に話す話さないのです。彼らは真
直に自白します。

「ええ顔を鏡で、どうだと主張する。主人は無言の感謝を改めて「さっきから云わんより
寧ろあの時の君の声で、滑稽と崇高の大差を来たした。

母は私の従妹に当る人のために」

こんな苦情をいうように読み出す。「それが唯動くものと思っていた」

私は退屈そうにもならず、真白なシャツに卸立ての四つばかりの金を貰い、三千代のいる
学校はどうしたら三寸も離れて、広い若葉の園は再び出ているようでした。

「いい積りだのかといったら、月桂寺さんは大に活動して嘆願に及んで来て、巻煙草の吸
い殻を蜂の巣のごとくにこにこと落ちつき払っているが、)父は、大分変って来ました。
私はそれを知っているとしか見えなかった。

よく分からない文ですが、前後3単語程度の範囲だけを見れば文法的になっていることが分かります。
今回はデータが夏目漱石なので硬い雰囲気になっていますが、ツイートなどで辞書を作れば当然、ツイートっぽい文を生成してくれます。


5. まとめ

この記事ではbotについて紹介し、中でも生成型のbotについて「数式を除いて」説明しました。
すこしでも理解の助けになればと思います。
もちろん、この記事でなんとなくイメージが付いたら、つぎは数式の理解のステップに進んでもらいたいと思います。
基本的な自然言語処理の用語や実装は、グラム先生のこちらのチュートリアルがとてもためになります。
Graham Neubig - チュートリアル資料

ちなみにマルコフ連鎖には重大な問題点があります。
ツイッターのデータを集めて学習するとき、その性質上
「元のツイートと全く同じ文」が生成されてしまうことが良くあります。
これを解決するためには、また別の言語モデルを使ったり、生成方法を変えてみたりする必要があります。
もし興味のある方は、この辺も考えて手直ししてみるといいでしょう。


僕もまだまだ初学者ですが、かつて躓いてた自分に説明するような感じでまた記事を投稿すると思うので、その時はまたお願いします。
「これの概念がよくわからん」みたいなメッセをついったーとかに送ってくれたら、僕の理解の範囲で説明してみるかもしれません。

最後に、この記事を読んでいて「ここがわからなかった」という部分があったらコメントで指摘してください。
僕も伝えることの練習として記事を書いているので、改めてその部分を書き直してみます。

では、よい言語ライフを。

*1:記述型のbotで有名なものとしてELIZAが挙げられます。

*2:余談ですが、僕は単語分割のタスクで研究しています。

*3:神様、僕は気づいてしまった - CQCQ - YouTube

*4:分割方法について興味がある方は、こちらのチュートリアルを読んでみてください。

*5:ツイートなどを扱う場合は、/:/や/,/といった文字列がテキストに含まれてる場合は弾く、といった処理をしなければ、読み込みの段階でバグってしまいます。

*6:別に<BOS>に続く単語でも確率は変わりませんが…