糖尿病網膜病変の画像分類における深層学習応用

0 はじめに

本プロジェクトは、機械視覚を活用した卒業研究として、糖尿病網膜病変の画像分類を行うものです。

プロジェクト実行結果:

卒業研究:糖尿病網膜病変予測

プロジェクト入手方法:

https://gitee.com/assistant-a/project-sharing

1 目標設定

この課題では、1000枚の網膜画像データセットを使用し、4つの段階に分類されています。深層学習フレームワークであるPyTorchを用いて、画像から分類を予測します。

2 データ前処理

1. データ分析

統計情報を確認すると、訓練データセットは不均等な分布となっています。画像数も限られているため、簡単な拡張処理を行いました。画像の左右・上下反転を用いてデータ拡張を行い、1つのクラスでは3倍、2・3クラスでは2倍の拡張を実施しました。0クラスの画像については何も行いませんでした。

これは処理後の訓練データセットの分布です。 訓練データと検証データは7:3で分割しています。

2. モデルトレーニング

2.1 モデル準備

使用するモデルはtorchvision.models内の標準モデルと事前学習済みパラメータを利用します。

from torchvision import models as models
# inception_v3,ResNet50
model = models.resnet50(pretrained=True)
# pretrainedをTrueに設定することで事前学習済みパラメータを使用します。

model.fc # 全結合層の入出力サイズを確認
# Linear(in_features=2048, out_features=1000, bias=True)

4分類のため、モデルの出力を以下のように調整します:

model.fc = torch.nn.Linear(in_features=2048, out_features=4, bias=True)
model.aux_logits = False # InceptionV3に必要な設定です。
# 説明は不明ですが、設定しないとエラーになります。

2.2 パラメータ設定

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device=device)
learning_rate = 1e-4
num_epochs = 10
batch_size = 32
optimizer = torch.optim.Adam(model.parameters(), lr = learning_rate)
loss_criterion = torch.nn.CrossEntropyLoss()

2.3 データ読み込みとテンソル変換

データ取得にはDatasetを継承したmydatasetクラスを定義し、画像処理と変換を実装しました。ここでは詳細を省略し、コードのみ示します。

