상위 목록: 하위 목록: 작성 날짜: 읽는 데 10 분 소요

단어 표현(Word Representation)

텍스트 전처리와 토큰화를 거치면 문장은 토큰의 나열이 되고, 각 토큰은 어휘 사전에서의 번호로 바뀝니다. 그러나 번호는 계산에 쓸 수 있는 값이 아닙니다. “사과”가 103번이고 “바나나”가 104번이라고 해서 두 단어가 비슷한 것도 아니고, 103 + 1이 바나나라는 뜻도 아닙니다. 번호는 단어를 구분하기 위한 이름표일 뿐, 단어의 성질을 담고 있지 않습니다.

신경망은 입력에 가중치를 곱하고 더하는 방식으로 동작하므로, 입력의 크기와 방향에 의미가 있어야 합니다. 그러므로 토큰을 신경망에 넣으려면 번호를 의미를 담은 숫자의 묶음, 즉 벡터(Vector)로 바꿔야 합니다. 단어를 벡터로 나타내는 방법을 단어 표현(Word Representation)이라 합니다.

단어 표현은 크게 두 갈래로 나뉩니다. 단어 하나에 차원 하나를 배정하고 해당 위치만 표시하는 희소 표현(Sparse Representation)과, 수백 개의 차원에 실수 값을 고르게 채워 단어의 성질을 나눠 담는 밀집 표현(Dense Representation)입니다. 이 강좌에서는 희소 표현에서 출발해, 그 한계를 해결하기 위해 등장한 단어 임베딩(Word Embedding)까지 차례대로 다룹니다.



원-핫 인코딩(One-hot Encoding)

가장 단순한 방법은 어휘 사전의 크기만큼 차원을 만들고, 단어의 번호에 해당하는 위치만 1로, 나머지는 모두 0으로 채우는 것입니다. 이를 원-핫 인코딩(One-hot Encoding)이라 합니다. 어휘가 “사과”, “바나나”, “자동차” 세 개라면 각 단어는 다음과 같이 표현됩니다.

\[\text{사과} = [1, 0, 0], \qquad \text{바나나} = [0, 1, 0], \qquad \text{자동차} = [0, 0, 1]\]


원-핫 벡터는 만들기 쉽고, 서로 다른 단어가 반드시 다른 벡터를 갖는다는 장점이 있습니다. 그러나 실제 어휘 사전은 수만 개의 토큰을 가지므로 벡터 하나의 길이도 수만이 되며, 그중 단 하나만 1이고 나머지는 모두 0입니다. 이처럼 대부분의 값이 0인 벡터를 희소 벡터(Sparse Vector)라 하며, 저장 공간과 연산이 대부분 0을 처리하는 데 낭비됩니다.

더 근본적인 문제는 단어 사이의 관계를 전혀 표현하지 못한다는 점입니다. 두 벡터가 얼마나 비슷한지는 보통 두 벡터가 이루는 각도로 재는데, 서로 다른 원-핫 벡터는 언제나 서로 직각을 이룹니다. “사과”와 “바나나”의 유사도도 0이고, “사과”와 “자동차”의 유사도도 0입니다. 원-핫 인코딩은 결국 번호를 벡터 모양으로 바꿨을 뿐, 번호가 가진 한계를 그대로 물려받습니다.

  • Tip : 원-핫 인코딩은 단어 표현으로는 부족하지만, 분류 모델의 정답 레이블을 나타낼 때는 여전히 널리 쓰입니다. 정답은 여러 범주 중 하나이며 범주 사이의 유사도를 표현할 필요가 없기 때문입니다.



단어 가방(Bag of Words)

단어 하나가 아니라 문서 전체를 벡터로 나타내야 할 때도 있습니다. 가장 직관적인 방법은 문서에 각 단어가 몇 번 등장했는지를 세어, 어휘 사전 크기의 벡터에 그 횟수를 채우는 것입니다. 단어들을 가방에 쏟아 넣고 순서는 버린 채 개수만 센다는 뜻에서 단어 가방(Bag of Words, BoW)이라 부릅니다.

