首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

中文文本自動糾錯在影視搜索中的應用與 Java 實現:基於 N-Gram 與詞典融合的工程化方案

     編輯:Java 相關知識

項目背景

視頻平台搜索框用戶輸入常含錯別字、同音字、漏字(如「非誠勿擾」輸入為「非誠無擾」)。為提升命中率與用戶體驗,需實現**毫秒級、高召回、可擴展**的中文糾錯服務。本文分享基於 **2-Gram 語言模型 + 領域詞典** 的融合方案,附核心 Java 實現與優化路徑。

技術方案選型對比

方案原理優點缺點適用場景
純詞典分詞匹配最大匹配/最少分詞 + 編輯距離實現簡單、解釋性強OOV (未登錄詞) 處理弱、組合詞覆蓋低垂直領域實體識別 (醫療、法律)
N-Gram 統計語言模型馬爾可夫假設:P(w1..wn) = ∏ P(wi|wi-n+1..wi-1)無需詞典、捕捉共現規律、處理 OOV需大規模語料、長距離依賴弱、冷啟動難通用搜索、輸入法、拼寫檢查
深度學習 (BERT/Seq2Seq)預訓練模型微調糾錯任務語義理解強、上下文建模佳推理延遲高、模型大、需 GPU、標註數據貴高精度要求、複雜語義糾錯
融合方案 (本文)領域詞典過濾候選 + 2-Gram 概率打分工程落地快、延遲低、解釋性強、領域適配性好長距離依賴受限、需維護詞典垂直搜索 (影視、電商、音樂)

核心算法流程

  1. 離線構建:
    • 收集領域語料 (影視劇名、演員、導演、標籤) → ICTCLAS 分詞 → 統計一元/二元詞頻 → 計算條件概率 P(w2|w1) = Count(w1,w2)/Count(w1)。
    • 構建領域實體詞典 (Trie 樹/DoubleArrayTrie),支援前綴匹配、模糊搜索。
  2. 在線糾錯:
    • 用戶查詢 `q` → 字符級分詞 → 找出詞典中**最長連續匹配子串** (Top-K)。
    • 對候選實體 `c` 計算得分:`Score(c) = λ * P_2gram(c) + (1-λ) * EditSim(q, c)`。
    • 閾值過濾 → 返回糾錯建議列表 (如「非誠勿擾」、「非誠勿擾十二月合集」)。

Java 核心實現

依賴




    com.ictclas
    ictclas
    2011



    org.apache.commons
    commons-lang3
    3.12.0

1. 分詞服務封裝


public class IctclasSegmenter implements AutoCloseable {
    private final ICTCLAS2011 ictclas;
    
    public IctclasSegmenter(String dictPath) {
        this.ictclas = new ICTCLAS2011();
        if (!ictclas.ICTCLAS_Init(dictPath.getBytes(StandardCharsets.GB2312), 0)) {
            throw new IllegalStateException("ICTCLAS 初始化失敗");
        }
        ictclas.ICTCLAS_SetPOSmap(2); // 北大二級標注集
    }
    
    public List segment(String text) {
        byte[] result = ictclas.ICTCLAS_ParagraphProcess(text.getBytes(StandardCharsets.GB2312), 0, 1);
        return Arrays.stream(new String(result, StandardCharsets.GB2312).split(" "))
                     .filter(s -> !s.isEmpty())
                     .collect(Collectors.toList());
    }
    
    @Override
    public void close() { ictclas.ICTCLAS_Exit(); }
}

2. 語言模型構建 (離線批處理)


public class NGramModelBuilder {
    private final Map unigram = new HashMap<>();
    private final Map bigram = new HashMap<>();
    private long totalUnigram = 0, totalBigram = 0;
    
    public void train(List tokens) {
        for (int i = 0; i < tokens.size(); i++) {
            String w1 = tokens.get(i);
            unigram.merge(w1, 1L, Long::sum);
            totalUnigram++;
            if (i + 1 < tokens.size()) {
                String w2 = tokens.get(i + 1);
                String key = w1 + "\t" + w2;
                bigram.merge(key, 1L, Long::sum);
                totalBigram++;
            }
        }
    }
    
    // 條件概率 P(w2|w1) = Count(w1,w2) / Count(w1)
    public double condProb(String w1, String w2) {
        long c1 = unigram.getOrDefault(w1, 0L);
        if (c1 == 0) return 0.0;
        long c12 = bigram.getOrDefault(w1 + "\t" + w2, 0L);
        return (double) c12 / c1;
    }
    
    // 序列對數概率 (避免下溢)
    public double logProb(List tokens) {
        double sum = 0;
        for (int i = 0; i < tokens.size() - 1; i++) {
            double p = condProb(tokens.get(i), tokens.get(i + 1));
            sum += (p > 0) ? Math.log(p) : -20; // 平滑懲罰
        }
        return sum;
    }
    
