首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

文本相似度 HanPL汉语言处理

编程知识
2024年08月05日 08:39

@

目录


前言

请各大网友尊重本人原创知识分享,谨记本人博客:南国以南i


提示:以下是本篇文章正文内容,下面案例可供参考

需求

当我们需要求两个或两个以上的字符串相似度百分比时,可以使用HanLP汉语言处理包来帮助我们,求两个文本的相似度百分比、海明距离、

简介

HanLP(Han Language Processing) 在汉语义处理方面具备强大的功能,由中国科学院计算技术研究所自然语言处理与社会人文计算研究中心开发。它提供了包括分词、词性标注、命名实体识别、依存句法分析、情感分析、文本分类等多种自然语言处理任务的功能。

主要功能:

  • 分词: HanLP提供了多种分词模型,包括基于规则的模型、基于神经网络的模型等,能够准确地进行中文分词。
  • 词性标注: 在分词的基础上,HanLP还能对词语进行词性标注,帮助理解词语在句子中的作用。
  • 命名实体识别: HanLP能够识别文本中的命名实体,如人名、地名、组织机构名等,这对于信息抽取等任务非常重要。
  • 依存句法分析: 通过对句子中各个词语之间的依存关系建模,HanLP能够分析句子结构,提取句子的语义信息。
  • 情感分析: HanLP支持对文本进行情感分析,判断文本所表达的情感倾向,如正面、负面或中性。
  • 文本分类: HanLP还提供了文本分类的功能,可以将文本按照预设的分类体系进行分类。

友情链接:HanLP项目主页HanLP下载地址详细介绍

实操开始

注意: 本文以Java开发语言为案例

1. 添加pom.xml依赖

	    <!--junit依赖-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
        <!--hanlp语言处理依赖-->
        <dependency>
            <groupId>com.hankcs</groupId>
            <artifactId>hanlp</artifactId>
            <version>portable-1.7.6</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.10.3</version>
        </dependency>

2. 文本相似度工具类

import com.hankcs.hanlp.seg.common.Term;
import com.hankcs.hanlp.tokenizer.StandardTokenizer;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;

import java.math.BigInteger;
import java.util.*;

public class MySimHash {
    private String tokens; //字符串
    private BigInteger strSimHash;//字符产的hash值
    private int hashbits = 64; // 分词后的hash数;


    public MySimHash(String tokens) {
        this.tokens = tokens;
        this.strSimHash = this.simHash();
    }

    private MySimHash(String tokens, int hashbits) {
        this.tokens = tokens;
        this.hashbits = hashbits;
        this.strSimHash = this.simHash();
    }


    /**
     * 清除html标签
     *
     * @param content
     * @return
     */
    private String cleanResume(String content) {
        // 若输入为HTML,下面会过滤掉所有的HTML的tag
        content = Jsoup.clean(content, Whitelist.none());
        content = StringUtils.lowerCase(content);
        String[] strings = {" ", "\n", "\r", "\t", "\\r", "\\n", "\\t", "&nbsp;"};
        for (String s : strings) {
            content = content.replaceAll(s, "");
        }
        return content;
    }


