分類のためのPyTorchの紹介

PyTorchとTensorFlowライブラリは、深層学習のために最もよく使われるPythonライブラリの2つです。

PyTorchはFacebookによって開発され、TensorFlowはGoogleのプロジェクトです。

今回は、分類問題を解くためにPyTorchライブラリをどのように利用できるかを見ていきます。

分類問題は、機械学習問題のカテゴリに属し、特徴のセットが与えられると、タスクは離散的な値を予測することです。

腫瘍が癌であるかどうかの予測や、学生が試験に合格しそうか不合格になりそうかどうかの予測などは、分類問題の一般的な例として挙げられます。

今回は、ある銀行の顧客のある特徴が与えられたとき、その顧客が6ヶ月後に銀行を辞める可能性が高いかどうかを予測することにする。

顧客が組織から離れる現象は、顧客離反とも呼ばれます。

従って、我々の課題は、様々な顧客特性に基づいて顧客解約を予測することです。

先に進む前に、プログラミング言語Pythonの中級レベルの習熟度があり、PyTorchライブラリがインストールされていることが前提になります。

また、基本的な機械学習の概念についてのノウハウがあれば、役に立つかもしれません。

PyTorchをインストールしていない場合は、以下のpipコマンドでインストールすることができます。

$ pip install pytorch


データセット

この記事で使用するデータセットは、このKaggleのリンクから自由に入手することができます。

必要なライブラリとデータセットをPythonアプリケーションにインポートしてみましょう。

import torch
import torch.nn as nn
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline


データセットを含むCSVファイルをインポートするには、pandasライブラリの read_csv() メソッドを使用します。

dataset = pd.read_csv(r'E:Datasetscustomer_data.csv')


データセットの形状を表示してみましょう。

dataset.shape


出力してみましょう。

(10000, 14)


出力は、データセットが10,000レコード、14カラムであることを示しています。

pandas dataframeの head() メソッドを使うと、データセットの最初の5行を表示することができます。

dataset.head()


出力します。

データセットに14のカラムがあることがわかります。

最初の13列に基づいて、我々のタスクは14列目の値、つまりExitedを予測することである。

ここで重要なのは、最初の13列の値はExited列の値が得られる6ヶ月前に記録されていることです。

なぜならタスクは顧客情報が記録された時点から6ヶ月後の顧客離脱を予測することだからです。

探索的データ解析

それでは、データセットに対して探索的データ分析を行ってみましょう。

まず、6ヶ月後に実際に銀行を辞めた顧客の比率を予測し、円グラフを使って可視化します。

まず、グラフのデフォルトのプロット・サイズを大きくしましょう。

fig_size = plt.rcParams["figure.figsize"]
fig_size[0] = 10
fig_size[1] = 8
plt.rcParams["figure.figsize"] = fig_size


次のスクリプトは、Exitedカラムの円グラフを描画します。

dataset.Exited.value_counts().plot(kind='pie', autopct='%1.0f%%', colors=['skyblue', 'orange'], explode=(0.05, 0.05))


出力されます。

出力は、我々のデータセットでは、顧客の20%が銀行を去ったことを示している。

ここで、1は顧客が銀行を去ったケースに属し、0は顧客が銀行を去らなかったシナリオを意味する。

データセット中のすべての地理的位置からの顧客数をプロットしてみましょう。

sns.countplot(x='Geography', data=dataset)


出力

出力は、顧客のほぼ半分がフランスに属しており、スペインとドイツに属する顧客の比率はそれぞれ25%であることを示しています。

では、それぞれの地域から来た顧客の数を、顧客の解約情報とともにプロットしてみましょう。

これを行うには seaborn ライブラリの countplot() 関数を使用します。

sns.countplot(x='Exited', hue='Geography', data=dataset)


出力

出力は、フランスの全体的な顧客数はスペインとドイツの顧客数の2倍であるにもかかわらず、銀行を去った顧客の比率はフランスとドイツの顧客で同じであることを示しています。

同様に、ドイツ人顧客とスペイン人顧客の全体数は同じであるが、銀行を去ったドイツ人顧客の数はスペイン人顧客の2倍であり、ドイツ人顧客の方が6ヶ月後に銀行を去る可能性が高いことが分かる。

