ナード戦隊データマン

機械学習と自然言語処理についてのブログ

wordnetで異なる抽象度の文を生成

wordnetは、同一概念の単語をまとめ、さらに概念の抽象度に基づくネットワークが定義されたデータです。

github.com

今回は、wordnetを使って単純な文を文レベルで抽象化・具体化します。

概要

以下の文があるとします:

おいしいコーヒーを飲む。

この文を、以下のように抽象化することができます。

おいしい飲み物を飲む。

具体化する場合は以下のようになります。

おいしいエスプレッソを飲む。

「おいしい」という言葉についても、「美味しい」「旨い」「辛い」「酸っぱい」などの類似語に置き換えられると考えます。

ただし、「飲む」という述語については、述語によって使われる項が異なるため、固定します。(これは、述語項構造解析の問題です)。

単純なモジュール

日本語のwordnetは以下のリンクから取得できます:

http://compling.hss.ntu.edu.sg/wnja/jpn/index.html

これを使えば、以下のような単純なモジュールにより、入力された語に接続される上位下位概念や、同義概念、構成物などを取得することができます。

# coding: utf-8

import sys
import sqlite3
from collections import defaultdict

CONNECTION = sqlite3.connect("./wnjpn.db")


def get_synset_from_wordid(conn, wordid):
    cur = conn.cursor()
    cur.execute("select synset from sense where wordid=?", (wordid, ))
    return cur.fetchone()[0]


def get_synset_from_lemma(conn, lemma):
    cur = conn.cursor()
    cur.execute("select wordid from word where lemma=?", (lemma, ))
    wordid = cur.fetchone()[0]
    return get_synset_from_wordid(conn, wordid)


def get_synlinks_child(conn, synset):
    cur = conn.cursor()
    cur.execute(
        "select synset2, link from synlink where synset1=?", (synset, ))
    return cur.fetchall()


def get_wordid_from_synset(conn, synset):
    cur = conn.cursor()
    cur.execute("select wordid from sense where synset=?", (synset, ))
    return cur.fetchall()


def get_lemma_from_wordid(conn, wordid):
    cur = conn.cursor()
    cur.execute("select lemma, lang from word where wordid=?", (wordid, ))
    return cur.fetchone()[0]


def get_synlinks_parent(conn, synset):
    cur = conn.cursor()
    cur.execute(
        "select synset1, link from synlink where synset2=?", (synset, ))
    return cur.fetchall()


def wn(word):
    conn = CONNECTION
    synset = get_synset_from_lemma(conn, word)
    synlinks = get_synlinks_child(conn, synset)
    wordids = defaultdict(dict)
    wordids["same"][synset] = get_wordid_from_synset(conn, synset)
    for synset, t in synlinks:
        if synset not in wordids[t]:
            wordids[t][synset] = []
        wordids[t][synset] += get_wordid_from_synset(conn, synset)
    for t, d in wordids.items():
        for sid, values in d.items():
            lemmas = [get_lemma_from_wordid(conn, value[0]) for value in values]
            yield ((t, sid), lemmas)


if __name__ == "__main__":
    import sys
    import pprint
    word = sys.argv[1]
    pprint.pprint(dict(wn(word)))
$ python3 wn.py パン

[出力]

