人工言語制作について最近考えていること

遅くなりましたが、この記事は[語学・言語学・言語創作 Advent Calendar 2020](https://adventar.org/calendars/4931)の22日目の記事です。

みなさん、自分だけの辞書編集ソフトウェア、作っていますか？

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">これは元も子もない話ですが，自分で自分の使い勝手に合った人工言語作成辞書ソフトを作成するのも楽しいぞ．(作成中は心折れること多いけど)</p>&mdash; 渡久地 信之 (@palfem_kleet) <a href="https://twitter.com/palfem_kleet/status/1323458631426207746?ref_src=twsrc%5Etfw">November 3, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ʕ•͠ω•ʔ 人工言語界隈2（もはや誰も人工言語を作っておらず、人工言語の辞書さえも作っておらず、かわりに人工言語の辞書を作るソフトウェアを作っている謎の界隈）</p>&mdash; 怪文書 (@exograph) <a href="https://twitter.com/exograph/status/1341310284028231680?ref_src=twsrc%5Etfw">December 22, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


プログラマにとってぼくのかんがえたさいきょうのプログラミング言語を作ることが一つのイニシエーションであるのと同じように、人工言語作者にとってはぼくのかんがえたさいきょうの辞書編集ソフトウェアを作ることが一つのイニシエーションであるようです。

この記事では、人工言語制作そのものというよりも、人工言語制作をスムーズム[^0]にするためのソフトウェア開発について筆者が考えていることや、役立ちそうな要素技術などを書いておきます。

# 類似音声検索

2020年も終わりに差し掛かり、サイバーパンク2077がローンチされ、シンギュラリティまで秒読みといった雰囲気になってきているのに、われわれが素朴な文字列マッチと線型探索でお茶を濁したままでいいはずがありません。

**辞書から検索するときに、実現形や音素列といった文字列表記そのものではなく、それらを弁別素性の変化を表す時系列データに変換してから検索するようにした方がいいのでは**ということをつらつら考えています。

## 従来の辞書検索システムの問題点

その言語の正書法による文字列検索は、検索辞書が単一言語の辞書に限定されている限りはそこまで問題ないのですが、複数言語の辞書間で横断検索などをやろうとすると途端に問題が出てきます。自分の言語と他人の作った言語のあいだで、同音もしくは類似音の語を一覧したい、とかですね。やりたくないですか！？ 筆者はやりたいです。

ではIPA表記で検索すればよいのかというと、そういうわけにもいきません。入力の面倒くささは[akrantiain](https://conlinguistics.wikia.org/ja/wiki/Akrantiain)などでなんとかなるかもしれませんが、そもそもIPA表記自体が基本的に雰囲気で運用されており（連続的な音声を離散的な文字で転写する以上、原理的に仕方ないのですが）、人によって書き方が異なるということ。簡易表記と精密表記の違いもありますし、同じ音を表すためにもいくつもの書き方があったりします。例えば、字母の上下に挟むように付加するタイプの結合文字を用いる<ruby>歯間音<rt>[interdental](https://en.wikipedia.org/wiki/Interdental_consonant)</ruby>`/◌̪͆/`などを、エンコードされた文字列上でどの順序で並べるべきか不定です[^1]。あるいは、二つの字母の間の微妙な調音点を指定したいときに、前寄りの字母に後寄りの結合文字を付加するか、その逆か、みたいな曖昧性もあります。

これらの問題は、検索インデックスを作成するときに内部でなんらかの規則に従って正規化すればいいのかもしれませんが、IPA表記という形式にこだわってわざわざそこまでするよりも、筆者は根本的に考え方を変えて、内部表現は音声かそれに準ずる連続的な時系列データにしてしまった方が良いのではないかと思うようになりました。こうしておけば将来的な拡張性も期待できます。つまり、音声入力を実装しようとなったときにも、その入力を適切に内部表現に変換することができれば[^2]、すでに辞書に記載されている正書法表記やIPA表記に基づいて生成された各語形の内部表現との類似度で検索することができるわけです。

## 弁別素性の時系列データ

筆者が「内部表現」ということばでイメージしているのは、ベクターグラフィックスの「パス」のようなものです。わかりやすくいうと、パスの各制御点は調音点や調音方法にあたります。パスの描く曲線が時系列データです。より厳密にいうと、各制御点はベクトルで、その基底はそれぞれの弁別素性を意味します。

例えば、ごく単純化して、ある言語に無声子音`c`と母音`v`という二つの音素しかないとします。このとき`[±consonantal]`と`[±voice]`という弁別素性があり、制御点の座標はこれらの素性の値の組み合わせで表現され、`c`は`(1, -1)`であり、`v`は`(-1, 1)`となります。

このようにベクトル空間に音素が分布することになるので、そこに時間次元を足すことで、一つの語形が表現される、という感じです。

## データの類似性をはかる

このセクションはけっこう技術的な話になります。半ば自分用。

データマイニングの文脈で類似性を比較するというのは、そのデータを線形っぽい何か（ほとんどの場合はベクトル）に変換したうえで、距離であったり内積であったりを測ることを言います。

二つのデータ間の類似性を得たいだけなら非常に楽なのですが、われわれは大量の項目に対して類似性比較をしなければなりません。どうにかして単純な線型探索よりも効率的な方法が欲しいです。このようなとき、ある種の事前計算テーブルが役に立ちます。

### 局所性鋭敏型ハッシュ

**<ruby>[局所性鋭敏型ハッシュ](https://ja.wikipedia.org/wiki/局所性鋭敏型ハッシュ)<rt>locality-sensitive hashing</ruby>**（LSH）はハッシュ関数ではなくてハッシュテーブルの作り方です。近い距離に位置するデータをまとめて1つのバケットに集めるようにすることで、それらを同一視（次元圧縮）する、というものです。距離関数は一定の条件のもと任意に設定できます。

事前パラメータテーブルの作り方がそのまま入力データの同一性の閾値（どこまで類似していれば同一とみなすか）となっていて、閾値を変えるには全てのデータのハッシュを計算しなおす必要があるっぽいです。

- よさげアルゴリズム https://www.google.com/search?q=resizable+lsh

### いろいろな距離

#### 知覚的ハッシュ

一般にハッシュといえば暗号学的ハッシュを思い浮かべるのではないかと思います。例えば、ファイルの誤り検出に使われているMD5やSHA256といった文字列は、特に技術者ではなくともインターネットに入り浸っていれば見かけることがあるのではないかと思います。

データの内容が少しでも異なっていれば、結果的に全く異なるハッシュが生成される、というのが暗号学的ハッシュの特徴（<ruby>アバランシェ効果<rt>[avalanche effect](https://en.wikipedia.org/wiki/Avalanche_effect)</ruby>）ですが、一方で**データの内容の類似性をある程度反映する**ハッシュ関数というのも考えられます。一般的に、こういった特徴を持つハッシュをまとめて「**<ruby>知覚的ハッシュ<rt>[perceptual hashing](https://en.wikipedia.org/wiki/Perceptual_hashing)</ruby>**」と呼びます。

知覚的ハッシュの主な用途は、ハミング距離などを組み合わせて類似データを検出することです。完全な重複データであれば暗号学的ハッシュでも検出できますが、1ビットでも異なるデータは無理です。例えば画像の解像度の違いやJPGのモスキートノイズを無視して同じ印象の画像を検索したりなど、微妙に異なっているデータの比較に知覚的ハッシュは有用です。もちろん画像や音楽などに限らず、アルゴリズム次第で任意のデータを扱うことができます。 

#### その他

（丸投げ）https://www.procrasist.com/entry/23-distance

# 全文検索

全文検索は非常に面倒くさいタスクの一つですよね。辞書が大きくなってくると線型探索ではとても太刀打ちできなくなってきます。サーバサイドで動くデータベースならすでに沢山のソリューションがあるのですが、クライアントサイドに全部載せたWebアプリを作りたい⋯⋯。

世の中には頭のおかしい人間が開発した[FM-index](https://en.wikipedia.org/wiki/FM-index)というアルゴリズムがあり、これを使うと、なんと**辞書全体のサイズには依存せず、ほぼクエリ文字列の長さにのみ[^6]比例する計算量で全文検索が実現できる**らしいです。その名のとおりインデクシングが必要なアルゴリズムなのですが、インデックスの作成も常人には到底理解できない摩訶不思議な発想がかかわっているので一見の価値あり。

- [ハクビシンにもわかる全文検索](https://qiita.com/erukiti/items/f11f448d3f4d73fbc1f9)

このセクションももうちょっと書きたかったけど時間が足りなかった。

- よさげアルゴリズム https://github.com/felipelouza/gsa-is

# バージョン管理

辞書のバージョン管理、したいですよね。Gitのシステムを辞書データの管理に直接使うことができないかと試行錯誤しています。

私見では人工言語作者・関係者の8割程度がすでに何らかの計算機技術者であり、今更何をという感じかもしれませんが、もしGitのことをよく知らない方がいたら今すぐ調べた方がいいと思います。ものすごく有用です。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ʕ•͠ω•ʔ git のような分散システムは辞書の編集と相性がいいと思ってる。上流の更新を拾いながらも、独自用語のメンテナンスを進めるといったことも容易に可能になる。話者は個々人で少しずつ異なるメンタルレキシコンを持つ、というモデルを容易に反映できる</p>&mdash; 怪文書 (@exograph) <a href="https://twitter.com/exograph/status/1307832430246555648?ref_src=twsrc%5Etfw">September 21, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ʕ•͠ω•ʔ 加えて、個々人の辞書の更新ごとに、その通知を近い人から伝搬させていくようなゴシップネットワークを組み合わせる。すると、あまり知らない人の更新も伝わってきて、それが良いと思えば自分のバージョンにも取り込める</p>&mdash; 怪文書 (@exograph) <a href="https://twitter.com/exograph/status/1307834285525614593?ref_src=twsrc%5Etfw">September 21, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ʕ•͠ω•ʔ このネットワークには、原理的には「唯一の正しい辞書」は存在しえない。個々人が自分で更新したり、他人の更新をマージしたりすることによって辞書が発展していく。より現実の言語運用に近い形で言語が発展していく</p>&mdash; 怪文書 (@exograph) <a href="https://twitter.com/exograph/status/1307837623180746752?ref_src=twsrc%5Etfw">September 21, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## Git辞書のすすめ

辞書をGitに乗せると便利なことが沢山あります。

- 変更履歴が残ります。手動で辞書項目に何月何日更新、とかやってる人はすごく楽になると思います。そして任意の変更履歴の時点にデータを巻き戻したりすることもできます。ディレクトリごと差分を比較したり、ファイルの行単位で（！）誰がいつどんな変更を行なったのか参照することができます。やろうと思えば、ファイルを分割したり統合したりしても行単位の変更履歴を維持することもできます[^3]。
- 安全な共同作業ができます。Gitを使った編集では、編集者それぞれが自分のマシンにデータのコピーをダウンロード（フェッチ）してきて、それを区切りのいいところまで編集したらその内容を中央サーバ（GitHubなど）にアップロード（プッシュ）する、という流れになりますが、もしその間に別の誰かが変更をアップロードしていたら、Gitがアップロードを弾いてくれます。競合の解決は人間の仕事です。
- 重複データの除去が自動で行われます。Gitは過去の状態を復元可能なようにすべての変更内容を保持していますが、これは毎回すべてのファイルを丸ごとコピーしているわけではなく、前回の更新からの変更部分と、日付などのメタデータをまとめたデータ構造（コミットオブジェクト）を新しく追加しています。これはコンテントアドレシング（後述）という仕組みによって可能になっています。

### コンテントアドレシング

**<ruby>コンテントアドレシング<rt>[content-addressing](https://en.wikipedia.org/wiki/Content-addressable_storage)</ruby>**なストレージとは、あるデータを特定するためにそのデータのハッシュをキーに用いるようなkey-valueストレージのことです。適切な（衝突しづらい）ハッシュ関数を選べば、自然に**<ruby>重複除去<rt>dedupe</ruby>**が行われる優れたシステムになります。

ハッシュ木（<ruby>マークル木<rt>Merkle tree</ruby>とも）とよばれる仕組みを利用することで、ディレクトリ構造など再帰的なデータ構造もハッシュ化することができるようになります。Gitは、Merkle treeをDAG（有向非循環グラフ）に拡張したMerkle DAGを用いて変更履歴を保存しています。ディレクトリのハッシュは、そのディレクトリが含んでいるすべてのファイルまたはディレクトリのハッシュのハッシュです。したがってディレクトリ内のファイルが一部でも変更されると、それより上のすべてのディレクトリのハッシュが変わります。変更していない部分に関してはそのハッシュは変わらないので、データも増えません。そして、その時点での最も新しいトップレベルディレクトリのハッシュをポイントしているのがコミットオブジェクトなわけです。

単純なkey-valueストアでありながら、木構造であるディレクトリ構造を保存できるのは面白いですよね。この仕組みをそのままファイルシステムにしてしまう[IPFS](https://ipfs.io)も存在します。

## Git辞書のすすめ⁻¹

さて、ここまでGitを紹介しましたが、実はまだまだ問題があり、現段階では辞書編集に使うことは現実的ではありません。残念。

- 辞書編集に最適なUIがありません。今のところ上で述べたようなことを辞書編集で活用するには直接ファイルをいじってコマンドを叩いたりするしかないですが、これをやるのはかなりつらみがあります。ここら辺を隠蔽してくれるWebアプリ[^4]が作れたらいいなと思っています。
- 辞書データの構造をコンテンツアドレシングなファイルシステムに最適化する必要があります。バカデカい単一の辞書ファイルをそのままGitに乗せてもあまり意味がなく、辞書項目ごとなど適切な単位でファイルを分割しなければなりません。どこまで情報単位を分割すべきか、その場合のファイル名やディレクトリ構成はどうすべきか、といったところは筆者も試行錯誤している最中です。
- 変更履歴や差分が行指向なので、データの記述方法を行単位にした方がいい気がします。その点で、筆者はJSONよりYAMLに注目しています[^5]。JSONだと、改行などがなくても有効なように設計してある関係上、無意味な更新を更新として許してしまうのでつらそう。

# あとがき

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ʕ•͠ω•ʔ で、git なわけだから、共同作業のためにはもうすでにプルリクとかの機能があるホスティングサービスがあるわけだけど、ここは辞書という対象領域にもっと特化したようなサービスが作れるのではないかと。それが<a href="https://t.co/vaqX0mNoGf">https://t.co/vaqX0mNoGf</a>、ってことで考えていた</p>&mdash; 怪文書 (@exograph) <a href="https://twitter.com/exograph/status/1307830800361033729?ref_src=twsrc%5Etfw">September 20, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

以前から人工言語界隈にいる方で、筆者の推進していた[Langueプロジェクト](https://conlinguistics.wikia.org/ja/wiki/Langue)という名前を聞いたことがある方がいるかもしれません。結局更新停止してしまっているわけですが、上記のような考えにピンときた方は、ぜひ筆者（[@exograph](https://twitter.com/exograph)）にコンタクトしてください⋯⋯。圧倒的に手が足りてないです。

---

[^0]: https://www.google.com/search?&q=%E3%82%B9%E3%83%A0%E3%83%BC%E3%82%BA%E3%83%A0
[^1]: この問題は他の結合文字も混ざると手に負えなくなります。特に歯まわりの音はExtIPAが絡むと非常にややこしく、<ruby>舌尖音<rt>[apical](https://en.wikipedia.org/wiki/Apical_consonant)</ruby>を表す結合文字は`/◌̺/`なのですが、<ruby>舌尖歯間音<rt>apical interdental</ruby>を表す結合文字は`/◌̺͆/`になり、<ruby>歯唇音<rt>[dentolabial](https://en.wikipedia.org/wiki/Labiodental_consonant#Dentolabial)</ruby>（<ruby>唇歯音<rt>[labiodental](https://en.wikipedia.org/wiki/Labiodental_consonant)</ruby>ではなく、その逆）を表す`/◌͆/`との組み合わせと区別がつきません。なんかもうめちゃくちゃですね
[^2]: まあこれ自体かなり難しそうですが
[^3]: 結構面倒くさそう。 https://devblogs.microsoft.com/oldnewthing/20190916-00/?p=102892
[^4]: [`isomorphic-git`](https://github.com/isomorphic-git/isomorphic-git)というGitのJavaScript実装があるので、ブラウザ上ではこれを使う
[^5]: YAMLはさらにカスタムタグや参照などの機能があって非常に高機能なので、どう使うか迷いますね
[^6]: `O(m log σ)`で、`m`は検索クエリの文字数、`σ`は文字の種類数