
Dense Embedding은 데이터의 의미를 고차원 공간에 압축해 표현하는 방법으로, 유사도 측정에 있어 Dense Embedding은 매우 중요한 역할을 한다. 여기서는 Dense Embedding의 개념과 이를 이용한 유사도 측정 방법에 대해 설명한다.
Dense Embedding은 고차원의 벡터로, 문장이나 단어, 이미지와 같은 데이터를 수치화하여 나타낸 것이다. 이러한 벡터는 딥러닝 모델을 통해 학습되며, 데이터 간의 의미적 유사도를 잘 반영하도록 설계된다. 예를 들어, '고양이'와 '동물'이라는 단어는 비슷한 의미를 가지기 때문에, Dense Embedding으로 표현했을 때 이 두 단어의 벡터는 서로 가까운 위치에 있을 것이다.
Dense Embedding을 사용해 유사도를 측정할 때 가장 많이 활용되는 방법은 코사인 유사도(Cosine Similarity)이다. 코사인 유사도는 두 벡터 간의 각도를 측정하여 얼마나 비슷한 방향을 가리키고 있는지를 평가한다. 수식으로는 다음과 같이 정의된다:
여기서:
코사인 유사도의 값은 -1에서 1 사이에 있으며, 1에 가까울수록 두 벡터의 방향이 매우 비슷함을 의미하고, 0에 가까울수록 직교에 가깝고, -1에 가까울수록 반대 방향을 가리킨다.
또 다른 방법으로는 유클리드 거리(Euclidean Distance) 또는 점수 함수(Score Function) 등을 활용할 수 있다. 유클리드 거리는 두 벡터 사이의 직선 거리를 계산하여 유사도를 측정하는 방식이며, 거리가 짧을수록 두 데이터가 더 유사하다고 볼 수 있다. 반면 점수 함수는 특정 목적을 위해 설계된 함수로, 예를 들어 검색엔진에서 쿼리와 문서의 관련성을 평가할 때 사용될 수 있다.
Dense Embedding의 특징은 고차원 공간에서 데이터를 압축하고, 의미적 유사성을 잘 반영한다는 것이다. 이는 기존의 Bag of Words와 같은 Sparse Representation 방식과 달리, 단어의 순서나 문맥을 잘 반영하여 유사도 측정의 정밀도를 높여준다. 특히 BERT와 같은 사전학습된 언어 모델을 통해 얻어진 Dense Embedding은 문장의 의미를 깊이 있게 표현하며, 이로 인해 보다 정밀한 유사도 계산이 가능해졌다.
[Input]
from datasets import load_dataset
dataset = load_dataset("squad_kor_v1")
[Input]
from transformers import AutoTokenizer
model_checkpoint = "BM-K/KoSimCSE-roberta-multitask"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
tokenized_input = tokenizer(dataset['train'][0]['context'], padding="max_length", truncation=True)
decoded_tokens = tokenizer.decode(tokenized_input['input_ids'])
print(decoded_tokens)
[output]
>> [CLS] 1839년 바그너는 괴테의 파우스트을 처음 읽고 그 내용에 마음이 끌려 이를 소재로 해서 하나의 교향곡을 쓰려는 뜻을 갖는다. 이 시기 바그너는 1838년에 빛 독촉으로 산전수전을 다 [UNK] 상황이라 좌절과 실망에 가득했으며 메피스토펠레스를 만나는 파우스트의 심경에 공감했다고 한다. 또한 파리에서 아브네크의 지휘로 파리 음악원 관현악단이 연주하는 베토벤의 교향곡 9번을 듣고 깊은 감명을 받았는데, 이것이 이듬해 1월에 파우스트의 서곡으로 쓰여진 이 작품에 조금이라도 영향을 끼쳤으리라는 것은 의심할 여지가 없다. 여기의 라단조 조성의 경우에도 그의 전기에 적혀 있는 것처럼 단순한 정신적 피로나 실의가 반영된 것이 아니라 베토벤의 합창교향곡 조성의 영향을 받은 것을 볼 수 있다. 그렇게 교향곡 작곡을 1839년부터 40년에 걸쳐 파리에서 착수했으나 1악장을 쓴 뒤에 중단했다. 또한 작품의 완성과 동시에 그는 이 서곡 ( 1악장 ) 을 파리 음악원의 연주회에서 연주할 파트보까지 준비하였으나, 실제로는 이루어지지는 않았다. 결국 초연은 4년 반이 지난 후에 드레스덴에서 연주되었고 재연도 이루어졌지만, 이후에 그대로 방치되고 말았다. 그 사이에 그는 리엔치와 방황하는 네덜란드인을 완성하고 탄호이저에도 착수하는 등 분주한 시간을 보냈는데, 그런 바쁜 생활이 이 곡을 잊게 한 것이 아닌가 하는 의견도 있다. [SEP] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD]
[Input]
from tqdm import tqdm, trange
import argparse
import random
import torch
import torch.nn.functional as F
from transformers import BertModel, BertPreTrainedModel, AdamW, TrainingArguments, get_linear_schedule_with_warmup
torch.manual_seed(42)
torch.cuda.manual_seed(42)
np.random.seed(42)
random.seed(42)
[Input]
sample_idx = np.random.choice(range(len(dataset['train'])), 128)
training_dataset = dataset['train'][sample_idx]
from torch.utils.data import (DataLoader, RandomSampler, TensorDataset)
q_seqs = tokenizer(training_dataset['question'], padding="max_length", truncation=True, return_tensors='pt')
p_seqs = tokenizer(training_dataset['context'], padding="max_length", truncation=True, return_tensors='pt')
train_dataset = TensorDataset(p_seqs['input_ids'], p_seqs['attention_mask'], p_seqs['token_type_ids'],
q_seqs['input_ids'], q_seqs['attention_mask'], q_seqs['token_type_ids'])
✅ training_dataset은 dataset['train']에서 random으로 128개의 샘플만 추출하여 사용하고 question과 context를 토큰화해서 하나의 데이터셋에 담는다.
[Input]
class BertEncoder(BertPreTrainedModel):
def __init__(self, config):
super(BertEncoder, self).__init__(config)
self.bert = BertModel(config)
self.init_weights()
def forward(self, input_ids, attention_mask=None, token_type_ids=None):
outputs = self.bert(input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids)
pooled_output = outputs[1]
return pooled_output
p_encoder = BertEncoder.from_pretrained(model_checkpoint)
q_encoder = BertEncoder.from_pretrained(model_checkpoint)
if torch.cuda.is_available():
p_encoder.cuda()
q_encoder.cuda()
✅ BertPreTrainedModel Class를 상속받아 CLS 토큰에 해당하는 출력층 토큰 outputs[1]을 사용한다.
outputs[1] corresponds to the pooled output, which is derived from the [CLS] token.
In the outputs returned by BertModel, outputs[0] is the last hidden state (representations of all tokens in the sequence), and outputs[1] is the pooled output (the representation of the [CLS] token after a pooling operation).
[Input]
def train(args, dataset, p_model, q_model):
# Dataloader
train_sampler = RandomSampler(dataset)
train_dataloader = DataLoader(dataset, sampler=train_sampler, batch_size=args.per_device_train_batch_size)
no_decay = ['bias', 'LayerNorm.weight']
optimizer_grouped_parameters = [
{'params': [p for n, p in p_model.named_parameters() if not any(nd in n for nd in no_decay)], 'weight_decay': args.weight_decay},
{'params': [p for n, p in p_model.named_parameters() if any(nd in n for nd in no_decay)], 'weight_decay': 0.0},
{'params': [p for n, p in q_model.named_parameters() if not any(nd in n for nd in no_decay)], 'weight_decay': args.weight_decay},
{'params': [p for n, p in q_model.named_parameters() if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
]
✅ weight_decay를 사용하지 않을 부분을 지정한다.
✅ Weight decay를 적용하지 않는 이유:
Bias 파라미터: 모델의 편향 학습에 중요하며, 불필요한 규제로 인해 성능 저하를 방지하기 위해 제외.
LayerNorm.weight 파라미터: 입력 데이터의 스케일 조절에 필요하며, weight decay로 인해 정규화 효과가 감소하는 것을 방지하기 위해 제외.
optimizer = AdamW(optimizer_grouped_parameters, lr=args.learning_rate, eps=args.adam_epsilon)
t_total = len(train_dataloader) // args.gradient_accumulation_steps * args.num_train_epochs
scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=args.warmup_steps, num_training_steps=t_total)
# Start training!
global_step = 0
p_model.zero_grad()
q_model.zero_grad()
torch.cuda.empty_cache()
train_iterator = trange(int(args.num_train_epochs), desc="Epoch")
for _ in train_iterator:
epoch_iterator = tqdm(train_dataloader, desc="Iteration")
for step, batch in enumerate(epoch_iterator):
q_encoder.train()
p_encoder.train()
if torch.cuda.is_available():
batch = tuple(t.cuda() for t in batch)
p_inputs = {'input_ids': batch[0],
'attention_mask': batch[1],
'token_type_ids': batch[2]
}
q_inputs = {'input_ids': batch[3],
'attention_mask': batch[4],
'token_type_ids': batch[5]}
p_outputs = p_model(**p_inputs) # (batch_size, emb_dim)
q_outputs = q_model(**q_inputs) # (batch_size, emb_dim)
# Calculate similarity score & loss
sim_scores = torch.matmul(q_outputs, torch.transpose(p_outputs, 0, 1)) # (batch_size, emb_dim) x (emb_dim, batch_size) = (batch_size, batch_size)
# target: position of positive samples = diagonal element
targets = torch.arange(0, args.per_device_train_batch_size).long()
if torch.cuda.is_available():
targets = targets.to('cuda')
sim_scores = F.log_softmax(sim_scores, dim=1)
loss = F.nll_loss(sim_scores, targets)
loss.backward()
optimizer.step()
scheduler.step()
q_model.zero_grad()
p_model.zero_grad()
global_step += 1
torch.cuda.empty_cache()
return p_model, q_model
✅ Checkpoint!
sim_scores: question의 임베딩과 context의 유사도를 내적을 사용하여 구한다. sim_scores[i][j]는 i번째 질문과 j번째 문서 사이의 유사도 점수를 나타낸다.
targets: 0부터 batch_size-1까지의 정수 시퀀스이다. 즉, 각 데이터 샘플의 정답 인덱스를 의미한다. 대각선 요소(sim_scores[i][i])는 각 질문과 그에 해당하는 정답 문서의 유사도 점수이기 때문이다.
loss: 로그 확률인 예측된 유사도와 정답 인덱스 사이의 nll_loss를 구한다.
✅
nll_loss란?
모델이 정답 문서에 높은 확률(즉, 높은 로그 확률)을 할당할수록 손실 값이 작아진다. 인자로 들어간 소프트맥스 함수는 확률 분포를 생성하므로, 정답 클래스의 확률을 높이면 다른 클래스들의 확률은 자연스럽게 낮아진다.
✅ negative sampling과 in-batch negative
🔹전통적인 negative sampling:
전체 데이터셋에서 임의로 네거티브 샘플을 추출하여 모델 학습에 사용. 주로 대규모 클래스나 어휘 집합을 처리할 때 계산량을 줄이기 위해 사용.
🔹In-batch negative:
배치 내의 다른 문서들을 네거티브 샘플로 사용합.
별도로 네거티브 샘플을 샘플링하거나 생성하지 않는다.
🔹배치 크기의 영향:
배치 크기가 클수록 배치 내 네거티브 샘플의 수가 증가하여 모델이 더 많은 네거티브 사례를 학습할 수 있다.
[Input]
args = TrainingArguments(
output_dir="dense_retireval",
evaluation_strategy="epoch",
learning_rate=2e-5,
per_device_train_batch_size=2,
per_device_eval_batch_size=2,
num_train_epochs=2,
weight_decay=0.01
)
p_encoder, q_encoder = train(args, train_dataset, p_encoder, q_encoder)
[Input]
valid_corpus = list(set([example['context'] for example in dataset['validation']]))[:10]
sample_idx = random.choice(range(len(dataset['validation'])))
query = dataset['validation'][sample_idx]['question']
ground_truth = dataset['validation'][sample_idx]['context']
if not ground_truth in valid_corpus:
valid_corpus.append(ground_truth)
print(f"Query: {query}")
print(f"Grount Truth: ground_truth")
[output]
>> Query: 반기문이 2016년 6월 이스라엘에 방문할 당시 이스라엘의 총리의 이름은 무엇인가?
Grount Truth: 2016년 1월 26일 반기문은 안전보장이사회의 중동 관련 토론에서 이스라엘이 최근 요르단 강 서안에서 유대인 정착촌을 확대하는 건 팔레스타인의 ...
[Input]
def to_cuda(batch):
return tuple(t.cuda() for t in batch)
with torch.no_grad():
p_encoder.eval()
q_encoder.eval()
q_seqs_val = tokenizer([query], padding="max_length", truncation=True, return_tensors='pt').to('cuda')
q_emb = q_encoder(**q_seqs_val).to('cpu') #(num_query, emb_dim)
p_embs = []
for p in valid_corpus:
p = tokenizer(p, padding="max_length", truncation=True, return_tensors='pt').to('cuda')
p_emb = p_encoder(**p).to('cpu').numpy()
p_embs.append(p_emb)
p_embs = torch.Tensor(p_embs).squeeze() # (num_passage, emb_dim)
print(p_embs.size(), q_emb.size())
[output]
>> torch.Size([11, 768]) torch.Size([1, 768])
✅ torch.Size([11, 768])
랜덤으로 선정한 10개 샘플 + Ground Truth = 11개
[Input]
dot_prod_scores = torch.matmul(q_emb, torch.transpose(p_embs, 0, 1))
print(dot_prod_scores.size())
rank = torch.argsort(dot_prod_scores, dim=1, descending=True).squeeze()
print(dot_prod_scores)
print(rank)
[output]
>> torch.Size([1, 11])
tensor([[ 9.4619, 9.6707, 8.3590, 10.9888, 7.4889, 6.7830, 10.5825, 9.6571,
4.7864, 7.1045, 18.2881]])
tensor([10, 3, 6, 1, 7, 0, 2, 4, 9, 5, 8])
[Input]
k = 5
print("[Search query]\n", query, "\n")
print("[Ground truth passage]")
print(ground_truth, "\n")
for i in range(k):
print("Top-%d passage with score %.4f" % (i+1, dot_prod_scores.squeeze()[rank[i]]))
print(valid_corpus[rank[i]])
[output]
>> [Search query]
반기문이 2016년 6월 이스라엘에 방문할 당시 이스라엘의 총리의 이름은 무엇인가?
[Ground truth passage]
2016년 1월 26일 반기문은 안전보장이사회의 중동 관련 토론에서 이스라엘이 최근 요르단 강 서안에서 유대인 정착촌을 확대하는 건 팔레스타인의 증오를 부추기고 국제사회를 모욕하는 행동이라고 지적하였다. 반기문은 억압받는 민족이 시대를 걸쳐 보여줬듯이 점령에 저항하는 건 인간의 본성이며, 이는 증오와 극단주의의 강력한 인큐베이터가 된다고 강조하면서 이스라엘의 조치를 강한 어조로 비판하였다. 이후 2016년 6월 이스라엘과 팔레스타인을 직접 방문하여 베냐민 네타냐후 이스라엘 총리, 마흐무드 압바스 팔레스타인 자치정부 수반을 각각 만나 분쟁 해결을 위한 중재를 하였다. 반기문은 이스라엘과 팔레스타인 양측 모두 이 땅에 역사적 종교적 연결점이 있음을 강조하면서, 유엔이 분쟁 해결책으로 제시해 온 평화 협상에 기반한 2국가 해법(two-state solution)을 다시금 상기시켰다.
Top-1 passage with score 18.2881
2016년 1월 26일 반기문은 안전보장이사회의 중동 관련 토론에서 이스라엘이 최근 요르단 강 서안에서 유대인 정착촌을 확대하는 건 팔레스타인의 증오를 부추기고 국제사회를 모욕하는 행동이라고 지적하였다. 반기문은 억압받는 민족이 시대를 걸쳐 보여줬듯이 점령에 저항하는 건 인간의 본성이며, 이는 증오와 극단주의의 강력한 인큐베이터가 된다고 강조하면서 이스라엘의 조치를 강한 어조로 비판하였다. 이후 2016년 6월 이스라엘과 팔레스타인을 직접 방문하여 베냐민 네타냐후 이스라엘 총리, 마흐무드 압바스 팔레스타인 자치정부 수반을 각각 만나 분쟁 해결을 위한 중재를 하였다. 반기문은 이스라엘과 팔레스타인 양측 모두 이 땅에 역사적 종교적 연결점이 있음을 강조하면서, 유엔이 분쟁 해결책으로 제시해 온 평화 협상에 기반한 2국가 해법(two-state solution)을 다시금 상기시켰다.
Top-2 passage with score 10.9888
2011년 10월 15일, 10.26 서울시장 보궐선거에 나선 한나라당 나경원 후보가 자신의 트위터에서 스스로를 지지하는 ‘자화자찬’하는 글을 올려 논란이 일었다. 자신이 작성한 글을 리트윗(재인용)해 “콘텐츠가 있는 공약과 정책 정말 멋집니다” 등 자신을 지지하는 댓글을 달았다. 이에 네티즌들이 ‘나르시즘 나경원’ ‘자화자찬도 유분수’ ‘알바의 실수인가’ 등의 지적을 하자 나경원 측은 2011년 10월 16일 해당 트위터 글을 삭제하고 “확인 결과 시스템 간에 충돌이 일어나 계정 연동 오류가 발생한 것으로 확인됐다”며 “현재 오류를 바로 잡았다”고 밝혔다. 그러나 2011년 10월 20일 한 트위터 사용자가 나경원 트위터 멘션 오류에 대해 트위터 본사에 문의한 내용을 온라인에 게시했는데 트위터 본사 답변에 따르면 “나 후보 측의 트위터 글은 트위터 내부 오류나 장애가 아니다”라며 “후보자는 트윗을 포스팅 하기 위해 외부 어플리케이션을 사용한 것으로 보인다”고 답했다. 이어 “이같은 오류나 장애는 트위터가 아닌 이 어플리케이션에서 발생된 것으로 보인다”며 트위터 계정 연동 오류가 아니라고 밝혀 '계정 연동 시스템 오류'라던 나경원의 해명이 거짓말로 드러났다.
Top-3 passage with score 10.5825
"나는 대통령 때 외무부에 지시해 독도 인근 해역에 배를 엄청나게 띄워 해상시위를 하도록 했다"며 "그때 배 선착장에 몇 백명이 올라가서 시위를 하니 일본 정부가 우리 외무부에 그만두도록 해달라고 요청했다"고 지난 1996년 당시 강원도 어민들이 벌인 '일본 독도 영유권 주장 망언 규탄' 대규모 해상시위를 언급하고 이어 "대통령이 문제다. 외교 활동을 제대로 못하고 있으니 (일본이 노대통령을) '바보'로 취급하는 것"이라고 노무현 정권의 외교 활동을 비난하였다. 그는 김대중 전 대통령의 방북 계획과 관련해 "말만 나오는 것이지 가기는 자기가 어디로 가느냐"면서 "누가 오라는 사람도 있는 것도 아니고…. 김정일에게 아무것도 갖다줄 게 없는데 누가 오라고 하겠나"라고 '쓴소리'를 가했다.
Top-4 passage with score 9.6707
1934년 7월, 사이토 내각을 이어 출범한 오카다 게이스케 내각은 과격파 청년 장교들의 계속되는 갈등 유발과 점령지 주민들의 계속되는 저항에 부딪쳤다. 1936년 2월 26일 육군 하급 장교 22명이 제1사단 휘하 3개 연대와 근위보병연대의 무장 병사들 1,400명을 이끌고 도쿄에서 반란을 일으켜 사이토 마코토 내대신, 다카하시 고레키요 대장대신, 와타나베 조타로 교육총감을 살해하는 2·26 사건이 일어났다. 또, 이 사건으로 경찰관 5명이 죽었으며, 스즈키 간타로 시종장이 부상을 입었다. 요코스카 진수부에 있던 진수부 사령장관 요나이 미쓰마사, 참모장 이노우에 시게요시는 도쿄 만에 함대를 집결시켰고 정부는 계엄령을 발동했다. 정부의 발빠른 수습으로 주동자들 중 한 명은 자결하고 나머지 지휘관들은 항복했으며, 병사 대부분이 부대로 돌아갔지만 계엄령은 5개월 넘게 이어졌다.
Top-5 passage with score 9.6571
이 선거에서는 (사전 여론조사 및 1차 투표 결과로 비추어 볼 때) 좌파 지지 유권자가 우파 지지 유권자보다 훨씬 많았음에도 불구하고, 결과는 우파 후보의 당선으로 나타났다. 이는 결선투표제하에서도 후보 단일화의 필요성이 제기될 수 있음을 보여주는 대표적인 사례다. 한편 최저 득표자를 탈락시키는 선거를 반복해서 했다면, 좌파 후보들 중에서 한 명(아마도 조스팽)이 당선되었겠지만, 이 방식은 지나치게 번거롭다는 문제가 있다. 그런데 이 번거로움을 해결해주는 제도가 있다. 바로 선호투표제다. 선호투표제에 대해서는 이 문서의 아래 문단 및 선호투표제 문서를 참조 바람. 그리고 2002년 프랑스 대선이 선호투표제로 치뤄졌을 경우의 결과는 《2002년 프랑스 대선이 선호투표제로 치뤄졌다면?》을 참조 바람.