LLM 분류가 틀리면 모델 탓일까 — 범인은 LLM 후처리의 정규식 188건

LLM정규식데이터파이프라인분류트러블슈팅

2분기 영업이익이 전년 대비 43.6% 줄었다는 공시가 어닝 서프라이즈로 분류돼 있었습니다.

LLM 분류 결과가 틀리면 프롬프트부터 고치고 싶어집니다. 그런데 원본 출력을 열어 보니 LLM은 정확했습니다.

event_tags        = {실적발표, 영업이익감소}
impact_direction  = '-'
confidence        = 0.950

범인은 그 뒤에서 태그를 카테고리로 접는 정규식 계층이었습니다.

규칙 배열을 위에서부터 훑어 처음 맞는 것을 반환하고, 태그가 여러 개면 태그 배열을 순서대로 돌며 처음 카테고리가 나오는 태그를 채택합니다.

그런데 방향이 없는 단어 실적발표가 EARNINGS_BEAT 패턴에 들어 있고 태그 배열에서 대체로 앞에 옵니다.

그래서 {실적발표, 영업이익감소}는 실적발표에서 BEAT로 확정되고 영업이익감소는 평가조차 되지 않습니다.

같은 경로로 188건이 뒤집혀 있었습니다.

고친 것은 예외 규칙 하나입니다. 명시적 악화 태그가 하나라도 있으면 배열 순서를 무시하고 MISS를 선점합니다.

export function normalizeTags(tags: string[]): SignalCategory | null {
  if (tags.some((tag) => normalizeTag(tag) === 'EARNINGS_MISS')) {
    return 'EARNINGS_MISS';
  }
  for (const tag of tags) {
    const category = normalizeTag(tag);
    if (category) return category;
  }
  return null;
}

실적발표를 BEAT 패턴에서 빼는 깨끗한 수정을 두고 왜 예외 규칙을 골랐는지, 188건이 어느 쪽에 얼마나 아팠는지가 아래에 있습니다.

왜 LLM이 아니었나?

이 시스템은 뉴스와 공시를 LLM에 넣어 이벤트 태그와 방향을 받습니다.

실적 악화가 서프라이즈로 나왔으니 출력이 잘못됐다고 보는 게 자연스럽고, 저도 프롬프트를 고칠 준비부터 했습니다.

그런데 원본 출력은 영업이익감소 태그에 방향 -, 확신도 0.95였습니다. 어느 쪽으로도 실적이 좋았다고 읽을 수 없는 출력입니다.

그러면 이걸 서프라이즈로 바꾼 건 누구일까요?

LLM 다음 단계입니다.

LLM이 주는 건 자유 문자열 태그이고, 이걸 시스템이 쓰는 고정 카테고리로 접는 계층이 따로 있습니다.

export function normalizeTag(tag: string): SignalCategory | null {
  const normalized = tag.toLowerCase().replace(/\s+/g, '');
  for (const rule of CATEGORY_RULES) {
    if (rule.pattern.test(normalized)) {
      return rule.category;
    }
  }
  return null;
}

규칙에서 실적 관련 부분만 떼어 보면 이렇습니다.

{ category: 'EARNINGS_BEAT',
  pattern: /실적발표|순이익증가|영업이익증가|매출증가|...|판매실적/ },

{ category: 'EARNINGS_MISS',
  pattern: /실적우려|순이익감소|영업이익감소|매출감소|...|손실확대/ },

실적발표에는 방향이 없습니다.

실적이 좋아도 발표하고 나빠도 발표합니다. 그런데 EARNINGS_BEAT 패턴 안에 들어 있고, LLM은 실적 관련 뉴스에 실적발표를 거의 항상 앞에 붙입니다.

방향을 가진 단어가 방향이 없는 단어에게 자리를 뺏깁니다. 배열에서 한 칸 뒤에 있다는 이유만으로요.

얼마나 뒤집혔나?

교정 전 교정 후
EARNINGS_BEAT 5,549 5,361
EARNINGS_MISS 126 323

188건이 BEAT에 잠겨 있었고, 교정 후 MISS 총계의 58%입니다.

절반 넘는 실적 악화 이벤트가 서프라이즈로 집계되고 있었다는 뜻이죠.

선점한 태그를 세어 보니 188건 전부가 실적발표 하나였습니다.

반대 방향의 오염은 작습니다. BEAT는 3.4%만 줄었습니다.

옮겨간 188건은 EARNINGS_BEAT 에서는 전체의 3.4% 라 보이지 않고, EARNINGS_MISS 에서는 전체의 58% 로 절반을 넘는다 EARNINGS_BEAT EARNINGS_MISS 교정 전 5,549건 중 교정 후 323건 중 3.4% 58% 점선 = 옮겨간 188건 같은 188건인데 한쪽은 반올림 오차고 다른 쪽은 절반이 넘는다
옮겨간 건수는 같습니다. 큰 쪽에서는 안 보이고 작은 쪽에서는 절반을 넘습니다.

