LoginSignup
3
2

More than 3 years have passed since last update.

【第8章】言語処理100本ノックでPyTorchに入門

Last updated at Posted at 2021-03-20

この章では言語処理100本ノック第8章を使ってPyTorchに入門します。ニューラルネットの解説は少なめです。

78番でGPUの利用が求められるので用意しておきましょう。私はGoogle Colaboratoryを使ってます。

記事執筆のタイミングでPyTorchは 1.8になってました。また公式で新たなチュートリアルLearn the Basicsが公開されたようなのでこれを参考にすると良いでしょう。

そしてまずは70番に備えて6章と7章のデータを用意しておきます。7章のデータのダウンロードはこちらの記事のやり方そのままです。

!wget https://archive.ics.uci.edu/ml/machine-learning-databases/00359/NewsAggregatorDataset.zip
import csv
import zipfile

import pandas as pd
from sklearn.model_selection import train_test_split

with zipfile.ZipFile("NewsAggregatorDataset.zip") as z:
    with z.open("newsCorpora.csv") as f:
        names = ('ID','TITLE','URL','PUBLISHER','CATEGORY','STORY','HOSTNAME','TIMESTAMP')
        df = pd.read_table(f, names=names, quoting=csv.QUOTE_NONE)

publisher_set = {"Reuters", "Huffington Post", "Businessweek", "Contactmusic.com", "Daily Mail"}
df = df[df['PUBLISHER'].isin(publisher_set)]
train, valid_test = train_test_split(df, train_size=0.8, random_state=0)
valid, test = train_test_split(valid_test, test_size=0.5, random_state=0)
FILE_ID = "0B7XkCwpI5KDYNlNUTTlSS21pQmM"
FILE_NAME = "GoogleNews-vectors-negative300.bin.gz"
!wget --load-cookies /tmp/cookies.txt "https://docs.google.com/uc?export=download&confirm=$(wget --quiet --save-cookies /tmp/cookies.txt --keep-session-cookies --no-check-certificate 'https://docs.google.com/uc?export=download&id=$FILE_ID' -O- | sed -rn 's/.*confirm=([0-9A-Za-z_]+).*/\1\n/p')&id=$FILE_ID" -O $FILE_NAME && rm -rf /tmp/cookies.txt
from gensim.models import KeyedVectors

word_vectors = KeyedVectors.load_word2vec_format('./GoogleNews-vectors-negative300.bin.gz', binary=True)

70. 単語ベクトルの和による特徴量

問題50で構築した学習データ,検証データ,評価データを行列・ベクトルに変換したい.例えば,学習データについて,すべての事例$x_i$の特徴ベクトル$\boldsymbol{x}_i$を並べた行列$X$と,正解ラベルを並べた行列(ベクトル)$Y$を作成したい.

$$
X = \begin{pmatrix}
\boldsymbol{x}_1 \\
\boldsymbol{x}_2 \\
\dots \\
\boldsymbol{x}_n \\
\end{pmatrix} \in \mathbb{R}^{n \times d},
Y = \begin{pmatrix}
y_1 \\
y_2 \\
\dots \\
y_n \\
\end{pmatrix} \in \mathbb{N}^{n}
$$

ここで,$n$は学習データの事例数であり,$\boldsymbol{x}_i \in \mathbb{R}^d$と$y_i \in \mathbb{N}$はそれぞれ,$i \in {1, \dots, n}$番目の事例の特徴量ベクトルと正解ラベルを表す.
なお,今回は「ビジネス」「科学技術」「エンターテイメント」「健康」の4カテゴリ分類である.$\mathbb{N}_{<4}$で$4$未満の自然数($0$を含む)を表すことにすれば,任意の事例の正解ラベル$y_i$は$y_i \in \mathbb{N}_{<4}$で表現できる.
以降では,ラベルの種類数を$L$で表す(今回の分類タスクでは$L=4$である).

$i$番目の事例の特徴ベクトル$\boldsymbol{x}_i$は,次式で求める.

$$
\boldsymbol{x}_i = \frac{1}{T_i} \sum_{t=1}^{T_i} \mathrm{emb}(w_{i,t})
$$

