Skip to content

Commit 0c01eb3

Browse files
authored
배포 v1.2.0
배포 v1.2.0
2 parents f0826f0 + 17cbd29 commit 0c01eb3

18 files changed

+713
-83
lines changed

.github/workflows/Deploy-ML-EC2.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,22 @@ jobs:
1515
uses: actions/checkout@v4
1616

1717
- name: Deploy to EC2
18-
uses: appleboy/[email protected]
18+
uses: appleboy/[email protected]
19+
env:
20+
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
1921
with:
2022
host: ${{ secrets.EC2_HOST_ML }}
2123
username: ${{ secrets.EC2_USER }}
2224
key: ${{ secrets.EC2_SSH_KEY }}
23-
25+
port: 22
26+
envs: OPENAI_KEY
2427
script: |
2528
# 프로젝트 디렉터리로 이동
2629
cd ~/MLOps
2730
2831
# 최신 코드 받기
2932
git pull origin main
33+
dvc pull
3034
3135
# MLOps 디렉터리로 이동
3236
cd MLOps

MLOps/app/main.py

Lines changed: 128 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,134 @@
1-
from fastapi import FastAPI, HTTPException
1+
"""
2+
MLOps 통합 API - 추천 시스템 + OpenAI 챗봇
3+
기존 추천 시스템과 새로운 OpenAI 챗봇을 하나의 API로 통합
4+
"""
5+
from fastapi import FastAPI, Request
6+
from fastapi.middleware.cors import CORSMiddleware
27
from fastapi.responses import JSONResponse
38
import traceback
4-
from .schema.recommendation_schema import ReccomendRequest, ReccomendResponse
5-
from .services.elk_client import ELKClient
6-
from .services.deepctr_service import DeepCTRService
9+
import os
10+
from datetime import datetime
11+
12+
# 라우터 임포트
13+
from app.routers import recommendation, chatbot
14+
15+
# 환경 변수 설정
16+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
717

818
app = FastAPI(
9-
title="MLOps Recommendation API - Simple",
10-
version="1.0.0",
11-
description="Place ID 기반 단순 추천 시스템"
19+
title="MLOps 통합 API",
20+
version="3.0.0",
21+
description="추천 시스템 + OpenAI 데이트 코스 챗봇 통합 서비스"
1222
)
1323

14-
# 서비스 초기화
15-
elk_client = ELKClient()
16-
deepctr_service = DeepCTRService()
17-
18-
@app.get("/api/recommend", response_model=ReccomendResponse)
19-
async def recommend_places(request: ReccomendRequest):
20-
"""Place ID 리스트 기반 추천 API"""
21-
try:
22-
top_3_place_ids, other_places = deepctr_service.rank_places_by_ctr(request.user_id, request.query)
23-
return JSONResponse(content={"top_3_place_ids": top_3_place_ids, "other_places": other_places})
24-
except Exception as e:
25-
traceback.print_exc()
26-
raise HTTPException(status_code=500, detail=str(e))
24+
# CORS 설정 (백엔드 연동용)
25+
app.add_middleware(
26+
CORSMiddleware,
27+
allow_origins=["*"],
28+
allow_credentials=True,
29+
allow_methods=["*"],
30+
allow_headers=["*"],
31+
)
32+
33+
# 전역 예외 핸들러
34+
@app.exception_handler(Exception)
35+
async def global_exception_handler(request: Request, exc: Exception):
36+
print("=================== 에러 발생 ===================")
37+
print(f"Request URL: {request.url}")
38+
print(f"에러 타입: {type(exc).__name__}")
39+
print(f"에러 메시지: {str(exc)}")
40+
traceback.print_exc()
41+
print("=============================================")
42+
return JSONResponse(
43+
status_code=500,
44+
content={"detail": f"Internal Server Error: {str(exc)}"}
45+
)
46+
47+
# 라우터 등록
48+
app.include_router(recommendation.router)
49+
app.include_router(chatbot.router)
50+
51+
@app.get("/")
52+
async def root():
53+
"""API 기본 정보"""
54+
return {
55+
"service": "MLOps 통합 API",
56+
"version": "3.0.0",
57+
"timestamp": datetime.now().isoformat(),
58+
"services": {
59+
"recommendation": {
60+
"endpoint": "/api/recommend",
61+
"description": "ELK + DeepCTR 기반 장소 추천",
62+
"status": "active" if hasattr(recommendation, 'deepctr_service') else "inactive"
63+
},
64+
"chatbot": {
65+
"endpoints": {
66+
"chat": "/api/chat",
67+
"stream": "/api/chat/stream",
68+
"stats": "/api/chat/stats"
69+
},
70+
"description": "OpenAI GPT 기반 데이트 코스 추천 챗봇",
71+
"status": "active" if hasattr(chatbot, 'openai_service') else "inactive"
72+
}
73+
},
74+
"documentation": "/docs"
75+
}
76+
77+
@app.get("/health")
78+
async def health_check():
79+
"""전체 서비스 헬스체크"""
80+
# 추천 시스템 상태 확인
81+
recommendation_status = "inactive"
82+
if hasattr(recommendation, 'deepctr_service') and recommendation.deepctr_service:
83+
recommendation_status = "active"
84+
85+
# 챗봇 상태 확인
86+
chatbot_status = "inactive"
87+
if hasattr(chatbot, 'openai_service') and chatbot.openai_service:
88+
chatbot_status = "active"
89+
90+
# 전체 상태 결정
91+
overall_status = "healthy" if (recommendation_status == "active" or chatbot_status == "active") else "unhealthy"
92+
93+
return {
94+
"status": overall_status,
95+
"timestamp": datetime.now().isoformat(),
96+
"services": {
97+
"recommendation": recommendation_status,
98+
"chatbot": chatbot_status
99+
},
100+
"active_chat_sessions": len(getattr(chatbot, 'active_sessions', {})),
101+
"version": "3.0.0"
102+
}
103+
104+
@app.get("/stats")
105+
async def get_overall_stats():
106+
"""전체 서비스 통계"""
107+
return {
108+
"api_version": "3.0.0",
109+
"services": {
110+
"recommendation": {
111+
"status": "active" if hasattr(recommendation, 'deepctr_service') else "inactive",
112+
"type": "ELK + DeepCTR"
113+
},
114+
"chatbot": {
115+
"status": "active" if hasattr(chatbot, 'openai_service') else "inactive",
116+
"type": "OpenAI GPT",
117+
"active_sessions": len(getattr(chatbot, 'active_sessions', {}))
118+
}
119+
},
120+
"endpoints": {
121+
"recommendation": ["/api/recommend", "/api/recommend/health"],
122+
"chatbot": ["/api/chat", "/api/chat/stream", "/api/chat/stats", "/api/chat/health"]
123+
},
124+
"timestamp": datetime.now().isoformat()
125+
}
126+
127+
if __name__ == "__main__":
128+
import uvicorn
129+
uvicorn.run(
130+
app,
131+
host="0.0.0.0",
132+
port=8000,
133+
log_level="info"
134+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: b09e9178a0a585a1a42af895f763c06b
3+
size: 191409
4+
hash: md5
5+
path: deepfm_model.pt

MLOps/app/model/deepfm_train.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@ class DeepFMModdelTrain:
1515
def __init__(self, data_path):
1616
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
1717
self.data = pd.read_csv(data_path)
18-
self.sparse_features = ["user_id", "user_name", "age", "gender", "place_id", "place_name","category", "sub_category"]
18+
self.sparse_features = ["userid", "name", "age", "gender", "place_id", "place_name","category", "subcategory"]
1919
self.sequence_feature = "like_list"
2020
self.linear_feature_columns = None
2121
self.dnn_feature_columns = None
2222
self.feature_names = None
2323
self.model_input = None
2424
self.target = "yn"
25-
self.model_path = "/home/ubuntu/working/MLOps/MLOps/app/model/deepfm_model.pt"
26-
self.encoders_path = "/home/ubuntu/working/MLOps/MLOps/app/model/label_encoders.pkl"
27-
self.key2index_path = "/home/ubuntu/working/MLOps/MLOps/app/model/key2index.pkl"
25+
self.model_path = "/home/ubuntu/MLOps/MLOps/app/model/deepfm_model.pt"
26+
self.encoders_path = "/home/ubuntu/MLOps/MLOps/app/model/label_encoders.pkl"
27+
self.key2index_path = "/home/ubuntu/MLOps/MLOps/app/model/key2index.pkl"
2828
self.model = None
2929
self.max_len = None
3030
self.label_encoders = {}
@@ -107,6 +107,24 @@ def predict(self, input_data):
107107
self.label_encoders = pickle.load(f)
108108
with open(self.key2index_path, 'rb') as f:
109109
self.key2index = pickle.load(f)
110+
111+
# 예측에 필요한 메타데이터 재구성
112+
temp_like_list = self.data[self.sequence_feature].apply(ast.literal_eval)
113+
self.max_len = max(len(x) for x in temp_like_list)
114+
115+
sparse_feature_names = ["userid", "name", "age", "gender", "place_id", "place_name", "category", "subcategory"]
116+
117+
reconstructed_sparse_features = [SparseFeat(feat, vocabulary_size=len(self.label_encoders[feat].classes_), embedding_dim=4)
118+
for feat in sparse_feature_names]
119+
120+
reconstructed_sequence_feature = [VarLenSparseFeat(SparseFeat(self.sequence_feature,
121+
vocabulary_size=len(self.key2index) + 1,
122+
embedding_dim=4),
123+
maxlen=self.max_len, combiner='mean')]
124+
125+
self.linear_feature_columns = reconstructed_sparse_features + reconstructed_sequence_feature
126+
self.dnn_feature_columns = reconstructed_sparse_features + reconstructed_sequence_feature
127+
self.feature_names = get_feature_names(self.linear_feature_columns + self.dnn_feature_columns)
110128

111129
# 입력 데이터를 DataFrame으로 변환
112130
if isinstance(input_data, dict):
@@ -115,13 +133,19 @@ def predict(self, input_data):
115133
input_df = input_data.copy()
116134

117135
# sparse feature 전처리
118-
sparse_feature_names = ["user_id", "user_name", "age", "gender", "place_id", "place_name","category", "sub_category"]
119136
for feature in sparse_feature_names:
120-
input_df[feature] = input_df[feature].fillna("unknown")
121-
# 학습 시 보지 못한 값은 'unknown'으로 처리
122-
input_df[feature] = input_df[feature].apply(
123-
lambda x: x if x in self.label_encoders[feature].classes_ else "unknown"
124-
)
137+
encoder = self.label_encoders[feature]
138+
known_classes = set(encoder.classes_)
139+
140+
# 'unknown'이 학습되었는지 확인
141+
unknown_in_classes = 'unknown' in known_classes
142+
143+
def transform_element(x):
144+
if pd.isna(x) or x not in known_classes:
145+
return 'unknown' if unknown_in_classes else encoder.classes_[0]
146+
return x
147+
148+
input_df[feature] = input_df[feature].apply(transform_element)
125149
input_df[feature] = self.label_encoders[feature].transform(input_df[feature])
126150

127151
# sequence feature 전처리
@@ -153,19 +177,19 @@ def encode_sequence(x):
153177
return model.predict(model_input)
154178

155179
if __name__ == "__main__":
156-
deepfm_train = DeepFMModdelTrain("/home/ubuntu/working/MLOps/data/final_click_log.csv")
180+
deepfm_train = DeepFMModdelTrain("../data/final_click_log.csv")
157181
deepfm_train.preprocess()
158182
model = deepfm_train.train()
159183
# 예시 데이터
160184
input_data = {
161-
"user_id": ["0x06fa1ba7a7e44621a2338e6093e53341", "0x6d132cda535848e295b8e489486ea841", "0x0fa0a9c4a283451181b77d91e3229c91"],
162-
"user_name": ["딩딩이", "댕댕이 언니", "에구궁"],
185+
"userid": ["0x06fa1ba7a7e44621a2338e6093e53341", "0x6d132cda535848e295b8e489486ea841", "0x0fa0a9c4a283451181b77d91e3229c91"],
186+
"name": ["딩딩이", "댕댕이 언니", "에구궁"],
163187
"age": [30, 60, 50],
164188
"gender": [1, 1, 0],
165189
"place_id": ["0xeb37b72b1fa54dc6a3867517ac2df6ef", "0x0528fbb073104d51974112a71d72b4e4", "0x1226fc5501194d2eba00383748045c20"],
166190
"place_name": ["롯데월드 쇼핑몰", "청아라 생선구이", "시골보쌈"],
167191
"category": ["쇼핑", "음식점&카페", "음식점&카페"],
168-
"sub_category": ["전문매장/상가", "한식", "한식"],
192+
"subcategory": ["전문매장/상가", "한식", "한식"],
169193
"like_list": ["[11, 12, 13, 14, 15, 16, 17, 18, 19, 20]", "[26, 22, 29, 44]", "[11, 28, 14, 29, 10, 22, 8, 25, 30]"]
170194
}
171195
prediction = deepfm_train.predict(input_data)

MLOps/app/model/key2index.pkl.dvc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: e0d99915c773ad623fb14d6e60f7b6d4
3+
size: 216
4+
hash: md5
5+
path: key2index.pkl
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 3c581e6414a751719352e4cd2c13f622
3+
size: 17428
4+
hash: md5
5+
path: label_encoders.pkl

0 commit comments

Comments
 (0)