👍📕😓🎉😆⚽✌

昨今、世界の砂漠化と並び、世界の絵文字(Emoji)化が進んでおり、社会問題として取り上げられています。

自然言語もEmoji化が進んでおり、今後数年以内に「日本語はEmojiにすべて置き換わる」とも言われております。
そこで、本ブログもこの流れを汲み、自然言語処理・日本語処理からEmoji処理・Emojineeringを中心とした技術へとシフトしていきたいと思います。
用いる言語を日本語からEmojiへt👍📕😓🎉😆⚽✌😙


ラティスのNbestを求める

はじめに

形態素解析とかの解析時に使うラティス(形態素候補をグラフにしたもの)のうち、1番ベストな解だけが欲しい場合が多いが、2番目以降の解も欲しい場合がある。
N番目までの解を効率よく求める方法があり、使いたいケースが出てきたのに書いてみる。

Nbestとは

  • 解析したときに、スコアが良い順に、第一候補(1best)の解だけでなく、第二候補、第三候補・・・の解が考えられるとき、第N候補までのことをNbestという
  • Nbest解
前向きDP後ろ向きA*アルゴリズム
  • http://ci.nii.ac.jp/naid/110002725063
  • ラティスにおいて、効率よくNbestを求める方法が提案されている
  • 最初に、1bestを求める方法と同じようにスタートノードからあるノードまでの最小コストhをすべてのノードについて求めておく
    • 1bestの時は、ゴールノードからスタートノードに向かって、最小コストが選ばれるノードを逆に辿る事で求められた
    • Nbestの時は、「A*アルゴリズム」の考え方を利用して求める
  • ゴールノードからスタートノードに向かって、今考えているノードから(最適とは限らない、これまで取ったパスによって変わる)ゴールまでのコストgを求めていく
  • 前側のコスト合計hと後ろ側のコスト合計gを使うと、スタートから今考えているノードを通って(今まで通ってきた最適とは限らない)ゴールまでのパスの合計値fが求められる
  • fを使って小さい順に考えるノードを増やしながら、スタートノードにたどり着くパスを見つけると、Nbestの解を順次求められる

書いてみたけどわかりにくい。
徳永さんの「日本語入力を支える技術」の中で丁寧に紹介されている。

コード

スタートノード(BOS)とゴールノード(EOS)が1つずつあり、必ずスタートノードからゴールノードにパスを持つようなDAGであれば大丈夫だと思うので、それで書いてみる。
(雑なので、パスのノードを別に持ったり、トポロジカル順序を求めてる無駄がある・・・)

#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include <map>
#include <set>

#define INF 99999999

struct Node {
  int id;
  int w;
  //id,w
  std::map<int,int> forward;
  std::map<int,int> backward;

  int h;
};

struct Path {
  int id;
  int g;
  int f;
  std::vector<int> rev; //現在idからゴールノードまでの逆順パス(注意:無駄にメモリを使っている)
};
bool operator>(const Path& a, const Path& b){
  return a.f > b.f;
}

//スタートとゴールとなるノードがそれぞれ1つずつあり、ゴールノード以外で終わるようなノードが無いDAG
class stDAG {
  int n; //ノード数
  int s, t; //スタートノード番号、ゴールノード番号
  std::vector<Node> nodes; //ノード情報

  //トポロジカル順序を求めるため
  bool topological_sort(std::vector<int>& order){
    std::vector<int> color(n);
    for(int i=0; i<n; i++){
      if(!color[i] && !topological_visit(i, order, color)) return false;
    }
    std::reverse(order.begin(), order.end());
    return true;
  }
  bool topological_visit(int v, std::vector<int>& order, std::vector<int>& color){
    color[v] = 1;
    for(std::map<int,int>::iterator itr = nodes[v].forward.begin();
        itr != nodes[v].forward.end();
        ++itr){
      if(color[itr->first] == 2) continue;
      if(color[itr->first] == 1) return false;
      if(!topological_visit(itr->first, order, color)) return false;
    }
    order.push_back(v);
    color[v] = 2;
    return true;
  }

public:
  //ノード数、スタートノードID、ゴールノードID
  stDAG(int n, int s, int t):
    n(n),
    nodes(n),
    s(s),
    t(t)
  {
    for(int i=0; i<n; i++){
      nodes[i].id = i;
      nodes[i].h = INF;
    }
  }

  //ノード情報の追加
  void add_node(int id, int w){
    nodes[id].w = w;
  }
  //エッジ情報の追加
  void add_edge(int id, int next_id, int w){
    nodes[id].forward[next_id] = w;
    nodes[next_id].backward[id] = w;
  }
  
  //前向きDP
  void forward_dp(){
    std::vector<int> order;
    if(!topological_sort(order)){
      std::cerr << "topological sort error" << std::endl;
      return;
    }
    if(nodes[order[0]].id != s || nodes[order[n-1]].id != t){
      std::cerr << "s,t id error" << std::endl;
      return;
    }

    nodes[order[0]].h = 0;
    for(int i=0; i<n; i++){
      int id = order[i];
      //std::cout << id << "\t" << nodes[id].h << std::endl;
      for(std::map<int,int>::iterator itr = nodes[id].forward.begin();
          itr != nodes[id].forward.end();
          ++itr){
        int next_id = itr->first;
        int next_w = nodes[next_id].w;
        int edge_w = itr->second;
        nodes[next_id].h = std::min(nodes[next_id].h, nodes[id].h + edge_w + next_w);
      }      
    }
  }

  //後向きA*
  void backward_astar(int N){
    int no = 1;
    std::priority_queue<Path, std::vector<Path>, std::greater<Path> > que;

    Path path;
    path.id = t;
    path.g = 0;
    path.f = nodes[t].h;
    path.rev.push_back(t);
    que.push(path);

    while(!que.empty()){
      path = que.top(); que.pop();
      int id = path.id;
      int bestf = path.f;

      if(id == s){
        std::cout << no << "best: ";
        for(int i=path.rev.size()-1; i>=0; i--){
          std::cout << path.rev[i] << " ";
        }
        std::cout << " => " << path.f + nodes[s].w << std::endl;
        if(no == N) return;

        no++;
      }

      for(std::map<int,int>::iterator itr = nodes[id].backward.begin();
          itr != nodes[id].backward.end();
          ++itr){
        int gg = nodes[id].w + itr->second + path.g;
        int ff = gg + nodes[itr->first].h;

        Path new_path;
        new_path.id = itr->first;
        new_path.g = gg;
        new_path.f = ff;
        new_path.rev = path.rev;
        new_path.rev.push_back(itr->first);
        que.push(new_path);
      }
    }
  }
};


int main(){
  //graph1
  stDAG dag(6, 0, 5);
  dag.add_node(0, 0);
  dag.add_node(1, 1);
  dag.add_node(2, 2);
  dag.add_node(3, 3);
  dag.add_node(4, 4);
  dag.add_node(5, 5);
  dag.add_edge(0, 1, 1);
  dag.add_edge(0, 2, 2);
  dag.add_edge(1, 3, 2);
  dag.add_edge(1, 4, 1);
  dag.add_edge(2, 3, 1);
  dag.add_edge(2, 4, 2);
  dag.add_edge(3, 5, 2);
  dag.add_edge(4, 5, 1);
  dag.forward_dp();
  dag.backward_astar(4);

  //graph2
  /*
  stDAG dag(7, 0, 6);
  dag.add_node(0, 3);
  dag.add_node(1, 1);
  dag.add_node(2, 2);
  dag.add_node(3, 1);
  dag.add_node(4, 2);
  dag.add_node(5, 3);
  dag.add_node(6, 2);
  dag.add_edge(0, 1, 2);
  dag.add_edge(0, 2, 1);
  dag.add_edge(1, 3, 3);
  dag.add_edge(2, 3, 1);
  dag.add_edge(3, 4, 1);
  dag.add_edge(3, 5, 2);
  dag.add_edge(4, 6, 4);
  dag.add_edge(5, 6, 1);
  dag.forward_dp();
  dag.backward_astar(4);
  */

  return 0;
}

結果

グラフ1

$ ./a.out
1best: 0 1 4 5  => 13
2best: 0 1 3 5  => 14
3best: 0 2 3 5  => 15
4best: 0 2 4 5  => 16
グラフ2

$ ./a.out
1best: 0 2 3 5 6  => 16
2best: 0 2 3 4 6  => 17
3best: 0 1 3 5 6  => 18
4best: 0 1 3 4 6  => 19

とりあえず合ってそうなので、大丈夫そう。

pandas使ってwikipediaの表データを取得する

はじめに

特定ジャンルの用語などをまとめて取得するのに、wikipediaの「〜の一覧」が有用だったりする。
wikipedia:一覧の一覧

多くは、リスト形式で書かれていたりするが、中には表(テーブル)形式でまとめられているものもある。
いろんな取得方法が考えられるが、pythonのデータ解析支援ライブラリの「pandas」を使うと簡単に取得できたのでメモしておく。

注意

Wikipedia:データベースダウンロード

記事を大量にダウンロードするためにクローラを使わないで下さい。
強引なクローリングは、ウィキペディアが劇的に遅くなる原因となります。

ウィキペディアのデータベースから自動的にデータの収集がなされた場合、
システム管理者によってあなたのサイトからウィキペディアへのアクセスを
禁止する措置が取られることもあります。
またウィキメディア財団が法的措置を検討することもあります。

自己責任でお願いします。

python、pandasの準備

