Example

Tiger Code Sha1:一种以静态码表统合中英文的输入方案


源码与分发:https://github.com/Sha1rholder/tiger-code-sha1


Tiger Code Sha1是一套针对中英混输体验优化的输入法编排方案。汉字循形取码,英文依词频与词形关系补全,标点由Lua协同处理;候选在本地预先编排,完全禁用动态用户词库及其学习,跳出semantic analysis、context-based prediction、用户画像、LLM等其他闭源云端算法依赖的设计基础,实现超过商业输入法的输入体验

所谓中文输入法的本质,就是一套将键盘上有限的字母序列映射到庞杂中文字符的方案。拼音是一种办法:循读音而求字,规则大半已在学校学过,用来便觉得自然。然而字音、字形与字义各有分别,读音相同,未必是同一个字;字形已认得,也未必知道怎样读。输入法取其中哪一条线索,都须另立一层从按键到文字的约定。所谓易学,首先在于这层约定能借用多少已有的知识

拼音的便利与限制,恰出于同一处。汉语声韵有规律,故拼写易记;也因为有规律,字母便不能任意组合,前面一段既定,后面的选择往往已很有限。即使把shi打全,“是、事、市、式”仍须再辨,这便有了选重。以信息论的洞见来说,音码的一次输入的信息熵太低,太可预测,能消除的不确定性太少。容易预料的字母,带来的新信息少,同音的文字,更无从单凭完整读音区分。剩下的工作,或由使用者选词,或由输入法依据上下文推测,总须有一处承担。遗憾的是,目前还没有什么基于音码的输入方案能兼顾人类无法感知的延迟、低到可忽略的部署成本并消弭选重的心智负担

形码则取汉字的构形为依据,把笔画、部件组织为字根,再按规则取码。同音而异形的字,由此可以分开;字形的区别较读音丰富,若编码安排得当,便能更充分地利用字母组合,使较少的按键完成较多的辨字工作。这是形码具有较高效率上限的根由,但效率还须连同学习成本来算。字根如何拆、键位如何记,若时时停手思索,码长的便宜也就未必落到实处。编码的容量与人的熟习,两者须相济

当初选择学习虎码时也不懂这些,单是嫌拼音打单字选重太烦,也不懂什么退火算法和字根分布优化;后来另作修改,也只因原方案未能尽合日用之需。我所写的中英文约各占一半,英文长词又常记不全,写程序还离不开种种标点。中文的省键、英文的补全、混输时的确认方式——以及,臭名昭著的“汉字/ASCii”模式状态机,遂须一起考虑。Tiger Code Sha1便由这些具体需要逐渐形成:以虎码为基础,为程序员等没有生僻字需求的技术写作者生成词典,再结合自己设计的英文输入模式,设计一套可以为Rime所用的中英混合输入法编排方案

象其物宜:汉字编码与精粹组词

见天下之赜,而拟诸其形容,象其物宜,是故谓之象

要在不依赖候选字词与context的情况下输入庞杂汉字,最根本的凭借是稳定的编码。生成程序 zh_dict.rs按仓库中的字表收字,每字只留一码:先依原虎码权重确定编码,随后遇到更短且尚未被占用的编码,才作替换。少数不合个人习惯的字以及码长与字根繁复程度呈相反关系的字另列重编码表,改其码而保留其排序权重。这是一种有次序的取舍,不求一次全局最优算尽码位(当然,我也做不到),也不强求原有重码尽数消失;只求常用字少费键,编码好记、符合直觉,同一个字的输入路径不再纷歧

有虎码以几百次退火算法优化中文编码的底子,单字的输入方案不必大改。然后便是制作组词的规范

考世间中文词典,带词频的词典,不是闭源收费,就是质量堪忧(类似虎码秃版原本),或早已不再与时俱进。权衡之下,便用了不再受Q群等私域影响的wordfreq,作些过滤。原虎码词组从各字分别取码,本方案的自动词组则附在首字编码之下,例如d列“中、中国、中心”,sn列“计、计划、计算”。选词时只用已经记住的首字编码,余下的字由候选补全。可补之词很多,不能一概塞入,故按词频×Σ(余下各字码长+1)排次,其中的1是逐字输入时确认上屏所需的空格。常见而能多省键的词,所得分数便高:语料所记的是使用次数,排词所计的是长期可以省去的动作,两者在这里合为一个尺度

词多也有词多之累。程序先剔除单字和字集外条目,又删除包含其他已收录短词的长词,如“计算机”因含“计算”而被筛去。这一办法有所牺牲,却可抑制相近长词层层堆积。合并时,基础单字居前,手工词随后,自动词只将中文候选补至3项;已有的基础候选若更多,便不再补,且以一页9项为限。于是单字的位置得以保持,词组也对实在常用之组合有了捷径,不必为了打词再负担一套庞大的记忆

