{"id":15650140,"url":"https://github.com/orangex4/simple-pinyin","last_synced_at":"2025-04-14T06:37:06.439Z","repository":{"id":129562397,"uuid":"510610199","full_name":"OrangeX4/simple-pinyin","owner":"OrangeX4","description":"simple-pinyin 基于隐马尔可夫模型的简易拼音输入法（拼音转汉字）","archived":false,"fork":false,"pushed_at":"2024-09-18T11:47:16.000Z","size":45365,"stargazers_count":45,"open_issues_count":1,"forks_count":4,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-27T20:22:11.141Z","etag":null,"topics":["ime","pinyin"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/OrangeX4.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-05T06:15:36.000Z","updated_at":"2025-02-19T12:03:05.000Z","dependencies_parsed_at":null,"dependency_job_id":"f5a04a8d-7e03-414c-aa3b-090fbbf88f9a","html_url":"https://github.com/OrangeX4/simple-pinyin","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OrangeX4%2Fsimple-pinyin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OrangeX4%2Fsimple-pinyin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OrangeX4%2Fsimple-pinyin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OrangeX4%2Fsimple-pinyin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/OrangeX4","download_url":"https://codeload.github.com/OrangeX4/simple-pinyin/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248836291,"owners_count":21169370,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ime","pinyin"],"created_at":"2024-10-03T12:33:36.233Z","updated_at":"2025-04-14T06:37:06.395Z","avatar_url":"https://github.com/OrangeX4.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 简易拼音输入法（拼音转汉字）\n\n## 一、使用介绍\n\n代码 Repo：https://github.com/OrangeX4/simple-pinyin\n\n```sh\ngit clone https://github.com/OrangeX4/simple-pinyin.git\n```\n\n首先要安装依赖：\n\n```sh\n# python\ncd pinyin\npip install -r requirements.txt\n\n# web\ncd web\nnpm install\n```\n\n简易拼音输入法提供了两种 UI，分别是命令行和前端模式。\n\n首先是 **命令行模式**：\n\n```sh\ncd pinyin\npython ./__init__.py\n```\n\n然后输入 `cli` 就能进入命令行模式的拼音输入法了。\n\n![](https://picgo-1258602555.cos.ap-nanjing.myqcloud.com/2022-07-05-13-37-49.png)\n\n其次是 **前端模式**：\n\n```sh\ncd pinyin\npython ./__init__.py\n```\n\n然后输入 `server` 就能启动一个服务器后端。\n\n再输入\n\n```sh\ncd web\nnpm run start\n```\n\n就能启动一个前端界面了：\n\n![](https://picgo-1258602555.cos.ap-nanjing.myqcloud.com/2022-07-05-13-28-32.png)\n\n当然你也可以选择 **在 Python 中导入该输入法** 并使用：\n\n```python\nfrom pinyin.ime import ime\n\nprint(ime('jintian', limit=1))  # 基础功能\nprint(ime('jintain', limit=1))  # 纠错功能\nprint(ime('ji\\'ntian', limit=1))  # 分词功能\nprint(ime('jintiantianqibucuo', limit=1))  # 短句功能\nprint(ime('jttqbc', limit=1))  # 首字母功能\nprint(ime('xiaolian', limit=1))  # emoji 功能\nprint(ime('nanjingdaxuerengongzhinengxueyuan', limit=1))  # 南京大学人工智能学院\nprint(ime('nanjingdx', limit=1))  # 南京大学\n```\n\n然后终端会输出\n\n```python\n[(('jin', 'tian'), '今天', -8.551883029536198)]\n[(('jin', 'tian'), '今天', -8.551883029536198)]\n[(('ji', 'ni', 'tan'), '记念堂', -23.44058007221551)]\n[(('jin', 'tian', 'tian', 'qi', 'bu', 'cuo'), '今天天气不错', -25.909553919977228)]\n[(('j', 't', 't', 'q', 'b', 'c'), '今天天气不错', -28.974507662573167)]\n[(('xiao', 'lian'), '笑脸', -11.639523579866978), (('xiao', 'lian'), '😄', -11.639533579866978)]\n[(('nan', 'jing', 'da', 'xve', 'ren', 'gong', 'zhi', 'neng', 'xve', 'yuan'), '南京大学人工智能学院', -53.58047344465382)]\n[(('nan', 'jing', 'd', 'x'), '南京大学', -17.561359659026092)]\n```\n\nPython 的项目结构为：\n\n```text\n.\n├── __init__.py\n├── cli  # 命令行模式\n│   ├── __init__.py\n├── cut  # 拼音划分代码\n│   ├── __init__.py\n│   └── cut_pinyin.py\n├── data  # 数据保存位置\n│   ├── ReadMe.txt.txt\n│   ├── all_pinyin.txt\n│   ├── checkpoints\n│   │   ├── hmm_emission_counter.json\n│   │   ├── hmm_start_counter.json\n│   │   └── hmm_transition_counter.json\n│   ├── emoji.json\n│   ├── emoji.txt\n│   ├── global_wordfreq.release.txt\n│   ├── hmm_emission.json\n│   ├── hmm_reversed_emission.json\n│   ├── hmm_reversed_transition.json\n│   ├── hmm_start.json\n│   ├── hmm_transition.json\n│   ├── intact_pinyin.txt\n│   └── test_words.txt\n├── hmm  # 隐马尔可夫模型\n│   ├── __init__.py\n│   └── viterbi.py\n├── ime  # 输入法函数\n│   ├── __init__.py\n│   ├── emoji.py  # emoji 处理及加载\n│   └── ime.py\n├── requirements.txt\n├── server  # 服务器\n│   ├── __init__.py\n├── tests  # 单元测试\n│   ├── __init__.py\n│   ├── test_cut_pinyin.py\n│   └── test_viterbi.py\n└── train  # 隐马尔可夫模型训练\n    ├── __init__.py\n    ├── dataset.py  # 加载数据\n    └── train_hmm.py  # 具体训练代码\n```\n\n以下是具体实现过程的介绍。\n\n## 二、拼音划分\n\n首先要解决的问题是，如何对长拼音序列进行划分。\n\n由于我们只有文本语料，没有输入法语料（即用户的输入习惯），我们只能够通过编写规则的方式进行拼音划分。\n\n- **普通情况**：完整的、无错的、没有歧义的拼音序列，例如 `kongqi` 只能划分为 `kong'qi`，即「空气」，并且是完整的、无错的、没有歧义的。这种情况处理起来比较简单，只需要按照「声母」和「韵母」的简单划分和匹配即可。\n- **拼音简写**：用户在输入的时候，往往不会输入完整的拼音，而是输入一部分拼音。而缩写的情况又有几种类别，由于没有输入法语料，我就按照我自己使用的简写方式频率排列，以「中国」举例如下：\n    - **先整后简**：`zhong'g`，也即「去尾字母完整划分」，我们在输入一个词语的时候，常常会输入了前一个字的完整拼音，又输入后一个字的开头拼音，输入法就会匹配到对应的词语，不需要输入完整的拼音。\n    - **完全简写**：`z'g`，我们只输入词语的拼音首字母，也是比较常见的情况。\n    - **先简后整**：`z'guo`，比较少见，一般出现在想要使用「完全简写」的方式输入，但是发现匹配不到，因此再输入后一个字的\n    - **部分简写**：`zh'g`，不太常见，但是也存在这种情况。\n- **顺序错误**：用户在输入拼音的时候，可能会因为打字打得比较快，有一些字的拼音的顺序弄反了，例如「小路」的 `xiao'lu` 打成了 `xaio'lu`，这时候输入法应该给予纠正。\n- **存在歧义**：例如 `xianmianguan` 既可以划分为 `xian'mian'guan`，即「鲜面馆」，也可以划分为 `xi'an'mian'guan`，即「西安面馆」，这时候拼音划分就存在着歧义。如果涉及到拼音简写，则歧义会更多，如 `zhongguo` 甚至可以划分为 `z'hong'gu'o`。\n\n我们需要将拼音划分分为两个不同的场景，不同场景的应用不同。第一个场景是「用户输入拼音序列划分」，第二个场景是「文字转拼音后划分」，前者用于预测，后者用于训练。\n\n用户输入拼音序列划分只需要使用简单的动态规划即可实现，将所有合法的拼音序列划分方式都给列举出来，然后同时进行预测。\n\n首先是输入拼音序列的划分，可以通过 `from pinyin.cut import cut_pinyin` 引入，具体的实现代码如下，使用了简单的动态规划，并加入了使用 `'` 分词的功能：\n\n```python\n# 加载完整拼音对应的拼音表 data/intact_pinyin.txt, 共 416 个\nintact_pinyin_set = set()\nwith open('data/intact_pinyin.txt', 'r', encoding='utf-8') as f:\n    intact_pinyin_set = set(s for s in f.read().split('\\n'))\n\n# 生成带残缺部分的拼音, 例如 'ruan' 对应的 'r', 'ru' 和 'rua', 共 504 个, 对应的拼音表为 data/all_pinyin.txt\nall_pinyin_set = set(s[:i] for s in intact_pinyin_set for i in range(1, len(s) + 1))\n\n# 用于保存动态规划答案的字典\nintact_cut_pinyin_ans = {}\nall_cut_pinyin_ans = {}\n# 动态规划判断进行拼音划分\ndef cut_pinyin(pinyin: str, is_intact=False, is_break=True):\n    '''\n    进行拼音划分, 返回拼音划分结果列表\n    pinyin: 待划分的拼音, 并且是无空格字符串, 例如 `kongjian`\n    is_intact: 拼音是否需要完整匹配, 默认为 False, 可以使用残缺部分的拼音进行分词\n    is_break: 是否开启分隔符, 开启后可以使用 ' 进行分割, 例如 `kong'jian`\n    \n    return: 拼音划分结果列表, 例如 `cut_pinyin('kongjian', True)`, \n            会返回 `[('kong', 'jian'), ('kong', 'ji', 'an')]`\n    '''\n    if is_intact:\n        pinyin_set = intact_pinyin_set\n        ans_dict = intact_cut_pinyin_ans    \n    else:\n        pinyin_set = all_pinyin_set\n        ans_dict = all_cut_pinyin_ans\n    # 如果保存有, 直接返回保存结果\n    if pinyin in ans_dict:\n        return ans_dict[pinyin]\n    # 如果 is_break, 就进行分割\n    if is_break and '\\'' in pinyin:\n        pinyins = pinyin.split('\\'')\n        components = [cut_pinyin(p, is_intact, False) for p in pinyins]\n        ans = components[0]\n        for i in range(1, len(components)):\n            ans = [p1 + p2 for p1 in ans for p2 in components[i]]\n        return ans\n    # 如果没有, 递归地动态规划生成\n    ans = [] if pinyin not in pinyin_set else [(pinyin,)]\n    for i in range(1, len(pinyin)):\n        # 进行划分 pinyin[:i], 如果是正确拼音, 就继续动态规划\n        if pinyin[:i] in pinyin_set:\n            appendices = cut_pinyin(pinyin[i:], is_intact, is_break=False)\n            for appendix in appendices:\n                ans.append((pinyin[:i],) + appendix)\n    ans_dict[pinyin] = ans\n    return ans\n```\n\n在 `cut_pinyin` 函数的基础上，我们可以加入拼音纠错功能，从第二个字母开始依次交换连续的两个字母，看看是否能够进行完整拼音划分，能的话就加入最终结果，进而实现纠错功能。\n\n```python\ndef cut_pinyin_with_error_correction(pinyin: str):\n    '''\n    纠错匹配, 从第二个字母开始, 依次交换两个连续字母并进行*完整划分*.\n    如果完整划分返回非空列表, 即匹配成功, 并加入到返回字典中.\n    pinyin: 待纠错划分的拼音\n\n    return: 返回字典, 字典的 key 为纠错后的拼音序列, value 为匹配成功的划分结果.\n            并且会包含一个 key = 'all' 的项, 包括了所有 value.\n    '''\n    ans = {}\n    for i in range(1, len(pinyin) - 1):\n        # 避免交换分词符\n        if pinyin[i-1] == '\\'' or pinyin[i] == '\\'' or pinyin[i + 1] == '\\'':\n            continue\n        key = pinyin[:i] + pinyin[i + 1] + pinyin[i] + pinyin[i + 2:]\n        value = cut_pinyin(key, is_intact=True)\n        if value:\n            ans[key] = value\n    ans['all'] = [p for t in ans.values() for p in t]\n    return ans\n```\n\n最后加入去尾字母划分功能，最后聚合一下，就实现了全部的输入拼音序列划分功能。\n\n```python\ndef cut_pinyin_with_strategy(pinyin: str):\n    '''\n    使用各种策略对拼音进行划分, 其中包括:\n    1. 完整划分\n    2. 去尾字母完整划分\n    3. 纠错划分\n    4. 去尾字母纠错划分\n    5. 模糊划分\n    6. 结果综合\n\n    pinyin: 待划分的拼音\n    '''\n    ans = {\n        'intact': cut_pinyin(pinyin, is_intact=True),\n        'intact_tail': [] if pinyin[-1] not in all_pinyin_set else [t + (pinyin[-1],) for t in cut_pinyin(pinyin[:-1], is_intact=True)],\n        'error_correction': cut_pinyin_with_error_correction(pinyin)['all'],\n        'error_correction_tail': [] if pinyin[-1] not in all_pinyin_set else [t + (pinyin[-1],) for t in cut_pinyin_with_error_correction(pinyin[:-1])['all']],\n        'fuzzy': cut_pinyin(pinyin, is_intact=False),\n        'combine': [],\n    }\n    ans['combine'] = set(ans['intact'] + ans['intact_tail'] + ans['error_correction'] + ans['error_correction_tail'] + ans['fuzzy'])\n    return ans\n```\n\n顺带一提，很多人误认为「略」的拼音是 `lue`，实际上应该是 `lve`，因此我们需要对拼音进行规范化：\n\n```python\ndef normlize_pinyin(pinyin: str):\n    \"\"\"\n    规范化拼音\n    将所有 ue 转化为 ve\n    \"\"\"\n    return pinyin.replace('ue', 've', -1)\n```\n\n其次是文字转拼音后划分, 这时候拼音划分是已知的, 所以只需进行简写处理, 然后给不同简化方式 **划分权重** 即可. 这一步需要在生成发射矩阵的时候设置.\n\n\n## 三、获取语料\n\n这里我们直接使用了「北京语言大学 BCC 语料库 http://bcc.blcu.edu.cn」的词频语料 `global_wordfreq.release.txt`，最终解释权归北语大数据与教育技术研究所所有。\n\n其中的语料格式大致为：\n\n```text\n第\t2002074595\n的\t943370349\n了\t255733044\n在\t197672850\n是\t171296602\n我\t169391220\n~\t44057380\n非常\t8056541\n一直\t8013106\n不会\t8010572\n应该\t8001472\n```\n\n即「词语 + 词频」的组合，不过注意到有一些非中文的词语，例如 `~\t44057380`，因此我们需要进行过滤，最后再使用 Python 的生成器功能，我们就能解耦合地进行数据的读入，具体代码位于 `train/dataset.py`。\n\n```python\ndef is_Chinese(word):\n    '''\n    判断一个字符串是否全由汉字组成, 用于过滤文本\n    '''\n    return all('\\u4e00' \u003c= ch \u003c= '\\u9fff' for ch in word)\n\ndef iter_word_and_freq():\n    \"\"\"\n    词频数据集, 迭代地返回 (word, freq)\n    \"\"\"\n    with open(words_path, 'r', encoding='utf-8') as f:\n        for line in f:\n            try:\n                word, frequency = line.split()\n                # 进行过滤\n                if is_Chinese(word):\n                    yield word, int(frequency)\n            except Exception as e:\n                pass\n```\n\n\n## 四、隐马尔可夫模型\n\n**隐马尔可夫模型** (Hidden Markov Model, HMM) 是一种统计模型，用来描述一个含有隐含未知参数的马尔可夫过程。\n\n隐马尔可夫模型有两个关键的概念：**状态** (state) 和 **观测** (observation)。隐马尔可夫链随机生成的状态的序列，称为状态序列；每个状态生成一个观测，由此产生的随机的观测的序列，称为观测序列。序列的每一个位置又可以看作一个时刻。\n\n![](https://picgo-1258602555.cos.ap-nanjing.myqcloud.com/2022-07-04-20-23-37.png)\n\n对于拼音输入法来说，**状态就是一个个汉字**，**观测就是对应的拼音**。作为状态的汉字是不知道的，唯一知道的只有用户输入的观测，也就是拼音。\n\n隐马尔可夫模型由初始状态概率向量 $\\pi$、状态转移概率矩阵 $A$ 和观测概率矩阵（也称为发射矩阵）$B$ 决定。因此，隐马尔可夫模型 $\\lambda$ 可以用三元组符号表示，即\n\n$$\n\\lambda = (\\pi, A, B)\n$$\n\n$\\pi, A, B$ 称为隐马尔可夫模型三要素。\n\n由定义可知，隐马尔可夫模型有两个重要的基本假设：\n\n- **齐次马尔可夫性假设**：隐马尔可夫链在任意时刻 $t$ 的状态只依赖于前一时刻 $t-1$ 的状态，与其他时刻的状态及观测无关，也与时刻 $t$ 的数值无关。\n- **观测独立性假设**：任意时刻的观测只与该时刻的状态有关，与其他观测及状态无关，也与时刻 $t$ 的数值无关。\n\n要使用隐马尔可夫模型来实现智能拼音输入法，我们 **首先要通过语料生成对应的隐马尔可夫模型**。\n\n要生成隐马尔可夫模型也十分简单，根据 **极大似然估计法** 的结果，我们只需要用 **频率** 代替 **概率** 即可，也就是要统计语料中的频率，从而生成 $(\\pi, A, B)$。\n\n对应的统计频率的代码相对简单，这里就不过多赘述，需要看的话可以看看 `train/train_hmm.py` 这个文件的代码。\n\n不过值得一提的是，由于状态转移矩阵 $A$ 和发射矩阵 $B$ 都是稀疏矩阵，即大部分位置均为 $0$，如果用普通的保存方式会十分占据空间，因此我们使用 JSON 格式将 Python 的字典保存下来。后续使用的时候，只需要加载 JSON 文件，就能重新恢复为位于内存中的 Python 字典了。对应生成的 JSON 文件位于 `data/hmm_xxx.json`.\n\n另外，由于后续概率计算数字可能越算越小，导致计算机无法计算，所以我们对所有概率都进行了自然对数运算处理。\n\n生成的 `hmm_start.json` 的部分内容：\n\n```json\n{\n    \"一\": -5.081293678906249,\n    \"丁\": -9.192433659783104,\n    \"丂\": -19.34085746030175,\n    \"七\": -10.159009715890134,\n    \"丄\": -16.747217610377284,\n    \"丅\": -16.301496324358553,\n    \"丆\": -19.25047339883348,\n    \"万\": -8.7175005342102\n}\n```\n\n生成的 `hmm_transition.json` 的部分内容：\n\n```json\n{\n    \"渗\": {\n        \"入\": -2.439070674759006,\n        \"出\": -1.9373713062580147,\n        \"性\": -4.919727895519666,\n        \"析\": -5.954770052141584,\n        \"水\": -3.190476888777123,\n        \"流\": -3.756776835259451,\n        \"漏\": -2.4294073791727087,\n        \"透\": -0.5143045447650618,\n    },\n    \"渚\": {\n        \"乡\": -4.32027991176323,\n        \"停\": -6.5756121199067294,\n        \"公\": -4.33654043263501,\n        \"山\": -4.052965142135882,\n        \"文\": -0.50469675623453,\n        \"村\": -4.877881600326951,\n        \"港\": -2.8828938894856275,\n        \"湖\": -2.9667753734663296,\n        \"镇\": -1.7247845019528727,\n    }\n}\n```\n\n生成的 `hmm_emission.json` 的部分内容：\n\n```json\n{\n    \"一\": {\n        \"y\": -0.9808292530117262,\n        \"yi\": -0.4700036292457356\n    },\n    \"模\": {\n        \"m\": -0.9808292530117262,\n        \"mo\": -0.5430199262500778,\n        \"mu\": -3.12336226291328\n    }\n}\n```\n\n训练完隐马尔可夫模型后，我们就要进行预测了。\n\n隐马尔可夫模型的预测问题，也称为解码 (decoding) 问题，就是在已知隐马尔可夫模型 $\\lambda=(\\pi, A, B)$ 和观测序列 $O=(o_1, o_2, \\cdots, o_{T})$ 的情况下，求使得观测序列条件概率 $P(I|O)$ 最大的状态序列 $I=(i_1, i_2, \\cdots, i_{T})$. 即给定观测序列，求最有可能的状态序列。\n\n这里我们使用维特比算法 (Viterbi algorithm) 来进行预测。\n\n为了加速维特比算法, 我们要先通过倒查表的方式计算出 `reversed_emission_matrix` 和 `reversed_transition_matrix`.\n\n```python\ndef gen_reversed_matrix(emission_matrix, transition_matrix):\n    '''\n    生成 emission_matrix 的倒查表, 即 reversed_emission_matrix[拼音] = {汉字: 概率}\n\n    生成 transition_matrix 和 emission_matrix 的联合倒查表,\n    即 reversed_transition_matrix[前一个汉字][拼音] = (后一个汉字, 最大概率)\n    '''\n    # 生成 emission_matrix 的倒查表, 即 reversed_emission_matrix[拼音] = {汉字: 概率}\n    reversed_emission_matrix = {}\n    for char in tqdm(emission_matrix):\n        for pinyin, prob in emission_matrix[char].items():\n            if pinyin not in reversed_emission_matrix:\n                reversed_emission_matrix[pinyin] = {}\n            reversed_emission_matrix[pinyin][char] = prob\n    json2file(reversed_emission_matrix, hmm_reversed_emission_path)\n\n    # 生成 transition_matrix 和 emission_matrix 的联合倒查表,\n    # 即 reversed_transition_matrix[前一个汉字][拼音] = (后一个汉字, 最大概率)\n    reversed_transition_matrix = {}\n    for previous in tqdm(transition_matrix):\n        reversed_transition_matrix[previous] = {}\n        for behind in transition_matrix[previous]:\n            for pinyin in emission_matrix[behind]:\n                prob = transition_matrix[previous][behind] + emission_matrix[behind][pinyin]\n                if pinyin not in reversed_transition_matrix[previous]:\n                    reversed_transition_matrix[previous][pinyin] = (behind, prob)\n                elif prob \u003e reversed_transition_matrix[previous][pinyin][1]:\n                    reversed_transition_matrix[previous][pinyin] = (behind, prob)\n    json2file(reversed_transition_matrix, hmm_reversed_transition_path)\n```\n\n然后是维特比算法的具体代码:\n\n```python\ndef viterbi(pinyin, limit=10):\n    \"\"\"\n    viterbi 算法\n\n    pinyin: 拼音元组, 例如 ('jin', 'tian')\n\n    return: 返回 limit 个最可能的汉字序列, 但是是 1 个全局最优解和 limit - 1 个局部最优解\n            并且返回剩余未搜索的拼音\n    \"\"\"\n    # 初始化, 找出第一个拼音对应的汉字以及 start 和 emission 概率之积 (对数下为相加)\n    char_and_prob = ((ch, start_vector[ch] + reversed_emission_matrix[pinyin[0]][ch]) for ch in reversed_emission_matrix[pinyin[0]])\n    # 取出概率最大的 limit 个\n    V = {char: prob for char, prob in heapq.nlargest(limit, char_and_prob, key=lambda x: x[1])}\n\n    for i in range(1, len(pinyin)):\n        py = pinyin[i]\n\n        prob_map = {}\n        for phrase, prob in V.items():\n            previous = phrase[-1]\n            if previous in reversed_transition_matrix and py in reversed_transition_matrix[previous]:\n                state, new_prob = reversed_transition_matrix[previous][py]\n                prob_map[phrase + state] = new_prob + prob\n\n        if prob_map:\n            V = prob_map\n        else:\n            # 没有概率, 因此没有完全搜索, 返回目前结果和未搜索拼音 pinyin[i:]\n            return sorted(V.items(), key=lambda x: x[1], reverse=True), pinyin[i:]\n    return sorted(V.items(), key=lambda x: x[1], reverse=True), ''\n```\n\n最后综合我们的分词功能和维特比算法，即可得到一个较为智能的输入法了。\n\n```python\n# 缓存结果, 加快判断\ndp = {}\ndef ime(pinyin: str, limit=7):\n    '''\n    输入法函数, 综合分词和维特比算法的最终结果\n    '''\n    if pinyin in dp:\n        return dp[pinyin][:limit]\n    # 计算结果\n    result = []\n    # 获取分词结果\n    cut = cut_pinyin_with_strategy(normlize_pinyin(pinyin))\n    # 先尝试完整划分\n    for pinyin in cut['intact'] + cut['intact_tail']:\n        vit, remain_pinyin = viterbi(pinyin)\n        if not remain_pinyin:\n            result.extend([(pinyin,) + t for t in vit])\n    # 如果完整划分的最小拼音大小小于等于 3, 则进行纠错\n    if not result or min([len(pinyin) for pinyin in cut['intact'] + cut['intact_tail']]) \u003c= 3:\n        for pinyin in cut['error_correction'] + cut['error_correction_tail']:\n            vit, remain_pinyin = viterbi(pinyin)\n            if not remain_pinyin:\n                result.extend([(pinyin,) + t for t in vit])\n    # 如果结果为空, 则进行模糊划分\n    if not result:\n        for pinyin in cut['fuzzy']:\n            vit, remain_pinyin = viterbi(pinyin)\n            result.extend([(pinyin,) + t for t in vit])\n    # 排序并取出前 limit 个\n    dp[pinyin] = sorted(result, key=lambda x: x[2], reverse=True)\n    return dp[pinyin][:limit]\n\n\nif __name__ == '__main__':\n    print(ime('jintian'))  # 基础功能\n    print(ime('jintain'))  # 纠错功能\n    print(ime('ji\\'ntian'))  # 分词功能\n    print(ime('jintiantianqibucuo'))  # 短句功能\n    print(ime('jttqbc'))  # 首字母功能\n    print(ime('nanjingdaxuerengongzhinengxueyuan'))  # 南京大学人工智能学院\n    print(ime('nanjingdx'))  # 南京大学\n```\n\n\n## 五、输入 Emoji\n\n我从 https://www.emojiall.com/zh-hans/all-emojis 中获取了所有的中文和 emoji 的对应数据，保存在了 `data/emoji.txt` 中。通过代码\n\n```python\ndef gen_emoji_json():\n    emoji_dict = {}\n    with open(emoji_file_path, 'r', encoding='utf-8') as f:\n        emoji = ''\n        for line in f:\n            line = line.strip()\n            # 跳过数字\n            if all(ch in '1234567890' for ch in line):\n                continue\n            if emoji:\n                # 去除前缀\n                if line.startswith('旗: '):\n                    line = line[3:]\n                emoji_dict[line] = emoji\n                emoji = ''\n            else:\n                emoji = line\n\n    # save to data/emoji.json\n    with open(emoji_json_path, 'w', encoding='utf-8') as f:\n        json.dump(emoji_dict, f, ensure_ascii=False, indent=4)\n\n\ndef load_emoji_dict():\n    return json.load(open(emoji_json_path, 'r', encoding='utf-8'))\n```\n\n我生成了如下格式的 emoji 字典，即中文和 emoji 的对应表：\n\n```json\n{\n    \"笑脸\": \"😄\",\n    \"苦笑\": \"😅\",\n    \"斜眼笑\": \"😆\",\n    \"微笑天使\": \"😇\",\n    \"呵呵\": \"🙂\",\n    \"倒脸\": \"🙃\",\n    \"笑得满地打滚\": \"🤣\",\n    \"表情脸\": \"😍\",\n    \"花痴\": \"😍\",\n    \"亲亲\": \"😗\",\n    \"飞吻\": \"😘\",\n    \"吐舌脸\": \"😛\",\n    \"好吃\": \"😋\",\n    \"想一想\": \"🤔\",\n}\n```\n\n最后我们更新一下 `ime()` 函数，如果第一个中文有对应 emoji 则在第二位加入 emoji 即可。\n\n```python\ndef ime(pinyin: str, limit=7):\n    '''\n    输入法函数, 综合分词和维特比算法的最终结果, 并且会加入 emoji\n    '''\n    def replace_with_emoji(tuples):\n        '''\n        如果第一个中文有对应 emoji, 则使用 emoji 将其替换\n        '''\n        if tuples and tuples[0][1] in emoji_dict:\n            return [tuples[0], (tuples[0][0], emoji_dict[tuples[0][1]], tuples[0][2] - 1e-5)] + tuples[1:-1]\n        else:\n            return tuples\n    \n    if pinyin in dp:\n        return replace_with_emoji(dp[pinyin][:limit])\n```\n\n\n## 六、Web 前端\n\nWeb 前端采用了 React 框架，个人比较喜欢 Google 家的 Material Design，因此选用了 MUI，一款基于 React 框架的 Material 组件库。\n\n![](https://picgo-1258602555.cos.ap-nanjing.myqcloud.com/2022-07-05-13-28-32.png)\n\n整个 UI 界面非常简单，由三个主要部分组成。\n\n1. 位于左上方的拼音输入法输入框，用以显示当前输入的拼音内容，例如当前为 `xiao'lian`，然后通过拼音实时获取到对应的推荐词列表。输入框的右边还会包括一个 emoji 选择按钮。\n2. 位于左下方的推荐词列表，最多显示 7 个。其中还包括匹配 emoji 的显示。\n3. 位于右边的文本输入框，会捕获按键并同步输入法输入的内容，可以用于测试输入法的效果。\n\n![](https://picgo-1258602555.cos.ap-nanjing.myqcloud.com/2022-07-05-13-29-51.png)\n\n前端具体的代码比较繁杂，这里也不过多赘述。\n\n\n## License\n\nThis project is licensed under the MIT License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Forangex4%2Fsimple-pinyin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Forangex4%2Fsimple-pinyin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Forangex4%2Fsimple-pinyin/lists"}