scikit-learn機械学習⑥ロジスティック回帰:実践編
前回は、ロジスティック回帰の理論的な側面を解説しました。今回は、scikit-learnを使ってロジスティック回帰を実装してみます。
今回は、scikit-learnの乳癌のデータセットを扱います。目的は、各患者に対して30個ある数値の入力データからその人が悪性の癌を患っているのかどうかを予測することです。つまり、ロジスティック回帰を使って悪性のガンである確率を予測します。
いつものように訓練用とテスト用にデータを分け、訓練用のデータで簡単な分析を行います。さらに、モデルを構築して、機械学習を行った後にモデルの評価をします。特に混同行列を使ったモデルの評価を詳しく解説します。
Python環境の設定
Pythonの環境設定については以前に詳しく解説しました。今回も、仮想環境を作ってscikit-learnなど必要なライブラリをインストールします。
mkdir logistic_regression
cd logistic_regression
python3 -m venv venv
source venv/bin/activate
# pip をアップグレードしておく
pip install --upgrade pip
# 必要なライブラリをインストール
pip install scikit-learn jupyter matplotlib pandasJupyterノートブックを立ち上げてPython3のノートブックを作成してください。Jupyterノートブックに関しては、こちらを参照してください。
あるいは、VSCode(Visual Studio Code)を使ってJupyterノートブックを作成しても構いません。私は、どちらかというとVSCodeをよく使います。これについてもこちらで簡単に解説しています。
データの読み込み
乳癌のデータセットは、scikit-learn から次のように読み込みます。
from sklearn.datasets import load_breast_cancer
data = load_breast_cancer(as_frame=True)今回は、as_frame=True と指定して pandas のデータフレームとして読み込みました。
データセットの説明は、次のように確認できます。
print(data.DESCR)説明は長いので一部のみをここに掲載します。
.. _breast_cancer_dataset:
Breast cancer wisconsin (diagnostic) dataset
--------------------------------------------
**Data Set Characteristics:**
:Number of Instances: 569
:Number of Attributes: 30 numeric, predictive attributes and the class
:Attribute Information:
- radius (mean of distances from center to points on the perimeter)
- texture (standard deviation of gray-scale values)
- perimeter
- area
- smoothness (local variation in radius lengths)
- compactness (perimeter^2 / area - 1.0)
- concavity (severity of concave portions of the contour)
- concave points (number of concave portions of the contour)
- symmetry
- fractal dimension ("coastline approximation" - 1)
The mean, standard error, and "worst" or largest (mean of the three
worst/largest values) of these features were computed for each image,
resulting in 30 features. For instance, field 0 is Mean Radius, field
10 is Radius SE, field 20 is Worst Radius.
- class:
- WDBC-Malignant
- WDBC-Benignこれを読むと569個のデータがあり、各々のデータには30個の数値が含まれています。ラベル(ターゲット)としては次の二つのクラスになります。
WDBC-Malignant
WDBC-Benign
WDBCは、Wisconsin Diagnostic Breast Cancerの略です。ラベルから、悪性(Malignant)と良性(Benign)の2種類の腫瘍のデータが含まれているのがわかります。
ターゲットの値を次のように表示してみます。
data.target.values以下の値が表示されます。
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0,
1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0,
1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1,
1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0,
0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1,
1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1,
1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0,
0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0,
1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1,
1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0,
0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0,
1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1,
1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1,
1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1,
1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1,
1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1])0と1のどちらが悪性なのかよくわかりません。
そこで、ターゲットの名前を確認します。
data.target_names次のように、ターゲットの名前が表示されました。
array(['malignant', 'benign'], dtype='<U9')つまり、ターゲットが0は悪性で、1は良性の腫瘍ということです。
データの探索的分析
このデータの分析をするには、医学的な知識(ドメイン知識)があった方が有利でしょう。しかし、私は医学的なことはよくわかりませんので、今回は与えられた入力データの数値から予測することだけを考えます。
まずは、データを見てみましょう。
data.frame.head(n=10)head(n=10) は先頭の10行を表示します。

ただ、デフォルトの設定だと、全てのカラムを表示しきれていません。表示されないカラムは … となって隠れています。そこで次のようにカラム表示数の最大値を変更することで全てのカラムを表示することがでいます。
# データフレームの最大表示列数を設定
import pandas as pd
pd.set_option('display.max_columns', 50)
data.frame.describe()これで全てのカラムを表示できました。左右にスクロールして確認してみてください。カラム(列)の数は31個あり、一番右がターゲット(0が悪性、1が良性)になっています。
つまり、各行には、予測するための数値が30個あります。また、各カラムの数値は大小さまざまなばらつきがあります。なので、各数値の範囲を見てみましょう。pandas のデータフレーム(DataFrame )の describe() メソッドを呼び出します。
data.frame.describe()以下のように統計値が表示されます。

なお、次のようにディクショナリ形式にすることもできます。
data.frame.describe().to_dict()これだと縦に長いのでやはりスクロールしないと収まりきらないです。
{'mean radius': {'count': 569.0,
'mean': 14.127291739894552,
'std': 3.5240488262120775,
'min': 6.981,
'25%': 11.7,
'50%': 13.37,
'75%': 15.78,
'max': 28.11},
'mean texture': {'count': 569.0,
'mean': 19.289648506151142,
'std': 4.301035768166949,
'min': 9.71,
'25%': 16.17,
'50%': 18.84,
'75%': 21.8,
'max': 39.28},
'mean perimeter': {'count': 569.0,
'mean': 91.96903339191564,
'std': 24.298981038754906,
'min': 43.79,
'25%': 75.17,
'50%': 86.24,
'75%': 104.1,
'max': 188.5},
... # 長いので省略VSCodeを使っているならば、pandas のデータフレームをテキスト表示することもできます。セルの左にある … をクリックすると以下のメニューが登場します。

Change Presentation をクリックすると、「text/html」か「text/plain」を選べるようになります。

「text/plain」を選ぶと以下のように表示されます。
mean radius mean texture mean perimeter mean area \
count 569.000000 569.000000 569.000000 569.000000
mean 14.127292 19.289649 91.969033 654.889104
std 3.524049 4.301036 24.298981 351.914129
min 6.981000 9.710000 43.790000 143.500000
25% 11.700000 16.170000 75.170000 420.300000
50% 13.370000 18.840000 86.240000 551.100000
75% 15.780000 21.800000 104.100000 782.700000
max 28.110000 39.280000 188.500000 2501.000000
mean smoothness mean compactness mean concavity mean concave points \
count 569.000000 569.000000 569.000000 569.000000
mean 0.096360 0.104341 0.088799 0.048919
std 0.014064 0.052813 0.079720 0.038803
min 0.052630 0.019380 0.000000 0.000000
25% 0.086370 0.064920 0.029560 0.020310
50% 0.095870 0.092630 0.061540 0.033500
75% 0.105300 0.130400 0.130700 0.074000
max 0.163400 0.345400 0.426800 0.201200
mean symmetry mean fractal dimension ... worst texture \
count 569.000000 569.000000 ... 569.000000
mean 0.181162 0.062798 ... 25.677223
std 0.027414 0.007060 ... 6.146258
min 0.106000 0.049960 ... 12.020000
25% 0.161900 0.057700 ... 21.080000
50% 0.179200 0.061540 ... 25.410000
75% 0.195700 0.066120 ... 29.720000
max 0.304000 0.097440 ... 49.540000
worst perimeter worst area worst smoothness worst compactness \
count 569.000000 569.000000 569.000000 569.000000
mean 107.261213 880.583128 0.132369 0.254265
std 33.602542 569.356993 0.022832 0.157336
min 50.410000 185.200000 0.071170 0.027290
25% 84.110000 515.300000 0.116600 0.147200
50% 97.660000 686.500000 0.131300 0.211900
75% 125.400000 1084.000000 0.146000 0.339100
max 251.200000 4254.000000 0.222600 1.058000
worst concavity worst concave points worst symmetry \
count 569.000000 569.000000 569.000000
mean 0.272188 0.114606 0.290076
std 0.208624 0.065732 0.061867
min 0.000000 0.000000 0.156500
25% 0.114500 0.064930 0.250400
50% 0.226700 0.099930 0.282200
75% 0.382900 0.161400 0.317900
max 1.252000 0.291000 0.663800
worst fractal dimension target
count 569.000000 569.000000
mean 0.083946 0.627417
std 0.018061 0.483918
min 0.055040 0.000000
25% 0.071460 0.000000
50% 0.080040 1.000000
75% 0.092080 1.000000
max 0.207500 1.000000
[8 rows x 31 columns]これだともう少しコンパクトに見渡せます。ただし、この設定は、後のセルにも影響するので、後でテーブルやグラフなどを表示したい場合は、切り替える必要があります。以上、いくつかの表示方法がありますが、好みで使い分けてください。
なお、各統計値の意味は以下になります。
個数(count)
平均(mean)
標準偏差(std)
最小値(min)
25パーセンタイル(25%)
50パーセンタイル(50%)
75パーセンタイル(75%)
最大値(max)
25パーセンタイル(25%)は、統計学における分位数の一つで、データセットを小さい方から大きい方へ並べたときに、全体の25%のデータがこの値以下になる点を指します。これは第1四分位数(Q1)とも呼ばれ、データの下位25%を上位75%から分ける値です。
例えば、ある試験の得点が100人分あり、これらを昇順に並べたとします。25パーセンタイル(第1四分位数)は、下から数えて25番目の得点に相当し、この得点以下の学生は全体の25%にあたります。この値は、データの分布を理解するのに役立ち、特にデータの散らばり具合や外れ値の影響を調べる際に使用されます。
四分位数は、データセットの中央値(メディアン、50パーセンタイル)や第3四分位数(75パーセンタイル)とともに、データの分布を記述するのに役立つ要約統計量です。これらの統計量を用いることで、データの中央値だけでなく、データの広がりや偏りも理解することができます。
比較のためにボックスプロットで各カラムの値の範囲をグラフ化しましょう。
import matplotlib.pyplot as plt
# 仮のデータフレームを生成(各カラムにはランダムなデータを使用)
df = data.frame.iloc[:, :30]
# 図のサイズを設定
plt.figure(figsize=(20, 10))
# ボックスプロットを描画
df.boxplot()
# タイトルと軸ラベルの設定
plt.title('Boxplot for 30 Columns')
plt.xlabel('Columns')
plt.ylabel('Values')
# グラフの表示
plt.xticks(rotation=90) # x軸のラベルを90度回転して表示
plt.tight_layout() # グラフのレイアウトを自動調整
plt.show()ちょっと小さくて見難いですが、以下のように表示されます。

こうしてみるといくつかのカラムでは値の範囲が比較的に大きくなっているのがわかります。
例えば、一番左の mean radius(平均半径)と左から4番目の mean area(平均領域)を比較します。
data.frame.iloc[:, [0, 3]].boxplot()
この二つの統計値は以下のように確認できます。
data.frame.iloc[:, [0, 3]].describe()
平均(mean)と標準偏差(std)を比較するとその違いが明確にわかります。
なお、大きな丸で表示されている点は、一般的に「外れ値」(Outliers)を示しています。ボックスプロットでは、箱の部分がデータの中央50%を表します。つまり、第1四分位数(25パーセンタイル)から第3四分位数(75パーセンタイル)までです。そして、中央の線が中央値(メディアン、50パーセンタイル)を示します。一方、外れ値はこれらの範囲から大きく離れたデータポイントを指しています。
以上より、このデータセットの中のいくつかのカラムはその値の範囲が比較的大きくなっています。各カラムがデータの特徴量を表しているので、大きな値は、少量の係数(傾き)の変化でも予測値に与える影響が大きくなります。
よって、各カラムで特徴量のスケールが異なると、最適化アルゴリズムが収束するまでに必要な時間が長くなることがあります。このため、後で見るように、特徴量を同じスケールに正規化します。各特徴量がモデルに与える影響が均一になり、最適化アルゴリズムの効率が向上します。
データの分割と正規化
まず、訓練用とテスト用にデータを分割します。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
data.data, data.target, test_size=0.2, random_state=42)test_size = 0.2 と指定して、80%を訓練用、20%をテスト用にしました。random_state は、乱数シードを固定するためのものなのです。
ちなみに、乱数シードに42という数字を使う人(私も含めて)をたまに見受けますが、ダグラス・アダムズのSF作品『銀河ヒッチハイク・ガイド』に登場する「究極の疑問に対する答え」がこの数値の由来です。
生命、宇宙、そして万物についての究極の疑問の答え」を問われたスーパーコンピュータ、ディープ・ソートが750万年の計算の末に出した答えが、「42」である。
なので、もちろん 42 である必要はありません。固定値ならばなんでもOKです。コンピュータの乱数は擬似なので、同じシードを指定すると毎回同じ乱数列が使われるため、実験などで再現性を確保するために指定します。よって、指定しなくとも構いませんが、その場合は結果が毎回異なる可能性があることになります。
なお、訓練用とテスト用にデータの数は以下のように確認できます。
len(X_train), len(X_test)この出力は、(455, 114)で、訓練用に455個のデータ、テスト用に114個のデータがあることになります。
次に、データの正規化を行います。
from sklearn.preprocessing import StandardScaler
# まず、訓練データから平均と標準偏差を計算する
scaler = StandardScaler()
scaler.fit(X_train)
# スケーラ(scaler)を使って、訓練データとテストデータを正規化する
X_train = scaler.transform(X_train)
X_test = scaler.transform(X_test)sklearn.preprocessing はデータの前処理用のツールが入っているライブラリです。ここからの StandardScaler (標準スケーラ)を使って訓練用データから各カラムごとに平均と標準偏差を計算します。このスケーラを使って訓練用データとテスト用データを正規化します。
正規化を行うことで各カラムの訓練データは平均が0で標準偏差が1になります。なお、テスト用のデータもほぼ平均が0で標準偏差が1になりますが、多少のずれは生じます。
実際に、値を見て見ましょう。
print('X_train 平均 :', np.mean(X_train))
print('X_train 標準偏差 :', np.std(X_train))
print('X_test 平均 :', np.mean(X_test))
print('X_test 標準偏差 :', np.std(X_test))
X_train の平均は非常に小さい正の値となっており、ゼロとみなして良いです。X_train の標準偏差は1です。これらと比較して、X_test の平均は0よりちょっと大きな値となっていますが、0に近いです。また、X_test の標準偏差は1に近い値となっていますが、1よりちょっと小さくなっています。
このように差が生じるのは、平均と標準偏差が訓練データから計算されているためです。訓練データのみを使って正規化のための平均と標準偏差を計算する理由は、テストで使うデータからの情報を訓練に使わないようにしているからです。
ただし、訓練用のデータもテスト用のデータも同じデータからランダムに選ばれたものなので、似たような分布になることを想定しています。また、将来的に発生する未知のデータも同じような分布を持つであろうと仮定しています。さもなければ、このような正規化の意味がなく、予測の精度も悪くなるでしょう。そのため、実際に機械学習を実用化するには、定期的にこのような想定や仮定が現在のデータとマッチしているのかを確認する必要があります。
ロジスティック回帰
では、正規化された訓練データを使ってロジスティック回帰を実行しましょう。
from sklearn.linear_model import LogisticRegression
model = LogisticRegression()
model.fit(X_train, y_train)これで、モデルのパラメータが訓練データに合わせてフィット(適合)されました。
次に、テストデータを使ってモデルによる分類結果の評価を行います。
from sklearn.metrics import classification_report
y_pred = model.predict(X_test)
print(classification_report(y_test, y_pred))
サポート(support)は、それぞれのクラス(0:悪性、1:良性)の数です。悪性が43個あり、良性が71個で、合計114個となっています。
混同行列(confusion matrix)
上述の結果を解説するために、まずは、下の図を眺めてください。

