若くない何かの悩み

何かをdisっているときは、たいていツンデレですので大目に見てやってください。

子供の命名のために名前を探索するツールを作った

子供が産まれるのに備え子供の名前を探索するツールを作りました。Linux、macOS、Windows で次のように名前の候補を列挙してくれます:

$ name search --space full 山田 --max-length 2 < ./filter.json | tee result.tsv
評点    画数    名前    読み    性別    天格    地格    人格    外格    総格
14      16      丈辞    ジョウジ        男性    吉      大吉    吉      大吉    大大吉
13      21      丈騎    タケキ  男性    吉      大吉    吉      大吉    大吉
...

github.com

名前探索器を開発した背景

12月に第二子が産まれました。第一子は人力で名前を探索したところあまり良い名前が思い浮かばず苦労した経験があります。最終的には友人の手を借りて命名したのですが、今回もまたお世話になるのは申し訳なかったため、システム的な解決を図りました。

開発した名前探索器の紹介

kuniwak/name は、常用漢字と人名用漢字、ひらがな、カタカナからなる文字列の空間から条件に当てはまる名前を探索します。探索の条件として JSON 形式でフィルタを指定します。フィルタによって空間内の文字列それぞれが判定され、フィルタの結果が真なら結果に残り、偽なら結果から取り除かれます。フィルタには次の要素を使えます:

説明 構文
{"true": {}} {"true": {}}
{"false": {}} {"false": {}}
論理積 {"and": [filter...]} {"and": [{"yomiCount": {"rune": "ア", "count": {"equal": 1}}}, {"yomiCount": {"rune": "イ", "count": {"equal": 1}}}]}
論理和 {"or": [filter...]} {"or": [{"yomiCount": {"rune": "ア", "count": {"equal": 1}}}, {"yomiCount": {"rune": "イ", "count": {"equal": 1}}}]}
否定論理 {"not": filter} {"not": {"yomiCount": {"rune": "ア", "count": {"equal": 1}}}}
性別 {"sex": sex} {"sex": "asexual"}
長さ {"length": count} {"length": 3}
読み仮名のモーラ数 {"mora": count} {"mora": {"equal": 3}}
よくある読み仮名 {"commonYomi": {}} {"commonYomi": {}}
画数 {"strokes": count} {"strokes": {"lessThan": 25}}
五格それぞれの最小値 {"minRank": 0-4}4=大大吉, 3=大吉, 2=吉, 1=凶, 0=大凶) {"minRank": 3}
五格の合計値の最小値 {"minTotalRank": byte} {"minTotalRank": 11}
指定した読み仮名の数 {"yomiCount": {"rune": string, "count": count}} {"yomiCount": {"rune": "ア", "count": {"equal": 1}}}
読み仮名のマッチ {"yomi": match} {"yomi": {"equal": "タロウ"}}
漢字のマッチ {"kanji": match} {"kanji": {"equal": "タロウ"}}
指定した漢字の数 {"kanjiCount": {"rune": string, "count": count}} {"kanjiCount": {"rune": "漢", equal": 1}}
count {"equal": byte} or {"lessThan": byte} or {"greaterThan": byte} {"lessThan": 1}
match {"equal": string} or {"startWith": string} or {"endWith": string} {"startWith": "タロ"}
sex "asexual" or "male" or "female" {"sex": "asexual"}

ちなみに私の使ったフィルタは次のとおりです:

{
  "and": [
    {"sex": "male"},
    {"mora": {"equal": 3}},
    {"minRank": 2},
    {"minTotalRank": 11},
    {"commonYomi": {}},
    {"length": {"equal": 2}},
    {
      "or": [
        {
          "and": [
            {"yomiCount": {"rune": "", "count": {"equal": 1}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"lessThan": 2}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}}
          ]
        },
        {
          "and": [
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"equal": 1}}},
            {"yomiCount": {"rune": "", "count": {"lessThan": 2}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}}
          ]
        },
        {
          "and": [
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"equal": 1}}}
          ]
        }
      ]
    },
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}}
  ]
}

