복습 음성 · 12분 02초 · 텍스트 요약 원리·구현 복습

번역기는 한 언어의 문장을 다른 언어의 문장으로 바꿨다. 요약기는 긴 문장에서 핵심 내용을 골라 짧게 표현한다. Encoder–Decoder 구조는 이어지지만, 줄이는 과정에서 어떤 정보를 남길지 판단해야 한다는 점이 추가된다.

뉴스 요약봇 실습은 데이터 준비, LSTM과 Attention을 이용한 요약 생성, 결과 평가로 구성돼 있다. 위 생성 음성에는 리뷰 자료를 이용한 텍스트 요약의 기초 설명도 들어 있다. 본문의 실행 예제는 직접 만든 짧은 글에서 문장을 고르는 계산이다.

1. 요약은 문장을 고르거나 새로 만들 수 있다

Extractive Summarization(추출적 요약)은 원문에 있는 문장을 골라 이어 붙인다. 원문 표현을 그대로 사용하므로 문장 단위로 출처를 찾기 쉽다. 다만 비슷한 문장을 중복해서 고르거나, 앞 문장의 설명이 빠져 대명사가 무엇을 가리키는지 알기 어려워질 수 있다.

Abstractive Summarization(추상적 요약)은 원문을 바탕으로 새로운 문장을 생성한다. 여러 문장에 나뉜 정보를 짧은 제목으로 묶을 수 있다. 대신 원문에 없던 내용이나 잘못된 관계를 만들어내는지 확인해야 한다.

예를 들어 설명용 원문에 도서관이 주말 운영을 두 시간 연장한다, 다음 달부터 적용한다가 있다면, 추출적 방식은 해당 문장들을 고른다. 추상적 방식은 도서관, 다음 달부터 주말 두 시간 연장 운영처럼 재구성할 수 있다. 어느 방식이든 날짜와 연장 시간을 정확하게 유지해야 한다.

짧은 문장을 출력했다는 사실만으로 좋은 요약이 되지는 않는다. 핵심 정보가 남았는지, 원문과 같은 뜻인지, 읽을 수 있는 문장인지가 평가 대상이다.

2. 리뷰 요약과 뉴스 제목의 데이터는 구분한다

수업의 기초 예제는 리뷰의 본문과 짧은 요약을 사용하고, 뉴스 프로젝트에서는 기사 본문과 제목을 연결한다. 컬럼 이름도 리뷰의 Text·Summary, 뉴스의 text·headlines처럼 다를 수 있다. 먼저 입력과 목표가 어느 열인지 확인해야 한다.

리뷰의 짧은 평가는 긍정·부정이나 제품 특징에 집중할 수 있다. 기사 제목은 사건의 주요 대상과 행동, 시점 등을 압축한다. 두 자료에 같은 길이 제한이나 어휘 수를 그대로 적용할 근거는 없다.

기사 제목을 Reference Summary(참조 요약)로 삼더라도 제목 하나에 기사의 모든 핵심 정보가 담기는 것은 아니다. 실제 요약문이 참조와 표현이 다르다는 이유만으로 바로 틀렸다고 판단하기 어려운 이유다.

3. 본문과 정답을 같은 방법으로 정제하지 않아도 된다

HTML 태그, 불필요한 공백, 축약 표현 등을 정리한 뒤 본문과 목표 요약을 함께 점검한다. 태그를 단순히 빈 문자열로 지우면 좋았다<br>다음에도가 좋았다다음에도처럼 붙을 수 있다. 문장 사이 구분이 남도록 공백을 넣어 처리하는 편이 안전하다.

Stopword(불용어)를 지울지도 따로 결정한다. 학습 예제에서는 본문 쪽의 불용어를 제거하고 요약 쪽은 문장 형태를 유지하는 구성이 있었다. 하지만 숫자와 이름, 부정 표현까지 함께 사라지면 의미가 달라진다. 모든 데이터에 같은 정제 함수를 그대로 적용하기보다 바뀐 문장을 직접 읽어야 한다.

특히 추출적 요약용 원문은 별도로 보관할 필요가 있다. 문장 경계와 문장부호를 모두 지운 문자열을 넣으면 고를 문장부터 제대로 나누기 어렵다. 전처리된 학습 입력과 사람이 읽을 원문을 각각 남겨 두면 결과를 비교하기도 편하다.

