🤤

ChatGPTで日英医薬品マスター作成の試み

2023/05/13に公開

結論

  • DeepL + GPT3.5である程度日英の医薬品名の名寄せは可能。
  • SemanticSimilarityExampleSelector + Few Shotが有効。
  • 名寄せ補助ツールとして、ChatGPTを使うのはあり。

背景

顧客「臨床データベースA(DB)と臨床データベースBを統合して解析してほしい」
こういった要望は定期的にあるがさて困った。なぜなら2つのDBは同じ臨床データなのだが、言語が異なるのだ。
またそれぞれ独自の入力ルールが多々見られ、良い結合keyが見つからない。あえて選ぶとしたら医薬品名になるのだが、片方を単純に翻訳しても医薬品名同士で結合できるわけではない。

医薬品名は一般名、製剤名、製品名、化学名など多種多様[1]であり、加えてデータベースによっては独自の前処理を施している場合もあるため、同じ言語の一般名同士であっても結合できないケースがある(というより結合できないことがほとんど)。

参考に、単純な翻訳で結合できないパターンの一例を示す。
一般名(JP):アダリムマブ(遺伝子組換え)
単純な翻訳:Adalimumab (genetical recombination)
あるDB:adalimumab

大文字、小文字は前処理で対応できるが、「(遺伝子組換え)」という文字があるDBでは抜けている。こういった例であっても容易に結合できるマスターがあると医薬品名を使った研究が捗る。

そこで、日本語と英語の医薬品名を名寄せできるマスターをChatGPT(API)を用いて自作することにした。
(正直に言うと、名寄せ処理はとにかくしんどいのでChatGPTがうまーく処理してくれることに期待している)
(一応下記操作の大半はWebのChatGPTでできることを確認済み)

方法

使用データ

  • 日本語の医薬品一般名(以下、J一般名)を基準とする。日本語の医薬品名は医薬品副作用データベース(JADER)から取得した。一部のJ一般名を使用した(3,774件)。
  • 英語の医薬品一般名(以下、E一般名)は、FDA Adverse Event Reporting System(FAERS)から取得した。

操作手順

日本語の一般名をDeepL(API)で翻訳した結果を翻訳医薬品一般名(以下、翻訳名)とした。
翻訳名とE一般名が突合できなかったものに関して、ChatGPTを使用してJ一般名を下記実験条件に沿って再翻訳した。(※実際は穴埋め問題を解かせた)

  1. Zero Shot
  2. Few Shot
  3. Few Shot + Example Selector

Example Selectorは「SemanticSimilarityExampleSelector」を使用した。
これは入力とのコサイン類似度が最大の埋め込みをFew Shot用のサンプルとして使用するものである。例えば「インスリン グルリジン」を名寄せしたいときに、Few Shotに「シスプラチン」や「アスピリン」を使用するよりも「インスリン デグルデク」や「インスリン リスプロ」を使用したほうが筋の良い回答を返してくれる期待が高まる。
これを実現してくれるのがSemanticSimilarityExampleSelector。

なお翻訳名とE一般名が名寄せできたものは1,164件であった。名寄せできなかったものは2,610件であった。名寄せできた1,164件中から6件をFew Shot Sampleとして選択した。

主なライブラリやモデルとか

  • langchain==0.0.125
  • openai==0.27.2
  • GPT3.5-turbo
  • python3.9

Prompt

Zero Shot

# Zero Shot
# DataFrame「df」の作成は割愛

# プロンプトの定義
template = """
I want you to act as an expert of translation.

For each Japanese generic name, write the English generic name in lower case.
- {text}
"""

prompt = PromptTemplate(
    input_variables=["text"],
    template=template,
)

# ※本来は全てのJ一般名をループさせて回答をリストに保存するが、以下のサンプルは1例のみ。
llm = OpenAI(model_name="gpt-3.5-turbo", temperature=0.0, frequency_penalty=0.0)
llm(prompt.format(text=df['generic_name_jp'].iloc[0]))

Few Shot

# Few Shot

# 例リストの作成
examples = [
    {'general_name_jp': 'ジヒドロコデインリン酸塩', 'general_name_en': 'dihydrocodeine phosphate'},
    {'general_name_jp': 'クロピドグレル硫酸塩', 'general_name_en': 'clopidogrel sulfate'},
    {'general_name_jp': 'セラペプターゼ', 'general_name_en': 'serrapeptase'},
    {'general_name_jp': '塩化ストロンチウム(89Sr)', 'general_name_en': 'strontium (89 sr) chloride'},
    {'general_name_jp': 'アガルシダーゼ アルファ', 'general_name_en': 'agalsidase alfa'},
    {'general_name_jp': 'ニボルマブ', 'general_name_en': 'nivolumab'}
]

# 例をフォーマットするためのテンプレートを作成
example_formatter_template = """
Japanese generic name: {general_name_jp}
English generic name: {general_name_en}\n
"""

example_prompt = PromptTemplate(
    input_variables=["general_name_jp", 'general_name_en'],
    template=example_formatter_template,
)

