Open In Colab

희소행렬과 밀집행렬

image.png

우리가 일상적으로 쓰는 단어들을 컴퓨터가 이해하는 것은 힘듭니다. 숫자로 변환하여 입력하여야 하죠.

단어를 숫자로 변환하는 가장 간단한 아이디어는 원-핫 인코딩 입니다. 단어장을 정의하고 단어장 내 단어 개수 만큼 백터 차원을 키운 뒤 해당하는 단어가 나올 때 그 값만 1을 넣어주는 방식입니다.

하지만 이런 방식은 단어집합이 커진다면 하나의 값만 1이고 나머지 값은 0인 고차원 벡터가 됩니다. 공간적으로 낭비가 심합니다.

또 단어간의 유사성을 나타낼 어느 요소도 없습니다. 만들기만 쉽지 실제 활용도는 매우 떨이집니다.

대안으로 나온 것이 밀집행렬입니다. 밀집행렬은 사용자가 설정한 값으로 모든 단어의 벡터 표현의 차원을 맞추고 값도 실수로 채워집니다.

밀집행렬은 단어집합이 커진다고 해서 벡터 차원이 늘어나지 않기 때문에 연산에 보다 효율적입니다.

또 각 차원을 주성분의 관점에서 살펴볼 수 있습니다. 차원 값이 유사한 단어끼리는 유사도가 높다고 할 수 있습니다.

물론 저절로 유사도가 높은 값을 바로 찾는 것은 아닙니다. 학습이 필요합니다. 지금부터 살펴볼 것은 이 학습방법 입니다.

워드투백터(Word2Vec)

앞서 말한 내용을 정리하면, 밀집 행렬은 저차원에 단어의 의미를 여러차원에 분산하여 표현한다는 말 입니다. 이렇게 되면 단어 벡터 간 유의미한 유사도를 계산할 수 있고, 이를 학습해야합니다.

워드투백터(Word2Vec) 방식은 이 학습 방식 중 하나 입니다. 비슷한 문맥에서 등장하는 단어들은 비슷한 의미를 가진다는 것을 먼저 가정합니다.

Word2Vec은 주변에 있는 단어로 중간 단어를 예측하거나 중간에 있는 단어로 주변 단어를 예측하는 방식을 통해 단어 간 연관성을 학습합니다.

쉽게 설명하면 단어들이 같은 문장에 자주 등장할수록 단어 간 유사도가 있다라고 판단하여 학습하는 것 입니다.

주변에 있는 단어로 중간 단어를 예측하는 것을 CBOW, 중간 단어로 주변 단어들을 예측하는 것을 Skip-Gram이라고 하는데 주로 Skip-Gram을 많이 사용합니다.

다만 원리를 이해하는데 CBOW 비교적 수월해서 CBOW 과정을 더 자세히 살펴보겠습니다.

image.png

'나는 이른 아침에 사과를 먹었다' 라는 입력 문장이 있다고 생각해봅시다. 여기서 주변 단어들로 중심단어를 맞추는 모델을 만들겁니다.

어디까지를 주변 단어로 정할 것인가를 윈도우라고 합니다. 윈도우가 2이고 중심단어가 '아침에'라면 나는/이른/사과를/먹었다가 모두 주변단어 입니다.

주변 단어들은 원-핫 인코딩 형식으로 정리가 되어 있을 것입니다. 여기에 가중치 행렬 (단어 집합 크기 * 단어 임베딩 크기)를 곱합니다. 여기서 단어 임베딩 크기는 사용자가 임의로 정합니다.

이 가중치 행렬을 행 단위로 보면 각 단어의 의미가 내포되어 있습니다. 왜냐하면 원-핫 인코딩 된 벡터는 가중치 행렬에서 해당하는 행 벡터만 추출하기 때문입니다.

원-핫 인코딩 벡터와 가중치 행렬 연산을 하게 되면 주변 단어 개수 만큼(윈도우 수 만큼) 값이 나오게 되는데 값들을 모두 평균냅니다. 여기서 주의할 점은 가중치 행렬은 모두 동일한 행렬을 사용합니다.

그렇게 되면 결과물이 단어 임베딩 크기를 같는 벡터 (1 단어 임베딩 크기)가 나오게 됩니다. 여기에 또 다른 가중치 행렬 (단어 임베딩 크기 단어 집합 크기)를 곱하면 초기 원-핫 인코딩 입력 벡터인 1 * N 꼴로 나오게 됩니다.

최종 결과물에 소프트 맥스 함수를 취했을때 원하는 단어 '아침에' 에 해당되는 원-핫 인코딩 벡터가 나오도록 모델을 학습시키는 것 입니다.

