1、端到端训练的具体含义是什么(一个loss function搞定训练,只需关注输入端和输出端)
1、千赞博客(YOLOv3,内附有v1,v2链接):yolo系列之yolo v3【深度解析】_木盏-CSDN博客_yolov3
2、 知乎江大白大佬(对新手快速了解很友好,但知识有一点点没覆盖到):
(有对应的视频讲解,跟博客内容基本一致,喜欢视频学习的可以到知乎上搜索)
3. yolov4相关的边缘技术(数据增强之类的,有些还比较详细,可以拓展一下):
想读懂YOLOV4,你需要先了解下列技术(一) - 知乎
4、B站子豪兄(v1-v3,讲的非常透):
5、 论文在这些博客里面基本都有链接
6. 本文的内容可能比较零散,还没整理,想具体了解Yolo可以看上面的博客或者我的其他文章
数据集配置文件coco128里面改了训练集的路径到自己的训练集,好像图片集可以直接连接到标签。(那如果标签文件夹改个名字呢,还能找到吗,它们之间是怎么联系的呢)这里验证集有点小问题,必须用images这个名字,换val什么之类的找不到测试集图片,后面看看怎么解决
最后训练了17个epoch左右就收敛的差不多了,
def parse_opt():
parser = argparse.ArgumentParser()
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model path(s)')
parser.add_argument('--source', type=str, default=ROOT / 'data/images', help='file/dir/URL/glob, 0 for webcam')
parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w')
parser.add_argument('--conf-thres', type=float, default=0.25, help='confidence threshold')
parser.add_argument('--iou-thres', type=float, default=0.45, help='NMS IoU threshold')
parser.add_argument('--max-det', type=int, default=1000, help='maximum detections per image')
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--view-img', action='store_true', help='show results')
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
parser.add_argument('--save-crop', action='store_true', help='save cropped prediction boxes')
parser.add_argument('--nosave', action='store_true', help='do not save images/videos')
parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --classes 0, or --classes 0 2 3')
parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS')
parser.add_argument('--augment', action='store_true', help='augmented inference')
parser.add_argument('--visualize', action='store_true', help='visualize features')
parser.add_argument('--update', action='store_true', help='update all models')
parser.add_argument('--project', default=ROOT / 'runs/detect', help='save results to project/name')
parser.add_argument('--name', default='exp', help='save results to project/name')
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
parser.add_argument('--line-thickness', default=3, type=int, help='bounding box thickness (pixels)')
parser.add_argument('--hide-labels', default=False, action='store_true', help='hide labels')
parser.add_argument('--hide-conf', default=False, action='store_true', help='hide confidences')
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')
opt = parser.parse_args()
opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand
print_args(FILE.stem, opt)
#FILE = Path(__file__).resolve()
return opt
#2021.10.31最新代码
weights:训练的权重
source:测试数据,可以是图片/视频路径,也可以是'0'(电脑自带摄像头),也可以是rtsp等视频流
output:网络预测之后的图片/视频的保存路径
img-size:网络输入图片大小
conf-thres:置信度阈值
iou-thres:做nms的iou阈值
device:设置设备
view-img:是否展示预测之后的图片/视频,默认False
save-txt:是否将预测的框坐标以txt文件形式保存,默认False
classes:设置只保留某一部分类别,形如0或者0 2 3
agnostic-nms:进行nms是否也去除不同类别之间的框,默认False(NMS是去除重叠比较大的同类的框,这个参数可能是去除不同类别的)
augment:推理的时候进行多尺度,翻转等操作(TTA)推理
update:如果为True,则对所有模型进行strip_optimizer操作,去除pt文件中的优化器等信息,默认为False
default是默认的参数,即使不写上去也会执行;action='store_true是写上去这个名字比如--view-img就为true;nargs='+'是可以输入多个参数(filter by class: --class 0, or --class 0 2 3)
FILE = Path(__file__).resolve()
Path----可以进行调用的PurePath子类
为啥Path()里面放__file__,
path().resolve:将路径设置为绝对路径,解析路径上的所有符号链接,并将其规范化(例如在Windows下将斜杠转换为反斜杠)。
ROOT得到YOLOv5根路径:
ROOT = FILE.parents[0] # YOLOv5 root directory
添加ROOT到系统路径:
if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) # add ROOT to PATH
ROOT = Path(os.path.relpath(ROOT, Path.cwd()))
os.path.relpath是求相对路径的(把绝对路径转化成相对路径,前面那个相对于后面那个的相对路径):,【python3学习笔记】os.path.relpath(path[, start])_Murphy.AI 的文章-CSDN博客
Path.cwd()获取当前路径,防止路径在ROOT前面,把这个补到后面weights等文件的路径中
def run(weights=ROOT / 'yolov5s.pt', # model.pt path(s)
source=ROOT / 'data/images', # file/dir/URL/glob, 0 for webcam
imgsz=640, # inference size (pixels)
conf_thres=0.25, # confidence threshold
iou_thres=0.45, # NMS IOU threshold
max_det=1000, # maximum detections per image
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu
view_img=False, # show results
save_txt=False, # save results to *.txt
save_conf=False, # save confidences in --save-txt labels
save_crop=False, # save cropped prediction boxes
nosave=False, # do not save images/videos
classes=None, # filter by class: --class 0, or --class 0 2 3
agnostic_nms=False, # class-agnostic NMS
augment=False, # augmented inference
visualize=False, # visualize features
update=False, # update all models
project=ROOT / 'runs/detect', # save results to project/name
name='exp', # save results to project/name
exist_ok=False, # existing project/name ok, do not increment
line_thickness=3, # bounding box thickness (pixels)
hide_labels=False, # hide labels
hide_conf=False, # hide confidences
half=False, # use FP16 half-precision inference
dnn=False, # use OpenCV DNN for ONNX inference
):
iou_thres:NMS IOU threshold
max_det=1000---------最大检测数量
visualize: --------可视化功能
project:结果保存在runs/detect中
name:文件名称'exp'
exist_ok:existing project/name ok, do not increment
opt = parser.parse_args()
opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand
print_args(FILE.stem, opt)
opt.imgsz这行是前面放参数的时候 ,default=[640],长度是1,调整成2
上面这几行要干嘛就没看懂了(这个是在代码最后面文章)
save_img = not nosave and not source.endswith('.txt')
同时满足nosave和后缀(.txt)都not(为什么source以.txt结尾img就不保存)
webcam = source.isnumeric() or source.endswith('.txt') or source.lower().startswith(
('rtsp://', 'rtmp://', 'http://', 'https://'))
isnumeric():如果输入中只有数字字符则返回True
lower():字符串转小写
source中全是数字,或者以.txt结尾,或者以这些网络开头就True
# Directories
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
这里exist_ok的意思好像是要不要覆盖,但不太确定
increment_path是创造文件夹并且返回路径
这里是创造保存预测框的路径,save_txt是True就保存在exp/'labels',False就保存在exp
Path(project)是运行结果保存的路径,在参数里面定义,Path()方法就是YOLO里面用的多的路径规范方法,就不纠结了,有时间研究一下
mkdir:在给定路径上创建一个新目录(parents=True什么意思,exist_ok=True什么意思又忘了)
如果save_txt=False,就创建save_dir,那上面就已经increment_path就已经创建save_dir并返回路径了,下面怎么再mkdir save_dir,还有save_txt=False是什么参数True呢,也就是说这执行的是什么功能
# Initialize
set_logging()
device = select_device(device)
half &= device.type != 'cpu' # half precision only supported on CUDA
set_logging():
有机会select_device()是什么内容一定要去看一下,是推理时候选择设备的原理是什么
w = str(weights[0] if isinstance(weights, list) else weights)
classify, suffix, suffixes = False, Path(w).suffix.lower(), ['.pt', '.onnx', '.tflite', '.pb', '']
check_suffix(w, suffixes) # check weights have acceptable suffix
pt, onnx, tflite, pb, saved_model = (suffix == x for x in suffixes) # backend booleans
stride, names = 64, [f'class{i}' for i in range(1000)] # assign defaults
isinstance函数:
dsfPython isinstance() 函数_小白_努力-CSDN博客
如果weights是列表就返回第一个元素,如果不是列表就直接返回,然后转成str。这里是weights可以写多个
suffix():最后一个组件的最后后缀(如果有的话)
这里classify=False不知道是干啥的,suffix是weights的后缀,suffixs是可供查找的后缀名
if pt:
model = torch.jit.load(w) if 'torchscript' in w else attempt_load(weights, map_location=device)
stride = int(model.stride.max()) # model stride
names = model.module.names if hasattr(model, 'module') else model.names # get class names
if half:
model.half() # to FP16
if classify: # second-stage classifier
modelc = load_classifier(name='resnet50', n=2) # initialize
modelc.load_state_dict(torch.load('resnet50.pt', map_location=device)['model']).to(device).eval()
elif onnx:
if dnn:
check_requirements(('opencv-python>=4.5.4',))
net = cv2.dnn.readNetFromONNX(w)
else:
check_requirements(('onnx', 'onnxruntime-gpu' if torch.has_cuda else 'onnxruntime'))
import onnxruntime
session = onnxruntime.InferenceSession(w, None)
else: # TensorFlow models
check_requirements(('tensorflow>=2.4.1',))
import tensorflow as tf
if pb: # https://www.tensorflow.org/guide/migrate#a_graphpb_or_graphpbtxt
def wrap_frozen_graph(gd, inputs, outputs):
x = tf.compat.v1.wrap_function(lambda: tf.compat.v1.import_graph_def(gd, name=""), []) # wrapped import
return x.prune(tf.nest.map_structure(x.graph.as_graph_element, inputs),
tf.nest.map_structure(x.graph.as_graph_element, outputs))
graph_def = tf.Graph().as_graph_def()
graph_def.ParseFromString(open(w, 'rb').read())
frozen_func = wrap_frozen_graph(gd=graph_def, inputs="x:0", outputs="Identity:0")
elif saved_model:
model = tf.keras.models.load_model(w)
elif tflite:
interpreter = tf.lite.Interpreter(model_path=w) # load TFLite model
interpreter.allocate_tensors() # allocate
input_details = interpreter.get_input_details() # inputs
output_details = interpreter.get_output_details() # outputs
int8 = input_details[0]['dtype'] == np.uint8 # is TFLite quantized uint8 model
imgsz = check_img_size(imgsz, s=stride) # check image size
pd, saved_model, tflite都是TensorFlow models
tflite:tensorflow 19: tflite 概念理解_yuanlulu的博客 -CSDN博客_tflite
这里是五种类型的权重分别执行什么代码,以后回来看
不过.pt可以先关注一下:
if pt:
model = torch.jit.load(w) if 'torchscipt' in w else attempt_load(weights, map_location=device)
stride = int(model.stride.max())
names = model.module.names if hasattr(model, 'module') else model.names
i if half:
model.half()
if classify:
modelc = load_classifier(name='resnet50', n=2)
modelc.load_state_dict(torch.load('resnet50.pt', map_location=device)['model']
这里代码没打完,有时间再看一下
# Dataloader
if webcam:
view_img = check_imshow()
cudnn.benchmark = True # set True to speed up constant image size inference
dataset = LoadStreams(source, img_size=imgsz, stride=stride, auto=pt)
bs = len(dataset) # batch_size
else:
dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt)
bs = 1 # batch_size
vid_path, vid_writer = [None] * bs, [None] * bs
check_imshow():检测环境是否支持图像显示
cudnn.benchmark = True:
在推理时使用torch.backends.cudnn.benchmark = true,可以让内置的 cuDNN 的 auto-tuner 自动寻找最适合当前配置的高效算法,来达到优化运行效率的问题。
一般来讲,应该遵循以下准则:
①. 如果网络的输入数据维度或类型上变化不大,设置 torch.backends.cudnn.benchmark = true 可以增加运行效率;
②. 如果网络的输入数据在每次 iteration 都变化的话,会导致 cnDNN 每次都会去寻找一遍最优配置,这样反而会降低运行效率。
所以,推理视频流时,应保证每个摄像头传递给算法的图片一样大小。
LoadStreams:
auto=pt,如果是pt就执行auto(虽然也不知道auto是干啥用的)
打开webcam之后这里dataset只得到一张图片,这跟stride=32有冲突吗,还有这里两种情况的e 代码
# Run inference
if pt and device.type != 'cpu':
model(torch.zeros(1, 3, *imgsz).to(device).type_as(next(model.parameters()))) # run once
dt, seen = [0.0, 0.0, 0.0], 0
for path, img, im0s, vid_cap in dataset:
t1 = time_sync()
if onnx:
img = img.astype('float32')
else:
img = torch.from_numpy(img).to(device)
img = img.half() if half else img.float() # uint8 to fp16/32
img /= 255.0 # 0 - 255 to 0.0 - 1.0
if len(img.shape) == 3:
img = img[None] # expand for batch dim
t2 = time_sync()
dt[0] += t2 - t1
torch.zeros(1,3,*imgsz)是创建一个作为试验的四维张量,不过*imgsz是怎么把长宽导进去的
next是迭代器,把model的参数一个一个推出来,调试后可以看到next(model.parameters())的形状是[32,3,6,6],为什么这里的参数是这个形状,还有为什么要放这种形状的输入进去,这个网络的图像大小是6*6吗???
img = img[None] 可以让图片张量直接增加一个维度
如果是Pt就向前run一次看能不能跑通
for 里面的im0s是什么用,下面记得注意一下
这里vid_cap是None,也就是说如果放进去mp4,循环可以跑出来的东西会多一维
if pt:
visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False
pred = model(img, augment=augment, visualize=visualize)[0]
elif onnx:
if dnn:
net.setInput(img)
pred = torch.tensor(net.forward())
else:
pred = torch.tensor(session.run([session.get_outputs()[0].name], {session.get_inputs()[0].name: img}))
else: # tensorflow model (tflite, pb, saved_model)
imn = img.permute(0, 2, 3, 1).cpu().numpy() # image in numpy
if pb:
pred = frozen_func(x=tf.constant(imn)).numpy()
elif saved_model:
pred = model(imn, training=False).numpy()
elif tflite:
if int8:
scale, zero_point = input_details[0]['quantization']
imn = (imn / scale + zero_point).astype(np.uint8) # de-scale
interpreter.set_tensor(input_details[0]['index'], imn)
interpreter.invoke()
pred = interpreter.get_tensor(output_details[0]['index'])
if int8:
scale, zero_point = output_details[0]['quantization']
pred = (pred.astype(np.float32) - zero_point) * scale # re-scale
pred[..., 0] *= imgsz[1] # x
pred[..., 1] *= imgsz[0] # y
pred[..., 2] *= imgsz[1] # w
pred[..., 3] *= imgsz[0] # h
pred = torch.tensor(pred)
t3 = time_sync()
dt[1] += t3 - t2
visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False
Path(path).stem,stem是拿出最后的部分,去掉后缀。
path={str}'/home/robot/yolov5-master/yolov5/data/images/bus.jpg'
Path(path)={PosixPath}/home/robot/yolov5-master/yolov5/data/images/bus.jpg
Path(path).stem='bus'
save_dir / Path(path).stem = {PosixPath}/runs/detect/exp10/bus
visualize = increment_path(save_dir / Path(path).stem, mkdir=True)=/runs/detect/exp10/bus
如果visualize=True就创建文件并且返回路径
# NMS
pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
dt[2] += time_sync() - t3
augment,这个参数的操作有时间了解一下
pred ---- Tensor(1,18900,85)
经过NMS之后变成了
# Second-stage classifier (optional)
if classify:
pred = apply_classifier(pred, modelc, img, im0s)
classify是干啥用的??
# Process predictions
for i, det in enumerate(pred): # per image
seen += 1
if webcam: # batch_size >= 1
p, s, im0, frame = path[i], f'{i}: ', im0s[i].copy(), dataset.count
else:
p, s, im0, frame = path, '', im0s.copy(), getattr(dataset, 'frame', 0)
p = Path(p) # to Path
save_path = str(save_dir / p.name) # img.jpg
txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}') # img.txt
s += '%gx%g ' % img.shape[2:] # print string
gn = torch.tensor(im0.shape)[[1, 0, 1, 0]] # normalization gain whwh
imc = im0.copy() if save_crop else im0 # for save_crop
annotator = Annotator(im0, line_width=line_thickness, example=str(names))
if len(det):
# Rescale boxes from img_size to im0 size
det[:, :4] = scale_coords(img.shape[2:], det[:, :4], im0.shape).round()
# Print results
for c in det[:, -1].unique():
n = (det[:, -1] == c).sum() # detections per class
s += f"{n} {names[int(c)]}{'s' * (n > 1)}, " # add to string
# Write results
for *xyxy, conf, cls in reversed(det):
if save_txt: # Write to file
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
with open(txt_path + '.txt', 'a') as f:
f.write(('%g ' * len(line)).rstrip() % line + '\n')
if save_img or save_crop or view_img: # Add bbox to image
c = int(cls) # integer class
label = None if hide_labels else (names[c] if hide_conf else f'{names[c]} {conf:.2f}')
annotator.box_label(xyxy, label, color=colors(c, True))
if save_crop:
save_one_box(xyxy, imc, file=save_dir / 'crops' / names[c] / f'{p.stem}.jpg', BGR=True)
det是预测的结果,len(det)是预测的数量
det[:, :4] = scale_coords(img.shape[2:], det[:, :4], im0.shape).round()
scale_coords是把det[ : , :4](coords(xyxy))从img.shape[2: ]变成im0.shape(这里det[ : , :4]是xyxy的格式吗?)
for c in det[:, -1].unique():
det[ : ,-1]是什么意思,看好像操作之后值都变了,虽然变成Tensor(3,),这里-1的理解是这段代码的关键unique()函数是去掉张量中重复的部分
det[ : ,-1] = tensor([0.,0.,67.])
det[ : ,-1] = tensor([0.,67.])
det[ : ,-1] == c的神奇功能:
for *xyxy, conf, cls in reversed(det):
这里挺奇怪的,*xyxy怎么拿东西出来的,det里面唯一有用的值也就是data里面的Tensor(5,6)了,但是这跟拿出来的*xyxy,没什么关系,det是这样的:
new
(1)annotator
im0 = annotator.result()
annotator具体的功能没看,好像是什么注释器
在data文件夹中的hyp中
上面这些参数默认就好,没什么改动的必要。
lr:学习率,在训练过程中会依次下降,初始学习率在第一行内容中。余弦退火就是为了动态降低学习率。
momentum:动量,一般为0.95左右
weight_decay:权重衰减,防止权重更新幅度过大,过拟合。(这个技术也是一直没时间看)
warmup:热身,让模型先热热身,熟悉熟悉数据,学习率要小,相当于只是去看看,还没正式训练呢,学习率太高说不定就学跑偏了。
def parse_opt(known=False):
parser = argparse.ArgumentParser()
parser.add_argument('--weights', type=str, default=ROOT / 'yolov5s.pt', help='initial weights path')
parser.add_argument('--cfg', type=str, default='', help='model.yaml path')
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
parser.add_argument('--hyp', type=str, default=ROOT / 'data/hyps/hyp.scratch.yaml', help='hyperparameters path')
parser.add_argument('--epochs', type=int, default=300)
parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs, -1 for autobatch')
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='train, val image size (pixels)')
parser.add_argument('--rect', action='store_true', help='rectangular training')
parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training')
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint')
parser.add_argument('--noval', action='store_true', help='only validate final epoch')
parser.add_argument('--noautoanchor', action='store_true', help='disable autoanchor check')
parser.add_argument('--evolve', type=int, nargs='?', const=300, help='evolve hyperparameters for x generations')
parser.add_argument('--bucket', type=str, default='', help='gsutil bucket')
parser.add_argument('--cache', type=str, nargs='?', const='ram', help='--cache images in "ram" (default) or "disk"')
parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training')
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%')
parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class')
parser.add_argument('--adam', action='store_true', help='use torch.optim.Adam() optimizer')
parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode')
parser.add_argument('--workers', type=int, default=8, help='maximum number of dataloader workers')
parser.add_argument('--project', default=ROOT / 'runs/train', help='save to project/name')
parser.add_argument('--name', default='exp', help='save to project/name')
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
parser.add_argument('--quad', action='store_true', help='quad dataloader')
parser.add_argument('--linear-lr', action='store_true', help='linear LR')
parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon')
parser.add_argument('--patience', type=int, default=100, help='EarlyStopping patience (epochs without improvement)')
parser.add_argument('--freeze', type=int, default=0, help='Number of layers to freeze. backbone=10, all=24')
parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)')
parser.add_argument('--local_rank', type=int, default=-1, help='DDP parameter, do not modify')
# Weights & Biases arguments
parser.add_argument('--entity', default=None, help='W&B: Entity')
parser.add_argument('--upload_dataset', action='store_true', help='W&B: Upload dataset as artifact table')
parser.add_argument('--bbox_interval', type=int, default=-1, help='W&B: Set bounding-box image logging interval')
parser.add_argument('--artifact_alias', type=str, default='latest', help='W&B: Version of dataset artifact to use')
opt = parser.parse_known_args()[0] if known else parser.parse_args()
return opt
rect是指要不要做矩形训练,例如448*448给修改成448*w这样一个尺寸,默认是不做。resume是指是否继续上次的训练,这是一个日志文件,在训练后会有一个last.py,是保存的权重文件。
如果只有一个类别,singgle-cls可以改成store-false,默认就是True了。如果是多类别的,这里要写store_true(nargs='?'是什么意思,还有const=True)
最后一个workers,windows系统一定要指定为0,指定其他的会百分百报错。windows和pytorch没有很好地兼容。(这个有待考证)
2.2.1 训练流程
pt文件保存了best和last两种,best是使用了4个指标对模型参数做了评估,得分是:
sum([0.0, 0.0, 0.1, 0.9]*[精确度, 召回率, [email protected], [email protected]:0.95]
所以主要还是根据mAP值。
yolov5的日志文件做的非常好 ,在runs文件夹下记录了每一次迭代的结果,进入其中一次查看。
有权重文件,有tensorboard需要的events文件,有记录的超参数。还有标签的分布情况。有记录的精度和召回率情况,还有对其中几个batch的预测结果。在results文件中,记录了各个指标,其中有精度和召回率等。(召回率是什么东西)