最新资讯

  • 【深度学习项目】语义分割-FCN网络(原理、网络架构、基于Pytorch实现FCN网络)

【深度学习项目】语义分割-FCN网络(原理、网络架构、基于Pytorch实现FCN网络)

2025-04-28 00:38:00 0 阅读

文章目录

  • 介绍
    • 深度学习语义分割的关键特点
    • 主要架构和技术
    • 数据集和评价指标
    • 总结
  • FCN网络
    • FCN 的特点
    • FCN 的工作原理
    • FCN 的变体和发展
    • FCN 的网络结构
    • FCN 的实现(基于Pytorch)
      • 1. 环境配置
      • 2. 文件结构
      • 3. 预训练权重下载地址
      • 4. 数据集,本例程使用的是PASCAL VOC2012数据集
      • 5. 训练方法
      • 6. 注意事项
      • 7. Pytorch官方实现的FCN网络框架图
      • 8. 完整代码
        • 8.1 src文件目录代码
        • 8.2 train_utils文件目录代码
        • 8.3 根目录代码

个人主页:道友老李
欢迎加入社区:道友老李的学习社区

介绍

深度学习语义分割(Semantic Segmentation)是一种计算机视觉任务,它旨在将图像中的每个像素分类为预定义类别之一。与物体检测不同,后者通常只识别和定位图像中的目标对象边界框,语义分割要求对图像的每一个像素进行分类,以实现更精细的理解。这项技术在自动驾驶、医学影像分析、机器人视觉等领域有着广泛的应用。

深度学习语义分割的关键特点

  • 像素级分类:对于输入图像的每一个像素点,模型都需要预测其属于哪个类别。
  • 全局上下文理解:为了正确地分割复杂场景,模型需要考虑整个图像的内容及其上下文信息。
  • 多尺度处理:由于目标可能出现在不同的尺度上,有效的语义分割方法通常会处理多种分辨率下的特征。

主要架构和技术

  1. 全卷积网络 (FCN)

    • FCN是最早的端到端训练的语义分割模型之一,它移除了传统CNN中的全连接层,并用卷积层替代,从而能够接受任意大小的输入并输出相同空间维度的概率图。
  2. 跳跃连接 (Skip Connections)

    • 为了更好地保留原始图像的空间细节,一些模型引入了跳跃连接,即从编码器部分直接传递特征到解码器部分,这有助于恢复细粒度的结构信息。
  3. U-Net

    • U-Net是一个专为生物医学图像分割设计的网络架构,它使用了对称的收缩路径(下采样)和扩展路径(上采样),以及丰富的跳跃连接来捕捉局部和全局信息。
  4. DeepLab系列

    • DeepLab采用了空洞/膨胀卷积(Atrous Convolution)来增加感受野而不减少特征图分辨率,并通过多尺度推理和ASPP模块(Atrous Spatial Pyramid Pooling)增强了对不同尺度物体的捕捉能力。
  5. PSPNet (Pyramid Scene Parsing Network)

    • PSPNet利用金字塔池化机制收集不同尺度的上下文信息,然后将其融合用于最终的预测。
  6. RefineNet

    • RefineNet强调了高分辨率特征的重要性,并通过一系列细化单元逐步恢复细节,确保输出高质量的分割结果。
  7. HRNet (High-Resolution Network)

    • HRNet在整个网络中保持了高分辨率的表示,同时通过多尺度融合策略有效地整合了低分辨率但富含语义的信息。

数据集和评价指标

常用的语义分割数据集包括PASCAL VOC、COCO、Cityscapes等。这些数据集提供了标注好的图像,用于训练和评估模型性能。

评价语义分割模型的标准通常包括:

  • 像素准确率 (Pixel Accuracy):所有正确分类的像素占总像素的比例。
  • 平均交并比 (Mean Intersection over Union, mIoU):这是最常用的评价指标之一,计算每个类别的IoU(交集除以并集),然后取平均值。
  • 频率加权交并比 (Frequency Weighted IoU):考虑每个类别的出现频率,对mIoU进行加权。

总结

随着硬件性能的提升和算法的进步,深度学习语义分割已经取得了显著的进展。现代模型不仅能在速度上满足实时应用的需求,还能提供非常精确的分割结果。未来的研究可能会集中在提高模型效率、增强跨域泛化能力以及探索无监督或弱监督的学习方法等方面。

FCN网络

FCN(Fully Convolutional Networks,全卷积网络)是一种用于计算机视觉任务的神经网络架构,尤其擅长处理像素级别的分类问题,例如语义分割。FCN 的核心思想是将传统的 CNN(卷积神经网络)中的全连接层替换为卷积层,这样可以接受任意大小的输入图像,并输出同样大小的概率图,其中每个像素点对应于该位置属于某个类别的概率。

FCN 的特点

  1. 任意尺寸输入:由于没有全连接层的限制,FCN 可以处理任意尺寸的输入图像。
  2. 端到端训练:FCN 支持从原始像素到最终预测结果的端到端训练,不需要预先提取特征或者分阶段训练。
  3. 多尺度上下文信息:通过在不同层级使用跳跃结构(skip architecture),FCN 能够结合低层的精细空间信息和高层的语义信息,从而提升分割精度。

FCN 的工作原理

  • 编码器部分:通常基于一个预训练好的分类网络(如 VGG、ResNet 等),移除掉最后的全连接层。这个部分负责提取图像特征,随着网络深度增加,感受野也逐渐扩大,能够捕捉到更大范围内的上下文信息。

  • 解码器部分:这部分用来逐步恢复特征图的空间分辨率,直到与输入图像相同大小。这通常是通过上采样操作完成的,比如转置卷积(Deconvolution 或 Fractionally-strided convolution)。在这个过程中,可以加入来自编码器早期层的特征(即跳跃连接),来帮助保持细节。

  • 跳跃连接(Skip Connections):为了保留更多的位置信息,FCN 会将编码器中较早层的特征图与解码器中对应的层进行融合。这种做法有助于改善边界区域的分割效果。

FCN 的变体和发展

自 FCN 提出以来,出现了许多改进版本,包括但不限于:

  • U-Net:一种具有对称的编码器-解码器结构的网络,广泛应用于医学图像分割领域。
  • DeepLab 系列:通过引入空洞卷积(Atrous Convolution)等技术来增强模型捕捉多尺度信息的能力。
  • PSPNet (Pyramid Scene Parsing Network):利用金字塔池化模块获取全局上下文信息。
  • RefineNet:专注于细节恢复,采用多路径优化策略来传递所有分辨率的信息。

这些模型在不同的应用场景中都有所应用,并且根据特定的任务需求不断进化和发展。

首个端到端的针对像素级预测的全卷积网络

FCN 的网络结构



Conv的参数量:77512*4096=102760448




FCN 的实现(基于Pytorch)

该项目主要是来自pytorch官方torchvision模块中的源码
https://github.com/pytorch/vision/tree/main/torchvision/models/segmentation

1. 环境配置

  • Python3.6/3.7/3.8
  • Pytorch1.10
  • Ubuntu或Centos(Windows暂不支持多GPU训练)
  • 最好使用GPU训练
  • 详细环境配置见requirements.txt

2. 文件结构

  ├── src: 模型的backbone以及FCN的搭建
  ├── train_utils: 训练、验证以及多GPU训练相关模块
  ├── my_dataset.py: 自定义dataset用于读取VOC数据集
  ├── train.py: 以fcn_resnet50(这里使用了Dilated/Atrous Convolution)进行训练
  ├── train_multi_GPU.py: 针对使用多GPU的用户使用
  ├── predict.py: 简易的预测脚本,使用训练好的权重进行预测测试
  ├── validation.py: 利用训练好的权重验证/测试数据的mIoU等指标,并生成record_mAP.txt文件
  └── pascal_voc_classes.json: pascal_voc标签文件

3. 预训练权重下载地址

  • 注意:官方提供的预训练权重是在COCO上预训练得到的,训练时只针对和PASCAL VOC相同的类别进行了训练,所以类别数是21(包括背景)
  • fcn_resnet50: https://download.pytorch.org/models/fcn_resnet50_coco-1167a1af.pth
  • fcn_resnet101: https://download.pytorch.org/models/fcn_resnet101_coco-7ecb50ca.pth
  • 注意,下载的预训练权重记得要重命名,比如在train.py中读取的是fcn_resnet50_coco.pth文件,
    不是fcn_resnet50_coco-1167a1af.pth

4. 数据集,本例程使用的是PASCAL VOC2012数据集

  • Pascal VOC2012 train/val数据集下载地址:http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar

5. 训练方法

  • 确保提前准备好数据集
  • 确保提前下载好对应预训练模型权重
  • 若要使用单GPU或者CPU训练,直接使用train.py训练脚本
  • 若要使用多GPU训练,使用torchrun --nproc_per_node=8 train_multi_GPU.py指令,nproc_per_node参数为使用GPU数量
  • 如果想指定使用哪些GPU设备可在指令前加上CUDA_VISIBLE_DEVICES=0,3(例如我只要使用设备中的第1块和第4块GPU设备)
  • CUDA_VISIBLE_DEVICES=0,3 torchrun --nproc_per_node=2 train_multi_GPU.py

6. 注意事项

  • 在使用训练脚本时,注意要将’–data-path’(VOC_root)设置为自己存放’VOCdevkit’文件夹所在的根目录
  • 在使用预测脚本时,要将’weights_path’设置为你自己生成的权重路径。
  • 使用validation文件时,注意确保你的验证集或者测试集中必须包含每个类别的目标,并且使用时只需要修改’–num-classes’、‘–aux’、‘–data-path’和’–weights’即可,其他代码尽量不要改动

7. Pytorch官方实现的FCN网络框架图

8. 完整代码

8.1 src文件目录代码
  • init.py
from .fcn_model import fcn_resnet50, fcn_resnet101

  • backbone.py
import torch
import torch.nn as nn


def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
    """3x3 convolution with padding"""
    return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
                     padding=dilation, groups=groups, bias=False, dilation=dilation)


def conv1x1(in_planes, out_planes, stride=1):
    """1x1 convolution"""
    return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)


