データナード

機械学習と自然言語処理についての備忘録 (旧ナード戦隊データマン)

wordnetをベクトル化: 意味の外延的定義

意味の外延的定義とは、語の意味を、その語が表すすべてのもので表す方法です。例えば、乗り物={車, 電車, 自転車, 飛行機,...} など、全乗り物の集合で乗り物の意味を表します。

今回は、この外延的定義のアイデアを借りてwordnetをベクトル化します。

概要

語の集合  W と語のベクトル集合  V = \{ f(w) | w \in W \} があるとします。そして、wordnetのように、階層的に語を定義するデータ  H があります。ある  h \in H の下位集合を  B_h で表します。

 v_0 \in V を、 v_1 = \alpha v_0 + (1- \alpha ) (1/|B_w|) \Sigma_{b \in B_w} f(b) で更新します。(ただし、 w v に対応した語)

この更新を数回繰り返すことで、wordnet vectorのようなものが出来上がります。

事前準備

gloveのダウンロード

gloveをダウンロードしてgensim化しておいてください。

wordnetのダウンロード

#!/bin/bash
mkdir data
pushd data
if [ -f wnjpn.db ]; then
    echo "wnjpn.db exists"
else
    wget http://compling.hss.ntu.edu.sg/wnja/data/1.1/wnjpn.db.gz
    gunzip *.gz
fi
popd

コード

グラフ化とベクトル化

import sqlite3
import pickle
import numpy as np
from gensim.models import KeyedVectors
from tqdm import tqdm

kv = KeyedVectors.load("./glove.bin", mmap="r")
conn = sqlite3.connect("./data/wnjpn.db")


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


def get_lemma(conn, synset):
    cur = conn.cursor()
    cur.execute("select * from synset where synset=?", (synset, ))
    return cur.fetchone()[2]


def update(vectors, graph, alpha=0.35):
    for e1, e2s in tqdm(graph.items()):
        if e1 not in vectors:
            continue
        v = [vectors[e2] for e2 in e2s if e2 in vectors]
        if v:
            vectors[e1] = alpha * vectors[e1] + (1 - alpha) * np.mean(v,
                                                                      axis=0)
        else:
            vectors[e1]


def build_edges(conn):
    cur = conn.cursor()
    cur.execute("select * from synlink")
    links = cur.fetchall()
    edges = set()
    for x in links:
        if x[2] in ["hypo", "dmtc", "dmtr", "enta"]:
            edges.add((x[0], x[1]))
        elif x[2] in ["hype", "dmnc", "dmnr"]:
            edges.add((x[1], x[0]))
    return edges


def extract_vectors(links):
    vectors = {}
    for link in links:
        for li in link:
            try:
                lem = get_lemma(conn, li)
                v = [kv[l] for l in lem.strip().split("_") if l in kv.wv.vocab]
                if v:
                    vectors[li] = np.mean(v, axis=0)
                else:
                    vectors[li] = np.random.rand(50)
            except Exception:
                pass
    return vectors


def build_graph(edges):
    graph = {}
    for e in edges:
        if e[0] not in graph:
            graph[e[0]] = []
        graph[e[0]].append(e[1])
    return graph


if __name__ == "__main__":
    print("step1")
    edges = build_edges(conn)
    print("step2")
    vectors = extract_vectors(edges)
    print("step3")
    graph = build_graph(edges)

    for i in range(15):
        update(vectors, graph)

    with open("wnvecs.pkl", "wb") as f:
        pickle.dump(vectors, f)

ベクトルをgensim化

# coding: utf-8
from gensim.models import KeyedVectors
from tqdm import tqdm
import pickle


def transform_to_gensim(infile="./wnvecs.pkl"):
    with open(infile, "rb") as f:
        vecs = pickle.load(f)

    kv = KeyedVectors(vector_size=50)

    for k, v in tqdm(vecs.items()):
        kv[k] = v

    kv.save("./wnmodel.bin")


if __name__ == "__main__":
    transform_to_gensim()

gloveと比較

# coding: utf-8
from gensim.models import KeyedVectors
from build_grapy import conn, get_synset, get_lemma

if __name__ == "__main__":
    import sys
    kv1 = KeyedVectors.load("./wnmodel.bin", mmap="r")
    kv2 = KeyedVectors.load("./glove.bin", mmap="r")

    word = sys.argv[1]

    print("[glove]")
    for label, value in kv2.most_similar(word):
        print((label, value))
    print()

    print("[wordnet vector]")
    for label, value in kv1.most_similar(get_synset(conn, word)):
        print((get_lemma(conn, label), value))