여러 문서의 단어 가방 벡터를 행으로 쌓으면, 행은 문서이고 열은 단어인 표가 만들어집니다. 이를 문서 단어 행렬(Document Term Matrix, DTM)이라 합니다. 다음은 네 개의 짧은 문서로 만든 문서 단어 행렬입니다.

문서 먹고 싶은 사과 바나나 길고 노란 저는 과일이 좋아요
문서 1 : 먹고 싶은 사과 1 1 1 0 0 0 0 0 0
문서 2 : 먹고 싶은 바나나 1 1 0 1 0 0 0 0 0
문서 3 : 길고 노란 바나나 바나나 0 0 0 2 1 1 0 0 0
문서 4 : 저는 과일이 좋아요 0 0 0 0 0 0 1 1 1


단어 가방은 단순하지만 문서의 주제를 꽤 잘 드러냅니다. 스포츠 기사에는 “경기”, “선수”, “득점”이 많이 등장하고, 경제 기사에는 “금리”, “주가”, “성장률”이 많이 등장하므로, 단어의 개수만으로도 문서를 분류할 수 있습니다. 지도 학습(Supervised Learning)에서 다룬 분류 알고리즘이 스팸 필터에 쓰일 때 입력으로 사용한 것이 바로 이러한 벡터입니다.

그러나 이름 그대로 단어의 순서를 버린다는 한계가 있습니다. “개가 사람을 물었다”와 “사람이 개를 물었다”는 조사를 제외하면 같은 단어 가방이 되며, 둘의 뜻이 정반대라는 사실은 표현되지 않습니다. 또한 원-핫 인코딩과 마찬가지로 어휘 크기만큼의 희소 벡터이며, 단어 사이의 유사도도 알 수 없습니다.



TF-IDF

단어 가방의 또 다른 문제는 모든 문서에 흔히 등장하는 단어가 큰 값을 갖는다는 점입니다. 거의 모든 문서에 등장하는 단어는 개수가 많더라도 문서를 구별하는 데 도움이 되지 않습니다. 반대로 특정 문서에만 집중적으로 등장하는 단어는 그 문서의 성격을 잘 드러냅니다.

TF-IDF(Term Frequency - Inverse Document Frequency)는 단어의 등장 횟수에 그 단어가 얼마나 드문지를 곱해 가중치를 조정합니다. 문서 \(d\)에서 단어 \(t\)의 가중치는 다음과 같이 계산합니다.

\[\text{TF-IDF}(t, d) = \text{tf}(t, d) \times \log \frac{N}{\text{df}(t)}\]


\(\text{tf}(t, d)\)는 문서 \(d\)에 단어 \(t\)가 등장한 횟수이고, \(N\)은 전체 문서의 수, \(\text{df}(t)\)는 단어 \(t\)가 등장한 문서의 수입니다. 뒤쪽의 로그 항을 역문서 빈도(Inverse Document Frequency, IDF)라 하며, 단어가 여러 문서에 퍼져 있을수록 작아집니다. 모든 문서에 등장하는 단어라면 \(\log 1 = 0\)이 되어 가중치가 사라집니다.

위의 네 문서에서 문서 3의 “바나나”를 계산해 보겠습니다. “바나나”는 문서 3에 2번 등장하므로 \(\text{tf} = 2\)이고, 네 문서 중 두 문서에 등장하므로 \(\text{df} = 2\)입니다. 따라서 IDF는 \(\log(4/2) \approx 0.693\)이며, TF-IDF는 \(2 \times 0.693 \approx 1.386\)이 됩니다. 한 문서에만 등장하는 “사과”의 IDF는 \(\log(4/1) \approx 1.386\)으로, 두 문서에 등장하는 “바나나”의 두 배입니다.

