C++ Russian Word Lemmatizer

プーシキン作品コンコーダンス・プログラムの設計において悩ましいのが,ロシア単語を出現形と見出語形(Lemmatized form)のいずれで扱うべきか,という課題である。自動コンコーダンスの目的は指定した語条件でコーパスを検索し,その用例が一覧できることにある — 私自身はそう思っているので,出現形でも見出語形でもどちらでもよいのだが,世の研究者は見出語にこだわるようで,北大の浦井教授・安藤教授が出版したドストエフスキイとカラムジンのコンコーダンスは見出語で整理されている。例えば,говорю(「語る」という動詞一人称現在形)という語(出現形)がテクストに出て来たら говорить(その不定形=見出語)として用例を纏めるのである。

というわけで,私の新しいソフトウェアも見出語でコンコーダンスを自動生成できるようにしたいと考えた(現行版 Опыт динамического составления конкордации к текстам А. С. Пушкина は出現形でしか処理できない)。そのためにロシア語形態素解析 C/C++ ライブラリ Lemmatizer を利用することにした。

これは UTF-8 エンコードのロシア語テクストを与えると,形態素辞書に基づいて見出語を解析することができる。ずいぶん前に Lemmatizer 試験プログラムを書いたので,見出語選定プロトタイプを作るのは簡単だった。しかしながら,Lemmatizer を用いて出現形を見出語に機械変換するに際して,出現形に対して可能性のある見出語が複数ある場合,予想だにしない結果を招くことがある。

複数のうちのどれを選ぶかを吟味せず,単純に Lemmatizer の解析結果の最初のものを見出語として採用するとする。例えば какая(「どんな」意の疑問代名詞女性単数主格)が出て来たら,普通なら誰でも,見出語は какой(男性単数主格)が得られると期待するだろう。ところが,Lemmatizer にかけると,какатькакой の二つが見出語候補として解析され,最初のものを採用すると,見出語は какать(幼児語「ウンチする」の動詞不定形)となってしまうのである。какая は確かに какать の副動詞と同形なので「間違い」ではない。でも人間が見たら一瞬でおかしいと思うわけだ。こういう形態上の曖昧さはどうしても自然言語処理には付いて回り,電子コンコーダンスを見出語ベースにする究極の問題点でもある。

周知の通り,ロシア語は名詞・形容詞の格変化,動詞の時制変化・形容詞/副詞化(能動形容詞,被動形容詞,副動詞の変化)など語形変化が凄まじい言語である。機械的に見出語を求めようとすると,上記のような滑稽な判断をする可能性がある。ならどうするか。究極は完璧な見出語選定は不可能であるが,品詞属性に見出語選定プライオリティを付けることによって,少しは改善できるだろうと考えた。какая の例では,Lemmatizer 解析で同時に得られる品詞情報に基づいて,動詞よりも代名詞のほうを優先して採用するようにすればよい。

「名詞・代名詞 > 形容詞長語尾形・物主他代名詞・数詞 > 副詞/述語・動詞人称/時制変化/不定形・形容詞短語尾形 > 能動/被動形動詞・副動詞 > 前置詞・接続詞・間投詞・小詞」の順で品詞優先度を付け,複数の解析結果がある場合,優先度の高いほうを見出語として採用することにした。同じ優先度のものが複数ある場合は,語長の短いもの,さらに優先度/語長の同じものが複数ある場合は最初に返却されたもの,とすることにした。

見出語選定プロトタイプ・プログラム lemmatizer.cpp を以下に示す。行テクストから句読点・約物を取り除いて露単語を切り出すのに Boost/tokenizer を使用した。

/* -*- coding: utf-8; mode: c++; -*-
 *
 *  Lemmatizer 解析/見出語選定
 *
 *    見出し語候補から以下の条件でひとつを採用する
 *    - 品詞プライオリティが最も高い
 *    - 単語長が最も短い
 *    - そのなかで Lemmatizer から最初に返却されるもの
 *
 *                            2012 (c) isao yasuda.
 */
 
#include <turglem/lemmatizer.hpp>
#include <turglem/russian/charset_adapters.hpp>
#include <boost/tokenizer.hpp>
#include <iostream>
#include <fstream>
 
// Lemmatizer ロシア語形態素解析辞書
static char dict[] =
    "/usr/local/share/turglem/russian/dict_russian.auto";
static char prdm[] =
    "/usr/local/share/turglem/russian/paradigms_russian.bin";
