PyTorch深度学习60分钟(一)

写在前面的话:这是跟着ApacheCN团队学习pytorch的学习笔记,主要资源来自pytorch官网和ApacheCN社区


一、PyTorch 是什么?

它是一个基于 Python 的科学计算包, 其主要是为了解决两类场景:

  • NumPy 的替代品, 以使用 GPU 的强大加速功能
  • 一个深度学习研究平台, 提供最大的灵活性和速度

个人感觉,其实无论是pytorch还是tensorflow其实都是帮忙解决了在GPU上的自动求导问题,这是对我们这些深度学习使用者来说最关键的。也就是说,通过这些框架,我们不用去过多地操心反向求导的过程,而是可以更多地专注于神经网络(或者说深度学习)的结构等问题。当然,它们也提供了相应的封装来满足一些基本需求。


二、新手入门

对于深度学习来说,一个比较重要的概念就是张量。数学上的定义是张量(Tensor)是一个定义在的一些向量空间和一些对偶空间的笛卡儿积上的多重线性映射。而简单来说其实就是矢量的推广。在同构的意义下,第零阶张量为标量,第一阶张量为向量 (Vector), 第二阶张量则成为矩阵 (Matrix)。对我们来说,常用的其实也就3阶和4阶的张量(这里没有把矩阵它们当成张量),更高阶张量其实也很难遇到。例如一张图片就是3阶张量,包括长、宽和通道(通常是RGB3通道)。在使用时也会变成4阶张量,因为会有batch的值。(也就是有很多3阶张量堆在一起)。

1.初识张量

Tensors 与 NumPy 的 ndarrays 非常相似, 除此之外还可以在 GPU 上使用张量来加速计算。

from __future__ import print_function
import torch

# 构建一个 5x3 的矩阵
# 张量只是创建了,但是未初始化
# 可以看出,其实这里就是生成了一个5行3列的矩阵
x = torch.Tensor(5, 3)
print(x)

>>tensor([[ 0.0000, -2.0000,  0.0000],
          [-2.0000,  0.0000,  0.0000],
          [ 0.0000,  0.0000,  0.0000],
          [ 0.0000,  0.0000,  0.0000],
          [ 0.0000,  0.0000,  0.0000]])
        

# 获取 size,注:torch.Size 实际上是一个 tuple(元组),所以它支持所有 tuple 的操作。
print(x.size())

>>torch.Size([5, 3])

#PS:torch.Size 实际上是一个 tuple(元组), 所以它支持所有 tuple(元组)的操作.

2.基本操作

加操作:

#第一种写法
y = torch.rand(5, 3)
print(x + y)

>>tensor([[ 0.8042, -1.5267,  0.5508],
          [-1.0805,  0.2719,  0.9532],
          [ 0.8435,  0.5595,  0.8556],
          [ 0.6867,  0.8612,  0.7824],
          [ 0.9080,  0.1819,  0.2504]])

#这里的y就是tensor([[ 0.8042,  0.4733,  0.5508],
#                 [ 0.9195,  0.2719,  0.9532],
#                 [ 0.8435,  0.5595,  0.8556],
#                 [ 0.6867,  0.8612,  0.7824],
#                 [ 0.9080,  0.1819,  0.2504]])
# 
# x是第一部分代码中的x
#第二种写法
print(torch.add(x, y))

>>tensor([[ 0.8042, -1.5267,  0.5508],
          [-1.0805,  0.2719,  0.9532],
          [ 0.8435,  0.5595,  0.8556],
          [ 0.6867,  0.8612,  0.7824],
          [ 0.9080,  0.1819,  0.2504]])
#第三种写法,提供一个输出 tensor 作为参数
result = torch.Tensor(5, 3)
torch.add(x, y, out = result)
print(result)

>>>>tensor([[ 0.8042, -1.5267,  0.5508],
            [-1.0805,  0.2719,  0.9532],
            [ 0.8435,  0.5595,  0.8556],
            [ 0.6867,  0.8612,  0.7824],
            [ 0.9080,  0.1819,  0.2504]])
#第四种写法,in-place(就地操作)
# adds x to y
y.add_(x)
print(y)

>>>>>>tensor([[ 0.8042, -1.5267,  0.5508],
              [-1.0805,  0.2719,  0.9532],
              [ 0.8435,  0.5595,  0.8556],
              [ 0.6867,  0.8612,  0.7824],
              [ 0.9080,  0.1819,  0.2504]])

索引(类似Numpy的索引):

print(x[:, 1])

>>tensor([-2.0000,  0.0000,  0.0000,  0.0000,  0.0000])

改变大小:

x = torch.randn(4, 4)

>>tensor([[ 0.2755, -0.1519,  0.0257, -0.7659],
          [ 0.7431, -1.0414,  0.5645, -1.0806],
          [ 0.7274, -0.5298, -1.5444, -0.2279],
          [-0.9928, -1.0443,  0.4778, -0.2496]])
          
