Transformerのメモ

Transformer

- Transformerの特徴のひとつは、学習を並列実行させることができることである。

  • RNNでは、時系列毎に結果を渡していく必要があるためシーケンシャルに処理をする必要がある。

- Transformerでは、並列学習させるため、以下の機構がある。

- Encoderへの入力は、データを配列で一気に渡す。(実際には、ミニバッチ学習させるため、「バッチ数 x 単語数 x 単語ベクトル次元数」となる。)
- 単語の順番で文章ベクトルを計算するのではなく、Attentionで単語間の関連を見て文章ベクトル化する。
- 但し、単語の順番を考慮しないと、文章の意味が変わる場合があるので、位置情報を単語ベクトルに付与して、文章ベクトルに入れていく。
- Multi-Head Attentionで、複数のAttentionを使い複数の関連を計算する。
- Multi-Head Attentionの層を複数重ねるためFeedForward(全結合)を入れる。
- Decorderでも、学習時にはデータを一気に渡すため、先の結果(予測結果の次の単語)を見ないようにマスクして、Attentionで関連を計算する。(Masked Multi-Head Attention)
- Multi-Head Attentionは、上記の「バッチ数 x 単語数 x 単語ベクトル次元数」のうちの「単語ベクトル次元数」をヘッド数で割って、各ヘッド毎に計算して、concatする。

 

参考情報

Attentionのメモ

Attention

  • Attentionは、大きく分けて2種類ある。
  • seq2seqで使用するSource-Target-Attentionと、自分自身に対するSelf-Attention
  • Attentionは、Mapからキーを元に値を選択する処理である。
  • どこに注意すればよいかを、キーから選択させるようにする。
  • よい選択ができるようにMapデータを学習させる。
  • 学習させるためには、勾配計算するため、微分できる必要がある。
  • そのため、単なるMapの選択操作をするのではなく、キーから割合をまとめて、値と掛け合わせて合算する。
  • その結果を元に損失関数で計算し、よい値になるようにMapを調整していく。
  • Mapのキーは、内積計算を行い類似度の高いものを高くする。
  • 類似度が高いものが、結果的に損失関数の評価がよくなるように調整していく。

Selft-Attention

  • Selft-Attentionは、自分自身のシーケンスデータの関連度を取得する。
  • CNN(畳み込みネットワーク)の場合は、フィルタ(カーネル)サイズの範囲で、評価を行う。
  • フィルタサイズが上限となるが、Attentionの場合は、シーケンスデータの全体を評価することができる。
  • CNNの場合、学習はよいフィルタを作りあげることだが、Attentionの場合は、よいMapを作りあげることである。
  • 自然言語処理のAttentionの場合、キーは単語ベクトルとなるため、単語ベクトルの分散表現の類似度が調整される。

参考情報

seq2seq

PyTorchでSeq2Seqを実装してみた を試してみる。

  • 引き算する数式を文字列で認識して、結果を出力する。
  • データはランダムに数字を生成して、実際に引き算して作成できるから簡単に用意できる。
  • seq2seqは、EncoderとDecoderの2種類のネットワークを作成して結合し、それぞれを学習させる。
  • Decoderは、学習時と推論時でロジックが変わる。
    • 学習時には、結果も時系列がわかっているため、次の時系列のデータを入力することができる。
    • 但し、LSTMの出力hは、次に渡していく必要がある。
    • 推論時には、Linearした後、SoftMaxまたは、argmaxで、文字を確定させ、それを次のLSTMに渡す。
    • それをまたEmbedingし単語ベクトル化して処理する。
  • ミニバッチ学習を行うため、学習時はバッチ数の配列操作となる。
        # Decoderで使うデータはoutput_tensorを1つずらしたものを使う
        # Decoderのインプットとするデータ
        source = output_tensor[:, :-1]

        # Decoderの教師データ
        # 生成開始を表す"_"を削っている
        target = output_tensor[:, 1:]
        for j in range(decoder_output.size()[1]):
            # バッチ毎にまとめてloss計算
            # 生成する文字は4文字なので、4回ループ
            loss += criterion(decoder_output[:, j, :], target[:, j])

