機械学習モデルの最適化をTabPFNにおまかせしてみる

##  はじめに

[**TabPFN**](https://github.com/automl/TabPFN)は、テーブルデータ（分類）を対象としたニューラルネットワークベースの自動機械学習モデルです。

以下は[**論文**](https://arxiv.org/abs/2207.01848)で紹介された内容の翻訳です。

>我々は、小さな表形式データセットに対して1秒以内に教師あり分類を行うことができ、ハイパーパラメータのチューニングを必要とせず、最先端の分類法と競争力のある学習済みTransformerであるTabPFNを発表します。TabPFNは我々のネットワークの重みに完全に内包されており、訓練とテストサンプルをセット値の入力として受け入れ、1回のフォワードパスでテストセット全体に対する予測を行う。TabPFNは事前データ適合ネットワーク(PFN)であり、オフラインで一度学習し、我々の事前データから抽出した合成データセットに対してベイズ推定を近似的に行う。この事前分布は因果推論からのアイデアを取り入れたものである。この事前分布は、単純な構造を優先する構造的因果モデルの大きな空間を含んでいる。OpenML-CC18に含まれる18個のデータセット（最大1000個の学習データ、最大100個の純粋な数値特徴（欠損値なし）、最大10クラス）を用いて、我々の手法がブーストツリーを明らかに上回り、複雑な最新のAutoMLシステムと同程度に、最大70倍のスピードアップを達成することを示す。また、GPUが利用可能な場合は3200倍の高速化を実現した。また、OpenMLの67の小規模な数値データセットでこれらの結果を検証した。全てのコード、学習済みTabPFN、対話型ブラウザデモ、ColabノートブックをこのhttpsのURLで提供しています。


Google Colabで実行してみました。


## 実行条件など
>-**Google colab**で実行
-**任意のデータセット**と**sklearn等のデータセット**を読み出せるようにしています。


## 実行

#### 1．読み込むデータセットとデータセットのタイプを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/4e6c1ca0-113a-d98e-1413-699dc61f3cb4.png)

#### 2．データを読み込みます。
以下は、Titanic(seaborn)データセット（分類）を読み込んだ際の表示です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/9c83ccb7-e2f0-55d7-6ac2-025e83961a46.png)

#### 3．データクリーニングを実行します。
※ 1．不要なデータ項目の削除、2．欠損データを含む行を削除、3．カテゴリーデータ項目を Labelエンコード、4．すべての Obeject_col を Label encord（☑ ＝実行）、5．データ項目名を英訳 を実装しています。

[**TabPFN**](https://github.com/automl/TabPFN)は、前処理を行わないと実行できませんでしたので、以下の前処理を実行しました。

**不要なデータ項目の削除**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/1aecf094-c120-9600-49fb-80d2a60c46a5.png)
**欠損データを含む行の削除**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/087b6e6c-fe8f-f176-24f6-7aee949b7346.png)
**指定したカテゴリーデータ項目のLabelエンコーディング**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/56ca3dcb-fd45-4ec9-55ca-2e4288301908.png)
**データクリーニングの最終確認**
データの型（Dtype）がすべて数値（int or float）になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/cdcb1b5a-6020-f785-3cd5-574df23ca66c.png)


#### 4．TabPFN
**N_ensemble_configurations**をUIで変更できるようにしました。
実行後、以下が表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/b133f15d-a822-ed60-6662-54b85562b078.png)


#### 5．モデル評価
実行結果は以下の通りです。以下のように混同行列とClassificationReportを表示させています。
行ったのは前処理だけ、あとは[**TabPFN**](https://github.com/automl/TabPFN)にお任せし、Accuracyは0.78でした。
※**N_ensemble_configurations を4 → 10 に変更すると、Accuracyは0.79となりました。**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/d51c8c7e-47a1-b24f-32c8-8d7ff4305af9.png)
以下は、テストデータ（True）と予測データ（Pred）をデータフレームに格納し、表示させたものです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/d646f907-77a8-bd40-2d91-5ce5fbf48e72.png)

**Note**
Breast cancerでも実行してみました。Accuracyは0.97でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/b26b4bab-b590-0c4b-17e8-21f74ed41424.png)


## 最後に
めっちゃ早いです。
インストールを除いた、データ読込みからtestデータ予測の完了まで約20秒程度です。（Titanicデータ）
前処理は必要ですが、学習～チューニングはお任せできます。

実行コードは以下です。
他の自動機械学習モデルの実行コードと互換の面から、**regression**に関するコードも含んでいます。あしからずご了承ください。


***
## 実行コード
### ライブラリのインストール

```:Python
#@title **Install Library**
!pip install tabpfn
```
```:Python
!pip install googletrans==4.0.0-rc1 --quiet
```
```:Python
#matplotlib日本語化
!pip install japanize-matplotlib
```
```:Python
import japanize_matplotlib
```


