상품 추천 알고리즘 만들기

이수찬·2023년 5월 27일
0

1. 추천 알고리즘 종류

추천 알고리즘은 대표적으로 콘텐츠 기반 필터링 모델과 협업 필터링 모델이 존재한다.

추천받을 사용자와 유사한 사용자가 높은 관심도를 보인 상품을 추천하고 싶었기 때문에, 여기서는 협업 필터링 모델을 사용했다.

협업 필터링 모델도 사용자 기반 추천 모델과 아이템 기반 추천으로 나뉘는데, 위와 같은 이유로 사용자 기반 추천 모델을 사용했다.

  • 사용자 기반 추천 : 비슷한 성향의 사용자들의 그룹화해 그룹이 선호하는 상품을 해당그룹에 속한 사용자게에 추천하는 방식이다.

  • 아이템 기반 추천 : 사용자가 이전에 구매했던 아이템을 기반으로 그 상품과 유사한 다른 상품을 추천하는 방식이다.

2. 사용한 알고리즘

사용자 기반 협업 필터링

  • 유사한 사용자의 행동과 선호도를 활용하여 추천하는 기술이다.

  • 사용자 항목 평가를 분석하여 패턴과 유사성을 식별하여 다른 유사한 사용자의 선호도를 기반으로 대상 사용자에게 항목을 추천한다.

3. 코드로 이해하기

Rating Entity

@Entity
@Getter
public class Rating {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    @Size(min = 0, max = 5)
    private Integer rating;
    
 }
  • 회원은 하나의 상품에 1개의 평가를 달 수 있다.
    (평가 : 0점 ~ 5점)

3-1 모든 사용자의 평가 수집하기

public List<Item> recommendItems(Member member) {

        List<Rating> userRatings = ratingRepository.findByMember(member);

        Map<Member, Map<Item, Integer>> userRatingsMap = new HashMap<>();
        for (Rating rating : ratingRepository.findAll()) {
            Member currentMember = rating.getMember();
            Item currentItem = rating.getItem();
            int currentRating = rating.getRating();

            userRatingsMap.computeIfAbsent(currentMember, k -> new HashMap<>())
                .put(currentItem, currentRating);
        }
        
        ...
        
}
  • 상품 추천 시스템 구축을 위해 가장 먼저 해야할 작업은 관련 데이터를 수집해야 한다.

  • 이를 위해 다른 회원들은 상품을 어떻게 평가했는지 확인한다.

  • UserRatingMap는 회원과 회원이 평가한 상품 - 상품의 등급을 연결하는 Map이다.

  • computeIfAbsent를 통해 currentMember가 userRatingMap에 Key로 존재하는지 확인한 후 존재한다면, currentMember를 새로운 Key로, HashMap은 value로 등록한다.
    값이 존재하면, HashMap에 상품에 대한 상품 등급정보를 추가한다.

3-2. 상품을 추천받을 회원과 다른 회원의 유사도 계산하기

...

        Map<Member, Double> userSimilarities = new HashMap<>();
        for (Member otherUser : userRatingsMap.keySet()) {
            if (!otherUser.equals(member)) {
                double similarity = calculateCosineSimilarity(userRatingsMap.get(member), userRatingsMap.get(otherUser));
                userSimilarities.put(otherUser, similarity);
            }
        }
...
  • 다른 회원이 평가한 상품의 등급이 존재하면, 코사인 유사성 측정을 통해 회원과 다른 회원간의 유사성을 계산한다.
  • 코사인 유사성 : 두 벡터 사이의 각도를 고려해 유사성을 결정한다.
    (회원과 다른 회원이 평가한 등급을 비교해 두 회원의 유사성을 계산한다.)
    (두 벡터의 내적이 크면 유사도가 높다는 것을 이용하여 계산)
  • 만약 회원이 상품을 추천받을 회원과 다른 회원이라면 calculateCosineSimilarity를 통해 유사도를 계산하여 userSimilarities(회원과 상품을 추천받을 회원사이의 유사도를 저정하는 Map)에 넣는다.

calculateCosineSimilarity

private double calculateCosineSimilarity(Map<Item, Integer> ratings1, Map<Item, Integer> ratings2) {
        double dotProduct = 0.0;
        double norm1 = 0.0;
        double norm2 = 0.0;

        for (Item item : ratings1.keySet()) {
            if (ratings2.containsKey(item)) {
                int rating1 = ratings1.get(item);
                int rating2 = ratings2.get(item);

                dotProduct += rating1 * rating2;
                norm1 += rating1 * rating1;
                norm2 += rating2 * rating2;
            }
        }

        if (norm1 == 0.0 || norm2 == 0.0) {
            return 0.0;
        }

        return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
    }
  • double dotProduct = 0.0; // 두 회원의 상품 등급에 대한 내적
    double norm1 = 0.0; // 벡터 A의 크기
    double norm2 = 0.0; // 벡터 B의 크기

  • 만약 같은 상품을 평가한 적이 있다면, 회원들이 평가한 상품 등급의 벡터값을 구한다.
    내적은 벡터 A와 B를 곱하고 더해서 계산하므로, 상품에 대한 두 회원이 평가한 등급을 곱하고 dotProduct에 더하여 계산한다.

  • norm1 == 0.0 || norm2 == 0.0를 통해 코사인 유사도를 계산할 수 없는 경우 return한다.
    (상품에 대한 등급을 매개지 않으면 유사도를 계산할 수 없기에)

코사인 유사도 공식 : cosine_similarity(A, B) = (A · B) / (||A|| ||B||)*

  • 코사인 유사도는 등급의 내적을 norm1, norm2의 제곱근의 곱으로 나누어 계산한다.
    결과는 두 사용자 간의 코사인 유사성 값으로 반환한다.
    (코사인 유사도는 두 벡터간의 유사도를 결정하기 위해 사용하는 척도이다.)

3-3. 상품 추천 목록 생성하기

...

        Map<Item, Double> itemRecommendations = new HashMap<>();
        for (Member otherUser : userSimilarities.keySet()) {
            double similarity = userSimilarities.get(otherUser);

            for (Item item : userRatingsMap.get(otherUser).keySet()) {
                if (!userRatingsMap.get(member).containsKey(item)) { // 추천 받을 회원이 평가한 상품은 제외
                    double rating = userRatingsMap.get(otherUser).get(item);
                    itemRecommendations.merge(item, rating * similarity, Double::sum);
                }
            }
        }
...
  • 상품을 추천받을 회원과 유사한 회원을 통해 상품 추천 Map을 생성한다.
  • 상품 추천 Map에 상품을 넣을 때는 추천 받을 회원이 평가한 상품은 제외하며, 유사한 회원과의 유사도를 통해 상품 추천 등급을 계산한다.

3-4. 상위 등급의 상품 5개 추천하기

...

        List<Item> recommendedItems = itemRecommendations.entrySet().stream()
            .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
            .limit(5)
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());

        return recommendedItems;
}
  • 상품 추천 Map에서 상품 추천 등급이 높은 순으로 5개를 뽑아 반환한다.
    (상위 등급순으로 5개를 추천한다.)

참고자료
https://deepdaiv.oopy.io/articles/1

0개의 댓글