Hun-Bot

DACON 멀티모달 바이어스 개발일지 01: 데이터 자동화와 운영 구조 선택

DACON multimodal VLM Colab Google Drive GitHub automation MLOps devlog

대회 링크 : https://dacon.io/competitions/official/236722/overview/description

DACON 멀티모달 바이어스 대회를 준비하면서, train 데이터를 직접 수집해서 모델을 학습시켜야 했기 때문에, 이 과정 자체를 효율화하며 사람의 반복 작업을 최소화하는 것을 목적으로 두고 공부 및 개발을 진행했습니다.

macOS + rclone
→ 외부 train source를 Google Drive에 직접 수집

Google Drive
→ raw data, processed data, cache, runs, submissions 저장

GitHub private repo
→ 코드, config, 문서, manifest, 실험 기록 관리

Colab Pro
→ GPU 학습 / 추론 실행

Shell script + Python script
→ 실제 자동화 로직

GitHub Actions
→ 나중에 검증/보고서 생성용 wrapper로만 사용

시작점: train data를 직접 구축해야 한다

이 대회는 제공되는 train 데이터가 사실상 예시 수준이고, 참가자가 직접 학습 데이터를 구성해야 하는 형태다.

처음 test 이미지를 살펴봤을 때 가장 먼저 눈에 들어온 특징은 다음과 같았다.

- 이미지 크기가 제각각이다.
- 흑백 이미지와 컬러 이미지가 섞여 있다.
- 이미지 안에 텍스트가 있는 경우가 있다.
- 인종, 나이, 성별, 상황이 매우 다양하다.
- stock image 계열로 보이는 이미지도 있다.

이걸 보고 처음에는 test 이미지의 분포를 참고해서 비슷한 train 데이터를 만들어야 하나 고민했다.

하지만 이 방향은 위험하다. test 이미지의 출처를 역추적하거나, stock image 원본 caption을 찾아보거나, test의 질문/선택지 패턴을 바탕으로 유사 train을 생성하는 것은 data leakage 위험이 크다.

그래서 기준을 이렇게 잡았다.

하지 않을 것:
- test 이미지 reverse search
- Shutterstock 등 stock image 원본 추적
- test 질문/선택지 패턴 기반 train 생성
- 평가 데이터에 맞춘 prompt rule 작성

해야할 것:
- 공개적으로 사용 가능한 외부 데이터 source 수집
- 라이선스와 출처 기록
- 일반화 가능한 VQA / bias / unknown 데이터 구성
- 이미지 크기, 흑백, OCR 등은 robustness 관점에서 처리

즉, 목표는 test를 베끼는 것이 아니라 근거가 있으면 답하고, 근거가 없으면 모른다고 답하는 VLM 학습 데이터 파이프라인을 만드는 것이다.

처음 생각했던 구조: Colab + GitHub Actions

처음에는 Colab Pro에서 학습하고 GitHub Actions로 자동화하면 모든 게 해결될 것 같았다.

대략 이런 그림이었다.

GitHub repo push
→ GitHub Actions 실행
→ 데이터 확인
→ 학습
→ 보고서 생성
→ submission 생성

하지만 금방 문제가 보였다.

GitHub Actions는 코드 검증, schema check, 보고서 생성, submission package 생성에는 적합하지만, VLM 학습이나 대용량 데이터 처리에는 적합하지 않다.

특히 이 프로젝트에서는 다음 작업들이 필요하다.

- 외부 이미지/annotation 다운로드
- 압축 해제
- 수천~수만 개 파일 정리
- OCR cache 생성
- VLM fine-tuning
- checkpoint 저장

이 작업들은 GitHub Actions보다 Colab이나 로컬 환경이 더 적합하다.

그래서 GitHub Actions의 역할을 줄였다.

GitHub Actions가 할 일:
- repo 구조 검증
- config/schema check
- run_manifest 검증
- submission.csv 검증
- 보고서 생성 wrapper
- package 생성

GitHub Actions가 하지 않을 일:
- 대용량 데이터 다운로드
- VLM 학습
- checkpoint 저장
- Google Drive 내부 파일 직접 처리

자동화의 핵심은 GitHub Actions가 아니라, 어디서 실행해도 같은 방식으로 돌아가는 shell script와 Python script가 되어야 한다고 판단했다.

두 번째 생각: GitHub private repo와 GH_TOKEN

private repo를 쓸 것이기 때문에, 처음에는 Colab에서 GitHub repo를 자동으로 clone/pull/push하려면 GH_TOKEN이 필요하다고 생각했다.

이 구조는 분명 장점이 있다.

Colab에서 private repo clone
→ 학습 실행
→ run_manifest/report/submission 자동 생성
→ GitHub에 자동 commit/push

