반응형
반응형


import numpy as np
import re
from nltk.corpus import stopwords
stop = stopwords.words('english')
def tokenizer(text):
text = re.sub('<[^>]*>', '', text)
emoticons = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)', text.lower())
text = re.sub('[\W]+', ' ', text.lower()) + ' '.join(emoticons).replace('-', '')
tokenized = [w for w in text.split() if w not in stop]
return tokenized

# generator 함수 stream_docs를 정의해서 한 번에 문서 하나를 읽어들이고 반환시키도록 한다.
def stream_docs(path):
with open(path, 'r', encoding='utf-8') as csv:
next(csv)
for line in csv:
text, label = line[:-3], int(line[-2])
yield text, label

# 테스트로 movie_data.csv 파일의 첫 번째 문서를 읽어보자
print(next(stream_docs(path='movie_data.csv')))

# stream_docs 함수로부터 문서 스트림을 읽어들이고 size파라미터에 특정 문서의 숫자를 반환하는
# get_minibatch 함수를 정의
def get_minibatch(doc_stream, size):
docs, y = [], []
try:
for _ in range(size):
text, label = next(doc_stream)
docs.append(text)
y.append(label)
except StopIteration:
return None, None
return docs, y

from sklearn.feature_extraction.text import HashingVectorizer
from sklearn.linear_model import SGDClassifier
vect = HashingVectorizer(decode_error='ignore', n_features=2**21, preprocessor=None, tokenizer=tokenizer)
clf = SGDClassifier(loss='log', random_state=1, n_iter=1)
doc_stream = stream_docs(path='movie_data.csv')

import pyprind
pbar = pyprind.ProgBar(45)
classes = np.array([0, 1])
for _ in range(45):
X_train, y_train = get_minibatch(doc_stream, size=1000)
if not X_train:
break
X_train = vect.transform(X_train)
clf.partial_fit(X_train, y_train, classes=classes)
pbar.update()

X_test, y_test = get_minibatch(doc_stream, size=5000)
X_test = vect.transform(X_test)
print('Accuarcy: %.3f' % clf.score(X_test, y_test))

clf = clf.partial_fit(X_test, y_test)

'''
머신러닝 모델을 웹 어플리케이션에 임베트하는 방법으로
데이터를 실시간으로 학습하는 방법을 익혀보자.
'''

# 피팅된 사이킷런 에스티메이터 직렬화
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)

# 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('-', '')
tokenized = [w for w in text.split() if w not in stop]
return tokenized

vect = HashingVectorizer(decode_error='ignore', n_features=2**21, preprocessor=None, tokenizer=tokenizer)


import pickle
import re
import os
#from vectorizer import vect
clf = pickle.load(open(os.path.join('pkl_objects', 'classifier.pkl'), 'rb'))


import numpy as np
label = {0:'negative', 1:'positive'}
example = ['I love this movie']
X = vect.transform(example)
print('Prediction: %s\nProbaility: %.2f%%' % (label[clf.predict(X)[0]], np.max(clf.predict_proba(X))*100))


반응형
반응형
pbar.py
import pyprind
import pandas as pd
import os
pbar = pyprind.ProgBar(50000)
labels = {'pos':1, 'neg':0}

df = pd.DataFrame()
'''
for s in ('test', 'train'):
for l in ('pos', 'neg'):
path = './aclImdb/%s/%s' % (s, l)
for file in os.listdir(path):
with open(os.path.join(path, file), 'r', encoding='utf-8') as infile:
txt = infile.read()
df = df.append([[txt, labels[l]]], ignore_index=True)
pbar.update()

df.columns = ['review', 'sentiment']

import numpy as np
np.random.seed(0)
df = df.reindex(np.random.permutation(df.index))
df.to_csv('./movie_data.csv', index=False)
'''
df = pd.read_csv('./movie_data.csv')
print('df.head(3):\n', df.head(3))
# pbar 실행

# 단어를 피처 벡터로 변환
import pandas as pd
import numpy as np
df = pd.DataFrame()
df = pd.read_csv('./movie_data.csv')
print('df.head(3):\n', df.head(3))

from sklearn.feature_extraction.text import CountVectorizer
count = CountVectorizer()
docs = np.array([
'The sun is shining',
'The weather is sweet',
'The sun is shining and the weather is sweet'
])
bag = count.fit_transform(docs)

print(count.vocabulary_)
print(bag.toarray())

'''
nd는 문서의 전체 개수, df(d,t)는 용어 t를 포함하는 문서 d의 개수
'''
from sklearn.feature_extraction.text import TfidfTransformer
tfidf = TfidfTransformer()
np.set_printoptions(precision=2)
print(tfidf.fit_transform(count.fit_transform(docs)).toarray())

print(df.loc[0, 'review'][-50:])

import re
def preprocessor(text):
text = re.sub('<[^>]*>', '', text)
emoticons = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)', text)
text = re.sub('[\W]+', ' ', text.lower()) + ' '.join(emoticons).replace('-', '')
return text

print(preprocessor(df.loc[0, 'review'][-50:]))
print(preprocessor("</a>This :) is :( a test :-)!"))

# 모든 영화 리뷰에 적용
df['review'] = df['review'].apply(preprocessor)

# 문서를 토큰으로 처리하기
def tokenizer(text):
return text.split()
print('tokenizer:\n', tokenizer('running like running and thus they run'))

from nltk.stem.porter import PorterStemmer
porter = PorterStemmer()

def tokenizer_porter(text):
return [porter.stem(word) for word in text.split()]

print('tokenizer_porter("runners like running and thus they run"):\n'
, tokenizer_porter('runners like running and thus they run'))

'''
불용어(stop-word) : 모든 종류의 텍스트에서 공통으로 많이 사용되는 단어들로 문서의 다른 종류들을 구별하는 데
유용할 만한 정보를 거의 가지고 있지 않은 (혹은 아주 조금만 가지고 있는) 경우를 말한다.
불용어의 예로는 is, and, has 같은 것들이 있다.
'''
import nltk
nltk.download('stopwords')

from nltk.corpus import stopwords
stop = stopwords.words('english')
[print(w) for w in tokenizer_porter('a runner likes running and runs a lot')[-10:] if w not in stop]

# 문서 분류를 위한 로지스틱 회귀 모델 훈련
X_train = df.loc[:25000, 'review'].values
y_train = df.loc[:25000, 'sentiment'].values
X_test = df.loc[25000:, 'review'].values
y_test = df.loc[25000:, 'sentiment'].values

# GridSearchCV 오브젝트를 사용하여 5-폴드(5-fold) 충화 교차검증을
# 사용하는 이번 로지스틱 회귀 모델에 대한 최적의 파라미터 세트를 찾아보자
from sklearn.grid_search import GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf = TfidfVectorizer(strip_accents=None, lowercase=False, preprocessor=None)
param_grid = [{'vect__ngram_range': [(1,1)],
'vect__stop_words': [stop, None],
'vect__tokenizer': [tokenizer, tokenizer_porter],
'clf__penalty': ['l1', 'l2'],
'clf__C': [1.0, 10.0, 100.0]},
{'vect__ngram_range': [(1,1)],
'vect__stop_words': [stop, None],
'vect__tokenizer': [tokenizer, tokenizer_porter],
'vect__use_idf': [False],
'vect__norm': [None],
'clf__penalty': ['l1', 'l2'],
'clf__C': [1.0, 10.0, 100.0]}]
lr_tfidf = Pipeline([('vect', tfidf),
('clf', LogisticRegression(random_state=0))])
gs_lr_tfidf = GridSearchCV(lr_tfidf, param_grid, scoring='accuracy', cv=5, verbose=1, n_jobs=1)
gs_lr_tfidf.fit(X_train, y_train)
print('Best parameter set : %s ' % gs_lr_tfidf.best_params_)

print('CV Accuracy: %.3f' % gs_lr_tfidf.best_score_)
clf = gs_lr_tfidf.best_estimator_
print('Test Accuracy: %.3f' % clf.score(X_test, y_test))
import numpy as np
import re
from nltk.corpus import stopwords
stop = stopwords.words('english')
def tokenizer(text):
text = re.sub('<[^>]*>', '', text)
emoticons = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)', text.lower())
text = re.sub('[\W]+', ' ', text.lower()) + ' '.join(emoticons).replace('-', '')
tokenized = [w for w in text.split() if w not in stop]
return tokenized

# generator 함수 stream_docs를 정의해서 한 번에 문서 하나를 읽어들이고 반환시키도록 한다.
def stream_docs(path):
with open(path, 'r', encoding='utf-8') as csv:
next(csv)
for line in csv:
text, label = line[:-3], int(line[-2])
yield text, label

# 테스트로 movie_data.csv 파일의 첫 번째 문서를 읽어보자
print(next(stream_docs(path='./machinelearning/movie_data.csv')))

# stream_docs 함수로부터 문서 스트림을 읽어들이고 size파라미터에 특정 문서의 숫자를 반환하는
# get_minibatch 함수를 정의
def get_minibatch(doc_stream, size):
docs, y = [], []
try:
for _ in range(size):
text, label = next(doc_stream)
docs.append(text)
y.append(label)
except StopIteration:
return None, None
return docs, y

from sklearn.feature_extraction.text import HashingVectorizer
from sklearn.linear_model import SGDClassifier
vect = HashingVectorizer(decode_error='ignore', n_features=2**21, preprocessor=None, tokenizer=tokenizer)
clf = SGDClassifier(loss='log', random_state=1, n_iter=1)
doc_stream = stream_docs(path='./machinelearning/movie_data.csv')

import pyprind
pbar = pyprind.ProgBar(45)
classes = np.array([0, 1])
for _ in range(45):
X_train, y_train = get_minibatch(doc_stream, size=1000)
if not X_train:
break
X_train = vect.transform(X_train)
clf.partial_fit(X_train, y_train, classes=classes)
pbar.update()

X_test, y_test = get_minibatch(doc_stream, size=5000)
X_test = vect.transform(X_test)
print('Accuarcy: %.3f' % clf.score(X_test, y_test))

clf = clf.partial_fit(X_test, y_test)


반응형
반응형

그동안 개인적으로 이런저런 일로 바쁘고, 많은 변화가 있어, 정말 오랜만에 포스팅을 하게 됩니다.


이번 포스팅에서는 여태까지 다루어 보았던 머신러닝 및 딥러닝, AI와 관련하여 그 기반이 되는 인공신경망에 대한 간략한 히스토리를 정리해보고자 합니다.

아, 물론 딥러닝의 꽃이라 부를 수 있는 CNN(Convolutional Neural Network)이나 RNN(Recurrent Neural Network) 등에 대해서는 제 블로그에서 아직 다루진 않았습니다만... 이에 대해서는 시간 나는대로 포스팅하도록 하겠습니다.


먼저 아래 그림으로 인공신경망에 대한 간략한 히스토리를 정리해봅니다.





그러면 위 그림을 보면서 인공신경망 발전의 역사를 살포시 살펴봅니다. (내용중 재탕하는 내용도 있으니 참고바랍니다.)


1943년 신경과학자인 Warren S. McCulloch과 논리학자인 Walter Pitts는 하나의 사람 뇌 신경세포를 하나의 이진(Binary)출력을 가지는 단순 논리 게이트로 설명했는데, 이를 McCulloch-Pitts 뉴런(MCP 뉴런)이라 부릅니다.



1957년 코넬 항공 연구소에 근무하던 Frank Rosenblatt은 MCP 뉴런 모델을 기초로 퍼셉트론(Perceptron) 학습 규칙이라는 개념을 고안하게 되는데, Rosenblatt은 하나의 MCP 뉴런이 출력신호를 발생할지 안할지 결정하기 위해, MCP 뉴런으로 들어오는 각 입력값에 곱해지는 가중치 값을 자동적으로 학습하는 알고리즘을 제안했습니다.


1958년 퍼셉트론이 발표된 후 같은 해 7월 8일자 뉴욕타임즈는 앞으로 조만간 걷고, 말하고 자아를 인식하는 단계에 이르는 컴퓨터 세상이 도래할 것이라는 다소 과격한 기사를 냈습니다.

하지만 1969년, 단순 퍼셉트론은 ​XOR 문제도 풀 수 없다는 사실을 MIT AI 랩 창시자인 Marvin Minsky 교수가 증명하였고, 아래 그림과 같은 다층 퍼셉트론(MLP)으로 신경망을 구성하면 XOR 문제를 풀 수 있다고 했습니다. 


그런데, Minsky 교수는 이러한 MLP에서 hidden layer의 가중치를 계산하는, 다시 말하면 MLP를 학습시키는 방법은 존재하지 않는다고 단정해버렸습니다.


이로 인해 떠들석했던 인공신경망과 관련된 학문과 기술은 더 이상 발전되지 않고 암흑기를 겪게 됩니다.


그러다가 1974년, 당시 하버드 대학교 박사과정이었던 Paul Werbos는 MLP를 학습시키는 방법을 찾게 되는데, 이 방법을 Minsky 교수에게 설명하지만 냉랭한 분위기속에 무시되버립니다.


Paul Werbos가 Minsky 교수에게 설명한 MLP를 학습시킬 수 있는 획기적인 방법이 바로 오류 역전파 (Backpropagation of errors)라는 개념입니다. 그냥 줄여서 역전파(Backpropagation)이라 부르기도 하지요.



이런 획기적인 방법 역시 당시 인공신경망의 대가와 학계로부터 무시당해버린 후 Werbos는 1982년 역전파에 대한 내용을 논문으로 발표하고 마무리합니다. Werbos의 역전파가 무시된 이후 10여년 넘게 AI 분야는 혹한기를 겪게 됩니다.


그러다가 1986년 인지심리학자이자 컴퓨터공학자였던 Geoffrey Hinton 교수는 Werbos가 제안한 오류 역전파 알고리즘에 대한 내용을 독자적으로 제안하게 됩니다. 하지만 Werbos가 수 년전에 먼저 논문으로 발표한 내용이기 때문에 Hinton 교수는 역전파 개념을 다시 발견했다고 보는 것이 맞을 겁니다.


그런데 다층구조로 되어 있는 MLP의 학습은 역전파를 통해 학습이 가능하지만 학습의 효과를 크게 하려면 인공신경망의 은닉층(hidden layer)를 많이 쌓아야 더 좋은 결과가 나올 수 있다는 것이 경험적으로 검증되었습니다.


하지만, 당시까지만 해도 활성함수로써 시그모이드 함수를 사용했고, 이는 은닉층의 개수가 많아질수록 역전파에 의한 가중치 계산이 불가능하게 되는 gradient vanishing이라는 문제에 직면하게 되었습니다.


gradient vanishing 문제는 인공신경망 분야의 2번째 암흑기가 시작되는 계기가 되버립니다.


1995년에는 당시 다른 방식으로 발전되었던 보다 단순한 머신러닝 알고리즘인 SVM, RandomForest와 같은 알고리즘이 손글씨 인식 등과 같은 분야에는 더 잘 작동한다고 Lecun 교수 등이 발표하기도 했습니다.

1998년 컴퓨터공학자인 Yann Lecun 교수는 1950년대 수행했던 고양이의 뇌파 실험에 영감을 얻어 이미지 인식을 획기적으로 개선할 수 있는 CNN(Convolutional Neural Network)라는 새로운 형태의 인공신경망을 고안하게 됩니다.




1950년대 수행했던 고양이의 실험은 고양이의 눈으로 보는 사물의 형태에 따라 뇌의 특정영역, 정확히 말하면 특정 뉴런만이 활성화 된다는 것을 알게 된 것입니다. Lecun 교수는 이러한 실험 결과에 영감을 얻어 CNN이라는 새로운 형태의 인공신경망을 고안하게 된 것입니다.




하지만 gradient vanishing 문제 등으로 인해 한동안 인공신경망 분야가 침체기를 겪고 있던 시기에, Hinton 교수와 Bengio 교수는 2006년, 2007년 두 편의 논문을 발표하였는데 초기 입력값을 잘 선택하면 아무리 인공신경망의 층의 개수가 많더라도 학습이 가능하며, 복잡한 문제도 층의 개수가 많게 구성된 인공신경망이라면 해결할 수 있다고 했습니다. 그리고 이렇게 층의 개수가 많은 신경망을 심층신경망(Deep Neural Network:DNN)이라 리브랜딩하고 심층신경망을 학습시키는 방법을 딥러닝(Deep Learning:DL)이라고 명명하게 됩니다.


그리고 2000년 네이처(Nature)지에 시그모이드를 대신하여 사용한 Rectifier라는 활성함수를 이용해 효과를 봤다는 내용의 생물학 분야의 논문이 발표되었습니다. Rectifier는 ReLU(Rectified Linear Unit)라고 불리게 되는 활성함수인데, 이 활성함수를 심층신경망의 딥러닝에 사용해보니 gradient vanishing 문제가 해결되버린 것입니다. 물론 심층신경망의 최종 출력층에서는 여전히 시그모이드나 SoftMax를 사용하지만 말이죠.


아무튼 활성함수 ReLU의 등장으로 딥러닝은 많은 발전을 이루게 되었고, 2012년 이미지 인식 분야의 유명한 대회인 ImageNet Large Scale Visiual Recognition Challenge(ILVRC)에서 캐나다 토론토 대학의 AlexNet이라 불리는 CNN 인공신경망으로 우승하게 되는데, 이전 까지 이미지 인식 오류율이 26%대였던 것이 15%대로 줄이게 된 것입니다.


이후 CNN의 신경망구조가 지속적으로 개선되고 GPU의 발전으로 현재는 5% 이내의 오류율로 학습이 가능하게 되었습니다.




현재 이미지 인식 수준은 아래 그림과 같이 주어진 사진을 설명하는 단계에 이르고 있습니다.



그리고 2016년 알파고가 등장하여 이세돌 9단을 4승1패로 이기면서 세계를 놀라게 했고, 현재는 인공신경망을 이용한 인공지능을 자율주행자동차와 같은 다양한 분야에 적용하고자 노력하고 있는 중입니다.

        


아마, 미래에는 스스로 생각하고 인지하는 인공지능이 탄생할지도 모르지요.



하지만....

아직까지 인공지능과 우리 인간의 뇌는 다릅니다!


  • 인간의 뇌는 약 1천억개의 뉴런과 100조개의 시냅스로 연결되어 있고, 20와트의 전력만으로 충분합니다.
  • 하지만 가장 거대한 인공신경망의 규모는 기껏해봐야 16,000개의 CPU코어상에서 1천만 뉴런과 10억개의 연결로 이루어져 있으며, 소모되는 전력은 3백만와트나 됩니다.
  • 인간의 뇌는 5개의 감각기관으로부터 5가지 유형의 입력만 받습니다.
  • 우리 아기들은 고양이를 학습하는데 라벨링된 10만장의 사진이 필요하지 않습니다.


솔직히 우리는,,, 학습이 이루어지는 우리 뇌의 기작을 잘 알지 못합니다.



이상으로 인공신경망에 대한 히스토리를 가볍게 살펴보았습니다.


아래 영상은 제가 ImageNet으로부터 다운받은 강아지 사진과 꽃 사진을 이용해 tensorflow로 학습을 하고 구글에서 제공해준 템플릿 코드를 이용하여 만들어본 안드로이드 프로그램을 구동한 것입니다.


왼쪽 동영상은 제 스마트폰으로 PC화면에 보이는 강아지와 꽃들을 촬영한 화면을 캡쳐 녹화한 것이고, 오른쪽 동영상은 우리집 강아지와 장미 조화를 스마트폰으로 촬영한 영상을 보인 것입니다. 화면에 표시되는 숫자는 이미지 인식 확률이며 1.0이 100%입니다.  


          


참고로 제 사진 50여장과 무작위 남자 사진 1000여장을 학습시켜본 결과 제 사진을 촬영했을 때 저를 인식한 확률이 매우 높았습니다.  

반응형
반응형

다층 퍼셉트론과 같은 인공신경망을 구현하는 것은 꽤 복잡하고 까다로운 작업이라는 것을 [37편]에서 잠시 느꼈을 것입니다. 그렇다면 우리가 구현한 인공신경망이 제대로 동작하는지, 혹은 정확도가 얼마인지 어떻게 측정해야 할까요?


인공신경망 구현의 핵심은 역전파라고 했으니, 역전파 알고리즘이 얼마나 정확하게 구현되었는지에 대해 검증하면 될 것 같습니다. 역전파 알고리즘을 우리가 직접 수동으로 검증하는 방법이 그라디언트 체킹(gradient checking)이라는 기법입니다.


이번 포스팅에서는 그라디언트 체킹에 대해 가볍게 살펴보고 넘어갑니다.


역전파에서 가중치를 업데이트하기 위해 계산해야 하는 식은 아래의 편미분 식이라는거 이미 알고 있습니다.



여기서 w는 신경망의 모든 가중치에 대한 행렬입니다. 이 미분값을 해석적 그라디언트(Analytical Gradient) 값이라 부릅니다.


도함수의 정의에 의해 해석적 그라디언트는 아래와 같이 표현됩니다.



[식1] 



[식1]의 오른쪽 항에서 ε이 매우 작은 값이라 할 때, 아래의 값으로 근사됩니다.



[식2] 



[식2]의 오른쪽 항의 값을 수치적 그라디언트(Numerical Gradient) 값이라 부릅니다.


아래 그림은 수치적 그라디언트의 기하학적인 의미를 나타낸 것입니다.


 



[식2]에서 ε을 -ε으로 바꾸어보면 아래의 식이 됩니다.



[식3]


[식2]와 [식3]의 양 변을 각각 더하면 아래의 식이 됩니다.




따라서,



[식4]



[식4]의 오른쪽 항을 새롭게 정의한 수치적 그라디언트로 채택합니다. 실제로 새롭게 정의한 수치적 그라디언트가 해석적 그라디언트와의 오차가 더 작게 나옵니다. 


수치적 그라디언트를 J'n 이라 하고 해석적 그라디어트를 J'a라 하면 절대오차 Err와 상대오차 Rerr는 다음과 같이 정의합니다.



[식5]






이 식에서 || ||는 행렬의 놈(norm)이며 행렬의 놈은 행렬의 모든 성분을 p제곱하여 더한 값의 p제곱근으로 정의됩니다.

n x m 행렬 A가 다음과 같을 때,

행렬 A의 놈은 아래와 같이 정의됩니다.



여기서 p=2인 경우를 프로베니우스 놈이라 부르며 가장 일반적으로 많이 사용되는 행렬 놈입니다.


우리가 구현한 역전파 알고리즘의 정확도는 해석적 그라디언트와 수치적 그라디언트의 상대 오차값으로 평가합니다. 역전파 알고리즘의 정확도를 평가하는 측도로써 절대 오차를 사용하지 않는 이유는 Err가 매우 작은 값이더라도 J'nJ'a  값 자체가 매우 작다면 상대적으로 오차율이 커지기 때문입니다.



상대오차 Rerr의 값에 따른 역전파 알고리즘의 정확도는 일반적으로 아래와 같은 규칙을 따릅니다.




이 규칙에 따라 [37편]에서 구현한 MLP를 검증해보도록 합니다.


아래의 코드를 [37편]의 dl_mlp_class.py에서 구현한 NeuralNetMLP 클래스의 메쏘드로 추가합니다.




이 코드는 [식5]의 Rerr를 계산하여 리턴하는 함수를 구현한 것입니다. 코드를 보면 그렇게 어려운 부분이 없습니다.


NeuralNetMLP 클래스의 fit() 메쏘드에서 역전파를 통해 경사를 계산하는 코드를 찾습니다.



이 코드 다음에 아래의 코드를 추가합니다.




그라디언트 체크 기능이 추가된 NeuralNetMLP 클래스가 완성되었습니다. 이제 아래의 코드를 작성합니다.




이 코드는 X_train, y_train의 5개 데이터에 대한 10회 학습의 그라디언트 체킹을 수행한 결과를 화면에 출력합니다. 시간이 제법 소요되므로 너긋하게 기다리면서 결과를 지켜보도록 합니다.


코드를 수행한 결과는 다음과 같습니다.


Warning: 3.537822909e-07
OK: 3.02628344172e-10
OK: 3.02609921621e-10
OK: 3.06158619281e-10
OK: 3.21874437906e-10
OK: 3.27658507646e-10
OK: 2.87684685361e-10
OK: 2.32833603418e-10
OK: 2.52932883134e-10
OK: 2.70109334393e-10



1회 학습을 빼고 2회~10회까지 그라디언트 체킹 결과는 매우 훌륭하다는 것을 알 수 있습니다. 따라서 [37편]에서 구현한 역전파 알고리즘은 제대로 구현한 것이라고 판단할 수 있습니다.


체킹이 마무리되면 fit()에 추가한 그라디언트 체킹 수행 코드를 주석 처리 해둡니다. 


반응형
반응형

이번 포스팅에서는 [34편]~[36편] 내용에서 다룬 다층 퍼셉트론을 파이썬으로 구현한 코드를 소개하고, MNIST의 손글씨 숫자 60,000개의 샘플을 딥러닝 학습을 수행한 후 손글씨 숫자에 대한 인식률을 살펴보는 것으로 하겠습니다.


MNIST 데이터는 다양한 사람들이 직접 쓴 숫자 0~9까지의 이미지 집합이며, 이미지의 크기는 28 x 28 크기로 표준화되어 있습니다. MNIST 데이터는 트레이닝을 위한 60,000개의 손글씨 숫자 이미지 데이터와 테스트를 위한 10,000개의 손글씨 숫자 이미지 데이터로 구성되어 있습니다.


먼저 아래의 링크를 눌러서 MNIST 데이터를 확보합니다.


☞ MNIST 데이터 받기



링크를 눌러 MNIST 데이터 홈페이지에 들어가면 아래와 같은 4개의 데이터 파일이 보일 것입니다.


  • train-images-idx3-ubyte.gz : training set images
  • train-labels-idx1-ubyte.gz : training set labels
  • t10k-images-idx3-ubyte.gz : test set images
  • t10k-labels-idx1-ubyte.gz : test set labels


이 파일들을 모두 다운로드 받아 특정 디렉토리에 저장한 후, 압축 프로그램을 이용해 압축을 해제합니다.


MNIST 데이터 홈페이지 아랫부분에 보면 이 파일들에 대한 포맷형식이 설명되어 있습니다. 파일 포맷을 요약해보면 다음과 같습니다.


TRAINING SET LABEL FILE (train-labels-idx1-ubyte):

[offset] [type]          [value]          [description] 
0000     32 bit integer  0x00000801(2049) magic number (MSB first) 
0004     32 bit integer  60000            number of items 
0008     unsigned byte   ??               label 
0009     unsigned byte   ??               label 
........ 
xxxx     unsigned byte   ??               label


label 값은 0~9까지임


TRAINING SET IMAGE FILE (train-images-idx3-ubyte):

[offset] [type]          [value]          [description] 
0000     32 bit integer  0x00000803(2051) magic number 
0004     32 bit integer  60000            number of images 
0008     32 bit integer  28               number of rows 
0012     32 bit integer  28               number of columns 
0016     unsigned byte   ??               pixel 
0017     unsigned byte   ??               pixel 
........ 
xxxx     unsigned byte   ??               pixel


 픽셀값은 1바이트씩 아래로 쭉 연결됨. 28x28=784바이트 단위로 하나의 손글씨 이미지로 구분되며 pixel값이 0이면 흰색 바탕, 255이면 검정색 숫자 부분을 의미함



