WordNetを使いやすいようにクラス化した話

# はじめに
　nltkライブラリにてWordNetを使って「十分使いやすいけどもっとザックリ使いたいなー」と思ったので、クラス化してみました。

# WordNet
　類義語や上位・下位などの関係性で示した概念辞書です。

# ダウンロード
　本題から逸れるため、以下を参照ください。コードもこちらを参考にさせていただきました。

https://happy-shibusawake.com/wordnet/456/#toc9

# 開発環境
python 3.9.12
janome 0.4.2
nltk 3.7


# コード
## コンストラクタと分かち書き
　トークナイザ・言語設定をコンストラクタで、単語を原形で変換するよう分かち書き関数を用意しました。
　原形でないとWordNetに引っかからない場合も多いので、分かち書きしています。
~~~ operate_wordnet.py
# -*- coding: utf-8 -*-
from nltk.corpus import wordnet as WN
from janome.tokenizer import Tokenizer

class OperateWordNet:
    def __init__(self, lang ="jpn"):
        self.t = Tokenizer()
        self.lang = lang
    
    def wakachi(self, text):
        return [token.base_form for token in self.t.tokenize(text)]

~~~
## 単語の意味
　当該単語を原形に変換し、意味を辞書に格納します。
~~~ operate_wordnet.py
    #単語の意味を表示
    def get_mean_word(self, text):
        mean_dict = {}
        mean_dict[text] =[(x, x.definition(self.lang)) for x in WN.synsets(self.wakachi(text)[0], lang = self.lang)]
        return mean_dict

~~~
## 単語同士の類似度を表示
　単語同士の類似度を表示します。複数の意味がある単語同士を比較する場合は、最大値を取ります。
~~~ operate_wordnet.py
    #単語同士の類似度を表示
    def get_similarity(self, word1, word2):
        syn = []
        for i in range(len(WN.synsets(word1, lang = self.lang))):
            for j in range(len(WN.synsets(word2, lang = self.lang))):
                syn.append(WN.synsets(self.wakachi(word1)[0], lang = self.lang)[i].path_similarity(WN.synsets(self.wakachi(word2)[0], lang = self.lang)[j]))
        if len(syn) != 0: val = round(max(syn),3)
        else: val = "-"
        return val

~~~
## 類義語検索
　当該単語の類義語を表示します。複数の意味をもつ単語は、包括したものを返します。
~~~ operate_wordnet.py
    #類義語検索
    def get_synonym(self, text):
        syno_list = set()
        for x in WN.synsets(self.wakachi(text)[0], lang = self.lang):
            syno_list = syno_list.union(set(x.lemma_names(lang = self.lang)))
        return list(syno_list)
~~~
## 上位語取得
　当該単語の上位語を表示します。上位になるほど概念的な内容になります。
~~~ operate_wordnet.py
    #上位語取得
    def get_higher_word(self, text):
        return [(x, x.definition(self.lang)) for x in WN.synsets(self.wakachi(text)[0], lang = self.lang)[0].hypernym_paths()[0]]
~~~

## 全コード
　クラス・実行部分などの全コードは以下になります。
~~~ operate_wordnet.py
# -*- coding: utf-8 -*-

from nltk.corpus import wordnet as WN
from janome.tokenizer import Tokenizer

class OperateWordNet:
    def __init__(self, lang ="jpn"):
        self.t = Tokenizer()
        self.lang = lang
    
    def wakachi(self, text):
        return [token.base_form for token in self.t.tokenize(text)]

    #単語の意味を表示
    def get_mean_word(self, text):
        mean_dict = {}
        mean_dict[text] =[(x, x.definition(self.lang)) for x in WN.synsets(self.wakachi(text)[0], lang = self.lang)]
        return mean_dict

    #単語同士の類似性を表示
    def get_similarity(self, word1, word2):
        syn = []
        for i in range(len(WN.synsets(word1, lang = self.lang))):
            for j in range(len(WN.synsets(word2, lang = self.lang))):
                syn.append(WN.synsets(self.wakachi(word1)[0], lang = self.lang)[i].path_similarity(WN.synsets(self.wakachi(word2)[0], lang = self.lang)[j]))
        if len(syn) != 0: val = round(max(syn),3)
        else: val = "-"
        return val

    #類義語検索
    def get_synonym(self, text):
        syno_list = set()
        for x in WN.synsets(self.wakachi(text)[0], lang = self.lang):
            syno_list = syno_list.union(set(x.lemma_names(lang = self.lang)))
        return list(syno_list)
    
    #上位語取得
    def get_higher_word(self, text):
        return [(x, x.definition(self.lang)) for x in WN.synsets(self.wakachi(text)[0], lang = self.lang)[0].hypernym_paths()[0]]


