<jupyter_start><jupyter_text>第10回: RNN: Exercise シェイクスピアをCharacter-Level RNNで学習する 概要本演習ではChainerを用いてCharacter Level RNNを実装します。まず、講師が概要を説明しますので、全体を掴んだところで演習に取り組んでください。 目標- ChainerでRNNを実装する- 言語モデルを学習させる- truncated backpropを実装する  Requirements<jupyter_code># install requirements
!apt -y install libcusparse8.0 libnvrtc8.0 libnvtoolsext1
!ln -snf /usr/lib/x86_64-linux-gnu/libnvrtc-builtins.so.8.0 /usr/lib/x86_64-linux-gnu/libnvrtc-builtins.so
!pip install https://github.com/kmaehashi/chainer-colab/releases/download/2018-02-06/cupy_cuda80-4.0.0b3-cp36-cp36m-linux_x86_64.whl
!pip install 'chainer==4.0.0b3'
!pip install tqdm
!pip install ipywidgets
!wget 'https://drive.google.com/uc?export=download&id=1cDr74NtAjCNqlhg8wozgvcRAMRx4MWpI' -O shakespeare.txt<jupyter_output>Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following NEW packages will be installed:
  libcusparse8.0 libnvrtc8.0 libnvtoolsext1
0 upgraded, 3 newly installed, 0 to remove and 0 not upgraded.
Need to get 28.9 MB of archives.
After this operation, 71.6 MB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu artful/multiverse amd64 libcusparse8.0 amd64 8.0.61-1 [22.6 MB]
Get:2 http://archive.ubuntu.com/ubuntu artful/multiverse amd64 libnvrtc8.0 amd64 8.0.61-1 [6,225 kB]
Get:3 http://archive.ubuntu.com/ubuntu artful/multiverse amd64 libnvtoolsext1 amd64 8.0.61-1 [32.2 kB]
Fetched 28.9 MB in 2s (12.7 MB/s)

