はじめに
本連載は、WANTEDLY TECH BOOK 2から抜粋し、再編集したものになります。
こんにちは。Wantedlyでサーバサイドと機械学習サイドを扱っている丹治です。名刺管理アプリ「Wantedly People」を作っています。
Wantedly Peopleは「人工知能」と「機械学習」を用いて賢く名刺管理を行うことができます。この記事では、アプリケーションの裏側で動いている機械学習サービスについて解説していきます。
マイクロサービスとしての機械学習
私たちはマイクロサービスとしての機械学習サービスを採用しています。マイクロサービスはRailsのような大きなサーバの中に機能があるのではなく、機械学習のある独立したタスクを行うだけの小さなサーバとして開発し、運用しているものです。
これにより、以下のメリットがあります。
- 開発者の使いたい言語で開発でき、メインのロジックとは切り離して開発できる。
- 要求される速度によって柔軟にスケール可能。
- 最悪の場合サービスごと捨てることや、置き換えが可能。
もちろんこれらにはデメリットもついてきます。
1番目のメリットに対しては、みんなが好き勝手な言語で開発し始めると、誰もメンテナンスできないサービスができてしまうデメリットがあります。新しいフレームワークや言語の導入には、前もって議論することや、まずは小さく作ってみて他の開発者に見てもらうなどのプロセスが必要です。
2番目の、負荷が上がったり下がったりした際に、自由にサーバの台数をスケールできるのはマイクロサービスアーキテクチャの利点といえます。ただこれにも管理コストが大きいといった問題があります。
3番目の、サービスごと捨てたり置き換えたりすることは、実際にWantedlyの開発でも行われています。利用しているAPIがはっきりしているため、それを代替できる手段があればそのサービスを使わない選択が容易だということです。
1つのことだけをうまくやる
この小さな機械学習のサービスに求められることは以下の通りです。
- 基本的に1つのことだけを行う。
- それをうまくやる。
- 仮に1つではなくても、多くて2つ、3つにする。
これはLinuxコマンドの哲学に似ています。個人的に小さなサービスはこのように機能すべきだと考えています。
機械学習では内部がどれだけ複雑でも、エンドポイントで見ると実際は1~2個の機能のみを提供することが多いです。そのため、先述したマイクロサービスとの相性が良いと考えられます。
実際にWantedlyでは、以下のようなサービスが動いています。
- ひたすら画像を人間から見てきれいにするサービス
- 名刺の文字情報からプロフィールを構築するサービス
- 漢字から読み仮名を当てるサービス
基本的にこれらのサーバはAPIコールをする以外の依存がなく、DBなどを除いてほぼ単一サーバとして動いています。概念図を以下に示します。
ここでは、アプリが通信する際のサーバの役割を模擬的に表しています。基本的にアプリはフロントエンドサーバとのみ通信を行います。そしてフロントサーバは、必要であれば機能としてそれぞれの機械学習を担当するサーバとAPI通信を行います。個々の機械学習サーバは、そのAPIの入力に対して学習済みモデルなどから推定された答えを返すことだけが仕事になります。また、必要であればDBや外部サービスを使うこともあります。
こうして見ると管理が大変そうですが、Wantedlyではこれらの機能はサーバの台数やデプロイ、1日1回のスケジューラも含めてKubernetesなどで実現されています。
サーバ構成例
例えば、ユーザーのコメントからそのコメントがスパムコメントかどうかを当てる機械学習を作ったとします。この機械学習の機能を、マイクロサービスとして載せる例を考えてみます。
言語はPythonを選択します。scikit-learnやディープラーニングなどでPythonの推定モデルができている前提です。
また、TornadoをWebアプリケーションサーバとして採用します。Tornadoは、Facebookが買収したFriendFeedが開発し、その後オープンソースとして公開されています。ノンブロッキングI/Oを採用し、シングルスレッドかつ軽量で高速な点が特長として挙げられます。単一機能を提供するだけのサーバで、Djangoのような大きなフレームワークは必要ありません。以下はTornadoのWebページから抜粋したものです。
By using non-blocking network I/O, Tornado can scale to tens of thousands of open connections, making it ideal for long polling, WebSockets, and other applications that require a long-lived connection to each user.
以下は、シンプルな機械学習サービスのサーバ構成例です。
ここでは、与えられた文字列を正規化する(例えば「(株)ウォンテッドリー」をDBにある「Wantedly株式会社」に正規化する)機械学習のサーバを考えてみます。また、前処理としてDBにある文字列との一致度などが必要かもしれません。
% tree . ├── README.md ├── api │ ├── spam_detect_handler.py │ └── ping_handler.py ├── spam_detect │ ├── spam_detector.py │ └── response.py ├── requirements.txt ├── script │ └── server ├── server.py ├── store │ ├── wantedly_db.py └── util └── config.py
- api/ディレクトリは、Tornadoのハンドラを登録します。サーバが受け付けるエンドポイント1つに対し、基本的には1つのクラスを配置します。
- spam_detect/ディレクトリには、メインの処理を行うファイルを配置します。実際はこのディレクトリ以下に、モデルファイルや前処理を行うクラスが配置されます。
- requirements.txtには、インストールすべきPythonライブラリが記述されています。
- script/serverはサーバ起動のスクリプトです。
- store/ディレクトリはデータベースや外部のサービスと通信を行う処理を抽象化するクラスを配置します。
- util/configは環境変数や設定をまとめるものです。
これにより、後から開発に参加するメンバーはディレクトリ構成を確認するだけで、どんなエンドポイントがあるか理解しやすいメリットがあります。また、サービスを行うロジックを、サーバと外部のデータ取得が分離された形で記述することができます。
サーバのソースコード
ここからはserver.pyの中身について、シンプルにまとめたケースを想定して解説します。
機械学習のように少数の機能だけを提供するサーバの場合、あまり抽象化しすぎず、読めば中身を理解できる程度がいいと個人的には思います。
このサーバが提供するエンドポイントは/ping
と/spam_detect
だけです。
mainではデータベース接続や外部リソースに必要なConfigと、サーバを動かすコードだけがあり、server_appではエンドポイントを登録しています。
Tornadoサーバ自体はノンブロッキングI/Oでシングルスレッドですが、起動時にフォークし、マルチプロセスで並列化することができます。(実運用ではnginxなどをリバースプロキシとして使うことが推奨されています)
#!/bin/env python from optparse import OptionParser import tornado.ioloop import tornado.web import tornado.httpserver from api import ping_handler from api import spam_detect_handler from util import config def server_app(conf): application = tornado.web.Application([ (r"/ping", ping_handler.PingHandler), (r"/spam_detect", spam_detect_handler.SpamDetectHandler, dict(conf=conf)), ]) return application def main(): parser = OptionParser() parser.add_option("-p", "--port", type="int", dest="port", default=8000) options, args = parser.parse_args() conf = config.Config() server = tornado.httpserver.HTTPServer(server_app(conf)) server.bind(options.port) server.start(0) # run server in parallel processes print("LISTEN: %d" % options.port) tornado.ioloop.IOLoop.current().start() if __name__ == "__main__": main()
Tornadoは、ハンドラを作ることでサーバのエンドポイントと処理を登録できます。以下のハンドラでは、ユーザーの入力の取得・チェックを行い、モデルの推定を読み出します。結果を受け取るとそのままJSON形式で書き出しています。
import json import tornado.web from spam_detect import spam_detector from spam_detect import response class SpamDetectHandler(tornado.web.RequestHandler): def initialize(self, wtd_dict): self.wtd_dict = wtd_dict def post(self): data = json.loads(self.request.body.decode('utf-8')) res = spam_detect.SpamDetector(data).perform() self.write({"is_spam": res})
SpamDetectorの中身は以下の通りになり、実際に推定を行う処理はここに記述されます。
from spam_detect import model class SpamDetector: def __init__(self, input): self.input = input def preprocess(self): self.tokens = self.input["comment"].split("\s+") def feature_vec(self): return model.extract_feature_vec(self.tokens) def process(self, vec): return model.predict(vec) def perform(self): # 前処理 self.preprocess() # 特徴量抽出 vec = self.feature_vec() # 推定 prediction = self.process(vec) # 推定結果を0/1で返す return 1 if prediction[0] > 0 else 0
機械学習に取り組むエンジニアは、必ずしもサーバサイドの技術に精通していわけではないため、実際に運用するためにはこうした枠組みが必要になると思います。