빈 값은 처음 검사한 뒤에도 다시 생길 수 있다. 정제 후 공백만 남은 문장, 토큰화 후 유효 토큰이 사라진 요약도 확인한다. 중복을 제거할 때는 본문과 요약의 연결을 유지한다. 서로 다른 기사가 같은 제목을 쓰는 경우가 있으므로 제목 중복만으로 원문까지 같다고 보지는 않는다.

4. 길이와 어휘 수는 분포를 보고 정한다

본문은 길고 요약은 짧다. 각각의 토큰 길이를 살펴보고 어느 정도의 자료를 유지할지 정한다. 학습 자료에서 길이 분포와 어휘 통계를 구한 뒤 검증·테스트에 같은 기준을 적용하면 평가 쪽 정보가 기준 선택에 섞이는 일을 줄일 수 있다.

기초 리뷰 예제의 본문 길이 50, 요약 길이 8 같은 설정은 해당 예제의 선택이다. 뉴스 기사에 그대로 대입하면 앞부분만 남고 사건의 중요한 설명이 잘릴 수 있다. 길이를 늘리면 보존하는 정보와 계산량이 함께 늘어난다.

어휘 사전은 학습 자료에서 만든다. 드문 단어를 제한하면 사전과 출력 층이 작아지지만, 인물·지역 이름 같은 중요한 단어가 UNK(미등록 토큰)로 바뀔 수 있다. 원문과 요약 양쪽의 미등록 비율을 확인해야 한다.

PAD, UNK, 시작·종료 토큰도 구분한다. 전처리 뒤 실제 요약이 비어 버렸는데 시작·종료 토큰만 붙이면 모양은 정상인 학습 자료가 될 수 있다. 모델에 넣기 전에 내용 토큰이 남았는지 확인하는 이유다.

5. LSTM과 Attention으로 요약을 만드는 흐름

Encoder(인코더)는 기사 본문을 읽고 각 위치의 상태를 만든다. Decoder(디코더)는 그 정보와 앞서 나온 요약 토큰을 이용해 다음 요약 토큰을 예측한다. LSTM을 사용하면 은닉 상태 h와 셀 상태 c를 함께 전달한다.

여러 층의 LSTM을 쌓으면 아래 층의 전체 시점 출력이 위 층의 입력으로 넘어간다. 인코더 마지막 상태를 디코더의 초기 상태로 연결할 때는 층 수와 크기를 맞춰야 한다. 인코더가 세 층이고 디코더가 한 층이면 어떤 층의 상태를 넘길지 정해 줘야 한다.

Attention은 요약의 각 위치에서 원문 전체 상태에 비중을 주고 문맥 벡터를 만든다. 본문 길이를 S, 요약 길이를 T, 은닉 크기를 H라고 두면 아래처럼 읽을 수 있다.

encoder 출력        (B, S, H)
decoder 출력        (B, T, H)
attention 점수      (B, T, S)
문맥 벡터           (B, T, H)
decoder와 문맥 연결 (B, T, 2H)
요약 토큰 점수      (B, T, 요약 어휘 수)

Attention 점수의 마지막 S축은 참고할 원문 위치다. 이 축으로 Softmax를 계산하고 원문의 PAD 위치를 가린다. 요약 정답의 PAD는 손실에서 별도로 제외한다. 앞 번역기 글에서 구분한 두 마스크가 그대로 필요하다.

6. 학습할 때와 요약문을 만들 때의 입력

정답 요약이 도서관 운영 연장이라면 디코더 입력은 BOS 도서관 운영 연장, 정답은 도서관 운영 연장 EOS다. 학습할 때 정답의 이전 토큰을 넣는 Teacher Forcing(교사 강요)을 사용한다. 최대 길이 때문에 잘라야 할 경우 같은 내용 토큰에서 입력과 정답을 만들어 EOS 자리를 남긴다.

생성은 BOS 하나에서 시작한다. 예측한 토큰을 다시 넣으며 EOS나 최대 생성 길이까지 진행한다. 이때 반복이 생기거나 특정 문구만 계속 내놓을 수 있다. 정답을 입력하는 검증 손실과 스스로 생성한 문장은 함께 확인해야 한다.

