四、使用张量表示真实世界数据
本章内容包括
在上一章中,我们了解到张量是 PyTorch 中数据的构建块。神经网络将张量作为输入,并产生张量作为输出。事实上,神经网络内部的所有操作以及优化过程中的所有操作都是张量之间的操作,神经网络中的所有参数(例如权重和偏置)都是张量。对于成功使用 PyTorch 这样的工具,对张量执行操作并有效地对其进行索引的能力至关重要。现在您已经了解了张量的基础知识,随着您在本书中的学习过程中,您对张量的灵活性将会增长。
现在我们可以回答一个问题:我们如何将一段数据、一个视频或一行文本表示为张量,以便适合训练深度学习模型?这就是我们将在本章学习的内容。我们将重点介绍与本书相关的数据类型,并展示如何将这些数据表示为张量。然后,我们将学习如何从最常见的磁盘格式加载数据,并了解这些数据类型的结构,以便了解如何准备它们用于训练神经网络。通常,我们的原始数据不会完全符合我们想要解决的问题,因此我们将有机会通过一些更有趣的张量操作来练习我们的张量操作技能。
本章的每个部分将描述一种数据类型,并且每种数据类型都将配有自己的数据集。虽然我们已经将本章结构化,使得每种数据类型都建立在前一种数据类型的基础上,但如果你愿意,可以随意跳跃一下。
在本书的其余部分中,我们将使用大量图像和体积数据,因为这些是常见的数据类型,并且在书籍格式中可以很好地再现。我们还将涵盖表格数据、时间序列和文本,因为这些也将引起许多读者的兴趣。因为一图胜千言,我们将从图像数据开始。然后,我们将演示使用代表患者解剖结构的医学数据的三维数组。接下来,我们将处理关于葡萄酒的表格数据,就像我们在电子表格中找到的那样。之后,我们将转向有序表格数据,使用来自自行车共享计划的时间序列数据集。最后,我们将涉足简·奥斯汀的文本数据。文本数据保留了其有序性,但引入了将单词表示为数字数组的问题。
在每个部分中,我们将在深度学习研究人员开始的地方停下来:就在将数据馈送给模型之前。我们鼓励您保留这些数据集;它们将成为我们在下一章开始学习如何训练神经网络模型时的优秀材料。
4.1 处理图像
卷积神经网络的引入彻底改变了计算机视觉(参见 mng.bz/zjMa
),基于图像的系统随后获得了全新的能力。以前需要高度调整的算法构建块的复杂流水线现在可以通过使用成对的输入和期望输出示例训练端到端网络以前所未有的性能水平解决。为了参与这场革命,我们需要能够从常见的图像格式中加载图像,然后将数据转换为 PyTorch 期望的方式排列图像各部分的张量表示。
图像被表示为一个规则网格中排列的标量集合,具有高度和宽度(以像素为单位)。 我们可能在每个网格点(像素)上有一个单一的标量,这将被表示为灰度图像;或者在每个网格点上有多个标量,这通常代表不同的颜色,就像我们在上一章中看到的那样,或者不同的 特征,比如来自深度相机的深度。
表示单个像素值的标量通常使用 8 位整数进行编码,如消费级相机。 在医疗、科学和工业应用中,发现更高的数值精度,如 12 位或 16 位,是很常见的。 这允许在像骨密度、温度或深度等物理属性的像素编码信息的情况下拥有更广泛的范围或增加灵敏度。
4.1.1 添加颜色通道
我们之前提到过颜色。 有几种将颜色编码为数字的方法。 最常见的是 RGB,其中颜色由表示红色、绿色和蓝色强度的三个数字定义。 我们可以将颜色通道看作是仅包含所讨论颜色的灰度强度图,类似于如果您戴上一副纯红色太阳镜看到的场景。 图 4.1 展示了一个彩虹,其中每个 RGB 通道捕获光谱的某个部分(该图简化了,省略了将橙色和黄色带表示为红色和绿色组合的内容)。
图 4.1 彩虹,分为红色、绿色和蓝色通道
彩虹的红色带在图像的红色通道中最亮,而蓝色通道既有彩虹的蓝色带又有天空作为高强度。 还要注意,白云在所有三个通道中都是高强度的。
4.1.2 加载图像文件
图像有多种不同的文件格式,但幸运的是在 Python 中有很多加载图像的方法。 让我们从使用 imageio
模块加载 PNG 图像开始(code/p1ch4/1_image_dog.ipynb)。
列表 4.1 code/p1ch4/1_image_dog.ipynb
# In[2]: import imageio img_arr = imageio.imread('../data/p1ch4/image-dog/bobby.jpg') img_arr.shape # Out[2]: (720, 1280, 3)
注意 我们将在整个章节中使用 imageio
,因为它使用统一的 API 处理不同的数据类型。 对于许多目的,使用 TorchVision 处理图像和视频数据是一个很好的默认选择。 我们在这里选择 imageio
进行稍微轻松的探索。
此时,img
是一个类似于 NumPy 数组的对象,具有三个维度:两个空间维度,宽度和高度;以及第三个维度对应于红色、绿色和蓝色通道。 任何输出 NumPy 数组的库都足以获得 PyTorch 张量。 唯一需要注意的是维度的布局。 处理图像数据的 PyTorch 模块要求张量按照 C × H × W 的方式布局:通道、高度和宽度。
4.1.3 更改布局
我们可以使用张量的 permute
方法,使用旧维度替换每个新维度,以获得适当的布局。 给定一个先前获得的输入张量 H × W × C,通过首先将通道 2 放在前面,然后是通道 0 和 1,我们得到一个正确的布局:
# In[3]: img = torch.from_numpy(img_arr) out = img.permute(2, 0, 1)
我们之前看到过这个,但请注意,此操作不会复制张量数据。 相反,out
使用与 img
相同的底层存储,并且仅在张量级别上处理大小和步幅信息。 这很方便,因为该操作非常便宜; 但是需要注意的是:更改 img
中的像素将导致 out
中的更改。
还要注意,其他深度学习框架使用不同的布局。 例如,最初 TensorFlow 将通道维度保留在最后,导致 H × W × C 的布局(现在支持多种布局)。 从低级性能的角度来看,这种策略有利有弊,但就我们的问题而言,只要我们正确地重塑张量,就不会有任何区别。
到目前为止,我们描述了单个图像。按照我们之前用于其他数据类型的相同策略,为了创建一个包含多个图像的数据集,以用作神经网络的输入,我们将图像存储在一个批次中,沿着第一个维度获得一个N × C × H × W张量。
作为使用stack
构建张量的略微更高效的替代方法,我们可以预先分配一个适当大小的张量,并用从目录加载的图像填充它,如下所示:
# In[4]: batch_size = 3 batch = torch.zeros(batch_size, 3, 256, 256, dtype=torch.uint8)
这表明我们的批次将由三个 RGB 图像组成,高度为 256 像素,宽度为 256 像素。注意张量的类型:我们期望每种颜色都表示为 8 位整数,就像大多数标准消费级相机的照片格式一样。现在我们可以从输入目录加载所有 PNG 图像并将它们存储在张量中:
# In[5]: import os data_dir = '../data/p1ch4/image-cats/' filenames = [name for name in os.listdir(data_dir) if os.path.splitext(name)[-1] == '.png'] for i, filename in enumerate(filenames): img_arr = imageio.imread(os.path.join(data_dir, filename)) img_t = torch.from_numpy(img_arr) img_t = img_t.permute(2, 0, 1) img_t = img_t[:3] # ❶ batch[i] = img_t
❶ 这里我们仅保留前三个通道。有时图像还具有表示透明度的 alpha 通道,但我们的网络只需要 RGB 输入。
4.1.4 数据归一化
我们之前提到神经网络通常使用浮点张量作为它们的输入。当输入数据的范围大致从 0 到 1,或从-1 到 1 时,神经网络表现出最佳的训练性能(这是由它们的构建块定义方式所决定的效果)。
因此,我们通常需要做的一件事是将张量转换为浮点数并对像素的值进行归一化。将其转换为浮点数很容易,但归一化则更加棘手,因为它取决于我们决定将输入的哪个范围置于 0 和 1 之间(或-1 和 1 之间)。一种可能性是仅通过 255(8 位无符号数中的最大可表示数)来除以像素的值:
# In[6]: batch = batch.float() batch /= 255.0
另一种可能性是计算输入数据的均值和标准差,并对其进行缩放,使输出在每个通道上具有零均值和单位标准差:
# In[7]: n_channels = batch.shape[1] for c in range(n_channels): mean = torch.mean(batch[:, c]) std = torch.std(batch[:, c]) batch[:, c] = (batch[:, c] - mean) / std
注意 这里,我们仅对一批图像进行归一化,因为我们还不知道如何操作整个数据集。在处理图像时,最好提前计算所有训练数据的均值和标准差,然后减去并除以这些固定的、预先计算的量。我们在第 2.1.4 节中的图像分类器的预处理中看到了这一点。
我们可以对输入执行几种其他操作,如几何变换(旋转、缩放和裁剪)。这些操作可能有助于训练,或者可能需要使任意输入符合网络的输入要求,比如图像的大小。我们将在第 12.6 节中遇到许多这些策略。现在,只需记住你有可用的图像处理选项即可。
4.2 3D 图像:体积数据
我们已经学会了如何加载和表示 2D 图像,就像我们用相机拍摄的那些图像一样。在某些情境下,比如涉及 CT(计算机断层扫描)扫描的医学成像应用中,我们通常处理沿着头到脚轴堆叠的图像序列,每个图像对应于人体的一个切片。在 CT 扫描中,强度代表了身体不同部位的密度–肺部、脂肪、水、肌肉和骨骼,按照密度递增的顺序–在临床工作站上显示 CT 扫描时,从暗到亮进行映射。每个点的密度是根据穿过身体后到达探测器的 X 射线量计算的,通过一些复杂的数学将原始传感器数据解卷积为完整体积。
CT(计算机断层扫描)只有一个强度通道,类似于灰度图像。这意味着通常情况下,原生数据格式中会省略通道维度;因此,类似于上一节,原始数据通常具有三个维度。通过将单个 2D 切片堆叠成 3D 张量,我们可以构建代表主体的 3D 解剖结构的体积数据。与我们在图 4.1 中看到的情况不同,图 4.2 中的额外维度代表的是物理空间中的偏移,而不是可见光谱中的特定波段。
图 4.2 从头部到下颌的 CT 扫描切片
本书的第二部分将致力于解决现实世界中的医学成像问题,因此我们不会深入讨论医学成像数据格式的细节。目前,可以说存储体积数据与图像数据的张量之间没有根本区别。我们只是在通道维度之后多了一个维度,深度,导致了一个形状为N × C × D × H × W的 5D 张量。
4.2.1 加载专用格式
让我们使用imageio
模块中的volread
函数加载一个样本 CT 扫描,该函数以一个目录作为参数,并将所有数字影像与通信医学(DICOM)文件²组装成一个 NumPy 3D 数组(code/p1ch4/ 2_volumetric_ct.ipynb)。
代码清单 4.2 code/p1ch4/2_volumetric_ct.ipynb
# In[2]: import imageio dir_path = "../data/p1ch4/volumetric-dicom/2-LUNG 3.0 B70f-04083" vol_arr = imageio.volread(dir_path, 'DICOM') vol_arr.shape # Out[2]: Reading DICOM (examining files): 1/99 files (1.0%99/99 files (100.0%) Found 1 correct series. Reading DICOM (loading data): 31/99 (31.392/99 (92.999/99 (100.0%) (99, 512, 512)
就像在第 4.1.3 节中所述,布局与 PyTorch 期望的不同,因为没有通道信息。因此,我们将使用unsqueeze
为channel
维度腾出空间:
# In[3]: vol = torch.from_numpy(vol_arr).float() vol = torch.unsqueeze(vol, 0) vol.shape # Out[3]: torch.Size([1, 99, 512, 512])
此时,我们可以通过沿着batch
方向堆叠多个体积来组装一个 5D 数据集,就像我们在上一节中所做的那样。在第二部分中我们将看到更多的 CT 数据。
4.3 表格数据的表示
在机器学习工作中我们会遇到的最简单形式的数据位于电子表格、CSV 文件或数据库中。无论媒介如何,它都是一个包含每个样本(或记录)一行的表格,其中列包含关于我们样本的一条信息。
起初,我们假设表格中样本出现的顺序没有意义:这样的表格是独立样本的集合,不像时间序列那样,其中样本由时间维度相关联。
列可能包含数值,例如特定位置的温度;或标签,例如表示样本属性的字符串,如“蓝色”。因此,表格数据通常不是同质的:不同列的类型不同。我们可能有一列显示苹果的重量,另一列用标签编码它们的颜色。
另一方面,PyTorch 张量是同质的。PyTorch 中的信息通常被编码为一个数字,通常是浮点数(尽管也支持整数类型和布尔类型)。这种数字编码是有意的,因为神经网络是数学实体,通过矩阵乘法和非线性函数的连续应用将实数作为输入并产生实数作为输出。
4.3.1 使用真实世界数据集
作为深度学习从业者的第一项工作是将异构的现实世界数据编码为浮点数张量,以便神经网络消费。互联网上有大量的表格数据集可供免费使用;例如,可以查看github.com/caesar0301/awesome-public-datasets
。让我们从一些有趣的东西开始:葡萄酒!葡萄酒质量数据集是一个包含葡萄牙北部绿葡萄酒样本的化学特征和感官质量评分的免费表格。白葡萄酒数据集可以在这里下载:mng.bz/90Ol
。为了方便起见,我们还在 Deep Learning with PyTorch Git 存储库中的 data/p1ch4/tabular-wine 目录下创建了数据集的副本。
该文件包含一个逗号分隔的值集合,由一个包含列名的标题行引导。前 11 列包含化学变量的值,最后一列包含从 0(非常糟糕)到 10(优秀)的感官质量评分。这些是数据集中按照它们出现的顺序的列名:
fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality
在这个数据集上的一个可能的机器学习任务是仅通过化学特征预测质量评分。不过,不用担心;机器学习不会很快消灭品酒。我们必须从某处获取训练数据!正如我们在图 4.3 中看到的,我们希望在我们的数据中的化学列和质量列之间找到一个关系。在这里,我们期望看到随着硫的减少,质量会提高。
图 4.3 我们(希望)在葡萄酒中硫和质量之间的关系
4.3.2 加载葡萄酒数据张量
然而,在进行这之前,我们需要以一种比在文本编辑器中打开文件更可用的方式来检查数据。让我们看看如何使用 Python 加载数据,然后将其转换为 PyTorch 张量。Python 提供了几种快速加载 CSV 文件的选项。三种流行的选项是
- Python 自带的
csv
模块 - NumPy
- Pandas
第三个选项是最节省时间和内存的。然而,我们将避免在我们的学习轨迹中引入额外的库,只是因为我们需要加载一个文件。由于我们在上一节中已经介绍了 NumPy,并且 PyTorch 与 NumPy 有很好的互操作性,我们将选择这个。让我们加载我们的文件,并将生成的 NumPy 数组转换为 PyTorch 张量(code/p1ch4/3_tabular_wine.ipynb)。
代码清单 4.3 code/p1ch4/3_tabular_wine.ipynb
# In[2]: import csv wine_path = "../data/p1ch4/tabular-wine/winequality-white.csv" wineq_numpy = np.loadtxt(wine_path, dtype=np.float32, delimiter=";", skiprows=1) wineq_numpy # Out[2]: array([[ 7\. , 0.27, 0.36, ..., 0.45, 8.8 , 6\. ], [ 6.3 , 0.3 , 0.34, ..., 0.49, 9.5 , 6\. ], [ 8.1 , 0.28, 0.4 , ..., 0.44, 10.1 , 6\. ], ..., [ 6.5 , 0.24, 0.19, ..., 0.46, 9.4 , 6\. ], [ 5.5 , 0.29, 0.3 , ..., 0.38, 12.8 , 7\. ], [ 6\. , 0.21, 0.38, ..., 0.32, 11.8 , 6\. ]], dtype=float32)
在这里,我们只规定 2D 数组的类型应该是 32 位浮点数,用于分隔每行值的分隔符,以及不应读取第一行,因为它包含列名。让我们检查所有数据是否都已读取
# In[3]: col_list = next(csv.reader(open(wine_path), delimiter=';')) wineq_numpy.shape, col_list # Out[3]: ((4898, 12), ['fixed acidity', 'volatile acidity', 'citric acid', 'residual sugar', 'chlorides', 'free sulfur dioxide', 'total sulfur dioxide', 'density', 'pH', 'sulphates', 'alcohol', 'quality'])
然后将 NumPy 数组转换为 PyTorch 张量:
# In[4]: wineq = torch.from_numpy(wineq_numpy) wineq.shape, wineq.dtype # Out[4]: (torch.Size([4898, 12]), torch.float32)
此时,我们有一个包含所有列的浮点torch.Tensor
,包括最后一列,指的是质量评分。³
连续值、有序值和分类值
当我们试图理解数据时,我们应该意识到三种不同类型的数值。第一种是连续值。当以数字表示时,这些值是最直观的。它们是严格有序的,各个值之间的差异具有严格的含义。声明 A 包比 B 包重 2 千克,或者 B 包比 A 包远 100 英里,无论 A 包是 3 千克还是 10 千克,或者 B 包来自 200 英里还是 2,000 英里,都有固定的含义。如果你在计数或测量带单位的东西,那么它很可能是一个连续值。文献实际上进一步将连续值分为不同类型:在前面的例子中,说某物体重两倍或距离远三倍是有意义的,因此这些值被称为比例尺。另一方面,一天中的时间具有差异的概念,但声称 6:00 比 3:00 晚两倍是不合理的;因此时间只提供一个区间尺度。
接下来是有序值。我们在连续值中具有的严格排序仍然存在,但值之间的固定关系不再适用。一个很好的例子是将小、中、大饮料排序,其中小映射到值 1,中 2,大 3。大饮料比中饮料大,就像 3 比 2 大一样,但这并不告诉我们有多大差异。如果我们将我们的 1、2 和 3 转换为实际容量(比如 8、12 和 24 液体盎司),那么它们将转变为区间值。重要的是要记住,我们不能在值上“做数学运算”以外的排序它们;尝试平均大=3 和小=1 并不会得到中等饮料!
最后,分类值既没有顺序也没有数值含义。这些通常只是分配了任意数字的可能性枚举。将水分配给 1,咖啡分配给 2,苏打分配给 3,牛奶分配给 4 就是一个很好的例子。将水放在第一位,牛奶放在最后一位并没有真正的逻辑;它们只是需要不同的值来区分它们。我们可以将咖啡分配给 10,牛奶分配给-3,这样也不会有显著变化(尽管在范围 0…N - 1 内分配值将对独热编码和我们将在第 4.5.4 节讨论的嵌入有优势)。因为数值值没有含义,它们被称为名义尺度。
4.3.3 表示分数
我们可以将分数视为连续变量,保留为实数,并执行回归任务,或将其视为标签并尝试从化学分析中猜测标签以进行分类任务。在这两种方法中,我们通常会从输入数据张量中删除分数,并将其保留在单独的张量中,以便我们可以将分数用作地面实况,而不将其作为模型的输入:
# In[5]: data = wineq[:, :-1] # ❶ data, data.shape # Out[5]: (tensor([[ 7.00, 0.27, ..., 0.45, 8.80], [ 6.30, 0.30, ..., 0.49, 9.50], ..., [ 5.50, 0.29, ..., 0.38, 12.80], [ 6.00, 0.21, ..., 0.32, 11.80]]), torch.Size([4898, 11])) # In[6]: target = wineq[:, -1] # ❷ target, target.shape # Out[6]: (tensor([6., 6., ..., 7., 6.]), torch.Size([4898]))
❶ 选择所有行和除最后一列之外的所有列
❷ 选择所有行和最后一列
如果我们想要将target
张量转换为标签张量,我们有两种选择,取决于策略或我们如何使用分类数据。一种方法是简单地将标签视为整数分数的向量:
# In[7]: target = wineq[:, -1].long() target # Out[7]: tensor([6, 6, ..., 7, 6])
如果目标是字符串标签,比如葡萄酒颜色,为每个字符串分配一个整数编号将让我们遵循相同的方法。
4.3.4 独热编码
另一种方法是构建分数的独热编码:即,将 10 个分数中的每一个编码为一个具有 10 个元素的向量,其中所有元素均设置为 0,但一个元素在每个分数的不同索引上设置为 1。 这样,分数 1 可以映射到向量(1,0,0,0,0,0,0,0,0,0)
,分数 5 可以映射到(0,0,0,0,1,0,0,0,0,0)
,依此类推。请注意,分数对应于非零元素的索引纯属偶然:我们可以重新排列分配,从分类的角度来看,没有任何变化。
这两种方法之间有明显的区别。将葡萄酒质量分数保留在整数分数向量中会对分数产生排序–这在这种情况下可能是完全合适的,因为 1 分比 4 分低。它还会在分数之间引入某种距离:也就是说,1 和 3 之间的距离与 2 和 4 之间的距离相同。如果这对我们的数量成立,那就太好了。另一方面,如果分数完全是离散的,比如葡萄品种,独热编码将更适合,因为没有暗示的排序或距离。当分数是连续变量时,独热编码也适用,例如在整数分数之间没有意义的情况下,比如 2.4,对于应用程序来说要么是这个要么是那个。
我们可以使用scatter_
方法实现独热编码,该方法将源张量中的值沿提供的索引填充到张量中:
# In[8]: target_onehot = torch.zeros(target.shape[0], 10) target_onehot.scatter_(1, target.unsqueeze(1), 1.0) # Out[8]: tensor([[0., 0., ..., 0., 0.], [0., 0., ..., 0., 0.], ..., [0., 0., ..., 0., 0.], [0., 0., ..., 0., 0.]])
让我们看看scatter_
做了什么。首先,我们注意到它的名称以下划线结尾。正如您在上一章中学到的,这是 PyTorch 中的一种约定,表示该方法不会返回新张量,而是会直接修改张量。scatter_
的参数如下:
- 指定以下两个参数的维度
- 指示要散布元素索引的列张量
- 包含要散布的元素或要散布的单个标量的张量(在本例中为 1)
换句话说,前面的调用读取,“对于每一行,取目标标签的索引(在我们的情况下与分数相符)并将其用作列索引设置值为 1.0。” 最终结果是一个编码分类信息的张量。
scatter_
的第二个参数,索引张量,需要与我们要散布到的张量具有相同数量的维度。由于target_onehot
有两个维度(4,898 × 10),我们需要使用unsqueeze
添加一个额外的虚拟维度到target
中:
# In[9]: target_unsqueezed = target.unsqueeze(1) target_unsqueezed # Out[9]: tensor([[6], [6], ..., [7], [6]])
调用unsqueeze
函数会添加一个单例维度,将一个包含 4,898 个元素的 1D 张量转换为一个大小为 (4,898 × 1) 的 2D 张量,而不改变其内容–不会添加额外的元素;我们只是决定使用额外的索引来访问元素。也就是说,我们可以通过target[0]
访问target
的第一个元素,通过target_unsqueezed[0,0]
访问其未挤压的对应元素。
PyTorch 允许我们在训练神经网络时直接使用类索引作为目标。但是,如果我们想将分数用作网络的分类输入,我们将不得不将其转换为一个独热编码张量。
4.3.5 何时进行分类
现在我们已经看到了如何处理连续和分类数据。您可能想知道早期边栏中讨论的有序情况是什么情况。对于这种情况,没有通用的处理方法;最常见的做法是将这些数据视为分类数据(失去排序部分,并希望也许我们的模型在训练过程中会捕捉到它,如果我们只有少数类别)或连续数据(引入一个任意的距离概念)。我们将在图 4.5 中的天气情况中采取后者。我们在图 4.4 中的一个小流程图中总结了我们的数据映射。
图 4.4 如何处理连续、有序和分类数据的列
让我们回到包含与化学分析相关的 11 个变量的data
张量。我们可以使用 PyTorch 张量 API 中的函数以张量形式操作我们的数据。让我们首先获取每列的均值和标准差:
# In[10]: data_mean = torch.mean(data, dim=0) data_mean # Out[10]: tensor([6.85e+00, 2.78e-01, 3.34e-01, 6.39e+00, 4.58e-02, 3.53e+01, 1.38e+02, 9.94e-01, 3.19e+00, 4.90e-01, 1.05e+01]) # In[11]: data_var = torch.var(data, dim=0) data_var # Out[11]: tensor([7.12e-01, 1.02e-02, 1.46e-02, 2.57e+01, 4.77e-04, 2.89e+02, 1.81e+03, 8.95e-06, 2.28e-02, 1.30e-02, 1.51e+00])
在这种情况下,dim=0
表示沿着维度 0 执行缩减。此时,我们可以通过减去均值并除以标准差来对数据进行归一化,这有助于学习过程(我们将在第五章的 5.4.4 节中更详细地讨论这一点):
# In[12]: data_normalized = (data - data_mean) / torch.sqrt(data_var) data_normalized # Out[12]: tensor([[ 1.72e-01, -8.18e-02, ..., -3.49e-01, -1.39e+00], [-6.57e-01, 2.16e-01, ..., 1.35e-03, -8.24e-01], ..., [-1.61e+00, 1.17e-01, ..., -9.63e-01, 1.86e+00], [-1.01e+00, -6.77e-01, ..., -1.49e+00, 1.04e+00]])
4.3.6 寻找阈值
接下来,让我们开始查看数据,看看是否有一种简单的方法可以一眼看出好酒和坏酒的区别。首先,我们将确定target
中对应于得分小于或等于 3 的行:
# In[13]: bad_indexes = target <= 3 # ❶ bad_indexes.shape, bad_indexes.dtype, bad_indexes.sum() # Out[13]: (torch.Size([4898]), torch.bool, tensor(20))
❶ PyTorch 还提供比较函数,例如 torch.le(target, 3),但使用运算符似乎是一个很好的标准。
注意,bad_indexes
中只有 20 个条目被设置为True
!通过使用 PyTorch 中称为高级索引的功能,我们可以使用数据类型为torch.bool
的张量来索引data
张量。这将基本上将data
过滤为仅包含索引张量中为True
的项目(或行)的项。bad_indexes
张量与target
具有相同的形状,其值为False
或True
,取决于我们的阈值与原始target
张量中每个元素之间的比较结果:
# In[14]: bad_data = data[bad_indexes] bad_data.shape # Out[14]: torch.Size([20, 11])
注意,新的bad_data
张量有 20 行,与bad_indexes
张量中为True
的行数相同。它保留了所有 11 列。现在我们可以开始获取关于被分为好、中等和差类别的葡萄酒的信息。让我们对每列进行.mean()
操作:
# In[15]: bad_data = data[target <= 3] mid_data = data[(target > 3) & (target < 7)] # ❶ good_data = data[target >= 7] bad_mean = torch.mean(bad_data, dim=0) mid_mean = torch.mean(mid_data, dim=0) good_mean = torch.mean(good_data, dim=0) for i, args in enumerate(zip(col_list, bad_mean, mid_mean, good_mean)): print('{:2} {:20} {:6.2f} {:6.2f} {:6.2f}'.format(i, *args)) # Out[15]: 0 fixed acidity 7.60 6.89 6.73 1 volatile acidity 0.33 0.28 0.27 2 citric acid 0.34 0.34 0.33 3 residual sugar 6.39 6.71 5.26 4 chlorides 0.05 0.05 0.04 5 free sulfur dioxide 53.33 35.42 34.55 6 total sulfur dioxide 170.60 141.83 125.25 7 density 0.99 0.99 0.99 8 pH 3.19 3.18 3.22 9 sulphates 0.47 0.49 0.50 10 alcohol 10.34 10.26 11.42
❶ 对于布尔 NumPy 数组和 PyTorch 张量,& 运算符执行逻辑“与”操作。
看起来我们有所发现:乍一看,坏酒似乎具有更高的总二氧化硫含量,等等其他差异。我们可以使用总二氧化硫的阈值作为区分好酒和坏酒的粗略标准。让我们获取总二氧化硫列低于我们之前计算的中点的索引,如下所示:
# In[16]: total_sulfur_threshold = 141.83 total_sulfur_data = data[:,6] predicted_indexes = torch.lt(total_sulfur_data, total_sulfur_threshold) predicted_indexes.shape, predicted_indexes.dtype, predicted_indexes.sum() # Out[16]: (torch.Size([4898]), torch.bool, tensor(2727))
这意味着我们的阈值意味着超过一半的葡萄酒将是高质量的。接下来,我们需要获取实际好葡萄酒的索引:
# In[17]: actual_indexes = target > 5 actual_indexes.shape, actual_indexes.dtype, actual_indexes.sum() # Out[17]: (torch.Size([4898]), torch.bool, tensor(3258))
由于实际好酒比我们的阈值预测多约 500 瓶,我们已经有了不完美的确凿证据。现在我们需要看看我们的预测与实际排名的匹配程度。我们将在我们的预测索引和实际好酒索引之间执行逻辑“与”(记住每个都只是一个由零和一组成的数组),并使用这些一致的酒来确定我们的表现如何:
# In[18]: n_matches = torch.sum(actual_indexes & predicted_indexes).item() n_predicted = torch.sum(predicted_indexes).item() n_actual = torch.sum(actual_indexes).item() n_matches, n_matches / n_predicted, n_matches / n_actual # Out[18]: (2018, 0.74000733406674, 0.6193984039287906)
我们大约有 2,000 瓶酒是正确的!由于我们预测了 2,700 瓶酒,这给了我们 74%的机会,如果我们预测一瓶酒是高质量的,那它实际上就是。不幸的是,有 3,200 瓶好酒,我们只识别了其中的 61%。嗯,我们得到了我们签约的东西;这几乎比随机好不了多少!当然,这一切都很天真:我们确切地知道多个变量影响葡萄酒的质量,这些变量的值与结果之间的关系(可能是实际分数,而不是其二值化版本)可能比单个值的简单阈值更复杂。
实际上,一个简单的神经网络将克服所有这些限制,许多其他基本的机器学习方法也将克服这些限制。在接下来的两章中,一旦我们学会如何从头开始构建我们的第一个神经网络,我们将有解决这个问题的工具。我们还将在第十二章重新审视如何更好地评估我们的结果。现在让我们继续探讨其他数据类型。
PyTorch 深度学习(GPT 重译)(二)(2)https://developer.aliyun.com/article/1485204