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

MeCabとマルコフ連鎖で遊ぶ

 MeCabをインストールして忍殺語辞書を入れた前回からのつづきです。
 環境はUbuntu14.04(VMware内)です。
 VWwareもUbuntuも初めて使ったのですが、思ったほどメモリも食わないし便利ですね。ただRAMが4GB以下のPCだとさすがに厳しそう。
 UbuntuGUIがかなり充実していてWindowsかよ!という印象。Windowsと違ってインストールしたソフトが/binや/libに分かれて入るんですね。これが便利さの原因なのかな?WindowsC++などで外部からライブラリをダウンロードしたりすると手であれこれヘッダーを動かしたりしなきゃいけなかったりしますもんね……。
 というわけでマルコフ連鎖で適当に文章を生成して遊んでみました。
 一番ハマったのはpythonからsqlite3を使う時です。原因不明のバグで、データベースに品詞分解した単語が書き込まれずかなり苦労しました。対話から何回も試したらうまくいきましたが、うまくいかなかった原因はわかっていません……。
 sqlite3を使ってがっつりやる方のスクリプトはまた後日として、今日はRAMにデータを積んで適当にやる感じのC++を載せます。

#include <bits/stdc++.h>
#include <mecab.h>
#define CHECK(eval) if (!eval) { \
    const char *e = tagger ? tagger->what() : MeCab::getTaggerError(); \
    std::cerr << "Exception:" << e << std::endl; \
    delete tagger; \
    return -1; }

using namespace std;
//分かち書きされた文章から単語を拾い、vectorに格納して返す
vector<string> split(const string &str, char delim){
    vector<string> res;
    size_t current = 0, found;
    while((found = str.find_first_of(delim, current)) != string::npos){
        res.push_back(string(str, current, found - current));
        current = found + 1;
    }
    res.push_back(string(str, current, str.size() - current));
    return res;
}
char* input;
//入力部分 無駄に長い
bool init(){
    std::ifstream ifs("hoge.txt");
    if(ifs.fail()){
        cout << "Failed" << endl;
        return false;
    }
    int begin = static_cast<int>(ifs.tellg());
    ifs.seekg(0, ifs.end);
    int end = static_cast<int>(ifs.tellg());
    int size = end - begin;
    ifs.clear();
    ifs.seekg(0, ifs.beg);
    char *str = new char[size + 1];
    ifs.read(str, size);
    input = str;
    return true;
}
map<string, vector<string>> table;
int main(int argc, char **argv){
    MeCab::Tagger *tagger = MeCab::createTagger("-Owakati");
    CHECK(tagger);
    if(!init()) return 0;
    const char* parsed = tagger->parse(input);
    char aaa = ' ';
    vector<string> splited = split(parsed, aaa);
    string w1 = "", w2 = "";
    for(string w3 : splited){
        if(w1 != "" && w2 != "") table[w1 + w2].push_back(w3);
        w1 = w2;
        w2 = w3;
    }
    string result = "";
    int k = rand()%(splited.size() - 1);
    string word1 = splited[k];
    string word2 = splited[k + 1];
    result += word1 + word2;
    for(int i = 0; i < 1000; i++){
        int ts = table[word1 + word2].size();
        int index = 0;
        if(ts >= 2) index = rand()%ts;
        string word3 = table[word1 + word2][index];
        result += word3;
        word1 = word2;
        word2 = word3;
    }
    cout << result << endl;
    return 0;
}

コンパイル

g++ -std=c++11 `mecab-config --cflags` hoge.cpp -o hoge `mecab-config --libs`

でできます。
 ちなみにinputとsplitは半分コピペです(オイ
 やっていることとしては、まず単語を前から順番に3つずつ見ていきます。このとき、前二つの単語を鍵、一番後ろの単語を要素とした連想配列に単語を格納していきます。あとは、最初に連続する二つの単語を与えて、そこから前二つの単語を鍵にもつ単語を選んでいくだけです。C言語のrandを使っているので毎回同じ結果が出るのがいまいち微妙です。
 というかマルコフ連鎖ってこれで合ってるのかちっとも確証はないのですが、wikiなんかを見た感じアバウトに定義されているようなのでまあいっかな、という感じ。
 なぜC++を選んだのかというと、僕はC++以外に使える言語がないので……。先述の通りsqlite3を使う方はpythonを使ったのですが、いまいちpythonのデータ構造に詳しくなく(dictionaryとかを使うのかな?)うまいやり方を思いつけませんでした。dictionaryの右辺にlistを持たせたかったのですが(※)。嫌われ者のC++ですが(僕も正直あまり好きではないです)STLは本当に便利ですね。まあ表現力ではlispなどには遠く及びませんが……。
 というわけでひとまずこんな感じ。
f:id:mio_hirona:20160323225748p:plain
 これはこれ以上改善させる気はありませんが(笑)、データベースファイルを作っている方は頑張ってデータを増やしてそれっぽい感じにしたいですね。
 [追記]書き忘れていましたが、上記のプログラムはmapの中身が空だったときの例外処理を書いていません。何らかの理由で上のプログラムでw3 = ""となり、かつそのときのw1+w2が一度しか登場しない時にバグが発生する可能性があります(Segmentation Faultになります)。上記のコードをコピペして使う場合は気をつけて下さい。
[さらに追記(3/25)]txtファイル中のスペースを排除していなかったのが原因のようです。trコマンドなどで修正できます。
[追記(三回目)]※ですがあとで試したらふつうにできました。ただ要素数0のリストで初期化したりする方法はよくわかりませんが。