LoginSignup
1
2

More than 1 year has passed since last update.

PythonからProphetを使用する際にGPL依存を回避する

Posted at

背景

ProphetはMeta(旧Facebook)社が開発しているOSSの時系列予測のライブラリで、ARモデルなどのように相関を持った時系列を扱うのではなくGeneralized Additive Modelという方法で各サンプルを独立なデータとして回帰モデルで扱うアプローチをとっています。

Prophetは内部ではStanという確率プログラミングのライブラリを使用していて、特にPyStanというインターフェースを使用しています。PyStanはv2系とv3系があり、Prophetは機能が削られたv3ではなくv2を使用しています。そしてこのPyStan v2はGPLライセンスです。

Prophetの開発陣もこの問題を認識しており1、すでに非GPLのCmdStanPyという別のインターフェースが試験的に使える形になっておりますが、依然としてProphetのインストール時にv2系のPyStanが強制的にインストールされていて本当にGPLを回避できているのかわからず気持ち悪いのでPyStanを必須の依存ライブラリから取り除き、CmdStanPyを簡単に使えるようにインストール手順も調整しました。

成果物

pip install prophet-nogplpoetry add prophet-nogplでインストールでき、使用するときにはprophetのままimportできます。このライブラリ自体はプラットフォーム依存はありませんが、上で書いたようにモデルファイルと共有ライブラリはLinux x86_64以外の場合は自分でビルドして$HOME/.prophet以下に配置する必要があります。コンパイルはGitHubレポジトリにあるcompile_cmdstanpy_model.pyを使用してください。

方法

成果物のprophet-nogplの内容を説明します。

プロジェクト構成

公式のレポジトリではプロジェクト構成は以下のようになっています。これをまずはそのままforkしてきます。

root/
  docs/
  examples/
  notebooks/
  python/
    prophet/
  python_shim/
  R/
  ...

root/python 以下が今回は関係する場所です。このディレクトリの下にrequirements.txtがあり、その中にがっつり

Cython>=0.22
cmdstanpy==0.9.77
pystan~=2.19.1.1
numpy>=1.15.4
pandas>=1.0.4
...

のようにPyStan v2系が指定されています。今回はこれを用いずにroot/以下にpyproject.tomlを配置し、Poetryを使用してプロジェクトを管理する方式を採用します。

[tool.poetry]
name = "prophet-nogpl"
version = "1.0.0"
description = ""
authors = ["Du Shiqiao <lucidfrontier.45@gmail.com>"]
packages = [
    { include = "prophet/*.py", from = "python"}
]

[tool.poetry.dependencies]
python = ">=3.8,<4"
tqdm = "^4.63.0"
python-dateutil = "^2.8.2"
holidays = "^0.13"
convertdate = "^2.4.0"
LunarCalendar = "^0.0.9"
pandas = "^1.4.1"
matplotlib = "^3.5.1"
cmdstanpy = "^1.0.1"
requests = "^2.27.1"

ポイントとしては [tool.poetry]packages で含めるべきprophetのディレクトリを指定し、[tool.poetry.dependencies]ではPyStanを除外しておきます。なお、公式のプロジェクトではCmdStanPyは0.9.77に固定でしたが、1.0以降でも特に問題がなかったのでそちらを使用しています。

モデルの修正

モデルは python/prophet/models.pyCmdStanPyBackendというクラスがあってこれが今回の修正対象です。これの中のload_modelというメソッドが特に重要です。以下がオリジナルの内容です。

    def load_model(self):
        import cmdstanpy
        self._add_tbb_to_path()
        model_file = pkg_resources.resource_filename(
            'prophet',
            'stan_model/prophet_model.bin',
        )
        return cmdstanpy.CmdStanModel(exe_file=model_file)

このように lib/python-<versoin>/site-packages/prophet/stan_model/prophet_model.bin という実行ファイルを指定していることがわかります。このファイルはProphetをインストールする際にsetup.py内で定義された方法でビルドされ、上記のフォルダにコピーされます。また、lddコマンドで確認するとlibtbb.so.2という共有ライブラリも必要であることがわかります。こちらはCmdStanのフォルダー内にあります。したがって、これらを事前に用意しておいてインターネット上に配置しておき、初回の実行時にダウンロードしてくるというアプローチをとることでインストール時のコンパイルの時間を削減でき、かなりストレスも低減できます。

