CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: LightGBM を Git のソースコードからインストールする

今回は LightGBM の Python パッケージを Git のソースコードからインストールする方法について。 まだリリースされていない最新の機能を使いたい、あるいは自分で改造したパッケージを使いたい、といった場合に。

なお、インストール方法は以下に記載されている。

github.com

使った環境は次の通り。

$ sw_vers                
ProductName:    Mac OS X
ProductVersion: 10.14.5
BuildVersion:   18F132
$ python -V
Python 3.7.3

下準備

まずはビルドに必要なパッケージをインストールしておく。

$ brew install cmake libomp

LightGBM のリポジトリをチェックアウトして python-package ディレクトリに移動しておく。

$ git clone https://github.com/microsoft/LightGBM.git
$ cd LightGBM/python-package 

インストール

公式のマニュアルを見ると、次のようにインストールすると書いてある。 ただ、これだと依存パッケージが一緒に入らない。

$ python setup.py install

なので、まずはソースコード配布物などのパッケージをまずはビルドした上で、それを使ってインストールするのが楽だと思う。

$ python setup.py sdist

これなら依存パッケージが同時に入る。

$ pip install dist/lightgbm-2.2.4.tar.gz

現時点 (2019-06-07) で未リリースのバージョンがインストールされた。 もちろん、これは Git の HEAD を使った開発版なので正式なバージョンがついているわけではない。

$ pip list | grep -i lightgbm                     
lightgbm     2.2.4  

作業ディレクトリを移動して lightgbm パッケージがインポートできることを確認する。

$ pushd /tmp && python -c "import lightgbm as lgb"

いじょう。

統計的学習の基礎 ―データマイニング・推論・予測―

統計的学習の基礎 ―データマイニング・推論・予測―

  • 作者: Trevor Hastie,Robert Tibshirani,Jerome Friedman,杉山将,井手剛,神嶌敏弘,栗田多喜夫,前田英作,井尻善久,岩田具治,金森敬文,兼村厚範,烏山昌幸,河原吉伸,木村昭悟,小西嘉典,酒井智弥,鈴木大慈,竹内一郎,玉木徹,出口大輔,冨岡亮太,波部斉,前田新一,持橋大地,山田誠
  • 出版社/メーカー: 共立出版
  • 発売日: 2014/06/25
  • メディア: 単行本
  • この商品を含むブログ (6件) を見る

Python: LightGBM の学習曲線をコールバックで動的にプロットする

LightGBM の学習が進む様子は、学習させるときにオプションとして verbose_eval などを指定することでコンソールから確認できる。 ただ、もっと視覚的にリアルタイムで確認したいなーと思ったので、今回はコールバックと Matplotlib を使って学習曲線を動的にグラフとしてプロットしてみることにした。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.5
BuildVersion:   18F132
$ python -V            
Python 3.7.3

下準備

下準備として LightGBM と Matplotlib をインストールしておく。 Seaborn は本来は必要ないんだけどデータセットの読み込みにだけ使っている。

$ pip install lightgbm matplotlib seaborn

学習曲線を動的にプロットする

今回書いてみたサンプルコードは次の通り。 Seaborn から Titanic データセットを読み込んで LightGBM のモデルが学習していく過程を可視化している。 グラフのプロットは LearningVisualizationCallback というコールバックを実装することで実現している。 そのままだとグラフが寂しいので、カスタムメトリックとして Accuracy も追加してみた。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from collections import defaultdict

import numpy as np
import lightgbm as lgb
import seaborn as sns
from sklearn.model_selection import StratifiedKFold
from matplotlib import pyplot as plt


class LearningVisualizationCallback(object):
    """学習の過程を動的にプロットするコールバック"""

    def __init__(self, fig=None, ax=None):
        self._metric_histories = defaultdict(list)
        self._metric_history_lines = {}
        self._metric_type_higher_better = {}
        self._best_score_lines = {}
        self._best_score_texts = {}

        # 初期化する
        self._fig = fig
        self._ax = ax
        if self._fig is None and self._ax is None:
            self._fig, self._ax = plt.subplots()
        self._ax.set_title('learning curve')
        self._ax.set_xlabel('round')
        self._ax.set_ylabel('score')
        self._fig.canvas.draw()
        self._fig.show()

    def __call__(self, env):
        # メトリックを保存する
        evals = env.evaluation_result_list
        for _, name, mean, is_higher_better, _ in evals:
            self._metric_histories[name].append(mean)

            # 初回だけの設定
            if env.iteration == 0:
                # メトリックの種別を保存する
                self._metric_type_higher_better[name] = is_higher_better
                # スコアの履歴を描画するオブジェクトを生成する
                history_line, = self._ax.plot([], [])
                history_line.set_label(name)
                self._metric_history_lines[name] = history_line
                # ベストスコアの線を描画するオブジェクトを生成する
                best_line = self._ax.axhline(0)
                best_line.set_color(history_line.get_color())
                best_line.set_linestyle(':')
                self._best_score_lines[name] = best_line
                # ベストスコアの文字列を描画するオブジェクトを生成する
                best_text = self._ax.text(0, 0, '', weight='bold')
                best_text.set_color(history_line.get_color())
                self._best_score_texts[name] = best_text

        # 可視化する
        for name, values in self._metric_histories.items():
            # グラフデータを更新する
            history_line = self._metric_history_lines[name]
            history_line.set_data(np.arange(len(values)), values)
            best_line = self._best_score_lines[name]
            best_find_func = np.max if self._metric_type_higher_better[name] else np.min
            best_score = best_find_func(values)
            best_line.set_ydata(best_score)
            best_text = self._best_score_texts[name]
            best_text.set_text('{:.6f}'.format(best_score))
            best_text.set_y(best_score)

        # グラフの見栄えを調整する
        self._ax.legend()
        self._ax.relim()
        self._ax.autoscale_view()

        # 再描画する
        plt.pause(0.001)

    def show_until_close(self):
        """ウィンドウを閉じるまで表示し続ける"""
        plt.show()


def accuracy(preds, data):
    """精度 (Accuracy) を計算する関数
    NOTE: 表示が eval set の LogLoss だけだと寂しいので"""
    y_true = data.get_label()
    y_pred = np.where(preds > 0.5, 1, 0)
    acc = np.mean(y_true == y_pred)
    return 'accuracy', acc, True


