text/Python

문장 생성 해보기 (feat. 네이버 기사 댓글)

hoonzii 2021. 4. 2. 16:36
반응형

이전 글을 통해 가져온 데이터를 이용해보자

가져온 데이터를 이용해 문장을 생성할 것이다.

 

1. 네이버 영화평 가져오기

 

네이버 영화평 가져오기

네이버 영화평 가져오기 설명 들어가기 전 네이버의 robots.txt 에 대해 먼저 숙지하자. 사용 언어 및 모듈     - python 3.7     - request = request 요청을 보내 html 값을 가져오기  ..

hoonzi-text.tistory.com

2. 네이버 기사 댓글 가져오기

 

네이버 기사 댓글 가져오기

네이버 기사 댓글 가져오기 들어가기 전 네이버의 robots.txt 에 대해 먼저 숙지하자. 사용 언어 및 모듈     - python 3.7     - request = request 요청을 보내 html 값을 가져오기     ..

hoonzi-text.tistory.com

 

우리는 GRU를 사용해 만들어 볼건데, 그래픽 카드가 없으니 colab을 이용할 것이다.

colab 사용법

https://velog.io/@s6820w/colab1

 

Google Colaboratory 사용법

Google Colab 사용법

velog.io

또한 대부분의 코드는 아래 링크의 코드를 이용했다.

wikidocs.net/45101

 

위키독스

온라인 책을 제작 공유하는 플랫폼 서비스

wikidocs.net

 

 

기본적인 rnn 과 lstm에 대한 설명은 해당 링크에 더욱 자세히 설명되어 있으니 들어가서 보는게 이 글을 읽는 것보다 나을 것이다. 

 

colab의 경우 구글이 만들어놓은 가상의 작업공간이기 때문에 

지난 링크에서 생성한 pandas DataFrame pickle 파일을 구글 드라이브에 올린뒤 파일을 읽어올 수 있다.

옆에 링크로 들어가야됌

 

링크를 들어가면 드라이브를 연결할 아이디 선택창이 나오고, 아이디를 선택하면 특정 코드 값이 나오게 된다.

코드값 복사해야됌

 

받아온 코드값으로 원래 작업창으로 돌아와 넣고 enter

이제 내 드라이브에 접근할수 있다.

이제 데이터를 읽어보자.

파일 경로는 각자 자신의 드라이브 폴더에 맞게 변경해야됌

잘 읽어온걸 확인할 수 있다.

우선 모델의 input 값으로 넣기 위해서 문장을 벡터로 나타낼 수 있어야한다. (Embedding 이라고 한다.)

 

우선 문장을 나눌 최소 단위인 token을 결정해야 한다.

음절 단위(한글자씩), 어절 단위(띄어쓰기 단위), 형태소 단위(형태소 분석이 필요함) 등으로 나뉠 수 있다.

 

각자에 대해서 조금더 써보자면

음절 단위로 할 경우

전체 token set의 경우 최대 한글 자모 조합 경우수 11,172자로 나올수 있다.

(영어 알파벳, 특수문자 등으로 인해 추가 될수 있지만...)

중요한 점은 어떤 한국어 단어가 오더라도 음절단위로 나눌수 있다는 점이다. (기억해야됌)

 

어절 단위의 경우

문장 생성시 음절 단위보다 다음에 나올 token 예측이 좋게 나올수 있다. (꼭 그런건 아니다.)

하지만 같은 단어라도 조사에 따라 다르기 때문에 (ex. 책이, 책을, 책에 등) 전체 token set이 어마어마하게 늘어난다.

게다가 새로운 데이터를 넣을때, 기존 token set에 없는 새로운 token이 생길 경우 해당 token은 추가하거나, 버리거나를 선택해야한다. (Out Of Vocabulary. OOV문제라고 한다.)

 

형태소 단위의 경우

문장 생성시 음절, 어절 단위보다 결과가 좋을 수 있다. (꼭 그런건 아니다.)

어절 단위의 문제점을 해결할 수 있다. (ex. 책+이, 책+을, 책+에 등)

같은 명사와 여러 조사로 나눠 각 단어 뒤에 무엇이 나올지에 대한 결과를 개선할 수 있고,

(ex.명사 다음엔 조사가 나온다)

어절 단위보다 token set 크기가 작아진다.

그러나 어절 단위와 마찬가지로 OOV 문제가 있다.

 

오늘 진행하는 글에선 음절 단위 token으로 만들려고 한다.

(진행시 어절 단위는 token set이 너무 커져 구글 colab에서 감당하지 못하더라....)

먼저 음절하나마다 정수 값을 부여해준다. 그럼 이런식으로 표현이 가능하다.

 

나는 밥을 먹는다

-> {나 : 1, 는 : 2, 밥 : 3, 을 : 4, 먹 : 5, 다 : 6, " " : 7} (token set 만들고)

-> [1, 2, 7, 3, 4, 7, 5, 2, 6] (token set을 이용해 벡터를 구성)

 

문장이 하나의 벡터로 표현이 됐다.

순서는 이러하다.

1. token set을 구성하고

2. token set을 통해 벡터 구성하기

 

코드로 구현해보자

vocab = set()
for i, row in tqdm(df.iterrows()): # dataframe의 row별로 반복
    comment = row['comment'] # 댓글 가져오기
    for token in comment: #음절 단위 token 반복
        if token not in vocab:
            vocab.add(token) # token set에 없다면 추가
        else:
            continue
vocab.add('<EOS>') # 문장 마지막을  <EOS>로 끝낼거라 <End of Sequence> token 추가
vocab = sorted(vocab) # 해도 안해도 그만
print ('고유 문자수 {}개'.format(len(vocab)))

결과를 보자

총 137,842개의 댓글로 만든 token set에 들어있는 token의 개수가 3535개이다.

해당 token을 숫자(index)로 나타내보자

# 고유 문자에서 인덱스로 매핑 생성
char2idx = {u:i for i, u in enumerate(vocab, start=1)} # padding 값으로 0을 줄것이기 때문에 start= 1

결과를 보자

특수문자 역시 index가 잘 들어가 있다.

이 token set을 이용해 문장-> 벡터로 나타내야 한다.

코드로 구현해보자.

text_as_int_list = []
for i, row in tqdm(df.iterrows()):
    temp_arr = np.array([char2idx[c] for c in row['comment']] + [char2idx['<EOS>']])
    text_as_int_list.append(temp_arr)

잘됐나 봅시다.

첫번째 문장의 벡터
벡터를 다시 복원한 모습. 잘복원 된걸 볼수있다.

 

우리는 문장 생성을 만들어야 하기 때문에 단어 순서를 학습 시켜야 한다.

"나는 밥을 먹는다" 문장의 경우 "나" 다음은 "는", "나는 밥" 다음은 "을" 이라는 단어를 정답으로 내게끔 만들어야 한다.

위에서 만든 문장 벡터를 해당 방식으로 바꿔보자.

import numpy as np
# model input으로 넣기 위해 vector list -> numpy 배열로 변경
text_as_int = np.array(text_as_int_list, dtype=object)

sequence_list = []
for comment in text_as_int:
    for i in range(len(comment)):
        sequence = comment[:i+1]
        sequence_list.append(sequence)

위 형식으로 문장 벡터를 변형 시켰다

이제 "나는 밥" 이라는 데이터가 문제가 되고,  "을" 이라는 데이터가 정답이 되게끔 변형 시켜보자

 

순서는 이렇다.

1. 모델에 넣기전 일정한 크기의 벡터로 만들어준다.

2. 벡터별로 마지막 열을 분리해 [문제]+[정답] 으로 분리한다.

 

먼제 1번 일정한 크기로 만들기 위해서 padding 작업을 한다.

모델에 넣기 위해서는 각 벡터를 같은 크기로 만들어 줘야한다. 위에서 만든 벡터는 크기가 1~ 댓글 문장 최대 크기 까지 일정하지 않기 때문에 균일하게 맞춰줘야한다.

그렇다면 크기의 기준은 어떻게 맞춰야 할까?

