PyTorch学习笔记

#########################################################

有关PyTorch一些学习笔记,目前笔记并不全面,只是针对性记录一些对应地铁预测中运用的的原理,函数,方法(有些没有使用的函数也会纪录,但并不详细)。

###########################################################

一、 张量模块

1. 张量(Tensor):PyTorch最基本的操作对象。张量是基于标量、向量和矩阵概念的延伸。可以把标量视为0维张量,向量视为1维张量,矩阵视为2维张量。

2. 创建张量的方法:

    • torch.Tensor()
#从python数组构建
a = [[1, 2, 3],[4 ,5 ,6]]
x = torch.Tensor(a)
print(x)
#tensor([[1., 2., 3.],
# [4., 5., 6.]])

3. 创建各种形式的随机数的方法

1) torch.rand()

#返回一个张量,服从区间为[0,1)的均匀分布

tensor1 = torch.rand(4)
tensor2 = torch.rand(2, 3)
print(tensor1, tensor2)
# tensor([0.7638, 0.3919, 0.9474, 0.6846])
# tensor([[0.3425, 0.0689, 0.6304],
# [0.5676, 0.8049, 0.3459]])

2) torch.randn()

#返回张量服从标准正态分布(均值为0,方差为1)

tensor1 = torch.randn(5)
tensor2 = torch.randn(2, 4)
print(tensor1, tensor2)
# tensor([ 0.4315, -0.3812, 0.9554, -0.8051, -0.9421])
# tensor([[-0.6991, 0.0359, 1.2298, -0.1711],
# [ 1.0056, 0.5772, 1.4460, -0.5936]])

3) torch.normal()

#torch.normal()的第一种用法为torch.normal(means, std, out=None)返回一个张量

tensor = torch.normal(mean=torch.arange(1., 11.), std= torch.arange(1, 0, -0.1))
print(tensor)
# tensor([0.0605, 2.5965, 3.3046, 4.2056, 5.0117, 6.7848, 6.3024, 7.9845, 9.4306, 9.7881])

#torch.normal()的第二种用法为torch.normal(mean, std, size*, out=None),共享均值和方程

tensor = torch.normal(2, 3, size=(1, 4))
print(tensor)
# tensor([[ 4.7555, -2.5026, -1.6333, -0.9256]])

4) torch.linspace()

tensor = torch.linspace(1, 10, steps=5)
print(tensor)
# tensor([ 1.0000, 3.2500, 5.5000, 7.7500, 10.0000])

5) torch.manual_seed()

torch.manual_seed(1)
temp1 = torch.rand(5)
print(temp1) # tensor([0.7576, 0.2793, 0.4031, 0.7347, 0.0293])
torch.manual_seed(1)
temp2 = torch.rand(5)
print(temp2) # tensor([0.7576, 0.2793, 0.4031, 0.7347, 0.0293])
temp3 = torch.rand(5)
print(temp3) # tensor([0.7999, 0.3971, 0.7544, 0.5695, 0.4388])

6) torch.ones()、torch.zeros()、torch.eye()

#ones全为1,zeros全为0,eye是对角线为1,其余为0

tensor1 = torch.zeros(2, 3)
tensor2 = torch.ones(2, 3)
tensor3 = torch.eye(3)
print(tensor1, tensor2, tensor3)
# tensor([[0., 0., 0.],
# [0., 0., 0.]])
# tensor([[1., 1., 1.],
# [1., 1., 1.]])
# tensor([[1., 0., 0.],
# [0., 1., 0.],
# [0., 0., 1.]])

4. 张量基本操作

1) 改变张量的形状

x = torch.rand(3, 2)
print(x.shape) # torch.Size([3, 2])
y = x.view(2, 3)
print(y.shape) # torch.Size([2,3])

2) 增加和删除维度

# 增加维度
a = torch.rand(3, 4)
b = torch.unsqueeze(a, 0)
c = a.unsqueeze(0)
print(b.shape) # torch.Size([1, 3, 4])
print(c.shape) # torch.Size([1, 3, 4])
# 删除维度
a = torch.rand(1, 1, 3, 4)
b = torch.squeeze(a)
c = a.squeeze(1)
print(b.shape) # torch.Size([3, 4])
print(c.shape) # torch.Size([1, 3, 4])

3) 交换维度

a = torch.rand(1, 3, 28, 32) # torch.Size([1, 3, 28, 32]
# 第一种方法
b = a.transpose(1, 3).transpose(1, 2) # torch.Size([1, 28, 32, 3])
print(b.shape)
# 第二种方法
c = a.permute(0, 2, 3, 1)
print(c.shape) # torch.Size([1, 28, 32, 3])

4) 拼接和分割

