単語類似度を用いた病名検索

テキスト分類ではコサイン類似度を用いてドキュメントの類似性を求めて文書検索に応用した。これを応用してあいまい病名検索に利用できないか検討してみた。

考え方の背景はテキスト分類の記事とそこに貼り付けてあるPDFのドキュメントを参考にされたい。ただし、病名検索の場合は、テキスト分類における文書が病名で、文書を構成する要素(形態素)が文字になる。例えば、病名「咽喉圧挫損傷」は文字「咽」「喉」「圧」「挫」「損」「傷」から構成され、文字空間上の1点で表現できるものと考え、それを病名ベクトル(厳密には文字空間上のベクトル)と定義し、病名間の類似度を病名ベクトルのコサイン類似度とする考え方をとる。

まず、図1に示す病名データベースの病名表記(F列)を文字に分解する(図2)。

図1.病名データベース

図2.病名表記を単語分解

このシートでID列(A列)は図1の病名データベースの行番号を示す。語列(B列)には病名を構成する文字が1文字ずつ格納される。図1のシートから図2のシートを作成するプログラムを図3に示す。

Option Explicit

'
' 病名を語分解してベクトル表現する
'
Sub 病名を語分解()
Dim ws1 As Worksheet
Dim ws2 As Worksheet
Dim row1 As Long
Dim row2 As Long
Dim str As String
Dim i As Integer

    Set ws1 = Worksheets("19・20章データベース用")
    Set ws2 = Worksheets("分解")
    
    row1 = 2
    row2 = 2
    Do Until ws1.Cells(row1, 6).Value = "" Or row1 > 119
        str = ws1.Cells(row1, 6).Value
        For i = 1 To Len(str)
            ws2.Cells(row2, 1).Value = row1 - 1
            ws2.Cells(row2, 2).Value = Mid(str, i, 1)
            row2 = row2 + 1
        Next
        row1 = row1 + 1
    Loop
    
End Sub

図3.病名を語分解するプログラム

次に、図2のシートをもとにピボットテーブルを作成する(図4)。

図4.ピボットテーブル


 ピボットテーブルの行には「語」を、列には「ID」を、そしてΣ値には「語」の個数を設定する。すると、図3のように1列(A列)目には病名を構成する単語が並び、B列にはIDが1の病名「むちうち損傷」の単語ベクトル(列ベクトル)が、C列にはIDが2の病名「咽喉圧挫損傷」の単語ベクトルが、・・・と各病名の単語ベクトルが生成される。

図4のピボットテーブルの右側に図5に示すような検索キーワードを入力するセルDR2とその隣にActiveXコントロールで検索ボタンを作成する(オブジェクト名はWVCommandButton)。

図5.類似病名検索

 検索ボタンをクリックすると、検索キーワードに入力された文字列から病名ベクトルを生成してDP列に出力する。図6にそのプログラム(検索ボタンWVCommandButtonのクリック時のイベントプロシージャ)を示す。

Option Explicit

'
' 単語ベクトル検索
'
Private Sub WVCommandButton_Click()
Dim query_str As String
Dim search_range As Range
Dim ICD11_code As String
Dim c As String
Dim i As Integer
Dim j As Long

    Set search_range = Worksheets("19・20章データベース用").Range("F2:O128")
    
    query_str = Me.Cells(2, 122).Value
    MsgBox query_str
    
    j = 5
    Do Until Me.Cells(j, 1).Value = ""
        Me.Cells(j, 120).Value = ""
        j = j + 1
    Loop
    
    '単語ベクトルに分解
    For i = 1 To Len(query_str)
        c = Mid(query_str, i, 1)
        Debug.Print i, c
        j = 5
        Do Until Me.Cells(j, 1).Value = ""
            If Me.Cells(j, 1).Value = c Then
                If Me.Cells(j, 120).Value = "" Then
                    Me.Cells(j, 120).Value = 1
                Else
                    Me.Cells(j, 120).Value = Me.Cells(j, 120).Value + 1
                End If
                Exit Do
            End If
            j = j + 1
        Loop
    Next
    ListCommandButton_Click

