From 5740748de05e0444d569c0c56885ae6282cc8d82 Mon Sep 17 00:00:00 2001 From: ensan Date: Sun, 23 Jul 2023 16:31:44 +0900 Subject: [PATCH] Add Documents --- Docs/README.md | 5 + Docs/Visions/dictionary.md | 36 +++++++ Docs/Visions/learning.md | 33 ++++++ Docs/conversion_algorithms.md | 187 ++++++++++++++++++++++++++++++++++ Docs/dicdata_format.md | 84 +++++++++++++++ Docs/failures.md | 44 ++++++++ 6 files changed, 389 insertions(+) create mode 100644 Docs/README.md create mode 100644 Docs/Visions/dictionary.md create mode 100644 Docs/Visions/learning.md create mode 100644 Docs/conversion_algorithms.md create mode 100644 Docs/dicdata_format.md create mode 100644 Docs/failures.md diff --git a/Docs/README.md b/Docs/README.md new file mode 100644 index 0000000..5e93692 --- /dev/null +++ b/Docs/README.md @@ -0,0 +1,5 @@ +# Documents + +本ディレクトリはAzooKeyKanaKanjiConverterModuleに関するドキュメントをまとめています。 + +azooKey本体については[azooKey/docs](https://github.com/ensan-hcl/azooKey/tree/develop/docs/overview.md)をご覧ください。 \ No newline at end of file diff --git a/Docs/Visions/dictionary.md b/Docs/Visions/dictionary.md new file mode 100644 index 0000000..11a498a --- /dev/null +++ b/Docs/Visions/dictionary.md @@ -0,0 +1,36 @@ +# Dictionary Vision + +## System Dictionary + +最近のazooKeyの内部変更で、システム辞書を任意のディレクトリから読み出せるようになりました。この変更によって、将来的に以下のようなことが可能です。 + +* オプションで語彙数の大きな辞書を選べるようにする +* 辞書データをazooKeyのシステムに同梱せず、定期更新を可能にする(Background Assets APIを利用できます) +* 外部からDLした辞書(サードパーティ製辞書、ベータ版辞書など)を利用する + +辞書サーバの準備などが問題となります。 + +* 比較的サイズが大きい(~100MB)データを配布するので、費用がバカにならない + * 高く見積もると、S3の場合、月あたりに100MBの辞書データを1000回転送するとして、月あたり約2000円 + * 課金プランにするという手はある(月100円としてユーザが20人いればペイする) + * ただし、開発の体制上、安定して辞書の更新ができるとは限らないので、今のところはここに責任を入れたくない + +## User Dictionary + +ユーザ辞書は非常に有用な機能ですが、導入当初からあまり仕様を変化させていません。そこで、今後考えられる機能の変更について議論します。 + +### More Configurable Entry + +現在のユーザ辞書は高度な設定が不可能です。品詞をマニュアルで設定するなどの方法でより高度な設定ができると良さそうです。 + +### Import / Export + +ユーザ辞書を外部からインポートしたり、逆に外部向けにエクスポートする機能が必要です。 + +### Custom Action + +現在、カッコを入力すると自動でカーソルを中央に移動する機能があります。これをユーザ辞書でも実現できると良さそうです。 + +### Dictionary Market + +ユーザが作成したユーザ辞書データをアプリ内で配布・ダウンロードできるようにします。オフィシャルな方法があることによって、辞書データの作成が活性化すると良さそうです。 \ No newline at end of file diff --git a/Docs/Visions/learning.md b/Docs/Visions/learning.md new file mode 100644 index 0000000..c310d28 --- /dev/null +++ b/Docs/Visions/learning.md @@ -0,0 +1,33 @@ +# Learning Vision + +学習機能の性能向上は重要です。 + +## 今後実現したい機能 + +### プライバシー + +キーボード上から「新たな学習を停止(プライベートモード)」「これまでの学習を利用しない(ゲストモード)」を有効化できることにより、ユーザがプライバシーを守りやすくなる可能性がある。 + +この機能の技術的な課題はキーボードからの設定の上書きである。上書きそのものは、設定の構造を次のようにすることで対応できる。 + +- アプリ側:App Groupの共有領域に「(設定内容, 更新日時)」のデータを保存 +- キーボード側:Private領域に「(設定内容, 更新日時)」のデータを保存 +- 読み出し:更新日時の新しい方を利用 + +しかし、キーボード側から上書きした場合にアプリ側の設定の表示を更新する方法が存在しない(フルアクセスがある場合は表示を更新できる)。これが実際の使用感にどれほど悪影響を与えるかは不明であり、おそらくあまり気にしなくて良いと思う。 + +また「ゲストモードの解除をキーボード上で可能にするか」という論点がある。ゲストがゲストモードを解除できたら意味がないかもしれないが、これは今後に回しても問題ないと思う。 + +### バックアップ + +学習のバックアップを定期的に取り、そこから過去の学習を復元できるようにすることが考えられる。 + +この機能の課題は、「復元」をどのようなUIで実現するかと、実際に「定期的に」は取れないのをどう解決するかである。 + +### 修正の自動検出 + +誤変換をユーザが「修正」したことを自動的に検出し、その候補の学習を変更することができると良い。 + +### 仕様の再検討 + +- 現在の学習は、特に長い候補において強すぎるため、弱くした方が良いかもしれない。 \ No newline at end of file diff --git a/Docs/conversion_algorithms.md b/Docs/conversion_algorithms.md new file mode 100644 index 0000000..6cc877b --- /dev/null +++ b/Docs/conversion_algorithms.md @@ -0,0 +1,187 @@ +# Conversion Algorithms + +azooKey内部で用いられている複雑な実装を大まかに説明します。 + +## かな漢字変換 + +変換処理では基盤としてViterbiアルゴリズムを用いています。 + +入力中には「1文字追加する」「1文字消す」「1文字置き換える」など、差分を利用しやすい場面が多いため、それぞれの場面に最適化したアルゴリズムを選択出来るようになっています。 + +アルゴリズムに特徴的な点として、文節単位に分割したあと、「内容語バイグラム」とでもいうべき追加のコストを計算します。このコスト計算により、「共起しやすい語」が共起している場合により評価が高く、「共起しづらい語」が共起している場合に評価が低くなります。 + +## 入力管理 + +入力管理は簡単に見えて非常に複雑な問題です。azooKeyでは`ComposingText`の内部で管理されています。 + +典型的なエッジケースは「ローマ字入力中に英語キーボードに切り替えて英字を打ち、日本語キーボードに戻って入力を続ける」という操作です。つまり、次の2つは区別できなければいけません。 + +``` +入力 k (日本語) // →k +入力 a (日本語) // →か +``` + +``` +入力 k (英語) // →k +入力 a (日本語) // →kあ +``` + +azooKeyの`ComposingText`は、次のような構造になっています。このように`input`を持つことによって、この問題に対処しています。 + +```swift +struct ComposingText { + // 入力の記録 + var input: [InputElement] + // ローマ字変換などを施した結果の文字列 + var convertTarget: String + // 結果文字列内のカーソル位置(一番左にある場合、0) + var convertTargetCursorPosition: Int +} + +struct InputElement { + // 入力した文字 + var character: Character + // 入力方式 + var inputStyle: InputStyle +} + +enum InputStyle { + // 直接入力 + case direct + // ローマ字入力 + case roman2kana +} +``` + +しかし、カーソルを考慮すると、問題はさらに複雑になります。これは、UIの表面からは想像もつかないほど複雑です! + +例えば、以下の状態を考えます。 + +```swift +ComposingText( + input: [ + InputElement("j", .roman2kana), + InputElement("a", .roman2kana), + ], + convertTarget: "じゃ", + // 重要: カーソルの位置は「じ|ゃ」となっている。 + convertTargetCursorPosition: 1 +) +``` + +ここで、「u」をローマ字入力した場合、どういう挙動になるでしょうか。ここにはデザインスペースがあります。 + +1. じうゃ +1. じゃう +1. じゅあ +1. 諦めて編集状態を解除する + +1は最も直感的で、azooKeyはこの方式をとっています。この場合、`input`を修正する必要があります。そこでazooKeyでは、「u」をローマ字入力した場合に`ComposingText`が次のように変化します。 + +```swift +ComposingText( + input: [ + InputElement("じ", .direct), + InputElement("u", .roman2kana), + InputElement("ゃ", .direct), + ], + convertTarget: "じうゃ", + convertTargetCursorPosition: 2 +) +``` + +一方でiOSの標準ローマ字入力では、「2」が選ばれています。これはある意味で綺麗な方法で、ローマ字入力時に「一度に」入力された単位は不可侵にしてしまう、という方法で上記の変化を無くしています。もしazooKeyがこの方式をとっているのであれば、以下のように変化することになります。しかし、このような挙動は非直感的でもあります。 + +```swift +ComposingText( + input: [ + InputElement("j", .roman2kana), + InputElement("a", .roman2kana), + InputElement("u", .roman2kana), + ], + convertTarget: "じゃう", + convertTargetCursorPosition: 3 +) +``` + +「3」の「じゅあ」を選んでいるシステムは知る限りありません。この方式は「ja / じゃ」の間に「u」を入れる場合はうまくいきますが、「cha / ちゃ」の「ち」と「ゃ」の間に「u」を入れる場合は入れる位置をどのように決定するのかという問題が残ります。(chua、とすることになるのでしょうか) + +「4」はある意味素直な立場で、「そんなんどうでもええやろ」な実装はしばしばこういう形になっています。合理的です。azooKeyも、ライブ変換中はカーソル移動を諦めているため、このように実装しています。 + +このように、入力にはさまざまなエッジケースがあります。こうした複雑なケースに対応していくため、入力の管理は複雑にならざるを得ないのです。 + +## 誤り訂正 + +誤り訂正は、上記の`ComposingText`を基盤としたアドホックな実装になっています。 + +具体的には、`ComposingText`のそれぞれの部分に対して + +* 「た」があれば「だ」も許す +* 「ts」とがあれば「た」に置き換える + +というような事前に列挙されたルールを適用します。 + +しかし、任意の回数適用を行えるとなると、「たたたたたたたたたた」が入ってきた場合、それぞれの「た」についてルールを適用するか否かで1024通りの候補が生じてしまいます。これでは困るので、実際には「ルールの適用は3回まで」というように制約をつけ、組み合わせ爆発を防いでいます。 + +また、ルールの適用をおこなった場合、候補のコストを追加することで「ある程度のコストをかけても上位にくる場合、誤っている可能性が高い」ということを表現しています。このコストは人力で決めていて、「か」「が」のような助詞同士のペアではより高くするなど一部調整をしています。 + +## 学習 + +学習は、「一時記憶(キーボードを開く〜閉じるの間)」と「長期記憶(半永続)」の2つのデータを用いて行います。一時記憶は揮発性メモリ上にのみ存在し、長期記憶はファイルとして非揮発性のストレージに保存します。 + +辞書検索においては、一時記憶と長期記憶の両方を検索します。通常利用の際には一時記憶のみを更新します。そして、キーボードを閉じる際に一時記憶の内容を長期記憶にマージします。長期記憶へのマージは一時記憶の更新に比べて負荷の大きな処理であるため、キーボードを閉じる際にのみ実施することで使用感を向上させています。 + +### 学習のアルゴリズム + +学習においては、選択された候補に対応する`[DicdataElement]`に対して、以下を学習します。 + +1. 学習が必要な単語に対して、その単語 +1. 文節 +1. 候補全体 + +例えば「この本を本屋で買った」の場合、「本」「本屋」「買っ」などが1の結果として学習されます。また、2の結果として「この本を」「本屋で」「買った」などが学習されます。また、3の結果として「この本を本屋で買った」全体が学習されます。 + +学習された表現は、ユーザ辞書と同じ仕組みで記録されます。元の単語とは別の表現として新たな辞書項目を追加し、計算時にそちらが優先的に選ばれるようコストを調整します。コストは「使用回数」などに応じて計算します。 + +#### 学習の減衰 + +学習したデータは時間が経つにつれ減衰します。具体的には、「使用回数」が32日おきに半減します。「使用回数」が0になった単語は学習から削除するため、1度使って学習された単語は32日間記憶されたのち、削除されます。2度使えば64日、4度使えば128日は維持されます。ただし学習には上限数があるため、上限に達した場合は古いものから削除されます。 + +#### 学習のリセット + +変換候補を長押しすることで「この候補の学習をリセット」することができます。この際、候補に含まれる`[DicdataElement]`に対して、それと同じものを一時記憶・長期記憶から削除します。 + +### 学習データファイルの更新 + +学習は「学習した内容」を示すファイル(`memory.louds`, `memory.loudschars2`, `memory.loudstxt3`など)を内部的に保存することで実現しています。この更新処理を安全に行うため、以下のような処理をしています。 + +1. 更新を反映した各ファイルを`memory.louds.2`のように書き出す +1. `.pause`を書き出す +1. それぞれの`.2`を元ファイルの位置にコピーする(`.2`ファイル自体は削除しない) +1. `.pause`を削除する + +このとき、読み出し側では + +* `.pause`がない場合、`.2`のつかないファイルを読み出す。 +* `.pause`がある場合、適当なタイミングで上記ステップの`3`以降を再実行する。また、`.pause`がある場合、学習機能を停止する。 + +上記手順では`.pause`がない間は`.2`のつかないファイルが整合性を保っており、`.pause`がある場合は`.2`のつくファイルが整合性を保っています。 + +例えば1と2のステップの実行中にエラーが生じた場合、一時記憶は失われますが、次回キーボードを開いた際は単に更新前のファイルを使うことができます。 + +3のステップの実行中にエラーが生じた場合、`.pause`があるため、次回キーボードを開いた際は学習を停止状態にします。ついで適切なタイミングで再度ステップ3を実行することで、安全に全てのファイルを更新することができます。 + +## 変換候補の並び順 + +変換候補の並び順の決定はとても難しい問題です。azooKeyではおおよそ以下のようになっています。`Converter.swift`が並び順を決めていますが、とても複雑な実装になっているため、改善したいと思っています。 + +``` +最初の5件: 完全一致または予測変換(ただし最低1つは完全一致) +そこから5件: 文節単位変換 +そこからn件: 付加的な変換(全部ひらがな、全部カタカナなど) +そこから: 前方一致で長い順・高評価順に辞書データを表示 +``` + +## ライブ変換 + +ライブ変換はかなり単純なアイデアで実現しています。ライブ変換のない場合と同様に変換候補をリクエストし、「(予測変換ではなく)完全一致変換の中で最も順位が高いもの」をディスプレイします。 \ No newline at end of file diff --git a/Docs/dicdata_format.md b/Docs/dicdata_format.md new file mode 100644 index 0000000..3f52d33 --- /dev/null +++ b/Docs/dicdata_format.md @@ -0,0 +1,84 @@ +# Dicdata Format + +azooKeyの辞書データは次のようなフォーマットになっています。 + +NOTE: LOUDSそのものに関する解説は行いません。 + +## DicdataElement型 + +```swift +struct DicdataElement { + // 単語の表記 + var word: String + // 単語のルビ(カタカナ) + var ruby: String + // 単語の左連接ID + var lcid: Int + // 単語の右連接ID + var rcid: Int + // 単語のMID + var mid: Int + // 単語の基礎コスト (PValue = Float16) + var baseValue: PValue + // コストの動的調整 + var adjust: PValue +} +``` + +注意すべき点は次のとおりです。 + +* 連語などの場合、`lcid`と`rcid`が異なる値を取ることがあります。 +* 単語の基礎コストは、歴史的な事情によって負の小数です。大きいほど頻出する単語でus。 +* コストの動的調整は、誤り訂正などのためにコストを調整したい場合に使います。例えば「大学生」の基礎コストを「-10」としたとき、「たいがくせい」と入力した誤り訂正の結果として「大学生」が得られている場合は、`adjust`を-3のような値として、合計コストが-13であるかのように振る舞わせます。 + +`DicdataElement`は`DicdataStore`で辞書データファイルから生成されます。 + +## 辞書データファイル + +辞書データは次の4つの種類のファイルからなります。 + +* `.louds` +* `.loudschars2` +* `.charID` +* `.loudstxt3` + +まず、`.louds`のファイルがLOUDS Trieをバイナリ形式で保存したものです。 + +次に、`.loudschars2`は各ノードに割り当てられた文字を記録するものです。ただしUnicode文字列の代わりに、1バイトのCharacter IDで表現されています。このため、`.loudschars2`は1バイトずつ処理できます。`.charID`がCharacterをIDに割り当てるためのデータを格納します。 + +最後に、`.loudstxt3`に各ノードに割り当てられたエントリーのデータが記録されています。 + +azooKeyの辞書ルックアップは次のように進みます。 + +1. 起動時に一度だけ`charID`が読み込みます。以降はこれを参照してクエリをID列に変換します。 +1. クエリを受け取ったら、ID列に変換します。クエリの先頭の文字に対応する`louds`と`loudschars2`を読み込みます。Swift側ではこの2つをセットにして`LOUDS`構造体が作られ、キャッシュされます。 +1. `LOUDS`を検索し、必要なノードの番号を列挙します。 +1. クエリの先頭の文字に対応する`loudstxt3`を読み込み、必要な番号のノードに記録されたデータを読み出します。読み出したデータを`DicdataElement`形式に変換し、以降の処理で利用します。なお、`loudstxt3`の方はキャッシュしないので、必要になるたびにIOが走ります。 + +### `.louds`の構造 + +`.louds`ファイルはLOUDSのbit列を保存したものです。 + +### `.loudschars2`の構造 + +TBW + +### `.charID`の構造 + +TBW + +### `.loudstxt3`の構造 + +TBW + +## 重みデータ(CID) + +品詞バイグラムの重み行列が疎行列になることから、CIDの重みデータはフォーマットを工夫しています。 + +TBW + +## 重みデータ(MID) + +こちらは疎行列ではないため、重み行列をそのままバイナリ化したものが`mm.binary`として保存されています。 + +TBW diff --git a/Docs/failures.md b/Docs/failures.md new file mode 100644 index 0000000..0597269 --- /dev/null +++ b/Docs/failures.md @@ -0,0 +1,44 @@ +# Failures + +azooKeyの開発上、明確に失敗だったと考えている実装や仕様をまとめます。これらは将来的に修正できるかもしれないし、できないかもしれないです。 + +このドキュメントの目的はazooKeyの判断ミスを明確にして、今後azooKeyのフォークを作成する方や、新たな日本語入力ソフトウェアを作ろうとする方に向けて知見を残しておくことです。 + +## コストの設計 + +辞書データのコストはざっくり言って対数尤度です。azooKeyの開発初期、コストは対数尤度をそのまま使っていました。これを続けてしまったため、普通は`Int`などにするのですが、azooKeyのコストは`Float16`になっています。 + +`Float16`の現実的に不便な点は以下の通りです。 + +* 整数型に比べて(おそらく)和の計算が遅い +* macOSが`Float16`に対応していないことに由来する困難がある + +今から作り直すなら、`UInt16`か`UInt8`にしています。サンプリングを工夫することで、`UInt8`でも十分な表現ができる可能性があります。 + +なお、今後のバージョンアップでこの変更を行う場合、ユーザ側でもマイグレーションが必要になります。 + +* 絵文字・顔文字・ユーザ辞書の再コンパイル +* 学習データの再コンパイル + +逆にいうと、気合と正しいマイグレーションの実装ができれば今からでも修正可能ではあります。 + +## CharIDの設計 + +`LOUDS`検索を高速化するため、`LOUDS`のラベルは文字コードそのものではなく、それに対応するCharIDにしています。 + +しかし、このCharIDをかなり雑に決めたところがあり、なぜか入っている文字やなぜか入っていない文字があります。 + +この辺りは一度再設計したいのですが、これもやはり上記のマイグレーションが必要になります。 + +## 品詞IDの設計 + +品詞IDは「ipadic+独自拡張」という形になっています。具体的には、かな漢字変換上分離したほうが良い3つの品詞を追加しています。 + +* 1316: EOS(文頭と文末は区別されるべきである) +* 1317: ?(疑問助詞等との接続の可能性が高い) +* 1318: !(強意の助詞等との接続の可能性が高い) + +しかし、ipadicはさまざまな点で難しい面のある品詞体系です([参考](https://zenn.dev/azookey/articles/c201408af14ae0))。もし最初から作り直すのであれば、品詞IDはUnidicのものを用いていたのではないかと思います。 + +上の2つと違って、品詞IDの変更は現実的でないと考えています。というのも、辞書生成モジュールを含む無数のコードでipadicが前提となっており、作業量が膨大になりすぎるからです。もちろんマイグレーションも必要になります。 +