# torch.cat()拼接方法的代码如下:
a = torch.rand(1, 2)
b = torch.rand(1, 2)
c = torch.rand(1, 2)
output1 = torch.cat([a, b, c], dim=0) # dim=0为按列拼接
print(output1.shape) # torch.Size([3, 2])
output2 = torch.cat([a, b, c], dim=1) # dim=1为按行拼接
print(output2.shape) # torch.Size([1, 6])
# torch.split()分割方法的代码如下:
a = torch.rand(3, 4)
output1 = torch.split(a, 2)
print(output1)
# (tensor([[0.2540, 0.1353, 0.4933, 0.0357],
# [0.3998, 0.7569, 0.4552, 0.5319]]), tensor([[0.3846, 0.5187, 0.4397, 0.0126]]))
output2 = torch.split(a, [2, 2], dim=1)
print(output2)
# (tensor([[0.2540, 0.1353],
# [0.3998, 0.7569],
# [0.3846, 0.5187]]), tensor([[0.4933, 0.0357],
# [0.4552, 0.5319],
# [0.4397, 0.0126]]))

5) 堆叠和分解

# torch.stack()堆叠方法的代码如下:
a = torch.rand(1, 2)
b = torch.rand(1, 2)
c = torch.rand(1, 2)
output1 = torch.stack([a, b, c], dim=0) # dim=0为按列拼接
print(output1.shape) # torch.Size([3, 1, 2])
output2 = torch.stack([a, b, c], dim=1) # dim=1为按行拼接
print(output2.shape) # torch.Size([1, 3, 2])
# torch.chunk()分解方法的代码如下:
a = torch.rand(3, 4)
output1 = torch.chunk(a, 2, dim=0)
print(output1)
# (tensor([[0.1943, 0.1760, 0.3022, 0.0746],
# [0.5819, 0.7897, 0.2581, 0.0709]]), tensor([[0.2137, 0.5694, 0.1406, 0.0052]]))
output2 = torch.chunk(a, 2, dim=1)
print(output2)
# (tensor([[0.1943, 0.1760],
# [0.5819, 0.7897],
# [0.2137, 0.5694]]), tensor([[0.3022, 0.0746],
# [0.2581, 0.0709],
# [0.1406, 0.0052]]))

6) 索引和切片

x = torch.rand(2, 3, 4)
print(x[1].shape) # torch.Size([3, 4])
y = x[1, 0:2, :]
print(y.shape) # torch.Size([2, 4])
z = x[:, 0, ::2]
print(z.shape) # torch.Size([2, 2])

7) 一些基本数学运算

# 所有元素的和
a = torch.rand(4, 3)
b = torch.sum(a)
print(b) # tensor(6.4069)
# 所有元素的乘积
a = torch.rand(4, 3)
b = torch.prod(a)
print(b) # tensor(2.0311e-05)
# 求所有元素的平均数
a = torch.rand(4, 3)
b = torch.mean(a)
print(b) # tensor(0.4836)
# 求方差
a = torch.rand(4, 3)
b = torch.var(a)
print(b) # tensor(0.0740)
# 求最大值
a = torch.rand(4, 3)
b = torch.max(a)
print(b) # tensor(0.8765)
# 求最小值
a = torch.rand(4,3)
b = torch.min(a)
print(b) # tensor(0.0397)

8) 向量运算和矩阵运算

包括点乘、叉乘、内积、外积

# 向量的点乘,a和b必须为一维
a = torch.Tensor([1, 2, 3])
b = torch.Tensor([1, 1, 1])
output = torch.dot(a, b)
print(output) # 等价于 1*1+2*1+3*1,tensor(6.)
# 向量的叉乘
a = torch.Tensor([1, 2, 3])
b = torch.Tensor([1, 1, 1])
output = torch.multiply(a, b)
print(output) # tensor([1., 2., 3.])
# 矩阵的内积
a = torch.Tensor([1, 2, 3])
b = torch.Tensor([1, 1, 1])
output = torch.inner(a, b)
print(output) # tensor(6.)
# 矩阵的外积:矩阵乘法
a = torch.Tensor([[1, 2, 3], [4, 5, 6]])
b = torch.Tensor([[1, 1], [2, 2], [3, 3]])
output = torch.matmul(a, b)
print(output)
# tensor([[14., 14.],
# [32., 32.]])

5. numpy转张量

a = np.ones([1, 2])
b = torch.from_numpy(a) # 进行转换
print(a, b) # [[1. 1.]] tensor([[1., 1.]], dtype=torch.float64)
a += 2
print(a, b) # [[3. 3.]] tensor([[3., 3.]], dtype=torch.float64)
b += 2 # 在a改变后,b也已经改变
print(a, b) # [[5. 5.]] tensor([[5., 5.]], dtype=torch.float64)

6. Cuda张量与CPU张量

  在深度学习过程中,GPU能起到加速作用。但是pytorch中的张量默认存放在CPU设备中,如果GPU可用,可以将张量转移到GPU中。两种方法如下:

x = torch.rand(2, 4)
print(x.device) # cpu
# 第一种方法
x = x.cuda()
print(x.device) # cuda:0
# 第二种方法
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
if torch.cuda.is_available():
x = x.to(device)
print(x.device) #cuda:0
# 转化为cpu
x = x.cpu()
print(x.device) # cpu

二、 数据模块:Dataset和Dataloader