plt.hist([len(s) for s in text_as_int_list], bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()

 

댓글 문자 길이를 표로 나타낸 모습

보고 적당한 길이로 정해준다.

나의 경우엔 길이 100 정도로 만들어줬다.

(길이 200 이상으로 만들면 코랩 램 크래쉬,,,,)

from tensorflow.keras.preprocessing.sequence import pad_sequences

max_len = 100
sequences = pad_sequences(sequence_list, maxlen=max_len, padding='pre')
# maxlen = 맞출 최대 길이
# padding = 'pre' 앞에서 부터 0값을 채워넣겠다는 의미

앞부분이 0으로 채워진 일정크기(길이 100) 벡터가 완성되었다.

 

2번 벡터 -> 문제 + 정답의 벡터들도 만들어준다

위 예시로는 [0,0,0,...]  /  [951] 만들수 있다.

np.random.shuffle(sequences)
# sequence 벡터를 섞어줌
split_num = int(len(sequences) * 0.9)
# train / text = 9:1로 나누기 위한 분리 지점 정하기

X = sequences[:split_num,:-1]
y = sequences[:split_num,-1]
# 리스트의 마지막 값을 제외하고 저장한 것은 X
# 리스트의 마지막 값만 저장한 것은 y. 이는 레이블(정답)에 해당됨.

# test 데이터를 통해 모델을 평가 하기 위함
X_text = sequences[split_num:,:-1]
y_test = sequences[split_num:,-1]

 

준비는 다됐다. 이제 모델을 돌려보자

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, Dense, GRU
from keras.callbacks import EarlyStopping

early_stopping = EarlyStopping() 
# 조기종료 콜백함수 정의
# epoch를 길게 돌리지 않고, 이전 epoch보다 accuracy가 낮을 경우 종료

model = Sequential()
model.add(Embedding(vocab_size, 300,input_length=max_len-1,mask_zero=True)) 
# 레이블을 분리하였으므로 이제 X의 길이는 max_len-1
model.add(GRU(128))
model.add(Dense(vocab_size, activation='softmax'))
model.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
# sparse_categorical_crossentropy
# categorical_crossentropy와 다른점은 정답 integer값을 원-핫벡터로 만들어주지 않아도 된다는점

model.summary()

hist = model.fit(X, y,validation_split=0.1,shuffle=True, epochs=100, batch_size= 2048,verbose=1, callbacks=[early_stopping])

모델 학습 과정

 

model.fit의 결과물에는 모델 학습 과정이 어땠는지 중간중간의 accuracy 값과 loss값을 저장 되어있다.

위 코드를 보면 hist라는 변수에 해당 값을 저장하는 것을 볼 수 있다.

표로 살펴보자

%matplotlib inline
import matplotlib.pyplot as plt

fig, loss_ax = plt.subplots()

acc_ax = loss_ax.twinx()

loss_ax.plot(hist.history['loss'], 'y', label='train loss')
loss_ax.plot(hist.history['val_loss'], 'r', label='val loss')

acc_ax.plot(hist.history['accuracy'], 'b', label='train acc')
acc_ax.plot(hist.history['val_accuracy'], 'g', label='val acc')

loss_ax.set_xlabel('epoch')
loss_ax.set_ylabel('loss')
acc_ax.set_ylabel('accuray')

loss_ax.legend(loc='upper left')
acc_ax.legend(loc='lower left')

plt.show()

학습은 이쁘게 잘 된것 같다. 하지만 정확도가 아주 처참하다.

앞서 따로 분리해놓은 test 데이터에 대해 accuracy를 살펴보자

 

loss_and_metrics = model.evaluate(X_text, y_test, batch_size=32)

print('')
print('loss : ' + str(loss_and_metrics[0]))
print('accuray : ' + str(loss_and_metrics[1]))

accuracy : 39%... 나름....

 

정확도가 50%보다 낮다. 하하

이제 샘플로 문장을 생성해보자

current_word = '아'
init_word = current_word # 처음 들어온 단어도 마지막에 같이 출력하기위해 저장
sentence = ''
n = 50
for _ in range(n): # n번 반복
    encoded = [char2idx[token] for token in current_word]#t.texts_to_sequences([current_word])[0] 
    # 현재 단어에 대한 정수 인코딩
    
    encoded = pad_sequences([encoded], maxlen=max_len, padding='pre') 
    # 데이터에 대한 패딩
    
    result = np.argmax(model.predict(encoded), axis=-1) 
    #model.predict_classes(encoded, verbose=0)
	
    # 입력한 X(현재 단어)에 대해서 Y를 예측하고 Y(예측한 단어)를 result에 저장.
    for word, index in char2idx.items(): 
        if index == result: # 만약 예측한 단어와 인덱스와 동일한 단어가 있다면
            break # 해당 단어가 예측 단어이므로 break
    current_word = current_word + word # 현재 단어 + ' ' + 예측 단어를 현재 단어로 변경
    sentence = sentence + word # 예측 단어를 문장에 저장
    if word == '<EOS>':
        break;
# for문이므로 이 행동을 다시 반복
sentence = init_word + sentence
sentence.replace('<EOS>',"")

'아' 라는 음절을 입력했을때 어떤 결과가 나오는지 보자

아직도 정신을 못차리고 있는듯 하다.

 

그럼 댓글 데이터에서 일부분을 자른뒤, 나머지 문장을 생성하게 해보자

org_word는 원래 댓글

current_word는 일부분 자른 댓글

그 밑 문장을 모델 생성 문장이다.

39% 성능임을 확인했다.

 

아쉬운점

- 파라미터가 되는 부분들(embeding size, batch size, hidden layer size 등)을 조절해주면서 최적의 모델을 만들 수 있어야 진정으로 이해하고 썼다 말할 수 있을텐데 그렇지 못했다.

 

- 데이터 수(약 13만개) 이지만 모델 학습하기엔 그다지 큰 데이터가 아니므로 좀더 큰 데이터로 학습하면 결과가 나아질 것 같다.

 

댓글 환영

반응형