この記事では、データセットの残りの列に関連する情報を視覚的にプロットしませんが、もしプロットしたい場合は、Python Seaborn Libraryで探索的データ分析を行う方法についての私の記事を確認してください。

データ前処理

PyTorchのモデルを学習させる前に、データの前処理を行う必要があります。

データセットを見てみると、2種類のカラムがあることがわかります。

NumericalとCategoricalです。

Numericalカラムには数値情報が格納されています。

CreditScoreBalanceAgeなどです。

同様に、GeographyGenderはカテゴリーカラムであり、顧客の所在地や性別などのカテゴリー情報を含む。

カテゴリーだけでなく、数値としても扱えるカラムもいくつかあります。

例えば、HasCrCardカラムは 1 か 0 を値として持つことができます。

しかし、HasCrCard` カラムには、顧客がクレジットカードを持っているかどうかという情報が含まれています。

カテゴリカルと数値の両方として扱えるカラムは、カテゴリカルとして扱うことをお勧めします。

しかし、これはデータセットが持つドメイン知識に完全に依存します。

もう一度データセットのすべてのカラムを表示して、どのカラムを数値として扱い、どのカラムをカテゴリカルとして扱うべきかを調べてみましょう。

データフレームの columns 属性は、すべてのカラムの名前を表示します。

dataset.columns


出力される。

出力:“`
Index([‘RowNumber’, ‘CustomerId’, ‘Surname’, ‘CreditScore’, ‘Geography’,
‘Gender’, ‘Age’, ‘Tenure’, ‘Balance’, ‘NumOfProducts’, ‘HasCrCard’,
‘IsActiveMember’, ‘EstimatedSalary’, ‘Exited’],
dtype=’object’)


データセットのカラムのうち、`RowNumber`、`CustomerId`、`Surname` のカラムは、値が完全にランダムで出力と関係がないため、使用しないことにします。例えば、顧客の苗字は、その顧客が銀行を辞めるかどうかに影響を与えません。残りのカラムのうち、`Geography`, `Gender`, `HasCrCard`, `IsActiveMember` カラムはカテゴリーカラムとして扱うことができます。これらのカラムのリストを作ってみましょう。

categorical_columns = [‘Geography’, ‘Gender’, ‘HasCrCard’, ‘IsActiveMember’]


Exited` カラム以外の残りのカラムは、数値のカラムとして扱うことができます。

numerical_columns = [‘CreditScore’, ‘Age’, ‘Tenure’, ‘Balance’, ‘NumOfProducts’, ‘EstimatedSalary’]


最後に、出力(`Exited` カラムの値)を `outputs` 変数に格納する。

outputs = [‘Exited’]


ここまでで、カテゴリカラム、数値カラム、出力カラムのリストを作成しました。しかし、現時点では、カテゴリカルカラムの型はカテゴリカルではありません。以下のスクリプトで、データセット内のすべてのカラムの型を確認することができます。

dataset.dtypes


出力する。

RowNumber int64
CustomerId int64
Surname object
CreditScore int64
Geography object
Gender object
Age int64
Tenure int64
Balance float64
NumOfProducts int64
HasCrCard int64
IsActiveMember int64
EstimatedSalary float64
Exited int64
dtype: object


Geography` と `Gender` カラムの型は object で、`HasCrCard` と `IsActive` カラムの型は int64 であることがわかる。カテゴリカラムの型は `category` に変換する必要があります。以下のように、 `astype()` 関数を使用して変換することができます。

for category in categorical_columns:
dataset[category] = dataset[category].astype(‘category’)


ここで、もう一度データセットのカラムの型をプロットしてみると、以下のような結果になります。

dataset.dtypes


出力

RowNumber int64
CustomerId int64
Surname object
CreditScore int64
Geography category
Gender category
Age int64
Tenure int64
Balance float64
NumOfProducts int64
HasCrCard category
IsActiveMember category
EstimatedSalary float64
Exited int64
dtype: object


それでは、`Geography`カラムのすべてのカテゴリを見てみましょう。

dataset[‘Geography’].cat.categories


出力してみましょう。