y = x.view(16)

>>tensor([ 0.2755, -0.1519,  0.0257, -0.7659,  0.7431, -1.0414,  0.5645,
          -1.0806,  0.7274, -0.5298, -1.5444, -0.2279, -0.9928, -1.0443,
           0.4778, -0.2496])
           
z = x.view(-1, 8)  # -1就是根据情况,由计算机自己推断这个维数

>>tensor([[ 0.2755, -0.1519,  0.0257, -0.7659,  0.7431, -1.0414,  0.5645,
           -1.0806],
          [ 0.7274, -0.5298, -1.5444, -0.2279, -0.9928, -1.0443,  0.4778,
           -0.2496]])

3.NumPy Bridge

将一个 Torch 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)

>>array([1., 1., 1., 1., 1.], dtype=float32)

#尽管转换了,但是两者依然共享内存
a.add_(1)
print(a)
print(b)

>>tensor([ 2.,  2.,  2.,  2.,  2.])
>>[2. 2. 2. 2. 2.]

#转换 NumPy 数组为 Torch Tensor
import numpy as np
a = np.ones(5)

>>array([1., 1., 1., 1., 1.])

b = torch.from_numpy(a)

>>tensor([ 1.,  1.,  1.,  1.,  1.], dtype=torch.float64)

#同样两者共享内存
np.add(a, 1, out = a)
print(a)
print(b)

>>[2. 2. 2. 2. 2.]
>>tensor([ 2.,  2.,  2.,  2.,  2.], dtype=torch.float64)
Note:

除了 CharTensor 之外, CPU 上的所有 Tensor 都支持与Numpy进行互相转换

4.CUDA Tensors

可以使用 .cuda 方法将 Tensors 在GPU上运行.

# 只要在  CUDA 是可用的情况下, 我们可以运行这段代码
if torch.cuda.is_available():
    x = x.cuda()
    y = y.cuda()
    x + y


三、自动求导

PyTorch 中所有神经网络的核心是 autograd自动求导包. 我们先来简单介绍一下, 然后我们会去训练我们的第一个神经网络。

autograd 自动求导包针对张量上的所有操作都提供了自动微分操作. 这是一个逐个运行的框架, 这意味着您的反向传播是由您的代码如何运行来定义的, 每个单一的迭代都可以不一样。

1.Variable(变量)

autograd.Variable 是包的核心类. 它包装了张量, 并且支持几乎所有的操作. 一旦你完成了你的计算, 你就可以调用 .backward()方法, 然后所有的梯度计算会自动进行。

pytorch允许通过 .data 属性来访问原始的张量, 而关于该 variable(变量)的梯度会被累计到 .grad 上去。


还有一个针对自动求导实现来说非常重要的类 - Function

VariableFunction 是相互联系的, 并且它们构建了一个非循环的图, 编码了一个完整的计算历史信息. 每一个 variable(变量)都有一个 .grad_fn 属性, 它引用了一个已经创建了 VariableFunction (除了用户创建的 Variable 之外 - 它们的 grad_fn is None

如果你想计算导数, 你可以在 Variable 上调用 .backward() 方法. 如果 Variable 是标量的形式(例如, 它包含一个元素数据), 你不必指定任何参数给 backward(), 但是, 如果它有更多的元素. 你需要去指定一个 grad_output 参数, 该参数是一个匹配 shape(形状)的张量。

import torch
from torch.autograd import Variable

#创建 variable(变量)
x = Variable(torch.ones(2, 2), requires_grad = True)
print(x)

>>tensor([[ 1.,  1.],
          [ 1.,  1.]])
          
#y 由操作创建,所以它有 grad_fn 属性.
y = x + 2
print(y)

>>tensor([[ 3.,  3.],
          [ 3.,  3.]])
 
z = y * y * 3
out = z.mean()
print(z, out)

>>tensor([[ 27.,  27.],
          [ 27.,  27.]])
>>tensor(27.)        

2.梯度

pytorch之类的框架对于我们学习者来说最大的帮助莫过于反向求导的简单化。

我们考虑上述例子的反向求导过程,首先,先写出整体的前向过程:
y = x + 2
z = 3 * y^2
out = \frac{1}{4} \sum_{i}^4{z_{i}}

所以在反向求导时:
\nabla_{z_{i}} = \frac{\partial{out}}{\partial{z_{i}}} = \frac{1}{4}
\nabla_{y} = \frac{\partial{z_{i}}}{\partial{y}} = 6 * y
\nabla_{x} = \frac{\partial{y}}{\partial{x}} = 1

故而:

\nabla_{x} = \frac{\partial{out}}{\partial{x}} = \frac{1}{4} * 6 * y * 1 = 4.5



如果没有框架,单纯编写这段代码其实比较的繁琐。而在使用了pytorch框架后,只需要调用out.backward(),pytorch就会自动求导其导数,将其存放在.grad中:

out.backward()
print(x.grad)

>>tensor([[ 4.5000,  4.5000],
          [ 4.5000,  4.5000]])

同时,梯度的有趣应用:

x = torch.randn(3)
x = Variable(x, requires_grad = True)

y = x * 2

gradients = torch.FloatTensor([0.1, 1.0, 0.0001])
y.backward(gradients)
print(x.grad)

>>tensor([ 0.2000,  2.0000,  0.0002])


四、实战之基本的卷积神经网络

神经网络可以使用 torch.nn 包构建。

autograd 实现了反向传播功能, 但是直接用来写深度学习的代码在很多情况下还是稍显复杂, torch.nn 是专门为神经网络设计的模块化接口. nn 构建于 Autograd 之上, 可用来定义和运行神经网络. nn.Modulenn 中最重要的类, 可把它看成是一个网络的封装, 包含网络各层定义以及 forward 方法, 调用 forward(input) 方法, 可返回前向传播的结果。

1.定义一个网络

import torch
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        # 卷积层 '1'表示输入图片为单通道, '6'表示输出通道数, '5'表示卷积核为5*5
        # 核心
        # 初始化的过程中其实没有再定义网络结构,只是定义了一些函数
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        # 仿射层/全连接层: 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):
        # 在由多个输入平面组成的输入信号上应用2D最大池化.
        # (2, 2) 代表的是池化操作的步幅
        
        # 这里正是从输入层,通过一个卷积之后,在经过一个pool
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        # 如果大小是正方形, 则只能指定一个数字
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        
        # 这边便是将x拉值,以便用于全连接
        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:]  # 除批量维度外的所有维度
        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)
   )