学習としては、以下のような感じで重みが計算されているはず。

  • LSTMの重み
    • Encorder
    • 計算式として、どの文字列の単語ベクトルを強く残すか
  • Embeding (単語ベクトル)
    • 0-9の数字の文字と、「 」、「-」、「_」の合計13種類をいかにうまく分散表現させて、計算しているように見せれるか

以下、コードを貼り付け

import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim

from sklearn.model_selection import train_test_split
import random
from sklearn.utils import shuffle

# 数字の文字をID化
char2id = {str(i): i for i in range(10)}

# 空白(10):系列の長さを揃えるようのパディング文字
# -(11):マイナスの文字
# _(12):系列生成開始を知らせる文字
char2id.update({" ": 10, "-": 11, "_": 12})

# 空白込みの3桁の数字をランダムに生成


def generate_number():
    number = [random.choice(list("0123456789"))
              for _ in range(random.randint(1, 3))]
    return int("".join(number))


# 確認
print(generate_number())
# 753

# 系列の長さを揃えるために空白パディング


def add_padding(number, is_input=True):
    number = "{: <7}".format(number) if is_input else "{: <5s}".format(number)
    return number


# 確認
num = generate_number()
print("\"" + str(add_padding(num)) + "\"")
# "636    "
# 7

# データ準備
input_data = []
output_data = []

# データを50000件準備する
while len(input_data) < 50000:
    x = generate_number()
    y = generate_number()
    z = x - y
    input_char = add_padding(str(x) + "-" + str(y))
    output_char = add_padding("_" + str(z), is_input=False)

    # データをIDにに変換
    input_data.append([char2id[c] for c in input_char])
    output_data.append([char2id[c] for c in output_char])

# 確認
print(input_data[987])
print(output_data[987])
# [1, 5, 11, 2, 6, 6, 10] (←"15-266")
# [12, 11, 2, 5, 1] (←"_-251")

# 7:3にデータをわける
train_x, test_x, train_y, test_y = train_test_split(
    input_data, output_data, train_size=0.7)


# データをバッチ化するための関数
def train2batch(input_data, output_data, batch_size=100):
    input_batch = []
    output_batch = []
    input_shuffle, output_shuffle = shuffle(input_data, output_data)
    for i in range(0, len(input_data), batch_size):
        input_batch.append(input_shuffle[i:i+batch_size])
        output_batch.append(output_shuffle[i:i+batch_size])
    return input_batch, output_batch


embedding_dim = 200  # 文字の埋め込み次元数
hidden_dim = 128  # LSTMの隠れ層のサイズ
vocab_size = len(char2id)  # 扱う文字の数。今回は13文字

# GPU使う用
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Encoderクラス


class Encoder(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim):
        super(Encoder, self).__init__()
        self.hidden_dim = hidden_dim
        self.word_embeddings = nn.Embedding(
            vocab_size, embedding_dim, padding_idx=char2id[" "])
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)

    def forward(self, sequence):
        embedding = self.word_embeddings(sequence)
        # Many to Oneなので、第2戻り値を使う
        _, state = self.lstm(embedding)
        # state = (h, c)
        return state


# Decoderクラス


class Decoder(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim):
        super(Decoder, self).__init__()
        self.hidden_dim = hidden_dim
        self.word_embeddings = nn.Embedding(
            vocab_size, embedding_dim, padding_idx=char2id[" "])
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)
        # LSTMの128次元の隠れ層を13次元に変換する全結合層
        self.hidden2linear = nn.Linear(hidden_dim, vocab_size)

    def forward(self, sequence, encoder_state):
        embedding = self.word_embeddings(sequence)
        # Many to Manyなので、第1戻り値を使う。
        # 第2戻り値は推論時に次の文字を生成するときに使います。
        output, state = self.lstm(embedding, encoder_state)
        output = self.hidden2linear(output)
        return output, state


# GPU使えるように。
encoder = Encoder(vocab_size, embedding_dim, hidden_dim).to(device)
decoder = Decoder(vocab_size, embedding_dim, hidden_dim).to(device)