TF-IDF는 검색 엔진의 문서 순위 계산과 핵심어 추출에서 오랫동안 표준으로 쓰였으며, 지금도 학습 없이 빠르게 동작하는 기준 모델로 널리 사용됩니다. 그러나 여전히 순서를 버리는 희소 표현이라는 점은 단어 가방과 같습니다. “강아지”와 “개”는 TF-IDF에서도 서로 무관한 열입니다.

  • Tip : TF-IDF의 계산식에는 여러 변형이 있습니다. \(\text{df}\)가 0이 되는 것을 막기 위해 분모에 1을 더하거나, \(\text{tf}\)에 로그를 씌워 등장 횟수의 영향을 완만하게 만들기도 합니다. 라이브러리마다 기본 공식이 다르므로 결과 값을 비교할 때는 어떤 공식을 사용했는지 확인해야 합니다.



분포 가설(Distributional Hypothesis)

희소 표현의 공통된 한계는 단어의 의미를 담지 못한다는 것입니다. 그렇다면 컴퓨터는 단어의 의미를 어디에서 알아낼 수 있을까요? 사전을 사람이 직접 만들어 넣는 방법도 있지만, 그 일은 규칙 기반 시대에 이미 한계에 부딪혔습니다.

해답은 언어학의 오래된 관찰에서 나왔습니다. “단어의 의미는 그 단어와 함께 쓰이는 단어들로 알 수 있다”는 생각으로, 이를 분포 가설(Distributional Hypothesis)이라 합니다. 1950년대에 언어학자 젤리그 해리스(Zellig Harris)와 존 퍼스(John Firth)가 정리한 개념이며, 퍼스의 “단어는 그것이 어울리는 친구들로 알 수 있다(You shall know a word by the company it keeps)”라는 문장으로 잘 알려져 있습니다.

예를 들어 처음 보는 단어 “멍뭉이”가 “멍뭉이가 꼬리를 흔들었다”, “멍뭉이에게 사료를 줬다”, “멍뭉이와 산책했다”처럼 쓰인다면, 우리는 그 단어가 개와 비슷한 뜻이라고 짐작할 수 있습니다. “꼬리”, “사료”, “산책”이라는 주변 단어가 “강아지”의 주변 단어와 겹치기 때문입니다. 즉, 비슷한 문맥에서 쓰이는 단어는 비슷한 의미를 가진다는 것입니다.

이 가설은 의미를 사람이 정의하지 않고 말뭉치에서 통계적으로 학습할 수 있다는 길을 열었습니다. 주변 단어의 분포를 벡터로 요약하면, 비슷한 문맥의 단어는 자연스럽게 비슷한 벡터를 갖게 됩니다. 이후의 모든 단어 임베딩 기법은 이 가설을 서로 다른 방식으로 구현한 것입니다.



단어 임베딩(Word Embedding)

단어 임베딩(Word Embedding)은 단어를 수십에서 수백 차원의 실수 벡터로 나타내는 밀집 표현입니다. 원-핫 벡터가 수만 차원 중 한 칸만 1이었다면, 임베딩 벡터는 300차원 정도의 모든 칸이 0이 아닌 실수로 채워져 있습니다. 차원 하나하나가 특정 단어에 대응하지 않고, 여러 차원이 함께 단어의 성질을 나누어 담습니다.

이렇게 만들어진 벡터 공간에서는 의미가 비슷한 단어가 가까운 위치에 놓입니다. 두 벡터의 가까움은 보통 코사인 유사도(Cosine Similarity)로 측정하며, 두 벡터가 이루는 각도의 코사인 값으로 정의합니다.

\[\text{cos}(\mathbf{a}, \mathbf{b}) = \frac{\mathbf{a} \cdot \mathbf{b}}{\lVert \mathbf{a} \rVert \lVert \mathbf{b} \rVert}\]


값은 -1에서 1 사이이며, 방향이 같을수록 1에, 직각일수록 0에 가까워집니다. 설명을 위해 3차원 임베딩을 가정해 “강아지”를 \([0.8, 0.3, 0.1]\), “개”를 \([0.7, 0.4, 0.1]\), “자동차”를 \([0.1, 0.2, 0.9]\)라 하면, “강아지”와 “개”의 코사인 유사도는 약 0.987이고 “강아지”와 “자동차”는 약 0.288입니다. 원-핫 인코딩에서는 두 값이 모두 0이었던 것과 비교하면, 임베딩이 단어 사이의 관계를 담아낸다는 점을 확인할 수 있습니다.

