
こんにちは。清野(@historoid1)です。
今回はロジスティック回帰の2周目です。
kaggleのデータで、分類タスクを実装してみましょう。

「回帰」といっていますが、ロジスティック回帰は分類モデルです。
ある観測値が特定のクラスに属している確率を予測します。

今回使用するデータは、kaggleの乳がんのデータセットです。
「良性」か「悪性」かの2クラス分類を行います。

まずは前回のおさらいとして、scikit-learnのirisデータを使って実装を見ていきましょう。
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個だけ使います。
文法がよくわからん、という方は以下の記事を参考にしてください。

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]])

データの内容を確認します。
今回使用するデータは、乳房腫瘤の生検データです。乳房の場合は、注射で組織を吸引して細胞を採取します(穿刺吸引)。
今回のデータは、生検した細胞の核についてのデータです。
なお「腫瘤」や「腫瘍」には良性か悪性かの判断は含まれていません。生理的には生じない組織がある、という意味です。
変数名 | 内容 |
---|---|
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つずつ説明はしていませんが、平均、最小で分けているだけなので意味はわかると思います。
では実装していきましょう。
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 Positive | False Negative |
予測で陰性 | False Negative | True 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%です。まあおかしな値ではないと思います。
ここで前提を振り返ってみましょう。
- 全データにおいて、良性の割合は、悪性の割合よりも大きい。
- 訓練データにおける両者の割合は、全データでの割合と近い。
- 検証データにおける両者の割合は、全データと比較すると悪性の割合がやや大きい。
こんな感じでしたね。つまり「そもそも良性と推測すると当たる確率が高い」という背景があります。
したがって、本モデルでは「迷ったら良性」と判断するようなバイアスが生じやすいことが分かります。
さて、検証データでは「悪性の割合がやや大きい」ことがわかっています。そのため「迷ったら良性」と判断するモデルでは、不正解になる場合が多くなり、結果として訓練データよりも正解率が小さくなったと考えられます。

上記で行ってきた方法は、ホールドアウト検証法といいます。
全データを訓練データと検証データに分けて、検証データを未知のデータとして扱うわけです。
- モデルの性能が検証データに依存すること。
- 全データを学習に使用できないこと。
ホールドアウト検証法では、検証用データセットでモデルの性能を評価します。したがって、検証用データセットが全データの性質をよく表していなければなりません。
そのため、検証用データセットでも良性と悪性の割合を調べていたのです。
また、全てのデータを学習に使えないのももったいないですよね。
しかし手元にある全てのデータで学習させると、検証できないというジレンマが生じます。
これらの問題を解決するのが、以下のk-分割交差検証法です。
KFCV: k-fold cross-validationでは、データをk分割します(自分で設定します)。
そのうち、k-1群を訓練データ、1群を検証データとして使います。
つまりk=10とすれば、全データの10%を検証データとして使うわけですね。
これを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個の正解率が出ています。
KFCVでは、すべての説明変数が独立同分布(IID: independent and identically distributed)であることを仮定しています。
IIDとは、それぞれの説明変数が独立であり、その確率分布も等しい、ことを指します。

ちょっとよくわからないので、勉強したらまたまとめます。
やっぱりこの問題が出てきましたね。良性と悪性の割合が等しい方が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にする、という行為はデータ全体の情報が含まれているためです。
分割した上で、標準化しないと意味がありません。

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