[Project] Kor-DEEPression - (5) Deployment & Conclusion

Kyung Jae, Cheong·2023년 2월 20일
0

개인프로젝트

목록 보기
17/17
post-thumbnail
post-custom-banner

한국형 우울증 딥러닝 예측 모델 및 진단 프로그램 "Kor-DEEPression" 개발 과정 정리 및 회고.
(5) Deployment & Conclusion (프로그램 배포 및 결론)


Kor-DEEPression

  • 한국형 우울증 딥러닝 예측 모델 및 진단 프로그램
  • Korean Depression Deep-Learning Model and Diagnosis Program

Project Information

  • 프로젝트명 : Kor-DEEPression
    • 한국형 우울증 딥러닝 예측 모델 및 진단 프로그램 개발
    • Development of Korean Depression Deep-Learning Model and Diagnosis Program
  • Codestates AI Bootcamp CP2 project
    • 자유주제 개인 프로젝트
  • Full-Stack Deep-Learning Project
    • DS (Data Science)
      • Machine Learning & Deep Learning 모델링
      • 모델 성능 평가 및 모델 개선
      • 모델 경량화 (tensorflow-lite)
    • DA (Data Analysis)
      • EDA 및 시각화 분석
      • Trend Dashboard 구현 (Looker-Studio)
    • DE (Data Engineering)
      • Back-end : Cloud DB 구축, 프로그램 Flask 배포
      • Front-end : Web page 제작 (HTML5, CSS3)

Process Pipeline 파이프라인

Outline & Intro. 개요 및 서론


5. Deployment 배포

(Part5) Deployment 배포

  • Web page Design 웹페이지 제작
  • Flask app Deployment Flask 앱 배포
  • Webpage Screenshots 웹페이지 스크린샷

5-1. Web page Design 웹페이지 제작

  • 위 슬라이드는 1차 개발 완료 당시의 디렉토리 구성과 웹 배포 과정을 담고 있으며, 피드백과 수정을 거친 최종 버전의 경우엔 다음 파트에서 다시 자세히 다루도록 하겠음.

  • 우선 웹페이지 디자인의 기본적인 틀은 기획 단계에서 구현 가능 범위를 확인할 때 만들어 두었었고, 웹페이지는 Bootstrap을 기반으로 HTML5와 CSS3를 직접 코딩함으로써 디자인 하였음.

  • 폰트는 가독성을 최대화 하기 위해 한글을 제외한 영문과 숫자는 Google Fonts의 'Open Sans' 글씨체를 이용했고, 한글의 경우엔 'Noto Sans KR' 글씨체를 이용하였음

<!-- 기본 틀 -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- 위에 두개 meta 태그는 반드시 맨 처음에 와야함! -->
  <title>Kor-DEEPression</title>
  <meta name="description" content="AI 딥러닝 기반 한국형 우울증 예측 프로젝트">
  <meta name="keywords" content="AI, Deep Learning, Depression">
  <meta name="author" content="KyungJae Cheong">
  <!-- Open Graph 설정 -->
  <meta property="og:title" content="Kor-DEEPression">
  <meta property="og:description" content="AI 딥러닝 기반 한국형 우울증 예측 프로젝트">
  <meta property="og:image" content="{{ url_for('static', filename='imgs/og-image.png') }}">
  <!-- Icons는 모든 기기에서 표시되도록 설정하여 너무 길어져서 생략 -->
  아이콘들이 들어갈 자리
  <!-- Bootstrap core CSS (디자인 템플릿) -->
  <link rel="stylesheet" href="../static/css/bootstrap.min.css">
  <!-- Custom styles CSS (폰트 및 스타일시트 설정) -->
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700,800">
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Noto+Sans+KR:100,300,400,500,700,900">
  <link rel="stylesheet" href="../static/css/custom_styles.css">
</head>
<body>
  내용이 들어갈 자리
  <!-- Bootstrap core JavaScript -->
  <script src="../static/js/bootstrap.bundle.min.js"></script>
</body>
</html>
  • 1차 개발 당시 404 error 페이지를 포함해 총 8개의 HTML파일을 만들었었고, 파일 및 폴더의 디렉토리 구성은 다음과 같음.
Kor-DEEPression
├── app.py
├── modules_for_app.py
├── form_label.json
├── tuning-models
│   ├── CNN_depr.h5
│   └── MLP_mdd.h5
├── templates
│   ├── home.html
│   ├── prediction.html
│   ├── result.html
│   ├── dashboard.html
│   ├── info.html
│   ├── contact.html
│   ├── 404.html
│   └── 404-2.html
├── static
│   ├── css
│   │   ├── bootstrap.min.css
│   │   └── custom_styles.css
│   ├── js
│   │   ├── bootstrap.bundle.min.js
│   │   └── bootstrap.bundle.min.js.map
│   ├── imgs
│   └── icons
├── Procfile
├── requirements.txt
└── runtime.txt
  • 웹페이지는 모바일 환경에서도 쾌적하게 이용할 수 있도록 최적화하는 방향으로 디자인을 했고, 모바일 버전의 기준은 가로 폭이 280px로 가장 짧은 편에 속하는 Galaxy Fold를 기준으로 최적화를 실시하였으며, 가로 길이 별로 구성 및 텍스트 배치 변화를 check하는 방식으로 디자인이 이루어졌음
  • 대시보드는 iframe을 통해 embedding하였으며, 관련코드는 다음과 같이 코딩하였음
