Hun-Bot

DACON 멀티모달 바이어스 개발일지 02: Colab 실험 하네스 설계

DACON multimodal VLM Colab Qwen experiment-harness MLOps devlog

이번 글은 DACON 멀티모달 바이어스 대회를 준비하면서 만든 Colab 추론 실험 하네스를 정리한 개발일지다.

우선, Colab Pro 계정을 사오길 바란다. 내가 생각 없이 코드만 만들고 일반 T4로 돌렸다가 너무 느려서 아까운 시간만 날렸다.

그래서, shard로 결과를 저장하면서 런타임이 죽어도 처음부터 다시 하지 않는 구조를 가진 실험 하네스를 만들었다.(여기에 하네스를 붙여도 되는지 모르겠지만, 편의상 그렇게 부르겠다.)

- Colab 런타임이 중간에 종료될 수 있다.
- 전체 8,500개를 한 번에 돌리고 마지막에 저장하면 중간 결과가 날아간다.
- 모델, prompt, modality, training 상태를 바꾸면서 실험하면 결과 폴더가 쉽게 꼬인다.
- public score가 높아도 private score가 높다는 보장이 없다.

핵심 목표는 하나였다.

Colab이 죽어도 처음부터 다시 하지 않는다.

현재 전략 요약

이번 대회 공지에 따르면 Public Score는 오픈 벤치셋 기반 샘플에서 산출되고, Private Score는 운영진 자체 제작 샘플에서 산출된다. 즉 Public Score가 매우 높아도 최종 Private 성능을 보장하지 않는다.

그리고 “이미지-텍스트 기반 질의응답 상황에서 AI 모델이 주어진 정보에 근거해 적절한 선택지를 예측하고, 판단이 어려운 상황에서는 불확실성을 올바르게 인식하는 능력을 평가” 하는 대회이기에 실험에서 반드시 이미지+텍스트 조합을 가져가야 한다고 생각했다.

아래는 현재까지의 실험 전략 요약이다.

목적 1. 추론 파이프라인 검증
목적 2. 큰 open VLM의 public-open-benchmark 대응력 확인
목적 3. prompt별 label distribution과 parser 안정성 확인
목적 4. 이후 QLoRA/SFT 실험을 위한 baseline 확보

실험 계획

초기 실험 계획은 다음과 같이 고정했다.

E002 Qwen3.5-9B / image+text / safe_generic_v0 / zero-shot
E004 Qwen3.5-9B / image+text / decisive_evidence_v1 / zero-shot

E006 InternVL3.5-8B / image+text / safe_generic_v0 / zero-shot

E008 GLM-4.1V-9B-Thinking / image+text / safe_generic_v0 / zero-shot

E009 Qwen3.5-9B / image+text / trained_v0 / QLoRA
E010 InternVL3.5-8B / image+text / trained_v0 / QLoRA
E011 GLM-4.1V-9B / image+text / trained_v0 / QLoRA

현재는 E002를 먼저 돌리고 있다.

E002
= Qwen3.5-9B
= image+text
= safe_generic_v0
= zero-shot

현재 Colab에서 확인된 실행 상태는 다음과 같다.

RUN_ID=E002_qwen35_9b_safe_generic_v0_zero_shot_imgtxt_full
LIMIT=0
BATCH_SIZE=2
SHARD_SIZE=50
already completed: 450

이는 이미 450개 샘플이 Drive에 shard 파일로 저장되었고, Colab이 끊기더라도 그 부분은 다시 계산하지 않아도 된다는 뜻이다.

각각의 결과는 마지막에 표 형태로 정리할 예정이다.

왜 shard 구조가 필요한가

기존 참고 코드는 전체 8,500개를 모두 추론한 뒤 마지막에 submission.csv를 저장하는 구조였다.

8500개 전체 추론
→ 마지막에 submission.csv 저장

이 구조는 Colab에서는 매우 위험하다. 중간에 런타임이 죽으면 몇 시간 동안 계산한 결과가 모두 날아갈 수 있다.

그래서 새 노트북에서는 다음 구조로 바꿨다. 다만, 초창기 실험으로 50개씩 shard로 저장하는 구조로 시작했고, 이후에 필요에 따라 shard 크기를 조절할 예정이고, 현재는 500개씩 저장하는 것을 고려하고 있다.

