ユーニックス総合研究所

  • home
  • archives
  • janome-aiueo

Janomeであいうえお作文器を作る【自然言語処理, Python】

Janomeであいうえお作文

私たちが話す言葉は「自然言語」と呼ばれます。
この自然言語を解析するのが「自然言語処理」です。

自然言語処理にはいくつか工程がありますが、今回はその中でも最も基本的と言える形態素解析を使って、あいうえお作文を行うプログラムをPythonで作ってみたいと思います。
形態素解析にはPythonのライブラリであるJanomeを使います。

今回は具体的には↓を見ていきます。

  • プログラムの出力結果
  • あいうえお作文とは?
  • Janomeとは?
  • プログラムの設計
  • プログラムのソースコード
  • ソースコードの解説

プログラムの出力結果

今回作成するプログラムを実行すると↓のような出力結果になります。

アイウエオ作文器  
お題(カタカナで入力してください) > アイウエオ  
「ア」 朝主人が新聞を読むときは必ず彼の膝の上に乗る  
「イ」 一樹の蔭とはよく云ったものだ  
「ウ」 うちの小供があまり騒いで楽々昼寝の出来ない時や、あまり退屈で腹加減のよくない折などは、吾輩はいつでもここへ出て浩 然の気を養うのが例である  
「エ」 縁は不思議なもので、もしこの竹垣が破れていなかったなら、吾輩はついに路傍に餓死したかも知れんのである  
「オ」 大飯を食った後でタカジヤスターゼを飲む  

お題(カタカナで入力してください) > ユウヒ  
「ユ」 友人は固より何も知らずに連れ出されたのであるが、バルザックは兼ねて自分の苦心している名を目付ようという考えだから 往来へ出ると何もしないで店先の看板ばかり見て歩行いている  
「ウ」 うちの小供があまり騒いで楽々昼寝の出来ない時や、あまり退屈で腹加減のよくない折などは、吾輩はいつでもここへ出て浩 然の気を養うのが例である  
「ヒ」 人のを見ると何でもないようだが自ら筆をとって見ると今更のようにむずかしく感ずる」これは主人の述懐である  

お題(カタカナで入力してください) > オセチ  
「オ」 大飯を食った後でタカジヤスターゼを飲む  
「セ」 背といい毛並といい顔の造作といいあえて他の猫に勝るとは決して思っておらん  
「チ」 地に露華あり  

お題(カタカナで入力してください) >  

プログラムを起動すると「お題(カタカナで入力してください) >」というプロンプトが表示されます。
そして「アイウエオ」とか「ユウヒ」など、アイウエオ作文のお題を入力すると、プログラムがアイウエオ作文を行い、その結果を出力します。
↑の場合、「オセチ」という作文のお題ではプログラムは

「オ」 大飯を食った後でタカジヤスターゼを飲む  
「セ」 背といい毛並といい顔の造作といいあえて他の猫に勝るとは決して思っておらん  
「チ」 地に露華あり  

という出力を行っています。

あいうえお作文とは?

「あいうえお作文」とは日本の言葉遊びです。
お題となる言葉を出し、そのお題にそって作文を行います。

たとえばお題が「ゆうひ」だったら、「ゆ」「う」「ひ」のそれぞれの文字について作文を行います。
たとえば↓のようにです。

  • 「ゆ」 夕陽を見ながら
  • 「う」 うちでお酒を飲む
  • 「ひ」 ひいばあちゃん

あいうえお作文は昔の遊びかと思いきや、現代でもよくおこなわれています。
たとえばお笑い芸人である博多華丸氏は、R-1グランプリと言うお笑いの大会であいうえお作文を行い優勝しています。

Janomeとは?

今回プログラムの作成に使うライブラリはJanome(ジャノメ)というライブラリです。

Janomeは形態素解析(けいたいそかいせき)を行うPythonのライブラリです。
形態素解析とは字句解析のことです。

