主要な流れ
①プロジェクトの概要を確認する
②データを取得する
③データ可視化によって、データの概要を理解する
④機械学習アゴリズムのため、データを準備する
⑤モデルを選択して訓練する
⑥モデルを微調整する。
⑦ソリューションを提出する。
⑧システムの稼働、監視、メンテナンス
問題解決のフレームワークとして、下記3点を明確する
①モデル: 教師あり学習 or 教師なし学習 or 強化学習
②テスクの種類: 分類 or 回帰 or その他
③学習方式: バッチ学習 or オンライン学習
例えば、
①訓練データにラベルがある場合は、教師あり学習を採用する。
②連続型の値を予測する場合は、回帰アルゴリズムを採用する。
・特徴が一つの場合 ⇒ 単回帰問題
・特徴が複数の場合 ⇒ 重回帰問題
・予測結果が一つの場合 ⇒ 単変量回帰問題
・予測結果が一つの場合 ⇒ 多変量回帰問題
③下記の場合、バッチ学習を採用する。
・データが一括にメモリに格納できるほど少ない
・一度訓練したら、後続データがない、もしくは頻繁なデータ更新がない
※データが多すぎ場合、
・MapReduceを使って、バッチ学習タスクを分けて複数のサーバで実行する。
・オンライン学習を採用する。
性能指標を確定する
①二乗平均平方根誤差(RMSE:Root Mean Squared Error)
⇒ L2ノルム、Euclidean normとも言う、、
と表記
②平均絶対誤差(MAE:Mean Absolute Error)
⇒ L1ノルム、Manhattan norm とも言う、と表記
※解釈
:訓練データの件数
:i件目の訓練データの特徴量が保持されるベクトル
例)
:i件目の訓練データのラベル(予測値の正解)
:全データの特徴量が保持される行列 ※各行は
の転置ベクトル
例)
:予測関数、「仮説」とも言う
:予測値、即ち
:訓練データを仮説hに適用した場合のコスト関数
データ構造を確認する(Pandas、Matplotlib、scikit-learnを使う)
メソッド | 用途 | 備考 |
---|---|---|
dataset.head() | 先頭N件のデータを確認する。 | デフォルト:先頭5件 |
dataset.info() | データ総件数、各属性の型及びnon-nullデータ件数を確認する。 | 欠損値がある場合は要注意 |
dataset['colname'].value_counts() | 該当列のカテゴリごとのデータ件数を確認する。 | |
dataset.describe() | 数値型の属性のデータ概要を確認する。 | 標準偏差や四分位の計算にnullデータが対象外なので要注意 |
dataset.hist(bins=50, figsize=(20,15)) | 数値型の各属性のヒストグラムを描画する。 | 横軸:属性値、縦軸:データ件数 |
■今の課題 | ||
①横軸の属性値が比率的に調整されたかもしれない(scaled) | ||
②横軸の属性値が上限つけられたかもしれない(capped) | ||
※システムが上限値を超えないと認識されるかもしれないので、下記の対処が必要である。 | ||
・レベルを訂正する。 | ||
・訓練データとテストデータから上限値のデータを削除する。 | ||
③各属性の比率が一致していない。 | ||
④ヒストグラムの末尾が重い場合があるので、パターン認識に困難する。 | ||
※後で正規分布に変換するのが望ましい |
テストデータの作成
①原則
作成されたテストデータを絶対に見ないでください。
見ちゃったら、面白いパターンに注目して、特定な機械学習モデルを選択してしまう恐れがある。
このテストデータを使って、一般化誤差を評価した場合、楽観的な結果になりがち、期待通りに実行できないかもしれない。
※データ・スヌーピング・バイアス(data snooping bias)の罠
②サンプルソース
from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(mydataset, test_size=0.2, random_state=42)
データセットの層ごとに十分なデータ量が保有することが重要である。
言い換えれば、割に少ない層数で、各層のデータ件数が多いことが望ましい。
from sklearn.model_selection import StratifiedShuffleSplit
mydataset["new_col"] = pd.cut(mydataset["key_col"],
bins=[0., 1.5, 3.0, 4.5, 6., np.inf],
labels=[1, 2, 3, 4, 5])
split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
for train_index, test_index in split.split(mydataset, mydataset["key_col"]):
strat_train_set = mydataset.loc[train_index]
strat_test_set = mydataset.loc[test_index]
for set_ in (strat_train_set, strat_test_set):
set_.drop("new_col", axis=1, inplace=True)
データを観察する
pandas.DataFrame.plot()メソッドを使って、散布図を描く。
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.plot.html
mydataset.plot(kind="scatter", x="longitude", y="latitude", alpha=0.4,
s=housing["population"]/100, label="population", figsize=(10,7),
c="median_house_value", cmap=plt.get_cmap("jet"), colorbar=True,
sharex=False)
plt.legend()
相関関係を確認する
corr_matrix = mydataset.corr()
>>> mydataset["median_house_value"].sort_values(ascending=False)
median_house_value 1.000000
median_income 0.687170
total_rooms 0.135231
housing_median_age 0.114220
households 0.064702
total_bedrooms 0.047865
population -0.026699
longitude -0.047279
latitude -0.142826
Name: median_house_value, dtype: float64
from pandas.plotting import scatter_matrix
attributes = ["median_house_value", "median_income", "total_rooms",
"housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))
※相関関係以外の変なデータを削除する必要がある。
※場合によっては、属性を組み合わせでから確認する必要がある。
housing["rooms_per_household"] = housing["total_rooms"]/housing["households"]
housing["bedrooms_per_room"] = housing["total_bedrooms"]/housing["total_rooms"]
housing["population_per_household"]=housing["population"]/housing["households"]
# And now let’s look at the correlation matrix again:
>>> corr_matrix = housing.corr()
>>> corr_matrix["median_house_value"].sort_values(ascending=False)
median_house_value 1.000000
median_income 0.687160
rooms_per_household 0.146285
total_rooms 0.135097
housing_median_age 0.114110
households 0.064506
total_bedrooms 0.047689
population_per_household -0.021985
population -0.026920
longitude -0.047432
latitude -0.142724
bedrooms_per_room -0.259984 #Better than total_rooms
Name: median_house_value, dtype: float64
データ準備
mydataset = strat_train_set.drop("median_house_value", axis=1)
labels = strat_train_set["median_house_value"].copy()
データクレンジング
属性に欠損値がある場合
①該当属性に欠損値のあるデータを削除する。
②該当属性を削除する。
③0や平均値で欠損値を補完する。
# Option 1
housing.dropna(subset=["total_bedrooms"])
# Option 2
housing.drop("total_bedrooms", axis=1)
# Option 3
median = housing["total_bedrooms"].median() #この値はテストデータや新データにも適用すべき
housing["total_bedrooms"].fillna(median, inplace=True)
# Option 3 もっと便利なツール
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy="median")
housing_num = housing.drop("ocean_proximity", axis=1) #数値型以外の属性を削除
imputer.fit(housing_num) #すべての数値型の属性に対して、平均値を計算する
>>> imputer.statistics_ #各属性の平均値が記録された
array([ -118.51 , 34.26 , 29. , 2119.5 , 433. , 1164. , 408. , 3.5409])
>>> housing_num.median().values #各属性の平均値が計算通り
array([ -118.51 , 34.26 , 29. , 2119.5 , 433. , 1164. , 408. , 3.5409])
X = imputer.transform(housing_num) #欠損値がある場合平均値で補完して、NumPyのarrayを返す
housing_tr = pd.DataFrame(X, columns=housing_num.columns) #PandasのDataFrameに変換する
カテゴリの属性を文字列から序数値に変更する(OrdinalEncoder)
>>> housing_cat = housing[["ocean_proximity"]]
>>> housing_cat.head(10)
ocean_proximity
17606 <1H OCEAN
18632 <1H OCEAN
14650 NEAR OCEAN
3230 INLAND
3555 <1H OCEAN
19480 INLAND
8879 <1H OCEAN
13685 INLAND
4937 <1H OCEAN
4861 <1H OCEAN
>>> from sklearn.preprocessing import OrdinalEncoder
>>> ordinal_encoder = OrdinalEncoder()
>>> housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
>>> housing_cat_encoded[:10]
array([[0.],
[0.],
[4.],
[1.],
[0.],
[1.],
[0.],
[1.],
[0.],
[0.]])
# 序数に対応するカテゴリを確認する
>>> ordinal_encoder.categories_
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
dtype=object)]
カテゴリー変数を数字として扱って機械学習するのは何故ダメなのか?
場合によっては、モデルが正しく生成できないからです。
機械学習アルゴリズムは「two nearby values are more similar than two distant values」と仮定するので、
「bad、average、good、excellent」といったカテゴリの場合は良いですが、品名A、B、Cの場合はうまく行かない。
カテゴリー変数の品名A、B、Cに優劣、平均はない。
例えば品名A、B、C、を1、2、3に変換してモデル生成をすると、1(A)よりも3(C)の方が良い製品、という間違ったモデルを生成してしまう。
また、数字として扱うと品目の平均をとれることになるので、1(A)+2(B)+3(C)=6/3=2(B)となります。これは品名の平均は2(B)になる、という誤ったモデルが生成されてしまうことになる。
品名に優劣、平均という概念はありませんが、数字ラベルを用いるとこのようなおかしなモデルを作ってしまうのでカテゴリー変数を数字で扱うのがダメなのだ。
では、解決するにはOne-Hotエンコーディングの出番だ。
One-Hotエンコーディング(OneHotEncoder)
One Hot encodingとは、カテゴリー変数を機械学習のアルゴリズムが学習しやすいように0と1で表現する処理のことです。
>>> from sklearn.preprocessing import OneHotEncoder
>>> cat_encoder = OneHotEncoder()
>>> housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
# 出力はNumPy arrayではなく、SciPy sparse matrixであることを注目してください。
# カテゴリ数が何千以上の場合は役に立つ。
>>> housing_cat_1hot
<16512x5 sparse matrix of type '<class 'numpy.float64'>'
with 16512 stored elements in Compressed Sparse Row format>
# 疎行列(sparse matrix)は、行ごとに値が「1」の属性が一つだけで、それ以外はすべて「0」である。
# メモリに大量の「0」を格納するのがもったいないので、疎行列が「0」以外の要素の位置のみが保持する。
# 普通の2D arrayとして扱えるが、Numpy arrayへ変換したい場合は、toarray()メソッドを使って良い。
>>> housing_cat_1hot.toarray()
array([[1., 0., 0., 0., 0.],
[1., 0., 0., 0., 0.],
[0., 0., 0., 0., 1.],
...,
[0., 1., 0., 0., 0.],
[1., 0., 0., 0., 0.],
[0., 0., 0., 1., 0.]])
# 対応するカテゴリを確認する
>>> cat_encoder.categories_
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
dtype=object)]
※カテゴリ数が多い場合は、One-Hotエンコーディングよりも表現学習( representation learning )が考えられる。
フィーチャースケーリング
機械学習アルゴリズムは、各数値型の特徴量の取りうる値の範囲(スケール)が非常に異なる場合はうまく学習できない。
例えば、体重と身長、家の価格と部屋数では、その単位と値の範囲が異なります。
そのため、学習前の前処理で、特徴量間のスケールを揃える必要があります。
※目標値はスケーリングする必要がない。
全特徴の単位を揃えるために、下記の2つ方法がある。
①正規化(normalization)※min-max scalingとも言う
特徴量の値の範囲を一定の範囲におさめる変換になります。主に[0, 1]か、[-1, 1]の範囲内におさめることが多いです。
正規化する前の特徴量の最小値は正規化されて0に、最大値は1となり、新しい特徴量は[0,1]におさまります。
②標準化(standardization)
特徴量の平均を0、分散を1にする変換になります。
方法 | Scikit-LearnのTransformer | ニューラルネットワーク学習に向く | 外れ値に強い |
---|---|---|---|
正規化 | MinMaxScaler | 〇 | × |
標準化 | StandardScaler | × | 〇 |
※ニューラルネットワークが入力値の取りうる範囲が0~1であることを期待する場合が多い。 |
正規化と標準化の使い分け
基本は標準化を用います。理由は、正規化の場合、外れ値が大きく影響してしまうためです。
ただし、画像データの場合は学習コストを下げるため、[0,1]の範囲に収まるよう255.0で割ることで正規化するのが一般的です。
正規化を使うとき:
- 画像処理におけるRGBの強さ[0,255]
- sigmoid, tanhなどの活性化関数を用いる、NNのいくつかのモデル
標準化を使うとき:
- ロジスティック回帰、SVM、NNなど勾配法を用いたモデル
- kNN, k-meansなどの距離を用いるモデル
- PCA, LDA(潜在的ディリクレ配分法), kernel PCA などのfeature extractionの手法
使わないとき:
決定木、ランダムフォレスト
データ変換のためのパイプライン
Scikit-Learnがデータ変換のステップを順次実行するパイプラインを提供する。
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
# Pipelineコンストラクタに順次実行するname/estimatorペアを定義する
num_pipeline = Pipeline([
('imputer', SimpleImputer(strategy="median")),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()), # 最後のestimatorは必ずtransformersにする。fit_transform()メソッドが必須
])
# パイプラインのfit()メソッドが呼び出された場合、各transformersのfit_transform()メソッドが順次実行する。
# 前の出力は次の入力となって、最後のestimatorに到達したら、そのfit()メソッドを実行する、
housing_num_tr = num_pipeline.fit_transform(housing_num)
# 数値型・カテゴリ型の特徴量を一遍に変換するには、Scikit-LearnがColumnTransformerを提供する。
from sklearn.compose import ColumnTransformer
# 数値型の特徴量のリスト
num_attribs = list(housing_num)
# カテゴリ型の特徴量のリスト
cat_attribs = ["ocean_proximity"]
# ColumnTransformerコンストラクタにtuples(name, transformer, target columns)のリストを定義する
# 注意点:各transformersの出力行数が同じであること。
full_pipeline = ColumnTransformer([
("num", num_pipeline, num_attribs), # num_pipelineを使って、数値型の特徴量を変換する
("cat", OneHotEncoder(), cat_attribs), # OneHotEncoderを使って、カテゴリ型の特徴量を変換する
])
housing_prepared = full_pipeline.fit_transform(housing)
# OneHotEncoderは疎行列を、num_pipelineは密行列を返す。
# ColumnTransformerは疎行列・密行列が混ぜる場合、
# 0でない値の比率がsparse_threshold(デフォルトは「0.3」)より小さい場合、疎行列を返す。
# ここは最終的に密行列が返す。
モデルの選定と訓練
単独のモデルよりも、アンサンブル学習のほうが効果出やすい。
交差検証
ファインチューニング
・グリッドサーチ
・ランダムサーチ