morikomorou’s blog

自分が学んだことなどの備忘録的なやつ

【python】言語処理100本ノック2020を解く(第8章前半)


はじめに

前回の続きで言語処理100本ノック解いていきたいと思います。

今回は第8章です。
ついにニューラルネットの実装にチャレンジしてみます。




第8章: ニューラルネット

深層学習フレームワークの使い方を学び,ニューラルネットワークに基づくカテゴリ分類を実装します.

第6章で取り組んだニュース記事のカテゴリ分類を題材として,ニューラルネットワークでカテゴリ分類モデルを実装する.なお,この章ではPyTorch, TensorFlow, Chainerなどの機械学習プラットフォームを活用せよ.

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

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


\boldsymbol{X} = \begin{pmatrix}
\boldsymbol{x}_{1} \\
\boldsymbol{x}_{2} \\
\vdots \\
\boldsymbol{x}_{n}
\end{pmatrix}\in \mathbb{R}^{n\times d}, \quad \boldsymbol{Y} = \begin{pmatrix}
y_{1} \\
y_{2} \\
\vdots \\
y_{n}
\end{pmatrix}\in \mathbb{N}^{n}

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

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

 
\boldsymbol{x}_{i} = \dfrac{1}{T_{i}}\sum_{t=1}^{T_{i}}\mathrm{emb}(w_{i,t})

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

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

  •  y_{i}= 0: 記事 x_{i}が「ビジネス」カテゴリの場合
  •  y_{i}= 1: 記事 x_{i}が「科学技術」カテゴリの場合
  •  y_{i}= 2: 記事 x_{i}が「エンターテイメント」カテゴリの場合
  •  y_{i}= 3: 記事 x_{i}が「健康」カテゴリの場合

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

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

  • 学習データの特徴量行列:  Xtrain \in \mathbb{R}^{N_{t}\times d}
  • 学習データのラベルベクトル:  Y_{train} \in \mathbb{N}^{N_{t}}
  • 検証データの特徴量行列:  X_{valid} \in \mathbb{R}^{N_{v}\times d}
  • 検証データのラベルベクトル:  Y_{valid} \in \mathbb{N}^{N_{v}}
  • 評価データの特徴量行列:  X_{test} \in \mathbb{R}^{N_{e}\times d}
  • 評価データのラベルベクトル:  Y_{test} \in \mathbb{N}^{N_{e}}

なお, N_{t},N_{v},N_{e}はそれぞれ,学習データの事例数,検証データの事例数,評価データの事例数である.

まずは第6章でもやったようにデータを読み込んで、学習、検証、評価用に分けます

# 単語ベクトルのロード
from gensim.models import KeyedVectors

file = './input/section7/GoogleNews-vectors-negative300.bin.gz'
model = KeyedVectors.load_word2vec_format(file, binary=True)

# データのロード
import pandas as pd
import re
import numpy as np

# ファイル読み込み
file = './input/section6/newsCorpora.csv'
data = pd.read_csv(file, encoding='utf-8', header=None, sep='\t', names=['ID', 'TITLE', 'URL', 'PUBLISHER', 'CATEGORY', 'STORY', 'HOSTNAME', 'TIMESTAMP'])
data = data.replace('"', "'")
# 特定のpublisherのみ抽出
publishers = ['Reuters', 'Huffington Post', 'Businessweek', 'Contactmusic.com', 'Daily Mail']
data = data.loc[data['PUBLISHER'].isin(publishers), ['TITLE', 'CATEGORY']].reset_index(drop=True)

# 学習用、検証用、評価用に分割する
from sklearn.model_selection import train_test_split

train, valid_test = train_test_split(data, test_size=0.2, shuffle=True, random_state=64, stratify=data['CATEGORY'])
valid, test = train_test_split(valid_test, test_size=0.5, shuffle=True, random_state=64, stratify=valid_test['CATEGORY'])