<body>
  	<!-- 위 내용은 생략 -->
    <div class="container">
        <div class="bs-docs-section">
            <div class="row">
                <div class="col-lg-2 col-md-1"></div>
                <div class="col-lg-8 col-md-10">
                    <div id="dash-area">
                        <iframe id="dash-content" src="https://lookerstudio.google.com/embed/reporting/a44f286d-d07a-41e5-bdea-a357f733b4ca/page/XZOED" frameborder="0" style="border:0;" allowfullscreen></iframe>
                    </div>
                </div>
                <div class="col-lg-2 col-md-1"></div>
            </div>
        </div>
    </div>
  	<!-- 아래 내용은 생략 -->
</body>
  • custom_styles.css
/* A4 용지 길이 비율로 최대한 맞춰줌 */
#dash-area {
    position: relative;
    width: 100%;
    padding-bottom: 140%; 
}
/* #dash-area 안에서 꽉차게끔 맞춰줌 */
#dash-content {
    position: absolute;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
}
  • 예측 및 결과 페이지는 Flask와 연동하여 동작하기 때문에 다음 파트에서 다루도록 하겠음.

5-2. Flask app Deployment Flask 앱 배포

Flask app 프로그래밍

  • Flask에서는 파일 구성과 폴더 이름이 중요해서 디렉토리는 아래와 같이 구성하였음
Kor-DEEPression
├── app.py
├── modules_for_app.py
├── form_label.json
├── final-models
│   ├── final_model_depr.tflite
│   └── final_model_mdd.tflite
├── templates
│   ├── home.html
│   ├── prediction.html
│   ├── result.html
│   ├── dashboard.html
│   ├── info.html
│   ├── contact.html
│   ├── 404.html
│   ├── 405.html
│   └── 404-2.html
├── static
│   ├── css
│   │   ├── bootstrap.min.css
│   │   └── custom_styles.css
│   ├── js
│   │   ├── bootstrap.bundle.min.js
│   │   └── bootstrap.bundle.min.js.map
│   ├── imgs
│   └── icons
├── Procfile
├── requirements.txt
└── runtime.txt
  • app.py (설문입력창과 결과창은 제외)
# 라이브러리 및 모듈함수 불러오기
from flask import Flask, render_template, request
from modules_for_app import *

# Flask app 지정
app = Flask(__name__)

# 404 error handling (주소값을 잘 못 입력한 경우)
@app.errorhandler(404)
def page_not_found(error):
    # templates/404.html 실행(404)
    return render_template('404.html'), 404

# 405 error handling (GET/POST 에러가 발생한 경우)
@app.errorhandler(405)
def method_not_allowed(error):
    # templates/405.html 실행(405)
    return render_template('405.html'), 405

# home page 실행함수(GET)
@app.route('/', methods=['GET'])
def home():
    # GET request
    if request.method == 'GET':
        # templates/home.html 실행(200)
        return render_template('home.html'), 200

# dashboard page 실행함수(GET)
@app.route('/dashboard', methods=['GET'])
def dashboard():
    # GET request
    if request.method == 'GET':
        # templates/dashboard.html 실행(200)
        return render_template('dashboard.html'), 200

# 프로그램 information page 실행함수(GET)
@app.route('/info', methods=['GET'])
def information():
    # GET request
    if request.method == 'GET':
        # templates/info.html 실행(200)
        return render_template('info.html'), 200

# 개발자 정보 page 실행함수(GET)
@app.route('/contact', methods=['GET'])
def contact():
    # GET request
    if request.method == 'GET':
        # templates/info.html 실행(200)
        return render_template('contact.html'), 200

# app.py파일 실행시 실행시킬 함수 : app
if __name__ == '__main__':
    app.run(debug=True)

설문입력 페이지

  • 설문입력 페이지(설문입력 및 예측이 실행되는 페이지)는 변수를 재조합하거나 예측을 실행하는 코드의 분량이 상당히 많기 때문에, 따로 함수들을 묶어서 module화하여 함수를 불러오는 방식으로 프로그래밍을 진행하였음.

  • 변수 재조합 및 인코딩 함수와 입력변수 확인용 디코딩 함수는 워낙 코드량이 방대하기 때문에 여기서는 다루지 않을 것이고, 자세한 코드는 다음 링크를 통해 확인 가능함

  • 대신 경량화한 .tflite 모델을 예측하는 함수는 중요하기 때문에, 자세히 다루어 보도록 하겠음.

  • modules_for_app.py (예측 실행 함수 부분)

# 라이브러리 import
import os
import json
import numpy as np
from tensorflow import lite as tflite

# tensorflow-cpu 경고문 출력 없애기
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'

# 변수 인코딩 및 디코딩 함수는 길고 복잡해서 표기하지 않겠음...
def Encoding_for_model(list_request, mode):
def Decoding_for_check(json_dir, dict_request, names_list):

