• FCN的代码解读


    目录

    模型初始化

    VGG初始化

    FCN初始化 

    图片的预处理

    图片处理

    图片编码

    计算相关参数

    模型训练

     一个小问题

    完整代码

    参考


    最近浅研究了一下关于图像领域的图像分割的相关知识,发现水还是挺深的,因为FCN差不多也是领域的开山鼻祖,所以就先从这个方面入手。理论就不多讲很多了,网上一搜一大堆,主要就是解析一下代码部分。

    模型初始化

    众所周知,FCN的后半段是新的,前半段一般移植自其他模型,这里我选择了Vgg16的模型结构。所以模型初始化分为两步,首先是对Vgg网络的初始化,然后是对Fcn网络的初始化。

    VGG初始化

     这里为了方便Vgg的选择将几个不同的Vgg封装在了列表中,以数字代表卷积后的输出通道数,卷积的输入通道数也就是前一个的输出通道数,M代表池化层,Vgg采用的都是卷积核为3的卷积层和大小为2的池化层核,所以这两个参数为已知无需标注。

    还需要注意的是因为Fcn是全卷积网络,所以是不需要最后的全连接层的,所以去掉。

    1. # Vgg网络结构配置(数字代表经过卷积后的channel数,‘M’代表池化层)
    2. cfg = {
    3. 'vgg11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    4. 'vgg13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    5. 'vgg16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
    6. 'vgg19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
    7. }
    8. # 由cfg构建vgg-Net的卷积层和池化层(block1-block5)
    9. def make_layers(cfg, batch_norm=False):
    10. layers = []
    11. in_channels = 3 # RGB初始值
    12. for v in cfg:
    13. if v == 'M': # 池化层
    14. layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
    15. else:
    16. conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
    17. if batch_norm: # 是否需要归一化
    18. layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
    19. else:
    20. layers += [conv2d, nn.ReLU(inplace=True)]
    21. in_channels = v # 这一层输出的通道数就是下一层输入的通道数
    22. return nn.Sequential(*layers)
    23. # 下面开始构建VGGnet
    24. class VGGNet(VGG):
    25. def __init__(self, pretrained=True, model='vgg16', requires_grad=True, remove_fc=True, show_params=False):
    26. super().__init__(make_layers(cfg[model]))
    27. self.ranges = ranges[model] # ranges是一个字典,键是model名字,后面的是池化层的信息
    28. # 获取VGG模型训练好的参数,并加载(第一次执行需要下载一段时间)
    29. if pretrained:
    30. exec("self.load_state_dict(models.%s(pretrained=True).state_dict())" % model)
    31. # 屏蔽预训练模型的权重,只训练最后一层的全连接的权重,因为fcn模型是建立在vgg16基础上训练的,所以前面训练好的VGG网络不修改
    32. if not requires_grad:
    33. for param in super().parameters():
    34. param.requires_grad = False
    35. # 去掉vgg最后的全连接层(classifier)
    36. if remove_fc:
    37. del self.classifier
    38. # 打印网络的结构
    39. if show_params == True:
    40. for name, param in self.named_parameters():
    41. print(name, param.size())
    42. def forward(self, x):
    43. output = {}
    44. # 利用之前定义的ranges获取每个max-pooling层输出的特征图,这个主要是FCN32的上采样要用到
    45. for idx, (begin, end) in enumerate(self.ranges): # enumerate用于枚举,同时给出元素和下标
    46. # self.ranges = ((0, 5), (5, 10), (10, 17), (17, 24), (24, 31)) (vgg16 examples)
    47. for layer in range(begin, end):
    48. x = self.features[layer](x)
    49. # 相当于把x矩阵放进layer层,然后得到输出,0-5代表第一个max-pool需要经过的层数,所以x1实际上就是第一个max-pool层输出
    50. output["x%d" % (idx + 1)] = x
    51. # x数字越大越深
    52. # output 为一个字典键x1d对应第一个max-pooling输出的特征图,x2...x5类推
    53. return output

    Fcn8s是需要融合前面3个池化层信息的,所以需要将Vgg模型的池化层信息记录下来,这也是foward在做的事情,可以看到这串代码实际上就是取出一串卷积层加上最后的池化层,做完之后把结果存储到字典中,最后output中存储的就是几个池化层的信息(因为每次都是以池化层为结束)。 

    FCN初始化 

    然后是关于FCN网络的初始化。FCN下有FCN32s,FCN16s,FCN8s,如下图:

    这是FCN8s,因为融合了不同深度的池化层的信息,因而相比直接输出对边缘处理会更加丝滑,因为浅的抽象层次往往对细节有着更好理解。但是作者也说了,并不是融合的越多越多好,Fcn4s相比并没有很大的精度提高,因此也是适可而止,因此下面就直接做Fcn8s。

    1. # 下面由VGG构建FCN8s
    2. class FCN8s(nn.Module):
    3. def __init__(self, pretrained_net, n_class):
    4. super().__init__()
    5. # 定义可能会用到的东西
    6. self.n_class = n_class
    7. self.pretrained_net = pretrained_net
    8. self.conv6 = nn.Conv2d(512, 512, kernel_size=1, stride=1, padding=0, dilation=1)
    9. self.conv7 = nn.Conv2d(512, 512, kernel_size=1, stride=1, padding=0, dilation=1) # 卷积核大小是1,本质上是全连接层
    10. # 这里写两个一样的可能是为了写出前后关系的感觉?
    11. self.relu = nn.ReLU(inplace=True)
    12. self.deconv1 = nn.ConvTranspose2d(512, 512, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
    13. self.bn1 = nn.BatchNorm2d(512)
    14. self.deconv2 = nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
    15. self.bn2 = nn.BatchNorm2d(256)
    16. self.deconv3 = nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
    17. self.bn3 = nn.BatchNorm2d(128)
    18. self.deconv4 = nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
    19. self.bn4 = nn.BatchNorm2d(64)
    20. self.deconv5 = nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
    21. self.bn5 = nn.BatchNorm2d(32)
    22. self.classifier = nn.Conv2d(32, n_class, kernel_size=1)
    23. def forward(self, x):
    24. output = self.pretrained_net(x)
    25. # 这个已经在前面的forward中初始化了,里面已经存储了相关特征图
    26. x5 = output['x5'] # max-pooling5的feature map (1/32) 5*5,160/32
    27. # print(x5.size())
    28. x4 = output['x4'] # max-pooling4的feature map (1/16)
    29. x3 = output['x3'] # max-pooling3的feature map (1/8)
    30. # 所以总结一下FCN里面的几个合成的步骤也就是反卷积->激活->标准化->加上前面的pool层继续
    31. score = self.relu(self.conv6(x5)) # conv6 size不变 (1/32)
    32. # score = self.relu(self.conv7(score)) # conv7 size不变 (1/32)
    33. # 这里我尝试把右边括号里的x5改成了score
    34. score = self.relu(self.deconv1(score)) # out_size = 2*in_size (1/16)
    35. # print(score.size()) # 反卷积之后变为两倍
    36. score = self.bn1(score + x4) # bn是标准化,表示加x4第二池化层的结果一同进行计算
    37. score = self.relu(self.deconv2(score)) # out_size = 2*in_size (1/8)
    38. score = self.bn2(score + x3)
    39. # 到这里为止就是全部的FCN步骤,接下来是反卷积到原尺寸
    40. # 此时是1/8,然后继续反卷积,每次扩大两倍边长直到最后和原图一样
    41. score = self.bn3(self.relu(self.deconv3(score))) # out_size = 2*in_size (1/4),反卷积后标准化
    42. score = self.bn4(self.relu(self.deconv4(score))) # out_size = 2*in_size (1/2)
    43. score = self.bn5(self.relu(self.deconv5(score))) # out_size = 2*in_size (1)
    44. score = self.classifier(score) # size不变,使输出的channel等于类别数,相当于对每个点分类
    45. return score

    因为代码是取自其他博主,因而在阅读过程中也遇到了一些问题,原代码对于score的处理如下,但是可以看到第一句和第二句对score处理了之后在第三句又对score重新赋值,这就代表了什么,前两句是无效的,这也是我疑惑的地方,后来我也去参考了一下这位博主参考的githug源码,猜想应该是要把处理后的score放进去继续处理,也就成为了上面的样子。

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

    每一次池化尺寸会减半,而后面每次反卷积就意味着尺寸会变为两倍,因此处理到最后也就成为了原来的尺寸。

    图片的预处理

    接下来就是对于训练图片的预处理,包括图片处理和图片编码部分。

    图片处理

    对图片本身的处理主要是尺寸变换还有标准化和打包这些,基本是通过库函数来完成,就不多说。 

    图片编码

    编码相对麻烦,需要用到独热编码,因为损失函数计算可能会用到。

    独热编码就是开辟n个位置,在对应的那个维度为1,剩下为0。比如性别可以是男/女,男是第一个,女是第二个,那么对于一个个体他的性别可以是男,编码10,或者女,编码01;再假设国籍可以是中国/美国/日本,那么一个人的国籍编码可以是100,010,001(中国,美国,日本),也就是永远一个为1,其他为0,为1的就对应他自己所属的。这里的类别也类似,假设有两个像素点,每个像素点要么01,属于第一类,要么10,属于第二类。

    独热编码如下:

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

    解释一下这个函数是干什么的,传入的参数就是一张图片,比如是160*160的一张图,本质当然是一个数字矩阵,现在要为每个像素点编码,因为有两个类别,所以每个像素点需要两个位置,因此加上一个维度n,全部置0,这个就是没编码前的矩阵,大小160*160*2。nmsk存储的是每一个像素点所对应类别在展平的未编码矩阵中的位置。

    举个栗子,现在的图片是二维矩阵([[0,1,0],[1,1,0],[0,0,1]]),那么开辟buf是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将这些1的位置记录下来,然后最后把对应位置的0替换为1,这样就完成了对图像像素的编码。

    这样编码后的图片怎么恢复为原来的图,很简单,只要找到1所在的位置是不是就可以了,那是不是就是找最大值在这个维度的位置,也就是argmax()函数,下面是一个简单演示:

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

    效果如下

     这在下面的训练代码中有所体现。

    计算相关参数

    这里的相关参数指的是精度acc还有iou这些,其他我还没有仔细推算过,主要讲一下精度这个吧。

    代码如下:

    1. # 在训练网络前定义函数用于计算Acc 和 mIou
    2. # 计算混淆矩阵
    3. def _fast_hist(label_true, label_pred, n_class):
    4. mask = (label_true >= 0) & (label_true < n_class) # 查找有效类别,mask是个bool类型向量
    5. # 计算匹配个数
    6. hist = np.bincount( # bincount输出每个元素的数量,np.bincount([1,1,2]) 输 出 : [0,2,1]代表0有0个,1有2个,2有1个
    7. n_class * label_true[mask].astype(int) + # astype代表把bool转为int
    8. label_pred[mask], minlength=n_class ** 2).reshape(n_class, n_class) # minlength=4表示最少计算到class*2,为0也计算,不然个数都不够
    9. '''
    10. 混淆矩阵 n_class = 2,矩阵2*2
    11. 0 1 标答
    12. 0 0*2+0 0*2+1
    13. 1 1*2+0 1*2+1
    14. 预测
    15. 一维向量的输出是 0,1,2,3,对应到矩阵中
    16. '''
    17. return hist
    18. # 根据混淆矩阵计算Acc和mIou
    19. def label_accuracy_score(label_trues, label_preds, n_class):
    20. """
    21. Returns accuracy score evaluation result.
    22. - overall accuracy
    23. - mean accuracy
    24. - mean IU
    25. """
    26. hist = np.zeros((n_class, n_class))
    27. for lt, lp in zip(label_trues, label_preds): # zip(a,b)就是一一对应打包起来
    28. hist += _fast_hist(lt.flatten(), lp.flatten(), n_class) # 展平送进去计算,也就是向量计算
    29. acc = np.diag(hist).sum() / hist.sum() # 计算主对角线的,也就是正确的数量
    30. with np.errstate(divide='ignore', invalid='ignore'):
    31. acc_cls = np.diag(hist) / hist.sum(axis=1)
    32. acc_cls = np.nanmean(acc_cls)
    33. with np.errstate(divide='ignore', invalid='ignore'):
    34. iu = np.diag(hist) / (
    35. hist.sum(axis=1) + hist.sum(axis=0) - np.diag(hist)
    36. )
    37. mean_iu = np.nanmean(iu)
    38. freq = hist.sum(axis=1) / hist.sum()
    39. 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匹配情况在四个位置的数量。一开始的musk我猜想是为了剔除无效的坐标,比如预测为3,但实际上没有这个类别也就没有计算的必要了。

    至于acc的计算,一定是预测和标答一致才算正确,所以就是对于主对角线求和除以总的像素点个数。

    模型训练

    以上就是所有相关的轮子,最后开始组装,也就是开始模型训练。

    模型训练实际上大同小异,设定优化器,损失函数,然后设定训练轮数,开始训练。

    1. def train(epo_num=50, show_vgg_params=False):
    2. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    3. if torch.cuda.is_available():
    4. print('使用GPU')
    5. else:
    6. print('使用CPU')
    7. vgg_model = VGGNet(requires_grad=True, show_params=show_vgg_params)
    8. fcn_model = FCN8s(pretrained_net=vgg_model, n_class=2) # 把训练好的几个maxpool层的集合传给fcn
    9. fcn_model = fcn_model.to(device) # 载入模型
    10. # 这里只有两类,采用二分类常用的损失函数BCE
    11. criterion = nn.BCELoss().to(device)
    12. # 随机梯度下降优化,学习率0.001,惯性分数0.7
    13. optimizer = optim.SGD(fcn_model.parameters(), lr=1e-3, momentum=0.7)
    14. # 记录训练过程相关指标
    15. all_train_iter_loss = []
    16. all_test_iter_loss = []
    17. test_Acc = []
    18. test_mIou = []
    19. # start timing
    20. prev_time = datetime.now()
    21. for epo in range(1, epo_num + 1):
    22. pbar = tqdm(train_dataloader) # 要先把训练集转进进度条里面
    23. # 训练
    24. train_loss = 0 # 一轮的总误差,全部图片的
    25. fcn_model.train()
    26. for index, (bag, bag_msk) in enumerate(pbar):
    27. bag = bag.to(device)
    28. bag_msk = bag_msk.to(device)
    29. optimizer.zero_grad() # 梯度清零
    30. output = fcn_model(bag) # 输出
    31. # print(output.shape)
    32. output = torch.sigmoid(output) # output.shape is torch.Size([4, 2, 160, 160])
    33. loss = criterion(output, bag_msk) # 计算和标答的误差
    34. # print('loss=',loss)
    35. loss.backward() # 需要计算导数,则调用backward()
    36. # print('grad_loss=',loss)
    37. iter_loss = loss.item() # .item()返回一个具体的值,一般用于loss和acc,这一张的误差
    38. all_train_iter_loss.append(iter_loss) # 把误差放进误差列表,方便最后画图
    39. train_loss += iter_loss # 加到一轮总的误差里
    40. optimizer.step() # 根据求导得到的进行更新
    41. output_np = output.cpu().detach().numpy().copy()
    42. bag_msk_np = bag_msk.cpu().detach().numpy().copy()
    43. bag_msk_np = np.argmax(bag_msk_np, axis=1)
    44. info = 'epoch {}, {}/{},train loss is {}'.format(epo, index, len(train_dataloader), iter_loss)
    45. pbar.set_description(info)
    46. # 验证
    47. test_loss = 0
    48. fcn_model.eval()
    49. with torch.no_grad():
    50. for index, (bag, bag_msk) in enumerate(test_dataloader):
    51. bag = bag.to(device)
    52. bag_msk = bag_msk.to(device)
    53. optimizer.zero_grad()
    54. output = fcn_model(bag)
    55. output = torch.sigmoid(output) # output.shape is torch.Size([4, 2, 160, 160])
    56. loss = criterion(output, bag_msk)
    57. iter_loss = loss.item()
    58. all_test_iter_loss.append(iter_loss)
    59. test_loss += iter_loss # 计算并记录误差
    60. output_np = output.cpu().detach().numpy().copy()
    61. output_np = np.argmax(output_np, axis=1)
    62. bag_msk_np = bag_msk.cpu().detach().numpy().copy()
    63. # 计算时间
    64. cur_time = datetime.now()
    65. # divmod(x,y)返回一个元组,第一个参数是整除的结果,第二个是取模的结果
    66. h, remainder = divmod((cur_time - prev_time).seconds, 3600)
    67. m, s = divmod(remainder, 60)
    68. time_str = "Time %02d:%02d:%02d" % (h, m, s) # 时分秒
    69. prev_time = cur_time # 更新时间
    70. info = 'epoch: %d, epoch train loss = %f, epoch test loss = %f, %s' \
    71. % (epo, train_loss / len(train_dataloader), test_loss / len(test_dataloader), time_str)
    72. print(info)
    73. acc, acc_cls, mean_iu = label_accuracy_score(bag_msk_np, output_np, 2)
    74. test_Acc.append(acc)
    75. test_mIou.append(mean_iu)
    76. print('Acc = %f, mIou = %f' % (acc, mean_iu))
    77. # 每2个epoch存储一次模型
    78. if np.mod(epo, 2) == 0:
    79. # 只存储模型参数
    80. torch.save(fcn_model.state_dict(), './pths/fcn_model_{}.pth'.format(epo))
    81. print('成功存储模型:fcn_model_{}.pth'.format(epo))

     一个小问题

    正文在上面就结束了,但是我还是有一个疑问,除了上面FCN模型那里有点小问题,还有一个地方就是关于onehot()中nmsk的计算,原作者的代码如下:

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

    区别就是这里的nmsk有一个-1,并且在恢复矩阵时选择了argmin()而非argmax()函数,但是实际上我用这样的一套去编码一个3*3矩阵在还原时,矩阵已经变样。

     

     可以看到无法恢复,但是奇怪的是我用这样的规则去看了恢复的图(下图中中间是标注,左边是用了nmsk-1和argmin()的组合,右边是nmsk和argmax()的组合)

    竟然毫无违和恢复了。嗯???还有这种操作?好像也没什么问题。众所周知,为什么可以比为什么不可以更加离奇。我百思不得其解,后来想了想这也许和图片本身一些特殊的性质也有关系,大致如下。

    这种图片首先是二分类,非1即0,所以这也就给了找最大1变为找最小也就是找0,使用argmin()的机会,那么按说这时候的输出应该是黑白颠倒,但是实际上并没有,为什么?因为nmsk-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...,导致这种错误发生的那几个像素点几乎不影响最终结果。

    完整代码

    1. import os
    2. import torch
    3. import torch.nn as nn
    4. from torch.utils.data import DataLoader, Dataset, random_split
    5. from torchvision import transforms
    6. from torchvision.models.vgg import VGG
    7. import cv2
    8. import numpy as np
    9. from tqdm import tqdm
    10. # 将标记图(每个像素值代该位置像素点的类别)转换为onehot编码
    11. def onehot(data, n):
    12. buf = np.zeros(data.shape + (n,)) # 相当于给每一个像素开辟一个维度,除了他其他都是其他
    13. nmsk = np.arange(data.size) * n + data.ravel() # revel表示展平多维数组,就是flatten
    14. # 前面的data.size是从第一个元素到最后一个元素(所有),下标0--n-1,表示的是行,乘一行个数n就是在在一维数组中一行的开始位置
    15. # 后面的是0--n-1表示的是类别,表示第几个
    16. # 索引nmsk存储了在一维数组中应该是1的位置,也就是正确答案
    17. buf.ravel()[nmsk-1] = 1 # 这个就是表示把对应的是1的(根据上面nmsk找到的索引值)值给buf
    18. return buf
    19. # 利用torchvision提供的transform,定义原始图片的预处理步骤(转换为tensor和标准化处理)
    20. transform = transforms.Compose([
    21. transforms.ToTensor(),
    22. transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])])
    23. # 利用torch提供的Dataset类,定义我们自己的数据集
    24. base_img = './data/bag_data/' # 训练集地址
    25. base_img_msk = './data/bag_data_msk/' # 标注地址
    26. class BagDataset(Dataset):
    27. def __init__(self, transform=None):
    28. self.transform = transform
    29. def __len__(self):
    30. return len(os.listdir(base_img))
    31. def __getitem__(self, idx):
    32. img_name = os.listdir(base_img)[idx] # index是随机数,是图片的索引值
    33. imgA = cv2.imread(base_img + img_name)
    34. imgA = cv2.resize(imgA, (160, 160))
    35. # img_name = '1.jpg'
    36. imgB = cv2.imread(base_img_msk + img_name, 0)
    37. imgB = cv2.resize(imgB, (160, 160))
    38. # 下面是对标注的一些处理
    39. imgB = imgB / 255 # 归一化
    40. imgB = imgB.astype('uint8') # 转化成整数
    41. imgB = onehot(imgB, 2)
    42. imgB = imgB.transpose(2, 0, 1) # 转置 0 1 2 -> 2 0 1 相当于几个维度的位置关系变化,就是把一开始加到最后的提到最前面,效果就是把两列的每一列变成一张图
    43. imgB = torch.FloatTensor(imgB)
    44. if self.transform:
    45. imgA = self.transform(imgA)
    46. return imgA, imgB
    47. # 实例化数据集
    48. bag = BagDataset(transform)
    49. train_size = int(0.9 * len(bag))
    50. test_size = len(bag) - train_size
    51. train_dataset, test_dataset = random_split(bag, [train_size, test_size]) # 划分数据集
    52. # 利用DataLoader生成一个分batch获取数据的可迭代对象
    53. train_dataloader = DataLoader(train_dataset, batch_size=4, shuffle=True, num_workers=4)
    54. test_dataloader = DataLoader(test_dataset, batch_size=4, shuffle=True, num_workers=4)
    55. # <-------------------------------------------------------->#
    56. # 下面开始定义网络模型
    57. # 先定义VGG结构
    58. # ranges 是用于方便获取和记录每个池化层得到的特征图
    59. # 例如vgg16,需要(0, 5)的原因是为方便记录第一个pooling层得到的输出(详见下午、稳VGG定义)
    60. ranges = {
    61. 'vgg11': ((0, 3), (3, 6), (6, 11), (11, 16), (16, 21)),
    62. 'vgg13': ((0, 5), (5, 10), (10, 15), (15, 20), (20, 25)),
    63. 'vgg16': ((0, 5), (5, 10), (10, 17), (17, 24), (24, 31)),
    64. 'vgg19': ((0, 5), (5, 10), (10, 19), (19, 28), (28, 37))
    65. }
    66. # Vgg网络结构配置(数字代表经过卷积后的channel数,‘M’代表池化层)
    67. cfg = {
    68. 'vgg11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    69. 'vgg13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    70. 'vgg16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
    71. 'vgg19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
    72. }
    73. # 由cfg构建vgg-Net的卷积层和池化层(block1-block5)
    74. def make_layers(cfg, batch_norm=False):
    75. layers = []
    76. in_channels = 3 # RGB初始值
    77. for v in cfg:
    78. if v == 'M': # 池化层
    79. layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
    80. else:
    81. conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
    82. if batch_norm: # 是否需要归一化
    83. layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
    84. else:
    85. layers += [conv2d, nn.ReLU(inplace=True)]
    86. in_channels = v # 这一层输出的通道数就是下一层输入的通道数
    87. return nn.Sequential(*layers)
    88. # 下面开始构建VGGnet
    89. class VGGNet(VGG):
    90. def __init__(self, pretrained=True, model='vgg16', requires_grad=True, remove_fc=True, show_params=False):
    91. super().__init__(make_layers(cfg[model]))
    92. self.ranges = ranges[model] # ranges是一个字典,键是model名字,后面的是池化层的信息
    93. # 获取VGG模型训练好的参数,并加载(第一次执行需要下载一段时间)
    94. if pretrained:
    95. exec("self.load_state_dict(models.%s(pretrained=True).state_dict())" % model)
    96. # 屏蔽预训练模型的权重,只训练最后一层的全连接的权重,因为fcn模型是建立在vgg16基础上训练的,所以前面训练好的VGG网络不修改
    97. if not requires_grad:
    98. for param in super().parameters():
    99. param.requires_grad = False
    100. # 去掉vgg最后的全连接层(classifier)
    101. if remove_fc:
    102. del self.classifier
    103. # 打印网络的结构
    104. if show_params == True:
    105. for name, param in self.named_parameters():
    106. print(name, param.size())
    107. def forward(self, x):
    108. output = {}
    109. # 利用之前定义的ranges获取每个max-pooling层输出的特征图,这个主要是FCN32的上采样要用到
    110. for idx, (begin, end) in enumerate(self.ranges): # enumerate用于枚举,同时给出元素和下标
    111. # self.ranges = ((0, 5), (5, 10), (10, 17), (17, 24), (24, 31)) (vgg16 examples)
    112. for layer in range(begin, end):
    113. x = self.features[layer](x)
    114. # 相当于把x矩阵放进layer层,然后得到输出,0-5代表第一个max-pool需要经过的层数,所以x1实际上就是第一个max-pool层输出
    115. output["x%d" % (idx + 1)] = x
    116. # x数字越大越深
    117. # output 为一个字典键x1d对应第一个max-pooling输出的特征图,x2...x5类推
    118. return output
    119. # 下面由VGG构建FCN8s
    120. class FCN8s(nn.Module):
    121. def __init__(self, pretrained_net, n_class):
    122. super().__init__()
    123. # 定义可能会用到的东西
    124. self.n_class = n_class
    125. self.pretrained_net = pretrained_net
    126. self.conv6 = nn.Conv2d(512, 512, kernel_size=1, stride=1, padding=0, dilation=1)
    127. self.conv7 = nn.Conv2d(512, 512, kernel_size=1, stride=1, padding=0, dilation=1) # 卷积核大小是1,本质上是全连接层
    128. # 这里写两个一样的可能是为了写出前后关系的感觉?
    129. self.relu = nn.ReLU(inplace=True)
    130. self.deconv1 = nn.ConvTranspose2d(512, 512, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
    131. self.bn1 = nn.BatchNorm2d(512)
    132. self.deconv2 = nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
    133. self.bn2 = nn.BatchNorm2d(256)
    134. self.deconv3 = nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
    135. self.bn3 = nn.BatchNorm2d(128)
    136. self.deconv4 = nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
    137. self.bn4 = nn.BatchNorm2d(64)
    138. self.deconv5 = nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
    139. self.bn5 = nn.BatchNorm2d(32)
    140. self.classifier = nn.Conv2d(32, n_class, kernel_size=1)
    141. def forward(self, x):
    142. output = self.pretrained_net(x)
    143. # 这个已经在前面的forward中初始化了,里面已经存储了相关特征图
    144. x5 = output['x5'] # max-pooling5的feature map (1/32) 5*5,160/32
    145. x4 = output['x4'] # max-pooling4的feature map (1/16)
    146. x3 = output['x3'] # max-pooling3的feature map (1/8)
    147. # 所以总结一下FCN里面的几个合成的步骤也就是反卷积->激活->标准化->加上前面的pool层继续
    148. # 这两句没用,或者说用错了
    149. score = self.relu(self.conv6(x5)) # conv6 size不变 (1/32)
    150. # 1/32可能没有融合进去?
    151. # 这里我尝试把右边括号里的x5改成了score
    152. score = self.relu(self.deconv1(score)) # out_size = 2*in_size (1/16)
    153. score = self.bn1(score + x4) # bn是标准化,表示加x4第二池化层的结果一同进行计算
    154. score = self.relu(self.deconv2(score)) # out_size = 2*in_size (1/8)
    155. score = self.bn2(score + x3)
    156. # 到这里为止就是全部的FCN步骤,接下来是反卷积到原尺寸
    157. # 此时是1/8,然后继续反卷积,每次扩大两倍边长直到最后和原图一样
    158. score = self.bn3(self.relu(self.deconv3(score))) # out_size = 2*in_size (1/4),反卷积后标准化
    159. score = self.bn4(self.relu(self.deconv4(score))) # out_size = 2*in_size (1/2)
    160. score = self.bn5(self.relu(self.deconv5(score))) # out_size = 2*in_size (1)
    161. score = self.classifier(score) # size不变,使输出的channel等于类别数,相当于对每个点分类
    162. # print(score.shape)
    163. # time.sleep(1000)
    164. return score
    165. # <---------------------------------------------->
    166. # 下面开始训练网络
    167. # 在训练网络前定义函数用于计算Acc 和 mIou
    168. # 计算混淆矩阵
    169. def _fast_hist(label_true, label_pred, n_class):
    170. mask = (label_true >= 0) & (label_true < n_class) # 查找有效类别,mask是个bool类型向量
    171. # 计算匹配个数
    172. hist = np.bincount( # bincount输出每个元素的数量,np.bincount([1,1,2]) 输 出 : [0,2,1]代表0有0个,1有2个,2有1个
    173. n_class * label_true[mask].astype(int) + # astype代表把bool转为int
    174. label_pred[mask], minlength=n_class ** 2).reshape(n_class, n_class) # minlength=4表示最少计算到class*2,为0也计算,不然个数都不够
    175. '''
    176. 混淆矩阵 n_class = 2,矩阵2*2
    177. 0 1 标答
    178. 0 0*2+0 0*2+1
    179. 1 1*2+0 1*2+1
    180. 预测
    181. 一维向量的输出是 0,1,2,3,对应到矩阵中
    182. '''
    183. return hist
    184. # 根据混淆矩阵计算Acc和mIou
    185. def label_accuracy_score(label_trues, label_preds, n_class):
    186. """
    187. Returns accuracy score evaluation result.
    188. - overall accuracy
    189. - mean accuracy
    190. - mean IU
    191. """
    192. hist = np.zeros((n_class, n_class))
    193. for lt, lp in zip(label_trues, label_preds): # zip(a,b)就是一一对应打包起来
    194. hist += _fast_hist(lt.flatten(), lp.flatten(), n_class) # 展平送进去计算,也就是向量计算
    195. acc = np.diag(hist).sum() / hist.sum() # 计算主对角线的,也就是正确的数量
    196. with np.errstate(divide='ignore', invalid='ignore'):
    197. acc_cls = np.diag(hist) / hist.sum(axis=1)
    198. acc_cls = np.nanmean(acc_cls)
    199. with np.errstate(divide='ignore', invalid='ignore'):
    200. iu = np.diag(hist) / (
    201. hist.sum(axis=1) + hist.sum(axis=0) - np.diag(hist)
    202. )
    203. mean_iu = np.nanmean(iu)
    204. freq = hist.sum(axis=1) / hist.sum()
    205. return acc, acc_cls, mean_iu
    206. from datetime import datetime
    207. import torch.optim as optim
    208. import matplotlib.pyplot as plt
    209. def train(epo_num=50, show_vgg_params=False):
    210. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    211. if torch.cuda.is_available():
    212. print('使用GPU')
    213. else:
    214. print('使用CPU')
    215. vgg_model = VGGNet(requires_grad=True, show_params=show_vgg_params)
    216. fcn_model = FCN8s(pretrained_net=vgg_model, n_class=2) # 把训练好的几个maxpool层的集合传给fcn
    217. fcn_model = fcn_model.to(device) # 载入模型
    218. # 这里只有两类,采用二分类常用的损失函数BCE
    219. criterion = nn.BCELoss().to(device)
    220. # 随机梯度下降优化,学习率0.001,惯性分数0.7
    221. optimizer = optim.SGD(fcn_model.parameters(), lr=1e-3, momentum=0.7)
    222. # 记录训练过程相关指标
    223. all_train_iter_loss = []
    224. all_test_iter_loss = []
    225. test_Acc = []
    226. test_mIou = []
    227. # start timing
    228. prev_time = datetime.now()
    229. for epo in range(1, epo_num + 1):
    230. pbar = tqdm(train_dataloader) # 要先把训练集转进进度条里面
    231. # 训练
    232. train_loss = 0 # 一轮的总误差,全部图片的
    233. fcn_model.train()
    234. for index, (bag, bag_msk) in enumerate(pbar):
    235. bag = bag.to(device)
    236. bag_msk = bag_msk.to(device)
    237. optimizer.zero_grad() # 梯度清零
    238. output = fcn_model(bag) # 输出
    239. # print(output.shape)
    240. # time.sleep(1000)
    241. output = torch.sigmoid(output) # output.shape is torch.Size([4, 2, 160, 160])
    242. loss = criterion(output, bag_msk) # 计算和标答的误差
    243. # print('loss=',loss)
    244. loss.backward() # 需要计算导数,则调用backward()
    245. # print('grad_loss=',loss)
    246. iter_loss = loss.item() # .item()返回一个具体的值,一般用于loss和acc,这一张的误差
    247. all_train_iter_loss.append(iter_loss) # 把误差放进误差列表,方便最后画图
    248. train_loss += iter_loss # 加到一轮总的误差里
    249. optimizer.step() # 根据求导得到的进行更新
    250. output_np = output.cpu().detach().numpy().copy()
    251. output_np = np.argmax(output_np, axis=1) # 找出所有通道里面的最小值
    252. # 相当于就是把两个维度的最小值的找到作为输出,也就是找的是0在两个索引中的位置,本质也是在找1的位置
    253. bag_msk_np = bag_msk.cpu().detach().numpy().copy()
    254. bag_msk_np = np.argmax(bag_msk_np, axis=1)
    255. info = 'epoch {}, {}/{},train loss is {}'.format(epo, index, len(train_dataloader), iter_loss)
    256. pbar.set_description(info)
    257. # 验证
    258. test_loss = 0
    259. fcn_model.eval()
    260. with torch.no_grad():
    261. for index, (bag, bag_msk) in enumerate(test_dataloader):
    262. bag = bag.to(device)
    263. bag_msk = bag_msk.to(device)
    264. optimizer.zero_grad()
    265. output = fcn_model(bag)
    266. output = torch.sigmoid(output) # output.shape is torch.Size([4, 2, 160, 160])
    267. loss = criterion(output, bag_msk)
    268. iter_loss = loss.item()
    269. all_test_iter_loss.append(iter_loss)
    270. test_loss += iter_loss # 计算并记录误差
    271. output_np = output.cpu().detach().numpy().copy()
    272. output_np = np.argmax(output_np, axis=1)
    273. bag_msk_np = bag_msk.cpu().detach().numpy().copy()
    274. # 解释一下为什么这里的0和1一样多,因为按照onehot,这里一开始实际上每个像素点对应onehot变化是[0,1]或者[1,0],所以10的总和是一样,因为每个像素点对应了一组[1,0]
    275. # 之后经过一个维度变换,160,160,2-->2,160,160也就是被分成了两张图片,找两个维度0所在的索引
    276. bag_msk_np = np.argmax(bag_msk_np, axis=1)
    277. # 计算时间
    278. cur_time = datetime.now()
    279. # divmod(x,y)返回一个元组,第一个参数是整除的结果,第二个是取模的结果
    280. h, remainder = divmod((cur_time - prev_time).seconds, 3600)
    281. m, s = divmod(remainder, 60)
    282. time_str = "Time %02d:%02d:%02d" % (h, m, s) # 时分秒
    283. prev_time = cur_time # 更新时间
    284. # print()
    285. info = 'epoch: %d, epoch train loss = %f, epoch test loss = %f, %s' \
    286. % (epo, train_loss / len(train_dataloader), test_loss / len(test_dataloader), time_str)
    287. print(info)
    288. acc, acc_cls, mean_iu = label_accuracy_score(bag_msk_np, output_np, 2)
    289. test_Acc.append(acc)
    290. test_mIou.append(mean_iu)
    291. print('Acc = %f, mIou = %f' % (acc, mean_iu))
    292. # 每2个epoch存储一次模型
    293. if np.mod(epo, 2) == 0:
    294. # 只存储模型参数
    295. torch.save(fcn_model.state_dict(), './pths/fcn_model_{}.pth'.format(epo))
    296. print('成功存储模型:fcn_model_{}.pth'.format(epo))
    297. # 绘制训练过程数据
    298. plt.figure()
    299. plt.subplot(221)
    300. plt.title('train_loss')
    301. plt.plot(all_train_iter_loss)
    302. plt.xlabel('batch')
    303. plt.subplot(222)
    304. plt.title('test_loss')
    305. plt.plot(all_test_iter_loss)
    306. plt.xlabel('batch')
    307. plt.subplot(223)
    308. plt.title('test_Acc')
    309. plt.plot(test_Acc)
    310. plt.xlabel('epoch')
    311. plt.subplot(224)
    312. plt.title('test_mIou')
    313. plt.plot(test_mIou)
    314. plt.xlabel('epoch')
    315. plt.show()
    316. if __name__ == "__main__":
    317. # 主程序
    318. train(epo_num=20, show_vgg_params=False) # 参数是设置是否打印网络结构

    参考

    代码来源

    FCN详解与pytorch简单实现(附详细代码解读)_zinc_abc的博客-CSDN博客

    数据集和代码

    mirrors / bat67 / pytorch-FCN-easiest-demo · GitCode

  • 相关阅读:
    《OpenCV4快速入门》------函数摘要
    redis主从复制和集群搭建
    【论文发表】2022 HIRE--首篇基于异构图神经网络的高阶关系知识蒸馏方法
    vivo手机如何隐藏应用 vivo手机隐藏应用方法
    生态系统服务—土壤侵蚀强度空间分布/降雨侵蚀力
    VMware Horizon 8 运维系列(二)win10设置共享桌面图标
    Docker0网络及原理探究
    聚观早报 | 遥感AI大模型发布;拼多多启动11.11大促
    2022-36~37周(8.29-9.11) 项目问题整理
    基于Spring MVC + Spring + MyBatis的【超市会员管理系统】
  • 原文地址:https://blog.csdn.net/weixin_60360239/article/details/127932583