薬剤師のプログラミング学習日記

プログラミングやコンピュータに関する記事を書いていきます

薬歴文書を機械学習でカテゴリ分類してみる

少し前まで自然言語データを用いた様々な機械学習の手法について勉強していました。しばらくはこれについて書いていこうと思います。今回のプログラムでは、次のような入力された文章に対してあらかじめ作成しておいたカテゴリに分類させる、ということをやってみます。
注:下記の入力の文章は私の創作です。

# テスト
>入力:  (O)今回からDPP-4阻害薬追加(A)血糖値はほぼ横ばい状態。低血糖症状とその対処について再確認。(P)血糖値確認
>推定カテゴリ:  ['代謝系に作用する薬剤']

>入力:  (S)便秘は良くなってきてるが、ときどき緩い。(O)改善しているが、下痢になることも。腹痛はない(A)毎晩服用している。頓服であるため様子をみて服用量を調節するよう説明する。
>推定カテゴリ:  ['消化器系に作用する薬剤']

>入力:  (S)最近は高い。(O)160/100、ふらつきを感じることもある
>推定カテゴリ:  ['循環器系に作用する薬剤']

>入力:  持ち込まれた薬は近くの医院で処方されていたが服用していた様子はない。
>推定カテゴリ:  ['持参薬']

>入力:  数日前から発熱あり。
>推定カテゴリ:  ['病原微生物に対する薬剤']

 


機械学習とは

明示的にプログラムすることなく過去のデータからルールやパターンを学習させ、予測や分類を行うことができます。
この『明示的にプログラムすることなく』というのは、例えば猫の画像を判別するような機械学習システムがあった場合、「猫である条件」のようなものを人間がプログラムしてそれを基に判別させるのではない、ということです(そもそも、そんなもの言葉で説明すること自体難しいですよね)。与えられたデータから、何を以って猫というのか、を自動的に獲得します。
幅広くAI(人工知能)と呼ばれる分野の内のひとつで、ショッピングサイトのおすすめやカメラの顔認識、車の自動運転など様々なところで使われています。

カテゴリ分類

機械学習のタスクの一つに文書分類というものがあります。これは書かれている内容から自動でカテゴリに分類するもので、例として以下のようなものがあります。

  • 電子メールを「スパムメール」か「それ以外」かの2つのカテゴリに分類する
  • Web記事を「経済」「エンタメ」「スポーツ」などのカテゴリに分類する

このような分類を行う場合には、「教師あり学習」と「教師なし学習」がありますが、今回は「教師あり学習」のナイーブベイズという技法を使ってやってみます。

教師あり学習

あらかじめ人手で正解ラベルを付けたデータを使い、アルゴリズムにデータと正解ラベルとの関係性を学習させます。そのアルゴリズムを用いて未知のデータに対して回帰(予測)や分類を行います。

プログラムの概要

f:id:enokisaute:20200502184620p:plain

文書データのベクトル化や分類器の学習・予測などプログラムの大部分は機械学習ライブラリのscikit-learn(sklearn)を使います。Anacondaには最初からscikit-learnも含まれているので、Anacondaを使用している人は別途インストールする必要はありません。
他にやることとしてデータの前処理、アルゴリズムの選択や評価などありますが、今回は細かいことは抜きにして、全体的な流れを簡単に書こうと思います。

文書をラベル付けして教師データを作成する

用いる教師データは個人情報に関わる記述や日付などの記載がない例文データです。
一口にラベル付けと言っても、以下のようにいろいろな分類の仕方があります。

  • 疾患(病気)ごとで分ける
  • 書かれている薬で分ける
  • ”副作用"、"アドヒアランス"などのトピックで分ける

などありますが、カテゴリ分けの基準は明確にしておく必要があります。例えば、
NSAIDsの胃腸障害について書かれていれば、内容的には「消化器系」に近くなったりすることもあり、この基準が曖昧だと「これは何だろう」と迷ってしまいます。
基準を明確にしていても、中には判断が難しいというようなデータもありますが、そのようなものは教師データとしては使いません。その筋の専門家である人間が見てはっきりと区別できないようなものは、いくら機械学習を使っても区別できるようにはならないからです(Coursera Machine Learning「Machine Learning System Design」より)。

今回は「今日の治療薬」の主要目次(『病原微生物に対する薬剤』など)13種類+新たに2種類のカテゴリを作成し、計15種類のカテゴリとしました。文書数は各カテゴリ10~数百程度です。

文書のベクトル化

データとして与える文書をプログラムで処理できるようにするために、ベクトル(一定の長さの数値の配列)に変換します。
ここでは、文書ごとの単語の出現回数をリストにするBag of Words(BoW)という手法を用います。単語が文書に含まれているかどうかだけを考え、語順は考慮しないという単純な方法ですが、機械学習では広く用いられているそうです。
Bag of WordsをPythonで書いてみる - 薬剤師のプログラミング学習日記

sklearnではCountVectorizerで簡単に求めることができます。

 
from sklearn.feature_extraction.text import CountVectorizer
 
text = ['副作用 と は 主 作用 で は ない 作用 です']     # 分かち書きされた文書
 
vectorizer = CountVectorizer()
vectorizer.fit(text)    # textの分割と語彙の構築
bag_of_words = vectorizer.transform(text)   # BoWを求める
 
print('語彙の内容: ', vectorizer.vocabulary_)
# 語彙の内容:  {'副作用': 3, '作用': 2, 'ない': 1, 'です': 0}
print('語彙の数: ', len(vectorizer.vocabulary_))
# 語彙の数:  4
print(bag_of_words.toarray())
# [[1 1 2 1]]  numpy配列に変換. 文書数×語彙数(この場合は1x4)の行列となる

