1. Pytorch环境
Pytorch的官方教程很详细,https://pytorch.org/,直接按照自己的环境安装对应的环境即可。
安装GPU版本的需要安装CUDA,https://developer.nvidia.cn/cuda-toolkit-archive这里可以下载以前的版本,不要装最新的,因为pytorch可能还没有对应的最新的版本。
2. tensor
2.1 Pytorch的数据类型
常用的是FloatTensor、IntTensor、ByteTensor,注意CPU上的数据类型和GPU上的数据类型是不一样的。
在程序中检查数据的类型时一般用isinstance(a, torch.FloatTensor)
。
1.dim=0(标量)
Dimension=0/rank=0,维度为0,也就是一个标量,一般loss的值是一个标量tensor
a = torch.tensor(1.)
a.dim() # 0
a.shape # torch.Size([])
2.dim=1(张量)
Dimension=1/rank=1,一维的向量,一般神经元的偏执为一个dim=1,size=1的tensor,例如bias = tensor([1])
b = torch.tensor([1.1]) # tensor([1.100])
b = torch.tensor([1.1, 2.2]) # tensor(1.100, 2.200)
c = torch.FloatTensor(1) # tensor([3.2239e-25])随机值
c = torch.FloatTensor(2) # tensor([x, x])随机值
d = torch.ones(2)
d.shape # torch.Size([2])
一般dim指的是维度,例如一个矩阵的dim就是2,而size就是shape,例如[2,2]。
3.dim=2
e = torch.randn(2,3)
e.shape # torch.Size([2,3])
e.shape(1) # 2
e.shape(2) # 3
e.shape[1] # 3
e.shape[0] # 2
4.dim=3
一般用来表示RNN input,例如每次送入5句话,即batch=5,每句话有10个单词,每个单词用100size的向量表示,那size = [10,5,100](一般习惯将batch放在第二维)。
f = torch.rand(1,2,3)
f.shape # torch.Size([1,2,3])
5.dim=4
四维适合卷积神经网络,图片的存储,[b,c,h,w],即batch,channel,height,width
g = torch.rand(2,3,28,28)
g.numel() # 4704,整个维度的元素的个数,即2*3*28*28
2.2 创建Tensor
一个tensor通常包含一下几个常用属性:
-
dtype,常见的有:
Data type dtype CPU tensor GPU tensor 32-bit floating point torch.float32
ortorch.float
torch.FloatTensor
torch.cuda.FloatTensor
64-bit floating point torch.float64
ortorch.double
torch.DoubleTensor
torch.cuda.DoubleTensor
32-bit integer (signed) torch.int32
ortorch.int
torch.IntTensor
torch.cuda.IntTensor
64-bit integer (signed) torch.int64
ortorch.long
torch.LongTensor
torch.cuda.LongTensor
通过
torch.ser_default_tensor_type(torch.FloatTensor)
修改默认的类型。CPU与GPU类型的相互转换:
>>> torch.zeros([2, 4], dtype=torch.int32) tensor([[ 0, 0, 0, 0], [ 0, 0, 0, 0]], dtype=torch.int32) >>> cuda0 = torch.device('cuda:0') >>> torch.ones([2, 4], dtype=torch.float64, device=cuda0) tensor([[ 1.0000, 1.0000, 1.0000, 1.0000], [ 1.0000, 1.0000, 1.0000, 1.0000]], dtype=torch.float64, device='cuda:0') # 从cpu –> gpu,使用data.cuda()即可。 # 若从gpu –> cpu,则使用data.cpu()。
-
requires_grad=Flase,可以先理解成在自动求梯度的过程中,对于该属性为False的tensor不会进行求解梯度。
-
.shape属性,返回数据的尺寸
如何创建一个tensor,主要有一下几种方法:
-
从其它类型转换
torch.from_numpy(np.array(2, 3.3)) # 从numpy转换 -> tensor([2.000, 3.300], dtype=torch.float64) torch.tensor([2., 3.2]) # 从list转换,类型默认是torch.FloatTensor,可以通过torch.ser_default_tensor_type(torch.FloatTensor)修改默认类型 torch.tensor([2., 3.2], dtype=torch.float64) # 或者创建时指定
-
未初始化Tensor
生成未初始化的数据,数据分布十分不规律,后续一定要覆盖掉。例如Torch.FloatTensor(d1,d2,d3)。
注意
torch.Tensor()/torch.FloatTensor()
接收的参数是shape维度,也可以接收现有的数据但必须是list,而torch.tensor()
只接收现有的数据,建议用torch.tensor()
用现有的数据创建,用torch.Tensor()
接收shape创建。 -
随机初始化Tensor
a = torch.rand(3,3) # 3x3矩阵,每个都在[0,1]之间 torch.rand_like(a) # 会将参数a的shape送给rand函数 torch.randint(1,10,[3,3]) #min, max, shape torch.empty(7, 19) # 创建7x19的矩阵 torch.randn(3, 3) # 3x3的矩阵,每个数据都符合N(0,1)的正态分布 torch.normal() # ? torch.full([2,3],0) # 2x3的元素全部为0
2.3 tensor常用方法
torch.reshape(input, (1, 2, 3, 4)) # 转换为四维
3. 梯度
导数和偏微分只能是描述的函数在自变量方向上的变化的情况,是一个标量;而梯度定义为一个向量,向量的每个元素是对不同的自变量的偏微分:
一般我们需要用梯度函数的极值点,通常是是极小值,用到的公式就是:
示例:
例如求一个函数的极小值,即目标为,
比局部最小值更可怕的问题是鞍点的情况,比如一个二维的函数,很可能出现一种情况,就是某个点是其中一个维度的极小值,但却是另一个维度的极大值。那么对于多维的函数来说这种情况是很常见的 。
3.1 影响优化器的性能的因素
-
初始化状态
-
学习率
-
逃离局部最小值的方法
给出一个惯性,也就是当找到极小值点的时候根据惯性继续向前,有可能会逃离出极小值点
4. 激活函数
为了将线性函数转换为非线性函数和方便求导,通常要在神经元的末尾加上激活函数。
4.1 sigmoid
sigmoid会将值压缩到(0,1)区间,同时导数非常容易求解:
torch.sigmoid()
4.2 tanh
tanh是由sigmoid变换而来的,导数:
torch.tanh()
4.3 ReLU
ReLU是非常适合现在深度学习的,导数计算起来非常简单,当x<0时,梯度=0,当x>0时,梯度=1,不会出现放大和缩小的情况,很难出现梯度弥散和梯度保障的情况
torch.relu()
优先使用ReLU函数。
import torch
import torch.nn.functional as F
from torch.autograd import Variable
x = torch.linspace(-5, 5, 200)
x = Variable(x)
x_np = x.data.numpy()
y_relu = F.relu(x).data.numpy()
y_sigmoid = F.sigmoid(x).data.numpy()
y_tanh = F.tanh(x).data.numpy()
y_softplus = F.softplus(x).data.numpy()
import matplotlib.pyplot as plt # python 的可视化模块, 我有教程 (https://mofanpy.com/tutorials/data-manipulation/plt/)
plt.figure(1, figsize=(8, 6))
plt.subplot(221)
plt.plot(x_np, y_relu, c='red', label='relu')
plt.ylim((-1, 5))
plt.legend(loc='best')
plt.subplot(222)
plt.plot(x_np, y_sigmoid, c='red', label='sigmoid')
plt.ylim((-0.2, 1.2))
plt.legend(loc='best')
plt.subplot(223)
plt.plot(x_np, y_tanh, c='red', label='tanh')
plt.ylim((-1.2, 1.2))
plt.legend(loc='best')
plt.subplot(224)
plt.plot(x_np, y_softplus, c='red', label='softplus')
plt.ylim((-0.2, 6))
plt.legend(loc='best')
plt.show()
5. 损失函数Loss
损失函数分为两大类:
-
Mean Squared Error均方差
-
Cross Entropy Loss
Entropy熵,熵越高,不确定性uncertainty越小,也就是越稳定的状态。
交叉熵就是两个不同的分布,定义为:
定义为散度,当两个分布重合的部分越低,也就是越不相似,其值越高,反之越低。
在分类的问题当中,通常采用one-hot编码,这种编码的Entropy为0,带入到Cross Entropy中:
恰好是两个分布的散度,我们可以将其作为优化目标,也就是模型的预测分布和真实分布之间的散度,最小化该散度,也就是最小化两个分布之间的差距。
可以求二分类和多分类问题的损失值,搭配softmax激活函数效果会更好。
- binary
- multi-class
- softmax
5.1 Loss梯度
由于模型的目标是将loss降到最低,因此需要找到loss的最小值,那么通过对loss求导的方式寻找。
Loss的梯度求解有两种方法
-
torch.autograd.grad(loss, [w1, w2, ...])
用最后的loss对参数[w1, w1, ...]进行求偏导,返回一个导数值的列表
-
loss.backward()
这种方法是求解梯度并进行反向传播,也就是说该方法完成了梯度的求解过程以及反向传播的过程
示例:
# MSE均方误差
x = torch.ones(1)
w = torch.full([1],2) # y = wx
mse = F.mse_loss(torch.ones(1), x*w)
# 计算loss的梯度
torch.autograd.grad(mse, [w]) # 第一个参数是loss,第二个参数是所有网络参数
# 或者loss对象可以直接调用backward()
mse.backward()
"""
CEL
"""
a = torch.rand(3)
p = F.softmax(a, dim=0)
p.backward()
6. 优化算法
x = torch.tensor([0, 0], requires_grad=True)
optimizer = torch.optim.Adam([x], lr=1e-3) # 创建优化器对象,传入的是待优化的参数和学习率
for step in range(20000):
pred = f(x)
optimizer.zero_grad()
pred.backward() # 就会生成相应的梯度信息
optimizer.step() # 每次调用一次step函数就会更新一次x' = x - lr*dx, y' = y - lr*dy...
7. Dataset/DataLoader
7.1 Dataset
7.2 DataLoader
torch.utils.data.DataLoader()
# 常用参数
- dataset
- batch_size
- shuffle 顺序打乱,默认为False
- num_workers 加载数据的线程数量,默认为0,主线程加载
dataloader = DataLoader(dataset, 64, shuffle=True)
for batch_data, targets in dataloader:
...
8. nn.Linear()
8. nn.Module模块
当自己需要实现一个网络模型的时候需要继承这个类,在内部定义的每一个层次也是nn.Module类,所以它们之间有嵌套关系。
- Every Layer is nn.Module
- nn.Module nested in nn.Module
继承nn.Module类后,需要在__init__(self)
函数中定义自己网络的所有层结构,然后在forward(self)
函数中串联每个层。
8.1 容器container
如果层次非常多的话,可以将所有层次都定义在nn.Sequential()
中:
self.net = nn.Sequential(
nn.Conv2d(1, 32, 5, 1, 1),
nn.MaxPool(2, 2),
nn.ReLU(True),
nn.BatchNorm2d(32),
nn.Conv2d(32, 64, 3, 1, 1),
nn.ReLU(True),
nn.BatchNorm2d(64),
...
# 这其中可以用自己的定义的其它网络
)
# 注意Squential接受的参数都是class
这种方式就不需要为每个层定义一个变量,也不需要为每个其进行forward串联。
8.2 参数管理
nn.Module会对网络中的参数进行管理,net.parameters()
会返回所有网络中的参数,通常使用这种方法将所有参数传到优化器中去。
示例
class BasicNet(nn.Module):
def __init__(self):
super(BasicNet, self).__init__()
self.net = nn.Linear(4, 3)
def forward(self, x):
return self.net(x)
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.net = nn.Sequential(
BasicNet(),
nn.ReLU(),
nn.Linear(3,2)
)
def forward(self, x):
return self.net(x)
device = torch.device('cuda')
net = Net()
net.to(device)
save/load
net.load_state_dict(torch.load('ckpt.mdl'))
# train...
torch.save(net.stat_dict(), 'ckpt.mdl')
train/test
对于dropout等有的层结构来说,train和test的状态是不太一样的,nn.Module提供了状态切换的方法。
net.train() # train
net.eval() # test
9. PyTorch神经网络结构
神经网络可以通过torch.nn包来构建。神经网络是基于自动梯度来定义一些模型,一个nn.Module包括层和一个forwoard(input)方法,返回为output。
例如一个前馈神经网络,它接受输入,让输入一个接着一个的通过一些层,最后给出输出。
一个典型的神经网络训练过程包括:
-
定义一个包含可训练参数的神经网络
-
迭代整个输入
-
通过神经网络处理输入
-
计算损失
-
反向传播梯度到神经网络的参数
-
更新网络参数,典型的用一个简单的更新方法:
9.1 定义神经网络
定义神经网络的结构:
import torch
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# 1 input image channel, 6 output channels, 5x5 square convolution
# kernel
self.conv1 = nn.Conv2d(1, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
# an affine operation: y = Wx + b
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
# Max pooling over a (2, 2) window
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
# If the size is a square you can only specify a single number
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
x = x.view(-1, self.num_flat_features(x))
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
def num_flat_features(self, x):
size = x.size()[1:] # all dimensions except the batch dimension
num_features = 1
for s in size:
num_features *= s
return num_features
net = Net()
print(net)
'''
Net(
(conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=400, out_features=120, bias=True)
(fc2): Linear(in_features=120, out_features=84, bias=True)
(fc3): Linear(in_features=84, out_features=10, bias=True)
)
'''
这是一个前馈网络,然后反向传播函数默认被autograd(自动梯度)定义了。
一个模型的可训练的参数可以通过调用net.parameters()
返回:
params = list(net.parameters())
print(len(params))
print(params[0].size())
'''
10
torch.Size([6,1,5,5])
'''
9.2 处理输入
接着将输入放到网络中,将得到的计算结果调用反向传播:
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)
'''
tensor([[-0.0233, 0.0159, -0.0249, 0.1413, 0.0663, 0.0297, -0.0940, -0.0135,
0.1003, -0.0559]], grad_fn=<AddmmBackward>)
'''
9.3 调用反向传播
然后把所有参数梯度缓存器置零,用随机的梯度来反向传播
net.zero_grad()
out.backward(torch.randn(1, 10))
至此,完成了定义一个神经网络,处理出入以及调用反向传播。
下面还需要处理:
- 计算损失值
- 更新网络中的权重
9.4 计算损失值
损失函数需要一对输入,即模型的输出和真实目标,然后返回一个值表示估计值与真实目标的距离。
在nn包中定义了多种损失函数,比如一个简单的损失函数是nn.MSELoss
,这是个计算均方误差的损失函数。
例如:
output = net(input)
target = torch.randn(10)
target = target.view(1, -1)
criterion = nn.MSELoss()
loss = criterion(output, target)
print(loss)
'''
tensor(1.3389, grad_fn=<MseLossBackWard>)
'''
9.5 将损失添加到反向传播
现在需要跟随损失到反向传播路径,可以使用它的.grad_fn
属性,其计算图为:
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d -> view -> linear -> relu -> linear -> relu -> linear -> MSELoss -> loss
所以,当调用loss.backward()
,整个图都会微分,而且所有的在图中的requires_grad=True
的张量将会让他们的grad张量累计梯度。
为了实现反向传播损失,所有需要做的事仅仅是使用loss.backward()
。需要清空现存的梯度,不然将会和现存的梯度累计到一起。
net.zero_grad() # 将现存梯度清零
loss.backward() # 反向传播
9.6 更新参数
最简单的更新规则就是随机梯度下降。
weight = weight - learning_rate * gradient
torch.optim
包中实现了多种更新规则函数,例如SGD,Nesterov-SGD,Adam,RMSProp等。
optimizer = optim.SGD(net.parameters(), lr=0.01) # 创建优化器
optimizer = zero_grad() # 清零梯度系数
output = net(input) # 处理输入
loss = criterion(output, target) # 计算损失
loss.backward() # 反向传播
optimizer.step() # 更新参数
10. CNN
11. RNN
11.1 模型的参数维度
单层RNN
对于单层的RNN,也就是每个时刻的输入和上一时刻的隐层状态经过一个线性层处理得到下一个时刻的隐层状态,这种单层的RNN的参数如下图所示:
输入的维度为二维,batch和词向量维度feature len,与一个权重矩阵做矩阵运算,权重矩阵的维度为二维,hidden len表示隐层向量的维度,矩阵运算完的维度为。
另一个输入是上一时刻的隐层状态,它的维度是,与权重矩阵做矩阵运算,矩阵的维度是,运算结果的维度是。
将两个结果进行相加得到要输入到下一时刻的隐层状态。
多层RNN
对于多层的RNN,包含了多个线性层,如下图所示是一个二层的RNN:
每一层线性层将上一层的输出和上一时刻的隐层状态作为输入,以二层为例,第一层以嵌入向量和上一时刻隐层状态作为输入,第二层以第一层的输出和上一时刻的隐层状态作为输入。其维度为:
一个句子有多少个单词,代表RNN进行多少次时间节点的运算
torch.nn.RNN
而在pytorch中使用RNN的API如下:
torch.nn.RNN(input_size, hidden_size, num_layers)
# - input_size 表示嵌入向量的维度,例如100
# - hidden_size 表示隐层状态向量的维度,例如20
# - num_layers 表示隐层层数/线性层层数
\
通过此API创建一个RNN网络模型,然后模型的输入和刚才图中的不一样,是每个时间状态下的输入,而在pytorch中RNN模型的输入是不需要单步处理每个时间状态的输入的,只需要将整个输入数据feed到模型中,模型会自动处理每个时间状态的输入。
# 模型的输入x_input, h_0; 输出h_t,out
# x_input:[seq len, batch, feature len]
# - seq len 表示每个句子的单词数
# - batch 表示多少个句子
# - feature len 表示每个单词的嵌入维度
# h_0:[layer, batch, hidden len] h_0可以不给
# - layer RNN线性层数
# - batch
# - hidden len 表示隐层向量维度
# h_t的维度和h_0一样,代表最后一个时间节点的隐层状态
# out:[seq len, batch, hidden len]输出每一个时间节点的最后一层的隐层状态
使用示例:
rnn = torch.nn.RNN(input_size=100, hidden_size=20, num_layers=2, batch_first=False)
x_input = torch.randn(5, 3, 100) # seq len, batch, feature len
h_0 = torch.randn(2, 3, 20) # layer len, batch, hidden len
h_n, out = rnn(x_input, h_0)
print(h_n.shape) # torch.Size([5, 3, 20])
print(out.shape) # torch.Size([2, 3, 20])
LSTM
torch.nn.LSTM(input_size, hidden_size, num_layers, bidirectional=False)
# - input_size: The number of expected features in the input x
# - hidden_size: The number of features in the hidden state h
# - bidirectional: 双向LSTM,默认为False
# 模型的输入
# x_input [seq len, batch, H_in] H_in=input_size
# h_0 [D*num_layers, batch, H_out] 双向D=2,单向D=1;H_out=hidden_size
# c_0 [D*num_layers, batch, H_cell] H_cell=hidden_size
# 模型的输出
# output [seq len, batch, D*H_out]
# h_n [D*num_layers, batch, H_out]
# c_n [D*num_layers, batch, H_cell]
# 示例
rnn = nn.LSTM(input_size=10, hidden_size=20, num_layers=2)
x_input = torch.randn(5, 3, 10) # seq len, batch, input_size
h_0 = torch.randn(2, 3, 20) # 1*num_layers, batch, hidden_size
c_0 = torch.randn(2, 3, 20) # 1*num_layers, batch, hidden_size
output, (h_n, c_n) = rnn(x_input, h_0, c_0)
GRU
torch.nn.GRU(input_size, hidden_size, num_layers, bidirectional=False)
# - input_size: The number of expected features in the input x
# - hidden_size: The number of features in the hidden state h
# - bidirectional: 双向GRU,默认为False
# 模型的输入
# x_input [seq len, batch, H_in] H_in=input_size
# h_0 [D*num_layers, batch, H_out] 双向D=2,单向D=1;H_out=hidden_size
# 模型的输出
# output [seq len, batch, D*H_out]
# h_n [D*num_layers, batch, H_out]
# 示例
rnn = nn.GRU(input_size=10, hidden_size=20, num_layers=2)
x_input = torch.randn(5, 3, 10) # seq len, batch, input_size
h_0 = torch.randn(2, 3, 20) # 1*num_layers, batch, hidden_size
output, h_n = rnn(x_input, h_0)
12. 词嵌入
12.1 torch.nn.Embedding
在torch中使用嵌入模型非常简单,是需要使用torch.nn.Embedding(num_embeddings, embedding_dim)
- num_embeddings [int]– size of the dictionary of embeddings
- embedding_dim [int] – the size of each embedding vector
- padding_idx [int] - 索引为padding_idx的词向量全部为0,把padding_idx设置为填充的值,如padding_idx=3,训练过程中索引为3的将始终设置为0,不进行参数更新,也就是说,为了便于模型训练需统一句子长度,对那些句子长度不够的句子进行填充,比如用值3进行填充,当用nn.Embedding()进行词向量嵌入时,对应的索引为3的向量将变为全为0的向量。这样就减少了填充值对模型训练的影响。例如:句子[2, 4, 5]和[1, 2, 7, 8, 9],两个句子不一样长,那么在输入到模型的时候就可以转换为[2, 4, 5, 3, 3], [1, 2, 7, 8, 9]。
Embedding
创建一个大的矩阵,矩阵的每一行代表一个单词,默认是随机初始化的。
# 整个字典一共有5个单词,每个单词向量的维度是20
embeds = torch.nn.Embedding(num_embeddings=5, embedding_dim=20)
# 读取一个词向量,output = embeds(input)
# - input: IntTensor或LongTensor的任意shape
# - output: (*, H),*代表input的shape,H=embeding_dim
input = torch.IntTensor([[0, 1, 2], [3, 4, 1]]) # torch.Size([2, 3])
output = embeds(input)
print(output.shape) # torch.Size([2, 3, 20])
一般在NLP中创建Embedding字典放在网络模型之前,作为预处理,这些embedding默认是随机初始化的,但在训练的过程中会进行训练。
除此之外,我们也可以导入已经训练好的词向量,但是需要设置训练过程中不更新:
embeds.weight = torch.nn.Parameter(emb)
# 固定embedding
embeds.weight.requires_grad = False
13. Attention
14. 其他常见操作
14.1 扁平化操作
class Flatten(nn.Module):
def __init__(self):
super(Flatten, self).__init__()
def forward(self, input):
return input.view(input.size(0), -1)
14.2 torch.manual_seed()
在一个实验中,如果需要使用到随机数,由于每次实验都需要生成数据,设置随机种子是为了确保每次生成固定的随机数(生成的随机数是一样的),这就使得每次实验结果显示一致了,有利于实验的比较和改进。使得每次运行该 python文件时生成的随机数相同。
用比较学术的话来讲,torch.manual_seed()为 CPU/GPU 中设置种子,生成随机数。
示例:
t.manual_seed(1000)
def get_fake_data(batch_size=8):
'''产生随机数据:y= x*2+3 , 加上一些噪声'''
x = t.rand(batch_size,1)*20
y = x * 2 +( 1 + t.randn(batch_size,1)) * 3
return x,y
x , y = get_fake_data()
plt.scatter(x.squeeze().numpy(),y.squeeze().numpy())
14.3 梯度弥散和梯度爆炸
梯度弥散
当网络的层数非常深时,会出现梯度非常小的问题,可能会接近于0,这样网络可能长时间得不到更新。
梯度爆炸
如果梯度在某个过程中计算的值很大,再经历一系列的乘方会出现梯度非常大的情况,因此需要再计算的过程中检查梯度是否大于自定义的阈值,将其限制在阈值范围内,这个操作称为Gradient Clipping:
loss = loss_fun(output, y)
model.zero_grad()
loss.backward()
for p in model.parameters():
torch.nn.utils.clip_grad_norm_(p, 10) # 限制在10的范围内
optimizer.step()