Early Stopping(조기 종료)은 검증 손실이 더 좋아지지 않는 상태가 이어지면 학습을 끝내는 방법이다. 가장 좋았던 가중치를 사용하려면 그 시점의 상태를 따로 저장하고 복원해야 한다. 마지막 epoch의 가중치와 가장 좋았던 가중치가 같다는 보장은 없다.

메모리에 최고 상태를 저장할 때 state_dict()를 변수에 대입만 하면 이후 학습에 따라 참조된 텐서가 바뀔 수 있다. 복사본을 보관하거나 파일로 저장한 뒤 되읽는 흐름을 확인한다. 선택에 사용한 검증 자료와 최종 평가용 테스트 자료도 나눈다.

7. TextRank는 문장 사이의 관계로 순위를 매긴다

추출적 요약의 한 방법인 TextRank는 문장을 Graph(그래프)의 노드로 두고 문장 사이의 관련성을 연결한다. 관련된 여러 문장, 특히 순위가 높은 문장과 연결되는 문장에 높은 점수가 모이도록 반복 계산한다. 그 점수로 일부 문장을 고르고 원래 순서로 돌려놓아 요약을 만든다. TextRank 논문

수업에서 사용한 Summa는 이런 방식의 요약 기능을 제공한다. ratio는 남길 분량의 비율, words는 대략적인 단어 수, split=True는 문장 목록 반환에 쓰인다. 원문이 아주 짧거나 비율이 너무 작으면 선택할 문장이 부족할 수 있다. 줄바꿈을 문장 구분에 사용하는 동작도 확인해야 한다. Summa 공식 저장소

아래 코드는 원리를 보기 위한 자체 예제다. Summa를 실행하는 대신, TF-IDF(단어 빈도와 역문서 빈도) 벡터의 코사인 유사도로 그래프를 만들었다. TextRank의 문장 순위 아이디어를 사용하되 유사도 계산과 전처리는 이 예제에 맞게 단순화했다.

8. 다섯 문장에서 두 문장을 골라보기

실행용 문장은 직접 만들었다. 앞의 네 문장은 도서관 운영에 관한 내용이고 마지막은 공원의 꽃에 관한 내용이다. 같은 단어를 공유하는 문장들이 어떻게 연결되는지 볼 수 있도록 구성했다.

자기 자신과의 유사도는 0으로 둔다. 각 행의 합으로 나누면 해당 문장에서 다른 문장으로 이동할 비중이 된다. 연결이 전혀 없는 문장은 모든 문장으로 균등하게 이동하도록 처리했다. 점수의 0.85는 연결을 따라 보내고 0.15는 전체에 고르게 나눠 주며 반복한다.

"""TextRank의 그래프 순위 아이디어를 TF-IDF 유사도로 확인하는 자체 예제."""
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer

SENTENCES = [
    "시립 도서관은 다음 달부터 주말 운영 시간을 늘린다.",
    "도서관은 주말 이용자가 늘어 운영 시간을 두 시간 연장하기로 했다.",
    "연장 운영은 토요일과 일요일에 적용된다.",
    "도서관은 주말 운영 연장에 맞춰 안내 인력을 추가로 배치한다.",
    "인근 공원에는 봄꽃이 피었다.",
]


def summarize(sentences, count=2):
    # 조사 분리 없이 공백 단위에 가까운 토큰을 사용하는 학습용 예제다.
    vectors = TfidfVectorizer(token_pattern=r"(?u)\b\w+\b").fit_transform(sentences)
    similarities = (vectors @ vectors.T).toarray()
    np.fill_diagonal(similarities, 0.0)
    n = len(sentences)
    totals = similarities.sum(axis=1, keepdims=True)
    transitions = np.divide(
        similarities, totals, out=np.full((n, n), 1 / n), where=totals != 0
    )
    rank = np.full(n, 1 / n)
    for _ in range(100):
        updated = 0.15 / n + 0.85 * transitions.T @ rank
        if np.abs(updated - rank).sum() < 1e-10:
            rank = updated
            break
        rank = updated
    selected = sorted(np.argsort(-rank, kind="stable")[:count].tolist())
    return rank, selected, [sentences[i] for i in selected]