修正版では以下のようにしました。

_model_dir_path = Path(os.environ.get("PROPHET_MODEL_DIR_PATH", default=Path.home().joinpath(".prophet")))

class CmdStanPyBackend(IStanBackend):

    def download_model_files(self):
        if (platform.system(), platform.machine()) != ("Linux", "x86_64"):
            logger.warning("only Linux x86_64 binary can be downloaded")
            logger.warning("please prepare compiled stan model binary by yourself")
            return

        import requests
        import shutil

        if not _model_dir_path.exists():
            _model_dir_path.mkdir()

        targets = {
            "libtbb.so.2": "https://github.com/lucidfrontier45/prophet-nogpl/releases/download/1.0.0/libtbb.so.2",
            "prophet_model.bin": "https://github.com/lucidfrontier45/prophet-nogpl/releases/download/1.0.0/prophet_model.bin"
        }

        for file_name, url in targets.items():
            target_path = _model_dir_path.joinpath(file_name)
            if not target_path.exists():
                logger.info(f"downloading {file_name}")
                with requests.get(url, stream=True) as r:
                    with target_path.open("wb") as f:
                        shutil.copyfileobj(r.raw, f)
                        target_path.chmod(0o755)

    def _add_tbb_to_path(self):
        """Add the TBB library to $PATH on Windows only. Required for loading model binaries."""
        if PLATFORM == "win":
            tbb_path = pkg_resources.resource_filename(
                "prophet",
                f"stan_model/cmdstan-{self.CMDSTAN_VERSION}/stan/lib/stan_math/lib/tbb"
            )
            os.environ["PATH"] = ";".join(
                list(OrderedDict.fromkeys([tbb_path] + os.environ.get("PATH", "").split(";")))
            )
        elif PLATFORM == "unix":
            tbb_path = str(_model_dir_path)
            old = os.environ.get("LD_LIBRARY_PATH")
            if old:
                os.environ["LD_LIBRARY_PATH"] = old + ":" + tbb_path
            else:
                os.environ["LD_LIBRARY_PATH"] = tbb_path    

    def load_model(self):
        self.download_model_files()
        import cmdstanpy
        self._add_tbb_to_path()
        model_file = str(_model_dir_path.joinpath("prophet_model.bin"))
        return cmdstanpy.CmdStanModel(exe_file=model_file)

download_model_filesはGitHubのリリースページに配置しておいたビルド済みのprophet_model.binlibtbb.so.2をダウンロードして環境変数PROPHET_MODEL_DIR_PATHで指定したフォルダ(デフォルトは$HOME/.prophet)に配置する関数です。load_modeldownload_model_filesを呼び出した後にそれらのファイルを読み込むようにしています。共有ライブラリをちゃんと読み込むように_add_tbb_to_pathLD_LIBRARY_PATHにも動的に追加しています。

なお、GitHubにはLinux x86_64向けのバイナリしか配置しておりません。それ以外の場合はCmdStan、CmdStanPy、C++コンパイラーをインストール後に以下のスクリプトを使用してビルドしてください。

import os.path
import shutil

import cmdstanpy

model_dir = "python/stan/unix"
model_name = "prophet.stan"
target_name = "prophet_model.bin"
sm = cmdstanpy.CmdStanModel(
    model_name=model_name,
    stan_file=os.path.join(model_dir, model_name),
    stanc_options={"O1": True},
    cpp_options={"O3": True},
)
cmdstan_path = cmdstanpy.cmdstan_path()
tbb_path = os.path.join(cmdstan_path, "stan/lib/stan_math/lib/tbb/libtbb.so.2")

shutil.copy(os.path.join(model_dir, "prophet"), target_name)
shutil.copy(tbb_path, "libtbb.so.2")
  1. https://github.com/facebook/prophet/issues/1221

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2