
K-means聚类推荐算法
K-means 聚类算法在图书推荐系统中的实现
1. K-means 算法简介
K-means 是一种经典的聚类算法,它的目标是将 n 个数据点划分为 k 个簇,使得每个数据点属于离它最近的簇(即最近的均值)。这是一种常用的无监督学习方法,广泛应用于数据挖掘、模式识别和推荐系统等领域。
1.1 算法基本原理
K-means 算法的基本步骤如下:
- 初始化:随机选择 k 个数据点作为初始簇中心
- 分配:将每个数据点分配到距离最近的簇中心所代表的簇
- 更新:重新计算每个簇的中心点(即簇内所有点的平均值)
- 迭代:重复步骤 2 和 3,直到簇的分配不再变化或达到最大迭代次数
1.2 算法特点
-
优点:
- 算法简单,易于实现
- 计算效率高,适合处理大规模数据
- 对球状簇效果较好
-
缺点:
- 需要预先指定簇的数量 k
- 对初始簇中心的选择敏感
- 容易陷入局部最优解
- 对非球状的簇效果不佳
2. 在图书推荐系统中的应用
在图书推荐系统中,我们可以利用 K-means 算法对图书进行聚类,然后基于用户的历史行为,推荐与其偏好相符的图书。
2.1 特征提取
首先,我们需要为每本图书提取特征向量,这些特征包括:
- 类别向量化:将图书类别(如文学、社科、教育等)转换为数值
- 价格归一化:将图书价格标准化到 0-1 范围内
- 用户行为分数:根据用户的浏览、加入购物车、购买等行为计算的分数
public double[] extractFeatures(Product product, List<UserBehavior> behaviors) {
double[] features = new double[3];
// 类别向量化
features[0] = encodeCategoryVector(product.getProductType());
// 价格归一化
features[1] = normalizePrice(product.getOutPrice());
// 用户行为分数
features[2] = calculateBehaviorScore(behaviors);
return features;
}
2.1.1 类别向量化
类别是一种文本特征,我们需要将其转换为数值才能用于聚类算法。这里采用了一种简单的哈希编码方式:
private double encodeCategoryVector(String category) {
// 简单的类别编码,实际应用中可以使用更复杂的编码方式
return category.hashCode() % 100 / 100.0;
}
2.1.2 价格归一化
价格范围可能差异很大,归一化可以使特征的权重更加平衡:
private double normalizePrice(Double price) {
// 假设价格范围在0-1000之间
return Math.min(price / 1000.0, 1.0);
}
2.1.3 用户行为分数
用户行为是重要的特征,不同行为有不同的权重:
private double calculateBehaviorScore(List<UserBehavior> behaviors) {
if (behaviors == null || behaviors.isEmpty()) {
return 0.0;
}
double score = 0.0;
for (UserBehavior behavior : behaviors) {
switch (behavior.getBehaviorType()) {
case "buy":
score += 1.0; // 购买权重最高
break;
case "cart":
score += 0.5; // 加入购物车次之
break;
case "view":
score += 0.2; // 浏览权重最低
break;
}
}
return Math.min(score / 10.0, 1.0); // 归一化到0-1之间
}
2.2 K-means 聚类实现
有了特征向量后,我们可以实现 K-means 聚类算法:
public KMeansResult kmeansCluster(List<BookFeature> books) {
if (books == null || books.isEmpty()) {
return new KMeansResult(new ArrayList<>(), new double[0][0]);
}
// 初始化K个中心点
double[][] centroids = initializeCentroids(books, K);
boolean changed = true;
int iterations = 0;
while(changed && iterations < MAX_ITERATIONS) {
changed = false;
iterations++;
// 分配阶段
for(BookFeature book : books) {
int oldClusterId = book.getClusterId();
int newClusterId = assignToCluster(book.getFeatures(), centroids);
if(oldClusterId != newClusterId) {
book.setClusterId(newClusterId);
changed = true;
}
}
// 更新中心点
updateCentroids(books, centroids);
}
return new KMeansResult(books, centroids);
}
2.2.1 初始化聚类中心
初始化聚类中心是 K-means 算法的重要步骤,这里采用随机选择的方式:
private double[][] initializeCentroids(List<BookFeature> books, int k) {
double[][] centroids = new double[k][books.get(0).getFeatures().length];
// 随机选择k个点作为初始中心点
for (int i = 0; i < k; i++) {
int randomIndex = random.nextInt(books.size());
centroids[i] = books.get(randomIndex).getFeatures().clone();
}
return centroids;
}
2.2.2 分配数据点到最近的聚类
计算每个数据点到各个聚类中心的距离,并分配到最近的聚类:
private int assignToCluster(double[] features, double[][] centroids) {
int closestCentroid = 0;
double minDistance = Double.MAX_VALUE;
for (int i = 0; i < centroids.length; i++) {
double distance = distance(features, centroids[i]);
if (distance < minDistance) {
minDistance = distance;
closestCentroid = i;
}
}
return closestCentroid;
}
private double distance(double[] p1, double[] p2) {
double sum = 0;
for(int i = 0; i < p1.length; i++) {
sum += Math.pow(p1[i] - p2[i], 2);
}
return Math.sqrt(sum);
}
2.2.3 更新聚类中心
根据分配到各个聚类的数据点,重新计算聚类中心:
private void updateCentroids(List<BookFeature> books, double[][] centroids) {
int[] clusterSizes = new int[centroids.length];
double[][] newCentroids = new double[centroids.length][centroids[0].length];
// 计算每个聚类的总和
for (BookFeature book : books) {
int clusterId = book.getClusterId();
clusterSizes[clusterId]++;
for (int i = 0; i < book.getFeatures().length; i++) {
newCentroids[clusterId][i] += book.getFeatures()[i];
}
}
// 计算新的中心点
for (int i = 0; i < centroids.length; i++) {
if (clusterSizes[i] > 0) {
for (int j = 0; j < centroids[i].length; j++) {
centroids[i][j] = newCentroids[i][j] / clusterSizes[i];
}
}
}
}
3. 推荐系统的实现
有了聚类结果后,我们需要基于用户的行为历史,找出用户偏好的聚类,并从这些聚类中推荐图书。
3.1 获取用户偏好的聚类
private List<Integer> getUserPreferredClusters(Long userId, KMeansResult result) {
// 获取用户最近的行为数据
List<UserBehavior> recentBehaviors = userBehaviorDao.findRecentByUserId(userId, 10);
if (recentBehaviors.isEmpty()) {
// 如果没有最近行为,则使用所有行为
recentBehaviors = userBehaviorDao.findByUserId(userId);
}
// 统计用户偏好的聚类
Map<Integer, Integer> clusterCount = new HashMap<>();
for (UserBehavior behavior : recentBehaviors) {
Long behaviorProductId = behavior.getProductId();
for (BookFeature feature : result.getBookFeatures()) {
if (feature.getProductId().longValue() == behaviorProductId) {
int clusterId = feature.getClusterId();
clusterCount.put(clusterId, clusterCount.getOrDefault(clusterId, 0) + 1);
}
}
}
// 返回出现次数最多的前3个聚类
return clusterCount.entrySet().stream()
.sorted(Map.Entry.<Integer, Integer>comparingByValue().reversed())
.limit(3)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
3.2 从偏好聚类中推荐图书
private List<Product> getRecommendedBooks(List<Integer> preferredClusters,
KMeansResult result,
List<Product> allProducts) {
if (preferredClusters.isEmpty()) {
return Collections.emptyList();
}
// 从偏好聚类中选择商品
List<Integer> recommendedProductIds = result.getBookFeatures().stream()
.filter(feature -> preferredClusters.contains(feature.getClusterId()))
.map(BookFeature::getProductId)
.collect(Collectors.toList());
if (recommendedProductIds.isEmpty()) {
return Collections.emptyList();
}
// 随机打乱,增加推荐多样性
Collections.shuffle(recommendedProductIds);
// 返回前10个推荐商品
return recommendedProductIds.stream()
.limit(10)
.map(productId -> allProducts.stream()
.filter(p -> p.getProductId().equals(productId))
.findFirst()
.orElse(null))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
3.3 冷启动问题的处理
当用户没有足够的行为数据,或者推荐结果为空时,我们需要处理冷启动问题:
public List<Product> getRecommendations(Long userId) {
// ... 前面的代码省略 ...
// 从相似聚类中获取推荐图书
List<Product> recommendations = getRecommendedBooks(userPreferredClusters, result, allProducts);
// 确保返回值不为空,避免触发冷启动
if (recommendations.isEmpty() && !allProducts.isEmpty()) {
// 如果推荐为空,随机选择几个商品
Collections.shuffle(allProducts);
recommendations = allProducts.stream()
.limit(Math.min(8, allProducts.size()))
.collect(Collectors.toList());
}
return recommendations;
}
4. 推荐系统完整流程
推荐系统的完整流程如下:
- 获取数据:获取所有上架商品和用户的行为数据
- 特征提取:为每本图书提取特征向量
- 聚类:使用 K-means 算法对图书进行聚类
- 用户偏好分析:根据用户的历史行为,找出用户偏好的聚类
- 推荐生成:从用户偏好的聚类中选择图书进行推荐
- 冷启动处理:处理新用户或推荐结果为空的情况
public List<Product> getRecommendations(Long userId) {
// 1. 获取数据
List<Product> allProducts = productDao.selectAllSale();
List<UserBehavior> userBehaviors = userBehaviorDao.findByUserId(userId);
// 2. 特征提取
List<BookFeature> bookFeatures = allProducts.stream()
.map(product -> {
final Long productIdLong = product.getProductId().longValue();
List<UserBehavior> productBehaviors = userBehaviors.stream()
.filter(behavior -> behavior.getProductId().equals(productIdLong))
.collect(Collectors.toList());
double[] features = kMeansService.extractFeatures(product, productBehaviors);
return new BookFeature(product.getProductId(), features, -1);
})
.collect(Collectors.toList());
// 3. 聚类
KMeansResult result = kMeansService.kmeansCluster(bookFeatures);
// 4. 用户偏好分析
List<Integer> userPreferredClusters = getUserPreferredClusters(userId, result);
// 处理偏好聚类为空的情况
if (userPreferredClusters.isEmpty()) {
Map<Integer, Integer> clusterStats = new HashMap<>();
for (BookFeature feature : result.getBookFeatures()) {
clusterStats.put(feature.getClusterId(),
clusterStats.getOrDefault(feature.getClusterId(), 0) + 1);
}
if (!clusterStats.isEmpty()) {
int largestCluster = clusterStats.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(0);
userPreferredClusters.add(largestCluster);
}
}
// 5. 推荐生成
List<Product> recommendations = getRecommendedBooks(userPreferredClusters, result, allProducts);
// 6. 冷启动处理
if (recommendations.isEmpty() && !allProducts.isEmpty()) {
Collections.shuffle(allProducts);
recommendations = allProducts.stream()
.limit(Math.min(8, allProducts.size()))
.collect(Collectors.toList());
}
return recommendations;
}
5. 系统优化与改进方向
虽然基本的 K-means 推荐系统已经实现,但仍有多个方面可以优化:
-
特征提取优化:
- 增加更多特征,如图书的评分、出版时间、作者信息等
- 使用更复杂的类别编码方式,如 Word2Vec 或 One-Hot 编码
-
聚类算法改进:
- 使用 K-means++ 初始化方法,提高聚类效果
- 尝试其他聚类算法,如 DBSCAN、层次聚类等
-
推荐策略优化:
- 结合协同过滤等其他推荐算法
- 加入时间衰减因子,更重视用户最近的行为
- 增加推荐结果的多样性
-
性能优化:
- 使用缓存存储聚类结果
- 定期更新聚类结果,而不是每次请求都重新计算
6. 总结
本文介绍了 K-means 聚类算法在图书推荐系统中的应用。通过对图书特征的提取和聚类,系统能够根据用户的历史行为,推荐与其偏好相似的图书。虽然基础实现已经完成,但在实际应用中,还需要根据具体场景进行优化和改进。
K-means 算法简单而高效,特别适合处理大规模数据,但也有其局限性。在实际应用中,可以考虑结合其他推荐算法,以提高推荐的准确性和多样性。
参考资料
- MacQueen, J. (1967). "Some methods for classification and analysis of multivariate observations". Proceedings of the Fifth Berkeley Symposium on Mathematical Statistics and Probability. 1: 281–297.
- Arthur, D., & Vassilvitskii, S. (2007). "k-means++: The advantages of careful seeding". Proceedings of the eighteenth annual ACM-SIAM symposium on Discrete algorithms. Society for Industrial and Applied Mathematics Philadelphia, PA, USA. pp. 1027–1035.
- 《推荐系统实践》,项亮著,人民邮电出版社
- 《机器学习》,周志华著,清华大学出版社
- 感谢你赐予我前进的力量