본문 바로가기

딥러닝/딥러닝을 이용한 자연어처리 입문

[딥러닝 NLP] 11. CNN(Convolution Neural Network)

# 11-01 CNN for NLP

이전에 NVIDIA 딥러닝 기초 강좌를 수강하며 이미지 처리를 위한 CNN에 대해 공부했었다.

또한 CNN for sentence classification 이라는 논문을 리뷰하며 NLP를 위한 CNN도 공부했으므로 링크만 남기고 넘어간다.

https://222ys.tistory.com/10

 

텍스트분류를 위한 신경망 설계도만 한 번 더 짚고 넘어간다.

크기가 4인 커널 2개, 3인 커널 2개, 2인 커널 2개를 사용해 6차원 벡터 2개, 7차원 벡터 2개, 8차원 벡터 2개를 얻는다.

얻은 6개의 벡터에 대해 맥스 풀링을 해 6개의 스칼라 값을 얻고, 전부 연결(concatenate)해 하나의 벡터로 만든다.

이렇게 얻은 벡터를 뉴런이 2개인 출력층에 완전 연결(Dense layer)하여 텍스트 분류를 수행한다.

 

구현 코드는 다음과 같다.

from tensorflow.keras.layers import Conv1D, GlobalMaxPooling1D

model = Sequential()
model.add(Conv1D(num_filters, kernel_size, padding='valid', activation='relu'))
# 커널 개수, 커널 크기, 패딩 방법(valid: 패딩없음/same: 입출력이 동일한 차원 갖도록 제로패딩), 활성화 함수
model.add(GlobalMaxPooling1D()) # 맥스풀링

 

# 11-02 CNN 실습

1. 1D CNN 실습

(1) IMDB 리뷰 분류

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, Dropout, Conv1D, GlobalMaxPooling1D, Dense
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
from tensorflow.keras.models import load_model

embedding_dim = 256 # 임베딩벡터 차원
dropout_ratio = 0.3 # 드롭아웃 비율
num_filters = 256 # 커널의 수
kernel_size = 3 # 커널의 크기
hidden_units = 128 # 뉴런의 수

model = Sequential()
model.add(Embedding(vocab_size, embedding_dim))
model.add(Dropout(dropout_ratio))
model.add(Conv1D(num_filters, kernel_size, padding='valid', activation='relu')) # 합성곱층
model.add(GlobalMaxPooling1D()) # 풀링층
model.add(Dense(hidden_units, activation='relu'))
model.add(Dropout(dropout_ratio))
model.add(Dense(1, activation='sigmoid')) # 이진분류

es = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=3)
mc = ModelCheckpoint('best_model.h5', monitor='val_acc', mode='max', verbose=1, save_best_only=True)

model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(X_train, y_train, epochs=20, validation_data=(X_test, y_test), callbacks=[es, mc])
loaded_model = load_model('best_model.h5')
print("\n 테스트 정확도: %.4f" % (loaded_model.evaluate(X_test, y_test)[1]))
# 0.8873

 

(2) 스팸메일 분류

from tensorflow.keras.layers import Dense, Conv1D, GlobalMaxPooling1D, Embedding, Dropout, MaxPooling1D
from tensorflow.keras.models import Sequential
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint

embedding_dim = 32
dropout_ratio = 0.3
num_filters = 32
kernel_size = 5

model = Sequential()
model.add(Embedding(vocab_size, embedding_dim))
model.add(Dropout(dropout_ratio))
model.add(Conv1D(num_filters, kernel_size, padding='valid', activation='relu')) # 합성곱층
model.add(GlobalMaxPooling1D()) # 풀링층
model.add(Dropout(dropout_ratio))
model.add(Dense(1, activation='sigmoid')) # 이진분류
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])

es = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=3)
mc = ModelCheckpoint('best_model.h5', monitor = 'val_acc', mode='max', verbose=1, save_best_only=True)

history = model.fit(X_train_padded, y_train, epochs=10, batch_size=64, validation_split=0.2, callbacks=[es, mc])
X_test_encoded = tokenizer.texts_to_sequences(X_test)
X_test_padded = pad_sequences(X_test_encoded, maxlen = max_len)
print("\n 테스트 정확도: %.4f" % (model.evaluate(X_test_padded, y_test)[1]))
# 0.9797

 

