목차


 

들어가며

 

안녕하세요! 이번 포스트는 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을 클릭합니다.
  2. 파일 이름 입력란에 _posts/.gitkeep 이라고 입력합니다.
    • _posts/까지만 입력하면 파일 이름이 아닌 폴더 경로로 인식됩니다. Git은 빈 폴더를 추적하지 않기 때문에, .gitkeep이라는 빈 파일을 생성하여 _posts 폴더가 저장소에 항상 존재하도록 보장하는 역할을 합니다. 파일 내용은 비워두어도 괜찮습니다.
  3. 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에서는 이 견고한 토대 위에서 베이스라인 모델의 성능을 개선하고, 프로젝트 전체를 회고하며 시리즈를 마무리하겠습니다. 감사합니다.

목차

1. 들어가며: 이미지로 문서를 분류하는 특별한 과제

2. 대회 개요

3. 왜 이미지로 문서를 분류하나요? (Q&A)

4. 앞으로의 여정: 시리즈 로드맵

5. 맺음말

1. 들어가며: 이미지로 문서를 분류하는 특별한 과제

안녕하세요! 제 AI 역량 강화를 위한 세 번째 경진대회 참여 후기를 공유하고자 합니다. 이번 과제는 '문서 분류'라는, 언뜻 보면 익숙한 자연어 처리(NLP) 문제처럼 보였습니다. 하지만 여기에는 한 가지 결정적인 반전이 있었습니다. 바로 텍스트가 아닌 문서의 '이미지' 그 자체를 사용해 분류해야 한다는 점이었습니다.

이 독특한 제약 조건 덕분에, 이번 프로젝트는 단순한 모델링을 넘어 '어떻게 문제를 정의하고 접근할 것인가'에 대한 깊은 고민부터 실무에 가까운 모듈화된 프로젝트 구축까지, 많은 학습 경험을 할 수 있었습니다. 이 블로그 시리즈를 통해 그 전체 여정을 공유하고자 합니다.

2. 대회 개요

이번 대회는 AI Stages에서 주최하는 Computer Vision(CV) 대회로, 주어진 문서의 이미지를 보고 어떤 종류의 문서인지 분류하는 과제입니다.

  • 📅 대회 기간 (Timeline): 2025년 6월 30일 (월) 10:00 ~ 2025년 7월 10일 (목) 19:00
  • 🎯 과제 (The Task): 총 17개 종류의 문서 이미지를 분류하는 다중 클래스 분류(Multi-class Classification) 문제입니다. 1,570장의 학습 이미지를 사용하여 3,140장의 평가 이미지가 어떤 문서인지 예측해야 합니다.
  • 📊 평가 지표 (Evaluation Metric): Macro F1 Score. 이 지표는 각 클래스별 F1 Score의 단순 평균으로, 클래스 간 데이터 불균형이 있을 때 모델의 성능을 보다 정확하게 측정할 수 있는 장점이 있습니다.

3. 왜 이미지로 문서를 분류하나요? (Q&A)

모델 개발에 앞서, 저에게는 한 가지 큰 의문이 있었습니다. 이 질문은 저 뿐만 아니라 많은 분들이 궁금해하실 내용이라 생각해 Q&A 형식으로 정리해 보았습니다.

Q: "문서 분류라면, 당연히 문서의 텍스트를 읽고 그 의미를 파악해야 하는 것 아닌가요? 왜 이미지를 사용하죠?"

A: 저도 처음에는 당연히 OCR(광학 문자 인식)을 사용해 텍스트를 추출한 뒤, 자연어 처리(NLP) 기술로 접근해야 한다고 생각했습니다. 하지만 강사님께 질문한 결과, 이 대회의 진짜 목적을 알 수 있었습니다. 강사님의 답변은 다음과 같았습니다.

"반드시 글씨가 담고 있는 의미를 이해해야 문서를 분류할 수 있는 것은 아닙니다. 이미지를 구성하는 픽셀 정보(로고의 위치, 문서의 레이아웃 등)만으로도 충분히 문서를 구분할 수 있습니다. 이번 대회의 의도는, 향후 학습할 NLP, OCR 같은 고급 기술에 앞서 순수 Computer Vision 접근법만으로 문서 분류가 얼마나 효과적일 수 있는지 직접 체험하게 하는 것입니다."

결국 이 과제는 'NLP 문제를 CV 방식으로 풀어보는' 독특한 경험을 제공하기 위해 의도적으로 설계된 것이었습니다. 이 사실을 깨닫고 나니 프로젝트가 훨씬 더 흥미롭게 느껴졌습니다.

4. 앞으로의 여정: 시리즈 로드맵

