Pytorch实战(二):VGG神经网络

文章目录

  • 一、诞生背景
  • 二、VGG网络结构
    • 2.1VGG块
    • 2.2网络运行流程
    • 2.3总结
  • 三、实战
    • 3.1搭建模型
    • 3.2模型训练
    • 3.3训练结果可视化
    • 3.4模型参数初始化


一、诞生背景

在这里插入图片描述
在这里插入图片描述
  从网络结构中可看出,所有版本VGG均全部使用3×3大小、步长为1的小卷积核,3×3卷积核同时也是最小的能够表示上下左右中心的尺寸。
在这里插入图片描述
假设输入图像尺寸为假输入为5×5,使用2次3×3卷积后最终得到1×1的特征图,那么这个1×1的特征图的感受野为5×5。这和直接使用一个5×5卷积核得到1×1的特征图是一样的。也就是说2次3×3卷积可以代替一次5×5卷积同时,并且,2次3×3卷积的参数更少(2×3×3=18<5×5=25)而且会经过两次激活函数进行非线性变换,学习能力会更好。同样的3次3×3卷积可以替代一次7×7的卷积。并且,步长为1可以不会丢失信息,网络深度增加可以提高网络性能。

二、VGG网络结构

2.1VGG块

在这里插入图片描述
一个VGG_bolck的组成:

  • 带填充以保持分辨率的卷积层:指对输入特征图卷积操作时会带有填充,使得只改变通道数而不改变图像高、宽。
  • 非线性激活函数ReLU:卷积操作后将特征图输入激活函数,提供使之具有非线性性。
  • 池化层、最大池化层:使用最大池化函数,不改变图像通道数,但会缩小图像尺寸。

对于卷积层、池化层有:

  • 卷积层:使用3x3大小的卷积核,padding=1,stride=1,output=(input-3+2×1)/1+1=input,使得特征图尺寸不变。
  • 池化层:使用2x2大小的核,padding=0,stride=2,output=(input-2)/2+1=1/2input,特征图尺寸减半。

2.2网络运行流程

输入层:输入大小为 ( 224 , 224 , 3 ) (224,224,3) (224,224,3)的RGB图像。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.3总结

在这里插入图片描述

三、实战

3.1搭建模型

import torch
from torch import nn
from torchsummary import summary

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")


