CBF를 사용한 추천 기능 구현하기

0taetae·2025년 1월 27일

📙어떤 기능일까?

  1. 데이터 수집 및 전처리
  2. TF-IDF를 사용한 키워드 추출
  3. 벡터화
  4. 코사인 유사도 계산
  5. 코사인 유사도의 상위 10개의 레시피 추출
  6. GPT API로 2차 필터링
  7. 최종 레시피 3개 추천

사용자의 활동 정보가 업데이트 될 텐데 어떻게 적용할 수 있을까 ❓

결론적으로 배치 스케줄러를 사용했다.
배치 스케줄러는 특정 작업을 미리 정해진 시간이나 주기에 따라 자동으로 실행하도록 관리하는 시스템이다.

다음은 배치 스케줄러 클래스를 별도로 만들어서 작성한 코드이다.

// 1시간 간격으로 모든 사용자의 TF-IDF 계산
@Scheduled(cron = "0 0 * * * *", zone = "Asia/Seoul")
public void scheduleUserProfileUpdate() {
	log.info("사용자 프로필 업데이트 배치 시작");
	userRepository.findAll().forEach(user ->
		userTFIDFService.updateUserTfidf(user.getUserId())
	);
	log.info("사용자 프로필 업데이트 배치 완료");
}

배치 스케줄러에 대해 더 알아보자❗

💡사용 시 주의 사항

  • @Scheduled가 적용된 메서드는 void 반환 타입을 가져야 하며, 매개변수를 받지 않아야 한다.
  • Application 클래스에 @EnableScheduling 어노테이션을 추가해야한다.

💡주요 속성

  • fixedDelay : 이전 작업 완료 시점으로부터 고정된 시간(밀리초) 이후에 실행
  • fixedRate : 이전 작업 시작 시점으로부터 고정된 시간(밀리초) 간격으로 실행
  • initialDelay : 최초 실행 전 대기 시간(밀리초)
  • cron : 크론 표현식을 사용하여 복잡한 실행 일정을 지정

💡크론 표현식이 뭐지..❓

초 분 시 일 월 요일 [년]
  • '*' : 모든 값
  • ',' : 값 목록 구분
  • '-' : 범위 지정
  • '/' : 증분 값
  • '?' : 특정 값 없음 (일, 요일에서 사용)
  • 'L' : 마지막(일, 요일에서 사용)
  • 'W' : 가장 가까운 평일 (일에서 사용)
  • '#' : N번째 특정 요일(요일에서 사용)

예시) 매우 월요일부터 금요일까지 오전 9시 45분에 실행

0 45 9 ? * MON-FRI

배치 스케줄러의 장점

  • 반복적인 작업을 자동화하여 인력과 시간을 절약한다.
  • 작업의 일관성을 유지한다.
  • 리소스 사용을 최적화하여 시스템 성능을 향상시킨다.

배치 스케줄러의 단점

  • 실시간 데이터 처리나 즉각적인 응답이 필요한 작업에는 적합하지 않다.
  • 미리 정해진 일정에 따라 작업이 실행되므로, 긴급한 작업이나 우선순위 변경에 즉시 대응하기 어렵다.
  • 대량의 작업을 한번에 처리하게 될 경우 많은 시스템 리소스가 필요할 수 있다.

📙구현해보기

1. 모든 레시피 재료들의 TF-IDF 계산

TF-IDF값을 정규화했다.

어떤 경우에 정규화가 필요할까❓

  • 문서 길이에 영향을 받지 않도록 조정해야 할 때
    • 긴 문서는 단어 개수가 많아져 TF 값이 커질 가능성이 있다. -> 이를 보정하려면 L2 정규화(벡터 정규화)를 적용
  • 코사인 유사도 등 벡터 연산을 사용할 때
    • TF-IDF 벡터를 비교할 때 코사인 유사도를 많이 쓰는데, 벡터 크기가 다르면 유사도 비교가 왜곡될 수 있다. -> 정규화를 적용하면 벡터 크기를 1로 맞춰서 비교가 더 공정해진다.

정규화 종류에 대해 알아보자❗

📌 L1 정규화 (값의 절대 합을 1로 만듦)

  • 희소 벡터를 유지하면서 비교하고 싶을 때 사용

📌 L2 정규화 (값의 제곱 합이 1이 되도록 조정)

  • 코사인 유사도를 계산할 때 일반적으로 많이 사용

📌 Min-Max Scaling (0~1 범위로 변환)

  • 머신러닝 모델에서 TF-IDF를 다른 피처와 함께 사용할 때 적용 가능

1-(1) IDF 계산

  • 특성이 전체 레시피에서 얼마나 희소한지

1-(2) 레시피 특성 추출 (레시피 재료)

1-(3) 각 특성에 대해 TF-IDF 계산

  • 해당 레시피에서 각 특성의 중요도를 나타냄

1-(4) 각 레시피의 모든 특성에 대한 TF-IDF 값 저장

