祝你好运的技术博客

Published on

《动手学深度学习》学习笔记(二)上

Authors
  • avatar
    Name
    祝你好运
    Twitter

注:说实话如果单纯看书,我觉得难度很大,但现在有了ChatGPT,不懂的直接甩过去,给我讲的明明白白,顺带一连串的彩虹屁,让我觉得我马上就能训练出ChatGPT 6!

第二章:预备知识

数据操作

首先我后面的学习都只用PyTorch,别的几种都不用了。然后PyTorch在导入的时候其实是torch,这个习惯了就好了。

初始化

然后我们数据操作先讲的就是数据初始化,比如生成12个浮点数(注意这里是0到11而不是1到12):

x = torch.arange(12, dtype=torch.float32)
x
tensor([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11.])

其实我认为刚开始学习的时候,应该教下面的,少一些参数,会更容易学习:

x = torch.arange(12)
x
tensor([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

很多时候我们想要一份全0数据,注意这里是一份3维数据,第一维有2个元素,第三维有4个。

torch.zeros((2, 3, 4))
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.]]])

有些时候我们又想要全1的数据:

torch.ones((2, 3, 4))
tensor([[[1., 1., 1., 1.],
         [1., 1., 1., 1.],
         [1., 1., 1., 1.]],

        [[1., 1., 1., 1.],
         [1., 1., 1., 1.],
         [1., 1., 1., 1.]]])

当然随机数也非常重要,尤其是我们训练开始的时候,直接用随机数初始化,然后开始训练。注意下面的是randn而不是randrand是随机出0到1的数,而randn是随机出0附近的正态分布的数,这个如果不了解可以去问大模型,我已经问明白了(当然也是之前学过,大概是高中):

torch.randn(3, 4)
tensor([[-0.7823, -2.0361,  0.6767,  1.6542],
        [ 0.4277, -1.8274,  0.0578, -0.3718],
        [-0.0502, -0.9920,  0.7946,  0.4622]])

调整大小

注意我们这里用的x就是上面的哈

X = x.reshape(3, 4)
X
tensor([[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11]])
X.shape
torch.Size([3, 4])

索引与切片

这里的索引与切片就类似Python里面的数组的索引和切片,那边懂了这里就没问题。比如下面的X[-1],这里意思就是X的第一维数据的最后一个元素。X[1:3]这个就是X的第一维数据做切片,从第1到第2个,一共两个。

X[-1], X[1:3]
(tensor([ 8.,  9., 10., 11.]),
 tensor([[ 4.,  5.,  6.,  7.],
         [ 8.,  9., 10., 11.]]))

而且修改值的时候,可以用单个值覆盖多维数据: 比如

x = torch.randn(3, 4, 5)
x[1:2] = 6
tensor([[[-1.8204, -1.2844,  0.4844,  0.4788, -0.0531],
         [-0.1200, -0.7851, -0.3964,  0.6313,  0.7284],
         [-1.0022,  0.5124,  1.1530,  0.4703, -0.0768],
         [ 0.4584,  0.6001, -0.6398, -0.3655,  0.4246]],

        [[ 6.0000,  6.0000,  6.0000,  6.0000,  6.0000],
         [ 6.0000,  6.0000,  6.0000,  6.0000,  6.0000],
         [ 6.0000,  6.0000,  6.0000,  6.0000,  6.0000],
         [ 6.0000,  6.0000,  6.0000,  6.0000,  6.0000]],

        [[-0.2148, -0.5980, -0.0466, -0.3548,  1.2645],
         [ 0.6480,  0.2927,  0.3504, -1.9341, -0.9903],
         [ 0.0111, -1.1888, -1.4203,  0.8937,  0.3412],
         [ 0.4003,  0.5873, -0.4195, -0.9600,  0.8594]]])

操作符

这里介绍了一些常见的操作符,比如加减乘除次方,拼接,求自然指数,求和。注意,有些操作符是要求两个运算数的维度要一样,比如下面的求和就会报错:

>>> x = torch.tensor([1, 2, 4])
>>> y = torch.tensor([3, 6, 9, 12])
>>> x + y
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: The size of tensor a (3) must match the size of tensor b (4) at non-singleton dimension 0

而有些运算符就不要求,比如求和,求自然指数

torch.exp(x)
X.sum()

对于维度不一样,又想做计算,可能会用到Broadcasting这种技术,他会把两个参与计算的tensor的维度搞成一样,这样就可以计算了。不是说维度不一样的数据就可以用broadcasting,而是数据复制的时候没有歧义(也就是要被复制的那一行或者列只有1个元素),比如下面的x和y

>>> x = torch.tensor([[1, 2, 3]])
>>> y = torch.tensor([[1], [2], [3]])
>>> x + y
tensor([[2, 3, 4],
        [3, 4, 5],
        [4, 5, 6]])

节省内存

当数据量很大的时候,复制数据会变得很慢,所以原地操作就是分必要了,而Python这种语言,他有的是原地修改,有些是复制的,除了基础类型(整数,浮点数,布尔),别的基本都是原地修改(字符串和元组例外)。树上举的例子也很好:

before = id(X)
X += Y
id(X) == before
True

数据处理

数据处理主要涵盖下面几个方面:数据读写,

数据读取

我们做深度学习的时候,很多数据都是.csv格式的,因为我们预处理好的数据一般都是表格形式的,也就是有行有列。这种数据读取我们一般用pandas,他是“Panel Data”,也就是面板数据(二维或者多维表格)数据。

读取数据:

我们先通过下面的脚本造一份csv数据:

import os

os.makedirs(os.path.join('..', 'data'), exist_ok=True)
data_file = os.path.join('..', 'data', 'house_tiny.csv')
with open(data_file, 'w') as f:
  f.write('''NumRooms,RoofType,Price
NA,NA,127500
2,NA,10600
4,Slate,178100
NA,NA,140000''')

然后我们通过使用pandas来读取这份数据:

import pandas as pd
data = pd.read_csv(data_file)
print(data)

   NumRooms RoofType   Price
0       NaN      NaN  127500
1       2.0      NaN   10600
2       4.0    Slate  178100
3       NaN      NaN  140000

数据准备

监督学习里面,我们是想要从输入的数据预测目标值。比如我们想要根据房子有几个房间,房顶类型,预测房价。那房间数量和房顶类型就是输入数据,我们应该从输入数据里面把他们切分出来。(数据是沿着上面的数据继续处理的)

>>> inputs, targets = data.iloc[:, :2], data.iloc[:, 2:]
>>> inputs = pd.get_dummies(inputs, dummy_na=True)
>>> print(inputs)
   NumRooms  RoofType_Slate  RoofType_nan
0       NaN           False          True
1       2.0           False          True
2       4.0            True         False
3       NaN           False          True
>>> print(targets)
    Price
0  127500
1   10600
2  178100
3  140000

我们注意到pandas的数据里面有些格子里面是NaN,这是not a number,简答来说就是数据不合法。那我们应该如何处理这样的数据呢?就是imputation or deletion,也就是插值,或者删除。也就是想办法把他们变得正常,或者干脆直接删掉不要了。

注:美国的屋顶这种slate,其实就是天然方形石片,然后按照特殊结构一片一片排在一起。这样下雨的时候雨水也可以沿着石片往下流,缝隙里面的水会到石片下面的防水层。这种结构跟中国的瓦片类似,只是中国的瓦片是兼具装饰和防水。而slate这种只是装饰层,价格高昂,富人采用,穷人用沥青瓦(不好看,当然也在与时俱进)。

roofType为什么要变成数字?

因为我们做训练的时候,大模型不能处理字符串,那这种slate的字符串,我们会想办法给他们编码,比如1代表slate。

roofType为什么要单独拆成2列?

因为上面我们说了要给他们编码,比如1是slate,2是tile,那我们可能会想要用特殊值0代表NA。但我们后面会用向量的距离来计算他们的相似性等等特点。0这个其实是跟1和2类似的数字,是大小的问题(在训练的时候,他起的作用就类似一种特殊roof,而不是有没有roof,这语义就错了)。这不是我们期望的,于是我们单独拆出来一列RoofType_nan,他就代表这一行roof是否有值。 而且如果我们roof有X个值,我们会拆成X+1列。

平均值插值法

还有一种填充方法就是用平均值。比如NumRooms这一列,如果有的行没有值,我们可以用整个列的平均值给他填充。类似下面的方法(注意,这里的数据是上面经过roofType拆分的):

inputs = inputs.fillna(inputs.mean())
print(inputs)

转换成tensor数据

我们训练的时候都是用tensor,而经过上面的步骤,我们已经把原始的数据转换成了纯数值类型(非常重要),然后导入:

import torch
X = torch.tensor(inputs.to_numpy(dtype=float))
Y = torch.tensor(targets.to_numpy(dtype=float))
X, Y
(tensor([[3., 0., 1.],
         [2., 0., 1.],
         [4., 1., 0.],
         [3., 0., 1.]], dtype=torch.float64),
 tensor([127500., 106000., 178100., 140000.], dtype=torch.float64))

为什么要调用`to_numpy(dtype=float)?

因为inputs里面包含行号,列名,这些数据在计算的时候没法用,必须去掉。而to_numpy就是做这个处理,而参数dtype=float是为了确保数据类型都变成了浮点数,如果不加,就有可能有些数据是布尔类型,加了这个参数之后就能控制他们全部会被转换成浮点数。

线性代数

简单来说这里就是线性代数课程里面学过的那些知识。注意我们一般用大写字母X, Y, Z来表示tensor值。但是还是有一些需要特殊注意的地方,比如:

哈达玛积

这个就是逐元素乘法:C[i,j] = A[i,j] × B[i,j],举个例子:

A =      B =
1 2      5 6
3 4      7 8

A ⊙ B =
1×5   2×6
3×7   4×8

=
5 12
21 32

我们都知道矩阵乘法,这个哈达玛积不是矩阵乘法,为什么需要这个东西呢?而且为什么我们学线性代数的时候不讲这个东西呢?因为哈达玛积在深度学习里面有特殊的含义,矩阵乘法可以用来把多个特征融合成一个新的特征,而哈达玛积是不想融合,只是控制每一个位置上的值:

Attention Score
=
0.9
0.1
0.7
0.2

Value:

Tom
Jerry
Mike
Lucy

直接做哈达玛积:

Tom 保留90%
Jerry 保留10%
Mike 保留70%
Lucy 保留20%

再比如,他可以用来控制一些位置的开关:

100 120
150 200

有一个mask:
1 0
0 1

做哈达玛积:
100×1   120×0
150×0   200×1
=
100 0
0 200

点乘

点乘就是先求哈达玛积,然后再sum一下就好了。

矩阵相乘

矩阵相乘略微复杂一点,就是计算结果多一点,详细的就不在这里写了,他的要求就是如果第一个矩阵是m*n,那第二个矩阵必须是n维的。

范数

范数是为了度量一个向量的长度,我们会比较熟悉 L2 范数,也就是欧几里得距离。比如 [3,4][3, -4],它的 L2 范数就是 55。如果是多维的,就是各维度平方和再开方:

x2=x12+x22++xn2\|x\|_2 = \sqrt{x_1^2 + x_2^2 + \cdots + x_n^2}

而l1范数是各个维度的绝对值和,也就是先求绝对值,再求和。

x1=x1+x2+...\|x\|_1 = |x_1| + |x_2| + ...

这里了解即可,深度学习里面,还是l2范数用的更多。