ChatGPTで日英医薬品マスター作成の試み
結論
- 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一般名を下記実験条件に沿って再翻訳した。(※実際は穴埋め問題を解かせた)
- Zero Shot
- Few Shot
- 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程度。ループ処理やめればいいのだけど、実装優先してこういったコードにした。いい感じの実装アドバイス求む!
Discussion