이런식으로 학습시키면 문장 내 단어가 붙어있는 비중이 클 수록 유사도가 높아지는 방식으로 모델이 학습됩니다.

여기서 유사도는 처음 나온 가중치 행렬 (단어 집합 크기 * 단어 임베딩 크기)의 행 값이 서로 유사해 지는 것을 의미합니다. 모델을 잘 학습시킨 뒤 이 가중치 행렬의 행 벡터를 각 단어의 M차원의 임베딩 벡터, 밀집 행렬로써 취급합니다.

Skip-Gram 방법은 설명한 CBOW 방법과 반대로 진행하면 됩니다. 중심 단어로 주변 단어를 추론하는 방법으로 진행합니다.

GloVe

앞서 공부한 Word2Vec는 여러가지 장점이 있지만, 사용자가 지정한 윈도우 크기에 성능이 의존된다는 치명적 단점이 있습니다.

이를 극복하기 위해 문장 전체의 정보를 이용하여 단어 임베딩을 한 모델이 Glove 입니다.

image.png

위 표는 문장 내 단어들이 동시에 등장할 확률을 정리한 값입니다. 열 값을 보면 ice와 steam이 있어 두 단어를 비교합니다.

'단단한'의미를 갖는 solid는 아무래도 ice와 같이 등장할 확률이 높고, gas는 steam과 같이 등장할 확률이 높습니다.

ice, steam 과 연관성이 조금씩 있는 water이나, 연관성이 전혀 없는 fashion의 비는 모두 1과 가까운 값을 같게 되는군요.

임베딩한 두 단어벡터의 내적은 전체 말뭉치의 등장 확률이 되도록 하는 것이 Glove 모델입니다. 나름 합리적인 모델입니다.

토큰화

자연어 처리에서 또 중요한 것중 하나가 토큰화 입니다. 토큰화란 문장을 토큰 시퀀스로 나누는 과정인데요.

가장 간단히 생각할 수 있는 토큰화 방법 두 가지로 단어 단위 토큰화와 문자 단위 토큰화가 있습니다.

단어 단위로 토큰화를 할때 단어를 구분하는 기준을 공백으로 한다면 갔었어, 갔었는데요 등 표현이 살짝 바뀌어도 다른 단어로 취급합니다.

학습된 토크나이저(은전한닢 등)을 사용한다면 이 문제를 조금 해결할 수 있겠지만, 어휘 집합 크기가 커지는 문제를 근본적으로 막기 힘듭니다.

문자 단위로 토큰화를 진행한다면 한글 기준 단어집합이 약 1만 2천개로 적절한 수준이고, 미등록 된 토큰이 나올 가능성도 없습니다.

하지만 각 문자 토큰은 의미 있는 단위가 되기 어렵습니다. 단순히 '어' 라는 단어를 보고 어떤 의미인지 유추하기 힘들겠죠.

또 토큰 시퀀스 길이가 너무 길어집니다. 문자 당 하나의 토큰이니 문장 내 문자 개수가 조금만 많아져도 모델로 학습하기 힘듭니다.

BPE

BPE(Byte Pair Encoding), 바이트 페어 인코딩은 원래 정보 압축 알고리즘으로 쓰였으나 최근 자연어 처리 모델에 많이 쓰이는 토큰화 기법입니다.

말뭉치의 모든 문장을 공백으로 나눠줍니다. 이를 프리토크나이즈 라고 합니다.

공백 기준으로 나눠진 단어로 초기 어휘 집합을 만듭니다. 여기까지는 단어 단위 토큰화와 같죠. 또 토큰의 빈도를 새어 정리합니다.

image.png

그 후 초기 어휘 집합에 있는 토큰들을 문자별로 모두 쪼갭니다. 그 후 쪼개진 문자들만을 가지고 다시 어휘 집합을 만듭니다.

예시 단어 기준으로 현재 어휘 집합은 (b, g, h, n, p, s, u) 입니다.

image.png

그 후 토큰을 2개씩 묶은 것의 빈도를 구합니다. 예를 들어 (h, u)는 hug로 10번, hugs로 5번이기 때문에 빈도는 총 15번이 됩니다.

가장 많이 등장한 토큰 쌍은 (u, g) 인데 이 토큰 쌍을 어휘 집합에 넣습니다.

그렇게 되면 어휘 집합은 (b, g, h, n, p, s, u, ug)가 됩니다.

image.png

어휘 집합에 ug 가 추가된 뒤 다시 토큰 쌍의 빈도를 모두 구합니다. 변동이 조금 있죠.

