深度学习修炼(六)——神经网络分类问题

文章目录

  • 6 分类任务
    • 6.1 前置知识
      • 6.1.1 分类
      • 6.1.2 分类的网络
    • 6.2 动手
      • 6.2.1 读取数据
      • 6.2.2 functional模块
      • 6.2.3 继续搭建分类神经网络
      • 6.2.4 继续简化
      • 6.2.5 训练模型
    • 6.3 暂退法
      • 6.3.1 重新看待过拟合问题
      • 6.3.2 在稳健性中加入扰动
      • 6.3.3 暂退法实际的实现
    • 6.4 后话

6 分类任务

在这一讲中,我们打算探讨一下神经网络中是如何处理分类任务的。

6.1 前置知识

6.1.1 分类

如果只是二分分类,这种分类我们大可只用0或者1来表示,那么我们只需要采用sigmoid函数来修正为0或1即可(如果不懂后面会重新讲sigmoid函数),那如果是多种可能呢?

假设每次输入是一个2×2的灰度图像。我们可以用一个标量表示每个像素值,每个图像对应四个特征x1,x2,x3,x4x_1,x_2,x_3,x_4x1,x2,x3,x4,此外,假设每个图像属于类别"猫",“鸡”,"狗"中的一个。

接下来,我们要选择如何表示标签,如同我们前面所说,我们肯定不可能说P = “猫”,我们有两个明显地选择:最直接的想法是选择y∈{1,2,3},其中正数分别代表{“狗”,“猫”,“鸡”}。

但是统计学家很早之前就发明了一种表示分类数据的简单方法:独热编码。独热编码是一个向量,它的分量和类别一样多。类别对应的分量设置为1,其他所有分量设置为0。比如说应用到我们这个例子上的话,我们就可以用独热编码来表示我们的标签y,其中猫对应(1,0,0),鸡对应(0,1,0),狗对应(0,0,1)。

6.1.2 分类的网络

在神经网络中,我们通常使用softmax回归来进行分类任务。

image-20211227152914400

在分类任务重,我们本质上是在算这个图片是不是某一类的概率,这个概率是要多大才能判定这个图片属于这个类?这个概率要达到什么标准?这个标准是我们自己定的,也就是所谓的阈值。

我们希望把我们通过运算算出来的y^j\hat{y}_jy^j可以直接看做是类别j的概率,但是这明显不可能,因为我们知道概率是处于0到1的,我们又没对这个模型做什么处理,他凭什么输出的值刚好就在0和1之间,是吧。

那么既然没有处理,那接下来就要让输出结果某种处理后,变成0-1之间,这个处理我们就叫做校准。那我们要用什么东西来校准呢?softmax函数。softmax函数可以将未规范化的预测变换为非负并且总和为1,同时要求模型保持可导。其函数形式为:y^=softmax(o),其中y^i=exp(oj)∑kexp(ok)\hat{y} = softmax(o),其中\hat{y}_i = \frac{exp(o_j)}{\sum_kexp(o_k)}y^=softmax(o),y^i=kexp(ok)exp(oj)

这里,对于所有的j总有0<=yj^\hat{y_j}yj^<=1。因此,y^\hat{y}y^可以视为一个正确的概率发布。softmax运算不会改变未规范化的预测o之间的顺序,只会确定分配给每个类别的概率。

从上面的公式我们可以看出,这个公式明显不是线性函数,但是由于我们是先通过输入特征的仿射变换做出o,然后再把o出入上面的softmax函数中的,所以,softmax回归是一个线性模型。

6.2 动手

或许前置的知识再多也是花里胡哨。让我们试着动手搭建一个神经网络来进行分类。不过,我们这一小节的目的是为了熟悉nn.Module模块和nn.functional模块。

6.2.1 读取数据

让我们先读取数据,这里我们暂时不使用前面学过的torch.datasets和torch.utils.data.DataLoader。如果你不是很懂下面的读取数据的代码,没关系,这看得懂看不懂都不是我们学习的重点。你照做即可。

