ユーニックス総合研究所

  • home
  • archives
  • python-janome-rhyme

Janomeで韻辞典を作成【Python, 自然言語処理】

Janomeで韻辞典を作成

人間が話す言語を「自然言語」と言います。
その自然言語を科学的に解析するのが「自然言語処理」と呼ばれるジャンルです。

自然言語処理を行うと、日本語の文章をプログラム的に解析することが出来ます。

今回は自然言語処理の工程の1つである「形態素解析」を行って、韻辞典を作ってみたいと思います。
このスクリプトはどういうものかと言うと、サンプルになる日本語の文章から韻辞典を構築して、ユーザーの入力にヒットする単語を表示するというものです。
仕組み的には非常に単純で、一部サポートしていない韻もあります。

具体的には↓を見ていきたいと思います。

  • 自然言語処理の工程について
  • 形態素解析とは?
  • Janomeを使った形態素解析
  • 韻辞典の作成ロジック
  • スクリプトの作成
  • スクリプトの実行

掲載しているスクリプトのライセンスはMITです。
改造など自由に行ってください。

自然言語処理の工程について

人間の話す自然言語を処理する自然言語処理では、いくつかの工程にわかれて自然言語を解析します。
まず最初に行うのが、この記事でもあつかう「形態素解析」です。
これは字句解析とも呼ばれ、自然言語処理のもっとも基礎的な解析の1つです。

形態素解析の次に構文解析、意味解析、文脈解析と続きます。
つまり↓のような工程になります。

  • 字句解析(形態素解析)
  • 構文解析
  • 意味解析
  • 文脈解析

自然言語処理の最終目的は、おそらく人間の話す言語をそのままプログラムで入出力できるようにするというものだと思います。
これはつまり、プログラムに自然言語で会話させるということです。
人間とプログラムが自然な会話を行えるようにするというのが、多くの研究者の目標の1つだと予想できます。

その目標を実現するためには↑の工程を最後までクリアする必要があります。
現在の技術では、最後の文脈解析の実現が非常にむずかしいと言われています。
これが簡単になったらおそらくプログラムが長文のブログを書いたりとか、人間と自然に会話するとかの実現が近づくと思われます。
そうなったらもう人間の友達もいらなくなってますますボッチがはかどりそうですね。

🦝 < 科学はボッチを幸せにする

形態素解析とは?

自然言語処理のもっとも基礎的な解析と思われる形態素解析とはどういうものなのでしょうか?
形態素解析とは日本語の文章の解析で必要になる解析です。

たとえば文章を字句(トークン)に分割するという処理を考えてみます。
英語であれば字句への分割はスペースをチョップすれば可能です。そういった意味では英語は非常に解析しやすい言語です。
しかし日本語の場合はどうでしょうか。日本語は単語と単語がスペースで区切られていません。
そのため、形態素解析という解析を行って、単語と単語を分割する必要があります。

形態素解析とは辞書を使った解析です。膨大なデータがつまった辞書を参照して、単語を分割していきます。
よって形態素解析の性能は辞書の性能に大きく左右されると言われています。

形態素解析によって日本語の文章を単語ごとに分割できれば、あとはそれらの単語を使って次の解析を行うことが出来ます。
次の解析である構文解析では単語の係り受けを木構造で表現するため、文章が単語に分割されていないとにっちもさっちもいきません。

🦝 < 字句に分割するのがすべての基本なのね

🐭 < N-gramって方法もあるよ

Janomeを使った形態素解析

形態素解析ライブラリとして有名なのがMeCab(メカブ)と呼ばれるライブラリです。
これは昔から人気のあるライブラリで、幅広く使われています。
Janome(ジャノメ)とはそのMeCabの辞書を使ったPythonの形態素解析器です。こちらはpipでインストール可能ですぐに使うことが出来て非常に手軽です。

Janomeを環境にインストールするには、pipなどを使って↓のようにインストールします。

> pip install Janome  

韻辞典の作成ロジック

今回のスクリプトで行う韻辞典の生成では、Janomeが生成するトークンを使います。
Janomeは文章を形態素解析すると、トークンにreadingという属性を埋め込みます。
このreadingはカタカナで書かれた単語の「読み方」です。

