深度学习 | Deep Learning

深度学习 | Deep Learning

前言 | Preface

深度学习,是现行的人工智能 Artificial Intelligence最最最最最重要的部分

其主要思想是模仿生物的神经元,然后用函数嵌套来拟合任意函数

标有上标 “[number]” 将在本节的拓展里面有讲解

图片使用的是 动手学机器学习的神经网络与多层感知机 部分,也有自己画的

为了阅读体验,本博客的实现部分与理论是分开的

张量 | Tensor

最基础的内容,所以篇幅会比较少


张量 Tensor 是对于矩阵 Matrix向量 Vector标量 Scalar概念的推广,本质上就是高维数组

在深度学习中,我们使用张量,是因为其表达能力更强,并且 PyTorch 库中的 tensor 实现中是可以自动微分 Automatic Differentination的(利用计算图机制)

因为在实际的张量计算中,会存下这个结果的来源,这就方便了反向传播 Backpropagat的过程

张量用 \(\text X\)/X 表示,区别于 \(\mathbf X\) (矩阵)

人工神经网络 | Artificial Neural Network

1945年开始,由沃伦 · 麦卡洛克 和沃尔特 · 皮茨等一众研究者设计

但是其没有更新参数的功能,需要人工填上去(名字有可能这么来的 doge.)

可以简单表示为 \(\mathbb I(\sum\limits_{i=1}^{N}w_ix_i \ge 0) = \mathbb{I}(\boldsymbol w^T\boldsymbol x \ge 0)\),而这个 \(\boldsymbol w\) 是需要手填的

感知机 | Perceptron

1958 年,由弗兰克 · 罗森布拉特(Frank Rosenblat)提出

相较于 ANN 多出了:

  • 偏置 Bias 将模型由原本的经过原点的线性函数调整为任意一个超平面,增强了模型的表达能力
  • 激活函数 Activation Function 模拟神经元的兴奋抑制状态

则模型可以表示为:

\[\hat y = \phi(\boldsymbol w^T\boldsymbol x+b) \]

并且弗兰克还提出了用来自动更新程序参数的方式

他利用生物的负反馈机制来调整参数,假设 \(\hat y-y\) 是模型接收到的反馈,则参数更新为:

\[w_i \longleftarrow w_i + (\hat y - y)x_i\\ b \longleftarrow b + (\hat y - y) \]

可以发现,这种操作十分类似梯度下降,已经初具雏形了

多层感知机 | Multi-layer Perceptron,MLP

介绍 | Introduction

人们发现,单层的感知机无法处理非线性的问题(E.g. 异或问题),而同时期的支持向量机 Supported Vector Machine 更加的优秀,神经网络的研究曾停滞不前

既然一层解决不了问题,为什么不多套几层呢?

所以,多层感知机 就这么提出来了

其核心思想是利用激活函数的非线性将数据升维来让整个模型达到非线性的效果

其正确性由普适逼近定理 Universal Approximation Theorem[1]保证

由于神经元的连接方式有很多种,所以我们规定使用前馈 Feedforward 结构

前馈结构

将神经元分为不同的层,每一层只与其前后相邻层的神经元相连接,层内以及隔一层以上的神经元之间没有连接

所以可以将结构分成:

  • 输入层 Input Layer 直接接收输入
  • 隐含层 Hidden Layer 进行处理
  • 输出层 Output Layer 输出结果

将多个单层感知机通过前馈结构结合就是多层感知机了[喜]


好,现在我们来说明为甚激活函数必须是非线性 Non-linear

假设现在有一个三层感知机,权重、偏置、激活函数分别为:\(\mathbf W_i,\boldsymbol b_i,\phi_i\)

输入 \(\boldsymbol x \in \R^3\)

  • 第一层:\(\boldsymbol l_1 = \phi_1(\mathbf W_1\boldsymbol x_1 + \boldsymbol b_1)\)\(\boldsymbol l_1 \in \R^4\)
  • 第二层:\(\boldsymbol l_2 = \phi_2(\mathbf W_2 \boldsymbol l_1 + \boldsymbol b_2)\)\(\boldsymbol l_2 \in \R^2\)
  • 第三层:\(y = \phi_3(\mathbf W_3 \boldsymbol l_2 + b_3)\)\(y \in \R\)

