线性神经网络:深度学习的“第一堂课”

上一篇我们把深度学习的预备知识都讲透了,现在终于可以正式开启深度学习之旅啦!就像学开车前先认识油门刹车,预备知识让我们知道了"是什么",现在我们要开始学习"怎么用"。

深度学习的本质,是用神经网络去拟合数据规律。而线性神经网络,就是深度学习最基础、最简单的版本------它就像神经网络的"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回归(分类)。

核心知识回顾:

  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"> y = X w + b y = Xw + b </math>y=Xw+b)
    • 损失函数:平方损失
    • 优化算法:小批量随机梯度下降
    • 本质:单层神经网络
  2. softmax回归

    • 模型:先线性变换,再softmax成概率
    • 损失函数:交叉熵损失
    • 适用场景:分类问题
    • 本质:单层神经网络(多个输出)
  3. 两种实现方式

    • 从零实现:理解原理,知道每一步在做什么
    • 简洁实现:用框架高级API,高效开发

小白须知:

  • 不需要背公式,理解概念就行
  • 报错是常态,学会看错误信息
  • 先跑通代码,再慢慢理解细节
  • 线性神经网络是基础,搞懂了它,后面的深度网络就好学了

写在最后

线性神经网络虽然简单,但它包含了深度学习的核心思想:定义模型、损失函数、优化算法,然后训练。后面的深度神经网络,不管多复杂,都是这个套路------只是模型更复杂(加了隐藏层和激活函数),仅此而已。

下一篇,我们将在这个基础上,加上隐藏层,变成多层感知机(MLP),也就是真正的"深度"神经网络啦!敬请期待~

如果有疑问,欢迎留言交流,一起避开入门坑,稳步进阶~

(注:文档部分内容参考《动手学深度学习》)

相关推荐
人工智能培训15 小时前
探析数字孪生的核心特性与应用价值
人工智能·深度学习·神经网络·机器学习·生成对抗网络
ftpeak16 小时前
TorchEasyRec:阿里巴巴开源的推荐系统深度学习框架详解
人工智能·深度学习·ai·开源·ai编程·ai开发
Yunzenn16 小时前
深度分析字节最新研究cola-DLM第 06 章:分块因果 DiT 先验 —— 在隐空间里做 Flow Matching
人工智能·rnn·深度学习·神经网络·生成对抗网络·架构·transformer
Rocky Ding*16 小时前
深入浅出讲解ERNIE-Image图像创作大模型
论文阅读·人工智能·深度学习·机器学习·ai作画·aigc·ai-native
xier_ran16 小时前
【infra之路】Transformer 核心计算流
人工智能·深度学习·transformer
rayyy917 小时前
神经网络模型的外推性验证
pytorch·python·深度学习
MediaTea18 小时前
DL:扩散模型的基本原理与 PyTorch 实现
人工智能·pytorch·python·深度学习·机器学习
机汇五金_18 小时前
深圳电力设备插箱厂家
深度学习
EnCi Zheng19 小时前
09aa-偏置是什么?
人工智能·pytorch·python·深度学习·神经网络