team-i-Five / code_Machine-learning

mumo 프로젝트 머신 러닝 코드 repo
4 stars 0 forks source link

ML 로딩 최적화 문제 #37

Open qkrtnqls1216 opened 11 months ago

qkrtnqls1216 commented 11 months ago

[문제] 웹에서 학습 데이터 전달 후 결과 값 요청 후 페이지 로딩 시간이 지연됨. ML 학습 시간이 오래 걸림.


image

image


qkrtnqls1216 commented 11 months ago

[해결 과정] 모델을 매번 학습시켜서 결과를 웹서버에 전달하는 부분에서 시간이 지체되는 것이라고 생각하여 학습된 모델 자체를 저장하면 시간을 단축시킬 수 있다는 것을 생각함.

[해결 방법] 머신러닝 모델을 저장시키는 방법인 joblib을 사용하여 학습한 모델을 저장한 후 저장된 모델을 불러와서 모델초기화 부분을 저장된 모델로 대체하여 계산을 진행

import json
import pymysql
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import NMF
from sklearn.metrics.pairwise import cosine_similarity
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
import joblib

app_nmf_future = FastAPI()

# MySQL 연결 설정 (애플리케이션 시작 시에 연결을 열고 종료 시에 닫음)
db = pymysql.connect(
    host='ifive-db.ckteh9hwnkjf.ap-northeast-2.rds.amazonaws.com',
    port=3306,
    user='admin',
    passwd='ifive1234',
    db='ifive',
    charset='utf8'
)

# MySQL 데이터베이스에서 과거 및 미래 데이터를 로드
past_sql = "SELECT * FROM musical_past"
future_sql = "SELECT * FROM musical_future"

# 데이터 로드
past_data = pd.read_sql(past_sql, db)
future_data = pd.read_sql(future_sql, db)

# MySQL 연결 닫기 (애플리케이션이 종료될 때)
db.close()

# 추가: 'synopsis_numpy_scale' 열의 데이터 타입 확인과 예시 값 출력
print("Data type of 'synopsis_numpy_scale' in past_data:", past_data['synopsis_numpy_scale'].dtype)
print("First few values of 'synopsis_numpy_scale' in past_data:", past_data['synopsis_numpy_scale'].head().tolist())

# 추가: 'synopsis_numpy_scale' 열의 데이터 타입 확인과 예시 값 출력
print("Data type of 'synopsis_numpy_scale' in future_data:", future_data['synopsis_numpy_scale'].dtype)
print("First few values of 'synopsis_numpy_scale' in future_data:", future_data['synopsis_numpy_scale'].head().tolist())

# 뮤지컬 추천을 위한 musical_id에 기반한 엔드포인트 정의
@app_nmf_future.get("/{musical_id}")
def recommend_future(musical_id: int):
    try:
        # 모델 불러오기
        loaded_nmf = joblib.load("npm_future_weights.joblib")

        # 선택한 작품의 인덱스 찾기
        selected_work_index_past = past_data[past_data['musical_id'] == musical_id].index[0]

        # 과거 데이터 파싱 및 스케일링
        past_data['synopsis_numpy_scale'] = past_data['synopsis_numpy_scale'].apply(lambda x: np.array(json.loads(x.decode('utf-8')) if isinstance(x, bytes) else x)) # bytes 형태인 경우에만 decode를 적용하도록 수정
        scaler_past = StandardScaler()
        past_data_scaled = scaler_past.fit_transform(np.vstack(past_data['synopsis_numpy_scale']))
        past_data_scaled = past_data_scaled - np.min(past_data_scaled) + 1e-10

        # 미래 작품 선택
        future_data['synopsis_numpy_scale'] = future_data['synopsis_numpy_scale'].apply(lambda x: np.array(json.loads(x.decode('utf-8')) if isinstance(x, bytes) else x))
        scaler_future = StandardScaler()
        future_data_scaled = scaler_future.fit_transform(np.vstack(future_data['synopsis_numpy_scale']))
        future_data_scaled = future_data_scaled - np.min(future_data_scaled) + 1e-10

        # 'list' 객체에 'tolist' 속성이 없는 경우를 고려하여 tolist 호출
        future_data['synopsis_numpy_scale'] = future_data['synopsis_numpy_scale'].apply(lambda x: x.tolist() if hasattr(x, 'tolist') else x if x is not None else [])

        # NMF 모델 초기화
        # nmf = NMF(n_components=10, init='random', random_state=42, max_iter=500)
        nmf = loaded_nmf  # 저장된 모델 불러오기

        # 특성 행렬 생성
        V = np.vstack(past_data_scaled)

        # NMF 모델 훈련
        W = nmf.transform(V)

        # NMF 모델 가중치 저장
        # joblib.dump(nmf, "npm_future_weights.joblib")

        # 미래 상영중인 데이터에 대한 특성 행렬 생성
        V_future = np.vstack(future_data_scaled)

        # NMF 모델을 사용하여 미래 상영중인 데이터의 특성 행렬 분해
        W_future = nmf.transform(V_future)

        # 선택한 작품과 다른 작품 간의 코사인 유사도 계산
        selected_work = W[selected_work_index_past].reshape(1, -1)
        similarities = cosine_similarity(W_future, selected_work)

        # 유사도가 높은 순서대로 정렬하여 유사한 작품 인덱스를 찾기
        similar_work_indices = similarities.argsort(axis=0)[::-1].flatten()
        top_n = min(5, len(similar_work_indices))

        # 상위 N개의 유사한 작품에 대한 정보를 추출하고 결과 리스트에 추가
        result = []
        for i in range(top_n):
            index = similar_work_indices[i]
            similarity = float(similarities[index])
            # 작품 정보 추출
            title = future_data.loc[index, 'title']
            musical_id = int(future_data.loc[index, 'musical_id'])
            # 결과 리스트에 작품 정보를 추가
            result.append({"title": title, "musical_id": musical_id, "similarity": similarity})

        return result
    except Exception as e:
        # 예외가 발생한 경우, 에러 응답을 반환
        return HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")

[ 결과화면] http://localhost:8080/recommend/future/3635 image


[해결 방법] 학습된 가중치값만을 저장하여 새로 학습될때 가중치 값을 저장된 가중치 값으로 대체하여 계산되도록 하여 이전 학습에서 얻은 특성을 보존하도록 하였더니 해결 됨. 그리고 FastAPI 애플리케이션이 요청을 처리할 때마다 모델 구성 요소를 다시 불러오는 것을 피하기 위해 저장된 모델로드하는 부분을 함수의 바깥으로 꺼냈더니 해결 됨.

import json
import pymysql
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import NMF
from sklearn.metrics.pairwise import cosine_similarity
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
import joblib

app_nmf = FastAPI()

# MySQL 연결 설정 (애플리케이션 시작 시에 연결을 열고 종료 시에 닫음)
db = pymysql.connect(
    host='ifive-db.ckteh9hwnkjf.ap-northeast-2.rds.amazonaws.com',
    port=3306,
    user='admin',
    passwd='ifive1234',
    db='ifive',
    charset='utf8'
)

# MySQL 데이터베이스에서 과거 및 현재 데이터를 로드
past_sql = "SELECT * FROM musical_past"
present_sql = "SELECT * FROM musical_present"

# 데이터 로드
past_data = pd.read_sql(past_sql, db)
present_data = pd.read_sql(present_sql, db)

# MySQL 연결 닫기 (애플리케이션이 종료될 때)
db.close()

# 추가: 'synopsis_numpy_scale' 열의 데이터 타입 확인과 예시 값 출력
print("Data type of 'synopsis_numpy_scale' in past_data:", past_data['synopsis_numpy_scale'].dtype)
print("First few values of 'synopsis_numpy_scale' in past_data:", past_data['synopsis_numpy_scale'].head().tolist())

# 추가: 'synopsis_numpy_scale' 열의 데이터 타입 확인과 예시 값 출력
print("Data type of 'synopsis_numpy_scale' in present_data:", present_data['synopsis_numpy_scale'].dtype)
print("First few values of 'synopsis_numpy_scale' in present_data:", present_data['synopsis_numpy_scale'].head().tolist())