先假设 \(\phi_i\) 是线性函数,即 \(\phi_i(\mathbf W \boldsymbol x + \boldsymbol b) = \mathbf W \phi_i (\boldsymbol x) + \boldsymbol b\)

则将 \(y\) 展开:

\[y = \mathbf W_1 \mathbf W_2 \mathbf W_3\phi_3(\phi_2(\phi_1(\boldsymbol x))) + \mathbf W_3\mathbf W_2 \boldsymbol b_1 + \mathbf W_3 \boldsymbol b_2 + b_3 \]

\(\mathbf W = \mathbf W_1\mathbf W_2\mathbf W_3\)\(\phi(·) = \phi_3(\phi_2(\phi_1(·)))\)\(b = \mathbf W_3 \mathbf W_2 \boldsymbol b_1 + \mathbf W_3 \boldsymbol b_2 + b_3\)

然后

\[y = \mathbf W \phi(\boldsymbol x) + b = \phi(\mathbf W\boldsymbol x + b) \]

很熟悉,对吧

没错,这就是单层感知机!!!

所以当 \(\phi\) 是线性函数的时候,模型仅仅在线性上提升了复杂度,退化成了单层感知机,仍然不能拟合非线性的问题

所以 \(\phi\) 才必须要是非线性函数

常用的激活函数

  • 逻辑斯谛函数 \(\sigma(x) = \frac{1}{1 + e^{-x}}\)\(\sigma'(x) = \sigma(x)(1-\sigma(x))\)

  • 双曲正切 \(\tanh(x) = \frac{e^x - e^{-x}}{e^{x}+e^{-x}}\)\(\tanh'(x) = 1 - \tanh^2(x)\)

  • 线性整流单元 Rectified linear unit, ReLU \(\text{ReLU}(x) = \max(x, 0)\)\(\text{ReLU}'(x) = \mathbb I(x \ge 0)\)

拓展 | Extend

[1] 普适逼近定理 | Universal Approximation Theorem

一个具有单一隐藏层的前馈神经网络,只要拥有足够多的神经元,并且使用了合适非线性激活函数,就能够以任意精度去逼近任何一个定义在有界闭集(紧致集)上的连续函数

表达成数学语言:

\(K \subseteq \R^n\) 是紧致集,\(f \in C(K)\)(连续函数空间)。给定非常数单调递增的连续函数 \(\sigma : \R \to \R\)(非多项式函数)

则:

\[(\forall \epsilon > 0) \left( \exist N \in \N,\alpha_i,\theta_i \in \R, \boldsymbol w_i \in \R^N \text{ s.t. } \sup_{\boldsymbol x \in K} \left|f(x)-\sum_{i=1}^{N}\alpha_i\sigma( \left<\boldsymbol w_i, \boldsymbol x \right> +\theta_i ) \right| \lt \epsilon \right) \tag{Thm E1.[1]} \]

该定理直接给出了神经网络的正确性

补充 | Addition

非线性的激活函数 | Non-linear Activation Function

这里我们把激活函数的作用看做升维

假设特征为二维向量 \(\boldsymbol x = (x, y)\),则激活函数将其变作三维向量 \(\boldsymbol x' = (x, y, f(x,y))\),其中 \(f : \R ^2 \to \R\) 是生成函数

如果 \(f\) 是线性的,即 \(f (a\boldsymbol x+b) = af(\boldsymbol x) + b\),则变化后的点还是在一个超平面上运动

\(f(\boldsymbol x)\) 表示一个超平面,且 \(f(\boldsymbol x)\) 的维度与 \((x,y)\) 相同,所以 \((x,y,f(\boldsymbol x))\) 的含义就是将 \((x, y, ·)\) 投影在 \(f(\boldsymbol x)\)

但如果 \(f\) 非线性,则变化后的点就不会仅仅限制于一个超平面内了,所以说,非线性的激活函数加强了感知机的表达能力

反向传播 | Backpropagat,BP

讲了怎么去拟合函数的,可是参数肯定是不能够自己调的

所以就要我们的老朋友梯度下降 Gradient Descent

利用张量的自动微分的操作,我们可以顺着系统建立的计算图中前向传播的反方向更新参数

原理就是链式法则

卷积神经网络 | Convolution Neural Network,CNN

卷积 | Convolution

既然名字里都有了,那一定很重要吧

一维卷积 | 1D Convolution

虽然不知道有什么用,但是可以帮助理解……吧?

对于连续函数 \(f : \R \to \R\)\(g : \R \to \R\),两者的卷积定义为:

\[(f*g)(t) = \int_{-\infin}^{\infin} f(\tau)g(t - \tau) \text{ d}\tau \tag{Def 1dC.1} \]

对于离散情况

\[(f*g)[n] = \sum_{m=-\infin}^{\infin} f[m]g[n -m] \tag{Def 1dC.2} \]

熟悉傅里叶变换的,肯定看出来了端倪

上面积分和‘和式中的 \(t - \tau\) 以及 \(n - m\) 表示了偏转,假设 \(g\) 是信号,\(f\) 是系统,则上式表达了系统 \(f\) 对于信号 \(g\)响应

具体的内容是信息处理[1]的内容,不属于我们学习的范畴,故不考虑,想看的可以看本节的拓展[1]

其实我们可以将 \(g(t - \tau)\) 看做一个权重,则上式就表示对于 \(f(t)\) 求一个加权平均

二维卷积 | 2D Convolution

这里只给出离散情况,连续[2]的去看拓展[2]

\[(f*g)[m,n] = \sum_k\sum_l f[k,l]g[m-k,n-l] \tag{Def 2dC.1} \]

对比 \(\text{Def 1dC.2}\),可以更好的理解该式子

由于卷积是关于 \(f\)\(g\) 是对称的,所以将 \(\text{Def 2dC.1}\) 改写成:

\[(f*g)[m,n] = \sum_{k}\sum_{l}f[m-k,n-l]g[k,l] \tag{2dC.2} \]

这里 \(f(·,·)\) 称作卷积核

CNN 中,为了方便计算,我们不妨稍微进行一下改变:

\[\sum_{k}\sum_{l}f(k,l)g(k+m,l+n) \tag{Cross-c} \]

这称作互相关 Cross-correlation操作

在实际应用中,超出范围的点记作 \(0\) 就是了,而这个式子的具体表现就是对应位置相乘再相加


既然一维的情况是对于某个信号的响应,换成我们的术语,大致可以说成是对于某个特征的识别(响应了就说明具有这种特征)

所以二维的情况就是聚焦与卷积核内,用卷积核提取特征

为什么要卷积 | Why We Need Convolution

上面提到卷积操作是对于特征的提取

其实这也是从生物得到的灵感:

在实际的视觉之中,我们所看见的实际上仅仅是一个很小的区域,然后再由大脑整合起来

而卷积核就是我们实际看到的视野,神经网络就是大脑

所以这仍然是对于生物的大脑的模拟


除了上面这个根本上的原因,还有一个更加现实的原因:如果将二维,甚至更高维的一个图片张量展平成一个一维向量,

全部接入 MLP 之后,就会发现,神经元的个数太多了,以至于每一层的计算都比较复杂,且有可能会很深

池化层/汇聚层 | Pooling Layers

实践 | Practice

在下面的代码中我们使用的是 pytorch

下载:

pip install torch

可能会用到 torchvision

pip install torchvision

这个部分的语言风格会与理论部分有不同

CNN

这个就没什么好讲的了

卷积 Convalution

torch.nn.Conv2d(
    in_channels,      # 输入通道数(RGB图片为3,灰度图为1)
    out_channels,     # 输出通道数(卷积核数量)
    kernel_size,      # 卷积核大小(int或tuple,如3或(3,5))
    stride=1,         # 步长(int或tuple)
    padding=0,        # 填充(int或tuple)
    dilation=1,       # 空洞卷积
    groups=1,         # 分组卷积
    bias=True,        # 是否使用偏置
    padding_mode='zeros'  # 填充模式
)

池化 Pooling

# 最大池化
torch.nn.MaxPool2d(
    kernel_size,      # 窗口大小
    stride=None,      # 步长(默认等于kernel_size)
    padding=0,        # 填充
    dilation=1,       # 空洞
    return_indices=False,  # 是否返回索引
    ceil_mode=False   # 是否使用ceil模式
)
# 平均池化
torch.nn.AvgPool2d(
    kernel_size,
    stride=None,
    padding=0,
    ceil_mode=False,
    count_include_pad=True,  # 是否包含填充
    divisor_override=None
)
# 自适应池化
torch.nn.AdaptiveAvgPool2d(output_size)  # 输出指定大小

这是一个小的实现:

import torch
import torch.nn as nn
import torch.nn.functional as F

class MyModel(nn.Module):
    def __init__(self, num_classes=10):
        super(MyModel, self).__init__()
        
        # 卷积层块
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1)
        self.bn1 = nn.BatchNorm2d(32)
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)  # 32x32 -> 16x16
        
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
        self.bn2 = nn.BatchNorm2d(64)
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)  # 16x16 -> 8x8
        
        self.conv3 = nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1)
        self.bn3 = nn.BatchNorm2d(128)
        self.pool3 = nn.AdaptiveAvgPool2d((4, 4))  # 自适应池化到4x4
        
        # 全连接层
        self.fc1 = nn.Linear(128 * 4 * 4, 256)
        self.dropout = nn.Dropout(0.5)
        self.fc2 = nn.Linear(256, num_classes)
        
    def forward(self, x):
        # 第一个卷积块
        x = self.conv1(x)
        x = self.bn1(x)
        x = F.relu(x)
        x = self.pool1(x)
        
        # 第二个卷积块
        x = self.conv2(x)
        x = self.bn2(x)
        x = F.relu(x)
        x = self.pool2(x)
        
        # 第三个卷积块
        x = self.conv3(x)
        x = self.bn3(x)
        x = F.relu(x)
        x = self.pool3(x)
        
        # 展平
        x = x.view(x.size(0), -1)
        
        # 全连接层
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        x = self.fc2(x)
        
        return x