50개 추론
→ part_000000_000049.csv 저장

다음 50개 추론
→ part_000050_000099.csv 저장

...

모든 part를 병합
→ submission.csv 생성

실제 저장 구조는 다음과 같다.

/content/drive/MyDrive/dacon_multimodal_bias/
├── runs/
│   └── E002_qwen35_9b_safe_generic_v0_zero_shot_imgtxt_full/
│       ├── parts/
│       │   ├── part_000000_000049.csv
│       │   ├── part_000050_000099.csv
│       │   ├── part_000100_000149.csv
│       │   └── ...
│       ├── heartbeat.json
│       ├── raw_full.csv
│       └── run_manifest.json

└── submissions/
    └── E002_qwen35_9b_safe_generic_v0_zero_shot_imgtxt_full/
        └── submission.csv

이 구조 덕분에 Colab 런타임이 죽어도 다음처럼 재개할 수 있다.

런타임 종료
→ 모델과 변수는 날아감
→ Drive의 part 파일은 유지됨
→ 다시 노트북 실행
→ 기존 part는 skip
→ 마지막 미완성 shard부터 재계산

즉 최악의 경우에도 손실되는 것은 현재 진행 중이던 50개 shard뿐이다.

노트북 전체 구조

노트북은 아래 순서로 구성했다. 좀 복잡하긴 한데, 노트북 파일을 같이 올릴 예정이기에 해당 파일을 보면 될 것 같다.

00. Install dependencies
01. Global experiment settings
02. Drive mount and path recovery
03. DACON test schema validation
04. Experiment plan registry
05. Prompt registry
06. Activate experiment
07. Multimodal active-run validation
08. Common prompt builder and answer parser
09. Resumable shard inference harness
10. Adapter A — Qwen/Qwen3.5-9B
11. Adapter B — Generic Transformers image-text pipeline for InternVL / GLM
12. Smoke inference
13. Output validation
14. Full submission guard
15. Run status check after Colab restart
16. Memory cleanup between models
17. QLoRA/SFT training experiments placeholder

실제로 E002 full을 재개할 때 매번 모든 셀을 돌릴 필요는 없다.

런타임이 완전히 죽은 뒤 재개할 때는 다음 셀만 실행하면 된다.

00
01
02
04
05
06
08
09
10
12

0307은 데이터 구조 검증 셀이므로, 이미 확인한 이후에는 생략해도 된다.

00. Install dependencies

첫 번째 셀은 Colab 런타임이 새로 시작되었을 때 필요한 패키지를 설치한다.

%pip install -U -q pandas tqdm accelerate safetensors qwen-vl-utils bitsandbytes
%pip install -U -q transformers
%pip install -U -q "pillow==10.4.0"

여기서 가장 중요한 것은 pillow==10.4.0이다.

실행 중 한 번 다음 에러가 발생했다.

ImportError: cannot import name '_Ink' from 'PIL._typing'

이 문제는 Pillow 패키지 버전이 꼬여서 발생했다. transformers를 import하는 과정에서 내부적으로 PIL.ImageText가 import되는데, 설치된 Pillow의 내부 typing 모듈과 버전이 맞지 않아 깨진 것이다.

해결은 단순하게 했다.

%pip install -U -q "pillow==10.4.0"

런타임이 새로 시작되면 이 설치 셀을 먼저 실행한다. 한 번 설치되어 있으면 대부분 빠르게 지나간다.

01. Global experiment settings

이 셀은 현재 실행할 실험을 선택하는 곳이다.

주요 변수는 다음과 같다.

ACTIVE_EXP_ID = "E002"
RUN_SUFFIX = "full"
LIMIT = 0
BATCH_SIZE = 2
SHARD_SIZE = 50
RESET_PARTS = False

각 변수의 의미는 다음과 같다.

ACTIVE_EXP_ID
→ 어떤 실험을 실행할지 선택한다.
→ E002, E004, E006, E008, E009, E010, E011 중 하나.

RUN_SUFFIX
→ run 이름에 붙는 suffix.
→ smoke, full, retry01 등.

LIMIT
→ 추론할 샘플 개수.
→ 100이면 smoke test.
→ 0이면 전체 8,500개 full run.