class Bottleneck(nn.Module):
    # Bottleneck in torchvision places the stride for downsampling at 3x3 convolution(self.conv2)
    # while original implementation places the stride at the first 1x1 convolution(self.conv1)
    # according to "Deep residual learning for image recognition"https://arxiv.org/abs/1512.03385.
    # This variant is also known as ResNet V1.5 and improves accuracy according to
    # https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch.

    expansion = 4

    def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
                 base_width=64, dilation=1, norm_layer=None):
        super(Bottleneck, self).__init__()
        if norm_layer is None:
            norm_layer = nn.BatchNorm2d
        width = int(planes * (base_width / 64.)) * groups
        # Both self.conv2 and self.downsample layers downsample the input when stride != 1
        self.conv1 = conv1x1(inplanes, width)
        self.bn1 = norm_layer(width)
        self.conv2 = conv3x3(width, width, stride, groups, dilation)
        self.bn2 = norm_layer(width)
        self.conv3 = conv1x1(width, planes * self.expansion)
        self.bn3 = norm_layer(planes * self.expansion)
        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample
        self.stride = stride

    def forward(self, x):
        identity = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        if self.downsample is not None:
            identity = self.downsample(x)

        out += identity
        out = self.relu(out)

        return out


class ResNet(nn.Module):

    def __init__(self, block, layers, num_classes=1000, zero_init_residual=False,
                 groups=1, width_per_group=64, replace_stride_with_dilation=None,
                 norm_layer=None):
        super(ResNet, self).__init__()
        if norm_layer is None:
            norm_layer = nn.BatchNorm2d
        self._norm_layer = norm_layer

        self.inplanes = 64
        self.dilation = 1
        if replace_stride_with_dilation is None:
            # each element in the tuple indicates if we should replace
            # the 2x2 stride with a dilated convolution instead
            replace_stride_with_dilation = [False, False, False]
        if len(replace_stride_with_dilation) != 3:
            raise ValueError("replace_stride_with_dilation should be None "
                             "or a 3-element tuple, got {}".format(replace_stride_with_dilation))
        self.groups = groups
        self.base_width = width_per_group
        self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3,
                               bias=False)
        self.bn1 = norm_layer(self.inplanes)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(block, 64, layers[0])
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2,
                                       dilate=replace_stride_with_dilation[0])
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2,
                                       dilate=replace_stride_with_dilation[1])
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2,
                                       dilate=replace_stride_with_dilation[2])
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512 * block.expansion, num_classes)

        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)

        # Zero-initialize the last BN in each residual branch,
        # so that the residual branch starts with zeros, and each residual block behaves like an identity.
        # This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
        if zero_init_residual:
            for m in self.modules():
                if isinstance(m, Bottleneck):
                    nn.init.constant_(m.bn3.weight, 0)

    def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
        norm_layer = self._norm_layer
        downsample = None
        previous_dilation = self.dilation
        if dilate:
            self.dilation *= stride
            stride = 1
        if stride != 1 or self.inplanes != planes * block.expansion:
            downsample = nn.Sequential(
                conv1x1(self.inplanes, planes * block.expansion, stride),
                norm_layer(planes * block.expansion),
            )

        layers = []
        layers.append(block(self.inplanes, planes, stride, downsample, self.groups,
                            self.base_width, previous_dilation, norm_layer))
        self.inplanes = planes * block.expansion
        for _ in range(1, blocks):
            layers.append(block(self.inplanes, planes, groups=self.groups,
                                base_width=self.base_width, dilation=self.dilation,
                                norm_layer=norm_layer))

        return nn.Sequential(*layers)

    def _forward_impl(self, x):
        # See note [TorchScript super()]
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x

    def forward(self, x):
        return self._forward_impl(x)


def _resnet(block, layers, **kwargs):
    model = ResNet(block, layers, **kwargs)
    return model


def resnet50(**kwargs):
    r"""ResNet-50 model from
    `"Deep Residual Learning for Image Recognition" `_

    Args:
        pretrained (bool): If True, returns a model pre-trained on ImageNet
        progress (bool): If True, displays a progress bar of the download to stderr
    """
    return _resnet(Bottleneck, [3, 4, 6, 3], **kwargs)


def resnet101(**kwargs):
    r"""ResNet-101 model from
    `"Deep Residual Learning for Image Recognition" `_

    Args:
        pretrained (bool): If True, returns a model pre-trained on ImageNet
        progress (bool): If True, displays a progress bar of the download to stderr
    """
    return _resnet(Bottleneck, [3, 4, 23, 3], **kwargs)

  • fcn_model.py
from collections import OrderedDict

from typing import Dict

import torch
from torch import nn, Tensor
from torch.nn import functional as F
from .backbone import resnet50, resnet101


class IntermediateLayerGetter(nn.ModuleDict):
    """
    Module wrapper that returns intermediate layers from a model

    It has a strong assumption that the modules have been registered
    into the model in the same order as they are used.
    This means that one should **not** reuse the same nn.Module
    twice in the forward if you want this to work.

    Additionally, it is only able to query submodules that are directly
    assigned to the model. So if `model` is passed, `model.feature1` can
    be returned, but not `model.feature1.layer2`.

    Args:
        model (nn.Module): model on which we will extract the features
        return_layers (Dict[name, new_name]): a dict containing the names
            of the modules for which the activations will be returned as
            the key of the dict, and the value of the dict is the name
            of the returned activation (which the user can specify).
    """
    _version = 2
    __annotations__ = {
        "return_layers": Dict[str, str],
    }

    def __init__(self, model: nn.Module, return_layers: Dict[str, str]) -> None:
        if not set(return_layers).issubset([name for name, _ in model.named_children()]):
            raise ValueError("return_layers are not present in model")
        orig_return_layers = return_layers
        return_layers = {str(k): str(v) for k, v in return_layers.items()}

        # 重新构建backbone,将没有使用到的模块全部删掉
        layers = OrderedDict()
        for name, module in model.named_children():
            layers[name] = module
            if name in return_layers:
                del return_layers[name]
            if not return_layers:
                break

        super(IntermediateLayerGetter, self).__init__(layers)
        self.return_layers = orig_return_layers

    def forward(self, x: Tensor) -> Dict[str, Tensor]:
        out = OrderedDict()
        for name, module in self.items():
            x = module(x)
            if name in self.return_layers:
                out_name = self.return_layers[name]
                out[out_name] = x
        return out


class FCN(nn.Module):
    """
    Implements a Fully-Convolutional Network for semantic segmentation.

    Args:
        backbone (nn.Module): the network used to compute the features for the model.
            The backbone should return an OrderedDict[Tensor], with the key being
            "out" for the last feature map used, and "aux" if an auxiliary classifier
            is used.
        classifier (nn.Module): module that takes the "out" element returned from
            the backbone and returns a dense prediction.
        aux_classifier (nn.Module, optional): auxiliary classifier used during training
    """
    __constants__ = ['aux_classifier']

    def __init__(self, backbone, classifier, aux_classifier=None):
        super(FCN, self).__init__()
        self.backbone = backbone
        self.classifier = classifier
        self.aux_classifier = aux_classifier

    def forward(self, x: Tensor) -> Dict[str, Tensor]:
        input_shape = x.shape[-2:]
        # contract: features is a dict of tensors
        features = self.backbone(x)

        result = OrderedDict()
        x = features["out"]
        x = self.classifier(x)
        # 原论文中虽然使用的是ConvTranspose2d,但权重是冻结的,所以就是一个bilinear插值
        x = F.interpolate(x, size=input_shape, mode='bilinear', align_corners=False)
        result["out"] = x

        if self.aux_classifier is not None:
            x = features["aux"]
            x = self.aux_classifier(x)
            # 原论文中虽然使用的是ConvTranspose2d,但权重是冻结的,所以就是一个bilinear插值
            x = F.interpolate(x, size=input_shape, mode='bilinear', align_corners=False)
            result["aux"] = x

        return result


class FCNHead(nn.Sequential):
    def __init__(self, in_channels, channels):
        inter_channels = in_channels // 4
        layers = [
            nn.Conv2d(in_channels, inter_channels, 3, padding=1, bias=False),
            nn.BatchNorm2d(inter_channels),
            nn.ReLU(),
            nn.Dropout(0.1),
            nn.Conv2d(inter_channels, channels, 1)
        ]

        super(FCNHead, self).__init__(*layers)


def fcn_resnet50(aux, num_classes=21, pretrain_backbone=False):
    # 'resnet50_imagenet': 'https://download.pytorch.org/models/resnet50-0676ba61.pth'
    # 'fcn_resnet50_coco': 'https://download.pytorch.org/models/fcn_resnet50_coco-1167a1af.pth'
    backbone = resnet50(replace_stride_with_dilation=[False, True, True])

    if pretrain_backbone:
        # 载入resnet50 backbone预训练权重
        backbone.load_state_dict(torch.load("resnet50.pth", map_location='cpu'))

    out_inplanes = 2048
    aux_inplanes = 1024

    return_layers = {'layer4': 'out'}
    if aux:
        return_layers['layer3'] = 'aux'
    backbone = IntermediateLayerGetter(backbone, return_layers=return_layers)

    aux_classifier = None
    # why using aux: https://github.com/pytorch/vision/issues/4292
    if aux:
        aux_classifier = FCNHead(aux_inplanes, num_classes)

    classifier = FCNHead(out_inplanes, num_classes)

    model = FCN(backbone, classifier, aux_classifier)

    return model


def fcn_resnet101(aux, num_classes=21, pretrain_backbone=False):
    # 'resnet101_imagenet': 'https://download.pytorch.org/models/resnet101-63fe2227.pth'
    # 'fcn_resnet101_coco': 'https://download.pytorch.org/models/fcn_resnet101_coco-7ecb50ca.pth'
    backbone = resnet101(replace_stride_with_dilation=[False, True, True])

    if pretrain_backbone:
        # 载入resnet101 backbone预训练权重
        backbone.load_state_dict(torch.load("resnet101.pth", map_location='cpu'))

    out_inplanes = 2048
    aux_inplanes = 1024

    return_layers = {'layer4': 'out'}
    if aux:
        return_layers['layer3'] = 'aux'
    backbone = IntermediateLayerGetter(backbone, return_layers=return_layers)

    aux_classifier = None
    # why using aux: https://github.com/pytorch/vision/issues/4292
    if aux:
        aux_classifier = FCNHead(aux_inplanes, num_classes)

    classifier = FCNHead(out_inplanes, num_classes)

    model = FCN(backbone, classifier, aux_classifier)

    return model

8.2 train_utils文件目录代码
  • init.py
from .train_and_eval import train_one_epoch, evaluate, create_lr_scheduler
from .distributed_utils import init_distributed_mode, save_on_master, mkdir

  • distributed_utils.py
from collections import defaultdict, deque
import datetime
import time
import torch
import torch.distributed as dist

import errno
import os


class SmoothedValue(object):
    """Track a series of values and provide access to smoothed values over a
    window or the global series average.
    """

    def __init__(self, window_size=20, fmt=None):
        if fmt is None:
            fmt = "{value:.4f} ({global_avg:.4f})"
        self.deque = deque(maxlen=window_size)
        self.total = 0.0
        self.count = 0
        self.fmt = fmt

    def update(self, value, n=1):
        self.deque.append(value)
        self.count += n
        self.total += value * n

    def synchronize_between_processes(self):
        """
        Warning: does not synchronize the deque!
        """
        if not is_dist_avail_and_initialized():
            return
        t = torch.tensor([self.count, self.total], dtype=torch.float64, device='cuda')
        dist.barrier()
        dist.all_reduce(t)
        t = t.tolist()
        self.count = int(t[0])
        self.total = t[1]

    @property
    def median(self):
        d = torch.tensor(list(self.deque))
        return d.median().item()

    @property
    def avg(self):
        d = torch.tensor(list(self.deque), dtype=torch.float32)
        return d.mean().item()

    @property
    def global_avg(self):
        return self.total / self.count

    @property
    def max(self):
        return max(self.deque)

    @property
    def value(self):
        return self.deque[-1]

    def __str__(self):
        return self.fmt.format(
            median=self.median,
            avg=self.avg,
            global_avg=self.global_avg,
            max=self.max,
            value=self.value)


class ConfusionMatrix(object):
    def __init__(self, num_classes):
        self.num_classes = num_classes
        self.mat = None

    def update(self, a, b):
        n = self.num_classes
        if self.mat is None:
            # 创建混淆矩阵
            self.mat = torch.zeros((n, n), dtype=torch.int64, device=a.device)
        with torch.no_grad():
            # 寻找GT中为目标的像素索引
            k = (a >= 0) & (a < n)
            # 统计像素真实类别a[k]被预测成类别b[k]的个数(这里的做法很巧妙)
            inds = n * a[k].to(torch.int64) + b[k]
            self.mat += torch.bincount(inds, minlength=n**2).reshape(n, n)

    def reset(self):
        if self.mat is not None:
            self.mat.zero_()

    def compute(self):
        h = self.mat.float()
        # 计算全局预测准确率(混淆矩阵的对角线为预测正确的个数)
        acc_global = torch.diag(h).sum() / h.sum()
        # 计算每个类别的准确率
        acc = torch.diag(h) / h.sum(1)
        # 计算每个类别预测与真实目标的iou
        iu = torch.diag(h) / (h.sum(1) + h.sum(0) - torch.diag(h))
        return acc_global, acc, iu

    def reduce_from_all_processes(self):
        if not torch.distributed.is_available():
            return
        if not torch.distributed.is_initialized():
            return
        torch.distributed.barrier()
        torch.distributed.all_reduce(self.mat)

    def __str__(self):
        acc_global, acc, iu = self.compute()
        return (
            'global correct: {:.1f}
'
            'average row correct: {}
'
            'IoU: {}
'
            'mean IoU: {:.1f}').format(
                acc_global.item() * 100,
                ['{:.1f}'.format(i) for i in (acc * 100).tolist()],
                ['{:.1f}'.format(i) for i in (iu * 100).tolist()],
                iu.mean().item() * 100)


class MetricLogger(object):
    def __init__(self, delimiter="	"):
        self.meters = defaultdict(SmoothedValue)
        self.delimiter = delimiter

    def update(self, **kwargs):
        for k, v in kwargs.items():
            if isinstance(v, torch.Tensor):
                v = v.item()
            assert isinstance(v, (float, int))
            self.meters[k].update(v)

    def __getattr__(self, attr):
        if attr in self.meters:
            return self.meters[attr]
        if attr in self.__dict__:
            return self.__dict__[attr]
        raise AttributeError("'{}' object has no attribute '{}'".format(
            type(self).__name__, attr))

    def __str__(self):
        loss_str = []
        for name, meter in self.meters.items():
            loss_str.append(
                "{}: {}".format(name, str(meter))
            )
        return self.delimiter.join(loss_str)

    def synchronize_between_processes(self):
        for meter in self.meters.values():
            meter.synchronize_between_processes()

    def add_meter(self, name, meter):
        self.meters[name] = meter

    def log_every(self, iterable, print_freq, header=None):
        i = 0
        if not header:
            header = ''
        start_time = time.time()
        end = time.time()
        iter_time = SmoothedValue(fmt='{avg:.4f}')
        data_time = SmoothedValue(fmt='{avg:.4f}')
        space_fmt = ':' + str(len(str(len(iterable)))) + 'd'
        if torch.cuda.is_available():
            log_msg = self.delimiter.join([
                header,
                '[{0' + space_fmt + '}/{1}]',
                'eta: {eta}',
                '{meters}',
                'time: {time}',
                'data: {data}',
                'max mem: {memory:.0f}'
            ])
        else:
            log_msg = self.delimiter.join([
                header,
                '[{0' + space_fmt + '}/{1}]',
                'eta: {eta}',
                '{meters}',
                'time: {time}',
                'data: {data}'
            ])
        MB = 1024.0 * 1024.0
        for obj in iterable:
            data_time.update(time.time() - end)
            yield obj
            iter_time.update(time.time() - end)
            if i % print_freq == 0:
                eta_seconds = iter_time.global_avg * (len(iterable) - i)
                eta_string = str(datetime.timedelta(seconds=int(eta_seconds)))
                if torch.cuda.is_available():
                    print(log_msg.format(
                        i, len(iterable), eta=eta_string,
                        meters=str(self),
                        time=str(iter_time), data=str(data_time),
                        memory=torch.cuda.max_memory_allocated() / MB))
                else:
                    print(log_msg.format(
                        i, len(iterable), eta=eta_string,
                        meters=str(self),
                        time=str(iter_time), data=str(data_time)))
            i += 1
            end = time.time()
        total_time = time.time() - start_time
        total_time_str = str(datetime.timedelta(seconds=int(total_time)))
        print('{} Total time: {}'.format(header, total_time_str))


def mkdir(path):
    try:
        os.makedirs(path)
    except OSError as e:
        if e.errno != errno.EEXIST:
            raise


def setup_for_distributed(is_master):
    """
    This function disables printing when not in master process
    """
    import builtins as __builtin__
    builtin_print = __builtin__.print

    def print(*args, **kwargs):
        force = kwargs.pop('force', False)
        if is_master or force:
            builtin_print(*args, **kwargs)

    __builtin__.print = print


def is_dist_avail_and_initialized():
    if not dist.is_available():
        return False
    if not dist.is_initialized():
        return False
    return True


def get_world_size():
    if not is_dist_avail_and_initialized():
        return 1
    return dist.get_world_size()


def get_rank():
    if not is_dist_avail_and_initialized():
        return 0
    return dist.get_rank()


def is_main_process():
    return get_rank() == 0


def save_on_master(*args, **kwargs):
    if is_main_process():
        torch.save(*args, **kwargs)


def init_distributed_mode(args):
    if 'RANK' in os.environ and 'WORLD_SIZE' in os.environ:
        args.rank = int(os.environ["RANK"])
        args.world_size = int(os.environ['WORLD_SIZE'])
        args.gpu = int(os.environ['LOCAL_RANK'])
    elif 'SLURM_PROCID' in os.environ:
        args.rank = int(os.environ['SLURM_PROCID'])
        args.gpu = args.rank % torch.cuda.device_count()
    elif hasattr(args, "rank"):
        pass
    else:
        print('Not using distributed mode')
        args.distributed = False
        return

    args.distributed = True

    torch.cuda.set_device(args.gpu)
    args.dist_backend = 'nccl'
    print('| distributed init (rank {}): {}'.format(
        args.rank, args.dist_url), flush=True)
    torch.distributed.init_process_group(backend=args.dist_backend, init_method=args.dist_url,
                                         world_size=args.world_size, rank=args.rank)
    setup_for_distributed(args.rank == 0)

  • train_and_eval.py
import torch
from torch import nn
import train_utils.distributed_utils as utils


def criterion(inputs, target):
    losses = {}
    for name, x in inputs.items():
        # 忽略target中值为255的像素,255的像素是目标边缘或者padding填充
        losses[name] = nn.functional.cross_entropy(x, target, ignore_index=255)

    if len(losses) == 1:
        return losses['out']

    return losses['out'] + 0.5 * losses['aux']


def evaluate(model, data_loader, device, num_classes):
    model.eval()
    confmat = utils.ConfusionMatrix(num_classes)
    metric_logger = utils.MetricLogger(delimiter="  ")
    header = 'Test:'
    with torch.no_grad():
        for image, target in metric_logger.log_every(data_loader, 100, header):
            image, target = image.to(device), target.to(device)
            output = model(image)
            output = output['out']

            confmat.update(target.flatten(), output.argmax(1).flatten())

        confmat.reduce_from_all_processes()

    return confmat


def train_one_epoch(model, optimizer, data_loader, device, epoch, lr_scheduler, print_freq=10, scaler=None):
    model.train()
    metric_logger = utils.MetricLogger(delimiter="  ")
    metric_logger.add_meter('lr', utils.SmoothedValue(window_size=1, fmt='{value:.6f}'))
    header = 'Epoch: [{}]'.format(epoch)

    for image, target in metric_logger.log_every(data_loader, print_freq, header):
        image, target = image.to(device), target.to(device)
        with torch.cuda.amp.autocast(enabled=scaler is not None):
            output = model(image)
            loss = criterion(output, target)

        optimizer.zero_grad()
        if scaler is not None:
            scaler.scale(loss).backward()
            scaler.step(optimizer)
            scaler.update()
        else:
            loss.backward()
            optimizer.step()

        lr_scheduler.step()

        lr = optimizer.param_groups[0]["lr"]
        metric_logger.update(loss=loss.item(), lr=lr)

    return metric_logger.meters["loss"].global_avg, lr


def create_lr_scheduler(optimizer,
                        num_step: int,
                        epochs: int,
                        warmup=True,
                        warmup_epochs=1,
                        warmup_factor=1e-3):
    assert num_step > 0 and epochs > 0
    if warmup is False:
        warmup_epochs = 0

    def f(x):
        """
        根据step数返回一个学习率倍率因子,
        注意在训练开始之前,pytorch会提前调用一次lr_scheduler.step()方法
        """
        if warmup is True and x <= (warmup_epochs * num_step):
            alpha = float(x) / (warmup_epochs * num_step)
            # warmup过程中lr倍率因子从warmup_factor -> 1
            return warmup_factor * (1 - alpha) + alpha
        else:
            # warmup后lr倍率因子从1 -> 0
            # 参考deeplab_v2: Learning rate policy
            return (1 - (x - warmup_epochs * num_step) / ((epochs - warmup_epochs) * num_step)) ** 0.9

    return torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=f)

8.3 根目录代码
  • pascal_voc_classes.json
{
    "aeroplane": 1,
    "bicycle": 2,
    "bird": 3,
    "boat": 4,
    "bottle": 5,
    "bus": 6,
    "car": 7,
    "cat": 8,
    "chair": 9,
    "cow": 10,
    "diningtable": 11,
    "dog": 12,
    "horse": 13,
    "motorbike": 14,
    "person": 15,
    "pottedplant": 16,
    "sheep": 17,
    "sofa": 18,
    "train": 19,
    "tvmonitor": 20
}
  • my_dataset.py
