자연어 처리 관련 용어
- 말뭉치(corpus) : 특정한 목적을 가지고 수집한 텍스트 데이터
- 문서(Document) : 문장들의 집합
- 문장(Sentence) : 여러 개의 토큰(단어, 형태소 등)으로 구성된 문자열, 마침표/ 느낌표 등의 기호로 구분
- 어휘집합(Vocabulary) : 코퍼스에 있는 모든 문서, 문장을 토큰화한 후 중복을 제거한 토큰의 집합
전처리
- 토큰화(Tokenization)
- 차원의 저주(Curse of Dimensionality)
- 불용어(Stop words)
- 어간 추출(Stemming)
- 표제어 추출(Lemmatization)
자연어를 컴퓨터로 처리하는 기술
자연어 처리(NLP)에는 자연어 이해(NLU) 와 자연어 생성(NLG) 의 세부 분야가 있음
벡터화(Vectorize)
CounterVectorize
)TfidVectorizer
)lower
, replace
...)문자열에서 특정한 규칙을 가지는 문자열의 집합을 찾아내기 위한 검색 방법
a-z
(소문자), A-Z
(대문자), 0-9
(숫자) 를 ^
제외한 나머지 문자를 regex
에 할당한 후 .sub
메소드를 통해 공백 문자열 ""로 치환
import re
# ^ : not 을 의미
regex = r"[^a-zA-Z0-9]" # 정규식
test_str = ("(Natural Language Processing) is easy!, AI!#n")
subst = "" # 치환할 문자
result = re.sub(regex, subst, test_str)
#'Natural Language Processing is easy AI'
SpaCy 라이브러리
import spacy
from spacy.tokenizer import Tokenizer
nlp = spacy.load("en_core_web_sm")
tokenizer = Tokenizer(nlp.vocab)
tokens = []
for doc in tokenizer.pipe(df['column_name']):
doc_tokens = [re.sub(r"[^a-z0-9]", "", token.text.lower()) for token in doc]
tokens.append(doc_tokens)
불용어 : 분석에 도움이 되지 않는 단어
대부분의 NLP 라이브러리는 일반적인 불용어를 내장하고 있음
nlp.Defaults.stop_words
# 불용어 제외하고 토크나이징 진행
tokens = []
for doc in tockenizer.pipe(df['column_name']):
doc_tokens = []
for token in doc:
# 토큰이 불용어와 구두점이 아니면 저장
if (token.is_stop == False) & (token.is_punct == False):
tokens.append(doc_tokens)
불용어 커스터마이징
STOP_WORDS = nlp.Defaults.stop_words.union(['smt_you_want_to except'])
token = []
for doc in tokenizer.pipe(df['column_name']):
doc_tokens = []
for token in doc:
if token.text.lower() not in STOP_WORDS:
doc_tokens.append(token.text.lower())
tokens.append(doc_tokens)
Poter
, Snowball
, Dawson
등 알고리즘 有
Spacy
는 stemming 제공하지 않음
nltk
from nltk.stem import PorterStemmer
ps = PoterStemmer()
words = ['wolf', 'wolves']
for word in words:
print(ps.stem(word))
# wolf
# wolv
## 어근이나 단어의 원형이 같지 않을 수 있음
Porter
알고리즘은 단어의 끝부분을 자르는 역할
복수형 → 단수형, 동사 → 타동사
lem = 'The social wolf. Wolves are complex.'
nlp = spacy.load?("en_core_web_sm")
doc = nlp(lem)
# The → the
# social → social
# wolf → wolf
# . → .
# Wolves → wolf
# are → be
# complex → complex
# . → .
def get_lemmas(text):
lemmas = []
doc = nlp(text)
for token in doc:
if ((token.is_stop == False) and (token.is_punct == False)) and (token.pos_ != 'PRON'):
lemmas.append(token.lemma_)
return lemmas
시각화
Squarify
라이브러리
문서-단어 행렬(DTM)
TF
단어 빈도만 고려
Sklearn CounterVectorizer
from sklearn.feature_extraction.text import CountVectorizer
# 문장으로 이루어진 리스트 저장
sentences_lst = text.split('\n')
# CountVectorizer 를 변수에 저장
vect = CountVectorizer()
# 어휘 사전을 생성
vect.fit(sentences_lst)
# text 를 DTM으로 변환
dtm_count = vect.transform(sentences_lst)
#.vocabulary_ 메소드로 모든 토큰과 맵핑된 인덱스 정보 확인
vect.vocabulary_
TF-IDF
TfidfVectorizer
tfidf = TfidfVectorizer(stop_words='english', max_features=15)
#fit 후 dtm
dtm_tfidf = tfidf.fit_transform(sentences_lst)
# dateframe으로 만들기
pd.DataFrame(dtm_tfidf.todense(), columns=tfidf.get_feature_names())
def tokenize(document):
doc = nlp(document)
return [token.lemma_.strip() for token in doc if (token.is_stop != True) and (token.is_punct != True) and (token.is_alpha == True)]
tfidf_tuned = TfidfVectorizer(stop_words='english',
tokenizer=tokenize,
ngram_range=(1,2), # (min_n, max_n) : min_n 개 ~ max_n 개를 갖는 n-gram(n개의 연속적인 토큰)을 토큰으로 사용
max_df=.7, # float(0~1), max_df*100% 이상 문서에 나타나는 토큰은 제거
min_df=3 # int, 최소 n개의 문서에 나타나는 토큰만 사용
)
쿼리와 가장 가까운 상위 K개의 근접한 데이터를 찾아서 K개 데이터의 유사성을 기반으로 점을 추정하거나 분류하는 예측 분석
Sklearn NearestNeighbors
from sklearn.neighbors import NearestNeighbors
# dtm을 사용해 NN모델 학습, 디폴트 최근접 5
nn = NearestNeighbors(n_neighbors=5, algorithm='kd_tree')
nn.fit(dtm_tfidt_amazon)
# 해당 문서와 가장 가까운 문서(0포함) 5개 거리(값이 작을수록 유사)와 인덱스 확인할 수 있음
token.pos_ != 'PRON'
대명사가 아닌 것
token.is_punct == False
구두점이 없는 곳
한국어 데이터는 자연어 처리에서 처리하기 어려운 데이터
- KoNLPy 라이브러리 사용 -- 형태소 분석기
- Py-hanspell -- 네이버 맞춤법 검사기를 이용한 파이썬 라이브러리
- Py-(ko)spacing, kss -- 띄어쓰기 기준
- Khaiii -- 카카오 오픈 소스 라이브러리(형태소 분석기)
- soynlp -- 반복되는 이모티콘이나 단어들을 정규화하기 위해 만들어짐(ex. ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ, ㅎㅎㅎㅎㅎㅎㅎ 같은 거)
등장 횟수 기반 단어 표현
단점 : 순서를 고려하지 않고 빈도수만 고려, 차원이 많아짐
분포 가설
원-핫 인코딩
↓ 가장 널리 알려진 임베딩 방법
단어를 벡터로 나타내는 방법
특정 단어 양 옆에 있는 두 단어(window size = 2)의 관계를 활용하기 때문에 분포 가설을 잘 반영
CBoW 와 Skip-gram 차이
Word2Vec 모델 구조(Skip-gram 기준)
Word2Vec 은 딥러닝일까?
NO! 딥러닝 = 은닉층이 2개 이상
ex. The tortoise jumped into the lake
- 중심 단어: The, 주변 문맥 단어 : tortoise, jumped
- 학습 샘플 : (the, tortoise), (the, jumped)
KeyError
발생 → OOV 문제Word2Vec을 더 적은 계산으로 하는 방법
- Sub-sampling
- 텍스트 자체가 가진 문제를 해결
- 'a', 'the' 처럼 얻을 정보가 없는 단어 제거
- Negative-sampling
- 불필요한 계산량 감소
- 무관한 단어들에 대해서는 weight 를 업데이트 하지 않아도 됨
- 무관한 단어는 target 값이 0 인 "negative" 값이고, 관련된 단어는 target 값이 1인 "positive" 한 값
Word2Vec
방식에 철자기반의 임베딩 방식을 더해준 새로운 임베딩 방식
OOV(Out of Vocabulary) 문제 해결
철자 단위 임베딩 방법
Character n-gram
fastText
방식은 3-6개로 묶은 character정보(3-6 grams) 단위 사용
3-6개 단위로 묶기 이전에 모델이 접두사와 접미사를 인식할 수 있도록 해당 단어 앞뒤로 "<",">"를 붙여줌
ex. <eating> 를 3-gram 하면
→ <ea eat ati tin ing ng>
이 방식을 3개부터 6개까지 진행하면 18개의 character-level n-gram을 얻을 수 있음
철자 단위 임베딩 적용
Word2Vec
은 두 단어 중 하나라도 말뭉치 내에 없다면 에러를 발생시키지만 fastText
는 높은 정확도로 두 단어의 임베딩 벡터를 구하고 유사도를 나타낼 수 있음
fastText
임베딩 벡터는 단어의 의미보다는 결과 쪽에 더 비중을 두고 있음
pad_sequences(padding='post', maxlen=n)
- 자연어 처리를 하다보면 각 문장(또는 문서)의 길이가 다른 경우가 있음
→ 기계는 길이가 전부 동일한 문서들에 대해서는 하나의 행렬로 보고 한꺼번에 묶어서 처리할 수 있음
→ 병렬 연산을 위해 여러 문장의 길이를 임의로 동일하게 맞춰주는 작업 필요- 데이터에 특정 값을 채워서 데이터의 크기를 조정하는 것을 패딩(padding) 이라고 함
- 케라스에서는
pad_sequence
제공pad_sequences
는 기본적으로 문서의 앞을 '0'으로 채우기 때문에 뒤를 '0'으로 채우려면padding='post'
maxlen
의 인자를 정수로 주게되면 해당 정수로 모든 문서의 길이가 동일해짐
model.add(Embedding(vacab_size, 300, weights=[embedding_matrix], input_length=max_len, trainable=False))
- trainable = False
- 훈련과정에서 embedding layer 제외
- 사전 학습된 데이터를 다시 학습하지 않음
연속형 데이터 : 데이터가 배치되는 순서, 즉 특정 데이터 앞 뒤로 어떤 데이터가 오는 지에 따라 각각의 의미가 변경되는 데이터
- 자연어, 시계열 데이터 등
RNN(Recurrent Neural Network, 순환 신경망) : 연속형 데이터를 처리하기 위한 신경망
- 단점 : 기울기 소실(gradient vanishing)로 인한 장기 의존성(long-term dependency) 문제 발생
↓ 개선
LSTM(Long Short Term Memory, 장단기 기억망), GRU(Gated Recurrent Unit)
N-gram
, 스무딩(smoothing)
, 백오프(back-off)
방법 고안 ↓ 극복
Word2Vec
이나 fastText
등의 출력값인 임베딩 벡터 사용 → 말뭉치에 등장하지 않더라도 의미/문법적으로 유사하면 선택class RNN:
def __init__(self, Wx, Wh, b): # Wx : 입력벡터(x)에 곱해지는 가중치, Wh: 은닉 상태 벡터(h_prev: 이전 시점의 은닉상태 벡터)에 곱해지는 가중치, b: 편향
self.params = [Wx, Wh, b]
self.grads = [np.zeros_like(Wx), no.zeors_like(Wh), np.zeros_like(b)] # 가중치 초기화
self.cache = None
def forward(self, x, h_prev):
Wx, Wh, b = self.params
t = np.matnul(h_prev, Wh_ + np.matmul(x, Wx), b
h_next = np.tanh(t)
self.cache = (x, h_prev, h_next)
return h_next
다양한 형태의 RNN
- one-to-many
- 1개의 벡터를 받아 Sequential한 벡터를 반환
- image captioning(이미지를 입력받아 이를 설명하는 문장 생성) 에 사용
- many-to-one
- Sequential 벡터를 받아 1개의 벡터를 반환
- 감성분석
- many-to-many(1)
- Sequential 벡터를 모두 입력받은 뒤 Sequential 벡터 출력
- seq2seq 구조, 기계번역
- many-to-many(2)
- Sequential 벡터를 입력받는 즉시 Sequential 벡터 출력
- 비디오를 프레임별로 분류
LSTM(Long Short Term Memory, 장단기 기억망)
고안된 배경
구조
# Keras 제공 Embedding 층 적용, LSTM 에 dropout 과 recurrent_dropout 적용
model = tf.keras.models.Sequential([
tf.keras.layers.Embedding(max_features, 128),
tf.keras.layers.LSTM(128, dropout=0.2, recurrent_dropout=0.2),
tf.keras.layers.Dense(1, activation='sigmoid')
])
model.compile(loss='binary_crossentropy',
optimizer='adam',
metrics=['accuracy'])
dropout
vsrecurrent_dropout
dropout
: 입력층의 노드 수를 제한recurrent_dropout
: 순환 드롭아웃, 순환층에서 과대적합을 방지하기 위함- RNN 모델 밖에서 dropout을 하면 정보손실이 발생할 수 있어 모델 안에서 인풋 데이터에 대한 노드를 끊어내어 정보손실을 방지함
model.summary()에서 LSTM의 param 개수
Embedding에서의 max_features(input_dim, 단어 갯수) * output_dim
→ 임베딩은 편향이 없기 때문에 더해주지 않음
LSTM의 units 인수
Embedding 레이어 차원수와 꼭 같을 필요는 없음
중소형 모델의 경우 128, 256, 512, 1024 를 주로 사용
배경
모델 구조
디코더에서 전달받은 Hidden-state 벡터를 어떻게 사용하여 단어를 생성할까
병렬화의 문제가 여전히 존재하지만 transfomer 가 해결해줌
어순은 어떻게 결정될까?
관사가 생성되는 과정은?
→ 입력 받는 값이 벡터가 아닌 행렬
Positional encoding(위치 인코딩)
단어의 상대적인 위치 정보를 제공하기 위한 벡터를 만드는 과정
수식
2i : 짝수, 2i+1 : 홀수
→ 계산하면 위치 인코딩 행렬이 생성 → positional encoding 행렬 + Embedding vector 행렬
def get_angles(pos, i, d_model):
# sin, cos 안에 들어갈 수치 구하는 함수
angle_rates = 1 / np.power(10000, (2*(i//2)) / np.float32(d_model))
return pos * angle_rates
def positional_encoding(position, d_model):
# positional encoding 구하는 함수
angle_rads = get_angles(np.arange(position)[:, np.newaxis], np.arrange(d_model)[np.newaxis, :], d_model)
# apply sin to even indices in the array; 2i
angle_rads[:, 0::2] = np.sin(angle_rads[:, 0::2])
#apply cos to odd indices in the array; 2i+1
angle_rads[:, 1::2] = np.cos(angle_rads[:, 1::2])
pos_encoding = angle_rads[np.newaxis, ...]
return tf.cast(pos_encoding, dtype=tf.float32)
Self-Attention
번역하려는 문장 내부 요소의 관계를 잘 파악하기 위해서 문장 자신에 대한 어텐션 메커니즘을 적용하는데 이를 의미
→ 지시대명사 "it" 이 어떤 대상을 가리키는지
기존 Attention 과는 달리 쿼리, 키, 밸류 모두 가중치 벡터
세가지 가중치 벡터를 대상으로 어텐션 적용
def scaled_dot_product_attention(q, k, v, mask):
# Tensorflow 에서 Self-Attention 구현 코드
# q, k, v 의 leading dimension은 동일
# k, v의 penultimation dimension 은 동일, i.e. : seq_len_q, seq_len_k
# Mask는 타입(padding or look ahead)에 따라 다른 차원을 가질 수 있음
# 덧셈시에는 브로드캐스팅 될 수 있어야 함
matmul_qk = tf.matmul(q, k, transpose_b = True) #(..., seq_len_q, seq_len_k)
# matmul_qk(쿼리와 키의 내적)을 dk제곱근으로 나눠줌
dk = tf.cast(tf.shape(k)[-1], tf.float32)
scaled_attention_logits = matmul_qk / ft.math.sqrt(dk)
# 마스킹
if mask is not None:
scaled_attention_logits += (mast * -1e9)
# 소프트 맥스 → attention score
attention_weights = tf.nn.softmax(scaled_attention_logits, axis=1) # (..., seq_len_q, seq_len_k)
output = tf.matmul(attention_weights, v) #(..., seq_len_q, depth_v)
return output, attention_weights
Multi-Head Attention
Add & Norm(Layer Normalization & Skip Connection)
Feed Forward(Feed Forward Neural Network)
def point_wise_feed_forward_network(d_model, dff):
# d_model : 모델의 차원
# dff : 은닉층의 차원 수
return tf.keras.Sequential({
tf.keras.layers.Dense(dff, activation='relu'), # (batch_size, seq_len, dff)
tf.keras.layers.Dense(d_model) # (batch_size, seq_len, d_model)
])
Masked Self(Multi-Head) Attention
디코더 블록에서 사용되는 특수한 Self-Attention
디코더는 Auto-Regressive(왼쪽 단어를 보고 오른쪽 단어를 예측)하게 단어를 생성하기 때문에 타겟 단어 뒤에 위치한 단어는 Self-Attention에 영향을 주지 않도록 마스킹(masking)해야 함
Self-Attention (without Masking) vs Masked Self-Attention
Masking : softmax를 취해주기 전 가려주고자 하는 요소에만 에 해당하는 매우 작은 수를 더해 줌
Encoder-Decoder Attention
Linear → Softmax