背景
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を簡単に使えるようにインストール手順も調整しました。
成果物
- GitHub: https://github.com/lucidfrontier45/prophet-nogpl
- PyPI: https://pypi.org/project/prophet-nogpl/
pip install prophet-nogpl
や poetry 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.py
にCmdStanPyBackend
というクラスがあってこれが今回の修正対象です。これの中の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.bin
とlibtbb.so.2
をダウンロードして環境変数PROPHET_MODEL_DIR_PATH
で指定したフォルダ(デフォルトは$HOME/.prophet
)に配置する関数です。load_model
はdownload_model_files
を呼び出した後にそれらのファイルを読み込むようにしています。共有ライブラリをちゃんと読み込むように_add_tbb_to_path
でLD_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")