ユーニックス総合研究所

  • home
  • archives
  • python-janome-siritori

Janomeでしりとりアプリを作る【Python, 形態素解析】

Janomeでしりとりアプリを作る

人間の話す言葉を「自然言語」と言いますが、この言語の解析を「自然言語処理」と言います。
自然言語処理はAIによる開発が活発になってきた昨今において注目されている分野です。

応用範囲は広く、自動書記や機械翻訳など多岐に渡ります。

自然言語処理の工程はいろいろありますが、その中の「形態素解析(けいたいそかいせき)」という工程は自然言語処理において基本的なものです。
Pythonにはこの形態素解析を行うライブラリに「Janome(ジャノメ)」があります。

この記事ではJanomeを使って簡単な「しりとりアプリ」を作ってみたいと思います。
具体的には↓を見ていきます。

  • 形態素解析とは?
  • Janomeとは?
  • しりとりアプリの設計
  • しりとりアプリを作る
  • しりとりアプリの動作風景

形態素解析とは?

形態素解析(けいたいそかいせき)とは簡単に言うとどんな処理なのでしょうか?
形態素解析とは、日本語の文章を単語のリストに分割する処理のことです。

英語の文章の解析では、英単語はスペースで区切られています。そのためスペースで単語を分割すれば簡単に単語ごとに分割することが出来ます。
しかし、日本語の文章の場合はそうもいきません。日本語の単語はスペースで区切られていないため、何か別の方法を使った解析が必要になります。
形態素解析がその手法の1つです。

形態素解析は日本語の文章を辞書を使って解析します。辞書の単語に照らし合わせて文章をパースし、文章を単語ごとに分割します。
こうすることで日本語の文章から単語のリストを得ることが可能です。

形態素解析は自然言語処理の中でもかなり基礎的な処理に位置づけられています。
形態素解析を行えるライブラリには「MeCab(メカブ)」などが有名で、広く使われています。

最近のPythonでは「Janome(ジャノメ)」というライブラリの人気が上がってきています。
今回はこのJanomeを使います。

Janomeとは?

Janomeとは形態素解析ライブラリです。
内部ではMeCabの辞書を使っており、速度はMeCabに劣りますがMeCabと同程度の精度で解析を行うことが可能とのことです。

Janomeはpipなどのパッケージマネージャで簡単に導入することが出来ます。
今回作成するアプリではこのJanomeを使うので、あらかじめ↓のようにしてJanomeをインストールしておく必要があります。

> pip install Janome  

MeCabはインストールに1手間かかるんですが、Janomeは↑のようにコマンド1発で導入することが出来ます。
そのため学習用の形態素解析ライブラリとして最近人気が高まってます。

しりとりアプリの設計

しりとりのルールですが、↓のようになっています。

  • 前の人の単語のおしりの文字から次の単語を言う
  • 単語のおしりに「ん」が付いたら負け

今回はユーザーは標準入力からしりとりの単語を入力します。そのため入力した単語に対しても形態素解析を行います。
コンピューターが自動でしりとりの単語を出力するわけですが、そのためには単語のプールが必要です。
単語のプールとは、コンピューターが単語を取り出す時に参照する言葉のプールです。

このプールの作成にも形態素解析を使います。
Janomeは日本語の文章を解析するとトークン(字句)のリストに変換してくれます。このリストをプールとして扱います。
プールの元になる日本語の文章には青空文庫で公開されている夏目漱石の「吾輩は猫である」の一文を使います。

アプリのおおまかな処理の流れは↓のようになります。

  1. 文章を解析してプールを作成
  2. ユーザーから入力を受け取る
  3. 形態素解析
  4. 単語に「ん」が付いていたらユーザーの負け
  5. コンピューターの単語を検索
  6. コンピューターの単語を表示
  7. 単語に「ん」が付いていたらコンピューターの負け

コンピューターも「ん」が付いていたら負けるようにしてありますが、コンピューターの検索した単語に「ん」が付いていたら、その単語を表示せずに次の単語を検索するなどの改造をすればコンピューターを強く出来ると思います。
今回はそこまでコンピューターに強さを求めていないので、↑のような流れで設計しました。

しりとりアプリを作る

それではしりとりアプリを作ります。
↓がコード全文です。

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


def analyze(text):  
    token_filters = [POSKeepFilter(['名詞'])]  
    a = Analyzer(token_filters=token_filters)  
    return a.analyze(text)  


def find(toks, tok):  
    for t in toks:  
        if tok.reading[-1] == t.reading[0]:  
            return t  

    return None  


def show(tok):  
    if tok is None:  
        print('見つかりませんでした。')  
        return  

    print(f'{tok.surface}({tok.reading})')  


def update(pool):  
    try:  
        surface = input('in > ')  
    except KeyboardInterrupt:  
        return False  

    if not len(surface):  
        return True  

    toks = list(analyze(surface))  
    if not len(toks):  
        return True  

    intok = toks[0]  
    if intok.reading[-1] == 'ン':  
        print('あなたの負けです。')  
        return True  

    cputok = find(pool, intok)  
    show(cputok)  

    if cputok.reading[-1] == 'ン':  
        print('CPUの負けです。')  
        return True  

    return True  


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

    while update(pool):  
        pass  


