pytorch学习笔记(删繁就简,一篇入门)

官方教程:https://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html
这是我在看官方教程过程中做的笔记,中间有一些东西是教程中没有讲到的,但是对理解代码,甚至是理解 pytorch 的一些思想很有用(王婆卖瓜,自卖自夸,溜…)相信你看了这一篇博客就基本入门 pytorch 了。

1. 数据处理

pytorch的数据处理类似于numpy,只不过换了个名字:tendsor 实际上就是 ndarray。pytorch的优点在于可以利用GPU进行加速。

1.1 基本数据操作
#############
x.view(shape)
#############
# 这居然是一个renshape函数!
x = torch.randn(4,4)
x = x.view(-1,8)
print(x.size())
# result:
torch.Size([2, 8])

输出的torch.Size([2, 8])实际上就是tuple,可以用tuple的方式进行处理和操作。

#############
x.item()
#############
# x为tensor,输出为python正常数据
x = torch.randn(1)
print(x)
print(x.item())
# result:
tensor([-1.7816])
-1.7815848588943481

pytorch中的函数如果加上后缀 _ 一般就是原地操作,也即操作之后原始变量在内存中的值会相应改变

y.add_(x)
1.2 tensor与numpy的关系
#############
b = x.numpy()
#############
# 将x转换为numpy数据类型,不过他们在内存中占用同一个地方。
# 也就是实际上b和x是同一个变量,对x进行操作之后,b的值也会响应改变
#############
b = torch.from_numpy(a)
#############
# 跟上面x.numpy()类似
1.3 cuda tensor

.to()实现在不同设备之间(cpu,gpu)移动数据

# let us run this cell only if CUDA is available
# We will use ``torch.device`` objects to move tensors in and out of GPU
if torch.cuda.is_available():
    device = torch.device("cuda")          # a CUDA device object
    y = torch.ones_like(x, device=device)  # directly create a tensor on GPU
    x = x.to(device)                       # or just use strings ``.to("cuda")``
    z = x + y
    print(z)
    print(z.to("cpu", torch.double))       # ``.to`` can also change dtype together!

##################### result ##########################
tensor([-0.7816], device='cuda:0')
tensor([-0.7816], dtype=torch.float64)

2. 自动梯度

2.1 tensor

torch.Tensor是pytorch中的核心。如果 .requires_grad 被设置为 True,则pytorch会记录这个tensor上的所有操作,当你调用 .backward() 时就会自动计算出所有的梯度(简直不要再方便了有木有!!!)如果想在某个操作之后停止记录对某变量的操作,使用 .detach() 即可,或者用with torch.no_grad(): 包围住对应的代码块。

每个tensor都有一个 .grad_fn 属性,记录了该变量被创建时所使用的函数,

x = torch.ones(2, 2, requires_grad=True)
y = x + 2
print(y)
z = y * y * 3
out = z.mean()
print(z, out)

##################### result ##########################
tensor([[3., 3.],
        [3., 3.]], grad_fn=<AddBackward0>)
tensor([[27., 27.],
        [27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward0>)
#############
y.data.norm()
#############
# 这就是一个求二范数的函数
x = torch.randn(3, requires_grad=True)
with torch.no_grad():
    y = x * 2
z = torch.sqrt(torch.sum(torch.pow(y,2)))
z.requires_grad_(True)
print(x)
print(y)
print(z)
print(y.data.norm())

##################### result ##########################
tensor([-1.2562, -0.1328,  0.0917], requires_grad=True)
tensor([-2.5123, -0.2655,  0.1834])
tensor(2.5330, requires_grad=True)
tensor(2.5330)

3. 神经网络

pytorch中提供了 torch.nn 包以方便的实现神经网络,nn 依赖于前面 2. 自动梯度 中讲的 autograd 实现对权重梯度的计算。一个 nn.Module 至少包含有网络的 layers 和一个 forward 函数,该函数实现前向传播,得到网络的输出 output。而反向传播的 backward 方法会通过 autograd 自动被创建,无需我们自己定义,需要时直接调用即可。

一般来说,构建、训练神经网络包括以下步骤

  1. 描述网络结构,定义参数/权重
  2. 输入数据集,用网络进行前向传播
  3. 计算 loss
  4. 反向传播计算梯度
  5. 权重更新 weight = weight - learning_rate * gradient
3.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
        # kernel
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        # an affine operation: y = Wx + b
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        # Max pooling over a (2, 2) window
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        # If the size is a square you can only specify a single number
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = x.view(-1, self.num_flat_features(x))
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

    def num_flat_features(self, x):
        size = x.size()[1:]  # all dimensions except the batch dimension
        num_features = 1
        for s in size:
            num_features *= s
        return num_features

net = Net()
print(net)

##################### result ##########################
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)
)