이번 대회는 단순히 주어진 노트북 파일(.ipynb)을 수정하는 수준을 넘어, 실무에 가까운 모듈화된(modular) 프로젝트를 직접 구축하는 귀한 경험이었습니다. 앞으로 이어질 본편 시리즈에서는 제가 겪었던 다음과 같은 과정을 상세히 공유해 드릴 예정입니다.

  • Part 2: 프로젝트 구조화 및 EDA (Project Scaffolding & EDA):
    단일 파일의 한계를 벗어나, 재사용과 확장이 용이한 프로젝트 구조를 설계하고 구축하는 과정을 다룹니다. 또한, 본격적인 모델링에 앞서 이미지 데이터를 깊이 있게 탐색하는 과정을 보여드립니다.
  • Part 3: 베이스라인 성능 개선 및 최종 회고 (Improving Baseline & Final Review)
    Part 1에서 다진 기반 위에서 초기 베이스라인 모델의 성능을 개선하기 위해 시도했던 다양한 실험 과정을 공유합니다. 마지막으로 팀 프로젝트에 대한 저의 기여와 배운 점, 그리고 앞으로의 개선 방향을 정리하며 시리즈를 마무리합니다.

5. 맺음말

이번 프로젝트는 단순히 정답을 찾아가는 과정이 아니었습니다. '왜?'라는 질문을 던지고, 서툴지만 체계적인 방법으로 저만의 답을 만들어가는 여정이었습니다. 앞으로 이어질 글에서는 제가 겪었던 고민의 흔적과 선택의 결과들을 꾸밈없이 보여드리고자 합니다. 이 기록이 비슷한 도전을 하는 누군가에게 작은 참고가 되기를 바랍니다. 다음 편에서는 그 여정의 첫걸음으로, 흩어진 아이디어를 담을 프로젝트의 뼈대를 세우고 데이터를 처음 마주했던 경험을 공유해 드리겠습니다. 저의 성장 기록에 함께해주세요. 감사합니다!

목차

  1. 들어가며: 깨진 글자와의 첫 만남
  2. 1단계: 필요한 로케일 모두 설치 및 생성
  3. 2단계: 시스템 로케일 설정 (올바른 방법)
  4. 3단계: 변경 사항 적용 및 확인
  5. 🚨 "invalid locale settings" 에러가 발생하나요? (개인 경험)
  6. 선택 사항: `unzip` 한글 파일명 깨짐 문제 해결
  7. 맺음말: 이제 한글은 문제없어요!

 


 

1. 들어가며: 깨진 글자와의 첫 만남

리눅스 서버에 처음 접속했을 때, 또는 파일을 열었을 때 `파일명: ???.txt`나 `내용: ▒▒▒▒`처럼 깨진 한글을 마주한 경험, 다들 한 번쯤 있으실 겁니다. 낯선 서버 환경에서 마주하는 깨진 글자는 당혹스러움 그 자체죠. 😫.

 

이런 현상은 시스템이 한글을 어떻게 읽고, 쓰고, 표시해야 하는지에 대한 '규칙'이 설정되지 않았기 때문에 발생합니다. 이 규칙을 바로 로케일(Locale) 이라고 부릅니다.

 

이번 포스트에서는 이 로케일을 제대로 설정해서, 지긋지긋한 한글 깨짐 현상을 해결하고 쾌적한 터미널 환경을 만드는 방법을 단계별로 알아보겠습니다. 누구나 쉽게 따라 할 수 있도록 차근차근 안내해 드릴게요.

 

 

2. 1단계: 필요한 로케일 모두 설치 및 생성

가장 먼저 할 일은 우리 시스템에 한글을 해석할 수 있는 재료, 즉 '언어 팩'을 설치하고, 시스템이 사용할 '규칙집(로케일)'을 만들어주는 것입니다.

여기서 중요한 점은, 나중에 터미널 기본 언어를 영어로 사용하고 싶을 경우를 대비해 **한글과 영어 로케일을 모두 생성**해두는 것이 좋습니다. 이렇게 하면 불필요한 에러를 미리 방지할 수 있습니다.

 

1. 한글 언어 팩 설치하기

sudo apt-get update
sudo apt-get install language-pack-ko

 

2. 필요한 로케일 생성하기

아래 명령어를 차례대로 실행하여 '한국어'와 '미국 영어' 로케일을 모두 생성합니다.

sudo locale-gen ko_KR.UTF-8
sudo locale-gen en_US.UTF-8

이제 시스템은 한글과 영어를 모두 이해할 준비를 마쳤습니다.


3. 2단계: 시스템 로케일 설정 (올바른 방법)

이제 시스템의 기본 언어를 지정할 차례입니다. update-locale 명령어를 사용하는 것이 가장 안전하며, 목표에 따라 아래 두 가지 옵션 중 하나를 선택하세요.

 

옵션 A: 한글을 기본 언어로 사용

시스템 메시지 등을 한글로 보고 싶을 때 사용합니다.