import os

import torch.utils.data as data
from PIL import Image


class VOCSegmentation(data.Dataset):
    def __init__(self, voc_root, year="2012", transforms=None, txt_name: str = "train.txt"):
        super(VOCSegmentation, self).__init__()
        assert year in ["2007", "2012"], "year must be in ['2007', '2012']"
        root = os.path.join(voc_root, "VOCdevkit", f"VOC{year}")
        assert os.path.exists(root), "path '{}' does not exist.".format(root)
        image_dir = os.path.join(root, 'JPEGImages')
        mask_dir = os.path.join(root, 'SegmentationClass')

        txt_path = os.path.join(root, "ImageSets", "Segmentation", txt_name)
        assert os.path.exists(txt_path), "file '{}' does not exist.".format(txt_path)
        with open(os.path.join(txt_path), "r") as f:
            file_names = [x.strip() for x in f.readlines() if len(x.strip()) > 0]

        self.images = [os.path.join(image_dir, x + ".jpg") for x in file_names]
        self.masks = [os.path.join(mask_dir, x + ".png") for x in file_names]
        assert (len(self.images) == len(self.masks))
        self.transforms = transforms

    def __getitem__(self, index):
        """
        Args:
            index (int): Index

        Returns:
            tuple: (image, target) where target is the image segmentation.
        """
        img = Image.open(self.images[index]).convert('RGB')
        target = Image.open(self.masks[index])

        if self.transforms is not None:
            img, target = self.transforms(img, target)

        return img, target

    def __len__(self):
        return len(self.images)

    @staticmethod
    def collate_fn(batch):
        images, targets = list(zip(*batch))
        batched_imgs = cat_list(images, fill_value=0)
        batched_targets = cat_list(targets, fill_value=255)
        return batched_imgs, batched_targets


def cat_list(images, fill_value=0):
    # 计算该batch数据中,channel, h, w的最大值
    max_size = tuple(max(s) for s in zip(*[img.shape for img in images]))
    batch_shape = (len(images),) + max_size
    batched_imgs = images[0].new(*batch_shape).fill_(fill_value)
    for img, pad_img in zip(images, batched_imgs):
        pad_img[..., :img.shape[-2], :img.shape[-1]].copy_(img)
    return batched_imgs


# dataset = VOCSegmentation(voc_root="/data/", transforms=get_transform(train=True))
# d1 = dataset[0]
# print(d1)

  • validation.py
import os
import torch

from src import fcn_resnet50
from train_utils import evaluate
from my_dataset import VOCSegmentation
import transforms as T


class SegmentationPresetEval:
    def __init__(self, base_size, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)):
        self.transforms = T.Compose([
            T.RandomResize(base_size, base_size),
            T.ToTensor(),
            T.Normalize(mean=mean, std=std),
        ])

    def __call__(self, img, target):
        return self.transforms(img, target)


def main(args):
    device = torch.device(args.device if torch.cuda.is_available() else "cpu")
    assert os.path.exists(args.weights), f"weights {args.weights} not found."

    # segmentation nun_classes + background
    num_classes = args.num_classes + 1

    # VOCdevkit -> VOC2012 -> ImageSets -> Segmentation -> val.txt
    val_dataset = VOCSegmentation(args.data_path,
                                  year="2012",
                                  transforms=SegmentationPresetEval(520),
                                  txt_name="val.txt")

    num_workers = 8
    val_loader = torch.utils.data.DataLoader(val_dataset,
                                             batch_size=1,
                                             num_workers=num_workers,
                                             pin_memory=True,
                                             collate_fn=val_dataset.collate_fn)

    model = fcn_resnet50(aux=args.aux, num_classes=num_classes)
    model.load_state_dict(torch.load(args.weights, map_location=device)['model'])
    model.to(device)

    confmat = evaluate(model, val_loader, device=device, num_classes=num_classes)
    print(confmat)


def parse_args():
    import argparse
    parser = argparse.ArgumentParser(description="pytorch fcn training")

    parser.add_argument("--data-path", default="/data/", help="VOCdevkit root")
    parser.add_argument("--weights", default="./save_weights/model_29.pth")
    parser.add_argument("--num-classes", default=20, type=int)
    parser.add_argument("--aux", default=True, type=bool, help="auxilier loss")
    parser.add_argument("--device", default="cuda", help="training device")
    parser.add_argument('--print-freq', default=10, type=int, help='print frequency')

    args = parser.parse_args()

    return args


if __name__ == '__main__':
    args = parse_args()

    if not os.path.exists("./save_weights"):
        os.mkdir("./save_weights")

    main(args)

  • transforms.py
import numpy as np
import random

import torch
from torchvision import transforms as T
from torchvision.transforms import functional as F


def pad_if_smaller(img, size, fill=0):
    # 如果图像最小边长小于给定size,则用数值fill进行padding
    min_size = min(img.size)
    if min_size < size:
        ow, oh = img.size
        padh = size - oh if oh < size else 0
        padw = size - ow if ow < size else 0
        img = F.pad(img, (0, 0, padw, padh), fill=fill)
    return img


class Compose(object):
    def __init__(self, transforms):
        self.transforms = transforms

    def __call__(self, image, target):
        for t in self.transforms:
            image, target = t(image, target)
        return image, target


class RandomResize(object):
    def __init__(self, min_size, max_size=None):
        self.min_size = min_size
        if max_size is None:
            max_size = min_size
        self.max_size = max_size

    def __call__(self, image, target):
        size = random.randint(self.min_size, self.max_size)
        # 这里size传入的是int类型,所以是将图像的最小边长缩放到size大小
        image = F.resize(image, size)
        # 这里的interpolation注意下,在torchvision(0.9.0)以后才有InterpolationMode.NEAREST
        # 如果是之前的版本需要使用PIL.Image.NEAREST
        target = F.resize(target, size, interpolation=T.InterpolationMode.NEAREST)
        return image, target


class RandomHorizontalFlip(object):
    def __init__(self, flip_prob):
        self.flip_prob = flip_prob

    def __call__(self, image, target):
        if random.random() < self.flip_prob:
            image = F.hflip(image)
            target = F.hflip(target)
        return image, target


class RandomCrop(object):
    def __init__(self, size):
        self.size = size

    def __call__(self, image, target):
        image = pad_if_smaller(image, self.size)
        target = pad_if_smaller(target, self.size, fill=255)
        crop_params = T.RandomCrop.get_params(image, (self.size, self.size))
        image = F.crop(image, *crop_params)
        target = F.crop(target, *crop_params)
        return image, target


class CenterCrop(object):
    def __init__(self, size):
        self.size = size

    def __call__(self, image, target):
        image = F.center_crop(image, self.size)
        target = F.center_crop(target, self.size)
        return image, target


class ToTensor(object):
    def __call__(self, image, target):
        image = F.to_tensor(image)
        target = torch.as_tensor(np.array(target), dtype=torch.int64)
        return image, target


class Normalize(object):
    def __init__(self, mean, std):
        self.mean = mean
        self.std = std

    def __call__(self, image, target):
        image = F.normalize(image, mean=self.mean, std=self.std)
        return image, target

  • train.py
import os
import time
import datetime

import torch

from src import fcn_resnet50
from train_utils import train_one_epoch, evaluate, create_lr_scheduler
from my_dataset import VOCSegmentation
import transforms as T


class SegmentationPresetTrain:
    def __init__(self, base_size, crop_size, hflip_prob=0.5, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)):
        min_size = int(0.5 * base_size)
        max_size = int(2.0 * base_size)

        trans = [T.RandomResize(min_size, max_size)]
        if hflip_prob > 0:
            trans.append(T.RandomHorizontalFlip(hflip_prob))
        trans.extend([
            T.RandomCrop(crop_size),
            T.ToTensor(),
            T.Normalize(mean=mean, std=std),
        ])
        self.transforms = T.Compose(trans)

    def __call__(self, img, target):
        return self.transforms(img, target)


class SegmentationPresetEval:
    def __init__(self, base_size, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)):
        self.transforms = T.Compose([
            T.RandomResize(base_size, base_size),
            T.ToTensor(),
            T.Normalize(mean=mean, std=std),
        ])

    def __call__(self, img, target):
        return self.transforms(img, target)


def get_transform(train):
    base_size = 520
    crop_size = 480

    return SegmentationPresetTrain(base_size, crop_size) if train else SegmentationPresetEval(base_size)


def create_model(aux, num_classes, pretrain=True):
    model = fcn_resnet50(aux=aux, num_classes=num_classes)

    if pretrain:
        weights_dict = torch.load("./src/fcn_resnet50_coco.pth", map_location='cpu')

        if num_classes != 21:
            # 官方提供的预训练权重是21类(包括背景)
            # 如果训练自己的数据集,将和类别相关的权重删除,防止权重shape不一致报错
            for k in list(weights_dict.keys()):
                if "classifier.4" in k:
                    del weights_dict[k]

        missing_keys, unexpected_keys = model.load_state_dict(weights_dict, strict=False)
        if len(missing_keys) != 0 or len(unexpected_keys) != 0:
            print("missing_keys: ", missing_keys)
            print("unexpected_keys: ", unexpected_keys)

    return model