ここで,$i$番目の事例は$T_i$個の(記事見出しの)単語列$(w_{i,1}, w_{i,2}, \dots, w_{i,T_i})$から構成され,$\mathrm{emb}(w) \in \mathbb{R}^d$は単語$w$に対応する単語ベクトル(次元数は$d$)である.すなわち,$i$番目の事例の記事見出しを,その見出しに含まれる単語のベクトルの平均で表現したものが$\boldsymbol{x}_i$である.今回は単語ベクトルとして,問題60でダウンロードしたものを用いればよい.$300$次元の単語ベクトルを用いたので,$d=300$である.

$i$番目の事例のラベル$y_i$は,次のように定義する.

$$
y_i = \begin{cases}
0 & (\mbox{記事}x_i\mbox{が「ビジネス」カテゴリの場合}) \\
1 & (\mbox{記事}x_i\mbox{が「科学技術」カテゴリの場合}) \\
2 & (\mbox{記事}x_i\mbox{が「エンターテイメント」カテゴリの場合}) \\
3 & (\mbox{記事}x_i\mbox{が「健康」カテゴリの場合}) \\
\end{cases}
$$

なお,カテゴリ名とラベルの番号が一対一で対応付いていれば,上式の通りの対応付けでなくてもよい.

以上の仕様に基づき,以下の行列・ベクトルを作成し,ファイルに保存せよ.

  • 学習データの特徴量行列: $X_{\rm train} \in \mathbb{R}^{N_t \times d}$
  • 学習データのラベルベクトル: $Y_{\rm train} \in \mathbb{N}^{N_t}$
  • 検証データの特徴量行列: $X_{\rm valid} \in \mathbb{R}^{N_v \times d}$
  • 検証データのラベルベクトル: $Y_{\rm valid} \in \mathbb{N}^{N_v}$
  • 評価データの特徴量行列: $X_{\rm test} \in \mathbb{R}^{N_e \times d}$
  • 評価データのラベルベクトル: $Y_{\rm test} \in \mathbb{N}^{N_e}$

なお,$N_t, N_v, N_e$はそれぞれ,学習データの事例数,検証データの事例数,評価データの事例数である.

単語ベクトルの平均を計算したいので、tokenizeしておきます。なんでも良いと思いますが私はspaCyを使います。

import spacy
nlp = spacy.load("en_core_web_sm", disabled=("ner", "parser"))

spaCyは勝手にいろんな解析をしますがtokenizeだけしたいときは無駄が多いです。disabledを指定しておきましょう。

あとは$X$を作れば良いですが、次の問題を見据えてtorch.tensor型にしておきましょう。

import torch

def get_textvec(text):
    doc = nlp(text)
    vecs = [word_vectors[token.text] for token in doc if token.text in word_vectors]
    return sum(vecs) / len(vecs)

X_train = torch.tensor([get_textvec(title) for title in train['TITLE']])
X_valid = torch.tensor([get_textvec(title) for title in valid['TITLE']])
X_test = torch.tensor([get_textvec(title) for title in test['TITLE']])
X_test.size()
torch.Size([1336, 300])

未知語のみからなる事例が無くてなによりです。またshapeも問題無さそうです。

$Y$はラベルをidに置換するだけですね。どうやってもできますがpandas.Seriesのメソッドを使ってやるといいでしょう。

category_dic = {'b': 0, 't': 1, 'e': 2, 'm': 3}
y_train = torch.tensor(train['CATEGORY'].map(category_dic).values)
y_valid = torch.tensor(valid['CATEGORY'].map(category_dic).values)
y_test = torch.tensor(test['CATEGORY'].map(category_dic).values)
print(y_test)
print(y_test.size())
tensor([2, 0, 0,  ..., 1, 0, 0])
torch.Size([1336])

mapメソッドで簡単にできるようで、実に便利だと思いました。

あとはこれらを保存します。といってもこれColabのディレクトリに保存してもすぐ消えてしまうのでなんかいい方法さがしてたらGoogle ドライブをローカルにマウントする という方法がありましたね(後で気づきましたがUIでもできるようでした)。

またPyTorchオブジェクトの保存は公式チュートリアルのSaving and Loading Models に書いてあります。簡単ですねえ。

data_dir = 'drive/MyDrive/colab/'
torch.save(X_train, data_dir + 'X_train.pt')
torch.save(X_valid, data_dir + 'X_valid.pt')
torch.save(X_test, data_dir + 'X_test.pt')
torch.save(y_train, data_dir + 'y_train.pt')
torch.save(y_valid, data_dir + 'y_valid.pt')
torch.save(y_test, data_dir +  'y_test.pt')
!ls drive/MyDrive/colab/
X_test.pt  X_train.pt  X_valid.pt  y_test.pt  y_train.pt  y_valid.pt