테스트를 위한 파일 포맷도 트레이닝 데이터와 마찬가지 포맷입니다. 트레이닝을 위한 라벨 데이터가 저장된 파일의 최초 8바이트에는 아이템 개수 등의 정보가 저장되어 있고 라벨은 9바이트째부터 시작됩니다. 손글씨 숫자 이미지가 저장된 파일의 최초 16바이트에는 이미지 개수, 가로 세로 픽셀수 등의 정보가 저장되어 있고 이미지 픽셀 데이터는 17바이트째부터 시작됩니다.


자, 그럼 아래의 코드를 봅니다.


dl_load_digits.py



위 코드의 load_mnist(path, kind='train')는 라벨 데이터가 저장된 파일과 이미지 데이터가 저장된 파일로부터 데이터를 모두 읽어 각각 numpy 배열 labels, images로 저장하여 리턴하는 함수입니다.


참고로 이 포스팅에서는 학습을 위한 데이터가 저장된 폴더는 dl_load_digits.py가 저장된 폴더 아래에 data/mnist 폴더입니다. 따라서 여러분들의 학습 데이터가 저장된 폴더 위치에 맞게 경로를 조정해줘야 한다는 거 잊지마세요~


코드에 등장하는 struct.unpack()은 저의 첫번째 책 '암호와 해킹'에서 간략하게 설명되어 있는데, 간단히 요약하여 설명하면, 파이썬의 struct 모듈은 파이썬 바이트 객체로 표현된 C구조체와 파이썬에서 사용하는 값을 상호 변환하는데 사용되는 각종 메쏘드들을 제공합니다.


>>> magic, n = struct.unpack('>II', imgpath.read(8))


이 코드의 의미는 imgpath 파일에서 8바이트 데이터를 big-endian(>)으로 읽고, 읽은 데이터를 unsigned integer(I) 2개로 나누어서 각각 magic, n 변수에 대입하라는 뜻입니다. 


이 코드를 실행하면 4개의 MNIST 데이터 파일을 모두 읽어, 트레이닝 이미지 데이터는 X_train, 트레이닝 라벨 데이터는 y_train, 테스트 이미지 데이터는 X_test, 테스트 라벨 데이터는 y_test로 저장하고 이미지 샘플의 개수 정보를 화면에 표시합니다.


코드를 실행하면 다음과 같은 결과가 나오면 데이터를 성공적으로 로드한 것입니다.


학습 샘플수     : 60000, 컬럼수: 784

테스트 샘플수  : 10000, 컬럼수: 784



이제, 아래의 코드를 dl_load_digits.py 아래 부분에 추가합니다.


dl_show_09digits.py



이 코드는 X_train에 저장된 0~9까지 이미지를 화면에 출력하는 코드입니다. 코드를 실행하면 다음과 같은 손글씨 숫자가 화면에 나올 겁니다.





위 코드를 아래와 같이 약간 수정해서 같은 숫자 25개에 대한 이미지를 화면에 출력하도록 해봅니다.


dl_show_samedigits.py



이 코드를 실행하면 아래와 같이 숫자 6에 대한 다양한 손글씨 이미지 25개를 화면에 출력합니다.




dl_show_samedigits.py 코드에서 y_train == 6 부분을 y_train == 5 등으로 바꾸어 다른 숫자들도 확인해보세요. 이로써 우리가 확보한 MNIST 손글씨 데이터가 어떤 식으로 되어있는지 확인을 했네요~


이제는 이 손글씨 데이터를 학습할 다층 퍼셉트론을 파이썬으로 구현하는 것입니다. 이 포스팅에 제시된 대부분의 소스코드는 Sebatian Raschka의 책 "Python Machine Learning"에서 발췌한 것이며 필요시 제가 조금 수정하거나 추가한 코드도 있기도 합니다. 아래에 제시된 MLP 구현 코드도 이 책에서 발췌한 것인데, 딥러닝 성능을 위해 추가적인 알고리즘이 적용되어 있는 제대로 된 다층 퍼셉트론을 구현한 코드입니다. 


dl_mlp_class.py






음... 코드가 참 깁니다. 딥러닝의 세계는 쉽지 않은 길입니다. NeuralNetMLP라는 이름의 클래스가 바로 다층 퍼셉트론을 구현한 부분입니다. 코드의 세부적인 내용을 이 포스팅에서 일일이 서술하는 것은 무리일 것 같아서, 함수 단위로 설명을 하도록 합니다.


먼저, NeuralNetMLP 클래스의 초기값을 위해 입력되는 인자들이 굉장히 많습니다. 이 인자들에 대해 가볍게 살펴보는 것으로 시작해봅니다.


  • n_output:
    • 출력층의 출력값 개수. 손글씨 숫자의 경우 0~9까지 10으로 지정하면 됨
  • n_features:
    • 입력층에 입력되는 특성값의 개수. 28x28 픽셀의 이미지 데이터이므로 784로 지정하면 됨
  • n_hidden:
    • 은닉층의 노드 개수. 손글씨 숫자 학습을 위해 50으로 지정할 것임
  • l1:
    • L1 정규화를 위한 람다값. 오버피팅 방지를 위한 것임
  • l2:
    • L2 정규화를 위한 람다값. 오버피팅 방지를 위한 것임
  • epochs:
    • 학습 반복 회수
  • eta:
    • learning rate η
  • alpha:
    • 가중치 업데이트를 보다 고속으로 처리하기 위한 모멘텀 학습 파라미터
  • decrease_const:
    • 학습 수렴률을 향상시키기 위해 learning rate을 학습 반복에 따라 감소시키기 위한 감쇠 상수
    • 처음에는 learning rate을 다소 큰 값으로 잡아 최소값에 빨리 다다르게 한 후, 감쇠 상수를 ​지속적으로 곱해서 learning rate을 점점 작아지게 할 용도로 사용됨
  • shuffle:
    • 매 반복마다 트레이닝 데이터를 뒤섞기를 위한 플래그. True이면 뒤섞음
  • minibatches:
    • 매 학습에 사용되는 무작위로 추출할 트레이닝 데이터의 실제 개수. 확률적 경사하강법 적용 개념임
    • 모집단에서 일정 크기의 표본을 추출하여 ​결과를 통계적으로 예측하는 것과 비슷한 개념


이것으로 NeuralNetMLP의 초기화를 위한 인자에 대해 설명했습니다. 보면 우리가 아직 다루지 않은 모멘텀 학습 파라미터라든가 감쇠 상수 같은 내용도 있는데, 그냥 이런 것들이 있다는 것만 알고 넘어갑니다.

아무튼 NeuralNetMLP는 [34편], [35편], [36편]에서 다룬 내용을 코드화 한 것으로 볼 수 있습니다.


이제 NeuralNetMLP의 각 함수에 대해 가볍게 설명합니다.


  • _encode_labels(self, y, k)
    • 출력층 10개 노드에서 출력되는 값이 (1, 0,...0)이면 0, (0, 1, 0,,,)이면 1, (0, 0, ...,1)이면 9를 의미하는 것으로 정의함
    • y는 손글씨 숫자의 라벨링 데이터, k는 출력층의 출력값 개수


  • _initialize_weights(self)
    • 입력층과 은닉층 사이의 가중치 w1과 은닉층과 출력층 사이의 가중치 w2의 값을 초기화함. 바이어스 항도 포함시킴


  • _sigmoid(self, z)
    • z에 대한 시그모이드 함수값을 리턴함. scipy.expit()은 시그모이드 함수임


  • _sigmoid_gradient(self, z)
    • z에 대한 시그모이드 함수의 미분값을 리턴함.


  • _add_bias_unit(self, X, how='column')
    • X에 바이어스 값을 추가해서 X_new로 둠. 행렬 계산의 특성상 입력층에서 은닉층, 은닉층에서 출력층으로의 계산을 위해 how 값을 'column', 'row'로 지정하여 바이어스를 행에 더하거나, 열에 더하도록 함


  • _feedforward(self, X, w1, w2)
    • w1, w2 가중치로 X를 순전파 시킵니다. 순전파한 결과는 a1, z2, a2, z3, a3로 리턴함


  • _L2_reg(self, lambda_, w1, w2), _L1_reg(self, lambda_, w1, w2)
    • L2 정규화와 L1 정규화 값을 리턴함


  • _get_cost(self, y_enc, output, w1, w2)
    • 로지스틱 비용함수 J를 리턴함. J에는 정규화를 위한 값이 추가되었음


  • _get_gradient(self, a1, a2, a3, z2, y_enc, w1, w2)
    • 역전파 알고리즘을 구현한 함수


  • predict(self, X)
    • X에 대한 예측값을 리턴


  • fit(self, X, y, print_progress=True)
    • 트레이닝 데이터 X, y를 이용해 다층 퍼셉트론을 학습시킴. 학습의 속도를 위해 minibatches로 지정된 개수만큼 데이터를 무작위로 추출하여 학습을 시킴 



코드의 세부적인 내용은 언급안했으나, 각 함수들의 역할과 이전 포스팅 [34편]~[36편]에서 다루었던 내용을 참고하여 코드를 다시 한번 보면 코드의 내용을 이해할 수 있을 것입니다.


이 코드를 dl_load_digits.py 맨 마지막 부분에 추가합니다.



이제는 구현한 다층 퍼셉트론으로 60,000개의 데이터에 대해 학습을 수행하는 코드를 볼 차례입니다.


dl_mlp_learning.py



이 코드는 결국 784-50-10 다층 퍼셉트론을 구성하고 50개의 데이터를 무작위로 추출한 후 1000번 반복 학습을 수행하는 코드입니다. 학습을 수행하는데 소요되는 시간은 컴퓨터의 성능에 따라 10~30분정도 소요됩니다. 한번 학습한 것을 또 학습할 수는 없으므로 pickle을 이용해 학습된 mlp 객체를 파일로 저장하여 나중에 다시 불러 사용할 수 있게 합니다.


추가한 코드를 실행합니다. 시간이 걸리므로 잠시 다른 일을 하고 와도 됩니다.


학습이 종료되면 학습 결과가 mlp_digits.pkl로 저장됩니다. 이 파일을 불러오려면 아래의 코드를 활용합니다.


dl_load_mlpobj.py



사용법은 이렇습니다. 여태까지 작성된 코드에서 dlp_mlp_learning.py 코드 부분은 주석 처리해서 나중에 트레이닝 데이터가 갱신되게 되면 재사용할 수 있도록 합니다. 그런 후, 주석 처리된 코드 아랫 부분에 dl_load_mlpobj.py 코드를 추가합니다. 이게 끝입니다. 이젠 10분 이상 걸리는 학습을 더 이상 수행하지 않아도 됩니다.


이제 학습의 결과가 어떠한지 살펴볼까요~. 일단 비용함수 J가 어떤 식으로 변화하는지 그래프로 살펴봅니다. 아래의 코드를 여태까지 작성한 코드 마지막 부분에 추가합니다.


dl_plot_cost1.py



추가한 코드를 실행하면 다음과 같은 결과가 화면에 나옵니다.




이 그래프는 실제로 비용함수의 값이 오르락 내리락 하는 그래프인데 실제로 그렇게 보이지 않네요. 아무튼 비용함수의 값이 오르락 내리락하면서 편차가 심합니다. 하지만 40,000번째 이후에는 오르락 내리락 하면서도 일정한 범위로 수렴한다는 것을 볼 수 있습니다.


그런데 이 녀석은 minibatches의 단위 50개의 데이터를 처리하는 과정에서 매회 비용함수를 계산한 것입니다. x축의 값을 보면 1,000번 반복한 것이 아니라 50,000번 반복한 것으로 나오는 이유입니다. 따라서 50개의 데이터를 처리한 것의 평균 비용함수의 값을 그래프로 나타내 봅니다.


dl_plot_cost1의 코드를 다음과 같이 수정합니다.


dl_plot_cost2.py



수정된 코드를 실행하면 다음과 같은 결과가 나옵니다.




그래프를 보면 800회 학습 이후에 비용함수가 특정값으로 수렴하고 있음을 알 수 있습니다.


이제는 학습된 결과를 살펴보도록 합니다. 아래는 60,000개의 학습 데이터를 이용해 학습을 시키고 학습 데이터에 대한 학습 정확도를 계산하는 코드입니다. 


dl_mlp_predict_train.py 

 


이 코드를 실행하면 다음과 같은 결과가 나옵니다.


예측성공/총개수: [58680]/[60000]

딥러닝 정확도: 97.80%



dl_mlp_predict1.py를 조금 수정하여 테스트 데이터에 대한 정확도를 계산해봅니다.


dl_mlp_predict_test.py


이 코드를 실행하면 다음과 같은 결과가 나옵니다.


예측성공/총개수: [9597]/[10000]
딥러닝 정확도: 95.97%



두 개의 결과를 비교해보면 테스트 데이터에 대한 정확도가 트레이닝 데이터에 대한 정확도보다 쪼금 작습니다. 이는 학습 결과가 약간 오버피팅이 되었음을 알 수 있습니다.


그렇다면 어떤 손글씨 숫자들이 제대로 인식이 안되었는지 확인해보죠~


아래의 코드를 여태까지 작성한 코드 마지막에 추가합니다.



위 코드를 추가한 코드를 실행하면 아래와 같은 결과가 화면에 보입니다.




우리가 학습시킨 MLP가 잘못 인식한 숫자들 중 일부입니다. 그런데, 일부 숫자는 사람이 보기에도 구분이 안되는 것이 있습니다. 1), 6), 7), 8), 13), 17), 22), 23), 25) 등은 우리 눈으로 보기에도 헷갈리는 숫자들입니다.


아무튼 이런 데이터들에 대해서는 사용자로 하여금 피드백을 줘서 제대로 된 숫자로 인식하도록 학습시키면 될 것입니다. 사용자의 피드백을 재학습시키는 내용에 대해서는 [22편] 마지막 부분에서 다루었습니다.  



만약 여러분들이 직접 쓴 숫자를 여기서 학습한 MLP를 이용해 인식하려고 하면 아래와 같은 로직으로 프로그램을 작성하면 됩니다.


  1. 여러분이 손으로 쓴 숫자를 이미지로 저장합니다.
  2. 이미지의 크기를 OpenCV의 cv2.resize()를 이용해 28 x 28 크기로 변환합니다.
  3. OpenCV의 cv2.threshold()를 이용해 숫자 부분과 배경부분을 검정색과 흰색으로 변환시켜 줍니다.
  4. 이렇게 변환된 이미지 픽셀을 numpy로 저장하고 ravel()을 이용해 1차원으로 변형한 후 이 값을 X로 둡니다.
  5. y_pred = mlp.predict(X)로 예측값인 y_pred를 확인하면 됩니다. 


OpenCV에 대한 자세한 내용은 제 블로그의 OpenCV 강좌를 참조하면 됩니다. 


이상으로 다층 퍼셉트론을 이용한 손글씨 숫자를 인식하는 내용은 마무리 하도록 하겠습니다.


다음 포스팅에서는 구현한 역전파 알고리즘의 정확도를 체크하는 방법인 gradient checking에 대해 가볍게 살펴보도록 하고, 이후 포스팅부터는 또 다른 딥러닝 방법인 Convolutional Neural Network(CNN)과 Recurrent Neural Network(RNN)에 대한 내용을 다루도록 하겠습니다.


반응형
반응형

[35]편에서 역전파에 대한 개념적인 내용을 살펴보았습니다. 그런데, [35편]에서 역전파 개념을 설명할 때 도입한 비용함수 J(w)는 아래와 같이 오차제곱합으로 정의했습니다.


 


여기서 Φ(z)는 활성 함수인 시그모이드 함수값인거죠.



그리고 z는 각 층에서 순입력 함수값인거 아실겁니다.


그런데 활성함수가 시그모이드이고 오차제곱합으로 정의된 비용함수는 경사하강법을 적용하는데 약간의 문제가 있습니다. 오차제곱합으로 정의된 비용함수 J(w)를 그래프로 그려보면 아래와 비슷한 모양이 됩니다.



이런 모양을 이루고 있는 함수에서 경사하강법을 적용하게 되면 우리가 원하는 최소값에 도달하는 것이 아니라 아래 그림과 같이 국소적인 부분에서의 최소값에 도달하고 더 이상 진행할 수 없는 상태가 될 수 있습니다.




경사하강법이 성공적으로 적용되려면 볼록형태의 그래프를 가진 함수이어야 합니다. 그래서 머리 좋은 몇몇 사람들이 로지스틱 회귀를 위한 비용함수를 경사하강법이 적용될 수 있도록 그래프가 볼록한 모양으로 되는 새로운 비용함수를 아래와 같이 정의하게 됩니다.



하~ 복잡한 수식이네요.. 아무튼 위 식에서 n은 트레이닝 데이터의 개수이며, t는 출력층의 출력값의 개수입니다. 그리고 y는 트레이닝 데이터에 대한 실제값이며 a는 트레이닝 데이터에 대한 활성 함수값 즉 학습에 의한 결과값입니다.


그런데 이 수식은 복잡해보여도 트레이닝 데이터에 대한 실제 결과값 y의 값을 0 또는 1로 정의해버리면 단순하게 됩니다. y값이 1이면 J(w)의 두번째 항이 0으로 되고, y값이 0이면 J(w)의 첫번째 항이 0으로 됩니다. 즉,



이 두 개의 식을 하나로 표현한 것이 저 위에 있는 복잡해보이는 수식이 됩니다. 자, 이제 [35편]에서 언급한 가중치 업데이트 식을 다시 가져와 봅니다.




가중치 업데이트를 하려면 J에 대한 w의 미분값이 필요한거 이제 다 아는 이야기입니다. 위 J(w) 식에서 a는 시그모이드 함수 Φ(z)입니다. 시그모이드 함수 Φ를 미분하면 Φ(1-Φ)가 됩니다. 이 부분을 유념하면서 진행하도록 합니다.



[35편]에서 역전파 개념 이해를 위해 도입된 2-2-2 다층 퍼셉트론을 다시 불러옵니다.




여기서 순전파에 있어 값의 흐름을 다시 불러와서 적어보면 다음과 같습니다.


[식 1] 


그리고, 출력층 a1(3), a2(3)에서의 출력값과 실제값의 오차 J1, J2는 로지스틱 회귀의 비용함수를 이용해 다음과 같이 정의합니다.


[식2]


 



이제 [35편]에서 설명한 바와 마찬가지로 방법으로 역전파를 이용해 가중치를 업데이트 해보도록 합니다. 계산의 편의를 위해 learning rate η는 1로 둡니다.


먼저, w1,1(2)에 대한 업데이트 식을 구해봅니다. 이를 위해 Jtotal을 w1,1(2)에 대해 미분한 값을 구해야겠지요~ a1(3)에서 Jtotal은 J1이므로,



[식2]와 [식1]을 참조하여 위의 식을 계산하면 다음과 같습니다.




따라서



마찬가지로 Jtotal을 w1,2(2)에 대해서 미분한 값은 다음과 같습니다.




동일한 방법으로 가중치 w2,1(2), w2,2(2)에 대해서 계산해보면 다음과 같은 결과가 유도됩니다.



이 식들을 하나의 식으로 표현하면 다음과 같이 됩니다.


 



위 식을 아래와 같은 식으로 단순화하여 표현해봅니다.


[식3] 



여기서, j, k는 각각 은닉층의 출력값 개수 범위와 출력층의 출력값 개수 범위를 적용하면 되므로, 이로써 출력층에서 은닉층으로의 역전파에 대해 파이썬으로 구현하기 위한 알고리즘을 마련한 것 같습니다.



[35편]을 참조하여 은닉층에서 입력층으로의 역전파시 가중치 업데이트 식을 계산하여 정리하면 다음과 같습니다.


[식4]




여기서, i는 입력층의 입력값 개수가 범위이며, [식4]를 통해 은닉층에서 출력층으로의 역전파에 대해 파이썬으로 구현하기 위한 알고리즘도 마련되었습니다.



n1-n2-n3 다층 퍼셉트론에서 역전파시 가중치 업데이트를 위한 식은 아래와 같이 표현할 수 있습니다.


[식5]



이로써, 역전파를 통해 가중치를 업데이트하는 일반화된 식을 이용해 파이썬으로 잘 구현하면 될 것 같습니다.

실제로 다층 퍼셉트론을 코드로 구현할 때, 성능 향상을 위하여 비용함수에 정규화 L1, L2를 적용한 항을 추가하여 계산하게 됩니다.


여기서 잠깐..

[35편]과 [36편]에서 역전파를 이해하기 위해 도입한 활성함수는 시그모이드 함수입니다. 시그모이드 함수는 활성함수로서 여러가지 장점이 있는 함수임에는 틀림이 없으나 은닉층이 매우 많은 심층신경망에서는 단점이 존재하는데, 신경망의 은닉층이 많아질수록 역전파에 의한 가중치 보정이 의미가 없어지는 gradient vanishing 문제가 발생합니다.


gradient vanishing은 역전파를 수행하는 은닉층의 개수가 많아지면 J(w)의 w에 대한 편미분값이 0에 가까와져서 가중치 업데이트 식에 의한 가중치 갱신이 거의 이루어지지 않기 때문에 발생하는 현상입니다. 이는 [식5]의 δ의 절대값이 1보다 작기때문에 δ를 곱하면 곱할수록 0에 가까와지기 때문입니다.


gradient vanishing 현상을 해결하기 위해 새로운 활성함수를 도입하게 되는데 바로 ReLU(Rectified Linear Unit)라는 함수입니다. ReLU는 아래 그래프와 같이 입력값이 0보다 작거나 같으면 0, 0보다 크면 입력값을 리턴하는 함수입니다.



심층신경망에서 활성함수로써 ReLU는 시그모이드보다 훨씬 나은 성능을 보이며, 대부분의 심층신경망에서 활성함수로 널리 사용되고 있습니다.


따라서 [35편]과 [36편]에서 보인 시그모이드를 활성함수로 한 예시는 역전파의 개념을 이해하는데 만족하시고 실제 응용시에는 ReLU를 활성함수로 적용하면 됩니다.


다음 포스팅에서는 Sebstian Raschka의 책 "Python Machine Learning"에 있는 다층 퍼셉트론을 구현한 파이썬 코드를 소개하고, MNIST에서 제공하는 손글씨 숫자 이미지 60,000개를 이용해 MLP 딥러닝 학습을 수행한 후 손글씨 숫자 인식률에 대해서 살펴보도록 합니다. 


반응형
반응형

1958년 퍼셉트론이 발표된 후 같은 해 7월 8일자 뉴욕타임즈는 앞으로 조만간 걷고, 말하고 자아를 인식하는 단계에 이르는 컴퓨터 세상이 도래할 것이라는 다소 과격한 기사를 냈습니다.

하지만 1969년, 단순 퍼셉트론은 ​XOR 문제도 풀 수 없다는 사실을 MIT AI 랩 창시자인 Marvin Minsky 교수가 증명하였고, 다층 퍼셉트론(MLP)으로 신경망을 구성하면 XOR 문제를 풀 수 있으나, 이러한 MLP를 학습시키는 방법은 존재하지 않는다고 단정해버렸습니다.


이로 인해 떠들석했던 인공신경망과 관련된 학문과 기술은 더 이상 발전되지 않고 침체기를 겪게 됩니다.


그런데 1974년, 당시 하버드 대학교 박사과정이었던 Paul Werbos는 MLP를 학습시키는 방법을 찾게 되는데, 이 방법을 Minsky 교수에게 설명하지만 냉랭한 분위기속에 무시되버립니다.

Paul Werbos가 Minsky 교수에게 설명한 MLP를 학습시킬 수 있는 획기적인 방법이 바로 오류 역전파 (Backpropagation of errors)라는 개념입니다.


이제 오류 역전파(앞으로 그냥 역전파라고 부르겠습니다)가 무엇인지 살펴보도록 합니다.

심층 신경망을 학습한다는 것은 최종 출력값과 실제값의 오차가 최소가 되도록 심층 신경망을 이루는 각 층에서 입력되는 값에 곱해지는 가중치와 바이어스를 계산하여 결정하는 것을 말합니다.


우리는 [6편] 아달라인을 학습할 때 인공 신경망의 출력값과 실제값의 오차가 최소가 되도록 가중치를 결정하는 방법인 경사하강법에 대해 다룬적이 있습니다.


경사하강법은 오차 곡선을 따라 일정한 크기로 내려가면서 최소값을 찾아가는 방법인데, 일정한 크기는 해당 지점에서 접선의 기울기와 learning rate으로 결정한다고 했지요.


기억이 가물거리면 다시 [6편]을 학습하고 옵니다.



☞ 경사하강법 다시보러 가기



심층 신경망을 학습하는데 유용하게 활용되는 역전파(backpropagtion) 알고리즘도 결국 이 경사하강법을 이용합니다. 그러면 역전파가 무엇인지 그 개념에 대해 대충(?) 살펴보도록 합니다.


먼저 아래와 같은 2-2-2 다층 퍼셉트론이 있다고 생각해봅니다. 여기서는 역전파의 개념만 살펴볼 예정이므로 각 층에 존재하는 바이어스는 생략했습니다. 또한 용어의 통일성을 위해 입력층의 x1, x2는 a1(1), a2(1)로 표기하기로 합니다.




[34편]에서 l층의 i번째 노드와 l+1층의 j번째 노드를 연결하는 가중치 w를 다음과 같이 정의했지요~




이 구조에서 적용되는 활성 함수는 시그모이드 함수 φ입니다.




입력층 -> 은닉층 사이의 값의 흐름은 다음과 같습니다.


[식1] 



그리고 은닉층 -> 출력층 사이의 값의 흐름은 다음과 같습니다.


[식2] 



이와 같이 입력층의 a1(1), a2(1)을 시작으로 출력층의 a1(3), a2(3) 값이 출력되는 과정을 순전파(feedforward)라 부릅니다.


우리는 아달라인을 다룰 때 오차제곱합을 비용함수로 도입해서, 이 비용함수가 최소가 되도록 가중치를 결정했습니다. 물론 가중치를 결정하는 방법은 경사하강법이었죠.


여기서도 비용함수 J를 오차제곱합으로 정의를 해보면 다음과 같이 될 겁니다.




여기서 y1, y2는 트레이닝 데이터에 대한 각 노드에 대응되는 실제값입니다. 순전파 1회가 마무리되면 J1과 J2의 값이 결정되겠죠.

입력값 a1(1), a2(1)과 실제값 y1, y2는 고정된 값이므로 J1과 J2는 결국 가중치 w1,1(1)~w2,2(2)를 변수로 가지는 함수가 됩니다.

우리의 목표는 J1과 J2의 값이 최소가 되도록 w1,1(1)~w2,2(2)를 결정해야 합니다.


아달라인에서와 마찬가지로 다층 퍼셉트론에서도 비용함수의 최소값을 찾아가는 방법으로 경사하강법을 활용한다고 했습니다. 각 층에서 가중치를 업데이트하기 위해서는 결국 각 층에서의 비용함수(오차로 불러도 됩니다.)의 미분값이 필요하게 되는 것이지요.


