심심해서 하는 블로그 :: 'Data Mining' 카테고리의 글 목록

하이퍼파라미터 최적화.. 중요한 건 알겠는데.. 일일히 하는게 번거롭네?


하이퍼파라미터의 최적화는 기계학습의 중요한 태스크 중에 하나입니다.

하이퍼파라미터를 어떻게 선택하는 가에 따라 Overfit 이 된 모델이 될 수 도 있고, Underfit 모델이 될 수 있습니다.

하지만 같은 종류의 모델을 사용하더라도 학습하는 데이터에 따라서 하이퍼파라미터의 값은 상이합니다.


우리는 최적화된 파라미터를 찾기 위해서 값을 조정하면서, 모델을 수행을 하고 모델을 검증을 합니다.

그리고 이걸 결과가 좋을 때까지 하이퍼 파라미터 값을 재변경해서 다시 학습 및 검증을 하는 과정을 반복합니다.

근데 이 과정이 생각보다 귀찮습니다.


Optuna : 하이퍼파라미터 최적화 프레임워크

Optuna는 하이퍼파라미터 최적화 태스크를 도와주는 프레임워크입니다.

파라미터의 범위를 지정해주거나, 파라미터가 될 수 있는 목록을 설정하면 매 Trial 마다 파라미터를 변경하면서, 최적의 파라미터를 찾습니다.


하이퍼파라미터의 범위나 목록은 아래의 방법으로 설정할 수 있습니다.


  • suggest_int : 범위 내의 정수형 값을 선택합니다.

n_estimators = trial.suggest_int('n_estimators',100,500)

  • suggest_categorical : List 내의 데이터 중 선택을 합니다.

criterion = trial.suggest_categorical('criterion' ,['gini', 'entropy'])

  • suggest_uniform : 범위 내의 균일 분포를 값으로 선택합니다.

subsample = trial.suggest_uniform('subsample' ,0.2,0.8)

  • suggest_discrete_uniform : 범위 내의 이산 균등 분포를 값으로 선택합니다.

max_features = trial.suggest_discrete_uniform('max_features', 0.05,1,0.05)

  • suggest_loguniform : 범위 내의 로그 함수 선상의 값을 선택합니다.

learning_rate = trial.sugget_loguniform('learning_rate' : 1e-6, 1e-3)

 

소스 코드

이전 포스팅에서 사용했던 XGBoost Regressor를 최적화 하는데 사용한 소스코드 입니다.

전체 소스코드는 https://www.kaggle.com/ssooni/xgboost-lgbm-optuna 를 참고하시면 됩니다.


매 Trial 마다 수행할 함수를 작성합니다.

함수 내부에는 하이퍼파라미터의 값을 정의하는 부분과 모델을 학습하는 부분을 작성하고 Loss Metric의 결과를 반환합니다.



이제 아래의 소스를 참고해서 optuna를 사용해서 최적의 파라미터를 찾아봅시다.

최적의 파라미터는 study.best_trial.params 에 저장되어 있습니다.



optuna는 추가적으로 학습하는 절차를 확인할 수 있는 시각화 툴도 제공합니다.

다양한 시각화 기법을 제공하지만 저는 매 Trial 마다 Loss가 어떻게 감소되었는 확인 할 수 있는 함수와

하이퍼 파라미터 별로 중요도를 확인할 수 있는 함수를 소개 합니다.



도움이 되셨으면, 아래의 공감 버튼을 눌러주세요.

공감 버튼은 작성자에게 큰 동기부여가 됩니다.

,

캐글에서 사랑받는 XGBoost

요즘 캐글에서 문제를 도전해보는 재미로 공부를 하고 있습니다.

최근에는 Tabular Playground Series - Jan 2021[ kaggle.com/c/tabular-playground-series-jan-2021 ] 를 푸는 중에 다른 사람들이 올려놓은 노트북을 참고 해보니,

대부분이 XGBoost를 사용해서 문제를 풀고 있는 것을 확인 했습니다.

왜 이렇게 사랑 받고 있는지 궁금해서 직접 사용해보고 느낀 점을 정리해서 포스팅 해보곘습니다.


앙상블 기법 : Boosting

그 전에 XGBoost에서 사용하는 Boosting이라는 앙상블 기법에 대해서 간단하게 설명을 해보겠습니다.

앙상블은 직관적으로 "집단 지성" 이라고 생각을 하면 편리합니다. 여러 개의 모델을 사용해서 각각의 예측 결과를 만들고 그 예측 결과를 기반으로 최종 예측결과를 정하는 방법이죠


그 중 Boosting이란 방법은 비교적 약한 모델들을 결합하여 하나의 강한 모델 만드는 과정입니다.

모델 A이 예측한 결과를 다음 모델인 B에서 보완하여 예측하고 최종적으로 C에서 보완을 해서 예측 결과를 만들어 내는 방식이죠.

Boosting을 사용하는 대표적인 모델은 AdaBoost, Gradient Boost 등등이 이 있고 XGBoost는 그 중 Gradient Boosting 을 사용해서 모델링을 합니다.


Gradient Boosting 설명을 수학없이 간결하게 해준 유튜브 영상이 있어서 링크를 남겨 둘게요.

이거보다 설명 잘 할 수 없을거 같아요. 꼭 보세요. 두 번 보세요.

https://www.youtube.com/watch?v=3CC4N4z3GJc


XGBoost : Extreme Gradient Boosting

장점 : 계산 속도 향상, Kaggle에서 수차례 우승한 경력

sklearn 에도 Gradient Boosting 로 Regression 또는 Classification 을 할 수 있는 매소드를 제공하고 있습니다만, 시간이 너무 오래 걸립니다.

하지만 XGBoost의 경우에는 병렬 연산을 지원하여 계산 속도를 향상 시킵니다.

또한 GPU를 사용 할 수 있는 옵션을 제공하기 때문에 학습하는데 소요되는 시간을 절약할 수 있습니다.

이것은 하이퍼파라미터 튜닝을 할 때도 특히 빛이 납니다. (CPU 만으로 50회를 돌리다 하루가 넘게 소요되서 당황스러웠던 기억이 있습니다.)

 

XGBoost는 Overfitting 을 Overfitting 을 제어 하기 위해 두 가지 방법을 제안합니다.

1. 모델의 복잡도를 제어하는 방법 : max_depth, min_child_weight, gamma 하이퍼파라미터 사용

2. 다음 모델로 Loss 를 전달할 때 Random Value를 추가하는 방법 : subsample, colsample_bytree 파라미터 사용

 

그 외에도 early_stopping_rounds 를 사용해서 눈에 띄지 않게 Loss가 감소하지 않은 경우에는 학습을 진행하지 않도록 제어도 가능합니다.

학습 성능의 경우에는 kaggle의 수차례 우승한 사례를 봤을 때, 충분히 우수하다고 판단됩니다.


 

 

단점 : Tree Based Learning, 복잡한 하이퍼 파라미터

 

모든 학습 알고리즘이 그렇듯, 장점과 단점이 있기 때문에 데이터의 분포나 상황에 알맞게 사용해야 합니다.

 

1. Tree Based Learning 를 학습할 때 학습 데이터에 예민하게 반응합니다.

학습 데이터에서 1~10 사이의 범주를 가진 라벨로 학습을 진행한 모델이 있다고 합시다.

이 모델은 데이터의 예측을 1 ~ 10 사이의 값으로 예측을 하고자 하고 10 초과, 1 미만으로 반환을 하기 어렵습니다.

 

2. 적절하게 튜닝을 하지 않은 모델은 Overfit이 쉽게 발생하는 문제 때문에 반드시 진행해야하는 절차입니다.

그런데 XGBoost는 튜닝을 할 때 손봐야할 파라미터가 너무 많습니다.

(당장 xgboost 공식 홈페이지에 가서 내용을 봐도, 파라미터가의 수가 너무 많습니다.)


소스코드

https://www.kaggle.com/ssooni/xgboost-lgbm-optuna

소스코드에는 XGBoost 외에도 LightGBM, Optuna, Stacking 앙상블을 사용한 내용도 있습니다.


,

이전 포스팅 : 2019/01/15 - [Data Mining] - [Python] 공공데이터 API 사용기 (feat 미세먼지)

