E資格学習 深層学習 Day4 ⑤Transformer
RNNとSeq2Seqのおさらい
- RNNは系列情報を内部状態ベクトルに変換する。t-1までの単語が来た時に、tにくる単語が何かを確率分布(事後確率)で出力する。始めの単語(Start of Sentence)の予測や、先頭単語を与えるこにより、文章を生成することが可能。
- Seq2Seqでは、Encoder RNNとDecoder RNNの2つが連結している。Encoder → 潜在変数(h)→ Decoderとして内部状態ベクトルhとして集約される。Decoderに正解を持たせることで教師あり学習ができる。
- 入力データ数の大小に関わらず、内部状態ベクトルは固定長という点に注意。長くなればなるほど、BLEU(評価値)が小さくなってしまう。そのため、長い系列長データの学習に弱いという特長がある。
Transformer
- 固定長ベクトル変換であるために、長い系列長の場合に精度がなかなか出ないという弱点を解決したAttention機構のみを使ったモデル。
- Encoder、Decoder双方にRNNを全く使っていない。
- 位置情報についてはPositional Encodingによって付加。
- Encoderでは、Positional Encodingによる位置情報付与→Dotprodcut Attention(Self Attention)→全結合層
- Decoderでは、Positional Encoding→未来の単語を見ないようなマスク処理(Masked Multihead Attention)をSelf Attentionで実施→ Encoderの出力とまとめてSource Target Attention →全結合層
Attention
- 足すと1になる重みを各系列データ毎に割り当てることで、入力値のどの部分に注目するのかを、注目する度合いを分散する仕組み。
- Self-Attention(自己注意)とSource Target Attentionの違い:Source Targetでは、Queryにターゲット、Key, Valueはソースデータを使う。自己注意ではQuery Key Valueは全て同じ情報が与えられる。
- Mulihead Attentionでは、Scaled Dot Attentionを8個分concatする。
Positional 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)
Scaled Dot Product Attention
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
Multihead 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