sudo update-locale LANG=ko_KR.UTF-8

 

옵션 B: 영어를 기본 언어로 사용 (한글 지원 포함)

대부분의 개발자에게 권장되는 설정입니다. 시스템 메시지는 영어로 유지하되, 한글 파일명이나 내용은 깨지지 않게 지원합니다.

sudo update-locale LANG=en_US.UTF-8

4. 3단계: 변경 사항 적용 및 확인

설정을 마쳤으면, **서버에서 로그아웃했다가 다시 접속**하여 변경사항을 시스템 전체에 적용합니다.

재접속 후, 터미널에 locale 명령어를 입력해서 설정이 잘 적용되었는지 확인해봅시다. 옵션 B를 선택했다면 아래와 같이 결과가 나타나야 합니다.

LANG=en_US.UTF-8
LANGUAGE=
LC_CTYPE="en_US.UTF-8"
... (기타 등등)
LC_ALL=

LANG 변수가 의도한 대로 설정되었다면 성공입니다. ✅


5. 🚨 "invalid locale settings" 에러가 발생하나요? (개인 경험)

혹시 위 단계를 진행하는 중에 아래와 같은 경고와 에러 메시지를 보셨나요?

bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = "en_US:ko_KR",
LC_ALL = "en_US.UTF-8",
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
*** update-locale: Error: invalid locale settings: LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8

원인: 이 에러는 시스템에게 "생성되지 않은" 로케일을 사용하라고 명령했기 때문에 발생합니다. 예를 들어, 1단계에서 ko_KR.UTF-8만 생성한 상태에서 2단계에서 en_US.UTF-8을 시스템 언어로 설정하려고 시도한 경우입니다.

 

해결책: 아주 간단합니다. 사용하려는 로케일을 생성해주기만 하면 됩니다. 아래 명령어로 빠진 영어 로케일을 생성한 뒤, 다시 update-locale 명령어를 실행하면 정상적으로 작동합니다.

sudo locale-gen en_US.UTF-8

다른 가이드에서 ~/.bashrc 파일에 export LANG=...와 같은 라인을 추가하라고 안내 할 수도 있는데 이 방법은 특정 사용자에게만 설정을 적용할 때는 유용하지만, 시스템 전체의 로케일 설정과 충돌을 일으키기 쉽습니다. 특히, 생성되지 않은 로케일을 `.bashrc`에 추가하고 source ~/.bashrc를 실행하면, 현재 터미널 세션이 불안정해지면서 위와 같은 에러가 계속 발생하게 됩니다. 시스템 전체 설정은 update-locale을 사용하고, 개인 설정 파일은 건드리지 않는 것이 가장 안전합니다.


6. 선택 사항: `unzip` 한글 파일명 깨짐 문제 해결

시스템 로케일 설정을 마쳤음에도 불구하고, Windows에서 만든 zip 파일의 압축을 풀 때 한글 파일명이 깨지는 경우가 있습니다. 이는 Windows(CP949 인코딩)와 Linux(UTF-8 인코딩)의 문자 처리 방식이 다르기 때문입니다.

이 문제를 매번 옵션을 주어 해결하는 것은 번거로우니, `unzip` 명령어를 실행할 때 항상 한글 인코딩 옵션이 적용되도록 별칭(alias)을 설정해두면 편리합니다.

 

1. `.bashrc` 파일에 alias 추가하기

아래 명령어를 실행하여 .bashrc 파일 맨 아래에 unzip 별칭을 추가합니다.

echo "alias unzip='unzip -O cp949'" >> ~/.bashrc

-O cp949 옵션은 unzip에게 "압축 파일 안의 파일명은 cp949 방식으로 되어 있으니, 이걸로 해석해줘" 라고 알려주는 역할을 합니다.

 

2. 설정 적용하기

마지막으로, 아래 명령어를 실행하여 변경된 .bashrc 설정을 현재 터미널 세션에 바로 적용합니다.

source ~/.bashrc

이제부터는 그냥 `unzip` 명령어만 사용해도 한글 파일명이 깨지지 않고 잘 풀리는 것을 확인할 수 있습니다.


7. 맺음말: 이제 한글은 문제없어요!

이번 포스트에서는 리눅스 터미널의 로케일(Locale)을 설정하여 한글 깨짐 문제를 해결하는 방법을 알아보았습니다. 특히 발생하기 쉬운 에러의 원인과 해결책을 함께 다루어, 이제 여러분의 서버는 한글을 안정적으로 처리할 수 있는 환경이 되었을 겁니다. 이제 한국어/영어를 사용할 수 있는 개발 환경이 준비 되었습니다. 👏

 

포스트를 따라오시면서 궁금했거나 추가로 해결한 문제가 있다면 댓글로 남겨주세요!

+ Recent posts