목차

  1. 들어가며: 지능형 AI 에이전트를 향한 첫걸음
  2. 대회 개요: Upstage AI Lab Competition
  3. 프로젝트 소개: 과학 기술 문서 기반 질의응답 시스템
  4. 주요 기능 및 개발 과정: 더 나은 답변을 찾기 위한 여정
  5. 프로젝트 회고: 성장과 학습의 기록
  6. 마치며: 대회를 넘어, 계속될 프로젝트

1. 🎯 들어가며: 똑똑한 AI 에이전트를 향한 첫걸음

이번 프로젝트는 제가 가장 기대했던 Retrieval Augmented Generation (RAG) 기반의 Q&A 시스템을 구축하는 프로젝트 였습니다. 원하는 정보를 정확하고 필요한 형식으로 가져오는 기술은 매우 강력하기 때문입니다. 방대한 데이터 속에서 단순한 내용 검색을 넘어, 상황까지 이해하고 적절한 답변을 제공하는 지능형 에이전트는 회사 내부 개발 문서 검색이나 법률 질의응답 등 다양한 도메인에서 높은 수요를 가지고 있습니다.

특히 RAG 기술을 과학 문서와 같은 특정한 전문 도메인에 맞춰 커스터마이징하고 질의응답 시스템을 구축할 수 있다면, 그 활용 가능성은 무궁무진해집니다. 이 경험은 AI 모델 구현력을 한층 더 깊게 만들어 줄 것이라 확신하며 배우고 싶은 열정이 많았습니다.

이 글을 통해 저희 팀이 더 나은 Q&A 시스템을 만들기 위해 어떤 고민을 했고, 어떤 기술들을 적용했으며, 그 과정에서 무엇을 배우고 느꼈는지 공유하고자 합니다.

Github Repository:

Information Retrieval for Scientific Documents Q&A


2. 대회 개요: Upstage AI Lab Competition

개요

Upstage AI Lab에서 주최한 이번 Information Retrieval 대회는 '과학상식 기반 질의응답'을 주제로 진행되었습니다. 주어진 과학 기술 문서들을 바탕으로 사용자의 질문에 가장 적절한 답변을 찾아내는 Q&A 시스템을 구축하는 것이 목표였습니다.

평가방법

대회에서 사용된 평가 지표는 수정된 MAP(Mean Average Precision)입니다. 이번 평가는 RAG 시스템이 생성한 최종 답변이 아닌, 질문에 적합한 레퍼런스 문서를 얼마나 잘 찾아내는가, 즉 검색(Retrieval) 단계의 성능에만 초점을 맞췄습니다.

가장 큰 특징은 기존 MAP 계산 방식에 특별한 규칙이 추가되었다는 점입니다. 대회에서 다루는 질문 중에는 과학 상식과 무관하여 문서 검색이 필요 없는 경우도 있었습니다. 이러한 질문에 대해서는 문서를 검색하지 않은 경우를 정답(1점)으로 처리하고, 불필요하게 문서를 검색한 경우에는 0점을 부여했습니다. 과학 관련 질문에 대해서만 기존의 MAP 계산 로직이 적용되었습니다


3. 🚀 프로젝트 소개: 과학 기술 문서 기반 질의응답 시스템

RAG, LLM과 어떻게 다를까요?

프로젝트를 소개하기 앞서, RAG가 LLM과 어떻게 다른지 짚고 넘어가는 것이 중요합니다. LLM은 방대한 데이터를 학습했지만, 학습 시점 이후의 실시간 정보나 특정 분야의 깊이 있는 정보는 알지 못해 잘못된 답변(Hallucination)을 할 수 있습니다. RAG는 이 문제를 해결하기 위한 효과적인 접근법입니다.

  • LLM (Large Language Model): 초거대 언어 모델로서 방대한 정보를 바탕으로 학습되어 아는 것이 많습니다. 하지만 대중적인 지식 위주로 학습되었고, 최신 정보가 실시간으로 업데이트되지 않아 오래된 정보를 제공할 수 있다는 단점이 있습니다.
  • RAG (Retrieval Augmented Generation): LLM이 외부의 최신 정보나 특정 지식에 접근할 수 있도록 설계된 기술입니다. 외부 데이터 소스나 도구를 활용하여 LLM의 한계를 보완하는 이 기술은 생각보다 복잡한 파이프라인으로 구성됩니다.

RAG는 먼저 외부 데이터를 잘게 쪼개고(Chunking) 검색이 용이하도록 가공하여 벡터 데이터베이스에 저장합니다. 사용자의 질문이 들어오면, 이 데이터베이스에서 가장 관련성 높은 정보를 검색(Retrieve)하고, 이 정보를 LLM에게 '참고 자료'로 함께 전달합니다. 최종적으로 LLM은 이 참고 자료를 바탕으로 신뢰도 높은 답변을 생성(Generate)합니다.

기본 이론

https://towardsdatascience.com/retrieval-augmented-generation-rag-from-theory-to-langchain-implementation-4e9bd5f6a4f2

기본 개념은 간단 합니다. 하지만, 시스템을 개발하는 개발자는 구성품 뿐만 아니라 작업 흐름을 이해하고 성능을 fine-tuning 해야해서 이해해야 하는 구성품와 RAG 구현하면서 해결 해야 하는 문제들은 아래 그림을 보면 미리보기 할 수 있습니다.

개발자가 생각하는 문제들

출처: https://datasciencedojo.com/blog/rag-framework-challenges-in-llm/

RAG를 배우는 것은 LLM을 일부 배우는 것을 포함하고 있으며 저는 인공지능 전공자가 아니고 LLM을 공부할 생각이 없었는데 이번 대회에서 LLM, LM, 임베딩 모델, 리트리버 모델, 도구 호출 모델, 등 모두 생각보게 되었습니다. 효율적인 답변을 생성하기 위해서 어떻게 높은 품질의 답변이 가능한지 신기해서 항상 흥미럽다 생각했는데 이제 그런 답변을 생성하기 위해서 얼마나 많은 반복적인 프롬트 엔지니어링와 테스팅을 통해서 가능한지 알게 되었고 구현하면서 결과가 상용 같이 않아서 시장에서 누구나 져렴하게 접근할 수 있는 LLM 대해서 감상하는 방법을 배우기도 했습니다. 인공지능 개발하는 일은 정말 엄청난 집중력와 노고가 요구되는 것 같습니다.

저희 팀의 접근 방식

flowchart TD
    A[👤 사용자 질문] --> B[🔌 API 계층]  
    B --> C[🧠 질문 인코딩]  
    C --> D{🔍 검색 전략}

    D -->|Sparse| E[📝 BM25 검색]  
    D -->|Dense| F[🎯 벡터 검색]

    E --> G[🔀 하이브리드 리랭킹]  
    F --> G

    G --> H{⚡ 캐시 확인}  
    H -->|Hit| I[📊 결과 반환]  
    H -->|Miss| J[💾 저장 후 반환]

    I --> K[📈 성능 평가]  
    J --> K

    style A fill:#e1f5fe
    style K fill:#f3e5f5
    style G fill:#fff3e0

저희 팀은 부트캠프에서 제공된 베이스라인 코드를 기반으로, 각자 시도해보고 싶은 접근법을 적용하며 과학 기술 문서 Q&A 시스템의 성능을 개선해 나가는 방식으로 실험을 진행했습니다.

  • 베이스라인 기술 스택
    • Python, Pytorch
    • Korean Indexing: Nori tokenizer
    • Embedding: KR-SBERT-V40K-klueNLI-augSTS (768D)
    • Retriever/Tool Calling: GPT-3.5
    • Vector Database: Elasticsearch
    • LLM: OpenAI API
  • 베이스라인 성능
    • 샘플 제출 파일 성능:
    • 베이스라인 리팩터링 후 성능 (동일):

샘플 제출 파일 성능

베이스라인 리팩터링 후 성능 (동일)


4. 🛠️ 주요 기능 및 개발 과정: 더 나은 답변을 찾기 위한 여정

저희는 더 정확하고 효율적인 Q&A 시스템을 만들기 위해 다음과 같은 기능들을 개발하고 실험했습니다.

데이터 기반 강화 (Data Foundation)

  • 향상된 과학문서 데이터셋 생성: 원본 데이터셋은 정보는 많지만 검색이 용이하지 않았습니다. 이를 해결하기 위해 문서 요약(Summary), 핵심 키워드(Keyword), 그리고 가상 질문(Hypothetical Question)을 생성하여 메타데이터로 추가했습니다. 이를 통해 일상적인 질문과 전문적인 문서 내용 사이의 간극을 줄이고자 했습니다.
  • 균형 잡힌 평가 데이터셋 생성: 제공된 평가 데이터셋이 없어 자체적으로 제작해야 했습니다. 초기 데이터셋은 특정 도메인에 편중되는 문제가 있어, 다양한 과학 분야를 고르게 포함하도록 균형을 맞춘 새로운 평가 데이터셋을 생성하여 보다 신뢰도 높은 평가가 가능하도록 했습니다.
  • 임베딩 모델 파인튜닝 (Finetuning): 우리가 직접 강화한 데이터셋을 기반으로 임베딩 모델을 파인튜닝하여, 과학 도메인에 특화된 검색(Retrieval) 성능을 끌어올렸습니다.

핵심 기술 스택 (Core Technology Stack)

  • 다양한 Retriever 및 Re-ranker 모델 실험: BM25, DPR, ColBERT와 같은 Retriever 모델과 bge-reranker, Llama-3 등 다양한 Re-ranker 모델을 실험하며 최적의 조합을 찾기 위해 노력했습니다.
  • 질의응답 캐싱 (Redis Caching): 반복적인 질문에 대해 빠른 응답 속도를 제공하고 시스템 부하를 줄이기 위해 Redis를 활용한 캐싱 시스템을 도입했습니다.

분석 및 개발 도구 (Analysis & Development Tools)

  • WandB를 활용한 추론 결과 시각화: 실험 결과를 눈으로만 이해하기에는 한계가 있었습니다. WandB를 연동하여 도메인별 성공/실패율, 성능 분포 등 추론 결과를 다각적으로 시각화하고 분석하여 모델 개선의 인사이트를 얻었습니다.
  • Streamlit 기반 비교 분석 대시보드: 통계적인 분석만으로는 부족했습니다. 하이퍼파라미터 변경이나 새로운 기법 적용 시, 각 샘플의 추론 결과가 어떻게 변하는지 질적으로 평가하기 위해 Streamlit UI를 개발했습니다. 이를 통해 평가 데이터, 기존 추론 결과, 새로운 추론 결과를 나란히 놓고 비교하며 변화를 직관적으로 파악할 수 있었습니다.
  • 단위 테스트(Unit Test) 도입: 복잡한 기능들이 추가되면서 디버깅이 점점 어려워졌습니다. AI 에이전트조차 해결하지 못하는 버그를 겪으며 개발 방식의 전환을 결심했고, 모듈별로 코드를 리팩터링하며 단위 테스트를 도입했습니다. 이를 통해 코드의 안정성을 확보하고, 설정 변경 시 문제가 어디서 발생하는지 체계적으로 추적할 수 있었습니다.
  • 대화형 CLI 메뉴 도입: 기능이 많아질수록 코드를 작성하고 터미널 명령어를 관리하는 것은 점점 어려워집니다. 체계적인 관리가 없다면 인적 오류가 발생할 가능성이 높고, 이는 비효율로 이어집니다. 특히 다른 사람에게 사용법을 전달할 때 발생할 수 있는 혼동과 오류를 예방하고, 사용 편의성을 개선하기 위해 대화형 CLI 메뉴를 도입했습니다.

IR 프로젝트 - 대화형 CLI

  1. Setup & Infrastructure: 의존성 설치, 환경 설정, 로컬 서비스(Elasticsearch, Redis) 시작/중지 등 프로젝트 인프라 설정 및 관리 기능을 제공합니다.

  1. Data Management: Elasticsearch에 문서를 효율적으로 색인하고 데이터 품질을 검증하는 등 데이터 관리 작업을 지원합니다.

  1. Experiments & Validation: 기본 검증부터 Ollama 모델을 활용한 고급 실험까지 병렬 처리를 포함한 다양한 검색 성능 검증 옵션을 제공합니다.

  1. Evaluation & Submission: 다양한 모델(OpenAI, Qwen2, Llama 등)을 선택하여 대회 제출용 파일을 생성하고 평가하는 작업을 수행합니다
     
  2. Translation: 검증 데이터 및 문서 번역(Ollama/Google), Redis 캐시 관리 등 번역 관련 기능을 지원합니다.
  3. Utilities: 시스템 상태 확인, 스크립트 목록 조회, Streamlit UI 실행 등 프로젝트 유지보수 및 모니터링에 유용한 기능을 제공합니다.

📊 결과

  • Private Dataset Results
    • MAP: 0.9000
    • MRR: 0.9030
  • Public Dataset Results
    • MAP: 0.8455
    • MRR: 0.8500

대회에서 저희 팀은 3위를 차지했습니다. 다만, 저는 팀의 최종 점수 달성 과정에서 개인적인 실험 결과를 제때 반영하지 못해 기여도에 대한 아쉬움이 남습니다. 프로젝트 후반부에 발생한 파이프라인 버그를 수정하는 데 많은 시간을 쏟느라 최종 제출 시점을 놓쳤고, 결국 제가 개선한 모델의 성능을 최종 점수에 반영하지 못했습니다.

팀의 최고 점수 달성에 크게 기여하지 못해 아쉬움이 남지만, 대회 성적을 넘어 향후 프로덕션 수준으로 발전시킬 수 있는 견고한 프로젝트 기반을 만들었다는 점에서 개인적으로 큰 의미를 찾고 있습니다.


5. 🌱 프로젝트 회고: 성장과 학습의 기록

👍 좋았던 점 (What Went Well)

가장 만족스러웠던 점은 데이터 분석 및 평가를 위한 효율적인 도구를 직접 구축한 것입니다. 단순히 모델을 만드는 것을 넘어, 결과를 체계적으로 분석할 수 있는 환경을 만들었다는 점에서 큰 성취감을 느꼈습니다.

  • Streamlit 비교 분석 대시보드: 개별 추론 결과를 기존 결과와 나란히 비교하며 질적 분석을 할 수 있었던 것이 모델 개선에 큰 도움이 되었습니다. 매번 파일을 열어보던 번거로움을 없애고 분석의 효율을 극대화했습니다.
  • WandB 통계 분석 대시보드: 전체적인 성능을 도메인별, 성공/실패별 등 다양한 기준으로 시각화하여 통계적 분석을 용이하게 했습니다.

🤔 아쉬웠던 점 (What Could Be Improved)

  • 팀 시너지 부족: RAG라는 주제를 처음 다루다 보니, 각자 어떤 부분에 어떻게 기여해야 할지 명확히 파악하기 어려웠습니다. 개인적으로는 기여했다고 생각하지만, 팀 전체의 시너지를 극대화하지 못한 점이 아쉬움으로 남습니다.

🔥 어려웠던 점 (Challenges Faced)

  • 예상치 못한 디버깅 시간: 대회 후반, 복잡해진 시스템의 버그를 해결하는 데 많은 시간을 쏟았습니다. 결국 대회가 끝날 무렵에야 문제가 해결되어, 충분한 실험을 진행하지 못하고 좋은 점수를 얻지 못한 것이 가장 아쉽습니다.
  • 까다로운 인덱스 관리: 여러 임베딩 모델을 실험하면서 모델 간 호환성 문제가 발생할 수 있다는 점, 그리고 인덱스가 존재하더라도 임베딩 벡터가 다를 수 있다는 미묘한 차이를 어렵게 깨달았습니다.

🤝 팀 기여 (Contributions to the Team)

  • 개선된 베이스라인 코드 공유
  • Summary, Keywords, Hypothetical Questions가 포함된 합성 데이터셋 생성 및 공유
  • 최종 README.md 작성 및 리포지토리 마무리 정리

💡 배운 점 (Lessons Learned)

  • 테스트 기반 개발의 중요성: 잘 작성된 문서는 물론, 안정적인 개발을 위해 단위 테스트는 선택이 아닌 필수라는 것을 절실히 깨달았습니다. 앞으로는 무조건 테스트 기반으로 개발을 진행할 것입니다.
  • Local 모델 vs. 상용 API: 로컬 모델은 API 의존성 없이 개발할 수 있다는 장점이 있지만, 상용 API 모델에 비해 지시를 따르는 능력이나 전반적인 성능에 큰 차이가 있다는 것을 알게 되었습니다. 특히 복잡한 프롬프트 파이프라인을 로컬 모델이 모두 소화할 수 있는지, 인프라 요구사항은 어떤지 반드시 고려해야 합니다.
  • 개발 프로세스의 중요성: 인공지능 프로젝트를 진행 하면서 인공지능와 전혀 관련 없는 문제들이 더 많은 것 같습니다. 인공지능 모델을 연구하고 개발하면서 경쟁력을 갖추기 위해서는 소프트웨어 개발, 디버깅, 데이타 검증 프로세스를 잘 이해해야 프로젝트 규모가 커지면서 관리하기 용이하게 관리할 수 있습니다.

6. ✨ 마치며: 대회를 넘어, 계속될 프로젝트

이번 프로젝트는 단순히 하나의 대회를 넘어, Information Retrieval 분야에 대해 깊이 있게 탐구하고, 팀원들과 함께 성장할 수 있었던 소중한 경험이었습니다. 이번 프로젝트를 통해 얻은 지식과 경험을 바탕으로, 더 나은 Q&A 시스템은 물론, 유사한 기술 스택을 활용한 챗봇 시스템, 코딩 에이전트, 멀티 에이전트 파이프라인 등 새로운 챌린지에 도전하며 계속 발전해 나갈 계획입니다. 이번 프로젝트를 통해 이전보다 더 큰 자신감과 배우고 싶은 열정이 생긴 것 같습니다.

 

배우면서 즐길 수 있는 것만큼 좋은 것은 없는 것 같습니다. 제게 소원이 하나 있다면, 부자가 되는 것보다 평생 즐겁게 배울 수 있는 기회를 갖는 것입니다. 이만 글을 마치며, 다음 포스팅에서 뵙겠습니다.

목차

  1. 1. 들어가며: 왜 블로그 자동화 에이전트인가?
  2. 2. 대회 개요: LangChain 프로젝트의 시작
  3. 3. 프로젝트 소개: PDF를 블로그 포스트로 변환하는 여정
  4. 4. 주요 기능 및 작동 방식
  5. 5. 프로젝트 회고: 성장과 배움의 기록
  6. 6. 맺음말: 대회를 넘어, 계속될 프로젝트

1. 들어가며: 왜 블로그 자동화 에이전트인가?

업스테이지 AI 부트캠프의 LangChain 프로젝트 대회에 참여하며 얻은 경험과 저희 팀의 결과물을 공유하고, 프로젝트에 대한 회고를 남겨보고자 합니다. 부트캠프 과정을 수강하다 보면 방대한 양의 교육 콘텐츠를 소화해야 합니다. 복잡하고 상세한 내용을 모두 기억하기란 현실적으로 어렵죠. 배운 것을 온전히 내 것으로 만들려면 별도의 학습과 정리가 필수적이지만, 막상 블로그에 정리하려고 하면 시간이 부족할 때가 많습니다. 자료를 준비하고, 생각을 다듬어 높은 품질의 글을 쓰는 데 며칠이 걸리기도 하고요. 이 과정을 더 효율적이고 체계적으로 만들 방법이 필요했습니다.

 

그래서 저희 팀은 '학습 자료(PDF)를 기반으로 블로그 초안을 자동으로 생성하고 발행까지 해주는 에이전트'를 만들기로 했습니다. 단순히 기존 LLM을 사용하는 것을 넘어, 우리만의 LLM 파이프라인을 직접 구축하며 기술의 핵심을 이해하고 싶었습니다. 내가 제어할 수 없는 남의 기술에 의존하기보다, 직접 미래를 만들어가는 경험이 더 가치 있다고 생각했기 때문입니다.

 

이 아이디어는 학습 블로그 외에도 신제품 소개, 기술 트러블슈팅 공유, 기업 홍보 등 다양한 분야로 확장될 수 있어 더욱 매력적이었습니다.


2. 대회 개요: LangChain 프로젝트의 시작

이번 대회는 자유 주제로 LangChain 프로젝트를 진행하는 것이었습니다. 단순히 Kaggle 점수나 모델 성능을 경쟁하는 것이 아니라, 프로젝트의 전반적인 완성도를 종합적으로 평가받는 자리였습니다. 평가 항목에는 기능 완성도, 기획, 아키텍처 설계, LLM 프롬프트 설계, 데이터 처리, UI, 코드 품질, 그리고 팀워크까지 포함되었습니다.


3. 프로젝트 소개: PDF를 블로그 포스트로 변환하는 여정

저희가 개발한 'Blog Content Creator Agent'는 PDF 문서를 고품질 블로그 포스트로 자동 변환하고 GitHub Pages에 게시하는 LangChain 기반 자동화 시스템입니다.

[*Review] 이 프로젝트의 핵심은 RAG(Retrieval-Augmented Generation) 파이프라인입니다. 이 구조를 통해 시스템은 사용자가 제공한 PDF 문서의 내용을 깊이 이해하고 핵심을 요약하여 블로그 초안을 생성합니다. 여기에 웹 검색 도구를 연동하여 최신 정보를 보강하고, 대화형 인터페이스를 통해 사용자의 수정 요청을 실시간으로 반영하며 콘텐츠의 완성도를 높입니다.

GitHub Repository: Link

 


4. 주요 기능 및 작동 방식

저희 팀이 만든 블로그 글 생성기의 주요 기능들을 소개합니다.

초안 생성 및 실시간 편집

핵심 기능은 라이브 편집기와 AI 챗봇이 결합된 인터페이스입니다. 사용자가 PDF를 업로드하면, 에이전트가 프롬프트를 거쳐 1차 초안을 생성해 좌측 편집기에 보여줍니다. 사용자는 우측의 대화창을 통해 AI와 대화하며 실시간으로 내용을 수정하고 보강할 수 있습니다.

