こんにちは、バックエンドエンジニアの飯沼です。
この記事では、『NEWT(ニュート)』で海外ツアーを予約する際に、パスポートの氏名の入力誤りを防ぐ仕組みを解説します。「スペル誤り」や「姓名逆入力」を、アルファベット氏名の辞書を使った簡単な方法でチェックできると考えて開発を始めましたが、思いのほか工夫が必要で、最終的にはニューラルネットワークを利用して「姓らしさ」をスコアリングするアプローチで解決しました。課題の背景から開発時の試行錯誤も含めて説明したいと思います。
🚨 課題:氏名を間違えると飛行機に乗れない
NEWTでは航空券やホテルの手配のために、ツアー予約時にアルファベット表記の氏名を入力頂いています。この氏名がパスポート記載の氏名と一致しないと、場合によっては飛行機に乗れないという最悪の体験をもたらすことになります。当然、これを防ぐために、手配時に誤りがないか人手でチェックをして、誤りの可能性がある場合はお客さまに確認をしたうえで手配を行っており、それなりの人的コストが掛かっている状態でした。この課題に対してはOCRを利用した「パスポートスキャナー」やプレースホルダーの見直しなど、以前から改善を行ってきましたが、氏名の入力誤りはまだ多く改善の余地があったため今回紹介するアラート機能を開発しました。
具体的には次の2種類の誤りが多く発生していました。
- 姓と名が逆:NEWTでは入力欄が姓→名の順に並んでいて、例えばYAMDA SAKURAと入力すべきところにSAKURA YAMADAと入力するパターンです。※ 実は私も初めてNEWTに登録する際に同じミスをしていました。これまでアルファベットで氏名を入力する際は名→姓の順で記載することが多かったので癖でそのように入力したのだと思います。
- パスポートに記載のヘボン式とは異なる表記:例えばSHUMPEIをSHUNPEIと記載してしまうようなケースです。※ ヘボン式ではP,Bなどの破裂音の直前の「ん」はNではなくMと表記します。私自身も初めてパスポートを新規申請した際に指摘されて知りました。初めての海外旅行でまだパスポートが手元に準備できてない場合に多く発生すると考えられます。なお、パスポート氏名は原則ヘボン式ですが、申請すればヘボン式以外の表記でも登録可能です。
💡 最初のアプローチ:氏名の辞書を利用する
まず、日本人のヘボン式アルファベット氏名の辞書を利用した方法を試しました。
※ 辞書はコールセンター業務などで漢字の読みを調べるなどの目的で販売・利用されているものであり、特定の個人名を含むものではありません。姓 (surname) と名 (given name) は別々の辞書として提供されています。
- 入力された姓が姓の辞書に存在し、かつ、入力された名が名の辞書に存在する → 誤りなしと判定
- 入力された姓が名の辞書に存在し、かつ、入力された名が姓の辞書に存在する → 姓と名が逆に入力されている可能性が高いと判定
- 入力された姓または名が辞書に存在しない → ヘボン式でないなど、スペル誤りの可能性が高いと判定
誤り(の可能性)を検出した場合はそれぞれ以下のようなアラートを表示します。
実装後の社内でのQAの結果… 上記の方法ではスペル誤りは検知できても姓名逆入力の検知が十分に機能しませんでした。
なぜでしょうか?辞書を調べたら、名の辞書に姓らしきエントリーが多く含まれており(その逆も)、逆転して入力した姓名が辞書のエントリーとマッチして誤りとして検知できていないことが分かりました。例えば、日本人の姓として代表的なSUZUKI, TANAKAなどが名の辞書にも含まれ、名として代表的なTAROが姓の辞書にも含まれていました。
詳しく見てみると2つ原因がありました。
- アルファベット表記にした場合に、姓名どちらともあり得るエントリーが存在する:例えば、TAROは名の「太郎(たろう)」、姓の「多呂(たろ)」、SUZUKIは姓の「鈴木(すずき)」、名の「鈴希(すずき)」とも解釈が可能です。姓7.6万件、名5.4万件のうち約1.7万件が姓/名のどちらにも存在していました。
- 辞書の不具合:私はお会いしたことがないのですが名の辞書に姓に多い「田中(たなか)」さんがいるなど、他にも気になるエントリーが多数存在しました。
辞書の不具合は購入元に問い合わせて解決できても、結局、私が知らない姓名どちらともあり得るエントリーが無数に存在するため、今回は次に紹介するアプローチでまとめて解決することにしました。
💡「姓らしさ」をスコアリングする
入力された姓名が逆であることを人間が判別する際には、姓/名それぞれを個別に見ているのではなく、総合的に見てこっちの方が姓(または名)らしいな、と考えているでしょう。これをヒントに、姓/名のバイナリーではなく、どっちが「姓」らしいかスコアリングするアプローチに切り替えました。
- 入力された姓/名の「姓らしさ」をそれぞれスコアリングする
- (入力された姓のスコア) < (入力された名のスコア) の場合に、姓名が逆転している可能性が高いと判断してアラートを表示する
例えば、*SAWA(沢)、*NUMA(沼)のようなパターンの名前は姓の可能性が高く、*HEI(平)のようなパターンは名である可能性が高い、というように表層的な文字の並びから姓/名の判別をすることができると考え、今回はニューラルネットワークによる分類モデルの学習を試しました。
Step 1: 学習の準備
姓/名の辞書のうち、どちらか一方の辞書にしか存在しないエントリーを利用します。エントリー数の内訳は以下の通りです。
- 姓の辞書にしか存在しないエントリー:約6.0万件
- 名の辞書にしか存在しないエントリー:約3.8万件
まず、氏名をニューラルネットワークに入力するために、今回はシンプルなOne-Hotエンコーディングで文字列を行列に変換する関数を準備します。※ 以降Pythonのサンプルコードを使って説明します。
import numpy as np MAX_LENGTH = 18 + 5 # 氏名の最大長 + 辞書への追加に備え、余裕をもたせて +5 = 23 def one_hot_encode(name): # 'abe' -> 'a', 'b', 'e' -> [1, 2, 5] xs = [ord(c) - ord('a') + 1 for c in name] # 長さを揃えるために末尾を0埋めする xs += [0] * (MAX_LENGTH - len(xs)) col_size = ord('z') - ord('a') + 2 # a-zの26種類 + 0値の1種類 = 27 one_hot = np.zeros((MAX_LENGTH, col_size)) for row, col in enumerate(xs): one_hot[row,col] = 1 if col != 0 else 0 return one_hot
例えば、入力が
abe
の場合、次のような2次元配列(MAX_LENGTH
行、27列の行列)を出力します。[ #0, a, b, c, d, e, f, ... # (27列) [0, 1, 0, 0, 0, 0, 0, ...], # a [0, 0, 1, 0, 0, 0, 0, ...], # b [0, 0, 0, 0, 0, 1, 0, ...], # e [0, 0, 0, 0, 0, 0, 0, ...], # 他のエントリーと長さを揃えるため0埋め ... # (MAX_LENGTH行) ]
上記の関数を使ってニューラルネットワークへの入力と教師ラベルを準備します。
family_names_only = ['abe', 'aizawa', ...] # 姓にしか存在しないエントリー first_names_only = ['aiko', ...] # 名にしか存在しないエントリー # ニューラルネットワークへの入力 family_name_inputs = np.stack([one_hot_encode(name) for name in family_names_only]) first_name_inputs = np.stack([one_hot_encode(name) for name in first_names_only]) all_inputs = np.concatenate([family_name_inputs, first_name_inputs]) # 3次元配列: エントリー数 x 氏名の最大長 x a-zと0値の27種類 family_name_inputs.shape # -> (59959, 23, 27) first_name_inputs.shape # -> (37843, 23, 27) all_inputs.shape # -> (97802, 23, 27) # 教師ラベル family_name_labels = np.ones(family_name_inputs.shape[0]) # 姓 → 1 first_name_labels = np.zeros(first_name_inputs.shape[0]) # 名 → 0 all_labels = np.concatenate([family_name_labels, first_name_labels]) # 1次元配列: インデックスは上記inputに対応しており、姓ならば1、名なら0が設定されている family_name_labels.shape # -> (59959,) first_name_labels.shape # -> (37843,) all_labels.shape # -> (97802,)
Step 2: ニューラルネットワークの学習と評価
学習と評価のためデータを分割します。以下の例ではscikit-learnの
train_test_split
を利用して、8割を学習用、2割をテスト用として分割しています。from sklearn.model_selection import train_test_split x_train, x_test, y_train, y_test = train_test_split(all_inputs, all_labels, test_size=0.2, random_state=32) # 学習に約7.8万件、テストに約2万件 x_train.shape # -> (78241, 23, 27) y_train.shape # -> (78241,) x_test.shape # -> (19561, 23, 27) y_test.shape # -> (19561,)
次にTensorFlowを利用してニューラルネットワークを構築します。今回はチュートリアルにもある手書き数字の分類モデルをベースに構築しました。
import tensorflow as tf nn_model = tf.keras.models.Sequential([ # 入力層: 2次元配列(23x27の行列) → 1次元(ベクトル)への変換 tf.keras.layers.Flatten(input_shape=(23, 27)), # 中間層 tf.keras.layers.Dense(128, activation='relu'), tf.keras.layers.Dropout(0.2), # 出力層: 姓/名 (1/0) の2値分類のため2 tf.keras.layers.Dense(2) ])
# 損失関数 # SparseCategoricalCrossentropyを使うことで、教師ラベルをOne-Hotでなく整数のまま利用できる loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) # 学習と評価 nn_model.compile(optimizer='adam', loss=loss_fn, metrics=['accuracy']) nn_model.fit(x_train, y_train, epochs=15) nn_model.evaluate(x_test, y_test, verbose=2)
この時点で、学習データに対して0.89、テストデータに対しては0.87の精度で姓/名を分類できました。
その後、中間層を増やしてみたり、CNNのモデルで試行錯誤してみましたが、精度はそれほど改善せず、最終的には最初に試した上記のモデルを採用しました。ちなみに、中間層を1層増やした場合は、学習データに対する精度は0.9を超えるようになりますが、テストデータに対する精度はむしろ下がり、学習データに対して過学習したような状態となりました。CNNのモデルで精度が改善しなかった理由としては、画像とは異なり、氏名という短い文字列のため、2次元空間にしたところで分類に有益な情報とはならなかったのではないかと考えています。
Step 3: 「姓らしさ」のスコアリング
最後に、学習したモデルを使って姓/名どちらの辞書にも存在するエントリーの「姓らしさ」をスコアリングします。モデルの出力をsoftmax関数に渡すことで確率を得ることができます。
encoded = one_hot_encode("suzuki").reshape(1, 23, 27) tf.keras.Sequential([ nn_model, tf.keras.layers.Softmax() ])(encoded) # -> [0.47084972, 0.52915025] 0.529の確率で姓(index=1)
これを利用して、以下のように「姓名逆入力」を検知します。
- input: 姓=TARO, 名=SUZUKI
- 姓である確率:
- (姓)TARO: 0.32
- (名)SUZUKI: 0.52
- 結果: 姓である確率(姓) < 姓である確率(名) → 「姓名逆入力」と判定
おわりに
以上、この記事ではNEWTの氏名入力誤りを防ぐための仕組みについて解説しました。この仕組みによって氏名を間違えずに確実に海外旅行を楽しんで頂けたら幸いです。
ニューラルネットワークは知っているけどOne-Hotエンコーディングなど文字列の扱い方は知らなかったという方にとっては、今後の課題解決の手段として参考になれば幸いです。ニューラルネットワーク自体に関しては以下の文献がとてもわかりやすいのでぜひ参考にしてください。
また、令和トラベルでは、一緒に働く仲間を募集しています。興味を持っていただいた方がいれば、ぜひご連絡をお待ちしております!