diff --git a/ebus_classify.ipynb b/ebus_classify.ipynb new file mode 100644 index 0000000..faafe57 --- /dev/null +++ b/ebus_classify.ipynb @@ -0,0 +1,446 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import glob\r\n", + "import os.path as osp\r\n", + "import random\r\n", + "import numpy as np\r\n", + "import json\r\n", + "from PIL import Image\r\n", + "from tqdm import tqdm\r\n", + "import matplotlib.pyplot as plt\r\n", + "%matplotlib inline\r\n", + "import csv\r\n", + "\r\n", + "import torch\r\n", + "import torch.nn as nn\r\n", + "import torch.optim as optim\r\n", + "import torch.utils.data as data\r\n", + "import torchvision\r\n", + "from torchvision import models, transforms\r\n", + "#乱数のシードを設定\r\n", + "torch.manual_seed(1235)\r\n", + "np.random.seed(1235)\r\n", + "random.seed(1235)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 入力画像の前処理クラス\n", + "# 訓練時と推論時で処理を変える\n", + "class ImageTransform():\n", + " \"\"\"\n", + " 画像の前処理クラス.訓練時と推論時で処理が異なる.\n", + " データ前処理:画像のリサイズ,色の標準化.\n", + " 訓練時データ拡張:RandomResezedCropとRandomHorizontalFlip\n", + " \n", + " Attributes\n", + " ----------\n", + " resize : int\n", + " リサイズの大きさ\n", + " mean : (R, G, B)\n", + " 各色チャネルの平均値\n", + " std : (R, G, B)\n", + " 各色チャネルの標準偏差\n", + " \"\"\"\n", + "\n", + " def __init__(self, resize, mean, std):\n", + " self.data_transform = {\n", + " 'train': transforms.Compose([\n", + " transforms.RandomResizedCrop(\n", + " resize, scale=(0.5, 1.0)),\n", + " transforms.RandomHorizontalFlip(),\n", + " transforms.ToTensor(), # Torchテンソルに変換\n", + " transforms.Normalize(mean, std) # 色の標準化\n", + " ]),\n", + " 'val': transforms.Compose([\n", + " transforms.Resize(resize),\n", + " transforms.CenterCrop(resize),\n", + " transforms.ToTensor(), # Torchテンソルに変換\n", + " transforms.Normalize(mean, std) # 色の標準化\n", + " ]),\n", + " }\n", + " \n", + " def __call__(self, img, phase='train'):\n", + " \"\"\"\n", + " Parameters\n", + " ----------\n", + " phase : 'train' or 'val'\n", + " 前処理のモード指定\n", + " \"\"\" \n", + " return self.data_transform[phase](img)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 訓練時の画像前処理の動作確認\r\n", + "# 実行するたびに処理結果画像が変わる\r\n", + "\r\n", + "# # 1. 画像読み込み\r\n", + "# image_file_path = './data/goldenretriever-3724972_640.jpg'\r\n", + "# img = Image.open(image_file_path) # [高さ][幅][チャネル]\r\n", + "\r\n", + "# # 2. 元画像の表示\r\n", + "# plt.imshow(img)\r\n", + "# plt.show()\r\n", + "\r\n", + "# 3. 画像の前処理\r\n", + "size = 224\r\n", + "mean = (0.485, 0.456, 0.406)\r\n", + "std = (0.229, 0.224, 0.225)\r\n", + "# transform = ImageTransform(size, mean, std)\r\n", + "# img_transformed = transform(img, phase=\"train\")\r\n", + "\r\n", + "# 4. (色,高さ,幅)を(高さ,幅,色)に変換し,0-1にクリップして表示\r\n", + "# img_transformed = img_transformed.numpy().transpose((1, 2, 0))\r\n", + "# img_transformed = np.clip(img_transformed, 0, 1)\r\n", + "# plt.imshow(img_transformed)\r\n", + "# plt.show()\r\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# アリと蜂の画像へのファイルパスのリストを作成\r\n", + "def make_datapath_list(phase=\"train\"):\r\n", + " \"\"\"\r\n", + " データのパスを格納したリストを作成\r\n", + "\r\n", + " Parameters\r\n", + " ----------\r\n", + " phase : 'train' or 'val'\r\n", + " 訓練データか,検証データかを指定\r\n", + " \r\n", + " Returns\r\n", + " -------\r\n", + " path_list : list\r\n", + " データへのパスを格納したリスト\r\n", + " \"\"\"\r\n", + "\r\n", + " rootpath = \"./data/ebus_data/\"\r\n", + " target_path = osp.join(rootpath + phase + '/**/*.jpg')\r\n", + " print(target_path)\r\n", + "\r\n", + " path_list = []\r\n", + "\r\n", + " # globを利用してサブディレクトリまでファイルパスを取得\r\n", + " for path in glob.glob(target_path):\r\n", + " path_list.append(path)\r\n", + " \r\n", + " return path_list\r\n", + "\r\n", + "# 実行\r\n", + "train_list = make_datapath_list(phase=\"train\")\r\n", + "val_list = make_datapath_list(phase=\"val\")\r\n", + "\r\n", + "train_list" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# EBUS画像のDatasetを作成する\r\n", + "\r\n", + "class EbusDataset(data.Dataset):\r\n", + " \"\"\"\r\n", + " EBUS画像のDatasetクラス.PyTorchのDatasetクラスを継承\r\n", + "\r\n", + " Attributes\r\n", + " ----------\r\n", + " file_list : list\r\n", + " 画像のパスを格納したリスト\r\n", + " transform : object\r\n", + " 前処理クラスのインスタンス\r\n", + " phase : 'train' or 'val'\r\n", + " 訓練か検証かを設定する\r\n", + " \"\"\"\r\n", + "\r\n", + " def __init__(self, file_list, transform=None, phase='train'):\r\n", + " self.file_list = file_list\r\n", + " self.transform = transform\r\n", + " self.phase = phase\r\n", + " \r\n", + " def __len__(self):\r\n", + " '''画像の枚数を返す'''\r\n", + " return len(self.file_list)\r\n", + " \r\n", + " def __getitem__(self, index):\r\n", + " '''\r\n", + " 前処理した画像のTensor形式のデータとラベルを取得\r\n", + " '''\r\n", + "\r\n", + " # index番目の画像をロード\r\n", + " img_path = self.file_list[index]\r\n", + " img = Image.open(img_path)\r\n", + "\r\n", + " # 画像の前処理を実施\r\n", + " img_transformed = self.transform(\r\n", + " img, self.phase)\r\n", + " \r\n", + " if self.phase == \"train\":\r\n", + " label = img_path[23:29]\r\n", + " elif self.phase == \"val\":\r\n", + " label = img_path[21:27]\r\n", + " \r\n", + " # ラベルを数値に変更\r\n", + " if label == \"Benign\":\r\n", + " label = 0\r\n", + " elif label == \"Malign\":\r\n", + " label = 1\r\n", + " \r\n", + " return img_transformed, label\r\n", + " \r\n", + "# 実行\r\n", + "train_dataset = EbusDataset(\r\n", + " file_list=train_list, transform=ImageTransform(size, mean, std), phase='train')\r\n", + "\r\n", + "val_dataset = EbusDataset(\r\n", + " file_list=val_list, transform=ImageTransform(size, mean, std), phase='val')\r\n", + "\r\n", + "# 動作確認\r\n", + "index = 0\r\n", + "print(train_dataset.__getitem__(index)[0].size())\r\n", + "print(train_dataset.__getitem__(index)[1])\r\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ミニバッチのサイズを指定\r\n", + "batch_size = 16\r\n", + "\r\n", + "# DataLoaderを作成\r\n", + "train_dataloader = torch.utils.data.DataLoader(\r\n", + " train_dataset, batch_size=batch_size, shuffle=True)\r\n", + "\r\n", + "val_dataloader = torch.utils.data.DataLoader(\r\n", + " val_dataset, batch_size=batch_size, shuffle=False)\r\n", + "\r\n", + "# 辞書型変数にまとめる\r\n", + "dataloaders_dict = {\"train\": train_dataloader, \"val\": val_dataloader}\r\n", + "\r\n", + "# 動作確認\r\n", + "batch_iterator = iter(dataloaders_dict[\"train\"]) # イテレータに変換\r\n", + "inputs, labels = next(batch_iterator) # 最初の要素を取り出す\r\n", + "print(inputs.size())\r\n", + "print(labels)\r\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 学習済みのVGG16モデルをロード\n", + "# VGG16モデルのインスタンスを生成\n", + "use_pretrained = True\n", + "net = models.vgg16(pretrained=use_pretrained)\n", + "\n", + "# VGG16の最後の出力層の出力ユニットをアリと蜂の2つに付け替える\n", + "net.classifier[6] = nn.Linear(in_features=4096, out_features=2)\n", + "\n", + "# 訓練モードに設定\n", + "net.train()\n", + "\n", + "print('ネットワーク設定完了:学習済みの重みをロードし,訓練モードに設定しました')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 損失関数の設定\n", + "criterion = nn.CrossEntropyLoss()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 転移学習で学習させるパラメータを,変数params_to_updateに格納\n", + "params_to_update = []\n", + "\n", + "# 学習させるパラメータ名\n", + "update_param_names = [\"classifier.6.weight\", \"classifier.6.bias\"]\n", + "\n", + "# 学習させるパラメータ以外は勾配計算せず固定\n", + "for name, param in net.named_parameters():\n", + " if name in update_param_names:\n", + " param.requires_grad = True\n", + " params_to_update.append(param)\n", + " print(name)\n", + " else:\n", + " param.requires_grad = False\n", + "\n", + "# params_to_updateの中身を確認\n", + "print(\"------------\")\n", + "print(params_to_update)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 最適化手法の設定\r\n", + "optimizer = optim.SGD(params=params_to_update, lr=0.001, momentum=0.9)\r\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# モデルを学習させる関数\r\n", + "\r\n", + "def train_model(net, dataloaders_dict, criterion, optimizer, num_epochs):\r\n", + "\r\n", + " # 初期設定\r\n", + " # GPU確認\r\n", + " device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\r\n", + " # device = torch.device(\"cpu\")\r\n", + " print(\"使用デバイス:\", device)\r\n", + "\r\n", + " # ネットワークをGPUへ\r\n", + " net.to(device)\r\n", + "\r\n", + " # ネットワークがある程度固定なら高速化\r\n", + " torch.backends.cudnn.benchmark = True\r\n", + "\r\n", + " # ログ出力\r\n", + " f = open('result.csv', 'w')\r\n", + " writer = csv.writer(f, lineterminator='\\n')\r\n", + " writer.writerow(['epoch', 'train_loss', 'train_acc', 'val_loss', 'val_acc'])\r\n", + " \r\n", + " # epochのループ\r\n", + " for epoch in range(num_epochs):\r\n", + " csvitems = []\r\n", + " csvitems.append(epoch+1)\r\n", + " print('')\r\n", + " print('Epoch {}/{}'.format(epoch+1, num_epochs))\r\n", + " print('---------------')\r\n", + "\r\n", + " # epochごとの学習と検証のループ\r\n", + " for phase in ['train', 'val']:\r\n", + " if phase == 'train':\r\n", + " net.train()\r\n", + " else:\r\n", + " net.eval()\r\n", + " \r\n", + " epoch_loss = 0.0 # epochの損失和\r\n", + " epoch_corrects = 0 # epochの正解数\r\n", + "\r\n", + " # 未学習時の性能を確かめるため epoch=0 の訓練は省略\r\n", + " if (epoch == 0) and (phase == 'train'):\r\n", + " csvitems.append(0)\r\n", + " csvitems.append(0)\r\n", + " continue\r\n", + "\r\n", + " # データローダーからミニバッチを取り出すループ\r\n", + " for inputs, labels in tqdm(dataloaders_dict[phase]):\r\n", + "\r\n", + " # GPUが使えるならGPUへデータ転送\r\n", + " inputs = inputs.to(device)\r\n", + " labels = labels.to(device)\r\n", + "\r\n", + " # optimizerを初期化\r\n", + " optimizer.zero_grad()\r\n", + "\r\n", + " # 順伝搬(forward)計算\r\n", + " with torch.set_grad_enabled(phase == 'train'):\r\n", + " outputs = net(inputs)\r\n", + " loss = criterion(outputs, labels)\r\n", + " _, preds = torch.max(outputs, 1)\r\n", + "\r\n", + " # 訓練時はバックプロパゲーション\r\n", + " if phase == 'train':\r\n", + " loss.backward()\r\n", + " optimizer.step()\r\n", + " \r\n", + " # イテレーション結果の計算\r\n", + " # lossの合計を更新\r\n", + " epoch_loss += loss.item() * inputs.size(0)\r\n", + " epoch_corrects += torch.sum(preds == labels.data)\r\n", + " \r\n", + " # epochごとのlossと正解率を表示\r\n", + " epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset)\r\n", + " epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset)\r\n", + " \r\n", + " print('')\r\n", + " print('{} Loss: {:.4f} Acc: {:.4f}'.format(\r\n", + " phase, epoch_loss, epoch_acc))\r\n", + " csvitems.append('{:.4f}'.format(epoch_loss))\r\n", + " csvitems.append('{:.4f}'.format(epoch_acc))\r\n", + " \r\n", + " writer.writerow(csvitems)\r\n", + " \r\n", + " f.close()\r\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 学習・検証を実行する\r\n", + "num_epochs = 30\r\n", + "train_model(net, dataloaders_dict, criterion, optimizer, num_epochs=num_epochs)\r\n", + "print('done.')\r\n" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "ac59ebe37160ed0dfa835113d9b8498d9f09ceb179beaac4002f036b9467c963" + }, + "kernelspec": { + "display_name": "Python 3.9.5 64-bit", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.5" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/ebus_classify.py b/ebus_classify.py new file mode 100644 index 0000000..de2b698 --- /dev/null +++ b/ebus_classify.py @@ -0,0 +1,310 @@ +import glob +import os.path as osp +import random +import numpy as np +from PIL import Image +from tqdm import tqdm +import matplotlib.pyplot as plt +import csv +import torch +import torch.nn as nn +import torch.optim as optim +import torch.utils.data as data +import torchvision +from torchvision import models, transforms +import sys + +# 入力画像の前処理クラス +# 訓練時と推論時で処理を変える +class ImageTransform(): + """ + 画像の前処理クラス.訓練時と推論時で処理が異なる. + データ前処理:画像のリサイズ,色の標準化. + 訓練時データ拡張:RandomResezedCropとRandomHorizontalFlip + + Attributes + ---------- + resize : int + リサイズの大きさ + mean : (R, G, B) + 各色チャネルの平均値 + std : (R, G, B) + 各色チャネルの標準偏差 + """ + + def __init__(self, resize, mean, std): + self.data_transform = { + 'train': transforms.Compose([ + transforms.RandomResizedCrop( + resize, scale=(0.5, 1.0)), + transforms.RandomHorizontalFlip(), + transforms.ToTensor(), # Torchテンソルに変換 + transforms.Normalize(mean, std) # 色の標準化 + ]), + 'val': transforms.Compose([ + transforms.Resize(resize), + transforms.CenterCrop(resize), + transforms.ToTensor(), # Torchテンソルに変換 + transforms.Normalize(mean, std) # 色の標準化 + ]), + } + + def __call__(self, img, phase='train'): + """ + Parameters + ---------- + phase : 'train' or 'val' + 前処理のモード指定 + """ + return self.data_transform[phase](img) + + +# 画像へのファイルパスのリストを作成 +def make_datapath_list(phase="train"): + """ + データのパスを格納したリストを作成 + + Parameters + ---------- + phase : 'train' or 'val' + 訓練データか,検証データかを指定 + + Returns + ------- + path_list : list + データへのパスを格納したリスト + """ + + rootpath = "./data/ebus_data_v5/" + target_path = osp.join(rootpath + phase + '/**/*.bmp') + # print(target_path) + + path_list = [] + + # globを利用してサブディレクトリまでファイルパスを取得 + for path in glob.glob(target_path): + path_list.append(path) + + return path_list + + +# EBUS画像のDatasetを作成する +class EbusDataset(data.Dataset): + """ + EBUS画像のDatasetクラス.PyTorchのDatasetクラスを継承 + + Attributes + ---------- + file_list : list + 画像のパスを格納したリスト + transform : object + 前処理クラスのインスタンス + phase : 'train' or 'val' + 訓練か検証かを設定する + """ + + def __init__(self, file_list, transform=None, phase='train'): + self.file_list = file_list + self.transform = transform + self.phase = phase + + def __len__(self): + '''画像の枚数を返す''' + return len(self.file_list) + + def __getitem__(self, index): + ''' + 前処理した画像のTensor形式のデータとラベルを取得 + ''' + + # index番目の画像をロード + img_path = self.file_list[index] + img = Image.open(img_path) + + # 画像の前処理を実施 + img_transformed = self.transform(img, self.phase) + + if self.phase == "train": + label = img_path[26:32] + elif self.phase == "val": + label = img_path[24:30] + + # ラベルを数値に変更 + if label == "Benign": + label = 0 + elif label == "Malign": + label = 1 + + return img_transformed, label + + +# モデルを学習させる関数 +def train_model(net, dataloaders_dict, criterion, optimizer, num_epochs): + + # 初期設定 + # GPU確認 + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + # device = torch.device("cpu") + print("使用デバイス:", device) + + # ネットワークをGPUへ + net.to(device) + + # ネットワークがある程度固定なら高速化 + torch.backends.cudnn.benchmark = True + + # ログ出力 + f = open('result.csv', 'w') + writer = csv.writer(f, lineterminator='\n') + writer.writerow(['epoch', 'train_loss', 'train_acc', 'val_loss', 'val_acc']) + + # epochのループ + for epoch in range(num_epochs): + csvitems = [] + csvitems.append(epoch+1) + print('') + print('Epoch {}/{}'.format(epoch+1, num_epochs)) + + # epochごとの学習と検証のループ + for phase in ['train', 'val']: + if phase == 'train': + net.train() + else: + net.eval() + + epoch_loss = 0.0 # epochの損失和 + epoch_corrects = 0 # epochの正解数 + + # 未学習時の性能を確かめるため epoch=0 の訓練は省略 + if (epoch == 0) and (phase == 'train'): + csvitems.append(0) + csvitems.append(0) + continue + + # データローダーからミニバッチを取り出すループ + for inputs, labels in tqdm(dataloaders_dict[phase]): + + # GPUが使えるならGPUへデータ転送 + inputs = inputs.to(device) + labels = labels.to(device) + + # optimizerを初期化 + optimizer.zero_grad() + + # 順伝搬(forward)計算 + with torch.set_grad_enabled(phase == 'train'): + outputs = net(inputs) + loss = criterion(outputs, labels) + _, preds = torch.max(outputs, 1) + + # 訓練時はバックプロパゲーション + if phase == 'train': + loss.backward() + optimizer.step() + + # イテレーション結果の計算 + # lossの合計を更新 + epoch_loss += loss.item() * inputs.size(0) + epoch_corrects += torch.sum(preds == labels.data) + + # epochごとのlossと正解率を表示 + epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset) + epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset) + + print('{} Loss: {:.4f} Acc: {:.4f}'.format( + phase, epoch_loss, epoch_acc)) + csvitems.append('{:.4f}'.format(epoch_loss)) + csvitems.append('{:.4f}'.format(epoch_acc)) + + writer.writerow(csvitems) + + f.close() + + +if __name__ == "__main__": + + # 乱数のシードを設定 + torch.manual_seed(1235) + np.random.seed(1235) + random.seed(1235) + + # モデル入力画像の仕様 + size = 224 + mean = (0.485, 0.456, 0.406) + std = (0.229, 0.224, 0.225) + + # データセットファイル名の取得 + train_list = make_datapath_list(phase="train") + val_list = make_datapath_list(phase="val") + # print(train_list) + + # データセットの読み込み + train_dataset = EbusDataset( + file_list=train_list, transform=ImageTransform(size, mean, std), phase='train') + val_dataset = EbusDataset( + file_list=val_list, transform=ImageTransform(size, mean, std), phase='val') + + # ミニバッチのサイズを指定 + batch_size = 16 + + # DataLoaderを作成 + train_dataloader = torch.utils.data.DataLoader( + train_dataset, batch_size=batch_size, shuffle=True) + val_dataloader = torch.utils.data.DataLoader( + val_dataset, batch_size=batch_size, shuffle=False) + + # 辞書型変数にまとめる + dataloaders_dict = {"train": train_dataloader, "val": val_dataloader} + + use_pretrained = True + # ResNet18 + net = models.resnet18(pretrained=use_pretrained) + net.fc = nn.Linear(in_features=512, out_features=2, bias=True) + # net = models.resnet101(pretrained=use_pretrained) + # net.fc = nn.Linear(in_features=2048, out_features=2, bias=True) + # # print(net) + # # sys.exit() + update_param_names = ["fc.weight", "fc.bias"] # 学習させるパラメータ名 + + # VGG16 + # net = models.vgg16(pretrained=use_pretrained) + # net.classifier[6] = nn.Linear(in_features=4096, out_features=2) + # update_param_names = ["classifier.3.weight", "classifier.3.bias", "classifier.6.weight", "classifier.6.bias"] + # update_param_names = ["classifier.6.weight", "classifier.6.bias"] + + # 訓練モードに設定 + net.train() + + print('ネットワーク設定完了:学習済みの重みをロードし,訓練モードに設定しました') + + # 損失関数の設定 + criterion = nn.CrossEntropyLoss() + + # 転移学習で学習させるパラメータを,変数params_to_updateに格納 + params_to_update = [] + + # 学習させるパラメータ名 + + # 学習させるパラメータ以外は勾配計算せず固定 + for name, param in net.named_parameters(): + if True: + # if name.startswith('classifier'): + # if name in update_param_names: + param.requires_grad = True + params_to_update.append(param) + print("update: ", name) + else: + param.requires_grad = False + print("freeze: ", name) + + # params_to_updateの中身を確認 + print("------------") + # print(params_to_update) + + # 最適化手法の設定 + optimizer = optim.SGD(params=params_to_update, lr=0.001, momentum=0.9) + + # 学習・検証を実行する + num_epochs = 50 + train_model(net, dataloaders_dict, criterion, optimizer, num_epochs=num_epochs) + print('done.') diff --git a/ebus_classify_multi.py b/ebus_classify_multi.py new file mode 100644 index 0000000..7f62643 --- /dev/null +++ b/ebus_classify_multi.py @@ -0,0 +1,303 @@ +import glob +import os.path as osp +import random +import numpy as np +from PIL import Image +from tqdm import tqdm +import matplotlib.pyplot as plt +import csv +import torch +import torch.nn as nn +import torch.optim as optim +import torch.utils.data as data +import torchvision +from torchvision import models, transforms +import sys + +# 入力画像の前処理クラス +# 訓練時と推論時で処理を変える +class ImageTransform(): + """ + 画像の前処理クラス.訓練時と推論時で処理が異なる. + データ前処理:画像のリサイズ,色の標準化. + 訓練時データ拡張:RandomResezedCropとRandomHorizontalFlip + + Attributes + ---------- + resize : int + リサイズの大きさ + mean : (R, G, B) + 各色チャネルの平均値 + std : (R, G, B) + 各色チャネルの標準偏差 + """ + + def __init__(self, resize, mean, std): + self.data_transform = { + 'train': transforms.Compose([ + transforms.RandomResizedCrop( + resize, scale=(0.5, 1.0)), + transforms.RandomHorizontalFlip(), + transforms.ToTensor(), # Torchテンソルに変換 + transforms.Normalize(mean, std) # 色の標準化 + ]), + 'val': transforms.Compose([ + transforms.Resize(resize), + transforms.CenterCrop(resize), + transforms.ToTensor(), # Torchテンソルに変換 + transforms.Normalize(mean, std) # 色の標準化 + ]), + } + + def __call__(self, img, phase='train'): + """ + Parameters + ---------- + phase : 'train' or 'val' + 前処理のモード指定 + """ + return self.data_transform[phase](img) + + +# 画像へのファイルパスのリストを作成 +def make_datapath_list(phase="train"): + """ + データのパスを格納したリストを作成 + + Parameters + ---------- + phase : 'train' or 'val' + 訓練データか,検証データかを指定 + + Returns + ------- + path_list : list + データへのパスを格納したリスト + """ + + rootpath = "./data/ebus_data_v5/" + target_path = osp.join(rootpath + phase + '/**/*.bmp') + # print(target_path) + + path_list = [] + + # globを利用してサブディレクトリまでファイルパスを取得 + for path in glob.glob(target_path): + path_list.append(path) + + return path_list + + +# EBUS画像のDatasetを作成する +class EbusDataset(data.Dataset): + """ + EBUS画像のDatasetクラス.PyTorchのDatasetクラスを継承 + + Attributes + ---------- + file_list : list + 画像のパスを格納したリスト + transform : object + 前処理クラスのインスタンス + phase : 'train' or 'val' + 訓練か検証かを設定する + """ + + def __init__(self, file_list, transform=None, phase='train'): + self.file_list = file_list + self.transform = transform + self.phase = phase + + def __len__(self): + '''画像の枚数を返す''' + return len(self.file_list) + + def __getitem__(self, index): + ''' + 前処理した画像のTensor形式のデータとラベルを取得 + ''' + + # index番目の画像をロード + img_path = self.file_list[index] + img = Image.open(img_path) + + # 画像の前処理を実施 + img_transformed = self.transform(img, self.phase) + + if self.phase == "train": + label = img_path[26:32] + elif self.phase == "val": + label = img_path[24:30] + + # ラベルを数値に変更 + if label == "Benign": + label = 0 + elif label == "Malign": + label = 1 + + return img_transformed, label + + +# モデルを学習させる関数 +def train_model(net, dataloaders_dict, criterion, optimizer, num_epochs): + + # GPU確認 + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + if torch.cuda.is_available(): + net.to(device) # ネットワークをGPUへ + net = torch.nn.DataParallel(net) # make parallel + torch.backends.cudnn.benchmark = True # ネットワークがある程度固定なら高速化 + + # ログ出力 + f = open('result.csv', 'w') + writer = csv.writer(f, lineterminator='\n') + writer.writerow(['epoch', 'train_loss', 'train_acc', 'val_loss', 'val_acc']) + + # epochのループ + for epoch in range(num_epochs): + csvitems = [] + csvitems.append(epoch+1) + print('') + print('Epoch {}/{}'.format(epoch+1, num_epochs)) + + # epochごとの学習と検証のループ + for phase in ['train', 'val']: + if phase == 'train': + net.train() + else: + net.eval() + + epoch_loss = 0.0 # epochの損失和 + epoch_corrects = 0 # epochの正解数 + + # 未学習時の性能を確かめるため epoch=0 の訓練は省略 + if (epoch == 0) and (phase == 'train'): + csvitems.append(0) + csvitems.append(0) + continue + + # データローダーからミニバッチを取り出すループ + for inputs, labels in tqdm(dataloaders_dict[phase]): + + # GPUが使えるならGPUへデータ転送 + inputs = inputs.to(device) + labels = labels.to(device) + + # optimizerを初期化 + optimizer.zero_grad() + + # 順伝搬(forward)計算 + with torch.set_grad_enabled(phase == 'train'): + outputs = net(inputs) + loss = criterion(outputs, labels) + _, preds = torch.max(outputs, 1) + + # 訓練時はバックプロパゲーション + if phase == 'train': + loss.backward() + optimizer.step() + + # イテレーション結果の計算 + # lossの合計を更新 + epoch_loss += loss.item() * inputs.size(0) + epoch_corrects += torch.sum(preds == labels.data) + + # epochごとのlossと正解率を表示 + epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset) + epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset) + + print('{} Loss: {:.4f} Acc: {:.4f}'.format( + phase, epoch_loss, epoch_acc)) + csvitems.append('{:.4f}'.format(epoch_loss)) + csvitems.append('{:.4f}'.format(epoch_acc)) + + writer.writerow(csvitems) + + f.close() + + +if __name__ == "__main__": + + # 乱数のシードを設定 + torch.manual_seed(1235) + np.random.seed(1235) + random.seed(1235) + + # モデル入力画像の仕様 + size = 224 + mean = (0.485, 0.456, 0.406) + std = (0.229, 0.224, 0.225) + + # データセットファイル名の取得 + train_list = make_datapath_list(phase="train") + val_list = make_datapath_list(phase="val") + # print(train_list) + + # データセットの読み込み + train_dataset = EbusDataset( + file_list=train_list, transform=ImageTransform(size, mean, std), phase='train') + val_dataset = EbusDataset( + file_list=val_list, transform=ImageTransform(size, mean, std), phase='val') + + # ミニバッチのサイズを指定 + batch_size = 16 + + # DataLoaderを作成 + train_dataloader = torch.utils.data.DataLoader( + train_dataset, batch_size=batch_size, shuffle=True) + val_dataloader = torch.utils.data.DataLoader( + val_dataset, batch_size=batch_size, shuffle=False) + + # 辞書型変数にまとめる + dataloaders_dict = {"train": train_dataloader, "val": val_dataloader} + + use_pretrained = True + # ResNet18 + # net = models.resnet18(pretrained=use_pretrained) + # net = models.resnet101(pretrained=use_pretrained) + # # print(net) + # # sys.exit() + # net.fc = nn.Linear(in_features=2048, out_features=2, bias=True) + # update_param_names = ["fc.weight", "fc.bias"] # 学習させるパラメータ名 + + # VGG16 + net = models.vgg16(pretrained=use_pretrained) + net.classifier[6] = nn.Linear(in_features=4096, out_features=2) + # update_param_names = ["classifier.3.weight", "classifier.3.bias", "classifier.6.weight", "classifier.6.bias"] + update_param_names = ["classifier.6.weight", "classifier.6.bias"] + + # 訓練モードに設定 + net.train() + + print('ネットワーク設定完了:学習済みの重みをロードし,訓練モードに設定しました') + + # 損失関数の設定 + criterion = nn.CrossEntropyLoss() + + # 転移学習で学習させるパラメータを,変数params_to_updateに格納 + params_to_update = [] + + # 学習させるパラメータ名 + + # 学習させるパラメータ以外は勾配計算せず固定 + for name, param in net.named_parameters(): + # if name.startswith('classifier'): + if name in update_param_names: + param.requires_grad = True + params_to_update.append(param) + print("update: ", name) + else: + param.requires_grad = False + print("freeze: ", name) + + # params_to_updateの中身を確認 + print("------------") + # print(params_to_update) + + # 最適化手法の設定 + optimizer = optim.SGD(params=params_to_update, lr=0.001, momentum=0.9) + + # 学習・検証を実行する + num_epochs = 50 + train_model(net, dataloaders_dict, criterion, optimizer, num_epochs=num_epochs) + print('done.') diff --git a/ebus_movie_classify.py b/ebus_movie_classify.py new file mode 100644 index 0000000..2477ce3 --- /dev/null +++ b/ebus_movie_classify.py @@ -0,0 +1,308 @@ +import glob +import os.path as osp +import random +import numpy as np +from PIL import Image +from tqdm import tqdm +import matplotlib.pyplot as plt +import csv +import torch +import torch.nn as nn +import torch.optim as optim +import torch.utils.data as data +import torchvision +from torchvision import models, transforms +import sys + +# 入力画像の前処理クラス +# 訓練時と推論時で処理を変える +class ImageTransform(): + """ + 画像の前処理クラス.訓練時と推論時で処理が異なる. + データ前処理:画像のリサイズ,色の標準化. + 訓練時データ拡張:RandomResezedCropとRandomHorizontalFlip + + Attributes + ---------- + resize : int + リサイズの大きさ + mean : (R, G, B) + 各色チャネルの平均値 + std : (R, G, B) + 各色チャネルの標準偏差 + """ + + def __init__(self, resize, mean, std): + self.data_transform = { + 'train': transforms.Compose([ + transforms.RandomResizedCrop( + resize, scale=(0.5, 1.0)), + transforms.RandomHorizontalFlip(), + transforms.ToTensor(), # Torchテンソルに変換 + transforms.Normalize(mean, std) # 色の標準化 + ]), + 'val': transforms.Compose([ + transforms.Resize(resize), + transforms.CenterCrop(resize), + transforms.ToTensor(), # Torchテンソルに変換 + transforms.Normalize(mean, std) # 色の標準化 + ]), + } + + def __call__(self, img, phase='train'): + """ + Parameters + ---------- + phase : 'train' or 'val' + 前処理のモード指定 + """ + return self.data_transform[phase](img) + + +# 画像へのファイルパスのリストを作成 +def make_datapath_list(phase="train", label="Benign"): + """ + データのパスを格納したリストを作成 + + Parameters + ---------- + phase : 'train' or 'val' + 訓練データか,検証データかを指定 + + Returns + ------- + path_list : list + データへのパスを格納したリスト + """ + + rootpath = "/mnt/d/usr/DL/EBUS/" + target_path = osp.join(rootpath + phase + "/" + label + '/**/*.png') + # print(target_path) + # print(len(glob.glob(target_path, recursive=True))) + + # globを利用してサブディレクトリまでファイルパスを取得 + path_list = [] + for path in glob.glob(target_path, recursive=True): + path_list.append(path) + + return path_list + + +# EBUS画像のDatasetを作成する +class EbusDataset(data.Dataset): + """ + EBUS画像のDatasetクラス.PyTorchのDatasetクラスを継承 + + Attributes + ---------- + file_list : list + 画像のパスを格納したリスト + transform : object + 前処理クラスのインスタンス + phase : 'train' or 'val' + 訓練か検証かを設定する + """ + + def __init__(self, file_list, transform=None, phase='train'): + self.file_list = file_list + self.transform = transform + self.phase = phase + + def __len__(self): + '''画像の枚数を返す''' + return len(self.file_list) + + def __getitem__(self, index): + ''' + 前処理した画像のTensor形式のデータとラベルを取得 + ''' + + # index番目の画像をロード + img_path = self.file_list[index] + img = Image.open(img_path) + # 画像の前処理を実施 + img_transformed = self.transform(img, self.phase) + + # パスからラベルを判定 + label = 0 if "Benign" in img_path else 1 + + return img_transformed, label + +# モデルを学習させる関数 +def train_model(net, dataloaders_dict, criterion, optimizer, num_epochs): + + # 演算デバイス設定 + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + print("使用デバイス:", device) + net.to(device) + # ネットワークがある程度固定なら高速化 + torch.backends.cudnn.benchmark = True + + # ログ出力 + f = open('result.csv', 'w') + writer = csv.writer(f, lineterminator='\n') + writer.writerow(['epoch', 'train_loss', 'train_acc', 'val_loss', 'val_acc']) + + # epochのループ + for epoch in range(num_epochs): + csvitems = [] + csvitems.append(epoch+1) + print('') + print('Epoch {}/{}'.format(epoch+1, num_epochs)) + + # epochごとの学習と検証のループ + for phase in ['train', 'val']: + if phase == 'train': + net.train() + else: + net.eval() + + epoch_loss = 0.0 # epochの損失和 + epoch_corrects = 0 # epochの正解数 + + # 未学習時の性能を確かめるため epoch=0 の訓練は省略 + if (epoch == 0) and (phase == 'train'): + csvitems.append(0) + csvitems.append(0) + continue + + # データローダーからミニバッチを取り出すループ + for inputs, labels in tqdm(dataloaders_dict[phase]): + + # GPUが使えるならGPUへデータ転送 + inputs = inputs.to(device) + labels = labels.to(device) + + # optimizerを初期化 + optimizer.zero_grad() + + # 順伝搬(forward)計算 + with torch.set_grad_enabled(phase == 'train'): + outputs = net(inputs) + loss = criterion(outputs, labels) + _, preds = torch.max(outputs, 1) + + # 訓練時はバックプロパゲーション + if phase == 'train': + loss.backward() + optimizer.step() + + # イテレーション結果の計算 + # lossの合計を更新 + epoch_loss += loss.item() * inputs.size(0) + epoch_corrects += torch.sum(preds == labels.data) + + # epochごとのlossと正解率を表示 + epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset) + epoch_acc = epoch_corrects.double() / len(dataloaders_dict[phase].dataset) + + print('{} Loss: {:.4f} Acc: {:.4f}'.format( + phase, epoch_loss, epoch_acc)) + csvitems.append('{:.4f}'.format(epoch_loss)) + csvitems.append('{:.4f}'.format(epoch_acc)) + + writer.writerow(csvitems) + + f.close() + + +if __name__ == "__main__": + + # 乱数のシードを設定 + torch.manual_seed(1235) + np.random.seed(1235) + random.seed(1235) + + # モデル入力画像の仕様 + size = 224 + mean = (0.485, 0.456, 0.406) + std = (0.229, 0.224, 0.225) + + # データセットファイル名の取得 + train_benign_list = make_datapath_list(phase="train", label="Benign") + train_malignant_list = make_datapath_list(phase="train", label="Malignant") + val_benign_list = make_datapath_list(phase="val", label="Benign") + val_malignant_list = make_datapath_list(phase="val", label="Malignant") + print("Dataset(orig): train benign:%d malignant:%d val benign:%d malignant:%d" % + (len(train_benign_list), len(train_malignant_list), len(val_benign_list), len(val_malignant_list) )) + num_traindata = 200 + num_valdata = 100 + train_benign_list = random.sample(train_benign_list, num_traindata) + train_malignant_list = random.sample(train_malignant_list, num_traindata) + val_benign_list = random.sample(val_benign_list, num_valdata) + val_malignant_list = random.sample(val_malignant_list, num_valdata) + print("Dataset(arranged): train benign:%d malignant:%d val benign:%d malignant:%d" % + (len(train_benign_list), len(train_malignant_list), len(val_benign_list), len(val_malignant_list) )) + + # データセットの読み込み + train_list = train_benign_list + train_malignant_list + val_list = val_benign_list + val_malignant_list + train_dataset = EbusDataset( + file_list=train_list, transform=ImageTransform(size, mean, std), phase='train') + val_dataset = EbusDataset( + file_list=val_list, transform=ImageTransform(size, mean, std), phase='val') + + # ミニバッチのサイズを指定 + batch_size = 16 + + # DataLoaderを作成 + train_dataloader = torch.utils.data.DataLoader( + train_dataset, batch_size=batch_size, shuffle=True) + val_dataloader = torch.utils.data.DataLoader( + val_dataset, batch_size=batch_size, shuffle=False) + + # 辞書型変数にまとめる + dataloaders_dict = {"train": train_dataloader, "val": val_dataloader} + + use_pretrained = True + # ResNet18 + # net = models.resnet18(pretrained=use_pretrained) + # net.fc = nn.Linear(in_features=512, out_features=2, bias=True) + net = models.resnet101(pretrained=use_pretrained) + net.fc = nn.Linear(in_features=2048, out_features=2, bias=True) + # print(net) + # sys.exit() + update_param_names = ["fc.weight", "fc.bias"] # 学習させるパラメータ名 + + # VGG16 + # net = models.vgg16(pretrained=use_pretrained) + # net.classifier[6] = nn.Linear(in_features=4096, out_features=2) + # update_param_names = ["classifier.3.weight", "classifier.3.bias", "classifier.6.weight", "classifier.6.bias"] + # update_param_names = ["classifier.6.weight", "classifier.6.bias"] + + # 訓練モードに設定 + net.train() + + print('ネットワーク設定完了:学習済みの重みをロードし,訓練モードに設定しました') + + # 損失関数の設定 + criterion = nn.CrossEntropyLoss() + + # 転移学習で学習させるパラメータを,変数params_to_updateに格納 + params_to_update = [] + + # 学習させるパラメータ名 + + # 学習させるパラメータ以外は勾配計算せず固定 + for name, param in net.named_parameters(): + # if True: + # if name.startswith('classifier'): + if name in update_param_names: + param.requires_grad = True + params_to_update.append(param) + print("update: ", name) + else: + param.requires_grad = False + print("freeze: ", name) + + # params_to_updateの中身を確認 + print("------------") + # print(params_to_update) + + # 最適化手法の設定 + optimizer = optim.SGD(params=params_to_update, lr=0.001, momentum=0.9) + + # 学習・検証を実行する + num_epochs = 50 + train_model(net, dataloaders_dict, criterion, optimizer, num_epochs=num_epochs) + print('done.')