# FewShotPromptTemplateオブジェクトを作成する
few_shot_prompt = FewShotPromptTemplate(
    examples=examples,
    example_prompt=example_prompt,
    prefix='For each Japanese generic name, write the English generic name in lower case.', # prefix: プロンプトの例の前に置かれるテキスト
    suffix='Japanese generic name: {input}\nEnglish generic name:', # suffix: プロンプトの例の後に続くテキスト
    input_variables=['input'],                       # input_variables: 入力変数
    example_separator='\n'                         # example_separator: prefix, examples, suffixを結合するときの文字列
)
"""

prompt = PromptTemplate(
    input_variables=["text"],
    template=template,
)

# ※本来は全てのJ一般名をループさせて回答をリストに保存するが、以下のサンプルは1例のみ。
llm = OpenAI(model_name="gpt-3.5-turbo", temperature=0, frequency_penalty=0)
llm((few_shot_prompt.format(input=df['generic_name_jp'].iloc[0])))

Few Shot + Example Selector

# Few Shot + Example Selector

# 例一覧の作成
all_examples = []
for key, value in zip(few_shot['generic_name_jp'], few_shot['generic_name_en']):
    tmp_dict = {'generic_name_jp':key, 'generic_name_en': value}
    all_examples.append(tmp_dict)

# 例をフォーマットするためのテンプレートを作成
example_formatter_template = """
Japanese generic name: {generic_name_jp}
English generic name: {generic_name_en}\n
"""

example_prompt = PromptTemplate(
    input_variables=["generic_name_jp", 'generic_name_en'],
    template=example_formatter_template,
)

# SemanticSimilarityExampleSelectorの準備
example_selector = SemanticSimilarityExampleSelector.from_examples(
    all_examples,       # 例
    OpenAIEmbeddings(), # 埋め込み生成のためのクラス
    FAISS,              # 埋め込みを保存し,類似検索するためのVectorStoreクラス
    k=5                 # 作成する例の数
)

# FewShotPromptTemplateオブジェクトを作成する
prompt_from_string_examples = FewShotPromptTemplate(
    example_selector=example_selector,
    example_prompt=example_prompt,
    prefix='For each Japanese generic name, write the English generic name in lower case.', # prefix: プロンプトの例の前に置かれるテキスト
    suffix='Japanese generic name: {input}\nEnglish generic name:', # suffix: プロンプトの例の後に続くテキスト
    input_variables=['input'],                       # input_variables: 入力変数
    example_separator='\n'                         # example_separator: prefix, examples, suffixを結合するときの文字列
)

# ※本来は全てのJ一般名をループさせて回答をリストに保存するが、以下のサンプルは1例のみ。
llm = OpenAI(model_name="gpt-3.5-turbo", temperature=0, frequency_penalty=0)
llm((prompt_from_string_examples.format(input=target['generic_name_jp'].iloc[0])))

結果

翻訳名とE一般名を名寄せできなかった2,610件のうち、Few Shot + Example Selectorで555件(21.3%)名寄せすることができた。
・・・少なくない?

考察

名寄せできなかったものの中には「存在しないJ一般名」「海外で販売されていない医薬品」も含まれていた。確認したところ、実際に名寄せしたかったけど名寄せできなかったJ一般名は1,109件であった。つまり半数以上(50.0%)をGPT3.5-turboで名寄せできたことになる。

名寄せできなかったJ一般名の傾向としては「配合剤」や「漢字を多く含むもの(例:システアミン酒石酸塩)」といったもの。但し全ての塩(えん)を含むJ一般名が名寄せできなかったというわけではないので、あくまで筆者の感じた傾向、という程度に留めてほしい。

名寄せできたものの中には、実は誤ったJ一般名、例えば商品名が含まれてたケースがあったが、本プロンプトを用いることで商品名であっても正しいE一般名に変換できた場合があった。
例:
一般名(JP):リリカ錠 ※商品名であり、データベースへの入力ミス
単純な翻訳:Lyrica Tablet
Few Shot + Example Selector:pregabalin

プレガバリンに関しては一般名、商品名に関する情報がネット上に多く存在するため、学習データにも含まれておりFew Shotであっても期待通りの翻訳ができた?これについては追加の検証が必要。

課題

  • Promptを吟味できていない。5パターンくらい試して一番筋が良かったものを採用した。
  • 事前に翻訳名とE一般名が突合できた組み合わせ(J一般名×E一般名)をEmbeddingしChromaなどに格納しておくことで、効率的にFew ShotのSelectionができるような気がするが、「J一般名Embedding」と「E一般名Embedding」を紐づけた状態でVector DBに格納する方法がわからなかった。有識者求む!
  • acetaminophenは、海外ではparacetamolと呼ばれることが多く、今回使用した海外DBでもparacetamolで登録されていた。こういったものはドメイン知識がないと名寄せ無理。
  • これだけの名寄せをGPT3.5-turboでやると数時間かかる。それでもコストは1 USD程度。ループ処理やめればいいのだけど、実装優先してこういったコードにした。いい感じの実装アドバイス求む!

参考

脚注
  1. https://www.hitachi-pi.co.jp/column/000072/ ↩︎

Discussion