MeCab(Natto) + マルコフ連鎖 ( + Ruby )でレシピ文章を自動生成②



前回までで、MeCabとマルコフ連鎖についてまとめてきました。

完成品です。

http://tools.beightlyouch.com/cook

全コード


全文です。

require 'natto'


$recipe_markov = {}

def parse(text)

    data = ["BEGIN","BEGIN"]

    nm = Natto::MeCab.new('-F%m')
    enum = nm.enum_parse(text)

    enum.each do |word|
        data << word.feature if !(n.is_bos? || n.is_eos?)
    end

    data << "END"
   
    data.each_cons(3).each do |datum|
        value = datum.pop
        key = datum
        
        $recipe_markov[key] ||= []
        $recipe_markov[key] << value
    end
end

def markov()
    random = Random.new
    key = ["BEGIN","BEGIN"]
    new_text = ""
    $indexes = []

    loop {
      begin
        length = $recipe_markov[key].length
        key = [key[1] , $recipe_markov[key][random.rand(0..length-1)]]

        if key[0] != "BEGIN"
          ret += key[0]
        end
        if key[1] == "END"
          break
        end
      rescue
      end
    }
    return new_text
end


  File.open('recipe.txt',"r") do |file|
    file.each_line do |line|
        parse(line)
    end
  end


loop{ 
  article = markov() 
  count += 1
  puts count.to_s +  ". " + article

  if count >= STEP_NUM
    puts "できあがり!"
  break
  end
}


1. MeCabによる分かち書き


まず、はじめの部分についてみていきます。recipes.txtには、下記のようにレシピ文章を羅列してあります。
それを1行ずつ読み込んでいます。


  File.open('recipes.txt',"r") do |file|
    file.each_line do |line|
        parse(line)
    end
  end
recipies.txt


1行を読み込んだのち、以下のメソッドでマルコフ連鎖用にparseしています。

def parse(text)

    data = ["BEGIN","BEGIN"]

    nm = Natto::MeCab.new('-F%m')
    enum = nm.enum_parse(text)

    enum.each do |word|
        data << word.feature if !(n.is_bos? || n.is_eos?)
    end

    data << "END"
   
    data.each_cons(3).each do |datum|
        value = datum.pop
        key = datum
        
        $recipe_markov[key] ||= []
        $recipe_markov[key] << value
    end
end


“骨を取り除き番茶で茹でる”という1行で考えてみます。
MeCabのenum_parseによって、形態素に分解されます。それらのfeature(単語自身)を、dataという配列に格納しています。

dataの中には元々”BEGIN”が2つありますから、

data = [“BEGIN”, “BEGIN”, “骨”, “を”, “取り除き”, “番茶”, “で”, “茹でる” ]

のようになります。


次に、data.each_cons(3).each…の部分では以下のような作業をしています。
まず、each_cons(3)によって、配列を重複ありで3要素に区切ります。つまり、
[“BEGIN”, “BEGIN”, “骨” ]
[“BEGIN”, “骨”, “を”]
[“骨”, “を”, “取り除き”]
.
.
.

のようになっています。


次に、以下の部分です。

value = datum.pop
key = datum

popは、配列の末尾を取り除くものです。
[“BEGIN”, “BEGIN”, “骨” ]の場合、
key = [“BEGIN”, “BEGIN”]
value = [“骨”]
のようになります。


最後に、以下の部分では、

        
$recipe_markov[key] ||= []
$recipe_markov[key] << value

グローバル変数のハッシュ recipe_markovに格納しています。
もし、keyに対応するvalueが存在しなかったらまず上の行でvalue用の配列が生成されます。下の行でその配列にvalueが追加されます。

[“骨”, “を”] => [ ]

[“骨”, “を”] => [“取り除き”]
別の文章で”骨を抜く”というフレーズがあった場合、
[“骨, “を”] => [“取り除き”, “抜く”]というように追加されていきます。


以上を見て分かるように、ここでは2階マルコフ連鎖を行っています。つまり、2つの単語によって、次の単語が決定されています。

これを全行繰り返すと、以下のように、対応表のようなものができ上がります。



あとは、それらをランダムに繋げ合わせて、新しい文章を作るだけです。



2. ランダムにつなぎ合わせる


つなぎ合わせている部分です。

def markov()
    random = Random.new

    key = ["BEGIN","BEGIN"]
    new_text = ""

    loop {
      begin
        length = $recipe_markov[key].length
        key = [key[1] , $recipe_markov[key][random.rand(0..length-1)]]

        if key[0] != "BEGIN"
          new_text += key[0]
        end

        if key[1] == "END"
          break
        end
      rescue
      end
    }
    return new_text
end


まず、文頭をランダムに選ぶために、[“BEGIN”, “BEGIN”]からはじめます。
[“BEGIN, “BEGIN”] => [“小さめ”, “リンゴ”, “オリーブ”, “人参”, “ボウル”, “マフィン”, “豆腐”…]のようになっているので、そこからランダムに選びます。
例えば”リンゴ”を選んだとします。すると、今度は[“BEGIN, “リンゴ”]をkeyとして、”END”が出てくるまで繰り返します。
“END”までたどり着いた場合、ループを抜け、完成されたテキストが返ってきます。



結果


結果は以下のようになります。

・魔法の呪文「bonprofit」(召し上がれ)を約2分間湯通ししたのち、小麦粉を加える。料理用温度計を用いてかき混ぜ中火で3~4時間おき、湯煎しながらオーブンで加熱する。冷蔵庫で一晩水にさらして水気を絞る。

・いちご、ムース用の卵黄を順に加えて味を調える。器に盛り、シナモンスティックを入れて混ぜる。

・ホットプレートを1600度に予熱した生地が膨らむのを防ぐ。火を通した肉や魚介類等の調味料を入れ、軽く和える。



“シナモンスティックを入れて混ぜる”など、多分原文そのまま出てしまっている部分や、”魔法の呪文「bonprofit」(召し上がれ)を約2分間湯通ししたのち、”など意味のわからない部分がありますが、味がある文章なのでそのままにしています。

Nを調整したり元の文章を増やしたりすることによって対応できるかもしれませんが、マルコフ連鎖は基本的に文章を繋げ合わせていくだけなので、このようなものかと思います。

コメント

タイトルとURLをコピーしました