하지만 지금 단계에서는 꼭 필요하지 않았다.

초기에는 다음 방식으로도 충분하다.

Drive에서 notebook 실행
→ repo zip을 /content/work에 풀기
→ shell script 실행
→ 결과는 Drive에 저장
→ 작은 결과만 나중에 수동으로 GitHub에 반영

그래서 GH_TOKEN은 당장 필수 요소에서 제외했다.

현재 기준은 다음과 같다.

GH_TOKEN 없이 가능한 것:
- Drive mount
- repo zip 압축 해제
- 데이터 수집
- 데이터 변환
- 학습
- 추론
- 보고서 생성
- 결과 파일 수동 반영

GH_TOKEN이 필요한 것:
- Colab에서 private repo 자동 clone/pull
- Colab에서 GitHub로 자동 commit/push
- Colab에서 GitHub Release 업로드

즉, 지금은 수동 동기화 + 명령어 자동화로 시작하고, 실험이 많아져서 귀찮아지는 시점에 GH_TOKEN을 도입하기로 했다.

내가 진짜 원했던 것: train data collector

중간에 방향을 다시 잡았다.

처음 대화에서는 보고서 자동화나 GitHub Actions 이야기가 많았지만, 내가 실제로 원했던 것은 이것이었다.

train data를 자동으로 수집해와서
내가 만든 Google Drive folder에
source별로 차곡차곡 정리하는 것

즉, 핵심은 train data collector다.

사람이 매번 해야 하는 작업은 이런 것이다.

- 데이터셋 사이트 들어가기
- zip 다운로드하기
- Drive에 업로드하기
- 압축 풀기
- 폴더 이름 정리하기
- 어떤 데이터였는지 기록하기
- 라이선스와 출처 남기기

이 작업이 반복되면 실수도 많아지고, 나중에 어떤 데이터로 학습했는지 추적하기도 어렵다.

그래서 collector는 다음 역할을 맡아야 한다.

collector가 할 일:
- config에 정의된 source 다운로드
- downloads/ 폴더에 원본 파일 저장
- 압축 해제
- source별 폴더 정리
- sha256 기록
- collection_manifest.csv 생성
- collection_manifest.json 생성

collector가 하지 않을 일:
- test 이미지 기반 유사 데이터 생성
- stock image 원본 추적
- 정답 추론
- 모델 학습

이제 전체 파이프라인은 이렇게 정리된다.

1. collect_train_sources.py
   외부 raw source를 Drive에 수집

2. build_data.py
   raw source를 DACON train.csv 형식으로 변환

3. train.py
   processed train/dev로 학습

4. infer.py
   test 추론

5. validate_submission.py
   제출 파일 검증

6. make_run_report.py
   실험 보고서 생성

처음 성공한 source: BBQ

collector를 붙인 뒤 가장 먼저 BBQ를 받아봤다.

BBQ를 먼저 선택한 이유는 단순하다.

- 비교적 가볍다.
- 이미지 데이터가 아니라 수집 테스트에 좋다.
- ambiguous / disambiguated 구조를 학습 데이터 설계에 참고할 수 있다.
- "근거 없으면 unknown" 전략을 보조하는 데 사용할 수 있다.

BBQ 수집이 성공했다는 것은 collector의 기본 구조가 정상이라는 뜻이다.

✅ config 읽기
✅ source 선택
✅ 다운로드
✅ 압축 해제 또는 repo 정리
✅ Drive 저장
✅ manifest 기록

이제 같은 방식으로 다른 source를 수집하면 된다.

다음 수집 대상

다음 source는 smoke pipeline 기준으로 작게 가져간다.

1. KoBBQ
2. A-OKVQA annotations
3. COCO 2017 annotations
4. COCO 2017 val images
5. VQA v2 val

아직 받지 않기로 한 source도 있다.

COCO train2017 전체 이미지

이유는 간단하다.

- 크다.
- 파일 수가 많다.
- Drive sync와 Colab I/O에 부담이 크다.
- 아직 build_data.py와 train.py가 완전히 검증되지 않았다.

먼저 작은 smoke source로 전체 파이프라인이 도는지 확인한 뒤, 필요하면 train2017 전체가 아니라 필요한 image_id subset만 받는 방식으로 가는 것이 낫다.

Colab에서 수집할 것인가, 로컬에서 수집할 것인가

처음에는 Colab CPU runtime에서 나머지 데이터를 수집하려고 했다.

하지만 곧 다시 구조를 조정했다.

데이터 수집은 GPU가 필요 없다.

다운로드
압축 해제
파일 이동
hash 계산
manifest 생성

