Search
Duplicate

머신 러닝 교과서/ 웹 애플리케이션에 머신 러닝 모델 내장

학습된 사이킷런 추정기 저장

머신러닝 모델을 훈련하려면 많은 계산 비용이 드는데, 애플리케이션을 재시작 할 때마다 모델을 다시 훈련하는 것은 바람직 하지 않다. 때문에 학습된 모델을 재사용하기 위해 파이썬의 pickle 모듈을 사용하는 것이 좋다.
이 모듈은 파이썬 객체의 구조를 압축된 바이트코드로 직렬화하고 복원할 수 있다.
분류기의 현재 상태를 저장하고 새로운 샘플을 분류할 때 훈련 데이터에서 모델을 다시 학습할 필요 없이 저장된 모델을 불러오기만 하면 된다.
import pickle import os dest = os.path.join('movieclassifier', 'pkl_objects') if not os.path.exists(dest): os.makedirs(dest) pickle.dump(stop, open(os.path.join(dest, 'stopwords.pkl'), 'wb'), protocol=4) pickle.dump(clf, open(os.path.join(dest, 'classifier.pkl'), 'wb'), protocol=4)
Python
8장의 온라인 알고리즘 다음에 위 코드를 실행하면 ‘../movieclassifier/pkl_objects/’ 패스에 직렬화된 파이썬 객체를 저장한다.
pickle 모듈의 dump 메서드를 사용하여 훈련된 로지스틱 회귀 모델 뿐만 아니라 NLTK 라이브러리의 불용어도 직렬화하여 저장한다. 이렇게 하면 서버에 NLTK 라이브러리를 설치할 필요가 없다.
protocol=4는 파이썬 3.4 버전 이상에서 사용할 수 있는 최신 pickle 프로토콜이다.
HashingVectorizer는 학습 과정이 없어서 pickle로 직렬화할 필요가 없다. 대신 현재 사용 중인 파이썬 세션에서 HashingVectorizer 객체를 임포트할 수 있도록 파이썬 스크립트를 만들어 보겠다.
아래의 코드를 movieclassifier 디렉터리 아래 vectorizer.py 파일로 만들어 두자
from sklearn.feature_extraction.text import HashingVectorizer import re import os import pickle cur_dir = os.path.dirname(__file__) stop = pickle.load(open(os.path.join(cur_dir, 'pkl_objects', 'stopwords.pkl'), 'rb')) def tokenizer(text): text = re.sub('<[^>]*>', '', text) emoticons = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)', text.lower()) text = re.sub('[\W]+', ' ', text.lower()) + ' '.join(emoticons).replace('-', '') return [w for w in text.split() if w not in stop] vect = HashingVectorizer(decode_error='ignore', n_features=2**21, preprocessor=None, tokenizer=tokenizer)
Python
movieclassifier 디렉터리를 이동하여 아래 코드를 실행하면 vectorizer를 임포트하고 분류기를 복원하고, 복원한 분류기 객체를 사용하여 문서 샘플을 전처리하고 감성 레이블을 예측한다.
import pickle import re import os from vectorizer import vect import numpy as np clf = pickle.load(open(os.path.join('pkl_objects', 'classifier.pkl'), 'rb')) label = {0:'양성', 1:'음성'} example = ['I love this movie'] X = vect.transform(example) print('예측: %s\n확률: %.2f%%' % (label[clf.predict(X)[0]], np.max(clf.predict_proba(X))*100))
Python

데이터를 저장하기 위해 SQLite 데이터베이스 설정

SQLite는 오픈소스 SQL 데이터베이스 엔진으로 대부분의 운영 체제를 지원한다. 파이썬은 표준 라이브러리에 SQLite 데이터베이스를 위한 sqlite3 API가 이미 포함되어 있기 때문에 SQLite를 이용하면 편리하다.
파이썬에서 SQLite를 사용하는 예시는 아래와 같다.
import sqlite3 conn = sqlite3.connect('reviews.sqlite') c = conn.cursor() c.execute('DROP TABLE IF EXISTS review_db') c.execute('CREATE TABLE review_db (review TEXT, sentiment INTEGER, date TEXT)') example1 = 'I love this movie' c.execute("INSERT INTO review_db (review, sentiment, date) VALUES (?, ?, DATETIME('now'))", (example1, 1)) example2 = 'I dislike this movie' c.execute("INSERT INTO review_db (review, sentiment, date) VALUES (?, ?, DATETIME('now'))", (example2, 0)) c.execute("SELECT * FROM review_db WHERE date BETWEEN '2017-01-01 00:00:00' AND DATETIME('now')") results = c.fetchall() conn.commit() conn.close() print(results)
Python