net.parameters() 方便的查看可训练的参数:

params = list(net.parameters())
print(len(params))
print(params[0].size())  # conv1's .weight

##################### result ##########################
10
torch.Size([6, 1, 5, 5])

进行测试时有两种方法,得到的结果是一样的:

input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)
out = net.forward(input)
print(out)

##################### result ##########################
tensor([[-0.1014,  0.1486,  0.0921, -0.0425, -0.0668, -0.0370,  0.0513,  0.0671,
          0.0178, -0.0078]], grad_fn=<AddmmBackward>)
tensor([[-0.1014,  0.1486,  0.0921, -0.0425, -0.0668, -0.0370,  0.0513,  0.0671,
          0.0178, -0.0078]], grad_fn=<AddmmBackward>)

注意torch.nn 只支持 mini-batch,也就是输入的样本必须有一个 batch 维度,比如 nn.Conv2d 的输入形式为 4D 的 tensor:nSamples x nChannels x Height x Width。如果输入只有一个样例,可以使用 input.unsqueeze(0) 来增加一个 batch 为 1 的维度

3.2 损失函数

当我们调用 loss.backward() 时,整个图就会关于 loss 求出微分,那些 requires_grad=True 的 tensor 将会有一个 .grad 的 tensor 来保存梯度。

注意:调用 loss.backward() 后只是通过反向传播算法计算出了所有可训练参数的梯度,并赋给了他们的 .grad 属性,但并没有对这些参数进行权重更新。如果想要更新权重,需要用到下一小节中讲的 torch.optim 包。

注意.zero_grad() 非常重要!根据 pytorch 中的 backward() 函数的计算,当网络参量进行反馈时,梯度是被积累的而不是被替换为新的值,因此需要每个 batch 都设置一遍 zero_grad() 了。为什么这么设计呢?我觉得可能是因为对有的复杂网络,我们可能需要定义多个loss,对他们都计算loss之后才统一更新权重,这个时候就需要多个梯度累加了。
参考:https://discuss.pytorch.org/t/why-do-we-need-to-set-the-gradients-manually-to-zero-in-pytorch/4903/3

非常非常重要
关于pytorch backword 更多的细节和问题可以参考:https://blog.csdn.net/douhaoexia/article/details/78821428
摘出我觉得写的非常好的一段
计算图本质就是一个类似二叉树的结构,能获取回传梯度(grad)的只有计算图的叶节点。注意是获取,而不是求取。中间节点的梯度在计算求取并回传之后就会被释放掉,没办法获取。
假设有一个网络 x2 —> |f1| —> y2 —> |f2| —> z2 , f1 、f2 是两个普通的函数,z2=f2(y2), y2=f1(x2) 。那么执行一次 z2.backward() 之后,发现x2.grad,w1.grad,w2.grad 都有值 ,但是 y2.grad 却是 None, 说明x2,w1,w2的梯度保留了,y2 的梯度获取不到。实际上,仔细想一想会发现,x2,w1,w2均为叶节点。在这棵计算树中 ,x2 与w1 是同一深度(底层)的叶节点,y2与w2 是同一深度,w2 是单独的叶节点,而y2 是x2 与 w1 的父节点,所以只有y2没有保留梯度值,印证了之前的说法。
与此同时也可以把网络某一部分参数,固定,不让其被训练,也就是设置 requires_grad = False。因为是叶节点(而不是中间节点),所以不求grad(grad为’None’),也不会影响网络的正常反向传播。

注意:据说 loss 必须是一个 scalar,而不能是一个 tensor,具体为什么我还不太理解,理解后再补充。