(u, n) 토큰쌍이 가장 많은 빈도를 가지기 때문에 이 토큰 쌍을 어휘 집합 내 넣습니다.

그렇게 되면 어휘 집합은 (b, g, h, n, p, s, u, ug, un)가 됩니다.

image.png

마찬가지로 어휘 집합이 업데이트 된 뒤 토큰 쌍의 빈도를 구합니다. (h, ug) 토큰 쌍의 빈도가 가장 크네요.

이 토큰 쌍을 어휘 집합 내 넣게 됩니다. 이런 식으로 토큰 쌍의 빈도를 계속 구해 어휘 집합을 업데이트 합니다.

어휘 집합 업데이트는 사용자가 지정하는 어휘 집합 크기가 될 때 까지 계속 반복됩니다.

또 중요한 것은 병합 우선순위를 기록하는 것입니다. 먼저 병합된 토큰을 높은 우선순위로 취급하는데, 새로운 단어를 토큰화 하는데 필요합니다.

image.png

앞서 살펴본 단어 중심, 문자 중심 토큰화와 다르게 BPE 방법은 단어 사전 크기를 억제할 수 있고, 정보도 효율적으로 압축할 수 있습니다.

또 분석 대상 언어에 대한 사전 지식이 거의 필요없습니다. 여기서 사용한 지식은 띄어쓰기가 보편적인 단어 구분 기준이 된다는 점 밖에 없습니다.

가장 중요한 장점으로 OOV(Out-Of-Vocabulary) 문제를 완화할 수 있습니다. 단어 사전에 없는 신조어나 오타도 단어를 문자별로 쪼갠뒤 토큰화 하므로 최대한 단어 내 찾을 수 있는 의미를 건저냅니다.

워드피스

앞서 살펴본 BPE를 조금 변형한 것이 워드피스 입니다. BPE는 GPT 모델에 쓰이고, 워드피스는 BERT 모델에 사용됩니다.

BPE는 매번 단어 사전에 넣는 토큰 쌍의 기준을 빈도로 잡았습니다. 직관적으로 당연한 얘기 입니다.

반면 워드피스는 빈도가 아닌 우도를 기준으로 단어 사전에 넣는 토큰 쌍을 결정합니다.

우도를 자세히 설명하면 단순히 같이 많이 나오는 것 보다 두 토큰이 쓰이는 빈도 대비 같이 나오는 비율이 높은 것을 기준으로 잡겠다는 겁니다.

BPE와 또 다른 차이점은 병합 우선순위를 만들지 않습니다. 만약 서브워드 후보가 여러게 포함되어 있을 경우 가장 긴 서브워드를 선택합니다.

BERT 모델 구조

BERT는 2018년 구글에서 발표한 임베딩 모델 입니다. Bidirectional Encoder Representations from Transformers 의 약자 입니다.

앞서 다룬 Word2Vec 등과 가장 큰 차이점은 문맥을 고려했다는 것 입니다.

겉으로 보기에 같은 단어라도 동음이이어일수도 있고, 문맥 내 다소 다른 뜻으로 적용될 수도 있습니다.

이런점까지 고려한것이 문맥을 고려한 자연어 임베딩 BERT입니다. 어떻게 문맥을 고려했는지 살펴보겠습니다.

image.png

BERT는 이전에 살펴본 트랜스포머 모델 중 인코더 부분만 사용합니다. 이중 멀티 헤드 어텐션이 문맥을 파악하는데 큰 역할을 합니다.

멀티 헤드 어텐션을 진행하면 문장의 각 단어를 문장의 다른 모든 단어와 연결해 관계 및 문맥을 고려해 의미를 학습하게 됩니다.

이전 글에 설명한 부분이기 때문에 멀티 헤드 어텐션 관련해서 더 자세히 설명하지는 않겠습니다.

BERT도 여러가지 구조가 있는데 기본 구조는 인코더 레이어 12개, 어텐션 해드 12개, 은닉 유닛 768개를 사용합니다.

image.png

공부하다가 이 부분에서 의문이 많이 들었는데요. 트랜스포머는 이미 임베딩 된 입력 벡터를 입력 값으로 사용했습니다.

하지만 지금 공부하는 것은 임배딩을 하기 위함이라 다소 모순이 있는데, 입력하는 곳에서는 어떤 일이 벌어지고 있는 것일까요?

우선 입력하는 문장을 워드피스 방식을 이용해 토큰화를 합니다. 토큰으로 쪼갠 뒤 단어 사전 내 숫자와 매핑 시킨것을 토큰 임베딩이라고 합니다.