語彙の中に「と」や「主」などが入っていませんが、これはデフォルトでは一文字のトークン(字句)が除外されているためです。実際のプログラムでは一文字でも必要なトークンはきちんと拾うようにします。
今回のプログラムでは、CountVectorizerのコンストラクタで分かち書きする関数text_to_wordsを指定します。またfit(), transform()には文書を要素としたリストをそのまま渡しています。
なお、分かち書きにはMeCabを使用しました。Pythonで使うにはインストールの必要がありますが、こちらの記事に詳しく書きました。
www.yakupro.info


分類器の学習と予測

学習

from sklearn.naive_bayes import MultinomialNB

clf = MultinomialNB()   # 多項モデルによるナイーブベイズ分類器を生成
clf = clf.fit(bag_of_words, label)        # 訓練データで学習
print(clf.score(bag_of_words, label))     # 訓練データにおける正解率
# 0.9405137449301487

予測(分類)

vec = vectorizer.transform([txt])      # 先に求めた辞書によるBoWを生成
print('>推定カテゴリ: ', clf.predict(vec))

sklearnではどのアルゴリズムもインスタンスの生成後はfit()で学習、score()でモデルの評価、predict()で予測、と共通したメソッド名になっているようです。
今回は学習時に取得したデータ全てを使ってモデルを学習した結果、その(訓練)データにおける正解率は94%となりましたが、この数字をモデルの評価に使うことはできません。本来は得られたデータを訓練データとテストデータに分割するなどした後、(未知の)テストデータに対してうまく機能するかで評価します。

やってみた感想

今回カテゴリは書かれている薬で分類することにしましたが、テストの結果を見ると薬の名前が含まれていない文章やSOAP形式で書かれていない文章に対しても、それらしいカテゴリに分類できているのが面白いです。一方、どうしてこれがこのカテゴリになるのか、というものもあり、まだまだ改善の余地は多そうです。
また、データを使える形に用意するまでが大変で、後はライブラリを使えば結構手軽にできるということを実感しました。今後はもうちょっと掘り下げてやっていこうと思います。

最後に、コードについて。コードを試してみたいという方は下図のような形でデータを配置して適切にSEARCH_DIRにパスを設定(read_fileのencodingも適宜変更)してやるか、
f:id:enokisaute:20200502162012p:plain
取りあえず動かしてみるだけならtextsとlabelに適当な文章とラベルを貼り付けると動かすことはできますが、データが少ないとたいした精度は出ません。

texts = ['textA', 'textB', 'textC', ...]
label = ['カテゴリA', 'カテゴリB', 'カテゴリC', ...]

いずれのやり方にしても、MeCabを入れていない方はインストールが必要になります。

import os
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
import MeCab
 
# ラベル付けした文書データでナイーブベイズを学習する
 
# ディレクトリを走査してファイルパスとそれに対応するカテゴリ名を返す
def get_data(dir):
    for root, dirs, files in os.walk(dir):
        for file in files:
            yield os.path.join(root, file), root.split('\\')[-1]
 
def read_file(f_path):
    with open(f_path, 'r', encoding='utf-8_sig') as f:
        return f.read()
 
# 分かち書きして単語のリストを返す
def text_to_words(text):
    tagger = MeCab.Tagger('-O chasen')
    chunks = tagger.parse(text).splitlines()
    words = []
    for chunk in chunks:
        line = chunk.split('\t')
        if len(line) > 3:
            if line[0] != '<DATE>' and line[3] != '名詞-数':  # 日付はあらかじめ<DATE>に置換しておいた
                words.append(line[2])
    return words
 
 
if __name__ == '__main__':
    SEARCH_DIR = 'C:\\Users\\***\\Desktop\\data'    # 訓練データのパス. 場所に合わせて設定する
 
    texts = []
    label = []
 
    # 文書データとラベルを収集
    for f_path, dir_name in get_data(SEARCH_DIR):
        text = read_file(f_path)
        texts.append(text)
        label.append(dir_name)
    '''
    texts = ['textA', 'textB', 'textC', ...]
    label = ['カテゴリA', 'カテゴリB', 'カテゴリC', ...]
    '''
    # lowercase:小文字に変換するか(Falseだとしない), min_df: (intの場合)この数字未満の文書に出現するトークンは無視する
    vectorizer = CountVectorizer(tokenizer=text_to_words, lowercase=False, min_df=3)
    # 辞書を作成してBoWを求める
    vectorizer.fit(texts)
    bag_of_words = vectorizer.transform(texts)
 
    print('語彙の内容: ', vectorizer.vocabulary_)
    print('語彙の数: ', len(vectorizer.vocabulary_))
 
    clf = MultinomialNB()   # 多項モデルによるナイーブベイズ分類器を生成
    clf = clf.fit(bag_of_words, label)    # 訓練データで学習
    print('正解率: ', clf.score(bag_of_words, label))    # 訓練データにおける正解率
 
    # テスト
    text1 = '(O)今回からDPP-4阻害薬追加(A)血糖値はほぼ横ばい状態。低血糖症状とその対処について再確認。(P)血糖値確認'
    text2 = '(S)便秘は良くなってきてるが、ときどき緩い。(O)改善しているが、下痢になることも。腹痛はない(A)毎晩服用している。頓服であるため様子をみて服用量を調節するよう説明する。'
    text3 = '(S)最近は高い。(O)160/100、ふらつきを感じることもある'
    text4 = '持ち込まれた薬は近くの医院で処方されていたが服用していた様子はない。'
    text5 = '数日前から発熱あり。'
 
    test_texts = [text1, text2, text3, text4, text5]
 
    for txt in test_texts:
        vec = vectorizer.transform([txt])      # 先に求めた辞書によるBoWを生成
        print('>入力: ', txt)
        print('>推定カテゴリ: ', clf.predict(vec))
        print()

参考文献