728x90
사전 학습 모델
언어 모델(language model)
- 시퀀스에 확률을 부여하는 모델
- 단어 시퀀스를 입력받아 해당 시퀀스가 얼마나 그럴듯한지 확률을 출력하는 모델
- 한국어 말뭉치로 학습한 언어 모델은 자연스러운 한국어 문장에 높은 확률값을 부여함
- 문장에서 i번째로 등장하는 단어를 wi로 표시한다면 n개 단어로 구성된 문장이 해당 언어에서 등장할 확률, 즉 모델의 출력은 P(w1, w2, ..., wn) 처럼 쓸 수 있음
- n개 단어가 동시에 나타날 결합 확률(joint probability)을 의미
- 잘 학습된 한국어 모델이 있다면 P(무모, 운전) 보다는 P(난폭, 운전)이 큰 확률값을 지녀야 함
- "난폭"이 나타난 다음에 "운전"이 나타날 확률을 조건부 확률(conditional probability)라고 함
- P(운전|난폭) = P(난폭, 운전) / P(난폭)
- 조건부 확률을 표기할 때 결과가 되는 사건을 앞에, 조건이 되는 사건을 뒤에 씀
- 결과가 되는 사건(운전)은 조건이 되는 사건(난폭)의 영향을 받아 변함
- 앞선 단어가 "난폭" 이라면 다음 단어로 자연스러운 단어는 선택지가 줄어들기 때문
- 결과가 되는 사건(운전)은 조건이 되는 사건(난폭)의 영향을 받아 변함
- 단어 3개가 동시에 등장할 결합 확률 수식
- P(w1, w2, w3) = P(w1) * P(w2|w1) * P(w3|w1, w2)
- 단어 3개로 구성된 문장이 나타나려면 다음 3가지 사건이 동시에 일어나야 함
- 첫 번째 단어(w1) 등장
- 첫 번째 단어 등장 후 두 번째 단어(w2) 등장
- 첫 번째 단어와 두 번째 단어 등장 후 세 번째 단어(w3) 등장
- 전체 단어 시퀀스가 나타날 확률은 이전 단어들이 주어졌을 때 다음 단어가 등장할 확률의 연쇄
- 이 때문에 언어 모델을 "이전 단어들이 주어졌을 때 다음 단어가 나타날 확률을 구하는 모델"이라고 정의하기도 함
순방향 언어 모델(forward language model)
- 임의의 단어 시퀀스가 해당 언어에서 얼마나 자연스러운지 이해하고 있는 언어모델을 구축하려면 이전 단어들이 주어졌을 때 다음 단어 맞히기로 정해도 목표를 달성할 수 있음
- 위 이미지에서 검은색 단어는 컨텍스트, 주황색 단어는 맞혀야 할 다음 단어
- 문장 앞부터 뒤로, 사람이 이해하는 순서대로 계산하는 모델을 순방향 언어 모델(forward language model)이라고 함
- GPT, ELMo 같은 모델이 이런 방식으로 사전 학습을 수행함
역방향 언어 모델(backward language model)
- 방향만 바뀌었을 뿐이고 다음 단어 맞히기 과정에서 전체 단어 시퀀스가 나타날 확률을 계산할 수 있음
넓은 의미의 언어 모델
- 최근에는 언어 모델을 다음 수식과 같이 정의함
- P(w|context)
- 이는 컨텍스트(주변 맥락 정보)가 전제된 상태에서 특정 단어가 나타날 조건부 확률을 나타냄
- 이렇게 정의된 언어 모델은 단어나 단어 시퀀스로 구성된 컨텍스트를 입력받아 특정 단어가 나타날 확률을 출력
- 컨텍스트와 맞힐 단어를 어떻게 설정하느냐에 따라 다양하게 변형 가능
마스크 언어 모델(masked language model)
- 학습 대상 문장에 빈칸을 만들어 놓고 해당 빈칸에 올 단어로 적절한 단어가 무엇일지 분류하는 과정으로 학습
- BERT가 대표적인 마스크 언어 모델
- 검정색 단어는 컨텍스트, 주황색 단어는 맞혀야 할 타깃 단어
- 마스크 언어 모델은 맞힐 단어를 계산할 때 문장 전체의 맥락을 참고할 수 있다는 것이 장점
- 따라서 마스크 언어 모델에는 양방향성(맞힐 단어 앞뒤를 모두 보는 특성)이 있음
언어 모델의 유용성
- 잘 학습된 언어 모델은 어떤 문장이 자연스러운지 가려낼 수 있어 그 자체로 값어치가 있음
- 학습 대상 언어의 풍부한 맥락을 포함하고 있다는 점도 장점
- 기계 번역, 문법 교정, 문장 생성 등 다양한 태스크를 수행할 수 있음
- 기계 번역 : P(?|You can't be free from death)
- 문법 교정 : P(두시 삼십 이분) > P(이시 서른 두분)
- 문장 생성 : P(?|발 없는 말이)
- 위 이미지는 네이버 댓글 데이터로 학습한 BERT 모델의 계산 결과
- 최근 언어 모델이 주목받는 이유 중 하나는 데이터 제작 비용 때문
- 다음 단어 맞히기나 빈칸 맞히기로 학습 태스크를 구성하면 사람이 일일이 수작업 하지 않아도 많은 학습 데이터를 확보할 수 있음
- 대규모 언어 모델 학습이 가능
- 또 하나의 이유는 전이 학습
- 대량의 말뭉치로 사전학습한 언어 모델을 다운스트림 태스크에 적용하면 적은 양의 데이터로도 성능을 큰 폭으로 올릴 수 있음
- 최근 제안되는 기법들은 사전학습 언어 모델을 바탕으로 할 때가 많음
- 언어 모델의 최종 출력값 또는 중간 출력값(임베딩)으로 다양한 태스크 수행
트랜스포머
- 2017년 구글이 제안한 seq2seq 모델
seq2seq
- 트랜스포머는 seq2seq 과제를 수행하는 모델
- 시퀀스 : 단어 같은 무언가의 나열
- seq2seq : 특정 속성을 지닌 시퀀스를 다른 속성의 시퀀스로 변환하는 작업
- 기계 번역을 예시로 든다면
- 소스 언어의 토큰 시퀀스를 다른 언어의 토큰 시퀀스로 변환하는 과제
- 예) 어제, 카페에, 갔었어, 거기, 사람, 많더라(소스 언어)
- I, went, to, the, cafe, there, were, many, people, there(타깃 언어)
- seq2seq는 소스와 타깃의 길이가 달라도 해당 과제를 수행하는 데 문제가 없음
- 소스 언어의 토큰 시퀀스를 다른 언어의 토큰 시퀀스로 변환하는 과제
인코더와 디코더
- 트랜스포머는 seq2seq 과제 수행에 특화된 모델로써, 임의의 시퀀스를 해당 시퀀스와 속성이 다른 시퀀스로 변환하는 작업이라면 꼭 기계번역이 아니더라도 수행할 수 있음
- 예) 필리핀 앞바다의 한 달 치 기온 데이터로 앞으로 1주일간 하루 단위로 태풍이 발생할지를 맞히는 과제 (기온의 시퀀스 -> 태풍 발생 여부의 시퀀스)
- 인코더는 소스 시퀀스의 정보를 압축해 디코더로 보내는 역할
- 인코더가 소스 시퀀스 정보를 압축하는 과정을 인코딩이라고 함
- 디코더는 인코더가 보내 준 소스 시퀀스 정보를 받아서 타깃 시퀀스를 생성
- 디코더가 타깃 시퀀스를 생성하는 과정을 디코딩 이라고 함
트랜스포머 블록
- 위 이미지는 트랜스포머의 인코더의 반복되는 요소를 나타낸 것
- 이런 구조를 블록 또는 레이어 라고 함
- 트랜스포머의 인코더는 인코더 블록을 수십 개 쌓아서 구성
- 인코더 블록의 구성 요소
- 멀티 헤드 어텐션(multi-head attention)
- 피드포워드 뉴럴 네트워크(feedforward neural network)
- 잔차 연결 및 레이어 정규화
- 디코더 블록도 인코더 블록과 본질적으로 다르지 않지만, 마스크를 적용한 멀티 헤드 어텐션과 인코더가 보내 온 정보와 디코더 입력을 함께 이용해 멀티 헤드 어텐션을 수행하는 모듈이 추가됨
셀프 어텐션(self attention)
- 어텐션은 시퀀스 입력에 수행하는 기계학습 방법의 일종
- 시퀀스 요소 가운데 중요한 요소에 집중하고, 그렇지 않은 요소는 무시해 태스크 수행 성능을 끌어 올림
- 어텐션은 기계 번역 과제에 처음 도입되었음
- 타깃 언어를 디코딩할 때 소스 언어의 단어 시퀀스 가운데 디코딩에 도움이 되는 단어 위주로 취사선택해서 번역 품질을 끌어올릴 수 있음
- 즉, 어텐션은 디코딩할 때 소스 시퀀스 가운데 중요한 요소만 추림
- 셀프 어텐션은 자신에게 수행하는 어텐션 기법
- 입력 시퀀스 가운데 태스크 수행에 의미 있는 요소들 위주로 정보를 추출
특징 및 장점
- 셀프어텐션은 자기 자신에 수행하는 어텐션
- 위 이미지는 "거기"라는 단어가 어떤 의미를 가지는지 계산하는 상황
- 잘 학습된 셀프 어텐션 모델이라면 "거기"에 대응하는 장소는 "카페" 라는 사실을 알아야 함
- 추가로 "거기"는 "갔었어"와도 연관이 있음
- 트랜스포머 인코더 블록 내부에서는 "거기"라는 단어를 인코딩할 때 카페, 갔었어 라는 단어의 의미를 강조해서 반영함
- 셀프 어텐션 수행 대상은 입력 시퀀스 전체
- 예) 어제 - 전체 입력 시퀀스, 카페 - 전체 입력 시퀀스, 갔었어 - 전체 입력 시퀀스 등 모두 어텐션 계산을 수행함
- 개별 단어와 전체 입력 시퀀스를 대상으로 어텐션 계산을 수행해 문맥 전체를 고려하므로 지역적인 문맥만 보는 CNN보다 강점이 있음
- 모든 경우의 수를 고려하기 때문에 시퀀스의 길이가 길어져도 정보를 잊거나 왜곡할 우려도 없으므로 RNN의 단점도 극복
- 어텐션과 셀프 어텐션의 주요 차이
- 어텐션은 소스 시퀀스 전체 단어들과 타깃 시퀀스 단어 하나 사이를 연결
- 셀프 어텐션은 입력 시퀀스 전체 단어들 사이를 연결
- 어텐션은 RNN 구조 위에서 동작하지만 셀프 어텐션은 RNN 없이 동작
- 타깃 언어의 단어를 1개 생성할 때 어텐션은 1회 수행하지만, 셀프 어텐션은 인코더, 디코더 블록의 개수만큼 반복 수행
계산 예시
- 셀프 어텐션은 쿼리(query), 키(key), 밸류(value) 3가지 요소가 서로 영향을 주고받는 구조
- 각 단어 벡터는 블록 내에서 쿼리, 키, 밸류 3가지로 변환
- 만약 트랜스포머 블록에 입력되는 문장이 6개 단어로 구성되어 있다면 이 블록 셀프 어텐션 계산 대상은 쿼리 벡터 6개, 키 벡터 6개, 밸류 벡터 6개로 모두 18개가 됨
- 셀프 어텐션 모듈은 위와 같은 결과에 밸류 벡터들을 가중합 하는 방식으로 계산
- Z카페 = 0.1 * V어제 + 0.1 * V카페 + ...... + 0.1 * V많더라
- 모든 단어에 대해 셀프 어텐션을 각각 수행한 후 그 결과를 다음 블록으로 전달
셀프 어텐션 동작 원리
import torch
import numpy as np
from torch.nn.functional import softmax
(1) 쿼리, 키, 밸류 생성
# 입력 벡터 시퀀스 x
x = torch.tensor([
[1, 0, 1, 0],
[0, 2, 0, 2],
[1, 1, 1, 1]
], dtype = torch.float64)
# 3개의 단어를 4개의 숫자로 표현
x
tensor([[1., 0., 1., 0.],
[0., 2., 0., 2.],
[1., 1., 1., 1.]], dtype=torch.float64)
- 인코더에서 수행되는 셀프 어텐션의 입력은 이전 인코더 블록의 출력 벡터 시퀀스
- 임베딩 차원 수가 4이고, 인코더에 입력된 단어 개수가 3일 경우 셀프 어텐션 입력은 위의 x와 같은 형태가 됨
- 4차원짜리 단어 임베딩이 3개 모인 형태
- 셀프 어텐션은 쿼리, 키, 밸류 3개 요소 사이의 문맥적 관계성을 추출하는 과정
- Q = X * WQ
- K = X * WK
- V = X * WV
- 위의 수식처럼 입력 벡터 시퀀스(X)에 쿼리, 키, 밸류를 만들어주는 행렬(WQ, WK, WV)을 각각 곱함
w_query = torch.tensor([
[1, 0, 1],
[1, 0, 0],
[0, 0, 1],
[0, 1, 1]
], dtype = torch.float64)
w_key = torch.tensor([
[0, 0, 1],
[1, 1, 0],
[0, 1, 0],
[1, 1, 0]
], dtype = torch.float64)
w_value = torch.tensor([
[0, 2, 0],
[0, 3, 0],
[1, 0, 3],
[1, 1, 0]
], dtype = torch.float64)
querys = torch.matmul(x, w_query)
querys
tensor([[1., 0., 2.],
[2., 2., 2.],
[2., 1., 3.]], dtype=torch.float64)
keys = torch.matmul(x, w_key)
keys
tensor([[0., 1., 1.],
[4., 4., 0.],
[2., 3., 1.]], dtype=torch.float64)
values = torch.matmul(x, w_value)
values
tensor([[1., 2., 3.],
[2., 8., 0.],
[2., 6., 3.]], dtype=torch.float64)
- w_query, w_key, w_value 는 태스크를 가장 잘 수행하는 방향으로 학습됨
(2) 셀프 어텐션 출력값 계산
- 셀프 어텐션의 정의
- 쿼리와 키벡터에 전치를 취한 행렬을 행렬곱한 뒤, 해당 행렬의 모든 요솟값을 키 차원 수의 제곱근으로 나누고, 이 행렬을 행 단위로 소프트맥스를 취해 스코어 행렬로 만들고 이 스코어 해렬에 밸류를 행렬곱
keys.T
tensor([[0., 4., 2.],
[1., 4., 3.],
[1., 0., 1.]], dtype=torch.float64)
attn_scores = torch.matmul(querys, keys.T)
attn_scores
tensor([[ 2., 4., 4.],
[ 4., 16., 12.],
[ 4., 12., 10.]], dtype=torch.float64)
- [2, 4, 4] 에서 2는 첫 번째 쿼리와 첫 번째 키 벡터 사이의 문맥적 관계성이 결합된 결과물
- 두 번째 요솟값(4)은 첫 번째 쿼리 벡터와 두 번째 키 벡터의 문맥적 관계성이 포함돼 있음
keys.shape # (3개의 단어) (차원:하나의 단어를 3개의 숫자로 표현)
torch.Size([3, 3])
# 소프트맥스 확률값
key_dim_sqrt = np.sqrt(keys.shape[-1])
attn_probs = softmax(attn_scores / key_dim_sqrt, dim = 1) # dim: numpy의 axis
attn_probs
tensor([[1.3613e-01, 4.3194e-01, 4.3194e-01],
[8.9045e-04, 9.0884e-01, 9.0267e-02],
[7.4449e-03, 7.5471e-01, 2.3785e-01]], dtype=torch.float64)
# 소프트맥스 확률과 밸류를 가중함
weighted_values = torch.matmul(attn_probs, values)
weighted_values
tensor([[1.8639, 6.3194, 1.7042],
[1.9991, 7.8141, 0.2735],
[1.9926, 7.4796, 0.7359]], dtype=torch.float64)
- 셀프 어텐션의 학습 대상은 쿼리, 키, 밸류를 만드는 가중치 행렬(WQ, WK, WV)임
멀티 헤드 어텐션(Multi-Head Attention)
- 셀프 어텐션을 동시에 여러 번 수행
- 위 이미지에서 입력 단어 수는 2개, 밸류의 차원은 3, 헤드는 8개인 멀티 헤드 어텐션을 나타냄
- 개별 헤드의 셀프 어텐션 수행 결과는 "입력 단어 수 * 밸류 차원 수"
- 위 이미지에서는 2 * 3 크기를 갖는 행렬
- 8개 헤드의 셀프 어텐션 수행 결과를 이어 붙이면 2 * 24의 행렬이 됨
- 개별 헤드의 셀프 어텐션 수행 결과는 "입력 단어 수 * 밸류 차원 수"
- 멀티 헤드 어텐션은 개별 헤드의 셀프 어텐션 수행 결과를 이어 붙인 행렬에 WO를 행렬곱해서 마무리
- WO의 크기는 "셀프 어텐션 수행 결과 행렬의 열 수 * 목표 차원 수"
GPT 구조
- GPT는 트랜스포머에서 인코더를 제외하고 디코더만 사용
- 인코더 쪽에서 보내오는 정보를 받는 모듈(멀티 헤드 어텐션)도 제거되어 있음
- 입력 단어 시퀀스가 "어제 카페 갔었어 거기 사람 많더라" 이고, 이번이 카페를 맞혀야 되는 상황이라면, GPT는 "어제"라는 단어만 참고할 수 있음
BERT
- 트랜스포머에서 디코더를 제외하고 인코더만 사용
- 입력 단어 시퀀스가 "어제 카페 갔었어 [MASK] 사람 많더라"라면 마스크 토큰 앞뒤 문맥을 모두 참고할 수 있음
- 앞뒤 정보를 준다고 해서 정답을 미리 알려주는 것이 아님
사전 학습 모델로 단어/문장을 벡터로 변환
파인튜닝
- 사전학습을 마친 언어 모델 위에 작은 모듈을 조금 더 쌓아 태스크를 수행하는 구조
- 사전학습을 마친 BERT와 그 위의 작은 모듈을 포함한 전체 모델을 다운스트림 데이터로 업데이트하는 과정을 파인튜닝 이라고 함
from transformers import BertTokenizer, BertConfig, BertModel
import torch
# 토크나이저 선언
# https://huggingface.co/beomi/kcbert-base
tokenizer = BertTokenizer.from_pretrained(
"beomi/kcbert-base",
do_lower_case = False, #알파벳 소문자 변환을 하지않고 학습을 했으면 False
)
tokenizer_config.json: 0%| | 0.00/49.0 [00:00<?, ?B/s]
vocab.txt: 0%| | 0.00/250k [00:00<?, ?B/s]
config.json: 0%| | 0.00/619 [00:00<?, ?B/s]
- 모델 초기화
- 사용대상 BERT 모델이 사전학습할 때 썼던 토크나이저를 그대로 사용해야 벡터 변환에 문제가 없음
- 모델과 토크나이저의 토큰화 방식이 다르면 모델이 엉뚱한 결과를 출력
# BERT 모델을 사전학습할 때 설정했던 설정값
pretrained_model_config = BertConfig.from_pretrained(
"beomi/kcbert-base"
)
# 위의 설정에 따라 모델 전체를 초기화한 뒤 미리 학습된 kcbert-base 체크포인트를 읽어들임
model = BertModel.from_pretrained(
"beomi/kcbert-base",
config = pretrained_model_config
)
model.safetensors: 0%| | 0.00/438M [00:00<?, ?B/s]
pretrained_model_config
BertConfig {
"architectures": [
"BertForMaskedLM"
],
"attention_probs_dropout_prob": 0.1,
"classifier_dropout": null,
"directionality": "bidi",
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 768,
"initializer_range": 0.02,
"intermediate_size": 3072,
"layer_norm_eps": 1e-12,
"max_position_embeddings": 300,
"model_type": "bert",
"num_attention_heads": 12,
"num_hidden_layers": 12,
"pad_token_id": 0,
"pooler_fc_size": 768,
"pooler_num_attention_heads": 12,
"pooler_num_fc_layers": 3,
"pooler_size_per_head": 128,
"pooler_type": "first_token_transform",
"position_embedding_type": "absolute",
"transformers_version": "4.51.3",
"type_vocab_size": 2,
"use_cache": true,
"vocab_size": 30000
}
# 입력값 만들기
sentences = ["안녕하세요", "하이"]
features = tokenizer(
sentences,
max_length = 10,
padding = "max_length",
truncation = True
)
features
{'input_ids': [[2, 19017, 8482, 3, 0, 0, 0, 0, 0, 0], [2, 15830, 3, 0, 0, 0, 0, 0, 0, 0]], 'token_type_ids': [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], 'attention_mask': [[1, 1, 1, 1, 0, 0, 0, 0, 0, 0], [1, 1, 1, 0, 0, 0, 0, 0, 0, 0]]}
- input_ids 는 2개의 입력 문장에 대해 워드피스 토큰화를 수행한 뒤 이를 토큰 인덱스로 변환한 결과
# BERT 로 단어/문장 수준 벡터 구하기
features = {k : torch.tensor(v) for k, v in features.items()}
features
{'input_ids': tensor([[ 2, 19017, 8482, 3, 0, 0, 0, 0, 0, 0],
[ 2, 15830, 3, 0, 0, 0, 0, 0, 0, 0]]),
'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]),
'attention_mask': tensor([[1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 0, 0, 0, 0, 0, 0, 0]])}
# 임베딩 계산
outputs = model(**features)
outputs
BaseModelOutputWithPoolingAndCrossAttentions(last_hidden_state=tensor([[[-0.6969, -0.8248, 1.7512, ..., -0.3732, 0.7399, 1.1907],
[-1.4803, -0.4398, 0.9444, ..., -0.7405, -0.0211, 1.3064],
[-1.4299, -0.5033, -0.2069, ..., 0.1285, -0.2611, 1.6057],
...,
[-1.4406, 0.3431, 1.4043, ..., -0.0565, 0.8450, -0.2170],
[-1.3625, -0.2404, 1.1757, ..., 0.8876, -0.1054, 0.0734],
[-1.4244, 0.1518, 1.2920, ..., 0.0245, 0.7572, 0.0080]],
[[ 0.7565, -1.7148, 2.1903, ..., -0.4291, 0.9469, 0.7394],
[ 1.1048, -1.3408, 1.9444, ..., 0.3340, 0.2276, -0.5220],
[-0.1316, -0.6982, 1.3086, ..., 0.4448, 0.0836, -0.6280],
...,
[ 0.0666, -0.7225, 1.8425, ..., 1.2435, 1.6184, 0.1339],
[ 0.1781, -0.7823, 1.6892, ..., 1.1230, 1.5262, 0.1855],
[-0.0183, -0.8602, 2.5182, ..., 0.9201, 0.3697, -0.7420]]],
grad_fn=<NativeLayerNormBackward0>), pooler_output=tensor([[-0.1594, 0.0547, 0.1101, ..., 0.2684, 0.1596, -0.9828],
[-0.9349, 0.1990, -0.0390, ..., 0.5419, 0.0375, -0.9960]],
grad_fn=<TanhBackward0>), hidden_states=None, past_key_values=None, attentions=None, cross_attentions=None)
# 문장 2개에 속한 각각의 토큰이 768차원의 벡터로 변환
# 단어 수준 벡터 시퀀스
outputs.last_hidden_state.shape
torch.Size([2, 10, 768])
# 문장 수준 벡터
outputs.pooler_output.shape
torch.Size([2, 768])
728x90
'09_DL(Deep_Learning)' 카테고리의 다른 글
34_XAI(설명 가능한 인공지능) (2) | 2025.05.11 |
---|---|
33_bert_문서분류 (2) | 2025.05.11 |
31_트랜스포머_토큰화 (3) | 2025.05.11 |
30_LLM의 개념 (1) | 2025.05.08 |
29_개체명 인식(양방향LSTM) (1) | 2025.05.08 |