티스토리 뷰

이번엔 multi classification을 해볼 것이다.

내가 가진 것은 페이스북 게시글로, 약 4000개 정도 된다.

supervised learning을 진행하려고 labeling을 쭉 하는데 너무 많아서 2000개만 하고 일단 돌려봤다.

 

labeling을 하는데, 내가 분류를 해도 헷갈리는 것이나 여러 항목에 걸쳐 있는 글들이 많았다.

경계도 모호하고 분류도 완벽하지 않다.

아마 학습시킬 때도 문제가 될 것 같다.

분류를 통합하거나 태그 형식으로 바꾸거나.

마치 도서관에서 일할 때 도서 분류를 하는 느낌이다.

 

개요

글을 분류하는 데에는 여러가지 방법이 있더라.

검색해보니 Doc2Vec, fasttext, Word2Vec(문장은 단어 vector의 평균으로), LSTM 또는 RNN, CNN 등이 있었다.

그 중 몇 개만 아래 접기에 넣어뒀다.

...더보기

뭘 해볼까 하다가 Word2Vec은 너무 많이 썼으니 LSTM을 써보기로 했다.

 

LSTM은 RNN이 역전파 과정에서 손실되는 gradient의 문제점을 고친 알고리즘이다.

수학적으로는 잘 모르겠다...

이 블로그에 너무 잘 나와있다.

 

본격적으로 순서를 살펴보면,

0. 데이터 추출

1. 자연어처리

2. LSTM

 LSTM은 keras를 이용했다.

pip install keras로 설치하면 된다.

 

0. 데이터 추출

내가 가진 페이스북(문서) 데이터는 2개로 구성되어 있다.

글 내용과 분류.

간단하다.

 

글 내용은 "헬스장에 블루투스 이어폰 두고가신분 찾아가세요!", "1시에 터미널로 같이 택시 타실 분 있나요?", "[공지] 선형대수학 다음 주 수업은 공강입니다." 등의 내용이다.

나는 8가지 분류로 나눴는데, 0: 분류 불가(내용이 없을 때), 1: 택시 동승, 2: 분실 ... 7: 단순 질문 등이다.

2000개까지 내가 직접 분류했다.

 

현재 data는 2000개의 글이 있고 각각 내용과 분류, 2가지 정보가 있는 것이다.

 

1. 자연어처리

글과 매우 비슷하다.

data의 첫 번째 컬럼(내용)을 KoNLPy의 Mecab으로 tokenize 한다.

결과는 ['헬스','장','에','블루투스', ... ]와 같이 된다.

from konlpy.tag import Mecab
def tokenize_sentense(text):
    mecab=Mecab()
    return mecab.morphs(text)

이제 각 단어를 번호에 매핑해야 한다.

['헬스','장','에','블루투스', ... ] -> [1,2,3,4, ...] 형태로 만드는 것이다.

num_list, w2n_dic, n2w_dic = word2num(tokenized_data)

#list_2d는 2차원 list이다. 2000개의 글과 각 글이 tokenized 된 단어들이다.
def word2num(list_2d):
    w2n_dic = dict()  # word가 key이고 index가 value인 dict
    n2w_dic = dict()  # index가 key이고 word가 value인 dict. 나중에 번호에서 단어로 쉽게 바꾸기 위해.
    idx = 1
    num_list = [[] for _ in range(len(list_2d))]   # 숫자에 매핑된 글의 리스트
    for k,i in enumerate(list_2d):
        if not i:
            continue
        elif isinstance(i, str): 
             # 내용이 단어 하나로 이루어진 경우, for loop으로 ['단어']가 '단'과 '어'로 나뉘지 않게 한다.
            if w2n_dic.get(i) is None:
                w2n_dic[i] = idx
                n2w_dic[idx] = i
                idx += 1
            num_list[k] = [dic[i]]
        else:
            for j in i:
                if w2n_dic.get(j) is None:
                    w2n_dic[j] = idx
                    n2w_dic[idx] = j
                    idx += 1
                num_list[k].append(w2n_dic[j])
    return num_list, w2n_dic, n2w_dic

편한 라이브러리가 있겠지만, 그냥 내가 구현했다.

(편한 라이브러리 아시는 분 알려주세요 ㅠ)

 

2.LSTM

이제 LSTM에 쓸 training data와 test data로 나눈다.

비율은 8:2다.

(이것도 제가 구현해서 썼어요...)

import np

def divide_data(x, y, train_prop=0.8):
    x = np.array(x)
    y = np.array(y)
    tmp = np.random.permutation(np.arange(len(x)))
    x_tr = x[tmp][:round(train_prop * len(x))]
    y_tr = y[tmp][:round(train_prop * len(x))]
    x_te = x[tmp][-(len(x)-round(train_prop * len(x))):]
    y_te = y[tmp][-(len(x)-round(train_prop * len(x))):]
    return x_tr, x_te, y_tr, y_te

x는 위에서 번호로 매핑한 단어들의 list가 되겠고(num_list) y는 사전에 분류한 데이터가 되겠다.

 

