[ロボット] 無脳詩人 djack の仕組み
人工無脳に詩を書かせて自動更新する「無脳詩人 djack」というブログをやってました。ろくな詩を書かないんで、もっとちゃんとした詩人に育てようとか、詩人としては見込みがないからコラムニストに転身させようかなどと考えてたんですが、昨年末にサーバーが事故を起こしたのを機に更新を停止しました。ブログ自体は残してあり、復旧できなかった分を除いて過去記事をご覧いただけます。
- 無脳詩人 djack
djack は、ひとまとまりのテキストを読み込んで、詩らしきものを出力するプログラムです。Ruby で書いてます。骨格部分は、入力テキストを品詞分解する Tokenizer クラスと、分解済みのトークン列からマルコフ連鎖アルゴリズムによる文法表を生成する Markov クラスから成ります。Markov クラスには詩のフレーズを作る役割も持たせてあります。
Tokenizer クラスは次のとおりです。このルーチンが精密であるほど最終的に出力されるフレーズもなめらかになりますが、詩というのは多少おかしなところがあってもいい文芸なので、こんな乱暴なルーチンでもけっこう間に合ってしまいます。
$KCODE = 'u'
class Tokenizer
def tokenize(text)
tokens = []
normalize(text).each do |line|
line.scan(/[ぁ-んー]+|[ァ-ンー]+|[亜-龠]+/) do |token| #1
if token =~ /^[ぁ-んー]/
if token =~ /^([はがのにへともを])(.+)/ #2
prefix = $1
token = $2
end
if token =~ /(.+)((ので)|(から)|(まで)|[もはがを])$/ #3
token = $1
suffix = $2
end
tokens.push prefix if prefix
tokens.push token
tokens.push suffix if suffix
else
tokens.push token
end
end
tokens.push nil
end
tokens
end
def normalize(text)
text.strip.split(/[。、, \s]+/)
end
end
class Tokenizer
def tokenize(text)
tokens = []
normalize(text).each do |line|
line.scan(/[ぁ-んー]+|[ァ-ンー]+|[亜-龠]+/) do |token| #1
if token =~ /^[ぁ-んー]/
if token =~ /^([はがのにへともを])(.+)/ #2
prefix = $1
token = $2
end
if token =~ /(.+)((ので)|(から)|(まで)|[もはがを])$/ #3
token = $1
suffix = $2
end
tokens.push prefix if prefix
tokens.push token
tokens.push suffix if suffix
else
tokens.push token
end
end
tokens.push nil
end
tokens
end
def normalize(text)
text.strip.split(/[。、, \s]+/)
end
end
文字コードは utf でなくてもかまいませんが、どこかで $KCODE を指定しておく必要があります。
この品詞分解プログラムは、連続する片仮名、連続する漢字を品詞と見なしています(#1)。「亜-龠」の箇所は、文字コードを調べるのが面倒なので、このくらいのルーチンでだいたいの漢字は拾えるだろうというやっつけです。
連続する平仮名については、さらに分解し、平仮名列の先頭に「はがのにへともを」のいずれか一文字が来たら、それを独立した品詞と見なします(#2)。また平仮名列の末尾に「ので」、「から」、「まで」のいずれか、あるいは「もはがを」のうち一文字が来たら、これも独立した品詞と見なします(#3)。
Markov クラスは次のとおりです。 今思えば、Markov という実装にとらわれた名前はまずいし、クラスを分ける必要も薄いので、Generator くらいの抽象的な名前で一つにまとめるほうが良さそうですが、実際に使っていたプログラムということで、そのまま出しておきます。
class Markov
def initialize(prefix_size = 2)
@default_prefix = Array.new(prefix_size, nil)
@state = {}
end
def chainup(tokens)
prefix = @default_prefix
tokens.each do |token|
if token == nil
prefix = @default_prefix
else
(@state[prefix] ||= []).push token
prefix = (prefix[1..-1] || []) + [token]
end
end
end
def generate(max_line = 40, trigger = nil)
prefix = trigger || @default_prefix
line = ''
before_prefix = nil
while suffix = @state[prefix]
token = suffix[rand(suffix.size)]
line += token
if before_prefix == prefix
break
elsif line.size > max_line
break
else
before_prefix = prefix
prefix = (prefix[1..-1] || []) + [token]
end
end
line
end
end
def initialize(prefix_size = 2)
@default_prefix = Array.new(prefix_size, nil)
@state = {}
end
def chainup(tokens)
prefix = @default_prefix
tokens.each do |token|
if token == nil
prefix = @default_prefix
else
(@state[prefix] ||= []).push token
prefix = (prefix[1..-1] || []) + [token]
end
end
end
def generate(max_line = 40, trigger = nil)
prefix = trigger || @default_prefix
line = ''
before_prefix = nil
while suffix = @state[prefix]
token = suffix[rand(suffix.size)]
line += token
if before_prefix == prefix
break
elsif line.size > max_line
break
else
before_prefix = prefix
prefix = (prefix[1..-1] || []) + [token]
end
end
line
end
end
initialize メソッドの prefix_size 引数の値は1以上の整数です。この値が大きいほど厳格な文法表ができますが、出力フレーズのバリエーションが乏しくなります。入力テキストが小さいときは1程度が無難です。実際の運用は2でやってました。
generate メソッドの max_line 引数は、出力フレーズの最大長を制御するパラメーターです。この値を越えるとその時点でフレーズの生成が打ち切られます。
長くなったので、続きは次のエントリーで。
上記クラスを使って詩を書くルーチンとその出力例を見ていただきます。