# 損失関数
criterion = nn.CrossEntropyLoss()

# 最適化
encoder_optimizer = optim.Adam(encoder.parameters(), lr=0.001)
decoder_optimizer = optim.Adam(decoder.parameters(), lr=0.001)

BATCH_NUM = 100
EPOCH_NUM = 100

all_losses = []
print("training ...")
for epoch in range(1, EPOCH_NUM+1):
    epoch_loss = 0  # epoch毎のloss

    # データをミニバッチに分ける
    input_batch, output_batch = train2batch(
        train_x, train_y, batch_size=BATCH_NUM)

    for i in range(len(input_batch)):

        # 勾配の初期化
        encoder_optimizer.zero_grad()
        decoder_optimizer.zero_grad()

        # データをテンソルに変換
        input_tensor = torch.tensor(input_batch[i], device=device)
        output_tensor = torch.tensor(output_batch[i], device=device)

        # Encoderの順伝搬
        encoder_state = encoder(input_tensor)

        # Decoderで使うデータはoutput_tensorを1つずらしたものを使う
        # Decoderのインプットとするデータ
        source = output_tensor[:, :-1]

        # Decoderの教師データ
        # 生成開始を表す"_"を削っている
        target = output_tensor[:, 1:]

        loss = 0
        # 学習時はDecoderはこのように1回呼び出すだけでグルっと系列をループしているからこれでOK
        # sourceが4文字なので、以下でLSTMが4回再帰的な処理してる
        decoder_output, _ = decoder(source, encoder_state)
        # decoder_output.size() = (100,4,13)
        # 「13」は生成すべき対象の文字が13文字あるから。decoder_outputの3要素目は
        # [-14.6240,  -3.7612, -11.0775,  ...,  -5.7391, -15.2419,  -8.6547]
        # こんな感じの値が入っており、これの最大値に対応するインデックスを予測文字とみなす

        for j in range(decoder_output.size()[1]):
            # バッチ毎にまとめてloss計算
            # 生成する文字は4文字なので、4回ループ
            loss += criterion(decoder_output[:, j, :], target[:, j])

        epoch_loss += loss.item()

        # 誤差逆伝播
        loss.backward()

        # パラメータ更新
        # Encoder、Decoder両方学習
        encoder_optimizer.step()
        decoder_optimizer.step()

    # 損失を表示
    print("Epoch %d: %.2f" % (epoch, epoch_loss))

    all_losses.append(epoch_loss)
    if epoch_loss < 1:
        break

print("学習終了")

# 推論
# Decoderのアウトプットのテンソルから要素が最大のインデックスを返す。つまり生成文字を意味する


def get_max_index(decoder_output):
    results = []
    for h in decoder_output:
        results.append(torch.argmax(h))
    return torch.tensor(results, device=device).view(BATCH_NUM, 1)


# 評価用データ
test_input_batch, test_output_batch = train2batch(test_x, test_y)
input_tensor = torch.tensor(test_input_batch, device=device)

predicts = []
for i in range(len(test_input_batch)):
    with torch.no_grad():  # 勾配計算させない
        encoder_state = encoder(input_tensor[i])

        # Decoderにはまず文字列生成開始を表す"_"をインプットにするので、"_"のtensorをバッチサイズ分作成
        start_char_batch = [[char2id["_"]] for _ in range(BATCH_NUM)]
        decoder_input_tensor = torch.tensor(start_char_batch, device=device)

        # 変数名変換
        decoder_hidden = encoder_state

        # バッチ毎の結果を結合するための入れ物を定義
        batch_tmp = torch.zeros(100, 1, dtype=torch.long, device=device)
        # print(batch_tmp.size())
        # (100,1)

        for _ in range(5):
            decoder_output, decoder_hidden = decoder(
                decoder_input_tensor, decoder_hidden)
            # 予測文字を取得しつつ、そのまま次のdecoderのインプットとなる
            decoder_input_tensor = get_max_index(decoder_output.squeeze())
            # バッチ毎の結果を予測順に結合
            batch_tmp = torch.cat([batch_tmp, decoder_input_tensor], dim=1)

        # 最初のbatch_tmpの0要素が先頭に残ってしまっているのでスライスして削除
        predicts.append(batch_tmp[:, 1:])

