くますきIT日記

IT系資格、競技プログラミングの情報を書いていきます。

E資格の勉強②機械学習レポート

目次

1 本投稿の目的

機械学習に関する学習のまとめ

2 線形回帰モデル

【概要】

教師データありの回帰手法(回帰問題を解くためのモデルの1つ)

入力:m次元のパラメータ

出力:入力の線形結合※予測値は^(ハット)をつける。

パラメータの誤差は最小二乗法で推定する。

 

【ハンズオン】

ボストンの住宅データセットから住宅価格の最適な査定を行ってみる。

######################
# 線形回帰
######################
# モジュールをインポート
from sklearn.datasets import load_boston
from pandas import DataFrame
import numpy as np
from sklearn.linear_model import LinearRegression # 線形回帰モジュール

# データセットインスタンス
dataset = load_boston()
# print(dataset) # {'data': array([[6.3200e-03, 1.8000e+01],・・

# データフレーム作成
df = DataFrame(data=dataset.data, columns=dataset.feature_names)
df["PRICE"] = np.array(dataset.target)

######################
# パターン①線形単回帰分析(説明変数が1)
######################
# 説明変数(部屋数)と目的変数を設定
data = df.loc[:, ["RM"]].values
target = df.loc[:, ["PRICE"]].values

# 線形回帰インスタンス生成
model = LinearRegression()
# model.get_params() パラメータの確認
model.fit(data, target) # パラメータの推定

# 未知のデータを予測(部屋数1の価格は?)
print(model.predict([[1]]))
# [-25.5685118] → 価格が負数(明らかにおかしい) → 部屋数1という学習データが無いため予測ができない(外挿)

# 未知のデータを予測(外挿でない「部屋数6」の価格は?)
print(model.predict([[6]]))
# [19.94203311] → 妥当かもしれない価格を取得できた。

######################
# パターン②線形重回帰分析(説明変数が複数)
######################
# 説明変数(犯罪率と部屋数)と目的変数を設定
data = df.loc[:, ["CRIM", "RM"]].values
target = df.loc[:, ["PRICE"]].values

# 線形回帰インスタンス生成
model = LinearRegression()
# model.get_params() パラメータの確認
model.fit(data, target) # パラメータの推定

# 未知のデータを予測1(犯罪率0.3で部屋数4の価格は?)
print(model.predict([[0.3, 4]]))
# 4.24007956
# 未知のデータを予測2(犯罪率0.3で部屋数5の価格は?)
print(model.predict([[0.3, 5]]))
# 12.6311478
# → 部屋数が1つ増えると価格は急激に上昇。部屋数の方が重要度が高い?と思われる。

【ハンズオン後の考察】

外挿問題について、今回は「価格が負数」という分かりやすい問題となっていたが、

分析データによっては、外挿問題が発生していることに気付かない可能性もあると思われる。学習結果について、以下のような方法で検証が必要と思われる。

 ・正常と不正な出力の基準を明確にして、不正な結果が出力されないか。

 ・実務担当者による判定結果と比較して、想定外の差異が無いか。

 

 

【関連記事】

後述のロジスティック回帰は教師データありの分類手法。
回帰と分類の違いは、以下URLが分かりやすかった。

aiacademy.jp

 

3 非線形回帰モデル

【概要】

線形でない現象に対する回帰手法

入力、出力、パラメータの誤差:線形回帰と同様

汎化性能を失う過学習に注意する必要がある。(線形回帰も注意が必要)
過学習の防止にはいくつか手法があるが、オーソドックスなものは正則化法(罰則法)

 ・L2ノルムを利用したRidge推定量でパラメータを0に近づける

 ・L1ノルムを利用したLasso推定量でパラメータを0にする

過学習が発生する場合、汎化性能を失う→学習データ以外の分析精度が低下する。

 

【ハンズオン】

非線形のデータを適切に予測できるモデルを作成する。

######################
# 非線形回帰
######################
# モジュールをインポート
import numpy as np
import matplotlib.pyplot as plt
from sklearn.kernel_ridge import KernelRidge


def true_func(x):
return 1 - 48 * x + 218 * x ** 2 - 315 * x ** 3 + 145 * x ** 4


# データ(散布)生成
n = 100
data = np.random.rand(n).astype(np.float32)
data = np.sort(data)
target = true_func(data) # 非線形のグラフを作成
# 散布にノイズを付与
target = target + 0.5 * np.random.randn(n)

# データ(散布)を描画
plt.scatter(data, target, color='blue', label='data')
plt.title('NonLinear Regression')