static char pred[] = 
    "/usr/local/share/turglem/russian/prediction_russian.auto";
 
// Lemmatizer 見出語属性構造体
struct wordst {
    std::string word; // 見出語
    int part;         // 品詞
    int prio;         // プライオリティ
    int len;          // 長さ
};
 
// Lemmatizer 解析/見出語選定
void lem_analyze(const tl::lemmatizer& lem, const char* s)
{
    // Lemmatizer 分析結果オブジェクトと件数
    tl::lem_result lr;
    size_t rcnt = lem.lemmatize<russian_utf8_adapter>(s, lr);
 
    wordst wst;                        // 見出語属性
    std::vector<wordst> wv;            // 見出語属性 vector
    std::vector<wordst>::iterator wit; // イテレータ
 
    // 見出し語候補 vector を生成し,最大プライオリティを確定
    int maxprio = 0;
    for (size_t i = 0; i < rcnt; i++) {
        u_int32_t src_form = lem.get_src_form(lr, i);
        wst.word = lem.get_text<russian_utf8_adapter>(lr, i, 0);
        wst.part = lem.get_part_of_speech(lr, i, src_form);
        wst.len  = wst.word.size();
        std::cout << "   [" << i << "] " << wst.word 
                  << "\t品詞番号: " << wst.part << std::endl;
        // 品詞番号でプライオリティ付け
        switch (wst.part) {
        // 品詞番号: プライオリティ
        case 0:  wst.prio = 5; break; // существительное 名詞
        case 1:  wst.prio = 4; break; // прилагательное 形容詞長語尾形
        case 2:  wst.prio = 3; break; // глагол 動詞
        case 3:  wst.prio = 5; break; // местоимение 代名詞
        case 4:  wst.prio = 4; break; // местоимение 代名詞(関係詞など?)
        case 5:  wst.prio = 4; break; // местоимение 代名詞?
        case 6:  wst.prio = 4; break; // числительное 数詞
        case 7:  wst.prio = 4; break; // порядковое числительное 順序数詞
        case 8:  wst.prio = 3; break; // наречие 副詞
        case 9:  wst.prio = 3; break; // предикатив 述語/副詞?
        case 10: wst.prio = 1; break; // предлог 前置詞
        case 11: wst.prio = 1; break; // POSL 後置詞?
        case 12: wst.prio = 1; break; // союз 接続詞
        case 13: wst.prio = 1; break; // междометие 間投詞
        case 14: wst.prio = 1; break; // (INP ?)
        case 15: wst.prio = 1; break; // (PHRASE 成句?)
        case 16: wst.prio = 1; break; // частица 小詞
        case 17: wst.prio = 3; break; // краткое прилагательное 形容詞短語尾形
        case 18: wst.prio = 2; break; // причастие 形動詞
        case 19: wst.prio = 2; break; // деепричастие 副動詞
        case 20: wst.prio = 2; break; // краткое причастие 形動詞短語尾形
        case 21: wst.prio = 3; break; // инфинитив 動詞不定形
        defaut:  wst.prio = 1; break;
        }
        if (wst.prio >= maxprio)
            maxprio = wst.prio;
        wv.push_back(wst);
    }
 
    // 最大プライオリティ品詞をもつ候補語のなかで最小長のものを確定
    int minlen = 1000;
    std::vector<std::string> vcand;
    for (wit = wv.begin(); wit < wv.end(); wit++) {
        if ((*wit).prio == maxprio) {
            if ((*wit).len <= minlen) {
                minlen = (*wit).len;
                vcand.push_back((*wit).word);
            }
        }
    }
 
    // 最小長の候補語の最初のものを見出語に決定
    std::string eto; // 選定見出語
    std::vector<std::string>::iterator sit;
    for (sit = vcand.begin(); sit != vcand.end(); sit++) {
        if ((*sit).size() ==  minlen) {
            eto = *sit;
            break;
        }
    }
    std::cout << "  選定見出語: " << eto << "\n";
}
  
// Boost tokenizer により,指定区切り文字で単語を切り出す
void wdtokenizer(std::string& line, std::vector<std::string>& sv)
{
    typedef boost::tokenizer<boost::char_separator<char> > tokenizer;
    // 区切り文字定義 -' を区切りとはしない
    boost::char_separator<char> sep("\t _:;.,?!~`\"\\[]{}()*&^%$#@+=|<>/", 
                    "", boost::drop_empty_tokens);
    tokenizer tok(line, sep);
    for (tokenizer::iterator ti = tok.begin(); ti != tok.end(); ti++) 
        sv.push_back(*ti);
}
 
