본문 바로가기

NLP/용어정리

[용어 정리] oracle summary

요약

 

용어 정리

  • 생성 요약 정답(abstractive gold summary): 새롭게 작성한 요약문. 기존 내용을 그대로 사용하지 않음.
  • 추출 요약 정답(extractive gold summary): 기존 내용에서 몇 개 문장을 뽑아 만든 요약문. 예컨대, 기존 내용이 10개의 문장으로 이뤄져있다고 가정하자. 그러면 1번째 문장이 요약문에 포함되면 '1', 아니면 '0' 으로 구분해놓는 걸 말한다.

 

문제점

  • 대부분의 요약 데이터셋은 생성 요약 정답은 있는데, 추출 요약 정답은 없다.

 

해결방법

: 그래서 unsupervised method 로 만들고자 한다.

  • 문장 몇 개를 뽑아 집합을 만들어서, 생성 요약 정답을 활용해 ROUGE score 를 구해 가장 높은 점수를 가진 집합을 extrastive ground-truth 로 활용할 것이다.
  • 방법은 greedy 하게 진행한다. 한문장씩 집합에 넣어보면서, ROUGE score 가 최대가 되도록 만든다.
  • 남은 후보 문장 없이 모두 거쳤을 경우, 알고리즘을 종료한다.
  • 이때 선택된 문장들은 모두 label 이 1이 된다. 선택받지 못한 문장들은 0 을 갖는다.

 

이렇게 기존 정답인 'abstractive gold summary' 를 활용하여 만들었기 때문에 그게 마치 신탁하는 느낌이 들어 'oracle summary' 라고 부른다.

 

(최하단에는 코드로 oracle summary 를 어떻게 구하는지 적어두었습니다.)


'Oracle Summary'  를 찾아서

깜빡이 안 킨 채 나타난 oracle summary

NLP 분야 중에 요약(summarization) 관련 논문을 읽어야 했는데, 생소한 단어가 등장했다. 바로 'oracle summary' 였다. 밑도 끝도 없이 바로 쓰시길래, 이게 대체 무슨 말인가 싶었다.

 

영어 구글 형님도 모르시는 단어

심지어 영어로 구글링해도 논문밖에 나오질 않았다. 싸늘하다, 왠지 이 분야에서만 쓸 것 같은 말인데.

 

유튜브에서 실마리를 찾다

유튜브에서 summarization 관련 영상을 찾아보다가, 정의를 발견했다.

  • ORACLE summary: 앞서 greedy 하게 추출된 추출요약용 정답 요약문

하지만 강의를 처음부터 들어봐도 무슨 말인지 알 수 없었다. 하지만 'LEAD-3' 와 함께 검색한다면 어느 정도 감을 잡을 수 있을 것 같다는 실마리를 발견했다.

 

과거 요약 논문에서 찾은 정의1

 

Fine-tune BERT for Extractive Summarization

 

 검색한 결과, 나온 논문이 있어 'oracle'만 넣어서 찾아봤더니 위와 같이 이야기한다.

2개의 데이터셋(CNN/DailyMail & NYT) 모두 생성 요약 정답(abstractive gold summaries)은 있는데, 추출요약 정답(extractive)이 없다. 그래서 greedy algorithm 을 사용하여 oracle summary 를 만들었다.

 

 

그렇다. 위에 유튜브에 greedy 라는 말도 이제 이해가 된다. greedy algorithm 을 통해서 만들어서 그렇게 표현했구나. 그렇다면, oracle summary 는 이렇게 정의해봐도 되지 않을까?

oracle summary: greedy algorithm 을 사용하여 만든 abstractive summary

 

 

하지만 아직 부족하다. 이왕 이렇게 된 거, 어떤 방식으로 만들었는지 알아내보자.

Fine-tune BERT for Extractive Summarization

ROUGE score 를 사용해서 greedily 하게 문장을 뽑는다고 하는데, 대충 이런 뜻인 것 같다.

  • abstractive gold summary 와 ROUGE score 를 각 문장마다 구한다.
  • 점수를 활용해서 greedily 하게 뽑는다.
  • 뽑히면 1, 아니면 0. 이렇게 extractive summary 에 필요한 label 을 제작한다.

하지만 확실하진 않다. 어떻게 뽑는 것이 greedily 한 건지 와닿지 않고, 정답은 총 몇개인지도 궁금하다. 그래서 다른 논문을 찾아봤다. 

 

과거 요약 논문에서 찾은 정의2

해당 논문이 처음 'oracle summary' 방식을 제시한 것 같다. 왜냐하면, 이 논문에는 'oracle summary' 라는 말조차 등장하지 않기 때문이다.

SummaRuNNer: A Recurrent Neural Network based Sequence Model for Extractive Summarization of Documents

 

요약하자면 이렇다.

  • 대부분의 요약 데이터셋은 생성 요약 정답은 있는데, 추출 요약 정답은 없다.
  • 그래서 unsupervised method 로 만들고자 한다.
  • 문장 몇 개를 뽑아 집합을 만들어서, 생성 요약 정답을 활용해 ROUGE score 를 구해 가장 높은 점수를 가진 집합을 extrastive ground-truth 로 활용할 것이다.
  • 방법은 greedy 하게 진행한다. 한문장씩 집합에 넣어보면서, ROUGE score 가 최대가 되도록 만든다.
  • 남은 후보 문장 없이 모두 거쳤을 경우, 알고리즘을 종료한다.
  • 이때 선택된 문장들은 모두 label 이 1이 된다. 선택받지 못한 문장들은 0 을 갖는다.

 

확실히 이해했다. 그리고 왜 'oracle summary' 라고 부르는지도 감이 왔다.

 

왜 하필 'oracle summary'라고 부르는가?

