PyTorch for Deep Learning & Machine Learning – Full Course(3) - NoTe0x008

Baobao0824

原链接:
https://www.youtube.com/watch?v=V_xro1bcAuA

写在前面

这一系列是我在学习他人的优质课程(博客文章、视频课程等)时所做的学习笔记,根据我自身的水平来进行学习,同时会进行思维的发散,补充原文中没有提到或者有错误的地方。同时也会进行经常性的更新和整理,让它和我的当下的状态更加契合。

这是Free Camp在油管中的PyTorch教程的第三期,我们主要来学习视频中Chapter2的部分。这一部分是在学习如何用神经网络来完成分类问题。

利用circle的二分类问题

Introduction to machine learning classification

分类问题是预测某个对象属于哪一类的任务。首先我们先制造一些数据吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import torch
import sklearn
# 虽然引入要这么import,但是安装的时候要用的包名是scikit-learn
from sklearn.datasets import make_circles
# 制造1000个变量
n_samples = 1000
X, y = make_circles(n_samples,noise=0.03,random_state=42)
# 这个函数会生成两个同心圆形状的样本点,内圈属于类别0,外圈属于类别1
# n_samples接受一个(2,)的tuple或者int,如果传入的是int,若为偶数,则内圈外圈的数据量评分,如果是奇数,那么内圈比外圈多一条数据。
# noise是Gaussian噪声标准差,值越大点就越分散。
print(f"First 5 samples of X:\n {X[:5]}")
print(f"First 5 samples of y:\n {y[:5]}")
# First 5 samples of X:
# [[ 0.75424625 0.23148074]
# [-0.75615888 0.15325888]
# [-0.81539193 0.17328203]
# [-0.39373073 0.69288277]
# [ 0.44220765 -0.89672343]]
# First 5 samples of y:
# [1 1 1 1 0]
# X的每一条有两个元素,分别是横坐标和纵坐标,y的值则代表了类别,0代表内圈1代表外圈

获得了数据之后,我们利用pandas将其做成一个dataframe,方便我们来进行查看和使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pandas as pd
circles = pd.DataFrame({"X1": X[:, 0], "X2": X[:, 1],"label": y})
circles.head(10)
# X1 X2 label
# 0 0.754246 0.231481 1
# 1 -0.756159 0.153259 1
# 2 -0.815392 0.173282 1
# 3 -0.393731 0.692883 1
# 4 0.442208 -0.896723 0
# 5 -0.479646 0.676435 1
# 6 -0.013648 0.803349 1
# 7 0.771513 0.147760 1
# 8 -0.169322 -0.793456 1
# 9 -0.121486 1.021509 0
circles.label.value_counts()
# value_counts方法可以分类统计每一个值都有多少
# label
# 1 500
# 0 500
# Name: count, dtype: int64

可以看出,外圈和内圈各生成了500个点。为了便于观察,我们将这些点进行可视化看一下,结果如下图所示。作者说,我们所使用的数据通常被称为玩具数据集,这类数据集规模较小,便于快速实验,同时又足够完整,适合用来练习深度学习的基础流程。

1
2
3
import matplotlib.pyplot as plt
plt.scatter(x=X[:, 0],y=X[:, 1],c=y,cmap=plt.cm.RdYlBu);
# x代表横轴,y代表y轴,c代表颜色,cmap是配色方案

由于图片比例的关系,这个圆看起来就没有那么的圆
由于图片比例的关系,这个圆看起来就没有那么的圆

Classification input and outputs

1
2
3
4
X.shape, y.shape
# ((1000, 2), (1000,))
type(X), X.dtype
# (numpy.ndarray, dtype('float64'))

现在Xy还是numpy数组,而不是张量,因此我们还得先将他们整理成张量。

1
2
3
4
5
6
7
8
9
10
11
12
# 把这对数据转成张量。
X = torch.from_numpy(X).type(torch.float)
y = torch.from_numpy(y).type(torch.float)
X[:5], y[:5]
# (tensor([[ 0.7542, 0.2315],
# [-0.7562, 0.1533],
# [-0.8154, 0.1733],
# [-0.3937, 0.6929],
# [ 0.4422, -0.8967]]),
# tensor([1., 1., 1., 1., 0.]))
type(X), X.dtype, y.dtype
# (torch.Tensor, torch.float32, torch.float32)

然后利用sklearn.model_selection.train_test_split函数可以将这些数据分为对应的训练集和测试集。

