上一篇我们把深度学习的预备知识都讲透了,现在终于可以正式开启深度学习之旅啦!就像学开车前先认识油门刹车,预备知识让我们知道了"是什么",现在我们要开始学习"怎么用"。
深度学习的本质,是用神经网络去拟合数据规律。而线性神经网络,就是深度学习最基础、最简单的版本------它就像神经网络的"Hello World",搞懂了它,后面的复杂网络就都是"换汤不换药"。
今天,我们就用最通俗的语言,把线性神经网络讲明白,包括线性回归 (预测数值,比如房价)和softmax回归(分类问题,比如识别图片),还会手把手教你从零写代码和用框架简洁实现。
一、线性回归:预测"多少"的问题
假设你想预测房价,根据房屋面积和房龄来估算价格,这就是一个典型的回归问题------预测一个连续的数值。线性回归,就是用一条直线(或平面、超平面)去拟合这些数据。
1. 线性回归的基本元素:就像"猜价格"的公式
我们先从最简单的例子开始:假设只用房屋面积来预测房价。
-
模型 :房价 = 权重 × 面积 + 偏置
用数学符号写就是: <math xmlns="http://www.w3.org/1998/Math/MathML"> y = w ⋅ x + b y = w \cdot x + b </math>y=w⋅x+b
这里的 <math xmlns="http://www.w3.org/1998/Math/MathML"> w w </math>w(权重)决定了面积每增加1平米,房价涨多少; <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b(偏置)就是当面积为0时的"基础价格"(虽然现实中没有0平米的房子,但偏置能让模型更灵活)。
-
损失函数 :衡量"猜得准不准"
光有模型还不行,我们得知道当前的 <math xmlns="http://www.w3.org/1998/Math/MathML"> w w </math>w 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b 猜得准不准。这时候就需要损失函数(Loss Function),它就像一把尺子,量一下预测值和真实值之间的差距。
回归问题最常用的是平方损失 :
<math xmlns="http://www.w3.org/1998/Math/MathML"> l o s s = 1 2 ( 预测值 − 真实值 ) 2 loss = \frac{1}{2}(预测值 - 真实值)^2 </math>loss=21(预测值−真实值)2为什么要平方?因为这样大的误差会被"放大惩罚",让模型更重视那些猜得特别离谱的情况。前面的 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 2 \frac{1}{2} </math>21 是为了求导时方便,不影响结果。
-
优化算法 :怎么"调整公式"让猜得更准
有了损失函数,我们的目标就是让损失越小越好。怎么调整 <math xmlns="http://www.w3.org/1998/Math/MathML"> w w </math>w 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b 呢?这就要用到梯度下降(Gradient Descent)。
通俗比喻:你在山顶想最快下山,脚下最陡的方向就是梯度,往反方向迈一步,就是梯度下降。每次迈多大步?由学习率(Learning Rate)决定------步子太小走得慢,步子太大可能会"跨过头"。
实际中我们常用小批量随机梯度下降(Mini-batch Stochastic Gradient Descent):不是每次都用所有数据算梯度,而是随机抽一小批(比如32个样本)来算,这样既能保证速度,又能保证准确性。
2. 矢量化加速:告别慢到哭的for循环
在训练模型时,我们经常希望同时处理整个小批量的样本。如果用Python的for循环一个一个算,那速度会慢到让你怀疑人生!
这时候就需要矢量化(Vectorization):利用线性代数库(如NumPy、PyTorch),直接对整个矩阵进行运算,而不是一个个元素去算。
举个简单的对比:
python
import torch
import time
n = 10000
a = torch.ones(n)
b = torch.ones(n)
# 方法1:用for循环
start = time.time()
c = torch.zeros(n)
for i in range(n):
c[i] = a[i] + b[i]
print(f'for循环耗时: {time.time() - start:.4f} 秒')
# 方法2:矢量化(直接相加)
start = time.time()
d = a + b
print(f'矢量化耗时: {time.time() - start:.4f} 秒')
你会发现,矢量化比for循环快几十倍甚至上百倍!这就是为什么深度学习框架都强调矢量化------告别慢到哭的for循环,拥抱闪电般的矩阵运算!
3. 正态分布与平方损失:为什么用平方损失?
你可能会问:为什么回归问题要用平方损失?这背后其实有概率论的支撑。
假设真实的模型是: <math xmlns="http://www.w3.org/1998/Math/MathML"> y = w ⋅ x + b + ϵ y = w \cdot x + b + \epsilon </math>y=w⋅x+b+ϵ,其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> ϵ \epsilon </math>ϵ 是噪声 (测量误差、随机因素等)。我们通常假设噪声服从正态分布 (高斯分布): <math xmlns="http://www.w3.org/1998/Math/MathML"> ϵ ∼ N ( 0 , σ 2 ) \epsilon \sim N(0, \sigma^2) </math>ϵ∼N(0,σ2)。
通俗理解:噪声就像考试时的"粗心分",有时候多扣点,有时候少扣点,但大多数时候在0附近波动,特别大或特别小的情况都很少见------这就是正态分布的特点。
在这个假设下,我们可以用最大似然估计(Maximum Likelihood Estimation)来推导,最终会发现:最大化似然函数等价于最小化平方损失!
所以,用平方损失不是拍脑袋决定的,而是在"噪声服从正态分布"这个合理假设下的数学必然结果。
4. 从线性回归到深度网络:原来神经网络这么简单!
你知道吗?线性回归其实就是一个单层神经网络!
- 输入层:就是我们的特征(比如面积、房龄)
- 输出层:就是预测结果(比如房价)
- 没有隐藏层:这就是它叫"线性"的原因
如果我们在中间加几层(隐藏层),再加上非线性激活函数,就变成了深度神经网络!所以说,线性回归是深度学习的基础,搞懂了它,后面的都好办。
5. 线性回归的从零开始实现:手把手写代码
光说不练假把式,我们来用PyTorch从零实现线性回归,这样你能真正理解每一步在做什么。
步骤1:生成数据集
我们先自己造一个数据集,这样方便验证效果。假设真实的模型是:
<math xmlns="http://www.w3.org/1998/Math/MathML"> y = 2 ⋅ x 1 − 3.4 ⋅ x 2 + 4.2 + 噪声 y = 2 \cdot x_1 - 3.4 \cdot x_2 + 4.2 + 噪声 </math>y=2⋅x1−3.4⋅x2+4.2+噪声
python
import torch
import random
def synthetic_data(w, b, num_examples):
"""生成 y = Xw + b + 噪声"""
X = torch.normal(0, 1, (num_examples, len(w))) # 生成均值0、方差1的随机数
y = torch.matmul(X, w) + b # 矩阵乘法
y += torch.normal(0, 0.01, 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, 1000)
步骤2:读取数据集
训练时我们需要一批一批地读取数据,所以写一个函数来生成小批量:
python
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
random.shuffle(indices) # 打乱样本顺序
for i in range(0, num_examples, batch_size):
batch_indices = torch.tensor(
indices[i: min(i + batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices]
步骤3:初始化模型参数
我们先随机初始化 <math xmlns="http://www.w3.org/1998/Math/MathML"> w w </math>w 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b,然后让模型自己去学习:
python
w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
requires_grad=True 表示我们要计算这些参数的梯度,这样才能用梯度下降更新它们。
步骤4:定义模型
就是那个简单的线性公式:
python
def linreg(X, w, b):
"""线性回归模型"""
return torch.matmul(X, w) + b
步骤5:定义损失函数
平方损失函数:
python
def squared_loss(y_hat, y):
"""平方损失"""
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
步骤6:定义优化算法
小批量随机梯度下降:
python
def sgd(params, lr, batch_size):
"""小批量随机梯度下降"""
with torch.no_grad(): # 这部分计算不需要梯度
for param in params:
param -= lr * param.grad / batch_size # 更新参数
param.grad.zero_() # 梯度清零,下次重新算
步骤7:训练
万事俱备,开始训练!
python
lr = 0.03 # 学习率
num_epochs = 3 # 训练轮数
net = linreg
loss = squared_loss
batch_size = 10
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
l = loss(net(X, w, b), y) # 计算损失
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 + 1}, loss {float(train_l.mean()):f}')
# 看看学出来的参数和真实参数差多少
print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')
你会发现,训练几轮后,损失变得很小,学出来的参数也和真实参数几乎一样!
6. 线性回归的简洁实现:用框架偷懒
从零写是为了理解原理,实际工作中我们都是用框架的高级API,几行代码就能搞定!
python
import numpy as np
from torch.utils import data
from torch import nn
# 注意:这里需要用到前面定义的synthetic_data函数,
# 如果单独运行这个代码块,需要先定义synthetic_data函数。
# 1. 生成数据集(和之前一样)
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
# 2. 读取数据集(用PyTorch的DataLoader)
def load_array(data_arrays, batch_size, is_train=True):
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)
# 3. 定义模型(nn.Sequential就像"积木盒子",把层放进去)
net = nn.Sequential(nn.Linear(2, 1))
# 4. 初始化参数
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
# 5. 定义损失函数(MSE就是均方误差)
loss = nn.MSELoss()
# 6. 定义优化算法(SGD)
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
# 7. 训练
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}')
看!简洁实现是不是清爽多了?框架帮我们把底层的细节都封装好了,我们只需要关注"搭积木"就行。
二、softmax回归:解决"哪一个"的问题
刚才讲的线性回归是预测数值(比如房价多少),那如果是分类问题呢?比如判断一张图片是猫还是狗,这时候就需要softmax回归。
1. 分类问题:从"预测数值"到"预测类别"
我们拿图像分类来举例,比如Fashion-MNIST数据集,它包含10类服饰:T恤、裤子、套衫、连衣裙、外套、凉鞋、衬衫、运动鞋、包、短靴。
-
独热编码 :怎么表示类别?
如果我们用1表示T恤、2表示裤子......这样不太好,因为类别之间没有"大小关系"(T恤≠1,裤子≠2,它们是平等的)。所以我们用独热编码(One-Hot Encoding):每个类别对应一个向量,只有对应位置是1,其他都是0。比如:
- T恤:[1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
- 裤子:[0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
-
softmax运算 :把输出变成概率
线性回归的输出是一个数值,而softmax回归的输出是多个数值(每个类别一个),我们希望这些输出能表示"属于每个类别的概率"。
这时候就需要softmax函数 ,它能把任意一组数值转换成"非负、总和为1"的概率分布:
<math xmlns="http://www.w3.org/1998/Math/MathML"> y ^ j = exp ( o j ) ∑ k exp ( o k ) \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)} </math>y^j=∑kexp(ok)exp(oj)通俗说就是:先把每个输出取指数(保证非负),然后除以总和(保证和为1),这样就得到了每个类别的概率!
-
交叉熵损失 :分类问题的"尺子"
分类问题不用平方损失,而是用交叉熵损失(Cross-Entropy Loss)。通俗理解:如果模型预测"是猫的概率是0.9",而真实标签确实是猫,那损失就很小;如果真实标签是狗,那损失就很大。
交叉熵的公式是:
<math xmlns="http://www.w3.org/1998/Math/MathML"> l ( y , y ^ ) = − ∑ j y j log y ^ j l(y, \hat{y}) = -\sum_j y_j \log \hat{y}_j </math>l(y,y^)=−∑jyjlogy^j
因为 <math xmlns="http://www.w3.org/1998/Math/MathML"> y y </math>y 是独热编码,只有一个位置是1,所以其实就是 <math xmlns="http://www.w3.org/1998/Math/MathML"> − log y ^ 正确类别 -\log \hat{y}_{正确类别} </math>−logy^正确类别------预测正确类别的概率越大,损失越小!
2. softmax回归的网络架构:还是单层神经网络!
和线性回归一样,softmax回归也是一个单层神经网络:
- 输入层:图片的像素(比如28×28的图片,展平成784维向量)
- 输出层:10个神经元(对应10个类别)
- 没有隐藏层
每个输出神经元对应一个类别的"未规范化预测",然后通过softmax变成概率。
3. 全连接层的参数开销:参数会很多吗?
正如你所见,全连接层无处不在。但顾名思义,全连接层是"完全"连接的,可能有很多可学习的参数。
具体来说,对于任何具有 d个输入 和 q个输出 的全连接层:
- 参数数量 = 输入 × 输出 + 偏置 = <math xmlns="http://www.w3.org/1998/Math/MathML"> d × q + q d \times q + q </math>d×q+q
举个例子,Fashion-MNIST有784个输入(28×28像素)和10个输出(10个类别):
- 参数数量 = 784 × 10 + 10 = 7850个参数
这个数量其实还不算多,但如果是更深的网络,比如输入1000维、输出1000维的全连接层:
- 参数数量 = 1000 × 1000 + 1000 = 1,001,000个参数!
这就是为什么后面我们会学到卷积神经网络(CNN)------它能大幅减少参数数量!
4. softmax运算与小批量样本的矢量化
(1)softmax运算(已讲过,简单回顾)
softmax函数能把任意一组数值转换成"非负、总和为1"的概率分布:
<math xmlns="http://www.w3.org/1998/Math/MathML"> y ^ j = exp ( o j ) ∑ k exp ( o k ) \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)} </math>y^j=∑kexp(ok)exp(oj)
(2)小批量样本的矢量化:一次算一批,更快!
为了提高计算效率并且充分利用GPU,我们通常会对小批量样本的数据执行矢量化计算,而不是一个样本一个样本地算。
假设我们有:
- 批量大小:n(比如256个样本)
- 输入维度:d(784维)
- 输出类别:q(10个类别)
那么小批量样本的矢量化计算可以表示为:
- <math xmlns="http://www.w3.org/1998/Math/MathML"> O = X W + b O = XW + b </math>O=XW+b (未规范化预测)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> Y ^ = softmax ( O ) \hat{Y} = \text{softmax}(O) </math>Y^=softmax(O) (概率)
其中:
- <math xmlns="http://www.w3.org/1998/Math/MathML"> X X </math>X 是 <math xmlns="http://www.w3.org/1998/Math/MathML"> n × d n \times d </math>n×d 的矩阵(每一行是一个样本)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> W W </math>W 是 <math xmlns="http://www.w3.org/1998/Math/MathML"> d × q d \times q </math>d×q 的矩阵(权重)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b 是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 × q 1 \times q </math>1×q 的向量(偏置,通过广播机制自动扩展)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> O O </math>O 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> Y ^ \hat{Y} </math>Y^ 都是 <math xmlns="http://www.w3.org/1998/Math/MathML"> n × q n \times q </math>n×q 的矩阵
一句话总结:小批量矢量化 = 一次算一批,充分利用GPU,快到飞起!
5. 信息论基础:交叉熵到底是什么?
你可能会问:交叉熵到底是什么?这要从信息论说起。
信息论的核心思想是量化数据中的信息内容。
(1)熵(Entropy):衡量"不确定性"
假设我们有一个概率分布P,熵H[P]衡量的是这个分布的"不确定性"或"惊奇程度":
<math xmlns="http://www.w3.org/1998/Math/MathML"> H [ P ] = ∑ j − P ( j ) log P ( j ) H[P] = \sum_j -P(j) \log P(j) </math>H[P]=∑j−P(j)logP(j)
通俗理解:
- 如果某个事件一定会发生(概率1),熵就是0------没有任何惊奇
- 如果事件的概率分布越均匀(比如10个类别每个概率都是0.1),熵就越大------不确定性越高
(2)信息量:某个事件带来的"惊奇程度"
当我们观察到一个概率为P(j)的事件j时,它带来的信息量是:
<math xmlns="http://www.w3.org/1998/Math/MathML"> − log P ( j ) -\log P(j) </math>−logP(j)
通俗理解:
- 概率越小的事件发生了,带来的信息量越大(比如"太阳从西边出来")
- 概率越大的事件发生了,带来的信息量越小(比如"太阳从东边出来")
(3)重新审视交叉熵
交叉熵H(P, Q)表示:用概率分布Q来编码真实分布P的数据,平均需要多少比特(或纳特)。
通俗理解:
- P是真实分布(比如标签),Q是模型预测的分布
- 交叉熵越小,说明Q和P越接近
这就是为什么我们用交叉熵作为损失函数!
6. 图像分类数据集:Fashion-MNIST
在实战之前,我们先认识一下Fashion-MNIST数据集,它是MNIST的"升级版"(MNIST太简单了,都是手写数字)。
python
import torchvision
from torchvision import transforms
from d2l import torch as d2l
# 读取数据集
trans = transforms.ToTensor() # 把图片转成张量,并归一化到0-1之间
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)
# 看看数据集大小
print(f'训练集大小: {len(mnist_train)}, 测试集大小: {len(mnist_test)}')
print(f'图片形状: {mnist_train[0][0].shape}') # 1个通道,28×28像素
# 定义函数把数字标签转成文字
def get_fashion_mnist_labels(labels):
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels]
# 看看前几个图片
batch_size = 18
X, y = next(iter(data.DataLoader(mnist_train, batch_size=batch_size)))
d2l.show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y))
整合所有组件:读取小批量数据
现在我们把读取数据集、读取小批量等功能整合在一起:
python
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=4),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=4))
这样,我们就可以用一个函数搞定数据加载了!
7. softmax回归的从零开始实现
我们继续从零开始,这样能理解得更透彻。
python
# 1. 初始化参数
num_inputs = 784 # 28×28
num_outputs = 10 # 10个类别
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)
# 2. 定义softmax操作
def softmax(X):
X_exp = torch.exp(X)
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 广播机制
# 3. 定义模型
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
# 4. 定义损失函数(交叉熵)
def cross_entropy(y_hat, y):
return -torch.log(y_hat[range(len(y_hat)), y])
# 5. 分类精度:计算预测正确的比例
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())
# 评估精度函数
def evaluate_accuracy(net, data_iter):
"""计算在指定数据集上模型的精度"""
metric = Accumulator(2) # 正确预测数、预测总数
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
# 6. 训练
lr = 0.1
num_epochs = 10
batch_size = 256
# 读取数据
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
# 训练函数
def train_epoch(net, train_iter, loss, updater):
metric = Accumulator(3) # 这里需要3个位置:损失总和、正确预测数、样本总数
for X, y in train_iter:
y_hat = net(X)
l = loss(y_hat, y)
if isinstance(updater, torch.optim.Optimizer):
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]
# 辅助类:累加器
class Accumulator:
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]
# 重新定义sgd函数(适配softmax回归)
def sgd(params, lr, batch_size):
"""小批量随机梯度下降"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
# 用我们的sgd函数
def updater(batch_size):
return sgd([W, b], lr, batch_size)
# 开始训练
for epoch in range(num_epochs):
train_metrics = train_epoch(net, train_iter, cross_entropy, updater)
test_acc = evaluate_accuracy(net, test_iter)
print(f'epoch {epoch + 1}, 训练损失: {train_metrics[0]:.3f}, 训练精度: {train_metrics[1]:.3f}, 测试精度: {test_acc:.3f}')
预测:用训练好的模型识别图片!
训练好模型后,我们就可以用它来预测新图片了!
python
def predict(net, test_iter, n=6):
"""预测标签"""
for X, y in test_iter:
break
trues = get_fashion_mnist_labels(y)
preds = get_fashion_mnist_labels(net(X).argmax(axis=1))
titles = [true + '\n' + pred for true, pred in zip(trues, preds)]
d2l.show_images(
X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])
# 试试预测!
predict(net, test_iter)
你会看到模型的预测结果------如果训练得好,大部分预测都是正确的!
8. softmax回归的简洁实现:框架大法好!
当然,实际中我们还是用框架的高级API,更简洁、更高效!
python
# 1. 读取数据
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
# 2. 定义模型
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
# 3. 初始化参数
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights)
# 4. 重新审视Softmax的实现:数值稳定性问题!
你知道吗?直接实现softmax可能会有**数值稳定性问题**!
假设某个未规范化预测o_j很大(比如1000),那么exp(o_j)就会是exp(1000)------这个数字太大了,计算机根本存不下,会导致**溢出**(Overflow)!
怎么解决呢?**技巧是:先减去最大值!**
softmax的巧妙性质:$\text{softmax}(o) = \text{softmax}(o - \max(o))$
也就是说,我们可以先把所有o_j减去最大值,这样exp后的数值就不会太大了!
# 5. 损失函数:softmax和交叉熵合并在一起了!
loss = nn.CrossEntropyLoss(reduction='none')
PyTorch的CrossEntropyLoss已经帮我们解决了数值稳定性问题------它把softmax和交叉熵合并在一起计算,更高效、更稳定!
# 6. 优化算法
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
# 7. 训练
num_epochs = 10
for epoch in range(num_epochs):
train_metrics = train_epoch(net, train_iter, loss, trainer)
test_acc = evaluate_accuracy(net, test_iter)
print(f'epoch {epoch + 1}, 训练损失: {train_metrics[0]:.3f}, 训练精度: {train_metrics[1]:.3f}, 测试精度: {test_acc:.3f}')
你看,简洁实现又少了好多代码!而且框架还帮我们解决了softmax数值稳定性的问题(防止指数溢出)。
三、小结:线性神经网络的核心要点
今天我们讲了线性神经网络,其实就是两个东西:线性回归 (预测数值)和softmax回归(分类)。
核心知识回顾:
-
线性回归:
- 模型: <math xmlns="http://www.w3.org/1998/Math/MathML"> y = w ⋅ x + b y = w \cdot x + b </math>y=w⋅x+b(或矩阵形式 <math xmlns="http://www.w3.org/1998/Math/MathML"> y = X w + b y = Xw + b </math>y=Xw+b)
- 损失函数:平方损失
- 优化算法:小批量随机梯度下降
- 本质:单层神经网络
-
softmax回归:
- 模型:先线性变换,再softmax成概率
- 损失函数:交叉熵损失
- 适用场景:分类问题
- 本质:单层神经网络(多个输出)
-
两种实现方式:
- 从零实现:理解原理,知道每一步在做什么
- 简洁实现:用框架高级API,高效开发
小白须知:
- 不需要背公式,理解概念就行
- 报错是常态,学会看错误信息
- 先跑通代码,再慢慢理解细节
- 线性神经网络是基础,搞懂了它,后面的深度网络就好学了
写在最后
线性神经网络虽然简单,但它包含了深度学习的核心思想:定义模型、损失函数、优化算法,然后训练。后面的深度神经网络,不管多复杂,都是这个套路------只是模型更复杂(加了隐藏层和激活函数),仅此而已。
下一篇,我们将在这个基础上,加上隐藏层,变成多层感知机(MLP),也就是真正的"深度"神经网络啦!敬请期待~
如果有疑问,欢迎留言交流,一起避开入门坑,稳步进阶~
(注:文档部分内容参考《动手学深度学习》)