寻找另一半:基于兴趣爱好的相似好友推荐策略

应用场景

在本人负责的一个交友类项目中,实现了一个向用户推荐可能感兴趣的其他用户的功能。推荐的目标是使推荐的用户尽可能相似。系统要求用户设置自己的兴趣爱好标签,因此推荐策略基于这些标签的相似度来进行

兴趣爱好相似度的计算方法

这里主要列举出几种基于距离的方法。

1. Jaccard 相似度

衡量两个集合相似度的指标。它的定义是两个集合交集的大小与并集的大小的比值。Jaccard相似度的值范围在0到1之间,值越接近1表示两个集合越相似。

公式:

J a c c a r d = ∣ A ∩ B ∣ ∣ A ∪ B ∣ Jaccard = \frac{|A\cap B|}{|A \cup B|} Jaccard=ABAB

使用步骤:

  • 将目标用户的兴趣爱好放到一个集合里
  • 将另一个用户的兴趣爱好放到另一个集合里
  • 通过公式计算两者的 Jaccard 相似度
  • 重复上述步骤,计算目标用户跟其他所有用户的Jaccard距离,根据距离降序排序,返回最匹配的若干个用户。

评价

实现简单直观,用户之间的共性越多,差异越小,则越相似。但这种方法没有考虑爱好之间的潜在关联。

2. 余弦相似度

余弦相似度是一种衡量两个非零向量之间角度的度量。它通过计算两个向量之间的夹角的余弦值来评估它们之间的相似性。如果两个向量在多维空间中指向相同的方向,则它们的余弦相似度接近1;如果方向相反,则接近-1;如果正交,则余弦相似度为0。

公式:
c o s i n e _ s i m = A ⋅ B ∣ ∣ A ∣ ∣ ∣ ∣ B ∣ ∣ cosine\_sim = \frac{A·B}{||A|| ||B||} cosine_sim=∣∣A∣∣∣∣B∣∣AB
A A A B B B 为两个向量,做内积在除以二者的模的乘积就是余弦相似度的值。

使用步骤:

  • 将用户的兴趣爱好转成向量
  • 计算目标用户与其他用户之间的余弦相似度
  • 返回余弦相似度最大的用户作为最匹配的结果

评价

这是一种较为适合的方法,爱好在被编码后映射到了高维空间中,相似类型的爱好聚集在一起。余弦值越大,代表两个向量的方向越一致,爱好就越相似,用户之间也就越相似。然而,将爱好编码成向量比较麻烦,编码的质量决定了推荐的质量。

3. 欧式距离

欧式距离是一种常用的度量两个点在多维空间中的直线距离的方法。它基于笛卡尔坐标系中的两点之间的距离公式,适用于多维空间的任务。

公式:
d i s t a n c e ( A , B ) = ∑ i = 1 n a i − b i 2 distance(A, B) = \sqrt{\sum_{i=1}^{n}{a_i - b_i}^2} distance(A,B)=i=1naibi2

评价
衡量两个向量的直线距离,距离越大则越不相似,反之越相似。虽然可用,但在多维空间上,向量的相似度通常用夹角来衡量更好。同样需要将爱好编码成向量。

爱好标签转向量的方法

1. 独热编码

将每个爱好标签转换为一个维度,如果用户拥有这个标签,则对应维度的值为 1,否则为 0。

例如,如果有 30 个爱好标签,则一个用户的爱好向量就是 30 维,如果用户有某个爱好,那么那一维上就设置成 1。

这种编码方式的爱好数量和顺序需要预先设定好,不支持用户自定义标签。适合爱好数量较少的情况,数量太多会导致向量非常稀疏,效果不佳。

2. 连续编码

将若干个爱好标签直接编码成数字(如 1、2、3…)。

这种编码方式简单直接,但可能会引入不必要的顺序关系,即数值相近的标签被假定为相似。

3. 机器学习编码

使用 Word2Vec 或 GloVe 这样的模型来获取每个标签的词向量。这些模型通常已经有训练好的权重模型,可以直接使用。

这种方式能够捕捉到标签间的语义关系,但需要额外的数据处理和模型训练工作。

Jaccard 相似度代码实现

项目中最终选择了 Jaccard 算法实现,毕竟比较简单快速。
我的兴趣爱好是两级的,所以对一级和二级标签分别计算相似度,然后加权。

public class JaccardSimilarityUtil {
    // 计算两个集合的 jaccard 相似度
    public static double jaccardSimilarity(Set<Integer> user_one, Set<Integer> user_two){
        Set<Integer> intersection = new HashSet<>(user_one);
        // 获取两个集合的交集
        intersection.retainAll(user_two);

        // 获取两个集合的并集
        Set<Integer> union = new HashSet<>(user_one);
        union.addAll(user_two);

        return (double) intersection.size() / union.size();

    }

    // 计算一级标签的 jaccard 相似度
    public static double firstLevelJaccardSimilarity(Map<Integer, Set<Integer>> user1Interests, Map<Integer, Set<Integer>> user2Interests) {
        Set<Integer> firstLevel1 = user1Interests.keySet();
        Set<Integer> firstLevel2 = user2Interests.keySet();
        return jaccardSimilarity(firstLevel1, firstLevel2);
    }

