Интерпретация кода FCN

Оглавление

Инициализация модели

Инициализация ВГГ

Инициализация FCN 

Предварительная обработка изображения

Обработка изображения

кодирование изображения

Расчет соответствующих параметров

обучение модели

 небольшая проблема

полный код

ссылка


Недавно я провел небольшое исследование соответствующих знаний о сегментации изображения в поле изображения и обнаружил, что вода все еще довольно глубока, потому что FCN является почти создателем поля, поэтому я начну сначала с этого аспекта. О теории много говорить не буду, много искал в интернете, в основном для анализа кодовой части.

Инициализация модели

Как мы все знаем, вторая половина FCN новая, а первая половина вообще пересаживается с других моделей.Здесь я выбираю структуру модели Vgg16. Поэтому инициализация модели делится на два этапа: сначала инициализация сети Vgg, а затем инициализация сети Fcn.

Инициализация ВГГ

 Здесь для удобства выбора Vgg несколько разных Vgg инкапсулированы в список, а число представляет собой количество выходных каналов после свертки.Количество входных каналов свертки - это количество выходных каналов предыдущего.M представляет слой пула Vgg использует слой свертки с ядром свертки 3 и ядро ​​слоя пула с размером 2, поэтому эти два параметра известны и не нуждаются в маркировке.

Следует также отметить, что поскольку Fcn является полностью сверточной сетью, последний полносвязный слой не нужен, поэтому он удаляется.