2. Multi-Kernel 1D CNN 실습

네이버 영화리뷰 분류

from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Embedding, Dropout, Conv1D, GlobalMaxPooling1D, Dense, Input, Flatten, Concatenate
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
from tensorflow.keras.models import load_model

# 입력층과 임베딩층 정의
embedding_dim = 128
dropout_ratio = (0.5, 0.8)
num_filters = 128
hidden_units = 128

# 드롭아웃 정의
model_input = Input(shape = (max_len,))
z = Embedding(vocab_size, embedding_dim, input_length = max_len, name="embedding")(model_input)
z = Dropout(dropout_ratio[0])(z)

# 커널 정의 및 맥스풀링 추가
conv_blocks = []

for sz in [3, 4, 5]:
    conv = Conv1D(filters = num_filters,
                         kernel_size = sz,
                         padding = "valid",
                         activation = "relu",
                         strides = 1)(z)
    conv = GlobalMaxPooling1D()(conv)
    conv_blocks.append(conv)

# 맥스풀링 결과 연결 및 전달
z = Concatenate()(conv_blocks) if len(conv_blocks) > 1 else conv_blocks[0] # 연결
z = Dropout(dropout_ratio[1])(z)
z = Dense(hidden_units, activation="relu")(z)
model_output = Dense(1, activation="sigmoid")(z) # 이진분류

model = Model(model_input, model_output)
model.compile(loss="binary_crossentropy", optimizer="adam", metrics=["acc"])

es = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=4)
mc = ModelCheckpoint('CNN_model.h5', monitor='val_acc', mode='max', verbose=1, save_best_only=True)

model.fit(X_train, y_train, batch_size=64, epochs=10, validation_split=0.2, verbose=2, callbacks=[es, mc])
loaded_model = load_model('CNN_model.h5')
print("\n 테스트 정확도: %.4f" % (loaded_model.evaluate(X_test, y_test)[1]))
# 0.8430

 

실제로 리뷰를 예측해보는 것은 앞의 RNN 실습코드와 같으므로 생략한다.

 

3. 사전훈련된 워드임베딩 실습

의도분류(Intent Classification)를 수행해본다.

깃허브 의도데이터 전처리

import os
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import urllib.request
from sklearn import preprocessing
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical
from sklearn.metrics import classification_report

urllib.request.urlretrieve("https://raw.githubusercontent.com/ukairia777/tensorflow-nlp-tutorial/main/11.%201D%20CNN%20Text%20Classification/dataset/intent_train_data.csv", filename="intent_train_data.csv")
urllib.request.urlretrieve("https://raw.githubusercontent.com/ukairia777/tensorflow-nlp-tutorial/main/11.%201D%20CNN%20Text%20Classification/dataset/intent_test_data.csv", filename="intent_test_data.csv")

train_data = pd.read_csv('intent_train_data.csv')
test_data = pd.read_csv('intent_test_data.csv')

intent_train = train_data['intent'].tolist()
label_train = train_data['label'].tolist()
intent_test = test_data['intent'].tolist()
label_test = test_data['label'].tolist()

# 레이블 인코딩. 레이블에 고유한 정수를 부여
idx_encode = preprocessing.LabelEncoder()
idx_encode.fit(label_train)

label_train = idx_encode.transform(label_train) # 주어진 고유한 정수로 변환
label_test = idx_encode.transform(label_test) # 고유한 정수로 변환
label_idx = dict(zip(list(idx_encode.classes_), idx_encode.transform(list(idx_encode.classes_))))

# 토큰화, 정수 인코딩
tokenizer = Tokenizer()
tokenizer.fit_on_texts(intent_train)
sequences = tokenizer.texts_to_sequences(intent_train)
word_index = tokenizer.word_index # 단어집합
vocab_size = len(word_index) + 1 # 단어집합 크기

# 패딩을 위한 확인
print('문장의 최대 길이 :',max(len(l) for l in sequences)) # 35
print('문장의 평균 길이 :',sum(map(len, sequences))/len(sequences)) # 9.364392396469789
max_len = 35

# 패딩, 원핫인코딩
intent_train = pad_sequences(sequences, maxlen = max_len)
label_train = to_categorical(np.asarray(label_train))