End Sub

'
' コサイン類似度の降順に結果を並べる
'
Private Sub ListCommandButton_Click()
Dim col As Long
Dim row As Long
Dim search_range As Range

    Set search_range = Worksheets("19・20章データベース用").Range("F2:O128")
    
    row = 5
    Do Until Me.Cells(row, 121).Value = ""
        Me.Cells(row, 121).Value = ""
        Me.Cells(row, 122).Value = ""
        Me.Cells(row, 123).Value = ""
        row = row + 1
    Loop
    
    row = 5
    col = 2
    Do Until Me.Cells(4, col).Value = ""
        Debug.Print Me.Cells(4, col).Value
        If Me.Cells(122, col).Value > 0 Then
            Me.Cells(row, 121).Value = Me.Cells(123, col).Value
            Me.Cells(row, 122).Value = search_range.Cells(col - 1, 1).Value
            Me.Cells(row, 123).Value = search_range.Cells(col - 1, 6).Value
            row = row + 1
        End If
        col = col + 1
    Loop
    Sort_Cos_Similarity
    
End Sub

'
' Sort_Cos_Similarity Macro
'
Private Sub Sort_Cos_Similarity()
    Me.Range("DQ5:DS119").Select
    Me.Sort.SortFields.Clear
    Me.Sort.SortFields.Add2 key:=Range( _
        "DQ5:DQ119"), SortOn:=xlSortOnValues, Order:=xlDescending, DataOption:= _
        xlSortNormal
    With Me.Sort
        .SetRange Range("DQ5:DS119")
        .Header = xlGuess
        .MatchCase = False
        .Orientation = xlTopToBottom
        .SortMethod = xlPinYin
        .Apply
    End With
End Sub

図6.病名を単語ベクトルの類似度の大きいものから順に出力するプログラム

WVCommandButton_Clickは、病名ベクトルを作り終えると関数ListCommandButton_Clickを呼び出す。この関数は、コサイン類似度の降順に病名を並べるプログラムである。

その説明をする前に図7を見てほしい。

図7.コサイン類似度の計算

 これはピボットテーブル図4のピボットテーブルの下端で、121行目に病名ベクトルのノルムを計算する計算式「=SQRT(SUMSQ(B5:B119))」が、122行名には当該病名ベクトルとDP列に出力された検索キーワードの病名ベクトルの内積を計算する式「=SUMPRODUCT(B5:B119,$DP5:$DP119)」が、そして123行名にはそれらから計算されるコサイン類似度の計算式「=B122/B121/$DP121」が入っている(計算式の例示はB列であるが、それがすべての病名について計算されている)。

関数ListCommandButton_Clickは、内積が0でない病名を抽出して、コサイン類似度と対応する病名及びDPC11コードを各々DQ列、DR列、DS列に転記し、最後に関数Sort_Cos_Similarityを使ってコサイン類似度の降順にソートする。こうして類似病名検索されたのが図8である。

図8.コサイン類似度病名検索

 興味深いことに同じような文字を含む病名が上位から順に並んでいる。これらは正規表現による検索では得られないもので、人間に近い感覚で類似病名が検索されていることがわかる。

ライブラリ構築

ここまでは、ピボットテーブルとExcel関数、そしてVBAを補助的に用いて単語類似度に基づく病名検索の考え方を説明しながら病名検索の試作品を作成してきた。しかしながら、実際のアプリケーションではピボットテーブルやExcel関数を利用するわけにはいかない。なぜなら、それらは間に手作業が介入するため自動化すること(ボタンを押せば一発で検索すること)ができないからである。

そこで、ここでは単語類似度に基づく病名検索のライブラリ化を行い、それを用いた病名検索のプロトタイプを作成する。図9に作成するプロトタイプを示す。

図9.ライブラリを用いた単語類似度病名検索

