- sc.lm 改用 kenlm 的 LAZY 载入:Windows 上默认的 POPULATE_OR_READ 会退成 READ, 把 33 MB 整份拷进私有堆且永不释放;现在是 MapViewOfFile 只读映射,可被系统回收 - dict_japanese.dat 在 Windows 上同样改为只读映射,与 POSIX 分支一致 - 神经整句模型改为开关首次生效时才载入(LazySentenceModel),默认关闭的桌面模型 不再占用约 24 MB - 新增 reset_sentence_cache():重排结果与上文只影响词格整句,只清 series 一层, 逐前缀查库与切分缓存保持热;set_rescoring_context 也改用它 - CircularBuffer 新增 find():命中时返回指针而不复制整份候选列表,并改为真正的 LRU; 全拼、双拼每键的命中路径改用它 - Google 解码器去掉每次调用前的 im_reset_search,沿用上游的公共前缀增量搜索 - 测试:出货 sc.lm 能映射载入并正常打分;增量搜索与从头搜结果逐步一致
191 lines
5.7 KiB
C++
191 lines
5.7 KiB
C++
#include "language_model.h"
|
||
|
||
#include <cmath>
|
||
#include <exception>
|
||
#include <map>
|
||
#include <mutex>
|
||
#include <new>
|
||
#include <type_traits>
|
||
|
||
#include "lm/config.hh"
|
||
#include "lm/model.hh"
|
||
#include "lm/state.hh"
|
||
#include "lm/word_index.hh"
|
||
#include "util/string_piece.hh"
|
||
|
||
namespace ngram
|
||
{
|
||
namespace
|
||
{
|
||
|
||
// 与 libime 的 DEFAULT_LANGUAGE_MODEL_UNKNOWN_PROBABILITY_PENALTY 取同一个值
|
||
// (libime/core/constants.h)。log10(1/6e7) ≈ -7.78。出货的 sc.lm 就是按这个
|
||
// 量级标定的,换数值等于整体重调未登录词相对已登录词的代价。
|
||
constexpr float kUnknownProbability = 1.0F / 60000000.0F;
|
||
|
||
// -a 22 -q 4 trie 生成的就是这个模型类型,必须与 tools/build_binary_main.cc
|
||
// 的调用参数保持一致,否则载入时会抛 FormatLoadException。
|
||
using Model = lm::ngram::QuantArrayTrieModel;
|
||
|
||
static_assert(sizeof(lm::ngram::State) <= kStateSize, "State storage too small; check KENLM_MAX_ORDER");
|
||
static_assert(alignof(lm::ngram::State) <= 8, "State storage under-aligned");
|
||
static_assert(std::is_standard_layout_v<lm::ngram::State> && std::is_trivial_v<lm::ngram::State>,
|
||
"lm::ngram::State must stay POD for the raw-storage cast below");
|
||
static_assert(std::is_same_v<WordIndex, lm::WordIndex>, "WordIndex must match kenlm's");
|
||
|
||
// kenlm 只认窄字符路径。path::string() 在 Windows 上按 ANSI 代码页转换,遇到代码
|
||
// 页外的字符会抛 std::system_error("No mapping for the Unicode character..."),
|
||
// 所以这里一律走 UTF-8,由 kenlm_file_io.h 在打开时转回宽字符。
|
||
std::string to_utf8(const std::filesystem::path &path)
|
||
{
|
||
const auto utf8 = path.u8string();
|
||
return std::string(utf8.begin(), utf8.end());
|
||
}
|
||
|
||
lm::ngram::State &as_lm(State &state)
|
||
{
|
||
return *reinterpret_cast<lm::ngram::State *>(state.data);
|
||
}
|
||
|
||
const lm::ngram::State &as_lm(const State &state)
|
||
{
|
||
return *reinterpret_cast<const lm::ngram::State *>(state.data);
|
||
}
|
||
|
||
} // namespace
|
||
|
||
class LanguageModel::Impl
|
||
{
|
||
public:
|
||
std::unique_ptr<Model> model;
|
||
std::string error;
|
||
State begin_state;
|
||
State null_state;
|
||
float unknown_penalty = std::log10(kUnknownProbability);
|
||
};
|
||
|
||
LanguageModel::LanguageModel(const std::filesystem::path &model_file) : impl_(std::make_unique<Impl>())
|
||
{
|
||
if (model_file.empty())
|
||
{
|
||
impl_->error = "empty model path";
|
||
return;
|
||
}
|
||
try
|
||
{
|
||
lm::ngram::Config config;
|
||
// 出货的模型不带 <s> / </s>,与 libime 一致地静默接受,否则 kenlm 会
|
||
// 在载入时抛异常。
|
||
config.sentence_marker_missing = lm::SILENT;
|
||
// 默认的 POPULATE_OR_READ 只在有 MAP_POPULATE 的 Linux 上映射,Windows 上会退成
|
||
// READ:整份 34 MB 拷进私有堆,常驻且换不出去。LAZY 在 Windows 上走
|
||
// MapViewOfFile 只读映射,页面按需调入、属于文件缓存,系统内存紧张时可直接丢弃,
|
||
// 词格没开时基本不占工作集。
|
||
config.load_method = util::LAZY;
|
||
impl_->model = std::make_unique<Model>(to_utf8(model_file).c_str(), config);
|
||
as_lm(impl_->begin_state) = impl_->model->BeginSentenceState();
|
||
as_lm(impl_->null_state) = impl_->model->NullContextState();
|
||
}
|
||
catch (const std::exception &e)
|
||
{
|
||
impl_->model.reset();
|
||
impl_->error = e.what();
|
||
}
|
||
catch (...)
|
||
{
|
||
impl_->model.reset();
|
||
impl_->error = "unknown error while loading language model";
|
||
}
|
||
}
|
||
|
||
LanguageModel::~LanguageModel() = default;
|
||
|
||
bool LanguageModel::valid() const
|
||
{
|
||
return impl_->model != nullptr;
|
||
}
|
||
|
||
const std::string &LanguageModel::error() const
|
||
{
|
||
return impl_->error;
|
||
}
|
||
|
||
WordIndex LanguageModel::index(std::string_view word) const
|
||
{
|
||
if (!impl_->model)
|
||
return 0;
|
||
return impl_->model->GetVocabulary().Index(StringPiece{word.data(), word.size()});
|
||
}
|
||
|
||
WordIndex LanguageModel::unknown() const
|
||
{
|
||
if (!impl_->model)
|
||
return 0;
|
||
return impl_->model->GetVocabulary().NotFound();
|
||
}
|
||
|
||
bool LanguageModel::is_unknown(WordIndex idx) const
|
||
{
|
||
return idx == unknown();
|
||
}
|
||
|
||
const State &LanguageModel::begin_state() const
|
||
{
|
||
return impl_->begin_state;
|
||
}
|
||
|
||
const State &LanguageModel::null_state() const
|
||
{
|
||
return impl_->null_state;
|
||
}
|
||
|
||
float LanguageModel::score(const State &in, WordIndex idx, State &out) const
|
||
{
|
||
if (!impl_->model)
|
||
return impl_->unknown_penalty;
|
||
// kenlm 的 Score 要求 in 与 out 不是同一个对象。
|
||
if (&in == &out)
|
||
{
|
||
State scratch = in;
|
||
return score(scratch, idx, out);
|
||
}
|
||
const float s = impl_->model->Score(as_lm(in), idx, as_lm(out));
|
||
return s + (is_unknown(idx) ? impl_->unknown_penalty : 0.0F);
|
||
}
|
||
|
||
float LanguageModel::score(const State &in, std::string_view word, State &out) const
|
||
{
|
||
return score(in, index(word), out);
|
||
}
|
||
|
||
float LanguageModel::single_word_score(const State &in, std::string_view word) const
|
||
{
|
||
State discarded;
|
||
return score(in, word, discarded);
|
||
}
|
||
|
||
void LanguageModel::set_unknown_penalty(float penalty)
|
||
{
|
||
impl_->unknown_penalty = penalty;
|
||
}
|
||
|
||
float LanguageModel::unknown_penalty() const
|
||
{
|
||
return impl_->unknown_penalty;
|
||
}
|
||
|
||
const LanguageModel &shared_language_model(const std::filesystem::path &model_file)
|
||
{
|
||
// 故意泄漏:模型可能在静态析构期还被其他单例引用,销毁顺序无法保证,而进程
|
||
// 退出时操作系统会回收这块只读映射(见构造函数里的 load_method)。
|
||
static std::mutex mutex;
|
||
static auto *cache = new std::map<std::string, std::unique_ptr<LanguageModel>>();
|
||
const std::lock_guard<std::mutex> guard(mutex);
|
||
auto &slot = (*cache)[to_utf8(model_file)];
|
||
if (!slot)
|
||
slot = std::make_unique<LanguageModel>(model_file);
|
||
return *slot;
|
||
}
|
||
|
||
} // namespace ngram
|