からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

7.1:言語モデルを使った文章生成【ゼロつく2のノート(実装)】

はじめに

 『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 本の内容を1つずつ確認しながらゆっくりと組んでいきます。

 この記事は、7.1節「言語モデルを使った文章生成」の内容です。文章生成を目的とした言語モデルをPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】


# 7.1節で利用するライブラリ
import numpy as np


7.1.2 文章生成の実装

 前章で実装したLSTMを用いた言語モデルRnnlmに文章生成機能を追加します。

 実装には、6.4.1項で実装した言語モデルのクラスRnnlmと、1.3.1項で実装したSoftmax関数softmax()を利用します。そのため、クラス・関数の定義を再実行するか、次の方法で実装済みのクラス・関数を読み込む必要があります。Rnnlmは「ch06」フォルダ内の「rnnlm.py」ファイルに、softmax()は「common」フォルダ内の「functions.py」ファイルに実装されています。

# 実装済みクラスの読み込み設定
import sys
sys.path.append('C://Users//「ユーザー名」//Documents//・・・//deep-learning-from-scratch-2-master')

# 実装済みクラスと関数を読み込み
from ch06.rnnlm import Rnnlm # LSTMを用いた言語モデル:6.4.1項
from common.functions import softmax # Softmax関数:1.3.1項

 「deep-learning-from-scratch-2-master」フォルダにパスを設定しておく必要があります。

・処理の確認

 図7-1を参考にして、文章生成で行う処理を確認していきます。

・データセットの読み込み

 まずは6.4節のときと同様に、PTBデータセットを読み込みます。

# PTBデータセットを読み込みライブラリのインポート
from dataset import ptb

# PTBデータセットを読み込み
corpus, word_to_id, id_to_word = ptb.load_data('train')

# 語彙数(単語の種類数)を取得
vocab_size = len(word_to_id)
print(vocab_size)

# 単語数を取得
corpus_size = len(corpus)
print(corpus_size)
10000
929589


 Rnnlmクラスのインスタンスを作成します。インスタンス作成時の引数の値には、デフォルト値を使用します。これは、次で読み込む学習済みのパラメータと対応させる必要があるためです。

# RNNLMのインスタンスを作成
model = Rnnlm()

# パラメータの初期値を確認
print(model.params[0])
print(model.params[0].shape)
[[-6.2552472e-03  1.8150551e-02 -1.4503438e-03 ...  2.6745778e-02
   6.1877705e-03 -4.1281590e-03]
 [-4.5018028e-03 -2.7203495e-03 -6.2381146e-03 ...  7.1418402e-03
   1.0493164e-03 -1.0854368e-02]
 [-1.3258258e-02  3.2072288e-03  1.6804747e-02 ... -5.4227891e-03
  -7.0164329e-03 -8.0148662e-03]
 ...
 [ 3.4142258e-03  2.2421738e-03 -3.4167015e-03 ... -1.1136994e-03
   1.1638995e-03  5.0220881e-03]
 [-3.7553969e-03  6.9573907e-05  6.0851971e-04 ...  3.6830239e-03
  -1.0657135e-02 -3.1288005e-03]
 [ 6.4188153e-03 -6.5606916e-03 -1.9480250e-03 ... -1.3211924e-02
  -5.6359647e-03  3.1271926e-03]]
(10000, 100)

 ちなみにparamsに格納されている0番目のパラメータは、Embeddingレイヤの重みです。

 RNNLMの学習処理は、6章で行いました。そこでここでは学習過程は省略して、学習済みのパラメータを利用しましょう。
 学習済みのパラメータは、「ch06」フォルダの「Rnnlm.pkl」ファイルです。Rnnlmクラスのload_params()メソッドにファイルパスを指定して読み込めます。

# 学習済みパラメータの読み込みに用いるライブラリを読み込み
import pickle

# 学習済みのパラメータを読み込み
model.load_params('C:/Users/「ユーザー名」/Documents/・・・/deep-learning-from-scratch-2-master/ch06/Rnnlm.pkl')