自然言語処理は↓のような工程に分かれています。

  • 字句解析
  • 構文解析
  • 意味解析
  • 文脈解析

↑のうち、もっとも基本的な解析が字句解析です。Janomeはこの字句解析を行います。
字句解析(形態素解析)とは、日本語の文章を単語のリストに変換する解析のことを言います。
たとえば「花がきれい」という文章があればこれを

花 / が / きれい  

↑のように単語ごとに分割します。
このように文章を単語ごとに分割しておくことで、字句解析以降の構文解析などの解析をやりやすくします。
解析された単語には「読み方」や「原形」や「品詞」など、その単語の情報が保存されます。
この情報を使うことでさまざまな自然言語処理を行うことが可能になります。

たとえば今回は、単語の「読み方」を参照します。
これは作文のお題にマッチする文章を検索するときに使います。

プログラムの設計

今回作成するプログラムは大きく分けて↓のような処理にわかれます。

  • 学習データの読み込み(夏目漱石の「吾輩は猫である」)
  • 入力されたお題から結果を生成

学習データとは言ってますが、今回は機械学習は行いません。
ただのJanomeによる辞書の作成を学習とここでは呼んでます。

入力されたお題から文章を生成しなくてはいけないのですが、今回はその文章の元になるデータとして、夏目漱石の「吾輩は猫である」の文章を使います。
これは青空文庫で公開されているものを使います。

このデータをファイルに保存しておいて、実行時にそのファイルを読み込みます。
そしてファイルの中身を形態素解析し、辞書データとして持っておきます。
お題から結果を生成するときに、お題の文字にマッチする読み方を持つトークン列を検索し、トークン列が見つかったらそれを元に文章を合成して結果を生成します。

文章にするとけっこう複雑なことをやっている印象がありますが、処理としては非常に単純な処理の組み合わせで作られています。
使っているのはif文とfor文ぐらいです。

プログラムのソースコード

プログラムのソースコードは↓になります。
このプログラムを実行するには↓のコードをsample.pyなどに保存し、python sample.pyなどで実行してください。

"""  
アイウエオ作文を行う  

License: MIT  
Created at: 2021/01/15  
"""  
# -*- coding: utf-8 -*-  
from janome.tokenizer import Tokenizer  


class AiueoMachine:  
    """  
    アイウエオ作文を行うマシン  
    """  
    def load(self, fname: str):  
        """  
        fnameのファイルを読み込んでモデルを初期化する  
        """  
        with open(fname, 'r', encoding='utf-8') as fin:  
            src = fin.read()  
            self.init_model(src)  

    def init_model(self, src: str):  
        """  
        srcの文字列を元にモデル(line_toks)を初期化する  
        line_toksはトークン列のリストである  
        """  
        self.line_toks: list = []  
        t = Tokenizer()  
        for line in src.split('。'):  
            toks = list(t.tokenize(line))  
            if not len(toks):  
                continue  
            self.line_toks.append(toks)  

    def merge_surface(self, toks: list) -> str:  
        """  
        トークン列の表層形(surface)をマージして、1つの文字列に変換する  
        """  
        s: str = ''  
        for tok in toks:  
            s += tok.surface  
        return s  

    def find_line(self, acronym: str) -> str:  
        """  
        acronym(頭文字)をもとにline_toksから合致するラインを探す  
        結果をラインの文字列として返す  
        """  
        for toks in self.line_toks:  
            tok = toks[0]  
            if tok.reading[0] == acronym:  
                return self.merge_surface(toks)  
        return None  

    def create(self, odai: str) -> str:  
        """  
        odaiを元にアイウエオ作文を行う  
        結果を文字列として返す  
        """  
        s: str = ''  
        for acronym in odai:  # 「acronym」は「頭文字」の意味  
            line: str = self.find_line(acronym)  
            if line:  
                s += f'「{acronym}」 {line}\n'  

        return s  


