Elsaの技術日記(徒然なるままに)

主に自分で作ったアプリとかの報告・日記を記載

MENU

sklearnを用いたロジスティク回帰(LogisticRegression)

先日Kaggleを使えるようにしました。
せっかくなので、Notebooksを用いて、ロジスティク回帰を試してみようと思います!!
データは前回も用いているポケモンステータスがまとめてあるcsvファイルを用います!!

■ロジスティック回帰とは?

目的変数が2値の時に利用する回帰のこと。
例えば、この人は購入するかしないか、やお昼を食べたか否か、などの2択(2値)に対して予測を行うための回帰曲線の1つです。
ロジスティック回帰はこのような図になります。
f:id:Elsammit:20200926162959p:plain


計算式は
f:id:Elsammit:20200926163214p:plain
で表せます。

ロジスティック回帰は下記コードを実行するとグラフが表示されます。

import numpy as np
import matplotlib.pyplot as plt
 
# ロジスティック関数
def logistic_function(x):
    return 1/ (1+ np.exp(-x))
 
# プロットの作成
x = np.linspace(-10,10,1000)
plt.plot(x, logistic_function(x))
plt.title("logistic function")
plt.xlabel("x")
plt.ylabel("y")

■Scikit-learnを用いたロジスティク回帰

まずScikit-learnですが、機械学習のライブラリです。
Scikit-learnの公式ドキュメントは下記になります。
scikit-learn: machine learning in Python — scikit-learn 0.23.2 documentation

Scikit-learnでロジスティック回帰を行う場合には、
linear_modelのLogisticRegressionモデル
を用いればよいです。

pythonでScikit-learnでのロジスティック回帰を用いる場合には、下記をimportする必要があります。

from sklearn.linear_model import LogisticRegression

■ロジスティク回帰を用いたポケモン識別率評価

今回は例として、ノーマルとはがねタイプでの分類についてまとめたいと思います。
ソースコード全体はこちらとなります。

import pandas as pd
from pandas import plotting  
import codecs
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

#目的変数yのための項目を追加するための関数.
def type_to_num(p_type,test):
    if p_type == test:
        return 0
    else:
        return 1

def metal_normal():

    # ポケモンステータスがまとめられたcsvファイルを読み出し.
    with codecs.open("/kaggle/input/pokemon-status/pokemon_status.csv", "r", "utf-8", "ignore") as file:
        df = pd.read_table(file, delimiter=",")  
    
    # はがねタイプのポケモンを抽出.
    metal1 = df[df['タイプ1'] == "はがね"]
    metal2 = df[df['タイプ2'] == "はがね"]
    metal = pd.concat([metal1, metal2])

   # ノーマルタイプのポケモンを抽出.
    normal1 = df[df['タイプ1'] == "ノーマル"]
    normal2 = df[df['タイプ2'] == "ノーマル"]
    normal = pd.concat([normal1,normal2])

    # ノーマル +はがねタイプのポケモンリストを作成し、追加ではがねタイプか否かを判定するためのフラグを項目に追加.
    pokemon_m_n = pd.concat([metal, normal], ignore_index=True)
    type1 = pokemon_m_n["タイプ1"].apply(type_to_num,test="はがね")
    type2 = pokemon_m_n["タイプ2"].apply(type_to_num,test="はがね")
    pokemon_m_n["type_num"] = type1*type2
    pokemon_m_n.head()

    # X軸にポケモンのステータスを抽出。 y軸にはがねタイプか否かの判定結果を登録.
    X = pokemon_m_n.iloc[:, 7:13].values
    y = pokemon_m_n["type_num"].values

    # 訓練データとテストデータに分割
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=0)
    
    #ロジスティックモデル生成
    lr = LogisticRegression(C=1.0)
    
    #訓練データを用いてロジスティック回帰をフィッティング
    lr.fit(X_train, y_train)
     
    #判定結果
    print("-------------------------------------------------")
    print("trainデータに対するscore: %.3f" % lr.score(X_train, y_train))
    print("testデータに対するscore: %.3f" % lr.score(X_test, y_test))
    print("-------------------------------------------------")

metal_normal()