    /**
     * 这个是对整个字符串进行hash计算
     *
     * @return
     */
    private BigInteger simHash() {
        tokens = cleanResume(tokens); // cleanResume 删除一些特殊字符
        int[] v = new int[this.hashbits];
        List<Term> termList = StandardTokenizer.segment(this.tokens); // 对字符串进行分词
        //对分词的一些特殊处理 : 比如: 根据词性添加权重 , 过滤掉标点符号 , 过滤超频词汇等;
        Map<String, Integer> weightOfNature = new HashMap<String, Integer>(); // 词性的权重
        weightOfNature.put("n", 2); //给名词的权重是2;
        Map<String, String> stopNatures = new HashMap<String, String>();//停用的词性 如一些标点符号之类的;
        stopNatures.put("w", ""); //
        int overCount = 5; //设定超频词汇的界限 ;
        Map<String, Integer> wordCount = new HashMap<String, Integer>();
        for (Term term : termList) {
            String word = term.word; //分词字符串
            String nature = term.nature.toString(); // 分词属性;
            //  过滤超频词
            if (wordCount.containsKey(word)) {
                int count = wordCount.get(word);
                if (count > overCount) {
                    continue;
                }
                wordCount.put(word, count + 1);
            } else {
                wordCount.put(word, 1);
            }
            // 过滤停用词性
            if (stopNatures.containsKey(nature)) {
                continue;
            }
            // 2、将每一个分词hash为一组固定长度的数列.比如 64bit 的一个整数.
            BigInteger t = this.hash(word);
            for (int i = 0; i < this.hashbits; i++) {
                BigInteger bitmask = new BigInteger("1").shiftLeft(i);
                // 3、建立一个长度为64的整数数组(假设要生成64位的数字指纹,也可以是其它数字),
                // 对每一个分词hash后的数列进行判断,如果是1000...1,那么数组的第一位和末尾一位加1,
                // 中间的62位减一,也就是说,逢1加1,逢0减1.一直到把所有的分词hash数列全部判断完毕.
                int weight = 1;  //添加权重
                if (weightOfNature.containsKey(nature)) {
                    weight = weightOfNature.get(nature);
                }
                if (t.and(bitmask).signum() != 0) {
                    // 这里是计算整个文档的所有特征的向量和
                    v[i] += weight;
                } else {
                    v[i] -= weight;
                }
            }
        }
        BigInteger fingerprint = new BigInteger("0");
        for (int i = 0; i < this.hashbits; i++) {
            if (v[i] >= 0) {
                fingerprint = fingerprint.add(new BigInteger("1").shiftLeft(i));
            }
        }
        return fingerprint;
    }


    /**
     * 对单个的分词进行hash计算;
     *
     * @param source
     * @return
     */
    private BigInteger hash(String source) {
        if (source == null || source.length() == 0) {
            return new BigInteger("0");
        } else {
            /**
             * 当sourece 的长度过短,会导致hash算法失效,因此需要对过短的词补偿
             */
            while (source.length() < 3) {
                source = source + source.charAt(0);
            }
            char[] sourceArray = source.toCharArray();
            BigInteger x = BigInteger.valueOf(((long) sourceArray[0]) << 7);
            BigInteger m = new BigInteger("1000003");
            BigInteger mask = new BigInteger("2").pow(this.hashbits).subtract(new BigInteger("1"));
            for (char item : sourceArray) {
                BigInteger temp = BigInteger.valueOf((long) item);
                x = x.multiply(m).xor(temp).and(mask);
            }
            x = x.xor(new BigInteger(String.valueOf(source.length())));
            if (x.equals(new BigInteger("-1"))) {
                x = new BigInteger("-2");
            }
            return x;
        }
    }

    /**
     * 计算海明距离,海明距离越小说明越相似;
     *
     * @param other
     * @return
     */
    private int hammingDistance(MySimHash other) {
        BigInteger m = new BigInteger("1").shiftLeft(this.hashbits).subtract(
                new BigInteger("1"));
        BigInteger x = this.strSimHash.xor(other.strSimHash).and(m);
        int tot = 0;
        while (x.signum() != 0) {
            tot += 1;
            x = x.and(x.subtract(new BigInteger("1")));
        }
        return tot;
    }


    /**
     * .
     * 求百分比
     *
     * @param s2
     * @return
     */
    public double getSemblance(MySimHash s2) {
        double i = (double) this.hammingDistance(s2);
        return 1 - i / this.hashbits;
    }

}

详细说明:

/*--------------------------------------相似度算法说明--------------------------------------
    借鉴hashmap算法找出可以hash的key值,因为我们使用的simhash是局部敏感哈希,这个算法的特点是只要相似的字符串只有个别的位数是有差别变化。
    那这样我们可以推断两个相似的文本,至少有16位的simhash是一样的。具体选择16位、8位、4位,大家根据自己的数据测试选择,虽然比较的位数越小越精准,但是空间会变大。
    分为4个16位段的存储空间是单独simhash存储空间的4倍。之前算出5000w数据是 382 Mb,扩大4倍1.5G左右,还可以接受:)  通过这样计算,我们的simhash查找过程全部降
    到了1毫秒以下。就加了一个hash效果这么厉害?我们可以算一下,原来是5000w次顺序比较,现在是少了2的16次方比较,前面16位变成了hash查找。后面的顺序比较的个数是多少?
    2^16 = 65536, 5000w/65536 = 763 次。。。。实际最后链表比较的数据也才 763次!所以效率大大提高!  到目前第一点降到3.6毫秒、支持5000w数据相似度比较做完了。
    还有第二点同一时刻发出的文本如果重复也只能保留一条和短文本相识度比较怎么解决。其实上面的问题解决了,这两个就不是什么问题了。  之前的评估一直都是按照线性计算来估计的,
    就算有多线程提交相似度计算比较,我们提供相似度计算服务器也需要线性计算。比如同时客户端发送过来两条需要比较相似度的请求,在服务器这边都进行了一个排队处理,一个接着一个,
    第一个处理完了在处理第二个,等到第一个处理完了也就加入了simhash库。所以只要服务端加了队列,就不存在同时请求不能判断的情况。 simhash如何处理短文本?换一种思路,
    simhash可以作为局部敏感哈希第一次计算缩小整个比较的范围,等到我们只有比较700多次比较时,就算使用我们之前精准度高计算很慢的编辑距离也可以搞定。当然如果觉得慢了,也可以使用余弦夹角等效率稍微高点的相似度算法";
     */

    /*--------------------------------------分词说明--------------------------------------
    分词,把需要判断文本分词形成这个文章的特征单词。 最后形成去掉噪音词的单词序列并为每个词加上权重,我们假设权重分为5个级别(1~5)。
    只要相似的字符串只有个别的位数是有差别变化。那这样我们可以推断两个相似的文本, 比如:“ 美国“51区”雇员称内部有9架飞碟,曾看见灰色外星人 ” ==>
    分词后为 “ 美国(4) 51区(5) 雇员(3) 称(1) 内部(2) 有(1) 9架(3) 飞碟(5) 曾(1) 看见(3) 灰色(4) 外星人(5)”, 括号里是代表单词在
    整个句子里重要程度,数字越大越重要。 2、hash,通过hash算法把每个词变成hash值, 比如“美国”通过hash算法计算为 100101, “51区”通过hash算法计算为 101011。
    这样我们的字符串就变成了一串串数字,还记得文章开头说过的吗,要把文章变为数字计算才能提高相似度计算性能,现在是降维过程进行时。 3、加权,通过 2步骤的hash生成结果,
    需要按照单词的权重形成加权数字串, 比如“美国”的hash值为“100101”,通过加权计算为“4 -4 -4 4 -4 4”;“51区”的hash值为“101011”,通过加权计算为 “ 5 -5 5 -5 5 5”。 4、合并,
    把上面各个单词算出来的序列值累加,变成只有一个序列串。 比如 “美国”的 “4 -4 -4 4 -4 4”,“51区”的 “ 5 -5 5 -5 5 5”, 把每一位进行累加, “4+5 -4+-5 -4+5 4+-5 -4+5 4+5” ==》 “9 -9 1 -1 1 9”。
    这里作为示例只算了两个单词的,真实计算需要把所有单词的序列串累加。 5、降维,把4步算出来的 “9 -9 1 -1 1 9” 变成 0 1 串,形成我们最终的simhash签名。 如果每一位大于0 记为 1,小于0 是统优先公司";


    /*--------------------------------------敏感哈希说明--------------------------------------
    算法找出可以hash的key值,因为我们使用的simhash是局部敏感哈希,这个算法的特点是只要相似的字 把需要判断文本分词形成这个文章的特征单词。
    最后形成去掉噪音词的只要相似的字符串只有个别的位数是有差别变化。那这样我们可以推断两个相似的文本,单词序分词是代表单词在整个句子里重要程度,数字越大越重要。
    2、hash,通过hash算法把每个词变成hash值, 比如“美国”通过hash算法计算为 100101, “51区”通过hash算法计算为 101011。 这样我们的字符串就变成了一串串数字,
    还记得文章开头说过的吗,要把文章变为数字加权,通过 家可能会有疑问,经过这么多步骤搞这么麻烦,不就是为了得到个 0 1 字符串吗?我直接把这个文本作为字符串输入v较,
    前面16位变成了hash查找。后面的顺序比较的个数是多,用hd5是用于生成唯一签来相差甚远;hashmap也是用于键值对查找,便于快速插入和查找的数据结构。不过我们主要解决的
    是文本相似度计算,要比较的是两个文章是否相识,当然我们降维生成了hashcode也是用于这个目的。看到这里估计大家就明白了,我们使用的sim是这样的,传统hash函数解决的是
    生成唯一值,比如 md5、hashmap等。md5是用于生成唯一签名串,只要稍微多加一个字符md5的两个数字看起来相差甚远;hashmap也是用于键值对查找,便于快速插入和查找的数
    据结构。不过我们主要解决的是文本相似度计算,要比较的是两个文章是否相识,当然我们降维生成了hashcode也是用于这个目的。看到这里估计大家就明白了,我们使用的simhash
    就算把文章中的字符串变成 01 串也还是可以用于计算相似度的,而传统的hashcode却不行。我们可以来做个测试,两个相差只有一个字符的文本串,“你妈妈喊你回家吃饭哦,回家
    罗回家罗” 和 “你妈妈叫你回家吃饭啦,回家罗回家罗”。短文本大量重复信息不会被过滤,是不是";
     */

    /*--------------------------------------过滤说明--------------------------------------
    最后形成去掉噪音词的单词序分词是代表单词在整个句子里重要程度,数字越大越重要。 最后形成去掉噪音词的单词序列并为每个词加上权重 2、hash,通过hash算法把每个词变成hash值,
    比如“美国”通过hash算法计算为 100101, “51区”通过hash算法计算为 101011。 这样我们的字符串就变成了一串串数字,还记得文章开头说过的吗,分为4个16位段的存储空间是单独simhash
    存储空间的4倍。之前算出5000w数据是 382 Mb,扩大4倍1.5G左右,还可以接受:) 要把文章变为数字加权,通过 家可能会有疑问,经过这么多步骤搞这么麻烦,不就是为了得到个 0 1 字符串吗?
    我直接把这个文本作为字符串输入,用hd5是用于生成唯一签来相差甚远;hashmap也是用于键值对查找,便于快速插入和查找的数据结构。不过我们主要解决的是文本相似度计算,要比较的是两个文章是否相识,
    当然我们降维生成了hashcode也是用于这个目的。看到这里估计大家就明白了,我们使用的sim是这样的,传统hash函数解决的是生成唯一值,比如 md5、hashmap等。md5是用于生成唯一签名串,只要稍微多加
    一个字符md5的两个数字看起来相差甚远;hashmap也是用于键值对查找,便于快速插入和查找的数据结构。不过我们主要解决的是文本相似度计算,要比较的是两个文章是否相识,当然我们降维生成了hashcode也
    是用于这个目的。看到这里估计大家就明白了,我们使用的simhash就算把文章中的字符串变成 01 串也还是可以用于计算相似度的,而传统的hashcode却不行。我们可以来做个测试,两个相差只有一个字符的文本
    串,“你妈妈喊你回家吃饭哦,回家罗回家罗” 和 “你妈妈叫你回家吃饭啦,回家罗回家罗”。短文本大量重复信息不会被过滤,";
     */