def main(args):
    device = torch.device(args.device if torch.cuda.is_available() else "cpu")
    batch_size = args.batch_size
    # segmentation nun_classes + background
    num_classes = args.num_classes + 1

    # 用来保存训练以及验证过程中信息
    results_file = "results{}.txt".format(datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))

    # VOCdevkit -> VOC2012 -> ImageSets -> Segmentation -> train.txt
    train_dataset = VOCSegmentation(args.data_path,
                                    year="2012",
                                    transforms=get_transform(train=True),
                                    txt_name="train.txt")

    # VOCdevkit -> VOC2012 -> ImageSets -> Segmentation -> val.txt
    val_dataset = VOCSegmentation(args.data_path,
                                  year="2012",
                                  transforms=get_transform(train=False),
                                  txt_name="val.txt")

    num_workers = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])
    train_loader = torch.utils.data.DataLoader(train_dataset,
                                               batch_size=batch_size,
                                               num_workers=num_workers,
                                               shuffle=True,
                                               pin_memory=True,
                                               collate_fn=train_dataset.collate_fn)

    val_loader = torch.utils.data.DataLoader(val_dataset,
                                             batch_size=1,
                                             num_workers=num_workers,
                                             pin_memory=True,
                                             collate_fn=val_dataset.collate_fn)

    model = create_model(aux=args.aux, num_classes=num_classes)
    model.to(device)

    params_to_optimize = [
        {"params": [p for p in model.backbone.parameters() if p.requires_grad]},
        {"params": [p for p in model.classifier.parameters() if p.requires_grad]}
    ]

    if args.aux:
        params = [p for p in model.aux_classifier.parameters() if p.requires_grad]
        params_to_optimize.append({"params": params, "lr": args.lr * 10})

    optimizer = torch.optim.SGD(
        params_to_optimize,
        lr=args.lr, momentum=args.momentum, weight_decay=args.weight_decay
    )

    scaler = torch.cuda.amp.GradScaler() if args.amp else None

    # 创建学习率更新策略,这里是每个step更新一次(不是每个epoch)
    lr_scheduler = create_lr_scheduler(optimizer, len(train_loader), args.epochs, warmup=True)

    if args.resume:
        checkpoint = torch.load(args.resume, map_location='cpu')
        model.load_state_dict(checkpoint['model'])
        optimizer.load_state_dict(checkpoint['optimizer'])
        lr_scheduler.load_state_dict(checkpoint['lr_scheduler'])
        args.start_epoch = checkpoint['epoch'] + 1
        if args.amp:
            scaler.load_state_dict(checkpoint["scaler"])

    start_time = time.time()
    for epoch in range(args.start_epoch, args.epochs):
        mean_loss, lr = train_one_epoch(model, optimizer, train_loader, device, epoch,
                                        lr_scheduler=lr_scheduler, print_freq=args.print_freq, scaler=scaler)

        confmat = evaluate(model, val_loader, device=device, num_classes=num_classes)
        val_info = str(confmat)
        print(val_info)
        # write into txt
        with open(results_file, "a") as f:
            # 记录每个epoch对应的train_loss、lr以及验证集各指标
            train_info = f"[epoch: {epoch}]
" 
                         f"train_loss: {mean_loss:.4f}
" 
                         f"lr: {lr:.6f}
"
            f.write(train_info + val_info + "

")

        save_file = {"model": model.state_dict(),
                     "optimizer": optimizer.state_dict(),
                     "lr_scheduler": lr_scheduler.state_dict(),
                     "epoch": epoch,
                     "args": args}
        if args.amp:
            save_file["scaler"] = scaler.state_dict()
        torch.save(save_file, "save_weights/model_{}.pth".format(epoch))

    total_time = time.time() - start_time
    total_time_str = str(datetime.timedelta(seconds=int(total_time)))
    print("training time {}".format(total_time_str))


def parse_args():
    import argparse
    parser = argparse.ArgumentParser(description="pytorch fcn training")

    parser.add_argument("--data-path", default="/data/", help="VOCdevkit root")
    parser.add_argument("--num-classes", default=20, type=int)
    parser.add_argument("--aux", default=True, type=bool, help="auxilier loss")
    parser.add_argument("--device", default="cuda", help="training device")
    parser.add_argument("-b", "--batch-size", default=4, type=int)
    parser.add_argument("--epochs", default=30, type=int, metavar="N",
                        help="number of total epochs to train")

    parser.add_argument('--lr', default=0.0001, type=float, help='initial learning rate')
    parser.add_argument('--momentum', default=0.9, type=float, metavar='M',
                        help='momentum')
    parser.add_argument('--wd', '--weight-decay', default=1e-4, type=float,
                        metavar='W', help='weight decay (default: 1e-4)',
                        dest='weight_decay')
    parser.add_argument('--print-freq', default=10, type=int, help='print frequency')
    parser.add_argument('--resume', default='', help='resume from checkpoint')
    parser.add_argument('--start-epoch', default=0, type=int, metavar='N',
                        help='start epoch')
    # Mixed precision training parameters
    parser.add_argument("--amp", default=False, type=bool,
                        help="Use torch.cuda.amp for mixed precision training")

    args = parser.parse_args()

    return args


if __name__ == '__main__':
    args = parse_args()

    if not os.path.exists("./save_weights"):
        os.mkdir("./save_weights")

    main(args)

  • train_multi_GPU.py
import time
import os
import datetime

import torch

from src import fcn_resnet50
from train_utils import train_one_epoch, evaluate, create_lr_scheduler, init_distributed_mode, save_on_master, mkdir
from my_dataset import VOCSegmentation
import transforms as T


class SegmentationPresetTrain:
    def __init__(self, base_size, crop_size, hflip_prob=0.5, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)):
        min_size = int(0.5 * base_size)
        max_size = int(2.0 * base_size)

        trans = [T.RandomResize(min_size, max_size)]
        if hflip_prob > 0:
            trans.append(T.RandomHorizontalFlip(hflip_prob))
        trans.extend([
            T.RandomCrop(crop_size),
            T.ToTensor(),
            T.Normalize(mean=mean, std=std),
        ])
        self.transforms = T.Compose(trans)

    def __call__(self, img, target):
        return self.transforms(img, target)


class SegmentationPresetEval:
    def __init__(self, base_size, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)):
        self.transforms = T.Compose([
            T.RandomResize(base_size, base_size),
            T.ToTensor(),
            T.Normalize(mean=mean, std=std),
        ])

    def __call__(self, img, target):
        return self.transforms(img, target)


def get_transform(train):
    base_size = 520
    crop_size = 480

    return SegmentationPresetTrain(base_size, crop_size) if train else SegmentationPresetEval(base_size)


def create_model(aux, num_classes):
    model = fcn_resnet50(aux=aux, num_classes=num_classes)
    weights_dict = torch.load("./fcn_resnet50_coco.pth", map_location='cpu')

    if num_classes != 21:
        # 官方提供的预训练权重是21类(包括背景)
        # 如果训练自己的数据集,将和类别相关的权重删除,防止权重shape不一致报错
        for k in list(weights_dict.keys()):
            if "classifier.4" in k:
                del weights_dict[k]

    missing_keys, unexpected_keys = model.load_state_dict(weights_dict, strict=False)
    if len(missing_keys) != 0 or len(unexpected_keys) != 0:
        print("missing_keys: ", missing_keys)
        print("unexpected_keys: ", unexpected_keys)

    return model


def main(args):
    init_distributed_mode(args)
    print(args)

    device = torch.device(args.device)
    # segmentation nun_classes + background
    num_classes = args.num_classes + 1

    # 用来保存coco_info的文件
    results_file = "results{}.txt".format(datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))

    VOC_root = args.data_path
    # check voc root
    if os.path.exists(os.path.join(VOC_root, "VOCdevkit")) is False:
        raise FileNotFoundError("VOCdevkit dose not in path:'{}'.".format(VOC_root))

    # load train data set
    # VOCdevkit -> VOC2012 -> ImageSets -> Segmentation -> train.txt
    train_dataset = VOCSegmentation(args.data_path,
                                    year="2012",
                                    transforms=get_transform(train=True),
                                    txt_name="train.txt")
    # load validation data set
    # VOCdevkit -> VOC2012 -> ImageSets -> Segmentation -> val.txt
    val_dataset = VOCSegmentation(args.data_path,
                                  year="2012",
                                  transforms=get_transform(train=False),
                                  txt_name="val.txt")

    print("Creating data loaders")
    if args.distributed:
        train_sampler = torch.utils.data.distributed.DistributedSampler(train_dataset)
        test_sampler = torch.utils.data.distributed.DistributedSampler(val_dataset)
    else:
        train_sampler = torch.utils.data.RandomSampler(train_dataset)
        test_sampler = torch.utils.data.SequentialSampler(val_dataset)

    train_data_loader = torch.utils.data.DataLoader(
        train_dataset, batch_size=args.batch_size,
        sampler=train_sampler, num_workers=args.workers,
        collate_fn=train_dataset.collate_fn, drop_last=True)

    val_data_loader = torch.utils.data.DataLoader(
        val_dataset, batch_size=1,
        sampler=test_sampler, num_workers=args.workers,
        collate_fn=train_dataset.collate_fn)

    print("Creating model")
    # create model num_classes equal background + 20 classes
    model = create_model(aux=args.aux, num_classes=num_classes)
    model.to(device)

    if args.sync_bn:
        model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model)

    model_without_ddp = model
    if args.distributed:
        model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.gpu])
        model_without_ddp = model.module

    params_to_optimize = [
        {"params": [p for p in model_without_ddp.backbone.parameters() if p.requires_grad]},
        {"params": [p for p in model_without_ddp.classifier.parameters() if p.requires_grad]},
    ]
    if args.aux:
        params = [p for p in model_without_ddp.aux_classifier.parameters() if p.requires_grad]
        params_to_optimize.append({"params": params, "lr": args.lr * 10})
    optimizer = torch.optim.SGD(
        params_to_optimize,
        lr=args.lr, momentum=args.momentum, weight_decay=args.weight_decay)

    scaler = torch.cuda.amp.GradScaler() if args.amp else None

    # 创建学习率更新策略,这里是每个step更新一次(不是每个epoch)
    lr_scheduler = create_lr_scheduler(optimizer, len(train_data_loader), args.epochs, warmup=True)

    # 如果传入resume参数,即上次训练的权重地址,则接着上次的参数训练
    if args.resume:
        # If map_location is missing, torch.load will first load the module to CPU
        # and then copy each parameter to where it was saved,
        # which would result in all processes on the same machine using the same set of devices.
        checkpoint = torch.load(args.resume, map_location='cpu')  # 读取之前保存的权重文件(包括优化器以及学习率策略)
        model_without_ddp.load_state_dict(checkpoint['model'])
        optimizer.load_state_dict(checkpoint['optimizer'])
        lr_scheduler.load_state_dict(checkpoint['lr_scheduler'])
        args.start_epoch = checkpoint['epoch'] + 1
        if args.amp:
            scaler.load_state_dict(checkpoint["scaler"])

    if args.test_only:
        confmat = evaluate(model, val_data_loader, device=device, num_classes=num_classes)
        val_info = str(confmat)
        print(val_info)
        return

    print("Start training")
    start_time = time.time()
    for epoch in range(args.start_epoch, args.epochs):
        if args.distributed:
            train_sampler.set_epoch(epoch)
        mean_loss, lr = train_one_epoch(model, optimizer, train_data_loader, device, epoch,
                                        lr_scheduler=lr_scheduler, print_freq=args.print_freq, scaler=scaler)

        confmat = evaluate(model, val_data_loader, device=device, num_classes=num_classes)
        val_info = str(confmat)
        print(val_info)

        # 只在主进程上进行写操作
        if args.rank in [-1, 0]:
            # write into txt
            with open(results_file, "a") as f:
                # 记录每个epoch对应的train_loss、lr以及验证集各指标
                train_info = f"[epoch: {epoch}]
" 
                             f"train_loss: {mean_loss:.4f}