# バッチ毎の予測結果がまとまって格納されてます。
print(len(predicts))
# 150
print(predicts[0].size())
# (100, 5)

# 表示
id2char = {str(i): str(i) for i in range(10)}
id2char.update({"10": "", "11": "-", "12": ""})
row = []
for i in range(len(test_input_batch)):
    batch_input = test_input_batch[i]
    batch_output = test_output_batch[i]
    batch_predict = predicts[i]
    for inp, output, predict in zip(batch_input, batch_output, batch_predict):
        x = [id2char[str(idx)] for idx in inp]
        y = [id2char[str(idx)] for idx in output]
        p = [id2char[str(idx.item())] for idx in predict]

        x_str = "".join(x)
        y_str = "".join(y)
        p_str = "".join(p)

        judge = "O" if y_str == p_str else "X"
        row.append([x_str, y_str, p_str, judge])
predict_df = pd.DataFrame(row, columns=["input", "answer", "predict", "judge"])

# 正解率を表示
print(len(predict_df.query('judge == "O"')) / len(predict_df))
# 0.8492
# 間違えたデータを一部見てみる
print(predict_df.query('judge == "X"').head(10))

RNN(LSTM)を使ってみる

今日は、以下を参考にLSTMを動かしてみる。

qiita.com

PyTorchを使って、「livedoorニュースコーパス」のニュース記事のタイトルの分類を行う。

データの読み込みには、Pandasを使い、形態素解析には、Mecabを使う。

PyTorchのデータ読み込みは、DataLoaderやDatasetがあった気がするがまぁいいだろう。

なので、まず以下のセットアップを行う。

 

pip install pandas
pip install torch
pip install mecab
pip install sklearn

LSTMとしては、PyTorchを使うので特に難しいことはない。使うだけなら。。

最終的に分類するので、LSTMを通した結果の文章ベクトルをLinearに通して、Softmaxで確率に落とせばよい。

単語の分散表現(単語ベクトル)は、以下でランダムに初期化して、学習させる。

nn.Embedding(vocab_size, embedding_dim)

今回のネットワークでは、この単語ベクトルとLSTMの重みが学習されていくはず。

単語ベクトルの分散具合と分類の分散具合が同じ方向になるのと、LSTMで隠れ層を伝わっていく時にどんな分散を強く伝えていけばよいかが学習されているのであろう。

 

使ったコードをコピペ

 

import os
from glob import glob
import pandas as pd
import linecache
import MeCab
import re

import torch
import torch.nn as nn

from sklearn.model_selection import train_test_split
import torch.optim as optim

# カテゴリを配列で取得
categories = [name for name in os.listdir(
    "text") if os.path.isdir("text/" + name)]
# print(categories)
# ['movie-enter', 'it-life-hack', 'kaden-channel', 'topic-news', 'livedoor-homme', 'peachy', 'sports-watch', 'dokujo-tsushin', 'smax']

datasets = pd.DataFrame(columns=["title", "category"])
for cat in categories:
    path = "text/" + cat + "/*.txt"
    files = glob(path)
    for text_name in files:
        title = linecache.getline(text_name, 3)
        s = pd.Series([title, cat], index=datasets.columns)
        datasets = datasets.append(s, ignore_index=True)

# データフレームシャッフル
datasets = datasets.sample(frac=1).reset_index(drop=True)
# print(datasets.head())

tagger = MeCab.Tagger("-Owakati")


