NLP 開源形近字算法補完計劃(完結篇) 前言 不足之處 拆字相似度 寫在完結前 java 實現思路 小結

前言

所有的故事都有開始,也終將結束。

本文將作爲 NLP 漢字相似度的完結篇,爲該系列畫上一個句號。

起-NLP 中文形近字相似度計算思路

承-中文形近字相似度算法實現,爲漢字 NLP 盡一點綿薄之力

轉-當代中國最貴的漢字是什麼?

不足之處

之所以有本篇,是因爲上一次的算法實現存在一些不足。

巴別塔

《聖經》中有關於巴別塔建造,最終人們因爲語言問題而停工的故事。

創11:6 “看哪!他們成爲一樣的人民,都是一樣的言語,如今既作起這事來,以後他們所要作的事,就沒有不成就的了。

創11:7 我們下去,在那裏變亂他們的口音,使他們的言語彼此不通。”

創11:8 於是,耶和華使他們從那裏分散在全地上;他們就停工不造那城了。

爲了避免語言問題,我一開始就實現了一個 exe4j 打包的對比程序,自己跑的很順暢。

小夥伴一跑,運行失敗。各種環境配置一頓操作,最後還是報錯。

於是,我寫了一個 python 簡易版本,便於做 NLP 研究的小夥伴們學習。

https://github.com/houbb/nlp-hanzi-similar/releases/tag/pythn

java 是一種語言,python 是一種語言。

編程語言,讓人和機器之間可以溝通,卻讓人與人之間產生了隔閡。

拆字

當代中國最貴的漢字是什麼? 一文中,我們首次說明了漢字的拆合。

漢字的拆分實現,核心目的之一就是爲了完善漢字的相似度比較。

通過對比漢字的拆分部分,然後獲取拆字的相似度,提高對比的準確性。

拆字相似度

簡單的需求

爲了便於小夥伴們理解,我們用產品經理的思維和大家介紹一下實現方式。

我的需求比較簡單。

你看,【明】可以拆分【日】【月】,【冐】也可以拆分爲【日】【月】。對比一下,結果是顯然的。

怎麼實現我不管,明天上線吧。

小夥伴們,應該已經知道怎麼實現了吧?

使用體驗

誠如產品所言,這個需求已經實現。

maven 引入

<dependency>
    <groupId>com.github.houbb</groupId>
    <artifactId>nlp-hanzi-similar</artifactId>
    <version>1.2.0</version>
</dependency>

使用

double rate1 = HanziSimilarHelper.similar('末', '未');

對應的結果爲:0.9696969696969697

更多使用細節,參考開源地址:

https://github.com/houbb/nlp-hanzi-similar

寫在完結前

涉及的項目

漢字的相似度計算到這裏算是告一段落。

主要涉及的資料及項目有:

拼音

拆字

四角編碼詞庫

漢字結構詞庫

漢字偏旁詞庫

筆畫數詞庫

當然,還可以結果 opencc4j 進行繁簡體的處理,此處不再延伸。

之後的計劃

NLP 的領域還有很多東西需要大家攻克,畢竟中文 NLP 纔剛剛開始。

技術尚未成功,同志仍需努力。

據說最近鵝城的某位黃老爺惹得大家怨聲載道。

很多小夥伴說,如果有一款軟件可以實現【月丷夫馬言卂彳山兀攴人言】的溝通功能,那麼我肯定會用。

所謂說者無心,聽者有意。

寫一個通訊軟件,主要是爲了鞏固下 netty 的學習,其他的都不重要。

雖然知道就算有,大家肯定也不太會改變,但是老馬還是準備試試。

java 實現思路

警告,如果你頭髮已經所剩無幾,或者對實現並不感興趣。

那麼就可以收藏+點贊+評論【不明覺厲】,然後離開了。

下面是枯燥的代碼實現環節。

程序員的思維

下面是程序員的思維。

首先要解決幾個問題:

(1)漢字的拆分實現

這個直接複用已經實現的漢字拆分實現。

List<String> stringList = ChaiziHelper.chai(charWord.charAt(0));

相同的一個漢字可以有多種拆分方式,簡單起見,我們默認取第一個。

(2)相似的比較

假設我們對比 A B 兩個漢字,可以拆分爲如下的子集。

A = {A1, A2, ..., Am}

