Pytorch相关笔记
nn.linear()
- nn.linear:输入张量的形状: [batch_size, in_features]
输出张量的形状: [batch_size, out_features]
输入 x (1,4): A的转置 (4,2): 输出 (1,2):
[ 1 2 3 4 ] × [ 0.1 0.2 ] [ 3.0 2.6 ]
[ 0.2 0.3 ]
[ 0.3 0.2 ]
[ 0.4 0.3 ]
nn.MSELoss()均方差损失**(Mean Squared Error Loss)**
\text{MSELoss}(x, y) = \frac{1}{N} \sum_{i=1}^{N} (x_i - y_i)^2
x_i\ :\text{模型预测值,第}\ i\ \text{个样本或像素点的输出}
y_i\ :\text{真实标签值,第}\ i\ \text{个样本的 ground truth}
N\ :\text{总共参与计算的元素数量(例如像素总数或样本数)}
nn.CrossEntropyLoss()交叉熵损失
理解:在交叉熵损失计算过程中真实标注值,只是用来提供索引,为的是获取预测结果在真实索引位置计算得到的概率值,交叉熵损失会对每一个像素点计算其为所有类别的概率,具体来说先是对预测结果的值进行softmax(),softmax其实就是把所有预测值进行概率分布取值范围(0-1),然后根据标签提供的索引值取真实类别的预测概率进行log()运算,生成结果范围为(0,+无穷),概率越大值越小。
假设有3个类别批次只有一张图,预测输出为 [2.0, 0.5, 0.3], 真实值为类别0
✅ 一、输入与输出
参数 | 类型 | 形状(Shape) | 说明 |
---|---|---|---|
input |
Tensor |
[N, C] / [N, C, H, W] |
网络输出的 logits,未经过 softmax |
target |
LongTensor |
[N] / [N, H, W] |
每个样本/像素的真实标签(整数类别索引) |
输出 | Tensor |
标量 /[N] / [N, H, W] |
损失值,取决于 reduction 设置 |
✅ 二、内部计算机制
CrossEntropyLoss = log_softmax + NLLLoss
具体步骤如下:
- 对
input
进行log_softmax
- 对每个样本,仅取其真实类别的
log(p_t)
- 取负、聚合
Tensor.transpose(b, c , h , w )维度转换理解
-
必要性:e.g.计算语义分割损失
-
目的:实现模型维度统一计算交叉熵损失。
-
参数理解:
n, c, h, w = inputs.size()
,(n,c,h,w)是模型推理输出维度,n是batch size,c是类别数,h高,w宽。其中底层值记录的是在坐标(h,w)位置该点为类别c的概率值为0-1。因此以通道判定类别。nt, ht, wt = target.size()
,(nt,ht,wt)为标标签gt。同样的nt为batch size,ht,ht,wt,为像素宽高,真实底层值表示在坐标(h,w)位置的真实类别数字,比如类别1则记为int 1 类别2 记为 int 2 ...... 为交叉熵损失提供真实类别索引。
def Focal_Loss(inputs, target, cls_weights, num_classes=21, alpha=0.5, gamma=2):
n, c, h, w = inputs.size()
nt, ht, wt = target.size()
if h != ht and w != wt:
inputs = F.interpolate(inputs, size=(ht, wt), mode="bilinear", align_corners=True)
temp_inputs = inputs.transpose(1, 2).transpose(2, 3).contiguous().view(-1, c)
temp_target = target.view(-1)
logpt = -nn.CrossEntropyLoss(weight=cls_weights, ignore_index=num_classes, reduction='none')(temp_inputs, temp_target)
pt = torch.exp(logpt)
if alpha is not None:
logpt *= alpha
loss = -((1 - pt) ** gamma) * logpt
loss = loss.mean()
return loss
- 维度转化:以上代码的维度转换为了提供CrossEntropyLoss 的输入,以像素坐标为样本,把模型输出摊成[n,c]二维向量,把真值索引摊成[n]一维向量。
- 维度转换的本质理解:在(b,c,h,w)-->(b,h,w,c)转换过程中,nn.transpose()和 nn.permute()函数不要进行抽象空间转换理解。可以把所有数值想象记录成多个三维长方体堆叠的四维数组,在坐标转换时,本质是对数组的读取顺序变更。如(b,c,h,w)应当理解成按照批次b,对各个批次的的c通道,依次提取高度为h时的升序w位置的值。而(b,h,w,c)所进行的所谓空间转换,不改变底层的各个数值排列顺序,而是按照(b,h,w,c)的顺序对底层数值依次读取。如(b,h,w,c)指的是按批次对各个批次在h截面,依次对宽度为w时,从小到大升序读取各通道所对应的值。本质上是对固定的多维空间数值序列,添加读取的序列。
torch.utils.data
DataLoader
- 用法:'from torch.utils.data import DataLoader'.
- 例子:
gen = DataLoader(train_dataset, shuffle = shuffle, batch_size = batch_size, num_workers = num_workers, pin_memory=True,
drop_last = True, collate_fn = unet_dataset_collate, sampler=train_sampler,
worker_init_fn=partial(worker_init_fn, rank=rank, seed=seed))
数据加载逻辑
┌───────────────┐
│ Dataset类 │ ← 自定义 UnetDataset
│ __getitem__() │ ← 定义如何读取单个样本
│ __len__() │ ← 返回样本总数
└──────┬────────┘
↓
┌──────────────────────────────┐
│ DataLoader(dataset, ...) │
│ 自动调用 Dataset[索引] │
│ 自动 batch 拼接样本 │
│ 使用 collate_fn 合并格式 │
└────────────┬─────────────────┘
↓
for batch in DataLoader:
训练 or 验证
- 自定义数据集类
train_dataset = UnetDataset(train_lines, input_shape, num_classes, True, VOCdevkit_path)
- 定义数据集处理方式,与 getitem() 样本返回。dataloader会根据batchsize的值读取train_dataset[1,2,..]样本,train_dataset根据UnetDataset.getitem()依次返回自定义格式的样本。
class UnetDataset(Dataset):
def __init__(self, annotation_lines, input_shape, num_classes, train, dataset_path):
super(UnetDataset, self).__init__()
self.annotation_lines = annotation_lines
self.length = len(annotation_lines)
self.input_shape = input_shape
self.num_classes = num_classes
self.train = train
self.dataset_path = dataset_path
def __len__(self):
return self.length
def __getitem__(self, index):
annotation_line = self.annotation_lines[index]
name = annotation_line.split()[0]
#-------------------------------#
# 从文件中读取图像
#-------------------------------#
jpg = Image.open(os.path.join(os.path.join(self.dataset_path, "VOC2007/JPEGImages"), name + ".jpg"))
png = Image.open(os.path.join(os.path.join(self.dataset_path, "VOC2007/SegmentationClass"), name + ".png"))
#-------------------------------#
# 数据增强
#-------------------------------#
jpg, png = self.get_random_data(jpg, png, self.input_shape, random = self.train)
jpg = np.transpose(preprocess_input(np.array(jpg, np.float64)), [2,0,1])
png = np.array(png)
png[png >= self.num_classes] = self.num_classes
#-------------------------------------------------------#
# 转化成one_hot的形式
# 在这里需要+1是因为voc数据集有些标签具有白边部分
# 我们需要将白边部分进行忽略,+1的目的是方便忽略。
#-------------------------------------------------------#
seg_labels = np.eye(self.num_classes + 1)[png.reshape([-1])]
seg_labels = seg_labels.reshape((int(self.input_shape[0]), int(self.input_shape[1]), self.num_classes + 1))
return jpg, png, seg_labels
def rand(self, a=0, b=1):
return np.random.rand() * (b - a) + a
def get_random_data(self, image, label, input_shape, jitter=.3, hue=.1, sat=0.7, val=0.3, random=True):
image = cvtColor(image)
label = Image.fromarray(np.array(label))
#------------------------------#
# 获得图像的高宽与目标高宽
#------------------------------#
iw, ih = image.size
h, w = input_shape
if not random:
iw, ih = image.size
scale = min(w/iw, h/ih)
nw = int(iw*scale)
nh = int(ih*scale)
image = image.resize((nw,nh), Image.BICUBIC)
new_image = Image.new('RGB', [w, h], (128,128,128))
new_image.paste(image, ((w-nw)//2, (h-nh)//2))
label = label.resize((nw,nh), Image.NEAREST)
new_label = Image.new('L', [w, h], (0))
new_label.paste(label, ((w-nw)//2, (h-nh)//2))
return new_image, new_label
#------------------------------------------#
# 对图像进行缩放并且进行长和宽的扭曲
#------------------------------------------#
new_ar = iw/ih * self.rand(1-jitter,1+jitter) / self.rand(1-jitter,1+jitter)
scale = self.rand(0.25, 2)
if new_ar < 1:
nh = int(scale*h)
nw = int(nh*new_ar)
else:
nw = int(scale*w)
nh = int(nw/new_ar)
image = image.resize((nw,nh), Image.BICUBIC)
label = label.resize((nw,nh), Image.NEAREST)
#------------------------------------------#
# 翻转图像
#------------------------------------------#
flip = self.rand()<.5
if flip:
image = image.transpose(Image.FLIP_LEFT_RIGHT)
label = label.transpose(Image.FLIP_LEFT_RIGHT)
#------------------------------------------#
# 将图像多余的部分加上灰条
#------------------------------------------#
dx = int(self.rand(0, w-nw))
dy = int(self.rand(0, h-nh))
new_image = Image.new('RGB', (w,h), (128,128,128))
new_label = Image.new('L', (w,h), (0))
new_image.paste(image, (dx, dy))
new_label.paste(label, (dx, dy))
image = new_image
label = new_label
image_data = np.array(image, np.uint8)
#---------------------------------#
# 对图像进行色域变换
# 计算色域变换的参数
#---------------------------------#
r = np.random.uniform(-1, 1, 3) * [hue, sat, val] + 1
#---------------------------------#
# 将图像转到HSV上
#---------------------------------#
hue, sat, val = cv2.split(cv2.cvtColor(image_data, cv2.COLOR_RGB2HSV))
dtype = image_data.dtype
#---------------------------------#
# 应用变换
#---------------------------------#
x = np.arange(0, 256, dtype=r.dtype)
lut_hue = ((x * r[0]) % 180).astype(dtype)
lut_sat = np.clip(x * r[1], 0, 255).astype(dtype)
lut_val = np.clip(x * r[2], 0, 255).astype(dtype)
image_data = cv2.merge((cv2.LUT(hue, lut_hue), cv2.LUT(sat, lut_sat), cv2.LUT(val, lut_val)))
image_data = cv2.cvtColor(image_data, cv2.COLOR_HSV2RGB)
return image_data, label
3.样本的批量整理:DataLoader(..., collate_fn=unet_dataset_collate), 在取得UnetDataset.getitem()中返回的各个样本后,需要根据自定义的整理函数collate_fn对该批次数据进行整理,以符合Pytorch的的训练要求。这一部分DataLoader在取得各个样本后会自动调用collate_fn函数对数据进行批次整理。转换成tensor格式
# DataLoader中collate_fn使用
def unet_dataset_collate(batch):
images = []
pngs = []
seg_labels = []
for img, png, labels in batch:
images.append(img)
pngs.append(png)
seg_labels.append(labels)
images = torch.from_numpy(np.array(images)).type(torch.FloatTensor)
pngs = torch.from_numpy(np.array(pngs)).long()
seg_labels = torch.from_numpy(np.array(seg_labels)).type(torch.FloatTensor)
return images, pngs, seg_labels
torch.nn.functional
- 用法:
import torch.nn.functional as F
F.interpolate()
- 插值对齐函数,常用来对齐模型输出与需要匹配的标签gt值:e.g.
n, c, h, w = inputs.size()
nt, ht, wt = target.size()
if h != ht and w != wt:
inputs = F.interpolate(inputs, size=(ht, wt), mode="bilinear", align_corners=True)
numpy相关笔记
np.bincount()
- 常用来计算模型推理结果与真实值之间的匹配程度e.g.
- a:表示输入flatten后的一维输入真值,b为flatten后的一维预测值
- np.bincount(n * a[k].astype(int) + b[k], minlength=n ** 2)计算的是根据公式na[k]+b[k]得到的一维数组按照0-nn的索引进行依次匹配,统计相同值总数,比如当前索引值为0则从na[k]+b[k]数组中统计0出现的次数,然后依次统计1,2,3...nn然后reshape成n*n的二维数组,这样就可以很方便的计算当a[k]=x,b[k]=y时的值所出现的次数。
def fast_hist(a, b, n):
#--------------------------------------------------------------------------------#
# a是转化成一维数组的标签,形状(H×W,);b是转化成一维数组的预测结果,形状(H×W,)
#--------------------------------------------------------------------------------#
k = (a >= 0) & (a < n)
#--------------------------------------------------------------------------------#
# np.bincount计算了从0到n**2-1这n**2个数中每个数出现的次数,返回值形状(n, n)
# 返回中,写对角线上的为分类正确的像素点
#--------------------------------------------------------------------------------#
return np.bincount(n * a[k].astype(int) + b[k], minlength=n ** 2).reshape(n, n)
.......
hist += fast_hist(label.flatten(), pred.flatten(), num_classes)