    // 计算二级标签的Jaccard相似度
    public static double secondLevelJaccardSimilarity(Map<Integer, Set<Integer>> user1Interests, Map<Integer, Set<Integer>> user2Interests) {
        Set<Integer> combinedFirstLevels = new HashSet<>(user1Interests.keySet());
        combinedFirstLevels.addAll(user2Interests.keySet());

        Set<Integer> secondLevel1 = new HashSet<>();
        Set<Integer> secondLevel2 = new HashSet<>();

        for (Integer firstLevel : combinedFirstLevels) {
            secondLevel1.addAll(user1Interests.getOrDefault(firstLevel, Collections.emptySet()));
            secondLevel2.addAll(user2Interests.getOrDefault(firstLevel, Collections.emptySet()));
        }

        return jaccardSimilarity(secondLevel1, secondLevel2);
    }

    // 计算综合Jaccard相似度
    public static double combinedJaccardSimilarity(Map<Integer, Set<Integer>> user1Interests, Map<Integer, Set<Integer>> user2Interests) {
        double firstLevelSimilarity = firstLevelJaccardSimilarity(user1Interests, user2Interests);
        double secondLevelSimilarity = secondLevelJaccardSimilarity(user1Interests, user2Interests);

        // 可以根据需要调整权重
        return 0.5 * firstLevelSimilarity + 0.5 * secondLevelSimilarity;
    }

    // 计算一个用户与其他多个用户的Jaccard相似度
    public static Map<Long, Double> calculateJaccardSimilarities(
            Map<Integer, Set<Integer>> targetUserInterests,
            Map<Long, Map<Integer, Set<Integer>>> otherUsersInterests) {

        Map<Long, Double> similarities = new HashMap<>();

        for (Map.Entry<Long, Map<Integer, Set<Integer>>> entry : otherUsersInterests.entrySet()) {
            Long userId = entry.getKey();
            Map<Integer, Set<Integer>> userInterests = entry.getValue();

            // 计算综合Jaccard相似度
            double similarity = combinedJaccardSimilarity(targetUserInterests, userInterests);
            similarities.put(userId, similarity);
        }

        return similarities;
    }
    public static void main(String[] args) {
    	// key:value -> 一级爱好:二级爱好
        Map<Integer, Set<Integer>> user1Interests = new HashMap<>();
        user1Interests.put(1, new HashSet<>(Arrays.asList(1, 2, 3)));
        user1Interests.put(2, new HashSet<>(Arrays.asList(4, 5)));

        Map<Integer, Set<Integer>> user2Interests = new HashMap<>();
        user2Interests.put(1, new HashSet<>(Arrays.asList(2, 3, 8)));
        user2Interests.put(2, new HashSet<>(Arrays.asList(4, 5)));
        user2Interests.put(3, new HashSet<>(Arrays.asList(6, 7)));

        double firstLevelSimilarity = firstLevelJaccardSimilarity(user1Interests, user2Interests);
        double secondLevelSimilarity = secondLevelJaccardSimilarity(user1Interests, user2Interests);
        double combinedSimilarity = combinedJaccardSimilarity(user1Interests, user2Interests);

        System.out.println("First Level Jaccard Similarity: " + firstLevelSimilarity);
        System.out.println("Second Level Jaccard Similarity: " + secondLevelSimilarity);
        System.out.println("Combined Jaccard Similarity: " + combinedSimilarity);
    }
}

提升系统响应的优化

在计算相似度的时候需要遍历用户计算,这是比较耗费时间的一个过程,尤其是用户数量很大的情况下,以下是本人尝试的一些小优化。

1. 设置过滤条件减少匹配数量

可以在前端预先设置筛选条件(如地域、年龄、性别等),让用户先做一个预筛选,这样后端就可以减少查找的用户数量,从而减少计算相似度的时间。

2. 添加适当索引和缓存结果

  • 在数据库中创建索引来加速查询速度,特别是对于频繁使用的查询字段。
  • 使用缓存来存储最近的匹配结果,当用户离开后再次进入该页面时使用缓存数据而不是重新计算。

3. 预计算

在低峰时段(如凌晨三点)设置定时任务预先计算出潜在的匹配对象列表,并将结果存储到缓存中,在用户请求时直接返回。

总结

本文探讨了如何在交友应用中利用兴趣爱好标签来推荐相似用户。我们讨论了几种相似度计算方法,包括 Jaccard 相似度、余弦相似度和欧式距离,并详细介绍了每种方法的应用步骤及优缺点。此外,还介绍了几种将爱好标签转化为向量的方法,如独热编码、连续编码以及使用机器学习模型进行编码。

在实际应用中,我们选择了 Jaccard 相似度算法来实现这一功能,因为它简单且易于实现。为了进一步提升用户体验,我们还实施了一些性能优化措施,包括设置过滤条件减少匹配数量、添加索引和缓存结果以及预计算等。

当然,这只是在个人项目中的一些简单尝试,推荐算法博大精深,以上只是讨论了比较简单的基于距离的方法。而且使用的数据也只有用户的兴趣爱好,用户的其他行为信息也是可以用来推荐的,还有很大的优化空间。

  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值