[AI 이론] 딥러닝 - 자연어(Text) 처리 이론과 실습 (Ft.RNN)
- AI & Data/이론
- 2022. 11. 17.
딥러닝 활용
딥러닝을 활용한 분야는 크게 3가지로 나뉩니다.
이번에는 자연어 중에서 어떻게 Text를 처리하는지 알아보겠습니다.
- 이미지 분야: 얼굴인식, 화질 개선, 물체 인식 및 태깅 등
- 음성 분야: AI 스피커, 노래 인식 등
- ✅Text 분야: 리뷰 분석, 챗봇 등
참고로 자연어 처리 분야를 NLP(Natural Language Processing)이라고 부릅니다.
자연어 전 처리
자연어를 처리 과정은 3가지 단계가 있습니다.
- 자연어 전 처리 (Preprocessing)
- 단어 표현 (Word Embedding)
- 모델 적용하기 (Modeling)
하나씩 알아볼게요.
자연어란?
AI 모델을 만드는데 가장 중요한 점은 "정확한 목표"와 "적절한 데이터"입니다.
오늘 다룰 내용은 자연어 처리 모델이니까 데이터는 "자연어"겠죠.
그럼 자연어가 뭔지를 먼저 알아야겠네요. 😀
자연어는 인공 언어와 대치되는 개념입니다.
프로그래밍 언어(Python, C, C++,...)와 사람이 사용하는 언어를 구분하기 위해 만들어진 개념이죠.
자연어와 인공 언어의 가장 큰 차이는 "규칙, 문법, 형식"입니다.
인공 언어는 "규칙, 문법, 형식"을 따라야만 소통이 가능합니다. 필수라는 뜻이죠.
자연어도 "규칙, 문법, 형식"은 존재하지만 따르지 않아도 큰 문제없이 소통이 가능합니다.
이런 자연어는 소통 수단에 따라 2가지로 나뉩니다. "문어(Written Language)"와 "구어(Spoken Language)"죠.
자연어를 처리하려면 모두 이해해야 합니다.
먼저 자연어의 어휘 분석(lexical), 구문분석(syntatic), 의미 분석(semantic) 지식을 이용해 문어의 내용을 이해할 수 있어야 합니다.
그리고 대화의 과정에서 발생한 모호한 내용을 처리하기 위해 충분하게 주어진 정보를 이용하여 구어의 내용을 이해할 수 있어야 합니다.
점점 복잡해지죠. 😵
물론 자연어를 처리하기 위해서는 해결할 문제들이 더 많이 남아있지만 여기까지만 설명하겠습니다.
혹시 궁금하시다면 아래 링크를 참고해주세요. (저도 많은 도움을 받았습니다. 감사합니다.)
자연어 전 처리
앞서 소개한 이유만으로도 자연어는 "전 처리"가 필요하다는 게 느껴지시죠?
자연어를 전 처리하는 방법은 정말 많습니다.
먼저 자연어를 수치화하기 전에 필요한 전 처리 3가지를 소개할게요.
- Noise canceling
- Tokenizing
- StopWord removal
Noise canceling
이어폰에 탑재된 기능으로 많이 들어보셨죠?
오늘은 Text에 대한 내용을 다룰 거라서 방식은 다릅니다.
Text에서 말하는 Noise canceling은
"자연어 문장의 스펠링 체크 및 띄어쓰기 오류 교정"입니다.
예를 들어볼게요.
"안녀하 세요. 반갑 스니다." ➡️ "안녕하세요. 반갑습니다."
이 기술은 여러분도 일상생활에서 자주 쓰고 있었네요. 하루에도 몇 번씩 말이죠. 👍
Tokenizing
다음에 소개할 방법은 Tokenizing입니다.
"문장을 Token으로 나누는 방식"이라고 정의할 수 있겠네요.
여기서 Token은 어절, 단어 등으로 목적에 따라 정의를 다르게 할 수 있습니다.
예를 들어볼게요.
"Kay의 블로그를 방문해주셔서 감사합니다."
➡️ ['Kay', '의', '블로그', '를', '방문', '해주셔서', '감사합니다', '.']
그런데 Token으로 나누는 이유는 뭘까요?
우리는 문장 Data를 딥러닝에 Input으로 넣어야 하는데요.
딥러닝이 인식하는 Input으로 바꾸려면 "수치화"한다는 의미겠죠?
이때 Token 단위로 나누면 Token마다 의미를 부여할 수 있다는 점에서 나온 방법입니다.
StopWord removal
StopWord는 불용어. 즉, 필요 없는 단어를 의미합니다.
감탄사가 대표적이고, 경우에 따라서는 접속사도 불용어가 될 수 있습니다.
예를 들어볼게요.
한 회사에서 작성한 회의록이 한 뭉치 있습니다.
회의록들을 주제에 따라 분류할 생각인데요.
이때, "그러나, 그래서"와 같은 접속사는 분류에 필요한 정보는 아닙니다.
"상품 기획", "사내 복지"등과 같은 단어들이 중요한 정보겠죠.
Bag of Words
위의 과정을 거치면 Data에는 필요한 정보들만 남아있겠네요.
이제 딥러닝 모델이 이해할 수 있게 수치화할 차례입니다.
이 때 필요한 개념이 "Bag of Words"입니다.
"Bag of Words"는 말 그대로 "단어들이 들어있는 가방"을 만드는 과정입니다.
예를 들어보죠.
['안녕', '만나서', '반가워', '나도', '만나서', '반가워'] ➡️ ['안녕':0, '만나서':1, '반가워':2, '나도':3]
왼쪽은 Tokenizing을 거친 Data입니다.
이 Data에 있는 Token들을 모두 확인해서 Index를 생성하는 것이 "Bag of Words"입니다.
(Index를 만들기만 하는 과정이므로 Data 자체는 아직 그대로입니다.)
"Bag of Words"만 거친 Index 자체에는 의미는 없고 그냥 순서라고 생각하셔도 됩니다.
아직 Token의 의미가 반영되지 않은 거죠.
Token Sequence
Index를 만들었으니 이제 수치로 변환해야죠.
그 과정을 "Token Sequence"라고 합니다.
뭔가 어색해 보이는 곳이 있죠?
마지막 Data를 보면 2개의 Token만 들어왔습니다.
이런 경우에는 길이를 맞추기 위해 추가적인 Data를 부여해줍니다.
이를 "Padding"이라고 하죠.
Padding? 어디서 들어본 것 같지 않나요?
바로 "이미지 처리(링크)"에서 다뤘습니다. 그 기능도 동일합니다. (통일!)
이때 Padding 처리해 줄 값은 Index에 포함되지 않은 값을 사용합니다.
일반적으로는 가장 긴 문장을 기준으로 나머지 문장들에 Padding 처리를 해줍니다.
하지만 유독 한 문장만 긴 경우라면 이를 제외해야 Input이 너무 커지는 것을 막을 수 있습니다.
Word Embedding
"Bag of Words"에서 Index는 아직 Token의 의미가 반영되지 않았다고 설명했죠?
이제 Token들에게 의미를 부여할 시간입니다.
*️⃣감잡기 *️⃣
"이미지 처리(링크)"에서 다룬 것으로 예를 들어
MLP에서 Pixel값을 Input으로 사용한 것인 "Back of Words".
CNN에서 특징을 활용하는 것이 "Word Embedding"이라고 보시면 되겠습니다.
"Word Embedding"을 하는 이유는 Token의 특징(의미)을 나타내기 위함이죠.
이를 위해 "Embedding table"를 사용합니다.
"Back of Words"의 Index에 별로 "Embedding table"에 Vector를 정의되어 있고, 이용해 의미를 부여하죠.
Vector를 어떻게 사용하는지 볼까요?
먼저 Vector에는 각 Token의 특징이 들어있다는 점이 중요합니다.
Vector끼리는 유사도를 구할 수도 있고, 연산도 가능하기 때문이죠.
유사도를 구하면 각 Token들의 의미가 비슷한 정도를 알 수 있겠죠.
그리고 연산을 하면 두 Token를 조합해 새로운 의미의 Token을 만들 수도 있습니다.
위의 예시를 보죠.
'어머니'와 '아버지'에 해당하는 "Embedding table"값은 '친구'나 '회사'보다 유사하다는 게 느껴집니다.
유사도는 이를 바탕으로 구할 수 있는 거죠. (자세한 설명은 생략하겠습니다.)
이제 코드로 직접 해보겠습니다.
사용할 데이터는 "IMDb"의 영화 리뷰 Data set입니다.
이 Data set은 Embedding 이전까지의 작업이 되어있습니다. (원래는 처음부터 다 해야 합니다😥)
import logging, os
logging.disable(logging.WARNING)
os.environ["TF_CPP_MIN_LOG_LEVEL"] = '3'
import json
import numpy as np
import tensorflow as tf
from keras.datasets import imdb
from keras.preprocessing import sequence
# 난수 고정
np.random.seed(123)
tf.random.set_seed(123)
# data load를 위함 함수 정의
np_load_old = np.load
np.load = lambda *a, **k: np_load_old(*a, allow_pickle=True, **k)
n_of_training_ex = 5000
n_of_testing_ex = 1000
PATH = "data\\"
def imdb_data_load():
# data load
X_train = np.load(PATH + "data_X_train.npy")[:n_of_training_ex]
y_train = np.load(PATH + "data_y_train.npy")[:n_of_training_ex]
X_test = np.load(PATH + "data_X_test.npy")[:n_of_testing_ex]
y_test = np.load(PATH + "data_y_test.npy")[:n_of_testing_ex]
# json 파일에 저장된 단어 index 불러오기
with open(PATH + "data_imdb_word_index.json") as f:
word_index = json.load(f)
# Dictionary의 "단어: Index" 를 "Index: 단어" 로 변환
inverted_word_index = dict((i, word) for (word, i) in word_index.items())
# 인덱스를 기준 단어를 문장으로 변환
decoded_sequence = " ".join(inverted_word_index[i] for i in X_train[0])
print("First X_train data sample: \n", decoded_sequence)
print("\n First train data sample token index sequence: \n", X_train[0])
print("Length of first train data sample token index sequence: ", len(X_train[0]))
print("First y_train data: ", y_train[0])
return X_train, y_train, X_test, y_test
X_train, y_train, X_test, y_test = imdb_data_load()
"lambda"를 이용해 함수를 하나 정의했습니다.
옵션 중 "allow_pickle"은 뭘까요?
Numpy의 공식 문서에 소개된 내용입니다.
간단하게 말하자면 "pickled 된 array의 loading을 허용할지에 대한 옵션"이죠.
여기서 pickled 되었다는 것은 binary형태로 저장되었다는 의미입니다.
다음에는 "def"로 함수를 정의했습니다.
내부에 소개할 부분은 4가지입니다.
1️⃣ ".npy"파일을 이용해 data를 load 하는 부분이 보이네요 (앞서 정의한 함수로 load 해줬습니다.)
2️⃣ 다음으로 json 파일에 미리 저장해둔 단어 index를 불러왔습니다.
3️⃣ 그리고 "(index, 단어)" 형태로 Dictionary를 재설정했죠.
4️⃣ 마지막으로 Index를 기준으로 단어를 문장으로 만들었습니다.
함수를 실행시키니 아래와 같은 내용이 출력되었습니다.
데이터를 불러와 "Tokenizing"과 "Bag of Word"가 된 것을 보았습니다.
이제 할 일은 "Token sequence" 겠죠?
# padding 수행
max_review_length = 300
X_train = sequence.pad_sequences(X_train, maxlen=max_review_length, padding='post')
X_test = sequence.pad_sequences(X_test, maxlen=max_review_length, padding='post')
print("\n<Padding> First X_train data sample token index sequence: \n", X_train[0])
"pad_sequences"라는 함수를 통해 padding을 진행합니다.
옵션을 살펴보면 "maxlen"으로 문장의 최대 길이 설정해줬네요. (너무 긴 문장의 뒷부분은 잘라내는 거죠.)
다음 옵션은 "padding"입니다.
default는 왼쪽에 0을 padding 해줍니다. 그리고 "padding='post'"으로 설정하면 오른쪽에 0을 padding 하죠.
만약 "value"옵션을 사용하면 "0" 대신 원하는 값으로 padding 할 수 있습니다.
RNN (Recurrent Neural Network)
자연어 문장의 전 처리까지 되면 이제 딥러닝 모델에 적용해야 합니다.
기존의 MLP 모델에 적용시키면 어떻게 될까요?
"이미지 처리(링크)"에서 말한 그 단점이 있어서 안됩니다.
Token으로 나눈 단어, 문장이 연결되거나, Token 간의 관계를 파악하기 어려워지는 거죠. 😱
"Token 간의 순서와 관계를 적용할 수 있는 모델"이 필요합니다.
그렇게 나온 모델이 RNN(순환 신경망)입니다.
위 이미지가 가장 기본적인 RNN의 구조입니다.
input X에는 "Word Embedding"을 거친 Data가 들어갑니다.
output Y는 사용자의 설정에 따라 달라집니다.
Token의 Vector 형태도 가능하고, "0 ~ 1"의 확률 혹은 Class도 가능하죠.
위 그림은 RNN의 보다 구체적인 구조입니다.
첫 RNN을 통과한 결과 값이 2가지로 나눠지네요.
이렇게 나눠진 값 중 "h"로 표기된 값은 다음 RNN의 학습에 참여합니다.
오른쪽에 있는 Data를 사용한다고 가정해볼게요.
첫 X에는 '안녕'이 들어가서 "h"로 결과가 나옵니다.
그리고 다음 X에는 '만나서'가 들어가면서 'h'도 RNN에 함께 들어가는 거죠!
최종적으로는 이런 구성의 RNN이 사용됩니다.
표기된 것처럼 마지막 Output Y만 FC를 거쳐서 모델의 예측값으로 나오게 됩니다.
(CNN 이후 FC 가 사용되는 점은 이미지 처리와 비슷하네요.)
중간 RNN의 Output으로 표기된 값들은 아니죠.
코딩하면서 사용할 "영화 리뷰"를 예로 들어봤습니다.
저런 Input이 들어가 최종적으로 "긍정", "부정"의 Class를 예측값으로 주게 됩니다.
"Word Embedding으로 Token의 특징을 찾고, RNN은 이전 Token의 영향을 받아 학습합니다."
이제 마지막으로 RNN을 직접 구성해보겠습니다.
데이터와 전 처리 과정은 위에서 진행한 것과 동일합니다.
import logging, os
logging.disable(logging.WARNING)
os.environ["TF_CPP_MIN_LOG_LEVEL"] = '3'
import json
import numpy as np
import tensorflow as tf
from keras.datasets import imdb
from keras.preprocessing import sequence
# 난수 고정
np.random.seed(123)
tf.random.set_seed(123)
# data load를 위함 함수 정의
np_load_old = np.load
np.load = lambda *a, **k: np_load_old(*a, allow_pickle=True, **k)
n_of_training_ex = 5000
n_of_testing_ex = 1000
PATH = "data\\"
def imdb_data_load():
# data load
X_train = np.load(PATH + "data_X_train.npy")[:n_of_training_ex]
y_train = np.load(PATH + "data_y_train.npy")[:n_of_training_ex]
X_test = np.load(PATH + "data_X_test.npy")[:n_of_testing_ex]
y_test = np.load(PATH + "data_y_test.npy")[:n_of_testing_ex]
# json 파일에 저장된 단어 index 불러오기
with open(PATH + "data_imdb_word_index.json") as f:
word_index = json.load(f)
# Dictionary의 "단어: Index" 를 "Index: 단어" 로 변환
inverted_word_index = dict((i, word) for (word, i) in word_index.items())
# 인덱스를 기준 단어를 문장으로 변환
decoded_sequence = " ".join(inverted_word_index[i] for i in X_train[0])
print("First X_train data sample: \n", decoded_sequence)
print("\n First train data sample token index sequence: \n", X_train[0])
print("Length of first train data sample token index sequence: ", len(X_train[0]))
print("First y_train data: ", y_train[0])
return X_train, y_train, X_test, y_test
X_train, y_train, X_test, y_test = imdb_data_load()
max_review_length = 300
X_train = sequence.pad_sequences(X_train, maxlen=max_review_length, padding='post')
X_test = sequence.pad_sequences(X_test, maxlen=max_review_length, padding='post')
# padding 수행
max_review_length = 300
X_train = sequence.pad_sequences(X_train, maxlen=max_review_length, padding='post')
X_test = sequence.pad_sequences(X_test, maxlen=max_review_length, padding='post')
print("\n<Padding> First X_train data sample token index sequence: \n", X_train[0])
여기까지는 완전히 같죠?
이제 모델을 구현하고, 학습시켜 봅시다.
# 모델 구현
embedding_vector_length = 32
model = tf.keras.models.Sequential([
tf.keras.layers.Embedding(1000, embedding_vector_length, input_length = max_review_length), # Word Embedding
tf.keras.layers.SimpleRNN(5),
tf.keras.layers.Dense(1, activation='sigmoid') # 이진분류라서 output을 1로 설정
])
print(model.summary())
# 모델 학습 방법 설정
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
# 모델 학습
model_history = model.fit(X_train, y_train, epochs=5, verbose=2)
"embedding_vector_length"를 정의했네요.
이 값은 Output의 Dimension을 정하는데 쓰입니다.
Embedding Vector의 크기. 즉 몇 개짜리의 특성으로 만들 것인가를 정하죠.
다음으로 모델의 구조를 들여다볼게요.
"tk.keras.layers.Embedding(input_dim, output_dim, input_length)"
새로운 Layer가 나왔죠? 바로 Embedding을 진행하는 Layer입니다.
input_dim은 들어 올 단어의 수를 알려주는 옵션입니다.
output_dim은 "embedding_vector_length"로 미리 정의한 값을 사용해 Output에 대해 설정해줬습니다.
input_length는 들어오는 단어 Vector의 크기를 정하는 옵션입니다.(너무 긴 것을 자르기 위함)
"tf.keras.layers.SimpleRNN(units)"
다음 Layer가 RNN이군요.
옵션인 units는 Layer의 Node 수를 설정해줍니다.
"tf.keras.layers.Dense()"
그리고 마지막엔 RNN의 Output을 이용해 학습할 layer입니다.
이진 분류 문제라서 Output을 '1'로, Activation function은 'sigmoid'로 설정했습니다.
"model.compile()"
이렇게 만든 모델을 학습시켜야겠죠?
loss는 binary_crossentropy를 사용했습니다. (이전 글에서 Loss function에 대해 설명한 적이 있습니다.)
분류 문제의 경우에는 CEE (Cross-Entropy Error)를 사용한다고 했었죠?
이진 분류이기 때문에 "Binary Cross Entropy"가 적용된 거죠.
마지막 과정입니다.
학습한 모델을 평가하고 모델의 예측값을 확인해보죠.
# 모델 평가
loss, test_acc = model.evaluate(X_test, y_test, verbose=0)
# 예측
predictions = model.predict(X_test)
print('\nTest loss: {:.4f} | Test accuracy: {}'.format(loss, test_acc))
print('\nPredicted test data class: ', 1 if predictions[0]>=0.5 else 0)
이 과정에서 확인할 부분은 마지막 출력 쪽입니다.
"predictions[0]>=0.5" 라는 조건이 보이시죠.
최종 목표는 "영화 리뷰의 긍정, 부정 예측"이기 때문에 0.5를 기준으로 분리해봤습니다.
(설정 상 0은 부정, 1은 긍정입니다.)
오늘의 과정을 여기까지입니다!!
loss나 accuracy가 좋지 않지만, 과정을 이해하기 위한 코드이므로 넘어가겠습니다. 😒
여기까지 "딥러닝 - 자연어 처리(Text)"에 대해 알아봤어요.
글이 도움이 되셨다면 공감 버튼 눌러주세요. 😊
'AI & Data > 이론' 카테고리의 다른 글
[AI 이론] 딥러닝 - 이미지 처리 이론과 실습 (Ft. MNIST) (0) | 2022.11.16 |
---|---|
[AI 이론] 딥러닝 모델의 학습 방법과 개념 (Ft. Tensorflow, Keras) (0) | 2022.11.14 |
[AI 이론] 손실 함수 한방에 끝내기 (Loss Function) (0) | 2022.11.11 |
[AI 이론] 활성 함수 한방에 끝내기 (Activation Function) (0) | 2022.11.10 |
[AI 이론] 딥러닝 - 퍼셉트론 한방에 끝내기 (Perceptron) (0) | 2022.11.08 |