课程5. 迁移学习
课程5. 迁移学习
- 卷积神经网络架构
- ImageNet
- 神经网络架构
- 实践
- 从 torchvision 加载模型
- 在一个图像上测试预先训练的网络
- 迁移学习
- 网络训练
- 冻结层
- 实践
- 准备数据
- 替换网络的最后一层
- 冻结层
- 网络训练
- 获取测试样本的质量指标
课程计划:
- 流行的神经网络架构
- 迁移学习
卷积神经网络架构
在上一课中,我们学习了卷积神经网络的运行原理,它非常适合处理图像。在本课中,我们将熟悉流行的卷积神经网络架构。
首先,简单说一下为什么我们需要分析特定的卷积网络架构。
当研究人员想要使用神经网络解决问题时,他们通常不会从头编写自己的神经网络,而是采用标准架构之一,例如 ResNet 或 DenseNet,并对其进行训练以完成任务。这非常方便:这些架构已经证明它们可以有效地解决分类问题,并且您无需自己设置架构(层数、层中的过滤器数量、将池化放在何处等),而是可以采用现成的成功架构(例如 ResNet)并对其进行训练。
选择哪种架构取决于问题的类型、问题的难度以及您拥有的训练数据的数量。数据越少,任务越简单,需要使用的网络就越轻,深度越浅,以免过度拟合。例如,最简单的AlexNet网络适合对MNIST或CIFAR图像进行分类。如果您的任务很复杂,即您拥有复杂不同形状和许多不同类别的图像,例如在 ImageNet 中,那么您将需要一个具有大量层的神经网络。
此外,不同的架构使用不同的附加技巧来提高网络效率。了解它们对于为你的任务选择正确的架构很有用。
在我们开始讨论神经网络架构之前,让我们先讨论一下 ImageNet。
ImageNet
ImageNet 是一个分为大量类别的图像数据库。也就是说,这是一个可以解决图像分类问题的数据集。 ImageNet 有两个版本:
- 旧版本:1,000 个类别,1,431,167 张图像(1,281,167/50,000/100,000 个训练/验证/测试)
- 新版本:21,000 个类别,14,197,122 张图片
2010年启动了基于ImageNet-1k版本的图像分类竞赛。参与者必须在 ImageNet 的训练集上训练机器学习模型,并在测试集上获得预测。使用准确度指标来评估解决方案。
比赛时间表:
神经网络架构
让我们简要讨论一下时间线上的架构(以及一些不在时间线上的架构)
- NEC-UIUC、XRCE 是非神经模型。它们基于经典机器学习的思想;
- AlexNet 是第一个在 ImageNet 上表现优异的卷积神经网络。这一刻被认为是深度学习的一次革命:人们相信神经网络可以成功地用于处理图像。 AlexNet 的架构相当简单,由我们已知的元素组成:conv、relu、fc、bachnorm、dropout;
- ZFNet、VGG — 也是常规卷积神经网络,但比 AlexNet 更深;
VGG 优于 AlexNet,很大程度上归功于 VGG 拥有更多的卷积层。这是有道理的:更多的层数以及更多的网络参数使得模型能够更好地适应数据中的复杂依赖关系。而在VGG成功之后,人们开始尝试进一步增加网络的层数,希望神经网络能够发挥更好的作用。
但事实证明事情并非如此简单:如果神经网络的层数太多,网络的学习效果就会开始变差。出现梯度衰减问题。
我们不会详细讲解梯度衰减问题的本质,以及解决该问题的所有方法,但会提到一种方法:跳跃连接(skip connection)。
跳过连接是在网络的非连续层之间添加额外的连接。
在上图的示例中,从网络的第一层到第四层添加了额外的连接。第四层将第 1 层和第 3 层的激活图的总和或连接作为输入。
在网络中添加跳过连接可以创建具有大量层的神经网络:
- ResNet:卷积网络 + 跳过连接(激活图的总和);
- DenseNet:卷积网络 + 跳过连接(激活图的连接);
以下是 ResNet-34(具有 34 层的 ResNet 版本)的示意图:
除了跳过连接之外,还有许多技巧可以提高神经网络的效率。其中一些技巧是针对特定任务的,而其他技巧则旨在改善神经网络的整体图像处理过程。
其他架构的示例:
- Inception(GoogLeNet);
- MobileNet:专为移动设备使用而定制的轻量级架构;
- ResNeXt、ShuffleNet、…
torchvision 中可用的架构可以在这里找到。
许多架构都有版本。例如:
- VGG-13, -16, -19;
-ResNet-18, -34, -50, 154, …
版本之间的区别主要在于层数(以及参数)。
实践
import numpy as np
import matplotlib.pyplot as pltfrom PIL import Imageimport torch
import torch.nn as nn
import torch.optim as optimimport torchvision
from torchvision import datasets, models, transforms
从 torchvision 加载模型
# resnet-18 模型
model = models.resnet18(pretrained=True)
model
输出:
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=512, out_features=1000, bias=True)
)
为了最有效地使用预先训练的 resnet18 网络,我们需要以某种方式预处理作为输入的所有图像。 resnet 系列的所有网络都有自己固定的变换:
resnet_transforms = transforms.Compose([transforms.Resize(256), # 每幅图像的尺寸将缩小到 256*256transforms.CenterCrop(224), # 图片的中心部分将被剪掉,尺寸为 224*224transforms.ToTensor(), # 图像从python数组转换为torch.Tensor格式transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 图像像素值归一化])# 你不必自己编写转换,你可以从模型中获取它们:
# resnet_transforms = models.ResNet18_Weights.IMAGENET1K_V1.transforms()
在一个图像上测试预先训练的网络
我们从互联网上下载一张狗的图片:
! wget "https://ichef.bbci.co.uk/news/640/cpsprodpb/475B/production/_98776281_gettyimages-521697453.jpg" -O "doggie.jpg"
让我们来看看图:
from PIL import Imageimage = Image.open('doggie.jpg')
image
np.array(image).shape
输出:(360, 640, 3)
让我们对图像进行变换,看看变换之后它会是什么样子:
image_transformed = resnet_transforms(image)
print(image_transformed.shape)
输出:torch.Size([3, 224, 224])
plt.imshow(image_transformed.permute(1,2,0).data.cpu().numpy())
我们将图像输入到网络输入并得到答案:
model.eval()with torch.no_grad():# image_transformed -> [image_transformed]model_output = model(image_transformed.reshape((1, 3, 224, 224)))model_output
我们来获取一下网络对图片的响应。类别编号和名称之间的关系可以在这里找到:链接
np.argmax(model_output.data.cpu().numpy())
输出:np.int64(207)
我们根据上面的链接发现第207号是’golden retriever’,金毛猎犬,和我们图一致!
迁移学习
Transfer Learning!
我们现在将讨论迁移学习范式。 Transfer Learning 译为知识的转移。我们将了解它是什么以及如何使用迁移学习来训练神经网络。
迁移学习有助于解决的问题:
我们知道,当训练数据集中的数据很少时,神经网络经常会过度拟合。当任务复杂时尤其如此。此类问题的一个例子是肿瘤图像的分类。在医学领域,用于训练神经网络的数据总是很少:收集起来非常困难。但这项任务相当复杂:肿瘤的形状和大小完全不同。
另一个很难收集足够数据的复杂任务的例子是鲜为人知的语言的机器翻译。为了训练一个从一种语言到另一种语言的机器翻译模型,需要大量的平行句子。平行句是由一对句子组成的:一种语言的句子和其第二种语言的准确翻译。最常见的是,此类优惠都是从互联网上收集的。例如维基百科,同一篇文章经常被翻译成许多不同的语言。
但如果某种语言没有被广泛使用,那么互联网上用该语言写的文本就会很少。而且,不可能用这种语言输入足够数量的句子并将其翻译成另一种语言。
这是一个常见的问题。它无处不在,不仅仅是在医学领域和机器翻译中。机器学习问题变得越来越复杂,解决这些问题所需的神经网络也变得越来越深,这需要越来越多的数据进行训练。简而言之,在机器学习中,数据永远是稀缺的。
解决这个问题的一种方法就是迁移学习。将知识从一个机器学习模型转移到另一个机器学习模型。迁移学习最简单的方法之一就是再训练。
网络训练
再训练的工作原理非常简单:
- 从头开始在 ImageNet(或其他大型数据集)上训练网络。
- 我们进一步在所需的数据集上训练网络。
ImageNet 通常是一个很好的预训练数据集。它包含大量不同的类别和形式,因此在对其进行训练的过程中,神经网络会学习对图片中各种模式做出反应的过滤器。就好像她“了解周围世界的物体是如何构成的”。即使新数据集中没有太多图像,这也能帮助网络更好地适应对新数据集中的新对象进行分类。
然而,这里有一个警告:大多数情况下,额外训练的数据集包含的类别数量与 ImageNet 不同。数据集中的类别数量会影响网络最后一层应该有多少个神经元。
这个问题解决起来很简单:在进一步训练之前,我们扔掉网络的最后一层,用一个新的层替换它,它将具有所需数量的神经元。该层的权重在重新训练之前将是随机的;它们将在重新训练过程中与整个神经网络一起进行训练。
重新训练几乎总是比从头开始训练效果更好(前提是重新训练的数据集不比预训练的数据集丰富)。
即使对于语义上相距甚远的数据集,重新训练也有效。例如,在 ImageNet 上进行预训练并在建筑物数据集上进行再训练也效果很好。
有时,在重新训练卷积神经网络之前,所有全连接层都会被替换。这是因为卷积层是特征提取层,而全连接层是分类器层。在重新训练的过程中,特征提取层学会了从图像中提取有用的特征,这在新的数据集上重新训练时也会有用。分类器层是相当专业的层,经过训练可以解决狭义的任务:对特定数据集中的图像进行分类。因此,它们可以针对新任务进行重新训练,以便它们学会以新的方式匹配从卷积层和任务类获得的特征。
冻结层
有时,为了加快神经网络的训练,会冻结预先训练的神经网络的几个初始层。
为什么他们要冻结初始层?我们记得,在训练卷积神经网络时,初始层响应图像中更简单、低级的模式,而外层响应更高级、复合的模式。无论我们解决什么问题,几乎所有图片中都会出现简单的图案。因此,神经网络的第一层在 ImageNet 上训练时学会提取的几乎所有信息在我们进一步对野生动物进行神经网络训练时也将有用。但最后的卷积层学会了提取有关 ImageNet 图像中复杂模式存在的信息。并且它们更具体地针对网络训练的数据集。并非所有这些都与野生动物分类有关。因此,在新的数据集上进一步训练远距离卷积层是有意义的,这样它们就会学会识别那些有助于对野生动物进行分类的复杂模式。
我们可以说,第一个卷积层学习理解图片中物体的基础、基本结构。然后我们将有关物体基本结构的知识转移到另一项任务。现在需要对神经网络进行训练,以便对野生动物进行分类,而不必从头开始:它只需要从它所知道的形式中学习组装十种野生动物的模式:即第一个预训练的卷积层识别的形式。
实践
准备数据
# 此单元格下载包含数据的 zip 存档
! pip install wldhx.yadisk-direct
! curl -L $(yadisk-direct https://disk.yandex.com/d/eS6LL7bLmrlO7w) -o dogs.zip
输出:
# 此单元格解压档案。包含数据的文件夹 ./dogs 将出现在 colab 中。
! unzip -qq dogs.zip
import numpy as np
import matplotlib.pyplot as pltfrom PIL import Imageimport torch
import torch.nn as nn
import torch.optim as optimimport torchvision
from torchvision import datasets, models, transforms
resnet_transforms = transforms.Compose([transforms.Resize(256), # 每幅图像的尺寸将缩小到 256*256transforms.CenterCrop(224), # 图片的中心部分将被剪掉,尺寸为 224*224transforms.RandomPerspective(distortion_scale=0.6, p=1.0),transforms.ToTensor(), # 图像从python数组转换为torch.Tensor格式transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 图像像素值归一化])
train_data = datasets.ImageFolder('./dogs/train', transform=resnet_transforms)
val_data = datasets.ImageFolder('./dogs/valid', transform=resnet_transforms)
test_data = datasets.ImageFolder('./dogs/test', transform=resnet_transforms)
会变成:
train_loader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)
val_loader = torch.utils.data.DataLoader(val_data, batch_size=64, shuffle=False)
test_loader = torch.utils.data.DataLoader(test_data, batch_size=64, shuffle=False)
替换网络的最后一层
让我们加载将进一步训练的模型:
model = models.resnet18(pretrained=True)
model
输出:
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=512, out_features=1000, bias=True)
)
model.fc
输出:Linear(in_features=512, out_features=1000, bias=True)
让我们用一个包含 70 个神经元的新层替换网络的最后一层(因为数据集中有 70 个类):
model.fc = nn.Linear(512, 70)
model
输出:
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=512, out_features=70, bias=True)
)
我们发现只有最后的输出类别(也就是最后一层发生了改变,其余的不变)
冻结层
# model.children() 返回神经网络子模块列表
# 在我们的例子中,这些是 resnet 块
len(list(model.children()))
输出:10
让我们冻结所有卷积层:
# 我们通过神经网络块
for i, layer in enumerate(model.children()):# 冻结前九个块if i < 9:# 我们遍历块的所有权重(参数)for param in layer.parameters():# 冻结参数param.requires_grad = False
我们将所有准备代码放在一起:
def create_model(model, num_freeze_layers, num_out_classes):# 替换网络的最后一层model.fc = nn.Linear(512, num_out_classes)# 冻结图层for i, layer in enumerate(model.children()):if i < num_freeze_layers:for param in layer.parameters():param.requires_grad = Falsereturn model
model = create_model(models.resnet18(pretrained=True), 9, 70)
网络训练
如果 GPU 可用,我们就将神经网络转移到 GPU:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device
def evaluate(model, dataloader, loss_fn):losses = []num_correct = 0num_elements = 0for i, batch in enumerate(dataloader):# 获取当前批次X_batch, y_batch = batchnum_elements += len(y_batch)# 此行禁用梯度计算with torch.no_grad():# 获取批量图像的网络响应logits = model(X_batch.to(device))# 计算当前批次的损失loss = loss_fn(logits, y_batch.to(device))losses.append(loss.item())# 计算网络响应作为每幅图像的类别编号y_pred = torch.argmax(logits, dim=1)# 计算当前批次中正确的网络响应数量num_correct += torch.sum(y_pred.cpu() == y_batch)# 计算最终正确答案的百分比accuracy = num_correct / num_elementsreturn accuracy.numpy(), np.mean(losses)def train(model, loss_fn, optimizer, n_epoch=3):# 网络训练周期for epoch in range(n_epoch):print("Epoch:", epoch+1)model.train(True)running_losses = []running_accuracies = []for i, batch in enumerate(train_loader):# 获取当前批次X_batch, y_batch = batch# 前向传递(获取对一批图像的响应)logits = model(X_batch.to(device))# 计算网络给出的答案和批次的正确答案的损失loss = loss_fn(logits, y_batch.to(device))running_losses.append(loss.item())loss.backward() # backpropagation(梯度计算)optimizer.step() # 更新网络权重optimizer.zero_grad() # 重置权重# 计算当前训练批次的准确率model_answers = torch.argmax(logits, dim=1)train_accuracy = torch.sum(y_batch == model_answers.cpu()) / len(y_batch)running_accuracies.append(train_accuracy)# 记录结果if (i+1) % 50 == 0:print("Average train loss and accuracy over the last 50 iterations:",np.mean(running_losses), np.mean(running_accuracies), end='\n')# 每个时期之后,我们都会得到验证样本的质量指标model.train(False)val_accuracy, val_loss = evaluate(model, val_loader, loss_fn=loss_fn)print("Epoch {}/{}: val loss and accuracy:".format(epoch+1, n_epoch,),val_loss, val_accuracy, end='\n')return model
# 再次声明模型
model = create_model(models.resnet18(pretrained=True), 9, 70)
model = model.to(device)# 选择损失函数
loss_fn = torch.nn.CrossEntropyLoss()# 选择优化算法和学习率。
# 你可以尝试不同的 learning_rate 值
learning_rate = 1e-3
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# 让我们开始训练模型
# 参数 n_epoch 可以变化
model = train(model, loss_fn, optimizer, n_epoch=3)
输出:
获取测试样本的质量指标
test_accuracy, _ = evaluate(model, test_loader, loss_fn)
print('Accuracy on the test', test_accuracy)
输出:
Accuracy on the test 0.81857145
相关文章:
课程5. 迁移学习
课程5. 迁移学习 卷积神经网络架构ImageNet神经网络架构实践从 torchvision 加载模型在一个图像上测试预先训练的网络 迁移学习网络训练冻结层实践准备数据替换网络的最后一层冻结层网络训练获取测试样本的质量指标 课程计划: 流行的神经网络架构迁移学习 卷积神经…...
SATA(Serial Advanced Technology Attachment)详解
一、SATA的定义与核心特性 SATA(串行高级技术附件)是一种 用于连接存储设备(如硬盘、固态硬盘、光驱)的高速串行接口标准,取代了早期的PATA(并行ATA)。其核心特性包括: 高速传输&am…...
常用的 MyBatis 标签及其作用
MyBatis 是一个优秀的持久层框架,它通过 XML 或注解的方式将 Java 对象与数据库操作进行映射。在 MyBatis 的 XML 映射文件中,可以使用多种标签来定义 SQL 语句、参数映射、结果映射等。以下是一些常用的 MyBatis 标签及其作用: 1. 基本标签 …...
Blender配置渲染设置并输出动画
在Blender中,渲染设置和渲染动画的选项位于不同的面板中。以下是具体步骤: 渲染设置 渲染设置用于配置输出格式、分辨率、帧率等参数。 打开右侧的 属性面板(按 N 键可切换显示)。 点击 “输出属性” 选项卡(图标是…...
网络故障排查指南:分治法与排除法结合的分层诊断手册
目录 一、排查方法论:分治法与排除法的结合 1. 分治法(Divide and Conquer) 2. 排除法(Elimination) 二、分层诊断实战手册 1. 物理层排查(设备与线路) 硬件检测三板斧 运维经验 2. 网络…...
【万字总结】前端全方位性能优化指南(三)——GPU渲染加速、WebGPU、OffscreenCanvas多线程渲染
theme: condensed-night-purple 前言 当每秒60帧的流畅渲染遭遇百万级多边形场景,传统CPU绘图如同单车道上的赛车——即便引擎轰鸣,依然难逃卡顿困局。现代GPU加速技术将渲染任务从「单车道」扩展到「八车道」,本章以分层爆破、API革命、线程联邦为技术支柱,拆解如何通过G…...
报错 - redis - Unit redis.service could not be found.
报错: Unit redis.service could not be found.Could not connect to Redis at 127.0.0.1:6379: Connection refused解决方法: 检查状态、有必要的话 重新安装 Linux 上查看状态 systemctl status redis显示以下内容,代表正常服务 出现下面…...
Windows系统本地部署OpenManus对接Ollama调用本地AI大模型
文章目录 前言1. 环境准备1.1 安装Python1.2. 安装conda 2. 本地部署OpenManus2.1 创建一个新conda环境2.2 克隆存储库2.3 安装依赖环境 3. 安装Ollama4. 安装QwQ 32B模型5. 修改OpenManus配置文件6. 运行OpenManus7.通过网页使用OpenManus8. 安装内网穿透8.1 配置随机公网地址…...
【递归,搜索与回溯算法篇】- 名词解释
一. 递归 1. 什么是递归? 定义: 函数自己调用自己的情况关键点: ➀终止条件: 必须明确递归出口,避免无限递归 ➁子问题拆分: 问题需能分解成结构相同的更小的子问题缺点: ➀栈溢出风险&#x…...
【设计模式】装饰模式
六、装饰模式 装饰(Decorator) 模式也称为装饰器模式/包装模式,是一种结构型模式。这是一个非常有趣和值得学习的设计模式,该模式展现出了运行时的一种扩展能力,以及比继承更强大和灵活的设计视角和设计能力,甚至在有些场合下&am…...
c库、POSIX库、C++库、boost库之间的区别和联系
文章目录 一、区别1. 定义和来源2. 功能范围3. 可移植性4. 语言支持5. 维护和更新 二、联系1. 相互补充2. 部分功能重叠3. 共同促进编程发展4. 代码兼容性 三、总结 一、区别 1. 定义和来源 C 库函数:由 ANSI C 和 ISO C 标准定义,是 C 语言编程的基础…...
算法及数据结构系列 - 树
系列文章目录 算法及数据结构系列 - 二分查找 算法及数据结构系列 - BFS算法 算法及数据结构系列 - 动态规划 算法及数据结构系列 - 双指针 算法及数据结构系列 - 回溯算法 文章目录 树框架树遍历框架N叉树遍历框架 经典题型124.二叉树的最大路径和105.从前序与中序遍历序列构造…...
go安装lazydocker
安装 先安装go环境 https://blog.csdn.net/Yqha1/article/details/146430281?fromshareblogdetail&sharetypeblogdetail&sharerId146430281&sharereferPC&sharesourceYqha1&sharefromfrom_link 安装lazydocker go install github.com/jesseduffield/laz…...
《深度学习》——YOLOv3详解
文章目录 YOLOv3简介YOLOv3核心原理YOLOv3改进YOLOv3网络结构 YOLOv3简介 YOLOv3(You Only Look Once, version 3)是一种先进的实时目标检测算法,由 Joseph Redmon 和 Ali Farhadi 开发。它在目标检测领域表现出色,具有速度快、精…...
使用spring-ai-ollama访问本地化部署DeepSeek
创建SpringBoot工程,引入依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"htt…...
Kafka消息自定义序列化
文章目录 1. 默认序列化2.自定义序列化3.示例4.自定义解序列化器 1. 默认序列化 在网络中发送数据都是以字节的方式,Kafka也不例外。Apache Kafka支持用户给broker发送各种类型的消息。它可以是一个字符串、一个整数、一个数组或是其他任意的对象类型。序列化器(se…...
使用Systemd管理ES服务进程
Centos中的Systemd介绍 CentOS 中的 Systemd 详细介绍 Systemd 是 Linux 系统的初始化系统和服务管理器,自 CentOS 7 起取代了传统的 SysVinit,成为默认的初始化工具。它负责系统启动、服务管理、日志记录等核心功能,显著提升了系统的启动速…...
编程语言选择分析:C#、Rust、Go 与 TypeScript 编译器优化
编程语言选择分析:C#、Rust、Go 与 TypeScript 编译器优化 在讨论编程语言的选择时,特别是针对微软的 C# 和 Rust,以及谷歌的 Go 语言,以及微软试图通过 Go 来拯救 TypeScript 编译器的问题,我们可以从多个角度来分析和…...
使用粘贴控件
HarmonyOS 5.0.3(15) 版本的配套文档,该版本API能力级别为API 15 Release 文章目录 约束与限制开发步骤 粘贴控件是一种特殊的系统安全控件,它允许应用在用户的授权下无提示地读取剪贴板数据。 在应用集成粘贴控件后,用户点击该控件…...
MySQL 客户端连不上(1045 错误)原因全解析
MySQL 客户端连不上(1045 错误)原因全解析 在我们学习 MySQL 或从事 MySQL DBA 工作期间,时常会遇到:“我尝试连接到 MySQL 并且收到1045 错误,但我确定我的用户和密码都没问题”。 不管你现在是否是高手还是高高手,都不可避免曾经在初学的时候犯过一些很初级的错误,例…...
麒麟系列Linux发行版探秘
以下内容摘自《银河麒麟操作系统进阶应用》一书。 银河麒麟操作系统(Kylin) 银河麒麟(Kylin)操作系统是中国自主研发的一款基于Linux内核的操作系统。它的发展历程可以追溯到2002年,最初由国防科技大学主导研发&…...
刘强东突然发声:不该用算法压榨最底层兄弟!东哥,真正的人民企业家
今天忙了一天,很累,准备睡觉的时候,看到网上盛传的刘强东的朋友圈,东哥又在朋友圈发文了。 说实话,看完之后,感动,真的感动。 尤其是当我看到这两句话的时候。 1、我们所学的知识、商业模式、技…...
信息收集与问答系统流程分析与改进建议
现有系统的问题与局限 1. 资源管理问题 二元决策机制过于简化:当前系统仅在令牌预算耗尽时才进入Beast Mode,缺乏渐进式资源分配策略缺少早期预算规划:没有基于问题复杂度的初始资源分配机制缺乏优先级资源分配:所有问题和策略消…...
【人工智能】如何理解transformer中的token?
如何理解transformer中的token? **一、Token在Transformer中的作用****二、文本分词的常见方法****1. 基于词典的分词(Dictionary-based Tokenization)****2. 子词分词(Subword Tokenization)****(1) WordPiece算法****(2) BPE&a…...
Spring Boot 集成 Kafka 消息发送方案
一、引言 在 Spring Boot 项目中,Kafka 是常用的消息队列,可实现高效的消息传递。本文介绍三种在 Spring Boot 中使用 Kafka 发送消息的方式,分析各自优缺点,并给出对应的 pom.xml 依赖。 二、依赖引入 在 pom.xml 中添加以下依赖: <dependencies><!-- Sprin…...
Hadoop•HDFS的Java API操作
听说这是目录哦 上传文件到HDFS🌈一、下载Windows版本的JDK和Hadoop二、配置物理机环境变量三、创建项目四 、添加依赖五、新建java类六、创建文件七、打开集群八、选中、运行 从HDFS下载文件🪐一、写代码二、HDFS要个文件三、物理机要个文件夹ÿ…...
电脑如何设置几分钟后自动关机
摘要:本文提供Windows、macOS和Linux系统设置定时自动关机的详细方法。 目录 一、Windows系统设置方法 设置定时关机 取消关机计划 二、macOS系统设置方法 设置定时关机取消关机计划 三、Linux系统设置方法 设置定时关机 取消关机计划 四、注意事项五、扩展&#x…...
固定公网 IP
固定公网 IP 是指为用户分配一个长期不变且可从互联网直接访问的 IP 地址,具有以下重要作用: 1. 搭建服务器 网站托管:可用于托管网站、博客或电子商务平台。 应用服务器:支持运行邮件服务器、游戏服务器、数据库等。 远程访问&…...
Linux安装go环境
安装一个lazydocker,根据文档需要先安装go环境 https://github.com/jesseduffield/lazydocker 官方文档解析 https://go.dev/doc/install 文档内容如下,一共三步 1.删除先前安装的go,解压下载的go压缩包到/usr/local目录 2.添加环境变量&…...
Git的基本使用
Git的基本使用 前言 :为什么使用GitGit基本操作1. 初始化2. Git分区3. 认识.git目录4. git基本操作 Git分支管理1. 基本操作2. Git分支设计规范 Git 标签管理1. Git标签的使用2. 标签使用规范3. Git标签与分支的区别 分离头指针问题1. 分离头指针问题的风险2. 分离头…...
鸿蒙Flutter开发故事:不,你不需要鸿蒙化
在华为牵头下,Flutter 鸿蒙化如火如荼进行,当第一次看到一份上百个插件的Excel 列表时,我也感到震惊,排名前 100 的插件赫然在列,这无疑是一次大规模的军团作战。 然后,参战团队鱼龙混杂,难免有…...
Mysql:关于命名
1. 命名的对象:库名、表名、列名、索引名 2. 用反引号包裹的情况下,命名时不允许使用空白字符、反引号,其它字符均可 3. 无反引号包裹的情况下,命名时仅允许使用:$、_、数字、大小写字母、中文字符(已知win系统支持)…...
JAVA————十五万字汇总
JAVA语言概述 JAVA语句结构 JAVA面向对象程序设计(一) JAVA面向对象程序设计(二) JAVA面向对象程序设计(三)工具类的实现 JAVA面向对象程序设计(四)录入异常处理 JAVA图形用户界面设…...
Chrome-Edge-IDEA-Win 常用插件-工具包
Chrome-Edge-IDEA-Win 常用插件-工具包 Chrome-Edge-IDEA-Win 常用插件-工具包谷歌插件chropathJSONViewOctotree - GitHub code treeXPath Helper书签侧边栏篡改猴Print Edit WEEdge浏览器插件IDEA插件CodeGlance Pro 代码迷你缩放图插件Alibaba Cloud ToolkitAlibaba Java Co…...
DeepSeek-R1论文深度解析:纯强化学习如何引爆LLM推理革命?
技术突破:从“无监督”到“自主进化”的跨越 paper :https://arxiv.org/pdf/2501.12948目录 技术突破:从“无监督”到“自主进化”的跨越1 DeepSeek-R1-Zero: RLnoSFT1.1 R1-Zero: GRPO(Group Relative Po…...
最新!Ubuntu Docker 安装教程
源自: AINLPer(每日干货分享!!) 编辑: ShuYini 校稿: ShuYini 时间: 2025-3-1 更多:>>>>大模型/AIGC、学术前沿的知识分享! 看到很多部署大模型的时候,都是基于docker安装部署的。…...
【Javascrip】Javascript练习01 REST API using Express.js.
针对该问题的项目路径 要求部分 what you need to doReview the tasks provided in the section below.Obtain the boilerplate code.Use your local development environment to implement a solution.Upload your solution for marking via Gradescope. There is no attempt…...
visual studion 2022如何使用PlaySound()
书籍:《windows程序设计(第五版)》的开始 环境:visual studio 2022 内容:HELLOWIN程序 说明:以下内容大部分来自腾讯元宝。 在Visual Studio 2022中使用PlaySound()函数播放音频,需完成以下步骤: 1. 配…...
C++相关基础概念之入门讲解(下)
1. 引用 int main() {const int a10;int& aaa;aa;cout<<aa<<endl; } 引用 不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空 间,它和它引用的变量 共用同一块内存空间(初…...
从零开始学可靠消息投递:分布式事务的“最终一致性”方案
一、什么是可靠消息投递?—— 消息队列的“防丢宝典” 可靠消息投递 是指通过消息队列(如 RocketMQ)确保消息在生产、传输、消费过程中不丢失、不重复、有序到达。其核心目标是在分布式系统中保障数据最终一致性,常用于订单处理、…...
生物化学笔记:医学免疫学原理 免疫系统的组成与功能+克隆选择学说
免疫系统的组成与功能 克隆选择学说 克隆选择学说(Clonal Selection Theory)是免疫学的核心理论之一,由 麦克法兰伯内特(Frank Macfarlane Burnet) 在 1957 年提出,用于解释特异性免疫反应的机制。 基本概…...
SpringBoot最佳实践之 - 使用AOP记录操作日志
1. 前言 本篇博客是个人在工作中遇到的需求。针对此需求,开发了具体的实现代码。并不是普适的记录操作日志的方式。以阅读本篇博客的朋友,可以参考此篇博客中记录日志的方式,可能会对你有些许帮助和启发。 2. 需求描述 有一个后台管理系统…...
MySql中 一条select语句的执行流程
一条 SELECT 语句的执行流程涉及到数据库管理系统(DBMS)的多个组件和阶段。以下是一个更为详细的执行流程,以关系型数据库(如 MySQL、PostgreSQL 等)为例: 1. 客户端发送查询 用户输入:用户在客…...
图论——kruskal算法
53. 寻宝(第七期模拟笔试) 题目描述 在世界的某个区域,有一些分散的神秘岛屿,每个岛屿上都有一种珍稀的资源或者宝藏。国王打算在这些岛屿上建公路,方便运输。 不同岛屿之间,路途距离不同,国王希望你可以规划建公路的方案,如何可以以最短的总公路距离将 所有岛屿联通…...
【dify】 dify环境变量配置说明
这是一份Dify平台的环境变量配置文件,对平台的各项功能、服务和组件进行参数设置。以下是对其主要部分的详细解读: 1. 通用变量(Common Variables) CONSOLE_API_URL:控制台API的后端URL,用于拼接授权回调…...
如何在 Vue 项目中实现动态组件加载,有什么应用场景?
大白话如何在 Vue 项目中实现动态组件加载,有什么应用场景? 什么是动态组件加载 在 Vue 项目里,动态组件加载就是能够在程序运行时动态地决定要渲染哪个组件。打个比方,就像你去餐馆点菜,不同的时间你可能想吃不同的…...
FRP在物联网设备中的穿透方案
物联网设备常位于NAT后,FRP为其提供稳定穿透链路。 配置要点 轻量化部署:使用ARM版本FRP客户端,适配树莓派等设备9。 自启动脚本:通过systemd或crontab实现设备重启后自动连接26。 低功耗优化:调整心跳间隔…...
Android 13深度定制:SystemUI状态栏时间居中显示终极实战指南
一、架构设计与技术解析 1. SystemUI状态栏核心布局机制 层级结构 mermaid 复制 graph TDPhoneStatusBarView --> StatusBarContents[status_bar_contents]StatusBarContents --> LeftLayout[status_bar_left_side]StatusBarContents --> ClockLayout[Clock控件]Left…...
Python实战(3)-数据库操作
前面说过,可用的SQL数据库引擎有很多,它们都有相应的Python模块。这些数据库引擎大都作为服务器程序运行,连安装都需要有管理员权限。为降低Python DB API的使用门槛,我选择了一个名为SQLite的小型数据库引擎。它不需要作为独立的…...
【redis】在 Spring中操作 Redis
文章目录 基础设置依赖StringRedisTemplate库的封装 运行StringList删库 SetHashZset 基础设置 依赖 需要选择这个依赖 StringRedisTemplate // 后续 redis 测试的各种方法,都通过这个 Controller 提供的 http 接口来触发 RestController public class MyC…...