[Nest.js]15. ChatGPT Fine-tuning 웹툰 학습(4)

김지엽·2023년 10월 26일
0
post-thumbnail
post-custom-banner

1. 개요

줄거리가 500자 이상이고, 장르 키워드 수가 9개 이상인 "로판" 웹툰 데이터를 학습시켰다.

이번에는 장르 키워드가 적은 웹툰들을 미세조정 모델을 통해 새로운 장르 키워드를 보충하는 것이 목표이다. 세세한 과정은 다음과 같다.

  1. 웹툰 데이터들 중 장르 키워드가 5개 미만인 데이터를 선별한다.
  2. 미세조정 모델을 통해 해당 웹툰의 제목, 줄거리, 카테고리로 새로운 장르 키워드를 요청한다.

2. 장르 키워드 요청

장르 키워드가 5개 미만인 "로판" 웹툰은 약 300개였다.

- 추천 장르 요청

먼저 웹툰 아이디를 통해서 미세조정 모델을 통해 추천 장르를 요청하는 메서드를 작성한다.

async createRecommendGenreText(webtoonId: string): Promise<string> {
    // 웹툰 데이터 불러오기
    const webtoon = await this.webtoonService.getWebtoonForId(webtoonId);
    const description = webtoon.description.replaceAll(/[\*\+#=\n]/g, "");

    // completion prompt 메세지 
    const messages = this.openaiService.create_3_5_PromptMessage(
        `너는 웹툰의 제목과 카테고리, 줄거리를 읽고 장르의 뜻과 연관 지어서 분석 후 줄거리의 뜻에 맞는 장르키워드를 알려주는 조수야`,
        `제목: ${webtoon.title}\n\n카테고리: ${webtoon.category}\n\n줄거리: ${description}\n\n\n\n위 제목과 줄거리를 가진 웹툰의 장르 키워드를 가장 적합한 순서대로 알려줘`
    );

    // 장르 분석 요청
    const result = await this.openaiService.create_3_5_Completion(
        this.configService.get<string>("OPENAI_WEBTOON_GENRE_MODEL"),
        messages,
        0.6,
        80
    );

    return result;
}

- 장르 요청 문제 발생

하지만 실험으로 몇가지 웹툰 데이터를 통해 추천된 장르를 확인한 결과는 예상과는 달랐다. 당연하게도 완벽하지 않은 결과였다. 물론 절반 정도의 키워드는 해당 웹툰과 연관된 키워드였지만 나머지는 달랐다.

따라서 여러가지 시도를 해보았다.

  1. 모델에게 요청할때 프롬프트의 메세지를 더 정확하게 바꿔본다
    => 실패..
  2. 모델에게 요청할때 temperaute을 높이거나 낮추면서 결과를 다양하거나 고정적으로 나오게 한다.
    => 실패..
  3. 요청을 여러번 한다.
    => 이것이 정답이었다..

여러가지 시도 끝에 여러번의 장르 요청 후 반복되는 키워드를 추출하는 것이 정확도 측면에서는 가장 좋았다.

- 추천 장르 요청(발전)

자세한 로직은 다음과 같다.
1. 조건을 통해 장르를 추천 받고 싶은 웹툰만을 선별한다.
2. 각 웹툰마다 7번 장르 요청을 받고 모든 키워드를 카운팅한다.
3. 3번이상 나온 키워드를 결과로 반환한다.

각 웹툰의 추천 장르 반환 메서드

async createRecommendGenre(webtoonId: string): Promise<string[]> {
    const genreCounter: { [genre: string]: number } = {};

    // 5번의 추천으로 각 키워드 마다 빈도 수 세기
    for (let i=0; i<7; i++) {
        const genreText = await this.createRecommendGenreText(webtoonId);
        const genres = genreText.split(" ");

        genres.forEach((genre) => {
            if (genre in genreCounter) genreCounter[genre] += 1;
            else genreCounter[genre] = 1;
        });
    }

    // 가장 빈도 수가 높은 7개의 키워드만 추출
    const genreCounterArray: [string, number][] = Object.entries(genreCounter);
    genreCounterArray.sort(
        (a: [string, number], b: [string, number]) => b[1] - a[1],
    );
    const recommendGenres = 
    genreCounterArray
    .filter((genreCounterElement) => {
        return genreCounterElement[1] > 2;
    })
    .map((genreCounterElement) => {
        return genreCounterElement[0];
    });

    return recommendGenres;
}

조건에 맞는 모든 웹툰의 장르를 추천 받아 DB 업데이트

async initWebtoonRecommendGenre(initRecommendGenreOptionDto: InitRecommendGenreOptionDto) {
    // 조건에 맞는 웹툰 불러오기
    const webtoons = await this.webtoonService.getAllWebtoonForOption({
        ...initRecommendGenreOptionDto,
    });

    for (let webtoon of webtoons) {
        const { webtoonId } = webtoon;
        let genres: string[] = JSON.parse(webtoon.genres);
        // 최종적으로 기존의 순서를 유지하기 배열을 뒤집는다. 
        genres = genres.reverse();

        // 웹툰 장르를 gpt에게 요청해서 받아오기
        let recommendGenres = await this.createRecommendGenre(webtoonId);

        // 추천 받은 장르가 원래 장르에 이미 포함되어 있다면 순서를 앞으로 조정
        for (let [genre, idx] of genres) {
            const r_idx = recommendGenres.indexOf(genre);
            if (r_idx !== -1) {
                recommendGenres.splice(r_idx, 1);
                recommendGenres.unshift(genre);
                genres[idx] = "delete";
            }
        }

        // 추천 받은 장르 중 기존에 이미 포함되어 있던 키워드는 삭제
        genres = genres.filter((genre) => { return genre !== "delete" });
        recommendGenres = [...genres, ...recommendGenres, "추천"];

        console.log(
            `제목: ${webtoon.title}\n줄거리: ${webtoon.description}\n기존 장르: ${webtoon.genres}\n추천 장르: ${recommendGenres}`,
        );

        await this.webtoonService.updateWebtoonForOption({
            webtoonId,
            genres: JSON.stringify(recommendGenres),
            genreCount: recommendGenres.length
        });
    }
}

- 결과



물론 위의 방법대로 해도 결과가 완벽한 것은 아니다. 다만, 정확도가 훨씬 오르기 때문에 내가 바라는 결과에 가까워진다.

하지만 단점은 하나의 웹툰마다 7번을 요청하기 때문에 비용이 매우 비싸진다는 단점이 존재한다..

발전한점

- 장르 추천 로직

리팩토링 전에는 웹툰당 한번의 추천으로 결과가 지금보다 훨씬 안좋았다. 하지만 "어차피 실제 서비스로 운영할 것도 아니고 구현만 하면 끝난 것 아닌가?"라는 마음을 가지고 다음단계로 넘어갔다.

리팩토링 후에는 최대한 좋은 결과를 위해서 많은 시도를 하고(비용이 많이 발생...) 결국에는 단점은 있지만 정확도는 괜찮은 방법을 알게되었다.

또한 사실 장르 키워드 카운팅은 최근에 공부하고 있는 정렬 알고리즘에서 개수 정렬에서 영감이 떠올랐고 구현으로 옮기게 되었다. 항상 더 많은 것을 알게되면 더 넓게 생각할 수 있다는 생각을 가지며 오늘의 포스팅을 마친다.

참고

정렬 알고리즘

profile
욕심 많은 개발자
post-custom-banner

0개의 댓글