まず、csvファイルからポケモンステータスのリストを収集し、その中ではがねタイプもしくはノーマルタイプをポケモンを抽出します。
抽出にはpandasを用いました。

    # ポケモンステータスがまとめられたcsvファイルを読み出し.
    with codecs.open("/kaggle/input/pokemon-status/pokemon_status.csv", "r", "utf-8", "ignore") as file:
        df = pd.read_table(file, delimiter=",")  
    
    # はがねタイプのポケモンを抽出.
    metal1 = df[df['タイプ1'] == "はがね"]
    metal2 = df[df['タイプ2'] == "はがね"]
    metal = pd.concat([metal1, metal2])

   # ノーマルタイプのポケモンを抽出.
    normal1 = df[df['タイプ1'] == "ノーマル"]
    normal2 = df[df['タイプ2'] == "ノーマル"]
    normal = pd.concat([normal1,normal2])

次に抽出したノーマルタイプとはがねタイプとして抽出したリストを結合し、
結合したリストに目的変数yのためにはがねタイプか否かを判断するためのフラグ項目を追加します。
目的変数yを追加する処理は下記となります。

def type_to_num(p_type,test):
    if p_type == test:
        return 0
    else:
        return 1

type1 = pokemon_m_n["タイプ1"].apply(type_to_num,test="はがね")
type2 = pokemon_m_n["タイプ2"].apply(type_to_num,test="はがね")
pokemon_m_n["type_num"] = type1*type2

これで機械学習を行うためのデータ加工が完了しました。

次にロジスティック回帰モデルでの機械学習を行うために訓練データとテストデータを分割します。
分割する処理は下記となります。

    # X軸にポケモンのステータスを抽出。 y軸にはがねタイプか否かの判定結果を登録.
    X = pokemon_m_n.iloc[:, 7:13].values
    y = pokemon_m_n["type_num"].values

    # 訓練データとテストデータに分割
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=0)

test_sizeが用意したデータ内でテストデータとして用いる割合を示しております。

最後にロジスティック回帰モデルで機械学習を行い、テストデータによる解析結果を実施。

    #ロジスティックモデル生成
    lr = LogisticRegression(C=1.0)
    
    #訓練データを用いてロジスティック回帰をフィッティング
    lr.fit(X_train, y_train)
     
    #判定結果
    print("-------------------------------------------------")
    print("trainデータに対するscore: %.3f" % lr.score(X_train, y_train))
    print("testデータに対するscore: %.3f" % lr.score(X_test, y_test))
    print("-------------------------------------------------")


ノーマルとはがねタイプの分類結果ですが、

-------------------------------------------------
trainデータに対するscore: 0.954
testデータに対するscore: 0.943
-------------------------------------------------

となりました!!
かなり高い確率でノーマルタイプとはがねタイプを分類することが出来ました!!

■別データ群を用いて実際に識別したらどうなる?

先ほどの結果により、ロジスティック回帰モデルでノーマル・はがねタイプをかなり正確に分類させることが出来ました。

実は今回用いたcsvファイルには最新ゲームであるポケットモンター剣盾(剣盾)のデータは入っておりません。
では、今回のモデルを剣盾に出現するポケモンのステータスデータに適用した場合の識別率を評価したいと思います!!

先ほどの判定モデル生成コードに続けて下記を追記下さい。
※剣盾に出現するポケモンのデータが格納されたcsvファイル名は"pokemon.csv"とします。

    with codecs.open("pokemon.csv", "r", "utf-8", "ignore") as file:
        check = pd.read_table(file, delimiter=",")  
    
    metal11 = check[check['type1'] == "はがね"]
    metal22 = check[check['type2'] == "はがね"]
    metal = pd.concat([metal11, metal22])

    elec1 = check[check['type1'] == "ノーマル"]
    elec2 = check[check['type2'] == "ノーマル"]
    elec = pd.concat([elec1,elec2])

    pokemon_check = pd.concat([metal,elec], ignore_index=True)
    type11 = pokemon_check["type1"].apply(type_to_num,test="はがね")
    type22 = pokemon_check["type2"].apply(type_to_num,test="はがね")
    pokemon_check["type_num"] = type11*type22
    pokemon_check.head()

    X = pokemon_check.iloc[:, 1:7].values
    y = pokemon_check["type_num"].values

    i = 0
    error1 = 0
    success1 = 0
    error2 = 0
    success2 = 0
    print("[はがねタイプと判断したポケモン一覧]")
    print("----------------------------------------")
    print("")

    while i < len(pokemon_check):
        y_pred = lr.predict(X[i].reshape(1, -1))
        if y_pred == 0:

            if pokemon_check.loc[i, ["type_num"]].values == 0:
                success1 += 1
            else:
                error1 += 1
                print(pokemon_check.loc[i, ["name"]])
                print("はがねタイプではないです")
                print("")
        else:
            if pokemon_check.loc[i, ["type_num"]].values == 0:
                error2 += 1
                print(pokemon_check.loc[i, ["name"]])
                print("はがねタイプです")
                print("")
            else:
                success2 += 1
        i += 1
    print("----------------------------------------")
    print("正しくはがねタイプと判断したポケモンの数: %d匹" % success1)
    print("正しくはがねタイプではないと判断ポケモンの数: %d匹" % success2)
    print("誤ってはがねタイプと判断したポケモンの数: %d匹" % error1)
    print("誤ってはがねタイプではないと判断したポケモンの数: %d匹" % error2)
    print("識別率:%.3f%%" % ((success1+success2)/(error1+error2+success1+success2)*100))