" 
                             f"lr: {lr:.6f}
"
                f.write(train_info + val_info + "

")

        if args.output_dir:
            # 只在主节点上执行保存权重操作
            save_file = {'model': model_without_ddp.state_dict(),
                         'optimizer': optimizer.state_dict(),
                         'lr_scheduler': lr_scheduler.state_dict(),
                         'args': args,
                         'epoch': epoch}
            if args.amp:
                save_file["scaler"] = scaler.state_dict()
            save_on_master(save_file,
                           os.path.join(args.output_dir, 'model_{}.pth'.format(epoch)))

    total_time = time.time() - start_time
    total_time_str = str(datetime.timedelta(seconds=int(total_time)))
    print('Training time {}'.format(total_time_str))


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(
        description=__doc__)

    # 训练文件的根目录(VOCdevkit)
    parser.add_argument('--data-path', default='/data/', help='dataset')
    # 训练设备类型
    parser.add_argument('--device', default='cuda', help='device')
    # 检测目标类别数(不包含背景)
    parser.add_argument('--num-classes', default=20, type=int, help='num_classes')
    # 每块GPU上的batch_size
    parser.add_argument('-b', '--batch-size', default=4, type=int,
                        help='images per gpu, the total batch size is $NGPU x batch_size')
    parser.add_argument("--aux", default=True, type=bool, help="auxilier loss")
    # 指定接着从哪个epoch数开始训练
    parser.add_argument('--start_epoch', default=0, type=int, help='start epoch')
    # 训练的总epoch数
    parser.add_argument('--epochs', default=20, type=int, metavar='N',
                        help='number of total epochs to run')
    # 是否使用同步BN(在多个GPU之间同步),默认不开启,开启后训练速度会变慢
    parser.add_argument('--sync_bn', type=bool, default=False, help='whether using SyncBatchNorm')
    # 数据加载以及预处理的线程数
    parser.add_argument('-j', '--workers', default=4, type=int, metavar='N',
                        help='number of data loading workers (default: 4)')
    # 训练学习率,这里默认设置成0.0001,如果效果不好可以尝试加大学习率
    parser.add_argument('--lr', default=0.0001, type=float,
                        help='initial learning rate')
    # SGD的momentum参数
    parser.add_argument('--momentum', default=0.9, type=float, metavar='M',
                        help='momentum')
    # SGD的weight_decay参数
    parser.add_argument('--wd', '--weight-decay', default=1e-4, type=float,
                        metavar='W', help='weight decay (default: 1e-4)',
                        dest='weight_decay')
    # 训练过程打印信息的频率
    parser.add_argument('--print-freq', default=20, type=int, help='print frequency')
    # 文件保存地址
    parser.add_argument('--output-dir', default='./multi_train', help='path where to save')
    # 基于上次的训练结果接着训练
    parser.add_argument('--resume', default='', help='resume from checkpoint')
    # 不训练,仅测试
    parser.add_argument(
        "--test-only",
        dest="test_only",
        help="Only test the model",
        action="store_true",
    )

    # 分布式进程数
    parser.add_argument('--world-size', default=1, type=int,
                        help='number of distributed processes')
    parser.add_argument('--dist-url', default='env://', help='url used to set up distributed training')
    # Mixed precision training parameters
    parser.add_argument("--amp", default=False, type=bool,
                        help="Use torch.cuda.amp for mixed precision training")

    args = parser.parse_args()

    # 如果指定了保存文件地址,检查文件夹是否存在,若不存在,则创建
    if args.output_dir:
        mkdir(args.output_dir)

    main(args)

  • predict.py
import os
import time
import json

import torch
from torchvision import transforms
import numpy as np
from PIL import Image

from src import fcn_resnet50


def time_synchronized():
    torch.cuda.synchronize() if torch.cuda.is_available() else None
    return time.time()


def main():
    aux = False  # inference time not need aux_classifier
    classes = 20
    weights_path = "./save_weights/model_2.pth"
    img_path = "./test.jpeg"
    palette_path = "./palette.json"
    assert os.path.exists(weights_path), f"weights {weights_path} not found."
    assert os.path.exists(img_path), f"image {img_path} not found."
    assert os.path.exists(palette_path), f"palette {palette_path} not found."
    with open(palette_path, "rb") as f:
        pallette_dict = json.load(f)
        pallette = []
        for v in pallette_dict.values():
            pallette += v

    # get devices
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    print("using {} device.".format(device))

    # create model
    model = fcn_resnet50(aux=aux, num_classes=classes+1)

    # delete weights about aux_classifier
    weights_dict = torch.load(weights_path, map_location='cpu')['model']
    for k in list(weights_dict.keys()):
        if "aux" in k:
            del weights_dict[k]

    # load weights
    model.load_state_dict(weights_dict)
    model.to(device)

    # load image
    original_img = Image.open(img_path)

    # from pil image to tensor and normalize
    data_transform = transforms.Compose([transforms.Resize(520),
                                         transforms.ToTensor(),
                                         transforms.Normalize(mean=(0.485, 0.456, 0.406),
                                                              std=(0.229, 0.224, 0.225))])
    img = data_transform(original_img)
    # expand batch dimension
    img = torch.unsqueeze(img, dim=0)

    model.eval()  # 进入验证模式
    with torch.no_grad():
        # init model
        img_height, img_width = img.shape[-2:]
        init_img = torch.zeros((1, 3, img_height, img_width), device=device)
        model(init_img)

        t_start = time_synchronized()
        output = model(img.to(device))
        t_end = time_synchronized()
        print("inference+NMS time: {}".format(t_end - t_start))

        prediction = output['out'].argmax(1).squeeze(0)
        prediction = prediction.to("cpu").numpy().astype(np.uint8)
        mask = Image.fromarray(prediction)
        mask.putpalette(pallette)
        mask.save("test_result.png")


if __name__ == '__main__':
    main()

测试图片:

预测结果:

本文地址:https://www.vps345.com/4087.html

搜索文章

Tags

PV计算 带宽计算 流量带宽 服务器带宽 上行带宽 上行速率 什么是上行带宽? CC攻击 攻击怎么办 流量攻击 DDOS攻击 服务器被攻击怎么办 源IP 服务器 linux 运维 游戏 云计算 ssh deepseek Ollama 模型联网 API CherryStudio 数据库 centos oracle 关系型 安全 分布式 llama 算法 opencv 自然语言处理 神经网络 语言模型 javascript 前端 chrome edge 进程 操作系统 进程控制 Ubuntu python MCP 阿里云 网络 网络安全 网络协议 ubuntu harmonyos 华为 开发语言 typescript 计算机网络 Dell R750XS 银河麒麟 kylin v10 麒麟 v10 spring boot websocket docker 实时音视频 adb nginx 监控 自动化运维 pycharm 深度学习 conda pillow 笔记 C 环境变量 进程地址空间 django fastapi flask web3.py gitlab numpy node.js json html5 firefox spring java RTSP xop RTP RTSPServer 推流 视频 kvm 无桌面 命令行 react.js 前端面试题 持续部署 android c++ c语言 zotero WebDAV 同步失败 代理模式 科技 ai 人工智能 个人开发 IIS .net core Hosting Bundle .NET Framework vs2022 经验分享 php 自动化 蓝耘科技 元生代平台工作流 ComfyUI ollama llm nuxt3 vue3 sql KingBase vim vscode uni-app AI 爬虫 数据集 宝塔面板 同步 备份 建站 tcp/ip 安全威胁分析 vscode 1.86 重启 排查 系统重启 日志 原因 机器学习 mcp mcp-proxy mcp-inspector fastapi-mcp agent sse 豆瓣 追剧助手 迅雷 nas 微信 内存 macos YOLO 目标检测 计算机视觉 LDAP https jvm kylin 面试 后端 架构 aws googlecloud 服务器繁忙 备选 网站 api 调用 示例 postman mock mock server 模拟服务器 mock服务器 Postman内置变量 Postman随机数据 网络结构图 tomcat maven intellij idea 腾讯云 filezilla 无法连接服务器 连接被服务器拒绝 vsftpd 331/530 qt http IIS服务器 IIS性能 日志监控 golang MQTT mosquitto 消息队列 vue.js 智能路由器 外网访问 内网穿透 端口映射 word图片自动上传 word一键转存 复制word图片 复制word图文 复制word公式 粘贴word图文 粘贴word公式 sqlite 华为认证 网络工程师 交换机 开源 openssl 密码学 spring cloud intellij-idea kafka hibernate shell Cursor sqlserver 统信 国产操作系统 虚拟机安装 游戏程序 windows ffmpeg 音视频 ftp github git 远程工作 pygame 小游戏 五子棋 android studio 容器 apache 孤岛惊魂4 Docker Compose docker compose docker-compose kubernetes 学习方法 程序人生 udp 僵尸进程 博客 编辑器 microsoft rust ssl DeepSeek-R1 API接口 Headless Linux 向日葵 远程登录 telnet pdf 多线程服务器 Linux网络编程 live555 rtsp rtp jenkins 云原生 ci/cd visualstudio 银河麒麟操作系统 国产化 flash-attention 报错 嵌入式硬件 驱动开发 硬件工程 嵌入式实习 电脑 ide 物联网 单片机 ecmascript nextjs react reactjs 搜索引擎 HTML audio 控件组件 vue3 audio音乐播放器 Audio标签自定义样式默认 vue3播放音频文件音效音乐 自定义audio播放器样式 播放暂停调整声音大小下载文件 MI300x DeepSeek Deepseek ux 多线程 Reactor 设计模式 性能优化 C++ pytorch svn stm32 string模拟实现 深拷贝 浅拷贝 经典的string类问题 三个swap 开发环境 SSL证书 JAVA Java 小程序 能力提升 面试宝典 技术 IT信息化 创意 社区 c# Dify Flask FastAPI Waitress Gunicorn uWSGI Uvicorn prometheus rpc 远程过程调用 Windows环境 直播推流 运维开发 eureka web安全 佛山戴尔服务器维修 佛山三水服务器维修 flutter Hyper-V WinRM TrustedHosts 联想开天P90Z装win10 matlab gitee YOLOv8 NPU Atlas800 A300I pro asi_bench jdk mount挂载磁盘 wrong fs type LVM挂载磁盘 Centos7.9 ecm bpm mongodb MCP server C/S LLM 安全架构 ddos 医疗APP开发 app开发 agi AIGC ansible playbook gpu算力 AI编程 华为云 集成学习 集成测试 权限 html FunASR ASR mysql 交互 代码调试 ipdb fpga开发 oceanbase rc.local 开机自启 systemd 麒麟 媒体 深度优先 图论 并集查找 换根法 树上倍增 MNN Qwen GaN HEMT 氮化镓 单粒子烧毁 辐射损伤 辐照效应 pppoe radius ESP32 arm开发 银河麒麟桌面操作系统 Kylin OS 串口服务器 Docker Hub docker pull 镜像源 daemon.json Linux AI agent audio vue音乐播放器 vue播放音频文件 Audio音频播放器自定义样式 播放暂停进度条音量调节快进快退 自定义audio覆盖默认样式 java-ee next.js 部署 部署next.js bash IDEA 国标28181 视频监控 监控接入 语音广播 流程 SIP SDP redis TCP服务器 qt项目 qt项目实战 qt教程 idm 根服务器 excel laravel Linux无人智慧超市 LInux多线程服务器 QT项目 LInux项目 单片机项目 vue css less 数据结构 学习 grafana junit SEO 漏洞 远程桌面 显示管理器 lightdm gdm kind 树莓派 VNC 负载均衡 阻塞队列 生产者消费者模型 服务器崩坏原因 rabbitmq 网络穿透 云服务器 webrtc SSH Xterminal 信号处理 信息与通信 HarmonyOS Next VMware安装mocOS VMware macOS系统安装 WSL2 系统安全 onlyoffice openEuler unity unity3d unix VMware安装Ubuntu Ubuntu安装k8s k8s VR手套 数据手套 动捕手套 动捕数据手套 web gcc express okhttp CORS 跨域 游戏机 Netty 即时通信 NIO dns 远程连接 HTTP 服务器控制 ESP32 DeepSeek 输入法 hadoop 反向代理 致远OA OA服务器 服务器磁盘扩容 3d 数学建模 系统架构 HCIE 数通 tensorflow Ubuntu 24 常用命令 Ubuntu 24 Ubuntu vi 异常处理 烟花代码 烟花 元旦 自动驾驶 C语言 大数据 vasp安装 查询数据库服务IP地址 SQL Server 远程 命令 执行 sshpass 操作 语音识别 apt 国内源 AutoDL eclipse gateway Clion Nova ResharperC++引擎 Centos7 远程开发 业界资讯 客户端 SVN Server tortoise svn r语言 数据挖掘 数据可视化 数据分析 selenium 测试工具 xpath定位元素 计算机 程序员 Linux PID debian n8n dity make 华为od dubbo 王者荣耀 devops 鸿蒙 鸿蒙系统 kamailio sip VoIP 1024程序员节 大数据平台 其他 安装教程 GPU环境配置 Ubuntu22 CUDA PyTorch Anaconda安装 程序 群晖 中间件 jar gradle 合成模型 扩散模型 图像生成 Java Applet URL操作 服务器建立 Socket编程 网络文件读取 AI大模型 大模型 大模型入门 大模型教程 dify ESXi Dell HPE 联想 浪潮 游戏引擎 抗锯齿 Kali 虚拟机 webstorm 实习 ukui 麒麟kylinos openeuler rust腐蚀 微服务 chatgpt DevEco Studio HarmonyOS OpenHarmony 真机调试 .net 回显服务器 UDP的API使用 vSphere vCenter 无人机 ruoyi springboot 升级 CVE-2024-7347 postgresql 温湿度数据上传到服务器 Arduino HTTP 需求分析 规格说明书 zabbix 微信小程序 监控k8s集群 集群内prometheus sdkman elasticsearch 相机 金仓数据库 2025 征文 数据库平替用金仓 飞书 软件工程 软件测试 web3 恒源云 tcp vscode1.86 1.86版本 ssh远程连接 autodl 智能手机 矩阵 big data IPMITOOL BMC 硬件管理 oneapi TRAE 工业4.0 open webui proxy模式 IMM 5G 3GPP 卫星通信 虚拟局域网 rtsp服务器 rtsp server android rtsp服务 安卓rtsp服务器 移动端rtsp服务 大牛直播SDK mysql离线安装 ubuntu22.04 mysql8.0 echarts 源码 毕业设计 课程设计 传统数据库升级 银行 前端框架 大语言模型 LLMs 单一职责原则 Python 网络编程 聊天服务器 套接字 TCP Socket 可信计算技术 鲲鹏 考研 小智AI服务端 xiaozhi TTS FTP 服务器 rclone AList webdav fnOS 在线office tcpdump 计算机外设 hive Hive环境搭建 hive3环境 Hive远程模式 gitea asp.net大文件上传 asp.net大文件上传下载 asp.net大文件上传源码 ASP.NET断点续传 asp.net上传文件夹 asp.net上传大文件 .net core断点续传 webgl iis 移动云 云服务 显卡驱动 Python基础 Python教程 Python技巧 yaml Ultralytics 可视化 yum 实战案例 zookeeper nfs SSL 域名 etcd 数据安全 RBAC chrome 浏览器下载 chrome 下载安装 mac 谷歌浏览器下载 rsyslog 虚幻 armbian u-boot epoll 毕昇JDK safari pip Mac 系统 历史版本 下载 安装 Trae IDE AI 原生集成开发环境 Trae AI MySql CPU 主板 电源 网卡 WSL win11 无法解析服务器的名称或地址 frp 项目部署到linux服务器 项目部署过程 本地部署 腾讯云大模型知识引擎 P2P HDLC 思科 微信小程序域名配置 微信小程序服务器域名 微信小程序合法域名 小程序配置业务域名 微信小程序需要域名吗 微信小程序添加域名 k8s资源监控 annotations自动化 自动化监控 监控service 监控jvm 机器人 虚拟化 半虚拟化 硬件虚拟化 Hypervisor EasyConnect Cline Kali Linux 黑客 渗透测试 信息收集 双系统 GRUB引导 Linux技巧 魔百盒刷机 移动魔百盒 机顶盒ROM 黑客技术 springboot远程调试 java项目远程debug docker远程debug java项目远程调试 springboot远程 网工 opensearch helm ssrf 失效的访问控制 Linux的基础指令 openwrt 图像处理 can 线程池 Agent 企业微信 Linux24.04 deepin SSE open Euler dde 统信UOS LLM Web APP Streamlit .netcore list 模拟实现 deepseek r1 游戏服务器 TrinityCore 魔兽世界 linux上传下载 sysctl.conf vm.nr_hugepages 昇腾 npu 健康医疗 互联网医院 adobe elk wps 安卓 fd 文件描述符 bug odoo 服务器动作 Server action 环境迁移 ios iphone Ubuntu 24.04.1 轻量级服务器 python3.11 cocoapods xcode dash 正则表达式 ip linux安装配置 文件分享 SenseVoice make命令 makefile文件 W5500 OLED u8g2 rnn seatunnel 镜像 netty sentinel etl wireshark 高效日志打印 串口通信日志 服务器日志 系统状态监控日志 异常记录日志 嵌入式 linux驱动开发 jupyter 毕设 minio navicat OD机试真题 华为OD机试真题 服务器能耗统计 ROS 微信公众平台 开机自启动 rag ragflow ragflow 源码启动 ipython 低代码 DigitalOcean GPU服务器购买 GPU服务器哪里有 GPU服务器 智能音箱 智能家居 加解密 Yakit yaklang MacOS录屏软件 jmeter 多个客户端访问 IO多路复用 TCP相关API mamba Vmamba bootstrap 软考 XCC Lenovo C++软件实战问题排查经验分享 0xfeeefeee 0xcdcdcdcd 动态库加载失败 程序启动失败 程序运行权限 标准用户权限与管理员权限 繁忙 解决办法 替代网站 汇总推荐 AI推理 CDN lio-sam SLAM IMX317 MIPI H265 VCU dba ui bonding 链路聚合 压力测试 tailscale derp derper 中转 cursor windows日志 bcompare Beyond Compare 模拟器 教程 Minecraft 硬件架构 HiCar CarLife+ CarPlay QT RK3588 防火墙 NAT转发 NAT Server Unity Dedicated Server Host Client 无头主机 stm32项目 embedding wsl Node-Red 编程工具 流编程 网络攻击模型 glibc npm 常用命令 文本命令 目录命令 编程 性能分析 thingsboard LORA NLP H3C iDRAC R720xd freebsd springsecurity6 oauth2 授权服务器 前后端分离 Linux awk awk函数 awk结构 awk内置变量 awk参数 awk脚本 awk详解 linux 命令 sed 命令 dell服务器 go 图形化界面 css3 服务器无法访问 ip地址无法访问 无法访问宝塔面板 宝塔面板打不开 策略模式 单例模式 redhat XFS xfs文件系统损坏 I_O error es 实时互动 iot virtualenv ArkUI 多端开发 智慧分发 应用生态 鸿蒙OS ollama下载加速 工作流 workflow arm 服务器主板 AI芯片 file server http server web server 中兴光猫 换光猫 网络桥接 自己换光猫 X11 Xming rdp 实验 我的世界服务器搭建 asm Windows Typore Wi-Fi 单元测试 功能测试 Spring Security 我的世界 我的世界联机 数码 UOS 统信操作系统 iperf3 带宽测试 缓存 ISO镜像作为本地源 换源 Debian 云电竞 云电脑 todesk ShenTong 视觉检测 职场和发展 db jetty undertow 交叉编译 NAS Termux Samba ruby wsl2 备份SQL Server数据库 数据库备份 傲梅企业备份网络版 llama3 Chatglm 开源大模型 线程 GCC Linux环境 hugo gaussdb xss 宝塔面板访问不了 宝塔面板网站访问不了 宝塔面板怎么配置网站能访问 宝塔面板配置ip访问 宝塔面板配置域名访问教程 宝塔面板配置教程 selete 高级IO 微信分享 Image wxopensdk ocr 思科模拟器 Cisco IM即时通讯 QQ 剪切板对通 HTML FORMAT AI-native Docker Desktop yolov8 AI写作 AI作画 聊天室 KylinV10 麒麟操作系统 Vmware 银河麒麟服务器操作系统 系统激活 visual studio code 文件系统 路径解析 Radius muduo 个人博客 aarch64 编译安装 HPC 流水线 脚本式流水线 HAProxy efficientVIT YOLOv8替换主干网络 TOLOv8 弹性计算 KVM 计算虚拟化 弹性裸金属 iBMC UltraISO windwos防火墙 defender防火墙 win防火墙白名单 防火墙白名单效果 防火墙只允许指定应用上网 防火墙允许指定上网其它禁止 log4j 小艺 Pura X EMQX 通信协议 Ubuntu DeepSeek DeepSeek Ubuntu DeepSeek 本地部署 DeepSeek 知识库 DeepSeek 私有化知识库 本地部署 DeepSeek DeepSeek 私有化部署 运维监控 langchain 直流充电桩 充电桩 域名服务 DHCP 符号链接 配置 IPMI 增强现实 沉浸式体验 应用场景 技术实现 案例分析 AR Xinference RAGFlow 音乐库 飞牛 实用教程 pyautogui mcu cmos 硬件 ros bot Docker 虚幻引擎 Nuxt.js leetcode 推荐算法 DocFlow gpt CH340 串口驱动 CH341 uart 485 midjourney ubuntu24 vivado24 代理 程序员创富 裸金属服务器 弹性裸金属服务器 p2p figma prompt 自动化编程 热榜 Ubuntu共享文件夹 共享目录 Linux共享文件夹 命名管道 客户端与服务端通信 7z 状态管理的 UDP 服务器 Arduino RTOS av1 电视盒子 k8s集群资源管理 云原生开发 cnn GoogLeNet AD域 RAGFLOW cd 目录切换 ros2 moveit 机器人运动 neo4j 数据仓库 数据库开发 数据库架构 database 技术共享 知识图谱 ai小智 语音助手 ai小智配网 ai小智教程 智能硬件 esp32语音助手 diy语音助手 lsb_release /etc/issue /proc/version uname -r 查看ubuntu版本 keepalived 边缘计算 sonoma 自动更新 mariadb xshell termius iterm2 模拟退火算法 docker run 数据卷挂载 交互模式 Open WebUI EtherNet/IP串口网关 EIP转RS485 EIP转Modbus EtherNet/IP网关协议 EIP转RS485网关 EIP串口服务器 chrome devtools chromedriver code-server 执法记录仪 智能安全帽 smarteye GPU SysBench 基准测试 ArcTS 登录 ArcUI GridItem wpf arkUI mybatis 服务网格 istio MS Materials js LInux AISphereButler 序列化反序列化 deep learning 强化学习 银河麒麟高级服务器 外接硬盘 Kylin flink searxng 信息可视化 网页设计 网络药理学 生物信息学 生信 PPI String Cytoscape CytoHubba 知识库 本地知识库部署 DeepSeek R1 模型 RoboVLM 通用机器人策略 VLA设计哲学 vlm fot robot 视觉语言动作模型 具身智能 测试用例 华为机试 rime 数据库系统 技能大赛 鸿蒙开发 移动开发 做raid 装系统 eNSP 网络规划 VLAN 企业网络 remote-ssh CentOS Stream CentOS camera Arduino 电子信息 语法 linux环境变量 迁移指南 框架搭建 c firewall llama.cpp ceph sequoiaDB 火绒安全 VPS .net mvc断点续传 nlp 内网服务器 内网代理 内网通信 捆绑 链接 谷歌浏览器 youtube google gmail VM搭建win2012 win2012应急响应靶机搭建 攻击者获取服务器权限 上传wakaung病毒 应急响应并溯源 挖矿病毒处置 应急响应综合性靶场 minicom 串口调试工具 图形渲染 车载系统 蓝桥杯 黑苹果 RustDesk自建服务器 rustdesk服务器 docker rustdesk 服务器管理 配置教程 服务器安装 网站管理 雨云 NPS 剧本 WebRTC uniapp alias unalias 别名 软件定义数据中心 sddc cuda cudnn nvidia RTMP 应用层 uni-file-picker 拍摄从相册选择 uni.uploadFile H5上传图片 微信小程序上传图片 prometheus数据采集 prometheus数据模型 prometheus特点 状态模式 政务 分布式系统 监控运维 Prometheus Grafana opcua opcda KEPServer安装 大模型微调 docker命令大全 nac 802.1 portal 混合开发 环境安装 JDK VS Code regedit 开机启动 Redis Desktop dock 加速 centos-root /dev/mapper yum clean all df -h / du -sh token sas 京东云 AD 域管理 spark HistoryServer Spark YARN jobhistory 网站搭建 serv00 怎么卸载MySQL MySQL怎么卸载干净 MySQL卸载重新安装教程 MySQL5.7卸载 Linux卸载MySQL8.0 如何卸载MySQL教程 MySQL卸载与安装 基础入门 gpt-3 文心一言 微信开放平台 微信公众号配置 VSCode 本地化部署 hexo 链表 网络用户购物行为分析可视化平台 大数据毕业设计 服务器数据恢复 数据恢复 存储数据恢复 raid5数据恢复 磁盘阵列数据恢复 自定义客户端 SAS matplotlib 玩机技巧 软件分享 软件图标 僵尸世界大战 游戏服务器搭建 AP配网 AK配网 小程序AP配网和AK配网教程 WIFI设备配网小程序UDP开 服务器部署ai模型 Anolis nginx安装 linux插件下载 私有化 人工智能生成内容 金融 AnythingLLM AnythingLLM安装 ai工具 rocketmq java-rocketmq v10 软件 ldap 拓扑图 minecraft GIS 遥感 WebGIS 深度求索 私域 环境配置 Claude 大大通 第三代半导体 碳化硅 大模型面经 大模型学习 URL 邮件APP 免费软件 pyqt 软件需求 基础环境 Kylin-Server Ubuntu Server Ubuntu 22.04.5 ubuntu20.04 开机黑屏 QT 5.12.12 QT开发环境 Ubuntu18.04 内网环境 h.264 micropython esp32 mqtt docker搭建nacos详解 docker部署nacos docker安装nacos 腾讯云搭建nacos centos7搭建nacos jina 匿名管道 USB转串口 飞牛NAS 飞牛OS MacBook Pro harmonyOS面试题 流式接口 cpp-httplib 沙盒 word RAID RAID技术 磁盘 存储 pgpool win服务器架设 windows server uv 多路转接 田俊楠 ip命令 新增网卡 新增IP 启动网卡 项目部署 网卡的名称修改 eth0 ens33 Playwright 自动化测试 outlook ssh远程登录 xrdp NFS SRS 流媒体 直播 软负载 多进程 PX4 ABAP Deepseek-R1 私有化部署 推理模型 USB网络共享 雨云服务器 YOLOv12 vmware 卡死 崖山数据库 YashanDB Qwen2.5-coder 离线部署 视频编解码 源码剖析 rtsp实现步骤 流媒体开发 iftop 网络流量监控 存储维护 NetApp存储 EMC存储 threejs 3D 浏览器开发 AI浏览器 ssh漏洞 ssh9.9p2 CVE-2025-23419 AI Agent 字节智能运维 性能测试 chfs ubuntu 16.04 粘包问题 相差8小时 UTC 时间 rustdesk Cookie ubuntu24.04.1 远程控制 远程看看 远程协助 IO模型 AI代码编辑器 cpu 实时 使用 三级等保 服务器审计日志备份 FTP服务器 ue4 着色器 ue5 seleium risc-v swoole Attention trea idea DBeaver kerberos Invalid Host allowedHosts 北亚数据恢复 oracle数据恢复 visual studio 干货分享 黑客工具 密码爆破 anaconda 西门子PLC 通讯 springcloud Docker引擎已经停止 Docker无法使用 WSL进度一直是0 镜像加速地址 perf 线性代数 电商平台 大文件分片上传断点续传及进度条 如何批量上传超大文件并显示进度 axios大文件切片上传详细教 node服务器合并切片 vue3大文件上传报错提示错误 大文件秒传跨域报错cors 流量运营 openstack Xen 压测 ECS 上传视频至服务器代码 vue3批量上传多个视频并预览 如何实现将本地视频上传到网页 element plu视频上传 ant design vue vue3本地上传视频及预览移除 TCP协议 宕机切换 服务器宕机 composer RAG 检索增强生成 文档解析 大模型垂直应用 产测工具框架 IMX6ULL 管理框架 系统开发 binder framework 源码环境 triton 模型分析 skynet transformer Logstash 日志采集 x64 SIGSEGV xmm0 DOIT 四博智联 开发 milvus wsgiref Web 服务器网关接口 PVE 代理服务器 bat 端口 查看 ss 阿里云ECS ardunio BLE 端口测试 firewalld 嵌入式系统开发 kali 共享文件夹 Unity插件 iventoy VmWare OpenEuler 嵌入式Linux IPC EMUI 回退 降级 gnu crosstool-ng 磁盘监控 服务器配置 docker部署翻译组件 docker部署deepl docker搭建deepl java对接deepl 翻译组件使用 rpa conda配置 conda镜像源 fast 大模型应用 Google pay Apple pay OpenSSH Jellyfin 自动化任务管理 easyui 远程服务 TrueLicense Linux find grep 离线部署dify 超融合 yum源切换 更换国内yum源 DNS 大模型部署 IPv4 子网掩码 公网IP 私有IP SSH 密钥生成 SSH 公钥 私钥 生成 UDP 灵办AI tidb GLIBC grub 版本升级 扩容 元服务 应用上架 服务器时间 游戏开发 vr SSH 服务 SSH Server OpenSSH Server 带外管理 企业网络规划 华为eNSP 设备 PCI-Express edge浏览器 VMware创建虚拟机 搭建个人相关服务器 sqlite3 音乐服务器 Navidrome 音流 Windsurf ping++ 磁盘镜像 服务器镜像 服务器实时复制 实时文件备份 trae Erlang OTP gen_server 热代码交换 事务语义 信创 信创终端 中科方德 大模型推理 Linux的权限 SWAT 配置文件 服务管理 网络共享 deekseek wordpress 无法访问wordpess后台 打开网站页面错乱 linux宝塔面板 wordpress更换服务器 dns是什么 如何设置电脑dns dns应该如何设置 DeepSeek行业应用 Heroku 网站部署 在线预览 xlsx xls文件 在浏览器直接打开解析xls表格 前端实现vue3打开excel 文件地址url或接口文档流二进 李心怡 多层架构 解耦 docker部署Python 架构与原理 C# MQTTS 双向认证 emqx 分析解读 分布式训练 WebUI DeepSeek V3 办公自动化 自动化生成 pdf教程 信号 mm-wiki搭建 linux搭建mm-wiki mm-wiki搭建与使用 mm-wiki使用 mm-wiki详解 算力 软链接 硬链接 风扇控制软件 数据管理 数据治理 数据编织 数据虚拟化 EtherCAT转Modbus ECT转Modbus协议 EtherCAT转485网关 ECT转Modbus串口网关 EtherCAT转485协议 ECT转Modbus网关 欧标 OCPP Ark-TS语言 g++ g++13 DenseNet 影刀 #影刀RPA# 物联网开发 lua hosts MacMini 迷你主机 mini Apple clickhouse 产品经理 CrewAI vue-i18n 国际化多语言 vue2中英文切换详细教程 如何动态加载i18n语言包 把语言json放到服务器调用 前端调用api获取语言配置文件 宠物 免费学习 宠物领养 宠物平台 MDK 嵌入式开发工具 论文笔记 sublime text 社交电子 arcgis 高效远程协作 TrustViewer体验 跨设备操作便利 智能远程控制