// 主処理
int main(int argc, char **argv)
{
    tl::lemmatizer lem;         // Lemmatizer instance
    std::string buffer;         // 行バッファ
    std::ifstream in(argv[1]);  // 引数(ファイル名)
  
    if (!in) {
        std::cerr << "usage: " << argv[0] << " file-name" << std::endl;
        return 1;
    }
 
    // Lemmatizer 文法辞書ロード
    try {
        lem.load_lemmatizer(dict, prdm, pred);
    }
    catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
   
    while (!in.eof()) {
        std::vector<std::string> v;
        getline(in, buffer, '\n');
        if (buffer.empty()) continue;
 
        // 単語切り出し
        wdtokenizer(buffer, v);
        std::cout << std::endl << buffer << std::endl;
  
        // 単語毎に Lemmatizer 解析を行う
        for (int i = 0; i < v.size(); i++) {
            std::cout << i << ". " << v[i] << std::endl;
            // lemmatizer analyze.
            try {
                if (!v[i].empty())
                    lem_analyze(lem, v[i].c_str());
            }
            catch (const std::exception& e) {
                std::cerr << "Error: " << e.what() << std::endl;
            }
        }
        v.clear();
    }
 
    return 0;
}

これを以下の通り GNU C/C++ でコンパイルする。

% g++ -g -I/usr/local/include -L/usr/local/lib -o lemmatizer \
lemmatizer.cpp -lturglem -lturglem-russian -lMAFSA

プーシキンの『エヴゲーニイ・オネーギン』の書き出しで試験してみた結果を示しておく。

入力:

Мой дядя самых честных правил,
Когда не в шутку занемог,
Он уважать себя заставил
И лучше выдумать не мог.
Его пример другим наука;
Но, боже мой, какая скука
С больным сидеть и день и ночь,
Не отходя ни шагу прочь!
Какое низкое коварство
Полуживого забавлять,
Ему подушки поправлять,
Печально подносить лекарство,
Вздыхать и думать про себя
Когда же черт возьмет тебя!

出力:

Мой дядя самых честных правил,
0. Мой
   [0] МЫТЬ	品詞番号: 2
   [1] МОЙ	品詞番号: 4
   [2] МОЙ	品詞番号: 4
  選定見出語: МОЙ
1. дядя
   [0] ДЯДЯ	品詞番号: 0
  選定見出語: ДЯДЯ
2. самых
   [0] САМЫЙ	品詞番号: 4
   [1] САМЫЙ	品詞番号: 4
   [2] САМЫЙ	品詞番号: 4
  選定見出語: САМЫЙ
3. честных
   [0] ЧЕСТНЫЙ	品詞番号: 1
   [1] ЧЕСТНЫЙ	品詞番号: 1
   [2] ЧЕСТНЫЙ	品詞番号: 1
   [3] ЧЕСТНОЙ	品詞番号: 1
   [4] ЧЕСТНОЙ	品詞番号: 1
   [5] ЧЕСТНОЙ	品詞番号: 1
  選定見出語: ЧЕСТНЫЙ
4. правил
   [0] ПРАВИТЬ	品詞番号: 2
   [1] ПРАВИЛО	品詞番号: 0
   [2] ПРАВИЛО	品詞番号: 0
   [3] ПРАВИТЬ	品詞番号: 2
  選定見出語: ПРАВИЛО
 
Когда не в шутку занемог,
0. Когда
   [0] КОГДА	品詞番号: 12
   [1] КОГДА	品詞番号: 8
  選定見出語: КОГДА
1. не
   [0] НЕ	品詞番号: 16
  選定見出語: НЕ
2. в
   [0] В	品詞番号: 10
  選定見出語: В
3. шутку
   [0] ШУТКА	品詞番号: 0
  選定見出語: ШУТКА
4. занемог
   [0] ЗАНЕМОЧЬ	品詞番号: 2
  選定見出語: ЗАНЕМОЧЬ
 
Он уважать себя заставил
0. Он
   [0] ОН	品詞番号: 3
  選定見出語: ОН
1. уважать
   [0] УВАЖАТЬ	品詞番号: 21
  選定見出語: УВАЖАТЬ
