Files
fanlusky 80b1fc42a1 feat(engine): 词格整句改用 kenlm 三元模型打分
词格原先的整句打分是一份没校准过的启发式 unigram,质量很差。这里内嵌 libime
在用的 kenlm 查询子集,改由它的三元模型 sc.lm 来打路径分。

- engine/ngram 只编 kenlm 的 query / trie-build 路径。不定义 WITH_THREADS,
  parallel_read.cc 因此退化成串行实现,整个子集零 Boost 依赖。
  KENLM_MAX_ORDER=3 必须与建模阶数一致,改它等于读不懂已发布的模型文件。
- ngram::LanguageModel 是唯一 include kenlm 头文件的地方,对外只给出一个纯
  std 的头,_HAS_AUTO_PTR_ETC 和 kenlm 的 include 目录都保持 PRIVATE。
- shared_language_model 按资源路径共享一份 mmap。kenlm 的查询是只读的且把
  上下文放在调用方的 State 里,所以并发会话不需要额外加锁。
- 模型缺失时 valid() 为假,词格退回原来的启发式打分:整句候选只是变差,不会
  停止工作。测试覆盖了缺模型降级与按路径共享。
- 出货的 ARPA 不带 <s>/</s>,所以解码从 null_state() 起算而非 begin_state()。

kenlm 是上游副本,不重排版、不进 clang-format,其 LGPL-2.1+ 与 BSD 例外清单
已补进 THIRD_PARTY_NOTICES.txt。googlepinyinime 那条 Fallback 整句路径不变。
2026-09-19 10:08:39 +08:00

78 lines
3.1 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
#include <cstddef>
#include <filesystem>
#include <memory>
#include <string>
#include <string_view>
// kenlm 三元语言模型的封装。对标 libime 的 libime/core/languagemodel.h,
// 去掉了 fcitx-utils 依赖(FCITX_D 私有指针宏、stringutils、fs),并把
// "按语言在若干目录里找 .lm" 的 LanguageModelResolver 一并去掉——本仓的模型
// 路径由 RuntimePaths 决定,不读 LIBIME_MODEL_DIRS 环境变量。
//
// 这个头文件刻意不 include 任何 kenlm 头:kenlm 仍在用 C++17 移除的
// std::binary_function,需要 _HAS_AUTO_PTR_ETC 才能编过。把 kenlm 关在
// language_model.cpp 里,engine 的其余部分就不必继承那个宏。
namespace ngram
{
using WordIndex = unsigned int;
// 足以容纳 lm::ngram::State(三元模型下是 17 字节,对齐到 4)。
// language_model.cpp 里有 static_assert 校验尺寸和对齐,改 KENLM_MAX_ORDER
// 会在那里编译期报错,而不是在运行时读出错误的分数。
inline constexpr std::size_t kStateSize = 20 + sizeof(void *);
struct State
{
alignas(8) unsigned char data[kStateSize] = {};
};
class LanguageModel
{
public:
// 载入失败(文件不存在、格式不符、KENLM_MAX_ORDER 与模型阶数不一致)不抛
// 异常,构造出一个 valid() == false 的对象:输入法在模型缺失时必须仍然
// 可用,退回到调用方的启发式打分。
explicit LanguageModel(const std::filesystem::path &model_file);
~LanguageModel();
LanguageModel(const LanguageModel &) = delete;
LanguageModel &operator=(const LanguageModel &) = delete;
bool valid() const;
// 载入失败时的原因,valid() 为真时为空。仅用于日志。
const std::string &error() const;
WordIndex index(std::string_view word) const;
WordIndex unknown() const;
bool is_unknown(WordIndex idx) const;
// 句首状态。解码整句时从这里起步,这样模型里的 <s> 二元/三元才起作用。
const State &begin_state() const;
// 无上下文状态。给孤立词打分时用。
const State &null_state() const;
// log10 概率。未登录词额外吃 unknown_penalty()。
float score(const State &in, WordIndex idx, State &out) const;
float score(const State &in, std::string_view word, State &out) const;
float single_word_score(const State &in, std::string_view word) const;
void set_unknown_penalty(float penalty);
float unknown_penalty() const;
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
// 进程内按路径共享的模型。出货的 sc.lm 有 34 MB,全拼词典和双拼词典各载一份
// 会白白多映射一遍;kenlm 的查询全是只读的(Score 是 const,状态在调用方的
// State 里),共享实例可以被多个线程同时使用。
//
// 返回的引用在进程生命周期内始终有效,载入失败时返回的是一个 valid() == false
// 的实例,调用方照常判空即可。
const LanguageModel &shared_language_model(const std::filesystem::path &model_file);
} // namespace ngram