「名刺の項目予測」モデル実装例

概要¶
「Sansan株式会社 名刺の項目予測」( https://signate.jp/competitions/26 )のチュートリアルです.
本コンペで与えられる画像データは学習用に2,480枚,テスト用に1,001枚です. 各名刺画像につき, 平均で約8領域の位置座標が
指定されていて, それらの領域画像に対して自動的にラベル付を行うアルゴリズムを作成するのが本コンペの課題です.
領域の種類は全部で9種類で, 会社名, 名前, 役職, 住所(郵便番号を含む), 電話番号, fax番号, 携帯番号, E-mailアドレス, HPのURLです.
今回用いる評価尺度は平均絶対誤差(Mean Absolute Error)です. この評価尺度は値が小さいほど精度のよさを表すため,
テストデータに対する平均絶対誤差をなるべく小さくするモデルを作成するのが今回の最終的な目的です.
このチュートリアルでは古典的な画像処理手法HOGを用いて特徴量抽出をして, 様々なモデルを用いたモデリングを行い比較しました.
結果, 異なるモデルでアンサンブル(majority voting)を行うことで精度が改善することがわかりました.
はじめに¶
分析環境としては以下を想定しています.
- OS: Windows 10 Pro
- 言語: Python==3.6.3
- ライブラリ
- pandas==0.23.0
- numpy==1.14.3
- Pillow==5.1.0
- scikit-image==0.13.0
- scikit-learn==0.19.1
メタデータの読み込みでpandasを, 画像データの読み込みの部分でPillowを, 特徴抽出の部分でscikit-imageを使いたいと思います.
学習用画像に対するメタデータ(train.csv)の読み込み¶
まず, 学習用画像に対するメタデータ(train.csv)を読み込み, 中身を確認します.
import os
import pandas as pd
_data_path = os.path.join('..','data','image','sansan')
_train_path = os.path.join(_data_path,'train.csv')
df = pd.read_csv(_train_path)
df.head()
左から順に, カラム"filename"は画像データファイル, カラム"left"~"bottom"は画像データにおける項目領域の座標にあたります.
カラム"company_name"~"url"はそれぞれの項目種類かどうかのフラグです.
後で実際の画像で確認しますが, 例えば1行目のデータを見ると画像ファイル"2842.png"の((491, 455), (796, 485))に当たる領域は"mobile",
つまり携帯電話番号が写っている画像となります.
学習用画像データ(train_images_1.zip, train_images_2.zip, train_images_3.zip)の読み込み¶
学習用画像データはzip形式ですが, まず作業ディレクトリにおいて, 解凍して一つのフォルダ(ここではtrain_imagesとします)にまとめておいてください.
画像ファイル"2842.png"の((491, 455), (796, 485))に当たる領域を見てみます.
from PIL import Image
_img_path = os.path.join(_data_path,'train_images')
img = Image.open(os.path.join(_img_path, '2842.png'))
領域を切り出すにはcropメソッドを使います. (left, top, right, bottom)を渡します.
img_cropped = img.crop((491, 455, 796, 485))
img_cropped
確かに携帯電話番号が写っています.
train.csvの3行目を見ると, 画像ファイル"95.png"の((320, 498), (865, 521))に当たる領域は"mobile"と"fax", つまり携帯電話番号とfax番号が写っている
画像となることが確認できます. 実際に見てみます.
img = Image.open(os.path.join(_img_path, '95.png'))
img_cropped = img.crop((320, 498, 865, 521))
img_cropped
確かにfax番号と携帯番号が写っています.
画像ファイルの数と領域の総数を確認します.
print('画像ファイルの数:', len(df['file_name'].unique()))
print('領域の総数:', len(df))
次に全データ数とそれぞれの項目の出現数を見てみます.
print('全データ:', len(df))
print(df[['company_name','full_name','position_name', 'address', 'phone_number', 'fax', 'mobile', 'email','url']].agg(sum))
領域別の項目の出現数を見てみます.
print('全データ:', len(df))
print(df[['company_name','full_name','position_name', 'address', 'phone_number', 'fax', 'mobile', 'email','url']].sum(axis=1).value_counts())
画像ファイル別の領域の数の分布を見てみます.
%matplotlib inline
df_img_file = df.groupby('file_name').apply(len)
df_img_file.hist()
手法¶
次にHOGにより画像の特徴量抽出を行い, ロジスティック回帰モデルによって項目を予測するモデルを構築します.
HOGによる特徴量抽出¶
まずモデリングする際に入力する特徴量を求めてみます.
このままの数値データだと次元が大きすぎるので低次元のベクトルに変換します. データを低次元に落とすことは計算量の削減と余計な
情報の削減をしてデータの本質的な特徴を抽出する効果があります. そのため, この処理をうまく行えれば性能を大きく向上させることが期待できます.
画像データの特徴量を抽出する方法論は数多く存在しますが, ここでは画像の特性を考慮した
HOG(Histogram of Oriented Gradients)という特徴量を紹介します. HOGの詳しい説明についてはたとえば[1], [2], [3]を参考にしてください.
一般に, 幾何学的変換(平行移動や回転移動)や照明の変動に頑健な特徴量です.
HOGによる特徴量抽出を行うまえに画像をグレースケール化し, 画像データの大きさをそろえるとその後都合がよいので, 以下のようにして,
領域を切り出した画像をグレースケール化し, 指定の大きさ(横, 縦)=(216, 72)にします.
img_cropped = img_cropped.convert('L') # convertメソッドによりグレースケール化
print('元の大きさ:', img_cropped.size)
img_resized = img_cropped.resize((216, 72)) # (216, 72)にリサイズする
print('リサイズ後の大きさ:', img_resized.size)
img_cropped
img_resized
scikit-imageではfeatureモジュールのhogに実装されています. skimage.featureからhogをインポートします.
また, 画像データを数値データ(配列)に変換するためにnumpyをインポートします.
import numpy as np
from skimage.feature import hog
img_array = np.array(img_resized) # 画像データを数値データに変換
img_feat = hog(img_array,
orientations = 12,
pixels_per_cell = (12, 12),
cells_per_block = (1, 1),
block_norm='L1-sqrt')
print(img_feat.shape)
今回の場合は上記のようなパラメータにより, 1296次元の特徴量が得られました.
pixels_per_cell, cells_per_blockなどのパラメータについて詳しく知りたい方は[4]を参照してください.
パラメータを変えることで特徴ベクトルの大きさを変えることができます.
これまでの処理(データの読み込み)⇒(特徴抽出)を一つの関数にまとめてみます.
項目ラベルが与えられたの場合(学習や検証の場合)は対応する項目ラベルも返すようにします.
import time
def load_data(file_name, img_dir, img_shape, orientations, pixels_per_cell, cells_per_block, label=True):
classes = ['company_name', 'full_name', 'position_name', 'address', 'phone_number', 'fax', 'mobile', 'email', 'url']
df = pd.read_csv(file_name)
n = len(df)
if label:
Y = np.zeros((n, len(classes)))
print('loading...')
s = time.clock()
for i, row in df.iterrows():
f, l, t, r, b = row['file_name'], row['left'], row['top'], row['right'], row['bottom']
img = Image.open(os.path.join(img_dir, f)).crop((l,t,r,b)) # 項目領域画像を切り出す
if img.size[0]<img.size[1]: # 縦長の画像に関しては90度回転して横長の画像に統一する
img = img.transpose(Image.ROTATE_90)
# preprocess
img = img.convert('L')
img_array = np.array(img.resize(img_shape))/255. # img_shapeに従った大きさにそろえる
# feature extraction
img_feat = hog(img_array,
orientations = orientations,
pixels_per_cell = pixels_per_cell,
cells_per_block = cells_per_block,
block_norm='L1-sqrt')
if i == 0:
feature_dim = len(img_feat)
print('feature dim:', feature_dim)
X = np.zeros((n, feature_dim))
if (i+1)%5000==0:
print((i+1), '/', n)
X[i,:] = np.array([img_feat])
if label:
y = list(row[classes])
Y[i,:] = np.array(y)
print('Done. Took', time.clock()-s, 'seconds.')
if label:
return X, Y
else:
return X
モデルを学習させるための学習用データを作成します.
img_shape = (216,72)
orientations = 12
pixels_per_cell = (12, 12)
cells_per_block = (1, 1)
X_train, Y_train = load_data(_train_path, _img_path, img_shape, orientations, pixels_per_cell, cells_per_block)
print('学習データの数:', X_train.shape[0])
print('特徴ベクトルの次元数:', X_train.shape[1])
モデルの学習¶
生成したデータ(X_train, Y_train)を用いてモデルの学習を行います.
このコンペは一般的にmulti-label分類問題といえます. 画像データに対して複数種類のラベルを複数割り当てる問題です.
アプローチは様々考えられますが, ここでは"one versus rest"法によりmulti-label問題に対応します.
これは, "カテゴリ1とその他", ..., "カテゴリCとその他"という具合に, ラベルの数Cだけ2値識別器を学習させることにより,
multi-label問題に対応する方法です. 2値識別器としては様々考えられますが,
ここではロジスティック回帰モデル, 多層ニューラルネットワークモデル, ランダムフォレストモデル, 勾配ブースティングモデルを用います.
実装はscikit-learnで行います. 以下のように抽象クラスを定義し, multi-label問題に対応できるようにします.
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.neural_network import MLPClassifier
class MultiLabelClassifier():
def __init__(self, n_out, model_name=None):
self.n_out = n_out
if model_name == 'LR':
print('LR')
elif model_name == 'GB':
print('GB')
elif model_name == 'RF':
print('RF')
elif model_name == 'MLP':
print('MLP')
else:
print('LR')
model_list = []
for l in range(self.n_out):
if model_name == 'LR':
model_list.append(LogisticRegression(C=0.1))
elif model_name == 'GB':
model_list.append(GradientBoostingClassifier())
elif model_name == 'RF':
model_list.append(RandomForestClassifier())
elif model_name == 'MLP':
model_list.append(MLPClassifier(alpha=0.05, early_stopping=True))
else:
model_list.append(LogisticRegression())
self.models = model_list
def fit(self, X, Y):
i = 0
start_overall = time.clock()
for model in self.models:
start = time.clock()
print('training model No.%s...'%(i+1))
model.fit(X, Y[:,i])
print('Done. Took', time.clock()-start, 'seconds.')
i += 1
print('Done. Took', time.clock()-start_overall, 'seconds.')
def predict(self, X):
i = 0
predictions = np.zeros((len(X), self.n_out))
start = time.clock()
print('predicting...')
for model in self.models:
predictions[:,i] = model.predict(X)
print(str(i+1),'/',str(self.n_out))
i += 1
print('Done. Took', time.clock()-start, 'seconds.')
return predictions
次に学習データ(X_train, Y_train)でモデルを学習させます.
まずはロジスティック回帰モデルで学習を行います.
model_lr = MultiLabelClassifier(n_out = 9, model_name='LR') # 今回は9項目あるため, クラス数は9個に設定
model_lr.fit(X_train, Y_train)
次に多層ニューラルネットワークモデルで学習を行います.
model_mlp = MultiLabelClassifier(n_out = 9, model_name='MLP') # 今回は9項目あるため, クラス数は9個に設定
model_mlp.fit(X_train, Y_train)
予測値の出力¶
学習が終わったらそれぞれの学習済みモデルに対してX_trainの予測値を出力します.
predictions_lr_train = model_lr.predict(X_train)
predictions_mlp_train = model_mlp.predict(X_train)
精度評価¶
まず, 学習データX_trainに対する予測値predictionsと答えY_trainを照らし合わせて学習データに対する精度を評価します.
本コンペの評価関数である平均絶対誤差(Mean Absolute Error)は以下のように実装できます.
def mae(y, yhat):
return np.mean(np.abs(y - yhat))
平均絶対誤差(Mean Absolute Error)によりそれぞれのモデルの学習データに対する精度を評価します.
print('Logistic Regression:', mae(Y_train, predictions_lr_train))
print('MLP:', mae(Y_train, predictions_mlp_train))
次に評価用画像に対する予測を行い, 精度を評価してみます.
_test_path = os.path.join(_data_path, 'test.csv')
_test_img_path = os.path.join(_data_path, 'test_images')
_ans_path = os.path.join(_data_path, 'ans.csv')
_fin_path = os.path.join(_data_path, 'fin.csv')
_tmp_path = os.path.join(_data_path, 'temp.csv')
X_test = load_data(_test_path, _test_img_path, img_shape, orientations, pixels_per_cell, cells_per_block, label=False)
predictions_lr_test = model_lr.predict(X_test)
predictions_mlp_test = model_mlp.predict(X_test)
評価用画像データに対する答えのファイルを読み込みます.
ans = pd.read_csv(_ans_path,header=None,index_col=0)
ans_array = np.array(ans)
平均絶対誤差(Mean Absolute Error)によりそれぞれのモデルの評価用データに対する精度を評価します.
print('Logistic Regression:', mae(ans_array, predictions_lr_test))
print('MLP:', mae(ans_array, predictions_mlp_test))
多層ニューラルネットワークモデルの精度が良いようです.
得られた2つの結果をアンサンブル(多数決)して精度を評価してみます.
predictions_ensemble_test = ((predictions_lr_test+predictions_mlp_test)>=1).astype(np.int)
mae(ans_array, predictions_ensemble_test)
少し改善が見られました.
まとめ¶
本チュートリアルでは, 特徴抽出⇒モデル学習という古典的な手法でモデリングを試してみました.
結果, 異なるモデルによるアンサンブルにより若干の精度改善がみられました.
さらなる改善案として, HOGのパラメータ(orientations, pixels_per_cellなど)を変え, 特徴量の次元数を変えること,
またはHOGのほかにいろいろな特徴量を連結させて新たに特徴量を作ることなどが考えられます.
そして, モデルのハイパーパラメータ(l2正則化項など)を最適化することでさらに精度が改善する可能性があります.
あるいは, 特徴量抽出を深層学習モデル(畳み込みニューラルネットワーク)によって自動的に獲得してしまうことも考えられます.
また, 2項目以上写っている場合はお互いがノイズとなって, 結果として誤認識率が上がってしまったことが考えられるので,
領域をさらに細かく特定するアルゴリズムによって1項目のみ写るように画像を分割することが改善案としてあります.
参考文献¶
- 中部大学 藤吉研究室 http://www.vision.cs.chubu.ac.jp/joint_hog/pdf/HOG+Boosting_LN.pdf(2016-08-18)
- 東工大 画像解析論 http://www.isl.titech.ac.jp/~nagahashilab/member/longb/imageanalysis/LectureNotes/ImageAnalysis07.pdf(2016-08-18)
- 100行で書く画像処理最先端 https://www.hal.t.u-tokyo.ac.jp/paper/2009/Journal_15.pdf(2016-08-18)
- http://scikit-image.org/docs/dev/api/skimage.feature.html?highlight=hog#skimage.feature.hog(2016-08-18)