    // 序列化持久化 (Kryo/Protobuf/JSON)
    public void save(Path path) throws IOException { ... }
    public static NGramModelBuilder load(Path path) throws IOException { ... }
}

3. 實體詞典 (DoubleArrayTrie 實現前綴/模糊匹配)


public class EntityDictionary {
    private final DoubleArrayTrie trie = new DoubleArrayTrie<>();
    
    public void build(Collection entities) {
        entities.forEach(e -> trie.put(e, e)); // value 存實體標準名
    }
    
    // 返回查詢中所有匹配的實體 (最長優先)
    public List matchLongest(String query) {
        List matched = new ArrayList<>();
        for (int i = 0; i < query.length(); i++) {
            List> entries = trie.commonPrefixSearch(query.substring(i));
            if (!entries.isEmpty()) {
                // 取最長匹配
                entries.sort((a, b) -> b.getKey().length() - a.getKey().length());
                matched.add(entries.get(0).getValue());
            }
        }
        return matched.stream().distinct().collect(Collectors.toList());
    }
}

4. 糾錯服務整合


@Service
public class SpellCheckService {
    private final NGramModelBuilder langModel;
    private final EntityDictionary dict;
    private final IctclasSegmenter segmenter;
    private static final double LAMBDA = 0.7; // 語言模型權重
    
    public List correct(String query, int topK) {
        // 1. 字符級分詞 (細粒度)
        List chars = query.chars().mapToObj(c -> String.valueOf((char)c)).collect(toList());
        
        // 2. 詞典匹配候選實體
        List candidates = dict.matchLongest(query);
        if (candidates.isEmpty()) return Collections.emptyList();
        
        // 3. 打分排序
        List scored = candidates.stream()
            .map(cand -> {
                List candChars = cand.chars().mapToObj(ch -> String.valueOf((char)ch)).collect(toList());
                double lmScore = langModel.logProb(candChars); // 語言模型分
                double editSim = 1.0 - (double) StringUtils.getLevenshteinDistance(query, cand) / Math.max(query.length(), cand.length());
                double finalScore = LAMBDA * normalize(lmScore) + (1 - LAMBDA) * editSim;
                return new CorrectionCandidate(cand, finalScore, lmScore, editSim);
            })
            .sorted(Comparator.comparingDouble(CorrectionCandidate::score).reversed())
            .limit(topK)
            .collect(toList());
        
        return scored;
    }
    
    private double normalize(double logProb) {
        // 簡單 Sigmoid 歸一化,實際可用 MinMax/Percentile
        return 1.0 / (1.0 + Math.exp(-logProb / 10));
    }
    
    @Data @AllArgsConstructor
    public static class CorrectionCandidate {
        private String entity;
        private double score;
        private double lmScore;
        private double editSim;
    }
}

工程化優化關鍵點

優化點方案效果
分詞效能ICTCLAS JNI 呼叫開銷大 → 改用純 Java 分詞 (HanLP/Jieba) 或字符級 N-Gram 免分詞延遲從 5ms 降至 <1ms
詞典匹配Trie 樹 → DoubleArrayTrie (Aho-Corasick 多模匹配) → 單次掃描全匹配O(n) 匹配,無回溯
語言模型存儲HashMap → 機器學習特徵雜湊 / 量化概率表 (int16) / RocksDB 持久化內存從 2GB 降至 200MB
冷啟動模型預熱載入、異步重載、版本化熱更新啟動 <5s,零停機發布
召回擴展同音字映射拼音索引、編輯距離 1/2 近鄰生成、用戶行為挖掘 (點擊/轉化)覆蓋率 +35%

實測效果 (離線評測集 10k 查詢)

  • 準確率: 89.2% (Top-1 完全匹配標準實體)
  • 召回率: 82.5% (有糾錯需求的查詢被覆蓋)
  • 平均延遲: 2.3 ms (P99 < 10 ms,單機 4C8G)
  • Bad Case 分析:長尾實體詞典缺失、多義詞歧義 (如「蒼穹」→「鬥破蒼穹」vs「蒼穹之下」)、跨詞邊界錯別字。

結語與演進路線圖

  1. 短期: 接入拼音索引解決同音錯別字、引入用戶點擊行為重排序、詞典自動化更新管線。
  2. 中期: 輕量化 BERT (DistilBERT/TinyBERT) 微調糾錯任務,作為二階段精排模型,融合統計分數。
  3. 長期: 統一多模態搜索理解層 (文本/語音/圖像),構建查詢重寫 + 實體鏈接 + 意圖識別一體化服務。

源碼已開源:GitHub Repo,歡迎 Star、Issue、PR 共建。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved