のんびり読書日記

日々の記録をつらつらと

マルコフ連鎖で文生成

今回はデータマイニングっぽい話ではなくて、ちょいネタで。昨日の記事でWP2TXTを使ってwikipediaのテキスト情報を取り出したので、これを使ってちょっと遊んでみます。以前プログラミング作法を読んだときに載っていた、マルコフ連鎖を試してみたいと思います。

プログラミング作法

プログラミング作法

作ったのはこんな感じ。そろそろコードのベタ張りはやめます。次あたりからはgithubにでも置きますかね。あれってちゃんとしたプロジェクトものしか置かない方がいいのかなと思ってたのですが、別に勉強用コードを置いてる人も結構いるんですね。僕も適当に置きまくろう。

#!/usr/bin/perl 
#
# Sentence generator using markov chain
#
# Usage:
#  1) Training mode
#    markov.pl -t -o chain.hdb < input.txt
#
#  2) Generation mode
#    markov.pl -g -n chain.hdb
#

use strict;
use warnings;
use Encode qw(decode encode);
use Getopt::Long;
use Text::MeCab;
use TokyoCabinet;
use Data::Dumper;

use constant {
    SEP           => "\t",
    NONWORD       => " ",
    SENTENCE_END  => "。",
    SAVE_INTERVAL => 10000,
    NUM_PREFIX    => 2,
};

sub usage_exit {
    print <<'USAGE';
Usage: 
 1) Training mode
  $ markov.pl -t -o dbm < input.txt
     -t, --train         training mode
     -o, --output dbm    output dbm path of word chain

 2) Generation mode
  $ markov.pl -g -n dbm
     -g, --generate      generation mode
     -c, --chain dbm     dbm path of word chain
USAGE
    exit 1;
}

sub _split_words {
    my ($mecab, $text) = @_;
    return if !$mecab || !$text;

    my @words;
    for (my $node = $mecab->parse($text); $node; $node = $node->next) {
        push @words, $node->surface if $node->surface;
    }
    return \@words;
}

sub _add_chain {
    my ($chain, $words) = @_;
    return if !$chain || !$words;

    my @prefixes;
    for (my $i = 0; $i < NUM_PREFIX; $i++) {
        push @prefixes, NONWORD;
    }
    my $nullkey = join SEP, (NONWORD) x NUM_PREFIX;
    foreach my $word (@{ $words }) {
        my $key = join SEP, @prefixes;
        next if $key eq $nullkey && $word eq SENTENCE_END;
        push @{ $chain->{$key} }, $word;
        shift @prefixes;
        push @prefixes, $word;
    }
}

sub _save_chain {
    my ($dbm, $chain) = @_;
    return if !$dbm || !$chain;

    foreach my $key (keys %{ $chain }) {
        my $words = $chain->{$key};
        next if !$words;
        my $val = join SEP, @{ $words };
        if ($dbm->get($key)) {
            $dbm->putcat($key, SEP.$val);
        }
        else {
            $dbm->putcat($key, $val);
        }
    }
}

sub train {
    my $dbpath = shift;
    return if !$dbpath;

    my $hdb = TokyoCabinet::HDB->new;
    if (!$hdb->open($dbpath, $hdb->OWRITER | $hdb->OCREAT | $hdb->OTRUNC)) {
        die "cannot open $dbpath";
    }

    my $mecab = Text::MeCab->new();
    my %chain;
    my $cnt = 0;
    while (my $line = <STDIN>) {
        my $words = _split_words($mecab, $line);
        _add_chain(\%chain, $words);
        if (++$cnt % SAVE_INTERVAL == 0) {
            _save_chain($hdb, \%chain);
            %chain = ();
        }
    }
}