my_transform = transforms.Compose([
    transforms.Resize((299,299)),
    transforms.ToTensor(),
#     transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
# inception_v3は入力が(299*299)のため変換が必要
# resnetの場合は必要ありません

class retinaDataset(Dataset):
    def __init__(self, imagepath=r"D:\course\junior_2\deep_learning\third\train", csv_path=" ",transform=my_transform):
        self.df = pd.read_csv(csv_path)
        
        # if (total is not None):
        #     self.df = self.df[:total]
        self.transform = transform
        self.imagepath = imagepath
        
    def __len__(self):
        return len(self.df)
    
    def __getitem__(self, index):
        img_path = os.path.join(self.imagepath, self.df.iloc[index].image +".png")
        img = Image.open(img_path)
        if(self.transform):
            img = self.transform(img)
        return img, torch.tensor(self.df.iloc[index].Retinopathy_grade)

train_dataset = retinaDataset(csv_path=r"D:\course\junior_2\deep_learning\mythird\train.csv")

train_dataloader = DataLoader(dataset=train_dataset, 
batch_size=batch_size, shuffle=True)
# DataLoaderでデータをバッチに分け、シャッフルします。

2.4 学習開始

for epoch in range(num_epochs):
    for data, target in tqdm(train_dataloader):
        data = data.to(device=device)
        target = target.to(device=device)
        
        score = model(data)
        optimizer.zero_grad()
        
        loss = loss_criterion(score, target)
        loss.backward()
        
        optimizer.step()
    
    print(f"epoch {epoch} の損失: {loss}")

2.5 モデル精度評価

def f_check_accuracy(model_i,model_r, loader):
    model_i.eval() # inception_v3モデル
    model_r.eval() # resnet50モデル
    num0=0
    num1=0
    num2=0
    num3=0
    total0=0
    total1=0
    total2=0
    total3=0
    correct_output = 0
    total_output = 0
    
    with torch.no_grad(): # 勾配計算を無効化しメモリ節約
        for x, y in tqdm(loader):
            x = x.to(device=device)
            y = y.to(device=device)
            
            score_i = model_i(x)
            score_r = model_r(x)
            _,predictions_i = score_i.max(1)
            _,predictions_r = score_r.max(1)
            for i in range (len(y)):
                if(y[i]==0):
                    total0=total0+1
                    if(predictions_i[i]==0):
                        num0=num0+1
                elif(y[i]==1):
                    total1=total1+1
                    if(predictions_r[i]==1):
                        num1=num1+1
                elif(y[i]==2):
                    total2=total2+1
                    if(predictions_r[i]==2):
                        num2=num2+1   
                elif(y[i]==3):
                    total3=total3+1
                    if(predictions_i[i]==3):
                        num3=num3+1 
            correct_output =num0+num1+num2+num3
            total_output =total0+total1+total2+total3
            
    # model.train()
    print("0クラスの精度",num0/total0,"正解数:",num0,"総数:",total0)
    print("1クラスの精度",num1/total1,"正解数:",num1,"総数:",total1)
    print("2クラスの精度",num2/total2,"正解数:",num2,"総数:",total2)
    print("3クラスの精度",num3/total3,"正解数:",num3,"総数:",total3)
    print(f"{total_output}件中、合計正解数: {correct_output}、精度: {float(correct_output/total_output)*100}%")

なぜinception_v3とresnet50を組み合わせたかについて説明します。 それぞれのモデルが異なるクラスにおいて精度が異なるため、両方を統合して精度向上を図りました。 この方法により、約20ポイントの精度向上が見られました。 さらに、Kaggleから追加データをダウンロードし、訓練データを増やすことも可能です。1000枚の画像は4分類には不十分です。

3 データ分析

データセット概要 1. ダウンロードリンク:messidor_features.arff 2. データセットの先頭には記述情報があり、学習には不要なので削除しています。pd.read_csv()で読み込むために簡略化しました。 もちろん削除せずにarffファイル専用の関数を使っても可能です。

from scipy.io import arff
import pandas as pd
df = arff.loadarff('messidor_features.arff') # タプル形式で返る
dataframe = pd.DataFrame(df[0])

3. データセットmessidor_features.arffにはMessidor画像集から抽出された特徴量が含まれており、画像に糖尿病網膜病変があるかどうかを予測するために使用されます。すべての特徴量は検出された病変、解剖部位の特徴、または画像全体の記述子を表します。このデータセットには20の属性があり、最後の属性がラベルです。 4. データセットの基本的な理解後、簡単な探索的分析を行います。まず、data.info()で欠損値やデータ型を確認します。

import pandas as pd

path='E:/Python_file/zuoye/messidor_features.arff'
Cnames = ['x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9',
      'x10', 'x11', 'x12', 'x13', 'x14', 'x15', 'x16', 'x17', 'x18', 'y']
# 不要な情報を削除
data=pd.read_csv(path,header=None,names=Cnames)
print('データセットの基本情報:')
print(data.info())

欠損値がないことが確認できました。ラベル「1」のデータは611件(53.1%)、ラベル「0」のデータは540件(46.9%)で、正例と負例のバランスが取れています。 5. pandas-profilingライブラリを使用して、データセットの分析レポートを自動生成できます。インストールはpip install pandas_profilingで可能です。

import pandas_profiling
report= pandas_profiling.ProfileReport(data)
report.to_file("output_file.html")

実行するとインタラクティブなHTMLファイルが生成され、データ型の検出、一意値や欠損値の計算、分位数統計、記述統計、変数間の相関熱マップなどが表示されます。 このデータセットの属性間の単調相関(Spearmanの順位相関係数ρ)を示すヒートマップを見てみましょう。ρの値は-1から+1の範囲で、-1は完全な負の単調相関、0は無相関、1は完全な正の単調相関を意味します。

三. アプローチ ロジスティック回帰の原理は多くのブログで詳しく解説されていますので、ここではコードのみを紹介します。sklearnライブラリのLogisticRegression()を使うことで、簡単に学習と予測が可能です。

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

X=data[data.columns[0:19]] # 特徴量を抽出
y=data['y']                # ラベル
x_train,x_test,y_train,y_test=train_test_split(X,y,train_size=0.8,random_state=90)

lr=LogisticRegression(max_iter=3000)
clm=lr.fit(x_train,y_train)
print('テストデータの予測結果:')
result=clm.predict(x_test)

LogisticRegression()のパラメータは多くありますが、設定すべきものは限られています。データはバランスが取れているため、class_weightパラメータは不要です。solverパラメータはデフォルトの「liblinear」で十分です。これは2値分類かつ小規模データセットのためです。max_iterはイテレーション回数を指定し、小さすぎると収束前に終了する可能性があります。ここでは3000回に設定しています。

四. 結果とモデル評価・可視化 1. テストデータの結果は図の通りで、「1」は病変あり、「0」はなしを意味します。訓練データとテストデータを分割する際、random_stateの値を変えると異なるテストセットが得られるため、random_state=90に設定しています。 2. モデル評価指標には再現率、適合率、正確率、F値、決定係数R²、ROC曲線のAUCなどがあります。classification_report()で一括評価できます。

from sklearn.metrics import classification_report
print('性能報告:')
print(classification_report(y_test,result))
confusion = metrics.confusion_matrix(y_test, result)

3. FPRを横軸、TPRを縦軸にしたROC曲線を描画し、AUC=0.77を算出しました。

from sklearn.metrics import roc_curve,auc
from matplotlib import pyplot as plt

fpr, tpr, thr = roc_curve(y_test, result, drop_intermediate=False)
fpr, tpr = [0] + list(fpr), [0] + list(tpr)
plt.plot(fpr, tpr)
plt.title('ROC curve for diabetes classifier')
plt.xlabel('False Positive Rate (1 - Specificity)')
plt.ylabel('True Positive Rate (Sensitivity)')
plt.grid(True)
plt.show()
print('AUC:'+ str(auc(fpr,tpr)))

このモデルの検出能力は良好です。 4. ロジスティック回帰モデルから変数の回帰係数を抽出し、重要度を棒グラフにしました。

print('ロジスティック回帰の係数:')
print(clm.coef_)
coef_lr = pd.DataFrame({'var' : x_test.columns,
                        'coef' : clm.coef_.flatten()
                        })

index_sort =  np.abs(coef_lr['coef']).sort_values().index
coef_lr_sort = coef_lr.loc[index_sort,:]

# 水平棒グラフ
fig,ax=plt.subplots()
x, y = coef_lr_sort['var'], coef_lr_sort['coef']
rects = plt.barh(x, y, color='dodgerblue')
plt.grid(linestyle="-.", axis='y', alpha=0.4)
plt.tight_layout()
# 値をラベルに追加
for rect in rects:
    w = rect.get_width()
    ax.text(w, rect.get_y()+rect.get_height()/2,'%.2f' %w,ha='left',va='center')

ロジスティック回帰は線形回帰の結果をシグモイド関数に入力するため、係数は依然として意味を持っています。 糖尿病網膜病変の影響が最も大きい5つの変数はx14,x1,x2,x0,x15であり、これらが主な原因であることが判明しました。

プロジェクト実行結果:

卒業研究:糖尿病網膜病変予測

プロジェクト入手方法:

https://gitee.com/assistant-a/project-sharing

タグ: 糖尿病網膜病変 深層学習 PyTorch 画像分類 機械視覚

7月6日 17:36 投稿