
【実装編】肺のCT画像からCOVID19かどうかの判断は可能か?【機械学習】
2021.12.20
実装編~肺のCT画像からCOVID19か予想できるのか?

今回は、前回に引き続き、肺の CT 画像から、COVID 19か否かを予測する深層学習モデルを、「PyTorch」で実装してみたいと思います。
PyTorch を採用した理由は、「Kaggle」での実装例も多く、公式ドキュメントも充実しているためです。
ちなみに、こちらの記事は「プログラミングで分類に挑戦する」ということが目的で、COVID 19を確実に分類できるわけではありませんので、予めご了承をお願い致します。
【前処理編】をお読みでない方は、まずはこちらからお読みください。
モデルの訓練
モデルの訓練部分は、PyTorch の公式チュートリアルを参考にしています。
【 PyTorch公式:チュートリアル】
https://pytorch.org/tutorials/
二値分類では、出力を2次元にして、「CrossEntropyloss」を用いる人もいるかと思います。
ですが、今回は1次元なので、「BinaryCrossEntropyloss」を用いています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | def train_fn(fold): print(f"### fold: {fold} ###") trn_idx = folds[folds['fold'] != fold].index val_idx = folds[folds['fold'] == fold].index train_dataset = TrainDataset(folds.loc[trn_idx].reset_index(drop=True), folds.loc[trn_idx].reset_index(drop=True)[CFG.target_col], transform1=get_transforms1(data='train'),transform2=to_tensor()) valid_dataset = TrainDataset(folds.loc[val_idx].reset_index(drop=True), folds.loc[val_idx].reset_index(drop=True)[CFG.target_col], transform1=get_transforms1(data='valid'),transform2=to_tensor()) train_loader = DataLoader(train_dataset, batch_size=CFG.batch_size, shuffle=True, num_workers=4) valid_loader = DataLoader(valid_dataset, batch_size=CFG.batch_size, shuffle=False, num_workers=4) model = Efnet_b2_ns(weight_path="/kaggle/input/pytorch-efnet-ns-weights/tf_efficientnet_b2_aa-60c94f97.pth") model.to(device) optimizer = Adam(model.parameters(), lr=CFG.lr, amsgrad=False) #scheduler = ReduceLROnPlateau(optimizer, 'min', factor=0.5, patience=2, verbose=True, eps=1e-6) scheduler = CosineAnnealingLR(optimizer, T_max=20, eta_min=0.001) criterion = nn.BCELoss()#weight = class_weight best_score = -100 best_loss = np.inf best_preds = None for epoch in range(CFG.epochs): start_time = time.time() model.train() avg_loss = 0. optimizer.zero_grad() tk0 = tqdm(enumerate(train_loader), total=len(train_loader)) for i, (images, labels) in tk0: images = images.to(device) labels = labels.to(device) y_preds = model(images.float()) y_preds = torch.sigmoid(y_preds.view(-1)) loss = criterion(y_preds, labels) loss.backward() optimizer.step() optimizer.zero_grad() avg_loss += loss.item() / len(train_loader) model.eval() avg_val_loss = 0. preds = [] valid_labels = [] tk1 = tqdm(enumerate(valid_loader), total=len(valid_loader)) for i, (images, labels) in tk1: images = images.to(device) labels = labels.to(device) with torch.no_grad(): y_preds = model(images.float()) y_preds = torch.sigmoid(y_preds.view(-1)) preds.append(y_preds.to('cpu').numpy()) valid_labels.append(labels.to('cpu').numpy()) loss = criterion(y_preds, labels) avg_val_loss += loss.item() / len(valid_loader) scheduler.step(avg_val_loss) preds = np.concatenate(preds) valid_labels = np.concatenate(valid_labels) score = auc(valid_labels,preds) elapsed = time.time() - start_time print(f' Epoch {epoch+1} - avg_train_loss: {avg_loss:.4f} avg_val_loss: {avg_val_loss:.4f} time: {elapsed:.0f}s') print(f' Epoch {epoch+1} - AUC: {score}') if score>best_score:#aucのスコアが良かったら予測値を更新...best_epochをきめるため best_score = score best_preds = preds print("====",f' Epoch {epoch+1} - Save Best Score: {best_score:.4f}',"===") torch.save(model.state_dict(), f'/kaggle/working/fold{fold}_efnet_b2_ns_.pth')#各epochのモデルを保存。。。best_epoch終了時のモデルを推論に使用 return best_preds, valid_labels |
純正の PyTorch では、人によって書き方にバリエーションがあるため、「各種ラッパーを使った方がいい」という意見もあります。
ですが、一度純正で書いてみることで、「どこで何をやっているか」が理解しやすくなるのです。
たとえば、ラッパーのひとつである「fastai」は、理解がしにくい代表的な例ですね。
1 2 | import fastai from fastai.vision import * |
データ型とsizeに注意!
データ型や size が異なると、損失関数やモデルにテンソルを通すとき、エラーが出ることがあります。
もし、以下のようなエラーが出たら、デバックして「dtype」や「size」を確認してみましょう。
1 | RuntimeError: Expected object of scalar type Long but got scalar type Float for argument #3 'mat1' in call to _th_addmm_ |
検証データに対するAUCを確認
モデルの訓練の関数を実行して、「out-of-fold の予測値」と「正解ラベル」を考慮することで、AUC が計算できます。
1 2 3 4 5 6 7 8 9 | # check your CV score! preds = np.concatenate(preds) valid_labels = np.concatenate(valid_labels) score = auc(valid_labels,preds) import datetime dt_now = datetime.datetime.now() print("現在時刻",dt_now) print("=====AUC(CV)======",score) |
1 2 | 現在時刻 2020-08-12 12:36:38.190845 =====AUC(CV)====== 0.9531154143179866 |
AUC では、高いスコアが出ていることがわかりますね。
test データに対しても、高いスコアが出れば、「今回学習したモデルには汎化性能がある」と言えます。
テストデータに対する予測をみる
先ほど学習させた、各 fold の重みを用いて、推論を実行します。
モデルの部分が少し違うことに注意!
先ほどのモデルのコードと、比較をしてみてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class Efnet_b2_inference(nn.Module): def __init__(self,weight_path): super().__init__() self.weight_path = weight_path self.model = geffnet.tf_efficientnet_b2_ns(pretrained=False) #さいごの部分を付け替え self.model.global_pool=nn.AdaptiveAvgPool2d(1) self.model.classifier = nn.Linear(self.model.classifier.in_features, 1) state_dict = torch.load(self.weight_path,map_location=device) self.model.load_state_dict(fix_model_state_dict(state_dict)) def forward(self, x): x = self.model(x)#ベースのモデルの流れに同じ return x def fix_model_state_dict(state_dict): from collections import OrderedDict new_state_dict = OrderedDict() for k, v in state_dict.items(): name = k if name.startswith('model.'): name = name[6:] # remove 'model.' of dataparallel new_state_dict[name] = v return new_state_dict |
ImageNet で学習させた重みは、出力が1000次元です。
ですが、自分で学習させたモデルは、出力が1次元であることを考慮せねばなりません。
推論を実際に行う部分
各 fold ごとのモデル出力の平均を、最終的な出力とする、単縦なアンサンブルを用いています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | def inference(model, test_loader, device): model.to(device) probs = [] labels = [] for i, (images,label) in tqdm(enumerate(test_loader), total=len(test_loader)): images = images.to(device) with torch.no_grad(): y_preds = model(images) y_preds = torch.sigmoid(y_preds.view(-1)) probs.append(y_preds.to('cpu').numpy()) labels.append(label.numpy()) probs = np.concatenate(probs) labels = np.concatenate(labels) return probs,labels #ensamble your folds' models! def submit(): print('run inference') test_dataset = TestDataset(test_df, transform1=get_transforms1(data='valid'),transform2=to_tensor()) test_loader = DataLoader(test_dataset, batch_size=CFG.batch_size, shuffle=False) probs = [] labels = [] for fold in range(4): weights_path = "/kaggle/working/fold{}_efnet_b2_ns_a_512_augmix_gridmask.pth".format(fold) model = Efnet_b2_inference(weights_path) _probs,_label = inference(model, test_loader, device) probs.append(_probs) labels.append(_label) probs = np.mean(probs, axis=0) return probs |
推論結果のAUCをみてみる
上の関数を実行すれば、AUC が確認できます。
1 2 3 4 | test_df['predict'] = submit() print(test_df.head()) score = auc(test_df['covid'].values[:],test_df['predict']) print("=====AUC(test)======",score) |
これによる結果は、以下のようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 | run inference 100%|██████████| 16/16 [00:01<00:00, 14.93it/s] 100%|██████████| 16/16 [00:00<00:00, 16.77it/s] 100%|██████████| 16/16 [00:00<00:00, 16.90it/s] 100%|██████████| 16/16 [00:00<00:00, 16.72it/s] filename covid predict 0 CODE19 Data/Testing Data/Non Covid/1845.png 0 0.422551 1 CODE19 Data/Testing Data/Non Covid/1501.png 0 0.481845 2 CODE19 Data/Testing Data/Non Covid/1923.png 0 0.624478 3 CODE19 Data/Testing Data/Non Covid/1562.png 0 0.452440 4 CODE19 Data/Testing Data/Non Covid/1952.png 0 0.521156 =====AUC(test)====== 0.5926229508196722 |
さいごに
今回は、Kaggle の環境を用いて画像分類を試してみました。
ただ、CV のスコアより大幅に低下しており、「今回モデルは訓練データに過学習してしまった」ということになります。
パラメーターが多いほど、表現できることは増えますが、少ないデータに対しては、過学習をしてしまうのです。
今回用いた「EfficientNet」は、VGG16 などに比べれば、パラメーターの数は多いです。
ただ、test データは少なく、画像の撮影にばらつきがあるなど、理想的なデータセットだったとは言えません。
また、実は、今回用いたものよりも、大規模なデータセットが既に公開されています。
70 GB ほどで、セグメンテーションマスク付きなので、勉強にはちょうど良いかもしれません。
【コーネル大学:BIMCV COVID-19+ 】
https://arxiv.org/abs/2006.01174
Kaggle公開中
なお、今回のコードは、Kaggleで公開しています。
リンク:肺のCT画像からCOVID19かどうか判断してみた!
実行環境も整っているので、ぜひ試してみてくださいね!
こちらの記事もオススメ!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ライトコードの日常12月 1, 2023ライトコードクエスト〜東京オフィス歴史編〜
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン