微信小程序实现用户心动模式

心动模式

必须使用hbuilderx 3.4.8-alpha及以上

安装插件

由于ECharts官方没有对uni-app的支持,所以我们需要安装第三方插件来使用ECharts

echarts插件下载地址

下载插件并导入HBuilder

image-20231106190632444

安装echarts

使用npm下载:

npm install echarts

代码实现

index.vue

详情参见代码注释

<template>
	<!-- 使用组件 -->
	<view><l-echart ref="chart"></l-echart></view>
</template>

<script>
	import * as echarts from 'echarts' //导入包

	export default {
		data() {
			return {
				option: { //指定图表的配置项和数据
					series: [{ //图表属性
						type: 'graph', //图表类型
						layout: 'force', //采用力引导布局
						edgeLabel: {
							show: false //不显示标签
						},
						force: {
							repulsion: 150 //节点间的斥力,数越大则节点间距越大
						},
						data: [] //节点数据
					}]
				},
			};
		},
		mounted() {
			var width = uni.getWindowInfo().screenWidth; //获取屏幕宽度
			var height = uni.getWindowInfo().screenHeight; //获取屏幕高度
			this.$refs.chart.resize({   //设置画布大小
				width: width,
				height: height
			});
			this.$refs.chart.init(echarts, chart => { //初始化图表
				chart.setOption(this.option); //初始化图表数据
				chart.showLoading(); //加载数据时的加载动画,以免页面卡死
				uni.request({ //向后端发起请求
					url: 'http://localhost:8080/users/data', //请求地址
					method: 'GET', //请求方法
					timeout: 60000, //超时时间,单位为毫秒。默认值为 60000
					dataType: 'json', //返回的数据为 JSON,返回后会对返回的数据进行一次 JSON.parse
					success(res) { // 成功回调函数
						var jsonObj = JSON.parse(JSON.stringify(res.data));  //解析得到的JSON数据
						for (var i = 0; i < jsonObj.length; i++) {  //定义头像大小
							jsonObj[i].symbolSize = jsonObj[i].symbolSize * width * 0.25;
						};
						chart.hideLoading(); //停止加载动画
						chart.setOption({ //更新数据
							series: [{
								force: {
									repulsion: width
								},
								data: jsonObj
							}]
						});
					},
					fail(err) { // 失败回调函数
						chart.hideLoading(); //停止加载动画
						uni.showToast({ //提示加载失败
							title: '请稍后重试!',
							icon: 'error' //图标:错误
						});
					}
				});
				chart.on('click', function(params) { //绑定点击事件
					uni.showModal({ //弹窗
						title: '跳转至用户主页', //标题
						content: 'id : ' + params.dataIndex, //弹窗内容
						cancelText: '再想想', //取消按钮的文字,默认为"取消"
						confirmText: '我来啦', //确定按钮的文字,默认为"确定"
						success(res) { //弹窗成功的回调函数
							if (res.confirm) { //点击确定
								uni.navigateTo({ //跳转页面
									url: '/pages/user/user?id=' + params
										.dataIndex, //页面路径,参数
									fail: function(res) { // 跳转失败
										console.log('跳转失败')
									}
								});
							}
						}
					})
				});
			});
		},
	}
</script>

<style>
</style>

当前效果如下:

image-20231106212002768

模拟后端数据

  1. 新建SpringBoot项目(这里使用Idea)

  2. 在application.yml文件中配置端口号:

    image-20231106212323515

  3. 添加实体类UserData:

    使用了lmbok的@Data注解,用于自动添加get,set方法

    只需在pom.xml文件内添加其依赖即可

    maven官方依赖地址:https://mvnrepository.com/artifact/org.projectlombok/lombok

    @Data
    public class UserData {
        private String id;
        private String symbol;
        private Double symbolSize;
    
        public UserData(String id, String symbol, Double symbolSize){
            this.id = id;
            this.symbol = symbol;
            this.symbolSize = symbolSize;
        }
    }
    
  4. 添加controller类的UserController文件:

    这里直接在controller层写完全部逻辑

    @RestController 声明为Controller且返回JSON格式数据

    @Slf4j lombok下的日志工具

    @RequestMapping(“/users”) 需要请求路径为 /users

    这里使用了GSon工具来将String数据转换为JSON数据,由于SpringBoot支持了GSon,所以我们只需要在pom.xml文件中引入其依赖即可

    maven官方依赖地址:https://mvnrepository.com/artifact/com.google.code.gson/gson

@RestController
@Slf4j
@RequestMapping("/users")
public class UserController {
    @GetMapping("/data")
    public UserData[] getData() {
        log.info("接到请求");
        // 模拟已经从数据库中获取到数据
        UserData[] data = new UserData[6];
        data[0] = new UserData("1", "image://https://ts1.cn.mm.bing.net/th?id=OIP-C.8PA76m8zwAMnRUfipfWA1AHaNK&w=120&h=185&c=8&rs=1&qlt=90&o=6&dpr=1.3&pid=3.1&rm=2", 1.0);
        data[1] = new UserData("2", "image://https://ts1.cn.mm.bing.net/th?id=OIP-C.8PA76m8zwAMnRUfipfWA1AHaNK&w=120&h=185&c=8&rs=1&qlt=90&o=6&dpr=1.3&pid=3.1&rm=2", 0.8);
        data[2] = new UserData("3", "image://https://ts1.cn.mm.bing.net/th?id=OIP-C.8PA76m8zwAMnRUfipfWA1AHaNK&w=120&h=185&c=8&rs=1&qlt=90&o=6&dpr=1.3&pid=3.1&rm=2", 0.55);
        data[3] = new UserData("4", "image://https://ts1.cn.mm.bing.net/th?id=OIP-C.8PA76m8zwAMnRUfipfWA1AHaNK&w=120&h=185&c=8&rs=1&qlt=90&o=6&dpr=1.3&pid=3.1&rm=2", 0.46);
        data[4] = new UserData("5", "image://https://ts1.cn.mm.bing.net/th?id=OIP-C.8PA76m8zwAMnRUfipfWA1AHaNK&w=120&h=185&c=8&rs=1&qlt=90&o=6&dpr=1.3&pid=3.1&rm=2", 0.22);
        data[5] = new UserData("6", "image://https://ts1.cn.mm.bing.net/th?id=OIP-C.8PA76m8zwAMnRUfipfWA1AHaNK&w=120&h=185&c=8&rs=1&qlt=90&o=6&dpr=1.3&pid=3.1&rm=2", 0.13);
        log.info("数据已返回:"+ Arrays.toString(data));
        return data;
    }
}

效果图

后端接收到前端发起的请求并返回:

image-20231106215016500

前端接收到后端返回的数据并成功展示:

image-20231106214936623

点击头像可以跳转页面:

image-20231106215217242

image-20231106215238963

余弦相似度算法

这里采用的是 余弦相似度算法 ,直接用即可,无需理解

