【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8

1.研究背景与意义

项目参考AAAI Association for the Advancement of Artificial Intelligence

研究背景与意义

随着人工智能技术的不断发展,计算机视觉领域的研究也取得了巨大的进展。目标检测是计算机视觉中的一个重要任务,它的应用范围广泛,包括图像识别、视频监控、自动驾驶等领域。其中,杂草识别是农业领域中的一个重要问题,它对于提高农作物的产量和质量具有重要意义。

传统的杂草识别方法主要依赖于人工特征提取和分类器的设计,这种方法存在着特征提取不准确、分类器性能有限等问题。近年来,基于深度学习的目标检测方法取得了显著的成果,其中YOLO(You Only Look Once)系列算法以其高效性能和实时性能受到了广泛关注。

YOLOv8是YOLO系列算法的最新版本,它在YOLOv3的基础上进行了改进,提高了检测精度和速度。然而,YOLOv8在杂草识别任务中仍然存在一些问题。首先,YOLOv8对于小尺寸的杂草目标检测效果较差,容易出现漏检和误检的情况。其次,YOLOv8在复杂背景下的杂草识别能力有限,容易受到背景干扰的影响。

因此,本研究旨在改进YOLOv8算法,提高其在杂草识别任务中的性能。具体而言,我们将融合YOLO-MS的MS-Block改进YOLOv8。YOLO-MS是一种多尺度检测的方法,它可以提高目标检测的精度和鲁棒性。MS-Block是一种多尺度特征融合的模块,它可以有效地融合不同尺度的特征信息。通过将这两种方法融合到YOLOv8中,我们可以提高其对小尺寸杂草的检测能力,并减少背景干扰对识别结果的影响。

本研究的意义主要体现在以下几个方面。首先,改进后的YOLOv8算法可以提高杂草识别的准确性和鲁棒性,有助于农业生产中对杂草的有效管理。其次,本研究采用的融合方法可以为其他目标检测任务提供借鉴和参考,推动计算机视觉领域的研究进一步发展。最后,本研究的成果可以为农业科技的发展提供支持,促进农业生产的现代化和智能化。

总之,改进YOLOv8算法在杂草识别系统中的应用具有重要的研究意义和实际应用价值。通过融合YOLO-MS的MS-Block,我们可以提高目标检测的精度和鲁棒性,为农业生产中的杂草管理提供有效的技术支持。同时,本研究的成果也将为计算机视觉领域的研究和应用提供新的思路和方法。

2.图片演示

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

3.视频演示

【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8_哔哩哔哩_bilibili

4.数据集的采集&标注和整理

图片的收集

首先,我们需要收集所需的图片。这可以通过不同的方式来实现,例如使用现有的公开数据集WeedDatasets。

在这里插入图片描述

labelImg是一个图形化的图像注释工具,支持VOC和YOLO格式。以下是使用labelImg将图片标注为VOC格式的步骤:

(1)下载并安装labelImg。
(2)打开labelImg并选择“Open Dir”来选择你的图片目录。
(3)为你的目标对象设置标签名称。
(4)在图片上绘制矩形框,选择对应的标签。
(5)保存标注信息,这将在图片目录下生成一个与图片同名的XML文件。
(6)重复此过程,直到所有的图片都标注完毕。

由于YOLO使用的是txt格式的标注,我们需要将VOC格式转换为YOLO格式。可以使用各种转换工具或脚本来实现。

下面是一个简单的方法是使用Python脚本,该脚本读取XML文件,然后将其转换为YOLO所需的txt格式。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import xml.etree.ElementTree as ET
import os

classes = []  # 初始化为空列表

CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))

def convert(size, box):
    dw = 1. / size[0]
    dh = 1. / size[1]
    x = (box[0] + box[1]) / 2.0
    y = (box[2] + box[3]) / 2.0
    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):
    in_file = open('./label_xml\%s.xml' % (image_id), encoding='UTF-8')
    out_file = open('./label_txt\%s.txt' % (image_id), 'w')  # 生成txt格式文件
    tree = ET.parse(in_file)
    root = tree.getroot()
    size = root.find('size')
    w = int(size.find('width').text)
    h = int(size.find('height').text)

    for obj in root.iter('object'):
        cls = obj.find('name').text
        if cls not in classes:
            classes.append(cls)  # 如果类别不存在,添加到classes列表中
        cls_id = classes.index(cls)
        xmlbox = obj.find('bndbox')
        b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
             float(xmlbox.find('ymax').text))
        bb = convert((w, h), b)
        out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')

xml_path = os.path.join(CURRENT_DIR, './label_xml/')

# xml list
img_xmls = os.listdir(xml_path)
for img_xml in img_xmls:
    label_name = img_xml.split('.')[0]
    print(label_name)
    convert_annotation(label_name)

print("Classes:")  # 打印最终的classes列表
print(classes)  # 打印最终的classes列表

整理数据文件夹结构

我们需要将数据集整理为以下结构:

-----data
   |-----train
   |   |-----images
   |   |-----labels
   |
   |-----valid
   |   |-----images
   |   |-----labels
   |
   |-----test
       |-----images
       |-----labels

确保以下几点:

所有的训练图片都位于data/train/images目录下,相应的标注文件位于data/train/labels目录下。
所有的验证图片都位于data/valid/images目录下,相应的标注文件位于data/valid/labels目录下。
所有的测试图片都位于data/test/images目录下,相应的标注文件位于data/test/labels目录下。
这样的结构使得数据的管理和模型的训练、验证和测试变得非常方便。