2. себя
   [0] СЕБЯ	品詞番号: 3
   [1] СЕБЯ	品詞番号: 3
  選定見出語: СЕБЯ
3. заставил
   [0] ЗАСТАВИТЬ	品詞番号: 2
  選定見出語: ЗАСТАВИТЬ
 
И лучше выдумать не мог.
0. И
   [0] И	品詞番号: 12
   [1] И	品詞番号: 13
  選定見出語: И
1. лучше
   [0] ХОРОШИЙ	品詞番号: 1
   [1] ЛУЧШЕ	品詞番号: 16
  選定見出語: ХОРОШИЙ
2. выдумать
   [0] ВЫДУМАТЬ	品詞番号: 21
  選定見出語: ВЫДУМАТЬ
3. не
   [0] НЕ	品詞番号: 16
  選定見出語: НЕ
4. мог
   [0] МОЧЬ	品詞番号: 2
  選定見出語: МОЧЬ
 
Его пример другим наука;
0. Его
   [0] ОНО	品詞番号: 3
   [1] ОНО	品詞番号: 3
   [2] ОН	品詞番号: 3
   [3] ОН	品詞番号: 3
   [4] ЕГО	品詞番号: 4
  選定見出語: ОН
1. пример
   [0] ПРИМЕРЕТЬ	品詞番号: 2
   [1] ПРИМЕР	品詞番号: 0
   [2] ПРИМЕР	品詞番号: 0
  選定見出語: ПРИМЕР
2. другим
   [0] ДРУГОЙ	品詞番号: 4
   [1] ДРУГОЙ	品詞番号: 4
   [2] ДРУГОЙ	品詞番号: 4
  選定見出語: ДРУГОЙ
3. наука
   [0] НАУКА	品詞番号: 0
  選定見出語: НАУКА
 
Но, боже мой, какая скука
0. Но
   [0] НО	品詞番号: 12
   [1] НО	品詞番号: 13
  選定見出語: НО
1. боже
   [0] БОГ	品詞番号: 0
  選定見出語: БОГ
2. мой
   [0] МЫТЬ	品詞番号: 2
   [1] МОЙ	品詞番号: 4
   [2] МОЙ	品詞番号: 4
  選定見出語: МОЙ
3. какая
   [0] КАКАТЬ	品詞番号: 19
   [1] КАКОЙ	品詞番号: 4
  選定見出語: КАКОЙ
4. скука
   [0] СКУКА	品詞番号: 0
  選定見出語: СКУКА
 
С больным сидеть и день и ночь,
0. С
   [0] С	品詞番号: 10
  選定見出語: С
1. больным
   [0] БОЛЬНОЙ	品詞番号: 1
   [1] БОЛЬНОЙ	品詞番号: 1
   [2] БОЛЬНОЙ	品詞番号: 1
   [3] БОЛЬНОЙ	品詞番号: 0
   [4] БОЛЬНОЙ	品詞番号: 0
   [5] БОЛЬНАЯ	品詞番号: 0
  選定見出語: БОЛЬНОЙ
2. сидеть
   [0] СИДЕТЬ	品詞番号: 21
  選定見出語: СИДЕТЬ
3. и
   [0] И	品詞番号: 12
   [1] И	品詞番号: 13
  選定見出語: И
4. день
   [0] ДЕТЬ	品詞番号: 2
   [1] ДЕНЬ	品詞番号: 0
   [2] ДЕНЬ	品詞番号: 0
  選定見出語: ДЕНЬ
5. и
   [0] И	品詞番号: 12
   [1] И	品詞番号: 13
  選定見出語: И
6. ночь
   [0] НОЧЬ	品詞番号: 0
   [1] НОЧЬ	品詞番号: 0
  選定見出語: НОЧЬ
 
Не отходя ни шагу прочь!
0. Не
   [0] НЕ	品詞番号: 16
  選定見出語: НЕ
1. отходя
   [0] ОТХОДИТЬ	品詞番号: 19
  選定見出語: ОТХОДИТЬ
2. ни
   [0] НИ	品詞番号: 12
   [1] НИ	品詞番号: 16
  選定見出語: НИ
3. шагу
   [0] ШАГ	品詞番号: 0
  選定見出語: ШАГ
4. прочь
   [0] ПРОЧИТЬ	品詞番号: 2
   [1] ПРОЧЬ	品詞番号: 8
  選定見出語: ПРОЧЬ
 