出力: ```
Index(['France', 'Germany', 'Spain'], dtype='object')


列のデータ型をカテゴリーに変更すると、列の各カテゴリーに一意のコードが割り当てられます。

例えば、Geography列の最初の5行をプロットして、そのコード値を出力してみましょう。

dataset['Geography'].head()


出力されます。

0    France
1     Spain
2    France
3    France
4     Spain
Name: Geography, dtype: category
Categories (3, object): [France, Germany, Spain]


次のスクリプトは Geography カラムの最初の5行の値に対するコードをプロットします。

dataset['Geography'].head().cat.codes


出力

0    0
1    2
2    0
3    0
4    2
dtype: int8


出力は、Franceが0、Spainが2としてコード化されたことを示している。

カテゴリーカラムを数値カラムから分離する基本的な目的は、数値カラムの値を直接ニューラルネットワークに与えることができるためである。

しかし、カテゴリカルカラムの値は、まず数値型に変換される必要がある。

カテゴリカラムの値をコード化することで、カテゴリカラムの数値変換のタスクを部分的に解決しています。

モデルの学習にはPyTorchを使うので、カテゴリカルカラムと数値カラムをテンソルに変換する必要があります。

まず、カテゴリカルカラムをテンソルに変換してみよう。

PyTorchでは、テンソルはnumpyの配列を介して作成することができます。

以下のスクリプトのように、まず4つのカテゴリーカラムのデータをnumpy配列に変換し、すべてのカラムを水平に積み重ねることにします。

geo = dataset['Geography'].cat.codes.values
gen = dataset['Gender'].cat.codes.values
hcc = dataset['HasCrCard'].cat.codes.values
iam = dataset['IsActiveMember'].cat.codes.values


categorical_data = np.stack([geo, gen, hcc, iam], 1)


categorical_data[:10]


上記のスクリプトは、カテゴリカルカラムの最初の10レコードを水平に積み重ねて表示する。

出力は以下の通りである。

出力は以下の通りである。

array([[0, 0, 1, 1],
       [2, 0, 0, 1],
       [0, 0, 1, 0],
       [0, 0, 0, 0],
       [2, 0, 1, 1],
       [2, 1, 1, 0],
       [0, 1, 1, 1],
       [1, 0, 1, 0],
       [0, 1, 0, 1],
       [0, 1, 1, 1]], dtype=int8)


さて、前述のnumpyの配列からテンソルを生成するには、単純に配列を torch モジュールの tensor クラスに渡せばよい。

カテゴリカルカラムのデータ型は torch.int64 であることを忘れないように。

categorical_data = torch.tensor(categorical_data, dtype=torch.int64)
categorical_data[:10]


出力

tensor([[0, 0, 1, 1],
        [2, 0, 0, 1],
        [0, 0, 1, 0],
        [0, 0, 0, 0],
        [2, 0, 1, 1],
        [2, 1, 1, 0],
        [0, 1, 1, 1],
        [1, 0, 1, 0],
        [0, 1, 0, 1],
        [0, 1, 1, 1]])


出力では、numpy のカテゴリデータの配列が tensor オブジェクトに変換されたことが分かる。

同じように、数値の列をテンソルに変換することができる。

numerical_data = np.stack([dataset[col].values for col in numerical_columns], 1)
numerical_data = torch.tensor(numerical_data, dtype=torch.float)
numerical_data[:5]


出力する。

tensor([[6.1900e+02, 4.2000e+01, 2.0000e+00, 0.0000e+00, 1.0000e+00, 1.0135e+05],
        [6.0800e+02, 4.1000e+01, 1.0000e+00, 8.3808e+04, 1.0000e+00, 1.1254e+05],
        [5.0200e+02, 4.2000e+01, 8.0000e+00, 1.5966e+05, 3.0000e+00, 1.1393e+05],
        [6.9900e+02, 3.9000e+01, 1.0000e+00, 0.0000e+00, 2.0000e+00, 9.3827e+04],
        [8.5000e+02, 4.3000e+01, 2.0000e+00, 1.2551e+05, 1.0000e+00, 7.9084e+04]])


出力では、最初の5行がデータセットの6つの数値カラムの値を含んでいるのが見えるだろう。

最後のステップは、出力されたnumpyの配列をtensorオブジェクトに変換することです。

outputs = torch.tensor(dataset[outputs].values).flatten()
outputs[:5]


出力

tensor([1, 0, 1, 0, 0])


では、カテゴリデータ、数値データ、そして対応する出力の形状をプロットしてみよう。

print(categorical_data.shape)
print(numerical_data.shape)
print(outputs.shape)


出力

torch.Size([10000, 4])
torch.Size([10000, 6])
torch.Size([10000])


モデルを学習する前に、1つの非常に重要なステップがあります。

我々は、カテゴリカラムを、一意な値が1つの整数で表される数値データに変換しました。

例えば、Geographyのカラムでは、Franceは0、Germanyは1で表されます。

予測のためのモデル作成

データをトレーニングセットとテストセットに分けたので、次はトレーニング用のモデルを定義します。

そのために、モデルを学習するためのクラス Model を定義します。

以下のスクリプトを見てください。

categorical_column_sizes = [len(dataset.cat.categories) for column in categorical_columns]
categorical_embedding_sizes = [(col_size, min(50, (col_size+1)//2)) for col_size in categorical_column_sizes]
print(categorical_embedding_sizes)


PyTorchを使ったことがない人にとっては、上のコードは難しく見えるかもしれません。

しかし、このスクリプトを噛み砕いて説明します。

最初の行では、PyTorchの nn モジュールの Module クラスを継承した Model クラスを宣言しています。

このクラスのコンストラクタ (__init__() メソッド) には、次のパラメータが渡されます。

    1. embedding_size: カテゴリカルカラムのエンベッディングサイズが格納される。
    1. num_numerical_cols: 数値カラムの総数を格納する。
  1. 出力サイズ output_size: 出力レイヤーのサイズ、または可能な出力の数。
    1. layers: すべてのレイヤーのニューロン数を含むリスト。
  2. p: ドロップアウト。デフォルト値は 0.5 である。

コンストラクタの内部では、いくつかの変数が初期化されます。

まず、all_embeddings 変数には、すべてのカテゴリカルカラムの ModuleList オブジェクトのリストが格納されます。

また、embedding_dropout 変数には、すべてのレイヤーのドロップアウト値が格納されます。

最後に、batch_norm_num には、すべての数値カラムの BatchNorm1d オブジェクトのリストが格納されます。

次に、入力層の大きさを求めるために、カテゴリカラムと数値カラムの数を足し合わせて input_size 変数に格納する。

その後、for ループが繰り返され、対応するレイヤーが all_layers リストに追加される。

追加されるレイヤーは以下の通りです。

  • Linear: 入力とウェイト行列の内積を計算するために使用されます。
  • ReLu: 活性化関数として適用されます。
  • BatchNorm1d: 数値列を一括して正規化する際に利用されます。
  • Dropout: オーバーフィッティングを回避するために使用される

forループの後、出力層がレイヤーリストに追加されます。

ニューラルネットワークのすべてのレイヤーを順次実行させたいので、レイヤーリストはnn.Sequential` クラスに渡されます。