sub generate {
    my $dbpath = shift;
    return if !$dbpath;

    my $hdb = TokyoCabinet::HDB->new;
    if (!$hdb->open($dbpath, $hdb->OREADER)) {
        die "cannot open $dbpath";
    }

    my @prefixes;
    for (my $i = 0; $i < NUM_PREFIX; $i++) {
        push @prefixes, NONWORD;
    }
    my $text;
    my $cnt = 0;
    while ($cnt < 5) {
        my $key = join SEP, @prefixes;
        my $val = $hdb->get($key);
        last if !$val;
        my @suffixes = split SEP, $val;
        my $suffix = $suffixes[rand @suffixes];
        last if !defined $suffix;
        $text .= $suffix;
        ++$cnt if $suffix eq SENTENCE_END;
        shift @prefixes;
        push @prefixes, $suffix;
    }
    print "$text\n";
}

sub main {
    my ($opt_train, $opt_gen, $opt_out, $opt_chain);
    GetOptions(
        'train'    => \$opt_train,
        'generate' => \$opt_gen,
        'output=s' => \$opt_out,
        'chain=s'  => \$opt_chain,
    );
    if ($opt_train && $opt_out) {
        train($opt_out);
    }
    elsif ($opt_gen && $opt_chain) {
        generate($opt_chain);
    }
    else {
        usage_exit();
    }
}

main();

まず入力文章をText::MeCab形態素解析して、ngramをDBMに保存します。次にそのDBMを使って、ランダムに文を生成します。形態素単位でしか受け付けられないようにしたのはよくなかったかなぁ。mecabは外部で呼び出すようにして、形態素単位でも文字単位でも受け付けられるようにした方が面白かったですかね。まあ今回はこのままで。

実際に動かしてみます。

% ./markov.pl --train -o chain.hdb < ~/data/wikipedia/txt/jawiki-latest-pages-articles.xml-001.txt
% ./markov.pl --generate -c chain.hdb
相馬市や、1975年、再び食卓に並ぶ方式とNTT大容量化により、計算されている。登場時はポール・ウェラー率いるザ・ジャムは、チェンジの際の幽邃な雰囲気を強く重要視される。エルヴィス・プレスリーもビートルズもデビューする。本土のほかに、さらに地方の戦国時代に は対応プレイヤー(いわゆるラブコメなどを用い理想的な研究も盛んであるLR法や、スケジュール管理を委託する例があるが、シュリケン ジャーの声も坂口哲夫から坂口候一にもかかわらず、その五感や戦闘機とA滑走路を離陸にもこの曲は当時の子供から苛烈ないじめを受けて『パーマン』(講談社)に入学。1921年・月をもっている。

% ./markov.pl --generate -c chain.hdb
現在、「FreeBSD」という位置付けがなされている。遺伝や進化学が主流となった。しかし、もし陰極線の正体に変更し、計画は個人差がある事を後に雑誌の記事'IHaveNoWords&IMustDesign'において、地震計を設置。初代アルデ王らの家漫画作品を読む』(小学館)においては相 良氏、秋田県雄勝郡東成瀬村出身)。その後2009年2月10日にブログで過去には1日新唐津市、56頁)。

さっぱり意味わかんね!もう少し同じジャンルの文章に絞ったりすれば、まだ分かる内容になるかな?面倒なので今回はここまでで^^;


でも人工無能は真面目に作ったら面白いと思うんですよね。あまり役に立つことはないけど、たまに見てにんまりしたり、自分で人工無能の挙動を変えていけるようにすれば、育成ゲーみたいに感じるかもしれないし。ちゃんとした対話システムとか、質問応答システムはいまのところ実用レベルのものはなかなか作れないのだから、とりあえずこの手の言語生成は意味のない内容でいいから、人を楽しませるものを作るべきではないかなーと思います。どうすれば楽しんでもらえるの?はまた難しいけど。

手っ取り早くなにか作って試すとしたら、どういう環境がいいかなぁ。何か対象のコンテンツを指定したら、それに対するコメントとかを自動で作成して、twitterにしょっちゅうポストするようなbotでも作ろうかな。blogpetみたいなものまで作りたいな。