実際に文章を生成する前に、前回同様、その手順を確認しておきましょう。そのコードが以下です。ここではCPUとGPUのどちらを使うかをNetクラスのインスタンスから得た後に、それを評価モードに変更してから、「私」の次にどんな形態素がくるかを推測するようにしています。コードが長いので順番に見ていきましょう(今回のノートブックには以下のコードをまとめて1つのセルに記述してあります)。
device = model.device
model.eval()
morphemes = ['私'] # 「morpheme」は「形態素」という意味
morpheme = morphemes[0] # 「私」の次に何がくるかを推測
sentence = morpheme
コメントにも書きましたが、「morpheme」とは「形態素」のことです。ここでは、「私」だけを例としていますが、この後で['私', '君']のように単一の形態素(文字列)を格納するリストを受け取り、「私」で始まる文章を生成し、次に「君」で始まる文章を生成し、……といった処理を行う関数を定義します。そのため、形態素を含むリストの名前をmorphemesと複数形にしてあります。この関数では、リストから要素を一つずつ取り出して、以下で見る処理を行っていきます。
変数morphemeにリストから要素を取り出しているのは、後で作成する関数でのループ処理でループ変数にリストの要素を代入する処理のエミュレートだと考えてください。変数sentenceは生成された文章を保存しておくのに使います。つまり、sentenceには「私」の後に推測された形態素が次々に結合されていき、最終的に「私は何とかかんとか」のような文字列になることを考えています(ただし、ここでは確認のための推測を一度だけ実行するので、「私」に続けては形態素が1つだけ追加されます)。
l = len(w2i) + 1
id = [[w2i[morpheme]]] # 形態素を辞書w2iでインデックス列(のリスト)に変換
tmp = torch.tensor(id).to(device) # それをテンソルに
model.init_hidden(1) # バッチサイズは1
o = model(tmp) # ニューラルネットワークモデルに入力
print(o.shape)
o = o.reshape(l) # 出力を一次元ベクトルに変形
print(o.shape)
次に形態素をインデックス列のリストに変換してニューラルネットワークに入力します。ここでは形態素が1つだけなので、形態素からインデックスに変換を行う辞書(w2i)を使用して、インデックス列(要素は1つだけ)を格納するリストを作成し、それをPyTorchのテンソルに変換したものをニューラルネットワークに入力しています。
このとき、バッチサイズを1としてinit_hiddenメソッドを呼び出している点に注意してください。今度はネットワークには、一度に1つのインデックス列だけを入力しているためです(学習時にはデータローダーを使って、一度にバッチサイズ=25個の分かち書きされたテキストを取得していました)。隠し状態のサイズはバッチサイズに依存するため、このようにしています。
得られた出力の形状を出力してから、その形状をベクトル(1次元配列)に変更している点にも注意が必要です(その要素数はl=辞書の要素数+1です)。これをどう使うかが以下のコードです。
probs = torch.nn.functional.softmax(o, dim=0).cpu().detach().numpy() # 確率に
next_idx = np.random.choice(l, p=probs) # インデックスをランダムに選択
next_morpheme = i2w[next_idx] # インデックスを形態素に
sentence += ' ' + next_morpheme # 「私」+' '+「推測された形態素」
id = [[w2i[next_morpheme]]] # 次に入力するインデックス
print(sentence)
print(next_morpheme)
上でベクトルにしたものをSoftmax関数に通すことで、出力を確率表現に変換しています。こうすることで、「私」の後にくると推測されるもののインデックスに対応する出力が高い値(確率)になります。
その後で呼び出しているnumpyのrandom.choiceメソッドについては少し説明が必要です。この関数は、ベクトル(1次元配列)からランダムに値を取り出します。ここで行っているように引数aに整数を渡した場合、それはnp.arange(a)を渡したのと同じです。例えば、4を与えれば、それは[0, 1, 2, 3, 4]という一次元ベクトルを渡したのと同じことになり、np.random.choiceメソッドはこのベクトルの中からランダムに値を取り出して返します。
num = np.random.choice(4)
print(num) # 0、1、2、3、4のいずれか
このとき、引数pでそれぞれの要素が選択される確率を指定できます。例えば、[0.05, 0.05, 0.05, 0.05, 0.80]というベクトルがあったとします。この場合は、最後の要素(4)が選択される確率が80%、その他の要素が選ばれる確率が5%ずつということです。よって、以下のようなコードがあったときには、4がたくさん表示されるようになるということです。
tmp = np.array([0.05, 0.05, 0.05, 0.05, 0.80])
for i in range(5):
num = np.random.choice(len(tmp), p=tmp)
print(f'{num}, ', end='') # 4が多めに表示される
これと同じことを上のコードでも行っているということです。
確率に従って得られたインデックスを形態素に変換すれば、それが「私」の次にくる形態素であり、ニューラルネットワークへの次の入力でもあります。
実行結果は例えば、次のようになります。
最初の2つの出力からはニューラルネットワークの出力が「バッチサイズ(1)×形態素の数(1)×(辞書の要素数+1)」という形状だったのをベクトルに変換したことが分かります。最後の2つの出力は「私」という入力から次にくる形態素として「は」が推測されたこと、次の入力が「は」になることを意味しています。
以上の処理を文章生成の上限回数だけループを回して、実行すれば、文章の生成を連続的に行えるでしょう。最終的に「。」などに出会うか、生成の上限回数に達するかしたら、そこで文章生成を終了します。
といったことをコードにまとめたのが以下です。
def make_sentence_from_one_word(morphemes, model, device, w2i, i2w):
model.eval()
batch_size = 1
l = len(w2i) + 1
eos = ['。', '!', '?']
result = []
with torch.no_grad():
for morpheme in morphemes:
model.init_hidden(batch_size)
sentence = morpheme
id = [[w2i[morpheme]]]
for idx in range(50):
t = torch.tensor(id).to(device)
outputs = model(t)
outputs = outputs.reshape(l)
probs = torch.softmax(outputs, dim=0).cpu().detach().numpy()
next_idx = np.random.choice(l, p=probs)
next_morpheme = i2w[next_idx]
sentence += ' ' + next_morpheme
if next_morpheme in eos:
break
id = [[w2i[next_morpheme]]]
result.append(sentence.replace(' ', ''))
return result
ここでは勾配を計算して逆伝播するようなことはないので、「with torch.no_grad():」で処理全体を囲んでいます(これにより速度やメモリの面で負荷が少しは減るはずです)。その内部ではおおよそ上で説明したようなことが行われているのが分かるでしょう。
実際に試してみましょう。
morphemes = ['私', '京都', '昨日', '明日', '今夜', '君', '猫', '檸檬']
make_sentence_from_one_word(morphemes, model, device, w2i, i2w)
実行結果は例えば以下のようになります。
それなりの文章もあれば、そうでないものもあります。が、少し気になるところがあります。上のコードを何度か実行してみると、最後の「檸檬」から生成される文章がかなりの頻度で「檸檬などごくありふれている。」となってしまうことです。
さらに、生成された文章の中で日本語としてきちんとしているものをWebで検索してみると、それらの多くが、梶井基次郎の著作に含まれているようです(下から2つ目の「猫は相変わらず抱き合ったまま少しも動こうとしない」もそうでした)。やはり、これはやりすぎて、文章生成というよりは、梶井基次郎の著作で「〇〇」で始まる文章を求めているだけになってしまっているようです。いわゆる過学習の状態になっているといえるでしょう。
学習が完了した状態では、誤差は「0.2」程度でした。もしかしたら、ここまで学習してしまうのはよくないのかもしれません。そこで次回は過学習を避ける方法や、どの程度まで学習をすればよいのかという指標について考えてみる予定です。
Copyright© Digital Advantage Corp. All Rights Reserved.