Dataset和DataLoader都是用来帮助我们加载数据集的两个重要工具类。Dataset用来构造支持索引的数据集。在训练时需要在全部样本中拿出小批量数据参与每次的训练,因此我们需要使用DataLoader,即DataLoader是用来在Dataset里取出一组数据(mini-batch)供训练时快速使用的。 

1. Dataset简介及用法:

Dataset作为一个抽象类,需要定义其子类来实例化。所以我们需要自己定义其子类或者使用已经定义好的子类。

#定义一个MyDataset类继承Dataset抽象类,并且改写其中的三个方法,

import torch
from torch.utils.data import Dataset
class MyDataset(Dataset):
def __init__(self):
pass
def __getitem__(self, index):
pass
def __len__(self):
pass

ps:

下面是一个时间序列的示例,输入3个实践部,输出1个时间步,batch_size = 5,代码如下。

import torch
from torch.utils.data import Dataset
class GetTrainTestData(Dataset):
def __init__(self, input_len, output_len, train_rate, is_train=True):
super().__init__()
# 使用sin函数返回10000个时间序列,如果不自己构造数据,就使用numpy,pandas等读取自己的数据为x即可。
# 以下数据组织这块既可以放在init方法里,也可以放在getitem方法里
self.x = torch.sin(torch.arange(0, 1000, 0.1))
self.sample_num = len(self.x)
self.input_len = input_len
self.output_len = output_len
self.train_rate = train_rate
self.src, self.trg = [], []
if is_train:
for i in range(int(self.sample_num*train_rate)-self.input_len-self.output_len):
self.src.append(self.x[i:(i+input_len)])
self.trg.append(self.x[(i+input_len):(i+input_len+output_len)])
else:
for i in range(int(self.sample_num*train_rate), self.sample_num-self.input_len-self.output_len):
self.src.append(self.x[i:(i+input_len)])
self.trg.append(self.x[(i+input_len):(i+input_len+output_len)])
print(len(self.src), len(self.trg))
def __getitem__(self, index):
return self.src[index], self.trg[index]
def __len__(self):
return len(self.src) # 或者return len(self.trg), src和trg长度一样

实例化我们定义好的Dataset子类GetTrainTestData

data_train = GetTrainTestData(input_len=3, output_len=1, train_rate=0.8, is_train=True)
data_test = GetTrainTestData(input_len=3, output_len=1, train_rate=0.8, is_train=False)
#7988 7988
#1994 1994

2. Dataloader简介及用法

Dataset和DataLoader是一起使用的,Dataset来加载数据,DataLoader来迭代加载进来的数据,DataLoader本质上就是用来将已经加载好的数据以模型能够接收的方式输入到即将训练的模型中去。

(1)深度学习设计的参数:

  • Data_size: 所有1数据的样本数量。  
  • Batch_size: 每个Batch加载多少个样本。
  • Batch: 每一批放进module训练的样本叫一个Batch。
  • Epoch:模型把所有样本训练完毕一次叫做一个Epoch。
  • Iteration:所有数据共分成了几个Batch,即需要训练几次才能遍历所有样本数据。
  • Shuffle:在抽取Batch之前是否将样本全部打乱顺序。

继续上个Dataset的例子,将数据输入到DataLoader中:

import torch
from torch import nn
from torch.utils.data import Dataset, DataLoader
class GetTrainTestData(Dataset):
def __init__(self, input_len, output_len, train_rate, is_train=True):
super().__init__()
# 使用sin函数返回10000个时间序列,如果不自己构造数据,就使用numpy,pandas等读取自己的数据为x即可。
# 以下数据组织这块既可以放在init方法里,也可以放在getitem方法里
self.x = torch.sin(torch.arange(1, 1000, 0.1))
self.sample_num = len(self.x)
self.input_len = input_len
self.output_len = output_len
self.train_rate = train_rate
self.src, self.trg = [], []
if is_train:
for i in range(int(self.sample_num*train_rate)-self.input_len-self.output_len):
self.src.append(self.x[i:(i+input_len)])
self.trg.append(self.x[(i+input_len):(i+input_len+output_len)])
else:
for i in range(int(self.sample_num*train_rate), self.sample_num-self.input_len-self.output_len):
self.src.append(self.x[i:(i+input_len)])
self.trg.append(self.x[(i+input_len):(i+input_len+output_len)])
print(len(self.src), len(self.trg))
def __getitem__(self, index):
return self.src[index], self.trg[index]
def __len__(self):
return len(self.src) # 或者return len(self.trg), src和trg长度一样
data_train = GetTrainTestData(input_len=3, output_len=1, train_rate=0.8, is_train=True)
data_test = GetTrainTestData(input_len=3, output_len=1, train_rate=0.8, is_train=False)
data_loader_train = DataLoader(data_train, batch_size=5, shuffle=False)
data_loader_test = DataLoader(data_test, batch_size=5, shuffle=False)
#7988 7988
#1994 1994

三、网络模块:torch.nn

1. Torch.nn函数简介

(1) nn.Linear()

#设置网络中的全连接层,用法torch.nn.Linear(in_features, out_feature, bias = True)

