文学フリマに向けてHeokuとPythonを用いた小説風文章生成アプリ『Bungoo』を太宰治モードにしました
2015/11/24に行われる文学フリマ東京に向けて、HeokuとPythonを用いた小説風文章生成アプリ『Bungoo』を太宰治モードにしました。
http://bungoo.herokuapp.com/Bungoo/
ある程度文章を入力してから"Shift + Enter"を押すと、その語の後に出やすい単語を表示するアプリケーションです。
今回の文学フリマでは『実践 太宰治』という本を出すということで、 それに合わせて『Bungoo』のほうも太宰治のテキストを青空文庫から抽出してリコメンドに利用するように修正しました。
『実践 太宰治』では、こちらのアプリケーションのロジックの基礎となる部分について 掲載する予定です。もしご興味ありましたら、ぜひ手にとってみてください。よろしくお願いします。
※なお、ソースコードはこちらです。
太宰治小説の単語バイグラムを用いた執筆補助プログラムの作成
(以下の文章は文学フリマに出品する『実践 太宰治』の原稿です。)
はじめに
前章では太宰治『人間失格』のテキストに用いられている単語を抽出し、可視化を行った。これで、太宰治の小説構造への理解が、多少なりとも深まったかもしれない。ここでもし、あなたが創作に興味を少しでも持っているならば、「太宰治のような小説を書くにはどうすれば良いだろう?」と思うこともあるだろう。そこで本章では、太宰治のテキストを利用し、太宰治の小説に含まれる単語をサジェストする、小説の執筆補助プログラムを作成していく。
概要
例えばここに、『今日私はラーメンを食べた』という文章があったとする。この文章を単語毎に、「今日」「私」「は」「ラーメン」「を」「食べた」と分割する。この時、これらの単語をランダムに並べ替え、「は」「私」「今日」「食べた」「を」「ラーメン」とすると、日本語としておかしい文章になってしまう。ある日本語の文を生成するためには、単語を適切な順番で並べる必要がある。それでは、プログラミングによって正しい語順を得るにはどうすれば良いのだろうか。可能であるならば、日本語の文法規則を網羅した「日本語の文法に従って文章を組み立てるプログラム」を作成したいところだが、それには、文法規則を自分たちで学び、プログラムに落としこむ手間がかかる。しかし、そんなことをせずとも、すでに存在する文章を用いることによって、簡単に比較的日本語らしい文章を生成することができる。
例えば、「私」という単語があったとき、次に結合する単語としては、「は」「が」「を」などの助詞が想定されるだろう。もちろんこれらの「私は」「私が」などの繋がりは、日本語の規則に従っているものであると同時に、実際に一般的な日本語の文章の中で用いられているものでもある。つまり、ある日本語データの中から、「私」とその後に続く単語のリストを抽出すれば、「私」の後に接続しやすい単語を得ることができる。あとはこのリストを用いて、ある入力された言葉から、先に続くことばを推定し、利用者に対して表示するプログラムを作成すれば良い。
それではどのようにすれば、適切な単語のリストを作成することができるのだろうか。 例えば、「太宰治の小説」と「Amazonの口コミデータ」をひとつずつピックアップした際、どちらが「太宰治の小説」でどちらが「Amazonの口コミデータ」か判別することは、大抵の人には容易なことだろう。このような分類が行えることの非常に単純な仮設として、「太宰治の小説」と「Amazonの口コミデータ」では、用いられている単語の種類が異なることが考えられる。逆に考えると、「太宰治の小説」で用いられている単語を用いることで、太宰治の小説らしい文章が作成できる可能性がある。
例えば、太宰治の作品から単語のリストを得た際、それらを用いたプログラムにおいて期待される入力と出力は、
input 「人間」 output 「失格」 input 「ヴィヨン」 output 「と」 input 「ヴィヨンと」 output 「妻」
というようなものであるということにする。これらの入力と出力の組合せは、意味の成立している文章を作成する用途にはいささか心もとないが、執筆が暗礁に乗り上げた際のひとつのサジェスチョンとしては、効果のあるものになる可能性がある。それでは以下、実際に以上のような機能を実現するプログラムを作成する。
実装
プログラミングには、python3.4.3を用いる。それではまず、以下のように、文章を各単語に分解したリストを用意する。
wordlist = ['たとえば', '、', '私', 'が', 'この', '写真', 'を', '見', 'て', '、', '眼', 'を', 'つぶる', '。', '既に', '私', 'は', 'この', '顔', 'を', '忘れ', 'て', 'いる', '。']
そして、辞書オブジェクトを用意し、 {単語:[単語の次の単語]}となるように値を代入していく。ここで、ある単語によって取得される値は、リストになることに注意してほしい。つまり、上のような文字列に対しては、{"私": ["が", "は"]}という、2つの値を含んだリストが最終的に得られる必要がある。
この処理を実現したのが以下のプログラムだ。
def make_bigram(wordlist): bigram = {} key = wordlist[0] for word in wordlist[1:]: # keyがbigramに存在しない場合、 # 辞書にkeyを追加 if key not in bigram: bigram[key] = [word] else: bigram[key].append(word) # keyを更新 key = word return bigram wordlist = ['たとえば', '、', '私', 'が', 'この', '写真', 'を', '見', 'て', '、', '眼', 'を', 'つぶる', '。', '既に', '私', 'は', 'この', '顔', 'を', '忘れ', 'て', 'いる', '。'] make_bigram(wordlist)
これによって作成されるデータは、以下となる。
{"つぶる": ["。"], "いる": ["。"], "が": ["この"], "この": ["写真", "顔"], "忘れ": ["て"], "たとえば": ["、"], "て": ["、", "いる"], "私": ["が", "は"], "見": ["て"], "写真": ["を"], "顔": ["を"], "を": ["見", "つぶる", "忘れ"], "は": ["この"], "、": ["私", "眼"], "。": ["既に"], "眼": ["を"], "既に": ["私"]}
これは、単語バイグラムと呼ばれる構造だ。例えばこの辞書に、"私"と入力すると、"私"に続く全ての単語を含んだリストが取得できる。それでは、『人間失格』の単語バイグラムを作成してみよう。
まずは以下のように、igo-pythonという形態素解析ライブラリを用いてテキストデータの分かち書きを行う。
def wakati(text): """ "私は" -> ["私", "は"]というように 分かち書きされる。 別途ipa辞書のインストールが必要なため注意。 """ import igo t = igo.Tagger.Tagger('ipadic') return t.wakati(text)
あとは、入出力部分を作成するだけだ。
def main(): # 『人間失格』のテキストデータを取得 with open("ningen.txt", "r") as f: text = f.read() # 単語を分かち書き wordlist = wakati(text) print(wordlist) # 『人間失格』バイグラムを作成 bigram = make_bigram(wordlist) txt = "" while True: sys.stdout.write(">>" + txt) # 入力 inp = input() if inp == 'q': break # テキストを更新 txt = txt + inp # 最後の単語を取得 last_word = wakati(txt)[-1] # バイグラムからデータを取得 print(set(bigram.get(last_word, "")))
それでは、実際にこのプログラムを動かしてみよう。
デモ
例えば以下のような文章を作成している途中、次に記述するべき単語がどうしても思い浮かばなかったとする。
今日は良い天気だ。私は
先ほど作成したプログラム上で、先ほどの文章を入力してみる。すると、
{'油絵', '実に', '思っ', 'いちど', '無い', 'いっ', '思い出す', '雪', '持ち前', '竹', '使っ', '黙っ', '薄れ', '過失', '眼', ----(中略)----, '理解'}
出力として、”は"という単語の後に『人間失格』内で用いられている単語がリストアップされる。 試しに、リストアップされた単語の最初のものだけを選択し、入出力を行っていく。
今日は良い天気だ。私は油絵の予言を警戒し強烈に
生成された単語の前後の繋がりが、日本語として適切であることが確認できるだろう。文章のヒントが得られたら、その後は自分で執筆を続けてみよう。もしかしたら過去の文豪に匹敵する文章が完成するかもしれない。
以上のように文章作成の際にプログラムを用いることで、執筆の際のアイディア源を増やすことができる可能性がある。創作に詰まってしまった方は、ぜひ以上のような方法を試し、新たなる創作の糧としていただきたい。
※ソースコード・デモは以下
http://bungoo.herokuapp.com/Bungoo/
- 作者: Steven Bird,Ewan Klein,Edward Loper,萩原正人,中山敬広,水野貴明
- 出版社/メーカー: オライリージャパン
- 発売日: 2010/11/11
- メディア: 大型本
- 購入: 20人 クリック: 639回
- この商品を含むブログ (44件) を見る
文学フリマ同人誌『実践 太宰治』の表紙ができました
画像が非常に残念なかんじですが、 直す気持ちが湧き上がらなかったらこのまま文フリに参戦します。
以上、宜しくお願いいたします。
- 作者: 石本敦夫
- 出版社/メーカー: オライリージャパン
- 発売日: 2014/09/18
- メディア: 大型本
- この商品を含むブログ (2件) を見る
PythonとElasticSearch、Kibanaを用いた太宰治小説の可視化 その2
(この文章は秋の文フリ用の原稿です。)
Kibana
前章(http://boonlab.hatenablog.com/entry/2015/10/26/000958)では、ElasticSearchへの小説データのインポートを行った。 それでは、それらのデータをデータ可視化ツールであるKibanaを用いて可視化していきたい。
まずは、https://www.elastic.co/downloads/kibanaからkibanaをダウンロードし(2015/11時点ではKibana 4.1.2)、展開したディレクトリ内の
bin/kibana
を実行してみよう。その後、ブラウザでhttp://localhost:5601にアクセスすると以下のような画面が表示される。
ここで、"Index name or pattern"に"dazai-demo"(indexの作成先)と入力して、Createを押下すると、インデックスがKibanaに登録される。
次に、Kibanaの左上に表示されている"Discover"を押下する。ElastickSerachへのデータのインポートに成功していれば、ここでデータを見ることができる。
例えば、"Selected Fields"にtextとpageを設定すると、以下のような画面となる。
ビジュアライズ
まずは、今回用いたテキスト『人間失格』の、頻出単語をヒストグラムにしてみよう。
Kibanaの画面から"Visualize"を押下すると、
のように、可視化方法を選ぶ画面に遷移する。ここで、"Vertical bar chat" → "From a new search"と進んでいくと、ヒストグラムの要素を設定する画面に遷移する。
ここで、Y軸を単語数、X軸を各単語(頻度順)で表現したいとする。 これを行うのは非常に簡単だ。 まず、Y-AxisをCountに設定する。その後、buckets → X-Axisから
ラベル | 設定内容 |
---|---|
Aggregation | Terms |
Field | text |
Order | Top |
Size | 60 |
Order By | metric: Count |
と設定する。
その後、左上にある右向きの三角形(再生ボタンに似ている)を押下してほしい。 以下のような画像が表示されるはずだ。
画像が表示されたら、右上の"Save Visualization"から、このグラフを保存しておこう。
それでは、グラフを見て欲しい。"た"、"だ"、"て"のような単語に紛れて、一番右に"人間"という単語がリストに(恣意的に!)含まれている。これは本文では、『もはや、自分は、完全に、人間で無くなりました』や、『自分には、淫売婦というものが、人間でも、女性でもない、白痴か狂人のように見え、そのふところの中で、自分はかえって全く安心して、ぐっすり眠る事が出来ました。』といった描写に用いられる、非常に重要な単語である。
それでは、次の章ではこの『人間失格』という小説の中で、"人間"という単語がいつ、どの程度用いられるかを可視化する。
小説内における単語の頻度推移
それでは、ページ毎の"人間"という単語の頻度の推移を追ってみよう。
まずは先ほどの手順同様、新しく"Vertica Bar Chart"を作成してもらいたい。
前回、pythonによってページ毎にテキストを分割していた。今回のx軸はページ、y軸を"人間"の単語数とする。
今回もY-AxisをCountに設定し、buckets → X-Axisは
ラベル | 設定内容 |
---|---|
Aggregation | Histogram |
Field | page |
Interval | 5 |
と設定する。
そして、上部の検索窓に"人間"と入力して、グラフを生成して欲しい。下のようなグラフが生成されるはずだ。
検定は行わずに印象で判断すると、小説の後半数十ページ、"人間"という単語が使われる頻度が激減している。これが、作中の主人公の人間性と関連していたら非常に面白いのだが、残念ながらそのような結論を導くにはもっと精緻な分析が必要であろう。しかし、このようなレベルの可視化でも、どのように太宰治の小説を読んでいくかのヒントの一つとすることはできるかもしれない。
おわりに
以上、PythonとElasticSearch、Kibanaを用いて太宰治『人間失格』の可視化を行った。このような解析が、オープンソース(プログラムのソースコードが世界に公開されていること)のプログラムでできることは、非常に好ましいことである。小説に対する新しい切り口が欲しい、あるいは文系分野の定量化に興味のある方は、これを機にこれらのツールを利用してみることをお勧めしたい。
(おわり)
第二十一回文学フリマ東京のブースが発表されました
PythonとElasticSearch、Kibanaを用いた太宰治小説の可視化
(以下の文章は秋の文フリの原稿です)
はじめに
読書という行為を、時間を抜きにして語ることはできない。例えば20000字程度の短編小説ならば、仮に分速500文字で読書可能だとすると、40分もの時間をわたしたちはそのテキストと向かい合うこととなる。わたしたちは読書を行っているあいだ、細胞の中でDNAを解読するリボソームのように、テキストをシリアルに視野の中に取り込み、脳の中で解釈していく。この際、解釈の方向は定められている。縦書きであれば上から下に。横書きであれば、(大抵の言語は)左から右に読まれるしかない。テキストは、ごく一部の例外を除けば、一方向にしか読まれ得ない。読書は非常に"時間"に似通った、あるいは随伴した現象だと考えられる。
以前発行した同人誌『実用 どんぐりと山猫』では、pythonとd3.jsを用いて小説中のある値の時間変化を可視化した。昨今、可視化ツールとして、プログラミング不要なKibanaが開発されている。そこで本稿では、小説における時の流れをKibanaを用いて可視化し、より簡便な小説のテキストの解析を目指す。
小説の中の時間構造
小説の中から、客観的な時間の指標を見出すことはできるだろうか。カフカ『城』に流れる停滞した時間。ボルヘス『バベルの図書館』における永遠の提示。桜庭一樹『私の男』で描写される逆行する日々。それら物語によって示される時間について、データとして評価できる形で客観的に抽出することは難しいだろう。そこで本稿では、小説の文字数を時間の指標とし、そこから見えてくる特徴を探していくこととする。
時間を軸としてある値を評価するためには、なんらかの基準が必要だ。一般にはタイムスタンプと呼ばれるものが用いられるが、小説には通常、そのような明確な時間の単位はない。今回はひとまず、これを500文字区切りで1づつ増える値と仮に設定する(文庫で1ページ程度に該当すると思われる)。そして可視化への中間地点として、以下のようなJSONデータを作成していく。
[ { 'page':1, 'text':'春はあけぼの、やうやう白くなりゆく、山ぎは...' }, { 'page':2, 'text':'夏は夜。月のころはさらなり...' }, ]
解析環境の用意
まずは自分のPCにpython3、ElasticSearch、Kibanaをインストールする。インストール法についての詳細は記述しない。また、解析するテキストについては、インターネット上に青空文庫や小説家になろうといった膨大なテキストデータがあるため、そちらを利用するのが早い。本稿では例として、太宰治『人間失格』のテキストを解析していくこととする。
ElastickSearch
ElastickSearchはJava製の全文検索エンジンであり、Apach Solrと並んで人気が非常に高い。導入にあたっては、(http://engineer.wantedly.com/2014/02/25/elasticsearch-at-wantedly-1.html) の内容がわかりやすいと思われる。今回重要な点としては、形態素解析器であるkuromojiのプラグインを導入しておくことだ。
ElastickSearchにデータをインポートするにあたって、あらかじめデータのマッピング(どのデータがどんな種類のデータか指定すること)を行う必要がある。マッピングについては、以下の内容のanalyze.jsonを作成すると良い。
{ "settings": { "analysis": { "filter": { "pos_filter": { "type": "kuromoji_part_of_speech", "stoptags": [ "助詞-格助詞-一般", "助詞-終助詞" ] }, "greek_lowercase_filter": { "type": "lowercase", "language": "greek" } }, "tokenizer": { "kuromoji": { "type": "kuromoji_tokenizer" }, "ngram_tokenizer": { "type": "nGram", "min_gram": "2", "max_gram": "3", "token_chars": [ "letter", "digit" ] } }, "analyzer": { "kuromoji_analyzer": { "type": "custom", "tokenizer": "kuromoji_tokenizer", "filter": [ "kuromoji_baseform", "pos_filter", "greek_lowercase_filter", "cjk_width" ] }, "ngram_analyzer": { "tokenizer": "ngram_tokenizer" } } } }, "mappings": { "novel": { "_source": { "enabled": true }, "_all": { "enabled": true, "analyzer": "kuromoji_analyzer" }, "properties": { "id": { "type": "integer", "index": "not_analyzed" }, "page": { "type": "integer", "index": "not_analyzed" }, "text": { "type": "string", "index": "analyzed", "analyzer": "kuromoji_analyzer" } } } } }
ここで、textフィールドにkuromoji_analyzerを指定している。こうすることで、解析テキストをあらかじめ形態素解析しておくことができる。
解析ファイルのJSON化
まず、『人間失格』のテキストデータを含んだningen.txtファイルを用意する。 そして、そのファイルを含んだディレクトリ上で、以下のpythonスクリプトを実行してみよう。
#!/usr/bin/env python # -*- coding: utf-8 -*- import json def main(): with open("./ningen.txt", "r") as f1: text = f1.read() # 1ページの文字数を500文字と仮定 pseud_char_num_per_page = 500 # ページ毎にテキストを区切っていく page_num = len(text) // pseud_char_num_per_page texts = [] for x in range(page_num): init = x * pseud_char_num_per_page text_of_page = text[init: init + pseud_char_num_per_page] texts.append({ "index": { "_index": "dazai-demo", "_type": "novel", "_id": x + 1 } }) texts.append({'id':x + 1, 'text':text_of_page, 'page':x}) bulk = "" for x in texts: bulk = bulk + json.dumps(x, ensure_ascii=False) + "\n" with open("./ningen.json", 'w') as f2: f2.write(bulk) print(bulk) if __name__ == '__main__': main()
これを実行すると、カレントディレクトリ配下に以下のようなningen.json(正確には、JSONテキストを結合したもの)が生成される。
{"index": {"_type": "novel", "_id": 150, "_index": "dazai-demo"}} {"page": 149, "text": "ュックサックを背負って友人の許もとを辞し、れいの喫茶店に立ち寄り、\n「きのうは、どうも。ところで、……」\n (中略)もし、これが全部事実だったら、そうして僕がこのひとの友人だったら、やっぱり脳病院に連れて行きたくなったかも知れない」\n「", "id": 150}
これは、テキストを500字毎に区切り、(架空の)ページ番号を振ったものである。 それでは、これらをElasticSearchにインポートしてみよう。
curl -XPUT localhost:9200/dazai-demo --data-binary @analyze.json curl -XPOST localhost:9200/_bulk --data-binary @ningen.json
その後、成功していたら以下のクエリによって検索が可能になっているはずである。
curl -XGET localhost:9200/dazai-demo/novel/_search -d '{"query":{"match":{"text":"東京"}}}'
問題なくデータのインポートは行えたであろうか。次項からは、Kibanaによる可視化を行っていく。
(続きます)