71. 単層ニューラルネットワークによる予測

問題70で保存した行列を読み込み,学習データについて以下の計算を実行せよ.

$$
\hat{\boldsymbol{y}}_1 = {\rm softmax}(\boldsymbol{x}_1 W), \\
\hat{Y} = {\rm softmax}(X_{[1:4]} W)
$$

ただし,${\rm softmax}$はソフトマックス関数,$X_{[1:4]} \in \mathbb{R}^{4 \times d}$は特徴ベクトル$\boldsymbol{x}_1, \boldsymbol{x}_2, \boldsymbol{x}_3, \boldsymbol{x}_4$を縦に並べた行列である.

$$
X_{[1:4]} = \begin{pmatrix}
\boldsymbol{x}_1 \\
\boldsymbol{x}_2 \\
\boldsymbol{x}_3 \\
\boldsymbol{x}_4 \\
\end{pmatrix}
$$

行列$W \in \mathbb{R}^{d \times L}$は単層ニューラルネットワークの重み行列で,ここではランダムな値で初期化すればよい(問題73以降で学習して求める).なお,$\hat{\boldsymbol{y}}_1 \in \mathbb{R}^L$は未学習の行列$W$で事例$x_1$を分類したときに,各カテゴリに属する確率を表すベクトルである.
同様に,$\hat{Y} \in \mathbb{R}^{n \times L}$は,学習データの事例$x_1, x_2, x_3, x_4$について,各カテゴリに属する確率を行列として表現している.

この辺はいろいろ調べればできると思います。バイアス項はいらないようです。

import torch
data_dir = 'drive/MyDrive/colab/'
X_train = torch.load(data_dir + 'X_train.pt')
X_valid = torch.load(data_dir + 'X_valid.pt')
X_test = torch.load(data_dir + 'X_test.pt')
y_train = torch.load(data_dir + 'y_train.pt')
y_valid = torch.load(data_dir + 'y_valid.pt')
y_test = torch.load(data_dir +  'y_test.pt')
from torch import nn

class Perceptron(nn.Module):
  def __init__(self, input_size, output_size):
    super().__init__()
    self.fc = nn.Linear(input_size, output_size, bias=False)
    torch.manual_seed(0)
    nn.init.normal_(self.fc.weight, 0.0, 1.0) 

  def forward(self, x):
    x = self.fc(x)
    return x
model = Perceptron(300, 4)
from torch.nn import functional as F

with torch.no_grad():
    y_hat_1 = F.softmax(model(X_train[:1]), dim=1)
    print(y_hat_1)
    Y_hat = F.softmax(model(X_train[:4]), dim=1)
    print(Y_hat)
tensor([[0.0231, 0.0937, 0.8191, 0.0641]])
tensor([[0.0231, 0.0937, 0.8191, 0.0641],
        [0.0292, 0.1466, 0.8093, 0.0148],
        [0.2996, 0.3287, 0.1773, 0.1944],
        [0.0985, 0.7173, 0.1282, 0.0560]])

ざっくり説明するとネットワークの定義はnn.moduleを継承したクラスを作り、インスタンス変数を定義してforward()メソッドで前向きの計算を定義しましょうという話ですね。

初期化メソッドでは親クラスの処理も必要なのでsuper()で親クラスを参照し、親クラスの初期化メソッドを呼び出してます。PyTorch公式チュートリアルではsuper()の引数を書いてますがPython3では省略できます。

    torch.manual_seed(0)
    nn.init.normal_(self.fc.weight, 0.0, 1.0) 

パラメータの初期化はランダムで良いとのことなのでこの部分は不要です。初期化の方法を記述してシードも決めておきたい場合こんな感じになります。

nn.Linearcallableオブジェクト(インスタンス化後関数のように使える)で、こいつをメンバに持たせることで最適化対象になります。

nn.moduleもcallableなのでインスタンスmodelも関数のように使えます。このときforward()が呼び出されます。ついでに誤差逆伝搬用の処理も走ります。この問題ではいらないのでwith torch.no_grad():で無効にしています。