次に、forward メソッドでは、カテゴリカラムと数値カラムの両方が入力として渡される。

カテゴリカルカラムの埋め込みは以下の行で行われる。

[(3, 2), (2, 1), (2, 1), (2, 1)]


数値カラムの正規化は以下のスクリプトで一括して行われる。

total_records = 10000
test_records = int(total_records * .2)


categorical_train_data = categorical_data[:total_records-test_records]
categorical_test_data = categorical_data[total_records-test_records:total_records]
numerical_train_data = numerical_data[:total_records-test_records]
numerical_test_data = numerical_data[total_records-test_records:total_records]
train_outputs = outputs[:total_records-test_records]
test_outputs = outputs[total_records-test_records:total_records]


最後に、埋め込まれたカテゴリカラム x と数値カラム x_numerical は連結され、シーケンシャルな layers に渡される。

モデルのトレーニング

モデルを学習するために、まず前節で定義した Model クラスのオブジェクトを作成する必要があります。

print(len(categorical_train_data))
print(len(numerical_train_data))
print(len(train_outputs))


print(len(categorical_test_data))
print(len(numerical_test_data))
print(len(test_outputs))


カテゴリ列の埋め込みサイズ、数値列の数、出力サイズ(この例では2)、隠れ層のニューロンを渡しているのがわかる。