まずカタカナに対応した母音の辞書を作っておいて、そのreadingの値を母音の文字列に変換します。
そうしてその変換した母音をトークンに保存して、トークン列として持っておきます。
こうしておいて、ユーザーの入力を解析して、その単語の母音にヒットする単語のリストを生成したトークン列の中から探します。

あとはヒットした単語のリストを画面に表示すれば、入力した単語の母音にヒットする単語が得られるという寸法です。

🦊 < めっちゃ単純だな

🦝 < せやな。しかしけっこう使えるよ

単語のreadingの母音辞書とのマッチングは非常に単純にしてあります。
そのためたとえば「ショ」とか「ミュ」といった、小文字を含んだ文字の母音は正確には取得できません。
「ショ」であればこれの母音は「ウ」か「オ」になります。しかし今回のスクリプトではこれは「イォ」になります。

この母音辞書とのマッチングを改善すればさらに高精度な韻辞典を生成できると思われます。
今回はそこまではやっていません。

スクリプトの作成

それではスクリプトです。
↓がコードの全文です。

# coding: utf-8  
from janome.analyzer import Analyzer  
from janome.tokenfilter import POSKeepFilter  


# 母音辞書  
rhyme_map = {  
    'ア': 'アァカガサザタダナハバパマヤャラワヮ',  
    'イ': 'イィキギシジチヂニヒビピミリ',  
    'ウ': 'ウゥクグスズツヅッヌフブプムユュルン',  
    'エ': 'エェケゲセゼテデネヘべぺメレ',  
    'オ': 'オォコゴソゾトドノホボポモヨョロヲ',  
}  


def parse_reading(reading):  
    """  
    readingをパースして母音(rhyme)にする  
    """  
    global rhyme_map  
    rhyme = ''  

    for c in reading:  
        for k, v in rhyme_map.items():  
            if c in v:  
                rhyme += k  
                break  

    return rhyme  


def is_in(toks, tok):  
    """  
    toksにtokが入っていたらTrue, でなければFalseを返す  
    """  
    for t in toks:  
        if t.surface == tok.surface:  
            return True  

    return False  


def unique(toks):  
    """  
    toksから重複したトークンを除く  
    """  
    dst = []  

    for tok in toks:  
        if not is_in(dst, tok):  
            dst.append(tok)  

    return dst  


def analyze(text):  
    """  
    テキストを解析してトークン列を生成する  
    """  
    token_filters = [POSKeepFilter(['名詞'])]  
    a = Analyzer(token_filters=token_filters)  
    toks = a.analyze(text)  
    toks = unique(toks)  

    for tok in toks:  
        tok.rhyme = parse_reading(tok.reading)  
        yield tok  


def find_toks(map_toks, rhyme):  
    """  
    rhymeにヒットするトークン列を探す  
    """  
    for tok in map_toks:  
        if rhyme in tok.rhyme:  
            yield tok  


def show_toks(toks):  
    """  
    toksを表示する  
    """  
    for tok in toks:  
        print(f'{tok.surface}({tok.reading}, {tok.rhyme})')  


def main():  
    """  
    textをもとに韻辞典を作成し、標準入力の単語の韻にヒットする単語のリストを表示する  
    """  
    text = '''  
吾輩は猫である。名前はまだ無い。  
 どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。この書生というのは時々我々を捕えて煮て食うという話である。しかしその当時は何という考もなかったから別段恐しいとも思わなかった。ただ彼の掌に載せられてスーと持ち上げられた時何だかフワフワした感じがあったばかりである。掌の上で少し落ちついて書生の顔を見たのがいわゆる人間というものの見始であろう。この時妙なものだと思った感じが今でも残っている。第一毛をもって装飾されべきはずの顔がつるつるしてまるで薬缶だ。その後猫にもだいぶ逢ったがこんな片輪には一度も出会わした事がない。のみならず顔の真中があまりに突起している。そうしてその穴の中から時々ぷうぷうと煙を吹く。どうも咽せぽくて実に弱った。これが人間の飲む煙草というものである事はようやくこの頃知った。  
    '''  

    # 辞書を生成  
    rhyme_map = list(analyze(text))  

    while True:  
        # 入力を取得  
        try:  
            s = input('in > ')  
        except KeyboardInterrupt:  
            break  

        if not len(s):  
            continue  

        # 入力をトークンに変換  
        toks = analyze(s)  
        tok = list(toks)[0]  

        # 入力の単語を辞書から検索  
        toks = find_toks(rhyme_map, tok.rhyme)  

        # 結果を出力  
        show_toks(toks)  