train = train.reset_index(drop=True)
valid = valid.reset_index(drop=True)
test = test.reset_index(drop=True)

# データ数の確認
print('学習データ')
print(train['CATEGORY'].value_counts())
print('検証データ')
print(valid['CATEGORY'].value_counts())
print('評価データ')
print(test['CATEGORY'].value_counts())

import re
from nltk import stem

# データの結合
df = pd.concat([train, valid, test], axis=0).reset_index(drop=True)

# 前処理
def preprocessing(text):
    text_clean = re.sub(r'[\"\'.,:;\(\)#\|\*\+\!\?#$%&/\]\[\{\}]', '', text)
    text_clean = re.sub('[0-9]+', '0', text_clean)
    text_clean = re.sub('\s-\s', ' ', text_clean)
    return text_clean

df['TITLE'] = df['TITLE'].apply(preprocessing)

以下出力です。

学習データ
b    4502
e    4223
t    1219
m     728
Name: CATEGORY, dtype: int64
検証データ
b    562
e    528
t    153
m     91
Name: CATEGORY, dtype: int64
評価データ
b    563
e    528
t    152
m     91
Name: CATEGORY, dtype: int64

続いて、平均単語ベクトルを計算し、学習にはpytorchを使用するので特徴ベクトルをテンソル化しておきます

import numpy as np
# 平均単語ベクトルの取得
def w2v(text):
    words = text.rstrip().split()
    vec = [model[word] for word in words if word in model]
    return np.array(sum(vec) / len(vec))

vecs = np.array([])
for text in df['TITLE']:
    if len(vecs) == 0:
        vecs = w2v(text)
    else:
        vecs = np.vstack([vecs, w2v(text)])

# 特徴ベクトルのテンソル化
import torch

# 乱数のシードを設定
torch.manual_seed(1234)
np.random.seed(1234)

X_train = torch.from_numpy(vecs[:len(train), :])
X_valid = torch.from_numpy(vecs[len(train):len(train)+ len(valid), :])
X_test = torch.from_numpy(vecs[len(train)+ len(valid):, :])
print(X_train.size())
print(X_train)

以下出力です。

torch.Size([10672, 300])
tensor([[ 0.0368,  0.0300, -0.0738,  ..., -0.1523,  0.0419, -0.0774],
        [ 0.0002, -0.0056, -0.0824,  ..., -0.0544,  0.0776, -0.0214],
        [ 0.0266, -0.0166, -0.0877,  ..., -0.0522,  0.0517,  0.0093],
        ...,
        [-0.0291,  0.0529, -0.1453,  ...,  0.0494,  0.1548, -0.0910],
        [-0.0269,  0.1204, -0.0289,  ..., -0.0062,  0.0739, -0.0327],
        [ 0.0361,  0.1236,  0.0260,  ..., -0.0099, -0.0193,  0.0262]])

最後に目的変数であるカテゴリもテンソル化して保存します

# ターゲットのテンソル化
category_dict = {'b': 0, 't': 1, 'e':2, 'm':3}
Y_train = torch.from_numpy(train['CATEGORY'].map(category_dict).values)
Y_valid = torch.from_numpy(valid['CATEGORY'].map(category_dict).values)
Y_test = torch.from_numpy(test['CATEGORY'].map(category_dict).values)
# 保存
torch.save(X_train, './input/section8/X_train.pt')
torch.save(X_valid, './input/section8/X_valid.pt')
torch.save(X_test, './input/section8/X_test.pt')
torch.save(Y_train, './input/section8/y_train.pt')
torch.save(Y_valid, './input/section8/y_valid.pt')
torch.save(Y_test, './input/section8/y_test.pt')

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

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

 \boldsymbol{\hat{y}}_{1} = \mathrm{softmax}(\boldsymbol{x}_{1}W)
 \hat{Y} = \mathrm{softmax}(X_{[1:4]}W)

