- 追加された行はこの色です。
- 削除された行はこの色です。
- 第7回 は削除されています。
- 第7回 の差分を削除
#author("2023-11-15T17:05:43+09:00","default:cmdsadmin","cmdsadmin")
* 第7回:教師なし学習・次元削減,機械学習の実践的アプローチ [#s98df3b7]
#author("2024-05-07T10:38:53+09:00","default:cmdsadmin","cmdsadmin")
#contents
* 7.1 教師なし学習・次元削減【中村】 [#ya1753a4]
** 次元削減 (dimensionality reduction) とは [#ia8e222f]
- データの変数(列)間の関係を学習し,次元数を減らすこと
-- より少ない特徴量でデータを説明する
-- なるべく情報量を落とさないように,複数の特徴量をまとめた新しい特徴量を作る
CENTER:&ref(第4回/4_unsupervised_learning.png,80%);
CENTER:図1:教師あり学習と教師なし学習のイメージ(再掲)
*** なぜ次元を削減するのか? [#lc8c2462]
- データの理解・可視化
-- 次元を削減することで,データの理解・可視化がやりやすくなる
-- 人間が理解できる表現方法はせいぜい3次元まで
--- 1次元:サンプル同士を直接値で比較できる
--- 2次元:サンプルを2軸で説明できる
--- 3次元:サンプルを空間上で可視化できる
--- 4次元以上: 1つの図では可視化不可能
- データの圧縮
-- 高次元のデータを低次元に圧縮することで,計算処理を効率化できる
- 次元の呪いへの対処
-- 次元が増えるほど訓練に必要なデータが指数的に増える (少ないままだと過学習が起こる)
-- 次元削減で減らす
//*** 身近な次元削減の例 [#a7c7a91c]
//- 身長hと体重wから肥満度を表す尺度BMIを求める (2次元→1次元)
//- 100名の学生の5教科テストの結果で,学生それぞれの学力を合計点で比べる (5次元→1次元)
*** 次元削減の手法の例 [#cad2d575]
- ''主成分分析 (principal component analysis, PCA)''
-- 特徴量を相互に統計的に関連しないように回転させる
- 非負値行列因子分解(non-negative matrix factorization, NMF)
-- データを非負の重み付き和に分解する
- t-SNE ( t-distributed stochastic neighbor embedding)
-- データポイントの距離を可能な限り維持する2次元表現を見つけようとする (可視化専用)
** 【例題】5科目のテスト結果の分析 [#v5311ff5]
- あなたは100人のクラスを担当する高校教師である
- 先日行われたの5科目の模擬試験(国語,数学,英語,物理,化学)の結果が返ってきている
-- https://www2.cmds.kobe-u.ac.jp/~masa-n/dshandson/exam-pca.csv
- これらのデータを分析し,''100人それぞれの学力を把握''したい
*** 準備 [#c403da35]
+ Google Colabを開き,新規ノートブックを作成
+ ノートブックの名前 Untitled.ipynb を exam-pca.ipynb に変更する
#準備(すべてに共通)
# PandasとNumpyをインポート
import pandas as pd
import numpy as np
# 日本語化Matplotlibもインポート
import matplotlib.pyplot as plt
#↓の1行は提出時にはコメントアウトしてください
!pip install japanize-matplotlib
import japanize_matplotlib
# Seabornもインポート
import seaborn as sns
# pickleをインポート(モデルの保存用)
import pickle
#データフレームをきれいに表示するメソッド
from IPython.display import display
#標準化はよく使うのでインポート
from sklearn.preprocessing import StandardScaler
#データの取り込み
data = pd.read_csv("https://www2.cmds.kobe-u.ac.jp/~masa-n/dshandson/exam-pca.csv", index_col="受験番号")
*** データを眺める [#f812a844]
display(data)
df = data.copy()
#要約統計量
display(df.describe())
#ヴァイオリン・プロット
sns.violinplot(df)
plt.show()
#相関係数
sns.heatmap(df.corr(), annot=True)
*** 課題 [#k1d8a1fe]
- 上記のEDAでは,学生全体の傾向はつかめるものの,''個別の学生の学力がどうなっているか''は可視化できていない
- よくやる方法は,''5科目の合計''を取って,比較する
-- 合計 = 国語 + 数学 + 英語 + 物理 + 化学
#5科目の合計を計算
total = pd.DataFrame(df.sum(axis=1), columns=["合計"])
display(total)
#横棒グラフに可視化する
total.sort_values("合計").plot.barh(title="5科目の合計", figsize=(8,16))
#積み上げ棒グラフで表示する(オプショナル)
#df.loc[total.sort_values("合計").index].plot.barh(stacked=True, figsize=(8,16))
- 5科目5次元の得点データを,「合計」という1次元の変数で説明している
-- 5次元を1次元に''次元削減''して分析している
- 5科目のデータを, 合計という ''新しい軸'' に移して分析している
CENTER:&attachref(./7_exam_total.png);~
CENTER:図2: 5科目の得点を合計点で比較・説明する
*** 情報量の減少 [#od0a8c04]
- 新しい軸「合計」を使うと,学生全員の学力を比較・説明しやすくなる
- 一方で,ほぼ同じ合計点でも,異なる傾向がある学生を見分けられなくなる
-- 57番と35番
-- 66番と19番
#66番と19番の得点を表示
display(df.loc[[66,19],:])
display(total.loc[[66,19]])
- なるべく情報量が減らないように,「新しい軸」を定義するにはどうしたらよいだろうか?
** 主成分分析 (PCA) [#e673b652]
*** 新しい軸をデータがなるべく散らばるように決める [#xd4b2c2e]
- 5科目の成績の例題で,新しい軸は以下のように一般化できる
新しい軸 = w1*国語 + w2*数学 + w3*英語 + w4*物理 + w5*化学
- w1 = w2 = w3 = w4 = w5 = 1.0 とした場合,「合計」の軸となる
-- しかし,合計では,66番と19番の傾向の差を説明できなかった
- そこで例えば,以下の新しい2軸を定義してみる
-- 理系力 = 0.3*国語 + 0.8*数学 + 0.3*英語 + 0.8*物理 + 0.8*化学
-- 文系力 = 0.8*国語 + 0.3*数学 + 0.8*英語 + 0.3*物理 + 0.3*化学
CENTER:&attachref(./7_exam_variance.png,80%);~
CENTER:図3: 新しい軸でデータを比較する
- 66番と19番はそれぞれの観点で比較できる
-- ほぼ同じ合計点でも,19番は文系科目に秀でており,66番は理系科目が優れている
- この場合,5次元のデータを2次元で説明することになる
*** PCAの考え方 [#w7b7abee]
- 多くの変数の情報をできるだけ損なわずに,少数の変数(''主成分'')に集約させることを目的とした解析手法
- 変数 x1, x2, ..., xn から主成分 z1 = w11*x1 + w21*x2 + ... + wn1*xn を求める時に,z1上の''分散を最大化''するようにw11,w21,..,wn1を決定する
-- z1を''第1主成分''という
- 同様に,z2 = w12*x1 + w22*x2 + ... + wn2*xn を,z1に''直交''し,かつ,z2上の分散を最大化するように,w12, w22,...,wn2を決定する
-- z2を''第2主成分''という.z1と直交させることで,z1で説明しきれなかった角度からデータを説明する
- 以降,z_kを決める時,z1, z2,...,z_{k-1}と直交し,かつ,z_k上の分散を最大化するように決定していく
-- kはn(=もとの次元数)以下の任意の整数
- 【発展】z1, z2, ..., zkを求める方法
-- [[共分散行列 S の固有方程式 Sw = λw を解く:https://zero2one.jp/learningblog/dimension-reduction-with-python/]]
*** 用語 [#e92fe7e0]
- ''主成分負荷量:'' w1k, w2k, ..., wnk を指す.主成分zkの計算に,各変数x1,x2,...,xnをどのぐらい使用しているかを表す
-- 固有ベクトルに相当する
- ''主成分得点:'' 元のデータを各成分z1, z2, ..., zkに変換した値を指す
-- それぞれの学生の5科目の得点を,文系力,理系力に変換した後のスコア
- ''分散'': 各主成分z1,z2,...,zkにおけるデータの分散
-- 固有値に相当する
- ''寄与率:'' 各主成分z1,z2,...,zkが元のデータ全体を何%説明しているかを示す
-- 主成分の重要度を理解できる
- ''累積寄与率:'' 寄与率の累積和.主成分z1,z2,...,ziまでで,元のデータ全体の何%が説明できているかを示す
-- kを決める際の目安にする
*** sklearn.decomposition.PCA [#ne94494e]
- 書式
from sklearn.decomposition import PCA
pca = PCA(ハイパーパラメータ)
#標準化されたデータフレームで学習させる
pca.fit(df_sc)
#主成分のラベル (データフレームの列・行に名前を付ける際に使う)
labels = [f"第{i+1}主成分" for i in range(pca.n_components_)]
#主成分得点に変換.データフレームに入れなおす
df_pca = pd.DataFrame(pca.transform(df_sc), index=df_sc, columns=labels)
#主成分得点を表示
print("【主成分得点】")
display(df_pca)
#主成分負荷量
df_comp = pd.DataFrame(pca.components_, index=labels, columns=df_sc.columns)
#主成分負荷量を表示
print("【主成分負荷量】")
display(df_comp)
#分散,寄与率
df_var = pd.DataFrame(pca.explained_variance_, index=labels, columns=["分散"])
df_var["寄与率"] = pca.explained_variance_ratio_
df_var["累積寄与率"] = pca.explained_variance_ratio_.cumsum()
print("【分散・寄与率】")
display(df_var)
- 説明書は[[こちら:https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.PCA.html]]
- 主なハイパーパラメータ
-- n_components: 保持する''主成分の数k''を指定.デフォルトで k=n (つまり,元のデータの次元数)
--- または,1より小さい非負数で,''累積寄与率''の閾値を指定し,これを超えるところまで主成分を計算することもできる
-- whiten: 白色化するか否か.デフォルトはFalse.Trueにすると,導出する主成分得点を自動的に標準化する.
- 主な属性
-- components_: 主成分負荷量.分散を最大化する固有ベクトルに相当する
-- explained_variance_: 各主成分の分散.固有値に相当する
-- explained_variance_ratio_: 各主成分が持つ分散の比率
*** 5科目テストのデータにPCAを適用 [#g3512b1e]
- 5科目テストのデータにPCAを適用し,主成分を抽出してみる
#まずは標準化
sc = StandardScaler()
sc.fit(df)
df_sc = pd.DataFrame(sc.transform(df), index=df.index, columns=df.columns)
#主成分分析を行う.次元数はデフォルトで元データの次元数(5)になる
from sklearn.decomposition import PCA
pca = PCA()
pca.fit(df_sc)
#主成分のラベル (データフレームの列・行に名前を付ける際に使う)
labels = [f"第{i+1}主成分" for i in range(pca.n_components_)]
#主成分得点に変換.データフレームに入れなおす
df_pca = pd.DataFrame(pca.transform(df_sc), index=df_sc.index, columns=labels)
#主成分得点を表示
print("【主成分得点】")
display(df_pca)
#主成分負荷量
df_comp = pd.DataFrame(pca.components_, index=labels, columns=df_sc.columns)
#主成分負荷量を表示
print("【主成分負荷量】")
display(df_comp)
#分散,寄与率
df_var = pd.DataFrame(pca.explained_variance_, index=labels, columns=["分散"])
df_var["寄与率"] = pca.explained_variance_ratio_
df_var["累積寄与率"] = pca.explained_variance_ratio_.cumsum()
print("【分散・寄与率】")
display(df_var)
- 結果を解釈する
-- 主成分負荷量の ''絶対値'' が大きいものが,その主成分に効いている変数
-- 符号(プラス・マイナス)に注意 → どちら向きに効いているかを意識すること
CENTER:&attachref(./7_exam_pca_results.png,70%);~
CENTER:図4: 5科目テストのPCA適用結果
- 第1,第2主成分のみで,可視化してみる
#第1,第2主成分のみを取り出す → 次元を2に削減
df_dim = df_pca.iloc[:,[0,1]]
display(df_dim)
#散布図を描く
ax = sns.scatterplot(df_dim, x="第1主成分", y="第2主成分")
#データフレームの各行を取り出し,各ポイントに受験番号(インデクス)をつける
for idx, row in df_dim.iterrows():
ax.text(row["第1主成分"], row["第2主成分"], idx)
#細かい装飾
ax.grid()
ax.axvline(x=0, c="red")
ax.axhline(y=0, c="blue")
CENTER:&attachref(./7_exam_pca_scatter.png,80%);~
CENTER:図5: 5科目テストの成績を2次元で可視化する
* 7.2 特徴量エンジニアリング【中村】 [#o4527101]
** 特徴量を開発する (feature engineering) [#v0a247f1]
- 精度の良い学習モデルを作るためには,正解データ(目的変数)をうまく説明できる特徴量(説明変数)が不可欠である
- データセットにすでに特徴量がたくさんある場合
-- → 効きそうなものを選択する ''【特徴量選択】''
- データセットに特徴量がない,あるいは,効きそうなものがない場合
-- → 特徴量を創る ''【特徴量エンジニアリング】''
** アプローチ [#b05bcf29]
- データを眺めて考える
-- EDAを行い,目的変数に関するパターンや法則が見えないか?
- ドメイン知識に基づいて作る
-- 目的変数に関連がありそうな要因は何か?
- 機械的に作る
-- ある変数xの2乗,3乗を変数として加える
-- 2つの変数x, y の積を変数として加える
** 【例題】製品の売上予測 [#p52414b3]
- RQ: あるスーパーで販売中のヨーグルトAの売上データに基づいて,Aが明日何個売れるかを予測したい
-- [[ヨーグルトの売上データ>データ#t3f682d0]]
--- https://www2.cmds.kobe-u.ac.jp/~masa-n/dshandson/yogurt.xlsx
【売上データ】
- #0: 売上日付
- #1: 売上数 (目的変数):その日に売れたAの個数
- #2: 売上額 (目的変数):その日に売れたAの総売上金額
CENTER:&attachref(./7_yogurt_data.png,70%);~
CENTER:図6:特徴量が少ないデータセット
【問題】
- 説明変数が日付しかない
→ 特徴量エンジニアリングによって,新しい特徴量を作る
*** 準備 [#v19a2231]
- Google Colabを開き,新規ノートブックを作成
- ノートブックの名前 Untitled.ipynb を yogurt.ipynb に変更する
#準備(すべてに共通)
# PandasとNumpyをインポート
import pandas as pd
import numpy as np
# 日本語化Matplotlibもインポート
import matplotlib.pyplot as plt
#↓の1行は提出時にはコメントアウトしてください
!pip install japanize-matplotlib
import japanize_matplotlib
# Seabornもインポート
import seaborn as sns
# pickleをインポート(モデルの保存用)
import pickle
# pandasのデータフレームを表示する関数
from IPython.display import display
#データをロードする(エクセルデータの読み込み)
data = pd.read_excel("https://www2.cmds.kobe-u.ac.jp/~masa-n/dshandson/yogurt.xlsx")
data
*** データを眺める [#q6a0df73]
#型チェック
data.dtypes
#整形
df = data.copy()
#売上日付をインデクスに
df = df.set_index("売上日付")
df
#売上個数を可視化
#箱ひげ図
df["売上数"].plot.box()
#時系列
df["売上数"].plot()
#月別に可視化
for y in [2021, 2022]:
for i in range(1,13):
df[(df.index.year==y)&(df.index.month==i)]["売上数"].plot.bar(title=f"{y}年{i}月", figsize=(8,6))
plt.show()
*** ドメイン知識による特徴量開発 [#xb3fee72]
ヨーグルトAの売上数に関係ありそうなものは何か?
- 曜日?
df["曜日"] = df.index.day_of_week
- 日?
df["日"] = df.index.day
- 月?
df["月"] = df.index.month
- 季節?:月で説明できそう.ヨーグルトに季節は関係ある?
- ''値段'':おそらく一番効くのでは?
-- 売上額を売上数で割れば,単価が出るはず!!
df["単価"] = df["売上額"] / df["売上数"]
- n日前の売上数?
n=3
for i in range(1, n+1):
#df["列"].shift(i)で,列を下にi桁ずらすことができる
df[f"{i}日前売上数"] = df["売上数"].shift(i)
df
CENTER:&attachref(./7_yogurt_features.png,60%);
CENTER:図7: 追加された特徴量
- 天気?
#天気のデータを拾ってくる.インデクスを日付に
data_weather = pd.read_csv("どこかの天気のデータ.csv")
df_w = data_weather.set_index("日付")
#売上データとマージする (参考:Python基礎演習6.2)
df_merged = pd.merge(df, df_w, left_index=True, right_index=True)
** 機械的に作る [#p736d690]
*** sklearn.preprocessing.PolynomialFeatures [#ic26c35c]
多項式を使って,特徴量を作成するライブラリ
- 書式
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(ハイパーパラメータ)
#データフレームをフィットさせる
poly.fit(df) #dfは特徴量作成の元になる列を含んだデータフレーム
#データフレームを変換して,データフレームに入れなおす
df_poly = pd.DataFrame(poly.transform(df), index=df.index, columns=poly_features_names_out())
#確認する
df_poly
- 説明書は[[こちら:https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.PolynomialFeatures.html]]
- ハイパーパラメータ
-- degree: デフォルトは2.多項式の最大次数を与える.(最小,最大)のようにタプルで与えることも可能
-- interaction_only: デフォルトはFalse.Trueにすると交互作用(異なる変数同士の掛け算)の項のみを抽出
-- include_bias: デフォルトはTrue.切片の項を出力するかどうか
#データフレームを適当に作る
df_sample = pd.DataFrame(data={"a":[1,2,3,4,5], "b":[60,70,80,90,100]})
df_sample
#多項式特徴量を作成する
from sklearn.preprocessing import PolynomialFeatures
poly=PolynomialFeatures(degree=3,include_bias=False)
poly.fit(df_sample)
df_poly = pd.DataFrame(poly.transform(df_sample), index=df_sample.index,
columns=poly.get_feature_names_out())
df_poly
CENTER:&attachref(./7_polynomial_features.png,60%);~
CENTER:図8: 多項式特徴量の作成
* 7.3 モデルのチューニング【伊藤】 [#w3ca61e3]
//** 関数化 [#w52dc57a]
//- あえて言う必要ない?
** 交差検証 [#nf81375e]
*** 予測性能評価 [#gf66e6cc]
- ホールドアウト法の問題点
-- ホールドアウト法:「学習に利用するデータ」と「予測性能をテストするデータ」に分割
-- 分割したデータに偏りが出る可能性
-- モデルの性能が低い原因が、適切なチューニングが行えていないせいなのか、分割時のデータの偏りなのかが不明瞭
*** K分割交差検証 [#q594b936]
-- 学習データとテストデータを変え、K回評価を繰り返し、平均性能を評価
CENTER:&attachref(./kFold.png,50%);~
CENTER:図9: 6分割交差検証
-- データの分け方に偏りが出ても、K回分の予測性能の評価指標の平均を取るので、偏りの影響を少なくできる
# KFoldの処理で分割時の条件を指定
from sklearn.model_selection import KFold
kf = KFold(n_splits = 3, shuffle = True, random_state = 0)
# cross_validate関数をインポートする
from sklearn.model_selection import cross_validate
# 線形回帰モデルの選択
from sklearn import linear_model
model = linear_model.LinearRegression()
# 交差検証
result = cross_validate(model, X, y, cv = kf, scoring = 'r2', return_train_score = True)
print(result)
-- 実行結果 {'fit_time': array([0.06101942, 0.01478291, 0.00645089]), 'score_time': array([0.01288962, 0.01835108, 0.00437713]), 'test_score': array([0.53904302, 0.54059838, 0.51332463]), 'train_score': array([0.56584437, 0.56791939, 0.57737164])}
#平均値を計算する
sum(result['test_score']) / len(result['test_score'])
-- 実行結果 0.5309886770444134
- [[シェアサイクルの需要予測問題に適用(bike_7.ipynb):https://drive.google.com/file/d/1Fh4gHh9L7b8wvNjlmIxxliRTLzh2bOY1/view?usp=drive_link]]
- 分類モデルを作るときの交差検証の注意点
--分類モデルでは,「各分割ブロック内のクラスの比率が等しくなる」ような指定をする必要がある。
--scikit-learnでは,StratfiedKFoldライブラリを利用
from sklearn.model_selection import StratfiedKFold
skf = StratfiedKFold(n_splits = 3, shuffle = True, random_state = 0)
** 訓練データ,検証データ,テストデータの分割 [#k8835473]
- 教師データを2つ(訓練データとテストデータ)に分割する問題点
-- チューニングしていくにあたり,テストデータに都合が良いようにチューニングをしてしまう
- 教師データを3つに分割
--①訓練データ(training):学習に利用するデータ
--②検証データ(validation):チューニングの参考にするためにモデルの予測性能を評価するデータ
--③テストデータ(test):学習にもチューニングの参考にも利用せず,最終的なモデルの予測性能を評価するためだけのデータ
-分割の手順
--1. 「①訓練データ&②検証データ」と「③テストデータ」の2つに分割
--2. 「①訓練データ」と「②検証データ」を分割
# train_test_split関数をインポートし、X, yのそれぞれを訓練データ、検証データ、テストデータに分けていく
from sklearn.model_selection import train_test_split
# まず、訓練・検証データとテストデータに分ける (訓練・検証:テスト=8:2)
X_train_val, X_test, y_train_val, y_test = train_test_split(X, y, test_size=0.2, random_state=1234)
# さらに、訓練データと検証データに分ける (訓練:検証=7:3)
X_train, X_val, y_train, y_val = train_test_split(X_train_val, y_train_val, test_size=0.3, random_state=1234)
-テストデータで最終チェックする際の注意点
-- 訓練・検証データと同様の前処理をテストデータにも改めて行う
-- ただし,ダミー変数化は「データ分割前に実施!!」->ランダムな分割によってダミー変数が一致しない可能性がある
- [[シェアサイクルの需要予測問題に適用(bikeGSCV.ipynb):https://drive.google.com/file/d/19FQb1AtUW0L5QLE3FBIl2YcdZwKDxqGN/view?usp=sharing]]
*** チューニングのためのK分割交差検証 [#u50a42e2]
-指定教科書「スッキリわかるPythonによる機械学習入門」p.484 図13-11参照
-- 最適なハイパーパラメータ値や前処理法を決めるため,チューニング時にK分割交差検証を行う
-- 最適なチューニングが決定したら,そのチューニング法を利用して,「訓練データ&検証データ」をまとめてモデルに再学習させる。
** グリッドサーチ [#s0cd6f75]
- ハイパーパラメータの最適化手法
-- ハイパーパラメータの組み合わせごとにモデルの性能を評価し,最適な値の組み合わせを見つけ出す
-- しらみつぶしの網羅的探索手法
--- 1. 探索するハイパーパラメータと範囲を定義
--- 2. ハイパーパラメータの全組み合わせで学習と検証
--- 3. 性能が最も高いときのハイパーパラメータを明示
# GridSearchCVクラスのインポート
from sklearn.model_selection import GridSearchCV
# 学習に使用するアルゴリズムの定義
estimator = tree.DecisionTreeRegressor(random_state=0)
# 探索するハイパーパラメータと範囲の定義
param_grid = [{
'max_depth': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
'min_samples_split': [2, 10, 20]
}]
# データセット分割数を定義
cv = 5
# GridSearchCVクラスを用いたモデルの定義
tuned_dt = GridSearchCV(estimator=estimator,
param_grid=param_grid,
cv=cv, return_train_score=False)
# モデルの学習・検証
tuned_dt.fit(X_train_val, y_train_val)
# 検証結果の確認
pd.DataFrame(tuned_dt.cv_results_).T
CENTER:&attachref(./GSCV.png,80%);~
CENTER:図10: 検証結果の表の一部
# 最も予測精度の高かったハイパーパラメータの確認
tuned_dt.best_params_
- 実行結果:
{'max_depth': 4, 'min_samples_split': 2}
# 最も予測精度の高かったモデルの引き継ぎ
best_dt = tuned_dt.best_estimator_
# モデルの検証
print('train_val score : ', best_dt.score(X_train_val, y_train_val))
print('test score : ', best_dt.score(X_test, y_test))
- 実行結果
train_val score : 0.6089775598869998
test score : 0.5767345219363884
//- [[シェアサイクルの需要予測問題に適用(bikeGSCV.ipynb):https://drive.google.com/file/d/19FQb1AtUW0L5QLE3FBIl2YcdZwKDxqGN/view?usp=sharing]]
* 7.4 最適化問題 【伊藤】[#q370d5e0]
- 与えられた制約条件の下で目的関数の値を最小(もしくは最大)にする解を求める問題
-- 産業を始めとする幅広い分野の多くの問題が最適化問題として定式化できる
CENTER:&attachref(./or.png,80%);~
CENTER:図10: 暮らしに溶け込む最適化問題(「ORを探せ!」日本オペレーションズ・リサーチ学会より、ライセンスCC-BY-ND)
*** 例題:輸送問題 [#hee1df4a]
- 数ヵ所の工場から数ヵ所の取引先に部品を輸送したい.輸送費が最小となる最適な計画を立てよ。
-- [[輸送問題の定式化:https://drive.google.com/file/d/1RH7wvCUe8PBIhp2VCgb6NiiVRwn-_T2M/view?usp=drive_link]]
- 供給点(工場)|I|=2, 需要点(取引先)|J|=4のときの輸送問題
!pip install pulp
# モジュールをインポート(同時にCBCソルバがインストールされる)
from pulp import *
# 工場のリスト(中身は供給上限)
capacity = [6, 10]
# 取引先のリスト(中身は需要量)
demand = [4, 6, 3, 3]
# 単位費用の行列 c_ij
cost = {(0, 0): 3,
(0, 1): 7,
(0, 2): 11,
(0, 3): 8,
(1, 0): 6,
(1, 1): 7,
(1, 2): 8,
(1, 3): 9 }
#問題を生成する
model = LpProblem("Transportation", LpMinimize)
# 決定変数x_ijを生成する
x = {(i,j): LpVariable("x{}-{}".format(i,j), lowBound=0, upBound=demand[j]) for i,j in cost}
# 目的関数
model += lpSum([cost[i,j] * x[i,j] for i,j in cost]), "Objective"
# 制約条件
# 工場の出荷上限
for i, Ci in enumerate(capacity):
model += lpSum([x[i,j] for j in range(len(demand))]) <= Ci, "Capacity{}".format(i)
# 取引先の需要
for j, dj in enumerate(demand):
model += lpSum([x[i,j] for i in range(len(capacity))]) == dj, "demand{}".format(j)
# 求解、問題modelの最適解が,CBCソルバによって計算される
model.solve()
# 結果の確認
print("Optimal Value =", value(model.objective))
for var in x.values():
# 誤差以上の値を持っている変数だけprint
if var.varValue > 1e-4:
print(var, var.varValue)
- 実行結果:
Optimal Value = 103.0
x0_0 4.0
x0_3 2.0
x1_1 6.0
x1_2 3.0
x1_3 1.0
//-- [[輸送問題を解く:https://colab.research.google.com/drive/1k6Fovo5OhL4m-zKZ99aVd19GiQHsdBc4?usp=drive_link]]
- 現実問題は,多くの変数と制約式が含まれた大規模な最適化問題となりうる.今回,COINプロジェクトのCBCソルバを使用したが,一定以上の問題を解くためには,商用ソルバの利用が必要となる.
- 商用ソルバでも実行可能な時間内に解が得られない場合,解法の開発が必要となるが,コンピューターの処理能力や商用ソルバの性能向上により,大規模な最適化問題であっても高速に解ける場合が多い.
*** 最適化問題と機械学習 [#k596bf07]
- 最適化問題を解くにはアルゴリズム(解法)が必要
-- たとえば,変数が離散的な値をとる離散最適化問題の一つナップサック問題を解くときには動的計画法を用いる.
-- 機械学習の一つである強化学習は動的計画法をもとにしている.
* 7.5 機械学習モデルのデプロイ・運用【陳】 [#e2620668]
本講義では,7.4 回まで機械学習モデルの構築・評価を扱った
モデルを実際に運用化するには,モデルのデプロイ (deploy)が必須
** 重要な概念的理解 [#re0ee9c5]
*** モデルデプロイ・運用の全体像 [#c22a055e]
CENTER:&ref(./7.5-overall.png,40%);~
CENTER:7.5 図1:モデルデプロイ・運用の全体像
*** サーバ (server) [#bce8b50c]
◇ ソフトウェアの観点からサーバの共通的な特徴をいえば
+ あるプログラムに Web URL のような''「住所」(address)''を付けること
+ プログラムの''「実行中」(online)'' という状態をずっと維持できること
◇ なぜプログラムにアドレスを付けるのか?
- 多様なルートでの受送信,管理,監視などを効率よくしたいから
- ローカル (edge,内部)やグローバル (cloud,外部) 環境によって変わる
◇サーバアドレスの一般的な形式
- IPアドレス:ポスト番号・・・
-- 例:192.168.0.1:8080/...
◇ サーバの一般的な種類
- Web サーバー: パソコン同士の通信を行う
- データベースサーバー: 様々なデータを管理・保存
*** API とは [#f5cd096a]
◇ Application Programming Interface (API)
- プログラムやサーバ等の間をつなぐ接点 (interface) のこと
- 「○○」を入力すると「△△」が出力されるとのこと
◇ REST API (または RESTful API)
- REpresentational State Transfer (REST)
-- Representational:具象化された
-- State:状態の
-- Transfer:転送
- 代表的な特徴:HTTPのリクエストメソッドを用いること
-- 7.5 図2のように 1つのアドレスに複数メソッドを設ける
CENTER:&ref(./7.5-REST_API.png,40%);~
CENTER:7.5 図2:一般的なAPIとREST APIの違い
** Google Colab で機械学習モデルをデプロイ! [#mfa09eac]
アヤメ分類問題を復習しましょう【[[参考>https://www2.cmds.kobe-u.ac.jp/wiki/dshandson/?%E7%AC%AC6%E5%9B%9E#zbeffbdf]]】
◇ 説明変数
- がくの長さ (sepal length)
- がくの幅 (sepal width)
- 花びらの長さ (petal length)
- 花びらの幅 (petal width)
◇ 目的変数
- 品種 (species)
-- setosa
-- virginica
-- versicolor
◇ 準備1
- [[Google Drive:https://drive.google.com/]]にアクセスして,Google Colabを新規作成してください
- 名前を ''handson7.5-1.ipynb'' とします
- 下記のコードを貼り付けて実行してください
*** ⓪ モデル構築・評価 [#u574366c]
※ 欠損値穴埋めや,特徴量の標準化をここで省略
# === モデルの構築・評価 ===
# PandasとNumpyをインポート
import pandas as pd
import numpy as np
# train_test_splitをインポート(データの分割用)
from sklearn.model_selection import train_test_split
#アヤメデータの取得
from sklearn.datasets import load_iris
iris = load_iris()
# 訓練データと検証データに分割
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, test_size=0.1)
# ロジスティック回帰による学習
from sklearn.linear_model import LogisticRegression
model = LogisticRegression(random_state = 0, C = 0.1, multi_class = 'auto', solver = 'lbfgs')
model.fit(X_train, y_train)
# モデル評価
print("Model score: ", model.score(X_train, y_train))
print("Test Accuracy: ", model.score(X_test, y_test))
【実行結果】
Model score: 0.9481481481481482
Test Accuracy: 1.0
*** ① モデルデプロイ [#r561de67]
# モデルデプロイのためのモジュールをインストール ※追記:バージョン指定
## 環境による事情ですが,もしかして''全てコードの実行前に,さきに必要な全てのモジュールをインストールする順序の守りが必要''
!pip install fastapi==0.104 nest-asyncio==1.5.8 pyngrok==7.0.1 uvicorn==0.24.0
【実行】
from fastapi import FastAPI # Python で RESTful API を構築するための最新の Web フレームワーク
from fastapi.middleware.cors import CORSMiddleware # CORSまたは「オリジン間リソース共有」
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=['*'],
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
)
@app.get('/')
async def root():
return {'hello': 'world'}
from flask import Flask, request, jsonify # Web アプリケーションフレームワークや受送信,データ整形に必要
from pydantic import BaseModel, conlist # リクエストbodyを定義するために必要
from typing import List # ネストされたBodyを定義するために必要
# リクエストbodyを定義
class Iris(BaseModel):
data: List[conlist(float, min_items=4, max_items=4)]
@app.post("/predict")
async def get_predictions(iris: Iris):
data = dict(iris)['data']
iris_types = {
0: 'setosa',
1: 'versicolor',
2: 'virginica'
}
prediction = list(map(lambda x: iris_types[x], model.predict(data).tolist()))
log_proba = model.predict_log_proba(data).tolist()
return {"prediction": prediction, "log_proba": log_proba}
import nest_asyncio # 非同期処理
from pyngrok import ngrok # ローカルサーバを外部ネットワークに簡単に公開できるトンネリングツール
import uvicorn # ASGI (Asynchronous Server Gateway Interface) Webサーバ
ngrok_tunnel = ngrok.connect(8000)
print('Public URL:', ngrok_tunnel.public_url)
nest_asyncio.apply()
uvicorn.run(app, port=8000)
【実行結果】
Public URL: https://f627-34-125-200-230.ngrok.io ← これがサーバのアドレス ※ 毎回実行時に変わる
INFO: Started server process [287]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
◇ サーバ稼働状況の確認
- サーバアドレスをWebで開くと「'hello': 'world'」がでればOK
*** ② モデル運用 [#o6626517]
◇ 準備2
- 上記の''handson7.5-1.ipynb''の【実行中】をそのまましてください
- [[Google Drive:https://drive.google.com/]]にアクセスして,Google Colabを新規作成してください
- 名前を ''handson7.5-2.ipynb'' とします
- 下記のコードを貼り付けて実行してください
# 送信用とJSON関係のモジュールをインポート
import requests
import json
# POST先URL (上記出力のサーバアドレス + APIパラメータ)
url = "https://f627-34-125-200-230.ngrok.io" + "/predict"
# JSON形式のデータ (アヤメ3組を入力してみる)
json_data = {
"data":[
[1, 2, 3, 4],
[3, 7, 2, 9],
[6, 8, 1, 3]
]
}
# POST送信
response = requests.post(
url,
data = json.dumps(json_data) # データをJSON形式にエンコード
)
# レスポンス内容の確認
res_data = response.text
print(res_data)
【実行結果】
{"prediction":["setosa","virginica","setosa"],"log_proba":[[-0.9210444567587419,-0.9267194253195576,-1.579652054455776],[-2.205814584875692,-4.76721288044762,-0.12631725142856434],[-0.011910465650825333,-4.763788201065021,-5.711836534624314]]}
◇ サーバの停止 (shutdown)
- 左側の実行中ボタンを押してください.
【実行結果】
INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [287]
◇ その他
- 興味のある方は,以下のようなPythonの実践も,調べながらお試しください
-- 事前学習済みモデル(pre-trained models)
-- モデルの監視 (model monitoring)