추천 알고리즘은 대표적으로 콘텐츠 기반 필터링 모델과 협업 필터링 모델이 존재한다.
추천받을 사용자와 유사한 사용자가 높은 관심도를 보인 상품을 추천하고 싶었기 때문에, 여기서는 협업 필터링 모델을 사용했다.
협업 필터링 모델도 사용자 기반 추천 모델과 아이템 기반 추천으로 나뉘는데, 위와 같은 이유로 사용자 기반 추천 모델을 사용했다.
사용자 기반 추천 : 비슷한 성향의 사용자들의 그룹화해 그룹이 선호하는 상품을 해당그룹에 속한 사용자게에 추천하는 방식이다.
아이템 기반 추천 : 사용자가 이전에 구매했던 아이템을 기반으로 그 상품과 유사한 다른 상품을 추천하는 방식이다.
사용자 기반 협업 필터링
유사한 사용자의 행동과 선호도를 활용하여 추천하는 기술이다.
사용자 항목 평가를 분석하여 패턴과 유사성을 식별하여 다른 유사한 사용자의 선호도를 기반으로 대상 사용자에게 항목을 추천한다.
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;
}
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에 상품에 대한 상품 등급정보를 추가한다.
...
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
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||)*
...
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);
}
}
}
...
...
List<Item> recommendedItems = itemRecommendations.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(5)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
return recommendedItems;
}