{('hprt', '07695965-n'): ['sandwich', 'サンド', 'サンドイッチ', 'サンドウィッチ'],
 ('hype', '07566863-n'): ['starches'],
 ('hype', '07622061-n'): ['baked_goods', 'オーブンで焼かれた食品'],
 ('hypo', '07680168-n'): ['anadama_bread', 'アナダマパン', 'アナダマブレッド'],
 ('hypo', '07680313-n'): ['bap'],
 ('hypo', '07680416-n'): ['barmbrack'],
 ('hypo', '07680517-n'): ['breadstick', 'bread-stick', 'ブレッドスティック'],
 ('hypo', '07680761-n'): ['brown_bread',
                          'boston_brown_bread',
                          'ブラウンブレッド',
                          '黒パン'],
 ('hypo', '07680932-n'): ['bun', 'roll', 'ロール', 'コッペパン'],
 ('hypo', '07681355-n'): ['caraway_seed_bread'],
 ('hypo', '07681450-n'): ['challah', 'hallah', 'ハラ', 'カラ'],
 ('hypo', '07681691-n'): ['cinnamon_bread', 'シナモンブレッド', 'シナモンパン'],
 ('hypo', '07681805-n'): ['cracked-wheat_bread'],
 ('hypo', '07681926-n'): ['cracker', 'クラッカー'],
 ('hypo', '07682197-n'): ['crouton', 'クルトン'],
 ('hypo', '07682316-n'): ['whole_wheat_bread',
                          'whole_meal_bread',
                          'brown_bread',
                          'dark_bread',
                          '全粒粉ブレッド',
                          'ブラウンブレッド',
                          '黒パン',
                          '全粒粉パン'],
 ('hypo', '07682477-n'): ['english_muffin', 'イングリッシュマフィン'],
 ('hypo', '07682624-n'): ['flatbread'],
 ('hypo', '07682808-n'): ['garlic_bread', 'ガーリックブレッド'],
 ('hypo', '07682952-n'): ['gluten_bread'],
 ('hypo', '07683138-n'): ['host', '聖体'],
 ('hypo', '07683786-n'): ['loaf', 'loaf_of_bread'],
 ('hypo', '07684164-n'): ['matzo',
                          'matzah',
                          'matzoh',
                          'unleavened_bread',
                          'マツァ',
                          'マッツァー',
                          'マッツォ'],
 ('hypo', '07684289-n'): ['nan', 'naan', 'ナン'],
 ('hypo', '07684422-n'): ['onion_bread',
                          'オニオンブレッド',
                          '玉ねぎパン',
                          '玉ねぎブレッド',
                          'オニオンパン'],
 ('hypo', '07684517-n'): ['raisin_bread', 'ぶどうパン', 'ブドウパン'],
 ('hypo', '07684600-n'): ['quick_bread', 'クイックブレッド'],
 ('hypo', '07685730-n'): ['rye_bread', 'ライ麦パン', '黒パン'],
 ('hypo', '07686461-n'): ['salt-rising_bread', 'ソルトライジングパン', 'ソルトライジングブレッド'],
 ('hypo', '07686634-n'): ['simnel'],
 ('hypo', '07686720-n'): ['sourdough_bread',
                          'sour_bread',
                          'サワードウブレッド',
                          'サワーブレッド'],
 ('hypo', '07686873-n'): ['toast', 'トースト'],
 ('hypo', '07687053-n'): ['wafer',
                          'ウェーハ',
                          'ウェファ',
                          'ウェハ',
                          'ウエハ',
                          'ウェハー',
                          'ウエハー'],
 ('hypo', '07687211-n'): ['light_bread', 'white_bread', '食パン'],
 ('msub', '07569106-n'): ['flour', '穀粉', '麦粉', '粉'],
 ('same', '07679356-n'): ['bread',
                          'breadstuff',
                          'staff_of_life',
                          'パン',
                          '麺麭',
                          '食麺麭',
                          'ブレッド',
                          '食パン',
                          '麺包']}

辞書のキーは (リンクの種類, 概念のID)となっており、その概念の単語が値となっています。 sameは同義語ですが、それ以外はリンクの種類です。リンクの種類の詳細は以下を参考にしてください。

http://compling.hss.ntu.edu.sg/wnja/jpn/detail.html

文を生成する

前述のモジュールを使って以下の例を実装します:

from wn import wn
from collections import defaultdict


def _ignore_word(word, ill_chars):
    for c in list(word):
        if c in ill_chars:
            return True
    return False


def example(w1, w2, w3):
    out = []
    template = "{}{}を{}"
    w1d = dict(wn(w1))
    w2d = dict(wn(w2))
    ill_chars = "abcdefghijklmnopqrstuvwxyz"
    ill_chars += ill_chars[:].upper()

    count = defaultdict(int)
    for key1, words1 in w1d.items():
        for key2, words2 in w2d.items():
            for word1 in words1:
                if _ignore_word(word1, ill_chars):
                    continue
                if word1.endswith(w1[-1]):
                    for word2 in words2:
                        if _ignore_word(word2, ill_chars):
                            continue
                        t = (key1[1], key2[1])
                        count[t] += 1
                        out.append(((key1[0], key2[0],
                                     key1[1], key2[1], count[t]),
                                    template.format(word1, word2, w3)))
    return out


if __name__ == "__main__":
    import sys
    import pprint
    pprint.pprint(dict(example(sys.argv[1], sys.argv[2], sys.argv[3])))

これは、以下の形式で文を生成するものです:

w1w2をw3

w1, w2, w3は入力語です。