ググればでてくるので省略。
Anacondaだと入ってるので、それを使う。

表形式でまとめられている一覧ページの取得

猫の品種の一覧」「犬の品種一覧」「ウサギの品種一覧」はそれぞれ写真付きで表形式一覧になっている。
DBpediaで見てみても、表形式のデータをきれいに取り出せてはいない模様。


以下で取得、ちょっとした置換処理、csv出力ができる。

import pandas

# 猫の品種の一覧
url = 'https://ja.wikipedia.org/wiki/%E7%8C%AB%E3%81%AE%E5%93%81%E7%A8%AE%E3%81%AE%E4%B8%80%E8%A6%A7'
df = pandas.io.html.read_html(url, encoding='utf-8', header=0)
df[0]['種類'] = df[0]['種類'].replace({'(英語版)':'', '\(.+\)':'', '\[\d+\]':'', '^en\:':''}, regex=True)
df[0].to_csv('neko.csv')

# 犬の品種一覧
url = 'https://ja.wikipedia.org/wiki/%E7%8A%AC%E3%81%AE%E5%93%81%E7%A8%AE%E4%B8%80%E8%A6%A7'
df = pandas.io.html.read_html(url, encoding='utf-8', header=[0,1])
df[0]['犬種'] = df[0]['犬種'].replace({'(英語版)':'', '\(.+\)':'', '\[\d+\]':'', '^en\:':''}, regex=True)
df[0].to_csv('inu.csv')

# ウサギの品種一覧
url = 'https://ja.wikipedia.org/wiki/%E3%82%A6%E3%82%B5%E3%82%AE%E3%81%AE%E5%93%81%E7%A8%AE%E4%B8%80%E8%A6%A7'
df = pandas.io.html.read_html(url, encoding='utf-8', header=0)
df[0]['和名'] = df[0]['和名'].replace({'(英語版)':'', '\(.+\)':'', '\[\d+\]':'', '^en\:':''}, regex=True)
df[0]['英名'] = df[0]['英名'].replace({'\[\d+\]':'', '^en\:':''}, regex=True)
df[0].to_csv('usagi.csv')

結果

$ python get_wiki.py

$ ls
get_wiki.py inu.csv     neko.csv    usagi.csv

$ wc -l *.csv
     759 inu.csv
      91 neko.csv
     130 usagi.csv
     980 total

$ head *.csv
==> inu.csv <==
,犬種,原産地,国際畜犬連盟[1],JKC登録,画像,Unnamed: 5_level_0
,分類,番号,Unnamed: 2_level_1,Unnamed: 3_level_1,Unnamed: 4_level_1,Unnamed: 5_level_1
,,,,,,
0,アーカンソー・ジャイアント・ブルドッグ,アメリカ合衆国,,,,
1,アーフェンピンシャー,ドイツ・フランス,2G-1,186,○,
2,アイスランド・シープドッグ,アイスランド,5G-3,289,,
3,アイディ,モロッコ,2G-2,247,,
4,アイリッシュ・ウォーター・スパニエル,アイルランド,8G-3,124,○,
5,アイリッシュ・ウルフハウンド,アイルランド,10G-2,160,○,
6,アイリッシュ・コリー,アイルランド,,,,

==> neko.csv <==
,種類,原産国,発生,身体のタイプ,毛足の長さ,毛色および模様,画像
0,アビシニアン,エチオピア,自然発生種,フォーリン,短毛,複色,
1,アメリカンカール,アメリカ合衆国,突然変異,セミフォーリン,短毛もしくは長毛,単色,
2,アメリカンショートヘア,アメリカ合衆国,自然発生種,セミコビー,短毛,All,
3,アメリカンボブテイル,アメリカ合衆国,自然発生種,ロング&サブスタンシャル,短毛もしくは長毛,All,
4,アメリカンワイヤーヘア,アメリカ合衆国,突然変異,セミコビー,Rex,All but colorpoint,
5,エキゾチックショートヘア,アメリカ合衆国,交配種,コビー,Short,All,
6,エジプシャンマウ,エジプト,自然発生種,セミフォーリン,短毛,ぶち,
7,オシキャット,アメリカ合衆国,交配種,セミフォーリン,短毛,Spotted,
8,オホースアズーレス,アメリカ合衆国,突然変異,フォーリン,Short,All,

==> usagi.csv <==
,和名,英名,原産,体重(kg),毛,耳,色,ARBA,BRC,画像
0,,Alaska,ドイツ,3.2-4.1,短,立,黒,No,Yes,
1,,Altex,アメリカ,5.9,短,立,Pointed White,No,No,---
2,アメリカン,American Blue,アメリカ,4.1-5.4,短,立,青,Yes,No,
3,アメリカン,American Albino,アメリカ,4.1-5.4,短,立,白,Yes,No,
4,アメリカン・ファジー・ロップ,American Fuzzy Lop,アメリカ,1.4-1.8,長,垂,さまざま,Yes,No,
5,アメリカン・セーブル,American Sable,アメリカ,4.1-4.5,短,立,Sable,Yes,No,
6,,Argente Bleu,フランス,2.7,短,立,青,No,Yes,---
7,,Argente Brun,フランス,2.7,短,立,茶,No,Yes,---
8,,Argente Clair,フランス,2.7,短,立,Blue Silver,No,No,---

合わせて1000種類程度の品種名が取得できた。
wikipediaに限った話ではないので、お手軽に表形式の情報抽出するならかなりよさそう。

参考

Minimal Acyclic Subsequential Transducerで遊ぶ

はじめに

https://pycon.jp/2015/ja/proposals/vote/11/
Pycon2015で発表された「Pythonで作って学ぶ形態素解析」で紹介されていた辞書データ構造の「Minimal Acyclic Subsequential Transducer」について、勉強のために書いてみた。

Minimal Acyclic Subsequential Transducerとは

  • Finite State Transducerの一種
  • Transducerにおいて、initial stateが一つで、同じ入力ラベルを共有する同じ状態からのの遷移が2つ以上なく、各最終状態での最終出力文字列が高々p個のとき、p-subsequentialで、pが整数ならfinitely subsequentialというらしい
  • minimal(状態数が最少)、Acyclic(サイクルが無い)
    • Cyril Allauzen, Mehryar Mohri, Finitely Subsequential Transducersとp-Subsequentiable Transducersあたりを読む
  • Lucene/Solrで使われているFST」はこれの事
    • Kagome(Golang), Janome(python)でも採用
    • 辞書サイズがコンパクトになるので内包してもそこまで大きくならない

構築方法


OpenFstとか使うイメージでいたけど、上記で紹介されている通り、一時的な状態を作らずに、ソート済みの入力から直接FSTを構築する方法が提案されている。
詳しい手順も上記の資料(qiita)で紹介されいているので、省略。

コード

論文にできるだけ従って、入力・出力は文字列、出力は一つのみ、で実装。
問題がある部分は、状態の探索(member関数)で線形に等価な状態を探索している点や、状態の等価性判定(equal関数)が下記の方法でもよいのか怪しい点(Def.2-3?)、など。
他の実装は全然見てないので、間違ってたら後で修正する。

#include <iostream>
#include <vector>
#include <algorithm>
#include <list>
#include <cstdio>
#include <map>
#include <queue>

//入力文字列の最大長
#define MAX_WORD_SIZE 100

struct State {
  State* next[0x100];
  std::string output[0x100];
  std::string state_out;
  bool final;
  int _num;

  State(){
    clear();
  }
  State(State* s){
    _num = -1;
    state_out = s->state_out;
    final = s->final;
    for(size_t i=0; i<0x100; i++){
      next[i] = s->next[i];
      output[i] = s->output[i];
    }
  }
  ~State(){}
  void clear(){
    _num = -1;
    state_out = "";
    final = false;
    for(size_t i=0; i<0x100; i++){
      next[i] = (State*)0;
      output[i] = "";
    }
  }

  //状態の等価チェック
  // 与えられた状態sに対して、遷移状態と出力記号などが完全に一致するかを確認
  bool equal(State* s){
    if(final && s->final){
      if(state_out == s->state_out) return true;
      return false;
    }

    std::queue< std::pair< std::pair<State*,std::string>, std::pair<State*,std::string> > > que;
    for(size_t i=0; i<0x100; i++){
      que.push(std::make_pair(std::make_pair(next[i],""), std::make_pair(s->next[i],"")));
    }

    while(!que.empty()){
      std::pair<std::pair<State*,std::string>, std::pair<State*,std::string> > p = que.front(); que.pop();
      if(p.first.second != p.second.second) return false;
      if(p.first.first == NULL && p.second.first == NULL) continue;
      if(p.first.first == NULL || p.second.first == NULL) return false;
      if(p.first.first->state_out != p.second.first->state_out) return false;

      for(size_t i=0; i<0x100; i++){
        if(p.first.first->next[i] == NULL && p.second.first->next[i] == NULL) continue;
        que.push(std::make_pair(std::make_pair(p.first.first->next[i],p.first.second + (char)i), 
                                std::make_pair(p.second.first->next[i],p.second.second + (char)i)));
      }
    }
    return true;
  }
};

struct Dictionary {
  Dictionary(){}
  ~Dictionary(){
    for(std::list<State*>::iterator itr = states.begin();
        itr != states.end();
        ++itr){
      delete *itr;
    }
    states.clear();
    for(size_t i=0; i<MAX_WORD_SIZE; i++){
      if(TempState[i]){
        delete TempState[i];
        TempState[i] = NULL;
      }
    }
  }