큰 쪽에서 작은 쪽으로 옮겨가는 구조라 작은 카테고리만 심하게 망가집니다.

비율로 보면 MISS는 156% 늘었고 BEAT는 3.4% 줄었습니다. 같은 188건인데 한쪽에서는 반올림 오차고 다른 쪽에서는 데이터의 절반입니다.

세는 방법에 따라 숫자가 달랐던 것도 적어 둡니다

“실적발표가 악화 태그보다 앞에 온 건”은 190건인데 “카테고리가 뒤집힌 건”은 188건입니다.

그 2건은 실적발표보다 더 앞에 다른 카테고리 태그가 있어서 애초에 BEAT도 아니었고, 다른 곳으로 잘못 분류돼 있었습니다.

first-match-wins에서는 버그가 다른 버그에 가려집니다. 증상으로 세면 놓치고, 규칙을 직접 돌려서 세야 보입니다.

왜 근본 해결을 하지 않았나?

가장 깨끗한 수정은 실적발표를 EARNINGS_BEAT 패턴에서 빼는 것입니다. 방향 없는 단어니까 방향 있는 카테고리에 있을 이유가 없습니다.

세어 보고 그만뒀습니다. 왜일까요?

다른 실적 태그 없이 실적발표만 달고 오는 이벤트가 1,646건이라, 이 단어를 빼면 그만큼이 통째로 미분류가 됩니다.

오분류 188건을 고치려고 1,646건을 버리는 셈입니다.

그래서 입증된 오분류만 좁게 고쳤습니다.

깨끗한 수정은 아니에요.

예외 규칙이 하나 생겼고, 실적발표 단독이 왜 아직 BEAT인지는 설명되지 않은 채 관찰 과제로 남았습니다.

다만 고칠 수 있는 것과 고쳐야 하는 것을 구분하지 않으면 1,646건을 잃습니다.

두 가지를 더 해야 했습니다

같은 정규화 로직이 TypeScript(런타임)와 Python(분석 스크립트) 두 벌이라 둘 다 고쳤습니다. 한쪽만 고치면 새 데이터는 맞고 과거를 판정하는 스크립트는 옛 정의로 계산합니다.

그리고 이 교정으로 표본 구성이 바뀌어, EARNINGS_MISS를 쓰던 진행 중인 검증 하나는 누적을 버리고 처음부터 다시 세기로 했습니다.

교정 전 MISS에 살아남은 건 LLM이 중립어를 먼저 내놓지 않은 이벤트뿐이라 선택 편향이 걸린 표본이었기 때문입니다.

남는 것

출력이 이상하면 출력을 만든 곳부터 의심하게 됩니다. 그런데 LLM과 사용처 사이에는 대개 후처리 계층이 있고, 그 계층은 조용합니다. 프롬프트를 고치기 전에 원본 출력을 한 번 열어보는 것으로 방향이 완전히 바뀌었습니다.

분류 규칙에 방향 없는 단어를 넣으면 방향이 사라집니다. 실적발표는 그 자체로 틀린 규칙이 아닙니다. 다만 방향을 가진 규칙 안에 있으면, 순서 하나로 반대 결론을 만듭니다.

작은 카테고리는 큰 카테고리의 반올림 오차로 망가집니다. 같은 188건이 한쪽에서는 3.4%고 다른 쪽에서는 58%입니다. 큰 쪽만 보고 있으면 영원히 안 보입니다.

자주 묻는 질문

규칙 순서를 바꾸면 되지 않나요

MISS 패턴을 BEAT 위로 올리면 이 사례는 맞습니다. 다만 순서 의존은 그대로 남아서 다음에 방향 없는 단어가 또 들어오면 같은 일이 반복됩니다. 예외 규칙은 순서와 무관하게 악화 태그를 선점하므로 그 의존을 한 자리에서 끊습니다.

정규식 대신 LLM 에게 카테고리까지 시키면 안 되나요

고정 카테고리를 LLM 이 직접 내면 값이 흔들리거든요. 같은 뉴스에 다른 카테고리가 나오면 과거 집계와 비교가 안 됩니다. 자유 태그는 LLM 이, 고정 카테고리는 결정론적 규칙이 맡는 지금 구조는 그래서 유지합니다.

왜 first-match-wins 를 쓰나요

태그 하나가 여러 카테고리에 걸릴 때 결정이 필요하고, 가장 단순한 결정이 순서입니다. 문제는 순서 자체가 아니라 방향 없는 단어가 방향 있는 규칙 안에 있었다는 것입니다.

과거 데이터는 다시 분류했나요

했습니다. 표의 교정 후 수치가 재분류 결과입니다. 다만 그 교정으로 표본 구성이 바뀌어서, EARNINGS_MISS 를 쓰던 검증 하나는 누적을 버리고 처음부터 다시 셉니다.


이 사이트의 수치는 주 1회 DB와 다시 대조하고, 판단이 바뀌면 지우지 않고 글 안에 덧붙입니다. 갱신은 RSS로 받을 수 있습니다.