def main():  
    print('アイウエオ作文器')  

    # アイウエオ作文に必要なデータを読み込む  
    # 今回は青空文庫で公開されている夏目漱石の「吾輩は猫である」を使う  
    # ロードには時間がかかるが、これはモデルとして中間表現を作成し、ファイルなどに  
    # 保存しておけば速くなると思われる  
    # 今回はその実装はしていない  
    aiueo = AiueoMachine()  
    aiueo.load('wagahai_wa_neko_de_aru.txt')  

    while True:  
        # ユーザーから入力(アイウエオ作文のお題)を受け取る  
        try:  
            s: str = input('お題(カタカナで入力してください) > ')  
        except (KeyboardInterrupt, EOFError):  
            break  

        # お題を元にアイウエオ作文を行う  
        result: str = aiueo.create(s)  
        print(result)  


main()  

ソースコードの解説

簡単ですがソースコードの解説になります。

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

必要モジュールをインポートしておきます。
今回はJanomeのtokenizerモジュールのTokenizerクラスを使います。

from janome.tokenizer import Tokenizer  

Tokenizerクラスは形態素解析を行うクラスです。
これのメソッドtokenize()に文字列を渡すと、その文字列の形態素解析をしてくれます。
結果はジェネレーターで返ってきますが、これをlist()などでリストに変換すると、Janomeのトークン列にすることができます。
トークンとは単語の情報のことで、このトークンに単語の「読み方」や「原形」などが保存されています。
たとえば読み方はトークンのreading属性を参照することで得ることができます。

main関数の作成

プログラムはmain関数から始まります。
main関数内では最初に「アイウエオ作文器」と出力させておきます。タイトルコールですね。

それからAiueoMachineをオブジェクトにします。
このAiueoMachineが作文を行うマシンです。
メソッドのload()に学習データの元になるファイル名を渡します。
今回は夏目漱石の「吾輩は猫である」のテキストをwagahai_wa_neko_de_aru.txtというファイルに保存してあります。
load()が学習データを読み込むと準備完了ですが、学習に使うテキストの量によっては時間がかかります。

学習データを初期化したら無限ループに入ってユーザーから入力を得ます。
この入力は文字列で、作文のお題になるものです。
入力例としては「アイウエオ」や「ユウヒ」などが適当です。

input()はユーザーがCtrl+Cを入力したら例外KeyboardInterruptを送出します。
それからEOFが入力された場合は例外EOFErrorも送出します。
これらの例外をキャッチしたらループから抜けるようにしておきます。

入力を得たら、AiueoMachinecreate()メソッドに入力された文字列を渡します。
create()メソッドは結果として文字列を返してきますが、これが作文の結果の文字列です。
これをprint()で出力してループの一連の処理はおわりです。

def main():  
    print('アイウエオ作文器')  

    # アイウエオ作文に必要なデータを読み込む  
    # 今回は青空文庫で公開されている夏目漱石の「吾輩は猫である」を使う  
    # ロードには時間がかかるが、これはモデルとして中間表現を作成し、ファイルなどに  
    # 保存しておけば速くなると思われる  
    # 今回はその実装はしていない  
    aiueo = AiueoMachine()  
    aiueo.load('wagahai_wa_neko_de_aru.txt')  

    while True:  
        # ユーザーから入力(アイウエオ作文のお題)を受け取る  
        try:  
            s: str = input('お題(カタカナで入力してください) > ')  
        except (KeyboardInterrupt, EOFError):  
            break  

        # お題を元にアイウエオ作文を行う  
        result: str = aiueo.create(s)  
        print(result)  


main()  

AiueoMachineで作文を行う

AiueoMachineは作文を行うクラスです。
__init__()はありません。

class AiueoMachine:  
    """  
    アイウエオ作文を行うマシン  
    """  

load()で学習元のデータを読み込む