BATCH_SIZE
→ 한 번에 모델에 넣을 샘플 수.
→ T4에서는 1이 안전하다.
→ 현재는 GPU 변경 후 2로 재개했다.

SHARD_SIZE
→ part 파일 하나에 저장할 샘플 수.
→ 50이면 50개마다 Drive에 저장.

RESET_PARTS
→ 기존 part 파일을 지우고 처음부터 다시 할지 여부.
→ 재개할 때는 반드시 False.

가장 위험한 변수는 RESET_PARTS다.

RESET_PARTS = False

재개할 때 이 값이 True면 이미 저장한 shard가 삭제될 수 있다. full run 중에는 절대 True로 바꾸지 않는다.

02. Drive mount and path recovery

이 셀은 Google Drive를 mount하고, 모든 경로를 복구한다.

핵심 경로는 다음과 같다. 이 경로는 내 Google Drive 폴더 구조에 맞게 설정되어 있다. 만약, 사용하고 싶다면 자신의 Drive 폴더 구조에 맞게 수정해야 한다.

근데, 아마 자동으로 생성해줄거라서 그냥 돌려도 같은 결과가 나올 것 같다.

DRIVE_ROOT
= /content/drive/MyDrive/dacon_multimodal_bias

TEST_CSV
= /content/drive/MyDrive/dacon_multimodal_bias/raw/dacon_open/test/test.csv

IMAGE_DIR
= /content/drive/MyDrive/dacon_multimodal_bias/raw/dacon_open/test/images

RUNS_ROOT
= /content/drive/MyDrive/dacon_multimodal_bias/runs

SUBMISSIONS_ROOT
= /content/drive/MyDrive/dacon_multimodal_bias/submissions

이 셀의 목적은 런타임이 새로 시작되어도 같은 Drive 폴더 구조를 다시 잡는 것이다.

from google.colab import drive

drive.mount("/content/drive")

Drive에 저장된 파일은 런타임이 죽어도 유지된다.

날아가는 것:
- 메모리 변수
- 로드된 모델
- 현재 GPU 상태
- 미저장 shard

안 날아가는 것:
- Drive의 raw 데이터
- Drive의 part 파일
- Drive의 submission 파일
- Drive의 run manifest

03. DACON test schema validation

이 셀은 test.csv와 이미지 폴더가 정상인지 확인하는 검증 셀이다.

처음에는 전체 8,500개 이미지를 모두 열어보도록 만들었다.

for i, row in tqdm(test_df.iterrows(), total=len(test_df), desc="validate test rows"):
    ...

하지만 이 방식은 너무 오래 걸렸다.

실제로 7,708개까지 확인하는 데 1시간 이상 걸렸다.

7708 / 8500
약 91%
에러 없음

따라서 이후에는 이 셀을 매번 실행하지 않는다.

현재 판단은 다음과 같다.

최초 1회
→ 실행할 가치 있음

이미 90% 이상 검증했고 데이터 구조가 바뀌지 않음
→ 생략 가능

다시 실행해야 하는 경우
→ DACON 데이터를 새로 받음
→ raw/dacon_open 폴더를 교체함
→ test.csv나 images 경로를 바꿈

즉 GPU 변경, 모델 변경, prompt 변경, Colab 재시작만으로는 03 셀을 다시 실행하지 않는다.

04. Experiment plan registry

이 셀은 실험 계획을 DataFrame으로 저장한다.

각 실험은 다음 정보를 가진다.

exp_id
model_short
model_id
model_family
modality
no_image
prompt
training
train_config
submit_candidate
diagnostic_only
notes

예를 들어 E002는 다음과 같다.

exp_id: E002
model_short: qwen35_9b
model_id: Qwen/Qwen3.5-9B
model_family: qwen
modality: image_text
no_image: False
prompt: safe_generic_v0
training: zero_shot
submit_candidate: True

이 정보를 experiment_plan_v1_multimodal.csv로 저장한다.

/content/drive/MyDrive/dacon_multimodal_bias/experiments/experiment_plan_v1_multimodal.csv

실험 계획을 CSV로 저장하는 이유는 나중에 다음을 추적하기 위해서다.

- 어떤 모델을 썼는지
- 어떤 prompt를 썼는지
- image+text인지 text-only인지
- zero-shot인지 trained인지
- 제출 후보인지 진단용인지