# 学習済みパラメータを確認
print(model.params[0])
print(model.params[0].shape)
[[-0.0668     0.01584   -0.0202    ...  0.02881   -0.04953   -0.0444   ]
 [-0.02687    0.03943   -0.0156    ...  0.047     -0.01478   -0.0002494]
 [-0.03857   -0.01062   -0.00392   ...  0.02908    0.000883  -0.01634  ]
 ...
 [ 0.1554    -0.04556   -0.1318    ... -0.00359   -0.1318     0.0317   ]
 [-0.04196   -0.08636   -0.0482    ... -0.04773   -0.12256    0.10504  ]
 [-0.0543    -0.1445    -0.04364   ...  0.02078   -0.0799     0.0981   ]]
(10000, 100)

 パラメータが置き換わりました。

・単語のサンプリング

 学習済みのパラメータを用いて単語をサンプリングします。

 最初に入力する単語を指定して、対応する単語IDに変換します。

# 最初の単語を指定
start_word = 'you'

# 最初の単語の単語IDを取得
start_id = word_to_id[start_word]
print(start_id)
316


 predict()メソッドでスコアを計算して、softmax()関数で総和が1になるようにスコアを正規化します。Softmax関数により正規化された出力は、確率分布のように扱えるのでした。

 ただし、predict()に渡す入力データはバッチデータに対応しているため、2次元配列である必要があります。この例は、バッチサイズ$N = 1$と言えます。

# 入力用に2次元配列に変換
x = np.array([start_id]).reshape((1, 1))
print(x)

# スコアを計算
score = model.predict(x)
print(np.round(score, 5))
print(np.sum(score))

# 正規化して確率分布に変換
p = softmax(score.flatten())
print(np.round(p, 6))
print(np.sum(p))
[[316]]
[[[-0.00286  0.00044 -0.00242 ...  0.00146  0.00124 -0.00218]]]
0.041011523
[1.e-04 1.e-04 1.e-04 ... 1.e-04 1.e-04 1.e-04]
1.0

 predict()の入力は、0番目のEmbedレイヤの入力データ$x_0$です。出力は、0番目のAffineレイヤの出力データ$\mathbf{y}_0 = (y_0, \cdots, y_{V-1})$です。$V$は語彙数です。

 ちなみに次のようにして、「you」の次の単語としての出現確率が最大の単語を確認できます。

# 確率の最大値
print(np.max(p))

# 確率が最大の単語ID
print(np.argmax(p))

# 確率が最大の単語
print(id_to_word[np.argmax(p)])
0.00010104248
6779
cellular

 この単語が「you」の次に一番出現しやすい単語です。(パラメータは同じはずなのに毎回単語が変わるのは何故だ?隠れ状態も同じだよなぁ。)

 各単語の出現確率pに従って、ランダムに単語を生成します。

# 次の単語をサンプリング
sampled = np.random.choice(len(p), size=1, p=p)
print(sampled)

# 単語を確認
print(id_to_word[sampled[0]])
[6877]
a.m.

 この単語IDをxに代入して同じ処理を行うことで、さらに次の単語を生成できます。

・文章生成

 単語のサンプリングを繰り返すことで、文章を生成します。

 最初に入力する単語の他に、サンプリングしない単語も指定しておきます。

# サンプリングする単語数を指定
sample_size = 10

# サンプルしない単語を指定
skip_words = ['N', '<unk>', '$']

# サンプルしない単語の単語IDを取得
skip_ids = [word_to_id[w] for w in skip_words]
print(skip_ids)
[27, 26, 416]


 指定した単語に続く文章を生成します。

 サンプリングされてもskip_idsに含まれる単語IDであった場合は採用しません。そのため繰り返し回数が固定されるfor文ではなく、while文を使ってword_idsが指定した単語数になるまで繰り返します。

# 単語IDの受け皿を初期化
word_ids = [start_id]

# 入力する単語を設定
x = start_id