이를 매우 효율적으로 해결하기 위한 방법이 바로 역전파 알고리즘입니다.

역전파란 역방향으로 오차를 전파시키면서 각층의 가중치를 업데이트하고 최적의 학습 결과를 찾아가는 방법입니다.


자, 이 경우를 한번 생각해봅니다.

순전파에 있어서 가중치의 값을 매우 미세하게 변화시키면 비용함수 J1, J2도 매우 미세하게 변화될 겁니다. 매우 미세하게 변화시킨다는 것은 미분을 한다는 이야기입니다. 그런데, 매우 미세한 영역으로 국한할 때 가중치의 미세변화와 이에 따른 비용함수의 미세변화가 선형적인 관계가 된다고 알려져 있습니다. 선형적인 관계라는 이야기는 결국 비용함수를 매우 미세하게 변화시키면 이에 따라 가중치도 매우 미세하게 변화되는데 이 역시 선형적인 관계라는 이야기입니다.


이런 원리에 의해, 순전파를 통해 출력층에서 계산된 오차 J1, J2의 각 가중치에 따른 미세변화를 입력층 방향으로 역전파시키면서 가중치를 업데이트하고, 또 다시 입력값을 이용해 순전파시켜 출력층에서 새로운 오차를 계산하고, 이 오차를 또다시 역전파시켜 가중치를 업데이트하는 식으로 반복합니다. 


역전파를 이용한 가중치 업데이트 절차는 아래와 같이 요약될 수 있습니다.


  1. 주어진 가중치 값을 이용해 출력층의 출력값을 계산함(순전파를 통해 이루어짐)
  2. 오차를 각 가중치로 미분한 값(실제로는 learning rate을 곱한 값)을 기존 가중치에서 빼줌(경사하강법을 적용하는 것이며, 역전파를 통해 이루어짐)
  3. 2번 단계는 모든 가중치에 대해 이루어짐
  4. 1~3단계를 주어진 학습회수만큼 또는 주어진 허용오차값에 도달할 때가지 반복함


2번 단계의 가중치 업데이트 식을 수식으로 표현하면 다음과 같습니다.




여기서 Jtotal은 해당 노드가 영향을 미치는 오차의 총합입니다. 예를 들면 출력층의 노드 a1(3)에서는 다른 노드의 오차에 영향을 전혀 미치지 않으므로 해당 노드에서 오차인 J1만 따지면 되지만, 은닉층의 a1(2) 노드는 출력층의 a1(3), a2(3) 노드의 오차에 영향을 미치므로 J1, J2 모두 더한 것이 Jtotal 값이 됩니다.


이번에는 역전파를 통한 가중치 업데이트 메커니즘을 살펴볼 것이므로 편의상 learning rate η는 1로 둡니다.


자, 그러면 실제로 역전파를 이용해 가중치를 업데이트 해보도록 하죠. 먼저 가중치 w1,1(2)에 대해서 계산을 해봅니다.

 w1,1(2)에 대한 업데이트 식은 아래와 같습니다.




미분의 연쇄법칙에 의해 오차의 가중치에 대한 미분값은 아래의 식으로 표현될 수 있습니다.




이제 위 식의 오른쪽 항에 있는 3개의 미분값을 하나하나 구해보도록 하지요~ 참고로 이 포스팅의 첫부분에서 서술한 순전파를 통한 값의 흐름을 요약한 수식을 참고하기 바랍니다.


역전파의 출발노드인 a1(3)에서 Jtotal의 값은 J1입니다. 따라서 위 식 오른쪽 항은 아래와 같이 계산됩니다.




따라서




역전파에서 가중치 업데이트를 위해 사용되는 오차의 가중치에 대한 미분값이 결국 역전파의 출발 노드의 활성 함수 값과 도착 노드의 활성 함수 값, 그리고 실제값만으로 표현되는 것을 알 수 있습니다.

​위 식의 오른쪽 항에서 처음 두 식의 값을 아래와 같이 δ1(3)으로 둡니다.


[식3]




역전파에 의해 업데이트 되는 w1,1(2)는 다음과 같습니다.




마찬가지 방법으로 w1,2(2)에 대한 업데이트 수식도 다음과 같습니다.




δ2(3)을 아래와 같은 수식으로 정의하고,


[식4]


w2,1(2), w2,2(2)에 대해서도 계산을 해보면




여기까지 서술해보니 뭔가 규칙성이 보입니다. 출력층에서 은닉층으로 역전파를 통해 전달되는 값이 결국 δ1(3) 과 δ2(3) 뿐이더라도 관련된 가중치를 업데이트하는데 충분하다는 것을 알 수 있습니다.




이제, 은닉층에서 입력층으로 향하는 역전파를 통해 w1,1(1)의 값을 업데이트 해보겠습니다.




w1,1(1)을 업데이트 하기 위해서 a1(2)에서 Jtotal을 w1,1(1)로 편미분한 값을 계산해야겠지요? 앞에서 적용한 것과 마찬가지로 미분의 연쇄법칙을 활용하면 다음과 같은 식이 됩니다.




위에서 언급했듯이 a1(2)에서 Jtotal은 J1과 J2를 더한 값이 됩니다. 따라서 위 식 오른쪽 항의 첫 번째 편미분값은 아래와 같이 계산됩니다. 




 그런데 출력층 -> 은닉층으로 역전파를 통한 가중치 계산시에 등장한 [식3]과 [식4]와 [식1], [식2]를 참고하여 계산해보면 다음과 같은 결과가 나옵니다.




가 됩니다.



나머지 편미분 값은 이미 해본 계산이므로 쉽게 계산됩니다.  w1,1(1)을 업데이트 하기 위한 편미분 값은 다음과 같습니다.



위 식의 오른쪽 항에서 처음 두 식을 δ1(2)로 둡니다. 




최종적으로 w1,1(1)의 업데이트 식은 아래와 같게 됩니다.



동일하게 w1,2(1)의 업데이트 식은 아래와 같습니다.



마찬가지로, δ2(2)를 다음과 같이 두고, 




w2,1(1), w2,2(2)에 대한 업데이트 식을 나타내면 다음과 같습니다.




따라서 모든 가중치에 대한 업데이트 식은 아래와 같이 표현할 수 있습니다.




여태까지 다룬 역전파 알고리즘 개념을 그림으로 도식화하여 나타내보면 다음과 같습니다.




이와 같이 순전파 -> 역전파 -> 가중치 업데이트 -> 순전파 .... 로 계속 반복해나가면 오차값이 0에 가까와지게 됩니다.  


이상으로 역전파 알고리즘 및 가중치 업데이트 원리를 살펴보았습니다. 역전파 알고리즘을 이해하기가 쬐금은 어렵지만, 이 부분이 딥러닝의 핵심적인 역할을 하는 부분이며, 완전한 이해가 힘들더라도 그 개념만이라도 알고 있으면 많은 도움이 됩니다.

반응형
반응형

[33편]까지 머신러닝의 기초적인 내용에 대해 거의 모두 다루었으므로, 이번 포스팅부터는 요즘 핫하게 뜨고 있는 딥러닝과 관련된 내용을 차근차근 다루어 보도록 하겠습니다.


먼저, [2편]에서 다루었던 인공신경망인 단층 퍼셉트론을 다시 상기해봅니다. 너무 오래된 일이라 기억이 나지 않으면 아래 링크를 눌러 살포시 다시 보고 옵니다.


☞ [2편] 퍼셉트론 다시 보러 가기



퍼셉트론의 활성 함수를 개선하여 퍼셉트론을 발전시킨 인공신경망이 아달라인이라고 했습니다. 아달라인은 [6편]에서 다루었는데, 이 역시 기억이 가물가물하다면 아래 링크를 눌러 다시 복습하고 옵니다.


☞ [6편] 아달라인 다시 보러 가기



이제, 퍼셉트론과 아달라인에 대해 다시 감을 잡았다고 생각하고, 포스팅을 진행하도록 하겠습니다.



[2편]과 [6편]에서 다룬 퍼셉트론과 아달라인은 데이터의 입력층과 출력층만 있는 구조입니다. 이러한 구조를 갖는 퍼셉트론을 단층 퍼셉트론이라 부른다고 했습니다.




퍼셉트론에서 결과값을 내놓는 부분은 결국 활성 함수(activation function)인데, 단층 퍼셉트론에서는 이 활성 함수가 1개밖에 없는 구조이지요. 위 그림에서 출력층의 활성 함수를 a로 표시했습니다.


인공신경망인 단층 퍼셉트론은 그 한계가 있는데, 비선형적으로 분리되는 데이터에 대해서는 제대로 된 학습이 불가능하다는 것입니다. 예를 들면 단층 퍼셉트론으로 AND연산에 대해서는 학습이 가능하지만, XOR에 대해서는 학습이 불가능하다는 것이 증명되었습니다.


이를 극복하기 위한 방안으로 입력층과 출력층 사이에 하나 이상의 중간층을 두어 비선형적으로 분리되는 데이터에 대해서도 학습이 가능하도록 다층 퍼셉트론(줄여서 MLP)이 고안되었습니다. 아래 그림은 다층 퍼셉트론의 구조의 한 예를 보인 것입니다.



입력층과 출력층 사이에 존재하는 중간층을 숨어 있는 층이라 해서 은닉층이라 부릅니다. 입력층과 출력층 사이에 여러개의 은닉층이 있는 인공 신경망을 심층 신경망(deep neural network)이라 부르며, 심층 신경망을 학습하기 위해 고안된 특별한 알고리즘들을 딥러닝(deep learning)이라 부릅니다.


따라서 딥러닝을 제대로 이해하기 위해서는 심층 신경망의 가장 기초적인 다층 퍼셉트론에 대해 제대로 알고 있어야 하겠지요~

다층 퍼셉토론에서는 입력층에서 전달되는 값이 은닉층의 모든 노드로 전달되며 은닉층의 모든 노드의 출력값 역시 출력층의 모든 노드로 전달됩니다. 이런 형식으로 값이 전달되는 것을 순전파(feedforward)라 합니다. 입력층과 은닉층에 있는 한개의 노드만 볼 때, 하나의 단층 퍼셉트론으로 생각할 수 있습니다. 따라서 은닉층에 있는 각각의 노드는 퍼셉트론의 활성 함수라고 볼 수 있습니다. 은닉층에 있는 각 노드에 이름을 a1~am으로 이름을 붙여서 그림을 그려보면 아래와 같겠지요.




마찬가지로 은닉층과 츨력층에 있는 한개의 노드만 고려해보면 이 역시 하나의 단층 퍼셉트론이며 출력층의 각 노드에 A1~A3로 이름을 붙여서 그림을 그려보면 아래와 같습니다.




은닉층과 출력층의 노드에 이름 붙여진 a1~am, A1~A3를 소문자 a로 통일되게 표현하기 위해 위첨자와 아래첨자를 도입해 봅니다. 입력층, 은닉층, 출력층은 각각 1번째, 2번째, 3번째 층이므로 a의 위첨자에 층을 표시하도록 해봅니다. 또한 은닉층에도 입력층에서와 같이 바이어스 a0를 추가하여 다층 퍼셉트론을 표현하면 아래 그림과 같이 됩니다.




위 그림에서 제시된 다층 퍼셉트론은 입력층의 노드수가 n개, 은닉층의 노드수가 m개, 출력층의 노드수가 3개입니다. 이렇게 구성되는 다층 퍼셉트론을 n-m-3 다층 퍼셉트론이라 부릅니다.

단층 퍼셉트론과 마찬가지로 다층 퍼셉트론에서도 바이어스 x0, a0의 값은 보통 1로 두면 됩니다.


그러면 다층 퍼셉트론이 동작하는 원리에 대해 살펴봅니다.


우리는 단층 퍼셉트론이 동작하는 원리는 다 알고 있습니다. 단층 퍼셉트론은 활성 함수가 내놓는 결과값이 실제값과 오류가 최소가 되도록 입력층에서 전달되는 값들에 곱해지는 가중치의 값을 결정하는 것입니다.


다층 퍼셉트론의 동작 원리 역시 단층 퍼셉트론의 동작 원리와 크게 다를 것이 없습니다. 차이점은 단층 퍼셉트론은 활성 함수가 1개인 반면, 다층 퍼셉트론은 은닉층과 출력층에 존재하는 활성 함수가 여러개이고, 이에 따른 가중치도 여러개인 것이지요.


다층 퍼셉트론은 아래와 같이 동작합니다.


  1. 각 층에서의 가중치를 임의의 값(보통 0으로 설정함)으로 설정합니다. 각 층에서 바이어스 값은 1로 설정합니다.
  2.  하나의 트레이닝 데이터에 대해서 각 층에서의 순입력 함수값을 계산하고 최종적으로 활성 함수에 의한 출력값을 계산합니다.
  3. 출력층의 활성 함수에 의한 결과값과 실제값이 허용 오차 이내가 되도록 각층에서의 가중치를 업데이트합니다.
  4. 모든 트레이닝 데이터에 대해서 출력층의 활성 함수에 의한 결과값과 실제값이 허용 오차 이내가 되면 학습을 종료합니다.


그런데, 여기서 한가지 어려움이 있습니다. 단층 퍼셉트론에서는 은닉층이 존재하지 않고, 입력층과 출력층만 존재하기 때문에 출력층의 결과값을 우리가 확보한 실제값과 비교하여 오차가 최소가 되도록 가중치를 업데이트하고 결정하면 됩니다. 하지만 다층 퍼셉트론에서는 입력층과 출력층 사이에 은닉층이 존재하고, 은닉층의 출력값에 대한 기준값을 정의할 수 없습니다. 은닉층에서 어떤 값이 출력되어야 맞다 틀리다하는 기준이 없다는 것이지요.


이러한 문제점을 해결하기 위해 다층 퍼셉트론에서는 출력층에서 발생하는 오차값을 이용하여 은닉층으로 역전파(backpropagation)시켜 은닉층에서 발생하는 오차값에 따라 은닉층의 가중치를 업데이트하게 됩니다. 역전파에 대한 내용은 나중에 자세히 다루도록 하겠습니다.


다층 퍼셉트론의 동작 원리를 이해하기 위해 좀 더 구체적으로 들어가 보겠습니다.


l층의 k번째 노드와 l+1층의 j번째 노드를 연결하는 가중치 w를 다음과 같이 정의해봅니다.


 

 


자 다시, 위에서 제시한 n-m-3 다층 퍼셉트론에서 은닉층에 있는 j번째 노드를 생각해봅니다.



은닉층의 j번째 노드에 입력되는 순입력 함수값은 아래와 같습니다.



순입력 함수값은 해당 노드의 활성 함수에 입력되는 값입니다. 우리가 여태까지 배웠던 활성 함수를 되새겨 보면, 단순 퍼셉트론에서는 활성 함수로 계단 함수(step function)를, 아달라인에서는 1차 항등 함수를, 로지스틱 회귀에서는 시그모이드 함수를 적용했습니다.

다층 퍼셉트론의 활성 함수로 시그모이드 함수를 적용하게 되면 분석이 복잡해지긴 하지만 시그모이드 함수의 결과가 미분가능한 꼴이어서 역전파 알고리즘을 적용할 수 있게 되고, 나아가 이미지 분류 등과 같은 복잡한 비선형 문제를 풀 수 있는 잠재력을 지니게 됩니다.


여기서 잠깐~! 퍼셉트론의 활성 함수는 시그모이드가 아닌데.. 시그모이드를 활성 함수로 적용하게 되면 이는 로지스틱 회귀를 겹겹히 쌓아 놓은 구조가 되므로, 엄밀히 말하면 다층 퍼셉트론이라는 말은 좀 어폐가 있어 보이네요~ 아무튼 그냥 다층 퍼셉트론이라 부르기로 합니다.


시그모이드 함수에 대해서는 [12편]에서 다루었습니다. 아래 링크를 눌러서 살포시 학습하고 오시죠~


☞ 로지스틱 회귀 다시 보기



시그모이드 함수를 적용한 활성 함수를 φ라 하면 




따라서 은닉층의 j번째 노드의 활성 함수는 다음과 같이 표현할 수 있습니다.




이와 같이 각 층에서 순입력 함수값을 계산하고, 시그모이드 활성 함수에 의해 출력값을 계산하여, 최종적으로 출력층에서 활성 함수에 의한 결과값이 나오면 실제값과 오차를 계산한 후 가중치를 업데이트하는 행위를 반복하여 학습을 수행합니다.


학습의 결과가 허용된 오차 이내가 되면 각 층의 신경망에 곱해지는 가중치가 결정되는 것이고, 최종적으로 학습을 종료하게 됩니다. 이런 과정이 결국 딥러닝의 핵심 개념이라 보면 됩니다.


다층 퍼셉트론은 순전파(feedforward) 인공 신경망의 전형적인 예입니다. 순전파라는 말은 루프를 돌지 않으면서 각 층의 출력값이 그 다음층의 입력값이 된다는 의미입니다. 나중에 다룰 또 다른 인공 신경망인 RNN(Recurrent Neural Network)은 순전파와는 대비되는 개념이 적용됩니다.


단층 퍼셉트론과는 달리 다층 퍼셉트론의 출력층의 출력 노드는 여러개가 될 수 있습니다. 만약 3개 품종을 지닌 아이리스를 분류하는 다층 퍼셉트론은 출력층의 출력노드를 3개로 구성하고 그 결과값에 따라 품종 분류는 아래와 그림과 같은 형식으로 하면 될 것입니다.



출력층을 3개의 노드로 구성하고, 아이리스의 3개 품종 setosa, versicolor, verginica에 대한 실제값을 (1, 0, 0), (0, 1, 0), (0, 0, 1)로 정의합니다. 출력층의 1번째 노드 a1(3), 2번째 노드 a2(3), 3번째 노드 a3(3)의 값이 (1, 0, 0) 스러우면 아이리스 품종을 setosa로, (0, 1, 0) 스러우면 versicolor로 분류하는 식입니다.

활성 함수가 시그모이드 함수이므로 실제 출력층에서의 출력값은 0~1사이의 값을 갖게 됩니다. 따라서 출력값은 (0.9, 0.01, 0.19)과 같은 꼴로 될 것이며, 이는 (1, 0, 0) 스러우므로 setosa로 분류하면 됩니다. 

만약 0~9까지 숫자를 인식하는 다층 퍼셉트론이라면 출력층의 노드 개수를 10개로 하면 될 것입니다. 


이정도로 다층 퍼셉트론에 대한 소개는 마무리 하도록 하겠습니다.



다음 포스팅에서는 심층 인공신경망 학습의 핵심인 역전파(backpropagation)에 대한 내용을 살펴보도록 하겠습니다. 


반응형
반응형

이번 포스팅에서는 밀도기반 클러스터링인 DBSCAN(Density-based Spatial Clustering of Applications with Noise)에 대해 살펴보고 클러스터링에 대한 내용을 마무리하도록 하겠습니다.



DBSCAN의 개념적 원리는 단순합니다. 이해를 위해 2차원 평면에 아래 그림과 같이 9개의 데이터가 분포되어 있다고 가정합니다.




여기에 반지름이 ε인 원이 있다고 하고, 1번 데이터부터 9번 데이터까지 원의 중심에 이 데이터들을 둔다고 생각해봅니다. 


 


1번 데이터를 반지름 ε인 원의 중심에 둔 그림입니다. 원안에 점이 1번을 제외하고는 없네요. 자 여기서 우리가 하나의 클러스터로 묶을 기준을 정의합니다. 이 원안에 적어도 4개 이상의 점들이 포함되는 녀석들만 골라서 하나의 클러스터로 분류하는 것으로 해보겠습니다. 위 그림처럼 1번 데이터를 원에 중심에 두었을 경우, 1번 데이터를 제외하고는 아무런 데이터가 없습니다. 이런 데이터들을 노이즈 데이터(noise point)라 부르며, 노이즈 데이터들은 클러스터에서 제외시킵니다.



위 그림에서 보는 바와 같이 3번 데이터를 원의 중심으로 두었을 때 4개의 점이 원안에 포함됩니다. 따라서 우리가 세운 기준에 충족하는 데이터가 됩니다. 이러한 데이터를 코어 데이터(core point)라 부릅니다. 코어 데이터들은 하나의 클러스터에 포함시킵니다.

그런데, 아래 그림에서 보는 것처럼 2번과 9번 데이터는 우리의 기준인 4개의 데이터를 포함하는 것에는 충족하지 못하지만 클러스터에 포함되는 코어 데이터인 3번 데이터를 포함하고 있습니다. 이런 데이터를 경계 데이터(border point)라 부르며 이 녀석들도 해당 클러스터에 포함시킵니다. 




이런 식으로 반지름 ε인 원을 이용해 코어 데이터, 경계 데이터, 노이즈 데이터들을 분류해보면 아래 그림과 같은 결과가 나옵니다.




위 그림은 최종적으로 데이터들을 분류한 것이며, 빨간색과 노란색 데이터들을 하나의 클러스터로 묶고, 회색 데이터는 클러스터에서 제외합니다.


DBSCAN의 원리는 이것이 다입니다.


자, 그러면 코드로 들어가 볼까요..


skl_dbscandata.py


이 코드는 필요한 모듈을 임포트한 후, 반달 모양으로 분포하는 샘플 데이터를 생성하고 화면에 출력합니다. 코드를 실행하면 아래와 같은 데이터 분포를 볼 수 있습니다.




샘플 데이터는 위쪽 반달 부분에 100개, 아래쪽 반들 부분에 100개의 데이터가 분포하고 있습니다.


이제 아래 코드와 같이 plotResult()라는 함수를 정의하고, k-means 클러스터링으로 샘플 데이터를 분류합니다. plotResult()는 다양한 클러스터링 기법으로 분류한 결과를 시각적으로 보이기 위한 것입니다.



skl_dbscandata.py에서 plt.scatter() 이하 2줄을 삭제하고 위 코드를 추가한 후 실행하면 아래와 같은 결과가 나옵니다.




k-means 클러스터링으로 분류한 결과는 어떤가요? 뭔가 제대로 분류된 것 같지가 않습니다. 아무래도 위쪽 반달과 아래쪽 반달로 구분해야 더 적절한것 같은데요.. 이제 32편에서 다루었던 응집형 계층적 클러스터링으로 한번 분류해보죠.


기존 코드에 아래의 코드를 추가합니다.




계층적 클러스터링의 결과도 k-means와 비슷하지만 그래도 좀 품질이 좋아진 것 같네요. 이제 이번 포스팅의 주제인 DBSCAN을 이용해 분류해봅니다. 기존 코드에 아래의 코드를 추가합니다.




코드에서 DBSCAN의 인자 eps가 위에서 언급했던 원의 반지름이며, min_samples는 원에 포함되는 데이터 개수의 최소값입니다. 추가한 코드를 실행하면 아래와 같은 결과가 나옵니다.




드디어 우리가 원하던 결과가 나온 것 같습니다.


우리가 여태 다루었던 k-means 클러스터링, 계층적 클러스터링, DBSCAN은 데이터 간 유사도를 위해 유클리드 거리를 적용하여 계산했습니다. 유클리드 거리를 이용하는 기법에는 '차원의 저주(curse of dimensionality)'라는 효과가 발생한다는 단점이 존재합니다.


차원의 저주는 머신러닝, 데이터 마이닝 분야에서 사용하는 용어인데 데이터의 차원이 증가하면 해당 공간의 부피가 기하급수적으로 증가하게 되고, 모델을 추정하기 위해 필요한 샘플 데이터의 개수도 기하급수적으로 증가합니다. 또한 공간 상에 분포하는 데이터의 밀도 역시 매우 희박하게 됩니다. 이런 현상을 차원의 저주라는 표현을 써서 어려움을 나타내는 것이지요.


아무튼 DBSCAN에서 차원의 저주를 극복하기 위해 적절한 ε의 값과 최소 샘플 데이터 개수를 정하는 것이 중요한 포인트입니다.



이로써 머신러닝 기초에 대한 개념과 원리, 내용에 대해서는 거의 다 살펴본 것 같습니다. 앞으로의 포스팅에서는 요즘 핫하게 뜨고 있는 딥러닝을 이해하기 위한 인공신경망에 대해 본격적으로 공부해 보기로 합니다. 


반응형
반응형

이번 포스팅에서는 클러스터링을 위한 또 다른 기법인 계층적 클러스터링(Hierarchical Clustering) 또는 계층적 군집화에 대해 살펴보겠습니다.


계층적 클러스터링은 특정 알고리즘에 의해 데이터들을 연결하여 계층적으로 클러스터를 구성해 나가는 방법입니다. k-means 클러스터링 방법과는 달리 계층적 클러스터링은 최초에 클러스터의 개수를 가정할 필요가 없다는 장점이 있습니다. 계층적 클러스터링은 아래와 같이 응집형(agglomerative) 계층적 클러스터링과 분리형(divisive) 계층적 클러스터링 2가지가 있습니다. 



응집형 계층적 클러스터링은 주어진 데이터에서 개별 데이터 하나하나를 독립된 하나의 클러스터로 가정하고 이들을 특정 알고리즘에 의해 병합하여 상위단계 클러스터를 구성하고, 이렇게 구성된 상위단계 클러스터를 특정 알고리즘에 의해 또 다시 병합하여 최종적으로 데이터 전체를 멤버로 하는 하나의 클러스터로 구성하는 방법입니다.


분리형 계층적 클러스터링은 응집형과는 완전히 반대로 진행되는데, 데이터 전체를 멤버로 하는 하나의 클러스터에서 시작하여 개별 데이터로 분리해나가는 식으로 클러스터를 구성하는 방법입니다.


이 포스팅에서는 응집형 계층적 클러스터링에 대해서만 살펴보도록 합니다.


계층적 클러스터링에서 상위 단계의 클러스터를 구성하기 위한 대표적인 특정 알고리즘은 단순연결(single linkage)과 완전연결(complete linkage)이라 불리는 기법이 있습니다.


단순연결 기법은 2개의 클러스터에서 각 클러스터에 속하는 멤버 사이의 거리가 가장 가까운 거리를 모두 계산하고, 이 값들중 가장 작은 값을 가지는 2개의 클러스터를 병합하여 상위 단계 클러스터를 구성하는 방법이며, 완전연결 기법은 2개의 클러스터에서 각 클러스터에 속하는 멤버 사이의 거리가 가장 먼 거리를 모두 계산하고, 이 값들중 가장 작은 값을 가지는 2개의 클러스터를 병합하여 상위 단계 클러스터를 구성하는 방법입니다.


글로 이해하는 것은 역시 어려운 법입니다. 예시를 들어 이해하는 것이 가장 쉽고 빠른 방법이죠.


아래 그림과 같이 좌표상에 0~4 번호가 붙어 있는 5개의 점이 있습니다. 이 5개의 점을 응집형 계층적 클러스터링으로 클러스터를 구성해보겠습니다. 클러스터를 구성하기 위한 알고리즘은 완전연결 기법을 이용하겠습니다. (완전연결 기법만 이해해도 단순연결은 쉽게 이해할 수 있습니다.)​


 



이들 점 사이의 거리를 계산하면 아래와 같습니다. 




