pytorch基础(二):构建简单的神经网络

前言

  本系列主要是对pytorch基础知识学习的一个记录,尽量保持博客的更新进度和自己的学习进度。本人也处于学习阶段,博客中涉及到的知识可能存在某些问题,希望大家批评指正。另外,本博客中的有些内容基于吴恩达老师深度学习课程,我会尽量说明一下,但不敢保证全面。

一、使用numpy完成神经网络

  在吴恩达老师深度学习前两章的每周作业中,我相信大多数人是利用numpy完成矩阵的运算。numpy自带的矩阵运算函数确实很方便,无论是前向传播还是反向传播,都比较容易实现。我们先用numpy搭建一个双层神经网络(通常不把输入层计算在内),隐藏层的激活函数均为relu,输出层的激活函数是线性激活函数:

import numpy as np

# N是批量大小; D_in是输入维度;
# 49/5000 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)

    # 反向传播,计算w1和w2对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

  这是官方文档(中文版)给出的例子(重点不在于测试神经网络的准确率,这里就不放代码运行的结果截图了),看过吴老师视频的可能发现例子中的神经网络不是特别“标准”,至少初始化参数的方式并不一致(注意x和w的维度),但是并不碍事,至少是一个完整的神经网络结构,具备神经网络必要的部分。

二、使用tensor完成神经网络

  我们已经使用numpy完成了一个神经网络,现在我们将前面程序中所有ndarray数据类型的变量全部替换成tensor数据类型的变量。

import torch

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)
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)
    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 = w1 - learning_rate * grad_w1
    w2 = w2 - learning_rate * grad_w2

  上面代码也是官方文档中给出的代码,从代码层面无法比较numpy和tensor的好坏,但是至少证明tensor单独可以实现神经网络。
  tensor使用的函数和语法与numpy有区别:

1.np.dot()=>torch.mm()
2.W.T=>W.t()
3.np.square()=>torch.pow(2)
4.np.copy()=>torch.clone()
4.tensor中元素数量为1时需要利用item()进行取值

三、pytorch的自动求导

import torch

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):

    # 前向传播
    y_pred = x.mm(w1).clamp(min=0).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_()

  以上就是利用pytorch自动求导构造神经网络的代码,可以看出,代码量变得更少。
  首先是前向传播部分,这时的前向传播的目的是在于计算最后输出层的结果,因为是双层结构,计算较为简单,一行代码就能得到神经网络输出结果。关键在于,我们不需要特意去保存中间的计算结果,在用numpy实现时我们需要保存每层计算得到的A[L]和Z[L]的值,因为反向传播时需要用到这些值。
  计算损失其实并没有变化,只需要注意是用tensor类型的数据在计算。
  反向传播部分,调用backward()方法,pytorch框架会自动帮你求导。需要注意的是,我们在初始化w1和w2时需要将参数 ==requires_grad== 的值设置为True,backward只会对requires_grad为True的张量进行求导。
  tensor张量的梯度数据保存在其 ==grad== 属性中,==grad== 其实也是一个tensor张量,因此在参数更新时可以直接计算。
  我们注意到在参数更新的代码块被 ==with torch.no_grad():== 包住,官方给这段代码的解释是:我们只想对w1和w2的值进行原地改变;不想为更新阶段构建计算图。你也可以这么理解,我们将w1和w2的 ==requires_grad== 值设置为True,就是告诉pytorch框架需要时刻记录与w1和w2有关的运算,用于对其求导,而我们在学习反向传播的理论知识时知道,参数更新其实和反向传播求导没有任何关系,所以这一部分的梯度并不用保存,使用==with torch.no_grad():== 就是告诉pytorch不用记录这部分运算。
  最后还需要注意的一点是梯度清0,pytorch求导的一个规则是计算得到的梯度会直接加到grad属性上,也就是说每次训练得到的grad值是累加了前面训练计算得到的grad值,很明显这不是我们需要的,所以在参数更新完成之后,我们需要对参数的grad进行清0操作,这就对应了最后两行代码。

四、补充说明

  在复现上述代码时,每个人的编程习惯可能不同,有的人可能在参数更新时使用的运算符是“-=”:

# 第一种方式更新参数
w1 = w1 - learning_rate * w1.grad
w2 = w2 - learning_rate * w2.grad

# 第二种方式更新参数
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad

  我们知道pytorch重载了运算符,因此可以直接使用基本运算符合来操作两个tensor变量。但是用第一种方式进行参数更新后(我最开始也使用的是第一种方法),接下来执行梯度清0语句时会报错:

在这里插入图片描述

  debug的时候发现,对w1进行更新后,它的一些属性也发生了变化:

在这里插入图片描述

在这里插入图片描述

  w1的grad属性变成了None,而zero_()方法是针对tensor类型的变量。除此之外,requires_grad的值也变为False。
  至于为什么会发现这样的变化,我在查阅相关资料后发现,pytorch对"-"进行了重载,没有对"="进行重载。

a = 1
b = 2
print(id(a))
a = a + b
print(id(a))

运行结果如下:

1546236096
1546236160

  从变量的id号可以看出,在进行赋值操作时,python其实是将计算结果保存到一个新建的内存空间,从根本上来说,就是新建了一个变量a。

print(id(w1))
w1 = w1 - learning_rate * w1.grad
w2 = w2 - learning_rate * w2.grad
print(id(w1))

输出结果为:

2181909743080
2182128952008

  因此在进行参数更新时,python新建了一个tensor类型的变量,并将计算结果保存到新建的w1中,至于为什么grad的值为None,我个人猜测是因为默认的tensor变量的requires_grad的值默认为False,不对梯度进行跟踪(希望由大佬能解释一下)。
  当我们用第二种方式对参数进行更新时:

print(id(w1))
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
print(id(w1))

输出结果为:

1636333876872
1636333876872

  可以看出在使用"-="操作符时,pytorch会将计算结果继续保存到原来的w1空间中,所以grad的值并不为None,置零操作时也不会报错。

总结

  官方文档关于这部分还有一个知识点:定义新的自动求导函数。这部分知识我并未特别好的理解,如果未来在实践中需要运用这个知识点可能会有更好的理解,到时候会写上这部分。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