Node.js를 넘어선 이미지 업로드 모더레이션: 멀티모달 채팅(Multimodal Chat)을 이용한 NSFW 및 폭력성 분류
요약
멀티모달 채팅 모델을 활용하여 이미지 업로드 시 NSFW 및 폭력성을 분류하는 설계 전략을 다룹니다. JSON 스키마를 통한 구조화된 응답과 검증, 그리고 비용 효율적인 평가 프로세스 구축의 중요성을 강조합니다.
핵심 포인트
- 설명 가능한 라벨을 위해 엄격한 JSON 스키마와 멀티모달 모델 활용 권장
- 단순 정확도보다 미탐(False Negatives)과 오탐(False Positives) 관리에 집중
- 정책 변경 시 재처리를 위해 모델의 원본 결정(raw decisions) 보관 필요
- 구조화된 응답 검증 실패 및 재시도에 따른 토큰 비용 상승 고려 필수
업로드된 이미지에 대해 정책상 설명 가능한 라벨(explainable labels)이 필요한 경우, 엄격한 JSON 스키마(JSON schema)와 함께 멀티모달 채팅(multimodal chat)을 사용하세요. 그렇지 않다면 관리형 고정 분류 체계(fixed-taxonomy) 서비스를 사용하는 것이 좋습니다. 여기에는 전용 이미지 모더레이션(image moderation) 엔드포인트가 없으므로, 실질적인 설계 방식은 정책 프롬프트(policy prompt), 시각 능력을 갖춘 채팅 모델(vision-capable chat model), 스키마 검증(schema validation), 그리고 보수적인 폴백(fallback) 전략을 구성하는 것입니다.
이것이 저의 짧은 답변입니다. 저는 모델의 산문(prose)을 허용/차단(allow/block) 결정에 직접적으로 사용하지는 않습니다. 감사(audit)를 위해 원래의 결정을 보관하고, 이를 작은 내부 상태(internal status)로 변환하며, 평가 세트(eval set)를 출시 게이트(release gate)로 삼습니다. 모델은 정책 시스템의 한 구성 요소일 뿐, 정책 시스템 그 자체는 아닙니다.
Python 이미지 업로드 모더레이션 예제는 NSFW 및 폭력성에 대해 무엇을 분류해야 하는가?
카테고리는 앱의 실제 규칙에서 가져와야 합니다. 일반적인 사용자 콘텐츠 제품의 경우, 저는 누드(nudity), 그래픽 폭력(graphic violence), 혐오 상징(hate symbols), 약물(drugs), 미성년자 위험(minors-risk)부터 시작합니다. 저는 이러한 라벨이 보편적이라고 가정하지 않습니다. 의료 포럼과 마켓플레이스는 서로 다른 임계값(thresholds)이 필요하며, 역사적 아카이브는 프로필 사진 제품이 거부해야 할 상징을 정당하게 보여줄 수도 있습니다.
저의 첫 번째 노트북(notebook) 작업은 의도적으로 지루하게 진행합니다. 허용된 사진, 차단된 사진, 모호한 사진의 작은 세트를 구성하고, 예상되는 카테고리 라벨을 작성하며, 정책 이유를 평이한 영어로 기록합니다. 그런 다음 모든 후보 모델에 대해 동일한 프롬프트와 스키마를 실행합니다. 제가 가장 먼저 신경 쓰는 점수는 차단 세트에서의 미탐(false negatives)이며, 그다음은 무해한 업로드에 대한 오탐(false positives)입니다. 전체 정확도(overall accuracy)는 이 두 가지를 모두 숨길 수 있습니다.
이 지점이 바로 JSON 스키마(JSON schema)가 제 역할을 하는 곳입니다. "graphic_violence": "high"를 포함하는 응답은 검증, 저장 및 비교가 가능합니다. “이것은 우려스러워 보입니다”와 같은 문단은 큐(queue)나 이의 신청(appeal)을 안정적으로 유도할 수 없습니다. 제공자(provider)의 응답을 allow, review, 또는 block과 같은 정규화된 상태(normalized status)와 함께 보관하세요. 정책이 변경될 때, 모든 오래된 기록을 마이그레이션하지 않고도 원본 결정(raw decisions)을 다시 재생(replay)할 수 있습니다.
저는 비용 측면의 문제를 아주 고통스러운 방식으로 배웠습니다. 하나의 평가 실행(evaluation run)이 1,870만 개의 입력 토큰(input tokens)을 소비했는데, 이는 제 예상치의 약 3.4배였습니다. 모든 크롭(crop)과 재시도(retry)마다 전체 정책 루브릭(policy rubric)을 반복했기 때문입니다. 제 노트북에서는 케이스당 합리적인 추정치가 나타났지만, 프로덕션 형태의 하네스(harness)는 각 소스를 여러 변형(variants)으로 확장한 뒤, 구조화된 응답(structured response)이 검증(validation)에 실패한 케이스들을 재시도했습니다. 저는 깔끔한 경로를 측정했고, 지저분한 경로를 위해 예산을 책정했습니다. 실행을 중단하고 피스처(fixture)와 시도(attempt)별로 사용량을 그룹화해 보니, 가장 큰 이미지가 주범이 아니라는 것을 발견했습니다. 확장된 케이스들에 걸쳐 중복된 정책 텍스트가 문제였습니다. 해결책은 추측이 아닌 측정에 있었습니다. 저는 프롬프트 토큰(prompt tokens)을 일급 평가 컬럼(first-class eval column)으로 만들었고, 전송 전에 이미지 변형들을 중복 제거(deduplicate)했으며, 요청당 비용(cost per request) 대신 수락된 결정당 비용(cost per accepted decision)을 보고했습니다. 마지막 분모가 중요한 이유는, 수동 검토(manual review)로 넘어가는 저렴한 응답은 작업을 완료한 것이 아니기 때문입니다. 또한 실험에 배치(batch) 수준의 상한선(ceiling)을 설정하여, 잘못된 승수(multiplier)가 발생하더라도 하루가 끝날 때쯤 놀라운 결과로 나타나는 대신 조기에 중단되도록 했습니다. 이제 저는 대규모 실행 전에 프롬프트 토큰을 계산하고, 평균뿐만 아니라 분포(distribution)를 검사합니다.
작은 배치부터 시작하세요.
산문 파싱(prose parsing)은 하지 마세요.
집중된 구현 (The focused implementation)
아래 예제는 로컬 이미지 하나를 검증된 POST /v1/chat/completions 경로로 전송합니다. 프로그램이 독립적으로 실행될 수 있도록 데이터 URL(data URL)을 사용하며, API 키와 비전 모델 ID(vision model ID)를 환경 변수에서 가져옵니다. 또한 구조화된 JSON을 요구하며, Retry-After를 준수하면서 속도 제한(rate limits)에 대해 재시도합니다. 이 글은 프레임워크 선택이 아닌 정책 경계(policy boundary)에 관한 것이므로 표준 라이브러리 HTTP 클라이언트를 사용합니다.
import base64
import json
import mimetypes
...
INFRAI_API_KEY를 설정하고, INFRAI_VISION_MODEL을 라이브 모델 카탈로그(live model catalog)에서 현재 사용 가능한 멀티모달 모델(multimodal model)로 설정한 다음, 이미지 경로와 함께 파일을 실행하세요. 모델 가용성은 변경될 수 있으므로, 기사에 특정 ID를 고정하여 작성하는 것은 피합니다. 명시적인 스키마(schema)는 폴백 경계(fallback boundary) 역할을 합니다. 즉, 형식이 잘못되었거나 누락된 필드는 조용히 허용하는 대신 수동 검토(manual review)를 위해 업로드되어야 합니다.
통합 경계 선택하기 (Choosing the integration boundary)
저는 분류 체계(taxonomy)를 누가 소유하는지, 내 저장소(repository)에 얼마나 많은 어댑터(adapter) 코드가 들어가는지, 그리고 결정을 재현(replay)할 수 있는지에 따라 시스템을 비교합니다. 정확한 모델 지원 사항은 변경될 수 있으므로, 도입을 결정하기 전에 각 제공업체의 최신 문서에서 이미지 입력 및 구조화된 출력(structured-output) 지원 여부를 확인하세요.
| 옵션 | 통합 형태 | 정책 및 평가 트레이드오프 (trade-off) | 선택하는 경우 |
|---|---|---|---|
| Infrai | 하나의 REST API 뒤에 있는 OpenAI 호환 채팅 | 내 팀이 스키마, 임계값(thresholds), 정규화(normalization) 및 평가(evals)를 소유함 | 모더레이션(moderation)이 다른 백엔드 기능과 나란히 위치하기를 기대하며, 하나의 일관된 계약(contract)을 원하는 경우 |
| ... |
Infrai의 관련 장점은 단순한 인터페이스 뒤에 숨겨진 광범위함에 있습니다. 20개 모듈에 걸친 295개의 경로(routes)가 하나의 키와 하나의 REST 계약 아래에 놓여 있습니다. 노트북(notebook)에서 프로덕션(production)으로 전환하는 Python 팀의 경우, 백엔드 기능을 추가하는 것이 또 다른 SDK, 자격 증명 세트, 어댑터를 추가하는 것이 아니라 또 다른 엔드포인트(endpoint)를 추가하는 것을 의미할 수 있습니다. 공개 탐색 응답(public discovery response) 또한 자기 기술적(self-describing)이므로, 클라이언트를 생성하기 전에 준비 상태와 스키마를 검사할 수 있습니다.
주의할 점도 분명합니다. Infrai는 이 경로에서 전용 이미지 모더레이션 엔드포인트를 제공하지 않으며, 이는 내 팀이 정책 문구, JSON 계약, 보정(calibration) 및 이의 신청(appeals)을 직접 관리해야 함을 의미합니다. 고정된 분류 체계와 운영 워크플로우를 원하는 경우에는 전문화된 관리형 모더레이션 제품을 사용하고, 공통 인터페이스보다 제공업체별 제어가 더 중요한 경우에는 OpenAI, Google 또는 Anthropic을 직접 사용하는 것이 좋습니다. 귀하의 이미지에 어떤 모델이 승리할지는 확실하지 않습니다. 결과는 상황에 따라 다를 수 있으며(your mileage may vary), 오직 대표적인 평가 세트(eval set)만이 이를 결정할 수 있습니다.
프롬프트 다듬기보다 실패 정책(Failure policy)이 더 중요합니다
프로덕션 게이트(production gate)에는 불확실성에 대한 명시적인 응답이 필요합니다. 저는 스키마 오류(schema failures), 알 수 없는 레이블(unknown labels), 그리고 신뢰도가 낮은 분류(low-confidence classifications)를 review(검토)로 매핑하고, 명확한 고위험 증거는 block(차단)으로 매핑하며, 오직 완벽하게 문제가 없는 경우에만 allow(허용)로 매핑합니다. 이러한 보수적인 매핑은 의도적으로 모델 프롬프트(model prompt) 외부에 둡니다. 프로덕션 코드(Product code)에서 이를 테스트하고, 버전 관리하고, 이의 신청(appeal) 과정에서 설명할 수 있어야 하기 때문입니다.
또한 저는 모델의 원시 결정(raw model decision), 정규화된 상태(normalized status), 정책 버전(policy version), 모델 ID(model ID), 그리고 요청 ID(request ID)를 저장합니다. 앞의 두 가지가 핵심적인 차이점입니다. 원시 증거(raw evidence)는 분류기(classifier)가 반환한 값을 보존하는 반면, 정규화된 값(normalized value)은 제가 카테고리 이름을 변경하거나 임계값(threshold)을 강화하더라도 다운스트림 시스템(downstream systems)이 안정적으로 유지되도록 합니다. 데이터 보관 및 접근 규칙은 사용자 업로드 데이터의 민감도와 일치해야 합니다. 감사 저장소(audit store)를 이미지를 영구적으로 보관하기 위한 핑계로 삼아서는 안 됩니다.
또 다른 유혹적인 주의 분산 요소가 있는데, 바로 이미지 업스케일링(image upscaling)입니다. Infrai는 선택 사항으로 Lanczos 전용 업스케일을 제공하지만, 크기 조정(resizing)은 모더레이션(moderation)과는 별개의 작업이며 안전 제어(safety control) 기능이 아닙니다. 저는 결정 경로(decision path)를 위해 원본 업로드 이미지를 테스트할 것입니다. 만약 제품에서 별도로 확대된 에셋(asset)이 필요하다면, 그것은 자체적인 목적과 보관 규칙을 가진 이미지 처리(image processing)로 취급하십시오.
이 섹션이 짧은 것은 의도적입니다. 진짜 힘든 작업은 영리한 프롬프트를 만드는 것이 아닙니다. 분류기가 불확실할 때 어떤 일이 일어날지 결정하고, 테스트를 통해 그 동작을 증명하며, 결정을 재검토할 수 있을 만큼 충분한 증거를 유지하는 것입니다.
이 설계를 복제하기 전에 측정하는 것들
출시 전에는 무해한 예외 사례(benign edge cases)와 정책 경계 사례(policy-boundary examples)를 포함하여 실제 업로드 혼합(upload mix)을 반영하는 레이블링된 세트(labeled set)를 고정(freeze)합니다. 저는 심각한 카테고리별 허위 미탐률(false-negative rate), 허위 탐률(false-positive rate), 수동 검토율(manual-review rate), 스키마 유효 응답률(schema-valid response rate), 그리고 최종 결정당 비용(cost per final decision)을 보고합니다. 또한 이미지 소스(image source)와 정책 카테고리(policy category)별로 결과를 세분화하여 분석합니다. 하나의 통합 점수(aggregate score)는 약물이 없는 쉬운 사진들 뒤에 숨겨진 잘못된 혐오 상징(hate-symbol) 결과를 가릴 수 있기 때문입니다.
그 후 프롬프트(prompt), 스키마(schema), 정책(policy) 또는 모델(model)이 변경될 때마다 테스트 스위트(suite)를 다시 실행합니다. 후보 모델은 카테고리 임계값(thresholds)을 충족하고, 리뷰(review) 물량이 팀의 처리 용량을 초과하지 않을 때만 배포됩니다. 변경된 모델에 실시간 결정을 맡기기 전에는 소규모 섀도우 런(shadow run)을 유지합니다. 이 지점에서 저의 노트북 습관이 도움이 됩니다. 모델을 선별할 때 사용했던 것과 동일한 픽스처(fixtures)와 어설션(assertions)이 프로덕션 회귀 테스트 하네스(regression harness)가 됩니다.
프롬프트 비용(Prompt cost)도 해당 하네스에 포함되어야 합니다. 반복되는 정책 텍스트를 계산하고, 재시도(retries)를 추적하며, 승인된 결정당 총 입력량을 측정하십시오. 고립된 상태에서 보면 아주 작은 요청일지라도, 크롭(crops), 재시도, 그리고 여러 후보 모델을 거치고 나면 비용이 많이 드는 배치(batch) 작업이 될 수 있습니다. 제가 파악한 바로는, "충분히 좋은" 모더레이션(moderation)을 위한 정직하고 보편적인 임계값은 존재하지 않습니다. 적절한 기준은 위해(harm)의 심각성, 리뷰어의 용량, 그리고 사용자가 항소(appeal)할 수 있는 범위에 따라 달라집니다.
폴백(fallback)이 단순히 설명되는 것에 그치지 않고 실제로 작동한 후에만 게이트(gate)를 배포하십시오. 단위 테스트(unit test)에서 잘못된 형식의 구조화된 출력(malformed structured output)을 입력해 보십시오. 모델이 리뷰를 선택하는지 확인하십시오. 그런 다음 고정된 이미지 세트(frozen image set)에서 실제 모델을 평가하고, 모든 결과에 정책 버전(policy version)을 기록하십시오. 그렇게 하면 설계의 재현성(reproducible)이 확보되며, 이는 저에게는 잘 다듬어진 데모 스크린샷보다 훨씬 더 중요합니다.
References
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기