模型训练
 Epoch   gpu_mem       box       obj       cls    labels  img_size
 1/200     20.8G   0.01576   0.01955  0.007536        22      1280: 100%|██████████| 849/849 [14:42<00:00,  1.04s/it]
           Class     Images     Labels          P          R     mAP@.5 mAP@.5:.95: 100%|██████████| 213/213 [01:14<00:00,  2.87it/s]
             all       3395      17314      0.994      0.957      0.0957      0.0843

 Epoch   gpu_mem       box       obj       cls    labels  img_size
 2/200     20.8G   0.01578   0.01923  0.007006        22      1280: 100%|██████████| 849/849 [14:44<00:00,  1.04s/it]
           Class     Images     Labels          P          R     mAP@.5 mAP@.5:.95: 100%|██████████| 213/213 [01:12<00:00,  2.95it/s]
             all       3395      17314      0.996      0.956      0.0957      0.0845

 Epoch   gpu_mem       box       obj       cls    labels  img_size
 3/200     20.8G   0.01561    0.0191  0.006895        27      1280: 100%|██████████| 849/849 [10:56<00:00,  1.29it/s]
           Class     Images     Labels          P          R     mAP@.5 mAP@.5:.95: 100%|███████   | 187/213 [00:52<00:00,  4.04it/s]
             all       3395      17314      0.996      0.957      0.0957      0.0845

5.核心代码讲解

5.2 predict.py

封装为类后的代码如下:

from ultralytics.engine.predictor import BasePredictor
from ultralytics.engine.results import Results
from ultralytics.utils import ops

class DetectionPredictor(BasePredictor):
    def postprocess(self, preds, img, orig_imgs):
        preds = ops.non_max_suppression(preds,
                                        self.args.conf,
                                        self.args.iou,
                                        agnostic=self.args.agnostic_nms,
                                        max_det=self.args.max_det,
                                        classes=self.args.classes)

        if not isinstance(orig_imgs, list):
            orig_imgs = ops.convert_torch2numpy_batch(orig_imgs)

        results = []
        for i, pred in enumerate(preds):
            orig_img = orig_imgs[i]
            pred[:, :4] = ops.scale_boxes(img.shape[2:], pred[:, :4], orig_img.shape)
            img_path = self.batch[0][i]
            results.append(Results(orig_img, path=img_path, names=self.model.names, boxes=pred))
        return results

这是一个名为predict.py的程序文件,它是一个基于检测模型进行预测的类DetectionPredictor的定义。该类继承自BasePredictor类。

该程序文件使用了Ultralytics YOLO库,使用AGPL-3.0许可证。

该文件中的DetectionPredictor类有一个postprocess方法,用于对预测结果进行后处理,并返回一个Results对象的列表。

在postprocess方法中,首先对预测结果进行非最大值抑制(non_max_suppression),根据设定的置信度阈值(conf)、重叠阈值(iou)、是否进行类别无关的非最大值抑制(agnostic_nms)、最大检测数(max_det)和类别列表(classes)进行过滤。

然后,对于每个预测结果,将其边界框坐标进行缩放,以适应原始图像的尺寸。

最后,将原始图像、图像路径、类别名称和预测边界框作为参数,创建一个Results对象,并将其添加到结果列表中。

该文件还包含一个示例代码,展示了如何使用DetectionPredictor类进行预测。

5.3 train.py
from copy import copy
import numpy as np
from ultralytics.data import build_dataloader, build_yolo_dataset
from ultralytics.engine.trainer import BaseTrainer
from ultralytics.models import yolo
from ultralytics.nn.tasks import DetectionModel
from ultralytics.utils import LOGGER, RANK
from ultralytics.utils.torch_utils import de_parallel, torch_distributed_zero_first

class DetectionTrainer(BaseTrainer):
    def build_dataset(self, img_path, mode='train', batch=None):
        gs = max(int(de_parallel(self.model).stride.max() if self.model else 0), 32)
        return build_yolo_dataset(self.args, img_path, batch, self.data, mode=mode, rect=mode == 'val', stride=gs)

    def get_dataloader(self, dataset_path, batch_size=16, rank=0, mode='train'):
        assert mode in ['train', 'val']
        with torch_distributed_zero_first(rank):
            dataset = self.build_dataset(dataset_path, mode, batch_size)
        shuffle = mode == 'train'
        if getattr(dataset, 'rect', False) and shuffle:
            LOGGER.warning("WARNING ⚠️ 'rect=True' is incompatible with DataLoader shuffle, setting shuffle=False")
            shuffle = False
        workers = 0
        return build_dataloader(dataset, batch_size, workers, shuffle, rank)

    def preprocess_batch(self, batch):
        batch['img'] = batch['img'].to(self.device, non_blocking=True).float() / 255
        return batch

    def set_model_attributes(self):
        self.model.nc = self.data['nc']
        self.model.names = self.data['names']
        self.model.args = self.args

    def get_model(self, cfg=None, weights=None, verbose=True):
        model = DetectionModel(cfg, nc=self.data['nc'], verbose=verbose and RANK == -1)
        if weights:
            model.load(weights)
        return model

    def get_validator(self):
        self.loss_names = 'box_loss', 'cls_loss', 'dfl_loss'
        return yolo.detect.DetectionValidator(self.test_loader, save_dir=self.save_dir, args=copy(self.args))

    def label_loss_items(self, loss_items=None, prefix='train'):
        keys = [f'{prefix}/{x}' for x in self.loss_names]
        if loss_items is not None:
            loss_items = [round(float(x), 5) for x in loss_items]
            return dict(zip(keys, loss_items))
        else:
            return keys

    def progress_string(self):
        return ('\n' + '%11s' *
                (4 + len(self.loss_names))) % ('Epoch', 'GPU_mem', *self.loss_names, 'Instances', 'Size')

    def plot_training_samples(self, batch, ni):
        plot_images(images=batch['img'],
                    batch_idx=batch['batch_idx'],
                    cls=batch['cls'].squeeze(-1),
                    bboxes=batch['bboxes'],
                    paths=batch['im_file'],
                    fname=self.save_dir / f'train_batch{ni}.jpg',
                    on_plot=self.on_plot)

    def plot_metrics(self):
        plot_results(file=self.csv, on_plot=self.on_plot)

    def plot_training_labels(self):
        boxes = np.concatenate([lb['bboxes'] for lb in self.train_loader.dataset.labels], 0)
        cls = np.concatenate([lb['cls'] for lb in self.train_loader.dataset.labels], 0)
        plot_labels(boxes, cls.squeeze(), names=self.data['names'], save_dir=self.save_dir, on_plot=self.on_plot)

