#月とすっぽん
月もすっぽんも同じように丸いが、比較にならないほどその違いは大きいこと。二つのものがひどく違っていることのたとえ。
https://dictionary.goo.ne.jp/word/%E6%9C%88%E3%81%A8%E9%BC%88/
大きな違いがあるらしいので、ディープラーニングを使い画像認識ができるか試してみましょう!
適宜、(少しですが)Pytorchの説明もします。(間違っていたらぜひ訂正してください。よろしくお願いします。)
コードはこちらです。
https://github.com/kyasby/Tuki-Suppon.git
##今回のキーワード
「月とすっぽん」
似て非なるものらしいです。
pytorchの「torchvision.datasets.ImageFolder」
kerasのfrom_from_directry
にあたるpytorchのtorchvision.datasets.ImageFolder
使用した記事があまりなかったので作りました。
フォルダーに画像を入れると自動でラベル付をしてくれます。便利です。
pytorchの「torch.utils.data.random_split」
これのおかげで、フォルダに写真を入れる段階ではtrainとtestに分ける必要がないです。
#使用するデータ
google画像から
・すっぽんの画像67枚
甲羅の上から見たような画像を集めました。
例えば、こんな感じの画像です。
(ぴぃさんのスッポン)http://photozou.jp/photo/show/235691/190390795
*すっぽんのいい感じの画像はあまりなかったので一部かめの画像で代用しています。
・月の画像70枚
丸い月が集まっている画像を集めました。手作業で切り抜いて、画面に大きく円が映るようにしました。
例えば、こんな感じの画像です。
*画像はスクレイピングしてきました。
*スクレイピングについては、ここでは触れません。
#データセット作成
.
├── main.ipynb
├── pics
├── tuki
| |-tuki1.png
| |-tuki2.png
|
└── kame
|-kame1.png
|-kame2.png
せっかく、ディレクトリに画像が分かれているので、torchvision.datasets.ImageFolde
を使用してディレクトリごとに自動でラベルづけをします。
##モジュールのインポート
import matplotlib.pyplot as plt
import numpy as np
import copy
import time
import os
from tqdm import tqdm
import torchvision.transforms as transforms
import torchvision.models as models
import torchvision
import torch.nn as nn
import torch
##前処理
transform_dict = {
'train': transforms.Compose(
[transforms.Resize((256,256)),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
]),
'test': transforms.Compose(
[transforms.Resize((256,256)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
])}
trainとtest用の前処理辞書を作ります。
transforms.Compose
を使うことで、前処理のシーケンスを作ることができます。引数に入れていった順番で処理されるみたいです。
今回は、
transforms.Resize(256, 256)
→画像を256x256にリサイズします。
transforms.RandomHorizontalFlip()
→左右逆転した画像を作成します。
transforms.ToTensor()
→PILまたはnumpy.ndarray((height x width x channel)で(0~255))
を
Tensor((channel x height x width)で(0.0~1.0))に変換してくれます。
PILやnumpyでは画像は(height x width x channel)の順番ですが、Pytorchでは(channel x height x width)の点には注意が必要です。この順番の方が扱いやすいという理由のようです。
transforms.Normalize(mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225])
→GRBのそれぞれを指定した平均値と標準偏差で正規化します。
ドキュメント
https://pytorch.org/docs/stable/torchvision/transforms.html
##データセット
# ex.
# data_folder = "./pics"
# transform = transform_dict["train"]
data = torchvision.datasets.ImageFolder(root=data_folder, transform=transform_dict[phase])
上記のディレクトリからデータセットを作成します。
##trainとtestの分離
# ex.
# train_ratio = 0.8
train_size = int(train_ratio * len(data))
# int()で整数に。
val_size = len(data) - train_size
data_size = {"train":train_size, "val":val_size}
# =>{"train": 112, "val": 28}
data_train, data_val = torch.utils.data.random_split(data, [train_size, val_size])
torch.utils.data.random_split(dataset, lengths)
はデータセットをランダムに、被りなく、分けてくれます。
datasetにはもちろんデータセットを、
lengthsにはデータセットの個数をリストで渡せます。
また、trainとvalidのデータサイズを辞書に格納しました。
# ex.
# data_train => Subset(data, [4,5,1,7])
# data_val => Subset(data, [3,8,2,6])
返り値は、リストの長さの数だけあります。
それぞれの返り値には、データセットとインデックス番号のリストが含まれています。
(Subsetとは何ですか?)
##データローダ
train_loader = torch.utils.data.DataLoader(data_train, batch_size=batch_size, shuffle=True)
val_loader = torch.utils.data.DataLoader(data_val, batch_size=batch_size, shuffle=False)
dataloaders = {"train":train_loader, "val":val_loader}
データローダを作ります。
Pytorchでは、このようにデータローダを作成しデータを読み込ませます。
又、これを辞書に入れました。
##画像の確認
def imshow(img):
img = img / 2 + 0.5
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
# 訓練データをランダムに取得
dataiter = iter(dataloaders["train"])
images, labels = dataiter.next()
# 画像の表示
imshow(torchvision.utils.make_grid(images))
# ラベルの表示
print(' '.join('%5s' % labels[labels[j]] for j in range(8)))
上記のコードでこんな感じに表示してくれるみたいです。
こちらから頂きました。 https://qiita.com/kuto/items/0ff3ccb4e089d213871d
##モデル作成
model = models.resnet18(pretrained=True)
for param in model.parameters():
print(param)
# => Parameter containing:
#tensor([[[[-1.0419e-02, -6.1356e-03, -1.8098e-03, ..., 5.6615e-02,
# 1.7083e-02, -1.2694e-02],
# ...
# -7.1195e-02, -6.6788e-02]]]], requires_grad=True)
モデルはResNet18を使用します。引数にpretrained=True
を入れることで、学習済みのモデルを使う事ができます。
既存のパラメータの学習はせずに、転移学習をします。
requires_grad=True
と表示されるweightは更新されます。
更新されないようにするために、以下のように設定します。
model
# => ResNet(
# (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
# (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
# (relu): ReLU(inplace=True)
# (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
# (layer1): Sequential(
# (0): BasicBlock(
# (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
# (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
# (relu): ReLU(inplace=True)
# (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
# (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
# )
# ...
# (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
# (fc): Linear(in_features=512, out_features=1000, bias=True)
# )
最終層が(fc)
だとわかったので、
for p in model.parameters():
p.requires_grad=False
model.fc = nn.Linear(512, 2)
model.parameters()
で全てのパラメータを取り出し、requires_grad=False
とした上で、最終層を上書きします。
##学習の設定
model = model.cuda() #GPUなしの場合はこの行はいらない。
lr = 1e-4
epoch = 40
optim = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=1e-4)
criterion = nn.CrossEntropyLoss().cuda() #GPUなしの場合は.cuda()はいらない。
GPUを使う場合は、modelをGPUに送ってやる必要があります。
モデルはほぼチュートリアルのままです。
https://pytorch.org/tutorials/beginner/transfer_learning_tutorial.html
##モデル作成
def train_model(model, criterion, optimizer, scheduler=None, num_epochs=25):
#:bool値を返す。
use_gpu = torch.cuda.is_available()
#始まりの時間
since = time.time()
best_model_wts = copy.deepcopy(model.state_dict())
best_acc = 0.0
#途中経過保存用に、リストを持った辞書を作ります。
loss_dict ={"train" : [], "val" : []}
acc_dict = {"train" : [], "val" : []}
for epoch in tqdm(range(num_epochs)):
if (epoch+1)%5 == 0:#5回に1回エポックを表示します。
print('Epoch {}/{}'.format(epoch, num_epochs - 1))
print('-' * 10)
# それぞれのエポックで、train, valを実行します。
# 辞書に入れた威力がここで発揮され、trainもvalも1回で書く事ができます。
for phase in ['train', 'val']:
if phase == 'train':
model.train() # 学習モード。dropoutなどを行う。
else:
model.val() # 推論モード。dropoutなどを行わない。
running_loss = 0.0
running_corrects = 0
for data in dataloaders[phase]:
inputs, labels = data #ImageFolderで作成したデータは、
#データをラベルを持ってくれます。
#GPUを使わない場合不要
if use_gpu:
inputs = inputs.cuda()
labels = labels.cuda()
#~~~~~~~~~~~~~~forward~~~~~~~~~~~~~~~
outputs = model(inputs)
_, preds = torch.max(outputs.data, 1)
#torch.maxは実際の値とインデクスを返します。
#torch.max((0.8, 0.1),1)=> (0.8, 0)
#引数の1は行方向、列方向、どちらの最大値を返すか、です。
loss = criterion(outputs, labels)
if phase == 'train':
optimizer.zero_grad()
loss.backward()
optimizer.step()
# statistics #GPUなしの場合item()不要
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels)
# (preds == labels)は[True, True, False]などをかえしますが、
# pythonのTrue, Falseはそれぞれ1, 0に対応しているので、
# sumで合計する事ができます。
#リストに途中経過を格納
loss_dict[phase].append(epoch_loss)
acc_dict[phase].append(epoch_acc)
# サンプル数で割って平均を求めます。
# 辞書にサンプル数を入れたのが生きてきます。
epoch_loss = running_loss / data_size[phase]
#GPUなしの場合item()不要
epoch_acc = running_corrects.item() / data_size[phase]
#tensot().item()を使う事で、テンソルから値を取り出す事ができます。
#print(tensorA) => tensor(112, device='cuda:0')
#print(tensorA.itme)) => 112
#formatを使いますが、.nfとすると、小数点以下をn桁まで出力できます。
#C言語と一緒ですね。
print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
# deep copy the model
# 精度が改善したらモデルを保存する
if phase == 'val' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())
# deepcopyをしないと、model.state_dict()の中身の変更に伴い、
# コピーした(はずの)データも変わってしまいます。
# copyとdeepcopyの違いはこの記事がわかりやすいです。
# https://www.headboost.jp/python-copy-deepcopy/
time_elapsed = time.time() - since
print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
print('Best val acc: {:.4f}'.format(best_acc))
# 最良のウェイトを読み込んで、返す。
model.load_state_dict(best_model_wts)
return model, loss_dict, acc_dict
##学習
model_ft, loss, acc = train_model(model, criterion, optim, num_epochs=epoch)
##学習を可視化
#loss, accを取り出します。
loss_train = loss["train"]
loss_val = loss["val"]
acc_train = acc["train"]
acc_val = acc["val"]
#このように書く事で、nrows x cols のグラフを作成する事ができます。
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10,5))
#0個目のグラフ
axes[0].plot(range(epoch), loss_train, label = "train")
axes[0].plot(range(epoch), loss_val, label = "val")
axes[0].set_title("Loss")
axes[0].legend()#各グラフのlabelを表示
#1個目のグラフ
axes[1].plot(range(epoch), acc_train, label = "train")
axes[1].plot(range(epoch), acc_val, label = "val")
axes[1].set_title("Acc")
axes[1].legend()
#0個目と1個目のグラフが重ならないように調整
fig.tight_layout()
##おまけ
###GoogleColabolatory
手軽にGPUを使う方法としてコラボがあります。
https://colab.research.google.com/notebooks/welcome.ipynb?hl=ja
コラボで画像を使うときには、zipにしてアップロードすると便利です。
(1枚1枚アップロードするのは大変です。)(ドライブと連携する方法でもOK)
その際、解凍は以下のようにできます。
#/content/pics.zipはそれぞれ変えてください。
!unzip /content/pics.zip -d /content/data > /dev/null 2>&1 &
また、ファイルを右クリックすると出てくる「パスをコピー」も便利です。
###matplotlib.plt
今回は、1行2列のグラフを出力しましたが、例えば2行2列だと例えば以下のようにグラフを作成できます。
ある、グラフにプロットを上書きして同時に2つプロットすることもできます。
グラフにつき、2つずつプロットしてみました。
loss_train = loss["train"]
loss_val = loss["val"]
acc_train = acc["train"]
acc_val = acc["val"]
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10,5))
axes[0,0].plot(range(epoch), loss_train, label = "train")
axes[0,0].plot(range(epoch), loss_val, label = "val")
axes[0,0].set_title("Loss")
axes[0,0].legend()
axes[0,1].plot(range(epoch), acc_train, c="red", label = "train")
axes[0,1].plot(range(epoch), acc_val, c="pink", label = "val")
axes[0,1].set_title("Train Loss")
axes[0,1].legend()
x = np.random.rand(100)
xx = np.random.rand(200)
axes[1,0].hist(xx, bins=25, label="xx")
axes[1,0].hist(x, bins=50, label="x")
axes[1,0].set_title("histgram")
y = np.random.randn(100)
z = np.random.randn(100)
axes[1,1].scatter(y, z, alpha=0.8, label="y,z")
axes[1,1].scatter(z, y, alpha=0.8, label="z,y")
axes[1,1].set_title("Scatter")
axes[1,1].legend()
fig.tight_layout()