やりたいこと
機械学習ではデータの前処理が重要です。
前処理には様々な手法が存在し、あるデータに対し複数の前処理を適用することも多々あります。
どの前処理をどの順番で適用すると精度がよくなるか、
試行錯誤のうちに、ソーコードがぐちゃぐちゃになってしまった経験はないでしょうか。
そんな前処理の組み合わせを、デコレータパターンで実現して、スッキリさせようという内容です。
注意
この記事を書くにあたって、デコレータを調べたらpythonには関数のデコレータという機能があるのを知りました。初心者かよ。
なので本内容は、デコレータ機能とは無関係となっております。
デコレータパターン
Javaを書いた事がある人なら、見たことがあると思います。
ファイル読み込みのときに
/// これはJava
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
こういう、オブジェクトの初期化で、直前のオブジェクトを渡すという
しちめんどくさい書き方のやつです。
こうするとbr
がファイルを読み込む際に、一旦fr
で処理して、その後にbr
の独自の処理が行われる仕組みです。
ある処理を行うオブジェクトに、次々と別の処理を追加できる(デコれる)のが、デコレータパターンです。
書き方が面倒ですが、処理の組み合わせごとにクラスを作る手間が省けるので、それなりに便利です。
前処理デコレータ
今回はデコレータパターンを応用して、
次のようなことを実現しました。
pre_processor = 前処理クラスA()
pre_processor = 前処理クラスB(pre_processor)
pre_processor = 前処理クラスC(pre_processor)
data = pre_processor(raw_data) # 前処理A -> 前処理B -> 前処理C の順で適用される。
データの前処理には嬉しいメリットが、デコレータパターンには備わっています。
メリット1. 単体でも動作
pre_processor = 前処理クラスA()
data = pre_processor(raw_data) # 前処理Aだけが適用される。
このように前処理クラス単体でも、後半のコードは何も変えなくても動作します。
メリット2. 順序を変えられる
pre_processor = 前処理クラスB()
pre_processor = 前処理クラスA(pre_processor)
pre_processor = 前処理クラスC(pre_processor)
data = pre_processor(raw_data) # 前処理B -> 前処理A -> 前処理C の順で適用される。
このように前処理オブジェクトを作る順序を変えても、後半のコードは何も変えなくても動作します。
関数で実装したときと比べて、何がうれしいのか?
クラスにしているので、関数のときよりも入出力の型を約束させることができます。
また、プロパティを与えることで前処理の実行部分で引数を渡す必要がなく、再利用性が高いです。つまり上記コードの「後半のコード」の書き換えが必要なくなる点がうれしいです。
今は初期化と同じ場所で使っているので魅力を感じないかもですが、
pre_processorを別の関数に渡して、別の場所で使うときなんかに効果を発揮します。
あとこれは本質ではありませんが、関数よりもリッチな使い方ができるのも利点です。
たとえば入力されたデータを保持するようにしておけば、前処理の各段階の状態を後から確認できるなど、色々な応用が考えられます。
Pythonによる実装
今回は処理対象のデータとしては、画像を想定しています。
ですが少しの修正でテーブルデータや音声データなど、ありとあらゆるものに対応できると思います。
画像の処理にはnumpyとcv2を使いました。
###ベースクラス
import math
from abc import abstractmethod
import numpy as np
import cv2
class BaseImagePreprocessor:
def __init__(self, parent:=None):
self._parent_processor = parent
def execute(self, image_np: np.ndarray) -> np.ndarray:
"""前処理を実行
"""
parent = self.get_parent()
if parent is not None:
image_np = parent.execute(image_np)
image_np = self._process(image_np)
return image_np
def __call__(self, image_np: np.ndarray) -> np.ndarray:
return self.execute(image_np)
def get_parent(self):
return self._parent_processor
@abstractmethod
def _process(self, image_np) -> np.ndarray:
"""独自の処理をここに書く
"""
pass
コンストラクタでparent
に、BaseImagePreprocessorオブジェクトを持たせておけば、
前処理実行execute()
で、まず先にparent
を実行するようになっています。
簡単ですね。
あとは具体的な前処理クラスを、このベースクラスから派生させて作ります。
ここでは例として、リサイズとグレースケール化の2つの実装を載せておきます。
中身はcv2を呼んでいるだけなので、大したことはしていないです。
【実装例】 リサイズ処理
class ResizePreprocessor(BaseImagePreprocessor):
def __init__(self, shape: tuple, preprocessor:BaseImagePreprocessor=None):
""" Resize
Args:
shape (tuple): リサイズ後の画像のサイズ (width, height)
"""
super().__init__(preprocessor)
self.shape = shape
def _process(self, image_np: np.ndarray) -> np.ndarray:
area_org = image_np.shape[0] * image_np.shape[1]
area_resized = self.shape[0] * self.shape[1]
if area_org > area_resized:
# 縮小
image_np = cv2.resize(image_np, self.shape, interpolation=cv2.INTER_AREA)
elif area_org < area_resized:
# 拡大
image_np = cv2.resize(image_np, self.shape, interpolation=cv2.INTER_LINEAR)
return image_np
【実装例】 グレースケール処理
class GrayScalePreprocessor(BaseImagePreprocessor):
def __init__(self, preprocessor:BaseImagePreprocessor=None):
super().__init__(preprocessor)
def _process(self, image_np: np.ndarray) -> np.ndarray:
image_np = cv2.cvtColor(image_np, cv2.COLOR_BGR2GRAY)
return image_np
BaseImagePreprocessorを継承させます。
デコレーションの処理はこのベースクラスが全部やってくれるので、
派生クラスでは前処理の方法を_process()
に書くだけでOKです。
【使用例】
# 前処理オブジェクトの用意
pre_processor = ResizePreprocessor((128, 64))
pre_processor = GrayScalePreprocessor(preprocessor=pre_processor)
raw_image = cv2.imread(fpath) # 画像読み込み
image = pre_processor(raw_image) # 前処理実行
# plot
plt.subplot(1, 2, 1, title="before")
plt.imshow(cv2.cvtColor(raw_image, cv2.COLOR_BGR2RGB))
plt.subplot(1, 2, 2, title="after")
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
plt.show()
plotにはmatplotlibを使いました。
実行結果
リサイズとグレースケール化が実行されました。
前処理の実行部分がかなりスッキリ書けたと思います。