output = net(input)
criterion = nn.MSELoss()                # 定义loss函数
loss = criterion(output, ground_truth)  # 计算loss
net.zero_grad()                         # 清理梯度,否则会积累
print(net.conv1.bias.grad)
loss.backward()                         # 反向传播
print(net.conv1.bias.grad)

##################### result ##########################
tensor([0., 0., 0., 0., 0., 0.])
tensor([-0.0122, -0.0004, -0.0031, -0.0044, -0.0003, -0.0003])

如果这个时候你观察 loss.grad_fn 属性,你就会得到与他相关的一系列计算操作

input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
      -> view -> linear -> relu -> linear -> relu -> linear
      -> MSELoss
      -> loss

比如下面的例子

print(loss.grad_fn)  # MSELoss
print(loss.grad_fn.next_functions[0][0])  # Linear
print(loss.grad_fn.next_functions[0][0].next_functions[0][0])  # ReLU

##################### result ##########################
<MseLossBackward object at 0x7fcf8308c550>
<AddmmBackward object at 0x7fcf8308c208>
<AccumulateGrad object at 0x7fcf8308c208>
3.3 权重更新

torch提供了 torch.optim 来实现不同的优化算法

import torch.optim as optim

# create your optimizer
optimizer = optim.SGD(net.parameters(), lr=0.01)

# in your training loop:
optimizer.zero_grad()   # zero the gradient buffers
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step()    # Does the update

4. 训练一个分类器

4.1 数据

torch 提供了一个包 torchvision 可以用来载入各种类型的数据,如图像、音频、文本等,免去了针对各种不同类型的数据专门寻找处理的包。它还含有一些常用数据集的 data loader,如 Imagenet,CIFAR10,MNIST等,可参见 torch.utils.data.DataLoadertorchvision.datasets

上面这一段是翻译的官方教程,我感觉说的不太清楚,这个东西不明白不要紧,紧接着下面就是我的介绍。come on!

重要:为了便于理解,我们先来看一看 torch.utils.data.Dataset 类,他是一个抽象的数据集类。为了处理特定的数据,我们可以根据所需要处理的数据特点,继承这个类,自定义一个子类。我们自定义的子类至少要有两个属性 __len____getitem__。前者返回数据集的长度,后者支持用 0~len(self) 的整数来索引数据集。

看一个例子

class RandomDataset(Dataset):
    def __init__(self, size, length):
        self.len = length
        self.data = torch.randn(length, size)

    def __getitem__(self, index):
        return self.data[index]

    def __len__(self):
        return self.len

好,了解了 dataset 类,我们再来看一看dataloader

重要dataloader 的处理逻辑是先通过 Dataset 类里面的 __getitem__ 函数获取单个的数据,然后组合成batch,再使用 collate_fn 所指定的函数对这个batch做一些操作,比如padding啊之类的。

# 各参数的含义可以查看官方文档 https://pytorch.org/docs/stable/data.html
torch.utils.data.DataLoader(
		dataset, batch_size=1, shuffle=False, 	
		sampler=None, batch_sampler=None, 
		num_workers=0, collate_fn=<function default_collate>, 
		pin_memory=False, drop_last=False, timeout=0, 
		worker_init_fn=None)

好了,上面还提到一个 torchvision.datasets,这又是个什么东西呀?

重要:简单来说,这就是 pytorch 官方提前给你定义好了一个(好多个)继承自 torch.utils.data.Dataset 的子类,他们跟前边我们自己定义的 RandomDataset 并没有什么差别。比如 torchvision.datasets.MNIST 就是针对 MNIST 数据集定义的一个数据集类,其他的类似,具体的应用可以参见下一小节 4.2.1 加载数据。其他已经定义好的数据集类可以参考官方文档 https://pytorch.org/docs/stable/torchvision/datasets.html

其实 torchvision 并不是仅仅为了处理数据集而设计的,除了 torchvision.datasets,他里边还定义了 torchvision.modelstorchvision.transformstorchvision.utils。关于他们各自的用法可以参考官方文档 https://pytorch.org/docs/stable/torchvision/index.html

4.2 训练一个图像分类器