MySQL에서 중복 키(Duplicate Key) 문제가 발생했다 . .❓🙄

  • 주요 문제점
    • 'recipe_tfidf' 테이블에 이미 존재하는 'feature'와 'recipe_id' 조합을 다시 삽입하려고 시도
    • 고유 제약 조건 위반
  • 해결방안
    • 삽입하려는 데이터가 이미 테이블에 존재하는지 확인
    • UPSERT 사용: INSERT 대신 'INSERT ... ON DUPLICATE KEY UPDATE' 구문을 사용하여 중복 시 업데이트하도록 변경
    • 고유 키 재검토
    • 대량의 데이터를 처리할 경우, 작은 배치로 나누어 처리하고 각 배치마다 중복을 확인
    • 예외 처리

다음과 같이 UPSERT를 사용한 코드를 작성하여 문제를 해결했다.

@Modifying
@Query(value = """
	INSERT INTO recipe_tfidf (recipe_id, feature, tfidf_value) 
	VALUES (:recipeId, :feature, :value)
	ON DUPLICATE KEY UPDATE 
	tfidf_value = VALUES(tfidf_value)
	""", nativeQuery = true)
@Transactional
void upsertTfIdf(@Param("recipeId") Long recipeId, @Param("feature") String feature, @Param("value") Double value);

2. 사용자 활동을 기반으로 TF-IDF 계산

처음에는 사용자 활동 요소로 레시피 분류, 레시피 메인재료, 레시피 재료를 생각하였다. 그러나 각각에 대해 TF-IDF 값을 계산하여 비교할 것이 아니므로 하나를 택하는게 맞다고 판단하였다.

다음은 TF-IDF 계산에 사용한 사용자의 활동 요소이다.

  • 사용자의 선호/기피 재료
  • 사용자가 북마크한 레시피의 재료
  • 사용자가 좋아요한 피드의 레시피 재료
  • 사용자가 조회한 레시피의 재료

중복 키 문제가 발생했다 . .❓🙄

첫번째 단계인 모든 레시피에 대한 TF-IDF 계산 시에도 중복키 문제가 발생했었다.
이번에는 복합키를 사용하여 문제를 해결했다.

2-(1) 사용자의 선호/기피 재료 조회

2-(2) 북마크한 레시피의 TF-IDF 평균 계산, 피드 좋아요한 레시피의 TF-IDF 평균 계산, 조회한 레시피의 TF-IDF 평균 계산

2-(3) 각 요소에 가중치를 두어 사용자의 TF-IDF 계산

이 벡터값도 L2 정규화 하였다.

3. 코사인 유사도 계산

모든 레시피와 사용자의 TF-IDF 값을 사용하여 코사인 유사도를 계산했다.

코사인 유사도를 계산하여 10개의 레시피를 필터링 하였다.

유사도가 0.0 🤔..❓

사용자 활동 벡터값이 업데이트 되었는데 왜 유사도가 0.0 일까?
사용자 활동 벡터값에 업데이트 된 요소가 선호/기피 재료 뿐이었다.
선호/기피 재료에 대한 가중치를 각각 1, -1로 두었는데, 그 외의 활동 요소을에 따라 벡터값이 업데이트가 되니까 유사도가 정상적으로 계산되었다.

💡결론적으로, 이것은 데이터의 희소성 때문이었다.

중복키 문제를 해결하였는데, 갑자기 중복 문제..❓

레시피 데이터 크롤링 과정에서 동일한 데이터가 수집되어 DB에 접근하여 강제로 레시피 데이터를 삭제 하였는데, 그 과정에서 문제가 발생하였다.
CASCADE로 인해 레시피 ID를 외래키로 갖고 있는 모든 데이터가 삭제 될 줄 알았는데, 직접 DB에 접근하여 데이터를 삭제하였기 때문에 CASCADE가 되지 않았다. 이로 인해 데이터 삭제가 정상적으로 이루어지지 않아서 오류가 발생하였던 것.

또한, 이 문제로 서버를 재빌드 하면서 배치 스케줄러를 적용해 놓았던 사용자 프로필 벡터 계산 메서드가 지정된 시간이 아니었음에도 실행되었고, 당연히 지정된 시간이 아니었기 때문에 postman으로 강제로 실행시켰는데 꼬임 현상으로 중복 문제가 발생하였다.
전혀 문제가 될 것이 아니라고 생각했는데, 왜냐하면 사용자 활동 벡터값을 저장할 때 UPSERT를 사용했기 때문이다.

이를 통해 외래키로 사용하는 데이터를 삭제할 때 이러한 점을 고려해야한다는 것을 다시 느끼게 되었다.⭐

4. GPT API로 2차 필터링

아래와 같이 프롬프트를 작성하였다.

### 현재 날씨 ###
현재 서울의 날씨: °C,
### 사용자 취향 분석 ###
1. 2. 3. 4. 5
### 추천 후보 목록 ###
레시피 ID: 
제목: 
종류: 
소요 시간: 
재료: 
난이도: 
과정 수: 
유사도: 
### 요청 사항 ###
- 추천 후보 목록 중에서 현재 날씨와 사용자 취향 분석을 반영한 상위 3개 요리 선정
- 선정된 상위 3개 요리의 레시피 ID만 리스트 형태로 응답
예시 응답 형식: [1234, 5678, 9012]

여기서 사용자 취향 분석 부분에는 랜덤으로 질문을 전달하여 답변을 넣은 것이다.

  • 예시) 매운 음식을 좋아하나요? -> 매운 음식을 좋아함 / 매운 음식을 싫어함

0개의 댓글