생성된 콘텐츠는 마크다운 형식으로 즉시 미리보기가 가능하여 사용자 편의성을 높였습니다.

웹 검색을 통한 정보 보강

정확하고 시의성 있는 글을 위해 AI 에이전트가 실시간 웹 검색을 통해 최신 정보를 찾아 콘텐츠에 반영할 수 있도록 구현했습니다. 이를 통해 정적인 문서를 기반으로 하더라도 풍부하고 현재성 있는 블로그 글을 만들 수 있습니다.

GitHub Pages 자동 발행

편집이 완료된 글은 발행 단계로 넘어가 제목, 카테고리, 태그를 설정한 뒤 버튼 클릭 한 번으로 자신의 GitHub Pages 블로그에 자동으로 게시할 수 있습니다.

 

발행된 글의 예시입니다. 저희는 Chirpy 테마 라이브러리를 활용하여 블로그의 메뉴, 사이드바, 목차 등의 요소들을 매번 구현해야 하는 번거로움을 줄였습니다. 아래는 부트캠프 발표용 PDF 자료를 입력하여 생성된 결과물인데, AI가 발표 자료의 맥락을 분석해 체계적인 목차를 스스로 생성한 것을 보고 깊은 인상을 받았습니다.

 


5. 프로젝트 회고: 성장과 배움의 기록

이번 LangChain 프로젝트는 단순한 과제를 넘어, 실용적이고 가치 있는 결과물을 만드는 소중한 기회였습니다. AI를 활용해 나만의 지능을 가진 프로그램을 만드는 경험은 웹 개발자로서 정해진 규칙 안에서만 움직이던 경험과는 또 다른, 놀라운 만족감을 주었습니다.

좋았던 점 (What Went Well)

  • 흥미로운 연구 주제: 일상과 업무에 바로 적용할 수 있는 재미있는 주제를 다룰 수 있었습니다.
  • 지속 개발 가능한 프로젝트: 대회로 끝나지 않고 앞으로도 계속 개선해나갈 수 있는 의미 있는 결과물을 만들었습니다.
  • PDF 파싱 지식 습득: 평소 관심 있던 PDF 파싱 기술을 직접 다루고 적용해보는 좋은 기회였습니다.
  • 실무 기술 적용: LangChain과 RAG 같은 최신 기술을 직접 구현하며 실무 역량을 키울 수 있었습니다.
  • 서로 배움의 장: 다른 팀들의 발표를 보며 다양한 주제 선정 배경과 접근 방식을 배울 수 있어 유익했습니다.

아쉬웠던 점 (What Could Be Better)

  • 발표 데모 오작동: 라이브 데모 중 예상치 못한 메모리 부족으로 프로그램이 멈춰 아쉬웠습니다. 속도 문제를 고려해 여러 브라우저 창을 미리 열어둔 것이 원인이었습니다. 결과물을 제대로 보여드리지 못해 속상했지만, 실제 환경에서의 예외 처리와 리소스 관리의 중요성을 깨닫는 계기가 되었습니다.

어려웠던 점 (Challenges Faced)

  • 짧은 개발 일정: 대회 기간이 면접 일정과 겹쳐 4일 정도 프로젝트에 온전히 집중하지 못해 아쉬움이 남습니다.
  • 서버 초기화: 개발 중이던 서버가 두 번이나 초기화되어 데이터를 유실하고 환경 설정을 반복해야 했습니다. 아마도 특정 VSCode 확장 프로그램의 메모리 누수가 원인으로 추정됩니다.
  • 프로젝트 통합의 어려움: 각자 개발한 기능들을 하나로 합치는 과정이 쉽지 않았습니다. 특히 제가 맡았던 대화 메모리 기능이 기존 아키텍처에 큰 변화를 요구하여 통합에 많은 노력이 필요했습니다.

팀 기여 및 배운 점 (Contributions & Lessons Learned)

  • 베이스라인 코드 공유: 프로젝트 초반, 팀원들이 구현 방향을 잡기 어려워할 때 빠르게 프로토타입을 만들어 공유했습니다. AI에게 템플릿을 요청하고 단계별로 조립하며 단시간에 만든 베이스라인 코드가 프로젝트의 좋은 출발점이 되었습니다.
  • 상세한 기획의 중요성: 기능 통합 과정에서 상세한 사전 기획이 얼마나 중요한지 절실히 깨달았습니다. 개발 방향에 대한 모두의 합의가 명확하지 않으면 통합 단계에서 큰 비용이 발생합니다. 이제는 AI를 통해 상세한 개발 계획을 쉽게 얻을 수 있으니, 기획 단계를 소홀히 할 이유가 없습니다.
  • 문서화의 가치: '지금 내가 짠 코드는, 미래의 나도 이해하지 못할 수 있다'는 생각으로 README 문서를 정말 열심히 작성했습니다. 시간이 지날수록 프로젝트 자체보다 잘 정리된 문서가 더 중요할 수 있다는 것을 느낍니다.

향후 개선 방향 (Future Improvements)

  • 다양한 데이터 소스 지원: PDF 외에도 Markdown, Text, 오디오, 이미지, 비디오 등 다양한 형태의 입력을 지원하도록 확장할 계획입니다.
  • 프롬프트 엔지니어링 고도화: 블로그의 목적과 톤앤매너에 맞는 글을 생성할 수 있도록 효과적인 프롬프트 템플릿 라이브러리를 구축하고 싶습니다.
  • UI/UX 개선: 프로토타이핑에 사용했던 Streamlit을 넘어, Chainlit이나 LibreChat 같은 프론트엔드 전용 라이브러리로 교체할 예정입니다. 특히 LibreChat의 완성도 높은 UI는 꼭 한번 구현해보고 싶습니다.

6. 맺음말: 대회를 넘어, 계속될 프로젝트

짧은 시간 동안 해보고 싶었던 모든 것을 다 구현하지는 못해 아쉬움이 남지만, LLM과 지능형 에이전트라는 흥미로운 주제에 깊이 몰입해볼 수 있어 정말 만족스러운 경험이었습니다. 이번 LangChain 프로젝트는 저에게 단순한 대회 참여로 끝나지 않을 것입니다. 앞으로 꾸준히 발전시켜 저만의 전문 분야로 만들어나갈 소중한 시작점이 될 것 같습니다.

 

이번 프로젝트는 단순히 모델을 학습시키는 것을 넘어, 여러 기술 요소를 조합해 하나의 시스템을 설계하는 MLOps에 가까운 경험이었습니다. 그 과정에서 얻는 배움이 정말 컸기에, 혹시 인공지능 분야에서 어떤 공부를 시작해야 할지 막막하다면 저처럼 직접 RAG 파이프라인과 지능형 에이전트를 만들어보는 것을 강력히 추천합니다. 이론을 넘어 실제 작동하는 무언가를 만들며 얻는 성취감과 깊은 이해는 그 무엇과도 바꿀 수 없을 것입니다.

 

 

목차


 

들어가며

 

안녕하세요! 이번 포스트는 RAG(검색 증강 생성) 시스템이나 다른 자동화 도구가 생성한 콘텐츠를 자동으로 게시할 수 있는 블로그를 만드는 과정을 안내합니다. GitHub에서 무료로 제공하는 정적 사이트 호스팅 서비스인 GitHub Pages와, 마크다운이라는 간단한 텍스트 형식으로 웹사이트를 만들 수 있게 도와주는 Jekyll이라는 정적 사이트 생성기를 사용할 것입니다.

이 가이드를 끝까지 따라오시면, 여러분의 자동화 시스템이 GitHub API를 통해 직접 새로운 포스트를 작성하고 발행할 수 있는 멋진 블로그를 갖게 될 것입니다. 그럼, 지금부터 단계별로 차근차근 시작해 보겠습니다.

1단계: GitHub 저장소(Repository) 생성 및 Pages 활성화

가장 먼저 블로그의 기반이 될 GitHub 저장소를 만들어야 합니다. GitHub Pages는 특정 규칙에 따라 만들어진 저장소의 콘텐츠를 웹사이트로 자동 변환해 줍니다.

  1. GitHub에 로그인한 후, 새 저장소(New Repository) 생성 페이지로 이동합니다.
  2. 저장소 이름(Repository name)을 반드시 your-username.github.io 형식으로 지정해야 합니다. (your-username 부분은 실제 GitHub 사용자 이름으로 변경해 주세요.) 이 이름 규칙은 GitHub Pages가 개인 계정 블로그를 인식하는 핵심적인 부분입니다.
  3. 저장소를 Public으로 설정하고, "Add a README file" 옵션을 체크하여 저장소를 생성합니다.

저장소가 성공적으로 생성되었다면, 이제 GitHub Pages 기능을 활성화할 차례입니다.

  1. 방금 만든 저장소의 메인 페이지에서 Settings 탭으로 이동합니다.
  2. 왼쪽 메뉴에서 Pages를 클릭합니다.
  3. Branch 섹션에서 소스를 main 브랜치로 선택하고 Save 버튼을 누릅니다.

잠시 후 페이지 상단에 "Your site is live at https://your-username.github.io"라는 메시지가 나타납니다. 이제 여러분의 블로그가 인터넷에 공개되었습니다! 아직은 아무 내용이 없지만, 곧 채워나갈 것입니다.

2단계: 블로그 기본 설정하기 (_config.yml)

이제 우리 블로그의 기본적인 정보와 디자인 테마를 설정해 보겠습니다. Jekyll은 _config.yml이라는 특별한 파일을 통해 사이트 전체의 설정을 관리합니다.

  1. 저장소 메인 페이지에서 Add file > Create new file 버튼을 클릭합니다.
  2. 파일 이름 입력란에 _config.yml이라고 정확하게 입력합니다.
  3. 파일 내용으로 아래 코드를 복사하여 붙여넣습니다. titledescription은 여러분의 블로그에 맞게 자유롭게 수정하세요.

title: My AI-Generated Blog
description: RAG 애플리케이션으로 생성된 포스트
theme: jekyll-theme-minimal
  • 각 항목은 다음과 같은 의미를 가집니다.
    • title: 브라우저 탭이나 검색 결과에 표시될 블로그의 제목입니다.
    • description: 블로그를 설명하는 짧은 문장으로, 검색 엔진 최적화(SEO)에 도움을 줍니다.
    • theme: 블로그의 전체적인 디자인을 결정하는 Jekyll 테마입니다. 우리는 GitHub에서 공식적으로 지원하는 jekyll-theme-minimal 테마를 사용했습니다. 다른 테마를 원하시면 GitHub Pages 지원 테마 목록에서 찾아 변경할 수 있습니다.
  1. 내용 작성이 완료되면 Commit changes 버튼을 눌러 파일을 저장합니다.

3단계: Jekyll 핵심 디렉토리 구조 만들기

Jekyll은 정해진 폴더 구조를 기반으로 동작합니다. 지금부터 우리 블로그에 꼭 필요한 두 개의 핵심 폴더, _posts_layouts를 만들겠습니다.

_posts: 게시글이 저장되는 공간