# 非線形回帰(カーネルリッジ回帰)インスタンスを生成
clf = KernelRidge(alpha=0.0002, kernel='rbf')
data = data.reshape(-1, 1) # 2次元1列の配列への変形
target = target.reshape(-1, 1) # 2次元1列の配列への変形
# 学習
clf.fit(data, target)
p_kridge = clf.predict(data)

# 学習結果を描画
plt.plot(data, p_kridge, color='red', linestyle='-', linewidth=3, markersize=6, label='kernel ridge')
plt.legend()

plt.savefig("test.png")

作成した画像ファイルは以下の通り。
(非線形の分布にフィットしたグラフを作成できている!)

f:id:you_it_blog:20211005233937p:plain

 

【関連記事】

非線形回帰の理解には以下サイトがとっかかりとして良かった。

www.gixo.jp

 

4 ロジスティック回帰モデル

【概要】

教師データありの分類手法(分類問題を解くためのモデルの1つ)

※名称に「回帰」とついているが、分類手法。紛らわしい・・。

入力:数値とm次元パラメータの線形結合をシグモイド関数※に入力

   ※入力値を0~1の範囲の数値に変換する関数の一つ。

    シグモイド関数は1/( 1+exp ^-ax)。

    「a」を使用する場合、aが1以下で小さいほど、緩やかな出力となる。

出力:0 or 1の2択に対して1となる確率。コインの裏表や、~する、しない等。

誤差関数には尤度関数を使用する。

 

【ハンズオン】

タイタニック号の乗客の生存状況を学習して、特定の条件[男性、30歳]にて生存できるか確認する。

######################
# ロジスティック回帰
######################
# モジュールをインポート
import numpy as np
from sklearn.datasets import fetch_openml
from sklearn.linear_model import LogisticRegression

# sklearnに付属している実験用データセットを読み込む
# データセットについての説明:https://www.openml.org/d/40945
titanic = fetch_openml(data_id=40945, as_frame=True)

# 必要な説明変数を取得(2列目:性別 、3列目:年齢)
x = titanic.data.to_numpy()[:, [2, 3]]
# 性別を数値に変換する。(male1,female0)
x[:, 0] = (x[:, 0] == 'male').astype(float)
x = x.astype(float)

# 欠損値が存在しない添え字の一覧を取得
# 一覧 = ~[反転](性別が欠損している索引の一覧 or 年齢が欠損している索引の一覧)
idx = ~(np.isnan(x[:, 0]) + np.isnan(x[:, 1]))
# 説明変数を絞り込み。(欠損値が存在しない索引に絞り込み)
x = x[idx]
# 目的変数を取得(欠損値が存在しない索引を取得)
y = titanic.target.to_numpy().astype(float)[idx]

# インスタンスを生成
model = LogisticRegression()
# 学習
model.fit(x, y)
# 未知のデータを判定。(男性、30)
print(model.predict([[1, 30]]))
# →[0.] = 生存できない

 

【関連記事】
使用したデータセットに関する説明。

説明変数と目的変数についての説明や、データの分布を確認できる。

www.openml.org

 

5 主成分分析

【概要】

複数の変数を1つに変換する。(次元の圧縮)

正しく利用することで、以下を実現する事ができる。

 ・使用する変数を減らして計算を簡単にする。

 ・(2~3次元に圧縮できる場合)データの可視化(=図への変換)が可能となる。

 

変換する際、線形変換後の値の分散が最大となる軸を探索する必要がある。

※線形変換後に全く値が散らばらない軸を使用すると、

 変換した値による分析が不可能となる。

 

探索はラグランジュ関数を微分して行う。

【用語】

寄与率:線形変換後の値が元々の情報と比較してどれほどの情報量を持っているか。

累積寄与率:各主成分の寄与率を大きい順に足し込んだ値。

 

【ハンズオン】

乳がん検査データ(32次元)を2次元に圧縮する。

また、圧縮したデータで適切に判定を実施できるか確認する。

######################
# 主成分分析
######################
# モジュールをインポート
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

# 乳がんデータセットを読み込む
cancer_ds = load_breast_cancer()

df_target = pd.DataFrame(cancer_ds.target, columns=["target"])
df_data = pd.DataFrame(cancer_ds.data, columns=cancer_ds.feature_names)
df = pd.concat([df_target, df_data], axis=1)

# 必要な説明変数を取得(備考:列番号でなく列名でも取得できる)
# dfから「全行」かつ「mean radius」列以降の列の情報を取得
# このデータセット1列目がtargetなので、上記の絞り込みで、目的変数を除いた全ての説明変数を取得できる。
x = df.loc[:, "mean radius":]
# 目的変数を取得(今回は欠損値が存在しない前提)
y = df.target

