저번 포스팅에 이어서 작성해보겠습니다.
5. 데이터 증강 & 생성
5.1. Augmentation - 역번역
기본으로 제공된 위협 대화 데이터는 규정상 AI로 생성하거나 외부 데이터를 가져올 수 없으므로 augmentation 과정으로만 데이터 수를 늘릴 수 있었다.
따라서 역번역을 돌리는 게 가장 효율적인 방법이라고 생각해서 이 방법을 선택했다.
5.1.1 HuggingFace
처음에는 허깅페이스의 NLLB 번역 모델(facebook/nllb-200-distilled-600M)을 밤에 돌려놓고 잤는데,
역시나 코랩의 GPU 사용량이 초과되어 중간에 멈춰서 실패했다.
import pandas as pd
import torch
from transformers import pipeline
from tqdm import tqdm
# 1. NLLB 번역 파이프라인 로드 (GPU 자동 감지)
# 600M 파라미터의 비교적 가벼운 고성능 모델을 사용
print("NLLB 번역 모델(ko->en) 로딩 중...")
translator_ko_to_en = pipeline(
'translation',
model='facebook/nllb-200-distilled-600M',
src_lang='kor_Hang', # 한국어
tgt_lang='eng_Latn' # 영어
)
print("NLLB 번역 모델(en->ko) 로딩 중...")
translator_en_to_ko = pipeline(
'translation',
model='facebook/nllb-200-distilled-600M',
src_lang='eng_Latn', # 영어
tgt_lang='kor_Hang' # 한국어
)
print("모델 로딩 완료.")
def back_translate_nllb(text):
"""
Meta AI의 NLLB 모델을 사용하여 역번역을 수행하는 함수
"""
try:
# (A) 한국어 -> 영어 번역
en_text = translator_ko_to_en(text, max_length=512)[0]['translation_text']
# (B) 영어 -> 한국어 번역
ko_text = translator_en_to_ko(en_text, max_length=512)[0]['translation_text']
return ko_text
except Exception as e:
print(f"--- 번역 오류 발생 (샘플 건너뜀): {e} ---")
return None
# 3. 원본 훈련 데이터 로드
try:
train_df = pd.read_csv("/content/drive/MyDrive/Colab Notebooks/아이펠/DLthon_pepero_day/Data/aiffel-dl-thon-dktc-online-15/train.csv")
except FileNotFoundError:
print("train.csv 파일을 찾을 수 없습니다. 경로를 확인하세요.")
exit()
print(f"원본 데이터 로드 완료. (총 {len(train_df)}개)")
# 4. 위협 클래스 (0~3) 데이터만 필터링
threat_df = train_df[train_df['class'] != '일반 대화'].copy()
normal_df = train_df[train_df['class'] == '일반 대화'].copy() # 일반 대화는 따로 보관
print(f"증강 대상 위협 데이터: {len(threat_df)}개")
# 5. 역번역 수행 (tqdm으로 진행 상황 표시)
# (이 작업은 GPU를 사용하므로 googletrans보다 훨씬 빠릅니다)
tqdm.pandas(desc="역번역 진행 중 (NLLB)")
# (선택) 데이터가 너무 많으면 1000개씩 끊어서 테스트
# threat_df = threat_df.head(1000)
# 'conversation' 컬럼의 각 행에 함수 적용
threat_df['augmented_conv'] = threat_df['conversation'].progress_apply(back_translate_nllb)
# 6. 증강된 데이터 정리
new_data_list = []
for idx, row in threat_df.iterrows():
new_conv = row['augmented_conv']
if new_conv and isinstance(new_conv, str) and new_conv.strip():
new_data_list.append({
'conversation': new_conv,
'class': row['class']
})
new_df = pd.DataFrame(new_data_list)
print(f"새롭게 증강된 데이터: {len(new_df)}개")
# 7. 원본 데이터 + 증강된 데이터 결합
# (원본 위협 + 원본 일반 + 증강된 위협)
final_augmented_df = pd.concat([threat_df, normal_df, new_df], ignore_index=True)
print(f"최종 훈련 데이터: {len(final_augmented_df)}개")
# 8. 새 훈련 파일로 저장
final_augmented_df.to_csv("/content/drive/MyDrive/Colab Notebooks/아이펠/DLthon_pepero_day/Data/aiffel-dl-thon-dktc-online-15/train_augmented_nllb.csv", index=False, encoding='utf-8-sig')
print("train_augmented_nllb.csv 파일 저장 완료!")
5.1.2. Googletrans
두 번째 방법으로 googletrans의 Translator을 사용했는데, ip 차단을 피하기 위해 청크도 나누고, 중간중간 대기하는 꼼수도 집어넣었다.
그 결과 성공적으로 역번역이 완료되었다.
import pandas as pd
import time
import numpy as np
from googletrans import Translator
from tqdm import tqdm
import random
# 1. 번역기 객체 생성
try:
translator = Translator()
except Exception as e:
print(f"Translator 객체 생성 실패: {e}")
exit()
def back_translate_google(text):
"""
googletrans를 사용한 역번역 함수 (IP 차단 방지용 딜레이 포함)
"""
try:
en_text = translator.translate(text, src='ko', dest='en').text
ko_text = translator.translate(en_text, src='en', dest='ko').text
# (중요) 샘플 1개당 딜레이
time.sleep(random.uniform(0.8, 1.3))
return ko_text
except Exception as e:
print(f"--- 번역 오류 발생 (샘플 건너뜀): {e} ---")
return None
# 3. 원본 훈련 데이터 로드
try:
train_df = pd.read_csv("/content/drive/MyDrive/Colab Notebooks/아이펠/DLthon_pepero_day/Data/aiffel-dl-thon-dktc-online-15/train.csv")
except FileNotFoundError:
print("train.csv 파일을 찾을 수 없습니다. 경로를 확인하세요.")
exit()
print(f"원본 데이터 로드 완료. (총 {len(train_df)}개)")
# 4. 위협 데이터 / 일반 대화 데이터 분리
threat_df = train_df[train_df['class'] != '일반 대화'].copy()
normal_df = train_df[train_df['class'] == '일반 대화'].copy() # 일반 대화는 따로 보관
print(f"증강 대상 위협 데이터: {len(threat_df)}개")
print(f"보존 대상 일반 대화: {len(normal_df)}개")
# 5. 위협 데이터를 1000개 단위로 분할 (IP 차단 방지)
CHUNK_SIZE = 1000
num_chunks = int(np.ceil(len(threat_df) / CHUNK_SIZE))
chunks = np.array_split(threat_df, num_chunks) # threat_df를 num_chunks 개수로 분할
print(f"{len(threat_df)}개의 위협 데이터를 {len(chunks)}개의 청크로 분할합니다.")
# 6. 청크별로 순회하며 역번역 수행
all_new_data_list = [] # 증강된 데이터를 모두 담을 리스트
for i, chunk_df in enumerate(chunks):
print(f"\n{'='*50}")
print(f"청크 {i+1}/{len(chunks)} (크기: {len(chunk_df)}) 역번역 시작...")
print(f"{'='*50}")
# tqdm 설정
tqdm.pandas(desc=f"청크 {i+1} 진행 중")
# 현재 청크에 대해 역번역 적용
chunk_df['augmented_conv'] = chunk_df['conversation'].progress_apply(back_translate_google)
# 번역 결과를 all_new_data_list에 저장
for idx, row in chunk_df.iterrows():
new_conv = row['augmented_conv']
if new_conv and isinstance(new_conv, str) and new_conv.strip():
all_new_data_list.append({
'conversation': new_conv,
'class': row['class']
})
print(f"청크 {i+1} 완료. 현재까지 총 {len(all_new_data_list)}개 증강됨.")
# 다음 청크로 넘어가기 전, IP 차단을 피하기 위해 긴 대기
if i < len(chunks) - 1: # 마지막 청크가 아니라면
print("IP 차단 방지를 위해 120초간 대기합니다...")
time.sleep(120) # 120초 대기
print("\n모든 청크 작업 완료.")
# 7. 증강된 데이터 + 원본 데이터 결합
new_df = pd.DataFrame(all_new_data_list)
print(f"새롭게 증강된 데이터: {len(new_df)}개")
# 원본 위협 데이터 + 원본 일반 대화 + 증강된 위협 데이터
final_augmented_df = pd.concat([threat_df, normal_df, new_df], ignore_index=True)
print(f"최종 훈련 데이터: {len(final_augmented_df)}개")
# 8. 새 훈련 파일로 저장
final_augmented_df.to_csv("/content/drive/MyDrive/Colab Notebooks/아이펠/DLthon_pepero_day/Data/aiffel-dl-thon-dktc-online-15/train_augmented.csv", index=False, encoding='utf-8-sig')
print("train_augmented.csv 파일 저장 완료!")
/usr/local/lib/python3.12/dist-packages/numpy/_core/fromnumeric.py:57: FutureWarning: 'DataFrame.swapaxes' is deprecated and will be removed in a future version. Please use 'DataFrame.transpose' instead.
return bound(*args, **kwds)
원본 데이터 로드 완료. (총 4950개)
증강 대상 위협 데이터: 3950개
보존 대상 일반 대화: 1000개
3950개의 위협 데이터를 4개의 청크로 분할합니다.
==================================================
청크 1/4 (크기: 988) 역번역 시작...
==================================================
청크 1 진행 중: 4%|▍ | 41/988 [02:53<1:13:40, 4.67s/it]--- 번역 오류 발생 (샘플 건너뜀): The read operation timed out ---
청크 1 진행 중: 78%|███████▊ | 768/988 [53:59<18:25, 5.02s/it]--- 번역 오류 발생 (샘플 건너뜀): The read operation timed out ---
청크 1 진행 중: 83%|████████▎ | 823/988 [58:12<13:30, 4.91s/it]--- 번역 오류 발생 (샘플 건너뜀): The read operation timed out ---
청크 1 진행 중: 90%|█████████ | 894/988 [1:03:34<07:40, 4.90s/it]--- 번역 오류 발생 (샘플 건너뜀): The read operation timed out ---
청크 1 진행 중: 96%|█████████▋| 953/988 [1:07:55<02:56, 5.04s/it]--- 번역 오류 발생 (샘플 건너뜀): The read operation timed out ---
청크 1 진행 중: 100%|██████████| 988/988 [1:10:29<00:00, 4.28s/it]
청크 1 완료. 현재까지 총 983개 증강됨.
IP 차단 방지를 위해 120초간 대기합니다...
==================================================
청크 2/4 (크기: 988) 역번역 시작...
==================================================
청크 2 진행 중: 5%|▍ | 48/988 [03:18<1:06:40, 4.26s/it]--- 번역 오류 발생 (샘플 건너뜀): The read operation timed out ---
청크 2 진행 중: 18%|█▊ | 175/988 [12:24<58:32, 4.32s/it]--- 번역 오류 발생 (샘플 건너뜀): The read operation timed out ---
청크 2 진행 중: 50%|█████ | 497/988 [34:32<34:04, 4.16s/it]--- 번역 오류 발생 (샘플 건너뜀): The read operation timed out ---
청크 2 진행 중: 52%|█████▏ | 515/988 [35:48<33:48, 4.29s/it]--- 번역 오류 발생 (샘플 건너뜀): The read operation timed out ---
청크 2 진행 중: 58%|█████▊ | 573/988 [40:05<38:53, 5.62s/it]--- 번역 오류 발생 (샘플 건너뜀): The read operation timed out ---
청크 2 진행 중: 70%|██████▉ | 687/988 [48:01<26:51, 5.35s/it]--- 번역 오류 발생 (샘플 건너뜀): The read operation timed out ---
청크 2 진행 중: 73%|███████▎ | 726/988 [50:57<27:15, 6.24s/it]--- 번역 오류 발생 (샘플 건너뜀): The read operation timed out ---
청크 2 진행 중: 77%|███████▋ | 765/988 [53:32<16:48, 4.52s/it]--- 번역 오류 발생 (샘플 건너뜀): The read operation timed out ---
청크 2 진행 중: 100%|██████████| 988/988 [1:08:18<00:00, 4.15s/it]
청크 2 완료. 현재까지 총 1963개 증강됨.
IP 차단 방지를 위해 120초간 대기합니다...
==================================================
청크 3/4 (크기: 987) 역번역 시작...
==================================================
청크 3 진행 중: 100%|██████████| 987/987 [1:01:50<00:00, 3.76s/it]
청크 3 완료. 현재까지 총 2950개 증강됨.
IP 차단 방지를 위해 120초간 대기합니다...
==================================================
청크 4/4 (크기: 987) 역번역 시작...
==================================================
청크 4 진행 중: 100%|██████████| 987/987 [1:02:17<00:00, 3.79s/it]청크 4 완료. 현재까지 총 3937개 증강됨.
모든 청크 작업 완료.
새롭게 증강된 데이터: 3937개
최종 훈련 데이터: 8887개
train_augmented.csv 파일 저장 완료!
5.2. 일반 대화 생성
이 파트가 이번 프로젝트의 핵심이자 가장 어려운 단계였다고 느꼈다.
나는 Gemini CLI에게 프롬프트를 줘서 생성을 시켰고, 팀원 중 한 분은 파이썬 코드 상에서 생성기를 돌리기도 했다.
결론만 말하자면 생성기보다는 Gemini CLI가 품질이 더 좋았다. 생성기가 만든 문장들은 자기들끼리의 패턴이 매우 유사했다. 물론 Gemini도 같은 현상이 있긴 했지만 프롬프트를 줘서 다시 수정이 가능했다.
데이터를 생성하고 수정한 흐름을 간략히 적어보자면,
- 1. 기본 train (4000) + 역번역 (4000) + Gemini 일반 대화.v1 (2000)
- 일반 대화의 턴이 기본 제공된 위협 대화 턴의 절반이라서 test 검증 결과, 일반 대화를 거의 잡아내지 못 했다.
- 따라서 턴을 두 배로 늘려 다시 시도했다.