  //辞書の状態に存在する状態かどうかチェック
  // [注意] 線形に全部等価チェックしているので非常に重い
  State* member(State* s){
    for(std::list<State*>::iterator itr = states.begin();
        itr != states.end();
        ++itr){
      State* p = *itr;
      if(s->equal(p)) return p;
    }
    return NULL;
  }

  void insert(State* s){
    states.push_back(s);
  }

  State* copy_state(State* s){
    return new State(s);
  }

  State* find_minimized(State* s){
    State* r = member(s);
    if(r == NULL){
      r = copy_state(s);
      insert(r);
    }
    return r;
  }
  void set_transition(State* s, char c, State* t){
    s->next[c] = t;
  }

  //辞書登録。inは辞書順ソートされている必要がある
  void create(const std::vector<std::string>& in, const std::vector<std::string>& out){
    for(size_t i=0; i<MAX_WORD_SIZE; i++) TempState[i] = new State;
    PreviousWord = "";
    TempState[0]->clear();

    std::string CurrentWord = "";
    std::string CurrentOutput = "";
    for(size_t t=0; t<in.size(); t++){
      CurrentWord = in[t];
      CurrentOutput = out[t];
      int i, j;
      i = 1;
      while(i<=CurrentWord.length() && i<=PreviousWord.length() && (CurrentWord[i-1] == PreviousWord[i-1])) i++;
      int PrefixLengthPlus1 = i;


      for(i=PreviousWord.size(); i>=PrefixLengthPlus1; i--){
        set_transition(TempState[i-1], PreviousWord[i-1], find_minimized(TempState[i]));
      }

      for(i=PrefixLengthPlus1; i<=CurrentWord.length(); i++){
        TempState[i]->clear();
        set_transition(TempState[i-1], CurrentWord[i-1], TempState[i]);
      }

      if(in[t] != PreviousWord){
        TempState[CurrentWord.length()]->final = true;
      }


      for(j=1; j<=PrefixLengthPlus1-1; j++){
        if(TempState[j-1] == NULL) continue;
        std::string outputStr = TempState[j-1]->output[CurrentWord[j-1]];
        std::string CommonPrefix = "";
        for(int k=0; k<std::min(outputStr.length(), CurrentOutput.length()); k++){
          if(outputStr[k] != CurrentOutput[k]) break;
          CommonPrefix += outputStr[k];
        }
        std::string WordSuffix = outputStr.substr(CommonPrefix.length());

        TempState[j-1]->output[CurrentWord[j-1]] = CommonPrefix;
        for(size_t c=0; c<0x100; c++){
          if(TempState[j]->next[c] != NULL){
            TempState[j]->output[c] = WordSuffix + TempState[j]->output[c];
          }
        }
        CurrentOutput = CurrentOutput.substr(CommonPrefix.length());
      }
      if(in[t] == PreviousWord){
        TempState[CurrentWord.length()]->state_out += CurrentOutput;
      }else{
        TempState[PrefixLengthPlus1-1]->output[CurrentWord[PrefixLengthPlus1-1]] = CurrentOutput;
      }
      PreviousWord = CurrentWord;
    }
    for(int i=CurrentWord.length(); i>=1; i--){
      TempState[i-1]->next[PreviousWord[i-1]] = find_minimized(TempState[i]);
      InitialState = find_minimized(TempState[0]);
    }
  }

  //検索
  std::string search(const std::string& query){
    std::string ret = "";
    State* p = InitialState;
    ret += p->state_out;
    for(int i=0; i<query.length(); i++){
      if(!p->next[query[i]]) return "";
      ret += p->output[query[i]];
      p = p->next[query[i]];
      ret += p->state_out;
    }
    if(p->final) return ret;
    return "";
  }


  //内部状態を出力
  void dump(){
    std::queue<State*> que;
    que.push(InitialState);
    int state_id = 0;
    while(!que.empty()){
      State* p = que.front(); que.pop();
      if(p->_num >= 0) continue;
      p->_num = state_id;
      for(int i=0; i<0x100; i++){
        if(p->next[i] == NULL) continue;
        que.push(p->next[i]);
      }
      state_id++;
    }

    std::cout << "size : " << states.size() << std::endl;
    std::map<int,int> memo;
    que.push(InitialState);
    while(!que.empty()){
      State* p = que.front(); que.pop();
      if(memo.count(p->_num) > 0) continue;
      memo[p->_num] = 1;

      std::cout << "node " << p->_num << " : [" << p->state_out << "] " << (p->final?"final":"") << std::endl;
      for(int i=0; i<0x100; i++){
        if(p->next[i] == NULL) continue;
        std::cout << "  " << (char)i << "->" << p->next[i]->_num << " [" << p->output[i] << "]" << std::endl;
        que.push(p->next[i]);
      }
    }
  }

private:
  std::list<State*> states;
  State* TempState[MAX_WORD_SIZE];
  std::string PreviousWord;
  State* InitialState;
};


int main(){
  Dictionary dic;

  std::vector<std::string> in, out;
  in.push_back("apr"); out.push_back("30");
  in.push_back("aug"); out.push_back("31");
  in.push_back("dec"); out.push_back("31");
  in.push_back("feb"); out.push_back("28");
  in.push_back("jan"); out.push_back("31");
  in.push_back("jul"); out.push_back("31");

  dic.create(in, out);

  dic.dump();

  std::cout << "-------" << std::endl;

  std::cout << "apr : " << dic.search("apr") << std::endl;
  std::cout << "aug : " << dic.search("aug") << std::endl;
  std::cout << "dec : " << dic.search("dec") << std::endl;
  std::cout << "feb : " << dic.search("feb") << std::endl;
  std::cout << "jan : " << dic.search("jan") << std::endl;
  std::cout << "jul : " << dic.search("jul") << std::endl;
  std::cout << "abc : " << dic.search("abc") << std::endl;

  return 0;
}

結果

size : 12
node 0 : [] 
  a->1 [3]
  d->2 [31]
  f->3 [28]
  j->4 [31]
node 1 : [] 
  p->5 [0]
  u->6 [1]
node 2 : [] 
  e->7 []
node 3 : [] 
  e->8 []
node 4 : [] 
  a->9 []
  u->10 []
node 5 : [] 
  r->11 []
node 6 : [] 
  g->11 []
node 7 : [] 
  c->11 []
node 8 : [] 
  b->11 []
node 9 : [] 
  n->11 []
node 10 : [] 
  l->11 []
node 11 : [] final
-------
apr : 30
aug : 31
dec : 31
feb : 28
jan : 31
jul : 31
abc : 

ランダムなアルファベット文字列でやってみても問題ないので一応大丈夫そう。
メモリ効率や高速化など実用レベルにするなら結構賢く書かないと大変そう。

GP-MIで遊ぶ

はじめに

http://live.nicovideo.jp/watch/lv228162988
先週のNL研のニコ生で、ベイズ的最適化についての招待講演を見ていて「SEは滑らかすぎる」という発言がよくわからなかったので、GP-MIを試してみる。


Contal et al., Gaussian Process Optimization with Mutual Information
http://arxiv.org/abs/1311.4825
http://econtal.perso.math.cnrs.fr/presentations/slides_icml14.pdf

コード

カーネルは、通常(?)のSquared Exponential kernel。
逆行列求めるところが重いのでEigen使う版も一緒に。
いつものごとく雑。

#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <algorithm>
#include <cmath>

static const double PI = 3.14159265358979323846264338;

//xorshift
// 注意: longではなくint(32bit)にすべき
unsigned long xor128(){
  static unsigned long x=123456789, y=362436069, z=521288629, w=88675123;
  unsigned long t;
  t=(x^(x<<11));
  x=y; y=z; z=w;
  return w=(w^(w>>19))^(t^(t>>8));
}
//[0,1)の一様乱数
double frand(){
  return xor128()%1000000/static_cast<double>(1000000); 
}
//正規乱数
double normal_rand(double mu, double sigma2){
  double sigma = sqrt(sigma2);
  double u1 = frand(), u2 = frand();
  double z1 = sqrt(-2*log(u1)) * cos(2*PI*u2);
  //double z2 = sqrt(-2*log(u1)) * sin(2*PI*u2);
  return mu + sigma*z1;
}

#ifdef USE_EIGEN
#include <Eigen/Core>
#include <Eigen/LU>
using namespace Eigen;
#else
//行列計算用
class Matrix {
  int m, n;
  std::vector<double> val;

  void splice(int i, int j){
    int N = val.size();
    if(i+(j-1)<N){
      std::vector<double> vl(N-j, 0.0);
      int vl_idx = 0, val_idx = 0;
      while(val_idx<N){
        if(val_idx<i||i+j-1<val_idx){
          vl[vl_idx] = val[val_idx];
          vl_idx++;
        }
        val_idx++;
      }
      val = vl;
    }
  }
  Matrix clone(){
    Matrix ret(m, n);
    for(int i=0; i<m; i++){
      for(int j=0; j<n; j++){
        ret.setVal(i, j, val[n*i+j]);
      }
    }
    return ret;
  }

public:
  Matrix():m(0),n(0){}
  Matrix(int m, int n):m(m),n(n){
    if(m>0 && n>0){
      for(int i=0; i<m*n; i++){
        val.push_back(0.0);
      }
    }
  }
  Matrix(const Matrix& mat){
    m = mat.m;
    n = mat.n;
    val = mat.val;
  }

