Sparse Dense Passage Retrieval 코드 실습

홍찬우·2023년 7월 29일

Sparse Retriever

TF-IDF

vectorizer = TfidfVectorizer(
    tokenizer=tokenizer_func,
    ngram_range=(1,2),
		# max_features = 50000
)

vectorizer.fit(corpus)
sp_matrix = vectorizer.transform(corpus)  # (num_passage, num_vocab)
  • ngram_range

    • default는 (1, 1)로 uni-gram만 사용

    • (1, 1)로 설정했을 땐 num_vocab이 26,797이며,

      (1, 2)로 설정하면 684,272로 훨씬 증가

  • max_features

    • default는 None으로 모든 feature가 사용

    • 숫자를 지정해주면 (num_passage, num_vocab) → (num_passages, max_features)

  • fit & transform

    • sp_matrix = vectorizer.fit_transform(corpus) 로 위 두 줄의 코드와 동일하게 가능

Query Embedding

query_vec = vectorizer.transform([query]) # (num_sentence, num_vocab)

wiki_json

{'text': 
'이 문서는 나라 목록이며, 전 세계 206개 나라의 각 현황과 주권 승인 정보를 개요 형태로 나열하고 있다.
\n\n이 목록은 명료화를 위해 두 부분으로 나뉘어 있다.
\n\n# 첫 번째 부분은 바티칸 시국과 팔레스타인을 포함하여 유엔 등 국제 기구에 가입되어 국제적인 승인을 널리 받았다고 여기는 195개 나라를 나열하고 있다.
\n# 두 번째 부분은 일부 지역의 주권을 사실상 (데 팍토) 행사하고 있지만, 아직 국제적인 승인을 널리 받지 않았다고 여기는 11개 나라를 나열하고 있다.
\n\n두 목록은 모두 가나다 순이다.
\n\n일부 국가의 경우 국가로서의 자격에 논쟁의 여부가 있으며, 이 때문에 이러한 목록을 엮는 것은 매우 어렵고 논란이 생길 수 있는 과정이다.
이 목록을 구성하고 있는 국가를 선정하는 기준에 대한 정보는 "포함 기준" 단락을 통해 설명하였다. 나라에 대한 일반적인 정보는 "국가" 문서에서 설명하고 있다.', 
'corpus_source': '위키피디아', 
'url': 'TODO', 
'domain': None, 
'title': '나라 목록', 
'author': None, 
'html': None, 
'document_id': 0}
  • key는 document_id와 동일


Dense Retriever

Flow (데이터가 어떻게 들어가서 어떤 흐름으로 학습이 되는가?)

  • training_dataset은 question, context, answer이 포함된 dictionary

    • QA가 아니라 retrieval이라 answer 정보는 필요 없음
  • 이를 TensorDataset을 이용해 passage와 query의 tokenize 결과를 모두 저장

  • passage encoder와 query encoder를 따로 생성

  • BertModelForSequenceClassification이 아닌, BertModel을 이용

    • 분류가 아니라, 모든 sequence에 대해 잘 요약할 수 있는 Model을 제작

Train

del p_inputs, q_inputs

  • 변수 자체를 삭제

  • 반복문을 돌면서 새로 할당이 되는데 왜 삭제? 메모리 이슈?


Inference

  • 다수 query 처리하는 방안 생각


Negative Sampling

What is it?

Positive example과 Negative example을 합쳐서 학습 데이터셋을 구성합니다. 
이 때 Negative example의 비율은 일반적으로 Positive example에 비해 매우 높게 설정됩니다. 
이렇게 구성된 학습 데이터셋은 모델이 관련성을 판별하는 데 필요한 문맥과 
관련 없는 문서를 구별하는 능력을 향상시킵니다.

Data

# 20개의 sample 사용
sample_idx = np.random.choice(range(len(dataset['train'])), 20)
training_dataset = dataset['train'][sample_idx]

num_neg = 3  # 사용할 negative sample 개수
corpus = np.array(corpus)
p_with_neg = []

for c in training_dataset['context']:
  while True:
    neg_idxs = np.random.randint(len(corpus), size=num_neg)

    if not c in corpus[neg_idxs]:
      p_neg = corpus[neg_idxs]

      p_with_neg.append(c)
      p_with_neg.extend(p_neg)
      break
  • 20개의 query와, 80개의 passage_with_neg 사용

    • 80 = (20 (1 + 3)) = (num_query (pos + neg))

  • 한 개의 positive passage와 num_neg의 negative passage

  • p_with_neg[0, 1, 2, 3] 는 0번 query에 대한 passage,
    p_with_neg[4, 5, 6, 7] 은 1번 query에 대한 passage, …


  • 0, 1, 2 index는 pos+neg 정보 (input_ids, attention_mask, token_type_ids)
  • 3, 4, 5 index는 query 정보 (input_ids, attention_mask, token_type_ids)

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(p_with_neg, padding="max_length", truncation=True, return_tensors='pt')