画面の動きは、検索ワード(セルB2)に病名を入力して[検索]ボタンをクリックすると、コンボボックスに類似度の降順に病名がリストされ、それから選択するとICD11コードがセルB3に表示されるようになっている。このプログラム(シートモジュール)を図10に示す。

Option Explicit

'
' 単語類似度検索
'
Dim gDic As CDic
Dim search_range As Range

'
' ワークシートがアクティブになったときgDicを初期化する
'
Private Sub Worksheet_Activate()
    Set gDic = New CDic
    Set search_range = Worksheets("19・20章データベース用").Range("F2:O119")
    gDic.addFromRange search_range
End Sub

'
' 検索ボタンを押されたときの処理
'
Private Sub WVButton_Click()
Dim query_str As String
Dim i As Integer
Dim cbox As ComboBox
    
Dim e() As sortElement
Dim d As CDisease
    
    Set cbox = Me.DiseaseComboBox
    cbox.Clear

    query_str = Me.Cells(2, 2).Value
    Set d = New CDisease
    d.init query_str
    
    'もしもgDicが初期化されていなかったら、初期化する
    If gDic Is Nothing Then
        Worksheet_Activate
    End If

    e = gDic.most_similar(d)
    
    For i = 1 To UBound(e)
        cbox.AddItem e(i).item.name_
    Next

End Sub

Private Sub DiseaseComboBox_Click()
Dim ICD11_code As String
    ICD11_code = Application.WorksheetFunction.VLookup(Me.DiseaseComboBox.Value, search_range, 6, False)
    Me.Cells(3, 2).Value = ICD11_code
End Sub

図10.ライブラリを利用した類似病名検索シートのシートモジュール

このプログラムは、病名クラス(CDisease)と辞書クラス(CDic)を利用している。

病名を表現するクラスCDiseaseの定義を図11に示す。

'-----------------------------------------------
'
' 病名を表現するクラス:CDisease
'
'-----------------------------------------------

Option Explicit

'
' 病名クラス
'
Public name_ As String      '病名
Private ch_ As Dictionary   '病名を構成する文字(key)とその頻度(item)

'
' 初期化
'
Public Sub init(name As String)
Dim i As Integer
Dim c As String
Dim index As Integer
    name_ = name
    Set ch_ = New Dictionary
    For i = 1 To Len(name)
        c = Mid(name, i, 1)
        If ch_.Exists(c) Then
            ch_(c) = ch_(c) + 1
        Else
            ch_(c) = 1
        End If
    Next
End Sub

'
' 病名ベクトルのノルムを返す
'
Public Function norm() As Double
Dim sum_ As Double
Dim c As Variant
    sum_ = 0#
    For Each c In ch_
        sum_ = sum_ + ch_(c) ^ 2
    Next
    norm = Math.Sqr(sum_)
End Function

'
' 当該病名と引数に指定された病名との内積を計算して返す
'
Public Function dot(d As CDisease) As Double
Dim c As Variant
    dot = 0#
    For Each c In ch_
        dot = dot + ch_(c) * d.freq(CStr(c))
    Next
End Function

'
' 当該病名と引数に指定された病名とのコサイン類似度を計算して返す
'
Public Function similarity(d As CDisease) As Double
    similarity = dot(d)
    If similarity = 0 Then Exit Function
    similarity = similarity / norm() / d.norm()
End Function

'
' 病名に含まれている文字の出現頻度を求める
'
Public Function freq(c As String) As Integer
    If ch_.Exists(c) Then
        freq = ch_(c)
    Else
        freq = 0
    End If
End Function

'
' 属性表示
'
Public Function toString() As String
Dim str As String
Dim c As Variant
Dim dlm As String
    str = name_ & "("
    dlm = ""
    For Each c In ch_
        str = str & dlm & c & ":" & ch_(c)
        dlm = ","
    Next
    toString = str & ")"
End Function

図11.病名クラス CDisease