05. Prompt registry

이 셀은 prompt를 이름으로 관리한다.

현재 주요 prompt는 세 가지다.

safe_generic_v0
→ 제출 후보용 기본 prompt

decisive_evidence_v1
→ 조금 더 적극적으로 증거를 사용하도록 한 prompt

trained_v0
→ QLoRA/SFT 이후 기본 prompt

safe_generic_v0의 핵심은 다음이다.

- image, context, question, answer options만 사용한다.
- 명시적 근거가 있으면 답한다.
- 근거가 부족하면 unknown/cannot be determined 계열 선택지를 고른다.
- 성별, 인종, 민족, 나이, 외모, 복장, 사회적 배경에 대한 stereotype을 사용하지 않는다.

중요한 점은 test 구조를 직접 언급하지 않는다는 것이다.

예를 들어 다음과 같은 문장은 제출 후보 prompt에 넣지 않는다.

There are normally exactly two people plus one uncertainty option.

이런 문장은 test 문항 구조를 관찰해서 만든 prompt처럼 보일 수 있고, Private generalization에도 좋지 않을 수 있다.

06. Activate experiment

이 셀은 현재 실행할 실험을 활성화한다.

예를 들어 현재 full run에서는 다음 설정을 쓴다.

activate_experiment(
    "E002",
    limit=0,
    batch_size=2,
    shard_size=50,
    run_suffix="full",
    reset_parts=False,
)

이 함수는 다음 전역 변수를 설정한다.

ACTIVE_EXP
ACTIVE_EXP_ID
MODEL_ID
MODEL_FAMILY
MODEL_SHORT
PROMPT_NAME
SYSTEM_PROMPT
NO_IMAGE
LIMIT
BATCH_SIZE
SHARD_SIZE
RUN_ID
RUN_DIR
SUB_DIR
PART_DIR
OUT_PATH
DUMP_RAW
RUN_MANIFEST

특히 중요한 것은 RUN_ID다.

현재 E002 full run의 RUN_ID는 다음과 같다.

E002_qwen35_9b_safe_generic_v0_zero_shot_imgtxt_full

이 이름을 기준으로 part, raw, submission이 저장된다.

RUN_DIR
= runs/E002_qwen35_9b_safe_generic_v0_zero_shot_imgtxt_full

PART_DIR
= runs/E002_qwen35_9b_safe_generic_v0_zero_shot_imgtxt_full/parts

OUT_PATH
= submissions/E002_qwen35_9b_safe_generic_v0_zero_shot_imgtxt_full/submission.csv

재개할 때는 반드시 동일한 ACTIVE_EXP_ID, RUN_SUFFIX, LIMIT, SHARD_SIZE를 유지해야 같은 RUN_ID가 생성되고 기존 part를 skip할 수 있다.

07. Multimodal active-run validation

이 셀은 현재 활성화된 run에 대해 image+text 입력이 정상인지 샘플 단위로 확인한다.

하지만 이미 test schema를 충분히 검증했고, full run 재개가 목적이라면 이 셀도 매번 실행할 필요가 없다.

처음 smoke 실행 전
→ 실행 권장

같은 데이터로 full 재개
→ 생략 가능

모델/prompt만 바꿈
→ 생략 가능

현재 루틴에서는 03, 07을 생략하고 다음 순서로 재개한다.

00 → 01 → 02 → 04 → 05 → 06 → 08 → 09 → 10 → 12

08. Common prompt builder and answer parser

이 셀은 모든 모델 adapter가 공통으로 사용하는 함수들을 정의한다.

핵심 함수는 다음과 같다.

parse_options
find_unknown_index
build_user_text
parse_answer

parse_options

answers 컬럼은 JSON 문자열 형태다.

["Not enough info", "The wife", "The husband"]

이를 Python list로 변환한다.

def parse_options(answers_field):
    if isinstance(answers_field, list):
        return answers_field
    return json.loads(answers_field)

find_unknown_index

선택지 중 unknown 계열 선택지가 몇 번인지 찾는다.

탐지하는 표현은 다음과 같다.

not enough
cannot
unknown
undetermined
not answerable
can't tell
insufficient

모델 출력 parsing에 실패했을 때 fallback으로 사용한다.

build_user_text

모델에게 줄 user message를 만든다.

