work.log

元エンジニアの備忘録的ブログ

Perlでマルコフ連鎖を使った短文の生成実験

投稿:2013-08-29 18:15  更新:

MeCab の形態要素解析、マルコフ連鎖に関する記事です。

前回までの記事で、形態要素解析とマルコフ連鎖に使う辞書の作成について書きましたが、今回はいよいよマルコフ連鎖で文章を生成してみたいと思います。

マルコフ連鎖で短文の生成実験

詳しい解説は最後に書くとして、マルコフ連鎖で文章生成するサンプルコードを先に書きたいと思います。

以下のコードを markov.pl という名前で保存します。

#!/usr/bin/perl

use strict;
use warnings;
use Text::MeCab;

    my $node   = '';
    my $key1   = '';
    my $key2   = '';
    my $suffix = '';
    my $rand   = 0;
    my @title  = ();
    my @prefix = ();
    my $markov = {};

    my $mecab = Text::MeCab->new();

    ## タイトル素材を書いたテキスト
    my $file   = 'title.txt';

    ## タイトル素材を配列に読み込み
    open(TXT, "< $file");
    @title = <TXT>;
    close(TXT);

    ## 読み込んだタイトルを分かち書きしてマルコフ辞書を作成
    foreach (@title) {

        chomp;
        $node = $mecab->parse($_);

        ## マルコフ辞書作成のサブルーチン
        &markov_dic($node);

    }

    ## マルコフ連鎖で文章生成スタート
    ## ループ回数を増やすと長くなりやすい
    for (my $i = 1; $i <= 6; $i++) {
        
        ## タイトル先頭の単語リストより先頭の単語を選定
        if ($i == 1) {

            $rand = &rand_count_array(\@prefix);
            $key1 = $prefix[$rand];

            print "$key1";

        ## 2回目移行は $suffix の単語を先頭のキーにして文字を探していく
        } else {

            $key1  = $suffix;

        }

        ## 連想配列に値がセットされていなければスキップ
        next if (! $markov->{$key1});

        ## key2 にくる単語を乱数を使ってセット
        $rand = &rand_count_array( \@{$markov->{$key1}->{rand}} );
        $key2 = @{$markov->{$key1}->{rand}}[$rand];

        ## suffix にくる単語を乱数を使ってセット
        $rand   = &rand_count_array( \@{$markov->{$key1}->{$key2}} );
        $suffix = @{$markov->{$key1}->{$key2}}[$rand];

        
        print "$key2";
        print "$suffix";

    }

    print "\n";

sub markov_dic {

    my $node    = shift;

    my $prefix  = '';
    my $count   = 0;
    my @word    = ();

    ## MeCab で分かち書きした単語を配列に追加していく
    while ($node->surface) {

        push(@word, $node->surface);

        $node = $node->next;
        $count++;

    }

    ## @word の配列を使って「3単語を1セット」にしていく
    for (my $i = 0; $i < $count -2; $i++) {

        $key1   = $word[$i];
        $key2   = $word[($i + 1)];
        $suffix = $word[($i + 2)];

        ## 意味が通じやすくするように先頭の単語は抜き出しておく
        if (! $prefix) {

            $prefix = $key1;

        }

        ## key2 の配列
        push( @{$markov->{$key1}->{rand}}, $key2 );

        ## suffix の配列
        push( @{$markov->{$key1}->{$key2}}, $suffix );

    } 

    ## 抜き出した単語は prefix 用の配列に追加していく
    push(@prefix, $prefix);

    return;

}

sub rand_count_array {

    my $array = shift;
    my $count = 0;
    my $rand  = 0;

    $count = @{$array} - 1;
    $rand  = int(rand($count));

    return($rand);

}

exit;

markov.pl は元となる「タイトルリスト」が必要ですので、title.txt を別途用意しました。

リストに記載するタイトル数は最低 10 件位ないと、同じ文章ばかり出力されるようになりますのでなるべく多めにしとくと良いです。

今回はテストなので ココ にあるエントリータイトルをリストとして使いました。

出力例として markov.pl が作成したタイトルをいくつか記載します。

