Sansan Builders Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

【Techの道も一歩から】第25回「できる限りわかりやすく規則による前処理・後処理を記述する」

f:id:s_yuka:20190621163804j:plain

こんにちは。 DSOC R&D グループの高橋寛治です。

テキストに対して何かしらのアルゴリズムにより結果を得た際に、どうしても出力したくない項目や、少し前処理を書けば改善される、といったことがあります。 例えば機械学習の出力を調整するには、パラメータの調整や学習データの整備となかなか大変な作業が必要ですが、現場ではできる限り早く結果を提供するために、前処理や後処理を追加して対応することがよくあります。 その際に極端な話ですが、都度都度コードに if 文を追記すると、後から読むのが非常に大変になります。

今回は、わりとすっきり記述できたと感じ、運用している設計について紹介します。

設計方針

前処理、中心的な処理、後処理の3パートにわけます。 役割を挙げると次になります。

  • 前処理:テキストを入力とし、中心的な処理のためのテキストの整形を行う
  • 中心的な処理:前処理されたテキストを入力とし、機械学習モデルなどによる主目的の処理を行う
  • 後処理:中心的な処理の結果を入力とし、取捨選択などを行う

また、開発中には各処理工程でどのように変更が加わったかをログで確認できるようにします。

処理の流れを管理するクラスに対して、前処理や後処理を追加するという機構とします。 前処理や後処理は基とするクラスを作成し、それを継承することでインターフェイスを統一します。 ルールを一つ足すには、クラスとして命名する必要が出てくるため、わかりやすさを確保するための一つの項目となります。

実装の具体例

手っ取り早く全体を知りたいというかたは、こちらをご確認ください。

ロギングに logzero と呼ばれるライブラリを利用していますので、 pip install logzero で適宜インストールしてください。

次のような出力が経過として見られるように実装を進めます。

[I 200212 16:11:30 sample:15] {'Description': 'sample', 'Preprocessing': ['UnicodeNormalizer'], 'Extractors': ['DummyExtractor'], 'Postprocessing': ['StopwordsFilter']}
[D 200212 16:11:30 worker:30] ["これはテストです。"]
[D 200212 16:11:30 worker:33] UnicodeNormalizer:["これはテストです。"]
[D 200212 16:11:30 worker:26] Candidates:[{"name": "HOGE", "algorithm": "dummy"}, {"name": "FUGA", "algorithm": "dummy"}]
[D 200212 16:11:30 worker:46] StopwordsFilter:[{"name": "FUGA", "algorithm": "dummy"}]
[I 200212 16:11:30 sample:16] [<Candidate(name=FUGA)>]

処理全体を管理するクラス

前処理、中心的な処理、後処理を管理するクラスを作成します。 行数が長くなってしまうため、前処理部分を抜粋して説明します。

前処理をリストとして管理します。 実際に処理を適用する際には、 process メソッドを実行します。 つまり、 前処理は process メソッドを持ち、文リストを入力し、それぞれの文に対して処理し、文リストを出力するという役割を持たせます。

利用者側は、前処理に使いたい処理を add_preprocessor メソッドで追加します。

class ExtractorWorker:
    def __init__(self, description=""):
        self.preprocessors = []
        ...

    def add_preprocessor(self, preprocessor):
        self.preprocessors.append(preprocessor)

    def _preprocess(self, sentences):
        logger.debug(json.dumps(sentences, ensure_ascii=False))
        for processor in self.preprocessors:
            sentences = processor.process(sentences)
            logger.debug(str(processor) + ":" + json.dumps(sentences, ensure_ascii=False))
        return sentences

    def extract(self, text):
        sentences = self._preprocess([text])
        ... 続く

前処理クラス

メインの処理、後処理も同様ですので、前処理クラスを抜粋して説明します。

前処理のインターフェイスを決めるために、基底クラスを定義します。 具体的な処理は継承した子クラスで定義します。

今回は単純なユニコード正規化を前処理として実装します。

import unicodedata
from abc import ABC, abstractmethod


class Preprocessor(ABC):
    @abstractmethod
    def process(self, text):
        pass

    def __repr__(self):
        return self.__class__.__name__


class UnicodeNormalizer(Preprocessor):
    def process(self, sentences):
        return [unicodedata.normalize("NFKC", sentence) for sentence in sentences]

利用する

メイン処理、後処理はこちらにあるため、同じフォルダにダウンロードしてください。 実際に動かしてみましょう。

from logzero import logger
from extractor import DummyExtractor
from preprocessor import UnicodeNormalizer
from postprocessor import StopwordsFilter
from worker import ExtractorWorker


if __name__ == "__main__":
    extractor = ExtractorWorker(description="sample")

    extractor.add_preprocessor(UnicodeNormalizer())
    extractor.add_extractor(DummyExtractor())
    extractor.add_postprocessor(StopwordsFilter())

    logger.info(extractor.get_steps())

    logger.info(extractor.extract("これはテストです。"))

必要なモジュールを読み込んだ後に、各処理を追加します。 logger.info(extractor.get_steps()) では処理の流れを出力します。

[I 200212 16:11:30 sample:15] {'Description': 'sample', 'Preprocessing': ['UnicodeNormalizer'], 'Extractors': ['DummyExtractor'], 'Postprocessing': ['StopwordsFilter']}

logger.info(extractor.extract("これはテストです。")) で実際に処理をします。 処理経過は仕込んでおいたログで閲覧できます。

[D 200212 16:11:30 worker:30] ["これはテストです。"]
[D 200212 16:11:30 worker:33] UnicodeNormalizer:["これはテストです。"]
[D 200212 16:11:30 worker:26] Candidates:[{"name": "HOGE", "algorithm": "dummy"}, {"name": "FUGA", "algorithm": "dummy"}]
[D 200212 16:11:30 worker:46] StopwordsFilter:[{"name": "FUGA", "algorithm": "dummy"}]

処理結果は次のような出力で確認することができます。

[I 200212 16:11:30 sample:16] [<Candidate(name=FUGA)>]

このように、出力に至る経過を詳細かつ簡単な記述で確認することができます。 前処理や後処理を追加する場合は、リストに追加するのみです。

簡易化して記述したため、例えば実際に利用する際には、解析結果に基となる文を参照できるように埋め込んだり、その他確信度や何かフラグを用意するようになります。

このコードだけでは依存関係は管理できませんが、処理を個別に分けられるような場合には強力な方法となるのではないでしょうか。

メンテナンスしやすいように

ちょっとした調整の要望が来た際に、メンテナンスしやすいようにしておくとすぐさま対応できますし、処理がきちんと動いていることを確認することができます。 記事中では触れませんでしたが、処理を一つずつ分けることで、単体テストも容易となります。

依存関係をうまく取り扱う方法を模索していきたいです。

▼本連載のほかの記事はこちら

buildersbox.corp-sansan.com

執筆者プロフィール

高橋寛治 Sansan株式会社 DSOC (Data Strategy & Operation Center) R&Dグループ研究員

阿南工業高等専門学校卒業後に、長岡技術科学大学に編入学。同大学大学院電気電子情報工学専攻修了。在学中は、自然言語処理の研究に取り組み、解析ツールの開発や機械翻訳に関連する研究を行う。大学院を卒業後、2017年にSansan株式会社に入社。キーワード抽出など自然言語処理を生かした研究開発に取り組む。

© Sansan, Inc.