本文内容整理自书籍《Deep Learning with PyTorch》 https://pytorch.org/deep-learning-with-pytorch-thank-you
本章涵盖了
在尝试理解数据时,应注意三种数值。
总结:连续值严格排序,各个值之间的差异具有严格的含义,可以进行数学运算;有序值严格排序,不可以进行数学运算;分类值仅有逻辑上的区别,排序和数学运算都没有意义。
每个书面字符都由一个代码(数字)表示,该代码是一系列适当长度的bits,可以唯一地标识每个字符。下面介绍两种常见的编码,ASCII和Unicode编码:
RGB色彩模式是工业界的一种颜色标准,是通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是目前运用最广的颜色系统之一。
图像表示为标量的集合,这些标量以规则的网格排列,并具有高度和宽度(以像素为单位)。每个网格点(像素)可能只有一个标量,可以表示为灰度图像,每个网格点可能有多个标量,它们通常代表不同的颜色或不同的功能,例如深度相机的深度。
代表单个像素值的标量通常使用8位整数编码,例如在消费类相机中。在医学,科学和工业应用中,您会发现具有较高数字精度的像素,例如12位和16位。在像素编码有关物理特性(例如骨密度,温度或深度)的信息的情况下,此精度可提供更大的范围或更高的灵敏度。
有几种将数字编码为颜色的方法。最常见的是 RGB,它定义了一种具有三个数字的颜色,分别代表红色,绿色和蓝色的强度。您可以将颜色通道视为仅是所讨论颜色的灰度强度图,类似于您通过一副纯红色太阳镜观察场景时所看到的情况。
从信息论的观点来看,描述信源的数据是信息和数据冗余之和,即:数据=信息+数据冗余。音频信号在时域和频域上具有相关性,也即存在数据冗余。将音频作为一个信源,音频编码的实质是减少音频中的冗余。自然界中的声音非常复杂,波形极其复杂,通常我们采用的是脉冲代码调制编码,即PCM编码。PCM通过抽样、量化、编码三个步骤将连续变化的模拟信号转换为数字编码。
未经过数据压缩,直接量化进行传输则被称为PCM(脉冲编码调制)。
音频采样率。音频采样率是指录音设备在一秒钟内对声音信号的采样次数,采样频率越高声音的还原就越真实越自然。在当今的主流采集卡上,采样频率一般共分为11025Hz、22050Hz、24000Hz、44100Hz、48000Hz五个等级,11025Hz能达到AM调幅广播的声音品质,而22050Hz和24000HZ能达到FM调频广播的声音品质,44100Hz则是理论上的CD音质界限,48000Hz则更加精确一些。
脉冲编码调制。脉冲编码调制(PulseCodeModulation),简称PCM。是对连续变化的模拟信号进行抽样、量化和编码产生的数字信号。
声音可以看作是某种介质(例如空气)在特定时间的压力波动。为了使人耳欣赏声音,压力必须以每秒20到20000次振荡之间的频率波动(以赫兹(Hz)为单位)。每秒更多的振荡将导致更高的感知音调。
通过使用麦克风及时记录压力波动并将每个时间点的每个压力水平转换为一个数字(例如16位整数),我们现在可以将声音表示为数字矢量。这就是所谓的脉冲编码调制(PCM),其中连续信号既可以在时间上采样,又可以在幅度上量化。如果要确保在录音中听到最高音调,则必须以略大于最大可听频率的两倍(即每秒刚超过40000次)的频率来录制样本。音频CD的采样频率为44100 Hz并非偶然。这意味着,如果不压缩就存储一个1小时(3600秒)的立体声(即2通道)CD轨道,以16位精度记录样本,则将为2 * 16 * 44100 * 3600 = 5080320000 bit= 605.6 MB。
音频格式过多,最流行的是WAV,AIFF,MP3,AAC,其中通常利用两个立体声通道之间时间序列中连续采样之间的相关性,以压缩形式对原始音频信号进行编码。作为消除几乎听不见的频率。这样可以大大降低存储要求(一个1小时的AAC格式的音频文件占用的空间不到60 MB)。此外,音频播放器可以在专用硬件上即时解码这些格式,而消耗的功率却很少。
import torch
import numpy as np
import csv
import os
import imageio
from multiprocessing import Pool
import scipy.io.wavfile as wavfile
from scipy import signal
DATA_ROOT_DIR = "PyTorch_learn/data"
if not os.path.exists(DATA_ROOT_DIR):
os.makedirs(DATA_ROOT_DIR)
葡萄酒质量数据集是可免费获得的表格数据,其中包含vinho verde(葡萄牙北部的葡萄酒)样品的化学特征以及感官质量得分。 您可以从 https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv 下载白葡萄酒的数据集。
该数据集文件包含一个逗号分隔的值集合,这些值组织在12列中,前面是包含列名的标题行。前11列表示化学变量的值;最后一列包含从0(最差)到10(最好)的感官质量评分。
[('fixed acidity ', '固定酸度'), ('volatile acidity ', '挥发性酸度'), ('citric acid ', '柠檬酸'), ('residual sugar ', '残留糖'), ('chlorides ', '氯化物'), ('free sulfur dioxide ', '游离二氧化硫'), ('total sulfur dioxide ', '总二氧化硫'), ('density ', '密度'), ('pH ', 'pH值'), ('sulphates ', '硫酸盐'), ('alcohol ', '醇'), ('qualit', '质量')]
!wget https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv -q
!mv winequality-white.csv $DATA_ROOT_DIR
wine_quality_data_path = os.path.join(DATA_ROOT_DIR, "winequality-white.csv")
wineq_numpy = np.loadtxt(wine_quality_data_path, dtype=np.float32, delimiter=";", skiprows=1)
wineq_numpy
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位浮点数)和用于分隔每行中值的定界符,并指出不应读取第一行,因为它包含列名。 接下来,检查是否已读取所有数据,
col_list = next(csv.reader(open(wine_quality_data_path), delimiter=';'))
wineq_numpy.shape, col_list
((4898, 12), ['fixed acidity', 'volatile acidity', 'citric acid', 'residual sugar', 'chlorides', 'free sulfur dioxide', 'total sulfur dioxide', 'density', 'pH', 'sulphates', 'alcohol', 'quality'])
然后把NumPy数组转换成一个PyTorch张量:
wineq = torch.from_numpy(wineq_numpy)
wineq.shape, wineq.type()
(torch.Size([4898, 12]), 'torch.FloatTensor')
此时,您将拥有一个torch.FloatTensor,其中包含所有列,包括最后一个列,这是质量得分
您可以将分数视为连续变量,将其保留为实数,然后执行回归任务,或者将其视为标签,然后尝试从分类任务中的化学分析中猜出该标签。 在这两种方法中,通常都将分数从输入数据的张量中删除,并将其保存在单独的张量中,以便您可以将分数用作基本事实,而无需将其输入到模型中:
data = wineq[:, :-1] # 选择除最后一列之外的所有行和列。
data, data.shape
(tensor([[ 7.0000, 0.2700, 0.3600, ..., 3.0000, 0.4500, 8.8000], [ 6.3000, 0.3000, 0.3400, ..., 3.3000, 0.4900, 9.5000], [ 8.1000, 0.2800, 0.4000, ..., 3.2600, 0.4400, 10.1000], ..., [ 6.5000, 0.2400, 0.1900, ..., 2.9900, 0.4600, 9.4000], [ 5.5000, 0.2900, 0.3000, ..., 3.3400, 0.3800, 12.8000], [ 6.0000, 0.2100, 0.3800, ..., 3.2600, 0.3200, 11.8000]]), torch.Size([4898, 11]))
target = wineq[:, -1] # 选择所有行和最后一列。
target, target.shape
(tensor([6., 6., 6., ..., 6., 7., 6.]), torch.Size([4898]))
如果要在标签的张量中转换目标张量,则有两个选项,具体取决于策略或要使用分类数据的方式。 一种选择是将标签视为分数的整数向量:
target = wineq[:, -1].long()
target
tensor([6, 6, 6, ..., 6, 7, 6])
另一种方法是构建分数的one-hot编码:
两种方法有明显的区别。将葡萄酒质量分数保持在分数的整数向量中会导致分数排序,这在这种情况下可能是适当的,因为分数1低于分数4。这还会在分数之间产生一定的距离。 (例如1和3之间的距离与2和4之间的距离相同。)如果这符合您的数量,那就太好了。另一方面,如果分数是纯定性的(例如颜色),则单热编码更适合,因为不涉及隐含的顺序或距离。当整数分数(例如2.4)之间的分数值对应用没有意义时,one-hot编码适用于定量分数。
您可以使用scatter_方法来实现one-hot编码,该方法将源张量中的值沿作为参数提供的索引填充张量。
target_onehot = torch.zeros(target.shape[0], 10)
target_onehot.scatter_(1, target.unsqueeze(1), 1.0)
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., ..., 1., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]])
现在看一下scatter_的作用。 首先,请注意,其名称以下划线结尾。PyTorch中的此约定表示该方法不会返回新的张量,而是会在就地修改张量。scatter_的参数是
换句话说,前面的调用是这样写的:“对于每一行,获取目标标签(分数)的索引(在该例子中,该分数与分数的位置索引一致),并将其用作列索引以设置值1.0。 结果是张量编码分类信息。”
scatter_的第二个参数,即索引张量,必须具有与刷新(scatter)到其中的张量相同的维数。 由于target_onehot具有二维(4898x10),因此您需要使用unsqueeze为target添加一个额外的虚拟尺寸:
target_unsqueezed = target.unsqueeze(1)
target_unsqueezed
tensor([[6], [6], [6], ..., [6], [7], [6]])
unsqueeze的调用增加了一个单例尺寸,从4898个元素的1D张量到大小(4898x1)的2D张量,而无需更改其内容。 没有添加元素; 您决定使用额外的索引来访问元素。 也就是说,您以target [0]访问target的第一个元素,并以target_unsqueezed [0,0]访问其未压缩对象的第一个元素。
PyTorch允许您在训练神经网络时直接将类索引用作目标(target)。 但是,如果您想将分数用作网络的模型输入,则必须将其转换为one-hot编码张量(因为这里分数作为分类值)。
现在返回到data张量,其中包含与化学分析关联的11个变量。 您可以使用PyTorch Tensor API中的函数以张量形式处理数据。 首先,获取每列的均值和标准差:
data_mean = torch.mean(data, dim=0)
data_mean
tensor([6.8548e+00, 2.7824e-01, 3.3419e-01, 6.3914e+00, 4.5772e-02, 3.5308e+01, 1.3836e+02, 9.9403e-01, 3.1883e+00, 4.8985e-01, 1.0514e+01])
data_var = torch.var(data, dim=0)
data_var
tensor([7.1211e-01, 1.0160e-02, 1.4646e-02, 2.5726e+01, 4.7733e-04, 2.8924e+02, 1.8061e+03, 8.9455e-06, 2.2801e-02, 1.3025e-02, 1.5144e+00])
在这种情况下,dim=0表示沿尺寸0进行缩减。此时,您可以通过减去平均值并除以标准差来规范化数据,这有助于学习过程(模型训练)。
data_normalized = (data - data_mean) / torch.sqrt(data_var)
data_normalized
tensor([[ 1.7209e-01, -8.1764e-02, 2.1325e-01, ..., -1.2468e+00, -3.4914e-01, -1.3930e+00], [-6.5743e-01, 2.1587e-01, 4.7991e-02, ..., 7.3992e-01, 1.3467e-03, -8.2418e-01], [ 1.4756e+00, 1.7448e-02, 5.4378e-01, ..., 4.7502e-01, -4.3677e-01, -3.3662e-01], ..., [-4.2042e-01, -3.7940e-01, -1.1915e+00, ..., -1.3131e+00, -2.6152e-01, -9.0544e-01], [-1.6054e+00, 1.1666e-01, -2.8253e-01, ..., 1.0048e+00, -9.6250e-01, 1.8574e+00], [-1.0129e+00, -6.7703e-01, 3.7852e-01, ..., 4.7502e-01, -1.4882e+00, 1.0448e+00]])
接下来,用眼睛看数据,寻找一种简单的方法,一眼就能分辨出好葡萄酒和坏葡萄酒。首先,使用torch.le函数确定目标中的哪些行对应于小于或等于3的分数:
bad_indexes = torch.le(target, 3)
bad_indexes.shape, bad_indexes.dtype, bad_indexes.sum()
(torch.Size([4898]), torch.bool, tensor(20))
注意,bad_indexes条目中只有20个被设置为1! 通过利用PyTorch中称为高级索引的功能,可以使用二进制张量来索引数据张量。 此张量实际上将数据筛选为仅与索引张量中的1对应的项目(或行)。 bad_indexes张量具有与目标相同的形状,其值是0或1,具体取决于阈值与原始目标张量中每个元素之间比较的结果:
bad_data = data[bad_indexes]
bad_data.shape
torch.Size([20, 11])
请注意,新的bad_data张量具有20行,与bad_indexes张量中带有1的行数相同。 它保留所有11列。 现在,您可以开始获取有关葡萄酒的信息,这些葡萄酒分为好,中和坏三类。 取每列的.mean():
bad_data = data[torch.le(target, 3)]
mid_data = data[torch.gt(target, 3) & torch.lt(target, 7)]
good_data = data[torch.ge(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))
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
乍一看,劣质葡萄酒似乎具有更高的总二氧化硫含量,还有其他差异。您可以使用二氧化硫总量的阈值作为区分好酒和差酒的粗略标准。现在获取总二氧化硫柱低于您之前计算的中点的索引,如下所示:
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()
(torch.Size([4898]), torch.bool, tensor(2727))
你的threshold意味着略多于一半的葡萄酒将是高品质的。接下来,你需要得到好酒的索引:
actual_indexes = torch.gt(target, 5)
actual_indexes.shape, actual_indexes.dtype, actual_indexes.sum()
(torch.Size([4898]), torch.bool, tensor(3258))
由于您拥有的优质葡萄酒比您预计的阈值多约500种,因此您已经有确凿的证据证明阈值并不完美。 现在,您需要查看您的预测与实际排名的吻合程度。 在预测索引和良好索引之间执行逻辑运算(请记住,每个索引是0和1组成的数组),并使用葡萄酒的交集来确定您的表现如何:
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_predicted, n_actual, n_matches / n_predicted, n_matches / n_actual
(2018, 2727, 3258, 0.74000733406674, 0.6193984039287906)
您有大约2,000瓶葡萄酒预测正确! 因为您预测有2700瓶葡萄酒,所以如果您预测葡萄酒是高质量的,那么就有74%的可能性(精确率)。 不幸的是,您有3200瓶优质葡萄酒,仅能识别出61%(召回率)。该结果仅比随机结果好。
当然,这个例子很简单。 您可以肯定地知道,多个变量会影响葡萄酒的质量,这些变量的值与结果之间的关系(可能是实际分数,而不是二值化版本)可能比简单的阈值更为复杂。
华盛顿特区自行车共享系统中的数据,报告了首都自行车共享系统中2011年至2012年之间的每小时租赁自行车计数以及相应的天气和季节性信息。您可以从 https://archive.ics.uci.edu/ml/datasets/bike+sharing+dataset 下载Bike-Sharing-Dataset.zip数据集。也可以直接从 https://github.com/deep-learning-with-pytorch/dlwpt-code/tree/master/data/p1ch4/bike-sharing-dataset 下载处理好的 hour-fixed.csv 数据集。
对于每小时,数据集报告以下变量:
+ instant | index of record
+ day | day of month
+ season | season (1: spring, 2: summer, 3: fall, 4: winter)
+ yr | year (0: 2011, 1: 2012)
+ mnth | month (1 to 12)
+ hr | hour (0 to 23)
+ holiday | holiday status
+ weekday | day of the week
+ workingday | working day status
+ weathersit | weather situation
| (1: clear, 2:mist, 3: light rain/snow, 4: heavy rain/snow)
+ temp | temperature in C
+ atemp | perceived temperature in C
+ hum | humidity
+ windspeed | windspeed
+ casual | number of causal users
+ registered | number of registered users
+ cnt | count of rental bikes
!wget https://raw.githubusercontent.com/deep-learning-with-pytorch/dlwpt-code/master/data/p1ch4/bike-sharing-dataset/hour-fixed.csv
--2019-11-27 10:06:41-- https://raw.githubusercontent.com/deep-learning-with-pytorch/dlwpt-code/master/data/p1ch4/bike-sharing-dataset/hour-fixed.csv Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 151.101.76.133 Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|151.101.76.133|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 1148001 (1.1M) [text/plain] Saving to: ‘hour-fixed.csv’ hour-fixed.csv 100%[===================>] 1.09M 418KB/s in 2.7s 2019-11-27 10:06:46 (418 KB/s) - ‘hour-fixed.csv’ saved [1148001/1148001]
!mv hour-fixed.csv $DATA_ROOT_DIR
bike_hour_fixed_csv_path = os.path.join(DATA_ROOT_DIR, 'hour-fixed.csv')
bikes_numpy = np.loadtxt(bike_hour_fixed_csv_path,
dtype=np.float32,
delimiter=",",
skiprows=1,
converters={1: lambda x: float(x[8:10])}) # Convert date strings to numbers corresponding to the day of the month in column
bikes = torch.from_numpy(bikes_numpy)
bikes
tensor([[1.0000e+00, 1.0000e+00, 1.0000e+00, ..., 3.0000e+00, 1.3000e+01, 1.6000e+01], [2.0000e+00, 1.0000e+00, 1.0000e+00, ..., 8.0000e+00, 3.2000e+01, 4.0000e+01], [3.0000e+00, 1.0000e+00, 1.0000e+00, ..., 5.0000e+00, 2.7000e+01, 3.2000e+01], ..., [1.7377e+04, 3.1000e+01, 1.0000e+00, ..., 7.0000e+00, 8.3000e+01, 9.0000e+01], [1.7378e+04, 3.1000e+01, 1.0000e+00, ..., 1.3000e+01, 4.8000e+01, 6.1000e+01], [1.7379e+04, 3.1000e+01, 1.0000e+00, ..., 1.2000e+01, 3.7000e+01, 4.9000e+01]])
在诸如此类的时间序列数据集中,行表示连续的时间点:按其排序的维度。 当然,您可以将每一行视为独立行,并尝试根据例如一天中的特定时间来预测循环自行车的数量,而与之前发生的情况无关。
但是,这种排序的存在使您有机会利用跨时间的因果关系。 例如,您可以早些时候下雨的事实来预测自行车的骑行次数。暂时,您将专注于学习如何将自行车共享数据集转换为神经网络可以以固定大小的块提取的数据。
该神经网络模型需要查看每个数量的值序列,例如乘车次数,一天中的时间,温度和天气状况,因此N个并行的大小为C的并行序列。C代表通道,在神经网络中,它是与一维数据列相同,如此处所示。 N维代表时间轴,这里是每小时输入一次。
您可能希望在更宽的观察期内(例如天)分解2年数据集。这样,您将获得N个(样本数)长度为L的C个序列的集合。换句话说,您的时间序列数据集是维度为3的张量,形状为N x C xL。C仍然是17个通道,而L则是一天中每小时24个通道。没有必要特别说明为什么我们必须使用24小时的大块时间,尽管一般的日常节奏可能会给我们提供可用于预测的模式。如果需要,我们可以改为使用7 * 24 = 168小时的块按周划分。
现在回到您的自行车共享数据集。第一列是索引(数据的全局顺序);第二列是日期;第六列是一天中的时间。您拥有创建行驶次数和其他外来变量的每日序列数据集所需的一切。您的数据集已经排序,但是如果没有排序,则可以在其上使用torch.sort进行适当排序。
注意:您在此处使用的文件版本hour-fixed.csv已进行了一些处理,以包含原始数据集中缺少的行。 我们假设丢失的时间有零个自行车处于活动状态(通常是清晨的时间)。
bikes.shape, bikes.stride()
(torch.Size([17520, 17]), (17, 1))
17520小时,17列。现在将数据重塑为三个轴(日、时和17列):
daily_bikes = bikes.view(-1, 24, bikes.shape[1])
daily_bikes.shape, daily_bikes.stride()
(torch.Size([730, 24, 17]), (408, 17, 1))
daily_bikes[0:2, :3, :], daily_bikes[0:2, :3, :].shape
(tensor([[[ 1.0000, 1.0000, 1.0000, 0.0000, 1.0000, 0.0000, 0.0000, 6.0000, 0.0000, 1.0000, 0.2400, 0.2879, 0.8100, 0.0000, 3.0000, 13.0000, 16.0000], [ 2.0000, 1.0000, 1.0000, 0.0000, 1.0000, 1.0000, 0.0000, 6.0000, 0.0000, 1.0000, 0.2200, 0.2727, 0.8000, 0.0000, 8.0000, 32.0000, 40.0000], [ 3.0000, 1.0000, 1.0000, 0.0000, 1.0000, 2.0000, 0.0000, 6.0000, 0.0000, 1.0000, 0.2200, 0.2727, 0.8000, 0.0000, 5.0000, 27.0000, 32.0000]], [[25.0000, 2.0000, 1.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000, 0.0000, 2.0000, 0.4600, 0.4545, 0.8800, 0.2985, 4.0000, 13.0000, 17.0000], [26.0000, 2.0000, 1.0000, 0.0000, 1.0000, 1.0000, 0.0000, 0.0000, 0.0000, 2.0000, 0.4400, 0.4394, 0.9400, 0.2537, 1.0000, 16.0000, 17.0000], [27.0000, 2.0000, 1.0000, 0.0000, 1.0000, 2.0000, 0.0000, 0.0000, 0.0000, 2.0000, 0.4200, 0.4242, 1.0000, 0.2836, 1.0000, 8.0000, 9.0000]]]), torch.Size([2, 3, 17]))
这里发生了什么?首先,bikes.shape [1]为17,它是自行车张量中的列数。但是代码的真正症结在于调用视图(view),这一点很重要:它改变了张量查看存储中包含的相同数据的方式。
在张量上调用view返回一个新的张量,该张量可以更改维数和跨步信息,而无需更改存储。结果,您可以以零成本重新布置张量,因为根本没有数据被复制。您的视图调用要求您为返回的张量提供新的形状。将-1用作占位符,表示“给定其他尺寸和元素的原始数量,剩下多少索引。”
请记住,在这种情况下,存储是数字的连续线性容器,即浮点数。您的单车张量在相应的存储中逐行存储,这通过前面对bikes.stride()的调用输出确认。
对于daily_bikes,步幅告诉您,沿小时维度(第二个)前进1个位置需要您将存储(或一组列)中的位置前进17个位置,而沿日期维度(第一个)前进则需要在存储时间24(此处为408,即17 * 24)中,您将前进等于行长度的元素数量。
最右边的维是原始数据集中的列数。在中间维度中,您将时间分为24个连续小时的块。换句话说,您现在每天有C个通道(channels)的N个L小时序列。为了获得所需的NxCxL顺序,您需要转置张量:
daily_bikes = daily_bikes.transpose(1, 2)
daily_bikes.shape, daily_bikes.stride()
(torch.Size([730, 17, 24]), (408, 1, 17))
daily_bikes[:2, :3, :], daily_bikes[:2, :3, :].shape
(tensor([[[ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15., 16., 17., 18., 19., 20., 21., 22., 23., 24.], [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]], [[25., 26., 27., 28., 29., 29., 30., 31., 32., 33., 34., 35., 36., 37., 38., 39., 40., 41., 42., 43., 44., 45., 46., 47.], [ 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.], [ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]]]), torch.Size([2, 3, 24]))
前面我们提到天气状况变量是序数。 实际上,它有4个级别:1表示最佳天气,4表示最坏。 您可以将此变量视为分类变量,其级别解释为标签或连续。 如果选择分类,则将变量转换为one-hot编码的向量,并将列与数据集连接起来。 为了使数据表示更容易,现在暂时限制为第一天。 首先,初始化一个零填充矩阵,其行数等于一天中的小时数,列数等于天气水平的数:
first_day = bikes[:24].long()
weather_onehot = torch.zeros(first_day.shape[0], 4)
first_day[:,9]
tensor([1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 2, 2, 2, 2])
然后根据每一行对应的水平将它们刷新(scatter)到我们的矩阵中。记得使用unsqueeze在前面添加单例维度(您将值减少1,因为天气情况的范围是1到4,而索引是基于0的。):
weather_onehot.scatter_(
dim=1,
index=first_day[:,9].unsqueeze(1) - 1, # 您将值减少1,因为天气情况的范围是1到4,而索引是基于0的。
value=1.0)
tensor([[1., 0., 0., 0.], [1., 0., 0., 0.], [1., 0., 0., 0.], [1., 0., 0., 0.], [1., 0., 0., 0.], [0., 1., 0., 0.], [1., 0., 0., 0.], [1., 0., 0., 0.], [1., 0., 0., 0.], [1., 0., 0., 0.], [1., 0., 0., 0.], [1., 0., 0., 0.], [1., 0., 0., 0.], [0., 1., 0., 0.], [0., 1., 0., 0.], [0., 1., 0., 0.], [0., 1., 0., 0.], [0., 1., 0., 0.], [0., 0., 1., 0.], [0., 0., 1., 0.], [0., 1., 0., 0.], [0., 1., 0., 0.], [0., 1., 0., 0.], [0., 1., 0., 0.]])
这一天以天气1开始,以天气2结束,所以看起来是对的。
最后,使用cat函数将矩阵连接到原始数据集。看看你的第一个结果:
torch.cat((bikes[:24], weather_onehot), 1)[:1]
tensor([[ 1.0000, 1.0000, 1.0000, 0.0000, 1.0000, 0.0000, 0.0000, 6.0000, 0.0000, 1.0000, 0.2400, 0.2879, 0.8100, 0.0000, 3.0000, 13.0000, 16.0000, 1.0000, 0.0000, 0.0000, 0.0000]])
在这里,您指定了原始自行车数据集和一热点编码的天气情况矩阵,这些矩阵将沿列维(例如1)连接在一起。 换句话说,将两个数据集的列堆叠在一起,或者将新的一键编码列添加到原始数据集。 为了使cat成功,张量沿其他尺寸(在这种情况下为行尺寸)必须具有相同的大小。
请注意,您的新后四列分别为1、0、0、0,这正是您期望的天气值为1。
使用重塑的daily_bikes张量可以完成相同的操作。 请记住,它的形状是(B,C,L),其中L =24。首先,创建零张量,其B和L相同,但附加列数与C相同:
daily_weather_onehot = torch.zeros(daily_bikes.shape[0], 4, daily_bikes.shape[2])
daily_weather_onehot.shape
torch.Size([730, 4, 24])
然后将one-hot编码散布到C维中的张量中。 由于操作是在原地执行的,因此仅张量的内容会更改:
daily_weather_onehot.scatter_(1, daily_bikes[:,9,:].long().unsqueeze(1) - 1, 1.0)
daily_weather_onehot.shape
torch.Size([730, 4, 24])
沿着C维连接:
daily_bikes = torch.cat((daily_bikes, daily_weather_onehot), dim=1)
这里检查一下one-hot转换的细节,首先查看提升维度前的形状和内容
daily_bikes[:,9,:].shape
torch.Size([730, 24])
daily_bikes[:3,9,:]
tensor([[1., 1., 1., 1., 1., 2., 1., 1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 3., 3., 2., 2., 2., 2.], [2., 2., 2., 2., 2., 2., 3., 2., 3., 2., 2., 2., 2., 2., 3., 3., 3., 1., 2., 1., 1., 1., 1., 1.], [1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])
接着查看提升维度后的形状和内容
temp_daily_bikes = daily_bikes[:,9,:].long().unsqueeze(1) - 1
temp_daily_bikes[0:3]
tensor([[[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1]], [[1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 2, 2, 0, 1, 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]]])
temp_daily_bikes.shape
torch.Size([730, 1, 24])
daily_weather_onehot[0:3]
tensor([[[1., 1., 1., 1., 1., 0., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., 0., 0., 1., 1., 1., 1.], [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 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., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 1., 1., 1., 1., 1.], [1., 1., 1., 1., 1., 1., 0., 1., 0., 1., 1., 1., 1., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0., 1., 0., 1., 0., 0., 0., 0., 0., 1., 1., 1., 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.]], [[1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], [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., 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.]]])
前面我们提到,这种方法不是处理天气情况变量的唯一方法。 确实,其标签具有有序关系,因此您可以认为它们是连续变量的特殊值。 您可以转换变量,使其从0.0到1.0运行:
daily_bikes[:, 9, :] = (daily_bikes[:, 9, :] - 1.0) / 3.0
将变量重新缩放为[0.0,1.0]间隔或[-1.0,1.0]间隔是您需要对所有定量变量进行的操作,例如温度(数据集中的第10列) )。 稍后您会看到原因; 现在,我们说这对培训过程很有帮助。
您有多种重新调整变量的可能性。 您可以将其范围映射到[0.0,1.0]
temp = daily_bikes[:, 10, :]
temp_min = torch.min(temp)
temp_max = torch.max(temp)
daily_bikes[:, 10, :] = (daily_bikes[:, 10, :] - temp_min) / (temp_max - temp_min)
或者减去均值,然后除以标准差:
temp = daily_bikes[:, 10, :]
daily_bikes[:, 10, :] = (daily_bikes[:, 10, :] - torch.mean(temp)) / torch.std(temp)
在后一种情况下,变量的平均值为零,标准偏差为零。 如果从高斯分布中提取变量,则68%的样本将位于[-1.0,1.0]区间。
太好了-您建立了另一个不错的数据集,以后将使用。 目前,仅了解一下时间序列的布局方式以及如何将数据整理成网络可以消化的形式就很重要。
其他类型的数据看起来像时间序列,因为存在严格的排序。 该类别中的前两名是文本和音频。
从 http://www.gutenberg.org/files/1342/1342-0.txt 下载《傲慢与偏见》
!wget http://www.gutenberg.org/files/1342/1342-0.txt
--2019-11-27 10:34:50-- http://www.gutenberg.org/files/1342/1342-0.txt Resolving www.gutenberg.org (www.gutenberg.org)... 152.19.134.47, 2610:28:3090:3000:0:bad:cafe:47 Connecting to www.gutenberg.org (www.gutenberg.org)|152.19.134.47|:80... connected. HTTP request sent, awaiting response... 200 OK Length: 799738 (781K) [text/plain] Saving to: ‘1342-0.txt’ 1342-0.txt 100%[===================>] 780.99K 13.4KB/s in 66s 2019-11-27 10:35:57 (11.8 KB/s) - ‘1342-0.txt’ saved [799738/799738]
!mv 1342-0.txt $DATA_ROOT_DIR
pride_and_prejudice_path = os.path.join(DATA_ROOT_DIR, "1342-0.txt")
with open(pride_and_prejudice_path, encoding='utf-8') as f:
text = f.read()
您将对字符进行one-hot编码,以将one-hot编码限制为对要分析的文本有用的字符集。在这种情况下,因为您以英语加载了文本,所以使用ASCII并处理少量编码是非常安全的。您也可以将所有字符都转换为小写,以减少编码中的字符数。同样,您可以筛选出与预期的文本种类无关的标点符号,数字和其他字符,这可能会或可能不会影响您的神经网络,具体取决于手头的任务。
此时,您需要解析文本中的字符,并为每个字符提供one-hot编码。每个字符将由一个长度等于编码中字符数的向量表示。该矢量将包含除与编码中字符位置相对应的索引处的1以外的其它位置都是零。
首先,将您的文本分成几行,然后选择任意一行着重关注:
lines = text.split('\n')
line = lines[200]
line
' Michaelmas, and some of his servants are to be in the house by'
创建一个张量,它可以容纳整行一个one-hot编码字符的总数:
letter_tensor = torch.zeros(len(line), 128) # 由于ASCII的限制,one-hot 硬编码为128
letter_tensor.shape
torch.Size([68, 128])
for i, letter in enumerate(line.lower().strip()):
letter_index = ord(letter) if ord(letter) < 128 else 0 # 文本使用双引号,这不是有效的ASCII码,所以在这里把它们屏蔽掉。
letter_tensor[i][letter_index] = 1
您可以以相同的方式进行单词级编码。 由于词汇表包含许多单词,因此该方法会产生可能不实际的编码向量。在本章的后面,您将看到一种通过使用嵌入在单词级别表示文本的更有效方法。
定义clean_words,它接受文本并将其返回小写并删除标点符号。
def clean_words(input_str):
punctuation = '.,;:"!?”“_-'
word_list = input_str.lower().replace('\n',' ').split()
word_list = [word.strip(punctuation) for word in word_list]
return word_list
words_in_line = clean_words(line)
line, words_in_line
(' Michaelmas, and some of his servants are to be in the house by', ['michaelmas', 'and', 'some', 'of', 'his', 'servants', 'are', 'to', 'be', 'in', 'the', 'house', 'by'])
word_list = sorted(set(clean_words(text)))
word2index_dict = {word: i for (i, word) in enumerate(word_list)}
len(word2index_dict), word2index_dict['impossible']
(7278, 3383)
word_tensor = torch.zeros(len(words_in_line), len(word2index_dict))
for i, word in enumerate(words_in_line):
word_index = word2index_dict[word]
word_tensor[i][word_index] = 1
print('{:2} {:4} {}'.format(i, word_index, word))
print(word_tensor.shape)
0 4167 michaelmas 1 429 and 2 6045 some 3 4511 of 4 3216 his 5 5842 servants 6 531 are 7 6546 to 8 728 be 9 3409 in 10 6466 the 11 3253 house 12 981 by torch.Size([13, 7278])
如何将编码压缩为更易于管理的大小,并限制大小增长? 好吧,可以使用浮点数向量,而不是使用多个零和一个1的向量。 举例来说,一个100个浮点数的向量确实可以表示大量的单词。 诀窍是找到一种有效的方法,以一种有助于下游学习的方式将单个单词映射到这个100维空间。 这种技术称为嵌入(embedding)。
!wget https://raw.githubusercontent.com/deep-learning-with-pytorch/dlwpt-code/master/data/p1ch4/image-dog/bobby.jpg
--2019-11-27 10:53:51-- https://raw.githubusercontent.com/deep-learning-with-pytorch/dlwpt-code/master/data/p1ch4/image-dog/bobby.jpg Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 151.101.76.133 Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|151.101.76.133|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 75801 (74K) [image/jpeg] Saving to: ‘bobby.jpg’ bobby.jpg 100%[===================>] 74.02K 131KB/s in 0.6s 2019-11-27 10:53:54 (131 KB/s) - ‘bobby.jpg’ saved [75801/75801]
!mv bobby.jpg $DATA_ROOT_DIR
imag_path = os.path.join(DATA_ROOT_DIR, 'bobby.jpg')
img_arr = imageio.imread(imag_path)
img_arr.shape
(720, 1280, 3)
在这一点上,img是一个具有NumPy数组的对象,具有三个维度:两个空间维度(宽度和高度),第三个维度分别对应于通道红色,绿色和蓝色。 任何输出NumPy数组的库都这样做以获得PyTorch张量。 唯一需要注意的是尺寸的布局。 处理图像数据的PyTorch模块要求将张量布置为C xH x W(分别为通道,高度和宽度)。
可以使用转置功能获得适当的布局。 给定输入张量W xH x C,您可以通过交换第一个和最后一个通道来获得正确的布局:
img = torch.from_numpy(img_arr)
out = torch.transpose(img, 0, 2)
还要注意,其他深度学习框架使用不同的布局。最初,TensorFlow将通道尺寸保持在最后,从而形成H xW x C布局。 (现在,它支持多种布局。)从低层性能的角度来看,此策略是有利有弊,但只要适当地重塑张量,它就不会对您有所影响。
到目前为止,您已经描述了一张图片。遵循与以前的数据类型相同的策略,创建包含多个图像的数据集以用作神经网络的输入,然后沿第一维将这些图像成批存储,以获得N x C x H x W张量。
作为使用stack构建张量的更有效的替代方法,您可以预先分配适当大小的张量,并用从目录加载的图像填充它,
batch_size = 100
batch = torch.zeros(100, 4, 256, 256, dtype=torch.uint8) # 对于png图像有4通道
url_base = "https://raw.githubusercontent.com/deep-learning-with-pytorch/dlwpt-code/master/data/p1ch4/image-cats/"
picture_name_list = ["cat1.png", "cat2.png", "cat3.png"]
for image_name in picture_name_list:
image_url = url_base + image_name
!wget $image_url -q
!mv $image_name $DATA_ROOT_DIR
filenames = [os.path.join(DATA_ROOT_DIR, image_name) for image_name in picture_name_list]
filenames
['PyTorch_learn/data/cat1.png', 'PyTorch_learn/data/cat2.png', 'PyTorch_learn/data/cat3.png']
for i, filename in enumerate(filenames):
img_arr = imageio.imread(filename)
batch[i] = torch.transpose(torch.from_numpy(img_arr), 0, 2)
batch = batch.float()
batch /= 255.0
另一种可能性是计算输入数据的均值和标准差并对其进行换算,以使输出在每个通道上的均值和单位标准差为零:
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
您可以对输入执行其他几种操作,包括旋转,缩放和裁切之类的几何变换。这些操作可能有助于模型训练,或者可能需要进行这些操作以使任意输入符合模型的输入要求,例如图像大小。现在,请记住您有可用的图像处理选项。
您已经学习了如何加载和表示2D图像,就像使用相机拍摄的图像一样。在涉及CT(Computed Tomography计算机断层扫描)扫描等医学成像应用程序的情况下,通常需要处理从头到脚方向堆叠的图像序列,每个序列对应于整个身体的切片。在CT扫描中,强度代表身体不同部位的密度:肺,脂肪,水,肌肉,骨骼,以密度递增的顺序排列,当在临床工作站上显示CT扫描时,从暗到亮映射。根据穿过人体后到达检测器的X射线量计算每个点的密度,并使用一些复杂的数学运算将原始传感器数据解卷积(deconvolve)为完整体积。
CT具有单个强度通道,类似于灰度图像。通常,在原始数据格式中,通道维被忽略了,因此原始数据通常具有三个维。通过将单个2D切片堆叠到3D张量中,您可以构建表示对象的3D解剖结构的体积数据。即额外维度表示物理空间中的偏移,而不是可见光谱的特定频带(不同的颜色通道)。
我们将不在此处详细介绍医学成像数据格式。 现在,足以说存储体积数据的张量和存储图像数据的张量之间不存在根本差异。 在通道尺寸之后,您有一个额外的尺寸,深度,导致5D张量为N x C x D x H x W。
使用imageio模块中的volread函数加载一个样本CT扫描,该函数以目录作为参数,并将所有DICOM(数字图像通信和存储)文件10组合成NumPy 3D数组中的一系列,如下列表所示。。
lung_CT_data_dir = "lung_CT_data"
lung_CT_data_dir = os.path.join(DATA_ROOT_DIR, lung_CT_data_dir)
if not os.path.exists(lung_CT_data_dir):
os.mkdir(lung_CT_data_dir)
url_base = "https://raw.githubusercontent.com/deep-learning-with-pytorch/dlwpt-code/master/data/p1ch4/volumetric-dicom/2-LUNG%203.0%20%20B70f-04083/"
dcm_file_numbers = 10 # 为了减小下载时间这里只摄者为了10,最大可设置为 99
def download(task_idx):
file_name = "0" * (6 - len(str(task_idx))) + str(task_idx) + ".dcm"
image_url = url_base + file_name
!wget $image_url -q
def multiprocess_download_file():
if dcm_file_numbers <= 10:
pool_number = dcm_file_numbers
elif dcm_file_numbers <= 50:
pool_number = 25
else:
pool_number = 50
p = Pool(pool_number)
for task_idx in range(dcm_file_numbers):
p.apply_async(download, args=(task_idx,))
p.close()
p.join()
print('所有文件下载完毕')
for task_idx in range(dcm_file_numbers):
file_name = "0" * (6 - len(str(task_idx))) + str(task_idx) + ".dcm"
!mv $file_name $lung_CT_data_dir
multiprocess_download_file()
所有文件下载完毕
vol_arr = imageio.volread(lung_CT_data_dir, 'DICOM')
vol_arr.shape
Reading DICOM (examining files): 1/10 files (10.010/10 files (100.0%) Found 4 correct series. Reading DICOM (loading data): 2/2 (100.0%)
(2, 512, 512)
同样在这种情况下,由于缺少通道(channel)信息,布局与PyTorch期望的布局不同。 您必须使用unsqueeze来建立channel维:
vol = torch.from_numpy(vol_arr).float()
vol = torch.transpose(vol, 0, 2)
vol = torch.unsqueeze(vol, 0)
vol.shape
torch.Size([1, 512, 512, 2])
此时,可以像本章前面所述,通过沿batch方向堆叠多个卷来组装5D数据集。
关于张量的形状,视频数据可以看作是体积数据,用“时间”维代替“深度”。 结果再次是形状为“ N x C x T x H x W”的5D张量。
视频有几种格式,尤其是通过利用时空冗余来压缩。 对我们来说幸运的是,“ imageio”也读取视频数据。 假设我们想在512 x 512 RBG视频中保留100个连续帧,以便使用卷积神经网络对动作进行分类。 我们首先为视频创建一个读取器实例,这将使我们能够获取有关视频的信息并在帧中进行迭代。
让我们看看视频的元数据是什么样的:
!wget "https://raw.githubusercontent.com/deep-learning-with-pytorch/dlwpt-code/master/data/p1ch4/video-cockatoo/cockatoo.mp4" -q
--2019-11-27 14:48:52-- https://raw.githubusercontent.com/deep-learning-with-pytorch/dlwpt-code/master/data/p1ch4/video-cockatoo/cockatoo.mp4 Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 151.101.76.133 Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|151.101.76.133|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 728751 (712K) [application/octet-stream] Saving to: ‘cockatoo.mp4’ cockatoo.mp4 100%[===================>] 711.67K 15.8KB/s in 28s 2019-11-27 14:49:21 (25.7 KB/s) - ‘cockatoo.mp4’ saved [728751/728751]
!mv "cockatoo.mp4" $DATA_ROOT_DIR
cockatoo_mp4_file_path = os.path.join(DATA_ROOT_DIR, "cockatoo.mp4")
reader = imageio.get_reader(cockatoo_mp4_file_path, 'ffmpeg')
meta = reader.get_meta_data()
meta
{'plugin': 'ffmpeg', 'nframes': inf, 'ffmpeg_version': '4.1-static https://johnvansickle.com/ffmpeg/ built with gcc 6.3.0 (Debian 6.3.0-18+deb9u1) 20170516', 'codec': 'h264', 'pix_fmt': 'yuv444p', 'fps': 20.0, 'source_size': (1280, 720), 'size': (1280, 720), 'duration': 14.0}
现在,我们拥有所有信息来确定将存储视频帧的张量的大小:
meta['size']
(1280, 720)
n_channels = 3
#n_frames = meta['nframes']
n_frames = 280
video = torch.FloatTensor(n_channels, n_frames, *meta['size'])
video.shape
torch.Size([3, 280, 1280, 720])
现在,我们仅遍历读取器,并将所有三个通道的值设置在正确的第i个时间片中。
for i, frame_arr in enumerate(reader):
frame = torch.from_numpy(frame_arr).float()
video[:, i] = torch.transpose(frame, 0, 2)
在上面,我们对单个帧进行了迭代,并在转置通道后将每个帧设置在“ C x T x H x W”视频张量中。 然后,我们可以通过堆叠多个4D张量或预先分配具有已知批次大小的5D张量并逐个剪辑地迭代填充(假设将剪辑修剪为固定数量的帧)来获得batch。
将视频数据等同于体积数据并不是用于模型训练的表示视频的唯一方法。 如果我们处理固定长度的视频突发,这是一种有效的策略。 一种替代策略是求助于能够处理长序列并及时利用短期和长期关系(LSTM)的网络体系结构,就像文本或音频一样。//当我们使用循环网络时,我们将看到这种架构。
下一个方法考虑了批次维度上的时间。 因此,我们将数据集构建为4D张量,在batch中逐帧堆叠:
time_video = torch.FloatTensor(n_frames, n_channels, *meta['size'])
for i, frame in enumerate(reader):
frame = torch.from_numpy(frame).float()
time_video[i] = torch.transpose(frame, 0, 2)
time_video.shape
torch.Size([280, 3, 1280, 720])
例如,在我们的数据科学家角色中,我们可能必须将音频样本馈送到我们的网络并对其进行分类,或生成字幕。在这种情况下,我们将不使用压缩数据,而是必须找到一种方法来加载某种格式的音频文件,并将其作为未压缩的时间序列布置在张量中。现在开始吧。
我们可以在音频目录中的ESC-50存储库(https://github.com/karoldvl/ESC-50 )中下载大量环境声音。例如,获取1-100038-A-14.wav,其中包含鸟鸣声。
为了加载声音,我们使用SciPy,特别是scipy.io.wavfile.read,它具有不错的属性,可以将数据作为NumPy数组返回:
!wget "https://raw.githubusercontent.com/deep-learning-with-pytorch/dlwpt-code/master/data/p1ch4/audio-chirp/1-100038-A-14.wav" -q
!mv "1-100038-A-14.wav" $DATA_ROOT_DIR
wav_file_path = os.path.join(DATA_ROOT_DIR, "1-100038-A-14.wav")
freq, waveform_arr = wavfile.read(wav_file_path)
freq, waveform_arr
(44100, array([ -388, -3387, -4634, ..., 2289, 1327, 90], dtype=int16))
读取函数返回两个输出,即采样频率和作为16位整数一维数组的波形。 它是一个1D阵列,告诉我们这是一个单声道录音-如果声音是立体声,我们将有两个波形(两个通道)。
我们可以将数组转换为张量。 我们可能还想将波形张量转换为浮点张量。
waveform = torch.from_numpy(waveform_arr).float()
waveform.shape
torch.Size([220500])
在典型的数据集中,我们将有一个以上的波形,并且可能有多个通道。根据执行任务(例如声音分类任务)所使用的网络类型,我们将需要以两种方式之一对张量进行布局。
对于基于级联学习的滤波器组对一维信号进行滤波的架构,例如卷积网络,我们需要将张量布置为N x C x L,其中N是数据集中声音的数量,C是声音通道的数量、L是采样的数量。
相反,对于包含时间序列概念的体系结构,就像我们在文本中提到的循环网络一样,数据的布局也需要按照L x N x C的顺序排列。直观地讲,这是因为后一种架构一次获取一组C值-信号不被视为一个整体,而是被视为随时间变化的单个输入。
尽管最简单,但这只是表示音频的一种方式,因此可以被神经网络消化。另一种方法是将音频信号转换成频谱图。
除了可以在时间上明确表示振荡之外,我们还可以表征那些振荡在短时间间隔内发生的频率。因此,例如,如果我们拔出(希望已调音的)吉他的第五弦,并专注于该录音的0.1秒,我们将看到波形以每秒440个周期振荡,加上在不同频率下较小的杂散振荡,调高音色。如果继续进行后续的0.1秒间隔,我们现在会看到频率内容没有变化,但是强度随着弦乐声音的衰减而变化。如果我们现在决定采摘另一根弦,我们将观察到新的频率随时间而消失。
我们确实可以构建一个在X轴上具有时间,在Y轴上当时所听到的频率的图,并将这些频率的强度编码为该X和Y的值。好的,开始看起来像是图片,对吧?
没错,频谱图是每个时间点每个频率处强度的表示。事实证明,可以训练为分析以频谱图表示的声音上的图像而构建的卷积神经网络。
让我们看看如何将之前加载的声音转换成声谱图。为此,我们需要诉诸一种将时域中的信号转换为其频率内容的方法。这就是所谓的傅立叶变换,而使我们能够有效地计算它的算法是快速傅立叶变换(FFT),它是目前使用最广泛的算法之一。如果我们在短时间内连续发出声音,我们可以逐列建立声谱图。
这是一般性的想法,在这里我们不会赘述太多。对我们来说幸运的是,SciPy具有的功能可以在给定输入波形的情况下为我们提供闪亮的频谱图。我们从SciPy导入信号模块,然后为频谱图函数提供我们之前获得的波形和采样频率。返回值为所有NumPy数组,即频率f_arr(沿Y轴的值),时间t_arr(沿X轴的值)和实际的Spectrogra sp_arr作为2D数组。将后者变成PyTorch张量很简单:
f_arr, t_arr, sp_arr = signal.spectrogram(waveform_arr, freq)
sp_mono = torch.from_numpy(sp_arr)
sp_mono.shape
torch.Size([129, 984])
维度为F x T,其中F为频率,T为时间。
如前所述,立体声具有两个通道,这将导致两个通道的频谱图。 假设我们有两个频谱图,每个通道一个。 我们可以分别转换两个通道:
sp_left = sp_right = sp_arr
sp_left_t = torch.from_numpy(sp_left)
sp_right_t = torch.from_numpy(sp_right)
sp_left_t.shape, sp_right_t.shape
(torch.Size([129, 984]), torch.Size([129, 984]))
并沿第一个维度堆叠两个张量以获得大小为C x F x T的两个通道图像,其中C是通道数:
sp_t = torch.stack((sp_left_t, sp_right_t), dim=0)
sp_t.shape
torch.Size([2, 129, 984])
如果要构建数据集以用作网络输入,我们将沿第一维堆叠表示数据集中多个声音的多个频谱图,从而得到N x C x F x T张量。
这种张量与我们将为图像数据集构建的张量没有区别,其中F代表图像的行,T代表图像的列。 确实,我们将使用完全相同的网络来解决频谱图上的分类问题。
在本章中,您涵盖了很多基础知识。 您学会了加载最常见的内容。本章涵盖了很多基础知识。 您学习了如何加载最常见的数据类型并对其进行整形,以供神经网络使用。 当然,世界上额外数据格式比我们希望在本章中描述的更多。 有些内容,例如医学史,过于复杂而无法涵盖。 但是,对于感兴趣的读者,我们确实在代码存储库中的额外Jupyter笔记本中提供了简短的音频和视频张量创建示例,其中包括数据类型和整形以供神经网络使用。https://github.com/deep-learning-with-pytorch/dlwpt-code/tree/master/p1ch4