# 훈련데이터 섞기
indices = np.arange(intent_train.shape[0])
np.random.shuffle(indices)
intent_train = intent_train[indices]
label_train = label_train[indices]

# 훈련데이터의 10%를 검증데이터로 분리
n_of_val = int(0.1 * intent_train.shape[0])
X_train = intent_train[:-n_of_val]
y_train = label_train[:-n_of_val]
X_val = intent_train[-n_of_val:]
y_val = label_train[-n_of_val:]
X_test = intent_test
y_test = label_test

 

사전훈련된 Glove 사용

!wget http://nlp.stanford.edu/data/glove.6B.zip
!unzip glove*.zip

embedding_dict = dict()
f = open(os.path.join('glove.6B.100d.txt'), encoding='utf-8')
for line in f:
    word_vector = line.split()
    word = word_vector[0]
    word_vector_arr = np.asarray(word_vector[1:], dtype='float32') # 100개의 값을 가지는 array로 변환
    embedding_dict[word] = word_vector_arr
f.close()

embedding_dim = 100
embedding_matrix = np.zeros((vocab_size, embedding_dim))
print('임베딩 테이블의 크기(shape) :',np.shape(embedding_matrix))

for word, i in word_index.items():
    embedding_vector = embedding_dict.get(word)
    if embedding_vector is not None:
        embedding_matrix[i] = embedding_vector

 

의도분류 수행

from tensorflow.keras.models import Model
from tensorflow.keras.layers import Embedding, Dropout, Conv1D, GlobalMaxPooling1D, Dense, Input, Flatten, Concatenate

kernel_sizes = [2, 3, 5]
num_filters = 512
dropout_ratio = 0.5

model_input = Input(shape=(max_len,))
output = Embedding(vocab_size, embedding_dim, weights=[embedding_matrix],
                      input_length=max_len, trainable=False)(model_input)

conv_blocks = []

for size in kernel_sizes:
    conv = Conv1D(filters=num_filters,
                         kernel_size=size,
                         padding="valid",
                         activation="relu",
                         strides=1)(output)
    conv = GlobalMaxPooling1D()(conv)
    conv_blocks.append(conv)

output = Concatenate()(conv_blocks) if len(conv_blocks) > 1 else conv_blocks[0]
output = Dropout(dropout_ratio)(output)
model_output = Dense(len(label_idx), activation='softmax')(output)
model = Model(model_input, model_output)

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])
model.summary()

 

검증데이터로 모델 평가

history = model.fit(X_train, y_train,
          batch_size=64,
          epochs=10,
          validation_data=(X_val, y_val))

 

accuracy, loss 시각화

epochs = range(1, len(history.history['acc']) + 1)
plt.plot(epochs, history.history['acc'])
plt.plot(epochs, history.history['val_acc'])
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epochs')
plt.legend(['train', 'test'], loc='lower right')
plt.show()

epochs = range(1, len(history.history['loss']) + 1)
plt.plot(epochs, history.history['loss'])
plt.plot(epochs, history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epochs')
plt.legend(['train', 'test'], loc='upper right')
plt.show()

테스트 데이터에 대해 모델 평가

X_test = tokenizer.texts_to_sequences(X_test)
X_test = pad_sequences(X_test, maxlen=max_len)
y_predicted = model.predict(X_test)
y_predicted = y_predicted.argmax(axis=-1) # 예측을 정수 시퀀스로 변환
print('정확도(Accuracy) : ', sum(y_predicted == y_test) / len(y_test)) # 0.99

 

# 11-03 문자 임베딩: CNN, RNN 비교

문자임베딩(Character Embeding): 'understand' 앞에 'mis-'를 붙이면 오해하다'라는 뜻이 된다고 유추하는 방식

 

1. 1D CNN으로 문자임베딩

단어를 문자 단위로 쪼개고 나서 입력으로 사용하는 것 외에는 CNN 작동구조는 똑같다.

Word2Vec이나 Glove가 해결하지 못하는 OOV 문제에 대응 가능

'have'를 h,a,v,e로 분리해 문자 단위 임베딩

 

 

2. BiLSTM으로 문자임베딩

똑같이 단어를 문자 단위로 분리해 순방향 LSTM, 역방향 LSTM을 수행한 값을 연결해 사용