Portfolio — Data / ML Engineer

강병수

이력서에 담지 못한 기술적 깊이를 정리한 프로젝트별 상세 포트폴리오입니다. 각 프로젝트에서 무엇을 시도했고, 왜 실패했고, 어떤 결정이 성능을 바꿨는지를 기록합니다.

4학년 1학기 재학 중 · 잔여 2학기 · 취업계 제출 시 입사 가능일 2026년 6월 23일
Silver Medal

CSIRO — Image2Biomass Prediction

초지(grassland) 이미지에서 식물 Biomass 양을 예측하는 회귀(Regression) 문제. Silver Medal을 수상했습니다.

문제 정의 — EDA에서 발견한 핵심 제약

가장 먼저 확인한 건 test set에 State, Species, NDVI, Height가 없다는 점이었습니다. 멀티모달로 설계하더라도 결국 inference는 image-only여야 했고, 이 제약이 이후 모든 설계 결정의 기준이 됐습니다.

추가로 Dry_Clover_g의 37.8%가 0 — zero-inflated target이라 단순 MSE 학습만으로는 clover 예측이 무너질 수 있었습니다.

모델 진화

초기엔 EfficientNet-B0/B4로 baseline을 잡고, 이후 DINOv2, 최종적으로 DINOv3 ViT-Large를 주력 backbone으로 정착시켰습니다.

  • 초기: EfficientNet-B0 / B4 — Baseline 및 초기 실험
  • 중반: DINOv2 — Self-supervised 전환 시도
  • 최종: DINOv3 ViT-Large — 주력 backbone
  • 비교: ConvNeXt-Base (cv5) — CNN 계열 앙상블용 다양성 확보
대표 아키텍처 — DINOv3 Dual-View

긴 quadrat 이미지를 좌우로 반 분할해 DINOv3 ViT-Large에 각각 통과시키고, FiLM(Feature-wise Linear Modulation)으로 양쪽 context를 섞는 방식입니다. 이미지를 하나로 줄여 넣는 것보다 공간 구조를 보존할 수 있었고, self-supervised 표현력을 최대한 유지하면서 회귀 성능을 끌어올릴 수 있었습니다.

Grassland quadrat sample — CSIRO biomass prediction
View L View R

실제 경진대회 데이터 샘플. 가로로 긴 quadrat 이미지를 중앙에서 좌/우로 분할해 각각 ViT-Large에 입력합니다.

Fine-tuning 전략 비교
v22Frozen backbone — head만 학습 (linear probe-like)
cv6Frozen + SSF adapter — backbone 고정, adapter만 학습
cv8Partial unfreeze + LLRD — 레이어별 LR decay
cv7 ✓Full fine-tune — backbone_lr_mult=0.084, pretrained weight 보호하며 전체 미세조정
v24TENT-style — inference 중 LayerNorm만 업데이트

→ Full fine-tune + 작은 backbone LR이 최종 best. "완전 동결"은 도메인 특화 패턴을 못 잡고, "균일 LR"은 pretrained feature를 덮어썼습니다.

전처리 & 증강

전처리 — 이미지를 가로 중앙에서 좌우 반 분할 → Resize(512→560) → Dual-view 입력 → ImageNet Normalize

증강 — RandomHorizontalFlip, RandomVerticalFlip, ColorJitter (hue=0.02로 축소 — 크게 두면 초록/갈색 식생 색상 붕괴)

TTA — Original + HFlip + VFlip (3x) / v24: TENT-style, augmentation 예측의 variance를 줄이는 방향으로 LayerNorm 업데이트

가장 큰 Breakthrough

v17에서 Optuna로 hyperparameter를 최적화했더니 CV 점수는 크게 올랐지만, LB와 gap이 벌어졌습니다. 분석 결과 "모델이 부족한 게 아니라 validation 설계에 leakage가 있었다"는 결론에 도달했습니다.

Sampling_Date 그룹 기반 CV로 전환 — 같은 날 촬영된 이미지들이 train/val에 섞이지 않도록 fold를 재구성했습니다.

"더 좋은 모델"을 찾기 전에 "제대로 된 측정"이 먼저였습니다.
cv7 — 최종 Refinement
  • Full-target weighted loss — 실제 평가 metric과 align된 loss로 학습
  • OOF 저장 + Ridge calibration — 예측 분포를 보정해 test 시 안정성 향상
  • Sampling_Date fold + OOF 체계 — 실험 전반을 재현 가능하게 정리