  int getM() const { return m; }
  int getN() const { return n; }
  double getVal(int i, int j) const { return val[n*i+j]; }
  void setVal(int i, int j, double x){ val[n*i+j] = x; }
  bool isSquare(){ return n==m; }

  Matrix add(const Matrix& mat){
    if(m == mat.m && n == mat.n){
      Matrix ret(m, n);
      for(int i=0; i<m; i++){
        for(int j=0; j<n; j++){
          ret.setVal(i, j, val[n*i+j] + mat.getVal(i,j));
        }
      }
      return ret;
    }
    return Matrix(-1, -1);
  }
  Matrix operator+(const Matrix& mat){ return add(mat); }

  Matrix sub(const Matrix& mat){
    if(m == mat.m && n == mat.n){
      Matrix ret(m, n);
      for(int i=0; i<m; i++){
        for(int j=0; j<n; j++){
          ret.setVal(i, j, val[n*i+j] - mat.getVal(i,j));
        }
      }
      return ret;
    }
    return Matrix(-1, -1);
  }
  Matrix operator-(const Matrix& mat){ return sub(mat); }

  Matrix prod(const Matrix& mat){
    if(n == 1 && mat.n == 1 && m == mat.m){
      Matrix ret(m, 1);
      for(int i=0; i<m; i++){
        ret.setVal(i, 0, getVal(i, 0) * mat.getVal(i, 0));
      }
      return ret;
    }
    else if(n == mat.m){
      Matrix ret(m, mat.n);
      for(int i=0; i<m; i++){
        for(int j=0; j<mat.n; j++){
          double d = 0.0;
          for(int k=0; k<n; k++){
            d += val[n*i+k] * mat.getVal(k,j);
          }
          ret.setVal(i, j, d);
        }
      }
      return ret;
    }
    return Matrix(-1, -1);
  }
  Matrix operator*(const Matrix& mat){ return prod(mat); }

  void time(double x){
    for(int i=0; i<m; i++){
      for(int j=0; j<n; j++){
        val[n*i+j] *= x;
      }
    }
  }

  Matrix transpose(){
    Matrix ret(n, m);
    for(int i=0; i<m; i++){
      for(int j=0; j<n; j++){
        ret.setVal(j, i, val[n*i+j]);
      }
    }
    return ret;
  }
  
  double cofactor(int i, int j){
    Matrix mat = clone();
    mat.splice(i*mat.n, mat.m);
    mat.m -= 1;
    for(int k=mat.m-1; k>=0; k--){
      mat.splice(k*mat.n+j, 1);
    }
    mat.n -= 1;
    return mat.det() * ( ((i+j+2)%2==0) ? 1 : -1);
  }
  double det(){
    if(isSquare()){
      if(m == 2){
        return val[0]*val[3]-val[1]*val[2];
      }else{
        double d = 0;
        for(int k=0; k<n; k++){
          d += val[k] * cofactor(0, k);
        }
        return d;
      }
    }else{
      return 0.0;
    }
  }
  Matrix cofactorMatrix(){
    Matrix mat(m, n);
    for(int i=0; i<m; i++){
      for(int j=0; j<n; j++){
        mat.setVal(j, i, cofactor(i, j));
      }
    }
    return mat;
  }
  Matrix inverse(){
    if(isSquare()){
      double d = det();
      if(d != 0){
        Matrix mat;
        if(m>2){
          mat = cofactorMatrix();
        } else {
          mat = Matrix(2, 2);
          mat.setVal(0, 0, val[3]);
          mat.setVal(0, 1, -val[1]);
          mat.setVal(1, 0, -val[2]);
          mat.setVal(1, 1, val[0]);
        }
        mat.time(1 / d);
        return mat;
      }
    }else{
      return Matrix(-1, -1);
    }
    return Matrix(-1, -1);
  }
};
std::ostream& operator<<(std::ostream& os, const Matrix& mat){
  for(int i=0; i<mat.getM(); i++){
    for(int j=0; j<mat.getN(); j++){
      os << mat.getVal(i,j) << " ";
    }
    os << std::endl;
  }
  return os;
}
#endif



class GP_MI {  
  double d; //xの次元
  double alpha; //活用探索のトレードオフ調整用パラメータ
  double noise_sigma2; //ノイズの分散
  int ITER; //サンプリング回数
  double gamma;

  bool flg_obs; //推定,観測を繰り返しているかどうかの確認用フラグ

  //観測データ
  std::vector< std::vector<double> > obs_x;
  std::vector<double> obs_y;

  //各x_iの探索範囲
  std::vector<double> x_min, x_max;

  //kernel(x,x')の値を求める
  double calc_kernel(const std::vector<double>& x1, const std::vector<double>& x2){
    //Squared exponential kernel
    double sigma2 = 1.0;
    double l2 = 0.01; //比較的ガタガタを試すので小さめ

    double z = 0.0;
    for(size_t i=0; i<x1.size(); i++){
      z += pow(((x1[i] - x2[i])), 2.0) / (2.0 * l2);
    }
    return sigma2 * exp(-z);
  }

public:
  GP_MI(int d, double alpha, double noise_sigma2, int ITER):
    d(d),
    alpha(alpha),
    noise_sigma2(noise_sigma2),
    gamma(0.0),
    x_min(d, 0.0),
    x_max(d, 1.0),
    ITER(ITER),
    flg_obs(false)
  { }

  //xの探索範囲を指定
  void set_x_range(int di, double mn, double mx){
    x_min[di] = mn;
    x_max[di] = mx;
  }

  //現在までの観測結果から推定される関数をもとに、一番探した方がよいxを返す
  std::vector<double> get_best_x(){
    //前回の観測が行われたかチェック
    if(!flg_obs){
      std::cerr << "please observe and set" << std::endl;
      return std::vector<double>();
    }
    flg_obs = false;
    
    //前計算C_T, Y_T
#ifdef USE_EIGEN
    MatrixXd C_T = MatrixXd::Zero(obs_y.size(), obs_y.size());
    MatrixXd Y_T = MatrixXd::Zero(obs_y.size(), 1);
    for(size_t i=0; i<obs_y.size(); i++){
      for(size_t j=0; j<obs_y.size(); j++){
        C_T(i,j) = calc_kernel(obs_x[i], obs_x[j]) + (i==j?noise_sigma2:0.0);
      }
    }
    for(size_t i=0; i<obs_y.size(); i++){
      Y_T(i,0) = obs_y[i];
    }
    MatrixXd C_T_inv = C_T.inverse();
#else
    Matrix C_T(obs_y.size(), obs_y.size());
    Matrix Y_T(obs_y.size(), 1);
    for(size_t i=0; i<obs_y.size(); i++){
      for(size_t j=0; j<obs_y.size(); j++){
        C_T.setVal(i,j, calc_kernel(obs_x[i], obs_x[j]) + (i==j?noise_sigma2:0.0) );
      }
    }
    for(size_t i=0; i<obs_y.size(); i++){
      Y_T.setVal(i,0, obs_y[i]);
    }
    Matrix C_T_inv = C_T.inverse();
#endif

    //ITER回サンプリングしてargmaxを求める
    double best_a = -1.0e10;
    std::vector<double> best_x;
    double best_sigma2 = 0;

    for(size_t iter=0; iter<ITER; iter++){
      //適当なxを生成
      std::vector<double> x(d);
      for(size_t i=0; i<d; i++) x[i] = (x_max[i]-x_min[i])*frand() + x_min[i];
      
#ifdef USE_EIGEN
      MatrixXd kT = MatrixXd::Zero(obs_y.size(), 1);
      for(size_t i=0; i<obs_y.size(); i++){
        kT(i,0) = calc_kernel(obs_x[i], x);
      }

      double kxx = calc_kernel(x, x);

      MatrixXd kCY = kT.transpose() * ( C_T_inv * Y_T );
      MatrixXd kCk = kT.transpose() * ( C_T_inv * kT );

      double mu = kCY(0,0);
      double sigma2 = kxx - kCk(0,0);
#else
      Matrix kT(obs_y.size(), 1);
      for(size_t i=0; i<obs_y.size(); i++){
        kT.setVal(i,0, calc_kernel(obs_x[i], x));
      }

      double kxx = calc_kernel(x, x);

      Matrix kCY = kT.transpose() * ( C_T_inv * Y_T );
      Matrix kCk = kT.transpose() * ( C_T_inv * kT );

      double mu = kCY.getVal(0,0);
      double sigma2 = kxx - kCk.getVal(0,0);
#endif

      //獲得関数の値を求める
      double phi = sqrt(alpha) * (sqrt(sigma2 + gamma) - sqrt(gamma));      
      if(best_a < mu + phi){
        best_a = mu + phi;
        best_x = x;
        best_sigma2 = sigma2;
      }
    }

    gamma += best_sigma2;
    return best_x;
  }

  //get_best_x()で得られたxに対する実際の関数の値を、観測結果として登録
  void set_observation(std::vector<double>& x, double y){
    obs_x.push_back(x);
    obs_y.push_back(y);
    flg_obs = true;
  }

};



//ブラックボックスな関数
double func(const std::vector<double>& x){
  return x[0] * sin(10.0 * x[0]);
}

