住所の表記ゆれを生成AIで何とかできないか（Fine Tuning編－最終版）

[前回](https://qiita.com/mine820/items/9ac99c129f0a9fa23ef8)の記事でFine Tuningを行ったが、とりあえずやっただけの記事になっていたので、ちゃんと最終形にして公開します。

# 学習データ作成
使用するモジュールは2つ。
```python
import numpy as np
import pandas as pd
```

まず、数値を「〇丁目」に変換する関数を用意。
```python
# '一丁目'みたいに変換
def get_choume(c):
    num10 = ["", "", "二", "三", "四", "五", "六", "七", "八", "九"]
    num1  = ["", "一", "二", "三", "四", "五", "六", "七", "八", "九"]
    
    juu = ""
    if (c//10 >= 1):
        juu = "十"
    return num10[c//10] + juu + num1[c%10] + "丁目"
```

一気に「anchor」「positive」「negative」の組み合わせで、「1丁目1番1号」～「19丁目19番19号」を作成。
```python
# データ作成
list_anchor = []
list_positive = []
list_negative = []

for c in range(1,20):
    for b in range(1,20):
        for g in range(1,20):
            print(str(c) + "-" + str(b) + "-" + str(g))
                
            # 間違い
            for inv in range(1,20):
                # 丁目違い
                if inv != c:
                    # 一丁目1番1号
                    list_anchor.append(get_choume(c) + str(b) + "番" + str(g) + "号")
                    list_positive.append(str(c) + "-" + str(b) + "-" + str(g))
                    list_negative.append(get_choume(inv) + str(b) + "番" + str(g) + "号")

                    # 1丁目1番1号
                    list_anchor.append(str(c) + "丁目" + str(b) + "番" + str(g) + "号")
                    list_positive.append(str(c) + "-" + str(b) + "-" + str(g))
                    list_negative.append(str(inv) + "丁目" + str(b) + "番" + str(g) + "号")

                    # 1-1-1
                    list_anchor.append(str(c) + "-" + str(b) + "-" + str(g))
                    list_positive.append(get_choume(c) + str(b) + "番" + str(g) + "号")
                    list_negative.append(str(inv) + "-" + str(b) + "-" + str(g))
                    list_anchor.append(str(c) + "-" + str(b) + "-" + str(g))
                    list_positive.append(str(c) + "丁目" + str(b) + "番" + str(g) + "号")
                    list_negative.append(str(inv) + "-" + str(b) + "-" + str(g))

                # 番違い
                if inv != b:
                    # 一丁目1番1号
                    list_anchor.append(get_choume(c) + str(b) + "番" + str(g) + "号")
                    list_positive.append(str(c) + "-" + str(b) + "-" + str(g))
                    list_negative.append(get_choume(c) + str(inv) + "番" + str(g) + "号")

                    # 1丁目1番1号
                    list_anchor.append(str(c) + "丁目" + str(b) + "番" + str(g) + "号")
                    list_positive.append(str(c) + "-" + str(b) + "-" + str(g))
                    list_negative.append(str(c) + "丁目" + str(inv) + "番" + str(g) + "号")

                    # 1-1-1
                    list_anchor.append(str(c) + "-" + str(b) + "-" + str(g))
                    list_positive.append(get_choume(c) + str(b) + "番" + str(g) + "号")
                    list_negative.append(str(c) + "-" + str(inv) + "-" + str(g))
                    list_anchor.append(str(c) + "-" + str(b) + "-" + str(g))
                    list_positive.append(str(c) + "丁目" + str(b) + "番" + str(g) + "号")
                    list_negative.append(str(c) + "-" + str(inv) + "-" + str(g))

                # 号違い
                if inv != g:
                    # 一丁目1番1号
                    list_anchor.append(get_choume(c) + str(b) + "番" + str(g) + "号")
                    list_positive.append(str(c) + "-" + str(b) + "-" + str(g))
                    list_negative.append(get_choume(c) + str(b) + "番" + str(inv) + "号")

                    # 1丁目1番1号
                    list_anchor.append(str(c) + "丁目" + str(b) + "番" + str(g) + "号")
                    list_positive.append(str(c) + "-" + str(b) + "-" + str(g))
                    list_negative.append(str(c) + "丁目" + str(b) + "番" + str(inv) + "号")

                    # 1-1-1
                    list_anchor.append(str(c) + "-" + str(b) + "-" + str(g))
                    list_positive.append(get_choume(c) + str(b) + "番" + str(g) + "号")
                    list_negative.append(str(c) + "-" + str(b) + "-" + str(inv))
                    list_anchor.append(str(c) + "-" + str(b) + "-" + str(g))
                    list_positive.append(str(c) + "丁目" + str(b) + "番" + str(g) + "号")
                    list_negative.append(str(c) + "-" + str(b) + "-" + str(inv))

# DataFrameにまとめる
out_data = pd.DataFrame(
        data={'anchor': list_anchor, 'positive': list_positive, 'negative': list_negative},
        columns=['anchor', 'positive', 'negative']
    )
```

作成した学習データをCSVファイルに保存。
```python
# ファイルに保存
out_data.to_csv("./output.csv")
```

作成したファイルはこんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/795a013e-d232-de01-467a-e27cf1866a34.png)


# Fine Tuning
学習済みモデルの取得。
```python
from sentence_transformers import SentenceTransformer, SentencesDataset, InputExample, losses, models
from sentence_transformers.evaluation import TripletEvaluator

# 学習済みモデルの読み込み
bert = models.Transformer('sonoisa/sentence-bert-base-ja-mean-tokens-v2')
```