经纪条贯:Zipf词频与英文词缀的变化

物有本末,事有终始。知所先后,则近道矣

鄙人记性差,是背单词困难户。我常想在电脑上实现ipad英文键盘那样带有IME性质的拼写检查和补全的工具。限于能力,我希望候选能提示我想输入的单词及其正确拼写,又不能依赖context和AI技术(不只是指Transformer),又要出词候选顺滑,难在补全的铨次。我还是选了用wordfreq提供词频,English Speller Database提供正确拼写以去除互联网渣滓,二者相合,可以筛出有意义的英文单词;然而仅按频率排队,同一词的复数、过去式、进行时及派生形式,仍常挤在同一前缀之下。一页候选要么被极常用词的相近词形占满,要么因同一词的频率分散到多个变体中,而充斥看似不常用的候选,词典固然显得博学,找词的人却未必受益。英语的词形变化既有规律,这些条目之间便有可利用的关系,不能只当作互不相干的字符串

为此,我在英文排词程序 en_dict.rs中设计了一套基于有向无环图、按后缀归纳词形关系的方法。它由至少5个字符的基本形式出发,依追加后缀、`y -> ies`、去e加ing、辅音双写等规则,找出词频表中实际存在的变体,连成有向边。例如understand -> understanding -> understandings,前后既有直接关系,也有间接关系。每条边都指向严格更长的词,故不可能绕回原点;按长度排列节点,便可依次求得每个词全部的基本形式。这便是“图”的用处:把层层派生化为可逐步计算的关系,而无须逐词标注语义。关系据字面规则确立,仍是从打英文单词的实用角度出发,并不冒充完整的语义归纳与词法分析

有了关系,还须决定怎样影响次序。我的办法是比较变体与各个基本形式的原始词频:变体若更少见,便将它的原始频率加给基本形式,同时给自身记一次**“降权”**;若同样常见或更常见,则不作这笔调整。提权通通算毕,再累和词频使之contribute to高频形式的“提权词频”,按所记数逐次降低一个Zipf尺度——相当于词频被除以10。例如`understandings`若比上述两个基本形式都少见,便分别给它们贡献一次原始频率,自身所得分数则除以100。至于复数原本比单数常用的情形,自不能只凭一个s便定其高下;当前数据里的developers与developer正是如此

从结果上看,wordfreq所用的Zipf刻度,取每十亿词中出现次数的常用对数:数值为6,约每千词出现一次;为3,约每百万词出现一次。相差一级,频率相差十倍,除以10相当于降低一级。程序仍用原始频率相加,算完所得是排序分数;自动词的保留门槛为`10⁻⁸`,相当于Zipf值1。先构图评分,后按拼写库与门槛筛选,便使最后未被收录的词,也有机会通过变体关系参与评分

关系愈繁杂,其中奥妙愈要算计清楚。同一变体若循两条路径到达同一个基本形式,贡献只能算一次;传递的也始终是原始频率,不能把已经提权的数值再往上滚。这里便用得上Rust强大的类型系统与标准数据结构:Text、Code分开文本与编码,TextEn、TextLower在构造时约束ASCII英文文本及其小写形式;HashMap建立词与节点的对应,HashSet保存不重复的基本形式集合,从设计上就规避重复数据与冲突。反之,若以次次遍历穷举再作运算,犹治丝而棼之也(换Rust前我用Python写过类似算法,跑一次10多分钟给我14900K干报废了一个,还好有保修)

评分之后,再补足相应的大小写形式,并在各类别内依分数、长度、字面顺序排序。最终只把超过4个字符的词作为补全目标,将每个词的全部非空前缀写入词典,例如python自p至全词均有登记。因此Rime即使关闭通用补全,仍可直接查到这些候选。短词通常径直敲完,长词则不必全凭记忆;该记多少、该补多少,由这套静态词表分担

融会贯通:中英文码表的混合

天下同归而殊途,一致而百虑

合表之际,短码的冲突须另有安排,虎码的码长限制恰好提供了一条边界。它的单字编码最长四码,简码也在此范围内;取消自动上屏以后,打到第四个字母不会突然提交汉字,继续到第五个字母,便越过常规中文编码的范围。合表时,有中文候选的编码,英文接在其后;没有中文候选的编码,先放输入串本身,再放补全词。于是underst的首选仍是underst,后面才是understand、understood、understanding。这份原样输出的位置,对变量名、缩写尤其有用;短码与中文重合时,则可用Enter提交原始编码

