大规模 MLOps 工程(二)(1)https://developer.aliyun.com/article/1517762
4.1.4 使用 PySpark 作业对测试集进行抽样
在本节中,您将通过使用 PySpark 作业随机采样 1,048,576 条记录(在上一节中确定的大小)来创建测试数据集进行实验。一旦采样了测试集,剩余的记录将持久保存到一个单独的 DC 出租车开发数据集中。开发和测试数据集还被分析以计算 p 值以及其他摘要统计信息。
由于整个 PySpark 作业的实现大约有 90 行代码,在本节中,作业被介绍为一系列代码片段。作业的前文部分,在列表 4.3 中显示的,类似于第 2 和第三章中的 PySpark 作业。与早期章节一样,作业的这一部分导入相关库并解析作业参数。
在 dctaxi_dev_test.py 中的第 4.3 节代码中读取的 PySpark DataFrame。
import sys from awsglue.transforms import * from awsglue.utils import getResolvedOptions from pyspark.context import SparkContext from awsglue.context import GlueContext from awsglue.job import Job args = getResolvedOptions(sys.argv, ['JOB_NAME', 'BUCKET_SRC_PATH', 'BUCKET_DST_PATH', 'SAMPLE_SIZE', 'SAMPLE_COUNT', 'SEED' ]) sc = SparkContext() glueContext = GlueContext(sc) logger = glueContext.get_logger() spark = glueContext.spark_session job = Job(glueContext) job.init(args['JOB_NAME'], args) BUCKET_SRC_PATH = args['BUCKET_SRC_PATH'] df = ( spark.read.format("parquet") .load( f"{BUCKET_SRC_PATH}" )) ❶
❶ 根据 BUCKET_SRC_PATH 参数构建一个 pandas DataFrame df。
与从清理后的 DC 出租车数据集中抽样有关的实现始于列表 4.4,其中计算整个数据集大小的样本分数,并将其保存到变量 sample_frac 中。为了在 PySpark 中计算清理后数据集的摘要统计信息,实现依赖于 Kaen 库的 PySpark 实用函数 spark_df_to_stats_pandas_df,该函数从名为 df 的 PySpark DataFrame 实例返回 pandas DataFrame。然后,pandas summary_df 提供了对清理后数据集中每列的平均值(mu)和标准差(sigma)的标准 pandas DataFrame API 访问。
在 dctaxi_dev_test.py 中的第 4.4 节代码中读取的 PySpark DataFrame。
SAMPLE_SIZE = float( args['SAMPLE_SIZE'] ) dataset_size = float( df.count() ) sample_frac = SAMPLE_SIZE / dataset_size ❶ from kaen.spark import spark_df_to_stats_pandas_df, \ pandas_df_to_spark_df, \ spark_df_to_shards_df ❷ summary_df = spark_df_to_stats_pandas_df(df) ❸ mu = summary_df.loc['mean'] ❹ sigma = summary_df.loc['stddev'] ❺
❶ 根据 Spark 的 randomSplit 方法所需的样本大小,以分数的形式表示。
❷ 从 kaen 包中导入 Spark 和 pandas 实用工具。
❸ 创建包含 Spark DataFrame 统计信息的 pandas DataFrame。
❹ 将数据集的平均值保存为 mu。
❺ 将数据集的标准差保存为 sigma。
汇总统计信息以及 sample_frac 值在列表 4.5 中用于执行随机抽样。PySpark 的 randomSplit 方法将经过清理的 DC 出租车数据集分割为 test_df,其中包含最多 SAMPLE_SIZE 行,并且总计来自 df 数据帧的 sample_frac 的整个数据集。
在 dctaxi_dev_test.py 中的第 4.5 节代码中读取的 PySpark DataFrame。
SEED = int(args['SEED']) ❶ SAMPLE_COUNT = int(args['SAMPLE_COUNT']) ❷ BUCKET_DST_PATH = args['BUCKET_DST_PATH'] for idx in range(SAMPLE_COUNT): dev_df, test_df = ( df ❸ .cache() .randomSplit([1.0 - sample_frac, sample_frac], ❹ seed = SEED) ) test_df = test_df.limit( int(SAMPLE_SIZE) ) ❺ test_stats_df = \ ❻ spark_df_to_stats_pandas_df(test_df, summary_df, pvalues = True, zscores = True) pvalues_series = test_stats_df.loc['pvalues'] if pvalues_series.min() < 0.05: SEED = SEED + idx ❼ else: break
❶ 使用 SEED 初始化伪随机数生成器。
❷ 通过使用最多 SAMPLE_COUNT 个样本,解决了选择不佳(p 值 < 0.05)的 SEED 值的问题。
❸ 将测试数据集抽样到 Spark 的 test_df DataFrame 中,其余抽样到 dev_df。
❹ 使用 df 中记录的 sample_frac 分数作为测试数据集。
❺ 确保 test_df 最多仅包含 SAMPLE_SIZE 条记录。
❻ 创建一个包含 test_df 摘要统计信息的 pandas test_stats_df DataFrame。
❼ 在出现不良样本(p 值 < 0.05)的情况下再次抽样,最多抽样 SAMPLE_COUNT 次。
列表 4.6 中显示的作业实现部分负责将开发(dev_df)和测试(test_df)数据集保存到 S3。对于每个数据集,Spark 将记录保存为 CSV 格式,带有标头信息,保存到 BUCKET_DST_PATH 中。此外,对于开发和测试,该实现还将其他元数据(稍后在本节中显示)保存到 BUCKET_DST_PATH 的子文件夹中:.meta/stats 和 .meta/shards。
stats 子文件夹存储一个包含摘要统计信息的 CSV 文件,包括计数、均值、p 值等。 shards 子文件夹被存储以便在训练期间处理数据集,并存储关于用于将数据集保存在 S3 中的 CSV 部分文件数和每个部分文件中的记录数的元数据。
列表 4.6 dctaxi_dev_test.py 中的 PySpark DataFrame 读取代码
for df, desc in [(dev_df, "dev"), (test_df, "test")]: ( df .write .option('header', 'true') .mode('overwrite') .csv(f"{BUCKET_DST_PATH}/{desc}") ) stats_pandas_df = \ spark_df_to_stats_pandas_df(df, summary_df, pvalues = True, zscores = True) ( pandas_df_to_spark_df(spark, stats_pandas_df) .coalesce(1) .write .option('header', 'true') .mode('overwrite') .csv(f"{BUCKET_DST_PATH}/{desc}/.meta/stats") ) ( spark_df_to_shards_df(spark, df) .coalesce(1) .write .option('header', True) .mode('overwrite') .csv(f"{BUCKET_DST_PATH}/{desc}/.meta/shards") ) job.commit()
为了方便起见,下面展示了 PySpark 作业的完整实现,它应该被保存在一个名为 dctaxi_dev_test.py 的文件中。
列表 4.7 PySpark dctaxi_dev_test.py 作业以抽样开发和测试数据集
import sys from awsglue.transforms import * from awsglue.utils import getResolvedOptions from pyspark.context import SparkContext from awsglue.context import GlueContext from awsglue.job import Job args = getResolvedOptions(sys.argv, ['JOB_NAME', 'BUCKET_SRC_PATH', 'BUCKET_DST_PATH', 'SAMPLE_SIZE', 'SAMPLE_COUNT', 'SEED' ]) sc = SparkContext() glueContext = GlueContext(sc) logger = glueContext.get_logger() spark = glueContext.spark_session job = Job(glueContext) job.init(args['JOB_NAME'], args) BUCKET_SRC_PATH = args['BUCKET_SRC_PATH'] df = ( spark.read.format("parquet") .load( f"{BUCKET_SRC_PATH}" )) SAMPLE_SIZE = float( args['SAMPLE_SIZE'] ) dataset_size = float( df.count() ) sample_frac = SAMPLE_SIZE / dataset_size from kaen.spark import spark_df_to_stats_pandas_df, \ pandas_df_to_spark_df, \ spark_df_to_shards_df summary_df = spark_df_to_stats_pandas_df(df) mu = summary_df.loc['mean'] sigma = summary_df.loc['stddev'] SEED = int(args['SEED']) SAMPLE_COUNT = int(args['SAMPLE_COUNT']) BUCKET_DST_PATH = args['BUCKET_DST_PATH'] for idx in range(SAMPLE_COUNT): dev_df, test_df = ( df .cache() .randomSplit( [1.0 - sample_frac, sample_frac], seed = SEED) ) test_df = test_df.limit( int(SAMPLE_SIZE) ) test_stats_df = \ spark_df_to_stats_pandas_df(test_df, summary_df, pvalues = True, zscores = True) pvalues_series = test_stats_df.loc['pvalues'] if pvalues_series.min() < 0.05: SEED = SEED + idx else: break for df, desc in [(dev_df, "dev"), (test_df, "test")]: ( df .write .option('header', 'true') .mode('overwrite') .csv(f"{BUCKET_DST_PATH}/{desc}") ) stats_pandas_df = \ spark_df_to_stats_pandas_df(df, summary_df, pvalues = True, zscores = True) ( pandas_df_to_spark_df(spark, stats_pandas_df) .coalesce(1) .write .option('header', 'true') .mode('overwrite') .csv(f"{BUCKET_DST_PATH}/{desc}/.meta/stats") ) ( spark_df_to_shards_df(spark, df) .coalesce(1) .write .option('header', True) .mode('overwrite') .csv(f"{BUCKET_DST_PATH}/{desc}/.meta/shards") ) job.commit()
在 dctaxi_dev_test.py 文件中执行 PySpark 作业之前,你需要配置几个环境变量。应使用相应 Python 变量的值设置 SAMPLE_SIZE 和 SAMPLE_COUNT 操作系统环境变量:
os.environ['SAMPLE_SIZE'] = str(SAMPLE_SIZE) os.environ['SAMPLE_COUNT'] = str(1)
与上一章节类似,PySpark 作业使用 utils.sh 脚本中的便捷函数执行。首先,在你的 bash shell 中使用以下命令下载该脚本到你的本地环境:
wget -q --no-cache https://raw.githubusercontent.com/ ➥ osipov/smlbook/master/utils.sh
一旦 utils.sh 脚本被下载,你可以使用它来启动和监视 dctaxi_dev_test.py 文件中实现的 PySpark 作业。在你的 shell 环境中运行以下命令来启动该作业:
source utils.sh PYSPARK_SRC_NAME=dctaxi_dev_test.py \ PYSPARK_JOB_NAME=dc-taxi-dev-test-job \ ADDITIONAL_PYTHON_MODULES="kaen[spark]" \ BUCKET_SRC_PATH=s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/parquet/vacuum \ BUCKET_DST_PATH=s3://dc-taxi-$BUCKET_ID-$AWS_DEFAULT_REGION/csv \ SAMPLE_SIZE=$SAMPLE_SIZE \ SAMPLE_COUNT=$SAMPLE_COUNT \ SEED=30 \ run_job
注意,该作业将要读取第三章保存在 parquet/vacuum 子文件夹中的 Parquet 文件,并将开发和测试数据集保存在你的 S3 存储桶的 csv/dev 和 csv/test 子文件夹下。该作业在 AWS Glue 上应该需要大约八分钟才能完成。假设它成功完成,它应该会产生以下类似的输出:
Attempting to run a job using: PYSPARK_SRC_NAME=dctaxi_dev_test.py PYSPARK_JOB_NAME=dc-taxi-dev-test-job AWS_DEFAULT_REGION=us-west-2 BUCKET_ID=c6e91f06095c3d7c61bcc0af33d68382 BUCKET_SRC_PATH=s3://dc-taxi-c6e91f06095c3d7c61bcc0af33d68382- ➥ us-west-2/parquet/vacuum BUCKET_DST_PATH=s3://dc-taxi-c6e91f06095c3d7c61bcc0af33d68382- ➥ us-west-2/csv SAMPLE_SIZE=1048576 SAMPLE_COUNT=1 BINS= SEED=30 upload: ./dctaxi_dev_test.py to s3://dc-taxi- ➥ c6e91f06095c3d7c61bcc0af33d68382-us-west-2/glue/dctaxi_dev_test.py 2021-08-15 17:19:37 2456 dctaxi_dev_test.py { "JobName": "dc-taxi-dev-test-job" } { "Name": "dc-taxi-dev-test-job" } { "JobRunId": [CA "jr_05e395544e86b1534c824fa1559ac395683f3e7db35d1bb5d591590d237954f2" } Waiting for the job to finish......................................SUCCEEDED
由于 PySpark 作业保留了关于数据集的元数据,你可以使用 pandas 预览元数据的内容。为了预览测试集的统计摘要,请执行以下 Python 代码:
pd.options.display.float_format = '{:,.2f}'.format test_stats_df = pd.read_csv(f"s3://dc-taxi-{os.environ['BUCKET_ID']}- ➥ {os.environ['AWS_DEFAULT_REGION']}/csv/test/.meta/stats/*.csv") test_stats_df = test_stats_df.set_index('summary') test_stats_df
假设 PySpark 作业执行正确,对于测试数据集的 test_stats_df 的打印输出应该类似于以下内容:
关于开发数据集的 CSV 部分文件(shards)的元数据应该已保存到你的 S3 存储桶的 csv/dev/.meta/shards 子文件夹中。如果你使用以下代码预览此元数据中的 pandas DataFrame
import pandas as pd dev_shards_df = pd.read_csv(f"s3://dc-taxi-{os.environ['BUCKET_ID']}- ➥ {os.environ['AWS_DEFAULT_REGION']}/csv/dev/.meta/shards/*") dev_shards_df.sort_values(by = 'id')
输出应该包含一个三列表,其中 id 列存储来自 S3 中 csv/dev 子文件夹的 CSV 部分文件的 ID,而 count 列中的相应条目指定了部分文件中的行数。数据框的内容应该类似于以下内容:
id | count | |
39 | 0 | 165669 |
3 | 1 | 165436 |
56 | 2 | 165754 |
53 | 3 | 165530 |
63 | 4 | 165365 |
…
72 | 75 | 164569 |
59 | 76 | 164729 |
2 | 77 | 164315 |
11 | 78 | 164397 |
22 | 79 | 164406 |
摘要
- 使用固定百分比的启发式方法来选择保留的测试数据集的大小可能会浪费宝贵的机器学习模型训练数据。
- 测量数据集大小增加导致递减的结果,有助于选择测试和验证数据集大小的下限。
- 确保测试数据集具有足够的 z 分数和 p 值,可以防止选择机器学习时数据集大小过小。
- 无服务器 PySpark 作业可用于评估替代测试数据集,并报告它们的统计摘要。
第二部分:用于无服务器机器学习的 PyTorch
在开始使用 PyTorch 之前,我花了几年时间使用 TensorFlow 的 1 和 2 版本。自从我转向 PyTorch 以来,我作为一个机器学习从业者变得更加高效,并且我发现学习和使用 PyTorch 的经历令人愉快。我想与本书的读者分享这种经历。在这个过程中,我旨在帮助你掌握 PyTorch 的核心元素,指导你了解框架中可用的抽象级别,并准备好从单独使用 PyTorch 转变为使用在 PyTorch 中实现并集成到更广泛的机器学习流水线中的机器学习模型。
- 在第五章,我涵盖了 PyTorch 的基础知识,介绍了核心张量应用程序接口(API),并帮助你掌握使用该 API 的流畅度,以降低后续章节的学习曲线。
- 在第六章,你将专注于学习 PyTorch 的深度学习方面,包括自动微分的支持、替代梯度下降算法和支持的实用工具。
- 在第七章,你将通过学习图形处理单元(GPU)的特性以及如何利用 GPU 加速你的机器学习代码,来扩展你的 PyTorch 程序。
- 在第八章,你将学习有关分布式 PyTorch 训练的数据并行方法,并深入探讨传统的基于参数服务器的方法与基于环形的分布式训练(例如 Horovod)之间的区别。
第五章:PyTorch 介绍:张量基础
本章涵盖
- 介绍 PyTorch 和 PyTorch 张量
- 使用 PyTorch 张量创建方法
- 理解张量操作和广播
- 探索在 CPU 上的 PyTorch 张量性能
在上一章中,你从 DC 出租车数据集的清理版本开始,并应用了数据驱动的抽样过程,以确定要分配给一个保留的测试数据子集的数据集的正确部分。你还分析了抽样实验的结果,然后启动了一个 PySpark 作业来生成三个不同的数据子集:训练、验证和测试。
这一章将暂时偏离 DC 出租车数据集,为你准备好使用 PyTorch 编写可扩展的机器学习代码。别担心;第七章会回到 DC 出租车数据集,以基准测试基线 PyTorch 机器学习模型。在本章中,你将专注于学习 PyTorch,这是深度学习和许多其他类型的机器学习算法的顶级框架之一。我曾在需要在机器学习平台上进行分布式训练的机器学习项目中使用过 TensorFlow 2.0、Keras 和 PyTorch,并发现 PyTorch 是最好的选择。PyTorch 可从特斯拉的关键生产机器学习用例¹扩展到 OpenAI 的最新研究²。
由于你需要在开始将它们应用于 DC 出租车数据集的机器学习之前对核心 PyTorch 概念有实际的理解,所以本章重点是为你提供对核心 PyTorch 数据结构:张量的深入知识。大多数软件工程师和机器学习实践者在数学、编程或数据结构课程中都没有使用张量,所以如果这是新的,你不应感到惊讶。
在第 5.1 节中,我介绍了 PyTorch 张量的全面定义。暂时记住,如果你曾经在编程语言中使用数组的数组(即,包含其他数组的数组)实现过矩阵,那么你已经在理解张量的路上走得很远了。作为一个工作定义,你可以将张量视为一种通用数据结构,可以存储和操作变量、数组、矩阵及其组合。在本书中,你遇到的最复杂的张量实际上是矩阵的数组,或者如果你更喜欢更递归的描述,是数组的数组的数组。
5.1 开始使用张量
本节在机器学习用例的背景下定义了张量,解释了张量的属性,包括张量的维度和形状,并最终向你介绍了使用 PyTorch 创建张量的基础知识,而不是使用本地 Python 数据类型。通过本节的结论,你应该准备好研究 PyTorch 张量相对于本地 Python 数据类型在机器学习用例中的优势了。
张量一词在数学、物理或计算机科学中使用时有微妙不同的定义。虽然从数学中了解张量的几何解释或从物理学中了解张量的应力力学解释可以丰富您对张量抽象方面的理解,但本书使用一个更狭窄的定义,更符合将张量应用于机器学习的从业者。本书中,该术语描述了一种数据结构(即数据容器),用于基本数据类型,如整数、浮点数和布尔值。
由于张量与数组密切相关,因此值得花一点时间回顾数组或 Python 列表的关键属性。数组只是数据值的有序集合。在大多数编程语言中,数组索引可以取值于一组有限的整数,基于数组中元素数量减一的范围。³例如,在 Python 中,这个范围是
range(len(a_list))
其中 a_list 是 Python 列表实例的名称。因此,不同的数组具有不同的有效索引值。相反,所有由基本数据类型组成的数组,无论长度如何,其张量维度都等于一。
维度在这里定义为访问数据结构中值所需的索引总数(而不是索引的值)。这个定义很方便,因为它帮助使用一个数字描述不同的数据结构。例如,矩阵的维度为二,因为需要两个索引,行索引和列索引,才能在矩阵中定位数据值。例如,使用 Python,一个简单的矩阵实现可以使用列表的列表:
mtx = [[3 * j + i for i in range(3)] for j in range(3)] print(mtx[1][2])
其中 mtx 的求值结果为
[[0, 1, 2], [3, 4, 5], [6, 7, 8]]
和 mtx[1][2]的值打印为 5。由于矩阵的维度为二,因此必须指定两个索引值——行索引为 1,列索引为 2——才能访问矩阵中 5 的值。
维度还指定了实现数据结构所需的数组嵌套的度量。例如,维度为 2 的 mtx 需要一个数组的数组,而维度为 3 的矩阵数组需要一个数组的数组的数组。如果考虑一个维度为 0 的数据结构,换句话说,需要零个索引来访问值的数据结构,很快就会意识到这个数据结构只是一个常规变量。对于维度(也称为张量秩)为 0、1、2 和 3 的张量的可视化,请参阅图 5.1。
图 5.1 张量秩(维度)等于访问张量中数据值所需的索引数量。与较低秩的张量不同,机器学习中没有公认的命名。
张量是一种能够存储任意数量维度数组的数据结构,或者更简洁地说,张量是一个 n 维数组。根据此定义,一个平面 Python 列表或任何扁平化的数组都是一维张量,有时也被描述为秩 1 张量。Python 变量是零维张量,通常被描述为标量,或者更少见地被描述为秩 0 张量。一个二维张量通常被称为矩阵。对于更高维的例子,请考虑用于表示灰度图像的矩阵,其中像素值为 0 为黑色,255 为白色,中间的数字为逐渐增加亮度的灰色颜色。然后,三维张量是一种有序灰度图像集合的便捷数据结构,因此三个指数中的第一个指定图像,其余两个指定图像中像素的行和列位置。三维张量也适用于彩色图像(但不适用于彩色图像集合),因此第一个指数指定颜色为红色、绿色、蓝色或不透明度(alpha)通道,而其余指数指定相应图像中的像素位置。继续这个例子,四维张量可以用于顺序的彩色图像集合。
有了这些基础知识,您就可以准备在 PyTorch 中创建您的第一个张量了。
列表 5.1 使用 PyTorch 实现的秩 0 张量
import torch as pt ❶ alpha = pt.tensor(42) ❷ alpha
❶ 导入 PyTorch 库并将其别名为 pt。
❷ 创建一个值为 42 的秩 0 张量(标量)。
一旦执行了这段代码,它会输出
tensor(42)
在导入 PyTorch 库并将其别名为 pt ❶ 之后,代码的下一行 ❷ 简单地创建一个标量(秩 0 张量)并将其赋值给一个名为 alpha 的变量。在 64 位 Python 运行时执行时,从列表 5.1 中的值 42 被表示为 64 位整数,alpha 张量将使用 PyTorch 的 torch.LongTensor 类进行实例化。
对于任何 PyTorch 张量,您可以使用 type 方法来发现用于实例化张量的特定张量类:
alpha.type()
这会输出
torch.LongTensor
torch.LongTensor,以及其他用于各种基本 Python 数据类型的张量类,都是 torch.Tensor 类的子类。⁴ torch.Tensor 的子类包括对不同处理器架构(设备)的支持;例如,torch.LongTensor 是具有 CPU 特定张量实现的类,而 torch.cuda.LongTensor 是具有 GPU 特定张量实现的类。有关 PyTorch 对 GPU 的支持,将在第七章中详细描述。
在您的机器学习代码中,您应主要依赖于张量的 dtype 属性,而不是 type 方法,因为 dtype 以与设备无关的方式返回张量的类型,确保您的代码可以轻松地在不同设备之间移植。对于 alpha 的 dtype,
alpha.dtype
输出数据类型的与设备无关的描述⁵
torch.int64
要访问张量存储的值,您可以使用 item 方法
alpha.item()
在这种情况下显示 42。
要确认 alpha 张量是一个标量,您可以访问张量的 shape 属性,
alpha.shape
打印出 torch.Size([])。
PyTorch 库使用 torch.Size 类来指定张量的大小(也称为形状)的详细信息。在这里,大小由一个空的、长度为零的列表组成,因为 alpha 标量的秩为 0。一般来说,torch.Size 列表的长度等于张量的维度。例如,
len(alpha.shape)
输出 0。张量的形状指定了沿张量维度存储的元素数量。例如,从 Python 列表创建的一个一维 PyTorch 张量的前五个斐波那契数,
arr = pt.tensor([1, 1, 2, 3, 5]) arr.shape
产生了 torch.Size([5]),这证实了 arr 张量的第一个和唯一维度中有五个元素。
如果您从 Python 列表的列表中创建一个 PyTorch 矩阵(秩为 2 的张量),
mtx = pt.tensor([ [ 2, 4, 16, 32, 64], [ 3, 9, 27, 81, 243]] )
然后 mtx.shape 返回 torch.Size([2, 5]) 的大小,因为 mtx 矩阵有两行和五列。
标准的 Python 索引和 item 方法继续按预期工作:要检索 mtx 张量左上角的值,您使用 mtx[0][0].item(),它返回 2。
在 PyTorch 中处理秩为 2 或更高的张量时,您需要了解一个重要的默认限制:尾部维度中的元素数量;换句话说,第二个及更高的维度必须保持一致,例如,如果您尝试创建一个第二个(列)维度有四个元素的矩阵,而其他列有五个元素。
用支持变量的 PyTorch 张量的 5.2 列表
pt.tensor([ [ 2, 4, 16, 32, 64], [ 3, 9, 27, 81, 243], [ 4, 16, 64, 256] ])
PyTorch 报告了一个错误:
ValueError: expected sequence of length 5 at dim 1 (got 4)
由于 PyTorch 使用零为基础的索引来表示维度,因此第二个维度的索引为 1,如 ValueError 所报告的那样。尽管默认的 PyTorch 张量实现不支持“不规则”张量,但 NestedTensor 包旨在为这类张量提供支持。⁶
大规模 MLOps 工程(二)(3)https://developer.aliyun.com/article/1517766