最後にpooling層を追加。
```python
# pooling層の追加
pooling = models.Pooling(bert.get_word_embedding_dimension())
model = SentenceTransformer(modules=[bert, pooling])
```

学習データの読み込み。
```python
# 学習データの読み込み
import pandas as pd
datasets_df = pd.read_csv('./output.csv')

# 学習用と評価用に分割（9:1）
from sklearn.model_selection import train_test_split
train, test = train_test_split(datasets_df, train_size=0.9, random_state=4)
```

ここで、パラメータ値の設定。（必要に応じて調整）
```python
BATCH_SIZE = 16
NUM_EPOCHS = 1
EVAL_STEPS = 1000
WARMUP_STEPS = int(len(train) // BATCH_SIZE * 0.1) 
OUTPUT_PATH = "./sbert_1"
```

学習データのローダーとロスを作成。
```python
from torch.utils.data import DataLoader

# 学習用ローダーの作成
train_dataset = SentencesDataset([InputExample(texts=[row["anchor"], row["positive"], row["negative"]]) for index,row in train.iterrows()], model)
train_dataloader = DataLoader(train_dataset, shuffle=True, batch_size=BATCH_SIZE)

# ロスの作成
train_loss = losses.TripletLoss(model=model)
```

評価の作成。
```python
# 評価の作成
evaluator = TripletEvaluator(test["anchor"].values, test["positive"].values, test["negative"].values)
```
<font color="red">※ネット上の情報で、データローダーを引数とした「TripletEvaluator(test_dataloader)」が見つかるが、少なくとも今は動かないので注意</font>

学習の実行。
```python
# 学習
model.fit(
        train_objectives=[(train_dataloader, train_loss)],
        evaluator=evaluator,
        epochs=NUM_EPOCHS,
        evaluation_steps=EVAL_STEPS,
        warmup_steps=WARMUP_STEPS,
        output_path=OUTPUT_PATH,
        )
```

Fine Tuningした結果は、指定したフォルダに保存される。
こんな感じで。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/35c1ec8b-5676-cc1f-e182-de257a59e780.png)
「pytorch_model.bin」がFine Tuning結果本体で、後は設定値等々。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/5ac0e9f6-ecd8-f1aa-c240-8ebe429045e1.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/4727266f-3325-f15d-ad7f-18d8d585e337.png)
「triplet_evaluation_results.csv」にロス結果が入っている。

ということで、ロスのグラフを表示。
（最後の行の「steps」の値が-1になるので、強制的に修正している）
```python
# ロスのグラフを表示
eval_df = pd.read_csv(OUTPUT_PATH+"/eval/triplet_evaluation_results.csv")
eval_df["steps"][len(train)//(BATCH_SIZE*EVAL_STEPS)*NUM_EPOCHS] = len(train) // BATCH_SIZE
eval_df.plot(x="steps", y=["accuracy_cosinus", "accuracy_manhattan", "accuracy_euclidean"])
```

こんなグラフが表示される。
（epochが1より大きいと重なってしまうので、工夫が必要）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/e424e617-f33b-d846-3f4c-066cb1b1eac6.png)

# 評価結果
Fine Tuningしたモデルの読み込み。
```python
from sentence_transformers import SentenceTransformer, SentencesDataset, InputExample, losses, models

# Fine Tuningしたモデルの読み込み
model = SentenceTransformer(OUTPUT_PATH)
```

評価したいデータ。
```python
# 評価したいデータ
sentences = [
    # 正解
    "東京都港区海岸一丁目２番３号",
    # 表記ゆれ
    "東京都港区海岸一丁目2番3号",
    "東京都港区海岸一丁目二番三号",
    "東京都港区海岸1丁目2番3号",
    "東京都港区海岸１丁目２番３号",
    "東京都港区海岸1-2-3",
    "東京都港区海岸１－２－３",
    # 不正解
    "東京都港区海岸一丁目１番３号",
    "東京都港区海岸一丁目1番3号",
    "東京都港区海岸一丁目一番三号",
    "東京都港区海岸1丁目1番3号",
    "東京都港区海岸１丁目１番３号",
    "東京都港区海岸1-1-3",
    "東京都港区海岸１－１－３",

    "東京都港区海岸一丁目１番１号",
    "東京都港区海岸一丁目1番1号",
    "東京都港区海岸一丁目一番一号",
    "東京都港区海岸1丁目1番1号",
    "東京都港区海岸１丁目１番１号",
    "東京都港区海岸1-1-1",
    "東京都港区海岸１－１－１"
]
```

評価の実行。
```python
# 評価
sentence_vectors = model.encode(sentences)
```

正解とのコサイン距離の計算。
```python
# コサイン距離の計算
import scipy.spatial
distances = scipy.spatial.distance.cdist(sentence_vectors, sentence_vectors, metric="cosine")[0]
```

一応、距離を数値で取得しておく。
```python
distances
```
こんな感じに出力される。（1つ目は正解と正解の距離なので、無視する）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/8c4a05a4-715a-7f64-e8b6-6d49fee51abf.png)

距離を棒グラフで可視化。
```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

# 棒グラフで距離を可視化
left = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])
plt.xticks(left)
height = distances[1:]
plt.bar(left, height)
```
こんな感じに出力される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130771/0ddda601-8f1b-0ce3-6a1a-6f37005e7173.png)

# 最後に
とりあえずこれでひと段落だが、せっかくなので、今後もいろいろとFine Tuningさせてみたい。