これは混同行列(confusion matrix)と呼ばれる表で、各用語の説明は次のようになります。ここでは、クラス0(悪性)が陽性(Positive)で、クラス1(良性)が陰性(Negative)として解説します。
TP(True Positive、真陽性)は、陽性を予測して、実際に陽性だった場合の数です。例えば、クラス0(悪性)を予測して、実際にそうだった数です。簡単にいうと、陽性を予測して正解だった数になります。
TN(True Negative、真陰性)は、陰性を予測して、実際に陰性だった場合の数です。例えば、クラス1(良性)を予測して、実際にそうだった数です。簡単にいうと、陰性を予測して正解だった数になります。
FP(False Positive、偽陽性)は、陽性を予測して、実際には陰性だった場合の数です。例えば、クラス0(悪性)を予測したのに、実際にはクラス1(良性)だった場合の数です。簡単にいうと陽性を予測して間違えた数になります。
FN(False Negative、偽陰性)は、陰性を予測して、実際には陽性だった場合の数です。例えば、クラス1(良性)を予測したのに、実際にはクラス0(悪性)だった場合の数です。簡単にいうと陰性を予測して間違えた数になります。
このでの陽性と陰性は定義の仕方によって決まるので、逆に、クラス0(悪性)が陰性(Negative)で、クラス1(良性)が陽性(Positive)と定義することもできます。この場合は、クラス1を予測しているというスタンスで問題を眺めていることになります。
以上の定義を使って精度、再現率、F1スコアを解説します。
精度(precision)
精度は、モデルがあるクラスを予測したうち、実際にそのクラスだった割合です。例えば、0(悪性)と予測して、実際に悪性だったのが98%あるということです。逆にいうと、良性なケースに対して悪性と予測したのが2%あったということになります。精度は適合率とも呼びます。
精度を式で表現すると以下になります。
$$
\text{precision} = \dfrac{\text{TF}}{\text{TF} + \text{FP}}
$$
なお、ここでは、クラス0(悪性)を陽性として捉えています。もちろん、クラス1(良性)を陽性として精度を計算することができますし、その結果として、クラス0とクラス1の両方の精度が評価の出力に含まれています。

再現率(recall)
再現率は、あるクラスの総数に対して、モデルがそのクラスを正しく予測した割合です。例えば、43個ある悪性のケースに対して95%を正確に予測したことになります。43個の悪性ケースの5%は見逃したことになります。
再現率を式で表現すると以下になります。
$$
\text{recall} = \dfrac{\text{TP}}{\text{TP} + \text{FN}}
$$
ここでも、クラス0(悪性)を陽性として捉えています。また、逆にクラス1(良性)を陽性として再現率を計算することもできます。よって、クラス0とクラス1の両方の再現率が評価の出力に含まれています。

なお、再現率は、真陽性率(True Positive Rate)とも呼ばれます。いろいろな呼び名が登場しますが、再現率がなにを意味するのかを理解すれば誤解が少なくなるでしょう。
F1スコア(f1-score)
病気の診断の予測では、再現率がなるべく高い方が良いでしょう。なぜなら悪性の腫瘍を見逃したくはないからです。
しかし、再現率を上げたいだけなら、全てのケースを悪性と予測して再現率を100%にすることもできます。ただし、この場合は精度が悪くなってしまいます。逆に精度を上げようとして、自信が高い時だけ悪性の予測すると再現率が下がることになりかねません。そこで精度と再現率を組み合わせた指標がF1スコアとなります。
F1スコアは、精度(precision)と再現率(recall)の調和平均で次のように定義されます。
$$
\text{F1スコア} = \dfrac{2 \times \text{precision} \times \text{recall}}{\text{precision} + \text{recall}}
$$
これを次のように表現した方が、理解しやすいかもしれません。
$$
\text{F1スコア} = \dfrac{\text{precision} \times \text{recall}}{\frac{\text{precision} + \text{recall}}{2}}
$$
つまり、精度と再現率を掛けたものを、精度と再現率の平均で割ったものになります。精度と再現率が両方とも100%ならばF1スコアも100%になります。
しかし、精度あるいは再現率のどちらか、あるいは両方とも低い値になると分子の方が掛け算の効果によってより小さくなります。よってF1スコアも小さくなります。つまり、精度と再現率の両方が1に近づくほどF1スコアが1に近づくことになるので、F1スコアを見ると両方の値がバランスよく高い値になっているかどうかを確認できます。
以上を踏まえて、精度、再現率、F1スコアを見てみると、このモデルの予測は、クラス0とクラス1の両方に対してある程度は良い評価になっているのがわかります。