실패 사례 & 배운 것
  • CV3 — bottom crop + timestamp 제거 → 오히려 성능 하락
  • v13/v14 — 강한 증강(TrivialAugmentWide, MixUp) → CV는 올랐지만 LB gap 증가
  • v17 Optuna — "CV 착시"를 만든 주범. validation 설계가 모델 복잡도보다 중요
  • v15 "Back to Basics" — 복잡한 실험이 안 먹힐 때 단순한 baseline의 가치

시도한 것들 — Physics constraints, Teacher-student distillation, Pseudo-labeling, Auxiliary task (NDVI/Height), Multi-backbone ensemble, Full-frame + SSF adapter, LLRD partial unfreeze, TENT inference adaptation

진행중

Translate Akkadian to English

약 4,000년 전 점토판에 기록된 구 아시리아 방언(Old Assyrian) 아카드어 음역을 영어로 번역하는 Seq2Seq NMT 문제. 평가 지표는 Geometric Mean(BLEU-4, chrF++)입니다.

왜 이 문제가 어려운가
  • 초저자원(Ultra-low resource) — Gold 학습 데이터가 1,561쌍에 불과
  • 특수문자 범람 — š, ṣ, ṭ, ḫ, ā 같은 diacritic, 첨자 숫자, OCR artifact → 서브워드 tokenizer에서 OOV 폭발
  • Formulaic 패턴 — 전체의 60~70%가 반복 공식구. "um-ma X-ma a-na Y-ma""Thus says X, to Y:"
  • 문서-문장 불일치 — 학습은 문서 단위, 테스트는 문장 단위 → domain gap
Old Assyrian cuneiform tablet

약 4,000년 전 구 아시리아 점토판. 이 텍스트의 음역을 영어로 번역하는 것이 문제의 목표입니다.

왜 ByT5인가

ByT5(Byte-level T5)를 선택한 가장 큰 이유는 byte-level 처리입니다. 아카드어 음역의 diacritic과 OCR artifact를 서브워드 vocab으로 처리하면 OOV가 폭발하지만, byte-level tokenizer는 어떤 문자도 1~3 byte로 안전하게 처리합니다.

V4-ASCII 정규화 — š→s, ṣ→s, ṭ→t, ḫ→h 등 diacritic을 완전 제거해 더 단순한 byte 시퀀스로 변환. 학습이 안정화됐습니다.
버전 진화 — 18번의 실험
V1 → LB 0.0 — 초기 실패, 기반 구축
문서 레벨 vs 문장 레벨 도메인 갭으로 !!!! 반복 출력. 체크포인트 경로, 토크나이저 메타 불일치도 겹쳤습니다.
→ 반드시 문장 레벨로 학습, 체크포인트 로딩 검증 필수
V2 → LB 11.8 — 첫 유의미한 Breakthrough
문서 단위를 문장 단위 문제로 재정의하고, V4-ASCII 정규화 도입. FP32 + LR=1e-4 + gradient clipping.
→ 데이터 단위를 잘못 정의하면 모델이 아무리 좋아도 무의미
V7~V8 → LB 22.9 — 데이터 확장
  • First-word anchor 기법 — Gold 1,561쌍 → 7,234쌍으로 확장
  • ByT5-small → ByT5-base(581M) 스케일업
  • Gold 데이터 3× upsample + OCR noise augmentation
→ 더 큰 모델보다 더 많은 올바른 데이터가 먼저
V9 → LB 13.2 — CAD 사전 추가 실패
Chicago Assyrian Dictionary 7,133쌍 추가 → dictionary 스타일 출력에 편향. 문장 번역이 아닌 단어 번역만 출력.
→ 데이터 양보다 도메인 일치가 중요. CAD 영구 금지.
V11 → LB 25.1 — 현재 최고점
  • 학습 데이터 10,821행 — new_segmented + Gold 3× + AICC pseudo-label + train_segmented
  • TM(Translation Memory) — TF-IDF char 2-4gram, sim≥0.88이면 hard copy
  • MBR 기초 — ~10 후보 Scoring으로 최적 선택
V16~V17 → LB 24.1 / 24.8 — MBR 고도화 시도
Segment-Level TM(V16), Sampling+MBR 17후보(V17) — 후보 품질 하락과 "군중 심리" 문제로 V11을 넘지 못했습니다.
V18 → LB 6.8 — TM Self-Reference 함정
TM top-k가 C와 R 양쪽에 들어가며 chrF=1.0 × 5×가중치로 MBR 장악 → 독일어 출력. 수정 완료: TM을 C에서 제거, R에만 유지.
Akkadian 버전별 LB 스코어 추이 차트