def main():
    rank, selected, summary = summarize(SENTENCES)
    print("scores:", [round(float(x), 4) for x in rank])
    print("selected sentence numbers:", [i + 1 for i in selected])
    print("summary:")
    for sentence in summary:
        print(sentence)


if __name__ == "__main__":
    main()

실행 결과를 적어둔다.

scores: [0.3315, 0.3146, 0.0455, 0.263, 0.0455]
selected sentence numbers: [1, 2]
summary:
시립 도서관은 다음 달부터 주말 운영 시간을 늘린다.
도서관은 주말 이용자가 늘어 운영 시간을 두 시간 연장하기로 했다.

두 문장 모두 원문에서 그대로 가져왔다. 주제와 관계없는 마지막 문장은 선택되지 않았다. 한편 선택된 두 문장은 주말 운영 연장이라는 내용이 겹친다. 순위가 높은 문장을 고르는 것만으로 중복이 모두 해결되지는 않는다는 점도 보인다.

이 예제는 한국어의 조사나 활용형을 분석하지 않고 공백 단위에 가까운 토큰을 사용한다. 운영과 운영은처럼 표기가 달라지면 다른 토큰으로 처리된다. 같은 뜻을 다른 표현으로 쓴 문장도 놓칠 수 있다. 지금 결과는 다섯 문장의 계산 확인이며, 실제 기사 요약 성능을 측정한 값은 아니다.

9. 요약 결과는 원문과 나란히 읽는다

학습 손실이 내려가도 여러 기사에 거의 같은 요약이 나오거나, 원문과 관계없는 표현을 반복할 수 있다. 생성 결과의 종류가 지나치게 적은지도 살펴본다. 자연스럽게 보이는 짧은 문장만 따로 읽으면 이런 문제를 놓치기 쉽다.

살펴볼 것 비교할 내용
핵심 정보 누가 무엇을 했는지, 중요한 원인과 결과가 남았는가
사실성 이름·날짜·숫자·부정 표현이 원문과 맞는가
문장 완성도 중간에 끊기거나 같은 말을 반복하지 않는가
압축 정도 지나치게 짧아 뜻을 잃거나 원문을 너무 길게 복사하지 않는가

ROUGE(참조 요약과 표현의 겹침을 보는 지표)도 사용할 수 있다. ROUGE-1은 단어 단위, ROUGE-2는 연속된 두 단어 단위의 겹침을 보고, ROUGE-L은 순서를 유지하는 공통 부분 수열을 활용한다. 같은 표현이 많이 겹치더라도 숫자 하나가 바뀌면 사실이 달라질 수 있다. 지표와 원문 대조를 함께 하는 이유다. ROUGE 논문

추출적 방식과 추상적 방식을 비교할 때는 같은 기사와 비슷한 출력 길이 조건을 사용한다. 원문 문장을 고르는 방법이 핵심 내용을 얼마나 보존하는지 먼저 보고, 생성 모델이 어떤 정보를 더 잘 압축하거나 놓치는지 비교하면 결과를 설명하기 쉽다.

정제할 때부터 요약에 필요한 뜻을 보존해야 한다. 모델 구조와 함께 입력에서 지운 내용, 정답을 만든 방법, 결과를 읽은 기준도 남겨두려고 한다.

실행 환경과 참고

개인 뉴스 요약봇·텍스트 요약 모델 복습 노트를 바탕으로 학습 흐름을 정리했다. 논문과 라이브러리 링크는 해당 설명 옆에 두었다.

  • 실행 환경: Python 3.12.9, NumPy 2.5.2, PyTorch 2.13.0 (CPU), scikit-learn 1.9.0.
  • 실행 명령: python summarization_example.py.
  • 확인 범위: TF-IDF 그래프 점수, 연결 없는 문장의 처리, 점수 합, 선택 문장이 원문에 존재하며 원래 순서를 유지하는지.
  • LSTM 요약 모델 전체 학습, Summa 실행, 실제 뉴스 자료의 ROUGE 측정은 이번에 하지 않았다. 본문에서 설명한 추상적 요약과 실행한 문장 추출 예제를 구분한다.