1
2
3
4
5
6
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y,test_size=0.2, random_state=42)
# test_size参数代表着取全体数据的多少作为测试数据,这里就是用80%的训练,20%的测试。
# 这个返回的数据顺序是固定的,不需要动我说实话。
len(X_train), len(X_test), len(y_train), len(y_test)
# (800, 200, 800, 200)

Architecture of a classification neural network

这些数据都处理好了之后,我们就可以开始创建模型了。还记得上一章讲的模型最重要的要素吗:结构、损失函数、优化器,以及训练和测试过程。

1
2
3
4
5
import torch
from torch import nn
device = "cuda" if torch.cuda.is_available() else "mps" if torch.mps.is_available() else "cpu"
device
# 'mps'

现在就到了创建模型的时间。首先,一个模型需要继承自nn.Module,然后从模型设计来说,该模型需要两个nn.Linear层,使其能适配我们的数据形状。同时别忘了定义对应的forward方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from sklearn import datasets

class CircleModelV0(nn.Module):
def __init__(self):
super().__init__()
# 输入数据是二元的(x,y)坐标,然后我们让中间层的神经元数量为5,5大于2,能让模型学习 更 复杂的特征组合。
# 如果小于2,那么模型无法学习复杂模式,容易欠拟合
# 如果太多了,可能完美记住训练数据,但测试集表现差导致过拟合(因为记住了太多无用的细节)
self.layer_1 = nn.Linear(in_features=2, out_features=5)
self.layer_2 = nn.Linear(in_features=5, out_features=1)
# 第二层神经元的输出就应该是我们要的结果了

# 定义forward方法,描述模型的前向传播过程
def forward(self, x):
return self.layer_2(self.layer_1(x)) # x -> layer_1 -> layer_2 -> output

# 实例化模型类并将其发送到目标设备
model_0 = CircleModelV0().to(device)
model_0
# CircleModelV0(
# (layer_1): Linear(in_features=2, out_features=5, bias=True)
# (layer_2): Linear(in_features=5, out_features=1, bias=True)
# )

Using torch.nn.Sequential

除了上述手动定义的方式,nn.Sequential可以让我们快速构建模型,而无需手动编写forward()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
model_0 = nn.Sequential(
nn.Linear(in_features=2, out_features=5),
nn.Linear(in_features=5, out_features=1)
).to(device)
model_0
# Sequential(
# (0): Linear(in_features=2, out_features=5, bias=True)
# (1): Linear(in_features=5, out_features=1, bias=True)
# )
model_0.state_dict()
# OrderedDict([('0.weight',
# tensor([[ 0.3472, 0.1467],
# [ 0.1566, 0.5652],
# [-0.5029, -0.5126],
# [-0.6709, 0.3610],
# [-0.1104, -0.4075]], device='mps:0')),
# ('0.bias',
# tensor([ 0.1106, -0.4730, 0.3918, 0.3028, -0.1557], device='mps:0')),
# ('1.weight',
# tensor([[-0.0118, -0.2562, -0.4312, 0.3098, 0.0387]], device='mps:0')),
# ('1.bias', tensor([0.4031], device='mps:0'))])

直接让他做预测看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
with torch.inference_mode():
untrained_preds = model_0(X_test.to(device))
len(untrained_preds),untrained_preds.shape
# (200, torch.Size([200, 1]))
len(X_test),X_test.shape
# (200, torch.Size([200, 2]))

# 预测值前10个
torch.round(untrained_preds[:10])
# tensor([[1.],
# [1.],
# [0.],
# [1.],
# [0.],
# [0.],
# [1.],
# [0.],
# [0.],
# [1.]], device='mps:0')

# 真实值前10个
y_test[:10]
# tensor([1., 0., 1., 0., 1., 1., 0., 0., 1., 0.])

Loss, optimizer and evaluation functions for classification

在分类问题中,我们通常选择的损失函数是二元交叉熵(Binary Cross Entropy)或者分类交叉熵(Categorical Cross Entropy,即普通交叉熵)。而我们使用的优化器一般是随机梯度下降(SGD)或者Adam。

在这里我们使用的损失函数是nn.BCEWithLogicalLoss