ソフトマックスの計算は何を使っても良いですが、PyTorchではdimの指定が必須のようです。この例ではdim=-1(最後の次元)でも同じです。その軸に関して合計1がになるように計算されます。

torch.nn.Softmaxはソフトマックス層という捉え方なのでこの問題では使うのは違和感があり、torch.softmaxは動きますがドキュメント化されてないのでtorch.nn.functional.softmaxを使っています)

72. 損失と勾配の計算

学習データの事例$x_1$と事例集合$x_1, x_2, x_3, x_4$に対して,クロスエントロピー損失と,行列$W$に対する勾配を計算せよ.なお,ある事例$x_i$に対して損失は次式で計算される.

$$
l_i = - \log [\mbox{事例}x_i\mbox{が}y_i\mbox{に分類される確率}]
$$

ただし,事例集合に対するクロスエントロピー損失は,その集合に含まれる各事例の損失の平均とする.

なんということでしょう、$x_i$の違いに目を瞑れば6章でやった多クラスロジスティック回帰と同じではありませんか、というストーリーです。

loss_fn = nn.CrossEntropyLoss()
loss1 = loss_fn(model(X_train[:1]), y_train[:1])
model.zero_grad()
loss1.backward()
print('損失 :', loss1)
print('勾配:')
print(model.fc.weight.grad)

損失 : tensor(3.7686, grad_fn=<NllLossBackward>)
勾配:
tensor([[-0.1732, -0.1089,  0.0972,  ...,  0.0212, -0.0214,  0.0392],
        [ 0.0166,  0.0104, -0.0093,  ..., -0.0020,  0.0020, -0.0038],
        [ 0.1452,  0.0913, -0.0815,  ..., -0.0178,  0.0179, -0.0329],
        [ 0.0114,  0.0071, -0.0064,  ..., -0.0014,  0.0014, -0.0026]])

ロスを計算して.backward()すると勾配がどんとん計算されていく、これが自動微分というやつですね。torch.Tensor型に対してなんらかの計算を行うと計算グラフが勝手に構築されるので、model.zero_grad()しておかないと勾配がどんどん蓄積されます。.backward()する前に.zero_grad()すると覚えておきましょう。

PyTorchのクロスエントロピーはソフトマックスの計算も含んでるのが若干罠かもしれませんが、ソフトマックス+クロスエントロピーの勾配は簡単に求められることを知っていれば納得です。行列$W$に対する勾配は次のように計算でき、.gradの値と一致します。.Tは転置で.matmulは行列積です。なお私の学習データにおいては$y_0=0$なので教師信号は[1,0,0,0]です。

with torch.no_grad():
    print((y_hat_1 - torch.tensor([1,0,0,0])).T.matmul(X_train[:1]))
tensor([[-0.1732, -0.1089,  0.0972,  ...,  0.0212, -0.0214,  0.0392],
        [ 0.0166,  0.0104, -0.0093,  ..., -0.0020,  0.0020, -0.0038],
        [ 0.1452,  0.0913, -0.0815,  ..., -0.0178,  0.0179, -0.0329],
        [ 0.0114,  0.0071, -0.0064,  ..., -0.0014,  0.0014, -0.0026]])
loss_fn = nn.CrossEntropyLoss()
loss = loss_fn(model(X_train[:4]), y_train[:4])
model.zero_grad()
loss.backward()
print('損失 :', loss1)
print('勾配:')
print(model.fc.weight.grad)
損失 : tensor(3.7686, grad_fn=<NllLossBackward>)
勾配:
tensor([[-0.0342, -0.0297,  0.0427,  ..., -0.0254, -0.0569,  0.0270],
        [ 0.0117,  0.0089, -0.0217,  ..., -0.0010,  0.0223,  0.0062],
        [ 0.0174,  0.0147, -0.0140,  ...,  0.0259,  0.0259, -0.0344],
        [ 0.0051,  0.0061, -0.0071,  ...,  0.0005,  0.0087,  0.0012]])

73. 確率的勾配降下法による学習

確率的勾配降下法(SGD: Stochastic Gradient Descent)を用いて,行列$W$を学習せよ.なお,学習は適当な基準で終了させればよい(例えば「100エポックで終了」など).

いろいろ書き方がありそうですが、私は公式チュートリアルのような学習ループを書くことにしました。

そのためには自分のデータをDataLoaderに扱わせるのが楽そうです。そのための方法は頑張って探しましょう。

