最新资讯

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

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

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

文章目录

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