main()  

コードの解説は↓です。

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

最初に↓のようにして必要モジュールをインポートしておきます。

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

AnalyzerはJanomeのクラスで、フィルターを設定できる形態素解析器です。
POSKeepFilterAnalyzerに設定するフィルターで、POSとはPart Of Speechの略です。
このフィルターに特定の品詞を設定し、そのオブジェクトをAnalyzerに渡すことで特定の単語のみを抽出することが出来るようになります。

main関数

アプリはmain関数から始まります。
main関数内ではtextにプールのもとになる日本語の文章を保存しておきます。
そしてそのtextanalyze()関数で解析してプール(トークンのリスト)に変換します。
あとはwhile文でupdate()を定期的に呼び出します。update()にはプールを渡しておきます。

while文はupdate()Trueを返すあいだループを続行します。

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

    while update(pool):  
        pass  


main()  

update関数

update()関数内ではしりとりのロジックを処理します。
まずinput()関数でユーザーからの入力を受け取ります。
このときにCtrl+CなどでKeyboardInterruptが発生した場合はreturn Falseとしてアプリを終了させます。

このユーザーの入力をanalyze()関数で形態素解析して、トークン列(toks)にします。
そしてトークンを1つだけ(intok)取り出しておきます。
このときintokreading属性をチェックして、末尾に「ン」が入っていたら「あなたの負けです。」と表示します。
トークンのreading属性にはカタカナで単語の読み方が保存されています。このためしりとりではこのreading属性をひんぱんに参照します。

次にpoolからintokをキーにしてトークンを検索します。これにはfind()関数を使います。
そしてfind()が返してきたトークン(cputok)はコンピューターのものとして扱います。
このトークンをshow()関数で画面に出力します。
そのcputokreadingの末尾に「ン」があれば、コンピューターの負けなので「CPUの負けです。」と表示します。

def update(pool):  
    try:  
        surface = input('in > ')  
    except KeyboardInterrupt:  
        return False  

    if not len(surface):  
        return True  

    toks = list(analyze(surface))  
    if not len(toks):  
        return True  

    intok = toks[0]  
    if intok.reading[-1] == 'ン':  
        print('あなたの負けです。')  
        return True  

    cputok = find(pool, intok)  
    show(cputok)  

    if cputok.reading[-1] == 'ン':  
        print('CPUの負けです。')  
        return True  

    return True  

analyze関数

analyze()関数では引数のtextを形態素解析して、結果をトークン列で返します。
token_filtersPOSKeepFilter()を保存します。このときにPOSKeepFilter()に「名詞」を指定しておきます。
そしてtoken_filtersAnalyzer()に渡してAnalyzerをオブジェクトにします。
こうすることでこのAnalyzerは名詞のみをtextから抽出するようになります。

Analyzeranalyzer()メソッドにtextを渡して形態素解析を行います。
その結果はトークン列ですが、それをreturnしておわりです。

def analyze(text):  
    token_filters = [POSKeepFilter(['名詞'])]  
    a = Analyzer(token_filters=token_filters)  
    return a.analyze(text)  

find関数

find関数は引数toksからtokにヒットするトークンを探します。
ヒットの定義はしりとりのルールに従います。つまり引数tokreadingの末尾がtoksのトークンのreadingの先頭にマッチしていたらそのトークンをヒットしたものとしてreturnします。
ヒットしなかった場合はNoneを返します。

def find(toks, tok):  
    for t in toks:  
        if tok.reading[-1] == t.reading[0]:  
            return t  

    return None  

show関数

show関数は引数tokを画面に出力します。
tokNoneだった場合は画面に「見つかりませんでした。」と表示します。

def show(tok):  
    if tok is None:  
        print('見つかりませんでした。')  
        return  

    print(f'{tok.surface}({tok.reading})')  

しりとりアプリの動作風景

このしりとりアプリを動作させると↓のような出力になります(一部見やすくするため整形しています)。
↓では最初にユーザーが「姉(あね)」を入力し、CPUが「猫(ねこ)」と出力しています。
そしてユーザーが「小手(こて)」と入力すると、CPUは「掌(てのひら)」と出力します。
見た感じちゃんとしりとりになってますね。

in > 姉  
猫(ネコ)  

in > 小手  
掌(テノヒラ)  

in > 落下  
彼(カレ)  

in > 烈火  
彼(カレ)  

in > レンコン  
あなたの負けです。  

最後に↑のようにユーザーが「レンコン」と入力して負けています。
単語がプールに無かった場合は「見つかりませんでした。」と表示されます。

おわりに

今回はJanomeを使って簡単なしりとりアプリを作ってみました。
形態素解析を行うとこのように比較的に簡単にしりとりアプリを作ることが出来ます。
スクリプトのライセンスはMITです。

🦝 < りんご、ごりら、らっぱ

🐭 < パン

🦝 < はい負け~