응집형 계층적 클러스터링에서 첫단계는 개별 데이터가 하나의 클러스터로 가정하고 시작하므로 점들간의 거리가 가장 가까운 2개의 점을 하나의 클러스터로 구성하면 될 것입니다. 점2와 점4의 거리가 가장 가까우므로 이 두점을 하나의 클러스터(아래 그림에서 초록색 타원)로 구성합니다.  



이제 남은 클러스터는 점0, 1, 3과 점2, 점4로 이루어져 있는 초록색 타원으로 표시된 클러스터이겠지요.


이제 각 점과 초록색 타원 클러스터내의 멤버와의 거리중 멀리 있는 거리를 계산합니다. 점0은 초록색 클러스터의 2개의 멤버와의 거리중 멀리 떨어져 있는 점4와의 거리 4.097740을 택합니다. 마찬가지로 점1과 초록색 클러스터의 멤버와 먼 거리는 6.358690이 될 것이고, 점3과 초록색 클러스터 멤버와 먼 거리는 3.846035가 됩니다.


이 값들 중 가장 작은 값은 점3과의 거리인 3.846035가 됩니다. 이 값과 점0과 점1 사이의 거리, 점1과 점3사이의 거리, 점0과 점3사이의 거리중 가장 작은 값을 가지는 거리를 선택합니다. 제일 작은 값은 초록색 클러스터와 점3과의 거리인 3.864035가 됩니다. 따라서 초록색 클러스터와 점3을 병합하여 상위단계 클러스터(아래 그림에서 큰 초록색 타원)를 구성합니다.




동일한 방법으로 점2, 3, 4를 포함하는 큰 초록색 타원으로 표시된 클러스터 멤버와 점0, 점1과 먼 거리와, 점0과 점1과의 거리 중 가장 가까운 거리를 선택하면 점0과 점1 사이의 거리가 됩니다. 따라서 그 다음 단계로 구성하는 클러스터는 점0과 점1을 묶은 클러스터(아래 그림에서 파란색 타원)가 됩니다.



마지막으로 남은 2개의 클러스터를 묶어 하나의 클러스터로 구성하고 종료합니다.  이 예는 결국 데이터를 초록색과 파란색 타원으로 표현된 2개의 클러스터를 최종 결과로 볼 수 있을 겁니다.


이제 응집형 계층적 클러스터링 방법에 대해 약간 이해가 가시나요? 비교하는 거리가 가까운 거리라는 것 빼고는 단순연결 기법을 이용하는 것도 완전연결과 동일한 절차로 수행하면 됩니다.


아래 코드를 봅니다.


skl_makerandompts.py



이 코드를 수행하면 응집형 계층적 클러스터링의 이해를 위해 도입했던 점0~4까지가 좌표상에 나타날 겁니다.


skl_makerandompts.py 맨 아래에 다음 코드를 추가합니다.


skl_caldist.py



이 코드는 scipy.spatial.distance 모듈이 제공하는 pdist와 squareform을 이용하여 skl_makerandompts.py가 생성한 점0~점4의 거리 행렬을 구하고 화면에 표시해줍니다.


점 5개의 대한 거리행렬은 5x5 정사각 행렬로 만들어집니다. pandas를 이용해 보기좋게 화면에 출력하면 다음과 같습니다.




각 점 사이의 거리가 계산되어 그 값을 요소로 하는 정사각행렬로 구성되었다는 것을 알 수 있습니다. 거리 행렬은 대각선 요소를 기준으로 대칭인 행렬이 됩니다.


참고로 pdist는 인자로 입력된 numpy 배열에 있는 값을 이용해 각 요소들의 거리를 계산하고 거리 행렬의 대각 요소의 윗 부분의 값들을 1차원 배열로 구성하여 리턴합니다.


skl_caldist.py 맨 아래에 다음 코드를 추가합니다.


skl_complete_linkage.py


이 코드는 scipy.cluster.hierarchy 모듈이 제공하는 linkage를 이용해 완전연결 기법을 적용한 응집형 계층적 클러스터링을 수행하고  그 결과를 화면에 표시합니다.


코드에서 주석처리한 부분은 주석 처리하지 않은 바로 위코드와 동일한 기능을 수행합니다.


이 코드를 수행하면 아래와 같은 결과가 나옵니다.




이 표에서 클러스터1~클러스터4는 완전연결을 이용해 응집형 계층적 클러스터링으로 차례로 구성되는 클러스터를 의미하며, 클러스터ID_1과 클러스터ID_2는 병합되는 클러스터 번호를 나타냅니다. 표에서 거리는 클러스터ID_1과 클러스터ID_2의 가장 먼 거리를 나타내며, 클러스터 멤버수는 병합된 클러스터에 속해 있는 멤버 개수를 의미합니다.


최초 5개의 점(응집형에서는 이 점들 최초 단계에서는 하나의 클러스터가 됨)이 있다는 것을 고려하여 표를 해석해보면, 1단계에서 클러스터ID 2와 4를 병합하여 6번째(클러스터ID는 5가 됨) 클러스터를 구성하고, 2단계에서 클러스터 ID 3과 5를 병합하여 7번째 클러스터를 구성합니다. 3단계에서 클러스터ID 0과 1을 병합하여 8번째 클러스터를 구성하고, 마지막 단계에서 클러스터 ID 6과 7을 병합하여 9번째 클러스터를 구성하고 종료합니다.


linkage()의 리턴값 row_clusters를 이용하여 클러스터의 계층 구조를 덴드로그램(dendrogram, 계통도)으로 나타낼 수 있습니다. 아래의 코드를 skl_complete_linkage.py 맨 아래에 추가합니다.


skl_dendrogram.py


코드를 실행하면 아래와 같은 응집형 계층적 클러스터링의 결과를 덴드로그램으로 보여줍니다.




덴드로그램에서 세로축은 완전연결 기법으로 계산한 클러스터 사이의 유클리드 거리를, 가로축은 최초 단계에서 클러스터(점)를 나타냅니다. 이 덴드로그램을 보면 점2와 점4는 하나의 클러스터로 봐도 무방할 정도로 유클리드 거리가 작지만, 점0과 점1은 하나의 클러스터로 보기에는 상대적으로 부적절하다는 것을 알 수 있습니다.


이러한 덴드로그램이 실제로 활용될 때 아래와 같이 상관도를 색상으로 표현해주는 히트맵(heat map)과 조합하여 표시해주면 더욱 직관적으로 파악할 수 있습니다.




위 그림처럼 화면에 나타내려면 약간 노동집약적인 트릭이 필요합니다. skl_dendrogram.py를 아래의 코드로 변경하면 됩니다. 코드에 대한 설명은 생략합니다.




scikit-learn은 응집형 계층적 클러스터링을 위해 AgglomerativeClustering이라는 클래스를 제공하며, 계층적 클러스터링에 의해 최종적으로 분류된 클러스터에 대한 정보만 보여줍니다.


아래의 코드를 skl_makerandompts.py 맨 아래 부분에 추가합니다.



코드를 수행하면 아래와 같은 결과가 나옵니다.


클러스터 분류 결과: [0 0 1 1 1]


[0 0 1 1 1]의 의미는 점0, 점1은 클러스터0, 점2, 점3, 점4는 클러스터1로 분류된다는 것을 나타냅니다. 따라서 최종적으로 분류한 클러스터의 개수는 2개가 되며, 이는 scipy를 이용했을 때와 클러스터링 결과가 동일하다는 것을 알 수 있습니다 


반응형
반응형

데이터 분포를 그래프로 표현하면 데이터가 몇개의 그룹으로 분류될 수 있는지 눈으로 확인할 수 있습니다. 그런데, 컴퓨터의 입장에서는 몇 개의 그룹으로 데이터를 분류해야 최적의 결과가 되는지 알기가 어렵습니다. 데이터가 산만하게 분포되어 있는 경우, 사람의 눈으로도 2개로 나누어야 할지, 3개로 나누어야 할지 모호할 때가 있습니다. 


이번 포스팅에서는 k-means 클러스터링으로 데이터를 분류하고자 할 때, 최적의 클러스터 개수를 결정하는 방법에 대해 살펴봅니다.


최적의 클러스터 개수를 결정하는데 사용되는 대표적인 방법은 다음과 같은 2가지가 있습니다.


  • 엘보우(elbow) 기법
  • 실루엣(silhouette) 기법



엘보우 기법

[30편]에서 k-means 클러스터링은 클러스터내 오차제곱합(SSE)의 값이 최소가 되도록 클러스터의 중심을 결정해나가는 방법이라고 했습니다. 만약 클러스터의 개수를 1로 두고 계산한 SSE 값과, 클러스터의 개수를 2로 두고 계산한 SSE 값을 비교했을 때, 클러스터의 개수를 2로 두고 계산한 SSE 값이 더 작다면, 1개의 클러스터보다 2개의 클러스터가 더 적합하다는 것을 짐작할 수 있습니다.


이런 식으로 클러스터의 개수를 늘려나가면서 계산한 SSE를 그래프로 그려봅니다. SSE의 값이 점점 줄어들다가 어느 순간 줄어드는 비율이 급격하게 작아지는 부분이 생기는데, 그래프 모양을 보면 팔꿈치에 해당하는 바로 그 부분이 우리가 구하려는 최적의 클러스터 개수가 됩니다. elbow가 우리말로 팔꿈치라는거 다 아실거고요~


[30편] skl_clusterdata.py 맨 아래에 다음 코드를 추가합니다.




elbow(X)는 클러스터 개수에 따른 데이터 X의 SSE 값을 그래프로 그려주는 함수입니다. 코드에서 km.inertia_가 k-means 클러스터링으로 계산된 SSE 값입니다.


위 코드를 추가한 코드를 실행하면 다음과 같은 그래프가 화면에 출력됩니다.




위 그래프를 보면 클러스터의 개수가 3일 때 팔꿈치 부분이라는 것을 알 수 있습니다. 따라서 주어진 데이터를 구분하기 위한 최적의 클러스터의 개수는 3개로 결정하면 됩니다.




실루엣 기법

실루엣 기법은 클러스터링의 품질을 정량적으로 계산해주는 방법입니다. i번째 데이터 x(i)에 대한 실루엣 계수 s(i) 값은 아래의 식으로 정의됩니다.


 


여기서 a(i)는 클러스터내 데이터 응집도(cohesion)를 나타내는 값으로, 데이터 x(i)와 동일한 클러스터내의 나머지 데이터들과의 평균 거리입니다. 이 거리가 작으면 응집도가 높겠지요.

b(i)는 클러스터간 분리도(separation)를 나타내는 값으로, 데이터 x(i)와 가장 가까운 클러스터내의 모든 데이터들과의 평균거리입니다.


만약 클러스터 개수가 최적화 되어 있다면 b(i)의 값은 크고, a(i)의 값은 작아집니다. 따라서 s(i)의 값은 1에 가까운 숫자가 됩니다. 반대로 클러스터내 데이터 응집도와 클러스터간 분리도의 값이 같으면 실루엣 계수 s(i)는 0이 됩니다. 즉 실루엣 계수가 0이라는 것은 데이터들을 클러스터로 분리하는 것이 무의미하다는 것이겠지요.


아무튼 클러스터의 개수가 최적화되어 있으면 실루엣 계수의 값은 1에 가까운 값이 됩니다.


아래의 코드를 봅니다.


skl_silhouette.py



이 코드에서 plotSilhouette(X, y_km)은 데이터 X와 X를 임의의 클러스터 개수로 계산한 k-means 결과인 y_km을 인자로 받아 각 클러스터에 속하는 개별 데이터의 실루엣 계수값을 수평 막대그래프로 그려주는 함수입니다.



>>> cluster_labels = np.unique(y_km)


y_km의 고유값을 멤버로 하는 numpy 배열을 cluster_labels로 둡니다. y_km의 고유값 개수는 클러스터의 개수와 동일합니다.



>>> n_clusters = cluster_labels.shape[0] 


클러스터 개수를 n_clusters로 둡니다.



>>> silhouette_vals = silhouette_samples(X, y_km, metric='euclidean')


실루엣 계수를 계산하고 그 결과를 silhouette_vals로 둡니다.



plotSilhouette()의 for 구문은 각 클러스터에 속하는 데이터들에 대한 실루엣 값을 수평 막대그래프로 그려주는 로직입니다. 클러스터를 구분하기 위해 matplotlib.cm에서 제공하는 컬러맵 중 JET를 이용해서 칠해줍니다.


참고로 JET 컬러맵은 아래와 같습니다.




왼쪽 파란색 부분은 0.0, 오른쪽 빨간색 부분은 1.0 값을 가집니다. 중간 부분의 색상을 선택하려면 0과 1사이의 실수를 정해주면 됩니다.



>>> silhoutte_avg = np.mean(silhouette_vals)

>>> plt.axvline(silhoutte_avg, color='red', linestyle='--')


모든 데이터들의 실루엣 계수의 평균값을 빨간 점선으로 표시합니다.



skl_silhouette.py는 [30편]에서 다루었던 150개의 샘플 데이터를 3개의 클러스터로 나누어 k-means 클러스터링을 수행하고, 실루엣 계수를 그래프로 그려주는 코드입니다.


코드를 실행하면 아래와 같은 결과가 화면에 나옵니다.




클러스터 1~3에 속하는 데이터들의 실루엣 계수가 0으로 된 값이 아무것도 없으며 실루엣 계수의 평균이 0.7보다 크므로 잘 분류된 결과라 해도 무방합니다.


skl_silhouette.py에서 아래의 코드를 찾아 인자 n_clusters의 값을 2로 수정해봅니다.


>>> km = KMeans(n_clusters=2, random_state=0)



수정한 코드를 실행하면 다음과 같은 결과가 화면에 나타납니다.




클러스터1은 실루엣 계수의 값이 비교적 좋은 편이지만, 클러스터2는 실루엣 계수가 0인 것들도 있고 전체적으로 값이 0.6이하로 품질이 좋지 않음을 알 수 있습니다.


실제 데이터를 분류한 k-means 클러스터링 결과는 아래와 같게 나옵니다.




당연히 클러스터 3개로 구분했을 때보다 품질이 좋지 않다는 것을 알 수 있습니다.


엘보우 기법은 k-means 클러스터링에만 유효하지만, 실루엣 기법은 k-means 클러스터링 이외의 다른 클러스터링에도 적용할 수 있는 기법입니다.


이제 클러스터링에 있어 최적의 결과를 보이는 클러스터의 개수를 구하는 방법에 대해 배웠으니 실무에 잘 응용하시면 되겠습니다. 


반응형
반응형

우리는 여태까지 답이 이미 제시되어 있는 데이터를 이용하여 학습을 수행하고, 알려져 있지 않은 데이터에 대해 예측을 하는 지도학습(supervised learning) 유형의 머신러닝에 관해 살펴보았습니다.


이제 머신러닝의 또 다른 방법인 비지도학습(unsupervised learning)에 대해 다루어보도록 합니다.


다양한 자동차 그림을 제시하고 버스, 트럭, 승용차를 구분하는 경우를 생각해봅니다. 제시한 그림에는 자동차의 그림만 있을 뿐이며, 각 그림에는 자동차 종류를 표시한 어떠한 글자도 답도 태그도 없습니다. 따라서 이런 유형의 분류 문제는 비지도학습의 한 예가 될 것입니다.


아래와 같은 150개의 데이터 분포가 있다고 가정합니다.




위 그림을 보면 데이터가 3개의 그룹으로 나누어져 있다고 생각할 수 있습니다. 데이터에는 좌표만 있을 뿐 각 데이터를 구분할 수 있는 답이나 태그가 없습니다. 앞에서 말한 자동차 그림을 제시하고 버스, 트럭, 승용차로 구분하라고 한 경우, 버스와 트럭, 승용차의 특성들을 고려하여 좌표상에 찍어보면 아마도 위와 비슷하게 3개의 그룹으로 분리되어 표시될 수도 있을 겁니다.


이런 유형의 문제를 해결하기 위해 가장 대중적으로 사용되는 방법이 클러스터링(clustering), 우리말로 군집화라고 하는 알고리즘입니다. 클러스터링은 특성이 비슷한 개체들을 하나의 그룹으로 구분해 주는 기법입니다.


클러스터링은 다음과 같은 4가지 유형으로 구분됩니다.

  • 클러스터 중심(centroid) 또는 평균 기반 클러스터링 k-means
  • 빈도수가 많은 중간점(medoid) 기반 클러스터링 k-medoids
  • 밀도 기반 클러스터링
  • 계층적(hierarchical) 클러스터링


이번 포스팅에서는 k-means 클러스터링(우리말로 k-평균 군집화)에 대해 살펴봅니다.


k-means 클러스터링에서 k는 구분하고자 하는 클러스터(그룹)의 개수를 의미합니다. 위에서 제시된 그림처럼 3개의 클러스터로 분리되어 있는 경우 k는 3이 될 것입니다.


k-means 클러스터링 알고리즘이 동작하는 원리는 의외로 단순하며 주어진 데이터를 구분하기 위한 단계는 아래와 같습니다.


  • 1단계: 
    • 클러스터의 개수 k값을 선택합니다. 위 그림과 같은 경우 k=3으로 두어야 하겠지요.
    • 데이터가 분포된 공간상에 클러스터 중심으로 가정할 임의의 지점을 k개 선택합니다.
  • 2단계: 
    • 임의로 선택한 k개의 클러스터의 중심과 개별 데이터 사이의 거리를 계산합니다.
    • 개별 데이터는 가장 가깝게 있는 클러스터의 중심을 그 데이터가 소속되는 클러스터로 할당합니다.
  • 3단계:
    • 클러스터에 속하게 된 데이터들의 평균값을 새로운 클러스터의 중심으로 둡니다.
  • 4단계
    • 2~3단계를 알고리즘이 수렴할 때까지 반복합니다. 수렴한다는 의미는 클러스터의 중심이 더 이상 변화가 없게 된다는 말입니다.

 

​2단계에서 언급한 거리(distance)는 유클리드 거리의 제곱을 말하며, 특성이 m개인 두 데이터 x, y의 유클리드 거리 제곱은 다음과 같이 정의됩니다.


N개의 데이터가 있고, 이를 K개의 클러스터로 분류한다고 하면, k-means 클러스터링은 아래 J값이 최소가 되도록 w(i, k)와 μ(k)를 결정하는 것입니다.


이 식에서 x(i)는 i번째 데이터, μ(k)는 k번째 클러스터의 중심이며, w(i, k)는 x(i)가 k번째 클러스터에 속하는 경우 1, 그렇지 않은 경우 0으로 정의되는 값입니다.


J를 클러스터내 오차제곱합 또는 왜곡값(distortion)이라 부르기도 하고 클러스터 관성값(cluster inertia)이라 부르기도 합니다.


이정도로 k-means 클러스터링의 내부적인 처리에 대해서는 마무리하도록 합니다.

k-means 클러스터링의 동작 방식에 대한 이해를 돕기 위해 아래의 그림을 보시죠~





위 그림은 k-means 클러스터링으로 3개의 그룹(k=3)으로 구분하는 메커니즘을 보인 것입니다.


iteration1에서 임의의 3개의 지점(십자가 표시)을 정하고 이를 각 클러스터의 중심으로 가정한 후, 각 데이터는 가장 가깝게 위치하는 중심에 해당하는 클러스터로 편입시킵니다. iteration1 그림에서 초록색으로 표시된 데이터는 맨 왼쪽 십자가가 가장 가까운 중심이며, 빨간색으로 표시된 데이터들은 위쪽 십자가, 파란색으로 표시된 데이터들은 맨 오른쪽 십자가 가장 가까운 중심입니다.

 

iteration2에서는 iteration1에서 분류된 각 색상별 점들의 평균값을 새로운 클러스터의 중심으로 정하고, 개별 데이터들과의 거리를 다시 계산한 후 새로운 클러스터로 구분합니다. 이런 식으로 알고리즘을 반복해서 클러스터의 중심이 변하지 않으면 알고리즘을 종료합니다. iteration5와 iteration6 그림을 보면 클러스터의 중심이 변하지 않았음을 볼 수 있습니다.


k-means 클러스터링 알고리즘은 초기에 선택하는 임의의 클러스터 중심의 위치에 따라 클러스터링의 성능과 품질에 영향을 많이 받습니다. k-means++ 알고리즘은 최초 클러스터의 중심을 임의로 선택하는 것이 아니라 특정 알고리즘에 의해 선택하여 k-means 클러스터링의 성능과 품질을 좋게 해줍니다.


k-means++에 대한 자세한 내용은 생략합니다.


이제 scikit-learn이 제공하는 k-means 클러스터링 API를 이용해서 코드를 구현해보도록 합니다. 아래의 코드를 봅니다.


skl_clusterdata.py


이 코드는 150개의 샘플 데이터를 3개의 클러스터로 구분하여 분포하도록 만들어줍니다. 코드를 실행하면 다음과 같은 결과가 나오게 됩니다.




skl_clusterdata.py 맨 아래 부분에 다음의 코드를 추가합니다.


 


코드에서 init_centroid는 k-means 클러스터링의 초기 클러스터 중심을 선택하는 방법을 지정해주는 변수로 정의했습니다. 이 값이 'random'이면 초기 클러스터의 중심을 임의로 선택하라는 의미이며, 이 값이 'k-means++'이면 초기 클러스터의 중심을 k-means++를 이용해 선택하라는 의미입니다. 따라서 주석을 적절하게 변경하여 'random'이나 'k-means++'로 지정하면 됩니다.



>>> km = KMeans(n_cluster=3, init=init_centroid, random_state=0)


KMeans는 scikit-learn이 제공하는 k-means 클러스터링 API입니다. 각 인자의 의미는 다음과 같습니다.

  • n_cluster: 클러스터의 개수 k 값을 지정합니다.
  • init: 초기 클러스터 중심을 선택하는 방법을 지정합니다. 디폴트는 k-means++



>>> y_km = km.fit_predict(X)


k-means 클러스터링으로 구분한 결과를 y_km으로 얻습니다. y_km은 각 데이터가 속하는 클러스터를 나타내는 인덱스로 구성된 numpy 배열입니다. 우리가 지정한 클러스터는 3개이므로 인덱스는 0, 1, 2중 하나가 될 것입니다. 만약 y_km의 값이 [0 1 2 0 0 1 .... ] 이라고 가정하면 1번째 데이터는 클러스터0, 2번째 데이터는 클러스터1, 3번째 데이터는 클러스터2, 4번째 데이터는 클러스터0, ... 이라는 의미입니다.



코드를 실행하면 아래와 같은 결과가 나옵니다.




3개의 그룹으로 분포된 데이터를 k-means 클러스터링으로 잘 구분해주었음을 알 수 있습니다. 


반응형
반응형

우리는 의사결정트리 학습과 랜덤 포레스트를 이용해 데이터를 학습하고 분류하는 것에 대해 공부한 적이 있습니다. 기억나지 않는다면 아래의 링크를 살포시 눌러서 가볍게 훝어보고 옵니다.

 

☞ 의사결정트리 학습 바로가기

 

☞ 랜덤 포레스트 바로가기

 

 

회귀 분석에 있어서도 의사결정트리를 이용하거나 랜덤 포레스트를 이용해서 회귀 모델을 구할 수 있습니다. 단 차이점은 분류를 위한 의사결정트리 학습과 랜덤 포레스트에서는 정보이득의 불순도 측정을 위해 지니 인덱스나 엔트로피를 사용했다면, 회귀 모델을 위한 정보이득의 불순도 측정은 평균 제곱 오차 MSE로 한다는 것입니다.

 

개념은 이전에 학습했던 것과 동일하므로 바로 코드로 들어가 보죠~

 

skl_DTRegression.py

 

 

>>> rm = DecisionTreeRegressor(max_depth=3)

 

의사결정트리 회귀를 위해 scikit-learn의 DecisionTreeRegressor 객체를 이용합니다. 이 객체의 인자 max_depth의 값을 적절하게 조절하여 언더피팅이나 오버피팅이 되지 않도록 하는 것이 중요합니다.

 

 

>>> sort_idx = X.ravel().argsort()

 

의사결정트리 회귀의 결과를 그래프로 표시하기 위해 X의 요소값이 작은 순서로 정렬한 인덱스를 sort_idx로 둡니다. 말이 어려운데, 예를 들어 [1, 0, 5].argsort()의 값은 [1, 0, 2]가 됩니다.

 

 

결정 계수의 값을 계산하고 결과를 화면에 출력하면 다음과 같습니다.

 

 

 

결정 계수의 값이 0.70으로 제법 훌륭한 결과가 나옵니다.

 

 

이제 랜덤 포레스트를 활용하여 회귀 모델을 구하는 것을 해보도록 합니다. skl_DTRegression.py에서 아래의 모듈을 임포트합니다.

 

>>> from sklearn.ensemble import RandomForestRegressor

 

 

rm = DecisionTreeRegressor(max_depth=3)을 아래의 코드로 변경합니다.

 

>>> rm = RandomForestRegressor(n_estimators=1000, criterion='mse',

                                                  random_state=1, n_jobs=-1)

 

 

수정한 코드를 실행하면 아래와 같은 결과가 나옵니다.

 

 

 

결정 계수의 값은 0.91로 매우 훌륭한 결과가 나옵니다만, 회귀 모델 그래프를 보니 뭔가 오버피팅 되어 있다는 느낌이 강합니다. 샘플 데이터를 트레이닝 데이터와 테스트 데이터로 나누어서 측정해봐야 겠습니다.

 

skl_DTRegression.py에서 rm = DecisionTreeRegressor(max_depth=3) 이하를 모두 지우고, 아래의 코드를 추가합니다.

 

 

 

위 코드를 적용한 skl_DTRegression.py를 실행하면 다음과 같은 결과가 나옵니다.

 

 

 

 

샘플 데이터의 70%를 트레이닝 데이터로 해서 랜덤 포레스트로 계산한 회귀 모델의 결정 계수는 0.921로 매우 높지만, 테스트 데이터를 적용하면 결정 계수의 값이 0.544로 뚝 떨어집니다. 따라서 이 데이터 모델에 대한 랜덤 포레스트 회귀 모델은 오버피팅이 강하게 되어 있다는 것을 알 수 있습니다.

 

데이터 모델에 따라서 랜덤 포레스트의 결과가 오버피팅이 되어 있을 지라도 테스트 데이터에 대한 결정 계수가 높게 나온다면 바람직한 모델을 구한 것이 됩니다. 아래의 코드를 skl_DTRegression.py에 맨 아래에 추가합니다.

 

 

 

이 코드는 '회귀 분석 - 회귀 모델의 적합도 측정' 포스팅에서 잔차 분석을 위해 사용된 MEDV와 나머지 주택 정보들에 대해, 랜덤 포레스트로 회귀 모델을 구하고 트레이닝 데이터 및 테스트 데이터의 결정 계수를 계산하는 코드입니다.

 

코드를 수행하면 아래와 같은 결과가 나옵니다.

 

R2 - Train: 0.980, Test: 0.910

 

 

결과를 보면 트레이닝 데이터의 결정 계수에 비해 테스트 데이터의 결정 계수가 작지만 그 값이 0.910으로 적합도가 매우 훌륭하게 나온다는 것을 알 수 있습니다.

 

이로써 회귀 분석에 대한 내용은 모두 마무리 하도록 하겠습니다.


반응형
반응형