엄연히 사람이 직접 만든 게 아니고, abstractive gold summary 를 가지고 뽑았기 때문에 이렇게 부르는 것이다. 그걸 마치 신탁하는 것 같다고 해서 저렇게 부르는 모양이다. (나는 토종 한국인이라 그들의 재치가 힘들다.)

 

아무튼 이젠 제대로 알았다...잊을 수 없다, oracle summary.

 


코드로 보는 oracle summary

사실 코드까지 볼 생각은 없었다. 보게 된 이유는 greedy 하게 한다는 말이 명확하게 와닿진 않았기 때문이다. 찾아보니, BertSUM github 에서 어떻게 작동하는지 찾을 수 있었다.

https://github.com/nlpyang/BertSum/blob/master/src/prepro/data_builder.py

 

GitHub - nlpyang/BertSum: Code for paper Fine-tune BERT for Extractive Summarization

Code for paper Fine-tune BERT for Extractive Summarization - GitHub - nlpyang/BertSum: Code for paper Fine-tune BERT for Extractive Summarization

github.com

 

딱 봐도 이 함수가 그 역할을 하는 것 같다.

 

def greedy_selection(doc_sent_list, abstract_sent_list, summary_size):
    def _rouge_clean(s):
        return re.sub(r'[^a-zA-Z0-9 ]', '', s)

    max_rouge = 0.0
    abstract = sum(abstract_sent_list, [])
    abstract = _rouge_clean(' '.join(abstract)).split()
    sents = [_rouge_clean(' '.join(s)).split() for s in doc_sent_list]
    evaluated_1grams = [_get_word_ngrams(1, [sent]) for sent in sents]
    reference_1grams = _get_word_ngrams(1, [abstract])
    evaluated_2grams = [_get_word_ngrams(2, [sent]) for sent in sents]
    reference_2grams = _get_word_ngrams(2, [abstract])

    selected = []
    for s in range(summary_size):
        cur_max_rouge = max_rouge
        cur_id = -1
        for i in range(len(sents)):
            if (i in selected):
                continue
            c = selected + [i]
            candidates_1 = [evaluated_1grams[idx] for idx in c]
            candidates_1 = set.union(*map(set, candidates_1))
            candidates_2 = [evaluated_2grams[idx] for idx in c]
            candidates_2 = set.union(*map(set, candidates_2))
            rouge_1 = cal_rouge(candidates_1, reference_1grams)['f']
            rouge_2 = cal_rouge(candidates_2, reference_2grams)['f']
            rouge_score = rouge_1 + rouge_2
            if rouge_score > cur_max_rouge:
                cur_max_rouge = rouge_score
                cur_id = i
        if (cur_id == -1):
            return selected
        selected.append(cur_id)
        max_rouge = cur_max_rouge

    return sorted(selected)

 

반으로 나눠서 뜯어보자. 아래 코드는 다음과 같은 일을 한다.

 

변수 설명

  • doc_sent_list: document 의 문장들
  • abstract_sent_list: 생성요약 정답(abstractive gold summary)
  • summary_size: 추출요약(abstractive oracle summary)에 포함할 문장의 개수

역할

  • ROUGE score 를 측정하기 위해, 1-gram/2-gram 단위로 나누고 있다.
def greedy_selection(doc_sent_list, abstract_sent_list, summary_size):
    def _rouge_clean(s):
        return re.sub(r'[^a-zA-Z0-9 ]', '', s)

    max_rouge = 0.0
	
    # CLEAN
	abstract = sum(abstract_sent_list, [])
    abstract = _rouge_clean(' '.join(abstract)).split()
    sents = [_rouge_clean(' '.join(s)).split() for s in doc_sent_list]

	# GET 1/2 GRAMS
	evaluated_1grams = [_get_word_ngrams(1, [sent]) for sent in sents]
    reference_1grams = _get_word_ngrams(1, [abstract])
    evaluated_2grams = [_get_word_ngrams(2, [sent]) for sent in sents]
    reference_2grams = _get_word_ngrams(2, [abstract])

 

변수 설명

  • selected: 선택된 문장들의 id 값을 저장하는 리스트

역할

  • selected 는 처음에 하나도 없는 상태로 시작한다.
  • 한문장씩 넣어서 reference 와 ROUGE score 를 구해 최댓값을 구한다.
  • 최댓값을 생성하는 문장 1개를 selected 에 추가한다.
  • 다시 반복하면서 ROUGE score 최대치를 달성하는 문장 1개를 넣는다.
  • 만약 1개 문장 넣는 거보다 직접 최댓값이 더 높다면 RETURN 한다.
    selected = []
    for s in range(summary_size):
        cur_max_rouge = max_rouge
        cur_id = -1
        for i in range(len(sents)):
            if (i in selected):
                continue
			
            # INSERT NEW SENTENCE 'i'
			c = selected + [i]
            
            # GET ROUGE SCORE
            candidates_1 = [evaluated_1grams[idx] for idx in c]
            candidates_1 = set.union(*map(set, candidates_1))
            candidates_2 = [evaluated_2grams[idx] for idx in c]
            candidates_2 = set.union(*map(set, candidates_2))
            rouge_1 = cal_rouge(candidates_1, reference_1grams)['f']
            rouge_2 = cal_rouge(candidates_2, reference_2grams)['f']
            rouge_score = rouge_1 + rouge_2

			# COMPARE
			if rouge_score > cur_max_rouge:
                cur_max_rouge = rouge_score
                cur_id = i

		# IF DON'T NEED TO ADD ONE MORE SENTENCE
		if (cur_id == -1):
            return selected
        selected.append(cur_id)
        max_rouge = cur_max_rouge

    return sorted(selected)

 

이제 정말 보내줄 수 있겠다, 이 녀석아. 사요나라.