def make_wakati(sentence):
    # MeCab分かち書き
    sentence = tagger.parse(sentence)
    # 半角全角英数字除去
    sentence = re.sub(r'[0-90-9a-zA-Za-zA-Z]+', " ", sentence)
    # 記号もろもろ除去
    sentence = re.sub(
        r'[\._-―─!@#$%^&\-‐|\\*\“()_■×+α※÷⇒—●★☆〇◎◆▼◇△□(:〜~+=)/*&^%$#@!~`){}[]…\[\]\"\'\”\’:;<>?<>〔〕〈〉?、。・,\./『』【】「」→←○《》≪≫\n\u3000]+', "", sentence)
    # スペースで区切って形態素の配列へ
    wakati = sentence.split(" ")
    # 空の要素は削除
    wakati = list(filter(("").__ne__, wakati))
    return wakati


# 単語ID辞書を作成する
word2index = {}
for title in datasets["title"]:
    wakati = make_wakati(title)
    for word in wakati:
        if word in word2index:
            continue
        word2index[word] = len(word2index)
print("vocab size : ", len(word2index))


def sentence2index(sentence):
    wakati = make_wakati(sentence)
    return torch.tensor([word2index[w] for w in wakati], dtype=torch.long)


category2index = {}
for cat in categories:
    if cat in category2index:
        continue
    category2index[cat] = len(category2index)
print(category2index)
#{'movie-enter': 0, 'it-life-hack': 1, 'kaden-channel': 2, 'topic-news': 3, 'livedoor-homme': 4, 'peachy': 5, 'sports-watch': 6, 'dokujo-tsushin': 7, 'smax': 8}


def category2tensor(cat):
    return torch.tensor([category2index[cat]], dtype=torch.long)


class LSTMClassifier(nn.Module):
    # モデルで使う各ネットワークをコンストラクタで定義
    def __init__(self, embedding_dim, hidden_dim, vocab_size, tagset_size):
        # 親クラスのコンストラクタ。決まり文句
        super(LSTMClassifier, self).__init__()
        # 隠れ層の次元数。これは好きな値に設定しても行列計算の過程で出力には出てこないので。
        self.hidden_dim = hidden_dim
        # インプットの単語をベクトル化するために使う
        self.word_embeddings = nn.Embedding(vocab_size, embedding_dim)
        # LSTMの隠れ層。これ1つでOK。超便利。
        self.lstm = nn.LSTM(embedding_dim, hidden_dim)
        # LSTMの出力を受け取って全結合してsoftmaxに食わせるための1層のネットワーク
        self.hidden2tag = nn.Linear(hidden_dim, tagset_size)
        # softmaxのLog版。dim=0で列、dim=1で行方向を確率変換。
        self.softmax = nn.LogSoftmax(dim=1)

    # 順伝播処理はforward関数に記載
    def forward(self, sentence):
        # 文章内の各単語をベクトル化して出力。2次元のテンソル
        embeds = self.word_embeddings(sentence)
        # 2次元テンソルをLSTMに食わせられる様にviewで3次元テンソルにした上でLSTMへ流す。
        # 上記で説明した様にmany to oneのタスクを解きたいので、第二戻り値だけ使う。
        _, lstm_out = self.lstm(embeds.view(len(sentence), 1, -1))
        # lstm_out[0]は3次元テンソルになってしまっているので2次元に調整して全結合。
        tag_space = self.hidden2tag(lstm_out[0].view(-1, self.hidden_dim))
        # softmaxに食わせて、確率として表現
        tag_scores = self.softmax(tag_space)
        return tag_scores


# 元データを7:3に分ける(7->学習、3->テスト)
traindata, testdata = train_test_split(datasets, train_size=0.7)

# 単語のベクトル次元数
EMBEDDING_DIM = 10
# 隠れ層の次元数
HIDDEN_DIM = 128
# データ全体の単語数
VOCAB_SIZE = len(word2index)
# 分類先のカテゴリの数
TAG_SIZE = len(categories)
# モデル宣言
model = LSTMClassifier(EMBEDDING_DIM, HIDDEN_DIM, VOCAB_SIZE, TAG_SIZE)
# 損失関数はNLLLoss()を使う。LogSoftmaxを使う時はこれを使うらしい。
loss_function = nn.NLLLoss()
# 最適化の手法はSGDで。lossの減りに時間かかるけど、一旦はこれを使う。
optimizer = optim.SGD(model.parameters(), lr=0.01)