下記までは、剣盾のポケモンデータを読み出して結合して~~なので割愛!!

 ~~~~
    X = pokemon_check.iloc[:, 1:7].values
    y = pokemon_check["type_num"].values

こちらで、剣盾で出現するノーマル or はがねタイプのポケモンデータから、
はがねタイプと識別できるか?はがねタイプではないと識別できるか?
を各ポケモンで判定するコードになります。

    while i < len(pokemon_check):
        y_pred = lr.predict(X[i].reshape(1, -1))
        if y_pred == 0:
            if pokemon_check.loc[i, ["type_num"]].values == 0:
                success1 += 1
            else:
                error1 += 1
                print(pokemon_check.loc[i, ["name"]])
                print("はがねタイプではないです")
                print("")
        else:
            if pokemon_check.loc[i, ["type_num"]].values == 0:
                error2 += 1
                print(pokemon_check.loc[i, ["name"]])
                print("はがねタイプです")
                print("")
            else:
                success2 += 1
        i += 1

それぞれ、
 ・success1:はがねタイプと正しく判定出来た場合に加算
 ・success2:はがねタイプではないと正しく判定出来た場合に加算
 ・error1:はがねタイプではないのにはがねタイプと判定してしまった場合に加算
 ・error2:はがねタイプなのにはがねタイプではないと判定してしまった場合に加算
となります。

ここで、ポケモンのステータスから目的変数yを想定するための処理は、

y_pred = lr.predict(X[i].reshape(1, -1))

で実行しております。

本処理を実行した結果、

-------------------------------------------------
[はがねタイプと判断したポケモン一覧]
----------------------------------------

name    ドーミラー
Name: 3, dtype: object
はがねタイプです

name    ガラルニャース
Name: 6, dtype: object
はがねタイプです

name    ゾウドウ
Name: 9, dtype: object
はがねタイプです

name    ダイオウドウ
Name: 10, dtype: object
はがねタイプです

name    アーマーガア
Name: 15, dtype: object
はがねタイプです

name    ドリュウズ
Name: 16, dtype: object
はがねタイプです

name    ガラルマッギョ
Name: 19, dtype: object
はがねタイプです

name    トゲデマル
Name: 25, dtype: object
はがねタイプです

name    ザシアン
Name: 26, dtype: object
はがねタイプです

name    バイウールー
Name: 36, dtype: object
はがねタイプではないです

name    ジジーロン
Name: 51, dtype: object
はがねタイプではないです

----------------------------------------
正しくはがねタイプと判断したポケモンの数: 19匹
正しくはがねタイプではないと判断ポケモンの数: 31匹
誤ってはがねタイプと判断したポケモンの数: 2匹
誤ってはがねタイプではないと判断したポケモンの数: 9匹
識別率:81.967%

識別率が80%台まで落ちてしまいました。。。泣
誤ってはがねタイプと判断したポケモンの数2匹に対して、
はがねタイプなのに誤ってしまったポケモンが9匹なので、正しくはがねタイプと判定出来ていないこと原因??
どうすればよいのか、までは分かっていない。。。


■最後に
初心者なりにロジスティック回帰モデルでポケモンのタイプ識別が行えました!!
はがねタイプとノーマルタイプを選んだ理由ですが、
実はpokemon_status.csvファイルで用意されたポケモンデータから識別率の高いタイプ組を選出しましたw。

ただ、識別率を上げるためにどうすればよいのか分からないため、更なる勉強が必要!!
これから機械学習も勉強していきます!!

■参考
ポケモンで学ぶ機械学習 - Qiita
Scikit-learn でロジスティック回帰(クラス分類編) - Qiita
ロジスティック回帰 - Qiita