버전별 Leaderboard 스코어 추이 — V1(0.0) → V11(25.1). 주요 전환점마다 설계 결정이 성능을 바꿨습니다.

ByT5 shared embedding 버그
발견한 버그: tie_word_embeddings=True에서 save_pretrainedlm_head.weight를 safetensors에 저장하지 않음 → 수동 load_state_dict 로딩 시 lm_head 랜덤 초기화 → EOS 즉시 출력. 해결: AutoModelForSeq2SeqLM.from_pretrained(local_files_only=True) 강제.
발견한 기술 인사이트
  • MBR C/R 분리 원칙 — Candidate(C)과 Reference(R)을 반드시 분리. sampling을 C에 넣으면 "다수결"로 TM을 무시하는 군중 심리 발생
  • TM self-reference 함정 — 같은 항목이 C와 R에 동시에 있으면 chrF=1.0 가중치 증폭으로 MBR 장악
  • 도메인 오염 — CAD(V9), German 데이터(V18) — "데이터 추가가 독이 됐던" 사례. 도메인 일치 > 데이터 양
  • BLEU vs chrF++ 불균형 — chrF++≈42 준수하지만 BLEU≈12가 발목. BLEU 개선 = 정확한 n-gram = TM coverage 확대
이 프로젝트의 병목은 모델 크기가 아니었습니다. 데이터 재구성(문서→문장), 정규화 일치(학습-추론), exact phrasing coverage(TM) — 이 세 가지가 성능을 결정했습니다. ByT5-large가 base보다 낮고, Gemma 4B가 ByT5-base보다 낮은 것이 그 증거입니다.
Diffusion LM 탐색 (진행중)

AR 방식의 한계("초반에 phrasing을 틀리면 끝까지 그대로")를 진단하고, MDLM(Masked Diffusion Language Model) 기반 ByT5 refinement를 탐색 중입니다. 파이프라인 구현이 상당히 진행됐으나, 성능 검증은 아직 진행 중입니다.

GraphRAG

Law RAG Agent

형법 조문과 판례를 연결하는 그래프 기반 하이브리드 RAG 시스템. 단순 벡터 유사도를 넘어 법조문 간 인용/참조 관계를 retrieval signal로 활용합니다.

Neo4j text-embedding-3-small gpt-4o-mini PyPDFLoader AIHub
왜 이 문제를 만들었나

법률 QA에서 순수 벡터 검색은 "의미적으로 비슷한 문서"를 가져올 뿐, 법적으로 연결된 문서를 가져오지 못합니다.

벡터 검색

판례 92도2540(정당방위 관련 사례)이 top-1 — 간접적 참고 자료

그래프 검색

제21조(정당방위) 조문이 직접 surface — 법적 정의의 출처

데이터 구축 & Chunk Granularity

형법 조문 — 법제처 PDF → PyPDFLoader → 361개 조문(Article 노드). 초기 장(章) 단위(47개)에서 조문 단위(361개)로 전환했습니다.

  • 장 단위(47) → 검색 결과가 너무 넓어 무관한 조문 포함
  • 조문 단위(361) → 법적 개념과 chunk 경계 일치 → 정확도 향상

판례 — AIHub 5,404개 중 3,000건 샘플링해 Precedent 노드 구성.

그래프 스키마 — Neo4j

FAISS/Chroma 대신 Neo4j를 선택한 이유: 조문 간 인용 관계, 판례-조문 참조, 키워드를 엣지로 표현해야 했기 때문입니다.

  • 노드: Article (형법 조문) · Precedent (판례) · Keyword (법률 핵심어)
  • 엣지: REFERENCES_ARTICLE (판례→조문) · REFERENCES_CASE (조문→판례) · HAS_KEYWORD
Law RAG 그래프 스키마 다이어그램 — Neo4j

Neo4j 그래프 스키마. Article·Precedent·Keyword 노드와 인용/참조 엣지로 법률 구조를 표현합니다.

Hybrid GraphRAG — 4가지 검색 경로
  1. Direct Article Lookup — 조문 번호 정규식 추출 → 직접 lookup
  2. Keyword SearchHAS_KEYWORD 엣지를 통한 개념 기반 검색
  3. Article Vector Search — 조문 embedding 기반 cosine similarity
  4. Precedent Vector + Graph Traversal — 판례 → REFERENCES_ARTICLE 엣지 → 조문 역추적

각 경로의 결과를 점수 기반으로 통합해 최종 context를 구성하고, gpt-4o-mini에 전달합니다.