### データセット読込み
#### データセットとデータセットタイプ（分類か回帰）選択のフォームセット
※**TabPFNで実行できるのはClassificationだけ。datasetもregressionデータは実行できませんのでご注意下さい。**

```:Python
#@title Select_Dataset { run: "auto" }
#@markdown  **<font color= "Crimson">注意</font>：かならず 実行前に 設定してください。TabPFNで実行できるのはClassificationのみです。**</font>

dataset_type = 'Classification' #@param ["Classification", "Regression"]
dataset = 'Titanic(seaborn) :binary' #@param ['Boston_housing :regression', 'Diabetes :regression', 'Breast_cancer :binary','Titanic :binary', 'Titanic(seaborn) :binary', 'Iris :classification', 'Loan_prediction :binary','wine :classification', 'Occupancy_detection :binary', 'Upload']
```


#### データセット読み込み→データセットのインフォと先頭5行表示
```:Python
#@title Load dataset

#ライブラリインポート
import numpy as np
import pandas as pd   #データを効率的に扱うライブラリ
import seaborn as sns #視覚化ライブラリ
import warnings       #警告を表示させないライブラリ
warnings.simplefilter('ignore')

'''
dataset（ドロップダウンメニュー）で選択したデータセットを読込み、データフレーム（df）に格納。
目的変数は、データフレームの最終列とし、FEATURES、TARGET、X、yを指定した後、データフレーム
に関する情報と先頭5列を表示。
任意のcsvデータを読込む場合は、datasetで'Upload'を選択。

'''

#任意のcsvデータ読込み及びデータフレーム格納、
if dataset =='Upload':
  from google.colab import files
  uploaded = files.upload()#Upload
  target = list(uploaded.keys())[0]
  df = pd.read_csv(target)

#Diabetes データセットの読込み及びデータフレーム格納、
elif dataset == "Diabetes :regression":
  from sklearn.datasets import load_diabetes
  diabetes = load_diabetes()
  df = pd.DataFrame(diabetes.data, columns = diabetes.feature_names)
  df['target'] = diabetes.target

#Breast_cancer データセットの読込み及びデータフレーム格納、
elif dataset == "Breast_cancer :binary":
  from sklearn.datasets import load_breast_cancer
  breast_cancer = load_breast_cancer()
  df = pd.DataFrame(breast_cancer.data, columns = breast_cancer.feature_names)
  #df['target'] = breast_cancer.target  #目的変数をカテゴリー数値とする時
  df['target'] = breast_cancer.target_names[breast_cancer.target]

#Titanic データセットの読込み及びデータフレーム格納、
elif dataset == "Titanic :binary":
  data_url = "https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv"
  df = pd.read_csv(data_url)
  #目的変数 Survived をデータフレーム最終列に移動
  X = df.drop(['Survived'], axis=1)
  y = df['Survived'] 
  df = pd.concat([X, y], axis=1)    #X,yを結合し、dfに格納

#Titanic(seaborn) データセットの読込み及びデータフレーム格納、
elif dataset == "Titanic(seaborn) :binary":
  df = sns.load_dataset('titanic')
  #重複データをカットし、目的変数 alive をデータフレーム最終列に移動
  X = df.drop(['survived','pclass','embarked','who','adult_male','alive'], axis=1)
  y = df['alive']                   #目的変数データ
  df = pd.concat([X, y], axis=1)    #X,yを結合し、dfに格納

#iris データセットの読込み及びデータフレーム格納、
elif dataset == "Iris :classification":
  from sklearn.datasets import load_iris
  iris = load_iris()
  df = pd.DataFrame(iris.data, columns = iris.feature_names)
  #df['target'] = iris.target  #目的変数をカテゴリー数値とする時
  df['target'] = iris.target_names[iris.target]

#wine データセットの読込み及びデータフレーム格納、
elif dataset == "wine :classification":
  from sklearn.datasets import load_wine
  wine = load_wine()
  df = pd.DataFrame(wine.data, columns = wine.feature_names)
  #df['target'] = wine.target  #目的変数をカテゴリー数値とする時
  df['target'] = wine.target_names[wine.target]

#Loan_prediction データセットの読込み及びデータフレーム格納、 
elif dataset == "Loan_prediction :binary":
  data_url = "https://github.com/shrikant-temburwar/Loan-Prediction-Dataset/raw/master/train.csv"
  df = pd.read_csv(data_url)

#Occupancy_detection データセットの読込み及びデータフレーム格納、 
elif dataset =='Occupancy_detection :binary':
  data_url = 'https://raw.githubusercontent.com/hima2b4/Auto_Profiling/main/Occupancy-detection-datatest.csv'
  df = pd.read_csv(data_url)
  df['date'] = pd.to_datetime(df['date'])    #[date]のデータ型をdatetime型に変更

#Boston データセットの読込み及びデータフレーム格納 
else:
  from sklearn.datasets import load_boston
  boston = load_boston()
  df = pd.DataFrame(boston.data, columns = boston.feature_names)
  df['target'] = boston.target

#データフレーム表示
df.info(verbose=True)         #データフレーム情報表示（verbose=Trueで表示数制限カット）
df.head()                     #データフレーム先頭5行表示
```