いくつかの出力例

# python3 test_wnvec.py math
[glove]
('maths', 0.7655044794082642)
('curriculum', 0.754166841506958)
('graders', 0.7464368343353271)
('instruction', 0.7285574674606323)
('grades', 0.725632905960083)
('undergraduate', 0.7126580476760864)
('mathematics', 0.70766282081604)
('exams', 0.6997538208961487)
('teaching', 0.6977997422218323)
('courses', 0.6964027285575867)

[wordnet vector]
('mathematical_statement', 0.960015058517456)
('pure_mathematics', 0.9390747547149658)
('linear_equation', 0.9240595102310181)
('mathematical_process', 0.9231561422348022)
('variable_quantity', 0.9200597405433655)
('applied_mathematics', 0.9187830090522766)
('geometry', 0.9101800322532654)
('equation', 0.9067227840423584)
('mathematical_function', 0.9067083597183228)
('statistics', 0.9059611558914185)
# python3 test_wnvec.py book
[glove]
('books', 0.9047631025314331)
('story', 0.8662747144699097)
('novel', 0.855073869228363)
('writing', 0.843974232673645)
('published', 0.8439115881919861)
('biography', 0.8398316502571106)
('author', 0.8371229767799377)
('wrote', 0.8293616771697998)
('written', 0.8216683864593506)
('titled', 0.815525233745575)

[wordnet vector]
('book', 0.8401479721069336)
('publication', 0.7956532835960388)
('reference', 0.7846757173538208)
('coffee-table_book', 0.7720504403114319)
('phrase_book', 0.7648301720619202)
('periodical', 0.7576828002929688)
('edition', 0.7527894377708435)
('compendium', 0.7480593323707581)
('instruction_book', 0.7416238188743591)
('material', 0.7404926419258118)
# python3 test_wnvec.py politician
[glove]
('businessman', 0.7817543745040894)
('mp', 0.7724176645278931)
('liberal', 0.7486167550086975)
('lawmaker', 0.73398756980896)
('jurist', 0.7278751134872437)
('parliamentarian', 0.7202853560447693)
('conservative', 0.7129016518592834)
('elected', 0.7100611925125122)
('candidate', 0.7029688954353333)
('citizen', 0.700545072555542)

[wordnet vector]
('governor', 0.7858524918556213)
('civil_authority', 0.7277477383613586)
('mayor', 0.727745771408081)
('parliamentarian', 0.6815254092216492)
('parliamentarian', 0.6815254092216492)
('jurist', 0.6712974309921265)
('deputy', 0.663148045539856)
('coadjutor', 0.6539346575737)
('backbencher', 0.6533874273300171)
('vicar-general', 0.6437203884124756)

いくつかの語をプロット

tsneで2次元へ圧縮してプロットしたものです。

f:id:mathgeekjp:20200302112547p:plain

objectやentityという抽象的な語が中央に位置する傾向にあります。

考察

gloveの場合、同一文脈で使われる語の類似度が高くなる傾向にありますが、wordnetベクトルの場合は階層を考慮した概念的な類似度のようなものが高くなる傾向にあるように見えます。

例えば、mathに類似する語として"curriculum"がgloveでは抽出されましたが、これはcurriculumという語が同一文脈で使われる傾向にあったに過ぎず、curriculumは学問ではありません。

一方で、wordnetベクトルの場合、mathという概念を表すものが類似していると示される傾向にあります。

wordnetベクトルを実用的に使う例は、パラフレーズの検出などのタスクにおける特徴量として使うケースです。同義文を探すという厳密なタスクでは、mathとcurriculumが一致していると判断されてしまうよりは、概念的に類似したものを評価してほしいわけです。もちろん、gloveなどのベクトルも特徴量にすることは可能ですが、特徴量を追加することによって(エントロピーの意味で)新しい情報が追加されなければその特徴量に価値はありません。その意味で、wordnetをベクトル形式に圧縮するこの手法は使いみちがあるかもしれません。

参考

  1. http://compling.hss.ntu.edu.sg/wnja/jpn/detail.htm
  2. GloVe: Global Vectors for Word Representation
  3. 放送大学テキスト「自然言語処理」 6章 意味の解析(1)
  4. 日本語WordNetを知る - Qiita