Key words
연속형 데이터, RNN(순환 신경망), LSTM & GRU, Attention
1. 언어 모델이란?
- 오늘 배운 것의 주제는 제목에서와 같이 뭐다?
Language Modeling
이다. 이 점을 유의해서 앞으로 펼쳐지는 내용을 머릿속에 그림으로 그려보자.
- 먼저, 언어 모델이란 무엇일까? 언어 모델이란 문장과 같은 단어 시퀀스에서 각 단어의 확률을 계산하는 모델이다. 어제 배운
Word2Vec
도 언어 모델 중의 하나라고 보면 된다.
- 예를 들어
C-Bow
에서 "I am a student"라는 문장이 만들어질 확률은 아래와 같이 조건부 확률의 곱으로 표현할 수 있다.
P("I","am","a","student")=P("I")×P("am"∣"I")×P("a"∣"I","am")×P("student"∣"I","am","a")
이 언어모델의 큰 stream을 보면 통계적 언어 모델과 신경망 언어모델로 나뉜다.
통계적 언어모델(Statistical Language Model, SLM)
- 신경망 모델의 등장 이전에 주로 사용되던 전통적인 접근 방식이다. 이름 그대로 통계적 방식으로 접근하는 걸 생각하면 되는데, 위의 문장으로 예를 들어보자.
P("I","am","a","student")=P("I")×P("am"∣"I")×P("a"∣"I","am")×P("student"∣"I","am","a")
- 여기서 P("I")를 구한다고 할 때, 전체 말뭉치가 100개고 그 중 I로 시작하는 문장이 10개라면? P("I") = 10%가 된다. 그렇다면 P("am"∣"I")는? 만약 전체 말뭉치가 100개고 그 중 'I am'으로 시작하는 문장이 1개라면? P("am"∣"I") = 1%가 된다. 이런 식으로 각각의 조건부 확률을 구해 다 곱하면 "I am a student"라는 문장이 나올 확률을 구할 수 있게 되는 것이다.
- 자, 언어 모델이 이런 식으로 학습을 한다고 했을 때 한계점이 보일 것이다. 통계적 언어 모델은 횟수 기반으로 확률을 계산하기 때문에 희소성(Sparsity) 문제를 가지고 있다.
- 무슨 말이냐면, 만약 "I studied this section ..." 문장에서 ... 에 3 times, 4 times, 5 times까지 학습을 했다고 했다고 해도 만약 6 times를 학습한 적이 없다면 이 모델은 절대 "I studied this section 6 times"라는 문장을 만들어낼 수가 없게 된다. 왜냐하면 위에서 조건부확률을 구했던 것처럼 표현해보면 P("I","studued","this","section", "6", "times)=0 이 되기 때문이다. 즉, 말뭉치에 등장하지 않으면 다양한 문장을 만들어낼 수 없는 한계를 희소성 문제라고 생각하면 된다.
- 참고 - 더 공부해볼만한 것
N-gram
(통계적 언어 모델 고도화 방법 중 하나), Back-off
/ Smoothing
(희소 문제 해결을 위한 방법론 중 하나)
신경망 언어 모델 (Neural Langauge Model)
- 신경망 언어 모델은 이전에 배운 것처럼 임베딩 벡터를 사용하기 때문에 말뭉치에 등장하지 않아도 문법적, 의미적으로 유사한 단어가 선택될 수 있어 보다 다양한 문장을 만들어내게 된다.
- 처음에 통계적 언어 모델만 사용하던 연구자들이 이 신경망 모델을 적용해보고 난 다음에 얼마나 놀랐으려나..!
2. 순환 신경망 (RNN, Recurrent Neural Network)
오늘의 메인 주제다. 진짜 어려워서 너어--무 고생했고 여기 적는 것 외에도 보충 공부 필요함.
- RNN에 대해서 알아보기 전에 잘 기억해야할 아주 핵심적인 단어가 있다. 그건 바로
연속형 데이터 (Sequential Data)
이다.
연속형 데이터 (Sequential Data)
의 정의는 '어떤 순서로 오느냐에 따라서 단위의 의미가 달라지는 데이터'를 말한다. 예를들어 언어로 이루어진 문장 등이 가장 대표적인 것 같고(한국어는 좀 덜하긴 하지만 라틴계열 문자는 순서에 따라 동사가 명사가 되기도 하는 등 의미가 달라지는 경우가 많음), 시계열로 이루어진 주가 데이터도 예시로 보았다.
- RNN은 이 연속형 데이터를 잘 처리하기 위해 고안된 신경망 모델이다!! 1 Of 연속형 데이터 = Language이니까 오늘 주제와 연관해서 쉽게 생각해보면, 언어 데이터를 잘 다루기 위해 만들어진 거라고 기억해도 될 것 같다.
- 김성범 교수님의 이 영상에서 RNN에 대해서 잘 설명해주고 있으니 잘 기억이 안날 때 잘 참고하도록 하자. 교수님 짱!
RNN의 기본 구조는 아래와 같다.
- 위 그림의 화살표는 아래와 같다. 3번째 항목은 기존 신경망 모델에서는 없었던 개념이다.
- 입력 벡터가 은닉층에 들어가는 것을 나타내는 화살표 ( 𝑊ℎ𝑥 )
- 은닉층로부터 출력 벡터가 생성되는 것을 나타내는 화살표 ( 𝑊𝑜ℎ,𝑏𝑜 )
- 은닉층에서 나와 다시 은닉층으로 입력되는 것을 나타내는 화살표. (𝑊ℎℎ,𝑏ℎ)
- 즉, RNN은 이전 시점의 정보들을 반영하여 더 정확한 예측을 하기 위해 활용해보자! 는 아이디어에서 출발했다. 위 그림을 보면 은닉층의 벡터(은닉 벡터) ht가 다음 은닉층에 전달되는 것을 볼 수 있다. 이게 RNN의 핵심이다!!
- 어떻게 ht에 ht−1를 합성한다는 거지? 김성범 교수님 영상에서 이 수식을 보니 생각해보기 편했고, 핵심을 잘 담고 있다는 생각이 들었다.
- ht = f(Wxh∗xt + Whh∗ht−1) (bias는 수식에 넣지 않음)
- yt = g(Who∗ht)
- f() = tanh, g() = softmax
- (위 수식에서 볼 수 있듯 가중치는 Wh,Wx 2개가 있다. 각각 입력 x를 h로 변환하기 위한 Wx와 RNN의 은닉층의 출력을 다음 h로 변환해주는 Wh이다. 주의할 것은 각 시점마다의 이 파라미터는 동일한 값이라는 것이다. 학습을 거치며 업데이트 되는 값이지 한 학습 내에서 계속 변하는 값이 아니라는 말이다!)
- 위 식에서는 𝑡-1, 𝑡만 표현했지만 그 과정이 쭉 있다고 생각해보면, 𝑡시점에 생성되는 hidden-state 벡터인 ℎ𝑡 는 해당 시점까지 입력된 벡터 𝑥1,𝑥2,⋯,𝑥𝑡−1,𝑥𝑡 의 정보를 모두 가지고 있게 되는 것이다. 연속형 데이터가 순서대로 입력되더라도 순서 정보를 모두 기억하기 때문에 연속형 데이터를 다룰 때 RNN을 많이 사용한다고 한다.
RNN의 종류
아래와 같이 다양한 형태의 RNN이 있다.
- 결과값을 언제 몇개를 뱉느냐에 따라 대강 어떤 목적으로 사용하는 모델인지는 구분이 될 수도 있을 것이다. 순서대로 예시는 다음과 같다.
- 이미지를 받아 이미지를 설명하는 문장 만들기, 감성분석(긍정/부정), 기계번역(위 그림과 같은 구조를 Seq2Seq라고 함), 비디오 프레임별 분류하기
그 외 참고
- tanh(하이퍼볼릭 탄젠트)를 쓰는 이유는 만약 ReLu를 쓰면 양수일 때 그 값을 그대로 뱉기 때문에 그대로 다음으로 넘어가면 넘어갈수록 값이 너무 커져서 기울기 폭발이 일어날 수 있기 때문이라고 한다. sigmoid를 안 쓰는 이유는 sigmoid보다 tanh가 좀 더 기울기 소실문제에서 견고하기 때문이라고 한다.(이건 tanh, sigmoid 미분 그래프의 범위를 찾아보면 바로 이해할 수 있다. sigmoid는 0~0.25 사이의 값을 가지기 때문에 상대적으로 더 작은 값들이 계속 곱해지다보면 소실 문제가 일어날 수 있다는 뜻임.)
- RNN을 파이썬으로 구현하면 아래와 같다. 참고차 넣어둠!
import numpy as np
class RNN:
"""
RNN을 파이썬 코드로 구현한 클래스입니다.
Args:
x: 입력되는 벡터
h_prev: 이전 시점의 은닉 상태 벡터(hidden state vector)
Wx: 입력 벡터(x)에 곱해지는 가중치
Wh: 은닉 상태 벡터(h_prev)에 곱해지는 가중치
b: 편향(bias)
"""
def __init__(self, Wx, Wh, b):
self.params = [Wx, Wh, b]
self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
self.cache = None
def forward(self, x, h_prev):
Wx, Wh, b = self.params
t = np.matmul(h_prev, Wh) + np.matmul(x, Wx) + b
h_next = np.tanh(t)
self.cache = (x, h_prev, h_next)
return h_next
일단 RNN에 대해선 이 정도로 다루고 넘어가도록 하자.
3. LSTM / GRU
LSTM(Long-Short Term Memory, 장단기 기억망)
은 RNN의 단점을 보완하기 위해 나온 모델이다. 그럼 RNN의 무슨 문제가 있다는 것일까?
- RNN의 단점
- 병렬화(Parrelization) 불가능: 연속형 데이터를 받아 순차적으로 데이터를 처리하다보니 GPU 연산의 장점인 병렬연산이 불가능하다. (즉, 전체 작업 속도가 느릴 수 밖에 없다는 것)
- 기울기 폭발(Exploding Gradient), 기울기 소실(Vanishing Gradient) 문제: 위에서 잠시 언급했지만 역전파 과정에서 tanh의 미분값이 계속 곱해지게 되는데, 이 과정에서 1 미만의 작은 숫자들이 계-속 곱해지다보면 기울기 소실 문제가 발생할 수 있다. (즉, 시퀀스 앞쪽으로 갈수록 역전파 정보가 제대로 전달될 수 없단 뜻이다.)
기울기 정보의 크기가 문제라면 "기울기 정보의 크기를 적절하게 조정하여 줄 수 있다면 문제를 해결할 수 있지 않을까?"라는 아이디어에서 나온 것이 바로 LSTM이라고 할 수 있다.
- 참고로 요즘엔 RNN이라고 하면 대부분 LSTM이나 GRU를 말하고, 오히려 기본적인 RNN을 Vanilla RNN이라고 별칭한다고 한다. 나도 앞으로는 RNN하면 그냥 LSTM이라고 디폴트로 생각하면 될 듯.
LSTM의 구조
- 위는 LSTM 셀 하나의 구조이다.
(후.. 정신 바짝 차려야 한다..) 기존 RNN에는 없던 'Gate'라는게 추가된 것을 볼 수 있다. 각각의 의미는 다음과 같다.
- Forget Gate (ft): 과거 정보를 얼마나 유지할 것인가?
- Input Gate (it) : 새로 입력된 정보는 얼마만큼 활용할 것인가?
- Output Gate (ot) : 두 정보를 계산하여 나온 출력 정보를 얼마만큼 넘겨줄 것인가?
- 아주 잘 기억해야할 것은 cell-state가 추가되었다는 것이다. 이 cell-state는 역전파 과정에서 활성화 함수를 거치지 않아 정보 손실이 없기 때문에 뒷쪽 시퀀스의 정보에 비중을 결정할 수 있으면서 동시에 앞쪽 시퀀스의 정보를 완전히 잃지 않을 수 있다고 한다.
- 구체적으로 각각 어떻게 작동하는지는 내용을 많이 찾아봤는데 복잡해서 완전히 이해하기가 어려웠다. 오늘은 우선 크게 크게 의미 위주로만 기억하도록 하고, 조만간 공부노트에서 파보기로 하자.
- 참고: 역전파 과정 레퍼런스
GRU
- GRU(Gated Recurrent Unit)는 LSTM의 간소화버전인데, 요즘에는 거의 LSTM을 쓴다고 하니 여기는 그 구조 이미지만 남겨두고 넘어간다.
4. Attention 어텐션
- 기계번역에서 아주아주아주아주아주 중요한 개념인 어텐션에 대해서도 다뤄보았다. 전에 읽었던 책에서 attention 개념을 처음 제안한게 한국인 교수님으로 봤었는데 괜히 혼자 내적친밀감 들고 막 그랬다. 암튼 기계번역에 쓰이는 거라는 걸 기억해!! 아래 그림 깔고 다음을 보면 된다.!!!
- 어텐션 개념은 역시 RNN의 단점을 보완하기 위해 나왔다. 그럼 RNN에 어떤 단점이 있다는 걸까?
- 그건 바로 기울기 소실로부터 나타나는 장기 의존성(Long-term dependency) 문제이다. 즉, RNN의 구조를 보면 앞의 정보가 뒤로 순차적으로 전달되는데, 문장이 길어진다면??? 당연히 시퀀스 맨 앞쪽에 위치하는 코튼의 정보를 상당부분 잃어버릴 수 밖에 없는 것이다.
- 이 문제를 어떻게 해결했을까? 기존 구조의 문제는 decoder가 단어를 예측할 때, encoder의 마지막 은닉층의 정보(= Context Vector)만을 활용하기 때문에 어떤 단어로 번역해야할지 각각 예측할 때, 원래 문장에서 더 중요한 단어에 집중할 수가 없다는 것이었다.
- 그래서 나온 대안은 바로 1) 매 시점 정보를 참고하고, 2) 더 중요한 단어에 집중하자라는 것이다.
- 그럼 중요도는 어떻게 계산할까? 가장 간단한 유사도 측정은 바로 내적이다! 이걸 포함하여 어텐션이 디코더에 적용되는 과정은 다음과 같다.
- 저기 Decoder의 hidden vector는 내가 번역할 단어라고 생각해도 된다.
- 먼저, '나는'을 번역하기 위해 해당 부분의 hidden-state vector를 준비한다. 그리고 'I', 'Love', 'You' 각각의 hidden-state vector와 내적을 통해 score를 계산한다.
- 이 score를 그대로 쓰면 정확한 비교가 어려우니까 softmax함수를 취해 0-1사이의 확률 값으로 변환해준다.
- 이 각각의 확률값에 아까 준비했던 'I', 'Love', 'You' 각각의hidden-state vector를 곱한다. 그걸 표현한게 위 4번 그림이다. 이 벡터들을 모두 더해 하나의 Context vector를 만든다.
- 최종적으로 이렇게 나온 context vector와 디코더의 히든 스테이트 벡터를 활용해(곱하기) 출력 단어를 결정한다.
- 위 과정을 디코더의 각 스텝마다 반복하며 번역을 진행한다.
자, 기존 RNN처럼 하나의 context vector를 사용하는게 아니라 각 단어마다 이렇게 어떤 단어에 집중할지를 결정하기 때문에, 시퀀스의 앞에 위치했다고 하더라도 토큰 정보가 소실되지 않아 번역 성능이 좋아진 것이다.
오늘은 간단히 적었지만 어텐션 개념에 대해 잘 알고 싶다면 위 김성범 교수님 영상을 한 번 보는 걸 추천한다.
5. 실습한 것
오늘은 코치님이 개념만 잘 챙겨가면 되고 코드는 나중에 이해해도 된다고 수차례 강조했었다. 오늘 실습은 그런 의미에서 간단했음. 실제로 LSTM 적용하려면 layer에 하나만 쌓으면 됐음!
다음 링크는 LSTM을 사용하여 Spam 메시지 분류를 수행한 캐글 노트북입니다. => Link
위 노트북에서 사용한 코드를 참고하여
캐글 데이터셋인 Women's E-Commerce Clothing Reviews 를 분류해 보세요.
- 분류에 사용될 텍스트 데이터 :
Review Text
열을 사용합니다.
- 레이블(label) 데이터 :
Recommended IND
열을 사용합니다.
[데이터 전처리]
(여러 열에 있던 것 통합해서 넣어두겠음)
np.random.seed(42)
tf.random.set_seed(42)
df = pd.read_csv('Womens Clothing E-Commerce Reviews.csv')
feature = 'Review Text'
target = 'Recommended IND'
df = df[[feature, target]]
print('feature 결측치 확인', df[feature].isna().sum())
df = df.dropna(axis = 0)
print('\nfeature 결측치 재확인', df[feature].isna().sum())
print('target 결측치 확인', df[target].isna().sum())
2)텍스트 분류를 수행해주세요.
- 데이터셋 split시 test_size의 비율은 20%로,
random_state = 42
로 설정합니다.
- Tokenizer의
num_words=3000
으로 설정합니다.
- pad_sequence의
maxlen=400
으로 설정합니다.
- 학습 시, 파라미터는
batch_size=128, epochs=10, validation_split=0.2
로 설정합니다.
- EarlyStopping을 적용합니다. 파라미터는
monitor='val_loss',min_delta=0.0001, patience=3
로 설정합니다.
- evaluate 했을 때의 loss와 accuarcy를 [loss, acc] 형태로 입력해주세요. Ex) [0.4321, 0.8765]
[텍스트 분류를 수행]
train, test = train_test_split(df, test_size=0.2, random_state = 42)
X_train, X_test = train[feature], test[feature]
y_train, y_test = train[target], test[target]
print('train shape', X_train.shape, y_train.shape,'\ntest shape', X_test.shape, y_test.shape)
num_words = 3000
maxlen = 400
batch_size=128
epochs=10
validation_split=0.2
monitor='val_loss'
min_delta=0.0001
patience=3
tokenizer = Tokenizer(num_words = num_words)
tokenizer.fit_on_texts(X_train)
'''
[오류 기록]
위 'tokenizer.fit_on_texts(X_train)'을 돌렸을 때 "AttributeError: 'float' object has no attribute 'lower'" 오류가 났다.
다시 데이터 전처리 단계로 돌아가서 결측값이 있는지 확인해보았더니 있었고 제거처리 후 제대로 작동하였다. 전처리 단계에서 결측치 확인하는 것 잊지 말자.
'''
X_train = tokenizer.texts_to_sequences(X_train)
X_train = sequence.pad_sequences(X_train, maxlen = maxlen)
print(X_train)
print('\n\n maxlen 잘 적용되었는지 확인 => len(padded[0]) ==', len(X_train[0]))
[모델링, 평가]
model = tf.keras.models.Sequential([
tf.keras.layers.Embedding(num_words, 300 , input_length = maxlen),
tf.keras.layers.LSTM(64),
tf.keras.layers.Dense(1, activation='sigmoid')
])
model.compile(loss='binary_crossentropy',
optimizer='adam',
metrics=['accuracy'])
model.fit(X_train, y_train, batch_size=batch_size, epochs=epochs, validation_split=validation_split, callbacks=[EarlyStopping(monitor=monitor,min_delta=min_delta, patience = patience)])
X_test = tokenizer.texts_to_sequences(X_test)
X_test = sequence.pad_sequences(X_test, maxlen = maxlen)
model.evaluate(X_test, y_test)
6. 그 외
- 오늘은 웜업 영상도 좋았음. 링크 기록해둔다. RNN, LSTM
- 이 문서도 레퍼런스로 좋을 듯 위키독스
Feeling
- 오늘 정말 완전 멘붕,., 지금까지는 아무리 어려워도 보통은 저녁 즈음 되면 머릿속에서 정리가 다 되었었는데, 안된다고 느낀게 오늘이 처음인 것 같다.
- 추가 공부가 절실하다..