int main(){
  //xの次元数  alpha  noise_sigma2  サンプリング回数
  GP_MI gpmi(1, 10.0, 0.0001, 500);

  //各次元の範囲
  gpmi.set_x_range(0, 0.0, 1.0);

  //最初に何点か求めておく
  std::vector<double> x(1);
  x[0] = 0;
  gpmi.set_observation(x, func(x));
  x[0] = 0.5;
  gpmi.set_observation(x, func(x));
  x[0] = 1.0;
  gpmi.set_observation(x, func(x));

  //自動で探索点を選ぶ
  for(int i=0; i<30; i++){
    std::vector<double> best_x = gpmi.get_best_x();
    std::cout << i << " : best_x = " << best_x[0] << std::endl;
    gpmi.set_observation(best_x, func(best_x));
  }

  return 0;
}

結果

最初に0,0.5,1.0の3点だけ求めた状態からスタート。
αがlog(2/δ), 0<δ<1とかだけど、小さいと隣の山に探しに行ってくれないので、大きいけど、α=10と50で試してみる。
グラフは、赤がμとσ^2、緑が獲得関数μ+φ。

x * sin(10.0 * x)

【α=10】

最適解は求められているけど、左の山が分散大きくても獲得関数の値が低いまま。
【α=50】

αを大きくする(大きくしすぎ?)と、左の山も探してくれてる。

x * cos(30.0 * x)

もっとガタガタしたもの。
【α=10】

うまくいっていない(本当の最適点はx=0.84ぐらい)。
動画で「滑らかすぎる」という発言があったけど、滑らかすぎて、関数の形の推定がうまくいかない=適切な探索点をみつけられない、ということか(?)。
ただ、ARDなSEカーネルやMaternカーネルの実装、最適なパラメータ探索(MCMC)がちょっとよくわかってない・・・


http://www.slideshare.net/issei_sato/deep-learning-41310617
> BOツール公開予定

ω・)待機。

Elman netを試す

はじめに

プロフェッショナルな「深層学習」本で紹介されているRNNの一種のElman netを書いてみる。

Recurrent Neural Network(RNN)とは

  • 再帰ニューラルネット
  • ネットワーク内部に内部有向閉路を持つようなニューラルネットの総称
    • Feedforwardの時は、入力層から出力層へ一方方向
  • この構造のおかげで、時系列や言語モデル、系列ラベリングなど前の状態を持つような問題に対して考慮できる
  • いろんな種類がある(以下はwikipediaから)
    • Fully Recurrent network
      • Hopfield network
      • Boltzmann machine
    • Simple recurrent network
      • Elman net
      • Jordan net
    • Echo state network
    • Long short term memory(LSTM) network
    • Bi-directional RNN
    • Hierarchical RNN
    • Stochastic neural networks
Simple recurrent network
  • 言葉のとおり、シンプルな構造のRNN
  • 代表的には「Elman net」と「Jordan net」がある
  • 3層(入力層、隠れ層、出力層)のFeedforward Neural netについて、どこからどこへ閉路を作るかで異なる
    • Elman net : 隠れ層から隠れ層への辺がある
    • Jordan net : 出力層から隠れ層への辺がある

コード

「深層学習」本では、Elman netのBPTT法(Backpropagation through time、時間でネットワークを分けて普通に逆誤差伝播)での学習が紹介されていると思われるので、これを書いてみる。

#include <iostream>
#include <vector>
#include <cstdio>
#include <algorithm>
#include <cmath>

static const double PI = 3.14159265358979323846264338;

//xorshift
// 注意: longではなくint(32bit)にすべき
unsigned long xor128(){
  static unsigned long x=123456789, y=362436069, z=521288629, w=88675123;
  unsigned long t;
  t=(x^(x<<11));
  x=y; y=z; z=w;
  return w=(w^(w>>19))^(t^(t>>8));
}
//[0,1)の一様乱数
double frand(){
  return xor128()%1000000/static_cast<double>(1000000); 
}
//正規乱数
double normal_rand(double mu, double sigma2){
  double sigma = sqrt(sigma2);
  double u1 = frand(), u2 = frand();
  double z1 = sqrt(-2*log(u1)) * cos(2*PI*u2);
  //double z2 = sqrt(-2*log(u1)) * sin(2*PI*u2);
  return mu + sigma*z1;
}


//隠れ層が1層の単純なRNN
// BPTT(Backpropagation through time)法で学習
class SimpleRNN {
  double eps; //学習率
  int Nin, Nhid, Nout; //各層のユニット数(バイアスを除く)
  std::vector< std::vector<double> > W; //時刻t-1での隠れ層から時刻tでの隠れ層へ
  std::vector< std::vector<double> > Win; //時刻tでの入力層から隠れ層へ
  std::vector< std::vector<double> > Wout; //時刻tでの隠れ層から出力層へ

  std::vector< std::vector<double> > u, v; //各時刻での隠れ層の入力, 出力層の入力
  std::vector< std::vector<double> > z, y; //各時刻での隠れ層の出力, 出力層の出力
  std::vector< std::vector<double> > delta, delta_out; //各時刻での各ユニットのδ値

public:
  SimpleRNN(int Nin, int Nhid, int Nout, double eps):
    eps(eps),
    Nin(Nin),
    Nhid(Nhid),
    Nout(Nout),
    W(Nhid+1, std::vector<double>(Nhid, 0)),
    Win(Nin+1, std::vector<double>(Nhid, 0)),
    Wout(Nhid+1, std::vector<double>(Nout, 0))
  {
    for(int i=0; i<Nhid+1; i++){
      for(int j=0; j<Nhid; j++){
        W[i][j] = normal_rand(0.0, 0.1);
      }
    }
    for(int i=0; i<Nin+1; i++){
      for(int j=0; j<Nhid; j++){
        Win[i][j] = normal_rand(0.0, 0.1);
      }
    }
    for(int i=0; i<Nhid+1; i++){
      for(int j=0; j<Nout; j++){
        Wout[i][j] = normal_rand(0.0, 0.1);
      }
    }
  }

  std::vector<int> forward_propagation(const std::vector< std::vector<double> >& in){
    int T = in.size();
    
    u = std::vector< std::vector<double> >(T, std::vector<double>(Nhid, 0));
    v = std::vector< std::vector<double> >(T, std::vector<double>(Nout, 0));
    z = std::vector< std::vector<double> >(T, std::vector<double>(Nhid+1, 0));
    y = std::vector< std::vector<double> >(T, std::vector<double>(Nout, 0));

    //各時刻tでの値を求める
    std::vector<int> ret;

    for(int t=0; t<T; t++){
      for(int i=0; i<Nhid; i++){
        u[t][i] = 0.0;
      }
      //入力層の出力->隠れ層の入力
      for(int i=0; i<Nin; i++){
        for(int j=0; j<Nhid; j++){
          u[t][j] += in[t][i] * Win[i][j];
        }
      }
      for(int i=0; i<Nhid; i++){ //バイアス
        u[t][i] += 1.0 * Win[Nin][i];
      }
      //時刻t-1での隠れ層の出力->時刻tでの隠れ層の入力
      for(int i=0; i<Nhid+1; i++){
        if(t!=0){
          for(int j=0; j<Nhid; j++){
            u[t][j] += z[t-1][i] * W[i][j];
          }
        }
        if(t==0 && i==Nhid){ //バイアス
          for(int j=0; j<Nhid; j++){
            u[t][j] += 1.0 * W[Nin][j];
          }
        }
      }
      //時刻tでの隠れ層の出力
      for(int i=0; i<Nhid; i++){
        z[t][i] = 1.0 / (1.0 + exp(-u[t][i]));
      }
      z[t][Nhid] = 1.0;
      //時刻tでの出力層の入力
      for(int i=0; i<Nhid+1; i++){
        for(int j=0; j<Nout; j++){
          v[t][j] += z[t][i] * Wout[i][j];
        }
      }
      //時刻tでの出力層の出力
      double Z = 0;
      double mx = -1.0;
      int mx_i = -1;
      for(int i=0; i<Nout; i++){
        Z += exp(v[t][i]);
      }
      for(int i=0; i<Nout; i++){
        y[t][i] = exp(v[t][i]) / Z;

        if(mx < y[t][i]){
          mx = y[t][i];
          mx_i = i;
        }
      }
      ret.push_back(mx_i);
    }
    return ret;
  }

  double back_propagation(const std::vector< std::vector<double> >& in, const std::vector<int>& out){
    double err = 0.0;
    int T = in.size();
    std::vector<int> res = forward_propagation(in);

    delta = std::vector< std::vector<double> >(T, std::vector<double>(Nhid, 0));
    delta_out = std::vector< std::vector<double> >(T, std::vector<double>(Nout, 0));

    //時刻の逆方向からdelta値と重みの更新を連続的にしていく
    for(int t=T-1; t>=0; t--){
      //時刻tでの出力層のdelta値
      for(int i=0; i<Nout; i++){
        if(out[t] == i){
          delta_out[t][i] = y[t][i] - 1.0;
          err += - 1.0 * log(y[t][i]);
        }else{
          delta_out[t][i] = y[t][i] - 0.0;
        }
      }
      //時刻tでの隠れ層の出力層からのdelta値
      for(int i=0; i<Nhid; i++){
        for(int j=0; j<Nout; j++){
          double fu = 1.0 / (1.0 + exp(-u[t][i]));
          double fdash = fu * (1.0 - fu);
          delta[t][i] += Wout[i][j] * delta_out[t][j] * fdash;
        }
      }
      //時刻tでの隠れ層の時刻t+1での隠れ層からのdelta値
      for(int i=0; i<Nhid; i++){
        for(int j=0; j<Nhid; j++){
          if(t+1>=T) continue; //時刻T以降の場合はdelta=0として扱う

          double fu = 1.0 / (1.0 + exp(-u[t][i]));
          double fdash = fu * (1.0 - fu);
          delta[t][i] += W[i][j] * delta[t+1][j] * fdash;
        }
      }
      //delta値を使って各重みの微分を計算し、SGDで重みを更新
      for(int j=0; j<Nhid+1; j++){ //隠れ層->出力層
        for(int k=0; k<Nout; k++){
          Wout[j][k] -= eps * (delta_out[t][k] * z[t][j]);
        }
      }
      for(int pj=0; pj<Nhid+1; pj++){ //隠れ層->隠れ層
        for(int j=0; j<Nhid; j++){
          if(t-1>=0){
            W[pj][j] -= eps * (delta[t][j] * z[t-1][pj]);
          }else{ //時刻t<0の場合は、バイアスだけ1でそれ以外の出力は0として扱う
            if(pj==Nhid){ //バイアス
              W[pj][j] -= eps * (delta[t][j] * 1.0);
            }
          }
        }
      }
      for(int i=0; i<Nin+1; i++){ //入力層->隠れ層
        for(int j=0; j<Nhid; j++){
          if(i<Nin){
            Win[i][j] -= eps * (delta[t][j] * in[t][i]);
          }else{ //バイアス
            Win[i][j] -= eps * (delta[t][j] * 1.0);
          }
        }
      }

    }
    return err;
  }

};