class VGG16(nn.Module):
    def __init__(self):
        super(VGG16, self).__init__()
        self.block1 = nn.Sequential(
            # 本案例中使用FashionMNIST数据集,所以输入通道数为1
            nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.block2 = nn.Sequential(
            nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.block3 = nn.Sequential(
            nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.block4 = nn.Sequential(
            nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.block5 = nn.Sequential(
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.fc = nn.Sequential(
            nn.Flatten(),
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(4096, 10)
        )

    def forward(self, x):
        x = self.block1(x)
        x = self.block2(x)
        x = self.block3(x)
        x = self.block4(x)
        x = self.block5(x)
        x = self.fc(x)
        return x


model = VGG16().to(device)
summary(model, (1, 224, 224))

在这里插入图片描述

3.2模型训练

  使用模板:

import torch
from torch import nn
import copy
import time
from torchvision.datasets import FashionMNIST
from torchvision import transforms
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import torch.utils.data as Data
train_data = FashionMNIST(root='./', train=True, download=True,
                               transform=transforms.Compose([transforms.Resize(size=224), transforms.ToTensor()]))
def train_val_process(train_data, batch_size=128):
    train_data, val_data = Data.random_split(train_data,
                                             lengths=[round(0.8 * len(train_data)), round(0.2 * len(train_data))])
    train_loader = Data.DataLoader(dataset=train_data,
                                   batch_size=batch_size,
                                   shuffle=True,
                                   num_workers=8)
    val_loader = Data.DataLoader(dataset=val_data,
                                 batch_size=batch_size,
                                 shuffle=True,
                                 num_workers=8)
    return train_loader, val_loader

train_dataloader, val_dataloader = train_val_process(train_data, batch_size=64)

def train(model, train_dataloader, val_dataloader, epochs=30, lr=0.001, model_saveName=None, model_saveCsvName=None ):
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    criterion = nn.CrossEntropyLoss()

    model = model.to(device)
    # 复制当前模型的参数
    best_model_params = copy.deepcopy(model.state_dict())
    # 最高准确率
    best_acc = 0.0
    # 训练集损失函数列表
    train_loss_list = []
    # 验证集损失函数列表
    val_loss_list = []
    # 训练集精度列表
    train_acc_list = []
    # 验证集精度列表
    val_acc_list = []
    # 记录当前时间
    since = time.time()
    for epoch in range(epochs):
        print("Epoch {}/{}".format(epoch + 1, epochs))
        print("-" * 10)
        # 当前轮次训练集的损失值
        train_loss = 0.0
        # 当前轮次训练集的精度
        train_acc = 0.0
        # 当前轮次验证集的损失值
        val_loss = 0.0
        # 当前轮次验证集的精度
        val_acc = 0.0
        # 训练集样本数量
        train_num = 0
        # 验证集样本数量
        val_num = 0
        # 按批次进行训练
        for step, (x, y) in enumerate(train_dataloader):  # 取出一批次的数据及标签
            x = x.to(device)
            y = y.to(device)
            # 设置模型为训练模式
            model.train()
            out = model(x)
            # 查找每一行中最大值对应的行标,即为对应标签
            pre_label = torch.argmax(out, dim=1)
            # 计算损失函数
            loss = criterion(out, y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            # 累计损失函数,其中,loss.item()是一批次内每个样本的平均loss值(因为x是一批次样本),乘以x.size(0),即为该批次样本损失值的累加
            train_loss += loss.item() * x.size(0)
            # 累计精度(训练成功的样本数)
            train_acc += torch.sum(pre_label == y.data)
            # 当前用于训练的样本数量(对应dim=0)
            train_num += x.size(0)
        # 按批次进行验证
        for step, (x, y) in enumerate(val_dataloader):
            x = x.to(device)
            y = y.to(device)
            # 设置模型为验证模式
            model.eval()
            torch.no_grad()
            out = model(x)
            # 查找每一行中最大值对应的行标,即为对应标签
            pre_label = torch.argmax(out, dim=1)
            # 计算损失函数
            loss = criterion(out, y)
            # 累计损失函数
            val_loss += loss.item() * x.size(0)
            # 累计精度(验证成功的样本数)
            val_acc += torch.sum(pre_label == y.data)
            # 当前用于验证的样本数量
            val_num += x.size(0)
        # 计算该轮次训练集的损失值(train_loss是一批次样本损失值的累加,需要除以批次数量得到整个轮次的平均损失值)
        train_loss_list.append(train_loss / train_num)
        # 计算该轮次的精度(训练成功的总样本数/训练集样本数量)
        train_acc_list.append(train_acc.double().item() / train_num)
        # 计算该轮次验证集的损失值
        val_loss_list.append(val_loss / val_num)
        # 计算该轮次的精度(验证成功的总样本数/验证集样本数量)
        val_acc_list.append(val_acc.double().item() / val_num)
        # 打印训练、验证集损失值(保留四位小数)
        print("轮次{} 训练 Loss: {:.4f}, 训练 Acc: {:.4f}".format(epoch+1, train_loss_list[-1], train_acc_list[-1]))
        print("轮次{} 验证 Loss: {:.4f}, 验证 Acc: {:.4f}".format(epoch+1, val_loss_list[-1], val_acc_list[-1]))
        # 如果当前轮次验证集精度大于最高精度,则保存当前模型参数
        if val_acc_list[-1] > best_acc:
            # 保存当前最高准确度
            best_acc = val_acc_list[-1]
            # 保存当前模型参数
            best_model_params = copy.deepcopy(model.state_dict())
            print("保存当前模型参数,最高准确度: {:.4f}".format(best_acc))
        # 训练耗费时间
        time_use = time.time() - since
        print("当前轮次耗时: {:.0f}m {:.0f}s".format(time_use // 60, time_use % 60))
    # 加载最高准确率下的模型参数,并保存模型
    torch.save(best_model_params, model_saveName)
    train_process = pd.DataFrame(data={'epoch': range(epochs),
                                       'train_loss_list': train_loss_list,
                                       'train_acc_list': train_acc_list,
                                       'val_loss_list': val_loss_list,
                                       'val_acc_list': val_acc_list
                                       })
    train_process.to_csv(model_saveCsvName, index=False)
    return train_process
model_saveName="VGG16_best_model.pth"
model_saveCsvName="VGG16_train_process.csv"
train_process = train(model, train_dataloader, val_dataloader, epochs=15, lr=0.001, model_saveName=model_saveName, model_saveCsvName=model_saveCsvName)

3.3训练结果可视化

def train_process_visualization(train_process):
    plt.figure(figsize=(12, 4))
    plt.subplot(1, 2, 1)
    plt.plot(train_process['epoch'], train_process['train_loss_list'], 'ro-', label='train_loss')
    plt.plot(train_process['epoch'], train_process['val_loss_list'], 'bs-', label='val_loss')
    plt.legend()
    plt.xlabel('epoch')
    plt.ylabel('loss')

    plt.subplot(1, 2, 2)
    plt.plot(train_process['epoch'], train_process['train_acc_list'], 'ro-', label='train_acc')
    plt.plot(train_process['epoch'], train_process['val_acc_list'], 'bs-', label='val_acc')
    plt.legend()
    plt.xlabel('epoch')
    plt.ylabel('acc')
    plt.legend()
    plt.show()
train_process_visualization(train_process)

  训练后可能会出现如下结果:
在这里插入图片描述
训练结果可能会时好时坏。事实上,VGG16共有16层网络,当进行反向传播从输出层向输入层运算时,可能会出现梯度消失使得参数无法收敛的情况。由于参数初始化是随机的,可能相对于真实值过大或过小,此时梯度消失就可能会使得参数值无法收敛。此时就需要按照一定的方式初始化参数。

3.4模型参数初始化

import torch
from torch import nn

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")


class VGG16(nn.Module):
    def __init__(self):
        super(VGG16, self).__init__()
        self.block1 = nn.Sequential(
            # 本案例中使用FashionMNIST数据集,所以输入通道数为1
            nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.block2 = nn.Sequential(
            nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.block3 = nn.Sequential(
            nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.block4 = nn.Sequential(
            nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.block5 = nn.Sequential(
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.fc = nn.Sequential(
            nn.Flatten(),
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(4096, 10)
        )
        # 参数初始化
        for m in self.modules():
            # 判断是否是具有参数的网络层,无参数就无需初始化
            if isinstance(m, nn.Conv2d):
                # Kaiming初始化方法常用于初始化卷积层参数w,需指定下一层使用的激活函数
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
                if m.bias is not None: # 偏置项bias有约定俗成的初始化方式(初始化为0)
                    nn.init.constant_(m.bias, val=0)
            elif isinstance(m, nn.Linear):
                # 全连接层参数初始化往往使用正态分布的方式
                nn.init.normal_(m.weight, mean=0, std=0.01)
                nn.init.zeros_(m.bias)
                if m.bias is not None:
                    nn.init.constant_(m.bias, val=0)


    def forward(self, x):
        x = self.block1(x)
        x = self.block2(x)
        x = self.block3(x)
        x = self.block4(x)
        x = self.block5(x)
        x = self.fc(x)
        return x


model = VGG16().to(device)

事实上,批次大小会影响模型学习到的特征和参数更新的方向。较大的批次可以获得更稳定的梯度更新,但可能会丢失一些细节信息;较小的批次则可以捕捉到更细节的模式,但更新的梯度可能会更加不稳定。合理的批次大小选择可以在训练速度和模型性能之间达到平衡。一般的,建议批次大小为64、128左右,而若硬件性能不够,也可通过减少全连接层参数个数以换取较大的批次,因为全连接层参数过多,往往并不全都需要:

self.fc = nn.Sequential(
    nn.Flatten(),
    nn.Linear(512 * 7 * 7, 256),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(256, 128),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(128, 10)
)

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

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

相关文章

Linux网络配置管理

目录 一、网络配置 1. 网卡配置 2. 路由 二、 网络信息查看 1.netstat 2. ss 三、 额外的命令 time 一、网络配置 之前我们学过 ifconfig &#xff0c;这个命令可以查看网络接口的地址配置信息&#xff0c;我们只知道它可以查看接口名称、IP 地址、子网掩码等。 但是&a…

java —— tomcat 部署项目

一、通过 war 包部署 1、将项目导出为 war 包&#xff1b; 2、将 war 包放置在 tomcat 目录下的 webapps 文件夹下&#xff0c;该 war 包稍时便自动解析为项目文件夹&#xff1b; 3、启动 tomcat 的 /bin 目录下的 startup.bat 文件&#xff0c;此时即可从浏览器访问项目首页…

windows 11 + kali wsl二合一配置步骤与踩坑

windows 11 kali wsl二合一配置步骤与踩坑 在前几天的某市攻防演练中&#xff0c;在攻防前期&#xff0c;我的虚拟机经常无缘无故出现断网、卡顿等现象&#xff0c;但找不出原因。 为了不影响后续的这些天的攻防演练&#xff0c;我选择在一个晚上通宵 在我的windows 11系统上…

2.作业2

目录 1.作业题目 A图 B代码 2.css盒子模型 0.css盒子模型 1.外边距&#xff08;margin&#xff09; 2.边框&#xff08;border&#xff09; 3.内边距&#xff08;padding&#xff09; ​编辑 3.GET方法与POST方法的区别 学习产出&#xff1a; html的作业 1.作业题目 A图…

无向图中寻找指定路径:深度优先遍历算法

刷题记录 1. 节点依赖 背景: 类似于无向图中, 寻找从 起始节点 --> 目标节点 的 线路. 需求: 现在需要从 起始节点 A, 找到所有到 终点 H 的所有路径 A – B &#xff1a; 路径由一个对象构成 public class NodeAssociation {private String leftNodeName;private Stri…

文华财经盘立方期货通鳄鱼指标公式均线交易策略源码

文华财经盘立方期货通鳄鱼指标公式均线交易策略源码&#xff1a; 新建主图幅图类型指标都可以&#xff01; VAR1:(HL)/2; 唇:REF(SMA(VAR1,5,1),3),COLORGREEN; 齿:REF(SMA(VAR1,8,1),5),COLORRED; 颚:REF(SMA(VAR1,13,1),8),COLORBLUE;

离线查询+线段树,CF522D - Closest Equals

一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 522D - Closest Equals 二、解题报告 1、思路分析 考虑查询区间已经给出&#xff0c;我们可以离线查询 对于这类区间离线查询的问题我们通常可以通过左端点排序&#xff0c;然后遍历询问同时维护左区间信息…

数据泄露态势(2024年5月)

监控说明&#xff1a;以下数据由零零信安0.zone安全开源情报系统提供&#xff0c;该系统监控范围包括约10万个明网、深网、暗网、匿名社交社群威胁源。在进行抽样事件分析时&#xff0c;涉及到我国的数据不会选取任何政府、安全与公共事务的事件进行分析。如遇到影响较大的伪造…

《金山 WPS AI 2.0:重塑办公未来的智能引擎》

AITOP100平台获悉&#xff0c;在 2024 世界人工智能大会这一科技盛宴上&#xff0c;金山办公以其前瞻性的视野和创新的技术&#xff0c;正式发布了 WPS AI 2.0&#xff0c;犹如一颗璀璨的星辰&#xff0c;照亮了智能办公的新征程&#xff0c;同时首次公开的金山政务办公模型 1.…

支持图片识别语音输入的LobeChat保姆级本地部署流程

文章目录 前言1. LobeChat对我们有哪些帮助?2. 本地安装LobeChat3. 如何使用LobeChat工具4. 安装Cpolar内网穿透5. 实现公网访问LobeChat6. 固定LobeChat公网地址 前言 本文主要介绍如何在Windows系统电脑本地部署LobeChat&#xff0c;一款高颜值的开源AI大模型智能应用&…

5-google::protobuf命名空间下常用的C++ API----message.h

#include <google/protobuf/message.h> namespace google::protobuf 假设您有一个消息定义为: message Foo {optional string text 1;repeated int32 numbers 2; } 然后&#xff0c;如果你使用 protocol编译器从上面的定义生成一个类&#xff0c;你可以这样使用它: …

Studying-代码随想录训练营day31| 56.合并区间、738.单调递增的数字、968.监控二叉树、贪心算法总结

第31天&#xff0c;贪心最后一节(ง •_•)ง&#x1f4aa;&#xff0c;编程语言&#xff1a;C 目录 56.合并区间 738.单调递增的数字 968.监控二叉树 贪心算法总结 56.合并区间 文档讲解&#xff1a;代码随想录合并区间 视频讲解&#xff1a;手撕合并区间 题目&#xf…

C语言下结构体、共用体、枚举类型的讲解

主要内容 结构体结构体数组结构体指针包含结构体的结构链表链表相关操作共用体枚举类型 结构体 结构体的类型的概念 结构体实现步骤 结构体变量的声明 struct struct 结构体名{ 数据类型 成员名1; 数据类型 成员名2; ..…

绝地求生PUBG兰博基尼怎么兑换 兰博基尼怎么获得

绝地求生采用虚幻4引擎制作&#xff0c;玩家们会在一个偏远的岛屿上出生&#xff0c;然后展开一场赢家通吃的生存竞赛&#xff0c;最后只会有1个人存活。当然&#xff0c;和其他生存游戏一样&#xff0c;玩家需要在广袤复杂的地图中收集武器、车辆、物资&#xff0c;而且也会有…

解决win10报“无法加载文件……profile.ps1,因为在此系统上禁止运行脚本”的问题

打开命令行报错 解决方法 使用管理员权限打开PowerShell&#xff1a;WinX, 选择“Windows PowerShell&#xff08;管理员&#xff09;” 输入&#xff1a;Set-ExecutionPolicy -ExecutionPolicy RemoteSigned 输入&#xff1a;y确认修改安全策略 &#xff1a;y确认修改安全策略…

探讨大数据在视频汇聚平台LntonCVS国标GB28181协议中的应用

随着摄像头和视频监控系统的普及和数字化程度的提高&#xff0c;视频监控系统产生的数据量急剧增加。大数据技术因其优秀的数据管理、分析和利用能力&#xff0c;成为提升视频监控系统效能和价值的重要工具。 大数据技术可以将视频监控数据与其他数据源进行融合分析&#xff0c…

【elasticsearch】IK分词器添加自定义词库,然后更新现有的索引

进入elasticsearch中的plugins位置&#xff0c;找到ik分词器插件&#xff0c;进入ik插件的config文件夹&#xff0c;当中有一个IKAnalyzer.cfg.xml配置文件。使用vim编辑器修改配置文件&#xff1a; vim IKAnalyzer.cfg.xml 配置文件如下&#xff08;添加了自定义字典的位置&…

pygame 音乐粒子特效

代码 import pygame import numpy as np import pymunk from pymunk import Vec2d import random import librosa import pydub# 初始化pygame pygame.init()# 创建屏幕 screen pygame.display.set_mode((1920*2-10, 1080*2-10)) clock pygame.time.Clock()# 加载音乐文件 a…

人工智能的新时代:从模型到应用的转变

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…