load()メソッドは引数のfnameのファイルを開き、その内容を読み込んで、init_model()メソッドに渡します。
init_model()は学習を行うメソッドです。
エンコーディングはutf-8固定になっています。
そのためファイルがcp932などの場合はエラーになります。

    def load(self, fname: str):  
        """  
        fnameのファイルを読み込んでモデルを初期化する  
        """  
        with open(fname, 'r', encoding='utf-8') as fin:  
            src = fin.read()  
            self.init_model(src)  

init_model()で学習を行う

init_model()メソッドは引数の文字列を元にして学習を行うメソッドです。
学習とは言ってますが、強化学習などの学習ではなくて内容的にはただの形態素解析です。

文字列を「。」で分割して文節のリストにします。そしての文節(line)をJanomeのTokenizerのメソッドtokenize()に渡します。
結果をlist()でトークン列のリストに変換し、トークン列が空じゃなければline_toksに追加します。
line_toksはトークン列のリストです。リストのリストなのでリストの入れ子ですね。

    def init_model(self, src: str):  
        """  
        srcの文字列を元にモデル(line_toks)を初期化する  
        line_toksはトークン列のリストである  
        """  
        self.line_toks: list = []  
        t = Tokenizer()  
        for line in src.split('。'):  
            toks = list(t.tokenize(line))  
            if not len(toks):  
                continue  
            self.line_toks.append(toks)  

create()でお題を元に作文を行う

create()メソッドは引数の文字列odaiをもとに作文を行い、その結果を文字列として返します。
odaifor文で回して頭文字(acronym)を取り出します。そしてその頭文字をfind_line()に渡して行(文節)を検索します。
行が見つかったらsに頭文字と行を保存します。
ループが終わったらsreturnしておわりです。

    def create(self, odai: str) -> str:  
        """  
        odaiを元にアイウエオ作文を行う  
        結果を文字列として返す  
        """  
        s: str = ''  
        for acronym in odai:  # 「acronym」は「頭文字」の意味  
            line: str = self.find_line(acronym)  
            if line:  
                s += f'「{acronym}」 {line}\n'  

        return s  

find_line()で行(文節)を探す

find_line()は引数の文字列acronym(頭文字)にヒットする行を学習データから探します。
内容的にはfor文でline_toksを回し、トークン列の先頭のトークンを取り出します。
その先頭のトークンの読み方(reading)の1文字目を参照して、acronymとマッチするか調べます。
マッチしたらそのトークンのトークン列は作文に適当な行と言うことになります。
マッチしたらトークン列をmerge_surface()メソッドに渡して、トークン列から文章を生成します。

マッチする行が見つからなかったらNoneを返します。

    def find_line(self, acronym: str) -> str:  
        """  
        acronym(頭文字)をもとにline_toksから合致するラインを探す  
        結果をラインの文字列として返す  
        """  
        for toks in self.line_toks:  
            tok = toks[0]  
            if tok.reading[0] == acronym:  
                return self.merge_surface(toks)  
        return None  

merge_surface()でトークン列の表層形をまとめる

merge_surface()メソッドはトークン列内のトークンの表層形(surface)をまとめて1つの文字列にして返します。
表層形とは単語のそのままの表記のことで、Janomeのトークンは属性surfaceとしてこれを持っています。
トークン列をfor文で回してトークンのsurfaceを参照し、文字列sに代入していきます。
こうするとトークン列から文章を復元することができます。

    def merge_surface(self, toks: list) -> str:  
        """  
        トークン列の表層形(surface)をマージして、1つの文字列に変換する  
        """  
        s: str = ''  
        for tok in toks:  
            s += tok.surface  
        return s  

おわりに

今回はJanomeを使ってあいうえお作文を行うプログラムを作りました。
マッチする行をランダムに選べるようにすれば、さらに表現豊かなプログラムになるかと思われます。
また学習データの中間表現の実装などいろいろ改造の余地はありそうです。
ライセンスはMITなので暇な人はやってみてください。

🦝 < 「ネコ」

🐭 < 寝ている顔が、子猫みたい