# サンプリング
while len(word_ids) < sample_size: # 指定された単語数になるまで
    # 入力用に2次元配列に変換
    x = np.array(x).reshape((1, 1))
    
    # スコアを計算
    score = model.predict(x)
    
    # 正規化して確率分布に変換
    p = softmax(score.flatten())
    
    # 単語IDをサンプリング
    sampled = np.random.choice(len(p), size=1, p=p)
    
    # 記録
    if (skip_ids is None) or (sampled not in skip_ids): # スキップワードでないとき
        # 入力単語を更新
        x = sampled
        
        # サンプルされた単語をリストに格納
        word_ids.append(int(x))

 サンプリングされた単語IDsampledは要素が1つの1次元配列なので、int()で数値のスカラに変換してからword_idsに格納します。

 サンプリングされた単語IDは、次のようになります。

# サンプリングされた単語ID
print(word_ids)
[316, 3371, 2053, 7103, 809, 8342, 4600, 9117, 2670, 3503]


 単語IDのリストを単語に変換して、全体を結合します。

# 単語を結合
txt = ' '.join([id_to_word[w_id] for w_id in word_ids])
print(txt)
you taxpayers notion king watches demonstrated alexander comex credibility consist

 以上で文章を生成できました。

・実装

 処理の確認ができたので、RNNLMを用いた文章生成クラスを実装します。Rnnlmクラスを継承して、文章生成メソッドを実装します。

# 文章生成用の言語モデルの実装
class RnnlmGen(Rnnlm):
    # 文章生成メソッド
    def generate(self, start_id, skip_ids=None, sample_size=100):
        # 単語IDの受け皿を初期化
        word_ids = [start_id]
        
        # 最初の単語IDを設定
        x = start_id
        
        # 文章を生成
        while len(word_ids) < sample_size: # 指定された単語数になるまで
            # 入力用に2次元配列に変換
            x = np.array(x).reshape((1, 1))
            
            # スコアを計算
            score = self.predict(x)
            
            # 正規化して確率分布に変換
            p = softmax(score.flatten())
            
            # 単語IDをサンプリング
            sampled = np.random.choice(len(p), size=1, p=p)
            
            # 記録
            if (skip_ids is None) or (sampled not in skip_ids): # スキップワードでないとき
                # 入力単語を更新
                x = sampled
                
                # サンプルされた単語をリストに格納
                word_ids.append(int(x))
        
        return word_ids


 実装したクラスを試してみましょう。

 RnnlmGenのインスタンスを作成します。こちらでも学習過程を省略して、学習済みのパラメータを利用します。

# 文章ジェネレータのインスタンスを作成
model = RnnlmGen()

# 学習済みのパラメータを読み込み
model.load_params('C:/Users/「ユーザー名」/Documents/・・・/deep-learning-from-scratch-2-master/ch06/Rnnlm.pkl')


 generate()メソッドで、sample_size引数に指定した単語数分の単語IDをサンプリングします。

# 文章生成(単語IDをサンプリング)
word_ids = model.generate(start_id, skip_ids, sample_size=100)
print(word_ids)
[316, 7059, 8983, 4434, 9260, 385, 181, 7324, 3150, 3819, 8779, 3577, 6132, 68, 4486, 2908, 8241, 5946, 9903, 7498, 4808, 3872, 7266, 3453, 8205, 376, 4040, 8079, 3994, 9004, 6128, 6248, 5532, 7830, 3982, 7964, 8304, 6574, 5669, 5879, 7262, 8980, 2137, 3824, 1054, 7156, 1721, 8763, 2834, 7657, 1571, 9535, 9545, 4456, 7251, 8827, 1584, 8030, 1392, 119, 6597, 2665, 3304, 2584, 2775, 9449, 5853, 3967, 4084, 5872, 64, 8742, 8251, 3263, 3878, 9255, 860, 2845, 3824, 4283, 6161, 3979, 1578, 8705, 232, 6462, 4032, 2408, 2456, 8953, 3251, 7678, 4224, 6422, 4473, 1044, 5368, 6392, 1230, 330]


 サンプリングした単語IDを単語に変換して結合します。