ただし、クラス0(悪性)の再現率がやや低めなのが気にはなります。
正解率(accuracy)
正解率は陰性と陽性の予測全体に対しての正解の割合で、次のように定義されます。
$$
\text{accuracy} = \dfrac{\text{TP} + \text{TN}}{\text{TP} + \text{TN} + \text{FP} + \text{FN}}
$$
ここで、$${\text{TP} + \text{TN}}$$は、正解した全体の数です。また、$${\text{TP} + \text{TN} + \text{FP} + \text{FN}}$$は、全体の個数です。なので、意味としては次のようになります。
$$
\text{正解率} = \dfrac{正解した数}{全体の数}
$$
よって、正解率は最もわかりやすい評価の指標です。ただし、正解率だけでは問題が生じることがあります。例えば、陰性の数が極端に少ないなどクラス間でバランスが大きく崩れている場合です。仮に、100個のデータがあって、陰性が1個だけだとすると、常に陽性を予測すれば正解率は99%になります。しかし、これだと再現率は0なので好ましくありません。よって、再現率などを含めて総合的にモデルを評価する必要があります。

この結果では、全体の正解率は97%ですが、クラス0(悪性)の再現率は95%です。よって、正解率だけで評価すると再現率が比較的に低いことを見逃す可能性があります。
マクロ平均(macro average)
マクロ平均は、すべてのクラスからの単純平均を計算する方法です。精度、再現率、F1スコアに対してそれぞれ計算されます。
$$
\begin{aligned}
& \\
\text{精度のマクロ平均} &= \dfrac{\text{0の精度} + \text{1の精度}}{2}
\end{aligned}
$$
$$
\begin{aligned}
& \\
\text{再現率のマクロ平均} &= \dfrac{\text{0の再現率} + \text{1の再現率}}{2}
\end{aligned}
$$
$$
\begin{aligned}
& \\
\text{F1スコアのマクロ平均} &= \dfrac{\text{0のF1スコア} + \text{1のF1スコア}}{2}
\end{aligned}
$$
なお、式を短くするために、クラス0、クラス1を省略して0と1で表現しています。
精度、再現率、F1スコアのマクロ平均は下図に示したところで確認できます。