包含以下步骤:

  1. torchvision 加载训练和测试数据集
  2. 定义一个CNN
  3. 定义一个 loss 函数
  4. 训练
  5. 测试
4.2.1 加载数据

pytorch 载入的数据集是元组 tuple 形式,里面包括了数据及标签 (train_data, label),其中的 train_data 数据是 PIL 图像,灰度范围为[0,1] 可以转换为 torch.Tensor 形式。torchvision.transforms 是pytorch中的图像预处理包,可以用torchvision.transforms.Compose把多个步骤整合到一起。transforms 中有以下常用函数

transforms.ToTensor()   # 把PIL图像(H*W*C,灰度[0,255])转化为torch.Tensor(C*H*W,灰度[0.0,1.0])
transforms.ToPILImage() # 把tensor转化为PIL图像
transforms.Resize(size,interpolation=2)      # 把给定的PIL图片resize到given size
transforms.Grayscale(num_output_channels=1)  # 将PIL图像转换为灰度图像
transforms.*Crop(size)          # 对PIL图像进行各种裁剪
transforms.Normalize(mean,std)  # 用均值和标准差正规化一个tensor

加载数据集的代码:

import torch
import torchvision
import torchvision.transforms as transforms

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

trainset = torchvision.datasets.CIFAR10(root='./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='./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')

iter(dataloader) 生成迭代器,然后用 .next() 逐个取出数据。

# get some random training images
dataiter = iter(trainloader)
images, labels = dataiter.next()

# show images
imshow(torchvision.utils.make_grid(images))

上面代码中 make_grid 用于将多张图片拼成一张图片显示

#####################
torchvision.utils.make_grid(tensor, nrow=8, padding=2, 
	normalize=False, range=None, scale_each=False, pad_value=0)
# nrow: 每一行显示的图象数
# padding: 相邻图像间的填充像素数
# normalize: 归一化到[0,1]间
#####################
4.2.2 定义CNN和loss
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

class Net(nn.Module):
	def __init__(self):
		super(Net, self).__init__()
		layers...
		
	def forward(self,x):
		computations...
		return x
		
net = Net()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
4.2.3 训练和测试

训练

for epoch in range(2):
	running_loss = 0.
	
	for i,data = enumerate(trainloader,0):
		inputs,labels = data
		optimizer.zero_grad()
		# forward + backward + optimize
		outputs = net(inputs)
		loss = criterion(outputs,labels)
		loss.backward()
		optimizer.step()

		# print statistics
        running_loss += loss.item()
        if i % 2000 == 1999:    # print every 2000 mini-batches
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, i + 1, running_loss / 2000))
            running_loss = 0.0

print('Finished Training')

测试

with torch.no_grad():
    for data in testloader:
    	retrive datas...
    	forward...
    compute accuracy...
4.3 用GPU运算

跟第 1 部分中讲的相同,用 device.to() 就可以,不过要注意数据加载时也要放在GPU中

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

net.to(device)

inputs, labels = inputs.to(device), labels.to(device)

5. 多块GPU并行计算

当你通过 .to(device) 指定使用 gpu 进行计算时,默认只使用一块 gpu,如果想要用多块 gpu 并行计算,非常简单,只需要一个语句(太残暴了!):

model = nn.DataParallel(model)
# model 是你定义的网络模型的实例
# 敲黑板,这短短的一行代码就是这一小节的核心!

当你使用上面一句代码之后,pytorch会自动将你一个batch的数据(基本平均地)分配给各个gpu。下面举个比较完整的例子,帮助加深记忆

import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader

class MyDataset(Dataset):
	def __init__(self,params):
		...
	def __getitem__(self,index):
		...
	def __len__(self):
		...

class MyModel(nn.Module):
	def __init__(self,params):
		super(Model, self).__init__()
		layers...
	def forward(self,input):
		computations...

# 实例化模型
model = MyModel(some_params)

# 设置并行处理,核心!
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
if torch.cuda.device_count() > 1:
	model = model.DataParallel(model)
model.to(device)

# 载入数据
my_loader = DataLoader(dataset=MyDataset(other_params),
			batch_size=batch_size, shuffle=True)

# 运行模型
for data in my_loader:
    input = data.to(device)
    output = model(input)
    print(output)

完 ~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值