深層学習Day3
前回の復習
AlexNet
2012年に開かれた画像認識コンペティションで優勝したモデル。
ディープラーニングが注目される大きなきっかけになったモデルでもある。
5層の畳み込み層及びプーリング層と、それに続く3層の全結合層から構成されている。
<確認テスト>
サイズ5×5の入力画像を、サイズ3×3のフィルタで畳み込んだ時の出力画像のサイズを答えよ。
ただし、ストライドは2、パディングは1とする。
⇒3×3
Section1 再帰的ニューラルネットワークの概念
再帰的ニューラルネットワーク(以下RNN)とは、時系列データに対応したニューラルネットワークである。時系列データに対応させるために、「時系列データを時系列順に入力していく」ことができるよなモデルになっている。
時系列データとは
時間的順序を追って一定間隔ごとに観察され、しかも相互に統計的依存関係が認められるようなデータの系列。
データの主な例は音声データ・テキストデータ・株価・天気など。
RNNの構造
<確認テスト>
RNNのネットワークには大きく分けて3つの重みがある。1つは入力から現在の中間層を定義する際にかけられる重み、1つは中間層から出力を定義する際にかけられる重みである。
残り1つの重みについて説明せよ。
⇒1時刻前の中間層から、現在時刻の中間層の計算をする際にかけられる重み。
BPTT
BPTT(Back Propagation Through Time)は時系列に沿って誤差逆伝播をすること。RNNにおけるパラメータ調整法。
ここで、いったん誤差逆伝播法の復習
<確認テスト>
連鎖律の原理を使い、dz/dxを求めよ
⇒以下で計算
z = t^2 \\
t = x+y \\
\\
\therefore \frac{dz}{dx}=\frac{dz}{dt} \frac{dt}{dx} = 2t
続いて、先ほどのRNNの構成図から、具体的計算を見てみる。
<確認テスト>
RNNの構成図の中の$y_1$を、$x,z_0,z_1,W_{in},W,W_{out}$を用いて数式で表せ。
(バイアスは任意の文字で、中間層の出力の活性化関数はSigmoid関数$g(x)$)
⇒以下で計算(バイアスは$b$とする)
y_1 = g(W_{out}z_1 + c) \\
z_1 = g(W z_0 + W_{in} x_1 + b)
実装演習
BPTTを実装。コード中でランダムデータを用意し、BPTTを行う。
# データを用意
# 2進数の桁数
binary_dim = 8
# 最大値 + 1
largest_number = pow(2, binary_dim)
# largest_numberまで2進数を用意
binary = np.unpackbits(np.array([range(largest_number)],dtype=np.uint8).T,axis=1)
input_layer_size = 2
hidden_layer_size = 16
output_layer_size = 1
weight_init_std = 1
learning_rate = 0.1
iters_num = 10000
plot_interval = 100
# ウェイト初期化 (バイアスは簡単のため省略)
W_in = weight_init_std * np.random.randn(input_layer_size, hidden_layer_size)
W_out = weight_init_std * np.random.randn(hidden_layer_size, output_layer_size)
W = weight_init_std * np.random.randn(hidden_layer_size, hidden_layer_size)
# Xavier
# He
# 勾配
W_in_grad = np.zeros_like(W_in)
W_out_grad = np.zeros_like(W_out)
W_grad = np.zeros_like(W)
u = np.zeros((hidden_layer_size, binary_dim + 1))
z = np.zeros((hidden_layer_size, binary_dim + 1))
y = np.zeros((output_layer_size, binary_dim))
delta_out = np.zeros((output_layer_size, binary_dim))
delta = np.zeros((hidden_layer_size, binary_dim + 1))
all_losses = []
for i in range(iters_num):
# A, B初期化 (a + b = d)
a_int = np.random.randint(largest_number/2)
a_bin = binary[a_int] # binary encoding
b_int = np.random.randint(largest_number/2)
b_bin = binary[b_int] # binary encoding
# 正解データ
d_int = a_int + b_int
d_bin = binary[d_int]
# 出力バイナリ
out_bin = np.zeros_like(d_bin)
# 時系列全体の誤差
all_loss = 0
# 時系列ループ
for t in range(binary_dim):
# 入力値
X = np.array([a_bin[ - t - 1], b_bin[ - t - 1]]).reshape(1, -1)
# 時刻tにおける正解データ
dd = np.array([d_bin[binary_dim - t - 1]])
u[:,t+1] = np.dot(X, W_in) + np.dot(z[:,t].reshape(1, -1), W)
z[:,t+1] = functions.sigmoid(u[:,t+1])
y[:,t] = functions.sigmoid(np.dot(z[:,t+1].reshape(1, -1), W_out))
#誤差
loss = functions.mean_squared_error(dd, y[:,t])
delta_out[:,t] = functions.d_mean_squared_error(dd, y[:,t]) * functions.d_sigmoid(y[:,t])
all_loss += loss
out_bin[binary_dim - t - 1] = np.round(y[:,t])
for t in range(binary_dim)[::-1]:
X = np.array([a_bin[-t-1],b_bin[-t-1]]).reshape(1, -1)
delta[:,t] = (np.dot(delta[:,t+1].T, W.T) + np.dot(delta_out[:,t].T, W_out.T)) * functions.d_sigmoid(u[:,t+1])
# 勾配更新
W_out_grad += np.dot(z[:,t+1].reshape(-1,1), delta_out[:,t].reshape(-1,1))
W_grad += np.dot(z[:,t].reshape(-1,1), delta[:,t].reshape(1,-1))
W_in_grad += np.dot(X.T, delta[:,t].reshape(1,-1))
# 勾配適用
W_in -= learning_rate * W_in_grad
W_out -= learning_rate * W_out_grad
W -= learning_rate * W_grad
W_in_grad *= 0
W_out_grad *= 0
W_grad *= 0
if(i % plot_interval == 0):
all_losses.append(all_loss)
print("iters:" + str(i))
print("Loss:" + str(all_loss))
print("Pred:" + str(out_bin))
print("True:" + str(d_bin))
out_int = 0
for index,x in enumerate(reversed(out_bin)):
out_int += x * pow(2, index)
print(str(a_int) + " + " + str(b_int) + " = " + str(out_int))
print("------------")
lists = range(0, iters_num, plot_interval)
plt.plot(lists, all_losses, label="loss")
plt.show()
学習が進んでいることがわかる。
Section2 LSTM
BPTTの更新式や確認テストでも見た通り、単純なRNNでは時系列を追うごとにsigmoid関数などの活性化関数をデータが通るため、勾配が消失/爆発してしまう。そのため、長期間の時系列データの学習が困難であることが、課題としてあった。
それを解決するために提案されたのが、LSTM(Long Short Term Memory)である。
<確認テスト>
シグモイド関数を微分した時、入力値が0の時に最大値を取る。
その値として正しいモノを選択肢から選べ。
⇒(2)0.25
LSTMの構造
LSTMでは、RNNに加えて、「記憶ベクトル」(CEC)というモデル内部の隠れベクトルを導入し、この記憶ベクトルに対して、「入力ゲート」「忘却ゲート」「出力ゲート」を駆使して入力データの情報を記憶ベクトルに反映させながらデータを学習させることで、長期的な情報の保存を可能している。
CEC
勾配爆発/勾配消失の解決策として、勾配が1であれば理論上無限時間でも勾配が正しく伝播するという考え方に基づいている。
入力ゲートと出力ゲート
基本的に時系列データを学習させる際は、重要な情報であればユニットの値を更新し、不要な情報であればユニットの値をそのままにしておきたい。そこで、CECと同時にそれに対して入力ゲートと出力ゲートを設けることで、「ある時刻iの情報を(受け取る/出力するか)否か」が判断できるようにしている。
こうすることで、学習に効果的な情報のみでモデル内部の数値が更新されるようになっている。
忘却ゲート
しかし、このままでは、CEC内に過去の不要な情報が残り続けてしまう。そのため、過去の情報が要らなくなった時点でその情報を忘却するための機能として忘却ゲートが提案された。
<確認テスト>
以下の文章をLSTMに入力し空欄に当てはまる単語を予測したいとする。文中の「とても」という言葉は空欄の予測においてなくなっても影響を及ぼさないと考えられる。
このような場合、どのゲートが作用すると考えられるか。
文:「映画面白かったね。ところで、とてもお腹が空いたから何か_______。」
⇒忘却ゲート。不要な情報と判断した時点でそれを削除する。
覗き穴結合
CECに保存されている過去の情報を、任意のタイミングで他のノードに伝播させたり、あるいは任意のタイミングで忘却させたい。
覗き穴結合は、CEC自身の値に重み行列を介して伝播可能にした構造。
実装演習
PyTorchとTorchtextを使って、Livedoorニュースコーパスの分類モデルの実装。
(モデル構成部分の実装)
# モデル本体
import torch
import torchtext
import torch.nn as nn
import torch.optim as optim
from torch.autograd import Variable
import torch.nn.functional as F
class livedoor_classify(nn.Module):
def __init__(self, vocab_size, embed_dim, h_dim, num_class, batch_first=True):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
self.embedding.weight.data.copy_(TEXT.vocab.vectors) #重み行列の初期値はFasttextの学習済みモデル
#LSTM
self.rnn = nn.LSTM(embed_dim, h_dim, num_layers=1, batch_first=batch_first)
#GRU
self.rnn = nn.GRU(embed_dim, h_dim, num_layers=1, batch_first=batch_first)
#双方向LSTM
self.rnn = nn.LSTM(embed_dim, h_dim, num_layers=1, batch_first=batch_first, bidirectional=True)
#attention機構
self.attention = nn.Sequential(
nn.Linear(h_dim, da),
nn.ELU(alpha = 0.1),
nn.Linear(da, 1)
)
self.classify = nn.Sequential(
nn.Linear(h_dim, int(h_dim/4)),
nn.BatchNorm1d(int(h_dim/4)),
nn.Linear(int(h_dim/4),num_class)
)
self.init_weights()
def init_weights(self):
initrange = 1
self.fc1.weight.data.uniform_(-initrange, initrange)
self.fc1.bias.data.zero_()
self.fc2.weight.data.uniform_(-initrange, initrange)
self.fc2.bias.data.zero_()
def forward(self, text):
embedded = self.embedding(text) #引数は単語IDのテンソルと重み行列
rnn_output, hidden = self.rnn(embedded)
attention_weight = F.softmax(self.attention(rnn_output), dim = 1)
final_feature = (feature_output * feature_attention_weight).sum(dim = 1)
x = self.classify(final_feature)
x = torch.sigmoid(x)
return x
Section3 GRU
LSTMでは、パラメータ数が多く計算負荷が高いという問題点があった。
そこで、パラメータ数を減らしつつ、精度は同等以上のものを望めるようになった構造がGRU。
GRUでは、前時刻の情報の忘却と更新が一つの部分で行われるようにし、CECと覗き穴結合をなくした。
<確認テスト>
LSTMとCECが抱える課題について、それぞれ簡潔に述べよ。
⇒
LSTMはパラメータ数が多いため、計算負荷が大きい。
CECは、自身で学習機能を備えていないため、入力ゲート・出力ゲートをつける必要がある。
<確認テスト>
LSTMとGRUの違いを簡潔に述べよ。
⇒
パラメータ数に大きな違いがあり、GRUの方がパラメータ数が少ないため、計算負荷が小さい。
実装演習
(Section2に含まれる)
Section4 双方向RNN
過去の情報だけでなく、未来の情報も加味することで精度向上を目指した、RNNの派生形のモデル。
文章の推敲・機械翻訳・文書分類など、自然言語処理分野でも使われていることは多い。
双方向RNNは、RNNを2層に積み重ねた構造をしているが、これをさらに何層も積み重ねるなど、より巨大なモデルとして提案されていたりする。
実装演習
(Section2に含まれる)
Section5 seq2seq
seq2seqは、Encoder-Decoderモデルの一種。これも機械対話や機械翻訳等に使用されている。
Encoder RNN
入力のデータを時刻ごとに区切って順に入力していく。
例えば、「私は昨日お餅を食べました。」というテキストデータであれば、「私 / は / 昨日 / お餅 / を / 食べ / ました / 。」という風に区切って(形態素解析をして)順に入力していく。
この際、単語の状態では計算機で扱えないので、Embeddingと呼ばれる、単語を分散表現ベクトルに変換する処理をすることで固定次元のベクトルに変換する。
最後の単語を入れたときの状態ベクトル($h^{t=T^X}$)が入力データの意味を表したベクトルとみなせる。
Decoder RNN
アウトプットするデータを1トークンごとに生成する構造。
EncoderRNNの最後の出力をDecoderRNNの$h^{t=1}$の初期値とし、各トークン(テキストの場合であれば単語)の生成確率を出力していく。
一番生成確率が高かった単語のベクトルを、逆にEmbeddingでテキストの状態の単語に戻す。
そしてこれを次の時刻のDecoderRNNの入力とする。
<確認テスト>
下記の選択肢から、seq2seqについて説明しているものを選べ。
⇒(2)
HRED
seq2seqの課題として、一問一答しかできず、問いに対して文脈も何もなくただ応答が行われ続けるという問題があった。
その問題点を解決するために提案されたのがHRED。
HREDでは過去n-1個の発話から次の発話を生成する。seq2seqでは、会話の文脈無視で応答がなされたが、HREDでは前の単語の流れに即して応答されるため、より自然な文章の生成が期待できる。
HREDはseq2seq+Context RNNの構造をしている。
(※Context RNNは、Encoderのまとめた各文章の系列をまとめて、これまでの会話コンテキスト全体を表すベクトルに変換する構造。)
これによって、過去の発話の履歴を加味した返答をできる。
HREDの課題
HREDは確率的な多様性が字面にしかなく、会話の「流れ」のような多様性がない。
同じコンテキストを与えられても、答えの内容が毎回同じになりがち。
短く、情報量に乏しい答えをしがち。(ex.「うん」「そうだね」)
⇒どの会話でも出てくるけど文脈は合ってる、ようなただの相槌のような返答ばかりを返すようになってしまう。
VHRED
VHREDとは、HREDにVAEの潜在変数の概念を追加したもの。
こうすることで、HREDの課題であった、会話の多様性の無さを解決しようとした。
<確認テスト>
seq2seqとHRED、HREDとVHREDの違いを簡潔に述べよ。
⇒seq2seqは会話の文脈を無視した応答をするといった課題があったが、HREDは文脈の流れを考慮して自然な返答をする。
HREDは短く情報量の乏しいような返事しかできない課題があったが、VHREDはHREDの課題をVAEの潜在変数の概念を追加して解決した。
オートエンコーダ
オートエンコーダとは、教師なし学習の一つ。
学習時の入力データは訓練データのみで、教師データは使用しない。
オートエンコーダは、入力データをEncoderを通して次元数を削減しつつ、全結合ニューラルネットワークで少数次元に圧縮する。そして今度は次元数がだんだん増えていく全結合ニューラルネットワークであるDecoderに通して、元のデータと同じになるように学習する。
このモデルの学習が進むと、元のデータの情報を少数次元で表現できることになるので、次元削減が行えることになる。
VAE
通常のオートエンコーダの場合、少数次元の潜在変数にデータの情報を圧縮で来ているが、その中身はわからない。VAEは、この潜在変数に標準正規分布を仮定したもの。
<確認テスト>
VAEに関する下記の説明文中の空欄に当てはまる言葉を答えよ。
自己符号化器の潜在変数に_______を導入したもの。
⇒確率分布(標準正規分布)
実装演習
PyTorchを使って、MNISTをAutoEncoderで学習させてみる。
import torch
import torchvision
import torchvision.transforms as transforms
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import random
import time
transform = transforms.Compose(
[transforms.ToTensor(),
])
dataset = torchvision.datasets.MNIST(root='./data', train=True,
download=True, transform=transform)
batch_size = 128
# データローダー
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,
shuffle=True, num_workers=6)
class Encoder(nn.Module):
def __init__(self, input_dim, z_dim):
super(Encoder, self).__init__()
self.input_dim = input_dim
self.encoder = nn.Sequential(
nn.Linear(input_dim, int(input_dim/2)),
nn.BatchNorm1d(int(input_dim/2)),
nn.LeakyReLU(0.2),
nn.Linear(int(input_dim/2), int(input_dim/4)),
nn.BatchNorm1d(int(input_dim/4)),
nn.LeakyReLU(0.2),
nn.Linear(int(input_dim/4), int(input_dim/8)),
nn.BatchNorm1d(int(input_dim/8)),
nn.LeakyReLU(0.2),
nn.Linear(int(input_dim/8), z_dim),
)
self.init_weights()
def init_weights(self):
for module in self.modules():
if isinstance(module, nn.Linear):
nn.init.kaiming_uniform_(module.weight) #Heの初期値
#nn.init.xavier_uniform_(module.weight) #xavierの初期値
module.bias.data.zero_()
def forward(self, input_data):
x = input_data.view(-1, self.input_dim)
x = self.encoder(x)
return x
class Decoder(nn.Module):
def __init__(self, z_dim, output_dim):
super(Decoder, self).__init__()
self.decoder = nn.Sequential(
nn.Linear(z_dim, int(output_dim/8)),
nn.BatchNorm1d(int(output_dim/8)),
nn.LeakyReLU(0.2),
nn.Linear(int(output_dim/8), int(output_dim/4)),
nn.BatchNorm1d(int(output_dim/4)),
nn.LeakyReLU(0.2),
nn.Linear(int(output_dim/4), int(output_dim/2)),
nn.BatchNorm1d(int(output_dim/2)),
nn.LeakyReLU(0.2),
nn.Linear(int(output_dim/2), output_dim),
)
self.init_weights()
def init_weights(self):
for module in self.modules():
if isinstance(module, nn.Linear):
nn.init.kaiming_uniform_(module.weight) #Heの初期値
#nn.init.xavier_uniform_(module.weight) #xavierの初期値
module.bias.data.zero_()
def forward(self, z):
x = self.decoder(z)
x = x.view(-1, 28, 28)
return x
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def train_func(Encoder, Decoder, batch_size, criterion, optimizer, dataloader, device):
#訓練モード
Encoder.train()
Decoder.train()
#lossの初期化
run_loss = 0
#バッチごとの計算
for batch_idx, (data, labels) in enumerate(dataloader):
#バッチサイズに満たない場合は無視
if data.size()[0] != batch_size:
break
img = data.to(device)
optimizer.zero_grad()
#順伝播
z = Encoder(img)
output = Decoder(z)
#loss計算
loss = criterion(img.squeeze(), output)
loss.backward()
optimizer.step()
run_loss += loss
run_loss /= len(dataloader)
return run_loss
def model_run(num_epochs, batch_size = batch_size, dataloader = dataloader, device = device):
#パラメータ
input_dim = 28*28
z_dim = 2
#モデル定義
E = Encoder(input_dim, z_dim).to(device)
D = Decoder(z_dim, input_dim).to(device)
#lossの定義
criterion = nn.MSELoss().to(device)
#optimizerの定義
optimizer = torch.optim.Adam(list(E.parameters()) + list(D.parameters()), lr=0.002, betas=(0.5, 0.999), eps=1e-08, weight_decay=0, amsgrad=False)
loss_list = []
for epoch in range(num_epochs):
start_time = time.time()
run_loss = train_func(E, D, batch_size, criterion, optimizer, dataloader, device)
loss_list.append(run_loss)
secs = int(time.time() - start_time)
mins = secs / 60
secs = secs % 60
#エポックごとに結果を表示
print('Epoch: %d' %(epoch + 1), " | 所要時間 %d 分 %d 秒" %(mins, secs))
print(f'\tLoss: {run_loss:.4f}')
return loss_list, E, D
loss_list, E, D = model_run(num_epochs = 10)
lossのグラフは以下。
潜在変数の分布をラベルで色分けした図が以下。
ラベルごとにある程度分布が異なっていることがわかる。
Section6 Word2Vec
RNNでは、単語のような可変長の文字列を入力として与えることはできないといった課題があった。
そこで、単語などのデータを固定長のベクトルとして扱えるようにするために、単語の分散表現を獲得するための方法がword2vecである。
メリット
大規模データの分散表現の学習が、現実的な計算速度とメモリ量で実現可能にした。
×:ボキャブラリ×ボキャブラリだけの重み行列が誕生。
○:ボキャブラリ×任意の単語ベクトル次元で重み行列が誕生。
実装演習
このWord2Vecを使うと、単語をベクトルに変換できるため、類似単語の検索などができる。
gensimと呼ばれるライブラリを使って、それを実装してみる。
import gensim
simple_model = gensim.models.KeyedVectors.load_word2vec_format('./entity_vector/jawiki.entity_vectors.300d.txt', binary = False)
simple_model.most_similar('数学')
結果は以下。
[('物理学', 0.8179923295974731),
('代数学', 0.8083963990211487),
('解析学', 0.7909031510353088),
('幾何学', 0.7880539894104004),
('数論', 0.7865225076675415),
('微分積分学', 0.7695502638816833),
('代数幾何', 0.7572118639945984),
('論理学', 0.7539937496185303),
('代数的整数論', 0.7533139586448669),
('線形代数', 0.7531590461730957)]
しっかり関係ありそうな単語が上位に出てきている。
Section7 Attention Mechanism
seq2seqは長い文章への対応が課題となっていた。文章の長さに関係なく、EncoderRNNで固定次元のベクトルにしなければならないため、長い文章の情報をうまく圧縮できていなかった。
それを解決するため、入力と出力のどの単語が関連しているのかを学習するのがAttention。
<確認テスト>
RNNとword2vec、seq2seqとAttentionの違いを簡潔に述べよ。
⇒RNNは時系列データの学習に適したニューラルネットワークの一種。word2vecは単語の分散表現ベクトルを得るための手法で、RNNに文章を入力する前のEmbeddingに該当する。
seq2seqは1つの時系列データから別の時系列データを得るニューラルネットワークで、Encoder-Decoderモデルの一種。Attentionは、時系列データ(seq2seqでは、RNNの出力)の中身で相互の関連度に重み付けして、より高度な文章の特徴を得るための手法。
実装演習
(Section2に含まれる)