MMDetection实战:MMDetection训练与测试

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第19天,点击查看活动详情
@[toc]

摘要

MMDetection是商汤和港中文大学针对目标检测任务推出的一个开源项目,它基于Pytorch实现了大量的目标检测算法,把数据集构建、模型搭建、训练策略等过程都封装成了一个个模块,通过模块调用的方式,我们能够以很少的代码量实现一个新算法,大大提高了代码复用率

GitHub链接:https://github.com/open-mmlab/mmdetection。

Gitee链接:https://gitee.com/open-mmlab/mmdetection。

主分支代码目前支持 PyTorch 1.5 以上的版本。主要特性:

  • 模块化设计

    MMDetection 将检测框架解耦成不同的模块组件,通过组合不同的模块组件,用户可以便捷地构建自定义的检测模型

  • 丰富的即插即用的算法和模型

    MMDetection 支持了众多主流的和最新的检测算法,例如 Faster R-CNN,Mask R-CNN,RetinaNet 等。

  • 速度快

    基本的框和 mask 操作都实现了 GPU 版本,训练速度比其他代码库更快或者相当,包括 Detectron2, maskrcnn-benchmarkSimpleDet

  • 性能高

    MMDetection 这个算法库源自于 COCO 2018 目标检测竞赛的冠军团队 MMDet 团队开发的代码,之后持续进行了改进和提升。

配置文件参数详解

faster_rcnn_r50_fpn_1x_coco.py文件为例,这个文件包含四个文件,分别是:faster_rcnn_r50_fpn.py、coco_detection.py、schedule_1x.py、default_runtime.py。

configs/_base_/schedules/schedule_1x.py

  1. optimizer = dict(type=SGD, lr=0.02, momentum=0.9, weight_decay=0.0001)# 设置优化器类型
  2. optimizer_config = dict(grad_clip=None) # 梯度裁剪配置
  3. #optimizer_config = dict(
  4. # _delete_=True, grad_clip=dict(max_norm=35, norm_type=2))
  5. # lr 参数
  6. lr_config = dict(
  7. policy=step, # lr decay的方式,其余的还有consine cyclic
  8. warmup=linear, # 初始的学习率增加的策略为线性增加
  9. warmup_iters=500, # warmup迭代500次
  10. warmup_ratio=0.001, # warmup的初始学习比率。
  11. step=[8, 11]) # 在8-11个epoch后开始进行lr decay
  12. runner = dict(type=EpochBasedRunner, max_epochs=12) # runner配置,默认epoch为12

faster_rcnn_r50_fpn.py

  1. # model settings
  2. model = dict(
  3. type=FasterRCNN,#model类型
  4. backbone=dict(
  5. type=ResNet,#backone类型
  6. depth=50,#网络层数
  7. num_stages=4,# resnetstage数量
  8. out_indices=(0, 1, 2, 3), # 输出的stage的序号
  9. frozen_stages=1,# 冻结的stage数量,即该stage不更新参数,-1表示所有的stage都更新参数
  10. norm_cfg=dict(type=BN, requires_grad=True),#表示所采用的归一化算子,一般是 BN 或者 GNrequires_grad 表示该算子是否需要梯度,也就是是否进行参数更新
  11. norm_eval=True,#控制整个骨架网络的归一化算子是否需要变成 eval 模式
  12. style=pytorch,# 网络风格:如果设置pytorch,则stride2的层是conv3x3的卷积层;如果设置caffe,则stride2的层是第一个conv1x1的卷积层
  13. init_cfg=dict(type=Pretrained, checkpoint=torchvision://resnet50)),# 表明backbone使用预训练参数,标注其位置
  14. neck=dict(
  15. type=FPN,# FPN特征融合neck
  16. in_channels=[256, 512, 1024, 2048],# FPN接受的channels,和backnone resnetstage2-5的输出channels对应
  17. out_channels=256,# feature pyramid每一层的输出channel
  18. num_outs=5),# 输出的feature pyramid特征层数
  19. rpn_head=dict(
  20. type=RPNHead,# RPN网络类型
  21. in_channels=256,# RPN网络的输入通道数
  22. feat_channels=256,# 特征层的通道数
  23. anchor_generator=dict(
  24. type=AnchorGenerator,
  25. scales=[8],# 生成的anchorbaselenbaselen = sqrt(w*h),whanchor的宽和高
  26. ratios=[0.5, 1.0, 2.0],# anchor的宽高比
  27. strides=[4, 8, 16, 32, 64]),# 在每个特征层上的anchor的步长(对应于原图)
  28. bbox_coder=dict(
  29. type=DeltaXYWHBBoxCoder,
  30. target_means=[.0, .0, .0, .0],# 均值
  31. target_stds=[1.0, 1.0, 1.0, 1.0]),# 均值
  32. loss_cls=dict(
  33. type=CrossEntropyLoss, use_sigmoid=True, loss_weight=1.0),
  34. loss_bbox=dict(type=L1Loss, loss_weight=1.0)),
  35. roi_head=dict(
  36. type=StandardRoIHead,
  37. bbox_roi_extractor=dict(
  38. type=SingleRoIExtractor,
  39. roi_layer=dict(type=RoIAlign, output_size=7, sampling_ratio=0),
  40. out_channels=256,
  41. featmap_strides=[4, 8, 16, 32]),
  42. bbox_head=dict(
  43. type=Shared2FCBBoxHead,# 对应head
  44. in_channels=256,# head接受的是feature pyramid的输出,in_channels表示进入head时的通道数是256
  45. fc_out_channels=1024,
  46. roi_feat_size=7,
  47. num_classes=80,# 使用coco数据集,所以是80
  48. bbox_coder=dict(
  49. type=DeltaXYWHBBoxCoder,
  50. target_means=[0., 0., 0., 0.],
  51. target_stds=[0.1, 0.1, 0.2, 0.2]),
  52. reg_class_agnostic=False,
  53. loss_cls=dict(
  54. type=CrossEntropyLoss, use_sigmoid=False, loss_weight=1.0),
  55. loss_bbox=dict(type=L1Loss, loss_weight=1.0))),
  56. # model training and testing settings
  57. train_cfg=dict(
  58. rpn=dict(
  59. assigner=dict(
  60. type=MaxIoUAssigner,# RPN网络的正负样本划分
  61. pos_iou_thr=0.7,# RPN网络的正负样本划分
  62. neg_iou_thr=0.3,# 负样本的iou阈值
  63. min_pos_iou=0.3,# 正样本的iou最小值。如果assignground truthanchors中最大的IOU低于0.3,则忽略所有的anchors,否则保留最大IOUanchor
  64. match_low_quality=True,
  65. ignore_iof_thr=-1),# 忽略bbox的阈值,当ground truth中包含需要忽略的bbox时使用,-1表示不忽略
  66. sampler=dict(
  67. type=RandomSampler,# 正负样本提取器类型
  68. num=256,# 需提取的正负样本数量
  69. pos_fraction=0.5,# 正样本比例
  70. neg_pos_ub=-1,# 最大负样本比例,大于该比例的负样本忽略,-1表示不忽略
  71. add_gt_as_proposals=False),# ground truth加入proposal作为正样本
  72. allowed_border=-1,# 允许在bbox周围外扩一定的像素
  73. pos_weight=-1,# 正样本权重,-1表示不改变原始的权重
  74. debug=False),# debug模式
  75. rpn_proposal=dict(
  76. nms_pre=2000,
  77. max_per_img=1000,
  78. nms=dict(type=nms, iou_threshold=0.7),# nms阈值
  79. min_bbox_size=0),
  80. rcnn=dict(
  81. assigner=dict(
  82. type=MaxIoUAssigner,# RCNN网络正负样本划分
  83. pos_iou_thr=0.5,# 正样本的iou阈值
  84. neg_iou_thr=0.5,# 负样本的iou阈值
  85. min_pos_iou=0.5,# 正样本的iou最小值。如果assignground truthanchors中最大的IOU低于0.3,则忽略所有的anchors,否则保留最大IOUanchor
  86. match_low_quality=False,
  87. ignore_iof_thr=-1),# 忽略bbox的阈值,当ground truth中包含需要忽略的bbox时使用,-1表示不忽略
  88. sampler=dict(
  89. type=RandomSampler,# 正负样本提取器类型
  90. num=512,# 需提取的正负样本数量
  91. pos_fraction=0.25,# 正样本比例
  92. neg_pos_ub=-1,# 最大负样本比例,大于该比例的负样本忽略,-1表示不忽略
  93. add_gt_as_proposals=True),# ground truth加入proposal作为正样本
  94. pos_weight=-1,# 正样本权重,-1表示不改变原始的权重
  95. debug=False)),
  96. test_cfg=dict(
  97. rpn=dict(
  98. nms_pre=1000,# nms之前保留的的得分最高的proposal数量
  99. max_per_img=1000,
  100. nms=dict(type=nms, iou_threshold=0.7),
  101. min_bbox_size=0), # 最小bbox尺寸
  102. rcnn=dict(
  103. score_thr=0.05,
  104. nms=dict(type=nms, iou_threshold=0.5),# nms阈值
  105. max_per_img=100)
  106. # soft-nms is also supported for rcnn testing
  107. # e.g., nms=dict(type=soft_nms, iou_threshold=0.5, min_score=0.05)
  108. ))

环境准备

CUDA:11.3

新建虚拟环境openmm

  1. conda create --name openmm python=3.7

然后,激活环境。

Win10执行命令:

  1. activate openmm

UBuntu执行命令:

  1. source activate openmm

进入虚拟环境后,安装pytorch,输入命令:

  1. conda install pytorch torchvision torchaudio cudatoolkit=11.3

image-20220507215920917

安装mmcv,执行命令:

  1. pip install mmcv-full

安装mmcv-full,等待的时间较长。如果不报错误,耐心等待即可。

image-20220507222635359

安装完成后,下载mmdetection, 地址链接:https://gitee.com/open-mmlab/mmdetection。

下载完成后,解压,然后pycharm打开。

添加刚才新建的虚拟环境。

image-20220507223223295

image-20220507223241733

在Terminal中激活openmm虚拟环境,防止虚拟环境没有切换过来。

image-20220507223425419

然后,安装mmdet,在Terminal中执行命令:

  1. python setup.py install

在安装mmdet的过程中,会自动下载所需要的安装包。如果存在不能下载的情况,需要单独安装。直到出现下图即可。

image-20220507223745756

验证环境

在工程的根目录新建checkpoints文件夹,下载预训练权重文件,链接:

  1. http://download.openmmlab.com/mmdetection/v2.0/faster_rcnn/faster_rcnn_r50_fpn_1x_coco/faster_rcnn_r50_fpn_1x_coco_20200130-047c8118.pth

下载完成后,将其放入到checkpoints文件夹

新建demo.py文件,插入代码:

  1. from mmdet.apis import init_detector, inference_detector
  2. config_file = configs/faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py
  3. # 从 model zoo 下载 checkpoint 并放在 `checkpoints/` 文件下
  4. # 网址为: http://download.openmmlab.com/mmdetection/v2.0/faster_rcnn/faster_rcnn_r50_fpn_1x_coco/faster_rcnn_r50_fpn_1x_coco_20200130-047c8118.pth
  5. checkpoint_file = checkpoints/faster_rcnn_r50_fpn_1x_coco_20200130-047c8118.pth
  6. device = cuda:0
  7. img=demo/demo.jpg
  8. # 初始化检测器
  9. model = init_detector(config_file, checkpoint_file, device=device)
  10. # 推理演示图像
  11. result=inference_detector(model, img)
  12. model.show_result(img, result, out_file=result.jpg)

运行代码:

image-20220507224645287

看到这张图说明环境没有问题。

接下来,使用这个环境训练自定义数据集。

训练

制作数据集

Labelme标注的数据集地址链接:

https://download.csdn.net/download/hhhhhhhhhhwwwwwwwwww/63242994?spm=1001.2014.3001.5503

有32个类别,分别是:‘c17’, ‘c5’, ‘helicopter’, ‘c130’, ‘f16’, ‘b2’, ‘other’, ‘b52’, ‘kc10’, ‘command’, ‘f15’, ‘kc135’, ‘a10’, ‘b1’, ‘aew’, ‘f22’, ‘p3’, ‘p8’, ‘f35’, ‘f18’, ‘v22’, ‘f4’, ‘globalhawk’, ‘u2’, ‘su-27’, ‘il-38’, ‘tu-134’, ‘su-33’, ‘an-70’, ‘su-24’, ‘tu-22’, ‘il-76’。

先将其转为COCO数据集,转换代码如下:

  1. # -*- coding:utf-8 -*-
  2. # !/usr/bin/env python
  3. import json
  4. import os
  5. import shutil
  6. from labelme import utils
  7. import numpy as np
  8. import glob
  9. import PIL.Image
  10. labels={c17:0,c5:1,helicopter:2,c130:3,f16:4,
  11. b2:5,other:6,b52:7,kc10:8,command:9,f15:10,
  12. kc135:11,a10:12,b1:13,aew:14,f22:15,p3:16,p8:17,
  13. f35:18,f18:19,v22:20,f4:21,globalhawk:22,u2:23,su-27:24,
  14. il-38:25,tu-134:26,su-33:27,an-70:28,su-24:29,tu-22:30,il-76:31}
  15. class MyEncoder(json.JSONEncoder):
  16. def default(self, obj):
  17. if isinstance(obj, np.integer):
  18. return int(obj)
  19. elif isinstance(obj, np.floating):
  20. return float(obj)
  21. elif isinstance(obj, np.ndarray):
  22. return obj.tolist()
  23. else:
  24. return super(MyEncoder, self).default(obj)
  25. class labelme2coco(object):
  26. def __init__(self, labelme_json=[], save_json_path=./tran.json):
  27. :param labelme_json: 所有labelme的json文件路径组成的列表
  28. :param save_json_path: json保存位置
  29. self.labelme_json = labelme_json
  30. self.save_json_path = save_json_path
  31. self.images = []
  32. self.categories = []
  33. self.annotations = []
  34. # self.data_coco = {}
  35. self.label = []
  36. self.annID = 1
  37. self.height = 0
  38. self.width = 0
  39. self.save_json()
  40. def data_transfer(self):
  41. for num, json_file in enumerate(self.labelme_json):
  42. imagePath=json_file.split(.)[0]+.jpg
  43. imageName=imagePath.split(\\)[-1]
  44. # print(imageName)
  45. with open(json_file, r) as fp:
  46. data = json.load(fp) # 加载json文件
  47. self.images.append(self.image(data, num,imageName))
  48. for shapes in data[shapes]:
  49. label = shapes[label].lower()
  50. if label not in self.label:
  51. self.categories.append(self.categorie(label))
  52. self.label.append(label)
  53. points = shapes[points] # 这里的point是用rectangle标注得到的,只有两个点,需要转成四个点
  54. # points.append([points[0][0],points[1][1]])
  55. # points.append([points[1][0],points[0][1]])
  56. self.annotations.append(self.annotation(points, label, num))
  57. self.annID += 1
  58. def image(self, data, num,imagePath):
  59. image = {}
  60. img = utils.img_b64_to_arr(data[imageData]) # 解析原图片数据
  61. # img=io.imread(data[imagePath]) # 通过图片路径打开图片
  62. # img = cv2.imread(data[imagePath], 0)
  63. height, width = img.shape[:2]
  64. img = None
  65. image[height] = height
  66. image[width] = width
  67. image[id] = num + 1
  68. # image[file_name] = data[imagePath].split(/)[-1]
  69. image[file_name] = imagePath
  70. self.height = height
  71. self.width = width
  72. return image
  73. def categorie(self, label):
  74. categorie = {}
  75. categorie[supercategory] = Cancer
  76. categorie[id] = labels[label] # 0 默认为背景
  77. categorie[name] = label
  78. return categorie
  79. def annotation(self, points, label, num):
  80. annotation = {}
  81. annotation[segmentation] = [list(np.asarray(points).flatten())]
  82. annotation[iscrowd] = 0
  83. annotation[image_id] = num + 1
  84. # annotation[bbox] = str(self.getbbox(points)) # 使用list保存json文件时报错(不知道为什么)
  85. # list(map(int,a[1:-1].split(,))) a=annotation[bbox] 使用该方式转成list
  86. annotation[bbox] = list(map(float, self.getbbox(points)))
  87. annotation[area] = annotation[bbox][2] * annotation[bbox][3]
  88. # annotation[category_id] = self.getcatid(label)
  89. annotation[category_id] = self.getcatid(label) # 注意,源代码默认为1
  90. # print(label,annotation[category_id])
  91. annotation[id] = self.annID
  92. return annotation
  93. def getcatid(self, label):
  94. for categorie in self.categories:
  95. if label == categorie[name]:
  96. return categorie[id]
  97. return 1
  98. def getbbox(self, points):
  99. # img = np.zeros([self.height,self.width],np.uint8)
  100. # cv2.polylines(img, [np.asarray(points)], True, 1, lineType=cv2.LINE_AA) # 画边界线
  101. # cv2.fillPoly(img, [np.asarray(points)], 1) # 画多边形 内部像素值为1
  102. polygons = points
  103. mask = self.polygons_to_mask([self.height, self.width], polygons)
  104. return self.mask2box(mask)
  105. def mask2box(self, mask):
  106. 从mask反算出其边框
  107. mask:[h,w] 0、1组成的图片
  108. 1对应对象,只需计算1对应的行列号(左上角行列号,右下角行列号,就可以算出其边框)
  109. # np.where(mask==1)
  110. index = np.argwhere(mask == 1)
  111. rows = index[:, 0]
  112. clos = index[:, 1]
  113. # 解析左上角行列号
  114. left_top_r = np.min(rows)+1 # y
  115. left_top_c = np.min(clos)+1 # x
  116. # 解析右下角行列号
  117. right_bottom_r = np.max(rows)
  118. right_bottom_c = np.max(clos)
  119. # return [(left_top_r,left_top_c),(right_bottom_r,right_bottom_c)]
  120. # return [(left_top_c, left_top_r), (right_bottom_c, right_bottom_r)]
  121. # return [left_top_c, left_top_r, right_bottom_c, right_bottom_r] # [x1,y1,x2,y2]
  122. return [left_top_c, left_top_r, right_bottom_c - left_top_c,
  123. right_bottom_r - left_top_r] # [x1,y1,w,h] 对应COCO的bbox格式
  124. def polygons_to_mask(self, img_shape, polygons):
  125. mask = np.zeros(img_shape, dtype=np.uint8)
  126. mask = PIL.Image.fromarray(mask)
  127. xy = list(map(tuple, polygons))
  128. PIL.ImageDraw.Draw(mask).polygon(xy=xy, outline=1, fill=1)
  129. mask = np.array(mask, dtype=bool)
  130. return mask
  131. def data2coco(self):
  132. data_coco = {}
  133. data_coco[images] = self.images
  134. data_coco[categories] = self.categories
  135. data_coco[annotations] = self.annotations
  136. return data_coco
  137. def save_json(self):
  138. self.data_transfer()
  139. self.data_coco = self.data2coco()
  140. # 保存json文件
  141. json.dump(self.data_coco, open(self.save_json_path, w), indent=4, cls=MyEncoder) # indent=4 更加美观显示
  142. def copy_image(dirs,files,image_type):
  143. for txt in files:
  144. image_path=txt.split(.)[0]+"."+image_type
  145. image_name=image_path.replace(\\,/).split(/)[-1]
  146. new_path=os.path.join(dirs,image_name)
  147. shutil.copyfile(image_path, new_path)
  148. labelme_json = glob.glob(USA-Labelme/*.json)
  149. from sklearn.model_selection import train_test_split
  150. trainval_files, test_files = train_test_split(labelme_json, test_size=0.2, random_state=55)
  151. print(trainval_files)
  152. os.makedirs(train2017,exist_ok=True)
  153. os.makedirs(val2017,exist_ok=True)
  154. copy_image(train2017,trainval_files,jpg)
  155. copy_image(val2017,test_files,jpg)
  156. labelme2coco(trainval_files, instances_train2017.json)
  157. labelme2coco(test_files, instances_val2017.json)

在mmdetection-master的根目录下面,新建data文件夹,然后再data文件夹下面新建coco文件夹,在coco文件夹下面新建annotations文件夹,将训练集和验证集的json放进去。将train2017文件夹和val2017文件夹放到coco文件夹下面,目录如下:

  1. mmdetection
  2. ├── data
  3. ├── coco
  4. ├── annotations
  5. ├── train2017
  6. └── val2017

如下图:

image-20220507225338447

到这里数据集制作完成了。

修改配置文件

configs/算法/配置文件。打开配置文件修改num_classes的个数,COCO默认是80,我们按照实际的类别修改即可。

例:configs/yolo/yolov3_d53_mstrain-608_273e_coco.py

image-20220508071047840

但是,有的模型在的类别找不到,那么我们如何找到呢?比如

configs/ssd/ssd300coco.py,这个配置文件里面就没有num_classes这个字段,我们寻找最上面的_base\字段。

image-20220508071409799

也有ssd300.py这个文件,将其打开。

image-20220508071537098

找到了num_classes这个字段,将其修改为数据集的类别个数。我们本次使用的数据集的类别是32,所以将其修改为32。

修改该学习率,路径“configs/ssd/ssd300_coco.py”。如下图:

image-20220508073839074

将2e-3改为2e-4,否则会出现loss为NAN的问题。

修改该BatchSize,路径“configs/ssd/ssd300_coco.py”。如下图:

image-20220508082041406

这是针对每张显卡设置Batchsize。调整到合适的大小就可以训练了。

修改epoch,在configs/ssd/ssd300_coco.py中添加

  1. runner = dict(type=EpochBasedRunner, max_epochs=500)

image-20220508114039780

修改数据集的类别

mmdet/core/evaluation/classnames.py找到def coco_classes():将COCO类别替换为自己数据的类别。本例是:

  1. def coco_classes():
  2. return [
  3. c17, c5, helicopter, c130, f16, b2,
  4. other, b52, kc10, command, f15,
  5. kc135, a10, b1, aew, f22, p3, p8,
  6. f35, f18, v22, f4, globalhawk, u2, su-27,
  7. il-38, tu-134, su-33, an-70, su-24, tu-22,
  8. il-76]

mmdet/datasets/coco.py找到class CoCoDataset(CustomDataset):将COCO的类别替换为自己数据集的类别。本例如下:

  1. class CocoDataset(CustomDataset):
  2. CLASSES = (c17, c5, helicopter, c130, f16, b2,
  3. other, b52, kc10, command, f15,
  4. kc135, a10, b1, aew, f22, p3, p8,
  5. f35, f18, v22, f4, globalhawk, u2, su-27,
  6. il-38, tu-134, su-33, an-70, su-24, tu-22,
  7. il-76)
  8. PALETTE = [(220, 20, 60), (119, 11, 32), (0, 0, 142), (0, 0, 230),
  9. (106, 0, 228), (0, 60, 100), (0, 80, 100), (0, 0, 70),
  10. (0, 0, 192), (250, 170, 30), (100, 170, 30), (220, 220, 0),
  11. (175, 116, 175), (250, 0, 30), (165, 42, 42), (255, 77, 255),
  12. (0, 226, 252), (182, 182, 255), (0, 82, 0), (120, 166, 157),
  13. (110, 76, 0), (174, 57, 255), (199, 100, 0), (72, 0, 118),
  14. (255, 179, 240), (0, 125, 92), (209, 0, 151), (188, 208, 182),
  15. (0, 220, 176), (255, 99, 164), (92, 0, 73), (133, 129, 255)]

开始训练

由于修改了参数,在训练之前还要重新编译一次。否则之前修改的参数不会生效。再次执行命令:

  1. python setup.py install

image-20220508082827140

然后就可以开始训练了。执行命令:

  1. python tools/train.py configs/ssd/ssd300_coco.py

image-20220508082929152对train.py重要参数的解析:

  • --work-dir:指定训练保存模型和日志的路径
  • --resume-from:从预训练模型chenkpoint中恢复训练
  • --no-validate:训练期间不评估checkpoint
  • --gpus:指定训练使用GPU的数量(仅适用非分布式训练)
  • --gpu-ids: 指定使用哪一块GPU(仅适用非分布式训练)
  • --seed:随机种子
  • --deterministic:是否为CUDNN后端设置确定性选项
  • --options: arguments in dict
  • --launcher: {none,pytorch,slurm,mpi} job launcher
  • --local_rank: LOCAL_RANK
  • --autoscale-lr: automatically scale lr with the number of

    测试

测试执行代码:

  1. python tools/test.py configs/ssd/ssd300_coco.py work_dirs/ssd300_coco/epoch_348.pth --out ./test_result/mask_rcnn_r50_fpn_1x/latest.pkl --eval bbox segm --s
  2. how

image-20220509211122119

然后我们就可以看到测试结果:

image-20220509211204795

完整代码和数据集:

https://download.csdn.net/download/hhhhhhhhhhwwwwwwwwww/85331635


文章标签:

原文连接:https://juejin.cn/post/7110207134377181221

相关推荐

翟佳:高可用、强一致、低延迟——BookKeeper的存储实现

管正雄:基于预训练模型、智能运维的QA生成算法落地

814. 二叉树剪枝 : 简单递归运用题

【综合笔试题】难度 3.5\u002F5,多解法热门二叉树笔试题

【java刷算法】牛客—剑指offer3栈、数组、递归、二分法的初步练习

leetcode 2342. Max Sum of a Pair With Equal Sum of Digits (python)

22张图带你深入剖析前缀、中缀、后缀表达式以及表达式求值

随机数索引(一题双解)【Leetcode每日(4.25)一题】C++

1260. 二维网格迁移 : 简单构造模拟题

坚持用C++刷牛客题(剑指offer专题)

日拱一卒,麻省理工教你信息安全和密码学

C语言——三种方式实现学生信息管理

快速排序及优化

萌新也能看懂的KMP算法

简答一波 HashMap 常见八股面试题 —— 算法系列(2)

素数算法(Prime Num Algorithm)

必须收藏!双目立体匹配算法:Patch Match Stereo实用详解教程

有哪些高质量的自学网站?

731. 我的日程安排表 II : 线段树(动态开点)的两种方式

LeetCode周赛302,这也太卷了,20分钟ak也只有300名……