신경망 안에서 임베딩은 임베딩 행렬(Embedding Matrix)로 구현됩니다. 어휘 크기가 \(V\)이고 임베딩 차원이 \(d\)라면, 크기가 \(V \times d\)인 가중치 행렬을 두고 단어 번호에 해당하는 행 하나를 꺼내 그 단어의 벡터로 사용합니다. 이는 원-핫 벡터에 임베딩 행렬을 곱한 것과 수학적으로 같지만, 실제로는 곱셈 없이 행을 바로 꺼내므로 훨씬 빠릅니다. 이 행렬은 처음에 무작위 값으로 초기화되고, 다른 가중치와 마찬가지로 역전파를 통해 학습됩니다.

  • Tip : PyTorch에서는 nn.Embedding(num_embeddings=V, embedding_dim=d)로 임베딩 행렬을 만들며, 토큰 번호의 텐서를 입력하면 각 번호에 해당하는 행을 꺼내 반환합니다. 입력이 정수 번호라는 점에서 일반적인 선형 층과 다릅니다.



Word2Vec

임베딩 행렬은 과제를 학습하는 과정에서 함께 학습될 수도 있지만, 대량의 말뭉치에서 미리 학습해 두고 여러 과제에 재사용할 수도 있습니다. 이 방식을 대중화한 것이 2013년 구글의 토마스 미콜로프(Tomas Mikolov) 연구진이 발표한 Word2Vec입니다.

Word2Vec은 분포 가설을 예측 문제로 바꿉니다. 문장에서 한 단어를 중심으로 앞뒤 몇 개의 단어를 윈도우(Window)로 잡고, 중심 단어와 주변 단어 사이의 예측을 학습합니다. 예측을 잘하려면 비슷한 문맥에서 쓰이는 단어들이 비슷한 벡터를 가져야 하므로, 학습이 끝나면 의미가 비슷한 단어끼리 가까이 모이게 됩니다. 정답이 문장 자체에 들어 있으므로 사람이 레이블을 붙일 필요가 없다는 점이 중요합니다.


CBOW와 Skip-gram

Word2Vec에는 예측의 방향에 따라 두 가지 구조가 있습니다. “나는 아침에 따뜻한 커피를 마셨다”에서 윈도우 크기가 2이고 중심 단어가 “따뜻한”이라고 하겠습니다.

  • CBOW(Continuous Bag of Words) : 주변 단어 “아침에”, “나는”, “커피를”, “마셨다”로 가운데 단어 “따뜻한”을 예측합니다. 주변 단어 벡터의 평균으로 예측하므로 학습이 빠르고, 자주 등장하는 단어를 안정적으로 학습합니다.
  • Skip-gram : 반대로 가운데 단어 “따뜻한” 하나로 주변 단어 각각을 예측합니다. 하나의 중심 단어에서 여러 개의 학습 쌍이 만들어지므로 데이터를 더 많이 활용하며, 드물게 등장하는 단어의 표현이 더 좋은 것으로 알려져 있습니다.


두 구조 모두 신경망은 매우 단순합니다. 입력 단어의 임베딩을 꺼내고, 출력 층에서 어휘 전체에 대한 확률을 계산하는 것이 전부입니다. 학습이 끝나면 출력 층은 버리고 입력 쪽 임베딩 행렬만 남겨 단어 벡터로 사용합니다. 예측 과제는 목적이 아니라 좋은 벡터를 얻기 위한 수단입니다.


네거티브 샘플링(Negative Sampling)

어휘가 수만 개라면 출력 층에서 매번 모든 단어의 확률을 계산하는 것은 매우 비쌉니다. 정답 단어 하나를 위해 수만 개의 점수를 계산하고 소프트맥스로 정규화해야 하기 때문입니다.