플라스크 웹 애플리케이션 개발

플라스크(Flask)는 파이썬으로 만들어진 웹 프레임워크이다.

첫 번째 플라스크 애플리케이션

플라스크 API와 친숙해지기 위해 우선 간단한 웹 애플리케이션을 만들어보겠다. 첫 번째로 만들 애플리케이션은 이름을 입력하는 폼 필드 하나만 가진 간단한 웹 페이지로 구성된다.
먼저 아래와 같은 디렉터리 구조를 만든다.
1st_flask_app_1/ app.py templates/ first_app.html
Python
app.py 파일은 플라스크 웹 애플리케이션을 구동하기 위해 파이썬 인터프리터로 실행할 핵심 코드를 담고 있다.
플라스크는 templates 디렉터레에서 웹 브라우저에 표시할 정적인 HTML 파일을 찾는다.
from flask import Flask, render_template app = Flask(__name__) @app.route('/') def index(): return render_template('first_app.html') if __name__ == '__main__': app.run()
Python
위 코드를 살펴보자
1.
애플리케이션은 하나의 모듈로 실행된다. 매개변수 __name__ 으로 새로운 플라스크 인스턴스를 초기화한다. 이렇게 하면 플라스크는 현재 디렉터리와 같은 위치에서 HTML 템플릿 폴더(templates)를 찾는다.
2.
그 다음 라우트 데코레이터(@app.route(‘/’))를 사용하여 특정 URL이 index 함수를 실행하도록 지정한다.
3.
여기서 index 함수는 단순히 templates 폴더 아래에 있는 first_app.html 파일을 화면에 출력한다.
4.
마지막으로 이 스크립트가 파이썬 인터프리터에 의해 직접 실행될 때만 run 메서드를 사용하여 애플리케이션을 시작한다. 이를 위해 if __name__ == ‘__main__’ 구문을 사용한다.
first_app.html은 다음과 같다.
<!DOCTYPE html> <html> <head> <title>첫 번째 애플리케이션</title> </head> <body> <div> 와우, 첫 번째 플라스크 웹 애플리케이션입니다! </div> </body> </html>
HTML
터미널에서 app.py를 실행하면 ‘* Running on http://127.0.0.1:5000/’ 이라는 메세지를 볼 수 있다. 이 주소를 브라우저에 입력하면 위 코드로 작성된 웹 사이트를 볼 수 있다.

폼 검증과 화면 출력

WTForms 라이브러리를 이용하면 폼 요소를 쉽게 추가할 수 있다.
2번째 샘플의 디렉터리 구조는 아래와 같다.
1st_flask_app_2/ app.py static/ style.css templates/ first_app.html hello.html _formhelpers.html
Python
app.py는 다음과 같이 수정한다.
from flask import Flask, render_template, request from wtforms import Form, TextAreaField, validators app = Flask(__name__) class HelloForm(Form): sayhello = TextAreaField('', [validators.DataRequired()]) @app.route('/') def index(): form = HelloForm(request.form) return render_template('first_app.html', form=form) @app.route('/hello', methods=['POST']) def hello(): form = HelloForm(request.form) if request.method == 'POST' and form.validate(): name = request.form['sayhello'] return render_template('hello.html', name=name) return render_template('first_app.html', form=form) if __name__ == '__main__': app.run(debug=True)
Python
위 코드를 살펴보자
1.
index 함수를 확장해서 wtforms의 TextAreaField 클래스를 사용하여 시작 웹 페이지에 텍스트 필드를 추가했다. 이 클래스는 사용자의 입력 텍스트가 안전한지 아닌지를 자동으로 확인한다.
2.
새로운 함수 hello를 정의하여 HTML 폼으로 전달된 내용을 검증한 후 hello.html 페이지를 출력한다. 여기서는 폼에 입력된 데이터를 HTTP body에 실어 서버로 전송하는 POST 메서드를 사용했다.
3.
app.run 메서드에 debug=True 매개변수를 설정해서 플라스크의 디버거를 활성화 한다.
진자2 템플릿 엔진을 사용하여 _formhelpers.html 파일에 매크로를 작성하자.
진자2 템플릿 엔진에 대한 설명은 책 범위를 벗어나므로 생략한다.
{% macro render_field(field) %} <dt> {{field.label}} <dd> {{field(**kwargs)|safe}} {% if field.errors %} <ul class = errors> {% for error in field.errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} </dd> </dt> {% endmacro %}
HTML
style.css를 만들어 HTML 문서의 스타일을 바꿔보자
body { font-size: 2em; }
CSS
first_app.html은 다음과 같이 수정한다.
<!DOCTYPE html> <html> <head> <title>첫 번째 애플리케이션</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css')}}"> </head> <body> {% from "_formhelpers.html" import render_field %} <div> 이름을 입력해 주세요 </div> <form method="post" action="/hello"> <dl> {{ render_field(form.sayhello) }} </dl> <input type="submit" value='이름 입력' name='submit_btn'> </form> </body> </html>
HTML
결과 페이지 hello.html은 다음과 같이 작성한다.
<!DOCTYPE html> <html> <head> <title>첫 번째 애플리케이션</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css')}}"> </head> <body> <div> {{ name }} 님 안녕하세요! </div> </body> </html>
HTML