def main():
    # Titanic データセットを読み込む
    dataset = sns.load_dataset('titanic')

    # 重複など不要な特徴量は落とす
    X = dataset.drop(['survived',
                      'class',
                      'who',
                      'embark_town',
                      'alive'], axis=1)
    y = dataset.survived

    # カテゴリカル変数を指定する
    categorical_columns = ['pclass',
                           'sex',
                           'embarked',
                           'adult_male',
                           'deck',
                           'alone']
    X = X.astype({c: 'category'
                  for c in categorical_columns})

    # LightGBM のデータセット表現に直す
    lgb_train = lgb.Dataset(X, y)

    # 学習の過程を可視化するコールバックを用意する
    visualize_cb = LearningVisualizationCallback()
    callbacks = [
        visualize_cb,
    ]

    # 二値分類を LogLoss で評価する
    lgb_params = {
        'objective': 'binary',
        'metrics': 'binary_logloss',
    }
    # 5-Fold CV
    skf = StratifiedKFold(n_splits=5,
                          shuffle=True,
                          random_state=42)
    lgb.cv(lgb_params, lgb_train,
           num_boost_round=1000,
           early_stopping_rounds=100,
           verbose_eval=10,
           folds=skf, seed=42,
           # Accuracy も確認する
           feval=accuracy,
           # コールバックを登録する
           callbacks=callbacks)

    # ウィンドウを閉じるまで表示し続ける
    visualize_cb.show_until_close()


if __name__ == '__main__':
    main()

上記に適当な名前をつけて実行してみよう。

$ python lgblearnviz.py

すると、モデルの学習に伴って次のようなアニメーションが表示される。

f:id:momijiame:20190903233754g:plain

いいかんじ。

なお、表示されているのは Validation Set に対するメトリックとなる。 Training Set も確認したかったんだけど、どうやら次のリリース (2.2.4?) でオプションに eval_train_metric が入るのを待つ必要がありそう。

あと、Jupyter Notebook で使うときは %matplotlib notebook マジックコマンドを使うと良い。

Python: google-api-python-client とサービスアカウントで Google Docs のファイルをダウンロードする

今回は Google Cloud Platform のサービスアカウントと google-api-python-client を使って Google Docs のファイルをダウンロードしてみる。 サービスアカウントというのは、人間ではなくアプリケーションなどのシステムが使うアカウントのこと。 例えば CI などの環境で Google Docs にあるファイルを操作するのに使えるかな。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.14.5
BuildVersion:   18F132
$ python -V
Python 3.7.3

Google Cloud Platform にプロジェクトを作成する

まずは Google Cloud Platform にプロジェクトを用意する。 なお、プロジェクトの ID は Google Cloud Platform 上で一意な必要がある点に注意する。

プロジェクトは Google Cloud Console から作っても良いけど、今回は Google Cloud SDK の CLI から作る。 そこで、まずは Google Cloud SDK を Homebrew Cask でインストールしておく。

$ brew cask install google-cloud-sdk

インストールしたら gcloud コマンドが使えるようになるので、早速プロジェクトを作成する。

$ gcloud projects create <project-id>

今回は example-gdrive-service-account という名前のプロジェクトを使った。

$ gcloud projects create example-gdrive-service-account

プロジェクトを作ったら、以下のようにしてデフォルトのプロジェクトに設定しておくと良い。

$ gcloud config set project example-gdrive-service-account

上記の操作が終わったら、Google Cloud Console で Google Drive API を有効化しておく。 Google Docs のデータは Google Drive に保存されているため。 また、この作業だけは gcloud コマンドから実行できない。

プロジェクトにサービスアカウントを作成する

続いてプロジェクトにサービスアカウントを追加する。

プロジェクトにサービスアカウントを追加するには、以下のように gcloud iam service-accounts create コマンドを使う。

$ gcloud iam service-accounts create <sa-id> \
    --display-name <sa-name>

今回は、次のように gdrive-access-account という名前のアカウントを作った。

$ gcloud iam service-accounts create gdrive-access-account \
    --display-name gdrive-access-account

サービスアカウントに紐付いた認証情報をダウンロードする

サービスアカウントができたら、次はサービスアカウントに紐付いた認証情報をダウンロードする。 この認証情報があればサービスアカウントのできる操作は全てできてしまうので注意して管理しよう。

認証情報は次のように gcloud iam service-accounts keys create コマンドでダウンロードできる。 対象となるサービスアカウントは --iam-account というオプションで指定する。 このときメールアドレスのような形式でサービスアカウントを特定する。

$ gcloud iam service-accounts keys create <key-file> \
      --iam-account <sa-name>@<project-id>.iam.gserviceaccount.com

例えば今回であれば次のようになる。

$ gcloud iam service-accounts keys create ./key.json \
      --iam-account gdrive-access-account@example-gdrive-service-account.iam.gserviceaccount.com

実行すると、次のように認証情報がダウンロードされる。 デフォルトでは JSON 形式となる。

$ file key.json
key.json: ASCII text, with very long lines

操作したい Google Docs のファイルまたはディレクトリをサービスアカウントに共有する

続いて Google Drive をブラウザで開いてサービスアカウントから操作したいファイルまたはディレクトリに移動する。 そして、メニューから「ファイル > 共有」を選択する。 ここで、招待する相手としてサービスアカウントのメールアドレスを入力する。

または、公開しても問題ないファイルであれば共有メニューの「詳細設定」からアクセスできるユーザを変更する。 具体的には「ウェブ上で一般公開」または「リンクを知っている全員」に指定すれば良い。 ただ、この場合はクレデンシャルなしでアクセスできるので、別にサービスアカウントを作る必要はないはず。

サービスアカウントの認証情報と google-api-python-client を使ってファイルにアクセスする

ここまでで下準備は完了した。 続いては Python のクライアントからファイルを操作する作業に入る。

まずは必要なパッケージをインストールする。

$ pip install google-api-python-client

Python のインタプリタを起動しよう。

$ python

まずは、先ほどダウンロードしてきた認証情報を元に Credentials のインスタンスを作る。

>>> from google.oauth2.service_account import Credentials
>>> credential_file = 'key.json'
>>> c = Credentials.from_service_account_file(credential_file)

続いて、認証情報を元に Google Drive にアクセスするためのオブジェクトを作る。

>>> from googleapiclient import discovery
>>> service = discovery.build('drive', 'v3', credentials=c)

続いてファイル ID を指定して実際にファイルをダウンロード (エクスポート) する。 ファイル ID はファイルの共有メニューにある「共有可能なリンクを取得」から確認できる。 例えば Google 図形描画であれば以下の `<file-id> の部分がそれに当たる。

https://docs.google.com/drawings/d/<file-id>/edit

今回ダウンロードしたかったのは Google 図形描画の PNG ファイルなので mimeType オプションに image/png を指定した。

>>> file_id = '<file-id-to-download>'
>>> request = service.files().export(fileId=file_id, mimeType='image/png')
>>> image_data = request.execute()

ファイルのバイト列が取得できたら、あとはそれを使うだけ。 例えばローカルのディスクに書き出すのであれば次のようにする。

>>> with open('drive.png', mode='wb') as f:
...     f.write(image_data)
... 

いじょう。

Ubuntu 18.04 LTS に OpenCL (NVIDIA CUDA Runtime) をインストールする

OpenCL は、CPU や GPU など様々なプラットフォームを抽象化して並列計算に用いるためのフレームワーク。 今回は Ubuntu 18.04 LTS + NVIDIA Tesla T4 の環境に OpenCL の NVIDIA CUDA ランタイムをインストールしてみる。

使った環境は次の通り。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.2 LTS"
$ uname -r
4.15.0-1033-gcp
$ nvidia-smi
Tue Jun  4 13:22:36 2019       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 418.67       Driver Version: 418.67       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla T4            On   | 00000000:00:04.0 Off |                    0 |
| N/A   58C    P8    10W /  70W |      0MiB / 15079MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

下準備

利用できる OpenCL のランタイムを確認するために clinfo パッケージをインストールしておく。

$ sudo apt-get update
$ sudo apt-get -y install clinfo

最初の状態では、次の通り利用できるランタイムが存在しない。

$ clinfo
Number of platforms                               0

NVIDIA CUDA をインストールする

実は OpenCL は NVIDIA CUDA をインストールすると自動的に使えるようになる。 なので NVIDIA CUDA をインストールしていく。

NVIDIA CUDA のインストール用 deb ファイルは以下から取得できる。

developer.nvidia.com

ちなみに、使う GPU のアーキテクチャ (Compute Capability) によってサポートされている CUDA のバージョンが異なる点に注意しよう。

Web サイトからファイルをダウンロードする。

$ sudo apt-get -y install wget
$ wget http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-repo-ubuntu1804_10.1.168-1_amd64.deb

システムに CUDA のリポジトリを登録する。

$ sudo dpkg -i cuda-repo-ubuntu1804_10.1.168-1_amd64.deb
$ sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/7fa2af80.pub
$ sudo apt-get update

CUDA をインストールする。

$ sudo apt-get -y install cuda

インストールが終わったらマシンを再起動する。

$ sudo shutdown -r now

マシンが再起動してきたら、nvidia-smi コマンドで GPU の状況が確認できるはず。

$ nvidia-smi
Tue Jun  4 13:20:22 2019       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 418.67       Driver Version: 418.67       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla T4            On   | 00000000:00:04.0 Off |                    0 |
| N/A   76C    P0    32W /  70W |      0MiB / 15079MiB |      6%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

また、clinfo コマンドを実行すると次のようにプラットフォームが認識されるはず。 現時点での OpenCL の最新バージョンは 2.1 なので、それに比べると 1.2 はちょっと古い。

$ clinfo | grep "Number of platforms" -A 3
Number of platforms                               1
  Platform Name                                   NVIDIA CUDA
  Platform Vendor                                 NVIDIA Corporation
  Platform Version                                OpenCL 1.2 CUDA 10.1.152

とはいえ、これで OpenCL を使った並列計算が NVIDIA の GPU を使って実行できる。

いじょう。

Ubuntu 16.04 LTS に OpenCL (Intel CPU Runtime) をインストールする

OpenCL は、CPU や GPU など様々なプラットフォームを抽象化して並列計算に用いるためのフレームワーク。 今回は Ubuntu 16.04 LTS の環境に Intel の CPU ランタイムをインストールしてみる。

使った環境は次の通り。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.6 LTS"
$ uname -r
4.4.0-148-generic

下準備

利用できる OpenCL のランタイムを確認するために clinfo パッケージをインストールしておく。

$ sudo apt-get -y install clinfo

初期の状態では何も利用できない。

$ clinfo
Number of platforms                               0

Intel CPU Runtime をインストールする

最初に、インストールに必要なパッケージをインストールしておく。

$ sudo apt-get -y install lsb-core

Intel CPU Runtime は以下の Web サイトからダウンロードする。

software.intel.com

上記からはいくつかのプラットフォーム向けのランタイムがダウンロードできる。 その中でも Intel® Xeon® Processor or Intel® Core™ Processor を選択する。

ダウンロードしたファイルを解凍する。

$ tar xvf l_opencl_p_18.1.0.015.tgz
$ cd l_opencl_p_18.1.0.015/

インストールスクリプトを実行する。

$ sudo sh install.sh

次のような手順で選択すればインストールできる。

5 (Installation) -> q (pager) -> accept -> 2 (I do NOT consent to the collection of my Information) -> 1 (Accept configuration and begin installation [ default ])

インストールが完了したら、先ほどと同じように clinfo コマンドを実行する。 次のように CPU Runtime が表示されれば上手くいってる。

$ clinfo | grep -A 3 "Number of platforms"
Number of platforms                               1
  Platform Name                                   Intel(R) CPU Runtime for OpenCL(TM) Applications
  Platform Vendor                                 Intel(R) Corporation
  Platform Version                                OpenCL 2.1 LINUX

いじょう。

Python: scikit-learn と色々な TF-IDF の定義について

自然言語処理にあまり馴染みがないのもあって、試しに TF-IDF (Term Frequency - Inverse Document Frequency) を自分で実装してみることにした。 その過程で知ったことについて書き残しておく。 端的に書いてしまうと、TF-IDF の定義は色々とある、ということ。

TF-IDF というのは、コーパス (全文書) に含まれる単語の重要度を評価するための手法。 その名の通り、文書単位で見た単語の頻度 (Term Frequency) と、コーパス単位で見た単語の頻度 (Inverse Document Frequency) を元に計算する。 これは例えば、ある文書の中で頻出する単語 (Term Frequency が高い) があれば、その単語はその文書の中では重要と考えられる。 しかし、その単語が別の文書でも同様によく登場する (Inverse Document Frequency が低い) のであれば、全体から見るとありふれた単語なので実は大して重要ではなくなる。

今回、TF-IDF を実装するにあたって scikit-learn の出力結果をお手本にすることにした。 しかし、やってみるとなかなか結果が一致しない。 原因を探るべくドキュメントやソースコードやチケットシステムを確認していくと、色々と事情が分かってきた。

動作を確認するのに使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.5
BuildVersion:   18F132
$ python -V                      
Python 3.7.3

下準備

まずは scikit-learn と pandas をインストールしておく。

$ pip install scikit-learn pandas

scikit-learn の TF-IDF を試す

まずはお手本となる scikit-learn の動作を確認しておこう。 scikit-learn の TF-IDF の実装は TfidfVectorizer にある。 コーパスは scikit-learn の例をそのまま使うことにした。 TF-IDF を計算する上で本来は必要となるはずの BoW (Bag of Words) については別途 CountVectorizer で集計している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pandas as pd

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer


def main():
    # データフレームを表示するときカラムを省略しない
    pd.set_option('display.max_columns', None)
    # 浮動小数点を表示するときは小数点以下 2 桁で揃える
    pd.options.display.float_format = '{:0.2f}'.format

    # 取り扱うコーパス
    corpus = [
        'This is the first document.',
        'This document is the second document.',
        'And this is the third one.',
        'Is this the first document?',
    ]

    # 単語の数をカウントする
    count_vectorizer = CountVectorizer()
    X_count = count_vectorizer.fit_transform(corpus)

    # 見やすさのために表示するときは pandas のデータフレームにする
    df = pd.DataFrame(data=X_count.toarray(),
                      columns=count_vectorizer.get_feature_names())
    print('--- BoW (Bag of Words) ---')
    print(df)

    # scikit-learn の TF-IDF 実装
    tfidf_vectorizer = TfidfVectorizer()
    X_tfidf = tfidf_vectorizer.fit_transform(corpus)

    # IDF を表示する
    print('--- IDF (Inverse Document Frequency) ---')
    df = pd.DataFrame(data=[tfidf_vectorizer.idf_],
                      columns=tfidf_vectorizer.get_feature_names())
    print(df)

    # TF-IDF を表示する
    print('--- TF-IDF ---')
    df = pd.DataFrame(data=X_tfidf.toarray(),
                      columns=tfidf_vectorizer.get_feature_names())
    print(df)


if __name__ == '__main__':
    main()

実行結果は次の通り。 BoW (Bag of Words) は各文書に含まれる単語のカウント、IDF (Inverse Document Frequency) はコーパスにおける単語の珍しさを示す。 例えば and は文書 2 にしか含まれていないので珍しいけど、is は全ての文書に含まれているので珍しくない。 それが IDF の 1.921.00 という数値にあらわれている。 最終的に and は文書 2 で 0.51 という重要度で評価されており、is は文書によってバラつきがあるものの 0.27 ~ 0.38 と評価されている。 TF (Term Frequency) については TfidfVectorizer に取得するインターフェースが見当たらなかった。

$ python sktfidf.py 
--- BoW (Bag of Words) ---
   and  document  first  is  one  second  the  third  this
0    0         1      1   1    0       0    1      0     1
1    0         2      0   1    0       1    1      0     1
2    1         0      0   1    1       0    1      1     1
3    0         1      1   1    0       0    1      0     1
--- IDF (Inverse Document Frequency) ---
   and  document  first   is  one  second  the  third  this
0 1.92      1.22   1.51 1.00 1.92    1.92 1.00   1.92  1.00
--- TF-IDF ---
   and  document  first   is  one  second  the  third  this
0 0.00      0.47   0.58 0.38 0.00    0.00 0.38   0.00  0.38
1 0.00      0.69   0.00 0.28 0.00    0.54 0.28   0.00  0.28
2 0.51      0.00   0.00 0.27 0.51    0.00 0.27   0.51  0.27
3 0.00      0.47   0.58 0.38 0.00    0.00 0.38   0.00  0.38

なお、一般的には andis といったどんな文書でも頻出する類の単語はストップワード (Stop Words) といって処理の対象外とする場合が多い。

自分で実装してみる

続いては TF-IDF を自分で実装してみよう。 TF-IDF の定義については、検索して上位にくるもの (日本語版 Wikipedia など) を用いた。 具体的には次の通り。

 TF(t_i, d_j) = \frac{文書 d_j に登場する単語 t_i の数}{文書 d_j に登場する全ての単語の数}

 IDF(t_i) = \log{\frac{全文書の数}{単語 t_i が含まれる文書の数}}

 TFIDF(t_i, d_j) = TF(t_i, d_j) \cdot IDF(t_i)

そして、上記の定義にもとづいたサンプルコードが次の通り。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
import pandas as pd

from sklearn.feature_extraction.text import CountVectorizer


def main():
    pd.set_option('display.max_columns', None)
    pd.options.display.float_format = '{:0.2f}'.format

    corpus = [
        'This is the first document.',
        'This document is the second document.',
        'And this is the third one.',
        'Is this the first document?',
    ]

    count_vectorizer = CountVectorizer()
    bow = count_vectorizer.fit_transform(corpus).toarray()
    print('--- BoW (Bag of Words) ---')
    df = pd.DataFrame(bow,
                      columns=count_vectorizer.get_feature_names())
    print(df)

    # TF を計算してるところ (行方向の処理)
    print('--- TF (Term Frequency) ---')
    # 文書に含まれる単語の数をカウントする
    number_of_words = np.sum(bow, axis=1, keepdims=True)
    # 文書の中での単語の頻度を計算する
    tf = bow / number_of_words
    df = pd.DataFrame(tf,
                      columns=count_vectorizer.get_feature_names())
    print(df)

    # IDF を計算してるところ (列方向の処理)
    print('--- IDF (Inverse Document Frequency) ---')
    # 文書の数をカウントする
    number_of_docs = len(corpus)
    # その単語が一つでも含まれる文書の数をカウントする
    number_of_docs_contain_word = np.count_nonzero(bow, axis=0)
    # 単語の珍しさを計算する
    idf = np.log(number_of_docs / number_of_docs_contain_word)
    df = pd.DataFrame([idf],
                      columns=count_vectorizer.get_feature_names())
    print(df)

    # TF-IDF を計算してるところ
    print('--- TF-IDF ---')
    # TF と IDF をかける
    tfidf = tf * idf
    df = pd.DataFrame(tfidf,
                      columns=count_vectorizer.get_feature_names())
    print(df)


if __name__ == '__main__':
    main()

上記を実行してみよう。 先ほどと見比べると分かる通り、IDF と TF-IDF の値が一致していない。 なお、TF については取得するインターフェースがなかったので比較できない。

$ python mytfidf.py 
--- BoW (Bag of Words) ---
   and  document  first  is  one  second  the  third  this
0    0         1      1   1    0       0    1      0     1
1    0         2      0   1    0       1    1      0     1
2    1         0      0   1    1       0    1      1     1
3    0         1      1   1    0       0    1      0     1
--- TF (Term Frequency) ---
   and  document  first   is  one  second  the  third  this
0 0.00      0.20   0.20 0.20 0.00    0.00 0.20   0.00  0.20
1 0.00      0.33   0.00 0.17 0.00    0.17 0.17   0.00  0.17
2 0.17      0.00   0.00 0.17 0.17    0.00 0.17   0.17  0.17
3 0.00      0.20   0.20 0.20 0.00    0.00 0.20   0.00  0.20
--- IDF (Inverse Document Frequency) ---
   and  document  first   is  one  second  the  third  this
0 1.39      0.29   0.69 0.00 1.39    1.39 0.00   1.39  0.00
--- TF-IDF ---
   and  document  first   is  one  second  the  third  this
0 0.00      0.06   0.14 0.00 0.00    0.00 0.00   0.00  0.00
1 0.00      0.10   0.00 0.00 0.00    0.23 0.00   0.00  0.00
2 0.23      0.00   0.00 0.00 0.23    0.00 0.00   0.23  0.00
3 0.00      0.06   0.14 0.00 0.00    0.00 0.00   0.00  0.00

なんとか結果を一致させてみる

さて、scikit-learn の実装と Wikipedia などを参考にした実装の結果が一致しないことが分かった。 一体何がまずかったのだろうか。 実は、scikit-learn のデフォルトの TF-IDF と結果を一致させるには定義を次のように変更する必要がある。

 TF(t_i, d_j) = \frac{文書 d_j に登場する単語 t_i の数}{文書 d_j に登場する全ての単語の数}

 IDF(t_i) = \log{\frac{全文書の数 + 1}{単語 t_i が含まれる文書の数 + 1}} + 1

 TFIDF(t_i, d_j) = || TF(t_i, d_j) \cdot IDF(t_i) ||_2

先ほどからの変更点は二つある。 まず、一つ目は IDF を計算するときに分子、分母、そして最終的な結果に 1 を加えている。 そして二つ目は TF-IDF を計算するときに結果を L2 で正規化している。

上記にもとづいて変更を加えたサンプルコードが次の通り。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
import pandas as pd

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.preprocessing import normalize


def main():
    pd.set_option('display.max_columns', None)
    pd.options.display.float_format = '{:0.2f}'.format

    corpus = [
        'This is the first document.',
        'This document is the second document.',
        'And this is the third one.',
        'Is this the first document?',
    ]

    count_vectorizer = CountVectorizer()
    bow = count_vectorizer.fit_transform(corpus).toarray()
    print('--- BoW (Bag of Words) ---')
    df = pd.DataFrame(bow,
                      columns=count_vectorizer.get_feature_names())
    print(df)

    print('--- TF (Term Frequency) ---')
    number_of_words = np.sum(bow, axis=1, keepdims=True)
    tf = bow / number_of_words
    df = pd.DataFrame(tf,
                      columns=count_vectorizer.get_feature_names())
    print(df)

    print('--- IDF (Inverse Document Frequency) ---')
    # 文書の数に 1 を足す
    number_of_docs = len(corpus) + 1
    # 単語が含まれる文書の数にも 1 を足す
    number_of_docs_contain_word = np.count_nonzero(bow, axis=0) + 1
    # 最終的な IDF にも 1 を足す
    idf = np.log(number_of_docs / number_of_docs_contain_word) + 1
    df = pd.DataFrame([idf],
                      columns=count_vectorizer.get_feature_names())
    print(df)

    print('--- TF-IDF ---')
    # 結果を L2 で正規化する
    tfidf = normalize(tf * idf)
    df = pd.DataFrame(tfidf,
                      columns=count_vectorizer.get_feature_names())
    print(df)


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python compatfidf.py 
--- BoW (Bag of Words) ---
   and  document  first  is  one  second  the  third  this
0    0         1      1   1    0       0    1      0     1
1    0         2      0   1    0       1    1      0     1
2    1         0      0   1    1       0    1      1     1
3    0         1      1   1    0       0    1      0     1
--- TF (Term Frequency) ---
   and  document  first   is  one  second  the  third  this
0 0.00      0.20   0.20 0.20 0.00    0.00 0.20   0.00  0.20
1 0.00      0.33   0.00 0.17 0.00    0.17 0.17   0.00  0.17
2 0.17      0.00   0.00 0.17 0.17    0.00 0.17   0.17  0.17
3 0.00      0.20   0.20 0.20 0.00    0.00 0.20   0.00  0.20
--- IDF (Inverse Document Frequency) ---
   and  document  first   is  one  second  the  third  this
0 1.92      1.22   1.51 1.00 1.92    1.92 1.00   1.92  1.00
--- TF-IDF ---
   and  document  first   is  one  second  the  third  this
0 0.00      0.47   0.58 0.38 0.00    0.00 0.38   0.00  0.38
1 0.00      0.69   0.00 0.28 0.00    0.54 0.28   0.00  0.28
2 0.51      0.00   0.00 0.27 0.51    0.00 0.27   0.51  0.27
3 0.00      0.47   0.58 0.38 0.00    0.00 0.38   0.00  0.38

今度はちゃんと IDF と TF-IDF が scikit-learn の出力した内容と揃っている。

色々な TF-IDF の定義がある

さて、ここまで見てきた通り同じコーパスなのに scikit-learn の実装と Wikipedia の定義で、二つの異なる結果が得られた。 どちらが正しい TF-IDF なんだろう、という疑問が湧くかもしれないけど、実はどちらも間違ってはいない。 というのも TF-IDF には色々な定義があるため。 この点は、以下のブログや英語版の Wikipedia の TF-IDF のページで詳しく解説されている。

yukinoi.hatenablog.com

en.wikipedia.org

どの TF-IDF の定義も、コーパスに含まれる単語の重要度を評価するという目的自体は変わらない。 ただ、その目的に至るまでの道のりが違っている。 ちなみに scikit-learn の TF-IDF の定義については、そのものずばりなものはおそらく上記にも載っていない。

たとえば IDF の定義は、全ての単語が含まれる仮想的な文書があるようにするところが IDF Smooth に近い。 これはゼロ除算が発生するのを防ぐのに有効な手法だと思う。 ただし、対数の外側でも 1 を加えるところは scikit-learn のオリジナルだと思う。

 IDF(t_i) = \log{\frac{全文書の数 + 1}{単語 t_i が含まれる文書の数 + 1}} + 1

この件はチケットも切られているんだけど、過去の経緯でそうなってるみたいで後方互換性を考えて直さないことになったっぽい。

github.com

最終的な結果を L2 で正規化するところも、ドキュメントを読むと分かる。

 TFIDF(t_i, d_j) = || TF(t_i, d_j) \cdot IDF(t_i) ||_2

L2 正規化と Smooth をしない

実は先ほどの Smooth と L2 正規化は TfidfVectorizer のオプションで無効にできる。

無効にしたサンプルコードは次の通り。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pandas as pd

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer


def main():
    pd.set_option('display.max_columns', None)
    pd.options.display.float_format = '{:0.2f}'.format

    corpus = [
        'This is the first document.',
        'This document is the second document.',
        'And this is the third one.',
        'Is this the first document?',
    ]

    count_vectorizer = CountVectorizer()
    X_count = count_vectorizer.fit_transform(corpus)

    df = pd.DataFrame(data=X_count.toarray(),
                      columns=count_vectorizer.get_feature_names())
    print('--- BoW (Bag of Words) ---')
    print(df)

    # 正規化しないし Smooth もしない
    tfidf_vectorizer = TfidfVectorizer(norm=None,
                                       smooth_idf=False)
    X_tfidf = tfidf_vectorizer.fit_transform(corpus)

    print('--- IDF (Inverse Document Frequency) ---')
    df = pd.DataFrame(data=[tfidf_vectorizer.idf_],
                      columns=tfidf_vectorizer.get_feature_names())
    print(df)

    print('--- TF-IDF ---')
    df = pd.DataFrame(data=X_tfidf.toarray(),
                      columns=tfidf_vectorizer.get_feature_names())
    print(df)


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python nol2smooth.py 
--- BoW (Bag of Words) ---
   and  document  first  is  one  second  the  third  this
0    0         1      1   1    0       0    1      0     1
1    0         2      0   1    0       1    1      0     1
2    1         0      0   1    1       0    1      1     1
3    0         1      1   1    0       0    1      0     1
--- IDF (Inverse Document Frequency) ---
   and  document  first   is  one  second  the  third  this
0 2.39      1.29   1.69 1.00 2.39    2.39 1.00   2.39  1.00
--- TF-IDF ---
   and  document  first   is  one  second  the  third  this
0 0.00      1.29   1.69 1.00 0.00    0.00 1.00   0.00  1.00
1 0.00      2.58   0.00 1.00 0.00    2.39 1.00   0.00  1.00
2 2.39      0.00   0.00 1.00 2.39    0.00 1.00   2.39  1.00
3 0.00      1.29   1.69 1.00 0.00    0.00 1.00   0.00  1.00

結果が先ほどと変わっていることが分かる。

L2 正規化と Smooth をしないパターンを自前で実装する

L2 正規化と Smooth をしないパターンについても、結果が揃う形で実装してみよう。

サンプルコードは次の通り。 IDF の分子と分母に 1 を加えないことで、全ての単語が含まれる仮想的な文書を想定していない。 また、TF-IDF を計算するところで TF として BoW をそのまま使っている。 これは TF の中でも Raw Frequency と呼ばれる定義のようだ。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
import pandas as pd

from sklearn.feature_extraction.text import CountVectorizer


def main():
    pd.set_option('display.max_columns', None)
    pd.options.display.float_format = '{:0.2f}'.format

    corpus = [
        'This is the first document.',
        'This document is the second document.',
        'And this is the third one.',
        'Is this the first document?',
    ]

    count_vectorizer = CountVectorizer()
    bow = count_vectorizer.fit_transform(corpus).toarray()
    print('--- BoW (Bag of Words) ---')
    df = pd.DataFrame(bow,
                      columns=count_vectorizer.get_feature_names())
    print(df)

    print('--- IDF (Inverse Document Frequency) ---')
    # 全ての単語が含まれる仮想的な文書を加えない
    number_of_docs = len(corpus)
    number_of_docs_contain_word = np.count_nonzero(bow, axis=0)
    idf = np.log(number_of_docs / number_of_docs_contain_word) + 1
    df = pd.DataFrame([idf],
                      columns=count_vectorizer.get_feature_names())
    print(df)

    print('--- TF-IDF ---')
    # BoW をそのまま使う (TF = Raw Frequency)
    tfidf = bow * idf
    df = pd.DataFrame(tfidf,
                      columns=count_vectorizer.get_feature_names())
    print(df)


if __name__ == '__main__':
    main()

上記を実行してみる。

$ python mynol2smooth.py 
--- BoW (Bag of Words) ---
   and  document  first  is  one  second  the  third  this
0    0         1      1   1    0       0    1      0     1
1    0         2      0   1    0       1    1      0     1
2    1         0      0   1    1       0    1      1     1
3    0         1      1   1    0       0    1      0     1
--- IDF (Inverse Document Frequency) ---
   and  document  first   is  one  second  the  third  this
0 2.39      1.29   1.69 1.00 2.39    2.39 1.00   2.39  1.00
--- TF-IDF ---
   and  document  first   is  one  second  the  third  this
0 0.00      1.29   1.69 1.00 0.00    0.00 1.00   0.00  1.00
1 0.00      2.58   0.00 1.00 0.00    2.39 1.00   0.00  1.00
2 2.39      0.00   0.00 1.00 2.39    0.00 1.00   2.39  1.00
3 0.00      1.29   1.69 1.00 0.00    0.00 1.00   0.00  1.00

ちゃんと結果が揃っている。

まとめ

軽い気持ちで TF-IDF を実装してみようと思ったら、思ったより面白かった。 TF-IDF は定義が色々とあるので、実装によって返す値が全然違う可能性がある。 scikit-learn の実装は過去の経緯も一部あるけど、デフォルトで便利なように工夫されていると感じた。

いじょう。

Python: scikit-learn の Dummy{Classifier,Regressor} を試してみる

つい最近 scikit-learn に DummyClassifier と DummyRegressor という実装があることを知ったので試してみた。 これらの実装は、説明変数の内容は使わず、主に目的変数の内容を代わりに使って、その名の通りダミーの結果を返す。 特定のデータセットと評価指標を使ったときの、最低ラインの性能を確認するのに便利そう。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.14.5
BuildVersion:   18F132
$ python -V
Python 3.7.3

下準備

まずは scikit-learn をインストールしておく。

$ pip install scikit-learn

DummyClassifier

DummyClassifier は、その名の通りダミーの分類器となる。 使い方は一般的な scikit-learn の分類器と何ら違いはない。 違いがあるとすれば与えた教師データに含まれる説明変数を学習しないという点。

教師データの目的変数の確率分布を再現する

動作を確認するためのサンプルコードとして以下を用意した。 デフォルトでは、教師データに含まれる目的変数の確率分布を再現するように動作する。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
from sklearn.dummy import DummyClassifier


def main():
    # ダミーの分類器 (デフォルトの strategy は 'stratified')
    clf = DummyClassifier()

    # 教師データの説明変数 (無視される)
    X_train = np.arange(6)
    # 教師データの目的変数
    y_train = np.array([0, 1, 2, 2, 1, 2])

    # モデルを教師データで学習する
    clf.fit(X_train, y_train)

    # 検証データの説明変数を元に推論する
    X_test = np.arange(6, 12)
    y_pred = clf.predict(X_test)
    y_pred_proba = clf.predict_proba(X_test)

    # 推論結果 (教師データのクラスの確率分布を元にランダム)
    print('Prediction:', y_pred)
    print('Prediction (probability):')
    print(y_pred_proba)

    # 各クラスの頻度
    print('Class probabilities:', clf.class_prior_)


if __name__ == '__main__':
    main()

上記を実行してみよう。 教師データとして与えた目的変数は 0 が 1 件、1 が 2 件、2 が 3 件だった。 得られる結果は毎回異なるものの、今回は 0 が 1 件、1 が 1 件、2 が 4 件と、わずかに異なっている。 また、predict_proba() メソッドから得られる確率は一つが 1.0 で残りが 0.0 となっている。

$ python dummy.py 
Prediction: [2 2 1 2 2 0]
Prediction (probability):
[[1. 0. 0.]
 [0. 0. 1.]
 [0. 0. 1.]
 [0. 0. 1.]
 [0. 0. 1.]
 [0. 0. 1.]]
Class probabilities: [0.16666667 0.33333333 0.5       ]

試しに、もっとたくさんの検証用データを予測させてみよう。 数が多くなれば、大数の法則で教師データの確率分布に近づくはずだ。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
from sklearn.dummy import DummyClassifier


def main():
    clf = DummyClassifier()

    X_train = np.arange(6)
    y_train = np.array([0, 1, 2, 2, 1, 2])

    clf.fit(X_train, y_train)

    # 大数の法則
    X_test = np.arange(6, 100000)
    y_pred = clf.predict(X_test)

    # 推論した結果の頻度を確認する
    values, count = np.unique(y_pred, return_counts=True)
    print('Prediction frequency', count / np.sum(count))

    # 教師データの頻度
    print('Class probabilities:', clf.class_prior_)


if __name__ == '__main__':
    main()

上記を実行してみる。 今度は理論値にかなり近い結果が得られた。

$ python dummy.py
Prediction frequency [0.16611997 0.33404004 0.49983999]
Class probabilities: [0.16666667 0.33333333 0.5       ]

最頻値を常に返す

DummyClassifier は、返す値を色々とカスタマイズできる。 例えば、最頻値を常に返したいときはインスタンス化するときの strategy オプションに 'most_frequent' を指定する。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
from sklearn.dummy import DummyClassifier


def main():
    clf = DummyClassifier(strategy='most_frequent')

    X_train = np.arange(6)
    y_train = np.array([0, 1, 2, 2, 1, 2])

    clf.fit(X_train, y_train)

    X_test = np.arange(6, 12)
    y_pred = clf.predict(X_test)
    y_pred_proba = clf.predict_proba(X_test)

    # 推論結果 (常に最も多いクラスを返す)
    print('Prediction:', y_pred)
    print('Prediction (probability):')
    print(y_pred_proba)


if __name__ == '__main__':
    main()

上記を実行してみよう。 たしかに、最頻値である 2 が常に返されるようになっている。

$ python dummy.py 
Prediction: [2 2 2 2 2 2]
Prediction (probability):
[[0. 0. 1.]
 [0. 0. 1.]
 [0. 0. 1.]
 [0. 0. 1.]
 [0. 0. 1.]
 [0. 0. 1.]]

最頻値を常に返す (確率については教師データに準拠する)

'most_frequent' に近いものの、確率の返し方が異なるのが 'prior' となる。 こちらでは predict_proba() メソッドの返す結果が、元の確率分布にもとづいたものになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
from sklearn.dummy import DummyClassifier


def main():
    clf = DummyClassifier(strategy='prior')

    X_train = np.arange(6)
    # 目的変数
    y_train = np.array([0, 1, 2, 2, 1, 2])

    clf.fit(X_train, y_train)

    X_test = np.arange(6, 12)
    y_pred = clf.predict(X_test)
    y_pred_proba = clf.predict_proba(X_test)

    # 推論結果 (常に最も多いクラスを返す)
    print('Prediction:', y_pred)
    print('Prediction (probability):')
    print(y_pred_proba)


if __name__ == '__main__':
    main()

上記を実行してみよう。 たしかに predict() メソッドが最頻値を返す点は変わらないものの、predict_proba() の結果が変わっている。

$ python dummy.py 
Prediction: [2 2 2 2 2 2]
Prediction (probability):
[[0.16666667 0.33333333 0.5       ]
 [0.16666667 0.33333333 0.5       ]
 [0.16666667 0.33333333 0.5       ]
 [0.16666667 0.33333333 0.5       ]
 [0.16666667 0.33333333 0.5       ]
 [0.16666667 0.33333333 0.5       ]]

指定した固定値を返す

任意の定数を返したいときは 'constant' を指定する。 以下では例として常に 1 を返している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
from sklearn.dummy import DummyClassifier


def main():
    clf = DummyClassifier(strategy='constant', constant=1)

    X_train = np.arange(6)
    y_train = np.array([0, 1, 2, 2, 1, 2])

    clf.fit(X_train, y_train)

    X_test = np.arange(6, 12)
    y_pred = clf.predict(X_test)
    y_pred_proba = clf.predict_proba(X_test)

    # 推論結果 (ユーザが指定したクラスを常に返す)
    print('Prediction:', y_pred)
    print('Prediction (probability):')
    print(y_pred_proba)


if __name__ == '__main__':
    main()

上記を実行してみよう。 たしかに、常に 1 が返っている。

$ python dummy.py 
Prediction: [1 1 1 1 1 1]
Prediction (probability):
[[0. 1. 0.]
 [0. 1. 0.]
 [0. 1. 0.]
 [0. 1. 0.]
 [0. 1. 0.]
 [0. 1. 0.]]

ランダム (一様分布) に返す

また、目的変数の元の確率分布に依存せず、一様分布にしたいときは 'uniform' を指定する。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
from sklearn.dummy import DummyClassifier


def main():
    clf = DummyClassifier(strategy='uniform')

    X_train = np.arange(6)
    y_train = np.array([0, 1, 2, 2, 1, 2])

    clf.fit(X_train, y_train)

    X_test = np.arange(6, 12)
    y_pred = clf.predict(X_test)
    y_pred_proba = clf.predict_proba(X_test)

    # 推論結果 (クラスを一様分布として返す)
    print('Prediction:', y_pred)
    print('Prediction (probability):')
    print(y_pred_proba)


if __name__ == '__main__':
    main()

上記を実行してみよう。 predict() の結果は試行回数が少ないので偏ってしまっているが predict_proba() は全てのクラスが均等な値になっている。

$ python dummy.py 
Prediction: [2 2 2 2 0 0]
Prediction (probability):
[[0.33333333 0.33333333 0.33333333]
 [0.33333333 0.33333333 0.33333333]
 [0.33333333 0.33333333 0.33333333]
 [0.33333333 0.33333333 0.33333333]
 [0.33333333 0.33333333 0.33333333]
 [0.33333333 0.33333333 0.33333333]]

DummyRegressor

同様に回帰問題であれば DummyRegressor を用いる。

平均値を常に返す

DummyRegressor はデフォルトで目的変数の平均値を常に返すようになっている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
from sklearn.dummy import DummyRegressor


def main():
    # ダミーの回帰 (デフォルトの strategy は 'mean' なので平均を返す)
    reg = DummyRegressor()

    # 教師データの説明変数 (無視される)
    X_train = np.arange(6)
    # 教師データの目的変数
    y_train = np.array([-1.0, 0.0, 1.0, 2.0, 4.0, 8.0])

    reg.fit(X_train, y_train)

    X_test = np.arange(6, 12)
    y_pred = reg.predict(X_test)

    # 推論した結果の頻度を確認する
    print('Prediction', y_pred)


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python dummy.py 
Prediction [2.33333333 2.33333333 2.33333333 2.33333333 2.33333333 2.33333333]

中央値を常に返す

DummyRegressor も返す値をカスタマイズできる。 例えば中央値を返したいのであれば 'median' を指定する。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
from sklearn.dummy import DummyRegressor


def main():
    # ダミーの回帰 (常に中央値を返す)
    reg = DummyRegressor(strategy='median')

    X_train = np.arange(6)
    y_train = np.array([-1.0, 0.0, 1.0, 2.0, 4.0, 8.0])

    reg.fit(X_train, y_train)

    X_test = np.arange(6, 12)
    y_pred = reg.predict(X_test)

    print('Prediction', y_pred)


if __name__ == '__main__':
    main()

上記を実行してみよう。 ちゃんと中央値を返している。

$ python dummy.py 
Prediction [1.5 1.5 1.5 1.5 1.5 1.5]

指定した値を常に返す

特定の値を返したいときは 'constant' を指定する。 以下では例として -999 を返している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
from sklearn.dummy import DummyRegressor


def main():
    # ダミーの回帰 (常に指定した値を返す)
    reg = DummyRegressor(strategy='constant', constant=-999)

    X_train = np.arange(6)
    y_train = np.array([-1.0, 0.0, 1.0, 2.0, 4.0, 8.0])

    reg.fit(X_train, y_train)

    X_test = np.arange(6, 12)
    y_pred = reg.predict(X_test)

    print('Prediction', y_pred)


if __name__ == '__main__':
    main()

上記を実行してみよう。 ちゃんと指定した値を返している。

$ python dummy.py 
Prediction [-999 -999 -999 -999 -999 -999]

まとめ

機械学習において最低ラインの性能というのは使うデータセットと評価指標に依存する。 得られた結果がどれくらい良いものなのかを検討する上で、最低ラインの性能は確認しておきたいもの。 そんなとき Dummy{Classifier,Regressor} を使うと、既存のデータパイプラインのコードを流用して確認できるので便利そうだ。