此博客以机器学习中的一些简单模型为基础,学习Pytorch的基本架构,不做过多深入每个函数的研究与探讨,这部分只需要面向具体的项目代码即可!文章最后面会附上一些相关链接用来参考学习。

Pytorch基础:Tensor(张量)

对于张量(Tensor),就可以理解为多为矩阵,也只是一种特别的存储方式而已,当然也可以表示一个元素的张量(或,标量)

1
2
3
4
tensor = torch.tensor([3.1433223]) 
print(tensor)
tensor.size()
tensor.item()
1
2
3
tensor([3.1433])
torch.Size([1])
3.143322229385376

Tensor 的基本类型

Tensor的基本数据类型有五种:

  • 32位浮点型:torch.FloatTensor。 (默认)
  • 64位整型:torch.LongTensor。
  • 32位整型:torch.IntTensor。
  • 16位整型:torch.ShortTensor。
  • 64位浮点型:torch.DoubleTensor。

除以上数字类型外,还有 byte和chart型

1
tensor([3.1426], dtype=torch.float16)

设备转换

相对于 Numpy 中多维矩阵的表示 ndarray 只能在 CPU 上运行,Tensor 可以在 GPU 上运行。所以有时候需要统一好每个 Tensor 所在设备。

1
2
3
4
5
6
#使用torch.cuda.is_available()来确定是否有cuda设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
#将tensor传送到设备
gpu_b=cpu_b.to(device)
gpu_b.type()
1
2
cuda
'torch.cuda.FloatTensor'

Autograd计算梯度数值

在创建张量时,可以通过设置 requires_grad=True来告诉 Pytorch 对该张量进行自助求导,Pytorch 会记录该张量的每一步操作历史并自动计算梯度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
x = torch.rand(5, 5, requires_grad=True)
x

# tensor([[0.0403, 0.5633, 0.2561, 0.4064, 0.9596],
# [0.6928, 0.1832, 0.5380, 0.6386, 0.8710],
# [0.5332, 0.8216, 0.8139, 0.1925, 0.4993],
# [0.2650, 0.6230, 0.5945, 0.3230, 0.0752],
# [0.0919, 0.4770, 0.4622, 0.6185, 0.2761]], requires_grad=True)

y = torch.rand(5, 5, requires_grad=True)
z=x**2+y**3

# tensor([[3.3891e-01, 4.9468e-01, 8.0797e-02, 2.5656e-01, 2.9529e-01],
# [7.1946e-01, 1.6977e-02, 1.7965e-01, 3.2656e-01, 1.7665e-01],
# [3.1353e-01, 2.2096e-01, 1.2251e+00, 5.5087e-01, 5.9572e-02],
# [1.3015e+00, 3.8029e-01, 1.1103e+00, 4.0392e-01, 2.2055e-01],
# [8.8726e-02, 6.9701e-01, 8.0164e-01, 9.7221e-01, 4.2239e-04]],
# grad_fn=<AddBackward0>)

