PyTorch 05:autograd 与逻辑回归

PyTorch 学习笔记

Posted by YEY on December 9, 2020

Lecture 05 autograd 与逻辑回归

本节课主要分为两部分:PyTorch 中的自动求导系统以及逻辑回归模型。我们知道,深度模型的训练就是不断地更新权值,而权值的更新需要求解梯度,因此,梯度在我们的模型训练过程中是至关重要的。然而,求解梯度通常十分繁琐,因此,PyTorch 中引入了自动求导系统帮助我们完成这一过程。在 PyTorch 中,我们无需手动计算梯度,只需要搭建好前向传播的计算图,然后根据 PyTorch 中的 autograd 方法就可以得到所有张量的梯度。

1. autograd:自动求导系统

torch.autograd.backward()

功能:自动求取计算图中各结点的梯度。

1
2
3
4
5
6
torch.autograd.backward(
    tensors,
    grad_tensors=None,
    retain_graph=None,
    create_graph=False
)

主要参数

  • tensors:用于求导的张量,如 loss
  • retain_graph:保存计算图,PyTorch 默认在反向传播完成后丢弃计算图,如需保存则将该项设为 True
  • create_graph:创建导数计算图,用于高阶求导。
  • grad_tensors:多梯度权重,当我们有多个 loss 需要计算梯度的时候,就需要设置各个 loss 的权重比例。

回顾一下如何通过计算图求解梯度:

\[y=(x+w) * (w+1)\]
  • $a = x+w$
  • $b=w+1$
  • $y=a*b$
\[\begin{align} \dfrac{\partial y}{\partial w} &= \dfrac{\partial y}{\partial a} \dfrac{\partial a}{\partial w} + \dfrac{\partial y}{\partial b} \dfrac{\partial b}{\partial w} \\[2ex] &= b * 1+ a * 1 \\[2ex] &= (w+1) + (x + w) \\[2ex] &= 2*w + x + 1 \\[2ex] &= 2*1 + 2 + 1 = 5 \end{align}\]

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)

a = torch.add(w, x)
b = torch.add(w, 1)
y = torch.mul(a, b)

# 如果希望后面再次执行该计算图,可以将 retain_graph 参数设为 True
# y.backward(retain_graph=True) 

y.backward()
print(w.grad)

输出结果:

1
tensor([5.])

当有多个 loss 需要计算梯度时,通过 grad_tensors 设置各 loss 权重比例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)

a = torch.add(w, x)
b = torch.add(w, 1)

# y0 = (x+w) * (w+1)    dy0/dw = 2*w + x + 1 = 5
y0 = torch.mul(a, b)

# y1 = (x+w) + (w+1)    dy1/dw = 2
y1 = torch.add(a, b)  

# 这种情况下,loss 是一个向量 [y0, y1]
loss = torch.cat([y0, y1], dim=0)

# 梯度的权重:dy0/dw 权重为 1,dy1/dw 权重为 2
grad_tensors = torch.tensor([1., 2.])

# gradient 传入 torch.autograd.backward() 中的 grad_tensors
loss.backward(gradient=grad_tensors)  

print(w.grad) # 5*1 + 2*2 = 9

输出结果:

1
tensor([9.])

torch.autograd.grad()

功能:求取梯度。

1
2
3
4
5
6
7
torch.autograd.grad(
    outputs,
    inputs,
    grad_outputs=None,
    retain_graph=None,
    create_graph=False
)

主要参数

  • outputs:用于求导的张量,如 loss
  • inputs:需要梯度的张量。
  • create_graph:创建导数计算图,用于高阶求导。
  • retain_graph:保存计算图。
  • grad_outputs:多梯度权重。

求取二阶梯度

1
2
3
4
5
6
7
8
9
10
x = torch.tensor([3.], requires_grad=True)
y = torch.pow(x, 2)  # y = x**2

# grad_1 = dy/dx = 2x = 2 * 3 = 6
grad_1 = torch.autograd.grad(y, x, create_graph=True)  
print(grad_1)

# grad_2 = d(dy/dx)/dx = d(2x)/dx = 2
grad_2 = torch.autograd.grad(grad_1[0], x)  
print(grad_2)

输出结果:

1
2
(tensor([6.], grad_fn=<MulBackward0>),)
(tensor([2.]),)

注意事项

  1. 梯度不自动清零。
  2. 依赖于叶子结点的结点,requires_grad 默认为 True
  3. 叶子结点不可执行原位操作 (in-place)。

代码示例 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. 梯度不会自动清零,重复求取会叠加,可以使用 .grad.zero_() 方法手动清零
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)

for i in range(3):
    a = torch.add(w, x)
    b = torch.add(w, 1)
    y = torch.mul(a, b)

    y.backward()
    print(w.grad)

# 梯度清零,下划线表示原位操作 (in-place)
w.grad.zero_()

for i in range(3):
    a = torch.add(w, x)
    b = torch.add(w, 1)
    y = torch.mul(a, b)

    y.backward()
    print(w.grad)
    w.grad.zero_()

输出结果:

1
2
3
4
5
6
tensor([5.])
tensor([10.])
tensor([15.])
tensor([5.])
tensor([5.])
tensor([5.])

代码示例 2

1
2
3
4
5
6
7
8
9
# 2. 依赖于叶子结点的结点, requires_grad 默认为 True
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)

a = torch.add(w, x)
b = torch.add(w, 1)
y = torch.mul(a, b)

print(a.requires_grad, b.requires_grad, y.requires_grad)

输出结果:

1
True True True

代码示例 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 3. 叶子结点不可执行 in-place (原位操作)。因为 PyTorch 计算图中引用叶子结点的值是
#    直接引用其前向传播时的地址,为了防止计算出错,叶子结点不可执行 in-place 操作。

#    in-place (原位操作): 从原始内存地址中直接改变数据。
#    非 in-place 操作: 开辟一块新的内存地址存储改变后的数据。

a = torch.ones((1, ))
print(id(a), a)

# 非 in-place 操作
a = a + torch.ones((1, ))
print(id(a), a)

# in-place 操作
a += torch.ones((1, ))
print(id(a), a)

输出结果:

1
2
3
4875211904 tensor([1.])
4875212336 tensor([2.])
4875212336 tensor([3.])

对叶子结点执行 in-place 操作将导致报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)

a = torch.add(w, x)
b = torch.add(w, 1)
y = torch.mul(a, b)

# 对非叶子结点 a 执行非 in-place 操作
print(a.add(1))

# 对非叶子结点 a 执行 in-place 操作
print(a.add_(1))

# 对叶子结点 w 执行非 in-place 操作
print(w.add(1))

# 对叶子结点 w 执行 in-place 操作,会报错
print(w.add_(1))

y.backward()

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
tensor([4.], grad_fn=<AddBackward0>)
tensor([4.], grad_fn=<AddBackward0>)
tensor([2.], grad_fn=<AddBackward0>)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/_pydev_bundle/pydev_umd.py", line 197, in runfile
    pydev_imports.execfile(filename, global_vars, local_vars)  # execute the script
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/_pydev_imps/_pydev_execfile.py", line 18, in execfile
    exec(compile(contents+"\n", file, 'exec'), glob, loc)
  File "/Users/andy/PycharmProjects/hello_pytorch/lesson/lesson-05/lesson-05-autograd.py", line 145, in <module>
    print(w.add_(1))
RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.

2. 逻辑回归

逻辑回归 (Logistic Regression)线性二分类 模型。

模型表达式:

\[y=f(WX+b)\] \[f(x)=\dfrac{1}{1+e^{-x}}\]

即:

\[y=\dfrac{1}{1+e^{-(WX+b)}}\]

这里,我们将 $f(x)$ 称为 Sigmoid 函数,又称 Logistic 函数:

\[\text{class}=\begin{cases}0, & y< 0.5 \\[2ex] 1, & y \ge 0.5 \end{cases}\]

线性回归是分析 自变量 $x$因变量 $y$ (标量) 之间关系的方法;而逻辑回归是分析 自变量 $x$因变量 $y$ (概率) 之间关系的方法。