int main(){
  const int TERM = 500; //TERM個単位でerrを確認
  const double finish_err = 0.01; //errがfinish_err未満になったら学習終了

  int Nin, Nhid, Nout;
  double eps;
  int trainN, testN;

  std::cin >> Nin >> Nhid >> Nout;
  std::cin >> eps;
  
  /// 学習 ////////////////////
  std::cin >> trainN;
  std::vector< std::vector< std::vector<double> > > in;
  std::vector< std::vector<int> > out;
  for(int i=0; i<trainN; i++){
    int t;
    std::cin >> t;

    std::vector< std::vector<double> > in_one;
    std::vector<int> out_one;
    for(int j=0; j<t; j++){
      double val;
      int outval;
      std::cin >> outval;
      std::vector<double> v;
      for(int k=0; k<Nin; k++){
        std::cin >> val;
        v.push_back(val);
      }
      in_one.push_back(v);
      out_one.push_back(outval);
    }

    in.push_back(in_one);
    out.push_back(out_one);
  }
  //データシャッフル用
  std::vector<int> rnd;
  for(int i=0; i<trainN; i++){
    rnd.push_back(i);
  }
  std::random_shuffle(rnd.begin(), rnd.end());

  //学習ループ
  SimpleRNN rnn(Nin, Nhid, Nout, eps);
  int iter = 0;
  double err = 0.0;
  while(true){
    //TERM個単位で確認
    if(iter!=0 && iter%TERM == 0){
      err /= TERM;
      std::cerr << "err = " << err << std::endl;
      if(err < finish_err) break;
      err = 0;
    }

    err += rnn.back_propagation(in[rnd[iter%trainN]], out[rnd[iter%trainN]]);
    iter++;
  }

  /// テスト ////////////////////
  std::cin >> testN;
  for(int i=0; i<testN; i++){
    int t;
    std::cin >> t;

    std::vector< std::vector<double> > in_one;
    std::vector<int> out_one;
    for(int j=0; j<t; j++){
      double val;
      int outval;
      std::cin >> outval;
      std::vector<double> v;
      for(int k=0; k<Nin; k++){
        std::cin >> val;
        v.push_back(val);
      }
      in_one.push_back(v);
      out_one.push_back(outval);
    }

    //結果の出力
    std::vector<int> res = rnn.forward_propagation(in_one);
    std::cout << "ref:";
    for(int j=0; j<out_one.size(); j++){
      std::cout << " " << out_one[j];
    }
    std::cout << std::endl;

    std::cout << "out:";
    for(int j=0; j<res.size(); j++){
      std::cout << " " << res[j];
    }
    std::cout << std::endl;

    std::cout << std::endl;
  }

  return 0;
}

入力ファイルの形式

[入力層のユニット数]  [隠れ層のユニット数]  [出力層のユニット数]
[学習率]
[学習インスタンス数]
[ケース1の数]
[出力1]  [次元1の値]  [次元2の値] ...
[出力2]  [次元1の値]  [次元2の値] ...
[出力3]  [次元1の値]  [次元2の値] ...
...
[テストインスタンス数]
[ケース1の数]
[出力1]  [次元1の値]  [次元2の値] ...
[出力2]  [次元1の値]  [次元2の値] ...
[出力3]  [次元1の値]  [次元2の値] ...
...

結果

おもちゃなケースで確認だけ。
http://d.hatena.ne.jp/jetbead/20111125/1322236626 でのサンプルを今回の形式に変換したもの。

  • 入力
10 10 3
0.01
6
3
2  1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2  0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2  0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4
2  1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2  0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
0  0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0
1  0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3
2  0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0
2  0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
2  0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4
2  0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0
2  0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
0  0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0
1  0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3
2  1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2  0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0
2  0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0
4
2  0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0
2  0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0
2  0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0
2  0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0
6
3
2  1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2  0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2  0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4
2  1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2  0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
0  0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0
1  0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3
2  0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0
2  0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
2  0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4
2  0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0
2  0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
0  0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0
1  0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3
2  1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2  0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0
2  0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0
4
2  0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0
2  0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0
2  0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0
2  0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0
  • 出力
$ ./a.out < in
err = 2.20794
err = 1.84924
err = 1.54379
err = 1.22335
err = 0.931081
err = 0.681177
err = 0.493641
err = 0.360733
err = 0.265542
err = 0.198631
err = 0.153081
err = 0.121373
err = 0.0987013
err = 0.0824887
err = 0.0702189
err = 0.0606353
err = 0.0533114
err = 0.0473444
err = 0.0423609
err = 0.0383958
err = 0.034996
err = 0.0320187
err = 0.0295968
err = 0.0274426
err = 0.0254882
err = 0.0238809
err = 0.0224107
err = 0.0210389
err = 0.0199064
err = 0.0188471
err = 0.0178354
err = 0.0170008
err = 0.0162053
err = 0.0154302
err = 0.0147935
err = 0.0141766
err = 0.0135647
err = 0.0130652
err = 0.0125743
err = 0.0120795
err = 0.0116788
err = 0.0112797
err = 0.0108714
err = 0.010544
err = 0.0102138
err = 0.00987132
ref: 2 2 2
out: 2 2 2

ref: 2 2 0 1
out: 2 2 0 1

ref: 2 2 2
out: 2 2 2

ref: 2 2 0 1
out: 2 2 0 1

ref: 2 2 2
out: 2 2 2

ref: 2 2 2 2
out: 2 2 2 2

ちゃんと収束して学習データはちゃんとラベルつけられている。
しかし、展開する時間tが長くなるほどネットワークが深く(deep)なってしまい、勾配消失(または発散)問題が起きてしまってうまく学習できない。

多層ニューラルネットを試す

はじめに

FeedForwardNeuralNetwork。プロフェッショナルな「深層学習」本のバックプロパゲーションの導出が丁寧にされていてわかりやすかったので、それに合わせて書いてみる。

各層の活性化関数はロジスティック(シグモイド)関数、出力層の活性化関数はソフトマックス関数、誤差関数は交差エントロピー

コード

1インスタンスごとに重みを更新(SGD)。
直近TERM個のインスタンスの誤差平均が終了条件を満たしたら終了。
学習が収束してくれているので大丈夫そう。

#include <iostream>
#include <vector>
#include <cstdio>
#include <algorithm>
#include <cmath>

static const double PI = 3.14159265358979323846264338;

//xorshift
// 注意: longではなくint(32bit)にすべき
unsigned long xor128(){
  static unsigned long x=123456789, y=362436069, z=521288629, w=88675123;
  unsigned long t;
  t=(x^(x<<11));
  x=y; y=z; z=w;
  return w=(w^(w>>19))^(t^(t>>8));
}
//[0,1)の一様乱数
double frand(){
  return xor128()%1000000/static_cast<double>(1000000); 
}
//正規乱数
double normal_rand(double mu, double sigma2){
  double sigma = sqrt(sigma2);
  double u1 = frand(), u2 = frand();
  double z1 = sqrt(-2*log(u1)) * cos(2*PI*u2);
  //double z2 = sqrt(-2*log(u1)) * sin(2*PI*u2);
  return mu + sigma*z1;
}


struct UnitW {
  double u; //ユニットへの入力
  double z; //ユニットの出力
  double delta; //ユニットでのδ_j^l = ∂E_n / ∂u_j^(l)
  std::vector<double> w; //このユニットから次の層のユニットへの重み
  UnitW(int num_next_layer_node):w(num_next_layer_node, 0){
    for(int i=0; i<w.size(); i++){
      w[i] = normal_rand(0.0, 0.1);
    }
  }
};


class MultiLayerNeuralNetwork {
  double eps; //学習率
  std::vector< std::vector<UnitW> > network;