ただし, \mathrm{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以降で学習して求める).なお, \boldsymbol{\hat{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}について,各カテゴリに属する確率を行列として表現している.

まずは単層のニューラルネットワークをSLNetという名前で作成しておきます。

# モデルの構築
from torch import nn

class SLNet(nn.Module):
    def __init__(self, input_size, output_size):
        super().__init__()
        self.fc = nn.Linear(input_size, output_size)
    
    def forward(self, x):
        logits = self.fc(x)
        return logits

model = SLNet(300, 4)
print(model)

出力は以下。

SLNet(
  (fc): Linear(in_features=300, out_features=4, bias=True)
)

事例1についてy_hatを計算します。softmaxはpytorchにモジュールがあるので使用します。
サンプル1個での予測の場合は、logitsが1次元のテンソルになってしまうので、dimには-1を指定します

logits = model(X_train[0])
y_hat_1 = nn.Softmax(dim=-1)(logits)
print(logits)
print(y_hat_1)

結果は以下

tensor([-0.0176,  0.0697,  0.0920,  0.0596], grad_fn=<AddBackward0>)
tensor([0.2332, 0.2545, 0.2603, 0.2520], grad_fn=<SoftmaxBackward0>)

事例1~4についてY_hatを計算します。

logits = model(X_train[:4])
Y_hat = nn.Softmax(dim=1)(logits)
print(logits)
print(Y_hat)

結果は以下

tensor([[-0.0176,  0.0697,  0.0920,  0.0596],
        [-0.0210,  0.0704,  0.0814,  0.0636],
        [ 0.0360,  0.1010,  0.0704,  0.0456],
        [ 0.0362,  0.1266,  0.0222,  0.1343]], grad_fn=<AddmmBackward0>)
tensor([[0.2332, 0.2545, 0.2603, 0.2520],
        [0.2330, 0.2553, 0.2581, 0.2536],
        [0.2432, 0.2595, 0.2517, 0.2455],
        [0.2390, 0.2616, 0.2357, 0.2637]], grad_fn=<SoftmaxBackward0>)

72. 損失と勾配の計算

学習データの事 x_{1}と事例集合 x_{1}, x_{2}, x_{3}, x_{4}に対して,クロスエントロピー損失と,行列 Wに対する勾配を計算せよ.なお,ある事例 x_{i}に対して損失は次式で計算される.

 l_{i}= -\log[事例 x_{i} y_{i}に分類される確率]
ただし,事例集合に対するクロスエントロピー損失は,その集合に含まれる各事例の損失の平均とする.

クロスエントロピーロスもモジュールがあるので使用します
ソフトマックスをモジュール内で行うので、引数には予測確率と目的変数を入れます。

# x_1のロスを求める
criterion = nn.CrossEntropyLoss()
logits = model(X_train[0])
loss = criterion(logits, Y_train[0])
print("損失: ", loss.item())
model.zero_grad()
loss.backward()
print("勾配: ")
print(model.fc.weight.grad)

出力は以下。

損失:  1.3460214138031006
勾配: 
tensor([[ 0.0086,  0.0070, -0.0172,  ..., -0.0355,  0.0098, -0.0181],
        [ 0.0094,  0.0076, -0.0188,  ..., -0.0388,  0.0107, -0.0197],
        [-0.0273, -0.0222,  0.0546,  ...,  0.1127, -0.0310,  0.0573],
        [ 0.0093,  0.0076, -0.0186,  ..., -0.0384,  0.0106, -0.0195]])
# x_1~x_4のロスを求める
logits = model(X_train[:4])
loss = criterion(logits, Y_train[:4])
print("損失: ", loss.item())
model.zero_grad()
loss.backward()
print("勾配: ")
print(model.fc.weight.grad)

出力は以下。

損失:  1.3807432651519775
勾配: 
tensor([[ 5.0444e-03,  3.9170e-03,  7.1611e-03,  ..., -2.0513e-03,
         -4.2177e-03,  5.3509e-04],
        [ 3.9538e-05, -6.0439e-03, -1.8717e-02,  ..., -1.5257e-02,
         -4.6948e-03, -6.8699e-03],
        [-1.0491e-02, -7.2170e-04,  2.5712e-02,  ...,  3.4016e-02,
         -7.4130e-03,  1.1578e-02],
        [ 5.4069e-03,  2.8486e-03, -1.4156e-02,  ..., -1.6707e-02,
          1.6325e-02, -5.2435e-03]])




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

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

まずは学習、検証等に使うデータセットを作成します。

# データセットを作成する
import torch.utils.data as data

class NewsDataset(data.Dataset):
    """
    newsのDatasetクラス
    
    Attributes
    ----------------------------
    X : テンソル
        単語ベクトルの平均をまとめたテンソル
    y : テンソル
        カテゴリをラベル化したテンソル
    phase : 'train' or 'val'
        学習か訓練かを設定する
    """
    def __init__(self, X, y, phase='train'):
        self.X = X
        self.y = y
        self.phase = phase
    
    def __len__(self):
        """全データサイズを返す"""
        return len(self.y)
    
    def __getitem__(self, idx):
        """idxに対応するテンソル形式のデータとラベルを取得"""
        return self.X[idx], self.y[idx]

train_dataset = NewsDataset(X_train, Y_train, phase='train')
valid_dataset = NewsDataset(X_valid, Y_valid, phase='val')
test_dataset = NewsDataset(X_test, Y_test, phase='val')

# 動作確認
idx = 0
print(train_dataset.__getitem__(idx)[0].size())
print(train_dataset.__getitem__(idx)[1])
print(valid_dataset.__getitem__(idx)[0].size())
print(valid_dataset.__getitem__(idx)[1])
print(test_dataset.__getitem__(idx)[0].size())
print(test_dataset.__getitem__(idx)[1])

出力は以下です。

torch.Size([300])
tensor(2)
torch.Size([300])
tensor(3)
torch.Size([300])
tensor(2)

次に学習、推論時にデータセットからデータを読むためのデータローダを作成します
後々ミニバッチ化しますが、今回はしないので、バッチサイズは1としておきます。

# DataLoaderを作成
batch_size = 1

train_dataloader = data.DataLoader(
            train_dataset, batch_size=batch_size, shuffle=True)
valid_dataloader = data.DataLoader(
            valid_dataset, batch_size=len(valid_dataset), shuffle=False)
test_dataloader = data.DataLoader(
            test_dataset, batch_size=len(test_dataset), shuffle=False)

dataloaders_dict = {'train': train_dataloader,
                    'val': valid_dataloader,
                    'test': test_dataloader,
                   }

# 動作確認
batch_iter = iter(dataloaders_dict['train'])
inputs, labels = next(batch_iter)
print(inputs.size())
print(labels)

出力は以下です。

torch.Size([1, 300])
tensor([2])

いよいよ学習です。
10epochで学習することにします。

from tqdm import tqdm
# 学習

# モデルの定義
net = SLNet(300, 4)
net.train()

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

# 最適化手法の定義
optimizer = torch.optim.SGD(net.parameters(), lr=0.01, momentum=0.9)

# 学習用の関数を定義
def train_model(net, dataloaders_dict, criterion, optimizer, num_epochs):
    
    # epochのループ
    for epoch in range(num_epochs):
        print('Epoch {} / {}'.format(epoch + 1, num_epochs))
        print('--------------------------------------------')
        
        # epochごとの学習と検証のループ
        for phase in ['train', 'val']:
            if phase == 'train':
                net.train() # 訓練モード
            else:
                net.eval() # 検証モード
            
            epoch_loss = 0.0 # epochの損失和
            epoch_corrects = 0 # epochの正解数
            
            # データローダーからミニバッチを取り出すループ
            for inputs, labels in tqdm(dataloaders_dict[phase]):
                optimizer.zero_grad() # optimizerを初期化
                
                # 順伝播計算(forward)
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = net(inputs)
                    loss = criterion(outputs, labels) # 損失を計算
                    _, preds = torch.max(outputs, 1) # ラベルを予想
                    
                    # 訓練時は逆伝播
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()
                    
                    # イテレーション結果の計算
                    # lossの合計を更新
                    epoch_loss += loss.item() * inputs.size(0)
                    # 正解数の合計を更新
                    epoch_corrects += torch.sum(preds == labels.data)
            
            # epochごとのlossと正解率の表示
            epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset)
            epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset)
            
            print('{} Loss: {:.4f}, Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))

            
# 学習を実行する
num_epochs = 10
train_model(net, dataloaders_dict, criterion, optimizer, num_epochs=num_epochs)

結果は以下です。

Epoch 1 / 10
--------------------------------------------
100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3014.67it/s]
train Loss: 0.4169, Acc: 0.8569
100%|████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 71.43it/s]
val Loss: 0.3204, Acc: 0.8936
Epoch 2 / 10
--------------------------------------------
100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3071.06it/s]
train Loss: 0.3108, Acc: 0.8925
100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 142.81it/s]
val Loss: 0.2800, Acc: 0.9055
Epoch 3 / 10
--------------------------------------------
100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3059.61it/s]
train Loss: 0.2897, Acc: 0.8996
100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 166.12it/s]
val Loss: 0.2809, Acc: 0.9063
Epoch 4 / 10
--------------------------------------------
100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3058.73it/s]
train Loss: 0.2794, Acc: 0.9027
100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 142.99it/s]
val Loss: 0.2728, Acc: 0.9123
Epoch 5 / 10
--------------------------------------------
100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3053.36it/s]
train Loss: 0.2718, Acc: 0.9055
100%|████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 83.34it/s]
val Loss: 0.2681, Acc: 0.9100
Epoch 6 / 10
--------------------------------------------
100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3017.23it/s]
train Loss: 0.2642, Acc: 0.9096
100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 142.39it/s]
val Loss: 0.2768, Acc: 0.9078
Epoch 7 / 10
--------------------------------------------
100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3044.77it/s]
train Loss: 0.2600, Acc: 0.9117
100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 142.50it/s]
val Loss: 0.2788, Acc: 0.9055
Epoch 8 / 10
--------------------------------------------
100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3027.49it/s]
train Loss: 0.2576, Acc: 0.9116
100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 142.93it/s]
val Loss: 0.2700, Acc: 0.9085
Epoch 9 / 10
--------------------------------------------
100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3023.21it/s]
train Loss: 0.2551, Acc: 0.9125
100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 142.87it/s]
val Loss: 0.2748, Acc: 0.9160
Epoch 10 / 10
--------------------------------------------
100%|██████████████████████████████████████████████████████████████████████████| 10672/10672 [00:03<00:00, 3016.37it/s]
train Loss: 0.2530, Acc: 0.9136
100%|███████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 142.24it/s]
val Loss: 0.2691, Acc: 0.9115

