浦島雑誌(旧)
2009-07-27
[ロボット]  無脳詩人 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

文字コードは 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

initialize メソッドの prefix_size 引数の値は1以上の整数です。この値が大きいほど厳格な文法表ができますが、出力フレーズのバリエーションが乏しくなります。入力テキストが小さいときは1程度が無難です。実際の運用は2でやってました。
generate メソッドの max_line 引数は、出力フレーズの最大長を制御するパラメーターです。この値を越えるとその時点でフレーズの生成が打ち切られます。

長くなったので、続きは次のエントリーで。
上記クラスを使って詩を書くルーチンとその出力例を見ていただきます。
この記事のトラックバックURL:
http://www.uraxima.com/notes/20090727a.tb

コメントを書く
コメント:

お名前:


チェックボックス:
(送信する前にチェックを入れてください)
コメント (3)
やんす (2009-07-28)
みんながこれを面白がるようにする自信があります
人工知能とは何を目指しているのかのいい例題になります
夏季学習には最適

浦島 (2009-07-28)
ありがとうございます。
続きもアップしました。

やんす (2009-07-30)
あるときは完備なるプログラマー
またあるときは愉快な芸能オヤジ
ここに繰り返し現れる主題の追跡
とてもいいリズムになっています