隠れ層が3つあり、それぞれ200、100、50ニューロンであることがわかります。

必要であれば、他のサイズも選択することができます。

モデルを印刷して、どのように見えるか見てみましょう。

8000
8000
8000
2000
2000
2000


出力。

出力:“`
class Model(nn.Module):

def init(self, embedding_size, num_numerical_cols, output_size, layers, p=0.4):
super().init()
self.all_embeddings = nn.ModuleList([nn.Embedding(ni, nf) for ni, nf in embedding_size])
self.embedding_dropout = nn.Dropout(p)
self.batch_norm_num = nn.BatchNorm1d(num_numerical_cols)

all_layers = []
num_categorical_cols = sum((nf for ni, nf in embedding_size))
input_size = num_categorical_cols + num_numerical_cols

for i in layers:
all_layers.append(nn.Linear(input_size, i))
all_layers.append(nn.ReLU(inplace=True))
all_layers.append(nn.BatchNorm1d(i))
all_layers.append(nn.Dropout(p))
input_size = i

all_layers.append(nn.Linear(layers[-1], output_size))

self.layers = nn.Sequential(*all_layers)

def forward(self, x_categorical, x_numerical):
embeddings = []
for i,e in enumerate(self.all_embeddings):
embeddings.append(e(x_categorical[:,i]))
x = torch.cat(embeddings, 1)
x = self.embedding_dropout(x)

x_numerical = self.batch_norm_num(x_numerical)
x = torch.cat([x, x_numerical], 1)
x = self.layers(x)
return x


最初の線形層では、6つの数値列があり、カテゴリ列の埋め込み次元の合計は5なので、6+5 = 11となり、変数 `in_features` の値は11であることがわかります。同様に、最後のレイヤーでは、`out_features`の値は2であり、可能な出力は2つだけである。

実際にモデルを学習する前に、モデルの学習に使用する損失関数とオプティマイザを定義する必要があります。今回は分類問題を解くので、クロスエントロピー損失を使用します。また、オプティマイザにはadamオプティマイザを使用します。

以下のスクリプトで、損失関数とオプティマイザを定義します。

embeddings = []
for i, e in enumerate(self.all_embeddings):
embeddings.append(e(x_categorical[:,i]))
x = torch.cat(embeddings, 1)
x = self.embedding_dropout(x)


これでモデルを学習するのに必要なものが揃いました。以下のスクリプトでモデルを学習させます。

x_numerical = self.batch_norm_num(x_numerical)


エポック数は300に設定されています。これはモデルを学習するために、完全なデータセットを300回使用することを意味します。for` ループが300回実行され、各反復の間、損失関数を使って損失が計算される。各反復の間の損失は `aggregated_loss` リストに追加される。重みを更新するために、`single_loss` オブジェクトの `backward()` 関数が呼び出されます。最後に、`optimizer` 関数の `step()` メソッドが勾配を更新します。25 エポックごとに損失が表示されます。

上のスクリプトの出力は以下のようになります。

model = Model(categorical_embedding_sizes, numerical_data.shape[1], 2, [200,100,50], p=0.4)


次のスクリプトは、損失をエポックに対してプロットします。

print(model)


出力

<div class="lazyload-wrapper"<div class="lazyload-placeholder" style="height:24rem"</div</div

出力は、最初は損失が急速に減少していることを示している。250回目のエポックの後、損失はほとんど減少していない。



### 予想すること

最後のステップは、テストデータに対して予測を行うことです。そのためには、 `categorical_test_data` と `numerical_test_data` を `model` クラスに渡せばよいだけです。そして、返された値を実際のテスト出力の値と比較することができます。次のスクリプトはテストクラスに対して予測を行い、テストデータに対するクロスエントロピー損失を表示します。

