学生による学生のためのデータサイエンス勉強会

【さきどりPython#11】kaggleデータとロジスティック回帰で2クラス分類

https://bdarc.net/wp-content/uploads/2020/05/icon-1.pnghistoroid

こんにちは。清野(@historoid1)です。

今回はロジスティック回帰の2周目です。

kaggleのデータで、分類タスクを実装してみましょう。

kaggleデータとロジスティック回帰で2クラス分類

ロジスティック回帰とは

「回帰」といっていますが、ロジスティック回帰は分類モデルです。

ある観測値が特定のクラスに属している確率を予測します。

今回使用するデータ

今回使用するデータは、kaggleの乳がんのデータセットです。

「良性」か「悪性」かの2クラス分類を行います。

前回のおさらい

まずは前回のおさらいとして、scikit-learnのirisデータを使って実装を見ていきましょう。

2クラス分類での教科書の実装

ライブラリのインポート

from sklearn.linear_model import LogisticRegression
from sklearn import datasets
from sklearn.preprocessing import StandardScaler

まあ、このへんはいつもどおりですね。

データセットを整形

今からするのは2クラス分類なので、irisデータを2クラスに限定します。

iris = datasets.load_iris()
features = iris.data[:100, :]
target = iris.target[:100]

1クラスに50個の標本が用意されているので、100個だけ使います。

文法がよくわからん、という方は以下の記事を参考にしてください。

【Pythonによるデータ分析】Pandasの基本操作5選

特徴量を正規化する

scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)

正規化というのは、変数の分布を揃えることです。scikit-learnでは、平均0、標準偏差1にします。

モデルの作成と訓練

これまでの流れ含めて、まとめます。

from sklearn.linear_model import LogisticRegression
from sklearn import datasets
from sklearn.preprocessing import StandardScaler

iris = datasets.load_iris()
features = iris.data[:100, :]
target = iris.target[:100]

scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)

logistic_regression = LogisticRegression(random_state=0)

model = logistic_regression.fit(features_standardized, target)

新しい観測値の作成とクラスの推測

上記で訓練したモデルを使って、推測してみます。

new_observation = [[0.5, 0.5, 0.5, 0.5]]

model.predict(new_observation)
# array([1])

クラスが1であると返ってきました。

確率の確認

予測クラスを返すだけではなく、確率を返すこともできます。

model.predict_proba(new_observation)
# array([[0.17738424, 0.82261576]])

kaggleデータでの2クラス分類

データの確認

データの内容を確認します。

今回使用するデータは、乳房腫瘤の生検データです。乳房の場合は、注射で組織を吸引して細胞を採取します(穿刺吸引)。

今回のデータは、生検した細胞の核についてのデータです。

なお「腫瘤」や「腫瘍」には良性か悪性かの判断は含まれていません。生理的には生じない組織がある、という意味です。

変数名内容
ID Numberそのままですね。IDです。
Diagnosis診断。Mが悪性、Bが良性です。
radius半径。細胞核の平均半径です。
texture質感。「グレースケールの標準偏差」とあります。
癌細胞の核は、通常の核と比べてモザイク状に見えます。そのため白黒画像で見れば、白(255)と黒(0)の標準偏差が大きくなるわけですね。
perimeter周長。細胞核の周長です。
細胞核は円形ですが、細胞の性質によって楕円形になったり、滴状になったりします。
area面積。細胞核の面積です。
smoothness滑らかさ。「半径の変位量」と説明にありますが、核の見え方ですね。
丸く見えるのか、デコボコに見えるのか。
compactnessコンパクトさ。(半径の2乗 / 面積) – 1.0で計算します。細胞核が円なら面積の方がπ倍大きいので、核が円形なら1/π – 1.0で、-0.6816になる(なんでこんな指標に? π倍すれば百分率で表せるのに)。
concavity凹み具合。細胞核の不整の指標。
concave points凹み箇所の個数。
symmetry細胞核の対称性。
fractal dimensionフラクタル次元。乳癌の細胞診で見るべき指標の1つです。
やはり細胞核の不整についての尺度で、悪性度が大きいほどフラクタル次元が大きくなります。