### データクリーニング

#### 不要なデータ項目の削除（項目名を指定し削除｜7割以上が欠損値の項目を削除☑）
```:Python
#@title 不要なデータ項目の削除（項目名を指定し削除｜7割以上が欠損値の項目を削除☑）
#@markdown  **<font color= "Crimson">注意</font>：Drop_label_is（カラムを指定して削除）の記載は <u> ' ID ' , ' Age '  </u> などとしてください。**</font>
Drop_label_is =  'sibsp', 'parch'#@param {type:"raw"}

try:
  if Drop_label_is is not "":
    Drop_label_is = pd.Series(Drop_label_is)
    print('-----------------------------------------------------------------------------------------')
    print("Drop of specified column:", Drop_label_is.values)
    df.drop(columns=list(Drop_label_is),axis=1,inplace=True)
  else:
    print('※削除カラムの指定なし→処理スキップ')
except:
    print("※正常に処理されませんでした。入力に誤りがないか確認してください。")

#データの7割以上が欠損値のカラムを削除（☑ ＝実行）
Over_70percent_missing_value_is_drop = True #@param {type:"boolean"}

#各列ごとに、7割欠損がある列を削除
if Over_70percent_missing_value_is_drop == True:
  for col in df.columns:
    nans = df[col].isnull().sum()  # nanになっている行数をカウント

    # nan行数を全行数で割り、7割欠損している列をDrop
    if nans / len(df) > 0.7: 
        # 7割欠損列を削除
        print('-----------------------------------------------------------------------------------------')
        print("Drop of missing 70% column:", col)
        df.drop(col, axis=1, inplace=True)    

print('-----------------------------------------------------------------------------------------')

df.head()
```

#### 欠損データを含む行を削除（☑ ＝実行）
```:Python
#@title 欠損データを含む行を削除（☑ ＝実行）
Null_Drop  = True #@param {type:"boolean"}

if Null_Drop == True:
  df = df.dropna(how='any')
df.head()
```

#### カテゴリーデータ項目を Labelエンコード（対象：Dtype が int64, float64 以外のデータ項目）
```:Python
#@title カテゴリーデータ項目を Labelエンコード（**対象：Dtype が int64, float64 以外のデータ項目**）
#@markdown  **<font color= "Crimson">注意</font>：指定は <u> ' ID ' , ' Age ' , </u> などとしてください。**
Object_label_to_encode_is = '', '', '' #@param {type:"raw"}
Object_label_to_encode_is = pd.Series(Object_label_to_encode_is)

from sklearn.preprocessing import LabelEncoder

encoders = dict()

try:
  for i in Object_label_to_encode_is:
    if Object_label_to_encode_is is not "":
      series = df[i]
      le = LabelEncoder()
      df[i] = pd.Series(
        le.fit_transform(series[series.notnull()]),
        index=series[series.notnull()].index
        )
      encoders[i] = le
      print('-----------------------------------------------------------------------------------------')
      print('[エンコードカラム]：',i)
      le_name_mapping = dict(zip(le.classes_, le.transform(le.classes_)))
      print(le_name_mapping)
    else:
      print('skip')

except:
    print("※正常に処理されなかった場合は入力に誤りがないか確認してください。")
print('-----------------------------------------------------------------------------------------') 
df.head()
```

#### すべての Obeject_col を Label encord（☑ ＝実行）
```:Python
#@title すべての Obeject_col を Label encord（☑ ＝実行）
Encord_all_object_label = True #@param {type:"boolean"}

from sklearn.preprocessing import LabelEncoder

if Encord_all_object_label == True:
  le = LabelEncoder()

  for col in df.columns:
    if df[col].dtype == 'object':
      df[col] = le.fit_transform(df[col].astype(str))
      print('-----------------------------------------------------------------------------------------') 
      print(col)
      print(le.classes_, "= [0, 1, 2...]" )
     
#    else:
#      print(col,'：エンコードしない→処理スキップ')

print('-----------------------------------------------------------------------------------------') 
df.head()
```

