PyTorch 深度学习(GPT 重译)(二)(1)https://developer.aliyun.com/article/1485203
4.4 处理时间序列
在前一节中,我们讨论了如何表示组织在平面表中的数据。正如我们所指出的,表中的每一行都是独立的;它们的顺序并不重要。或者等效地,没有列编码关于哪些行先出现和哪些行后出现的信息。
回到葡萄酒数据集,我们本可以有一个“年份”列,让我们看看葡萄酒质量是如何逐年演变的。不幸的是,我们手头没有这样的数据,但我们正在努力手动收集数据样本,一瓶一瓶地。在此期间,我们将转向另一个有趣的数据集:来自华盛顿特区自行车共享系统的数据,报告 2011-2012 年 Capital Bikeshare 系统中每小时租赁自行车的数量,以及天气和季节信息(可在此处找到:mng.bz/jgOx
)。我们的目标是将一个平面的二维数据集转换为一个三维数据集,如图 4.5 所示。
图 4.5 将一维多通道数据集转换为二维多通道数据集,通过将每个样本的日期和小时分开到不同的轴上
4.4.1 添加时间维度
在源数据中,每一行是一个单独的小时数据(图 4.5 显示了这个的转置版本,以更好地适应打印页面)。我们希望改变每小时一行的组织方式,这样我们就有一个轴,它以每个索引增加一天的速度增加,另一个轴代表一天中的小时(与日期无关)。第三个轴将是我们的不同数据列(天气、温度等)。
让我们加载数据(code/p1ch4/4_time_series_bikes.ipynb)。
代码清单 4.4 code/p1ch4/4_time_series_bikes.ipynb
# In[2]: bikes_numpy = np.loadtxt( "../data/p1ch4/bike-sharing-dataset/hour-fixed.csv", dtype=np.float32, delimiter=",", skiprows=1, converters={1: lambda x: float(x[8:10])}) # ❶ bikes = torch.from_numpy(bikes_numpy) bikes # Out[2]: tensor([[1.0000e+00, 1.0000e+00, ..., 1.3000e+01, 1.6000e+01], [2.0000e+00, 1.0000e+00, ..., 3.2000e+01, 4.0000e+01], ..., [1.7378e+04, 3.1000e+01, ..., 4.8000e+01, 6.1000e+01], [1.7379e+04, 3.1000e+01, ..., 3.7000e+01, 4.9000e+01]])
❶ 将日期字符串转换为对应于第 1 列中的日期的数字
对于每个小时,数据集报告以下变量:
- 记录索引:
instant
- 月份的日期:
day
- 季节:
season
(1
:春季,2
:夏季,3
:秋季,4
:冬季) - 年份:
yr
(0
:2011,1
:2012) - 月份:
mnth
(1
到12
) - 小时:
hr
(0
到23
) - 节假日状态:
holiday
- 一周的第几天:
weekday
- 工作日状态:
workingday
- 天气情况:
weathersit
(1
:晴朗,2
:薄雾,3
:小雨/小雪,4
:大雨/大雪) - 摄氏度温度:
temp
- 摄氏度感知温度:
atemp
- 湿度:
hum
- 风速:
windspeed
- 休闲用户数量:
casual
- 注册用户数量:
registered
- 租赁自行车数量:
cnt
在这样的时间序列数据集中,行代表连续的时间点:有一个维度沿着它们被排序。当然,我们可以将每一行视为独立的,并尝试根据一天中的特定时间来预测循环自行车的数量,而不考虑之前发生了什么。然而,存在排序给了我们利用时间上的因果关系的机会。例如,它允许我们根据较早时间下雨的事实来预测某个时间的骑车次数。目前,我们将专注于学习如何将我们的共享单车数据集转换为我们的神经网络能够以固定大小的块摄入的内容。
这个神经网络模型将需要看到每个不同数量的值的一些序列,比如骑行次数、时间、温度和天气条件:N个大小为C的并行序列。C代表神经网络术语中的通道,对于我们这里的 1D 数据来说,它与列是相同的。N维度代表时间轴,这里每小时一个条目。
4.4.2 按时间段塑造数据
我们可能希望将这两年的数据集分成更宽的观测周期,比如天。这样我们将有N(用于样本数量)个长度为L的C序列集合。换句话说,我们的时间序列数据集将是一个三维张量,形状为N × C × L。C仍然是我们的 17 个通道,而L将是 24:每天的每小时一个。虽然我们必须使用 24 小时的块没有特别的原因,但一般的日常节奏可能会给我们可以用于预测的模式。如果需要,我们也可以使用 7 × 24 = 168 小时块按周划分。所有这些当然取决于我们的数据集具有正确的大小–行数必须是 24 或 168 的倍数。此外,为了使这有意义,我们的时间序列不能有间断。
让我们回到我们的共享单车数据集。第一列是索引(数据的全局排序),第二列是日期,第六列是一天中的时间。我们有一切需要创建每日骑行次数和其他外生变量序列的数据集。我们的数据集已经排序,但如果没有,我们可以使用torch.sort
对其进行适当排序。
注意我们使用的文件版本 hour-fixed.csv 已经经过一些处理,包括在原始数据集中包含缺失的行。我们假设缺失的小时没有活跃的自行车(它们通常在清晨的小时内)。
要获得我们的每日小时数据集,我们只需将相同的张量按照 24 小时的批次查看。让我们看一下我们的bikes
张量的形状和步幅:
# In[3]: bikes.shape, bikes.stride() # Out[3]: (torch.Size([17520, 17]), (17, 1))
这是 17,520 小时,17 列。现在让我们重新塑造数据,使其具有 3 个轴–天、小时,然后我们的 17 列:
# In[4]: daily_bikes = bikes.view(-1, 24, bikes.shape[1]) daily_bikes.shape, daily_bikes.stride() # Out[4]: (torch.Size([730, 24, 17]), (408, 17, 1))
这里发生了什么?首先,bikes.shape[1]
是 17,即bikes
张量中的列数。但这段代码的关键在于对view
的调用,这非常重要:它改变了张量查看相同数据的方式,而数据实际上是包含在存储中的。
正如您在上一章中学到的,对张量调用view
会返回一个新的张量,它会改变维度和步幅信息,但不会改变存储。这意味着我们可以在基本上零成本地重新排列我们的张量,因为不会复制任何数据。我们调用view
需要为返回的张量提供新的形状。我们使用-1
作为“剩下的索引数量,考虑到其他维度和原始元素数量”的占位符。
还要记住上一章中提到的存储是一个连续的、线性的数字容器(在本例中是浮点数)。我们的bikes
张量将每一行按顺序存储在其相应的存储中。这是通过之前对bikes.stride()
的调用输出来确认的。
对于daily_bikes
,步幅告诉我们,沿着小时维度(第二维)前进 1 需要我们在存储中前进 17 个位置(或者一组列);而沿着天维度(第一维)前进需要我们前进的元素数量等于存储中一行的长度乘以 24(这里是 408,即 17×24)。
我们看到最右边的维度是原始数据集中的列数。然后,在中间维度,我们有时间,分成 24 个连续小时的块。换句话说,我们现在有一天中L小时的N序列,对应C个通道。为了得到我们期望的N×C×L顺序,我们需要转置张量:
# In[5]: daily_bikes = daily_bikes.transpose(1, 2) daily_bikes.shape, daily_bikes.stride() # Out[5]: (torch.Size([730, 17, 24]), (408, 1, 17))
现在让我们将之前学到的一些技巧应用到这个数据集上。
4.4.3 准备训练
“天气情况”变量是有序的。它有四个级别:1
表示好天气,4
表示,嗯,非常糟糕。我们可以将这个变量视为分类变量,其中级别被解释为标签,或者作为连续变量。如果我们决定采用分类方式,我们将把变量转换为一个独热编码向量,并将列与数据集连接起来。⁴
为了更容易呈现我们的数据,我们暂时限制在第一天。我们初始化一个以一天中小时数为行数,天气级别数为列数的零填充矩阵:
# In[6]: first_day = bikes[:24].long() weather_onehot = torch.zeros(first_day.shape[0], 4) first_day[:,9] # Out[6]: 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])
然后我们根据每行对应级别向我们的矩阵中散布 1。记住在前几节中使用unsqueeze
添加一个单例维度:
# In[7]: weather_onehot.scatter_( dim=1, index=first_day[:,9].unsqueeze(1).long() - 1, # ❶ value=1.0) # Out[7]: tensor([[1., 0., 0., 0.], [1., 0., 0., 0.], ..., [0., 1., 0., 0.], [0., 1., 0., 0.]])
❶ 将值减 1 是因为天气情况范围从 1 到 4,而索引是从 0 开始的
我们的一天从天气“1”开始,以“2”结束,所以这看起来是正确的。
最后,我们使用cat
函数将我们的矩阵与原始数据集连接起来。让我们看看我们的第一个结果:
# In[8]: torch.cat((bikes[:24], weather_onehot), 1)[:1] # Out[8]: 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]])
在这里,我们规定我们的原始bikes
数据集和我们的独热编码的“天气情况”矩阵沿着列维度(即 1)进行连接。换句话说,两个数据集的列被堆叠在一起;或者等效地,新的独热编码列被附加到原始数据集。为了使cat
成功,需要确保张量在其他维度(在这种情况下是行维度)上具有相同的大小。请注意,我们新的最后四列是1, 0, 0, 0
,正如我们期望的天气值为 1 时一样。我们也可以对重塑后的daily_bikes
张量执行相同操作。记住它的形状是(B, C, L),其中L = 24。我们首先创建一个零张量,具有相同的B和L,但具有C个额外列:
# In[9]: daily_weather_onehot = torch.zeros(daily_bikes.shape[0], 4, daily_bikes.shape[2]) daily_weather_onehot.shape # Out[9]: torch.Size([730, 4, 24])
然后我们将独热编码散布到张量的C维度中。由于这个操作是原地执行的,因此只有张量的内容会改变:
# In[10]: daily_weather_onehot.scatter_( 1, daily_bikes[:,9,:].long().unsqueeze(1) - 1, 1.0) daily_weather_onehot.shape # Out[10]: torch.Size([730, 4, 24])
并且我们沿着C维度进行连接:
# In[11]: daily_bikes = torch.cat((daily_bikes, daily_weather_onehot), dim=1)
我们之前提到这不是处理“天气情况”变量的唯一方式。实际上,它的标签具有有序关系,因此我们可以假设它们是连续变量的特殊值。我们可以将变量转换为从 0.0 到 1.0 的范围:
# In[12]: daily_bikes[:, 9, :] = (daily_bikes[:, 9, :] - 1.0) / 3.0
正如我们在前一节中提到的,将变量重新缩放到[0.0, 1.0]区间或[-1.0, 1.0]区间是我们希望对所有定量变量进行的操作,比如temperature
(我们数据集中的第 10 列)。稍后我们会看到为什么要这样做;现在,我们只需说这对训练过程有益。
对变量重新缩放有多种可能性。我们可以将它们的范围映射到[0.0, 1.0]
# In[13]: 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))
或者减去均值并除以标准差:
# In[14]: temp = daily_bikes[:, 10, :] daily_bikes[:, 10, :] = ((daily_bikes[:, 10, :] - torch.mean(temp)) / torch.std(temp))
在后一种情况下,我们的变量将具有 0 均值和单位标准差。如果我们的变量是从高斯分布中抽取的,那么 68%的样本将位于[-1.0, 1.0]区间内。
太棒了:我们建立了另一个不错的数据集,并且看到了如何处理时间序列数据。对于这次的概览,重要的是我们对时间序列的布局有了一个概念,以及我们如何将数据整理成网络可以处理的形式。
其他类型的数据看起来像时间序列,因为有严格的顺序。排在前两位的是什么?文本和音频。接下来我们将看一下文本,而“结论”部分有关于音频的附加示例的链接。
4.5 表示文本
深度学习已经席卷了自然语言处理(NLP)领域,特别是使用重复消耗新输入和先前模型输出组合的模型。这些模型被称为循环神经网络(RNNs),它们已经成功应用于文本分类、文本生成和自动翻译系统。最近,一类名为transformers的网络以更灵活的方式整合过去信息引起了轰动。以前的 NLP 工作负载以包含编码语言语法规则的规则的复杂多阶段管道为特征。现在,最先进的工作是从头开始在大型语料库上端对端训练网络,让这些规则从数据中出现。在过去几年里,互联网上最常用的自动翻译系统基于深度学习。
我们在这一部分的目标是将文本转换为神经网络可以处理的东西:一个数字张量,就像我们之前的情况一样。如果我们能够做到这一点,并且稍后选择适合我们文本处理工作的正确架构,我们就可以使用 PyTorch 进行自然语言处理。我们立即看到这一切是多么强大:我们可以使用相同的 PyTorch 工具在不同领域的许多任务上实现最先进的性能;我们只需要将问题表述得当。这项工作的第一部分是重新塑造数据。
4.5.1 将文本转换为数字
网络在文本上操作有两个特别直观的层次:在字符级别上,逐个处理字符,以及在单词级别上,其中单词是网络所看到的最细粒度的实体。我们将文本信息编码为张量形式的技术,无论我们是在字符级别还是单词级别操作,都是相同的。而且这并不是魔法。我们之前就偶然发现了它:独热编码。
让我们从字符级别的示例开始。首先,让我们获取一些要处理的文本。这里一个了不起的资源是古腾堡计划(www.gutenberg.org)。这是一个志愿者努力,将文化作品数字化并以开放格式免费提供,包括纯文本文件。如果我们的目标是更大规模的语料库,维基百科语料库是一个突出的选择:它是维基百科文章的完整集合,包含 19 亿字和 440 多万篇文章。在英语语料库网站(www.english-corpora.org)可以找到其他语料库。
让我们从古腾堡计划网站加载简·奥斯汀的《傲慢与偏见》:www.gutenberg.org/files/1342/1342-0.txt。我们只需保存文件并读取它(code/p1ch4/5_text_jane_austen.ipynb)。
代码清单 4.5 code/p1ch4/5_text_jane_austen.ipynb
# In[2]: with open('../data/p1ch4/jane-austen/1342-0.txt', encoding='utf8') as f: text = f.read()
4.5.2 独热编码字符
在我们继续之前,还有一个细节需要注意:编码。这是一个非常广泛的主题,我们只会简单提及。每个书面字符都由一个代码表示:一个适当长度的比特序列,以便每个字符都可以被唯一识别。最简单的编码是 ASCII(美国信息交换标准代码),可以追溯到 1960 年代。ASCII 使用 128 个整数对 128 个字符进行编码。例如,字母a对应于二进制 1100001 或十进制 97,字母b对应于二进制 1100010 或十进制 98,依此类推。这种编码适合 8 位,这在 1965 年是一个很大的优势。
注意 128 个字符显然不足以涵盖所有需要正确表示非英语语言中的书写文本所需的字形、重音、连字等。为此,已经开发了许多使用更多比特作为代码以涵盖更广字符范围的编码。这更广范围的字符被标准化为 Unicode,将所有已知字符映射到数字,这些数字的位表示由特定编码提供。流行的编码包括 UTF-8、UTF-16 和 UTF-32,其中数字分别是 8 位、16 位或 32 位整数序列。Python 3.x 中的字符串是 Unicode 字符串。
我们将对字符进行独热编码。将独热编码限制在对所分析文本有用的字符集上是非常重要的。在我们的情况下,由于我们加载的是英文文本,使用 ASCII 并处理一个小编码是安全的。我们还可以将所有字符转换为小写,以减少编码中不同字符的数量。同样,我们可以筛选掉标点、数字或其他与我们期望的文本类型无关的字符。这可能对神经网络有实际影响,具体取决于手头的任务。
此时,我们需要遍历文本中的字符,并为每个字符提供一个独热编码。每个字符将由一个长度等于编码中不同字符数的向量表示。这个向量将包含除了在编码中字符位置对应的索引处的一个之外的所有零。
我们首先将文本拆分为一系列行,并选择一个任意的行进行关注:
# In[3]: lines = text.split('\n') line = lines[200] line # Out[3]: '“Impossible, Mr. Bennet, impossible, when I am not acquainted with him'
让我们创建一个能够容纳整行所有独热编码字符总数的张量:
# In[4]: letter_t = torch.zeros(len(line), 128) # ❶ letter_t.shape # Out[4]: torch.Size([70, 128])
❶ 由于 ASCII 的限制,128 个硬编码
请注意,letter_t
每行保存一个独热编码字符。现在我们只需在正确位置的每行设置一个 1,以便每行表示正确的字符。其中 1 应设置的索引对应于编码中字符的索引:
# In[5]: for i, letter in enumerate(line.lower().strip()): letter_index = ord(letter) if ord(letter) < 128 else 0 # ❶ letter_t[i][letter_index] = 1
❶ 文本使用方向性双引号,这不是有效的 ASCII,因此我们在这里将其筛选掉。
4.5.3 对整个单词进行独热编码
我们已经将我们的句子进行了独热编码,以便神经网络可以理解。单词级别的编码可以通过建立词汇表并对句子–单词序列–进行独热编码来完成。由于词汇表有很多单词,这将产生非常宽的编码向量,这可能不太实用。我们将在下一节看到,在单词级别表示文本有一种更有效的方法,即使用嵌入。现在,让我们继续使用独热编码,看看会发生什么。
我们将定义clean_words
,它接受文本并以小写形式返回,并去除标点。当我们在我们的“不可能,本内特先生”line
上调用它时,我们得到以下结果:
# In[6]: 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 # Out[6]: ('“Impossible, Mr. Bennet, impossible, when I am not acquainted with him', ['impossible', 'mr', 'bennet', 'impossible', 'when', 'i', 'am', 'not', 'acquainted', 'with', 'him'])
接下来,让我们构建一个单词到编码索引的映射:
# In[7]: word_list = sorted(set(clean_words(text))) word2index_dict = {word: i for (i, word) in enumerate(word_list)} len(word2index_dict), word2index_dict['impossible'] # Out[7]: (7261, 3394)
请注意,word2index_dict
现在是一个以单词为键、整数为值的字典。我们将使用它来高效地找到一个单词的索引,因为我们对其进行独热编码。现在让我们专注于我们的句子:我们将其分解为单词,并对其进行独热编码–也就是说,我们为每个单词填充一个独热编码向量的张量。我们创建一个空向量,并为句子中的单词分配独热编码值:
# In[8]: word_t = torch.zeros(len(words_in_line), len(word2index_dict)) for i, word in enumerate(words_in_line): word_index = word2index_dict[word] word_t[i][word_index] = 1 print('{:2} {:4} {}'.format(i, word_index, word)) print(word_t.shape) # Out[8]: 0 3394 impossible 1 4305 mr 2 813 bennet 3 3394 impossible 4 7078 when 5 3315 i 6 415 am 7 4436 not 8 239 acquainted 9 7148 with 10 3215 him torch.Size([11, 7261])
此时,tensor
在大小为 7,261 的编码空间中表示了一个长度为 11 的句子,这是我们字典中的单词数。图 4.6 比较了我们拆分文本的两种选项的要点(以及我们将在下一节中看到的嵌入的使用)。
字符级别和单词级别编码之间的选择让我们需要做出权衡。在许多语言中,字符比单词要少得多:表示字符让我们只表示几个类别,而表示单词则要求我们表示非常多的类别,并且在任何实际应用中,要处理字典中不存在的单词。另一方面,单词传达的意义比单个字符要多得多,因此单词的表示本身就更具信息量。鉴于这两种选择之间的鲜明对比,或许并不奇怪中间方法已经被寻找、发现并成功应用:例如,字节对编码方法⁶从一个包含单个字母的字典开始,然后迭代地将最常见的对添加到字典中,直到达到规定的字典大小。我们的示例句子可能会被分割成这样的标记:⁷
▁Im|pos|s|ible|,|▁Mr|.|▁B|en|net|,|▁impossible|,|▁when|▁I|▁am|▁not|▁acquainted|▁with|▁him
图 4.6 编码单词的三种方式
对于大多数内容,我们的映射只是按单词拆分。但是,较少见的部分–大写的Impossible和名字 Bennet–由子单元组成。
4.5.4 文本嵌入
独热编码是一种在张量中表示分类数据的非常有用的技术。然而,正如我们预料的那样,当要编码的项目数量实际上是无限的时,独热编码开始失效,就像语料库中的单词一样。在仅仅一本书中,我们就有超过 7,000 个项目!
我们当然可以做一些工作来去重单词,压缩替代拼写,将过去和未来时态合并为一个标记,等等。但是,一个通用的英语编码将会非常庞大。更糟糕的是,每当我们遇到一个新单词时,我们都需要向向量中添加一个新列,这意味着需要向模型添加一组新的权重来解释这个新的词汇条目–这将从训练的角度来看是痛苦的。
如何将我们的编码压缩到一个更易管理的大小,并限制大小增长?嗯,与其使用许多零和一个单一的向量,我们可以使用浮点数向量。比如,一个包含 100 个浮点数的向量确实可以表示大量的单词。关键是要找到一种有效的方法,以便将单个单词映射到这个 100 维空间,从而促进下游学习。这被称为嵌入。
原则上,我们可以简单地遍历我们的词汇表,并为每个单词生成一组 100 个随机浮点数。这样做是可以的,因为我们可以将一个非常庞大的词汇表压缩到只有 100 个数字,但它将放弃基于含义或上下文的单词之间距离的概念。使用这种单词嵌入的模型将不得不处理其输入向量中的非常少的结构。一个理想的解决方案是以这样一种方式生成嵌入,使得在相似上下文中使用的单词映射到嵌入的附近区域。
嗯,如果我们要手动设计一个解决这个问题的解决方案,我们可能会决定通过选择将基本名词和形容词映射到轴上来构建我们的嵌入空间。我们可以生成一个 2D 空间,其中轴映射到名词–水果(0.0-0.33)、花朵(0.33-0.66)和狗(0.66-1.0)–和形容词–红色(0.0-0.2)、橙色(0.2-0.4)、黄色(0.4-0.6)、白色(0.6-0.8)和棕色(0.8-1.0)。我们的目标是将实际的水果、花朵和狗放在嵌入中。
当我们开始嵌入词时,我们可以将苹果映射到水果和红色象限中的一个数字。同样,我们可以轻松地将橘子、柠檬、荔枝和猕猴桃(补充我们的五彩水果列表)进行映射。然后我们可以开始处理花朵,并将玫瑰、罂粟花、水仙花、百合花等映射…嗯。不太多棕色的花朵。好吧,向日葵可以被映射到花朵、黄色和棕色,然后雏菊可以被映射到花朵、白色和黄色。也许我们应该将猕猴桃更新为接近水果、棕色和绿色的映射。对于狗和颜色,我们可以将红骨映射到红色附近;嗯,也许狐狸可以代表橙色;金毛寻回犬代表黄色,贵宾犬代表白色,以及…大多数狗都是棕色。
现在我们的嵌入看起来像图 4.7。虽然对于大型语料库来说手动操作并不可行,但请注意,尽管我们的嵌入大小为 2,除了基本的 8 个词之外,我们描述了 15 个不同的词,并且如果我们花时间进行创造性思考,可能还能塞进更多词。
图 4.7 我们的手动词嵌入
正如你可能已经猜到的那样,这种工作可以自动化。通过处理大量的有机文本语料库,类似我们刚刚讨论的嵌入可以生成。主要区别在于嵌入向量中有 100 到 1,000 个元素,并且轴不直接映射到概念:相似概念的词在嵌入空间的相邻区域中映射,其轴是任意的浮点维度。
虽然具体的算法⁹有点超出了我们想要关注的范围,但我们想提一下,嵌入通常是使用神经网络生成的,试图从句子中附近的词(上下文)中预测一个词。在这种情况下,我们可以从单热编码的词开始,并使用一个(通常相当浅的)神经网络生成嵌入。一旦嵌入可用,我们就可以将其用于下游任务。
结果嵌入的一个有趣方面是相似的词不仅聚集在一起,而且与其他词有一致的空间关系。例如,如果我们取苹果的嵌入向量,并开始加减其他词的向量,我们可以开始执行类似苹果-红色-甜+黄色+酸的类比,最终得到一个与柠檬的向量非常相似的向量。
更现代的嵌入模型–BERT 和 GPT-2 甚至在主流媒体中都引起轰动–更加复杂且具有上下文敏感性:也就是说,词汇表中的一个词到向量的映射不是固定的,而是取决于周围的句子。然而,它们通常像我们在这里提到的更简单的经典嵌入一样使用。
4.5.5 文本嵌入作为蓝图
嵌入是一种必不可少的工具,当词汇表中有大量条目需要用数字向量表示时。但在本书中我们不会使用文本和文本嵌入,所以您可能会想知道为什么我们在这里介绍它们。我们认为文本如何表示和处理也可以看作是处理分类数据的一个示例。嵌入在独热编码变得繁琐的地方非常有用。事实上,在先前描述的形式中,它们是一种表示独热编码并立即乘以包含嵌入向量的矩阵的有效方式。
在非文本应用中,我们通常没有能力事先构建嵌入,但我们将从之前避开的随机数开始,并考虑将其改进作为我们学习问题的一部分。这是一种标准技术–以至于嵌入是任何分类数据的独热编码的一个突出替代方案。另一方面,即使我们处理文本,改进预先学习的嵌入在解决手头问题时已经成为一种常见做法。¹⁰
当我们对观察结果的共现感兴趣时,我们之前看到的词嵌入也可以作为一个蓝图。例如,推荐系统–喜欢我们的书的客户也购买了…–使用客户已经互动过的项目作为预测其他可能引起兴趣的上下文。同样,处理文本可能是最常见、最深入研究序列的任务;因此,例如,在处理时间序列任务时,我们可能会从自然语言处理中所做的工作中寻找灵感。
4.6 结论
在本章中,我们涵盖了很多内容。我们学会了加载最常见的数据类型并将其塑造为神经网络可以消费的形式。当然,现实中的数据格式比我们在一本书中描述的要多得多。有些,如医疗史,太复杂了,无法在此处涵盖。其他一些,如音频和视频,被认为对本书的路径不那么关键。然而,如果您感兴趣,我们在书的网站(www.manning.com/books/deep-learning-with-pytorch)和我们的代码库(github.com/deep-learning-with-pytorch/dlwpt-code/ tree/master/p1ch4
)提供了音频和视频张量创建的简短示例。
现在我们熟悉了张量以及如何在其中存储数据,我们可以继续迈向本书目标的下一步:教会你训练深度神经网络!下一章将涵盖简单线性模型的学习机制。
4.7 练习
- 使用手机或其他数码相机拍摄几张红色、蓝色和绿色物品的照片(如果没有相机,则可以从互联网上下载一些)。
- 加载每个图像,并将其转换为张量。
- 对于每个图像张量,使用
.mean()
方法来了解图像的亮度。 - 获取图像每个通道的平均值。您能仅通过通道平均值识别红色、绿色和蓝色物品吗?
- 选择一个包含 Python 源代码的相对较大的文件。
- 构建源文件中所有单词的索引(随意将您的标记化设计得简单或复杂;我们建议从用空格替换
r"[^a-zA-Z0-9_]+"
开始)。 - 将您的索引与我们为傲慢与偏见制作的索引进行比较。哪个更大?
- 为源代码文件创建独热编码。
- 使用这种编码会丢失哪些信息?这些信息丢失与傲慢与偏见编码中丢失的信息相比如何?
4.8 总结
- 神经网络要求数据表示为多维数值张量,通常是 32 位浮点数。
- 一般来说,PyTorch 期望数据根据模型架构沿特定维度布局–例如,卷积与循环。我们可以使用 PyTorch 张量 API 有效地重塑数据。
- 由于 PyTorch 库与 Python 标准库及周围生态系统的互动方式,加载最常见类型的数据并将其转换为 PyTorch 张量非常方便。
- 图像可以有一个或多个通道。最常见的是典型数字照片的红绿蓝通道。
- 许多图像的每个通道的位深度为 8,尽管每个通道的 12 和 16 位并不罕见。这些位深度都可以存储在 32 位浮点数中而不会丢失精度。
- 单通道数据格式有时会省略显式通道维度。
- 体积数据类似于 2D 图像数据,唯一的区别是添加了第三个维度(深度)。
- 将电子表格转换为张量可能非常简单。分类和有序值列应与间隔值列处理方式不同。
- 文本或分类数据可以通过使用字典编码为一热表示。很多时候,嵌入提供了良好且高效的表示。
这有点轻描淡写:en.wikipedia.org/wiki/Color_model
。
来自癌症影像存档的 CPTAC-LSCC 集合:mng.bz/K21K
。
作为更深入讨论的起点,请参考 en.wikipedia.org/wiki/Level_of_measurement
。
这也可能是一个超越主要路径的情况。可以尝试将一热编码推广到将我们这里的四个类别中的第i个映射到一个向量,该向量在位置 0…i 有一个,其他位置为零。或者–类似于我们在第 4.5.4 节讨论的嵌入–我们可以取嵌入的部分和,这种情况下可能有意义将其设为正值。与我们在实际工作中遇到的许多事物一样,这可能是一个尝试他人有效方法然后以系统化方式进行实验的好地方。
Nadkarni 等人,“自然语言处理:简介”,JAMIA,mng.bz/8pJP
。另请参阅 en.wikipedia.org/wiki/Natural-language_processing
。
最常由 subword-nmt 和 SentencePiece 库实现。概念上的缺点是字符序列的表示不再是唯一的。
这是从一个在机器翻译数据集上训练的 SentencePiece 分词器。
实际上,通过我们对颜色的一维观点,这是不可能的,因为向日葵的黄色和棕色会平均为白色–但你明白我的意思,而且在更高维度下效果更好。
一个例子是 word2vec: code.google.com/archive/p/word2vec
。
这被称为微调。
五、学习的机制
本章涵盖了
- 理解算法如何从数据中学习
- 将学习重新定义为参数估计,使用微分和梯度下降
- 走进一个简单的学习算法
- PyTorch 如何通过自动求导支持学习
随着过去十年中机器学习的蓬勃发展,从经验中学习的机器的概念已经成为技术和新闻界的主题。那么,机器是如何学习的呢?这个过程的机制是什么–或者说,背后的算法是什么?从观察者的角度来看,一个学习算法被提供了与期望输出配对的输入数据。一旦学习发生,当它被喂入与其训练时的输入数据足够相似的新数据时,该算法将能够产生正确的输出。通过深度学习,即使输入数据和期望输出相距很远,这个过程也能够工作:当它们来自不同的领域时,比如一幅图像和描述它的句子,正如我们在第二章中看到的那样。
5.1 建模中的永恒教训
允许我们解释输入/输出关系的建模模型至少可以追溯到几个世纪前。当德国数学天文学家约翰内斯·开普勒(1571-1630)在 17 世纪初发现他的三大行星运动定律时,他是基于他的导师第谷·布拉赫在裸眼观测(是的,用肉眼看到并写在一张纸上)中收集的数据。没有牛顿的万有引力定律(实际上,牛顿使用了开普勒的工作来解决问题),开普勒推断出了可能适合数据的最简单几何模型。顺便说一句,他花了六年时间盯着他看不懂的数据,连续的领悟,最终制定了这些定律。¹ 我们可以在图 5.1 中看到这个过程。
图 5.1 约翰内斯·开普勒考虑了多个可能符合手头数据的模型,最终选择了一个椭圆。
开普勒的第一定律是:“每颗行星的轨道都是一个椭圆,太阳位于两个焦点之一。”他不知道是什么导致轨道是椭圆的,但是在给定一个行星(或大行星的卫星,比如木星)的一组观测数据后,他可以估计椭圆的形状(离心率)和大小(半通径矢量)。通过从数据中计算出这两个参数,他可以预测行星在天空中的运行轨迹。一旦他弄清楚了第二定律–“连接行星和太阳的一条线在相等的时间间隔内扫过相等的面积”–他也可以根据时间观测推断出行星何时会在空间中的特定位置。²
那么,开普勒如何在没有计算机、口袋计算器甚至微积分的情况下估计椭圆的离心率和大小呢?我们可以从开普勒自己在他的书《新天文学》中的回忆中学到,或者从 J.V.菲尔德在他的一系列文章“证明的起源”中的描述中了解(mng.bz/9007
):
本质上,开普勒不得不尝试不同的形状,使用一定数量的观测结果找到曲线,然后使用曲线找到更多位置,用于他有观测结果可用的时间,然后检查这些计算出的位置是否与观测到的位置一致。
–J.V.菲尔德
那么让我们总结一下。在六年的时间里,开普勒
- 从他的朋友布拉赫那里得到了大量的好数据(不是没有一点挣扎)
- 尝试尽可能将其可视化,因为他觉得有些不对劲
- 选择可能适合数据的最简单模型(椭圆)
- 将数据分割,以便他可以处理其中一部分,并保留一个独立的集合用于验证
- 从一个椭圆的初步离心率和大小开始,并迭代直到模型符合观测结果
- 在独立观测上验证了他的模型
- 惊讶地回顾过去
为你准备了一本数据科学手册,一直延续至 1609 年。科学的历史实际上是建立在这七个步骤上的。几个世纪以来,我们已经学会了偏离这些步骤是灾难的前兆。
这正是我们将要从数据中学习的内容。事实上,在这本书中,几乎没有区别是说我们将拟合数据还是让算法从数据中学习。这个过程总是涉及一个具有许多未知参数的函数,其值是从数据中估计的:简而言之,一个模型。
我们可以认为从数据中学习意味着底层模型并非是为解决特定问题而设计的(就像开普勒的椭圆一样),而是能够逼近更广泛函数族的模型。一个神经网络可以非常好地预测第谷·布拉赫的轨迹,而无需开普勒的灵感来尝试将数据拟合成椭圆。然而,艾萨克·牛顿要从一个通用模型中推导出他的引力定律就要困难得多。
在这本书中,我们对不是为解决特定狭窄任务而设计的模型感兴趣,而是可以自动调整以专门为任何一个类似任务进行自我特化的模型–换句话说,根据与手头特定任务相关的数据训练的通用模型。特别是,PyTorch 旨在使创建模型变得容易,使拟合误差对参数的导数能够被解析地表达。如果最后一句话让你完全不明白,别担心;接下来,我们有一个完整的章节希望为你澄清这一点。
本章讨论如何自动化通用函数拟合。毕竟,这就是我们用深度学习做的事情–深度神经网络就是我们谈论的通用函数–而 PyTorch 使这个过程尽可能简单透明。为了确保我们理解关键概念正确,我们将从比深度神经网络简单得多的模型开始。这将使我们能够从本章的第一原则理解学习算法的机制,以便我们可以在第六章转向更复杂的模型。
5.2 学习只是参数估计
在本节中,我们将学习如何利用数据,选择一个模型,并估计模型的参数,以便在新数据上进行良好的预测。为此,我们将把注意力从行星运动的复杂性转移到物理学中第二难的问题:校准仪器。
图 5.2 展示了本章末尾我们将要实现的高层概述。给定输入数据和相应的期望输出(标准答案),以及权重的初始值,模型接收输入数据(前向传播),通过将生成的输出与标准答案进行比较来评估误差的度量。为了优化模型的参数–其权重,使用复合函数的导数链式法则(反向传播)计算单位权重变化后误差的变化(即误差关于参数的梯度)。然后根据导致误差减少的方向更新权重的值。该过程重复进行,直到在未见数据上评估的误差降至可接受水平以下。如果我们刚才说的听起来晦涩难懂,我们有整整一章来澄清事情。到我们完成时,所有的部分都会成为一体,这段文字将变得非常清晰。
现在,我们将处理一个带有嘈杂数据集的问题,构建一个模型,并为其实现一个学习算法。当我们开始时,我们将手工完成所有工作,但在本章结束时,我们将让 PyTorch 为我们完成所有繁重的工作。当我们完成本章时,我们将涵盖训练深度神经网络的许多基本概念,即使我们的激励示例非常简单,我们的模型实际上并不是一个神经网络(但!)。
图 5.2 我们对学习过程的心理模型
5.2.1 一个热门问题
我们刚从某个偏僻的地方旅行回来,带回了一个时髦的壁挂式模拟温度计。它看起来很棒,完全适合我们的客厅。它唯一的缺点是它不显示单位。不用担心,我们有一个计划:我们将建立一个读数和相应温度值的数据集,选择一个模型,迭代调整其权重直到误差的度量足够低,最终能够以我们理解的单位解释新的读数。⁴
让我们尝试按照开普勒使用的相同过程进行。在这个过程中,我们将使用一个他从未拥有过的工具:PyTorch!
5.2.2 收集一些数据
我们将首先记录摄氏度的温度数据和我们新温度计的测量值,并弄清楚事情。几周后,这是数据(code/p1ch5/1_parameter_estimation.ipynb):
# In[2]: t_c = [0.5, 14.0, 15.0, 28.0, 11.0, 8.0, 3.0, -4.0, 6.0, 13.0, 21.0] t_u = [35.7, 55.9, 58.2, 81.9, 56.3, 48.9, 33.9, 21.8, 48.4, 60.4, 68.4] t_c = torch.tensor(t_c) t_u = torch.tensor(t_u)
这里,t_c
值是摄氏度温度,t_u
值是我们未知的单位。我们可以预期两个测量中都会有噪音,来自设备本身和我们的近似读数。为了方便起见,我们已经将数据放入张量中;我们将在一分钟内使用它。
5.2.3 可视化数据
图 5.3 中我们数据的快速绘图告诉我们它很嘈杂,但我们认为这里有一个模式。
图 5.3 我们的未知数据可能遵循一个线性模型。
注意 剧透警告:我们知道线性模型是正确的,因为问题和数据都是虚构的,但请耐心等待。这是一个有用的激励性例子,可以帮助我们理解 PyTorch 在幕后做了什么。
5.2.4 选择线性模型作为第一次尝试
在没有进一步的知识的情况下,我们假设将两组测量值之间转换的最简单模型,就像开普勒可能会做的那样。这两者可能是线性相关的–也就是说,通过乘以一个因子并添加一个常数,我们可以得到摄氏度的温度(我们忽略的误差):
t_c = w * t_u + b
这个假设合理吗?可能;我们将看到最终模型的表现如何。我们选择将w
和b
命名为权重和偏差,这是线性缩放和加法常数的两个非常常见的术语–我们将一直遇到这些术语。⁶
现在,我们需要根据我们拥有的数据来估计w
和b
,即我们模型中的参数。我们必须这样做,以便通过将未知温度t_u
输入模型后得到的温度接近我们实际测量的摄氏度温度。如果这听起来像是在一组测量值中拟合一条直线,那么是的,因为这正是我们正在做的。我们将使用 PyTorch 进行这个简单的例子,并意识到训练神经网络实质上涉及将模型更改为稍微更复杂的模型,其中有一些(或者是一吨)更多的参数。
让我们再次详细说明一下:我们有一个具有一些未知参数的模型,我们需要估计这些参数,以便预测输出和测量值之间的误差尽可能低。我们注意到我们仍然需要准确定义一个误差度量。这样一个度量,我们称之为损失函数,如果误差很大,应该很高,并且应该在完美匹配时尽可能低。因此,我们的优化过程应该旨在找到w
和b
,使得损失函数最小化。
5.3 较少的损失是我们想要的
损失函数(或成本函数)是一个计算单个数值的函数,学习过程将尝试最小化该数值。损失的计算通常涉及取一些训练样本的期望输出与模型在馈送这些样本时实际产生的输出之间的差异。在我们的情况下,这将是我们的模型输出的预测温度t_p
与实际测量值之间的差异:t_p - t_c
。
我们需要确保损失函数在t_p
大于真实t_c
和小于真实t_c
时都是正的,因为目标是让t_p
匹配t_c
。我们有几种选择,最直接的是|t_p - t_c|
和(t_p - t_c)²
。根据我们选择的数学表达式,我们可以强调或折扣某些错误。概念上,损失函数是一种优先考虑从我们的训练样本中修复哪些错误的方法,以便我们的参数更新导致对高权重样本的输出进行调整,而不是对一些其他样本的输出进行更改,这些样本的损失较小。
这两个示例损失函数在零处有明显的最小值,并且随着预测值向任一方向偏离真值,它们都会单调增加。由于增长的陡峭度也随着远离最小值而单调增加,它们都被称为凸函数。由于我们的模型是线性的,所以损失作为w
和b
的函数也是凸的。⁷ 损失作为模型参数的凸函数的情况通常很容易处理,因为我们可以通过专门的算法非常有效地找到最小值。然而,在本章中,我们将使用功能更弱但更普遍适用的方法。我们这样做是因为对于我们最终感兴趣的深度神经网络,损失不是输入的凸函数。
对于我们的两个损失函数|t_p - t_c|
和(t_p - t_c)²
,如图 5.4 所示,我们注意到差的平方在最小值附近的行为更好:对于t_p
,误差平方损失的导数在t_p
等于t_c
时为零。另一方面,绝对值在我们希望收敛的地方具有未定义的导数。实际上,这在实践中并不是问题,但我们暂时将坚持使用差的平方。
图 5.4 绝对差与差的平方
值得注意的是,平方差也比绝对差惩罚更严重的错误。通常,有更多略微错误的结果比有几个极端错误的结果更好,而平方差有助于按预期优先考虑这些结果。
PyTorch 深度学习(GPT 重译)(二)(3)https://developer.aliyun.com/article/1485205