w1=おいしい, w2=コーヒー, w3=飲む

のような入力をします。

$ python3 example.py おいしい コーヒー 飲む

[出力]

{('also', 'hype', '01716227-a', '07881800-n', 1): '旨味しい飲み料を飲む',
 ('also', 'hype', '01716227-a', '07881800-n', 2): '旨味しい飲み物を飲む',
 ('also', 'hype', '01716227-a', '07881800-n', 3): '旨味しい水ものを飲む',
 ('also', 'hype', '01716227-a', '07881800-n', 4): '旨味しい飲物を飲む',
 ('also', 'hype', '01716227-a', '07881800-n', 5): '旨味しい飲料を飲む',
 ('also', 'hype', '01716227-a', '07881800-n', 6): '旨味しい水物を飲む',
 ('also', 'hype', '02368336-a', '07881800-n', 1): '甘い飲み料を飲む',
 ('also', 'hype', '02368336-a', '07881800-n', 2): '甘い飲み物を飲む',
 ('also', 'hype', '02368336-a', '07881800-n', 3): '甘い水ものを飲む',
 ('also', 'hype', '02368336-a', '07881800-n', 4): '甘い飲物を飲む',
 ('also', 'hype', '02368336-a', '07881800-n', 5): '甘い飲料を飲む',
 ('also', 'hype', '02368336-a', '07881800-n', 6): '甘い水物を飲む',
 ('also', 'hype', '02368787-a', '07881800-n', 1): '酸っぱい飲み料を飲む',
 ('also', 'hype', '02368787-a', '07881800-n', 2): '酸っぱい飲み物を飲む',
 ('also', 'hype', '02368787-a', '07881800-n', 3): '酸っぱい水ものを飲む',
 ('also', 'hype', '02368787-a', '07881800-n', 4): '酸っぱい飲物を飲む',
 ('also', 'hype', '02368787-a', '07881800-n', 5): '酸っぱい飲料を飲む',
 ('also', 'hype', '02368787-a', '07881800-n', 6): '酸っぱい水物を飲む',
 ('also', 'hype', '02368787-a', '07881800-n', 7): '酸い飲み料を飲む',
 ('also', 'hype', '02368787-a', '07881800-n', 8): '酸い飲み物を飲む',
 ('also', 'hype', '02368787-a', '07881800-n', 9): '酸い水ものを飲む',
 ('also', 'hype', '02368787-a', '07881800-n', 10): '酸い飲物を飲む',
 ('also', 'hype', '02368787-a', '07881800-n', 11): '酸い飲料を飲む',
 ('also', 'hype', '02368787-a', '07881800-n', 12): '酸い水物を飲む',
 ('also', 'hypo', '01716227-a', '07731122-n', 1): '旨味しい代用コーヒーを飲む',
 ('also', 'hypo', '01716227-a', '07731122-n', 2): '旨味しい代替コーヒーを飲む',
 ('also', 'hypo', '01716227-a', '07919441-n', 1): '旨味しいアイリッシュコーヒーを飲む',
 ('also', 'hypo', '01716227-a', '07919572-n', 1): '旨味しいカフェオレを飲む',
 ('also', 'hypo', '01716227-a', '07919665-n', 1): '旨味しいデミタスを飲む',
 ('also', 'hypo', '01716227-a', '07919787-n', 1): '旨味しいデカフェを飲む',

...と続く

何がしたいのか

このようなプログラムを作った理由は、「2つの文の共通性を判定するモデルを作るために、それを評価するテストデータを作成せよ」という要件があったためです。

本当は、テストデータはアノテーション基準を明らかにした上で、アノテーションによって定義するべきかもしれません。しかし、そのアノテーション基準そのものがまだできておらず、「ユニットテスト的な目的」で使うようなテストデータが欲しかったためです。

共通性は、抽象階層のレベルに応じて定義が変わる可能性があるため、その階層の名前によってラベル付けすることができると仮定しました。例えば、「各々のラベルがすべて"same"ならばTrue」といったテストケースを考えることができます。

ただし、このような単純化は「2つのテキストの共通性を判定せよ」という一般的な問題を解くには十分ではないため、最終的にはステークホルダーの想定する「共通性」に基づいたアノテーション基準の制定に繋げる必要があります。

参考

  1. 日本語 WordNet (wn-ja)
  2. 日本語 WordNet (wn-ja)
  3. 述語項構造シソーラス (岡山大学竹内研)