Context:
...

Question:
...

Options:
0. ...
1. ...
2. ...

Choose the correct option using the image and text evidence.

parse_answer

모델이 생성한 텍스트에서 최종 label을 뽑는다.

우리는 모델에게 다음 형식으로 답하게 한다.

Reasoning: <one short sentence>
Answer: <0, 1, or 2>

parse_answer는 먼저 Answer: 0/1/2 패턴을 찾고, 없으면 숫자, 없으면 선택지 문자열, 그래도 없으면 unknown option으로 fallback한다.

이 parser는 완벽한 정답 생성기가 아니라, 모델 출력 형식을 안정적으로 CSV label로 변환하기 위한 후처리다.

09. Resumable shard inference harness

이 셀이 전체 노트북의 핵심이다.

주요 함수는 다음과 같다.

write_csv_atomic
write_json_atomic
update_manifest
part_path_for
read_completed_sample_ids
merge_parts
run_resumable_inference

write_csv_atomic

CSV를 안전하게 저장한다.

직접 to_csv로 최종 파일에 쓰면, 저장 중 런타임이 끊겼을 때 깨진 파일이 남을 수 있다.

그래서 임시 파일에 먼저 쓰고, 저장이 완료되면 os.replace로 교체한다.

submission.csv.tmp
→ 저장 완료
→ submission.csv로 atomic replace

part_path_for

각 shard의 파일명을 만든다.

part_000000_000049.csv
part_000050_000099.csv
part_000100_000149.csv

read_completed_sample_ids

이미 저장된 part 파일을 읽어 완료된 sample_id를 set으로 만든다.

그래서 재개 시 이미 처리한 샘플은 다시 처리하지 않는다.

already completed: 450
skip existing: part_000000_000049.csv
skip existing: part_000050_000099.csv
...

merge_parts

저장된 모든 part 파일을 합쳐 전체 submission을 만든다.

추론 중간에는 다음과 같은 로그가 나온다.

merged rows: 8500 | missing: 8400

이 뜻은 전체 8,500개 중 아직 8,400개가 비어 있다는 의미다. 오류가 아니다.

마지막까지 완료되면 다음과 같이 바뀐다.

merged rows: 8500 | missing: 0
wrote: submissions/<RUN_ID>/submission.csv
wrote: runs/<RUN_ID>/raw_full.csv

run_resumable_inference

실제 추론 루프다.

흐름은 다음과 같다.

1. test.csv 로드
2. LIMIT 적용
3. 기존 part 파일 확인
4. shard 단위로 rows 분리
5. 이미 part가 있으면 skip
6. pending rows만 predict_batch로 추론
7. part CSV 저장
8. merge_parts 실행
9. 모든 shard가 끝나면 final submission 생성

여기서 predict_batch는 모델 adapter가 제공한다. 즉 09번 셀은 모델에 의존하지 않는다.

09번 셀
→ 공통 추론 엔진

10번 셀
→ Qwen 전용 predict_batch 정의

11번 셀
→ InternVL/GLM용 predict_batch 정의

10. Adapter A — Qwen/Qwen3.5-9B

이 셀은 E002/E004에서 사용하는 Qwen adapter다.

핵심 역할은 다음과 같다.

- AutoProcessor 로드
- AutoModelForImageTextToText 로드
- image processor 설정
- 4bit quantization 설정
- qwen_vl_utils.process_vision_info 사용
- Qwen chat template 적용
- predict_batch(rows) 정의

현재 T4나 L4 환경에서는 메모리 때문에 4bit quantization을 사용한다.

QWEN_LOAD_IN_4BIT = "auto"

이 설정은 GPU VRAM이 32GB 미만이면 자동으로 4bit를 사용한다.

T4 16GB
→ 4bit 사용

L4 24GB
→ 4bit 사용

A100 40GB
→ full dtype 가능

현재 실행 로그에서는 bitsandbytes 관련 warning이 나온다.

bitsandbytes FutureWarning

이는 실행 실패가 아니라 PyTorch 내부 경고이므로 무시해도 된다.

image+text message 구성

Qwen adapter는 각 row를 다음 메시지 구조로 바꾼다.