# データの標準化(データの大小を異なる項目間で比較できるようにする)
# ※主成分分析はデータの標準化を必ず実施する※
# https://free.kikagaku.ai/tutorial/basic_of_machine_learning/learn/machine_learning_unsupervised
x = StandardScaler().fit_transform(x)
# データを学習用とテスト用に分割する(train_test_split:デフォルトでは25%がテスト用、75%が学習用となる)
x_train, x_test, y_train, y_test = train_test_split(x, y, random_state=0)

# 主成分分析のインスタンス生成
pca = PCA(n_components=2)
x_train_pca = pca.fit_transform(x_train)

# 主成分の寄与率は以下で確認可能。
# print(pca.explained_variance_ratio_)
# → [0.43711585 0.19513199]
# 第一成分で元データの43.3%、第二成分で元データの19.5%の情報を持っている。累積寄与率は62.8%

# 主成分分析した結果を図で確認してみる。
# 結果をデータフレーム化
df_pca = pd.DataFrame(x_train_pca, columns=["PC{}".format(x + 1) for x in range(2)])
# データフレームに解析結果も追加(主成分分析は教師なし学習であるが、今回は色分けするため追加)
df_pca['Outcome'] = y_train.values

# 目的変数ごとに色分けして描画するため、分割する。
malignant = df_pca[df_pca['Outcome'] == 0] # 悪性データ
benign = df_pca[df_pca['Outcome'] == 1] # 良性データ
# 描画
plt.scatter(x=malignant['PC1'], y=malignant['PC2'], label='malignant', c='r', marker='x', alpha=0.5)
plt.scatter(x=benign['PC1'], y=benign['PC2'], label='benign', c='b', marker='x', alpha=0.5)
plt.legend()
plt.xlabel('PC 1')
plt.ylabel('PC 2')
plt.savefig("aa.png")

描画結果は以下。概ね正しく分類できていると判断できる。
※かたよらずに混ざっている場合、分類が正しく実施できていないと判断する。

f:id:you_it_blog:20211009000447p:plain


さらに、主成分分析の結果を利用したクラスタリングを行う。
(主成分分析の目的はこれ。次元を減らして終わり・・ではない)
k平均法(k-means clustering)を使用する。

from sklearn.cluster import KMeans

# クラスター数は2(悪性、良性)
model = KMeans(n_clusters=2)
# クラスタリング
y_pred = model.fit_predict(x_train_pca)
# データフレームにクラスタリング結果を追加。
# 乳がんデータセットの目的変数は「0:悪性、1:良性」。KMeansの実行結果は「0:良性、1:悪性」なので、値を反転させた物を追加する。
df_pca['Outcome_pred'] = y_pred
df_pca['Outcome_pred'] = df_pca['Outcome_pred'].map({1: 0, 0: 1}).astype(int)

# 正解と学習結果を描画して目で確認する。
# ※主成分分析を用いてクラスタリングした結果と、正解データを描画する。今回は教師データが存在するため可能。
pred_kmeans_1 = df_pca[df_pca['Outcome_pred'] == 0]
pred_kmeans_2 = df_pca[df_pca['Outcome_pred'] == 1]
# 教師データを基にしたデータを描画(色付きの「×」を描画)
plt.scatter(x=malignant['PC1'], y=malignant['PC2'], label='malignant', c='r', marker='x')
plt.scatter(x=benign['PC1'], y=benign['PC2'], label='benign', c='b', marker='x')
# クラスタリングの結果を描画
plt.scatter(x=pred_kmeans_1['PC1'], y=pred_kmeans_1['PC2'], label='Ir1', c='b', alpha=0.5)
plt.scatter(x=pred_kmeans_2['PC1'], y=pred_kmeans_2['PC2'], label='Ir2', c='r', alpha=0.5)
plt.legend()
plt.xlabel('PC 1')
plt.ylabel('PC 2')
plt.savefig("aaa.png")

描画結果は以下。概ね正しく分類できているが・・境界付近に誤りがある。
※「□」と「×」の色が異なる部分が誤り。

ただ、一定の精度を保ったまま32次元 → 2次元まで圧縮できたため効果はある。

f:id:you_it_blog:20211009094859p:plain



【関連記事】

分かりやすい説明が記載されている。

一読すると、主成分分析をよく理解できた。

logics-of-blue.com

qiita.com

 

 

6 アルゴリズム_1(k近傍法-KNN)