1
2
3
4
5
6
7
8
loss_fn = nn.BCEWithLogitsLoss()
# 内置Sigmoid激活函数,输入数据必须先经过Sigmoid激活函数处理,然后再传入BCELoss。
optimizer = torch.optim.SGD(params=model_0.parameters(),lr=0.1)
# 顺便定义一个准确率计算函数,看看最后到底能有多少是对的
def accuracy_fn(y_true, y_pred):
correct = torch.eq(y_true, y_pred).sum().item()
acc = (correct/len(y_pred)) * 100
return acc

Train and test loops

别忘了一个训练循环包括以下步骤:

  1. 前向传播
  2. 计算损失
  3. 优化器清零梯度
  4. 损失反向传播
  5. 优化器更新参数

我们模型输出的是原始对数几率(logits)可以通过将其传入某种激活函数来将其转换为预测概率(例如,二分类使用Sigmoid,多分类使用Softmax),然后可以通过四舍五入或者argmax将预测概率转换为预测标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
model_0.eval() 
with torch.inference_mode():
y_logits = model_0(X_test.to(device))[:5]
y_logits
# tensor([[0.5730],
# [0.6049],
# [0.4159],
# [0.6178],
# [0.2990]], device='mps:0')

# 使用sigmoid函数让logits变成预测概率
y_pred_probs = torch.sigmoid(y_logits)
y_pred_probs
# tensor([[0.6395],
# [0.6468],
# [0.6025],
# [0.6497],
# [0.5742]], device='mps:0')

对于我们预测的概率值,直接进行区间取整处理。

  • y_pred_probs >= 0.5时,y=1
  • y_pred_probs < 0.5时,y=0
1
2
3
4
5
6
7
# 预测的类型
y_preds = torch.round(y_pred_probs)
# 完整流程直接给结果
y_pred_labels = torch.round(torch.sigmoid(model_0(X_test.to(device))[:5]))
y_preds.squeeze(), y_pred_labels.squeeze()
# (tensor([1., 1., 1., 1., 1.], device='mps:0'),
# tensor([1., 1., 1., 1., 1.], device='mps:0', grad_fn=<SqueezeBackward0>))

接下来构建对应的训练代码。

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
torch.manual_seed(42)
torch.cuda.manual_seed(42)
# 设定训练轮数
epochs = 100
# 把数据放到对应的设备中
X_train, y_train = X_train.to(device), y_train.to(device)
X_test, y_test = X_test.to(device), y_test.to(device)
# 建立训练和评估循环
for epoch in range(epochs):
# 训练模式
model_0.train()
# 1. 前向传播
y_logits = model_0(X_train).squeeze()
y_pred = torch.round(torch.sigmoid(y_logits))
# 将logits做成预测标签
# 2. 计算损失和正确率
# nn.BCELoss需要预测概率当做输入,而nn.BCEWithLogitsLoss需要logit当输入
loss = loss_fn(y_logits, y_train)
acc = accuracy_fn(y_true=y_train, y_pred=y_pred)
# 3. 优化器清零
optimizer.zero_grad()
# 4. 反向传播
loss.backward()
# 5. 优化器步进
optimizer.step()
# 测试模式
model_0.eval()
with torch.inference_mode():
# 1. 前向传播
test_logits = model_0(X_test).squeeze()
test_pred = torch.round(torch.sigmoid(test_logits))
# 2. 计算测试损失/准确率
test_loss = loss_fn(test_logits,y_test)
test_acc = accuracy_fn(y_true=y_test,y_pred=test_pred)

# 打印看一下数据
if epoch % 10 == 0:
print(f"Epoch: {epoch} | Loss: {loss:.5f}, Acc: {acc:.2f}% | Test loss: {test_loss:.5f}, Test acc: {test_acc:.2f}%")

输出如下。

1
2
3
4
5
6
7
8
9
10
Epoch: 0 | Loss: 0.71773, Acc: 50.00% | Test loss: 0.72110, Test acc: 50.00%
Epoch: 10 | Loss: 0.70335, Acc: 50.00% | Test loss: 0.70843, Test acc: 50.00%
Epoch: 20 | Loss: 0.69769, Acc: 54.75% | Test loss: 0.70326, Test acc: 55.50%
Epoch: 30 | Loss: 0.69539, Acc: 54.00% | Test loss: 0.70094, Test acc: 51.00%
Epoch: 40 | Loss: 0.69440, Acc: 52.75% | Test loss: 0.69973, Test acc: 49.00%
Epoch: 50 | Loss: 0.69392, Acc: 52.12% | Test loss: 0.69895, Test acc: 47.00%
Epoch: 60 | Loss: 0.69365, Acc: 51.88% | Test loss: 0.69837, Test acc: 46.50%
Epoch: 70 | Loss: 0.69348, Acc: 51.38% | Test loss: 0.69789, Test acc: 46.50%
Epoch: 80 | Loss: 0.69337, Acc: 50.88% | Test loss: 0.69749, Test acc: 47.00%
Epoch: 90 | Loss: 0.69328, Acc: 51.38% | Test loss: 0.69714, Test acc: 47.50%