#### データ項目名を英訳（☑ ＝実行）
```:Python
#@title データ項目名を英訳（☑ ＝実行）
Column_English_translation = False #@param {type:"boolean"}

from googletrans import Translator

if Column_English_translation == True:

  eng_columns = {}
  columns = df.columns
  translator = Translator()
  
  for column in columns:
    eng_column = translator.translate(column).text
    eng_column = eng_column.replace(' ', '_')
    eng_columns[column] = eng_column
    df.rename(columns=eng_columns, inplace=True)

print('-----------------------------------------------------------------------------------------')
print('[カラム名_翻訳結果（翻訳しない場合も表示）]')
print('-----------------------------------------------------------------------------------------') 
df.head(0)
```

### TabPFN

#### TabPFN実行
```:Python
#@title **TabPFN実行**

N_ensemble_configurations = 4 #@param {type:"slider", min:3, max:10, step:1}

from tabpfn.scripts.transformer_prediction_interface import TabPFNClassifier
import torch

#FEATURES、TARGET、X、yを指定 
FEATURES = df.columns[:-1]    #説明変数のデータ項目を指定
TARGET = df.columns[-1]       #目的変数のデータ項目を指定
X = df.loc[:, FEATURES]       #FEATURESのすべてのデータをXに格納
y = df.loc[:, TARGET]         #TARGETのすべてのデータをyに格納

#testとtrainを分割
from sklearn.model_selection import train_test_split

if dataset_type == 'Classification':
  X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 1, stratify = y)
else:
  X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 1, test_size=0.25)

#訓練データとテストデータ準備
train_data = pd.concat([X_train, y_train], axis=1)
test_data = pd.concat([X_test, y_test], axis=1)
test_data = test_data.iloc[:,:-1]  #testデータの目的変数削除

# GPU"cuda"を指定
device = "cuda" if torch.cuda.is_available() else "cpu"

# モデルの定義
# N_ensemble_configurations の値を大きくすると精度upする（遅くなる）
classifier = TabPFNClassifier(device = device, N_ensemble_configurations=N_ensemble_configurations)
classifier.fit(X_train, y_train)
```

### モデル評価
```:Python
#@title モデル評価

#指標関連ライブラリインストール
from sklearn.metrics import r2_score   # 決定係数
from sklearn.metrics import mean_squared_error  # RMSE
from sklearn.metrics import mean_absolute_error  #MAE
from sklearn.metrics import f1_score  #F1スコア
from sklearn.metrics import confusion_matrix  #混同行列
from sklearn.metrics import classification_report  #classification report

import matplotlib.pyplot as plt

# TabPFNの予測
y_pred = classifier.predict(X_test)

#y_test_pred = pd.read_csv(filePath)

if dataset_type == 'Classification':
  print('Confusion matrix_test')
  #混同行列
  sns.set(rc = {'figure.figsize':(1.5,1.5)})
  sns.heatmap(confusion_matrix(y_test,y_pred),
              square=True, cbar=True, annot=True, cmap='Blues',fmt='g')
  plt.xlabel('Predicted', fontsize=11)
  plt.ylabel('Actual', fontsize=11)
  plt.show()
  print('-----------------------------------------------------------------------------------------')
  print('Classification report_test')
  print(classification_report(y_true=y_test, y_pred=y_pred))  

else:
  print('Regression report')
  print('-----------------------------------------------------------------------------------------')
  print('  RMSE\t test: %.2f' % (
      mean_squared_error(y_test, y_pred, squared=False)))
  print('  MAE\t test: %.2f' % (
      mean_absolute_error(y_test, y_pred)))
  print('  R²\t test: %.2f' % (
        r2_score(y_test, y_pred)    # テスト
      ))
  print('-----------------------------------------------------------------------------------------')
  plt.figure(figsize = (5,5))
  plt.title('Prediction Accuracy')
  ax = plt.subplot(111)
  ax.scatter(y_test, y_pred,alpha=0.7)
  ax.set_xlabel('y_test')
  ax.set_ylabel('y_pred')
  ax.plot(y_test,y_test,color='red',alpha =0.5)
  plt.show()
```
```:Python
#@title　testデータ予測（☑ ＝csv保存実行）

csv_output = False #@param {type:"boolean"}

#y_pred（array）をデータフレーム（Series）に,y_testのindexをリセット
y_pred_ = pd.Series(y_pred)
y_test_ = y_test.reset_index()

#testとpredをデータフレームに格納
col_name = ['index','True','Pred']
result=pd.concat([y_test_,y_pred_],axis=1)
result.columns = col_name

#csv出力
if csv_output == True:
  result.to_csv('pred_test_data.csv',encoding='utf_8_sig',index=False)
  from google.colab import files
  files.download('pred_test_data.csv')

result
```

## 関連記事

https://qiita.com/hima2b4/items/143a5c0ab5d202d490da

https://qiita.com/hima2b4/items/560a0121e9d196f8de8b

https://qiita.com/hima2b4/items/842a7a3094bf1f60f7b5

https://qiita.com/hima2b4/items/e3c48f558f2a8d9ff8bf

https://github.com/automl/TabPFN