hanLP探索-语义距离计算的实现

hanLP探索——语义距离计算的实现

在翻看hanLP源码时,看见了计算词语语义距离的方法,不由得引起了我强烈的好奇心,是什么样的逻辑可以计算词语语义之间的距离。

探索

在jar包中有一个类com.hankcs.hanlp.dictionary.CoreSynonymDictionary ,根据注释可以知道这个类是与核心同义词词典相关的,其中有方法distance()可以计算词语距离,看代码。

/**
     * 语义距离
     * @param itemA
     * @param itemB
     * @return
     */
    public static long distance(CommonSynonymDictionary.SynonymItem itemA, CommonSynonymDictionary.SynonymItem itemB)
    {
        return itemA.distance(itemB);
    }

    /**
     * 判断两个单词之间的语义距离
     * @param A
     * @param B
     * @return
     */
    public static long distance(String A, String B)
    {
        CommonSynonymDictionary.SynonymItem itemA = get(A);
        CommonSynonymDictionary.SynonymItem itemB = get(B);
        if (itemA == null || itemB == null) return Long.MAX_VALUE;

        return distance(itemA, itemB);
    }

从中可以很明显的看出,这里调用的方法是CommonSynonymDictionary.SynonymItem.distance(),继续寻找。

/**
     * 语义距离
     * @param other
     * @return
     */
    public long distance(Synonym other)
    {
        return Math.abs(id - other.id);
    }

一步步深入之后,找到了如上的代码。从代码中可以看出,计算两词语的语义距离只是取词语的id做差绝对值的计算就可以得到,并没有设想的特别算法逻辑。那么这里的id又是怎么得来的?

经过寻找,发现这些词语都是从核心同义词字典文件中得到的。如下是字典文件CoreSynonym.txt内容:

Aa01A01= 人 士 人物 人士 人氏 人选
Aa01A02= 人类 生人 全人类
Aa01A03= 人手 人员 人口 人丁 口 食指
Aa01A04= 劳力 劳动力 工作者
Aa01A05= 匹夫 个人
Aa01A06= 家伙 东西 货色 厮 崽子 兔崽子 狗崽子 小子 杂种 畜生 混蛋 王八蛋 竖子 鼠辈 小崽子
Aa01A07= 者 手 匠 客 主 子 家 夫 翁 汉 员 分子 鬼 货 棍 徒
Aa01A08= 每人 各人 每位
Aa01A09= 该人 此人
Aa01B01= 人民 民 国民 公民 平民 黎民 庶 庶民 老百姓 苍生 生灵 生人 布衣 白丁 赤子 氓 群氓 黔首 黎民百姓 庶人 百姓 全民 全员 萌
Aa01B02= 群众 大众 公众 民众 万众 众生 千夫
Aa01B03# 良民 顺民
Aa01B04# 遗民 贱民 流民 游民 顽民 刁民 愚民 不法分子 孑遗
Aa01C01= 众人 人人 人们
Aa01C02= 人丛 人群 人海 人流 人潮
Aa01C03= 大家 大伙儿 大家伙儿 大伙 一班人 众家 各户
Aa01C04= 们 辈 曹 等
Aa01C05@ 众学生
Aa01C06# 妇孺 父老兄弟 男女老少 男女老幼
Aa01C07# 党群 干群 军民 工农兵 劳资 主仆 宾主 僧俗 师徒 师生 师生员工 教职员工 群体 爱国志士 党外人士 民主人士 爱国人士 政群 党政群 非党人士 业内人士 工农分子 军警民 党政军民
Aa01D01@ 角色
Aa02A01= 我 咱 俺 余 吾 予 侬 咱家 本人 身 个人 人家 斯人

文件中将词语做了分类,同一行的词语语义相近或相同,位于行首的字符串应该就是id了。那这些字符串是如何参与计算的?继续寻找。