【概要】

教師有り学習の分類手法。

識別対象データを受け取った際、識別対象データに最も近い(最近傍)のデータをk個取得。取得したk個のデータの分類を集計して、最も多い分類を返す。

例:k=3の場合

 ①識別対象データの最近傍を3個取得。

 ②「分類A:2個、分類B:1個」の場合 → 識別対象データの分類はAである、と判定する。

 ※「①」で取得するデータを、事前に学習させておく必要がある。

 

上記の通りなので、kの値が変われば結果が変わる。

kが増えるほど、取得する最近傍のデータが増えるため、識別結果は平均的になる。

言い換えるならば、kが増えるほど、識別範囲の境界線が滑らかな線になる。

 

【ハンズオン】

人口のデータ(と仮定したデータ)を分類する。
まず、学習データの作成。

######################
# k近傍法(KNN)
######################
# モジュールをインポート
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats


# 訓練データ生成処理(乱数なので、結果は毎回変わる)
def generate_data():
# 説明変数生成
x0 = np.random.normal(size=50).reshape(-1, 2) - 1 # 分類A
x1 = np.random.normal(size=50).reshape(-1, 2) + 1 # 分類B
# 作成した説明変数を結合
x = np.concatenate([x0, x1])
# 目的変数を生成して結合
y = np.concatenate([np.zeros(25), np.ones(25)]).astype(np.int)
return x, y


# 訓練データの作成 + 作成したデータの描画(確認のため)
x_train, y_train = generate_data()
plt.scatter(x_train[:, 0], x_train[:, 1], c=y_train)
plt.savefig("aa.png")

 

k近傍法に最適そうなデータを作成できた。

f:id:you_it_blog:20211009165234p:plain

次に、予測を行ってみる。(見やすくするため、判定の境界も描画する)
①numpyでの実装

# 距離を計算する関数(2乗するので符号の考慮が不要となる)
def distance(x1, x2):
return np.sum((x1 - x2) ** 2, axis=1)


# 予測処理
def knc_predict(x_train, y_train, X_test):
y_pred = np.empty(len(X_test), dtype=y_train.dtype)
for i, x in enumerate(X_test):
distances = distance(x, x_train)
nearest_index = distances.argsort()[:3] # 練習としてk=3を使用する。
mode, _ = stats.mode(y_train[nearest_index])
y_pred[i] = mode
return y_pred


# 結果を描画する関数
def plt_resut(x_train, y_train, y_pred):
xx0, xx1 = np.meshgrid(np.linspace(-5, 5, 100), np.linspace(-5, 5, 100))
# 散布図を描画
plt.scatter(x_train[:, 0], x_train[:, 1], c=y_train)
# 塗りつぶし2次元等高線図を描画。(判定の境界を確認できる)
plt.contourf(xx0, xx1, y_pred.reshape(100, 100).astype(dtype=np.float), alpha=0.2, levels=np.linspace(0, 1, 3))
plt.savefig("aaaa.png")


xx0, xx1 = np.meshgrid(np.linspace(-5, 5, 100), np.linspace(-5, 5, 100))
x_test = np.array([xx0, xx1]).reshape(2, -1).T

y_pred = knc_predict(x_train, y_train, x_test)
plt_resut(x_train, y_train, y_pred)

f:id:you_it_blog:20211009165540p:plain

 

②scikit-learnでの実装

from sklearn.neighbors import KNeighborsClassifier

xx0, xx1 = np.meshgrid(np.linspace(-5, 5, 100), np.linspace(-5, 5, 100))
xx = np.array([xx0, xx1]).reshape(2, -1).T
knc = KNeighborsClassifier(n_neighbors=3).fit(x_train, y_train) # 練習としてk=3を使用する。
plt_resut(x_train, y_train, knc.predict(xx))

f:id:you_it_blog:20211009170019p:plain

※結果に差異があるが、これはそもそもの教師データ(ランダム値)が実行の度に変わるため。教師データが同じであれば、結果も同じとなる。

 →scikit-learnの利便性が高いことを実感できる。

 

7 アルゴリズム_2(k平均法-K-mean)

【概要】

教師なし学習の分類手法。

受け取ったデータをk個のクラスタに分類する。

 

【処理の手順】

以下を、収束(= 結果が変化しなくなる)するまで繰り返す。

 ①各データからクラスタの中心となるデータを設定する。(初回はランダム)

 ②各データを最も距離の近いクラスタに分類する。

 ③分類したクラスタの平均ベクトル(中心)を計算する。

 ④クラスタの中心を更新して、再度①~③を行う。

 