其中 BatchNorm2d 实现的标准化

torch.nn.BatchNorm2d(
    num_features,           # 输入通道数(必需)
    eps=1e-5,              # 防止除零的小常数
    momentum=0.1,          # 均值和方差的更新动量
    affine=True,           # 是否使用可学习的缩放和偏移
    track_running_stats=True  # 是否跟踪运行时的均值和方差
)

还有 F.relu 是可以用 PyTorch 自带的 nn.relu

当然,我们也可以套用前人为我们训练好的网络(以 ResNet18 为例)

首先我们要明白,为什么可以套用

于是,我们将卷积层可视化了出来:

不同层次卷积核示意

深层的图片是套了一个 GAN 生成出来的,不然原图过于的恐怖了(全是眼睛、毛发等突出的特征

观察浅层和中层的卷积核

这里用一下老师课件的话(总结的很好了:

浅层网络(低级特征 - 极具通用性): 在 CNN 的最前面几层,卷积核学会的通常是极其基础的视觉元素,比如:边缘(水平、垂直、对角线)、角点、颜色斑块和明暗渐变

中层网络(中级特征): 组合浅层的边缘和角点,形成基础的几何形状(如圆形、多边形)或纹理(如网格、条纹)。

而深层的,只要不是全新的任务,也可以直接用,而这种模型用的是很大很大的数据训练的,你觉得会有多少数据是完全没有出现过的呢?

RNN

难的不是 RNN 本身,而是前后的数据处理

这里用到的 LSTM

self.lstm = nn.LSTM(
    input_size = embedding_dim,
    hidden_size = hidden_dim,
    num_layers = num_layers,
    batch_first = True,
    bidirectional = True,
)

embedding_dim 是词向量的维度,作为输入的大小

hiden_size 是隐藏层的大小

num_layrs 是循环的层数(默认为 1,可以不加)

bacth_first 是否将 bacth 放在张量的第一维

bidirectional 是否开启双向 RNN

这很简单的

def collate_fn(batch):
    inputs, targets = zip(*batch)
    lengths = [len(seq) for seq in inputs]
    max_len = max(lengths)

    padded_inputs = torch.zeros(len(inputs), max_len, dtype=torch.long)
    padded_targets = torch.zeros(len(targets), max_len, dtype=torch.float)

    for i, (seq, tgt) in enumerate(zip(inputs, targets)):
        padded_inputs[i, : len(seq)] = seq
        padded_targets[i, : len(tgt)] = tgt

    return padded_inputs, padded_targets, lengths

这个函数是将长短不一的单词补齐,应该是 baseline 里面有的

这里看到它的前向传播:

def forward(self, x, lengths):
    embedded = self.embedding(x)

    packed_embedded = pack_padded_sequence( # 1
        embedded, lengths, batch_first = True, enforce_sorted = False
    )

    packed_output, _ = self.lstm(packed_embedded)

    lstm_out, _ = pad_packed_sequence( # 2
        packed_output, batch_first = True, total_length = x.size(1)
    )

    return self.fc(lstm_out).squeeze(-1)

注释 1 的部分是将词向量的实际维度传入 RNN,这样子避免了很多的无效运算

注释 2 的部分则是将词向量的维度还原

这是完整的神经网络部分

class BitLSTMSegmentation(nn.Module):
    def __init__(self, vocab_size, embedding_dim = 64, hidden_dim = 512, num_layers = 5, dropout = 0.5): # embedding 可以增加,可提升精度,但可能过拟合
        super(BitLSTMSegmentation, self).__init__()
        self.vocab_size = vocab_size

        self.embedding = nn.Embedding(vocab_size + 1, embedding_dim, padding_idx = 0)

        self.lstm = nn.LSTM(
            input_size = embedding_dim,
            hidden_size = hidden_dim,
            num_layers = num_layers,
            batch_first = True,
            bidirectional = True,
        )

        self.fc = nn.Linear(hidden_dim * 2, 1) # 双向的 LSTM 隐藏层的维度会翻倍
    
    def forward(self, x, lengths):
        embedded = self.embedding(x)

        packed_embedded = pack_padded_sequence(
            embedded, lengths, batch_first = True, enforce_sorted = False
        )

        packed_output, _ = self.lstm(packed_embedded)

        lstm_out, _ = pad_packed_sequence(
            packed_output, batch_first = True, total_length = x.size(1)
        )

        return self.fc(lstm_out).squeeze(-1)

训练部分会有很大的不同

因为计算损失函数要用到掩码这个操作

with autocast('cuda'):
    outputs = model(inputs, lengths)

    mask = torch.arange(inputs.shape[1])[None, :] < torch.tensor(lengths)[:, None] # 这里会触发广播机制
    mask = mask.to(device)
    outputs = outputs[mask]
    targets = targets[mask]
    loss = criterion(outputs, targets)

这里模型的初始化:

model = BitLSTMSegmentation(vocab_size, embedding_dim = 64, hidden_dim = 128).to(device)
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.Adam(model.parameters(), lr = 0.0002)

用的是 Adam 梯度下降,二分类交叉熵误差

用了一个掩码mask) 来计算 loss

经过不断地调参,我获得了——

score

可以发现,我的最优成绩从 \(0.9807\) 提升了足足 \(0.0001\)!(*^▽^*)

posted @ 2026-05-22 21:25  Yangyihao  阅读(20)  评论(1)    收藏  举报