GitHub : https://github.com/ssooni/data_mining_practice

소소한 근황 소개

미세먼지 API를 사용하는 것을 이후로 SVM이나 Correlation 적용하는 등의 시도를 하였으나,

생각보다 결과도 좋지 않았고 내가 알고 있는 지식의 한계가 와서... 대학원을 가버렸다(응?)

또 약간의 지식을 배웠으니까 블로그에 정리를 해봅니다.


Sequence Model

앞선 포스팅에서 매 시간마다 측정한 미세먼지 및 기상 데이터를 수집을 하였습니다.

미세먼지나 기상데이터의 특징이라고 한다면 시간에 따른 순서가 있는 데이터라는 점입니다.

이러한 종류의 데이터를 모델링하는 걸 Sequence Model이라고 하며 RNN, LSTM, 이번에 다룰 GRU 등이 있다.

Sequence Model에 주로 적용하는 데이터는 주식이나 텍스트 등이 있다.

(우리가 사용하는 언어도 어순이 있기 때문에 Sequence Model 대상이다.)


일반적으로 사용하는 신경망에서 Sequence Data 를 사용하기 어려운 이유는 2가지입니다.

입력 데이터와 출력 데이터의 길이가 가변적이다.

대표적으로 CNN은 입력값이나 출력값의 길이가 모델을 학습하거나 적용할 때 동일합니다.

하지만 우리가 주로 보는 동영상이나 책을 입력 데이터로 한다면 각각의 데이터의 길이가 다른데,

이러한 점은 일반적인 인공 신경망 알고리즘에는 적합하지 않은 데이터입니다.

(일부 논문에서는 0으로 부족한 공간을 채워서 연구한 사례가 있긴 합니다.)

 

일반적인 신경망 알고리즘에서는 데이터의 순서를 반영하지 않습니다.

데이터를 섞어도 학습 결과가 크가 변화하지 않습니다.

즉, 데이터의 선후 관계가 전혀 반영이 되지 않는다는 점이 한계입니다.


RNN의 구조

뒤에서 자꾸 무언가를 준다..

RNN의 학습하는 과정을 도식을 하면 아래와 같이 보통 표현합니다.

 

RNN 구조 도식

파란 색으로 부분을 cell 이라고 합니다.

각각의 cell은 매 단계 마다 일렬로 입력되는 input x와 이전 단계의 결과로 산출된 hidden 값 h를 입력으로 받고 출력으로 예측 값인 y 와 다음 단계로 전달할 hidden 값을 전달합니다.


매 단계마다 이전 단계에서 만들어진 hidden 값을 사용하여 y 값을 생성하기 때문에
입력 값의 순서가 다르면 hidden 값 역시 다르기 때문에 앞서 말한 선후 관계가 반영되지 않은 문제점을 해결할 수 있습니다.

또한 RNN은 다양한 Input 과 Output 의 구조를 가질 수 있습니다.
1:N, N:M, N:1 등의 다양한 구조의 데이터를 적용합니다.


GRU : Gated Recurrent Unit

RNN은 단점이 있습니다. Cell 내부에서는 실제로 소수점 단위의 계산이 이뤄지고 있고 그 결과로 h 값이 발생하는데,

Sequence가 너무 긴 데이터의 경우에 0에 가까운 값이 오고 가게 되어 컴퓨터가 계산할 수 없는 작은 수로 수렴하게 되는 문제가 있습니다.

Gradient Vanishing 으로 불리는 이 문제는 이전 단계의 정보를 선택적으로 학습을 하는 즉, "줄 건 줘" 방식으로 계산하는 LSTM이나 GRU를 통해 개선이 되었습니다.


그리고 이전 단계의 데이터의 반영과 현재 데이터에 대한 반영의 비율을 Gate 를 통해 제어를 합니다.
GRU는 Reset / Update 두 종류의 게이트를 사용해서 학습하는 모델입니다.


Reset Gate

Reset Gate 에서는 이전 단계의 Hidden status 값과 현 단계의 x 값을 Sigmoid 함수에 적용하여 0 ~ 1 사이의 값을 얻습니다.

 이 값을 이용해서 과거(이전 데이터)의 Hidden 정보를 현재의 정보를 구하는데 얼마만큼 반영 할 것인지 정합니다.


Update Gate

Update Gate 에서도 이전 단계의 Hidden status 값과 현 단계의 x 값을 Sigmoid 함수에 적용하여 0 ~ 1 사이의 값을 얻습니다.

물론 Reset Gate의 Weight 와 다른 Weight를 이용해 계산합니다. 

이 값을 이용해서 전체 데이터의 양을 1로 했을 때 현재의 정보를 반영할 비율을 $z_t$, 과거의 정보를 반영할 비율을 $1-z_t$ 로 합니다.
이제 현재의 정보를 아래의 식을 이용해서 구합니다. 

Reset Gate에서 구한 $r_t$를 이용해서 현재 정보를 얻습니다.

마지막으로 현재의 HIdden Status를 과거의 Hiddent Status와 현재의 정보를 앞서 구한 비율 $z_t$ 를 적용하여 계산 후 다음 단계로 전달합니다.

 

 

Pytorch 로 적용 해보기

전체 소스 코드 : https://github.com/ssooni/data_mining_practice/blob/master/dust_weather/GRU.ipynb

class GRU(nn.Module):
    def __init__(self, n_layers, hidden_dim, input_dim, n_classes=1, dropout_p=0.2):
        super(GRU, self).__init__()
        self.n_layers = n_layers
        self.hidden_dim = hidden_dim

        self.dropout = nn.Dropout(dropout_p)
        self.gru = nn.GRU(input_dim, self.hidden_dim, num_layers=self.n_layers, batch_first=True, dropout=dropout_p)
        self.fc = nn.Linear(self.hidden_dim, n_classes)

    def forward(self, x):
        h0 = self._init_state(x.size(0))
        out, (hn) = self.gru(x, (h0.detach()))
        out = self.fc(out[:, -1, :]) 
        return out

    def _init_state(self, batch_size=1):
        weight = next(self.parameters()).data
        return weight.new(self.n_layers, batch_size, self.hidden_dim).zero_().to(DEVICE)

 

전체 소스코드와 실행 결과를 GitHub 에 올려 두었습니다. 
매 시간 측정된 날씨 데이터와 미세 먼지 데이터를 이용해서 예측을 해보는 것을 해보았습니다.
실습용으로 사용할려고, 전처리도 간단하게 하고 프로그램도 간단하게 구현했습니다. 
참고용으로만 사용하시고, 원하는 도메인에 적절하게 적용하세요

 

> 공감 버튼은 작성자에게 큰 동기부여가 됩니다.

,

GitHub : https://github.com/ssooni/data_mining_practice


새로운 시리즈 시작!!

이전 카카오 시리즈에 이어서 미세먼지 데이터와 기상 데이터를 이용해서 데이터 마이닝 기법들을 적용하는 연습을 해보았습니다.

(그거 보다 궁금했어요.. 기상과 미세먼지와 밀접한 관계가 있는지 말이죠)

데이터 수집, 전처리, 상관 분석 등 적용 해보고 싶은 방법들을 적용하고 결과를 관찰하도록 하겠습니다.


미세먼지 데이터 수집