Discussing options to improve a model

这个概率我说实话,似乎就像是没有学到任何东西,准确率和直接随机预测似乎没有区别,为了做一些深入检查我们需要将它们进行可视化。

1
2
3
4
5
6
7
8
9
10
from helper_functions import plot_predictions, plot_decision_boundary
# 这里的helper_functions是作者提供好的工具类,里面有一些函数。
# 打印预测边界
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.title("Train")
plot_decision_boundary(model_0, X_train, y_train)
plt.subplot(1, 2, 2)
plt.title("Test")
plot_decision_boundary(model_0, X_test, y_test)

可视化显示的图片如图所示。

说实话我看不出这个图片有什么用
说实话我看不出这个图片有什么用

要改进一个模型,我们都有什么方法呢?

  • 增加层数,为模型提供更多学习的机会。
  • 增加隐藏层神经元数量,例如从5个增加到10个。
  • 延长训练时间,增加epoch的数量。
  • 更换激活函数,尝试不同的非线性激活函数。
  • 调整学习率,改变优化器更新参数的步长。
  • 更换损失函数,尝试不同的损失计算方式。

这些都是模型视角的调整,直接作用于模型本身,而非数据层面,都是我们可以手动调整的,被称作超参数。现在我们增加网络层数、增加训练论述,以及增加隐藏层的单元数,尝试使用我们的V1版模型。

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
class CircleModelV1(nn.Module):
def __init__(self):
super().__init__()
self.layer_1 = nn.Linear(in_features=2, out_features=10)
self.layer_2 = nn.Linear(in_features=10, out_features=10)
self.layer_3 = nn.Linear(in_features=10, out_features=1)

def forward(self, x):
# z = self.layer_1(x)
# z = self.layer_2(z)
# z = self.layer_3(z)
return self.layer_3(self.layer_2(self.layer_1(x)))
# 这种写法会在底层自动利用可能的运算加速。

model_1 = CircleModelV1().to(device)
model_1
# CircleModelV1(
# (layer_1): Linear(in_features=2, out_features=10, bias=True)
# (layer_2): Linear(in_features=10, out_features=10, bias=True)
# (layer_3): Linear(in_features=10, out_features=1, bias=True)
# )
# 创建损失函数
loss_fn = nn.BCEWithLogitsLoss()
# 创建优化器
optimizer = torch.optim.SGD(params=model_1.parameters(), lr=0.1)
# 和之前一样,只不过是把模型换成了model_1
torch.manual_seed(42)
torch.cuda.manual_seed(42)
# 设定训练轮数
epochs = 100
# 把数据放到对应的设备中
X_train, y_train = X_train.to(device), y_train.to(device)
X_test, y_test = X_test.to(device), y_test.to(device)
# 建立训练和评估循环
for epoch in range(epochs):
# 训练模式
model_1.train()
# 1. 前向传播
y_logits = model_1(X_train).squeeze()
y_pred = torch.round(torch.sigmoid(y_logits))
# 将logits做成预测标签
# 2. 计算损失和正确率
# nn.BCELoss需要预测概率当做输入,而nn.BCEWithLogitsLoss需要logit当输入
loss = loss_fn(y_logits, y_train)
acc = accuracy_fn(y_true=y_train, y_pred=y_pred)
# 3. 优化器清零
optimizer.zero_grad()
# 4. 反向传播
loss.backward()
# 5. 优化器步进
optimizer.step()
# 测试模式
model_1.eval()
with torch.inference_mode():
# 1. 前向传播
test_logits = model_1(X_test).squeeze()
test_pred = torch.round(torch.sigmoid(test_logits))
# 2. 计算测试损失/准确率
test_loss = loss_fn(test_logits,y_test)
test_acc = accuracy_fn(y_true=y_test,y_pred=test_pred)

