ラビットチャレンジレポート 深層学習 その5
DCGAN
GAN(Generative Adversarial Network)とは
生成器と識別機を競わせて学習する生成&識別モデル。
Generator(生成器):乱数からデータを生成
Discriminator(識別器):入力データが真値(学習データ)であるかを識別する。
2プレイヤーのミニマックスゲームとは
- 1人が自分の勝利する確率を最大化する作戦をとる。
- もう一人は相手が勝利する確率を最小化する作戦をとる。
- GANでは、価値関数Vに対し、D(識別器)が最大化、G(生成器)が最小化を行う。
ここを少し解説する。
GANの単一データの損失関数は、バイナリークロスエントロピーで表せる。
・・・・(1)
- 真データを扱うときは、,となるので、(1)にそれぞれ代入すると、
・・・(2)
- 生成データを扱うときは、,となるので、これを(1)に代入する。
・・・(3)
(1)、(2)の2つを足し合わせると、
複数データをとるために期待値をとち、符号を逆にすると、価値関数は下記のようになる。
・・・(4)
判別器を評価する時
D(x)は0~1の範囲を出力するが、真データが来た時、1を出力してほしい。
(4)の右辺第一項は、D(x)=1が来た時、最大化できる。
また、D(G(z))は、生成器が出力したデータの判定なので、偽データとして判定してほしい。
よって、D(G(z))=0が理想である。よって、右辺第二項もこのとき最大化できる。
結果的に、判別器を理想通りに動作させるには、価値関数V(D,G)を最大化するようパラメータを調整することになる。
生成器を評価する時
生成器の理想は、判別器が間違って判定すること(生成器の出力に1を出力すること)である。
(4)の右辺第一項は、生成器には関係ないので無視する。
右辺第二項は、D(G(z))=1を生成するのが望ましい。すなわち、右辺第二項が最小化されることが望ましい。
結果的に、生成器を理想通り動作させるには、評価関数V(D,G)を最小化するようパラメータを調整することになる。
最適化方法
具体的な最適化方法は下記のとおりである。
1. 判別器の性能を更新するよう、価値関数を最大化
- 生成器のパラメータθgを固定
- 真データと生成データをm個ずつサンプルする
- を勾配上昇法(Gradient Ascent)で更新
2.生成器の性能を更新するよう、価値関数を最小化
- 判別器のパラメータθdを固定
- 生成データをm個ずつサンプル
- θgを勾配降下法(Gradient Descent)で更新
なぜ生成器は本物のようなデータを生成するのか
- 生成データが本物とそっくりな状況とは、であるはず。
- 価値関数がの時に最適化されていることを示せばよい。
- 二つのステップで確認する。
- Gを固定し、価値関数が最大値をとる時のD(x)を算出
- 上記のD(x)を価値関数に代入し、Gが価値関数を最小化する条件を算出
ステップ1:価値関数を最大化するD(x)の値は?
- Generatorを固定する。
y=D(x)、a=P_{data}(x)、b=P_g(x)とおけば、
y=D(x), a=pdata(x), b=pg(x)なので、
これが、価値関数が最大値をとる時のD(x)の値である。
DCGAN(Deep Convolutional GAN)
- GANを利用した画像生成モデル
- 生成器
- Pooling層の代わりに転置畳み込み層を利用
- 最終層はtanh、その他はReLU関数で活性化
- 判別器
- Pooling層の代わりに畳み込み層を使用
- Leaky ReLU関数で活性化
- 共通事項
- 中間層に全結合層を使わない
- バッチノーマライゼーションを適用
ラビットチャレンジレポート 深層学習 その4
Section5 物体検知とセマンティックセグメンテーション
物体認識の種類
分類(Classification) | 画面全体のクラスラベル |
物体検知(Object Detection) | 画面の中で物体を個別にボックス(bounding boxでとらえる |
意味領域分割(Semantic Segmentation) | 各ピクセルに対し単一のクラスラベル |
個体領域分割(Instance Segmentation) | 各ピクセルレベルで個別にクラスラベル |
一般に上から下に行くにしたがって、タスクの難易度は上がる。
物体認識に使用されるデータセット
物体認識モデルの評価には、ベンチマークとなるデータセットが良く使われる。
データセットは、それぞれの特徴があるので、想定用途によって使い分けられる。
- 代表的データセット
クラス下図 | Train+val数 | BOX/画像 | 解説 | |
VOC12 | 20 | 11,540 | 2.4 | VOC=Visual Object Classes 主要貢献者が2012年に亡くなったことに伴い終了 |
ILSVRC17 | 200 | 476,668 | 1.1 | コンペは2017年に終了(後継はOpen Images Challenge) ImageNet(21,841クラス/1400万枚以上)のサブセット |
MS COCO18 | 80 | 123,287 | 7.3 | COCO=COmmon Object Context 物体位置推定に対する新たな評価指標を提案 |
OICOD18 | 500 | 1,743,042 | 7.0 | ILSVRCの後継 ILSVRCやMS COCOとは異なるannotation process。Open Images V4(6000クラス以上/900万枚以上)のサブセット |
- 各データセットの画像サイズ
それぞれで画像サイズも異なる。
-
- PASCAL VOC:470×380
- ImageNet:500×400
- MS COCO:640×480
- Open Images:一様ではない
- データセットの使い分け方
- BOX/画像
少ないと、アイコン的な映りで、日常感とはかけ離れやすい。
大きいと、部分的な重なりなども見られ、日常生活のコンテクストと近くなる。
-
- クラス数
多いと一般に情報量は多いが、多すぎると、ImageNetのように過度に細かいクラス分けになっている時がある。
各データのポジショニングマップを書くと下記のようになる。
評価指標
Confusionマトリクス、Precision, Recall
Confusionマトリクス
Precision, Recall
Confidenceの閾値を変化させることで、Precision-Recall Curveが描ける。
IoU:Intersection over Union
正解のBBOXと推論結果のBBOXの面積の合計で、双方がオーバーラップした部分の面積を割った数字である。
物体検出においては、ラベルの確信度のみでなく、物体位置の予測精度も重要なため、この指標が良く用いられる
AP:Average Precision
IoUやConfidenceのある一点について閾値を定めれば、TrueとFalseを判定でき、Precision、Recallを計算できる。
しかし、モデルの評価としては、Confidenceの閾値を変化させたとき、閾値が高くても精度が高いものは指標としては高くあるべきで、ランキング評価のようなものも欲しくなる。
このための指標として、AP(Average Precision)というものが存在する。
定義としては、IoUの閾値を固定(0.5)し、Confidenceを走査することで得られるPrecise-Recall Curve(PR曲線)の下側の面積の値となる。
物体検出のクラスの数がCの時、平均AP(mAP)は下記のように計算される。
- MS COCOで導入された指標
IoU閾値を固定せず、0.5から0.95まで0.05刻みでAP&mAPを計算し、算術平均をとったもの。
FPS:Flames per Second
物体検出は自動運転のようなリアルタイム処理の必要な用途もあるので、処理時間の指標も必要である。そのため、1秒当たり何フレーム処理したか(FPS)と一回の推論に必要な時間(inference time)という指標がある。
比較には、lなんのデータセットを用いたかも考慮する必要がある。
物体検知モデルの概説
物体検知モデルは以前からあり、SIFTという技術が主流だったが、2012年に発表されたAlexNetを皮切りに、DCNNへと主流が移り変わった。
その後の流れは、下記のようになる。
物体検知で、黄色と黄緑に文字が塗り分けられているが、それぞれ下記のような検出器になる。
-黄緑:2段階検出器(Two-Stage detector)
-
- 候補領域の検出とクラス推定を別々に行う
- 相対的に精度が高い傾向
- 相対的に計算量が大きく推論も遅い傾向
-黄色:1段階検出器(One-Stage detector)
-
- 候補領域の検出とクラス推定を同時に行う
- 相対的に精度が低い傾向
- 相対的に計算量が小さく推論が速い傾向
それぞれのネットワークの構造は下記のようになる。
入力から特徴マップを取得するところまでは同じだが、One-stage detectorは、出力のネットワークを2つに分けて、それぞれClassificationとBox regressionに分けて出力する。
Two-Stage detectorは、2つに分けた出力をさらにネットワークに入力し、Classification出力とBOXの位置出力を分けて出力する。
動作イメージはそれぞれ下記になる。
Two-stage detectorは、一段目のネットワークで、オブジェクトの位置を推定し、クロップする。
2段目のネットワークで、クロップしたBOXの位置情報を細かく把握し、物体の識別をする。
One-Stage detectorは、上記を一つのネットワークで行う。
One-Stage detectorは、Single Shot Detector(SSD)とも呼ばれる。
以下、利用が拡大しているSSDの説明をする。
SSD:Single Shot Detector
SSDのイメージとしては、下記のようにDefaultのBOXを用意し、それを変形してconfidenceを出力する形になる。
`SSDのネットワークアーキテクチャ
下記のVGG16のネットワークがベースになっている。
下記がSSDのネットワークアーキテクチャである。
特徴としては、中間層からそれぞれ直接出力するパスがあることと、VGG16のFC層2層をConv層に変更し、最後のFC層は削除しているところである。
特徴マップからの出力
SSDでは、分類と、Default Boxの位置と大きさを変更するため、マップ中の1つの特徴量における1つのDefalut Boxについての出力サイズは、
#Class+4
となる。ここで、#Classは分類するクラスの数、4というのは、オフセット項(位置の差分(ΔX, Δy)、大きさの差分(Δw, Δh)の数である。
オフセット項は、位置や大きさに関して下記のようにパラメータ化される。
位置(x,y)に対しては線形であるのに対し、大きさ(w, h)に関しては指数で効いていることに注意する。
マップ中の各特徴量にk個のDefault Boxを用意すると、出力サイズは
k(#Calss+4)
になる。
さらに、特徴マップのサイズがm×nであるとすると、出力サイズは
k(#Lcass+4)mn
になる。
結局、特徴マップごとに用意するDefault Boxの数は、k×m×n個となる。
SDDのデフォルトBOXの数の計算例を示す。
上記のネットワークで、青のパスでは、デフォルトBOXは各特徴に4つ、赤のパスでは、デフォルトBOXは各特徴に6つ用意するとする。
また、VOCデータセットでは、クラス数20に背景クラス1が考慮され、#Class=21となる。
青、赤それぞれのパスを計算すると、
4×(21+4)×38×38+4×(21+4)×3×3+4×(21+4)×1×1
6×(21+4)×19×19+6×(21+4)×10×10+6×(21+4)×5×5
合計で、8,732×(21+4)になる。
余談だが、上記のイメージ図における各フィルターのサイズだが、階層が進むにつれて物理的にフィルタが小さくなるわけではなく、サイズは一緒で解像度が低くなるようなイメージである。
フィルタのサイズが大きければ細かい特徴量が把握でき、小さければ、より全体の特徴を把握できる。
その他の工夫
- Non-Maximum Suppression
これは、RCNNでも用いられている手法である。
同じ物体に対して、BOXが重なるような形で検出されることがあるが、この重複を解決するのがNon-Maximum SUppressionである。
具体的には、閾値のIoUをαなどと決めておき、同じ物体に対して複数重なっているBOXの中で、最もConfidenceが高いBOXに対して、IoU=α以上の値で重なっているBOXは、すべて消去するという方法でBOXの重複をなくす。
- Hard Negative Mining
背景が指定されている検出器だと、背景を検出するBOXの数のほうが、背景ではない物体を検出するBOXより圧倒的に数が多くなってしまうことがある。
そこで、背景:非背景のBOXの数の比に制限を加えて、バランスをとるようにする手法である。
損失関数
損失関数は、検出位置と、分類のconfidenceと、双方に対して定義する
Semantic Segmentation
Semantic Segmenttationは、ピクセルごとに物体の分類を行うものである。
ネットワークの模式図は下記のようになる。
畳み込みで特徴マップのサイズを小さくし、クラス分類をしたのち、クラス数分のチャネルを持ったマップに拡張する。これをアップサンプリングという。
^なぜわざわざ特徴マップを小さくしてプーリングしたのち、アップサンプリングをするというステップを踏むのか
(答え)特徴マップを小さくすることで、画像全体の特徴を掴んで分類する。
アップサンプリングの仕組み
アップサンプリングの技術をDeconvolution/Transposed Convolutionと呼ぶ。
この仕組みは、畳み込みと同様、プーリングされた画像に対して、カーネルフィルタを用いる。
1.特徴マップのpixel感覚をstrideだけ空ける。
2.特徴マップの周りに(kernel size-1)-paddingだけ余白を作る。
3.畳み込み演算を行う
下図はkernel size=3, padding=1, stride=1のDeconv.による3×3の特徴マップから5×5にUp-samplingされる様子を図示している。
Deconvでは、poolingで失われた情報が復元されるわけではない。
- 輪郭情報の補完
Poolingによるローカルな情報(輪郭)が失われるので、低レイヤーpooling層の出力を要素ごとに加算することで、輪郭情報を補完する。
U-net
上記に近い発想のネットワークとして、U-netがある。
Encoder部分で畳み込みで段階的に小さくした特徴マップを、Decoder部分で段階的に逆畳み込みで大きくしていくネットワークで、各ステップにスキップコネクションを使って輪郭情報を補完している。
特徴としては、低レイヤーの情報を要素ごとに加算するのではなく、チャネル方向に結合するところである。
Dilated Convolution
最後に、Convolution段階で受容野を広げるDilated Convolutionについて紹介する。
これはアップサンプリング時と同様の操作を畳み込み時に行うものになる。
これによって、より少ない層数で広い受容野の特徴量を獲得することが可能になる。
ラビットチャレンジレポート 深層学習Day4 その3
Section 5 Transformer
5-1 Seq2seq
seq2seqとは
系列(sequence)を入力として、系列を出すもの。Encoder-Decoderモデルとも呼ばれる。
入力系列が内部状態に変換(encode)され、内部状態から系列に変換(decode)される。
実応用上も、入力・出力ともに系列情報的なものが多く、自然言語処理に応用される。
-翻訳
-音声認識
-チャットボット
seq2seqの仕組み
seq2seqはRNNを使用した言語モデルを二つ連結した形になっている。
- 言語モデルとは
言語モデルとは、単語の並びに確率を与えるものである。
単語の並びに尤度、すなわち文章として自然化を確率で評価する。
具体的な手法としては、時刻t-1までの上方で、時刻tの事後確率を求め、同時確率を計算する。
- RNN
RNNは、系列情報を処理して内部状態に変換することが出来る。
- RNN×言語モデル
各地点で次のどの単語が来れば自然(事後確率が最大)かを出力できる。
言語モデルを再現するようにRNNの重みが学習されていれば、ある時点の次の単語を予測することが出来る。
先頭単語を与えれば、文章を生成することも可能になる。
Seq2seqの構造
Encoder側はRNN×言語モデルで内部状態を生成する。
Decoder側はRNN×言語モデルで構造はほぼ同じだが、隠れ状態の初期値にEncoderの内部状態を受け取り、文章を生成する。
学習に関しては、Decoderのoutput側に正解を与えれば、end2endで教師あり学習が行える。
- Teacher Forcing
Q:下記のモデルで起こる問題は?
A:Encoder側から渡された内部状態から生成した出力を次段に入力し、その出力をその次の段に入力し..ということを時系列で繰り返すので、ある段で誤差が生じると、時系列が進むにつれて誤差が増幅してしまうという問題がある。
そこで、正解ラベルを直接Decoderの入力にするというTeacher Forcingの手法が考案された。
Q:上記のTeacher Forcingのモデルで起こる問題は?
A:実際の推論では、教師データをdecoderに与えないため、学習時にはうまく収束したモデルでも、実際の推論では期待通りの動作をしない場合があるという問題がある。
対策として、一定の確率に基づいてTeacher forcingを与える場合とそうでない場合を混ぜて学習をするというScheduled Sampling手法が考案された。
実装演習
seq2seqのコードを実装する。
まず、必要なライブラリのインポートと環境設定をする。
注意点としては、wheel.pep425tagsが最新バージョンには無いため、wheel=0.34.1にダウングレードする必要があること、pytorchは旧バージョンはリンクが消えている場合があるので、最新版をインストールすることである。
pip install wheel==0.34.1 pip install pytorch
from os import path from wheel.pep425tags import get_abbr_impl, get_impl_ver, get_abi_tag platform = '{}{}-{}'.format(get_abbr_impl(), get_impl_ver(), get_abi_tag()) accelerator = 'cu80' if path.exists('/opt/bin/nvidia-smi') else 'cpu' #!pip install -q http://download.pytorch.org/whl/{accelerator}/torch-0.4.0-{platform}-linux_x86_64.whl torchvision import torch print(torch.__version__) print(torch.cuda.is_available()) ! wget https://www.dropbox.com/s/9narw5x4uizmehh/utils.py ! mkdir images data # data取得 ! wget https://www.dropbox.com/s/o4kyc52a8we25wy/dev.en -P data/ ! wget https://www.dropbox.com/s/kdgskm5hzg6znuc/dev.ja -P data/ ! wget https://www.dropbox.com/s/gyyx4gohv9v65uh/test.en -P data/ ! wget https://www.dropbox.com/s/hotxwbgoe2n013k/test.ja -P data/ ! wget https://www.dropbox.com/s/5lsftkmb20ay9e1/train.en -P data/ ! wget https://www.dropbox.com/s/ak53qirssci6f1j/train.ja -P data/ import random import numpy as np from sklearn.model_selection import train_test_split from sklearn.utils import shuffle from nltk import bleu_score import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from torch.nn.utils.rnn import pad_packed_sequence, pack_padded_sequence from utils import Vocab # デバイスの設定 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") torch.manual_seed(1) random_state = 42 print(torch.__version__)
次にデータセットの準備をする。今回は英語-日本語の対訳コーパスであるTanaka Corpus(
Tanaka Corpus - EDRDG Wiki )を使用する。
今回は、そのうちの一部分を取り出したsmall_parallel_enja: 50k En/Ja Parallel Corpus for Testing SMT Methods ( https://github.com/odashi/small_parallel_enja )を使用する。
データの読み込みと単語の分割をする
def load_data(file_path): # テキストファイルからデータを読み込むメソッド data = [] for line in open(file_path, encoding='utf-8'): words = line.strip().split() # スペースで単語を分割 data.append(words) return data train_X = load_data('./data/train.en') train_Y = load_data('./data/train.ja') # 訓練データと検証データに分割 train_X, valid_X, train_Y, valid_Y = train_test_split(train_X, train_Y, test_size=0.2, random_state=random_state)
この時点で入力と教師データは以下のようになっている。
print('train data', train_X[0]) print('valid data', valid_X[0])
train data ['where', 'shall', 'we', 'eat', 'tonight', '?'] valid data ['you', 'may', 'extend', 'your', 'stay', 'in', 'tokyo', '.']
単語辞書の作成をする。
データセットに登場する各単語にIDを割り振る。
ここで、特殊文字のPADは、バッチの大きさをそろえるためにダミー。BOSは系列の始まり、EOSは系列の終わりを表す。
UNKは語彙に存在しない単語を表す。
# まず特殊トークンを定義しておく PAD_TOKEN = '<PAD>' # バッチ処理の際に、短い系列の末尾を埋めるために使う (Padding) BOS_TOKEN = '<S>' # 系列の始まりを表す (Beggining of sentence) EOS_TOKEN = '</S>' # 系列の終わりを表す (End of sentence) UNK_TOKEN = '<UNK>' # 語彙に存在しない単語を表す (Unknown) PAD = 0 BOS = 1 EOS = 2 UNK = 3 MIN_COUNT = 2 # 語彙に含める単語の最低出現回数 再提出現回数に満たない単語はUNKに置き換えられる # 単語をIDに変換する辞書の初期値を設定 word2id = { PAD_TOKEN: PAD, BOS_TOKEN: BOS, EOS_TOKEN: EOS, UNK_TOKEN: UNK, } # 単語辞書を作成 vocab_X = Vocab(word2id=word2id) vocab_Y = Vocab(word2id=word2id) vocab_X.build_vocab(train_X, min_count=MIN_COUNT) vocab_Y.build_vocab(train_Y, min_count=MIN_COUNT) vocab_size_X = len(vocab_X.id2word) vocab_size_Y = len(vocab_Y.id2word) print('入力言語の語彙数:', vocab_size_X) print('出力言語の語彙数:', vocab_size_Y)
語彙数は下記である。
入力言語の語彙数: 3725 出力言語の語彙数: 4405
vocab_X.id2wordを見るとIDが0~3の語句は特殊文字に割り当てられ、4以下が文章に出てくる単語に割り当てられている。
次にテンソルへ変換する。
まず、モデルが文章を認識できるように文章を単語IDのリストに変換する。
def sentence_to_ids(vocab, sentence): # 単語(str)のリストをID(int)のリストに変換する関数 ids = [vocab.word2id.get(word, UNK) for word in sentence] ids += [EOS] # EOSを加える return ids train_X = [sentence_to_ids(vocab_X, sentence) for sentence in train_X] train_Y = [sentence_to_ids(vocab_Y, sentence) for sentence in train_Y] valid_X = [sentence_to_ids(vocab_X, sentence) for sentence in valid_X] valid_Y = [sentence_to_ids(vocab_Y, sentence) for sentence in valid_Y]
この時点で入力と教師データは以下のようになっている。
train data [132, 321, 28, 290, 367, 12, 2] valid data [8, 93, 3532, 36, 236, 13, 284, 4, 2]
データセットからバッチを取得するデータローダーを定義する。
この際、ながさのことなる複数の系列をバッチで並列に扱えるように、短い系列にはダミーでパディングし、パッチ内の系列の長さを合わせる。
(batch_size,max_length)サイズの行列を得るが、実際にモデルを学習する時にはバッチを跨いで各時刻ごとにすすめるので、転置する。
def pad_seq(seq, max_length): # 系列(seq)が指定の文長(max_length)になるように末尾をパディングする res = seq + [PAD for i in range(max_length - len(seq))] return res class DataLoader(object): def __init__(self, X, Y, batch_size, shuffle=False): """ :param X: list, 入力言語の文章(単語IDのリスト)のリスト :param Y: list, 出力言語の文章(単語IDのリスト)のリスト :param batch_size: int, バッチサイズ :param shuffle: bool, サンプルの順番をシャッフルするか否か """ self.data = list(zip(X, Y)) self.batch_size = batch_size self.shuffle = shuffle self.start_index = 0 self.reset() def reset(self): if self.shuffle: # サンプルの順番をシャッフルする self.data = shuffle(self.data, random_state=random_state) self.start_index = 0 # ポインタの位置を初期化する def __iter__(self): return self def __next__(self): # ポインタが最後まで到達したら初期化する if self.start_index >= len(self.data): self.reset() raise StopIteration() # バッチを取得 seqs_X, seqs_Y = zip(*self.data[self.start_index:self.start_index+self.batch_size]) # 入力系列seqs_Xの文章の長さ順(降順)に系列ペアをソートする seq_pairs = sorted(zip(seqs_X, seqs_Y), key=lambda p: len(p[0]), reverse=True) seqs_X, seqs_Y = zip(*seq_pairs) # 短い系列の末尾をパディングする lengths_X = [len(s) for s in seqs_X] # 後述のEncoderのpack_padded_sequenceでも用いる lengths_Y = [len(s) for s in seqs_Y] max_length_X = max(lengths_X) max_length_Y = max(lengths_Y) padded_X = [pad_seq(s, max_length_X) for s in seqs_X] padded_Y = [pad_seq(s, max_length_Y) for s in seqs_Y] # tensorに変換し、転置する batch_X = torch.tensor(padded_X, dtype=torch.long, device=device).transpose(0, 1) batch_Y = torch.tensor(padded_Y, dtype=torch.long, device=device).transpose(0, 1) # ポインタを更新する self.start_index += self.batch_size return batch_X, batch_Y, lengths_X
次にモデルの構築をする。
EncoderとDecoderのRNNを定義する。
PackedSequenceというクラスを使用する。これはパディング部分の計算を省略できるため、可変長の系列のバッチを効率よく計算できる。
また、出力されたテンソルはpad_packed_sequenceを用いて通常のテンソルに戻す。
まず、Encoderを構築する。
Encoder側でバッチを処理する際に、pack_padded_sequence関数によってpacked_sequenceに変換し、処理を終えた後にpad_packed_sequence関数によってtensorに戻す。
class Encoder(nn.Module): def __init__(self, input_size, hidden_size): """ :param input_size: int, 入力言語の語彙数 :param hidden_size: int, 隠れ層のユニット数 """ super(Encoder, self).__init__() self.hidden_size = hidden_size self.embedding = nn.Embedding(input_size, hidden_size, padding_idx=PAD) self.gru = nn.GRU(hidden_size, hidden_size) def forward(self, seqs, input_lengths, hidden=None): """ :param seqs: tensor, 入力のバッチ, size=(max_length, batch_size) :param input_lengths: 入力のバッチの各サンプルの文長 :param hidden: tensor, 隠れ状態の初期値, Noneの場合は0で初期化される :return output: tensor, Encoderの出力, size=(max_length, batch_size, hidden_size) :return hidden: tensor, Encoderの隠れ状態, size=(1, batch_size, hidden_size) """ emb = self.embedding(seqs) # seqsはパディング済み packed = pack_padded_sequence(emb, input_lengths) # PackedSequenceオブジェクトに変換 output, hidden = self.gru(packed, hidden) output, _ = pad_packed_sequence(output) return output, hidden
つぎにDecoder側を構築する。
今回はDecoder側でパディングを行わないので、通常のtensorのままRNNに入力する。
class Decoder(nn.Module): def __init__(self, hidden_size, output_size): """ :param hidden_size: int, 隠れ層のユニット数 :param output_size: int, 出力言語の語彙数 :param dropout: float, ドロップアウト率 """ super(Decoder, self).__init__() self.hidden_size = hidden_size self.output_size = output_size self.embedding = nn.Embedding(output_size, hidden_size, padding_idx=PAD) self.gru = nn.GRU(hidden_size, hidden_size) self.out = nn.Linear(hidden_size, output_size) def forward(self, seqs, hidden): """ :param seqs: tensor, 入力のバッチ, size=(1, batch_size) :param hidden: tensor, 隠れ状態の初期値, Noneの場合は0で初期化される :return output: tensor, Decoderの出力, size=(1, batch_size, output_size) :return hidden: tensor, Decoderの隠れ状態, size=(1, batch_size, hidden_size) """ emb = self.embedding(seqs) output, hidden = self.gru(emb, hidden) output = self.out(output) return output, hidden
次に、一連の処理をまとめるEncoderDecoderのクラスを定義する。
Scheduled Samplingを採用し、一定の確率でターゲット系列を入力とするかどうかを切り替えられるようにクラスを定義しておきます。
class EncoderDecoder(nn.Module): """EncoderとDecoderの処理をまとめる""" def __init__(self, input_size, output_size, hidden_size): """ :param input_size: int, 入力言語の語彙数 :param output_size: int, 出力言語の語彙数 :param hidden_size: int, 隠れ層のユニット数 """ super(EncoderDecoder, self).__init__() self.encoder = Encoder(input_size, hidden_size) self.decoder = Decoder(hidden_size, output_size) def forward(self, batch_X, lengths_X, max_length, batch_Y=None, use_teacher_forcing=False): """ :param batch_X: tensor, 入力系列のバッチ, size=(max_length, batch_size) :param lengths_X: list, 入力系列のバッチ内の各サンプルの文長 :param max_length: int, Decoderの最大文長 :param batch_Y: tensor, Decoderで用いるターゲット系列 :param use_teacher_forcing: Decoderでターゲット系列を入力とするフラグ :return decoder_outputs: tensor, Decoderの出力, size=(max_length, batch_size, self.decoder.output_size) """ # encoderに系列を入力(複数時刻をまとめて処理) _, encoder_hidden = self.encoder(batch_X, lengths_X) _batch_size = batch_X.size(1) # decoderの入力と隠れ層の初期状態を定義 decoder_input = torch.tensor([BOS] * _batch_size, dtype=torch.long, device=device) # 最初の入力にはBOSを使用する decoder_input = decoder_input.unsqueeze(0) # (1, batch_size) decoder_hidden = encoder_hidden # Encoderの最終隠れ状態を取得 # decoderの出力のホルダーを定義 decoder_outputs = torch.zeros(max_length, _batch_size, self.decoder.output_size, device=device) # max_length分の固定長 # 各時刻ごとに処理 for t in range(max_length): decoder_output, decoder_hidden = self.decoder(decoder_input, decoder_hidden) decoder_outputs[t] = decoder_output # 次の時刻のdecoderの入力を決定 if use_teacher_forcing and batch_Y is not None: # teacher forceの場合、ターゲット系列を用いる decoder_input = batch_Y[t].unsqueeze(0) else: # teacher forceでない場合、自身の出力を用いる decoder_input = decoder_output.max(-1)[1] return decoder_outputs]
次に訓練を行う。
基本的にはクロスエントロピーを損失関数として扱うが、パディングを行うと
mce = nn.CrossEntropyLoss(size_average=False, ignore_index=PAD) # PADを無視する def masked_cross_entropy(logits, target): logits_flat = logits.view(-1, logits.size(-1)) # (max_seq_len * batch_size, output_size) target_flat = target.view(-1) # (max_seq_len * batch_size, 1) return mce(logits_flat, target_flat)
学習を行う
# ハイパーパラメータの設定 num_epochs = 10 batch_size = 64 lr = 1e-3 # 学習率 teacher_forcing_rate = 0.2 # Teacher Forcingを行う確率 ckpt_path = 'model.pth' # 学習済みのモデルを保存するパス model_args = { 'input_size': vocab_size_X, 'output_size': vocab_size_Y, 'hidden_size': 256, } # データローダを定義 train_dataloader = DataLoader(train_X, train_Y, batch_size=batch_size, shuffle=True) valid_dataloader = DataLoader(valid_X, valid_Y, batch_size=batch_size, shuffle=False) # モデルとOptimizerを定義 model = EncoderDecoder(**model_args).to(device) optimizer = optim.Adam(model.parameters(), lr=lr)
実際に損失関数を計算する関数を定義する。
def compute_loss(batch_X, batch_Y, lengths_X, model, optimizer=None, is_train=True): # 損失を計算する関数 model.train(is_train) # train/evalモードの切替え # 一定確率でTeacher Forcingを行う use_teacher_forcing = is_train and (random.random() < teacher_forcing_rate) max_length = batch_Y.size(0) # 推論 pred_Y = model(batch_X, lengths_X, max_length, batch_Y, use_teacher_forcing) # 損失関数を計算 loss = masked_cross_entropy(pred_Y.contiguous(), batch_Y.contiguous()) if is_train: # 訓練時はパラメータを更新 optimizer.zero_grad() loss.backward() optimizer.step() batch_Y = batch_Y.transpose(0, 1).contiguous().data.cpu().tolist() pred = pred_Y.max(dim=-1)[1].data.cpu().numpy().T.tolist() return loss.item(), batch_Y, pred
LOSS以外に自然言語のモデルの性能を評価する指標で、BLEUというものがある。
BLEUは機械翻訳の分野において最も一般的な自動評価基準の一つで、予め用意した複数の参照訳と、機械翻訳モデルが出力した訳のn-gramのマッチ率に基づく指標である。
NLTK (Natural Language Tool Kit) という自然言語処理で用いられるライブラリを用いて簡単に計算することができる。
def calc_bleu(refs, hyps): """ BLEUスコアを計算する関数 :param refs: list, 参照訳。単語のリストのリスト (例: [['I', 'have', 'a', 'pen'], ...]) :param hyps: list, モデルの生成した訳。単語のリストのリスト (例: ['I', 'have', 'a', 'pen']) :return: float, BLEUスコア(0~100) """ refs = [[ref[:ref.index(EOS)]] for ref in refs] # EOSは評価しないで良いので切り捨てる, refsのほうは複数なのでlistが一個多くかかっている hyps = [hyp[:hyp.index(EOS)] if EOS in hyp else hyp for hyp in hyps] return 100 * bleu_score.corpus_bleu(refs, hyps)
モデルの訓練を行う。
# 訓練 best_valid_bleu = 0. for epoch in range(1, num_epochs+1): train_loss = 0. train_refs = [] train_hyps = [] valid_loss = 0. valid_refs = [] valid_hyps = [] # train for batch in train_dataloader: batch_X, batch_Y, lengths_X = batch loss, gold, pred = compute_loss( batch_X, batch_Y, lengths_X, model, optimizer, is_train=True ) train_loss += loss train_refs += gold train_hyps += pred # valid for batch in valid_dataloader: batch_X, batch_Y, lengths_X = batch loss, gold, pred = compute_loss( batch_X, batch_Y, lengths_X, model, is_train=False ) valid_loss += loss valid_refs += gold valid_hyps += pred # 損失をサンプル数で割って正規化 train_loss = np.sum(train_loss) / len(train_dataloader.data) valid_loss = np.sum(valid_loss) / len(valid_dataloader.data) # BLEUを計算 train_bleu = calc_bleu(train_refs, train_hyps) valid_bleu = calc_bleu(valid_refs, valid_hyps) # validationデータでBLEUが改善した場合にはモデルを保存 if valid_bleu > best_valid_bleu: ckpt = model.state_dict() torch.save(ckpt, ckpt_path) best_valid_bleu = valid_bleu print('Epoch {}: train_loss: {:5.2f} train_bleu: {:2.2f} valid_loss: {:5.2f} valid_bleu: {:2.2f}'.format( epoch, train_loss, train_bleu, valid_loss, valid_bleu)) print('-'*80)
学習結果は下記である。epochが進むにつれて、lossが下がり、bleuが上がる様子がわかる。
Epoch 1: train_loss: 52.31 train_bleu: 3.39 valid_loss: 48.69 valid_bleu: 4.53 -------------------------------------------------------------------------------- Epoch 2: train_loss: 44.39 train_bleu: 7.72 valid_loss: 44.60 valid_bleu: 8.25 -------------------------------------------------------------------------------- Epoch 3: train_loss: 40.10 train_bleu: 11.43 valid_loss: 42.04 valid_bleu: 10.51 -------------------------------------------------------------------------------- Epoch 4: train_loss: 37.07 train_bleu: 14.47 valid_loss: 40.62 valid_bleu: 11.69 -------------------------------------------------------------------------------- Epoch 5: train_loss: 35.06 train_bleu: 16.38 valid_loss: 39.91 valid_bleu: 13.65 -------------------------------------------------------------------------------- Epoch 6: train_loss: 33.08 train_bleu: 18.89 valid_loss: 39.76 valid_bleu: 15.42 -------------------------------------------------------------------------------- Epoch 7: train_loss: 31.52 train_bleu: 20.73 valid_loss: 40.81 valid_bleu: 18.62 -------------------------------------------------------------------------------- Epoch 8: train_loss: 30.28 train_bleu: 22.82 valid_loss: 39.79 valid_bleu: 16.66 -------------------------------------------------------------------------------- Epoch 9: train_loss: 28.73 train_bleu: 25.12 valid_loss: 40.29 valid_bleu: 18.01 -------------------------------------------------------------------------------- Epoch 10: train_loss: 28.03 train_bleu: 26.22 valid_loss: 40.33 valid_bleu: 16.73 -------------------------------------------------------------------------------- >|| 次に、実際に学習したモデルで実際に文章を生成してみる。 >|python| # 学習済みモデルの読み込み ckpt = torch.load(ckpt_path) # cpuで処理する場合はmap_locationで指定する必要があります。 model.load_state_dict(ckpt) model.eval() def ids_to_sentence(vocab, ids): # IDのリストを単語のリストに変換する return [vocab.id2word[_id] for _id in ids] def trim_eos(ids): # IDのリストからEOS以降の単語を除外する if EOS in ids: return ids[:ids.index(EOS)] else: return ids # テストデータの読み込み test_X = load_data('./data/dev.en') test_Y = load_data('./data/dev.ja') test_X = [sentence_to_ids(vocab_X, sentence) for sentence in test_X] test_Y = [sentence_to_ids(vocab_Y, sentence) for sentence in test_Y] test_dataloader = DataLoader(test_X, test_Y, batch_size=1, shuffle=False) # 生成 batch_X, batch_Y, lengths_X = next(test_dataloader) sentence_X = ' '.join(ids_to_sentence(vocab_X, batch_X.data.cpu().numpy()[:-1, 0])) sentence_Y = ' '.join(ids_to_sentence(vocab_Y, batch_Y.data.cpu().numpy()[:-1, 0])) print('src: {}'.format(sentence_X)) print('tgt: {}'.format(sentence_Y)) output = model(batch_X, lengths_X, max_length=20) output = output.max(dim=-1)[1].view(-1).data.cpu().tolist() output_sentence = ' '.join(ids_to_sentence(vocab_Y, trim_eos(output))) output_sentence_without_trim = ' '.join(ids_to_sentence(vocab_Y, output)) print('out: {}'.format(output_sentence)) print('without trim: {}'.format(output_sentence_without_trim))
結果は下記である。それなりにターゲットに近い文章が生成できている。
src: i can 't swim at all . tgt: 私 は 少し も 泳げ な い 。 out: 私 は 全然 泳げ な い 。 without trim: 私 は 全然 泳げ な い 。 </S> </S> </S> </S> </S> </S> </S> </S> </S> </S> </S> </S> </S>
Bleuを評価する。
# BLEUの計算 test_dataloader = DataLoader(test_X, test_Y, batch_size=1, shuffle=False) refs_list = [] hyp_list = [] for batch in test_dataloader: batch_X, batch_Y, lengths_X = batch pred_Y = model(batch_X, lengths_X, max_length=20) pred = pred_Y.max(dim=-1)[1].view(-1).data.cpu().tolist() refs = batch_Y.view(-1).data.cpu().tolist() refs_list.append(refs) hyp_list.append(pred) bleu = calc_bleu(refs_list, hyp_list) print(bleu)
17.71570480086109
- BeamSearch
テストデータにたいしてあらたな文を生成する際、これまでは各時刻で最も確率の高い単語を生成し、それを次のステップの入力として使っていた。しかし、本当にやりたいことは、文全体の尤度が最も高くなるような文章の生成である。そこで各単語だけでなく、もう少し大局的な評価をしていく必要がある。
BeamSearchでは、各時刻において一定の数Kのそれまでのスコアの高い文を保持しながら選択をおこなっていく。
Transformer
ニューラルネット機械翻訳の問題点
ニューラルネット機械翻訳の課題として文章が長くなると精度が落ちるという点がある。
下図は、Encoder-Decoderモデルと統計的機械翻訳モデルの文章の長さに対する精度を比較したものである。
統計的機械翻訳モデルは、文章が長くてもBleuスコアが落ちないのに対し、Encoder-Decoderモデルでは、文章が長くなった時に顕著にBleuスコアが低下することが分かる。
これは、翻訳元の内容を一つのベクトルで表現しているため、文長が長くなると表現力が足りなくなるからである。
Attention(注意機構)
上記課題を解決するものとして、Attentionが提案された。
これは、(翻訳タスクの場合)翻訳先の各単語を選択する際に、翻訳元の文中の隠れ状態に重みをつけて利用する機構である。言い換えると、翻訳の際に、翻訳元のどの単語に注意を当てて処理するかを決める機構である。
下記はAttentionの例である。
英語→フランス語の翻訳で、注意が当たっている語句が白くハイライトされている。対訳の語句が最も白くなっていることが分かるが、関連する語句(EuropianとEconomic等)も薄くハイライトされていることが分かる。
Attentionの仕組み
非常に簡単に言えば、Query(検索クエリ)に一致する
Keyを索引し、対応するValueを取り出す操作である。
つまり、辞書オブジェクトの機能と同じと言える。
Atenttionの性能
下図のように、文章が長くなっても翻訳精度は落ちない。
Transformer
Transformerは、"Attention is all you need"という論文で2017年6月に発表された機械学習翻訳のアルゴリズム。
RNNを使わず、Attentionのみで構成されている。
当時のSOTAをRNNよりはるかに少ない計算量で実現している。
英仏(3600万文)の学習を8GPUで3.5日で完了している。
Transformerの主要モジュール
Transformerの主要モジュールは、下記の通り、Positional Encoding、Dot Product Attention、Feed Foward、Maskingである。
Attentionの種類
AttentionにはSource Target AttentionとSelf attentionの2種類がある。
Source Target Attentionは、Queryは外部のターゲットであり、KeyとValueは自身の入力データになる。Self-Attentionの場合は、Query, Key, Valueすべてが自身の入力データである。
Self-Attention
TransformerのEncoderでは、self-attentionを6層積んで各単語をエンコードする。
Self-Attentionでは、各単語をエンコードして重みづけしたものをそれぞれの単語の内部状態として持つ。
例えとしては、ウィンドウが文章全体に及ぶ畳み込み演算を行っているイメージである。
Position-Wise Feed-forward Network
位置情報を保持したまま順伝搬させるブロック。
2層の全結合ニューラルネットワークを用い、線形変換→ReLu→線形変換の順番で処理をする。
Scaled dot product attention
全単語に関するattentionをまとめて計算する
*は、次元数である。
Multi Head Attention
重みパラメータの異なる8個のヘッドを使用する。
畳み込みで8つのカーネルフィルターを使用するイメージ
Decoder
Encoderと同じく6層のattentionで構成されている。
各層で2種類のattention機構を用いている。
self-attentionは直下の層の出力へ当てんっし恩をかけている。
未来の情報を見ないようにマスクしている。
Encoder-Decoder attentionは入力文の情報を収集する役割である。encoderの出力へアテンションをかけている。
Add&Norm
計算を効率よく行うために加えた層
- Add(Residual Connection): 入出力の差分を学習させる層。具体的には、出力に入力をそのまま加算させるだけである。学習・テストエラーの低減効果を狙っている。
- Norm(Layer Normalization): 各層においてバイアスを除く活性化関数への入力を平均0、分散1に正規化する。学習の高速化を狙っている。
Positional Encoding
RNNを用いないので、単語列の語順情報を追加する必要があるため、この層がある。
単語の位置情報を、下記のような三角関数でエンコードする。
これらをconcatすることで、位置のソフトな2進数表現を得る。
下記はPosition Encodingした単語のイメージで、右半分はcosによる生成、左半分はsinによる生成である。
Attentionの可視化
Attetionを可視化した図が下記である。
左の上の文章は、it was too tiredの「it」はAnimalを強く差している。
一方で「it was too wide」では、「it」はstreetを強く差しており、attentionが文意を踏まえて注意を向けていることが分かる。
実装演習
Transformerの実装の演習をする。
transformerはRNNやCNNを使用せず、Attentionのみを用いるモデルである。
翻訳に限らない自然言語処理のあらゆるタスクで圧倒的な性能を示すことが知られている。
まず、必要なライブラリと環境を準備する。
! wget https://www.dropbox.com/s/9narw5x4uizmehh/utils.py ! mkdir images data # data取得 ! wget https://www.dropbox.com/s/o4kyc52a8we25wy/dev.en -P data/ ! wget https://www.dropbox.com/s/kdgskm5hzg6znuc/dev.ja -P data/ ! wget https://www.dropbox.com/s/gyyx4gohv9v65uh/test.en -P data/ ! wget https://www.dropbox.com/s/hotxwbgoe2n013k/test.ja -P data/ ! wget https://www.dropbox.com/s/5lsftkmb20ay9e1/train.en -P data/ ! wget https://www.dropbox.com/s/ak53qirssci6f1j/train.ja -P data/ import time import numpy as np from sklearn.utils import shuffle from sklearn.model_selection import train_test_split import matplotlib import matplotlib.pyplot as plt import seaborn as sns %matplotlib inline from nltk import bleu_score import torch import torch.nn as nn import torch.optim as optim import torch.nn.functional as F from utils import Vocab device = torch.device("cuda" if torch.cuda.is_available() else "cpu") torch.manual_seed(1) random_state = 42
PAD = 0 UNK = 1 BOS = 2 EOS = 3 PAD_TOKEN = '<PAD>' UNK_TOKEN = '<UNK>' BOS_TOKEN = '<S>' EOS_TOKEN = '</S>'
データセットを準備し、データローダーの定義などをする。
def load_data(file_path): """ テキストファイルからデータを読み込む :param file_path: str, テキストファイルのパス :return data: list, 文章(単語のリスト)のリスト """ data = [] for line in open(file_path, encoding='utf-8'): words = line.strip().split() # スペースで単語を分割 data.append(words) return data train_X = load_data('./data/train.en') train_Y = load_data('./data/train.ja') # 訓練データと検証データに分割 train_X, valid_X, train_Y, valid_Y = train_test_split(train_X, train_Y, test_size=0.2, random_state=random_state) MIN_COUNT = 2 # 語彙に含める単語の最低出現回数 word2id = { PAD_TOKEN: PAD, BOS_TOKEN: BOS, EOS_TOKEN: EOS, UNK_TOKEN: UNK, } vocab_X = Vocab(word2id=word2id) vocab_Y = Vocab(word2id=word2id) vocab_X.build_vocab(train_X, min_count=MIN_COUNT) vocab_Y.build_vocab(train_Y, min_count=MIN_COUNT) vocab_size_X = len(vocab_X.id2word) vocab_size_Y = len(vocab_Y.id2word) def sentence_to_ids(vocab, sentence): """ 単語のリストをインデックスのリストに変換する :param vocab: Vocabのインスタンス :param sentence: list of str :return indices: list of int """ ids = [vocab.word2id.get(word, UNK) for word in sentence] ids = [BOS] + ids + [EOS] # EOSを末尾に加える return ids train_X = [sentence_to_ids(vocab_X, sentence) for sentence in train_X] train_Y = [sentence_to_ids(vocab_Y, sentence) for sentence in train_Y] valid_X = [sentence_to_ids(vocab_X, sentence) for sentence in valid_X] valid_Y = [sentence_to_ids(vocab_Y, sentence) for sentence in valid_Y] class DataLoader(object): def __init__(self, src_insts, tgt_insts, batch_size, shuffle=True): """ :param src_insts: list, 入力言語の文章(単語IDのリスト)のリスト :param tgt_insts: list, 出力言語の文章(単語IDのリスト)のリスト :param batch_size: int, バッチサイズ :param shuffle: bool, サンプルの順番をシャッフルするか否か """ self.data = list(zip(src_insts, tgt_insts)) self.batch_size = batch_size self.shuffle = shuffle self.start_index = 0 self.reset() def reset(self): if self.shuffle: self.data = shuffle(self.data, random_state=random_state) self.start_index = 0 def __iter__(self): return self def __next__(self): def preprocess_seqs(seqs): # パディング max_length = max([len(s) for s in seqs]) data = [s + [PAD] * (max_length - len(s)) for s in seqs] # 単語の位置を表現するベクトルを作成 positions = [[pos+1 if w != PAD else 0 for pos, w in enumerate(seq)] for seq in data] # テンソルに変換 data_tensor = torch.tensor(data, dtype=torch.long, device=device) position_tensor = torch.tensor(positions, dtype=torch.long, device=device) return data_tensor, position_tensor # ポインタが最後まで到達したら初期化する if self.start_index >= len(self.data): self.reset() raise StopIteration() # バッチを取得して前処理 src_seqs, tgt_seqs = zip(*self.data[self.start_index:self.start_index+self.batch_size]) src_data, src_pos = preprocess_seqs(src_seqs) tgt_data, tgt_pos = preprocess_seqs(tgt_seqs) # ポインタを更新する self.start_index += self.batch_size return (src_data, src_pos), (tgt_data, tgt_pos)
- 各モジュールの定義
1.Position Encoding
入力系列の埋め込み行列に単語の位置情報を加算する。
def position_encoding_init(n_position, d_pos_vec): """ Positional Encodingのための行列の初期化を行う :param n_position: int, 系列長 :param d_pos_vec: int, 隠れ層の次元数 :return torch.tensor, size=(n_position, d_pos_vec) """ # PADがある単語の位置はpos=0にしておき、position_encも0にする position_enc = np.array([ [pos / np.power(10000, 2 * (j // 2) / d_pos_vec) for j in range(d_pos_vec)] if pos != 0 else np.zeros(d_pos_vec) for pos in range(n_position)]) position_enc[1:, 0::2] = np.sin(position_enc[1:, 0::2]) # dim 2i position_enc[1:, 1::2] = np.cos(position_enc[1:, 1::2]) # dim 2i+1 return torch.tensor(position_enc, dtype=torch.float)
Position Encodingを可視化すると次のようになる。
縦軸が単語の位置を、横軸が成分の次元を表しており、濃淡が加算される値である。
ここでは最大系列長を50、隠れ層の次元数を256とした。
pe = position_encoding_init(50, 256).numpy() plt.figure(figsize=(16,8)) sns.heatmap(pe, cmap='Blues') plt.show()
2.Attention
Scaled Dot-Product Attention
Attentionには、注意の重みを隠れ層一つのフィードフォワードネットワークで求めるAdditive Attentionと、注意の重みを内積で求めるDot-prudoct Attentionが存在する。
一般にDot-Product Attentionのほうがパラメータが少なくて高速であり、Transformerではこちらを使う。
class ScaledDotProductAttention(nn.Module): def __init__(self, d_model, attn_dropout=0.1): """ :param d_model: int, 隠れ層の次元数 :param attn_dropout: float, ドロップアウト率 """ super(ScaledDotProductAttention, self).__init__() self.temper = np.power(d_model, 0.5) # スケーリング因子 self.dropout = nn.Dropout(attn_dropout) self.softmax = nn.Softmax(dim=-1) def forward(self, q, k, v, attn_mask): """ :param q: torch.tensor, queryベクトル, size=(n_head*batch_size, len_q, d_model/n_head) :param k: torch.tensor, key, size=(n_head*batch_size, len_k, d_model/n_head) :param v: torch.tensor, valueベクトル, size=(n_head*batch_size, len_v, d_model/n_head) :param attn_mask: torch.tensor, Attentionに適用するマスク, size=(n_head*batch_size, len_q, len_k) :return output: 出力ベクトル, size=(n_head*batch_size, len_q, d_model/n_head) :return attn: Attention size=(n_head*batch_size, len_q, len_k) """ # QとKの内積でAttentionの重みを求め、スケーリングする attn = torch.bmm(q, k.transpose(1, 2)) / self.temper # (n_head*batch_size, len_q, len_k) # Attentionをかけたくない部分がある場合は、その部分を負の無限大に飛ばしてSoftmaxの値が0になるようにする attn.data.masked_fill_(attn_mask, -float('inf')) attn = self.softmax(attn) attn = self.dropout(attn) output = torch.bmm(attn, v) return output, attn
Multi-Head Attention
TransformerではAttentionを複数のヘッドで並列に扱うMulti-Head Attentionを採用している。複数のヘッドでAttentionを行うことにより、各ヘッドが異なる部分空間を処理でき、精度が向上する。
class MultiHeadAttention(nn.Module): def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1): """ :param n_head: int, ヘッド数 :param d_model: int, 隠れ層の次元数 :param d_k: int, keyベクトルの次元数 :param d_v: int, valueベクトルの次元数 :param dropout: float, ドロップアウト率 """ super(MultiHeadAttention, self).__init__() self.n_head = n_head self.d_k = d_k self.d_v = d_v # 各ヘッドごとに異なる重みで線形変換を行うための重み # nn.Parameterを使うことで、Moduleのパラメータとして登録できる. TFでは更新が必要な変数はtf.Variableでラップするのでわかりやすい self.w_qs = nn.Parameter(torch.empty([n_head, d_model, d_k], dtype=torch.float)) self.w_ks = nn.Parameter(torch.empty([n_head, d_model, d_k], dtype=torch.float)) self.w_vs = nn.Parameter(torch.empty([n_head, d_model, d_v], dtype=torch.float)) # nn.init.xavier_normal_で重みの値を初期化 nn.init.xavier_normal_(self.w_qs) nn.init.xavier_normal_(self.w_ks) nn.init.xavier_normal_(self.w_vs) self.attention = ScaledDotProductAttention(d_model) self.layer_norm = nn.LayerNorm(d_model) # 各層においてバイアスを除く活性化関数への入力を平均0、分散1に正則化 self.proj = nn.Linear(n_head*d_v, d_model) # 複数ヘッド分のAttentionの結果を元のサイズに写像するための線形層 # nn.init.xavier_normal_で重みの値を初期化 nn.init.xavier_normal_(self.proj.weight) self.dropout = nn.Dropout(dropout) def forward(self, q, k, v, attn_mask=None): """ :param q: torch.tensor, queryベクトル, size=(batch_size, len_q, d_model) :param k: torch.tensor, key, size=(batch_size, len_k, d_model) :param v: torch.tensor, valueベクトル, size=(batch_size, len_v, d_model) :param attn_mask: torch.tensor, Attentionに適用するマスク, size=(batch_size, len_q, len_k) :return outputs: 出力ベクトル, size=(batch_size, len_q, d_model) :return attns: Attention size=(n_head*batch_size, len_q, len_k) """ d_k, d_v = self.d_k, self.d_v n_head = self.n_head # residual connectionのための入力 出力に入力をそのまま加算する residual = q batch_size, len_q, d_model = q.size() batch_size, len_k, d_model = k.size() batch_size, len_v, d_model = v.size() # 複数ヘッド化 # torch.repeat または .repeatで指定したdimに沿って同じテンソルを作成 q_s = q.repeat(n_head, 1, 1) # (n_head*batch_size, len_q, d_model) k_s = k.repeat(n_head, 1, 1) # (n_head*batch_size, len_k, d_model) v_s = v.repeat(n_head, 1, 1) # (n_head*batch_size, len_v, d_model) # ヘッドごとに並列計算させるために、n_headをdim=0に、batch_sizeをdim=1に寄せる q_s = q_s.view(n_head, -1, d_model) # (n_head, batch_size*len_q, d_model) k_s = k_s.view(n_head, -1, d_model) # (n_head, batch_size*len_k, d_model) v_s = v_s.view(n_head, -1, d_model) # (n_head, batch_size*len_v, d_model) # 各ヘッドで線形変換を並列計算(p16左側`Linear`) q_s = torch.bmm(q_s, self.w_qs) # (n_head, batch_size*len_q, d_k) k_s = torch.bmm(k_s, self.w_ks) # (n_head, batch_size*len_k, d_k) v_s = torch.bmm(v_s, self.w_vs) # (n_head, batch_size*len_v, d_v) # Attentionは各バッチ各ヘッドごとに計算させるためにbatch_sizeをdim=0に寄せる q_s = q_s.view(-1, len_q, d_k) # (n_head*batch_size, len_q, d_k) k_s = k_s.view(-1, len_k, d_k) # (n_head*batch_size, len_k, d_k) v_s = v_s.view(-1, len_v, d_v) # (n_head*batch_size, len_v, d_v) # Attentionを計算(p16.左側`Scaled Dot-Product Attention * h`) outputs, attns = self.attention(q_s, k_s, v_s, attn_mask=attn_mask.repeat(n_head, 1, 1)) # 各ヘッドの結果を連結(p16左側`Concat`) # torch.splitでbatch_sizeごとのn_head個のテンソルに分割 outputs = torch.split(outputs, batch_size, dim=0) # (batch_size, len_q, d_model) * n_head # dim=-1で連結 outputs = torch.cat(outputs, dim=-1) # (batch_size, len_q, d_model*n_head) # residual connectionのために元の大きさに写像(p16左側`Linear`) outputs = self.proj(outputs) # (batch_size, len_q, d_model) outputs = self.dropout(outputs) outputs = self.layer_norm(outputs + residual) return outputs, attns
3.Position-Wise Feed Foward Network
単語列の位置ごとに独立して処理する2層のネットワークであるPosition-Wise Feed Forward Networkを定義する。
class PositionwiseFeedForward(nn.Module): """ :param d_hid: int, 隠れ層1層目の次元数 :param d_inner_hid: int, 隠れ層2層目の次元数 :param dropout: float, ドロップアウト率 """ def __init__(self, d_hid, d_inner_hid, dropout=0.1): super(PositionwiseFeedForward, self).__init__() # window size 1のconv層を定義することでPosition wiseな全結合層を実現する. self.w_1 = nn.Conv1d(d_hid, d_inner_hid, 1) self.w_2 = nn.Conv1d(d_inner_hid, d_hid, 1) self.layer_norm = nn.LayerNorm(d_hid) self.dropout = nn.Dropout(dropout) self.relu = nn.ReLU() def forward(self, x): """ :param x: torch.tensor, size=(batch_size, max_length, d_hid) :return: torch.tensor, size=(batch_size, max_length, d_hid) """ residual = x output = self.relu(self.w_1(x.transpose(1, 2))) output = self.w_2(output).transpose(2, 1) output = self.dropout(output) return self.layer_norm(output + residual)
4.Masking
TransformerではAttentionに対して2つのマスクを定義する。
一つはkey側の系列のPADトークンに対してAttentionを行わないようにするマスク。
def get_attn_padding_mask(seq_q, seq_k): """ keyのPADに対するattentionを0にするためのマスクを作成する :param seq_q: tensor, queryの系列, size=(batch_size, len_q) :param seq_k: tensor, keyの系列, size=(batch_size, len_k) :return pad_attn_mask: tensor, size=(batch_size, len_q, len_k) """ batch_size, len_q = seq_q.size() batch_size, len_k = seq_k.size() pad_attn_mask = seq_k.data.eq(PAD).unsqueeze(1) # (N, 1, len_k) PAD以外のidを全て0にする pad_attn_mask = pad_attn_mask.expand(batch_size, len_q, len_k) # (N, len_q, len_k) return pad_attn_mask
もう一つはDecoder側でSelf Attensionを行う際に、各時刻で未来の情報に対するAttentionを行わないようにするマスクである。
def get_attn_subsequent_mask(seq): """ 未来の情報に対するattentionを0にするためのマスクを作成する :param seq: tensor, size=(batch_size, length) :return subsequent_mask: tensor, size=(batch_size, length, length) """ attn_shape = (seq.size(1), seq.size(1)) # 上三角行列(diagonal=1: 対角線より上が1で下が0) subsequent_mask = torch.triu(torch.ones(attn_shape, dtype=torch.uint8, device=device), diagonal=1) subsequent_mask = subsequent_mask.repeat(seq.size(0), 1, 1) return subsequent_mask
- モデルの定義
Encoder
Encoderは、Self AttentionとPosition-Wise Forward Networkからなるブロックを拭く早々繰り返すので、ブロックのクラスEnoderLayerを定義した後にEncoderを定義
class EncoderLayer(nn.Module): """Encoderのブロックのクラス""" def __init__(self, d_model, d_inner_hid, n_head, d_k, d_v, dropout=0.1): """ :param d_model: int, 隠れ層の次元数 :param d_inner_hid: int, Position Wise Feed Forward Networkの隠れ層2層目の次元数 :param n_head: int, ヘッド数 :param d_k: int, keyベクトルの次元数 :param d_v: int, valueベクトルの次元数 :param dropout: float, ドロップアウト率 """ super(EncoderLayer, self).__init__() # Encoder内のSelf-Attention self.slf_attn = MultiHeadAttention( n_head, d_model, d_k, d_v, dropout=dropout) # Postionwise FFN self.pos_ffn = PositionwiseFeedForward(d_model, d_inner_hid, dropout=dropout) def forward(self, enc_input, slf_attn_mask=None): """ :param enc_input: tensor, Encoderの入力, size=(batch_size, max_length, d_model) :param slf_attn_mask: tensor, Self Attentionの行列にかけるマスク, size=(batch_size, len_q, len_k) :return enc_output: tensor, Encoderの出力, size=(batch_size, max_length, d_model) :return enc_slf_attn: tensor, EncoderのSelf Attentionの行列, size=(n_head*batch_size, len_q, len_k) """ # Self-Attentionのquery, key, valueにはすべてEncoderの入力(enc_input)が入る enc_output, enc_slf_attn = self.slf_attn( enc_input, enc_input, enc_input, attn_mask=slf_attn_mask) enc_output = self.pos_ffn(enc_output) return enc_output, enc_slf_attn class Encoder(nn.Module): """EncoderLayerブロックからなるEncoderのクラス""" def __init__( self, n_src_vocab, max_length, n_layers=6, n_head=8, d_k=64, d_v=64, d_word_vec=512, d_model=512, d_inner_hid=1024, dropout=0.1): """ :param n_src_vocab: int, 入力言語の語彙数 :param max_length: int, 最大系列長 :param n_layers: int, レイヤー数 :param n_head: int, ヘッド数 :param d_k: int, keyベクトルの次元数 :param d_v: int, valueベクトルの次元数 :param d_word_vec: int, 単語の埋め込みの次元数 :param d_model: int, 隠れ層の次元数 :param d_inner_hid: int, Position Wise Feed Forward Networkの隠れ層2層目の次元数 :param dropout: float, ドロップアウト率 """ super(Encoder, self).__init__() n_position = max_length + 1 self.max_length = max_length self.d_model = d_model # Positional Encodingを用いたEmbedding self.position_enc = nn.Embedding(n_position, d_word_vec, padding_idx=PAD) self.position_enc.weight.data = position_encoding_init(n_position, d_word_vec) # 一般的なEmbedding self.src_word_emb = nn.Embedding(n_src_vocab, d_word_vec, padding_idx=PAD) # EncoderLayerをn_layers個積み重ねる self.layer_stack = nn.ModuleList([ EncoderLayer(d_model, d_inner_hid, n_head, d_k, d_v, dropout=dropout) for _ in range(n_layers)]) def forward(self, src_seq, src_pos): """ :param src_seq: tensor, 入力系列, size=(batch_size, max_length) :param src_pos: tensor, 入力系列の各単語の位置情報, size=(batch_size, max_length) :return enc_output: tensor, Encoderの最終出力, size=(batch_size, max_length, d_model) :return enc_slf_attns: list, EncoderのSelf Attentionの行列のリスト """ # 一般的な単語のEmbeddingを行う enc_input = self.src_word_emb(src_seq) # Positional EncodingのEmbeddingを加算する enc_input += self.position_enc(src_pos) enc_slf_attns = [] enc_output = enc_input # key(=enc_input)のPADに対応する部分のみ1のマスクを作成 enc_slf_attn_mask = get_attn_padding_mask(src_seq, src_seq) # n_layers個のEncoderLayerに入力を通す for enc_layer in self.layer_stack: enc_output, enc_slf_attn = enc_layer( enc_output, slf_attn_mask=enc_slf_attn_mask) enc_slf_attns += [enc_slf_attn] return enc_output, enc_slf_attns
- Decoder
Self Attention, source-target Atetntion, Position-Wise Feed Foward Networkからなるブロックを複数層繰り返すので、ブロックのクラスDecoderLayerを定義した後に、Decoderを定義する。
class DecoderLayer(nn.Module): """Decoderのブロックのクラス""" def __init__(self, d_model, d_inner_hid, n_head, d_k, d_v, dropout=0.1): """ :param d_model: int, 隠れ層の次元数 :param d_inner_hid: int, Position Wise Feed Forward Networkの隠れ層2層目の次元数 :param n_head: int, ヘッド数 :param d_k: int, keyベクトルの次元数 :param d_v: int, valueベクトルの次元数 :param dropout: float, ドロップアウト率 """ super(DecoderLayer, self).__init__() # Decoder内のSelf-Attention self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout) # Encoder-Decoder間のSource-Target Attention self.enc_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout) # Positionwise FFN self.pos_ffn = PositionwiseFeedForward(d_model, d_inner_hid, dropout=dropout) def forward(self, dec_input, enc_output, slf_attn_mask=None, dec_enc_attn_mask=None): """ :param dec_input: tensor, Decoderの入力, size=(batch_size, max_length, d_model) :param enc_output: tensor, Encoderの出力, size=(batch_size, max_length, d_model) :param slf_attn_mask: tensor, Self Attentionの行列にかけるマスク, size=(batch_size, len_q, len_k) :param dec_enc_attn_mask: tensor, Soutce-Target Attentionの行列にかけるマスク, size=(batch_size, len_q, len_k) :return dec_output: tensor, Decoderの出力, size=(batch_size, max_length, d_model) :return dec_slf_attn: tensor, DecoderのSelf Attentionの行列, size=(n_head*batch_size, len_q, len_k) :return dec_enc_attn: tensor, DecoderのSoutce-Target Attentionの行列, size=(n_head*batch_size, len_q, len_k) """ # Self-Attentionのquery, key, valueにはすべてDecoderの入力(dec_input)が入る dec_output, dec_slf_attn = self.slf_attn( dec_input, dec_input, dec_input, attn_mask=slf_attn_mask) # Source-Target-AttentionのqueryにはDecoderの出力(dec_output), key, valueにはEncoderの出力(enc_output)が入る dec_output, dec_enc_attn = self.enc_attn( dec_output, enc_output, enc_output, attn_mask=dec_enc_attn_mask) dec_output = self.pos_ffn(dec_output) return dec_output, dec_slf_attn, dec_enc_attn class Decoder(nn.Module): """DecoderLayerブロックからなるDecoderのクラス""" def __init__( self, n_tgt_vocab, max_length, n_layers=6, n_head=8, d_k=64, d_v=64, d_word_vec=512, d_model=512, d_inner_hid=1024, dropout=0.1): """ :param n_tgt_vocab: int, 出力言語の語彙数 :param max_length: int, 最大系列長 :param n_layers: int, レイヤー数 :param n_head: int, ヘッド数 :param d_k: int, keyベクトルの次元数 :param d_v: int, valueベクトルの次元数 :param d_word_vec: int, 単語の埋め込みの次元数 :param d_model: int, 隠れ層の次元数 :param d_inner_hid: int, Position Wise Feed Forward Networkの隠れ層2層目の次元数 :param dropout: float, ドロップアウト率 """ super(Decoder, self).__init__() n_position = max_length + 1 self.max_length = max_length self.d_model = d_model # Positional Encodingを用いたEmbedding self.position_enc = nn.Embedding( n_position, d_word_vec, padding_idx=PAD) self.position_enc.weight.data = position_encoding_init(n_position, d_word_vec) # 一般的なEmbedding self.tgt_word_emb = nn.Embedding( n_tgt_vocab, d_word_vec, padding_idx=PAD) self.dropout = nn.Dropout(dropout) # DecoderLayerをn_layers個積み重ねる self.layer_stack = nn.ModuleList([ DecoderLayer(d_model, d_inner_hid, n_head, d_k, d_v, dropout=dropout) for _ in range(n_layers)]) def forward(self, tgt_seq, tgt_pos, src_seq, enc_output): """ :param tgt_seq: tensor, 出力系列, size=(batch_size, max_length) :param tgt_pos: tensor, 出力系列の各単語の位置情報, size=(batch_size, max_length) :param src_seq: tensor, 入力系列, size=(batch_size, n_src_vocab) :param enc_output: tensor, Encoderの出力, size=(batch_size, max_length, d_model) :return dec_output: tensor, Decoderの最終出力, size=(batch_size, max_length, d_model) :return dec_slf_attns: list, DecoderのSelf Attentionの行列のリスト :return dec_slf_attns: list, DecoderのSelf Attentionの行列のリスト """ # 一般的な単語のEmbeddingを行う dec_input = self.tgt_word_emb(tgt_seq) # Positional EncodingのEmbeddingを加算する dec_input += self.position_enc(tgt_pos) # Self-Attention用のマスクを作成 # key(=dec_input)のPADに対応する部分が1のマスクと、queryから見たkeyの未来の情報に対応する部分が1のマスクのORをとる dec_slf_attn_pad_mask = get_attn_padding_mask(tgt_seq, tgt_seq) # (N, max_length, max_length) dec_slf_attn_sub_mask = get_attn_subsequent_mask(tgt_seq) # (N, max_length, max_length) dec_slf_attn_mask = torch.gt(dec_slf_attn_pad_mask + dec_slf_attn_sub_mask, 0) # ORをとる # key(=dec_input)のPADに対応する部分のみ1のマスクを作成 dec_enc_attn_pad_mask = get_attn_padding_mask(tgt_seq, src_seq) # (N, max_length, max_length) dec_slf_attns, dec_enc_attns = [], [] dec_output = dec_input # n_layers個のDecoderLayerに入力を通す for dec_layer in self.layer_stack: dec_output, dec_slf_attn, dec_enc_attn = dec_layer( dec_output, enc_output, slf_attn_mask=dec_slf_attn_mask, dec_enc_attn_mask=dec_enc_attn_pad_mask) dec_slf_attns += [dec_slf_attn] dec_enc_attns += [dec_enc_attn] return dec_output, dec_slf_attns, dec_enc_attns
Transofomer全体のモデルのクラスを定義する。
class Transformer(nn.Module): """Transformerのモデル全体のクラス""" def __init__( self, n_src_vocab, n_tgt_vocab, max_length, n_layers=6, n_head=8, d_word_vec=512, d_model=512, d_inner_hid=1024, d_k=64, d_v=64, dropout=0.1, proj_share_weight=True): """ :param n_src_vocab: int, 入力言語の語彙数 :param n_tgt_vocab: int, 出力言語の語彙数 :param max_length: int, 最大系列長 :param n_layers: int, レイヤー数 :param n_head: int, ヘッド数 :param d_k: int, keyベクトルの次元数 :param d_v: int, valueベクトルの次元数 :param d_word_vec: int, 単語の埋め込みの次元数 :param d_model: int, 隠れ層の次元数 :param d_inner_hid: int, Position Wise Feed Forward Networkの隠れ層2層目の次元数 :param dropout: float, ドロップアウト率 :param proj_share_weight: bool, 出力言語の単語のEmbeddingと出力の写像で重みを共有する """ super(Transformer, self).__init__() self.encoder = Encoder( n_src_vocab, max_length, n_layers=n_layers, n_head=n_head, d_word_vec=d_word_vec, d_model=d_model, d_inner_hid=d_inner_hid, dropout=dropout) self.decoder = Decoder( n_tgt_vocab, max_length, n_layers=n_layers, n_head=n_head, d_word_vec=d_word_vec, d_model=d_model, d_inner_hid=d_inner_hid, dropout=dropout) self.tgt_word_proj = nn.Linear(d_model, n_tgt_vocab, bias=False) nn.init.xavier_normal_(self.tgt_word_proj.weight) self.dropout = nn.Dropout(dropout) assert d_model == d_word_vec # 各モジュールの出力のサイズは揃える if proj_share_weight: # 出力言語の単語のEmbeddingと出力の写像で重みを共有する assert d_model == d_word_vec self.tgt_word_proj.weight = self.decoder.tgt_word_emb.weight def get_trainable_parameters(self): # Positional Encoding以外のパラメータを更新する enc_freezed_param_ids = set(map(id, self.encoder.position_enc.parameters())) dec_freezed_param_ids = set(map(id, self.decoder.position_enc.parameters())) freezed_param_ids = enc_freezed_param_ids | dec_freezed_param_ids return (p for p in self.parameters() if id(p) not in freezed_param_ids) def forward(self, src, tgt): src_seq, src_pos = src tgt_seq, tgt_pos = tgt src_seq = src_seq[:, 1:] src_pos = src_pos[:, 1:] tgt_seq = tgt_seq[:, :-1] tgt_pos = tgt_pos[:, :-1] enc_output, *_ = self.encoder(src_seq, src_pos) dec_output, *_ = self.decoder(tgt_seq, tgt_pos, src_seq, enc_output) seq_logit = self.tgt_word_proj(dec_output) return seq_logit
学習
def compute_loss(batch_X, batch_Y, model, criterion, optimizer=None, is_train=True): # バッチの損失を計算 model.train(is_train) pred_Y = model(batch_X, batch_Y) gold = batch_Y[0][:, 1:].contiguous() # gold = batch_Y[0].contiguous() loss = criterion(pred_Y.view(-1, pred_Y.size(2)), gold.view(-1)) if is_train: # 訓練時はパラメータを更新 optimizer.zero_grad() loss.backward() optimizer.step() gold = gold.data.cpu().numpy().tolist() pred = pred_Y.max(dim=-1)[1].data.cpu().numpy().tolist() return loss.item(), gold, pred MAX_LENGTH = 20 batch_size = 64 num_epochs = 15 lr = 0.001 ckpt_path = 'transformer.pth' max_length = MAX_LENGTH + 2 model_args = { 'n_src_vocab': vocab_size_X, 'n_tgt_vocab': vocab_size_Y, 'max_length': max_length, 'proj_share_weight': True, 'd_k': 32, 'd_v': 32, 'd_model': 128, 'd_word_vec': 128, 'd_inner_hid': 256, 'n_layers': 3, 'n_head': 6, 'dropout': 0.1, } # DataLoaderやモデルを定義 train_dataloader = DataLoader( train_X, train_Y, batch_size ) valid_dataloader = DataLoader( valid_X, valid_Y, batch_size, shuffle=False ) model = Transformer(**model_args).to(device) optimizer = optim.Adam(model.get_trainable_parameters(), lr=lr) criterion = nn.CrossEntropyLoss(ignore_index=PAD, size_average=False).to(device) def calc_bleu(refs, hyps): """ BLEUスコアを計算する関数 :param refs: list, 参照訳。単語のリストのリスト (例: [['I', 'have', 'a', 'pen'], ...]) :param hyps: list, モデルの生成した訳。単語のリストのリスト (例: [['I', 'have', 'a', 'pen'], ...]) :return: float, BLEUスコア(0~100) """ refs = [[ref[:ref.index(EOS)]] for ref in refs] hyps = [hyp[:hyp.index(EOS)] if EOS in hyp else hyp for hyp in hyps] return 100 * bleu_score.corpus_bleu(refs, hyps) # 訓練 best_valid_bleu = 0. for epoch in range(1, num_epochs+1): start = time.time() train_loss = 0. train_refs = [] train_hyps = [] valid_loss = 0. valid_refs = [] valid_hyps = [] # train for batch in train_dataloader: batch_X, batch_Y = batch loss, gold, pred = compute_loss( batch_X, batch_Y, model, criterion, optimizer, is_train=True ) train_loss += loss train_refs += gold train_hyps += pred # valid for batch in valid_dataloader: batch_X, batch_Y = batch loss, gold, pred = compute_loss( batch_X, batch_Y, model, criterion, is_train=False ) valid_loss += loss valid_refs += gold valid_hyps += pred # 損失をサンプル数で割って正規化 train_loss /= len(train_dataloader.data) valid_loss /= len(valid_dataloader.data) # BLEUを計算 train_bleu = calc_bleu(train_refs, train_hyps) valid_bleu = calc_bleu(valid_refs, valid_hyps) # validationデータでBLEUが改善した場合にはモデルを保存 if valid_bleu > best_valid_bleu: ckpt = model.state_dict() torch.save(ckpt, ckpt_path) best_valid_bleu = valid_bleu elapsed_time = (time.time()-start) / 60 print('Epoch {} [{:.1f}min]: train_loss: {:5.2f} train_bleu: {:2.2f} valid_loss: {:5.2f} valid_bleu: {:2.2f}'.format( epoch, elapsed_time, train_loss, train_bleu, valid_loss, valid_bleu)) print('-'*80)
実行結果
Epoch 1 [0.8min]: train_loss: 77.55 train_bleu: 4.72 valid_loss: 41.38 valid_bleu: 11.02 -------------------------------------------------------------------------------- Epoch 2 [0.8min]: train_loss: 39.45 train_bleu: 12.25 valid_loss: 32.30 valid_bleu: 17.59 -------------------------------------------------------------------------------- Epoch 3 [0.8min]: train_loss: 32.22 train_bleu: 17.89 valid_loss: 28.26 valid_bleu: 21.75 -------------------------------------------------------------------------------- Epoch 4 [0.8min]: train_loss: 28.39 train_bleu: 21.59 valid_loss: 25.91 valid_bleu: 24.61 -------------------------------------------------------------------------------- Epoch 5 [0.8min]: train_loss: 25.89 train_bleu: 24.35 valid_loss: 24.46 valid_bleu: 26.96 -------------------------------------------------------------------------------- Epoch 6 [0.8min]: train_loss: 24.04 train_bleu: 26.66 valid_loss: 23.14 valid_bleu: 28.96 -------------------------------------------------------------------------------- Epoch 7 [0.8min]: train_loss: 22.57 train_bleu: 28.52 valid_loss: 22.37 valid_bleu: 29.68 -------------------------------------------------------------------------------- Epoch 8 [0.8min]: train_loss: 21.40 train_bleu: 30.22 valid_loss: 21.59 valid_bleu: 31.05 -------------------------------------------------------------------------------- Epoch 9 [0.8min]: train_loss: 20.37 train_bleu: 31.49 valid_loss: 20.98 valid_bleu: 31.94 -------------------------------------------------------------------------------- Epoch 10 [0.8min]: train_loss: 19.48 train_bleu: 32.70 valid_loss: 20.47 valid_bleu: 33.20 -------------------------------------------------------------------------------- Epoch 11 [0.8min]: train_loss: 18.71 train_bleu: 33.77 valid_loss: 20.19 valid_bleu: 33.62 -------------------------------------------------------------------------------- Epoch 12 [0.8min]: train_loss: 17.98 train_bleu: 35.06 valid_loss: 19.86 valid_bleu: 33.94 -------------------------------------------------------------------------------- Epoch 13 [0.8min]: train_loss: 17.35 train_bleu: 35.94 valid_loss: 19.32 valid_bleu: 34.66 -------------------------------------------------------------------------------- Epoch 14 [0.8min]: train_loss: 16.75 train_bleu: 36.75 valid_loss: 19.23 valid_bleu: 35.18 -------------------------------------------------------------------------------- Epoch 15 [0.8min]: train_loss: 16.23 train_bleu: 37.60 valid_loss: 19.11 valid_bleu: 35.40
評価用コードの作成
def test(model, src, max_length=20): # 学習済みモデルで系列を生成する model.eval() src_seq, src_pos = src batch_size = src_seq.size(0) enc_output, enc_slf_attns = model.encoder(src_seq, src_pos) tgt_seq = torch.full([batch_size, 1], BOS, dtype=torch.long, device=device) tgt_pos = torch.arange(1, dtype=torch.long, device=device) tgt_pos = tgt_pos.unsqueeze(0).repeat(batch_size, 1) # 時刻ごとに処理 for t in range(1, max_length+1): dec_output, dec_slf_attns, dec_enc_attns = model.decoder( tgt_seq, tgt_pos, src_seq, enc_output) dec_output = model.tgt_word_proj(dec_output) out = dec_output[:, -1, :].max(dim=-1)[1].unsqueeze(1) # 自身の出力を次の時刻の入力にする tgt_seq = torch.cat([tgt_seq, out], dim=-1) tgt_pos = torch.arange(t+1, dtype=torch.long, device=device) tgt_pos = tgt_pos.unsqueeze(0).repeat(batch_size, 1) return tgt_seq[:, 1:], enc_slf_attns, dec_slf_attns, dec_enc_attns def ids_to_sentence(vocab, ids): # IDのリストを単語のリストに変換する return [vocab.id2word[_id] for _id in ids] def trim_eos(ids): # IDのリストからEOS以降の単語を除外する if EOS in ids: return ids[:ids.index(EOS)] else: return ids
学習済みモデルと、テストデータの読み込みを行う。
# 学習済みモデルの読み込み model = Transformer(**model_args).to(device) ckpt = torch.load(ckpt_path) model.load_state_dict(ckpt) # テストデータの読み込み test_X = load_data('./data/dev.en') test_Y = load_data('./data/dev.ja') test_X = [sentence_to_ids(vocab_X, sentence) for sentence in test_X] test_Y = [sentence_to_ids(vocab_Y, sentence) for sentence in test_Y]
文章を生成してみる。
test_dataloader = DataLoader( test_X, test_Y, 1, shuffle=False ) src, tgt = next(test_dataloader) src_ids = src[0][0].cpu().numpy() tgt_ids = tgt[0][0].cpu().numpy() print('src: {}'.format(' '.join(ids_to_sentence(vocab_X, src_ids[1:-1])))) print('tgt: {}'.format(' '.join(ids_to_sentence(vocab_Y, tgt_ids[1:-1])))) preds, enc_slf_attns, dec_slf_attns, dec_enc_attns = test(model, src) pred_ids = preds[0].data.cpu().numpy().tolist() print('out: {}'.format(' '.join(ids_to_sentence(vocab_Y, trim_eos(pred_ids)))))
結果
src: show your own business . tgt: 自分 の 事 を しろ 。 out: 君 の 商売 は 自分 の 商売 を 見せ て い る 。
Targetほどこなれてはいないものの、直訳としては大方あっている。
Bleuの評価
# BLEUの評価 test_dataloader = DataLoader( test_X, test_Y, 128, shuffle=False ) refs_list = [] hyp_list = [] for batch in test_dataloader: batch_X, batch_Y = batch preds, *_ = test(model, batch_X) preds = preds.data.cpu().numpy().tolist() refs = batch_Y[0].data.cpu().numpy()[:, 1:].tolist() refs_list += refs hyp_list += preds bleu = calc_bleu(refs_list, hyp_list) print(bleu)
24.974396035182135
計算負荷はRNNより低いにも関わらず、良好なBleu値が出た。
ラビットチャレンジレポート 深層学習Day4 その2
Section3 軽量化・高速化技術
- 軽量化・高速化が必要な理由
深層学習は、計算量が多く、計算機の負担が大きいネットワークである。
しかも、年10倍程度の速度で、処理データが増加し、モデルが複雑になっている。
対して、コンピュータの処理速度の向上は、ほぼムーアの法則(18か月で約2倍の集積度)に律速されている。
つまり、計算機の負荷は、年々高くなる方向であり、軽量化・高速化技術は必須となっている。
中でも、複数の計算機(ワーカー)を使用し、並列的にニューラルネットワークを計算することで効率の良い学習を行う分散深層学習は、今日の深層学習では必須の技術となっている。
3-1 データ並列化
意やモデルを各ワーカーに子モデルとしてコピーし、データを文化として各ワーカーに計算させる手法である。
データ並列化技術は、各モデルのパラメータの合わせ方で、同期型と非同期型がある。
同期型
1.各ワーカーの計算が終わるのを待ち、各ワーカーの勾配が出たところで勾配の平均を計算し、親モデルのパラメータを更新する。
2.パラメータ更新後の親モデルを子モデルとしてコピーする
3.1,2を繰り返すことで学習する
非同期型
各ワーカーはお互いの計算を待たず、各子モデルごとに更新を行う。
学習が終わった子モデルは、パラメータサーバーにPushされる。
学習が終わったワーカーが新たに学習を始めるときは、パラメータからPOPしたモデルに対して学習を始める。
3-2 モデル並列化
親モデルを各ワーカーに分割し、それぞれのモデルを学習させる手法。すべてのデータで学習が終わった後で、一つのモデルに復元する。
モデルが大きい時には、モデル並列化、データが大きい時には、データ並列化をするとよい。
データ並列化では別々のコンピュータを使用することが多いが、モデル並列化は、ワーカー同士のより高速な通信が必要なので、同一コンピュータ内で、GPUコアを分けて並列化することが多い。
モデル並列の効果
モデルのパラメータが多いほど、スピードアップの効率も向上する。
3-3GPUによる高速化
- CPU
高速化なコアが少数。複雑な処理が得意
比較的低性能なコアが多数あり、簡単な並列処理が得意。
ニューラルネットワークの計算が非常に得意。
もともとの目的のグラフィック以外の用途で使用されるGPUの総称をGPGPUという。
3-3量子化(Quantization)
計算精度を、64bit(倍精度)、32bit(単精度)、8bit(半精度)などに調整することを量子化という。
ネットワークが大きくなると、大量のパラメータが必要になり、学習や推論に多くのメモリと演算処理が必要になる。
通常のパラメータの64bit浮動小数点を32bitなど下位の精度に落とすことで、メモリと演算処理の削減を行える。
量子化の利点としては、計算の高速化と小メモリ化、欠点としては精度の低下が挙げられる。
計算の高速化
倍精度と単精度演算では演算性能が大きく違うため、量子化で精度を落とすことにより多くの計算をすることが出来る。下記のTeslaでは、計算速度が約2倍違う。
単精度では、TeslaV100では107TFLOPSに達し、大きな高速化が図れる。
省メモリ化
bit数を下げると、計算で確保するbit数がそれに応じて小さくて済むため、使用するメモリ量が少なくなる。
3-4蒸留(Distillation)
蒸留は学習済みの精度の高いモデルの知識を軽量なモデルへ継承することで、モデルの簡略化をする手法である。
教師モデルと生徒モデル(蒸留の学習方法)
教師モデルとして、学習済みの高度で複雑なモデルを用意し、生徒モデルとして軽量なモデルを用意する。
教師モデルの重みを固定し、下記のような形で、教師モデルと生徒モデルで同一の入力データを入力し、誤差を評価した重みの更新を生徒モデルだけに返す。
このようにして、教師モデルの出力に生徒モデルを近づけるよう学習する。
Section4 応用技術
4-1 MobileNets
提案の背景
近年の画像認識タスクに用いられる最新のニューラルネットワークアーキテクチャは、多くのモバイル及び組み込みアプリケーションの実行速度を上回る高い計算資源を必要とされる。
そこで、ネットワークのアーキテクチャを工夫し、モバイルアプリケーションの画像処理向けに同等精度で計算量を削減したネットワークを提案した。
提案手法
ストライド1でパディングを適用した場合の畳み込み計算の計算量は、H×W×K×K×C×M
- MobileNetsの計算手法
Depthwise CovolutionとPointwise Convolutionを組み合わせて軽量化を実現している。
-
- Depthwise Convolution
入力マップのチャネルごとに畳み込みを実施する。
カーネルのフィルタ数が1。
入力マップをそれらと結合する(入力マップのチャネル数と同じになる)
下記のスキームでは、計算量はH×W×K×K×C (一般的な畳み込みの1/M)
-
- Pointwise Convolution
1×1 convとも呼ばれる。
カーネルサイズが1×1×C、フィルタ数Mである。
入力マップのポイントごとに組み込みを実施する。
出力マップはフィルタ分だけ作成可能。
下記のスキームでは出力マップの計算量は、H×W×C×M
-
- Depthwise Separable Convolution
MobileNetsの計算方法は、DepthwiseとPointwiseを組み合わせた下記のDepthwise Separable Convolutionと呼ばれる方法をとっている。
1.まず、Depthwise Convolutionでチャネルごとに空間方向へ畳み込む。すなわち、チャネルごとにDk×Dk×1のサイズのフィルターをそれぞれ用いて計算を行うため、その計算量は(H×W×Dk×Dk×C)である。
2.次にDepthwise Convolutionの出力をPointwise Convolutionによってチャネル方向に畳み込む。すなわち、出力チャネルごとに1×1×Mサイズのフィルターをそれぞれ用いて計算を行うため、その計算量は(H×W×C×M)となる。
下記の図が一般的な畳み込み演算と、MobileNetsの演算を比較した図である。
一般的な畳み込み演算では、H×W×K×K×C×Mの計算量であるのに対し、MobileNetsでは、H×W×C×(K×K+M)の計算量となる。仮にK=3、M=4の場合、MobileNetsの計算量は、一般的な畳み込み演算の計算量に対し、(K×K+M)/(K×K×M)=13/36に軽量化できる。
4-2 DenseNet
論文タイトル:Densely Connected Convolutional Networks
概要
ニューラルネットワークでは、層が深くなるにつれて学習が難しkなるという問題があったが、前の層から層をスキップして後方の層に直接接続するというResNetのアイデアが生まれ、非常に深い層のネットワークが実現可能になった。
後方と前方の層をスキップ接続することでパフォーマンスが向上するならば、すべてのレイヤーを直接接続すればさらにパフォーマンスが向上するのではないかという発想を元に生まれたのがDenseNetである。
DenseNetのメリット
- 勾配消失の削減
- 特徴伝達の強化
- 特徴の効率的な利用
- パラメータ数の削減
- 正則化効果の期待
DenseNetの構造
DenseNetは次のようなブロックで構成されている。
1.Inital Convolution
2.Dense Block
3.Transition layer
4.Classification layer
- Dense Block
Dense Blockは、前層の入力をbatch正規化、Relu関数による変換、3×3畳み込みによる変換で処理する。
そして下記のように、前層以前の入力を合わせて処理して出力としてこの層の出力とする。
入力特徴マップのチャンネル数がl×kだった場合、出力は(l+1)×kとなる。
第l層の出力は下記のように書ける。
- growth rate
kをネットワークのgrowth rateと呼ぶ。kが大きくなるほどネットワークが大きくなるため、小さな整数に設定するのが良い。
- Transition layer
中間層でチャネルサイズを変更し、特徴マップのサイズを変更してダウンサンプリングを行い、次のDense Blockに繋ぐ。
DenseNetとResNetの比較
同じパラメータ数では、ResNetよりDenseNetのほうが誤差が小さかったという報告がある。
Review: DenseNet — Dense Convolutional Network (Image Classification)
4-4 Batch Norm、Layer Norm、Instanse Norm
データのばらつきを補正し、学習をしやすくするためにデータの平均を0、分散を1に修正する正規化という処理がある。
- Batch Norm(バッチ正規化)
ミニバッチに含まれるサンプルの同一チャネルが同一分布に従うよう正規化する。バッチサイズが小さいと、効果が小さくなるという課題がある。バッチサイズはハードウェアのスペックに律速されることが多く、学習上問題になることがある。
- Layer Norm(レイヤー正規化)
それぞれのサンプルで、すべてのチャネルで平均と分散を求めて正規化する。バッチサイズに依存しないことが特長。
- Instance Norm(インスタンス正規化)
各サンプルの各チャネルごとに正規化する。コントラストの正規化に寄与し、画像のスタイル転送やテクスチャ合成タスクで利用される。
ラビットチャレンジレポート 深層学習 Day4 その1
Section1 強化学習
1-1強化学習とは
長期的に報酬を最大化できるように環境のなかで行動を選択できるエージェントを作ることを目標とする機械学習の一分野である。
下記のように、ある方策Πに基づいた行動をとり、状態Sの変化を観測して価値Vを報酬として受け取るというプロセスが、強化学習のイメージである。この報酬を最大化するように方策を決定する。
1-2 強化学習の応用例
囲碁やチェスなどのゲームが有名だが、実用的には、例えばマーケティングへの応用が想定できる。
エージェントはプロフィールと購入履歴に基づいてキャンペーンメールを送る。
行動としては、顧客ごとに送信非送信のふたつの行動を選ぶことになる。
報酬としては、キャンペーンのコストという負の報酬とキャンペーンで生み出される売り上げという正の報酬をうけることになる。
1-3探索と利用のトレードオフ
環境について完璧な知識があれば、最適な行動について完璧な予測が可能ではないかとも考えられる。例えば、どのような顧客にキャンペーンメールを送ると、どのような行動をとるか既知の状況である。
強化学習では、上記仮定が成り立たないと考える。不完全な知識を元に行動しながらデータを収集し、最適な行動を見つけていく。
ここで、過去のデータを元に最適な行動のみをとり続けるという方策をとるとする。この場合は、他に存在するかもしれないベストの行動を見つけることは出来ない。これは探索が足りない状態ということが出来る。
一方で、未知の行動のみをとり続けるという方策をとると、過去の経験が生かせない。これは経験が足りない状態ということが出来る。
両者の方策のトレードオフを、探索と利用のトレードオフと呼ばれる。行動の方策を決める場合は、探索と利用の間のバランスをとることが必要である。
1-4強化学習イメージ
計算のため、以上の強化学習のイメージを数式で表現する。
下図のように方策πに対しては、方策関数 を定義、価値Vに関しては、行動価値関数 [tex;: Q(s,a)]を定義する。
1-5 強化学習の差分
強化学習と通常の機械学習(教師あり/なし)との違いは、学習の目標が異なることである。
・教師あり/なし学習では、データに含まれるパターンを見つけ出だすこと、及びそのデータから予測することが目標である。
・強化学習では、優れた方策を見つけることが目標である。
1-6 強化学習の歴史
強化学習の概念は以前からあったが、深層学習以上に計算能力が必要なため、コンピューティング能力が追いついておらず、注目されない冬の時代もあった。近年はコンピューティング能力が上がり、強化学習のコンセプトが実現可能になりつつあり、注目を集めている。
また、関数近似法とQ学習を組み合わせる手法が登場し、注目を集めている。
1-7価値関数
価値関数とは、価値Vを表す関数で、状態価値関数と行動価値関数の二種類がある。
状態価値関数:状態sの価値を表す関数
行動価値関数:ある状態sの時の行動aの価値を表す関数
1-8方策関数
方策ベースの強化学習手法において、ある状態でどのような行動をとるのかの確率を与える関数 のこと。
関数の関係
は、VやQを元にどのような行動をするか決定する。
:状態+価値関数:ゴールまで今の方策を続けたときの報酬の予測値が得られる。
1-9方策勾配法について
価値関数を求め、それを元に行動を決定するアプローチもあるが、やを求めるため、複雑でメモリを消費することや連続空間では扱いにくいといった課題がある。
そのため、方策関数を直接学習するというアプローチも考えられる。これが方策勾配法である。
θでパラメタライズされた方策関数π(s,a|θ)を考える。
最適なθを求めるため、下記のような反復方法を考える。
∇J(θ)はニューラルネットワークの誤差関数、εは学習率に相当する。θの式では加算されているが、これはθの最大値を求めるからである。
強化学習がパラメータの最適値を求める問題となり、ニューラルネットワークを使用することが可能になった。
J(θ)は、方策の評価関数で、方策πとその時の報酬Qを用いて、下記のように書ける。
θによる微分を求めると、下記の期待値となる。
Section2 Alpha GO
ここでは、強化学習、及びAIに大きなインパクトを残したDeep Mindが開発したAlpha Goのアルゴリズムについて解説する。Alpha Goは2015年に人間のプロ棋士にハンディキャップなしで初めて勝利した囲碁プログラムとなった。次いで2016年に世界トップ棋士であるイ・セドル、2017年に柯潔に勝利し、コンピュータにとって、人間に打ち勝つことが最も難しいと思われた囲碁で人間に勝利したプログラムとして、世界に大きな衝撃を与えた。その後、過去の試合データを使用せず、自己対戦の強化学習のみで学習したAlpha ZeroがAlpha Goを上回る性能を示した。
ここでは、Alpha Go (Lee)とAlpha Zeroについて解説する。
Alpha Go (Lee)
Alpha Go(Lee)の学習プロセスは、下記のステップで進む。
1. 教師あり学習によるRollOutPolicyとPolicyNetの学習
2. 強化学習によるPolicyNetの学習
3. 強化学習によるValueNetの学習
Alpha Go(Lee)のPolicyNet
入力は、19×19の2次元で、48チャンネルである。
ネットワークは、CNNで、softMaxによって19×19の着手予想確率を出力する。
入力の48チャンネルはそれぞれ下記のような入力に対応する。
RollOutPolicy
PolicyNetはニューラルネットネットワークなので、盤面の推論の際の計算量が多く、時間がかかる。そこで、探索中に高速で着手確率を出すために使用されるためにRollOutPolicyという方策関数が用意されている。こちらは線形の関数のため、計算が速い(PolicyNetの約1000倍)。RollOutPolicyで打ち手を絞り込み、PolicyNetで精度の高い打ち手を出すといった使い分けが考えられる。
RollOutPolicyの入力は以下である。
PolicyNetとRollOutPolicyの教師あり学習
KGS Go Server(ネット囲碁対局サイト)の棋譜データから300万局面分の教師を用意し、教師と同じ着手を予測できるよう学習を行った。
具体的には、教師が着手した手を1とし、残りを0とした19×19次元の配列を教師とし、それを分類問題として学習した。
この制度で作成したPolicyNetは57%程度の精度である。
PolicyNetの強化学習
まず、PolicyNetの強化学習の過程を500Iterationごとに記録し保存して、PolicyPoolを作る。
次に現状のPolicyNetとPolicyPoolからランダムに選択されたPolicyNetと対局シミュレーションを行い、その結果を用いて方策勾配法で学習を行う。
現状のPolicyNet同士の対局ではなく、PolicyPoolに保存されているものとの対局を使用する理由は、対局に幅を持たせて過学習を防ごうというのが主たる理由である。
この学習をmini batch size 128で1万回行った。
ValueNetの学習
PolicyNetを使用して対局シミュレーションを行い、その結果の勝敗を教師として学習する。
教師データ作成の手順は、
1. まずSL PolicyNet(教師あり学習で作成したPOlicyNet)でN手まで打つ。
2. N+1手目の手をランダムに選択し、その手で進めた局面をS(N+1)とする。
3.S(N+1)からRL PolicyNet(強化学習で作成しtあPolicyNet)で終局まで打ち、その勝敗報酬をRとする。
S(N+1)とRを教師データ対とし、損失関数を平均二乗誤差とし、回帰問題として学習した。
この学習をmini batch size 32で5000万回行った。
N手からN+1手からのPolicyNetを別々にしてある理由は、過学習を防ぐためである。
モンテカルロ木探索
min-max探索やαβ探索では、盤面の価値や勝率予想値が必要だが、囲碁では
盤面価値や勝率予想値を出すことが困難とされてきた。
そこで、盤面評価値によらず、末端評価値、つまり勝敗のみを使って探索を行うという発想で生まれた探索法である。囲碁の場合、他のボードゲームと違い、最大手数はマスの数でほぼ限定されるため、末端局面に到達しやすい。
具体的には、現局面から末端局面までをPlayOutと呼ばれるランダムシミュレーションを多数回行い、その勝敗を集計して着手の優劣を決定する。
また、該当手のシミュレーションが一定数を超えたら、その手を着手した後の局面をシミュレーション開始局面とするよう、探索気を成長させる。
この探索木の成長を行うという点が、モンテカルロ木探索の優れているところである。
モンテカルロ木探索は、この木の成長を行うことによって、一定条件下では探索結果は最善手を返すことが理論的に証明されている。
Alpha Go(Lee)のモンテカルロ木探索は、選択、評価、バックアップ、成長という4つのステップで構成されている。
AlphaGo Zero
AlphaGo(Lee)とAlphaGo Zeroの違い
1. 教師あり学習を一切行わず、強化学習のみで作成
2. 特徴入力からヒューリスティックな要素を排除し、石の配置のみにした
3. PolicyNetとValueNetを1つのネットワークに統合した
4. Residual netを導入した
5. モンテカルロ木探索からRollOutシミュレーションをなくした
AlphaGo Zeroの学習法
AlphaGo Zeroの学習は、自己対局による教師データの作成、学習、ネットワークの更新の3ステップで構成される。
- 自己対局による教師データの作成
現状のネットワークでモンテカルロ木探索を用いて自己対局を行う。
まず30手までランダムで打ち、そこから探索を行い、勝敗を決定する。
自己対局中の各局面での着手選択確率分布と勝敗を記録する。
教師データの形は、[局面、着手選択確率分布、勝敗]が1セットとなる。
- 学習
自己対局で作成した教師データを用い、学習を行う
NetworkのPolicy部分の教師に着手選択確率分布を用い、Value部分の教師に勝敗を用いる。
損失関数はPolicy部分はクロスエントロピー、Value部分は平均二乗誤差。
- ネットワーク更新
学習後、現状のネットワークと学習後のネットワークとで対局テストを行い、学習後のネットワークの勝率が高かった場合、学習後のネットワークを現状のネットワークとする。
AlphaGo ZeroのPolicyValueNet
入力は17チャンネルの19×19の盤面で、途中からPolicy出力とValue出力に分れる。また中間にResidual Blockを挟む。
Residual Block
ネットワークにショートカット構造を追加して、勾配爆発、勾配消失を防いだもの。
Residual Networkを使うことにより、100層を超えるネットワークでも安定した学習が可能になった。
また、Redisual Networkを使うことにより層数が異なるネットワークのアンサンブルの効果を得られるという話もある。
- Residual Blockの工夫
- Bottleneck
1×1 KernelのComvolutionを利用し、1層目で次元削減を行って2層目で次元を復元する3層構造にしたもの。2層のものと比べて計算量はほぼ同じだが、1層増やせるメリットがある
- PreActivation
ResidualBlockの並びをBatchNorm→ReLu→Convolution→BatchNorm→ReLu→Comvolution→Addとすることにより性能が上昇した
- Network構造の工夫
- WideResNet
ConvolutionのFilter数をk倍にしたResNet。1倍→k倍xブロック→2k倍yブロックと段階的に幅を増やしていくのが一般的。Filterを増やすことにより、浅井層数でも深い層数のものと同等以上の精度となり、またGPUをより効率的に使用できるため学習も早い。
-
- PyramidNet
WideResNetで幅が広がった直後の層に、過度の負担がかかり精度を落とす原因になっているとし、段階的にではなく、各層でfilterを増やしていくResnet。
ラビットチャレンジ 深層学習Day3 その3
Section5 Seq2Seq
Seq2Seq2は、2014年にGoogleにより発表された技術であり、機械翻訳やチャットボットのようなシーケンシャルなデータの処理に向いた技術である。
基本的な構造は下図のようになっている。
入力データを中間データに変換するEncoderと言われる構造があり、中間層に中間データを送る。そして、中間層は、Decoderと呼ばれる構造にデータを送り、Decoderは、送られたデータを出力データに変換する。
Seq2SeqはこのようなEncoder-Decoderと呼ばれる構造をとっている。
5-1 Encoder RNN
Seq2SeqのEncoderとして用いられるEncoder RNNの説明をする。
ユーザーがインプットしたテキストデータ尾w、単語などのトークンに区切って渡す構造になっている。
Encoder RNNがテキストデータを処理する手順は、次のとおりである。
1. 文章を単語などに分割し、トークンごとのIDに分割するTakingと呼ばれる処理をする。
2. IDからそのトークンを表す分散表現ベクトルに変換するEmbeddingと呼ばれる処理をする。
3. 2で変換したベクトルvecをRNNに入力し、hidden stateを出力する。このhiddem stateと次の入力vecをRNNに入力してhidden stateを出力という流れを繰り返す。
4.最後のvecを入力した時のhidden stateをfinal stateとする。このfinal stateがthought vectorと呼ばれ、入力した文章の意味を示すベクトルとなる。
5-2 Decoder RNN
中間層からのデータを、単語などのトークンごとに出力データとして生成する構造。
Decoder RNNの処理手順は次のようになる。
1.前述のEncoder RNNのthought vectorをDocoderRNNのinitial stateとして設定し、Enbeddingを入力。tokenの生成確率を出力する。
2.生成確率に基づいてtokenをランダムに選ぶsamplingという処理を行う
3.2で選ばれたtokenをEmbeddingして、Decoder RNNの次の段の入力とする。
4.1-3を繰り返し、2で得られたtokenを文字列に直すDetokenizeという処理を行う。
- 確認テスト
下記の選択肢から、seq2seqについて説明しているものを選べ
(1)時刻に関して順方向と逆方向のRNNを構成し、それら2つの中間層表現を特徴量として利用するものである。
(2)RNNを用いたEncoder-Decoderモデルの一種であり、機械翻訳などのモデルに使われる。
(3)構文木などの木構造に対して、隣接単語から表現ベクトル(フレーズ)を作るという演算を再帰的に行い(重みは共通)、文全体の表現ベクトルを得るニューラルネットワークである。
(4)RNNの一種であり、単純なRNNにおいて問題となる勾配消失問題をCECとゲートの概念を導入することで解決したものである。
[解答](2)
(1)→双方向RNN
(2)→seq2seq
(3)→通常のRNNなど
(4)→LSTM
- 演習チャレンジ
機械翻訳タスクにおいて、入力は複数の単語からなる文章であり、それぞれの単語はone-hotベクトルで表現されている。Encoderにおいて、それらの単語は単語埋め込みによる特徴量に変換され、そこからRNNによって(一般にはLSTMを使うことが多い)時系列の情報を持つ特徴へとエンコードされる。以下は入力である文(文章)とを時系列の情報を持つ特徴量へとエンコードする関数である。ただし_activation関数は何らかの活性化関数を表すとする。
(き)にあてはまるコードを示せ。
[解答]
E.dot(w)
単語wはone-hotベクトルであり、それを単語埋め込みにより別の特徴量に変換する。これは埋め込み行列を用いてE.dot(w)と書ける。
5-3 HRED
過去n-1個の発話から次の発話を生成する技術。
seq2seqは、会話の文脈無視の応答だったが、HREDでは、前の単語の流れに即して応答されるため、より人間らしい文章が生成される。
HREDの構造は、seq2seq+context RNNである。context RNNは、Encoderのまとめた各文章をまとめて、これまでの会話コンテキスト全体を表すベクトルに変換する構造である。過去の発話の履歴を加味した応答を出来る。
HREDの課題
HREDは確率的な多様性が字面にしかなく、会話の「流れ」のような多様性がない。そのため、同じコンテキストを与えられても、答えの内容が名毎回会話の流れとしては同じものしか出せない。
また、HREDは短く情報量に乏しい答えをしがちで、短いよくある答えを学ぶ傾向がある。たとえば、「うん」「そうだね」など。
5-4 VHRED
HREDにVAEの潜在変数の概念を追加したもので、HREDの課題をVAEの洗剤変数の概念を追加することで解決した構造である。
- 確認テスト
sesq2seqとHRED、HREDとVHREDの違いを簡潔に述べよ。
[解答]
・seq2はEncoderとDecoderを接続した構成で、会話の文脈を読み取れないが、HREDは、context-RNNを導入することで、過去の会話を踏まえた文章を生成できる。
・HREDは会話の流れのような多様性がないが、VHREDは、VAEの潜在変数の概念を追加することで会話の流れをより把握できる。
5-5 VAE
VAEは変分オートエンコーダ(Variational Autoencoder)で、ニューラルネットワークを用いた生成モデルの一種である。
5-5-1 オートエンコーダ―
オートエンコーダとは、教師無し学習の一つである。
そのため学習時の入力データは、訓練データのみで教師データは利用しない。
- オートエンコーダの構造
オートエンコーダの構造の概念図は下記になる。
入力データからencoderと呼ばれるニューラルネットワークで潜在変数zに変換し、逆にDecoderで潜在変数zから元画像を復元する。
zの次元が入力データのより小さい場合は、次元削減とみなすことが出来る。
5-5-2 VAE
通常のオートエンコーダの場合は、何かしら潜在変数zにデータを押し込んでいるが、その構造がどのような状態か分からない。
VAEはこの潜在変数Zの確率分布z~N(0.1)を仮定した。それにより、データ同士の関係性が示せるようになった。
- 確認テスト
VAEに関する下記の説明文中の空欄に当てはまる言葉を答えよ。
自己符号化器の潜在変数に_____を導入したもの。
[解答]
確率分布z_N(0.1)
VAEは、自己符号化器の潜在変数内で、データ同士の関連性を示すために、確率分布を導入している。
そのため、生成モデルとして使用される。
Section6 Word2vec
RNNでは、単語のような可変長の文字列をニューラルネットワークに与えることはできないため、固定長形式で単語を表す必要がある。
そこで単語をベクトルに変換して表現する方法が開発された。
まず、One-hotベクトル表現がある。これは、各単語ごとに簡便なロジックで表現が出来るが、1万語あれば、1万次元のベクトルが必要になり、しかもほとんどゼロの疎なベクトルである。これは計算リソース上無駄が多いので、数百次元で単語を表現できる分散表現が考案された。
分散表現
one-hotベクトルでは、すべてのベクトル値を1か0で表すが、分散表現では、様々な数値をとる。また、単語同士の演算も可能で、単語の類似度も計算が出来る。
one-hotベクトルに比較して少ない次元数で表現が可能なので、データサイズを抑えることが出来る。
Word2vecの仕組み
Word2vecの基本的な仕組みは、2層のニューラルネットワークを用いて文章中の単語を予測するものである。
Word2Vecには、CBowと、skip-gramの2つのニューラルネットワークモデルが搭載されている。
- CBOW
周囲の単語からある単語を予測するモデル
- Skip-gram
ある単語からその周囲の単語を予測するモデル。
Section7 Attention Mechanism
seq2seqでは、2単語でも100単語でも固定次元ベクトルの中に入力をしなくてはならず、長い文章への対応は難しい。
そこで、文章が長くなるほどそのシーケンスの内部表現の次元が大きくなる仕組みとしてAttention機構が考案された。これは、「入力と出力のどの単語が関連しているのか」の関連度を学習する仕組みである。
attentionは、どの要素に注目すべきかを推論し、その部分を集中的に処理することで、推論の精度を向上させる。自然言語処理で注目されたが、その後画像処理にも応用されている。
- 確認テスト
RNNとword2vec,seq2seqとAttentionの違いを簡潔に述べよ
[解答]
・RNNは時系列のデータを処理するのに適したネットワークである。word2vecは単語の分散表現を得るためのネットワークである。
・seq2seqは、ある時系列データから、別の時系列データを得るための手法である。Attentionは、時系列データの関連性にそれぞれ重みをつける手法である。
ラビットチャレンジ 深層学習Day3 その2
Section2 LSTM
RNNの課題
- 勾配消失
時間を遡るほど勾配が消失してしまうため、長い時系列の学習が困難だった。
勾配消失は、誤差逆伝搬法で深い層のニューラルネットワークを遡る時に見られる現象で、1より小さい値になる微分値が多数乗算されることにより引き起こされる。例えば、活性化関数として使用されるシグモイド関数は、微分値は最大でも0.25にしかならないため、2つ以上乗算されると、非常に小さな値となり、勾配はほぼ消失してしまう。
- 確認テスト
シグモイド関数を微分した時、入力値が0の時に最大値をとる。その値として正しいものを選択肢から選べ。
(1)0.15
(2)0.25
(3)0.35
(4)0.45
[解答]
(2)0.25
- 勾配爆発
勾配消失とは逆に、勾配が層を逆伝搬するごとに指数関数的に大きくなってしまう問題。活性化関数に恒関数を用いたりすると、この問題が起こりやすい。
- 演習チャレンジ
RNNや深いモデルでは勾配の消失または勾配爆発が起こる傾向がある。勾配爆発を防ぐために勾配のクリッピングを行うという手法がある。具体的には勾配のノルムが閾値を超えたら、勾配のノルムを閾値に正規化するというものである。以下は勾配のクリッピングを行う関数である。
(さ)にあてはまるコードをかけ
[解答]
gradient*rate
LSTM(Long Short Term Memory)は、RNNの勾配消失や勾配爆発という問題を解決するために考案された構造である。
次に、LSTMの全体像を示す。
入力から出力の間がRNNの中間層に相当する。中間層で自己回帰(青点線)していることがわかる。
中間層内の構造の工夫がLSTMである。
1-2 CEC
LSTMの中心はCEC(Constant Error carousel:定誤差カルーセル)である。このセルは、勾配を記憶する役割を果たしている。
具体的には、CECの中では、勾配消失及び勾配爆発の解決法として、勾配を1としている。
CECの課題は、入力データについて時間依存度の関係なく重みが一律のため、学習が出来ないということである。
つまり、学習と記憶の役割を中間層内で分けている。
学習については、入力層の重荷については入力ゲート、出力層への重みについては出力ゲートでそれぞれ担っている。
2-2入力ゲートと出力ゲート
入力ゲート
入力ゲートは、CECに記憶する内容を調整する機能を持つ。
具体的には入力側の変数x(t)と、出力からの一つ前の数値の回帰h(t-1)にそれぞれ重みWiとUiを掛け合わせて演算した値をCECの入力との内積演算ユニットに送る。
出力ゲート
出力ゲートは、ECEからの出力を調整して出力層に送る機能を持つ。
具体的には入力側の変数x(t)と、出力からの一つ前の数値の回帰h(t-1)にそれぞれ重みWoとUoを掛け合わせて演算した値をCECの出力との内積演算ユニットに送る。
このように、入力・出力ゲートを追加することで、それぞれのゲートへの入力値の重みを重み行列W,Uで可変可能とすることで、CECの課題を解決している。
2-3忘却ゲート
CECは過去のすべての情報が保管されているが、逆に不要な場合でも削除が出来ない。
そこで、過去の上方がいらなくなった場合、そのタイミングで情報を忘却するための機能として、忘却ゲートが設けられた。
上記の赤枠が忘却ゲートである。
忘却ゲートは、入出力にそれぞれWf, Ufの重みを乗算した関数忘却関数f(t)を作る。次にCECの一つ前の時間の出力c(t-1)と内積を取り、CEC内で入力層からの信号i(t)a(t)と線形結合をとり、下記c(t)を出力する。
忘却関数f(t)と内積をとることで、CECの記憶の忘却度が調整される。
- 確認テスト
以下の文章をLSTMに入力し、空欄に当てはまる単語を予測したいとする。文中の「とても」という言葉は空欄の予測において、無くなっても影響を及ぼさないと考えられる。
このような場合、どのゲートが作用すると考えられるか。
「映画おもしろかったね。ところで、とてもおなかが空いたからなにか____。」
[解答]
文意に影響を及ぼさないため、忘却ゲートが作用していると考えられる。
- 演習チャレンジ
以下のプログラムはLSTMの順伝搬を行うプログラムである。ただし、_sigmoid関数は、要素ごとにシグモイドを作用させる関数である。
(け)にあてはまるコードを示せ。
[解答]
input_gate*a+forget_gate*c
Section 3 GRU
従来のLSTMでは、パラメータ数が多いため、計算負荷が大きかった。そこで、パラメータを大幅に削って計算負荷を小さくし、かつ精度は同等以上を見込める構造として、GRUが考案された。
GRUはリセットゲート、更新ゲート、活性化関数を持つ内部ノードからなり、CECは持っていない。
数式化のために、下記のように信号と重みの定義をする。
:入力信号
:入力重み
:入力からリセットゲートへの重み
:出力からリセットゲートへの重み
:リセットゲートのバイアス
:リセットゲートの出力信号
:出力信号の回帰への重み
:内部ノードの活性化関数
:入力から更新ゲートへの重み
:出力から更新ゲートへの重み
:更新ゲートのバイアス
:更新ゲートの出力信号
:出力信号
まず、リセットゲートの出力は、下記のように表せる。
次段の内部演算ユニットの出力は下記のようになる。
よって、活性化関数f(x)を持つ内部ノードの出力は下記のようになる。
また、更新ゲートの出力は下記のようになる。
よって、出力層の出力h(t)は下記のように表せる。
- 確認テスト
LSTMとCECが抱える問題について、それぞれ簡潔に述べよ
[解答]
LSTMはパラメータが多く、計算負荷が高いため、計算に時間がかかる。CECは勾配1で記憶するが、情報処理が出来ない。そこで、周りに情報処理のためのゲートを設けるため、構造が複雑になる。
実装演習
RNNで単語予測をするネットワークをtensorflowで記述した。
import tensorflow as tf import numpy as np import re import glob import collections import random import pickle import time import datetime import os # logging levelを変更 tf.logging.set_verbosity(tf.logging.ERROR) class Corpus: def __init__(self): self.unknown_word_symbol = "<???>" # 出現回数の少ない単語は未知語として定義しておく self.unknown_word_threshold = 3 # 未知語と定義する単語の出現回数の閾値 self.corpus_file = "./corpus/**/*.txt" self.corpus_encoding = "utf-8" self.dictionary_filename = "./data_for_predict/word_dict.dic" self.chunk_size = 5 self.load_dict() words = [] for filename in glob.glob(self.corpus_file, recursive=True): with open(filename, "r", encoding=self.corpus_encoding) as f: # word breaking text = f.read() # 全ての文字を小文字に統一し、改行をスペースに変換 text = text.lower().replace("\n", " ") # 特定の文字以外の文字を空文字に置換する text = re.sub(r"[^a-z '\-]", "", text) # 複数のスペースはスペース一文字に変換 text = re.sub(r"[ ]+", " ", text) # 前処理: '-' で始まる単語は無視する words = [ word for word in text.split() if not word.startswith("-")] self.data_n = len(words) - self.chunk_size self.data = self.seq_to_matrix(words) def prepare_data(self): """ 訓練データとテストデータを準備する。 data_n = ( text データの総単語数 ) - chunk_size input: (data_n, chunk_size, vocabulary_size) output: (data_n, vocabulary_size) """ # 入力と出力の次元テンソルを準備 all_input = np.zeros([self.chunk_size, self.vocabulary_size, self.data_n]) all_output = np.zeros([self.vocabulary_size, self.data_n]) # 準備したテンソルに、コーパスの one-hot 表現(self.data) のデータを埋めていく # i 番目から ( i + chunk_size - 1 ) 番目までの単語が1組の入力となる # このときの出力は ( i + chunk_size ) 番目の単語 for i in range(self.data_n): all_output[:, i] = self.data[:, i + self.chunk_size] # (i + chunk_size) 番目の単語の one-hot ベクトル for j in range(self.chunk_size): all_input[j, :, i] = self.data[:, i + self.chunk_size - j - 1] # 後に使うデータ形式に合わせるために転置を取る all_input = all_input.transpose([2, 0, 1]) all_output = all_output.transpose() # 訓練データ:テストデータを 4 : 1 に分割する training_num = ( self.data_n * 4 ) // 5 return all_input[:training_num], all_output[:training_num], all_input[training_num:], all_output[training_num:] def build_dict(self): # コーパス全体を見て、単語の出現回数をカウントする counter = collections.Counter() for filename in glob.glob(self.corpus_file, recursive=True): with open(filename, "r", encoding=self.corpus_encoding) as f: # word breaking text = f.read() # 全ての文字を小文字に統一し、改行をスペースに変換 text = text.lower().replace("\n", " ") # 特定の文字以外の文字を空文字に置換する text = re.sub(r"[^a-z '\-]", "", text) # 複数のスペースはスペース一文字に変換 text = re.sub(r"[ ]+", " ", text) # 前処理: '-' で始まる単語は無視する words = [word for word in text.split() if not word.startswith("-")] counter.update(words) # 出現頻度の低い単語を一つの記号にまとめる word_id = 0 dictionary = {} for word, count in counter.items(): if count <= self.unknown_word_threshold: continue dictionary[word] = word_id word_id += 1 dictionary[self.unknown_word_symbol] = word_id print("総単語数:", len(dictionary)) # 辞書を pickle を使って保存しておく with open(self.dictionary_filename, "wb") as f: pickle.dump(dictionary, f) print("Dictionary is saved to", self.dictionary_filename) self.dictionary = dictionary print(self.dictionary) def load_dict(self): with open(self.dictionary_filename, "rb") as f: self.dictionary = pickle.load(f) self.vocabulary_size = len(self.dictionary) self.input_layer_size = len(self.dictionary) self.output_layer_size = len(self.dictionary) print("総単語数: ", self.input_layer_size) def get_word_id(self, word): # print(word) # print(self.dictionary) # print(self.unknown_word_symbol) # print(self.dictionary[self.unknown_word_symbol]) # print(self.dictionary.get(word, self.dictionary[self.unknown_word_symbol])) return self.dictionary.get(word, self.dictionary[self.unknown_word_symbol]) # 入力された単語を one-hot ベクトルにする def to_one_hot(self, word): index = self.get_word_id(word) data = np.zeros(self.vocabulary_size) data[index] = 1 return data def seq_to_matrix(self, seq): print(seq) data = np.array([self.to_one_hot(word) for word in seq]) # (data_n, vocabulary_size) return data.transpose() # (vocabulary_size, data_n) class Language: """ input layer: self.vocabulary_size hidden layer: rnn_size = 30 output layer: self.vocabulary_size """ def __init__(self): self.corpus = Corpus() self.dictionary = self.corpus.dictionary self.vocabulary_size = len(self.dictionary) # 単語数 self.input_layer_size = self.vocabulary_size # 入力層の数 self.hidden_layer_size = 30 # 隠れ層の RNN ユニットの数 self.output_layer_size = self.vocabulary_size # 出力層の数 self.batch_size = 128 # バッチサイズ self.chunk_size = 5 # 展開するシーケンスの数。c_0, c_1, ..., c_(chunk_size - 1) を入力し、c_(chunk_size) 番目の単語の確率が出力される。 self.learning_rate = 0.005 # 学習率 self.epochs = 1000 # 学習するエポック数 self.forget_bias = 1.0 # LSTM における忘却ゲートのバイアス self.model_filename = "./data_for_predict/predict_model.ckpt" self.unknown_word_symbol = self.corpus.unknown_word_symbol def inference(self, input_data, initial_state): """ :param input_data: (batch_size, chunk_size, vocabulary_size) 次元のテンソル :param initial_state: (batch_size, hidden_layer_size) 次元の行列 :return: """ # 重みとバイアスの初期化 hidden_w = tf.Variable(tf.truncated_normal([self.input_layer_size, self.hidden_layer_size], stddev=0.01)) hidden_b = tf.Variable(tf.ones([self.hidden_layer_size])) output_w = tf.Variable(tf.truncated_normal([self.hidden_layer_size, self.output_layer_size], stddev=0.01)) output_b = tf.Variable(tf.ones([self.output_layer_size])) # BasicLSTMCell, BasicRNNCell は (batch_size, hidden_layer_size) が chunk_size 数ぶんつながったリストを入力とする。 # 現時点での入力データは (batch_size, chunk_size, input_layer_size) という3次元のテンソルなので # tf.transpose や tf.reshape などを駆使してテンソルのサイズを調整する。 input_data = tf.transpose(input_data, [1, 0, 2]) # 転置。(chunk_size, batch_size, vocabulary_size) input_data = tf.reshape(input_data, [-1, self.input_layer_size]) # 変形。(chunk_size * batch_size, input_layer_size) input_data = tf.matmul(input_data, hidden_w) + hidden_b # 重みWとバイアスBを適用。 (chunk_size, batch_size, hidden_layer_size) input_data = tf.split(input_data, self.chunk_size, 0) # リストに分割。chunk_size * (batch_size, hidden_layer_size) # RNN のセルを定義する。RNN Cell の他に LSTM のセルや GRU のセルなどが利用できる。 cell = tf.nn.rnn_cell.BasicRNNCell(self.hidden_layer_size) outputs, states = tf.nn.static_rnn(cell, input_data, initial_state=initial_state) # 最後に隠れ層から出力層につながる重みとバイアスを処理する # 最終的に softmax 関数で処理し、確率として解釈される。 # softmax 関数はこの関数の外で定義する。 output = tf.matmul(outputs[-1], output_w) + output_b return output def loss(self, logits, labels): cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=labels)) return cost def training(self, cost): # 今回は最適化手法として Adam を選択する。 # ここの AdamOptimizer の部分を変えることで、Adagrad, Adadelta などの他の最適化手法を選択することができる optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate).minimize(cost) return optimizer def train(self): # 変数などの用意 input_data = tf.placeholder("float", [None, self.chunk_size, self.input_layer_size]) actual_labels = tf.placeholder("float", [None, self.output_layer_size]) initial_state = tf.placeholder("float", [None, self.hidden_layer_size]) prediction = self.inference(input_data, initial_state) cost = self.loss(prediction, actual_labels) optimizer = self.training(cost) correct = tf.equal(tf.argmax(prediction, 1), tf.argmax(actual_labels, 1)) accuracy = tf.reduce_mean(tf.cast(correct, tf.float32)) # TensorBoard で可視化するため、クロスエントロピーをサマリーに追加 tf.summary.scalar("Cross entropy: ", cost) summary = tf.summary.merge_all() # 訓練・テストデータの用意 # corpus = Corpus() trX, trY, teX, teY = self.corpus.prepare_data() training_num = trX.shape[0] # ログを保存するためのディレクトリ timestamp = time.time() dirname = datetime.datetime.fromtimestamp(timestamp).strftime("%Y%m%d%H%M%S") # ここから実際に学習を走らせる with tf.Session() as sess: sess.run(tf.global_variables_initializer()) summary_writer = tf.summary.FileWriter("./log/" + dirname, sess.graph) # エポックを回す for epoch in range(self.epochs): step = 0 epoch_loss = 0 epoch_acc = 0 # 訓練データをバッチサイズごとに分けて学習させる (= optimizer を走らせる) # エポックごとの損失関数の合計値や(訓練データに対する)精度も計算しておく while (step + 1) * self.batch_size < training_num: start_idx = step * self.batch_size end_idx = (step + 1) * self.batch_size batch_xs = trX[start_idx:end_idx, :, :] batch_ys = trY[start_idx:end_idx, :] _, c, a = sess.run([optimizer, cost, accuracy], feed_dict={input_data: batch_xs, actual_labels: batch_ys, initial_state: np.zeros([self.batch_size, self.hidden_layer_size]) } ) epoch_loss += c epoch_acc += a step += 1 # コンソールに損失関数の値や精度を出力しておく print("Epoch", epoch, "completed ouf of", self.epochs, "-- loss:", epoch_loss, " -- accuracy:", epoch_acc / step) # Epochが終わるごとにTensorBoard用に値を保存 summary_str = sess.run(summary, feed_dict={input_data: trX, actual_labels: trY, initial_state: np.zeros( [trX.shape[0], self.hidden_layer_size] ) } ) summary_writer.add_summary(summary_str, epoch) summary_writer.flush() # 学習したモデルも保存しておく saver = tf.train.Saver() saver.save(sess, self.model_filename) # 最後にテストデータでの精度を計算して表示する a = sess.run(accuracy, feed_dict={input_data: teX, actual_labels: teY, initial_state: np.zeros([teX.shape[0], self.hidden_layer_size])}) print("Accuracy on test:", a) def predict(self, seq): """ 文章を入力したときに次に来る単語を予測する :param seq: 予測したい単語の直前の文字列。chunk_size 以上の単語数が必要。 :return: """ # 最初に復元したい変数をすべて定義してしまいます tf.reset_default_graph() input_data = tf.placeholder("float", [None, self.chunk_size, self.input_layer_size]) initial_state = tf.placeholder("float", [None, self.hidden_layer_size]) prediction = tf.nn.softmax(self.inference(input_data, initial_state)) predicted_labels = tf.argmax(prediction, 1) # 入力データの作成 # seq を one-hot 表現に変換する。 words = [word for word in seq.split() if not word.startswith("-")] x = np.zeros([1, self.chunk_size, self.input_layer_size]) for i in range(self.chunk_size): word = seq[len(words) - self.chunk_size + i] index = self.dictionary.get(word, self.dictionary[self.unknown_word_symbol]) x[0][i][index] = 1 feed_dict = { input_data: x, # (1, chunk_size, vocabulary_size) initial_state: np.zeros([1, self.hidden_layer_size]) } # tf.Session()を用意 with tf.Session() as sess: # 保存したモデルをロードする。ロード前にすべての変数を用意しておく必要がある。 saver = tf.train.Saver() saver.restore(sess, self.model_filename) # ロードしたモデルを使って予測結果を計算 u, v = sess.run([prediction, predicted_labels], feed_dict=feed_dict) keys = list(self.dictionary.keys()) # コンソールに文字ごとの確率を表示 for i in range(self.vocabulary_size): c = self.unknown_word_symbol if i == (self.vocabulary_size - 1) else keys[i] print(c, ":", u[0][i]) print("Prediction:", seq + " " + ("<???>" if v[0] == (self.vocabulary_size - 1) else keys[v[0]])) return u[0] def build_dict(): cp = Corpus() cp.build_dict() if __name__ == "__main__": #build_dict() ln = Language() # 学習するときに呼び出す #ln.train() # 保存したモデルを使って単語の予測をする ln.predict("some of them looks like")
結果の一部
lipoprotein : 1.3668956e-14 ldl : 1.4424414e-14 statin : 1.378644e-14 a--z : 1.3740787e-14 simvastatin : 1.413377e-14 bmi : 3.1720919e-07 covariates : 2.8344898e-06 yhl : 2.2330354e-07 yol : 9.992968e-11 obesity : 1.3501551e-09 evgfp : 6.1829963e-09 unintended : 4.6785074e-09 sizes : 2.569963e-07 obese : 1.9368042e-07 <???> : 2.9195742e-05 Prediction: some of them looks like et
- 演習チャレンジ
GROもLSTMと同様にRNNの一種であり、単純な
RNNにおいて問題となる勾配消失問題を解決し、長期的な依存関係を学習することが出来る。LSTMに比べ変数の数やゲートの数が少なく、より単純なモデルであるが、タスクによってはLSTMより良い性能を発揮すr。以下のプログラムはGRUである。ただし、_sigmoid関数は要素ごとにシグモイド関数を作用させる関数である。
(こ)にあてはまるコードを書け。
[解答]
GRUの更新ゲートの出力なので、下記になる。
(1-z)*h+z*h_bar
- 確認テスト
LSTMとGRUの違いを述べよ
[解答]
LSTMはCEC、入力ゲート、忘却ゲート、出力ゲートを備え、構成が複雑でパラメータが多い。
GRUは、リセットゲート、更新ゲートで更新され、LSTMより構成が単純でパラメータが少ない。
Section4 双方向RNN
過去の上方だけでなく、未来の情報を加味することで、精度を向上させるモデル。文章の推敲や機械翻訳などに使用される。
- 演習チャレンジ
以下は双方向RNNの順伝搬を行うプログラムである。順方向については、入力から中間層への重みW_f、1ステップ前の中間層出力から中間層への重みをU_f、逆方向に関しては同様にパラメータW_b、U_bを持ち、両者の中間表現を合わせた特徴から出力層への重みはVである。_rnn関数はRNNの順伝搬を表し中間層の系列を返す関数であるとする。(か)にあてはまるコードを示せ。
[解答]
hsは、順方向と逆方向の中間表現の特徴量を合わせたものなので、下記になる。
np.concatenate([h_f,h_b[::,-1]],axis=1)