视觉进阶篇——YOLOv5环境配置与训练评估(很长,请耐心)

部署运行你感兴趣的模型镜像

学习这篇文章之前请完成目前发布的所有视觉学习的所有内容,否则你可能会产生“跟不上的疑惑”。
当然了,你学完了也不一定能跟上,哈哈哈哈!


前言

从这篇开始我们正式进入基于深度学习的目标检测的领域。现在目标检测的模型非常之多,但是我还是喜欢让大家从yolov5学习,因为简单,并且容易去给大家讲解。从这章开始,将大家一起学习如何根据github上面的说明进行学习,以此抛转引玉,之后大家可以学着学习其他模型。
请注意,我并不带大家一点一点吃透yolov5网络模型,这是你自己下去该学习的。


一、环境配置:从零搭建 YOLOv5 开发环境

1.前期准备

根据之前的学习相信你现在应该已经有了以下基础环境

  • 名叫yolov5(或者其他)并安装了pytorch的conda环境
  • 与环境配套的CUDA、CUDNN(conda安装的pytorch自带)
  • nvidia显卡

2.拉取yolov5源码

#我在主目录下创建了一个yolo文件夹用于存放我们学习的所有yolo模型
cd ~/yolo
#如果觉得慢,别忘记加速技巧
git cloen https://github.com/ultralytics/yolov5

在这里插入图片描述

3. pycharm关联conda环境并打开yolov5文件夹

忘记步骤的同学到墙边罚站去
在这里插入图片描述
在这里插入图片描述

4.安装yolov5依赖

我们可以在pycharm中看到yolov5的文件夹结构,这时我们找到.md文件,一般来说github上的代码都会写这样一个md说明文件,帮助我们更好的使用他们的代码。yolov5为我们提供了中文的版本,所以我们选择zh-CN的版本
在这里插入图片描述
这是md文件的结构,在这张图片的右上角有着图片渲染功能和文本对应图片功能
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们使用第三个图片渲染功能继续操作。
细心的你应该会发现,这与我们打开的github主页的描述是相同的。
在这里插入图片描述
说明github会为访问者默认加载.md文件以提供更好的体验。

下面的教程你可以选择看浏览器中的教程,也可以看pycharm中渲染的教程。
在这里插入图片描述
这里为我们提供了最新的yolov11实现,但是这不是我们今天的目的。所以我们继续往下滑。
在这里插入图片描述
这里给出了一个非常明确的安装教程。当然你也可以去看yolov5文档,这是yolov5官方给出的教程,非常权威。与我接下来的教程是差不多的。
https://docs.ultralytics.com/yolov5/
在这里插入图片描述在这里插入图片描述
如果你选择留下继续,那我们正式开始。打开左下角的终端
在这里插入图片描述
它会自动激活关联的虚拟环境并且进入到当前文件夹下
此时我们来跟着教程进行pip配置

pip install -r requirements.txt

在这里插入图片描述
requirements.txt描述的是这个代码里所需要的所有依赖要求。这个命令会更根据requirements.txt中的各种pip包的要求进行安装当前环境所需要的包
在这里插入图片描述
在这里插入图片描述
此时已经完成了yolov5环境的配置。
在这里插入图片描述
文档的最后给出了非常详细的教程,大家也可以自己去学习。
这里我们使用detect.py去验证一下环境的安装。
运行按钮只教一次(左侧文件栏选中右键运行也是一样的)
在这里插入图片描述
此时会出现一堆红色的东西,先不要害怕。我们简单分析一下。
在这里插入图片描述

detect: weights=yolov5s.pt, source=data/images, data=data/coco128.yaml, imgsz=[640, 640], conf_thres=0.25,
iou_thres=0.45, max_det=1000, device=, view_img=False, save_txt=False,
save_format=0, save_csv=False, save_conf=False, save_crop=False,
nosave=False, classes=None, agnostic_nms=False, augment=False,
visualize=False, update=False, project=runs/detect, name=exp,
exist_ok=False, line_thickness=3, hide_labels=False, hide_conf=False,
half=False, dnn=False, vid_stride=1 YOLOv5 🚀 v7.0-447-ge76591cb
Python-3.9.23 torch-1.13.1 CUDA:0 (NVIDIA GeForce RTX 3060 Laptop GPU,
5790MiB)

这条信息侧面告诉了我们当前的所有环境,大家可以通过这个来判断是否成功安装pytorch和驱动。
在这里插入图片描述
这里告诉我们最好使用numpy1.x,我们当前的numpy2.0.2,因此我们需要改一下版本。
这里我选择使用numpy1.23.5

pip install numpy==1.23.5

在这里插入图片描述
这里会给一个opencv包依赖的错误,这是因为在pip install -r requirements.tx时,通常在满足要求的条件下安装当前包的最新版,我们不需要4.12的opencv,这里降成4.8.0的即可

 pip install opencv-python==4.8.0

在这里插入图片描述
这里我使用的清华源报错找不到4.8.0,但是给出了存在的所有版本。这个告诉我们需要安装对应标签名一模一样的版本。

 pip install opencv-python==4.8.0.74

在这里插入图片描述
重新运行detect.py
在这里插入图片描述
没有了之前的错误,只留下我们的训练配置信息
此时显示我们需要yolov5s.pt并且在访问github去下载。
那我们肯定选择手动加速一下~顺便把yolov5n.pt也下载一下

wget https://github.com/ultralytics/yolov5/releases/download/v7.0/yolov5s.pt
wget https://github.com/ultralytics/yolov5/releases/download/v7.0/yolov5n.pt

直接下载到我们的yolov5项目文件夹即可
重新运行detect.py

在这里插入图片描述
好了,现在你应该发现了,这其实是一个模型测试文件,用于测试yolov5s.pt模型同时也是测试yolov5环境的工具,我们后期也会通过这个测试我们自己训练的代码。
我们找到runs/detect/exp4文件夹看看里面的内容(路径是基于当前目录的)
在这里插入图片描述
在这里插入图片描述
如果你也看到这些,恭喜你通过yolov5环境测试


二、数据集准备:从图片到训练数据的魔法转换

1.准备自己的数据集

在视觉学习中我们已经了解到我们常见的数据集格式,如coco、yolo、voc等等。
这里你应该要先自主打标做好自己的数据集,或者是使用公开数据集。在比赛的时候一个好的数据集比你使用更先进的模型所起的效果要好的多。
data/scripts中给出了下载常见数据集的代码,但是一般都需要加速哦
在这里插入图片描述
下载coco128数据集看一下,标准yolo数据集格式
在这里插入图片描述

在这里插入图片描述

2.数据集目录规范

在这里插入图片描述
看这篇文章之前相信你已经看了机器学习训练的那篇文章,这个时候对数据集的结构和读取方式有了初步的了解。
在yolov5/data目录下存放着一堆yaml文件,这是数据集读取文件,我们可以看到目前主流的几种数据集格式ImageNet、coco、VOC等等。
我们打开一个coco.yaml查看一下内容
在这里插入图片描述
这是典型的coco数据集格式,它通过读取存放着每行以图像路径 为格式的txt文件进行读取数据集
#Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, …]
注释也给出了训练集和数据集的格式要求
在这里插入图片描述
能够读取多种数据集格式的原因肯定与它的dataloader.py有关
在这里插入图片描述

class LoadImagesAndLabels(Dataset):
    """Loads images and their corresponding labels for training and validation in YOLOv5."""

    cache_version = 0.6  # dataset labels *.cache version
    rand_interp_methods = [cv2.INTER_NEAREST, cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_AREA, cv2.INTER_LANCZOS4]

    def __init__(
        self,
        path,
        img_size=640,
        batch_size=16,
        augment=False,
        hyp=None,
        rect=False,
        image_weights=False,
        cache_images=False,
        single_cls=False,
        stride=32,
        pad=0.0,
        min_items=0,
        prefix="",
        rank=-1,
        seed=0,
    ):
        """Initializes the YOLOv5 dataset loader, handling images and their labels, caching, and preprocessing."""
        self.img_size = img_size
        self.augment = augment
        self.hyp = hyp
        self.image_weights = image_weights
        self.rect = False if image_weights else rect
        self.mosaic = self.augment and not self.rect  # load 4 images at a time into a mosaic (only during training)
        self.mosaic_border = [-img_size // 2, -img_size // 2]
        self.stride = stride
        self.path = path
        self.albumentations = Albumentations(size=img_size) if augment else None

        try:
            f = []  # image files
            for p in path if isinstance(path, list) else [path]:
                p = Path(p)  # os-agnostic
                if p.is_dir():  # dir
                    f += glob.glob(str(p / "**" / "*.*"), recursive=True)
                    # f = list(p.rglob('*.*'))  # pathlib
                elif p.is_file():  # file
                    with open(p) as t:
                        t = t.read().strip().splitlines()
                        parent = str(p.parent) + os.sep
                        f += [x.replace("./", parent, 1) if x.startswith("./") else x for x in t]  # to global path
                        # f += [p.parent / x.lstrip(os.sep) for x in t]  # to global path (pathlib)
                else:
                    raise FileNotFoundError(f"{prefix}{p} does not exist")
            self.im_files = sorted(x.replace("/", os.sep) for x in f if x.split(".")[-1].lower() in IMG_FORMATS)
            # self.img_files = sorted([x for x in f if x.suffix[1:].lower() in IMG_FORMATS])  # pathlib
            assert self.im_files, f"{prefix}No images found"
        except Exception as e:
            raise Exception(f"{prefix}Error loading data from {path}: {e}\n{HELP_URL}") from e

        # Check cache
        self.label_files = img2label_paths(self.im_files)  # labels
        cache_path = (p if p.is_file() else Path(self.label_files[0]).parent).with_suffix(".cache")
        try:
            cache, exists = np.load(cache_path, allow_pickle=True).item(), True  # load dict
            assert cache["version"] == self.cache_version  # matches current version
            assert cache["hash"] == get_hash(self.label_files + self.im_files)  # identical hash
        except Exception:
            cache, exists = self.cache_labels(cache_path, prefix), False  # run cache ops

        # Display cache
        nf, nm, ne, nc, n = cache.pop("results")  # found, missing, empty, corrupt, total
        if exists and LOCAL_RANK in {-1, 0}:
            d = f"Scanning {cache_path}... {nf} images, {nm + ne} backgrounds, {nc} corrupt"
            tqdm(None, desc=prefix + d, total=n, initial=n, bar_format=TQDM_BAR_FORMAT)  # display cache results
            if cache["msgs"]:
                LOGGER.info("\n".join(cache["msgs"]))  # display warnings
        assert nf > 0 or not augment, f"{prefix}No labels found in {cache_path}, can not start training. {HELP_URL}"

        # Read cache
        [cache.pop(k) for k in ("hash", "version", "msgs")]  # remove items
        labels, shapes, self.segments = zip(*cache.values())
        nl = len(np.concatenate(labels, 0))  # number of labels
        assert nl > 0 or not augment, f"{prefix}All labels empty in {cache_path}, can not start training. {HELP_URL}"
        self.labels = list(labels)
        self.shapes = np.array(shapes)
        self.im_files = list(cache.keys())  # update
        self.label_files = img2label_paths(cache.keys())  # update

        # Filter images
        if min_items:
            include = np.array([len(x) >= min_items for x in self.labels]).nonzero()[0].astype(int)
            LOGGER.info(f"{prefix}{n - len(include)}/{n} images filtered from dataset")
            self.im_files = [self.im_files[i] for i in include]
            self.label_files = [self.label_files[i] for i in include]
            self.labels = [self.labels[i] for i in include]
            self.segments = [self.segments[i] for i in include]
            self.shapes = self.shapes[include]  # wh

        # Create indices
        n = len(self.shapes)  # number of images
        bi = np.floor(np.arange(n) / batch_size).astype(int)  # batch index
        nb = bi[-1] + 1  # number of batches
        self.batch = bi  # batch index of image
        self.n = n
        self.indices = np.arange(n)
        if rank > -1:  # DDP indices (see: SmartDistributedSampler)
            # force each rank (i.e. GPU process) to sample the same subset of data on every epoch
            self.indices = self.indices[np.random.RandomState(seed=seed).permutation(n) % WORLD_SIZE == RANK]

        # Update labels
        include_class = []  # filter labels to include only these classes (optional)
        self.segments = list(self.segments)
        include_class_array = np.array(include_class).reshape(1, -1)
        for i, (label, segment) in enumerate(zip(self.labels, self.segments)):
            if include_class:
                j = (label[:, 0:1] == include_class_array).any(1)
                self.labels[i] = label[j]
                if segment:
                    self.segments[i] = [segment[idx] for idx, elem in enumerate(j) if elem]
            if single_cls:  # single-class training, merge all classes into 0
                self.labels[i][:, 0] = 0

        # Rectangular Training
        if self.rect:
            # Sort by aspect ratio
            s = self.shapes  # wh
            ar = s[:, 1] / s[:, 0]  # aspect ratio
            irect = ar.argsort()
            self.im_files = [self.im_files[i] for i in irect]
            self.label_files = [self.label_files[i] for i in irect]
            self.labels = [self.labels[i] for i in irect]
            self.segments = [self.segments[i] for i in irect]
            self.shapes = s[irect]  # wh
            ar = ar[irect]

            # Set training image shapes
            shapes = [[1, 1]] * nb
            for i in range(nb):
                ari = ar[bi == i]
                mini, maxi = ari.min(), ari.max()
                if maxi < 1:
                    shapes[i] = [maxi, 1]
                elif mini > 1:
                    shapes[i] = [1, 1 / mini]

            self.batch_shapes = np.ceil(np.array(shapes) * img_size / stride + pad).astype(int) * stride

        # Cache images into RAM/disk for faster training
        if cache_images == "ram" and not self.check_cache_ram(prefix=prefix):
            cache_images = False
        self.ims = [None] * n
        self.npy_files = [Path(f).with_suffix(".npy") for f in self.im_files]
        if cache_images:
            b, gb = 0, 1 << 30  # bytes of cached images, bytes per gigabytes
            self.im_hw0, self.im_hw = [None] * n, [None] * n
            fcn = self.cache_images_to_disk if cache_images == "disk" else self.load_image
            with ThreadPool(NUM_THREADS) as pool:
                results = pool.imap(lambda i: (i, fcn(i)), self.indices)
                pbar = tqdm(results, total=len(self.indices), bar_format=TQDM_BAR_FORMAT, disable=LOCAL_RANK > 0)
                for i, x in pbar:
                    if cache_images == "disk":
                        b += self.npy_files[i].stat().st_size
                    else:  # 'ram'
                        self.ims[i], self.im_hw0[i], self.im_hw[i] = x  # im, hw_orig, hw_resized = load_image(self, i)
                        b += self.ims[i].nbytes * WORLD_SIZE
                    pbar.desc = f"{prefix}Caching images ({b / gb:.1f}GB {cache_images})"
                pbar.close()

    def check_cache_ram(self, safety_margin=0.1, prefix=""):
        """Checks if available RAM is sufficient for caching images, adjusting for a safety margin."""
        b, gb = 0, 1 << 30  # bytes of cached images, bytes per gigabytes
        n = min(self.n, 30)  # extrapolate from 30 random images
        for _ in range(n):
            im = cv2.imread(random.choice(self.im_files))  # sample image
            ratio = self.img_size / max(im.shape[0], im.shape[1])  # max(h, w)  # ratio
            b += im.nbytes * ratio**2
        mem_required = b * self.n / n  # GB required to cache dataset into RAM
        mem = psutil.virtual_memory()
        cache = mem_required * (1 + safety_margin) < mem.available  # to cache or not to cache, that is the question
        if not cache:
            LOGGER.info(
                f"{prefix}{mem_required / gb:.1f}GB RAM required, "
                f"{mem.available / gb:.1f}/{mem.total / gb:.1f}GB available, "
                f"{'caching images ✅' if cache else 'not caching images ⚠️'}"
            )
        return cache

    def cache_labels(self, path=Path("./labels.cache"), prefix=""):
        """Caches dataset labels, verifies images, reads shapes, and tracks dataset integrity."""
        x = {}  # dict
        nm, nf, ne, nc, msgs = 0, 0, 0, 0, []  # number missing, found, empty, corrupt, messages
        desc = f"{prefix}Scanning {path.parent / path.stem}..."
        with Pool(NUM_THREADS) as pool:
            pbar = tqdm(
                pool.imap(verify_image_label, zip(self.im_files, self.label_files, repeat(prefix))),
                desc=desc,
                total=len(self.im_files),
                bar_format=TQDM_BAR_FORMAT,
            )
            for im_file, lb, shape, segments, nm_f, nf_f, ne_f, nc_f, msg in pbar:
                nm += nm_f
                nf += nf_f
                ne += ne_f
                nc += nc_f
                if im_file:
                    x[im_file] = [lb, shape, segments]
                if msg:
                    msgs.append(msg)
                pbar.desc = f"{desc} {nf} images, {nm + ne} backgrounds, {nc} corrupt"

        pbar.close()
        if msgs:
            LOGGER.info("\n".join(msgs))
        if nf == 0:
            LOGGER.warning(f"{prefix}WARNING ⚠️ No labels found in {path}. {HELP_URL}")
        x["hash"] = get_hash(self.label_files + self.im_files)
        x["results"] = nf, nm, ne, nc, len(self.im_files)
        x["msgs"] = msgs  # warnings
        x["version"] = self.cache_version  # cache version
        try:
            np.save(path, x)  # save cache for next time
            path.with_suffix(".cache.npy").rename(path)  # remove .npy suffix
            LOGGER.info(f"{prefix}New cache created: {path}")
        except Exception as e:
            LOGGER.warning(f"{prefix}WARNING ⚠️ Cache directory {path.parent} is not writeable: {e}")  # not writeable
        return x

    def __len__(self):
        """Returns the number of images in the dataset."""
        return len(self.im_files)

    # def __iter__(self):
    #     self.count = -1
    #     print('ran dataset iter')
    #     #self.shuffled_vector = np.random.permutation(self.nF) if self.augment else np.arange(self.nF)
    #     return self

    def __getitem__(self, index):
        """Fetches the dataset item at the given index, considering linear, shuffled, or weighted sampling."""
        index = self.indices[index]  # linear, shuffled, or image_weights

        hyp = self.hyp
        if mosaic := self.mosaic and random.random() < hyp["mosaic"]:
            # Load mosaic
            img, labels = self.load_mosaic(index)
            shapes = None

            # MixUp augmentation
            if random.random() < hyp["mixup"]:
                img, labels = mixup(img, labels, *self.load_mosaic(random.choice(self.indices)))

        else:
            # Load image
            img, (h0, w0), (h, w) = self.load_image(index)

            # Letterbox
            shape = self.batch_shapes[self.batch[index]] if self.rect else self.img_size  # final letterboxed shape
            img, ratio, pad = letterbox(img, shape, auto=False, scaleup=self.augment)
            shapes = (h0, w0), ((h / h0, w / w0), pad)  # for COCO mAP rescaling

            labels = self.labels[index].copy()
            if labels.size:  # normalized xywh to pixel xyxy format
                labels[:, 1:] = xywhn2xyxy(labels[:, 1:], ratio[0] * w, ratio[1] * h, padw=pad[0], padh=pad[1])

            if self.augment:
                img, labels = random_perspective(
                    img,
                    labels,
                    degrees=hyp["degrees"],
                    translate=hyp["translate"],
                    scale=hyp["scale"],
                    shear=hyp["shear"],
                    perspective=hyp["perspective"],
                )

        nl = len(labels)  # number of labels
        if nl:
            labels[:, 1:5] = xyxy2xywhn(labels[:, 1:5], w=img.shape[1], h=img.shape[0], clip=True, eps=1e-3)

        if self.augment:
            # Albumentations
            img, labels = self.albumentations(img, labels)
            nl = len(labels)  # update after albumentations

            # HSV color-space
            augment_hsv(img, hgain=hyp["hsv_h"], sgain=hyp["hsv_s"], vgain=hyp["hsv_v"])

            # Flip up-down
            if random.random() < hyp["flipud"]:
                img = np.flipud(img)
                if nl:
                    labels[:, 2] = 1 - labels[:, 2]

            # Flip left-right
            if random.random() < hyp["fliplr"]:
                img = np.fliplr(img)
                if nl:
                    labels[:, 1] = 1 - labels[:, 1]

            # Cutouts
            # labels = cutout(img, labels, p=0.5)
            # nl = len(labels)  # update after cutout

        labels_out = torch.zeros((nl, 6))
        if nl:
            labels_out[:, 1:] = torch.from_numpy(labels)

        # Convert
        img = img.transpose((2, 0, 1))[::-1]  # HWC to CHW, BGR to RGB
        img = np.ascontiguousarray(img)

        return torch.from_numpy(img), labels_out, self.im_files[index], shapes

    def load_image(self, i):
        """Loads an image by index, returning the image, its original dimensions, and resized dimensions.

        Returns (im, original hw, resized hw)
        """
        im, f, fn = (
            self.ims[i],
            self.im_files[i],
            self.npy_files[i],
        )
        if im is None:  # not cached in RAM
            if fn.exists():  # load npy
                im = np.load(fn)
            else:  # read image
                im = cv2.imread(f)  # BGR
                assert im is not None, f"Image Not Found {f}"
            h0, w0 = im.shape[:2]  # orig hw
            r = self.img_size / max(h0, w0)  # ratio
            if r != 1:  # if sizes are not equal
                interp = cv2.INTER_LINEAR if (self.augment or r > 1) else cv2.INTER_AREA
                im = cv2.resize(im, (math.ceil(w0 * r), math.ceil(h0 * r)), interpolation=interp)
            return im, (h0, w0), im.shape[:2]  # im, hw_original, hw_resized
        return self.ims[i], self.im_hw0[i], self.im_hw[i]  # im, hw_original, hw_resized

    def cache_images_to_disk(self, i):
        """Saves an image to disk as an *.npy file for quicker loading, identified by index `i`."""
        f = self.npy_files[i]
        if not f.exists():
            np.save(f.as_posix(), cv2.imread(self.im_files[i]))

    def load_mosaic(self, index):
        """Loads a 4-image mosaic for YOLOv5, combining 1 selected and 3 random images, with labels and segments."""
        labels4, segments4 = [], []
        s = self.img_size
        yc, xc = (int(random.uniform(-x, 2 * s + x)) for x in self.mosaic_border)  # mosaic center x, y
        indices = [index] + random.choices(self.indices, k=3)  # 3 additional image indices
        random.shuffle(indices)
        for i, index in enumerate(indices):
            # Load image
            img, _, (h, w) = self.load_image(index)

            # place img in img4
            if i == 0:  # top left
                img4 = np.full((s * 2, s * 2, img.shape[2]), 114, dtype=np.uint8)  # base image with 4 tiles
                x1a, y1a, x2a, y2a = max(xc - w, 0), max(yc - h, 0), xc, yc  # xmin, ymin, xmax, ymax (large image)
                x1b, y1b, x2b, y2b = w - (x2a - x1a), h - (y2a - y1a), w, h  # xmin, ymin, xmax, ymax (small image)
            elif i == 1:  # top right
                x1a, y1a, x2a, y2a = xc, max(yc - h, 0), min(xc + w, s * 2), yc
                x1b, y1b, x2b, y2b = 0, h - (y2a - y1a), min(w, x2a - x1a), h
            elif i == 2:  # bottom left
                x1a, y1a, x2a, y2a = max(xc - w, 0), yc, xc, min(s * 2, yc + h)
                x1b, y1b, x2b, y2b = w - (x2a - x1a), 0, w, min(y2a - y1a, h)
            elif i == 3:  # bottom right
                x1a, y1a, x2a, y2a = xc, yc, min(xc + w, s * 2), min(s * 2, yc + h)
                x1b, y1b, x2b, y2b = 0, 0, min(w, x2a - x1a), min(y2a - y1a, h)

            img4[y1a:y2a, x1a:x2a] = img[y1b:y2b, x1b:x2b]  # img4[ymin:ymax, xmin:xmax]
            padw = x1a - x1b
            padh = y1a - y1b

            # Labels
            labels, segments = self.labels[index].copy(), self.segments[index].copy()
            if labels.size:
                labels[:, 1:] = xywhn2xyxy(labels[:, 1:], w, h, padw, padh)  # normalized xywh to pixel xyxy format
                segments = [xyn2xy(x, w, h, padw, padh) for x in segments]
            labels4.append(labels)
            segments4.extend(segments)

        # Concat/clip labels
        labels4 = np.concatenate(labels4, 0)
        for x in (labels4[:, 1:], *segments4):
            np.clip(x, 0, 2 * s, out=x)  # clip when using random_perspective()
        # img4, labels4 = replicate(img4, labels4)  # replicate

        # Augment
        img4, labels4, segments4 = copy_paste(img4, labels4, segments4, p=self.hyp["copy_paste"])
        img4, labels4 = random_perspective(
            img4,
            labels4,
            segments4,
            degrees=self.hyp["degrees"],
            translate=self.hyp["translate"],
            scale=self.hyp["scale"],
            shear=self.hyp["shear"],
            perspective=self.hyp["perspective"],
            border=self.mosaic_border,
        )  # border to remove

        return img4, labels4

    def load_mosaic9(self, index):
        """Loads 1 image + 8 random images into a 9-image mosaic for augmented YOLOv5 training, returning labels and
        segments.
        """
        labels9, segments9 = [], []
        s = self.img_size
        indices = [index] + random.choices(self.indices, k=8)  # 8 additional image indices
        random.shuffle(indices)
        hp, wp = -1, -1  # height, width previous
        for i, index in enumerate(indices):
            # Load image
            img, _, (h, w) = self.load_image(index)

            # place img in img9
            if i == 0:  # center
                img9 = np.full((s * 3, s * 3, img.shape[2]), 114, dtype=np.uint8)  # base image with 4 tiles
                h0, w0 = h, w
                c = s, s, s + w, s + h  # xmin, ymin, xmax, ymax (base) coordinates
            elif i == 1:  # top
                c = s, s - h, s + w, s
            elif i == 2:  # top right
                c = s + wp, s - h, s + wp + w, s
            elif i == 3:  # right
                c = s + w0, s, s + w0 + w, s + h
            elif i == 4:  # bottom right
                c = s + w0, s + hp, s + w0 + w, s + hp + h
            elif i == 5:  # bottom
                c = s + w0 - w, s + h0, s + w0, s + h0 + h
            elif i == 6:  # bottom left
                c = s + w0 - wp - w, s + h0, s + w0 - wp, s + h0 + h
            elif i == 7:  # left
                c = s - w, s + h0 - h, s, s + h0
            elif i == 8:  # top left
                c = s - w, s + h0 - hp - h, s, s + h0 - hp

            padx, pady = c[:2]
            x1, y1, x2, y2 = (max(x, 0) for x in c)  # allocate coords

            # Labels
            labels, segments = self.labels[index].copy(), self.segments[index].copy()
            if labels.size:
                labels[:, 1:] = xywhn2xyxy(labels[:, 1:], w, h, padx, pady)  # normalized xywh to pixel xyxy format
                segments = [xyn2xy(x, w, h, padx, pady) for x in segments]
            labels9.append(labels)
            segments9.extend(segments)

            # Image
            img9[y1:y2, x1:x2] = img[y1 - pady :, x1 - padx :]  # img9[ymin:ymax, xmin:xmax]
            hp, wp = h, w  # height, width previous

        # Offset
        yc, xc = (int(random.uniform(0, s)) for _ in self.mosaic_border)  # mosaic center x, y
        img9 = img9[yc : yc + 2 * s, xc : xc + 2 * s]

        # Concat/clip labels
        labels9 = np.concatenate(labels9, 0)
        labels9[:, [1, 3]] -= xc
        labels9[:, [2, 4]] -= yc
        c = np.array([xc, yc])  # centers
        segments9 = [x - c for x in segments9]

        for x in (labels9[:, 1:], *segments9):
            np.clip(x, 0, 2 * s, out=x)  # clip when using random_perspective()
        # img9, labels9 = replicate(img9, labels9)  # replicate

        # Augment
        img9, labels9, segments9 = copy_paste(img9, labels9, segments9, p=self.hyp["copy_paste"])
        img9, labels9 = random_perspective(
            img9,
            labels9,
            segments9,
            degrees=self.hyp["degrees"],
            translate=self.hyp["translate"],
            scale=self.hyp["scale"],
            shear=self.hyp["shear"],
            perspective=self.hyp["perspective"],
            border=self.mosaic_border,
        )  # border to remove

        return img9, labels9

    @staticmethod
    def collate_fn(batch):
        """Batches images, labels, paths, and shapes, assigning unique indices to targets in merged label tensor."""
        im, label, path, shapes = zip(*batch)  # transposed
        for i, lb in enumerate(label):
            lb[:, 0] = i  # add target image index for build_targets()
        return torch.stack(im, 0), torch.cat(label, 0), path, shapes

    @staticmethod
    def collate_fn4(batch):
        """Bundles a batch's data by quartering the number of shapes and paths, preparing it for model input."""
        im, label, path, shapes = zip(*batch)  # transposed
        n = len(shapes) // 4
        im4, label4, path4, shapes4 = [], [], path[:n], shapes[:n]

        ho = torch.tensor([[0.0, 0, 0, 1, 0, 0]])
        wo = torch.tensor([[0.0, 0, 1, 0, 0, 0]])
        s = torch.tensor([[1, 1, 0.5, 0.5, 0.5, 0.5]])  # scale
        for i in range(n):  # zidane torch.zeros(16,3,720,1280)  # BCHW
            i *= 4
            if random.random() < 0.5:
                im1 = F.interpolate(im[i].unsqueeze(0).float(), scale_factor=2.0, mode="bilinear", align_corners=False)[
                    0
                ].type(im[i].type())
                lb = label[i]
            else:
                im1 = torch.cat((torch.cat((im[i], im[i + 1]), 1), torch.cat((im[i + 2], im[i + 3]), 1)), 2)
                lb = torch.cat((label[i], label[i + 1] + ho, label[i + 2] + wo, label[i + 3] + ho + wo), 0) * s
            im4.append(im1)
            label4.append(lb)

        for i, lb in enumerate(label4):
            lb[:, 0] = i  # add target image index for build_targets()

        return torch.stack(im4, 0), torch.cat(label4, 0), path4, shapes4

YOLOv5 之所以能“三合一”支持
目录:path/to/imgs
文件:path/to/imgs.txt
列表:[path1, path2, …]
是在类 __init__构造函数里用 「三段判断」 把三种写法统一转成「图片绝对路径列表」。

path = path if isinstance(path, list) else [path]

f = []  # 最终收集到的图片绝对路径
for p in path:              # 遍历每个元素(可能是目录、txt、或单路径)
    p = Path(p)             # 变成 OS 无关路径对象

    # === 1. 目录模式 ===
    if p.is_dir():
        # 递归扫描所有常见图片后缀
        f += glob.glob(str(p / "**" / "*.*"), recursive=True)

    # === 2. 文件模式(.txt 清单) ===
    elif p.is_file() and p.suffix == '.txt':
        with open(p) as t:
            # 每行一张图,允许相对路径
            parent = str(p.parent)
            for line in t.read().strip().splitlines():
                line = line.strip()
                if line.startswith('./'):
                    line = parent + line[1:]   # ./a.jpg → 绝对
                f.append(line)

    # === 3. 单文件(不是 txt)→ 直接抛异常 ===
    elif p.is_file():
        raise ValueError(f"{p} 不是 .txt 清单,请检查路径")

    # === 4. 不存在 → 抛异常 ===
    else:
        raise FileNotFoundError(f"{p} 不存在")
你在 yaml 里写经过上面循环后 f 的内容说明
train: /data/images[/data/images/0001.jpg, /data/images/0002.jpg, ...]目录递归扫描
train: /data/train.txt文件内每行一张图 → 同上列表清单模式,路径可相对可绝对
train: [/data/A, /data/B]A/ 下所有图 + B/ 下所有图列表即批量目录/清单混合

通过这种方式将图片数据集转换为一个图片绝对路径列表方便后续会加载成缓存便于快速读取。

这时候有同学可能就有疑问,很多时候我们只在 yaml 里写了图片目录,却从来没有显式给出标签路径, 但是程序仍然能"秒定位"到对应的 .txt 标签文件。

这是因为yolov5默认了一个“关键词”,只要你的文件夹叫 images 和 labels,并且放在同一父目录下,YOLOv5 就能自动推导出标签路径,无需你在 yaml 里再写一行。所以我们在写yaml文件时无需再写一遍标签文件的存放路径。也就是说只要代码读取到了你的图像基本上就能找到你的标签文件

# 代码位置:utils/dataloaders.py  ~  img2label_paths()
def img2label_paths(img_paths):
    # Define label paths as a function of image paths
    sa, sb = os.sep + 'images' + os.sep, os.sep + 'labels' + os.sep  # /images/, /labels/
    return [sb.join(x.rsplit(sa, 1)).rsplit('.', 1)[0] + '.txt' for x in img_paths]

names是另一个“关键词”,代表标签名和索引的映射
names索引从0开始,标志着数据集中的待测物体的类别。
好的,现在你应该明白了yolov5是如何加载数据集的了。
学长这里给出以前学习使用的一款水果数据集。这里要感谢当时帮助我一起打标的同学,大小大概一个多G,并不是整个水果数据集的全部。
链接: https://pan.baidu.com/s/1R70mSnvV6_t5l0VkYEJBNQ 提取码: p9xd
–来自百度网盘超级会员v6的分享

在这里插入图片描述
已经划分好了训练集和验证集。但是今天并不打算就这样进行训练,而是将原有的训练集作为数据集划分为训练集和验证集,将原有的验证集作为测试集进行测试。

首先让我们为我们的水果数据集制作我们的yaml配置文件
只需要设置训练集验证集和测试集的路径、我们的标签名(苹果、香蕉、橙子、火龙果)以及种类数nc

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c1440c6c32d5432aa7daaaf3dc25a956.png
之前说过,yolo训练的时候使用的是txt标签文件,但是我们的标签是xml格式,所以我们需要对其做一个转换。并不是说不可以一开始直接打标成yolo格式,是因为xml标签可视的信息更多,便于我们在打标时快速发现问题。

3.数据集划分

刚才说过我们要将数据集进行划分,一般来说训练集和验证集的比例是7:3或者8:2,如果将一个数据集划分为训练集、验证集和测试集,则为7:2:1,8:1:1或者6:3:1这个大家可以自行选择。划分的不同也会影响训练的结果。
我们这里给出划分三种数据集的代码,由于不需要测试集,我们置零或者注销相关代码。为了好看,我把数据集文件夹重新命名并移动了一下。
将原val中的xml文件夹删除,因为测试集不需要标签,img改为test并移到data同级目录下。将原train文件夹中的img和xml改成为images和Annotations,移到test同级目录下,删除空的data之类文件夹后得到一个看着更加标准的数据集。
images:图像文件夹
Annotations:标签文件夹
test:测试图片
在这里插入图片描述
代码可以专门作为划分数据集的代码,支持训练集、验证集和测试集并可以自主定义划分的标签后缀并不局限于xml文件

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
VOC 格式数据集划分(支持 train/val/test)
用法示例:
  python split_voc.py --xml_dir /data/VOC/Annotations --out_dir /data/VOC/ImageSets
  python split_voc.py --xml_dir ./Anno --out_dir ./Sets --train 0.8 --val 0.1 --test 0.1 --seed 123 --ext .json
"""
import random
import argparse
from pathlib import Path


def split_dataset(ann_dir: Path, out_dir: Path,
                  train_ratio: float = 0.7, val_ratio: float = 0.2, test_ratio: float = 0.1,
                  seed: int = 42, ext: str = '.xml'):
    """划分数据集为 train/val/test 三个清单文件"""
    if abs(train_ratio + val_ratio + test_ratio - 1.0) > 1e-6:
        raise ValueError("train_ratio + val_ratio + test_ratio 必须 = 1.0")

    # ① 收集所有标注文件
    files = sorted(ann_dir.rglob(f'*{ext}'))          # 递归 + 排序
    if not files:
        raise FileNotFoundError(f"{ann_dir} 下未找到 *{ext} 文件")

    # ② 随机打乱
    random.seed(seed)
    indices = list(range(len(files)))
    random.shuffle(indices)

    # ③ 计算切分点
    train_size = int(len(files) * train_ratio)
    val_size   = int(len(files) * val_ratio)
    test_size  = len(files) - train_size - val_size   # 剩余给 test

    # ④ 写入清单(自动建目录)
    out_dir.mkdir(parents=True, exist_ok=True)
    stems = [p.stem for p in files]                   # 去掉后缀,兼容多"."文件名

    def write_list(name, idxs):
        (out_dir / f'{name}.txt').write_text('\n'.join([stems[i] for i in idxs]) + '\n')

    write_list('train', indices[:train_size])
    write_list('val',   indices[train_size:train_size + val_size])
    write_list('test',  indices[train_size + val_size:])

    print(f"✅ 划分完成 → {out_dir}")
    print(f"   总样本 {len(files)}  train={train_size}  val={val_size}  test={test_size}")
    print(f"   随机种子 {seed}  扩展名 {ext}")


def main():
    parser = argparse.ArgumentParser(description='VOC 格式数据集划分(支持 train/val/test)')
    parser.add_argument('--xml_dir',  type=Path, default="/home/moon/yolo/yolov5/VOC/Annotations", help='标注目录(会递归搜索)')
    parser.add_argument('--out_dir',  type=Path, default="/home/moon/yolo/yolov5/VOC/ImageSets/Main", help='输出清单目录')
    parser.add_argument('--train',  type=float, default=0.7, help='训练集比例(默认 0.7)')
    parser.add_argument('--val',    type=float, default=0.3, help='验证集比例(默认 0.2)')
    parser.add_argument('--test',   type=float, default=0, help='测试集比例(默认 0.1)')
    parser.add_argument('--seed',   type=int,   default=42,  help='随机种子(默认 42)')
    parser.add_argument('--ext',    type=str,   default='.xml', help='标注扩展名(默认 .xml)')
    args = parser.parse_args()

    split_dataset(args.xml_dir, args.out_dir,
                  train_ratio=args.train, val_ratio=args.val, test_ratio=args.test,
                  seed=args.seed, ext=args.ext)


if __name__ == '__main__':
    main()

用法:
标注文件目录:/home/moon/yolo/yolov5/VOC/Annotations(对应自己的修改)
输出目录:/home/moon/yolo/yolov5/VOC/ImageSets/Main(建议只修改ImageSets之前的路径,即/home/moon/yolo/yolov5/VOC)
划分比例:train=0.7, val=0.3, test=0(可自定义)
随机种子:42(随机程度)
文件扩展名:.xml(标签后缀)
这里我选择使用8:2进行划分
在这里插入图片描述
在这里插入图片描述
生成的txt文件是逐行存储图片文件名(不带后缀),有时候除不尽可能test会有一到两个,大家自己简单移动到任意一个即可。
此时的train.txt和val并不是我们训练所需要的,可以认为是中间产物。
下一步我们将xml标签转换为yolo标签并加上绝对路径或者相对路径。

4.数据集格式转换

在之前视觉学习中我们已经说过了xml与yolo的映射关系,这里直接给出一个转换代码

# -*- coding: utf-8 -*-
import xml.etree.ElementTree as ET
import os
import argparse
from pathlib import Path
from os import getcwd

# 支持的图片格式
SUPPORTED_IMAGE_FORMATS = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif', '.webp']


def find_image_file(image_id, image_dir):
    """查找对应标注文件的图片文件"""
    for ext in SUPPORTED_IMAGE_FORMATS:
        image_path = image_dir / f"{image_id}{ext}"
        if image_path.exists():
            return image_path
    return None


def convert(size, box):
    """将边界框坐标转换为YOLO格式"""
    dw = 1.0 / size[0]
    dh = 1.0 / size[1]
    x = (box[0] + box[1]) / 2.0 - 1
    y = (box[2] + box[3]) / 2.0 - 1
    w = box[1] - box[0]
    h = box[3] - box[2]
    x = x * dw
    w = w * dw
    y = y * dh
    h = h * dh
    return x, y, w, h


def convert_annotation(image_id, ann_dir, label_dir, classes):
    """转换单个标注文件"""
    xml_file = ann_dir / f"{image_id}.xml"

    if not xml_file.exists():
        print(f"⚠️  警告:标注文件不存在 {xml_file}")
        return False

    try:
        tree = ET.parse(xml_file)
        root = tree.getroot()
        size = root.find('size')

        if size is None:
            print(f"⚠️  警告:{xml_file} 中未找到size信息")
            return False

        w = int(size.find('width').text)
        h = int(size.find('height').text)

        label_file = label_dir / f"{image_id}.txt"
        with open(label_file, 'w', encoding='utf-8') as out_file:
            for obj in root.iter('object'):
                difficult_elem = obj.find('difficult')
                difficult = difficult_elem.text if difficult_elem is not None else '0'

                name_elem = obj.find('name')
                if name_elem is None:
                    continue

                cls = name_elem.text
                if cls not in classes or int(difficult) == 1:
                    continue

                cls_id = classes.index(cls)
                xmlbox = obj.find('bndbox')

                if xmlbox is None:
                    continue

                try:
                    xmin = float(xmlbox.find('xmin').text)
                    xmax = float(xmlbox.find('xmax').text)
                    ymin = float(xmlbox.find('ymin').text)
                    ymax = float(xmlbox.find('ymax').text)
                except (AttributeError, ValueError) as e:
                    print(f"⚠️  警告:{xml_file} 中边界框格式错误: {e}")
                    continue

                # 标注越界修正
                xmin = max(0, min(xmin, w))
                xmax = max(0, min(xmax, w))
                ymin = max(0, min(ymin, h))
                ymax = max(0, min(ymax, h))

                b = (xmin, xmax, ymin, ymax)
                bb = convert((w, h), b)
                out_file.write(f"{cls_id} {' '.join([f'{a:.6f}' for a in bb])}\n")

        return True

    except ET.ParseError as e:
        print(f"❌ 错误:解析XML文件失败 {xml_file}: {e}")
        return False
    except Exception as e:
        print(f"❌ 错误:处理文件 {xml_file} 时发生异常: {e}")
        return False


def main():
    parser = argparse.ArgumentParser(description='VOC转YOLO格式,支持多种图片格式')
    parser.add_argument('--voc_root', type=Path, default='/home/moon/yolo/yolov5/VOC',
                        help='VOC数据集根目录')
    parser.add_argument('--classes', nargs='+', default=['apple', 'banana', 'orange', 'dragonfruit'],
                        help='类别列表')
    parser.add_argument('--image_dir', type=str, default='images',
                        help='图片目录名称(相对于voc_root)')
    parser.add_argument('--ann_dir', type=str, default='Annotations',
                        help='标注文件目录名称(相对于voc_root)')
    parser.add_argument('--sets_dir', type=str, default='ImageSets/Main',
                        help='数据集划分文件目录(相对于voc_root)')
    parser.add_argument('--output_dir', type=str, default='dataSet_path',
                        help='输出文件列表目录(相对于voc_root)')
    args = parser.parse_args()

    # 设置路径
    voc_root = Path(args.voc_root)
    image_dir = voc_root / args.image_dir
    ann_dir = voc_root / args.ann_dir
    sets_dir = voc_root / args.sets_dir
    label_dir = voc_root / 'labels'
    output_dir = voc_root / args.output_dir

    classes = args.classes
    sets = ['train', 'val', 'test']

    print(f"🎯 开始转换 VOC → YOLO 格式")
    print(f"📁 VOC根目录: {voc_root}")
    print(f"🖼️  图片目录: {image_dir}")
    print(f"📄 标注目录: {ann_dir}")
    print(f"📊 类别: {classes}")

    # 创建输出目录
    label_dir.mkdir(parents=True, exist_ok=True)
    output_dir.mkdir(parents=True, exist_ok=True)

    total_success = 0
    total_failed = 0

    for image_set in sets:
        set_file = sets_dir / f"{image_set}.txt"

        if not set_file.exists():
            print(f"⚠️  跳过:{set_file} 不存在")
            continue

        try:
            with open(set_file, 'r', encoding='utf-8') as f:
                image_ids = [line.strip() for line in f.readlines() if line.strip()]
        except Exception as e:
            print(f"❌ 错误:读取 {set_file} 失败: {e}")
            continue

        if not image_ids:
            print(f"⚠️  跳过:{set_file} 为空")
            continue

        # 创建数据集列表文件
        list_file = output_dir / f"{image_set}.txt"
        success_count = 0
        failed_count = 0

        with open(list_file, 'w', encoding='utf-8') as f_list:
            for image_id in image_ids:
                # 查找图片文件
                image_path = find_image_file(image_id, image_dir)
                if image_path is None:
                    print(f"⚠️  警告:未找到图片文件 {image_id}")
                    failed_count += 1
                    continue

                # 转换标注
                if convert_annotation(image_id, ann_dir, label_dir, classes):
                    f_list.write(f"{image_path}\n")
                    success_count += 1
                else:
                    failed_count += 1

        total_success += success_count
        total_failed += failed_count

        print(f"✅ {image_set}: 成功 {success_count}, 失败 {failed_count}")

    print(f"\n🎉 转换完成!")
    print(f"📊 总计: 成功 {total_success}, 失败 {total_failed}")
    print(f"📁 标签文件输出到: {label_dir}")
    print(f"📋 数据集列表输出到: {output_dir}")

    # 保存类别文件
    classes_file = voc_root / 'classes.txt'
    with open(classes_file, 'w', encoding='utf-8') as f:
        for cls in classes:
            f.write(f"{cls}\n")
    print(f"🏷️  类别文件保存到: {classes_file}")


if __name__ == '__main__':
    main()

用法:
–voc_root: VOC数据集根目录(一般修改这个即可)
–classes: 类别列表(需与yaml文件中类别顺序对应)
–image_dir: 图片目录名(已经默认为images)
–ann_dir: 标注文件目录名(已经默认为Annotations)
–sets_dir: 数据集划分文件目录(默认无需更改)
在这里插入图片描述

最终在dataSet_path文件夹下得到我们真正需要的txt文件
在这里插入图片描述

现在让我们补全yaml文件吧
在这里插入图片描述


三、模型训练:从零开始训练你的检测模型

1.选择合适的预训练模型

预训练模型 = 别人已经替你训练好的神经网络权重文件。
我们往往并不需要从零开始训练,而是直接加载别人练好的参数,再在我们的小数据集上**微调(Fine-Tune)**即可。可以说大多数情况我们做的都是微调模型,而不是从零训练。
站在巨人肩膀上处理问题!

从零训练预训练 + 微调
需要海量数据几百~几千张就能用
需要昂贵算力普通 GPU/CPU 即可
收敛慢、易过拟合收敛快、泛化好
领域预训练模型作用
图像ResNet50、EfficientNet、YOLOv5s.pt识别、检测、分割
文本BERT、GPT、T5翻译、问答、生成
语音Wav2Vec 2.0语音识别、合成
多模态CLIP图文匹配、零样本分类

yolov5官方给出了从n到x的不同规模的预训练权重,在之前我们已经下载好了n和s,大家也可以尝试其他的模型。
通过这张表大家也可以很明显看出来越往下模型效果越好,参数量越大,所需要的算力资源越大。算力的重要性一目了然。
在小样本数据集中我的笔记本GPU只能支持我使用到s或者m,但是对于机器人比赛已经绰绰有余了。
在这里插入图片描述

在这里插入图片描述

2.网络配置文件

在yolov5/models文件夹中存储着网络模型和不同模型参数的网络配置yaml文件
在这里插入图片描述
我们打开yolov5n.yaml

# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license

# Parameters
nc: 80 # number of classes
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.25 # layer channel multiple
anchors:
  - [10, 13, 16, 30, 33, 23] # P3/8
  - [30, 61, 62, 45, 59, 119] # P4/16
  - [116, 90, 156, 198, 373, 326] # P5/32

# YOLOv5 v6.0 backbone
backbone:
  # [from, number, module, args]
  [
    [-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2
    [-1, 1, Conv, [128, 3, 2]], # 1-P2/4
    [-1, 3, C3, [128]],
    [-1, 1, Conv, [256, 3, 2]], # 3-P3/8
    [-1, 6, C3, [256]],
    [-1, 1, Conv, [512, 3, 2]], # 5-P4/16
    [-1, 9, C3, [512]],
    [-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
    [-1, 3, C3, [1024]],
    [-1, 1, SPPF, [1024, 5]], # 9
  ]

# YOLOv5 v6.0 head
head: [
    [-1, 1, Conv, [512, 1, 1]],
    [-1, 1, nn.Upsample, [None, 2, "nearest"]],
    [[-1, 6], 1, Concat, [1]], # cat backbone P4
    [-1, 3, C3, [512, False]], # 13

    [-1, 1, Conv, [256, 1, 1]],
    [-1, 1, nn.Upsample, [None, 2, "nearest"]],
    [[-1, 4], 1, Concat, [1]], # cat backbone P3
    [-1, 3, C3, [256, False]], # 17 (P3/8-small)

    [-1, 1, Conv, [256, 3, 2]],
    [[-1, 14], 1, Concat, [1]], # cat head P4
    [-1, 3, C3, [512, False]], # 20 (P4/16-medium)

    [-1, 1, Conv, [512, 3, 2]],
    [[-1, 10], 1, Concat, [1]], # cat head P5
    [-1, 3, C3, [1024, False]], # 23 (P5/32-large)

    [[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
  ]

可以清晰看到整个网络模型的架构,开头的nc数量在训练时会被我们数据集yaml文件中的nc进行覆盖
这次训练我们就简单使用一下yolov5n这个权重

3.训练参数

打开trian.py,官方就已经给出了单GPU和多GPU训练的指令。从这里开始,我们的训练指令并不是简单的一个按钮了,而是在终端中执行python命令,通过不同配置参数完成我们的训练
在这里插入图片描述
yolov5在train.py中给出了参数的英文解释
在这里插入图片描述
在这里插入图片描述
这里以表格的形式给出中文解释,但是英文的表述应该更加准确,所以大家需要提高英语水平。
🔧 1. 模型与权重

参数默认值白话解释要不要改
weightsyolov5s.pt预训练权重;换模型就改它常用
cfg‘’网络结构 yaml;空=用 weights 自带一般不动
datacoco128.yaml数据集配置文件;换数据必改常用
hyphyp.scratch-low.yaml超参配方;调参才动进阶

🎯 2. 训练规模

参数默认值白话解释要不要改
epochs100总轮次;小数据集 50-300常用
batch_size16总批量;显存占满即可必调
imgsz640输入分辨率;大=慢+准常用
optimizerSGD优化器;换 Adam/AdamW 可改可选

⚡ 3. 加速与技巧

参数默认值白话解释要不要改
rectFalse矩形训练(batch 内少 padding)推理常用
cacheNoneRAM/Disk 缓存;数据少→ram常用
multi_scaleFalse±50% 分辨率抖动;涨点慢可选
single_clsFalse多类当单类练;特殊需求极少
sync_bnFalse多卡同步 BN;DDP 才有效多卡必开

🧊 4. 正则与约束

参数默认值白话解释要不要改
label_smoothing0.0标签平滑;防过拟合可选
freeze[0]冻结层; backbone=10 只练头微调常用
cos_lrFalse余弦退火;学习率曲线更平滑可选

⏹️ 5. 早停与保存

参数默认值白话解释要不要改
patience100早停忍耐轮次;无提升即停可减小
save_period-1每 N 轮存 ckpt;-1=只存 best/last大模型可 10
nosaveFalse只存 final;省磁盘极少

🌐 6. 分布式 & 设备

参数默认值白话解释要不要改
device‘’GPU 编号;‘’=自动常用
workers8数据加载线程;CPU 多可增大报错再减
local_rank-1DDP 自动注入;手动勿改勿动

🏃 7. 训练控制

参数默认值白话解释要不要改
resumeFalse断点续训;给路径或 True中断必用
evolve0超参进化;>0=遗传算法调参调参才开

📁 8. 输出路径

参数默认值白话解释要不要改
projectruns/train实验根目录可改
nameexp子文件夹;exp/ exp2/ …可改
exist_okFalse允许覆盖旧实验调试时可开

这么多参数!学长,这我怎么背,诶~记住下面这段话

换数据改 data,换模型改 weights,显存决定 batch_size,epochs从200慢慢减,其余默认先跑着看!

调参是一门经验与技术并存的学问,同一个模型,调参调的好不仅训练快效果还好。

4.训练开始

在你大致了解上述参数时,我们就可以进行训练了

# 使用预训练权重启动训练,绝对/相对路径都可
python train.py --img 640 --batch 16 --epochs 100 \
                --data VOC/fruit.yaml --weights yolov5n.pt

我们的图片大小为标准的480P,也就是640*480,为了不丢失信息,我们一般使用640大小,当然你也可以选择480,都是以正方形作为训练
训练开始,之前我们已经有了机器学习训练的经验了,起手仍然是打印超参数信息和设备信息。

在这里插入图片描述
第一次需要下载缺失的字体,你也可以进行手动加速,我直接暂停手动下载重新开始
这里是训练的超参数,也是改进的重要依据
在这里插入图片描述
网络模型
在这里插入图片描述
加载数据集并转换为缓存,如果训练出现是因为数据集的错误,需要删除对应缓存重新生成
在这里插入图片描述
开始训练,训练轮数,GPU显存占用,loss值,和一系列评价指标
在这里插入图片描述
耐心等待训练结束(cpu和GPU训练速度相差可能有5-10倍甚至更多),如果出现out of memory字样说明你的显存不够用了,减少batch或者换小模型吧
在这里插入图片描述
不到一小时,训练结果就出来了,对于要应用于比赛的目标检测模型来说,我们的目的就是把四个评价指标在不过拟合的情况下尽可能的刷高。
我们先看看产出文件夹内的东西
在这里插入图片描述
在这里插入图片描述
混淆矩阵(Confusion Matrix):是分类任务中最常用的性能可视化工具之一

真实类对角线值主要错误去向解读
apple~0.97几乎无分得非常准
banana~0.88少量→apple基本正确,偶被认成苹果
orange~0.80少量→banana尚可,偶被认成香蕉
dragonfruit~0.60大量→background最难类,近四成被当背景
background~0.98少量→dragonfruit背景学得很好,但把火龙果也当背景

后续分析/改进方向:

  • dragonfruit 召回低

增大数据量 / 增强 / 调高该类损失权重
检查标注:是否框太小/被遮挡?

  • apple → banana 混淆

颜色/形状相似 → 加 HSV 增强、MixUp、Copy-Paste
调高输入分辨率(imgsz=832)让细节更明显

  • 背景误检龙果

负样本里加「龙果局部」patch,降低 False Positive
调低背景类置信度阈值(conf_thres)

在这里插入图片描述
F1-Confidence 曲线(F1 得分随置信度阈值变化的趋势图),它告诉你:
当模型把“置信度 ≥ x” 的样本才当作正例时,各类别的 F1 得分是多少?最优阈值在哪?

类别峰值 F1最优阈值*说明
apple~0.97~0.25最稳;低阈值就能高 F1
banana~0.88~0.30良好;稍松即可
orange~0.80~0.35中等;阈值要更严
dragonfruit~0.60~0.45最差;必须高置信才可靠
all classes0.95 @ 0.3720.372全局最优阈值

告诉我们:
dragonfruit 单独补救
调高该类损失权重
增加该类训练样本 / 增强

在这里插入图片描述
标签-尺寸」分布热力图(Labels-Width 散点密度图),用来告诉你:
数据集中所有目标的 归一化宽度 与 实例数量 的关系,以及每个宽度区间里各类别各占多少。

宽度区间计数峰值主导类别解读
0.05–0.15~1200 个dragonfruit 青色占大头小目标最多,龙果偏小
0.15–0.30~800 个banana/orange 交替中等目标,香蕉橙子为主
0.30–0.60~400 个apple 红色突出大目标多是苹果
>0.6<100 个几乎无极大目标极少
  • 小目标(<0.1)过多

增大数据增强 Copy-Paste、Mosaic 比例
调高 anchor_t=4.0 或自定义小 anchor
训练时 imgsz 增大(832/1024)让小目标变大

  • 大目标(>0.5)过少

检查是否框被截断或图片分辨率不足
主动添加「近距离拍摄」样本

  • 类别-尺寸失衡

dragonfruit 全是小框 → 专门补大框样本
apple 全是大框 → 补中远距离拍摄

  • 与 anchor 对比

运行 python utils/autoanchor.py --data your.yaml
看当前 anchor 是否与小目标峰值对齐,不对齐就重算
在这里插入图片描述
Labels Correlogram(标签相关矩阵图),它把数据集中所有目标框的 4 个几何属性两两配对,画出散点密度 + 线性关系,一眼就能看出:
框的宽-高、中心点 x-y、尺寸-位置 之间有没有规律、有没有异常

子图现象解读
width vs height散点呈对角带状宽≈高 → 大多数目标是近似正方形
x vs y均匀散布在 0-1目标在图像全域均匀出现,无显著聚类
width vs x两端稍暗,中间亮大目标更集中在画面中心(透视自然现象)
height vs y同上垂直方向同理
对角线直方图近似高斯尺寸分布单峰,无严重长尾

在这里插入图片描述
Precision-Confidence 曲线(精度随置信度阈值变化的趋势),它告诉你:
当模型只把“置信度 ≥ x” 的样本当作正例时,每一类的 Precision(查准率)是多少?
越高越好,越靠左越好。
在这里插入图片描述
PR 曲线(Precision-Recall Curve) 的「终极版」:
它把每个类别的 Precision 随 Recall 变化的整条轨迹画出来,面积 = AP(Average Precision),均值 = mAP@0.5。

类别AP(曲线下面积)解读
apple0.994几乎完美,误检极少,召回极高
banana0.944良好;唯一 <0.95,需重点关注
orange0.980优秀
dragonfruit0.983优秀(打破“小物体=低AP”魔咒
mAP@0.50.975整体性能极佳,已接近工业上线标准

后续改进:
banana 补数据/调参
查看 confusion_matrix → 是否被错分成 apple/orange
增训 banana 样本 / Copy-Paste / 颜色增强
单独调高 banana 的 class weight 或 focal loss γ
在这里插入图片描述
Recall-Confidence 曲线(召回率随置信度阈值变化的趋势),它告诉你:
当模型只把“置信度 ≥ x” 的样本当作正例时,每一类还能找回多少正样本?
越靠右上越好,横坐标=阈值,纵坐标=召回率。

类别Recall@0.000(最左)下降趋势解读
apple~0.995极缓慢几乎无漏检,阈值再低也找不回更多
banana~0.99缓慢优秀,低阈值下已接近全召回
orange~0.985缓慢同上
dragonfruit~0.98缓慢小目标也做到全召回
all classes0.99 @ 0.000全局阈值=0 时召回 99%漏检极少

在这里插入图片描述
results.png 是 YOLOv5 训练过程的「六合一仪表盘」:
它把 损失下降 和 指标上升 放在同一张图里,一眼就能判断训练是否健康、是否过拟合、是否该停。

面板曲线含义理想趋势本例解读(示例)
train/box_loss训练集 框坐标 损失↓ 趋 0已降到 0.02收敛良好
train/obj_loss训练集 objectness 损失↓ 趋 00.01收敛
train/cls_loss训练集 分类 损失↓ 趋 00.005收敛
val/box_loss验证集 框坐标 损失↓ → 平稳0.015无上扬无过拟合
val/obj_loss验证集 objectness 损失↓ → 平稳0.01平稳
metrics/mAP_0.5验证集 mAP@0.5↑ → 平稳0.975极高
metrics/mAP_0.5:0.95验证集 mAP@0.5:0.95↑ → 平稳0.95极高

异常对照

现象可能原因快速修复
val loss 上扬过拟合早停 / 增数据 / 降模型 / 正则
train ↓ val ↑严重过拟合降学习率 / 加 DropOut / 更多数据
loss 震荡大学习率过大降 lr / 加大 batch
mAP 早平学习率过小升 lr / 减 patience

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

内容重点检查
train_batch0.jpg训练集第 0 个 batch 的输入图像(已增强)1. 增强是否正常(mosaic、颜色、尺寸)
2. 标签框是否同步绘制
val_batch0_labels.jpg验证集第 0 个 batch 的人工标注1. 框是否漏/错
2. 类别文字是否正确
val_batch0_pred.jpg同上,但框是模型预测 + 置信度1. 框位置是否对齐
2. 置信度是否合理
3. 有无漏检/误检
检查项结果说明
增强正常可见 mosaic 拼接、色彩未失真
标注正确类别文字与框一致,无乱码
预测对齐框与目标基本重合,置信度 0.8-1.0 为主
漏检极少肉眼未见明显漏框
误检可控低置信度 0.3-0.5 少量,可调阈值过滤

异常对照

现象可能原因快速修复
框漂移/错位增强参数过猛degrees/translate
类别乱码中文路径/字体缺失用英文标签 + 指定字体
大量漏框anchor 不匹配重跑 autoanchor
置信度普遍低学习率不当/未收敛检查 loss 曲线,调 lr

综上分析,这次训练其实已经可以使用了,当然如果你对这个结果不满意,你也可以把batch改大,然后把轮数改小(你也可以增大找到快趋于平滑的轮数),工作线程也加一点,同时修改我们的超参数

5.超参数

超参数是认为定义的某些会影响训练结果的参数,调参调的好,月薪少不了。
在yolov5/data/hyps存放着yolov5给出的超参数训练配置,每次训练开始,我们可以根据上次次训练得到可视化图来调整我们的超参数。batch,imgs等也属于超参数。
在这里插入图片描述
我们在训练中使用的是low.yaml

lr0: 0.01 # initial learning rate (SGD=1E-2, Adam=1E-3)
lrf: 0.01 # final OneCycleLR learning rate (lr0 * lrf)
momentum: 0.937 # SGD momentum/Adam beta1
weight_decay: 0.0005 # optimizer weight decay 5e-4
warmup_epochs: 3.0 # warmup epochs (fractions ok)
warmup_momentum: 0.8 # warmup initial momentum
warmup_bias_lr: 0.1 # warmup initial bias lr
box: 0.05 # box loss gain
cls: 0.5 # cls loss gain
cls_pw: 1.0 # cls BCELoss positive_weight
obj: 1.0 # obj loss gain (scale with pixels)
obj_pw: 1.0 # obj BCELoss positive_weight
iou_t: 0.20 # IoU training threshold
anchor_t: 4.0 # anchor-multiple threshold
# anchors: 3  # anchors per output layer (0 to ignore)
fl_gamma: 0.0 # focal loss gamma (efficientDet default gamma=1.5)
hsv_h: 0.015 # image HSV-Hue augmentation (fraction)
hsv_s: 0.7 # image HSV-Saturation augmentation (fraction)
hsv_v: 0.4 # image HSV-Value augmentation (fraction)
degrees: 0.0 # image rotation (+/- deg)
translate: 0.1 # image translation (+/- fraction)
scale: 0.5 # image scale (+/- gain)
shear: 0.0 # image shear (+/- deg)
perspective: 0.0 # image perspective (+/- fraction), range 0-0.001
flipud: 0.0 # image flip up-down (probability)
fliplr: 0.5 # image flip left-right (probability)
mosaic: 1.0 # image mosaic (probability)
mixup: 0.0 # image mixup (probability)
copy_paste: 0.0 # segment copy-paste (probability)

🎯 1. 优化器与学习率

参数默认值口诀何时调
lr00.01SGD=1e-2,Adam=1e-3loss 爆炸 ↓ 10 倍;loss 不动 ↑ 2-5 倍
lrf0.01OneCycle 终点倍数想更慢收敛 → 0.1;想更快 → 0.001
momentum0.937SGD 惯性震荡大 → 0.8;太平 → 0.98
weight_decay5e-4L2 正则过拟合 → 1e-3;欠拟合 → 1e-5

🔥 2. 损失函数权重(按类 imbalance 调)

参数默认值口诀何时调
box0.05框坐标 loss 增益框偏移大 ↑ 0.1;框已准 ↓ 0.01
cls0.5分类 loss 增益类 imbalance ↑ 1.0;平衡 ↓ 0.2
obj1.0objectness 增益小目标多 ↑ 1.5;大目标 ↓ 0.5
fl_gamma0Focal Loss γ难例/ imbalance >0(1.5-2.5);易例=0

🧊 3. 数据增强(低增强配方)
yolov5有自己的马赛克数据增强方法,下面是数据增强的参数

参数默认值口诀何时调
degrees0.0旋转角度工业刚性物体 0;自然场景 ±10-15
translate0.1平移比例低增强 ≤0.2;高增强 0.3-0.5
scale0.5缩放增益低增强 ≤0.5;高增强 0.8-1.0
mosaic1.0四图拼接概率大/小目标都多 1.0;超大图 0.5
mixup0.0MixUp 概率低增强 0;通用 0.1-0.2
copy_paste0.0Copy-Paste 概率小目标少 0.3-0.5;已充足 0

⚙️ 4. 锚框与 IoU 阈值

参数默认值口诀何时调
iou_t0.20正样本 IoU 门限小目标多 ↓ 0.15;大目标 ↑ 0.25
anchor_t4.0anchor 倍数阈值瘦长物体 ↑ 6-8;正方形 ↓ 3

🧪 5. 颜色增强(不变形)

参数默认值口诀何时调
hsv_h0.015色调漂移颜色鲁棒 0.01-0.02;强增强 0.1
hsv_s0.7饱和度低增强 ≤0.7;强增强 0.8-1.0
hsv_v0.4亮度低增强 ≤0.4;强增强 0.6-0.9

想快收敛用默认低增强;想涨点→逐档加 degrees/scale/mixup/copy_paste,小目标加 copy_paste,颜色
imbalance 加 HSV,loss 震荡调 lr,框不准加 box,类 imbalance 加 cls & fl_gamma!


四、模型推理(模型测试)

1.测试参数

这里我们还是用到我们的detect.py
在这里插入图片描述
yolov5有着各种途径的检测手法,这里我们使用对文件夹内图像进行批处理

Usage - sources:
    $ python detect.py --weights yolov5s.pt --source 0                               # webcam
                                                     img.jpg                         # image
                                                     vid.mp4                         # video
                                                     screen                          # screenshot
                                                     path/                           # directory
                                                     list.txt                        # list of images
                                                     list.streams                    # list of streams
                                                     'path/*.jpg'                    # glob
                                                     'https://youtu.be/LNwODJXcvt4'  # YouTube
                                                     'rtsp://example.com/media.mp4'  # RTSP, RTMP, HTTP stream

与train.py相同,test.py也有自己的推理配置参数
在这里插入图片描述
🔍 1. 输入/模型(最常用)

参数默认值人话解释我什么时候改
--weightsyolov5s.pt换模型;s/m/l/x 或自定义要速度→s;要精度→m/l
--sourcedata/images数据来源;文件夹/视频/摄像头/URL每次推理必改
--imgsz640推理分辨率;越大越准越慢手机→320;PC→640;服务器→1280
--conf-thres0.25置信度门槛;低于此数丢弃误检多→↑0.4;漏检多→↓0.15
--iou-thres0.45NMS 重叠阈值;越大保留越多框框重叠大→↑0.6;框稀少→↓0.3

🎯 2. 结果输出(按需开关)

参数默认值人话解释我什么时候改
--view-imgFalse弹窗看结果;调试时打开想实时看→True
--save-txtFalse保存 YOLO 坐标文件要导入下游→True
--save-csvFalse保存 CSV 表格Excel 用户→True
--save-cropFalse把每个框裁成小块做识别/跟踪→True
--nosaveFalse完全不存图/视频;只看弹窗磁盘紧张→True

🎨 3. 画框外观(美观/可视化)

参数默认值人话解释我什么时候改
--line-thickness3框线粗细(像素)高清图→5;小图→1
--hide-labelsFalse隐藏类别文字只做计数→True
--hide-confFalse隐藏置信度报告/演示→True

⚡ 4. 性能加速(推理更快)

参数默认值人话解释我什么时候改
--device“”GPU 编号;“”=自动多卡→"0,1";CPU→"cpu"
--halfFalseFP16 半精度;RTX 必开20 系列以上→True;速度↑30%
--vid-stride1视频跳帧;每 N 帧检一次长视频→5-10;实时→1

🔍 5. 高级功能(极少用)

参数默认值人话解释我什么时候改
--classesNone只检某几类;例 --classes 0 2只要人/车
--agnostic-nmsFalse跨类 NMS(不同类也抑制)密集场景→True
--augmentFalseTTA 测试时增强;精度↑速度↓决赛/报告→True
--visualizeFalse保存特征图;调试用研究特征→True

🗂️ 6. 输出路径(按需改)

参数默认值人话解释我什么时候改
--projectruns/detect结果总目录改项目名
--nameexp子目录;自动 exp/ exp2/ …改实验名
--exist-okFalse允许覆盖旧结果调试→True

日常只改 4 个:
–weights(模型)
–source(数据)
–conf-thres(门槛)
–device(GPU)
其余默认先跑,看结果再动!

2.开始测试

#我们以阈值超过0.8作为可见
python detect.py --weights runs/train/exp3/weights/best.pt --source VOC/test --conf-thres 0.8 --device 0

在这里插入图片描述
在这里插入图片描述
发现还是有一些图片是没有识别到的,我们到输出结果文件夹看一眼
在这里插入图片描述

这里就有四分之一个苹果没有识别全,可能是我们给的阈值太高了
在这里插入图片描述
再试一次,使用默认阈值0.25,发现有很多之前没有检测到的现在显示检测到了
在这里插入图片描述
右下角这个苹果也识别出来了,但是置信度不高。
这就要求我们在实际使用的时候要把握好置信度的使用,你到底是要查准,还是要查全。
在这里插入图片描述

3.半自动数据增强思想

有时候我们无法一开始就获得大量有效的数据集,自己又不想浪费大量时间去打标几千张图片。这个时候我们可以使用detect.py中**–save-txt**参数。我们可以进行几百张数据集规模的打标然后训练。
同时对原始数据集进行数据增强但不增强标签。
在这里插入图片描述
在这里插入图片描述
代码运行完毕后会再生成一个标签文件夹存放测试数据的txt标签
在这里插入图片描述
是不是变相的实现了数据增强呢?
至于你是否要把txt标签文件再转换为xml标签,就看你个人咯。
完全可以写一个标签转换代码嘛~
顺便说一句,搞视觉的你手里不得有coco、txt、xml等主流格式的转换代码嘛~
什么?
你没有?想想自己有没有努力!!!

import os
import glob
from PIL import Image
import xml.etree.ElementTree as ET
from xml.dom import minidom

# 配置路径(请根据实际情况修改)
yolo_dir = '/path/to/yolo/labels'  # YOLO标签目录
output_xml_dir = '/path/to/output/xml'  # XML输出目录
img_dir = '/path/to/images'  # 图片目录
missing_labels_log = '/path/to/missing_labels.txt'  # 缺失标签记录文件

# 类别标签映射(必须与YOLO类别ID对应)
labels = ["person", "car", "bicycle"]  # 示例类别

# 确保输出目录存在
os.makedirs(output_xml_dir, exist_ok=True)

# 获取所有图片路径(支持多种格式)
img_paths = glob.glob(os.path.join(img_dir, '*.jpg')) + \
            glob.glob(os.path.join(img_dir, '*.png')) + \
            glob.glob(os.path.join(img_dir, '*.jpeg'))

missing_images = []
processed_count = 0

for img_path in img_paths:
    processed_count += 1
    img_name = os.path.splitext(os.path.basename(img_path))[0]
    
    if processed_count % 100 == 0:
        print(f"Processing: {processed_count}/{len(img_paths)}")

    try:
        # 打开图片获取尺寸
        with Image.open(img_path) as img:
            width, height = img.size
        
        # 检查标签文件是否存在
        label_path = os.path.join(yolo_dir, f"{img_name}.txt")
        if not os.path.exists(label_path):
            missing_images.append(img_path)
            continue
            
        # 创建XML结构
        annotation = ET.Element('annotation')
        ET.SubElement(annotation, 'folder').text = 'VOC2007'
        ET.SubElement(annotation, 'filename').text = os.path.basename(img_path)
        
        size = ET.SubElement(annotation, 'size')
        ET.SubElement(size, 'width').text = str(width)
        ET.SubElement(size, 'height').text = str(height)
        ET.SubElement(size, 'depth').text = '3' if img.mode == 'RGB' else '1'

        # 解析YOLO标签
        with open(label_path, 'r') as f:
            lines = f.readlines()
        
        for line in lines:
            parts = line.strip().split()
            if len(parts) < 5:
                continue
                
            try:
                class_id = int(parts[0])
                # 检查类别ID是否有效
                if class_id < 0 or class_id >= len(labels):
                    print(f"Warning: Invalid class ID {class_id} in {label_path}")
                    continue
                    
                # 转换YOLO格式为像素坐标
                x_center = float(parts[1]) * width
                y_center = float(parts[2]) * height
                w = float(parts[3]) * width
                h = float(parts[4]) * height
                
                # 计算边界框并确保在图像范围内
                xmin = max(0, int(x_center - w/2))
                ymin = max(0, int(y_center - h/2))
                xmax = min(width, int(x_center + w/2))
                ymax = min(height, int(y_center + h/2))
                
                # 跳过无效边界框
                if xmin >= xmax or ymin >= ymax:
                    continue
                
                # 添加对象节点
                obj = ET.SubElement(annotation, 'object')
                ET.SubElement(obj, 'name').text = labels[class_id]
                ET.SubElement(obj, 'pose').text = 'Unspecified'
                ET.SubElement(obj, 'truncated').text = '0'
                ET.SubElement(obj, 'difficult').text = '0'
                
                bbox = ET.SubElement(obj, 'bndbox')
                ET.SubElement(bbox, 'xmin').text = str(xmin)
                ET.SubElement(bbox, 'ymin').text = str(ymin)
                ET.SubElement(bbox, 'xmax').text = str(xmax)
                ET.SubElement(bbox, 'ymax').text = str(ymax)
                
            except (ValueError, IndexError) as e:
                print(f"Error parsing line in {label_path}: {line.strip()} - {e}")

        # 格式化并保存XML
        xml_str = ET.tostring(annotation, 'utf-8')
        pretty_xml = minidom.parseString(xml_str).toprettyxml(indent="    ")
        
        output_path = os.path.join(output_xml_dir, f"{img_name}.xml")
        with open(output_path, 'w') as xml_file:
            xml_file.write(pretty_xml)
            
    except Exception as e:
        print(f"Error processing {img_path}: {str(e)}")

# 记录缺失标签
if missing_images:
    with open(missing_labels_log, 'w') as f:
        f.write("\n".join(missing_images))
    print(f"{len(missing_images)} images missing labels logged")

print(f"Conversion completed. Processed {len(img_paths)} images")

五、调试指南:常见问题与解决方案(持续更新,欢迎与我反馈,我会及时补充)

6.1 CUDA Out of Memory

现象RuntimeError: CUDA out of memory
解决

# 降低批量大小
python train.py --batch 8 ...

# 启用混合精度训练
python train.py --batch 16 --device 0 --amp ...

6.2 标签格式错误

现象ValueError: not enough values to unpack
解决

  • 检查标签文件是否为 YOLO 格式(类别 ID、中心点坐标、宽高)
  • 确认 data.yaml 中的 nc 与实际类别数一致

6.3 模型推理速度慢

现象:FPS 低于预期
解决

  • 使用量化/剪枝优化模型
  • 升级显卡驱动
  • 启用 TensorRT 或 ONNX Runtime 加速

总结:

通过本文的系统学习,你可以快速掌握 YOLOv5 的训练和推理了。实际应用中,建议结合具体场景调整参数(如小目标检测需增大 iou_thr),并善用可视化工具(如 TensorBoard)监控训练过程。遇到问题时,优先检查数据质量(标签准确性、图片分辨率一致性)和硬件配置(显存、CUDA 版本)。

您可能感兴趣的与本文相关的镜像

Llama Factory

Llama Factory

模型微调
LLama-Factory

LLaMA Factory 是一个简单易用且高效的大型语言模型(Large Language Model)训练与微调平台。通过 LLaMA Factory,可以在无需编写任何代码的前提下,在本地完成上百种预训练模型的微调

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值