  void init_network(const std::vector<int>& num_layer_node){
    for(int i=0; i<num_layer_node.size(); i++){
      network.push_back( std::vector<UnitW>() );

      //(num_layer_node[i])番目をバイアスとして使うのですべての層で+1用意
      for(int j=0; j<num_layer_node[i] + 1; j++){
        if(i < num_layer_node.size()-1){ //入力層、隠れ層
          network[i].push_back(UnitW(num_layer_node[i+1]));
          
          if(j == num_layer_node[i]){ //バイアスの重みは0
            for(int k=0; k<network[i][j].w.size(); k++){
              network[i][j].w[k] = 0.0;
            }
          }
        }else{ //出力層
          network[num_layer_node.size()-1].push_back(UnitW(0));
        }
      }
    }
  }

public:
  MultiLayerNeuralNetwork(double eps, const std::vector<int>& num_layer_node):eps(eps){
    init_network(num_layer_node);
  }

  std::vector<double> forward_propagation(const std::vector<double>& in){
    for(int i=0; i<network.size(); i++){
      for(int j=0; j<network[i].size(); j++){
        network[i][j].u = 0.0;
      }
    }

    //入力層
    for(int j=0; j<network[0].size()-1; j++){
      network[0][j].z = in[j];
    }
    network[0][network[0].size()-1].z = 1.0;   

    //隠れ層と出力層
    for(int i=0; i<network.size()-1; i++){
      for(int j=0; j<network[i].size(); j++){
        //現在の層の出力zと重みを合わせて次の層の入力に渡す
        for(int k=0; k<network[i+1].size()-1; k++){
          network[i+1][k].u += network[i][j].z * network[i][j].w[k];
        }
      }
      //次の層の出力zを計算
      if(i < network.size()-2){ //隠れ層
        for(int j=0; j<network[i+1].size(); j++){
          network[i+1][j].z = 1.0 / (1.0 + exp(-network[i+1][j].u));
        }
        network[i+1][network[i+1].size()-1].z = 1.0; //バイアスは常に出力は1
      }else{ //出力層
        double Z = 0.0;
        for(int j=0; j<network[i+1].size()-1; j++){
          Z += exp(network[i+1][j].u);
        }
        for(int j=0; j<network[i+1].size()-1; j++){
          network[i+1][j].z = exp(network[i+1][j].u) / Z;
        }
      }
    }

    std::vector<double> ret;
    for(int i=0; i<network[network.size()-1].size()-1; i++){
      ret.push_back(network[network.size()-1][i].z);
    }
    return ret;
  }

  double back_propagation(const std::vector<double>& in, const std::vector<double>& d){
    double err = 0.0;
    for(int i=0; i<network.size(); i++){
      for(int j=0; j<network[i].size(); j++){
        network[i][j].delta = 0.0;
      }
    }

    //現在のネットワークでの結果を計算
    std::vector<double> y = forward_propagation(in);

    //出力層のdelta値の計算
    for(int i=0; i<network[network.size()-1].size()-1; i++){
      network[network.size()-1][i].delta = y[i] - d[i];
      err += - d[i] * log(y[i]);
    }

    //各ユニットのdelta値の計算
    for(int i=network.size()-2; i>=0; i--){
      for(int j=0; j<network[i].size(); j++){
        for(int k=0; k<network[i+1].size()-1; k++){
          double fu = 1.0 / (1.0 + exp(-network[i][j].u));
          double fdash = fu * (1.0 - fu);
          network[i][j].delta += network[i+1][k].delta * network[i][j].w[k] * fdash;
        }
      }
    }
    
    //各ユニットのdelta値を使って各重みにおける微分を計算し、SGDで重みを更新
    for(int i=network.size()-2; i>=0; i--){
      for(int j=0; j<network[i].size(); j++){
        for(int k=0; k<network[i+1].size()-1; k++){
          network[i][j].w[k] -= eps * (network[i+1][k].delta * network[i][j].z);
        }
      }
    }
    return err;
  }
};


int main(){
  const int TERM = 500; //TERM個単位でerrを確認
  const double finish_err = 0.01; //errがfinish_err未満になったら学習終了


  int layerN, trainN, testN;
  double eps;
  std::cin >> layerN;

  //各層のユニット数
  std::vector<int> num_layer_node;
  for(int i=0; i<layerN; i++){
    int num;
    std::cin >> num;
    num_layer_node.push_back(num);
  }
  int inN = num_layer_node[0];
  int outN = num_layer_node[num_layer_node.size()-1];

  std::cin >> eps;

  /// 学習 ////////////////////
  std::cin >> trainN;
  std::vector< std::vector<double> > out, in;
  for(int i=0; i<trainN; i++){
    std::cerr << "\r" << i;
    std::vector<double> out_one(outN, 0), in_one;
    int t;
    std::cin >> t;
    out_one[t] = 1;
    out.push_back(out_one);
    for(int j=0; j<inN; j++){
      double d;
      std::cin >> d;
      in_one.push_back(d);
    }
    in.push_back(in_one);
  }
  std::cerr << std::endl;

  //データシャッフル用
  std::vector<int> rnd;
  for(int i=0; i<trainN; i++){
    rnd.push_back(i);
  }
  std::random_shuffle(rnd.begin(), rnd.end());


  //学習ループ
  MultiLayerNeuralNetwork nn(eps, num_layer_node);
  int iter = 0;
  double err = 0.0;
  while(true){
    //TERM個単位で確認
    if(iter != 0 && iter%TERM == 0){
      err /= TERM;
      std::cerr << "err = " << err << std::endl;
      if(err < finish_err) break;
      err = 0;
    }

    err += nn.back_propagation(in[rnd[iter%trainN]], out[rnd[iter%trainN]]);
    iter++;
  }

  /// テスト ////////////////////
  std::cin >> testN;
  int match = 0;
  for(int i=0; i<testN; i++){
    std::vector<double> in_one;
    int out_t;
    std::cin >> out_t;

    for(int j=0; j<inN; j++){
      double d;
      std::cin >> d;
      in_one.push_back(d);
    }

    std::vector<double> res = nn.forward_propagation(in_one);
    double res_mx = 0;
    int res_i = 0;
    for(int j=0; j<res.size(); j++){
      if(res[j]>res_mx){
        res_mx = res[j];
        res_i = j;
      }
    }

    if(out_t == res_i) match++;
    std::cout << out_t << "\t" << res_i << "\t" << res_mx << std::endl;
  }

  //Accuracyの出力
  std::cout << "Acc = " << match/(double)testN << std::endl;

  return 0;
}

入力ファイルの形式

[layer数]
[入力層のユニット数]  [隠れ層1層目のユニット数]  ...  [隠れ層k層目のユニット数]  [出力層のユニット数]
[学習率]
[学習インスタンス数]
[出力]  [次元1の値]  [次元2の値] ...
...
テストインスタンス数
[出力]  [次元1の値]  [次元2の値] ...
...

出力は、0〜出力ユニット数-1の値。入力の次元は、入力層のユニット数と同じ事が必要。

結果

隠れ層1層のケースで確認。

XORパターン
  • 入力
$ cat xor.txt
3
2 2 2
0.01
4
0  0 0
1  0 1
1  1 0
0  1 1
4
0  0 0
1  0 1
1  1 0
0  1 1
  • 出力
$ ./a.out < xor.txt
3
err = 0.697136
...
err = 0.00992856
0	0	0.992468
1	1	0.991095
1	1	0.991083
0	0	0.986047
Acc = 1
MNISTの手書き数字認識

scikit_learn_dataのやつ。

  • 入力
$ cat mnist.txt
3
784 100 10
0.01
60000
0  0.0 0.0 ...(省略)
...(省略)
10000
0  0.0 0.0 ...(省略)
...(省略)
  • 出力
$ ./a.out < mnist.txt
59999
err = 2.28309
err = 1.34394
err = 1.08896
...
Acc = 0.9776
  • 収束までにかかる時間

CPU(i7-4790)計算で、結果でるまで16分30秒ぐらい。
コードが違うけど、chainerだとGPU使って30秒程度で終わる、、、やっぱりchainerだな!

参考

Feature-Weighted Linear Stackingメモ

はじめに

Sill et al., Feature-Weighted Linear Stacking, 2009
http://arxiv.org/abs/0911.0460

最近、コンペ上位者の手法としてよく見かける手法「Stacking」の一つについてメモ。

Stacking

Feature-Weighted Linear Stacking(FWLS)

概要的には、各学習結果をまとめあげる部分を線形和として表す際に、メタ素性をさらに考慮することで、細かい調整を行えるようにした感じ。

  • L個の機械学習モデル g_i
    • 入力xに対して、g_i(x)は予測結果を表す
  • M個のメタ素性関数 f_i
    • 入力xに対して、f_i(x)はメタ素性に関する何らかの実数を表す
Standard linear regression stacking
  • 単純に各学習器を線形和として考え、その重みを学習する
  • ブレンドした予測結果b(x) = Σ_i w_i * g_i(x)
    • このモデルごとの重みw_iによってブレンドされる
    • w_iは学習で求める
  • Leo Breiman, Stacked Regressions, 1996が参照されてるけど、このBreimanさんみた事あると思ったらランダムフォレストの人だった
Feature-weighted linear stacking
  • 上記に加えさらに「メタ素性」を考慮し、各学習器とメタ素性の組み合わせの線形和として考え、その重みを学習する
  • ブレンドした予測結果b(x)=Σ_{i,j} v_{ij} f_j(x) g_i(x)
    • 上記の重みw_iをメタ素性の関数をブレンドしたものとする
    • w_i(x) = Σ_j v_{ij} f_j(x)
    • v_{ij}は学習で求める