if __name__ == "__main__":
    OWN = OperateWordNet()
    #単語の意味を取得
    mean = OWN.get_mean_word("犬")

    print("・意味")
    for k,v in mean.items():
        print(k,v)

    #単語の類似度を取得
    val = OWN.get_similarity("犬", "猫")
    print("\n・類似度")
    print(["犬", "猫"], val)
    
    #単語の類義語を取得
    syn_word = OWN.get_synonym("猫")
    print("\n・類義語")
    print(syn_word)
    
    #上位語を取得
    higher_word = OWN.get_higher_word("犬")
    print("\n・上位語")
    for one_word in higher_word:
        print(one_word)
~~~

# 実行結果
　上記のコードの実行結果は以下になります。

・意味
犬 [(Synset('dog.n.01'), ['有史以前から人間に家畜化されて来た（おそらく普通のオオカミを先祖とする）イヌ属の動物', '多数の品種がある']), 
(Synset('spy.n.01'), ['敵の情報を得るために国家に雇われた、または競合他社の企業秘密を得るために会社に雇われた秘密諜報部員'])]

・類似度
['犬', '猫'] 0.2

・類義語
['ネコ', 'キャット', '猫', 'にゃんにゃん']

・上位語
(Synset('entity.n.01'), ['（生命がある、あるいは生命がないに関わらず）それ自身の明確な存在を持つと感知される、知られている、あるいは推定される何か'])
(Synset('physical_entity.n.01'), ['物理的な存在がある実体'])
(Synset('object.n.01'), ['触れることができて目に見える実体', '影を落とすことができる実体'])
(Synset('whole.n.02'), ['1つのものとしてとらえらえる、部分の集合'])
(Synset('living_thing.n.01'), ['生きている（または一度は生きていたば）実在物'])
(Synset('organism.n.01'), ['独立して行うまたは機能する能力を持つ（またはそのように発達することができる）生き物'])
(Synset('animal.n.01'), ['自発的な運動に特徴付けられる、生命を持つ有機体'])
(Synset('chordate.n.01'), ['脊索または脊柱を持つ脊索門の動物'])
(Synset('vertebrate.n.01'), ['分割した脊柱と頭蓋骨または頭蓋に包まれた大脳を持つ骨性または軟骨性頭蓋骨を持つ動物'])
(Synset('mammal.n.01'), ['皮膚がほぼ毛で覆われている、温血脊椎動物', '単孔類の小亜綱を除いて子供は生きたまま生まれミルクで育てられる'])
(Synset('placental.n.01'), ['胎盤を持つ哺乳動物', '単孔動物と有袋動物以外のすべての哺乳動物'])
(Synset('carnivore.n.01'), ['地上性または水生の食肉哺乳動物'])
(Synset('canine.n.02'), ['引っ込まない爪と典型的な長い鼻を持つ様々な裂脚類の哺乳動物各種'])
(Synset('dog.n.01'), ['有史以前から人間に家畜化されて来た（おそらく普通のオオカミを先祖とする）イヌ属の動物', '多数の品種がある'])

# 最後に
　BM25で「単語が微妙に嚙み合わなくて、類似度が低い」みたいな状態が発生したときに使えたりしないかなぁ、と思ってます。

# 参考
[鷹の目週末プログラマー](https://happy-shibusawake.com/wordnet/456/#toc9 "Pythonで自然言語処理ツールWordnetの辞書を使ってみる")