# Vgg网络结构配置(数字代表经过卷积后的channel数,‘M’代表池化层)
cfg = {
    'vgg11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'vgg13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'vgg16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
    'vgg19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}

# 由cfg构建vgg-Net的卷积层和池化层(block1-block5)
def make_layers(cfg, batch_norm=False):
    layers = []
    in_channels = 3  # RGB初始值
    for v in cfg:
        if v == 'M':  # 池化层
            layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
        else:
            conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
            if batch_norm:  # 是否需要归一化
                layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
            else:
                layers += [conv2d, nn.ReLU(inplace=True)]
            in_channels = v  # 这一层输出的通道数就是下一层输入的通道数
    return nn.Sequential(*layers)

# 下面开始构建VGGnet
class VGGNet(VGG):
    def __init__(self, pretrained=True, model='vgg16', requires_grad=True, remove_fc=True, show_params=False):
        super().__init__(make_layers(cfg[model]))
        self.ranges = ranges[model]  # ranges是一个字典,键是model名字,后面的是池化层的信息

        # 获取VGG模型训练好的参数,并加载(第一次执行需要下载一段时间)
        if pretrained:
            exec("self.load_state_dict(models.%s(pretrained=True).state_dict())" % model)

        # 屏蔽预训练模型的权重,只训练最后一层的全连接的权重,因为fcn模型是建立在vgg16基础上训练的,所以前面训练好的VGG网络不修改
        if not requires_grad:
            for param in super().parameters():
                param.requires_grad = False

        # 去掉vgg最后的全连接层(classifier)
        if remove_fc:
            del self.classifier

        # 打印网络的结构
        if show_params == True:
            for name, param in self.named_parameters():
                print(name, param.size())

    def forward(self, x):
        output = {}
        # 利用之前定义的ranges获取每个max-pooling层输出的特征图,这个主要是FCN32的上采样要用到
        for idx, (begin, end) in enumerate(self.ranges):  # enumerate用于枚举,同时给出元素和下标
            # self.ranges = ((0, 5), (5, 10), (10, 17), (17, 24), (24, 31)) (vgg16 examples)
            for layer in range(begin, end):
                x = self.features[layer](x)
                # 相当于把x矩阵放进layer层,然后得到输出,0-5代表第一个max-pool需要经过的层数,所以x1实际上就是第一个max-pool层输出
            output["x%d" % (idx + 1)] = x
            # x数字越大越深
        # output 为一个字典键x1d对应第一个max-pooling输出的特征图,x2...x5类推
        return output

Fcn8s необходимо объединить информацию первых трех слоев пула, поэтому необходимо записать информацию слоя пула модели Vgg.Перейдите к последнему слою пула, сохраните результат в словаре после завершения и сохраните информацию нескольких объединение слоев в окончательный вывод (потому что каждый раз он заканчивается объединяющим слоем). 

Инициализация FCN 

Затем речь идет об инициализации сети FCN. Под FCN имеются FCN32, FCN16 и FCN8, как показано на рисунке ниже:

Это FCN8, потому что информация слоя пула разной глубины объединяется, поэтому обработка краев будет более гладкой, чем прямой вывод, потому что уровень мелкой абстракции часто лучше понимает детали. Но автор также сказал, что дело не в том, что чем больше слияние, тем лучше.По сравнению с Fcn4s, точность не сильно улучшается, поэтому этого достаточно, поэтому следующие будут непосредственно делать Fcn8s.

# 下面由VGG构建FCN8s
class FCN8s(nn.Module):

    def __init__(self, pretrained_net, n_class):
        super().__init__()
        # 定义可能会用到的东西
        self.n_class = n_class
        self.pretrained_net = pretrained_net
        self.conv6 = nn.Conv2d(512, 512, kernel_size=1, stride=1, padding=0, dilation=1)
        self.conv7 = nn.Conv2d(512, 512, kernel_size=1, stride=1, padding=0, dilation=1)  # 卷积核大小是1,本质上是全连接层
        # 这里写两个一样的可能是为了写出前后关系的感觉?
        self.relu = nn.ReLU(inplace=True)
        self.deconv1 = nn.ConvTranspose2d(512, 512, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn1 = nn.BatchNorm2d(512)
        self.deconv2 = nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn2 = nn.BatchNorm2d(256)
        self.deconv3 = nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn3 = nn.BatchNorm2d(128)
        self.deconv4 = nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn4 = nn.BatchNorm2d(64)
        self.deconv5 = nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn5 = nn.BatchNorm2d(32)
        self.classifier = nn.Conv2d(32, n_class, kernel_size=1)

    def forward(self, x):
        output = self.pretrained_net(x)
        # 这个已经在前面的forward中初始化了,里面已经存储了相关特征图
        x5 = output['x5']  # max-pooling5的feature map (1/32)  5*5,160/32
        # print(x5.size())
        x4 = output['x4']  # max-pooling4的feature map (1/16)
        x3 = output['x3']  # max-pooling3的feature map (1/8)

        # 所以总结一下FCN里面的几个合成的步骤也就是反卷积->激活->标准化->加上前面的pool层继续

        score = self.relu(self.conv6(x5))  # conv6  size不变 (1/32)
        # score = self.relu(self.conv7(score))  # conv7  size不变 (1/32)

        # 这里我尝试把右边括号里的x5改成了score
        score = self.relu(self.deconv1(score))  # out_size = 2*in_size (1/16)
        # print(score.size())  # 反卷积之后变为两倍
        score = self.bn1(score + x4)  # bn是标准化,表示加x4第二池化层的结果一同进行计算
        score = self.relu(self.deconv2(score))  # out_size = 2*in_size (1/8)

        score = self.bn2(score + x3)

        # 到这里为止就是全部的FCN步骤,接下来是反卷积到原尺寸

        # 此时是1/8,然后继续反卷积,每次扩大两倍边长直到最后和原图一样
        score = self.bn3(self.relu(self.deconv3(score)))  # out_size = 2*in_size (1/4),反卷积后标准化
        score = self.bn4(self.relu(self.deconv4(score)))  # out_size = 2*in_size (1/2)
        score = self.bn5(self.relu(self.deconv5(score)))  # out_size = 2*in_size (1)
        score = self.classifier(score)  # size不变,使输出的channel等于类别数,相当于对每个点分类
        return score

Поскольку код взят у других блоггеров, я также столкнулся с некоторыми проблемами в процессе чтения.Исходный код обрабатывает оценку следующим образом, но вы можете видеть, что после того, как первое и второе предложения были обработаны для оценки, третье предложение также обрабатывается. Оценка переназначается, что это значит? Первые два предложения недействительны, что тоже меня смущает. Позже я также ссылаюсь на исходный код github, на который ссылается этот блогер. Я думаю, он должен поставить обработанная оценка Зайдите и продолжите обработку, и она станет такой, как указано выше.

score = self.relu(self.conv6(x5))    # conv6  size不变 (1/32)
score = self.relu(self.conv7(score)) # conv7  size不变 (1/32)
score = self.relu(self.deconv1(x5))   # out_size = 2*in_size (1/16)       
score = self.bn1(score + x4)                      
score = self.relu(self.deconv2(score)) # out_size = 2*in_size (1/8)           
score = self.bn2(score + x3)                      
score = self.bn3(self.relu(self.deconv3(score)))  # out_size = 2*in_size (1/4)
score = self.bn4(self.relu(self.deconv4(score)))  # out_size = 2*in_size (1/2)
score = self.bn5(self.relu(self.deconv5(score)))  # out_size = 2*in_size (1)
score = self.classifier(score)                    # size不变,使输出的channel等于类别数

Каждый размер пула будет уменьшен вдвое, а каждая последующая деконволюция означает, что размер будет удваиваться, поэтому обработка в конечном итоге станет исходным размером.

Предварительная обработка изображения

Следующим шагом является предварительная обработка обучающих изображений, включая обработку изображений и кодирование изображений.

Обработка изображения

Обработка самого изображения в основном включает в себя преобразование размеров, стандартизацию и упаковку и т.д., которые в основном выполняются через библиотечные функции, поэтому много говорить не буду. 

кодирование изображения

Кодирование является относительно громоздким, и требуется однократное кодирование, поскольку могут использоваться вычисления функции потерь.

Однократное кодирование должно открыть n позиций, которые равны 1 в соответствующем измерении и 0 в остальных . Например, пол может быть мужской/женский, первым является мужской, а вторым — женский, тогда для человека его пол может быть мужским, код 10, или женским, код 01; и предположим, что национальность может быть китайская/американская. /японский, то код национальности человека может быть 100 010 001 (Китай, США, Япония), то есть у одного всегда 1, у других 0, а тот, что 1, соответствует стране, к которой он принадлежит. Категории здесь также аналогичны, если предположить, что есть два пикселя, каждый пиксель либо 01, что относится к первой категории, либо 10, что относится ко второй категории.

Горячее кодирование выглядит следующим образом:

def onehot(data, n):
    buf = np.zeros(data.shape + (n,))  # 相当于给每一个像素开辟一个维度,除了他其他都是其他
    nmsk = np.arange(data.size) * n + data.ravel()  # revel表示展平多维数组,就是flatten
    # 前面的data.size是从第一个元素到最后一个元素(所有),下标0--n-1,表示的是行,乘一行个数n就是在在一维数组中一行的开始位置
    buf.ravel()[nmsk] = 1  # 这个就是表示把对应的是1的(根据上面nmsk找到的索引值)值给buf
    return buf

Объясните что делает эта функция.Передается параметр картинка,типа картинка 160*160.Суть конечно цифровая матрица.Теперь нам нужно кодировать каждый пиксель,потому что категорий две,поэтому Каждый пиксель нужен две позиции, поэтому добавьте размер n, все установлено на 0, это матрица до кодирования, размер 160 * 160 * 2. nmsk сохраняет положение категории, соответствующей каждому пикселю в сглаженной незакодированной матрице.

Например, текущая картинка представляет собой двумерную матрицу ([[0,1,0],[1,1,0],[0,0,1]]), тогда буфер представляет собой 3*3*2 матрица, все 0, первый пиксель 0, то есть категория первая, поэтому код этого пикселя [1,0], второй 1, код [0,1], та же причина , и, наконец, Просто замените каждый элемент в исходной матрице на закодированный, и в итоге он будет [[[1,0],[0,1]...]], но это не так просто написать.Поэтому , мы можем сначала записать позицию 1 и, наконец, заменить ее напрямую. Первые четыре сплющенной закодированной матрицы равны 1001. Давайте обсудим, как это получилось.Первый код пикселя равен 10, и позиция этой 1 в окончательной сплющенной матрице 0=0*2+0, вторая позиция 1 равна 3=1*2+1, поэтому можно найти алгоритм:

WZ (конечная позиция 1) = WZ (индекс пикселя) * количество категорий + категория пикселей

Поэтому nmsk используется для записи позиций этих единиц, а затем, наконец, заменяет 0 в соответствующих позициях единицами, тем самым завершая кодирование пикселей изображения.

Как восстановить закодированную картинку в исходную картинку очень просто, достаточно выяснить достаточно ли позиции 1, найти ли позицию максимального значения в этом измерении, то есть функция argmax(), следующая это простая демонстрация:

    imgB = np.array([1, 0, 1, 1, 0, 1, 1, 0, 0]).reshape(3, 3)
    print('编码前:\n', imgB)
    imgB = onehot(imgB, 2)
    # print('2:', imgB)
    print('恢复:\n', np.argmax(imgB, 2))

Эффект следующий

 Это показано в обучающем коде ниже.

Расчет соответствующих параметров

Соответствующие параметры здесь относятся к точности acc и iou, остальные я не рассчитывал тщательно, поэтому поговорим в основном о точности.

код показывает, как показано ниже:

# 在训练网络前定义函数用于计算Acc 和 mIou
# 计算混淆矩阵
def _fast_hist(label_true, label_pred, n_class):
    mask = (label_true >= 0) & (label_true < n_class)  # 查找有效类别,mask是个bool类型向量
    # 计算匹配个数
    hist = np.bincount(  # bincount输出每个元素的数量,np.bincount([1,1,2]) 输 出 : [0,2,1]代表0有0个,1有2个,2有1个
        n_class * label_true[mask].astype(int) +  # astype代表把bool转为int
        label_pred[mask], minlength=n_class ** 2).reshape(n_class, n_class)  # minlength=4表示最少计算到class*2,为0也计算,不然个数都不够
    '''
    混淆矩阵  n_class = 2,矩阵2*2
        0        1     标答
    0   0*2+0    0*2+1  
    1   1*2+0    1*2+1
    预测
    一维向量的输出是  0,1,2,3,对应到矩阵中
    '''
    return hist


# 根据混淆矩阵计算Acc和mIou
def label_accuracy_score(label_trues, label_preds, n_class):
    """
        Returns accuracy score evaluation result.
      - overall accuracy
      - mean accuracy
      - mean IU
    """
    hist = np.zeros((n_class, n_class))
    for lt, lp in zip(label_trues, label_preds):  # zip(a,b)就是一一对应打包起来
        hist += _fast_hist(lt.flatten(), lp.flatten(), n_class)  # 展平送进去计算,也就是向量计算
    acc = np.diag(hist).sum() / hist.sum()  # 计算主对角线的,也就是正确的数量
    with np.errstate(divide='ignore', invalid='ignore'):
        acc_cls = np.diag(hist) / hist.sum(axis=1)
    acc_cls = np.nanmean(acc_cls)
    with np.errstate(divide='ignore', invalid='ignore'):
        iu = np.diag(hist) / (
                hist.sum(axis=1) + hist.sum(axis=0) - np.diag(hist)
        )
    mean_iu = np.nanmean(iu)
    freq = hist.sum(axis=1) / hist.sum()
    return acc, acc_cls, mean_iu

Это связано с проблемой вычисления матрицы путаницы, Сама матрица путаницы очень проста, то есть вычислить количество совпадений 00, 01, 10 и 11. 01 означает, что метка равна 0, но прогноз равен 1. , а другой такой же.

Входными параметрами являются матрица разметки, матрица предсказания и количество категорий.Из комментариев видно, что развернутые четыре позиции 0, 1, 2 и 3 равны 0*2+0, 0*2+1, 1* 2 соответственно +0, 1*2+1, поэтому в настоящее время прогнозируемое значение рассматривается как метка строки, а ответ используется как метка столбца, чтобы легко вычислить количество ситуаций совпадения 0-1 в четырех позиции. Я предполагаю, что мускус в начале был для устранения неверных координат, таких как предсказание 3, но на самом деле нет необходимости в расчете, если нет такой категории.

Что касается расчета акк, то он должен быть верным, если предсказание и ответ согласуются, поэтому он заключается в суммировании главной диагонали и делении на общее количество пикселей.

обучение модели

Выше приведены все соответствующие колеса, и, наконец, приступайте к сборке, то есть к началу обучения модели.

Обучение модели на самом деле похоже, установите оптимизатор, функцию потерь, а затем установите количество раундов обучения, чтобы начать обучение.

def train(epo_num=50, show_vgg_params=False):
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    if torch.cuda.is_available():
        print('使用GPU')
    else:
        print('使用CPU')

    vgg_model = VGGNet(requires_grad=True, show_params=show_vgg_params)
    fcn_model = FCN8s(pretrained_net=vgg_model, n_class=2)  # 把训练好的几个maxpool层的集合传给fcn
    fcn_model = fcn_model.to(device)  # 载入模型
    # 这里只有两类,采用二分类常用的损失函数BCE
    criterion = nn.BCELoss().to(device)
    # 随机梯度下降优化,学习率0.001,惯性分数0.7
    optimizer = optim.SGD(fcn_model.parameters(), lr=1e-3, momentum=0.7)

    # 记录训练过程相关指标
    all_train_iter_loss = []
    all_test_iter_loss = []
    test_Acc = []
    test_mIou = []
    # start timing
    prev_time = datetime.now()

    for epo in range(1, epo_num + 1):
        pbar = tqdm(train_dataloader)  # 要先把训练集转进进度条里面
        # 训练
        train_loss = 0  # 一轮的总误差,全部图片的
        fcn_model.train()
        for index, (bag, bag_msk) in enumerate(pbar):
            bag = bag.to(device)
            bag_msk = bag_msk.to(device)

            optimizer.zero_grad()  # 梯度清零
            output = fcn_model(bag)  # 输出
            # print(output.shape)
            output = torch.sigmoid(output)  # output.shape is torch.Size([4, 2, 160, 160])
            loss = criterion(output, bag_msk)  # 计算和标答的误差
            # print('loss=',loss)
            loss.backward()  # 需要计算导数,则调用backward()
            # print('grad_loss=',loss)
            iter_loss = loss.item()  # .item()返回一个具体的值,一般用于loss和acc,这一张的误差
            all_train_iter_loss.append(iter_loss)  # 把误差放进误差列表,方便最后画图
            train_loss += iter_loss  # 加到一轮总的误差里
            optimizer.step()  # 根据求导得到的进行更新

            output_np = output.cpu().detach().numpy().copy()
            bag_msk_np = bag_msk.cpu().detach().numpy().copy()
            bag_msk_np = np.argmax(bag_msk_np, axis=1)

            info = 'epoch {}, {}/{},train loss is {}'.format(epo, index, len(train_dataloader), iter_loss)
            pbar.set_description(info)
        # 验证
        test_loss = 0
        fcn_model.eval()
        with torch.no_grad():
            for index, (bag, bag_msk) in enumerate(test_dataloader):
                bag = bag.to(device)
                bag_msk = bag_msk.to(device)

                optimizer.zero_grad()
                output = fcn_model(bag)
                output = torch.sigmoid(output)  # output.shape is torch.Size([4, 2, 160, 160])
                loss = criterion(output, bag_msk)
                iter_loss = loss.item()
                all_test_iter_loss.append(iter_loss)
                test_loss += iter_loss  # 计算并记录误差

                output_np = output.cpu().detach().numpy().copy()
                output_np = np.argmax(output_np, axis=1)
                bag_msk_np = bag_msk.cpu().detach().numpy().copy()

        # 计算时间
        cur_time = datetime.now()
        # divmod(x,y)返回一个元组,第一个参数是整除的结果,第二个是取模的结果
        h, remainder = divmod((cur_time - prev_time).seconds, 3600)
        m, s = divmod(remainder, 60)
        time_str = "Time %02d:%02d:%02d" % (h, m, s)  # 时分秒
        prev_time = cur_time  # 更新时间
        info = 'epoch: %d, epoch train loss = %f, epoch test loss = %f, %s' \
               % (epo, train_loss / len(train_dataloader), test_loss / len(test_dataloader), time_str)
        print(info)

        acc, acc_cls, mean_iu = label_accuracy_score(bag_msk_np, output_np, 2)
        test_Acc.append(acc)
        test_mIou.append(mean_iu)

        print('Acc = %f, mIou = %f' % (acc, mean_iu))
        # 每2个epoch存储一次模型
        if np.mod(epo, 2) == 0:
            # 只存储模型参数
            torch.save(fcn_model.state_dict(), './pths/fcn_model_{}.pth'.format(epo))
            print('成功存储模型:fcn_model_{}.pth'.format(epo))

 небольшая проблема

На выше текст закончился, но у меня остался вопрос.Помимо небольшой проблемы в модели FCN выше, есть еще место про вычисление nmsk в onehot().Код оригинального автора выглядит следующим образом :

def onehot(data, n):
    buf = np.zeros(data.shape + (n,))  # 相当于给每一个像素开辟一个维度,除了他其他都是其他
    nmsk = np.arange(data.size) * n + data.ravel()  # revel表示展平多维数组,就是flatten
    buf.ravel()[nmsk-1] = 1  # 这个就是表示把对应的是1的(根据上面nmsk找到的索引值)值给buf
    return buf

Отличие в том, что nmsk здесь имеет -1, и при восстановлении матрицы выбирается функция argmin() вместо argmax(), но на самом деле я использую такой набор для кодирования матрицы 3*3. матрица изменилась.

 

 Видно, что его нельзя восстановить, но странно то, что я использовал это правило, чтобы посмотреть на восстановленную картинку (середина картинки внизу — метка, слева — комбинация nmsk-1 и argmin() , а справа — nmsk и argmax() Комбинация)

Нарушения и восстановления не было. Эм? ? ? Еще предстоит такая операция? Кажется, нет проблем. Мы все знаем, почему это возможно, более странно, чем почему это не так. Я не мог понять, а потом подумал, может быть, это как-то связано с какими-то особыми свойствами самой картинки, примерно следующим образом.

Такого рода картинка это в первую очередь бинарная классификация, либо 1, либо 0, так это же дает возможность найти максимум 1 найти минимум, то есть найти 0, и использовать argmin(), то вывод при этом время должно быть черным и белым наоборот, но на самом деле нет, почему? Потому что нмск-1.

Предполагая, что объект кодирования равен 111000, то сглаживание после нормального кодирования должно быть 01 01 01 10 10 10, но из-за nmsk-1 все 1 позиции должны быть сдвинуты вперед, первая становится -1, а в конце, Окончательный результат кодирования — 10 10 11 01 01 00, и в это время минимальный индекс между двумя соседями оказывается равным 1, 1, 0, 0, 0, 0, и можно найти две точки:

Один - это самое обычное восстановление, почему, ведь 0101.. становится 1010 после продвижения вперед... Потом находим минимум, 0 заменяет исходную 1, поэтому текущий поиск минимума эквивалентен исходному поиску максимума.

Во-вторых , третья ошибка восстановления 1, почему, это потому, что перемещение привело к тому, что два 01 были закодированы как 0110, а затем 11 было перемещено в позицию, соответствующую 1, а затем argmin() выводит первый индекс для тот же параметр Нижний индекс становится 0, и ошибка восстанавливается.Из вышеприведенных результатов программы то же самое верно.Для каждого соединения 10 1 восстанавливается до 0, что приводит к ошибке.

Так почему же приведенное выше изображение восстанавливается без проблем? Ответ заключается в том, что на картинке слишком мало 10 границ, большинство из них 000...111...000...111..., несколько пикселей, которые вызывают эту ошибку, почти не влияют на конечный результат.

полный код

import os
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset, random_split
from torchvision import transforms
from torchvision.models.vgg import VGG
import cv2
import numpy as np
from tqdm import tqdm


# 将标记图(每个像素值代该位置像素点的类别)转换为onehot编码
def onehot(data, n):
    buf = np.zeros(data.shape + (n,))  # 相当于给每一个像素开辟一个维度,除了他其他都是其他
    nmsk = np.arange(data.size) * n + data.ravel()  # revel表示展平多维数组,就是flatten
    # 前面的data.size是从第一个元素到最后一个元素(所有),下标0--n-1,表示的是行,乘一行个数n就是在在一维数组中一行的开始位置
    # 后面的是0--n-1表示的是类别,表示第几个
    # 索引nmsk存储了在一维数组中应该是1的位置,也就是正确答案
    buf.ravel()[nmsk-1] = 1  # 这个就是表示把对应的是1的(根据上面nmsk找到的索引值)值给buf
    return buf


# 利用torchvision提供的transform,定义原始图片的预处理步骤(转换为tensor和标准化处理)
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])])

