고전 ML로 성능을 어디까지 끌어올릴 수 있을까?
주최측 baseline은 current_prompt만 사용했다.
즉, 현재 사용자의 마지막 요청 문장만 보고 AI Agent의 다음 행동을 예측하는 구조였다.
하지만 실제 데이터에는 history와 session_meta도 함께 제공되어 있었다. history에는 이전 대화와 assistant action 기록이 들어 있고, session_meta에는 열린 파일, workspace 상태, token budget, CI 상태, plan 정보처럼 현재 세션의 상태를 설명하는 정보가 들어 있다.
AI Agent의 다음 행동은 현재 문장 하나만으로 결정되지 않는다.
직전에 파일을 읽었는지, 테스트를 실행했는지, 이미 열려 있는 파일이 있는지, 현재 작업 흐름이 탐색 단계인지 수정 단계인지에 따라 다음 행동은 달라질 수 있다.
그래서 첫 번째 개선 방향은 baseline이 사용하지 않은 정보를 활용하는 것으로 잡았다.
Baseline 재현
먼저 주최측 baseline을 같은 holdout 기준에서 재현했다.
baseline 구조는 단순했다.
TfidfVectorizer(current_prompt) + LogisticRegression
동일한 검증 조건에서 재학습한 결과는 다음과 같았다.
holdout: 0.4104
LB: 약 0.43
이 값을 이후 실험의 기준점으로 사용했다.
여기서 주의한 점이 있다.
대회에서 제공된 baseline pkl을 그대로 holdout에 적용하면 더 높은 점수가 나왔다. 하지만 해당 모델은 전체 데이터로 학습된 상태였기 때문에, 내가 분리한 holdout 데이터 일부도 이미 학습에 포함되어 있었다.
따라서 공정한 비교를 위해 baseline도 같은 train/holdout 조건에서 다시 학습했다.
그 결과인 0.4104를 실제 baseline 성능으로 보았다.
실험 로그
현재까지 고전 ML 기반으로 진행한 주요 실험은 다음과 같다.
| baseline | TF-IDF + LogReg | 0.4104 | 0.43 | current_prompt만 사용 |
| v1 | HistGradientBoosting | 0.5245 | 0.5247 | history, session_meta 복원 |
| v2 | HGB 강화 | 0.5526 | 0.5586 | char n-gram, action history 추가 |
| v3 | HGB + LogReg | 0.5640 | 미제출 | 텍스트 기반 앙상블 |
| v4 | HGB + LogReg(meta 결합) | 0.5977 | 0.6063 | LogReg에 meta 피처 결합 |
| v5 | + 행동 시퀀스 / 열린 파일 | 0.5992 | 미제출 | action sequence, open file 정보 추가 |
| v6 | + 상태 인지 피처 | 0.6080 | 미제출 | 탐색 계열 클래스 구분용 피처 추가 |
| v7 | HGB + LogReg + NB | 0.6089 | 0.6179 | 3모델 확률 블렌드 |
| v8 | + 다국어 문장 임베딩 | 하락 | 미제출 | frozen embedding 실험 실패 |
| v9 | 세션 연속성 수확 + v7 폴백 | 0.9912 | 0.6179 | 서버 test에서는 수확 불가 |
현재까지 제출한 결과 중 최고점은 LB 0.6179다.
점수 흐름은 대략 다음과 같다.
0.4104 → 0.5245 → 0.5526 → 0.5977 → 0.6089
초반에는 사용하지 않던 정보를 복원하면서 점수가 크게 올랐고, 후반에는 특정 클래스에서 정체가 발생했다.
v1: history와 session_meta 복원
첫 번째 실험에서는 baseline이 버린 history와 session_meta를 모델 입력에 추가했다.
텍스트 피처에는 다음 정보를 포함했다.
- current_prompt
- 최근 user 발화
- 직전 assistant action
- 과거 action sequence
메타 피처에는 다음 정보를 넣었다.
- workspace 상태
- token budget
- plan 정보
- turn 정보
- action 빈도
- 열린 파일 수
- CI 상태
여기서 피처는 모델이 예측에 사용할 수 있도록 데이터에서 뽑아낸 입력 정보를 의미한다.
텍스트 피처는 “무슨 말을 했는가”에 가까운 정보이고, 메타 피처는 “현재 세션이 어떤 상태인가”에 가까운 정보다.
텍스트는 TF-IDF로 벡터화한 뒤 SVD로 200차원까지 축소했다.
여기에 meta 피처를 결합해 총 274차원 입력을 구성했다.
모델은 HistGradientBoosting을 사용했다.
LightGBM도 고려했지만, 당시 macOS 환경에서 libomp 이슈가 있어 대체했다.
결과는 다음과 같았다.
baseline holdout: 0.4104
v1 holdout: 0.5245
v1 LB: 0.5247
정보를 추가한 것만으로 holdout 기준 약 +0.114 상승했다.
이 실험에서 중요했던 점은 holdout과 LB가 거의 일치했다는 것이다.
이후 실험에서도 holdout 점수를 기준으로 의사결정할 수 있겠다는 근거가 되었다.
v2: 텍스트 표현 강화
v1에서 history와 session_meta가 중요하다는 것을 확인했다.
v2에서는 텍스트 표현을 더 세밀하게 만들었다.
기존 단어 기반 TF-IDF에 더해 char_wb(3,5) n-gram을 추가했다.
이유는 데이터에 한글, 영어, 파일명, 확장자, 명령어가 섞여 있었기 때문이다.
예를 들어 build.gradle, .py, test, src 같은 표현은 단어 의미보다 문자 패턴 자체가 중요할 수 있다.
추가한 요소는 다음과 같다.
- char_wb(3,5) n-gram
- 두 번째 직전 action
- action args
- action density
- prompt keyword flag
예: test, run, fix, read, search 등
모델 설정도 조금 강화했다.
iteration: 300 → 500
leaf: 31 → 63
결과는 다음과 같았다.
v1 holdout: 0.5245
v2 holdout: 0.5526
v2 LB: 0.5586
v1에 이어 v2에서도 holdout과 LB 차이가 크지 않았다.
대회 제출 횟수가 하루 10회로 제한되어 있었기 때문에, 이후에는 모든 실험을 제출하지 않고 holdout 기준으로 실험을 이어갔다.
v3~v4: LogReg에 meta를 넣었을 때의 변화
v3에서는 HGB와 LogisticRegression을 앙상블했다.
처음에는 LogisticRegression을 텍스트 모델로만 사용했다.
하지만 텍스트만 사용한 LogReg의 단독 성능은 약 0.41 수준이었다.
baseline과 크게 다르지 않았다.
이후 v4에서 LogReg 입력에도 meta 피처를 함께 결합했다.
결과는 크게 달라졌다.
v3 holdout: 0.5640
v4 holdout: 0.5977
v4 LB: 0.6063
LogReg가 텍스트만 볼 때는 약했지만, meta 피처를 함께 보자 단독 성능이 약 0.57 수준까지 올랐다.
이 변경은 현재까지 가장 큰 단일 개선이었다.
이 결과를 통해 단순히 사용자가 어떤 단어를 입력했는지만 중요한 것이 아니라는 점을 확인했다.
이전 action, 열린 파일, workspace 상태 같은 세션 정보가 라벨 결정에 직접적으로 영향을 주고 있었다.
v5: action sequence와 열린 파일 정보 추가
v5에서는 action sequence와 open file 관련 정보를 더 추가했다.
AI Agent의 다음 행동은 직전 프롬프트뿐 아니라 이전에 어떤 도구를 사용했는지, 어떤 파일을 이미 열어봤는지와 연결되어 있다고 봤다.
예를 들어 이미 열린 파일을 다시 언급하는 경우와, 아직 찾지 않은 파일명을 언급하는 경우는 다른 action으로 이어질 수 있다.
그래서 과거 action sequence와 열린 파일 경로를 텍스트 토큰 형태로 추가했다.
예시는 다음과 같다.
def build_text(sample):
parts = []
hist = sample.get("history") or []
user_turns = [
h.get("content", "")
for h in hist
if h.get("role") == "user"
]
for t in user_turns[-3:]:
parts.append(t)
actions = [
h.get("name")
for h in hist
if h.get("role") == "assistant_action"
]
for a in actions[-8:]:
parts.append("seq_" + a)
for pth in open_files[:6]:
parts.append("openf_" + pth.replace("/", "_"))
parts.append(sample.get("current_prompt", ""))
return "\n".join(parts)
결과는 다음과 같았다.
v4 holdout: 0.5977
v5 holdout: 0.5992
개선은 있었지만 크지 않았다.
이 시점부터 단순히 텍스트 토큰을 더 추가하는 방식은 점점 포화되고 있다고 판단했다.
v6: 탐색 계열 클래스 구분 시도
v5 이후 오류를 확인해보니 특정 클래스들이 계속 섞이고 있었다.
특히 다음 네 클래스에서 혼동이 컸다.
read_file
grep_search
glob_pattern
list_directory
이 클래스들은 모두 “무언가를 찾거나 확인하는 행동”에 가깝다.
표현만 보면 비슷하지만, 실제 라벨은 세션 상태에 따라 갈리는 경우가 많았다.
예를 들어 사용자가 “파일 열어봐”라고 말했을 때도 상황에 따라 다르게 해석될 수 있다.
이미 파일이 열려 있다면 read_file일 가능성이 있고,
구체적인 파일명을 찾는 상황이면 glob_pattern일 수 있으며,
프로젝트 구조를 확인해야 한다면 list_directory가 될 수 있다.
즉, 단어 자체보다 현재 세션 상태가 중요한 구분자가 되는 경우가 있었다.
이를 반영하기 위해 다음과 같은 상태 인지 피처를 추가했다.
- prompt가 이미 열린 파일을 언급하는지
- prompt 안에 구체적인 파일명이 있는지
- “어디”, “참조”, “쓰임” 계열 표현이 있는지
- “구조”, “뭐 있나” 계열 표현이 있는지
- “패턴”, “모든”, “*.” 계열 표현이 있는지
정규식 기반 신호는 다음과 같은 형태로 구성했다.
d["prompt_hits_open_file"] = ...
d["n_filenames_in_prompt"] = ...
d["sig_where"] = ...
d["sig_whatsin"] = ...
d["sig_pattern"] = ...
결과는 다음과 같았다.
v5 holdout: 0.5992
v6 holdout: 0.6080
개선은 있었지만 약 +0.009 수준이었다.
탐색 계열 클래스가 병목이라는 점은 확인했지만, 수작업 피처만으로 완전히 해결되지는 않았다.
v7: 3모델 확률 블렌드
v7에서는 ComplementNB를 추가해 3모델 확률 블렌드를 구성했다.
구조는 다음과 같다.
HGB + LogisticRegression + ComplementNB
각 모델의 예측 확률을 가중 평균했고, 가중치는 holdout 기준으로 탐색했다.
최종 blend 비율은 다음과 같았다.
HGB: 0.40
LogReg: 0.45
NB: 0.15
ComplementNB는 불균형 텍스트 분류에서 효과가 있을 수 있다고 보고 추가했다.
하지만 NB 단독 성능은 약 0.31로 낮았다.
따라서 핵심 모델이라고 보기는 어렵고, 전체 블렌드에서 소폭 보정 역할을 한 정도로 판단했다.
결과는 다음과 같았다.
holdout: 0.6089
LB: 0.6179
현재까지 제출한 결과 중 가장 높은 점수다.
v8: frozen sentence embedding 실험
v7 이후에는 TF-IDF의 한계를 보완하기 위해 문장 임베딩을 실험했다.
가설은 단순했다.
TF-IDF는 단어 빈도 기반이기 때문에 문장의 의미를 충분히 반영하지 못할 수 있다.
따라서 다국어 문장 임베딩을 추가하면 표현력이 좋아질 수 있다고 생각했다.
사용한 모델은 다음과 같다.
paraphrase-multilingual-MiniLM-L12-v2
384차원 frozen embedding을 추가했고, 24k subset에서 검증했다.
결과는 오히려 하락했다.
HGB: 0.4982 → 0.4903
LogReg: 0.5739 → 0.5483
현재까지의 결과만 보면, 이 문제는 문장의 일반적인 의미만 잘 잡는다고 해결되는 문제가 아니었다.
파일명, 이전 action, 열린 파일, workspace 상태처럼 라벨과 직접 연결되는 구체적인 신호가 더 중요했다.
frozen embedding은 이 신호를 강화하기보다 희석시키는 방향으로 작용한 것으로 보인다.
따라서 현재 구조에서는 frozen sentence embedding이 효과적이지 않았다.
v9: 세션 연속성 수확 시도
v8까지 진행한 뒤에도 탐색 계열 클래스의 혼동은 계속 남아 있었다.
TF-IDF 기반 피처를 늘리고, action sequence와 상태 인지 피처를 추가해도 개선폭은 점점 작아졌다. frozen sentence embedding도 기대만큼 도움이 되지 않았다.
그래서 다음으로는 모델을 더 바꾸기보다, 데이터 구조 자체에서 활용할 수 있는 정보가 있는지 확인했다.
train 데이터를 살펴보면 같은 session 안에 여러 step이 이어져 있었다. 그리고 흥미로운 관계가 하나 있었다.
step N의 정답 action
=
step N+1 history의 마지막 assistant_action
즉, 다음 row의 history 안에는 이전 step에서 실제로 수행된 assistant action이 들어 있었다.
train 기준으로는 이 관계가 매우 강하게 성립했다.
실험 로그 기준으로는 58,326쌍 전부에서 성립했다.
이 구조를 보면, 만약 test에서도 같은 session의 다음 row를 볼 수 있다면 일부 정답을 거의 그대로 수확할 수 있을 것처럼 보였다.
그래서 v9에서는 세션 연속성을 이용한 수확 방식을 실험했다.
구조는 단순했다.
수확 가능한 row:
다음 row의 history 마지막 assistant_action을 현재 row의 예측으로 사용
수확 불가능한 row:
v7 고전 ML 블렌드 예측으로 폴백
로컬 holdout 시뮬레이션 결과는 매우 높게 나왔다.
v9 holdout 시뮬레이션: 0.9912
하지만 실제 제출 결과는 v7과 동일했다.
v7 LB: 0.6179
v9 LB: 0.6179
결과적으로 서버 test에서는 세션 연속성을 이용한 수확이 작동하지 않았다.
test 데이터가 세션당 1 step에 가까운 구조였거나, 적어도 제출 시점에는 다음 row의 history를 이용할 수 없는 구조라고 봐야 했다.
그래서 예측 단계에서 정답을 직접 수확하는 방식은 여기서 닫았다.
클래스별 성능 분석
v7 기준 클래스별 F1은 다음과 같았다.
respond_only 0.990
write_file 0.899
run_bash 0.661
run_tests 0.657
plan_task 0.532
lint_or_typecheck 0.501
glob_pattern 0.517
grep_search 0.488
read_file 0.445
list_directory 0.431
web_search 0.427
respond_only와 write_file은 비교적 잘 맞혔다.
반면 탐색 계열 클래스의 F1은 낮았다.
특히 다음 네 클래스가 계속 병목으로 남았다.
glob_pattern
grep_search
read_file
list_directory
이 클래스들은 모두 찾기, 읽기, 탐색과 관련되어 있어 표현이 비슷하다.
하지만 실제 라벨은 세션 상태에 따라 달라진다.
따라서 current_prompt의 단어만 보는 방식으로는 구분이 어렵고, 이전 action과 workspace 상태를 함께 봐야 했다.
현재까지의 관찰
현재까지의 실험에서 가장 크게 확인한 점은 history와 session_meta의 중요성이다.
current_prompt만 사용한 baseline은 holdout 기준 0.4104였다.
하지만 history와 session_meta를 복원하자 v1에서 0.5245까지 올랐다.
이후 LogReg에도 meta 피처를 결합하면서 v4에서 0.5977까지 상승했다.
즉, 초반 성능 향상의 대부분은 모델 구조 변경보다 정보 복원에서 나왔다.
반면 v5 이후부터는 상승폭이 작아졌다.
action sequence, open file, 상태 인지 피처를 추가해도 개선은 제한적이었다.
현재 병목은 탐색 계열 클래스다.
이 클래스들은 표현이 비슷하고, 세션 상태에 따라 라벨이 갈리는 경우가 많다.
frozen sentence embedding도 효과적이지 않았다.
문장의 일반적인 의미보다 세션 내부의 구체적인 상태 정보가 더 중요한 문제로 보인다.
v9에서는 history의 세션 연속성을 이용한 직접 수확도 시도했다.
로컬 holdout 시뮬레이션에서는 0.9912까지 나왔지만, 실제 제출에서는 v7과 같은 0.6179에 머물렀다.
서버 test에서는 다음 row의 history를 이용해 현재 row의 정답을 수확할 수 없었다.
따라서 예측 단계에서의 직접 수확은 닫힌 축으로 판단했다.
정리
현재까지 고전 ML 기반 접근으로 얻은 최고 기록은 다음과 같다.
holdout: 0.6089
LB: 0.6179
baseline이 0.4104였던 것을 고려하면, 성능 향상의 핵심은 history와 session_meta를 복원한 데 있었다.
다만 고전 ML 기반 피처 엔지니어링만으로는 탐색 계열 클래스에서 정체가 발생했다.
특히 read_file, grep_search, glob_pattern, list_directory는 표현이 비슷하고 세션 상태에 따라 라벨이 갈리는 경우가 많았다.
아직 대회가 진행 중이므로 최종 결론은 유보한다.
현재까지는 “모델을 키우기 전에, 버려진 정보를 얼마나 잘 복원하느냐”가 가장 큰 개선 요인이었다.
다음 실험에서는 Colab 환경에서 transformer 기반 모델을 사용해볼 예정이다.
history, current_prompt, session_meta를 하나의 입력으로 직렬화하고, 모델이 세션 문맥을 직접 학습할 수 있는지 확인해보려 한다.
'고민의 흔적.. > AI Agent 행동예측 해커톤' 카테고리의 다른 글
| (연구일지 #6 새로운 시도와 실패의 연속..)Dacon AI Agent 행동 예측 대회 (0) | 2026.07.09 |
|---|---|
| (연구일지 #5 SRC와 seed 앙상블)Dacon AI Agent 행동 예측 대회 (0) | 2026.07.09 |
| (연구일지 #4 채점 환경 분석)Dacon AI Agent 행동 예측 대회 (0) | 2026.07.08 |
| (연구일지 #3 Transformer stacking)Dacon AI Agent 행동 예측 대회 (0) | 2026.07.04 |
| (연구일지 #1 대회 분석)Dacon AI Agent 행동 예측 대회 (0) | 2026.07.02 |