import torch
from torch import nn
linear = torch.nn.Linear(in_features=64, out_features=1)
input = torch.rand(10, 64)
output = linear(input)
print(output.shape) # torch.Size([10, 1])

 (2) nn.Conv1d()

#一维卷积层,在由多个输入平面组成的输入信号上应用一维卷积,一般应用于文本或时间序列数据

#输入维度为(N, Cin, Lin),输出维度(N, Cout, Lout

conv1 = nn.Conv1d(in_channels=256, out_channels=10, kernel_size=2, stride=1, padding=0)
input = torch.randn(32, 32, 256) # [batch_size, L_in, in_channels]
input = input.permute(0, 2, 1) # 交换维度:[batch_size, embedding_dim, max_len]
out = conv1(input) # [batch_size, out_channels, L_out]
print(out.shape) # torch.Size([32, 10, 31]),31=(32+2*0-1*1-1)/1+1

(3) nn.Conv2d()

#二维卷积一般应用于图像,输入维度为(N, Cin, Lin, Win),

输出维度为(N, Cout, Lout, Wout

import torch
x = torch.randn(3, 1, 5, 4) # [N, in_channels, H_in, W_in]
conv = torch.nn.Conv2d(1, 4, (2, 3)) # [in_channels, out_channels, kernel_size]
output = conv(x)
print(output.shape) # torch.Size([3, 4, 4, 2]), [N, out_channels, H_out, W_out]

(4) nn.BatchNorm1d

(5) nn.BatchNorm2d

(6) nn.RNN

(7) nn.LSTM

#使用nn.LSTM( ) 可以直接构建若干层的LSTM,构造时传入的三个参数依次是[feature_len, hidden_len, num_layers]

#nn.LSTM( ) 的输入为当前Batch所有seq_len个时刻的样本,输入的shape为[seq_len, batch_size, feature_len],输出为在所有时刻最后一层上的输出、隐藏单元和记忆单元。

#所有时刻最后一层上的输出的shape为[seq_len, batch_size, hjidden_len],隐藏层和记忆单元的shape为[num_lauers, batch_size, hidden_len]

import torch
from torch import nn
# 构建4层的LSTM,输入的每个词用10维向量表示,隐藏单元和记忆单元的尺寸是20
lstm = nn.LSTM(input_size=10, hidden_size=20, num_layers=4)
# 输入的x:其中batch_size是3表示有三句话,seq_len=5表示每句话5个单词,feature_len=10表示每个单词表示为长10的向量
x = torch.randn(5, 3, 10)
# 前向计算过程,这里不传入h_0和C_0则会默认初始化
out, (h, c) = lstm(x)
print(out.shape) # torch.Size([5, 3, 20]) 最后一层10个时刻的输出
print(h.shape) # torch.Size([4, 3, 20]) 隐藏单元
print(c.shape) # torch.Size([4, 3, 20]) 记忆单元

2. 借助torch.nn.Module构建深度学习模型的类class

  • call()函数的用法

#使class能够像函数一样被调用

 

class A():
def __call__(self):
print('Python is useful')
a = A()
a() # 输出结果为:Python is useful
  • repr()函数的用法
class Cat:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"{self.__class__.__name__}({self.name}, {self.age})"
my_Cat = Cat("petty", 3)
print(my_Cat) # 输出结果为:Cat(petty, 3)

#举一个借助torch.nn.Module并使用__init__( )构造函数和forward( )函数构建深度学习模型的类的例子

import torch
from torch import nn
class MyNet(nn.Module):
def __init__(self):
super(MyNet, self).__init__() # 使用父类的方法初始化子类
self.linear1 = torch.nn.Linear(96, 1024) # [96,1024]
self.relu1 = torch.nn.ReLU(True)
self.batchnorm1d_1 = torch.nn.BatchNorm1d(1024)
self.linear2 = torch.nn.Linear(1024, 7 * 7 * 128) # [1024,6272]
self.relu2 = torch.nn.ReLU(True)
self.batchnorm1d_2 = torch.nn.BatchNorm1d(7 * 7 * 128)
self.ConvTranspose2d = nn.ConvTranspose2d(128, 64, 4, 2, padding=1)
def forward(self, x):
x = self.linear1(x)
x = self.relu1(x)
x = self.batchnorm1d_1(x)
x = self.linear2(x)
x = self.relu2(x)
x = self.batchnorm1d_2(x)
x = self.ConvTranspose2d(x)
return x
model = MyNet()

#下面使用一下MyNet类

model = MyNet() # 实例化的过程中没有传入参数
input = torch.rand([32, 96]) # 输入的最后一个维度要与nn.Linear(96, 1024)中第一个维度96相同
target = model(input)
print(target.shape) # torch.Size([32, 1, 28, 28])

#上述实例化的过程中是没有参数输入的,也就是在构造类的过程中使用构造函数的第一种形式def__init__(self)。此外,在构造类的过程中很多情况下使用构造函数的第二种形式def__init__(self, 参数1,参数2,···,参数n),在这种情况下,对类实例化的过程中也需要输入相应的参数,举例代码如下:

#构建网络
class MyNet(nn.Module):
def __init__(self,in_dim,n_hidden_1,n_hidden_2,out_dim):
super(MyNet, self).__init__()
self.layer1 = nn.Sequential(nn.Linear(in_dim,n_hidden_1),nn.BatchNorm1d(n_hidden_1))
self.layer2 = nn.Sequential(nn.Linear(n_hidden_1,n_hidden_2),nn.BatchNorm1d(n_hidden_2))
self.layer3 = nn.Sequential(nn.Linear(n_hidden_2,out_dim))
def forward(self,x):
x = torch.relu(self.layer1(x))
x = torch.relu(self.layer2(x))
x = self.layer3(x)
return x
# 实例化网络层
model = MyNet(28 * 28, 300, 100, 10) # 实例化的过程中传入参数
input = torch.rand(10, 28 * 28)
target = model(input)
print(target.shape) # torch.Size([10, 10])

3. 激活函数模块:torch.nn.functional

(1)Sigmoid函数

Sigmoid函数在物理意义上最接近生物神经元,使用matplotlib包来绘制sigmoid函数图像,代码如下

import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-10,10)
y_sigmoid = 1/(1+np.exp(-x))
y_tanh = (np.exp(x)-np.exp(-x))/(np.exp(x)+np.exp(-x))
fig = plt.figure()
# plot sigmoid
ax = fig.add_subplot()
ax.plot(x,y_tanh)
ax.grid()
ax.set_title('Sigmoid')
plt.show()