74. 正解率の計測

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

推論、および正解率を求める関数を作成しておきます。

def calc_acc(net, dataloader):
    net.eval()
    corrects = 0
    with torch.no_grad():
        for inputs, labels in dataloader:
            outputs = net(inputs)
            _, preds = torch.max(outputs, 1) # ラベルを予想
            corrects += torch.sum(preds == labels.data)
    return corrects / len(dataloader.dataset)

acc_train = calc_acc(net, train_dataloader)
acc_valid = calc_acc(net, valid_dataloader)
acc_test = calc_acc(net, test_dataloader)
print('学習データの正解率: {:.4f}'.format(acc_train))
print('検証データの正解率: {:.4f}'.format(acc_valid))
print('テストデータの正解率: {:.4f}'.format(acc_test))

結果は以下。
90%近く当てれるとは、すごいですね。

学習データの正解率: 0.9198
検証データの正解率: 0.9115
テストデータの正解率: 0.8958

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

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

以下の問題76も一緒にやってしまいます

76. チェックポイント

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

学習用の関数を変更して、エポックごとのロス、正解率をリストに格納していきます。
ついでに各エポックの学習が終わったあとに、重み等をチェックポイントファイルとして保存します。

# 学習用の関数を定義
def train_model(net, dataloaders_dict, criterion, optimizer, num_epochs):
    train_loss = []
    train_acc = []
    valid_loss = []
    valid_acc = []
    # epochのループ
    for epoch in range(num_epochs):
        print('Epoch {} / {}'.format(epoch + 1, num_epochs))
        print('--------------------------------------------')
        
        # epochごとの学習と検証のループ
        for phase in ['train', 'val']:
            if phase == 'train':
                net.train() # 訓練モード
            else:
                net.eval() # 検証モード
            
            epoch_loss = 0.0 # epochの損失和
            epoch_corrects = 0 # epochの正解数
            
            # データローダーからミニバッチを取り出すループ
            for inputs, labels in tqdm(dataloaders_dict[phase]):
                optimizer.zero_grad() # optimizerを初期化
                
                # 順伝播計算(forward)
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = net(inputs)
                    loss = criterion(outputs, labels) # 損失を計算
                    _, preds = torch.max(outputs, 1) # ラベルを予想
                    
                    # 訓練時は逆伝播
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()
                    
                    # イテレーション結果の計算
                    # lossの合計を更新
                    epoch_loss += loss.item() * inputs.size(0)
                    # 正解数の合計を更新
                    epoch_corrects += torch.sum(preds == labels.data)
            
            # epochごとのlossと正解率の表示
            epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset)
            epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset)
            if phase == 'train':
                train_loss.append(epoch_loss)
                train_acc.append(epoch_acc)
            else:
                valid_loss.append(epoch_loss)
                valid_acc.append(epoch_acc)
            
            print('{} Loss: {:.4f}, Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
        
        # チェックポイントの保存
        torch.save({'epoch': epoch,
                    'model_state_dict': model.state_dict(),
                    'optimizer_state_dict': optimizer.state_dict()
                   },
                   './output/section8/checkpoint{}.pt'.format(epoch + 1))
    return train_loss, train_acc, valid_loss, valid_acc

# 学習を実行する

# モデルの定義
net = SLNet(300, 4)
net.train()

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

# 最適化手法の定義
optimizer = torch.optim.SGD(net.parameters(), lr=0.01, momentum=0.9)

num_epochs = 10
train_loss, train_acc, valid_loss, valid_acc = train_model(net,
            dataloaders_dict, criterion, optimizer, num_epochs=num_epochs)

出力は省略します。
エポックごとのロスと、正解率を表示します。

fig, ax = plt.subplots(1,2, figsize=(10, 5))
epochs = np.arange(num_epochs)
ax[0].plot(epochs, train_loss, label='train')
ax[0].plot(epochs, valid_loss, label='valid')
ax[0].set_title('loss')
ax[0].set_xlabel('epoch')
ax[0].set_ylabel('loss')
ax[1].plot(epochs, train_acc, label='train')
ax[1].plot(epochs, valid_acc, label='valid')
ax[1].set_title('acc')
ax[1].set_xlabel('epoch')
ax[1].set_ylabel('acc')
ax[0].legend(loc='best')
ax[1].legend(loc='best')
plt.tight_layout()
plt.savefig('fig74.png')

結果は以下の通り。
4epochほどでvalid lossが下がらなくなってきてますね。

おわりに

8章前半終わりです。
後半はGPUを使ってみたり、多層にしてみたりします。
pytorch初めて使いましたが、以下の本が入門としても大変参考になりました。