Какое низкое коварство
0. Какое
   [0] КАКОЙ	品詞番号: 4
   [1] КАКОЙ	品詞番号: 4
  選定見出語: КАКОЙ
1. низкое
   [0] НИЗКИЙ	品詞番号: 1
   [1] НИЗКИЙ	品詞番号: 1
  選定見出語: НИЗКИЙ
2. коварство
   [0] КОВАРСТВО	品詞番号: 0
   [1] КОВАРСТВО	品詞番号: 0
  選定見出語: КОВАРСТВО
 
Полуживого забавлять,
0. Полуживого
   [0] ПОЛУЖИВОЙ	品詞番号: 0
   [1] ПОЛУЖИВОЙ	品詞番号: 0
   [2] ПОЛУЖИВОЙ	品詞番号: 1
   [3] ПОЛУЖИВОЙ	品詞番号: 1
   [4] ПОЛУЖИВОЙ	品詞番号: 1
  選定見出語: ПОЛУЖИВОЙ
1. забавлять
   [0] ЗАБАВЛЯТЬ	品詞番号: 21
  選定見出語: ЗАБАВЛЯТЬ
 
Ему подушки поправлять,
0. Ему
   [0] ОНО	品詞番号: 3
   [1] ОН	品詞番号: 3
  選定見出語: ОН
1. подушки
   [0] ПОДУШКА	品詞番号: 0
   [1] ПОДУШКА	品詞番号: 0
   [2] ПОДУШКА	品詞番号: 0
  選定見出語: ПОДУШКА
2. поправлять
   [0] ПОПРАВЛЯТЬ	品詞番号: 21
  選定見出語: ПОПРАВЛЯТЬ
 
Печально подносить лекарство,
0. Печально
   [0] ПЕЧАЛЬНО	品詞番号: 8
   [1] ПЕЧАЛЬНЫЙ	品詞番号: 1
   [2] ПЕЧАЛЬНО	品詞番号: 9
  選定見出語: ПЕЧАЛЬНЫЙ
1. подносить
   [0] ПОДНОСИТЬ	品詞番号: 21
   [1] ПОДНОСИТЬ	品詞番号: 21
  選定見出語: ПОДНОСИТЬ
2. лекарство
   [0] ЛЕКАРСТВО	品詞番号: 0
   [1] ЛЕКАРСТВО	品詞番号: 0
   [2] ЛЕКАРСТВО	品詞番号: 0
   [3] ЛЕКАРСТВО	品詞番号: 0
  選定見出語: ЛЕКАРСТВО
 
Вздыхать и думать про себя
0. Вздыхать
   [0] ВЗДЫХАТЬ	品詞番号: 21
  選定見出語: ВЗДЫХАТЬ
1. и
   [0] И	品詞番号: 12
   [1] И	品詞番号: 13
  選定見出語: И
2. думать
   [0] ДУМАТЬ	品詞番号: 21
  選定見出語: ДУМАТЬ
3. про
   [0] ПРО	品詞番号: 10
  選定見出語: ПРО
4. себя
   [0] СЕБЯ	品詞番号: 3
   [1] СЕБЯ	品詞番号: 3
  選定見出語: СЕБЯ
 
Когда же черт возьмет тебя! 
0. Когда
   [0] КОГДА	品詞番号: 12
   [1] КОГДА	品詞番号: 8
  選定見出語: КОГДА
1. же
   [0] ЖЕ	品詞番号: 12
   [1] ЖЕ	品詞番号: 16
  選定見出語: ЖЕ
2. черт
   [0] ЧЕРТА	品詞番号: 0
   [1] ЧЕРТ	品詞番号: 0
  選定見出語: ЧЕРТ
3. возьмет
   [0] ВЗЯТЬ	品詞番号: 2
  選定見出語: ВЗЯТЬ
4. тебя
   [0] ТЫ	品詞番号: 3
   [1] ТЫ	品詞番号: 3
  選定見出語: ТЫ

この例ではまずまずである。プライオリティはもう少し試験して見直しをしなければならないだろう。егооноон に集約されてしまう,等は目をつぶるしかない。見出語変換で仮に誤りがあったとしても,用例を人間が視て妥当性の判断は可能だ。正規表現で幅をもたせた条件指定ができれば,そういう根本的問題点を少しはカバーできると思う。それにしても,機械が言語解析をやると面白いことが起こるものである。