これで変数の意味が分かりましたね。全部で30個くらい説明変数があるので、説明変数を1つずつ説明はしていませんが、平均、最小で分けているだけなので意味はわかると思います。

データのインポート

では実装していきましょう。

Google Driveのマウントとデータフレームの作成

from google.colab import drive
drive.mount('/content/drive')

import pandas as pd
df = pd.read_csv('/content/drive/My Drive/xxxx/xxxx.csv')

kaggleのサイトからCSVファイルをダウンロードして、Google Driveに入れておいてください。

パスは自分の環境に合わせて設定してください。

データフレームの形状の確認

df.shape
# (569, 33)

これで標本数が569個、説明変数が33個あることがわかりました。

説明変数は実際には、IDや空白列が含まれているので30個が変数として使えます。

欠損値の確認

お約束の欠損値の確認です。欠損値の確認は癖にしておいてくださいね。

df.isna().any()

'''
id                         False
diagnosis                  False
radius_mean                False
texture_mean               False
perimeter_mean             False
area_mean                  False
smoothness_mean            False
compactness_mean           False
concavity_mean             False
concave points_mean        False
symmetry_mean              False
fractal_dimension_mean     False
radius_se                  False
texture_se                 False
perimeter_se               False
area_se                    False
smoothness_se              False
compactness_se             False
concavity_se               False
concave points_se          False
symmetry_se                False
fractal_dimension_se       False
radius_worst               False
texture_worst              False
perimeter_worst            False
area_worst                 False
smoothness_worst           False
compactness_worst          False
concavity_worst            False
concave points_worst       False
symmetry_worst             False
fractal_dimension_worst    False
Unnamed: 32                 True
dtype: bool
'''

最後のUnmaned列だけがTrueでしたね。

この列はデータが何も入っていない列なので、削除してしまいましょう。

df.drop('Unnamed: 32', axis=1, inplace=True)
欠損値の有無

「欠損値を確認しましょう」と毎回注意していますが、欠損値があったらダメというわけではありません

どの列に、どれくらい欠損値が含まれているのか把握しましょう、という意味です。

目的変数

目的変数の設定

target = df['diagnosis']

これはとくに説明の必要はありませんね。診断結果が目的変数になります。

カテゴリラベルをカテゴリ変数に変換

現在、目的変数はB(良性)かM(悪性)です。

このままでもOKですが、せっかくなのでカテゴリ変数に変換する方法を学んでおきましょう。

from sklearn.preprocessing import LabelEncoder

target_enc = LabelEncoder().fit_transform(target)

こうすることによって、Bは0、Mは1に変換されます。

カテゴリに不均衡はないか確認

データ自体の構造も確認しておきましょう。もしほとんど良性だったら、それに合わせて対応しなければなりません。

import numpy as np

np.unique(target_enc, return_counts=True)

# (array([0, 1]), array([357, 212]))

クラス0つまり良性が357件、悪性が212件ですね。

データ自体に1.5倍くらいの差があることがわかりました。

説明変数

説明変数の確認をしましょう。

変数名の確認

df.columns

'''
Index(['id', 'diagnosis', 'radius_mean', 'texture_mean', 'perimeter_mean',
       'area_mean', 'smoothness_mean', 'compactness_mean', 'concavity_mean',
       'concave points_mean', 'symmetry_mean', 'fractal_dimension_mean',
       'radius_se', 'texture_se', 'perimeter_se', 'area_se', 'smoothness_se',
       'compactness_se', 'concavity_se', 'concave points_se', 'symmetry_se',
       'fractal_dimension_se', 'radius_worst', 'texture_worst',
       'perimeter_worst', 'area_worst', 'smoothness_worst',
       'compactness_worst', 'concavity_worst', 'concave points_worst',
       'symmetry_worst', 'fractal_dimension_worst'],
      dtype='object')
'''

