テキスト分類ではコサイン類似度を用いてドキュメントの類似性を求めて文書検索に応用した。これを応用してあいまい病名検索に利用できないか検討してみた。
考え方の背景はテキスト分類の記事とそこに貼り付けてある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 件のコメント:
コメントを投稿