注意
跳转至页面底部 下载完整示例代码。
torch.nn 到底是什么?#
创建日期:2018年12月26日 | 最后更新:2025年1月24日 | 最后验证:2024年11月5日
作者: Jeremy Howard, fast.ai。感谢 Rachel Thomas 和 Francisco Ingham。
我们建议将此教程作为 Notebook 而非脚本运行。要下载 Notebook (.ipynb) 文件,请点击页面顶部的链接。
PyTorch 提供了设计精良的模块和类:torch.nn、torch.optim、Dataset 和 DataLoader,旨在帮助你创建和训练神经网络。为了充分利用它们的功能并针对你的问题进行定制,你需要确切地理解它们在做什么。为了建立这种理解,我们将首先在 MNIST 数据集上训练一个基础神经网络,而不使用这些模型的任何高级特性;起初我们只使用最基础的 PyTorch 张量功能。然后,我们将一次一个地逐步添加 torch.nn、torch.optim、Dataset 或 DataLoader 中的特性,确切展示每个组件的作用,以及它是如何让代码变得更加简洁或灵活的。
本教程假设你已经安装了 PyTorch,并且熟悉张量操作的基础知识。(如果你熟悉 Numpy 数组操作,你会发现这里使用的 PyTorch 张量操作几乎是一样的)。
MNIST 数据设置#
我们将使用经典数据 MNIST,它由手写数字(0 到 9)的黑白图像组成。
我们将使用 pathlib 来处理路径(它是 Python 3 标准库的一部分),并使用 requests 下载数据集。我们只在用到模块时才导入它们,这样你可以清楚地看到每个环节所使用的内容。
from pathlib import Path
import requests
DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"
PATH.mkdir(parents=True, exist_ok=True)
URL = "https://github.com/pytorch/tutorials/raw/main/_static/"
FILENAME = "mnist.pkl.gz"
if not (PATH / FILENAME).exists():
content = requests.get(URL + FILENAME).content
(PATH / FILENAME).open("wb").write(content)
该数据集采用 numpy 数组格式,并使用 pickle 存储(这是一种 Python 特有的数据序列化格式)。
每张图片大小为 28 x 28,存储为长度为 784 (=28x28) 的展平行。让我们先看看其中一张;我们需要先将其重塑为 2 维。
from matplotlib import pyplot
import numpy as np
pyplot.imshow(x_train[0].reshape((28, 28)), cmap="gray")
# ``pyplot.show()`` only if not on Colab
try:
import google.colab
except ImportError:
pyplot.show()
print(x_train.shape)

