Developer Blog AIフレームワーク 開発ブログ

2025年11月07日 2025年10月30日

寿限無

MarkovとRNN

PyainoにMarkov.pyを加えました。

ufiesiaにあってpyainoになかったMarkov.pyをpyainoに加えました。ufiesiaのとは少し違いますが、マルコフ連鎖の辞書を作って、それを基にして文章が生成できるモジュールです。

Markov.pyで「いろは歌」

ともかく、Markov.pyを使って、いろは歌が正しくなぞれるかを試してみました。

from pyaino import Markov

text = ('いろはにほへとちりぬるを' 
        'わかよたれそつねならむ' 
        'うゐのおくやまけふこえて' 
        'あさきゆめみしゑひもせす')

markov = Markov.Markov()
markov.make_dictionary(text)
gen_text = markov.generate_text(50, 'い')
print(gen_text)

やってみればわかりますが、いろは歌の「い」からはじめて50文字を生成すると、ちゃんと「いろは歌」になるし、途中の文字からはじめても続きを正しく生成します。ここでマルコフ連鎖の辞書を確認しておきます。

markov.markov {('い',): ['ろ'], ('ろ',): ['は'], ('は',): ['に'], ('に',): ['ほ'], ('ほ',): ['へ'], ('へ',): ['と'], ('と',): ['ち'], ('ち',): ['り'], ('り',): ['ぬ'], ('ぬ',): ['る'], ('る',): ['を'], ('を',): ['わ'], ('わ',): ['か'], ('か',): ['よ'], ('よ',): ['た'], ('た',): ['れ'], ('れ',): ['そ'], ('そ',): ['つ'], ('つ',): ['ね'], ('ね',): ['な'], ('な',): ['ら'], ('ら',): ['む'], ('む',): ['う'], ('う',): ['ゐ'], ('ゐ',): ['の'], ('の',): ['お'], ('お',): ['く'], ('く',): ['や'], ('や',): ['ま'], ('ま',): ['け'], ('け',): ['ふ'], ('ふ',): ['こ'], ('こ',): ['え'], ('え',): ['て'], ('て',): ['あ'], ('あ',): ['さ'], ('さ',): ['き'], ('き',): ['ゆ'], ('ゆ',): ['め'], ('め',): ['み'], ('み',): ['し'], ('し',): ['ゑ'], ('ゑ',): ['ひ'], ('ひ',): ['も'], ('も',): ['せ'], ('せ',): ['す']}

「いろは歌」では、ある文字が決まると、それに続く文字は1つに決まるので、例えば、辞書のキー'い'に対し値は'ろ'というように、辞書も極めて単純です。

Markov.pyで「寿限無」

さて、それでは「いろは歌」よりは少し長い「寿限無」ではどうなるでしょう?そうあの「じゅげむじゅげむごこうのすりきれ...」です。

from pyaino import Markov

text = ('寿限無 寿限無 五劫の擦り切れ\n'
        '海砂利水魚の 水行末 雲来末 風来末\n'
        '食う寝る処に 住む処\n'
        '藪柑子の 藪柑子\n'
        'パイポ パイポ パイポの シューリンガン\n'
        'シューリンガンの グーリンダイ\n'
        'グーリンダイの ポンポコピーの ポンポコナーの\n'
        '長久命の 長助\n')    

markov = Markov.Markov(n=1)
markov.make_dictionary(text)
gen_text = markov.generate_text(150, '寿')
print(gen_text)

「いろは歌」とは打って変わって、うまく行きません。Markv.pyはN階マルコフ連鎖にも対応しているので、markov = Markov.Markov(n=1)のn=1をn=2とかn=3とかにして、これと対応して、gen_text = markov.generate_text(150, '寿')の'寿'を、'寿限'や'寿限無'にしてみても、なかなかうまくはいきません。たったこれだけの文章なのになかなか手ごわいのです。そこで、作られたマルコフ連鎖の辞書を見てみると、

markov.markov {('寿',): ['限'], ('限',): ['無'], ('無',): [' '], (' ',): ['寿', '五', '水', '雲', '風', '住', '藪', 'パ', 'シ', 'グ', 'ポ', '長'], ('五',): ['劫'], ('劫',): ['の'], ('の',): ['擦', ' ', '\n'], ('擦',): ['り'], ('り',): ['切'], ('切',): ['れ'], ('れ',): ['\n'], ('\n',): ['海', '食', '藪', 'パ', 'シ', 'グ', '長'], ('海',): ['砂'], ('砂',): ['利'], ('利',): ['水'], ('水',): ['魚', '行'], ('魚',): ['の'], ('行',): ['末'], ('末',): [' ', '\n'], ('雲',): ['来'], ('来',): ['末'], ('風',): ['来'], ('食',): ['う'], ('う',): ['寝'], ('寝',): ['る'], ('る',): ['処'], ('処',): ['に', '\n'], ('に',): [' '], ('住',): ['む'], ('む',): ['処'], ('藪',): ['柑'], ('柑',): ['子'], ('子',): ['の', '\n'], ('パ',): ['イ'], ('イ',): ['ポ', '\n', 'の'], ('ポ',): [' ', 'の', 'ン', 'コ'], ('シ',): ['ュ'], ('ュ',): ['ー'], ('ー',): ['リ', 'の'], ('リ',): ['ン'], ('ン',): ['ガ', '\n', 'の', 'ダ', 'ポ'], ('ガ',): ['ン'], ('グ',): ['ー'], ('ダ',): ['イ'], ('コ',): ['ピ', 'ナ'], ('ピ',): ['ー'], ('ナ',): ['ー'], ('長',): ['久', '助'], ('久',): ['命'], ('命',): ['の'], ('助',): ['\n']}