# 모델 예측 기능 (tensorflow-lite)
    # 경량화 없이 1차 개발을 완료했으나, Koyeb free-tier(nano)의 한계로 오류가 자주 발생함
    # Keras 모델(.h5)로 predict를 실행하는 것이 생각보다 리소스를 많이 잡아먹는 작업임을 깨달음
    # 따라서 이를 개선하기 위해 Keras모델을 tensorflow-lite를 통해 경량화하여 진행해보았음
        # 이에 대한 코드는 final-models 폴더에 저장되어 있음
    # 테스트 결과 압도적인 속도의 개선을 확인했으며, 그동안 발생했었던 오류와 경고들도 더 이상 발생하지 않음을 확인함
def predict_prob_tflite(data, model_dir):
    '''
    predict_prob_tflite
        tensorflow-lite를 이용하여 예측 확률값을 얻는 기능 
    ---
    입력 변수 정보
        data : (list) 예측하고자하는 데이터
        model_dir : (str) tflite model의 directory
    ---
    출력 변수
        prob : (float) 예측 확률값의 퍼센트값 (소수점 둘째자리)
        pred : (int) 예측 클래스 (0 or 1)
    '''
    # 모델 불러오기
    interpreter = tflite.Interpreter(model_path=model_dir)
    # 메모리 할당하기
    interpreter.allocate_tensors()
    # input, output 정보담기
    input_details = interpreter.get_input_details()
    output_details = interpreter.get_output_details()
    
    # data를 input shape에 맞게 변경
    input_shape = input_details[0]['shape']
    input_data = np.array(data, dtype=np.float32).reshape(input_shape)
    
    # tensor에 맞게 세팅 후 invoke실시
    interpreter.set_tensor(input_details[0]['index'], input_data)
    interpreter.invoke()
    
    # 예측 실행 (확률값이 출력됨)
    output_data = interpreter.get_tensor(output_details[0]['index'])
    
    # 출력변수1) 예측 확률값을 퍼센트로 변환
    prob = int(round(output_data[0][0], 4)*(10**4))/100
    
    # 출력변수2) 확률 값을 통해 예측 클래스를 반환 (기준은 0.5)
    pred = np.where(output_data<0.5, 0, 1)[0][0]
    
    # 출력변수들 최종 반환
    return prob, pred
  • app.py ( 설문 입력 및 예측 실행 부분 )
# prediction page 실행함수(GET, POST)
    # 입력과 예측을 분리하면 여러기기에서 동시에 입력했을 경우에
    # 서로 값이 꼬이는 현상이 발견되어 입력과 예측을 한번에 진행하도록 함
    # 결과 값을 계속해서 다음 페이지에 넘겨주는 방식으로 전달 함 