from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader


train_data = TensorDataset(X_train, y_train)
valid_data = TensorDataset(X_valid, y_valid)
test_data = TensorDataset(X_test, y_test)

train_dataloader = DataLoader(train_data, batch_size=1, shuffle=True)
valid_dataloader = DataLoader(valid_data, batch_size=len(valid_data), shuffle=False)
test_dataloader = DataLoader(test_data, batch_size=len(test_data), shuffle=False)
def train_loop(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)
    for batch, (X, y) in enumerate(dataloader):
        # Compute prediction and loss
        pred = model(X)
        loss = loss_fn(pred, y)

        # Backpropagation
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if batch % 3000 == 0:
            loss, current = loss.item(), batch * len(X)
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")


def test_loop(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    test_loss, correct = 0, 0

    with torch.no_grad():
        for X, y in dataloader:
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()

    test_loss /= size
    correct /= size
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")

model = Perceptron(300, 4)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1.0)

epochs = 10
for t in range(epochs):
    print(f"Epoch {t+1}\n-------------------------------")
    train_loop(train_dataloader, model, loss_fn, optimizer)
    test_loop(valid_dataloader, model, loss_fn)
print("Done!")

6章の正解率をあっさり超えたのではないでしょうか。モデル自体はロジスティック回帰で最適化手法が若干違うだけなので、この改善は単語分散表現のおかげですね。

optimizer.zero_grad()は最適化対象(初期化メソッドの引数で指定してます)の勾配を0にします。前の問題のmodel.zero_grad()model中の全パラメータの勾配を0にします。今回はどちらでも同じですね。

optimizer.step()で勾配に基づきパラメータが更新されます。

あとは特に説明すること無いですよね?

74. 正解率の計測

問題73で求めた行列を用いて学習データおよび評価データの事例を分類したとき,その正解率をそれぞれ求めよ.

既に求められるようにしてました。

 test_loop(train_dataloader, model, loss_fn)
Test Error: 
 Accuracy: 90.0%, Avg loss: 0.286043 
 test_loop(test_dataloader, model, loss_fn)
Test Error: 
 Accuracy: 85.9%, Avg loss: 0.000312 

75. 損失と正解率のプロット

問題73のコードを改変し,各エポックのパラメータ更新が完了するたびに,訓練データでの損失,正解率,検証データでの損失,正解率をグラフにプロットし,学習の進捗状況を確認できるようにせよ.

これはリアルタイムでプロットしないと進捗確認とは言わないですよね、環境がGoogle ColabなのでMatplotlibでプロットしても良いですが、それだと非Jupyter環境でおそらく通用しないので、そうなるとTensorBoardを使うべきな気がしますね。

%load_ext tensorboard
The tensorboard extension is already loaded. To reload it, use:
  %reload_ext tensorboard
%tensorboard --logdir=runs
<IPython.core.display.Javascript object>

この辺がColabでTensorBoardを使うための設定です。なお、お使いのブラウザの設定によっては403が出てしまうようです。

あとはColab関係なくPyTorchでTensorBoardを使う方法に従って学習コードを改変すれば良いです。

from torch.utils.tensorboard import SummaryWriter

def train_loop(dataloader, model, loss_fn, optimizer, epoch, writer):
    size = len(dataloader.dataset)
    for batch, (X, y) in enumerate(dataloader):
        # Compute prediction and loss
        pred = model(X)
        loss = loss_fn(pred, y)

        # Backpropagation
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if batch % 3000 == 0:
            loss, current = loss.item(), batch * len(X)
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")
            writer.add_scalar('Loss/train', loss, epoch)

def test_loop(dataloader, model, loss_fn, epoch, writer):
    size = len(dataloader.dataset)
    test_loss, correct = 0, 0

    with torch.no_grad():
        for X, y in dataloader:
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()

    test_loss /= size
    correct /= size
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
    writer.add_scalar('Loss/valid', test_loss, epoch)
    writer.add_scalar('Accuracy/valid', correct, epoch)

model = Perceptron(300, 4)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1.0)
writer = SummaryWriter()

epochs = 50
for epoch in range(epochs):
    print(f"Epoch {epoch+1}\n-------------------------------")
    train_loop(train_dataloader, model, loss_fn, optimizer, epoch, writer)
    test_loop(valid_dataloader, model, loss_fn, epoch, writer)
print("Done!")
writer.close()

