ひかりマテリアル

はにかむねっと

マルコフ連鎖で文章を作る


👤 sudosan 📆 6月 29, 2016 📝 2

皆さんこんにちは。すどです。

今までミルクちゃん裏垢bot(@milkchan_web)を運用してきたわけですが、最近問題が発生しました。

ミルクちゃん裏垢botは、タイムラインのデーターを元に文章を生成するわけですが、その生成に既存のあるプログラムを利用していました。

しかし、ミルクちゃんの学習データー量が増えたため(約1600万文字)、そのプログラムが動かなくなってしまったため自力で作ったのでここに簡単に書こうかと思います。

 

マルコフ連鎖とは

マルコフ連鎖とはそれっぽい文章を適当に作る仕組みです。

詳しくは割愛しますのでこちら等を御覧ください

 

まず元の文章に次のようなものがあるものとします

 

「野獣先輩は人間の鏡」

「うんこはくさい」

「風の中のすばる」

 

次に、これを単語ごとに分解します。

 

「野獣/先輩/は/人間/の/鏡/EOS」

「うんこ/は/くさい/EOS」

「風/の/中/の/すばる/EOS」

(EOSとは文章が終わるという意味です)

 

そして「野獣」の次に来るのは「先輩」、「先輩」の次に来るのは「は」、「くさい」の次は「EOF」・・・というデーターベースをつくります。

今回はデーターベースにmongoを利用しましたので、以下の様なデーターベースができます

{t1:”野獣”,t2:”先輩”,_id:”hogehoge”}

{t1:”先輩”,t2:は”,_id:”hogehoge2″}

{t1:”くさい”,t2:null,_id:”hogehoge3″}

・・・・・

 

そして、このデータベースの中から初めの単語をランダムで出力します。

そして、その単語に繋がる単語をつなげていき、文章終了になるまでコレを繰り返します。

つなげる候補が多数あった場合はランダムで選択します。

こうすることによって、例えば、

「野獣先輩はくさい」

「うんこは人間の中のすばる」

「風の中の中の鏡」

といった文章を作り出すことができます。

欠点

このように面白い文を生成することのできるマルコフ連鎖ですが、欠点もあります

欠点1、元の文をそのまま出力してしまう可能性がある。

マルコフ連鎖は確率的に元の文をそのまま出力してしまう可能性があります。これはTwitterの場合パクツイしてしまうことがあるので注意が必要です。

ただ、候補の数が大きければそれほど問題はないでしょう

欠点2、文章が意味不明

この実装例では助詞から始まらないようにする等の処理をしてないので

「は人間のすばる」といった文法的に滅茶苦茶な文章を出力してしまう可能性があります

欠点3。無限ループ

この例で言うと、「風邪の中の中の中の中の中の中の・・・」と無限にループしてしまう可能性があります

今回はツイートすることを前提に、130文字を超えた時点で途中でも出力するようにしました。

実装例

今回はnode.jsをりようしました。ツイートする機能もついていますが、不必要なら除去してください。

形態素解析する機能はついていないので、事前にmecabで分かち書きしたデーターを用意してください

分かち書きコマンド:mecab -O wakati

var fs = require('fs');
var ProgressBar = require('progress');
var MongoClient = require('mongodb').MongoClient;
var dbUrl = 'mongodb://localhost:27017/tweetdata';
var update = 0;

MongoClient.connect(dbUrl, function (err, db) {
  var Collection = db.collection('global');
  fs.readFile('./' + process.argv[2], 'utf8', function (err, text) {
    var bun = text.split("\n");
    var x = bun.filter(function(e){return e !== "";});
    //console.log(x);
    //console.log();
    var bar = new ProgressBar('[:bar] :percent :etas', {
      complete: '=',
      incomplete: ' ',
      width: 20,
      total: x.length
    });
    for(var i = 0; i < x.length; i++) {
      //console.log(x[i]);
      bar.tick();
      //console.log('\n' + ((i+1)/x.length)*100 + "%");
      var tango = x[i].split(" ");
      tango = tango.filter(function(e){return e !== "";});
      //console.log(tango);
      for(i2 = 0; i2 < tango.length; i2++) {
        //console.log(i2+"/"+tango.length);
        if(i2===tango.length-1) {
          var obj = {t1:tango[i2],t2:null};
          //console.log(obj);
          Collection.insert(obj,function(){update++;console.log(update+"/"+x.length);if(update===x.length){process.exit();}});
          //console.log(i);
          if(i===x.length-1) {
            //setTimeout(function(){process.exit();}, 5000);
          }
        }
        else {
          var obj = {t1:tango[i2],t2:tango[i2+1]};
          //console.log(obj);
          Collection.insert(obj);
        }
      }      
    }
    //process.exit();
  });
});
var MongoClient = require('mongodb').MongoClient;