3. 案例验证

    public static void main(String[] args) {
        String text = "杏南一区";
        List<String> itemList = new LinkedList<String>();
        itemList.add("杏南一区");
        itemList.add("杏南二区");
        itemList.add("杏南三区");
        itemList.add("杏南四区");
        itemList.add("杏南五区");
        itemList.add("杏南六区");
        itemList.add("杏南七区");
        itemList.add("杏南八区");
        itemList.add("杏南九区");
        itemList.add("杏南十区");
        System.out.println("======================================");
        long startTime = System.currentTimeMillis();
        MySimHash hash1 = new MySimHash(text, 64);
        List<Double> list = new LinkedList<Double>();
        for (String str : itemList) {
            MySimHash hash2 = new MySimHash(str, 64);
            //海明距离越小说明越相似
            System.out.println("海明距离:" + hash1.hammingDistance(hash2) + "###" + "文本相似度:" + hash2.getSemblance(hash1));
            list.add(hash2.getSemblance(hash1));
        }
        long endTime = System.currentTimeMillis();
        Double max = Collections.max(list);
        int index = list.indexOf(max);//获取集合下标
        System.out.println("======================================");
        System.out.println("耗时:" + (endTime - startTime));
        System.out.println("相似度集合内容:" + list.toString());
        System.out.println("集合中最大值:" + max + "###" + "集合下标:" + index);
        System.out.println("对比内容:" + text + "###" + "相似度最高:" + itemList.get(index));
    }

4. 验证结果

测试

总结

我是南国以南i记录点滴每天成长一点点,学习是永无止境的!转载请附原文链接!!!

参考链接

