1
0

日本語Bertでセマンティック検索

Posted at

前回:https://qiita.com/momae41/items/20365326777d4a914ff3
この記事ではPineconeを用いて日本語でセマンティック検索を実践した例を紹介します。

目次

使ったモデル
実装内容
課題
感想
参考

使ったモデル

今回は参考記事をもとに以下のモデルを使用しました。

実装内容

セットアップ

まずはなんでもいいのでセンテンスを用意してください。今回は自作しました。

all_sentences = [
    "ダーウィンの進化論は嘘です",
    "バナナはおやつに入ります",
    "地球は平面です",
    "日本では応用数学科はまだまだ少ない方です",
    "数学と数理科学の違いはなんですか",
    "散歩をするとバナナがおやつに入らないかもしれないことに気づきました",
    "地球の半径を数理的に求めることができます",
    "私はバナナが好きな数学者です",
    "数学とバナナは同じです",
    "残念ながら、地球とバナナとゴリラは同じではありません",   #10
    "ダーウィンはバナナをおやつと考えました",
    "これ以上無意味な文章を作ることをやめませんか",
    "数理の世界は長い年月を経て進歩してきましたが、人間は長い年月を経てゴリラに近づきました",
    "ダーウィンは進化論の提唱者ですが、ダテミキオはカロリー0理論の提唱者です",
    "その理論を応用することで、バナナを用いてブラックホールを生成する方法を数学的に導くことができます",
    "ピザはその高さを0に近づけることで体積が0に近づくためカロリーは0",
    "ダーウィンはゴリラの進化元です",
    "バナナのカロリーは1本86キロカロリーです",
    "どうして地球にはピザが自生していないのですか",
    "ここまでだいたい嘘"   #20
]

参考記事と同様にベクトル化します。

import torch
device = 'cuda' if torch.cuda.is_available() else 'cpu'
from sentence_transformers import SentenceTransformer

model = SentenceTransformer('sentence-transformers/paraphrase-multilingual-mpnet-base-v2', device=device)

all_embeddings = model.encode(all_sentences)

トークン化

from transformers import BertJapaneseTokenizer

tokenizer_cl_tohoku = BertJapaneseTokenizer.from_pretrained('cl-tohoku/bert-base-japanese')

all_tokens_cl_tohoku = [tokenizer_cl_tohoku.tokenize(sentence) for sentence in all_sentences]

Pineconeの準備とデータの挿入まで

from pinecone import Pinecone, PodSpec
pc = Pinecone(api_key='<APIキー>')
pc.create_index(
    name="semantic-search-in-japanese", 
    
    dimension=all_embeddings.shape[1], 
    metric="cosine",
    spec=PodSpec(
        environment='gcp-starter', 
        pod_type='starter'
    )
)
index = pc.Index(name="semantic-search-in-japanese")
upserts = []
for i, (embedding, tokens) in enumerate(zip(all_embeddings, all_tokens_cl_tohoku)):
    upserts.append((str(i), embedding.tolist(), {'tokens': tokens}))

index.upsert(vectors=upserts)

検索

ここまでの作業でデータが入ったので色々検索してみます。結果は簡略化しています。
「数学」

query = "数学"
xq = model.encode([query]).tolist()

result = index.query(vector=xq, top_k=5, includeMetadata=True)
result
{'matches': [{'id': '4',
              'metadata': {'tokens': ['数学',
                                      'と',
                                      '数理',
                                      '科学',
                                      'の',
                                      '違い',
                                      'は',
                                      'なん',
                                      'です',
                                      'か']},
              'score': 0.705078781,
              'values': []},
             {'id': '6',
              'metadata': {'tokens': ['地球',
                                      'の',
                                      '半径',
                                      'を',
                                      '数理',
                                      '的',
                                      'に',
                                      '求める',
                                      'こと',
                                      'が',
                                      'でき',
...
                                      'です']},
              'score': 0.426291198,
              'values': []}],
 'namespace': '',
 'usage': {'read_units': 6}}

「バナナと平面」

query = "バナナと平面"
xq = model.encode([query]).tolist()

result = index.query(vector=xq, top_k=5, includeMetadata=True)
result
{'matches': [{'id': '1',
             'metadata': {'tokens': ['バナナ', 'は', 'お', 'やつ', 'に', '入り', 'ます']},
             'score': 0.838205338,
             'values': []},
            {'id': '9',
             'metadata': {'tokens': ['残念',
                                     'ながら',
                                     '、',
                                     '地球',
                                     'と',
                                     'バナナ',
                                     'と',
                                     'ゴリ',
                                     '##ラ',
                                     'は',
                                     '同じ',
                                     'で',
                                     'は',
                                     'あり',
                                     'ませ',
                                     'ん']},
             'score': 0.720716298,
             'values': []},
            {'id': '8',
             'metadata': {'tokens': ['数学', 'と', 'バナナ', 'は', '同じ', 'です']},
...
                                     'た']},
             'score': 0.652782381,
             'values': []}],
'namespace': '',
'usage': {'read_units': 6}}

「ダーウィンは数学者ですか?」

query = "ダーウィンは数学者ですか?"
xq = model.encode([query]).tolist()

result = index.query(vector=xq, top_k=5, includeMetadata=True)
result
{'matches': [{'id': '16',
              'metadata': {'tokens': ['ダーウィン',
                                      'は',
                                      'ゴリ',
                                      '##ラ',
                                      'の',
                                      '進化',
                                      '元',
                                      'です']},
              'score': 0.746318281,
              'values': []},
             {'id': '13',
              'metadata': {'tokens': ['ダーウィン',
                                      'は',
                                      '進化',
                                      '論',
                                      'の',
                                      '提唱',
                                      '者',
                                      'です',
                                      'が',
                                      '、',
                                      'ダ',
                                      '##テ',
                                      '##ミ',
...
                                      'た']},
              'score': 0.404249668,
              'values': []}],
 'namespace': '',
 'usage': {'read_units': 6}}

課題

上ではなんとなく良さそうなレスポンスが得られていますが、残念ながらトークン化の段階で失敗している単語も複数あります。
例えば「ゴリラ」は不適切に処理されており、ゴリラに関するレスポンスは返ってきません。
image.png

query = "ゴリラ"
xq = model.encode([query]).tolist()

result = index.query(vector=xq, top_k=5, includeMetadata=True)
result
{'matches': [{'id': '19',
              'metadata': {'tokens': ['ここ', 'まで', 'だい', '##たい', '嘘']},
              'score': 0.527226746,
              'values': []},
             {'id': '1',
              'metadata': {'tokens': ['バナナ', 'は', 'お', 'やつ', 'に', '入り', 'ます']},
              'score': 0.415789127,
              'values': []},
             {'id': '11',
              'metadata': {'tokens': ['これ',
                                      '以上',
                                      '無意',
                                      '##味',
                                      'な',
                                      '文章',
                                      'を',
                                      '作る',
                                      'こと',
                                      'を',
                                      'やめ',
                                      'ませ',
                                      'ん',
                                      'か']},
              'score': 0.3710922,
              'values': []},
...
                                      'です']},
              'score': 0.349844605,
              'values': []}],
 'namespace': '',
 'usage': {'read_units': 6}}

英語はスペースで単語が区切られるので、そもそも日本語でのtokenizeが難しいことを実感。

感想

ベクトルの類似度を検索に利用するのはシンプルながら強力なアイデアだと思いました。自然言語処理の話は初めて体験しましたが、興味あるドメインで検索機能を利用してチャットボットも作りたくなりました。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0