B = {B1, B2, ..., Bm}

/**
 * 獲取拆分後對應的拆分字符
 * @param charWord 字符
 * @return 結果
 */
private char[] getSplitChars(String charWord) {
    List<String> stringList = ChaiziHelper.chai(charWord.charAt(0));

    // 這裏應該選擇哪一個是有講究的。此處爲了簡單,默認選擇第一個。
    String string = stringList.get(0);
    return string.toCharArray();
}

拆分後的子集對比有多種實現方式,簡單起見,我們直接遍歷元素,判斷另一個子集是否存在。

當然,遍歷的時候要以拆分數量較少的的爲基準。

int minLen = Math.min(charsOne.length, charsTwo.length);

// 比較
double totalScore = 0.0;
for(int i = 0; i <  minLen; i++) {
    char iChar = charsOne[i];
    String textChar = iChar+"";
    if(ArrayPrimitiveUtil.contains(charsTwo, iChar)) {
        //累加分數
    }
}

(3)拆分子集的權重

比如 兩個漢字都是子集,但是因爲筆畫數不同,權重也不同。

我們用一個子集的筆畫數佔整體漢字的筆畫數計算權重。

 int textNumber = getNumber(textChar, similarContext);

double scoreOne = textNumber*1.0 / numberOne * 1.0;
double scoreTwo = textNumber*1.0 / numberTwo * 1.0;

totalScore += (scoreOne + scoreTwo) / 2.0;

ps: 這裏的除以 2,是爲了歸一化。保證最後的結果在 0-1 之間。

(4)筆畫數

獲取筆畫數的方式,我們可以直接複用以前的方法。

如果沒有匹配的,默認筆畫數爲 1。

private int getNumber(String text, IHanziSimilarContext similarContext) {
    Map<String, Integer> map = similarContext.bihuashuData().dataMap();
    Integer number = map.get(text);
    if(number == null) {
        return 1;
    }
    return number;
}

java 完整實現

我們把所有的碎片拼接起來,就得到一個完整的實現。

/**
 * 拆字
 *
 * @author 老馬嘯西風
 * @since 1.0.0
 */
public class ChaiziSimilar implements IHanziSimilar {

    @Override
    public double similar(IHanziSimilarContext similarContext) {
        String hanziOne = similarContext.charOne();
        String hanziTwo = similarContext.charTwo();

        int numberOne = getNumber(hanziOne, similarContext);
        int numberTwo = getNumber(hanziTwo, similarContext);

        // 拆分
        char[] charsOne = getSplitChars(hanziOne);
        char[] charsTwo = getSplitChars(hanziTwo);

        int minLen = Math.min(charsOne.length, charsTwo.length);

        // 比較
        double totalScore = 0.0;
        for(int i = 0; i <  minLen; i++) {
            char iChar = charsOne[i];
            String textChar = iChar+"";
            if(ArrayPrimitiveUtil.contains(charsTwo, iChar)) {
                int textNumber = getNumber(textChar, similarContext);

                double scoreOne = textNumber*1.0 / numberOne * 1.0;
                double scoreTwo = textNumber*1.0 / numberTwo * 1.0;

                totalScore += (scoreOne + scoreTwo) / 2.0;
            }
        }

        return totalScore * similarContext.chaiziRate();
    }

    /**
     * 獲取拆分後對應的拆分字符
     * @param charWord 字符
     * @return 結果
     */
    private char[] getSplitChars(String charWord) {
        List<String> stringList = ChaiziHelper.chai(charWord.charAt(0));

        // 這裏應該選擇哪一個是有講究的。此處爲了簡單,默認選擇第一個。
        String string = stringList.get(0);

        return string.toCharArray();
    }

    /**
     * 獲取筆畫數
     * @param text 文本
     * @param similarContext 上下文
     * @return 結果
     */
    private int getNumber(String text, IHanziSimilarContext similarContext) {
        Map<String, Integer> map = similarContext.bihuashuData().dataMap();

        Integer number = map.get(text);
        if(number == null) {
            return 1;
        }

        return number;
    }

}

小結

本文引入了漢字拆字,進一步豐富了相似度的實現。

當然,實現本身依然有很多值得提升的地方,比如拆分後的選擇,是否可以遞歸拆分等,這個還是留給後人研究吧。

我是老馬,期待與你的下次重逢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章