CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: LightGBM でカテゴリ変数を扱ってみる

以前このブログで LightGBM を使ってみる記事を書いた。 ただ、この記事で使っている Iris データセットにはカテゴリ変数が含まれていなかった。

blog.amedama.jp

そこで、今回はマッシュルームデータセットを使ってカテゴリ変数が含まれる場合を試してみる。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.13.4
BuildVersion:   17E202
$ python -V
Python 3.6.5

マッシュルームデータセットについて

マッシュルームデータセットはAgaricales Agaricaceae Lepiota という品種のキノコの外見的な特徴と毒の有無を記録したもの。 具体的には、次のような特徴が全てカテゴリ変数の形で入っている。 要するに、外見的な特徴から毒の有無を判定するモデルを作りたい、ということ。

  • 毒の有無
  • 傘の形
  • 傘の表面
  • 傘の色
  • 部分的な変色の有無
  • 匂い
  • ひだの形状
  • ひだの間隔
  • ひだの大きさ
  • ひだの色
  • 柄の形
  • 柄の根本
  • 柄の表面 (つぼより上)
  • 柄の表面 (つぼより下)
  • 柄の色 (つぼより上)
  • 柄の色 (つぼより下)
  • 覆いの種類
  • 覆いの色
  • つぼの数
  • つぼの種類
  • 胞子の色
  • 群生の仕方
  • 生息地

データセットは次の Web サイトから得られる。

UCI Machine Learning Repository: Mushroom Data Set

まずは wget や curl を使ってデータセットをダウンロードしておこう。

$ wget https://archive.ics.uci.edu/ml/machine-learning-databases/mushroom/agaricus-lepiota.data

下準備

続いて、今回使う Python のパッケージをインストールしておく。

$ brew install cmake gcc@7
$ export CXX=g++-7 CC=gcc-7
$ pip install --no-binary lightgbm lightgbm pandas

インストールできたら Python のインタプリタを起動する。

$ python

データセットの CSV をパースする

続いてはダウンロードしてきた CSV を pandas の DataFrame に変換する。 ここで注目すべきは dtype として 'category' を指定しているところ。 これによってパースされた各次元がカテゴリ型として扱われる。

>>> import pandas as pd
>>> columns = [
...     'poisonous',
...     'cap-shape',
...     'cap-surface',
...     'cap-color',
...     'bruises',
...     'odor',
...     'gill-attachment',
...     'gill-spacing',
...     'gill-size',
...     'gill-color',
...     'stalk-shape',
...     'stalk-root',
...     'stalk-surface-above-ring',
...     'stalk-surface-below-ring',
...     'stalk-color-above-ring',
...     'stalk-color-below-ring',
...     'veil-type',
...     'veil-color',
...     'ring-number',
...     'ring-type',
...     'spore-print-color',
...     'population',
...     'habitat',
... ]
>>> df = pd.read_csv('agaricus-lepiota.data', header=None, names=columns, dtype='category')

上記でマッシュルームデータセットの 23 次元の特徴を持ったデータが読み込まれた。

>>> df.head()
  poisonous cap-shape cap-surface   ...   spore-print-color population habitat
0         p         x           s   ...                   k          s       u
1         e         x           s   ...                   n          n       g
2         e         b           s   ...                   n          n       m
3         p         x           y   ...                   k          s       u
4         e         x           s   ...                   n          a       g

[5 rows x 23 columns]
>>> df.dtypes
poisonous                   category
cap-shape                   category
cap-surface                 category
cap-color                   category
bruises                     category
odor                        category
gill-attachment             category
gill-spacing                category
gill-size                   category
gill-color                  category
stalk-shape                 category
stalk-root                  category
stalk-surface-above-ring    category
stalk-surface-below-ring    category
stalk-color-above-ring      category
stalk-color-below-ring      category
veil-type                   category
veil-color                  category
ring-number                 category
ring-type                   category
spore-print-color           category
population                  category
habitat                     category
dtype: object

pandas のカテゴリ型は Series#cat#categories で水準を確認できたりする。

>>> df.poisonous.cat.categories
Index(['e', 'p'], dtype='object')

整数値にラベルエンコードする

ただし、上記ではカテゴリ型の中身が 'object' になっているため、そのままでは LightGBM に食べさせることができない。 LightGBM では int, float, boolean しか扱うことができないため。 そこで scikit-learnLabelEncoder を使って対応する整数値に変換する。

>>> from sklearn import preprocessing
>>> for column in df.columns:
...     target_column = df[column]
...     le = preprocessing.LabelEncoder()
...     le.fit(target_column)
...     label_encoded_column = le.transform(target_column)
...     df[column] = pd.Series(label_encoded_column).astype('category')
... 

これで DataFrame がカテゴリ型のまま中身が整数になった。

>>> df.head()
   poisonous  cap-shape   ...     population  habitat
0          1          5   ...              3        5
1          0          5   ...              2        1
2          0          0   ...              2        3
3          1          5   ...              3        5
4          0          5   ...              0        1

[5 rows x 23 columns]

あとは普通に LightGBM で学習して汎化性能を検証するだけ。

まずはデータセットを説明変数と目的変数に分割する

>>> X, y = df.drop('poisonous', axis=1), df.poisonous

続いてデータセットを学習用、ハイパーパラメータ調整用、ホールドアウト検証用の 3 つに分割する。

>>> from sklearn.model_selection import train_test_split
>>> # データセットを学習用と検証用に分割する
... X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.7, random_state=42)
>>> # 検証用データセットをハイパーパラメータ調整用とバリデーション用に分割する
... X_eval, X_test, y_eval, y_test = train_test_split(X_val, y_val, test_size=0.5, random_state=42)

LightGBM を学習させる

続いて、分割したデータを元に LightGBM を学習させる。 学習に使う元ネタが pandas の DataFrame で、適切にカテゴリ型になっていれば LightGBM はそれを識別してくれる。

>>> import lightgbm as lgb
>>> lgb_train = lgb.Dataset(X_train, y_train)
>>> lgb_eval = lgb.Dataset(X_eval, y_eval, reference=lgb_train)
>>> # 学習用パラメータ
... lgbm_params = {
...     # 二値分類問題
...     'objective': 'binary',
...     # 評価方法
...     'metric': 'binary_error',
... }
>>> # 学習
... model = lgb.train(lgbm_params, lgb_train, valid_sets=lgb_eval)

ホールドアウト検証を用いた性能評価

学習させたモデルをホールドアウト検証用のデータセットで評価する。

>>> # バリデーション用データセットで汎化性能を確認する
... y_pred_proba = model.predict(X_test, num_iteration=model.best_iteration)
>>> import numpy as np
>>> # しきい値 0.5 で最尤なクラスに分類する
... y_pred = np.where(y_pred_proba > 0.5, 1, 0)

今回、精度で評価したところ全てのデータを正しく判別できた。 分割方法などにも依存するものの、このデータセットとモデルであればほぼ 100% に近い精度が得られるようだった。

>>> # 精度を確認する
... from sklearn.metrics import accuracy_score
>>> accuracy_score(y_test, y_pred)
1.0

また、カテゴリ型が指定されていない pandas の DataFrame や、それ以外をデータセットの元ネタにするときは、次のように categorical_feature を指定すると良い。

>>> lgb_train = lgb.Dataset(X_train, y_train, categorical_feature=list(X.columns))

いじょう。

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理