全部で32列ありますが、’id’と’diagnosis’は変数としては使えません。

説明変数の設定

では必要な列だけを抜き出して、正規化も行いましょう。

features = df.iloc[:, 2:32]
scaler = StandardScaler()
features_standardized = scaler.fit_transform(features)

訓練データと検証データの分離

せっかくなので、訓練データと検証データを分離してみましょう。

訓練データと検証データの分離

from sklearn.model_selection import train_test_split

ftr_train, ftr_test, trgt_train, trgt_test = train_test_split(features_standardized, target_enc, test_size=0.2, random_state=0)

クラスの不均衡を確認

訓練データと検証データとで不均衡がないか調べます。

np.unique(trgt_train, return_counts=True)
# (array([0, 1]), array([290, 165]))

np.unique(trgt_test, return_counts=True)
# (array([0, 1]), array([67, 47]))

訓練データでは良性が悪性の1.7倍くらいで、検証データでは1.5倍くらいですね。

元データが約1.7倍なので、そこまでおかしな分離ではなさそうです(が、頭に入れておきましょう)。

モデルの作成と訓練、評価

モデルの作成と訓練

訓練データを使って、モデルの作成と訓練を行いましょう。

logistic_regression = LogisticRegression(random_state=0)
model = logistic_regression.fit(ftr_train, trgt_train)

検証

上記で作成したモデルの検証を行います。

model.score(ftr_test, trgt_test)

# 0.9649122807017544

ロジスティック回帰モデルのscoreは、正解率 Accuracyが返ってきます。

$$Accuracy=\frac{TP + TN}{TP+TN+FP+FN}$$

正解率は上記のように定義されます。

実際に陽性実際に陰性
予測で陽性True PositiveFalse Negative
予測で陰性False NegativeTrue Negative

注意

精度と正解率は違うものです。

精度 precision は適合率ともいいます。

$$Precision=\frac{TP}{TP+FP}$$

精度は上記のように定義されます。つまり陽性と判断(予測)されたもののうち、実際に陽性であるものの割合が精度です。

正解率は、すべての判断(予測)のうち、真陽性と真陰性の割合です。見ているものが違うので注意しましょう。

検証結果に対する考察

model.score(ftr_train, trgt_train)
# 0.989010989010989

model.score(ftr_test, trgt_test)
# 0.9649122807017544

訓練データの正解率は98.9%、検証データの正解率は96.5%です。まあおかしな値ではないと思います。

ここで前提を振り返ってみましょう。

  1. 全データにおいて、良性の割合は、悪性の割合よりも大きい。
  2. 訓練データにおける両者の割合は、全データでの割合と近い。
  3. 検証データにおける両者の割合は、全データと比較すると悪性の割合がやや大きい。

こんな感じでしたね。つまり「そもそも良性と推測すると当たる確率が高い」という背景があります。

したがって、本モデルでは「迷ったら良性」と判断するようなバイアスが生じやすいことが分かります。

さて、検証データでは「悪性の割合がやや大きい」ことがわかっています。そのため「迷ったら良性」と判断するモデルでは、不正解になる場合が多くなり、結果として訓練データよりも正解率が小さくなったと考えられます。

交差検証

ホールドアウト検証法

上記で行ってきた方法は、ホールドアウト検証法といいます。

全データを訓練データと検証データに分けて、検証データを未知のデータとして扱うわけです。

ホールドアウト検証法の欠点

  • モデルの性能が検証データに依存すること。
  • 全データを学習に使用できないこと。

ホールドアウト検証法では、検証用データセットでモデルの性能を評価します。したがって、検証用データセットが全データの性質をよく表していなければなりません。

そのため、検証用データセットでも良性と悪性の割合を調べていたのです。

また、全てのデータを学習に使えないのももったいないですよね。

しかし手元にある全てのデータで学習させると、検証できないというジレンマが生じます。

これらの問題を解決するのが、以下のk-分割交差検証法です。

k-分割交差検証法

