住所の表記ゆれを生成AIで何とかできないか（お試し編）

# きっかけ
転職後、徐々に生活が落ち着いてきたので、ここらでエンジニア的なところもペースを戻したいなと。

# で、やりたいこと
例えば企業情報があって、会社名や郵便番号、電話番号などは一致しているんだけど、住所だけ一致しないとき、これは同じ会社の情報なのか判断できないだろうか？

## 前提条件
- あくまでも「表記ゆれ」を対象とします

## （ここでの）表記ゆれの定義
- 半角－全角の違い
- 漢数字－アラビア数字の違い
- 丁目、番地、号の省略（「-」で記述）

## とりあえず保留
- 建物名の省略
- 階数の省略

# 今回のデータ
デジタル庁の[アドレス・ベース・レジストリ](https://www.digital.go.jp/policies/base_registry_address)を参考に正解を決め、それをベースに確認するデータを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/e7e2207f-46be-b946-66c4-1df85c306b67.png)
※今回は、上の「住居表示を実施しているアドレス」を対象にして、下の「住居表示を実施していないアドレス」は見送ります
- 正解
    - 「東京都港区海岸一丁目２番３号」
- 表記ゆれ
    - 半角「東京都港区海岸一丁目2番3号」
    - 全角「東京都港区海岸一丁目2番3号」
    - アラビア数字「東京都港区海岸1丁目2番3号」「東京都港区海岸１丁目２番３号」
    - 丁目、番地、号の省略「東京都港区海岸1-2-3」「東京都港区海岸１－２－３」
- 不正解
    - 正解「東京都港区海岸一丁目１番１号」
    - 半角「東京都港区海岸一丁目1番1号」
    - 漢数字「東京都港区海岸一丁目一番一号」
    - アラビア数字「東京都港区海岸1丁目1番1号」「東京都港区海岸１丁目１番１号」
    - 丁目、番地、号の省略「東京都港区海岸1-1-1」「東京都港区海岸１－１－１」

一応、不正解との比較もします。
なお、町名までは合っているもの（郵便番号があっている前提なので）としました。

# 環境構築
まずはベースモデルとして、[日本語用Sentence-BERTモデル（バージョン2）](https://huggingface.co/sonoisa/sentence-bert-base-ja-mean-tokens-v2)を使用します。
ライセンスは「cc-by-sa-4.0」とのことですので、利用を明記しておけば商用利用も可能です。

## インストール
以下をインストールします。（詳細は省略）
- CUDA 11.8、cuDNN v8.9.4
- PyTorch 2.0.1（とりあえずGPU版）
- transformers 4.7.0
- fugashi、ipadic

また、ローカル環境でJupyterNotebookを使いました。
（matplotlibも使用しました）

# ソース
公式ページに従い、以下のコードを作成します。（途中まで）
```python
from transformers import BertJapaneseTokenizer, BertModel
import torch


class SentenceBertJapanese:
    def __init__(self, model_name_or_path, device=None):
        self.tokenizer = BertJapaneseTokenizer.from_pretrained(model_name_or_path)
        self.model = BertModel.from_pretrained(model_name_or_path)
        self.model.eval()

        if device is None:
            device = "cuda" if torch.cuda.is_available() else "cpu"
        self.device = torch.device(device)
        self.model.to(device)

    def _mean_pooling(self, model_output, attention_mask):
        token_embeddings = model_output[0] #First element of model_output contains all token embeddings
        input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
        return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(input_mask_expanded.sum(1), min=1e-9)


    def encode(self, sentences, batch_size=8):
        all_embeddings = []
        iterator = range(0, len(sentences), batch_size)
        for batch_idx in iterator:
            batch = sentences[batch_idx:batch_idx + batch_size]

            encoded_input = self.tokenizer.batch_encode_plus(batch, padding="longest", 
                                           truncation=True, return_tensors="pt").to(self.device)
            model_output = self.model(**encoded_input)
            sentence_embeddings = self._mean_pooling(model_output, encoded_input["attention_mask"]).to('cpu')

            all_embeddings.extend(sentence_embeddings)

        # return torch.stack(all_embeddings).numpy()
        return torch.stack(all_embeddings)


MODEL_NAME = "sonoisa/sentence-bert-base-ja-mean-tokens-v2"  # <- v2です。
model = SentenceBertJapanese(MODEL_NAME)
```

そのあとに、今回のデータを用意して、ベクトルを求めます。
```python
sentences = [
    # 正解
    "東京都港区海岸一丁目２番３号",
    # 表記ゆれ
    "東京都港区海岸一丁目2番3号",
    "東京都港区海岸一丁目二番三号",
    "東京都港区海岸1丁目2番3号",
    "東京都港区海岸１丁目２番３号",
    "東京都港区海岸1-2-3",
    "東京都港区海岸１－２－３",
    # 不正解
    "東京都港区海岸一丁目１番１号",
    "東京都港区海岸一丁目1番1号",
    "東京都港区海岸一丁目一番一号",
    "東京都港区海岸1丁目1番1号",
    "東京都港区海岸１丁目１番１号",
    "東京都港区海岸1-1-1",
    "東京都港区海岸１－１－１"
]
sentence_vectors = model.encode(sentences)
```
ここでは768次元のベクトルが求まります。


最後に正解とのコサイン距離を求めます。
```python
import scipy.spatial
distances = scipy.spatial.distance.cdist(sentence_vectors, sentence_vectors, metric="cosine")[0]
```

# 結果
何のチューニングもしない状態で、以下の結果になりました。

|  |  | 東京都港区海岸一丁目２番３号 |
|---:|---|---:|
| 1 | 東京都港区海岸一丁目2番3号 | 2.22044605e-16 |
| 2 | 東京都港区海岸一丁目二番三号 |  6.11741123e-02 |
| 3 | 東京都港区海岸1丁目2番3号 | 7.29590979e-03 |
| 4 | 東京都港区海岸１丁目２番３号 | 7.29590979e-03 |
| 5 | 東京都港区海岸1-2-3 | 5.00328437e-02 |
| 6 | 東京都港区海岸１－２－３ | 5.00328437e-02 |
| 7 | 東京都港区海岸一丁目１番１号 | 2.45641245e-01 |
| 8 | 東京都港区海岸一丁目1番1号 | 2.45641314e-01 |
| 9 | 東京都港区海岸一丁目一番一号 | 2.57492124e-01 |
| 10 | 東京都港区海岸1丁目1番1号 | 2.58257453e-01 |
| 11 | 東京都港区海岸１丁目１番１号 | 2.58257453e-01 |
| 12 | 東京都港区海岸1-1-1 | 2.06726289e-01 |
| 13 | 東京都港区海岸１－１－１ | 2.06726289e-01 |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/284868f5-fd47-54d0-c283-86f23432888d.png)

一応、正解（表記ゆれ）／不正解はわかりそう。
なおアラビア数字・ハイフンの半角・全角は、どっちかにそろえてから処理していそうですね。

# 今後の予定
- 今のモデルを住所の表記ゆれ用にチューニングしたいと思います
- モデル自体をもっと性能の良いものに見直したいです

# 参考資料
こちらを参考にさせていただいております。

https://qiita.com/sonoisa/items/1df94d0a98cd4f209051