見出し画像

PyTorch深層学習⑨ロジスティック回帰:実装編

前回は、ロジスティック回帰の理論的な側面を解説しました。今回は、PyTorchを使ったロジスティック回帰の実装を行います。

実装をすることでロジスティック回帰の仕組みがより鮮明に理解できます。

訓練用のデータの準備などから始めて、実際に訓練を行い、テストの結果を確認します。


Python環境の設定

Pythonの仮想環境を作ってPyTorchとJupyterなどをインストールします。

mkdir logistic_regression
cd linear_regression

python3 -m venv venv
source venv/bin/activate

# pip をアップグレードしておく
pip install --upgrade pip

# 必要なライブラリをインストール
pip install torch matplotlib jupyter

いつものようにJupyterノートブックを立ち上げてPython3のノートブックを作成してください。

データの準備

NumPyを使ってデータを生成し、最後にPyTorchのTensorへと変換します。訓練用とテスト用のデータを準備します。

NumPyデータの生成

まずは、必要なライブラリをインポートします。

import numpy as np
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

前回の記事で扱った2次元データを生成します。

これらの点の位置は$${(x_1, x_2)}$$で与えられ、クラスは$${y}$$が0か1かで指定します。

🔵 クラス1($${y=0}$$)
🔴 クラス2($${y=1}$$)

次の generate_data(N) では、各クラスのデータ数(N)を指定してランダムにデータを生成します。

# 2次元データを指定されたデータポイントの数だけ各クラスに対して生成
def generate_data(N):
    
    # クラス1のx1、x2のデータを生成
    class1_x1 = np.random.normal(2.0, 3.0, N)
    class1_x2 = np.random.normal(1.0, 2.0, N)

    # クラス2のx1、x2のデータを生成
    class2_x1 = np.random.normal(2.0, 3.0, N)
    class2_x2 = np.random.normal(7.0, 1.0, N)

    # x1 と x2 のデータをそれぞれ結合
    x1 = np.concatenate([class1_x1, class2_x1])
    x2 = np.concatenate([class1_x2, class2_x2])

    # 最初のN個は、クラス1。次のN個は、クラス2。
    y = np.array([0] * N + [1] * N)
    
    return x1, x2, y

データは以下に定義される正規分布に従うように生成されます。

  • クラス1

    • $${x_1}$$:平均2.0、標準偏差3.0

    • $${x_2}$$:平均1.0、標準偏差2.0

  • クラス2

    • $${x_1}$$:平均2.0、標準偏差3.0

    • $${x_2}$$:平均7.0、標準偏差1.0

このように生成することでクラス1が上部に、クラス2が下部に分散するようにしています。このクラス1とクラス2のデータを結合して、$${x_1}$$と$${x_2}$$のデータとします。

また、正解データ(ラベル)の$${y}$$は最初のN個をクラス1($${y=0}$$)、次のN個をクラス2($${y=1}$$)としています。

この関数を使って訓練用のデータを生成します。

# ランダム・シードの指定
np.random.seed(123)

x1, x2, y = generate_data(100)

なお、再現性を確保するためにランダム・シードを固定しています。

このデータをmatplotlibを使って表示します。

plt.scatter(x1, x2, color='k')
plt.xlabel("x1")
plt.ylabel("x2")
plt.show()

これが入力値になります。これに対して各点のクラスを予測するモデルをロジスティック回帰を使って構築します。

ラベルの色をつけて訓練データを表示してみます。

def label_to_color(value):
    return np.array(['r', 'b'])[value]

plt.scatter(x1, x2, color=label_to_color(y))
plt.xlabel("x1")
plt.ylabel("x2")
plt.show()

上図のように各データのクラスを予測できるようになれば良いわけです。

入力値としては、各点に対して二つの値があるように$${x_1}$$と$${x_2}$$をまとめます。

# x1とx2を合わせて2次元のテンソル(NumPy)にする
x = np.float32(list(zip(x1, x2)))

x.shape

まず、zip でジェネレータ(Generator)にしたものをリストに変換した後、NumPy(32ビット浮動小数点型)にしました。シェイプが(200, 2)なので各点に2つの値があるが確認できます。

全てを表示すると200行になるので割愛しますが、以下のようになっています。

x

また、ラベルの$${y}$$も1次元追加して2次元テンソル(行列)にします。

# 1次元追加
y = y[:, np.newaxis]

y.shape