max_len = p_seqs['input_ids'].size(-1)  # 512
p_seqs['input_ids'] = p_seqs['input_ids'].view(-1, num_neg+1, max_len)
p_seqs['attention_mask'] = p_seqs['attention_mask'].view(-1, num_neg+1, max_len)
p_seqs['token_type_ids'] = p_seqs['token_type_ids'].view(-1, num_neg+1, max_len)
  • q_seqs의 size는 [20, 512] = [num_example, max_len]

  • p_seqs의 size는 [20, 4, 512] = [num_example, pos+neg, max_len]


Model

targets = torch.zeros(args.per_device_train_batch_size).long()
  • positive example은 모두 0번째에 위치하므로 torch.zeros(batch_size)

  • nll 손실함수를 이해할 필요가 있음 (아래)

p_outputs = p_model(**p_inputs)  # (batch_size*(num_neg+1), emb_dim)
q_outputs = q_model(**q_inputs)  # (batch_size*, emb_dim)

# Calculate similarity score & loss
p_outputs = p_outputs.view(args.per_device_train_batch_size, -1, num_neg+1)
# [batch_size, hidden_size, pos+neg]
q_outputs = q_outputs.view(args.per_device_train_batch_size, 1, -1)
# [batch_size, 1(num_query), hidden_size]

sim_scores = torch.bmm(q_outputs, p_outputs).squeeze()  
# [batch_size, 1, pos+neg]  ==squeeze==>  [batch_size, pos+neg]
sim_scores = sim_scores.view(args.per_device_train_batch_size, -1)
# 위 코드와 size가 동일한데 왜 하는지 잘 모르겠음
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()
  • torch.bmm

    • bmm = batch matrix multiplication

    • bmm( [B, N, M] * [B, M, P] ) ⇒ [B, N, P]

  • loss = F.nll_loss(sim_scores, targets)

    • 두 모델의 output으로부터 구한 sim_scores로 loss를 구하고, 역전파하면
      동시에 두 모델이 gradient update
  • sim_scores

    # sim_scores
    tensor([[-4.8033, -0.3004, -1.8040, -2.4461],
    				[-7.4634, -0.3286, -2.2671, -1.7379]]
    
    # loss
    tensor(4.3046, grad_fn=<NllLossBackward0>)
    Log softmax 결과 값인 [-4.8033, -0.3004, -1.8040, -2.4461]은 다음과 같이 해석할 수 있습니다.
    
    주어진 결과 값 [-4.8033, -0.3004, -1.8040, -2.4461]는 각 클래스에 속할 확률에 대한 로그값을 나타냅니다.
    
    주어진 결과 값을 exponential 함수에 적용하여 원래의 확률 값을 얻습니다.
    
    [exp(-4.8033), exp(-0.3004), exp(-1.8040), exp(-2.4461)] ≈ [0.008, 0.741, 0.164, 0.086]
    얻어진 값들을 총 합이 1이 되도록 정규화(normalize)합니다.
    
    [0.008, 0.741, 0.164, 0.086] / (0.008 + 0.741 + 0.164 + 0.086) ≈ [0.008, 0.679, 0.150, 0.079]
    해석 결과, 주어진 값 [-4.8033, -0.3004, -1.8040, -2.4461]의 
    로그 softmax 결과는 다음과 같이 해석할 수 있습니다:
    
    첫 번째 클래스: 약 0.8%의 확률
    두 번째 클래스: 약 67.9%의 확률
    세 번째 클래스: 약 15.0%의 확률
    네 번째 클래스: 약 7.9%의 확률

  • nll loss function

    Negative log likelihood 손실 함수는 다음과 같이 계산됩니다:
    
    L = -log(p),
    여기서,
    L: 손실 값
    p: 실제 타깃 값에 해당하는 클래스의 확률 값
    
    실제 타깃 값이 0인 경우, 해당 클래스의 확률 값은 0.008입니다. 
    따라서, negative log likelihood 손실 함수를 적용하면 다음과 같이 계산됩니다:
    
    L = -log(0.008) ≈ 4.828
    • nll의 default reduction은 mean으로, batch size가 2이므로 각각 로그 우도를 구한 뒤 평균

    • 즉, target은 항상 0번 째 index이므로 target=torch.zeros 로 선언하고,
      그 0번 째 index를 nll 손실 함수에서 사용







※ 모든 이미지 및 코드 출처는 네이버 커넥트재단 부스트캠프 AI Tech 5기입니다. ※

profile
AI-Kid

0개의 댓글