네거티브 샘플링(Negative Sampling)은 이 문제를 이진 분류로 바꿔 해결합니다. 실제로 함께 등장한 단어 쌍(“따뜻한”, “커피를”)은 1로, 말뭉치에서 무작위로 뽑은 관계없는 단어 쌍(“따뜻한”, “주차장”)은 0으로 맞히도록 학습합니다. 한 번에 정답 하나와 오답 몇 개만 계산하므로 연산량이 어휘 크기와 무관해집니다. 오답은 등장 빈도가 높은 단어일수록 더 자주 뽑히도록 하되, 빈도를 그대로 쓰지 않고 약간 완만하게 조정한 분포에서 뽑습니다.


벡터 연산

Word2Vec이 큰 관심을 받은 이유 중 하나는 벡터 사이의 차이가 관계를 나타낸다는 점이었습니다. “왕”의 벡터에서 “남자”를 빼고 “여자”를 더하면 그 결과에 가장 가까운 단어가 “여왕”이 되는 식입니다. “서울 - 한국 + 일본”이 “도쿄” 근처에 놓이는 것처럼, 성별이나 수도와 같은 관계가 벡터 공간에서 일정한 방향으로 나타납니다.

다만 이러한 결과는 모든 관계에서 깔끔하게 나타나지는 않으며, 입력에 사용한 단어 자체를 후보에서 제외하는 등 평가 방식에 따라 결과가 달라진다는 지적도 있습니다. 벡터 연산은 임베딩이 의미 구조를 담는다는 것을 보여 주는 흥미로운 증거이지만, 정확한 추론 도구로 받아들여서는 안 됩니다.



GloVe와 FastText

Word2Vec은 윈도우 안의 국소적인 문맥만 보고 학습합니다. 말뭉치 전체에서 두 단어가 얼마나 자주 함께 등장하는지에 대한 전역적인 통계는 직접 사용하지 않습니다.

2014년 스탠퍼드 대학교에서 발표한 GloVe(Global Vectors for Word Representation)는 말뭉치 전체에서 단어 쌍이 함께 등장한 횟수를 먼저 세어 동시 등장 행렬(Co-occurrence Matrix)을 만듭니다. 그런 다음 두 단어 벡터의 내적이 동시 등장 횟수의 로그에 가까워지도록 학습합니다. 전역 통계를 활용하는 행렬 분해 방식과 Word2Vec의 예측 방식의 장점을 결합한 방법으로, 성능은 과제에 따라 Word2Vec과 비슷하거나 조금 앞섭니다.

두 방법 모두 단어를 더 이상 나눌 수 없는 단위로 다루므로, 학습할 때 보지 못한 단어에는 벡터가 없습니다. 2016년 페이스북에서 발표한 FastText는 단어를 글자 단위의 n-gram 조각으로 나누어 이 문제를 해결합니다. 예를 들어 “apple”은 경계 기호를 붙여 <ap, app, ppl, ple, le> 같은 조각으로 나뉘고, 단어 벡터는 조각 벡터들의 합으로 만들어집니다.

조각 단위로 학습하므로, 처음 보는 단어라도 조각이 학습되어 있다면 벡터를 만들 수 있습니다. 또한 어근을 공유하는 단어들이 조각을 공유하므로 형태가 비슷한 단어끼리 비슷한 벡터를 갖습니다. 텍스트 전처리와 토큰화에서 다룬 것처럼 한국어는 어미와 조사가 붙어 형태가 다양하게 변하는 언어이므로, 이러한 조각 기반 방식이 특히 효과적입니다.

  • Tip : FastText의 조각 아이디어는 서브워드 토큰화와 같은 문제의식에서 출발합니다. 다만 서브워드 토큰화는 입력을 조각으로 나누는 방법이고, FastText는 단어 벡터를 조각 벡터로 조립하는 방법이라는 점이 다릅니다.



정적 임베딩의 한계

