멀티캠퍼스 데이터분석 6월 19일 수업 내용 — BART 이론, KoBART 요약 모델 Seq2Seq 학습, 인터뷰 JSON 데이터 수집, 복습 문제
BART 요약 모델
1. BART 이론
2. 데이터 준비 — DatasetDict & 토큰화
3. ROUGE 평가 지표
4. Seq2SeqTrainer 학습
5. 문장 생성 — generate() 매개변수
인터뷰 데이터 수집
BART(Bidirectional and Auto-Regressive Transformers) 는 Transformer 기반의 Encoder-Decoder 혼합 모델입니다.
| 구성 | 역할 | 참고 모델 |
|---|---|---|
| Encoder | 문장을 이해 | BERT 방식 |
| Decoder | 문장을 생성 | GPT 방식 |
| 토큰 | 의미 |
|---|---|
<PAD> | 빈 칸 채우기 (패딩) |
<UNK> | OOV (미등록 단어) |
<SEP> | 두 번째 문장 구분 |
</s> (EOS) | 전체 문장의 끝 — 생성이 언제 끝나는지 표시 |
# !pip install lighteval
# !pip install rouge_score evaluate
import evaluate
import numpy as np
from datasets import Dataset, DatasetDict
from transformers import (AutoTokenizer, AutoModelForSeq2SeqLM,
DataCollatorForSeq2Seq, Seq2SeqTrainer,
Seq2SeqTrainingArguments)
model_name = 'digit82/kobart-summarization'
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
train_docs = [
'정부는 중소기업 세제 혜택과 R&D 세액 공제를 확대한다고 밝혔다',
'해당 기업은 분기 실적에서 매출 성장을 기록했으며 신제품 출시를 예고했다'
]
train_sums = [
'정부가 중소기업 지원을 확대한다.',
'기업이 실적 개선과 신제품 출시를 예고했다.'
]
valid_docs = ['교육부가 디지털 교과서 도입을 추진한다고 발표했다']
valid_sums = ['교육부가 디지털 교과서 도입을 추진한다.']
# DatasetDict — train/validation을 하나의 객체로 관리
raw_ds = DatasetDict({
'train': Dataset.from_dict({
'document': train_docs, 'summary': train_sums
}),
'validation': Dataset.from_dict({
'document': valid_docs, 'summary': valid_sums
})
})
max_input_len = 512
max_target_len = 128
def token_fn(batch):
# 입력 문장 인코딩
inputs = tokenizer(
batch['document'],
max_length = max_input_len,
padding = 'max_length',
truncation = True
)
# 출력(요약) 문장 인코딩
labels = tokenizer(
batch['summary'],
max_length = max_target_len,
padding = True,
truncation = True
)
# 패딩 토큰을 -100으로 변환
labels_ids = np.array(labels['input_ids'])
labels_ids[labels_ids == tokenizer.pad_token_id] = -100
inputs['labels'] = labels_ids.tolist()
return inputs
tokenized_ds = raw_ds.map(
token_fn, batched=True,
remove_columns=['document', 'summary']
)
💡 labels의 패딩 토큰을
-100으로 바꾸는 이유
nn.CrossEntropyLoss()는 기본적으로-100값을 무시하도록 설계되어 있습니다.
패딩 위치까지 손실 계산에 포함되면 모델이 "패딩을 생성하는 법"을 학습하게 되므로,
패딩 부분을-100으로 마스킹하여 실제 단어 부분만 학습되도록 합니다.
data_collator = DataCollatorForSeq2Seq(tokenizer=tokenizer, model=model)
💡 Seq2Seq 모델 전용 데이터 콜레이터로, 배치마다 입력과 라벨을 적절히 패딩하고
디코더 입력(decoder_input_ids)도 자동으로 생성합니다.
ROUGE 는 생성된 문장과 정답 문장 사이에 공통으로 등장하는 단어의 비율을 측정합니다.
| 지표 | 측정 방식 |
|---|---|
| ROUGE-1 | 개별 단어(1-gram)의 겹침 정도 |
| ROUGE-2 | 연속된 2단어(2-gram)의 겹침 정도 |
| ROUGE-L | 가장 길게 공통으로 이어지는 부분 문자열(LCS) 기반 |
from konlpy.tag import Komoran
rouge = evaluate.load('rouge')
komoran = Komoran()
def metrics(eval_pred):
preds, labels = eval_pred
# -100을 다시 패딩 토큰 ID로 복원 (디코딩을 위해)
labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
# 토큰 ID → 텍스트 디코딩
pred_str = tokenizer.batch_decode(preds, skip_special_tokens=True)
label_str = tokenizer.batch_decode(labels, skip_special_tokens=True)
# 좌우 공백 제거 (다르면 다른 문장으로 측정됨)
pred_str = [doc.strip() for doc in pred_str]
label_str = [doc.strip() for doc in label_str]
result = rouge.compute(
predictions = pred_str,
references = label_str,
tokenizer = lambda x: komoran.morphs(x) # 한국어 형태소 기준 비교
)
return {k: round(v * 100, 2) for k, v in result.items()}
💡
np.where(labels != -100, labels, tokenizer.pad_token_id)
학습용으로 -100 처리했던 라벨을 평가 시에는 다시 패딩 토큰 ID로 되돌려야
batch_decode()로 정상적인 텍스트 디코딩이 가능합니다.
args = Seq2SeqTrainingArguments(
output_dir = './kobart',
eval_strategy = 'epoch',
save_strategy = 'epoch',
learning_rate = 5e-5,
num_train_epochs = 5,
logging_steps = 2,
load_best_model_at_end = True,
metric_for_best_model = 'rougeL',
greater_is_better = True,
# 생성 관련 설정
predict_with_generate = True, # 평가 시 직접 문장 생성
generation_max_length = 64, # 생성 문장 최대 토큰 길이
generation_num_beams = 4 # 빔 서치 후보 개수
)
trainer = Seq2SeqTrainer(
model = model,
args = args,
train_dataset = tokenized_ds['train'],
eval_dataset = tokenized_ds['validation'],
processing_class = tokenizer,
data_collator = data_collator,
compute_metrics = metrics
)
trainer.train()
| 매개변수 | 설명 |
|---|---|
predict_with_generate | 평가 시 실제 텍스트를 생성하여 평가 (분류와 다른 점) |
generation_max_length | 생성 문장의 최대 토큰 길이 |
generation_num_beams | 빔 서치 시 탐색할 후보 문장 개수 |
test_text = """과학기술정보통신부는 초거대 AI 연구 인프라 지원을 강화한다고 밝혔다.
스타트업 대상으로 GPU 리소스를 확대 제공할 계획이다."""
inputs = tokenizer(
test_text, return_tensors='pt',
truncation=True, max_length=max_input_len
)
generate_ids = model.generate(
**inputs,
max_new_tokens = 64, # 생성 토큰 최대 길이
min_new_tokens = 5, # 생성 토큰 최소 길이
num_beams = 4, # 빔 서치 후보 개수
do_sample = False, # False: 빔서치(결정적) / True: 확률 기반 샘플링(무작위)
length_penalty = 0.6, # 길이에 대한 패널티
no_repeat_ngram_size = 3, # 동일한 n-gram 반복 방지
repetition_penalty = 1.1, # 토큰 반복 패턴에 패널티
bos_token_id = tokenizer.bos_token_id,
eos_token_id = tokenizer.eos_token_id,
pad_token_id = tokenizer.pad_token_id
)
print(tokenizer.decode(generate_ids[0], skip_special_tokens=True))
| 매개변수 | 설명 |
|---|---|
num_beams | 여러 후보 문장을 동시에 탐색 후 가장 가능성 높은 문장 선택 |
do_sample | False: 빔서치(일관된 결과) / True: 확률적 샘플링(다양한 결과) |
length_penalty | <1: 짧은 문장에 가산점(간결한 요약) / >1: 긴 문장에 가산점 |
no_repeat_ngram_size | 지정 크기의 n-gram이 반복되지 않도록 방지 |
repetition_penalty | 이미 생성된 토큰의 재등장에 패널티 부여 |
💡
length_penalty설정 가이드
- 요약 작업:
0.6 ~ 0.8(짧고 압축된 문장 선호)- 번역/생성 작업:
1.0 ~ 1.5(자연스럽고 풍부한 문장 선호)
from glob import glob
import pandas as pd
import json
file_list = glob('../data/인터뷰/*.json')
# pandas로 바로 읽기 (중첩 구조 확인용)
df = pd.read_json(file_list[0])
# json 라이브러리로 직접 읽기 (딕셔너리 구조 탐색)
with open(file_list[0], 'r', encoding='utf-8') as f:
data = json.load(f)
from pprint import pprint
pprint(data['dataSet']['answer']['summary']['text'])
💡
with open(...) as f:패턴
파일을 열고 작업이 끝나면 자동으로 닫아줍니다.
f.close()를 명시적으로 호출할 필요가 없어 안전합니다.
answers, summarys = [], []
for file in file_list:
try:
with open(file, 'r', encoding='utf-8') as f:
dict_data = json.load(f)
answer = dict_data['dataSet']['answer']['raw']['text']
summary = dict_data['dataSet']['answer']['summary']['text']
answers.append(answer)
summarys.append(summary)
except Exception as e:
print(e) # 구조가 다른 파일은 건너뛰고 에러 출력
print(len(summarys))
💡
try/except로 일부 파일 오류 방지
대량의 JSON 파일을 처리할 때 일부 파일의 구조가 다르거나 깨져 있을 수 있습니다.
try/except로 감싸면 한 파일의 오류 때문에 전체 작업이 중단되지 않습니다.
df = pd.DataFrame(zip(answers, summarys), columns=['answer', 'summary'])
df.to_csv('인터뷰.csv', index=False)
answers, summarys 데이터 중 상위 100개 → trainDatasetDict 생성digit82/kobart-summarizationSeq2SeqTrainer 로 파인튜닝import pandas as pd
from datasets import Dataset, DatasetDict
from transformers import (AutoTokenizer, AutoModelForSeq2SeqLM,
DataCollatorForSeq2Seq, Seq2SeqTrainer,
Seq2SeqTrainingArguments)
import numpy as np
df = pd.read_csv('인터뷰.csv')
train_df = df.head(100)
valid_df = df.tail(20)
raw_ds = DatasetDict({
'train': Dataset.from_dict({
'document': train_df['answer'].tolist(),
'summary' : train_df['summary'].tolist()
}),
'validation': Dataset.from_dict({
'document': valid_df['answer'].tolist(),
'summary' : valid_df['summary'].tolist()
})
})
model_name = 'digit82/kobart-summarization'
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
max_input_len = 512
max_target_len = 256
def token_fn(batch):
inputs = tokenizer(
batch['document'], max_length=max_input_len,
padding='max_length', truncation=True
)
labels = tokenizer(
batch['summary'], max_length=max_target_len,
padding=True, truncation=True
)
labels_ids = np.array(labels['input_ids'])
labels_ids[labels_ids == tokenizer.pad_token_id] = -100
inputs['labels'] = labels_ids.tolist()
return inputs
tokenized_ds = raw_ds.map(
token_fn, batched=True,
remove_columns=['document', 'summary']
)
data_collator = DataCollatorForSeq2Seq(tokenizer=tokenizer, model=model)
args = Seq2SeqTrainingArguments(
output_dir = './kobart_interview',
eval_strategy = 'epoch',
save_strategy = 'epoch',
learning_rate = 5e-5,
num_train_epochs = 5,
logging_steps = 2,
predict_with_generate = True,
generation_max_length = 64,
generation_num_beams = 4
)
trainer = Seq2SeqTrainer(
model = model,
args = args,
train_dataset = tokenized_ds['train'],
eval_dataset = tokenized_ds['validation'],
processing_class = tokenizer,
data_collator = data_collator
)
trainer.train()
💡 인터뷰 데이터의 특징
인터뷰 답변(answer)은 일반적으로 뉴스 기사보다 길기 때문에
max_target_len(출력 최대 길이)을 128 → 256 으로 늘려 더 풍부한 요약을 생성하도록 합니다.
| 개념 | 설명 |
|---|---|
| BART | Encoder(BERT) + Decoder(GPT) 혼합 Seq2Seq 모델 |
<PAD>/<UNK>/<SEP>/</s> | 패딩/미등록/문장구분/종료 특수 토큰 |
DatasetDict | train/validation 등을 하나의 객체로 관리 |
labels의 -100 | CrossEntropyLoss가 무시하는 마스킹 값 (패딩 손실 제외) |
DataCollatorForSeq2Seq | Seq2Seq 전용 동적 패딩 + 디코더 입력 생성 |
| ROUGE-1/2/L | n-gram 겹침 / 최장 공통 부분열 기반 평가 |
rouge.compute(tokenizer=...) | 형태소 분석기 기준으로 ROUGE 계산 |
predict_with_generate | 평가 시 실제 문장 생성 후 비교 |
num_beams | 빔 서치 후보 문장 개수 |
do_sample=False | 빔서치(결정적) / True: 확률 샘플링(다양성) |
length_penalty | 짧은/긴 문장 생성 선호도 조절 |
no_repeat_ngram_size | n-gram 반복 방지 |
repetition_penalty | 토큰 반복에 패널티 |
json.load(f) | JSON 파일을 dict로 파싱 |
with open(...) as f: | 파일 자동 닫기 컨텍스트 매니저 |
try/except | 일부 파일 오류가 전체 중단을 막음 |
to_csv(index=False) | 인덱스 제외하고 CSV 저장 |