KFCV: k-fold cross-validationでは、データをk分割します(自分で設定します)。

そのうち、k-1群を訓練データ、1群を検証データとして使います。

つまりk=10とすれば、全データの10%を検証データとして使うわけですね。

これをk回繰り返します。つまり訓練データと検証データを入れ替えて、k個のモデルを作ります。

そして、k個のモデルが学習した重みの平均値をモデル全体の性能とします。

k-分割交差検証法の実装

では実装していきましょう。途中までは、これまでと同じ方法です。

目的変数、説明変数の設定

# 説明変数
features = df.iloc[:, 2:32]

# 目的変数
from sklearn.preprocessing import LabelEncoder
target = df['diagnosis']
target = LabelEncoder().fit_transform(target)

# 標準化
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()

ここまではとくに変わりませんね。

パイプラインの作成

これから行う処理は先ほどと変わりませんが、k個分のモデルを作ることになります。

for文で回してもよいのですが、scikit-learnには一連の作業を処理するのに便利なメソッドがあります。それがパイプラインです。

# モデルの作成
from sklearn.linear_model import LogisticRegression
logit = LogisticRegression()

# パイプラインの作成
from sklearn.pipeline import make_pipeline
pipeline = make_pipeline(scaler, logit)

このパイプラインは、標準化して、ロジスティック回帰モデルに入れるということを表しています。

モデルの作成

# k-分割交差検証
from sklearn.model_selection import KFold
kf_model = KFold(n_splits=10, shuffle=True, random_state=0)

n_splitsがk分割の指定です。

モデルの訓練

# 訓練
from sklearn.model_selection import cross_val_score
cv_results = cross_val_score(pipeline,  # 使用するパイプライン
                             features,  # 説明変数
                             target,  # 目的変数
                             cv=kf_model,  # 使用する交差検証モデル
                             scoring='accuracy',  # 使用する評価関数
                             n_jobs=-1)  # 使用するCPUコア数

これまでは作成したモデルに対してfitさせていましたが、cross_val_scoreで直接評価を出すことができます。

結果

cv_results
'''
array([1.        , 0.94736842, 0.96491228, 1.        , 0.98245614,
       0.94736842, 0.96491228, 0.98245614, 1.        , 1.        ])
'''

1.0という結果が出ていますね。100%の正解率ということです。

「ほんとかよ」と思いますが、これがk-分割交差検証法です。今回は10分割したので、10個の正解率が出ています。

k-分割交差検証法の注意点

独立同分布

KFCVでは、すべての説明変数が独立同分布(IID: independent and identically distributed)であることを仮定しています。

IIDとは、それぞれの説明変数が独立であり、その確率分布も等しい、ことを指します。

https://bdarc.net/wp-content/uploads/2020/05/icon-1.pnghistoroid

ちょっとよくわからないので、勉強したらまたまとめます。

クラスの不均衡

やっぱりこの問題が出てきましたね。良性と悪性の割合が等しい方がKFCVには良いとされています。

scikit-learnには、クラスの比率が等しくなるようにk-分割する方法が備わっています。

これを層化k-分割交差検証法といいます。

# 層化k-分割交差検証
from sklearn.model_selection import  StratifiedKFold
kf_model = StratifiedKFold(n_splits=10, shuffle=True, random_state=0)

# StratifiedKFoldの結果
'''
array([0.94736842, 0.94736842, 0.96491228, 1.        , 1.        ,
       0.96491228, 0.98245614, 1.        , 0.98245614, 0.98214286])
'''

データの前処理は分けて行う

今回はパイプラインで行っているので意識していませんが、標準化(正規化)は訓練データと検証データとで分けて行う必要があります。

というのも、平均を0にしてSDを1にする、という行為はデータ全体の情報が含まれているためです。

分割した上で、標準化しないと意味がありません。


https://bdarc.net/wp-content/uploads/2020/05/icon-1.pnghistoroid

今回はここまでです。

ロジスティック回帰だけでなく、交差検証法についても理解できましたか?

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です