[
  {
    "role": "system",
    "content": [{"type": "text", "text": SYSTEM_PROMPT}],
  },
  {
    "role": "user",
    "content": [
      {"type": "image", "image": image_obj},
      {"type": "text", "text": build_user_text(...)},
    ],
  },
]

즉 모든 주요 실험은 image+text 기반으로 돌아간다.

predict_batch

predict_batch(rows)는 다음을 반환한다.

preds: List[int]
raws: List[str]

preds는 최종 label이고, raws는 모델이 생성한 원문이다.

이 raw output은 나중에 raw_full.csv에 저장된다.

runs/<RUN_ID>/raw_full.csv

11. Adapter B — InternVL / GLM generic adapter

이 셀은 E006/E008을 위한 generic adapter다.

현재 우선순위는 E002/E004이므로 아직 핵심 실험에는 사용하지 않았다.

계획은 다음과 같다.

E006 InternVL3.5-8B / image+text / safe_generic_v0 / zero-shot
E008 GLM-4.1V-9B / image+text / safe_generic_v0 / zero-shot

단, InternVL과 GLM은 Qwen과 input template이 다를 수 있으므로, generic adapter가 바로 동작하지 않으면 노트북 전체를 바꾸지 않고 이 셀의 predict_batch만 수정한다.

12. Smoke inference / Full inference

이 셀은 실제로 추론을 시작한다.

merged = run_resumable_inference()
display(merged.head())

현재 full run에서는 다음과 같은 로그가 나온다.

rows=8500 | RUN_ID=E002_qwen35_9b_safe_generic_v0_zero_shot_imgtxt_full | MODEL_ID=Qwen/Qwen3.5-9B
LIMIT=0 | BATCH_SIZE=2 | SHARD_SIZE=50
PART_DIR: /content/drive/MyDrive/dacon_multimodal_bias/runs/E002_qwen35_9b_safe_generic_v0_zero_shot_imgtxt_full/parts
already completed: 450
skip existing: part_000000_000049.csv
skip existing: part_000050_000099.csv
...
shard 450:500 | pending=50

이 로그는 resume이 정상 작동하고 있다는 뜻이다.

already completed: 450
→ 이미 450개 완료

skip existing
→ 기존 part 파일은 다시 계산하지 않음

shard 450:500
→ 450번째부터 재개

13. Output validation

이 셀은 최종 output이 제출 가능한 형식인지 확인한다.

검증 항목은 다음과 같다.

- row 수가 맞는가
- sample_id 순서가 test.csv와 같은가
- label 값이 0, 1, 2만 있는가
- NaN이 없는가
- raw_full.csv도 같이 존재하는가

full run이 끝나면 다음을 확인한다.

sub = pd.read_csv(OUT_PATH)
print(sub.shape)
print(sub["label"].value_counts().sort_index().to_dict())
display(sub.head())

정상 제출 파일 조건은 다음이다.

rows = 8500
columns = sample_id, label
label ∈ {0, 1, 2}
NaN 없음
sample_id 순서가 test.csv와 동일

14. Full submission guard

이 셀은 현재 run이 진짜 full submission 후보인지 확인한다.

LIMIT=0
→ full submission 후보

LIMIT=100
→ smoke run, 제출 후보 아님

따라서 smoke run에서 생성된 submission.csv를 실수로 제출하지 않도록 guard를 둔다.

15. Run status check after Colab restart

Colab이 죽은 뒤 다시 켰을 때 진행 상태를 확인하는 셀이다.

자주 사용하는 확인 코드는 다음과 같다.

print(RUN_ID)
print(PART_DIR)
print("parts:", len(list(PART_DIR.glob("part_*.csv"))))

또는 완료된 row 수를 직접 합산할 수도 있다.

import pandas as pd

n = sum(
    len(pd.read_csv(p))
    for p in PART_DIR.glob("part_*.csv")
)

print(n, "/ 8500")

현재는 450개가 완료된 상태로 확인되었다.

16. Memory cleanup between models

다른 모델로 넘어가기 전 GPU memory를 비우는 셀이다.

del model
torch.cuda.empty_cache()

E002와 E004는 같은 Qwen 모델을 사용하고 prompt만 다르므로, 모델을 다시 로드하지 않고 재사용할 수 있다.

하지만 Qwen에서 InternVL 또는 GLM으로 넘어갈 때는 이 셀을 실행하는 것이 안전하다.

