diff --git a/.gitignore b/.gitignore
index 2eea525..867b6d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,6 @@
-.env
\ No newline at end of file
+.env
+*.pkl
+__pycache__
+tf_model/**/*
+log.txt
+experiments/**/temp/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 84a6bde..3cb791c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,10 +1,6 @@
repos:
-
- - repo: https://github.com/PyCQA/flake8
- rev: 3.9.2
+ - repo: https://github.com/psf/black
+ rev: 20.8b1
hooks:
- - id: flake8
-
-
-
-
+ - id: black
+ language_version: python3
diff --git a/README.md b/README.md
index 8ffc8f4..f70e538 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,6 @@
# MLOps
-๐ Build MLOps system step by step ๐
\ No newline at end of file
+๐ Build MLOps system step by step ๐
+
+## ๋ฌธ์
+
+- [API DOCS](./docs/api-list.md)
\ No newline at end of file
diff --git a/app/router/__init__.py b/app/api/__init__.py
similarity index 100%
rename from app/router/__init__.py
rename to app/api/__init__.py
diff --git a/app/api/router/__init__.py b/app/api/router/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/api/router/predict.py b/app/api/router/predict.py
new file mode 100644
index 0000000..4c76874
--- /dev/null
+++ b/app/api/router/predict.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+from typing import List
+
+
+import numpy as np
+from fastapi import APIRouter
+from starlette.concurrency import run_in_threadpool
+
+from app import models
+from app.api.schemas import ModelCorePrediction
+from app.database import engine
+from app.utils import ScikitLearnModel, my_model
+from logger import L
+
+
+models.Base.metadata.create_all(bind=engine)
+
+
+router = APIRouter(
+ prefix="/predict", tags=["predict"], responses={404: {"description": "Not Found"}}
+)
+
+
+@router.put("/insurance")
+async def predict_insurance(info: ModelCorePrediction, model_name: str):
+ """
+ ์ ๋ณด๋ฅผ ์
๋ ฅ๋ฐ์ ๋ณดํ๋ฃ๋ฅผ ์์ธกํ์ฌ ๋ฐํํฉ๋๋ค.
+
+ Args:
+ info(dict): ๋ค์์ ๊ฐ๋ค์ ์
๋ ฅ๋ฐ์ต๋๋ค. age(int), sex(int), bmi(float), children(int), smoker(int), region(int)
+
+ Returns:
+ insurance_fee(float): ๋ณดํ๋ฃ ์์ธก๊ฐ์
๋๋ค.
+ """
+
+ def sync_call(info, model_name):
+ """
+ none sync ํจ์๋ฅผ sync๋ก ๋ง๋ค์ด ์ฃผ๊ธฐ ์ํ ํจ์์ด๋ฉฐ ์
์ถ๋ ฅ์ ๋ถ๋ชจ ํจ์์ ๊ฐ์ต๋๋ค.
+ """
+ model = ScikitLearnModel(model_name)
+ model.load_model()
+
+ info = info.dict()
+ test_set = np.array([*info.values()]).reshape(1, -1)
+
+ pred = model.predict_target(test_set)
+ return {"result": pred.tolist()[0]}
+
+ try:
+ result = await run_in_threadpool(sync_call, info, model_name)
+ L.info(
+ f"Predict Args info: {info}\n\tmodel_name: {model_name}\n\tPrediction Result: {result}"
+ )
+ return result
+
+ except Exception as e:
+ L.error(e)
+ return {"result": "Can't predict", "error": str(e)}
+
+
+@router.put("/atmos")
+async def predict_temperature(time_series: List[float]):
+ """
+ ์จ๋ 1์๊ฐ ๊ฐ๊ฒฉ ์๊ณ์ด์ ์
๋ ฅ๋ฐ์ ์ดํ 24์๊ฐ ๋์์ ์จ๋๋ฅผ 1์๊ฐ ๊ฐ๊ฒฉ์ ์๊ณ์ด๋ก ์์ธกํฉ๋๋ค.
+
+ Args:
+ time_series(List): 72์๊ฐ ๋์์ 1์๊ฐ ๊ฐ๊ฒฉ ์จ๋ ์๊ณ์ด ์
๋๋ค. 72๊ฐ์ ์์๋ฅผ ๊ฐ์ ธ์ผ ํฉ๋๋ค.
+
+ Returns:
+ List[float]: ์
๋ ฅ๋ฐ์ ์๊ฐ ์ดํ 24์๊ฐ ๋์์ 1์๊ฐ ๊ฐ๊ฒฉ ์จ๋ ์์ธก ์๊ณ์ด ์
๋๋ค.
+ """
+ if len(time_series) != 72:
+ L.error(f"input time_series: {time_series} is not valid")
+ return {"result": "time series must have 72 values", "error": None}
+
+ def sync_pred_ts(time_series):
+ """
+ none sync ํจ์๋ฅผ sync๋ก ๋ง๋ค์ด ์ฃผ๊ธฐ ์ํ ํจ์์ด๋ฉฐ ์
์ถ๋ ฅ์ ๋ถ๋ชจ ํจ์์ ๊ฐ์ต๋๋ค.
+ """
+ time_series = np.array(time_series).reshape(1, -1, 1)
+ result = my_model.predict_target(time_series)
+ L.info(
+ f"Predict Args info: {time_series.flatten().tolist()}\n\tmodel_name: {my_model.model_name}\n\tPrediction Result: {result.tolist()[0]}"
+ )
+
+ return {"result": result, "error": None}
+
+ try:
+ result = await run_in_threadpool(sync_pred_ts, time_series)
+ return result.tolist()
+
+ except Exception as e:
+ L.error(e)
+ return {"result": "Can't predict", "error": str(e)}
diff --git a/app/api/router/train.py b/app/api/router/train.py
new file mode 100644
index 0000000..5fda7b8
--- /dev/null
+++ b/app/api/router/train.py
@@ -0,0 +1,109 @@
+import multiprocessing
+import os
+import re
+import subprocess
+
+
+from fastapi import APIRouter
+
+from app.utils import NniWatcher, ExperimentOwl, base_dir, get_free_port, write_yml
+from logger import L
+
+router = APIRouter(
+ prefix="/train", tags=["train"], responses={404: {"description": "Not Found"}}
+)
+
+
+@router.put("/insurance")
+def train_insurance(
+ experiment_name: str = "exp1",
+ experimenter: str = "DongUk",
+ model_name: str = "insurance_fee_model",
+ version: float = 0.1,
+):
+ """
+ insurance์ ๊ด๋ จ๋ ํ์ต์ ์คํํ๊ธฐ ์ํ API์
๋๋ค.
+
+ Args:
+ experiment_name (str): ์คํ์ด๋ฆ. ๊ธฐ๋ณธ ๊ฐ: exp1
+ experimenter (str): ์คํ์์ ์ด๋ฆ. ๊ธฐ๋ณธ ๊ฐ: DongUk
+ model_name (str): ๋ชจ๋ธ์ ์ด๋ฆ. ๊ธฐ๋ณธ ๊ฐ: insurance_fee_model
+ version (float): ์คํ์ ๋ฒ์ . ๊ธฐ๋ณธ ๊ฐ: 0.1
+
+ Returns:
+ msg: ์คํ ์คํ์ ์ฑ๊ณต๊ณผ ์๊ด์์ด ํฌํธ๋ฒํธ๋ฅผ ํฌํจํ NNI Dashboard์ ์ฃผ์๋ฅผ ๋ฐํํฉ๋๋ค.
+
+ Note:
+ ์คํ์ ์ต์ข
๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ์ง ์์ต๋๋ค.
+ """
+ PORT = get_free_port()
+ L.info(
+ f"Train Args info\n\texperiment_name: {experiment_name}\n\texperimenter: {experimenter}\n\tmodel_name: {model_name}\n\tversion: {version}"
+ )
+ path = "experiments/insurance/"
+ try:
+ write_yml(path, experiment_name, experimenter, model_name, version)
+ nni_create_result = subprocess.getoutput(
+ "nnictl create --port {} --config {}/{}.yml".format(PORT, path, model_name)
+ )
+ sucs_msg = "Successfully started experiment!"
+
+ if sucs_msg in nni_create_result:
+ p = re.compile(r"The experiment id is ([a-zA-Z0-9]+)\n")
+ expr_id = p.findall(nni_create_result)[0]
+ nni_watcher = NniWatcher(expr_id, experiment_name, experimenter, version)
+ m_process = multiprocessing.Process(target=nni_watcher.excute)
+ m_process.start()
+
+ L.info(nni_create_result)
+ return {"msg": nni_create_result, "error": None}
+
+ except Exception as e:
+ L.error(e)
+ return {"msg": "Can't start experiment", "error": str(e)}
+
+
+@router.put("/atmos")
+def train_atmos(expr_name: str):
+ """
+ ์จ๋ ์๊ณ์ด๊ณผ ๊ด๋ จ๋ ํ์ต์ ์คํํ๊ธฐ ์ํ API์
๋๋ค.
+
+ Args:
+ expr_name(str): NNI๊ฐ ์คํํ ์คํ์ ์ด๋ฆ ์
๋๋ค. ์ด ํ๋ผ๋ฏธํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก project_dir/experiments/[expr_name] ๊ฒฝ๋ก๋ก ์ฐพ์๊ฐ config.yml์ ์ด์ฉํ์ฌ NNI๋ฅผ ์คํํฉ๋๋ค.
+
+ Returns:
+ str: NNI์คํ์ด ์คํ๋ ๊ฒฐ๊ณผ๊ฐ์ ๋ฐํํ๊ฑฐ๋ ์คํ๊ณผ์ ์์ ๋ฐ์ํ ์๋ฌ ๋ฉ์ธ์ง๋ฅผ ๋ฐํํฉ๋๋ค.
+
+ Note:
+ ์คํ์ ์ต์ข
๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ์ง ์์ต๋๋ค.
+ """
+
+ nni_port = get_free_port()
+ expr_path = os.path.join(base_dir, "experiments", expr_name)
+
+ try:
+ nni_create_result = subprocess.getoutput(
+ "nnictl create --port {} --config {}/config.yml".format(nni_port, expr_path)
+ )
+ sucs_msg = "Successfully started experiment!"
+
+ if sucs_msg in nni_create_result:
+ p = re.compile(r"The experiment id is ([a-zA-Z0-9]+)\n")
+ expr_id = p.findall(nni_create_result)[0]
+ check_expr = ExperimentOwl(expr_id, expr_name, expr_path)
+ check_expr.add("update_tfmodeldb")
+ check_expr.add("modelfile_cleaner")
+
+ m_process = multiprocessing.Process(target=check_expr.execute)
+ m_process.start()
+
+ L.info(nni_create_result)
+ return {"msg": nni_create_result, "error": None}
+
+ else:
+ L.error(nni_create_result)
+ return {"msg": nni_create_result, "error": None}
+
+ except Exception as e:
+ L.error(e)
+ return {"msg": "Can't start experiment", "error": str(e)}
diff --git a/app/api/schemas.py b/app/api/schemas.py
new file mode 100644
index 0000000..73118ea
--- /dev/null
+++ b/app/api/schemas.py
@@ -0,0 +1,31 @@
+from pydantic import BaseModel
+
+
+class ModelCoreBase(BaseModel):
+ model_name: str
+
+
+class ModelCorePrediction(BaseModel):
+ """
+ predict_insurance API์ ์
๋ ฅ ๊ฐ ๊ฒ์ฆ์ ์ํ pydantic ํด๋์ค์
๋๋ค.
+
+ Attributes:
+ age(int)
+ sex(int)
+ bmi(float)
+ children(int)
+ smoker(int)
+ region(int)
+ """
+
+ age: int
+ sex: int
+ bmi: float
+ children: int
+ smoker: int
+ region: int
+
+
+class ModelCore(ModelCoreBase):
+ class Config:
+ orm_mode = True
diff --git a/app/database.py b/app/database.py
index 0c04917..cc2bcf7 100644
--- a/app/database.py
+++ b/app/database.py
@@ -1,23 +1,43 @@
import os
+
+from dotenv import load_dotenv
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
-from dotenv import load_dotenv
+load_dotenv(verbose=True)
def connect(db):
+ """
+ database์์ ์ฐ๊ฒฐ์ ์ํ ํจ์ ์
๋๋ค.
+
+ Args:
+ db(str): ์ฌ์ฉํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ด๋ฆ์ ์ ๋ฌ๋ฐ์ต๋๋ค.
- load_dotenv(verbose=True)
+ Returns:
+ created database engine: ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฐ๊ฒฐ๋ ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค.
+
+ Examples:
+ >>> engine = connect("my_db")
+ >>> query = "SHOW timezone;"
+ >>> engine.execute(query).fetchall()
+ [('Asia/Seoul',)]
+ >>> print(engine)
+ Engine(postgresql://postgres:***@127.0.0.1:5432/my_db)
+ """
+ print(db)
POSTGRES_USER = os.getenv("POSTGRES_USER")
POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD")
POSTGRES_PORT = os.getenv("POSTGRES_PORT")
POSTGRES_SERVER = os.getenv("POSTGRES_SERVER")
- POSTGRES_DB = db
- SQLALCHEMY_DATABASE_URL = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}\
- @{POSTGRES_SERVER}:{POSTGRES_PORT}/{POSTGRES_DB}"
+
+ SQLALCHEMY_DATABASE_URL = (
+ f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@"
+ + f"{POSTGRES_SERVER}:{POSTGRES_PORT}/{db}"
+ )
connection = create_engine(SQLALCHEMY_DATABASE_URL)
@@ -25,7 +45,6 @@ def connect(db):
POSTGRES_DB = os.getenv("POSTGRES_DB")
-
engine = connect(POSTGRES_DB)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
diff --git a/app/models.py b/app/models.py
new file mode 100644
index 0000000..efd76e5
--- /dev/null
+++ b/app/models.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+import datetime
+
+
+from sqlalchemy import Column, Integer, String, FLOAT, DateTime, ForeignKey, LargeBinary
+from sqlalchemy.sql.functions import now
+from sqlalchemy.orm import relationship
+
+from app.database import Base
+
+KST = datetime.timezone(datetime.timedelta(hours=9))
+
+
+class ModelCore(Base):
+ __tablename__ = "model_core"
+
+ model_name = Column(String, primary_key=True)
+ model_file = Column(LargeBinary, nullable=False)
+
+ model_metadata_relation = relationship(
+ "ModelMetadata", backref="model_core.model_name"
+ )
+
+
+class ModelMetadata(Base):
+ __tablename__ = "model_metadata"
+
+ experiment_name = Column(String, primary_key=True)
+ model_core_name = Column(
+ String, ForeignKey("model_core.model_name"), nullable=False
+ )
+ experimenter = Column(String, nullable=False)
+ version = Column(FLOAT)
+ train_mae = Column(FLOAT, nullable=False)
+ val_mae = Column(FLOAT, nullable=False)
+ train_mse = Column(FLOAT, nullable=False)
+ val_mse = Column(FLOAT, nullable=False)
+ created_at = Column(DateTime(timezone=True), server_default=now())
+
+
+class TempModelData(Base):
+ __tablename__ = "temp_model_data"
+
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ model_name = Column(String, nullable=False)
+ model_file = Column(LargeBinary, nullable=False)
+ experiment_name = Column(String, nullable=False)
+ experimenter = Column(String, nullable=False)
+ version = Column(FLOAT, nullable=False)
+ train_mae = Column(FLOAT, nullable=False)
+ val_mae = Column(FLOAT, nullable=False)
+ train_mse = Column(FLOAT, nullable=False)
+ val_mse = Column(FLOAT, nullable=False)
diff --git a/app/query.py b/app/query.py
new file mode 100644
index 0000000..33f6e9b
--- /dev/null
+++ b/app/query.py
@@ -0,0 +1,102 @@
+UPDATE_TEMP_MODEL_DATA = """
+ DELETE FROM temp_model_data
+ WHERE id NOT IN (
+ SELECT id
+ FROM temp_model_data
+ WHERE experiment_name = '{}'
+ ORDER BY {}
+ LIMIT {}
+ )
+ """
+
+
+SELECT_TEMP_MODEL_BY_EXPR_NAME = """
+ SELECT *
+ FROM temp_model_data
+ WHERE experiment_name = '{}'
+ ORDER BY {};
+ """
+
+
+SELECT_MODEL_METADATA_BY_EXPR_NAME = """
+ SELECT *
+ FROM model_metadata
+ WHERE experiment_name = '{}'
+ """
+
+INSERT_MODEL_CORE = """
+ INSERT INTO model_core (
+ model_name,
+ model_file
+ ) VALUES(
+ '{}',
+ '{}'
+ )
+ """
+
+INSERT_MODEL_METADATA = """
+ INSERT INTO model_metadata (
+ experiment_name,
+ model_core_name,
+ experimenter,
+ version,
+ train_mae,
+ val_mae,
+ train_mse,
+ val_mse
+ ) VALUES (
+ '{}',
+ '{}',
+ '{}',
+ '{}',
+ '{}',
+ '{}',
+ '{}',
+ '{}'
+ )
+"""
+
+UPDATE_MODEL_CORE = """
+ UPDATE model_core
+ SET
+ model_file = '{}'
+ WHERE
+ model_name = '{}'
+ """
+
+UPDATE_MODEL_METADATA = """
+ UPDATE model_metadata
+ SET
+ train_mae = {},
+ val_mae = {},
+ train_mse = {},
+ val_mse = {}
+ WHERE experiment_name = '{}'
+ """
+
+DELETE_ALL_EXPERIMENTS_BY_EXPR_NAME = """
+ DELETE FROM temp_model_data
+ WHERE experiment_name = '{}'
+"""
+
+INSERT_OR_UPDATE_MODEL = """
+UPDATE model_core
+SET model_name='{mn}', model_file='{mf}'
+WHERE model_core.model_name='{mn}';
+INSERT INTO model_core (model_name, model_file)
+SELECT '{mn}', '{mf}'
+WHERE NOT EXISTS (SELECT 1
+ FROM model_core as mc
+ WHERE mc.model_name = '{mn}');
+"""
+
+INSERT_OR_UPDATE_SCORE = """
+UPDATE atmos_model_metadata
+SET mae='{score1}', mse='{score2}'
+WHERE atmos_model_metadata.model_name='{mn}';
+INSERT INTO atmos_model_metadata (model_name, experiment_id, mae, mse)
+SELECT '{mn}', '{expr_id}', '{score1}', '{score2}'
+WHERE NOT EXISTS (SELECT 1
+ FROM atmos_model_metadata as amm
+ WHERE amm.model_name = '{mn}');
+"""
\ No newline at end of file
diff --git a/app/router/predict.py b/app/router/predict.py
deleted file mode 100644
index f60a49e..0000000
--- a/app/router/predict.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from fastapi import APIRouter
-
-router = APIRouter(
- prefix="/predict",
- tags=["predict"],
- responses={404: {"description": "Not Found"}}
-)
-
-
-@router.get("/")
-def hello_world():
- return {"message": "Hello predict"}
diff --git a/app/utils.py b/app/utils.py
new file mode 100644
index 0000000..57fa070
--- /dev/null
+++ b/app/utils.py
@@ -0,0 +1,518 @@
+import codecs
+import glob
+import io
+import multiprocessing
+import os
+import pickle
+import re
+import shutil
+import socketserver
+import subprocess
+import time
+import zipfile
+
+
+import tensorflow as tf
+import yaml
+
+from app.database import engine
+from app.query import *
+from logger import L
+
+os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
+
+base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+physical_devices = tf.config.list_physical_devices("GPU")
+if physical_devices:
+ tf.config.experimental.set_memory_growth(physical_devices[0], enable=True)
+
+
+class CoreModel:
+ """
+ predict API ํธ์ถ์ ๋ฐ์์ ๋ ์ฌ์ฉ๋ ML ๋ชจ๋ธ์ ๋ก๋ํ๋ ํด๋์ค์
๋๋ค.
+
+ Attributes:
+ model_name(str): ์์ธก์ ์คํํ ๋ชจ๋ธ์ ์ด๋ฆ
+ model(obj): ๋ชจ๋ธ์ด ์ ์ฅ๋ ์ธ์คํด์ค ๋ณ์
+ query(str): ์
๋ ฅ๋ฐ์ ๋ชจ๋ธ์ด๋ฆ์ ๊ธฐ๋ฐ์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ชจ๋ธ์ ๋ถ๋ฌ์ค๋ SQL query์
๋๋ค.
+ """
+
+ def __init__(self, model_name):
+ self.model_name = model_name
+ self.model = None
+ self.query = """
+ SELECT model_file
+ FROM model_core
+ WHERE model_name='{}';
+ """.format(
+ self.model_name
+ )
+
+ def load_model(self):
+ """
+ ๋ณธ ํด๋์ค๋ฅผ ์์๋ฐ์์ ๋ ์ด ํจ์๋ฅผ ๊ตฌํํ์ง ์์ผ๋ฉด ์์ธ๋ฅผ ๋ฐ์์ํต๋๋ค.
+ """
+ raise Exception
+
+ def predict_target(self, target_data):
+ """
+ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ถ๋ฌ์ ์ธ์คํด์ค ๋ณ์์ ์ ์ฅ๋ ๋ชจ๋ธ์ ๊ธฐ๋ฐ์ผ๋ก ์์ธก์ ์ํํฉ๋๋ค.
+
+ Args:
+ target_data: predict API ํธ์ถ ์ ์
๋ ฅ๋ฐ์ ๊ฐ์
๋๋ค. ์๋ฃํ์ ๋ชจ๋ธ์ ๋ฐ๋ผ ๋ค๋ฆ
๋๋ค.
+
+ Returns:
+ ์์ธก๋ ๊ฐ์ ๋ฐํ ํฉ๋๋ค. ์๋ฃํ์ ๋ชจ๋ธ์ ๋ฐ๋ผ ๋ค๋ฆ
๋๋ค.
+ """
+ return self.model.predict(target_data)
+
+
+class ScikitLearnModel(CoreModel):
+ """
+ Scikit learn ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ธฐ๋ฐ์ ๋ชจ๋ธ์ ๋ถ๋ฌ์ค๊ธฐ ์ํ ํด๋์ค์
๋๋ค.
+ Examples:
+ >>> sk_model = ScikitLearnModel("my_model")
+ >>> sk_model.load_model()
+ >>> sk_model.predict_target(target)
+ predict result
+ """
+
+ def __init__(self, *args):
+ super().__init__(*args)
+
+ def load_model(self):
+ """
+ ๋ชจ๋ธ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ถ๋ฌ์ ์ธ์คํด์ค ๋ณ์์ ์ ์ฅํ๋ ํจ์ ์
๋๋ค. ์์๋ฐ์ ๋ถ๋ชจํด๋์ค์ ์ธ์คํด์ค ๋ณ์๋ฅผ ์ด์ฉํ๋ฉฐ, ๋ฐํ ๊ฐ์ ์์ต๋๋ค.
+ """
+ _model = engine.execute(self.query).fetchone()
+ if _model is None:
+ raise ValueError("Model Not Found!")
+
+ self.model = pickle.loads(codecs.decode(_model[0], "base64"))
+
+
+class TensorFlowModel(CoreModel):
+ """
+ Tensorflow ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ธฐ๋ฐ์ ๋ชจ๋ธ์ ๋ถ๋ฌ์ค๊ธฐ ์ํ ํด๋์ค์
๋๋ค.
+ Examples:
+ >>> tf_model = TensorflowModel("my_model")
+ >>> tf_model.load_model()
+ >>> tf_model.predict_target(target)
+ predict result
+ """
+
+ def __init__(self, *args):
+ super().__init__(*args)
+
+ def load_model(self):
+ """
+ ๋ชจ๋ธ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ถ๋ฌ์ ์ธ์คํด์ค ๋ณ์์ ์ ์ฅํ๋ ํจ์ ์
๋๋ค. ์์๋ฐ์ ๋ถ๋ชจํด๋์ค์ ์ธ์คํด์ค ๋ณ์๋ฅผ ์ด์ฉํ๋ฉฐ, ๋ฐํ ๊ฐ์ ์์ต๋๋ค.
+ """
+ _model = engine.execute(self.query).fetchone()
+ if _model is None:
+ raise ValueError("Model Not Found!")
+ model_buffer = pickle.loads(codecs.decode(_model[0], "base64"))
+ model_path = os.path.join(base_dir, "tf_model", self.model_name)
+
+ with zipfile.ZipFile(model_buffer, "r") as bf:
+ bf.extractall(model_path)
+ self.model = tf.keras.models.load_model(model_path)
+
+
+my_model = TensorFlowModel("test_model")
+my_model.load_model()
+
+
+def write_yml(path, experiment_name, experimenter, model_name, version):
+ """
+ NNI ์คํ์ ์์ํ๊ธฐ ์ํ config.ymlํ์ผ์ ์์ฑํ๋ ํจ์ ์
๋๋ค.
+
+ Args:
+ path(str): ์คํ์ ๊ฒฝ๋ก
+ experiment_name(str): ์คํ์ ์ด๋ฆ
+ experimenter(str): ์คํ์์ ์ด๋ฆ
+ model_name(str): ๋ชจ๋ธ์ ์ด๋ฆ
+ version(float): ๋ฒ์
+
+ Returns:
+ ๋ฐํ ๊ฐ์ ์์ผ๋ฉฐ ์
๋ ฅ๋ฐ์ ๊ฒฝ๋ก๋ก ymlํ์ผ์ด ์์ฑ๋ฉ๋๋ค.
+ """
+ with open("{}/{}.yml".format(path, model_name), "w") as yml_config_file:
+ yaml.dump(
+ {
+ "authorName": f"{experimenter}",
+ "experimentName": f"{experiment_name}",
+ "trialConcurrency": 1,
+ "maxExecDuration": "1h",
+ "maxTrialNum": 10,
+ "trainingServicePlatform": "local",
+ "searchSpacePath": "search_space.json",
+ "useAnnotation": False,
+ "tuner": {
+ "builtinTunerName": "Anneal",
+ "classArgs": {"optimize_mode": "minimize"},
+ },
+ "trial": {
+ "command": "python trial.py -e {} -n {} -m {} -v {}".format(
+ experimenter, experiment_name, model_name, version
+ ),
+ "codeDir": ".",
+ },
+ },
+ yml_config_file,
+ default_flow_style=False,
+ )
+
+ yml_config_file.close()
+
+ return
+
+
+class NniWatcher:
+ """
+ experiment_id๋ฅผ ์
๋ ฅ๋ฐ์ ํด๋น id๋ฅผ ๊ฐ์ง nni ์คํ์ ๋ชจ๋ํฐ๋งํ๊ณ ๋ชจ๋ธ ํ์ผ์ ๊ด๋ฆฌํด์ฃผ๋ ํด๋์ค์
๋๋ค.
+ ์์ฑ๋๋ scikit learn ๋ชจ๋ธ์ DB์ ์์ ํ
์ด๋ธ์ ์ ์ฅํ์ฌ ์ฃผ๊ธฐ์ ์ผ๋ก ์
๋ฐ์ดํธ ํฉ๋๋ค.
+ ์ดํ ์คํ์ ๋ชจ๋ ํ๋ก์ธ์ค๊ฐ ์ข
๋ฃ๋๋ฉด ๊ฐ์ฅ ์ฑ๋ฅ์ด ์ข์ ๋ชจ๋ธ๊ณผ ์ ์๋ฅผ ์
๋ฐ์ดํธ ํฉ๋๋ค.
+
+ Attributes:
+ experiment_id(str): nni experiment๋ฅผ ์คํํ ๋ ์์ฑ๋๋ id
+ experiment_name(str): ์คํ์ ์ด๋ฆ
+ experimenter(str): ์คํ์์ ์ด๋ฆ
+ version(str): ์คํ์ ๋ฒ์
+ minute(int): ๊ฐ์ ์ฃผ๊ธฐ
+ is_kill(bool, default=True): ์คํ ๊ฐ์ํ๋ฉฐ ์คํ์ด ๋๋๋ฉด ์ข
๋ฃํ ์ง ๊ฒฐ์ ํ๋ ๋ณ์
+ top_cnt(int, default=3): ์์๋ก ์ต๋ ๋ช๊ฐ์ ์คํ์ ์ ์ฅํ ์ง ๊ฒฐ์ ํ๋ ๋ณ์
+ evaluation_criteria(str, default="val_mae"): ์ด๋ค ํ๊ฐ๊ธฐ์ค์ผ๋ก ๋ชจ๋ธ์ ์
๋ฐ์ดํธ ํ ์ง ๊ฒฐ์ ํ๋ ๋ณ์
+
+ Examples:
+ >>> watcher = NniWatcher(expr_id, experiment_name, experimenter, version)
+ >>> watcher.execute()
+ """
+
+ def __init__(
+ self,
+ experiment_id,
+ experiment_name,
+ experimenter,
+ version,
+ minute=1,
+ is_kill=True,
+ top_cnt=3,
+ evaluation_criteria="val_mae",
+ ):
+ self.experiment_id = experiment_id
+ self.experiment_name = experiment_name
+ self.experimenter = experimenter
+ self.version = version
+ self.is_kill = is_kill
+ self.top_cnt = top_cnt
+ self.evaluation_criteria = evaluation_criteria
+ self._wait_minute = minute * 20
+ self._experiment_list = None
+ self._running_experiment = None
+
+ def excute(self):
+ """
+ ๋ชจ๋ ํจ์๋ฅผ ์คํํฉ๋๋ค.
+ """
+ self.watch_process()
+ self.model_final_update()
+
+ def get_running_experiment(self):
+ """
+ ์คํ์ค์ธ ์คํ์ ๋ชฉ๋ก์ ๊ฐ์ ธ์ ์ ์ฅํฉ๋๋ค.
+ """
+ self._experiment_list = subprocess.getoutput("nnictl experiment list")
+ self._running_experiment = [
+ expr
+ for expr in self._experiment_list.split("\n")
+ if self.experiment_id in expr
+ ]
+ L.info(self._running_experiment)
+
+ def watch_process(self):
+ """
+ ์ฌ์ฉ์๊ฐ ์ง์ ํ ์๊ฐ์ ์ฃผ๊ธฐ๋ก ์คํ ํ๋ก์ธ์ค๊ฐ ์งํ ์ค์ธ์ง ๊ฐ์ํ๊ณ "DONE"์ํ๋ก ๋ณ๊ฒฝ๋๋ฉด ์คํ์ ์ข
๋ฃํฉ๋๋ค.
+ ๋ชจ๋ธ์ score๋ฅผ DB์ ์ฃผ๊ธฐ์ ์ผ๋ก ์
๋ฐ์ดํธ ํด์ค๋๋ค.
+ """
+ if self.is_kill:
+ while True:
+ self.get_running_experiment()
+ if self._running_experiment and ("DONE" in self._running_experiment[0]):
+ _stop_expr = subprocess.getoutput(
+ "nnictl stop {}".format(self.experiment_id)
+ )
+ L.info(_stop_expr)
+ break
+
+ elif self.experiment_id not in self._experiment_list:
+ L.error("Experiment ID not in Current Experiment List")
+ L.info(self._experiment_list)
+ break
+
+ else:
+ self.model_keep_update()
+ time.sleep(self._wait_minute)
+
+ def model_keep_update(self):
+ """
+ scikit learn ๋ชจ๋ธ์ ์ฑ๋ฅ์ DB์ ์
๋ฐ์ดํธ ํฉ๋๋ค.
+ """
+ engine.execute(
+ UPDATE_TEMP_MODEL_DATA.format(
+ self.experiment_name, self.evaluation_criteria, self.top_cnt
+ )
+ )
+
+ def model_final_update(self):
+ """
+ ์คํ ์ข
๋ฃ์ ์คํ๋๋ ํจ์๋ก ๋ชจ๋ธ์ ์ต์ข
์ ์์ ๋ชจ๋ธ ํ์ผ์ DB์ ์
๋ฐ์ดํธ ํด์ค๋๋ค.
+ """
+ final_result = engine.execute(
+ SELECT_TEMP_MODEL_BY_EXPR_NAME.format(
+ self.experiment_name, self.evaluation_criteria
+ )
+ ).fetchone()
+
+ saved_result = engine.execute(
+ SELECT_MODEL_METADATA_BY_EXPR_NAME.format(self.experiment_name)
+ ).fetchone()
+
+ a = pickle.loads(codecs.decode(final_result.model_file, "base64"))
+ pickled_model = codecs.encode(pickle.dumps(a), "base64").decode()
+
+ if saved_result is None:
+ engine.execute(
+ INSERT_MODEL_CORE.format(final_result.model_name, pickled_model)
+ )
+ engine.execute(
+ INSERT_MODEL_METADATA.format(
+ self.experiment_name,
+ final_result.model_name,
+ self.experimenter,
+ self.version,
+ final_result.train_mae,
+ final_result.val_mae,
+ final_result.train_mse,
+ final_result.val_mse,
+ )
+ )
+ elif (
+ saved_result[self.evaluation_criteria]
+ > final_result[self.evaluation_criteria]
+ ):
+ engine.execute(
+ UPDATE_MODEL_CORE.format(pickled_model, saved_result.model_name)
+ )
+ engine.execute(
+ UPDATE_MODEL_METADATA.format(
+ final_result.train_mae,
+ final_result.val_mae,
+ final_result.train_mse,
+ final_result.val_mse,
+ self.experiment_name,
+ )
+ )
+
+ engine.execute(DELETE_ALL_EXPERIMENTS_BY_EXPR_NAME.format(self.experiment_name))
+
+
+def zip_model(model_path):
+ """
+ ์
๋ ฅ๋ฐ์ ๋ชจ๋ธ์ ๊ฒฝ๋ก๋ฅผ ์ฐพ์๊ฐ ๋ชจ๋ธ์ ์์ถํ์ฌ ๋ฉ๋ชจ๋ฆฌ ๋ฒํผ๋ฅผ ๋ฐํํฉ๋๋ค.
+
+ Args:
+ model_path(str): ๋ชจ๋ธ์ด ์๋ ๊ฒฝ๋ก์
๋๋ค.
+
+ Returns:
+ memory buffer: ๋ชจ๋ธ์ ์์ถํ ๋ฉ๋ชจ๋ฆฌ ๋ฒํผ๋ฅผ ๋ฐํํฉ๋๋ค.
+
+ Note:
+ ๋ชจ๋ธ์ ๋์คํฌ์ ํ์ผ๋ก ์ ์ฅํ์ง ์์ต๋๋ค.
+ """
+ model_buffer = io.BytesIO()
+
+ basedir = os.path.basename(model_path)
+
+ with zipfile.ZipFile(model_buffer, "w") as zf:
+ for root, dirs, files in os.walk(model_path):
+
+ def make_arcname(x):
+ return os.path.join(root.split(basedir)[-1], x)
+
+ for dr in dirs:
+ dir_path = os.path.join(root, dr)
+ zf.write(filename=dir_path, arcname=make_arcname(dr))
+ for file in files:
+ file_path = os.path.join(root, file)
+ zf.write(filename=file_path, arcname=make_arcname(file))
+
+ return model_buffer
+
+
+def get_free_port():
+ """
+ ํธ์ถ ์ฆ์ ์ฌ์ฉ๊ฐ๋ฅํ ํฌํธ๋ฒํธ๋ฅผ ๋ฐํํฉ๋๋ค.
+
+ Returns:
+ ํ์ฌ ์ฌ์ฉ๊ฐ๋ฅํ ํฌํธ๋ฒํธ
+
+ Examples:
+ >>> avail_port = get_free_port() # ์ฌ์ฉ ๊ฐ๋ฅํ ํฌํธ, ๊ทธ๋๊ทธ๋ ๋ค๋ฆ
+ >>> print(avail_port)
+ 45675
+ """
+ with socketserver.TCPServer(("localhost", 0), None) as s:
+ free_port = s.server_address[1]
+ return free_port
+
+
+class ExperimentOwl:
+ """
+ experiment_id๋ฅผ ์
๋ ฅ๋ฐ์ ํด๋น id๋ฅผ ๊ฐ์ง nni ์คํ์ ๋ชจ๋ํฐ๋งํ๊ณ ๋ชจ๋ธ ํ์ผ์ ๊ด๋ฆฌํด์ฃผ๋ ํด๋์ค์
๋๋ค.
+ ํ์ํ ๊ธฐ๋ฅ์ instance.add("method name") ๋ฉ์๋๋ก ์ถ๊ฐํ์ฌ ์ฌ์ฉํ ์ ์์ต๋๋ค.
+
+ ํ์ฌ ๋ณด์ ํ ๊ธฐ๋ฅ
+ 1. (๊ธฐ๋ณธ)nnictl experiment list(shell command)๋ฅผ ์ฃผ๊ธฐ์ ์ผ๋ก ํธ์ถํ์ฌ ์คํ์ด ํ์ฌ ์งํ์ค์ธ์ง ํ์
ํฉ๋๋ค.
+ ์คํ์ ์ํ๊ฐ DONE์ผ๋ก ๋ณ๊ฒฝ๋๋ฉด ์ต๊ณ ์ ์ ๋ชจ๋ธ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๊ณ nnictl stop experiment_id๋ฅผ ์คํํ์ฌ ์คํ์ ์ข
๋ฃํ ํ ํ๋ก์ธ์ค๊ฐ ์ข
๋ฃ๋ฉ๋๋ค.
+
+ 2. ํ์ผ๋ก ์์ฑ๋๋ ๋ชจ๋ธ์ด ๋๋ฌด ๋ง์์ง์ง ์๋๋ก ์ ์งํฉ๋๋ค.(3๊ฐ ์ด์ ๋ชจ๋ธ์ด ์์ฑ๋๋ฉด ์ฑ๋ฅ์์ผ๋ก 3์ ๋ฏธ๋ง์ ์ญ์ ) instance ์์ฑ ์
+ mfile_manage = False๋ก ๊ธฐ๋ฅ์ ์ฌ์ฉํ์ง ์์ ์ ์์ต๋๋ค.(default True)
+
+ 3. (method) update_tfmodelbd
+ ํ
์ํ๋ก์ฐ๋ฅผ ์ด์ฉํ ์คํ ์ ์์ฑ๋๋ ๋ชจ๋ธ์ ์คํ์ด ์ข
๋ฃ๋๋ฉด DB์ ์ ์ฅํ๊ฑฐ๋ ์ ์๊ฐ ํฅ์๋์์ ์ ์
๋ฐ์ดํธ ํด์ค๋๋ค.
+
+ 4. (method) modelfile_cleaner
+ ๋ชจ๋ ์คํ์ด ์ข
๋ฃ๋๊ณ ๋ชจ๋ธ์ด ์ ์ฅ๋๋ฉด temp ํด๋์ ์๋ ๋ชจ๋ธํ์ผ๋ค์ ๋ชจ๋ ์ง์์ค๋๋ค.
+
+ Attributes:
+ experiment_id(str): nni experiment๋ฅผ ์คํํ ๋ ์์ฑ๋๋ id
+ experiment_name(str): ์คํ์ ์ด๋ฆ
+ experiment_path(str): ์คํ์ฝ๋๊ฐ ์๋ ๊ฒฝ๋ก
+ mfile_manage(bool, default=True): ์ฃผ๊ธฐ์ ์ผ๋ก ํ์ผ ์ญ์ ์ฌ๋ถ
+ time(int or float, default=5): ๊ฐ์์ฃผ๊ธฐ(๋ถ)
+
+ Examples:
+ >>> owl = ExperimentOwl(id, name, path)
+ >>> owl.add("update_tfmodeldb")
+ >>> owl.add("modelfile_cleaner")
+ >>> owl.execute()
+ """
+
+ def __init__(
+ self, experiment_id, experiment_name, experiment_path, mfile_manage=True, time=5
+ ):
+ self.__minute = 60
+ self.time = time * self.__minute
+ self.experiment_id = experiment_id
+ self.experiment_name = experiment_name
+ self.experiment_path = experiment_path
+ self.mfile_manage = mfile_manage
+ self.__func_list = [self.main]
+
+ def execute(self):
+ """
+ instance.add("method name")์ผ๋ก ์ ์ฅ๋ ๋ฉ์๋๋ค์ ์์๋๋ก ๋ชจ๋ ์คํ์ํต๋๋ค.
+ """
+ for func in self.__func_list:
+ func()
+
+ def add(self, func_name):
+ func = getattr(self, func_name)
+ self.__func_list.append(func)
+
+ def main(self):
+ """
+ ExperimentOwlํด๋์ค๋ก ์ธ์คํด์ค๋ฅผ ์์ฑ ํ ์คํ์ ๊ธฐ๋ณธ์ ์ผ๋ก ์คํ๋๋ ๊ธฐ๋ฅ์
๋๋ค.
+ ์ฌ์ฉ์๊ฐ ์ง์ ํ ์๊ฐ์ ์ฃผ๊ธฐ๋ก ์คํ ํ๋ก์ธ์ค๊ฐ ์งํ ์ค์ธ์ง ๊ฐ์ํ๊ณ "DONE"์ํ๋ก ๋ณ๊ฒฝ๋๋ฉด ์คํ์ ์ข
๋ฃํฉ๋๋ค.
+ ์ธ์คํด์ค ์์ฑ ์ mfile_manage์ต์
์ด True์ด๋ฉด ๋ชจ๋ธ ํ์ผ์ด ๋๋ฌด ๋ง์์ง์ง ์๊ฒ ์ ์ ์์๋ก 3์ ์ดํ๋ ์ญ์ ํฉ๋๋ค.(default True)
+ """
+ while True:
+ time.sleep(self.__minute)
+
+ expr_list = subprocess.getoutput("nnictl experiment list")
+
+ running_expr = [
+ expr for expr in expr_list.split("\n") if self.experiment_id in expr
+ ]
+ print(running_expr)
+ if running_expr and ("DONE" in running_expr[0]):
+ stop_expr = subprocess.getoutput(
+ "nnictl stop {}".format(self.experiment_id)
+ )
+ L.info(stop_expr)
+ break
+
+ elif self.experiment_id not in expr_list:
+ L.info(expr_list)
+ break
+
+ else:
+ if self.mfile_manage:
+ model_path = os.path.join(
+ self.experiment_path,
+ "temp",
+ "*_{}*".format(self.experiment_name),
+ )
+ exprs = glob.glob(model_path)
+ if len(exprs) > 3:
+ exprs.sort()
+ [shutil.rmtree(_) for _ in exprs[3:]]
+
+ def update_tfmodeldb(self):
+ """
+ ์คํ์ด ์ข
๋ฃ๋๋ฉด ๋ชจ๋ธ์ DB์ ์ ์ฅํ๊ฑฐ๋ ์ด๋ฏธ ๊ฐ์ ์ด๋ฆ์ ๋ชจ๋ธ์ด ์กด์ฌํ ์ ์ ์๋ฅผ ๋น๊ตํ์ฌ ์
๋ฐ์ดํธ ํฉ๋๋ค.
+ """
+ model_path = os.path.join(
+ self.experiment_path, "temp", "*_{}*".format(self.experiment_name)
+ )
+ exprs = glob.glob(model_path)
+ if not exprs:
+ return 0
+
+ exprs.sort()
+ exprs = exprs[0]
+ metrics = os.path.basename(exprs).split("_")[:2]
+ metrics = [float(metric) for metric in metrics]
+
+ score_sql = """SELECT mae
+ FROM atmos_model_metadata
+ WHERE model_name = '{}'
+ ORDER BY mae;""".format(
+ self.experiment_name
+ )
+ saved_score = engine.execute(score_sql).fetchone()
+
+ if not saved_score or (metrics[0] < saved_score[0]):
+ winner_model = os.path.join(
+ os.path.join(self.experiment_path, "temp", self.experiment_name)
+ )
+ if os.path.exists:
+ shutil.rmtree(winner_model)
+ os.rename(exprs, winner_model)
+
+ m_buffer = zip_model(winner_model)
+ encode_model = codecs.encode(pickle.dumps(m_buffer), "base64").decode()
+
+ engine.execute(
+ INSERT_OR_UPDATE_MODEL.format(mn=self.experiment_name, mf=encode_model)
+ )
+ engine.execute(
+ INSERT_OR_UPDATE_SCORE.format(
+ mn=self.experiment_name,
+ expr_id=self.experiment_id,
+ score1=metrics[0],
+ score2=metrics[1],
+ )
+ )
+ L.info("saved model %s %s" % (self.experiment_id, self.experiment_name))
+
+ def modelfile_cleaner(self):
+ """
+ temp ํด๋์ ์๋ ๋ชจ๋ ๋ชจ๋ธํ์ผ์ ์ญ์ ํฉ๋๋ค.
+ ๊ฐ์ฅ ๋ง์ง๋ง์ ์คํํ์ฌ ์ ์ฅ๋๊ณ ๋จ์ ๋ชจ๋ธํ์ผ๋ค์ ์ญ์ ํ๋ ์ฉ๋๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
+ """
+ model_path = os.path.join(self.experiment_path, "temp", "*")
+ exprs = glob.glob(model_path)
+ [shutil.rmtree(_) for _ in exprs]
diff --git a/docs/api-list.md b/docs/api-list.md
new file mode 100644
index 0000000..bd022a6
--- /dev/null
+++ b/docs/api-list.md
@@ -0,0 +1,141 @@
+# API List
+
+API_URL : 127.0.0.1
+
+- [API List](#api-list)
+ - [Train](#train)
+ - [Insurance](#insuranceํ๋ จ)
+ - [Temperature](#Temperatureํ๋ จ)
+ - [Prediction](#predict)
+ - [Insurance](#insurance์์ธก)
+ - [Temperature](#Temperature์์ธก)
+
+
+## Train
+
+### Insuranceํ๋ จ
+
+#### ์์ฒญ
+
+```
+PUT {{API_URL}}/train/insurance
+```
+
+| ํ๋ผ๋ฏธํฐ | ํ๋ผ๋ฏธํฐ ์ ํ | ๋ฐ์ดํฐ ํ์
| ํ์ ์ฌ๋ถ | ์ค๋ช
|
+| --------------- | ------------- | ----------- | --------- | ----------------------- |
+| `experiment_name` | `body` | `str` | `(default) exp1` | ํ์ต์ด๋ฆ |
+| `experimenter` | `body` | `str` | `(default) DongUk` | ์ฐ๊ตฌ์ ์ด๋ฆ |
+| `model_name` | `body` | `str` | `(default) insurance_fee_model` | ํ์ต ๋ชจ๋ธ ์ด๋ฆ |
+| `version` | `body` | `float` | `(default) 0.1` | ๋ชจ๋ธ ๋ฒ์ |
+
+
+
+
+#### ์๋ต
+
+| ํค | ๋ฐ์ดํฐ ํ์
| ์ค๋ช
|
+| -------------- | ----------- | ------------- |
+| `msg` | `string` | NNI Dashboard ์ ๋ณด |
+| `error` | `string` | ์๋ฌ๋ด์ฉ |
+
+
+```jsonc
+{
+ "msg": Info Message,
+ "error": "Error info"
+}
+```
+
+
+### Temperatureํ๋ จ
+
+#### ์์ฒญ
+
+| ํ๋ผ๋ฏธํฐ | ํ๋ผ๋ฏธํฐ ์ ํ | ๋ฐ์ดํฐ ํ์
| ํ์ ์ฌ๋ถ | ์ค๋ช
|
+| --------------- | ------------- | ----------- | --------- | ----------------------- |
+| `expr_name` | `body` | `string` | โ
| ํ์ต์ด๋ฆ |
+
+
+
+
+#### ์๋ต
+
+| ํค | ๋ฐ์ดํฐ ํ์
| ์ค๋ช
|
+| -------------- | ----------- | ------------- |
+| `msg` | `string` | NNI Dashboard ์ ๋ณด |
+| `error` | `string` | ์๋ฌ๋ด์ฉ |
+
+
+```jsonc
+{
+ "msg": Info Message,
+ "error": "Error info"
+}
+```
+
+## Predict
+
+### Insurance์์ธก
+
+#### ์์ฒญ
+
+```
+PUT {{API_URL}}/predict/insurance
+```
+
+| ํ๋ผ๋ฏธํฐ | ํ๋ผ๋ฏธํฐ ์ ํ | ๋ฐ์ดํฐ ํ์
| ํ์ ์ฌ๋ถ | ์ค๋ช
|
+| --------------- | ------------- | ----------- | --------- | ----------------------- |
+| `age` | `body` | `int` | โ
| ๋์ด |
+| `sex` | `body` | `int` | โ
| ์ฑ๋ณ |
+| `bmi` | `body` | `float` | โ
| bmi์์น |
+| `children` | `body` | `int` | โ
| ์๋
์ |
+| `smoker` | `body` | `int` | โ
| ํก์ฐ์ฌ๋ถ |
+| `region` | `body` | `int` | โ
| ๊ฑฐ์ฃผ์ง์ญ |
+
+
+
+#### ์๋ต
+
+| ํค | ๋ฐ์ดํฐ ํ์
| ์ค๋ช
|
+| -------------- | ----------- | ------------- |
+| `result` | `float` | ์์ธก๋ ๋ณดํ๋ฃ ๊ฐ |
+| `error` | `string` | ์๋ฌ๋ด์ฉ |
+
+
+```jsonc
+{
+ "result": 3213.123,
+ "error": "Error info"
+}
+```
+
+
+### Temperature์์ธก
+
+#### ์์ฒญ
+
+```
+PUT {{API_URL}}/predict/atmos
+```
+
+| ํ๋ผ๋ฏธํฐ | ํ๋ผ๋ฏธํฐ ์ ํ | ๋ฐ์ดํฐ ํ์
| ํ์ ์ฌ๋ถ | ์ค๋ช
|
+| --------------- | ------------- | ----------- | --------- | ----------------------- |
+| `time_series` | `body` | `List[float]` | โ
| 72์ผ๊ฐ์ ์จ๋๋ฐ์ดํฐ |
+
+
+
+
+#### ์๋ต
+
+| ํค | ๋ฐ์ดํฐ ํ์
| ์ค๋ช
|
+| -------------- | ----------- | ------------- |
+| `result` | `List[float]` | ์์ธก๋ ํฅํ 24์ผ๊ฐ ์จ๋๊ฐ |
+| `error` | `string` | ์๋ฌ๋ด์ฉ |
+
+
+```jsonc
+{
+ "result": [32.32, 33.32, 34.11...],
+ "error": "Error info"
+}
+```
diff --git a/experiments/atmos_tmp_01/config.yml b/experiments/atmos_tmp_01/config.yml
new file mode 100644
index 0000000..031af06
--- /dev/null
+++ b/experiments/atmos_tmp_01/config.yml
@@ -0,0 +1,19 @@
+experimentName: GRU
+searchSpaceFile: search_space.json
+trialCommand: python train.py
+trialCodeDirectory: .
+trialConcurrency: 1
+maxExperimentDuration: 2h
+maxTrialNumber: 2
+tuner:
+ # choice:
+ # TPE, Anneal, Evolution, SMAC, BatchTuner, GridSearch, Hyperband
+ # NetworkMorphism, MetisTuner, BOHB, GPTuner, PBTTuner, DNGOTuner
+ # SMAC need to be installed (pip install nni[SMAC])
+ # https://nni.readthedocs.io/en/stable/Tuner/BuiltinTuner.html#Evolution
+ name: Anneal
+ classArgs:
+ optimize_mode: minimize # maximize or minimize
+trainingService:
+ platform: local
+ useActiveGpu: True
\ No newline at end of file
diff --git a/experiments/atmos_tmp_01/preprocessing.py b/experiments/atmos_tmp_01/preprocessing.py
new file mode 100644
index 0000000..981f638
--- /dev/null
+++ b/experiments/atmos_tmp_01/preprocessing.py
@@ -0,0 +1,11 @@
+import pandas as pd
+
+
+def preprocess(data):
+
+ # missing data
+ data = data.fillna(method="ffill")
+
+ # etc.
+
+ return data
diff --git a/experiments/atmos_tmp_01/search_space.json b/experiments/atmos_tmp_01/search_space.json
new file mode 100644
index 0000000..40fdad8
--- /dev/null
+++ b/experiments/atmos_tmp_01/search_space.json
@@ -0,0 +1,4 @@
+{
+ "layer_n": {"_type":"randint", "_value":[2, 3]},
+ "cell": {"_type":"randint", "_value":[24, 30]}
+}
\ No newline at end of file
diff --git a/experiments/atmos_tmp_01/train.py b/experiments/atmos_tmp_01/train.py
new file mode 100644
index 0000000..4a541c0
--- /dev/null
+++ b/experiments/atmos_tmp_01/train.py
@@ -0,0 +1,147 @@
+import os
+import sys
+import time
+from preprocessing import preprocess
+
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+import numpy as np
+import nni
+import pandas as pd
+import tensorflow as tf
+from tensorflow import keras
+from tensorflow.keras.models import Sequential
+from tensorflow.keras.layers import Dense
+from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
+from tensorflow.keras.layers import GRU
+from sklearn.metrics import mean_absolute_error, mean_squared_error
+
+
+from expr_db import connect
+
+physical_devices = tf.config.list_physical_devices("GPU")
+if physical_devices:
+ tf.config.experimental.set_memory_growth(physical_devices[0], enable=True)
+
+
+def make_dataset(data, label, window_size=365, predsize=None):
+ feature_list = []
+ label_list = []
+
+ if isinstance(predsize, int):
+ for i in range(len(data) - (window_size + predsize)):
+ feature_list.append(np.array(data.iloc[i : i + window_size]))
+ label_list.append(
+ np.array(label.iloc[i + window_size : i + window_size + predsize])
+ )
+ else:
+ for i in range(len(data) - window_size):
+ feature_list.append(np.array(data.iloc[i : i + window_size]))
+ label_list.append(np.array(label.iloc[i + window_size]))
+
+ return np.array(feature_list), np.array(label_list)
+
+
+def split_data(data_length, ratio):
+ """
+ return index based on ratio
+ --------------------------------------------------
+ example
+
+ >>> split_data(data_length = 20, ratio = [1,2,3])
+ [3, 10]
+ --------------------------------------------------
+ """
+ ratio = np.cumsum(np.array(ratio) / np.sum(ratio))
+
+ idx = []
+ for i in ratio[:-1]:
+ idx.append(round(data_length * i))
+
+ return idx
+
+
+def main(params):
+ con = connect("postgres")
+ data = pd.read_sql("select tmp from atmos_stn108;", con)
+
+ data = preprocess(data)
+
+ train_feature, train_label = make_dataset(data, data, 72, 24)
+
+ idx = split_data(train_feature.shape[0], [6, 3, 1])
+ X_train, X_valid, X_test = (
+ train_feature[: idx[0]],
+ train_feature[idx[0] : idx[1]],
+ train_feature[idx[1] :],
+ )
+ y_train, y_valid, y_test = (
+ train_label[: idx[0]],
+ train_label[idx[0] : idx[1]],
+ train_label[idx[1] :],
+ )
+
+ model = Sequential()
+ for layer in range(params["layer_n"]):
+ if layer == params["layer_n"] - 1:
+ model.add(GRU(params["cell"]))
+ else:
+ model.add(
+ GRU(
+ params["cell"],
+ return_sequences=True,
+ input_shape=[None, train_feature.shape[2]],
+ )
+ )
+ model.add(Dense(24))
+
+ base_dir = os.path.dirname(os.path.abspath(__file__))
+ parent_dir = base_dir.split(os.path.sep)[-1]
+
+ model_path = "./"
+ model.compile(loss="mae", optimizer=keras.optimizers.Adam(lr=0.001))
+ early_stop = EarlyStopping(monitor="val_loss", patience=5)
+ expr_time = time.strftime("%y%m%d_%H%M%S")
+ model_path = os.path.join(model_path, f"./temp")
+ if not os.path.exists(model_path):
+ os.makedirs(model_path)
+
+ # ์คํ์์์๊ฐ์ ์ฌ๋ฌ ๋ชจ๋ธ๊ฐ์ ๊ตฌ๋ถ์ ์ํด ์์๋ก ๋ฃ์์ง๋ง
+ # ์ฌ๋ฌ ์์ปค๋ฅผ ๋์์ ์คํ์ํฌ ๊ฒฝ์ฐ ๊ฒน์น ์ ์์. ์ถํ ๋ณ๊ฒฝ ํ์!!
+ filename = os.path.join(model_path, f"./{parent_dir}_{expr_time}")
+ print(filename)
+ checkpoint = ModelCheckpoint(
+ filename,
+ monitor="val_loss",
+ verbose=1,
+ save_best_only=True,
+ mode="auto",
+ )
+
+ model.fit(
+ X_train,
+ y_train,
+ epochs=2,
+ batch_size=128,
+ validation_data=(X_valid, y_valid),
+ callbacks=[early_stop, checkpoint],
+ )
+
+ y_true = y_test.reshape(y_test.shape[0], y_test.shape[1])
+ y_hat = model.predict(X_test)
+
+ mae = mean_absolute_error(y_true, y_hat)
+ mse = mean_squared_error(y_true, y_hat)
+
+ src_f = os.path.join(model_path, f"./{parent_dir}_{expr_time}")
+ dst_f = os.path.join(
+ model_path, f"./{mae:.03f}_{mse:.03f}_{parent_dir}_{expr_time}"
+ )
+ os.rename(src_f, dst_f)
+
+ nni.report_final_result(mae)
+
+
+if __name__ == "__main__":
+ params = nni.get_next_parameter()
+ main(params)
diff --git a/experiments/expr_db.py b/experiments/expr_db.py
new file mode 100644
index 0000000..fc6ae08
--- /dev/null
+++ b/experiments/expr_db.py
@@ -0,0 +1,21 @@
+import os
+from dotenv import load_dotenv
+import sqlalchemy
+
+
+def connect(db="postgres"):
+ """Returns a connection and a metadata object"""
+
+ load_dotenv(verbose=True)
+
+ POSTGRES_USER = os.getenv("POSTGRES_USER")
+ POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD")
+ POSTGRES_SERVER = os.getenv("POSTGRES_SERVER")
+ POSTGRES_PORT = os.getenv("POSTGRES_PORT")
+ POSTGRES_DB = db
+
+ url = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_SERVER}:{POSTGRES_PORT}/{POSTGRES_DB}"
+
+ connection = sqlalchemy.create_engine(url)
+
+ return connection
diff --git a/experiments/insurance/config.yml b/experiments/insurance/config.yml
new file mode 100644
index 0000000..4a22ca2
--- /dev/null
+++ b/experiments/insurance/config.yml
@@ -0,0 +1,21 @@
+authorName: ehddnr
+experimentName: Lab04
+trialConcurrency: 1
+maxExecDuration: 1h
+maxTrialNum: 10
+#choice: local, remote, pai
+trainingServicePlatform: local
+#nniManagerIp:
+#choice: true, false
+searchSpacePath: search_space.json
+useAnnotation: false
+tuner:
+ #choice: TPE, Random, Anneal, Evolution, BatchTuner, MetisTuner
+ #SMAC (SMAC should be installed through nnictl)
+ builtinTunerName: Anneal
+ classArgs:
+ #choice: maximize, minimize
+ optimize_mode: minimize
+trial:
+ command: python trial.py
+ codeDir: .
\ No newline at end of file
diff --git a/experiments/insurance/query.py b/experiments/insurance/query.py
new file mode 100644
index 0000000..8476f9e
--- /dev/null
+++ b/experiments/insurance/query.py
@@ -0,0 +1,95 @@
+# insert temp
+
+INSERT_TEMP_MODEL = """
+ INSERT INTO temp_model_data (
+ model_name,
+ model_file,
+ experiment_name,
+ experimenter,
+ version,
+ train_mae,
+ val_mae,
+ train_mse,
+ val_mse
+ ) VALUES (
+ {},
+ '{}',
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {}
+ )
+"""
+
+
+# INSERT
+INSERT_MODEL_CORE = """
+ INSERT INTO model_core (
+ model_name,
+ model_file
+ ) VALUES(
+ %s,
+ '%s'
+ )
+ """
+
+INSERT_MODEL_METADATA = """
+ INSERT INTO model_metadata (
+ experiment_name,
+ model_core_name,
+ experimenter,
+ version,
+ train_mae,
+ val_mae,
+ train_mse,
+ val_mse
+ ) VALUES (
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s,
+ %s
+ )
+ """
+
+# UPDATE
+UPDATE_MODEL_METADATA = """
+ UPDATE model_metadata
+ SET
+ train_mae = %s,
+ val_mae = %s,
+ train_mse = %s,
+ val_mse = %s
+ WHERE experiment_name = %s
+ """
+UPDATE_MODEL_CORE = """
+ UPDATE model_core
+ SET
+ model_file = '%s'
+ WHERE
+ model_name = %s
+ """
+
+# pd READ_SQL
+SELECT_ALL_INSURANCE = """
+ SELECT *
+ FROM insurance
+ """
+
+SELECT_VAL_MAE = """
+ SELECT val_mae
+ FROM model_metadata
+ WHERE model_core_name = %s
+ """
+
+SELECT_MODEL_CORE = """
+ SELECT *
+ FROM model_core
+ WHERE model_name = %s
+ """
diff --git a/experiments/insurance/search_space.json b/experiments/insurance/search_space.json
new file mode 100644
index 0000000..774f778
--- /dev/null
+++ b/experiments/insurance/search_space.json
@@ -0,0 +1,6 @@
+{
+ "max_depth": {"_type":"randint", "_value":[2, 10]},
+ "colsample_bytree": {"_type":"uniform", "_value":[0.3, 1.0]},
+ "learning_rate": {"_type":"uniform", "_value":[0.001, 0.1]},
+ "n_estimators": {"_type":"randint", "_value":[100, 200]}
+}
\ No newline at end of file
diff --git a/experiments/insurance/trial.py b/experiments/insurance/trial.py
new file mode 100644
index 0000000..6c61747
--- /dev/null
+++ b/experiments/insurance/trial.py
@@ -0,0 +1,161 @@
+import codecs
+import getopt
+import os
+import pickle
+import sys
+
+
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+
+import nni
+import numpy as np
+import pandas as pd
+from dotenv import load_dotenv
+from expr_db import connect
+from query import *
+from sklearn.metrics import mean_absolute_error, mean_squared_error
+from sklearn.model_selection import KFold, train_test_split
+from sklearn.preprocessing import LabelEncoder
+from xgboost.sklearn import XGBRegressor
+
+load_dotenv(verbose=True)
+
+
+def preprocess(x_train, x_valid, col_list):
+ """
+ param:
+ x_train : train dataset dataframe
+ x_valid : validation dataset dataframe
+ col_list : columns that required for LabelEncoding
+ return:
+ tmp_x_train.values : numpy.ndarray
+ tmp_x_valid.values : numpy.ndarray
+ """
+ tmp_x_train = x_train.copy()
+ tmp_x_valid = x_valid.copy()
+
+ tmp_x_train.reset_index(drop=True, inplace=True)
+ tmp_x_valid.reset_index(drop=True, inplace=True)
+
+ encoder = LabelEncoder()
+
+ for col in col_list:
+ tmp_x_train.loc[:, col] = encoder.fit_transform(tmp_x_train.loc[:, col])
+ tmp_x_valid.loc[:, col] = encoder.transform(tmp_x_valid.loc[:, col])
+
+ return tmp_x_train.values, tmp_x_valid.values
+
+
+def main(params, engine, experiment_info, connection):
+ """
+ param:
+ params: Parameters determined by NNi
+ engine: sqlalchemy engine
+ experiment_info: information of experiment [dict]
+ connection: connection used to communicate with DB
+ """
+
+ df = pd.read_sql(SELECT_ALL_INSURANCE, engine)
+ experimenter = experiment_info["experimenter"]
+ experiment_name = experiment_info["experiment_name"]
+ model_name = experiment_info["model_name"]
+ version = experiment_info["version"]
+
+ label_col = ["sex", "smoker", "region"]
+
+ y = df.charges.to_frame()
+ x = df.iloc[:, :-1]
+
+ x_train, x_valid, y_train, y_valid = train_test_split(
+ x, y, test_size=0.2, random_state=42
+ )
+
+ kf = KFold(n_splits=5, shuffle=True, random_state=42)
+
+ cv_mse, cv_mae, tr_mse, tr_mae = [], [], [], []
+ fold_mae, fold_model = 1e10, None
+
+ for trn_idx, val_idx in kf.split(x, y):
+ x_train, y_train = x.iloc[trn_idx], y.iloc[trn_idx]
+ x_valid, y_valid = x.iloc[val_idx], y.iloc[val_idx]
+
+ # ์ ์ฒ๋ฆฌ
+ x_tra, x_val = preprocess(x_train, x_valid, label_col)
+
+ # ๋ชจ๋ธ ์ ์ ๋ฐ ํ๋ผ๋ฏธํฐ ์ ๋ฌ
+ model = XGBRegressor(**params)
+
+ # ๋ชจ๋ธ ํ์ต ๋ฐ Early Stopping ์ ์ฉ
+ model.fit(x_tra, y_train, eval_set=[(x_val, y_valid)], early_stopping_rounds=10)
+
+ y_train_pred = model.predict(x_tra)
+ y_valid_pred = model.predict(x_val)
+ # Loss ๊ณ์ฐ
+ train_mse = mean_squared_error(y_train, y_train_pred)
+ valid_mse = mean_squared_error(y_valid, y_valid_pred)
+ train_mae = mean_absolute_error(y_train, y_train_pred)
+ valid_mae = mean_absolute_error(y_valid, y_valid_pred)
+
+ cv_mse.append(valid_mse)
+ cv_mae.append(valid_mae)
+ tr_mse.append(train_mse)
+ tr_mae.append(train_mae)
+
+ new_mae = min(fold_mae, valid_mae)
+ if new_mae != fold_mae:
+ fold_model = model
+
+ cv_mse_mean = np.mean(cv_mse)
+ cv_mae_mean = np.mean(cv_mae)
+ tr_mse_mean = np.mean(tr_mse)
+ tr_mae_mean = np.mean(tr_mae)
+ pickled_model = codecs.encode(pickle.dumps(fold_model), "base64").decode()
+
+ connection.execute(
+ INSERT_TEMP_MODEL.format(
+ model_name,
+ pickled_model,
+ experiment_name,
+ experimenter,
+ version,
+ tr_mae_mean,
+ cv_mae_mean,
+ tr_mse_mean,
+ cv_mse_mean,
+ )
+ )
+
+ nni.report_final_result(cv_mae_mean)
+ print("Final result is %g", cv_mae_mean)
+ print("Send final result done.")
+
+
+if __name__ == "__main__":
+ params = nni.get_next_parameter()
+ engine = connect()
+ argv = sys.argv
+ experiment_info = {}
+
+ try:
+ opts, etc_args = getopt.getopt(
+ argv[1:],
+ "e:n:m:v:",
+ ["experimenter=", "experiment_name=", "model_name=", "version="],
+ )
+ for opt, arg in opts:
+ if opt in ("-e", "--experimenter"):
+ experiment_info["experimenter"] = f"'{arg}'"
+ elif opt in ("-n", "--experiment_name"):
+ experiment_info["experiment_name"] = f"'{arg}'"
+ elif opt in ("-m", "--model_name"):
+ experiment_info["model_name"] = f"'{arg}'"
+ elif opt in ("-v", "--version"):
+ experiment_info["version"] = arg
+
+ except getopt.GetoptError:
+ sys.exit(2)
+
+ with engine.connect() as connection:
+ with connection.begin():
+ main(params, engine, experiment_info, connection)
diff --git a/logger.py b/logger.py
new file mode 100644
index 0000000..b3f056d
--- /dev/null
+++ b/logger.py
@@ -0,0 +1,21 @@
+import logging
+import logging.handlers
+from colorlog import ColoredFormatter
+
+
+L = logging.getLogger("snowdeer_log")
+L.setLevel(logging.DEBUG)
+
+formatter = ColoredFormatter(
+ fmt="%(log_color)s [%(levelname)s] %(reset)s %(asctime)s [%(filename)s:%(lineno)d - %(funcName)20s()]\n\t%(message)s",
+ datefmt="%y-%m-%d %H:%M:%S",
+)
+
+fileHandler = logging.FileHandler("./log.txt")
+streamHandler = logging.StreamHandler()
+
+fileHandler.setFormatter(formatter)
+streamHandler.setFormatter(formatter)
+
+L.addHandler(fileHandler)
+L.addHandler(streamHandler)
diff --git a/main.py b/main.py
index 454a233..9e40cab 100644
--- a/main.py
+++ b/main.py
@@ -1,21 +1,32 @@
from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+import uvicorn
-from app.router import predict
-from app.database import SessionLocal
+
+from app.api.router import predict, train
app = FastAPI()
-app.include_router(predict.router)
+origins = ["*"]
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
-def get_db():
- db = SessionLocal()
- try:
- yield db
- finally:
- db.close()
+app.include_router(predict.router)
+app.include_router(train.router)
@app.get("/")
def hello_world():
return {"message": "Hello World"}
+
+
+if __name__ == "__main__":
+ uvicorn.run(
+ "main:app", host="0.0.0.0", port=8000, reload=True, reload_dirs=["app/"]
+ )
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..ea0763c
Binary files /dev/null and b/requirements.txt differ