#在调用激活函数的时候,不需要手动实现公式,直接调用torch.nn函数即可,里面已经封装好了常用的所有激活函数

input_x = torch.randn(4)
print(input_x)
output = torch.sigmoid(input_x)
print(output)
#tensor([ 1.1902, 1.2787, -0.6267, 0.3341])
#tensor([0.7668, 0.7822, 0.3483, 0.5828])

 

(2)Tanh函数

import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-10,10)
y_tanh = (np.exp(x)-np.exp(-x))/(np.exp(x)+np.exp(-x))
fig = plt.figure()
ax = fig.add_subplot()
ax.plot(x,y_tanh)
ax.grid()
ax.set_title('Tanh')
plt.show()

#使用示范:

import torch
input_x = torch.randn(4)
print(input_x)
output = torch.tanh(input_x)
print(output)
#tensor([-0.1574, -0.2590, 0.7387, -0.1653])
#tensor([-0.1561, -0.2533, 0.6284, -0.1638])

(3) Relu函数:修正线性单元(The Rectified Linear Unit)的简称

import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-10,10)
fig = plt.figure()
ax = fig.add_subplot()
y_relu = np.array([0*item if item<0 else item for item in x ])
ax.plot(x,y_relu)
ax.grid()
ax.set_title('ReLu')
plt.show()

#实际使用时的调用方法如下

import torch
input_x = torch.randn(4)
print(input_x)
output = torch.nn.functional.relu(input_x)
print(output)
#tensor([ 0.0439, -0.7945, -1.0825, 0.9218])
#tensor([0.0439, 0.0000, 0.0000, 0.9218])

(4)LeakyRelu函数

四、优化器模块

1.Optimizer的使用

在使用torch.potim之前,需要构建optimizer对象,它可以保持当前的参数状态并根据得到的梯度进行参数更新。

optimizer = optim.Adam(net.parameters(), lr = 0.001)

另外,torch.optim还实现了单次优化的方法,可以更新参数。

一旦参数的梯度被backward( )计算好之后,可以调用optimizer.step( )函数进行单次优化。

2. 梯度下降法

(1)批量梯度下降(Batch Gradient Descent,BGD)。

批量梯度下降算法是针对整个数据集,通过对所有样本的计算损失函数来求解梯度的方向。这种算法的好处是利于寻找全局最优解,梯度方差小;但是当样本数据很多时计算量会很大,会导致训练过程很慢。

(2)随机梯度下降(Stochastic Gradient Descent,SGD)。

当训练数据N很大时,通过计算总的损失函数来求梯度代价很大,所以一个常用的方法是计算训练集中的一个样本的损失函数来求解梯度的方向,这就是随机梯度下降的方法。
虽然采用这种方法,训练速度快,但准确度也因此下降,梯度方差会变大,同时由于鞍点的存在可能导致局部梯度为零,无法继续移动,使得最优解只是局部最优。

(3)小批量梯度下降(Mini-Batch Gradient DescentMBGD)

为了在提高训练速度的同时,保持梯度方差大小合适,以便于寻找全局最优解,提出了小批量梯度下降法,即把数据分为若干批量,按批量来更新参数,这样,一个批量中的数据可以共同决定梯度的方向,下降时也不易跑偏,减少了梯度下降的随机性,也减少了计算量。需要注意的是,批量大小虽然不影响梯度的期望,但会影响梯度的方差。根据经验,批量越大时,随机梯度的方差越小,训练也越稳定,因此可以选择较高的学习率;批量较小时,需要设置较小的学习率,否则模型可能难以收敛。

(4)动量优化算法(Momentum)。

动量优化算法是一种有效缓解梯度估计随机的算法,通过使用最近一段时间内的平均梯度来代替当前时刻的随机梯度作为参数更新的方向,从而提高优化速度。使用该算法可实现惯性保持,主要思想是引人一个积攒历史梯度信息动量来加速 SGD。采用相关物理知识解释,沿山体滚下的铁球,向下的力总是不变,产生动量不断积累,速度也自然越来越快;同时左右的弹力不断切换,动量积累的结果是相互抵消,也就减弱了球的来回振荡。这样就说明了采用了动量的随机梯度下降法为何可以有效缓解梯度下降方差,提高优化速度。虽然使用该方法不能保证收敛到全局最优,但能够使得梯度越过山谷和鞍点。跳出局部最优。

一般而言,在迭代初期,梯度方向都比较一致,动量法会起到加速作用,可以更快到达最优点,而在迭代后期,梯度方向不一致,在收敛值附近振荡,动量法会起到减速作用,增加稳定性。由于动量优化算法是基于随机梯度下降算法所提出的,因此通过设置SGD函数的相关参数即可。在上述所提到的 SGD函数中,参数momentum(动量因子)就是代表动量相关的操作,默认取数值0,只需选择合适数值就可以实现动量优化算法。

3.逐参数适应学习率方法

之前的方法对所有的参数都是同一个学习率,现在对不同的参数可以通过逐参数适应学习率方法设置不同的学习率。以下是几种常见的方法。

(1)AdaGrad。

AdaGrad是一种逐参数自适应学习率的优化算法,可以为不同的变量提供不同的学习率。它将学习率与参数相适应,对频繁参数的罕见更新和较小更新执行更大的更新,增加了罕见但信息丰富的特征的影响,因此非常适合处理稀疏数据。但其中的一个缺点是:在深度学习中单调的学习率被证明通常过于激进且过早停止学习。
该算法的基本思想是对每个变量采用不同的学习率,这个学习率在一开始比较大,用于快速梯度下降;随着优化过程进行对于已经下降很多的变量,减缓学习率,对于还没怎么下降的变量,则保持较大的学习率。其更新参数的方式如下。

其中,t代表每一次迭代; ε一般是一个极小值,防止分母为0;Gi,t表示了前t步参数θi,梯度的平方累加。
其在PyTorch中的用法代码如下:

optimizer=torch.optim.Adagrad(params,lr=0.01,lr_decay=0, weight_decay=0,initial_accumulator_value=0)

上述代码中参数的含义为:params(iterable)为待优化参数的iterable或者定义了参数组的dict;

lr(float,可选)为学习率(默认:0.01);

lr_decay(float,可选)为学习率衰减(默认:0);

weight_decay(float,可选)为权重衰减(L2惩罚,默认:0)

(2)RMSProp

(3)Adam
Adam算法即自适应时刻估计方法(Adaptive Moment Estimation), 相当于自适应学习率(RMSProp)和动量法相结合,能够计算每个参数的自适应学习率,将惯性保持和环境感知这两个优点集于一身。其中,动量直接并入了梯度一阶矩的估计。其次,相比于缺少修正因子导致二阶矩估计可能在训练初期具有很高偏置的RMSProp,Adam包括偏置修正,修正从原点初始化的一阶矩(动量项)和(非中心的)二阶矩估计。其更新参数的公式如下。

其中,Mt为梯度的第一时刻平均值(一阶动量项),Vt为梯度的第二时刻非中心方差(二阶动量项),在实际计算中,β1设为0.9,β2设为0.9999,ε是一个取值很小的值(一般为le-8), 为了避免分母为0。
PyTorch上实现代码如下。

optimizer = torch.optim.Adam(params, lr=0.001, betas= (0.9, 0.999),eps=1e-08, weight_decay=0)

五、训练和测试模块

1.训练函数的整体框架流程

首先需要将神经网络的运行模式设定为训练模式,只需要通过 net. train()就可以将运行模式设置为训练模式,并且定义相关变量便于训练时对返回值进行统计。

接着使用for循环遍历每个Batch进行训练,注意enumerate返回值有两个,一个是 Batch的序号,一个是数据(包含训练数据和标签)。在开始训练以前,首先要梯度清零,通过 optimizer.zero_grad()实现,其作用是清除所有优化的torch.Tensor(权重、偏差等)的梯度。

在梯度清零以后就可以输入数据计算结果以及损失,接着需要对损失进行反向传播,即loss.backward(),这是通过autograd 包来实现的,autograd 包会根据 loss 进行过的数学运算来自动计算其对应的梯度。具体来说,损失函数loss是由模型的所有权重w经过一系列运算得到的,若某个权重的 requires_grads 为 True,则该权重的所有上层参数(后面层的权重)的,grad_fn属性中就保存了对应的运算,在使用loss,backward()后,会一层层地反向传播计算每个权重的梯度值,并保存到该权重的.grad属性中。

