본문 바로가기
backend

[AI] 간단한 AI api 호출로 nsfw 컨텐츠 검수해보기

by BK0625 2025. 4. 22.
반응형

 

이직하고난지 한달도 되지 않아... 이전 회사의 ai 서비스 개발 경험을 바탕으로 새로운 업무를 맡게 되었다.

 

이른바 ai를 통해 nsfw 컨텐츠 검수 필터링 기능 도입이였다.

 

크리에이터와 팬을 잇는 서비스의 특성 상 이런 센슈얼한 이슈가 될 수 있는 컨텐츠는 필연적인 바... 현재 회사에는 모든 검수가 수동으로 이루어지고 있다고 했다. 해당 담당자의 화면에는 모든 포스트를 열람할 수 있는 화면이 뜨고 거기서 담당자가 문제가 될 만한 컨텐츠인지 아닌지를 직접 확인하고 있던 상황..

 

이런 상황이기 때문에 당연히 담당자가 놓치거나하는 이슈가 발생할 수 있기 때문에 이 부분에서 ai를 통해 센슈얼한 컨텐츠만 필터링을 해서 볼 수 있는 기능이 요구 사항이였다.

 

처음 기존 담당자가 진행했던 부분은 직접 모델을 학습 시켜 필터링을 하는거였지만 여기서 두 가지의 문제가 있다.

 

첫번째, 과연 데이터를 학습 시켰을 때 모델이 우리가 원하는 성능을 충족 시킬 것인가 라는 것이였는데 내가 알기로 ai 모델은 망각효과라는게 있어서 애매한 학습은 오히려 성능을 더 떨어뜨릴 수 있다고 알고 있다. 그리고 데이터를 학습시키기에 적합한 데이터와 라벨링 역시도 문제였다. 그리고 일단 나는 이미 만들어진 모델로 ai 서비스 개발을 해본거지 학습을 시켜본 적이 없...

 

두번째, 현재 우리 서비스의 백엔드는 php ci3로 이루어져있다. 만약 ai 모델을 돌릴려면 별도의 ai 서버를 두어야 하는 문제가 발생한다.

 

나는 기능적인 요구 사항을 보았을 때 굳이 모델 학습이 필요가 없다고 판단했다. 그래서 nsfw 컨텐츠를 검수할 수 있는 모델이 있나 허깅 페이스를 뒤지기 시작했다.

 

https://huggingface.co/Falconsai/nsfw_image_detection

 

Falconsai/nsfw_image_detection · Hugging Face

Model Card: Fine-Tuned Vision Transformer (ViT) for NSFW Image Classification Model Description The Fine-Tuned Vision Transformer (ViT) is a variant of the transformer encoder architecture, similar to BERT, that has been adapted for image classification ta

huggingface.co

 

 

그리고 이 모델을 찾아낼 수 있었다. 이 모델은 이미지를 api로 쏠 수 있었고 해당 이미지가 nsfw 이미지인지 normal 이미지인지를 0부터 1로 판단하여(nsfw+normal=1) 얼마나 성인용 컨텐츠인지를 판별할 수 있었다. 실습은 내가 익숙한 nodejs로 진행하였다.

 

async function checkNSFW(filePath) {
    const imageBuffer = readFileSync(filePath);
    const base64Image = imageBuffer.toString('base64');
    const res = await axios.post(
      'https://api-inference.huggingface.co/models/Falconsai/nsfw_image_detection',
      { inputs: base64Image },
      {
        headers: {
            'Authorization': `Bearer ${process.env['hugging-key']}`,
            'Content-Type': 'application/json',
        },
      }
    );
    console.log(res)
    return res.data;
  }

 

 

예시 코드는 다음과 같다. 허깅페이스 api 키는 허깅페이스 홈페이지에서 발급 받을 수 있다. 실습해본 코드에서는 파일을 서버에 저장했기 때문에 함수 인자로 filePath를 받아온다. 해당 api는 base64로 변환을 해줘야 됐기 때문에 해당 과정을 거친 후 api로 그냥 쏘면 된다. 그러면 알아서 판단해서 리턴값을 준다.

 

 

일반적인 이미지를 업로드 했을 때는 normal이 0.987이 넘는 모습

 

 

반대로 성인 컨텐츠를 올렸을 때는 nsfw가 0.98이 넘는 모습을 보인다.

 

블러가 쳐져있어 알아보기 어렵거나 애매한 사진들도 얼추 잡아 낼 수 있었다. 걱정했던 부분은 피부가 노출된다고 다 야한 컨텐츠가 아니기 때문에 그 부분을 걱정했었는데

 

 

그래도 얼추 normal이 높게 나오는 모습을 보여 사용이 가능하다고 판단이 되었다.

 