# 利用torch提供的Dataset类,定义我们自己的数据集
base_img = './data/bag_data/'  # 训练集地址
base_img_msk = './data/bag_data_msk/'  # 标注地址


class BagDataset(Dataset):

    def __init__(self, transform=None):
        self.transform = transform

    def __len__(self):
        return len(os.listdir(base_img))

    def __getitem__(self, idx):
        img_name = os.listdir(base_img)[idx]  # index是随机数,是图片的索引值
        imgA = cv2.imread(base_img + img_name)
        imgA = cv2.resize(imgA, (160, 160))
        # img_name = '1.jpg'
        imgB = cv2.imread(base_img_msk + img_name, 0)
        imgB = cv2.resize(imgB, (160, 160))
        # 下面是对标注的一些处理
        imgB = imgB / 255  # 归一化
        imgB = imgB.astype('uint8')  # 转化成整数
        imgB = onehot(imgB, 2)
        imgB = imgB.transpose(2, 0, 1)  # 转置  0 1 2 -> 2 0 1 相当于几个维度的位置关系变化,就是把一开始加到最后的提到最前面,效果就是把两列的每一列变成一张图
        imgB = torch.FloatTensor(imgB)

        if self.transform:
            imgA = self.transform(imgA)

        return imgA, imgB


# 实例化数据集
bag = BagDataset(transform)

train_size = int(0.9 * len(bag))
test_size = len(bag) - train_size
train_dataset, test_dataset = random_split(bag, [train_size, test_size])  # 划分数据集

# 利用DataLoader生成一个分batch获取数据的可迭代对象
train_dataloader = DataLoader(train_dataset, batch_size=4, shuffle=True, num_workers=4)
test_dataloader = DataLoader(test_dataset, batch_size=4, shuffle=True, num_workers=4)

# <-------------------------------------------------------->#
# 下面开始定义网络模型
# 先定义VGG结构

# ranges 是用于方便获取和记录每个池化层得到的特征图
# 例如vgg16,需要(0, 5)的原因是为方便记录第一个pooling层得到的输出(详见下午、稳VGG定义)
ranges = {
    'vgg11': ((0, 3), (3, 6), (6, 11), (11, 16), (16, 21)),
    'vgg13': ((0, 5), (5, 10), (10, 15), (15, 20), (20, 25)),
    'vgg16': ((0, 5), (5, 10), (10, 17), (17, 24), (24, 31)),
    'vgg19': ((0, 5), (5, 10), (10, 19), (19, 28), (28, 37))
}

# Vgg网络结构配置(数字代表经过卷积后的channel数,‘M’代表池化层)
cfg = {
    'vgg11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'vgg13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'vgg16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
    'vgg19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}


# 由cfg构建vgg-Net的卷积层和池化层(block1-block5)
def make_layers(cfg, batch_norm=False):
    layers = []
    in_channels = 3  # RGB初始值
    for v in cfg:
        if v == 'M':  # 池化层
            layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
        else:
            conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
            if batch_norm:  # 是否需要归一化
                layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
            else:
                layers += [conv2d, nn.ReLU(inplace=True)]
            in_channels = v  # 这一层输出的通道数就是下一层输入的通道数
    return nn.Sequential(*layers)


# 下面开始构建VGGnet
class VGGNet(VGG):
    def __init__(self, pretrained=True, model='vgg16', requires_grad=True, remove_fc=True, show_params=False):
        super().__init__(make_layers(cfg[model]))
        self.ranges = ranges[model]  # ranges是一个字典,键是model名字,后面的是池化层的信息

        # 获取VGG模型训练好的参数,并加载(第一次执行需要下载一段时间)
        if pretrained:
            exec("self.load_state_dict(models.%s(pretrained=True).state_dict())" % model)

        # 屏蔽预训练模型的权重,只训练最后一层的全连接的权重,因为fcn模型是建立在vgg16基础上训练的,所以前面训练好的VGG网络不修改
        if not requires_grad:
            for param in super().parameters():
                param.requires_grad = False

        # 去掉vgg最后的全连接层(classifier)
        if remove_fc:
            del self.classifier

        # 打印网络的结构
        if show_params == True:
            for name, param in self.named_parameters():
                print(name, param.size())

    def forward(self, x):
        output = {}
        # 利用之前定义的ranges获取每个max-pooling层输出的特征图,这个主要是FCN32的上采样要用到
        for idx, (begin, end) in enumerate(self.ranges):  # enumerate用于枚举,同时给出元素和下标
            # self.ranges = ((0, 5), (5, 10), (10, 17), (17, 24), (24, 31)) (vgg16 examples)
            for layer in range(begin, end):
                x = self.features[layer](x)
                # 相当于把x矩阵放进layer层,然后得到输出,0-5代表第一个max-pool需要经过的层数,所以x1实际上就是第一个max-pool层输出
            output["x%d" % (idx + 1)] = x
            # x数字越大越深
        # output 为一个字典键x1d对应第一个max-pooling输出的特征图,x2...x5类推
        return output



# 下面由VGG构建FCN8s
class FCN8s(nn.Module):

    def __init__(self, pretrained_net, n_class):
        super().__init__()
        # 定义可能会用到的东西
        self.n_class = n_class
        self.pretrained_net = pretrained_net
        self.conv6 = nn.Conv2d(512, 512, kernel_size=1, stride=1, padding=0, dilation=1)
        self.conv7 = nn.Conv2d(512, 512, kernel_size=1, stride=1, padding=0, dilation=1)  # 卷积核大小是1,本质上是全连接层
        # 这里写两个一样的可能是为了写出前后关系的感觉?
        self.relu = nn.ReLU(inplace=True)
        self.deconv1 = nn.ConvTranspose2d(512, 512, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn1 = nn.BatchNorm2d(512)
        self.deconv2 = nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn2 = nn.BatchNorm2d(256)
        self.deconv3 = nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn3 = nn.BatchNorm2d(128)
        self.deconv4 = nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn4 = nn.BatchNorm2d(64)
        self.deconv5 = nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn5 = nn.BatchNorm2d(32)
        self.classifier = nn.Conv2d(32, n_class, kernel_size=1)

    def forward(self, x):
        output = self.pretrained_net(x)
        # 这个已经在前面的forward中初始化了,里面已经存储了相关特征图
        x5 = output['x5']  # max-pooling5的feature map (1/32)  5*5,160/32
        x4 = output['x4']  # max-pooling4的feature map (1/16)
        x3 = output['x3']  # max-pooling3的feature map (1/8)

        # 所以总结一下FCN里面的几个合成的步骤也就是反卷积->激活->标准化->加上前面的pool层继续

        # 这两句没用,或者说用错了
        score = self.relu(self.conv6(x5))  # conv6  size不变 (1/32)

        # 1/32可能没有融合进去?

        # 这里我尝试把右边括号里的x5改成了score
        score = self.relu(self.deconv1(score))  # out_size = 2*in_size (1/16)

        score = self.bn1(score + x4)  # bn是标准化,表示加x4第二池化层的结果一同进行计算
        score = self.relu(self.deconv2(score))  # out_size = 2*in_size (1/8)

        score = self.bn2(score + x3)

        # 到这里为止就是全部的FCN步骤,接下来是反卷积到原尺寸

        # 此时是1/8,然后继续反卷积,每次扩大两倍边长直到最后和原图一样
        score = self.bn3(self.relu(self.deconv3(score)))  # out_size = 2*in_size (1/4),反卷积后标准化
        score = self.bn4(self.relu(self.deconv4(score)))  # out_size = 2*in_size (1/2)
        score = self.bn5(self.relu(self.deconv5(score)))  # out_size = 2*in_size (1)
        score = self.classifier(score)  # size不变,使输出的channel等于类别数,相当于对每个点分类
        # print(score.shape)
        # time.sleep(1000)
        return score


# <---------------------------------------------->
# 下面开始训练网络

# 在训练网络前定义函数用于计算Acc 和 mIou
# 计算混淆矩阵
def _fast_hist(label_true, label_pred, n_class):
    mask = (label_true >= 0) & (label_true < n_class)  # 查找有效类别,mask是个bool类型向量
    # 计算匹配个数
    hist = np.bincount(  # bincount输出每个元素的数量,np.bincount([1,1,2]) 输 出 : [0,2,1]代表0有0个,1有2个,2有1个
        n_class * label_true[mask].astype(int) +  # astype代表把bool转为int
        label_pred[mask], minlength=n_class ** 2).reshape(n_class, n_class)  # minlength=4表示最少计算到class*2,为0也计算,不然个数都不够
    '''
    混淆矩阵  n_class = 2,矩阵2*2
        0        1     标答
    0   0*2+0    0*2+1  
    1   1*2+0    1*2+1
    预测
    一维向量的输出是  0,1,2,3,对应到矩阵中
    '''
    return hist


# 根据混淆矩阵计算Acc和mIou
def label_accuracy_score(label_trues, label_preds, n_class):
    """
        Returns accuracy score evaluation result.
      - overall accuracy
      - mean accuracy
      - mean IU
    """
    hist = np.zeros((n_class, n_class))
    for lt, lp in zip(label_trues, label_preds):  # zip(a,b)就是一一对应打包起来
        hist += _fast_hist(lt.flatten(), lp.flatten(), n_class)  # 展平送进去计算,也就是向量计算
    acc = np.diag(hist).sum() / hist.sum()  # 计算主对角线的,也就是正确的数量
    with np.errstate(divide='ignore', invalid='ignore'):
        acc_cls = np.diag(hist) / hist.sum(axis=1)
    acc_cls = np.nanmean(acc_cls)
    with np.errstate(divide='ignore', invalid='ignore'):
        iu = np.diag(hist) / (
                hist.sum(axis=1) + hist.sum(axis=0) - np.diag(hist)
        )
    mean_iu = np.nanmean(iu)
    freq = hist.sum(axis=1) / hist.sum()
    return acc, acc_cls, mean_iu


from datetime import datetime

import torch.optim as optim
import matplotlib.pyplot as plt


def train(epo_num=50, show_vgg_params=False):
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    if torch.cuda.is_available():
        print('使用GPU')
    else:
        print('使用CPU')

    vgg_model = VGGNet(requires_grad=True, show_params=show_vgg_params)
    fcn_model = FCN8s(pretrained_net=vgg_model, n_class=2)  # 把训练好的几个maxpool层的集合传给fcn
    fcn_model = fcn_model.to(device)  # 载入模型
    # 这里只有两类,采用二分类常用的损失函数BCE
    criterion = nn.BCELoss().to(device)
    # 随机梯度下降优化,学习率0.001,惯性分数0.7
    optimizer = optim.SGD(fcn_model.parameters(), lr=1e-3, momentum=0.7)

    # 记录训练过程相关指标
    all_train_iter_loss = []
    all_test_iter_loss = []
    test_Acc = []
    test_mIou = []
    # start timing
    prev_time = datetime.now()

    for epo in range(1, epo_num + 1):
        pbar = tqdm(train_dataloader)  # 要先把训练集转进进度条里面
        # 训练
        train_loss = 0  # 一轮的总误差,全部图片的
        fcn_model.train()
        for index, (bag, bag_msk) in enumerate(pbar):

            bag = bag.to(device)
            bag_msk = bag_msk.to(device)

            optimizer.zero_grad()  # 梯度清零
            output = fcn_model(bag)  # 输出
            # print(output.shape)
            # time.sleep(1000)
            output = torch.sigmoid(output)  # output.shape is torch.Size([4, 2, 160, 160])
            loss = criterion(output, bag_msk)  # 计算和标答的误差
            # print('loss=',loss)
            loss.backward()  # 需要计算导数,则调用backward()
            # print('grad_loss=',loss)
            iter_loss = loss.item()  # .item()返回一个具体的值,一般用于loss和acc,这一张的误差
            all_train_iter_loss.append(iter_loss)  # 把误差放进误差列表,方便最后画图
            train_loss += iter_loss  # 加到一轮总的误差里
            optimizer.step()  # 根据求导得到的进行更新

            output_np = output.cpu().detach().numpy().copy()
            output_np = np.argmax(output_np, axis=1)  # 找出所有通道里面的最小值
            # 相当于就是把两个维度的最小值的找到作为输出,也就是找的是0在两个索引中的位置,本质也是在找1的位置
            bag_msk_np = bag_msk.cpu().detach().numpy().copy()
            bag_msk_np = np.argmax(bag_msk_np, axis=1)
            info = 'epoch {}, {}/{},train loss is {}'.format(epo, index, len(train_dataloader), iter_loss)
            pbar.set_description(info)

        # 验证
        test_loss = 0
        fcn_model.eval()
        with torch.no_grad():
            for index, (bag, bag_msk) in enumerate(test_dataloader):
                bag = bag.to(device)
                bag_msk = bag_msk.to(device)

                optimizer.zero_grad()
                output = fcn_model(bag)
                output = torch.sigmoid(output)  # output.shape is torch.Size([4, 2, 160, 160])
                loss = criterion(output, bag_msk)
                iter_loss = loss.item()
                all_test_iter_loss.append(iter_loss)
                test_loss += iter_loss  # 计算并记录误差

                output_np = output.cpu().detach().numpy().copy()
                output_np = np.argmax(output_np, axis=1)
                bag_msk_np = bag_msk.cpu().detach().numpy().copy()
                # 解释一下为什么这里的0和1一样多,因为按照onehot,这里一开始实际上每个像素点对应onehot变化是[0,1]或者[1,0],所以10的总和是一样,因为每个像素点对应了一组[1,0]
                # 之后经过一个维度变换,160,160,2-->2,160,160也就是被分成了两张图片,找两个维度0所在的索引
                bag_msk_np = np.argmax(bag_msk_np, axis=1)

        # 计算时间
        cur_time = datetime.now()
        # divmod(x,y)返回一个元组,第一个参数是整除的结果,第二个是取模的结果
        h, remainder = divmod((cur_time - prev_time).seconds, 3600)
        m, s = divmod(remainder, 60)
        time_str = "Time %02d:%02d:%02d" % (h, m, s)  # 时分秒
        prev_time = cur_time  # 更新时间
        # print()
        info = 'epoch: %d, epoch train loss = %f, epoch test loss = %f, %s' \
               % (epo, train_loss / len(train_dataloader), test_loss / len(test_dataloader), time_str)
        print(info)

        acc, acc_cls, mean_iu = label_accuracy_score(bag_msk_np, output_np, 2)
        test_Acc.append(acc)
        test_mIou.append(mean_iu)

        print('Acc = %f, mIou = %f' % (acc, mean_iu))
        # 每2个epoch存储一次模型
        if np.mod(epo, 2) == 0:
            # 只存储模型参数
            torch.save(fcn_model.state_dict(), './pths/fcn_model_{}.pth'.format(epo))
            print('成功存储模型:fcn_model_{}.pth'.format(epo))
    # 绘制训练过程数据
    plt.figure()
    plt.subplot(221)
    plt.title('train_loss')
    plt.plot(all_train_iter_loss)
    plt.xlabel('batch')
    plt.subplot(222)
    plt.title('test_loss')
    plt.plot(all_test_iter_loss)
    plt.xlabel('batch')
    plt.subplot(223)
    plt.title('test_Acc')
    plt.plot(test_Acc)
    plt.xlabel('epoch')
    plt.subplot(224)
    plt.title('test_mIou')
    plt.plot(test_mIou)
    plt.xlabel('epoch')
    plt.show()


if __name__ == "__main__":
    # 主程序
    train(epo_num=20, show_vgg_params=False)  # 参数是设置是否打印网络结构

ссылка

источник кода

Подробное объяснение FCN и простая реализация pytorch (с подробной интерпретацией кода) - Блог Zinc_abc - Блог CSDN

Набор данных и код

зеркала / bat67 / pytorch-FCN-самая простая демо-версия · GitCode

Supongo que te gusta

Origin blog.csdn.net/weixin_60360239/article/details/127932583
Recomendado
Clasificación