在经过反向传播计算得到每个权重的梯度值以后,需要通过step()函数执行一次优化步骤,通过梯度下降法来更新参数的值,也就是说,每迭代一次,通过 optimizer.step进行一次单次优化。

另外,为了计算模型的平均损失以及准确率,在每次迭代之后总是对损失 Loss 进行相加,同时记录迭代训练得到正确结果的次数,便于在后续计算评价损失及准确率。

2.测试函数

与训练模式不同,在测试阶段,参数不进行更新,梯度也不进行变化,而是直接使用在训练阶段已经学习好的权重和方差值运行模型。因此,必须把网络的模式调整为测试模式,这可以通过net.eval()函数实现,其作用是保证每个参数都固定,确保每个 Mini- Batch的均值和方差都不变,尤其是针对包含Dropout和Batch Normalization的网络,更需要调整网络的模式,避免参数更新。

在选定好网络模式以后,为了确保参数的梯度不进行变化,需要通过with_torch.no drad( )模块改变测试状态,在该模块下,所有计算得出的Tensor的requires_grad都自动设置为False,即不会对模型的权重和偏差求导。
由于是测试模式,只需要输入数据得到输出以及损失即可,不需要对模型参数进行更新。因此此处也少了反向传播 loss.backward()函数以及单次优化 optimizer.step()函数的步骤。至于其他的步骤,与训练模块相似,包括计算平均损失以及模型准确率,具体现代码如下。

#指定不进行梯度变化:
with torch.no_grad():
for batch_idx,(data,target) in enumerate(data_loader):
data = data.to(device).float()
target = target.to(device).long()
output = net(data)
loss = criterion(output,target)
total_loss += loss.item()
prediction = torch.argmax(output,1)
correct += (prediction == target).sum().item()
sample_num += len(prediction)
loss = total_loss/test_batch_num
acc=correct/sample_num
return loss, acc

3. 训练模式和测试模式的核心框架

(1)训练模式

for epoch in range(0,epochs):
#model train
model.train()
for batch_id,(inputs,labels) in enumerate(data_loader):
target=model(inputs)
loss = criterion(target,labels.squeeze())
optimizer.zero_grad()
loss. backward()
optimizer.step()

(2)测试模式

#model test
with torch.no_grad():
model. eval()
for batch_id, (inputs,labels) in enumerate(data_loader):
predict= model(inputs)
loss = criterion(predict,labels.squeeze())

六、循环神经网络

交通专业领域涉及很时间序列问题,如短时客流预测问题,即利用以往的数据预测未来短时内的客流量,是一种时间序列问题。
为了更好地处理时间序列问题,学者提出了循环神经网络结构(Recurrent Neural Network,RNN),最基本的循环神经网络由输入层、一个隐藏层和一个输出层组成。
RNN结构如图所示,

如果去掉有W的带箭头的连接线,即为普通的全连接神经网络。

其中,X是一个向量,代表输人层的值;S是一个向量,表示隐藏层的值,其不仅取决于当前的输人X,还取决于上一时刻隐藏层的值;0也是一个向量,它表示输出层的值;U 是输人层到隐藏层的权重矩阵;V是隐藏层到输层的权重矩阵;W是隐藏层上一时刻的值作为当前时刻输人的权重矩阵。

把此结构按照时间线展开,可得到展开的RNN 结构。

网络在t时刻接收到输入Xt,之后,隐藏层的值是St,输出值是Ot,St的值不仅取决于Xt,还取决于St-1。因此,在RNN结构下,当前时刻的信息在下一时刻也会被输入到网络中,网络中的信息形成时间相关性,解决了处理时间序列的问题。神经网络模型“学”到的东西隐含在权值W中。基础的神经网络只在层与层之间建立全连接,而RNN 与它们最大的不同之处在于同一层内的神经元在不同时刻也建立了全连接,即W与时间有关。
根据不同的输入输出模式,可以将RNN 分为以下四种结构:one to one;one to many;many to one 和 many to many。其中,最典型的结构属于one to one结构,这种结构为给定一个输入值来预测一个输出值。

one to one结构如图所示:

one to many:

many to one:

many to many:

(1)适用于机器翻译,自动问答

(2)适用于视频每一帧的分类和命名实体标记等

七、LSTM

LSTM(Long Short-TermMemory)模型属于循环神经网络RNN的一种。由Hochreiter等人于1997年提出,至今已有20多年的发展历史。传统RNN取得巨大成功的原因在于其能够将历史信息进行记忆存储并通过前馈神经网络(例如多层感知机)向传通、当经历长期的信息传递之后,由于梯度消失和梯度爆炸等原因,会存在有用信息丢失的问题,导致无法记忆长期信息。不同于传统RNN,LSTM由于其独特的记忆单元,能在一定程度上解决长期依赖问题,因此在自然语言处理(NLP)模式识别、短时交通预测等领域的表现均超越了传统RNN,取得了巨大的成功。