var dbUrl = 'mongodb://localhost:27017/tweetdata';

var twitter = require('ntwitter');

var tw = new twitter({
        consumer_key        : '',
        consumer_secret     : '',
        access_token_key    : '',
        access_token_secret : ''
});

var text = null;
var t2 = null;
MongoClient.connect(dbUrl, function (err, db) {
  var Collection = db.collection('global');
  var arg = process.argv[2];
  if(arg) {
    console.log(arg);
    var query = { "t1" : arg };
    var cursor = Collection.find(query);
    cursor.count(function(err, count) {
      var random = Math.floor(Math.random()*count);
      console.log(random+"/"+count);
      cursor.sort({ "t1": -1});
      cursor.skip(random);
      cursor.limit(1);
      cursor.toArray(function(err, docs){
        var dobj = docs[0];
        console.log(docs);
        text = dobj.t1;
        t2 = dobj.t2;
        t2test();
      });
    });
  }
  else {
    var query = {};
    var cursor = Collection.find(query);
    Collection.count(function(err, count) {
      var random = Math.floor(Math.random()*count);
      cursor.sort({"t1" : -1});
      cursor.skip(random);
      cursor.limit(1);
      cursor.toArray(function(err, docs){
        var dobj = docs[0];
        console.log(docs[0]);
        text = dobj.t1;
        t2 = dobj.t2;
	t2test();
      });
    });
  }
function t2test(){
    if(t2==null||text.length>130) {
      if(text.length>5) {
        text = text.replace(/@|@|#|#/g,"");
        text = text.replace(/です/g,"ですわ");
        text = text.replace(/ます/g,"ますわ");
        tw.updateStatus(text,function(data){process.exit();});
      }
      //console.log(text);
      else{
      process.exit();
      }
    }
    else {
    //console.log("t2/"+t2);
    var query = {"t1":t2};
    var cursor = Collection.find(query);
    cursor.count(function(err, count) {
      var random2 = Math.floor(Math.random()*count);
      console.log(random2+"/"+count);
      cursor.sort({ "t1": -1});
      cursor.skip(random2);
      cursor.limit(1);
      cursor.toArray(function(err, docs){
        var dobj = docs[0];
        console.log(docs);
        if(dobj.t2!=null) {
        text += dobj.t2;
        }
        else {
        //text += dobj.t1;
        }
        t2 = dobj.t2;
        t2test();
      });
    });
   }
  }
});

 

mecabとmongodbが必要です。

mongodbでは、t1にインデックスを張ってないととても遅いので注意してください。

インデックスを張るにはmongoにログインし、ターミナルでdb.hogehoge.createIndex({“t1”: 1});

動かしてみる

無事、740万単語をインプットしたので動かしてみました。

このインプット作業がかなり重く、メモリも食ったので500行ずつに分割して処理しました。

完全にランダムに


最初のワードを指定して

「野獣」から始まる文を作ってみます

楽しい✌


関連

共有

コメント

👤 I.K

🕓8月 8, 2016 10:46 am 📎パーマリンク

いつも楽しく読ませていただいています。

>>(EOSとは文章が終わるという意味です)

EOFではないでしょうか。
間違えていたらごめんなさい。

返信

👤 Tak

🕓5月 28, 2018 2:20 pm 📎パーマリンク

ありがたいサイトですね。

いやあ、End Of Sentense なのでEOSでOKだと思いますよー
EOFはEnd Of File

返信

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください