1.前言

目标检测领域,从FasterRCNN的实战,Anchor,从loss到全局解析到YOLO V1,YOLO V2,YOLO V3的网络讲解,YOLO V3的anchor和loss都已经讲完了。按照进程来说,理应讲解一些最新的,SOTA的模型了,比如YOLO V4或者EfficientDet,但是要是目标检测领域中不讲点SSD和RetinaNet,感觉就是不完整的。所以接下来两栏,对SSD和RetinaNet进行相关的解析,这里的SSD代码是基于Pytorch的,链接如下:

https://github.com/amdegroot/ssd.pytorch​github.com​github.com

知乎上也有大佬对SSD做了不错的解析,个人觉得很完整、很Nice! 下面抛出链接:

JimmyHua:【SSD算法】史上最全代码解析-核心篇​zhuanlan.zhihu.com

别人的都这么完整了,为什么我还要自己再总结呢?

2. SSD网络结构

SSD是YOLO V1出来后,YOLO V2出来前的一款One-stage目标检测器。仔细阅读论文会发现他的很多tricks被后面提出的,其他的检测网络所借鉴。就网络结构这块,SSD用到了多尺度的特征图,其实在YOLO V3的darknet53中,也是用到了多尺度特征图的思想。因为我们知道,较浅层的特征图上,每个cell的感受野不是很大,所以适合检测较小的物体,而在较深的特征图上,每个cell的感受野比较大了,适合检测较大的物体。

SSD使用了VGG-16的部分网络(从VGG16中截取了一部分)作为其特征提取网络。定义在ssd.py文件中。该SSD网络由三部分组成,分别为:

· 用于图片特征提取的网络:VGG base
· 用于引出多尺度特征图的网络:Extra
· 用于框位置检测和分类的网络:loc_layers和conf_layers

各自的位置如下图所示,其实可以看出来,这里选择了6个特征图作为框位置检测和分类网络的输入,其中2个来自VGG base,4个来自Extra

(1)用于图片特征提取的网络:VGG base

其中vgg函数定义为:

def vgg(cfg, i, batch_norm=False):"""根据cfg,生成类似VGG16的backbone"""layers = []in_channels = ifor v in cfg:if v == 'M':layers += [nn.MaxPool2d(kernel_size=2, stride=2)]elif v == 'C':# ceil模式就是会把不足square_size的边给保留下来,单独另算,或者也可以理解为在原来的数据上补充了值为-NAN的边layers += [nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]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 = vpool5 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)conv6 = nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6)conv7 = nn.Conv2d(1024, 1024, kernel_size=1)layers += [pool5, conv6,nn.ReLU(inplace=True), conv7, nn.ReLU(inplace=True)]return layers

SSD中的VGG参数设置如下,字典表中的key为输入图片的大小,value为各卷积层输出的维度:

# vgg卷积层结构
base = {'300': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M',512, 512, 512],'512': [],
}

(2)用于引出多尺度特征图的网络:Extra

Extra网络定义如下:

def add_extras(cfg, i, batch_norm=False):"""为了后续的多尺度提取,在VGG Backbone后面添加了卷积网络。Extra layers added to VGG for feature scaling"""layers = []in_channels = iflag = Falsefor k, v in enumerate(cfg):if in_channels != 'S':if v == 'S':layers += [nn.Conv2d(in_channels, cfg[k + 1],kernel_size=(1, 3)[flag], stride=2, padding=1)]else:layers += [nn.Conv2d(in_channels, v, kernel_size=(1, 3)[flag])]flag = not flagin_channels = vreturn layers

其中Extra的具体参数设置如下,字典表中的key为输入图片的大小,value为各卷积层输出的维度:

extras = {'300': [256, 'S', 512, 128, 'S', 256, 128, 256, 128, 256],'512': [],
}

(3)用于框位置检测和分类的网络:loc_layers和conf_layers

loc_layersconf_layers 是定义在函数 multibox 中的,该函数需要输入上面介绍的VGG base 和 Extras ,在提取的6个特征图上的基础上引入简单的一层3x3卷积层进行位置信息和分类信息的提取,定义如下:

def multibox(vgg, extra_layers, cfg, num_classes):""""""loc_layers = []conf_layers = []vgg_source = [21, -2] #在vgg上提取的两个特征图的位置# 在vgg backbone上引出两个分支,做位置回归和分类# 这里记录这两个分支的卷积网络for k, v in enumerate(vgg_source):loc_layers += [nn.Conv2d(vgg[v].out_channels,cfg[k] * 4, kernel_size=3, padding=1)]conf_layers += [nn.Conv2d(vgg[v].out_channels,cfg[k] * num_classes, kernel_size=3, padding=1)]# 在extra layer上引出四个分支,做位置回归和分类# 这里记录这四个分支的卷积网络for k, v in enumerate(extra_layers[1::2], 2):loc_layers += [nn.Conv2d(v.out_channels, cfg[k]* 4, kernel_size=3, padding=1)]conf_layers += [nn.Conv2d(v.out_channels, cfg[k]* num_classes, kernel_size=3, padding=1)]return vgg, extra_layers, (loc_layers, conf_layers)

至此,SSD的网络部分就讲完了,在现在看来倒是觉得一般,但是当时刚发布的话真的是很亮眼的!

2. SSD中的anchor

Anchor是目标检测领域非常常见的一种技巧,在SSD原论文中,作者称之为默认框(default bounding boxes),在代码中,称之为先验框(prior),即一些大小和长宽固定的框的集合,一般和特征图的cell对应(每个特征图的cell中对应固定数量、大小的框)。每个anchor中存储着位置信息分类信息

SSD论文中,作者提到

We associate a set of default bounding boxes with each feature map cell, for multiple feature maps at the top of the network

这意味着,在SSD中不同尺度的特征图上的cell,内置的默认框/anchor尺度是不同的,也就是的特征图负责检测物体,所以较特征图的cell的anchor尺寸较小(和YOLO V3中一样的思想),如下图所示

其次,在同一张特征图上(如上图(b)),不同cell对应的anchor大小比例都是一样的,只不过位置上来说都是以各自cell为中心的。

非常有趣的是,在原论文中,作者把这种通过平移获得同一尺寸特征图所有anchor的行为叫做 tile (贴瓦片/贴砖),就很形象!

代码中,定义SSD所有候选框的程序为:

class PriorBox(object):"""1、计算先验框,根据feature map的每个像素生成box;2、框的中个数为: 38×38×4+19×19×6+10×10×6+5×5×6+3×3×4+1×1×4=87323、 cfg: SSD的参数配置,字典类型"""def __init__(self, cfg):super(PriorBox, self).__init__()self.image_size = cfg['min_dim']# number of priors for feature map location (either 4 or 6)self.num_priors = len(cfg['aspect_ratios'])self.variance = cfg['variance'] or [0.1]self.feature_maps = cfg['feature_maps']self.min_sizes = cfg['min_sizes'] #[21, 45, 99, 153, 207, 261]self.max_sizes = cfg['max_sizes'] #[45, 99, 153, 207, 261, 315]self.steps = cfg['steps'] #各特征图相对于原图的缩放比例self.aspect_ratios = cfg['aspect_ratios']self.clip = cfg['clip']self.version = cfg['name']for v in self.variance:if v <= 0:raise ValueError('Variances must be greater than 0')def forward(self):mean = []for k, f in enumerate(self.feature_maps):#feature map每行for i, j in product(range(f), repeat=2): #feature 每行的每个元素f_k = self.image_size / self.steps[k] #第k层的特征图大小# unit center x,y# 每个特征图单元的中心位置cx = (j + 0.5) / f_kcy = (i + 0.5) / f_k# aspect_ratio: 1# rel size: min_sizes_k = self.min_sizes[k]/self.image_size #最小size相对于原图的比例mean += [cx, cy, s_k, s_k] #方形# aspect_ratio: 1# rel size: sqrt(s_k * s_(k+1))s_k_prime = sqrt(s_k * (self.max_sizes[k]/self.image_size))mean += [cx, cy, s_k_prime, s_k_prime] #方形,比上面的大# rest of aspect ratios# 在方形的基础上改变形状for ar in self.aspect_ratios[k]:mean += [cx, cy, s_k*sqrt(ar), s_k/sqrt(ar)]mean += [cx, cy, s_k/sqrt(ar), s_k*sqrt(ar)]# back to torch landoutput = torch.Tensor(mean).view(-1, 4) #(8732,4)if self.clip:output.clamp_(max=1, min=0) #进行裁剪,不越界return output

可见,SSD中共有不同大小,不同位置的anchor8732个

38×38×4+19×19×6+10×10×6+5×5×6+3×3×4+1×1×4=8732

从上面等式可以看出,SSD的特征图尺寸有以下六种:

38×38特征图:每个cell负责4个anchor
19×19特征图:每个cell负责6个anchor
10×10特征图:每个cell负责6个anchor
5×5特征图:每个cell负责6个anchor
3×3特征图:每个cell负责4个anchor
1×1特征图:每个cell负责4个anchor

其中每个特征图对应的anchor的大小和比例的设置,论文中是这么说的

结合上述的代码,瞬间理解作者的意思呀!代码中的s_k就是对应上述公式中的sk,代码中的s_k_prime对应上述公式中的

,然后一顿操作,获得所有anchor的中心坐标和宽高信息。 到这里,SSD的anchor也是弄清楚了,下面介绍利用anchor进行相应的

match、encode、decode,理解了以下三个你就算是对SSD入门了。

3. SSD的match、encode、decode

(1)decode

decode就是解码的意思,简单来说,就是将某一特定形式的数据转换成另一种形式的数据。在SSD中,解码就是将网络的输出预测的框的信息)转化为真实框的数据(相对于原图),这么做的原因有:

  1. 训练的时候,将预测的框的位置解码回原图大小的框,然后与ground truth真实框求IOU
  2. 测试的时候,将预测的框的位置解码回原图大小的框,然后使用NMS找到要检测的物体

该函数定义在layers/box_utils.py文件中,如下:

def decode(loc, priors, variances):"""将网络的输出通过anchor和ssd中独有的variances解码成(左上坐标,右下坐标)该解码后的坐标是基于原图大小的Args:loc (tensor): location predictions for loc layers,Shape: [num_priors,4]priors (tensor): Prior boxes in center-offset form.Shape: [num_priors,4].variances: (list[float]) Variances of priorboxesReturn:decoded bounding box predictions"""# variation [0.1, 0.2],boxes = torch.cat((priors[:, :2] + loc[:, :2] * variances[0] * priors[:, 2:], #中心坐标的decodepriors[:, 2:] * torch.exp(loc[:, 2:] * variances[1])), 1)boxes[:, :2] -= boxes[:, 2:] / 2boxes[:, 2:] += boxes[:, :2]return boxes

我们可以看出,SSD网络的位置输出,是一个(num_priors,4) 的张量,其中num_priors指的是先验框anchor的个数,即8732个。上述代码中

解码后的box的中心坐标为priors[:,:2]+ loc[:,:2]* variances[0]* priors[:,2:]
解码后的box的宽高为priors[:,2:]* torch.exp(loc[:,2:]* variances[1]

为了更好地说明,我们令

  • 先验框的位置为

  • 解码前的位置为
  • 解码后的位置为
  • 用于调整检测值

有以下关系成立

这样,就将网络的输出通过anchor和SSD中独有的variances解码成相对于原图的(左上坐标,右下坐标)。

(2)encode 和 match

encode就是编码的意思,和上述的解码decode是相对的,即将真实框(基于原图大小)映射到SSD的输出空间上,作为SSD的监督/标签信息,这样做的原因是为了更方便的求解损失函数。但是这里会有一个问题,如何进行匹配,即如何把真实框ground truth和先验框anchor进行对应,也就是哪些anchor负责检测某一真实框ground truth的检测?

这里代码中是这么解决的,该标注的我都标注了:

def match(threshold, truths, priors, variances, labels, loc_t, conf_t, idx):"""Target:把和每个prior box 有最大的IOU的ground truth box进行匹配,同时,编码包围框,返回匹配的索引,对应的置信度和位置Args:threshold: IOU阈值,小于阈值设为bgtruths: ground truth boxes, shape[num_objects,4]priors: 先验框, shape[num_priors,4]variances: prior的方差, list(float)labels: 图片的所有类别,shape[num_obj]loc_t: 用于填充encoded loc 目标张量conf_t: 用于填充encoded conf 目标张量idx: 现在的batch index """# jaccard index 交并比overlaps = jaccard(truths,point_form(priors)) #  #(num_objects,num_priors)# [num_objects,1] 和每个ground truth box 交集最大的 prior boxbest_prior_overlap, best_prior_idx = overlaps.max(1, keepdim=True)# [1,num_priors] 和每个prior box 交集最大的 ground truth boxbest_truth_overlap, best_truth_idx = overlaps.max(0, keepdim=True)best_truth_idx.squeeze_(0) #(num_priors,),每个元素取值范围为(0,num_objects)best_truth_overlap.squeeze_(0)  #(num_priors,)best_prior_idx.squeeze_(1) #(num_objects,),每个元素取值范围为(0,num_priors)best_prior_overlap.squeeze_(1) #(num_objects,)# index_fill(dim,index,val)按照指定的维度轴dim 根据index去对应位置,将原tensor用参数val值填充# 下面的意思是在所有与真实框相交IOU最大的anchor中,找到真实框相交最大IOU的anchor# 相互的意思,彼此是彼此最大,重叠设置为2best_truth_overlap.index_fill_(0, best_prior_idx, 2)  # ensure best prior# 保证每一个ground truth 匹配它的都是具有最大IOU的prior# 根据 best_prior_dix 锁定 best_truth_idx里面的最大IOU prior# num_objects次循环for j in range(best_prior_idx.size(0)):best_truth_idx[best_prior_idx[j]] = j# 提取出所有匹配的ground truth boxmatches = truths[best_truth_idx]          # Shape: [num_priors,4]# # 提取出所有GT框的类别  conf = labels[best_truth_idx] + 1         # Shape: [num_priors]conf[best_truth_overlap < threshold] = 0  # label as background# 编码包围框loc = encode(matches, priors, variances) # # [num_priors,4]# # 保存匹配好的loc和conf到loc_t和conf_t中loc_t[idx] = loc    # [num_priors,4] encoded offsets to learnconf_t[idx] = conf  # [num_priors] top class label for each prior

从上面可以看出来,该match函数主要做了以下几件事:

  1. 求一张图片中所有真实框和先验框的交并比IOU
  2. 求出与真实框最大IOU的先验框anchor位置索引best_prior_idx和IOU大小
  3. 求出与先验框anchor最大IOU的真实框的位置索引best_truth_idx和IOU大小
  4. 如果某个先验框和真实框的IOU互为最大,我们将其重叠IOU设为2
  5. 每个anchor对应的最大IOU的真实框坐标输入到一个张量 match 中,该张量 match 的维度为 [num_priors,4],存放所有先验框对应的真实框的坐标信息
  6. 选择置信度小与阈值threshold的先验框anchor为背景
  7. 对match 按照先验框anchorvariance进行encode,完成 match 到模型输出空间的位置转换
  8. 存储并且返回

到这里,match函数就结束了,其中第7步中的encode是decode的反向过程,代码如下:

def encode(matched, priors, variances):"""将真实框映射到encode空间Encode the variances from the priorbox layers into the ground truth boxeswe have matched (based on jaccard overlap) with the prior boxes.Args:matched: (tensor) Coords of ground truth for each prior in point-formShape: [num_priors, 4].priors: (tensor) Prior boxes in center-offset formShape: [num_priors,4].variances: (list[float]) Variances of priorboxesReturn:encoded boxes (tensor), Shape: [num_priors, 4]"""# dist b/t match center and prior's centerg_cxcy = (matched[:, :2] + matched[:, 2:])/2 - priors[:, :2]# encode varianceg_cxcy /= (variances[0] * priors[:, 2:])# match wh / prior whg_wh = (matched[:, 2:] - matched[:, :2]) / priors[:, 2:]g_wh = torch.log(g_wh) / variances[1]# return target for smooth_l1_lossreturn torch.cat([g_cxcy, g_wh], 1)  # [num_priors,4]

为了更好地说明,我们令

  • 先验框的位置为

  • 编码前的位置为
  • 编码后的位置为
  • 用于调整检测值

有以下关系成立

至此,SSD核心又难懂的一部分就被啃掉了。接下来根据真实框encode后的标签数据和SSD前向推理后的预测值求解损失函数。

4. SSD的损失

论文中给出的损失函数篇幅是

可见,SSD有两个损失函数:

位置损失+分类/置信度损失

论文中也是先对真实框进行匹配和encode,然后对位置/坐标信息使用smoothL1函数求解损失;对前景pos背景neg分别求解交叉熵损失

结合代码(layers/modules/multibox_loss.py):

        loc_data, conf_data, priors = predictionsnum = loc_data.size(0) #batch sizepriors = priors[:loc_data.size(1), :]num_priors = (priors.size(0))num_classes = self.num_classes# match priors (default boxes) and ground truth boxesloc_t = torch.Tensor(num, num_priors, 4)conf_t = torch.LongTensor(num, num_priors)# 对一个batch中每个图片for idx in range(num):truths = targets[idx][:, :-1].datalabels = targets[idx][:, -1].datadefaults = priors.datamatch(self.threshold, truths, defaults, self.variance, labels,loc_t, conf_t, idx)if self.use_gpu:loc_t = loc_t.cuda()conf_t = conf_t.cuda()# wrap targetsloc_t = Variable(loc_t, requires_grad=False)conf_t = Variable(conf_t, requires_grad=False)

模型先将网络输出(预测值)和真实框映射到输出空间(match+encode,真实值)分别求解出来。

        # 正样本的条件pos = conf_t > 0 #背景设为0了num_pos = pos.sum(dim=1, keepdim=True)# 正样本的Localization Loss (Smooth L1)# Shape: [batch,num_priors,4]pos_idx = pos.unsqueeze(pos.dim()).expand_as(loc_data)loc_p = loc_data[pos_idx].view(-1, 4) #预测的位置loc_t = loc_t[pos_idx].view(-1, 4) #真实的位置loss_l = F.smooth_l1_loss(loc_p, loc_t, size_average=False) #(nums_anchors,4)

接着设置正样本的条件,对所有的正样本,使用smoothL1求解位置损失,并取平均(如下)

        # # 正样本个数N = num_pos.data.sum()loss_l /= N

我们知道,如果不对负样本进行限制,那么SSD中的负样本数量是要远大于正样本的。这样会导致模型难以训练,论文中说到:

我们要使用所谓的highest confidence loss对每个anchor进行排序,从而选择损失最大的前top k个框作为负样本,保证正负比例为1:3,这样才会加速优化和训练。作者称这个过程为:hard negative mining,代码中是这样实现的:

        # Compute max conf across batch for hard negative miningbatch_conf = conf_data.view(-1, self.num_classes) #(nums_anchors,num_classes)# gather(dim, index) #  batch_conf.gather(1, conf_t.view(-1, 1))就是取conf_t中物体类别索引在对应的batch_conf中的位置loss_c = log_sum_exp(batch_conf) - batch_conf.gather(1, conf_t.view(-1, 1))# Hard Negative Mining# 需要使用 hard negative mining 将正负样本按照 1:3 的比例把负样本抽样出来# 针对所有batch的confidence,按照置信度误差进行降序排列,取出前top_k个负样本# 置信度误差越大,实际上就是预测背景的置信度越小。# 把所有conf进行logsoftmax处理(均为负值),预测的置信度越小,# 则logsoftmax越小,取绝对值,则|logsoftmax|越大,降序排列-logsoftmax,取前 top_k 的负样本loss_c[pos] = 0  # filter out pos boxes for nowloss_c = loss_c.view(num, -1)_, loss_idx = loss_c.sort(1, descending=True)_, idx_rank = loss_idx.sort(1)# 每个batch中正样本的数目,shape[b,1]num_pos = pos.long().sum(1, keepdim=True)num_neg = torch.clamp(self.negpos_ratio*num_pos, max=pos.size(1)-1)neg = idx_rank < num_neg.expand_as(idx_rank)# Confidence Loss Including Positive and Negative Examples# # shape[b,M] --> shape[b,M,num_classes]pos_idx = pos.unsqueeze(2).expand_as(conf_data)neg_idx = neg.unsqueeze(2).expand_as(conf_data)

挑选出所有正样本负样本后,使用交叉熵求解损失函数:

        # # 提取出所有筛选好的正负样本(预测的和真实的),然后求交叉熵conf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1, self.num_classes)targets_weighted = conf_t[(pos+neg).gt(0)]loss_c = F.cross_entropy(conf_p, targets_weighted, size_average=False)

最后取个平均,分类/置信度损失就完成了。

        # Sum of losses: L(x,c,l,g) = (Lconf(x, c) + αLloc(x,l,g)) / N# # 正样本个数N = num_pos.data.sum()loss_c /= N

至此,SSD的损失函数部分就结束了,整个SSD的讲解也就完成了,还要其他的一些细节,如果想深入了解,可以对论文和代码进行细读!

5. 总结和未来计划

虽然目前SSD用的不多了,但是其思想还是值得借鉴的。未来将继续对目标检测的相关模型:RetinaNet、EfficientDet、YOLO V4进行讲解;顺带着写一下目前所做的一些关于事故检测/交通流参数提取,这些和实际应用相关的文章(等我把论文投出去后,也会加入线程!)本篇文章中如果哪里有问题,欢迎留言区留言,感谢您的批评指正!

ssd网络结构_SSD论文与代码详解相关推荐

  1. ViT:视觉Transformer backbone网络ViT论文与代码详解

    Visual Transformer Author:louwill Machine Learning Lab 今天开始Visual Transformer系列的第一篇文章,主题是Vision Tran ...

  2. PointNet++论文及代码详解

    这篇也是借鉴知乎刘昕宸大佬的文章 https://zhuanlan.zhihu.com/p/266324173 1- motivation PointNet++是对PointNet的改进 想读懂Poi ...

  3. Alphapose论文代码详解

    注:B站有相应视频,点击此链接即可跳转观看https://www.bilibili.com/video/BV1hb4y117mu/ 第1节 人体姿态估计的基本概念 第2节:Alphapose 2.1A ...

  4. 【卷积神经网络结构专题】一文详解AlexNet(附代码实现)

    关注上方"深度学习技术前沿",选择"星标公众号", 资源干货,第一时间送达! [导读]本文是卷积神经网络结构系列专题第二篇文章,前面我们已经介绍了第一个真正意义 ...

  5. FPN论文解读 和 代码详解

    FPN论文解读 和 代码详解 论文地址:[Feature Pyramid Networks for Object Detection](1612.03144v2.pdf (arxiv.org)) 代码 ...

  6. Pytorch | yolov3原理及代码详解(一)

    YOLO相关原理 : https://blog.csdn.net/leviopku/article/details/82660381 https://www.jianshu.com/p/d13ae10 ...

  7. yoloV3代码详解(注释)

    原文链接:https://www.cnblogs.com/hujinzhou/p/guobao_2020_3_13.html yolo3各部分代码详解(超详细) </h1><div ...

  8. sgd 参数 详解_代码笔记--PC-DARTS代码详解

    DARTS是可微分网络架构搜搜索,PC-DARTS是DARTS的拓展,通过部分通道连接的方法在网络搜索过程中减少计算时间的内存占用.接下来将会结合论文和开源代码来详细介绍PC-DARTS. 1 总体框 ...

  9. 从PointNet到PointNet++理论及代码详解

    从PointNet到PointNet++理论及代码详解 1. 点云是什么 1.1 三维数据的表现形式 1.2 为什么使用点云 1.3 点云上以往的相关工作 2. PointNet 2.1 基于点云的置 ...

最新文章

  1. (十)MySQL日志
  2. perl 分析mysql binlog
  3. JDK 14中更好的NPE消息
  4. string、stringbuilder、stringbuffer区别
  5. 基于ASP.net的电力系统分析精品课程网站
  6. form跳转php页面,form表单页面跳转方式提交练习
  7. 标贝科技推出情感合成 TTS,让语音交互更有温度!
  8. RHEL6入门系列之三十一,管理计划任务
  9. java %3e%3e位移_JAVA移位运算符
  10. 我的世界怎么在服务器中显示键位,我的世界基础键位操作介绍 | 我的世界 | MC世界侠...
  11. java面试的职业规划怎么说_java面试技巧-职业规划有技巧
  12. 华为数通笔记-DHCPv6原理与实验
  13. 深入理解Nginx负载均衡和反向代理_学习笔记
  14. Saved Blogs
  15. BigDecimal.ROUND_HALF_EVEN (银行家算法)
  16. c语言 strcpy作用,c语言中的strcpy什么意思,简单点解释
  17. 基础算法题:723. PUM
  18. html如何引入iconfont官网图标
  19. 如何区分“衬线体字”和“无衬线体字”?Linux Mint中如何安装字体?
  20. 红旗linux考试,红旗Linux认证考试介绍

热门文章

  1. Java基础学习总结(16)——Java制作证书的工具keytool用法总结
  2. linux 故障监控必备五款软件
  3. css样式之background详解(格子效果)
  4. 麻烦大家看了我的文章觉得有用就关注我下
  5. mysql安装图解 mysql图文安装教程(详细说明)
  6. Fedora 18下 升级内核后VirtualBox不能正常使用的问题
  7. django in的一点心得
  8. vue 封装dialog_element-ui 封装dialog组件
  9. python递归函数是指_python 函数递归作业求解析
  10. android 返回字符串,android – 如何从异步回调使用Retrofit返回String或JSONObject?