RNN和LSTM的最大区别在于分布在隐藏层的神经元结构,传统RNN的神经元结构简单, 例如仅包含一个激活函数层。LSTM的记忆单元(Block)更加复杂,LSTM模中增加了状态c,称为单元状态(Cell State),用来保存长期的状态,而LSTM的关键就是怎样控制长期状态c,LSTM使用三个控制开关,第一个开关负责控制如何继续保存长期状态c,第二个开关负责控制把即时状态输人到长期状态c,第三个开关负责控制是否把长期状态c作为当前的LSTM的输入。长期状态c的控制如图所示,其中三个开关分别是使用三个门来控制的,这种去除和增加单元状态中信息的门是一种让信息选择性通过的方法。

LSTM用两个门来控制单元状态c的内容,一是遗忘门 (Forget Gate) ,遗忘门决定了上一时刻的单元状态ct-1有多少保留到当前时刻ct; 二是输入门 (Input Gate) ,输入门决定了当前时刻网络的输入xt,有多少保存到单元状态ct。LSTM 用输出门 (Output Gate) 来控制单元状态ct,有多少输出到LSTM的当前输出值ht

下面对这三个门逐一介绍,在以下示意图中,黄色矩形是学习得到的神经网络层,粉色圆形表示运算操作,箭头表示向量的传输过程。
遗忘门:决定应丢弃或保留哪些信息。来自前一个隐藏状态的信息和当前输入的信息同时传递到 sigmoid 函数中去,输出值介于 0 和 1 之间,越接近 0 意味着越应该丢弃,越接近 1 意味着越应该保留。



输人门:输入门用于更新细胞状态。首先将前一层隐藏状态的信息和当前输入的信息传递到 sigmoid 函数中去。将值调整到 0~1 之间来决定要更新哪些信息。0 表示不重要,1 表示重要。其次还要将前一层隐藏状态的信息和当前输入的信息传递到 tanh 函数中去,创造一个新的侯选值向量。最后将 sigmoid 的输出值与 tanh 的输出值相乘,sigmoid 的输出值将决定 tanh 的输出值中哪些信息是重要且需要保留下来的。

更新细胞状态:前一层的细胞状态与遗忘向量逐点相乘。如果它乘以接近 0 的值,意味着在新的细胞状态中,这些信息是需要丢弃掉的。然后再将该值与输入门的输出值逐点相加,将神经网络发现的新信息更新到细胞状态中去。至此,就得到了更新后的细胞状态。

输出门:输出门用来确定下一个隐藏状态的值,隐藏状态包含了先前输入的信息。首先,我们将前一个隐藏状态和当前输入传递到 sigmoid 函数中,然后将新得到的细胞状态传递给 tanh 函数。最后将 tanh 的输出与 sigmoid 的输出相乘,以确定隐藏状态应携带的信息。再将隐藏状态作为当前细胞的输出,把新的细胞状态和新的隐藏状态传递到下一个时间步长中去。

八、图卷积神经网络

现实中更多重要的数据集都是用图的形式存储的,例如:知识图谱、社交网络、通信网络、蛋白质分子结构等。这些图网络的形式并不像图像一样是排列整齐的矩阵,而是具有空间拓扑图结构的不规则数据。
对于具有拓扑结构的图数据,可以按照定义用于网格化结构数据的卷积的思想来定义。拓扑图上的卷积操作,如图下图所示,将每个节点的邻居节点的特征传播到该节点,再进行加权,就可以得到该点的聚合特征值。类似于 CNN,图卷积也共享权重,不过不同于 CNN 中每个 kernel的权重都是规则的矩阵,按照对应位置分配,图卷积中的权重通常是一个集合。在对一个节点计算聚合特征值时,按一定规律将参与聚合的所有点分配为多个不同的子集,同一个子集内的节点采用相同的权重,从而实现权重共享。例如,对于下图,可以规定和红色点距离为1的点为1邻域子集,距离为2的点为2邻域子集。当然,也可以采用更加复杂的策略,如按照距离图重心的远近来分配权重。权重的分配策略有时也称为label策略,对邻接节点分配labellabel相同节点的共享一个权重。

对于拓扑图结构的数据,图卷积神经网络能很好地抽取节点与节点直接的连接关系。因此,GCN近几年得到了快速发展,从谱图卷积滤波器,到切比雪夫多项式滤波器,再到一阶近似滤波器,GCN的表现能力得到了极大的提升。
图上的卷积定义基本上可以分为两类,一个是基于谱的图卷积,它们通过傅里叶变换将节点映射到频域空间,通过在频域空间上做乘积来实现时域上的卷积,最后再将做完乘积的特征映射回时域空间。而另一种是基于空间域的图卷积,与传统的 CNN 很像,只不过在图结构上更难定义节点的邻居以及与邻居之间的关系。
定义一张图G=(V,E,A),V是图的节点集合,E是图的边集合,A∈Rn∗n代表该网络的邻接矩阵,则GCN卷积操作公式为:

A波浪=A+I,I是单位矩阵
D波浪是A波浪的度矩阵(degree matrix)
H是每一层的特征,对于输入层的话,H就是X
σ是非线性激活函数

 

posted on   zc-DN  阅读(140)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署
点击右上角即可分享
微信分享提示