Marginalia Search의 NSFW 필터 개발기
2026년 3월 30일
Marginalia Search에 NSFW 필터를 붙이는 작업을 진행 중입니다. API 사용자들이 요청한 기능이거든요.
검색 엔진은 이미 UT1 리스트 기반의 도메인 필터링을 가지고 있었지만, 이 방식은 그다지 포괄적이지 못했습니다. 결국 처음부터 구현한 단일 은닉층 신경망으로 정착했는데요, 거기에 도달하기까지의 여정이 꽤 흥미롭습니다.
속도와 정확성의 줄타기
분류 작업에서는 항상 속도와 일반화 능력 사이에 긴장이 존재합니다. 빠르면서도 정확한 결과를 내는 것은 생각보다 까다로운 작업이죠. 특히 검색 엔진에서 실행되는 필터라면 CPU에서 정말 빨리 돌아야 합니다.
이 제약 때문에 Transformer 기반 모델이나 최신 기법들은 처음부터 고려 대상이 아니었어요. 아무리 성능이 좋아도 속도 조건을 만족하지 못하거든요.
Fasttext 시도
처음에 시도한 방법 중 하나가 fasttext였습니다. 메타(예전 페이스북)에서 만든 분류 라이브러리인데, 이름처럼 정말 빠릅니다. 게다가 Marginalia Search는 이미 언어 식별에 fasttext를 사용 중이어서 새로운 의존성도 필요 없었습니다.
다만 분류기를 학습시키려면 수만 개 수준의 샘플 데이터가 필요한데, 검색 엔진을 운영하는 입장에서는 후보 샘플을 찾는 게 오히려 쉬웠어요. 말 그대로 검색하면 되니까요!
학습 데이터 구성
문제는 이 샘플들을 몇 주에 걸쳐 수동으로 라벨링해야 한다는 것이었습니다. 더 효율적인 방법이 분명 있을 테니까요.
기존 NSFW 데이터셋이 있긴 하지만, 빠른 분류기는 데이터의 형태와 맥락에 매우 민감합니다. Reddit 댓글로 학습시킨 분류기는 Reddit 댓글에만 잘 작동하고, 검색 결과에는 형편없는 성능을 보이거든요.
그래서 다른 접근을 시도했습니다. 최신 기법은 분류기로 쓸 수 없지만, 학습 데이터 라벨링에는 충분히 사용할 수 있다는 생각이었어요.
LLM의 생성 능력은 유명하지만, 사실 비지도 분류 작업에서도 꽤 우수합니다. Ollama와 Qwen 3.5 같은 오픈소스 모델들이면 충분하고, 일반 PC에서도 돌릴 수 있습니다.
파이프라인
- 검색 쿼리 실행
- 결과를 ollama/Qwen 3.5로 전달하면서 NSFW 분류 지시 제공
- 결과를 SAFE 또는 NSFW로 라벨링
이 방식은 샘플당 몇 초 정도 걸리지만, 사람이 개입할 필요가 없어서 며칠 동안 쭉 돌려놓을 수 있습니다. 결과 품질도 사람이 10,000개쯤 라벨링하다가 피로해진 것 정도는 충분히 상쇄할 수 있을 정도였어요.
물론 "NSFW인지 아닌지 판단하기 애매한" 경우는 항상 존재합니다. 하지만 충분히 일관성 있고 합리적인 판단을 내렸으므로 이 작업에는 적합했습니다.
문제 발생
약 10,000개 샘플(NSFW 60%, SAFE 40%)을 수집해 fasttext로 모델을 만들었는데, 결과가 형편없었습니다.
원인은 학습 데이터의 편향이었어요. NSFW 관련 쿼리로 샘플을 모았기 때문에, 실제로 NSFW 콘텐츠가 아닌데도 NSFW와 연관된 검색어를 포함한 문서들이 많았던 겁니다.
빠른 분류기는 이런 노이즈에 민감하게 반응해서 실제 검색 결과에 적용하면 수많은 거짓 양성(false positive)을 발생시켰습니다.
더 큰 샘플셋의 문제
이 시점에서 검색 엔진의 전체 문서(약 125M개)를 Qwen으로 분류하는 방법도 생각해봤습니다. 하지만 이건 실제로는 불가능한 아이디어였어요. 소비자 GPU에서 이런 작업을 하려면 약 20년이 필요하고, 전기료와 열배출 문제도 감당할 수 없거든요.
결국 실제 NSFW 콘텐츠는 드물기 때문에, 대표성 있는 샘플 구성이 비용 측면에서 엄청나게 비싼 문제였습니다. 10만 개 정도까지는 가능하지만, 그 정도 샘플로는 낮은 기저율을 고려할 때 부족합니다.
신경망으로의 전환
fasttext가 데이터의 노이즈에서 비관련 특성들을 집어내고 있다고 가정했습니다. 그렇다면 특성 자체를 제한해서 분류기의 초점을 맞출 수 있지 않을까요?
이 접근법은 실제로는 예상 외로 성공한 몇 가지 단순한 아이디어에서 영감을 받았습니다.