はじめに
別記事 CPLEXサンプル(diet.py)をWatson StudioのDecision Optimization上で動かす の続編です。
上記の記事で動作が確認されたCPLEXのコード(Python API)は、Watson Machine Learningにデプロイして、Webサービスとして呼び出すことが可能です。その手順を以下で説明します。
以下で説明するコード一式は、次のURLにアップしてあります。
(2020-09-28 Watson ML v2の仕様変更に伴い、コード修正)
対象アプリケーション
上記のものとまったく同じです。
pythonコード: model.py
CSVファイル: diet_food.csv
、diet_nutrients.csv
とdiet_food_nutrients.csv
となります。
認証情報の取得
最初にWatson MLの認証情報(localtionとapikey)を取得する必要があります。手順は、下記リンク先にあるので、そちらを参照して下さい。
なお、locationに関してはダラスのサイトの場合、決め打ちで 'us-south' になります。
space_idの取得
Watson ML v2になってから、space_idも必要になりました。手順については、下記リンク先を参照して下さい。
gzファイルの生成
Watson Machine Learningにモデルを登録するために、登録対象のコードを事前にzipないしgz形式に圧縮する必要があります。
生成用のコマンドは以下のとおりです。
export COPYFILE_DISABLE=1
tar czvf diet-model.gz main.py model.py
最初のEXPORT文はMACで圧縮をする場合に必要になります。
この環境変数設定をしないと、変なファイルもtarに入ってしまい、その後の処理でエラーになります。
もう一つのポイントは、業務ロジックが一切含まれていないmain.py
をアーカイブに含めることです。
このコードは汎用的に利用可能なので、上記のgithubからダウンロードしたものをそのままコピーして利用可能です。
main.py
はPythonコードのエントリーポイントになり、入力のcsvファイルをデータフレームに読み込み、更に辞書型変数inputs
にセットします。その後で最適化のロジックが実装されているmodel.pyを呼び出します。
また、最適化実行後の後処理も行っています。
簡易的なフレームワークのようなものだと考えてください。
実装の内容までユーザーが意識する必要はないのですが、参考までにmain.pyの中身もアップしておきます。
from functools import partial, wraps
import os
from os.path import splitext
import time
import threading
import traceback
import sys
import ntpath
import pandas
from six import iteritems
from docplex.util.environment import get_environment
output_lock = threading.Lock()
def set_stop_callback(cb):
env = get_environment()
env.abort_callbacks += [cb]
def get_all_inputs():
'''Utility method to read a list of files and return a tuple with all
read data frames.
Returns:
a map { datasetname: data frame }
'''
result = {}
env = get_environment()
for iname in [f for f in os.listdir('.') if splitext(f)[1] == '.csv']:
with env.get_input_stream(iname) as in_stream:
df = pandas.read_csv(in_stream)
datasetname, _ = splitext(iname)
result[datasetname] = df
return result
def callonce(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not wrapper.called:
wrapper.called = True
return f(*args, **kwargs)
wrapper.called = False
return wrapper
@callonce
def write_all_outputs(outputs):
'''Write all dataframes in ``outputs`` as .csv.
Args:
outputs: The map of outputs 'outputname' -> 'output df'
'''
global output_lock
with output_lock:
for (name, df) in iteritems(outputs):
csv_file = '%s.csv' % name
print(csv_file)
with get_environment().get_output_stream(csv_file) as fp:
if sys.version_info[0] < 3:
fp.write(df.to_csv(index=False, encoding='utf8'))
else:
fp.write(df.to_csv(index=False).encode(encoding='utf8'))
if len(outputs) == 0:
print("Warning: no outputs written")
def wait_and_save_all_cb(outputs):
global output_lock
# just wait for the output_lock to be available
t = time.time()
with output_lock:
pass
elapsed = time.time() - t
# write outputs
write_all_outputs(outputs)
def get_line_of_model(n):
env = get_environment()
with env.get_input_stream('model.py') as m:
lines = m.readlines()
return lines[n - 1].decode("utf-8")
class InterpreterError(Exception):
pass
if __name__ == '__main__':
inputs = get_all_inputs()
outputs = {}
set_stop_callback(partial(wait_and_save_all_cb, outputs))
env = get_environment()
# The IS_DODS env must be True for model.py if running in DODS
os.environ['IS_DODS'] = 'True'
# This allows docplex.mp to behave the same (publish kpis.csv and solution.json
# if this script is run locally
os.environ['DOCPLEX_CONTEXT'] = 'solver.auto_publish=True'
with env.get_input_stream('model.py') as m:
try:
exec(m.read().decode('utf-8'), globals())
except SyntaxError as err:
error_class = err.__class__.__name__
detail = err.args[0]
line_number = err.lineno
fileName = ntpath.basename(err.filename)
# When the error occurs in model.py, there is no err.filename
if fileName == "<string>":
fileName = "model.py"
imsg = '\nFile "' + fileName + '", line %s\n' % line_number
if err.text != None:
imsg += err.text.rstrip() + '\n'
spaces = ' ' * (err.offset - 1) if err.offset > 1 else ''
imsg += spaces + "^\n"
imsg += '%s: %s\n' % (error_class, detail)
sys.tracebacklimit = 0
raise InterpreterError(imsg)
except Exception as err:
error_class = err.__class__.__name__
detail = ""
if(len(err.args) > 0):
detail = err.args[0]
cl, exc, tb = sys.exc_info()
ttb = traceback.extract_tb(tb)
if(len(ttb) > 1):
for i in range(len(ttb)):
fileName = ntpath.basename(ttb[i][0])
line = ttb[i][3]
if(fileName == "<string>"):
fileName = "model.py"
line = get_line_of_model(ttb[i][1])
## need to use basename, otherwise we get the full path
ttb[i] = (fileName, ttb[i][1], ttb[i][2], line)
ttb = ttb[1:]
s = traceback.format_list(ttb)
imsg = '\n' + (''.join(s))
imsg += '%s: %s\n' % (error_class, detail)
sys.tracebacklimit = 0
raise InterpreterError(imsg)
else:
write_all_outputs(outputs)
モデルの登録
モデルをWatson Machine Learingに登録するためには、下記に示すml-deploy.py
を使ってください。
実行前に3箇所修正する必要があり、apikey
、location
、space_id
を事前に調べた値に設定します。
(2020-09-28追記)
モデル登録時、Webサービス登録時のMata情報の設定の方法がML v2に対応して微妙に変わっています。
(ModelMetaNames.SOFTWARE_SPEC_UID, ConfigurationMetaNames.HARDWARE_SPEC)
登録がうまくいかない場合は、このあたりをチェックして下さい。
(参考リンク)
https://medium.com/@AlainChabrier/migrate-your-python-code-for-do-in-wml-v2-instances-710025796f7
https://www.ibm.com/support/producthub/icpdata/docs/content/SSQNUZ_current/wsj/wmls/wmls-deploy-python-types.html
# -*- coding: utf-8 -*-
# コマンドによる事前準備
# $ pip install -U ibm-watson-machine-learning
# $ MACでは次が重要
# $ export COPYFILE_DISABLE=1
# $ tar czvf diet-model.gz main.py model.py
# main.py 共通に使われるmodel呼び出し用コード
# model.py DO実装コード model.builderで動作確認したもの
import sys
# Watson ML credentails
apikey = 'xxxx'
location = 'us-south'
tarfile = 'diet-model.gz'
# --------------------------------------------------------
# メインルーチン
# --------------------------------------------------------
if __name__ == '__main__':
# 引数の受け取り
argv = sys.argv
argc = len(argv)
wml_credentials = {
"apikey": apikey,
"url": 'https://' + location + '.ml.cloud.ibm.com'
}
from ibm_watson_machine_learning import APIClient
client = APIClient(wml_credentials)
client.spaces.list()
space_id = 'xxxx'
client.set.default_space(space_id)
software_spec_uid = client.software_specifications.get_uid_by_name("do_12.10")
print(software_spec_uid)
# 登録に必要な情報の設定
mdl_metadata = {
client.repository.ModelMetaNames.NAME: "Diet Python",
client.repository.ModelMetaNames.DESCRIPTION: "Diet Python",
client.repository.ModelMetaNames.TYPE: "do-docplex_12.10",
client.repository.ModelMetaNames.SOFTWARE_SPEC_UID: software_spec_uid
}
# モデルの登録
model_details = client.repository.store_model(model=tarfile, meta_props=mdl_metadata)
# モデルUIDの取得
model_uid = client.repository.get_model_uid(model_details)
print( model_uid )
# Webサービス化に必要な情報
meta_props = {
client.deployments.ConfigurationMetaNames.NAME: "Diet Python Web",
client.deployments.ConfigurationMetaNames.DESCRIPTION: "Diet Python Web",
client.deployments.ConfigurationMetaNames.BATCH: {},
client.deployments.ConfigurationMetaNames.HARDWARE_SPEC: {'name': 'S', 'nodes': 1} # S / M / XL
}
# Webサービス化
deployment_details = client.deployments.create(model_uid, meta_props=meta_props)
deployment_uid = client.deployments.get_uid(deployment_details)
print( deployment_uid )
# Webサービスの一覧表示
client.deployments.list()
このコードでは、最初のAPI呼び出しclient.repository.store_model
でモデルの登録を、次のAPI呼び出しであるclient.deployments.create
でWebサービス化をしています。
$ python ml-deploy.py
と実行すると、以下のような結果がかえってくるはずです。
779aad56-68d8-464b-89f5-2b46452b7a3d
List Models
------------------------------------ ------------------------------------- ------------------------ ----------------
GUID NAME CREATED TYPE
779aad56-68d8-464b-89f5-2b46452b7a3d DIET_PYTHON 2020-07-30T01:51:53.502Z do-docplex_12.10
03731a59-9a75-4d9a-b345-2df11f05e08d Auto generated DO docplex model 2020-07-29T12:45:50.047Z do-docplex_12.10
211f73af-6a7c-445c-a5b8-ffa526fd703f WAREHOUSE OPL 2020-07-21T04:06:37.264Z do-opl_12.10
b862c975-dc26-4b0a-94d5-06f099fb5a51 Auto generated DO opl model 2020-07-19T06:59:38.305Z do-opl_12.10
3bc5312b-88bc-4740-9302-8c80eab2e1d3 Auto generated DO docplex 12.10 model 2020-07-17T05:16:08.670Z do-docplex_12.10
#######################################################################################
Synchronous deployment creation for uid: '779aad56-68d8-464b-89f5-2b46452b7a3d' started
#######################################################################################
ready.
------------------------------------------------------------------------------------------------
Successfully finished deployment creation, deployment_uid='e3b2cf98-e81e-4619-87de-52a4a228e580'
------------------------------------------------------------------------------------------------
e3b2cf98-e81e-4619-87de-52a4a228e580
------------------------------------ ------------------------------------------ ----- ------------------------ -------------
GUID NAME STATE CREATED ARTIFACT_TYPE
e3b2cf98-e81e-4619-87de-52a4a228e580 DIET_PYTHON Deployment ready 2020-07-30T01:51:58.421Z model
13c873dc-84f4-43ff-a0c5-898b80e802ed Auto generated DO docplex deployment ready 2020-07-29T12:45:50.292Z model
cf184900-bce6-4737-bfdb-ec8902057f35 WAREHOUSE OPL Deployment ready 2020-07-21T04:06:42.233Z model
00616f70-64b0-46fb-97f1-3284bc8a6640 Auto generated DO opl deployment ready 2020-07-19T06:59:38.400Z model
ece5c71c-a9a2-4be1-8100-8b20867f13f0 Auto generated DO docplex 12.10 deployment ready 2020-07-17T05:16:08.764Z model
------------------------------------ ------------------------------------------ ----- ------------------------ -------------
モデルの呼び出し
登録ができたら、REST APIによりモデルを呼び出すことが可能です。
呼び出しコードml-submit.py
のサンプルと結果例を示します。
deployment_uidには、先ほど登録時に返ってきたIDを設定して下さい。
この場合も、apikey
、location
とspace_id
に設定が必要です。
# -*- coding: utf-8 -*-
# コマンドによる事前準備
# $ pip install -U ibm-watson-machine-learning
import sys
# Watson ML credentails
apikey = 'xxxx'
location = 'us-south'
import pandas as pd
# Watson ML credentails
# DO Deployment ID
deployment_uid = 'xxxx'
# Input CSV File
input_data1 = 'diet_food.csv'
input_data2 = 'diet_nutrients.csv'
input_data3 = 'diet_food_nutrients.csv'
# --------------------------------------------------------
# メインルーチン
# --------------------------------------------------------
if __name__ == '__main__':
# 引数の受け取り
argv = sys.argv
argc = len(argv)
wml_credentials = {
"apikey": apikey,
"url": 'https://' + location + '.ml.cloud.ibm.com'
}
from ibm_watson_machine_learning import APIClient
client = APIClient(wml_credentials)
client.spaces.list()
space_id = '20f3d4c5-1faa-4c80-a361-4da68d362b0f'
client.set.default_space(space_id)
input_df1 = pd.read_csv(input_data1)
input_df2 = pd.read_csv(input_data2)
input_df3 = pd.read_csv(input_data3)
solve_payload = {
client.deployments.DecisionOptimizationMetaNames.INPUT_DATA: [
{
"id": input_data1,
"values" : input_df1
},
{
"id": input_data2,
"values" : input_df2
},
{
"id": input_data3,
"values" : input_df3
}
],
client.deployments.DecisionOptimizationMetaNames.OUTPUT_DATA: [
{
"id":".*\.csv"
}
]
}
# DO Job 投入
job_details = client.deployments.create_job(deployment_uid, solve_payload)
job_uid = client.deployments.get_job_uid(job_details)
print( job_uid )
# status確認
from time import sleep
while job_details['entity']['decision_optimization']['status']['state'] not in ['completed', 'failed', 'canceled']:
print(job_details['entity']['decision_optimization']['status']['state'] + '...')
sleep(5)
job_details=client.deployments.get_job_details(job_uid)
detail = job_details['entity']['decision_optimization']['output_data']
# 結果確認
import json
detail2 = job_details['entity']['decision_optimization']
# 最終ステータス表示
print(json.dumps(detail2['status'], indent=2))
for item in detail:
id = item['id']
fields = item['fields']
values = item['values']
df_work = pd.DataFrame(values, columns=fields)
name = id[:id.index('.csv')]
print('name = ', name)
print(df_work.head())
df_work.to_csv(id, index=False)
呼び出しコマンド
$ python ml-submit.py
結果サンプル
Note: 'limit' is not provided. Only first 50 records will be displayed if the number of records exceed 50
------------------------------------ ------------------- ------------------------
ID NAME CREATED
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx do-space-xx 2020-09-25T08:55:05.513Z
------------------------------------ ------------------- ------------------------
04f0ecf7-57ea-494b-a372-3fb1d88101cf
queued...
queued...
queued...
queued...
running...
running...
running...
{
"completed_at": "2020-09-27T23:04:16.378Z",
"running_at": "2020-09-27T23:04:15.715Z",
"state": "completed"
}
name = kpis
Name Value
0 Total Calories 2000.000000
1 Total Calcium 800.000000
2 Total Iron 11.278318
3 Total Vit_A 8518.432542
4 Total Dietary_Fiber 25.000000
name = stats
Name Value
0 STAT.cplex.size.integerVariables 0
1 STAT.cplex.size.continousVariables 9
2 STAT.cplex.size.linearConstraints 7
3 STAT.cplex.size.booleanVariables 0
4 STAT.cplex.size.constraints 7
name = solution
name value
0 Spaghetti W/ Sauce 2.155172
1 Chocolate Chip Cookies 10.000000
2 Lowfat Milk 1.831167
3 Hotdog 0.929698