7[0;23r8[1ASelecting previously unselected package libcusparse8.0:amd64.
(Reading database ... 16712 files and directories currently installed.)
Preparing to unpack .../libcusparse8.0_8.0.61-1_amd64.deb ...
7[24;0f[42m[30mProgress: [  0%][49m[39m [.................................................[...]<jupyter_text>Colaboratory用ランタイムをGPUにするランタイム -> ランタイムの変更「ハードウェアアクセラレータ」でGPUを選択 おまじない必要なモジュールをimportしましょう。<jupyter_code>%matplotlib inline
import chainer
from chainer import serializers
from chainer import cuda, Function, optimizers
from chainer import Link, Chain, ChainList, Variable
import chainer.functions as F
import chainer.links as L
import matplotlib.pyplot as plt
import tqdm
from tqdm import tqdm_notebook
import numpy as np
import cupy as cp<jupyter_output>/usr/local/lib/python3.6/dist-packages/cupy/core/fusion.py:659: FutureWarning: cupy.core.fusion is experimental. The interface can change in the future.
  util.experimental('cupy.core.fusion')<jupyter_text>Note:以下でCPU or GPUを使うか選択してください。<jupyter_code># xp = np  # for CPU computations
xp = cp  # for GPU computations<jupyter_output><empty_output><jupyter_text>Character Level言語モデル ある入力列$x_1, x_2, \dots, x_t$が与えられたときに、$x_{t+1}$を予測するモデルを文字レベルの言語モデル(character level language model)と呼びます。たとえば、'hello'を学習したCharacter Level言語モデルは、'hell'が与えられたときに、次に現れるアルファベット'o'を予測する事ができます。本演習では、Shakespeareの悲劇の一つである『リア王』を用いて、Character Level Language Modelを学習させてみます。  学習データの構造と前処理 学習データの構造さて、学習に用いるデータセットは、例えば以下のような文章で構成されています。```BIONDELLO:Marry, that it may not pray their patience.'KING LEAR:The instant common maid, as we may less bea brave gentleman and joiner: he that finds us with waxAnd owe so full of presence and our fooder at ourstaves. It is remorsed the bridal's man his gracefor every business in my tongue, but I was thinkingthat he contends, he hath respected thee.``` 文章を実際に扱うには、幾つかのプログラム上の工夫が必要になります。まず、学習データ中で使用される文字や記号をリカレントニューラルネットワークに入力する際に、これらの文字や記号をベクトルで表記する必要があります。例えば、'b'という文字をネットワークに入力する際には、アルファベットの二番目の文字なので、`...00010`というように、下から2bit目を立たせて表現します。 このように、記号をネットワークで扱いやすいような分散表現に変換する操作のことをword embeddings(単語の分散表現)と呼びます。実際に言語モデルや翻訳モデルを獲得する際には、このように文字(character)を変換するのではなく、よりまとまった表現として語(word)を分散表現に変換するWord to Vector(W2V)という手法も広く使われることも覚えておくと良いでしょう。 Chainerはこうした文字埋め込みの分散表現が`links.EmbedID`に実装されています。例えば、`abc...xyz`(小文字アルファベット、23文字)から構成される、任意の時系列データを学習させる場合の前処理は、以下のように記述できます。  Example```pythonembed = L.EmbedID(23, l1_hidden_units)``` EmbedIDでは入力をone-hotベクトルとして、次の層に渡す役割を担います。  前処理(preprocessing) 今回使用するデータセットは、アルファベット(大文字、小文字)、及び句読記号(コンマ、コロン、セミコロンなど)にから構成されそうです。実際にデータ・セットを読み込んで確かめてみましょう。<jupyter_code># load text file
with open('./shakespeare.txt', 'r') as f:
    full_text = f.readlines()
# check how it looks like (if you want)
# print(full_text)<jupyter_output><empty_output><jupyter_text>さて、`list`に格納された`full_text`を学習データセットとして扱うには少し工夫が必要です。すなわち、- 文字をone-hot表現に変換- 段落の終わりを告げる``を導入- 行の終わりを告げる``を導入する必要があります。まず、文字のone-hot表現への変換ですが、これは文字と(uniqueな)IDの一対一対応を持つ辞書を作成すれば良いでしょう。また、``, ``の導入も上記の辞書を使用することで解決できますが、これによって、データをネットワークにfeedする際に区切りをつけて処理を行ったり、改行を扱ったりすることが出来ます。この前処理(preprocessing)は、以下のように実装されます。<jupyter_code>symbol_dict = {'<EOL>': 0, '<EOP>': 1}
dataset = []
for line in full_text:
    if line == '\n':
        dataset.append(Variable(xp.array([symbol_dict['<EOP>']], dtype=xp.int32)))
        continue
    for letter in line.replace('\n', ''):
        if not letter in symbol_dict:
            symbol_dict[letter] = len(symbol_dict)
        dataset.append(Variable(xp.array([symbol_dict[letter]], dtype=xp.int32)))
    dataset.append(Variable(xp.array([symbol_dict['<EOL>']], dtype=xp.int32)))
# symbol dict: symbols -> id
print(symbol_dict)<jupyter_output>{'<EOL>': 0, '<EOP>': 1, 'T': 2, 'h': 3, 'a': 4, 't': 5, ',': 6, ' ': 7, 'p': 8, 'o': 9, 'r': 10, 'c': 11, 'n': 12, 'e': 13, 'm': 14, 'l': 15, 'i': 16, "'": 17, 'd': 18, 'u': 19, 's': 20, 'f': 21, 'I': 22, 'y': 23, 'v': 24, ';': 25, 'q': 26, 'H': 27, 'b': 28, 'k': 29, 'x': 30, 'A': 31, 'B': 32, 'w': 33, 'g': 34, 'G': 35, '.': 36, 'O': 37, 'N': 38, 'D': 39, 'E': 40, 'L': 41, ':': 42, 'M': 43, 'K': 44, 'R': 45, 'j': 46, 'S': 47, '-': 48, 'F': 49, 'W': 50, 'U': 51, 'Y': 52, 'C': 53, 'V': 54, 'P': 55, '?': 56, '!': 57, 'J': 58, 'X': 59, 'z': 60, 'Q': 61, 'Z': 62}<jupyter_text>keyとvalueの関係を反転させた辞書配列`symbol_dict_inv`を用意します．<jupyter_code># inv dict: id -> symbols
symbol_dict_inv = {v: k for k, v in symbol_dict.items()}
print(symbol_dict_inv)<jupyter_output>{0: '<EOL>', 1: '<EOP>', 2: 'T', 3: 'h', 4: 'a', 5: 't', 6: ',', 7: ' ', 8: 'p', 9: 'o', 10: 'r', 11: 'c', 12: 'n', 13: 'e', 14: 'm', 15: 'l', 16: 'i', 17: "'", 18: 'd', 19: 'u', 20: 's', 21: 'f', 22: 'I', 23: 'y', 24: 'v', 25: ';', 26: 'q', 27: 'H', 28: 'b', 29: 'k', 30: 'x', 31: 'A', 32: 'B', 33: 'w', 34: 'g', 35: 'G', 36: '.', 37: 'O', 38: 'N', 39: 'D', 40: 'E', 41: 'L', 42: ':', 43: 'M', 44: 'K', 45: 'R', 46: 'j', 47: 'S', 48: '-', 49: 'F', 50: 'W', 51: 'U', 52: 'Y', 53: 'C', 54: 'V', 55: 'P', 56: '?', 57: '!', 58: 'J', 59: 'X', 60: 'z', 61: 'Q', 62: 'Z'}<jupyter_text>ネットワークに与えられる入力列(dataset)は以下のように、記号に対応したidに置き換えられた形になります ```python[2, 3, 4, 5, 6, 7, 8, 9, 9, 10, 7, 11, 9, ...``` この入力列は実際には、```python['T', 'h', 'a', 't', ',', ' ', 'p', 'o', 'o', 'r', ' ' ```を表しています。(気になる方は以下のサンプルコードを実行してみてください)<jupyter_code># below won't work for gpu
# print([v.data[0] for v in dataset])
# below won't work for gpu
# print([symbol_dict_inv[v.data[0]] for v in dataset][0:400])<jupyter_output><empty_output><jupyter_text>Note:以上がネットワークにどのような情報を与えるのか決める、前処理(preprocessing)の作業となります。言語処理においてどのように前処理を行うのかという問題は、望ましい出力を得るために重要となります。  Character Level言語モデルを学習するRNNの実装では、実際にCharacter Level Language Modelを学習するRNNを`Chainer`で実装していきましょう。 今回は、学習データの関係上、二層のRNNを使用して実装を行います。<jupyter_code>class CharRNN(Chain):
    def __init__(
        self,
        n_input,
        n_hidden,
        n_output,
    ):
        super(CharRNN, self).__init__()
        with self.init_scope():
            self.input = L.EmbedID("*** YOUR CODE HERE ***")  # YCH
            self.lstm = L.LSTM("*** YOUR CODE HERE ***")  # YCH
            self.lstm2 = L.LSTM("*** YOUR CODE HERE ***")  # YCH
            self.out = L.Linear("*** YOUR CODE HERE ***")  # YCH
            
    def __call__(self, inpt):
        x = "*** YOUR CODE HERE ***"  # YCH
        h1 = "*** YOUR CODE HERE ***"  # YCH
        h2 = "*** YOUR CODE HERE ***"  # YCH
        y = "*** YOUR CODE HERE ***"  # YCH
        return y
    
    def reset_state(self):
        '''
        a function to clear the states in the hidden layer
        '''
        self.lstm.reset_state()  # YCH
        self.lstm2.reset_state()  # YCH<jupyter_output><empty_output><jupyter_text>truncated BPの実装 BPTTの問題非常に長いシークエンスを学習することは、勾配計算をかなり前まで遡らなければならないことを意味します。こうした勾配計算は講義で解説したBPTT(Back Propagation Through Time)として実行されますが、* 計算過程で勾配消失・爆発を引き起こす* (長期にわたる勾配計算では誤差計算を保持しなければならないため)メモリリソースを圧迫するという点で問題があります。これらの問題を解決するために、しばしばバックプロパゲーションを短い時間範囲で切り捨てる場合があります。この手法はtruncated backpropagationと呼ばれます。これはヒューリスティックな手法であり、もちろん切り捨てられた勾配は失われますが、上記のような問題を上手く解決し、結果的に学習効率を向上させることが出来ます。  truncated BP `Chainer`にはtruncated backpropagationの実装として、`Variable.unchain_backward()`というメソッドが実装されています。`Backward Unchaining`は変数から計算履歴(勾配計算)の履歴を断ち切ります。 `truncated backpropagation`の例を以下に示します。<jupyter_code>rnn = CharRNN(n_input=len(symbol_dict), n_hidden=100, n_output=len(symbol_dict))
model = L.Classifier(rnn)
optimizer = optimizers.SGD()
optimizer.setup(model)<jupyter_output><empty_output><jupyter_text>Note:GPUを使用して学習する場合は、下記のコードを実行してください。<jupyter_code># !!! FOR GPU !!!
gpu_device = 0
cuda.get_device(gpu_device).use()
model.to_gpu(gpu_device)
# train rnn with truncated backpropagation
def partial_fit():
    loss = 0
    count = 0
    seqlen = len(dataset)

    rnn.reset_state()
    for i in tqdm.trange(seqlen-1, leave=False):
        cur_word = dataset[i]
        next_word = dataset[i+1]
        loss += model(cur_word, next_word)
        count += 1
        if count % 100 == 0 or count == seqlen:
            model.cleargrads()
            loss.backward()
            loss.unchain_backward()
            optimizer.update()
    print('Train Error: {:4f}'.format(float(loss.data)))
    return loss<jupyter_output><empty_output><jupyter_text>各ステップでの誤差は`loss`に蓄積されています。今回の実装では、100stepごとに蓄積された誤差に対してバックプロパゲーションを行うと同時に、`loss.unchain_backward()`メソッドを呼び出すことで、蓄積された損失を計算履歴から消去(切り捨て)しています。  学習では実際に学習してみましょう。CharRNNの学習には非常に時間がかかります。学習回数は5-10epoch程度が良いでしょう。<jupyter_code>epoch_loss = []
for i in tqdm.trange(3, leave=False):
    epoch_loss.append(partial_fit())<jupyter_output>0%|          | 0/3 [00:00<?, ?it/s]
  0%|          | 0/99992 [00:00<?, ?it/s][A
  0%|          | 1/99992 [00:05<152:29:45,  5.49s/it][A
  0%|          | 2/99992 [00:05<80:52:12,  2.91s/it] [A
  0%|          | 35/99992 [00:05<4:41:59,  5.91it/s][A
  0%|          | 66/99992 [00:06<2:32:05, 10.95it/s][A
  0%|          | 96/99992 [00:06<1:46:19, 15.66it/s][A
  0%|          | 116/99992 [00:09<2:12:55, 12.52it/s][A
  0%|          | 150/99992 [00:09<1:43:51, 16.02it/s][A
  0%|          | 170/99992 [00:09<1:34:21, 17.63it/s][A
  0%|          | 200/99992 [00:10<1:24:22, 19.71it/s][A
  0%|          | 232/99992 [00:10<1:13:26, 22.64it/s][A
  0%|          | 267/99992 [00:10<1:04:25, 25.80it/s][A<jupyter_text>順伝搬の計算 計算履歴を保存しないネットワーク評価RNNの順伝搬計算で必要なのは、入力及び一時刻前の隠れ層の状態のみで、逆伝搬のように計算履歴をすべて保存する必要はありません。`Chainer`では計算履歴を保存しないForward用の`chainer.config.enable_backprop`という`flag`を用意しています。  Example```pythonwith chainer.no_backprop_mode():    x = Variable(x_data)    feat = fixed_func(x)y = predictor_func(feat)y.backward()``` Warning:no_backprop_modeを使用した場合、計算グラフに値が保存されなくなります。それに伴い、loss.backward()は呼び出せなくなります。 では、例に習って順伝搬の計算を実装してみましょう。<jupyter_code>init_len = 10
rnn.reset_state()
out = []
with chainer.no_backprop_mode():
    x = dataset[11]
    for i in tqdm.trange(init_len):
        rnn(dataset[i])
    for i in tqdm.trange(init_len, 400):
        x = Variable(xp.array([rnn(x).data.argmax()], dtype=xp.int32))
        out.append(x)
predicted = [int(o.data) for o in out]
for cid in predicted:
    if cid == symbol_dict['<EOL>'] or cid==symbol_dict['<EOP>']:
        print('')
        continue
    print(symbol_dict_inv[cid], end='')<jupyter_output><jupyter_text>学習データの読み込み 学習済みデータの読み込みCLRNNの学習はGPUを使用していても時間がかかります。すでに学習済みのモデルがありますので、そちらを読み込んで遊んでみましょう。<jupyter_code>!wget 'https://drive.google.com/uc?export=download&id=1p75Yi0MABq8BhPMU_Na7zMiAPtskvA9J' -O clrnn.npz<jupyter_output><jupyter_text>事前に学習したモデルは`.npz`という`pickle`形式で保存されています。モデルの読み込みには`chainer.serializers`を使用します。学習した時と同じ設定でインスタンスを作らなければならない点に注意しましょう。(隠れ素子の数等パラメタに注意)<jupyter_code>rnn_loaded = CharRNN(n_input=len(symbol_dict), n_hidden=1000, n_output=len(symbol_dict))
model = L.Classifier(rnn_loaded)
serializers.load_npz("./clrnn.npz", model)
# !!! FOR GPU !!!
gpu_device = 0
cuda.get_device(gpu_device).use()
model.to_gpu(gpu_device)
init_len = 10
rnn.reset_state()
out = []
with chainer.no_backprop_mode():
    x = dataset[11]
    for i in tqdm.trange(init_len):
        rnn(dataset[i])
    for i in tqdm.trange(init_len, 400):
        x = Variable(xp.array([rnn_loaded(x).data.argmax()], dtype=xp.int32))
        out.append(x)
        
predicted = [int(o.data) for o in out]
for cid in predicted:
    if cid == symbol_dict['<EOL>'] or cid==symbol_dict['<EOP>']:
        print('')
        continue
    print(symbol_dict_inv[cid], end='')<jupyter_output><empty_output>
StarcoderdataJupyterStructuredCleanDedup