- 2. 기본 train (4000) + 역번역 (4000) + Gemini 일반 대화.v2 (2000)
- 턴을 두 배(10턴)로 늘린 일반 대화로 test 해보니 늘리기 전보단 loss가 낮았지만 그래도 만족할만한 성능은 아니었다. (캐글 제출 결과 f1 스코어 0.63)
- 그리고 나서 "공격적인 단어와 비속어가 포함된 일반 대화"를 추가로 생성했는데, 그 이유는 아래와 같다.
- (1) 예측이 틀린 test 데이터의 일반 대화를 읽어보니, 공격적인 단어와 비속어가 포함된 일반 대화였다.
- (2) 위에서 언급한 1D CNN의 단점인 "Max pooling으로 인한 정보 손실"로 위협 대화와 공격적 단어가 포함된 일반 대화 구분이 어려울 것이라고 생각했다.
- 그래서 다음 시도에는 기존 일반 대화 1500개와 공격적인 일반 대화 500개를 추가로 생성해서 데이터셋에 포함시켰다.
- 3. 기본 train (4000) + 역번역 (4000) + Gemini 일반 대화.v2 (1500) + Gemini 공격적 일반 대화 (500)
- 캐글 제출 결과 f1 스코어 0.65로 적지만 성능 향상을 보긴 했다.
- 이 점수는 오랜 시간과 노력을 들여 만든 AI 생성 데이터의 한계라고 생각해서,
- 다음으로는 AI Hub의 실제 메신저 데이터를 정제해서 사용했다.
- 4. 기본 train (4000) + 역번역 (4000) + AI Hub (1500) + Gemini 공격적 일반 대화 (500)
- 캐글 제출 결과 f1 스코어 0.79로 대폭 향상되었다.
- 이때 위에서 언급한 GIGO를 뼈저리게 느꼈다.
- (AI Hub 데이터 정제 과정은 분량이 너무 길어져서 생략)
6. Ablation Study
이렇게 생성한 전처리, 토큰화, 데이터로더, 모델, 데이터를 모두 종합해서
팀원들끼리 각자 맡은 모델을 학습시키며 Ablation study를 진행했다.

