Pythonにロジスティック回帰で画像を学習させてみました

今回はロジスティック回帰で画像とラベルの対応関係を教師付き学習させて画像分類の精度を検証して遊んでみました。

ちなみに前回は画像を教師なし学習のk-means法でカテゴリ分けしました。

データセット

17 Category Flower Datasetで公開されている花の画像を使いました。

$ wget http://www.robots.ox.ac.uk/~vgg/data/flowers/17/17flowers.tgz
$ tar xvfz 17flowers.tgz

イメージファイル名は"image_連番.jpg"の形式で品種との対応は下記みたいです。

連番 (連番 - 1) / 80 品種
0001 - 0080 0 Daffodil ラッパズイセン
0081 - 0160 1 Snowdrop スノードロップ
0161 - 0240 2 LilyValley スズラン
0241 - 0320 3 Bluebell ブルーベル
0321 - 0400 4 Crocus クロッカス
0401 - 0480 5 Iris アヤメ
0481 - 0560 6 Tigerlily オニユリ
0561 - 0640 7 Tulip チューリップ
0641 - 0720 8 Fritillary クロユリ
0721 - 0800 9 Sunflower ヒマワリ
0801 - 0880 10 Daisy ヒナギク
0881 - 0960 11 ColtsFoot フキタンポポ
0961 - 1040 12 Dandelion タンポポ
1041 - 1120 13 Cowslip キバナノクリンザクラ
1121 - 1200 14 Buttercup キンポウゲ
1201 - 1280 15 Windflower アネモネ
1281 - 1360 16 Pansy パンジー

検証

特徴量にはsurfのbag-of-Visual Words, haralick, edginess_sobelを使いました。
それぞれの組み合わせで交差検証して正解率を求めて混合行列ROC曲線で可視化してみました。

surf haralick edginess_sobel 正解率
1 × × 59.0%
2 × × 16.9%
3 × × 9.2%
4 × 60.4%
5 × 59.0%
6 × 18.4%
7 60.4%

60.4%は低い。。。

混合行列とROC曲線1: surfのみ

f:id:nihma:20150119164103p:plain

f:id:nihma:20150119164127p:plain

混合行列とROC曲線2: haralickのみ

f:id:nihma:20150119164403p:plain

f:id:nihma:20150119164425p:plain

混合行列とROC曲線3: edginess_sobelのみ

f:id:nihma:20150119164617p:plain

f:id:nihma:20150119164651p:plain

混合行列とROC曲線4: surf/haralick

f:id:nihma:20150119164725p:plain

f:id:nihma:20150119164757p:plain

混合行列とROC曲線5: surf/edginess_sobel

f:id:nihma:20150119164831p:plain

f:id:nihma:20150119164903p:plain

混合行列とROC曲線6: haralick/edginess_sobel

f:id:nihma:20150119164942p:plain

f:id:nihma:20150119165007p:plain

混合行列とROC曲線7: surf/haralick/edginess_sobel

f:id:nihma:20150119165042p:plain

f:id:nihma:20150119165111p:plain

コード

今回のコードです。

import mahotas as mh
import numpy as np
from glob import glob
from mahotas.features import surf
from sklearn.cluster import KMeans
from sklearn.cross_validation import ShuffleSplit
from sklearn.metrics import confusion_matrix
from sklearn.metrics import precision_recall_curve, roc_curve
from sklearn.metrics import auc
from collections import defaultdict
from matplotlib import pylab
from sklearn.linear_model import LogisticRegression

def save_plot_confusion_matrix(file_name, cms, y_labels):
  cm_avg = np.mean(cms, axis=0)
  cm_norm = cm_avg / np.sum(cm_avg, axis=0)
  pylab.clf()
  fig = pylab.figure(figsize=(11,10))
  ax = fig.add_subplot(111)
  res = ax.imshow(cm_norm, cmap='Blues', interpolation='nearest')
  for i, cas in enumerate(cm_avg):
      for j, c in enumerate(cas):
          n = c
          if n > 0:
              pylab.text(j-.2, i+.2, n, fontsize=12, color='red')
  cb = fig.colorbar(res)
  ax.set_xticks(range(len(y_labels)))
  ax.xaxis.set_ticks_position("bottom")
  ax.set_xticklabels(y_labels, rotation=270)
  ax.set_yticks(range(len(y_labels)))
  ax.set_yticklabels(y_labels)
  pylab.title('Confusion Matrix')
  pylab.xlabel('Predicted class')
  pylab.ylabel('True class')
  pylab.grid(False)
  pylab.savefig(file_name)

def save_plot_roc(file_name, y_labels, roc_scores, tprs, fprs):
  pylab.clf()
  pylab.figure(figsize=(35, 35))
  column = 4
  row = int(len(y_labels) / column) + 1
  for i, y_label in enumerate(y_labels):
    pylab.subplot(row, column, i+1)
    auc_score = np.mean(roc_scores[y_label])
    label = '%s vs rest' % y_label
    
    pylab.grid(True)
    pylab.plot([0, 1], [0, 1], 'k--')
    # 混合行列に合わせて平均にしたいけどとりあえず全てplotしとく
    for j, tpr in enumerate(tprs[y_label]):
      pylab.plot(fprs[y_label][j], tpr)
      pylab.fill_between(fprs[y_label][j], tpr, alpha=0.5)
    pylab.xlim([0.0, 1.0])
    pylab.ylim([0.0, 1.0])
    pylab.xlabel('False Positive Rate')
    pylab.ylabel('True Positive Rate')
    pylab.title('ROC curve (AUC = %0.2f) / %s' %
                (auc_score, label), verticalalignment="bottom")
    pylab.legend(loc="lower right")
  pylab.savefig(file_name, bbox_inches="tight")