# 打印看一下数据
if epoch % 10 == 0:
print(f"Epoch: {epoch} | Loss: {loss:.5f}, Acc: {acc:.2f}% | Test loss: {test_loss:.5f}, Test acc: {test_acc:.2f}%")

我说实话这个效果也一般。

1
2
3
4
5
6
7
8
9
10
Epoch: 0 | Loss: 0.69396, Acc: 50.88% | Test loss: 0.69261, Test acc: 51.00%
Epoch: 100 | Loss: 0.69305, Acc: 50.38% | Test loss: 0.69379, Test acc: 48.00%
Epoch: 200 | Loss: 0.69299, Acc: 51.12% | Test loss: 0.69437, Test acc: 46.00%
Epoch: 300 | Loss: 0.69298, Acc: 51.62% | Test loss: 0.69458, Test acc: 45.00%
Epoch: 400 | Loss: 0.69298, Acc: 51.12% | Test loss: 0.69465, Test acc: 46.00%
Epoch: 500 | Loss: 0.69298, Acc: 51.00% | Test loss: 0.69467, Test acc: 46.00%
Epoch: 600 | Loss: 0.69298, Acc: 51.00% | Test loss: 0.69468, Test acc: 46.00%
Epoch: 700 | Loss: 0.69298, Acc: 51.00% | Test loss: 0.69468, Test acc: 46.00%
Epoch: 800 | Loss: 0.69298, Acc: 51.00% | Test loss: 0.69468, Test acc: 46.00%
Epoch: 900 | Loss: 0.69298, Acc: 51.00% | Test loss: 0.69468, Test acc: 46.00%

画图出来看一下,还是刚才的图片代码。

1
2
3
4
5
6
7
8
from helper_functions import plot_predictions, plot_decision_boundary
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.title("Train")
plot_decision_boundary(model_1, X_train, y_train)
plt.subplot(1, 2, 2)
plt.title("Test")
plot_decision_boundary(model_1, X_test, y_test)

除了背景斜率有点区别之外我感觉都差不多
除了背景斜率有点区别之外我感觉都差不多

Creating a straight line dataset

都差不多,那么我们就需要排查排查了。最有效的方法是准备一个小问题看看他能不能办得到。也就是我们需要再次用上一回的那个直线数据集了。

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
# 和上一次一样创建数据。
weight = 0.7
bias = 0.3
start = 0
end = 1
step = 0.01
X_regression = torch.arange(start, end, step).unsqueeze(dim=1)
y_regression = weight * X_regression + bias
# 线性回归公式
X_regression[:5], y_regression[:5]
# (tensor([[0.0000],
# [0.0100],
# [0.0200],
# [0.0300],
# [0.0400]]),
# tensor([[0.3000],
# [0.3070],
# [0.3140],
# [0.3210],
# [0.3280]]))
# 分出训练集和测试集。
train_split = int(0.8 * len(X_regression))
X_train_regression, y_train_regression = X_regression[:train_split], y_regression[:train_split]
X_test_regression, y_test_regression = X_regression[train_split:], y_regression[train_split:]
# 检查一下每一边的长度。
len(X_train_regression), len(X_test_regression), len(y_train_regression), len(y_test_regression)
# (80, 20, 80, 20)
plot_predictions(train_data=X_train_regression, train_labels=y_train_regression,test_data=X_test_regression,test_labels=y_test_regression)

感觉这个图比上次的大多了
感觉这个图比上次的大多了

Evaluating our model’s predictions

为了测试我们的模型到底有没有问题,我们仿照model_1的结构,设置一个其它参数一样只是输入输出有区别的model_2来进行测试。

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
model_2 = nn.Sequential(
nn.Linear(in_features=1, out_features=10),
nn.Linear(in_features=10, out_features=10),
nn.Linear(in_features=10, out_features=1)
).to(device)
model_2
# Sequential(
# (0): Linear(in_features=1, out_features=10, bias=True)
# (1): Linear(in_features=10, out_features=10, bias=True)
# (2): Linear(in_features=10, out_features=1, bias=True)
# )
loss_fn = nn.L1Loss()
optimizer = torch.optim.SGD(params=model_2.parameters(), lr=0.01)
# 训练模型
torch.manual_seed(42)
torch.cuda.manual_seed(42)
# 训练轮数
epochs = 1000
X_train_regression, y_train_regression = X_train_regression.to(device), y_train_regression.to(device)
X_test_regression, y_test_regression = X_test_regression.to(device), y_test_regression.to(device)
# 训练过程
for epoch in range(epochs):
y_pred = model_2(X_train_regression)
loss = loss_fn(y_pred, y_train_regression)
optimizer.zero_grad()
loss.backward()
optimizer.step()
model_2.eval()
with torch.inference_mode():
test_pred = model_2(X_test_regression)
test_loss = loss_fn(test_pred, y_test_regression)
# Print out what's happenin'
if epoch % 100 == 0:
print(f"Epoch: {epoch} | Loss: {loss:.5f} | Test loss: {test_loss:.5f}")

