[밑바닥부터 시작하는 딥러닝] 자연어와 단어의 분산 표현
본 내용은 밑바닥부터 시작하는 딥러닝 2 도서를 참고하여 작성되었습니다.
밑바닥부터 시작하는 딥러닝 2 - 예스24
직접 구현하면서 배우는 본격 딥러닝 입문서 이번에는 순환 신경망과 자연어 처리다! 이 책은 『밑바닥부터 시작하는 딥러닝』에서 다루지 못했던 순환 신경망(RNN)을 자연어 처리와 시계열 데
www.yes24.com
우리는 이번 장부터 RNN의 세계를 향해 밑바닥부터 차근차근 나아가 볼 것이다.
이번 장에서는 그 첫 번째, 컴퓨터에게 우리가 사용하는 단어를 이해시키기 위한 방법을 이해해보자.
자연어 처리란?
우리가 평소에 쓰는 말을 "자연어" 라고 한다.
그러므로 NLP(Natural Language Processing)란, '자연어를 처리하는 분야'이고, 알기 쉽게 풀어보면 '우리의 말을 컴퓨터에게 이해시키기 위한 기술(분야)'이다.
그러면 프로그래밍 언어, 마크업 언어처럼 이해시키면 되는 것 아닌가? 싶지만,
프로그래밍 언어는 명백한 프로토콜과 규칙이 있다.
이를 "딱딱한 언어"라고 하고, 그와 반대로 자연어는 "부드러운 언어"라고 한다.
부드러운 언어는 엄격한 문법을 지키지 않아도 자연스럽게 이해할 수 있다.
캠릿브지 대학의 연결구과에 따르면, 한 단어 안에서 글자가 어떤 순서로 배되열어 있지는는 중하요지 않고, 첫 번째와 마지막 글자가 올바른 위치에 있는 것이 중다요하고 한다. 나머지 글들자은 완전히 엉진망창의 순서로 되어 있라을지도 당신은 아무 문제 없이 이것을 읽을 수 있다. 왜하냐면, 인간의 두뇌는 모든 글자를 하하나나 읽는 것이 아니라 단어 하나를 전체로 인하식기 때이문다.
우리는 위 문장을 무리없이 이해할 수 있지만, 엄격하게 의미와 알고리즘을 정의하려고 하면 난처함에 빠지게 된다.
NLP는 즉, 이와 같은 "모호함"을 다루기 위해 발달한 분야이다.
좀 더 엄밀하게 들어가보자.
단어의 의미
우리의 말은 '문자'로 구성되며, 말의 의미는 '단어'로 구성된다. 단어는 의미의 최소 단위인 셈이다.
그래서 자연어를 컴퓨터에게 이해시키는 데는 무엇보다 '단어의 의미'를 이해시키는 게 중요하다.
이번 장의 주제는 컴퓨터에게 '단어의 의미' 이해시키기이다. 더 정확히 말하면 '단어의 의미'를 잘 나타내는 표현 방법(자료구조)을 정의하는 것이다.
구체적으로는 다음 세가지 기법을 알아보자.
- 시소러스를 활용한 기법 (이번 장)
- 통계 기반 기법 (이번 장)
- 추론 기반 기법(word2vec, 다음 장)
시소러스
시소러스란, "유의어 사전"을 의미한다.
아래 예시 그림을 보자.
위에서도 보듯, 각 단어에는 이와 유사하거나 같은 의미를 갖는 단어들이 많이 존재한다.
작문 시에 같은 단어를 중복 사용하는 것을 피하는 '동어 반복 회피' 개념 때문에 이와같은 시소러스가 등장하게 되었는데, 이를 20세기부터 NLP로 다루기 시작했다는 것이 흥미로울 따름이다.
여기서 한발 더 나아가 NLP에 이용되는 시소러스에는 단어 사이에 '상위와 하위' 혹은 '전체와 부분' 등 더 세세한 관계까지 정의해둔 경우도 있다.
이를 그림으로 표현하면 아래와 같다.
이와 같은 자료구조를 이용하면 컴퓨터에게 단어의 의미를 이해시키고 활용하는 알고리즘을 작성하기 용이할 것이다.
WordNet
자연어 처리 분야에서 가장 유명한 시소러스는 WordNet이다.
WordNet은 프린스턴 대학교에서 1985년부터 구축하기 시작한 전통 있는 시소러스로, 지금까지 많은 연구와 다양한 자연어 처리 애플리케이션에서 활용되고 있다.
WordNet을 사용하면 유의어를 얻거나 '단어 네트워크'를 이용할 수 있다. 또한 단어 네트워크를 사용해 단어 사이의 유사도도 구할 수 있다.
시소러스의 문제점
근데, WordNet과 같은 시소러스는 사람이 수작업으로 직접 레이블링한 것으로, 이런 방식에는 크나큰 결점이 존재한다.
다음은 시소러스 방식의 대표적인 문제점이다.
시대 변화에 대응하기 어렵다.
언어는 살아 움직인다.
때로는 새로운 단어가 생겨나고, 구닥다리 옛말은 언젠가 잊혀진다.
이러한 변화에 대응하려면 시소러스를 사람이 수작업으로 끊임없이 갱신해야 한다.
사람을 쓰는 비용은 크다.
영어를 예로 들면, 현존하는 영어 단어의 수는 1천만 개가 넘는다고 한다. 따라서 이상적으로는 이 방대한 단어 사이에 연관관계를 맺어야 하는데, 이는 사람 손으로 하기엔 얻는 이득에 비해 비용적 손실이 너무 크다.
단어의 미묘한 차이를 표현할 수 없다.
시소러스에는 뜻이 비슷한 단어들을 묶는다. 그러나 실제로 비슷한 단어들이라도 미묘한 차이가 있는 법이다.
"빈티지"와 "레트로"는 의미가 같지만, 용법은 다르다.
시소러스는 이러한 미묘한 차이를 표현할 수 없다.
(표현하려면 더 많은 비용이 들 것이다.)
이 문제를 피하기 위해, 곧이어 '통계 기반 기법'과 신경망을 사용한 '추론 기반 기법'을 알아볼 것이다.
이 두 기법에서는 대량의 텍스트 데이터로부터 '단어의 의미'를 자동으로 추출한다. 그 덕분에 사람은 손수 단어를 연결짓는 중노동에서 해방되는 것이다.
통계 기반 기법
우리는 이제부터 통계 기반 기법을 살펴보면서 말뭉치(Corpus)를 사용할 것이다.
말뭉치(Corpus)란?
말뭉치는 자연어 처리 연구나 애플리케이션을 염두에 두고 수집된 텍스트 데이터이다.
일반적인 문장 책과 달리, 텍스트 데이터의 단어 각각에 '품사'가 트리 구조 형태로 레이블링될 수 있는게 특징인데, 이번 장에서는 해당 레이블일 이용하지 않고 단순한 텍스트 데이터로 다룰 것이다.
자연어 처리에는 다양한 말뭉치가 사용된다. 유명한 것으로는 위키백과와 구글 뉴스 등의 텍스트 데이터, 셰익스피어의 소설 같은 것들이 있다.
이번 장에서는 우선 문장 하나로 이뤄진 단순한 텍스트 데이터를 이용한 뒤, 차근차근 실용적인 말뭉치도 다뤄보자.
파이썬으로 말뭉치 전처리하기
먼저 아래와 같은 문자열을 정의했다.
이 문자열을 단어별로 끊기 위해, 한가지 전처리를 수행하자.
'.'이 hello와 연결되는 형태를 막기 위해, '.' 앞에 공백을 한 칸 추가했다.
이제 해당 공백을 기준으로 split을 수행할 것이다.
이상적으로는 정규 표현식을 이용하는게 좋겠지만, 문제의 단순화를 위해 건너뛰었다.
그 결과, 다음과 같이 단어들의 집합을 모아둔 배열을 완성했다.
이제 이를 단어 <-> id간 변환을 수행하자.
단어를 텍스트 그대로 조작하면 자료구조 상으로 비효율적이다.
Random Access, 이분탐색 등이 어려워진다.
이것으로 단어 ID와 단어의 대응표가 만들어졌다. 그렇다면 실제 어떤 내용이 담겨있는지 확인해보자.
무사히 잘 담긴 것을 확인할 수 있었다.
이제 주어진 문장을 '단어 ID 목록' 형태로 바꿔보자.
이제 주어진 문자열 말뭉치를 ID값으로 다룰 수 있게 되었다.
이제까지 주어진 메소드들을 하나의 preprocess 함수로 정의해 사용해보자.
정상 동작하는 것을 확인할 수 있었다.
이제 본격적으로 '통계 기반 기법'을 적용해보자.
우리는 이 기법을 통해, 단어를 벡터로 표현할 수 있게 될 것이다.
단어의 분산 표현
우리는 색을 표현할 때, 아래와 같이 표현이 가능하다.
- 코발트 블루
- RGB(0, 73, 140)
여기서 우리가 주목해야 할 점은 RGB값이다.
우리는 위와 같은 고유명사도 사용하지만, 때로는 RGB값이 좀 더 어떤 느낌인지 짐작하기 쉬워진다.
"푸른 빛인데, 초록빛이 은은하게 나는구나!"
또한, 고유명사보다 벡터로 작성된 형태가 컴퓨터가 좀 더 이해하기 쉽다.
이와 같이 각 단어를 "벡터화" 할 수 없을까?
더 정확하게 말하자면, 간결하고 이치에 맞는 벡터 표현을 단어라는 영역에서도 구축할 수 있을까?
이제부터 우리가 원하는 것은 '단어의 의미'를 정확하게 파악할 수 있는 벡터 표현이다.
이를 자연어 처리 분야에서는 단어의 분산 표현(distributional representation)이라고 한다.
분포 가설
자연어 처리에는 단어를 벡터로 표현하는 연구가 수없이 이루어져 왔는데, 그 중요한 기법들의 거의 모두가 단 하나의 간단한 아이디어에 뿌리를 두고 있다.
단어의 의미는 주변 단어에 의해 형성된다
우리가 훈히 문맥, 맥락으로 이해하는 이를 자연어 처리에서는 "분포 가설"(distributional hypothesis) 이라 하며, 단어를 벡터로 표현하는 최근 연구도 대부분 이 가설에 기초한다.
간단하게 예시를 들어 설명하겠다.
- I drink beer.
- We drink wine.
위 문장을 통해, beer와 wine은 "drink 하는 것" 이라는 연결고리 형성이 완료된다.
다시 다른 예시를 보자.
- I guzzle beer.
- We guzzle wine.
위 예시를 통해, "drink 할 수 있는 것은 guzzle 할 수도 있다" 라는 의미의 연결이 가능해진다. (참고로 "guzzle"의 뜻은 '폭음하다' 이다.)
앞으로 '맥락'이라는 단어를 자주 사용할 것이다. 맥락은 주변에 놓인 단어를 가리킨다.
또한 '윈도우 크기'라는 단어를 주변의 단어를 포함할 영역의 크기로 사용할 것이다.
아래 예시를 보고 이해해보자.
동시발생 행렬
이제 분포 가설에 기초해 단어를 벡터로 나타내는 방법을 생각해보자.
주변 단어를 '세어 보는' 방법이 자연스럽게 떠오를 것이다.
이 책에서는 이를 '통계 기반(statistical based) 기법' 이라고 하겠다.
위 문장의 You와 맥락이 연결되어 있는 단어의 빈도를 세어보자.
윈도우 크기는 1로 가정한다.
say가 1번 등장했다.
이를 통해 You라는 단어를 [0, 1, 0, 0, 0, 0, 0] 이라는 벡터로 표현했다.
계속해서 진행해보자,
이렇게 표 하나가 만들어졌다.
이 표의 각 행은 해당 단어를 표현한 벡터가 된다.
이 표를 동시발생 행렬(co-occurrence matrix)이라 한다.
이 표를 만드는 메소드를 정의해두자.
벡터 간 유사도
앞에서 동시발생 행렬을 활용해 단어를 벡터로 표현하는 방법을 알아봤다.
그럼 이제 두 벡터간 유사도를 측정하는 방법을 알아보자.
벡터 사이의 유사도는 다양하게 정의되어 있다.
대표적으로는 벡터의 내적이나 유클리드 거리 등이 있겠지만, 단어 벡터의 유사도를 나타낼 때는 코사인 유사도를 자주 이용한다.
여기서 위 절댓값은 벡터의 크기(L2 노름, 유클리드 거리)를 나타내고, dot은 내적을 의미한다.
실행 결과 0.7이 나왔는데, 절댓값의 최댓값이 1인 것을 감안하면 "유사성이 상당히 높다"라고 할 수 있다.
유사 단어의 랭킹 표시
이제 이 유사도를 이용해, 특정 단어가 주어지면 그 검색어와 비슷한 단어를 유사도 순으로 출력하는 함수를 만들어보자.
이 코드는 다음 순서로 동작한다.
- 검색어의 단어 벡터를 꺼낸다.
- 검색어의 단어 벡터와 다른 모든 단어 벡터와의 코사인 유사도를 각각 구한다.
- 계산한 코사인 유사도 결과를 기준으로 값이 높은 순서대로 출력한다.
여기서 argsort()라는 메소드를 사용했는데, 이는 배열의 원소를 오름차순으로 정렬한 뒤 그 인덱스를 반환한다.
예시로 보면 다음과 같다.
이 메소드를 이용해 you를 검색어로 지정해 유사한 단어들을 출력해보자.
You에 가장 가까운 단어는 goodbye가 됐다.
i가 가까운건 이해가 가는데, goodbye와 hello가 가까운건 납득이 안된다.
이는 현재 말뭉치의 크기가 작다는 것이 원인이니 나중에 개선하기로 하고,
아직 우리는 통계 기반 기법의 '기본'을 맛봤으니, 좀 더 개선할 방법들을 하나씩 배워보자.
통계 기반 기법 개선하기
상호정보량 - PPMI
앞 절에서 본 동시발생 행렬을 구할 때, 우리는 두 단어가 "동시에 발생한 횟수"를 세어 행렬에 더했다.
사실 이 "동시에 발생한 횟수"는 별로 좋은 특징이 아니다. 아래 예를 통해 자세히 알아보자.
말뭉치에서 the와 car의 동시발생을 생각해보자.
분명 "... the car ..." 라는 문구가 자주 보일 것이다. 따라서 두 단어의 동시 발생 횟수는 아주 많을 것이다.
한편, car와 drive는 확실히 관련이 깊다. 하지만 the car와 비교하면 drive는 the에 밀릴 것이다. 이는 알고리즘이 the가 car와 더 자주 등장했다고 판단했기 때문이다.
우리가 원한 결과는 이게 아니다.
이 문제를 해결하기 위해, 아래 지표를 새로 도입해보자.
PMI(Pointwise Mutual Information, 점별 상호정보량)
PMI는 다음 식으로 정의된다.
이를 말로 풀어내면 다음과 같다.
"두 단어가 동시에 등장할 확률 / 한 단어가 각각 등장할 확률을 곱한 것"에 로그를 씌운 것
이제 이 지표를 가지고 위 문제를 다시 풀어보자.
말뭉치의 단어 수가 1만개라고 할 때,
the는 1000회, car는 20회, drive는 10회 등장했다고 하자.
그리고 the와 car의 동시 등장 횟수는 10회,
drive와 car의 동시 등장 횟수는 5회라고 해보자.
위와 같이 drive와 car가 더 강한 연관관계를 갖게 되었다.
로그를 씌우는 이유는 다음과 같다.
- 로그를 취하지 않으면 곱셈 스케일이라 해석이 불편하다.
- 로그를 취하면 → 몇 배 더 자주 나타나는가를 덧셈 형태로 계산 가능하다.
- 정보량 개념으로 해석이 가능하다.
하지만 이 방법의 경우, 동시 등장 횟수가 0이 되면 log0 이 음의 무한대가 되어 산술연산이 어려워지는데, 그래서 나온 개념이 PPMI이다.
PPMI(Positive PMI)
PPMI는 이름에서 알 수 있듯이, PMI와 0 중 최댓값을 사용한다.
즉, PMI의 연산 결과가 음수일 경우, 이를 0으로 보정한다.
이를 구현하면 아래와 같다.
그럼 이제 동시발생 행렬을 PPMI 행렬로 변환해보자.
이로써 you와 i사이의 관계가 hello, goodbye보다 더욱 높은 관계를 갖게 되었다.
차원 감소 - 특잇값 분해
하지만 위 방식에도 아직 문제가 남아있다. 행렬의 원소를 보면 대부분이 0으로 이루어져 있고, 말뭉치의 어휘 수가 증가함에 따라 각 단어 벡터의 차원 수도 똑같이 증가하게 된다.
이 행렬의 내용을 들여다보면 원소 대부분이 0인 것을 알 수 있다.
즉, 벡터의 원소 대부분이 중요하지 않다.
게다가 이런 벡터는 노이즈에 약하고 강건(robust)하지 않다.
이 문제를 해결하기 위한 가장 대중적인 방법은 벡터의 "차원 감소"이다.
차원 감소(dimensionality reduction)는 문자 그대로 벡터의 차원을 줄이는 방법을 말한다.
그러나 단순히 줄이기만 하는 게 아니라, '중요한 정보'는 최대한 유지하면서 줄이는 게 핵심이다.
직관을 위해 아래 그림을 봐보자.
현재 왼쪽의 그림은 각 점이 2차원 데이터의 형태로 나열되어 있다.
하지만 우측 그림은 이 점들의 정보를 단순한 "축까지의 거리"로 표현했고, 축 자체도 의미를 갖도록 변경했다.
이로써 1차원 축만으로도 대부분의 데이터 비교를 수행할 수 있게 되었다.
차원을 감소시키는 방법은 여러가지이지만, 대표적으로 활용하는 방법은 특이값 분해(SVD)이다. 특이값 분해는 임의의 행렬을 세 행렬의 곱으로 분해하며, 수식으로는 다음과 같다.
지금부터 간단하게 이 원리를 따라가보자.
SVD는 기본적으로 기존 행렬을 다음과 같이 분리한다.
- U: m×m 직교행렬 → 데이터의 행 방향 패턴
- S: m×n 대각행렬 → 각 값이 분산, 큰 순서대로 정렬됨, 특이값(σ)이 큼 = 데이터의 중요한 축임을 나타내는 지표가 된다.
- V^t: n×n 직교행렬 → 특징(feature) 방향 패턴
그러므로 아래와 같이 의미없는 데이터를 줄일 수 있다.
- S의 특이값은 데이터 변동(분산)을 나타낸다.
- 큰 특이값은 중요한 패턴을, 작은 특이값은 노이즈나 덜 중요한 정보를 나타낸다.
- 차원 감소 = 큰 특이값 k개만 남기고 나머지 버림.
즉,
가 된다.
그러므로
- Uk: 처음 k개의 열만 사용
- Sk 처음 k개의 특이값만 남긴 대각행렬
- Vk: 처음 k개의 열만 사용
로 중요하지 않은 데이터를 줄이게 된다.
SVD에 의한 차원 감소
이제 위를 코드로 구현해보자.
두번째 열까지의 데이터만을 수집했고, 이를 그래프로 그려보았다.
hello와 goodbye가 가까이 있고, i와 you가 가까이 있다.
이는 더 많은 데이터셋으로 수행한다면 더욱 정확한 결과가 나올 것이다.
이번 장에서 배운 것
- WordNet 등의 시소러스를 이용하면 유의어를 얻거나 단어 사이의 유사도를 측정하는 등 유용한 작업을 할 수 있다.
- 시소러스 기반 기법은 시소러스를 작성하는 데 엄청난 인적 자원이 든다거나 새로운 단어에 대응하기 어렵다는 문제가 있다.
- 현재는 말뭉치를 이용해 단어를 벡터화하는 방식이 주로 쓰인다.
- 최근의 단어 벡터화 기법들은 대부분 '단어의 의미는 주변 단어에 의해 형성된다'는 분포 가설에 기초한다.
- 통계 기반 기법은 말뭉치 안의 각 단어에 대해서 그 단어의 주변 단어의 빈도를 집계한다.(동시발생 행렬)
- 동시발생 행렬을 PPMI 행렬로 변환하고 다시 차원을 감소시킴으로써, 거대한 '희소벡터'를 작은 '밀집벡터'로 변환할 수 있다.
- 단어의 벡터 공간에서는 의미가 가까운 단어는 그 거리도 가까울 것으로 기대된다.