この記事 はGitHubhttps ://github.com/Jack-Cherish/PythonParkに含まれてい ます。技術的な乾物記事、整理された学習資料、および第一線のメーカーのインタビュー経験の共有があります。スターへようこそ。
I.はじめに
この記事は、Pytorchディープラーニングセマンティックセグメンテーションチュートリアルシリーズに属しています。
このシリーズの記事の内容は次のとおりです。
- Pytorchの基本的な使用法
- セマンティックセグメンテーションアルゴリズムの説明
PS:記事に表示されているすべてのコードは、私のgithubにダウンロードできます。フォロー、スターへようこそ:クリックして表示
2.プロジェクトの背景
深層学習アルゴリズムは、問題を解決する方法にすぎません。トレーニング用に選択するネットワークの種類、前処理の種類、使用する損失と最適化の方法はすべて、特定のタスクに応じて決定されます。
それでは、まず今日のタスクを見てみましょう。
はい、それはUNetペーパーの古典的なタスクです:医療画像のセグメンテーション。
そのシンプルさと使いやすさから、今日のタスクとして選ばれました。
このタスクを簡単に説明します。アニメーションに示されているように、セル構造図を作成するには、各セルを互いに分離する必要があります。
トレーニングデータは、512x512の解像度でわずか30個です。これらの写真は、フルーツハエの電子顕微鏡写真です。
さて、タスクの紹介が完了し、トレーニングモデルが始まります。
3、UNetトレーニング
深層学習モデルをトレーニングするには、それを3つのステップに簡単に分割できます。
- データの読み込み:データの読み込み方法、タグの定義方法、使用するデータ拡張方法はすべて、このステップで実行されます。
- モデルの選択:このシリーズの前回の記事で説明したUNetネットワークであるモデルを用意しました。
- アルゴリズムの選択:アルゴリズムの選択は、選択する損失と使用する最適化アルゴリズムです。
各ステップはより一般的であり、今日の医療画像セグメンテーションタスクに基づいて説明を拡張します。
1.データの読み込み
このステップでは、多くのことを実行できます。率直に言って、画像の読み込み方法とラベルの定義方法にすぎません。アルゴリズムの堅牢性を高めたり、データセットを増やしたりするために、いくつかのデータ拡張操作を実行できます。
データを処理しているので、処理方法を決める前に、まずデータがどのように見えるかを見てみましょう。
データの準備ができました、すべてここ(Github):クリックして表示
Githubのダウンロード速度が遅い場合は、記事の最後にあるBaiduリンクを使用してデータセットをダウンロードできます。
データはトレーニングセットとテストセットに分けられ、それぞれ30枚あり、トレーニングセットにはラベルがあり、テストセットにはラベルがありません。
データ読み込みの処理は、タスクとデータセットに応じて決定されます。セグメンテーションタスクでは、あまり処理する必要はありませんが、データ量が30枚と少ないため、いくつかのデータ拡張方法を使用して拡張できます。私たちのデータセット。
Pytorchは、データのロードを容易にする方法を提供します。このフレームワークを使用して、データをロードできます。疑似コードを見てください:
# ================================================================== #
# Input pipeline for custom dataset #
# ================================================================== #
# You should build your custom dataset as below.
class CustomDataset(torch.utils.data.Dataset):
def __init__(self):
# TODO
# 1. Initialize file paths or a list of file names.
pass
def __getitem__(self, index):
# TODO
# 1. Read one data from file (e.g. using numpy.fromfile, PIL.Image.open).
# 2. Preprocess the data (e.g. torchvision.Transform).
# 3. Return a data pair (e.g. image and label).
pass
def __len__(self):
# You should change 0 to the total size of your dataset.
return 0
# You can then use the prebuilt data loader.
custom_dataset = CustomDataset()
train_loader = torch.utils.data.DataLoader(dataset=custom_dataset,
batch_size=64,
shuffle=True)
これは標準のテンプレートです。このテンプレートを使用して、データの読み込み、タグの定義、およびデータ拡張の実行を行います。
datadata.pyファイルを作成し、次のようにコードを記述します。
import torch
import cv2
import os
import glob
from torch.utils.data import Dataset
import random
class ISBI_Loader(Dataset):
def __init__(self, data_path):
# 初始化函数,读取所有data_path下的图片
self.data_path = data_path
self.imgs_path = glob.glob(os.path.join(data_path, 'image/*.png'))
def augment(self, image, flipCode):
# 使用cv2.flip进行数据增强,filpCode为1水平翻转,0垂直翻转,-1水平+垂直翻转
flip = cv2.flip(image, flipCode)
return flip
def __getitem__(self, index):
# 根据index读取图片
image_path = self.imgs_path[index]
# 根据image_path生成label_path
label_path = image_path.replace('image', 'label')
# 读取训练图片和标签图片
image = cv2.imread(image_path)
label = cv2.imread(label_path)
# 将数据转为单通道的图片
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
label = cv2.cvtColor(label, cv2.COLOR_BGR2GRAY)
image = image.reshape(1, image.shape[0], image.shape[1])
label = label.reshape(1, label.shape[0], label.shape[1])
# 处理标签,将像素值为255的改为1
if label.max() > 1:
label = label / 255
# 随机进行数据增强,为2时不做处理
flipCode = random.choice([-1, 0, 1, 2])
if flipCode != 2:
image = self.augment(image, flipCode)
label = self.augment(label, flipCode)
return image, label
def __len__(self):
# 返回训练集大小
return len(self.imgs_path)
if __name__ == "__main__":
isbi_dataset = ISBI_Loader("data/train/")
print("数据个数:", len(isbi_dataset))
train_loader = torch.utils.data.DataLoader(dataset=isbi_dataset,
batch_size=2,
shuffle=True)
for image, label in train_loader:
print(image.shape)
コードを実行すると、次の結果が表示されます。
コードを説明します。
__init__関数は、このクラスの初期化関数です。指定された画像パスに従って、すべての画像データが読み取られ、self.imgs_pathリストに格納されます。
__len__関数は、このクラスがインスタンス化された後、len()関数を介して呼び出されたデータ量を返すことができます。
__getitem__関数はデータ取得関数です。この関数では、データの読み取り方法、処理方法を記述でき、ここでデータの前処理とデータ拡張を実行できます。ここでの私の処理は非常に単純です。画像を読み取って、単一チャネルの画像に処理するだけです。同時に、ラベルの画像ピクセルは0と255であるため、0と1になるには255で除算する必要があります。同時に、データ拡張はランダムに実行されました。
拡張機能は、定義されたデータ拡張機能です。何をするかは関係ありません。ここでは、単純な回転操作を実行しました。
このクラスでは、データセットをシャッフルするためにいくつかの操作を実行する必要はなく、バッチサイズに応じてデータを読み取る方法について心配する必要もありません。このクラスをインスタンス化した後、torch.utils.data.DataLoaderメソッドを使用してバッチサイズのサイズを指定し、データを中断するかどうかを決定できるためです。
Pytorchから提供されたDataLoaderは非常に強力であり、データをCUDAメモリにロードするかどうかに関係なく、データのロードに使用するプロセスの数を指定することもできます。この記事では取り上げていないため、再度説明しません。
2.モデルの選択
すでにモデルを選択しており、前回の記事「Pytorch Deep Learning Practical Tutorial(2):UNet SemanticSegmentationNetwork」で説明したUNetネットワーク構造を使用します。
ただし、ネットワークを微調整する必要があります。紙の構造に応じて、モデル出力のサイズは画像入力のサイズよりもわずかに小さくなります。紙のネットワーク構造を使用する場合は、結果が出力された後にサイズ変更操作を行う必要があります。この手順を保存するために、ネットワークの出力サイズが画像の入力サイズと正確に等しくなるようにネットワークを変更できます。
unet_parts.pyファイルを作成し、次のコードを記述します。
""" Parts of the U-Net model """
"""https://github.com/milesial/Pytorch-UNet/blob/master/unet/unet_parts.py"""
import torch
import torch.nn as nn
import torch.nn.functional as F
class DoubleConv(nn.Module):
"""(convolution => [BN] => ReLU) * 2"""
def __init__(self, in_channels, out_channels):
super().__init__()
self.double_conv = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True),
nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
def forward(self, x):
return self.double_conv(x)
class Down(nn.Module):
"""Downscaling with maxpool then double conv"""
def __init__(self, in_channels, out_channels):
super().__init__()
self.maxpool_conv = nn.Sequential(
nn.MaxPool2d(2),
DoubleConv(in_channels, out_channels)
)
def forward(self, x):
return self.maxpool_conv(x)
class Up(nn.Module):
"""Upscaling then double conv"""
def __init__(self, in_channels, out_channels, bilinear=True):
super().__init__()
# if bilinear, use the normal convolutions to reduce the number of channels
if bilinear:
self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
else:
self.up = nn.ConvTranspose2d(in_channels // 2, in_channels // 2, kernel_size=2, stride=2)
self.conv = DoubleConv(in_channels, out_channels)
def forward(self, x1, x2):
x1 = self.up(x1)
# input is CHW
diffY = torch.tensor([x2.size()[2] - x1.size()[2]])
diffX = torch.tensor([x2.size()[3] - x1.size()[3]])
x1 = F.pad(x1, [diffX // 2, diffX - diffX // 2,
diffY // 2, diffY - diffY // 2])
x = torch.cat([x2, x1], dim=1)
return self.conv(x)
class OutConv(nn.Module):
def __init__(self, in_channels, out_channels):
super(OutConv, self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1)
def forward(self, x):
return self.conv(x)
unet_model.pyファイルを作成し、次のコードを記述します。
""" Full assembly of the parts to form the complete network """
"""Refer https://github.com/milesial/Pytorch-UNet/blob/master/unet/unet_model.py"""
import torch.nn.functional as F
from .unet_parts import *
class UNet(nn.Module):
def __init__(self, n_channels, n_classes, bilinear=True):
super(UNet, self).__init__()
self.n_channels = n_channels
self.n_classes = n_classes
self.bilinear = bilinear
self.inc = DoubleConv(n_channels, 64)
self.down1 = Down(64, 128)
self.down2 = Down(128, 256)
self.down3 = Down(256, 512)
self.down4 = Down(512, 512)
self.up1 = Up(1024, 256, bilinear)
self.up2 = Up(512, 128, bilinear)
self.up3 = Up(256, 64, bilinear)
self.up4 = Up(128, 64, bilinear)
self.outc = OutConv(64, n_classes)
def forward(self, x):
x1 = self.inc(x)
x2 = self.down1(x1)
x3 = self.down2(x2)
x4 = self.down3(x3)
x5 = self.down4(x4)
x = self.up1(x5, x4)
x = self.up2(x, x3)
x = self.up3(x, x2)
x = self.up4(x, x1)
logits = self.outc(x)
return logits
if __name__ == '__main__':
net = UNet(n_channels=3, n_classes=1)
print(net)
この調整後、ネットワークの出力サイズは画像の入力サイズと同じになります。
3.アルゴリズムの選択
損失の選択は非常に重要です。損失の選択は、データのフィッティングにおけるアルゴリズムの有効性に影響を与えます。
損失の選択もタスクによって決定されます。今日のタスクでは、セルのエッジをセグメント化するだけで済みます。これは非常に単純なバイナリ分類タスクであるため、BCEWithLogitsLossを使用できます。
BCEWithLogitsLossとは何ですか?BCEWithLogitsLossは、2つのカテゴリのクロスエントロピーを計算するためにPytorchによって提供される関数です。
その式は次のとおりです。
私の機械学習シリーズのチュートリアルを見た友人は、ロジスティック回帰の損失関数であるこの式に精通している必要があります。シグモイド関数のしきい値が[0,1]であるという特徴を使用して分類します。
特定の式の導出については、私の機械学習シリーズのチュートリアル「機械学習の実用的なチュートリアル(6):ロジスティック回帰の基礎における勾配上昇アルゴリズム」を参照できます。これはここでは繰り返されません。
目的関数、つまり損失が決定されますが、この目的を最適化する方法は?
最も簡単な方法は、使い慣れた勾配降下アルゴリズムを使用して、徐々に局所的な極値に近づくことです。
しかし、この種の単純な最適化アルゴリズムは解決に時間がかかります。つまり、最適な解決策を見つけるのに手間がかかります。
さまざまな最適化アルゴリズムは、実際には勾配降下です。たとえば、最も一般的なSGDは、勾配降下に基づく改良された確率的勾配降下アルゴリズムです。勢いは、指数関数的減衰の形で履歴勾配を蓄積するために運動量SGDを導入します。
これらの最も基本的な最適化アルゴリズムに加えて、適応パラメータの最適化アルゴリズムもあります。このタイプのアルゴリズムの最大の特徴は、各パラメーターの学習率が異なることです。これらの学習率は、学習プロセス全体で自動的に調整され、収束効果が向上します。
この記事では、適応最適化アルゴリズムRMSPropを選択します。
スペースが限られているため、ここでは拡張しません。この最適化アルゴリズムだけを説明するだけでは不十分です。RMSPropはAdaGradに基づく改良版であるため、RMSPropを理解するには、まずAdaGradとは何かを知る必要があります。
RMSPropよりも高度な最適化アルゴリズムもあります。有名なAdamは、Momentum + RMSPropアルゴリズムの改訂版と見なすことができます。
要するに、初心者の場合、RMSPropがより高度な適応最適化アルゴリズムであることを知っておく必要があるだけです。
次に、UNetをトレーニングするためのコードの記述を開始し、train.pyを作成して、次のコードを記述します。
from model.unet_model import UNet
from utils.dataset import ISBI_Loader
from torch import optim
import torch.nn as nn
import torch
def train_net(net, device, data_path, epochs=40, batch_size=1, lr=0.00001):
# 加载训练集
isbi_dataset = ISBI_Loader(data_path)
train_loader = torch.utils.data.DataLoader(dataset=isbi_dataset,
batch_size=batch_size,
shuffle=True)
# 定义RMSprop算法
optimizer = optim.RMSprop(net.parameters(), lr=lr, weight_decay=1e-8, momentum=0.9)
# 定义Loss算法
criterion = nn.BCEWithLogitsLoss()
# best_loss统计,初始化为正无穷
best_loss = float('inf')
# 训练epochs次
for epoch in range(epochs):
# 训练模式
net.train()
# 按照batch_size开始训练
for image, label in train_loader:
optimizer.zero_grad()
# 将数据拷贝到device中
image = image.to(device=device, dtype=torch.float32)
label = label.to(device=device, dtype=torch.float32)
# 使用网络参数,输出预测结果
pred = net(image)
# 计算loss
loss = criterion(pred, label)
print('Loss/train', loss.item())
# 保存loss值最小的网络参数
if loss < best_loss:
best_loss = loss
torch.save(net.state_dict(), 'best_model.pth')
# 更新参数
loss.backward()
optimizer.step()
if __name__ == "__main__":
# 选择设备,有cuda用cuda,没有就用cpu
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 加载网络,图片单通道1,分类为1。
net = UNet(n_channels=1, n_classes=1)
# 将网络拷贝到deivce中
net.to(device=device)
# 指定训练集地址,开始训练
data_path = "data/train/"
train_net(net, device, data_path)
プロジェクトをより明確かつ簡潔にするために、モデルフォルダーを作成します。このフォルダーには、ネットワーク構造コードであるunet_parts.pyおよびunet_model.pyであるモデル関連のコードが含まれています。
utilsフォルダーを作成し、データ読み込みツールdataset.pyなどのツール関連のコードを配置します。
このモジュラー管理により、コードの保守性が大幅に向上します。
Train.pyはプロジェクトのルートディレクトリに配置でき、コードについて簡単に説明します。
データは30個しかないため、トレーニングセットと検証セットを分割せず、トレーニングセットの損失値が最も小さいネットワークパラメータを最適なモデルパラメータとして保存します。
問題がなければ、損失が徐々に収束していることがわかります。
4、予測
モデルがトレーニングされたら、それを使用してテストセットへの影響を確認できます。
プロジェクトのルートディレクトリにpredict.pyファイルを作成し、次のコードを記述します。
import glob
import numpy as np
import torch
import os
import cv2
from model.unet_model import UNet
if __name__ == "__main__":
# 选择设备,有cuda用cuda,没有就用cpu
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 加载网络,图片单通道,分类为1。
net = UNet(n_channels=1, n_classes=1)
# 将网络拷贝到deivce中
net.to(device=device)
# 加载模型参数
net.load_state_dict(torch.load('best_model.pth', map_location=device))
# 测试模式
net.eval()
# 读取所有图片路径
tests_path = glob.glob('data/test/*.png')
# 遍历所有图片
for test_path in tests_path:
# 保存结果地址
save_res_path = test_path.split('.')[0] + '_res.png'
# 读取图片
img = cv2.imread(test_path)
# 转为灰度图
img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# 转为batch为1,通道为1,大小为512*512的数组
img = img.reshape(1, 1, img.shape[0], img.shape[1])
# 转为tensor
img_tensor = torch.from_numpy(img)
# 将tensor拷贝到device中,只用cpu就是拷贝到cpu中,用cuda就是拷贝到cuda中。
img_tensor = img_tensor.to(device=device, dtype=torch.float32)
# 预测
pred = net(img_tensor)
# 提取结果
pred = np.array(pred.data.cpu()[0])[0]
# 处理结果
pred[pred >= 0.5] = 255
pred[pred < 0.5] = 0
# 保存图片
cv2.imwrite(save_res_path, pred)
実行後、データ/テストディレクトリで予測結果を確認できます。
それでおしまい!
ファイブ、ついに
- この記事では、主にモデルのトレーニングの3つのステップ、データの読み込み、モデルの選択、アルゴリズムの選択について説明します。
- これは単純な例です。通常のビジョンタスクのトレーニングははるかに複雑です。例:モデルをトレーニングするとき、検証セットのモデルの精度に応じて保存するモデルを選択する必要があります。損失の収束などの観察を容易にするためにテンソルボードをサポートする必要があります。
気に入って読んで、癖をつけて、WeChat公式アカウントで検索【JackCui-AI】インターネットを這うストーカーをフォロー