main()  

スクリプトを解説します。

必要モジュールのインポート

スクリプトの先頭で必要モジュールをインポートしておきます。
今回はJanomeのAnalyzerPOSKeepFilterを使います。

from janome.analyzer import Analyzer  
from janome.tokenfilter import POSKeepFilter  

Analyzerは形態素解析を行う解析器です。
POSKeepFilterAnalyzerに渡すフィルターで、これに「名詞」とか「動詞」を指定すると、その品詞のみの単語を抽出することが出来ます。

main関数

スクリプトはmain関数からはじまります。

def main():  
    """  
    textをもとに韻辞典を作成し、標準入力の単語の韻にヒットする単語のリストを表示する  
    """  
    text = '''  
吾輩は猫である。名前はまだ無い。  
 どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。この書生というのは時々我々を捕えて煮て食うという話である。しかしその当時は何という考もなかったから別段恐しいとも思わなかった。ただ彼の掌に載せられてスーと持ち上げられた時何だかフワフワした感じがあったばかりである。掌の上で少し落ちついて書生の顔を見たのがいわゆる人間というものの見始であろう。この時妙なものだと思った感じが今でも残っている。第一毛をもって装飾されべきはずの顔がつるつるしてまるで薬缶だ。その後猫にもだいぶ逢ったがこんな片輪には一度も出会わした事がない。のみならず顔の真中があまりに突起している。そうしてその穴の中から時々ぷうぷうと煙を吹く。どうも咽せぽくて実に弱った。これが人間の飲む煙草というものである事はようやくこの頃知った。  
    '''  

    # 辞書を生成  
    rhyme_map = list(analyze(text))  

    while True:  
        # 入力を取得  
        try:  
            s = input('in > ')  
        except KeyboardInterrupt:  
            break  

        if not len(s):  
            continue  

        # 入力をトークンに変換  
        toks = analyze(s)  
        tok = list(toks)[0]  

        # 入力の単語を辞書から検索  
        toks = find_toks(rhyme_map, tok.rhyme)  

        # 結果を出力  
        show_toks(toks)  

main関数内では韻辞典(rhyme_map)のもとになる文章textanalyze()関数で解析し、韻辞典を生成します。
今回は文章は夏目漱石の「吾輩は猫である」の一部を使っています。
それから無限ループに入りinput()関数でユーザーからの入力を受けます。
そして入力をanalyze()関数で解析して、トークンを得ます。
そのトークンの母音にヒットするトークン列をrhyme_mapの中から探します。これにはfind_toks()関数を使います。
find_toks()で得たトークン列をshow_toks()関数で出力し、一連の処理は完了です。

analyze関数

analyze()関数は引数のtextを解析してトークン列に変換します。

def analyze(text):  
    """  
    テキストを解析してトークン列を生成する  
    """  
    token_filters = [POSKeepFilter(['名詞'])]  
    a = Analyzer(token_filters=token_filters)  
    toks = a.analyze(text)  
    toks = unique(toks)  

    for tok in toks:  
        tok.rhyme = parse_reading(tok.reading)  
        yield tok  

最初にtoken_filtersというリストを作ります。
そしてそのリストにはPOSKeepFilter()のオブジェクトを渡しておきます。POSKeepFilter()にはリストで名詞を指定しておきます。こうすることでAnalyzerが名詞のみの単語を抽出するようになります。
Analyzer()token_filtersを渡してオブジェクト(a)にします。
そしてAnalyzeranalyze()メソッドに文章を渡します。するとトークン列が生成されます。
そのトークン列をunique関数に渡して、トークン列から重複したトークンを除外します。

トークン列をfor文で回して、parse_reading()にトークンのreading属性を渡します。
すると結果が母音になって帰ってくるので、それをトークンのrhyme属性に入れておきます。
それが完了したらトークンをyieldしておきます。

