一、PyTorch
(一)张量的创建
1.概念
- 在PyTorch中,所有数据被封装为张量参与运算
- 张量是指一个同类型数据的多维矩阵,默认类型为
float32
- 在PyTorch中对张量的运算是通过类实现的,类中封装了对张量进行运算的一些函数
2.基本创建
- 通过tensor函数进行创建
- 适用于已有具体数据,根据数据创建张量
#1.创建标量 data = torch.tensor(10) print(data) #2.使用NumPy数组创建 data = np.random.randn(2,3) data = torch.tensor(data) print(data) #3.使用list创建 data = [[10.,20.,30.],[10.,20.,30.]] data = torch.tensor(data) print(data)
- 通过Tensor类进行创建
- 适用于根据形状创建张量,也可以用于指定数据创建张量
#1.根据形状创建张量 data = torch.Tensor(2,3) print(data) #2.根据数据创建张量 data = torch.Tensor([2,3]) print(data)
- 通过具体类别函数创建
- 适用于创建指定类型的张量
data = torch.IntTensor(2,3)#默认int32 print(data) """ ShortTensor:int16 LongTensor:int64 FloatTensor:float32 """ #若传入数据类型不匹配,会发生类型转换
3.创建线性张量
- 指定步长
#1.指定步长 """ torch.arange() :param1:开始值 :param2:结束值 :param3:步长 :return:张量 """ data = torch.arange(0,10,3) print(data)
- 指定区间指定元素个数
#2.在指定区间中指定元素个数 """ torch.linspace() :param1:开始值 :param2:结束值 :param3:元素个数 :return:张量 """ data = torch.linspace(0,10,5) print(data)
4.创建随机张量
- 随机创建
#1.创建随机张量 """ torch.randn() :param1:行数 :param2:列数 :return:张量 """ data = torch.randn(2,3) print(data) #2.固定随机数种子 torch.random.manual_seed(0)#固定随机数种子为0 #3.int类型等其他类型示例 """ torch.randint() :param1:起始点 :param2:结束点 :param3:形状 :return: 返回int类型的随机张量 """ data = torch.randint(0,10,[3,3])
5.创建指定值张量
- 创建全0张量
- 指定形状
#1.指定形状 """ torch.zeros() :param1:行数 :param2:列数 :return:全0张量 """ data = torch.zeros(2,3) print(data)
- 模仿样板张量
#2.根据其他张量形状创建 """ torch.zeros_like() :param:样板张量 :return:同形状的全0张量 """ data2 = torch.zeros_like(data) print(data2)
- 创建全1张量
- 指定形状
#1.指定形状 """ torch.ones() :param1:行数 :param2:列数 :return:全1张量 """ data = torch.ones(2,3) print(data)
- 模仿样板张量
#2.根据其他张量形状创建 """ torch.ones_like() :param:样板张量 :return:同形状的全1张量 """ data2 = torch.ones_like(data) print(data2)
- 创建指定值的张量
- 指定形状
#1.指定值和形状 """ torch.full() :param1:传入形状信息,使用列表或元组 :param2:传入指定值 :return: 指定值和形状的张量 """ data = torch.full([2,3],2) print(data)
- 模仿样板张量
#2.根据其他张量形状创建 """ torch.full() :param1:传入形状信息,使用样板张量 :param2:传入指定值 :return: 指定值和形状的张量 """ data2 = torch.full_like(data,2) print(data2)
(二)张量的元素类型转换
1.type函数
data = torch.zeros(2,3)
print(data.dtype)#此时类型为float32
"""
data.type()
:param1:具体的一个数据类型的Tensor类
:return:新的指定类型的张量
"""
data = data.type(torch.DoubleTensor)
print(data.dtype)
2.具体类型函数
data = torch.zeros(2,3)
print(data.dtype)#此时类型为float32
"""
data.double()
:return:double类型的新张量
如果要转换为其他类型,可以调用相应的具体类型函数
"""
data = data.double()
print(data.dtype)
(三)张量的计算
1.张量的基本运算
- 生成新张量
#1.生成新张量 data = torch.randint(0,10,[3,3]) print(data) """ data.add() :param:数值 :return:将data加上该数值后,返回新张量 """ data = data.add(10) print(data) """ data.sub():减法 data.mul():乘法 data.div():除法 data.neg():取反 """
- 作用于原张量
#2.作用在原张量 data = torch.randint(0,10,[3,3]) print(data) """ data.add_() :param:数值 :return:将data加上该数值后,返回原张量 """ data.add_(10)#注意:带了下划线,其他的同理 print(data)
2.阿达玛积
- 概念:阿达玛积是指矩阵对应位置的元素相乘
- 使用
mul
函数#1.使用mul函数 data1 = torch.tensor([[6,1],[2,3]]) data2 = torch.tensor([[2,4],[6,8]]) """ data.mul() :param:指定相乘的第二个矩阵 :return:两个矩阵的阿达玛积,返回新张量 """ data = data1.mul(data2) print(data)
- 使用
*
运算符#2.使用*运算符 data1 = torch.tensor([[6,1],[2,3]]) data2 = torch.tensor([[2,4],[6,8]]) data = data1*data2 print(data)
3.点积运算
- 概念:线性代数中的矩阵相乘运算,符合线性代数矩阵相乘的要求
- 使用
@
运算符#1.使用@运算符 data1 = torch.tensor([[6,1],[2,3]]) data2 = torch.tensor([[2,4],[6,8]]) data = data1@data2 print(data)
- 使用
mm
函数#2.使用mm函数 """ 要求矩阵的维数都是二维(行,列) 也就是说,没有批次这个维数,只进行两个矩阵的相乘,对比bmm函数 并不代表限制矩阵的维数 """ data1 = torch.tensor([[6,1],[2,3]]) data2 = torch.tensor([[2,4],[6,8]]) """ torch.mm() :param1:左侧的矩阵 :param2;右侧的矩阵 :return:相乘后的新矩阵 """ data = torch.mm(data1,data2) print(data)
- 使用
bmm
函数#3.使用bmm函数 """ 要求矩阵的维数都是三维(批次,行,列) 第一个维度:表示批次 例如批次为n,可以理解为列表,列表中存有n个矩阵 两个列表中对应位置的矩阵相乘,返回n个结果 第二个维度:行数 第三个维度:列数 """ data1 = torch.randn(3,4,5) data2 = torch.randn(3,5,3) """ torch.bmm() :param1:左侧的矩阵 :param2;右侧的矩阵 :return:相乘后的新矩阵 """ data = torch.bmm(data1,data2) print(data)
- 使用
matmul
函数#4.使用matmul函数 """ 适用于多种维数 torch.matmul() :param1:左侧矩阵 :param2:右侧矩阵 :return:相乘后的新矩阵 """ #二维 data1 = torch.randn(4,5) data2 = torch.randn(5,3) data = torch.matmul(data1,data2) print(data) #三维 data3 = torch.randn(3,4,5) data4 = torch.randn(3,5,3) data = torch.matmul(data3,data4) print(data) #维数不同的 data1 = torch.randn(4, 5)#在运算时会视为3个相同矩阵,批次为3 data3 = torch.randn(3, 5, 5) data = torch.matmul(data1,data3) print(data)
4.指定运算设备
- 注意点
- 注意要安装对应GPU版本的PyTorch,安装对应的CUDA
- 默认将张量创建在CPU控制的内存中,简称创建在CPU上
- 不同设备上的张量不能直接进行运算
- 使用
cuda
函数# 1.使用CUDA data = torch.randint(0,10,[5,5]) print(data.device)#打印device属性,表示张量存储的设备,此时是CPU """ data.cuda() 将数据放GPU上 :return:打印时返回其在CUDA中的编号(cuda:n,n从0开始,表示编号n的显卡) """ data = data.cuda() print(data.device)#此时在GPU上 """ data.cpu() 将数据放CPU上 :return:打印时显示cpu """ data = data.cpu() print(data.device)
- 直接创建在GPU上
#2.直接在GPU上创建张量 """ 在创建时传入device参数 """ data = torch.randint(0,10,[5,5],device="cuda:0") print(data.device)
- 使用
to
方法#3.使用to方法 data = torch.randint(0,10,[5,5]) print(data.device)#打印device属性,表示张量存储的设备,此时是CPU """ data.to() :param:要移动到的设备 """ data = data.to("cuda:0") print(data.device)#此时在GPU上 data = data.to("cpu") print(data.device)
(四)张量类型转换
1.张量转换为NumPy数组
- 使用
numpy
函数
#1.使用numpy函数
data = torch.randint(0,10,[2,2])
"""
data.numpy()
:return:将data转换为ndarray类型
注意:转换前后的对象是共享内存的!
"""
data_numpy = data.numpy()
print(type(data))
print(type(data_numpy))
- 追加
copy
函数
#2.使用copy函数
data2 = torch.randint(0, 10, [2, 2])
"""
在numpy函数后面加上copy函数,
使得转换前后的对象不共享内存空间
"""
data_numpy2 = data2.numpy().copy()
print(type(data2))
print(type(data_numpy2))
2.NumPy数组转换为张量
- 使用
from_numpy
data_numpy = np.array([1,2,3,4]) """ torch.from_numpy() :param:numpy数组 :return:张量 注意:默认是共享内存的,可以使用copy解决 """ data_tensor1 = torch.from_numpy(data_numpy) print(type(data_tensor1)) data_tensor2 = torch.from_numpy(data_numpy.copy())#注意copy的位置在括号内 print(type(data_tensor2))
- 使用
torch.tensor
data_numpy = np.array([1, 2, 3, 4]) """ 直接向torch.tensor中传递numpy数组 默认不共享内存 """ date_tensor = torch.tensor(data_numpy) print(type(date_tensor))
3.标量张量与数字的转换
- 提取单个元素的张量
data1 = torch.tensor(10) data2 = torch.tensor([10]) data3 = torch.tensor([[10]]) """ data.item() :return:将tensor中的元素转为标量返回 注意:tensor中只能有一个元素,维度任意 """ print(data1.item()) print(data2.item()) print(data3.item())
(五)张量拼接操作
1.torch_cat
data1 = torch.randint(0,10,[3,4,5])
data2 = torch.randint(0,10,[3,4,5])
print(data1)
print(data2)
"""
torch.cat()
:param1:要拼接的张量,使用列表
:dim:拼接的维数,结合不同维数的作用来考虑
:return:拼接后的张量
从这个例子上看,
维数为0:表示个数进行拼接,所有矩阵放到一个列表里
维数为1:按行拼接,对应矩阵纵向拼接到一起
维数为2:按列拼接,对应矩阵横向拼接到一起
"""
# 1.按照0维度进行拼接
data = torch.cat([data1,data2],dim=0)
print(data)
# 2.按照1维度进行拼接
data = torch.cat([data1,data2],dim=1)
print(data)
# 3.按照2维度进行拼接
data = torch.cat([data1,data2],dim=2)
print(data)
2.torch.stack
data1 = torch.randint(0,10,[4,5])
data2 = torch.randint(0,10,[4,5])
print(data1)
print(data2)
"""
torch.stack()
:param1:要拼接的张量,使用列表
:dim:拼接的维数,结合不同维数的作用来考虑
:return:拼接后的张量
从这个例子上看,
维数为0:表示个数进行拼接,所有矩阵放到一个列表里
维数为1:按行拼接,对应矩阵的对应行按照行拼接在一起
维数为2:按列拼接,对应矩阵的对应行按照列拼接在一起
"""
# 1.按照0维度进行拼接
data = torch.stack([data1,data2],dim=0)
print(data)
# 2.按照1维度进行拼接
data = torch.stack([data1,data2],dim=1)
print(data)
# 3.按照2维度进行拼接
data = torch.stack([data1,data2],dim=2)
print(data)
(六)张量索引操作
1.行列索引
data = torch.randint(0,10,[5,5])
print(data)
#1.行索引
"""
data[n]
n从0开始,表示第n行
"""
print(data[0])
#2.列索引
"""
data[:,n]
冒号:表示从...到...,省略前面表示从头部开始,省略后面表示到末尾结束
逗号:逗号前为行,逗号后为列
"""
print(data[:,0])
#3.指定行列
"""
data[:,:]
灵活使用冒号,可以得到局部数据
"""
print(data[0:2,0:3])
2.列表索引
def test02():
data = torch.randint(0,10,[5,5])
print(data)
#列表索引
"""
data([],[])
:param1:表示行坐标
:param1:表示列坐标
:return:组合对应的行列坐标从矩阵中取值,返回一个列表
注意:行列坐标个数要相等!
"""
print(data[[0,2,4],[0,2,4]])
3.布尔索引
data = torch.randint(0,10,[5,5])
print(data)
#布尔索引
"""
data可以与数字进行比较,
结果是满足条件的位置为True,不满足的位置为False的张量
然后使用data[条件]进行索引,对应True的位置进行输出
返回的是一个一维的张量
"""
print(data[data>3])
print(data[data[:,3]>3]) #返回所有行中,编号3的列>3的所有行
print(data[:,data[3]>3])#返回所有列中,编号3的行>3的所有列
4.多维索引
data = torch.randint(0,10,[3,5,5])
print(data)
#多维索引
"""
data[]
我们可以认为多维张量是多个张量的列表
:param1:表示选择第几个tensor
:param2:行索引
:param3:列索引
"""
print(data[0,:,2:3])
(七)张量形状操作
1.查看张量形状
data = torch.tensor([[1,2,3],[4,5,6]])
print(data)
#1.shape
"""
data.shape[]
:param:可选,表示指定维数
:return:张量的形状
"""
print(data.shape)
#2.size
"""
data.size()
:param:可选,表示指定维数
:return:张量的形状
"""
print(data.size())
2.reshape
data = torch.tensor([[1,2,3],[4,5,6]])
print(data)
"""
data.reshape()
:param1:转换后张量的行数
:param2:转换后张量的列数
:return:转换后的张量
注意:转换前后的张量元素个数要相等!
技巧:可以对行列中使用-1来让程序自动计算另一个数值,前提是行列有一个已经被指定
"""
data_new = data.reshape(3,2)
print(data_new)
data_new = data.reshape(3,-1)
print(data_new)
3.transpose
data = torch.randint(0,10,[3,4,5])
print(data)
#transpose
"""
torch.transpose()
:param1:要操作的张量
:param2:要交换的维度1
:param3:要交换的维度2
:return:交换维度1和维度2,返回交换后的张量
缺点:一次只能交换两个维度
"""
data_new = torch.transpose(data,0,1)
print(data_new)
4.permute
data = torch.randint(0,10,[3,4,5])
print(data)
"""
torch.permute()
:param1:要操作的张量
:param2:维度交换列表,对应位置交换为对应的维数
:return:交换后的张量
可以一次交换多个维度
"""
data_new = torch.permute(data,[2,0,1])
#表示将原来的维数顺序(0,1,2)交换为(2,0,1)
print(data_new)
5.view和contiguous
data = torch.randint(0,10,[4,5])
print(data)
"""
data.is_contiguous()
判断张量是否存储在连续的空间中
:return:True/False
"""
print(data.is_contiguous())
"""
data.view()
:param1:维度数值
:param2:维度数值
:return:交换维度数值后的张量
注意:view只能用于连续空间存储的张量
"""
data_new = data.view(5,4)
print(data_new)
data_test = torch.transpose(data,1,0)
#一般来说,经过transpose和permute之后,张量的存储空间不再连续
print(data_test.is_contiguous())
#可以使用contiguous()使存储空间连续
data_new = data_test.contiguous().view(4,5)
print(data_new)
6.squeeze和unsqueeze
#1.squeeze
data = torch.randint(0,10,[1,2,3,4])
print(data)
"""
data.squeeze()
去除维数为1的维度
:param:可选,指定维度
:return:去除维度后的张量
"""
data_new_1 = data.squeeze()
print(data_new_1)
#2.unsqueeze
data = torch.randint(0,10,[3,4])
print(data)
"""
data.unsqueeze()
增加维度,维数为1
:param:在指定位置添加维度
:return:添加维度之后的张量
"""
data_new_2 = data.unsqueeze(0)
print(data_new_2)
(八)张量运算函数
1.均值
data = torch.randint(0,10,[2,3],dtype=torch.float64)#生成整数随机数,以float64类型存储
print(data)
"""
data.mean()
:param dim:指定维度,表示按该维度求均值
:return:将所有元素相加求均值,返回均值,张量类型
"""
print(data.mean())
print(data.mean(dim=0))#按行这个维度,表示每一列按行相加求均值
print(data.mean(dim=1))#按列这个维度,表示每一行按列相加求均值
2.求和
data = torch.randint(0, 10, [2, 3], dtype=torch.float64)
print(data)
"""
data.sum()
:param dim:指定维度,表示按该维度求和
:return:将所有元素相加,返回和,张量类型
"""
print(data.sum())
print(data.sum(dim=0))#按行这个维度,表示每一列按行相加
print(data.sum(dim=1))#按列这个维度,表示每一行按列相加
3.次方
data = torch.randint(0, 10, [2, 3], dtype=torch.float64)
print(data)
"""
data.pow()
:param:将张量的每一个元素进行对应的次方
:return:平方后的结果,张量类型
"""
data = data.pow(2)
print(data)
4.平方根
data = torch.randint(0, 10, [2, 3], dtype=torch.float64)
print(data)
"""
data.sqrt()
将张量的每一个元素进行开平方根
:return:平方根后的结果,张量类型
"""
data = data.sqrt()
print(data)
5.指数
data = torch.randint(0, 10, [2, 3], dtype=torch.float64)
print(data)
"""
data.exp()
将张量的每一个元素作为指数计算e^指数的值
:return:运算后的结果,张量类型
"""
data = data.exp()
print(data)
6.对数
data = torch.randint(0, 10, [2, 3], dtype=torch.float64)
print(data)
"""
data.log()
将张量的每一个元素进行对数运算,默认以e为底
:return:运算后的结果,张量类型
"""
data = data.log()
print(data)
data = data.log2()#以2为底
print(data)
(九)梯度运算
1.一元的梯度运算
- 一元标量梯度运算:类似于一元函数求导,自变量只有一个值
""" 对于要求导的张量,我们需要设置一些属性: requires_gard:True dtype:一般设置为浮点数 """ x = torch.tensor(10,requires_grad=True,dtype=torch.float64) fx = x**2 + 1 #构建计算函数 #自动微分 """ fx.backward() 对fx进行求导得到导函数 将x=10代入求值 """ fx.backward() #访问梯度 print(x.grad)
- 一元向量梯度运算:类似于一元函数求导,自变量有多个值
x = torch.tensor([10,20,30,40],requires_grad=True,dtype=torch.float64) fx = x**2 + 1 #此时fx是一个向量 gx = fx.mean() #此时fx是一个标量 """ backward() 对于向量而言,不能直接使用,需要使用mean或者sum mean:求平均损失 sum:求总损失 过程: 1.fx有4个变量,调用mean得到: gx = 1/4 * fx 2.对gx进行求导,自变量为fx: g'x = 1/4 * f'x 3.根据复合函数求导规则: g'x = 1/4 * 2x = 1/2 * x 4.代入每一个x的值即可 """ gx.backward() print(x.grad)
2.多元的梯度运算
- 多标量梯度运算:对多元函数求偏导,每个自变量只有一个值
x1 = torch.tensor(10,requires_grad=True,dtype=torch.float64) x2 = torch.tensor(20, requires_grad=True, dtype=torch.float64) #构造函数 fx = x1**2 + x2**2 + 1 #标量可以直接使用backward fx.backward() print(x1.grad) print(x2.grad)
- 多变量梯度运算:对多元函数求偏导,每个自变量有多个值
x1 = torch.tensor([10,20],requires_grad=True,dtype=torch.float64) x2 = torch.tensor([30,40], requires_grad=True, dtype=torch.float64) #构造函数 fx = x1**2 + x2**2 gx = fx.sum() gx.backward() print(x1.grad) print(x2.grad)
(十)控制梯度运算
1.控制自动微分
x = torch.tensor(10,requires_grad=True,dtype=torch.float64)
#1.方式一
"""
with torch.no_grad()
是一个上下文管理器
作用是在其中计算得到的张量,其requires_grad=False
这些张量不会在Tensor上追踪计算历史,不会用于自动微分
适用于不需要进行梯度运算的张量,减少内存使用和计算时间
"""
with torch.no_grad():
y = x**2
print(y.requires_grad) #此时y的requires_grad为False
#2.方式二
"""
@torch.no_grad()
是一个语法糖
作用是其修饰的函数,返回值默认的requires_grad=False
"""
@torch.no_grad()
def func(x):
return x**2
y = func(x)
print(y.requires_grad)
#3.方式三
"""
torch.set_grad_enabled(False)
进行全局设置,一般不使用
"""
torch.set_grad_enabled(False)
y = x**2
print(y.requires_grad)
2.梯度累计和梯度清零
x = torch.tensor([10,20,30,40],requires_grad=True,dtype=torch.float64)
#现象:连续运算时会发现x的梯度值是会将历史的梯度值累加的
for _ in range(3):
f1 = x**2
f2=f1.mean()
f2.backward()
print(x.grad)
#避免累加需要进行梯度清零
for _ in range(3):
f1 = x**2
f2=f1.mean()
"""
先判断:当梯度不为0时
执行:使用x.grad.data.zero_()将梯度清零
"""
if x.grad is not None:
x.grad.data.zero_()
f2.backward()
print(x.grad)
3.简单的梯度优化函数
x = torch.tensor(10,requires_grad=True,dtype=torch.float64)
for _ in range(1000):
fx = x**2
if x.grad is not None:
x.grad.data.zero_()
fx.backward()
x.data = x.data - 0.001*x.grad
print("%.10f"%x.data)
(十一)自动微分张量的类型转换
1.现象
对一个设置requires_grad=True
的张量使用numpy
进行转换时,会出现报错:
RuntimeError: Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.
2.解决方法:detach
x = torch.tensor(10, requires_grad=True, dtype=torch.float64)
"""
x.detach()
分离x,产生一个新的张量,与x共享内存
"""
data_numpy = x.detach().numpy()
print(data_numpy)
(十二)模型组件
1.损失函数
#导包
import torch.nn as nn
#初始化平方损失函数对象
"""
nn.MSELoss()
是一个类,
类中重写了__call__方法
因此可以作为一个函数使用
"""
criterion = nn.MSELoss()
y_pred = torch.randn(3,5,requires_grad=True)
y_true = torch.randn(3,5)
#计算损失
"""
将该类作为函数使用
:param1:预测值
:param2:真实值
:return:平方损失,tensor类型
"""
loss = criterion(y_pred,y_true)
print(loss)
2.假设函数
#导包
import torch.nn as nn
"""
nn.Linear()
:param1:输入数据的特征数
:param2:输出数据的特征数
:return: Linear对象
"""
model = nn.Linear(in_features=10,out_features=5)
#输入数据
inputs = torch.randn(5,10)
"""
将该类作为函数使用
:param:输入数据
:return:预测值
"""
y_pred = model(inputs)
3.优化方法
#导包
import torch.optim as optim
model = nn.Linear(in_features=10, out_features=5)
"""
optim.SGD()
:param1:传入参数,可以使用model.parameters()
:param lr:学习率
"""
optimizer = optim.SGD(model.parameters(),lr=0.001)
#调用backward函数得到梯度值
#(省略一些调用逻辑)
#进行梯度清零可以使用:optimizer.zero_grad()
#更新模型参数
optimizer.step()
(十三)数据集对象
1.自定义数据集对象
from torch.utils.data import Dataset
#需要实现以下方法
class Data(Dataset):#继承Dataset
def __init__:#初始化
def __len__:#返回长度
def __getitem__:#索引
2.构建数据加载器
from torch.utils.data import DataLoader
#创建数据集
x = torch.randn(100,8)
y = torch.randint(0,2,[x.size(0),])
#1.构建数据类
dataset = Data(x,y)
#2.构建数据加载器
dataloader = DataLoader(dataset,batch_size=4,shuffle=True)
3.简单数据集
from torch.utils.data import TensorDataset
x = torch.randn(100,8)
y = torch.randint(0,2,[x.size(0),])
dataset = TensorDataset(x,y)
dataloader = DataLoader(dataset,batch_size=4,shuffle=True)
(十四)模型保存与加载
1.模型对象
class Model(nn.Module):
def __init__(self,input_size,output_size):
super(Model,self).__init__()
self.linear1 = nn.Linear(input_size,output_size*2)
self.linear2 = nn.Linear(input_size*2,output_size)
def forward(self,inputs):
"""
当模型类继承了nn.Model并实现了forward函数
此时可以将模型对象作为函数使用
:param inputs:
:return:
"""
inputs = self.linear1(inputs)
output = self.linear2(inputs)
return output
2.直接存储方式
#存储
model = Model(64,10)
"""
torch.save
:param1:要存储的模型对象
:param2:存储的位置
:param pickle_module:存储方式
:param pickle_protocol:存储协议
"""
torch.save(model,"model/model.pth",pickle_module=pickle,pickle_protocol=2)
#加载
"""
torch.load
:param1:加载模型的存储位置
:param pickle_module:加载方式
:param map_location:加载到的设备
"""
model = torch.load("model/model.pth",pickle_module=pickle,map_location="cpu")
3.存储模型的网络参数
#存储
#自定义要存储的参数:字典形式
#选择要存储的参数即可
save_params = {
"init_params":{"input_size":64,"output_size":10},
"acc_score": 0.88,
"avg_loss": 0.53,
"item_num": 34,
"optimizer_params":optimizer.state_dict(),
"model_params":model.state_dict()
}
#存储模型参数
torch.save(save_params,"model/model.pth")
#加载
#从磁盘中加载到内存
model_params = torch.load("model/model.pth")
#使用参数初始化模型
model = Model(model_params["init_params"]["input_size"],model_params["init_params"]["output_size"])
model.load_state_dict(model_params["model_params"])
#使用参数初始化优化器
optimizer = optim.Adam(model.parameters())
optimizer.load_state_dict(model_params["optimizer_params"])
#其他参数同理
二、深度学习基础
(一)神经网络简介
graph LR
id0("输入")-->id1(第一层神经元)-->id2(第二层神经元)--"......"-->id3(第n层神经元)-->id4(输出)
- 我们将输入称为输入层,输出称为输出层,中间余下的层级统称为隐藏层
- 信息经过输入层进入隐藏层,隐藏层通过既定算法对信息进行处理,然后传递到输出层
- 每一层由多个神经元组成,同一层的神经元一般来说是没有连接的
(二)激活函数
1.作用
- 激活函数的作用是为每一层的输出数据进行变换,为其添加非线性因素
- 这样一来,神经网络就可以拟合出曲线模型,而不是仅仅只能拟合线性模型
2.要求
- 由于我们在更新网络参数时,使用的是反向传播算法
- 因此我们要求激活函数可微
3.sigmoid激活函数
$$
f(x)=frac{1}{1+e^{-x}}
$$
- sigmoid函数将任意输入映射到(0,1)
- 当输入值在[-6,6]时,输出值才会有明显差异
- 当输入值在[-3,3]时,输出值才会有较好效果
- sigmoid函数导数的数值范围是(0,0.25)
- 当输入值在[-6,6]之外时,导数值接近于0,此时网络参数更新极其缓慢
- 当输入值趋于0时,导数值趋于0.25
- sigmoid神经网络在5层之内就会产生梯度消失现象,因此此函数用得不多,一般只用于二分类问题
- 梯度消失:神经网络类似于多层复合函数,对其求梯度适用链式法则,而多重sigmoid函数复合,因为其范围(0.0.25),所以值会接近于0
4.tanh激活函数
$$
f(x)=frac{1-e^{-2e}}{1+e^{-2e}}
$$
- tanh函数将任意输入映射到(-1,1)
- 当输入值在(-3,3)之外时,会被映射为-1或1
- tanh函数导数的数值范围是(0,1)
- 当输入值在(-3,3)之外时,导数值接近于0
- tanh函数以0为中心,其收敛速度比sigmoid快,但同样存在梯度消失问题
- 在实际使用时,可以在隐藏层使用tanh函数,在输出层使用sigmoid函数
5.ReLU激活函数
$$
f(x)=max(0,x)
$$
- ReLU函数将负值映射为0,正值保持不变
- ReLU函数计算简单,能够提供模型的训练效率
- 但当网络参数采用随机初始化时,可能存在负值的参数,这可能导致正负值保留情况出现预料之外的效果
- ReLU函数在x>0时不存在饱和问题(梯度趋于0),能够保持梯度不衰减,缓解梯度消失问题
- 但当训练深入时,可能存在部分输入转变为负数,导致对应权重无法更新,我们称之为神经元死亡
- 但这种情况不一定是负面的,部分神经元输出为0,可以减少参数间的依存关系,使网络更加稀疏,减小过拟合风险
6.SoftMax激活函数
$$
softmax(z_i)=frac{e^{z_i}}{sum_j e^{z_j}}
$$
- SoftMax函数适用于多分类问题,是对sigmoid函数在多分类上的推广,目的在于将多分类的结果以概率的形式表示
- SoftMax函数将输出的分类结果转换为(0,1)之间的概率值,所有结果的概率和为1
- 简单来说,函数的分子是对某一结果取e的指数,函数的分母是对所有结果取e的指数再求和
7.激活函数的选择
- 对应隐藏层:
- 优先考虑ReLU函数及其变体,注意神经元死亡问题
- 不要使用sigmoid函数,使用tanh函数替代
- 对于输出层:
- 二分类问题:sigmoid函数
- 多分类问题:SoftMax函数
- 回归问题:identity函数
- identity:$f(x)=x$
(三)参数初始化
1.初始化参数
- 一般来说我们需要初始化的参数有:
- 权重:比较重要,可以使用一些参数初始化方法
- 偏置:一般为0
- 一般来说实际开发过程中,PyTorch框架会给我们参数对应的默认值,一般不需要改动,除非模型效果较差的时候考虑改动参数
2.均匀分布初始化
- 权重参数从区间内均匀取值,一般是在$(-frac{1}{sqrt d},frac{1}{sqrt d})$区间上,d表示每个神经元的输入数量,也就是有多少数据进入一个神经元
#先随便搞个模型
linear = nn.Linear(5,3)
#初始化
"""
nn中有一个init模块
从中调用各种初始化方式
均匀分布就是uniform(带_表示对原数据修改)
参数是模型的权重linear.weight
"""
nn.init.uniform_(linear.weight)
print(linear.weight)
3.正态分布初始化
- 随机初始化从均值为0,标准差为1的高斯分布中取值,一般来说这些值很小
linear = nn.Linear(5,3)
#初始化
"""
normal()
:param1:输入权重
:param mean:要指定正态分布的均值
:param std:要指定正态分布的标准差
"""
nn.init.normal_(linear.weight,mean=0,std=1)
print(linear.weight)
4.固定值初始化
- 没啥好说的...设置一个固定值作为初始化值,其实还有个分类:
- 全0初始化:固定值为0,一般去初始化偏置,不要初始化权重,权重全0这个神经元就会输出0,导致下一层相关神经元也可能为0
- 全1初始化:固定值为1
linear = nn.Linear(5,3)
#初始化
"""
constant()
:param1:输入权重
:param2:要指定的固定值
"""
nn.init.constant_(linear.weight,5)
print(linear.weight)
"""
zeros()
全0初始化
:param:输入权重
"""
nn.init.zeros_(linear.weight)
print(linear.weight)
"""
ones()
全1初始化
:param:输入权重
"""
nn.init.ones_(linear.weight)
print(linear.weight)
5.kaiming初始化
- 也称为HE初始化,分为正态分布的HE初始化和均匀分布的HE初始化
linear = nn.Linear(5,3)
#正态分布的kaiming初始化
"""
kaiming_normal()
:param:输入权重
"""
nn.init.kaiming_normal_(linear.weight)
print(linear.weight)
#均匀分布的kaiming初始化
"""
kaiming_uniform()
:param:输入权重
"""
nn.init.kaiming_uniform_(linear.weight)
print(linear.weight)
6.xavier初始化
- 也称为Glorot初始化,基本思路是让每一层的激活值和梯度的方程在传播过程中保持一致,同样分为正态分布的xavier初始化和均匀分布的xavier初始化
linear = nn.Linear(5,3)
#正态分布的xavier初始化
"""
xavier_normal()
:param:输入权重
"""
nn.init.xavier_normal_(linear.weight)
print(linear.weight)
#均匀分布的xavier初始化
"""
xavier_uniform()
:param:输入权重
"""
nn.init.xavier_uniform_(linear.weight)
print(linear.weight)
(四)反向传播
1.梯度下降算法
$$
w{new}=w{old}-eta frac{partial E}{partial w}
$$
- $frac{partial E}{partial w}$:梯度,寻找该点增长最快的方向,它的反向就是下降最快的方向
- $eta$:学习率,也就是每次运动的一个距离,学习率太大容易错过最优解,学习率太小训练效率很慢,最好随着训练过程动态调整
- 举个栗子:
- 有一个二维的函数$y=x^2$,其导数就是$y'=2x$,目的是求最小值的点
- 初始点为(4,16),学习率为0.25,一次迭代得到$w_{new}=4-0.25(24)=2$,新的点为(2,4)
- 继续:$w_{new}=2-0.25(22)=2$,新的点为(1,1)
- 继续:$w_{new}=1-0.25(21)=0.5$,新的点为(0.5,0.25)
- 继续:$w_{new}=0.5-0.25(20.5)=0.25$,新的点为(0.25,0.0625)
- 重复不断地逼近(0,0)
- 对于更高维度的,无非就是对每个参数求偏导,然后对应相减嘛
2.三个重要概念
- Epoch:指的是对整个模型完成一次学习,称为一个Epoch
- 以读书举例,表示将一本书读完,多个Epoch就表示反复读这本书多次
- Batch:指的是对一个子数据集完成一次学习,称为一个Batch
- 以读书举例,表示一本书我每次读10页,这10页读完就完成一个Batch,多个Batch就表示多次读10页直到把书读完呗
- Batch的数量,应该是整个模型的大小除以Batch的大小,也就是每次读10页,要多少次能读完,当然最后一次如果小于10页也需要读一次
- Iteration:指的是使用一个子数据集对参数完成一次更新,称为一个Iteraiton
- 以读书举例,我在读书之前对整本书做一个笔记,然后每读10页有了新的了解,就对这个笔记进行更改,Interation的个数与Batch相同
3.多种梯度下降算法
不同的梯度下降算法的根本区别就在于Batch的大小不同,也就是每次读的页数不同
- BGD:Batch的大小是整个样本,也就是每次我都将书读完
- SGD:Batch的大小是一个样本,也就是每次我都只读一页
- Mini-Batch:Batch的大小是指定的一个数,就记为n吧,也就是每次我都读n页
4.前向传播和反向传播的概念
- 前向传播:指的是按照输入层--隐藏层--输出层的顺序进行传播,逐层运算之后得到预测值
- 反向传播:得到前向传播的预测值之后,与真实值之间的差异结合得到损失函数,由损失函数反向地求各个参数的偏导,就是反向传播
5.反向传播算法
BP算法,又称为误差反向传播算法,用于求解模型的参数梯度,从而使用梯度下降法更新模型参数:
- 通过正向传播得到预测值,通过损失函数计算预测值和真实值之间的误差
- 通过反向传播将误差传递给网络中的神经元,对神经元的参数进行调整,从而缩小误差
举个栗子:
graph LR
id0("输入:两个特征")-->id1(神经元A)
id0-->id2(神经元B)
id1-->id3(神经元C)
id1-->id4(神经元D)
id2-->id3
id2-->id4
id3-->id5(输出)
id4-->id5
- 假设:
- 输入:i、j
- 下列运算函数中,x为自变量,k为偏置是常数,x的系数a,b,c,d为权重,也就是要更新的参数
- 神经元A:运算函数$O_1=a_1 x_1+a_2 x2+k{1}$
- 神经元B:运算函数$O_2=b_1 x_1+b_2 x2+k{2}$
- 神经元C:运算函数$O_3=c_1 x_1+c_2 x2+k{3}$
- 神经元D:运算函数$O_4=d_1 x_1+d_2 x2+k{4}$
- 假设所有神经元的激活函数是sigmoid,例如神经元A的输出结果$O_1$会经过激活函数sigmoid,有$M_1=sigmoid(O_1)$作为该神经元最终的输出结果
- 假设最终损失函数为平方损失,用L表示
- 真实值分别为$t_1$和 $t_2$,为常数
- 过程:
- 正向传播:
- i和j传入神经元A和B,输出$M_1$和$M_2$
- $M_1$和$M_2$作为新的输入值,传入神经元C和D,输出$M_3$和$M_4$
- 因为后续没有其他神经元,则$M_3$和$M_4$为最终输出结果,也就是预测值
- 计算损失函数$L=frac12((M_3-t_1)^2+(M_4-t2)^2)$
- 反向传播:
- 我们的目的是使得L的值最小,方式是通过修改参数a,b,c,d
- 例如更新神经元C中的$c_1$:
- 将$c_1$视为自变量,用损失函数L对$c_1$求导
- 分析一下,L是$M_3$和$M_4$的函数,其中$M_4$与$c_1$无关,求导后是0;而$M_3$与$c_1$有关
- $M_3$是$O_3$的函数,$O_3$是$c_1$的函数
- 那么本质上,从L到$c_1$是一个复合函数求导,使用链式法则
- $frac{partial L}{partial c_1}=frac{partial L}{partial M_3}frac{partial M_3}{partial O_3}frac{partial O_3}{partial c_1}$
- 由此求得$c_1$的梯度,使用梯度下降算法更新$C_1$即可
- 其他神经元和参数的处理同理,本质上就是进行复合函数求导
- 需要注意的是在求神经元A和神经元B的参数时,要考虑到其结果是作为输入传入神经元C和D的,因此$M_3$和$M_4$均与之相关
import torch
import torch.nn as nn
import torch.optim as optim
#自定义网络类需要继承父类nn.Module
class Net(nn.Module):
def __init__(self):
super(Net,self).__init__()
self.linear_1 = nn.Linear(in_features=2,out_features=2)#神经元A和B
self.linear_2 = nn.Linear(in_features=2,out_features=2)#神经元C和D
#查看默认的参数
print(self.linear_1.weight.data,self.linear_1.bias.data)#神经元A和B的参数和偏置
print(self.linear_2.weight.data,self.linear_2.bias.data)#神经元C和D的参数和偏置
print("-"*10)
#进行正向传播的函数
def forward(self,x):
#第一层
x=self.linear_1(x)#经过第一层得到输出
x=torch.sigmoid(x)#经过sigmoid得到最终输出
#第二层
x=self.linear_2(x)
x=torch.sigmoid(x)
#返回最终输出结果:预测值
return x
#模拟一次反向传播
def test():
#输入数据,二维列表表示一批数据
inputs = torch.tensor([[1,2]],dtype=torch.float)
#真实值
target = torch.tensor([[2,5]],dtype=torch.float)
#初始化网络对象
net = Net()
#得到预测值
output = net(inputs)#因为父类中实现了__call__方法,默认调用了Net中的forward函数
print(f"预测值:{output}")#打印预测值
#计算误差:使用平方误差
loss = torch.sum((output-target)**2) / 2
print(f"损失值:{loss}")
#构建优化器
optimizer = optim.SGD(net.parameters(),lr=0.5)
#梯度清零
optimizer.zero_grad()
#反向传播
loss.backward()#相当于对所有参数求导,得到梯度值
#参数更新
optimizer.step()#使用梯度下降算法更新参数
#打印每个参数的梯度
print(net.linear_1.weight.grad.data)
print(net.linear_2.weight.grad.data)
print("-"*10)
#更新后的参数值
print(net.linear_1.weight.data, net.linear_1.bias.data) # 神经元A和B的参数和偏置
print(net.linear_2.weight.data, net.linear_2.bias.data) # 神经元C和D的参数和偏置
if __name__ == '__main__':
test()
(五)优化方法
1.现象
对于传统的梯度下降算法,可能会遇到:
- 梯度值过小,导致参数更新缓慢
- 碰到鞍点或局部最小值,参数无法继续更新
2.指数移动加权平均
- 一些概念前提:
- 算术平均:指的是所有数拥有相同的权重,简单相加之后除以个数,得到的就是算术平均
- 加权平均:指的是为每个数赋予一定权重,每个数乘以权重再相加,除以个数得到加权平均
- 移动平均:指的是指定一个数值n,计算邻近的n个数,得到移动平均
- 结合上面的概念我们得到:
- 指数移动加权平均:根据距离的远近赋予数不同的权重,一般来说距离越近权重越大,由此得到的加权平均数,我们称之为指数移动加权平均
$$
S_n=begin{cases}Vn,当n=0时 beta S{n-1}+(1-beta)V_n,当nneq0时end{cases}
$$
- 其中S是指数移动加权平均值
- n表示距离
- $beta$表示调节权重,越大则平均数越平缓
随机生成的例子:
import torch
import matplotlib.pyplot as plt
#指数加权平均
def test01():
_, axes = plt.subplots(1, 3)
torch.manual_seed(0)
#随机产生20个数
noise = torch.randint(0,50,size=[20,],dtype=torch.float)
print(noise)
distance = torch.arange(1,21,1)
print(distance)
axes[0].scatter(distance,noise)
#指数移动加权平均
beta=0.9
exp_weight_avg = []
for idx,temp in enumerate(noise,1):
if idx==1:
exp_weight_avg.append(temp)
continue
new_temp = exp_weight_avg[idx-2]*beta+(1-beta)*temp
exp_weight_avg.append(new_temp)
#绘制
axes[1].plot(distance,exp_weight_avg)
axes[1].set_title("beta=0.9")
#指数移动加权平均
beta=0.5
exp_weight_avg = []
for idx,temp in enumerate(noise,1):
if idx==1:
exp_weight_avg.append(temp)
continue
new_temp = exp_weight_avg[idx-2]*beta+(1-beta)*temp
exp_weight_avg.append(new_temp)
#绘制
axes[2].plot(distance,exp_weight_avg)
axes[2].set_title("beta=0.5")
plt.show()
3.Momentum
- 当参数出现更新缓慢或无法更新的情况时,Momentum通过指数加权平均法,累计历史的梯度值对参数进行更新,可以优化梯度,
- 动量项的计算方法:
$$
vt=beta *v{t-1}+eta*L(w_t)
$$- $v_{t-1}$:表示第t次迭代的动量项
- $w_t$:表示当前参数
- $L(w_t)$:表示当前参数的梯度值
- $beta$:表示权重系数,一般取0.9
- $eta$:表示学习率
- 参数的更新方法:
$$
w_{t+1}=wt-v{t+1}
$$ - 举个栗子:
- 假设权重$beta = 0.9$,取学习率为0.1,初始化动量项$v_0=0$
- 第一次梯度值:动量项$v_1=0.9v_0+0.1L(w_0)$,更新参数$w_1=w_0-v_1$
- 第二次梯度值:动量项$v_2=0.9v_1+0.1L(w_1)$,更新参数$w_2=w_1-v_2$
- 注意:Momentum并没有对学习率进行优化
4.AdaGrad
- AdaGrad对不同的参数分量使用不同的学习率,其学习率总体上会逐渐减小
- 学习率计算公式:
$$
eta^*=frac{eta}{sqrt{s}+epsilon}
$$- $eta$:学习率
- $g$:表示当前的梯度
- $s$:表示累计的平方梯度,$st=s{t-1}+g{t-1}odot g{t-1}$,其中$odot$表示阿达玛积
- $epsilon$:表示一个小常数,用于避免分母为0的情况,通常取非常小的数,例如$1^{-9}$
- 参数更新公式:
$$
w_{t+1}=wt-eta^*g{t+1}
$$ - 注意:AdaGrad可能导致学习率过早降低到一个较小值,导致模型训练后期学习率较小,不易找到最优解
5.RMSProp
- RMSProp是对AdaGrad的优化,其使用指数移动加权平均梯度替代了历史梯度平方和,也就是更改了s的求法
- 学习率计算公式:
$$
eta^*=frac{eta}{sqrt{s}+epsilon}
$$- $eta$:学习率
- $g$:表示当前的梯度
- $s$:表示累计的平方梯度,$$st=beta s{t-1}+(1-beta)g{t-1}odot g{t-1}$$,其中$odot$表示阿达玛积
- $epsilon$:表示一个小常数,用于避免分母为0的情况,通常取非常小的数,例如$1^{-9}$
- 参数更新公式:
$$
w_{t+1}=wt-eta^*g{t+1}
$$
6.Adam
- Adam是对Momentum和RMSProp的综合,同时使用移动加权平均的梯度和移动加权平均的学习率
(六)正则化
1.作用
- 正则化的主要作用是缓解过拟合
2.Dropout层
- Dropout通过减少神经元之间的连接,达到降低网络复杂度的目的
- 我们对Dropout设置一个概率p,表示丢弃的概率,也就是将张量元素设置为0
- 为了校正张量元素设置为0带来的影响,会对非0元素进行缩放,缩放比例为$frac{1}{1-p}$
#初始化Dropout对象
"""
nn.Dropout
是一个类,作用是随机丢弃神经元,可以传入参数p
:param p:表示丢弃神经元的概率,默认为0.5
"""
dropout = nn.Dropout(p=0.8)
#初始化随机数据
data = torch.randint(0,10,size=[8,8],dtype=torch.float)
print(data)
#将data数据经过Dropout丢弃
out = dropout(data)
print(out)#观察结果会发现出现大量的0,表示神经元被丢弃
(七)批量归一化
1.作用
- Batch Normalization(批量归一化,简写为BN)的主要作用是控制数据的分布,从而加快网络的收敛
- 因为我们在输入数据的时候,通常是分多个Batch的,每个Batch内的数据可能是满足不同分布的
- 例如我们将第一批数据满足的分布称为a,第二批数据满足的分布称为b
- 当第一批数据进入网络时,模型参数会进行调整以适应a分布
- 当第二批数据进入网络时,模型参数会进行调整以适应b分布
- 这就意味着,网络的参数会进行较大幅度的调整,不利于网络的收敛
- 此时,我们可以通过BN对数据进行标准化,使其分布相对稳定一些,从而加快网络收敛
2.公式
$$
f(x)=gammafrac{x-E(x)}{sqrt{Var(x)+epsilon}}+beta
$$
- $gamma$、$beta$:可学习参数,相当于对标准化之后的值做一个线性变换
- $epsilon$:表示一个小常数,用于避免分母为0的情况,通常取非常小的数,例如$1^{-9}$
- $E(x)$:表示变量的均值,使用的是移动加权方法
- $Var(x)$;表示变量的方差,使用的是移动加权方法
3.API
#随机生成数据
"""
生成的形状是[batch_size,channel,height,width]
batch_size:该批次中有几个样本
channel:通道数,可以理解为有几个特征
height:高,也就是行数
width:宽,也就是列数
"""
inputs = torch.randint(0,10,[1,3,5,5],dtype=torch.float)
print(inputs)
print("-"*50)
#创建BN对象
"""
nn.BatchNorm2d()
:param num_features:表示通道数
:param affine:表示是否携带学习参数,若为否,那么gamma和beta分别为1和0
:param momentum:调节移动加权平均值的计算,这个用于控制计算均值和方差
:param eps:表示小常数
"""
bn = nn.BatchNorm2d(num_features=3,affine=False,momentum=0.1,eps=1e-6)
output = bn(inputs)
print(output)
三、卷积神经网络
(一)卷积神经网络简介
- 卷积神经网络是深度学习在计算机视觉领域的重要成果,是含有卷积层的神经网络,卷积层的作用是用于自动学习和提取图像的特征
- 我们之前了解到的全连接神经网络,对于图像的处理计算开销大,且难以保留图像的特征,准确率低
- 卷积神经网络由三部分组成:
- 卷积层:用于提取图像中的局部特征
- 池化层:用于降低参数的维度
- 全连接层:用于输出想要的结果
(二)图像的基本知识
1.像素
- 像素:图像由像素点组成,每个像素点的范围是[0,255],像素点越大表示该点越亮
- 注意,仅有像素的图片是黑白的
#创建了一个200*200的像素值全为0的图像 img = np.zeros([200,200]) """ plt.imshow() :param:传入像素点的取值 :param cmap:用于映射标量数据到颜色,默认值为'viridis',若传入的是RGB类型则忽略此参数 :param vmin:当使用标量数据且没有显式范数时,vmin 和 vmax 定义颜色图涵盖的数据范围 """ plt.imshow(img,cmap="gray",vmin=0) plt.show() #200*200像素值全为1的图像 img = np.ones([200,200]) plt.imshow(img,cmap="gray",vmin=0) plt.show() #随机的200*200 img = np.random.randint(0,256,size=[200,200]) plt.imshow(img, cmap="gray", vmin=0) plt.show()
2.通道
- 通道:一般来说,彩色图片是由多张图片叠加而成的,通道就是说由哪些图片叠加而成
- RGB通道:图像由R、G、B三个通道叠加而成,分别表示红色、绿色、蓝色
- RGBA通道:相比与RGB通道多添加了一个A通道,表示透明度,其值越小,图像越透明
""" plt.imread() 用于读取图片信息 :param:图片地址 :return:ndarray类型的数值 得到的ndarray,例如(1080, 1920, 3) 第一个维数表示高度上有多少像素点,即高度上有1080个像素点 第二个维数表示宽度上有多少像素点,即宽度上有1920个像素点 第三个维数表示有多少通道,即JPG格式有3个通道(RGB),PNG格式一般有4个通道(RGBA) """ img = plt.imread("data/001.jpg") img = np.transpose(img,[2,0,1])#将通道数放到第一个维数上,方便后续遍历 for channel in img: #展示每个通道的图片 print(channel) plt.imshow(channel) plt.show()
(三)卷积层
1.卷积计算
- 卷积计算:是指通过一个卷积核(也称为滤波器),对原有图像进行计算,得到特征图
- 过程:
- 假设一张图片的大小是100*100的,也就是一个100行、100列的一个矩阵,记为A矩阵
- 卷积核也是一个矩阵,一般是需要学习的,我们假设它是5*5的,也就是一个5行、5列的矩阵,记为B矩阵
- 我们从A矩阵的左上角开始,将B矩阵按左上角对齐的方式放置到A矩阵上,那么会有5行、5列的元素重合对应
- 我们将对应元素作阿达玛积,然后相加的得到一个数,作为特征图的第1行第1列的元素值
- 然后,我们将卷积核水平右移一位,继续作阿达玛积求和,作为特征图的第1行第2列的元素值
- 以此类推,直到卷积核的最右侧与A矩阵的最右侧重合,得到特征图第1行全部数值
- 随后,我们将卷积核水平平移回到最左侧的位置,然后下移一位,逐步右移继续计算特征图第2行的数值
- 以此类推,直到卷积核与A矩阵的右下角重合,得到特征图的全部数值
2.Padding
- 通过上述的计算过程,我们可以发现,经过计算之后的特征图比原图的尺寸要小
- 如果我们想要得到一个与原图尺寸相同的特征图,就可以在原图周围添加Padding
- 所谓的Padding,就是对原图的一个扩展,也就是在矩阵周围添加n圈元素,n就是Padding的值
- 例如:(其中的0是代表一个元素,没有具体值的含义)
$$
left[begin{matrix}0&0&0&0&0&0&0 end{matrix}right] to Padding=1 to left[begin{matrix}0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0end{matrix}right]
$$
- 例如:(其中的0是代表一个元素,没有具体值的含义)
3.Stride
- 再考虑上面的计算过程,我们一直是通过右移和下移1位来得到特征图的
- 实际上,我们也可以一次右移和下移多位来得到特征图,移动的位数我们称之为步长,也就是Stride
- 注意:Stride能够影响计算的次数,以及最终得到的特征图的尺寸
4.多通道卷积计算
- 实际使用时,我们遇到的图像大多数是多通道的
- 对于多通道的情况,进行卷积计算的过程也差不多:
- 我们需要为每一个通道提供一个对应的卷积核
- 对每一个通道,使用对应的卷积核按照卷积计算的过程得到对应的特征图矩阵
- 最后将得到的所有特征图矩阵按矩阵方式相加,得到最终的特征图矩阵
- 当然,这个效果可以等价为每计算完一个对应位置就相加,可以节省一点存储空间
5.多卷积核卷积计算
- 对于每一个卷积核,我们理解为一种理解图像的角度
- 那么,对于同一个图像,我们是可以通过多个角度来分析图像的,也就是可以使用多个卷积核进行卷积计算
- 多卷积核卷积计算的过程:
- 例如,对于一个图像,我们有两个卷积核
- 我们分别用两个卷积核与图像做卷积计算,得到两个特征图
- 我们将两个特征图视为一个图像的两个通道,形成一个结果(一般来说,我们会将这个结果送到下一个卷积层)
- 对于多通道的原图,过程是类似的:
- 我们先提供多个对应的卷积核组
- 每一个卷积核组与原图按照多通道卷积计算的规则得到一个特征图
- 最后我们将所有得到的特征图作为一个图形的多个通道,形成结果
6.特征图大小
- 我们知道,特征图的大小与一下参数相关:
- size:卷积核的尺寸,一般都是奇数的方阵
- Padding:填充的圈数
- Stride:步长
- 设原图大小为$WW$,卷积核大小为$FF$,Padding=P,Stride=S,特征图大小为$N*N$,那么:
$$
N=frac{W+2P-F}{S}+1
$$- 先考虑填充情况,填充后的图像尺寸为$(W+2P)*(W+2P)$
- 卷积核右移和下移的长度就是$(W+2P)-F$,思考卷积核右侧(下侧)与填充图的右侧(下侧)重合移动的距离
- 用这个距离除以卷积核一次移动的步长,就是移动时单个方向的计算次数,我们知道每次计算形成特征图的一个元素,也就是$frac{W+2P-F}{S}$
- 别忘了,卷积核第一次重合时也进行了一次计算,也就是移动之前我们也进行了一次计算,因此需要加1
- 最终得到特征图的大小就是$N=frac{W+2P-F}{S}+1$
- 对于不是方阵的原图,分别计算长宽即可
7.API
- 卷积层对于输入的数据有形状要求:(BatchSize,Channel,Height,Width)
- BatchSize:批次
- Channel:通道数
- Height:高度
- Width:宽度
- 单个卷积核
#读取图像 img = plt.imread("data/001.jpg") print(img.shape) #创建卷积核 """ nn.Conv2d() 常用于图像处理 :param in_channels:输入图像的通道数 :param out_channels:输出特征图的通道数,也是卷积核组的数量 :param kernel_size:卷积核尺寸 :param stride:步长 :param padding:填充数 """ conv = nn.Conv2d(in_channels=3,out_channels=1,kernel_size=3,stride=1,padding=0) #处理输入数据,使其满足卷积层要求 img = torch.tensor(img,dtype=torch.float32).permute(2,0,1) #因为只有一个批次,直接在批次处填1即可 img = img.unsqueeze(0) print(img.shape) #传入卷积层处理 img = conv(img) print(img) #显示处理后的图像 img = img.squeeze(0).permute(1,2,0) #将图像的格式改回(Height,Width,Channel) plt.imshow(img.detach().numpy()) plt.show()
- 多个卷积核
#读取图像 img = plt.imread("data/001.jpg") #创建卷积核 conv = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3, stride=1, padding=0) #out_channels改一下就行 #处理输入数据,使其满足卷积层要求 img = torch.tensor(img,dtype=torch.float32).permute(2,0,1) #因为只有一个批次,直接在批次处填1即可 img = img.unsqueeze(0) #传入卷积层处理 img = conv(img) #显示处理后的图像 img = img.squeeze(0).permute(1,2,0) #将图像的格式改回(Height,Width,Channel) #多通道的图像要分通道展示 plt.imshow(img[:,:,0].detach().numpy()) plt.show() plt.imshow(img[:, :, 1].detach().numpy()) plt.show() plt.imshow(img[:, :, 2].detach().numpy()) plt.show()
(四)池化层
1.池化计算
- 池化计算:
- 与卷积层类似,我们会有一个池化核,也是矩阵,一般来说,池化核的计算是固定的,也就是不进行学习
- 我们通过与卷积核一样的操作,移动池化核,按照一定的规则进行计算得出数值
- 计算规则:
- 最大池化:每次选择池化核范围内最大的数值
- 平均池化:每次计算池化核范围内数据的均值
2.Stride和Padding
- 与卷积层的Stride相同,指的是步长
- 与卷积层的Padding相同,指的是填充圈数,一般默认进行填充的元素为0
3.多通道池化计算
- 对于多通道数据,池化计算会分别对每个通道进行池化计算
- 但与卷积层不同,池化计算的结果不会进行合并,会输出与通道数数量相同的多个结果
4.API
- 卷积层对于输入的数据有形状要求:(BatchSize,Channel,Height,Width)
- BatchSize:批次
- Channel:通道数
- Height:高度
- Width:宽度
- 单通道池化
#创建了一个3*3矩阵 inputs = torch.randint(0,10,[3,3],dtype=torch.float32) print(inputs) #处理数据 inputs = inputs.unsqueeze(0).unsqueeze(0) #1.最大池化 max_pool = nn.MaxPool2d(kernel_size=2,stride=1,padding=0) output = max_pool(inputs) print(output) #2.平均池化 avg_pool = nn.AvgPool2d(kernel_size=2,stride=1,padding=0) output = avg_pool(inputs) print(output)
- 多通道池化
#随机构建了输入,通道数为3,3*3的矩阵数据 inputs = torch.randint(0,10,size=[3,3,3],dtype=torch.float32) print(inputs) inputs.unsqueeze(0) #1.最大池化 max_pool = nn.MaxPool2d(kernel_size=2,stride=1,padding=0) output = max_pool(inputs) print(output) #2.平均池化 avg_pool = nn.AvgPool2d(kernel_size=2,stride=1,padding=0) output = avg_pool(inputs) print(output)
四、循环神经网络
(一)循环神经网络简介
- 循环神经网络,又称RNN网络,主要用于自然语言处理方向
- 主要研究目的是通过计算机算法理解自然语言
(二)词嵌入层
1.作用
- 作用:将文本进行数值化
2.使用过程
- 过程:根据输入词的数量构建一个词向量矩阵,例如可以对100个词转换为100个向量,每个向量有128个特征,从而构成$100*128$的矩阵
- 先对输入语料进行分词,为每一个词提供单独的索引,形成词表
- 构建词嵌入矩阵,词索引对应的向量即为其数值化后的向量
- 词嵌入层:
- 词嵌入层的初始化是默认是通过均值为0,标准差为1的正态分布进行的,相当于随机初始化
- 当输入一个词时,会使用随机的向量来表示该词
- 该词参与到后续任务,会与目标结果进行对比产生损失
- 通过反向传播来更新词嵌入层中对于词的表示
text = "读书不是为了雄辩和驳斥,也不是为了轻信和盲从,而是为了思考和权衡。"
#1.分词
words = jieba.lcut(text)
print(words)
#2.构建词表
index_to_word = {} #索引->单词
word_to_index = {} #单词->索引
#去重
words = list(set(words))
print(words)
#生成索引
for idx,word in enumerate(words):
index_to_word[idx] = word
word_to_index[word] = idx
print(index_to_word)
print(word_to_index)
#3.构建词嵌入层
"""
nn.Embedding()
:param num_embeddings:表示词的个数,即词嵌入矩阵的高度
:param embedding_dim:表示每个词的特征数,一般来说会给一个比较大的值
:return:返回Embedding对象
"""
embed = nn.Embedding(num_embeddings=len(index_to_word),embedding_dim=5)
#4.将文本转为词向量表示
for idx in range(len(index_to_word)):
print(index_to_word[idx])#查看第一个词
print(embed(torch.tensor(idx)))#查看第一个词对应的向量
(三)循环网络层
1.RNN网络原理
graph LR
id( )--"h0"-->id0("神经元")--"h1"-->id1(神经元)--"h2"-->id2(神经元)
- 注:以上的神经元表示的是同一个神经元
- 每一个神经元的工作过程:
- 初始化隐藏层h0,一般为全0向量
- 输入数据每次输入一个词进入神经元,神经元处理该词之后输出h1
- h1和下一个输入再次进入神经元输出h2
- 每一个h都包含了其之间所有词的计算语义
- 以此类推,直到某一步神经元输出
- RNN中可以有多个神经元,神经元的数量会影响输出的数据维度
2.计算公式
$$
ht=tanh((w{input}xt+b{input})+(w{hide}h{t-1}+b_{hide}))
$$
- tanh:激活函数
- $w{input},w{hide}$:分别为输入数据$xt$和上一次的隐藏数据$h{t-1}$对应的权重
- $b{input},b{hide}$:分别为输入数据$xt$和上一次的隐藏数据$h{t-1}$对应的偏置
3.RNN的使用
- RNN的输入要求:
- 输入数据维度为(seq_len,batch_size,input_size)
- seq_len:表示序列的长度,也就是词的数量,数量是根据分词的
- batch_size:表示一批输入几个数据(句子)
- input_size:表示输入数据的维度
- 隐藏层维度为(num_layers,batch_size,hidden_size)
- num_layers:循环层的层数,也就是有几层神经元