【ハンズオン】

「主成分分析」で使用したため省略する。

 

8 SVM(サポートベクターマシン)

【概要】

2クラス分類問題の代表的な手法の1つ。

→回帰問題や教師なし学習でも応用されている。

※ここでは、線形分類をハンズオン対象とするが、

 カーネル関数と呼ばれる関数を用いることで、非線形の分類も可能。

 

【用語】

マージン最大化:

 分類問題なので、「k近傍法」に記載した図のように、分類の境界となる線が発生する。この線とデータの距離をマージンと呼び、マージンが大きいほど、適切な境界が設定できている。SVMでは、このマージンが最大となる境界を探す。この考え方をマージン最大化と呼ぶ。

ハードマージン:

 分類を完璧に識別できる境界が設定可能、という前提の分類。

ソフトマージン:

 分類を完璧に識別できない(誤差がある)、という前提の分類。

 ※現実としては、全ての情報を完璧に分類できる、という事はありえない。

 ソフトマージンにおける誤差を表す変数を「スラック変数」と呼び、

 誤差を最小化したうえで、マージンが最大となる境界を探す。

サポートベクトル:

 境界に最も近いデータのこと。SVMはマージンが最大となる境界を探すため、

 境界から離れているデータは、境界作成に全く影響しない。(あっても無くても結果に影響しない)

 境界に最も近いデータの = 境界の決定に使用されるデータをサポートベクトルと呼ぶ。

 

【ハンズオン】

アヤメの分類(2クラス分類)を行う。

######################
# SVM
######################
# モジュールをインポート
from sklearn import datasets, svm
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap

# データセットの読み込みhttps://scikit-learn.org/stable/datasets/toy_dataset.html#iris-dataset
iris = datasets.load_iris()

# 説明変数は最初の2つのみを取得(がく片の長さ、がく片の幅)
# ハンズオンのために結果を2次元の図で出力するため。
x = iris.data[:, :2]
# 目的変数の取得
y = iris.target

# 2:iris virginica」以外のインデックスを取得(データセット3クラス。実施する分類は2クラス分類のため)
idx = (iris.target != 2)
# 2:iris virginica」以外のデータのみに絞り込み
x = x[idx]
y = y[idx]

# SVMインスタンス生成
clf = svm.SVC(C=1.0, kernel='linear')
# データに最適化フィット
clf.fit(x, y)

# 背景色を利用して、分類と境界を描画する
# グラフの表示エリアの取得(説明変数の最大+1 と 最小-1の範囲とする)
x1_min = min(x[:, 0]) - 1
x1_max = max(x[:, 0]) + 1
x2_min = min(x[:, 1]) - 1
x2_max = max(x[:, 1]) + 1

# グリッド情報の作成。
# mgrid:格子状の配列を作成。500jなので、500×500の要素数となる、
xx, yy = np.mgrid[x1_min:x1_max:500j, x2_min:x2_max:500j]

# グリッド情報を並べなおす(np.c_:配列を結合する。ravel():配列を1次元に変換する)
xg = np.c_[xx.ravel(), yy.ravel()]

# 各グリッドの点の分類結果の予測をZに格納
z = clf.predict(xg)

# グリッド情報を並べなおす
z = z.reshape(xx.shape)

# 分類ごと塗りつぶしに使用する色の設定
cmap = ListedColormap([(0.5, 1, 1, 1), (1, 0.93, 0.5, 1)])

# 背景の色を表示
plt.pcolormesh(xx, yy, z == 0, cmap=cmap)

# 軸ラベルを設定
plt.xlabel('sepal length')
plt.ylabel('sepal width')

Xc0 = x[y == 0] # 目的変数;0のデータ
Xc1 = x[y == 1] # 目的変数;1のデータ

# 目的変数;0のデータを描画
plt.scatter(Xc0[:, 0], Xc0[:, 1], c='#E69F00', linewidths=0.5, edgecolors='black')
# 目的変数;1のデータを描画
plt.scatter(Xc1[:, 0], Xc1[:, 1], c='#56B4E9', linewidths=0.5, edgecolors='black')

# サポートベクトルを取得
SV = clf.support_vectors_

# サポートベクトルの点を赤枠で囲む(可視化のため)
plt.scatter(SV[:, 0], SV[:, 1], c='black', linewidths=1.0, edgecolors='red')

# 描画したグラフを表示
plt.savefig("ii.png")

f:id:you_it_blog:20211010102259p:plain

作成された境界と、境界の決定に使用されているサポートベクトルを確認できる。