个人常用而通用词表顾不到的内容,再以少量手工条目补入。中文自定义表按Tab分隔编码与文本,例如k加“能不能”,bl加两个HTML标签<blockquote></blockquote>用于向AI更高效地传递语义;手工添加的英文词条,在所属大小写类别中排于自动词之前或之后,再接受长度过滤与前缀展开。英文条目若与自动词或其他自定义词精确重复,生成时即报冲突。准备好词频数据,一次cargo run,重新部署Rime,修改便进入主词典。这里的个人词表是由人维护、编入文件的静态条目,方案完全禁用动态用户词库及其学习;sort: original保留生成时的次序,反复选择也不会暗中改变

精益求精:写代码与全半角标点

致广大而尽精微

词序安排妥当,输入仍可能败在一个括号上。写程序时敲下print(,若先被换成某个候选,再附上全角括号,前面省下的几键,转眼又花在删除上。标点虽然形小,在程序中却有确切的作用;输入中文时所需的便利,到了代码里便可能成为妨碍。要兼顾两者,须把符号的去向也纳入规则

本方案的Lua处理器位于常规标点处理之前:缓冲区中尚有编码,遇到ASCII符号,便执行`engine:commit_text(input .. symbol)`,将原串连同符号一起上屏并清空缓冲区。取的是实际输入,故print(、object.、foo_都能保持原样;缓冲区为空,则交回常规标点表。相应地,中文须先确认文字,再输入标点,不能靠逗号代替选字。这一约定只看可见的输入状态,无须辨认正在写哪一种语言,也不分析前文。Ctrl、Alt、Super组合键与释放事件不受干扰,Shift所产生的符号仍可照常使用

引号、尖括号与下划线另有一层处理:当前候选若是左引号,再按同一键便提交右引号;<、>、_也可由其中文符号候选切到原始符号。操作如双击,程序只检查当前选中的候选,并不计较两键相隔几毫秒;特殊映射先处理,随后才处理普通的原串提交。至于数学符号、希腊字母和emoji,则由F8切换的符号表提供。一个按键兼任数职,只要确认方式明确,也可以各得其所

执简驭繁:零词库学习、可预测性与输入的稀疏性

易则易知,简则易从

如今我将其用作跨Windows、macOS和Linux的日常输入方案(手机上没找到好用的九键方案暂时搞不定),不作语义分析,不取句子上下文,不调用AI,也不向云端索取候选;动态用户词库与学习功能则明确禁用。公开词频、拼写词表与少量手工偏好,在本地生成时已排成次序。因而新装之后便可照用,无须先“养”一阵词库;相同文件迁往另一台机器,位置也随之保留。有Lua支持的Rime前端,无论Weasel (Windows)、Squirrel (macOS)、fcitx5-Rime (Linux),都可承接同一套规则。放之四海,我确信它可跻身“世界上综合输入体验最好的中文输入方案”之一,甚至就是世界上最好的中英混输方案

实际使用并略作分析后,我又惊觉:即使将中文与英语这两个全球使用人数最多的语种的编码合在一起,再算上我奢侈的英文重码,整个未经压缩的纯文本词典也才28 Million Bytes左右……同时,“输入任意2、3、4个字母,候选数不超过5个的概率”分别是1.18%、82.90%和98.12%,信息熵在前4码中分布不均的程度高到离谱。人类两大语种从输入效率的角度竟“literally”稀疏至此,令人唏嘘

由此也能得到一点关于英汉文字系统的直观认识:字符虽繁,日常使用却远非均匀而无规律。中英文合入一表,许多汉字仍只须几个字母便可唯一辨出,许多英文长词也只须一个短前缀,便能在前列找到正确拼写。后者常还有其他匹配,便利来自次序,未必来自唯一;然而两种便利都说明,文字使用中的统计规律,已经提供了相当多的线索。完全禁用个人学习,仅借助人们共同的用字习惯便足矣

所谓文字使用中的低熵,与开篇所求的每键高效,正在这点相接:前者说明有冗余可省,后者要求把辨别目标所需的信息,尽量集中到少量按键里。香农将消息的意义与传输问题分开讨论,在此也有切近的例证。输入器依符号约定找到文字,依语言统计安排次序,便能完成许多工作;至于文字所要表达的意思,原在写作者心中。这个项目所做的,便是让已有的构形、拼写与使用规律各尽其用,使二十六个键足以承接两种文字,而少劳一些记忆,少费一些踌躇


文笔幼稚,囫囵写就,敬请斧正!

Inspired by the stylish documentation of Rime.