def try_model(X, y, y_labels, confusion_matrix_filename, roc_filename):
  cv = ShuffleSplit(n=len(X), n_iter=10, test_size=0.3, indices=True, random_state=0)
  
  cms = []  # 混合行列
  scores = [] # 正解率
  roc_scores = defaultdict(list)
  tprs = defaultdict(list)
  fprs = defaultdict(list)
  
  for train, test in cv:
    X_train, y_train = X[train], y[train]
    X_test,  y_test  = X[test],  y[test]
    clf = LogisticRegression()
    clf.fit(X_train, y_train)
    scores.append(clf.score(X_test, y_test))
    
    y_pred = clf.predict(X_test)
    cms.append(confusion_matrix(y_test, y_pred, y_labels))
    
    proba = clf.predict_proba(X_test)
    for i, label in enumerate(y_labels):
       y_label_test = np.asarray(y_test == label, dtype=int)
       proba_label = proba[:, i]
       
       fpr, tpr, roc_thresholds = roc_curve(y_label_test, proba_label)
       roc_scores[label].append(auc(fpr, tpr))
       tprs[label].append(tpr)
       fprs[label].append(fpr)
  
  summary = (np.mean(scores), np.std(scores))
  print "accuracy mean:%.3f\tstd:%.3f\t" % summary
  
  # 結果の生成
  save_plot_confusion_matrix(confusion_matrix_filename, np.asarray(cms), y_labels)
  save_plot_roc(roc_filename, y_labels, roc_scores, tprs, fprs)

feature_category_num = 512

images = glob('./image_*.jpg')

# 画像ごとの局所特徴量を取り出す
alldescriptors = []
for im in images:
  im = mh.imread(im, as_grey=True)
  im = im.astype(np.uint8)
  alldescriptors.append(surf.surf(im, descriptor_only=True))

# 局所特徴量からVisual Wordsを決める
concatenated = np.concatenate(alldescriptors)
km = KMeans(feature_category_num)
km.fit(concatenated)

# 各画像のbag-of-Visual Wordsを求める
features = []  # 特徴量1: surf
for d in alldescriptors:
  c = km.predict(d)
  features.append(np.array([np.sum(c == ci) for ci in range(feature_category_num)]))

haralicks = [] # 特徴量2: haralick
edginess_sobels = [] # 特徴量3: edginess_sobel
for im in images:
  im = mh.imread(im, as_grey=True)
  im = im.astype(np.uint8)
  edges = mh.sobel(im, just_filter=True)
  edges = edges.ravel()
  edginess_sobels.append([np.sqrt(np.dot(edges, edges)),1])  # 一応1いれとく
  haralicks.append(mh.features.haralick(im).mean(0))

y_labels = ['Daffodil',   'Snowdrop',  'LilyValley', 'Bluebell',   'Crocus', 
            'Iris',       'Tigerlily', 'Tulip',      'Fritillary', 'Sunflower',
            'Daisy',      'ColtsFoot', 'Dandelion',  'Cowslip',    'Buttercup',
            'Windflower', 'Pansy']
y = []
for image in images:
  n = int(image[len('./image_'):-len('.jpg')])
  n = (n - 1) / 80
  y.append(y_labels[n])

y = np.array(y)

# 1. surf
X = np.array(features)
try_model(np.array(X, dtype=int), y, y_labels, "confusion_matrix1.png", "roc1.png")
# accuracy mean:0.590    std:0.013

# 2. haralick
X = np.array(haralicks)
try_model(np.array(X, dtype=int), y, y_labels, "confusion_matrix2.png", "roc2.png")
# accuracy mean:0.169    std:0.014

# 3. edginess_sobel
X = np.array(edginess_sobels)
try_model(np.array(X, dtype=int), y, y_labels, "confusion_matrix3.png", "roc3.png")
# accuracy mean:0.092    std:0.009

# 4. surf, haralick
X = []
for i, _ in enumerate(y):
  X.append(np.concatenate((features[i], haralicks[i])))
try_model(np.array(X, dtype=int), y, y_labels, "confusion_matrix4.png", "roc4.png")
# accuracy mean:0.604    std:0.014

# 5. surf, edginess_sobel
X = []
for i, _ in enumerate(y):
  X.append(np.concatenate((features[i], edginess_sobels[i])))
try_model(np.array(X, dtype=int), y, y_labels, "confusion_matrix5.png", "roc5.png")
# accuracy mean:0.590    std:0.016

# 6. haralick, edginess_sobel
X = []
for i, _ in enumerate(y):
  X.append(np.concatenate((haralicks[i], edginess_sobels[i])))
try_model(np.array(X, dtype=int), y, y_labels, "confusion_matrix6.png", "roc6.png")
# accuracy mean:0.184    std:0.016

# 7, surf, haralick, edginess_sobel
X = []
for i, _ in enumerate(y):
  X.append(np.concatenate((features[i], haralicks[i], edginess_sobels[i])))
try_model(np.array(X, dtype=int), y, y_labels, "confusion_matrix7.png", "roc7.png")
# accuracy mean:0.604    std:0.013

思ったこととか

もうちょい前処理の工夫や特徴量の追加やパラメータのチューニングなどをすれば精度は上げられるかもしれないです。 あとはロジスティック回帰以外の分類器についても比較したりするのも面白そうに思います。 まだ遊ぶ余地はありそうです。