🦝 < yieldは使うとジェネレーターになるよ

uniqe関数

unique関数では引数のtoksから重複したトークンを除外して、dstに保存します。
重複しているかの判定はis_in()関数を使います。

def unique(toks):  
    """  
    toksから重複したトークンを除く  
    """  
    dst = []  

    for tok in toks:  
        if not is_in(dst, tok):  
            dst.append(tok)  

    return dst  

is_in関数

is_in()関数は引数toksの中にtokが含まれていたらTrueを返し、含まれていなければFalseを返します。
比較にはトークンのsurface属性を使います。surfaceとは表層形のことで、これはトークンが持つ元の文章のそのままの表記の文字列のことです。

def is_in(toks, tok):  
    """  
    toksにtokが入っていたらTrue, でなければFalseを返す  
    """  
    for t in toks:  
        if t.surface == tok.surface:  
            return True  

    return False  

parse_reading関数

parse_reading()関数はトークンのreadingを母音の文字列に変換します。
変換にはrhyme_mapという辞書を使います。これはグローバル変数です。
for文でreadingを1文字ずつ見ていって、その文字に該当するカタカナが辞書の中にあるかチェックします。
辞書の中に存在したらそれに対応する母音をrhyme変数に保存します。
最後にrhyme変数をreturnしておわりです。

# 母音辞書  
rhyme_map = {  
    'ア': 'アァカガサザタダナハバパマヤャラワヮ',  
    'イ': 'イィキギシジチヂニヒビピミリ',  
    'ウ': 'ウゥクグスズツヅッヌフブプムユュルン',  
    'エ': 'エェケゲセゼテデネヘべぺメレ',  
    'オ': 'オォコゴソゾトドノホボポモヨョロヲ',  
}  


def parse_reading(reading):  
    """  
    readingをパースして母音(rhyme)にする  
    """  
    global rhyme_map  
    rhyme = ''  

    for c in reading:  
        for k, v in rhyme_map.items():  
            if c in v:  
                rhyme += k  
                break  

    return rhyme  

find_toks関数

find_toks()関数は引数map_toksの中から、引数rhymeにヒットするトークン列を探します。
トークンがrhymeを含んでいたら、そのトークンをyieldします。

def find_toks(map_toks, rhyme):  
    """  
    rhymeにヒットするトークン列を探す  
    """  
    for tok in map_toks:  
        if rhyme in tok.rhyme:  
            yield tok  

show_toks関数

show_toks関数は引数toksを標準出力に出力します。
トークンのsurface, reading, rhyme属性を画面に表示します。

def show_toks(toks):  
    """  
    toksを表示する  
    """  
    for tok in toks:  
        print(f'{tok.surface}({tok.reading}, {tok.rhyme})')  

スクリプトの実行

このスクリプトを実行すると↓のような結果になります(見易くするため一部整形してあります)。

in > 話  
吾輩(ワガハイ, アアアイ)  
話(ハナシ, アアイ)  

in > 穴  
吾輩(ワガハイ, アアアイ)  
名前(ナマエ, アアエ)  
話(ハナシ, アアイ)  
薬缶(ヤカン, アアウ)  
真中(マンナカ, アウアア)  
穴(アナ, アア)  
煙草(タバコ, アアオ)  

in > 記憶  
記憶(キオク, イオウ)  
妙(ミョウ, イオウ)  
装飾(ソウショク, オウイオウ)  

「話」という単語は読み方は「ハナシ」なので、「アアイ」で韻を踏むことが出来ます。
↑の出力を見ると「吾輩」にヒットしているのがわかります。
「吾輩」は「ワガハイ」という読み方なので、「アアアイ」で韻を踏むことが出来ます。
「アアイ」は「アアアイ」の中に含まれているので検索にヒットしています。

その他「穴」という単語はかなりたくさんヒットしています。
いっぽう「記憶」は「妙」にヒットするなど、一部疑問符も付く結果です。

おわりに

今回は形態素解析で韻辞典を生成してみました。
ライミングは上品な言葉遊びですが、このスクリプトを使えばちょっとだけカンニングも出来そうです。

🦝 < YO! このライムはだいぶタイプだぜ!

🐭 < ワッサップ! メ~ン