# 评估模式
model_2.eval()
with torch.inference_mode():
y_preds = model_2(X_test_regression)
plot_predictions(train_data=X_train_regression.cpu(), train_labels=y_train_regression.cpu(),test_data=X_test_regression.cpu(),test_labels=y_test_regression.cpu(),predictions=y_preds.cpu());
1
2
3
4
5
6
7
8
9
10
Epoch: 0 | Loss: 0.75986 | Test loss: 0.91103
Epoch: 100 | Loss: 0.02858 | Test loss: 0.00081
Epoch: 200 | Loss: 0.02533 | Test loss: 0.00209
Epoch: 300 | Loss: 0.02137 | Test loss: 0.00305
Epoch: 400 | Loss: 0.01964 | Test loss: 0.00341
Epoch: 500 | Loss: 0.01940 | Test loss: 0.00387
Epoch: 600 | Loss: 0.01903 | Test loss: 0.00379
Epoch: 700 | Loss: 0.01878 | Test loss: 0.00381
Epoch: 800 | Loss: 0.01840 | Test loss: 0.00329
Epoch: 900 | Loss: 0.01798 | Test loss: 0.00360

再可视化一下结果看看:

感觉结果也不太对
感觉结果也不太对

The missing piece – non-linearity

到底哪里出了问题呢?实际上就是我们的非线性函数不够多。我们现在更新一下我们的类,让他在每一层处理中间添加一个非线性的激活函数,创建一个模型model_3

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
from torch import nn
class CircleModelV2(nn.Module):
def __init__(self):
super().__init__()
self.layer_1 = nn.Linear(in_features=2, out_features=10)
self.layer_2 = nn.Linear(in_features=10, out_features=10)
self.layer_3 = nn.Linear(in_features=10, out_features=1)
self.relu = nn.ReLU() # relu是一个非线性的激活函数

def forward(self, x):
return self.layer_3(self.relu(self.layer_2(self.relu(self.layer_1(x)))))

model_3 = CircleModelV2().to(device)
model_3
# CircleModelV2(
# (layer_1): Linear(in_features=2, out_features=10, bias=True)
# (layer_2): Linear(in_features=10, out_features=10, bias=True)
# (layer_3): Linear(in_features=10, out_features=1, bias=True)
# (relu): ReLU()
# )
loss_fn = nn.BCEWithLogitsLoss()
optimizer = torch.optim.SGD(model_3.parameters(), lr=0.1)
# 同样的方法来训练model_3
torch.manual_seed(42)
torch.cuda.manual_seed(42)
# 设定训练轮数
epochs = 1000
# 把数据放到对应的设备中
X_train, y_train = X_train.to(device), y_train.to(device)
X_test, y_test = X_test.to(device), y_test.to(device)
# 建立训练和评估循环
for epoch in range(epochs):
# 训练模式
model_3.train()
# 1. 前向传播
y_logits = model_3(X_train).squeeze()
y_pred = torch.round(torch.sigmoid(y_logits))
# 将logits做成预测标签
# 2. 计算损失和正确率
# nn.BCELoss需要预测概率当做输入,而nn.BCEWithLogitsLoss需要logit当输入
loss = loss_fn(y_logits, y_train)
acc = accuracy_fn(y_true=y_train, y_pred=y_pred)
# 3. 优化器清零
optimizer.zero_grad()
# 4. 反向传播
loss.backward()
# 5. 优化器步进
optimizer.step()
# 测试模式
model_3.eval()
with torch.inference_mode():
# 1. 前向传播
test_logits = model_3(X_test).squeeze()
test_pred = torch.round(torch.sigmoid(test_logits))
# 2. 计算测试损失/准确率
test_loss = loss_fn(test_logits,y_test)
test_acc = accuracy_fn(y_true=y_test,y_pred=test_pred)

