投稿者

インターシステムズジャパン
記事 Toshihiko Minamoto · 9月 30 6m read

d[IA]gnosis: ベクトルデータベースにおける類似度検索と LLM の使用による診断の抽出

この連載記事を終えていなかったことに気付きました!

GIF de Shame On You Meme | Tenor

今日の記事では、フロントエンドから最適なオプションを選択できるように、テキストに最も類似する ICD-10 診断を抽出するプロダクションプロセスについて説明します。

診断の類似度検索:

アプリケーション内で、HL7 で受け取った診断リクエストを示す画面から、医療従事者が入力したテキストに最も近い ICD-10 診断を検索できます。

検索プロセスを高速化するために、HL7 メッセージを取得する際に受信した診断をベクトル化したテキストをデータベースに保存しました。 これを行うために、メッセージから診断コードを抽出し、ベクトルを生成するメソッドにそれを送信する単純な BPL を実装しました。

受信した診断をベクトル化するコードは以下のようになります。

ClassMethod GetEncoding(sentence As %String) As %String [ Language = python ]
{
        import sentence_transformers
        # create the model and form the embeddings
        model = sentence_transformers.SentenceTransformer('/iris-shared/model/')
        embeddings = model.encode(sentence, normalize_embeddings=True).tolist() # Convert search phrase into a vector# convert the embeddings to a stringreturn str(embeddings)
}

こうすることで、診断をベクトル化し、検索するたびにもう一度ベクトル化する必要がなくなります。 このとおり、ダウンロードしたモデルでベクトルを生成するために  sentence_transformer  ライブラリを使用しています。

受信した診断がすべてベクトル化されてデータベースに保存されているため、SELECT クエリを実行するだけで、受信した診断に最も近い ICD-10 診断を抽出できます。

では、ウェブサービスに公開されている、最も類似する 25 件の診断を返すメソッドのコードを見てみましょう。

ClassMethod GetCodeOptions(idRequest As%String) As%Status
{
	set ret = $$$OKtry {
        set sql = 
            "SELECT TOP 25 * FROM (SELECT C.CodeId, C.Description, VECTOR_DOT_PRODUCT(C.VectorDescription, R.VectorDescription) AS Similarity FROM ENCODER_Object.Codes C, ENCODER_Object.CodeRequests R WHERE R.ID = ?) WHERE Similarity > 0.5 ORDER BY Similarity DESC"set statement = ##class(%SQL.Statement).%New()
		$$$ThrowOnError(statement.%Prepare(sql))
        set rs = statement.%Execute(idRequest)

        set array = []
        while rs.%Next() {
            do array.%Push({
                    "CodeId": (rs.%Get("CodeId")),
                    "Description": (rs.%Get("Description")),
                    "Similarity": (rs.%Get("Similarity"))
                })
        }
        set%response.Status = ..#HTTP200OK
        write array.%ToJSON()

    } catch ex {
        set%response.Status = ..#HTTP400BADREQUEST
        return ex.DisplayString()
    }
    quit ret
}

クエリを見てみると、検索を 25 件に制限し、類似度が 0.5 を超える診断に限定しているのがわかります。 類似度計算には、VECTOR_COSINE を使用することも可能ですが、VECTOR_DOT_PRODUCT  メソッドを選択しました。 この場合、今のところこれらに大きな違いは見つかっていません。

検索結果を以下に示します。

LLM を使って診断を特定する

ここまでは、完璧に特定される診断の単純な検索しか行っていませんが、 自由記述形式のテキストから直接診断を特定することは可能でしょうか?

では試してみましょう!

この機能では、選択した LLM モデルに質問を送信する API を提供する Ollama を使用します。 docker-compose.yml ファイルを見ると、Ollama コンテナーの宣言があります。

## llm locally installed  ollama:    build:      context:.      dockerfile:ollama/Dockerfile    container_name:ollama    volumes:    -./ollama/shared:/ollama-shared    ports:      -"11434:11434"

まず、コンテナーデプロイに LLM llama3.2 がダウンロードされることが定義されています。 その理由は? テストでは最も良いパフォーマンスを見せると思ったからです。

これが、コンテナーがデプロイされたときに実行する entrypoint.sh  ファイルのコンテンツとなります。

#!/bin/bash
echo "Starting Ollama server..."
ollama serve &
SERVE_PID=$!