# 単語を結合
txt = ' '.join([id_to_word[w_id] for w_id in word_ids])
print(txt)
you cubic ordinarily let buffett sooner for misleading loan apiece destroying structural explore filters thus lowest finishing incentives mink cameras manages crashes seagram homosexual vacuum retain excesses cycling thinks reinforcing intelogic adverse consistent phenomenon retailer civilian epicenter supports edwards toll plead coupons amusing allocated march pre-trial philippine chiron inspired petrochemicals downturn influx allied-signal sunday fray altman seek ozone carlos 's annuity land restaurant battle step maynard difficulty anywhere finished high-risk to shippers nurses approve weight glare values dark allocated macdonald stevenson owner dollars averaging under '80s plunge rough die upheaval connecticut predictions counterpart g.m.b proper units corn balloon malcolm slide


 最後に、文章の区切りを示す記号<eos>.(ピリオド)と\n(改行)に置換して文章を整形します。

# 文章を整形
txt = txt.replace(' <eos> ', '.\n')
print(txt)
you cubic ordinarily let buffett sooner for misleading loan apiece destroying structural explore filters thus lowest finishing incentives mink cameras manages crashes seagram homosexual vacuum retain excesses cycling thinks reinforcing intelogic adverse consistent phenomenon retailer civilian epicenter supports edwards toll plead coupons amusing allocated march pre-trial philippine chiron inspired petrochemicals downturn influx allied-signal sunday fray altman seek ozone carlos 's annuity land restaurant battle step maynard difficulty anywhere finished high-risk to shippers nurses approve weight glare values dark allocated macdonald stevenson owner dollars averaging under '80s plunge rough die upheaval connecticut predictions counterpart g.m.b proper units corn balloon malcolm slide

 文章を生成できました。ある程度の文法は再現されているように思われます。(<eos>は、中々出現しないっぽい?)

 この項では、6.4節で実装したLSTMを用いた言語モデルを使いました。次項では、6.5節で取り上げた(このブログでは実装しなかった)改良版の言語モデルを用いてより良い文章を目指します。

7.1.3 さらに良い文章へ

 6.5節で扱った改良版の言語モデルBetterRnnlmを用いて文章生成クラスをを実装します。

 前項と同様に、BetterRnnlmを読み込んでおきます。「ch06」フォルダ内の「better_rnnlm.py」ファイルに実装されています。

# 実装済みクラスの読み込み用の設定
import sys
sys.path.append('C://Users//「ユーザー名」//Documents//・・・//deep-learning-from-scratch-2-master')

# BetterRnnlmGenクラスを読み込み
from ch06.better_rnnlm import BetterRnnlm # 改良版LSTMを用いた言語モデル:6.5.4項


・実装

 前項のクラス定義のRnnlmBetterRnnlmに置き換えるだけで実装できます。

# 文章生成用の言語モデルの実装
class BetterRnnlmGen(BetterRnnlm):
    # 文章生成メソッド
    def generate(self, start_id, skip_ids=None, sample_size=100):
        # 単語IDの受け皿を初期化
        word_ids = [start_id]
        
        # 最初の単語IDを設定
        x = start_id
        
        # 文章を生成
        while len(word_ids) < sample_size: # 指定された単語数になるまで
            # 入力用に2次元配列に変換
            x = np.array(x).reshape((1, 1))
            
            # スコアを計算
            score = self.predict(x)
            
            # 正規化して確率分布に変換
            p = softmax(score.flatten())
            
            # 単語IDをサンプリング
            sampled = np.random.choice(len(p), size=1, p=p)
            
            # 記録
            if (skip_ids is None) or (sampled not in skip_ids): # スキップワードでないとき
                # 入力単語を更新
                x = sampled
                
                # サンプルされた単語をリストに格納
                word_ids.append(int(x))
        
        return word_ids

 BetterRnnlmに実装されているメソッドが継承されます。

