# データ分析、機械学習の基礎 ###### tags: `データ分析 機械学習` ### Kaggleのワークフロー 1. 探索的データ分析 2. ベースラインモデルの構築 3. Validationの構築 4. 特徴量エンジニアリング 5. ハイパーパラメータ調整 6. アンサンブル ### パッケージの読み込み * 頻繁に利用するので短い名前をつけておく ``` python= import numpy as np import pandas as pd ``` ### データの読み込み * Pandasに含まれるread_csv()を利用する * pandas.DataFrameと呼ばれる形式で読みこむ ``` python= train = pd.read_csv('../dir/dir/train.csv') test = pd.read_csv('../dir/dir/test.csv') ``` #### .head() .tail()は数行の中身を返す 引数で数を指定(無しなら5行) ``` python= train.head(10) test.tail(5) ``` #### 行方向に結合して、一括で処理できるようにする * sort = Falseとすることで、列をソートしないようにしている * sortすると列の並びが変わる * Falseの場合( PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked) * Trueの場合(Age Cabin Embarked Fare Name Parch PassengerId Pclass Sex SibSp Survived Ticket) ``` python= data = pd.concat([train, test], sort=False) ``` #### 欠損値の数を計算する * すべての要素について、欠損値かどうかの真偽値を返す * 欠損値を補完すべきなのか??? * 欠損値はそもそも欠損していることに意味がある可能性がある * 闇雲に欠損値を補完するのは得策ではない * そもそもLightGBMなら欠損値をそのまま使えるしね * じゃあどうするのか * 欠損値をそのまま扱う * 代表的な値で欠損値を補完する * 他の特徴量から欠損値を予測して補完する * 欠損値か否かの情報を用いて新しい特徴量を作る ```python= data.isnull().sum() ``` ### EDA:Exploratory Data Analysis(探索的データ分析) * データの特徴を探求し、構造を理解することを目的とした初動調査 * 要するに、今から扱うデータについて理解しろということ #### Pandas Profilingで概要の確認 ```python= import pandas as pd import pandas_profiling train = pd.read_csv('dir/dir/train.csv') train.profile_report() ``` #### 各特徴量について目的変数との関係を確認する * 各特徴量について、目的変数(例えば、タイタニックなら死亡(0)生存(1))との関係を確認する * 予測性能に寄与する可能性のある仮説を見つける ##### ex)Ageと目的変数の関係(matplotlib.pyplotを使用) * binsは表示する棒の数 例えば0~90歳のレンジでbins=30なら1本の棒は3歳分 * dropna()は欠損値をドロップ(除外)している * fillna()は欠損値を置換(穴埋め)する * alphaは透過度 ``` python= import pandas as pd import matplotlib.pyplot as plt plt.hist(train.loc[train['Survived'] == 0, 'Age'].dropna(), bins=30, alpha=0.5, label='0') plt.hist(train.loc[train['Survived'] == 1, 'Age'].dropna(), bins=30, alpha=0.5, label='1') plt.xlabel('Age') plt.ylabel('count') plt.legend(title='Servived') ``` ![](https://i.imgur.com/0RnIdH4.png) ##### ex)SibSp(兄弟、配偶者の数)と目的変数の関係(seabornを使用) * x引数に集計したい列名を指定 * hue引数ではx引数を分割して集計したい列名を指定 * data引数には、pandas.DataFrameを指定 * plt.legendのloc引数にはupper rightを指定して凡例を右上に設置 指定しない場合は自動配置 ```python= import seaborn as sns sns.countplot(x='SibSp', hue='Survived', data=train) plt.legend(loc='upper right', title='Survived') ``` ![](https://i.imgur.com/MmQN9Ls.png) ### Feature Engineering(特徴量エンジニアリング) ##### 「特徴量エンジニアリングはデータサイエンスの芸術的な部分である」 --Sergey Yurgenson * 特徴量エンジニアリングとは、機械学習モデルのパフォーマンスと精度を向上させるために、追加の変数(特徴量)を構築してデータセットに追加すること ##### 仮説から新しい特徴量を作る * 例えばEDAによって、Parch・SibSpの両者を足し合わせて「家族の人数」という新たな特徴量を作ると、予測性能に寄与しそうという仮説を立てその検証をする * 4行目と5行目はtrainとtestに作成したFamilySizeを振り分けている ```python= import seaborn as sns data['Familysize'] = data['Parch'] + data['SibSp'] + 1 train['Familysize'] = data['Familysize'][:len(train)] test['Familysize'] = data['Familysize'][len(train):] sns.countplot(x='Familysize',data = train, hue='Survived') ``` ![](https://i.imgur.com/SbbffGN.png) * (0=死亡 1=生存) * 作成したFamilySizeを眺めるとまず1人の乗客が多いことが分かる * また1人の乗客は死亡率は非常に高いことが分かる * またFamilySize >= 5の場合も死亡が生存を上回っていることが分かる * よってそれらを加味して新たなAlone特徴量を作成する ```python= #さらに新しい特徴量Aloneを作る data['Alone'] = 0 #DataFrame型変数.loc['行ラベル名', '列ラベル名'] 要するにFamilysizeが1の人のAloneを1にする処理 data.loc[data['Familysize'] == 1, 'Alone'] = 1 train['Alone'] = data['Alone'][:len(train)] test['Alone'] = data['Alone'][len(train):] ``` ### 機械学習アルゴリズムを使う(LightGBM) * 非常に使用頻度が高い「LightGBM」という勾配ブースティングのパッケージを利用する * 特徴量エンジニアリングによって作成されたdataを突っ込む * 使用方法 * 学習用・検証用にデータセットを分割する * LightGBMは決定木をもとにした機械学習アルゴリズム * 次々と学習を重ねていくため、高い予測性能が期待できる * だが、過学習に陥る可能性あり * よって学習に利用しない検証用データセットに対する性能を見ながら学習を打ち切る「early stopping」を利用する * よってX_trainをX_train(学習用)とX_valid(検証用)に分割する * random_stateでseedのセット * stratifyにy_trainを指定することで、分割後の学習用データセットと検証用データセットの中身の割合が等しくなるようにしている ```python= from sklearn.model_selection import train_test_split X_train,X_valid,y_train,y_valid = \ train_test_split(X_train,y_train,test_size=0.3,random_state=0,stratify=y_train) ``` * カテゴリ変数をリスト形式で宣言する * LightGBMでは、カテゴリ変数に対して特別な処理を自動的に実行してくれる * 明示的にLightGBMに教えれば良いだけ * カテゴリ変数とは??? * カテゴリ変数とは身長とか体重とか数値で表せる変数ではなく、性別(男性と女性)とか、好きな色とかのように区別するための尺度 * LightGBMは決定木なのでカテゴリ変数をそのまま扱える * 扱えない場合にはOne hot Encodingとかをして数値化して扱えるようにする ```python= categorical_features = ['Embarked', 'Pclass', 'Sex'] ``` ##### LightGBMの学習・予測 * 3行目と5行目で学習用データセットと検証用データセットをlightgbm.Datasetを使って作成している * lightgbm.Datasetの第1引数にはX(特徴量)、第2引数にはy(目的変数)を渡す * カテゴリ変数がある場合には、categorical_featureで指定 * 検証用データセットにはreferenceで学習用データセットのlightgbm.Dataset(今回ではlgb_train)を渡す * paramsでハイパーパラメータを設定する ```python= import lightgbm as lgb lgb_train = lgb.Dataset(X_train, y_train, categorical_feature=categorical_features) lgb_eval = lgb.Dataset(X_valid, y_valid, reference=lgb_train, categorical_feature=categorical_features) params={ 'objective': 'binary' } ``` * lgb.train()ではnum_boost_round(学習の実行回数の最大値)を1000に設定 * early_stopping_roundsは,early_stoppingの判定基準 * ここでは10にセットしており、連続して10回学習を重ねても検証用データセットに対する性能が改善しなかった場合に学習を打ち切る ```python= model = lgb.train(params,# ハイパーパラメーターの投入 lgb_train,#訓練データの指定 valid_sets=[lgb_train,lgb_eval], #モデル評価用データ verbose_eval=10, #学習サイクルを10ずつ表示 num_boost_round=1000, #学習の実行回数の最大値 early_stopping_rounds=10 #アーリーストッピング ) y_pred = model.predict(X_test, num_iteration=model.best_iteration) ``` * 今回はy_predの値として、0~1の連続値が入っている * 今回は「y_pred > 0.5」ならば「y == 1」そうでなければ「y == 0」という離散値に変換 * astype(int)で1もしくは0に変換 ```python= y_pred = (y_pred > 0.5).astype(int) sub['Survived'] = y_pred sub.to_csv('submission.csv', index=False) ``` ##### 特徴量の重要度を調べる ```python= lgb.plot_importance(model, height=0.5, figsize=(12,8)) ``` ![](https://i.imgur.com/tVSDWxR.png) ### ハイパーパラメータを調整する * 機械学習アルゴリズムの振る舞いはハイパーパラメータで制御されている * よってハイパーパラメータの値次第で予測結果は変わる * ハイパーパラメータの調整は主に2種類 * 手動 * チューニングツール * データサイズが大きいと、調整が現実的な時間で終わらないので、ツールを使うのが良い #### Optunaを使う あらかじめ関数内のtrial.suggest_int()のように、探索範囲を定義する ```python= import optuna from sklearn.metrics import log_loss def objective(trial): params = { #binary=二値分類 ラベルは0or1 'objective':'binary', #関数内のtrial.suggest_int()の中に探索範囲を定義 'max_bin':trial.suggest_int('max_bin', 255, 500), 'learning_rate':0.05, 'num_levels':trial.suggest_int('num_levels', 32, 128), } lgb_train = lgb.Dataset(X_train, y_train, categorical_feature=categorical_features) lgb_eval = lgb.Dataset(X_valid, y_valid, reference=lgb_train, categorical_feature=categorical_features) model = lgb.train(params, lgb_train, valid_sets=[lgb_train, lgb_eval], verbose_eval=10, num_boost_round=1000, early_stopping_rounds=10) y_pred_valid = model.predict(X_valid, num_iteration=model.best_iteration) #指標は損失なので、小さいほど望ましい score = log_loss(y_valid, y_pred_valid) return score ``` ```python= study = optuna.create_study(sampler=optuna.samplers.RandomSampler(seed=0)) study.optimize(objective, n_trials=40) study.best_params ``` * n_trialsは実行回数 ここでは40回 * 今回の範囲で最適な値をとったハイパーパラメータがstudy.best_paramsに格納 * study.best_paramsの値を用いて、LightGBMを学習・予測し直す ```python= params = { 'objective': 'binary', 'max_bin': study.best_params['max_bin'], 'learning_rate': 0.05, 'num_leaves': study.best_params['num_levels'] } lgb_train = lgb.Dataset(X_train, y_train, categorical_feature=categorical_features) lgb_eval = lgb.Dataset(X_valid, y_valid, reference=lgb_train, categorical_feature=categorical_features) model = lgb.train(params, lgb_train, valid_sets=[lgb_train, lgb_eval], verbose_eval=10, num_boost_round=1000, early_stopping_rounds=10) y_pred = model.predict(X_test, num_iteration=model.best_iteration) ``` ### Cross Validation(交差検証) * 交差検証とは、統計学において標本データを分割し、その一部をまず解析して、残る部分でその解析のテストを行い、解析自身の妥当性の検証・確認に当てる手法を指す。データの解析がどれだけ本当に母集団に対処できるかを良い近似で検証・確認するための手法である * 要するに、学習用データセットから検証用データセットを作成し、自分のモデルの性能を評価する * Cross Validationには学習用データセットを無駄にしないという利点がある * ホールドアウト検証では、検証用データセットに該当する部分を学習に利用できないという欠点がある * しかし、Cross Validationでは複数の分割を実施知るので、全体として与えられたデータセットを漏れなく学習に利用できる ##### ホールドアウト検証 * LightGBMでやったように、学習用データセットを分割した上で(train,valid)LightGBMを学習させた * train_test_split()でデータを分割できるよね ##### KFold(K-分割交差検証) * Cross Validationとは複数回にわたって、異なる方法でデータセットを分割し、それぞれでホールドアウト検証を実行する * 1回のホールドアウト検証で生じる偏りに対する懸念を弱めることができる * K-分割交差検証は、データをK個に分割してそのうち1つをテストデータに残りのK-1個を学習データとして正解率の評価を行います * これをK個のデータすべてが1回ずつテストデータになるようにK回学習を行なって精度の平均をとる手法です * train_test_split()を複数回やってもできるけど、sklearnには便利なKFoldというクラスがあるよ ![](https://i.imgur.com/pteatpg.png) ```python= from sklearn.model_selection import KFold y_preds = [] models = [] oof_train = np.zeros((len(X_train),)) #n_splitsはデータの分割数 検証はここで指定した数値の回数行われる #shuffle=Trueとすることで、データセットを分割前にシャッフルしている #shuffle=Falseにした場合は現状のデータセットのままで分割 #random_stateはシードの指定 cv = KFold(n_splits=5, shuffle=True, random_state=0) categorical_features = ['Embarked', 'Pclass', 'Sex'] params = { 'objective': 'binary', 'max_bin': 300, 'learning_rate': 0.05, 'num_leaves': 40 } #要素と同時に各分割のindex(fold_id)も取得している for fold_id, (train_index, valid_index) in enumerate(cv.split(X_train)): X_tr = X_train.loc[train_index, :] X_val = X_train.loc[valid_index, :] y_tr = y_train[train_index] y_val = y_train[valid_index] lgb_train = lgb.Dataset(X_tr, y_tr, categorical_feature=categorical_features) lgb_eval = lgb.Dataset(X_val, y_val, reference=lgb_train, categorical_feature=categorical_features) model = lgb.train(params, lgb_train, valid_sets=[lgb_train, lgb_eval], verbose_eval=10, num_boost_round=1000, early_stopping_rounds=10) oof_train[valid_index] = model.predict(X_val, num_iteration=model.best_iteration) y_pred = model.predict(X_test, num_iteration=model.best_iteration) y_preds.append(y_pred) models.append(model) ``` * Cross Validationを実施した時は、各分割でのスコアの平均をスコアと見なすことが多い(CVスコア) * 学習用データセットを分割した最小単位をそれぞれ「fold」と呼ぶ * Out_of_fold(oof)・・・・・・各分割で学習に使われなかったfold、つまり答えに使われたfoldのこと * oof_trainという変数名「trainのoof」という意味 * 要するに、各分割でのoofに対する予測値を格納している ```python= pd.DataFrame(oof_train).to_csv('oof_train_kfold.csv', index=False) scores = [ m.best_score['valid_1']['binary_logloss'] for m in models ] score = sum(scores) / len(scores) print('===CV scores===') print(scores) print(score) ``` ```python= from sklearn.metrics import accuracy_score y_pred_oof = (oof_train > 0.5).astype(int) accuracy_score(y_train, y_pred_oof) ``` * accuracy_scoreで正解率を計算している ```python= len(y_preds) #5 y_preds[0][:10] ``` ![](https://i.imgur.com/muzftKy.png) ```python= y_sub = sum(y_preds) / len(y_preds) y_sub = (y_sub > 0.5).astype(int) ``` * y_predsに保存されている各分割での予測値を平均した値を、最終的なsubmitに利用します * 1行目で平均を計算 * 2行目で連続値を離散値に変換 ### データセットの分割方法 * データセットの分割にあたっては、データセットや課題設定の特徴を意識する * というのも、目的は未知にデータセットに対する性能を高めること * そのため、優れた検証用データセットとは、Private LBに似ているデータセット * 先ほどのKFoldでは「y==1」の割合が2,4の時に顕著に異なっている ```python= from sklearn.model_selection import KFold cv = KFold(n_splits=5, shuffle=True, random_state=0) for fold_id, (train_index, valid_index) in enumerate(cv.split(X_train)): X_tr = X_train.loc[train_index, :] X_val = X_train.loc[valid_index, :] y_tr = y_train[train_index] y_val = y_train[valid_index] print(f'fold_id: {fold_id}') print(f'y_tr y==1 rate: {sum(y_tr)/len(y_tr)}') print(f'y_val y==1 rate: {sum(y_val)/len(y_val)}') ``` ![](https://i.imgur.com/vmheJS7.png) * 「y=1」の割合が均等でない場合、「y=1」を重要視したり逆に軽視したりと、機械学習アルゴリズムの学習がうまくいかない傾向にある * このような状況では適切に特徴を学習できず、未知のデータセットに対する性能が劣化する可能性 * よって割合を保ったままCross Validationを実施するのが望ましい * そんな時に、sklearnのStratifiedKFold()が使える ```python= from sklearn.model_selection import StratifiedKFold cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0) for fold_id, (train_index, valid_index) in enumerate(cv.split(X_train, y_train)): X_tr = X_train.loc[train_index, :] X_val = X_train.loc[valid_index, :] y_tr = y_train[train_index] y_val = y_train[valid_index] print(f'fold_id: {fold_id}') print(f'y_tr y==1 rate: {sum(y_tr)/len(y_tr)}') print(f'y_val y==1 rate: {sum(y_val)/len(y_val)}') ``` ![](https://i.imgur.com/pnfT5jR.png) * KFoldをStratifiedKFoldを差し替えると目的変数の割合が整う * cv.split(X_train)をcv.split(X_train, y_train)となっている * こうすることによって、y_trainを基にした分割を実行してくれる ```python= from sklearn.model_selection import StratifiedKFold y_preds = [] models = [] oof_train = np.zeros((len(X_train),)) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0) categorical_features = ['Embarked', 'Pclass', 'Sex'] params = { 'objective': 'binary', 'max_bin': 300, 'learning_rate': 0.05, 'num_leaves': 40 } for fold_id, (train_index, valid_index) in enumerate(cv.split(X_train, y_train)): X_tr = X_train.loc[train_index, :] X_val = X_train.loc[valid_index, :] y_tr = y_train[train_index] y_val = y_train[valid_index] lgb_train = lgb.Dataset(X_tr, y_tr, categorical_feature=categorical_features) lgb_eval = lgb.Dataset(X_val, y_val, reference=lgb_train, categorical_feature=categorical_features) model = lgb.train(params, lgb_train, valid_sets=[lgb_train, lgb_eval], verbose_eval=10, num_boost_round=1000, early_stopping_rounds=10) oof_train[valid_index] = model.predict(X_val, num_iteration=model.best_iteration) y_pred = model.predict(X_test, num_iteration=model.best_iteration) y_preds.append(y_pred) models.append(model) pd.DataFrame(oof_train).to_csv('oof_train_skfold.csv', index=False) print(oof_train[:10]) scores = [ m.best_score['valid_1']['binary_logloss'] for m in models ] score = sum(scores) / len(scores) print('===CV scores===') print(scores) print(score) from sklearn.metrics import accuracy_score y_pred_oof = (oof_train > 0.5).astype(int) accuracy_score(y_train, y_pred_oof) y_sub = sum(y_preds) / len(y_preds) y_sub = (y_sub > 0.5).astype(int) y_sub[:10] sub['Survived'] = y_sub sub.to_csv('submission_lightgbm_skfold.csv', index=False) ``` ##### 分割の際に気をつけること * 分割の際に気をつけたいことは、目的変数の割合以外にも、以下のような点がある * データセット内に時系列がないか * データセット内にグループが存在しないか ### Ensemble(アンサンブル) * アンサンブルとは、複数の機械学習モデルを組み合わせることで性能の高い予測値を獲得する手法 * 適当な3つのcsvファイルを利用してみる ```python= import pandas as pd sub_lgbm_sk = pd.read_csv('../input/submit-files/submission_lightgbm_skfold.csv') sub_lgbm_ho = pd.read_csv('../input/submit-files/submission_lightgbm_holdout.csv') sub_rf = pd.read_csv('../input/submit-files/submission_randomforest.csv') ``` * 相関を計算するためには、pandas.DataFrame.corr()が利用できる * 事前に各予測値をdfというpandas.DataFrameにまとめる ```python= df = pd.DataFrame({'sub_lgbm_sk': sub_lgbm_sk['Survived'].values, 'sub_lgbm_ho': sub_lgbm_ho['Survived'].values, 'sub_rf': sub_rf['Survived'].values}) df.head() ``` ![](https://i.imgur.com/84Tu2GF.png) * pandas.DataFrame.corr()を実行すると、列同士の相関が計算できる * アンサンブルの観点では多様性が大切なので、予測値の相関が小さい方が望ましい * 絶対的な基準はないけど、0.95以下なら充分に相関が小さいと言える ```python= df.corr() ``` ![](https://i.imgur.com/iR61EAF.png) * 3つのファイルの予測値部分を合計する ```python= sub = pd.read_csv('../input/titanic/gender_submission.csv') sub['Survived'] = sub_lgbm_sk['Survived'] + sub_lgbm_ho['Survived'] + sub_rf['Survived'] sub.head(20) ``` ![](https://i.imgur.com/EOfu8ve.png) * 合計が2以上の場合は全体として(目的変数)の予測値を1とする ```python= sub['Survived'] = (sub['Survived'] >= 2).astype(int) sub.to_csv('submission_lightgbm_ensemble.csv', index=False) sub.head(10) ``` ![](https://i.imgur.com/WFqH2Zw.png) <span style="color: #ff3333">aaa</span> <span style="text-decoration: underline">aaa</span> ## 雑談 (0→やまぴ 1→みや 2→やすい 3→りょうくん 4→アミカ)