Springboot整合HanLP实现两段文字语义比对

一、maven

        <!-- jdk8专用 -->
        <dependency>
            <groupId>org.apache.opennlp</groupId>
            <artifactId>opennlp-tools</artifactId>
            <version>1.9.4</version>
        </dependency>

        <dependency>
            <groupId>com.hankcs</groupId>
            <artifactId>hanlp</artifactId>
            <version>portable-1.8.4</version>
        </dependency>

二、中文向量模型
提供百度网盘:
cc.zh.300.zip
提取码:6783
三、引入

  1. 将下载好的cc.zh.300.vec模型放到一个目录,大小4.4G。
  2. 由于模型太大,有200w的向量词,后期在加载模型时,只加载50w的向量词。或者用其他方法提高加载速度。

四、工具类
1、yml配置:

hanlp:
  vector:
    # 相似度阈值
    threshold: 0.8  
    # 模型存放目录
    sourcePath: D:\***\cc.zh.300.vec
    # 向量词加载50万
    maxWords: 500000  
    preload: true

2、工具类

import com.hankcs.hanlp.HanLP;
import org.springframework.beans.factory.annotation.Value;

import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;

public class FastTextChineseSimilarity {

    private final Map<String, float[]> wordVectors;
    private final int vectorSize = 300; // FastText词向量维度
    private final float[] zeroVector = new float[vectorSize];

    @Value("${hanlp.vector.maxWords}")//50w
    private int maxWords;

    // 初始化加载词向量模型
    public FastTextChineseSimilarity(String modelPath) throws IOException {
        this.wordVectors = new HashMap<>();
        loadModel(modelPath);
    }

    // 加载FastText词向量模型
    private void loadModel(String modelPath) throws IOException {
        System.out.println("开始加载词向量模型...");
        long start = System.currentTimeMillis();

        try (BufferedReader reader = Files.newBufferedReader(Paths.get(modelPath))) {
            // 跳过第一行(词数和维度信息)
            String firstLine = reader.readLine();
            System.out.println("模型信息: " + firstLine);

            String line;
            int count = 0;
            while ((line = reader.readLine()) != null) {
                //只加载50w的向量词
                if (wordVectors.size() >= maxWords) return;

                String[] parts = line.split(" ");
                if (parts.length != vectorSize + 1) continue;

                String word = parts[0];
                float[] vector = new float[vectorSize];
                for (int i = 0; i < vectorSize; i++) {
                    vector[i] = Float.parseFloat(parts[i + 1]);
                }
                wordVectors.put(word, vector);

                if (++count % 100000 == 0) {
                    System.out.println("已加载 " + count + " 个词向量");
                }
            }
        }

        System.out.printf("模型加载完成,耗时 %.2f秒,总词向量数: %d%n",
                (System.currentTimeMillis() - start) / 1000.0, wordVectors.size());
    }

    // 中文分词(简单实现,实际应用建议使用专业分词器)
    private List<String> segmentChinese(String text) {
        if (text == null || text.isEmpty()) {
            return Collections.emptyList();
        }
        return HanLP.segment(text).stream()
                .map(term -> term.word)
                .collect(Collectors.toList());
    }

    // 获取句子向量(词向量平均值)
    public float[] getSentenceVector(String sentence) {
        List<String> words = segmentChinese(sentence);
        float[] sum = new float[vectorSize];
        int validWords = 0;

        for (String word : words) {
            float[] vector = wordVectors.get(word);
            if (vector != null) {
                for (int i = 0; i < vectorSize; i++) {
                    sum[i] += vector[i];
                }
                validWords++;
            }
        }

        if (validWords > 0) {
            for (int i = 0; i < vectorSize; i++) {
                sum[i] /= validWords;
            }
            return sum;
        }

        return zeroVector;
    }

    // 计算余弦相似度
    public double cosineSimilarity(float[] vec1, float[] vec2) {
        if (vec1 == null || vec2 == null || vec1.length != vec2.length) {
            return 0.0;
        }

        double dotProduct = 0.0;
        double norm1 = 0.0;
        double norm2 = 0.0;

        for (int i = 0; i < vec1.length; i++) {
            dotProduct += vec1[i] * vec2[i];
            norm1 += Math.pow(vec1[i], 2);
            norm2 += Math.pow(vec2[i], 2);
        }

        if (norm1 == 0 || norm2 == 0) {
            return 0.0;
        }

        return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
    }

    // 计算两段文本的语义相似度
    public double calculateSimilarity(String text1, String text2) {
        float[] vec1 = getSentenceVector(text1);
        float[] vec2 = getSentenceVector(text2);
        return cosineSimilarity(vec1, vec2);
    }
}

五、使用
相似度由自己定

//初始化模型
FastTextChineseSimilarity similarity = new FastTextChineseSimilarity(modelUrl);
String text1 = map.get("text1");
String text2 = map.get("text2");
//计算语义相似度(基于关键词匹配)
double distance = similarity.calculateSimilarity(text1, text2);
System.out.printf("语义相似度: %.3f%n", distance); //越接近 1 表示语义相似,自己配置阈值
/**
 * 余弦相似度值	相似程度描述	  适用场景示例
 * 0.8 ~ 1.0	高度相似	      重复文本、轻微改写的句子
 * 0.6 ~ 0.8	中度相似	      主题一致但表述不同的段落
 * 0.4 ~ 0.6	低相似	      部分关键词重叠但主题关联较弱
 * 0 ~ 0.4	    不相似	      主题或关键词几乎无重叠
*/

六、测试

   public static void main(String[] args) {
        try {
            FastTextChineseSimilarity similarity = new FastTextChineseSimilarity("D:\\***\\cc.zh.300.vec");
            String text1 = "我是一个中国人";
            String text2 = "我是一个有中国身份证的人";
            double similarityScore = similarity.calculateSimilarity(text1, text2);
            System.out.println("文本相似度: " + similarityScore);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
开始加载词向量模型...
模型信息: 2000000 300
已加载 100000 个词向量
已加载 200000 个词向量
已加载 300000 个词向量
已加载 400000 个词向量
已加载 500000 个词向量
文本相似度: 0.9012756555329973
Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