Files
AzooKeyKanaKanjiConverter/Docs/conversion_algorithms.md
2023-07-23 16:31:44 +09:00

187 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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件: 付加的な変換(全部ひらがな、全部カタカナなど)
そこから: 前方一致で長い順・高評価順に辞書データを表示
```
## ライブ変換
ライブ変換はかなり単純なアイデアで実現しています。ライブ変換のない場合と同様に変換候補をリクエストし、「(予測変換ではなく)完全一致変換の中で最も順位が高いもの」をディスプレイします。