@app.route('/prediction', methods=['GET','POST'])
def prediction():
    # GET request
    if request.method == 'GET':
        # templates/prediction.html 실행(200)
        return render_template('prediction.html'), 200
    
    # POST request(submit으로부터 실행)
    if request.method == 'POST':
        try:
            '''
            Request Form 으로부터 변수를 가져오기
            '''
            # 전체 변수이름과 값을 담을 리스트와 딕셔너리
            names_request = list()  # 변수 이름을 담을 리스트
            dict_request = dict()   # 변수 이름과 값을 담을 딕셔너리
            
            # 수치형 변수 (Q18 ~ 19, 총 3개) : 변수별로 dtype이 다르기 때문에 따로 진행
            names_numeric = ['age', 'height', 'weight']
            dict_numeric = dict()   # 변수 값을 담을 딕셔너리
            # Q18. Age : integer (19 ~ 80)
            dict_numeric[names_numeric[0]] = int(request.form[names_numeric[0]])
            # Q19. Height : float (100 ~ 200)
            dict_numeric[names_numeric[1]] = float(request.form[names_numeric[1]])
            # Q19. Weight : float (30 ~ 150)
            dict_numeric[names_numeric[2]] = float(request.form[names_numeric[2]])            
            # 전체리스트 및 딕셔너리 업데이트
            names_request.extend(names_numeric)
            dict_request.update(dict_numeric)
            
            # 이진형 변수 (Q7 ~ Q13, 7개) : 모두 integer이기 때문에 for문으로 진행
            names_binary = ['limitation', 'modality', 'w_change', 'w_control',
                            'high_bp', 'diabetes', 'high_lipid']
            dict_binary = dict()    # 변수 값을 담을 딕셔너리
            # Q 7 ~ 13. 이진형 변수
            for name in names_binary:
                dict_binary[name] = int(request.form[name])            
            # 전체리스트 및 딕셔너리 업데이트
            names_request.extend(names_binary)
            dict_request.update(dict_binary)
            
            # 범주형 변수 (Q1 ~ Q6 & Q14 ~ Q17, 총 10개) : 모두 integer이기 때문에 for문으로 진행
            names_category = ['gender','edu','household','marital','economy','health',
                              'drk_freq','drk_amount','smoke','stress']
            dict_category = dict()  # 변수 값을 담을 딕셔너리
            # Q1 ~ Q6 & Q14 ~ Q17. 범주형 변수
            for name in names_category:
                dict_category[name] = int(request.form[name])            
            # 전체리스트 및 딕셔너리 업데이트
            names_request.extend(names_category)
            dict_request.update(dict_category)
            
            
            '''
            Modeling용 데이터 생성
            '''
            # 수집한 변수 값들을 List로 묶어주기(수치형, 이진형, 범주형 순서)
            values_numeric = [dict_numeric[name] for name in names_numeric]
            values_binary = [dict_binary[name] for name in names_binary]
            values_category = [dict_category[name] for name in names_category]
            
            # Feature 조합 및 Encoding 실시 (modules_for_app에서 불러온 함수)
            values_encoded_numeric = Encoding_for_model(values_numeric, mode='numeric')
            values_encoded_binary = Encoding_for_model(values_binary, mode='binary')
            values_encoded_category = Encoding_for_model(values_category, mode='category')
            
            # 리스트 병합
            values_encoded = values_encoded_numeric + values_encoded_binary + values_encoded_category
            
            
            '''
            입력 Check용 데이터 생성
            '''
            # 이진형과 범주형은 Label이 따로 존재하므로 변수명 리스트를 순서대로 묶기(이진형, 범주형 순서)
            names_bin_cat = names_binary + names_category
            
            # Decoding 실시 (modules_for_app에서 불러온 함수)
            labels_bin_cat = Decoding_for_check(json_dir='./form_labels.json',
                                                dict_request=dict_request,
                                                names_list=names_bin_cat)
            
            # 수치형 변수값 리스트를 Label값을 담은 리스트와 병합하여 Check용 딕셔너리 생성
            check_request = values_numeric + labels_bin_cat
            dict_check = dict(zip(names_request, check_request))
            
            
            '''
            우울증 및 주요우울장애 예측
                확률퍼센트값과 예측클래스를 산출
            '''
            # 모델(.tflite)파일 디렉토리 지정
            model_dir_depr = './final-models/final_model_depr.tflite'
            model_dir_mdd = './final-models/final_model_mdd.tflite'
            
            # Depression 모델 예측 (확률 퍼센트값 & 예측 클래스)
            prob_depr, pred_depr = predict_prob_tflite(values_encoded, model_dir_depr)
            
            # 예측 클래스를 문자열로 Decoding
            class_depr = str()
            if pred_depr == 0:
                class_depr = "정상"
            elif pred_depr == 1:
                class_depr = "우울증"
            
            '''
            확인 페이지 출력 (예측 결과도 넘겨주기)
            '''
            # "정상" 인 경우 : 다음 단계로 바로 넘어감
            if class_depr == "정상":
                # templates/prediction.html에 변수들을 전달 (200)
                return render_template('prediction.html',
                                       checklist = dict_check,
                                       percnt_depr = prob_depr,
                                       label_depr = class_depr), 200
            
            # "우울증" 인 경우 : MDD 모델 예측을 추가적으로 실시
            elif class_depr == "우울증":
                # MDD 모델 예측 (확률 퍼센트값 & 예측 클래스)
                prob_mdd, pred_mdd = predict_prob_tflite(values_encoded, model_dir_mdd)

                # 예측 클래스를 문자열로 Decoding
                class_mdd = str()
                if pred_mdd == 0:
                    class_mdd = "경도우울증"
                elif pred_mdd == 1:
                    class_mdd = "주요우울장애"
                
                # 오류없이 예측을 수행한 경우 : templates/prediction.html에 변수들을 전달 (200)
                return render_template('prediction.html',
                                       checklist = dict_check,
                                       percnt_depr = prob_depr,
                                       label_depr = class_depr,
                                       percnt_mdd = prob_mdd,
                                       label_mdd = class_mdd), 200
        
        # try문에서 에러가 발생한 경우 : templates/404-2.html 실행(404)
        except:
            return render_template('404-2.html'), 404
  • prediction.html (입력 확인 페이지 부분, POST로 "checklist" 변수를 전달 받을때만 출력됨)
{% if checklist %}
<div class="d-grid gap-2" id="check-values">
  <legend class="mt-4">Final Check. 입력값 확인</legend>
  예측에 앞서 아래 표를 통해<br>
  입력값들을 확인해 주십시오.<br>
  <br>