이름 그대로, _posts 폴더는 우리 블로그의 모든 게시글(포스트) 파일이 저장될 장소입니다. RAG 애플리케이션은 바로 이 폴더에 마크다운(.md) 형식의 글 파일을 생성하게 될 것입니다.

  1. 저장소 메인 페이지에서 Add file > Create new file을 클릭합니다.

    • _posts/까지만 입력하면 파일 이름이 아닌 폴더 경로로 인식됩니다. Git은 빈 폴더를 추적하지 않기 때문에, .gitkeep이라는 빈 파일을 생성하여 _posts 폴더가 저장소에 항상 존재하도록 보장하는 역할을 합니다. 파일 내용은 비워두어도 괜찮습니다.파일 이름 입력란에 _posts/.gitkeep 이라고 입력합니다.
  2. Commit changes 버튼을 눌러 파일을 저장합니다. 이제 _posts 폴더가 성공적으로 생성되었습니다.

 

 

_layouts: 페이지 디자인 템플릿 보관함

_layouts 폴더에는 각 페이지가 어떻게 보일지를 정의하는 HTML 템플릿 파일들이 들어갑니다. 예를 들어 '블로그 글' 템플릿, '메인 페이지' 템플릿 등을 만들어두고 필요할 때마다 가져와서 내용을 채워 넣는 방식입니다. 이 폴더를 만드는 과정은 다음 4단계에서 자세히 다루겠습니다.

4단계: 블로그의 기본 레이아웃(Layout) 설계하기

이제 _layouts 폴더 안에 실제 디자인 템플릿 파일들을 만들어 보겠습니다. 우리는 모든 페이지의 기본이 될 default.html과 블로그 게시글 전용인 post.html 두 가지를 만들 것입니다.

default.html 생성: 모든 페이지의 기본 틀

default.html은 우리 블로그의 가장 바깥 껍데기 역할을 합니다. 모든 페이지에 공통으로 들어갈 HTML 구조(예: <html>, <body> 태그)를 여기에 정의합니다.

  1. Add file > Create new file을 클릭하고, 파일 경로를 _layouts/default.html 로 지정합니다.
  2. 아래의 HTML 코드를 복사하여 붙여넣고 커밋합니다.

<!DOCTYPE html>
<html>
  <head>
    <title>{{ page.title }}</title>
  </head>
  <body>
    {{ content }}
  </body>
</html>
  • {{ page.title }}{{ content }}는 Jekyll이 사용하는 Liquid 템플릿 언어의 변수입니다. Jekyll이 사이트를 빌드할 때, {{ page.title }}은 해당 페이지의 제목으로, {{ content }}는 각 페이지의 실제 마크다운 내용이 HTML로 변환된 것으로 자동 교체됩니다.

post.html 생성: 블로그 게시글 전용 양식

이제 default.html의 기본 구조를 그대로 사용하면서, 블로그 게시글에 필요한 제목과 작성일 등을 추가로 보여주는 post.html 템플릿을 만들겠습니다.

  1. 다시 Add file > Create new file을 클릭하고, 파일 경로를 _layouts/post.html 로 지정합니다.
  2. 아래 코드를 복사하여 붙여넣고 커밋합니다.

---
layout: default
---
<h1>{{ page.title }}</h1>
<p>{{ page.date | date_to_string }}</p>
<hr>
{{ content }}

맨 위의 --- layout: default --- 구문은 이 post.html 템플릿이 default.html 레이아웃을 기반으로 한다는 것을 Jekyll에게 알려주는 중요한 설정입니다. 따라서 이 템플릿을 사용하는 페이지는 default.html<body> 태그 안에 <h1>(제목), <p>(날짜), <hr>, 그리고 실제 글 내용({{ content }})이 순서대로 표시됩니다.

5단계: 방문자를 맞이할 첫 페이지 만들기 (index.md)

이제 블로그의 대문 역할을 할 홈페이지를 만들 차례입니다. 이 페이지는 _posts 폴더에 있는 모든 게시글의 목록을 자동으로 보여주는 기능을 갖게 됩니다.

  1. 저장소 메인 페이지에서 Add file > Create new file을 클릭합니다.
  2. 파일 이름을 index.md로 지정합니다.
  3. 아래의 코드를 복사하여 붙여넣고 커밋합니다.

---
layout: default
---
# 내 블로그에 오신 것을 환영합니다

## 포스트
<ul>
  {% for post in site.posts %}
    <li>
      <a href="{{ post.url }}">{{ post.title }}</a>
    </li>
  {% endfor %}
</ul>
  • 여기서는 Jekyll의 Liquid 템플릿 언어로 된 for 반복문이 사용되었습니다. {% for post in site.posts %}는 Jekyll에게 _posts 폴더 안에 있는 모든 글(site.posts)을 하나씩 순회하라는 명령어입니다. 각 글에 대해 제목({{ post.title }})과 고유 주소({{ post.url }})를 가져와 <a> 태그로 감싸진 목록(<li>) 항목을 생성합니다. 덕분에 우리는 새 글을 쓸 때마다 홈페이지를 수정할 필요 없이, _posts 폴더에 파일을 추가하기만 하면 자동으로 목록이 업데이트됩니다.

6단계: RAG 연동을 위한 GitHub PAT(Fine-grained Token) 생성하기

마지막으로, 우리 RAG 애플리케이션이 GitHub 저장소에 접근하여 글을 쓸 수 있도록 허용하는 인증 키(PAT)를 생성해야 합니다. 보안을 위해 최소한의 권한만 가진 토큰을 만드는 것이 중요합니다.

  1. GitHub 우측 상단의 프로필 아이콘을 클릭한 후, Settings로 이동합니다.
  2. 왼쪽 메뉴 맨 아래에 있는 Developer settings를 클릭합니다.
  3. Personal access tokens > Fine-grained tokens를 선택하고 Generate new token 버튼을 클릭합니다.
  4. Token name에 "RAG Application Writer"와 같이 용도를 알아보기 쉬운 이름을 입력하고 Expiration에서 토큰의 유효 기간을 설정합니다. (보안을 위해 주기적으로 갱신하는 것이 좋습니다.)

  5. Repository access 섹션에서 "Only select repositories"를 선택한 후, 드롭다운 메뉴에서 방금 만든 your-username.github.io 저장소를 선택합니다.
  6. Permissions 섹션으로 스크롤하여 Repository permissions를 찾은 뒤, Contents 항목의 접근 수준(Access)을 Read and write로 변경합니다. RAG 애플리케이션이 글을 읽고 쓸 수 있게 하는 핵심 권한입니다.

  7. 페이지 하단의 Generate token 버튼을 클릭합니다.
  8. 중요: 생성된 토큰은 화면에 단 한 번만 표시됩니다. 반드시 안전한 곳(예: 비밀번호 관리자)에 즉시 복사해 두세요. 이 페이지를 벗어나면 다시는 토큰 값을 확인할 수 없습니다.

이제 이 토큰을 사용하면 RAG 애플리케이션이 우리를 대신하여 블로그에 자동으로 글을 게시할 수 있게 됩니다!

마무리하며

모든 단계를 완료했습니다! 지금까지 우리는 RAG 애플리케이션의 콘텐츠를 담을 GitHub Pages 블로그를 성공적으로 구축했으며, 애플리케이션이 저장소에 안전하게 접근하는 데 필요한 Personal Access Token까지 생성했습니다. 방금 커밋한 파일들이 GitHub 서버에 반영되고 사이트가 다시 빌드되기까지는 몇 분 정도 소요될 수 있습니다.

잠시 후 https://your-username.github.io 주소로 접속하여 "내 블로그에 오신 것을 환영합니다"라는 제목이 보이는 홈페이지를 확인해 보세요. 이제 이 블로그는 RAG 애플리케이션으로부터 새로운 포스트를 받을 모든 준비를 마쳤습니다. 수고하셨습니다!

 

 


유용한 참고 링크

1. 시작하기 (Getting Started)

2. 콘텐츠 작성 및 설정 (Content & Configuration)

3. 고급 설정 및 보안 (Advanced & Security)

 

Introduction: 대회 개요 및 프로젝트 목표

이번 포스트는 저희 팀이 Upstage AI Lab에서 주최한 Dialogue Summarization 대회 #4에 참가하며 얻은 경험과 결과에 대한 회고입니다. 이 대회는 일상적인 대화 내용을 요약하는 모델을 만드는 것을 목표로 했으며, 저희는 이 과정에서 모델링, 데이터 전처리, 그리고 협업에 대해 많은 것을 배울 수 있었습니다.

 