只要在 nn.Module 的子类中定义了 forward 函数, backward 函数就会自动被实现(利用 autograd )。 在 forward 函数中可使用任何 Tensor 支持的操作。

并不像tensorflow需要显式地定义参数,pytorch在上述过程中只需要用户输入维度信息,参数的维度便可由计算机自动给出。网络的可学习参数通过 net.parameters() 返回,net.named_parameters 可同时返回学习的参数以及名称。

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

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

向前的输入是一个 autograd.Variable, 输出也是如此。注意: 这个网络(LeNet)的预期输入大小是 32x32, 使用这个网上 MNIST 数据集, 请将数据集中的图像调整为 32x32

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

>>tensor([[-0.0821,  0.1081,  0.0103,  0.1502,  0.0191,  0.0097, -0.0175,
           -0.0804, -0.0055, -0.0382]])
Note:
  • torch.nn 只支持小批量(mini-batches), 不支持一次输入一个样本, 即一次必须是一个 batch
  • nn.Conv2d 的输入必须是 4 维的, 形如 nSamples x nChannels x Height x Width


2.损失函数

损失函数采用 (output, target) 输入对, 并计算预测输出结果与实际目标的距离。

在 nn 包下有几种不同的损失函数。一个简单的损失函数是: nn.MSELoss 计算输出和目标之间的均方误差

output = net(input)
target = Variable(torch.arange(1, 11))  # 一个虚拟的目标
criterion = nn.MSELoss()

loss = criterion(output, target)
print(loss)

现在, 如果你沿着 loss 反向传播的方向使用.grad_fn 属性, 你将会看到一个如下所示的计算图:

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

所以, 当我们调用loss.backward(), 整个图与损失是有区别的, 图中的所有变量都将用 .grad 梯度累加它们的变量。


3.反向传播

为了反向传播误差, 我们所要做的就是 loss.backward()。你需要清除现有的梯度, 否则梯度会累加之前的梯度。
现在我们使用 loss.backward(), 看看反向传播之前和之后 conv1 的梯度。

net.zero_grad()     # 把之前的梯度清零

print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)

>>None

loss.backward()

print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)

>>tensor([ 0.1580, -0.0348, -0.1106,  0.0706, -0.0937, -0.0539])


4.更新权重

实践中使用的最简单的更新规则是随机梯度下降( SGD ):

weight = weight - learning\_rate * gradient

可以使用简单的 python 代码来实现这个:

learning_rate = 0.01
for f in net.parameters():
    f.data.sub_(f.grad.data * learning_rate)

当然,更新权重的方法pytorch也已经做了封装(torch.optim),方便我们调用:

import torch.optim as optim

# 新建一个优化器, 指定要调整的参数和学习率
optimizer = optim.SGD(net.parameters(), lr = 0.01)

# 在训练过程中:
optimizer.zero_grad()   # 首先梯度清零(与 net.zero_grad() 效果一样)
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step()    # 更新参数

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,873评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,483评论 1 306
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,525评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,595评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,018评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,958评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,118评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,873评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,643评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,813评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,293评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,615评论 3 262
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,306评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,170评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,968评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,107评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,894评论 2 278