</div>
<table class="table table-hover" id="check-table">
  <thead>
    <tr class="table-primary">
      <th scope="col" style="text-align: center;">Question</th>
      <th scope="col" style="text-align: left;">Response</th>
    </tr>
  </thead>
  <tbody>
    <tr class="table-dark">
      <th scope="row">Gender<br>(성별)</th>
      <td>{{checklist['gender']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Education<br>(최종 학력)</th>
      <td>{{checklist['edu']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Household<br>(세대 유형)</th>
      <td>{{checklist['household']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Marital<br>(혼인 상태)</th>
      <td>{{checklist['marital']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Employ<br>(경제 활동)</th>
      <td>{{checklist['economy']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Healthy<br>(건강 상태)</th>
      <td>{{checklist['health']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Limitation<br>(활동 제한)</th>
      <td>{{checklist['limitation']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Modality<br>(2주간 이환)</th>
      <td>{{checklist['modality']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">W Change<br>(체중 변화)</th>
      <td>{{checklist['w_change']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">W Control<br>(체중 조절)</th>
      <td>{{checklist['w_control']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">High BP<br>(고혈압)</th>
      <td>{{checklist['high_bp']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Diabetes<br>(당뇨병)</th>
      <td>{{checklist['diabetes']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">High lipid<br>(고지혈증)</th>
      <td>{{checklist['high_lipid']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Drink freq<br>(음주 빈도)</th>
      <td>{{checklist['drk_freq']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Drink amt<br>(1회 음주량)</th>
      <td>{{checklist['drk_amount']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Smoking<br>(흡연 여부)</th>
      <td>{{checklist['smoke']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Stress<br>(스트레스)</th>
      <td>{{checklist['stress']}}</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Age<br>(만나이)</th>
      <td>만 {{checklist['age']}} 세</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Height<br>(키)</th>
      <td>{{checklist['height']}} cm</td>
    </tr>
    <tr class="table-dark">
      <th scope="row">Weight<br>(체중)</th>
      <td>{{checklist['weight']}} kg</td>
    </tr>
  </tbody>
</table>
<p style="text-align: center;">
  <br>
  잘못된 입력값이 있다면<br>
  뒤로가기 통해 입력 값을 수정해주십시오.
  <br><br>
  <a href="javascript:window.history.back();">
    <button type="button" class="btn btn-outline-primary btn-lg">
      뒤로가기
    </button>
  </a>
</p>
<p style="text-align: center;">
  입력값의 문제가 없는 경우<br>
  아래 버튼 통해 예측을 진행합니다.
</p>
<form action="/result" method="post">
  <input type="hidden" name="depr_percnt" value="{{percnt_depr}}">
  <input type="hidden" name="depr_class" value="{{label_depr}}">
  {% if percnt_mdd and label_mdd %}
  <input type="hidden" name="mdd_percnt" value="{{percnt_mdd}}">
  <input type="hidden" name="mdd_class" value="{{label_mdd}}">
  {% endif %}
  <div class="d-grid gap-2">
    <button class="source-button btn btn-primary btn-lg" type="submit" name="predict_type" value="depr">
      <img src="../static/imgs/head-heart-32.png">
      Predict Depression
    </button>
  </div>
</form>
<br><br>
{% endif %}

결과 페이지

  • 1차 개발 당시에는 설문 입력 후 global 변수로 저장하여 예측을 실시하도록 했었지만, 동시 접속시 마지막 입력 결과만 출력되는 문제가 발생하였음.
  • 모델 경량화를 통해 두개의 모델을 한번에 돌려도 무리가 없어지고 예측 속도도 빨라짐을 확인하였기 때문에, 입력(submit)과 동시에 모델 예측을 수행하도록 코드를 재구성 하였고, 예측 결과값은 POST method를 통해 계속해서 넘겨주는 방식으로 문제를 해결하였음.
  • POST method만 허용하기 때문에, URL로 바로 result창으로 접근하는 GET방식은 405 error가 발생하므로 405 error페이지도 추가적으로 디자인하였음.
  • app.py ( 결과 페이지 실행 부분 )
# 예측 결과 출력 함수(POST)
@app.route('/result', methods=['POST'])
def result():
    # POST request(submit으로부터 실행)
    if request.method == 'POST':
        # submit 버튼 값 가져오기
        pred_mode = request.form['predict_type']
        
        # submit 값이 "depr"인경우
        if pred_mode == 'depr':
            try:
                # 변수 가져오기
                depr_prob = request.form['depr_percnt']
                depr_class  = request.form['depr_class']
                
                # "정상"인 경우 : 바로 결과페이지로 이동
                if depr_class == "정상":
                    # templates/result.html 실행(200)
                    return render_template('result.html',
                                           percnt_depr = depr_prob,
                                           label_depr = depr_class), 200
                
                # "우울증"인 경우 : 변수를 추가적으로 가져오고 결과페이지로 이동
                elif depr_class == "우울증":
                    # 변수 가져오기
                    mdd_prob = request.form['mdd_percnt']
                    mdd_class = request.form['mdd_class']
                    
                    # templates/result.html 실행(200)
                    return render_template('result.html',
                                           percnt_depr = depr_prob,
                                           label_depr = depr_class,
                                           percnt_mdd = mdd_prob,
                                           label_mdd = mdd_class), 200
            
            # try문에서 에러가 발생한 경우 : templates/404-2.html 실행(404)
            except:
                return render_template('404-2.html'), 404
        
        # submit 값이 "mdd"인경우
        if pred_mode == 'mdd':
            try:
                # 변수 가져오기
                add_depr_prob = request.form['add_depr_percnt']
                add_depr_class = request.form['add_depr_class']
                add_mdd_prob = request.form['add_mdd_percnt']
                add_mdd_class = request.form['add_mdd_class']
                
                # templates/result.html 실행(200)
                return render_template('result.html',
                                       add_percnt_depr = add_depr_prob,
                                       add_label_depr = add_depr_class,
                                       add_percnt_mdd = add_mdd_prob,
                                       add_label_mdd = add_mdd_class), 200
                
            # try문에서 에러가 발생한 경우 : templates/404-2.html 실행(404)
            except:
                return render_template('404-2.html'), 404
  • result.html ( 정상으로 분류된 경우의 출력 화면 )
{% if percnt_depr %}
<div class="row">
  <div class="col-md-3 col-lg-4"></div>
  <div class="col-md-6 col-lg-4">
    <br><br>
    Depression
    <h1 style="background-color: #343a40;">우울증<br>예측 결과</h1>
    <br>
    <ul class="nav nav-pills">
      <li class="nav-item">
        <a class="nav-link active" data-bs-toggle="pill" href="#result-depr" aria-selected="true">
          결과 (Result)
        </a>
      </li>
      <li class="nav-item">
        <a class="nav-link disabled" data-bs-toggle="pill" href="#" aria-selected="false">/</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" data-bs-toggle="pill" href="#model-depr" aria-selected="false">
          모델정보
        </a>
      </li>
    </ul>
    <div class="tab-content">
      <div id="result-depr" class="tab-pane fade active show">
        <br>
        {% if label_depr=="정상" %}
        <h2>{{label_depr}}<br>(Normal)</h2>
        {% elif label_depr=="우울증" %}
        <h2>{{label_depr}}<br>(Depression)</h2>
        {% endif %}
        <br>
        <p>우울증 진단 확률 값<br>{{percnt_depr}} %</p>
        <p style="background-color: #343a40;">
          우울증 예측을 실시한 결과,<br>진단 확률 값은 {{percnt_depr}} % 이며,
          <br>
          이는 "{{label_depr}}" 군에<br>해당하는 수치입니다.
          <br>
          <small class="text-muted">
            확률 값 50% 이상 : 우울증으로 분류
          </small>
        </p>
      </div>
      <div id="model-depr" class="tab-pane fade">
        <br>
        <h3>
          <img src="../static/imgs/head-heart-32.png">
          우울증(Depression)<br>예측 모델
        </h3>
        <p>
          Deep-Learning 1D-CNN<br>
          <small class="text-muted">
            (Convolutional Neural Networks)
            <br>
            Modeling by Tensorflow(Keras)
          </small>
        </p>
        <h5>검증 정확도(Accuracy)</h5>
        <p>84 % (0.8366)</p>
        <h5>검증 ROC_AUC score</h5>
        <p>83 % (0.8291)</p>
      </div>
    </div>
  </div>
  <div class="col-md-3 col-lg-4"></div>
</div>
{% endif %}
  • result.html ( 우울증으로 분류된 경우의 출력 화면 )
{% if label_depr=="우울증" %}
<div class="row">
  <div class="col-md-3 col-lg-4"></div>
  <div class="col-md-6 col-lg-4">
    <br><br>
    Major Depressive Disorder
    <h1 style="background-color: #343a40;">주요우울장애<br>추가 예측</h1>
    추가 예측을 원하시는 경우<br>아래 버튼을 클릭해주십시오<br><br>
    <form action="/result" method="post">
      <input type="hidden" name="add_depr_percnt" value="{{percnt_depr}}">
      <input type="hidden" name="add_depr_class" value="{{label_depr}}">
      <input type="hidden" name="add_mdd_percnt" value="{{percnt_mdd}}">
      <input type="hidden" name="add_mdd_class" value="{{label_mdd}}">
      <div class="d-grid gap-2">
        <button class="source-button btn btn-primary btn-lg" type="submit" name="predict_type" value="mdd">
          <img src="../static/imgs/head-heart-32.png">
          Predict MDD
        </button>
      </div>
    </form>
  </div>
  <div class="col-md-3 col-lg-4"></div>
</div>
{% endif %}
  • result.html ( 우울증에서 추가적으로 MDD예측 버튼을 실행한 경우의 출력 화면 )
{% if add_percnt_depr %}
<div class="row">
  <div class="col-md-2"></div>
  <div class="col-md-4">
    <br><br>
    Major Depressive Disorder
    <h1 style="background-color: #343a40;">주요우울장애<br>추가예측 결과</h1>
    <br>
    <ul class="nav nav-pills">
      <li class="nav-item">
        <a class="nav-link active" data-bs-toggle="pill" href="#add-result-mdd" aria-selected="true">
          결과 (Result)
        </a>
      </li>
      <li class="nav-item">
        <a class="nav-link disabled" data-bs-toggle="pill" href="#" aria-selected="false">/</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" data-bs-toggle="pill" href="#add-model-mdd" aria-selected="false">
          모델정보
        </a>
      </li>
    </ul>
    <div class="tab-content">
      <div id="add-result-mdd" class="tab-pane fade active show">
        <br>
        {% if add_label_mdd == '경도우울증' %}
        <h2>{{add_label_mdd}}<br>(Minor Depr.)</h2>
        {% elif add_label_mdd == '주요우울장애' %}
        <h2>{{add_label_mdd}}<br>(MDD)</h2>
        {% endif %}
        <br>
        <p>주요우울장애 진단 확률 값<br>{{add_percnt_mdd}} %</p>
        <p style="background-color: #343a40;">
          MDD 예측을 실시한 결과,<br>진단 확률 값은 {{add_percnt_mdd}} % 이며,
          <br>
          이는 "{{add_label_mdd}}" 군에<br>해당하는 수치입니다.
          <br>
          <small class="text-muted">
            확률 값 50% 이상 : MDD로 분류
          </small>
        </p>
      </div>
      <div id="add-model-mdd" class="tab-pane fade">
        <br>
        <h3>
          <img src="../static/imgs/head-heart-32.png">
          주요우울장애(MDD)<br>예측 모델
        </h3>
        <p>
          Deep-Learning MLP<br>
          <small class="text-muted">
            (Multi-Layer Perceptron)
            <br>
            Modeling by Tensorflow(Keras)
          </small>
        </p>
        <h5>검증 정확도(Accuracy)</h5>
        <p>77 % (0.7722)</p>
        <h5>검증 ROC_AUC score</h5>
        <p>78 % (0.7838)</p>
      </div>
    </div>
  </div>
  <div class="col-md-4">
    <br><br>
    Depression
    <h1 style="background-color: #343a40;">우울증<br>예측 결과</h1>
    <br>
    <ul class="nav nav-pills">
      <li class="nav-item">
        <a class="nav-link active" data-bs-toggle="pill" href="#add-result-depr" aria-selected="true">
          결과 (Result)
        </a>
      </li>
      <li class="nav-item">
        <a class="nav-link disabled" data-bs-toggle="pill" href="#" aria-selected="false">/</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" data-bs-toggle="pill" href="#add-model-depr" aria-selected="false">
          모델정보
        </a>
      </li>
    </ul>
    <div class="tab-content">
      <div id="add-result-depr" class="tab-pane fade active show">
        <br>
        <h2>{{add_label_depr}}<br>(Depression)</h2>
        <br>
        <p>우울증 진단 확률 값<br>{{add_percnt_depr}} %</p>
        <p style="background-color: #343a40;">
          우울증 예측을 실시한 결과,<br>진단 확률 값은 {{add_percnt_depr}} % 이며,
          <br>
          이는 "{{add_label_depr}}" 군에<br>해당하는 수치입니다.
          <br>
          <small class="text-muted">
            확률 값 50% 이상 : 우울증으로 분류
          </small>
        </p>
      </div>
      <div id="add-model-depr" class="tab-pane fade">
        <br>
        <h3>
          <img src="../static/imgs/head-heart-32.png">
          우울증(Depression)<br>예측 모델
        </h3>
        <p>
          Deep-Learning 1D-CNN<br>
          <small class="text-muted">
            (Convolutional Neural Networks)
            <br>
            Modeling by Tensorflow(Keras)
          </small>
        </p>
        <h5>검증 정확도(Accuracy)</h5>
        <p>84 % (0.8366)</p>
        <h5>검증 ROC_AUC score</h5>
        <p>83 % (0.8291)</p>
      </div>
    </div>
  </div>
  <div class="col-md-2"></div>
</div>
{% endif %}

Koyeb 앱 생성 및 배포

  • 웹 배포 서비스는 Koyeb을 통해 진행되었음.
  • Section3 식중독 Project에서 서비스한 웹페이지는 Heroku를 통해 배포되었으나, 2022년 11월에 무료 서비스를 지원하지 않게됨으로써 차선책으로 Heroku와 동일하게 Github 레포지토리 연결 및 자동 배포가 가능한 Koyeb을 이용하게 되었고, RAM 메모리 512MB가 무료 배포의 제한이었던 Heroku와 동일하게 Koyeb 또한 512MB안에서 무료 배포가 가능하지만 Koyeb은 이를 반으로 나눈 256MB Nano plan을 2개까지 무료 배포를 실시할 수 있으므로, 이번 프로젝트의 프로그램 배포는 Koyeb Nano plan을 통해 진행되었음.
  • 256MB의 제한 안에서 프로그램을 배포해야했기 때문에, requirements.txt의 구성은 최대한 간결하게 진행해야했음.
    • Pyhton 3.8, Flask, gunicorn, tensorflow-cpu 2.9 4가지 이외의 라이브러리를 설치할 경우엔 RAM 초과 현상이 발생하므로 주어진 4가지의 기능들로만 배포를 진행해야했음
    • 특히 tensorflow의 경우엔 cpu 버전으로 설치하지 않으면 용량이 매우 크고, 웹 배포 서비스에서도 GPU를 지원하지 않기 때문에 tensorflow-cpu로 진행해야 했고, 2.10 버전 이상에서는 tensorflow-intel을 설치함으로인해 RAM 초과현상이 발생하므로 버전은 2.9로 제한했어야했음

필수 구성 파일

  • Procfile (WSGI, 포트, worker, thread, app지정)
    • 웹과 앱을 서로 연결하는 WSGI는 gunicorn으로 설정함
    • Worker 및 Thread는 Koyeb Nono plan의 vCPU가 1인 관계로 1로 설정함
      • 1 이상으로 설정했더니.. worker timeout으로 인해 무한루프에 빠져버리는 현상이 발생했음.. 사양의 한계는 어쩔수 없는 듯...
web: gunicorn --bind :8000 --workers 1 --threads 1 app:app
  • runtime.txt (Python 버전 지정)
    • 파이썬 버젼을 지정할 수 있도록 해주는 파일
python-3.8.16
  • requirements.txt (pip 설치 목록 지정)
    • 앱 구동을 위해 설치해야할 pip 목록을 지정하는 파일
    • Python, Flask, gunicorn 설치 후 pip freeze를 통해 파일을 생성하고, tensorflow-cpu==2.9.2 만 추가해줌으로써 설치 목록을 지정함
certifi==2022.12.7
click==8.1.3
colorama==0.4.6
Flask==2.2.2
gunicorn==20.1.0
importlib-metadata==6.0.0
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.1
Werkzeug==2.2.2
wincertstore==0.2
zipp==3.11.0
tensorflow-cpu==2.9.2
  • 필수 구성 요소들만 충족 된다면, 연동한 Github 레포지토리에 변경사항을 push하는 경우 자동으로 재배포가 이루어지게 됨.

5-3. Webpage Screenshots 웹페이지 스크린샷

Home Page

Prediction Page

Result Page

Dashboard Page


6. Conclusion 결론

(Part6) Conclusion 결론

  • Key Takeaways 프로젝트 핵심
  • My Takeaways 느낀점 회고

6-1. Key Takeaways 프로젝트 핵심

  • 본 프로젝트는 우울증을 예측 진단하는 프로그램을 개발하는 프로젝트이며, 데이터베이스 구축부터 시각화분석, 모델링, 웹페이지 배포까지 진행한 Full-stack 딥러닝 프로젝트임

  • 평소에 정신질환 분야에 대한 딥러닝 프로젝트를 진행해보고 싶었고, 그 중에서도 가장 흔하게 널리 알려져 있는 우울증을 주제로써 선정하게 되었음

  • 본 프로젝트의 목표는 DS, DA, DE 모두를 아우르는 Full-Stack 딥러닝 프로젝트로써, Codestates AI 부트캠프 과정에서 배운 모든 내용들을 활용하고 응용해보기 위해 노력하였음

  • 모델링은 선형모델이나 트리모델과 같은 머신러닝 기법도 활용 할 수 있도록 이진분류 문제로 정의하여 진행하였고, 모델링 결과 모델들 모두 성능면에서 개인적으로 만족스러운 결과를 얻을 수 있었음

  • 평가지표는 Accuracy만을 고려하려 했으나, 진단 분야에서는 Accuracy뿐만 아니라 F1 score나 ROC_AUC score도 함께 고려해야하기 때문에 평가지표를 추가하게 되었고, ROC_AUC score를 중심으로 모델링을 실시하게 되었음

  • 모델 경량화를 실시하기 이전에는 속도면에서 아쉬움이 남았었고, 연산 오류도 자주 발생해 고민이 많았었음. 그러던 중에 tensorflow-lite로 모델을 경량화 시킬 수 있음을 알게 되어, 경량화를 실시하였고, 속도와 오류 문제 모두 해결할 수 있었음

6-2. My Takeaways 느낀점 회고

  • 좋았던 점
    • DS, DA, DE 모두를 아우르는 Full-Stack 딥러닝 프로젝트를 진행하면서, 그동안 부트캠프에서 배웠던 모든 개념들을 다시 돌아 볼 수 있어서 정말 유익했던 시간이었다고 생각함
    • 개인 프로젝트를 통해 개인적으로 관심이 깊은 정신질환 분야에 대한 딥러닝 프로젝트를 진행할 수 있어서 좋았고, 시행착오를 정말 많이 겪었지만 오류들을 해결하는 과정에서 많은 것들을 배울 수 있었음
    • 모델 성능과 웹페이지 디자인도 개인적으로는 만족스러운 편이라 뿌듯하기도 하고 후련하기도 함
  • 아쉬운점
    • 웹페이지의 가독성을 높이기 위해 최대한 노력했으나, HTML이나 CSS를 좀 더 잘 다룰 수 있었더라면 더 깔끔하고 괜찮은 디자인의 웹페이지를 만들 수 있지 않았을까 하는 아쉬움이 남음
    • Koyeb 웹 배포 서비스 무료 버전에서 허용하는 범위 안에서 프로그램을 설계해야 했기 때문에, 제한된 라이브러리 밖에 설치하지 못하여 코드가 다소 복잡해진 점도 다소 아쉬움으로 남음
  • 추가회고
    • 1개월 간 진행된 코드스테이츠 마지막 프로젝트가 드디어 끝이 났다. 후련하기도 하면서 한편으로는 뭔가 아쉬움도 많이 남았던 프로젝트였다고 생각함..
    • 프로젝트를 기획하고 프로그램을 구현하는 과정 속에서 정말 많은 우여곡절과 시행착오들이 있었지만, 끝까지 포기하지 않고 마무리 지을 수 있어서 다행이라 생각하고, 이번 프로젝트를 통해 그동안 배웠던 개념들을 쭉 되돌아보고 정리할 수 있어서 좋았음
    • 포트폴리오와 이력서를 작성하느라 프로젝트 블로그 정리도 계속 미루다 이제서야 조금 여유가 생겨 블로그 포스팅을 작성하게 되었고, 작성하면서 느낀 것이지만 생각보다 분량이 많아서 정리하는데 애를 먹긴했었음..
    • 부트캠프 수료를 몇일 앞둔 상황에서 앞으로 취업을 하고 어떠한 프로젝트들을 수행하게 될지 기대가 되기도 하고, 이정도로 괜찮은가 걱정이 들기도 하다. 그래도 주어진 현재에 최선을 다하다보면 더 밝은 내일이 있을 것이라 기대해본다.
    • 앞으로도 파이팅! 아자아자!

profile
Machine Learning (AI) Engineer & BackEnd Engineer (Entry)
post-custom-banner

0개의 댓글