Pytorch 的快速入门,参见 通过两个神经元的极简模型,清晰透视 Pytorch 工作原理。本文结合手写体识别项目,给出一个具体示例和直接关联代码的解释。
1. 代码
下面代码展示了完整的手写体识别的 Python 程序代码。代码中有少量注释。在本文后半部分,给出其中关键代码的详细注释。
import numpy as np
import torch
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import torchvision
'''
运行说明
安装依赖命令,要安装这个版本。
pip install torch==1.4.0 torchvision==0.5.0
pip install matplotlib numpy
代码按照内容的顺序,方便阅读。
'''
# 1 导入数据集
train_loader = torch.utils.data.DataLoader(
datasets.MNIST(root='./data', #root表示数据加载的相对目录
train=True, #train表示是否加载数据库的训练集,False时加载测试集
download=True,#download表示是否自动下载
transform=transforms.Compose([#transform表示对数据进行预处理的操作
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),batch_size=64, shuffle=True)#batch_size表示该批次的数据量 shuffle表示是否洗牌
'''
关于下面这段代码的说明:
transform=transforms.Compose([#transform表示对数据进行预处理的操作
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
这段代码使用了PyTorch的transforms模块,将数据预处理操作组合到一起。
其中,transforms.Compose()函数用于将多个数据预处理操作(也称为transform)组合成一个可执行的函数。而在这个例子中,Compose()函数传入了两个transform函数:ToTensor()和Normalize()。
ToTensor()函数用于将PIL Image或numpy.ndarray数据类型转换成torch.FloatTensor类型,即将图片转换成tensor。
Normalize()函数则用于对tensor进行归一化操作。它需要传入两个参数,分别是均值和标准差。这个例子中,均值为0.1307,标准差为0.3081。这意味着对于每个通道,像素的值都将被减去均值后再除以标准差,使得处理后的数据均值为0,标准差为1。这样做的好处是可以使模型更容易地学习到数据的特征,加速训练过程。
因此,这段代码所做的工作是将输入的图片数据先转换为tensor,再进行归一化处理,最终返回处理后的tensor数据。
'''
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('./data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),batch_size=64, shuffle=True)
def imshow(img):
img = img / 2 + 0.5 # unnormalize
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
# 得到batch中的数据
dataiter = iter(train_loader) # 通过 iter(train_loader) 将 train_loader 转换为一个 Iterator 迭代器对象。
images, labels = dataiter.next() # 通过 dataiter.next() 获取这个迭代器对象的下一个元素,即从 train_loader 中取出一批数据。
# 将这一批数据中的图像数据和标签数据分别赋值给变量 images 和 labels。
# 展示图片
imshow(torchvision.utils.make_grid(images))
# 1.2.1 定义神经网络
import torch
import torch.nn as nn
import torch.nn.functional as F#可以调用一些常见的函数,例如非线性以及池化等
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# 1 input image channel, 6 output channels, 5x5 square convolution
# 输入图片是1 channel输出是6 channel 利用5x5的核大小
self.conv1 = nn.Conv2d(1, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
# 全连接 从16 * 4 * 4的维度转成120
self.fc1 = nn.Linear(16 * 4 * 4, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
# 在(2, 2)的窗口上进行池化
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
x = F.max_pool2d(F.relu(self.conv2(x)), 2)#(2,2)也可以直接写成数字2
x = x.view(-1, self.num_flat_features(x))#将维度转成以batch为第一维 剩余维数相乘为第二维
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:] # 第一个维度batch不考虑
num_features = 1
for s in size:
num_features *= s
return num_features
net = Net()
print(net)
# 1.2.2 前向传播
image = images[:2]
label = labels[:2]
print(image.size())
print(label)
out = net(image)
print(out)
# 1.2.3 计算损失
image = images[:2]
label = labels[:2]
out = net(image)
criterion = nn.CrossEntropyLoss()
loss = criterion(out, label)
print(loss)
# 1.2.4 反向传播与更新参数
#创建优化器
import torch.optim as optim
optimizer = optim.SGD(net.parameters(), lr=0.01)#lr代表学习率
criterion = nn.CrossEntropyLoss()
# 在训练过程中
image = images[:2]
label = labels[:2]
optimizer.zero_grad() # 消除累积梯度
out = net(image)
loss = criterion(out, label)
loss.backward()
optimizer.step() # 更新参数
# 1.3 开始训练
def train(epoch):
net.train() # 设置为training模式
running_loss = 0.0
for i, data in enumerate(train_loader):
# 得到输入 和 标签
inputs, labels = data
# 消除梯度
optimizer.zero_grad()
# 前向传播 计算损失 后向传播 更新参数
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 打印日志
running_loss += loss.item()
if i % 100 == 0: # 每100个batch打印一次
print('[%d, %5d] loss: %.3f' %
(epoch + 1, i + 1, running_loss / 100))
running_loss = 0.0
train(1)
# 1.4 观察模型预测效果
correct = 0
total = 0
with torch.no_grad():#或者model.eval()
for data in test_loader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (100 * correct / total))
2. 注释
2.1 transforms.Compose()
transform=transforms.Compose([#transform表示对数据进行预处理的操作
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
这段代码使用了PyTorch的transforms模块,将数据预处理操作组合到一起。
其中,transforms.Compose()函数用于将多个数据预处理操作(也称为transform)组合成一个可执行的函数。而在这个例子中,Compose()函数传入了两个transform函数:ToTensor()和Normalize()。
-
ToTensor()函数用于将PIL Image或numpy.ndarray数据类型转换成torch.FloatTensor类型,即将图片转换成tensor。
-
Normalize()函数则用于对tensor进行归一化操作。它需要传入两个参数,分别是均值和标准差。这个例子中,均值为0.1307,标准差为0.3081。这意味着对于每个通道,像素的值都将被减去均值后再除以标准差,使得处理后的数据均值为0,标准差为1。这样做的好处是可以使模型更容易地学习到数据的特征,加速训练过程。
因此,这段代码所做的工作是将输入的图片数据先转换为tensor,再进行归一化处理,最终返回处理后的tensor数据。
2.2 transforms.ToTensor()
transforms.ToTensor()
是 PyTorch 中的一个数据预处理方法,用于将 PIL 图像或 numpy 数组转换为张量(Tensor)类型,并进行标准化。具体来说,该方法将 PIL 图像或 numpy 数组的值缩放到 [0, 1] 的范围内,并将其通道顺序从 H x W x C 转换为 C x H x W。
以下是一个简单的例子:
import torch
from torchvision import transforms
from PIL import Image
# 读取一张图片
img = Image.open("example.jpg")
# 对图片进行预处理
transform = transforms.Compose([
transforms.Resize((224, 224)), # 重设图片大小为 224x224
transforms.ToTensor() # 将 PIL 图像或 numpy 数组转换为张量类型
])
img_tensor = transform(img)
print(img_tensor.shape) # 输出 (3, 224, 224)
print(torch.min(img_tensor), torch.max(img_tensor)) # 输出 0 和 1,表示值已经被缩放到 [0, 1] 的范围内
此示例从文件加载一张图像,并使用 transforms.Resize()
方法重设图像大小。然后使用 transforms.ToTensor()
方法将图像转换为张量。最后,将处理后的张量打印出来,显示它的形状和最小/最大值。
2.3 transforms.Normalize()
transforms.Normalize()
是 PyTorch 中的一个数据预处理方法,用于对张量进行标准化,即让所有元素都满足均值为 0、方差为 1 的分布。在深度学习中,对数据进行标准化是提高训练稳定性和效果的常用方法之一。
该方法需要传入两个参数:均值(mean)和标准差(std)。标准化公式为:
output = (input - mean) / std
在这个公式中,input 是输入的张量,mean 和 std 是指定的均值和标准差,在对每个元素逐一计算(减去均值再除以标准差)之后得到 output 张量。
以下是一个简单的示例:
import torch
from torchvision import transforms
# 构造一个三通道的张量,大小为 3x5
tensor = torch.tensor([
[[1.0, 2.0, 3.0, 4.0, 5.0],
[6.0, 7.0, 8.0, 9.0, 10.0],
[11.0, 12.0, 13.0, 14.0, 15.0]],
[[2.0, 3.0, 4.0, 5.0, 6.0],
[7.0, 8.0, 9.0, 10.0, 11.0],
[12.0, 13.0, 14.0, 15.0, 16.0]],
[[3.0, 4.0, 5.0, 6.0, 7.0],
[8.0, 9.0, 10.0, 11.0, 12.0],
[13.0, 14.0, 15.0, 16.0, 17.0]]
])
# 求均值和标准差
mean = torch.mean(tensor)
std = torch.std(tensor)
# 借助 transforms.Normalize 对张量进行标准化
transform = transforms.Normalize(mean=mean, std=std)
normalized_tensor = transform(tensor)
print(normalized_tensor)
在这个例子中,我们构造了一个大小为 3x5 的张量,并对它进行了标准化。标准化的结果是把每个元素都减去了张量的均值,然后除以了张量的标准差。最后我们输出标准化后的结果。
更进一步的例子:
import torch
from torchvision import transforms
from PIL import Image
import matplotlib.pyplot as plt
# 读取一张图片
img = Image.open("example.jpg")
plt.imshow(img)
plt.show()
# 对图片进行预处理
transform = transforms.Compose([
transforms.Resize((224, 224)), # 重设图片大小为 224x224
transforms.ToTensor() # 将 PIL 图像或 numpy 数组转换为张量类型
])
img_tensor = transform(img)
print(img_tensor.shape) # 输出 (3, 224, 224)
print(torch.min(img_tensor), torch.max(img_tensor)) # 输出 0 和 1,表示值已经被缩放到 [0, 1] 的范围内
# 转换张量维度,并把它转换为 numpy 数组
img_numpy = img_tensor.permute(1, 2, 0).numpy()
plt.imshow(img_numpy)
plt.show()
运行结果如下:
2.4 torchvision.utils.make_grid()
torchvision.utils.make_grid(images)
是 PyTorch 的一个函数,用于将多张图片合成一张组合图,可用于可视化多张图像的数据。
该函数的主要参数是 images
,代表输入的张量的列表或者张量。除此之外,还可以接受一些其他参数来控制输出的组合图的样式和尺寸。
下面是一个简单的代码示例:
import torch
import torchvision
import matplotlib.pyplot as plt
# 读取一些用于示例的图片
images = [torch.randn((3, 256, 256)), torch.randn((3, 256, 256)), torch.randn((3, 256, 256))]
# 将图片进行组合,并转换为可显示的numpy数组格式
grid_image = torchvision.utils.make_grid(images, padding=10)
numpy_image = grid_image.cpu().numpy().transpose((1, 2, 0))
# 显示组合图
plt.imshow(numpy_image)
plt.show()
在上面的示例中,我们模拟生成了三张
3
×
256
×
256
3\times 256\times 256
3×256×256 的图片,使用 torchvision.utils.make_grid
将这三张图片组合成一张
3
×
514
×
778
3\times 514 \times 778
3×514×778 的大图,并且添加了 padding=10
参数来增加每个小图之间的边框宽度,最后将组合图转换为numpy数组格式,并使用 matplotlib
库进行显示。执行上述代码,将会得到如下的组合图:
2.5 x.view()
x.view函数可以将一个PyTorch Tensor(张量)重塑成一个新的形状,这个新形状中每个维度的大小都可以指定。例如:对于一个形状为(2,3)的二维Tensor(张量),我们可以使用view(6)
函数将其重塑成一个大小为(6,)的一维Tensor(张量)。在该语句x.view(-1, self.num_flat_features(x))中,-1的含义是我们不知道应该设定的维度数字,需要让PyTorch帮助我们计算,同时保证重塑后的张量元素数量与原来的张量一致。
作为一个例子,假设有一个形状为(2,3,4)的三维张量x,其中2表示batch size,3表示width,4表示height。我们可以使用以下代码将其重塑为一个2D张量:
x = x.view(-1, 3*4) # 将width和height两个维度合并成为一个维度3*4
其中,-1指示在执行重塑操作时,根据原张量的shape自动计算剩余维度的大小,具体而言,在这个例子中,-1表示batch size保持不变,由原张量的batch size的值决定,而重塑后的张量每个元素都相同,都是一个大小为3*4的一维数组。
2.6 nn.CrossEntropyLoss()
nn.CrossEntropyLoss() 是一个用于计算分类问题中损失函数的PyTorch模块。该损失函数是基于交叉熵衡量预测输出和真实标签之间的距离,常用于多分类问题中。
假设我们有一个3分类的分类问题,每个样本有4个特征。我们可以使用以下代码来定义模型和损失函数:
import torch
import torch.nn as nn
# 定义模型
class MyModel(nn.Module):
def __init__(self):
super(MyModel, self).__init__()
self.fc = nn.Linear(4, 3) # 4个特征,3个类别
def forward(self, x):
out = self.fc(x)
return out
model = MyModel()
# 定义损失函数
criterion = nn.CrossEntropyLoss()
接下来,我们可以加载训练数据,例如利用DataLoader
将数据分成batch进行处理。假定一个batch中包含了10个样本,我们可以使用以下代码计算出这个batch中所有样本的损失函数值:
# 假设X是一个10*4的张量,y是一个包含10个整数的列表,表示10个样本的标签
X = torch.randn(10, 4)
y = torch.randint(0, 3, (10,))
# 前向传播,获取预测输出
outputs = model(X)
# 计算损失
loss = criterion(outputs, y)
其中,outputs
是一个10*3的张量,表示对于这10个样本来说,模型对3个类别的预测输出。 y
是一个长度为10的列表,表示这10个样本的标签(整数形式)。 criterion(outputs, y)
计算了这个batch中所有样本的损失函数值,返回一个大小为1的tensor。
更具体地说,使用nn.CrossEntropyLoss()
时,损失函数的具体计算过程如下:
- 对于每个预测输出(即模型对于一个样本对每个类别的预测概率),通过softmax函数将其转换为概率值;
- 找到该样本真实标签所对应的类别,并将其对应的概率值作为损失函数的一部分;
- 将所有样本的损失函数值进行求和或平均。
以下是使用nn.CrossEntropyLoss()
计算损失的具体实现:
假设有一个3分类问题,我们已经得到了一个大小为 1 × 3 的张量outputs和一个大小为1的张量y。其中outputs表示模型对于这个样本属于每个类别的概率,例如outputs=[0.1, 0.8, 0.1]表示该样本对于第二个类别(对应0.8这个最大概率)的预测最准确。
import torch.nn.functional as F
outputs = torch.tensor([0.1, 0.8, 0.1])
y = torch.tensor([1])
# 将outputs通过softmax函数转化为概率值
probs = F.softmax(outputs, dim=0)
print('probs:', probs) # 如 output=[0.1, 0.8, 0.1] 则 probs=[0.2119, 0.5761, 0.2120]
# 使用y作为索引从probs中选取对应的概率值
loss = F.nll_loss(torch.log(probs), y)
print('loss:', loss) # tensor(0.5472)
这里使用了nn.functional.nll_loss()
函数的反向版本,该函数用于计算负对数似然损失,即输入是log概率而不是概率本身。其中,torch.log(probs)
用于将概率转换成对数概率。上面的例子中,输出的loss值为0.5472。
关于 F.nll_loss
补充说明如下:
F.nll_loss
是PyTorch
中计算负对数似然损失的函数,其使用方式一般与nn.CrossEntropyLoss()
相同。使用F.nll_loss
时,需要将模型输出(即经过softmax后得到的每个类别的概率)的log值以及真实标签作为该函数的输入。
在上面的例子中,我们首先使用了softmax函数对模型输出进行转换,得到了一个大小为1×3的概率张量probs
,其中每个元素表示该样本属于这个类别的可能性。然后,使用torch.log
函数获取probs
的对数,以略微减少计算开销,并将其作为负对数似然损失函数的输入,同时传入真实标签y
进行计算。
更具体地解释,原理如下:
首先,将probs
进行对数化处理,可以把交叉熵函数转化为nll_loss函数。此处使用log函数。这样得到的结果就不会那么离散,让训练变得更加稳定。
然后,我们希望用probs
的值来估算输出结果的概率真相性,因此需要找到对应的真实标签,这个标签被单独编码成了一个整数。这里使用了索引技巧:将每个样本的真实标签y
作为索引,从probs
张量中选取对应的概率值。由于处于计算负对数似然损失,需要对所选出的概率再取反。最终的结果即为使用该样本计算得到的损失值。
在上面的例子中,我们的模型输出[0.1, 0.8, 0.1]
表示该样本可能属于第二个类别的概率最高,因此真实标签为1,可以得到以下计算过程:
outputs
=[0.1, 0.8, 0.1]
probs
=[0.266, 0.468, 0.266]
,对outputs
使用了softmax
函数转换为概率值log_probs
=[-1.3235, -0.7586, -1.3235]
,对probs
使用了torch.log()
函数, 进行log操作nll_loss(log_probs, y)
=0.7586
,函数使用y作为索引从log_probs中选取做为结果和计算梯度。
这个值可以用于反向传播和更新权重值,以提高模型预测能力。
2.7 loss.backward()
loss.backward()
是一个 PyTorch 中用来计算误差反向传播(Backpropagation)的函数,它将使用自动微分(Automatic differentiation)来计算图中每个参数的梯度。
在神经网络中,我们通常需要最小化模型输出与真实标签之间的误差。通过解析损失函数中的参数梯度,我们可以更新每个参数的值以使误差逐渐减少。这就是反向传播算法的核心思想。
在计算过程中,每一步计算结果都会被存储在计算图中,而 loss.backward()
函数就是用来计算该计算图中所有参数的梯度,并将它们累加到各自的变量对象中。
在具体使用时,用户需要在代码中定义所有涉及到的参数,并且对需要进行反向传播的损失函数调用 backward()
方法。例如:
import torch
# 定义神经网络
net = torch.nn.Linear(10, 1)
# 定义优化器
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)
# 定义损失函数
criterion = torch.nn.MSELoss()
# 模型训练
for epoch in range(10):
optimizer.zero_grad() # 梯度清零
input = torch.randn(1, 10) # 随机生成训练数据
output = net(input) # 将输入送入神经网络得到输出
target = torch.randn(1, 1) # 随机生成目标标签
loss = criterion(output, target) # 计算损失
loss.backward() # 反向传播
optimizer.step() # 更新优化器参数
print("Training finished!")
在这个简单的示例中,我们定义了一个简单的神经网络,使用随机数据进行训练。每个 epoch 中,我们执行一次正向传播以计算模型输出,然后计算损失。接着,调用 loss.backward()
函数计算梯度,最后使用优化器更新参数。
optimizer.step()
是一个 PyTorch 中用来更新模型参数的函数。在反向传播计算完成后,我们需要更新模型中各个参数的值,从而优化训练效果。
3. 代码中用到的几个代码 Python 包
这段代码使用了以下几个 Python 库:
-
numpy
:Python 的开源数学计算库,用于处理向量、矩阵等多维数组数据,以及数值计算、线性代数等操作。 -
torch
:PyTorch 是基于 Python 的开源机器学习框架,提供了动态计算图、自动微分等功能,可以方便地构建深度神经网络模型。 -
torchvision
:PyTorch 的扩展库,提供了常用图像数据集、变换(transforms)、模型结构和预训练权重等功能。它可以帮助我们更方便地处理图像数据,并且提供了常见的网络结构(如 VGG、ResNet 等)。 -
matplotlib.pyplot
:Python 的绘图库,提供了一些函数用于绘制图形,例如折线图、柱状图、散点图等。 -
torch.nn
:PyTorch 中的模型层或损失函数等等都在这里。这个包只是一个为 nn 设定的空间,同样也会有定义网络中常见的层结构(如全连接层、卷积层、池化层等)的代码。 -
torch.nn.functional
:PyTorch 中提供了各种常用的非线性函数、池化函数,以及 softmax 等函数。 -
torch.optim
:PyTorch 提供了各种优化器,例如 SGD、Adam 等。这些优化器可以帮助我们更方便地更新神经网络中的参数,提高模型的训练效果。