読者です 読者をやめる 読者になる 読者になる

らこらこブログ

唐揚げとアニメとプログラミングが大好きです

メカらこv3の文章生成 実装と試運転結果

前回までのあらすじ

N-POSモデルとN-gramモデルを線形結合していい感じの文章生成させてみよう

実装してみた

モデル

module Model {
  export class PosSet {
    pos1:string;
    pos2:string;
    pos3:string;
  }

  export class GramSet {
    gram1:string;
    gram2:string;
    gram3:string;
  }

  export class Token {
    pos:string;
    gram:string;
  }
}

実装

初期化

    var textArray = [Model.BOS.gram, Model.BOS.gram];
    var posArray = [Model.BOS.pos, Model.BOS.pos];
    var probabilityMap:{[gram:string]:{[pos:string]:number}};
    var position = posArray.length;
  • textArray : 生成される文の配列 最後にjoinする
  • posArray : 生成に使う品詞列
  • probabilityMap : [gram][pos]で単語と品詞のペアについての確率をキャッシュする連想配列
  • position : ループカウンタ

条件を満たすまで以下をwhile(true)でループする

N-POSモデルでの確率推定

      probabilityMap = {};

      // N-POS estimation
      var pos1 = posArray[position - 2];
      var pos2 = posArray[position - 1];
      for (var i = 0; i < posSets.length; i++) {
        var posSet = posSets[i];
        if (pos1 === posSet.pos1 && pos2 === posSet.pos2) {
          // pos match set
          for (var j = 0; j < tokens.length; j++) {
            var token = tokens[j];
            if (token.pos === posSet.pos3) {
              // pos match gram
              var probability = 1 / tokens.length / posSets.length;
              if (probabilityMap[token.gram] && probabilityMap[token.gram][token.pos]) {
                probabilityMap[token.gram][token.pos] += probability * K;
              } else {
                if (!probabilityMap[token.gram]) {
                  probabilityMap[token.gram] = {};
                }
                probabilityMap[token.gram][token.pos] = probability * K;
              }
            }
          }
        }
      }

直前2品詞が一致するPosSetを抽出し、それぞれの第3品詞に対応するTokenを抽出した上でペアの確率を連想配列に保存。すでに存在している場合は加算している。データストアに保存する際は重複を考慮しないため、中身が同じPosSet、Tokenはおそらく多数出現する。加算によってそこの部分を吸収して確率に反映する。

加算する確率にはファクター K を掛けている。

N-gramモデルでの確率推定

      // N-gram estimation
      for (var i = 0; i < gramSets.length; i++) {
        var gramSet = gramSets[i];
        var gram1 = textArray[position - 2];
        var gram2 = textArray[position - 1];
        if (gram1 == gramSet.gram1 && gram2 == gramSet.gram2) {
          for (var j = 0; j < tokens.length; j++) {
            var token = tokens[j];
            if (token.gram === gramSet.gram3) {
              // pos match gram
              var probability = 1 / tokens.length / gramSets.length;
              if (probabilityMap[token.gram] && probabilityMap[token.gram][token.pos]) {
                probabilityMap[token.gram][token.pos] += probability * (1 - K);
              } else {
                if (!probabilityMap[token.gram]) {
                  probabilityMap[token.gram] = {};
                }
                probabilityMap[token.gram][token.pos] = probability * (1 - K);
              }
            }
          }
        }
      }

基本的にN-POSと処理の流れは変わらず、直前2単語が一致するGramSetの第3単語と、それに対応する品詞を抽出して確率を加算している。こちらの確率には (1-K) を掛けている。

選別

確率が高い品詞と単語のペアを選びたいが、常に最高の確率のものを選んでいると文の生成がかたよると思われるので、上位3位からランダムにサンプリングしたものを使うようにした。

      var samples:{gram:string; pos:string; probability:number}[] = [];
      for (var gram in probabilityMap) {
        for (var pos in probabilityMap[gram]) {
          var sample = {
            gram: gram,
            pos: pos,
            probability: probabilityMap[gram][pos]
          };
          if (sample.probability > 0) {
            samples.push(sample);
          }
        }
      }
      if (samples.length === 0) {
        break;
      }
      samples.sort((a, b)=>b.probability - a.probability);
      if (samples.length > 3) {
        samples = samples.splice(0, 3);
      }
      var sampled = sampleOne(samples);
      if (sampled.gram === "") {
        break;
      }
      else {
        textArray.push(sampled.gram);
        posArray.push(sampled.pos);
        position++;
        console.log(textArray.join(""));
      }