영화 리뷰 분류기를 웹 애플리케이션으로 만들기

사용자가 리뷰를 입력하면 예측을 보여주고, 사용자가 맞음, 틀림 버튼을 입력해서 예측 결과에 대한 피드백을 보낼 수 있는 예제를 만들어보겠다.

파일과 폴더: 디렉터리 구조 살펴보기

디렉터리 구조는 아래와 같이 구성한다.
app.py reviews.sqlite vectorizer.py /pkl_objects classifier.pkl stopwords.pkl /static style.css /templates _formhelpers.html results.html reviewform.html thanks.html
Python

메인 애플리케이션 app.py 구현

app.py는 다음과 같이 작성한다.
from flask import Flask, render_template, request from wtforms import Form, TextAreaField, validators import pickle import sqlite3 import os import numpy as np # 로컬 디렉터리에서 HashingVectorizer를 임포트한다. from vectorizer import vect app = Flask(__name__) ##### 분류기 준비 cur_dir = os.path.dirname(__file__) clf = pickle.load(open(os.path.join(cur_dir, 'pkl_objects', 'classifier.pkl'), 'rb')) db = os.path.join(cur_dir, 'reviews.sqlite') def classify(document): label = {0:'negative', 1:'positive'} X = vect.transform([document]) y = clf.predict(X)[0] proba = np.max(clf.predict_proba(X)) return label[y], proba def train(document, y): X = vect.transform([document]) clf.partial_fit(X, [y]) def sqlite_entry(path, document, y): conn = sqlite3.connect(path) c = conn.cursor() c.execute("INSERT INTO review_db (review, sentiment, date) VALUES (?, ?, DATETIME('now'))", (document, y)) conn.commit() conn.close() ##### 플라스크 class ReviewForm(Form): moviereview = TextAreaField('', [validators.DataRequired(), validators.length(min=15)]) @app.route('/') def index(): form = ReviewForm(request.form) return render_template('reviewform.html', form=form) @app.route('/results', methods=['POST']) def results(): form = ReviewForm(request.form) if request.method == 'POST' and form.validate(): review = request.form['moviereview'] y, proba = classify(review) return render_template('results.html', content=review, prediction=y, probability=round(proba*100, 2)) return render_template('reviewform.html', form=form) @app.route('/thanks', methods=['POST']) def feedback(): feedback = request.form['feedback_button'] review = request.form['review'] prediction = request.form['prediction'] inv_label = {'negative':0, 'positive':1} y = inv_label[prediction] if feedback == 'Incorrect': y = int(not(y)) train(review, y) sqlite_entry(db, review, y) return render_template('thanks.html') if __name__ == '__main__': app.run(debug=True)
Python

리뷰 폼 구성

reviewform.html은 다음과 같이 작성한다.
<!DOCTYPE html> <html> <head> <title>영화 분류</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css')}}"> </head> <body> <h2>영화에 대한 리뷰를 써 주세요:</h2> {% from "_formhelpers.html" import render_field %} <form method=post action="/results"> <dl> {{ render_field(form.moviereview, cols='30', rows='10') }} </dl> <div> <input type=submit value='리뷰 입력' name='submit_btn'> </div> </form> </body> </html>
HTML