서울시 열린데이터광장(http://data.seoul.go.kr/)에서 시간별 평균 대기오염도 정보 데이터를 제공합니다.

서울의 구 단위로 시간단위 미세먼지 농도를 제공하고 2009년 데이터도 존재하길래 사용하기 적절하다고 판단하여 인증 키를 취득하였습니다.

회원가입 절차를 진행한 후에 간단하게 사용 목적을 명시하면 인증 키를 획득할 수 있었습니다.

데이터의 저작권은 출처를 밝히면 자유롭게 사용이 가능합니다.




기상 데이터 수집


열린데이터광장에 서울시의 과거 기상데이터도 제공하지만, 데이터가 썩 만족스럽지가 않아서 기상자료개방포탈(https://data.kma.go.kr/cmmn/main.do)에서 기상데이터를 수집하였습니다.

기상포탈에는 Open-API 서비스로 다양한 종류의 기상 데이터를 제공합니다.

저는 시간 단위로 측정된 미세먼지 데이터를 수집하였기 때문에 기상 데이터도 종관기상관측 시간자료 를 이용하였습니다.

마찬가지로 회원가입을 진행한 후에 API 사용 신청을 하면 별다른 승인 절차 없이 API 사용이 가능합니다.

API Key는 마이페이지 > 오픈 API 현황에서 확인 가능합니다.




API 호출 프로그램 작성

전체 소스코드는 GitHub를 참조바랍니다.

API Key 및 전체 데이터의 경우 공유가 힘든 점 양해바랍니다.


REST API는 Http URL의 요청을 처리하여 우리가 보통 사용하는 WEB 서비스는 Response를 HTML로 하는 것과 달리 JSON이나 XML 등의 형태로 Response하는 API 입니다.

Python에는 requests 모듈이 있어서 http url만 입력하면 reponse를 획득할 수 있습니다.


미세먼지 데이터 API

먼저 서울시 시간별 평균 대기오염도 정보 데이터 API를 호출하는 프로그램을 작성하였습니다.

URL에 특정일자를 넣어주면 사용자가 설정한 start_index에서 end_index까지 데이터를 불러오는 API 구조입니다.

list_total_count 라고 해당 호출의 최대 index 값을 제공하므로 아래의 절차로 프로그램을 구상하였습니다.


  1. 최초 호출에서 list_total_count를 획득한다.
  2. 다음 호출에서 end_index 값을 list_total_count까지 사용한다.
  3. 날짜별로 데이터를 CSV로 저장한다.

## callAPI.py
def call_api(api_name, start_date, end_date, dir_name):
    # API 키는 공개하기 힘든 점 양해 바랍니다.
    api_key = open("./raw_data/api_key").readlines()[0].strip()
    url_format = 'http://openAPI.seoul.go.kr:8088/{api_key}/json/{api_name}/1/{end_index}/{date}'
    headers = {'content-type': 'application/json;charset=utf-8'}

    for date in pd.date_range(start_date, end_date).strftime("%Y%m%d"):
        # 최초 1회 Call은 해당 일자의 데이터 수를 확인한다.
        url = url_format.format(api_name=api_name, api_key=api_key, end_index=1, date=date)
        response = requests.get(url, headers=headers)
        end_index = response.json()[api_name]["list_total_count"]
        print("Max Count(%s): %s" % (date, end_index))

        # 해당 일자의 모든 데이터를 불러온다.
        url = url_format.format(api_name=api_name, api_key=api_key, end_index=end_index, date=date)
        response = requests.get(url, headers=headers)
        result = pd.DataFrame(response.json()[api_name]["row"])

        # 수집된 데이터를 CSV로 저장합니다.
        result.to_csv("./raw_data/%s/dust_%s.csv" % (dir_name, date), index=False, encoding="utf-8")

        # API 부하 관리를 위해 0.5초 정도 쉬어 줍시다 (찡긋)
        sleep(0.5)





기상 데이터 API


다음은 기상데이터 API를 사용하는 함수를 작성하였습니다.

이 API는 조회 시작 일자와 시간 / 최종 일자와 시간, 가져올 데이터의 수를 Param으로 사용합니다.

안전하게 저는 1회 요청시 하루 단위로 넉넉하게 최대 100건 정도를 요청하였습니다.

매 시간 단위로 측정되는 데이터라 많아야 24건이기 때문이죠.

결과는 마찬가지로 CSV 파일로 저장하였습니다.


## callAPI.py
def call_weather_api(start_date, end_date):
    # API 키는 공개하기 힘든 점 양해 바랍니다.
    api_key = open("./raw_data/weather_api").readlines()[0].strip()
    url_format = 'https://data.kma.go.kr/apiData/getData?type=json&dataCd=ASOS&dateCd=HR&startDt={date}&startHh=00&endDt={date}&endHh=23&stnIds={snt_id}&schListCnt=100&pageIndex=1&apiKey={api_key}'

    headers = {'content-type': 'application/json;charset=utf-8'}
    for date in pd.date_range(start_date, end_date).strftime("%Y%m%d"):
        print("%s Weather" % date)
        url = url_format.format(api_key=api_key, date=date, snt_id="108")
        response = requests.get(url, headers=headers, verify=False)
        result = pd.DataFrame(response.json()[-1]["info"])
        print(result.head())
        result.to_csv("./raw_data/weather/weather_%s.csv" % date, index=False, encoding="utf-8")

        # API 부하 관리를 위해 0.5초 정도 쉬어 줍시다 (찡긋)
        sleep(0.5)



데이터 종합

미세먼지와 기상 데이터를 2009년 1월 1일부터 2019년 1월 1일 구간으로 수집하였습니다.

두 개의 데이터는 공통적으로 날짜 컬럼이 있기 때문에 날짜 컬럼을 기준으로 Join을 해주었습니다.

데이터의 양이 많아진 만큼 File I/O 속도가 느린 CSV 포멧이 아닌 HDF 포멧으로 변경하였습니다.


## callAPI.py
def concat_data():
    df_list = list()

    # ./raw_data/dust 아래의 모든 파일을 읽습니다.
    for root, dirs, files in os.walk("./raw_data/dust", topdown=False):
        for name in files:
            df_list.append(pd.read_csv(os.path.join(root, name)))

    dust = pd.DataFrame(pd.concat(df_list, sort=False))

    # Datetime 형태로 Index를 변경해줍니다.
    dust["MSRDT"] = dust["MSRDT"].apply(lambda x: dt.datetime.strptime(str(x), "%Y%m%d%H%M"))
    dust = dust.set_index("MSRDT")

    df_list.clear()

    # ./raw_data/weather 아래의 모든 파일을 읽습니다.
    for root, dirs, files in os.walk("./raw_data/weather", topdown=False):
        for name in files:
            df_list.append(pd.read_csv(os.path.join(root, name)))
    weather = pd.DataFrame(pd.concat(df_list, sort=False))

    # Datetime 형태로 Index를 변경해줍니다.
    weather["TM"] = weather["TM"].apply(lambda x: dt.datetime.strptime(x, "%Y-%m-%d %H:%M"))
    weather = weather.set_index("TM")

    # join() 함수는 같은 index 끼리의 join을 제공합니다.
    weather.join(dust, how="inner").to_hdf("./raw_data/data.hdf", "master")
    dust.to_hdf("./raw_data/data.hdf", "dust")
    weather.to_hdf("./raw_data/data.hdf", "weather")


제공받은 데이터에서 미세먼지에 해당하는 컬럼은 PM10 또는 PM25가 있습니다.

각각 10μm, 2.5μm 이하의 먼지의 농도라고 하는데 그 중에서 초미세먼지 농도에 해당하는 PM25를 사용하고자 한다.

데이터의 분포를 확인하고자 Box Plot을 그려서 확인하고자 한다.

이 때 지역별로 확인해 보는 것이 눈에 더 잘 보일거 같아서 확인해 보았는데..




아니 저 수치가 정말 나올 수 있는 수치인가요..??

서대문구는 무슨 일이 있었던 걸까요..??

노이즈라고 생각이 드는데 다음 포스팅에서 어느 정도 전처리를 하고 사용해야 할 거 같아요.

2021.01.06 : 기상청에서 관리하던 데이터를 공공데이터포털에서 관리하는 걸로 변경이 되었습니다.


긴 글 읽어 주셔서 감사합니다.

공감 버튼은 작성자에게 큰 동기부여가 됩니다.


,

Github > https://github.com/ssooni/data_mining_practice

- 자세한 소스코드는 Github에 올렸습니다.

- 데이터 파일도 올려 드리고 싶지만 실제 카카오톡 대화내용이라 일부분만 올렸습니다.

- 소스코드는 짬짬히 업로드할 예정입니다.




클러스터링

비지도학습의 대표 주자인 클러스터링은 각각의 노드들의 군집들을 산출해줍니다.

그 중에서 가장 간단한 게 K-means 알고리즘이고, 때마침 학교에서 공부한 적이 있으니까 해봐야겠다는 생각을 했습니다. 

하지만 이 알고리즘은 클러스터가 몇 개 나올것이다는 것을 제가 직접 설정해야 하는 점이 마음에 안들었습니다.

지금 Input으로 들어오는 벡터는 300차원이라 어디에 군집에 형성되어 있는지 짐작하기 어렵기 때문에 K-means 알고리즘은 사용하기 어렵다고 판단하였습니다.

저는 군집 갯수를 사전에 설정하지 않아도 군집을 산출해주는 알고리즘이 필요했고, 그 결과 DBSCAN 알고리즘을 선정하였습니다.


DBSCAN 

DBSCAN은 밀도 기반의 클러스터링 기법입니다.

DBSCAN의 클러스터는 아래 두 가지 조건에 의해 생성됩니다.


1. 이웃하는 점들의 간격이 사용자가 정한 거리(eps) 이내에 있는 경우를 산출

2. 군집에 포함하는 점들이 최소 클러스터 원소 수(min_samples)보다 많은 경우를 산출합니다.

min_sample = 3 인 경우


Core Point는 1번 조건과 2번 조건을 모두 충족하는 Point를 의미합니다. (빨간 점) 

Non-Core Point는 1번 조건을 만족하지만 2번 조건을 만족하지 못하는 Point입니다. (파란 점)

Non-Core Point는 Cluster를 이루는 경계 점 역할을 합니다.

Noise는 2개의 조건을 모두 만족하지 않은 경우로 클러스터를 구성하지 않습니다. (녹색 점)


이런 방법으로 클러스터를 찾는 것의 장점은 클러스터의 갯수를 지정할 필요 없다원형이 아닌 클러스터도 추출이 가능하다는 점입니다.

다른 알고리즘은 클러스터의 중심점을 이동하면서 클러스터 중심점 내부에 몇개의 점이 들어 있는 가에 집중되어 있기 때문에 클러스터의 모양이 필연적으로 원형으로 형성됩니다.

하지만 DBSCAN은 각각 개별 포인트 간의 거리를 측정하면서 Non-Core Point에 의해 클러스터의 경계가 형성되기 때문에 원형이 아닌  모양의 클러스터도 형성이 가능하다는 점이 장점입니다.


하지만 데이터에 대한 충분한 이해도를 가지고 있지 않는다면 eps와 min_points 값을 정하는 것이 어려운 것이 단점입니다.

다른 알고리즘은 단순히 10개의 분류를 만들어 달라 하면 10개를 만들어 주기 때문에 데이터 내부의 값들을 이해하지 않아도 충분히 클러스터를 만들어 낼 수 있습니다.

하지만 DBSCAN은 적어도 포인트 간이 이루는 최대 거리가 얼마인지 정도는 파악해야 유효한 클러스터를 만들어 낼 수 있습니다.


Word Vector Clustering

워드 백터를 클러스터링하면 어떤 결과가 나올까? 하는 의문에서 시작하였습니다.

모든 코드는 GitHub를 참조바랍니다.




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from sklearn.cluster import DBSCAN
from gensim.models import Word2Vec
 
def cluster(eps, min_sample):
    # Word Vector를 Load 합니다.
    model = Word2Vec.load("./result/embedding.model")
 
    word_vector = model.wv.vectors
    match_index = model.wv.index2word
    model.init_sims(replace=True)
 
    # 두 글자이상 한글인 경우만 사용
    han = re.compile(r"[가-힣]{2,}")
 
    # DBSCAN 알고리즘 적용
    dbscan = DBSCAN(eps=eps, min_samples=min_sample)
    clusters = dbscan.fit_predict(word_vector)
 
    df = pd.DataFrame(clusters, columns=["cluster"], index=match_index).reset_index()
    df.columns = ["word""cluster"]
    print(df.head())
 
    # 한글만 필터링 처리
    df = df[df["word"].apply(lambda x: len(han.findall(x)) > 0)]
 
    # 노이즈 포인트 제거
    df = df[df["cluster"!= -1]
 
    print(df.groupby(["cluster"]).count())
 
    df.to_excel(pd.ExcelWriter("./result/cluster.xlsx"), index=False)
 
cs


line 15-16

본격적으로 DBSCAN 알고리즘을 적용하는 부분입니다.

eps와 min_samples를 정하여 넣어준 후 line 16에서 word vector를 클러스터링 해주었습니다.

fit_predict 함수를 사용하면 클러스터링된 결과를 리스트로 산출해줍니다.

이 때 -1은 노이즈 포인트를 의미합니다.


line 23-27

클러스터링 후 데이터를 일부 처리하였습니다.

2글자 이상의 한글만 유효한 결과로 선정하였으며, 노이즈 포인트도 제거 하였습니다.


이 프로그램을 실행하면 아주 허접한 그래프를 보여주었습니다.


클러스터링을 했을 때 종결 어미나 일자, 직책 같은 정보를 담고 있는 클러스터를 보고 신기했습니다.

뭔가 클러스터링을 하면 전체적인 주제를 알 수 있지 않을까? 생각 했지만 아쉽게도 그런 정보는 나타나질 않았네요.

찾아 보니까 LDA라는 걸 사용하면, 토픽 모델링이 가능하다고 하는데 다음번엔 그것을 시도해볼까합니다.

 

긴 글 읽어주셔서 감사합니다.

공감 버튼은 작성자에게 큰 동기부여가 됩니다.


,


Github > https://github.com/ssooni/data_mining_practice

- 자세한 소스코드는 Github에 올렸습니다.

- 데이터 파일도 올려 드리고 싶지만 실제 카카오톡 대화내용이라 일부분만 올렸습니다.

- 소스코드는 짬짬히 업로드할 예정입니다.




Word to Vector

Word2Vec은 간단하게 텍스트를 숫자로 변환하는 것을 의미합니다.

컴퓨터가 모든 텍스트의 의미를 이해해서, Context를 잘 잡아낸다면 굳이 이런 작업을 하지 않아도 됩니다.

하지만 아직까지 컴퓨터는 숫자로 이루어진 세계에 대한 연산을 더 잘 합니다. 

숫자로 이루어진 세계에서 마치 컴퓨터가 우리가 글을 이해하는 듯한 알고리즘을 적용한다면 문맥을 이해할 수 있지 않을까? 하는 취지에서 시작되었습니다.


그렇다면 텍스트를 특정한 벡터 좌표로 표현할려면 어떤 방법들이 있을까요.

하나는 각각의 차원에 텍스트가 가질 수 있는 특성을 부여하는 방법입니다.



각각의 차원에 특성을 두고 사람과 닭이 가지는 특성에 맞게 배치를 하였습니다. 

사람과 닭은 예제에서 보여준 4가지의 속성에 따라 서로 구분이 가능하다는 것을 알 수 있습니다.

하지만 원숭이와 사람을 이 속성이 따라 구분하면 어떻게 될까요?



사람과 원숭이는 4가지의 속성으로는 서로 구분이 가기 힘듭니다.

따라서 우리는 사람과 원숭이를 구분할 수 있는 특성을 추가적으로 넣어야 합니다.

이렇게 계속 다른 것과 구분을 하기 위해 특성을 추가하다보면 이 세상 모든 단어를 표현하는데 필요한 차원은 무한대에 가까울 것입니다.

그리고 그런 특성을 사람이 일일히 부여하는 것은 매우 피곤한 일입니다.


CBOW 방법 vs Skip-Gram 방법

그래서 이번에는 다른 방법을 사용하여, 텍스트를 벡터로 표현하고자 합니다.

 


위의 문제가 주어졌을 때 운동이나 밥, 노래들을 잘하는지는 이전 또는 이후 문장을 확인해야 알 수 있습니다. 

주변의 단어를 통해 중심 단어를 추측하는 방법이 CBOW 방법입니다.  



반대로 Skip-Gram 방법은 어떤 단어가 등장했을 때 근접한 단어가 발생하는 양상을 측정하는 방법입니다.

노래라는 중심 단어를 기준으로 앞뒤 N개의 단어가 등장하는 확률을 구하여 Vector로 만들어 줍니다.



관련 키워드 찾기

Word2Vec 모듈의 설치방법은 아래와 같습니다.

>> pip install word2vec


앞선 시리즈에서 형태소 분석기를 통해 Token을 만들었는데 그것을 이용해서 Word2Vec를 알고리즘을 적용합니다.

자세한 소스코드는 GitHub를 참조해주세요.





word2vector.py


1
2
3
4
5
6
7
8
9
10
11
12
13
14
def create_model(filename, skip_gram=False):
    tokens = pd.read_csv(filename)
    tokens = tokens[tokens["contents"].apply(lambda x: 'http' not in x)]
 
    sentence = tokens["token"].apply(lambda x: ast.literal_eval(x)).tolist()
    
    if skip_gram:
        model = Word2Vec(sentence, min_count=10, iter=20, size=300, sg=1)
    else:
        model = Word2Vec(sentence, min_count=10, iter=20, size=300, sg=0)
 
    model.init_sims(replace=True)
    model.save("./result/embedding.model")
 
cs


line 3

카카오톡의 대화 내용 중에서 링크를 첨부하는 경우를 제외하였습니다. 


line 5

Word2Vec의 Sentence는 iterable한 자료형이 들어가면 됩니다.

저는 이전 시리즈에서 형태소 분석기로 Tokenize한 결과를 넣었습니다.


line 7-10

Skip Gram 방법을 사용할지 CBOW 방법을 사용할지 선택합니다.

각각의 파라미터는 반드시 똑같이 따라 하실 필요는 없습니다.


size : 몇 차원의 Vector를 만들지 여부를 결정합니다.

min_count : 특정 빈도수 이상 발생한 단어에 대하여 Embedding을 합니다.

iter: 반복 횟수


line 13

완성된 모델을 파일로 저장합니다.


word2vector.py

1
2
3
4
5
def most_similar():
    model = Word2Vec.load("./result/embedding.model")
    print("용돈과 관련된 키워드 : ", model.most_similar("용돈"))
    print("졍이와 관련된 키워드 : ", model.most_similar("졍이"))
    print("쭈니와 관련된 키워드 : ", model.most_similar("쭈니"))
cs


line 2

앞서 생성한 모델을 읽어드립니다.


line 3 - 5

most_similar 함수는 Word Vector에 Cosine 값을 내적을 통해 구한 뒤 그 값이 클수록 유사하다는 Cosine Similarity 방법을 사용하여 특정 단어와 관련된 단어를 찾아줍니다. 



관련도 수치가 낮은 게 아쉽긴 합니다만, 관련된 단어라고 생각하는 것들이 나와서 나름 만족 스럽습니다.

다음 시리즈에서는 벡터화된 데이터를 이용하여, 클러스터링을 하면 어떤 결과가 나오는지 확인해보도록 하겠습니다.


긴 글 읽어 주셔서 감사합니다.

공감 버튼은 글쓴이에게 큰 동기부여가 됩니다.

 

,

rGithub > https://github.com/ssooni/data_mining_practice

- 자세한 소스코드는 Github에 올렸습니다.

- 데이터 파일도 올려 드리고 싶지만 실제 카카오톡 대화내용이라 일부분만 올렸습니다.

- 소스코드는 짬짬히 업로드할 예정입니다.




카카오톡 시리즈의 2번째로 워드클라우드를 만들어 보았습니다.

이전 시간에서 만들었던 명사 추출 결과를 사용하여 워드클라우드를 그릴 것입니다.

 

1. 단어의 빈도 수를 측정  

워드클라우드에서 글자 크기를 정하는 기준을 저는 단어의 빈도수로 결정하였습니다. 

특정 컬럼을 그룹으로 하여 집계하는 방법은 pandas에서 간단하게 구현이 가능합니다.


mwordcloud.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def draw_wordcloud(kkma_result):
    # List로 되어있는 열을 Row 단위로 분리
    tokens = pd.DataFrame(kkma_result["token"].apply(lambda x: ast.literal_eval(x)).tolist())
 
    tokens["Date"= kkma_result["Date"]
    tokens["Speaker"= kkma_result["Speaker"]
    tokens["timetype"= kkma_result["timetype"]
    tokens["time"= kkma_result["time"]
    tokens["contents"= kkma_result["contents"]
    
    tokens = tokens.set_index(["Date""Speaker""timetype""time""contents"])
    tokens = tokens.T.unstack().dropna().reset_index()
 
    tokens.columns = ["Date""Person""time_type""time""sntc""index""token"]
    print(tokens.head())
 
    # 빈도수 집계
    summary = tokens.groupby(["token"])["index"].count().reset_index()
    summary = summary.sort_values(["index"], ascending=[False]).reset_index(drop=True)
 
    """ 이하 생략 """ 
cs


모든 소스코드는 GitHub를 참조바랍니다.


제가 정의한 draw_wordcloud()는 집계를 하고 빈도수가 많은 순서대로 정렬한 후 워드클라우드를 생성합니다.


line 2-15

시리즈 1의 결과를 보면 token 컬럼 안에 리스트로 토큰들을 저장하도록 구성되어 있습니다. 


이렇게 구성되어 있으면 분석하기 상당히 불편하므로 리스트 안에 있는 원소를 풀어서 새로운 행을 구성하는 것이 line2 - 14 입니다.

만약 시리즈를 이어서 하는 것이 아니라면 line 2-14번은 생략하셔도 됩니다.


line 18 - 19

pandas.DataFrame의 groupby는 특정한 컬럼을 그룹으로 묶어줍니다.

그리고 그룹을 대상으로 min(), max(), count()등의 집계함수를 제공합니다. 

이 경우에는 모든 구간에 대하여 token을 기준으로 발생한 빈도수를 집계하였습니다. 

순위를 측정하기 위해서 집계한 결과를 내림차순으로 정렬까지 완료합니다.


2. 워드클라우드  

데이터과학에서 시각화는 생각보다 큰 비중을 차지하는 분야입니다. 

어떻게 보여줘야 분석에 용이하고 데이터 분석결과를 받아 드리는 사람들이 쉽게 받아 드릴수 있기 때문입니다.

측정한 빈도수를 기반으로 Line나 막대 그래프도 그릴 수 있습니다만, 몇 번 나왔는지에 큰 의미를 두지 않는다면 빈도수는 TMI가 될 수 있습니다.

키워드에만 집중하기 위한 시각화 표현 기법으로 주로 워드클라우드를 사용합니다.

그리고 Python의 WordCloud 모듈은 개발자들에게 쉽고 빠른 방법으로 워드클라우드를 생성하는 것을 도와줍니다.


>> pip install wordcloud


워드클라우드 모듈을 설치가 완료되었다면 이제 사용하는 소스 코드를 작성해 봅시다.




mwordcloud.py

1
2
3
4
5
6
7
8
def draw_wordcloud(kkma_result):
 
    """ 상단 생략 """
 
    wc = WordCloud(font_path='./font/NanumBrush.ttf', background_color='white', width=800, height=600).generate(" ".join(summary["token"]))
    plt.imshow(wc)
    plt.axis("off")
    plt.show()
cs


line 4

WordCloud의 generate() 함수는 공백으로 분리된 문자열 리스트를 받습니다.

그리고 가장 첫 번째의 단어를 가장 크게, 가장 마지막 단어를 가장 작게 표현합니다.

앞서 빈도 수를 기준으로 token을 정렬하였기 때문에 가장 많은 빈도 수가 나오는 문자가 가장 크게 나올 것입니다. 

아래 그림처럼 말이죠.


 

카카오톡은 이모티콘과 사진, 동영상을 전송한 채팅 기록에 대하여는 (이모티콘) 이런 식으로 저장하니까 이모티콘과 사진이 압도적으로 많이 나오는 걸 볼 수 있습니다.


3. 특정 단어를 제외하자  

정말 직업으로 이모티콘을 개발하는 사람끼리 얘기하는 것이라면 이모티콘 단어가 큰 의미로 받아 지겠지만, 그냥 개발자와 그 여자친구간에 얘기에서 이모티콘은 큰 의미가 있는 단어는 아닙니다.

이번 단계에서는 노이즈라고 생각하는 단어들을 제외하는 과정을 진행해서 유효한 단어들만 보여주고 싶습니다.


mwordcloud.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def get_except_keyword(filename):
    keyword_list = list()
    with open(filename, encoding='utf-8') as f:
        for keyword in f.readlines():
            keyword_list.append(keyword.strip())
    print(keyword_list)
    return keyword_list
 
def draw_wordcloud(kkma_result):
 
    """ 상단 생략 """
    
    # 특정 단어 필터링
    except_keyword = get_except_keyword("./raw_data/except_word.txt")
    summary = summary[summary["token"].apply(lambda x: x not in except_keyword)]
    summary = summary[summary["token"].apply(lambda x: len(x) > 1)]
 
    wc = WordCloud(font_path='./font/NanumBrush.ttf', background_color='white', width=800, height=600).generate(" ".join(summary["token"]))
    plt.imshow(wc)
    plt.axis("off")
    plt.show()
 
cs



./raw_data/except_word.txt 에는 아래와 같이 제외하고자 하는 단어들의 리스트가 저장 되어있습니다.


./raw_data/except_word.txt


line 14 - 16

get_except_word() 함수에서 해당 파일을 읽어서 리스트 형태로 제외 문자열을 가지고 있습니다.

line 15에서 해당 문자열 리스트 안에 token이 포함되어 있으면 제외하는 로직이 적용됩니다.

그리고 추가적으로 line 16에서 token의 글자 수가 1개인 경우도 제외하였습니다.



아까보다는 저한테는 필요한 것이 나오긴 했지만, 좀 더 필터링을 한다면 더 좋을 거 같아요.

근데 사각형 너무 식상하지 않나요?


4. 워드클라우드 Mask 적용

Mask를 적용하면 워드클라우드가 이미지에 따라서 맞춰서 단어를 배열해 줍니다. 

바탕색이 투명한 PNG 파일을 하나 준비합니다.

저는 여자친구가 좋아하는 데덴네 이미지를 하나 준비하였습니다.


mwordcloud.py

1
2
3
4
5
6
7
8
9
10
11
12
13
from PIL import Image
 
def draw_wordcloud(kkma_result):
 
    """ 상단 생략 """
    
    denne_mask = np.array(Image.open("./font/denne.png"))
 
    wc = WordCloud(font_path='./font/NanumBrush.ttf', background_color='white', width=800, height=600, mask=denne_mask).generate(" ".join(summary["token"]))
    plt.imshow(wc)
    plt.axis("off")
    plt.show()
 
cs

 

line 7

마스크로 사용할 이미지를 읽어서 Numpy.Array로 저장합니다. 


line 9

WordCloud 생성자에 mask 파라미터에 line 7에 만들어 둔 마스크를 넣어 줍니다.

 


실행한 결과입니다. 

원하는 이미지를 사용해서 적용해 보는 것이 좋을 거 같아요.

다음 시리즈는 Word2Vec을 적용해본것을 올려 볼까 합니다.


전체적인 소스코드는 GitHub를 참조하시고, 궁금한 사항이 있으시면 댓글 달아 주시면 최대한 빨리 답변드리겠습니다.


긴 글 읽어주셔서 감사합니다.

공감버튼은 작성자에게 큰 동기부여가 됩니다.

,

- 자세한 소스코드는 Github에 올렸습니다.

- 데이터 파일도 올려 드리고 싶지만 실제 카카오톡 대화내용이라 일부분만 올렸습니다.

- 소스코드는 짬짬히 업로드할 예정입니다.


오래 전부터 시도를 해보고 싶었지만, 카카오톡 대화 데이터를 이용해서 워드 클라우드부터 Word2Vector 등등 텍스트 마이닝에서 사용하는 기법을 사용해 볼려고 합니다.

통계적 지식은 거의 전무하지만, 1년간 우연히 텍스트 마이닝 프로젝트를 하면서 어깨 너머 보고 배운 것을 기반으로 소소한 프로젝트를 만들어 보았습니다.


프로젝트 폴더 구조


1. 카카오톡 대화를 Text 파일로 저장합니다.

컴퓨터 카카오톡을 켜서 아무 대화 방에서 Ctrl + S를 누르면 대화를 텍스트 파일로 저장이 가능합니다.

저는 여자친구와 나눈 대화를 실험 재료로 사용하고자 합니다.



그리고 프로젝트의 raw_data 폴더에 넣어 둡니다.

저는 raw_data/kko.txt 파일로 저장해두었습니다.



2. 정규표현식

정규표현식은 찾고자하는 텍스트가 일정한 패턴을 가지고 있을 때, 해당 패턴을 표현하기 위해 사용하는 언어입니다.

가독성이 썩 좋지 않아서 처음에 진입하기 꺼려지지만, 알아두면 텍스트에서 원하는 부분만 가져오기 편합니다.

카카오톡에 저장된 내용을 보면 다음과 같은 패턴을 알 수 있습니다.



언젠가 사용하리라 생각하면서, 일자 정보누가 몇 시어떤 말을하는지까지 패턴을 이용하여 추출하고자 합니다.

위의 패턴을 정규표현식으로 표현하면 다음과 같습니다.



꼼꼼하게 날짜의 패턴에서 숫자의 갯수를 지정하거나, 시간 부분도 표현이 가능하지만, 정형화된 패턴이라 너프하게 표현했습니다.  

이제 Python으로 정규표현식을 적용해봅시다.


regex.py의 일부분

1
2
3
4
5
6
7
8
9
def apply_kko_regex(msg_list):
    kko_pattern = re.compile("\[([\S\s]+)\] \[(오전|오후) ([0-9:\s]+)\] ([^\n]+)")
    kko_date_pattern = re.compile("--------------- ([0-9]+년 [0-9]+월 [0-9]+일) ")
 
    emoji_pattern = re.compile("["u"\U0001F600-\U0001F64F"  # emoticons
                               u"\U0001F300-\U0001F5FF"  # symbols & pictographs
                               u"\U0001F680-\U0001F6FF"  # transport & map symbols
                               u"\U0001F1E0-\U0001F1FF"  # flags (iOS)
                               "]+", flags=re.UNICODE)
cs


파이썬의 re package으로 정규표현식을 컴파일하여 문자열에서 패턴 추출을 할 수 있도록 제공합니다.

카카오톡 대화 부분과 날짜 패턴을 생성합니다. 그리고 추가적으로 이모지 패턴을 넣어줍니다. 

(이모지 패턴은 StackOverflow를 참고하여 작성하였습니다.)

이모지 패턴은 이후에 사용할 형태소 분석기 꼬꼬마(Kkma)에서 이모지를 만나면 에러가 발생해서, 이모지를 제거하기 위한 목적으로 만들었습니다.

regex.py의 일부분

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def apply_kko_regex(msg_list):    
 
    """ 상단 생략 """
 
    kko_parse_result = list()
    cur_date = ""
 
    for msg in msg_list:
        # 날짜 부분인 경우
        if len(kko_date_pattern.findall(msg)) > 0:
            cur_date = dt.datetime.strptime(kko_date_pattern.findall(msg)[0], "%Y년 %m월 %d일")
            cur_date = cur_date.strftime("%Y-%m-%d")
        else:
            kko_pattern_result = kko_pattern.findall(msg)
            if len(kko_pattern_result) > 0:
                tokens = list(kko_pattern_result[0])
                # 이모지 데이터 삭제
                tokens[-1= re.sub(emoji_pattern, "", tokens[-1])
                tokens.insert(0, cur_date)
                kko_parse_result.append(tokens)
 
    kko_parse_result = pd.DataFrame(kko_parse_result, columns=["Date""Speaker""timetype""time""contents"])
    kko_parse_result.to_csv("./result/kko_regex.csv", index=False)
 
    return kko_parse_result
cs

이후의 코드는 파일에서 읽은 메세지를 정규식을 이용하여 패턴을 추출하고 이모지의 경우 삭제하는 코드입니다.


line 14

문자열 Type str의 내장 함수 findall()은 정규식 패턴에 해당하는 모든 문자열을 추출해줍니다. 


line 18

특정 패턴을 가진 문자열을 치환하는 경우 re.sub()를 사용합니다.

re.sub(패턴, 치환 문자열, 대상 str) 

지금의 경우에는 모든 이모지 패턴의 문자를 메세지 텍스트에서 삭제하기 위해 사용하였습니다.


line 22-23

추출된 결과를 pandas.DataFrame에 저장하고 csv 파일로 저장합니다.





3. 한글 형태소 분석기 적용

우리가 글을 배울 때, 단어를 익히고 문장을 배우고 문장들을 이어서 문맥을 완성합니다. 

이처럼 텍스트를 분석할 때 처음부터 문장 전체를 사용하지 않습니다. 

작은 단위로 쪼개는 과정을 Tokenizing 라고 하는데, 형태소 단위까지 쪼개는 경우도 있고, 단어 중심으로 쪼개는 경우도 있습니다.


저는 한글 형태소 분석 모듈 KoNLPy를 이용하여 형태소 단위로 쪼개는 것을 선택하였습니다.

설치는 Anaconda Prompt에서 아래의 명령어를 입력하면 설치가 가능합니다.


>> pip install konlpy


KoNLPy에는 Hannanum, Kkma, Komoran 등의 형태소 분석 패키지를 제공하는데 저는 Kkma(꼬꼬마)를 사용했습니다.

특별한 이유는 없습니다.. 꼬꼬마라는 네이밍이 귀여워서 선택했을 뿐..


kkma_token.py의 일부분

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import re
import pandas as pd
from konlpy.tag import Kkma
 
 
def get_noun(msg_txt):
    kkma = Kkma()
    nouns = list()
    # ㅋㅋ, ㅠㅠ, ㅎㅎ  
    pattern = re.compile("[ㄱ-ㅎㅏ-ㅣ]+")
    msg_txt = re.sub(pattern, "", msg_txt).strip()
 
    if len(msg_txt) > 0:
        pos = kkma.pos(msg_txt)
        for keyword, type in pos:
            # 고유명사 또는 보통명사
            if type == "NNG" or type == "NNP":
                nouns.append(keyword)
        print(msg_txt, "->", nouns)
 
    return nouns
cs


line 10 - 11
카카오톡 데이터로 분석하다보니 ㅋㅋ ㅎㅎ 등을 제거하는 로직을 추가하였습니다.
ㅋㅋ, ㅎㅎ, ㅜㅜ는 그래도 꼬꼬마에서 무난하게 이모티콘으로 해석해 줍니다.
하지만 "ㅁㄹ데ㅂ제더레ㅐ벚대ㅏ에"와 고양이가 타이핑한 게 들어가니까 성능이 매우 느려져서 제거하는 것을 추가 했습니다. 

line 14
물론 kkma.nouns() 라는 명사만 따로 추출하는 함수를 제공해주는데, 모든 조합 가능한 명사를 제공하는 건지 마음에 안들어서 kkma.pos()를 사용하여 문장에 속하는 것들의 품사를 분석한 뒤 그 중에 명사나 고유 명사에 해당하는 것만 추출했습니다.

결과물


카카오톡 특징상 맞춤법, 띄어쓰기가 전혀 안되있다보니 정확성은 떨어집니다.

서로간의 애칭이나 신조어가 들어가는 경우에 더욱 이상하게 분리를 하는데 이 경우에는 사전에 추가를 해주면 됩니다.



4. Kkma 사전에 키워드 추가

Anaconda 설치경로/Lib/site-packages/konlpy/java 경로로 이동합니다.

kkma-2.0.jar의 압축을 풀어 준 후 폴더로 이동합니다.

폴더 안에 dic 폴더에 사전들이 있습니다.


해당 사전에 마지막에 추가하고자 하는 내용을 넣어 줍니다.

명사만 저는 추가하였습니다.




이전 폴더로 이동해서 jar 파일로 다시 아카이브한 후 덮어쓰면 사전이 적용 됩니다.


카카오톡 대화 내용에 형태소 분석기를 적용하여 보았습니다.

다음 포스팅에서는 워드클라우드를 생성하는 것을 주제로 찾아 뵙겠습니다.


도움되셨다면 공감버튼 눌러주세요.

공감 버튼은 작성자에게 큰 동기부여가 됩니다.



,


1. PyMySQL

MySQL 데이터베이스와 연결하기 위한 오픈소스 라이브러리입니다. 해당 라이브러리를 설치하기 위해서 python -m pip install PyMySQL 명령어로 설치가능하지만, 만약 PyCharm 을 이용한다면. Setting에서 Project Interpreter 에서 아래 사진의 + 버튼을 누른뒤 PyMySQL 를 검색하여 설치할 수 있습니다.




설치가 완료가 되면 간단한 예제 코드로 Connection 테스트를 합니다.



1
2
3
4
5
6
7
8
9
10
11
12
import pymysql.cursors
 
connection = pymysql.connect(host='localhost', user='root', password='12', db='ssooni',
                             charset='utf8', autocommit=True)
 
cursor = connection.cursor()
sql = "select * from ssooni.board"
cursor.execute(sql)
 
result = cursor.fetchall()
connection.close()
print(result)
cs


- host  : MySQL가 설치되어 있는 원격지 주소를 넣어 줍니다.

- user / password :  계정명 / 비밀번호

- db : Default로 사용할 Database 이름을 넣어줍니다.

- charset : 인코딩 정보

- autocommit : Query 실행 후 자동으로 commit 명령어를 전송합니다. 

이 부분을 넣지 않으면, Insert나 Update를 실행해도 실제 DB에 반영되지 않습니다. 

실수로 데이터를 삭제 / 수정하는 일을 조금 막고자한다면, 이 옵션을 사용하지 않고, Query 실행 후 commit 할 시점에 connection.commit() 라인을 추가합니다.

  

Connection 객체로 DB 연결을 설정하고 Cursor로 데이터를 가지고 오는 Python에서 자주 볼 수 있는 형태로 데이터를 가지고 옵니다. 실행하면 아래와 같은 결과를 볼 수 있습니다.


실행 결과 

(1, 'ssooni', 'Welcome! Hi Je'), (13, 'aa', 'aaa'), (14, 'aa', 'aaaa'), (15, '?????', '?? ??????\n'))



2. pandas.DataFrame으로 변환

앞선 실행 결과는 Column 이 없어서 각각의 원소들이 어떤 데이터인지 알 수가 없습니다. pandas를 사용하여 Column 명을 할당하고, 데이터를 쉽게 분석하고 싶기도 합니다. 


보통 Python DB 라이브러리에서는 cursor.description 에 각각의 컬럼에 대한 정보를 따로 담습니다. 그래서 따로 함수를 짜서 cursor.description 안에서 컬럼명을 가지고 온 후 매핑을 하는 과정을 작성했습니다만, 최근에 나온 라이브러리는 그것마저 해줍니다.



1
2
3
4
5
6
7
8
9
10
11
12
import pymysql.cursors
 
connection = pymysql.connect(host='localhost', port=3306, user='root'
                             password='12', db='ssooni',
                             charset='utf8', autocommit=True, 
                             cursorclass=pymysql.cursors.DictCursor)
 
cursor = connection.cursor()
sql = "select * from ssooni.board"
cursor.execute(sql)
result = cursor.fetchall()
print(result)
cs


아까 코드와 달라진 점은 cursorclass에 DictCursor 를 추가했습니다. DB를 조회한 결과를 Column 명이 Key 인 Dictionary로 저장해 줍니다.


실행 결과

[{'bno': 1, 'userName': 'ssooni', 'contents': 'Welcome! Hi Je'}, {'bno': 13, 'userName': 'aa', 'contents': 'aaa'}, {'bno': 14, 'userName': 'aa', 'contents': 'aaaa'}, {'bno': 15, 'userName': '?????', 'contents': '?? ??????\n'}]

 

이전 결과와 달리 Column 명이 있어서 각각의 원소가 의미하는 것이 무엇인지 쉽게 이해 할 수 있습니다.

실행 결과를 Pandas DataFrame으로 바꾸는 것은 더욱 간단합니다. 


1
2
3
4
import pandas as pd
 
df = pd.DataFrame(result)
print(df)
cs


실행 결과

   bno        contents userName

0    1  Welcome! Hi Je   ssooni

1   13             aaa       aa

2   14            aaaa       aa

3   15     ?? ??????\n    ?????


이제 DataFrame으로 평균을 구하는 등 통계도 할 수 있고, 단순히 CSV 파일로 저장하여 엑셀을 정말 잘하는 팀원에게 노동을 시킬 수도 있습니다. 


도움이 되셨다면 공감 버튼 한 번만 눌러주세요. 

공감 버튼은 저에게 큰 동기부여가 됩니다.

,
Untitled

1. pandas 소개

데이터 분석할 때, 정말 효자 라이브러리입니다.

Python을 이용해서 데이터를 분석하는 프로젝트에서 유용하게 사용한 라이브러리입니다.

pandas는 DataFrame 이라는 자료형을 이용하여, 데이터를 저장하고 가공합니다.

In [1]:
import pandas as pd
import numpy as np

matrix = np.matrix([[1,3], [2,5]])
print("numpy.matrix : \n", matrix)

df = pd.DataFrame([[1,3], [2,5]])
print("pandas DataFrame :\n", df)
numpy.matrix : 
 [[1 3]
 [2 5]]
pandas DataFrame :
    0  1
0  1  3
1  2  5

DataFrame은 numpy matrix에서 Matrix 행의 속성 index 와 열의 속성 column 이 적용된 형태를 합니다.

데이터베이스 테이블이나 엑셀같은 표에 데이터를 저장하는 형태와 동일합니다.

numpy.matrix를 사용하면서 불편한 점은 행렬 내부의 원소를 접근할 때 반드시 index 단위로 접근해야 했습니다.

프로그램을 혼자 짤 때는 선택한 이 원소의 의미를 이해할 수 있지만, 함께하는 프로젝트에서는 코드를 이해하는데 어렵습니다.

(거기다 난도가 있는 통계적 알고리즘 구현까지 들어가면 통계베이스 없는 저같은 개발자는 일일히 실행해야 겨우 의미를 알게 되요)

하지만 pandas는 각각의 행과 열을 지정할 수 있어서, 선택하는 데이터가 어떤 속성을 가지는지 쉽게 이해가 가능합니다.

In [2]:
matrix = np.matrix([[1,3], [2,5]])
print("A의 수학 성적 : ", matrix[0,0])

df = pd.DataFrame([[1,3], [2,5]], columns=["math", "computer"], index=["A", "B"])
print("A의 수학 성적 : ", df.loc["A", "math"])
A의 수학 성적 :  1
A의 수학 성적 :  1

또한 DataFrame 클래스안의 메소드 만으로도 프로젝트에서 사용하는 합, 평균, 분산, 표준 편차 등의 단순 통계부터

데이터를 재구성하기 위한 이동 평균, 이동 합, Resample, Groupby와 시각화까지 지원합니다.

이번 시간에는 시각화 기능을 알아 보도록 해요.




2. 시각화

DataFrame의 plot을 이용하면 다양한 형태의 그래프를 그릴 수 있습니다.

numpy Matrix를 그래프로 그린다면 matplotlib 라이브러리를 사용해서 설정을 해야한다는 것에 비해면 엄청 간단한 방법으로 그래프를 그릴 수 있어요

DataFrame.plot.bar() : 막대 그래프

DataFrame.plot.line() : 선 그래프

DataFrame.plot.scatter(x, y) : 산포도 그래프

DataFrame.plot.box() : Box 그래프

앞서 사용한 A와 B의 성적표로 막대 그래프와 선 그래프를 그려 봅시다

In [3]:
""" %matplotlib inline : 주피터 노트북 설정 때문에 사용한 거에요, """ 
%matplotlib inline 

df.plot.bar()
df.plot.line()
df.plot.box()
Out[3]:
<matplotlib.axes._subplots.AxesSubplot at 0x1e284adb9b0>

데이터 수가 많으면 숫자로 보았을 때 데이터가 어떤 분포를 가지는지 쉽게 알 수 없는데, 간단하게 그래프를 그려서 확인할 수 있어요.

산포도 그래프는 sklearn 라이브러리에 있는 샘플 데이터로 한 번 그려볼게요

sklearn 라이브러리가 없다면 아나콘다 콘솔에 pip install sklearn 명령어로 설치하시고 진행해주세요

데이터 마이닝 수업에서 단골 손님으로 언급되는 Iris 데이터를 이용해서 산포도 그래프를 그려볼게요.

아래의 함수를 실행하여, 데이터를 적재합니다.

In [4]:
from sklearn.datasets import load_iris
iris = load_iris()
data = iris['data']
feature = iris['feature_names']
label = iris['target']

print("data의 타입 : ", type(data))

# numpy.ndarray데이터도 pandas DataFramed의 데이터로 적용이 가능합니다.
iris_df = pd.DataFrame(data, columns=feature)

# label 컬럼을 추가해줍니다.
iris_df.loc[:, "label"] = label
print(iris_df.head())
data의 타입 :  <class 'numpy.ndarray'>
   sepal length (cm)  sepal width (cm)  petal length (cm)  petal width (cm)  \
0                5.1               3.5                1.4               0.2   
1                4.9               3.0                1.4               0.2   
2                4.7               3.2                1.3               0.2   
3                4.6               3.1                1.5               0.2   
4                5.0               3.6                1.4               0.2   

   label  
0      0  
1      0  
2      0  
3      0  
4      0  

sklearn 라이브러리는 통계 알고리즘을 담고 있는 라이브러리인데요.

샘플로 데이터를 담고 있어서 데이터를 수집하는데 신경을 덜 쓸 수 있어요.

numpy.ndarraypandas.DataFrame 간의 호환성도 좋아서 여러분이 작성한 알고리즘의 결과가 numpy.ndarray 형태라 하여도,

pandas DataFrame으로 변환하여 사용할 수 있답니다.

이제 scatter 함수로 산포도를 그러볼게요. x축과 y축으로 사용할 컬럼명을 매개변수로 반드시 넣어주셔야 합니다.

In [5]:
iris_df.plot.scatter(x="sepal length (cm)", y="label")
Out[5]:
<matplotlib.axes._subplots.AxesSubplot at 0x1e2829fa5c0>

그려 놓고 보니까 각각이 무엇을 의미하는지 해석하기가 힘드네요. 서로 구분할 수 있도록 색을 넣어 줍시다.

label에 따라 색을 적용하기위해 color라는 컬럼을 새로 만들었습니다.

In [6]:
# iris 컬럼의 label은 0과 1, 2로 구성되어 있습니다. 
# label 중에 0, 1, 2 값을 같는 index를 찾아 줍니다.
zero = iris_df[iris_df["label"] == 0].index
one = iris_df[iris_df["label"] == 1].index
two = iris_df[iris_df["label"] == 2].index

# 그리고 각각의 index에 따라서 색깔 데이터를 넣어 줍니다.
iris_df.loc[zero, "color"] = "r"
iris_df.loc[one, "color"] = "g"
iris_df.loc[two, "color"] = "b"

iris_df.plot.scatter(x="sepal length (cm)", y="label", c=iris_df['color'])
Out[6]:
<matplotlib.axes._subplots.AxesSubplot at 0x1e286a6f828>

pandas.DataFrame으로 간단하게 데이터 가시화를 해보았습니다.

마지막으로 여러 종류의 그래프를 한 곳에 그리는 방법도 공유해드릴게요.

In [7]:
ax = iris_df["sepal length (cm)"].plot.line(color='green')
ax_2 = iris_df["petal length (cm)"].plot.bar(ax=ax, color='red')

# x축에 사용할 라벨
ax_2.set_xticks(iris_df.index[0::10])
ax_2.set_xticklabels(iris_df.index[0::10], rotation=45)

# y축에 사용할 라벨을 지정합니다.
ax_2.set_ylabel("length(cm)")

ax_2.legend()
Out[7]:
<matplotlib.legend.Legend at 0x1e284aa0f28>

plot 메소드들은 Axes라는 좌표축 객체를 반환합니다.

ax.set_xticks() : x축의 간격을 지정합니다.

ax.set_xticklabels() : x축의 간격에 라벨을 지정합니다.

ax.set_ylabel() : y축의 라벨을 지정합니다.

ax.legend() : 범례를 그래프에 넣어줍니다.

긴글 읽어 주셔서 감사합니다.

도움이 되셨으면 공감 버튼 꼭 눌러주세요.


,