赞
踩
Pytorch提供了Dataset和Data Loader来帮助处理数据集。对于语义分割的训练数据,假设我们已经有了原始图像及对应的标签图像。为了训练网络模型,我们需要原始图像的Tensor数据,形状为(N,C, H, W)。其中N为样本数量,C为通道数量,H和W表示图像的像素高度和宽度。同时需要保证所有训练图像具有相同的C,H和W。标签数据可以使用(N,H,W)形状的标准标签,或者使用one-hot encoding标签。
要在pytorch中实现自定义数据集,我们可以继承pytorch的Dataset,并实现__len__(用来返回数据数量)和__getItem__(用来返回图像数据和标签数据)方法。这里我们在训练数据集目录下做一个npy文件,用来统计所有训练数据。npy文件的形状为(N, 2), N代表样本数量,2代表图像和标签。
我们生成1.jpg, 1.png, 直到237.jpg,237.png的237个训练样本的py文件数据
data = np.array([[f'raw_data/{x}.jpg', f'groundtruth/{x}.png'] for x in range(1, 237)])
root = os.getcwd() + "/final"
np.save(f"{root}/index.npy", data)
colors数据定义标签图像中,不同分类的颜色值
colors = torch.tensor([[0, 0, 0], # background
[128, 0, 0], # cat
[0, 128, 0]] # cow
)
接下来,我们定义一个自定义Dataset,读取图片我们使用的torchvision库,并指定返回RGB格式的数据。这里有一些细节需要我们注意下:
class SSDataset(Dataset): def __init__(self, root: str, colors: Tensor, transform=None): self.root = root self.colors = colors self.transform = transform self.data_list = np.load(f'{root}/index.npy') def __len__(self): return len(self.data_list) def __getitem__(self, index) -> T_co: names = self.data_list[index] image_file_path = f'{self.root}/{names[0]}' mask_file_path = f'{self.root}/{names[1]}' image = torchvision.io.read_image(image_file_path, ImageReadMode.RGB) mask = torchvision.io.read_image(mask_file_path, ImageReadMode.RGB) if self.transform is not None: image = self.transform(image) mask = self.transform(mask) mask = OneHotEncoder.encode_mask_image(mask, self.colors) return image, mask
###3.2.2、one-hot编码
直接读取的标签图像数据,是无法用于损失计算的。我们需要对它进行编码,编码的结果可以是典型的(H,W)形状的标签数据,或者采用one-hot编码。
one-hot编码能够把分类数据(cow, cat and etc.)编码成计算机可识别的二值数据, 它的最大优势是,编码后的分类数据是公平的,有利于计算机对离散分类数据的处理工作。对于语义分割,我们采用one-hot编码, 好处是如果我们需要对分割后的对象做对象检测,实例边框绘制等操作,one-hot编码的数据更加容易实现。
我们的one-hot编码实现了对图像的tensor数据和score进行one-hot编码的功能.对score进行one-hot编码的功能将用于模型训练和验证。
class OneHotEncoder():
@staticmethod
def encode_mask_image(mask_image: Tensor, colors: Tensor) -> Tensor:
height, width = mask_image.shape[1:]
one_hot_mask = torch.zeros([len(colors), height, width], dtype=torch.float)
for label_index, label in enumerate(colors):
one_hot_mask[label_index, :, :] = torch.all(mask_image == label[:, None, None], dim=0).float()
return one_hot_mask
@staticmethod
def encode_score(score: Tensor) -> Tensor:
num_classes = score.shape[1]
label = torch.argmax(score, dim=1)
pred = F.one_hot(label, num_classes=num_classes)
return pred.permute(0, 3, 1, 2)
原始的图像数据,并不总是能符合数据处理需求的标准,我们使用的不同的人工智能框架,也对应了不同类型和要求的输入数据格式, 所以我们需要对数据进行预处理。数据预处理是一个功能庞大的概念,在本文中,我们主要使用数据预处理中的数据变换功能,处理时机被分为两个阶段:图像加载阶段和数据处理阶段。
在图像加载阶段,我们面临了读取的图片的大小,长宽比例,图片格式不同,以及不同的图像处理库带来的数据差异等问题。对应的需要做缩放,裁减,格式转换,维度变换等一些列预处理操作。在上面数据集的代码中,我们预留了转换参数 transform, 可以根据需要插入多项预处理程序。
torchvision.transforms包中已经包含了大量预置的预处理程序,我们也可以定制自己的预处理程序. 如果使用的图片处理库时PIL或者opencv, torchvision的tranform包,提供了对PIL image或ndarray转换成Tensor的ToTensor转换,方便我们应对不同类型的数据。这里我们以图片缩放和长宽比统一为例,加入Resize和CenterCrop两个预处理程序。
transform = T.Compose([
T.Resize(224),
T.CenterCrop(224)
])
ss_dataset = SSDataset(root, colors, transform)
经过上述预处理程序,dataset返回的数据都将被裁减成224*224大小的数据。
在数据处理阶段,我们需要对数据进行归一化和正态分布,以便提升收敛速度。torchvison.transorm,提供了正态分布的预处理程序Normalize,其中需要均方差(mean)和标准差(std)两个参数。我们有两种方式来使用这个处理程序:
这里我们使用第一种方式,归一化和正态分布独立成两个过程。Torchvisontion提供的其它格式图像数据转换成Tensor的ToTensor方法,内部做了归一化处理,我们当然可以先把我们的Tensor转换成(H,W,C)格式的ndarray再使用这个处理程序进行归一化, 但这样的操作显然不太优雅。 这里我们直接使用颜色的最大值255作为除数,直接对Tensor数据进行归一化处理。均方差和标准差我们使用imagenet提供的数值:
transform = T.Compose([
T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
…
input = transform(input/255.)
数据处理阶段的预处理程序,我们选择提到dataset外面,在训练之前使用。那么在测试,验证和预测之前,需要注意必须要使用同样的数据预处理程序来对数据做预处理。
pytorch 使用数据加载器来(DataLoader)来间接使用数据集。通常完整的数据集会被份组成训练集,验证集和测试集三部分,为了简化,我们把数据集中的数据按0.8, 0.2的比例,拆分成训练集和验证集二部分。
data_len = len(ss_dataset) indices = list(range(data_len)) split = int(data_len * 0.8) train_indices, val_indices = indices[:split], indices[split:] train_data = Subset(ss_dataset, train_indices) val_data = Subset(ss_dataset, val_indices) BATCH_SIZE = 4 train_loader = DataLoader( train_data, BATCH_SIZE, shuffle=True, drop_last=True, num_workers=4, pin_memory=True, ) val_loader = DataLoader( val_data, 1, shuffle=False, drop_last=True, num_workers=2, pin_memory=False, )
拆分了训练集和验证集的数据集,使得用于训练和验证的数据规模变得更小,不利于模型训练。交叉验证把样本数据分成k份, 称为k fold,选择其中一份用作验证,其它用作训练。之后再选择另一份用作验证,如此循环指定的次数或者全部fold都被作为验证集训练过。
交叉验证通常在样本数量较少时有用,当有大量样本时,通常不做交叉验证。
至此,我们已经准备好了网络模型,数据集,并且使用数据加载器对数据集进行了拆分。接下来,我们使用数据集对网络模型进行训练,用以获取理想的权重值。
为了训练网络模型,我们需要关注已下几个方面:学习准则,优化和指标
学习准则是模型如何学习的评估标准。对于语义分割网络,我们的学习准则需要能够对多个分类进行损失评估,常用的损失函数有Softmax, SVM , CrossEntropy。这里我们选用CrossEntropyLoss作为学习准则, 并在前向计算后通过学习准则计算损失。
Pytorch提供了CrossEntropyLoss的实现,是Softmax和CrossEntropy的组合,其内部实现首先使用Softmax把打分转换成不同分类的概率,再使用log函数把概率转换成熵值,最后使用NLLLoss计算损失值。
criterion = nn.CrossEntropyLoss()
…
# forward
score = model(input)
loss = criterion(score, target)
train_loss += loss.item()
优化是通过学习准则,优化模型参数,使得学习准则中的损失可以随着学习达到降低的效果。常用的优化函数有SGD,Adam。
这里我们选用SGD作为优化函数,配合动量来避免局部最优解, 并在反向传播时优化模型权重值。SGD通过backward来计算梯度,并调整权重值。对于权重参数,pytorch要求参数必须是requires_grad=True且is_leaf=False才会计算梯度。在权重初始化章节,我们的初始化方式,默认已经正确设置了这两个属性。另外,在模型训练上,我们通常先使用一个较大的学习率,用以加快收敛速度,之后使用较小的学习率,在小范围内寻找最优权重值。为此我们使用了lr_scheduler,用以在训练过程中动态降低学习率。
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum)
scheduler = lr_scheduler.StepLR(optimizer, step_size=Trainer.STEP_SIZE, gamma=0.5)
…
# backward
optimizer.zero_grad()
loss.backward()
optimizer.step()
…
scheduler.step()
在模型训练,验证和测试阶段,我们需要获取一些指标参数,用来反馈结果是否理想。常见的指标有Loss,Precision,Recall, F Measure, Pixel Accuracy, Mean Accuracy, Mean IU,Frequency Weighted IU, Jaccard Similarity等。
# metrics
pred = OneHotEncoder.encode_score(score)
cm = Trainer.confusion_matrix(target, pred)
acc = torch.diag(cm).sum().item() / torch.sum(cm).item()
train_acc += acc
iu = torch.diag(cm) / (cm.sum(dim=1) + cm.sum(dim=0) - torch.diag(cm))
mean_iu += torch.nanmean(iu).item()
if verbose and iteration % iterations_per_epoch == 0:
print(f'epoch {epoch + 1} / {epochs}: loss: {train_loss/iterations_per_epoch:.5f}, accuracy:{train_acc/iterations_per_epoch:.5f}, mean IU:{mean_iu/iterations_per_epoch:.5f}')
train_loss = 0.0
train_acc = 0.0
mean_iu = 0.0
Confusion Matrix可以辅助计算诸如Precision, Recall, F Measure等多个指标值。这里我们使用Confusion Matrix来计算Pixel Accuracy和Mean IU。
@staticmethod def confusion_matrix(target: Tensor, input: Tensor) -> Tensor: if target.dim() != input.dim(): raise IOError('target and input must has same dimension') if 4 == target.dim(): y_true = torch.flatten(target.permute(1, 0, 2, 3), 1, 3).int() y_pred = torch.flatten(input.permute(1, 0, 2, 3), 1, 3).int() elif 3 == target.dim(): y_true = torch.flatten(target, 1, 2).int() y_pred = torch.flatten(input, 1, 2).int() else: raise IOError('target and input must be a 3D or 4D matrix') n_classes = y_true.shape[0] cm = torch.zeros((n_classes, n_classes)) for i in range(n_classes): for j in range(n_classes): num = torch.sum((y_true[i] & y_pred[j]).int()) cm[i, j] += num return cm
现在,数据读取,学习准则,优化和指标结合起来的训练代码是这样子的:
class Trainer(object): def __init__(self, model: torch.nn.Module, transform, train_loader: DataLoader, val_loader: DataLoader, class_names, class_colors): self.model = model self.transform = transform self.visualizer = Visualizer(class_names, class_colors) self.acc_thresholds = 0.95 self.best_mean_iu = 0 self.train_loader = train_loader self.val_loader = val_loader def train(self, epochs=50, learning_rate=1e-3, momentum=0.7, step_size=2, verbose=True): start_time = datetime.now() print(f'start training at {start_time}') iterations_per_epoch = 20 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') optimizer = torch.optim.SGD(self.model.parameters(), lr=learning_rate, momentum=momentum) scheduler = lr_scheduler.StepLR(optimizer, step_size=step_size, gamma=0.5) criterion = nn.CrossEntropyLoss() train_loss = 0.0 train_acc = 0.0 train_iu = 0.0 data_len = len(train_loader) self.model.to(device) self.model.train() for epoch in range(epochs): lr_current = optimizer.param_groups[0]['lr'] print(f'learning rate:{lr_current}') for batch_index, data in enumerate(self.train_loader): iteration = batch_index + epoch * data_len + 1 input = Variable(self.transform(data[0].float()/255).to(device)) target = data[1].float().to(device) # forward score = self.model(input) loss = criterion(score, target) train_loss += loss.item() # backward optimizer.zero_grad() loss.backward() optimizer.step() # metrics pred = OneHotEncoder.encode_score(score) cm = Trainer.confusion_matrix(target, pred) acc = torch.diag(cm).sum().item() / torch.sum(cm).item() train_acc += acc iu = torch.diag(cm) / (cm.sum(dim=1) + cm.sum(dim=0) - torch.diag(cm)) train_iu += torch.nanmean(iu).item() if verbose and iteration % iterations_per_epoch == 0: mean_acc = train_acc/iterations_per_epoch mean_iu = train_iu/iterations_per_epoch print(f'epoch {epoch + 1} / {epochs}: loss: {train_loss/iterations_per_epoch:.5f}, accuracy:{mean_acc:.5f}, mean IU:{mean_iu:.5f}') train_loss = 0.0 train_acc = 0.0 train_iu = 0.0 scheduler.step() end_time = datetime.now() print(f'end training at {end_time}') @staticmethod def confusion_matrix(target: Tensor, input: Tensor) -> Tensor if target.dim() != input.dim(): raise IOError('target and input must has same dimension') if 4 == target.dim(): y_true = torch.flatten(target.permute(1, 0, 2, 3), 1, 3).int() y_pred = torch.flatten(input.permute(1, 0, 2, 3), 1, 3).int() elif 3 == target.dim(): y_true = torch.flatten(target, 1, 2).int() y_pred = torch.flatten(input, 1, 2).int() else: raise IOError('target and input must be a 3D or 4D matrix') n_classes = y_true.shape[0] cm = torch.zeros((n_classes, n_classes)) for i in range(n_classes): for j in range(n_classes): num = torch.sum((y_true[i] & y_pred[j]).int()) cm[i, j] += num return cm … }
这里需要注意的地方是从DataLoader取到的数据,除了预处理程序,我们还对输入数据和标签数据做了类型转换,这是因为我们使用的网络模型,优化器,对输入数据是有类型要求的。这里我们使用的网络模型和损失函数需要输入是float类型的数据,所以通过DataLoader获取的数据,我们都进行了类型转换。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。