-
CLOVA-Studio로 인공지능 데이터 평가 서버 구축하기 feat. 데이터 품질 개선 알고리즘Project/OpenList 2024. 9. 25. 21:54반응형
안녕하세요! 오늘은 Openlist 팀에서 CLOVA-Studio를 이용해 인공지능 데이터 평가 서버를 구축한 경험을 공유해보려고 합니다. 데이터 품질 개선을 위해 적용한 알고리즘도 함께 소개할게요.
작동 조건
- Redis Pub/Sub을 이용해 WAS와 통신합니다.
- Redis에서 "ai_generate" 채널을 구독하다가 processAiEvaluate 메시지가 오면 평가 수가 설정된 number 이하인 모든 아이템을 평가합니다.
{ "message": "processAiEvaluate", "body": number }
평가 방식
평가 수가 number 이하인 모든 아이템을 전부 가져온 다음, 이를 10개씩 자른 뒤 가장 괜찮은 아이템 3개를 뽑아달라고 네이버 HyperClova X에 요청을 보냅니다.
너는 평가자야 사용자가 대 중 소 카테고리를 기반으로 추천 체크리스트를 만들었어 체크리스트에는 10개의 항목이 있어. 이 중에서 카테고리에 잘 어울리는 체크리스트 3개를 선택해줘
그리고 그 결과를 데이터베이스에 반영하고, 모든 평가가 끝나면 결과를 토대로 최종 점수를 업데이트해줍니다.
구조
- 간단한 프로그램이므로 별다른 프레임워크를 쓰지 않고 Node.js로만 구현했습니다.
- PM2를 이용해 백그라운드에서 실행했습니다.
- Redis에서 "ai_generate" 채널을 구독하다가 processAiEvaluate 메시지가 오면 평가 프로세스를 실행합니다.
- 관리자 페이지에서 볼 수 있도록 진행 사항을 Redis에 publish 해줍니다.
async function main() { const redisSub = await subscriber.connect(); // 레디스 구독 redisSub.subscribe("ai_generate", async function (data) { try { const { message, body } = parseJsonData(data); if (message === "processAiEvaluate") { publisher.send( "ai_evaluate", `received: ${message} ${body} processAiEvaluate start` ); const evaluateCountMax = parseInt(body); // 메인 데이터베이스에서 조건에 맞는 모든 아이템 가져오기 const checklistItemsByEvaluateCount = await getChecklistItemsByEvaluateCount(evaluateCountMax); // 가져온 데이터를 인공지능에게 보낼 형식으로 변형 const result = transformAndChunkItems(checklistItemsByEvaluateCount); const evaluateCycle = result.length; publisher.send("ai_evaluate", `expected count: ${evaluateCycle}`); // 인공지능을 이용해 모든 아이템 평가 후 메인 데이터베이스 업데이트 await processResultsConcurrency(result); publisher.send("ai_evaluate", `모든 평가가 완료되었습니다.`); // 평가 결과를 토대로 최종 점수 업데이트 await setFinalScore(); publisher.send("ai_evaluate", `final_score가 업데이트되었습니다.`); } } catch (error) { console.error("An error occurred:", error); publisher.send("ai_evaluate", `An error occurred: ${error}`); } }); }
문제 해결 과정
HyperClova X의 정확도 문제
프롬프트 개선
기존 프롬프트
너는 평가자야 사용자가 대 중 소 카테고리를 기반으로 추천 체크리스트를 만들었어 체크리스트에는 10개의 항목이 있어. 이 중에서 카테고리에 잘 어울리는 체크리스트 3개를 선택해줘
기존 프롬프트로 요청하면 같은 아이템을 보내도 항상 다른 아이템을 뽑습니다. 즉, 평가가 아닌 랜덤하게 아이템을 선별하는 거죠. 또한 JSON 형식을 지키지 않고 보내는 경우가 많았습니다.
그래서 번호만 주는 것이 아니라 이유도 같이 보내도록 하고, JSON 형식의 예시를 미리 다 지정해주었습니다.
변경된 프롬프트
너는 평가자야. 사용자가 대/중/소 카테고리를 기반으로 추천 체크리스트를 만들었어. 체크리스트에는 10개의 항목이 있어. 이 중에서 카테고리에 잘 어울리는 체크리스트 3개를 선택해서 번호와 이유를 JSON 형식으로 제공해 줘. 오직 JSON 형식으로만 출력해야 해. 사용자가 입력할 정보 1. 대/중/소 카테고리 JSON { "mainCategory": "대", "subCategory": "중", "minorCategory": "소" } 2. 체크리스트 JSON { "1": "생성된 체크리스트1", "2": "생성된 체크리스트2", "3": "생성된 체크리스트3", "4": "생성된 체크리스트4", "5": "생성된 체크리스트5", "6": "생성된 체크리스트6", "7": "생성된 체크리스트7", "8": "생성된 체크리스트8", "9": "생성된 체크리스트9", "10": "생성된 체크리스트10" } { "select": [ "number1", "number2", "number3" ], "reason": { "number1": "reason1", "number2": "reason2", "number3": "reason3" } }
사용자 입력 예시
{ "mainCategory":"해외 여행", "subCategory":"라오스", "minorCategory":"관광명소" } { "1": "라오스 문화에 대해 알아보기", "2": "불교 사원 방문하기", "3": "자연 경관 감상하기", "4": "동굴 탐험하기", "5": "액티비티 체험하기", "6": "현지 음식 즐기기", "7": "전통 공예품 구매하기", "8": "교통 수단 이용 시 주의사항 숙지하기", "9": "안전에 유의하며 여행하기", "10": "여행 준비물 꼼꼼히 챙기기" }
AI 답변 예시
{ "select": [ "2", "3", "5" ], "reason": { "2": "라오스는 불교 국가로 유명하여 많은 사원들이 있습니다. 라오스의 역사와 문화를 체험할 수 있는 좋은 기회가 될 것입니다.", "3": "라오스는 아름다운 자연 경관으로 유명합니다. 특히, 메콩강과 카르스트 지형 등은 꼭 봐야 할 관광명소입니다.", "5": "라오스는 액티비티 천국이라고 불립니다. 카약, 래프팅, 다이빙 등 다양한 액티비티를 즐길 수 있습니다." } }
정확도가 많이 올라갔어요. 또한 이유를 같이 저장했기 때문에 차후에 최종 점수 평가 시 해당 인공지능의 신뢰도를 파악할 때 데이터로 사용할 수 있게 되었죠.
코드에서 검증
프롬프트를 업데이트해도 여전히 이상한 답변이 오거나 JSON이 아닌 형식으로 오는 경우가 있었습니다. 이럴 땐 코드에서 검증을 하고 형식에 맞지 않으면 다시 요청하도록 처리했어요.
우선 답변이 JSON 형식인지, 그리고 의도한 답변 형태로 오는지 검사하는 로직을 추가했습니다.
// CLOVA API의 결과를 파싱하는 함수 async function aiResultParser(result) { const content = result?.result?.message?.content; const parsedContent = JSON.parse(content); const { select, reason } = parsedContent; return { select, reason }; } // CLOVA API의 결과가 유효한지 검사하는 함수 async function checkValidResult(select, reason, checklistDto) { if (select.length !== 3) { throw new Error("select 항목이 3개가 아닙니다."); } if (Object.keys(reason).length !== 3) { throw new Error("reason 항목이 3개가 아닙니다."); } if (Object.keys(reason).some((item) => isNaN(item))) { throw new Error("reason의 키 항목이 숫자가 아닙니다."); } if (select.some((item) => isNaN(item))) { throw new Error("select 항목이 숫자가 아닙니다."); } const checklistDtoKeys = Object.keys(checklistDto); if (!Object.keys(reason).every((key) => checklistDtoKeys.includes(key))) { throw new Error("reason의 키에 해당하는 checklistDto의 키가 없습니다."); } }
이를 이용해 잘못된 답변이 오면 다시 요청을 보내도록 했고, 10번 재시도해도 이상한 요청이 오는 경우는 실패 처리를 하도록 만들었습니다.
// 체크리스트를 평가하는 메인 함수 async function processAiResult(categoryDto, checklistDto, maxRetries = 10) { let retryCount = 0; // 처리 과정 중 문제가 생기면 최대 10번 다시 요청 보내기 while (retryCount < maxRetries) { try { // AI에게 결과 받기 const result = await evaluateChecklistItem(categoryDto, checklistDto); // 파싱 시도 const { select, reason } = await aiResultParser(result); // CLOVA API의 결과가 유효한지 검사하는 함수 await checkValidResult(select, reason, checklistDto); return { select, reason }; } catch (error) { retryCount++; } } // 만약 10번 다 실패하면 해당 요청은 실패 처리 if (retryCount === maxRetries) { await publisher.send("ai_evaluate_error", "모든 재시도 실패"); return { select: undefined, reason: undefined }; } }
점수 업데이트
이제 점수를 업데이트해줘야 합니다. 이때 단순히 선택된 수를 점수로 하면 먼저 생성된 체크리스트가 상대적으로 많이 뽑히므로, 오래된 데이터가 뽑힐 확률이 더 높아지게 되죠.
그래서 많이 평가되었는데 적게 선택되면 적게 평가되고 적게 선택된 것보다 점수가 낮고, 많이 평가되었는데 많이 선택되면 적게 평가되고 많이 선택된 것보다 점수가 높도록 계산식을 세웠습니다.
SET final_score = ROUND((selected_count_by_naver_ai::float / GREATEST(evaluated_count_by_naver_ai, 1)) * 100 + LOG(GREATEST(evaluated_count_by_naver_ai, 1)));
다음과 같은 식을 적용하여서 점수를 매겼다.
오래 걸리는 문제
API 요청 한 번당 약 6~7초가 걸리는데, 보통 이런 API를 800개 정도 보내므로 지나치게 오래 걸리는 문제가 발생했어요.
PromisePool
PromisePool을 이용하면 동시에 API를 최대 10개씩 보낼 수 있습니다. 만약 먼저 끝난 요청이 있으면 그 자리에 새로운 API를 넣어주는 식이죠.
// AI 평가를 동시에 여러 개 처리하는 함수 async function processResultsConcurrency(result) { const proccessCycle = result.length; // 동시에 처리할 작업 수를 10개로 설정 const { results, errors } = await PromisePool.withConcurrency(10) .for(result) .process(async (item) => { const { category, contents } = item; const contentIDs = Object.keys(contents); // API 요청 보내기 const { select, reason } = await processAiResult(category, contents); // 데이터베이스에 결과 반영하기 await incrementCounts(contentIDs, "evaluated"); await incrementCounts(Object.keys(reason), "selected"); await insertReasons(reason); }); }
429 에러
네이버 HyperClova X API의 경우 요청을 1초 내에 다시 보내면 429 에러를 반환합니다. 그래서 해당 에러가 발생하면 시간을 두고 다시 요청을 보내야 해요.
이를 해결하는 방법은 간단합니다. 에러가 나면 해당 요청을 랜덤 시간 후에 다시 요청하면 됩니다. 랜덤으로 하는 이유는 딜레이 시간을 다 같게 하면 다음 요청들도 또 동시에 보내서 다시 429 에러가 발생하기 때문이에요.
아까 processAiResult의 catch 부분 코드를 업데이트해줍시다.
// processAiResult의 catch 일부... // 429 에러인 경우, 2초에서 10초 사이의 랜덤한 시간 동안 대기 후 재시도 if (error?.response?.status === 429) { const delayTime = getRandomDelay(2000, 10000); // 2초에서 10초 사이의 랜덤한 시간 await publisher.send( "ai_evaluate_error", `Too many requests::Waiting for ${delayTime}ms before retry...` ); await delay(delayTime); continue; // 다음 시도로 이동 }
결과
여기까지 따라와서 잘 코드를 작성하셨다면, 우분투 서버에 배포하는 과정은 아래 포스팅을 참고해주세요.
Node.js 서버 인스턴스 세팅 및 PM2를 이용한 서버 자동화
Node.js 서버 인스턴스 세팅 및 PM2를 이용한 서버 자동화 | Notion
이 글에서는 우분투 환경에서 Node.js 프로젝트를 세팅하고, pm2를 활용하여 서버를 자동화하는 과정을 단계별로 설명하겠습니다. 먼저, Node.js를 설치하고, git을 사용하여 프로젝트를 복제한 후,
msmspark.notion.site
이상으로 CLOVA-Studio를 이용한 인공지능 데이터 평가 서버 구축과 데이터 품질 개선 알고리즘에 대해 살펴보았습니다. 이 글이 여러분의 프로젝트에 도움이 되길 바라며, 더 궁금한 점이나 질문이 있다면 언제든 댓글 남겨주세요! 🙌
저희 Openlist 팀은 앞으로도 더 좋은 서비스를 만들기 위해 노력하겠습니다. 여러분의 관심과 응원 부탁드려요! 감사합니다. 😊👍
반응형'Project > OpenList' 카테고리의 다른 글
Ubuntu 서버에서 Node.js 프로젝트 세팅하기 feat. PM2로 서버 자동화 (2) 2024.09.25 GPT-4로 인공지능 데이터 캐싱 서버 구축하기 feat. Promise Pool (1) 2024.09.25 Pipe & Filter 아키텍처로 인공지능 데이터 캐싱 시스템 구축하기 (0) 2024.09.25 NestJS로 CLOVA Studio API 연동하기: 완벽 가이드 (1) 2024.09.25