분석하고자 하는 데이터의 설명변수와 응답변수가 선형적인 관계가 아니라 곡선 형태로 되어 있는 경우, 선형 회귀 모델을 이용해 계산하게 되면 오차가 크게 나타날 것입니다.

 

만약 분석하고자 하는 데이터 분포가 2차원 곡선 형태로 되어 있으면 2차원 곡선으로, 3차원 곡선 형태로 되어 있으면 3차원 곡선으로 접근하는 것이 오차가 작겠지요.

 

아래와 같은 데이터 분포를 봅니다.

 

 

 

위 그래프와 같은 데이터 분포는 위로 볼록한 2차원 곡선 형태로 되어 있다고 말할 수 있겠지요. 이런 경우, 우리가 구하고자 하는 회귀 모델 식은 아래의 식과 같이 가정할 수 있습니다.

 

 

 

동일한 개념으로 일반적인 곡선에 대해 회귀 모델을 가정하면 아래와 같이 표현할 수 있겠습니다.

 

 

 

따라서 위 그래프는 d=2 로 두고 회귀 모델을 구하는 것이라고 보면 됩니다.

 

회귀 모델식을 위와 같이 다차원 다항식으로 두고 회귀 분석을 수행하는 것을 다항 회귀(Polynomial Regression)라 합니다. 그런데 이 다항 회귀도 결국 다중 회귀식의 일종임을 알 수 있습니다. 앞서 말했듯이 다중 회귀는 설명 변수가 여러 개 있는 식에서 설명 변수 계수를 구하는 것이라 했습니다. ​아래의 치환식을 생각해봅니다.


이 치환식을 적용하면 위에서 보인 다항 회귀 모델식은 아래와 같은 식으로 표현할 수 있습니다.

 

 

즉, 다항 회귀 모델은 다중 회귀 모델로 계산될 수 있다는 말입니다.

 

코드를 보면서 이해해 보도록 합니다. 먼저 아래와 같이 필요한 모듈을 임포트합니다.

 

 

위 코드 아래에 다음 코드를 추가합니다.

 

skl_polyregression.py

 

이 코드에서 X와 y는 이 포스팅 처음에 보인 그래프에서 데이터의 분포와 같은 값을 가집니다. 다항 회귀를 적용하려면 아래와 같은 순서로 로직을 구현합니다.

 

  1. 다항 회귀를 위한 필요한 차수만큼 항을 추가하기 위해 PolynomialFeatures() 객체를 생성합니다.
  2. 트레이닝 데이터 X를 PolynomialFeatures.fit_transform(X)로 변형합니다.
  3. LinearRegression.fit()에 2단계의 결과를 적용하여 회귀 모델을 구합니다.

 

 

그러면 코드를 보시죠~

 

>>> lr = LinearRegression()

>>> pr = LinearRegression()

 

lr은 단순 선형 회귀 모델로 계산하기 위한 것이고, pr은 다항 회귀를 적용하여 다중 회귀 모델로 계산하기 위한 것입니다. 따라서 pr이 우리가 주목해야 할 부분입니다.

 

 

>>> quadratic = PolynomialFeatures(degree=2)

 

다항 회귀를 위해 2차항을 적용합니다.

 

 

>>> X_quad = quadratic.fit_transform(X)

 

트레이닝 데이터 X를 2차항이 적용된 다항 회귀 모델로 변형합니다.

 

 

>>> pr.fit(X_quad, y)

 

다항 회귀를 위해 변형된 트레이닝 데이터 X_quad를 이용해 회귀 모델 pr을 계산합니다.

 

 

>>> y_quad_fit = pr.predict(quadratic.fit_transform(X_fit))

 

계산된 회귀 모델로 좌표값 X_fit의 예측값을 계산합니다. 계산한 (X_fit, y_quad_fit)을 그래프에 그려주면 회귀 모델 그래프가 그려집니다.

 

 

나머지 코드는 회귀 모델 그래프를 화면에 그려주고, 단순 선형 모델의 MSE와 결정 계수값, 다항 회귀 모델을 적용하여 계산한 회귀 모델의 MSE와 결정 계수값을 보여주는 코드입니다.

 

코드를 실행하면 다음과 같은 결과가 나옵니다.

 

 

 

그래프를 보면, 단순 회귀 모델로 계산된 것은 점선으로 된 직선이고, 다항 회귀를 적용한 회귀 모델은 초록색 곡선입니다. 또한 단순 회귀 모델에 의해 계산된 MSE는 569.78로 다소 크지만 다항 회귀를 적용한 다중 회귀 모델의 MSE는 61.33으로 많이 줄어들었음을 알 수 있습니다. 또한 결정 계수의 값도 0.83에서 0.98로 1에 더욱 가까와져 적합도가 더욱 좋아졌습니다.

 

그러면 여태 다루었던 주택 정보에 다항 회귀를 적용해 보도록 하겠습니다. '회귀 분석 - 준비하기'에서 보인 페어플롯을 다시 보도록 합니다.

 

 

 

페어플롯에서 MEDV-LSTAT에 해당하는 그래프를 보면 데이터가 2차원 곡선 형태 비스므리하게 분포되어 있음을 알 수 있습니다.

 

skl_polyregression.py를 아래의 코드로 수정합니다.

 

skl_polyregression1.py

 

이 코드는 3차 다항 회귀 모델을 적용하는 부분이 추가된 것 말고는 전체적인 로직이 skl_polyregression.py와 동일합니다. 코드를 실행하면 아래와 같은 결과가 나옵니다.

 

 

 

각 회귀 모델의 결정 계수를 보면 결과가 썩 훌륭하지는 못합니다. 모든 비선형 모델에 다항 회귀 모델을 적용하는 것이 팔방미인은 아닙니다. 가끔은 데이터를 다른 식으로 변형해서 단순 회귀 모델로 계산할 때 더 좋은 결과를 보일 때도 있습니다. 이는 데이터를 바라보고 분석하는 사용자의 직관이나 경험이 중요하다는 것을 말해 줍니다.

 

MEDV-LSTAT 데이터에서, MEDV는 로그 값을 취하고, LSTAT은 제곱근 값을 취한 후, 단순 회귀 모델로 계산해보는 코드는 아래와 같습니다.

 

 

skl_polyregression1.py의 #그래프 그리기 부분을 삭제하고  이 코드를 추가합니다. 수정한 코드를 실행하면 다음과 같은 결과가 나옵니다.

 

 

 

결정 계수 값이 0.69로 이전의 결과보다 더 나아졌다는 것을 알 수 있습니다.

 

이는 데이터의 분포 형태에 따라 단순 회귀 모델로 계산할지, 다항 회귀를 적용한 다중 회귀 모델로 계산해야 할지 결정하는 것도 중요하지만, 데이터를 바라보고 직관적으로 해석하는 사용자의 경험이나 능력도 중요하다는 것을 말해줍니다.


반응형
반응형

우리는 로지스틱 회귀를 공부할 때, 머신러닝을 통해 획득한 모델이 오버피팅 또는 언더피팅이 되지 않도록 하기 위해 사용되는 기법인 정규화에 대해 살펴본 적이 있습니다. 물론 기억이 가물가물 하겠지요~ 아래의 링크를 살포시 눌러 기억을 되살리고 옵니다~

 

☞ 로지스틱 회귀 살펴보러 가기

 

 

회귀 분석에서도 정규화를 적용하여 회귀 모델을 계산하게 되면 최적의 모델을 구하는데 도움이 됩니다.

 

정규화를 적용하여 회귀 분석을 수행하는 방법은 아래와 같이 3가지가 있습니다.

 

  • 리지 회귀(Ridge Regression)
    • L2 정규화 적용 모델
  • 라쏘(LASSO; Least Absolute Shrinkage and Selection Operator)
    • L2 정규화 보다 약한 모델
  • Elastic Net
    • L2와 LASSO를 조합한 모델

 

위 3가지 정규화 방법에 따라 회귀 모델을 구하기 위해 사용되는 비용함수 J(w)는 다음과 같습니다.

 

리지 회귀를 위한 비용함수

 

                      

 

 

라쏘를 위한 비용함수

 

                      

 

 

Elastic Net을 위한 비용함수

 

                   

 

 

바로 직전 포스팅의 skl_ra.py에 필요한 모듈을 추가하고 lr = LinearRegression() 부분 다음에 아래 코드와 같이 Ridge(), LASSO(), ElasticNet() 을 추가합니다.

 

skl_ra1.py

 

 

먼저 위 코드와 같이 skl_ra1.py에서 lr = Ridge() 부분만 주석을 해제하고 나머지 호출 부분은 주석 처리해줍니다.

 

 

>>> lr = Ridge(alpha=1.0)

 

Ridge()의 인자 alpha는 위에서 보인 수식의 λ와 비슷한 역할을 합니다. 이 값을 1.0으로 설정합니다.

 

 

>>> lr = ElasticNet(alpha=1.0, l1_ratio=0.5)

 

ElasticNet()인자 l1_ratio는 Elastic Net을 위한 cost function 식에서 λ2 의 비율을 뜻합니다. 이 값을 1.0으로 설정하면 LASSO와 동일한 효과를 갖습니다.

 

 

이 코드에 바로 앞 포스팅에서 적용한 MSE, 결정 계수를 계산해주는 코드를 추가하여 실행하면 다음과 같은 결과가 나옵니다.

 

MSE - 트레이닝 데이터: 20.14, 테스트 데이터: 27.76
R2 - 트레이닝 데이터: 0.76, 테스트 데이터: 0.67


 

 

주석을 순차적으로 변경하여 코드를 수행하여 결과를 정리하면 다음과 같습니다.

 

LinearRegression 적용

MSE - 트레이닝 데이터: 19.96, 테스트 데이터: 27.20
R2 - 트레이닝 데이터: 0.76, 테스트 데이터: 0.67

 

Ridge 적용

MSE - 트레이닝 데이터: 20.14, 테스트 데이터: 27.76
R2 - 트레이닝 데이터: 0.76, 테스트 데이터: 0.67

 

LASSO 적용

MSE - 트레이닝 데이터: 24.72, 테스트 데이터: 32.35
R2 - 트레이닝 데이터: 0.71, 테스트 데이터: 0.61

 

Elastic Net 적용

MSE - 트레이닝 데이터: 24.38, 테스트 데이터: 31.87
R2 - 트레이닝 데이터: 0.71, 테스트 데이터: 0.62

 

 

리지 회귀, 라쏘 기법에서 인자로 입력되는 alpha의 값을 적절하게 조절하여 회귀 모델의 적합도를 향상 시킬 수 있습니다. 

 


반응형
반응형

[23편] '회귀 분석 - 준비하기'에서 회귀 분석에는 단순 회귀 분석과 다중 회귀 분석이 있다고 했습니다. 단순 회귀 분석은 설명 변수가 1개인 경우이며, 다중 회귀 분석은 설명 변수가 2개 이상인 경우라고 말했지요.

 

우리는 [25편]까지 1978년 보스턴 외곽 지역의 14개 범주의 주택 정보 데이터 중 방의 개수(RM)와 주택 가격(MEDV)에 대해서 회귀 모델을 계산했습니다. RM-MEDV 분포는 설명 변수가 1개이므로 단순 회귀 모델로 분석할 수 있으며, 데이터의 분포와 계산된 회귀 모델은 2차원 평면에 표현할 수 있습니다.

 

만약 설명 변수가 2개인 다중 회귀 분석의 경우, 데이터의 분포와 계산된 회귀 모델은 3차원 공간에 표현할 수 있을 것입니다. 이 때 회귀 모델은 2차원 평면이 되겠지요. 그런데, 설명 변수가 3개 이상인 다중 회귀 분석인 경우에는 이를 시각적으로 표현할 방법이 없습니다.

 

아무튼 우리는 이런 사실을 염두해 두고 진행하도록 하지요~

 

 

우리가 계산한 회귀 모델에 대한 적합도를 확인하는 방법은 대표적으로 다음과 같은 두 가지가 있습니다.

 

  • 잔차 분석(Residual Analysis)
  • 결정 계수(Coefficient of determination)

 

계산한 회귀 모델이 어느 정도의 적합도를 가지고 있는지 계산하려면 전체 데이터에서 일부분은 회귀 모델을 구하기 위한 트레이닝 데이터로 사용하고 나머지 데이터를 이용해 회귀 모델을 테스트하는 것이 여러면에서 좋습니다.

 

 

 

잔차 분석

잔차(residual)는 회귀 모델에 의해 예측된 반응 값 y^와 실제 반응 값 y의 차를 말합니다. 결국 이전 포스팅에서 언급한 offset과 동일한 개념입니다.

 

 

 

아래의 코드를 봅니다.

 

skl_ra.py

 

skl_ra.py는 보스턴 외곽 지역 주택정보 14개 범주에서 처음 13개는 설명변수로 하고 주택 가격 MEDV는 반응 값으로 하여 잔차를 분석하는 코드입니다.

 

>>> X = df.iloc[:, :-1].values

 

주택 정보에서 마지막 열(MEDV)을 제외한 열의 값들을 X로 둡니다.

 

 

>>> X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3,

                                                              random_state=0)

 

데이터의 70%는 트레이닝 데이터로, 나머지 30%는 테스트 데이터로 하기 위해 데이터를 분리합니다.

 

 

나머지 코드는 matplotlib을 이용해 가로축은 회귀 모델에 의해 예측된 값, 세로축은 잔차로 하는 좌표 평면에서 트레이닝 데이터의 예측값 vs. 잔차, 테스트 데이터의 예측값 vs. 잔차를 각각 파란색 동그라미, 연두색 사각형으로 표시하고, 잔차가 0이 되는 기준선을 빨간색 선으로 표시합니다.

 

코드를 실행하면 다음과 같은 결과가 나옵니다.

 

 

 

 

위 그림과 같은 그래프를 잔차 그림(Residual Plot)이라 부릅니다. 회귀 모델의 적합도는 예측값에 대한 잔차가 0에 가까울수록 좋습니다. 잔차 그림에서 잔차 0 기준선으로부터 멀리 떨어진 데이터는 이상치로 분류할 수 있을 것입니다.

 

 

 

결정 계수 R2

 

통계학에서 추정한 예측 모델이 주어진 자료에 적합도를 표현하는 척도로 결정 계수라는 것을 사용합니다. 결정 계수는 R 제곱으로 표현하며 0과 1사이의 값을 가집니다. 결정 계수는 다음과 같이 정의됩니다.

 

 

여기서, y(i)와 y^(i)는 각각 i번째 데이터의 반응값과 예측 반응값이며, μy는 반응값의 평균입니다. 이 수식은 뭔가 익숙한 수식으로 구성되어 있습니다. 위 수식에서 분수식의 분자는 회귀 모델을 구하기 위해 사용된 식에서 봤으며, 분모는 반응값 y의 분산입니다.

 

위 식의 분자는 평균 제곱 오차(Mean Square Error: MSE)로 불리는 식입니다.

 

 

MSE만으로도 회귀 모델의 적합도를 정량적으로 나타내는 하나의 척도로 사용할 수 있습니다. MSE의 값이 0에 가까울 수록 적합도가 높은 것입니다.

 

만약 트레이닝 데이터에 대한 MSE와 테스트 데이터에 대한 MSE를 비교해서, 테스트 데이터에 대한 MSE의 값이 다소 크게 나온다면 계산한 예측 모델이 오버피팅 되었다고 볼 수 있습니다. 따라서 트레이닝 데이터의 MSE와 테스트 데이터의 MSE가 엇비슷한 수준인 경우가 바람직합니다.

 

자, 다시 결정 계수는 아래와 같은 식으로 정의할 수 있습니다.

 

 

여기서 Var(y)는 y의 분산입니다. 앞서 말한 바와 같이 결정 계수는 0과 1사이의 값을 가지며 1에 가까울 수록 적합도가 높다는 것을 의미합니다.

 

아래의 코드를  skl_ra.py의 맨 아래에 추가합니다.

이 코드는 위 잔차 그림에 해당하는 데이터들에 대한 MSE와 결정 계수의 값을 계산하여 화면에 출력하는 코드입니다.

코드를 실행한 결과는 아래와 같습니다.

 

MSE - 트레이닝 데이터: 19.96, 테스트 데이터: 27.20

R2 - 트레이닝 데이터: 0.76, 테스트 데이터: 0.67

 

 

MSE의 값을 보면, 테스트 데이터의 MSE가 트레이닝 데이터의 MSE에 비해 크게 나옵니다. 즉 이는 우리가 계산한 회귀 모델이 오버피팅 되어있음을 의미합니다. 결정 계수의 값을 보더라도 트레이닝 데이터의 값에 비해 테스트 데이터에 대한 결정 계수값이 다소 작게 나옵니다.

​이제 skl_ra.py에서 lr = Regression()을 찾아 아래의 코드로 수정하여 RANSAC 알고리즘을 적용한 회귀 모델에 대해 MSE와 결정 계수를 계산해봅니다.

위 코드로 수정한 skl_ra.py를 실행하여 나온 MSE와 결정 계수의 값은 아래와 같습니다.

MSE - 트레이닝 데이터: 23.52, 테스트 데이터: 33.67

R2 - 트레이닝 데이터: 0.72, 테스트 데이터: 0.60

결과를 보면 RANSAC을 적용한 회귀 모델의 적합도가 RANSAC을 적용하지 않은 회귀 모델의 적합도보다 더 나쁘게 나온다는 것을 알 수 있습니다. MSE의 값은 커졌고 오버피팅 결과로 나오며 결정 계수의 값도 더 낮게 나옵니다.

물론 RANSAC의 residual_threshold의 값을 적절하게 조절하면 더 나은 결과를 보일 수도 있습니다.​

아무튼 이번에는 RANSAC을 적용하지 않는 회귀 모델이 조금 더 나은 결과를 보인다는 것을 알 수 있네요.​

회귀 모델에 대한 적합도를 측정하는 방법은 이 정도에서 마무리 하도록 하겠습니다.


반응형
반응형

회귀 모델은 이상치(outlier)라 불리는 주류 또는 정상 분포에서 벗어난 값에 의해 영향을 많이 받습니다. 데이터 과학자들은 이런 이상치들을 그들의 경험과 산업 분야에 대한 지식에 근거해 걸러냅니다.

 

RANSAC(RANdom SAample Consensus)이라 불리는 알고리즘은 정상 분포에 속해 있는 데이터 집합(이를 inlier로 부릅니다)으로 회귀 분석을 수행할 수 있게 해주는 기법입니다.

 

RANSAC 알고리즘은 다음과 같은 절차로 수행됩니다.

 

  1. 데이터에서 임의의 개수를 선택하여 이를 inlier로 가정하고 회귀 모델을 구합니다.
  2. 나머지 데이터들을 회귀 모델과 비교하고 사용자가 지정한 허용 오차내에 있는 데이터들을 inlier로 포함시킵니다.
  3. 재구성된 inlier를 이용해 다시 회귀 모델을 구합니다.
  4. 회귀 모델과 inlier의 오차를 측정합니다.
  5. 이 오차가 사용자가 지정한 값 내에 있거나 지정한 알고리즘 구동 반복회수에 도달했으면 종료합니다. 만약 이 조건을 만족하지 못하면 1단계부터 다시 시작합니다.

 

scikit-learn은 RANSAC 알고리즘을 위한 RANSACRegressor 객체를 제공합니다. 아래의 코드를 봅니다.

 

skl_ransac.py

 

 

[24편]의 skl_lrgd.py와 달라진 부분은 LinearRegression()을 RANSACRegressor() 객체의 첫 번째 인자로 대입한 부분입니다.

 

>>> ransac = RANSACRegressor(LinearRegression(), max_trials=100, min_samples=50,

                            residual_metric=lambda x: np.sum(np.abs(x), axis=1),

                            residual_threshold=5.0, random_state=0)

 

RANSACRegressor의 인자는 다음과 같은 의미를 가집니다.

  • max_trials: 알고리즘의 최대 반복 회수
  • min_samples: inlier 값으로 무작위로 선택할 샘플의 최소 개수
  • residual_metric: 회귀 모델과 데이터의 오차 측정 함수 지정
  • residual_threshold: inlier로 포함시키기 위한 허용 오차

 

residual_metric에 지정된 람다 함수는 샘플 데이터와 회귀 모델과의 수직 거리의 합을 계산하는 것입니다.

residual_threshold의 값으로 5.0을 지정하여 허용 오차가 5.0 이내이면 inlier로 포함시킵니다. 이 값을 지정하지 않으면 scikit-learn은 허용 오차 값으로 대상 변수 y의 중앙값 절대 편차(Median Absolute Deviation)를 디폴트로 지정합니다. RANSAC에서 이 허용 오차의 값을 적절하게 지정하는 것은 중요한데, 적절한 허용 오차를 찾는 것이 또 다른 문제가 되기도 합니다. 이는 데이터를 분석하는 사람의 경험치나 능력으로 해결되는 문제입니다. 아무튼 여기서는 허용 오차의 값을 5.0으로 지정했습니다.

 

나머지 코드는 inlier와 특이값을 산점도로 그리고, 회귀 모델을 matplotlib으로 그려주는 로직입니다. skl_ransac.py를 실행하면 아래와 같은 결과가 나옵니다.

 

 

 

위 그림에서 파란색 원으로 표시된 데이터가 inlier이며 연두색 사각형으로 표시된 데이터가 이상치입니다. skl_ransac.py를 실행하여 도출된 회귀선 기울기와 절편의 값을 이전 포스팅의 skl_lrgd.py를 실행하여 도출된 값과 비교해보면 기울기는 9.102에서 9.621로 변했고, y절편의 값도 -34.671에서 -37.137로 변했다는 것을 알 수 있습니다.

 

이상치를 포함한 모든 데이터에 대해서 계산한 회귀 모델과 이상치를 제거한 후 계산한 회귀 모델 중 어느 것이 더 더 나은 것인지는 테스트를 하기 전까지는 알기 어렵습니다. 다음 포스팅에서는 우리가 계산한 회귀 모델의 적합도에 대해 정량적으로 측정하고 분석하는 방법에 대해 살펴보겠습니다.


반응형
반응형

[23편]에서 다룬 1978년 미국 보스턴 외곽 지역의 주택 정보 중 몇가지 특성에 대한 히트맵을 다시 불러오겠습니다.



위 히트맵에서 방의 개수(RM)와 주택가격(MEDV)의 상관관계를 보면 피어슨 상관계수의 값이 0.7이므로 매우 강한 양의 상관관계를 가지고 있다는 것을 알 수 있습니다. 이는 방의 개수가 많으면 주택가격이 비싸지는 경향이 있다는 것이지요.


이번 포스팅에서는 RM-MEDV의 데이터 분포에 가장 일치하는 회귀직선을 구하는 것을 파이썬으로 구현해봅니다.


데이터 분포와 가장 일치하는 회귀 모델을 구하는 방법 중 하나가 최소제곱법(Ordinary Least Square; OLS)을 이용하는 것인데, 회귀 모델에 의해 예측되는 반응값 y^와 실제 반응값 y와 차의 제곱의 합이 최소가 되는 회귀 모델을 구하는 것입니다. 이는 곧 바로 앞 포스팅에서 언급한 오프셋의 제곱의 합이 최소가 되는 직선을 구하는 것과 같습니다.


[23편]에서 회귀 모델을 아래와 같이 정의했지요.

 


오프셋은 다음과 같이 정의했습니다.



최소제곱법은 모든 데이터에 대해 오프셋을 제곱하고 모두 더한 값이 최소가 되는 것이므로 아래의 값이 최소가 되는 회귀모델을 구하는 것입니다. (데이터의 개수를 n개라 가정합니다.) 



앗,, 이 수식 어디선가 본듯합니다. 바로 아래의 아달라인 비용함수 J(w)와 비슷합니다.




앞에 곱해진 1/2을 무시하면 위의 두 식은 그냥 똑같습니다. 기억이 가물가물하다면, 아래의 링크를 살포시 눌러서 아달라인에 대해 복습을 하고 옵니다.


☞ 아달라인 복습하러 가기

 


아달라인은 비용함수 J(w)의 값이 최소가 되도록 순입력 함수의 가중치를 업데이트 한다고 했습니다.

그런데,  회귀 모델을 구하는 것도 결국 J(w)의 값이 최소가 되는 회귀 모델의 y절편과 설명변수 x의 계수인 w0w1을 구하는 것이므로 아달라인을 구현한 코드를 그대로 가져와서 조금만 수정하면 될 것 같습니다.


아래의 코드를 봅니다.


lrgd0.py



이 코드는 [7편]에 있는 아달라인을 구현한 adaline.py에서 predict(self,X)의 리턴값만 바꾼겁니다. 아래의 링크를 눌러서 adaline.py 코드를 보고옵니다


☞ 아달라인 구현 코드 보러 가기

 


최소제곱법 기반 회귀 모델의 가중치를 구하는 LinearRegressionGD()는 아달라인의 활성화 함수의 결과값을 -1 또는 1로 변환하는 것만 빼고는 동일한 로직임을 알 수 있습니다.


lrgd0.py 코드의 아랫부분에 아래 코드를 추가합니다.


lrgd1.py 



이 코드는 RM-MEDV의 표준화된 값을 이용하여, 비용함수의 가중치를 업데이트 하는 반복회수를 x축에, LinearRegressionGD에 의해 계산되는 비용함수의 값을 y축에 두고 그래프를 그립니다. 실행 결과는 다음과 같습니다.



그래프를 보면 5회 계산에서 값이 수렴함을 알 수 있습니다. 즉 이 부분에서 회귀 모델이 정해지는 겁니다.


아래 코드를 lrgd1.py 코드 아랫부분에 추가합니다.


lrgd2.py



이 코드는 표준화 된 RM값을 x축에, 표준화 된 MEDV값을 y축으로 해서 RM-MEDV의 분포를 나타내고 위에서 구한 회귀 모델을 그립니다. 코드를 실행하면 아래와 같은 결과가 나옵니다.




회귀 모델을 구했으므로 방이 5개인 주택 가격을 예측해 보도록 하겠습니다. 이 그래프는 표준화된 값으로 되어 있습니다. 따라서 실제 집값으로 환산하려면 표준화된 값을 원래 값으로 변환해 주어야겠지요. 아래의 코드를 lrgd2.py 코드에 추가합니다.


lrgd3.py 


num_rooms는 RM의 값을 지정해주는 곳입니다. 방이 5개인 주택가격을 알아보고자 하는 것이므로 실수형으로 5.0을 대입합니다. 우리가 구한 회귀 모델은 표준화 값에 기반하므로, 방의 개수 5.0을 표준화되 값으로 변환한 후 회귀 모델에 의한 예측값을 구해야 합니다. 이 예측값 역시 표준화된 값이며, 표준화된 값을 원래의 값으로 변환시켜주는 것은 StandardScaler.inverse_transform()을 이용하면 됩니다.


코드를 실행하면 아래와 같은 결과가 나옵니다.


방이 [5]개인 주택가격은 약 [10840]달러입니다.




scikit-learn을 이용하여 회귀 모델 구하기

이 포스팅에서 여태까지 다룬 내용은 최소제곱법 기반으로 회귀직선을 구하는 원리와 구현 코드를 작성해보는 것이었습니다. 그런데, 이 내용을 이해했다면 앞으로 scikit-learn이 제공해주는 API를 이용하는 것이 더 낫습니다.  