・文章生成

 RnnlmGenクラスのインスタンスを作成して、学習済みのパラメータを読み込みます。学習済みのパラメータは、「https://www.oreilly.co.jp/pub/9784873118369/BetterRnnlm.pkl(クリックするとダウンロードが始まります)」からダウンロードできます。ダウンロードしたpklファイルは、「ch06」フォルダに保存しておきます。

# 文章ジェネレータのインスタンスを作成
model = BetterRnnlmGen()

# 学習済みのパラメータを読み込み
model.load_params('C:/Users/「ユーザー名」/Documents/・・・/deep-learning-from-scratch-2-master/ch06/BetterRnnlm.pkl')


 前項と同様にして、文章を生成します。

# 文章生成
word_ids = model.generate(start_id, skip_ids, sample_size=100) # 単語IDをサンプリング
txt = ' '.join([id_to_word[i] for i in word_ids]) # 単語に変換して結合
txt = txt.replace(' <eos> ', '.\n') # 文章を整形
print(txt)
you 're out the long-term sources of public community sources said.  
indirectly diverted by the anger of the drexel t. altman ag unit also of goldman sachs & co. and big bank people are forecasting that rating group has lifted its stock in the short battle.  
the supreme court 's ruling confirms more selling demand such as airline skin systems and voluntary liability.  
the verdict is n't valid mr. hastings said.  
mr. keenan faces also trelleborg 's subsidiary that has for a company with interests in the military and am divisions and said it was signing

 さっきよりも自然な文章になりましたか?(ここでは省略しましたが、2行目時点のtxtを表示すれば、改行されているところに<eos>があるのを確認できます。)

 文を設定してその続きを生成することもできます。

# 文を設定
start_words = 'the meaning of life is'
print(start_words)

# 単語ごとに分割
print(start_words.split(' '))

# 単語IDに変換
start_ids = [word_to_id[w] for w in start_words.split(' ')]
print(start_ids)
the meaning of life is
['the', 'meaning', 'of', 'life', 'is']
[32, 4748, 42, 2262, 40]


 インスタンスmodelには、先ほど入力したときの隠れ状態が保持されているので、reset_state()で隠れ状態を初期化しておきます。あるいは、もう一度インスタンスを作成します。

 指定した文の単語を順番に入力します。ただしここでは、最後の単語は入力しません。

# 隠れ状態を初期化
model.reset_state()

# 単語を順番に入力
for x in start_ids[:-1]:
    x = np.array(x).reshape((1, 1))
    model.predict(x)

 これで文の情報が隠れ状態に保存されました。(1語ずつ入れなきゃいけないの?)

 指定した文の最後の単語をgenerate()メソッドに入力して、続きの文章を生成します。

# 文章を生成
word_ids = model.generate(start_ids[-1], skip_ids)

# 設定した文と結合
word_ids = start_ids[:-1] + word_ids

# 文章を整形
txt = ' '.join([id_to_word[w_id] for w_id in word_ids])
txt = txt.replace(' <eos> ', '.\n')
print(txt)
the meaning of life is n't considered like.  
the winners were n't enough in the usx.  
the loss was to be even worse because it was one of its members to treat it.  
in contrast a 190-point drop in demand was somewhere between the house and the treasury.  
tokyo stocks closed higher in hong kong manila singapore and singapore.  
seoul manila and seoul were mixed.  
the investment was bomb watched.  
it rose times lower but lagged by fresh gains.  
more people said the market was late by very thin in thursday 's list of three days

 「the meaning of life is」に続く文章を生成できました。

 前節では、単語や文を入力してその続きの文章を生成しました。次節では、入力した文章を別の文章に変換することを考えます。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.

おわりに

 久々の更新となりました。ラストスパート!

 2021年の3月12日は、モーニング娘。'21の小田さくらさん22歳のお誕生日!!!おめでたい!

 おださくホント凄いから聴いて!早く世間に見付かってほしい🍡

【次節の内容】

www.anarchive-beta.com