PyTorch学习笔记
目录
- PyTorch三大基本功能demo
- 回归问题 理论
- 回归问题 实战
- one-hot简介
- 分类问题引入
- 激活函数引入
- 结果处理
- 手写数字识别实战
- PyTorch数据类型
- 创建 Tensor
- PyTorch 中的索引与切片
- 维度变换
- 拼接与拆分
- 矩阵的运算
- 统计属性
- Compare 比较运算
- 高阶 OP
- 什么是梯度
- 常见函数梯度
- 激活函数及其梯度
- 损失函数(Loss)及其梯度
- 感知机
- 链式法则
- MLP(Multi-Layer Perceptron) 反向传播
- 2D 函数优化实例
- Logistic Regression
- Cross Entropy 交叉熵
- 多分类问题实战
- 激活函数
- GPU加速
- 测试
- Visdom 可视化
- 过拟合 & 欠拟合
- Train-Val-Test 划分
- 如何减轻 Over-fitting——Regularization
- 动量与学习率衰减
- Early Stop, Dropout
- 什么是卷积
- 卷积神经网络
- 池化和采样
- Feature Scaling
- 经典CNN
- nn.Module
- 数据增强
PyTorch三大基本功能demo
cuda加速的调用与比较
import torch
from torch import autograd
import time
# cuda加速的调用与比较
a = torch.rand(10000,10000)
b = torch.rand(10000,10000)
t0 = time.time()
c = a.matmul(b)
t1 = time.time()
print(a.device, t1-t0, c.norm(2))
a = a.cuda()
b = b.cuda()
t0 = time.time()
c = a.matmul(b)
t1 = time.time()
print(a.device, t1-t0, c.norm(2))
cpu 8.01131296157837 tensor(24667266.)
cuda:0 0.014377355575561523 tensor(24995950., device='cuda:0')
自动求导功能
神经网络,层数再深,嵌套再多,也是一系列函数经过运算与求导计算得来的。所以自动求导至关重要。
import torch
from torch import autograd
import time
x = torch.tensor(2.) #初始化x为2
a = torch.tensor(1., requires_grad=True) #初始化变量a,并添加requires_gard标记为True
b = torch.tensor(2., requires_grad=True) #同理a初始化b,c
c = torch.tensor(3., requires_grad=True)
y = a**2 * x + b * x + c #方程式
print("before:", a.grad, b.grad, c.grad) #输出求导前的数据值
gards = autograd.grad(y, [a,b,c]) #分别对abc求偏导
print("after:", gards[0],gards[1],gards[2]) #输出求导后的数据值
before: None None None
after: tensor(4.) tensor(2.) tensor(1.)
常用网络层
PyTorch内部封装了很多基本模型,可以让我们直接像搭积木一样的进行神经网络的搭建。
回归问题 理论
地位
梯度下降法——至关重要。表现出来的学习能力,就是梯度下降算法得出来的,这是整个Deep Learning的核心。
原理
通过对函数进行求导,得到导数后与原值进行作差,再根据步长(s)进行计算,来确定下次x迭代的值。公式如下: 然后再对进行迭代。 这样做的目的是让x在函数极值出反复横跳,达到不用求导分析单调性而得到函数极值点的目的。
补充
如果步长s设置的过大,很可能会越过极值点,导致反复横跳的幅度过大而导致计算误差。 相反,如果s过小,则可能会造成计算资源浪费,迭代次数过多。 初学者一般设置
上述的求解是最基本的求解,由上述方法发展出很多更好的求解器。
关于精确解
CFS:Close Form Solution CFS就是一个方程(组)的精确解,比如一元二次方程组,便很容易球的CFS。 但现实生活中存在很多不确定性因素(噪声 Noise),比如人的主观判断、数据采集误差等,所以求的CFS的情况很少。 目前经过实际经验可以证明:通过梯度下降求得的解在实际生产中表现良好,所以没必要去求CFS。
局部最优解用例
假设有如下函数定义: 其中表示从0.01到1的高斯噪声。
给出如下的数百个数据: 此时,定义 为什么要这样定义呢? 根据前文提到的梯度下降算法核心思想,所求得的为某函数的极小值,所以此处令,因为给出的原始数据来源于上文提到的带有噪声的算法,所以主观上趋于0。所以当时,。
关于局部最优解
类似(三次函数)存在两个极值点(极大值/极小值)及以上的函数,通过梯度下降算法智能找到局部最优解,可以将此过程理解为钢珠从碗口滚下,而这个碗可能是奇形怪状的,起伏较大的。钢珠可能在某个低谷就停下了,而这个低谷不是碗中最低的那个。 为了解决这一问题,诞生了一门学科最优化,而梯度下降便是其中的一个优化方法,尽管s设置的过小的时候很难找到全局最优,但s数值稍大就有几率找到全局最优。 另外一种改进过的优化方法叫Adam:
为了避免遇到局部最优解就停,所以在梯度下降算法中引入了动量,就变成了常用的Adam,另外Adam还有个学习率自适应。
所以要么转化为凸优化问题(可以找到全局最优解);要么在非凸上尽可能找到全局最优解。而深度学习一般是第二种方法。
——zhude
回归问题 实战
来求得当前的b, w对于所有的已知数据(x和y)的偏差值。 定义函数。此时分为两种情况:对w求偏导,对b求偏导。
对w求偏导: 根据公式求得w的偏导。
对b求偏导: 根据公式求得b的偏导。
然后根据偏导公式,代入x y,计算得到趋势值,来决定下一步的走向和幅度。
import torch
import numpy as np
# 传入b w和点数的集,计算loss函数的值(损失函数的值)
def compute_error_for_line_given_points(b,w,points):
totalError = 0
for i in range(0, len(points)):
x = points[i, 0]
y = points[i, 1]
totalError += (y - (w*x+b)) **2
return totalError / float(len(points))
# 求导,迭代,返回迭代后的b w数据
def step_gradient(b_crt, w_crt, points, learningRate):
# 初始化b w导数值变量
b_gradient = 0
w_gradient = 0
N = float(len(points))
for i in range(0, len(points)):
x = points[i, 0]
y = points[i, 1]
# 对b w的导数分别带入xy数据,并/N得到总体偏移方向和程度
# 手动对b w求偏导可得如下:
b_gradient += -(2/N) * (y - ((w_crt * x) + b_crt))
w_gradient += -(2/N) * x * (y - ((w_crt * x) + b_crt))
# 返回新的b w
new_b = b_crt - (learningRate * b_gradient)
new_w = w_crt - (learningRate * w_gradient)
return [new_b, new_w]
# 进行迭代
def gradient_descent_runner(points, starting_b, starting_m, learning_rate, num_iterations):
b = starting_b
m = starting_m
for i in range(num_iterations):
b, m = step_gradient(b, m, np.array(points), learning_rate)
return [b, m]
def run():
points = np.genfromtxt("data.csv", delimiter=",")
learning_rate = 0.0001
initial_b = 0 # initial y-intercept guess
initial_m = 0 # initial slope guess
num_iterations = 1000
print("Starting gradient descent at b = {0}, m = {1}, error = {2}"
.format(initial_b, initial_m,
compute_error_for_line_given_points(initial_b, initial_m, points))
)
print("Running...")
[b, m] = gradient_descent_runner(points, initial_b, initial_m, learning_rate, num_iterations)
print("After {0} iterations b = {1}, m = {2}, error = {3}".
format(num_iterations, b, m,
compute_error_for_line_given_points(b, m, points))
)
if __name__ == '__main__':
run()
Starting gradient descent at b = 0, m = 0, error = 5565.107834483211
Running...
After 1000 iterations b = 0.08893651993741346, m = 1.4777440851894448, error = 112.61481011613473
one-hot简介
概念
one-hot编码方式其实很简单,通过例子来说明: 有0,1,2四个数字,那么可以理解为三个类型。 其中 0 用[1,0,0]表示,1 用[0,1,0]表示,2 用[0,0,1]表示。 只要表达的每一个数据都只有1位为1,其余全是0即可。
为什么是这样表示?
(原因之一):
假如将上述的0 1 2表达的例子看作空间坐标,那么0 1 2之间的空间距离(欧氏距离)不管怎么计算,均相等。
那为什么不用[0,1]表示0,[0,2]表示1呢?那是因为映射到空间坐标轴上,它们处于同一直线上,在神经网络看来,它们之间将会存在相关性。
而我们做分类,就是为了消除相关性的。尽管看似one-hot的数据长度很长(在处理某些大规模分类问题的时候),但是GPU专门为其做了并行优化,实际使用下来速度并不慢。
需要明确的一点是,one-hot只是多种encode方法中的其中一种,神经网络实际运用中还存在其他的encode方法。
其他使用one-hot的原因日后会进行补充。
——本版块知识来源于zhude
分类问题引入
手写数字识别问题
如图,是一个手写数据的集合。每张图片为28*28,共有7000张。
我们约定:每张图片都由黑,白两个像素构成。分别在方阵(n*n矩阵)中0为白,1为黑。且只有这两种表示方法。将28*28的方阵展平成784*1的矩阵(这样做可以消除数据间的相关性)
算法模型
为了解决此类复杂的问题,我们使用三个线性模型进行数据处理。
其中,[]中间有几个数字,便表示这个向量是个几维向量。且内部数字仅表示数据规模,而不表示数据的具体信息。如[1,1,dx]就表示一个三维向量,且前两个维度的长度恒为1,即前两个维度不表示任何数据,仅第三个维度dx表示数据。所以可以将[a]升高维度变为[1,a]来符合矩阵运算的规则。
同理,[1,a] + [a] = [1,a]也成立,但a位置处的数据会根据矩阵加法法则进行改变。但其数据规模不变。
对于数据的运算,运用如下公式进行计算(暂时不需要了解为什么选用这种方法,仅了解如何处理数据即可):
需要明确的是:H X W b均为向量(或矩阵),其运算法则为矩阵的运算法则。 同理,
另外,根据上文提到的loss损失函数,我们在这里也定义一个损失函数:
,简单来说就是求解 pred(预测值)和 Y(真实值)之间的欧氏距离。这就是本例的损失函数。
算法小结
整体公式非常好理解,表示为数学公式即为: 将第一个模型的输出定义为,然后将作为输入,输入到第二个模型中去。以此类推。
其中的运算过程我们并不需要了解,只需了解把图片投入到算法中后,经过训练,会返回一个十维向量,其内部包含输出信息。
激活函数引入
在上例,虽然通过嵌套增强了其表达能力,但实际还是线性模型。
但对于一个手写数字来说,大多数时候非线性部分要多于线性部分。人脑之所以能够识别,是因为人脑具有很强的非线性拟合能力。
所以要添加一个非线性部分,使用激活函数来增加非线性拟合能力。
对于每个人脑的神经元来说,输出并不是简单的数学运算,而是具有「阈值」的。比如输入了一个很小的值,输出大约和0差不多,但当这个值过大的时候,输出接近 1 左右。本段描述的便是非常经典的 sigmoid 函数(后续补充)。
但为了简化理解,此处使用 ReLu 函数作为示例。
ReLU函数
ReLU 函数的解析式为: ReLU 函数拥有许多特性,它能够很好地避免离散问题(后续会进行补充)。 这个函数的值非常容易计算,图像就在本节下方(用Python绘制)。
%matplotlib inline
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
x = np.array([-4, 0, 3])
y = np.array([0, 0, 3])
plt.plot(x,y)
[<matplotlib.lines.Line2D at 0x2b8dfcaa850>]
激活函数如何使用
上文提到,对于手写识别案例的三层嵌套,使用的公式为:。
激活函数的使用非常简单,只需将改为即可。
同理,也要进行同样的操作。推广一下便是,激活函数加入的时候,只需即可。
结果处理
上例中,, 我们传入一个参数X,即
pred的输出假设为
为了将pred的输出处理为one-hot表示,我们引入一个新的函数。
此函数的作用是:返回pred中数值最大的位置为1,其余位置全为0。在此处,
手写数字识别实战
与上文不同的是,在实际使用中,一般第最后一层函数(此处为)套的并不是 ReLU,而是根据需求套其他的激活函数(比如 sigmoid)。
import torch
from torch import nn
from torch.nn import functional as F
from torch import optim
import torchvision
from matplotlib import pyplot as plt
#从pytorch_codes/mist_utils.py内引入封装完成的方法
from pytorch_codes.mist_utils import plot_curve,plot_image,one_hot
# Step.1 加载数据集(不用写,加载数据复制就可,现在无需掌握)
# 一次处理图片的数量
print(torch.cuda.is_available())
batch_size = 128
train_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('mnist_data', train=True, download=True,
transform=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
# 将数据等效到0的附近,来优化深度学习效率
torchvision.transforms.Normalize(
(0.1307,), (0.3081,))
])),
batch_size=batch_size, shuffle=True)
test_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('mnist_data/', train=False, download=True,
transform=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(
(0.1307,), (0.3081,))
])),
batch_size=batch_size, shuffle=False)
x,y = next(iter(train_loader))
print(x.shape, y.shape, x.min(), x.max()) # 输出数据集的参数
# 输出中,512张图片,1个通道,28行,28列
plot_image(x,y,'image sample') # 输出图片
# 创建网络(三层嵌套)
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# xw+b
self.fc1 = nn.Linear(28*28, 256) #第一层,256由经验决定,推导过程在上面
self.fc2 = nn.Linear(256, 64) #第二层,输入必须是fc1的输出,过程在上面,输出是64,经验决定
self.fc3 = nn.Linear(64, 10) #第三层,输出必须是10,因为结果是0-9的是个数字的one-hot编码
def forward(self,x):
#从x的输入进行层层迭代
#x:[b,1,28,28]
#h1 = relu(xw+b) = fc1
# x = F.relu(self.fc1(x))
x = self.fc1(x)
#h2 = relu(h1w2 + b2)
# x = F.relu(self.fc2(x))
x = self.fc2(x)
#h3不加激活函数 = h2w3+b3
x = self.fc3(x)
return x
net = Net()
# 使用现成的梯度下降优化器, momentum是动量,以后会讲
optimizer = optim.SGD(net.parameters(), lr = 0.01, momentum = 0.9 )
# train_loss 保存起来迭代的loss数据
train_loss = []
for epoch in range(10): #理论这里的循环次数越多,训练准确率就越高。
for batch_idx ,(x, y) in enumerate(train_loader):
# 现在X:[b,1,28,28], y:[512]
# 因为Net只能接受二维数据,所以要把x进行「降维打击」!
# [b, 1, 28, 28] ==> [b, feature]
x = x.view(x.size(0), 28*28)
out = net(x)
# 将真实的y转化为onehot
y_onehot = one_hot(y)
# 通过loss函数求得真实y和预测y的差距
# loss = mse(out, y_onehot)
loss = F.mse_loss(out, y_onehot)
# 得到梯度
optimizer.zero_grad()
loss.backward()
# w' = w - lr*grad
optimizer.step()
# 保存loss
train_loss.append(loss.item())
# 训练完成,得到了比较好的[w1,b1,w2,b2,w3,b3]
# 打印loss函数信息,可视化训练的效果
plot_curve(train_loss)
# 进行准确度测试
total_correct = 0
for x,y in test_loader:
x = x.view(x.size(0), 28*28)
out = net(x)
# out: [b,1] ==> pred: [b]
pred = out.argmax(dim = 1)
correct = pred.eq(y).sum().float().item()
total_correct += correct
total_num = len(test_loader.dataset)
acc = total_correct / total_num
print("Test Acc: ", acc)
True
torch.Size([128, 1, 28, 28]) torch.Size([128]) tensor(-0.4242) tensor(2.8215)
Test Acc: 0.9582
PyTorch数据类型
| Python Type | CPU Tensor | GPU Tensor |
|---|---|---|
| float | torch.FloatTensor | torch.cuda.FloatTensor |
| double | torch.DoubleTensor | torch.cuda.DoubleTensor |
| half | torch.HalfTensor | torch.cuda.HalfTensor |
| char | torch.CharTensor | torch.cuda.CharTensor |
| byte | torch.ByteTensor | torch.cuda.ByteTensor |
| short | torch.ShortTensor | torch.cuda.ShortTensor |
| int | torch.IntTensor | torch.cuda.IntTensor |
| long | torch.LongTensor | torch.cuda.LongTensor |
其中比较常用的是:Double、Float、Byte、Int、Long
String问题
看了上面的数据类型,你会发现其实少了一个非常重要的基本数据类型:String。String类型在Python编程中使用非常频繁,但是PyTorch作为一个深度学习框架,其主要目的就是实现算法的高效训练。所以PyTorch并不支持String类型。
我们上文提到过一种编码方式one-hot。那么可以用one-hot对字符串进行编码吗?答案是可以的。
比如我们令dot = [0,1], cat = [1,0]。或者把26个英文字母进行编码。都是贴合实际的编码方式。但是大多数情况下,one-hot对string编码是不切实际的。理由如下:
- one-hot的维度太多时,会造成运算困难(显存爆炸)。比如我们要对所有的英文单词进行编码,那么这个数据量是难以想象的。
- one-hot还会消除词与词之间的相关性。在上文介绍one-hot时我们提到,one-hot实际上是一个空间高维坐标的表示方法,而坐标轴之间的相关性十分弱。例如like和love这两个单词都有喜欢的意思,那么one-hot编码会消除二者之间的共性。
所以在自然语言处理(NLP)类型问题中,一般不使用one-hot进行编码。而是会更改编码的方式,比如使用Word2vec、glove等编码方式。因为自然语言处理十分复杂,所以在此文章中不做介绍,如有兴趣请自行了解。
类型推断与检验
类型推断
a = torch.randa(2,3)
print(a.type)
print(type(a))
其中randa(m,n)方法是指通过正态分布随机生成一个2行3列的张量矩阵。
类型检验
a = torch.randa(2,3)
print(isinstance(a, torch.FloatTensor))
值得注意的是:CPU和GPU上的数据是不一样的。
CPU<==>GPU类型转换
data = torch.randa(2,3)
# 转换成GPU矩阵张量
data = data.cuda()
# 转换成CPU矩阵张量
data = data.cpu()
标量
在PyTorch中,标量的意思是“0维向量(Dimension = 0)”。使用torch.tensor(float)进行创建。
注意,torch.tensor(float)和torch.tensor([float])是不一样的。后者是一个一维向量。
在PyTorch中使用最多的标量就是损失函数。如果对损失函数进行类型推断的话,那么它一般来讲是一个tensor.FloatTensor()。
判断标量
使用len(a.shape) == 0或者a.dim() == 0。
Dim = 1 的张量
初始化
上文提到,初始化Dim = 1 的张量使用torch.tensor([a1, a2, ...]) 这是其中一种初始化方法,以下是另外几种不同的初始化方法:
Dim = 1, Size = 1:
torch.FloatTensor(1)
Dim = 1, Size = 2:
torch.FloatTensor(2)
Dim = 1, Size = 2, from NumPy:
data = np.ones(2) # output = array([1., 1.])
torch.from_numpy(data)
Dim Size Shape
import torch
a = torch.randn(700)
print(a.size())
print(a.shape)
print(a.dim())
a = torch.randn(2,700)
print(a.size())
print(a.shape)
print(a.dim())
torch.Size([700])
torch.Size([700])
1
torch.Size([2, 700])
torch.Size([2, 700])
2
没啥好说的,看代码和输出就完事了! 在非严谨意义上,Shape和Size其实是一种东西。而Dim表示的是一个Tensor总共有多少个维度。所以可以理解为len(a.shape) == a.dim()
Dim = 2 的张量
使用torch.randn(2,3)生成一个dim = 2的张量。上文已经到在此不再赘述。
import torch
a = torch.randn(2, 3)
print(a)
tensor([[-0.6125, -1.6625, 1.3960],
[-0.6522, 0.1846, 1.8785]])
Dim = n 的张量
使用torch.randn(a1, a2, ..., an)。
这个张量能够表达的数据总和与一个 a[a1a2a3...*an]的数组相当。
一个Tensor能表达的数据总和使用a.numel()来获取。
特殊地,Dim = 0的标量能表达的数据为1。
import torch
a = torch.randn(2,3,4)
print("a的数据总和为:", a.numel())
print(a)
b = torch.randn(2,3,1,2)
print(b)
a的数据总和为: 24
tensor([[[ 1.0786, -0.7592, 0.9769, -0.7696],
[ 0.4920, -1.0772, -1.1417, 0.1720],
[ 1.1554, 1.8928, 1.7532, -0.6573]],
[[-0.5564, -0.8849, 0.7356, 1.3341],
[-0.2971, 1.3314, 0.7921, -0.4720],
[-1.1311, 0.0083, 1.4959, 0.7705]]])
tensor([[[[ 0.1817, -0.1085]],
[[ 0.7388, -0.0956]],
[[ 0.1781, -1.4507]]],
[[[ 1.1239, -0.9434]],
[[ 1.1975, -0.1320]],
[[-1.7088, 0.4738]]]])
创建 Tensor
从 NumPy 导入
import torch
import numpy as np
a = np.array([2,3,3])
b = torch.from_numpy(a)
print(b)
a = np.ones([2,3]) # 二维,数据默认为1,每一维长度为3的张量
b = torch.from_numpy(a)
print(b)
tensor([2, 3, 3])
tensor([[1., 1., 1.],
[1., 1., 1.]], dtype=torch.float64)
从 List 导入
此处比较容易混淆的是torch.tensor()方法和torch.Tensor()方法的使用规则。
简单总结一下便是:
小写tensor接收现有数据作为参数。大写Tensor接收shape作为参数。
torch.tensor(data),其中data只能是一个float或者[[a1,a2,...],[b1,b2,...], ...]...
data可以是一个任意维度向量的array表示。具体使用请看下例:
import torch
print(torch.tensor([2., 3.2])) # 小写tensor
print(torch.Tensor(2,3)) # 大写Tensor
print(torch.FloatTensor(2,3)) # 大写Tensor
print(torch.tensor([[1.,2.],[3., 4.]]))
tensor([2.0000, 3.2000])
tensor([[-2.1675e-27, 4.5604e-41, -1.3800e-22],
[ 3.0718e-41, 4.4842e-44, 0.0000e+00]])
tensor([[-2.1675e-27, 4.5604e-41, -1.3800e-22],
[ 3.0718e-41, 4.4842e-44, 0.0000e+00]])
tensor([[1., 2.],
[3., 4.]])
生成未初始化数据
如何生成一个空的数据(申请内存空间)?
- Torch.empty(shape),传入shape
- Torch.FloatTensor(shape),同上,但内存空间未分配
- 不推荐使用 torch.FloatTensor([1, 2]) = torch.tensor([1, 2])
未初始化的 Tensor 会出现什么问题?
tensor([[-2.1675e-27, 4.5604e-41, -1.3800e-22],
[ 3.0718e-41, 4.4842e-44, 0.0000e+00]])
如上是上个节点的输出,会发现数据参差不齐,既有大的也有小的,还有0.
所以在使用未初始化的数据之前,一定要进行赋值操作(数据写入)。
一个小细节
在 PyTorch 中,存在默认数据类型——进行 Tensor 初始化操作的时候,会自动转换为默认数据类型。
可以使用torch.set_default_tensor_type(torch.XXXXTensor)进行默认数据类型的改变。
另外一个小细节是: 如果使用int进行小写tensor初始化,比如torch.tensor([int, int]),那么它的类型是LongTensor。
另一个小细节
DoubleTorch一般是增强学习使用的。
import torch
print(torch.get_default_dtype())
print(torch.tensor([1.,1]).type()) #输出初始化类型
print(torch.Tensor(1,2).type()) #输出初始化类型
print(torch.tensor([1,1]).type()) #使用int进行初始化tensor()
torch.set_default_tensor_type(torch.DoubleTensor) #改变默认类型
print(torch.Tensor(1,2).type()) #再次输出初始化类型
torch.float64
torch.DoubleTensor
torch.DoubleTensor
torch.LongTensor
torch.DoubleTensor
随机初始化
上文提到,空初始化时会导致非常大、非常小以及0的数字产生。所以为了解决上述问题,最好在实际项目中使用随机初始化。
均匀采样
函数 torch.rand(shape) 生成的数据是均匀分布在 0~1 范围内的。
如果我们想让数据均匀分布在n以内,那么应该使用 x = n*torch(shape)。
rand_like()和randint()
假设一张量 a 符合 a.shape == [3,3],那么 b = torch.rand_like(a) == [3,3],但其内部数据不同。
简而言之,rand_like()的作用就是随机初始化和 a.shape 相同的 b 张量。
randint的使用方法有些特殊,它需要在 shape 前制定最大和最小值。
torch.randint(min, max, shape(带逗号))。
具体使用请看下例:
import torch
a = torch.rand(3,3)
print(a.shape)
b = torch.rand_like(a)
print(b.shape)
print(b)
c = 10 * torch.rand_like(b)
print(c)
print(torch.randint(1, 100, [2, 2]))
torch.Size([3, 3])
torch.Size([3, 3])
tensor([[0.6227, 0.8523, 0.9433],
[0.3965, 0.8545, 0.1008],
[0.0685, 0.8566, 0.6431]])
tensor([[3.1071, 0.9462, 2.0900],
[2.3369, 6.2986, 3.7430],
[7.7611, 3.0723, 8.9341]])
tensor([[88, 94],
[19, 53]])
在本节以上使用的是randn()函数,和rand函数区别其实并不大,只不过rand()是均匀分布,而randn()是正态分布。
Tips:可以使用torch.full(shape, initValue)生成默认值均为 initValue 的 Tensor。
Tips2:torch.arange(start, end, step)生成一个数组。
Tips3:torch.normal()介绍
返回一个张量,包含从给定参数means,std的离散正态分布中抽取随机数。 均值means是一个张量,包含每个输出元素相关的正态分布的均值。 std是一个张量,包含每个输出元素相关的正态分布的标准差。 均值和标准差的形状不须匹配,但每个张量的元素个数须相同。
参数:
- means (Tensor) – 均值
- std (Tensor) – 标准差
- out (Tensor) – 可选的输出张量
import torch
print(torch.full([3], 0.5))
print(torch.arange(1, 0, -0.2))
torch.normal(mean=torch.full([5], 0.), std=torch.arange(1, 0., -0.2))
tensor([0.5000, 0.5000, 0.5000])
tensor([1.0000, 0.8000, 0.6000, 0.4000, 0.2000])
tensor([ 1.8034, -1.8158, -0.3480, -0.2122, -0.4916])
Full 初始化
上面的例子中我们已经使用了full函数,但是还有几点需要明确。
- full默认的初始化类型是default类型
- 使用torch.full([], num)初始化标量
- 使用torch.full(shape, num)初始化张量,且全部张量的数据均为 num
- 使用full_like(tensor, num)函数快速实现同结构初始化
Arange/Range 初始化
上方 Tips2 已经讲过 arange() 的用法,在此继续补充一下:
- torch.arange(from, to)可以快速生成步长为1的tensor序列(shape==[])
- range()和arange()使用方法完全一样,但arange生成的是 ,range生成的是
Linspace/Logspace 初始化
不多说!直接上代码比较好懂一些
import torch
print(torch.linspace(0, 10, steps=3)) #三等分
print(torch.linspace(0, 10, steps=4)) #四等分
print(torch.logspace(0, 2, steps=2)) #10的0次方到2次方,分为两次迭代完成
print(torch.logspace(1, -1, steps=3)) #10的1次方到-1次方,分为三次迭代完成
print(torch.logspace(1, -1, base=np.e, steps=3)) #更换底数为e
tensor([ 0., 5., 10.])
tensor([ 0.0000, 3.3333, 6.6667, 10.0000])
tensor([ 1., 100.])
tensor([10.0000, 1.0000, 0.1000])
tensor([2.7183, 1.0000, 0.3679])
Ones/Zeros/Eye 初始化
import torch
print(torch.ones(3, 3)) #初始化为1
print(torch.zeros(2, 2)) #初始化为0
print(torch.eye(3, 4)) #初始化对角线为1,其余为0,左上角开始
print(torch.eye(2)) #简写
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
tensor([[0., 0.],
[0., 0.]])
tensor([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.]])
tensor([[1., 0.],
[0., 1.]])
Randperm 初始化
使用 torch.randperm(num) 初始化一个 的Tensor。
num 只能是正整数
import torch
print(torch.randperm(5))
tensor([0, 2, 1, 4, 3])
PyTorch 中的索引与切片
最简单的索引方式
有一 tensor a,直接使用 a[num] 即可索引 a 的第一个维度的第 num-1 个数据。
假设 a = torch.randn(4,3,28,28)。这里的 a.shape 其实是卷积神经网络中的一般Tensor初始化方法。
它的意义是:每次传入4张图片,每张图片有3个通道(RGB),每个通道的x为28像素,y为28像素。
假如我们执行 a[0].shape,那么输出就会是 [3,28,28]。意思是:第一个维度的第0张图片,返回出来的数据表示“这张图片有3个通道,x为28,y为28”。
执行 a[0,0].shape 则同理。会输出 [28,28],表示的是:第一个维度的第0张图片的第0个通道,返回的数据表示“这张图片的第0个通道的图片长宽分别为28,28”。
假设我们现在已经读取出了一些图片,并传入了 tensor a 中。那么我们直接使用 a[0,0,2,4]便可以取得相应的数据。
import torch
a = torch.rand(4,3,28,28)
print(a[0].shape)
print(a[0,0].shape)
print(a[0,0,2,4])
torch.Size([3, 28, 28])
torch.Size([28, 28])
tensor(0.6905)
选择索引
在索引过程中,我们可以使用 :num 表达式进行数据索引。:num 可以理解为一个箭头,指的是 num 之前的数据。如果num是负值,那么 :num 指的就是 num 之后的所有数据。
: = <-->,冒号前后均可以放数字,用法就是上述的拓展。
除了上述的“箭头运算符”,还有一种运算符“步长运算符”(这俩名字我瞎取的)。0:28:2 表示的是从0-28,每隔 2 生成一个对应的数来取值。
具体请看代码
import torch
a = torch.rand(4,3,28,28)
print(a[1:2].shape)
print(a[1:].shape)
print(a[:-3, :, :, :].shape)
print(a[:, :, ::2, 10:11:].shape)
torch.Size([1, 3, 28, 28])
torch.Size([3, 3, 28, 28])
torch.Size([1, 3, 28, 28])
torch.Size([4, 3, 14, 1])
Index 索引
依旧是 tensor a.shape == [4,3,28,28],可以使用 a.index_select(dim, array) 进行索引。dim 表示的是“对第几个维度进行操作,从0开始”,array是一个tensor里面的值表示的是要对这个维度的哪些数据进行索引。
import torch
a = torch.rand(4,3,28,28)
print(a.index_select(0, torch.tensor([0,3])).shape)
print(a.index_select(2, torch.tensor([1, 2])).shape)
print(a.index_select(3, torch.arange(27)).shape)
torch.Size([2, 3, 28, 28])
torch.Size([4, 3, 2, 28])
torch.Size([4, 3, 28, 27])
无语 索引
符号 ... 代表的是“任意”。这个设定挺让人无语的对吧,所以这么记住就好啦。
通过 ... 和上文所述的符号进行组合,可以减少很多代码量。 目前看不大出来,但是当维度一变多,这个功能就显得十分重要了。
import torch
a = torch.rand(4,3,28,28)
print(a[..., 0:28:2].shape)
print(a[:, 1:3, ...].shape)
torch.Size([4, 3, 28, 14])
torch.Size([4, 2, 28, 28])
Mask 索引
使用 torch.mask_select(tensor, mask) 其中 mask = x.ge(0.5) 来筛选 0.5 以上的数据值。
Take 索引
使用 torch.take(src, torch.tensor([m, n])) 来将 src 打平,后筛选 m 到 n 的数据。
以上两种方法不是很常用,因为均会将 Tensor 打平(dim --> 1)
维度变换
维度变换是 PyTorch 中一个很常用且很重要的方面。它可以变换 Tensor 的维度,来达到灵活数据处理的效果。
View/Reshape 变换
在 PyTorch 中, torch.view() 和 torch.reshape() 函数是完全一样的。
在变换中,表示的数据大小是始终不变的。可以使用 prod(array) 来计算可表达的数据个数。
比如,有一个 CNN 的数据传入 Tensor.size = ([4,1,28,28]),那么其表达的数据总和为 prod(Tensor)。
特殊的,如果制定view的其中一个参数为-1,那么会自动推算-1所代表的数值。但是不能同时出现两个-1。
使用 view/reshape 函数可以对其维度个数进行操作:
import torch
a = torch.rand(4,1,28,28)
print(a.shape)
a = a.view(4, 28*28)
print(a.shape)
torch.Size([4, 1, 28, 28])
torch.Size([4, 784])
需要注意的是:数据存储的顺序是非常重要的,需要时刻记住!
当维度变换时,数据的大小如果和变换前不一致,那么就会报错:
import torch
a = torch.rand(4,1,28,28)
a.view(4, 783)
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
/tmp/ipykernel_4737/871776397.py in <module>
2
3 a = torch.rand(4,1,28,28)
----> 4 a.view(4, 783)
RuntimeError: shape '[4, 783]' is invalid for input of size 3136
Unsqueeze 变换
Unsqueeze,字面意思即“不挤压”“展开”。即它的作用是把维度进行展开。
比如Unsqueeze可以把[4,20]展开成为[1,4,20],但是其数据量还是不变的。在前面加了1维度,输出出来仅仅是多了一个[]。
在实际使用中,使用的较多的是链式调用。
import torch
a = torch.rand(4,20)
print(a.shape)
a = a.unsqueeze(0)
print(a.shape)
a = a.unsqueeze(-1)
print(a.shape)
a = a.unsqueeze(2)
print(a.shape)
a = a.unsqueeze(0).unsqueeze(0) #链式调用
print(a.shape)
torch.Size([4, 20])
torch.Size([1, 4, 20])
torch.Size([1, 4, 20, 1])
torch.Size([1, 4, 1, 20, 1])
torch.Size([1, 1, 1, 4, 1, 20, 1])
Squeeze 变换
顾名思义,挤压维度,更简单的理解是删掉一对[]。
只能删除[1]的部分。具体看代码
import torch
b = torch.rand(1,1,1,2,3,4)
print(b.squeeze().shape)
print(b.squeeze(0).shape)
print(b.squeeze(-4).shape)
torch.Size([2, 3, 4])
torch.Size([1, 1, 2, 3, 4])
torch.Size([1, 1, 2, 3, 4])
Expand/Repeat 变换
Expand和Repeat的效果是完全一致的,只不过有实现和效率差异
Expand:数据存储方式。
Repeat:把数据拷贝一遍。
原理:
Expand:在需要的情况下复制数据,效率较高。一般推荐使用Expand。
Repeat:始终复制数据,运行效率较低,不太建议使用。
import torch
a = torch.rand(4,32,14,14)
b = torch.rand(1,32,1,1)
# 原来的shape,dim必须一致(维度数)
print(b.expand(4,32,14,14).shape)
# Repeat函数用法稍微不太一样,内部参数填写的是 要拷贝的次数。
print(b.repeat(4,10,10,10).shape)
torch.Size([4, 32, 14, 14])
torch.Size([4, 320, 10, 10])
矩阵转置
.t() 方法只能接收 dim = 2 的Tensor(二维)。
直接进行调用即可,无需传入任何参数,输出为转置后的内容。
Transpose 维度交换
维度交换需要背诵一些套路:
import torch
a = torch.rand(4,3,32,32) # B W H C
a1 = a.transpose(1,3).contiguous().view(4,3*32*32).view(4,3,32,32) # B C H W
a2 = a.transpose(1,3).contiguous().view(4,3*32*32).view(4,32,32,3).transpose(1,3) # B C W H --> B W H C(再次transpose)
print(torch.all(torch.eq(a, a1)))
print(torch.all(torch.eq(a, a2)))
tensor(False)
tensor(True)
Permute 维度交换
上文提到,Transpose函数会进行矩阵的转置操作,恢复数据相对复杂且反人类。
而且 Transpose 函数也会遇到内存不连续问题,需要使用 contiguous() 函数进行内存连续化操作。
Permute函数则很好的解决了这个问题。
import torch
a = torch.rand(4,3,28,64)
print(a.permute(0,2,3,1).shape) # 填写第x个维度来自哪里
torch.Size([4, 28, 64, 3])
Boardcast 自动扩展
- 这是一个机制,并没有具体的函数去调用,直接进行tensor的加法运算即可。
- 能够进行维度扩展,自动实现。
- 能够节省数据,不会复制data
实现原理:
- 在前面插入1个维度
- 扩展此插入的维度为目标维度
看如下样例:
假设有A = [class,students,scores],我们要把score = [5.0]加入scores中。
一般来说需要使用score.unsqueeze(0).unsqueeze(0).expand_as(A)。
但是使用boardcast就很简洁。
那么boardcast如何使用呢?
- 从最后的维度开始。
- 每一个维度可以指定shape为[1]或[n],其中n为目标运算dim的大小。比如有4门课,我想只给第一门课加分,其他课不变,那么就应该使用{1,0,0,0}。
几种情况:
目标:[4,32,14,14]
过程:[14,14] => [1,1,14,14] => [4,32,14,14]
--
目标:[4,32,14,14]
过程:[2,32,14,14] => Error
解决:[2,32,14,14][0] => [32,14,14] => [1,32,14,14] => [4,32,14,14]
--
目标:[4,32,14,14]
过程:[14,1] => [1,1,14,1] => [4,32,14,14]
import torch as t
a = t.rand(2, 3, 4)
b = t.rand(1)
print((a + b).shape)
torch.Size([2, 3, 4])
拼接与拆分
- Cat
- Stack
- Split
- Chunk
Cat
import torch
a = torch.rand(4,32,8)
b = torch.rand(5,32,8)
print(torch.cat([a,b],dim=0).shape)
c = torch.rand(5,32,8)
d = torch.rand(5,30,8)
print(torch.cat([c,d],dim=1).shape)
torch.Size([9, 32, 8])
torch.Size([5, 62, 8])
Stack
与Cat不同,但效果类似。Cat是把数据直接合并进同一个维度,但是Stack则是创建一个新维度再进行维度的相加。
具体看代码:
import torch
a = torch.rand(4,32,8)
b = torch.rand(4,32,8)
print(torch.stack([a,b], dim=1).shape)
print(torch.stack([a,b], dim=0).shape)
torch.Size([4, 2, 32, 8])
torch.Size([2, 4, 32, 8])
Split
拆分数据
- 根据长度
import torch
# 根据长度
ab = torch.rand(2,32,8)
aa,bb = ab.split(1, dim=0)
print(aa.shape, bb.shape)
abc = torch.rand(3,32,8)
ab,c = abc.split([2,1], dim=0)
print(ab.shape, c.shape)
torch.Size([1, 32, 8]) torch.Size([1, 32, 8])
torch.Size([2, 32, 8]) torch.Size([1, 32, 8])
Chunk
拆分数据
- 根据数量
import torch
# 根据数量
ab = torch.rand(2,32,8)
aa,bb = ab.chunk(2, dim=0)
print(aa.shape, bb.shape)
abc = torch.rand(3,32,8)
ab,c = abc.chunk(2, dim=0)
print(ab.shape, c.shape)
abc = torch.rand(3,32,8)
a,b,c = abc.chunk(3, dim=0)
print(a.shape, b.shape, c.shape)
torch.Size([1, 32, 8]) torch.Size([1, 32, 8])
torch.Size([2, 32, 8]) torch.Size([1, 32, 8])
torch.Size([1, 32, 8]) torch.Size([1, 32, 8]) torch.Size([1, 32, 8])
矩阵的运算
+和-上面在boardcast一节已经介绍完毕。
Matmul 乘法
torch.matmul = @
import torch
a = torch.ones(2,2)
b = torch.Tensor([[3.,3.],[3.,3.]])
print(torch.matmul(a,b))
print(a @ b)
tensor([[6., 6.],
[6., 6.]])
tensor([[6., 6.],
[6., 6.]])
在实战中的例子
import torch
a = torch.rand(4,784)
x = torch.rand(4,784)
w = torch.rand(512,784)
#.t()只适用于2D矩阵,高维矩阵进行转置需要使用transpose()方法。
print((x @ w.t()).shape)
torch.Size([4, 512])
高纬度矩阵相乘
import torch
a = torch.rand(4,3,28,64)
b = torch.rand(4,3,64,32)
print((a @ b).shape)
c = torch.rand(4,1,64,32)
print((a @ c).shape) #第二维度使用boardcast进行了升维操作
torch.Size([4, 3, 28, 32])
torch.Size([4, 3, 28, 32])
Pow(er)
进行Tensor次方运算。
import torch
a = torch.full([2,2], 3.)
print(a.pow(2))
aa = a**2
print(aa)
print(aa.sqrt())
print(aa.rsqrt()) # 平方根的倒数 1/3
print(aa**0.5)
tensor([[9., 9.],
[9., 9.]])
tensor([[9., 9.],
[9., 9.]])
tensor([[3., 3.],
[3., 3.]])
tensor([[0.3333, 0.3333],
[0.3333, 0.3333]])
tensor([[3., 3.],
[3., 3.]])
Log 和 e
import torch
a = torch.exp(torch.ones([2,2])) # 进行e的x次方运算
print(a)
print(torch.log(a))
tensor([[2.7183, 2.7183],
[2.7183, 2.7183]])
tensor([[1., 1.],
[1., 1.]])
近似值
- .floor向下取整
- .ceil向上取整
- .round四舍五入
- .trunc拆分出来整数部分
- .frac拆分出来小数部分
- .clamp小于n的值替换为n
import torch
grad = torch.rand(2,3) * 15
print(grad)
print(grad.clamp(8))
tensor([[ 0.8666, 12.5426, 5.6770],
[ 2.5591, 11.0521, 4.7801]])
tensor([[ 8.0000, 12.5426, 8.0000],
[ 8.0000, 11.0521, 8.0000]])
统计属性
Norm 范数
norm(范数)和normalize(正则化)表达的意思完全不同,注意不要混淆。
注意:norm函数使用的范数是Vector Norm,其1范数定义式为 ,n范数定义式为
norm函数可以指定维度,指定哪个维度,哪个维度便会消失,返回一个tensor包含了范数数据
import torch
a = torch.full([8], 1.)
b = a.view(2,4)
c = a.view(2,2,2)
print(a.norm(1), b.norm(1), c.norm(1))
# 求解2范数,结果应该为8的平方根
print(a.norm(2), b.norm(2), c.norm(2))
print(b.norm(2, dim=1)) #返回的shape是[2],消掉了一个维度
tensor(8.) tensor(8.) tensor(8.)
tensor(2.8284) tensor(2.8284) tensor(2.8284)
tensor([2., 2.])
常用统计函数
- .mean平均值
- .sum求和
- .min最小值
- .max最大值
- .prod累乘
- .argmax最大值的索引位置(.indexOf(max()))
- .argmin最小值的索引位置(.indexOf(min()))
argmin和argmax都会返回把一个tensor打平成[n]的一维tensor的最大、最小值所在索引位置。这样取出来的索引就会丢失原有的维度信息,不利于我们进行数据处理。
为了防止上面所说的问题,在调用argmax和argmin的时候需要进行dim的传入。
import torch
a = torch.rand(4,14,14)
print(a.argmax()) #打平处理得到的数据
print(a.argmax(dim=2)) #第三个维度上的最小的数所在的位置,返回的数据的shape为[4,14]
tensor(10)
tensor([[10, 2, 4, 5, 0, 10, 10, 5, 1, 2, 8, 3, 0, 10],
[ 6, 10, 9, 4, 10, 3, 1, 0, 1, 3, 1, 8, 3, 5],
[ 7, 4, 6, 8, 12, 12, 8, 5, 1, 4, 12, 12, 7, 8],
[ 7, 5, 6, 2, 8, 3, 8, 3, 12, 13, 6, 9, 1, 7]])
Keepdim 用来保持返回的数据的维度信息和元数据一致。
这样说可能比较抽象,具体看例子就很好理解了:
import torch
a = torch.rand(4, 10)
print(a.max(dim=1))
print(a.argmax(dim=1))
# 保留原来的维度信息
print(a.max(dim=1, keepdim=True))
torch.return_types.max(
values=tensor([0.8998, 0.9284, 0.8526, 0.8843]),
indices=tensor([0, 4, 3, 8]))
tensor([0, 4, 3, 8])
torch.return_types.max(
values=tensor([[0.8998],
[0.9284],
[0.8526],
[0.8843]]),
indices=tensor([[0],
[4],
[3],
[8]]))
Top-K
将tensor中的数据进行筛选,返回前k个最大的数据值,以及数据值的索引tensor。
如果要求 Bottom-K (倒数最大),那么在topk函数内指定一下 largest=False 即可。
import torch
a = torch.rand(4, 10)
print(a.topk(3, dim=1))
print(a.topk(3, dim=1, largest=False))
torch.return_types.topk(
values=tensor([[0.8542, 0.7850, 0.7199],
[0.8396, 0.6456, 0.5934],
[0.9925, 0.9619, 0.6878],
[0.8499, 0.7984, 0.6508]]),
indices=tensor([[9, 2, 5],
[3, 8, 1],
[1, 5, 8],
[0, 1, 8]]))
torch.return_types.topk(
values=tensor([[0.0270, 0.1211, 0.3108],
[0.0186, 0.0696, 0.1245],
[0.0227, 0.1103, 0.1829],
[0.0763, 0.0797, 0.3132]]),
indices=tensor([[4, 3, 7],
[4, 0, 6],
[4, 2, 0],
[6, 9, 7]]))
K-th
同上,只不过返回的是第 k “小” 的量,基本用法与上面 Top-K 差不多。
注意,是 “第 K 小” 的,不是第 K 大的!
(有点反人类对吧,没办法pwq)
import torch
a = torch.rand(4, 10)
print(a.kthvalue(4, dim=1))
torch.return_types.kthvalue(
values=tensor([0.4978, 0.2375, 0.1363, 0.4208]),
indices=tensor([7, 5, 6, 3]))
Compare 比较运算
正如 Python 中定义的那样,PyTorch的基本数据类型同样支持各种运算符:
- >
- >=
- <
- <=
- !=
- ==
但值得注意的是,PyTorch中并不含有Boolean型数据类型,所以返回的值中一般都是使用 1 表示 True,0 表示 False。
其中有一个不太一样的,但同样重要的函数 torch.eq() 和 torch.equal() 方法,返回的就是 Boolean 型的值。正因如此,大家使用 eq() 方法要多于上述的运算符。
但是eq和equal还是存在区别的,eq方法返回的是一个Tensor,equal方法返回的就是单个的Python型Boolean数据类型。
import torch
a = torch.rand(4, 5)
b = torch.rand(4, 5)
print(torch.equal(a, b))
print(torch.eq(a,b))
print(a > b)
False
tensor([[False, False, False, False, False],
[False, False, False, False, False],
[False, False, False, False, False],
[False, False, False, False, False]])
tensor([[False, True, True, False, True],
[ True, True, False, False, False],
[ True, True, False, False, True],
[False, False, False, True, True]])
高阶 OP
- Where
- Gather
Where
其中 condition 决定了返回的数据Tensor中值的来源(x或y)
import torch
cond = torch.rand(2,2)
a = torch.ones(2,2)
b = torch.zeros(2,2)
# 如果cond的值大于0.5,那么就从a中取值,否则从b中取值。
print(cond)
print(torch.where(cond > 0.5, a, b))
tensor([[0.1341, 0.6901],
[0.7860, 0.5705]])
tensor([[0., 1.],
[1., 1.]])
Gather
Gather 其实就是一个查表的过程。
input其实就是一张表,index就是查表的项。输出的Tensor就是index于input中对应的值。
可能不是很好理解,接下来看具体的案例:
import torch
prob = torch.randn(4,10)
idx = prob.topk(dim=1, k=3)
# 只输出数据索引值,方便与下面进行对照
print(idx[1])
# 生成Label(也可以理解为字典)
label = torch.arange(10) + 100
print(label)
# 进行 Gather 操作,讲所有数字+100后输出
print(torch.gather(label.expand_as(prob), dim=1, index=idx[1]))
tensor([[2, 1, 9],
[8, 4, 5],
[9, 7, 6],
[1, 6, 9]])
tensor([100, 101, 102, 103, 104, 105, 106, 107, 108, 109])
tensor([[102, 101, 109],
[108, 104, 105],
[109, 107, 106],
[101, 106, 109]])
什么是梯度
导数是最接近梯度的概念。 我们高中学过的二维求导,是在平面内进行的。但是将这个概念拓展一下,导数其实是在任意维度的空间内都可以运算求解的。
导数其实是某一点在某一指定方向的变化趋势,是一个标量。
偏微分这个概念其实是导数的严格化。导数可以求任意方向的变化量,但是偏微分要求所求的必须是「空间中某一坐标轴方向」的导数。比如有下面一个三维空间方程:
看起来就是对于一个自变量z求x和y的偏导数。概念并不是很难。
那么什么是梯度呢? 梯度就是所有偏微分的向量。其定义式为:
简单套一下定义,上面那个方程的梯度就是
且在不同的x和y处,梯度也不同:
梯度更直观的理解
如果把空间内磁铁的磁场强度方程代入求解梯度的话,并把梯度使用箭头表示(因为是向量所以可以这样做),那么箭头就是一堆小磁针的样子(只不过小磁针的长度反映了变化趋势的大小)。
如何在梯度的帮助找到极小值解
由以上公式进行 的移动,进而找到极小值。
如果再进行一个类比的话,那么你可以理解为函数定义域内有一个盆地。盆地上也是坑坑洼洼的,起伏较大。
把一颗小球从某个地方(随机初始化点)丢下去,那么经过一段时间,小球总会到达那个比较低的点(可能是全局最优解也可能是局部最优解)就停住。小球没有惯性,其移动的轨迹就是梯度下降模拟的轨迹。
凸函数
对于一个函数(不限坐标维度),对于函数上任意两点,拉一条线段,线段的中点所对应的值始终小于前面两点,那么这个函数就被称为凸函数。
继续上一节的类比,如果这个函数是凸函数,那么丢个小球它始终会前往最低点,也就是全局最优解。
但是现实生活中几乎不会出现凸函数。一般都是带有几个盆地的图形。而随机初始化的话,可能就会陷入到局部最优解(不是最低的盆地)中去。
ResNet
ResNet是一种优化算法,假设一个起伏很大,有很多坑的地形,丢小球就很难找到最低的那个盆地。但是通过ResNet则可以进行一些优化
- 加速搜索效率
- 更容易找到全局最优解
那么是如何实现的呢?暂且理解为在神经网络旁边加了一条支路。
Saddle Point 鞍点
假设有如下函数
函数图像在下方,从 x = 0 这条直线看过去的话,那么就会得到一条抛物线。
试想一下,如果在 x = n, y = 0这一点上丢小球,小球最终会在哪儿?答案是在(0,0)这一点。因为根据梯度下降,整条抛物线上的最低点就是这个点,从而导致小球不能前往最低方向。
所以在实战中,遇到比较多的、危害最大的反而不是「局部最优解」,而是「鞍点」。
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
auto_add_to_figure=False
fig = plt.figure()
ax = Axes3D(fig)
x = np.arange(-1, 1, 0.01)
y = np.arange(-1, 1, 0.01)
X, Y = np.meshgrid(x,y)
Z = X**2 - Y**2
plt.xlabel('x')
plt.ylabel('y')
ax.plot_surface(X,Y,Z, cmap="rainbow")
plt.show()
/tmp/ipykernel_41562/2544617408.py:6: MatplotlibDeprecationWarning: Axes3D(fig) adding itself to the figure is deprecated since 3.4. Pass the keyword argument auto_add_to_figure=False and use fig.add_axes(ax) to suppress this warning. The default value of auto_add_to_figure will change to False in mpl3.5 and True values will no longer work in 3.6. This is consistent with other Axes classes.
ax = Axes3D(fig)
效率优化
- 初始值
- 学习率(Learning Rate)
- 动量(Escape Minima)
前两个前面已经介绍完毕。我们着重介绍一下动量(EM)。
正如前面鞍点那一节中提到的,小球并不具备惯性。而动量就是给小球加上惯性。假如有一个「W」形状的地形,在最左边的一条直线上设为初始点。那么到达第一个弯处就会停止。但实际上更低的弯在第二个弯处,那么使用动量就会使小球冲过中间的山峰,到达最低点。
常见函数梯度
套用上面梯度的定义,对于一维,导数和梯度是大致相同的。 假设有一函数 ,将这个函数理解为神经网络的单元的话,那么我们要优化的目标就是 w 和 b 的取值。
先求偏微分:
提升一下难度,正如前面的loss函数一样,在这里定义均方差的公式为
偏微分求解:
那么求解了偏微分后应该怎么使用呢?
把 换成 即可,b 同理。你问我怎么换?
也就是直接把当前 w 的值代入偏微分求得的公式直接计算即可。
如果你把这个过程降维成一维,那么就很容易理解了,就是导数嘛,然后通过趋势进行自我更新
激活函数及其梯度
什么叫做激活函数
很久之前生物学家在研究青蛙的神经元的时候,发现青蛙的神经元并不是直接进行输出,而是通过某种条件分歧之后再决定要不要输出。
而中间的这个条件进行数学量化之后得到的函数我们就称为「激活函数」。
科学家们为了让激活函数可以使用梯度下降进行优化,所以设计成了光滑的可导函数。非常著名且重要的 Sigmoid 函数就是连续且可导的。
Sigmoid 函数
Sigmoid 函数的解析式为:
图像为:
import matplotlib.pyplot as plt
import numpy as np
def sigmoid(x):
return 1. / (1. + np.exp(-x))
def plot_sigmoid():
x = np.arange(-8,8, 0.01)
y = sigmoid(x)
plt.plot(x,y)
plt.show()
plot_sigmoid()
这个函数可以把 的取值压缩到 内。
通过求导可得
正因 Sigmoid 函数有这种特点,所以我们在处理数值较大的数据的时候经常使用 Sigmoid 函数进行数据压缩处理,比如 RGB 通道的0-255就可以使用 Sigmoid 函数压缩到 0-1 的范围内。
import torch
Z = torch.linspace(-100, 100, 10)
print(Z)
print(torch.sigmoid(Z))
tensor([-100.0000, -77.7778, -55.5556, -33.3333, -11.1111, 11.1111,
33.3333, 55.5556, 77.7778, 100.0000])
tensor([0.0000e+00, 1.6655e-34, 7.4564e-25, 3.3382e-15, 1.4945e-05, 9.9999e-01,
1.0000e+00, 1.0000e+00, 1.0000e+00, 1.0000e+00])
Tanh 函数
Tanh是另一种激活函数,其解析式为:
导数为:
图像为:
import matplotlib.pyplot as plt
import numpy as np
def tanh(x):
return (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x))
def plot_tanh():
x = np.arange(-8,8, 0.01)
y = tanh(x)
plt.plot(x,y)
plt.show()
plot_tanh()
ReLU 函数
可以说是现代深度学习的基石。上文已经介绍,不再赘述。
但值得注意的是,在实战中,一般优先使用ReLU函数而不是其他函数,当ReLU函数不能够胜任优化工作的时候再考虑其他函数。
损失函数()及其梯度
- Mean Squared Error 均方差(MSE)
- Cross Entropy Loss(CEL)
CEL在此不做详细介绍,后文会讲解。
先来了解MSE
MSE
定义式(三种方法):
MSE 的求导
以下是使用autograde自动求导:
torch.set_default_tensor_type(torch.FloatTensor)
x = torch.ones(1)
w = torch.full([1], 2.)
b = torch.full([1], 1.).requires_grad_()
# 第一个参数为预测值,第二个参数为实际值(直接把函数表达式丢进去就好)
mse = F.mse_loss(torch.ones(1), x*w + b)
print(mse)
# 下方语句报错,因为w没有设置可导属性
# print(torch.autograd.grad(mse, [w]))
# 设置可导属性,注意函数名称
w.requires_grad_()
# 下方语句报错,因为mse内的w还是不可导的
# torch.autograd.grad(mse, [w])
mse = F.mse_loss(torch.ones(1), x*w + b)
# 对 w 和 b 同时求导
print(torch.autograd.grad(mse, [w, b]))
tensor(4., grad_fn=<MseLossBackward>)
(tensor([4.]), tensor([4.]))
使用loss.backward进行求解梯度
x = torch.ones(1)
w = torch.full([1], 2., requires_grad=True)
b = torch.full([1], 1., requires_grad=True)
mse = F.mse_loss(torch.ones(1), x*w + b)
mse.backward()
print(w.grad, b.grad)
tensor([4.]) tensor([4.])
Softmax
Softmax函数是十分常用的函数之一。它的作用是把一组数据压缩为0-1范围内,且加和为1的数据。
感知机
概念及命名
一个层可以有很多个子层构成,比如在如图所示的层中,有1个输入层接收n个数据值,(隐藏层:1个求和层,1个激活函数层,1个loss函数层),1个输出层。
输入:第0层,其中的元素使用 来表示第0到n-1个节点。其中上标的0表示的是第0层。
权重:使用 其中n上标表示的是第n个隐藏层,i表示的是前面链接的x的n,j表示的是后面链接的节点的n。
求和层:对x*w进行求和,并输出。如果有多个求和节点那么其表示方法为 其中n表示的是第n个隐藏层,i表示的是这个隐藏层中本节点是第i个节点。
激活函数层:同上求和层。激活函数输出的值被称作 大写的 。
单层感知机的推导公式
具体推导公式太难打了(懒),直接丢结论
对于上图所示的 loss 层,都有如下定义:
对w(权重)的求导结果为:
import torch
from torch.nn import functional as F
x = torch.randn(1,10)
w = torch.randn(1,10, requires_grad=True)
o = torch.sigmoid(x@w.t())
E = F.mse_loss(torch.ones(1,1), o)
E.backward()
# 就是上面讲述的过程
print(w.grad)
tensor([[-0.1208, -0.1442, 0.2102, -0.2024, 0.0094, -0.1730, -0.1135, -0.1622,
0.0716, 0.1055]])
多输出感知机
上一节所推导的是单层感知机。将一些层进行数量的拓展后,就得到了「多层感知机」。
对w(权重)的求导结果为:
import torch
from torch.nn import functional as F
x = torch.randn(1,10)
w = torch.randn(3,10, requires_grad=True)
o = torch.sigmoid(x@w.t())
# 转置后相乘就变成了1,3的矩阵,对这个矩阵o进行mse求loss
E = F.mse_loss(torch.ones(1,3), o)
E.backward()
print(w.grad)
tensor([[-2.3309e-04, -5.9936e-05, -8.9145e-04, 6.1908e-04, 7.1413e-04,
1.2533e-03, -3.8722e-04, 6.8182e-04, 5.2380e-04, -4.3392e-04],
[-1.0539e-02, -2.7100e-03, -4.0307e-02, 2.7992e-02, 3.2289e-02,
5.6667e-02, -1.7508e-02, 3.0829e-02, 2.3684e-02, -1.9620e-02],
[-3.1510e-02, -8.1024e-03, -1.2051e-01, 8.3690e-02, 9.6538e-02,
1.6942e-01, -5.2346e-02, 9.2170e-02, 7.0809e-02, -5.8659e-02]])
链式法则
链式法则十分重要。它可以把最后一层的误差(Loss函数求得)然后反向一层一层输出到权重(w)上去,从而达到逐步迭代求得最优解的效果。
链式法则其实比较简单,它的数学定义是:
链式法则的目的主要是求解复合函数的导数。它也可以去简化普通的求导过程,让人们易于理解。
例子
假设有如下函数定义: 很简单吧,就是先关于y2对y1求导,然后再乘上y1对w2求导。 但是如果要对原式进行直接展开求导的话,就会变得复杂一些。尤其是复杂函数以及带有激活函数的函数来说更为复杂。
现在对下图所示的的层进行链式法则求解
import torch
x = torch.tensor(1.)
w1 = torch.tensor(2., requires_grad=True)
b1 = torch.tensor(1.)
w2 = torch.tensor(2., requires_grad=True)
b2 = torch.tensor(1.)
y1 = x*w1 + b1
y2 = y1*w2 + b2
# 目的是求解y2对w1的导数。
# 先手动使用链式法则
dy2_dy1 = torch.autograd.grad(y2, [y1], retain_graph=True)[0]
dy1_dw1 = torch.autograd.grad(y1, [w1], retain_graph=True)[0]
print(dy2_dy1 * dy1_dw1)
# 再自动使用链式法则求导
dy2_dw1 = torch.autograd.grad(y2, [w1], retain_graph=True)[0]
print(dy2_dw1)
tensor(2.)
tensor(2.)
MLP(Multi-Layer Perceptron) 反向传播
DL最后的一些公式,再坚持坚持!
我们将上面的多层感知机样例再次推广,在x前面的再加一层,然后拿个板子挡住,不告诉你前面的输入是怎样得来的。我们令这里的输入为 共N个。
同理上面的多层感知机求导过程,我们可以得到如下的公式:
我们可以发现,不管这个网络怎么变化,有一个地方的公式是不变的:
为了简化表示,我们可以使用我们使用 来取代 。
进阶求导
假设有如下图所示的一个网络,有 于 有关,但中间的关系比较复杂。应用链式法则进行求导操作。
首先写出来 E 的表达式,然后进行数据代入(细节请自行推导)
综上所述,E对于跨层变量的求导的表达式为
公式聚合
根据上文的定义,我们其实可以不用关心前面的数据是怎么来的。我们这里设前面的数据来源为 ,那么当前的 E 对于 的导数就是
那么有结论总结如下:
对于一个输出节点,总有 对于一个隐藏层节点,总有
所以在实际使用中,我们只需要通过分步求解(也可能是递归求解)就能找到这整个神经网络中E与各W的关系。
而这一过程是通过「最后已知输出」来实现的,是个逆向求解的过程。经过求解就可以去更新各W的拟合值。所以它的名字叫「反向传播(MLP)」,是不是非常生动形象?
2D 函数优化实例
假设有如下函数: 绘制其图像和等高线地形图可以发现,这个图形类似有四个角,四个碗的图形。
这个图形通常是用来检测算法的优越性的。此函数的四个最小解都在 Z = 0,是个比较特殊的函数。
已知上面的几组数据,使用梯度下降的方法求解极值点,根据拟合出来的数据与标准答案进行比较之后就可以了解这个方法的优劣。
下面进行模拟实战一下
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
auto_add_to_figure=False
fig = plt.figure()
ax = Axes3D(fig)
x = np.arange(-6, 6, 0.1)
y = np.arange(-6, 6, 0.1)
X, Y = np.meshgrid(x,y)
Z = (X**2 + Y - 11)*(X**2 + Y - 11) + (X + Y**2 - 7)*(X + Y**2 - 7)
plt.xlabel('x')
plt.ylabel('y')
ax.plot_surface(X,Y,Z, cmap="rainbow")
plt.show()
plt.figure(figsize=(12,12))
plt.contourf(X,Y,Z, 128)
plt.contour(X,Y,Z, 128, colors='black')
plt.show()
/tmp/ipykernel_4012/826876748.py:6: MatplotlibDeprecationWarning: Axes3D(fig) adding itself to the figure is deprecated since 3.4. Pass the keyword argument auto_add_to_figure=False and use fig.add_axes(ax) to suppress this warning. The default value of auto_add_to_figure will change to False in mpl3.5 and True values will no longer work in 3.6. This is consistent with other Axes classes.
ax = Axes3D(fig)
# 梯度下降求解极小值点
import torch
def himmelblau(x):
return (x[0] ** 2 + x[1] - 11) ** 2 + (x[0] + x[1] ** 2 - 7) ** 2
x = torch.tensor([0.,0.],requires_grad=True)
# Adam 上文提到过,是带有动量(惯性)的梯度下降
optimizer = torch.optim.Adam([x], lr=0.001)
for step in range(20000):
#预测值
pred = himmelblau(x)
# 梯度初始化为0
optimizer.zero_grad()
pred.backward()
optimizer.step()
if step % 2000 == 0:
print('step {}: x = {}, f(x) = {}'.format(step, x.tolist(), pred.item()))
step 0: x = [0.0009999999310821295, 0.0009999999310821295], f(x) = 170.0
step 2000: x = [2.3331809043884277, 1.9540694952011108], f(x) = 13.73090934753418
step 4000: x = [2.9820079803466797, 2.0270984172821045], f(x) = 0.014858869835734367
step 6000: x = [2.999983549118042, 2.0000221729278564], f(x) = 1.1074007488787174e-08
step 8000: x = [2.9999938011169434, 2.0000083446502686], f(x) = 1.5572823031106964e-09
step 10000: x = [2.999997854232788, 2.000002861022949], f(x) = 1.8189894035458565e-10
step 12000: x = [2.9999992847442627, 2.0000009536743164], f(x) = 1.6370904631912708e-11
step 14000: x = [2.999999761581421, 2.000000238418579], f(x) = 1.8189894035458565e-12
step 16000: x = [3.0, 2.0], f(x) = 0.0
step 18000: x = [3.0, 2.0], f(x) = 0.0
Logistic Regression
之前很多课程都自觉不自觉用到了 Logistic Regression 的内容,只是没有这个具体的概念。
- 这里的 Logistic 指的是 Sigmoid 函数。
对于我们一般的线性函数,比如 就可以被称作 Regression,Regression一般是指连续值。
而对于 ,外边是一层Sigmoid函数,那么就可以被称为 Classification。因为它类似一个分类问题。上文已经绘制出了 Sigmoid 函数的图像,只有在0附近的一些点才不区域0或1,其他大部分的值都是在0 1附近分布的。
Regression 与 Classification
Regression 问题旨在解决目标为 的问题。
即这个问题的最优解就是「预测值=实际值」。换句话说,就是求得损失函数 最小。
Classification 问题想要解决的是对于一个 ,求得最接近「真实可能性」 的情况。
- 其中 函数输出的是「可能性」。
为什么会出现这种分类?
Regression 问题上文已经提到过很多。它的原理是通过梯度下降和反向传播进行优化 w 和 b 的值来达到极小值点从而求得最优解。
但是对于一个Classification问题,其「准确率函数」定义为 的话,那么我们对猫和狗进行分类,可能就会出现某次预测的概率为 0.4,正确值为1,使用二分acc定义的二分预测,0.4 预测就是 0。
下面是两种使用Regression处理Classification问题的缺陷:
- (1)但是我们通过梯度下降算法将会把 0.4 优化成 0.45,但是对于上面的分类问题损失函数 acc 来说,0.45 和 0.4 的acc值是一样的。这就造成了梯度下降失败,程序会一直卡在这里空跑,此时的梯度会降为0,陷入了局部最优解的陷阱。
- (2)我们通过梯度下降把 0.4 优化成 0.51,假如说对于一个acc一共有5组数据,多了一组数据预测正确将会造成acc的值上升0.2,就会造成 w 优化一丁点,acc就会造成很大的浮动。我们一般称这种现象叫「梯度爆炸」,loss 函数在此是不连续的。
为什么叫 Logistic Regression?
上文提到,Sigmoid 函数一般是用来处理分类问题(Classification)的,但是大家普遍的叫它 Logistic Regression。为什么会出现这种情况?
原因有二,其一是历史原因,大家都这样叫过来了所以就延续了这个传统。正如 F 在PyTorch中指的就是 torch.nn 包里的 Functional 类一样。
第二个比较重要的原因是 Logistic Regression 的损失函数的定义。它定义的损失函数不是上文提到的acc函数,也就削弱了上文的两种缺点。比如一个值 0.6 ,你要把它往 1 优化,梯度下降完全可以做到,只要定义的损失函数是 Regression 的就可以。
正如上文所说,我们一般把使用 MSE 的称为 Regression,把使用 Cross Entropy 的称为 Classification。
Cross Entropy 交叉熵
Entropy 的翻译叫做「熵」,也叫「不确定性」。其在信息学上的数学定义是
更接地气点,可以理解为「惊喜度」,熵越高,越稳定,越不容易出现「惊喜」。
具体请看下面的例子
import torch
def Entropy(a):
return -(a*torch.log2(a)).sum()
a = torch.full([4], 1/4)
print(Entropy(a))
b = torch.tensor([0.1,0.1,0.1,0.7])
print(Entropy(b))
c = torch.tensor([0.0001,0.0001,0.0001,0.999])
print(Entropy(c))
tensor(2.)
tensor(1.3568)
tensor(0.0054)
Cross Entropy
与Entropy定义不同,Cross Entropy 的函数(H)的定义式为
其中 指的是「散度」,英文为 KL Divergence,可以衡量两个分部的散开程度。
当两个函数重叠部分很小的时候,那么散度就会很高,相反,如果是两个完全相同的函数,那么散度就为0。
特殊的,如果P = Q,那么Cross Entropy = Entropy。因为 P=Q ,所以散度 = 0
在one-hot编码中,易得 ,所以 ,这就造成了在优化one-hot编码类型时,就直接优化于 p q 的离散度,会朝着离散度为0的方向进行优化。这正是我们优化的目标。
Binary Classification 二分类问题
对于猫狗分类问题,套用上方 Cross Entropy 介绍我们可以得到如下数学公式: 很容易可以知道,对于二分类问题来说,不是猫就是狗,且猫狗加起来的概率为1。所以我们令原始数据 ,预测数据 。然后得到表达式如下
正如上面所讲到的,P是原始数据,Q是预测数据。所以我们优化的方向就是P Q相差最小。
我们将上面的公式定义为二分类问题的通用公式。
使用 Cross Entropy 会使得在处理分类问题的时候梯度收敛更加快速,比MSE的性能要好一些。
但是MSE也可以处理Classification问题,正如 Logistic Regression 所使用的那样。
多分类问题实战
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
batch_size=200
learning_rate=0.01
epochs=10
train_loader = torch.utils.data.DataLoader(
datasets.MNIST('../data', train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=batch_size, shuffle=True)
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('../data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=batch_size, shuffle=True)
w1, b1 = torch.randn(200, 784, requires_grad=True),\
torch.zeros(200, requires_grad=True)
w2, b2 = torch.randn(200, 200, requires_grad=True),\
torch.zeros(200, requires_grad=True)
w3, b3 = torch.randn(10, 200, requires_grad=True),\
torch.zeros(10, requires_grad=True)
# 初始化至关重要,没有这三行初始化将会造成梯度不更新等问题。
torch.nn.init.kaiming_normal_(w1)
torch.nn.init.kaiming_normal_(w2)
torch.nn.init.kaiming_normal_(w3)
# 降维并输出目标
def forward(x):
x = x@w1.t() + b1
x = F.relu(x)
x = x@w2.t() + b2
x = F.relu(x)
x = x@w3.t() + b3
x = F.relu(x)
return x
optimizer = optim.SGD([w1, b1, w2, b2, w3, b3], lr=learning_rate)
criteon = nn.CrossEntropyLoss()
for epoch in range(epochs):
for batch_idx, (data, target) in enumerate(train_loader):
data = data.view(-1, 28*28)
logits = forward(data)
# 通过当前的wb进行预测,然后将预测结果和目标结果传入loss中进行计算
loss = criteon(logits, target)
# 梯度参数初始化为0
optimizer.zero_grad()
# 计算梯度信息
loss.backward()
# print(w1.grad.norm(), w2.grad.norm())
# 执行更新所有参数
optimizer.step()
if batch_idx % 100 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
test_loss = 0
correct = 0
for data, target in test_loader:
data = data.view(-1, 28 * 28)
logits = forward(data)
test_loss += criteon(logits, target).item()
pred = logits.data.max(1)[1]
correct += pred.eq(target.data).sum()
test_loss /= len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))
/usr/lib/python3.9/site-packages/torchvision/datasets/mnist.py:498: UserWarning: The given NumPy array is not writeable, and PyTorch does not support non-writeable tensors. This means you can write to the underlying (supposedly non-writeable) NumPy array using the tensor. You may want to copy the array to protect its data or make it writeable before converting it to a tensor. This type of warning will be suppressed for the rest of this program. (Triggered internally at /build/python-pytorch/src/pytorch-1.9.0-cuda/torch/csrc/utils/tensor_numpy.cpp:174.)
return torch.from_numpy(parsed.astype(m[2], copy=False)).view(*s)
/usr/lib/python3.9/site-packages/torch/autograd/__init__.py:147: UserWarning: CUDA initialization: CUDA unknown error - this may be due to an incorrectly set up environment, e.g. changing env variable CUDA_VISIBLE_DEVICES after program start. Setting the available devices to be zero. (Triggered internally at /build/python-pytorch/src/pytorch-1.9.0-cuda/c10/cuda/CUDAFunctions.cpp:115.)
Variable._execution_engine.run_backward(
Train Epoch: 0 [0/60000 (0%)] Loss: 2.835827
Train Epoch: 0 [20000/60000 (33%)] Loss: 1.012647
Train Epoch: 0 [40000/60000 (67%)] Loss: 0.785662
Test set: Average loss: 0.0020, Accuracy: 8865/10000 (89%)
Train Epoch: 1 [0/60000 (0%)] Loss: 0.381609
Train Epoch: 1 [20000/60000 (33%)] Loss: 0.430275
Train Epoch: 1 [40000/60000 (67%)] Loss: 0.271996
Test set: Average loss: 0.0014, Accuracy: 9186/10000 (92%)
Train Epoch: 2 [0/60000 (0%)] Loss: 0.291730
Train Epoch: 2 [20000/60000 (33%)] Loss: 0.310789
Train Epoch: 2 [40000/60000 (67%)] Loss: 0.279571
Test set: Average loss: 0.0012, Accuracy: 9310/10000 (93%)
Train Epoch: 3 [0/60000 (0%)] Loss: 0.146089
Train Epoch: 3 [20000/60000 (33%)] Loss: 0.351250
Train Epoch: 3 [40000/60000 (67%)] Loss: 0.197446
Test set: Average loss: 0.0011, Accuracy: 9352/10000 (94%)
Train Epoch: 4 [0/60000 (0%)] Loss: 0.284456
Train Epoch: 4 [20000/60000 (33%)] Loss: 0.139722
Train Epoch: 4 [40000/60000 (67%)] Loss: 0.282829
Test set: Average loss: 0.0010, Accuracy: 9414/10000 (94%)
Train Epoch: 5 [0/60000 (0%)] Loss: 0.220984
Train Epoch: 5 [20000/60000 (33%)] Loss: 0.149520
Train Epoch: 5 [40000/60000 (67%)] Loss: 0.151711
Test set: Average loss: 0.0009, Accuracy: 9458/10000 (95%)
Train Epoch: 6 [0/60000 (0%)] Loss: 0.204040
Train Epoch: 6 [20000/60000 (33%)] Loss: 0.209264
Train Epoch: 6 [40000/60000 (67%)] Loss: 0.153245
Test set: Average loss: 0.0009, Accuracy: 9483/10000 (95%)
Train Epoch: 7 [0/60000 (0%)] Loss: 0.167720
Train Epoch: 7 [20000/60000 (33%)] Loss: 0.135129
Train Epoch: 7 [40000/60000 (67%)] Loss: 0.173989
Test set: Average loss: 0.0008, Accuracy: 9504/10000 (95%)
Train Epoch: 8 [0/60000 (0%)] Loss: 0.071857
Train Epoch: 8 [20000/60000 (33%)] Loss: 0.196765
Train Epoch: 8 [40000/60000 (67%)] Loss: 0.133666
Test set: Average loss: 0.0008, Accuracy: 9534/10000 (95%)
Train Epoch: 9 [0/60000 (0%)] Loss: 0.222232
Train Epoch: 9 [20000/60000 (33%)] Loss: 0.128276
Train Epoch: 9 [40000/60000 (67%)] Loss: 0.097196
Test set: Average loss: 0.0008, Accuracy: 9558/10000 (96%)
激活函数
ReLU函数有很多优点,其中最重要的两个优点就是几乎可以避免梯度爆炸和梯度离散的情况。因为它在大于0的时候导数为1,很好的保留了原信息且计算简单,不会出现倍增,倍减的现象。
Leaky ReLU
但是ReLU还是有几率会出现梯度离散的情况。我们为了解决这一问题,我们引入「Leaky ReLU」 泄露ReLU函数。 泄露 Leaky ReLU 函数为了弥补在小于0处梯度离散的情况,我们可以令函数的图像变成下面这样:
如何使用呢?直接把在代码中把ReLU改成LeakyReLU即可。具体请查看官方文档,倾斜角度也是可以更改的。
SELU
SELU函数的数学定义为: 其函数大概长这样:
但是实际中并不是很常用
SoftPlus
函数定义式为 图像与上面的十分类似。
GPU加速
PyTorch 目前切换设备是比较方便的,想要在devices之间迁移,只需要使用 .to(device) 函数即可实现。
import torch
device = torch.device("cuda:0")
a = torch.full([1], 456).to(device)
测试
我们在Train的时候也要做一些Test。
如下图片,上面那部分是训练准确率,下面那部分是Loss函数的值。蓝色的线是理想情况下的训练图线;橙色是非理想情况下的训练图线。
可以发现,在理想情况下loss下降acc上升是十分舒适的。但是橙色的曲线是怎么来的呢?
比较直观的解释就是,深度学习一开始确实学到了本质的东西并加以优化(橙色前半段稳定优化),但是随着学的东西变多了,本质的特征反而更难以提取,也就是说「只学到了皮毛」,就会导致学习根基不稳,很难做到「举一反三」,造成后续学习的波动较大,发挥不稳定。
我们称上面橙色线所示的情况为「Over Fitting」 过度拟合
上面这种说法很像我们人类学习一种技能时的状态,很容易就能理解。
那么如何进行 Validation(Test)呢?
Validation(Test)
准确度 Acc 函数测试: 在分类问题中,我们直接对 CE(Cross Entropy)输出的结果进行 argmax 操作。如手写数字识别问题中,通过argmax获得最大的值的位置,然后对比原数据中最大的值的位置,如果相同则记为有效判断。
那么准确率函数 Acc 的定义就是 Acc = 预测准确个数 / 总个数。在上文也提到过。
import torch
# 预测数据
pred_label = torch.tensor([9,5,4,1])
# 真实数据
label = torch.Tensor([9,1,2,1])
correct = torch.eq(pred_label, label)
print(correct)
# 通过sum函数计算 Acc 的值
print(correct.sum().float().item() / 4)
tensor([ True, False, False, True])
0.5
Visdom 可视化
上文提到,如果想要认为检测训练情况,可以使用 Acc 函数进行实时输出而避免陷入到 over-fitting 中。
但是这样毕竟不是很直观,我们能否使用一种可视化的方式进行检测?
答案是肯定的。我们可以使用 pip install tensorboardX进行包的安装。
!pip install tensorboardX
Defaulting to user installation because normal site-packages is not writeable
Collecting tensorboardX
Downloading tensorboardX-2.4-py2.py3-none-any.whl (124 kB)
[K |████████████████████████████████| 124 kB 510 kB/s
[?25hRequirement already satisfied: numpy in /usr/lib/python3.9/site-packages (from tensorboardX) (1.21.2)
Collecting protobuf>=3.8.0
Downloading protobuf-3.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.1 MB)
[K |████████████████████████████████| 1.1 MB 3.1 MB/s
[?25hInstalling collected packages: protobuf, tensorboardX
Successfully installed protobuf-3.18.0 tensorboardX-2.4
但是tensorbaord在使用过程中比较别扭。它需要传入NumPy类型的数据。
我们在日常跑训练的时候一般都是跑在GPU上的,所以要使用tensorboard进行可视化,那么就要执行 .cpu().data.nupy()来先转化到cpu上再转换为numpy数据。所以一般不常用tensorboard进行可视化。
我们一般使用的可视化是 Visdom 可视化。它和tensorboard是不太一样的。
它与tensorboard相比有如下优点:
- 不占用本地空间存储训练数据
- 刷新速度快
- 接收原生pytorch数据
- 窗口效率非常高,可以任意布局
- 内容更加密集
- 可以同时监听多个类型
使用pip install visdom安装visdom
!pip install visdom
如果Visdom在使用过程中出现问题,那么可以尝试去 GitHub clone 一下仓库并从源代码安装。
如果已经安装完毕,可以使用 python -m visdom.server开启Visdom服务器进程。
!python3 -m visdom.server
/home/aieson/.local/lib/python3.9/site-packages/visdom/server.py:39: DeprecationWarning: zmq.eventloop.ioloop is deprecated in pyzmq 17. pyzmq now works with default tornado and asyncio eventloops.
ioloop.install() # Needs to happen before any tornado imports!
Checking for scripts.
It's Alive!
INFO:root:Application Started
You can navigate to http://localhost:8097
如果输出了上面这种信息,那么就说明Visdom已经开启成功!
接下来进行实际上手;画一条曲线
from visdom import Visdom
viz = Visdom()
# 第一个参数是 y1、y2... 第二个参数是 x,win指的是窗口的「字符ID」,最后一个opts中的dict指定的title就是窗口的标题。
viz.line([[0., [1.]]], [0.], win="train_loss", opts=dict(title='train loss'))
Setting up a new session...
'train_loss'
同样,也可以直接进行 tensor图片 和 字符串 显示。
使用方式一看就懂,很简单。
from visdom import Visdom
viz = Visdom()
viz.images(data.view(-1,1,28,28), win='x')
viz.text(str(pred.datch().cpu.numpy()), win='pred', opts=dict(title="pred"))
下面进行手写数字识别的可视化操作:
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from visdom import Visdom
batch_size=200
learning_rate=0.01
epochs=10
train_loader = torch.utils.data.DataLoader(
datasets.MNIST('../data', train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
# transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=batch_size, shuffle=True)
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('../data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
# transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=batch_size, shuffle=True)
class MLP(nn.Module):
def __init__(self):
super(MLP, self).__init__()
self.model = nn.Sequential(
nn.Linear(784, 200),
nn.LeakyReLU(inplace=True),
nn.Linear(200, 200),
nn.LeakyReLU(inplace=True),
nn.Linear(200, 10),
nn.LeakyReLU(inplace=True),
)
def forward(self, x):
x = self.model(x)
return x
device = torch.device('cuda:0')
net = MLP().to(device)
optimizer = optim.SGD(net.parameters(), lr=learning_rate)
criteon = nn.CrossEntropyLoss().to(device)
viz = Visdom()
viz.line([0.], [0.], win='train_loss', opts=dict(title='train loss'))
viz.line([[0.0, 0.0]], [0.], win='test', opts=dict(title='test loss&acc.',
legend=['loss', 'acc.']))
global_step = 0
for epoch in range(epochs):
for batch_idx, (data, target) in enumerate(train_loader):
data = data.view(-1, 28*28)
data, target = data.to(device), target.cuda()
logits = net(data)
loss = criteon(logits, target)
optimizer.zero_grad()
loss.backward()
# print(w1.grad.norm(), w2.grad.norm())
optimizer.step()
global_step += 1
viz.line([loss.item()], [global_step], win='train_loss', update='append')
if batch_idx % 100 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
test_loss = 0
correct = 0
for data, target in test_loader:
data = data.view(-1, 28 * 28)
data, target = data.to(device), target.cuda()
logits = net(data)
test_loss += criteon(logits, target).item()
pred = logits.argmax(dim=1)
correct += pred.eq(target).float().sum().item()
viz.line([[test_loss, 100 * correct / len(test_loader.dataset)]],
[global_step], win='test', update='append')
viz.images(data.view(-1, 1, 28, 28), win='x')
viz.text(str(pred.detach().cpu().numpy()), win='pred',
opts=dict(title='pred'))
test_loss /= len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))
Setting up a new session...
Train Epoch: 0 [0/60000 (0%)] Loss: 2.303651
Train Epoch: 0 [20000/60000 (33%)] Loss: 2.272769
Train Epoch: 0 [40000/60000 (67%)] Loss: 2.254143
Test set: Average loss: 0.0110, Accuracy: 3536.0/10000 (35%)
Train Epoch: 1 [0/60000 (0%)] Loss: 2.200569
Train Epoch: 1 [20000/60000 (33%)] Loss: 2.062448
Train Epoch: 1 [40000/60000 (67%)] Loss: 2.018279
Test set: Average loss: 0.0092, Accuracy: 4688.0/10000 (47%)
Train Epoch: 2 [0/60000 (0%)] Loss: 1.897265
Train Epoch: 2 [20000/60000 (33%)] Loss: 1.723909
Train Epoch: 2 [40000/60000 (67%)] Loss: 1.452120
Test set: Average loss: 0.0071, Accuracy: 6016.0/10000 (60%)
Train Epoch: 3 [0/60000 (0%)] Loss: 1.364549
Train Epoch: 3 [20000/60000 (33%)] Loss: 1.424324
Train Epoch: 3 [40000/60000 (67%)] Loss: 1.238655
Test set: Average loss: 0.0060, Accuracy: 6280.0/10000 (63%)
Train Epoch: 4 [0/60000 (0%)] Loss: 1.305910
Train Epoch: 4 [20000/60000 (33%)] Loss: 1.279902
Train Epoch: 4 [40000/60000 (67%)] Loss: 1.336320
Test set: Average loss: 0.0055, Accuracy: 6592.0/10000 (66%)
Train Epoch: 5 [0/60000 (0%)] Loss: 1.114036
Train Epoch: 5 [20000/60000 (33%)] Loss: 1.034047
Train Epoch: 5 [40000/60000 (67%)] Loss: 0.973069
Test set: Average loss: 0.0045, Accuracy: 7523.0/10000 (75%)
Train Epoch: 6 [0/60000 (0%)] Loss: 0.959234
Train Epoch: 6 [20000/60000 (33%)] Loss: 0.751936
Train Epoch: 6 [40000/60000 (67%)] Loss: 0.493682
Test set: Average loss: 0.0026, Accuracy: 8648.0/10000 (86%)
Train Epoch: 7 [0/60000 (0%)] Loss: 0.585685
Train Epoch: 7 [20000/60000 (33%)] Loss: 0.457602
Train Epoch: 7 [40000/60000 (67%)] Loss: 0.486000
Test set: Average loss: 0.0022, Accuracy: 8787.0/10000 (88%)
Train Epoch: 8 [0/60000 (0%)] Loss: 0.371014
Train Epoch: 8 [20000/60000 (33%)] Loss: 0.421661
Train Epoch: 8 [40000/60000 (67%)] Loss: 0.592093
Test set: Average loss: 0.0020, Accuracy: 8863.0/10000 (89%)
Train Epoch: 9 [0/60000 (0%)] Loss: 0.438965
Train Epoch: 9 [20000/60000 (33%)] Loss: 0.461019
Train Epoch: 9 [40000/60000 (67%)] Loss: 0.487724
Test set: Average loss: 0.0019, Accuracy: 8938.0/10000 (89%)
当上面的程序跑起来之后,就可以在Visdom中看到可视化的效果啦:
过拟合 & 欠拟合
在现实生活中,我们经常可以通过「一眼看过去」就可以得知一些点的分布曲线,但是我们并不知道这个曲线的解析式是什么。
比如房价,一些点分布在平面上,我们高中就学过线性回归直线方程,很容易就可以直接通过代入求解得到这个曲线的解析式。
但是对于成绩来说,大部分的人都是分布在一个班的中间部分的,我们要求解班级人数关于成绩的解析式,直观看来是个正态分布或者二次函数,所以也是可以求解的。
但是对于一些问题我们想要根据数学思想去求解的话,几乎是不可能的。比如,你可以用数学去解释为什么你可以分辨「这个数字是5」,「这个动物是猫」吗?很难。因为我们自身都不太理解自身神经的工作方式,所以解释不了。
而深度学习我们同样解释不了,这就是一个「黑箱」,我们只知道这个黑箱通过某些简单的数学方法可以优化得到我们想要的数据,但具体为什么这样堆叠,这样迭代,什么样的数据才是最好的,什么样的激活函数适合这个问题,我们一概不知道。
而且在日常生活中人们采集到的数据一般都具有人为误差,毕竟人不是机器,不能保证百分百准确。
正因上面两个方面的原因,我们才需要大量的数据,一是为了让黑箱学习到更优良的参数,二是为了尽可能的消除误差。
Model Capacity 模型容量
假如说我们的一个模型中只有一个可变参数,那么它的学习能力是非常弱的。而我们增加参数,通过增加x的次方进行参数的增加就能得到更高层次的拟合效果。
下面这个多项式就是多个参数的式子。它的参数为n个,学习能力会变强很多。所以 Model Capacity 就会变大。
而我们的层数越多,通过之前的推导,每增加一个层,未知参数就会增加, Model Capacity 就会变大,所需要的显存也就越大。所以理论上来讲,越多的层数就越容易拟合到更优良的结果。
under-fitting 欠拟合
如果我们所使用的 模型能力 < 真实模型的复杂度(假设已经知道),比如我们用一个线性函数去拟合二次函数,这种情况我们就称作「under-fitting」欠拟合。
如何判断训练中这个模型是否出现了under-fitting情况?
- Train 过程中的 Acc 和 Loss 是很差的
- Validation(也叫Test)过程中的 Acc 也是很差的
over-fitting 过拟合
与上面相反,如果我们所使用的 模型能力 >> 真实模型的复杂度(假设已经知道),比如我们使用五次函数去拟合一次函数,得到了得到令人不满意的结果,这种情况就称为「over-fitting」过拟合。
下图就是一个过拟合的经典例子,很容易就能看出来过拟合趋近于「偏向每一个点」,从而导致拟合的结果不佳。
如何判断训练中这个模型是否出现了over-fitting情况?
- Train 过程中的 Acc 和 Loss 「非常好」
- Validation(Test)中的效果不好
over-fitting还有另外一种称呼「Generalization Performance」幻化能力。当幻化能力不好的时候就代表着这个模型可能是over-fitting了。
在实际中我们通常遇到的就是 over-fitting,因为当代计算机的性能实在太过强大,所以可能在模型设计的时候陷入过拟合的情况。
Train-Val-Test 划分
上节提到了过拟合十分常见且危害较大,那么如何检测over-fitting?
上面的手写数字识别项目其实就用到了检测的方法。原理就是把一个数据集分为两部分,大部分用来Train,小部分用来Test。而Test的时候依旧要求解 Acc 和 Loss。最后套用上面的判断方法:
- Train 过程中的 Acc 和 Loss 「非常好」
- Validation(Test)中的效果不好
如果符合那么就说明发生了over-fitting。
在实际跑项目的时候,可以使用「Check Point」存档点的思想——把Train和Test的Acc和Loss都比较好的情况(即过拟合发生之前最好的情况)通过存档点保存下来各个变量的参数。当我们确定发生过拟合之后,直接把前面存档点的数据拿出来用即可。
Validation Set
上面提到我们使用划分开的 Test Set 做作为过拟合检测的标志,但其实更标准且常用的是把数据集划分为三个Set:
- Train Set 只用来训练模型
- Validation Set 用来检测过拟合
- Test Set 模型训练结束后进行验收工作
K-fold Cross Validation
K-fold Cross Validation 就是把上面提到的Train Set 平均分成N份数据,每一份数据中的 用来 Train 模型,剩下的 (随机取得)用来作为 Validation Set使用。
本大节提到的分类方法请查看MNIST的数据取得源代码。
如何减轻 Over-fitting——Regularization
- 提供更多的数据(以毒攻毒不推荐使用,代价较大)
- 减轻模型「Model Complexity」模型复杂度
- Dropout(后面会讲到)
- Data argumentation(后面会讲到)
- Early Stopping(提前停止,使用 Check Point)
Regularization
上面提到,对于一个 Cross Entropy Loss 的定义式为:
那么在后面加上一个项,变为如下的样子:
其中加入的那一部分是指对 就是指的网络参数,然后对其求解范数。
为什么要这样做呢?我们直到 Loss 函数的优化方向是极小值,所以加入一个参数之后整个 Loss 函数也会顺带范数项进行优化。也就是如果这个参数设置的理想的话,就会使得这整个 Over-fitting 网络的表达能力下降。
即比如给定一个 类型的问题,但我们的网络复杂度是 ,其那么我们只需要令 上的系数趋于0即可。而范数项的加入就是在做这件事情,也就是说这一项会「帮助我们把网络优化称为适合问题目标的复杂度」。
其中的数理逻辑不做深究。
以上是Regularization的其中一种不常用形式,叫做L1-Regularization,只是为了更好理解这个效果。我们在实际使用中通常使用的是如下形式的 L2-Regularization,其定义是: 其中表示的是「二范数」。
那么如何在代码中使用 L2-Regularization 呢?
很简单,只需要在优化器初始化的时候设置 weight_decay = Integer 即可
import torch
device = torch.device('cuda:0')
net = MLP().to(device)
# 在step过程中会自动帮你设置范数以及优化,「只要存在weight_decay」即可
optimizer = optim.SGD(net.parameters(), lr=0.001, weight_decay=0.01)
creitron = nn.CrossEntropyLoss().to(device)
在PyTorch中实际上并不能很方便的使用 L1-Regularization,所以我们需要手动实现一下。
前置知识:
1范数的求法
直接无脑求个和就完事,所以实现出来是如下的代码:
import torch
regularization_loss = 0
for param in model.parameters():
regularization_loss += torch.sum(torch.abs(param))
classify_loss = criteon(logits, target)
loss = classify_loss + 0.01 * regularization_loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
动量与学习率衰减
momentum 动量(惯性)
我们前面可以知道,偷渡更新的公式是 。
我们可以直观想象一下,什么是惯性?答案就是「保持原来运动方向不变」的性质。而我们在梯度下降算法中的移动方向是“下一步的纯方向”,我们在这里引入一个算子 ,这个算子的意义是「保存第k-1次的梯度更新方向」,通过将这个算子代入梯度下降算法中之后,就有了更新梯度的时候,会有一部分的方向是和原方向保持一致的这样一种性质。因为这个性质和我们日常生活中的「惯性」很相似,所以在深度学习中也被乘坐惯性(Momentum)。
根据上面的分析,下面的这个公式应该很容易就可以理解了: 其中 分别表示的是 learning rate 和 momentum 。
与上面调用相同,momentum 的调用也是十分方便的,直接在optimizer中指定momentum= 即可:
optimizer = optim.SGD(net.parameters(), lr=0.001, weight_decay=0.01, momentum=0.78)
如果实际使用中并不能指定momentum,那么就很有可能是因为这种优化器内置了动量,无需人工指定。
Learning Rate Decay
Learining Rate其实对深度学习的影响很大,如下图所示,找到一个较为优秀的LR是深度学习的关键。
其实可以换个角度来解决这个问题——LR不固定,从大到小依次衰减。
这样做有什么好处呢?很显然前面不容易找到最优解的时候进行大幅的跳动,而快要找到最优解的时候调小LR,从而避免在最优解左右反复横跳难以收敛的情况。
import torch
# 初始化优化器
optimizer = torch.optim.SGD(model.parameters(), args.lr, momentum=args.momentum, weight_decay=args.weight_decay)
# 模式是最小检测模式,patience是如果多少次没有改善loss,那么就执行lr调整,factor是调整因数,0.5就是调整为原来的0.5倍。
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer=optimizer, mode=min, patience=10, factor=0.5)
for epoch in xrange(args.start_epoch, args.epochs):
train(train_loader, model, criterion, optimizer, epoch)
result_avg, loss_val = validate(val_loader, model, criterion, epoch)
scheduler.step(loss_val) # 执行step操作,其内部具体实现和更改lr都是自动且直接作用于optimizer上的。
另外还有一种方式——暴力实现,即添加一个计数器,每当执行到某个区间的时候就手动改变lr的值。
而这部分的代码并不用我们傻傻的去实现,PyTorch也帮我们封装好了:
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
这样在执行step的过程中,每当循环30次,则进行lr *= 0.1 的操作。
Early Stop, Dropout
之前在讲over-fitting的时候就了解到了「Early Stop」提前停止。我们再次明确一下,使用Training Set 和 Test Set 的 Acc 综合考虑进行over-fitting的确定。
如何 Early Stop
- 使用 Validation Set 来做模型参数的选择
- 使用 Validation Set 来检测训练效率和性能
- 在最高点停止训练
没有Train完,我们怎么知道「这个点」就是最大 Acc 点?
答案是没有方法判断它就是最大点。DL有很多小Trick在里面,所以这个要靠经验和实践来判断。
Dropout
也是一个防止over-fitting的有效手段
简单来说,Dropout就是通过「通过某设定的概率暂时改变输出为0」如果我们把两个节点的连线去掉来表示0的话,那么下图的左边就是普通没Dropout或者 断掉的概率 = 0的情况,右图就是断掉的概率为0.5的情况。
所以当我们forward的时候,我们的网络使用率和参数量就会少一些。
加了 Dropout 可以消除某些噪声。
如何使用 Dropout?
很简单,直接在声明网络的时候加入 Dropout 函数即可。
需要明确的是:
torch.nn.Dropout(p)和tf.nn.dropout(keep_prob)是不一样的。前者的1表示全部断掉,后者的1表示全部连接。
另外,Dropout操作一般只用于Train Set,Test和Validation Set几乎是用不到的。
为了避免这种情况,我们需要在计算Test Set Loss的时候手动取消掉Dropout函数的效果,只需要添加一句 net_dropout.ecal() 即可。
Stochastic Gradient Descent 随机梯度下降
- not random!不是随机!有一定的随机性 在此只是明确一下概念,上面的MNIST也用到了这个方法。
简而言之就是把数据集通过某种手段随机分成 Batch,分拨传入显卡训练,目的是为了节省显存。
手敲一些代码就会发现,上面定义的时候全是用的SGD,这个SGD就是 Stochastic Gradient Descent。
什么是卷积
计算机视觉的核心之一——卷积神经网络。
图片问题
对于一张图片,我们如果忽略Alpha通道的话,那么就是三个通道进行表示:RGB,即红绿蓝。我们通过处理可以得到一张图片的三张红绿蓝的分别表示。因为忽略了alpha透明度层,所以我们这里的灰度信息全部被删除,只使用0和1表示白和带有颜色。
人眼特性
上个世纪,科学家们苦于没有性能强劲的计算平台,连手写数字识别都难以跑起来。
为了解决这一问题,科学家们发现了人眼的一些特性:局部相关性。
假如我们看一张桌子,上面有个大大的蛋糕,旁边有几个粉笔头。那么我们的眼睛肯定第一眼看过去目光会被蛋糕吸引。等我们回过神来,才会看到旁边不起眼的粉笔头。
科学家们模拟人眼,发明了「感受眼」。科学家们就通过局部相关性提出了「卷积神经网络」。它会通过感受眼把一张图片画成多个小块,然后单独拿出一块来处理,然后移动眼球位置,直到所有的块块都被处理完毕。
推荐视频
www.bilibili.com/video/BV1VV… www.bilibili.com/video/BV1R5… www.bilibili.com/video/BV1JX…
Kernel 卷积核
不同的卷积核有不同的作用。
Sharpen 锐利化
0 -1 0
-1 5 -1
0 -1 0
Blur 模糊
1 1 1
1 1 1
1 1 1
Edge Detect 边缘检测
0 1 0
1 -4 1
0 1 0
其中边缘检测的效果如图所示:
我们一般认为经过卷积核处理过后的图片叫做「Feature Map」特征图。
卷积神经网络
具体是什么看过视频就懂了,不再赘述。
命名规则
CNN中需要命名的变量较多。
- x:[bs,3,28,28]分别对应 batch_size,通道数,竖直像素个数,水平像素个数
- one_k:指「一个Kernel」[3,3,3]分别对应 通道数,Kernel竖直大小,水平大小
- multi—_k:[16,3,3,3]分别对应 一共有多少个Kernel,通道数,Kernel竖直大小,水平大小
- bias:[16]偏置,等于Kernel数量
- out:[b,16,28,28]分别表示 batch_size,Kernel生成的Feature Map数量,竖直像素个数,水平像素个数
层间关系
在卷积神经网络中,一般来讲,卷积核(Kernel)提取的信息是由低维走向高纬的。
比如有一辆车的图片,第一个Kernel就是要提取它的轮廓外形,第二个可能会提取它是不是有轮子,第三个可能提取的是有几块玻璃等等。
nn.Conv2d
使用PyTorch进行卷积操作。
import torch
# 1表示的是输入通道(RGB为3),3表示的是Kernel数量,kernel_size表示Kernel的大小,stride表示每次移动的步长,padding表示外围要不要加一圈空白
layer = torch.nn.Conv2d(1,3, kernel_size=3, stride=1, padding=0)
x = torch.rand(1,1,28,28) # 用x模拟输入图片
# 进行卷积操作
out = layer.forward(x)
print(out.shape) # 输出的图片大小减少了2,表示卷积操作已经发生
layer = torch.nn.Conv2d(1,3, kernel_size=3, stride=1, padding=1)
x = torch.rand(1,1,28,28) # 用x模拟输入图片
# 进行卷积操作
out = layer.forward(x)
print(out.shape) # 输出的图片大小不变,表示padding成功
# 步长设置为2就会图片压缩为14*14
# 但其实不推荐使用forward函数,可以使用pytorch帮你封装好的hooks函数,会自动判断你想做什么并针对性的进行一些后端优化
out = layer(x)
print(out.shape)
torch.Size([1, 3, 26, 26])
torch.Size([1, 3, 28, 28])
torch.Size([1, 3, 28, 28])
池化和采样
Pooling 下采样
下采样是一种图片压缩图片的采样方法。
Max Pooling
假设有4x4的一张图片,使用 Max Pooling 进行下采样的时候就是使用 Max{x1,x2...xn}进行操作,即对一个区域区颜色最深的那个像素点,然后以此类推得到目标图。
Avg Pooling
与上面的Max不同,这里只是把Max函数换成了求平均函数Avg。
Upsample 上采样
和下采样相反,上采样的作用是放大图片。
F.interplolate
直接使用 F.interpolate(tensor, scale_factor = n, mode='nearest') 进行「对tensor的以nearest模式上采样2倍」。
其他操作请参考PyTorch文档。
import torch
x = torch.rand(1,16,14,14)
layer = torch.nn.MaxPool2d(2, stride=2)
print(layer(x).shape)
torch.Size([1, 16, 7, 7])
/usr/lib/python3.9/site-packages/torch/nn/functional.py:718: UserWarning: Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at /build/python-pytorch/src/pytorch-1.9.0-cuda/c10/core/TensorImpl.h:1153.)
return torch.max_pool2d(input, kernel_size, stride, padding, dilation, ceil_mode)
ReLU在CNN中的使用
ReLU在生成Feature Map中的效果
import torch
x = torch.randn(1,4,2,1)
layer = torch.nn.ReLU(inplace=True)
# 过滤掉0
out = layer(x)
print(out)
tensor([[[[0.0719],
[0.7993]],
[[0.6612],
[1.5523]],
[[0.0000],
[0.0000]],
[[0.9953],
[1.4525]]]])
Feature Scaling
Sigmoid 函数会出现梯度离散的情况,尤其是它在导数接近0的地方。对于这种情况,梯度会长时间得不到更新。所以我们一般使用ReLU函数。
但是我们有时候也不得不使用 Sigmoid 函数。因此我们希望把这个输入的值控制在有效范围之内。
比如我们想让-10000~10000的数值数据使用 Sigmoid 函数。这样就很容易出现梯度离散。所以要进行 Norm 操作。我们希望把这个值均匀落在0附近,从而得到 Sigmoid 函数的有效梯度。
Image Normalization
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.450], std=[0.229,0.224,0.225])
一看就懂,就是把数据缩小\放大成我们想要的数据范围。
Batch Normalization
假设一个Batch是[N,C,HW],N是几张图片,C是通道,HW是宽高乘积。
为了说明较为简便,这里设置这个batch的实际数据为[6,3,784],那么对通道维度做Normalize的话,那么就得到了一个shape为[3]的tensor。
为什么是3呢?其他维度都消除掉了吗?对的。这里的3个数据只表示「这个Batch中所有图片通道的平均值」,因为只有三个通道,所以就生成了shape为3的tensor。
同理,如果要对N维度求均值,那么返回的tensor的shape就是6.它的意义是「这个Batch上的6张图片,每张图片的均值」。
import torch
x = torch.rand(100,16,784)
layer = torch.nn.BatchNorm1d(16)
out = layer(x)
# 求解均值
print(layer.running_mean.shape)
# 求解方差
print(layer.running_var.shape)
torch.Size([16])
torch.Size([16])
Pipeline
Output:
在我们使用BatchNorm的过程中,其内部的值就和梯度下降一样,会自动更新梯度信息。
nn.BatchNorm2d
Norm操作其实就是缩放操作。那么对于一个二维的Tensor,直接进行使用即可,和1d差不多。
import torch
x = torch.rand(1,16,7,7)
layer = torch.nn.BatchNorm2d(16)
# 把16这一维度放缩到01这一分布
out = layer(x)
# 输出的tensor shape不变
print(out.shape)
# weight信息(PyTorch的自动命名规则),即wx+b中的w
print(layer.weight)
# bies信息(自动命名规则),wx+b中的b
print(layer.bias)
torch.Size([1, 16, 7, 7])
Parameter containing:
tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
requires_grad=True)
Parameter containing:
tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
requires_grad=True)
值得注意的是,在实际Train的过程中出现上面的w和b一个全是1,一个全是0这种情况是不正常的(此处没有默认网络,所以默认就是1和0)。
出现这种情况的原因是没有设置affine参数为True(默认是False),即不会自动更新梯度信息,只是做一个Normalize操作。
Test
和Dropout一样,测试和训练的用法是不一样的。
通过上面的Normalization公式可以得知,layer会在running_xxx函数调用的时候进行更新 和 ,然后在手动梯度更新的时候(step())进行应用x和y的梯度信息。
但是在Test的时候,因为只用来判断acc,不需要更新梯度,所以在Test的时候会把running出来的值直接赋值给 和 。
要实现上面这张行为,只需要提前调用layer.eval()即可。
通过上面的Normalize操作,我们在使用 类Sigmoid 函数的时候就会得到一个更好一些的训练结果。
总结一下,在使用 类Sigmoid 函数的时候使用 Normalize 操作的优点如下:
- 收敛更快
- 性能更好
- 更加稳定
经典CNN
介绍一些经典的CNN神经网络结构。
LeNet-5
最开始 LeNet-5 解决的问题是手写数字识别。
这个网路结构因为识别的手写数字难度不大,所以结构并不复杂。但是它达到了 99.2% 的准确率。
AlexNet
8层网络结构,比较深。
Alxe成功在ImageNet(数据量非常大的一个人工智能比赛)上取得第一名。准确率提升比较大。
使用了5个卷积层,更利于提取图片中的特征。并且开创了Pooling(池化)操作的先河。还把 ReLU 函数引入到了CNN中;使用了Dropout操作防止Over-fitting。
上面的两种网络结构因为年代过于久远,现在使用频率很少。
VGG
VGG 有很多版本,比如 VGG-11 VGG-16 VGG-19
现在很多Kernel的大小都是 3x3 ,而之前AlexNet中的窗口大小是8x8。因为当时人们觉得「窗口视野更大,感知到的信息应该更多」。但是经过牛津大学VGG实验室发现,小窗口反而能带来更多的优势。
比如小窗口运算速度较快,梯度下降更加明显,特征提取更加优越等。
现在我们实际使用中都是偏向小窗口进行的。除了3x3,甚至有网络使用1x1的Kernel。
所以VGG的最大贡献是发明了更小的Kernel,这个影响意义重大,直到现在人们都在使用。
1x1 Convolution
加入有这样一张图片[3,28,28],那么对其使用 [2,3,3] 的2个3x3卷积核运算就会得到一个[2,28,28]的Feature Map。
这里可能有一些难理解,因为卷积核只是一个运算工具,它针对的是「一张图片的所有通道,并合并成一个输出」。也就是,加入你的图片只有1个通道,但卷积核的数量是32,输出照样是32张Feature Map。
这里比较难理解,建议多看一些文章。
GoogLeNet
获得人工智能大赛的第一名。
- 命名很 special ,为了纪念LeNet,所以把Google后面的l改成了L大写。随便怎么念都可以。
比起VGG,GoogLeNet又增加了三层,layer是22。
使用更小的卷积核的基础上,对于同一层,可以使用更多类型的卷积核。比如1x1的卷积核和3x3的卷积核可以在同一层中混用。
如何保证处理出来的数据的大小是一样的呢?(层之间的输入必须是相同shape数据)。为了统一化输入数据,可以使用padding等上文提到过的方式统一。
那么上面的趋势就是「层数越多,效果越好」。但事实却不是这样的。如果层数过多,网络结构就会深,训练就会变得困难。所以要权衡一下网络层数和实际效果,而不是无脑堆叠层数。
ResNet 深度残差网络
中国何凯明团队提出的新型网络。
上面所述,当我们网络深度增加的时候,loss函数所在的位置就会和前面的层变远,梯度下降和反向传播算法的影响就会变弱,所以深层的网络不利于训练。
在更深的网络中,容易出现梯度离散。梯度下降对前面的基层几乎趋于0,而且在计算的过程中可能会出现计算误差,层层积累下来之后就会造成前面的梯度信息「几乎不更新」。就导致了「一直Train但效果不见」的现象。
我们称上面这种现象是梯度离散现象的一种。
那么ResNet到底做了什么呢?
上图就是一个ResNet的网络模型。简单来讲就是在两层之间搭建了一条梯度更新的高速公路。假设上图所示的层在实际使用的过程中出现了梯度离散的情况,那么这条高速公路就是把离散的梯度重新拉回正规。
更加形象的理解就是,给了数据两种选择,如果传统的道路走不好(路况不佳),那么就会选择新的路。但是新的路它会导致层数退化(数据全都走高速公路那么这一层就没有意义了)。
这个高速公路被提出者称为「shortcut」。
ResNet 提出的初衷就在于解决多层网络训练困难的问题。假如一个训练最佳的层数为22,我们给它了30层去训练,加上ResNet就可能让30层退化到22层以达到更好的训练效果。
值得注意的是,ResNet 伸出去的那一条线里面包含着的几层被称作一个「unit」。一般一个unit中有2-3层。在unit层中进行卷积操作的时候,为了可以使输入数据和输出数据直接相加,我们就需要保证在unit运算中输出的数据「必须和原始数据相同」。
但是还有一个问题。我们在使用ResNet的过程中,决定要不要使用shortcut线路的权利在网络手中,而我们是不能干预的。加入我们有50层网络,里面有很多很多shortcut,那么在训练的时候不排除「全部退化」的情况,也就是整个网络退化为1层。
这个问题开发团队当然也想到了,他们的解决办法是:如果这个网络退化了,那么就会把这个地方单独拿出来去「军训」。也就是会想办法把这一段网络给「扶上墙」,来增加集体的拟合效力。但是这个unit实在是「烂泥」,扶上墙已经不可能了,ResNet就会继续使用shortcut。
如果拟人化的话,ResNet就是一个「除劣存优」,但又「富有热心去扶持坏unit」的一个网络结构教官。
nn.Module
这个类十分重要,在PyTorch中涉及DL的大部分的方法/类全都继承自nn.Module。
- 可以使用内置的层,比如ReLU,Sigmoid,Conv2d...
- Container 即 nn.Sequential。简化多层数网络搭建的工作量
基于nn.Module 产生的所有节点(节点内部可以再次使用Module进行嵌套)。
那么类似于二叉树的定义,所有使用Module的子节点都叫做children(相对概念)。
下面是使用Module嵌套类:
import torch
import torch.nn as nn
class BasicNet(nn.Module):
def __init__(self):
super(BasicNet, self).__init__()
# 可以自行查看Linear方法的文档,自动生成输入为4输出为3。
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)
另外,也可以对net直接使用to(device)函数进行CPU和GPU的转移。
Save and Load
我们在训练的过程中,如果要训练7天,但是第六天的时候断电了怎么办呢?那就得重新返工了。PyTorch非常贴心的考虑到了这一点,允许自动添加CheckPoint进行数据的保存操作。
import torch
# 读取保存数据
net.load_state_dict(torch.load('ckpt.mdl'))
# 保存数据到本地文件
torch.save(net.state_dict(), 'ckpt.mdl')
Train/Test
上面提到过,一个数据集在Train和Test的时候行为是不太一样的。PyTorch也提供了相应的方法来管理不同的行为。
import torch
device = torch.device('cuda:0')
net = Net()
net.to(device=device)
# 切换为 Train 状态
net.train()
# 切换为 Test 状态
net.eval()
实现自定义层
前面进行卷积的时候,需要进行数据打平操作才能进行全连接层的运算。
我们就需要进行一下数据展平操作然后丢到全连接层中去。
但是在Sequential类中需要进行class的堆叠,而不能使用view方法,需要进行一些class封装。
import torch
import torch.nn as nn
class Flatten(nn.Module):
def __init__(self):
super(Flatten, self).__init__()
def forward(self, input):
return input.view(input.size(0), -1)
class TestNet(nn.Module):
def __init__(self):
super(TestNet, self).__init__()
self.net = nn.Sequential(
nn.Conv2d(1,16,stride = 1, padding=1, kernel_size=3),
nn.MaxPool2d(2,2),
Flatten(),
nn.Linear(1*14*14, 10)
)
def forward(self, x):
return self.net(x)
testNet = TestNet()
testNet.train()
TestNet(
(net): Sequential(
(0): Conv2d(1, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(2): Flatten()
(3): Linear(in_features=196, out_features=10, bias=True)
)
)
数据增强
在实际使用神经网络训练的时候,可能会遇到数据集不足的情况。这个时候就需要我们对图片进行一些处理来「最大化利用每一张图片的信息」。
比如,我们有20张篮球照片,训练这20张照片的效果显然不是很好,网络并不能充分的进行学习。但是如果我们对每一张图片都使用一些图像处理层面的变换,把这20张拓展成100张,那么就会改进训练效果。
常见的方式有:平移,旋转,镜像,错切,缩放,甚至可以增加噪声。具体用例在此不再赘述。
需要明确的一点是,我们通过这些手段处理图像并不能明显的改善训练的效果。毕竟是通过简单平移出来的图片,并没有直接改变图片所包含的信息量。所以在使用的时候不能过分依赖于数据增强操作。