これも200行なので全てを掲載しませんが、以下のようになります。

y

最初のほうは0で、後から1が続きます。100個ずつ0と1が並んでいる状態です。

このデータの順番をランダムに入れ替えます。

# データポイントの順番をランダムに入れ替える
idx = torch.randperm(len(x))

x = x[idx]
y = y[idx]

データの位置とラベルの値に不要な相関がないようにしています。

今回はバッチを使わずに、すべてのデータで訓練するので必要はないですが、大きなデータセットを扱う場合は、バッチの中に同じパターンが繰り返し出現しないようにエポックごとにデータをシャッフルします。

これでNumPy形式でのデータの準備ができました。

Tensorデータの準備

次にNumPyからPyTorchのTensorへと変換します。

まずは、NumPyで作ったデータのデータ型を確認します。

x.dtype, y.dtype

$${x}$$は、32ビットの浮動小数点型で、$${y}$$は64ビットの整数型です。

これらを32ビットの浮動小数点型のTensorにします。

$${x}$$に関しては、すでに32ビットの浮動小数点型なので明示的にデータ型を指定する必要はないですが、次のように変換します。

x_train = torch.tensor(x, dtype=torch.float32)
x_train.shape

Tensorのデータ型を指定しているので仮にNumPyデータの方が異なるデータ型だったとしても間違いがないでしょう。これで、$${x}$$が2次元テンソルのTensorになりました。

次に、ラベル$${y}$$も変換します。

y_train = torch.tensor(y, dtype=torch.float32)
y_train.shape

これも、2次元テンソルのTensorになりました。32ビットの浮動小数点型を指定しているのはPyTorchのモジュール(損失関数)がこのデータ型を想定しているからです。整数型のままだと後でエラーが生じます。

これで訓練データ x_train と y_train がで準備できました。

同様の作業を繰り返してテスト用のデータも作ります。そのために、訓練用データを作成するのと同じコードを関数としてまとめました。

# データを生成してTensorにするところまで全て処理する
def prepare_data(N):
    
    # 2次元データを指定されたデータポイントの数だけ各クラスに対して生成    
    x1, x2, y = generate_data(N)
    
    # x1とx2を合わせて2次元のテンソル(NumPy)にする
    x = np.float32(list(zip(x1, x2)))
    
    # 1次元追加
    y = y[:, np.newaxis]
    
    # データポイントの順番をランダムに入れ替える
    idx = torch.randperm(len(x))
    x = x[idx]
    y = y[idx]
    
    # Tensorに変換
    x_data = torch.tensor(x, dtype=torch.float32)
    y_data = torch.tensor(y, dtype=torch.float32)

    return x_data, y_data 

これを使って、テストデータを生成します。

# テスト用データの生成
np.random.seed(777)

x_test, y_test = prepare_data(100)

なお、ランダムシードは訓練用のデータを作成した時とは異なる値に設定しています。よって、生成されるデータも異なるものになります。また、再現性も確保されます。

以上で、データの準備が完了しました。

次にロジスティック回帰モデルを構築します。

ロジスティック回帰モデル

前回の記事でロジスティック回帰は線形モデルとシグモイド関数の組み合わせとして表現できると解説しました。

次のように、線形層を nn.Linear で作成します。その際、入力値が$${x_1, x_2}$$の二つで、出力値が一つなので、nn.Linear(2, 1) と指定します。この線形層からの出力をシグモイド関数に通すことでロジットから確率へと変換します。

# 線形モデルとシグモイド関数

# 線形モデル
linear = nn.Linear(2, 1)

# シグモイド関数
sigmoid = nn.Sigmoid()

# フィード・フォワード
z = linear(x_train)

# ロジットから確率へ変換
p_hat = sigmoid(z)

p_hat

p_hat($${\hat{p}}$$)は、クラス2である確率の予想です。200個の入力に対して、200個の出力が出されます。

上記のコードをまとめて、PyTorchのモジュールとして以下のように定義します。

# 線形モデルとシグモイドを組み合わせたロジスティック回帰のモデル

class LogisticRegression(nn.Module):
    
    def __init__(self):
        super().__init__()

        # Linear model
        self.linear = nn.Linear(2, 1)
        
        # Sigmoid function
        self.sigmoid = nn.Sigmoid()
        
    def forward(self, x):
        z = self.linear(x)
        
        p_hat = self.sigmoid(z)
        
        return p_hat