LSTM에 쓸 input은 크기가 일정해야 하고, output은 one-hot 벡터여야 한다.

그래서 조금 손을 봐줘야 한다.

from keras.preprocessing import sequence
from keras.utils import np_utils

x_tr = sequence.pad_sequences(x_tr, maxlen=50) # 50개 넘으면 자르고 안되면 0으로 채움
x_te = sequence.pad_sequences(x_te, maxlen=50)
y_tr = np_utils.to_categorical(y_tr) # one_hot으로 변형
y_te = np_utils.to_categorical(y_te) # 3 -> [0,0,0,1,0, ..]

이건 keras에서 함수를 지원해준다.

너무 감사합니다.

 

아, maxlen을 50으로 만든 이유는, 데이터의 평균 단어(token) 수가 79개였는데 편차도 크고 짧은 건 20개 수준이어서 50개가 적당해보였다.

자신의 상황에 맞는 수를 정하면 될 것 같다.

 

이제 LSTM 모델을 구현해보자.

keras를 사용했다.

(그게 예제로 있었어서... 파이토치나 텐서플로우도 잘 나온 게 있었다면 그걸로 했을 듯)

 

from keras.models import Sequential
from keras.layers import Dense, LSTM, Embedding

# x_tr,x_te,y_tr,y_te
# words_num은 총 단어의 종류. +1을 해준 이유는 단어 수가 적은 글의 경우 빈 칸에 0이 있기 때문에.
model = Sequential()
model.add(Embedding(words_num+1, len(x_tr[0])))  # 사용된 단어 수 & input 하나 당 size
model.add(LSTM(len(x_tr[0])))
model.add(Dense(len(y_tr[0]), activation='softmax'))  # 카테고리 수

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
# 어... 그냥 따라했음...
history = model.fit(x_tr, y_tr, batch_size=100, epochs=20, validation_data=(x_te,y_te))
# epoch 수를 적당히 조절하길. 너무 많이 하면 끝이 안남.

# history에서 출력되는 내용
Epoch 14/20
 100/1673 [>.............................] - ETA: 1s - loss: 0.4775 - acc: 0.820
 200/1673 [==>...........................] - ETA: 1s - loss: 0.4224 - acc: 0.840
 300/1673 [====>.........................] - ETA: 1s - loss: 0.4116 - acc: 0.840
 400/1673 [======>.......................] - ETA: 1s - loss: 0.3728 - acc: 0.860
 500/1673 [=======>......................] - ETA: 1s - loss: 0.3712 - acc: 0.856
 600/1673 [=========>....................] - ETA: 1s - loss: 0.3919 - acc: 0.845
 700/1673 [===========>..................] - ETA: 1s - loss: 0.3990 - acc: 0.841
 800/1673 [=============>................] - ETA: 1s - loss: 0.3951 - acc: 0.841
 900/1673 [===============>..............] - ETA: 0s - loss: 0.4028 - acc: 0.838
 1000/1673 [================>.............] - ETA: 0s - loss: 0.4061 - acc: 0.836
 1100/1673 [==================>...........] - ETA: 0s - loss: 0.4063 - acc: 0.834
 1200/1673 [====================>.........] - ETA: 0s - loss: 0.3967 - acc: 0.838
 1300/1673 [======================>.......] - ETA: 0s - loss: 0.3886 - acc: 0.843
 1400/1673 [========================>.....] - ETA: 0s - loss: 0.3866 - acc: 0.843
 1500/1673 [=========================>....] - ETA: 0s - loss: 0.3817 - acc: 0.845
 1600/1673 [===========================>..] - ETA: 0s - loss: 0.3740 - acc: 0.850
 1673/1673 [==============================] - 2s 1ms/step - loss: 0.3748 - acc: 0.8500
 - val_loss: 0.6193 - val_acc: 0.7895

epoch마다 val_acc를 출력해준다.

20 epoch가 지났을 때 정확도는 82.30%였다.

생각보다 낮았다.

 

데이터를 살펴보니 내가 분류할 때도 애매했던 것들은 제대로 학습이 안 됐다.

상대적으로 경계가 명확한 분류는 잘 됐다.

 

다음은 카테고리 별 정확성이다.

즉, 결과를 binary( 1을 1로 예측했는가 & 1이 아닌 것을 1이 아닌 것으로 예측했는가)로 만들었을 때 정확도이다.

분류 0 1 2 3 4 5 6 7
정확도 100% 99.28% 94.26% 94.50% 96.41% 92.82% 91.87% 95.45%

무슨 의미가 있겠냐마는...

1의 경우, 내가 확실하게 분류할 수 있는 카테고리였다.

역시나 인공지능도 잘 분류했다.

내가 가장 헷갈렸던 것이 5,6이었다. 둘은 경계가 모호했다.

내가 그래서 얘도 헷갈렸나보다.

 

그래서 5,6을 통합하고, 3,7을 통합했더니 정확도가 87.08%로 올라갔다.

그래도 90%이 안되긴 하지만 카테고리가 여러 개인 것을 감안하면.... ㅎ...

 

끝!

다음엔 분류 방식을 조금 바꿔봐야겠다.

댓글