Model(
(all_embeddings): ModuleList(
(0): Embedding(3, 2)
(1): Embedding(2, 1)
(2): Embedding(2, 1)
(3): Embedding(2, 1)
)
(embedding_dropout): Dropout(p=0.4)
(batch_norm_num): BatchNorm1d(6, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(layers): Sequential(
(0): Linear(in_features=11, out_features=200, bias=True)
(1): ReLU(inplace)
(2): BatchNorm1d(200, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(3): Dropout(p=0.4)
(4): Linear(in_features=200, out_features=100, bias=True)
(5): ReLU(inplace)
(6): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(7): Dropout(p=0.4)
(8): Linear(in_features=100, out_features=50, bias=True)
(9): ReLU(inplace)
(10): BatchNorm1d(50, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(11): Dropout(p=0.4)
(12): Linear(in_features=50, out_features=2, bias=True)
)
)


出力します。

loss_function = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)


テストセットでの損失は0.3685で、トレーニングセットで達成された0.3465よりわずかに多く、我々のモデルがわずかにオーバーフィットしていることを示しています。

出力層は2つのニューロンを含むと指定したので、各予測は2つの値を含むことに注意することが重要です。例えば、最初の5つの予測値は次のようなものです。

epochs = 300
aggregated_losses = []

for i in range(epochs):
i += 1
y_pred = model(categorical_train_data, numerical_train_data)
single_loss = loss_function(y_pred, train_outputs)
aggregated_losses.append(single_loss)

if i%25 == 1:
print(f’epoch: {i:3} loss: {single_loss.item():10.8f}’)

optimizer.zero_grad()
single_loss.backward()
optimizer.step()

print(f’epoch: {i:3} loss: {single_loss.item():10.10f}’)


出力。

epoch: 1 loss: 0.71847951
epoch: 26 loss: 0.57145703
epoch: 51 loss: 0.48110831
epoch: 76 loss: 0.42529839
epoch: 101 loss: 0.39972275
epoch: 126 loss: 0.37837571
epoch: 151 loss: 0.37133673
epoch: 176 loss: 0.36773482
epoch: 201 loss: 0.36305946
epoch: 226 loss: 0.36079505
epoch: 251 loss: 0.35350436
epoch: 276 loss: 0.35540250
epoch: 300 loss: 0.3465710580


このような予測の背後にある考え方は、実際の出力が0である場合、インデックス0の値はインデックス1の値よりも高く、その逆もまた然りであるべきだということである。次のスクリプトでリストの中で最大の値のインデックスを取り出すことができる。

plt.plot(range(epochs), aggregated_losses)
plt.ylabel(‘Loss’)
plt.xlabel(‘epoch’);


出力する。

ここでもう一度、`y_val`リストの最初の5つの値を表示してみましょう。

with torch.no_grad():
y_val = model(categorical_test_data, numerical_test_data)
loss = loss_function(y_val, test_outputs)
print(f’Loss: {loss:.8f}’)


出力

Loss: 0.36855841


元々予測された出力のリストでは、最初の5つのレコードについて、ゼロインデックスの値が最初のインデックスの値よりも大きいので、処理された出力の最初の5行に0を見ることができます。

最後に、 `sklearn.metrics` モジュールの `confusion_matrix`, `accuracy_score`, `classification_report` クラスを用いて、テストセットの精度、正確さ、再現率を、混乱行列と一緒に求めることができます。

print(y_val[:5])


出力します。

tensor([[ 1.2045, -1.3857],
[ 1.3911, -1.5957],
[ 1.2781, -1.3598],
[ 0.6261, -0.5429],
[ 2.5430, -1.9991]])

“`

出力は、我々のモデルが84.65%の精度を達成したことを示しています。

これは、ニューラルネットワークモデルのすべてのパラメータをランダムに選択したという事実を考えると、かなり印象的です。

私は、より良い結果を得ることができるかどうかを確認するために、モデルのパラメータ、すなわち、訓練/テストの分割、隠れ層の数とサイズ、などを変更してみることをお勧めします。

結論

PyTorchはFacebookが開発した、分類、回帰、クラスタリングなど様々なタスクに使用できる一般的なディープラーニングライブラリです。

この記事では、表形式データの分類にPyTorchライブラリを使用する方法について説明します。

タイトルとURLをコピーしました