スクリーンショット 2021-03-13 193138.png

76. チェックポイント

問題75のコードを改変し,各エポックのパラメータ更新が完了するたびに,チェックポイント(学習途中のパラメータ(重み行列など)の値や最適化アルゴリズムの内部状態)をファイルに書き出せ.

state_dict()属性を保存します。

def test_loop(dataloader, model, loss_fn, epoch, writer):
    size = len(dataloader.dataset)
    test_loss, correct = 0, 0

    with torch.no_grad():
        for X, y in dataloader:
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()

    test_loss /= size
    correct /= size
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
    writer.add_scalar('Loss/valid', test_loss, epoch)
    writer.add_scalar('Accuracy/valid', correct, epoch)
    data = {'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict()}
    torch.save(data, f'drive/MyDrive/checkpoint_{epoch + 1}.pt')

model = Perceptron(300, 4)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1.0)
writer = SummaryWriter()

epochs = 10
for epoch in range(epochs):
    print(f"Epoch {epoch+1}\n-------------------------------")
    train_loop(train_dataloader, model, loss_fn, optimizer, epoch, writer)
    test_loop(valid_dataloader, model, loss_fn, epoch, writer)
print("Done!")
writer.close()

これでとりあえず保存はできますが、これでは実験のたびにファイルが書き換えられてしまいます。さらに実際には、どのデータを使ったのかという情報やその他ハイパラも何らかの形で保存しておかないと大変なことになります。筆者はFairseqの、validationで一番良かったモデルをcheckpoint_best.ptという名前で保存してくれる機能や、モデル保存先ディレクトリにすでチェックポイントのファイルがあった時は自動でそれを読み込んで続きから学習してくれる機能に感動しました。この機能が無いと推論時にとりあえずどのモデルを使えば良いかわからなかったり、うっかりモデルファイルを上書きしてしまったり...という憂き目を見ます。

77. ミニバッチ化

問題76のコードを改変し,$B$事例ごとに損失・勾配を計算し,行列$W$の値を更新せよ(ミニバッチ化).$B$の値を$1, 2, 4, 8, \dots$と変化させながら,1エポックの学習に要する時間を比較せよ.

既にバッチサイズを指定できるコードになっています。バッチサイズが大きいほど学習時間は短くなります。精度については大き過ぎると個々のデータの特徴が無視されて悪化しがち、小さすぎると学習が不安定になったり局所解にはまったりしがち(なので学習率などで工夫が必要)、という印象がありますね。といったところでこの問題はスキップさせてください。

78. GPU上での学習

問題77のコードを改変し,GPU上で学習を実行せよ.

use_cuda = torch.cuda.is_available()
device = 'cuda' if use_cuda else 'cpu'
print(f'Using {device} device')
Using cuda device

としておき、あとはGPUに乗せたいオブジェクトに対し.to(device)すれば良いです。

Dataset全体を最初にGPUに乗せるか、DataLoaderからgenerateされたミニバッチを毎回GPUに乗せるか若干迷いましたが、大抵データはとても大きいので後者が普通ですよね?

GPUを使うときはDataLoaderでpin_memory=Trueにしておくのが推奨のようです。

train_dataloader = DataLoader(train_data, batch_size=16, shuffle=True, pin_memory=use_cuda)
valid_dataloader = DataLoader(valid_data, batch_size=len(valid_data), shuffle=False, pin_memory=use_cuda)
from torch.utils.tensorboard import SummaryWriter

def train_loop(dataloader, model, loss_fn, optimizer, epoch, writer, device):
    size = len(dataloader.dataset)
    for batch, (X, y) in enumerate(dataloader):
        # Compute prediction and loss
        X = X.to(device)
        y = y.to(device)
        pred = model(X)
        loss = loss_fn(pred, y)

        # Backpropagation
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if batch % 3000 == 0:
            loss, current = loss.item(), batch * len(X)
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")
            writer.add_scalar('Loss/train', loss, epoch)

def test_loop(dataloader, model, loss_fn, epoch, writer, device):
    size = len(dataloader.dataset)
    test_loss, correct = 0, 0

    with torch.no_grad():
        for X, y in dataloader:
            X = X.to(device)
            y = y.to(device)
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()

    test_loss /= size
    correct /= size
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
    writer.add_scalar('Loss/valid', test_loss, epoch)
    writer.add_scalar('Accuracy/valid', correct, epoch)
    data = {'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict()}
    torch.save(data, f'drive/MyDrive/checkpoint_{epoch + 1}.pt')

model = Perceptron(300, 4).to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1.0)
writer = SummaryWriter()