(50000, 784)
PyTorch 使用 torch.tensor 而不是 numpy 数组,因此我们需要转换我们的数据。
import torch
x_train, y_train, x_valid, y_valid = map(
torch.tensor, (x_train, y_train, x_valid, y_valid)
)
n, c = x_train.shape
print(x_train, y_train)
print(x_train.shape)
print(y_train.min(), y_train.max())
tensor([[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
...,
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.]]) tensor([5, 0, 4, ..., 8, 4, 8])
torch.Size([50000, 784])
tensor(0) tensor(9)
从零开始构建神经网络(不使用 torch.nn)#
让我们先仅使用 PyTorch 张量操作来创建一个模型。我们假设你已经熟悉神经网络的基础知识。(如果你不熟悉,可以在 course.fast.ai 学习)。
PyTorch 提供了创建随机或全零张量的方法,我们将使用它们为简单的线性模型创建权重和偏差。这些只是普通的张量,但有一个非常特殊的补充:我们告诉 PyTorch 它们需要梯度。这使得 PyTorch 会记录对张量执行的所有操作,以便在反向传播过程中自动计算梯度!
对于权重,我们在初始化之后设置 requires_grad,因为我们不希望把初始化这一步也计入梯度。(注意,PyTorch 中以 _ 结尾的操作表示原地(in-place)操作。)
注意
我们在这里使用 Xavier 初始化(通过乘以 1/sqrt(n))来初始化权重。
import math
weights = torch.randn(784, 10) / math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)
得益于 PyTorch 自动计算梯度的能力,我们可以使用任何标准的 Python 函数(或可调用对象)作为模型!因此,我们直接写一个简单的矩阵乘法和广播加法来创建一个简单的线性模型。我们还需要一个激活函数,所以我们编写 log_softmax 并使用它。记住:虽然 PyTorch 提供了许多预定义的损失函数、激活函数等,但你可以使用纯 Python 轻松编写自己的函数。PyTorch 甚至会自动为你的函数生成快速的加速器代码或向量化 CPU 代码。
在上面,@ 代表矩阵乘法操作。我们将对一批数据(本例中为 64 张图像)调用该函数。这就是一次前向传播(forward pass)。请注意,由于我们是从随机权重开始的,此时我们的预测效果不会比随机猜好多少。
bs = 64 # batch size
xb = x_train[0:bs] # a mini-batch from x
preds = model(xb) # predictions
preds[0], preds.shape
print(preds[0], preds.shape)
tensor([-2.4298, -1.8243, -2.2278, -2.1598, -2.7122, -2.7420, -2.4954, -2.2113,
-2.3062, -2.2536], grad_fn=<SelectBackward0>) torch.Size([64, 10])
如你所见,preds 张量不仅包含张量值,还包含梯度函数。我们稍后将使用它来进行反向传播。
让我们实现负对数似然损失作为损失函数(同样,我们可以直接使用标准 Python)
def nll(input, target):
return -input[range(target.shape[0]), target].mean()
loss_func = nll
让我们检查一下使用随机模型时的损失,以便观察稍后经过反向传播后是否有所改进。
tensor(2.3384, grad_fn=<NegBackward0>)
我们再实现一个计算模型准确率的函数。对于每个预测,如果最大值的索引与目标值匹配,则预测正确。
def accuracy(out, yb):
preds = torch.argmax(out, dim=1)
return (preds == yb).float().mean()
让我们检查一下随机模型的准确率,看看随着损失的改善,准确率是否也会提高。
tensor(0.1719)
现在我们可以运行训练循环了。在每次迭代中,我们将:
选择一小批数据(大小为
bs)使用模型进行预测
计算损失
loss.backward()更新模型的梯度,在本例中为weights和bias。
我们现在使用这些梯度来更新权重和偏差。我们在 torch.no_grad() 上下文管理器中执行此操作,因为我们不希望这些动作被记录到下一次梯度计算中。你可以在此处阅读更多关于 PyTorch 的 Autograd 如何记录操作的信息。
然后我们将梯度设为零,以便为下一次循环做准备。否则,我们的梯度会记录所有已发生操作的累加值(即 loss.backward() 会将梯度添加到现有的值上,而不是替换它们)。
提示
你可以使用标准的 Python 调试器单步执行 PyTorch 代码,从而在每一步检查各种变量的值。取消下面 set_trace() 的注释即可尝试。
from IPython.core.debugger import set_trace
lr = 0.5 # learning rate
epochs = 2 # how many epochs to train for
for epoch in range(epochs):
for i in range((n - 1) // bs + 1):
# set_trace()
start_i = i * bs
end_i = start_i + bs
xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]
pred = model(xb)
loss = loss_func(pred, yb)
loss.backward()
with torch.no_grad():
weights -= weights.grad * lr
bias -= bias.grad * lr
weights.grad.zero_()
bias.grad.zero_()
就是这样:我们完全从零开始创建并训练了一个最小化的神经网络(在本例中是逻辑回归,因为我们没有隐藏层)!
让我们检查一下损失和准确率,并与我们之前得到的结果进行比较。我们预计损失会减少,准确率会增加,事实也确实如此。
tensor(0.0785, grad_fn=<NegBackward0>) tensor(1.)
使用 torch.nn.functional#
现在我们将重构代码,使其实现与之前相同的功能,但开始利用 PyTorch 的 nn 类来使代码更简洁、更灵活。从这里开始的每一步,我们都应让代码:更短、更易懂,或更灵活。
第一步也是最简单的一步是让代码变短,方法是用 torch.nn.functional 中的函数替换我们手写的激活函数和损失函数(按照惯例,通常导入为 F)。该模块包含了 torch.nn 库中的所有函数(而库的其他部分包含类)。除了各种损失和激活函数外,你还会在这里找到一些创建神经网络的便捷函数,例如池化函数。(也有用于卷积、线性层等的函数,但正如我们将看到的,这些函数最好使用库的其他部分来处理。)
如果你使用的是负对数似然损失和 log softmax 激活,PyTorch 提供了一个单一的函数 F.cross_entropy,它结合了两者。因此,我们甚至可以从模型中移除激活函数。
import torch.nn.functional as F
loss_func = F.cross_entropy
def model(xb):
return xb @ weights + bias
注意,我们不再在 model 函数中调用 log_softmax。让我们确认我们的损失和准确率是否与之前相同。
tensor(0.0785, grad_fn=<NllLossBackward0>) tensor(1.)
使用 nn.Module 重构#
接下来,我们将使用 nn.Module 和 nn.Parameter 来实现更清晰、更简洁的训练循环。我们将继承 nn.Module(它本身就是一个能够跟踪状态的类)。在本例中,我们想要创建一个包含权重、偏差和前向传播方法的类。nn.Module 拥有许多属性和方法(例如 .parameters() 和 .zero_grad()),我们将使用它们。
注意
nn.Module(大写 M)是一个 PyTorch 特有的概念,是我们将会大量使用的类。nn.Module 不要与 Python 概念中的(小写 m)模块混淆,后者是可导入的 Python 代码文件。
from torch import nn
class Mnist_Logistic(nn.Module):
def __init__(self):
super().__init__()
self.weights = nn.Parameter(torch.randn(784, 10) / math.sqrt(784))
self.bias = nn.Parameter(torch.zeros(10))
def forward(self, xb):
return xb @ self.weights + self.bias
由于我们现在使用的是对象而不是函数,所以首先必须实例化我们的模型。
model = Mnist_Logistic()
现在我们可以像以前一样计算损失了。请注意,nn.Module 对象像函数一样使用(即它们是可调用的),但在底层 PyTorch 会自动调用我们的 forward 方法。
tensor(2.4703, grad_fn=<NllLossBackward0>)
之前在训练循环中,我们必须按名称更新每个参数的值,并手动分别为每个参数清零梯度,像这样:
with torch.no_grad():
weights -= weights.grad * lr
bias -= bias.grad * lr
weights.grad.zero_()
bias.grad.zero_()
现在,我们可以利用 model.parameters() 和 model.zero_grad()(两者均由 PyTorch 为 nn.Module 定义)来使这些步骤更简洁,并减少因遗忘某些参数而导致的错误,尤其是在模型变得更复杂时。
with torch.no_grad():
for p in model.parameters(): p -= p.grad * lr
model.zero_grad()
我们将小型训练循环包装在一个 fit 函数中,以便以后可以再次运行它。
def fit():
for epoch in range(epochs):
for i in range((n - 1) // bs + 1):
start_i = i * bs
end_i = start_i + bs
xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]
pred = model(xb)
loss = loss_func(pred, yb)
loss.backward()
with torch.no_grad():
for p in model.parameters():
p -= p.grad * lr
model.zero_grad()
fit()
让我们再次确认损失确实下降了。
tensor(0.0808, grad_fn=<NllLossBackward0>)
使用 nn.Linear 重构#
我们继续重构代码。与其手动定义和初始化 self.weights 和 self.bias,并计算 xb @ self.weights + self.bias,我们将改用 PyTorch 类 nn.Linear 来表示线性层,它为我们完成了所有这些工作。PyTorch 有许多预定义的层类型,可以极大地简化我们的代码,而且通常还会使其运行得更快。
我们像以前一样实例化模型并计算损失。
model = Mnist_Logistic()
print(loss_func(model(xb), yb))
tensor(2.3728, grad_fn=<NllLossBackward0>)
我们仍然可以使用之前的 fit 方法。
tensor(0.0828, grad_fn=<NllLossBackward0>)
使用 torch.optim 重构#
PyTorch 还有一个包含各种优化算法的包:torch.optim。我们可以使用优化器中的 step 方法来进行前向传播更新,而不是手动更新每个参数。
这将允许我们替换之前手动编写的优化步骤:
with torch.no_grad():
for p in model.parameters(): p -= p.grad * lr
model.zero_grad()
转而只需使用:
(optim.zero_grad() 将梯度重置为 0,我们需要在计算下一个小批次的梯度之前调用它。)
from torch import optim
我们将定义一个小函数来创建模型和优化器,以便将来可以复用它。
def get_model():
model = Mnist_Logistic()
return model, optim.SGD(model.parameters(), lr=lr)
model, opt = get_model()
print(loss_func(model(xb), yb))
for epoch in range(epochs):
for i in range((n - 1) // bs + 1):
start_i = i * bs
end_i = start_i + bs
xb = x_train[start_i:end_i]
yb = y_train[start_i:end_i]
pred = model(xb)
loss = loss_func(pred, yb)
loss.backward()
opt.step()
opt.zero_grad()
print(loss_func(model(xb), yb))
tensor(2.2313, grad_fn=<NllLossBackward0>)
tensor(0.0817, grad_fn=<NllLossBackward0>)
使用 Dataset 重构#
PyTorch 有一个抽象的 Dataset 类。Dataset 可以是任何具有 __len__ 函数(由 Python 标准 len 函数调用)和 __getitem__ 函数来对其进行索引的对象。本教程演示了一个很好的示例,创建了一个自定义的 FacialLandmarkDataset 类作为 Dataset 的子类。
PyTorch 的 TensorDataset 是一个包装张量的 Dataset。通过定义长度和索引方式,它还使我们能够沿张量的第一维度进行迭代、索引和切片。这将使我们在训练时能够更轻松地同时访问自变量和因变量。
from torch.utils.data import TensorDataset
x_train 和 y_train 都可以合并到一个 TensorDataset 中,这样更易于迭代和切片。
之前,我们必须分别迭代 x 和 y 值的小批次:
现在,我们可以同时执行这两个步骤:
tensor(0.0810, grad_fn=<NllLossBackward0>)
使用 DataLoader 重构#
PyTorch 的 DataLoader 负责管理批次。你可以从任何 Dataset 创建 DataLoader。DataLoader 使迭代批次变得更加容易。不再需要使用 train_ds[i*bs : i*bs+bs],DataLoader 会自动为我们提供每个小批次。
from torch.utils.data import DataLoader
train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs)
之前,我们的循环是这样迭代批次 (xb, yb) 的:
现在,我们的循环变得更加清晰,因为 (xb, yb) 是从数据加载器自动加载的:
tensor(0.0816, grad_fn=<NllLossBackward0>)
得益于 PyTorch 的 nn.Module、nn.Parameter、Dataset 和 DataLoader,我们的训练循环现在变得小得多,也更容易理解。让我们尝试添加在实践中创建有效模型所需的基本特性。
添加验证#
在第 1 部分中,我们只是为了在训练数据上建立一个合理的训练循环。在现实中,为了识别是否过拟合,你总是应该有一个 验证集。
打乱训练数据对于防止批次之间的相关性和过拟合非常 重要。另一方面,无论我们是否打乱验证集,验证损失都是相同的。由于打乱数据需要额外的时间,因此打乱验证数据是没有意义的。
我们将为验证集使用两倍于训练集的批次大小。这是因为验证集不需要反向传播,因此占用的内存更少(不需要存储梯度)。我们利用这一点来使用更大的批次大小,从而更快地计算损失。
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)
我们将在每个 epoch 结束时计算并打印验证损失。
(注意,我们总是先调用 model.train() 再进行训练,先调用 model.eval() 再进行推理,因为 nn.BatchNorm2d 和 nn.Dropout 等层需要这些设置以确保在不同阶段表现正确。)
model, opt = get_model()
for epoch in range(epochs):
model.train()
for xb, yb in train_dl:
pred = model(xb)
loss = loss_func(pred, yb)
loss.backward()
opt.step()
opt.zero_grad()
model.eval()
with torch.no_grad():
valid_loss = sum(loss_func(model(xb), yb) for xb, yb in valid_dl)
print(epoch, valid_loss / len(valid_dl))
0 tensor(0.3556)
1 tensor(0.3207)
创建 fit() 和 get_data()#
我们现在自己做一点重构。由于我们两次经过类似的流程来计算训练集和验证集的损失,让我们将其合并为一个函数 loss_batch,该函数用于计算一个批次的损失。
我们为训练集传入一个优化器,并用它执行反向传播。对于验证集,我们不传入优化器,因此该方法不会执行反向传播。
fit 运行训练模型所需的必要操作,并为每个 epoch 计算训练和验证损失。
import numpy as np
def fit(epochs, model, loss_func, opt, train_dl, valid_dl):
for epoch in range(epochs):
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(epoch, val_loss)
get_data 返回训练集和验证集的数据加载器。
def get_data(train_ds, valid_ds, bs):
return (
DataLoader(train_ds, batch_size=bs, shuffle=True),
DataLoader(valid_ds, batch_size=bs * 2),
)
现在,获取数据加载器并训练模型的整个过程只需 3 行代码即可完成:
0 0.32092293989658355
1 0.6815678340911865
你可以使用这基础的 3 行代码来训练各种模型。让我们看看能否用它们来训练一个卷积神经网络 (CNN)!
切换到 CNN#
我们现在要用三个卷积层来构建神经网络。因为上一节中的函数对模型形式没有任何假设,我们无需进行任何修改就能用它们来训练 CNN。
我们将使用 PyTorch 预定义的 Conv2d 类作为我们的卷积层。我们定义了一个包含 3 个卷积层的 CNN。每个卷积后都跟着一个 ReLU。最后,我们执行一次平均池化。(注意 view 是 PyTorch 版本的 Numpy reshape)
class Mnist_CNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1)
self.conv2 = nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1)
self.conv3 = nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1)
def forward(self, xb):
xb = xb.view(-1, 1, 28, 28)
xb = F.relu(self.conv1(xb))
xb = F.relu(self.conv2(xb))
xb = F.relu(self.conv3(xb))
xb = F.avg_pool2d(xb, 4)
return xb.view(-1, xb.size(1))
lr = 0.1
动量(Momentum) 是随机梯度下降的一种变体,它也考虑了之前的更新,通常会带来更快的训练速度。
0 0.3434015565633774
1 0.23287556912004947
使用 nn.Sequential#
torch.nn 还有另一个可以用来简化代码的便捷类:Sequential。Sequential 对象会按顺序运行其中包含的每个模块。这是编写神经网络的一种更简单的方法。
为了利用这一点,我们需要能够轻松地从给定的函数定义一个 自定义层。例如,PyTorch 没有 view 层,我们需要为我们的网络创建一个。Lambda 将创建一个层,我们可以在使用 Sequential 定义网络时使用它。
class Lambda(nn.Module):
def __init__(self, func):
super().__init__()
self.func = func
def forward(self, x):
return self.func(x)
def preprocess(x):
return x.view(-1, 1, 28, 28)
用 Sequential 创建的模型非常简单:
model = nn.Sequential(
Lambda(preprocess),
nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.AvgPool2d(4),
Lambda(lambda x: x.view(x.size(0), -1)),
)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
0 0.3942904595375061
1 0.2313709025502205
包装 DataLoader#
- 我们的 CNN 相当简洁,但它只能用于 MNIST,因为:
它假设输入是一个 28*28 的长向量
它假设最终 CNN 网格大小为 4*4(因为那是我们使用的平均池化核大小)
让我们消除这两个假设,以便我们的模型可以处理任何 2 维单通道图像。首先,我们可以通过将数据预处理移动到生成器中来移除最初的 Lambda 层:
def preprocess(x, y):
return x.view(-1, 1, 28, 28), y
class WrappedDataLoader:
def __init__(self, dl, func):
self.dl = dl
self.func = func
def __len__(self):
return len(self.dl)
def __iter__(self):
for b in self.dl:
yield (self.func(*b))
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)
接下来,我们可以用 nn.AdaptiveAvgPool2d 替换 nn.AvgPool2d,它允许我们定义我们想要的输出张量大小,而不是处理当前的输入张量。因此,我们的模型将能适用于任何大小的输入。
model = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.AdaptiveAvgPool2d(1),
Lambda(lambda x: x.view(x.size(0), -1)),
)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
让我们试试看:
0 0.41216081080436706
1 0.2388353627860546
使用你的 加速器#
如果你幸运地拥有如 CUDA 等加速器(你可以从大多数云服务提供商那里以约 0.5 美元/小时的价格租用),你可以使用它来加速你的代码。首先,在 PyTorch 中检查你的加速器是否在工作:
# If the current accelerator is available, we will use it. Otherwise, we use the CPU.
device = torch.accelerator.current_accelerator().type if torch.accelerator.is_available() else "cpu"
print(f"Using {device} device")
Using cuda device
让我们更新 preprocess 以将批次移动到加速器:
最后,我们可以将模型移动到加速器:
model.to(device)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
你会发现它现在运行得更快了。
0 0.21489832620620727
1 0.19117642662525178
结语#
现在我们拥有了一个通用的数据流水线和训练循环,你可以用它来训练许多种类的 Pytorch 模型。要看看现在训练模型有多简单,请查看 mnist_sample notebook。
当然,你还需要添加许多功能,如数据增强、超参数调整、训练监控、迁移学习等。这些功能在 fastai 库中均有提供,该库采用了本教程中演示的相同设计方法,为想要进一步深入研究模型的开发者提供了一个自然的进阶步骤。
我们在本教程开始时承诺通过示例来解释 torch.nn、torch.optim、Dataset 和 DataLoader 的每一个组件。让我们总结一下我们所学到的内容:
torch.nn:
Module:创建一个可调用对象,它的行为像函数,但也可以包含状态(例如神经网络层权重)。它知道它包含哪些Parameter,可以将其所有梯度清零,遍历它们以进行权重更新等。
Parameter:一种张量包装器,告诉Module它具有在反向传播期间需要更新的权重。只有设置了 requires_grad 属性的张量才会被更新。
functional:一个模块(按照惯例通常导入到F命名空间),其中包含激活函数、损失函数等,以及非状态化的层版本(如卷积层和线性层)。
torch.optim:包含如SGD等优化器,在反向传播步骤中更新Parameter的权重。
Dataset:具有__len__和__getitem__的对象的抽象接口,包括 Pytorch 提供的诸如TensorDataset的类。
DataLoader:获取任何Dataset并创建一个返回数据批次的迭代器。
脚本运行总耗时:(0 分 25.144 秒)