# 各エポックの合計loss値を格納する
losses = []
# 100ループ回してみる。(バッチ化とかGPU使ってないので結構時間かかる...)
for epoch in range(100):
    all_loss = 0
    for title, cat in zip(traindata["title"], traindata["category"]):
        # モデルが持ってる勾配の情報をリセット
        model.zero_grad()
        # 文章を単語IDの系列に変換(modelに食わせられる形に変換)
        inputs = sentence2index(title)
        # 順伝播の結果を受け取る
        out = model(inputs)
        # 正解カテゴリをテンソル化
        answer = category2tensor(cat)
        # 正解とのlossを計算
        loss = loss_function(out, answer)
        # 勾配をセット
        loss.backward()
        # 逆伝播でパラメータ更新
        optimizer.step()
        # lossを集計
        all_loss += loss.item()
    losses.append(all_loss)
    print("epoch", epoch, "\t", "loss", all_loss)
print("done.")

ゼロから作るDeep Learning2(その3)seq2seq

「7章 RNNによる文章生成」のメモ。

この章では、seq2seqと呼ばれる、時系列データから別の時系列データへ変換するニューラルネットワークを扱う。自然言語処理だと、翻訳や予約等に使われ、Encoder-Decoderモデルとも言う。

 

Encoder

 単語列の分散表現から文章の分散表現を生成する。

 

Decoder

 これまで与えられた単語から、次に出現する単語の確率分布を出力する。

 

確率分布から一番高いものを必ず選択することを決定的選択という。
(同じデータであれば、確率も同じなので、次に選択する単語は一意に決まる)

それに対して、確率的選択は、確率分布を使って、サンプリングして次に採用する単語を決める。
「1%の確率の単語でも、100回やれば1回ぐらいは選択される。毎回結果が変わる。(変わらない場合もある。)」

基本的に決定的なアルゴリズムを使ってしまうと、結果が一意に決まってしまうので、学習させにくい気がする。

のちに出てくるAttentionのキーからバリューを引く一連の処理は、決定的なのか確率的なのか。

 

ソースコードリーディング時によく出る単語

predict(): 単語のスコアを出す
choice(): スコアからサンプリングする

 

「7.2.2 時系列データ変換用のトイ・プロブレム」では、足し算をseq2seqで解く。

 

実際に足し算を計算しているわけではなく、数字の単語の分散表現の値でパターンを予測して出力している。

ある数字の次に「+」がきて、その次にある数字の単語がくるという文章の分散表現がEncoderで作られる。
※このネットワークでは、1桁ずつの数字がそれぞれ単語になる。

このネットワークの学習のポイントは、
数字の分散表現が実際計算する足し算と同じようなベクトルにうまくなるように学習されるはず。
(Embedding層の学習)

LSTMの重み付けがどう学習されていくかは、イメージがつきにくい。
数字と+記号の違いがうまく伝搬するように学習されていくのかもしれないが、
数字の列の大きさが固定ではないので、そうもいかない。
数字列 「+」 数字列 の並びでくるので、最初の数字単語の分散表現を最後までちゃんと
伝搬させた方がよいのだろうからそういう重み付けになるのかな。
で、「入力データの反転」で数字の桁を判定させている。
出力に近い方が(同じ重みつけなら)伝搬に有利なので、桁が大きい方を優先させた方がよいので
そのようになるのであろう。


例えば、人間なら、791305 + 3462 を計算する場合に、
なんとなく、 790000 + 3500 ぐらいの数になるだろうと予測して計算しているのに似ている。

 

■RNNの学習と推論の違い

RNNで文章生成を行う場合に、学習時と推論時ではデータの与え方が違う。
学習時は正解がわかっているため、時系列方向のデータをまとめて与えることができる。(なので、Decoderで正解データの単語列からLSTMの重みを学習させることができる。)
しかし、推論時には、次にくる未来のデータ(次の単語の確率)はわからないため、
次に出現する単語のサンプリングを次々に行っていく必要がある。

 

Attentionでは、時系列ではなく、まとめて与えるため、未来のデータがわからないようにMaskする。