在张量进行操作后,grad_fn 已经被赋予了一个新的函数,这个函数引用了一个创建了这个 Tensor 类的 Function 对象。 Tensor 和 Function 互相连接生成了一个非循环图,它记录并且编码了完整的计算历史。每个张量都有一个 `.grad_fn 属性,如果这个张量是用户手动创建的那么这个张量的 grad_fnNone

当计算完成后调用 .backward() 方法自动计算梯度并且将计算结果保存到 grad 属性中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
z.backward()
print(x.grad,y.grad)

# 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., 1.]])
# 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., 1.]])

# 我们的返回值不是一个标量,所以需要输入一个大小相同的张量作为参数,
# 这里我们用ones_like函数根据x生成一个张量
z.backward(torch.ones_like(x))
print(x.grad)

# tensor([[0.2087, 1.3554, 0.5560, 1.0009, 0.9931],
# [1.2655, 0.1223, 0.8008, 1.1127, 0.7261],
# [1.1052, 0.2579, 1.8006, 0.1544, 0.3646],
# [1.8855, 1.2296, 1.9061, 0.9313, 0.0648],
# [0.5952, 1.6190, 0.8430, 1.9213, 0.0322]])

我们可以使用 with torch.no_grad() 上下文管理器临时禁止对已设置 requires_grad=True 的张量进行自动求导。这个方法在测试集计算准确率的时候会经常用到,例如:

1
2
3
4
with torch.no_grad():
print((x +y*2).requires_grad)

# False

使用 .no_grad() 进行嵌套后,代码不会跟踪历史记录,也就是说保存的这部分记录会减少内存的使用量并且会加快少许的运算速度。

数据的加载和预处理

PyTorch通过 torch.utils.data 对一般常用的数据加载进行了封装,可以很容易地实现多线程数据预读和批量加载。 并且 torchvision 已经预先实现了常用图像数据集,包括前面使用过的CIFAR-10,ImageNet、COCO、MNIST、LSUN等数据集,可通过 torchvision.datasets 方便的调用。这里不具体介绍。

Dataset

Dataset 是一个抽象类,为了能够方便的读取,需要将要使用的数据包装为 Dataset 类。 自定义的 Dataset 需要继承它并且实现两个成员方法:

- `__getitem__()` 该方法定义用索引(`0` 到 `len(self)`)获取一条数据或一个样本
-  `__len__()` 该方法返回数据集的总长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#引用
from torch.utils.data import Dataset
import pandas as pd

#定义一个数据集
class BulldozerDataset(Dataset):
""" 数据集演示 """
def __init__(self, csv_file):
"""实现初始化方法,在初始化的时候将数据读载入"""
self.df=pd.read_csv(csv_file)
def __len__(self):
'''
返回df的长度
'''
return len(self.df)
def __getitem__(self, idx):
'''
根据 idx 返回一行数据
'''
return self.df.iloc[idx].SalePrice
# ------------------------------------------------------------------------
# 实例化一个对象访问它
ds_demo= BulldozerDataset('median_benchmark.csv')
# 实现了 __len__ 方法所以可以直接使用len获取数据总数
len(ds_demo)
# 用索引可以直接访问对应的数据,对应 __getitem__ 方法
ds_demo[0]

自定义的数据集已经创建好了,下面我们使用官方提供的数据载入器,读取数据。

Dataloader

DataLoader 为我们提供了对 Dataset 的读取操作,常用参数有:

  • batch_size(每个batch的大小)、
  • shuffle(是否进行shuffle操作)、
  • num_workers(加载数据的时候使用几个子进程)。

下面做一个简单的操作:

1
2
3
4
5
6
7
8
9
10
11
dl = torch.utils.data.DataLoader(ds_demo, batch_size=10, shuffle=True, num_workers=0)

# DataLoader返回的是一个可迭代对象,我们可以使用迭代器分次获取数据
idata=iter(dl)
print(next(idata))

# tensor([24000., 24000., 24000., 24000., 24000., 24000., 24000., 24000., 24000., 24000.], dtype=torch.float64)

# 常见的用法是使用for循环对其进行遍历
for i, data in enumerate(dl):
print(i,data)

我们已经可以通过 Dataset 定义数据集,并使用 Datalorder 载入和遍历数据集,除了这些以外, PyTorch 还提供能 torcvision 的计算机视觉扩展包。

torchvision 包

torchvision 是PyTorch中专门用来处理图像的库。

torchvision.datasets

torchvision.datasets 可以理解为PyTorch团队自定义的dataset,这些dataset帮我们提前处理好了很多的图片数据集,我们拿来就可以直接使用: - MNIST - COCO - Captions - Detection - LSUN - ImageFolder - Imagenet-12 - CIFAR - STL10 - SVHN - PhotoTour 我们可以直接使用,示例如下:

1
2
3
4
5
import torchvision.datasets as datasets
trainset = datasets.MNIST(root='./data', # 表示 MNIST 数据的加载的目录
train=True, # 表示是否加载数据库的训练集,false的时候加载测试集
download=True, # 表示是否自动下载 MNIST 数据集
transform=None)# 表示是否需要对数据进行预处理,none为不进行预处理

torchvision.models

torchvision 不仅提供了常用图片数据集,还提供了训练好的模型,可以加载之后,直接使用,或者在进行迁移学习 torchvision.models 模块的 子模块中包含以下模型结构。 - AlexNet - VGG - ResNet - SqueezeNet - DenseNet

1
2
3
#我们直接可以使用训练好的模型,当然这个与datasets相同,都是需要从服务器下载的
import torchvision.models as models
resnet18 = models.resnet18(pretrained=True)

torchvision.transforms

transforms 模块提供了一般的图像转换操作类,用作数据处理和数据增强

1
2
3
4
5
6
7
8
from torchvision import transforms as transforms
transform = transforms.Compose([
transforms.RandomCrop(32, padding=4), #先四周填充0,在把图像随机裁剪成32*32
transforms.RandomHorizontalFlip(), #图像一半的概率翻转,一半的概率不翻转
transforms.RandomRotation((-45,45)), #随机旋转
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.229, 0.224, 0.225)), #R,G,B每层的归一化用到的均值和方差
])

肯定有人会问:(0.485, 0.456, 0.406), (0.2023, 0.1994, 0.2010) 这几个数字是什么意思?官方的这个帖子有详细的说明: https://discuss.pytorch.org/t/normalization-in-the-mnist-example/457/21 这些都是根据ImageNet训练的归一化参数,可以直接使用,我们认为这个是固定值就可以。

神经网络包nn和优化器optimizer

torch.nn 是专门为神经网络设计的模块化接口。nn 构建于 Autograd 之上,可用来定义和运行神经网络。 这里我们主要介绍几个一些常用的类。除了nn别名以外,我们还引用了nn.functional,这个包中包含了神经网络中使用的一些常用函数,这些函数的特点是,不具有可学习的参数(如ReLU,pool,DropOut等),这些函数可以放在构造函数中,也可以不放,但是这里建议不放。一般情况下我们会将nn.functional 设置为大写的F,这样缩写方便调用。

1
2
3
4
5
# 首先要引入相关的包
import torch
# 引入torch.nn并指定别名
import torch.nn as nn
import torch.nn.functional as F

定义一个网络

PyTorch 中已经为我们准备好了现成的网络模型,只要继承 nn.Module,并实现它的 forward 方法,PyTorch 会根据 autograd,自动实现 backward 函数,在 forward 函数中可使用任何 tensor 支持的函数,还可以使用 if、for 循环、print、log 等 Python 语法,写法和标准的 Python 写法一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Net(nn.Module):
def __init__(self):
# nn.Module子类的函数必须在构造函数中执行父类的构造函数
super(Net, self).__init__()

# 卷积层 '1'表示输入图片为单通道, '6'表示输出通道数,'3'表示卷积核为3*3
self.conv1 = nn.Conv2d(1, 6, 3)
#线性层,输入1350个特征,输出10个特征
self.fc1 = nn.Linear(1350, 10) #这里的1350是如何计算的呢?这就要看后面的forward函数
#正向传播
def forward(self, x):
print(x.size()) # 结果:[1, 1, 32, 32]
# 卷积 -> 激活 -> 池化
x = self.conv1(x) #根据卷积的尺寸计算公式,计算结果是30,具体计算公式后面第二章第四节 卷积神经网络 有详细介绍。
x = F.relu(x)
print(x.size()) # 结果:[1, 6, 30, 30]
x = F.max_pool2d(x, (2, 2)) #我们使用池化层,计算结果是15
x = F.relu(x)
print(x.size()) # 结果:[1, 6, 15, 15]
# reshape,‘-1’表示自适应
#这里做的就是压扁的操作 就是把后面的[1, 6, 15, 15]压扁,变为 [1, 1350]
x = x.view(x.size()[0], -1)
print(x.size()) # 这里就是fc1层的的输入1350
x = self.fc1(x)
return x

net = Net()
print(net)

# Net(
# (conv1): Conv2d(1, 6, kernel_size=(3, 3), stride=(1, 1))
# (fc1): Linear(in_features=1350, out_features=10, bias=True)
# )

网络的可学习参数通过 net.parameters() 返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
for parameters in net.parameters():
print(parameters)

# ----------输出----------
Parameter containing:
tensor([[[[ 0.2745, 0.2594, 0.0171],
[ 0.0429, 0.3013, -0.0208],
[ 0.1459, -0.3223, 0.1797]]],


[[[ 0.1847, 0.0227, -0.1919],
[-0.0210, -0.1336, -0.2176],
[-0.2164, -0.1244, -0.2428]]],


[[[ 0.1042, -0.0055, -0.2171],
[ 0.3306, -0.2808, 0.2058],
[ 0.2492, 0.2971, 0.2277]]],


[[[ 0.2134, -0.0644, -0.3044],
[ 0.0040, 0.0828, -0.2093],
[ 0.0204, 0.1065, 0.1168]]],


[[[ 0.1651, -0.2244, 0.3072],
[-0.2301, 0.2443, -0.2340],
[ 0.0685, 0.1026, 0.1754]]],


[[[ 0.1691, -0.0790, 0.2617],
[ 0.1956, 0.1477, 0.0877],
[ 0.0538, -0.3091, 0.2030]]]], requires_grad=True)
Parameter containing:
tensor([ 0.2355, 0.2949, -0.1283, -0.0848, 0.2027, -0.3331],
requires_grad=True)
Parameter containing:
tensor([[ 2.0555e-02, -2.1445e-02, -1.7981e-02, ..., -2.3864e-02,
8.5149e-03, -6.2071e-04],
[-1.1755e-02, 1.0010e-02, 2.1978e-02, ..., 1.8433e-02,
7.1362e-03, -4.0951e-03],
[ 1.6187e-02, 2.1623e-02, 1.1840e-02, ..., 5.7059e-03,
-2.7165e-02, 1.3463e-03],
...,
[-3.2552e-03, 1.7277e-02, -1.4907e-02, ..., 7.4232e-03,
-2.7188e-02, -4.6431e-03],
[-1.9786e-02, -3.7382e-03, 1.2259e-02, ..., 3.2471e-03,
-1.2375e-02, -1.6372e-02],
[-8.2350e-03, 4.1301e-03, -1.9192e-03, ..., -2.3119e-05,
2.0167e-03, 1.9528e-02]], requires_grad=True)
Parameter containing:
tensor([ 0.0162, -0.0146, -0.0218, 0.0212, -0.0119, -0.0142, -0.0079, 0.0171,
0.0205, 0.0164], requires_grad=True)

net.named_parameters 可同时返回可学习的参数及名称

1
2
3
4
5
6
for name,parameters in net.named_parameters():

# conv1.weight : torch.Size([6, 1, 3, 3])
# conv1.bias : torch.Size([6])
# fc1.weight : torch.Size([10, 1350])
# fc1.bias : torch.Size([10])

forward函数的输入和输出都是Tensor

1
2
3
4
5
6
7
8
9
10
11
12
13
input = torch.randn(1, 1, 32, 32) # 这里的对应前面fforward的输入是32
out = net(input)
input.size()

# torch.Size([1, 1, 32, 32])

out.size()

torch.Size([1, 1, 32, 32])
torch.Size([1, 6, 30, 30])
torch.Size([1, 6, 15, 15])
torch.Size([1, 1350])
torch.Size([1, 10])

在反向传播前,先要将所有参数的梯度清零

1
2
net.zero_grad() 
out.backward(torch.ones(1,10)) # 反向传播的实现是PyTorch自动实现的,我们只要调用这个函数即可

注意torch.nn 只支持 mini-batches,不支持一次只输入一个样本,即一次必须是一个batch。也就是说,就算我们输入一个样本,也会对样本进行分批,所以,所有的输入都会增加一个维度,我们对比下刚才的 inputnn 中定义为3维,但是我们人工创建时多增加了一个维度,变为了4维,最前面的1即为batch-size。

损失函数

在nn中PyTorch还预制了常用的损失函数,下面我们用MSELoss用来计算均方误差:

1
2
3
4
5
6
7
y = torch.arange(0,10).view(1,10).float()
criterion = nn.MSELoss()
loss = criterion(out, y)
#loss是个scalar,我们可以直接用item获取到他的python类型的数值
print(loss.item())

# 28.92203712463379

优化器

在反向传播计算完所有参数的梯度后,还需要使用优化方法来更新网络的权重和参数,例如随机梯度下降法(SGD)的更新策略如下:

weight = weight - learning_rate * gradient

torch.optim 中实现大多数的优化方法,例如 RMSProp、Adam、SGD等,下面我们使用SGD做个简单的样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch.optim

out = net(input) # 这里调用的时候会打印出我们在forword函数中打印的x的大小
criterion = nn.MSELoss()
loss = criterion(out, y)
#新建一个优化器,SGD只需要要调整的参数和学习率
optimizer = torch.optim.SGD(net.parameters(), lr = 0.01)
# 先梯度清零(与net.zero_grad()效果一样)
optimizer.zero_grad()
loss.backward()

#更新参数
optimizer.step()

这样,神经网络的数据的一个完整的传播就已经通过PyTorch实现了。

深度学习基础

监督学习和无监督学习

监督学习、无监督学习、半监督学习、强化学习是我们日常接触到的常见的四个机器学习方法:

  • 监督学习:通过已有的训练样本(即已知数据以及其对应的输出)去训练得到一个最优模型(这个模型属于某个函数的集合,最优则表示在某个评价准则下是最佳的),再利用这个模型将所有的输入映射为相应的输出。
  • 无监督学习:它与监督学习的不同之处,在于我们事先没有任何训练样本,而需要直接对数据进行建模。
  • 半监督学习 :在训练阶段结合了大量未标记的数据和少量标签数据。与使用所有标签数据的模型相比,使用训练集的训练模型在训练时可以更为准确。
  • 强化学习:我们设定一个回报函数(reward function),通过这个函数来确认否越来越接近目标,类似我们训练宠物,如果做对了就给他奖励,做错了就给予惩罚,最后来达到我们的训练目的。

这里我们只着重介绍监督学习,因为我们后面的绝大部们课程都是使用的监督学习的方法,在训练和验证时输入的数据既包含输入x,又包含x对应的输出y,即学习数据已经事先给出了正确答案。

线性回归 (Linear Regreesion)

这里不解释具体原理,直接看写法,体会训练过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 注意,这里我们使用了一个新库叫 seaborn 如果报错找不到包的话请使用pip install seaborn 来进行安装
import torch
from torch.nn import Linear, Module, MSELoss
from torch.optim import SGD
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns

# 下面我生成一些随机的点,来作为我们的训练数据,回归:y = 5*x + 7
x = np.random.rand(256)
noise = np.random.randn(256) / 4
y = x * 5 + 7 + noise
df = pd.DataFrame()
df['x'] = x
df['y'] = y

# 我们随机生成了一些点,下面将使用PyTorch建立一个线性的模型来对其进行拟合,这就是所说的训练的过程,由于只有一层线性模型,所以我们就直接使用了:
model=Linear(1, 1)
# 其中参数(1,1)代表输入输出的特征(feature)数量都是1
# Linear 模型的表达式是y=wx+b,其中w代表权重,b代表偏置

# 损失函数我们使用均方损失函数
criterion = MSELoss()

# 优化器我们选择最常见的优化方法 SGD,就是每一次迭代计算 mini-batch 的梯度,然后对参数进行更新,学习率 0.01
optim = SGD(model.parameters(), lr = 0.01)

# 训练3000次
epochs = 3000

# 准备训练数据: x_train, y_train 的形状是(256, 1),
# 代表mini-batch大小为256,feature为1. astype('float32') 是为了下一步可以直接转换为 torch.float
x_train = x.reshape(-1, 1).astype('float32')
y_train = y.reshape(-1, 1).astype('float32')

# 开始训练
for i in range(epochs):
# 整理输入和输出的数据,这里输入和输出一定要是torch的Tensor类型
inputs = torch.from_numpy(x_train)
labels = torch.from_numpy(y_train)
#使用模型进行预测
outputs = model(inputs)
#梯度置0,否则会累加
optim.zero_grad()
# 计算损失
loss = criterion(outputs, labels)
# 反向传播
loss.backward()
# 使用优化器默认方法优化
optim.step()
if (i%100==0):
#每 100次打印一下损失函数,看看效果
print('epoch {}, loss {:1.4f}'.format(i,loss.data.item()))

训练完成了,看一下训练的成果是多少。用 model.parameters() 提取模型参数。 $w$, $b$ 是我们所需要训练的模型参数,我们期望的数据 $w=5$,$b=7$ 可以做一下对比

1
2
3
4
[w, b] = model.parameters()
print (w.item(),b.item())

# 4.994358062744141 7.0252156257629395

损失函数(Loss Function)

损失函数(loss function)是用来估量模型的预测值(我们例子中的output)与真实值(例子中的y_train)的不一致程度,它是一个非负实值函数,损失函数越小,模型的鲁棒性就越好。 我们训练模型的过程,就是通过不断的迭代计算,使用梯度下降的优化算法,使得损失函数越来越小。损失函数越小就表示算法达到意义上的最优。

这里有一个重点:因为PyTorch是使用mini-batch来进行计算的,所以损失函数的计算出来的结果已经对mini-batch取了平均。

常见(PyTorch内置)的损失函数有以下几个:

nn.L1Loss:

输入x和目标y之间差的绝对值,要求 x 和 y 的维度要一样(可以是向量或者矩阵),得到的 loss 维度也是对应一样的

nn.NLLLoss:

用于多分类的负对数似然损失函数$loss(x, class) = -x[class]$;

NLLLoss中如果传递了weights参数,会对损失进行加权,公式就变成了

n.MSELoss:

均方损失函数 ,输入x和目标y之间均方差

nn.CrossEntropyLoss:

多分类用的交叉熵损失函数,LogSoftMax 和 NLLLoss 集成到一个类中,会调用 nn.NLLLoss 函数,我们可以理解为 CrossEntropyLoss()=log_softmax() + NLLLoss()

因为使用了NLLLoss,所以也可以传入weight参数,这时loss的计算公式变为:

所以一般多分类的情况会使用这个损失函数;

nn.BCELoss:

计算 x 与 y 之间的二进制交叉熵。$loss(o,t)=-\frac{1}{n}\sum_i(t[i] log(o[i])+(1-t[i]) log(1-o[i]))$ 与NLLLoss类似,也可以添加权重参数:

用的时候需要在该层前面加上 Sigmoid 函数。

梯度下降

在介绍损失函数的时候我们已经说了,梯度下降是一个使损失函数越来越小的优化算法,在无求解机器学习算法的模型参数,即约束优化问题时,梯度下降(Gradient Descent)是最常采用的方法之一。所以梯度下降是我们目前所说的机器学习的核心,了解了它的含义,也就了解了机器学习算法的含义。

Mini-batch的梯度下降法

对整个训练集进行梯度下降法的时候,我们必须处理整个训练数据集,然后才能进行一步梯度下降,即每一步梯度下降法需要对整个训练集进行一次处理,如果训练数据集很大的时候处理速度会很慢,而且也不可能一次的载入到内存或者显存中,所以我们会把大数据集分成小数据集,一部分一部分的训练,这个训练子集即称为Mini-batch。 在PyTorch中就是使用这种方法进行的训练,可以看看上一章中关于dataloader的介绍里面的batch_size就是我们一个Mini-batch的大小。

为了介绍的更简洁,使用 吴恩达老师的 deeplearning.ai 课程板书。

对于普通的梯度下降法,一个epoch只能进行一次梯度下降;而对于Mini-batch梯度下降法,一个epoch可以进行Mini-batch的个数次梯度下降。 img 普通的batch梯度下降法和Mini-batch梯度下降法代价函数的变化趋势,如下图所示: img

  • 如果训练样本的大小比较小时,能够一次性的读取到内存中,那我们就不需要使用Mini-batch,

  • 如果训练样本的大小比较大时,一次读入不到内存或者现存中,那我们必须要使用 Mini-batch来分批的计算

  • Mini-batch size的计算规则如下,在内存允许的最大情况下使用2的N次方个size img

torch.optim是一个实现了各种优化算法的库。大部分常用优化算法都有实现,我们直接调用即可。

torch.optim.SGD

随机梯度下降算法,带有动量(momentum)的算法作为一个可选参数可以进行设置,样例如下:

1
2
3
#lr参数为学习率,对于SGD来说一般选择0.1 0.01.0.001,如何设置会在后面实战的章节中详细说明
##如果设置了momentum,就是带有动量的SGD,可以不设置
optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9)

torch.optim.RMSprop

除了以上的带有动量Momentum梯度下降法外,RMSprop(root mean square prop)也是一种可以加快梯度下降的算法,利用RMSprop算法,可以减小某些维度梯度更新波动较大的情况,使其梯度下降的速度变得更快

1
2
#我们的课程基本不会使用到RMSprop所以这里只给一个实例
optimizer = torch.optim.RMSprop(model.parameters(), lr=0.01, alpha=0.99)

torch.optim.Adam

Adam 优化算法的基本思想就是将 Momentum 和 RMSprop 结合起来形成的一种适用于不同深度学习结构的优化算法

1
2
# 这里的lr,betas,还有eps都是用默认值即可,所以Adam是一个使用起来最简单的优化方法
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999), eps=1e-08)

方差/偏差

  • 偏差度量了学习算法的期望预测与真实结果的偏离程序,即刻画了学习算法本身的拟合能力
  • 方差度量了同样大小的训练集的变动所导致的学习性能的变化,即模型的泛化能力 img

从图中我们可以看出 - 高偏差(high bias)的情况,一般称为欠拟合(underfitting),即我们的模型并没有很好的去适配现有的数据,拟合度不够。 - 高方差(high variance)的情况一般称作过拟合(overfitting),即模型对于训练数据拟合度太高了,失去了泛化的能力。

如何解决这两种情况呢?

  • 欠拟合:

    • 增加网络结构,如增加隐藏层数目;
    • 训练更长时间;
    • 寻找合适的网络架构,使用更大的NN结构;
  • 过拟合 :

    • 使用更多的数据;
    • 正则化( regularization);
    • 寻找合适的网络结构;

正则化

利用正则化来解决High variance 的问题,正则化是在 Cost function 中加入一项正则化项,惩罚模型的复杂度,这里我们简单的介绍一下正则化的概念

L1正则化

损失函数基础上加上权重参数的绝对值 $L=E_{in}+\lambda{\sum_j} \left|w_j\right|$;

L2正则化

损失函数基础上加上权重参数的平方和 $L=E_{in}+\lambda{\sum_j} w^2_j$;

需要说明的是:$L_1$ 相比于 $L_2$ 会更容易获得稀疏解。

卷积神经网络经典模型LeNet-5

卷积神经网路的开山之作,麻雀虽小,但五脏俱全,卷积层、pooling层、全连接层,这些都是现代CNN网络的基本组件 - 用卷积提取空间特征;

  • 由空间平均得到子样本;
  • 用 tanh 或 sigmoid 得到非线性;
  • 用 multi-layer neural network(MLP)作为最终分类器;
  • 层层之间用稀疏的连接矩阵,以避免大的计算成本。 img

输入:图像Size为32*32。

输出:10个类别,分别为0-9数字的概率

  1. C1层是一个卷积层,有6个卷积核(提取6种局部特征),核大小为5 * 5
  2. S2层是pooling层,下采样(区域:2 * 2 )降低网络训练参数及模型的过拟合程度。
  3. C3层是第二个卷积层,使用16个卷积核,核大小:5 * 5 提取特征
  4. S4层也是一个pooling层,区域:2*2
  5. C5层是最后一个卷积层,卷积核大小:5 * 5 卷积核种类:120
  6. 最后使用全连接层,将C5的120个特征进行分类,最后输出0-9的概率
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import torch.nn as nn
class LeNet5(nn.Module):

def __init__(self):
super(LeNet5, 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) # 这里论文上写的是conv,官方教程用了线性层
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 = LeNet5()
print(net)

# LeNet5(
# (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)
# )

相关链接

《Python深度学习基于PyTorch》 | Python技术交流与分享 (feiguyunai.com)

ShusenTang/Dive-into-DL-PyTorch: 本项目将《动手学深度学习》(Dive into Deep Learning)原书中的MXNet实现改为PyTorch实现。 (github.com)