机器学习训练的 5 个步骤

  1. 数据:数据收集、清洗、划分、预处理。
  2. 模型:根据任务的难易程度,选择简单的线性模型或者复杂的神经网络模型等等。
  3. 损失函数:根据不同任务选择不同的损失函数并计算其梯度。例如:在线性回归中,我们可以选择均方误差损失函数;在分类任务中,我们可以选择交叉熵损失函数。
  4. 优化器:得到梯度之后,我们选择某种优化器来更新权值。
  5. 迭代训练:有了数据、模型、损失函数和优化器之后,我们就可以进行迭代训练了。

代码示例


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import numpy as np
torch.manual_seed(10)

# ============================== Step 1/5: 生成数据 ===================================
sample_nums = 100
mean_value = 1.7
bias = 1
n_data = torch.ones(sample_nums, 2)
x0 = torch.normal(mean_value * n_data, 1) + bias    # 类别0 数据 shape=(100, 2)
y0 = torch.zeros(sample_nums)                       # 类别0 标签 shape=(100, 1)
x1 = torch.normal(-mean_value * n_data, 1) + bias   # 类别1 数据 shape=(100, 2)
y1 = torch.ones(sample_nums)                        # 类别1 标签 shape=(100, 1)
train_x = torch.cat((x0, x1), 0)
train_y = torch.cat((y0, y1), 0)


# ============================== Step 2/5: 选择模型 ===================================
class LR(nn.Module):
    def __init__(self):
        super(LR, self).__init__()
        self.features = nn.Linear(2, 1)
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        x = self.features(x)
        x = self.sigmoid(x)
        return x


lr_net = LR()   # 实例化逻辑回归模型

# ============================== Step 3/5: 选择损失函数 ================================
loss_fn = nn.BCELoss()  # 二分类交叉熵损失 Binary Cross Entropy Loss

# ============================== Step 4/5: 选择优化器 ==================================
lr = 0.01   # 学习率
optimizer = torch.optim.SGD(lr_net.parameters(), lr=lr, momentum=0.9)    # 随机梯度下降

# ============================== Step 5/5: 模型训练 ====================================
for iteration in range(1000):

    # 前向传播
    y_pred = lr_net(train_x)

    # 计算 loss
    loss = loss_fn(y_pred.squeeze(), train_y)

    # 反向传播
    loss.backward()

    # 更新参数
    optimizer.step()

    # 绘图
    if iteration % 20 == 0:

        mask = y_pred.ge(0.5).float().squeeze()  # 以 0.5 为阈值进行分类
        correct = (mask == train_y).sum()   # 计算正确预测的样本个数
        acc = correct.item() / train_y.size(0)   # 计算分类准确率

        plt.scatter(x0.data.numpy()[:, 0], x0.data.numpy()[:, 1], c='r', label='class 0')
        plt.scatter(x1.data.numpy()[:, 0], x1.data.numpy()[:, 1], c='b', label='class 1')

        w0, w1 = lr_net.features.weight[0]
        w0, w1 = float(w0.item()), float(w1.item())
        plot_b = float(lr_net.features.bias[0].item())
        plot_x = np.arange(-6, 6, 0.1)
        plot_y = (-w0 * plot_x - plot_b) / w1

        plt.xlim(-5, 7)
        plt.ylim(-7, 7)
        plt.plot(plot_x, plot_y)

        plt.text(-5, 5, 'Loss=%.4f' % loss.data.numpy(), fontdict={'size': 20, 'color': 'red'})
        plt.title("Iteration: {}\nw0:{:.2f} w1:{:.2f} b:{:.2f} accuracy:{:.2%}".format(iteration, w0, w1, plot_b, acc))
        plt.legend()

        plt.show()
        plt.pause(0.5)

        if acc > 0.99:
            break

3. 总结

本节课介绍了 PyTorch 自动求导系统中的 torch.autograd.backwardtorch.autograd.grad 这两个常用方法,并演示了一阶、二阶导数的求导过程;理解了自动求导系统,以及数据载体 —— 张量,前向传播构建计算图,计算图求取梯度过程。有了这些知识之后,我们就可以开始正式训练机器学习模型。这里通过演示逻辑回归模型的训练,学习了机器学习回归模型的五大模块:数据、模型、损失函数、优化器和迭代训练过程。这五大模块将是后面学习的主线。

下节内容:数据读取机制:Dataloader 与 Dataset

知识共享许可协议本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 欢迎转载,并请注明来自:YEY 的博客 同时保持文章内容的完整和以上声明信息!