辞書のキーと、値の対応を見てみると、文字によっては、一つの文字に対してたくさんの値があることが分かります。どうりでうまくいかないわけです。

RNNで「寿限無」

言うは易しく行うは難しではなく、考えるより先に「ともかくやってみよう!」です。私の某古巣の大先輩の言葉ですが...。でもいきなりプログラムが長くなってしまいますが、悪しからず。

from pyaino.Config import *
from pyaino import common_function as cf
from pyaino import RNN
import time

# -- 各設定値 --
hidden_size = 32  # 中間層のニューロン数 
epoch = 300       # 学習回数
interval = 10     # 文章生成間隔

# -- 文章の用意 --
text = ('寿限無 寿限無 五劫の擦り切れ\n'
        '海砂利水魚の 水行末 雲来末 風来末\n'
        '食う寝る処に 住む処\n'
        '藪柑子の 藪柑子\n'
        'パイポ パイポ パイポの シューリンガン\n'
        'シューリンガンの グーリンダイ\n'
        'グーリンダイの ポンポコピーの ポンポコナーの\n'
        '長久命の 長助\n')    

tokenizer = cf.Tokenizer(text)
vocab_size = tokenizer.vocab_size()

data = tokenizer.encode(text) # 文章を数値データに変換
print('vocab_size =', vocab_size)
print('< 元の文字列 >')
print(text)
print('< 変換後のデータ >')
print(data)

# -- モデルの定義と各層の初期化 --
model = RNN.RNN_erf(vocab_size, hidden_size, hidden_size, vocab_size,
                    stateful=True, 
                    ol_act='Softmax',
                    loss='CrossEntropyErrorMasked',
                    optimize='Adam',
                    )
model.summary()

# -- 学習 -- 
error_record, acc_record = [], []; start_time = time.time()
print('学習を開始します')
for i in range(epoch):
    model.reset_state()
    x = data[:-1].reshape(1, -1) # バッチ軸追加
    t = data[ 1:].reshape(1, -1) # バッチ軸追加
    y, l = model.forward(x, t)#, dropout=0.5)
    acc  = cf.get_accuracy(y, t)
    model.backward()
    model.update(eta=0.0006)
    error_record.append(float(l))
    acc_record.append(acc)

    # -- 経過の表示 --
    elapsed_time = time.time() - start_time
    print('Epoch{:4d} elapse{:5.0f} | Err{:7.4f} prplx{:7.3f} acc{:7.3f}'
          .format(i, elapsed_time, float(l), np.exp(l), acc))
    if i % interval == 0:
        model.reset_state()
        seed = data[:10]
        created_data = model.generate(seed, 123, stochastic=False)
        created_text = tokenizer.decode(created_data)
        print(created_text)

# -- 学習曲線(エラーと正解率) --
cf.graph_for_error(error_record, acc_record)

# -- 最終確認(生成テスト) --
model.reset_state()
seed = data[:10]
created_data = model.generate(seed, 123, stochastic=False)
created_text = tokenizer.decode(created_data)
print(created_text)

hidden_sizeをはじめとして、少々ハイパーパラメータの調整が必要ですが、それを適切に設定すれば、きれいな学習曲線が得られ、元の「寿限無」がきちんと生成されます。添付したグラフは青がエラー、オレンジが正解率を表します。
さてこのプログラムを実行した結果で、特筆すべきは、seed = data[:10]のところを、seed = data[:1]とかdata[:2]とかというように短くしてもなお、ちゃんと「寿限無」が最後まで正しく綴られることです。つまり人に「寿」や「寿限」から始めて「寿限無」を最後まで言ってみろ!と言われて、最後まで言えるように、初めに与えられる文字の数がどうであれ、ちゃんと最後まで正しく生成できるのです。これは先のMarkov.pyのように文字の並びに従って辞書を作る方法では出来ないことなのです。こんなに短い「寿限無」を正しく綴れて何ほどのものかと思われるかもしれませんが、ニューラルネットワークの非常に優れた特性が表れていると思います。そう、決して大げさではなく、ルールを人がプログラムしたのでは出来ない、あるいは出来たとしても、様々なケースに対応するためには、とてつもなく複雑なルールをプログラムしなければならない、そんなことが簡単にできてしまうのです。
ここでは、最も単純なmodelで極めて短い「寿限無」をなぞっただけですが、ニューラルネットワークのポテンシャルを感じ取ってもらうには、単純であるがゆえに、はっきりとわかるのではないでしょうか?

ブログのご感想やAIフレームワーク「ufiesia」「pyaino」に関するお問い合わせは、
問い合わせフォームからお送りください。

お問い合わせ
  • お問い合わせ内容によりましては、ご期待に添えない場合やご回答が出来ない事が有ります。