Caiwen的博客

深度学习-线性神经网络

2025-07-06 08:19

1 线性回归

1.1 问题定义

给定 nn 维输入 x=[x1,x2,,xn]T\mathbf{x}=\begin{bmatrix} x_1,x_2,\dots,x_n \end{bmatrix}^T

需要确定一个 nn 维权重 w=[w1,w2,,wn]T\mathbf{w}=\begin{bmatrix} w_1,w_2,\dots,w_n \end{bmatrix}^T 和一个标量偏差 bb

输出一个预测值 y=w1x1+w2x2++wnxn+b=w,x+by=w_1x_1+w_2x_2+\dots+w_nx_n+b=\left \langle \mathbf{w},\mathbf{x} \right \rangle + b

平方损失:(y,y^)=12(yy^)2\ell (y,\hat{y})=\frac{1}{2}(y-\hat{y})^2,其中 yy 是真实值,y^\hat{y} 是估计值

然后我们有训练数据:

  • X=[x1,x2,,xn]T\mathbf{X}=\begin{bmatrix} \mathbf{x}_1,\mathbf{x}_2,\dots,\mathbf{x}_n \end{bmatrix}^T,表示样本数据的输入,xi\mathbf{x}_i 是一个列向量。合成一个矩阵后转置之后,矩阵的每一行就是一个样本数据
  • y=[y1,y2,,yn]\mathbf{y}=\begin{bmatrix} y_1,y_2,\dots,y_n \end{bmatrix} 为样本的输出数据

有损失函数

(X,y,w,b)=12ni=1n(yixi,wb)2=12nyXwb2\ell (\mathbf{X},\mathbf{y},\mathbf{w},b)=\frac{1}{2n}\sum_{i=1}^n(y_i-\left \langle \mathbf{x}_i,\mathbf{w} \right \rangle-b)^2=\frac{1}{2n}||\mathbf{y}-\mathbf{Xw}-b||^2

可以把 bb 给合并进 Xw\mathbf{Xw} 中,方法是给 X\mathbf{X} 附加一列元素全为 bb 的列向量,然后给行向量 w\mathbf{w} 的最后加一个元素 11。然后就可以合并成:

(X,y,w)=12nyXw2\ell (\mathbf{X},\mathbf{y},\mathbf{w})=\frac{1}{2n}||\mathbf{y}-\mathbf{Xw}||^2

然后选取最合适的 w\mathbf{w}bb 来使损失函数最小

1.2 梯度下降

挑选一个初始值 w0\mathbf{w}_0,然后重复迭代参数 ttwt=wt1ηwt1\mathbf{w}_t=\mathbf{w}_{t-1}-\eta\frac{\partial \ell}{\partial \mathbf{w}_{t-1}}。其中 η\eta 为学习率,是一个需要认为设置的参数,不能太大也不能太小。

在整个训练集上计算梯度耗时很长,我们可以随机采样 bb 个样本 i1,i2,,ibi_1,i_2,\dots,i_b 来近似损失:

1biIb(xi,yi,w)\frac{1}{b}\sum_{i\in I_b} \ell(\mathbf{x}_i,y_i,\mathbf{w})

python
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
import random import torch from matplotlib import pyplot as plt from matplotlib_inline import backend_inline # 生成噪声数据 def synthetic_data(w, b, count): X = torch.normal(0, 1, (count, len(w))) y = torch.matmul(X, w) + b y += torch.normal(0, 0.1, y.shape) # 加噪声 return X, y.reshape((-1, 1)) true_w = torch.tensor([2, -3.4]) true_b = 4.2 features, labels = synthetic_data(true_w, true_b, 10000) # plt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1) # plt.show() # 随机取样 def data_iter(batch_size, features, labels): count = len(features) indices = list(range(count)) random.shuffle(indices) for i in range(0, count, batch_size): batch_indices = torch.tensor(indices[i: min(i + batch_size, count)]) yield features[batch_indices], labels[batch_indices] batch_size = 10 # 设置初始预测值 w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True) b = torch.zeros(1, requires_grad=True) # 获取预测值的矩阵 def linreg(X, w, b): return torch.matmul(X, w) + b # 损失函数 def squared_loss(y_hat, y): return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2 # 梯度下降 def sgd(params, lr, batch_size): with torch.no_grad(): # 在这区域中进行的运算不会被算入梯度 for param in params: param -= lr * param.grad / batch_size param.grad.zero_() # 训练 lr = 0.03 num_epochs = 10 # 训练次数 net = linreg # 选用神经网络 loss = squared_loss # 选用损失函数 for epoch in range(num_epochs): for X, y in data_iter(batch_size, features, labels): # 使用 linreg 进行预测,然后 squared_loss 来计算损失 l = loss(net(X, w, b), y) # 目前得到的 l 是一个 (batch_size, 1) 形状的向量,使用 sum 将其转为标量,然后就可以求梯度 l.sum().backward() sgd([w, b], lr, batch_size) # 进行梯度下降 # 输出统计数据 with torch.no_grad(): train_l = loss(net(features, w, b), labels) print(f'epoch: {epoch}, loss: {float(train_l.mean()):f}') print(f'w_hat: {w}') print(f'b_hat: {b}') print(f'delta w: {true_w - w.reshape(true_w.shape)}') print(f'delta b: {true_b - b}')

1.3 简便写法

使用 pytorch 定义好的工具。先生成数据集:

python
1
2
3
true_w = torch.tensor([2, -3.4]) true_b = 4.2 features, labels = synthetic_data(true_w, true_b, 10000)

使用框架自带的随机采样读取器:

python
1
2
3
4
5
6
7
def load_array(data_arrays, batch_size, is_train=True): """构造一个PyTorch数据迭代器""" dataset = data.TensorDataset(*data_arrays) return data.DataLoader(dataset, batch_size, shuffle=is_train) batch_size = 10 data_iter = load_array((features, labels), batch_size)

然后定义神经网络,其中 nn.Sequential 把神经网络中的每一层连接,nn.Linear(2, 1) 相当于是一个全连接层,表示有 22 个因素影响 11 个输出结果(相当于是 2×12\times 1 的矩阵)

python
1
2
3
4
5
6
from torch import nn net = nn.Sequential(nn.Linear(2, 1)) # 设置初始权重和偏置 net[0].weight.data.normal_(0, 0.01) net[0].bias.data.fill_(0)

计算均方误差使用的是 MMSELoss

python
1
loss = nn.MSELoss()

然后设置优化算法,即梯度下降

python
1
trainer = torch.optim.SGD(net.parameters(), lr=0.03)

然后是训练过程:

python
1
2
3
4
5
6
7
8
9
num_epochs = 3 for epoch in range(num_epochs): for X, y in data_iter: l = loss(net(X) ,y) trainer.zero_grad() l.backward() trainer.step() l = loss(net(features), labels) print(f'epoch {epoch + 1}, loss {l:f}')

2 损失函数

2.1 L2 Loss

l(y,y)=12(yy)2l(y,y')=\frac{1}{2}(y-y')^2

优点:离准确值比较远的时候梯度会很大,下降更快

缺点:有时候可能不希望下降特别快

2.2 L1 Loss

l(y,y)=yyl(y,y')=|y-y'|

优点:永远能以一个固定的速率下降

缺点:原点处不可导,且预测值和准确值比较靠近的时候会发生剧烈抖动

2.3 Huber's Robust Loss

l(y,y)={yy12 if yy>112(yy)2 otherwise l(y,y')= \begin{cases} |y-y'|-\frac{1}{2} & \text{ if } |y-y'|>1 \\ \frac{1}{2}(y-y')^2 & \text{ otherwise } \end{cases}

结合上面两个损失函数,损失较大的时候以恒定速度下降,损失较小的时候下降速度比较平滑

3 Softmax 回归

3.1 问题定义

由多个因素和多个权重,线性确定出多个类的比例 oio_i

我们的预测概率为 y^=sofmax(o)\mathbf{\hat{y}}=\text{sofmax}(\mathbf{o})

其中 y^i=exp(oi)kexp(ok)\hat{y}_i=\frac{\text{exp}(o_i)}{\sum_k\text{exp}(o_k)},也就是把多个类比例用自然指数函数转为一个非负数之后再分百分比

而精确值 y=[y1,y2,,yn]T\mathbf{y}=\begin{bmatrix} y_1,y_2,\dots,y_n \end{bmatrix}^T

其中:

yi={1 if i=y0otherwisey_i=\begin{cases} 1 &\text{ if } i=y\\ 0 &\text{otherwise} \end{cases}

也就是精确值只有正确分类的权重为 11,其他都为 00

我们一般使用交叉熵来衡量两个概率的区别 H(p,q)=ipilog(qi)H(\mathbf{p},\mathbf{q})=\sum_i-p_i\log(q_i)

那么损失函数:

l(y,y^)=iyilogyi^=logy^yl(\mathbf{y},\mathbf{\hat{y}})=-\sum_iy_i\log\hat{y_i}=-\log\hat{y}_y

3.2 梯度下降

准备训练数据集:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def get_dataloader_workers(): """使用4个进程来读取数据""" return 4 def load_data_fashion_mnist(batch_size, resize=None): """下载Fashion-MNIST数据集,然后将其加载到内存中""" trans = [transforms.ToTensor()] if resize: trans.insert(0, transforms.Resize(resize)) trans = transforms.Compose(trans) mnist_train = torchvision.datasets.FashionMNIST( root="../data", train=True, transform=trans, download=True) mnist_test = torchvision.datasets.FashionMNIST( root="../data", train=False, transform=trans, download=True) return (data.DataLoader(mnist_train, batch_size, shuffle=True, num_workers=get_dataloader_workers()), data.DataLoader(mnist_test, batch_size, shuffle=False, num_workers=get_dataloader_workers()))

设置迭代器

python
1
2
batch_size = 256 train_iter, test_iter = load_data_fashion_mnist(32, resize=64)

定义权重和偏置:

python
1
2
3
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True) b = torch.zeros(num_outputs, requires_grad=True)

其中 W\mathbf{W}784×10784\times 10 的矩阵。我们这里图片像素为 28×2828\times 28,由于图片颜色只有一个通道,所以每个像素的亮度就相当于是一个决定图片类型的因子,共有 784784 个因子,然后每个因子由分别决定 1010 个类别

然后有 softmax 函数,其传入一个矩阵。函数将对每个项求幂,然后对每一行进行求和,得到每一行的规范化常数,然后每一行除以规范化常数,使得每一行的加和为 11

python
1
2
3
4
def softmax(X): X_exp = torch.exp(X) partition = X_exp.sum(1, keepdim=True) return X_exp / partition # 这里应用了广播机制

然后定义神经网络,其中 X\mathbf{X} 是样本数据,这里 reshape 以确保样本数据中每一行都是一个样本(行数设为 -1 以自动推导 batch_size),每一列都是该样本的像素信息(把二维像素信息拍平成一维)

python
1
2
def net(X): return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)

然后样本数据与我们当前预测的权重矩阵相乘,就得到了每个样本对于每个分类的比例,然后再通过 softmax 将比例规范化,同时加上偏置

通过神经网络就能得到我们的 y_hat

然后我们定义交叉熵损失函数:

python
1
2
def cross_entropy(y_hat, y): return - torch.log(y_hat[range(len(y_hat)), y])

我们这里的 y 是一个向量,yiy_i 表示第 ii 个样本属于哪类。y_hat[range(len(y_hat)), y] 中,range(len(y_hat)) 指明了我们要取 y_hat 的所有行(len 取的是行数),y 是一个 list,指明了每行取哪一列,算是 python 的语法糖。然后按照交叉熵损失的定义来求

为了统计精度数据,我们还需要 accuracy 函数来计算我们这个预测结果在这个样本数据上正确了多少个:

python
1
2
3
4
5
6
def accuracy(y_hat, y): """计算预测正确的数量""" if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: y_hat = y_hat.argmax(axis=1) # 列上找最大的概率,即为我们最终确定的结果 cmp = y_hat.type(y.dtype) == y return float(cmp.type(y.dtype).sum())

accuracy(y_hat, y) / len(y) 即为正确率

然后定义实用类 Accumulator 作为累加器:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
class Accumulator: """在n个变量上累加""" def __init__(self, n): self.data = [0.0] * n def add(self, *args): self.data = [a + float(b) for a, b in zip(self.data, args)] def reset(self): self.data = [0.0] * len(self.data) def __getitem__(self, idx): return self.data[idx]

定义 evaluate_acccuracy 来计算在整个数据集上的精确度:

python
1
2
3
4
5
6
7
8
9
def evaluate_accuracy(net, data_iter): """计算在指定数据集上模型的精度""" if isinstance(net, torch.nn.Module): net.eval() # 将模型设置为评估模式 metric = Accumulator(2) # 正确预测数、预测总数 with torch.no_grad(): for X, y in data_iter: metric.add(accuracy(net(X), y), y.numel()) return metric[0] / metric[1]

定义一个 Animator,以便后续动态观察我们的训练情况

python
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
from matplotlib_inline import backend_inline def use_svg_display(): """Use the svg format to display a plot in Jupyter. Defined in :numref:`sec_calculus`""" backend_inline.set_matplotlib_formats('svg') class Animator: """在动画中绘制数据""" def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1, figsize=(3.5, 2.5)): # 增量地绘制多条线 if legend is None: legend = [] use_svg_display() self.fig, self.axes = plt.subplots(nrows, ncols, figsize=figsize) if nrows * ncols == 1: self.axes = [self.axes, ] # 使用lambda函数捕获参数 self.config_axes = lambda: d2l.set_axes( self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend) self.X, self.Y, self.fmts = None, None, fmts def add(self, x, y): # 向图表中添加多个数据点 if not hasattr(y, "__len__"): y = [y] n = len(y) if not hasattr(x, "__len__"): x = [x] * n if not self.X: self.X = [[] for _ in range(n)] if not self.Y: self.Y = [[] for _ in range(n)] for i, (a, b) in enumerate(zip(x, y)): if a is not None and b is not None: self.X[i].append(a) self.Y[i].append(b) self.axes[0].cla() for x, y, fmt in zip(self.X, self.Y, self.fmts): self.axes[0].plot(x, y, fmt) self.config_axes() display.display(self.fig) display.clear_output(wait=True)

定义训练函数 train_epoch

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def train_epoch(net, train_iter, loss, updater): """训练模型一个迭代周期(定义见第3章)""" # 将模型设置为训练模式 if isinstance(net, torch.nn.Module): net.train() # 训练损失总和、训练准确度总和、样本数 metric = Accumulator(3) for X, y in train_iter: # 计算梯度并更新参数 y_hat = net(X) l = loss(y_hat, y) if isinstance(updater, torch.optim.Optimizer): # 使用PyTorch内置的优化器和损失函数 updater.zero_grad() l.mean().backward() updater.step() else: # 使用定制的优化器和损失函数 l.sum().backward() updater(X.shape[0]) metric.add(float(l.sum()), accuracy(y_hat, y), y.numel()) # 返回训练损失和训练精度 return metric[0] / metric[2], metric[1] / metric[2]

定义 updater

python
1
2
def updater(batch_size): return sgd([W, b], lr, batch_size)

总训练函数 train

python
1
2
3
4
5
6
7
8
9
10
11
12
def train(net, train_iter, test_iter, loss, num_epochs, updater): """训练模型(定义见第3章)""" animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9], legend=['train loss', 'train acc', 'test acc']) for epoch in range(num_epochs): train_metrics = train_epoch(net, train_iter, loss, updater) test_acc = evaluate_accuracy(net, test_iter) animator.add(epoch + 1, train_metrics + (test_acc,)) train_loss, train_acc = train_metrics assert train_loss < 0.5, train_loss assert train_acc <= 1 and train_acc > 0.7, train_acc assert test_acc <= 1 and test_acc > 0.7, test_acc

设置学习率和训练次数后即可训练:

python
1
2
3
lr = 0.1 num_epochs = 10 train(net, train_iter, test_iter, cross_entropy, num_epochs, updater)

3.3 简便写法

一样的准备数据集

python
1
2
batch_size = 256 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

然后定义神经网络:

python
1
2
3
4
5
6
7
8
9
10
# PyTorch不会隐式地调整输入的形状。因此, # 我们在线性层前定义了展平层(flatten),来调整网络输入的形状 # nn.Flatten 将会保留第一维度,然后剩余的维度全部展平 net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10)) def init_weights(m): if type(m) == nn.Linear: nn.init.normal_(m.weight, std=0.01) net.apply(init_weights);

然后定义 loss

python
1
loss = nn.CrossEntropyLoss(reduction='none')

设置优化算法

python
1
trainer = torch.optim.SGD(net.parameters(), lr=0.1)

然后训练:

python
1
2
num_epochs = 10 train(net, train_iter, test_iter, loss, num_epochs, trainer)
最后更新于:2025-07-17 09:23

Caiwen
本文作者
一只蒟蒻,爱好编程和算法