그 중, 1D CNN만 뽑아보면 아래와 같다.

결론은 [2, 3, 4] 사이즈의 필터와 256 크기의 필터 개수를 사용한 모델이 가장 낮은 loss를 보여 최종 모델로 선택했다.
이렇게 다른 변인을 통제하고 하이퍼파라미터만 바꿔가며 비교하고 표로 정리하니, 비교도 편하고 가독성이 좋아서 앞으로도 이런 방식으로 Ablation study를 진행해야겠다고 느꼈다.
회고
1. 발표
이렇게 프로젝트를 마치고 마지막 날에 발표도 잘 하고 끝냈다.
발표하면서 받은 피드백은 거의 ppt 구성이었는데,
"왜 필터 사이즈를 [2, 3, 4]로 했는지? 데이터를 직접 보고 판단해서 그렇게 정한 것인지?" 라는 질문이 들어왔다.
Ablation Study 하면서 최적의 필터 사이즈를 찾은 건데, 발표할 당시 시간이 매우 촉박했고, 15분 안에 끝내야 한다는 생각에 ablation study 부분을 설명할 때 최종 하이퍼파라미터를 짧게라도 언급하고 지나갔어야 했는데, 그냥 표만 제시하고 ~~이렇게 나왔다~ 라는 식으로 끝내서 청자들에게 전달이 잘 되지 않은 것 같다. 다음부터는 더 꼼꼼히 전달하는 연습을 해야겠다.
2. 마찰
사실 팀 plan을 짤 때 다른 두 팀원의 마찰이 있었는데,
제3자가 자세히 적긴 뭐해서 결론만 말하자면, 필자가 중간에서 두 분의 입장을 글로 정리해보고 개인적으로 얘기 나누며 결국에는 오해를 풀었고, 마지막에는 타 팀보다 더 사이 좋은 팀이 된 느낌이다. (역시 남자들은 싸우면 친해진다.)
결론적으로 소프트 스킬의 중요성을 배우게 된 사건이었다.
3. EDA
타 팀의 발표를 보니 EDA를 통해 각 클래스에서 공통적으로 자주 등장하는 단어들을 전처리 단계에서 제거한 것이 기억에 남았다.
난 왜 그런 생각을 못 했을까 싶으면서도 한편으로는 모델과 데이터 생성에만 집중하다 보니 그랬나? 하는 생각도 들었다.
그래서 EDA는 필수라는 생각이 들었다.
4. Github
팀원 중 한 분이 컴공이라 깃헙에 대한 지식과 기술이 있을 것이라고 얘기가 나와서 그 분이 깃헙 관리를 맡아서 해주셨다.
레포 구조 짜기, PR 받아서 conflict 안 나게 merge하기, 코드, 데이터 파일 관리하기 등등,,, 할 게 굉장히 많아 보이고 애쓰시는 게 보였다. 이번 해커톤으로 깃헙을 이용한 협업을 처음 해봤는데, 아이펠 과정에서 연습해두면 나중에 굉장히 유용할 것 같다는 생각이 들었다.
저번 포스팅의 맨 처음에 언급했었지만, 마지막으로 느낀 점을 적어보자면,
- GIGO (Garbage In Garbage Out)를 절실히 느꼈다.
- LLM이 생성하는 데이터는 한계가 명확하다.
- EDA는 필수다.
- 협업을 위한 Github 관리 능력의 중요성을 깨달았다.
- Soft skill도 Hard skill 만큼 중요하다.
발표 전날에는 거의 밤새서 완성했는데, 다 끝내고 보니 참 의미있던 과정이었다.
'AI > NLP' 카테고리의 다른 글
| [11/17] 아이펠 리서치 15기 TIL | vocab_size에 따른 ML 모델의 성능 비교 (뉴스 카테고리 다중 분류) (0) | 2025.11.27 |
|---|---|
| [11/13] 아이펠 리서치 15기 TIL | Going-Deeper 시작, 형태소 분석기 (1) | 2025.11.21 |
| [11/6~11/11] 아이펠 리서치 15기 TIL | DLthon: 감정 분류 해커톤 (1) (0) | 2025.11.15 |
| [11/05] 아이펠 리서치 15기 TIL | Transformer 구현 (0) | 2025.11.15 |
| [10/31] 아이펠 리서치 15기 TIL | Transformer (0) | 2025.11.05 |