这是一个用于训练基于检测模型的程序文件train.py。它使用了Ultralytics YOLO库,主要包括以下几个部分:

  1. 导入所需的库和模块。
  2. 定义了一个名为DetectionTrainer的类,该类继承自BaseTrainer类,用于训练基于检测模型的任务。
  3. 在DetectionTrainer类中定义了一些方法,包括构建数据集、构建数据加载器、预处理批次、设置模型属性等。
  4. 在DetectionTrainer类中还定义了一些用于可视化训练过程和结果的方法,如绘制训练样本、绘制指标、绘制训练标签等。
  5. 在主函数中,创建了一个DetectionTrainer对象,并调用其train方法进行训练。

该程序文件的功能是使用YOLO模型进行目标检测任务的训练,训练过程中会加载指定的模型和数据集,并根据指定的超参数进行训练。训练过程中会输出训练进度、损失值等信息,并可视化训练样本和训练结果。

5.5 extra_modules\afpn.py
from collections import OrderedDict
import torch
import torch.nn as nn
import torch.nn.functional as F
from ..modules.conv import Conv
from ..modules.block import C2f, C3, C3Ghost
from .block import *

class BasicBlock(nn.Module):
    expansion = 1

    def __init__(self, filter_in, filter_out):
        super(BasicBlock, self).__init__()
        self.conv1 = Conv(filter_in, filter_out, 3)
        self.conv2 = Conv(filter_out, filter_out, 3, act=False)

    def forward(self, x):
        residual = x

        out = self.conv1(x)
        out = self.conv2(out)

        out += residual
        return self.conv1.act(out)


class Upsample(nn.Module):
    def __init__(self, in_channels, out_channels, scale_factor=2):
        super(Upsample, self).__init__()

        self.upsample = nn.Sequential(
            Conv(in_channels, out_channels, 1),
            nn.Upsample(scale_factor=scale_factor, mode='bilinear')
        )

    def forward(self, x):
        x = self.upsample(x)
        return x


class Downsample_x2(nn.Module):
    def __init__(self, in_channels, out_channels):
        super(Downsample_x2, self).__init__()

        self.downsample = Conv(in_channels, out_channels, 2, 2, 0)

    def forward(self, x):
        x = self.downsample(x)
        return x


class Downsample_x4(nn.Module):
    def __init__(self, in_channels, out_channels):
        super(Downsample_x4, self).__init__()

        self.downsample = Conv(in_channels, out_channels, 4, 4, 0)

    def forward(self, x):
        x = self.downsample(x)
        return x


class Downsample_x8(nn.Module):
    def __init__(self, in_channels, out_channels):
        super(Downsample_x8, self).__init__()

        self.downsample = Conv(in_channels, out_channels, 8, 8, 0)

    def forward(self, x):
        x = self.downsample(x)
        return x


class ASFF_2(nn.Module):
    def __init__(self, inter_dim=512):
        super(ASFF_2, self).__init__()

        self.inter_dim = inter_dim
        compress_c = 8

        self.weight_level_1 = Conv(self.inter_dim, compress_c, 1)
        self.weight_level_2 = Conv(self.inter_dim, compress_c, 1)

        self.weight_levels = nn.Conv2d(compress_c * 2, 2, kernel_size=1, stride=1, padding=0)

        self.conv = Conv(self.inter_dim, self.inter_dim, 3)

    def forward(self, input1, input2):
        level_1_weight_v = self.weight_level_1(input1)
        level_2_weight_v = self.weight_level_2(input2)

        levels_weight_v = torch.cat((level_1_weight_v, level_2_weight_v), 1)
        levels_weight = self.weight_levels(levels_weight_v)
        levels_weight = F.softmax(levels_weight, dim=1)

        fused_out_reduced = input1 * levels_weight[:, 0:1, :, :] + \
                            input2 * levels_weight[:, 1:2, :, :]

        out = self.conv(fused_out_reduced)
        return out


class ASFF_3(nn.Module):
    def __init__(self, inter_dim=512):
        super(ASFF_3, self).__init__()

        self.inter_dim = inter_dim
        compress_c = 8

        self.weight_level_1 = Conv(self.inter_dim, compress_c, 1)
        self.weight_level_2 = Conv(self.inter_dim, compress_c, 1)
        self.weight_level_3 = Conv(self.inter_dim, compress_c, 1)

        self.weight_levels = nn.Conv2d(compress_c * 3, 3, kernel_size=1, stride=1, padding=0)

        self.conv = Conv(self.inter_dim, self.inter_dim, 3)

    def forward(self, input1, input2, input3):
        level_1_weight_v = self.weight_level_1(input1)
        level_2_weight_v = self.weight_level_2(input2)
        level_3_weight_v = self.weight_level_3(input3)

        levels_weight_v = torch.cat((level_1_weight_v, level_2_weight_v, level_3_weight_v), 1)
        levels_weight = self.weight_levels(levels_weight_v)
        levels_weight = F.softmax(levels_weight, dim=1)

        fused_out_reduced = input1 * levels_weight[:, 0:1, :, :] + \
                            input2 * levels_weight[:, 1:2, :, :] + \
                            input3 * levels_weight[:, 2:, :, :]

        out = self.conv(fused_out_reduced)
        return out


......

该程序文件是一个实现了AFPN(Adaptive Feature Pyramid Network)的模型。AFPN是一种用于目标检测任务的特征金字塔网络,用于提取不同尺度的特征并进行融合。

该文件定义了一些辅助模块,如BasicBlock、Upsample、Downsample_x2等,以及三个不同尺度的ASFF(Adaptive Spatial Feature Fusion)模块,分别为ASFF_2、ASFF_3和ASFF_4。ASFF模块用于将不同尺度的特征进行融合。

