[Sansanデータ解析チャレンジ] 名刺の記載項目予測 チュートリアル

はじめに

本チュートリアルでは「[Sansanデータ解析チャレンジ] 名刺の記載項目予測」コンペティションを題材にして、Pythonとscikit-learnを利用した分類器の実装方法を解説します。

掲載コードは、Python2.7.8で動作確認を行っています。また、掲載コードでは以下のライブラリを使用します:

本チュートリアルは、下記を参考にしております:

事前準備

ライブラリの読み込み

必要ライブラリをインポートします。

In [1]:
import os
import pandas as pd
from PIL import Image
import numpy as np
import sklearn

データの読み込み

まずは、コンペティションページからデータをダウンロードし、正解ラベルが与えられている「訓練データ」を読み込みます。sansan-001 ディレクトリ上で以下のコードを実行し、訓練データ train.csv を読み込み df_train に格納します。

In [2]:
df_train = pd.read_csv('train.csv')

df_trainの中身を以下のコードで確認します。filenameカラムに名刺画像のファイル名が格納されています。また、left, top, right, bottomに部分画像の座標が格納されています。company_name, full_name, ..., urlが正解ラベルです。たとえば0行目の部分画像は mobile1で残りは0となっており、この部分画像には 携帯番号だけが含まれることがわかります。

In [3]:
df_train.head()
Out[3]:
filename left top right bottom company_name full_name position_name address phone_number fax mobile email url
0 2842.png 491 455 796 485 0 0 0 0 0 0 1 0 0
1 182.png 24 858 311 886 0 0 0 0 0 0 1 0 0
2 95.png 320 498 865 521 0 0 0 0 0 1 1 0 0
3 2491.png 65 39 497 118 1 0 0 0 0 0 0 0 0
4 3301.png 271 83 333 463 0 1 1 0 0 0 0 0 0

また、訓練データには25,357件の部分画像が含まれることが、以下のコードで確認できます。

In [4]:
df_train.shape
Out[4]:
(25357, 14)

ここで、実際に0行めの部分画像を見てみましょう。まず、0行目の部分画像の情報を rowに格納します。

In [5]:
row = df_train.iloc[0, :]
row
Out[5]:
filename         2842.png
left                  491
top                   455
right                 796
bottom                485
company_name            0
full_name               0
position_name           0
address                 0
phone_number            0
fax                     0
mobile                  1
email                   0
url                     0
Name: 0, dtype: object
            

画像ファイル row.filenameを開き、row.left, row.top, row.right, row.bottomで与えられる長方形で切り取り、imgに格納します。imgを見ると、たしかに携帯番号だけが含まれることが確認できます。

In [6]:
DIR_IMAGES = 'images'
img = Image.open(os.path.join(DIR_IMAGES, row.filename))
img = img.crop((row.left, row.top, row.right, row.bottom))
img
Out[6]:

訓練データと同様に「テストデータ」を読み込みます。訓練データと異なり、正解ラベル company_name, full_name, ..., urlが含まれないことが確認できます。

In [7]:
df_test = pd.read_csv('test.csv')
df_test.head()
Out[7]:
filename left top right bottom
0 1942.png 66 359 361 386
1 1128.png 58 373 519 422
2 2719.png 62 289 297 314
3 641.png 58 668 416 747
4 2529.png 42 212 303 244

正解が与えられている訓練データを使って予測モデルを学習し、テストデータに対する予測結果の出力を目指します。なお、テストデータには、8,918件の部分画像が含まれています。

In [8]:
df_test.shape
Out[8]:
(8918, 5)

以降、このチュートリアルでは簡単のため、訓練データ500件、テストデータ100件だけを対象にします。

In [9]:
df_train = df_train.sample(500, random_state=0)
df_test = df_test.sample(100, random_state=0)

特徴ベクトルの作成

部分画像を数値化して特徴ベクトルを作成し、予測モデルの学習に利用できるようにします。0行目の部分画像を例として、特徴ベクトルの作成手順を説明します。いま、imgに0行目の部分画像が格納されています。

In [10]:
img
Out[10]:

扱いやすくするため、まずは画像をグレースケールに変換します。

In [11]:
img = img.convert('L')
img
Out[11]:

部分画像ごとに大きさが違うと扱いづらいため、100ピクセル × 100ピクセルの正方形に変換します。

In [12]:
IMG_SIZE = 100
img = img.resize((IMG_SIZE, IMG_SIZE), resample=Image.BICUBIC)
img
Out[12]:

画像を100行100列の数値行列に変換します

In [13]:
x = np.asarray(img, dtype=np.float)
x.shape
Out[13]:
(100, 100)

行列の各要素は、対応するピクセルの明度を表します。

In [14]:
x
Out[14]:
array([[ 204.,  203.,  203., ...,  222.,  223.,  223.],
       [ 204.,  203.,  203., ...,  222.,  223.,  223.],
       [ 204.,  203.,  203., ...,  222.,  223.,  223.],
       ...,
       [ 204.,  204.,  205., ...,  223.,  223.,  224.],
       [ 204.,  204.,  205., ...,  223.,  223.,  224.],
       [ 204.,  204.,  205., ...,  223.,  223.,  224.]])

最後に、100行100列の行列を、10000次元のベクトルに変換します

In [15]:
x = x.flatten()
x
Out[15]:
array([ 204.,  203.,  203., ...,  223.,  223.,  224.])
In [16]:
x.shape
Out[16]:
(10000,)

以上の手続きをすべての部分画像に対して適用し、特徴ベクトルを作成します。訓練データの特徴ベクトルをX_trainに格納します。

In [17]:
X_train = []
for i, row in df_train.iterrows():
    img = Image.open(os.path.join(DIR_IMAGES, row.filename))
    img = img.crop((row.left, row.top, row.right, row.bottom))
    img = img.convert('L')
    img = img.resize((IMG_SIZE, IMG_SIZE), resample=Image.BICUBIC)

    x = np.asarray(img, dtype=np.float)
    x = x.flatten()
    X_train.append(x)

X_train = np.array(X_train)

同様に、テストデータの特徴ベクトルをX_testに格納します。

In [18]:
X_test = []
for i, row in df_test.iterrows():
    img = Image.open(os.path.join(DIR_IMAGES, row.filename))
    img = img.crop((row.left, row.top, row.right, row.bottom))
    img = img.convert('L')
    img = img.resize((IMG_SIZE, IMG_SIZE), resample=Image.BICUBIC)

    x = np.asarray(img, dtype=np.float)
    x = x.flatten()
    X_test.append(x)

X_test = np.array(X_test)     

正解ラベルの取得

訓練データの正解ラベルを行列Y_trainに格納します。

In [19]:
columns = ['company_name', 'full_name', 'position_name',
           'address', 'phone_number', 'fax',
           'mobile', 'email', 'url']
Y_train = df_train[columns].values

以上で、予測モデリングの準備が整いました。訓練データX_train, Y_trainを用いて予測モデルを学習し、X_testに対する予測結果の出力を目指します。

予測モデルの学習

それでは、予測モデルの学習を行っていきます。コンペティションページに投稿するまで予測精度が確認できないのは、様々な予測モデルを試す上で不便なため、訓練データを、予測モデルの学習に用いる開発データと予測精度の確認に用いる評価データに分割します。ここでは、80%を開発、20%を評価に用います。

In [20]:
from sklearn.model_selection import train_test_split
X_dev, X_val, Y_dev, Y_val = train_test_split(X_train, Y_train, train_size=0.8, random_state=0)

訓練データ500件が、400件の開発データと100件の評価データに分割されました。

In [21]:
print X_dev.shape, Y_dev.shape
print X_val.shape, Y_val.shape
Out[21]:
(400, 10000) (400, 9)
(100, 10000) (100, 9)

前処理:標準化

この後で用いるPCAでは、特徴ベクトルの各次元の平均が0であり、各次元の分散に偏りがないことが求められています。そこでまず、各次元の平均が0、分散が1になるようにデータを変換します。この処理は標準化と呼ばれます。scikit-learnでは標準化のためのツールとして、sklearn.preprocessing.StandardScalerが用意されています。まず、scaler = StandardScaler()で変換器を初期化し、scaler.fit(X_dev_scaled)で開発データにあわせて変換器を構築します。