マクロ平均は、クラスのバランスが悪い(つまり、クラス間でサンプルサイズに大きな違いがある)時に特に有効です。なぜなら、マクロ平均はすべてのクラスを等しく扱うため、少数派のクラスが持つ性能も大多数派のクラスと同様に評価に影響を与えるからです。
このデータセットでは、クラス0(悪性)の方が少数派ですが、両方に対して精度、再現率、F1スコアがある程度高いのであまり違いは観測できません。
加重平均(weighted average)
加重平均は、クラスのサポート(つまりサンプルサイズ)に基づいて各クラスの評価値(精度、再現率、F1スコア)に重みを付けて平均を取ります。よって、サンプルサイズが大きいクラスは、平均値に大きな影響を与えます。
$$
\begin{aligned}
& \\
\text{精度の加重平均} &= \dfrac{\text{0の精度} \times \text{数} + \text{1の精度} \times \text{数}}{\text{0の数} + \text{1の数}}
\end{aligned}
$$
$$
\begin{aligned}
& \\
\text{再現率の加重平均} &= \dfrac{\text{0の再現率} \times \text{数} + \text{1の再現率}\times \text{数}}{\text{0の数} + \text{1の数}}
\end{aligned}
$$
$$
\begin{aligned}
& \\
\text{F1スコアの加重平均} &= \dfrac{\text{0のF1スコア} \times \text{数} + \text{1のF1スコア}\times \text{数}}{\text{0の数} + \text{1の数}}
\end{aligned}
$$

加重平均も、ある意味、クラスの不均衡がある場合に役立ちます。各クラスの重みがそのサポート(クラスのサンプルサイズ)に基づいているため、サンプルサイズが大きいクラスが全体の評価により大きな影響を与えます。つまり、少数派のクラスをある程度無視して良い場合には加重平均は良い指標となります。つまり、特定のクラスが多数を占め、そのクラスにおける良好なパフォーマンスが全体的な目標にとって重要な場合、加重平均はそのクラスの影響を適切に反映します。
なお、クラス間の数のバランスがほぼ等しい場合は、加重平均とマクロ平均はほぼ同じ値となります。
今回のデータセットでは、少数派であるクラス0(悪性)の予測が重要なのでマクロ平均の方が重要な指標となります。なぜなら、医療の文脈では、偽陰性(実際には悪性であるが、良性と予測されるケース)を最小限に抑えることが非常に重要だからです。悪性のケースを見逃すことは、患者にとって重大な健康上のリスクをもたらすため、マクロ平均は全てのクラスの予測性能を均等に評価し、特に少数派クラスのパフォーマンスを適切に反映することで、このタイプのエラーに対するモデルの感度を測定します。
次回予告
次回も、ロジスティック回帰の実験を続けます。新しいコンセプトとしてリッジ回帰やラッソ回帰についても触れる予定です。
お楽しみに!