그리고 여기서 두번째 문제가 발생했는데 검열된 이미지들을 보니 대놓고 nsfw 컨텐츠도 있었지만 입던 속옷만 있는 사진이라던지 등 ai가 nsfw로 판단하기 어려운 이미지들도 있었다. 따라서 그 부분도 검열이 필요 했는데 대부분 저런 애매한 컨텐츠는 이미지와 동시에 상상력을 자극할만한 워딩이 동반되는 경우가 있었기 때문에 그 부분을 검열하면 될 거라고 판단했다. 따라서 이 부분은 그냥 프롬프팅 엔지니어링으로 해결하기로 했다.

 

모델은 gpt-4의 경량화 버전으로 가격에서 이점이 있고 max-token 값도 128k로 사실상 청킹이 필요 없는 gpt-4o를 선택했다.

 

마크업 방식이 LLM이 더 잘 알아듣는다는 이야기가 있어 마크업 방식으로 프롬프트를 작성하였으며 토큰 절약을 위해 영어로 작성하였다.

 

프로세스는 다음과 같았다.

 

  1. 입력 받은 타이틀 및 본문 내용을 하나의 문자열로 만듬
  2. 해당 문자열에 block된 단어가 있는지 판단하여 1차 검수(해당 단어들이 있으면 무조건 필터링이 되어야 하므로 굳이 LLM을 호출할 필요가 없기 때문)
  3. 프롬프트를 통해 LLM의 역할을 명시하고 문맥 상의 위반 사항도 잡아낼 수 있도록 프롬프트 작성
  4. 해당 문자열이 1차 검수를 통과하면 프롬프트와 함께 gpt api 호출
  5. 위반한 컨텐츠면 1, 아니면 0을 반환하도록 설정(token 절약)

코드는 다음과 같다.

 

const getAnswer = async (text) => {
    const input = text;
    
    if (containsExplicitKeyword(input)) {
        console.log('미리 지정된 워딩들로 text 검열')
        return "1";
    }
    
    const countToken = countTokens(input)

    /**왠만한 긴 포스트의 길이가 10000을 못 넘기에 거의 발생할 일이 없을거로 판단. */
    if(countToken>128000) {
        console.log('max token 검열');
        return "1";
    }
    

    const prompt = `
        # Task
        You are a content moderation system that classifies whether a given text contains adult or inappropriate content.

        Evaluate the following text and determine if it violates the platform's content policy.

        # Rules for violation (return 1 if any of the following applies, otherwise return 0):
        - Contains adult/sexually explicit content that is not age-restricted.
        - Contains suggestive or implicitly sexual content (e.g., flirty or provocative remarks) that may exceed acceptable standards, even if not explicit.
        - Implies or hints at revealing the body or drawing sexual attention, even if using indirect, trendy, or slang expressions (e.g., “신박하게 오오티디 보여주기”, “my ootd❤️” with revealing context).
        - Uses language that may be offensive, hateful, or cause discomfort to others.
        - Attacks, insults, or degrades individuals or groups; promotes violence, discrimination, or inhumane treatment.
        - References to minors or criminal activity that require stricter moderation.
        - Attempts to evade moderation using spacing, misspellings, abbreviations, or coded language but is still interpretable in context.
        - Shares contact info, bank accounts, or external platform usernames or links for transactional purposes.
        - Slang or abbreviated words that represent inappropriate content (e.g., "뷰지", "ㅂㅈ", etc.).
        - Includes language that may indirectly suggest or provoke sexual imagination or inappropriate thoughts, even in a subtle, sarcastic, or humorous way (e.g., “이거 보고 코피 날 수도 있어”, “그냥 흔드는 건데 무슨 상상해?”).

        # Output Format
        Respond only with:
        - 1 (if the text violates any of the rules)
        - 0 (if the text does not violate any of the rules)

        # Text to Moderate
        "${input}"
    `;

    
    const res = await openai.chat.completions.create({
        model: 'gpt-4o', // 또는 'gpt-3.5-turbo'
        messages: [
          { role: 'system', content: 'You are a content moderation system that classifies whether a given text contains adult or inappropriate content.' },
          { role: 'user', content: prompt },
        ],
        temperature: 0.7,
      });
    
      console.log(res.choices[0].message.content);
      return res.choices[0].message.content;
}

 

이렇게 하면 거의 대부분의 위반 컨텐츠들을 잡아낼 수 있다. 프롬프트에서 문맥을 따져서 판단하라는 문구를 추가하여 애매한 워딩도 잡아낼 수 있게 추가하였으며 role을 확실히 명시하여 그 부분을 중점적으로 보게 하였다.

 

따라서 은유적인 표현까지도 1을 뱉어내도록 하는데 성공하였다.

 

이 두가지를 조합하여 필터링을 구축하면 모니터링 업무의 공수를 많이 줄일 수 있을거로 판단된다. 이 프로젝트는 아직 개발 중이라 완료 후 성과도 포스팅 해보겠다!

반응형