저희가 다룬 데이터셋은 일상 대화(school, work, healthcare, travel 등)를 담은 .csv 형식의 파일이었습니다. 각 대화는 2~7명의 화자(#Person1#과 같은 라벨로 구분)로 구성되었고, 대화 내용과 함께 정답 요약문이 제공되었습니다. 데이터에는 오타, 구두점 오류, 줄바꿈 문자(\\n), HTML 태그</p> 같은 다양한 노이즈가 포함되어 있어 전처리 과정이 중요했습니다.

 

대회의 베이스라인으로는 한국어에 특화된 BART 기반 모델인 KoBART가 제공되었습니다. 저희는 이 베이스라인을 시작점으로 삼아 성능을 개선해 나가는 것을 주요 목표로 설정했습니다. 전체 프로젝트 기간은 2025년 7월 25일부터 8월 6일까지였습니다.

 

(Git Repository: https://github.com/AIBootcamp13/upstageailab-nlp-summarization-nlp_2/tree/wb2x)

 


Results: 핵심 결과 및 평가

저희 팀은 대회 목표를 달성하기 위해 KoBART 모델을 기반으로 다양한 실험을 진행했습니다. 데이터 전처리 단계에서는 대화에 포함된 노이즈를 제거하고, 개인 정보 마스킹된 부분(#PersonN#)을 모델이 이해하기 쉬운 특수 토큰으로 처리하는 방식을 시도했습니다.

 

이번 대회에서 저희 팀과 개인 최고 점수는 다음과 같습니다.

 

팀 최고 점수(Final): 45.7824점

 

개인 최고 점수(Final): 40.6768점

모델의 성능은 ROUGE 점수를 통해 평가되었습니다. 특히, 각 대화에 대해 세 가지 다른 정답 요약문을 기준으로 평가가 진행되었기 때문에, 저희는 다양한 요약 방식을 포괄하는 모델을 만드는 데 집중했습니다. 이 점수들은 저희가 공개 테스트 데이터에서 얻은 최종 결과입니다.


Retrospective (회고)

이번 프로젝트는 지금까지 진행한 프로젝트 중에서 저에게 가장 힘들었던 프로젝트였습니다. 제가 선택한 접근 방식 때문에 예상치 못한 어려움이 있었고, 그 과정에서 많은 것을 배울 수 있었습니다.

처음 베이스라인 코드를 실행했을 때, 오래된 라이브러리 때문에 순조롭게 실행되지 않는 문제가 있었습니다. 라이브러리 의존성 문제를 해결하려 했지만, 버전 교체 후에도 추가적인 디버깅이 필요했습니다. 결국, 이후 연구와 커스터마이징이 힘들 것으로 판단하여 baseline.ipynb를 리팩토링하기로 마음먹었습니다.

 

하지만 대회가 끝나고 나서 제가 리팩토링을 선택한 것에 다음과 같은 단점이 있었다는 것을 알게 되었습니다.

  1. 베이스라인 성능은 더 이상 개선하기 힘들 정도로 최적화된 상태였습니다.
  2. NLP 일상 대화 프로젝트는 작동 원리가 매우 복잡하여 초보자가 문제 원인을 추적하기 힘듭니다.
  3. 각 단계별 아웃풋이 올바른지 판단하기 어려웠고, 복합적인 원인을 추적하는 것이 더 복잡했습니다.

이번 프로젝트를 통해 개발 인프라와 MLOps의 중요성을 절감했습니다. 특히 Hydra라는 설정 관리 프레임워크의 작동 원리를 잘 이해하지 못했던 것이 큰 문제였습니다. 체계적으로 연구 파라미터를 관리하기 위해 여러 설정 파일을 분리했지만, 일부 설정이 예상대로 적용되지 않았습니다. 어떤 부분이 적용되고 안 되는지 명확히 파악할 수 없어 모든 실험이 의미 없게 되거나, 잘못된 방향으로 유도되는 최악의 상황을 초래했습니다. Monolithic 주피터 노트북 코드는 상호 의존성이 너무 복잡해 디버깅에 큰 집중력이 요구되는데, 이러한 경험은 저로 하여금 주피터 노트북으로 작업하는 것을 꺼리게 만들었습니다. 결과적으로, AI 개발자로서 계속 성장하기 위해서는 단순히 모델을 개발하는 것을 넘어, 연구 환경을 체계적으로 구축하는 프레임워크 사용법을 더 잘 이해할 필요가 있다고 생각합니다.

 

'반드시 생각한 방식대로 작동해야 한다'는 고집 때문에 디버깅에 너무 많은 시간을 쏟았습니다. 이로 인해 창의적인 사고와 다양한 분석 방법을 시도할 기회를 놓치게 된 것이 가장 아쉬운 점입니다.

 

Keep (좋았던 점)

  • 프로젝트 소유권 확보: 베이스라인을 개선하는 대신, 프로젝트 전체를 직접 리팩토링하여 저만의 프로젝트를 만들 수 있었습니다. 덕분에 작업물의 소유권이 명확해졌고, 앞으로도 재사용 및 커스터마이징이 가능한 프로젝트 구조를 구축했습니다.
  • 성능 개선 경험: 리팩토링 후 직접 만든 프로젝트가 기존 베이스라인 모델의 성능(47점)을 돌파하는 경험을 했습니다. 비록 어떤 요인이 성능 개선에 기여했는지 명확히 파악하지 못했지만, 전처리/후처리 오작동을 직접 겪으면서 성능 저하의 원인을 깊이 있게 이해할 수 있었습니다.
  • 일상 대화 프로젝트 경험: NLP 프로젝트를 처음부터 끝까지 직접 구현하며 일상 대화 데이터의 특성을 몸소 익힐 수 있었습니다.

Problem (어려웠거나 아쉬웠던 점)

Hydra 설정 관리 디버깅 문제: 이번 대회의 가장 큰 걸림돌은 Hydra 설정 관리 프레임워크였습니다. 체계적인 연구를 위해 config.yaml, dialogue_dataset.yaml 등 단계별로 설정을 분리했지만, 일부 설정이 예상대로 적용되지 않았습니다. 어떤 부분이 적용되고 안 되는지 명확히 파악할 수 없어 모든 실험이 의미 없게 되거나, 잘못된 방향으로 유도되는 최악의 상황을 초래했습니다.

  • 고집스러운 접근: '반드시 생각한 방식대로 작동해야 한다'는 고집 때문에 디버깅에 너무 많은 시간을 쏟았습니다. 이로 인해 창의적인 사고를 통한 다양한 분석 방법이나 솔루션을 시도할 기회를 놓치게 된 것이 가장 아쉬운 점입니다.
  • 복잡한 원인 분석: NLP 일상 대화 프로젝트는 작동 원리가 매우 복잡하여, 문제의 원인을 추적하기가 어려웠습니다. 각 단계별 아웃풋이 올바른지 판단하기 힘들었고, 복합적인 원인을 분석하는 데 큰 어려움을 겪었습니다.

 

Try (다음에 시도해볼 점)

  • 데이터 분석 및 생성: 워드 클라우드를 만들거나, 고급 EDA(탐색적 데이터 분석) 기법을 적용하여 데이터를 더 깊이 있게 분석하고 이해하고 싶습니다. 또한, 데이터 생성 기술을 활용해 데이터의 양을 늘려보고 싶습니다.
  • 다양한 모델과 방법론 적용: 다른 모델을 사용해보고, 토픽 기반 학습을 시도하며, 논문에서 제시된 다양한 방법론들을 적용해보고 싶습니다.
  • LLM 활용: LLM(대규모 언어 모델)을 사용해 주제 중심, 사건 중심 등 다양한 관점의 요약문을 생성하는 실험을 해보고 싶습니다.

Conclusion: 프로젝트를 마치며

대화 요약 프로젝트는 단순히 모델을 구현하는 것을 넘어, 비정형 텍스트 데이터를 깊이 있게 이해하고 효과적으로 전처리하는 과정의 중요성을 깨닫게 해주었습니다. 이번 경험을 통해 얻은 지식은 앞으로 진행할 NLP 프로젝트에 큰 도움이 될 것입니다. 긴 글 읽어주셔서 감사합니다.


딥러닝 프로젝트를 시작할 때 GPU 가속을 위해 PyTorch 라이브러리를 설치해보신 적이 있나요? 하나의 프로젝트만 진행할 때는 문제가 없지만, 여러 개의 가상 환경(venv)을 만들 때마다 수십 GB에 달하는 PyTorch 패키지를 중복해서 설치하게 되면 디스크 공간 낭비가 심해집니다.

 

저도 패키지 관리 효율성을 높이는 방법을 고민하다가, 기존에 설치된 PyTorch 라이브러리를 다른 가상 환경에서 재사용하는 방법을 알게 되었습니다. 이번 포스트에서는 이 방법을 여러분과 공유하려 합니다. 물론 이 방법이 완벽한 해결책은 아니며, 장단점이 존재한다는 점을 염두에 두고 봐주시면 좋겠습니다.


문제 제기: PyTorch 설치의 저장 공간 낭비

제가 사용하는 원격 리눅스 GPU 서버는 RTX 3090 그래픽카드를 사용하고 있습니다. Nvidia CUDA와 cuDNN 라이브러리는 시스템 전역(system-wide)에 설치되어 있어서 가상 환경을 만들 때마다 새로 설치할 필요가 없습니다. 하지만 프로젝트별로 다른 버전의 PyTorch가 필요할 수 있고, 저의 경우 PyTorch 2.6.0 버전을 주로 사용합니다.

문제는 PyTorch, Torchaudio, Torchvision 같은 필수 패키지들이 각각 상당한 용량을 차지한다는 점입니다.

  • torch: 1.5 GB
  • torchaudio: 11 MB
  • torchvision: 13 MB

이 패키지들을 합치면 총 1.5 GB 정도의 공간을 차지합니다. 만약 10개의 프로젝트를 진행하면서 10개의 가상 환경을 만든다면, PyTorch만으로도 약 15 GB의 디스크 공간이 낭비되는 셈입니다.


해결책: .pth 파일을 이용한 라이브러리 상속

저장 공간을 효율적으로 활용하기 위해, 이미 설치된 PyTorch 라이브러리를 새로운 가상 환경에서 "상속"하여 사용하는 방법을 소개합니다. 이 방법은 Python의 .pth 파일을 활용하여 특정 가상 환경이 다른 가상 환경의 패키지 경로를 참조하도록 만드는 것입니다.

site-packages 디렉터리와 .pth 파일: Python은 모듈을 불러올 때 sys.path에 등록된 경로들을 탐색합니다. 가상 환경의 site-packages 디렉터리 내부에 있는 .pth 파일은 이 sys.path에 추가 경로를 등록해주는 역할을 합니다. 우리는 이 특성을 이용해 기존 PyTorch 라이브러리가 있는 경로를 새로운 가상 환경에 추가할 것입니다.

1. 소스(Source) 가상 환경 경로 확인

먼저, PyTorch가 이미 설치되어 있는 기존 가상 환경의 site-packages 경로를 확인합니다.

# Source 가상 환경 경로 확인
/home/wb2x/workspace/text-recognition/venv/bin/python -c "import site; print(site.getsitepackages()[0])"

# 결과 예시
/home/wb2x/workspace/text-recognition/venv/lib/python3.10/site-packages


2. 타겟(Target) 가상 환경 경로 확인

다음으로, PyTorch를 재사용하고 싶은 새로운 가상 환경의 site-packages 경로를 확인합니다.

# Target 가상 환경 경로 확인
/home/wb2x/workspace/nlp-advanced/.venv/bin/python -c "import site; print(site.getsitepackages()[0])"

# 결과 예시
/home/wb2x/workspace/nlp-advanced/.venv/lib/python3.10/site-packages


3. .pth 파일 생성 및 적용

이제 소스 가상 환경의 site-packages 경로를 타겟 가상 환경의 site-packages 디렉터리에 .pth 파일로 저장합니다.

# Source 경로를 Target에 .pth 파일로 추가
echo "/home/wb2x/workspace/text-recognition/venv/lib/python3.10/site-packages" > /home/wb2x/workspace/nlp-advanced/.venv/lib/python3.10/site-packages/reuse_torch.pth


4. PyTorch 재사용 테스트

마지막으로, 타겟 가상 환경을 활성화하고 PyTorch가 정상적으로 불러와지는지 확인합니다.

# 타겟 가상 환경 활성화
source /home/wb2x/workspace/nlp-advanced/.venv/bin/activate

# PyTorch 버전 확인
python -c "import torch; print(torch.__version__)"


결론: 장점과 단점, 그리고 주의사항

이 방법은 디스크 공간을 절약하는 데 매우 효과적이지만, 몇 가지 단점과 주의사항이 있습니다.

장점:

  • 디스크 공간 절약: PyTorch 같은 대용량 라이브러리를 여러 가상 환경에서 중복 설치할 필요가 없습니다.

단점 & 주의사항:

  • 버전 관리의 어려움: 이 방법의 가장 큰 단점은 버전 충돌 가능성입니다. 만약 소스 가상 환경의 PyTorch 버전(예: 2.6.0)이 타겟 프로젝트의 다른 종속성(dependencies)과 호환되지 않으면 문제가 발생할 수 있습니다. 각 프로젝트가 엄격하게 특정 버전을 요구하는 경우, 이 방법을 사용하지 않는 것이 좋습니다.
  • 환경의 독립성 저해: 가상 환경의 핵심 목적은 프로젝트별로 독립적인 환경을 구축하는 것입니다. 이 방법은 가상 환경 간의 종속성을 만드므로, 환경의 독립성을 일부 해칠 수 있습니다.

이 방법을 사용할 때는 여러분의 프로젝트 환경과 요구사항을 충분히 고려해야 합니다. 무조건적인 사용보다는, 디스크 공간 절약이 꼭 필요한 경우에만 신중하게 적용하는 것을 추천합니다.

'Pytorch' 카테고리의 다른 글

[PyTorch 기초] PyTorch 환경 설정 가이드  (2) 2025.06.19

 

 

https://towardsdatascience.com/foundations-of-nlp-explained-visually-beam-search-how-it-works-1586b9849a24/

 

AI로 일상 대화를 요약하는 등 자연어 처리(NLP) 프로젝트를 진행하다 보면, 모델을 파인튜닝하는 과정에서 num_beams라는 파라미터를 마주치게 됩니다. 이 값을 조절하면 결과물의 품질이 달라진다고 하는데, 과연 어떤 원리일까요? 이 글에서는 NLP가 아직 낯선 분들도 쉽게 이해하실 수 있도록, 텍스트 생성의 핵심 기술인 빔 서치(Beam Search)가 무엇이며, num_beams=4 설정이 내부적으로 어떻게 동작하는지 함께 분석해 보겠습니다.


1. 가장 간단한 방식: 그리디 서치 (Greedy Search)

빔 서치를 이해하려면 먼저 가장 기본적이고 직관적인 텍스트 생성 방식인 그리디 서치(Greedy Search)를 알아야 합니다.

그리디 서치는 각 단계에서 가장 확률이 높은 단어 하나만을 선택해 문장을 이어 나가는 방식입니다. 눈앞의 가장 좋아 보이는 길만 따라가는 것과 같죠. 이 방식은 매우 빠르고 간단하지만, 처음에 잘못된 단어를 선택하면 나중에 어색하거나 의미가 맞지 않는 문장으로 이어질 위험이 큽니다.

예를 들어, 장기적으로는 "The cat"으로 시작하는 것이 최적의 문장일 수 있는데도, 첫 단어로 "A"가 아주 약간 더 높은 확률을 가졌다는 이유만으로 "A"를 선택하면 뒤따르는 문장 전체의 품질이 저하될 수 있습니다.

 

그리디 서치는 빔 서치에서 num_beams=1로 설정한 것과 기술적으로 동일합니다. 즉, 각 단계에서 고려할 후보 문장(beam)의 개수가 단 하나인 셈입니다.


2. 더 나은 문장을 위한 탐색: 빔 서치 (Beam Search)

빔 서치는 그리디 서치의 단점을 보완하는 기법입니다. 한 번에 하나의 최선책만 보는 대신, 지정된 개수(num_beams)만큼의 가능성 있는 후보 문장들을 동시에 고려하며 탐색을 진행합니다.

 

num_beams=4는 모델이 텍스트를 생성하는 매 단계마다 가장 확률이 높은 후보 시퀀스 4개를 계속 유지하라는 의미입니다. 이 방식은 초반의 선택이 다소 불리해 보여도, 장기적으로 더 나은 문장이 될 가능성을 열어두어 최종적으로 더 자연스럽고 일관된 문장을 만들어 냅니다.

빔 서치(num_beams=4)의 단계별 동작 과정

그렇다면 num_beams=4는 실제로 어떻게 문장을 만들어낼까요? 고양이에 대한 대화를 요약하는 상황을 가정하고, 모델의 내부 동작을 단계별로 살펴보겠습니다.

 

Tip: num_beams와 같은 파라미터는 yaml 파일으로 관리하면 쉽게 편집 할 수 있습니다. 

YAML 기반 설정

 

1단계: 첫 단어 생성

모델은 문장을 시작할 때, 가장 확률 높은 단어 하나만 선택하는 대신 가장 유력한 후보 4개를 모두 유지합니다. 이 4개의 후보가 바로 우리의 "빔(beam)"이 됩니다.

# 모델이 첫 단어 생성을 시작하고 상위 4개의 후보(빔)를 유지합니다.

# 점수(Score)는 로그 확률값이며, 0에 가까울수록 확률이 높다는 의미입니다.
      +--> "The"   (Score: -0.2)  # 빔 1
      |
 ------+--> "A" (Score: -0.5) # 빔 2  
|  
+--> "He" (Score: -0.8) # 빔 3  
|  
+--> "She" (Score: -0.9) # 빔 4

 

추천 영상

https://www.youtube.com/watch?v=BvQHggTHaLQ&themeRefresh=1

 

2단계: 두 번째 단어 생성 및 가지치기(Pruning)

다음으로, 모델은 4개의 각 빔에 대해 다음 단어를 예측합니다. 이렇게 생성된 모든 조합(4xN개)의 누적 점수를 다시 계산한 후, 전체 중에서 가장 점수가 높은 상위 4개의 시퀀스만 남기고 나머지는 버립니다. 이 과정을 통해 "He", "She"처럼 가능성이 낮아진 빔은 자연스럽게 버려지고, 더 유망한 두 단어 조합이 새로운 빔이 됩니다.

https://python.plainenglish.io/understanding-decoding-strategies-greedy-beam-search-top-k-top-p-27fbefcc3295

# 각 빔에서 파생될 수 있는 모든 경우의 수를 평가하고...

"The" -> "The cat" (Score: -0.4)  
\-> "The dog" (Score: -0.7)  
"A" -> "A cat" (Score: -0.6)  
\-> "A man" (Score: -0.8)  
"He" -> "He saw" (Score: -1.1)  
\-> "He ran" (Score: -1.2)  
"She" -> "She was" (Score: -1.3)  
\-> "She sat" (Score: -1.4)

# ...전체 조합 중 가장 좋은 4개의 시퀀스만 다음 단계를 위해 남깁니다.
      +--> "The cat" (Score: -0.4)  # 새로운 빔 1
      |
 ... --+--> "A cat" (Score: -0.6) # 새로운 빔 2  
|  
+--> "The dog" (Score: -0.7) # 새로운 빔 3  
|  
+--> "A man" (Score: -0.8) # 새로운 빔 4

 

 

예시

설명: 빔 서치를 통해서 생성할 수 있는 결과 입니다(predictions 참고). 얼핏 보면 약간 이해가 되는 말 같이 보일 수도 있지만 사실 일상 대화 요약을 비극하게 실패한 장면 입니다. 길이 조절과 내용 요약 방법은 다른 파라미터에 달려 있습습니다.

WanDB UI

 

3단계: 과정 반복 및 최종 선택

이처럼 빔을 확장하고 점수가 낮은 빔을 잘라내는(pruning) 과정은 문장이 끝을 의미하는 토큰을 만나거나 최대 길이에 도달할 때까지 반복됩니다. 모든 생성이 끝나면, 최종 후보 빔들 중에서 가장 높은 점수를 받은 시퀀스 하나가 최종 결과물로 선택됩니다.

# ...몇 단계를 더 거친 후, 최종 후보 빔들이 다음과 같다고 가정해 봅시다.
      +--> "The cat slept on the mat."       (Final Score: -0.8)
      |
 ... --+--> "A cat was sleeping soundly."  (Final Score: -0.9)  
|  
+--> "The dog played with the toy."  (Final Score: -1.1)  
|  
+--> "A man walked down the street." (Final Score: -1.3)

 

모델은 가장 높은 점수를 받은 "The cat slept on the mat."을 최종 요약문으로 선택합니다.


3. 빔 서치와 다른 생성 전략들

빔 서치는 여러 텍스트 생성(디코딩) 전략 중 하나이며, 크게 결정론적 방식확률적 방식으로 나뉩니다.

  • 결정론적 방식 (Deterministic Methods)
    동일한 입력값에 대해 언제나 똑같은 결과를 출력합니다.
    • 그리디 서치 (Greedy Search): 가장 단순한 결정론적 방식으로, 실질적으로 빔 서치에서 num_beams=1로 설정한 것과 같습니다.
    • 빔 서치 (Beam Search): num_beams를 1보다 큰 값으로 설정하여 여러 후보를 동시에 탐색하는, 더 발전된 결정론적 방식입니다.

적용 예시

YAML 기반 설정

  • 확률적 방식 (Stochastic/Sampling Methods)
    생성 과정에 무작위성을 도입하여 매번 다른 결과를 생성할 수 있으며, 주로 빔 서치의 대안으로 사용됩니다. 이 때문에 많은 프로젝트 설정에서 빔 서치를 활성화하면 샘플링 옵션(do_sample: false)은 비활성화하는 것을 볼 수 있습니다.
    • Top-k 샘플링: 가장 확률이 높은 k개의 단어 중에서 무작위로 다음 단어를 선택합니다.
    • Top-p (Nucleus) 샘플링: 확률의 총합이 p를 넘는 가장 작은 단어 집합 내에서 무작위로 다음 단어를 선택합니다.

적용 예시

YAML 기반 설정

 


4. 결론

빔 서치(Beam Search)는 단순히 매 순간의 최선이 아닌, 종합적으로 더 완성도 높은 결과물을 찾아 나서는 탐색 기법입니다.

그리디 서치의 근시안적인 단점을 보완하기 위해 여러 개의 유력한 후보(빔)를 유지하고, 가능성이 낮은 경로는 과감히 버리면서 계산 효율성과 결과물의 품질 사이에서 균형을 맞춥니다. 이러한 특성 덕분에 빔 서치는 기계 번역, 텍스트 요약 등 일관되고 자연스러운 문장 생성이 중요한 여러 NLP 작업에서 기본적이면서도 가장 강력한 도구 중 하나로 사용되고 있습니다.

 

 

목차


 

1. 들어가며

안녕하세요. '이미지로 문서 분류하기' 시리즈의 마지막 파트입니다. 이번 포스트에서는 대회에서 최종 성능을 개선하기 위해 제가 시도했던 구체적인 방법들과, 10일간의 짧은 여정을 통해 무엇을 배우고 느꼈는지에 대한 솔직한 회고를 담아보려 합니다.

초기 베이스라인 모델은 준수한 성능으로 시작했지만, 곧 데이터 증강의 함정에 빠져 성능이 급격히 떨어지는 등 좌절의 순간을 맞았습니다. 이 글은 그 문제를 해결하기 위한 체계적인 실험, 새로운 모델의 발견, 그리고 그 과정에서 겪었던 현실적인 어려움과 성장에 대한 기록입니다.

 

(참고: 프로젝트 기간 동안의 전체 작업 타임라인은 다음과 같습니다.)

타임라인:

  1. 개인 베이스라인 코드 구현 (1-2일차)
  2. WanDB를 이용한 모델 모니터링 도입 (3-4일차)
  3. 온라인 데이터 증강법 연구 및 디버깅 (4-5일차)
  4. 잘못된 예측 분석을 위한 EDA 2차 진행 (7-8일차)
  5. 오프라인 데이터셋 증강 및 볼륨 증가 (9일차)
  6. 프로젝트 발표 자료 정리 (11일차)

 

 

이 포스트에서 공유하는 모든 실험과 최종 코드는 아래 깃허브 리포지토리에 정리해 두었습니다. 전체 소스 코드를 직접 확인하며 글을 읽으시면 더욱 이해하기 쉬울 겁니다.

▶︎ 프로젝트 깃허브 리포지토리 바로가기


2. 결과부터 공유: 최종 리더보드

본격적인 여정을 이야기하기에 앞서, 10일간의 실험과 우여곡절 끝에 제가 받은 최종 성적표부터 공유하려 합니다. 저의 최종 제출 점수인 0.93은 공개 리더보드에서 24위를 기록했습니다.

이제부터 이 점수를 얻기까지의 길고 험난했던 과정을 자세히 들려드리겠습니다.

 

3. 예상치 못한 성능 저하: 데이터 증강의 역설

저는 초기 모델로 ResNet34와 ResNet50을 사용했습니다. ResNet50 베이스라인 모델의 F1 score는 학습 데이터셋에서 89%로 괜찮았지만, 정작 대회 test 데이터셋에서는 79% 수준에 그쳤습니다.

Baseline Model (ResNet50)

성능을 개선하기 위해 다양한 데이터 증강 방법을 연구했습니다. 하지만 기대와 달리, 온라인 데이터 증강을 적용할수록 성능은 계속해서 떨어졌습니다.

Data Augmentation -> Performance Drop

명확한 성능 저하 앞에서 다른 모델을 실험하는 것은 무의미하다고 판단했고, 왜 이런 현상이 발생하는지 원인을 찾는 데 많은 시간을 쏟았습니다. 특히 데이터 증강 라이브러리(albumentations, augraphy)를 디버깅하는 과정은 순탄치 않았습니다. AI 어시스턴트에게서 받은 예제 코드는 번번이 실패했고, 결국 데이터 증강이 성능에 악영향만 미친다는 잠정 결론에 이르렀습니다.

대회 7일차에 달성한 최악의 성능은 정말 실망스러웠습니다.

Worst Performance with Augmentation


4. 성능 개선을 위한 체계적 접근: 3단계 학습 실험

데이터 증강의 함정에서 벗어나기 위해, 저는 가설을 세우고 체계적으로 접근하기로 했습니다. "무작위 증강이 아닌, 단계적이고 점진적인 증강이 효과가 있을 것이다"라는 아이디어로 3단계 학습을 설계했습니다.

실험 전략:

  1. 이미지 크기 축소: 반복 테스트를 통해 이미지 크기를 ResNet이 사전 학습된 224x224로 줄이는 것이 과적합 방지에 효과적임을 발견했습니다.
  2. 3단계 점진적 증강: 약한 증강부터 심한 증강까지 단계를 나누어 학습을 진행했습니다.
  3. 데이터 불균형 해소: 샘플 수가 적었던 클래스 3, 7, 14의 데이터를 수동으로 2배 늘렸습니다.
  4. 데이터 레이블 수정: 분석 과정에서 발견한 잘못된 레이블을 수정했습니다.

Phase 1: 약한 증강 (Test Score: 0.8415)

원본 데이터셋과 약간의 회전(±20도)이 적용된 데이터셋으로 학습했습니다. Confusion Matrix를 분석한 결과, '입/퇴원 증명서'와 '외래진료 증명서' 클래스를 여전히 혼동하는 것을 발견했습니다. 원인 파악을 위해 학습 데이터를 직접 검토했고, 잘못 레이블된 이미지 5개를 찾아 수정했습니다.

Phase 2: 중간 증강 (Test Score: 0.8627)

잘못된 레이블을 수정한 데이터로 다시 학습하자, 이전 단계에서 혼동하던 클래스들의 분류 정확도가 개선되는 효과를 볼 수 있었습니다.

Phase 3: 심한 증강 (Test Score: 0.8779)

가장 강한 증강(±90도 회전)을 적용해 마지막 학습을 진행했고, 점진적으로 성능이 향상되는 결과를 얻었습니다.


5. 새로운 모델의 발견: ConvNeXt 실험

ResNet50으로 점진적 성능 향상을 확인한 후, 더 나은 아키텍처를 시도해보기로 했습니다.

ConvNeXt (Test Score: 0.8792)

기본 데이터셋으로 ConvNeXt를 학습했을 때, ResNet50의 최종 단계와 비슷한 성능을 단번에 달성했습니다. 학습 과정이 매우 안정적이었지만, 학습 시간이 길다는 단점이 있었습니다.

ConvNeXtV2 (Test Score: 0.93 - Personal Best)

모델 업그레이드, 데이터 볼륨 10배 증가, 그리고 모든 강도의 증강을 동시에 적용한 결과, 93%라는 큰 폭의 성능 개선을 이룰 수 있었습니다. 학습 효율이 너무 좋아 8번째 epoch에서 수동으로 학습을 중지해야 할 정도였습니다.

안타깝게도 이 모델로 더 많은 실험을 진행하지는 못했습니다. 대회 마지막 이틀을 남겨둔 9일차에 예상치 못하게 장염에 걸려 정상적으로 활동할 수 없었기 때문입니다.


6. 프로젝트 회고

이번 프로젝트는 점수 이상의 많은 것을 남겼습니다.

6.1. 좋았던 점 (What Went Well)

  • 홀로서기: 처음으로 프로젝트 세팅 전반을 혼자 진행하며 많이 배울 수 있었습니다.
  • 선입견 파괴: OCR 없이 순수 이미지 정보만으로도 문서를 효과적으로 분류할 수 있다는 사실을 직접 체험했습니다.
  • 데이터 전처리 경험: 다양한 증강 라이브러리를 직접 사용해보며 각 도구의 장단점을 파악할 수 있었습니다.
  • 분석 도구 개발: 예측 실패 원인을 분석하기 위해 요구사항에 맞는 분석 도구를 직접 개발해 본 경험이 소중했습니다.
  • WanDB 활용: 체계적이고 시각적인 실험 모니터링이 모델링에 얼마나 강력한 도구인지 깨달았습니다.

6.2. 아쉬웠던 점 (What Could Be Better)

  • 건강 문제: 가장 중요한 시기에 아파서 활동하지 못한 점이 가장 아쉽습니다.
  • 팀워크: 각자 개별적으로 프로젝트를 세팅하는 경험도 중요했지만, 초기에 베이스라인 코드, 모니터링 도구 등을 공유하며 시작했다면 연구에 더 집중할 수 있었을 것입니다.
  • 협업 방식: 경험 부족으로 인해 더 효율적으로 협업하지 못한 점이 아쉽습니다.

6.3. 어려웠던 점 (Challenges Faced)

  • 데이터 증강: 초기에 데이터 증강 파이프라인을 구현하는 데 가장 큰 어려움을 겪었습니다. 라이브러리 사용법 미숙, 버전 차이로 인한 혼란, 문서 이미지에 부적합한 증강법을 잘 몰랐던 점 등이 원인이었습니다. 단기간에 개발해야 한다는 압박감에 공식 문서를 차분히 보지 못한 것이 후회됩니다.
  • 서버 초기화: 대회 9일차, 데이터셋을 증강하는 스크립트를 실행하던 중 자원 소모 문제로 GPU 서버 인스턴스가 예고 없이 종료되었습니다. 코드는 Git으로 복구했지만, 개인적으로 정리하던 참조 문서들이 모두 사라져 다시 만들어야 했습니다.

6.4. 팀 기여 및 배운 점 (Team Contributions & Lessons Learned)

  • 인사이트 공유: ResNet50에서 이미지 크기를 224x224로 사용했을 때 과적합이 줄어든 이유, Train/Test 데이터 간의 품질 차이("도메인 갭") 문제 등을 분석하고 팀에 공유했습니다.
  • 분석 결과 공유: EDA 및 에러 분석 결과를 시각 자료와 함께 공유하며, 어떤 데이터 왜곡이 모델 성능에 큰 영향을 미치는지, 어떤 증강이 필요한지에 대한 구체적인 근거를 제시했습니다.
  • 레이블 검증: 잘못 레이블된 학습 데이터를 직접 찾아내 수정했고, 이는 Confusion Matrix에서 뚜렷한 성능 개선으로 이어졌습니다.

6.5. 향후 개선 방향 (Future Improvements)

  • 설정 관리: Hydra와 같은 설정 관리 프레임워크를 프로젝트 초기에 도입하고, 더 깊이 학습할 필요성을 느꼈습니다.
  • 실험 공유 방식: WanDB만으로는 연구 목적을 파악하기 어려워, 실험 내용을 글로 요약해서 공유하는 방식을 보완해야 합니다.
  • 선택과 집중: 다음에는 모든 것을 다 하려 하기보다, 한두 가지 주제에 집중하여 더 깊이 있는 결과를 내고 싶습니다.


7. 맺음말

대회를 마치며 최종 순위표의 숫자가 전부는 아니라는 것을 다시 한번 느낍니다. 데이터 증강이 오히려 성능을 떨어뜨렸던 역설적인 상황부터, 가장 중요했던 마지막 순간에 찾아온 건강 문제와 서버 초기화까지, 이번 프로젝트는 예상치 못한 문제들과 싸워나가는 현실적인 개발 과정 그 자체였습니다. 가장 큰 수확은 '화려한 모델'이 아니라 '체계적인 접근'이 문제 해결의 핵심이라는 깨달음입니다. 가설을 세우고(3단계 학습), 데이터를 깊게 분석하고(레이블 수정), 꾸준히 기록하는 과정의 중요성을 체감할 수 있었습니다.

비록 아쉬움은 남지만, 고통스러웠던 만큼 더 단단해질 수 있었던 성장의 기록을 이것으로 마칩니다. 감사합니다.

목차


 

1. 들어가며 (Introduction)

안녕하세요! '이미지로 문서 분류하기' 시리즈의 두 번째 파트입니다. Part 1: 시리즈 소개에서는 이 대회의 독특한 목표와 접근 방식에 대해 이야기 나눴습니다.

이제 본격적으로 모델을 만들기 전에, 가장 중요하지만 종종 간과되는 단계를 먼저 밟아보려 합니다. 바로 흩어져 있는 아이디어와 코드를 담아둘 튼튼하고 체계적인 '집'을 짓는 일, 즉 프로젝트 구조화(Scaffolding)입니다. 이번 포스트에서는 왜 제가 단순한 Jupyter Notebook으로 시작하지 않았는지, 그리고 어떻게 프로젝트의 뼈대를 잡고 초기 데이터를 탐색했는지 그 과정을 공유하겠습니다.


2. 왜 Jupyter Notebook만으로는 부족할까?

많은 분이 Jupyter Notebook에서 ML 프로젝트를 시작합니다. 저 또한 빠른 테스트와 시각적인 확인이 가능해 애용합니다. 하지만 프로젝트가 조금만 복잡해져도 Notebook 방식은 한계를 드러내기 시작합니다.

  • 관리의 어려움: 코드가 위에서 아래로 길어지면 실행 순서가 꼬이기 쉽고, 전체 구조를 파악하기 어렵습니다.
  • 재사용성의 한계: 특정 함수나 클래스를 다른 프로젝트에서 가져다 쓰기 번거롭습니다.
  • 협업 및 버전 관리: 여러 사람이 하나의 Notebook 파일을 동시에 수정하거나 Git으로 변경 사항을 추적하는 것은 거의 재앙에 가깝습니다.

실무에서는 이런 문제를 피하기 위해 처음부터 기능별로 코드를 분리하는 모듈화(.py 파일 기반) 방식을 선호합니다. 예를 들어, 데이터 로딩 함수는 data_loader.py에, 자주 쓰는 유틸리티 함수는 utils.py에 모아두는 식이죠. 이렇게 하면 코드를 재사용하기 쉽고, 테스트가 용이하며, 장기적으로 개발 효율성이 크게 향상됩니다.

이러한 이유로, 이번 프로젝트에서는 처음부터 확장성을 고려한 폴더 구조 위에서 개발을 시작하겠습니다.


3. 완벽한 프로젝트 구조를 찾아서: 2단계 여정

3.1. 첫 번째 시도: 스크립트를 이용한 자동화

노트북의 혼돈에서 벗어나기 위한 저의 첫걸음은, 재사용 가능한 기본 프로젝트 구조를 만드는 과정을 자동화하는 것이었습니다. 매번 수동으로 폴더와 파일을 생성하고 싶지 않았기에, 이 초기 설정을 처리해 줄 setup_project.py라는 생성 스크립트를 작성했습니다. 이 스크립트는 config, data, models 등 필요한 모든 폴더를 생성하고, 기본적인 boilerplate 코드로 채워주는 역할을 했습니다. 스크립트 전체와 템플릿 코드는 저의 깃허브 리포지토리에서 확인하실 수 있습니다. 이 스크립트 덕분에, 단 한 줄의 명령어로 즉시 개발을 시작할 수 있는 프로젝트를 구성할 수 있었습니다.

# 깃허브에서 리포지토리를 클론한 후
python setup_project.py document-classifier

이 작은 자동화 도구 덕분에 지루한 준비 작업을 건너뛰고 곧바로 흥미로운 핵심 작업에 집중할 수 있습니다.

 

내용:

1. setup_project.py

2. project_templates/ # 폴더

project_templates/ # 폴더 내용

사용 예시

python setup_project.py document_classifier

그 결과, 다음과 같은 초기 구조가 만들어졌습니다:

document_classifer/
├── checkpoints/
├── config/
├── data/
├── inference/
├── logs/
├── models/
├── notebooks/
├── outputs/
├── trainer/
├── utils/
├── predict.py         # 예측 스크립트가 루트에 위치
├── README.md
├── requirements.txt
└── train.py           # 훈련 스크립트가 루트에 위치

3.2. 두 번째 시도: src 레이아웃으로의 리팩토링

위 구조로도 프로젝트는 돌아갔지만, 점점 지저분하게 느껴지기 시작했습니다. train.pypredict.py 같은 핵심 실행 스크립트들이 루트 디렉토리에 섞여 있었습니다. data/, models/, trainer/에 보조 모듈들을 추가할수록, '실행용 스크립트'와 '라이브러리용 코드' 사이의 경계가 모호해졌습니다. 그 점이 저를 계속 불편하게 만들었습니다. 프로젝트가 커질수록 이 불분명한 분리 구조가 혼란을 야기하고 유지보수를 어렵게 만들 것이라는 사실을 직감했습니다. 이 깨달음이 저의 첫 번째 중요한 우회로였습니다. 저는 모델링에 더 깊이 파고들기 전에, 잠시 멈춰서 src (source) 디렉토리를 도입하는 방향으로 프로젝트 전체를 리팩토링하기로 결심했습니다.

 

리팩토링의 목표는 모든 파이썬 모듈을 src 라는 잘 정의된 단일 폴더로 옮기는 것이었습니다. 이를 통해 애플리케이션의 핵심 로직을 프로젝트 루트 레벨에 위치한 스크립트, 설정 파일, 문서 등과 명확하게 분리할 수 있었습니다.

리팩토링을 마친 후, 마침내 제가 현재 사용하고 있는 더 깔끔한 프로젝트의 새로운 청사진이 완성되었습니다. 제가 원했던 '관심사의 분리'가 명확히 이루어진 구조입니다.

document-classifier/
├── src/                     # 핵심 소스 코드
│   ├── data/                # 데이터 로딩, 전처리, 증강
│   ├── models/              # 모델 아키텍처 정의
│   ├── training/            # 훈련 루프, 트레이너, 콜백
│   ├── utils/               # 보조 유틸리티 함수
│   └── inference/           # 추론 및 예측
│
├── configs/
│   ├── base.yaml
│   └── resnet50.yaml
│
├── data/                    # 데이터
│   ├── raw/                 # └─ 원본 데이터
│   └── processed/           # └─ 전처리 데이터
│
├── notebooks/
├── scripts/
│   ├── train.py
│   ├── predict.py
│   └── evaluate.py
│
├── tests/
├── outputs/
│   ├── models/              # 모델 가중치
│   ├── predictions/
│   └── figures/
│
├── environment.yml          # Conda 환경 설정
├── requirements.txt
├── project_setup.py
├── setup.py
├── README.md
└── .gitignore

 

이 리팩토링은 단순히 파일을 정리하는 것 이상의 의미가 있었습니다. 다음과 같은 실질적인 이점을 가져다주었습니다.

  • 명확성: 모든 핵심 재사용 코드가 src 안에 있다는 것이 즉시 명확해졌습니다.
  • 유지보수성: 의존성을 관리하고 순환 참조(circular import) 오류를 피하기가 더 쉬워졌습니다.
  • 확장성: 이 전문적인 레이아웃은 대규모 파이썬 애플리케이션의 표준이며, 새로운 기능을 추가하거나 다른 개발자가 기여하기에 훨씬 용이한 구조입니다.

이제 훨씬 더 깔끔하고 견고한 토대를 갖추었으니, 저는 드디어 이 프로젝트의 핵심 과제인 데이터 탐색과 강력한 분류 모델 훈련에 집중할 수 있게 되었습니다.


4. 데이터 살펴보기: EDA 결과 요약

데이터 경로 설정의 어려움

EDA를 시작하기 전, 데이터셋 경로 설정에서 많은 어려움을 겪었습니다. 작업 환경과 경로를 연결하는 과정에서 저의 설정이 계속 실패하면서 절대 경로를 사용해야만 했습니다. 결국, 유틸리티 스크립트를 사용해 프로젝트 경로를 설정할 수 있었습니다. 이 문제에 대한 자세한 설명은 추후 별도의 블로그 포스트에서 다루겠습니다.


프로젝트 경로를 못 찾아 헤매다 보니 EDA를 AI의 도움을 받아 진행했습니다. 이미지를 어떻게 분석해야 할지, 어떤 메서드를 써야 할지 검색해서 예제를 찾는 것이 항상 마음대로 되지는 않습니다. 대부분 필요한 코드와 구현 방식을 찾아도, 막상 적용하려면 설명이 부족하거나 추가 검색이 필요해 시간 소모가 컸습니다. 블로그를 봐도 버전이 달라 메서드명이 변경된 경우도 많아 한 번에 제대로 하기가 쉽지 않았습니다. 그래서 그냥 AI를 사용하기로 했습니다. AI를 사용하니 상황에 맞는 템플릿을 받을 수 있어서 참 좋았습니다.

 

AI의 도움을 통한 EDA 수행

데이터 경로 설정 문제를 해결한 후, AI의 도움을 받아 효과적으로 EDA를 수행했습니다. AI는 상황에 맞는 템플릿을 제공해주어 분석에 큰 도움이 되었습니다.

기초 EDA 요약

데이터셋의 구조와 클래스 분포

  • Train 데이터셋: 이미지 1570개
  • Test 데이터셋: 3140개 이상의 이미지
  • 클래스 개수: 17개의 문서 분류
  • 클래스 불균형: 클래스 간 이미지 수 불균형이 존재하여, 가중치 샘플링이나 클래스 가중치 사용이 필요합니다.

데이터 특성과 품질

  • 이미지 크기 및 비율: 다양한 크기와 종횡비로 인해 표준화된 전처리가 필요합니다.
  • 데이터 품질: 일부 누락된 이미지 파일이 감지되었으며, 파일 이름은 일관된 패턴을 따르고 중복은 발견되지 않았습니다.

주요 통찰

  1. 이미지 품질: 훈련 및 테스트 데이터 간 회전 각도와 조명 조건에서 큰 차이가 있습니다.
  2. 조명 및 노이즈: 테스트셋의 과다 노출 이미지가 훈련셋보다 두 배 많았고, 노이즈 종류의 차이도 있었습니다.
  3. 클래스 불균형: 일부 클래스에 대한 샘플 수가 적어 학습에 영향을 미칠 수 있습니다.

단계별 EDA 예시

1. 데이터셋 이미지

Train

Test

위 이미지는 train과 test 데이터 셋의 샘플 이미지입니다. Train 데이터 셋은 이상적인 품질을 가지고 있는 반면 테스트 데이터 셋에서는 다양한 품질 차이를 관찰할 수 있습니다.

 

2. 클래스 분포 그래프

클래스 분포 그래프는 다양한 카테고리 간의 상당한 불균형을 보여줍니다. 1,13,14번 클래스는 다른 클래스들에 비해 이미지 수량이 상당히 작아서 심각하게 과소 대표되어 있습니다. 이 불균형은 모델의 학습 과정에 영향을 줄 수 있으며, 클래스 가중치나 데이터 증강 같은 기법이 필요할 수 있습니다.

 

3. 클래스별 샘플 이미지

클래스 수가 많아서 예시로 첫 3개 클래스만 포함했습니다.

 

4. 이미지 크기 분포 등

이미지 크기 분포 그래프는 데이터셋 전반에 걸친 이미지 크기의 변화를 보여줍니다.

5. 이미지 상세 통계


📏 이미지 크기 통계 (Image Detail Statistics)

구분 (Category) 최소 (Min) 최대 (Max) 평균 (Average)
너비 (Width) 384 753 497.0
높이 (Height) 348 682 538.8
종횡비 (Aspect Ratio) 0.56 2.16 0.97

 

 

📐 상위 10개 가장 흔한 해상도 (Top 10 Most Common Resolutions)

순위 (Rank) 해상도 (Resolution) 개수 (Count) 비율 (Percentage)
1 443 x 591 963 64.2%
2 591 x 443 267 17.8%
3 682 x 384 18 1.2%
4 608 x 430 10 0.7%
5 643 x 407 8 0.5%
6 641 x 408 7 0.5%
7 512 x 512 6 0.4%
8 626 x 418 5 0.3%
9 638 x 410 5 0.3%
10 637 x 411 5 0.3%

6. 클래스 불균형

 

Train 데이터 셋에서 각 클래스는 대부분 100개 이미지가 있었지만 임신/출산 진료비 지급 신청서, 이력서, 소견서 클래스에는 이미지 수량이 많이 부족 하다는 것을 발견 했습니다.


5. 맺음말: 구조 완성, 이제 모델로

이번 Part 2에서는 본격적인 모델링에 앞서 튼튼한 기반을 다지는 과정을 공유했습니다. 처음의 미숙했던 폴더 구조에서부터 src 레이아웃을 도입하며 겪었던 리팩토링 여정과, 그 구조 위에서 데이터를 처음 살펴본 EDA 결과까지를 담았습니다.

좋은 구조를 갖추는 일이 당장은 돌아가는 것처럼 보여도, 장기적으로는 더 빠른 길이라는 것을 다시 한번 느낄 수 있었습니다.

이제 모든 준비는 끝났습니다. 다음 Part 3에서는 이 견고한 토대 위에서 베이스라인 모델의 성능을 개선하고, 프로젝트 전체를 회고하며 시리즈를 마무리하겠습니다. 감사합니다.

+ Recent posts