- Published on
《动手学深度学习》学习笔记(二)上
- Authors

- Name
- 祝你好运
注:说实话如果单纯看书,我觉得难度很大,但现在有了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而不是rand,rand是随机出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 范数,也就是欧几里得距离。比如 ,它的 L2 范数就是 。如果是多维的,就是各维度平方和再开方:
而l1范数是各个维度的绝对值和,也就是先求绝对值,再求和。
这里了解即可,深度学习里面,还是l2范数用的更多。