これは病名を文字空間ベクトルモデル(病名を構成する個々の文字が座標軸で、その出現頻度が座標の値となる多次元空間上の1点で病名を表すモデル)で表現するクラスモジュールである。このクラスは、病名を格納する文字列型の属性(name_)と病名を構成する文字とその頻度を格納するDictionary型の属性(ch_)をクラス属性として持っている。また、メソッドとしては、病名を引数にとり、属性name_やch_に格納する初期化メソッドinit、病名ベクトルのノルムを計算して返すnormメソッド、引数に指定された病名との内積を計算して返すdotメソッド、引数に指定された病名とのコサイン類似度を計算して返すsimilarityメソッド、病名に含まれている文字の出現頻度を求めるfreqメソッド、そして、インスタンスを文字列化するtoStringメソッドからなる。

次に、病名の辞書クラスCDicのソースコードを図12に示す。これも、CDiseaseと同様にクラスモジュールで作成されている。

'-----------------------------------------------
'
' 病名を構成する文字を管理する辞書クラス:CDic
'
'-----------------------------------------------

Option Explicit

Private diseases_ As Collection '病名(CDisease)コレクション(keyは病名)

' コンストラクタ
Private Sub Class_Initialize()
    Set diseases_ = New Collection
End Sub

' 病名をRangeから追加
Public Sub addFromRange(r As Range)
Dim i As Integer
Dim disease_name As String
    For i = 1 To r.Rows.Count
        disease_name = r.Cells(i, 1).Value
        add disease_name
    Next
End Sub

' 病名を追加
Public Sub add(str As String)
Dim disease As CDisease
    Set disease = New CDisease
    disease.init str
    diseases_.add disease, str
End Sub

'
' 第1引数に指定された病名(d)とのcos類似度を計算して
' 類似度の降順に並べたsortElement構造体配列を返す
'
Public Function most_similar(d As CDisease) As sortElement()
Dim x As CDisease
Dim i As Integer
Dim row As Integer
Dim similarity As Double
Dim e() As sortElement
Dim length As Integer

    'cos類似度を計算してリスト
    length = 0
    For i = 1 To diseases_.Count
        Set x = diseases_.item(i)
        similarity = x.similarity(d)
        If similarity > 0 Then
            length = length + 1
            ReDim Preserve e(length) As sortElement
            e(length).key = similarity
            Set e(length).item = x
        End If
    Next
    
    'cos類似度の降順にソート(降順)
    qsort_ e, 1, length
    
    most_similar = e

End Function

'クイックソート(e().keyの降順)
Private Sub qsort_(a() As sortElement, iLeft As Variant, iRight As Variant)
Dim i, j As Integer
Dim b As sortElement

    '中央値を取得
    Dim iMid As Variant '中央値
    iMid = a(Int((iLeft + iRight) / 2)).key
    
    i = iLeft '左側の探索用変数
    j = iRight '右側の探索用変数
    
    '中央値から左側と右側の値を入れ替えていく
    Do
        '中央値から左側のループ
        Do While a(i).key > iMid
            '中央値以下の値まで右側に探索していく
            i = i + 1
        Loop
        
        '中央値から右側のループ
        Do While iMid > a(j).key
            '中央値以上の値まで左側に探索していく
            j = j - 1
        Loop
        
        '左側探索と右側探索の位置が交差したら終了
        If i >= j Then Exit Do
        
        'まだ交差していない場合、左側と右側の値を入れ替える
        b = a(i)
        a(i) = a(j)
        a(j) = b
        
        '左側は1つ右からスタート
        i = i + 1
        '右側は1つ左からスタート
        j = j - 1
    Loop
    
    '中央値から左側を入れ替えていく(再帰)
    If iLeft < i - 1 Then
        Call qsort_(a, iLeft, i - 1)
    End If
    
    '中央値から右側を入れ替えていく(再帰)
    If j + 1 < iRight Then
        Call qsort_(a, j + 1, iRight)
    End If
    
End Sub

