我的一个项目中,需要使用 Rust 将拼音转换成汉字。搜索了一下 Github,实现这个需求的库并不多,其主要思路都是借助 HMM 模型和 Viterbi 算法进行转换。Pinyin2Hanzi 这个库比较完善,不仅提供 HMM 模型转换,而且提供 DAG 模型转换。另一方面,这个库也提供了模型的训练代码。但是这个库是在 Python 上实现的。
而我的需求是要求原生 Rust 实现,不借助 PyO3 调用 Python 代码。因此需要将这个 Python 库翻译重写成 Rust 代码。而我之前并没有深入了解 HMM 和 DAG,这次重写的的时间又很紧张。
所以,我通过下面的一些手段和方法快速完成这次重写,成果就是 pinyinchch。
我只将 Pinyin2Hanzi 的推理代码进行了重写,训练代码并没有重写
核心代码转换
我使用 Qoder 提供的免费额度,使用 Qoder 的 IDE 将 Pinyin2Hanzi 直接翻译成 Rust 代码。没有设置提示词,也没有使用特殊的指令,在 Qoder 的对话面板中添加要重写的文件夹后,直接输入“用 Rust 代码重写”。期间 Qoder 会要求我确认一些命令,重写过程基本上花费了半个小时。并且 Qoder 还生成了一些示例代码和 README 文档。
整个过程体验还是很舒服的,不需要我做太多操作。生成的代码,都能顺利运行。我没有向 Qoder 输入更多指令,由 Python 到 Rust 中基本上是一对一翻译。得益于 Pinyin2Hanzi 这个库良好的代码风格,可以说为 Qoder 的翻译事半功倍。
Rust 代码重构
需要将生成的代码按照 Rust 的一些代码规范进行小范围重构。并且将代码合理拆分成不同的模块,方便后续迭代。主要是处理下面的这两个问题,如何分发 HMM 和 DAG 模型?以及,如何对字符串进行拼音分词?
分发 HMM 和 DAG 模型
我希望通过引入 crate 就可以使用默认的 HMM 和 DAG 模型,而不是其他项目使用 pinyinchch 后还需要配置模型。而 crates.io 有一个要求,上传的 crate 大小不能超过 10M。Pinyin2Hanzi 提供的 HMM 模型和 DAG 模型都是 JSON 格式,而且大小都超过了 10M。而且如果模型都放在 pinyinchch 中,很容易将 pinyinchch 的大小变得很大。所以将默认的 HMM 模型和 DAG 模型放在单独的 crate 中。
将模型嵌入代码前,需要对模型做一些额外的处理。模型都是静态资源,如果直接使用 include_str 嵌入 JSON,然后借助 serde_json 反序列化成对象,无疑会浪费很多内存和计算资源。换一种方式,将 JSON 转换成二进制文件,使用 include_bytes 嵌入,并借助零拷贝反序列技术,将模型顺利转换成对应的内存对象。
在 Rust 中零拷贝反序列化框架,比较优秀的是 rykv 这个库。我将 JSON 转换成 rykv 这一操作操作,放在 xtask 脚本中。使用 serde_json 将 JSON 全部反序列化后,使用 rykv 进行序列化并写入到文件中。实现这个转换的代码,只需要在模型对象中添加相关的注解,就可以生成对应的序列化和反序列化的代码。
为了方便在 pinyinchch 中共享这些类型,我单独创建了一个 pinyinchch-type。除了在 xtask 中需要使用 serde 生成代码歪,pinyinchch 中主要使用 rkyv 进行反序列化。这里为了减轻代码编译的压力,在 pinyinchch-type 中开启 feature,需要哪一种序列化和反序列化的代码,则使用对应的 feature。
pinyinchch 使用 snafu 进行错误处理。下面提供一些主要流程的代码
类型定义
完整代码见 pinyinchch-type/hmm.rs
#[derive(Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)
)]
pub struct HmmData {
pub data: HashMap<String, f64>,
pub default: f64,
}
反序列化和序列化
完整代码见 xtask/task.rs
let hmm_start = serde_json::from_reader::<_, HmmData>(reader).with_whatever_context( | _ | "Couldn't read hmm_start.json") ?;
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>( & hmm_start).with_whatever_context( | _ | "The hmm_start couldn't serialize to rkyv") ?;
将序列化后的二进制数据写入文件中
完整代码见 xtask/task.rs
let mut buf = Vec::new();
buf.write_all( & bytes).with_whatever_context( | _ | "Couldn't write to buf") ?;
// 写入文件
let file = File::create(bin_data_root_path.join( & new_file_name)).with_whatever_context( | _ | format!("Couldn't create {new_file_name}")) ?;
let mut writer = BufWriter::new(file);
writer.write_all( & buf).with_whatever_context( | _ | format!("Couldn't write to {new_file_name}")) ?;
writer.flush().with_whatever_context( | _ | "Couldn't flush {new_file_name}") ?;
嵌入和反序列化
完整代码见 pinyinchch-type/lib.rs
#[macro_export]
macro_rules! embed_data {
($name:ident,$t:ty,$byte:ident,$path:literal) => {
pub const $byte: &'static [u8] = include_bytes!($path);
pub static $name: ::std::sync::LazyLock<$t> = ::std::sync::LazyLock::new(|| {
let mut aligned = rkyv::util::AlignedVec::<16>::new();
aligned.extend_from_slice($byte);
rkyv::from_bytes::<$t, rkyv::rancor::Error>(&aligned).expect(concat!(
"Failed to crate ",
stringify!($name),
"stringify!($name)",
$path
))
});
};
}
拼音字符串分词
Pinyin2Hanzi 并不支持分词,它只将输入的拼音序列转换成汉字。所以需要对拼音进行分词。
提供两种分词方式,一种是字典分词 pinyin_split ,要求输入的字符串都是由合法拼音拼接成的。一种是前缀字典树分词 pinyin_split_by_trie_tokenizer,将这个字符串尽可能获取拼音。
Ext Link: https://github.com/ixmoyren/pinyinchch
评论区
写评论还没有评论