In [22]:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(X_dev)
Out[22]:
StandardScaler(copy=True, with_mean=True, with_std=True)

構築した変換器を用いて、標準化します。

In [23]:
X_dev_scaled = scaler.transform(X_dev)

標準化の後、たしかに平均が0、分散が1になっていることが確認できます。

In [24]:
X_dev_scaled.mean(axis=0)
Out[24]:
array([ -3.16413562e-17,  -3.16413562e-17,   3.14193116e-16, ...,
        -1.51545443e-16,  -3.47777362e-16,   1.02695630e-17])
In [25]:
X_dev_scaled.var(axis=0)
Out[24]:
array([ 1.,  1.,  1., ...,  1.,  1.,  1.])

前処理:次元削減

10,000次元の特徴ベクトルから有効な特徴を抽出するため、次元削減を行います。ここでは、PCAを用いて10次元に減らします。まず、開発データを用いて次元削減器を構築します。decomposer=PCA()で次元削減器を初期化し、decomposer.fit(X_dev)で開発データにあわせて次元削減器を構築します。

In [26]:
from sklearn.decomposition import PCA
decomposer = PCA(n_components=10, random_state=0)
decomposer.fit(X_dev_scaled)
Out[26]:
PCA(copy=True, iterated_power='auto', n_components=10, random_state=0,
          svd_solver='auto', tol=0.0, whiten=False)

次元削減器を用いて、開発データX_dev_scaledの次元削減を行い、X_dev_pcaに格納します。次元削減は、decomposer.transform(X_dev_scaled)で実行できます。

In [27]:
X_dev_pca = decomposer.transform(X_dev_scaled)

X_dev_pcaがたしかに10次元になっていることが確認できます。

In [28]:
print X_dev_pca.shape
Out[28]:
(400, 10)

構築した変換器、次元削減器を評価データにも適用します。

In [29]:
X_val_scaled = scaler.transform(X_val)
X_val_pca = decomposer.transform(X_val_scaled)

ロジスティック回帰の学習

次元削減後のデータを用いて、予測モデルを学習します。本コンペティションで扱う問題は、一つの部分画像に複数ラベル(例:「電話番号」「ファックス番号」)が割り当てられることのあるマルチラベル分類です。そこで、各サンプルに各ラベルが割り当てられるか否かを予測する二値分類器を、ラベルごとに学習します。本コンペティションの場合は、ラベルは9種類(「会社名」「名前」…「ホームページURL」)ですので、9個の分類器を学習します。

ここでは予測モデルとして、L2正則化付きのロジスティック回帰を用います。正則化パラメータは、まずは0.01とします。ラベルごとに分類器を学習し、classifiersに格納します。classifier = LogisticRegression()でロジスティック回帰モデルを初期化し、classifier.fit(X_dev_pca, y)でモデルを学習します。y = Y_dev[:, j]は正解ラベルのj列目です。

In [30]:
from sklearn.linear_model import LogisticRegression

classifiers = []
for j in range(Y_dev.shape[1]):
    y = Y_dev[:, j]
    classifier = LogisticRegression(penalty='l2', C=0.01)
    classifier.fit(X_dev_pca, y)
    classifiers.append(classifier)

学習した分類器を使って、評価データの予測結果を出力します。classifier.predict_proba(X_val_pca)は、評価データX_val_pcaに対する予測結果として、各サンプルが「負例である確率」「正例である確率」を2次元配列で返します。ここでは「正例である確率」だけを用いるため、1列目だけを取り出しています。

In [31]:
Y_val_pred = np.zeros(Y_val.shape)
for j in range(Y_dev.shape[1]):
    classifier = classifiers[j]
    y = classifier.predict_proba(X_val_pca)[:, 1]
    Y_val_pred[:, j] = y

Y_val_predに、評価データ中の100件のサンプルのそれぞれについて、9種類のラベルのそれぞれが割り当てられる確率が格納されました。

In [32]:
Y_val_pred.shape
Out[32]:
(100, 9)

評価データの正解Y_valを用いて、予測結果Y_val_predの精度を測ります。本コンペティションの評価指標と同じ、マクロ平均AUCを用います。

In [33]:
from sklearn.metrics import roc_auc_score
roc_auc_score(Y_val, Y_val_pred, average='macro')
Out[33]:
0.81590230361126037

まずまずの精度となることが確認できました。なお、二値分類器によるマルチラベル分類は、sklearn.multiclass.OneVsRestClassifierを用いることで簡単に実装できます。以下のコードで、同じモデルが学習できます。

In [34]:
from sklearn.multiclass import OneVsRestClassifier

classifier = OneVsRestClassifier(LogisticRegression(penalty='l2', C=0.01))
classifier.fit(X_dev_pca, Y_dev)
Y_val_pred = classifier.predict_proba(X_val_pca)
In [35]:
roc_auc_score(Y_val, Y_val_pred, average='macro')      
Out[35]:
0.81590230361126037

ハイパーパラメータの選択

いまの例では、正則化パラメータを0.01に固定していました。実際はデータごとに、適切な正則化パラメータの値は異なります。交差検証により、適切な値を選択します。交差検証の中では、標準化→次元削減→分類器の学習という手続きを何度も行うことになります。まず、この手続きをパイプライン (sklearn.pipeline.Pipeline)として定義します。stepsに学習の手順を記述し、pipeline=Pipeline(steps)でパイプラインを定義します。

In [36]:
from sklearn.pipeline import Pipeline

steps = [('scaler', StandardScaler()),
         ('decomposer', PCA(10, random_state=0)),
         ('classifier', OneVsRestClassifier(LogisticRegression(penalty='l2')))]
pipeline = Pipeline(steps)

5分割の交差検証で、正則化パラメータを 0.01, 0.1, 1.0, 10., 100. から選ぶことにします。sklearn.model_selection.GridSearchCVを使うと、交差検証によるハイパーパラメータの選択を簡単に記述できます。まず、paramsに候補パラメータを記述します。ここでは、pipeline中の'classifier'OneVsRestClassifier) の内部の`estimator` (LogisticRegression) の正則化パラメータCの候補として [0.01, 0.1, 1.0, 10., 100.]を用います。このことを、{'classifier__estimator__C': [0.01, 0.1, 1.0, 10., 100.]} として記述し、GridSearchCVpipelineと共に渡します。

In [37]:
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import make_scorer

params = {'classifier__estimator__C': [0.01, 0.1, 1.0, 10., 100.]}
scorer = make_scorer(roc_auc_score, average='macro', needs_proba=True)

predictor = GridSearchCV(pipeline, params, cv=5, scoring=scorer)

predictor.fit(X_dev, Y_dev)により、開発データを用いて交差検証を実行し、適切な正則化パラメータを探します。

In [38]:
predictor.fit(X_dev, Y_dev)
Out[38]:
GridSearchCV(cv=5, error_score='raise',
       estimator=Pipeline(steps=[('scaler', StandardScaler(copy=True, with_mean=True, with_std=True)), ('decomposer', PCA(copy=True, iterated_power='auto', n_components=10, random_state=0,
  svd_solver='auto', tol=0.0, whiten=False)), ('classifier', OneVsRestClassifier(estimator=LogisticRegression(C=1.0, class_weight=None, d...=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False),
          n_jobs=1))]),
       fit_params={}, iid=True, n_jobs=1,
       param_grid={'classifier__estimator__C': [0.01, 0.1, 1.0, 10.0, 100.0]},
       pre_dispatch='2*n_jobs', refit=True, return_train_score=True,
       scoring=make_scorer(roc_auc_score, needs_proba=True, average=macro),
       verbose=0)

交差検証により選ばれた正則化パラメータを以下のコードで確認できます。

In [39]:
predictor.best_params_
Out[39]:
{'classifier__estimator__C': 10.0}

C=10.0が選ばれたことがわかりました。このモデルを用いて、評価データに対する予測を行い、その精度を確認します。GridSearchCVでは、適切なハイパーパラメータを決めた後、データ全体を使ってそのパラメータでモデルを学習し直します。predictor.predict_proba()により、再学習後のモデルによる予測結果を出力することができます。

In [40]:
Y_val_pred = predictor.predict_proba(X_val)
roc_auc_score(Y_val, Y_val_pred, average='macro')
Out[40]:
0.82581633343114969

C=0.01で学習したときよりも、評価データに対して高い予測精度が得られていることが確認できました。

ここまで、次元削減の要素数を10に固定していましたが、この要素数も調整すべきハイパーパラメータです。次の例では、正則化パラメータと一緒に、要素数も調整します。pipeline中の`decomposer` (PCA)の要素数n_componentsの候補として[10, 20, 50]を用いることを、{'decomposer__n_components': [10, 20, 50]}により記述し、GridSearchCVに渡します。predictor.fit(X_train, Y_train)により、適切な正則化パラメータと要素数を探します。

In [41]:
params = {'classifier__estimator__C': [0.01, 0.1, 1.0, 10., 100.],
         'decomposer__n_components': [10, 20, 50]}

predictor = GridSearchCV(pipeline, params, cv=5, scoring=scorer)
predictor.fit(X_dev, Y_dev)
Out[41]:
GridSearchCV(cv=5, error_score='raise',
       estimator=Pipeline(steps=[('scaler', StandardScaler(copy=True, with_mean=True, with_std=True)), ('decomposer', PCA(copy=True, iterated_power='auto', n_components=10, random_state=0,
  svd_solver='auto', tol=0.0, whiten=False)), ('classifier', OneVsRestClassifier(estimator=LogisticRegression(C=1.0, class_weight=None, d...=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False),
          n_jobs=1))]),
       fit_params={}, iid=True, n_jobs=1,
       param_grid={'classifier__estimator__C': [0.01, 0.1, 1.0, 10.0, 100.0], 'decomposer__n_components': [10, 20, 50]},
       pre_dispatch='2*n_jobs', refit=True, return_train_score=True,
       scoring=make_scorer(roc_auc_score, needs_proba=True, average=macro),
       verbose=0)

正則化パラメータとして C=1.0, 次元削減の要素数として n_components=50が選ばれたことが確認できます。

In [42]:
predictor.best_params_
Out[42]:
{'classifier__estimator__C': 1.0, 'decomposer__n_components': 50}

このハイパーパラメータを用いて学習したモデルで、評価データに対する予測精度が向上することがわかります。

In [43]:
Y_val_pred = predictor.predict_proba(X_val)
roc_auc_score(Y_val, Y_val_pred, average='macro')
Out[43]:
0.8764621490578669

予測結果の提出

それでは、この予測モデルを使ってテストデータに対する予測を行い、結果をコンペティションページに提出しましょう。ここまでは訓練データX_train, Y_trainの一部を評価データ X_val, Y_valとして学習には用いず、残りのデータで予測モデルを学習してきました。既に評価は済んだので、訓練データ全体で予測モデルを学習し直します。

In [44]:
final_predictor = predictor.best_estimator_
final_predictor.fit(X_train, Y_train)
Out[43]:
Pipeline(steps=[('scaler', StandardScaler(copy=True, with_mean=True, with_std=True)), ('decomposer', PCA(copy=True, iterated_power='auto', n_components=50, random_state=0,
  svd_solver='auto', tol=0.0, whiten=False)), ('classifier', OneVsRestClassifier(estimator=LogisticRegression(C=1.0, class_weight=None, d...=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False),
          n_jobs=1))])

テストデータX_testに対する予測結果を出力し、提出用のファイルとして書き出します。

In [44]:
Y_test_pred = final_predictor.predict_proba(X_test)
np.savetxt('submission.dat', Y_test_pred, fmt='%.6f')

なお、このチュートリアルではテストデータの一部(100件)だけを対象としてきたため、submission.datには100件分の予測結果しか含まれていません。そのため、submission.datをコンペティションページで提出するとエラーとなります。実際に提出する際には、テストデータ全体に対する予測結果を出力してください。コンペティションデータセットの中にあるsample-submission.datを参考にしてください。