このフィルタでは最低限の姓名判断のほか、性別の指定、モーラ数の制限、長さの制限、よくある名前の読みとの一致、親と子の名前の同一性の制限(第一子との公平性のため)、望ましくない漢字の除去をしています。姓名判断を条件に加えた理由は、自分の名前を姓名判断にかけてみるときちんと良いものであることがわかり親はちゃんと考えてつけたんだなあということがわかるからです。

このフィルタによって、私の苗字では全空間探索によりおよそ 4500 件ほどの名前の候補が得られます。あとはこれを Google Sheets 等にまとめ、好ましい名前を抽出するとよいでしょう。5000件未満であれば 1h 未満で目視による選別を終えられます。

仕組み

このツールは全空間探索と頻出空間探索の2つの探索モードを持っています:

  • 全空間探索
    • 常用漢字と人名用漢字、ひらがな、カタカナからなる文字列の空間から名前を探索します。低速ですが候補数は多くなります。探索する名前の長さの上限値に2より大きい数を指定すると候補数が爆発的に増えるため非常に低速になります。名前の読みは MeCab によって推定します。
  • 頻出空間探索
    • s1r-J/jinmei-dict を使い、よくある名前とその読みのペアからなる空間から探索します。高速ですが候補数は少なくなります。

この探索範囲から得られた名前の候補について性別やモーラ数を推定します。性別の推定には ENAMDICT/JMnedict を使っています。そうして得られた名前の候補と付加情報をもとにフィルタの条件を満たすもののみを表示します。

読みの推定方法の模索

前述の通り、読みの推定には MeCab を使っています。辞書として NEologd を使うと現代的な読みを出力してくれます。なお MeCab を使う前には常用漢字 + 人名用漢字の漢字ごとに標準的な読みのリストのデカルト積を取っていました。ただあまりにも精度が悪かったために別の方法を模索し neologd/namelti にいきつきました(Namelti の紹介記事)。Namelti は NEologd の辞書を使った MeCab により人名の読みを推定するツールです。

ちなみに Namelti の精度の評価には苦労しました。そのままではビルドできないためパッチをあてる必要がありました。最終的に Docker 上でビルドに成功し(kuniwak/debian-namelti として公開)、その精度がかなりよいことがわかりました。ただ Namelti そのままを使おうとするとプロセスが別になるため IPC が必要となり、この IPC は名前の候補数だけ呼び出されるため呼び出しのオーバーヘッドが大きいです。Namelti の実装を読んだところ、MeCab を素朴に使っているだけということがわかったため MeCab の Go 言語バインディングである shogo82148/go-mecab を使い、IPC を使わず動作するように Go 言語で Namelti を再実装しました。

Windows から cgo で MeCab を呼び出す

一番苦労したのは Windows 環境の cgo 経由で MeCab を呼び出すことです。shogo82148/go-mecab では CGO_LDFLAGSCGO_CFLAGSmecab-config が指示する値で設定するよう指示しています。ただ Windows のコマンドプロンプトや PowerShell から、Bash で実装されている mecab-config そのままを呼び出すことができないこと、また C:\Program Files\MeCab のような空白を含むパス配下にあるヘッダやオブジェクトファイルがあると cgo で意図しない位置でパラメータの区切りと判定されることがハードルとなりました。そこで C# で mecab-config を再実装し、これらの問題を解決しました(kuniwak/mecab-config-windows)。これは dotnet tool install -g MecabConfig でインストールできます。この実装では次のように空白を含むパスでも cgo に渡るパラメータが正しく解釈されるようにエスケープしています:

$ mecab-config --libs
"-LC:\Program Files\MeCab\lib" -lmecab -lstdc++

ただぶっちゃけ、WSL2 で起動する方が百万倍楽だと思います。WSL2 で使ってください。

終わりに

こうして作った名前の探索器から得られた候補から子供に名前をつけられました。