PyTorch 神经网络分类
Reference: PyTorch Neural Network Classification
该页面由 Jupyter Notebook 生成,原文件于 Github
# 先导入包
import torch
from torch import nn
import matplotlib.pyplot as plt
torch.__version__
'2.5.1+cu124'
分类问题有二分类、多分类、多标签等情况。二分类问题则是或不是;多分类问题具有多个类别区分;多标签问题则一个目标可以被分配多个选项。
分类神经网络的一般架构:
项目 | 二分类 | 多分类 |
---|---|---|
输入层 Shape(in_features ) |
与特征数相同 | 与特征数相同 |
隐藏层 | 特定问题特定分析 | 特定问题特定分析 |
每个隐藏层的神经元数量 | 特定问题特定分析,一般从 10 到 512 | 特定问题特定分析,一般从 10 到 512 |
输出层 Shape(out_features ) |
1(一个类别) | 每个类 1 个输出 |
隐藏层激活函数 | 通常是 ReLU | 通常是 ReLU |
输出层激活函数 | Sigmoid(torch.sigmoid ) |
Softmax(torch.softmax ) |
损失函数 | 二元交叉熵(torch.nn.BCELoss ) |
交叉熵(torch.nn.CrossEntropyLoss ) |
优化器 | SGD,Adam | SGD,Adam |
使用 Scikit-Learn 中的 make_circles()
方法生成两个带有不同颜色圆点的圆。
需要安装 Scikit-Learn:pip install scikit-learn
from sklearn.datasets import make_circles
n_samples = 1000
X, y = make_circles(n_samples,
noise=0.03,
random_state=42)
plt.scatter(x=X[:, 0],
y=X[:, 1],
c=y,
cmap=plt.cm.RdYlBu)
<matplotlib.collections.PathCollection at 0x22c9fc26610>
看看输入 Shape 和 输出 Shape,然后弄清楚输入层 Shape(特征数)和输出层 Shape。
X.shape, y.shape # 输入 Shape 和输出 Shape
((1000, 2), (1000,))
X[0].shape, y[0].shape # 输入层 Shape 和输出层 Shape
((2,), ())
这说明 X 的一个样本有两个特征(向量),而对应的 y 只有一个特征(标量)。
- 有两个输入对应一个输出。
具体来说:
- 将数据转换为张量。
- 将数据分成训练集和测试集。
X = torch.from_numpy(X).type(torch.float)
y = torch.from_numpy(y).type(torch.float)
X.dtype, y.dtype
(torch.float32, torch.float32)
使用 Scikit-Learn 中的函数 train_test_split()
。
test_size=0.2
(80%训练,20%测试),因为分割是随机发生的,所以使用 random_state=42
,使得随机可复现。
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)
len(X_train), len(y_train), len(X_test), len(y_test)
(800, 800, 200, 200)
构建模型的步骤:
- 设置与设备相关的代码。
- 通过继承
nn. module
来构造一个模型。 - 定义损失函数和优化器。
- 创建一个训练循环。
# 1. 设置设备
device = "cuda" if torch.cuda.is_available() else "cpu"
device
'cuda'
模型类的操作:
- 继承
nn.Module
。 - 在构造函数中创建 2 层
nn.Linear
线性层,能够处理 X 和 y 的形状。 - 定义一个
forward()
方法,该方法包含模型的前向传递计算。 - 实例化模型类并将其发送到目标设备。
class CircleModelV0(nn.Module):
def __init__(self):
super().__init__()
self.layer_1 = nn.Linear(in_features=2, out_features=5)
self.layer_2 = nn.Linear(in_features=5, out_features=1)
def forward(self, x):
return self.layer_2(self.layer_1)
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) )
由上面代码可知该模型类的结构为: 2(输入层) -> 5(隐藏层) -> 1(输出层)
也可以使用 nn.Sequential
执行与上面相同的操作。nn.Sequential
按层出现的顺序对输入数据执行前向传递计算。
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) )
自定义模型类可以自定义更多细节,而 nn.Sequential()
则更方便。
常见损失函数:
损失函数 | 适用类型 | 代码 |
---|---|---|
交叉熵损失函数 | 多分类 | torch.nn.CrossEntropyLoss |
平均绝对误差 MAE,L1 Loss | 回归问题 | torch.nn.L1Loss |
均方误差 MSE,L2 Loss | 回归问题 | torch.nn.MSELoss |
常见优化器:
优化器 | 适用类型 | 代码 |
---|---|---|
随机梯度下降(SGD) | 分类问题、回归问题等 | torch.optim.SGD() |
Adam | 分类问题、回归问题等 | torch.optim.Adam() |
此处讨论二分类问题,使用一个二元交叉熵损失函数。
注意:损失函数是衡量模型预测错误程度的函数,损失越高,模型越差。
此外,PyTorch 文档经常将损失函数称为“损失准则(loss criterion)”或“准则(criterion)”,这些都是描述同一事物的不同方式。
二元交叉熵函数有 torch.nn.BCELoss()
和 torch.nn.BCEWithLogitsLoss()
。
torch.nn.BCELoss()
:创建一个损失函数,用于测量目标(标签)和输入(特征)之间的二进制交叉熵。torch.nn.BCEWithLogitsLoss()
:它内置了一个 sigmoid 层,其他这与上面的相同。
torch.nn.BCEWithLogitsLoss()
的文档指出,它比在 nn.Sigmoid
层之后使用 torch.nn.BCELoss()
在数值上更稳定。
对于优化器,将使用 torch.optim.SGD()
以 0.1 的学习率优化模型参数。
loss_fn = nn.BCEWithLogitsLoss()
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
线性层的公式为:
$$ y = x \cdot \text{Weights}^T + bias $$模型的原始输出通常被称为logits。使用激活函数将 logits 转换成与真值标签相比较的数字。
torch.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)
model_0 = model_0.to(device)
for epoch in range(epochs):
model_0.train()
y_logits = model_0(X_train).squeeze()
y_pred = torch.round(torch.sigmoid(y_logits))
loss = loss_fn(y_logits, y_train)
acc = accuracy_fn(y_true=y_train, y_pred=y_pred)
optimizer.zero_grad()
loss.backward()
optimizer.step()
model_0.eval()
with torch.inference_mode():
test_logits = model_0(X_test).squeeze()
test_pred = torch.round(torch.sigmoid(test_logits))
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}/{epochs} | Loss: {loss:.5f}, Accu: {acc:.2f}% | Test Loss: {test_loss:.5f}, Test Accu: {test_acc:.2f}%")
0/100 | Loss: 0.70365, Accu: 49.88% | Test Loss: 0.71542, Test Accu: 45.50%
10/100 | Loss: 0.70059, Accu: 50.25% | Test Loss: 0.71142, Test Accu: 45.00%
20/100 | Loss: 0.69869, Accu: 50.38% | Test Loss: 0.70858, Test Accu: 46.00%
30/100 | Loss: 0.69740, Accu: 50.75% | Test Loss: 0.70641, Test Accu: 46.00%
40/100 | Loss: 0.69648, Accu: 50.75% | Test Loss: 0.70469, Test Accu: 45.50%
50/100 | Loss: 0.69580, Accu: 50.50% | Test Loss: 0.70329, Test Accu: 46.50%
60/100 | Loss: 0.69527, Accu: 50.75% | Test Loss: 0.70214, Test Accu: 46.00%
70/100 | Loss: 0.69487, Accu: 50.88% | Test Loss: 0.70117, Test Accu: 45.50%
80/100 | Loss: 0.69455, Accu: 50.75% | Test Loss: 0.70036, Test Accu: 45.00%
90/100 | Loss: 0.69429, Accu: 50.75% | Test Loss: 0.69966, Test Accu: 45.00%
模型看起来它很好地完成了训练和测试步骤,但结果似乎并没有太大的变化。每次数据分割时,准确率在 50% 左右。这是一个平衡的二进制分类问题,这意味着模型的性能与随机猜测差不多。
从指标来看,模型似乎是随机猜测。绘制一个模型预测的图,它试图预测的数据以及它为某个东西是类0还是类1创建的决策边界。
为此,编写一些代码,一个名为 plot_decision_boundary()
的有用函数,该函数创建一个 NumPy meshgrid,以可视化地绘制我们的模型预测某些类的不同点。
import numpy as np
def plot_decision_boundary(model: torch.nn.Module, X: torch.Tensor, y: torch.Tensor):
"""Plots decision boundaries of model predicting on X in comparison to y.
Source - https://madewithml.com/courses/foundations/neural-networks/ (with modifications)
"""
# Put everything to CPU (works better with NumPy + Matplotlib)
model.to("cpu")
X, y = X.to("cpu"), y.to("cpu")
# Setup prediction boundaries and grid
x_min, x_max = X[:, 0].min() - 0.1, X[:, 0].max() + 0.1
y_min, y_max = X[:, 1].min() - 0.1, X[:, 1].max() + 0.1
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 101), np.linspace(y_min, y_max, 101))
# Make features
X_to_pred_on = torch.from_numpy(np.column_stack((xx.ravel(), yy.ravel()))).float()
# Make predictions
model.eval()
with torch.inference_mode():
y_logits = model(X_to_pred_on)
# Test for multi-class or binary and adjust logits to prediction labels
if len(torch.unique(y)) > 2:
y_pred = torch.softmax(y_logits, dim=1).argmax(dim=1) # mutli-class
else:
y_pred = torch.round(torch.sigmoid(y_logits)) # binary
# Reshape preds and plot
y_pred = y_pred.reshape(xx.shape).detach().numpy()
plt.contourf(xx, yy, y_pred, cmap=plt.cm.RdYlBu, alpha=0.7)
plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.RdYlBu)
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())
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)
由图可知,模型目前正在尝试用直线分割红点和蓝点。由于我们的数据是圆形的,所以画一条直线最多只能把它从中间切开。
在机器学习方面,模型是欠拟合的,这意味着它没有从数据中学习预测模式。
尝试解决模型的拟合不足问题。
- 专注于模型(而不是数据)。
技巧 | 作用 |
---|---|
增加更多隐藏层 | 每一层都可能增加模型的学习能力,每一层都能够学习数据中的某种新模式。更多的层通常被称为使神经网络更深 |
增加更多隐藏神经元 | 与上面类似,每层更多的隐藏单元意味着模型学习能力的潜在增加。更多的隐藏单元通常被称为使你的神经网络更宽 |
增加训练轮数 | 如果模型训练更久,它可能会学到更多 |
改变激活函数 | 有些数据无法仅用直线拟合,使用非线性激活函数可以帮助解决这个问题 |
改变学习率 | 优化器的学习率决定了模型每一步应该改变多少参数,太多了模型会过度校正,太少了模型学习不足 |
改变损失函数 | 不同的问题需要不同的损失函数 |
迁移学习 | 从一个与问题领域相似的问题领域中提取一个预先训练好的模型,并根据问题进行调整 |
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)
self.relu = nn.ReLU()
def forward(self, x):
return self.layer_3(self.relu(self.layer_2(self.relu(self.layer_1(x)))))
model_1 = CircleModelV1().to(device)
# model_1 = nn.Sequential(
# nn.Linear(2, 10),
# nn.ReLU(),
# nn.Linear(10, 10),
# nn.ReLU(),
# nn.Linear(10, 1),
# ).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) (relu): ReLU() )
loss_fn = nn.BCEWithLogitsLoss()
optimizer = torch.optim.SGD(params=model_1.parameters(),
lr=0.1)
重新训练:
torch.manual_seed(42)
epochs = 1500
X_train, y_train = X_train.to(device), y_train.to(device)
X_test, y_test = X_test.to(device), y_test.to(device)
model_1.to(device)
for epoch in range(epochs):
model_1.train()
y_logits = model_1(X_train).squeeze()
y_pred = torch.round(torch.sigmoid(y_logits))
loss = loss_fn(y_logits, y_train)
acc = accuracy_fn(y_true=y_train, y_pred=y_pred)
optimizer.zero_grad()
loss.backward()
optimizer.step()
model_1.eval()
with torch.inference_mode():
test_logits = model_1(X_test).squeeze()
test_pred = torch.round(torch.sigmoid(test_logits))
test_loss = loss_fn(test_logits, y_test)
test_acc = accuracy_fn(y_true=y_test, y_pred=test_pred)
if epoch % 100 == 0:
print(f"{epoch}/{epochs} | Loss: {loss:.5f}, Accu: {acc:.2f}% | Test Loss: {test_loss:.5f}, Test Accu: {test_acc:.2f}%")
0/1500 | Loss: 0.69295, Accu: 50.00% | Test Loss: 0.69319, Test Accu: 50.00%
100/1500 | Loss: 0.69115, Accu: 52.88% | Test Loss: 0.69102, Test Accu: 52.50%
200/1500 | Loss: 0.68977, Accu: 53.37% | Test Loss: 0.68940, Test Accu: 55.00%
300/1500 | Loss: 0.68795, Accu: 53.00% | Test Loss: 0.68723, Test Accu: 56.00%
400/1500 | Loss: 0.68517, Accu: 52.75% | Test Loss: 0.68411, Test Accu: 56.50%
500/1500 | Loss: 0.68102, Accu: 52.75% | Test Loss: 0.67941, Test Accu: 56.50%
600/1500 | Loss: 0.67515, Accu: 54.50% | Test Loss: 0.67285, Test Accu: 56.00%
700/1500 | Loss: 0.66659, Accu: 58.38% | Test Loss: 0.66322, Test Accu: 59.00%
800/1500 | Loss: 0.65160, Accu: 64.00% | Test Loss: 0.64757, Test Accu: 67.50%
900/1500 | Loss: 0.62362, Accu: 74.00% | Test Loss: 0.62145, Test Accu: 79.00%
1000/1500 | Loss: 0.56818, Accu: 87.75% | Test Loss: 0.57378, Test Accu: 86.50%
1100/1500 | Loss: 0.48153, Accu: 93.50% | Test Loss: 0.49935, Test Accu: 90.50%
1200/1500 | Loss: 0.37056, Accu: 97.75% | Test Loss: 0.40595, Test Accu: 92.00%
1300/1500 | Loss: 0.25458, Accu: 99.00% | Test Loss: 0.30333, Test Accu: 96.50%
1400/1500 | Loss: 0.17180, Accu: 99.50% | Test Loss: 0.22108, Test Accu: 97.50%
可视化一下:
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)
现在模型的分类就有了显著的效果。
利用 Scikit-Learn 的 make_blobs()
方法。这个方法将创建任意数量的类(使用 centers
参数)。
- 使用
make_blobs()
创建一些多类数据。 - 将数据转换为张量(
make_blobs()
的默认值是使用 NumPy 数组)。 - 使用
train_test_split()
将数据拆分为训练集和测试集。 - 可视化数据。
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)
X_blob = torch.from_numpy(X_blob).type(torch.float)
y_blob = torch.from_numpy(y_blob).type(torch.LongTensor)
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)
plt.figure(figsize=(10, 6))
plt.scatter(X_blob[:, 0], X_blob[:, 1], c=y_blob, cmap=plt.cm.RdYlBu)
<matplotlib.collections.PathCollection at 0x22cb30b01d0>
创建一个 nn.Module
的子类,接受三个超参数:
input_features
:输入特征的数量。output_features
:输出特征数(等效于NUM_CLASSES
或多类分类问题中的类数)。hidden_units
:每个隐藏层使用的隐藏神经元的数量。
device = "cuda" if torch.cuda.is_available() else "cpu"
from torch import nn
class BlobModel(nn.Module):
def __init__(self, input_features, output_features, hidden_units=8):
super().__init__()
self.model = 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.model(x)
model_2 = BlobModel(input_features=NUM_FEATURES,
output_features=NUM_CLASSES,
hidden_units=8).to(device)
model_2
BlobModel( (model): 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) ) )
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model_2.parameters(),
lr=0.1)
试着看看模型前向输出:
y_logits = model_2(X_blob_train.to(device))[:5]
y_logits
tensor([[-0.7586, -0.6810, -1.5180, -1.1178], [-0.2398, -1.2335, -0.9858, -0.2899], [ 0.2528, -0.2379, 0.1882, -0.0066], [ 0.2391, -0.2472, 0.1494, 0.0213], [-0.1214, -0.9804, -0.6918, -0.1923]], device='cuda:0', grad_fn=<SliceBackward0>)
再看看经过激活函数 Softmax 之后的结果:
y_pred_probs = torch.softmax(y_logits, dim=1)
y_pred_probs
tensor([[0.3080, 0.3328, 0.1441, 0.2150], [0.3577, 0.1324, 0.1696, 0.3402], [0.3011, 0.1843, 0.2823, 0.2323], [0.3000, 0.1845, 0.2743, 0.2413], [0.3424, 0.1450, 0.1936, 0.3190]], device='cuda:0', grad_fn=<SoftmaxBackward0>)
经过 Softmax 函数之后,先前的数字变成预测到某类的概率。这些预测概率本质上是说模型认为目标样本(输入)映射到每个类的程度。
由于y_pred_probs中的每个类都有一个值,因此最高值的索引是模型认为特定数据样本最属于的类。
可以使用 torch.argmax()
检查哪个索引具有最高值。
torch.argmax(y_pred_probs[0])
tensor(1, device='cuda:0')
torch.manual_seed(42)
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)
model_2.to(device)
for epoch in range(epochs):
model_2.train()
y_logits = model_2(X_blob_train)
y_pred = torch.softmax(y_logits, dim=1).argmax(dim=1)
loss = loss_fn(y_logits, y_blob_train)
acc = accuracy_fn(y_true=y_blob_train, y_pred=y_pred)
optimizer.zero_grad()
loss.backward()
optimizer.step()
model_2.eval()
with torch.inference_mode():
test_logits = model_2(X_blob_test)
test_pred = torch.softmax(test_logits, dim=1).argmax(dim=1)
test_loss = loss_fn(test_logits, y_blob_test)
tess_acc = accuracy_fn(y_true=y_blob_test, y_pred=test_pred)
if epoch % 10 == 0:
print(f"{epoch}/{epochs} | Loss: {loss:.5f}, Acc: {acc:.2f}% | Test Loss: {test_loss:.5f}, Test Acc: {test_acc:.2f}%")
0/100 | Loss: 1.15883, Acc: 40.38% | Test Loss: 1.07554, Test Acc: 99.00%
10/100 | Loss: 0.64476, Acc: 96.75% | Test Loss: 0.66069, Test Acc: 99.00%
20/100 | Loss: 0.42535, Acc: 98.50% | Test Loss: 0.43074, Test Acc: 99.00%
30/100 | Loss: 0.25294, Acc: 99.12% | Test Loss: 0.24508, Test Acc: 99.00%
40/100 | Loss: 0.11232, Acc: 99.25% | Test Loss: 0.10229, Test Acc: 99.00%
50/100 | Loss: 0.06627, Acc: 99.25% | Test Loss: 0.05848, Test Acc: 99.00%
60/100 | Loss: 0.05068, Acc: 99.25% | Test Loss: 0.04293, Test Acc: 99.00%
70/100 | Loss: 0.04300, Acc: 99.25% | Test Loss: 0.03491, Test Acc: 99.00%
80/100 | Loss: 0.03836, Acc: 99.25% | Test Loss: 0.02988, Test Acc: 99.00%
90/100 | Loss: 0.03525, Acc: 99.25% | Test Loss: 0.02663, Test Acc: 99.00%
使用准确率评估:
model_2.eval()
with torch.inference_mode():
y_logits = model_2(X_blob_test)
y_preds = torch.softmax(y_logits, dim=1).argmax(dim=1)
print(f"Test accuracy: {accuracy_fn(y_true=y_blob_test, y_pred=y_preds)}%")
Test accuracy: 99.5%
可视化评估:
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.title("Train")
plot_decision_boundary(model_2, X_blob_train, y_blob_train)
plt.subplot(1, 2, 2)
plt.title("Test")
plot_decision_boundary(model_2, X_blob_test, y_blob_test)
评估指标 | 定义 | 代码 |
---|---|---|
正确率 | 模型预测正确的占比 | torchmetrics.Accuracy() 或 sklearn.metrics.accuracy_score() |
精确率 | $\text{Precision}=\frac {TP}{TP+FP}$ | torchmetrics.Precision() 或 sklearn.metrics.precision_score() |
召回率 | $\text{Recall}=\frac {TP}{TP+FN}$ | torchmetrics.Recall() 或 sklearn.metrics.recall_score() |
F1-Score | 将查准率和查全率合并为一个指标。1 是最好的,0 是最坏的 | torchmetrics.F1Score() 或 sklearn.metrics.f1_score() |
混淆矩阵 | 以表格方式将预测值与真实值进行比较,如果100%正确,矩阵中的所有值将从左上角到右下角(对角线) | torchmetrics.ConfusionMatrix 或 sklearn.metrics.plot_confusion_matrix() |
分类报告 | 一些主要分类指标的集合,如精度,召回率和f1分数 | sklearn.metrics.classification_report() |