scikit-learn의 회귀 분석 API를 사용하면 표준화시키고 표준화시킨 값을 본래값으로 변환시키고 하는 번거로운 작업을 해줄 필요가 없습니다. 이런 번거로운 작업들은 scikit-learn 회귀 분석 API 내부적으로 다 처리해주기 때문이죠. 


아래의 코드를 봅니다.


skl_lrgd.py 


이 코드는 scikit-learn의 LinearRegression()을 이용해 RM-MEDV의 회귀 모델 기울기와 y절편을 구하고, RM-MEDV의 분포와 회귀 모델을 그래프로 그립니다. 데이터를 분석할 때 회귀 모델의 기울기나 y절편의 값이 중요한 의미를 가질때 가 있는데, 이럴 경우 위 코드를 참조하여 회귀 모델의 기울기나 y절편을 구하면 됩니다.


코드를 실행하면 다음과 같은 결과가 화면에 나옵니다.




이제 아래의 코드를 skl_lrgd.py 코드 아랫부분에 추가하고 실행하면 앞에서 예측한 방 5개짜리 주택가격과 동일한 결과가 나오는 것을 볼 수 있습니다.



반응형
반응형

우리가 여태까지 다루었던 퍼셉트론, 로지스틱 회귀, SVM, 의사결정 트리, 랜덤 포레스트, kNN과 같은 알고리즘은 트레이닝 데이터를 학습한 후, 주어진 데이터에 대해 어떤 그룹에 속하는지 또는 어떤 범주에 포함되는지 예측하는 분류를 위한 알고리즘입니다.


이번 포스팅에서 다룰 회귀 분석(Regression Analysis)은 분류가 아니라 데이터들 사이의 상관관계라던가, 추이를 예측한다던가, 대상 값 자체를 예측하는 지도학습 알고리즘입니다. 예를 들어, 방의 개수와 집값의 상관 관계라던가, 과거 10년간의 영업 실적을 분석하여 미래의 영업 실적을 예측하는 것 등은 회귀 분석으로 가능합니다.


아래의 식을 살포시 봅니다.



이 식은 우리가 잘 알고 있는 1차 함수이며, 입력 값이 1개인 퍼셉트론의 순입력 함수로 볼 수 있습니다. 회귀 분석에서는 x를 설명 변수(explanatory variable)라 부르고, y^를 반응 변수(response variable) 또는 대상 변수(target variable)라 부릅니다. 그리고 가중치 w0은 y절편,  w1은 설명 변수의 계수가 됩니다.


회귀 분석은 주어진 데이터 (x, y)의 분포를 분석하여 이 분포와 가장 일치하는 직선을 구하는 것이 최종 목표입니다. 결국 회귀 분석의 최종 목표는 데이터 (x, y)의 분포와 가장 일치하는 직선이 되는 w0와 w1 계산하는 것입니다.



위 그림에서 9개의 설명 변수 x와 그에 대응하는 실제 반응 변수 y의 쌍 (x, y)의 분포를 초록색 사각형으로 표시한 것입니다. 회귀 분석은 이 9개의 사각형의 분포에 가장 일치하는 직선을 구하는 것이며, 이 직선을 회귀직선(regression line)이라 하며 일반적인 경우를 포함하면 그냥 회귀 모델이라 합니다. 회귀선과 개별 사각형과의 수직으로 떨어진 거리를 오프셋이라 합니다.

설명 변수의 회귀 직선 방정식에 의한 반응 값을 y^, 설명 변수에 대한 실제 반응값을 y라 하면, 오프셋의 크기는 다음과 같습니다.


이 값은 결국 실제값과 회귀 분석에 의한 예측값의 오차(error)입니다.


여기서 예로 든 것처럼 설명 변수가 1개에 기반한 회귀 분석을 단순 회귀 분석(simple regression analysis)이라 합니다. 만약, 설명 변수가 1개가 아니라 n개이면 회귀 분석에서 구해야 할 식은 우리에게 익숙한(퍼셉트론의 순입력 함수가 바로 이것이죠..) 아래의 식처럼 되겠지요.



여기서 x0 = 1로 두면 됩니다~

이와 같이 설명 변수가 2개 이상에 기반한 회귀 분석을 다중 회귀 분석(multiple regression analysis)라 합니다.




회귀 분석에 대한 개념은 이쯤에서 마무리하고, 이제 우리가 학습할 데이터를 확보해야겠습니다. 본 포스팅에 첨부한 파일은 1978년 미국 보스턴 교외의 주택 정보 데이터입니다. 첨부한 데이터를 다운로드 받습니다.


이 데이터는 1978년 보스턴 교외의 506개의 주택에 대해 14개 범주의 수치를 정리한 것입니다. 14개 범주는 아래와 같습니다.


  • CRIM: 타운별 1인당 범죄율
  • ZN:  25,000 평방 피트가 넘는 다수 거주자를 위한 주거 지역 비율
  • INDUS: 타운당 비소매 사업 면적 비율
  • CHAS: 길이 강과 경계하면 1, 아니면 0
  • NOX: 산화질소 농도( 0.1ppm 단위)
  • RM: 주택당 평균 방수
  • AGE: 1940년 이전에 건축된 주인이 거주하는 주택 비율
  • DIS: 보스턴의 5개 고용 센터까지 가중치 거리
  • RAD: 방사형 고속도로로의 접근성 지수
  • TAX: $10,000 당 최대 자산 가치 비율
  • PTRATIO: 타운별 학생-교사 비율
  • B: 아프라카계 미국인의 비율을 Bk라고 할 때, 1000 x (Bk - 0.63)^2 의 값
  • LSTAT: 인구의 낮은 백분율
  • MEDV: 주인이 거주하는 주택 가격의 중간값($1,000 단위)


주택 정보 데이터를 다운로드 받았으면 적당한 폴더에 저장합니다.

이제 우리가 다운로드 받은 주택 정보 데이터를 pandas의 DataFrame 객체로 읽어 출력해봅니다.



코드를 실행하면 주택 정보의 첫 번째 몇 개가 화면에 출력됩니다.



이로써 데이터를 제대로 확보했다는 것을 확신할 수 있네요~



트레이닝 데이터로 머신러닝을 수행하기 앞서, 탐색적 데이터 분석(Exploratory Data Analysis; EDA)이라는 방법을 이용해 데이터를 파악하는 것이 매우 중요하며, 데이터 과학자들은 이를 권장하고 있습니다.


이를 위해 seaborn이라는 유용한 도구 하나를 설치합니다.


pip install seaborn


seaborn은 matplotlib 기반의 통계와 관련된 차트나 분포도를 그려주는 기능들을 제공해주는 파이썬 확장 라이브러리입니다. 우리가 seaborn으로 먼저 해 볼 것은 페어플롯(pairplot)이라는 기능입니다. 일단, 아래 코드를 보실까요..



seaborn의 sns가 제공하는 pairplot()이 위에서 언급한 페어플롯을 그려주는 함수입니다. 설명을 위해 코드를 실행해보기로 하죠. 코드를 실행하면 아래와 같은 페어플롯 그래프가 등장합니다.




위 그래프를 보면 세로축은 위에서부터 LSTAT, INDUS, NOX, RM, MEDV 이고, 가로축은 왼쪽부터 LSTAT, INDUS, NOX, RM, MEDV 으로 되어 있습니다.


세로축의 RM과 가로축의 MEDV가 만나는 부분에 있는 그래프는, 세로축 값은 RM, 가로축 값은 MEDV로 하는 그래프라는 의미입니다. 따라서 위 그림을 보면 한 눈에 각 범주끼리 상관관계를 쉽게 파악할 수 있겠지요.


예를 들어볼까요..

세로축이 NOX, 가로축이 RM이 대응되는 그래프는 아래와 같습니다.




이 그래프에서 분포를 보면 특징적인 모양이 나타나지 않고, 무작위로 분포되어 있는 것처럼 보입니다. 이는 NOX와 RM의 상관관계가 별로 없다는 것이며, NOX의 값이 어떻게 변하든 간에 RM의 값에 영향을 별로 주지 않는다는 의미입니다.


세로축의 LSTAT과 가로축의 MEDV에 해당하는 그래프를 보면 LSTAT의 값이 커지면 MEDV의 값은 작아지고, LSTAT의 값이 작아지면 MEDV의 값이 커지는 경향을 보입니다. 이를 통해, LSTAT과 MEDV는 관련성이 있는 값이라는 것을 알 수 있는 것이죠.


눈으로 관련성을 개략적으로 파악했으니 이젠 정량적인 숫자로도 나타내 봐야겠습니다. 이를 위해 약간의 통계 및 수학적인 지식이 필요한데, 자세한 것은 스킵하고 핵심적인 것만 가볍게(?) 설명합니다.


데이터들 간의 상관관계를 정량화시키려면 상관행렬(correlation matrix)이라는 것을 만들어야 합니다. 상관행렬은 공분산행렬(covariance matrix)이라는 것과 밀접한 관련이 있습니다.  


공분산이란 두 개 이상의 데이터가 어떤 연관성을 가지며 분포하는지에 대한 것을 정량적으로 나타낸 값입니다. 실제로, 데이터를 표준화하여 계산한 공분산행렬이 곧 상관행렬입니다.


아무튼 이 상관행렬은 피어슨 상관계수(Pearson correlation coefficients)라 불리는 값을 포함하는 정사각 행렬입니다. 피어슨 상관계수는 줄여서 피어슨의 r이라고 부르기도 합니다.


두 개의 특성 데이터 세트 xy의 피어슨 상관계수 r은 다음과 같은 식으로 정의됩니다.



여기서 σxσy는 x의 표준편차, y의 표준편차이며, σxy는 x와 y의 공분산입니다.


피어슨 상관계수 r은 -1과 1사이의 값을 가지며, 0에 가까울수록 상관관계가 없다는 것을 나타내고, -1이나 1에 가까울수록 상관관계가 강하다는 것을 나타냅니다. 피어슨 상관계수 r의 범위에 따른 상관관계는 일반적으로 다음과 같습니다.


-1.0 ≤ r ≤ -0.7   : 매우 강한 음의 상관관계

-0.7 < r ≤ -0.3   : 강한 음의 상관관계

-0.3 < r ≤ -0.1   : 약한 음의 상관관계

-0.1 < r ≤ 0.1    : 상관관계 없음

0.1 < r ≤ 0.3     : 약한 양의 상관관계

0.3 < r ≤ 0.7     : 강한 양의 상관관계

0.7 < r ≤ 1.0     : 매우 강한 양의 상관관계


머리 복잡한 통계 지식은 이정도로 하고 이제 seaborn의 유용한 기능을 활용해서 보스턴 주택 정보들 간의 상관관계를 정량적으로 표시해보겠습니다. 상관관계를 시각화해서 보여주는 방법 중 히트맵(heatmap)이라는 방법이 있습니다. 양의 상관도가 높으면 빨간색 계통으로, 음의 상관도가 높으면 파란색 계통으로, 아무런 상관관계가 없으면 흰색과 가까운 색으로 표현하는 것입니다.


아래의 코드는 위에서 설명한 페어플롯을 히트맵 + 정량적 상관관계로 표현한 버전입니다.



이 코드를 실행하면 아래와 같은 정량적 상관관계 수치와 함께 히트맵이 화면에 출력됩니다.





히트맵을 보면 어떤 데이터들이 상관관계가 있는지 정량적인 값으로 쉽게 살펴볼 수 있습니다.


이제 회귀 분석을 위한 준비는 되었으니, 다음 포스팅에서는 히트맵에 표시된 데이터 쌍에서 상관관계가 특별한 몇 개에 대해 회귀 분석을 위한 코드를 구현해 보도록 하겠습니다. 


반응형
반응형

우리는 [21편]에서 50,000개의 영화 리뷰 데이터를 한꺼번에 메모리에 올려놓고 일괄적으로 머신러닝을 수행한 후, 새로운 리뷰 데이터에 대해 몇 퍼센트의 확률로 긍정적 의견인지 부정적 의견인지 분류해보았습니다.


그런데 이런 방식은 약간 문제가 있습니다. 만약 영화 리뷰 데이터가 5백만개라면 어떨까요? 이 데이터를 메모리에 한꺼번에 올려놓고 머신러닝을 수행하게 된다면 보통의 PC같은 경우 메모리 부족현상이 발생하게 될 겁니다.


따라서 이런 경우에는 영화 리뷰 데이터를 2,000개씩 수행하는 머신러닝을 250번 반복하여 학습하는 것이 메모리 활용면에서는 효율적이겠지요.


기억을 되살려보면 '확률적 경사하강법을 적용한 아달라인 구현하기' 포스팅에서 대용량 데이터에 대해 머신러닝을 수행하는 경우와 웹과 같은 온라인 환경에서 머신러닝을 위해서는 확률적 경사하강법을 적용하는 것이 적합하다고 했습니다.


웹에 머신러닝을 적용하는 경우 크라우드소싱 형태로 머신러닝의 학습 품질을 높혀줄 수도 있습니다. 예를 들어, 어떤 사람이 영화 리뷰 사이트에서 리뷰를 작성한 후 별점을 주어 제출하면 그 데이터를 실시간으로 학습하여 새로운 머신러닝 데이터를 확보할 수도 있고, 구글 번역기를 이용해 영어를 한글로 번역했더니 번역이 제대로 되지 않아 번역된 내용을 수정하여 다시 제출하면 이를 이용해 새롭게 학습하여 보다 나은 번역 품질을 확보하는 것도 가능합니다.




이와 같이 실시간으로 입력되어 들어오는 데이터를 그때 그때 학습하기 위해서는 확률적 경사하강법을 이용하는 것이 가장 적합합니다. 

일단 이미 확보된 50,000개의 리뷰 데이터를 이용해 확률적 경사하강법을 적용한 로지스틱 회귀로 머신러닝을 수행하고, 이후 입력되는 데이터에 대해서는 사용자의 피드백에 따라 선택적으로 학습을 수행하는 방법으로 로직을 구성하면 됩니다.


이렇게 작성한 로직을 웹 프로그래밍을 통해 적용하면 되겠지요. 솔직히 저는 웹 프로그래밍에 익숙하지는 않습니다. 따라서 여기서는 웹 프로그래밍에 대한 부분은 스킵하고, 확률적 경사하강법을 적용한 로지스틱 회귀 부분만 포커스를 맞추도록 하겠습니다.


먼저 이전 포스팅에서 구현했던 skl_sentiment_analysis1.py를 다음과 같이 약간 수정하여 mylib 폴더에 sgd_tokenizer.py로 저장합니다.


sgd_tokenizer.py



이 코드의 sgd_tokenizer()는 이모티콘을 제외한 HTML 태그나 문장부호를 제거하고 모두 소문자로 변환한 후 그 결과를 리턴하는 skl_sentiment_analysis1.py의 tokenizer() 함수에서, 정지단어를 제거하여 리턴하는 로직을 추가한 함수입니다.



확률적 경사하강법을 적용하여 영화 리뷰 데이터를 아래와 같은 절차로 머신러닝을 수행할 것입니다.

  • 확률적 경사하강법 버전의 로지스틱 회귀를 이용함
  • 영화 리뷰 데이터는 1,000개씩 머신러닝을 수행함
  • 머신러닝을 위한 총 리뷰 데이터는 45,000개로 함
  • 머신러닝 결과 테스트는 나머지 리뷰 데이터 5,000개로 함
  • 머신러닝 결과는 파일로 저장함


아래의 코드를 작성한 후 skl_sentiment_analysis5.py로 저장합니다.


skl_sentiment_analysis5.py

 



>>> def stream_docs(path)


이 녀석은 path로 전달되는 정제된 영화 리뷰 데이터 파일을 읽고 실제 리뷰 텍스트와 그 라벨(긍정은 1, 부정은 0)을 리턴하는 제너레이터(generator)입니다. stream_docs(path)를 함수라 하지 않고 제너레이터라고 말한 이유는 stream_docs(path)가 yield 키워드를 이용해 결과값을 반환해주는 녀석이기 때문입니다.


보통 함수는 return 키워드로 결과값을 반환하고 함수를 종료해버립니다. 함수를 종료해버린다는 의미는 이 함수를 다시 호출할 때 함수의 로직을 처음부터 다시 시작한다는 말인거죠. 그런데 yield 키워드로 결과값을 반환하는 제너레이터는 결과값을 반환한 이후에도 그 상태를 그대로 간직하고 있습니다. 이 후 다시 이 제너레이터를 호출하게 되면 마지막으로 yield를 통해 반환한 그 상태 이후부터 다시 시작하는 것이죠. 따라서 보통 제너레이터를 재호출할 때는 next()를 이용합니다.



>>> next(f) 


파일을 오픈한 후 최초로 next(f)한 것은 리뷰 데이터의 첫 번째 라인이 제목(review, sentiment)을 표시하는 부분이기 때문에 그냥 패스하는 것입니다.  



>>> for line in f:

          text, label = line[-3:], int(line[-2])

          yield text, label


리뷰 데이터 파일에서 한 줄씩 읽어 리뷰 텍스트와 라벨로 구분하고, 이를 반환합니다. yield로 반환했기 때문에 다음번 호출에서 그 다음줄 데이터를 처리하게 됩니다.



>>> def get_minibatch(doc_stream, size)


size로 제시되는 크기만큼 리뷰 데이터를 읽어 텍스트 데이터, 라벨을 리스트로 만들어 리턴합니다.



>>> vect = HashingVectorizer(decode_error='ignore', n_features=2**21,

                                               tokenizer=sgd_tokenizer)


이전에 사용했던 CountVectorizer()나 TfidfVectorizer()는 모든 데이터를 일괄적으로 메모리에 올려놓고 구동되는 함수입니다. 따라서 이 함수들은 지속적으로 유입되는 데이터에 대해서 부분부분 적용할 수 없습니다. HashingVectorizer()는 해싱 기법을 이용하며, 데이터 독립적으로 구동되는 함수이므로, 이 함수를 이용하여 특성 벡터를 구성합니다.



>>> X_train, y_train = get_minibatch(doc_stream, size=1000)

    if not X_train:

       break


get_minibatch()로 1000개의 리뷰 데이터를 리스트로 전달받고, X_train의 값이 없으면 for구문을 탈출합니다.



>>> X_train = vect.transform(X_train)


X_train을 HashingVectorizer()를 이용해 특성 벡터로 변환합니다.



>>> clf.partial_fit(X_train, y_train, classes=classes)


확률적 경사하강법 버전의 로지스틱 회귀 SGDClassifier(loss='log', random_state=1, n_iter=1)의 partial_fit()을 이용해 부분 데이터 머신러닝을 수행합니다. partial_fit()의 인자 classes는 가능한 y_train의 값을 numpy 배열로 지정해줍니다.



skl_sentiment_analysis5.py의 마지막 부분에 아래의 코드를 추가하고 skl_sentiment_analysis6.py로 저장합니다.




이 코드는 나머지 5,000개의 리뷰 데이터를 가지고 테스트해보고, 정확도를 표시한 후, 머신러닝 결과를 SGDClassifier.pkl에 저장하는 로직입니다.


skl_sentiment_analysis6.py를 실행하면 아래와 같은 메시지가 화면에 보일 겁니다.


정확도: 0.874

머신러닝 데이터 저장 완료




정확도는 이전의 것과 비교해서 다소 떨어지지만, skl_sentiment_analysis6.py를 이용하면 데이터의 용량과 관계없이 메모리에 대한 걱정을 하지 않고 머신러닝을 수행할 수 있습니다.


위에서 제시된 sgd_tokenizer.py를 수정하여 아래와 같은 코드로 작성하고 mylib 폴더에 vectorizer.py로 저장합니다.


vectorizer.py




이제 저장된 머신러닝 결과를 불러와서 새로운 리뷰 데이터에 대한 예측을 수행해봅니다. 아래의 코드를 작성하고 skl_sentiment_analysis7.py로 저장합니다.


skl_sentiment_analysis7.py



이 코드를 실행하면 다음과 같은 결과가 나옵니다. 참고로 확률적 경사하강법 버전이므로 동일한 코드를 실행했다고 하더라도 결과는 각자 다를 수 있습니다.


예측: 긍정적 의견

확률: 75.780%



이 코드들을 활용해 실제 웹에 적용할 수도 있는데, 다른 부분은 모두 그대로 적용하면 되지만, 예측된 결과를 사용자가 보고 이에 대한 피드백을 제공했을 때, 이를 반영하는 부분만 웹에 적절하게 적용해주면 됩니다.

사용자가 입력한 영화 리뷰 데이터를 user_reivew라고 하고, user_review에 대한 예측 결과의 피드백을 y라고 한다면,


>>> X = vect.transform(user_review)

>>> clf.partial_fit(X, y, classes=classes)

 

와 같은 코드로 업데이트 하고, 이에 대한 머신러닝 결과를 다시 pikcle 객체를 이용해 파일로 저장하면 됩니다.


실제 웹에서는 사용자가 피드백을 줄 때마다 수행하는 것보다는 사용자의 피드백을 DB에 기록해뒀다가 특정 시간에 일괄적으로 반영하는 것이 좋을 것입니다.


반응형
반응형

이번에는 우리가 준비한 영화 리뷰 데이터로 머신러닝을 수행해 보도록 하겠습니다.




영화 리뷰 50,000개는 자연어 처리를 하기 위한 데이터로는 많은 데이터가 결코 아닙니다. 실제 데이터 분석이나 감정 분석을 위해서는 말그대로 빅데이터가 필요합니다. 또한 빅데이터를 확보하였다고 하더라도 체계적이고 효율적으로 데이터를 분석하여 이를 머신러닝에 적용해야 하는데, 본 포스팅에서는 50,000개의 영화 리뷰 데이터 속에 있는 단어와 각 리뷰의 긍정 또는 부정 의견에 대한 관련성을 학습하는 것이 목적이라서, 여기서 수행한 머신러닝 결과로 실무에 적용하기에는 부족하다는 것을 참고하시기 바랍니다.


우리가 확보한 영화 리뷰 데이터에는 문맥 분석을 위해 필요없는 글자들이 있을 수 있는데, 리뷰 데이터를 크롤링하는 과정에서 HTML 태그가 포함되는 경우라던가, 글쓴이에 의해 추가된 문장부호, 정지단어(stopwords)가 포함되는 경우입니다.

정지단어는 대부분의 문서에 등장하는 아주 일반적인 단어이며, 문서를 구별할 수 있는 유용한 정보가 없는 단어를 말합니다. 하지만 정지단어를 무조건 제거한다고 해서 더 좋은 결과가 나오는 것도 아니라는 점도 참고하세요~