%matplotlib inlinefrom pathlib import Path
import requestsDATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"PATH.mkdir(parents=True, exist_ok=True)URL = "http://deeplearning.net/data/mnist/"
FILENAME = "mnist.pkl.gz"if not (PATH / FILENAME).exists():content = requests.get(URL + FILENAME).content(PATH / FILENAME).open("wb").write(content)import pickle
import gzipwith gzip.open((PATH / FILENAME).as_posix(), "rb") as f:((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")from matplotlib import pyplot
import numpy as nppyplot.imshow(x_train[0].reshape((28, 28)), cmap="gray")
print(x_train.shape)

out:

image-20220401140349705

对于分类任务不像回归,它的输出并不是1,而是类别数。

image-20220401140753163

如果我们要将某样本归为十个类中的一个,那么输出结果就是对应类的独热编码,也就是向量中有10个数字。

举个例子,如果我们输入的样本是1×784,那么我可以在第二层将变为784×128,输出层变为128×10,然后输出的10个值再交由softmax回归将其每个数的范围降到0-1,然后看其属于哪个类别的数字大即属于哪个类别。

6.2.2 functional模块

functional模块有的实际上Module都有,但是在调用一些不涉及参数的例如不涉及w和b的方法时,推荐使用functional。

如,继续上面的例子,我们先将数据分为训练集和测试集,然后将数据转为张量:

import torchx_train, y_train, x_valid, y_valid = map(torch.tensor, (x_train, y_train, x_valid, y_valid)
)
n, c = x_train.shape
x_train, x_train.shape, y_train.min(), y_train.max()
print(x_train, y_train)
print(x_train.shape)
print(y_train.min(), y_train.max())

我们利用functional模块来搭建一个线性回归模型试试:

import torch.nn.functional as F# 指定损失函数为交叉熵
loss_func = F.cross_entropy# 定义模型
def model(xb):return xb.mm(weights) + bias

交叉熵实际上就是对数似然损失函数,这个知识点我们在机器学习的练功方式(十一)——逻辑回归_弄鹊的博客-CSDN博客已经提过了,忘记的可以去复习一下。

# 定义小批量随机梯度下降每次取多少样本
bs = 64
xb = x_train[0:bs]  # a mini-batch from x
yb = y_train[0:bs]
# 初始化随机参数
weights = torch.randn([784, 10], dtype = torch.float,  requires_grad = True) 
bias = torch.zeros(10, requires_grad=True)# 打印损失值
print(loss_func(model(xb), yb))

6.2.3 继续搭建分类神经网络

让我们继续以上的工作。我们利用nn.Module来搭建一个softmax回归分类器。

from torch import nnclass Mnist_NN(nn.Module):def __init__(self):super().__init__()self.hidden1 = nn.Linear(784, 128)self.hidden2 = nn.Linear(128, 256)self.out  = nn.Linear(256, 10)def forward(self, x):x = F.relu(self.hidden1(x))x = F.relu(self.hidden2(x))x = self.out(x)return x

在前面第三讲的时候我们忘记提到的一点是:在利用torch框架搭建神经网络时,无需自己去写一个反向传播,只需写好正向传播即可,对于反向传播torch框架内部已经提供给我们了。

在以上搭建的神经网络中,我们在__ init __()方法中先写好了神经网络的每个层,然后在forward()方法中,我们让x首先输入到第一层中,输出的结果用F中的relu激活函数激活一下再流往下一层。第二层同理,得出的结果也是再次使用relu激活函数激活,然后最终通过输出层输出。

定义好之后,我们需要实例化该神经网络类:

net = Mnist_NN()
print(net)

out:

Mnist_NN(
(hidden1): Linear(in_features=784, out_features=128, bias=True)
(hidden2): Linear(in_features=128, out_features=256, bias=True)
(out): Linear(in_features=256, out_features=10, bias=True)
)

对于net对象,实际上torch为其写好了内置方法net_parameters,其依次返回name,parameter。

#可以打印我们定义好名字里的权重和偏置项for name, parameter in net.named_parameters():print(name, parameter,parameter.size())

out:

image-20220401144218461

6.2.4 继续简化

我们可以利用TensorDataset和DataLoader来继续简化上述的过程。

在前面我们学过,我们小批量随机梯度下降。其简单来说就是取小批量的数据,然后取其损失的均值来代替全局损失。在第二讲我们学过DataLoader,它可以用于每次从总数据中每次抽取batch_size的数据。

而对于TensorDataset,它可以将传入的数据张量转换为torch可识别的dataset格式类型的数据,从而方便让DataLoader处理。

让我们看一下以下的代码:

from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader# 转换DataLoader可处理的类型并且批量提取数据
train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs, shuffle=True)valid_ds = TensorDataset(x_valid, y_valid)
valid_dl = DataLoader(valid_ds, batch_size=bs * 2)
def get_data(train_ds, valid_ds, bs):return (DataLoader(train_ds, batch_size=bs, shuffle=True),DataLoader(valid_ds, batch_size=bs * 2),)

6.2.5 训练模型

一般在训练模型时,我们都会加上model.train()方法,这样模型在训练时会自动加入Bathc Normalization(批量归一化)和Dropout(暂退法),防止过拟合。而在测试模型时,我们一般加上model.eval()方法,这样就不会调用归一化和暂退法。

Dropout下一小节就讲了,这里只要知道它是防止过拟合的即可。

让我们看一下下面的代码:

def loss_batch(model, loss_func, xb, yb, opt=None):# 计算损失值loss = loss_func(model(xb), yb)# 如果优化器不为空if opt is not None:# 执行反向传播loss.backward()# 更新参数opt.step()# 梯度清零opt.zero_grad()# 返回损失总值,和批量x的长度return loss.item(), len(xb)

然后训练:

import numpy as npdef fit(steps, model, loss_func, opt, train_dl, valid_dl):for step in range(steps):model.train()for xb, yb in train_dl:loss_batch(model, loss_func, xb, yb, opt)model.eval()with torch.no_grad():losses, nums = zip(*[loss_batch(model, loss_func, xb, yb) for xb, yb in valid_dl])val_loss = np.sum(np.multiply(losses, nums)) / np.sum(nums)print('当前step:'+str(step), '验证集损失:'+str(val_loss))

指定优化器:

from torch import optim
def get_model():model = Mnist_NN()return model, optim.SGD(model.parameters(), lr=0.001)

调用上面所有写的函数即可:

# 对总数据进行分批量
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
# 拿到模型和优化器
model, opt = get_model()
# 开始训练
fit(25, model, loss_func, opt, train_dl, valid_dl)

out:

[{“metadata”:{“trusted”:true},“cell_type”:“code”,“source”:"# 对总数据进行分批量\ntrain_dl, valid_dl = get_data(train_ds, valid_ds, bs)\n# 拿到模型和优化器\nmodel, opt = get_model()\n# 开始训练\nfit(25, model, loss_func, opt, train_dl, valid_dl)",“execution_count”:null,“outputs”:[{“output_type”:“stream”,“text”:“当前step:0 验证集损失:2.27946646194458\n当前step:1 验证集损失:2.2453414649963377\n当前step:2 验证集损失:2.1901785594940186\n当前step:3 验证集损失:2.0985936725616456\n当前step:4 验证集损失:1.9537098587036132\n当前step:5 验证集损失:1.749969289779663\n当前step:6 验证集损失:1.4996562250137329\n当前step:7 验证集损失:1.2475741912841798\n当前step:8 验证集损失:1.041046302986145\n当前step:9 验证集损失:0.8889363418579102\n当前step:10 验证集损失:0.7784037446022034\n当前step:11 验证集损失:0.6955419854164123\n当前step:12 验证集损失:0.6328013188362122\n当前step:13 验证集损失:0.5834899019241333\n当前step:14 验证集损失:0.5442861658096313\n当前step:15 验证集损失:0.5122919623851776\n当前step:16 验证集损失:0.4864934079647064\n当前step:17 验证集损失:0.46465890626907347\n当前step:18 验证集损失:0.4461087041854858\n当前step:19 验证集损失:0.4306937822341919\n当前step:20 验证集损失:0.4172893476009369\n当前step:21 验证集损失:0.40593954257965087\n”,“name”:“stdout”}]}]

6.3 暂退法

在前一小节中我们提到了dropout暂退法。在这一小节中,我们来着重谈论一下。

6.3.1 重新看待过拟合问题

当面对更多的特征而样本不足时,线性模型往往会过拟合。相反,当给出更多的样本而不是特征,通常线性模型不会过拟合。不幸的是,线性模型泛化的可靠性是由代价的。简单地说,线性模型没有考虑到特征之间的交互作用。对于每个特征,线性模型都必须指定正的或负的权重。

泛化小和灵活性之间的这种基本权衡被描述为偏差——方差权衡。线性模型有很高的偏差:它们只能表示一小类函数。然而,这些模型的方差很低:它们在不同的随机数据样本上可以得出了相似的结果。

如果把偏差——方差看成一个色谱,那么与之相反的一端的是深度神经网络。神经网络并不局限与单独查看每个特征,而是学习特征之间的交互。例如:神经网络可能推断“尼日利亚”和“西联汇款”一起出现在电子邮件中表示垃圾邮件,但单独出现则不表示垃圾邮件。

即使我们有比特征多得多的样本,深度神经网络也有可能过拟合。

6.3.2 在稳健性中加入扰动

在探究泛化性之前,我们先来定义一个什么是一个“好”的预测模型。前面我们说过,我们希望训练误差和泛化误差之间折中,做到一个模型能够很好地拟合训练数据,也能够很好地预测未知数据。所以根据经典泛化理论 :为了缩小训练和测试性能之间的差距,应该以简单的模型为目标。简单些以较小维度的形式展现。这也是为什么开头我们用了最简单的线性模型来讲解原理的缘故。而正则化也是一样,为了说明我们是要减少特征向量的大小,我们直接对特征向量大小对应的概念——范数来下手。

简单性的另一个角度是平滑性。即函数不应该对其输入的微小变化敏感(这个也叫鲁棒性)。这也是为什么我们第一章线性模型所需的数据中我们往里添加了噪声点。因为一个好的模型不应该因为某些微小的噪声就失效。

所以根据上面所说的原理,科学家提出了一个想法:在训练过程中,在进行后续层的计算(我们的计算比如第一层输入层是不计算的,主要的计算在第二层)之前,我们先对网络添加小部分噪声。因为当训练一个有多层的深层网络时,注入噪声只会在输入——输出映射上增强平滑性。

这个想法被称为暂退法也叫丢弃法。暂退法在前向传播的过程中,计算每一内部层的同时注入噪声,这已经称为训练神经网络的常用技术。这种方法之所以被称为暂退法,是因为我们从表面上看是在训练的过程中丢弃一些神经元。在整个训练过程的每一次迭代中,标准暂退法包括在计算下一层之前将当前层中的一些结点置零。

6.3.3 暂退法实际的实现

回想之前做的多层感知机。如果我们将暂退法应用到隐藏层,以p的概率将隐藏单元置为零时,结果可以看作是只包含原始神经元子集的网络。比如在下图中,删除了h2和h5,因此输出的计算不再依赖于h2和h5,并且它们各自的梯度在执行反向传播时也会消失。这样,输出层的计算不能过度依赖于h1-h5的任何一个元素。

image-20220102145259596

通常,我们在测试的时候不会用到暂退法。给定一个训练好的模型和一个新的样本,我们不会丢弃任何节点,因此不需要标准化。但是研究人员就需要去用暂退法去估计神经网络预测的不确定性了;如果通过应用不同暂退法(一种暂退法包含抹去某些节点)得到的预测结果都是一致的,那么我们可以说网络发挥更稳定。

6.4 后话

我知道,这一小节你学的很不愉快。实际上,太多细致的知识无法在一个小节中直接说完,所以请不要紧张,这里即使你不懂太多的东西,你只要会一个就行了——搭建个网络,即使不会训练也没有关系,先会搭网络就足够了。在后面的学习中你会发现,除了搭网络这个地方不一样,实际上训练的过程都是一样的。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度学习修炼(七)——卷积神经网络

文章目录7 卷积神经网络7.1 卷积网络和传统网络的区别7.2 卷积7.2.1 卷积过程画大饼7.2.2 图像的不变性7.2.3 互相关运算*(补充)7.2.4 图像颜色通道*(补充)7.2.5 步幅7.2.6 多次卷积7.2.7 边缘填充7.2.8 特征图的大小7.2.9 卷积参数共享7.3 池化7.4 整体网络架构7.5 后话7 卷积…

网络爬虫(一)——爬虫及其实现

文章目录1.1 爬虫概述1.1.3 网络爬虫和浏览器的区别1.1.2 网络爬虫的定义1.2 requests请求库1.2.1 requests基本概念1.2.2 疫情数据爬取1.2.3 get请求1.2.4 headers请求头1.2.5 Cookies验证1.3 Beautiful Soup解析库1.3.1 安装1.3.2 对象的创建1.3.3 find方法1.3.4 后话1.4 正则…

Windows五种IO模型性能分析和Linux五种IO模型性能分析

Windows五种IO模型性能分析和Linux五种IO模型性能分析 http://blog.csdn.net/jay900323/article/details/18141217 http://blog.csdn.net/jay900323/article/details/18140847 重叠I/O模型的另外几个优点在于&#xff0c;微软针对重叠I/O模型提供了一些特有的扩展函数。当使用重…

C++从0到1的入门级教学(十一)——友元

文章目录11 友元11.1 全局函数做友元11.2 友元类11.3 成员函数做友元11 友元 让我们引入一个例子来讲述友元是什么。 生活中你的家有客厅&#xff0c;有卧室&#xff0c;客厅所有来的客人都可以进去&#xff0c;但是你的卧室是私有的。对于认识的人来说你可以允许它进去&…

KeyMob:为国内应用开发者管理的广告聚合平台

为什么80%的码农都做不了架构师&#xff1f;>>> 应用开发者在应用中嵌入广告SDK的来源主要包括两种&#xff1a;使用移动广告平台与移动广告聚合平台。国内有多少家提供移动广告管理的平台&#xff1f;据统计&#xff0c;这两个版本&#xff0c;已经有四五十家。虽…

模拟航班查询及预定系统 编写示例

一、建立C#窗体 所需控件&#xff1a; Label标签 Button 按钮 TextBox 文本框 ComboBox 组合框 DATaGridView 数据显示 DateTimePicker 日期表 NumericUpDown 数字选择 二、建立后台数据库 大概需要四张表 1&#xff0c;航空公司表 2&#xff0c;城市信息表 3&#xff0c;航班…

数据结构杂谈(七)——串

文章目录7 串7.1 基本知识7.1.1 串的定义:rose:定义:rose:各种概念:rose:字符串和线性表的区别7.1.2 串的抽象类型数据定义7.1.3 串的比较:rose:原理7.2 串的存储结构:rose:7.2.1串的顺序存储:rose:7.2.2 串的链式存储7.3 基本操作:rose:7.3.1 返回子串操作:rose:7.3.2 比较操作…

Linux实现的IEEE 802.q VLAN

本文转载自&#xff1a; http://blog.chinaunix.net/uid-20786208-id-4291059.html Technorati 标签: Linux VLAN--------------------------我是快乐的分割线-------------------------------------------------- 第一部分&#xff1a;VLAN的核心概念 说起IEEE 802.1q&#xf…

C++从0到1的入门级教学(十二)——运算符重载

文章目录12 运算符重载12.1 加法运算符重载12.2 左移运算符重载12.2.1 演示与说明12.2.2 常见的友元使用&#xff1a;重载>>运算符12.3 递增运算符重载12.4 赋值运算符重载12.5 关系运算符重载12.6 函数调用运算符重载12 运算符重载 在本讲中&#xff0c;我们会设计到一…

C++从0到1的入门级教学(十三)——继承

文章目录13 继承13.1 继承的基本语法13.2 继承方式13.3 继承的对象模型13.4 继承中构造和析构顺序13.5 继承同名成员处理方式13.6 继承同名静态成员处理方式13.7 多继承语法13.8 菱形继承13 继承 继承是面向对象三大特性之一。有些类和类之间存在特殊的关系&#xff0c;如下图…

线性代数(二)

2 解线性方程组 1 Ax b的列图像实质是A的列向量有各种线性组合&#xff0c;b为其中的一种组合结果。 2 Ax b可以写为Axx1a1...xnanbAx x_1a_1...x_na_n bAxx1​a1​...xn​an​b&#xff0c;其中a1,a2...ana_1,a_2...a_na1​,a2​...an​为A中的列向量。 3 当Ax 0时&#…

xor方程组消元 UVA 11542 Square

题目传送门 题意&#xff1a;给n个数&#xff0c;选择一些数字乘积为平方数的选择方案数。训练指南题目。 分析&#xff1a;每一个数字分解质因数。比如4&#xff0c; 6&#xff0c; 10&#xff0c; 15&#xff0c;, , , , 令&#xff0c;表示选择第i个数字&#xff0c;那么&am…

深度学习修炼(八)——经典卷积网络

文章目录8 经典卷积网络8.1 LeNet模型8.2 Alexnet8.3 VGG8.4 ResNet8.5 感受野8 经典卷积网络 在前面一讲&#xff0c;我们谈论了关于卷积神经网络的诸多细节。综合来讲&#xff0c;卷积神经网络就是含卷积层的网络。在本讲中&#xff0c;我们将会根据卷积神经网络发展的历史&…

视觉中的经典图像特征小结(一): 颜色直方图, HOG, LBP

[普兒原创, 如有错误和纰漏欢迎指正. 更新中...] 1. 颜色直方图 颜色空间在本质上是定义在某种坐标系统下的子空间&#xff0c;空间中的每一个坐标表示一种不同的颜色。颜色空间的目的在于给出某种颜色标准&#xff0c;使得不同的设备和用途都能对颜色有一致的描述。这里主要介…

C++从0到1的入门级教学(七)——指针

文章目录7 指针7.1 指针的基本概念7.2 指针变量的定义和使用7.3 指针所占内存空间7.4 空指针7.5 野指针7.6 void*指针7.7 指向指针的指针7.8 const修饰指针7.9 指针和数组7.10 指针和函数7 指针 指针是指向另外一种类型的符合类型&#xff0c;和引用类似&#xff0c;指针也实现…

C++从0到1的入门级教学(五)——字符串、向量和数组

文章目录5 字符串、向量和数组5.1 命名空间5.2 标准库string5.2.1 定义和初始化string对象5.2.2 string对象上的操作5.2.2.1 读取string对象5.2.2.2 风格5.2.2.3 使用getline读取一整行5.2.2.4 empty和size操作5.2.2.5 size_type类型5.2.2.6 比较string对象5.2.2.7 string对象的…

媒体格式分析之flv -- 基于FFMPEG

本来是应该先写一个媒体文件格式的简单讲解的&#xff0c;还没来得及写&#xff0c;以后再写。今天就先根据ffmpeg的flv.c的flv_demux这个结构体来讲解一下当前比较流行的媒体格式flv. FLV 是FLASH VIDEO的简称&#xff0c;FLV流媒体格式是随着Flash MX的推出发展而来的视频格式…

C++从0到1的入门级教学(三)——表达式和运算符

文章目录3 运算符3.1 表达式3.1.1 基本概念3.1.2 运算符和运算对象3.1.3 运算对象的转换3.1.4 左值和右值3.2 运算符3.2.1 算术运算符3.2.2 赋值运算符3.2.3 比较运算符3.2.4 逻辑运算符3.2.5 成员访问运算符3.2.6 条件运算符3 运算符 C提供了一套供操作内置数据类型的运算符&…

谈谈用SQLite和FMDB而不用Core Data

谈谈用SQLite和FMDB而不用Core Data 发布于&#xff1a;2014-04-22 11:22阅读数&#xff1a;4235 凭良心讲&#xff0c;我不能告诉你不去使用Core Data。它不错&#xff0c;而且也在变好&#xff0c;并且它被很多其他Cocoa开发者所理解&#xff0c;当有新人加入你的组或者需要别…

Idea工具开发 SpringBoot整合JSP(毕设亲测可用)

因为&#xff0c;临近毕业了&#xff0c;自己虽然也学了很多框架。但是&#xff0c;都是在别人搭建好的基础上进行项目开发。但是springboot的官方文档上明确指出不提倡使用jsp进行前端开发&#xff0c;但是在校期间只学了jsp作为前端页面。所以&#xff0c;废话不多说&#xf…