자연어는 인간이 일상적으로 사용하는 언어다. 이런 자연어를 이해하고 처리하는 것을 자연어 처리 NLP 라고 한다. 자연어 처리를 통해 텍스트, 음성 분석과 번역, 요약, 감정 분석 등이 가능하다. 자연어 처리의 기본이 되는 개념 두가지, 토큰화와 벡터화를 알아보도록 하자
토큰화는 텍스트를 나누는 과정이다. 텍스트 데이터를 처리하기 위해 문장을 의미있는 단어로 나누게 되는데, 그 단위는 다양하다. 언어별로 형태가 다르기 때문에 이에 따른 토큰화 방법을 잘 설정해야 한다.
영어의 경우는 단어로 띄어쓰기가 되어 있지만 한국어는 조사가 붙어 있다. "I like apple"은 띄어쓰기를 기준으로 텍스트를 나누면 각각이 최소 단위의 의미를 가지고 있다.
같은 의미의 문장 "나는 사과를 좋아한다"는 띄어쓰기만으로는 의미상의 분류가 어렵다. "나는"은 다시 명사 "나" 와 조사 "-는" 으로 나뉠 수 있다. 따라서 한국어와 같은 공백만으로 분류가 명확하지 않은 언어에서는 형태소 분석기를 사용하여 토큰화 작업을 한다.
문장 토큰화는 텍스트를 문장 단위로 나누는 과정으로 보통 문장 부호를 기준으로 구분한다. 그러나 소수점 등 문장이 아닌 곳에 문장 부호가 사용된 경우가 있다. 이를 걸러 내야 하기 때문에 문장 토큰화에도 모델과 규칙이 필요하다. 한국어에는 KSS가 있다.
컴퓨터는 우리의 언어를 이해할 수 없다. 따라서 컴퓨터가 이해할 수 있는 형태로 언어를 변환할 필요가 있다.
정수 인코딩은 언어를 모델이 이해 가능한 숫자 형태로 변환하는 과정이다. 문장을 토큰화해서 단어, 서브워드, 문자 단위로 나눠 각 토큰에 고유한 정수 ID를 부여한다. 해당 문장은 숫자 시퀀스로 표현될 수 있다.
keras의 Tokenizer는 기본적으로 공백 단위의 토크화와 일부 특수문자를 제거하는 단순 전처리를 지원한다.
문장을 숫자 시퀀스로 표현해보자.
# 문장 토근화
text = "나는 사과를 좋아한다"
tokenizer = Tokenizer()
# 인덱스 학습을 시킨다.
tokenizer.fit_on_texts([text])
# 텍스트를 시퀀스로 반환한다
sequences = tokenizer.texts_to_sequences([text])
sequences # [[1, 2, 3]]
# 단어를 key로 인덱스를 value 로 출력
word_index = tokenizer.word_index
word_index # {'나는': 1, '사과를': 2, '좋아한다': 3}
문서에서 단어 빈도수를 세보자.
docs = [
'원 핫 인코딩은 고차원의 희소 벡터',
'워드 임베딩은 저차원의 밀집 벡터',
'워드 임베딩을 사용하면 언어적 의미 학습이 가능',
]
tokenizer = Tokenizer()
tokenizer.fit_on_texts(docs)
# 단어 카운트
tokenizer.word_counts
# "벡터"와 "워드"는 2이고 나머지는 1
# 문장 카운트
tokenizer.document_count # 3
# 각 단어가 몇개의 문장에 포함되었는지 카운트
tokenizer.word_docs
# "벡터"와 "워드"는 2이고 나머지는 1
#각 단어에 매겨진 인덱스 값
tokenizer.word_index
# 고유한 단어에 인덱스 값이 매핑됨
# 인덱스 값으로 구성된 숫자 시퀀스
sequences = tokenizer.texts_to_sequences(docs)
sequences
# [[3, 4, 5, 6, 7, 1],
# [2, 8, 9, 10, 1],
# [2, 11, 12, 13, 14, 15, 16]]
# 가장 긴 문장 길이에 맞춰 패딩된 시퀀스
max_len = max([len(i) for i in sequences])
padded_sequences = pad_sequences(sequences, max_len)
패딩은 NLP나 시계열 모델에서 길이가 다른 시퀀스를 빈 곳에 0을 채워 길이를 맞춘다. 아래는 패딩된 시퀀스 padded_sequences 다. 길이를 맞추기 위해 빈 곳에는 0이 들어갔다.
array([[ 0, 3, 4, 5, 6, 7, 1],
[ 0, 0, 2, 8, 9, 10, 1],
[ 2, 11, 12, 13, 14, 15, 16]], dtype=int32)
Sequential을 사용하여 아래처럼 한번에 구할 수 있다. Keras의 Tokenizer는 단어 인덱스를 1부터 시작하기 때문에, 인덱스 0은 패딩 값으로 예약된다. 그래서 input_dim에 1을 더했다.
# 모델을 순차적으로 층을 쌓는 구조로 정의
model = Sequential()
word_index = tokenizer.word_index
# 임베딩 층 추가
model.add(Embedding(input_dim=len(word_index) + 1, output_dim=max_len+1))
# 출력 벡터 반환 (패딩된 입력 시퀀스 예측)
model.predict(padded_sequences)
결과 벡터는 아래와 같다.
array([[[-0.00857245, 0.02472864, 0.00420759, -0.03509233,
-0.00070393, -0.02817063, -0.00813448, 0.02640537],
[ 0.04682406, -0.03670243, 0.01014663, ...]],
...
[[ 0.02936871, ... 0.03133484, 0.01352866]]],
dtype=float32)
단어 사전(Vocabulary)은 고유한 토큰을 모아 인덱스를 매핑한 집합을 말한다. 이는 텍스트를 정수 시퀀스로 변환하는 기준이 된다. 위에서 model.predict(padded_sequences) 했던 구조와 같다.
모델이 학습할 때 단어 사전에 없는 단어가 들어온다면 해당 단어는 정수 인코딩이나 임베딩으로 변환할 수 없다. 이 과정에서 발생하는 문제를 OOV(Out-Of-Vocabulary)라고 한다.
이를 해결하기 위해 OOV 전용 토큰으로 처리하거나 단어를 더 작게 쪼개는 방법 등을 사용한다.
언어는 단어 간의 의미적 관계가 존재한다. 모델에도 이런 관계가 반영되게 해야 한다. 벡터화는 텍스트 데이터의 특징을 수치적으로 표현하여 벡터 공간에서 단어 간의 관계를 나타낸다.
벡터화 기법에는 BOW, TF-IDF, Word Embedding, Transformer 기반 임베딩 등이 있으며, 여기에서는 가장 기초적인 BOW와 TF-IDF 기법을 알아본다.
원-핫 인코딩은 vocabulary에 있는 단어들을 고유한 숫자 벡터로 표현한다. 이는 단어간 의미적 관계를 반영하는 기법이 아니다.
원-핫 인코딩은 대부분의 값이 0인 벡터인 Sparse Vector(희소 벡터) 가 자주 나타나 메모리 낭비를 초래한다. Word Embedding 기법은 이러한 단점을 해결한다.
BOW은 컴퓨터는 숫자만 알아듣기 때문에 단어의 입력 순서 즉, 문맥은 중요하지 않다는 가정으로 시작한다. 가방안에 문장의 모든 단어를 넣어 섞고 꺼내면 해당 단어의 빈도수를 알 수 있다.
문장의 구조나 의미를 파악하기 위해서는 단어의 빈도수만으로는 정보가 부족하다. 이러한 단점을 보완한 확장 기법으로 TF-IDF가 있다.
DTM(Document-Term Matrix)은 여러 문서를 행으로 두고, Vocabulary에 포함된 단어를 열로 두어 빈도를 수치로 표현한 문서-단어 행렬이다. 각 칸에는 단어 빈도나 TF-IDF 값 등이 들어가고 문서를 수치화하여 분류, 군집, 주제 모델링에 활용할 수 있다.
특정 문서에서 자주 등장하면서 다른 문서에서는 잘 나타나지 않는 단어일수록 TF-IDF 값이 높아진다.
sklearn의 CountVectorizer와 TfidfVectorizer를 사용하여 DTM과 TF-IDF를 구현해보자.
# 말뭉치 데이터
corpus = [
"Hey, I'm Dana!",
"I love cats.",
"Cats are super cute!",
]
# CountVectorizer 객체 생성
vector = CountVectorizer()
# 코퍼스로부터 각 단어의 등장 빈도수를 기록 (Bag-of-Words 방식)
vector.fit_transform(corpus).toarray()
'''
array([[0, 0, 0, 1, 1, 0, 0],
[0, 1, 0, 0, 0, 1, 0],
[1, 1, 1, 0, 0, 0, 1]])
'''
# TF-IDF 벡터화 객체 생성
tfidfv = TfidfVectorizer().fit(corpus)
# 코퍼스를 TF-IDF 방식으로 벡터화
tfidfv.transform(corpus).toarray()
'''
array([[0. , 0. , 0. , 0.70710678, 0.70710678,
0. , 0. ],
[0., 0.60534851, 0. , 0. , 0. ,
0.79596054, 0.],
[0.52863461, 0.40204024, 0.52863461, 0. , 0., 0. , 0.52863461]])
'''
좋은 내용 잘 읽고 갑니다~정말 유익해요 많은 도움됐습니다^^