该文件还定义了一个BlockBody_P345模块,用于构建AFPN的主体部分。BlockBody_P345包含了多个BasicBlock模块和ASFF模块,用于提取和融合不同尺度的特征。

最后,该文件定义了一个AFPN_P345模块,用于整合各个部分并输出最终的特征图。

另外,该文件还定义了一个BlockBody_P345_Custom模块,它继承自BlockBody_P345,并可以根据传入的参数选择不同类型的BasicBlock模块。

总体来说,该程序文件实现了一个用于目标检测的AFPN模型,并提供了一些可定制的选项。

5.6 extra_modules\attention.py
import torch
from torch import nn

class EMA(nn.Module):
    def __init__(self, channels, factor=8):
        super(EMA, self).__init__()
        self.groups = factor
        assert channels // self.groups > 0
        self.softmax = nn.Softmax(-1)
        self.agp = nn.AdaptiveAvgPool2d((1, 1))
        self.pool_h = nn.AdaptiveAvgPool2d((None, 1))
        self.pool_w = nn.AdaptiveAvgPool2d((1, None))
        self.gn = nn.GroupNorm(channels // self.groups, channels // self.groups)
        self.conv1x1 = nn.Conv2d(channels // self.groups, channels // self.groups, kernel_size=1, stride=1, padding=0)
        self.conv3x3 = nn.Conv2d(channels // self.groups, channels // self.groups, kernel_size=3, stride=1, padding=1)

    def forward(self, x):
        b, c, h, w = x.size()
        group_x = x.reshape(b * self.groups, -1, h, w)  # b*g,c//g,h,w
        x_h = self.pool_h(group_x)
        x_w = self.pool_w(group_x).permute(0, 1, 3, 2)
        hw = self.conv1x1(torch.cat([x_h, x_w], dim=2))
        x_h, x_w = torch.split(hw, [h, w], dim=2)
        x1 = self.gn(group_x * x_h.sigmoid() * x_w.permute(0, 1, 3, 2).sigmoid())
        x2 = self.conv3x3(group_x)
        x11 = self.softmax(self.agp(x1).reshape(b * self.groups, -1, 1).permute(0, 2, 1))
        x12 = x2.reshape(b * self.groups, c // self.groups, -1)  # b*g, c//g, hw
        x21 = self.softmax(self.agp(x2).reshape(b * self.groups, -1, 1).permute(0, 2, 1))
        x22 = x1.reshape(b * self.groups, c // self.groups, -1)  # b*g, c//g, hw
        weights = (torch.matmul(x11, x12) + torch.matmul(x21, x22)).reshape(b * self.groups, 1, h, w)
        return (group_x * weights.sigmoid()).reshape(b, c, h, w)

这个程序文件包含了一些额外的模块,包括EMA、SimAM、SpatialGroupEnhance等。其中,EMA模块实现了指数移动平均,SimAM模块实现了自适应模块,SpatialGroupEnhance模块实现了空间组增强。此外,还包含了一些其他的模块,如TopkRouting、KVGather和QKVLinear等。这些模块的具体功能和实现细节可以在代码中找到。

6.系统整体结构

根据以上分析,该程序是一个用于杂草识别系统的工程。它使用了改进的YOLOv8模型,并包含了训练、预测、导出模型以及用户界面等功能。

下表总结了每个文件的功能:

文件路径功能
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\export.py将YOLOv8模型导出为其他格式的文件
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\predict.py运行YOLOv8模型进行预测
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\train.py训练YOLOv8模型
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\ui.py创建杂草识别系统的用户界面
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\afpn.py实现了AFPN模型
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\attention.py包含了一些额外的模块,如EMA、SimAM、SpatialGroupEnhance等
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\block.py定义了一些基础的模块,如Conv、C3、CSP等
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\dynamic_snake_conv.py实现了动态蛇形卷积模块
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\head.py定义了YOLOv3和YOLOv4的头部模块
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\kernel_warehouse.py定义了一些卷积核的存储和加载函数
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\orepa.py实现了OREPA模块
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\rep_block.py实现了RepBlock模块
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\RFAConv.py实现了RFAConv模块
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules_init_.py定义了extra_modules模块的初始化文件
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\ops_dcnv3\setup.py安装和编译DCNv3模块的脚本
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\ops_dcnv3\test.py测试DCNv3模块的脚本
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\ops_dcnv3\functions\dcnv3_func.py实现了DCNv3模块的函数
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\ops_dcnv3\functions_init_.py定义了ops_dcnv3.functions模块的初始化文件
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\ops_dcnv3\modules\dcnv3.py实现了DCNv3模块
E:\视觉项目\shop\【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8\code\extra_modules\ops_dcnv3\modules_init_.py定义了ops_dcnv3.modules模块的初始化文件

7.YOLOv8简介

YOLOv8目标检测算法继承了YOLOv1 系列的思考,是一种新型端到端的目标检测算法,尽管现在原始检测算法已经开源,但是鲜有发表的相关论文.YOLOv8的网络结构如图1[1所示,主要可分为Input输入端、Backbone 骨干神经网络、Neck混合特征网络层和 Head预测层网络共4个部分.
在这里插入图片描述
输入端( input)方法包含的功能模块有:马赛克( mosaic)数据增强、自适应锚框( anchor)计算、自适应图片缩放和 Mixup 数据增强['6].马赛克数据增强包括3种方式:缩放,色彩空间调整和马赛克增强.
该方法通过将4张图像进行随机的缩放﹑裁剪和打乱分布方式等操作来重新拼接图像,可丰富检测的数据集,具体步骤可见图2.随机缩放增加的许多小目标,非常适于解决卫星数据往往因距离目标过远从而导致图像中几乎都是小目标这一问题.在自适应锚框计算中, YOLO算法在每次训练数据之前,都会根据标注信息自动计算该数据集最合适的锚框尺寸,然后自动匹配最佳锚框.而自适应图片缩放只在检测时使用,由于不同场景需求不同,因而基于缩放系数YOLOv8提供了多尺度的不同大小模型.Mixup 数据增强鼓励模型对训练样本有一个线性的理解,具体做法是在目标检测中将两幅图像的像素值按照图像透明度的通道信息( alpha值)进行线性融合,对于标签box的处理直接采用拼接( con-cat)的方法拼接到一起.

在这里插入图片描述
Backbone骨干网络指用来提取图像特征的网络,整体结构包括注意力机制( focus)模块、跨阶段局部网络[ 7] ( cross stage partial network , CSP)和空间金字塔池化结构( spatial pyramid pooling,SPP).其中, Focus模块的作用是在图片进入 Backbone骨干网络前,对图片进行切片操作,即在一张图片中每隔一个像素取一个值,获得4张互补的图片,最后将新生成的图片经过卷积操作,得到没有信息丢失的2倍下采样特征图.YOLOv8使用了CSPNet 中的C2f网络,网络见图3,其中 CBS 就是卷积层,而瓶颈层( bottleneck layer)使用的是1* 1的卷积神经网络.C2f网络在保证轻量化的同时获得更加丰富的梯度流信息.而SPP结构是空间金字塔池化,能将任意大小的特征图转换成固定大小的特征向量,即把输入的特征地图划分为多个尺度,然后对每个图进行最大池化,再将提取的特征值拼接起来成为一维向量,输入SPP层获取分类.
在这里插入图片描述

Neck 结构如图4所示,它是由卷积层和C2f模块组成的的网络层,采用了路径聚合网络( path ag-gregation network ,PAN)和特征金字塔网络( featurepyramid networks , FPN)的结构对特征进行多尺度融合,目标是将图像特征传递到预测层.其中 PAN结构指图4左半边,它自底向上进行下采样,使顶层特征包含图像位置信息,两个特征最后进行融合,使不同尺寸的特征图都包含图像语义信息和图像特征信息,保证了网络对不同尺寸的图片的准确预测.而FPN结构指图4右半边,指通过自顶向下进行上采样,将高层特征与底层特征进行融合,从而同时利用低层特征的高分辨率和高层特征的丰富语义信息,并进行了多尺度特征的独立预测,对小物体的检测效果有明显的提升.从 FPN模块的基础上看,它增加了自底向上的特征金字塔结构,保留了更多的浅层位置特征,将整体特征提取能力进一步提升.

在这里插入图片描述
Head的结构如图5所示,在该结构中 YOLOv8采用了解耦检测头( decoupled-head )[ 18],因为分类和定位的关注点不同,分类更关注目标的纹理内容而定位更关注目标的边缘信息.因而解耦头结构考虑到分类和定位所关注的内容的不同,采用不同的分支来进行运算,提升了检测效果,相对应的回归头的通道数也改变了.

在这里插入图片描述

8.MS-Block简介

实时目标检测,以YOLO系列为例,已在工业领域中找到重要应用,特别是在边缘设备(如无人机和机器人)中。与之前的目标检测器不同,实时目标检测器旨在在速度和准确性之间追求最佳平衡。为了实现这一目标,提出了大量的工作:从第一代DarkNet到CSPNet,再到最近的扩展ELAN,随着性能的快速增长,实时目标检测器的架构经历了巨大的变化。

尽管性能令人印象深刻,但在不同尺度上识别对象仍然是实时目标检测器面临的基本挑战。这促使作者设计了一个强大的编码器架构,用于学习具有表现力的多尺度特征表示。具体而言,作者从两个新的角度考虑为实时目标检测编码多尺度特征:

从局部视角出发,作者设计了一个具有简单而有效的分层特征融合策略的MS-Block。受到Res2Net的启发,作者在MS-Block中引入了多个分支来进行特征提取,但不同的是,作者使用了一个带有深度卷积的 Inverted Bottleneck Block块,以实现对大Kernel的高效利用。

从全局视角出发,作者提出随着网络加深逐渐增加卷积的Kernel-Size。作者在浅层使用小Kernel卷积来更高效地处理高分辨率特征。另一方面,在深层中,作者采用大Kernel卷积来捕捉广泛的信息。

基于以上设计原则,作者呈现了作者的实时目标检测器,称为YOLO-MS。为了评估作者的YOLO-MS的性能,作者在MS COCO数据集上进行了全面的实验。还提供了与其他最先进方法的定量比较,以展示作者方法的强大性能。如图1所示,YOLO-MS在计算性能平衡方面优于其他近期的实时目标检测器。
在这里插入图片描述

具体而言,YOLO-MS-XS在MS COCO上获得了43%+的AP得分,仅具有450万个可学习参数和8.7亿个FLOPs。YOLO-MS-S和YOLO-MS分别获得了46%+和51%+的AP,可学习参数分别为810万和2220万。此外,作者的工作还可以作为其他YOLO模型的即插即用模块。通常情况下,作者的方法可以将YOLOv8的AP从37%+显著提高到40%+,甚至还可以使用更少的参数和FLOPs。

CSP Block是一个基于阶段级梯度路径的网络,平衡了梯度组合和计算成本。它是广泛应用于YOLO系列的基本构建块。已经提出了几种变体,包括YOLOv4和YOLOv5中的原始版本,Scaled YOLOv4中的CSPVoVNet,YOLOv7中的ELAN,以及RTMDet中提出的大Kernel单元。作者在图2(a)和图2(b)中分别展示了原始CSP块和ELAN的结构。

在这里插入图片描述

上述实时检测器中被忽视的一个关键方面是如何在基本构建块中编码多尺度特征。其中一个强大的设计原则是Res2Net,它聚合了来自不同层次的特征以增强多尺度表示。然而,这一原则并没有充分探索大Kernel卷积的作用,而大Kernel卷积已经在基于CNN的视觉识别任务模型中证明有效。将大Kernel卷积纳入Res2Net的主要障碍在于它们引入的计算开销,因为构建块采用了标准卷积。在作者的方法中,作者提出用 Inverted Bottleneck Block替代标准的3 × 3卷积,以享受大Kernel卷积的好处。

MS-Block

基于前面的分析,参考该博客提出了一个带有分层特征融合策略的全新Block,称为MS-Block,以增强实时目标检测器在提取多尺度特征时的能力,同时保持快速的推理速度。

MS-Block的具体结构如图2©所示。假设是输入特征。通过1×1卷积的转换后,X的通道维度增加到n*C。然后,作者将X分割成n个不同的组,表示为,其中。为了降低计算成本,作者选择n为3。

注意,除了之外,每个其他组都经过一个 Inverted Bottleneck Block层,用表示,其中k表示Kernel-Size,以获得。的数学表示如下:

在这里插入图片描述

根据这个公式,作者不将 Inverted Bottleneck Block层连接到,使其作为跨阶段连接,并保留来自前面层的信息。最后,作者将所有分割连接在一起,并应用1×1卷积来在所有分割之间进行交互,每个分割都编码不同尺度的特征。当网络加深时,这个1×1卷积也用于调整通道数。

Heterogeneous Kernel Selection Protocol

除了构建块的设计外,作者还从宏观角度探讨了卷积的使用。之前的实时目标检测器在不同的编码器阶段采用了同质卷积(即具有相同Kernel-Size的卷积),但作者认为这不是提取多尺度语义信息的最佳选项。

在金字塔结构中,从检测器的浅阶段提取的高分辨率特征通常用于捕捉细粒度语义,将用于检测小目标。相反,来自网络较深阶段的低分辨率特征用于捕捉高级语义,将用于检测大目标。如果作者在所有阶段都采用统一的小Kernel卷积,深阶段的有效感受野(ERF)将受到限制,影响大目标的性能。在每个阶段中引入大Kernel卷积可以帮助解决这个问题。然而,具有大的ERF的大Kernel可以编码更广泛的区域,这增加了在小目标外部包含噪声信息的概率,并且降低了推理速度。

在这项工作中,作者建议在不同阶段中采用异构卷积,以帮助捕获更丰富的多尺度特征。具体来说,在编码器的第一个阶段中,作者采用最小Kernel卷积,而最大Kernel卷积位于最后一个阶段。随后,作者逐步增加中间阶段的Kernel-Size,使其与特征分辨率的增加保持一致。这种策略允许提取细粒度和粗粒度的语义信息,增强了编码器的多尺度特征表示能力。

正如图所示,作者将k的值分别分配给编码器中的浅阶段到深阶段,取值为3、5、7和9。作者将其称为异构Kernel选择(HKS)协议。

在这里插入图片描述

作者的HKS协议能够在深层中扩大感受野,而不会对浅层产生任何其他影响。第4节的图4支持了作者的分析。此外,HKS不仅有助于编码更丰富的多尺度特征,还确保了高效的推理。

如表1所示,将大Kernel卷积应用于高分辨率特征会产生较高的计算开销。然而,作者的HKS协议在低分辨率特征上采用大Kernel卷积,从而与仅使用大Kernel卷积相比,大大降低了计算成本。

在这里插入图片描述

在实践中,作者经验性地发现,采用HKS协议的YOLO-MS的推理速度几乎与仅使用深度可分离的3 × 3卷积相同。

在这里插入图片描述

如图所示,作者模型的Backbone由4个阶段组成,每个阶段后面跟随1个步长为2的3 × 3卷积进行下采样。在第3个阶段后,作者添加了1个SPP块,与RTMDet中一样。在作者的编码器上,作者使用PAFPN作为Neck来构建特征金字塔[31, 35]。它融合了从Backbone不同阶段提取的多尺度特征。Neck中使用的基本构建块也是作者的MS-Block,在其中使用3 × 3深度可分离卷积进行快速推理。

此外,为了在速度和准确性之间取得更好的平衡,作者将Backbone中多级特征的通道深度减半。作者提供了3个不同尺度的YOLO-MS变体,即YOLO-MS-XS、YOLO-MS-S和YOLO-MS。不同尺度的YOLO-MS的详细配置列在表2中。对于YOLO-MS的其他部分,作者将其保持与RTMDet相同。

9.训练结果可视化分析

评价指标

epoch:训练纪元数。
train/box_loss、train/obj_loss、train/cls_loss:分别是框、对象和类预测训练期间的损失指标。
指标/精度、指标/召回、指标/mAP_0.5、指标/mAP_0.5:0.95:不同 IoU(并集交集)阈值下的精度、召回率和平均精度 (mAP)。
val/box_loss、val/obj_loss、val/cls_loss:框、对象和类预测的验证损失。
x/lr0、x/lr1、x/lr2:模型中不同层或阶段的学习率。

训练结果可视化

综合分析,我们可以从以下几个方面进行可视化和讨论:

训练和验证损失:训练和验证期间损失(框、对象、类)如何随时间变化。
精确度和召回率:评估模型的准确性及其检测所有相关实例的能力。
平均精度 (mAP):评估模型在检测具有不同 IoU 阈值的对象时的整体性能。
学习率进展:了解学习率在不同时期的变化及其对训练的影响。
我们将首先为这些方面创建可视化,然后提供详细的分析。让我们从可视化开始。

import matplotlib.pyplot as plt
import seaborn as sns

# Set the aesthetic style of the plots
sns.set_style("whitegrid")

def plot_metrics(data, metric_names, title, ylabel):
    plt.figure(figsize=(12, 6))
    for metric in metric_names:
        plt.plot(data['epoch'], data[metric], label=metric)
    plt.title(title)
    plt.xlabel('Epoch')
    plt.ylabel(ylabel)
    plt.legend()
    plt.show()

# Plotting training and validation losses
plot_metrics(data, ['train/box_loss', 'train/obj_loss', 'train/cls_loss', 'val/box_loss', 'val/obj_loss', 'val/cls_loss'], 
             'Training and Validation Losses per Epoch', 'Loss')

# Plotting precision and recall
plot_metrics(data, ['metrics/precision', 'metrics/recall'], 'Precision and Recall per Epoch', 'Value')

# Plotting mAP
plot_metrics(data, ['metrics/mAP_0.5', 'metrics/mAP_0.5:0.95'], 'Mean Average Precision (mAP) per Epoch', 'mAP')

# Plotting learning rates
plot_metrics(data, ['x/lr0', 'x/lr1', 'x/lr2'], 'Learning Rates per Epoch', 'Learning Rate')

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可视化可让您深入了解 YOLOv8 模型在训练期间的各个方面的性能。我们来分析一下这些结果:

训练和验证损失:

损失(盒子、对象、类)通常会随着时间的推移而减少,表明模型学习良好。
验证损失遵循与训练损失类似的趋势,这表明模型没有过度拟合。
重要的是要注意这些损失的收敛性,因为巨大的差异可能表明存在泛化问题。

精确率和召回率:

准确率衡量的是实际正确识别的阳性识别的比例,而召回率衡量的是正确识别的实际阳性识别的比例。
精度仍然很低,这可能表明该模型做出了许多误报预测。
召回率逐渐增加,表明模型查找所有相关实例的能力不断提高。
优化精确度和召回率之间的平衡至关重要,尤其是在杂草检测中,漏掉杂草(低召回率)或将作物错误分类为杂草(低精确度)可能会付出高昂的代价。

平均精度 (mAP):

mAP 是一个综合指标,用于评估 YOLO 等对象检测模型。
两个 IoU 阈值(0.5 和 0.5:0.95)下的 mAP 随着时间的推移而增加,这是模型性能的积极迹象。
mAP_0.5:0.95是一个更严格的指标,因为它考虑了各种IoU阈值,并且该指标的增加特别令人鼓舞。

学习率进展:

不同层/阶段的学习率显示了模型的学习过程如何在历元内进行微调。
学习率的变化会影响学习的速度和质量。适当设置这些对于确保高效且有效的培训至关重要。
总之,该模型在损失减少和 mAP 增加方面显示出与历代相比有希望的改进。然而,低精度可能是一个问题,可以通过进一步调整模型、调整分类阈值或使用额外的数据增强技术来解决。学习率策略似乎很有效,损失和 mAP 指标的稳步改善就证明了这一点。为了进一步详细分析,深入研究特定的误报和漏报案例,分析不同类别杂草的性能,并可能对错误分类的实例进行错误分析,将是有益的。

10.系统整合

下图完整源码&数据集&环境部署视频教程&自定义UI界面

在这里插入图片描述

参考博客《【改进YOLOv8】杂草识别系统:融合YOLO-MS的MS-Block改进YOLOv8》

11.参考文献


[1]林文树,张金生,何乃磊.基于改进YOLO v4的落叶松毛虫侵害树木实时检测方法[J].农业机械学报.2023,54(4).DOI:10.6041/j.issn.1000-1298.2023.04.031 .

[2]银霞,叶绍泽.基于卷积神经网络的嵌入式排水管道缺陷检测系统[J].城市勘测.2023,(2).DOI:10.3969/j.issn.1672-8262.2023.02.043 .

[3]安小松,宋竹平,梁千月,等.基于CNN-Transformer的视觉缺陷柑橘分选方法[J].华中农业大学学报.2022,41(4).

[4]王道累,李明山,姚勇,等.改进SSD的光伏组件热斑缺陷检测方法[J].太阳能学报.2023,44(4).DOI:10.19912/j.0254-0096.tynxb.2021-1470 .

[5]胡定一.基于深度学习的缺陷柑橘分类识别检测方法研究[D].2021.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/238910.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

可视化监控云平台/智能监控EasyCVR如何使用脚本创建ramdisk挂载并在ramdisk中临时运行

视频云存储/安防监控EasyCVR视频汇聚平台基于云边端智能协同&#xff0c;支持海量视频的轻量化接入与汇聚、转码与处理、全网智能分发、视频集中存储等。安防管理视频平台EasyCVR拓展性强&#xff0c;视频能力丰富&#xff0c;具体可实现视频监控直播、视频轮播、视频录像、云存…

修改 vCenter Server 的 FQDN | hostname | PNID

目录 1 先决条件2. 修改 VC 7.0.3 hostname&#xff08;1&#xff09;备份 VCSA&#xff08;2&#xff09;为VCSA的新hostname创建DNS记录&#xff08;3&#xff09;修改 VCSA 的hostname① 进入vCenter Server VAMI② 查看当前 FQDN③ 编辑网络设置④ 选择网络适配器⑤ 修改 h…

Vue中各种混淆用法汇总

目录 Vue是一个流行的JavaScript框架&#xff0c;许多开发人员都在使用它来构建动态Web应用程序。在使用Vue时&#xff0c;会遇到各种混淆的用法&#xff0c;这些用法可以让您更有效地使用Vue。在本篇文章中&#xff0c;我们将介绍Vue中各种混淆的用法&#xff0c;并提供相应的…

智慧物联|消防物联网监管指挥平台实现对物联网消防警情的全面监测和智能管理

智慧消防是以物联网技术为基础的一种创新型监管指挥系统。它通过集成无线传感器、云计算和大数据分析等先进技术&#xff0c;实现对消防设备和消防员的全方位监测和管理。在传统的消防管理模式中&#xff0c;往往存在信息闭塞、响应速度慢等问题&#xff0c;而智慧消防系统的应…

王者荣耀展示

实现代码&#xff1a; package com.sxt;import javax.swing.*; import java.awt.*;public class Background extends GameObject {public Background(GameFrame gameFrame) {super(gameFrame);// TODO Auto-generated constructor stub}Image bg Toolkit.getDefaultToolkit()…

Leetcode 90 子集 II

题意理解&#xff1a; 求一个集合的子集&#xff1a;该集合有重复元素。 集合[1,2,2] []、[1]、[2]、[2]、[12]、[12]、[22]、[122] 子集&#xff1a;[]、[1]、[2]、[12]、[22]、[122]——由于元素有重复需要对子集进行去重操作。 所以此题的难点在于&#xff1a;子集去重 解题…

temu国内三大仓库在哪里

在拼多多跨境电商平台Temu的运营过程中&#xff0c;仓储物流是至关重要的一环。根据现有资料&#xff0c;Temu在国内设有三个主要仓库&#xff0c;分别位于广州、义乌和香港。这些仓库的位置选择是基于地理优势和市场需求的考虑。 先给大家推荐一款拼多多/temu运营工具——多多…

压缩软件电脑版哪个好?

压缩软件是我们存储文件、清理电脑、向他人发送文件经常用到的工具&#xff0c;下面就从头到尾操作一遍各个软件压缩步骤&#xff0c;根据需求选择好啦。可以放心的是&#xff0c;这四款软件都经过了安全测试&#xff0c;能够保证文件的安全性&#xff0c;并且能够兼容多种操作…

3_流量预测综述阅读_Cellular traffic prediction with machine learning: A survey

为了方便学习英语书写&#xff0c;总结的一些话用英语书写 ♥目录♥ 0、文献来源and摘要1、introduction2、prediction problems and datasets2.1 prediction problems2.2 dataset&#xff08;1&#xff09;Telecom Italia 意大利电信 2015&#xff08;2&#xff09;City Cell…

OOD : DMAD Diversity-Measurable Anomaly Detection

Diversity-Measurable Anomaly Detection 基于重建的异常检测模型通过抑制异常的泛化能力来迭代学习。然而&#xff0c;由于这种抑制&#xff0c;不同的正常模式的重建效果也会变得不理想。为了解决这个问题&#xff0c;本文提出了一种称为多样性可测量异常检测&#xff08;DMA…

计算机网络编程

网络编程 Java 是第一大编程语言和开发平台。它有助于企业降低成本、缩短开发周期、推动创新以及改善应用服务。如今全球有数百万开发人员运行着超过 51 亿个 Java 虚拟机&#xff0c; Java 仍是企业和开发人员的首选开发平台。 课程内容的介绍 1. 计算机网络基础 2. So…

射频功率放大器的参数有哪些

射频功率放大器是射频通信系统中重要的组件&#xff0c;用于将输入的射频信号放大到需要的功率水平。在设计和选择射频功率放大器时&#xff0c;需要考虑多种参数。下面西安安泰将详细介绍射频功率放大器的常见参数。 1、P1dB功率压缩点 当放大器的输入功率比较低时&#xff0c…

2024年AI云计算专题研究报告:智算带来的变化

今天分享的人工智能系列深度研究报告&#xff1a;《2024年AI云计算专题研究报告&#xff1a;智算带来的变化》。 &#xff08;报告出品方&#xff1a;华泰证券&#xff09; 报告共计&#xff1a;32页 Al 云计算 2024:关注智算带来的新变化 通过对海内外主要云厂商及其产业链…

Godot Engine:跨平台游戏开发的新境界 | 开源日报 No.92

godotengine/godot Stars: 62.6k License: MIT Godot Engine 是一个功能强大的跨平台游戏引擎&#xff0c;可用于创建 2D 和 3D 游戏。它提供了一套全面的常见工具&#xff0c;让用户可以专注于制作游戏而不必重复造轮子。该引擎支持将游戏一键导出到多个平台上&#xff0c;包…

crmeb后台自定义菜单并生成代码

crmeb v5 版本&#xff0c; 前后端分端 后台菜单的生成 进入后台界面之后&#xff0c;我们可以看到界面如下 找到 维护->开发配置->权限维护->添加规则按扭 我们要在设置的 菜单之下&#xff0c;添加一个 基础配置的 子菜单 提交之后&#xff0c;刷新页面就会在列…

在开发微信小程序的时候,报错navigateBack:fail cannot navigate back at firstpage

这个错误的意思是&#xff1a;在这个页面已经是第一个页面了&#xff0c;没办法再返回了 报错原因 这个错误原因其实也简单&#xff0c;就是在跳转的时候使用了wx.redirectTo()&#xff0c;使用wx.redirectTo()相当于重定向&#xff0c;不算是从上一个页面跳转过来的&#xf…

消费升级:无人零售的崛起与优势

消费升级&#xff1a;无人零售的崛起与优势 随着人们生活水平的提高&#xff0c;消费内容正在从生存型消费转向以精神体验和享乐为主的发展型消费。社会居民的消费结构不断变迁&#xff0c;明显呈现消费升级趋势。个性化和多元化消费势头正在崛起&#xff0c;特别是无人零售的自…

nextcloud如何将一个文件共享给所有人

nextcloud能够将文件/文件夹共享给某个用户或者用户组或者生成链接分享&#xff0c;但是无法直接将某个文件共享给nextcloud内部所有用户&#xff0c;并且nextcloud只有分组的概念&#xff0c;没有分组上下级的概念。 我们可以一个用户一个用户的共享&#xff0c;或者创建一个…

使用rancher rke快速安装k8s集群

概述 Rancher Kubernetes Engine&#xff08;RKE&#xff09;是一个用于部署、管理和运行Kubernetes集群的开源工具。旨在简化Kubernetes集群的部署和操作。 RKE具有以下特点和功能&#xff1a; 简化的部署过程 RKE提供了一个简单的命令行界面&#xff0c;使您可以轻松地部署…

WhatsApp全球获客怎么做?

一、导语 随着全球数字化趋势的加速&#xff0c;WhatsApp作为一种即时通讯工具&#xff0c;已经成为了连接全球用户的桥梁。 对于企业和营销人员来说&#xff0c;利用WhatsApp拓展全球业务是一种非常有效的策略&#xff0c;本文将为您揭示WhatsApp全球获客的秘密&#xff0c;…