"semantic similarity보다 citation/reference가 더 강한 retrieval signal이었습니다."
판례가 어떤 조문을 인용하는지, 조문이 어떤 사건에서 적용됐는지 — 이 구조적 연결이 의미 벡터보다 법률 QA에서 더 정확한 맥락을 제공했습니다.
한계 & 개선 방향
  • Relation 설정 미흡 — 일부 인용 관계 누락으로 graph traversal coverage 제한
  • 파라미터 튜닝 부족 — 4가지 경로 가중치를 휴리스틱으로 고정

개선 방향: Bayesian Optimization(가중치 자동 탐색), RAFT(법률 도메인 fine-tuning), NodeRAG(정교한 그래프 활용)

AI Festival 본선

Finance Agent

LangGraph · LangChain 기반 국내 주식 정보 분석 AI 에이전트. HyperCLOVA X 파인튜닝 모델로 한국 투자 커뮤니티의 모호한 언어를 구조화된 질의로 번역합니다.

FastAPI LangGraph HyperCLOVA X yfinance FinanceDataReader pandas-ta Cloud Run
핵심 설계 철학

이 프로젝트의 핵심은 "LLM이 답을 직접 계산한다"가 아닙니다. 한국 주식 커뮤니티의 은어와 모호한 표현을 정량 조건으로 번역하는 데 LLM을 쓰고, 실제 계산은 deterministic Python 로직이 담당합니다.

"LLM으로 은어를 '이해'했다"가 아니라, 은어를 실행 가능한 파라미터로 '번역'했습니다. 이 설계 덕분에 이후 단계는 재현 가능하고, LLM hallucination이 계산 결과에 개입할 여지가 없습니다.
시스템 아키텍처 — LangGraph State Machine

analyze_intent → 조건부 분기 → 분석 노드 → generate_response 흐름의 라우터형 state machine입니다.

get_stock_price get_top_volume_stocks get_rsi_stocks get_moving_average_break get_bollinger_band_touch get_volume_change_stocks detect_moving_average_cross get_stocks_by_criteria get_market_index get_daily_news search_keyword_news +3 more

AgentState는 parsing 슬롯(intent, ticker, rsi_threshold, band_type 등)과 실행 결과 슬롯(volume_top_stocks, market_index_value, final_answer 등)으로 이원화했습니다.

HyperCLOVA X 파인튜닝 — Semantic Parser

한국 주식 자연어를 실행 가능한 라우팅 파라미터로 변환하는 semantic parser입니다. Instruction-style 데이터셋(data.csv)으로 학습했습니다. 의도 분석은 LLM + 규칙 보정 하이브리드 — 날짜 보정, 골든/데드크로스 키워드, 회사 별칭(삼전, 하이닉스, 엔솔)까지 반영합니다.

떡상등락률 +15% 이상
동전주현재가 1,000원 미만
거래량 터짐전일 대비 5배 이상
쩜상시가=고가=저가=상한가
눌림목5일선 아래 눌린 구간
물타기20%↓ 종목, 평단가 낮추기
바닥 반등저점 대비 5% 이상 상승
설거지고점 후 대량 장대 음봉
기술적 지표 — Deterministic Analyzer

모든 지표는 tools.py에서 Python으로 계산한 뒤 결과만 포매팅합니다. LLM이 숫자를 만들어낼 여지를 차단하는 의도적 설계입니다.

  • RSIta.rsi(length=16) via FinanceDataReader
  • 이동평균 돌파 — rolling mean 대비 10% 이상 높을 때 돌파 판정
  • 볼린저 밴드 — 20일 이평 ± 2σ, 상단/하단 터치 분기
  • 골든/데드크로스 — 5일/20일 rolling mean 교차 판정
  • 전 종목 스캔ThreadPoolExecutor 병렬화
데이터 소스
  • 단일 종목/지수 — yfinance (.KS/.KQ, ^KS11/^KQ11)
  • 전 종목 스크리닝 — FinanceDataReader
  • 기술적 지표 — pandas-ta + pandas rolling
  • 뉴스 — 네이버 금융 스크래핑 + 네이버 뉴스 검색 API

On-demand snapshot 방식 — 질문 시점에 최신 가용 데이터를 조회합니다.

한계 — 파서와 Analyzer의 정합성

파인튜닝 데이터셋은 MACD, 스토캐스틱, ADX까지 포함하지만, 실행 로직은 일부 미구현. 또한 generate_response_node는 코드 기준으로 템플릿 포매팅입니다 — LLM이 숫자를 만들어낼 여지를 차단하는 의도적 설계이지만, 자연어 응답 생성으로 발전할 여지가 있습니다.