여기서 매핑된 숫자는 (1 * 단어 임베딩 크기) 벡터와 또 다시 매핑되는데요. 이 매핑된 벡터는 단어 사전 개수 만큼 존재하겠죠.

이 벡터를 지속적으로 학습시키는 것이 기본 아이디어입니다. 그래서 여기까지 진행한 것만 보면 Word2Vec와 다를 것이 없습니다.

세그먼트 임베딩은 문장이 2개인 경우 두 문장을 0과 1로 구분해주는 과정이고, 위치 임베딩은 트랜스포머에서 배운 위치 임베딩과 동일합니다.

모델 맨 앞에 이 과정을 모두 합한 임베딩 레이어가 존재합니다. 이렇게 입력값을 임베딩 레이어에 통과시킨 뒤 트랜스포머 인코더를 계속 통과시키게 되고, 인코더 내 멀티 헤드 어텐션 부분에서 문맥까지 파악하는 것입니다.

다시 정리하면 임베딩 레이어에서 토큰 그 자체만 보고 학습이 진행되고, 또 트랜스포머 인코더 부분에서 문맥을 학습하는 것 입니다.

그렇다면 이 모델을 어떤 방식으로 학습시킬까요? 바로 알아봅시다.

BERT 사전학습

image.png

첫번째 BERT 사전학습 방식인 MLM (masked language modeling) 입니다. 입력 토큰 중 일부를 마스킹 한 후 원래 단어를 맞추는 방식이죠.

마스킹 된 토큰 행을 기준으로 출력 값의 행 벡터 (1 * 단어 임베딩 크기)를 피드 포워드 층에 입력하고 소프트 맥스 함수를 씌웁니다.

그렇게 되면 마스크 된 단어가 어느 단어를 가르키는지에 대한 확률이 나오는데 이 부분을 지속적으로 학습하는 것 입니다.

이런식으로 학습하게 되면 모델 내 파라미터들이 모두 업데이트가 되겠죠.

다만 이 BERT 모델로 임베딩 된 것을 추후 사용할 때 마스킹 된 토큰을 입력값으로 사용하지 않는 경우 모델 간 불일치가 벌어집니다.

이 문제를 극복하기 위해 전체 데이터 중 12%는 마스킹, 1.5%는 무작위로 다른 단어 대체, 1.5%는 그대로 단어를 사용합니다.

예시 그림을 보게되면 MASK 토큰 행의 출력값을 가지고 무슨 단어가 올 지 예측해야하고, he 대신 king을 입력값으로 사용한 행의 출력값을 가지고 문장 내 원래 쓰인 he 단어를 예측해야합니다. 또 play 단어는 실제로도 play가 맞기 때문에 play 토큰 행의 출력값을 가지고 다시 play 단어를 예측해야 합니다.

image.png

두번째 BERT 사전학습 방식은 NSP (next sentence prediction) 입니다.

두 문장을 입력하고 두번째 문장이 첫번째 문장의 다음 문장이 맞는지 이진분류 하는 방식입니다.

여기서 맨 앞 CLS 토큰은 문장의 시작임을 알려주는 토큰으로 사실 이전부터 지속적으로 사용했던 토큰입니다.

이 CLS 토큰 행을 기준으로 한 출력 값의 행 벡터는 모든 토큰의 집계 표현을 보유하므로 문장 전체에 대한 표현을 담고 있다고 할 수 있습니다.

그렇기 때문에 CLS 토큰 기준 출력 값의 행 벡터를 사용해 피드포워드 층과 소프트맥스 함수를 통과시켜 이진 분류를 수행합니다.

MLM 방식과 마찬가지로 이 이진 분류가 잘 되게 모델을 계속 학습합니다.

사실 두 사전학습 방식을 설명하기 위해 따로 이미지를 분류했지만, 동시에 이루어지는 프로세스라는 것 또한 기억합시다.

느낀점

자연어 처리 분야를 공부하기 위해 임베딩 부분 학습이 필수입니다. 제 나름대로 학습순서를 정해 공부했는데요.

개인적으로 정말 궁금햇던 부분인데 학습하는데 어렵기도 했고, 질문사항이 생길때 해결하기가 힘들었습니다.

하지만 명쾌하게 이해하고 나니 정말 기분이 좋습니다. 저도 부족하지만 혹시 이해 안되시면 질문 남겨주세요.

다음에는 사전학습 된 BERT 모델을 가지고 파인튜닝하여 다양한 문제를 해결하는 모델을 구현해보겠습니다.