WordPressのコメントフォームでカテゴリ別のアイキャッチ画像を作成して
WordPressのコメントフォームで名前のみを必須にする
WordPressのTwentyTwelveにページトップへのスクロール機能を表示する
WordPressカスタマイズにプラグインを使わずになる場合の対処
WordPressでJavascriptの読み込み位置を最適化
TwentyTwelveの横幅とメタ情報を削除
TwentyTwelveにヘッダ用ウィジェットエリアを追加
TwentyTwelveでカテゴリ別のアイキャッチ画像を表示をカスタマイズの準備
TwentyTwelveにパンくずリストを追加
TwentyTwelveでカテゴリ別のコメントフォームで名前のみをカスタマイズする

元のタイトルとそんなに変わってないものも出力されましたが、まずまず思うようにできたと思います。

当たり前ですが、過去のタイトルをマルコフ辞書に学習させているので、まるで自分が付けたタイトルのようです。

このタイトル生成の仕組みについては、以下で少し詳しく説明します。

マルコフ連鎖で短文生成に使ったロジック

今回のタイトル生成ロジックは大体以下のよう感じになってます。

  1. 過去のタイトルをMeCabで分かち書き
  2. 分かち書きした単語を 3単語1組 にしてマルコフ辞書を生成
  3. マルコフ辞書を生成する際に、最初の単語のみ主語になるように抜き出して主語リスト生成
  4. ループ処理でマルコフ連鎖
  5. 初回ループのみ主語リストよりランダムにピックアップ
  6. 以降はマルコフ辞書の中から紐付いた単語をランダムにピックアップしていく
  7. タイトルができる

このロジックで作られたマルコフ辞書の例として、”TwentyTwelve” をキーとした辞書は以下のようになっています。

※ 見やすくするため利用したタイトルリストは、先頭から 10 タイトルに絞ったものです。

'TwentyTwelve' => {
                    'から' => [
                                '上下',
                                'フィードバック'
                              ],

                    'の'   => [
                              '子',
                              'タイトル',
                              '見出し',
                              'タイトル',
                              '一覧',
                              '横',
                              'アイキャッチ'
                            ],

                    'を'   => [
                              'カスタマイズ'
                           ],

                    'rand' => [
                              'を',
                              'の',
                              'から',
                              'の',
                              'から',
                              'の',
                              'の',
                              'の',
                              'の',
                              'の'
                            ]
}

連想配列 $markov->{$key1} の中の配列 “rand” は $key2 にくる単語をランダムに抽出させるための配列で、ここに格納される単語が重複するほど出現する確率は高くなります。

$suffix にくる単語も同様です。

なので、$key1 に “TwentyTwelve” が来た場合、$key2 は “の” が出現する確率が高く、”の” がキーに選ばれた場合は若干ですが “タイトル” が出現する確率が高くなります。

また、初回のみ意味を通じやすくするため、読み込んだタイトル先頭の単語を必ずどれか使い、以降は $suffix を $key1 に代入しながら連鎖をしていくという感じです。

ループ回数を 5 にしているのは結構適当で、$suffix で選択された単語が $key1 になければスキップされるため、様子を見ながら決めたという感じです。

まとめ

最低限ですがなんとなく形にはなったと思うので、マルコフ連鎖を使ったエントリータイトルの自動化のブログネタは一度ここで終わらせようと思ってます。

特定キーワードとの紐付けはこの例には書いてませんが、大量にタイトルを学習させたマルコフ辞書を用意しておいて、与えたキーワードが @prefix の中にあれば自動生成とかそんな感じなるのかなと思います。

※ 一つの記事を書く場合には何かしらの「テーマ」があると思いますので、そのテーマに合わせた辞書を使えばといいんじゃないかなと。

あとは 3単語 ではなく 4単語1組 にして辞書を作成したりとか、工夫次第ではもっとマシになるかなと思います。

マルコフ連鎖で長文を作成した場合は、その文章は意味をほとんど持たなくなりますが、タイトルのような短文の場合だと比較的意味は維持できているような感じがします。

ただし、あくまでも「タイトル候補」としてしか利用できないと思いますが。

以上長くなりましたが、「Perlでマルコフ連鎖を使った短文の生成実験」はこれでお終いです。

最後に参考にさせていただいたページのリンクを張っておきます。