終了条件は

  • 候補がない場合
  • 選別された単語が空の場合

のどちらかを満たすこととした。

試運転

\( K \) の値によってどうなるか、簡単な文を分解させて再構築させることで実験してみた。

与えた文は「今日は雨です。明日も雨の模様。」で、ループが50回を超えるようなら強制的に終了させるようにした。各Kの条件で10回ずつ生成してみる

K=0(N-gramのみ)

  • 今日は雨です。明日も雨の模様。
  • 今日は雨です。明日も雨の模様。
  • 今日は雨です。明日も雨の模様。
  • 今日は雨です。明日も雨の模様。
  • 今日は雨です。明日も雨の模様。
  • 今日は雨です。明日も雨の模様。
  • 今日は雨です。明日も雨の模様。
  • 今日は雨です。明日も雨の模様。
  • 今日は雨です。明日も雨の模様。
  • 今日は雨です。明日も雨の模様。

この学習データでは当然こうなる。ここに品詞データを与えてどう変化していくかを見る

K=0.25

  • 明日は雨の模様。
  • 今日は雨の模様。
  • 明日は雨です。明日も雨の模様。
  • 今日は模様です。今日も模様の雨。
  • 今日は雨です。今日も模様の模様。
  • 明日は模様の模様。
  • 明日も模様です。今日も模様です。今日は模様の模様。
  • 今日も模様です。今日も模様です。明日も模様の雨。
  • 今日は雨の雨。
  • 今日は模様です。今日も模様の雨。

元の文に存在しない結合が出てきた。

K=0.5

  • 今日は模様の模様。
  • 明日は雨の模様。
  • 明日は模様です。明日は雨の雨。
  • 明日も雨です。今日も雨です。今日も模様です。今日は模様の模様。
  • 明日も模様の模様。
  • 明日は雨です。明日は模様の模様。
  • 明日も雨の模様。
  • 明日は雨です。今日は雨です。今日は模様の雨。
  • 今日も模様です。今日は模様です。今日は雨の雨。
  • 明日も雨です。今日は雨です。今日は模様です。明日は雨です。明日も雨です。明日も模様です。明日も模様です。今日も雨です。明日は模様です。今日も模様

「雨の模様」に起因する「名詞+の+名詞」の結合が暴れだしてる。50回中断されたケースもでてきた。

K=0.75

  • 今日は模様の模様。
  • 今日も模様の模様。
  • 明日も模様です。今日も模様の模様。
  • 明日は雨の雨。
  • 明日も模様です。明日は模様です。明日も雨です。明日も模様の模様。
  • 明日は模様です。今日は模様です。今日も雨です。今日は雨です。今日は模様の模様。
  • 明日は雨です。明日も雨です。明日は雨です。明日は雨です。今日も雨の雨。
  • 今日は雨の雨。
  • 今日は模様の雨。
  • 今日も雨です。明日は雨の模様。

「雨の模様」になる方が少なくなってきた。

K=1 (N-POSのみ)

  • 今日は模様です。今日も雨の雨。
  • 明日は模様です。今日も雨です。今日は模様です。今日は雨です。明日は模様の雨。
  • 今日は雨です。今日は模様です。明日も模様です。今日は模様の雨。
  • 今日も雨の雨。
  • 明日も雨の雨。
  • 今日も雨の雨。
  • 今日も雨です。今日は雨です。明日は雨です。明日は模様です。今日も模様の雨。
  • 明日も模様の模様。
  • 今日は雨です。明日は雨の雨。
  • 今日は模様です。明日は雨です。今日も雨の模様。

K=0.75の時とあんまり変わらない。

今回のまとめ

N-POSの影響力がめちゃくちゃ強い。ちょっと混ぜるだけで候補の数が増えるので多彩になる一方で文脈的に意味不明な「雨の雨」とかがたくさん出てくる。学習データを増やすと更にこの傾向は顕著になると思うのでKは小さめがいい気がする。上位3件からサンプリングという抽選方法にも問題がある気がするがここはbotの面白さを引き出せそうな気がするのでこのままにする。