epochs = 20
for epoch in range(epochs):
    print(f"Epoch {epoch+1}\n-------------------------------")
    train_loop(train_dataloader, model, loss_fn, optimizer, epoch, writer, device)
    test_loop(valid_dataloader, model, loss_fn, epoch, writer, device)
print("Done!")
writer.close()
Epoch 15
-------------------------------
loss: 0.245224  [    0/10684]
Test Error: 
 Accuracy: 89.7%, Avg loss: 0.000226 

Epoch 16
-------------------------------
loss: 0.299054  [    0/10684]
Test Error: 
 Accuracy: 89.1%, Avg loss: 0.000226 

Epoch 17
-------------------------------
loss: 0.184624  [    0/10684]
Test Error: 
 Accuracy: 89.7%, Avg loss: 0.000225 

Epoch 18
-------------------------------
loss: 0.077698  [    0/10684]
Test Error: 
 Accuracy: 89.8%, Avg loss: 0.000225 

Epoch 19
-------------------------------
loss: 0.375871  [    0/10684]
Test Error: 
 Accuracy: 89.3%, Avg loss: 0.000224 

Epoch 20
-------------------------------
loss: 0.031386  [    0/10684]
Test Error: 
 Accuracy: 89.1%, Avg loss: 0.000225 

Done!

79. 多層ニューラルネットワーク

問題78のコードを改変し,バイアス項の導入や多層化など,ニューラルネットワークの形状を変更しながら,高性能なカテゴリ分類器を構築せよ.

nn.Sequentialnn.ModuleListを使うと楽にできそうです。n層にするときは後者が良さそうですが、今回は2層でいいかなと思い前者を採用します。

from torch import nn

class MultiLayerPerceptron(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super().__init__()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(input_size, hidden_size),
            nn.ReLU(),
            nn.Dropout(0.2),
            nn.Linear(hidden_size, output_size),
        )
        torch.manual_seed(0)
        self.linear_relu_stack.apply(self._init_weights)

    def _init_weights(self, m):
        if type(m) == nn.Linear:
            nn.init.normal_(m.weight, 0.0, 1.0) 

    def forward(self, x):
        x =  self.linear_relu_stack(x)
        return x

model = MultiLayerPerceptron(300, 200, 4).to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1.0)
writer = SummaryWriter()

epochs = 20
for epoch in range(epochs):
    print(f"Epoch {epoch+1}\n-------------------------------")
    model.train()
    train_loop(train_dataloader, model, loss_fn, optimizer, epoch, writer, device)
    model.eval()
    test_loop(valid_dataloader, model, loss_fn, epoch, writer, device)
print("Done!")
writer.close()
Epoch 15
-------------------------------
loss: 0.104908  [    0/10684]
Test Error: 
 Accuracy: 90.1%, Avg loss: 0.000336 

Epoch 16
-------------------------------
loss: 0.030560  [    0/10684]
Test Error: 
 Accuracy: 88.9%, Avg loss: 0.000373 

Epoch 17
-------------------------------
loss: 0.032152  [    0/10684]
Test Error: 
 Accuracy: 89.5%, Avg loss: 0.000351 

Epoch 18
-------------------------------
loss: 0.161151  [    0/10684]
Test Error: 
 Accuracy: 90.1%, Avg loss: 0.000372 

Epoch 19
-------------------------------
loss: 0.029951  [    0/10684]
Test Error: 
 Accuracy: 89.4%, Avg loss: 0.000367 

Epoch 20
-------------------------------
loss: 0.015658  [    0/10684]
Test Error: 
 Accuracy: 90.0%, Avg loss: 0.000362 

Done!

単語分散表現が優秀なので精度はそんなに変わらないかなと思います。

dropoutくらいはやっておくかと思いnn.Dropoutを加えてます。これは過学習を抑止するためのポピュラーな方法の一つで、適当な確率に従って要素の値を0にします。

dropoutやbatch normalizationを追加したときは推論時model.eval()しておくことで推論時のみ無効にすることができます。

これで8章が終わりました。コードのミス・改善点などあればご指摘いただけると幸いです。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2