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

具体步骤如下:

  1. input 进行 log_softmax
  2. 对每个样本,仅取其真实类别的 log(p_t)
  3. 取负、聚合

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 验证
  1. 自定义数据集类
train_dataset   = UnetDataset(train_lines, input_shape, num_classes, True, VOCdevkit_path)
  1. 定义数据集处理方式,与 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)