余弦相似度算法详解: https://blog.csdn.net/zz_dd_yy/article/details/51926305

  1. pom.xml引入依赖

           <!--结合操作工具包-->
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.5</version>
            </dependency>
            <!--bean实体注解工具包-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>
            <!--汉语言包,主要用于分词-->
            <dependency>
                <groupId>com.hankcs</groupId>
                <artifactId>hanlp</artifactId>
                <version>portable-1.8.4</version>
            </dependency>
    
  2. Tokenizer 分词工具类

    package com.example.commoms;
    
    import com.example.dao.Word;
    import com.hankcs.hanlp.HanLP;
    import com.hankcs.hanlp.seg.common.Term;
    
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class Tokenizer {
        /**
         * 分词
         */
        public static List<Word> segment(String sentence) {
    
            //1、 采用HanLP中文自然语言处理中标准分词进行分词
            List<Term> termList = HanLP.segment(sentence);
    
            //上面控制台打印信息就是这里输出的
            System.out.println(termList.toString());
    
            //2、重新封装到Word对象中(term.word代表分词后的词语,term.nature代表改词的词性)
            return termList.stream().map(term -> new Word(term.word, term.nature.toString())).collect(Collectors.toList());
        }
    }
    
    
  3. Word 封装分词结果

    package com.example.dao;
    
    import lombok.Data;
    
    import java.util.Objects;
    
    /**
     * 封装分词结果
     */
    @Data
    public class Word implements Comparable {
    
        // 词名
        private String name;
        // 词性
        private String pos;
        // 权重,用于词向量分析
        private Float weight;
    
        public Word(String name, String pos) {
            this.name = name;
            this.pos = pos;
        }
    
        @Override
        public int hashCode() {
            return Objects.hashCode(this.name);
        }
    
        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final Word other = (Word) obj;
            return Objects.equals(this.name, other.name);
        }
    
        @Override
        public String toString() {
            StringBuilder str = new StringBuilder();
            if (name != null) {
                str.append(name);
            }
            if (pos != null) {
                str.append("/").append(pos);
            }
    
            return str.toString();
        }
    
        @Override
        public int compareTo(Object o) {
            if (this == o) {
                return 0;
            }
            if (this.name == null) {
                return -1;
            }
            if (o == null) {
                return 1;
            }
            if (!(o instanceof Word)) {
                return 1;
            }
            String t = ((Word) o).getName();
            if (t == null) {
                return 1;
            }
            return this.name.compareTo(t);
        }
    }
    
  4. CosineSimilarity 相似率具体实现工具类

    package com.example.commoms;
    
    
    
    import com.example.dao.Word;
    import org.apache.commons.lang3.StringUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.util.CollectionUtils;
    
    import java.math.BigDecimal;
    import java.util.*;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * 判定方式:余弦相似度,通过计算两个向量的夹角余弦值来评估他们的相似度 余弦夹角原理: 向量a=(x1,y1),向量b=(x2,y2) similarity=a.b/|a|*|b| a.b=x1x2+y1y2
     * |a|=根号[(x1)^2+(y1)^2],|b|=根号[(x2)^2+(y2)^2]
     */
    public class CosineSimilarity {
        protected static final Logger LOGGER = LoggerFactory.getLogger(CosineSimilarity.class);
    
        /**
         * 1、计算两个字符串的相似度
         */
        public static double getSimilarity(String text1, String text2) {
    
            //如果wei空,或者字符长度为0,则代表完全相同
            if (StringUtils.isBlank(text1) && StringUtils.isBlank(text2)) {
                return 1.0;
            }
            //如果一个为0或者空,一个不为,那说明完全不相似
            if (StringUtils.isBlank(text1) || StringUtils.isBlank(text2)) {
                return 0.0;
            }
            //这个代表如果两个字符串相等那当然返回1了(这个我为了让它也分词计算一下,所以注释掉了)
    //        if (text1.equalsIgnoreCase(text2)) {
    //            return 1.0;
    //        }
            //第一步:进行分词
            List<Word> words1 = Tokenizer.segment(text1);
            List<Word> words2 = Tokenizer.segment(text2);
    
            return getSimilarity(words1, words2);
        }
    
        /**
         * 2、对于计算出的相似度保留小数点后六位
         */
        public static double getSimilarity(List<Word> words1, List<Word> words2) {
    
            double score = getSimilarityImpl(words1, words2);
    
            //(int) (score * 1000000 + 0.5)其实代表保留小数点后六位 ,因为1034234.213强制转换不就是1034234。对于强制转换添加0.5就等于四舍五入
            score = (int) (score * 1000000 + 0.5) / (double) 1000000;
    
            return score;
        }
    
        /**
         * 文本相似度计算 判定方式:余弦相似度,通过计算两个向量的夹角余弦值来评估他们的相似度 余弦夹角原理: 向量a=(x1,y1),向量b=(x2,y2) similarity=a.b/|a|*|b| a.b=x1x2+y1y2
         * |a|=根号[(x1)^2+(y1)^2],|b|=根号[(x2)^2+(y2)^2]
         */
        public static double getSimilarityImpl(List<Word> words1, List<Word> words2) {
    
            // 向每一个Word对象的属性都注入weight(权重)属性值
            taggingWeightByFrequency(words1, words2);
    
            //第二步:计算词频
            //通过上一步让每个Word对象都有权重值,那么在封装到map中(key是词,value是该词出现的次数(即权重))
            Map<String, Float> weightMap1 = getFastSearchMap(words1);
            Map<String, Float> weightMap2 = getFastSearchMap(words2);
    
            //将所有词都装入set容器中
            Set<Word> words = new HashSet<>();
            words.addAll(words1);
            words.addAll(words2);
    
            AtomicFloat ab = new AtomicFloat();// a.b
            AtomicFloat aa = new AtomicFloat();// |a|的平方
            AtomicFloat bb = new AtomicFloat();// |b|的平方
    
            // 第三步:写出词频向量,后进行计算
            words.parallelStream().forEach(word -> {
                //看同一词在a、b两个集合出现的此次
                Float x1 = weightMap1.get(word.getName());
                Float x2 = weightMap2.get(word.getName());
                if (x1 != null && x2 != null) {
                    //x1x2
                    float oneOfTheDimension = x1 * x2;
                    //+
                    ab.addAndGet(oneOfTheDimension);
                }
                if (x1 != null) {
                    //(x1)^2
                    float oneOfTheDimension = x1 * x1;
                    //+
                    aa.addAndGet(oneOfTheDimension);
                }
                if (x2 != null) {
                    //(x2)^2
                    float oneOfTheDimension = x2 * x2;
                    //+
                    bb.addAndGet(oneOfTheDimension);
                }
            });
            //|a| 对aa开方
            double aaa = Math.sqrt(aa.doubleValue());
            //|b| 对bb开方
            double bbb = Math.sqrt(bb.doubleValue());
    
            //使用BigDecimal保证精确计算浮点数
            //double aabb = aaa * bbb;
            BigDecimal aabb = BigDecimal.valueOf(aaa).multiply(BigDecimal.valueOf(bbb));
    
            //similarity=a.b/|a|*|b|
            //divide参数说明:aabb被除数,9表示小数点后保留9位,最后一个表示用标准的四舍五入法
            double cos = BigDecimal.valueOf(ab.get()).divide(aabb, 9, BigDecimal.ROUND_HALF_UP).doubleValue();
            return cos;
        }
    
    
        /**
         * 向每一个Word对象的属性都注入weight(权重)属性值
         */
        protected static void taggingWeightByFrequency(List<Word> words1, List<Word> words2) {
            if (words1.get(0).getWeight() != null && words2.get(0).getWeight() != null) {
                return;
            }
            //词频统计(key是词,value是该词在这段句子中出现的次数)
            Map<String, AtomicInteger> frequency1 = getFrequency(words1);
            Map<String, AtomicInteger> frequency2 = getFrequency(words2);
    
            //如果是DEBUG模式输出词频统计信息
    //        if (LOGGER.isDebugEnabled()) {
    //            LOGGER.debug("词频统计1:\n{}", getWordsFrequencyString(frequency1));
    //            LOGGER.debug("词频统计2:\n{}", getWordsFrequencyString(frequency2));
    //        }
            // 标注权重(该词出现的次数)
            words1.parallelStream().forEach(word -> word.setWeight(frequency1.get(word.getName()).floatValue()));
            words2.parallelStream().forEach(word -> word.setWeight(frequency2.get(word.getName()).floatValue()));
        }
    
        /**
         * 统计词频
         *
         * @return 词频统计图
         */
        private static Map<String, AtomicInteger> getFrequency(List<Word> words) {
    
            Map<String, AtomicInteger> freq = new HashMap<>();
            //这步很帅哦
            words.forEach(i -> freq.computeIfAbsent(i.getName(), k -> new AtomicInteger()).incrementAndGet());
            return freq;
        }
    
        /**
         * 输出:词频统计信息
         */
        private static String getWordsFrequencyString(Map<String, AtomicInteger> frequency) {
            StringBuilder str = new StringBuilder();
            if (frequency != null && !frequency.isEmpty()) {
                AtomicInteger integer = new AtomicInteger();
                frequency.entrySet().stream().sorted((a, b) -> b.getValue().get() - a.getValue().get()).forEach(
                        i -> str.append("\t").append(integer.incrementAndGet()).append("、").append(i.getKey()).append("=")
                                .append(i.getValue()).append("\n"));
            }
            str.setLength(str.length() - 1);
            return str.toString();
        }
    
        /**
         * 构造权重快速搜索容器
         */
        protected static Map<String, Float> getFastSearchMap(List<Word> words) {
            if (CollectionUtils.isEmpty(words)) {
                return Collections.emptyMap();
            }
            Map<String, Float> weightMap = new ConcurrentHashMap<>(words.size());
    
            words.parallelStream().forEach(i -> {
                if (i.getWeight() != null) {
                    weightMap.put(i.getName(), i.getWeight());
                } else {
                    LOGGER.error("no word weight info:" + i.getName());
                }
            });
            return weightMap;
        }
    
    }
    
  5. AtomicFloat 原子类

    package com.example.commoms;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * jdk没有AtomicFloat,写一个
     */
    public class AtomicFloat extends Number {
    
        private AtomicInteger bits;
    
        public AtomicFloat() {
            this(0f);
        }
    
        public AtomicFloat(float initialValue) {
            bits = new AtomicInteger(Float.floatToIntBits(initialValue));
        }
    
        //叠加
        public final float addAndGet(float delta) {
            float expect;
            float update;
            do {
                expect = get();
                update = expect + delta;
            } while (!this.compareAndSet(expect, update));
    
            return update;
        }
    
        public final float getAndAdd(float delta) {
            float expect;
            float update;
            do {
                expect = get();
                update = expect + delta;
            } while (!this.compareAndSet(expect, update));
    
            return expect;
        }
    
        public final float getAndDecrement() {
            return getAndAdd(-1);
        }
    
        public final float decrementAndGet() {
            return addAndGet(-1);
        }
    
        public final float getAndIncrement() {
            return getAndAdd(1);
        }
    
        public final float incrementAndGet() {
            return addAndGet(1);
        }
    
        public final float getAndSet(float newValue) {
            float expect;
            do {
                expect = get();
            } while (!this.compareAndSet(expect, newValue));
    
            return expect;
        }
    
        public final boolean compareAndSet(float expect, float update) {
            return bits.compareAndSet(Float.floatToIntBits(expect), Float.floatToIntBits(update));
        }
    
        public final void set(float newValue) {
            bits.set(Float.floatToIntBits(newValue));
        }
    
        public final float get() {
            return Float.intBitsToFloat(bits.get());
        }
    
        @Override
        public float floatValue() {
            return get();
        }
    
        @Override
        public double doubleValue() {
            return (double) floatValue();
        }
    
        @Override
        public int intValue() {
            return (int) get();
        }
    
        @Override
        public long longValue() {
            return (long) get();
        }
    
        @Override
        public String toString() {
            return Float.toString(get());
        }
    }
    
  6. 可以在方法中调用CosineSimilarity.getSimilarity("String1","String2");来比较String1和String2的相似度,相识度值越大就越相似,完全相等为1。

心动算法

我们基于余弦相似度算法进行加权中和便可得到心动用户:

  1. 分别得到用户的标签,好友,队伍,点赞文章字符串列表

  2. 将得到的列表分别于当前用户的列表进行相似度比较

  3. 将得到的相似度值进行加权:

    • 标签较为重要,故占 50%
    • 其次为好友,占 20%
    • 文章,占 20%
    • 队伍, 占 10%
  4. 可设置头像大小:
    s y m b o l S i z e = 标签相识度 ∗ 0.5 + 好友相似度 ∗ 0.2 + 文章点赞相似度 ∗ 0.2 + 队伍相似度 ∗ 0.1 symbolSize=标签相识度*0.5+好友相似度*0.2+文章点赞相似度*0.2+队伍相似度*0.1 symbolSize=标签相识度0.5+好友相似度0.2+文章点赞相似度0.2+队伍相似度0.1

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值