# 打印看一下数据
if epoch % 10 == 0:
print(f"Epoch: {epoch} | Loss: {loss:.5f}, Acc: {acc:.2f}% | Test loss: {test_loss:.5f}, Test acc: {test_acc:.2f}%"
# ...
# Epoch: 990 | Loss: 0.57520, Acc: 86.38% | Test loss: 0.57986, Test acc: 87.00%

最后这个准确率看起来似乎达到了要求,画图看一下吧。

1
2
3
4
5
6
7
8
9
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.title("Train")
plot_decision_boundary(model_1, X_train, y_train)
# model_1,没有非线性函数
plt.subplot(1, 2, 2)
plt.title("Test")
plot_decision_boundary(model_3, X_test, y_test)
# model_3,有非线性函数

你要是这样的话那我大概能看出来这个图是干嘛的
你要是这样的话那我大概能看出来这个图是干嘛的

同时作者问我们,能不能尝试让模型在测试集上的准确率达到80%以上呢?

对于神经网络,我们并不明确告知模型该学习什么,而是赋予其工具,让它能自主发掘数据中的模式与规律,这些工具便是线性与非线性函数。

1
2
3
4
5
6
7
8
9
A = torch.arange(-10, 10, 1, dtype=torch.float32)
A
# tensor([-10., -9., -8., -7., -6., -5., -4., -3., -2., -1., 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
torch.relu(A)
# tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
# 看来relu函数是把负数全部变成0
torch.sigmoid(A)
# tensor([4.5398e-05, 1.2339e-04, 3.3535e-04, 9.1105e-04, 2.4726e-03, 6.6929e-03, 1.7986e-02, 4.7426e-02, 1.1920e-01, 2.6894e-01, 5.0000e-01, 7.3106e-01, 8.8080e-01, 9.5257e-01, 9.8201e-01, 9.9331e-01, 9.9753e-01, 9.9909e-01, 9.9966e-01, 9.9988e-01])
# 这个函数就是把logit变成概率的那个函数。

这样直接看sigmod说实话看着太费劲了,我们把它可视化一下。

看起来很像酸碱中和滴定时候的ph值变化
看起来很像酸碱中和滴定时候的ph值变化

Putting it all together with a multiclass problem

接下来我们尝试进行一个多分类问题,区别于传统的猫狗大战,这种多分类问题并不是非此即彼的选择,有很多个类别要用,例如猫狗还是鸡。首先我们依旧获取一些数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.model_selection import train_test_split
# 生成若干团状数据点,每个团代表一个类别

# 设定对应的超参数
NUM_CLASSES = 4
NUM_FEATURES = 2
RANDOM_SEED = 42

# 创建多分类数据
X_blob, y_blob = make_blobs(n_samples=1000,n_features=NUM_FEATURES,centers=NUM_CLASSES, cluster_std=1.5, random_state= RANDOM_SEED, return_centers=False) # pyright: ignore[reportAssignmentType]
# n_features是特征数量,centers是类别数,cluster_std是标准差,如果return_centers为True的话会再返回一个array,但是他还是会报错所以加个ignore

# 将数据变成Tensor
X_blob = torch.from_numpy(X_blob).type(torch.float32)
y_blob = torch.from_numpy(y_blob).type(torch.int64)
# 将数据分成训练集和测试集
X_blob_train, X_blob_test, y_blob_train, y_blob_test = train_test_split(X_blob,y_blob,test_size=0.2,random_state=RANDOM_SEED)
# 可视化,横纵坐标分别是X的两个特征,颜色代表类别
plt.figure(figsize=(10,7))
plt.scatter(X_blob[:,0],X_blob[:,1],c=y_blob,cmap=plt.get_cmap("RdYlBu"))

这个图颇有我上学期做数学建模的意思了
这个图颇有我上学期做数学建模的意思了

接下来我们就可以创建对应的模型了。我们的模型包括三层线性层,每一层之间都用一个ReLU激活函数。

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
from torch import nn
device = "cuda" if torch.cuda.is_available() else "mps" if torch.mps.is_available() else "cpu"
class BlobModel(nn.Module):
def __init__(self, input_features, output_features, hidden_units=8):
super().__init__()
self.linear_layer_stack = nn.Sequential(
nn.Linear(in_features=input_features, out_features=hidden_units),
nn.ReLU(),
nn.Linear(in_features=hidden_units, out_features=hidden_units),
nn.ReLU(),
nn.Linear(in_features=hidden_units, out_features=output_features)
)
def forward(self,x):
return self.linear_layer_stack(x)

model_4 = BlobModel(input_features=2, output_features=4, hidden_units=8).to(device)
model_4
# BlobModel(
# (linear_layer_stack): Sequential(
# (0): Linear(in_features=2, out_features=8, bias=True)
# (1): ReLU()
# (2): Linear(in_features=8, out_features=8, bias=True)
# (3): ReLU()
# (4): Linear(in_features=8, out_features=4, bias=True)
# )
# )

然后就是定义损失函数、优化器以及编写训练代码,这里的损失函数以及优化器都是比较适合多分类任务的。

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
epochs = 100
# 把数据放到对应的数据集中
X_blob_train, y_blob_train = X_blob_train.to(device), y_blob_train.to(device)
X_blob_test, y_blob_test = X_blob_test.to(device), y_blob_test.to(device)
# 训练周期
for epoch in range(epochs):
model_4.train()
y_logits = model_4(X_blob_train)
y_pred = torch.softmax(y_logits,dim=1).argmax(dim=1)
loss = loss_fn(y_logits,y_blob_train)
accrancy = accuracy_fn(y_true=y_blob_train, y_pred=y_pred)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 测试环节
model_4.eval()
with torch.inference_mode():
test_logits = model_4(X_blob_test)
test_preds = torch.softmax(test_logits,dim=1).argmax(dim=1)
test_loss = loss_fn(test_logits, y_blob_test)
test_acc = accuracy_fn(y_true=y_blob_test, y_pred=test_preds)
if epoch %10 ==0:
print(f"Epoch: {epoch},Loss: {loss:.4f}, Acc: {accrancy:.2f}, Test Loss: {test_loss:.4f}, Test acc: {test_acc:.2f}%")
# Epoch: 0,Loss: 0.9296, Acc: 64.50, Test Loss: 0.8292, Test acc: 73.00%
# Epoch: 10,Loss: 0.5612, Acc: 93.38, Test Loss: 0.4929, Test acc: 95.50%
# Epoch: 20,Loss: 0.3503, Acc: 98.50, Test Loss: 0.2927, Test acc: 99.00%
# Epoch: 30,Loss: 0.1760, Acc: 99.00, Test Loss: 0.1423, Test acc: 99.50%
# Epoch: 40,Loss: 0.0937, Acc: 99.12, Test Loss: 0.0772, Test acc: 99.50%
# Epoch: 50,Loss: 0.0659, Acc: 99.12, Test Loss: 0.0537, Test acc: 99.50%
# Epoch: 60,Loss: 0.0536, Acc: 99.12, Test Loss: 0.0427, Test acc: 99.50%
# Epoch: 70,Loss: 0.0468, Acc: 99.12, Test Loss: 0.0365, Test acc: 99.50%
# Epoch: 80,Loss: 0.0426, Acc: 99.12, Test Loss: 0.0325, Test acc: 99.50%
# Epoch: 90,Loss: 0.0397, Acc: 99.12, Test Loss: 0.0298, Test acc: 99.50%

这个效果真的是出奇的好,直接让他画图来看看效果吧。

1
2
3
4
5
6
7
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.title("Train")
plot_decision_boundary(model_4, X_blob_train, y_blob_train)
plt.subplot(1, 2, 2)
plt.title("Test")
plot_decision_boundary(model_4, X_blob_test, y_blob_test)

这个边界分的非常的准确我说实话
这个边界分的非常的准确我说实话

作者同时还给我们介绍了一些其他的判断训练结果的方法,例如混淆矩阵,混淆矩阵是一个N*N(N是类别数)的表格,用于展示模型预测结果与真实标签之间的关系。

总结

在这一个Chapter中,我们学习了如何创建一个分类任务专用的神经网络,同时也掌握了更多的优化模型的方法。感觉很多神经网络的代码似乎都能看得懂了这下。

  • 标题: PyTorch for Deep Learning & Machine Learning – Full Course(3) - NoTe0x008
  • 作者: Baobao0824
  • 创建于 : 2026-06-09 15:42:09
  • 更新于 : 2026-06-09 15:42:09
  • 链接: https://blog.baobao0824.top//学习笔记NoTe/NT-0x008/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论