1. 이 스크립트가 하는 일
“로컬에 저장된 한글 기사를 불러와, 미리 파인튜닝해둔 ke-T5 번역 모델로 ‘조각내서 번역’하고, 결과를 파일로 저장하는 스크립트”
조금 더 자세히 말하면:
- 파인튜닝해둔 T5 기반 한→영 번역 모델과 토크나이저를 로드하고
- 긴 한국어 기사 텍스트를 토큰 길이 기준으로 적당한 크기의 chunk로 분할한 뒤
- 각 chunk를 순서대로 번역하고
- 번역된 결과를 합쳐서 콘솔에 출력 + 파일로 저장합니다.
2. 상단 설정부: 모델/디바이스/프롬프트
import re
from pathlib import Path
import torch
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
MODEL_DIR = "./ke_t5_ko2en_tech_final"
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("사용 디바이스:", device)
tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR)
model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_DIR).to(device)
model.eval()
PROMPT_PREFIX = "한글을 영어로 번역:\n"
MAX_INPUT_TOKENS = 256
MAX_TARGET_TOKENS = 256
2-1. 라이브러리
- re : 정규표현식으로 문단/문장을 나누는 데 사용
- Path : 파일 경로 다루기 (OS에 따라 경로 구분자 달라도 깔끔하게 처리)
- torch : PyTorch 텐서 & GPU 사용
- transformers : Hugging Face의 T5 계열 번역 모델을 불러오기 위한 라이브러리
2-2. MODEL_DIR
MODEL_DIR = "./ke_t5_ko2en_tech_final"
- 파인튜닝이 끝난 모델이 저장된 로컬 폴더 경로입니다.
- 이 폴더 안에는 config.json, pytorch_model.bin, tokenizer.json 같은 파일들이 들어 있습니다.
- from_pretrained(MODEL_DIR)로 이 디렉토리에서 모델과 토크나이저를 불러옵니다.
2-3. 디바이스 선택
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
- GPU가 있으면 cuda, 없으면 cpu를 사용하도록 자동 선택.
- 이후 텐서와 모델을 .to(device)로 올려서 동일한 디바이스에서 연산하게 합니다.
2-4. 모델과 토크나이저 로드
tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR)
model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_DIR).to(device)
model.eval()
- AutoTokenizer / AutoModelForSeq2SeqLM:
- T5 계열 번역 모델을 로드할 때 사용하는 추상화 클래스.
- .to(device) : 방금 설정한 GPU/CPU로 모델을 이동.
- model.eval() : 추론 모드로 전환 (Dropout 끄기 등).
2-5. 프롬프트와 토큰 길이
PROMPT_PREFIX = "한글을 영어로 번역:\n"
MAX_INPUT_TOKENS = 256
MAX_TARGET_TOKENS = 256
- PROMPT_PREFIX
- 이 모델을 “인스트럭션 기반(프롬프트 기반)”으로 파인튜닝했기 때문에,
입력마다 앞에 "한글을 영어로 번역:\n"을 붙여서 동일한 형식을 맞춥니다.
- 이 모델을 “인스트럭션 기반(프롬프트 기반)”으로 파인튜닝했기 때문에,
- 토큰 길이 제한
- MAX_INPUT_TOKENS : 인풋(프롬프트+한글 문장)의 최대 토큰 수
- MAX_TARGET_TOKENS : 출력(영어 번역문)의 최대 토큰 수
- 너무 길면 메모리/시간이 폭주하므로 적당한 값으로 조절한 것.
3. 긴 텍스트를 잘게 나누는 핵심 함수: chunk_text
def chunk_text(text: str, tokenizer, max_tokens: int, prompt_prefix: str) -> list[str]:
"""
긴 텍스트를 모델이 처리 가능한 길이(max_tokens) 안으로 잘라주는 함수.
너무 정교한 문장 분리는 아니지만, 기사 테스트용으로는 충분합니다.
"""
# 문단 단위로 먼저 나누기 (빈 줄 기준)
paragraphs = re.split(r"\n\s*\n+", text.strip())
chunks: list[str] = []
for para in paragraphs:
para = para.strip()
if not para:
continue
3-1. 문단 → 문장 → chunk 순서
- 문단 단위로 1차 분할
- \n 두 줄 이상(빈 줄)을 기준으로 문단을 나눕니다.
- 문장 단위로 2차 분할
- 마침표(.), 물음표(?), 느낌표(!) 뒤의 공백 기준으로 문장을 나눕니다.
- 각 문장들을 이어 붙이며 토큰 길이 체크
- 지금까지 모인 문장들을 하나의 chunk로 유지하다가,
새 문장을 붙였을 때 max_tokens를 넘으면 새 chunk로 끊습니다.
- 지금까지 모인 문장들을 하나의 chunk로 유지하다가,
계속 보면:
# 문장 단위로 한 번 더 나누기
sentences = re.split(r"(?<=[\.!?])\s+", para)
current = ""
for sent in sentences:
sent = sent.strip()
if not sent:
continue
# 현재 chunk에 문장을 추가했을 때 길이를 체크
candidate = (current + " " + sent).strip() if current else sent
tokenized = tokenizer(
prompt_prefix + candidate,
add_special_tokens=True,
return_attention_mask=False,
return_token_type_ids=False,
)
length = len(tokenized["input_ids"])
- current : 지금까지 모은 문장들(현재 chunk의 텍스트)
- sent : 새로 처리할 문장
- candidate : current에 sent를 붙인 가상의 텍스트
- tokenizer(...)로 프롬프트까지 포함한 전체 길이를 토큰 단위로 계산합니다.
if length <= max_tokens:
# 아직 여유 있으면 이어 붙이기
current = candidate
else:
# 지금까지 모은 current는 하나의 chunk로 확정
if current:
chunks.append(current)
- length <= max_tokens : 아직 여유 → 그냥 이어 붙이고 계속 진행.
- length > max_tokens :
- 지금까지의 current는 chunk로 확정하고 chunks 리스트에 추가.
- 이제 sent를 어떻게 처리할지 별도로 판단합니다.
3-2. 한 문장 자체가 너무 긴 특수 케이스 처리
# 바로 이 문장(sent) 자체가 너무 긴 경우 → 강제로 잘라서 넣기
tokenized_sent = tokenizer(
prompt_prefix + sent,
add_special_tokens=True,
return_attention_mask=False,
return_token_type_ids=False,
)["input_ids"]
if len(tokenized_sent) > max_tokens:
# 토큰 단위로 잘라서 여러 chunk로 분해
start = 0
# 프롬프트 부분 길이를 대략 보정
prompt_tokens = len(
tokenizer(
prompt_prefix,
add_special_tokens=True,
return_attention_mask=False,
return_token_type_ids=False,
)["input_ids"]
)
usable = max_tokens - prompt_tokens
- 어떤 문장은 자기 혼자만 넣어도 max_tokens를 넘어갈 수 있습니다.
- 예: 표, 아주 긴 문단이 한 줄에 몰려 있을 때.
- 이럴 때는 토큰 배열을 슬라이스(자르기) 해서 몇 덩어리로 강제로 나눕니다.
- prompt_tokens를 따로 구해서 프롬프트 길이를 제외한 실제 문장에 쓸 수 있는 토큰 수(usable)를 계산.
while start < len(tokenized_sent):
piece_ids = tokenized_sent[start : start + usable]
piece_text = tokenizer.decode(
piece_ids, skip_special_tokens=True
)
# 프롬프트 내용이 포함되어 있을 수 있으니 제거
piece_text = piece_text.replace(prompt_prefix, "").strip()
if piece_text:
chunks.append(piece_text)
start += usable
current = ""
- tokenized_sent에서 usable 길이만큼 잘라서 piece_ids를 만들고,
- 다시 텍스트로 디코딩하여 chunk 리스트에 추가.
- 디코딩된 텍스트에 프롬프트 문자열이 섞여 들어갈 수 있으니 replace(prompt_prefix, "")로 제거.
3-3. 일반적인 경우: 문장 단위로 새 chunk 시작
else:
# 이 문장 하나는 max_tokens 안이므로 새 chunk로 시작
current = sent
- sent 혼자서는 max_tokens 안에 들어가는 경우,
- current를 이 문장으로 초기화하고,
- 다음 문장부터 다시 이어 붙이며 길이를 체크.
3-4. 마지막 마무리
# 문단 내에서 남아 있는 chunk가 있으면 추가
if current:
chunks.append(current)
return chunks
- 루프가 끝나고 나서도 current에 내용이 남아 있으면 마지막 chunk로 추가.
- 결과적으로 chunks는 각 chunk가 토큰 길이 제한을 넘지 않는 한국어 텍스트 리스트가 됩니다.
4. 단일 chunk 번역: translate_chunk
def translate_chunk(text: str) -> str:
"""단일 chunk(짧은 문단/문장 묶음)를 번역"""
src_text = PROMPT_PREFIX + text
inputs = tokenizer(
src_text,
return_tensors="pt",
truncation=True,
max_length=MAX_INPUT_TOKENS,
)
input_ids = inputs["input_ids"].to(device)
attention_mask = inputs["attention_mask"].to(device)
with torch.no_grad():
generated_ids = model.generate(
input_ids=input_ids,
attention_mask=attention_mask,
max_length=MAX_TARGET_TOKENS,
num_beams=5,
early_stopping=True,
)
output_text = tokenizer.decode(generated_ids[0], skip_special_tokens=True)
return output_text.strip()
핵심 포인트
1. 프롬프트 + 텍스트
- src_text = PROMPT_PREFIX + text
- 학습할 때와 완전히 같은 형식으로 입력을 만들어줘야 모델이 제대로 동작합니다.
2. 토크나이징 & 텐서 변환
inputs = tokenizer(
src_text,
return_tensors="pt",
truncation=True,
max_length=MAX_INPUT_TOKENS,
)
3. 모델 추론 (생성)
with torch.no_grad():
generated_ids = model.generate(
input_ids=input_ids,
attention_mask=attention_mask,
max_length=MAX_TARGET_TOKENS,
num_beams=5,
early_stopping=True,
)
- torch.no_grad() : 추론 시에는 그래디언트 계산 필요 없으므로 메모리 절약.
- model.generate(...) :
- num_beams=5 : Beam Search 사용 (조금 더 좋은 문장을 찾기 위한 탐색 전략).
- max_length : 번역문 길이 제한.
- early_stopping=True : EOS 토큰 등이 나오면 일찍 종료.
4. 디코딩
output_text = tokenizer.decode(generated_ids[0], skip_special_tokens=True)
return output_text.strip()
- 생성된 토큰 ID를 실제 문자열(영문 번역 텍스트)로 변환.
- skip_special_tokens=True : <pad>, </s> 같은 특수 토큰 제거.
- .strip() : 앞뒤 공백 제거.
5. 전체 긴 텍스트 번역: translate_long_text
def translate_long_text(text: str) -> str:
"""기사처럼 긴 텍스트 전체를 번역"""
chunks = chunk_text(text, tokenizer, MAX_INPUT_TOKENS, PROMPT_PREFIX)
print(f"총 {len(chunks)}개 chunk로 분할되었습니다.")
translated_chunks: list[str] = []
for i, chunk in enumerate(chunks, start=1):
print(f"[{i}/{len(chunks)}] 번역 중...")
translated = translate_chunk(chunk)
translated_chunks.append(translated)
# chunk 사이에 빈 줄을 하나 두고 이어 붙이기
return "\n\n".join(translated_chunks)
1. 먼저 chunking
- chunk_text를 이용해 긴 기사 텍스트를 여러 개의 작은 chunk로 나눕니다.
- 몇 개로 잘렸는지 로그 출력: 총 N개 chunk로 분할되었습니다.
2. 각 chunk 번역 + 진행상황 표시
for i, chunk in enumerate(chunks, start=1):
print(f"[{i}/{len(chunks)}] 번역 중...")
translated = translate_chunk(chunk)
translated_chunks.append(translated)
-
- 실제 실행 시, 긴 문서를 번역하면 시간이 꽤 걸리므로 현재 몇 번째 chunk를 번역 중인지 콘솔에 표시해서 진행 상황을 알 수 있게 합니다.
3. 번역 결과 합치기
return "\n\n".join(translated_chunks)
- 각 chunk 번역 결과를 빈 줄 1줄을 사이에 두고 이어 붙여 읽기 좋은 단락 구조를 유지합니다.
6. 파일 단위 입·출력: translate_article_file
def translate_article_file(input_path: str, output_path: str | None = None):
"""
로컬 텍스트 파일(한국어 기사)을 읽어서 번역 후,
화면에 출력하고, 필요하면 output_path로 저장.
"""
input_path = Path(input_path)
text = input_path.read_text(encoding="utf-8")
print(f"입력 파일: {input_path}")
print(f"문자 수: {len(text)}\n")
translated = translate_long_text(text)
# 결과 출력 (앞부분만 맛보기로 보고 싶으면 슬라이스해서 출력해도 됨)
print("\n===== 번역 결과 (전체) =====\n")
print(translated)
if output_path:
out_path = Path(output_path)
out_path.write_text(translated, encoding="utf-8")
print(f"\n번역 결과를 '{out_path}'에 저장했습니다.")
- 입력 파일 읽기
input_path = Path(input_path)
text = input_path.read_text(encoding="utf-8")
-
- 문자열 경로를 Path 객체로 변환해서 사용.
- UTF-8 인코딩으로 전체 텍스트를 읽어옵니다.
- 기본 정보 출력
- 파일 경로와 총 문자 수를 출력해서,
“내가 어떤 파일을 번역 중인지” 확인할 수 있게 합니다.
- 파일 경로와 총 문자 수를 출력해서,
- 실제 번역 수행
translated = translate_long_text(text)
-
- 아까 만든 translate_long_text를 호출하여
긴 기사 전체를 번역합니다.
- 아까 만든 translate_long_text를 호출하여
- 결과 출력과 저장
- 콘솔에 전체 결과를 출력.
- output_path가 전달된 경우에만 파일로 저장.
- 저장 경로도 Path로 처리하고 UTF-8로 기록.
7. 스크립트 진입점: if __name__ == "__main__":
if __name__ == "__main__":
# 1) 파일에서 기사 읽어서 번역하는 경우
INPUT_FILE = "article_ko_1.txt" # 한국어 기사 텍스트 파일
OUTPUT_FILE = "article_en_1.txt" # 번역 결과 저장 파일 (원치 않으면 None)
translate_article_file(INPUT_FILE, OUTPUT_FILE)
# 2) 직접 붙여넣은 긴 문자열을 테스트하고 싶다면 아래처럼 사용
# long_text = \"\"\"여기에 한국어 기사 전체를 붙여넣으세요...\"\"\"
# result = translate_long_text(long_text)
# print(result)
- 이 부분은 스크립트를 직접 실행했을 때만 동작합니다.
- 다른 파일에서 import translate_article로 가져다 쓸 때는 실행되지 않음.
- 기본 사용 방식:
- 같은 폴더에 article_ko_1.txt 파일을 만들어 한국어 기사를 붙여넣고
- python translate_article.py 를 실행하면
- article_en_1.txt에 번역 결과가 저장됩니다.
- 주석 처리한 부분은 직접 문자열을 코드 안에 붙여넣고 테스트할 때 사용하는 예시입니다.
8. 요약
- 문제 상황
- 논문/기사처럼 긴 한국어 텍스트를 한 번에 번역하려면,
모델이 처리할 수 있는 최대 토큰 길이를 넘어서 에러가 발생하거나 품질이 떨어질 수 있습니다.
- 논문/기사처럼 긴 한국어 텍스트를 한 번에 번역하려면,
- 해결 전략
- 문단 → 문장 → 토큰 기준으로 텍스트를 적당한 크기로 잘게 나눈다.
- 각 조각(chunk)을 동일한 프롬프트 형식으로 모델에 넣어 번역한다.
- 번역된 조각들을 다시 합쳐서 하나의 영어 문서로 만든다.
- 진행 상황과 결과를 보기 쉽게 로그/파일로 정리한다.
- 이 스크립트의 장점
- 이미 파인튜닝해 둔 T5 모델을 그대로 활용할 수 있고,
- 토큰 길이 기준으로 안전하게 잘라서 긴 문서도 무리 없이 번역 가능하며,
- 구조가 단순해서 Colab / 서버 / 로컬 PC 어디서나 쉽게 재사용할 수 있습니다.
'HRDI_AI > 머신러닝_딥러닝 핵심 기술과 실무 중심 생성형 AI 프로젝트' 카테고리의 다른 글
| Day1: 인공지능 프로그래밍 준비-1 (0) | 2025.12.04 |
|---|---|
| finetune_ke_t5_ko2en.py (0) | 2025.11.28 |
| AutoModelForSeq2SeqLM의 손실함수 (0) | 2025.11.27 |
| AutoModelForSeq2SeqLM (0) | 2025.11.27 |
| AdamW란 무엇인가, 왜 쓰는가, 수식/직관/실전 세팅까지 (0) | 2025.11.27 |