협업필터링 아이템-기반 추천 (슬로프 원 알고리즘)
on 조잘조잘
기존 주절주절의 추천 방식은 협업필터링을 이용한 사용자 기반(User-basd) 추천이었다. 협업필터링 관련 글 보러가기
하지만 이번에 아이템 기반(Item-based)로 바꾸게 되었다. 그리고 머하웃 라이브러리를 이용하지 않고 직접 알고리즘을 구현해 사용하고 있다. 왜 이런 선택을 했을까?
왜 아이템 기반으로 바꾸게 되었나?
먼저, 사용자 기반으로 흘러가게 되는 방식은 한 사용자가 많은 선호도를 남겼을 때 다른 사용자와의 유사도를 더욱 정확하게 측정할 수 있다. 반대로 이야기하면 해당 사용자가 적은 양의 선호도를 남긴다면 유사도를 측정하기가 힘들다는 것이다.
위 상황이라면 과연 새로운 유저와 비슷한 취향을 가진 사용자는 누구일까? 새로운 유저가 B 혹은 C 상품에 대해 선호도를 남길 때까지 애매하다.
하지만 반대로 상품 기반으로 측정한다면 A 상품을 좋아한다면 D 상품도 좋아하는 것을 볼 수 있다. 따라서 새로운 유저에게 바로 D 상품을 추천할 수 있게 된다.
주절주절은 서비스 특성상 상품의 추가는 빈번하지 않다. 유저의 추가는 빈번하다고 예상하기에 조금의 데이터로도 유저가 추천을 받을 수 있는 상황을 만드는 것이 중요하다고 판단했다. 이러한 이유때문에 주절주절은 사용자 기반 추천에서 아이템 기반 추천으로 변경하게 되었다.
왜 알고리즘을 직접 구현했나?
이전에 주절주절은 알고리즘을 위해 머하웃 라이브러리를 사용했다. 라이브러리에 대해 미숙해서인지 많은 에러를 직면했다… 먼저, 매번 커넥션을 가져가는 상황도 존재했고(프록시를 만들어 해결) 다른 라이브러리(유레카)와 의존성이 충돌하는 경우도 발생했다. 또한, 몇몇 유저에게는 추천이 잘 되지 못하는 상황도 발생했다… 마지막으로 알고리즘에 대해 자세히 모르다보니 최적화를 어떻게 진행할 지 감을 잡기가 힘들었다.
이러한 모든 이유때문에 직접 알고리즘을 구현해보는 것도 좋을 것 같다는 판단 하에 직접 만들어보게 되었다. 사용한 알고리즘은 슬로프 원 알고리즘이다. 간단하게 식을 보고 어떤 방식으로 알고리즘이 흘러가는지 살펴보자.
슬로프 원 알고리즘
편차 구하기
예측 점수 구하기
자, 이제 그림으로 슬로프 원 알고리즘을 살펴보자.
먼저, A~D 까지의 주류가 존재한다. 여기서 새로운 유저의 A의 예상 선호도를 측정해보자.
주류 A 를 기준으로 각 주류의 선호도 편차를 구하고 모든 유저의 평균을 구한다.
A-B 주류 선호도 편차 평균 구하기
유저 ‘가’ 입장에서 A-B 주류 선호도 편차는 4.5 - 2.0 = 2.5 이다.
유저 ‘나’ 입장에서 A-B 주류 선호도 편차는 3.5 - 5.0 = -1.5 이다.
이후 주류 선호도 편차를 더하고 계산된 유저 총 수를 나눈다. (2.5 - 1.5) / 2 = 0.5
A-C 주류 선호도 편차 평균 구하기
유저 ‘가’ 입장에서 A-C 주류 선호도 편차는 4.5 - 5.0 = -0.5 이다.
유저 ‘나’ 입장에서 C 에 대한 평가가 없으니 패스한다.
이후 주류 선호도 편차를 더하고 계산된 유저 총 수를 나눈다. (-0.5 ) / 1 = -0.5
A-D 주류 선호도 편차 평균 구하기
유저 ‘가’ 입장에서 A-D 주류 선호도 편차는 4.5 - 4.5 = 0 이다.
유저 ‘나’ 입장에서 A-D 주류 선호도 편차는 3.5 - 2.5 = 1.0 이다.
이후 주류 선호도 편차를 더하고 계산된 유저 총 수를 나눈다. 1 / 2 = 0.5
자 이제 예상 선호도를 구해보자.
(인원 수(새로운 유저의 선호도 + 편차 평균) + … ) / (평균을 낸 인원수 + … )
식을 진행해보자. 여기서 새로운 유저는 C에 대한 선호도가 없으니 패스한다.
(A-B ) + (A-D) / (A-B에 대한 평가 남긴 인원 수) + (A-D에 대한 평가 남긴 인원 수)
( 2(4.5 + 0.5) + 2(4.5 + 0.5) ) / (2 + 2) = ( 10 + 10 ) / 4 = 5
새로운 유저에게 A 주류에 대한 예상 선호도는 5.0 점이 되는 것이다.
자바 코드로 구현하기
자바 코드로 구현할 때 생각하는 구조는 다음과 같다.
먼저, 주류 선호도 편차를 구하기 위한 데이터 세팅이다.
또한 이 데이터 세팅을 하면서 나중에 편하게 계산할 수 있도록 유저 별 데이터 세팅도 같이 해준다.
public class DataMatrix {
private final Map<Long, Map<Long, ItemCounter>> matrix;
private final Map<Long, Map<Long, Double>> dataByMember;
public DataMatrix(List<DataModel> dataModel) {
this.dataByMember = new HashMap<>();
this.matrix = new HashMap<>();
prepareMatrix(dataModel);
}
private void prepareMatrix(List<DataModel> dataModel) {
for (DataModel model : dataModel) {
final Long memberId = model.getMemberId();
final Map<Long, Double> itemByMember = dataByMember
.computeIfAbsent(memberId, id -> new HashMap<>());
for (Entry<Long, Double> itemPreference : itemByMember.entrySet()) {
final Long itemId = itemPreference.getKey();
final Double preference = itemPreference.getValue();
if(itemId.equals(model.getItemId())) continue;
final Map<Long, ItemCounter> primaryMap =
matrix.computeIfAbsent(model.getItemId(), id -> new HashMap<>());
final Map<Long, ItemCounter> secondaryMap =
matrix.computeIfAbsent(itemId, id -> new HashMap<>());
primaryMap.computeIfAbsent(itemId, id -> new ItemCounter()).addSum(model.getPreference() - preference);
secondaryMap.computeIfAbsent(model.getItemId(), id -> new ItemCounter()).addSum(preference - model.getPreference());
}
itemByMember.put(model.getItemId(), model.getPreference());
}
}
public Map<Long, Map<Long, ItemCounter>> getMatrix() {
return matrix;
}
public Map<Long, Double> getDataByMember(Long id) {
return dataByMember.getOrDefault(id, new HashMap<>());
}
}
이후, 추천해야 할 유저의 아이디가 온다면 위 데이터를 가지고 추천을 해준다.
public class Recommender {
public List<RecommendationResponse> recommend(DataMatrix dataMatrix, Long memberId, double minPreference) {
final Map<Long, Map<Long, ItemCounter>> matrix = dataMatrix.getMatrix();
final Map<Long, Double> dataByMember = dataMatrix.getDataByMember(memberId);
final List<RecommendationResponse> recommendItems = new ArrayList<>();
for (Entry<Long, Map<Long, ItemCounter>> matrixEntry : matrix.entrySet()) {
final Long primaryItemId = matrixEntry.getKey();
final Map<Long, ItemCounter> matrixValue = matrixEntry.getValue();
if(dataByMember.containsKey(primaryItemId)) continue;
double sumValue = 0.0;
long count = 0;
for (Entry<Long, Double> itemPreference : dataByMember.entrySet()) {
final Long itemId = itemPreference.getKey();
final Double preference = itemPreference.getValue();
final ItemCounter itemCounter = matrixValue.get(itemId);
final double deviation = itemCounter.getDeviation();
sumValue += (preference + deviation) * itemCounter.getCount();
count += itemCounter.getCount();
}
final double expectedPreference = sumValue / count;
if(expectedPreference >= minPreference) {
recommendItems.add(new RecommendationResponse(primaryItemId, expectedPreference));
}
}
return recommendItems;
}
}
이것으로 추천 알고리즘 작성이 완성되었다.
주절주절의 예상 선호도 모습입니다!! 읽어주셔서 감사합니다 =]