FWLS optimization problem
  • 最適化したい問題(v_{ij}の学習)は、
  • min_v Σ_入力x Σ_{i,j} (v_{ij} f_j(x) g_i(x) - 正解y(x))^2
  • ただし、入力xはスタッキングのパラメータを求めるために各学習器の学習で使っていない入力集合を使ったほうがよい
    • K-fold cross validationとか、各学習に使う入力集合には気をつける

モデルの数

  • 大体数十から数百ぐらいを組み合わせる模様

メタ素性の例

  • 常に1を返すf_0(x)=1やg_0(x)=1を用意する事で、結果がよくなったりする
  • NetflixPrizeでのモデルブレンディングに使ったメタ素性
    • ユーザーが特定の期間で3つ以上の評価をしたかのbinary
    • 評価された回数の対数
    • 評価した累計日数の対数
    • 映画の平均視聴率のベイズ推定
    • ユーザーの評価数の対数
    • ユーザーの平均視聴率
    • など
  • LSHTC4(Wikipediaの文書のタグ当て)の優勝者のモデルブレンディングに使ったメタ素性
    • 低頻度ラベルかどうか、高頻度ラベルかどうか
    • 分類器結果での異なるインスタンス集合の数
    • 一番投票されるインスタンス集合への投票数
    • 各分類器の結果で一番多い/少ないインスタンスの頻度
    • 各分類器のインスタンス
    • 各分類器で同じ結果の分類器数
    • 各分類器のprecision/recall
    • 各分類器と出力のモード?とのジャッカード類似度
    • など

係り受け解析メモ

はじめに

今年の目標にしていた係り受け解析関係の資料について雑多にメモしておく。リンク集。
拾いきれていない、最新の論文まで追えていないので、あとで追加・整理し直す。

解析処理について説明している日本語資料

係り受け解析について説明している書籍

ネット上の文書をきれいにするライブラリ「NetTextCleaner」を公開しました

エイプリルフールネタです:) 見ればわかりますが、30分ぐらいで作ったものなので、言ってることもコードもデータも結構適当です:p

はじめに

インターネット上で扱われる文書(Twitter2chニコニコ動画など)には、特殊な用語や言い回しをしているものがあり、ネットの文書を扱う多くの自然言語処理研究者を苦しめています。


一番問題となることは、「これらの特殊な用語や言い回しを含む文を形態素解析すると解析に失敗すること」で、これによってIDFの計算や統計的言語モデルがおかしくなったりしてしまいます。


そこで、「書き手が書きたいことが、ひねくれて、実際に書かれる」という仮説を考え、生成的な確率モデルである「Noisy Channel Model」を用いてモデル化し、書き手が本当に書きたかったきれいな文へ変換することで、自然言語処理に失敗しない文章にすることができるライブラリを作成しました。
今後、各ウェブ企業などに導入され、ネット上のあらゆる文書に適用されることで、きれいになっていくでしょう。


Noisy Channel Modelによる変換

本当に書きたかったきれいな文章をS、実際に書いてしまった特殊な言い回しをした文章をTとします。
これをNoisy Channel Modelを用いると、
T = argmax P(T' | S)
 \,\,\,\,= argmax P(T') * P(S | T') / P(S)
 \,\,\,\,= argmax P(T') * P(S | T')
のようにモデル化でき、変換モデルP(S | T')を統計的に求めることができる気がします。


本ライブラリでは、ネット上からサンプルを各1件ずつ抽出し、統計的に変換モデルを作成しています。

ライブラリの概要

「変換モデル」と「変換プログラム」の2つから成ります。
「変換プログラム」は、正規表現を用いて上記の変換システムを実装してあり、「変換モデル」は変換モデルが含まれています。


実際のプログラムは、下記のAppendixにまとめられています。

使用例

以下のように、ネット上の文書を、一般的な理解可能な文書へ変換します。

$ ls
model.dat   cleaner.pl

$ perl cleaner.pl
あくしろよ
=> はやくしてください

猫、ぐうかわ
=> 猫、ぐうの音もでないほどかわいい

賢者タイム
=> 男性のみに訪れる特別な時間

帰って、どうぞ
=> 帰ってください

評価

Twitterのタイムラインを流れていたツイートに対してこれらを適用した結果、精度66.7%(3件中2件正解)という高精度で変換することができています。

免責事項

このライブラリを用いて起きたあらゆる問題・障害に対して責任を持ちません。
おそらく適当に変更して使っていただいて問題ないと思います。

Appendix

変換プログラムと変換モデルを以下に添付します。コピペによって利用する事ができます。


【cleaner.pl】

#!/usr/local/bin/perl
use strict;
use warnings;

my $dic_path = "./model.dat";
my @src_rules;
my @dst_rules;

open(IN, "<", $dic_path) or die;
while(<IN>){
    chomp;
    my ($src_rule, $dst_rule) = split(/\s+/, $_, 2);
    push(@src_rules, $src_rule);
    push(@dst_rules, $dst_rule);
}


while(<>){
    chomp;
    my $text = $_;

    for(my $i=0; $i<@src_rules; $i++){
        $text =~ s/$src_rules[$i]/qq(\"$dst_rules[$i]\")/eeg;
    }

    print "=> ", $text, "\n";
}

【model.dat】

(.+?)かわいいよ\1	$1がかわいい
(.+?)\1アンド\1	とても$1
(.+?)のバーゲンセール	$1の希少価値が落ちている
(.+?)ェ…	$1…
(.+?)過ぎて謝謝	$1過ぎて申し訳ない
(.+?)に(\d+?)ペリカ	$1だと思います
(.+?)に自信ニキ	$1に自信がある人
(.+?)は(ポイー|ポイ-)で	$1はいらない
あ…(察し)	心中お察しします
アイエ.+?\!\s*(.+?)\!\?\s*\1ナンデ\!\?	$1だ
あくしろよ	はやくしてください
アッー!	アーッ!
あまり私を怒らせない方がいい	私を怒らせると大変な事になりますよ
ありがとナス	ありがとう
\(.+?)だー!/	$1です
あーげない	あげません
いーらない	いりません
くーださい	ください
イイハナシダナー	いい話です
1ヶ月綺麗な言葉をかけ続けた(.+?)と1ヶ月罵声を浴びせ続けた\1	1ヶ月きれいな言葉と罵声で$1がどう変化したか
いつから(.+?)だと錯覚していた?	$1は錯覚です
今北産業	今来たところなのでこれまでの流れを三行で説明してください
びゃあ゛ぁ゛゛ぁうまひぃ゛ぃぃ゛	いやあ、うまい
(.+?)しろよ、色々と捗るぞ	$1すれば世界が変わりますよ?オススメですよ?
インド人を右に	ハンドルを右に
イ゙ェアアアア	\(断末魔\)
(.+?)から来るぞ!気をつけろ!	$1から何か来ます
ウキウキでワロタw+?	ウキウキで笑いました
イライラでワロタw+?	イライラで笑いました
(?<!ドワ|ニワ)ンゴ	。
(.+?)の法則が乱れる!	(.+?)がよくわからないがとにかくめちゃくちゃな状態になっている
うはwwwおkwww	ハイテンションなので笑って許せます
うーんこの	なにかがおかしい
教えてエロい人	教えてください
オナシャス	お願いします
おめぇの(.+?)ねぇから	あなたの$1はありません
おもんねーわ	面白くない
オワコン	終わったコンテンツ
(.+?)がね・・・	$1が致命的です
(.+?)か何か?	$1ではないと思いますが$1でしょうか?
キマシタワー	百合展開、きました
きんもーっ☆	キモいです
は犠牲になったのだ	は死にました
ぎゃんかわ	とてもかわいい
ギャン泣き	ギャンギャン泣きわめく
草不可避	笑いを耐えられません
くやしいのう	くやしいですね
ぐうかわ	ぐうの音もでないほどかわいい
グンマー	群馬県
賢者タイム	男性のみに訪れる特別な時間
激おこぷんぷん丸	結構怒っています
こ↑こ↓	ここ
これで勝つる	これで勝てます
GOD’S and DEATH	ありがとうございます
33\-4	圧倒的大差
(.+?)、どうぞ	$1ください
志村うしろ!	後ろから危険が迫っています
正直、すまんかった	ごめんなさい
(小並感)	(小学生並みの感想)
が一晩でやってくれました。	が尋常ではないはやさで仕事をしてくれました
じゃあの	また会いましょう
少し、頭冷やそうか	冷静になりましょう
すっごい(.+?)よ!	すごく$1です!
すまぬ…すまぬ…	ごめんなさい
ですしおすし	ですし
でっていう	だからなに?
どう見ても(.+?)です。本当にありがとうございました。	どう見ても$1です\(残念\)
日本語でおk	日本語でお願いします
日本よ、これが(.+?)だ	これこそ真の$1
はいじゃないが	それどころじゃないでしょう
微レ存	微粒子レベルで存在しています
ふつくしい	美しい
△	さんかっこいい
ボスケテ	助けて
無理ぽ	もう無理っぽい
メシウマ	他人の不幸で今日も飯がうまい
メシマズ	他人の幸福で飯がまずい
もうだめぽ	もうダメだ・・・
(.+?)のライフはゼロよ!	$1は瀕死です!
モルダーあなた疲れてるのよ	疲れておかしくなっています
やめたげてよぉ	やめてあげてください
やろう、ぶっころしてやる	ブチ切れました
許してヒヤシンス	許してください
よろしい、ならば戦争だ	でしたら、戦争で勝負をつけましょう
ワシの(.+?)は108式まであるぞ	まだ本気を出していないが、本気を出したらもっとすごいですよ?