'
' 第1引数に指定された病名(d)とのcos類似度を計算して
' 2引数に指定されたRange(r)にcos類似度と病名のタプルをリストする
'
Public Sub diseasesIntoRange(d As CDisease, r As Range)
Dim row As Integer
Dim e() As sortElement

    '1引数に指定された病名(d)とのcos類似度の降順のソート
    e = Me.most_similar(d)

    '第2引数に指定されたRange(r)リスト
    row = 1
    Do Until row > UBound(e)
        If row > r.Rows.Count Then Exit Do
        r.Cells(row, 1).Value = e(row).key
        r.Cells(row, 2).Value = e(row).item.name_
        row = row + 1
    Loop
    
    ' 余白は空欄
    Do Until row > r.Rows.Count
        r.Cells(row, 1).Value = ""
        r.Cells(row, 2).Value = ""
        row = row + 1
    Loop

End Sub

図11.病名の辞書クラスCDic

CDicクラスは病名集を表現するオブジェクトである。このクラスの唯一のプライベート変数diseases_はCDiseaseクラスのインスタンスである病名オブジェクトのコレクションである。このクラスには、病名集をExcelのRange型から読み込んでCDiseaseインスタンスを生成してdiseases_コレクションに追加するaddFromRangeメソッドがある。これはこのオブジェクトの初期化メソッドであり、このオブジェクトの他のメソッドを利用する前に必ず一度だけ呼び出さなければならない。たとえば、図10の「ライブラリを利用した類似病名検索シートのシートモジュール」では、ワークシートがアクティブになった際に呼び出されるWorksheet_Activateイベントプロシージャでこの初期化メソッドが呼び出されている。その際、引数に与える病名集は図1「病名データベース」に示したワークシートの黄色い部分である(このRangeの第1列が病名集になっている)。

addメソッドは病名を単体で辞書へ追加するメソッドで、通常はaddFromRangeから呼び出される下請け的なメソッドである。

most_similarメソッドは、引数に指定したCDiseaseオブジェクトの病名インスタンスdに類似した病名をコサイン類似度の降順にリストして返すメソッドである。なお、戻り値は図12に示す構造体の配列になっている(構造体の宣言は標準モジュールで行う)。

Type sortElement
    item As Object
    key As Double
End Type

図12.most_similarメソッドの戻り値(配列)の要素となる構造体

この構造体はObject型の要素itemと実数型(倍精度浮動小数点数)の要素keyからなっており、itemにはCDiseaseクラスのオブジェクトが、keyにはそのインスタンスとdのコサイン類似度が格納されている。図9の[検索]ボタンがクリックされると図10のWVButton_Clickイベントプロシージャが呼び出され、e = gDic.most_similar(d)が実行され、検索ワードに入力された病名dと類似した病名が辞書gDicから類似した順にリストされ、それがコンボボックス(cbox)に追加される。 その結果、図9に示すようなコンボボックスが得られる。

CDicクラスには、引数に指定された病名dに類似した病名をその類似度の高い順にリストして引数に指定されたExcelのRangeへ出力するdiseasesIntoRangeメソッドも実装している。これは、類似病名をコンボボックスではなくExcelのシートに直接表示したい場合に役立つメソッドである。 

最後に図11内にあるqsort_メソッドは図12に示す構造体の配列をkeyの降順にソートするクイックソートである。これは内部でしか使わないのでプライベートメソッドになっている。

図9でコンボボックスから病名を選択すると、図10のDiseaseComboBox_Clickイベントプロシージャが呼び出され、選択された病名(Me.DiseaseComboBox.Value)に対応するICDコードを図1の病名データベースから(Vlookup関数を使って)求め、Excelのシート(セルB3)に出力する。

0 件のコメント:

コメントを投稿

退院サマリーの標準化

 頼んでおいた「退院サマリー標準化の試み」 1) が届いたので読んでみた。この中に「 病院での診療録の質を向上させるために最も有効な方法の1つは、退院サマリーを監査することである 」という記述がある。その理由として「 日常的な診療記録(経過記録;progress note)は、入...