결과 페이지 템플릿 만들기

results.html은 다음과 같이 작성한다.
<!DOCTYPE html> <html> <head> <title>Movie Classification</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css')}}"> </head> <body> <h3>입력한 영화 리뷰:</h3> <div>{{ content }}</div> <h3>예측:</h3> <div>이 영화 리뷰는 <strong>{{ prediction =='positive' and '긍정' or '부정' }}</strong>적입니다. (probability: {{ probability }} %).</div> <div id='button'> <form action="/thanks" method="post"> <input type="submit" value='맞음' name='feedback_button'> <input type="submit" value='틀림' name='feedback_button'> <input type="hidden" value='{{ prediction }}' name='prediction'> <input type="hidden" value='{{ content }}' name='review'> </form> </div> <div> <form action="/"> <input type="submit" value='리뷰 추가 입력'> </form> </div> </body> </html>
HTML
style.css는 다음과 같이 작성한다.
body { width: 600px; } .button { padding-top: 20px; }
CSS
thanks.html은 다음과 같이 작성한다.
<!DOCTYPE html> <html> <head> <title>영화 분류</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css')}}"> </head> <body> <h3>피드백을 보내 주셔서 감사합니다!</h3> <div id='button'> <form action="/"> <input type=submit value='리뷰 추가 입력'> </form> </div> </body> </html>
HTML

공개 서버에 웹 애플리케이션 배포

PythonAnywhere는 파이썬 웹 애플리케이션의 호스팅에 특화되어 있는 서비스이다. PythonAnywhere는 하나의 웹 애플리케이션을 무료로 실행할 수 있는 비기너(beginner) 계정을 제공한다.

PythonAnywhere 계정 만들기

(계정 만드는 내용 생략)

영화 분류 애플리케이션 업로드

(앞서 작성한 파일들을 디렉터리 구조에 맞게 업로드 하는 내용 생략)

영화 분류기 업데이트

사용자가 분류 결과에 대한 피드백을 전송하면 예측 모델이 업데이트 되지만, 웹 서버가 중단되거나 다시 시작되면 clf 객체에 업데이트한 내용이 모두 사라진다.
이를 영구적으로 유지하는 방법 중의 하나는 clf 객체를 업데이트될 때마다 pickle 모듈로 직렬화하는 방법인데, 이는 사용자가 늘어나면 비효율적이 되기도 하고, 여러 사용자가 피드백을 전달하면 pickle 파일이 망가질 수 있다.
다른 방법은 SQLite 데이터베이스에 수집된 피드백 데이터를 사용하여 예측 모델을 업데이트하는 것이다.
PythonAnywhere 서버에서 SQLite 데이터베이스를 내려받아 로컬 컴퓨터에서 clf 객체를 업데이트하고 새로운 pickle 파일을 PythonAnywhere에 업로드할 수 있다.
분류기를 로컬 컴퓨터에서 업데이트 하기 위해 movieclassifier 디렉터리에 update.py 스크립트를 만들어보자
import pickle import sqlite3 import os import numpy as np # 로컬 디렉터리에서 HashingVectorizer를 임포트한다. from vectorizer import vect def update_model(db_path, model, batch_size=10000): conn = sqlite3.connect(db_path) c = conn.cursor() c.execute('SELECT * from review_db') results = c.fetchmany(batch_size) while results: data = np.array(results) X = data[:, 0] y = data[:, 1].astype(int) classes = np.array([0, 1]) X_train = vect.transform(X) model.partial_fit(X_train, y, classes=classes) results = c.fetchmany(batch_size) conn.close() return model cur_dir = os.path.dirname(__file__) clf = pickle.load(open(os.path.join(cur_dir, 'pkl_objects', 'classifier.pkl'), 'rb')) db = os.path.join(cur_dir, 'reviews.sqlite') clf = update_model(db_path=db, model=clf, batch_size=10000)
Python
app.py에 update_model을 임포트하는 코드를 추가한다.
이러면 app.py 스크립트가 실행될 때 update_model 함수를 호출한다.
from update import update_model if __name__ == '__main__': clf = update_model(db_path=db, model=clf, batch_size=10000) app.run(debug=True)
Python