Word2Vec, GloVe, FastText는 단어마다 하나의 고정된 벡터를 학습합니다. 학습이 끝나면 어떤 문장에서 쓰이든 같은 단어는 같은 벡터로 바뀌므로, 이들을 정적 임베딩(Static Embedding)이라 부릅니다.

문제는 자연어 처리란?에서 다룬 어휘적 중의성(Lexical Ambiguity)입니다. “배가 고프다”의 배와 “배를 타다”의 배는 다른 단어이지만, 정적 임베딩에서는 둘 다 같은 벡터입니다. 이 벡터는 두 의미의 문맥을 평균한 어중간한 위치에 놓이게 되며, 어느 쪽 의미도 정확히 나타내지 못합니다.

또 다른 문제는 임베딩이 말뭉치의 편향을 그대로 학습한다는 점입니다. 말뭉치에서 특정 직업이 특정 성별과 자주 함께 등장했다면, 임베딩 공간에서도 그 직업은 그 성별 쪽에 가깝게 놓입니다. 임베딩은 언어가 실제로 쓰인 방식을 기록한 것이므로, 그 안의 고정관념도 함께 기록됩니다. 이렇게 학습된 임베딩을 채용이나 추천 같은 시스템에 사용하면 편향이 결과로 이어질 수 있습니다.

첫 번째 문제의 해법은 단어 벡터를 문장을 읽은 뒤에 결정하는 것입니다. 같은 “배”라도 주변에 “고프다”가 있으면 음식 쪽으로, “타다”가 있으면 탈것 쪽으로 벡터를 바꾸는 방식으로, 이를 문맥 임베딩(Contextual Embedding)이라 합니다. 문맥 임베딩을 만들려면 문장 전체를 순서대로 읽고 정보를 모으는 구조가 필요하며, 다음 강좌에서 다룰 순환 신경망이 그 첫 번째 시도였습니다.



단어 표현 방식 정리

방식 표현 단어 간 유사도 미등록어 특징
원-핫 인코딩 희소, 어휘 크기 차원 표현 불가 처리 불가 가장 단순, 레이블 표현에 사용
단어 가방 · DTM 희소, 문서 단위 표현 불가 처리 불가 순서를 버리고 개수만 셈
TF-IDF 희소, 문서 단위 표현 불가 처리 불가 드문 단어에 높은 가중치
Word2Vec 밀집, 수백 차원 표현 가능 처리 불가 주변 단어 예측으로 학습
GloVe 밀집, 수백 차원 표현 가능 처리 불가 전역 동시 등장 통계 활용
FastText 밀집, 수백 차원 표현 가능 조각으로 처리 형태 변화가 많은 언어에 유리
문맥 임베딩 밀집, 문장마다 다름 표현 가능 토크나이저에 따름 중의성 해결, 현재의 표준

단어 표현의 발전은 사람이 정한 기준을 데이터에서 학습한 기준으로 바꾸는 과정이었습니다. 원-핫 인코딩과 단어 가방은 단어를 서로 무관한 기호로 보았고, 분포 가설에 기반한 임베딩은 말뭉치에서 단어 사이의 관계를 학습했으며, 문맥 임베딩은 같은 단어라도 문장에 따라 다른 의미를 부여하기에 이르렀습니다.

오늘날의 대규모 언어 모델도 입력의 첫 단계에서는 이 강좌에서 다룬 임베딩 행렬을 그대로 사용합니다. 토큰 번호로 임베딩 행렬의 행을 꺼내는 것은 같고, 그 벡터를 문장 전체의 문맥에 맞게 여러 층에 걸쳐 고쳐 나가는 과정이 덧붙여졌을 뿐입니다.

  • Tip : 미리 학습된 정적 임베딩은 지금도 데이터가 적거나 연산 자원이 제한된 환경에서 유용합니다. 문장 분류처럼 간단한 과제라면 FastText 벡터의 평균만으로도 괜찮은 기준 성능을 빠르게 얻을 수 있습니다.



  • Writer by : 윤대희

댓글 남기기