public boolean load(InputStream inputStream)
    {
        trie = new DoubleArrayTrie<SynonymItem>();
        TreeMap<String, SynonymItem> treeMap = new TreeMap<String, SynonymItem>();
        String line = null;
        try
        {
            BufferedReader bw = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
            ArrayList<Synonym> synonymList = null;
            //这里是从核心同义词字典文件中按照行读取内容
            while ((line = bw.readLine()) != null)
            {
                //这里将读取到内容按照空格分割开了
                String[] args = line.split(" ");
                //这里调用了Synonym.create()方法,构建了词语的数组,这里应该就是关键
                synonymList = Synonym.create(args);
                char type = args[0].charAt(args[0].length() - 1);
                for (Synonym synonym : synonymList)
                {
                    treeMap.put(synonym.realWord, new SynonymItem(synonym, synonymList, type));
                    // 这里稍微做个test
                    //assert synonym.getIdString().startsWith(line.split(" ")[0].substring(0, line.split(" ")[0].length() - 1)) : "词典有问题" + line + synonym.toString();
                }
            }
            bw.close();
            // 获取最大语义id
            if (synonymList != null && synonymList.size() > 0)
            {
                maxSynonymItemIdDistance = synonymList.get(synonymList.size() - 1).id - SynonymHelper.convertString2IdWithIndex("Aa01A01", 0) + 1;
            }
            //这里构造一个双数组trie树,便于词语的查找
            int resultCode = trie.build(treeMap);
            if (resultCode != 0)
            {
                logger.warning("构建" + inputStream + "失败,错误码" + resultCode);
                return false;
            }
        }
        catch (Exception e)
        {
            logger.warning("读取" + inputStream + "失败,可能由行" + line + "造成");
            return false;
        }
        return true;
    }

进入方法Synonym.create()查看具体逻辑。

/**
     * @see com.hankcs.hanlp.corpus.synonym.Synonym#create(String)
     * @param args
     * @return
     */
    public static ArrayList<Synonym> create(String[] args)
    {
        ArrayList<Synonym> synonymList = new ArrayList<Synonym>(args.length - 1);

        //果然行首就是id,这里获取了行首的字符串
        String idString = args[0];
        Type type;
        //这里获取了id字符串的最后一个字符,看来不同字符代表着不同的含义
        switch (idString.charAt(idString.length() - 1))
        {
            case '=':
                //这里表示同义词
                type = Type.EQUAL;
                break;
            case '#':
                //这里表示同类词
                type = Type.LIKE;
                break;
            default:
                //这里表示封闭词,没有同义词或者同类词
                type = Type.SINGLE;
                break;
        }
        /*
        这里将id字符串转换为了数字,里面的规则是:
        id =    (idString.charAt(0) - 'A') * 26L * 10 * 10 * 26 * 10 * 10 +
                (idString.charAt(1) - 'a') * 10 * 10 * 26 * 10 * 10 +
                (idString.charAt(2) - '0') * 10 * 26 * 10 * 10 +
                (idString.charAt(3) - '0') * 26 * 10 * 10 +
                (idString.charAt(4) - 'A') * 10 * 10 +
                (idString.charAt(5) - '0') * 10 +
                (idString.charAt(6) - '0') ;    // 编码等号前面的
        */
        long startId = SynonymHelper.convertString2IdWithIndex(idString, 0);    // id从这里开始
        for (int i = 1; i < args.length; ++i)
        {
            if (type == Type.LIKE)
            {
                synonymList.add(new Synonym(args[i], startId + i, type));             // 如果不同则id递增
            }
            else
            {
                synonymList.add(new Synonym(args[i], startId, type));             // 如果相同则不变
            }
        }
        return synonymList;
    }

结论

至此,弄清楚了hanLP中语义距离计算的整个逻辑,首先读取已有的核心同义词文件CoreSynonym.txt,按照特定的规则构建trie树,计算词语语义距离时,即从trie树中查找词语,计算两词语id的差绝对值即为词语距离,如果没有记录则直接返回Long.MAX_VALUE。其中并没有特定的算法计算逻辑,核心同义词文件是计算的基础。其中的tire树(又叫单词查找树)类似于数据结构中的哈夫曼树,可以高效的查找词语。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值