전부 CPU와 I/O 작업이다.

따라서 Colab Pro GPU는 학습용으로 아끼고, 데이터 수집은 다른 방식으로 하는 것이 더 낫다.

처음에는 로컬 CPU에서 수집하는 방식을 생각했다.

macOS local
→ 데이터 다운로드
→ 압축 해제
→ Drive로 sync

하지만 내 Mac은 로컬 용량이 부족했다.

그래서 최종적으로 선택한 구조는 다음이다.

macOS + rclone
→ 외부 zip URL을 Google Drive로 직접 전송

Colab CPU
→ Drive에 올라간 zip을 압축 해제하고 manifest 생성

Colab GPU
→ 학습과 추론 실행

왜 rclone인가

로컬 디스크 용량이 충분하다면 staging folder에 데이터를 받고 Drive로 옮기면 된다.

하지만 지금 상황에서는 큰 zip을 Mac에 저장하기 어렵다.

그래서 Mac을 저장소로 쓰지 않고, 전송 제어용 조종기처럼 쓰기로 했다.

외부 데이터 URL
→ macOS rclone copyurl
→ Google Drive raw/external/.../downloads/

이 구조의 장점은 다음과 같다.

- Mac 로컬에 큰 zip을 저장하지 않아도 된다.
- Colab GPU runtime을 데이터 다운로드에 낭비하지 않는다.
- Drive에 원본 zip이 source별로 정리된다.
- Colab에서 나중에 압축 해제와 manifest 생성을 이어갈 수 있다.

즉, Mac은 계산 서버도 저장소도 아니다. 그냥 Drive로 데이터를 보내는 컨트롤러다.

최종 선택한 역할 분리

이번에 최종적으로 선택한 역할 분리는 다음과 같다.

macOS
→ rclone으로 외부 source를 Google Drive에 직접 업로드

Google Drive
→ raw data, processed data, cache, runs, submissions 저장

Colab CPU
→ 압축 해제, manifest 생성, 가벼운 데이터 검증

Colab GPU
→ VLM 학습, 추론

GitHub private repo
→ 코드, config, 문서, 실험 기록 관리

GitHub Actions
→ 나중에 검증과 보고서 생성을 위한 선택적 wrapper

이 구조가 마음에 드는 이유는 각 도구의 역할이 명확하기 때문이다.

GitHub는 저장소가 아니다.
→ 코드와 기록만 관리한다.

Drive는 코드 저장소가 아니다.
→ 데이터와 산출물만 관리한다.

Colab은 데이터 창고가 아니다.
→ GPU 작업장으로 쓴다.

Mac은 대용량 저장소가 아니다.
→ 데이터 전송과 관리 명령 실행용으로 쓴다.

현재 자동화 명령 구조

자동화는 GitHub Actions에 직접 넣기보다 shell script와 Python script로 만든다.

이유는 하나다.

같은 명령을
Colab에서도 실행하고,
로컬에서도 실행하고,
나중에는 GitHub Actions에서도 호출할 수 있게 하기 위해서다.

현재 기준으로 중요한 스크립트는 다음과 같다.

scripts/
├── collect_train_sources.py
├── build_data.py
├── train.py
├── infer.py
├── validate_submission.py
├── make_run_report.py
└── ops/
    └── collect_train_sources.sh

collector 실행은 다음과 같은 형태로 한다.

python scripts/collect_train_sources.py \
  --config configs/collect/train_sources_v0_smoke.yaml \
  --paths-config configs/paths.local.yaml \
  --sources bbq

Colab에서는 shell 명령 앞에 !를 붙인다.

!bash scripts/ops/collect_train_sources.sh \
  configs/collect/train_sources_v0_smoke.yaml \
  --sources bbq

이 차이를 명확히 해두는 것도 중요했다. Colab cell은 기본적으로 Python이고, shell 명령은 ! 또는 %%bash로 실행해야 한다.

실험 추적 구조도 같이 필요하다

데이터 수집 자동화만 만들면 충분할 것 같지만, 실제로는 실험이 늘어나면 금방 헷갈린다.

이번 실험에서 뭘 바꿨지?
dataset ratio였나?
prompt였나?
OCR였나?
LoRA rank였나?
왜 이 run을 만들었지?
이 submission은 어떤 checkpoint에서 나왔지?

그래서 실험 추적 구조도 같이 필요하다.

최소한 다음 파일은 남겨야 한다.

runs/<RUN_ID>/run_manifest.json
reports/runs/<RUN_ID>.md
reports/experiment_graph.json
reports/experiment_graph.md
experiments/<RUN_ID>/experiment.yaml

각 run은 다음 정보를 가져야 한다.