아무튼 원본 리뷰 데이터에서 HTML 태그나 문장부호 같은 문자들을 제거하도록 하겠으나, :), :(, ^^, ^*^ 와 같은 이모티콘은 중요한 정보일 수 있으므로 제거하지 않도록 합니다.


아래의 코드를 봅니다.


skl_sentiment_analysis1.py 



우리는 바로 앞 포스팅에서 skl_sentiment_analysis0.py를 실행하여 50,000개 리뷰 데이터를 하나의 csv 파일로 만들었습니다. 이 코드는 앞 포스팅에서 저장한 파일을 읽어서 pandas의 DataFrame 객체로 저장한 후, 특수기호, HTML 태그 등을 제거하여 새로운 csv 파일에 저장하는 로직입니다.

preprocessor(text) 함수는 파이썬 정규 표현식을 이용하여 이모티콘을 제외한 HTML 태그나 특수 기호를 제거한 데이터를 리턴합니다. 정규 표현식에 대한 내용은 여기서 다루지 않겠습니다.



>>> df['review'] = df['review'].apply(preprocessor)


df['review']의 값을 preprocessor() 함수를 적용하여 그 리턴값을 다시 취하는 코드입니다. 따라서 이 라인이 실행되면 preprocessor()가 실행된 결과값이 df['review']로 저장됩니다.


df.to_csv()를 이용해 최종 결과를 refined_movie_review.csv 파일에 저장해둡니다.


아래의 코드로 저장된 파일을 읽어 첫 부분만 화면에 표시해봅니다.



코드 실행 결과는 다음과 같습니다.



이전 포스팅에서 보인 결과와 비교하면 문장 부호가 사라졌고 모두 소문자로 바뀌었음을 알 수 있습니다.


그 다음으로 할 일은 문장에서 각 단어들을 분리하는 것입니다. 문장에서 단어를 분리하는 가장 쉬운 방법은 공백으로 분리하는 것입니다. 공백으로 분리하는 것 말고 단어줄기(word stemming)라 불리는 기법으로 분리하는 방법이 있습니다. 이 방법은 어떤 단어가 있을 때 그 단어의 원형으로 대체하여 분리하는 방법입니다. 예를 들어 running 이라는 현재 진행형 단어가 있을 때, 이 단어의 원형은 run이므로, 이 단어를 run으로 대체하여 분리한다는 말입니다. 단어줄기 기법은 1979년 Martin F. Porter가 개발했기 때문에 Porter stemmer로 알려져 있습니다.


이해를 돕기 위해 아래의 문장을 봅니다.


runners like running and thus they run


이 문장을 단순히 공백으로 분리한 결과와 단어줄기 기법으로 분리한 결과는 다음과 같습니다.


  • 공백으로 분리: ['runners', 'like', 'running', 'and', 'thus', 'they', 'run']
  • 단어줄기로 분리: ['runner', 'like', 'run', 'and', 'thu', 'they', 'run']


파이썬 확장 라이브러리 중 NLTK라는 것이 있습니다. NLTK(Natural Language ToolKit)는 자연어 처리와 관련된 다양한 API들을 제공하고 있는 유용한 라이브러리입니다. 윈도우 명령 프롬프트나 리눅스 쉘에서 아래의 명령으로 NLTK를 설치합니다.


pip install nltk



NLTK가 성공적으로 잘 설치되었으면 아래의 코드를 작성하고, mylib 폴더에 tokenizer.py로 저장합니다.


tokenizer.py 


이 코드에서 stop은 영어의 정지단어 모음이며 tokenizer(), tokernizer_porter() 함수는 각각 공백으로 단어를 분리, 단어줄기로 단어를 분리하여 리스트로 리턴하는 함수입니다.


아래의 코드를 실행해보기 바랍니다.




이제 50,000개의 리뷰 데이터를 가지고 머신러닝을 돌려보겠습니다. 우리는 바로 앞 포스팅에서 모든 문서에 존재하는 단어장을 구성하고 이 단어장에 각 문서에 등장하는 문자의 빈도수를 매핑한 후 이 모든 데이터를 숫자로 매핑하여 특성 벡터를 구성하였습니다. 이를 위해 사용된 것이 scikit-learn의 CountVectorizer()와 TfidfTransformer() 클래스입니다. 내용은 바로 직전 포스팅을 참고하세요~


scikit-learn은 CountVectorizer()와 TfidfTransformer()의 기능을 합쳐 놓은 TfidfVectorizer()라는 클래스를 제공합니다. 이 클래스를 이용해 영화 리뷰 문장을 특성 벡터로 구성하고, 로지스틱 회귀 알고리즘을 이용해 머신러닝을 수행하도록 하겠습니다.


보통 대용량의 데이터를 이용해 머신러닝을 수행하려면 강력한 컴퓨팅 파워와 긴 처리 시간이 필요합니다. 만약 코드를 수행할 때마다 머신러닝 알고리즘을 구동하게 되면 매우 비효율적이겠지요. 따라서 많은 시간을 투자하여 얻은 머신러닝 결과는 파일로 저장하여 한번 수행한 머신러닝을 더 이상 수행할 필요없이, 파일로 저장된 머신러닝 결과를 필요할 때마다 불러서 사용하도록 하면 좋겠지요.


아래의 코드를 작성하고 skl_sentiment_analysis3.py로 저장합니다.


skl_sentiment_analysis3.py 



이 코드는 전처리된 영화 리뷰 데이터 35,000개를 이용해 머신러닝을 수행하고, 머신러닝한 결과는 나머지 15,000개 리뷰 데이터로 검증해보고, 머신러닝 결과는 pickle 모듈의 dump()를 이용해 파일로 저장하는 코드입니다.


실제로, 이 코드를 실행하기 전에, 자연어 처리를 위한 머신러닝 수행에 있어 아래와 같은 파라미터 최적값을 찾아야 합니다.

  • 정지단어를 제거하는 것이 좋은가 제거하지 않는 것이 좋은가
  • 공백으로 단어를 분리하는 것이 좋은가 단어줄기로 단어를 분리하는 것이 좋은가
  • 단어 빈도수를 계산할 때, 순수한 단어 빈도수를 사용하는 것이 좋은가 tf-idf를 사용하는 것이 좋은가
  • 머신러닝 알고리즘의 파리미터는 어떤 값을 취해야 좋은가
    • 로지스틱 회귀의 경우, C값과 L1, L2 정규화에 대한 최적의 파라미터를 찾아내야 함


이를 위해 아래와 같은 다소 복잡한 코드를 꽤 오랜 시간동안 구동하여 최적의 파라미터 값을 찾아야 합니다.



다소 복잡한 이 코드의 설명은 스킵하겠습니다. 아무튼 이 코드를 실행하여 도출된 최적 파라미터는 다음과 같습니다.


  • 정지단어는 제거하지 않는 것이 좋음
  • 공백으로 단어를 분리하는 것이 좋음
  • tf-idf를 이용한 단어 빈도수를 사용하는 것이 좋음
  • 로지스틱 회귀는 C=10.0, L2 정규화를 사용하는 것이 좋음


참고로 이 코드의 실행 시간은 매우 깁니다. 제 아마존 서버 기준으로 1시간 이상 걸리네요..


위에 제시한 skl_sentiment_analysis.py는 이 코드에서 나온 결과를 적용한 코드이므로, 여러분들은 바로 위 코드를 수행하지 않아도 됩니다. 그러면 머신러닝 수행 코드의 주요 부분만 살펴봅니다.



>>> tfidf = TfidfVectorizer(lowercase=False, tokenizer=tokenizer)


TfidfVectorizer()에 사용할 단어 구분을 위한 함수로 tokenizer()를 적용합니다. tokenizer()는 단어를 공백으로 분리하여 리스트로 리턴하는 함수입니다.



>>> lr_tfidf = Pipeline([('vect', tfidf),

             ('clf', LogisticRegression(C=10.0, penalty='l2', random_state=0))])


scikit-learn의 Pipeline()은 인자로 입력된 리스트에 정의된 함수를 순차적으로 적용합니다. Pipeline()의 인자인 리스트는 그 요소로 ('함수별명', 함수)와 같은 튜플로 되어 있어야 합니다. 따라서 lr_tfidf는 별명이 'vect'로 정의된 tfidf와 별명이 'clf'로 정의된 LogisticRegression()을 순차적으로 실행시킵니다. 



>>> lr_tfidf.fit(X_train, y_train)


X_train, y_train을 이용해 머신러닝을 수행합니다. lr_tfidf는 TfidfVectorizer()와 LogisticRegression()을 순차적으로 실행하므로, X_train, y_train을 tf-idf 벡터로 전환하고 이를 LogisticRegression()을 이용해 머신러닝을 수행합니다.



>>> pickle.dump(lr_tfidf, open(os.path.join(dest, 'classifier.pkl'), 'wb'),

                                                                protocol=4)


우리가 수행한 머신러닝 결과는 lr_tfidf 입니다. 이것을 현재 디렉토리 아래에 있는 data/pklObject/classifier.pkl 파일에 저장합니다. 저장된 머신러닝 결과는 아래의 코드로 언제든지 읽어올 수 있습니다.



다시 본래 코드로 돌아가서, 코드를 수행하면 잠시 후 아래와 같은 결과가 화면에 나옵니다.


머신러닝 시작
머신러닝 종료
테스트 종료: 소요시간 [12]초
테스트 정확도: 0.950
머신러닝 데이터 저장 완료



제 아마존 서버에서 실행 시간은 약 12초정도 걸렸고, 15,000개 테스트 리뷰 데이터의 정확도는 0.950으로 나옵니다. 그런데, 50,000개의 자료를 가지고 미지의 문장에 대해 긍정적인 문장인지 부정적인 문장인지를 구분하는 것은 쉽지가 않은 일입니다. 실제로는 문맥과 문맥속에 포함된 함축된 의미까지 파악해야 제대로 된 결과가 나오겠지만, 여기서는 단어 하나하나에 포커스를 맞추어 머신러닝을 수행하였기 때문에, 특정 단어와 그 문서의 부정/긍정을 연관시키는 것이므로 그 결과의 품질에 대해서는 너무 기대하지 않는 것이 좋습니다. 따라서 앞에서 언급했던 바와 같이 텍스트 문서의 처리 방법과 그 개념만 이해하는 것이 좋을것 같습니다.


이제 아래의 코드를 봅니다.


skl_sentiment_analysis4.py 



코드를 실행하고 '영문으로 리뷰를 작성하세요: '가 등장하면 영어로 대충 리뷰를 작성하고 엔터를 누르면 입력한 문장에 대한 긍정 또는 부정적 의견일 확률을 표시합니다. 그런데 생각보다 정확하지는 않습니다. 프로그램을 종료하려면 리뷰 입력란에 그냥 엔터를 치면 됩니다.


테스트 정확도: 0.950

영문으로 리뷰를 작성하세요: i love this movie
예측: 긍정적 의견
확률: 92.753%

영문으로 리뷰를 작성하세요: i like this movie
예측: 부정적 의견
확률: 72.450%

영문으로 리뷰를 작성하세요: i like it
예측: 긍정적 의견
확률: 85.629%

영문으로 리뷰를 작성하세요: hated movie
예측: 부정적 의견
확률: 99.738%



결과를 보면 'i like this movie'는 72.450%의 확률로 부정적 의견으로 예측했고, 'i like it'은 85.629%의 확률로 긍정적 의견이라 내놓네요..-.- 


반응형
반응형

우리는 인터넷이나 소셜 미디어를 통해 다양한 의견을 글로써 표현하고 있습니다. 예를 들어 영화를 한편 보고 난 후 그 영화와 관련된 사이트에 가서 영화가 재미있었다 재미없었다라던지, 물건을 구입한 후 그 물건이 좋더라, 좋지않다라고 리뷰를 한다던가, 요즘처럼 정치적인 이슈가 많은 상황에서 관련 기사에 댓글 등으로 자신의 의견을 내놓습니다.




자연어 처리(Natural Language Processing; NLP) 기술 중 하나인 감정 분석(sentiment analysis) 기술은 머신러닝 알고리즘을 이용하여 문맥의 편향성(polarity)을 바탕으로 어떤 사안에 대해 긍정적인지 부정적인지, 또는 희노애락과 같은 감정 등을 분류하는 기술입니다.


이번 포스팅에서는 스탠포드 대학에서 제공하는 영화 리뷰 댓글 50,000개 데이터로 머신러닝을 수행하고 이를 통해 감정 분석을 수행하는 방법에 대해 살펴보겠습니다. 다룰 내용은 다음과 같습니다.


  • 비정형 텍스트 데이터 준비하기
  • 텍스트로부터 특성 벡터 구성하기
  • 영화 리뷰에 대해 '긍정' 또는 '부정'으로 분류할 수 있도록 머신러닝을 수행
  • out-of-core 학습 기법을 활용하여 대용량 테스트 데이터로 작업하기



먼저 아래의 링크를 클릭하여 영화 리뷰 댓글 50,000개가 수록되어 있는 데이터를 다운로드 받습니다.


☞ 영화 리뷰 데이터 다운로드 받기



이  파일은 어떤 영화에 대한 50,000개의 별점 리뷰에 대해 별점이 6개 이상이면 '긍정'으로, 별점이 5개 이하이면 '부정'으로 구분한 데이터로 구성되어 있습니다.


데이터를 다운로드 받은 후 적당한 폴더에 압축을 풉니다.


우리가 방금 다운로드 받은 50,000개의 리뷰는 작지도 크지도 않은 데이터이지만 머신러닝을 위해 데이터를 처리하다 보면 진행률이 어떻게 되는지 궁금할 수 도 있으니 진행률 표시를 위해 아래의 코드를 작성하여 progbar.py로 저장합니다. 이전 포스팅에서 설명했듯이 우리가 편의에 의해 작성한 라이브러리들은 mylib 폴더에 저장하였으므로 이 파일도 mylib 폴더에 저장합니다.


progbar.py



이 데이터를 아래 코드를 이용해 하나의 csv 파일로 만듭니다. 생성되는 csv 파일은 사용자들이 텍스트로 작성한 리뷰글과 해당 리뷰글에 1(긍정) 또는 0(부정)으로 매핑되어 있는 데이터로 구성되어 있습니다.


skl_sentiment_analysis0.py 



코드에서 path의 값은 여러분이 다운로드 받고 압축을 푼 파일 경로를 적으면 됩니다.


코드에 있는 ProgBar는 데이터 처리 진행률을 보여주기 위해 제가 임시로 만든 클래스입니다. 이 녀석이 없으면 데이터 처리가 잘되고 있는지 없는지 조금 답답하겠다 싶어서요.

 

원래 파일이 84MB로 제법 큰 파일이기 때문에 컴퓨터 성능에 따라 처리 시간이 꽤 소요될 수 있습니다. 참고로 제 아마존 서버에서 처리시간은 2분정도 걸리네요.


이 코드가 제대로 실행되면 movie_review.csv라는 파일이 생깁니다. 이 파일의 크기 역시 65MB 정도로 제법 큽니다.


자, 그러면 아래의 코드를 이용해 방금 만든 csv 파일의 앞부분과 뒷부분을 살펴봅니다.


이 코드를 실행하면 다음과 같은 데이터 세트를 볼 수 있습니다.





이제 우리가 머신러닝을 수행해야 할 데이터 50,000개가 이런식으로 준비되었습니다. 0번부터 49999번까지 각각의 문장을 1개의 문서로 생각하면 50,000개의 문서 데이터를 확보한 것입니다.


이 50,000개의 문서는 다양한 단어로 구성되어 있을 겁니다. 지금부터 하고 싶은 것은 50,000개의 문서에 있는 단어가 수록된 일종의 단어장(bag-of-words)을 만들어 보는 것입니다. 우리가 중학교 때 영어 사전을 들고 다니지 않고 중요한 단어를 외우기 위해 단어장으로 공부했던 기억을 떠올리면 됩니다. 아... 지금 중학교 세대는 단어장을 들고 다니는지 잘 모르겠군요~  아무튼 단어장에는 중복되는 단어가 없어야 하겠지요.


아래의 3개의 문장을 봅니다.


문장1 : the car is expensive

문장2 : the truck is cheap

문장3 : the car is expensive and the truck is cheap


문장1~문장3에 등장하는 단어는 모두 7개이며 아래와 같은 알파벳 순서의 단어장을 구성할 수 있습니다.


단어장 [and, car, cheap, expensive, is, the, truck]


자, 이제 단어장에다 문장1에 등장하는 단어의 빈도수를 적어보면 아래와 같습니다.



나머지 문장에 대해서도 똑같이 단어 빈도수를 적어서 정리해보면 다음과 같은 표가 만들어집니다.




여기서 우리가 만든 단어장의 단어대신 단어가 위치하는 인덱스로 모두 대체하게 되면 and -> 0, car -> 1,..., truck -> 6으로 되겠지요. 각 단어를 숫자로 바꾸어 다시 표를 정리해보면 다음과 같이 될 겁니다.




이 표는 문장1~문장3에 등장하는 단어로 단어장을 구성하고, 단어장에 등장하는 단어를 그 순서에 대응되는 숫자로 바꾸어서 구성하였으며, 이렇게 구성된 단어장에다가 문장1~문장3에 등장하는 단어의 빈도수를 적어 정리한 것입니다.


이 표는 결국 문장1~문장3의 특성을 나타내는 표로 볼 수 있고, 일종의 배열 즉 벡터로 구성할 수 있습니다. 따라서 여기까지 수행한 내용이 텍스트로 되어 있는 문장의 특성을 벡터로 구성하는 절차를 보인 것입니다.


아래의 코드를 봅니다.




scikit-learn의 CountVectorizer는 위에서 설명한 텍스트 문장을 벡터로 구성하는 것을 편리하게 해주는 클래스입니다.  


이 코드를 실행하면 아래와 같은 결과가 나옵니다.


{'expensive': 3, 'is': 4, 'truck': 6, 'and': 0, 'cheap': 2, 'car': 1, 'the': 5}

[[0 1 0 1 1 1 0]

 [0 0 1 0 1 1 1]

 [1 1 1 1 2 2 1]]


결과를 보면 위에서 정리한 표와 동일함을 알 수 있습니다.


여기서 한가지 더 고려해야 할 것은, 문장에서 등장하는 단어가 차별화된 정보를 가지고 있는지 또는 분석에 있어서 유용한 단어인지 아닌지를 판단해야하는데, 이를 위해 사용되는 것이 단어 빈도수-역 문서 빈도수(term frequency-inverse document frequency)라 불리는 기법입니다. 이를 보통 tf-idf 기법이라 부릅니다.


영어 단어 'is'와 같은 be 동사같은 경우에는 문서에서 등장하는 빈도수가 많지만 큰 의미를 둘만한 단어는 아닙니다. 따라서 문서에 등장하는 단어들의 연관성을 평가하는 것이 중요한 것이죠. 이와 같이 단어의 연관성을 평가하는 방법 중 하나가 tf-idf 기법입니다.


어떤 단어 t가 문서 d에 나타나는 빈도수를 tf(t, d)로 정의하고, 우리가 분석하고자 하는 총 문서의 개수를 nd로, 단어 t를 가지고 있는 문서의 개수를 df(d, t)로 정의해 봅니다. scikit-learn에서 역 문서 빈도수 idf(t, d)는 다음과 같이 정의하고 있습니다.




scikit-learn에서 tf-idf(t, d)는 다음과 같이 정의합니다.




scikit-learn은 문서에 등장하는 단어의 연관성을 판단할 때 위의 tf-idf(t, d)의 값을 이용하게 됩니다. 위에서 설명한 코드 아래 부분에 다음의 코드를 추가합니다.




scikit-learn은 tf-idf(t, d계산을 위해 TfidfTransformer()라는 클래스를 제공합니다. TfidfTransformer()는 위에서 서술한 tf-idf(t, d 값의 L2 정규화 값을 계산해 줍니다. L2 정규화에 대한 자세한 내용은 패스합니다. 이 코드가 추가된 코드를 실행하면 아래와 같은 결과가 나옵니다.


[[ 0.    0.56  0.    0.56  0.43  0.43  0.  ]
[ 0.    0.    0.56  0.    0.43  0.43  0.56]
[ 0.4   0.31  0.31  0.31  0.48  0.48  0.31]]


결과를 보면 그냥 빈도수만 나열하면 1인 값이 해당 단어의 문서내에서의 빈도수와 그 단어가 존재하는 문서의 개수에 따라 그 값이 달라지는 것을 알 수 있습니다. 예를 들면 문서3의 경우 and와 car에 해당하는 단어가 각각 1번씩 나타나지만 tf-idf의 값은 0.4, 0.31로 수치가 다르게 나오게 됩니다.

  

이정도로 기초 지식과 개념을 이해했으므로 다음 포스팅에서는 우리가 다운로드한 영화 리뷰 데이터를 가지고 놀아봐야겠습니다.


반응형
반응형

kNN은 k-Nearest Neighbors의 약자이며, 지도학습(supervised learning)에 활용되는 가장 단순한 종류의 알고리즘입니다. kNN은 여태까지 다루었던 로지스틱 회귀, SVM, 의사결정트리 학습과는 근본적으로 다른 메커니즘에 의한 학습 방법에 의해 이루어집니다. kNN은 트레이닝 데이터로부터 예측을 위한 함수를 학습하는 것이 아니라 트레이닝 데이터 자체를 기억하는 것이 핵심입니다.


kNN은 OpenCV 강좌에서 다루었기 때문에 그 자세한 내용은 아래 링크를 참조하고 이 포스팅에서는 생략합니다.


☞ kNN 자세히 살펴보기


자, 그러면 바로 코드로 가볼까요..


이전 포스팅에서 작성한 skl_rf.py에 아래의 모듈을 추가합니다.




if __name__ == '__main__' 코드에 다음의 코드에 해당하는 부분을 찾아 수정합니다.



KNeighborsClassifier()의 인자 metric='minkowski'에 등장하는 'minkowski'는 유클리드 거리(Euclidean distance)와 맨하튼 거리(Manhattan distance)를 일반화 한 것입니다. 먼저 유클리드 거리와 맨하튼 거리를 아래의 그림을 통해 이해해 봅니다.


위 그림에서 보는 바와 같이 유클리드 거리는 좌표계에 두 점이 있을 때 두 지점의 최단거리(엄밀하게 말하면 유클리드 좌표계에서 최단거리)이며, 맨하튼 거리는 격자를 이루는 선이 길이라고 생각하고 그 길을 따라 잰 거리를 말합니다. minkowski 거리는 이 두 거리를 하나의 식으로 나타낸 것인데, 다음과 같은 식으로 나타낼 수 있습니다.



여기서 p=1 이면 맨하튼 거리를 나타내며, p=2 이면 유클리드 거리를 나타냅니다.


KNeighborsClassifier(n_neighbors=5, p=2, metric='minkowski') 는 5개의 이웃과의 거리를 기준으로 분류하는 것을 말하고, p=2, metric='minkowski'는 거리 측정을 유클리드 거리로 한다는 의미입니다. 이로써 추가된 코드의 의미는 이해 되었습니다.

이제 마지막으로 plot_decision_region()의 title의 인자를 적절하게 수정하고 skl_supervised.py로 저장합니다.


skl_supervised.py의 if __name__ == '__main__'



이 코드를 실행하면 아래와 같은 결과가 나옵니다.


 

​skl_supervised.py는 scikit-learn에서 제공하는 퍼셉트론, 로지스틱 회귀, SVM, 의사결정트리, 랜덤포레스트, kNN을 모두 포괄하는 코드로 구성되어 있습니다. 머신러닝에서 지도학습 류를 학습할 때 참고할 수 있는 소스코드라고 생각합니다.


이로써 지도학습(supervised learning) 알고리즘에 대한 대부분의 내용을 살펴보았습니다. 


반응형
반응형

이전 포스팅에서 다루었던 의사결정트리 학습법은 훌륭한 머신러닝의 한 방법이지만, 주어진 학습 데이터에 따라 생성되는 의사결정트리가 매우 달라져서 일반화하여 사용하기가 어렵고, 의사결정트리를 이용한 학습 결과 역시 성능과 변동의 폭이 크다는 단점을 가지고 있습니다.


의사결정트리의 이런 단점을 극복하기 위해 랜덤 포레스트가 등장하게 되었습니다.

랜덤 포레스트(Random Forest)는 우리말로 무작위 숲으로 직역할 수 있는데, 이 무작위 숲은 여러 개의 무작위 의사결정트리로 이루어진 숲이라는 개념으로 이해하면 됩니다.


랜덤 포레스트의 학습 원리는 아래와 같습니다.


  1. 주어진 트레이닝 데이터 세트에서 무작위로 중복을 허용해서 n개 선택합니다.
  2. 선택한 n개의 데이터 샘플에서 데이터 특성값(아이리스 데이터의 경우, 꽃잎너비, 꽃잎길이 등이 데이터 특성값임)을 중복 허용없이 d개 선택합니다.
  3. 이를 이용해 의사결정트리를 학습하고 생성합니다.
  4. 1~3단계를 k번 반복합니다.
  5. 1~4단계를 통해 생성된 k개의 의사결정트리를 이용해 예측하고, 예측된 결과의 평균이나 가장 많이 등장한 예측 결과를 선택하여 최종 예측값으로 결정합니다.


1단계에서 무작위로 중복을 허용해서 선택한 n개의 데이터를 선택하는 과정을 부트스트랩(bootstrap)이라 부르며, 부트스트랩으로 추출된 n개의 데이터를 부트스트랩 샘플이라 부릅니다. scikit-learn이 제공하는 랜덤 포레스트 API는 부트스트랩 샘플의 크기 n의 값으로 원래 트레이닝 데이터 전체 개수와 동일한 수를 할당합니다.


2단계에서 d값으로는 보통 주어진 트레이닝 데이터의 전체 특성의 개수의 제곱근으로 주어집니다. 즉, 트레이닝 데이터의 전체 특성의 개수를 m이라고 하면 d의 값은 다음과 같습니다.



5단계에서 여러 개의 의사결정트리로부터 나온 예측 결과들의 평균이나 다수의 예측 결과를 이용하는 방법을 앙상블(ensemble) 기법이라고 합니다. 다수의 예측 결과를 선택하는 것은 다수결의 원칙과 비슷하다 해서 Majority Voting(다수 투표)이라고 부릅니다.


부트스트랩을 이용해 무작위 의사결정트리의 집합인 랜덤 포레스트를 구성하는 것처럼, 부트스트랩으로 다양한 분류기에 대해 앙상블 기법을 활용하여 특징적인 하나의 분류기로 구성하는 것을 배깅(bagging)이라 부릅니다. bagging은 bootstrap aggregating의 약자입니다.


아래 그림은 배깅을 통해 랜덤 포레스트를 구성한 예를 보인 것입니다.




아래 GIF 애니메이션은 앙상블 기법을 이용하여 랜덤 포레스트를 이용해 결과값을 예측하는 개념을 말해줍니다.



랜덤 포레스트에서 우리가 눈여겨 봐야 할 숫자는 4단계에서 의사결정트리를 만드는 횟수인 k입니다. k는 생성되는 의사결정트리의 개수이며, 이 값이 커지면 예측 결과의 품질을 더 좋게 해주지만 컴퓨터의 성능 문제를 일으킬 수 있겠습니다. 따라서 적절한 k값을 주는게 좋겠지요.


자, 그러면 scikit-learn에서 제공하는 랜덤 포레스트 API를 이용해 아이리스 데이터 분류에 적용해봅니다.


이전 포스팅까지 작성한 skl_tree.py에 아래의 모듈을 추가적으로 임포트합니다.



if __name__ == '__main__' 에서 해당되는 부분을 찾아 아래 코드로 수정합니다.




여기서 n_estimators=10 은 위에서 언급한 생성할 의사결정트리의 개수인 k의 값입니다. n_jobs는 학습을 수행하기 위해 CPU 코어 2개를 병렬적으로 활용하는 의미입니다. 만약 코어의 개수가 이 보다 많다면 그에 맞는 코어의 개수를 적용하면 더욱 성능이 향상되겠지요.


마지막으로 plot_decision_region() 함수의 title 인자의 값을 적절하게 수정하고, skl_rf.py로 저장합니다.


skl_rf.py의 if __name__ == '__main__' 


코드를 수행하면 아래와 같은 결과가 나옵니다.





반응형
반응형

바로 앞 포스팅 '의사결정트리 학습'에서 언급했던 불순도를 계산하는 3가지 방법은 다음과 같습니다.


  1. 지니 인덱스
  2. 엔트로피
  3. 분류오류


이 3가지 불순도 계산 방법은 각각 특성을 가지고 있습니다. 아래와 같은 상황을 생각해봅니다.

어떤 데이터 세트에 여러가지 데이터가 섞여 있다고 가정합니다. 이 데이터 세트에 특정 부류(예를 들면 다각형들이 모여있는 집단에서 원으로 분류되는 부류와 같이..)에 속하는 멤버가 차지하는 비율을 가지고 불순도를 계산해 보도록 합니다. 비율은 0~100%의 범위를 가지지만 이를 0~1사이의 값으로 조정하더라도 본질적으로 차이가 없다는 것은 잘 알고 있지요?


이 비율이 0에서 1로 변해갈때 3가지 불순도 계산 방법의 추이는 어떻게 되는지 그래프로 그려보면 아래 그림과 같습니다.


 


이 그래프에서 엔트로피(scaled)로 되어 있는 회색 실선으로 나타낸 곡선은 원래 엔트로피의 0.5배한 그래프입니다. 그래프를 보면 3가지 불순도 계산 방법 모두 0 또는 1에 가까와질 수록 불순도가 낮아지고, 0.5일 때 불순도가 가장 높게 나옵니다. 이는 곧, 데이트 세트에 여러가지 부류에 속하는 데이터가 섞여 있는 경우, 특정 부류에 속하는 멤버입장에서 고려할 때 그 멤버가 차지하는 비율이 0에 가까와지거나 1에 가까와 질 때 가장 순도가 높고, 0.5일 때 불순도가 가장 높다..라고 해석 됩니다. 


분류오류의 경우 멤버가 차지하는 비율이 0.5까지 높아짐에 따라 선형적으로 불순도가 증가하는 것에 반해, 지니 인덱스와 엔트로피는 멤버가 차지하는 비율이 증가함에 따라 불순도가 빠르게 증가하다가 0.5 근방에서 최대를 보이는 패턴을 갖습니다. 특히 엔트로피의 경우가 불순도가 더 급격하게 증가하는 경향을 보입니다.


3가지 불순도의 특성에 대해서는 이 정도에서 마무리 하도록 하겠습니다.


이제 scikit-learn에서 제공하는 의사결정트리 학습 API를 이용해서 아이리스 데이터에 적용해보도록 합니다.


여태까지 우리가 작성한 코드에 아래의 모듈을 추가합니다.




그리고 if __name__ == '__main__'에서  분류기 알고리즘 적용 부분에 아래의 코드로 수정합니다.




이 코드에서 DecisionTreeClassifier()가 scikit-learn이 제공하는 의사결정트리 학습 API이며 criterion='entropy'는 엔트로피를 불순도 계산 방법으로 적용한다는 의미입니다. scikit-learn의 DecisionTreeClassifier()가 지원하는 불순도 계산 방법은 지니 인덱스와 엔트로피입니다. 지니 인덱스를 적용하고자 하면 criterion = 'gini'로 하면 됩니다.


마지막으로 plot_decision_region() 함수의 title 인자로 적절한 값을 적용합니다.


수정된 코드를 skl_tree.py로 저장합니다.


skl_tree.py의 if __name__ == '__main__' 


skl_tree.py를 실행하면 다음과 같은 결과가 나옵니다.






반응형
반응형

우리는 한번쯤 스무고개라는 놀이를 해본 경험이 있을 겁니다. 상대방이 가지고 있는 답을 20번 이내의 질문으로 알아 맞추는 것이죠. 스무고개는 정답을 맞추는 사람이 질문을 잘 던져야 답을 제대로 빨리 찾아낼 수 있습니다.


스무고개와 비슷한 원리로, 의사결정트리 분류기(decison tree classifier)는 일련의 질문에 근거하여 주어진 데이터를 분류해주는 알고리즘입니다. 아래의 그림을 보시죠.



이 그림은 휴일에 무엇을 할지 결정하기 위한 의사결정트리의 한 예입니다. 흰색 둥근 사각형으로 되어 있는 부분이 의사결정을 위한 질문이고, 회색으로 채워진 사각형은 의사결정이 된 부분입니다.


이와 비슷한 원리로 꽃잎길이와 꽃잎너비와 같이 수치로 되어 있는 데이터에 대해서도 의사결정트리를 이용해 분류가 가능하겠지요. 예를 들면 아래 그림과 같이 3-depth 질문으로 분류가 가능한 질문을 만들 수가 있을 것입니다.

 


위 그림을 해설하면 이렇습니다.

첫 번째 질문인 꽃잎너비가 0.75 이하인 녀석들만 분류를 해봤더니 setosa로 모두 분류되었고, 그렇지 않은 녀석들 중에 꽃잎길이가 4.9 이하인 녀석들만 모아보니 versicolor와 verginica의 비율이 10:4로, 꽃잎길이가 4.9 초과하는 녀석들만 모아보니 versicolor와 verginica의 비율이 2:10으로 나오더라 입니다. 마찬가지로 각기 분류된 녀석들 각각에서 꽃잎너비가 1.6이하인지, 꽃잎길이가 5.0 이하인지로 질문을 던져서 각각 분류해보니 최종적으로 맨 아래의 회색 사각형처럼 분류되더라..가 결론입니다.


만약 이 질문의 깊이가 더 깊어지면 보다 더 정확한 값들로 분류가 되겠지만, 이전 포스팅에서 말한 오버피팅이 되버리는 문제가 발생할 수 있으므로 적절한 선에서 질문을 잘라줘야 합니다.


그렇다면 이런 일련의 질문들을 어떻게 만들 수 있을까요...?

의사결정트리 학습은 트레이닝 데이터를 이용해 데이터를 최적으로 분류해주는 질문들을 학습하는 머신러닝입니다.


의사결정트리 학습에 대한 개념은 이것이 다입니다. 이제 의사결정트리에서 질문들을 만들어내기 위한 메커니즘에 대해 가볍게 살펴봅니다. 말은 가볍게라고 썼지만 결코 가볍지 않습니다.

의사결정트리 학습에서 각 노드에서 분기하기 위한 최적의 질문은 정보이득(Information Gain)이라는 값이 최대가 되도록 만들어주는 것이 핵심입니다. 어느 특정 노드에서 m개의 자식 노드로 분기되는 경우 정보이득은 아래의 식으로 정의합니다.





여기서, f는 분기를 시키기 위한 트레이닝 데이터의 특성값(예를 들어, 꽃잎 길이 또는 꽃잎 너비)이며 Dp는 부모노드에 존재하는 데이터 세트, Dj는 j번째 자식노드에 존재하는 데이터 세트입니다. I(Dp)는 Dp의 데이터 불순도(impurity), I(Dj)는 Dj의 데이터 불순도를 의미합니다. 그리고 Np와 Nj는 Dp의 데이터 개수와 Dj의 데이터의 개수입니다.


여기서 데이터 불순도란 데이터가 제대로 분류되지 않고 섞여 있는 정도를 말하는데, 이에 대해서는 아래에서 좀 더 자세히 다루도록 하겠습니다.


아무튼 정보이득 IG는 자식노드의 데이터 불순도가 작으면 작을수록 커지게 됩니다.


계산을 단순화하기 위해 보통 의사결정트리에서 분기되는 자식 노드의 개수는 2개로 하는데, 이를 이진 의사결정트리(Binary decision tree)라고 부릅니다. 특정 노드에서 분기되는 2개의 자식노드를 각각 L과 R첨자를 나타내서 표현하여 위 식을 단순화하면 아래와 같은 식이 됩니다.




아까 위에서 데이터 불순도를 언급했는데, 이진 의사결정트리에서 데이터 불순도를 측정하는 방법은 아래와 같이 3가지가 있습니다.


  1. 지니 인덱스(Gini Index)
  2. 엔트로피(entropy)
  3. 분류오류(classification error)

c개의 부류(class)로 분류되는 트레이닝 데이터가 있는 경우, 노드 t에 존재하는 데이터 중, i부류( i  c)에 속하는 데이터의 비율을 p(i\t) 표현하기로 하면,  위에서 말한 3가지 데이터 불순도 I(t)는 아래와 같은 식에 의해 계산됩니다.


지니 인덱스

엔트로피


분류오류

 
정보이득과 데이터 불순도를 이해하기 위해 아래의 예를 가지고 실제 계산을 해봅니다.




위 그림과 같이 80개의 데이터가 40개씩 두 종류의 데이터로 분류되어 있고, 이를 (40, 40)과 같이 표현해 봅니다. 의사결정트리 A는 (40, 40)을 (30, 10), (10, 30)으로 분리하는 것이고, 의사결정트리 B는 (40, 40)을 (20, 40), (20, 0)으로 분리하는 것입니다.

여기서 데이터 불순도 계산 방법으로 분류오류를 적용하고, 최종적으로 A, B의 정보이득을 각각 계산해보도록 하겠습니다.

Dp의 데이터 불순도를 계산하려면 p(i\t)를 계산해야 하겠죠. p(i\t)는 노드 t에 존재하는 데이터 중, i부류에 속하는 데이터 비율이라고 했습니다. 위의 예에서 2가지 부류만 있으므로 편의상 제1부류, 제2부류라 하겠습니다. (40, 40)은 80개의 데이터 중 40개는 제1부류에, 나머지 40개는 제2부류에 속하는 멤버이므로 p(1\t), p(2\t)는 모두 0.5로 같습니다. 따라서 위에서 서술한 분류오류에 의한 데이터 불순도 역시 제1부류, 제2부류 모두 0.5로 같습니다.


마찬가지 방법으로 DLDR의 데이터 불순도를 계산할 수 있겠지요. 계산식을 적어보면 다음과 같습니다.


의사결정트리 A의 경우 불순도 I(Dp), I(DL), I(DR) 및 정보이득 IG 계산






따라서 정보이득 계산식에 의한 정보이득 값은 다음과 같습니다.




의사결정트리 B의 경우 불순도 I(Dp), I(DL), I(DR) 및 정보이득 IG 계산 


 






따라서 정보이득계산 식에 의한 정보이득 값은 다음과 같습니다.



이제 정보이득을 계산하는 원리는 대충 이해했겠지요.


비슷한 방법으로 데이터 불순도를 계산하는 방법으로 지니 인덱스, 엔트로피를 적용하였을 경우 A, B에서 정보이득은 다음과 같이 계산됩니다.


지니 인덱스로 계산한 경우

  • A에서 정보이득 : 0.125
  • B에서 정보이득 : 약 0.16

엔트로피로 계산한 경우
  • A에서 정보이득 : 0.19
  • B에서 정보이득 : 0.31

의사결정트리에서 정보이득이 최대가 되는 것을 채택해야 하므로, 분류오류를 적용하였을 때는 A, B가 모두 같은 값이 나와서 어떤 것을 선택하더라도 무관했지만, 지니 인덱스로 계산하거나 엔트로피로 계산한 경우에는 B가 A보다 정보이득 값이 크므로 B를 선택하게 될 것입니다.

다음 포스팅에서는 3가지 데이터 불순도의 특성에 대해 가볍게 살펴보고 scikit-learn의 의사결정트리 학습에 의한 아이리스 데이터 분류를 수행하는 코드를 살펴보겠습니다.


반응형
반응형


[15편] scikit-learn SVM을 비선형 분리 모델에 적용하기  머신러닝/딥러닝 / 파이썬(Python)

2017. 3. 31. 0:51

복사http://sams.epaiai.com/220970865707

번역하기 전용뷰어 보기

SVM이 머신러닝 실무자들에게 인기가 높은 이유 중 하나는 선형으로 분류가 되지 않는 모델에 대해서도 적용할 수 있다는 장점 때문입니다.


아래는 표준 정규분포로부터 200개의 샘플을 추출하고, 추출한 샘플을 100개씩 두 개의 그룹으로 나누어, 두 그룹에 속하는 멤버들이 0보다 큰지 아닌지에 대한 논리값을 순서대로 XOR 연산하여 나온 결과 중 True는 1로, False는 -1로 라벨링한 후, 이를 좌표에 표시하는 코드입니다.




코드를 실행하면 아래와 같이 선형 분리가 되지 않는 모양으로 빨간 사각형 그룹의 점과 파란 x 그룹들의 점들이 분포하고 있는 그림을 볼 수 있습니다.



 

위 그림처럼 분포되어 있는 빨간색 그룹과 파란색 그룹을 선형으로 분류하기가 쉽지 않습니다. 엄밀히 말하면 불가능합니다. 다시 말하면, 위 그림과 같은 비선형 모델은 여태 우리가 다루었던 퍼셉트론이나 로지스틱 회귀로는 머신러닝을 수행하기가 어렵습니다. 그렇다면 위 그림처럼 분포하고 있는 두 그룹을 어떻게 하면 선형으로 분류할 수 있을까요? 아래의 그림을 봅니다.




원리는 이렇습니다.


위의 왼쪽 그림은 2개의 값 (X1, X2)로 되어 있는 데이터 샘플의 분포를 2차원 평면에 그려 놓은 것입니다. 데이터의 분포는 원형으로 되어 있어서 선형 분리가 되지 않습니다. (X1, X2) 2차원으로 분포된 데이터를 변환 Φ를 통해 1차원 높은 3차원 (Z1, Z2, Z3) 형태의 분포로 변환하게 되면 오른쪽 위 그림처럼 빨간색 그룹과 파란색 그룹을 선형(엄밀하게 말하면 2차원 초평면으로 분리가 가능함)으로 분리할 수 있게 됩니다. 이렇게 선형으로 분리한 후 다시 2차원 (X1, X2) 분포로 변환시키면 오른쪽 아래 그림처럼 빨간색 그룹과 파란색 그룹을 회색 원 모양의 경계선으로 두 그룹을 분류하게 됩니다.


이런 원리에 입각하여 정리하면 또 다시 복잡한 수식이 등장하게 되는데, 이 부분을 다 스킵하면 결국 커널 함수(kernel function)라고 하는 것이 나옵니다. 커널 함수는 두 분류의 집단에 분포하고 있는 값들에 대한 벡터 내적 계산의 효율화를 위해 도입된 것으로 그 종류가 많은데, 이에 대해서 너무 깊게 알 필요는 없을 것 같습니다.


아무튼 커널 함수 중 가장 광범위하게 사용되는 것이 Radial Basic Function kernel(RBF 커널)입니다. RBF 커널과 관련된 식이 가우스 함수(Gaussian fucntion)와 동일한 형태여서 가우시안 커널(Gaussian kernel)로도 불립니다.

참고로, RBF 커널 k는 다음과 같은 다소 복잡한 수식으로 표현됩니다.


여기서 γ는 최적화를 위한 자유 파라미터(free parameter)라고 부릅니다. 따라서 RBF 커널을 적용하는 SVM은 정규화와 관련된 값 C와 자유 파라미터 γ의 값을 지정해주어야 합니다. 이에 대해서는 이 포스팅의 마지막 부분에서 다루어 보도록 합니다.



자, 그러면 scikit-learn이 제공하는 SVM으로 RBF 커널을 적용하여 위에서 서술한 비선형 분리 모델에 대해 머신러닝을 수행한 결과를 살펴보도록 합니다.


먼저, 'scikit-learn을 이용한 SVM' 포스팅에서 구현했던 skl_svm.py의 if __name__ == '__main__'에서 아이리스 데이터를 읽어 들이는 부분을 주석처리하고 이 아래 부분에 위에서 언급한 표준 정규분포부터 200개의 데이터를 추출하고 좌표에 표시하는 코드를 추가한 후, SVM 객체를 호출하는 부분과 plot_decision_region()을 호출하는 부분을 아래와 같이 수정합니다.



 



if __name__ == '__main__': 코드는 아래와 같습니다.




이 코드를 수행하면 아래와 비슷한 결과가 나옵니다. 참고로 200개 샘플 데이터는 프로그램을 수행할 때마다 달라지므로 본 포스팅에서 제시한 결과와 다를 수 있습니다.





RBF 커널을 적용한 SVM으로 머신러닝을 수행하면 선형으로 분리가 되지 않는 데이터에 대해서도 훌륭하게 머신러닝을 수행하여 제대로 된 학습이 이루어질 수 있음을 알 수 있습니다. 


그러면 아이리스 데이터에 rbf 커널을 가진 SVM을 적용하는데, γ 값을 달리해서 적용해 보겠습니다. 

아이리스 데이터를 읽는 코드는 아래와 같습니다. 

​skl_svm_rbf.py

 





 


 

위 결과를 보면 γ 값이 작으면 트레이닝 데이터를 잘 분류하면서도 테스트 데이터에 대해서도 훌륭한 결과를 보여주지만, γ 값이 커지면 트레이닝 데이터에는 매우 잘 맞아 떨어지지만 테스트 데이터에 대해서는 결과가 썩 만족스럽게 나오지 않습니다. 따라서 이 결과는 오버피팅 된 결과이며 γ 값의 크기는 오버피팅과 관련이 되어 있음을 알 수 있습니다.


따라서 적절한 γ 값을 적용하여 오버피팅이나 언더피팅이 되지 않게 하는 것이 중요합니다.


반응형
반응형

여태까지 다루었던 scikit-learn의 퍼셉트론, 로지스틱 회귀, SVM은 경사하강법 알고리즘을 적용한 것입니다.

만약 머신러닝을 수행할 데이터가 대용량이라면 확률적 경사하강법(Stochastic Gradient Descent) 알고리즘을 적용한 버전이 여러모로 효율적일 수 있습니다.


scikit-learn의 SGDClassifier 클래스는 확률적 경사하강법 알고리즘을 적용한 퍼셉트론, 로지스틱 회귀, SVM을 활용할 수 있도록 해줍니다.


확률적 경사하강법을 적용한 퍼셉트론, 로지스틱 회귀, SVM을 이용하려면 아래와 같은 코드를 적용하면 됩니다.



이전 포스팅의 skl_svm.py에서 아래의 모듈을 추가합니다.




scikit-learn의 Perceptron(), LogisticRegression(), SVC() 대신 SGDClassifier()의 loss 인자값을 달리해서 확률적 경사하강법 알고리즘을 적용한 것으로 대체할 수 있습니다.




수정한 코드를 반영하여 skl_sgd.py로 저장합니다. 


skl_sgd.py 



확률적 경사하강법을 적용한 퍼셉트론, 로지스틱 회귀, SVM의 실행 결과는 다음과 같습니다.


확률적 경사하강법 적용 퍼셉트론 실행 결과





확률적 경사하강법 적용 로지스틱 회귀 실행 결과





확률적 경사하강법 SVM 실행 결과 


 




결과를 보면 퍼셉트론을 제외하고 로지스틱 회귀나 SVM은 영역 구분 자체가 완전히 달라졌음을 알 수 있습니다. 참고로 확률적 경사하강법을 적용한 코드는 실행할 때마다 그 결과값이 다르게 나옵니다. 


반응형
반응형

로지스틱 회귀와 함께 분류를 위한 강력한 머신러닝 알고리즘으로 널리 사용되는 것이 바로 SVM(Support Vector Machine)입니다.


SVM은 퍼셉트론의 개념을 확장하여 적용한 알고리즘인데, 퍼셉트론이 분류 오류를 최소화하는 알고리즘인 반면 SVM은 margin을 최대가 되도록 하는 알고리즘입니다.


여기서 말하는 margin은 분류를 위한 경계선과 이 경계선에 가장 가까운 트레이닝 데이터 사이의 거리를 말합니다. 이 경계선에 가장 가까운 트레이닝 데이터들을 support vector라고 부릅니다. 아래의 그림을 보시죠~

위의 왼쪽 그림을 보면 빨간색 원으로 표시된 집단과 초록색 더하기 기호로 표시된 집단을 분류하는 경계선은 다양하게 존재할 수 있습니다. SVM은 두 집단을 분류하는 경계선 중 support vector와의 거리가 가장 멀리 떨어져 있는 경계선을 찾아내는 알고리즘입니다.

이 알고리즘은 경계선에 가장 가까이 있는 support vector를 지나는 선과 거리가 최대가 되는 경계선을 구함으로써 그 목적을 달성하게 됩니다. 여기서 말한 선은 엄밀히 말하면 다차원 공간의 초평면(hyperplane)인데, 초평면의 개념을 설명하기도 복잡하고, 실제 초평면을 머릿속으로 상상하기도 힘들므로, 그냥 선으로 생각하고 사용해도 큰 무리가 없으므로 초평면을 선으로 생각하도록 하겠습니다.


SVM이 퍼셉트론의 개념으로부터 출발하고 확장된 것이므로 입력되는 트레이닝 데이터, 입력값과 곱해지는 가중치와 관련된 수식들이 등장하게 되는데, 중간단계의 복잡한 수식을 모두 스킵하면 결국 아래 수식의 최소 값을 구하는 것이 SVM의 핵심 원리가 됩니다.



여기서 j는 트레이닝 데이터의 특성값의 개수가 그 범위가 되며, i는 트레이닝 데이터의 개수가 그 범위가 됩니다. C는 로지스틱 회귀에서 설명했던 정규화와 관련된 상수와 비슷한 개념이며 ξ는 여유 변수(slack variables)라고 부르는데, 이 변수는 분류 오류가 생기는 데이터에 적절한 패널티를 부여함으로써 비선형적으로 분리되는 모델을 완화시켜 최적화된 수렴값을 가지도록 하기 위한 선형 제약(linear contraints) 값입니다.


말이 좀 어렵지만 여기서 우리가 눈여겨 볼 부분이 C입니다. C값을 변화시키면 알고리즘이 결정하는 경계선이 달라집니다. 아래의 그림을 보면서 살포시 이해를 해봅니다.




위 그림과 같이 빨간색 원으로 이루어진 집단과 초록색 더하기 기호로 이루어진 집단이 분포하고 있을 때, C 값이 크면 왼쪽 그림처럼, C 값이 작으면 오른쪽 그림처럼 경계선이 결정됩니다. 왼쪽 그림에서 경계선은 오버피팅되었다고 볼 수 있고 오른쪽 그림에서 경계선은 최적화 되었다고 볼 수 있습니다. 


대충 눈치를 챘겠지만 scikit-learn의 SVM을 적용할 때도 위에서 설명한 C값이 등장하게 됩니다.

이전 포스팅에서 구현했던 skl_logistic.py에서..


아래와 같이 SVM을 활용하기 위한 모듈을 추가합니다.




그리고 if __name__ == '__main__': 에서 로지스틱 회귀 알고리즘을 호출하는 부분을 아래의 내용으로 바꿉니다.



마지막으로 plot_decision_regions() 함수 호출부분에서 title 인자를 적절한 값으로 수정합니다.


수정된 코드를 skl_svm.py로 저장합니다.


skl_svm.py 



추가된 코드에서 C 값으로 1.0이 할당되어 있습니다. 이 C값을 조정함으로써 언더피팅과 오버피팅을 조절하게 됩니다.  

이제 코드를 수행해보면 아래와 같은 결과가 나옵니다.






로지스틱 회귀가 구분한 경계선과 비교해보면 경계선과 support vector와의 거리가 동일하게 그려져 있음을 알 수 있습니다. 결과의 품질은 로지스틱 회귀와 동일하게 나오네요. 


반응형
반응형

로지스틱 회귀(logistic regression)는 선형 또는 바이너리 분류 문제를 위한 단순하면서도 보다 강력한 분류 알고리즘입니다. 로지스틱 회귀에서 등장하는 회귀(regression)는 통계에서 말하는 회귀와는 별로 관계가 없는, 머신러닝의 분류와 관련된 알고리즘 이름으로 생각하면 됩니다.


로지스틱 회귀는 선형 분리 모델에서 훌륭하게 동작하며, 실제로 가장 많이 사용되는 분류 알고리즘 중 하나입니다. 머리가 좀 아프더라도 로지스틱 회귀를 살포시 이해하도록 해봅니다.


로지스틱 회귀를 이해하기 위한 첫 단계는 다음과 같은 약간의 확률과 관련된 지식이 필요합니다.

영어로 odds는 어떤 일이 일어날 승산을 말합니다. 특정 사건의 승산률 odds ratio는 다음과 같이 정의됩니다.



여기서 p는 어떤 특정 사건이 발생할 확률입니다. 즉 odds ratio는 어떤 특정 사건이 일어날 확률과 그 사건이 일어나지 않을 확률의 비로 정의됩니다.


다시 우리의 머신러닝 주제로 돌아갑니다. 우리가 머신러닝을 수행해서 얻은 학습 결과를 이용해 어떤 값에 대한 예측값이 실제 결과값과 동일하게 나올 확률을 p라고 하면 이 머신러닝의 odds ratio 역시 위와 같이 정의됩니다.


이제, odds ratio의 로그값을 함수값으로 가지는 함수를 정의해 봅니다.




p는 0과 1사이의 수이고,  f(p)의 범위는 실수 전체가 됩니다. 우리가 이전 포스팅에서 다루었던 순입력 함수의 리턴값을 다시 한번 상기해 봅니다.  순입력 함수의 리턴값을 z로 표기하면 아래와 같습니다.



z는 w의 값에 따라 실수 전체에 대해 매핑되는 값이며, z의 값에 따라 입력된 트레이닝 데이터 X가 어떤 집단에 속하는지 아닌지 결정하게 되는 값입니다. 따라서 위에서 정의한 f(p)의 값을 z로 매핑할 수 있습니다.




여기서, p를 z에 관한 식으로 나타내면 아래와 같은 식이 됩니다. (고등학교때 배웠던 지수와 로그의 관계식을 알면 됩니다!)




이 확률 p를 z에 관한 함수로 표현하여 다음과 같이 나타냅니다.



이 함수를 그래프로 그려보면 다음과 같은 s자 형태의 곡선으로 나타납니다. 이런 이유로 이 함수를 sigmoid 함수라고 부릅니다.  




로지스틱 회귀에서는 순입력 함수의 리턴값에 대해 가중치 업데이트 여부를 결정하는 활성 함수로 이 sigmoid 함수를 이용합니다.




즉 로지스틱 회귀는 순입력 함수의 리턴값을 sigmoid 함수에 대입하여 그 결과값을 가지고 가중치 업데이트를 할지 말지를 결정하는 것이 핵심입니다. sigmoid 함수의 리턴값은 곧 입력한 트레이닝 데이터가 특정 클래스에 속할 확률이 되므로, 로지스틱 회귀는 입력되는 트레이닝 데이터의 특정 클래스에 포함될 예측 확률에 따라 머신러닝을 수행하는 알고리즘임을 알 수 있습니다.


우리는 실생활에서 예측되는 부류(class) 그 자체 뿐만 아니라 그 부류에 속할 확률이 유용할 때가 많습니다. 일기예보에서 내일 비가 올 확률을 예측하는 것이라던지, 환자의 증상을 보고 그 환자가 특정 질병에 걸렸을 확률을 계산하는 것 등이 그 예입니다. 로지스틱 회귀는 실제로 의학 분야에서 광범위하게 활용되고 있기도 합니다.

로지스틱 회귀에서 활용되는 비용함수 J(w)와 가중치 업데이트 식은 퍼셉트론의 그것과 동일한 원리이며, 이에 대한 구체적인 내용은 본 포스팅의 목적에 어울리지 않게 너무 어려워서 패스하도록 합니다.


그러면 아이리스 데이터를 가지고 scikit-learn을 이용하여 로지스틱 회귀 알고리즘으로 머신러닝을 수행하는 코드를 봅니다.


이전 포스팅에서 설명한 skl_perceptron.py 코드에서 아래의 내용을 반영합니다.


필요 모듈 임포트하는 부분에서 아래 코드를 추가합니다.




if __name__ == '__main__': 에서 아래 코드를 찾습니다.


 



이 부분을 아래의 코드로 수정합니다.




마지막으로 plot_decision_regions() 함수 호출부분에서 title 인자를 적절하게 변경합니다.

이렇게 작성된 코드를 skl_logistic.py로 저장합니다.

skl_logistic.py

 

 

결국 scikit-learn의 퍼셉트론 코드와 로지스틱 회귀 코드는 알고리즘을 선택하고 적용하는 부분만 다릅니다.


그런데 LogisticRegression()을 보면 C=1000.0 이라는 생소한 부분이 보입니다. 이 값을 이해하려면 머신러닝에 있어 공통적으로 나타나는 문제인 오버피팅과 이를 해결하기 위한 정규화라는 개념을 알아야 합니다.


오버피팅(Overfitting)이란 말 그대로 너무 잘 맞아 떨어진다는 뜻이며, 이는 머신러닝을 위해 입력된 값에만 너무 잘 맞아 떨어지도록 계산을 해서 입력에 사용된 트레이닝 데이터 이외의 데이터들에 대해서는 잘 맞아 떨어지지 않는 경우가 발생하게 되어 미지의 데이터에 대해 그 결과를 예측하기 위한 머신러닝의 결과로는 바람직하지 않습니다.


아래의 그림을 보면 오버피팅을 이해할 수 있습니다. 물론 이 그림은 'Python Machine Learning'에서 가져온 것입니다.




위 그림에서 2번째 그림처럼 최적의 조건을 찾기 위해 사용되는 것이 정규화(regularization)라는 기법인데, 정규화를 통해 데이터 모델의 복잡성을 튜닝하여 언더피팅과 오버피팅의 트레이드 오프(trade-off)를 찾아내는 것입니다. 로지스틱 회귀에서 사용되는 정규화 기법은 가장 일반적으로 사용되는 L2 정규화인데, 위의 코드에서 생소했던 C=1000.0 부분이 L2 정규화와 관련된 인자로 이해하시면 됩니다.


L2 정규화는 로지스틱 회귀의 비용함수 J(w)를 아래와 같은 식으로 변형하여 동작합니다.



여기서 λ를 정규화 파라미터라고 부르며, C값은 λ의 역수로 정의합니다.



따라서 C값을 감소시키면 λ가 커지게 되며, 이는 정규화를 강하게 한다는 의미입니다. 아무튼 생소했던 C값이 무엇인지 대충은 알았으므로 이쯤에서 넘어 가도록 하겠습니다.


코드를 수행하면 다음과 같은 결과가 나옵니다.





결과를 보면 scikit-learn 퍼셉트론에 비해 로지스틱 회귀가 아이리스 데이터에 대해 더 정확도 높게 분류하고 있고, 머신러닝에 의해 분리된 영역이 퍼셉트론의 결과와 다르다는 것을 알 수 있습니다.


이것으로 로지스틱 회귀에 대한 내용은 마무리하겠습니다. 


반응형

+ Recent posts

반응형