echo "Waiting for Ollama server to be active..."
while ! ollama list | grep -q 'NAME'; do
  sleep 1
done

ollama pull llama3.2

wait $SERVE_PID

Ollama の機能を活用するために、自由形式のテキストを分析する画面を、LLM を使って入力されたテキストから診断を抽出するように変更しました。

また、llama3.2 が診断を直接抽出するために必要なプロンプトを作成するメソッドを追加して、ビジネスプロセスを変更しました。

Method AnalyzeText(text As %String, analysisId As %String, language As %String) As %String [ Language = python ]
{
    import sentence_transformers
    import iris
    import requests
<span class="hljs-keyword">try</span>:
    url = <span class="hljs-string">"http://ollama:11434/api/generate"</span>
    data = {
        <span class="hljs-string">"model"</span>: <span class="hljs-string">"llama3.2"</span>,
        <span class="hljs-string">"prompt"</span>: <span class="hljs-string">"Extrae únicamente los diagnósticos del siguiente texto separándolos por , y sin añadir interpretaciones: "</span>+text,
        <span class="hljs-string">"stream"</span>: <span class="hljs-keyword">False</span>
    }
    response = requests.post(url, json=data)
    analyzedText = response.json()
    
    model = sentence_transformers.SentenceTransformer(<span class="hljs-string">'/iris-shared/model/'</span>)
    phrases = analyzedText[<span class="hljs-string">'response'</span>].split(<span class="hljs-string">","</span>)
    sqlsentence = <span class="hljs-string">""</span>
    <span class="hljs-comment"># iris.cls("Ens.Util.Log").LogInfo("ENCODER.BP.AnalyzeTextProcess", "AnalyzeText", "Starting process")</span>
    <span class="hljs-keyword">for</span> phraseToAnalyze <span class="hljs-keyword">in</span> phrases :
        <span class="hljs-keyword">if</span> phraseToAnalyze != <span class="hljs-string">""</span>:
            embedding = model.encode(phraseToAnalyze, normalize_embeddings=<span class="hljs-keyword">True</span>).tolist()
            sqlsentence = <span class="hljs-string">"INSERT INTO ENCODER_Object.TextMatches (CodeId, Description, Similarity, AnalysisId, RawText) SELECT TOP 50 * FROM (SELECT CodeId, Description, VECTOR_DOT_PRODUCT(VectorDescription, TO_VECTOR('"</span>+str(embedding)+<span class="hljs-string">"', DECIMAL)) AS Similarity, '"</span>+analysisId+<span class="hljs-string">"', '"</span>+phraseToAnalyze+<span class="hljs-string">"' FROM ENCODER_Object.Codes) ORDER BY Similarity DESC"</span>
            stmt = iris.sql.prepare(<span class="hljs-string">"INSERT INTO ENCODER_Object.TextMatches (CodeId, Description, Similarity, AnalysisId, RawText) SELECT TOP 50 * FROM (SELECT CodeId, Description, VECTOR_DOT_PRODUCT(VectorDescription, TO_VECTOR(?, DECIMAL)) AS Similarity, ?, ? FROM ENCODER_Object.Codes) WHERE Similarity &gt; 0.65 ORDER BY Similarity DESC"</span>)                    
            rs = stmt.execute(str(embedding), analysisId, phraseToAnalyze)        
<span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> err:
    iris.cls(<span class="hljs-string">"Ens.Util.Log"</span>).LogInfo(<span class="hljs-string">"ENCODER.BP.AnalyzeTextProcess"</span>, <span class="hljs-string">"AnalyzeText"</span>, repr(err))
    <span class="hljs-keyword">return</span> repr(err)

<span class="hljs-keyword">return</span> <span class="hljs-string">"Success"</span>

}

このプロンプトは非常に単純なものであるため、必要に応じて改善し、調整することも可能ですが、その選択はユーザーにお任せします。 このメソッドはカンマ区切りで LLM レスポンスを取得し、見つかった診断をベクトル化してデータベースに保存します。 以下は結果の例です。

右下の方に、左下のテキストに関して LLM が見つけたものがすべて表示されています。

これで、診断をコーディングするのに役立つ、LLM モデルを使ったアプリケーションが完成しました。 ここで見たように、実装はまったく複雑ではなく、より複雑で包括的なソリューションを構築するための十分な基礎となります。

お読みいただきありがとうございました!