# 가중치 저장 모델 불러오기
# 함수 밖에서 호출하는 이유
# FastAPI 애플리케이션이 요청을 처리할 때마다 모델 구성 요소를 다시 불러오는 것을 피하기 위해서이다.
loaded_nmf_components = joblib.load("npm_present_weights.joblib")

# 뮤지컬 추천을 위한 musical_id에 기반한 엔드포인트 정의
@app_nmf.get("/{musical_id}")
def recommend(musical_id: int):
    try:
        # 선택한 작품의 인덱스 찾기
        selected_work_index_past = past_data[past_data['musical_id'] == musical_id].index[0]

        # 과거 데이터 파싱 및 스케일링
        past_data['synopsis_numpy_scale'] = past_data['synopsis_numpy_scale'].apply(lambda x: np.array(json.loads(x.decode('utf-8')) if isinstance(x, bytes) else x))
        scaler_past = StandardScaler()
        past_data_scaled = scaler_past.fit_transform(np.vstack(past_data['synopsis_numpy_scale']))
        past_data_scaled = past_data_scaled - np.min(past_data_scaled) + 1e-10

        # 현재 작품 선택
        present_data['synopsis_numpy_scale'] = present_data['synopsis_numpy_scale'].apply(lambda x: np.array(json.loads(x.decode('utf-8')) if isinstance(x, bytes) else x))
        scaler_present = StandardScaler()
        present_data_scaled = scaler_present.fit_transform(np.vstack(present_data['synopsis_numpy_scale']))
        present_data_scaled = present_data_scaled - np.min(present_data_scaled) + 1e-10

        # 'list' 객체에 'tolist' 속성이 없는 경우를 고려하여 tolist 호출
        present_data['synopsis_numpy_scale'] = present_data['synopsis_numpy_scale'].apply(lambda x: x.tolist() if hasattr(x, 'tolist') else x if x is not None else [])

        # NMF 모델 초기화
        nmf = NMF(n_components=10, init='random', random_state=42, max_iter=500)
        # 미리 저장해둔 가중치 값을 새로운 NMF 모델의 가중치로 설정
        # 새로운 데이터에 대한 학습을 진행할 때 이전에 저장된 가중치 값이 초기값으로 사용되어, 이전 학습에서 얻은 특성을 보존하면서 학습
        nmf.components_ = loaded_nmf_components

        # 특성 행렬 생성
        V = np.vstack(past_data_scaled)

        # NMF 모델 훈련
        W = nmf.transform(V)

        # 학습된 모델의 가중치 값만 저장
        # joblib.dump(nmf.components_, "npm_present_weights.joblib")

        # 현재 상영중인 데이터에 대한 특성 행렬 생성
        V_present = np.vstack(present_data_scaled)

        # NMF 모델을 사용하여 현재 상영중인 데이터의 특성 행렬 분해
        W_present = nmf.transform(V_present)

        # 선택한 작품과 다른 작품 간의 코사인 유사도 계산
        selected_work = W[selected_work_index_past].reshape(1, -1)
        similarities = cosine_similarity(W_present, selected_work)

        # 유사도가 높은 순서대로 정렬하여 유사한 작품 인덱스를 찾기
        similar_work_indices = similarities.argsort(axis=0)[::-1].flatten()
        top_n = min(5, len(similar_work_indices))

        # 상위 N개의 유사한 작품에 대한 정보를 추출하고 결과 리스트에 추가
        result = []
        for i in range(top_n):
            index = similar_work_indices[i]
            similarity = float(similarities[index])
            # 작품 정보 추출
            title = present_data.loc[index, 'title']
            musical_id = int(present_data.loc[index, 'musical_id'])
            # 결과 리스트에 작품 정보를 추가
            result.append({"title": title, "musical_id": musical_id, "similarity": similarity})

        return result
    except Exception as e:
        # 예외가 발생한 경우, 에러 응답을 반환
        return HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")

[결과화면] http://localhost:8080/recommend/present/3635 image