17. QLoRA/SFT training placeholder

E009~E011은 아직 실행하지 않는다.

이유는 다음이 아직 준비되지 않았기 때문이다.

- processed train.csv
- processed dev.csv
- ambiguous/disambiguated metadata
- private-like validation set
- QLoRA adapter path
- 내부 Balanced Accuracy 평가 코드

QLoRA/SFT는 Public Score를 더 올리기 위한 단계가 아니라, Private-like validation에서 zero-shot baseline을 이기기 위한 단계다.

앞으로 train data를 구성할 때는 다음 비율을 목표로 한다.

40% private-like synthetic visual bias QA
20% ambiguous/disambiguated text-bias QA
15% OCR / text-in-image synthetic QA
15% public VQA / commonsense QA
10% robustness augmentation / counterfactual pairs

Public이 오픈 벤치셋 기반이라는 점을 고려하면, open benchmark 데이터만 많이 모으는 것은 위험하다곤 생각한다.

실제로 모델이 운영진 자체 제작 샘플에서 잘 일반화하려면, 다양한 유형의 데이터를 섞어서 학습하는 것이 좋을 것이다.

현재 실행 루틴

E002 full을 재개할 때의 최종 루틴은 다음이다.

1. 00 dependency 설치
2. 01 Global settings 확인
3. 02 Drive mount
4. 04 Experiment plan registry
5. 05 Prompt registry
6. 06 Activate experiment
7. 08 Common parser
8. 09 Resumable harness
9. 10 Qwen Adapter A
10. 12 run_resumable_inference

생략하는 셀은 다음이다.

03 DACON test schema validation
07 Multimodal active-run validation

이 둘은 최초 검증용이다. 데이터가 바뀌지 않았으면 매번 실행하지 않는다.

끝난 뒤 해야 할 일

E002 full이 완료되면 다음을 확인한다.

sub = pd.read_csv(OUT_PATH)
print(sub.shape)
print(sub["label"].value_counts().sort_index().to_dict())
display(sub.head())

그 다음 public 제출 후보로 기록한다.

submission path:
/content/drive/MyDrive/dacon_multimodal_bias/submissions/E002_qwen35_9b_safe_generic_v0_zero_shot_imgtxt_full/submission.csv

Public Score는 참고 지표로만 기록한다.

중요한 것은 이후에 만들 private-like validation에서의 성능이다.

Public Score
→ open benchmark 대응력 참고

Private-like dev Balanced Accuracy
→ 실제 의사결정 기준

다음 단계

E002 full이 끝나면 다음 순서로 진행한다.

1. E002 submission 제출 및 public score 기록
2. label distribution 확인
3. raw_full.csv에서 parse failure나 이상 응답 확인
4. E004 smoke 또는 full 실행 여부 판단
5. InternVL/GLM adapter 점검
6. rclone 기반 train source 수집 재개
7. build_data.py 구현
8. private-like validation set 생성
9. QLoRA/SFT 실험 E009~E011 진행

이번 단계의 핵심은 모델 성능 자체보다 실험 운영 안정성이다.

이제 Colab이 죽어도 처음부터 다시 하지 않는다.
모델이 바뀌어도 하네스는 그대로 쓴다.
Prompt가 바뀌어도 run ID와 part directory가 분리된다.
Public Score는 참고만 하고, 최종적으로는 private-like validation을 만들어야 한다.

이 구조가 잡혔으니 다음부터는 모델 교체와 데이터 구축에 집중할 수 있다.

마치며

이번 글에서는 DACON 멀티모달 바이어스 대회 준비를 위한 Colab 추론 실험 하네스 설계와 코드 해설을 다뤘다.

핵심 목표는 Colab 런타임이 죽어도 처음부터 다시 하지 않는 구조를 만드는 것이었다.

코드는 다시 공부해서 개인적으로 해석한 내용과 최적화 된 부분을 올릴 예정이지만, 전체적인 아이디어와 구조는 이번 글에서 충분히 설명했다고 생각한다.

결과값은 첫번째 글에 정리할 예정이다. 결과를 먼저 보여주고, 그 다음 과정을 보여주는 흐름이 더 좋을 것 같다고 생각한다.

data_automation_multimodal_bias 2 / 2

목차

댓글