- run_id
- parent_run
- hypothesis_id
- dataset_config
- train_config
- infer_config
- base_model
- adapter_path
- train_csv
- dev_csv
- submission_csv
- dev_metrics
- decision

실험은 단순한 로그가 아니라 그래프여야 한다.

observation
→ hypothesis
→ change
→ run
→ result
→ decision
→ next run

이 구조가 있어야 “왜 이 선택을 했는지”를 나중에 다시 이해할 수 있다.

왜 지금은 GitHub Actions를 미루는가

GitHub Actions는 나중에 필요하다.

하지만 지금 당장은 핵심이 아니다.

지금 가장 중요한 것은 다음이다.

1. 데이터가 자동으로 수집되는가?
2. Drive에 source별로 정리되는가?
3. manifest가 남는가?
4. build_data.py가 raw source를 읽을 수 있는가?
5. Colab에서 학습을 재시작할 수 있는 구조인가?

GitHub Actions는 이 다음 단계다.

나중에 붙일 때도 Actions 안에 로직을 직접 넣기보다는 기존 shell script를 호출하는 wrapper로만 둔다.

GitHub Actions
→ bash scripts/ops/report.sh
→ bash scripts/ops/package_submission.sh
→ bash scripts/ops/validate.sh

이렇게 해야 자동화 로직이 여러 곳에 흩어지지 않는다.

지금까지의 결정 요약

이번 개발 과정에서 정리된 결정은 다음과 같다.

1. train data를 수동으로 모으지 않는다.
   → collector를 만든다.

2. test 이미지 기반으로 train을 만들지 않는다.
   → leakage 위험을 피한다.

3. GitHub에는 raw data를 올리지 않는다.
   → private repo라도 코드와 기록만 관리한다.

4. Google Drive를 artifact store로 쓴다.
   → raw, processed, cache, runs, submissions를 저장한다.

5. Colab Pro는 GPU 학습용으로 쓴다.
   → 데이터 수집에 GPU runtime을 쓰지 않는다.

6. GitHub Actions는 핵심 자동화가 아니다.
   → shell/Python script를 먼저 만들고, Actions는 wrapper로 둔다.

7. Mac 로컬 용량이 부족하다.
   → rclone으로 외부 URL을 Drive에 직접 보낸다.

8. 실험마다 run_manifest와 graph를 남긴다.
   → 실험 계보를 추적한다.

다음 단계

다음 개발 단계는 명확하다.

1. rclone 설정
2. KoBBQ / A-OKVQA / COCO val / VQA val을 Drive로 수집
3. Colab에서 압축 해제와 manifest 생성
4. build_data.py 구현
5. collected raw source를 DACON train.csv 형식으로 변환
6. data_report.json 생성
7. 첫 smoke training 실행
8. run_manifest / run_report / experiment_graph 생성

특히 다음 글에서는 build_data.py를 중심으로 정리할 예정이다.

핵심은 외부 source를 단순히 모으는 것이 아니라, 대회 형식에 맞게 다음 schema로 변환하는 것이다.

sample_id
image_path
context
question
answers
label

그리고 내부 검증용으로는 다음 메타데이터도 함께 유지할 계획이다.

source
is_ambiguous
language
requires_ocr
protected_attribute_type
license_source

이렇게 해야 모델 성능을 전체 accuracy 하나로만 보지 않고, slice별로 분석할 수 있다.

ambiguous accuracy
disambiguated accuracy
unknown recall
OCR-required accuracy
grayscale robustness
source별 accuracy

마무리

이번 단계에서 가장 큰 변화는 “학습을 어떻게 돌릴까?”에서 “학습 가능한 데이터를 어떻게 재현 가능하게 모을까?”로 관점이 바뀐 것이다.

처음에는 Colab, GitHub Actions, GH_TOKEN 같은 도구 중심으로 생각했다. 하지만 실제 문제는 도구가 아니라 데이터 운영이었다.

그래서 지금의 결론은 단순하다.

데이터 수집은 collector가 한다.
데이터 저장은 Drive가 한다.
학습은 Colab이 한다.
코드와 기록은 GitHub가 한다.
큰 파일은 GitHub에 올리지 않는다.
실험은 graph로 추적한다.

이 구조가 잡히면 이후의 작업은 훨씬 단순해진다.

이제부터는 매번 데이터를 손으로 옮기는 대신, source를 config에 추가하고 collector를 실행하면 된다. 그리고 학습 결과는 run manifest와 graph에 남긴다.

다음 목표는 build_data.py다. 수집된 raw source를 실제 대회 train format으로 바꾸는 단계로 넘어간다.

data_automation_multimodal_bias 1 / 2

목차

댓글