From:https://www.cnblogs.com/bgyb/p/18336255
本文地址: http://www.shuzixingkong.net/article/793
0评论
提交 加载更多评论
其他文章 《花100块做个摸鱼小网站! 》第一篇—买云服务器和初始化环境
一、前言 大家好呀,我是summo,前面我已经写了我为啥要做这个摸鱼小网站的原因,从这篇文章开始我会一步步跟大家聊聊我是怎么搭起这个网站的。我知道对很多新手来说,建网站可能挺头大的,不知道从哪里开始,所以我会尽量写得简单明了,让大家一看就懂,少走弯路。 咱们先从买服务器开始说起。现在阿里云好像还有免
《花100块做个摸鱼小网站! 》第一篇—买云服务器和初始化环境 《花100块做个摸鱼小网站! 》第一篇—买云服务器和初始化环境 《花100块做个摸鱼小网站! 》第一篇—买云服务器和初始化环境
面向对象的编码设计原则
简单讲过程思维是数据结构加操作;对象思维则是一个整体,既包含数据结构又包含操作,也就是面向对象中的属性和行为。 在进行面向对象设计和编码的道路上,众多知名前辈结合自己的实践和认知高度抽象概况出了具有指导思想意义的设计原则。这里的每个原则细细品来都是意味深长,但是需要注意的是,就像数据库范式一样,它是
面向对象的编码设计原则 面向对象的编码设计原则
SpringBoot Session共享,配置不生效问题排查 → 你竟然在代码里下毒!
开心一刻 快 8 点了,街边卖油条的还没来,我只能给他打电话 大哥在电话中说到:劳资卖了这么多年油条,从来都是自由自在,自从特么认识了你,居然让我有了上班的感觉! Session 共享 SpringBoot session 共享配置,我相信你们都会,但出于负责的态度,我还是给你们演示一遍 添加依赖
SpringBoot Session共享,配置不生效问题排查 → 你竟然在代码里下毒! SpringBoot Session共享,配置不生效问题排查 → 你竟然在代码里下毒! SpringBoot Session共享,配置不生效问题排查 → 你竟然在代码里下毒!
【解决方案】Java 互联网项目中消息通知系统的设计与实现(下)
书接上回,消息通知系统(notification-system)作为一个独立的微服务,完整地负责了 App 端内所有消息通知相关的后端功能实现。该系统既需要与文章系统、订单系统、会员系统等相关联,也需要和其它业务系统相关联,是一个偏底层的通用服务系统。
【解决方案】Java 互联网项目中消息通知系统的设计与实现(下) 【解决方案】Java 互联网项目中消息通知系统的设计与实现(下)
感谢「河南图奕网络」赞助园子,成为第一家创始赞助商
在8月1日发布救援行动-赞助商计划后,我们并没有抱什么奢望,更没有妄想很快能找到赞助商,只是为救园多一点可能的希望,万一找到一家赞助商,就会多一份救园力量。没想到第2天就有幸遇到一家有意向的企业
感谢「河南图奕网络」赞助园子,成为第一家创始赞助商 感谢「河南图奕网络」赞助园子,成为第一家创始赞助商 感谢「河南图奕网络」赞助园子,成为第一家创始赞助商
一直让 PHP 程序员懵逼的同步阻塞异步非阻塞,终于搞明白了
经常听到身边写 Java、Go 的朋友提到程序异步、非阻塞、线程、协程,让系统性能提高到百万、千万并发,使我甚是惊讶属实羡慕。
一直让 PHP 程序员懵逼的同步阻塞异步非阻塞,终于搞明白了 一直让 PHP 程序员懵逼的同步阻塞异步非阻塞,终于搞明白了
css手撕奥运五环
巴黎奥运会正如火如荼地进行,本文来使用 CSS 来画一个奥运五环。奥运五环是相互连接的,因此在视觉上会产生重叠效果,这也是实现五环最有挑战性的部分。接下来,将利用 CSS 的伪元素,巧妙地实现环环相扣的效果! 根据五环的位置特点,可以将中间的黑色环设置为 HTML 的父元素,而将其他颜色的环设置为子
css手撕奥运五环 css手撕奥运五环 css手撕奥运五环
MySQL 是如何实现数据的排序的?
1.背景 或许你面试的时候被问到了mysql的排序问题 又或许你在学习排序算法的时候想到了数据库的排序是如何实现的呢 下面重点从面试的角度来回答这个问题 2.面试回答 1.普通面试者回答 普通面试者的回答通常是点对点的回答,如下: MySQL 实现数据的排序主要通过排序算法和索引结构来完成。排序操作