やっていることは同じです。モジュールの初期化(__init__)で線形層とシグモイド関数を作成します。また、forward では、入力値を線形層に通してからシグモイド関数を適用してロジットから確率(予測)へと変換しています。

このモジュール(LogisticRegression)を使うとコードがスッキリします。次のようにモデルを生成します。

# ロジスティック回帰モデルの作成
model = LogisticRegression()

このモデルのパラメータをSGDオプティマイザに渡します。学習率は0.1としておきます。

# SGDオプティマイザ
learning_rate = 0.1
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

あとは、損失関数であるバイナリクロスエントロピーを作成します。


# バイナリクロスエントロピー損失関数
loss_function = nn.BCELoss()

以上で、ロジスティック回帰モデルの訓練の準備が整いました。

訓練ループ

訓練ループに関しては線形回帰の時と同じです。

# 訓練ループ

epochs = 10000

for epoch in range(epochs):
    
    optimizer.zero_grad()
    
    p_hat = model(x_train)
    
    loss = loss_function(p_hat, y_train)
    
    loss.backward()
    
    optimizer.step()
    
    if epoch % 1000 == 0:
        print(f'Loss: {loss.item():.2f}')

10,000エポックの訓練ループで1000エポックごとに損失値を表示しました。損失値が下がって安定しているのが確認できます。

なお、optimizer.zero_grad()は誤差逆伝播で計算する勾配を0にするのですが、loss.backward()の前に呼ぶ必要があります。ここでは一番最初に呼んでいますが、loss.backward()の直前でも構いません。

これで訓練終了です。

訓練結果

まずは、訓練データで訓練後のモデルの予測結果を見てみます。

# 後で勾配の計算が必要ないことを指定
with torch.no_grad():
    p_hat = model(x_train)

with torch.no_grad(): を使っているのは、訓練ではないので勾配の計算に必要な情報を生成しなくともよいと指定しています。

このモデルが予測した確率が50%より大きければクラス2を予測したとみなします。

# 確率が50%以上ならクラス2と予測
y_hat = p_hat > 0.5

y_hat

結果としてラベルの予測 y_hat($${\hat{y}}$$)はTrueとFalseの二値となります。

ラベルの値と比べるために、これを整数型に変換します。

y_hat = y_hat.numpy().astype(np.int32)

y_hat

次のようにラベルの予測値が正解である割合をパーセンテージとして計算します。これが正解率(Accuracy)となります。

accuracy = np.sum(y_hat == y_train.numpy()) / len(y_train) * 100

print(f'{accuracy}%')

99.0%なので予測のほとんどが正解となっています。

予測された値を matplotlib で表示します。

plt.scatter(x_train[:, 0], x_train[:, 1], color=label_to_color(y_hat.squeeze()))
plt.show()

よく予測できています。

ただし、これはモデルを訓練したデータで予測をしているので正解率が高いのはある意味当然とも言えます。

そこでテスト用に別に生成したデータを使って正解率を見てみましょう。

テスト結果

テストデータでの正解率を計算するために、前述したコードを一つの関数にまとめました。

def evaluate_results(model, x_data, y_data):
    # 後で勾配の計算が必要ないことを指定
    with torch.no_grad():
        p_hat = model(x_data)

    # 確率が50%以上ならクラス2と予測
    y_hat = p_hat > 0.5
    y_hat = y_hat.numpy().astype(np.int32)

    # 正解率の計算
    accuracy = np.sum(y_hat == y_data.numpy()) / len(y_data) * 100
    print(f'{accuracy}%')
    
    # グラフを表示
    plt.scatter(x_data[:, 0], x_data[:, 1], color=label_to_color(y_hat.squeeze()))
    plt.show()

これを使ってテストデータを指定しテスト結果を表示します。

evaluate_results(model, x_test, y_test)

結果は次のようになりました。

98.0%なので良い正解率(精度)になっています。

以上、単純な例ではありますが、ロジスティック回帰のモデルの実装の概要が掴めたと思います。

次回予告

次回はニューラルネットワークを解説します。これまで扱ってきた線形回帰やロジスティック回帰もニューラルネットワークとして捉えることができます。

より一般的にニューロンや活性化関数といった概念を通して理解することで、その後に登場する畳み込みニューラルネットワークや再帰型ニューラルネットワークなどに対する理解への橋渡しとなります。

(続く)

いいなと思ったら応援しよう!