# 初始
# 前言
使用 conda 安装时不用另外装 cuda 和 cudnn,它自己会去装
查看 cuda 版本 nvidia-smi
conda install pytorch torchvision torchaudio cudatoolkit=11.0 -c pytorch
pip3 install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu110
实测 cuda11.0 可以 cuda10.2 的
# 换源
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --add channels
# 安装
# 什么是 pytorch?
pytorch 是一个基于 Python 的科学计算包,它主要有两个用途:
- 类似于 Numpy 但是能利用 GPU 加速
- 一个非常灵活和快速用于深度学习的研究平台。
# 检查 GPU 是否可用
import torch | |
torch.cuda.is_available() # True 为可用 |
# 两个法宝函数
dir(torch) | |
dir(torch.cuda) | |
help(torch.cuda.is_available) |
# Dataset
创建类来加载数据集,
import os | |
from torch.utils.data import Dataset | |
from PIL import Image | |
class MyData(Dataset): | |
def __init__(self, root_dir, label_dir): | |
self.root_dir = root_dir | |
self.label_dir = label_dir | |
self.path = os.path.join(root_dir, label_dir) | |
self.img_path = os.listdir(self.path) | |
def __getitem__(self, idx): | |
img_name = self.img_path[idx] | |
img_item_path = os.path.join(self.root_dir, self.label_dir, img_name) | |
img = Image.open(img_item_path) | |
label = self.label_dir | |
return img, label | |
def __len__(self): | |
return len(self.img_path) | |
if __name__ == '__main__': | |
root_dir = "../dataset/train" | |
ants_label_dir = "ants" | |
ants_dataset = MyData(root_dir, ants_label_dir) | |
bees_label_dir = "bees" | |
bees_dataset = MyData(root_dir, bees_label_dir) | |
train_dataset = ants_dataset + bees_dataset |
# tensorboard
from torch.utils.tensorboard import SummaryWriter | |
writer = SummaryWriter("logs") # 文件名称 | |
# y = x | |
for i in range(100): | |
writer.add_scalar("y = x", i, i) | |
writer.close() |
打开终端
在终端输入
conda activate pytorch | |
tensorboard --logdir=logs #事件文件所在文件夹 | |
tensorboard --logdir=logs --port=6007 # 指定端口 |
# Transform
在 transforms.py 中有很多的类,相当于一个一个的工具
# Totensor
from torchvision import transforms | |
from PIL import Image | |
from torch.utils.tensorboard import SummaryWriter | |
img_path = "D:\\Projects\\pytorch-learn\\dataset\\train\\ants\\0013035.jpg" | |
img = Image.open(img_path) | |
tensor_trans = transforms.ToTensor() # 从 transforms 中返回类 ToTensor | |
tensor_img = tensor_trans(img) # 使用该类,调用的__call__方法 | |
print(tensor_img) | |
print(tensor_img.max(), tensor_img.min()) | |
writer = SummaryWriter('logs') | |
writer.add_image("Tensor_img", tensor_img) | |
writer.close() |
# Normalize
from torchvision import transforms | |
from torch.utils.tensorboard import SummaryWriter | |
from PIL import Image | |
img_path = "D:\\Projects\\pytorch-learn\\dataset\\train\\ants\\0013035.jpg" | |
img = Image.open(img_path) | |
# ToTensor | |
tensor_trans = transforms.ToTensor() # 从 transforms 中返回类 ToTensor | |
tensor_img = tensor_trans(img) # 使用该类,调用的__call__方法 | |
# Normalize | |
print(tensor_img[0][0][0]) | |
trans_norm = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) | |
img_norm = trans_norm(tensor_img) | |
print(img_norm[0][0][0]) |
# Resize
from torchvision import transforms | |
from torch.utils.tensorboard import SummaryWriter | |
from PIL import Image | |
img_path = "D:\\Projects\\pytorch-learn\\dataset\\train\\ants\\0013035.jpg" | |
img = Image.open(img_path) | |
# ToTensor | |
tensor_trans = transforms.ToTensor() | |
writer = SummaryWriter('logs') | |
# Resize | |
trans_resize = transforms.Resize((512, 512)) # 指定图片的宽高,是对图片进行操作,不是对张量 | |
img_resize = trans_resize(img) | |
img_resize = tensor_trans(img_resize) # 最后再转为张量 | |
writer.add_image("resize", img_resize, 0) | |
print(img_resize) |
# Compose
from torchvision import transforms | |
from torch.utils.tensorboard import SummaryWriter | |
from PIL import Image | |
img_path = "D:\\Projects\\pytorch-learn\\dataset\\train\\ants\\0013035.jpg" | |
img = Image.open(img_path) | |
# ToTensor | |
tensor_trans = transforms.ToTensor() | |
writer = SummaryWriter('logs') | |
# Resize | |
trans_resize_2 = transforms.Resize(512) # 图片等比例缩放 | |
trans_compose = transforms.Compose([trans_resize_2, tensor_trans]) # 注意前后参数类型的匹配 | |
img_resize_2 = trans_compose(img) | |
writer.add_image("resize", img_resize_2, 2) | |
print(img_resize_2) |
# torchvision 数据集使用
官方文档
import torchvision | |
from torch.utils.tensorboard import SummaryWriter | |
train_set = torchvision.datasets.CIFAR10(root="../dataset", train=True, download=True) | |
test_set = torchvision.datasets.CIFAR10(root="../dataset", train=False, download=True) | |
print(test_set[0]) # 返回图片和类别(这里的类别是编号,编号和类别名称是对应的) | |
print(test_set.classes) # 类别的名称 | |
img, target = test_set[0] | |
print(img) | |
print(target) | |
print(test_set.classes[target]) | |
img.show() | |
print(test_set[0]) |
import torchvision | |
from torch.utils.tensorboard import SummaryWriter | |
dataset_transform = torchvision.transforms.Compose([ | |
torchvision.transforms.ToTensor() | |
]) | |
# 对每张图片进行 transform | |
train_set = torchvision.datasets.CIFAR10(root="../dataset", train=True, transform=dataset_transform, download=True) | |
test_set = torchvision.datasets.CIFAR10(root="../dataset", train=False, transform=dataset_transform, download=True) | |
writer = SummaryWriter("logs") | |
for i in range(10): | |
img, target = test_set[i] | |
writer.add_image("test_set", img, i) | |
writer.close() |
# dataloader
import torchvision | |
# 准备的测试数据集 | |
from torch.utils.data import DataLoader | |
from torch.utils.tensorboard import SummaryWriter | |
test_data = torchvision.datasets.CIFAR10("../dataset", train=False, transform=torchvision.transforms.ToTensor()) | |
test_loader = DataLoader(dataset=test_data, batch_size=64, shuffle=True, num_workers=0, drop_last=True) | |
# 测试数据集中第一张图片及 target | |
img, target = test_data[0] | |
print(img.shape) # torch.Size([3, 32, 32]) | |
print(target) # 3 | |
for data in test_loader: | |
imgs, targets = data | |
print(imgs.shape) | |
print(targets) | |
''' | |
torch.Size([64, 3, 32, 32]) | |
tensor([3, 2, 9, 2, 7, 3, 0, 8, 7, 2, 8, 3, 0, 7, 7, 1, 7, 0, 5, 5, 5, 0, 4, 4, | |
3, 1, 0, 3, 0, 5, 8, 7, 1, 1, 4, 8, 1, 5, 4, 7, 9, 8, 9, 1, 3, 2, 6, 0, | |
2, 2, 1, 0, 4, 9, 5, 3, 8, 2, 6, 2, 0, 7, 4, 1]) | |
''' | |
writer = SummaryWriter("dataloader") | |
for epoch in range(2): | |
step = 0 | |
for data in test_loader: | |
imgs, targets = data | |
# print(imgs.shape) | |
# print(targets) | |
writer.add_images("Epoch: {}".format(epoch), imgs, step) | |
step = step + 1 | |
writer.close() |
# nn
# 简单例子
import torch | |
from torch import nn | |
class Tudui(nn.Module): | |
def __init__(self): | |
super().__init__() | |
def forward(self, input): | |
output = input + 1 | |
return output | |
tudui = Tudui() | |
x = torch.tensor(1.0) | |
output = tudui(x) | |
print(output) |
# 卷积
对应位相乘后最终再相加
import torch | |
import torch.nn.functional as F | |
input = torch.tensor([[1, 2, 0, 3, 1], | |
[0, 1, 2, 3, 1], | |
[1, 2, 1, 0, 0], | |
[5, 2, 3, 1, 1], | |
[2, 1, 0, 1, 1]]) | |
kernel = torch.tensor([[1, 2, 1], | |
[0, 1, 0], | |
[2, 1, 0]]) | |
input = torch.reshape(input, (1, 1, 5, 5)) | |
kernel = torch.reshape(kernel, (1, 1, 3, 3)) | |
print(input.shape) | |
print(kernel.shape) | |
output = F.conv2d(input, kernel, stride=1) | |
print(output) | |
output2 = F.conv2d(input, kernel, stride=2) | |
print(output2) | |
output3 = F.conv2d(input, kernel, stride=1, padding=1) | |
print(output3) | |
''' | |
torch.Size([1, 1, 5, 5]) | |
torch.Size([1, 1, 3, 3]) | |
tensor([[[[10, 12, 12], | |
[18, 16, 16], | |
[13, 9, 3]]]]) | |
tensor([[[[10, 12], | |
[13, 3]]]]) | |
tensor([[[[ 1, 3, 4, 10, 8], | |
[ 5, 10, 12, 12, 6], | |
[ 7, 18, 16, 16, 8], | |
[11, 13, 9, 3, 4], | |
[14, 13, 9, 7, 4]]]]) | |
''' |
import torch | |
import torchvision | |
from torch import nn | |
from torch.nn import Conv2d | |
from torch.utils.data import DataLoader | |
from torch.utils.tensorboard import SummaryWriter | |
dataset = torchvision.datasets.CIFAR10("../dataset", train=False, transform=torchvision.transforms.ToTensor(), | |
download=True) | |
dataloader = DataLoader(dataset, batch_size=64) | |
class Tudui(nn.Module): | |
def __init__(self): | |
super(Tudui, self).__init__() | |
self.conv1 = Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1, padding=0) | |
def forward(self, x): | |
x = self.conv1(x) | |
return x | |
tudui = Tudui() | |
print(tudui) | |
''' | |
Tudui( | |
(conv1): Conv2d(3, 6, kernel_size=(3, 3), stride=(1, 1)) | |
) | |
''' | |
writer = SummaryWriter("../logs") | |
step = 0 | |
for data in dataloader: | |
imgs, targets = data | |
output = tudui(imgs) | |
print(imgs.shape) | |
print(output.shape) | |
# torch.Size([64, 3, 32, 32]) | |
writer.add_images("input", imgs, step) | |
# torch.Size([64, 6, 30, 30]) -> [xxx, 3, 30, 30] | |
output = torch.reshape(output, (-1, 3, 30, 30)) | |
writer.add_images("output", output, step) | |
step = step + 1 | |
writer.close() |
# 最大池化
注意池化核和卷积核在图像上的移动方式不同。
池化的目的是保留输入的特征,但同时把数据量减小。像把 1080p 的视频变成 720p 的
import torch | |
import torchvision | |
from torch import nn | |
from torch.nn import MaxPool2d | |
from torch.utils.data import DataLoader | |
from torch.utils.tensorboard import SummaryWriter | |
dataset = torchvision.datasets.CIFAR10("../data", train=False, download=True, | |
transform=torchvision.transforms.ToTensor()) | |
dataloader = DataLoader(dataset, batch_size=64) | |
class Tudui(nn.Module): | |
def __init__(self): | |
super(Tudui, self).__init__() | |
self.maxpool1 = MaxPool2d(kernel_size=3, ceil_mode=False) | |
def forward(self, input): | |
output = self.maxpool1(input) | |
return output | |
tudui = Tudui() | |
writer = SummaryWriter("../logs_maxpool") | |
step = 0 | |
for data in dataloader: | |
imgs, targets = data | |
writer.add_images("input", imgs, step) | |
output = tudui(imgs) | |
writer.add_images("output", output, step) | |
step = step + 1 | |
writer.close() |
# 非线性激活
- relu
- sigmoid
import torch | |
import torchvision | |
from torch import nn | |
from torch.nn import ReLU, Sigmoid | |
from torch.utils.data import DataLoader | |
from torch.utils.tensorboard import SummaryWriter | |
input = torch.tensor([[1, -0.5], | |
[-1, 3]]) | |
input = torch.reshape(input, (-1, 1, 2, 2)) | |
print(input.shape) | |
class Tudui(nn.Module): | |
def __init__(self): | |
super(Tudui, self).__init__() | |
self.relu1 = ReLU() | |
def forward(self, input): | |
output = self.relu1(input) | |
return output | |
tudui = Tudui() | |
output = tudui(input) | |
print(output) | |
''' | |
torch.Size([1, 1, 2, 2]) | |
tensor([[[[1., 0.], | |
[0., 3.]]]]) | |
''' |
import torch | |
import torchvision | |
from torch import nn | |
from torch.nn import ReLU, Sigmoid | |
from torch.utils.data import DataLoader | |
from torch.utils.tensorboard import SummaryWriter | |
dataset = torchvision.datasets.CIFAR10("../dataset", train=False, download=True, | |
transform=torchvision.transforms.ToTensor()) | |
dataloader = DataLoader(dataset, batch_size=64) | |
class Tudui(nn.Module): | |
def __init__(self): | |
super(Tudui, self).__init__() | |
self.relu1 = ReLU() | |
self.sigmoid1 = Sigmoid() | |
def forward(self, input): | |
output = self.sigmoid1(input) | |
return output | |
tudui = Tudui() | |
writer = SummaryWriter("./logs_relu") | |
step = 0 | |
for data in dataloader: | |
imgs, targets = data | |
writer.add_images("input", imgs, global_step=step) | |
output = tudui(imgs) | |
writer.add_images("output", output, step) | |
step += 1 | |
writer.close() |
非线性变换的目的主要是为网络引入非线性特征,非线性越多,才能训练出符合各种曲线,符合各种特征的模型,加强模型的泛化能力。
# 线性层及其他层
# 正则化层
加快网络的训练速度
# 线性层
import torch | |
import torchvision | |
from torch import nn | |
from torch.nn import Linear | |
from torch.utils.data import DataLoader | |
dataset = torchvision.datasets.CIFAR10("../dataset", train=False, transform=torchvision.transforms.ToTensor(), | |
download=True) | |
dataloader = DataLoader(dataset, batch_size=64, drop_last=True) | |
class Tudui(nn.Module): | |
def __init__(self): | |
super(Tudui, self).__init__() | |
self.linear1 = Linear(196608, 10) | |
def forward(self, input): | |
output = self.linear1(input) | |
return output | |
tudui = Tudui() | |
for data in dataloader: | |
imgs, targets = data | |
print(imgs.shape) | |
output = torch.reshape(imgs, (1, 1, 1, -1)) | |
print(output.shape) | |
output = tudui(output) | |
print(output.shape) | |
''' | |
torch.Size([64, 3, 32, 32]) | |
torch.Size([1, 1, 1, 196608]) | |
torch.Size([1, 1, 1, 10]) | |
线性层 | |
''' |
import torch | |
import torchvision | |
from torch import nn | |
from torch.nn import Linear | |
from torch.utils.data import DataLoader | |
dataset = torchvision.datasets.CIFAR10("../dataset", train=False, transform=torchvision.transforms.ToTensor(), | |
download=True) | |
dataloader = DataLoader(dataset, batch_size=64, drop_last=True) | |
class Tudui(nn.Module): | |
def __init__(self): | |
super(Tudui, self).__init__() | |
self.linear1 = Linear(196608, 10) | |
def forward(self, input): | |
output = self.linear1(input) | |
return output | |
tudui = Tudui() | |
for data in dataloader: | |
imgs, targets = data | |
print(imgs.shape) | |
output = torch.flatten(imgs) | |
print(output.shape) | |
output = tudui(output) | |
print(output.shape) | |
''' | |
torch.Size([64, 3, 32, 32]) | |
torch.Size([196608]) | |
torch.Size([10]) | |
''' |
# Sequential
求取卷积时候的是 padding 和 stride
dilation 默认值是 1
import torch | |
from torch import nn | |
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential | |
from torch.utils.tensorboard import SummaryWriter | |
class Tudui(nn.Module): | |
def __init__(self): | |
super(Tudui, self).__init__() | |
self.model1 = Sequential( | |
Conv2d(3, 32, 5, padding=2), | |
MaxPool2d(2), | |
Conv2d(32, 32, 5, padding=2), | |
MaxPool2d(2), | |
Conv2d(32, 64, 5, padding=2), | |
MaxPool2d(2), | |
Flatten(), | |
Linear(1024, 64), | |
Linear(64, 10) | |
) | |
def forward(self, x): | |
x = self.model1(x) | |
return x | |
tudui = Tudui() | |
print(tudui) | |
input = torch.ones((64, 3, 32, 32)) | |
output = tudui(input) | |
print(output.shape) | |
writer = SummaryWriter("./logs_seq") | |
writer.add_graph(tudui, input) | |
writer.close() |
# 损失函数和反向传播
针对分类问题
import torch | |
from torch.nn import L1Loss | |
from torch import nn | |
inputs = torch.tensor([1, 2, 3], dtype=torch.float32) | |
targets = torch.tensor([1, 2, 5], dtype=torch.float32) | |
inputs = torch.reshape(inputs, (1, 1, 1, 3)) | |
targets = torch.reshape(targets, (1, 1, 1, 3)) | |
loss = L1Loss(reduction='sum') | |
result = loss(inputs, targets) | |
loss_mse = nn.MSELoss() | |
result_mse = loss_mse(inputs, targets) | |
print(result) | |
print(result_mse) | |
x = torch.tensor([0.1, 0.2, 0.3]) | |
y = torch.tensor([1]) | |
x = torch.reshape(x, (1, 3)) | |
loss_cross = nn.CrossEntropyLoss() | |
result_cross = loss_cross(x, y) | |
print(result_cross) |
import torchvision | |
from torch import nn | |
from torch.nn import Sequential, Conv2d, MaxPool2d, Flatten, Linear | |
from torch.utils.data import DataLoader | |
dataset = torchvision.datasets.CIFAR10("../dataset", train=False, transform=torchvision.transforms.ToTensor(), | |
download=True) | |
dataloader = DataLoader(dataset, batch_size=1) | |
class Tudui(nn.Module): | |
def __init__(self): | |
super(Tudui, self).__init__() | |
self.model1 = Sequential( | |
Conv2d(3, 32, 5, padding=2), | |
MaxPool2d(2), | |
Conv2d(32, 32, 5, padding=2), | |
MaxPool2d(2), | |
Conv2d(32, 64, 5, padding=2), | |
MaxPool2d(2), | |
Flatten(), | |
Linear(1024, 64), | |
Linear(64, 10) | |
) | |
def forward(self, x): | |
x = self.model1(x) | |
return x | |
loss = nn.CrossEntropyLoss() | |
tudui = Tudui() | |
for data in dataloader: | |
imgs, targets = data | |
outputs = tudui(imgs) | |
result_loss = loss(outputs, targets) | |
print(result_loss) |
# 优化器
# 基本使用
import torch | |
import torchvision | |
from torch import nn | |
from torch.nn import Sequential, Conv2d, MaxPool2d, Flatten, Linear | |
from torch.optim.lr_scheduler import StepLR | |
from torch.utils.data import DataLoader | |
dataset = torchvision.datasets.CIFAR10("../dataset", train=False, transform=torchvision.transforms.ToTensor(), | |
download=True) | |
dataloader = DataLoader(dataset, batch_size=1) | |
class Tudui(nn.Module): | |
def __init__(self): | |
super(Tudui, self).__init__() | |
self.model1 = Sequential( | |
Conv2d(3, 32, 5, padding=2), | |
MaxPool2d(2), | |
Conv2d(32, 32, 5, padding=2), | |
MaxPool2d(2), | |
Conv2d(32, 64, 5, padding=2), | |
MaxPool2d(2), | |
Flatten(), | |
Linear(1024, 64), | |
Linear(64, 10) | |
) | |
def forward(self, x): | |
x = self.model1(x) | |
return x | |
loss = nn.CrossEntropyLoss() | |
tudui = Tudui() | |
optim = torch.optim.SGD(tudui.parameters(), lr=0.01) | |
for epoch in range(20): | |
running_loss = 0.0 | |
for data in dataloader: | |
imgs, targets = data | |
outputs = tudui(imgs) | |
result_loss = loss(outputs, targets) | |
optim.zero_grad() # 把模型中可被调节的参数的梯度设置为 0 | |
result_loss.backward() # 得到可被调节的参数的梯度 | |
optim.step() # 对参数调优 | |
running_loss = running_loss + result_loss | |
print(running_loss) |
理解:在优化器的创建中传入了网络模型的参数,根据后文优化器能够对模型的参数进行修改可以推测,在创建中传入的参数是引用参数,所以在反向传播计算出梯度之后,优化器能够直接修改到模型的参数。
# 优化器的选择
Pytorch 中有四种常用的优化器,SGD、Momentum、RMSProp、Adam,那我们该如何选择呢。
# 1.SGD
参数介绍:
--lr (float) : 学习率
--momentum (float,可选):动量因子(默认为 0)
--weight_decay (float,可选):权重衰减(L2 惩罚,默认为 0)
--dampening (float,可选):动量的抑制因子(默认为 0)
--nesterov (bool,可选):使用 Nesterov 动量(默认为 false)
示例代码:
optimizer = torch.optim.SGD(model.parameters(), lr=0.01) |
优缺点:
优点:使用 mini-batch 的时候,模型可以收敛的更快
缺点:在随机选择梯度的时候会引入噪声,使得权重更新的方向不一定正确。
不能解决局部最优解的问题。
推荐指数: 0 星
# 2.RMSProp (Root Mean Square Prop,均方根传递)
RMSProp 是 RProp 的改进版,同时也是 Adagard 的改进版,它的思想上:在梯度震动较大的项,在下降时,减小其下降速度;对于震动幅度较小的项,在下降时,加速其下降速度。同时 RMSProp 采用均方根作为分母,可以缓解 Adagard 学习率下降过快的问题。
其对于 RNN 具有很好的效果。
参数介绍:
--lr (float) : 学习率
--momentum (float,可选):动量因子(默认为 0)
--alpha(float,可选):平滑常数(默认:0.99)
--eps (float, 可选):为了增加数值计算的稳定性而加到分母里的项(默认:1e-8)
--centered (bool, 可选):如果为 True,计算中心化的 RMSProp,并且用它的方差预测值对梯度进行归一化
--weight_decay (float,可选):权重衰减(L2 惩罚,默认为 0)
示例代码:
optimizer = torch.optim.RMSprop(model.parameters(), lr=0.01) |
优缺点:
优点:可缓解 Adagrad 学习率下降较快的问题,并且引入均方根,可以减少摆动,适合处理非平稳目标,对于 RNN 效果很好。
缺点:依然依赖于全局学习率。
推荐程度:四星半 RMSProp 算法已被证明是一种有效且实用的深度神经网络优化算法。目前它是深度学习从业者经常采用的优化算法之一。
# 3.Adam(AMSGrad)
它是一种将 Momentum 算法和 RMSProp 算法结合起来而使用的一种算法,既使用动量来累计梯度,又使得收敛速度更快同时使得波动的幅度更小,并进行偏差修正。
参数介绍:
--lr (float) : 学习率
--betas (Tuple [float,float], 可选):用于计算梯度以及梯度平方的运行平均值的系数(默认:0.9,0.999)
--eps (float, 可选):为了增加数值计算的稳定性而加到分母里的项(默认:1e-8)
--weight_decay (float,可选):权重衰减(L2 惩罚,默认为 0)
示例代码:
optimizer = torch.optim.Adam(model.parameters(),lr=0.01) |
优点:
对目标函数没有平稳要求,即 loss function 可以随着时间变化。
参数的更新不受梯度的伸缩变换影响。
更新的步长能够被限制在大致的范围内(初始学习率)。
能较好的处理噪音样本,能天然地实现步长退火过程(自动调整学习率)。
很适合应用于大规模的数据及参数的场景、不稳定目标函数、梯度稀疏或梯度存在很大噪声的问题。
推荐程度:五星。 非常推荐,基本上是目前最常用的优化方法。
四种常用优化器 Loss 函数比较图
对于重要参数学习率,这里推荐设置 1e-3 或者 1e-2.
# 现有模型的修改和使用
import torchvision | |
# train_data = torchvision.datasets.ImageNet("../data_image_net", split='train', download=True, | |
# transform=torchvision.transforms.ToTensor()) | |
from torch import nn | |
vgg16_false = torchvision.models.vgg16(pretrained=False) | |
vgg16_true = torchvision.models.vgg16(pretrained=True) | |
print(vgg16_true) | |
train_data = torchvision.datasets.CIFAR10('../data', train=True, transform=torchvision.transforms.ToTensor(), | |
download=True) | |
# vgg16_true.add_module('add_linear', nn.Linear(1000, 10)) | |
vgg16_true.classifier.add_module('add_linear', nn.Linear(1000, 10)) | |
print(vgg16_true) | |
print(vgg16_false) | |
vgg16_false.classifier[6] = nn.Linear(4096, 10) | |
print(vgg16_false) |
# 网络模型保存与读取
# 保存
# -*- coding: utf-8 -*- | |
# 作者:小土堆 | |
# 公众号:土堆碎念 | |
import torch | |
import torchvision | |
from torch import nn | |
vgg16 = torchvision.models.vgg16(pretrained=False) | |
# 保存方式 1, 模型结构 + 模型参数 | |
torch.save(vgg16, "vgg16_method1.pth") | |
# 保存方式 2,模型参数(官方推荐) 以字典的形式保存模型参数 | |
torch.save(vgg16.state_dict(), "vgg16_method2.pth") | |
# 陷阱 | |
class Tudui(nn.Module): | |
def __init__(self): | |
super(Tudui, self).__init__() | |
self.conv1 = nn.Conv2d(3, 64, kernel_size=3) | |
def forward(self, x): | |
x = self.conv1(x) | |
return x | |
tudui = Tudui() | |
torch.save(tudui, "tudui_method1.pth") |
# 读取
# -*- coding: utf-8 -*- | |
# 作者:小土堆 | |
# 公众号:土堆碎念 | |
import torch | |
from model_save import * | |
# 方式 1-》保存方式 1,加载模型 | |
import torchvision | |
from torch import nn | |
model = torch.load("vgg16_method1.pth") | |
# print(model) | |
# 方式 2,加载模型 | |
vgg16 = torchvision.models.vgg16(pretrained=False) | |
vgg16.load_state_dict(torch.load("vgg16_method2.pth")) | |
# model = torch.load("vgg16_method2.pth") | |
# print(vgg16) | |
# 陷阱 1 | |
# class Tudui(nn.Module): | |
# def __init__(self): | |
# super(Tudui, self).__init__() | |
# self.conv1 = nn.Conv2d(3, 64, kernel_size=3) | |
# | |
# def forward(self, x): | |
# x = self.conv1(x) | |
# return x | |
model = torch.load('tudui_method1.pth') | |
print(model) |
# 利用 GPU 进行训练
找到网络模型、数据(输入、标注)和标注、损失函数, .cuda()
后返回即可。
# 方式一
import torch | |
import torchvision | |
from torch.utils.tensorboard import SummaryWriter | |
# 准备数据集 | |
from torch import nn | |
from torch.utils.data import DataLoader | |
train_data = torchvision.datasets.CIFAR10(root="../dataset", train=True, transform=torchvision.transforms.ToTensor(), | |
download=True) | |
test_data = torchvision.datasets.CIFAR10(root="../dataset", train=False, transform=torchvision.transforms.ToTensor(), | |
download=True) | |
# length 长度 | |
train_data_size = len(train_data) | |
test_data_size = len(test_data) | |
# 如果 train_data_size=10, 训练数据集的长度为:10 | |
print("训练数据集的长度为:{}".format(train_data_size)) | |
print("测试数据集的长度为:{}".format(test_data_size)) | |
# 利用 DataLoader 来加载数据集 | |
train_dataloader = DataLoader(train_data, batch_size=64) | |
test_dataloader = DataLoader(test_data, batch_size=64) | |
# 创建网络模型 | |
class Tudui(nn.Module): | |
def __init__(self): | |
super(Tudui, self).__init__() | |
self.model = nn.Sequential( | |
nn.Conv2d(3, 32, 5, 1, 2), | |
nn.MaxPool2d(2), | |
nn.Conv2d(32, 32, 5, 1, 2), | |
nn.MaxPool2d(2), | |
nn.Conv2d(32, 64, 5, 1, 2), | |
nn.MaxPool2d(2), | |
nn.Flatten(), | |
nn.Linear(64*4*4, 64), | |
nn.Linear(64, 10) | |
) | |
def forward(self, x): | |
x = self.model(x) | |
return x | |
tudui = Tudui() | |
# 先检测 GPU 是否可用 | |
if torch.cuda.is_available(): | |
tudui = tudui.cuda() | |
# 损失函数 | |
loss_fn = nn.CrossEntropyLoss() | |
if torch.cuda.is_available(): | |
loss_fn = loss_fn.cuda() | |
# 优化器 | |
# learning_rate = 0.01 | |
# 1e-2=1 x (10)^(-2) = 1 /100 = 0.01 | |
learning_rate = 1e-2 | |
optimizer = torch.optim.SGD(tudui.parameters(), lr=learning_rate) | |
# 设置训练网络的一些参数 | |
# 记录训练的次数 | |
total_train_step = 0 | |
# 记录测试的次数 | |
total_test_step = 0 | |
# 训练的轮数 | |
epoch = 10 | |
# 添加 tensorboard | |
# writer = SummaryWriter("../logs_train") | |
for i in range(epoch): | |
print("-------第 {} 轮训练开始-------".format(i+1)) | |
# 训练步骤开始 | |
tudui.train() | |
for data in train_dataloader: | |
imgs, targets = data | |
if torch.cuda.is_available(): | |
imgs = imgs.cuda() | |
targets = targets.cuda() | |
outputs = tudui(imgs) | |
loss = loss_fn(outputs, targets) | |
# 优化器优化模型 | |
optimizer.zero_grad() | |
loss.backward() | |
optimizer.step() | |
total_train_step = total_train_step + 1 | |
if total_train_step % 100 == 0: | |
print("训练次数:{}, Loss: {}".format(total_train_step, loss.item())) | |
# writer.add_scalar("train_loss", loss.item(), total_train_step) | |
# 测试步骤开始 | |
tudui.eval() | |
total_test_loss = 0 | |
total_accuracy = 0 | |
with torch.no_grad(): | |
for data in test_dataloader: | |
imgs, targets = data | |
if torch.cuda.is_available(): | |
imgs = imgs.cuda() | |
targets = targets.cuda() | |
outputs = tudui(imgs) | |
loss = loss_fn(outputs, targets) | |
total_test_loss = total_test_loss + loss.item() | |
accuracy = (outputs.argmax(1) == targets).sum() | |
total_accuracy = total_accuracy + accuracy | |
print("整体测试集上的Loss: {}".format(total_test_loss)) | |
print("整体测试集上的正确率: {}".format(total_accuracy/test_data_size)) | |
# writer.add_scalar("test_loss", total_test_loss, total_test_step) | |
# writer.add_scalar("test_accuracy", total_accuracy/test_data_size, total_test_step) | |
total_test_step = total_test_step + 1 | |
torch.save(tudui, "tudui_{}.pth".format(i)) | |
print("模型已保存") | |
# writer.close() |
# 方式二
import torch | |
import torchvision | |
from torch.utils.tensorboard import SummaryWriter | |
# 准备数据集 | |
from torch import nn | |
from torch.utils.data import DataLoader | |
# 定义训练的设备 | |
device = torch.device("cuda") | |
train_data = torchvision.datasets.CIFAR10(root="../dataset", train=True, transform=torchvision.transforms.ToTensor(), | |
download=True) | |
test_data = torchvision.datasets.CIFAR10(root="../dataset", train=False, transform=torchvision.transforms.ToTensor(), | |
download=True) | |
# length 长度 | |
train_data_size = len(train_data) | |
test_data_size = len(test_data) | |
# 如果 train_data_size=10, 训练数据集的长度为:10 | |
print("训练数据集的长度为:{}".format(train_data_size)) | |
print("测试数据集的长度为:{}".format(test_data_size)) | |
# 利用 DataLoader 来加载数据集 | |
train_dataloader = DataLoader(train_data, batch_size=64) | |
test_dataloader = DataLoader(test_data, batch_size=64) | |
# 创建网络模型 | |
class Tudui(nn.Module): | |
def __init__(self): | |
super(Tudui, self).__init__() | |
self.model = nn.Sequential( | |
nn.Conv2d(3, 32, 5, 1, 2), | |
nn.MaxPool2d(2), | |
nn.Conv2d(32, 32, 5, 1, 2), | |
nn.MaxPool2d(2), | |
nn.Conv2d(32, 64, 5, 1, 2), | |
nn.MaxPool2d(2), | |
nn.Flatten(), | |
nn.Linear(64*4*4, 64), | |
nn.Linear(64, 10) | |
) | |
def forward(self, x): | |
x = self.model(x) | |
return x | |
tudui = Tudui() | |
tudui = tudui.to(device) | |
# 损失函数 | |
loss_fn = nn.CrossEntropyLoss() | |
loss_fn = loss_fn.to(device) | |
# 优化器 | |
# learning_rate = 0.01 | |
# 1e-2=1 x (10)^(-2) = 1 /100 = 0.01 | |
learning_rate = 1e-2 | |
optimizer = torch.optim.SGD(tudui.parameters(), lr=learning_rate) | |
# 设置训练网络的一些参数 | |
# 记录训练的次数 | |
total_train_step = 0 | |
# 记录测试的次数 | |
total_test_step = 0 | |
# 训练的轮数 | |
epoch = 10 | |
# 添加 tensorboard | |
# writer = SummaryWriter("../logs_train") | |
for i in range(epoch): | |
print("-------第 {} 轮训练开始-------".format(i+1)) | |
# 训练步骤开始 | |
tudui.train() | |
for data in train_dataloader: | |
imgs, targets = data | |
imgs = imgs.to(device) | |
targets = targets.to(device) | |
outputs = tudui(imgs) | |
loss = loss_fn(outputs, targets) | |
# 优化器优化模型 | |
optimizer.zero_grad() | |
loss.backward() | |
optimizer.step() | |
total_train_step = total_train_step + 1 | |
if total_train_step % 100 == 0: | |
print("训练次数:{}, Loss: {}".format(total_train_step, loss.item())) | |
# writer.add_scalar("train_loss", loss.item(), total_train_step) | |
# 测试步骤开始 | |
tudui.eval() | |
total_test_loss = 0 | |
total_accuracy = 0 | |
with torch.no_grad(): | |
for data in test_dataloader: | |
imgs, targets = data | |
imgs = imgs.to(device) | |
targets = targets.to(device) | |
outputs = tudui(imgs) | |
loss = loss_fn(outputs, targets) | |
total_test_loss = total_test_loss + loss.item() | |
accuracy = (outputs.argmax(1) == targets).sum() | |
total_accuracy = total_accuracy + accuracy | |
print("整体测试集上的Loss: {}".format(total_test_loss)) | |
print("整体测试集上的正确率: {}".format(total_accuracy/test_data_size)) | |
# writer.add_scalar("test_loss", total_test_loss, total_test_step) | |
# writer.add_scalar("test_accuracy", total_accuracy/test_data_size, total_test_step) | |
total_test_step = total_test_step + 1 | |
torch.save(tudui, "tudui_{}.pth".format(i)) | |
print("模型已保存") | |
# writer.close() |
# 下面是原博客内容
# Tensor
Tensor 类似于 Numpy 的 ndarray,但是可以用 GPU 加速,使用前我们需要导入 torch 包。
import torch | |
# 下面代码构建一个 5x3 的未初始化的矩阵 | |
x = torch.empty(5,3) | |
print(x) | |
# 输出 | |
tensor([[9.9184e-39, 8.7245e-39, 9.2755e-39], | |
[8.9082e-39, 9.9184e-39, 8.4490e-39], | |
[9.6429e-39, 1.0653e-38, 1.0469e-38], | |
[4.2246e-39, 1.0378e-38, 9.6429e-39], | |
[9.2755e-39, 9.7346e-39, 1.0745e-38]]) |
下面代码分别构造一个零初始化的矩阵,它的类型 (dtype) 是 long:
x = torch.zeros(5,3 dtype=torch.long) | |
print(x) | |
# 输出 | |
tensor([[0, 0, 0], | |
[0, 0, 0], | |
[0, 0, 0], | |
[0, 0, 0], | |
[0, 0, 0]]) |
我们可以从已有的 tensor 信息 (size 和 dtype) 来构造 tensor。但也可以用不同的 dtype 来构造。
x = x.new_ones(5, 3, dtype=torch.double) # new_* methods take in sizes | |
print(x) | |
x = torch.randn_like(x, dtype=torch.float) # override dtype! | |
print(x) |
我们可以是用 size 函数来看它的 shape:
print(x.size()) | |
#输出: | |
torch.Size([5, 3]) |
注意 torch.Size 其实是一个 tuple,因此它支持所有的 tuple 操作。
# Operation
接下来我们来学习一些 PyTorch 的 Operation。Operation 一般可以使用函数的方式使用,但是为了方便使用,PyTorch 重载了一些常见的运算符,因此我们可以这样来进行 Tensor 的加法:
y = torch.rand(5, 3) | |
print(x + y) |
我们也可以用 add 函数来实现加法:
print(torch.add(x, y)) |
我们也可以给加法提供返回值 (而不是生成一个新的返回值):
result = torch.empty(5, 3) | |
torch.add(x, y, out=result) # x + y 的结果放到 result 里。 | |
print(result) |
我们也可以把相加的结果直接修改第一个被加数:
# 把 x 加到 y | |
y.add_(x) | |
print(y) |
注意:就地修改 tensor 的 operation 以下划线结尾。比如: x.copy_(y), x.t_(), 都会修改 x。
# Tensor 的变换
我们也可以使用类似 numpy 的下标运算来操作 PyTorch 的 Tensor:
#打印 x 的第一列 | |
print(x[:, 1]) |
如果想 resize 或者 reshape 一个 Tensor,我们可以使用 torch.view:
x = torch.randn(4, 4) | |
y = x.view(16) | |
z = x.view(-1, 8) # -1 的意思是让 PyTorch 自己推断出第一维的大小。 | |
print(x.size(), y.size(), z.size()) |
如果一个 tensor 只有一个元素,可以使用 item () 函数来把它变成一个 Python number:
x = torch.randn(1) | |
print(x) | |
#输出的是一个 Tensor | |
tensor([-0.6966]) | |
print(x.item()) | |
#输出的是一个数 | |
-0.6966081857681274 |
# Tensor 与 Numpy 的互相转换
Torch Tensor 和 NumPy 数组的转换非常容易。它们会共享内存地址,因此修改一方会影响另一方。把一个 Torch Tensor 转换成 NumPy 数组的代码示例为:
a = torch.ones(5) | |
print(a) | |
#tensor([ 1., 1., 1., 1., 1.]) | |
b = a.numpy() | |
print(b) | |
#[1. 1. 1. 1. 1.] |
修改一个会影响另外一个:
a.add_(1) | |
print(a) | |
# tensor([ 2., 2., 2., 2., 2.]) | |
print(b) | |
# [2. 2. 2. 2. 2.] |
把把 NumPy 数组转成 Torch Tensor 的代码示例为:
import numpy as np | |
a = np.ones(5) | |
b = torch.from_numpy(a) | |
np.add(a, 1, out=a) | |
print(a) | |
# [2. 2. 2. 2. 2.] | |
print(b) | |
# tensor([ 2., 2., 2., 2., 2.], dtype=torch.float64) |
CPU 上的所有类型的 Tensor (除了 CharTensor) 都可以和 Numpy 数组来回转换。
# CUDA Tensor
Tensor 可以使用 to () 方法来移到任意设备上:
# 如果有 CUDA | |
# 我们会使用 ``torch.device`` 来把 tensors 放到 GPU 上 | |
if torch.cuda.is_available(): | |
device = torch.device("cuda") # 一个 CUDA device 对象。 | |
y = torch.ones_like(x, device=device) # 直接在 GPU 上创建 tensor | |
x = x.to(device) # 也可以使用 ``.to ("cuda")`` 把一个 tensor 从 CPU 移到 GPU 上 | |
z = x + y | |
print(z) | |
print(z.to("cpu", torch.double)) # ``.to`` 也可以在移动的过程中修改 dtype | |
# 输出: | |
tensor([ 0.3034], device='cuda:0') | |
tensor([ 0.3034], dtype=torch.float64) |
# Autograd: 自动求导
PyTorch 的核心是 autograd 包。 我们首先简单的了解一些,然后用 PyTorch 开始训练第一个神经网络。autograd 为所有用于 Tensor 的 operation 提供自动求导的功能。我们通过一些简单的例子来学习它基本用法。
# 从自动求导看 Tensor
torch.Tensor 是这个包的核心类。如果它的属性 requires_grad 是 True,那么 PyTorch 就会追踪所有与之相关的 operation。当完成 (正向) 计算之后, 我们可以调用 backward (),PyTorch 会自动的把所有的梯度都计算好。与这个 tensor 相关的梯度都会累加到它的 grad 属性里。
如果不想计算这个 tensor 的梯度,我们可以调用 detach (),这样它就不会参与梯度的计算了。为了阻止 PyTorch 记录用于梯度计算相关的信息 (从而节约内存),我们可以使用 with torch.no_grad ()。这在模型的预测时非常有用,因为预测的时候我们不需要计算梯度,否则我们就得一个个的修改 Tensor 的 requires_grad 属性,这会非常麻烦。
关于 autograd 的实现还有一个很重要的 Function 类。Tensor 和 Function 相互连接从而形成一个有向无环图,这个图记录了计算的完整历史。每个 tensor 有一个 grad_fn 属性来引用创建这个 tensor 的 Function (用户直接创建的 Tensor,这些 Tensor 的 grad_fn 是 None)。
如果你想计算梯度,可以对一个 Tensor 调用它的 backward () 方法。如果这个 Tensor 是一个 scalar (只有一个数),那么调用时不需要传任何参数。如果 Tensor 多于一个数,那么需要传入和它的 shape 一样的参数,表示反向传播过来的梯度。
创建 tensor 时设置属性 requires_grad=True,PyTorch 就会记录用于反向梯度计算的信息:
x = torch.ones(2, 2, requires_grad=True) | |
print(x) |
然后我们通过 operation 产生新的 tensor:
y = x + 2 | |
print(y) |
是通过 operation 产生的 tensor,因此它的 grad_fn 不是 None。
print(y.grad_fn) | |
# <AddBackward0 object at 0x7f35409a68d0> |
再通过 y 得到 z 和 out
z = y * y * 3 | |
out = z.mean() | |
print(z, out) | |
# z = tensor([[ 27., 27.],[ 27., 27.]]) | |
# out = tensor(27.) |
requires_grad_() 函数会修改一个 Tensor 的 requires_grad。
a = torch.randn(2, 2) | |
a = ((a * 3) / (a - 1)) | |
print(a.requires_grad) | |
a.requires_grad_(True) | |
print(a.requires_grad) | |
b = (a * a).sum() | |
print(b.grad_fn) |
输出是:
False | |
True | |
<SumBackward0 object at 0x7f35766827f0> |
# 梯度
现在我们里反向计算梯度。因为 out 是一个 scalar,因此 out.backward () 等价于 out.backward (torch.tensor (1))。
out.backward() |
我们可以打印梯度 d (out)/dx:
print(x.grad) | |
# tensor([[ 4.5000, 4.5000], | |
[ 4.5000, 4.5000]]) |
我们手动计算验证一下。为了简单,我们把 记为 o。,,并且。
因此,, 因此,。
我们也可以用 autograd 做一些很奇怪的事情!比如 y 和 x 的关系是 while 循环的关系 (似乎很难用一个函数直接表示 y 和 x 的关系?对 x 不断平方直到超过 1000,这是什么函数?)
x = torch.randn(3, requires_grad=True) | |
y = x * 2 | |
while y.data.norm() < 1000: | |
y = y * 2 | |
print(y) | |
# tensor([ -692.4808, 1686.1211, 667.7313]) | |
gradients = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float) | |
y.backward(gradients) | |
print(x.grad) | |
# tensor([ 102.4000, 1024.0000, 0.1024]) |
我们可以使用”with torch.no_grad ()” 来停止梯度的计算:
print(x.requires_grad) | |
print((x ** 2).requires_grad) | |
with torch.no_grad(): | |
print((x ** 2).requires_grad) |
输出为:
True | |
True | |
False |
# PyTorch 神经网络简介
神经网络可以通过 torch.nn 包来创建。我们之前简单的了解了 autograd,而 nn 会使用 autograd 来定义模型以及求梯度。一个 nn.Module 对象包括了许多网络层 (layer),并且有一个 forward (input) 方法来返回 output。如下图所示,我们会定义一个卷积网络来识别 mnist 图片。
图:识别 MNIST 数据的神经网络
训练一个神经网络通常需要如下步骤:
- 定义一个神经网络,它通常有一些可以训练的参数
- 迭代一个数据集 (dataset)
- 处理网络的输入
- 计算 loss (会调用 Module 对象的 forward 方法)
- 计算 loss 对参数的梯度
- 更新参数,通常使用如下的梯度下降方法来更新: weight = weight - learning_rate * gradient
# 定义网络
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 个通道的灰度图,输出 6 个通道 (feature map),使用 5x5 的卷积核 | |
self.conv1 = nn.Conv2d(1, 6, 5) | |
# 第二个卷积层也是 5x5,有 16 个通道 | |
self.conv2 = nn.Conv2d(6, 16, 5) | |
# 全连接层 | |
self.fc1 = nn.Linear(16 * 5 * 5, 120) | |
self.fc2 = nn.Linear(120, 84) | |
self.fc3 = nn.Linear(84, 10) | |
def forward(self, x): | |
# 32x32 -> 28x28 -> 14x14 | |
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2)) | |
# 14x14 -> 10x10 -> 5x5 | |
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:] # 除了 batch 维度之外的其它维度。 | |
num_features = 1 | |
for s in size: | |
num_features *= s | |
return num_features | |
net = Net() | |
print(net) | |
# Net( | |
(conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1)) | |
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1)) | |
(fc1): Linear(in_features=400, out_features=120, bias=True) | |
(fc2): Linear(in_features=120, out_features=84, bias=True) | |
(fc3): Linear(in_features=84, out_features=10, bias=True) | |
) |
我们只需要定义 forward 函数,而 backward 函数会自动通过 autograd 创建。在 forward 函数里可以使用任何处理 Tensor 的函数。我们可以使用函数 net.parameters () 来得到模型所有的参数。
params = list(net.parameters()) | |
print(len(params)) | |
# 10 | |
print(params[0].size()) # conv1 的 weight | |
# torch.Size([6, 1, 5, 5]) |
# 测试网络
接着我们尝试一个随机的 32x32 的输入来检验 (sanity check) 网络定义没有问题。注意:这个网络 (LeNet) 期望的输入大小是 32x32。如果使用 MNIST 数据集 (28x28),我们需要缩放到 32x32。
input = torch.randn(1, 1, 32, 32) | |
out = net(input) | |
print(out) | |
# tensor([[-0.0198, 0.0438, 0.0930, -0.0267, -0.0344, 0.0330, 0.0664, | |
0.1244, -0.0379, 0.0890]]) |
默认的梯度会累加,因此我们通常在 backward 之前清除掉之前的梯度值:
net.zero_grad() | |
out.backward(torch.randn(1, 10)) |
注意:torch.nn 只支持 mini-batches 的输入。整个 torch.nn 包的输入都必须第一维是 batch,即使只有一个样本也要弄成 batch 是 1 的输入。
比如,nn.Conv2d 的输入是一个 4D 的 Tensor,shape 是 nSamples x nChannels x Height x Width。如果你只有一个样本 (nChannels x Height x Width),那么可以使用 input.unsqueeze (0) 来增加一个 batch 维。
# 损失函数
损失函数的参数是 (output, target) 对,output 是模型的预测,target 是实际的值。损失函数会计算预测值和真实值的差别,损失越小说明预测的越准。
PyTorch 提供了这里有许多不同的损失函数: http://pytorch.org/docs/nn.html#loss-functions。最简单的一个损失函数是:nn.MSELoss,它会计算预测值和真实值的均方误差。比如:
output = net(input) | |
target = torch.arange(1, 11) # 随便伪造的一个 “真实值” | |
target = target.view(1, -1) # 把它变成 output 的 shape (1, 10) | |
criterion = nn.MSELoss() | |
loss = criterion(output, target) | |
print(loss) |
如果从 loss 往回走,需要使用 tensor 的 grad_fn 属性,我们 Negative 看到这样的计算图:
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d | |
-> view -> linear -> relu -> linear -> relu -> linear | |
-> MSELoss | |
-> loss |
因此当调用 loss.backward () 时,PyTorch 会计算这个图中所有 requires_grad=True 的 tensor 关于 loss 的梯度。
print(loss.grad_fn) # MSELoss | |
print(loss.grad_fn.next_functions[0][0]) # Add | |
print(loss.grad_fn.next_functions[0][0].next_functions[0][0]) # Expand | |
#输出: | |
<MseLossBackward object at 0x7f445b3a2dd8> | |
<AddmmBackward object at 0x7f445b3a2eb8> | |
<ExpandBackward object at 0x7f445b3a2dd8> |
# 计算梯度
在调用 loss.backward () 之前,我们需要清除掉 tensor 里之前的梯度,否则会累加进去。
net.zero_grad() # 清掉 tensor 里缓存的梯度值。 | |
print('conv1.bias.grad before backward') | |
print(net.conv1.bias.grad) | |
loss.backward() | |
print('conv1.bias.grad after backward') | |
print(net.conv1.bias.grad) |
# 更新参数
更新参数最简单的方法是使用随机梯度下降 (SGD): weight=weight−learningrate∗gradientweight=weight−learningrate∗gradient 我们可以使用如下简单的代码来实现更新:
learning_rate = 0.01 | |
for f in net.parameters(): | |
f.data.sub_(f.grad.data * learning_rate) |
通常我们会使用更加复杂的优化方法,比如 SGD, Nesterov-SGD, Adam, RMSProp 等等。为了实现这些算法,我们可以使用 torch.optim 包,它的用法也非常简单:
import torch.optim as optim | |
# 创建 optimizer,需要传入参数和 learning rate | |
optimizer = optim.SGD(net.parameters(), lr=0.01) | |
# 清除梯度 | |
optimizer.zero_grad() | |
output = net(input) | |
loss = criterion(output, target) | |
loss.backward() | |
optimizer.step() # optimizer 会自动帮我们更新参数 |
注意:即使使用 optimizer,我们也需要清零梯度。但是我们不需要一个个的清除,而是用 optimizer.zero_grad () 一次清除所有。
# 训练一个分类器
介绍了 PyTorch 神经网络相关包之后我们就可以用这些知识来构建一个分类器了。
# 如何进行数据处理
一般地,当我们处理图片、文本、音频或者视频数据的时候,我们可以使用 python 代码来把它转换成 numpy 数组。然后再把 numpy 数组转换成 torch.xxxTensor。
- 对于处理图像,常见的 lib 包括 Pillow 和 OpenCV
- 对于音频,常见的 lib 包括 scipy 和 librosa
- 对于文本,可以使用标准的 Python 库,另外比较流行的 lib 包括 NLTK 和 SpaCy
对于视觉问题,PyTorch 提供了一个 torchvision 包 (需要单独安装),它对于常见数据集比如 Imagenet, CIFAR10, MNIST 等提供了加载的方法。并且它也提供很多数据变化的工具,包括 torchvision.datasets 和 torch.utils.data.DataLoader。这会极大的简化我们的工作,避免重复的代码。
在这个教程里,我们使用 CIFAR10 数据集。它包括十个类别:”airplane”, “automobile”, “bird”, “cat”, “deer”, “dog”, “frog”, “horse”, “ship”,”truck”。图像的对象是 3x32x32,也就是 3 通道 (RGB) 的 32x32 的图片。下面是一些样例图片。
图:cifar10 样例
# 训练的步骤
- 使用 torchvision 加载和预处理 CIFAR10 训练和测试数据集。
- 定义卷积网络
- 定义损失函数
- 用训练数据训练模型
- 用测试数据测试模型
# 数据处理
通过使用 torchvision,我们可以轻松的加载 CIFAR10 数据集。首先我们导入相关的包:
import torch | |
import torchvision | |
import torchvision.transforms as transforms |
torchvision 读取的 datasets 是 PILImage 对象,它的取值范围是 [0, 1],我们把它转换到范围 [-1, 1]。
transform = transforms.Compose( | |
[transforms.ToTensor(), | |
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) | |
trainset = torchvision.datasets.CIFAR10(root='/path/to/data', train=True, | |
download=True, transform=transform) | |
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, | |
shuffle=True, num_workers=2) | |
testset = torchvision.datasets.CIFAR10(root='/path/to/data', train=False, | |
download=True, transform=transform) | |
testloader = torch.utils.data.DataLoader(testset, batch_size=4, | |
shuffle=False, num_workers=2) | |
classes = ('plane', 'car', 'bird', 'cat', | |
'deer', 'dog', 'frog', 'horse', 'ship', 'truck') |
我们来看几张图片,如下图所示,显示图片的代码如下:
import matplotlib.pyplot as plt | |
import numpy as np | |
# 显示图片的函数 | |
def imshow(img): | |
img = img / 2 + 0.5 # [-1,1] -> [0,1] | |
npimg = img.numpy() | |
plt.imshow(np.transpose(npimg, (1, 2, 0))) # (channel, width, height) -> (width, height, channel) | |
# 随机选择一些图片 | |
dataiter = iter(trainloader) | |
images, labels = dataiter.next() | |
# 显示图片 | |
imshow(torchvision.utils.make_grid(images)) | |
# 打印 label | |
print(' '.join('%5s' % classes[labels[j]] for j in range(4))) |
图:随机选择的图片
# 定义卷积网络
网络结构和上一节的介绍类似,只是输入通道从 1 变成 3。
import torch.nn as nn | |
import torch.nn.functional as F | |
class Net(nn.Module): | |
def __init__(self): | |
super(Net, self).__init__() | |
self.conv1 = nn.Conv2d(3, 6, 5) | |
self.pool = nn.MaxPool2d(2, 2) | |
self.conv2 = nn.Conv2d(6, 16, 5) | |
self.fc1 = nn.Linear(16 * 5 * 5, 120) | |
self.fc2 = nn.Linear(120, 84) | |
self.fc3 = nn.Linear(84, 10) | |
def forward(self, x): | |
x = self.pool(F.relu(self.conv1(x))) | |
x = self.pool(F.relu(self.conv2(x))) | |
x = x.view(-1, 16 * 5 * 5) | |
x = F.relu(self.fc1(x)) | |
x = F.relu(self.fc2(x)) | |
x = self.fc3(x) | |
return x | |
net = Net() |
\subsubsection {定义损失函数和 optimizer} 我们这里使用交叉熵损失函数,Optimizer 使用带冲量的 SGD。
import torch.optim as optim | |
criterion = nn.CrossEntropyLoss() | |
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) |
\subsubsection {训练网络} 我们遍历 DataLoader 进行训练。
for epoch in range(2): # 这里只迭代 2 个 epoch,实际应该进行更多次训练 | |
running_loss = 0.0 | |
for i, data in enumerate(trainloader, 0): | |
# 得到输入 | |
inputs, labels = data | |
# 梯度清零 | |
optimizer.zero_grad() | |
# forward + backward + optimize | |
outputs = net(inputs) | |
loss = criterion(outputs, labels) | |
loss.backward() | |
optimizer.step() | |
# 定义统计信息 | |
running_loss += loss.item() | |
if i % 2000 == 1999: | |
print('[%d, %5d] loss: %.3f' % | |
(epoch + 1, i + 1, running_loss / 2000)) | |
running_loss = 0.0 | |
print('Finished Training') |
# 在测试数据集上进行测试
我们进行了 2 轮迭代,可以使用测试数据集上的数据来进行测试。首先我们随机抽取几个样本来进行测试。
dataiter = iter(testloader) | |
images, labels = dataiter.next() | |
imshow(torchvision.utils.make_grid(images)) | |
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4))) |
随机选择出来的测试样例如下图所示。
图:随机测试的结果
我们用模型来预测一下,看看是否正确预测:
outputs = net(images) |
outputs 是 10 个分类的 logits。我们在训练的时候需要用 softmax 把它变成概率 (CrossEntropyLoss 帮我们做了),但是预测的时候没有必要,因为我们只需要知道哪个分类的概率大就行。
_, predicted = torch.max(outputs, 1) | |
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]] | |
for j in range(4))) | |
# cat ship ship ship |
预测中的四个错了一个,似乎还不错。接下来我们看看在整个测试集合上的效果:
correct = 0 | |
total = 0 | |
with torch.no_grad(): | |
for data in testloader: | |
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)) | |
# Accuracy of the network on the 10000 test images: 55 % |
看起来比随机的瞎猜要好,因为随机猜的准确率大概是 10% 的准确率,所以模型确实学到了一些东西。我们也可以看每个分类的准确率:
class_correct = list(0. for i in range(10)) | |
class_total = list(0. for i in range(10)) | |
with torch.no_grad(): | |
for data in testloader: | |
images, labels = data | |
outputs = net(images) | |
_, predicted = torch.max(outputs, 1) | |
c = (predicted == labels).squeeze() | |
for i in range(4): | |
label = labels[i] | |
class_correct[label] += c[i].item() | |
class_total[label] += 1 | |
for i in range(10): | |
print('Accuracy of %5s : %2d %%' % ( | |
classes[i], 100 * class_correct[i] / class_total[i])) |
结果为:
Accuracy of plane : 52 % | |
Accuracy of car : 66 % | |
Accuracy of bird : 49 % | |
Accuracy of cat : 34 % | |
Accuracy of deer : 30 % | |
Accuracy of dog : 45 % | |
Accuracy of frog : 72 % | |
Accuracy of horse : 71 % | |
Accuracy of ship : 76 % | |
Accuracy of truck : 55 % |
# GPU 上训练
为了在 GPU 上训练,我们需要把 Tensor 移到 GPU 上。首先我们看看是否有 GPU,如果没有,那么我们还是 fallback 到 CPU。
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") | |
print(device) | |
# cuda:0 |
用 GPU 进行训练:
class Net2(nn.Module): | |
def __init__(self): | |
super(Net2, self).__init__() | |
self.conv1 = nn.Conv2d(3, 6, 5).to(device) | |
self.pool = nn.MaxPool2d(2, 2).to(device) | |
self.conv2 = nn.Conv2d(6, 16, 5).to(device) | |
self.fc1 = nn.Linear(16 * 5 * 5, 120).to(device) | |
self.fc2 = nn.Linear(120, 84).to(device) | |
self.fc3 = nn.Linear(84, 10).to(device) | |
def forward(self, x): | |
x = self.pool(F.relu(self.conv1(x))) | |
x = self.pool(F.relu(self.conv2(x))) | |
x = x.view(-1, 16 * 5 * 5) | |
x = F.relu(self.fc1(x)) | |
x = F.relu(self.fc2(x)) | |
x = self.fc3(x) | |
return x | |
net = Net2() | |
criterion = nn.CrossEntropyLoss() | |
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) | |
for epoch in range(20): | |
running_loss = 0.0 | |
for i, data in enumerate(trainloader, 0): | |
# 得到输入 | |
inputs, labels = data | |
inputs, labels = inputs.to(device), labels.to(device) | |
# 梯度清零 | |
optimizer.zero_grad() | |
# forward + backward + optimize | |
outputs = net(inputs) | |
loss = criterion(outputs, labels) | |
loss.backward() | |
optimizer.step() | |
# 定义统计信息 | |
running_loss += loss.item() | |
if i % 2000 == 1999: | |
print('[%d, %5d] loss: %.3f' % | |
(epoch + 1, i + 1, running_loss / 2000)) | |
running_loss = 0.0 | |
print('Finished Training') |
# 通过例子学 PyTorch
下面我们通过使用不同的方法来实现一个简单的三层 (一个隐层) 的全连接神经网络来熟悉 PyTorch 的常见用法。
# 使用 Numpy 实现三层神经网络
我们需要实现一个全连接的激活为 ReLU 的网络,它只有一个隐层,没有 bias,用于回归预测一个值,loss 是计算实际值和预测值的欧氏距离。这里完全使用 numpy 手动的进行前向和后向计算。numpy 数组就是一个 n 维的数值,它并不知道任何关于深度学习、梯度下降或者计算图的东西,它只是进行数值运算。
import numpy as np | |
# N 是 batch size;D_in 是输入大小 | |
# H 是隐层的大小;D_out 是输出大小。 | |
N, D_in, H, D_out = 64, 1000, 100, 10 | |
# 随机产生输入与输出 | |
x = np.random.randn(N, D_in) | |
y = np.random.randn(N, D_out) | |
# 随机初始化参数 | |
w1 = np.random.randn(D_in, H) | |
w2 = np.random.randn(H, D_out) | |
learning_rate = 1e-6 | |
for t in range(500): | |
# 前向计算 y | |
h = x.dot(w1) | |
h_relu = np.maximum(h, 0) | |
y_pred = h_relu.dot(w2) | |
# 计算 loss | |
loss = np.square(y_pred - y).sum() | |
print(t, loss) | |
# 反向计算梯度 | |
grad_y_pred = 2.0 * (y_pred - y) | |
grad_w2 = h_relu.T.dot(grad_y_pred) | |
grad_h_relu = grad_y_pred.dot(w2.T) | |
grad_h = grad_h_relu.copy() | |
grad_h[h < 0] = 0 | |
grad_w1 = x.T.dot(grad_h) | |
# 更新参数 | |
w1 -= learning_rate * grad_w1 | |
w2 -= learning_rate * grad_w2 |
# 使用 Tensor 来实现三层神经网络
和前面一样,我们还是实现一个全连接的 Relu 激活的网络,它只有一个隐层并且没有 bias。loss 是预测与真实值的欧氏距离。之前我们用 Numpy 实现,自己手动前向计算 loss,反向计算梯度。这里还是一样,只不过把 numpy 数组换成了 PyTorch 的 Tensor。但是使用 PyTorch 的好处是我们可以利用 GPU 来加速计算,如果想用 GPU 计算,我们值需要在创建 tensor 的时候指定 device 为 gpu。
import torch | |
dtype = torch.float | |
device = torch.device("cpu") | |
# device = torch.device ("cuda:0") # 如果想在 GPU 上运算,把这行注释掉。 | |
N, D_in, H, D_out = 64, 1000, 100, 10 | |
x = torch.randn(N, D_in, device=device, dtype=dtype) | |
y = torch.randn(N, D_out, device=device, dtype=dtype) | |
w1 = torch.randn(D_in, H, device=device, dtype=dtype) | |
w2 = torch.randn(H, D_out, device=device, dtype=dtype) | |
learning_rate = 1e-6 | |
for t in range(500): | |
h = x.mm(w1) | |
h_relu = h.clamp(min=0) # 使用 clamp (min=0) 来实现 ReLU | |
y_pred = h_relu.mm(w2) | |
loss = (y_pred - y).pow(2).sum().item() | |
print(t, loss) | |
grad_y_pred = 2.0 * (y_pred - y) | |
grad_w2 = h_relu.t().mm(grad_y_pred) | |
grad_h_relu = grad_y_pred.mm(w2.t()) | |
grad_h = grad_h_relu.clone() | |
grad_h[h < 0] = 0 | |
grad_w1 = x.t().mm(grad_h) | |
w1 -= learning_rate * grad_w1 | |
w2 -= learning_rate * grad_w2 |
# 实现 autograd 来实现三层神经网络
还是和前面一样实现一个全连接的网络,只有一个隐层而且没有 bias,使用欧氏距离作为损失函数。这个实现使用 PyTorch 的 Tensor 来计算前向阶段,然后使用 PyTorch 的 autograd 来自动帮我们反向计算梯度。PyTorch 的 Tensor 代表了计算图中的一个节点。如果 x 是一个 Tensor 并且 x.requires_grad=True,那么 x.grad 这个 Tensor 会保存某个 scalar (通常是 loss) 对 x 的梯度。
import torch | |
dtype = torch.float | |
device = torch.device("cpu") | |
# device = torch.device ("cuda:0") # 如果有 GPU 可以注释掉这行 | |
# N 是 batch size;D_in 是输入大小 | |
# H 是隐层的大小;D_out 是输出大小。 | |
N, D_in, H, D_out = 64, 1000, 100, 10 | |
# 创建随机的 Tensor 作为输入和输出 | |
# 输入和输出需要的 requires_grad=False (默认), | |
# 因为我们不需要计算 loss 对它们的梯度。 | |
x = torch.randn(N, D_in, device=device, dtype=dtype) | |
y = torch.randn(N, D_out, device=device, dtype=dtype) | |
# 创建 weight 的 Tensor,需要设置 requires_grad=True | |
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True) | |
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True) | |
learning_rate = 1e-6 | |
for t in range(500): | |
# Forward 阶段: mm 实现矩阵乘法,但是它不支持 broadcasting。 | |
# 如果需要 broadcasting,可以使用 matmul | |
# clamp 本来的用途是把值 clamp 到指定的范围,这里实现 ReLU。 | |
y_pred = x.mm(w1).clamp(min=0).mm(w2) | |
# pow (2) 实现平方计算。 | |
# loss.item () 得到这个 tensor 的值。也可以直接打印 loss,这会打印很多附加信息。 | |
loss = (y_pred - y).pow(2).sum() | |
print(t, loss.item()) | |
# 使用 autograd 进行反向计算。它会计算 loss 对所有对它有影响的 | |
# requires_grad=True 的 Tensor 的梯度。 | |
loss.backward() | |
# 手动使用梯度下降更新参数。一定要把更新的代码放到 torch.no_grad () 里 | |
# 否则下面的更新也会计算梯度。后面我们会使用 torch.optim.SGD, | |
# 它会帮我们管理这些用于更新梯度的计算。 | |
with torch.no_grad(): | |
w1 -= learning_rate * w1.grad | |
w2 -= learning_rate * w2.grad | |
# 手动把梯度清零 | |
w1.grad.zero_() | |
w2.grad.zero_() |
# 使用自定义的 ReLU 函数
这里还是那个全连接网络的例子,不过这里我们不使用 clamp 来实现 ReLU,而是我们自己来实现一个 MyReLU 的函数。
import torch | |
class MyReLU(torch.autograd.Function): | |
""" | |
为了实现自定义的实现autograd的函数,我们需要基础torch.autograd.Function, | |
然后再实现forward和backward两个函数。 | |
""" | |
@staticmethod | |
def forward(ctx, input): | |
""" | |
在forward函数,我们的输入是input,然后我们根据input计算输出。 | |
# 同时为了下面的 backward, | |
我们需要使用save_for_backward来保存用于反向计算的数据到ctx里, | |
# 这里我们需要保存 input。 | |
""" | |
ctx.save_for_backward(input) | |
return input.clamp(min=0) | |
@staticmethod | |
def backward(ctx, grad_output): | |
""" | |
从ctx.saved_tensors里恢复input | |
然后用input计算梯度 | |
""" | |
input, = ctx.saved_tensors | |
grad_input = grad_output.clone() | |
grad_input[input < 0] = 0 | |
return grad_input | |
dtype = torch.float | |
device = torch.device("cpu") | |
N, D_in, H, D_out = 64, 1000, 100, 10 | |
x = torch.randn(N, D_in, device=device, dtype=dtype) | |
y = torch.randn(N, D_out, device=device, dtype=dtype) | |
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True) | |
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True) | |
learning_rate = 1e-6 | |
for t in range(500): | |
# 为了调用我们自定义的函数,我们需要使用 Function.apply 方法,把它命名为 'relu' | |
relu = MyReLU.apply | |
# 我们使用自定义的 ReLU 来进行 Forward 计算 | |
y_pred = relu(x.mm(w1)).mm(w2) | |
loss = (y_pred - y).pow(2).sum() | |
print(t, loss.item()) | |
loss.backward() | |
with torch.no_grad(): | |
w1 -= learning_rate * w1.grad | |
w2 -= learning_rate * w2.grad | |
w1.grad.zero_() | |
w2.grad.zero_() |
# 和 Tensorflow 的对比
这里我们还是和前面一样,实现一个隐层的全连接神经网络,优化的目标函数是预测值和真实值的欧氏距离。这个实现使用基本的 Tensorflow 操作来构建一个计算图,然后多次执行这个计算图来训练网络。Tensorflow 和 PyTorch 最大的区别之一就是 Tensorflow 使用静态计算图和 PyTorch 使用动态计算图。在 Tensorflow 里,我们首先构建计算图,然后多次执行它。
import tensorflow as tf | |
import numpy as np | |
# 首先构建计算图。 | |
# N 是 batch 大小;D_in 是输入大小。 | |
# H 是隐单元个数;D_out 是输出大小。 | |
N, D_in, H, D_out = 64, 1000, 100, 10 | |
# 输入和输出是 placeholder,在用 session 执行 graph 的时候 | |
# 我们会 feed 进去一个 batch 的训练数据。 | |
x = tf.placeholder(tf.float32, shape=(None, D_in)) | |
y = tf.placeholder(tf.float32, shape=(None, D_out)) | |
# 创建变量,并且随机初始化。 | |
# 在 Tensorflow 里,变量的生命周期是整个 session,因此适合用它来保存模型的参数。 | |
w1 = tf.Variable(tf.random_normal((D_in, H))) | |
w2 = tf.Variable(tf.random_normal((H, D_out))) | |
# Forward pass:计算模型的预测值 y_pred | |
# 注意和 PyTorch 不同,这里不会执行任何计算, | |
# 而只是定义了计算,后面用 session.run 的时候才会真正的执行计算。 | |
h = tf.matmul(x, w1) | |
h_relu = tf.maximum(h, tf.zeros(1)) | |
y_pred = tf.matmul(h_relu, w2) | |
# 计算 loss | |
loss = tf.reduce_sum((y - y_pred) ** 2.0) | |
# 计算梯度。 | |
grad_w1, grad_w2 = tf.gradients(loss, [w1, w2]) | |
# 使用梯度下降来更新参数。assign同样也只是定义更新参数的操作,不会真正的执行。 | |
# 在Tensorflow里,更新操作是计算图的一部分; | |
# 而在PyTorch里,因为是动态的”实时“的计算, | |
# 所以参数的更新只是普通的Tensor计算,不属于计算图的一部分。 | |
learning_rate = 1e-6 | |
new_w1 = w1.assign(w1 - learning_rate * grad_w1) | |
new_w2 = w2.assign(w2 - learning_rate * grad_w2) | |
# 计算图构建好了之后,我们需要创建一个session来执行计算图。 | |
with tf.Session() as sess: | |
# 首先需要用session初始化变量 | |
sess.run(tf.global_variables_initializer()) | |
# 这是 fake 的训练数据 | |
x_value = np.random.randn(N, D_in) | |
y_value = np.random.randn(N, D_out) | |
for _ in range(500): | |
# 用 session 多次的执行计算图。每次 feed 进去不同的数据。 | |
# 这里是模拟的,实际应该每次 feed 一个 batch 的数据。 | |
# run 的第一个参数是需要执行的计算图的节点,它依赖的节点也会自动执行, | |
# 因此我们不需要手动执行 forward 的计算。 | |
# run 返回这些节点执行后的值,并且返回的是 numpy array | |
loss_value, _, _ = sess.run([loss, new_w1, new_w2], | |
feed_dict={x: x_value, y: y_value}) | |
print(loss_value) |
# 使用 nn 模块来实现三层神经网络
我们接下来使用 nn 模块来实现这个简单的全连接网络。前面我们通过用 Tensor 和 Operation 等 low-level API 来创建 动态的计算图,这里我们使用更简单的 high-level API。
import torch | |
print(torch.__version__) | |
# N 是 batch size;D_in 是输入大小 # H 是隐层的大小;D_out 是输出大小。 N, D_in, H, D_out = 64, 1000, 100, 10 | |
# 创建随机的 Tensor 作为输入和输出 x = torch.randn (N, D_in) | |
y = torch.randn(N, D_out) | |
# 使用 nn 包来定义网络。nn.Sequential 是一个包含其它模块 (Module) 的模块。 # 每个 Linear 模块使用线性函数来计算,它会内部创建需要的 weight 和 bias。 model = torch.nn.Sequential ( | |
torch.nn.Linear(D_in, H), | |
torch.nn.ReLU(), | |
torch.nn.Linear(H, D_out), | |
) | |
# 常见的损失函数在 nn 包里也有,不需要我们自己实现 loss_fn = torch.nn.MSELoss (size_average=False) | |
learning_rate = 1e-4 | |
for t in range(500): | |
# 前向计算:通过 x 来计算 y。Module 对象会重写__call__函数, # 因此我们可以把它当成函数来调用。 y_pred = model (x) | |
# 计算 loss loss = loss_fn (y_pred, y) | |
print(t, loss.item()) | |
# 梯度清空,调用 Sequential 对象的 zero_grad 后所有里面的变量都会清零梯度 model.zero_grad () | |
# 反向计算梯度。我们通过 Module 定义的变量都会计算梯度。 loss.backward () | |
# 更新参数,所有的参数都在 model.paramenters () 里 | |
with torch.no_grad(): | |
for param in model.parameters(): | |
param -= learning_rate * param.grad |
# 使用 optim 包
前面我们使用 nn 模块时是自己来更新模型参数的,PyTorch 也提供了 optim 包,我们可以使用里面的 Optimizer 来自动的更新模型参数。除了最基本的 SGD 算法,这个包也实现了常见的 SGD+momentum, RMSProp, Adam 等算法。
import torch | |
N, D_in, H, D_out = 64, 1000, 100, 10 | |
x = torch.randn(N, D_in) | |
y = torch.randn(N, D_out) | |
model = torch.nn.Sequential( | |
torch.nn.Linear(D_in, H), | |
torch.nn.ReLU(), | |
torch.nn.Linear(H, D_out), | |
) | |
loss_fn = torch.nn.MSELoss(size_average=False) | |
# 使用 Adam 算法,需要提供模型的参数和 learning rate learning_rate = 1e-4 | |
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) | |
for t in range(500): | |
y_pred = model(x) | |
loss = loss_fn(y_pred, y) | |
print(t, loss.item()) | |
# 梯度清零,原来调用的是 model.zero_grad,现在调用的是 optimizer 的 zero_grad optimizer.zero_grad () | |
loss.backward() | |
# 调用 optimizer.step 实现参数更新 optimizer.step () |
# 自定义 nn 模块
对于复杂的网络结构,我们可以通过基础 Module 了自定义 nn 模块。这样的好处是用一个类来同样管理,而且更容易复用代码。
import torch | |
class TwoLayerNet(torch.nn.Module): | |
def __init__(self, D_in, H, D_out): | |
""" 在构造函数里,我们定义两个nn.Linear模块,把它们保存到self里。 """ | |
super(TwoLayerNet, self).__init__() | |
self.linear1 = torch.nn.Linear(D_in, H) | |
self.linear2 = torch.nn.Linear(H, D_out) | |
def forward(self, x): | |
""" 在forward函数里,我们需要根据网络结构来实现前向计算。 通常我们会上定义的模块来计算。 """ | |
h_relu = self.linear1(x).clamp(min=0) | |
y_pred = self.linear2(h_relu) | |
return y_pred | |
N, D_in, H, D_out = 64, 1000, 100, 10 | |
x = torch.randn(N, D_in) | |
y = torch.randn(N, D_out) | |
model = TwoLayerNet(D_in, H, D_out) | |
criterion = torch.nn.MSELoss(size_average=False) | |
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4) | |
for t in range(500): | |
y_pred = model(x) | |
loss = criterion(y_pred, y) | |
print(t, loss.item()) | |
optimizer.zero_grad() | |
loss.backward() | |
optimizer.step() |
# 流程控制和参数共享
为了展示 PyTorch 的动态图的能力,我们这里会实现一个很奇怪模型:这个全连接的网络的隐层个数是个 1 到 4 之间的随机数,而且这些网络层的参数是共享的。
import random | |
import torch | |
class DynamicNet(torch.nn.Module): | |
def __init__(self, D_in, H, D_out): | |
""" 构造3个nn.Linear实例。 """ | |
super(DynamicNet, self).__init__() | |
self.input_linear = torch.nn.Linear(D_in, H) | |
self.middle_linear = torch.nn.Linear(H, H) | |
self.output_linear = torch.nn.Linear(H, D_out) | |
def forward(self, x): | |
# 输入和输出层是固定的,但是中间层的个数是随机的 (0,1,2), # 并且中间层的参数是共享的。 | |
# 因为每次计算的计算图是动态 (实时) 构造的, # 所以我们可以使用普通的 Python 流程控制代码比如 for 循环 # 来实现。读者可以尝试一下怎么用 TensorFlow 来实现。 # 另外一点就是一个 Module 可以多次使用,这样就 # 可以实现参数共享。 | |
h_relu = self.input_linear(x).clamp(min=0) | |
for _ in range(random.randint(0, 3)): | |
h_relu = self.middle_linear(h_relu).clamp(min=0) | |
y_pred = self.output_linear(h_relu) | |
return y_pred | |
N, D_in, H, D_out = 64, 1000, 100, 10 | |
x = torch.randn(N, D_in) | |
y = torch.randn(N, D_out) | |
model = DynamicNet(D_in, H, D_out) | |
criterion = torch.nn.MSELoss(size_average=False) | |
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, momentum=0.9) | |
for t in range(500): | |
y_pred = model(x) | |
loss = criterion(y_pred, y) | |
print(t, loss.item()) | |
optimizer.zero_grad() | |
loss.backward() | |
optimizer.step() |
# 迁移学习示例
在这个教程里,我们会学习怎么使用迁移学习来训练模型。通常我们的训练数据量不会很大,很难达到像 ImageNet 那样上百万的标注数据集。我们可以使用迁移学习来解决训练数据不足的问题。迁移学习里,我们根据训练数据的多少通常可以采取如下方法:
- 训练数据很少 那么我们通常把一个 pretraning 的网络的大部分固定住,然后只是把最后一个全连接层换成新的 (最后一层通常是不一样的,因为分类的数量不同),然后只训练这一层
- 训练数据较多 我们可以把 pretraining 的网络的前面一些层固定住,但后面的层不固定,把最后一层换新的,然后训练
- 训练数据很多 所有的 pretraining 的层都可以 fine-tuning,只是用 pretraining 的参数作为初始化参数。
首先我们引入依赖:
from __future__ import print_function, division | |
import torch | |
import torch.nn as nn | |
import torch.optim as optim | |
from torch.optim import lr_scheduler | |
import numpy as np | |
import torchvision | |
from torchvision import datasets, models, transforms | |
import matplotlib.pyplot as plt | |
import time | |
import os | |
import copy | |
plt.ion() |
# 加载数据
我们使用 torchvision 和 torch.utils.data 包来加载数据。我们要解决的问题是训练一个模型来区分蚂蚁和蜜蜂,每个类别我们大概有 120 个训练数据,另外每个类有 75 个验证数据。这是一个很小的训练集,如果直接用一个神经网络来训练,效果会很差。现在我们用迁移学习来解决这个问题。数据可以在这里下载,下载后请解压到 data 目录下。
# 训练的时候会做数据增强和归一化 | |
# 而验证的时候只做归一化 | |
data_transforms = { | |
'train': transforms.Compose([ | |
transforms.RandomResizedCrop(224), | |
transforms.RandomHorizontalFlip(), | |
transforms.ToTensor(), | |
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) | |
]), | |
'val': transforms.Compose([ | |
transforms.Resize(256), | |
transforms.CenterCrop(224), | |
transforms.ToTensor(), | |
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) | |
]), | |
} | |
data_dir = '../data/hymenoptera_data' | |
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x), | |
data_transforms[x]) | |
for x in ['train', 'val']} | |
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4, | |
shuffle=True, num_workers=4) | |
for x in ['train', 'val']} | |
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']} | |
class_names = image_datasets['train'].classes | |
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") |
# 可视化图片
我们来显示几张图片看看,下图是一个 batch 的图片,显示的代码如下:
def imshow(inp, title=None): | |
inp = inp.numpy().transpose((1, 2, 0)) | |
mean = np.array([0.485, 0.456, 0.406]) | |
std = np.array([0.229, 0.224, 0.225]) | |
inp = std * inp + mean | |
inp = np.clip(inp, 0, 1) | |
plt.imshow(inp) | |
if title is not None: | |
plt.title(title) | |
plt.pause(0.001) | |
# 得到一个 batch 的数据 | |
inputs, classes = next(iter(dataloaders['train'])) | |
# 把 batch 张图片拼接成一个大图 | |
out = torchvision.utils.make_grid(inputs) | |
imshow(out, title=[class_names[x] for x in classes]) |
图:迁移学习数据示例
# 训练模型
现在我们来实现一个用于训练模型的通用函数。这里我们会演示怎么实现:
- learning rate 的自适应
- 保存最好的模型
在下面的函数中,参数 scheduler 是来自 torch.optim.lr_scheduler 的 LR scheduler 对象 (_LRScheduler 的子类)
def train_model(model, criterion, optimizer, scheduler, num_epochs=25): | |
since = time.time() | |
best_model_wts = copy.deepcopy(model.state_dict()) | |
best_acc = 0.0 | |
for epoch in range(num_epochs): | |
print('Epoch {}/{}'.format(epoch, num_epochs - 1)) | |
print('-' * 10) | |
# 每个 epoch 都分为训练和验证阶段 | |
for phase in ['train', 'val']: | |
if phase == 'train': | |
scheduler.step() | |
model.train() # 训练阶段 | |
else: | |
model.eval() # 验证阶段 | |
running_loss = 0.0 | |
running_corrects = 0 | |
# 变量数据集 | |
for inputs, labels in dataloaders[phase]: | |
inputs = inputs.to(device) | |
labels = labels.to(device) | |
# 参数梯度清空 | |
optimizer.zero_grad() | |
# forward | |
# 只有训练的时候 track 用于梯度计算的历史信息。 | |
with torch.set_grad_enabled(phase == 'train'): | |
outputs = model(inputs) | |
_, preds = torch.max(outputs, 1) | |
loss = criterion(outputs, labels) | |
# 如果是训练,那么需要 backward 和更新参数 | |
if phase == 'train': | |
loss.backward() | |
optimizer.step() | |
# 统计 | |
running_loss += loss.item() * inputs.size(0) | |
running_corrects += torch.sum(preds == labels.data) | |
epoch_loss = running_loss / dataset_sizes[phase] | |
epoch_acc = running_corrects.double() / dataset_sizes[phase] | |
print('{} Loss: {:.4f} Acc: {:.4f}'.format( | |
phase, epoch_loss, epoch_acc)) | |
# 保存验证集上的最佳模型 | |
if phase == 'val' and epoch_acc > best_acc: | |
best_acc = epoch_acc | |
best_model_wts = copy.deepcopy(model.state_dict()) | |
print() | |
time_elapsed = time.time() - since | |
print('Training complete in {:.0f}m {:.0f}s'.format( | |
time_elapsed // 60, time_elapsed % 60)) | |
print('Best val Acc: {:4f}'.format(best_acc)) | |
# 加载最优模型 | |
model.load_state_dict(best_model_wts) | |
return model |
# 可视化预测结果的函数
def visualize_model(model, num_images=6): | |
was_training = model.training | |
model.eval() | |
images_so_far = 0 | |
fig = plt.figure() | |
with torch.no_grad(): | |
for i, (inputs, labels) in enumerate(dataloaders['val']): | |
inputs = inputs.to(device) | |
labels = labels.to(device) | |
outputs = model(inputs) | |
_, preds = torch.max(outputs, 1) | |
for j in range(inputs.size()[0]): | |
images_so_far += 1 | |
ax = plt.subplot(num_images//2, 2, images_so_far) | |
ax.axis('off') | |
ax.set_title('predicted: {}'.format(class_names[preds[j]])) | |
imshow(inputs.cpu().data[j]) | |
if images_so_far == num_images: | |
model.train(mode=was_training) | |
return | |
model.train(mode=was_training) |
# fine-tuning 所有参数
我们首先加载一个预训练的模型 (imagenet 上的 resnet),因为我们的类别数和 imagenet 不同,所以我们需要删掉原来的全连接层,换成新的全连接层。这里我们让所有的模型参数都可以调整,包括新加的全连接层和预训练的层。
model_ft = models.resnet18(pretrained=True) | |
num_ftrs = model_ft.fc.in_features | |
model_ft.fc = nn.Linear(num_ftrs, 2) | |
model_ft = model_ft.to(device) | |
criterion = nn.CrossEntropyLoss() | |
# 所有的参数都可以训练 | |
optimizer_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9) | |
# 每 7 个 epoch learning rate 变为原来的 10% | |
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1) | |
model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler, | |
num_epochs=25) |
最终我们得到的分类准确率大概在 94.7%。
# fine-tuning 最后一层参数
我们用可以固定住前面层的参数,只训练最后一层。这比之前要快将近一倍,因为反向计算梯度只需要计算最后一层。但是前向计算的时间是一样的。
model_conv = torchvision.models.resnet18(pretrained=True) | |
for param in model_conv.parameters(): | |
param.requires_grad = False | |
# 新加的层默认 requires_grad=True | |
num_ftrs = model_conv.fc.in_features | |
model_conv.fc = nn.Linear(num_ftrs, 2) | |
model_conv = model_conv.to(device) | |
criterion = nn.CrossEntropyLoss() | |
# 值训练最后一个全连接层。 | |
optimizer_conv = optim.SGD(model_conv.fc.parameters(), lr=0.001, momentum=0.9) | |
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_conv, step_size=7, gamma=0.1) | |
model_conv = train_model(model_conv, criterion, optimizer_conv, | |
exp_lr_scheduler, num_epochs=25) |
最终我们得到的分类准确率大概在 96%。