深度学习笔记

1. 深度学习简介

一个学校有 nn 门考试,每一门考试所占权重为 WnW_n, 对于一个学生,他的每一门考试的分数为 xnx_n, 再加上一个固定的常量 bb, 学校用于判断是否录取这个学生的逻辑为:

y^=W1x1+W2x2++Wnxn+b\hat{y} = W_1 x_1 + W_2 x_2 + \dots + W_n x_n + b

缩写为:

y^=Wx+b\hat{y} = Wx + b

由于最终所期望的计算结果 y^\hat{y} 为一个数值,所以 WW 是一个 1×n1 \times n 的矩阵:

W=[W1,W2,,Wn]\mathbf{W} = [W_1, W_2, \dots, W_n]

xx 则是一个 n×1n \times 1 的矩阵(如下所示),这样进行矩阵乘法 W×xW \times x 才会得到一个 1×11 \times 1 的矩阵,即 y^\hat{y}

x=[x1x2xn]\mathbf{x} = \begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_n \end{bmatrix}

y^\hat{y} 的值则被称之为预测值,而 yy 则被称之为真实值,学校会根据这个值来判断是否录取这个学生。深度学习的目的就是找到一组 WWbb 的值,使得预测值 y^\hat{y} 与真实值 yy 之间的差距最小。

2. 感知器 (Perceptron)

在一个坐标系中有两类点,一类是红色,一类是蓝色,每个点都有两个坐标 (x1,x2)(x_1, x_2),现在需要找到一条直线,将这两类点分开。

首先,我们假设这条直线的方程为:

y=W1x1+W2x2+b=0y = W_1 x_1 + W_2 x_2 + b = 0

其中 W1W_1W2W_2 是直线的斜率,bb 是直线的截距。

最开始,我们先随机初始化 W1W_1, W2W_2bb 的值,然后根据这个方程计算出每个点的 yy 值,如果 yy 值大于 0,则将这个点归为红色,如果 yy 值小于 0,则将这个点归为蓝色。

然后我们对每个点进行分类,如果分类错误,则调整 W1W_1, W2W_2bb 的值,直到所有点都被正确分类。调整的逻辑为:

W1=W1+α×(yy^)×x1W_1 = W_1 + \alpha \times (y - \hat{y}) \times x_1 W2=W2+α×(yy^)×x2W_2 = W_2 + \alpha \times (y - \hat{y}) \times x_2 b=b+α×(yy^)b = b + \alpha \times (y - \hat{y})

其中 α\alpha 是一个学习率,用来控制每次调整的幅度。在课程种提供了代码,可以参考代码来实现感知器。

首先我们有一个 csv 数据集,数据集的格式为:

0.78051,-0.063669,1
0.28774,0.29139,1
0.40714,0.17878,0
0.2923,0.4217,0
...

其中第一列和第二列是特征 x1x_1x2x_2,第三列是标签 yys,标签为 1 表示红色,标签为 0 表示蓝色。

然后我们读取数据集,并进行处理,代码如下:

感知器训练
import matplotlib.pyplot as plt
import numpy as np
import pandas


def stepFunction(t):
    return 1 if t >= 0 else 0


def prediction(X: np.ndarray, W: np.ndarray, b: float) -> int:
    return stepFunction((np.matmul(X, W) + b)[0])


def perceptronStep(
    X: np.ndarray,
    y: np.ndarray,
    W: np.ndarray,
    b: float,
    learn_rate: float = 0.01,
) -> tuple[np.ndarray, float]:
    for i, x_n in enumerate(X):
        y_hat = prediction(x_n, W, b)
        match y[i] - y_hat:
            case 1:
                W[0] += x_n[0] * learn_rate
                W[1] += x_n[1] * learn_rate
                b += learn_rate
            case -1:
                W[0] -= x_n[0] * learn_rate
                W[1] -= x_n[1] * learn_rate
                b -= learn_rate
    return W, b


def trainPerceptronAlgorithm(
    X: np.ndarray,
    y: np.ndarray,
    learn_rate: float = 0.01,
    num_epochs: int = 25,
) -> list[tuple[float, float]]:
    x_min, x_max = min(X.T[0]), max(X.T[0])
    y_min, y_max = min(X.T[1]), max(X.T[1])
    weights = np.array(np.random.rand(2, 1))
    bias = np.random.rand(1)[0] + x_max

    boundary_lines = []
    for i in range(num_epochs):
        weights, b = perceptronStep(X, y, weights, bias, learn_rate)
        boundary_lines.append((-weights[0] / weights[1], -b / weights[1]))

    # draw all the boundary lines
    x_vals = np.linspace(x_min, x_max, 100)
    line_n = 10
    for i, (m, b) in enumerate(boundary_lines[-line_n:]):
        y_vals = m * x_vals + b
        color = "b" if i < line_n - 1 else "r"
        line_style = "--" if i < line_n - 1 else "-"
        plt.plot(x_vals, y_vals, color=color, linestyle=line_style)

    # plot the data points
    plt.scatter(X[:, 0], X[:, 1], c=["b" if y_n else "r" for y_n in y])
    plt.xlim(x_min, x_max)
    plt.ylim(y_min, y_max)
    plt.show()

    return boundary_lines
def load_data() -> tuple[np.ndarray, np.ndarray]:
    df = pandas.read_csv("data.csv")
    X = df[["x", "y"]].to_numpy()
    y = df["label"].to_numpy()
    return X, y


def main():
    X, y = load_data()
    trainPerceptronAlgorithm(X, y, num_epochs=25)

其中,最核心的函数是 perceptronStep,这个函数会调用 prediction 函数来计算每个点的预测值,然后根据预测值和真实值的差距来调整 W1W_1, W2W_2bb 的值。而 trainPerceptronAlgorithm 函数则连续调用 perceptronStep 函数,num_epochs 参数用来控制迭代次数,learn_rate 参数用来控制每次调整的幅度,最终会绘制出每次迭代(训练)后的直线,并展示出来。此时我们得到的 W1W_1, W2W_2bb 的值就是最终的模型参数。

3. 激活函数 (Activation Function) 与误差函数 (Error Function)

上面的感知器图像是一条直线,但实际场景中,直线很可能无法对所有点进行分类。此时我们需要更加复杂的函数来告诉我们当前距离目标还有多远(误差的大小)。为了进行梯度下降 (Gradient Descent),我们需要一个连续、可微分 (Differentiable) 的函数来衡量误差。当输入变化时,其输出也应该变化,否则就像在阿兹台克金字塔上一样,当我们水平方向移动时,并没有任何高度变化,无法对训练方向进行优化。

对于非线性的误差函数,值只有 0 或 1 ,当输入值在某个零界点变化时,输出从 0 跃迁到 1 ,这对于预测来说并不友好,我们需要的是当输入一个很大的负数时,输出接近 0 ,当输入一个很大的正数时,输出接近 1 ,所以采用 sigmoid 函数作为激活函数。

Sigmoid 函数的表达式为:

σ(x)=11+ex\sigma(x) = \frac{1}{1 + e^{-x}}

当分类的对象不止是 2 种,而是 nn 种时,可以使用 Softmax 函数来进行分类。对于一组输入 x1,x2,,xnx1, x2, \dots, xn,Softmax 函数的表达式为:

σ(xi)=exiex1+ex2++exj=exij=1nexj\sigma(x_i) = \frac{e^{x_i}}{e^{x_1} + e^{x_2} + \dots + e^{x_j}} = \frac{e^{x_i}}{\sum_{j=1}^{n} e^{x_j}}

最大似然估计(Maximum Likelihood Estimation, MLE)是深度学习训练中常用的目标函数,其核心思想是:通过优化模型参数,使模型对真实标签的预测概率最大化。

假设我们有两个模型,想比较它们的优劣,可以将同一组带有真实标签的数据输入两个模型,观察每个模型对这些标签的预测概率。将所有样本的预测概率相乘,得到整体的似然值,用以衡量模型对数据的拟合程度。

然而,由于概率值通常较小,多个样本的概率乘积会迅速趋近于零,且对单个样本概率的微小变化会导致整体似然值产生较大波动。因此,在实际计算中,我们通常对似然函数取 对数 ,将乘积变为求和,从而提高数值稳定性并便于优化。

0.6×0.2×0.1×0.7=0.00840.6 \times 0.2 \times 0.1 \times 0.7 = 0.0084 log(0.6×0.2×0.1×0.7)=log(0.6)+log(0.2)+log(0.1)+log(0.7)4.78log(0.6 \times 0.2 \times 0.1 \times 0.7) = log(0.6) + log(0.2) + log(0.1) + log(0.7) \approx -4.78

由于概率都是小于 1 的,所以对数的值都为负数,将它们的和乘以 -1 得到一个正数 (按照上面的公式为 4.78) ,这个数则被称为交叉熵 (Cross Entropy),交叉熵越大则误差越大,我们的目标就是最小化交叉熵。

CrossEntropy=i=1nyi×log(pi)+(1yi)×log(1pi)CrossEntropy = - \sum_{i=1}^{n} y_i \times log(p_i) + (1 - y_i) \times log(1 - p_i) CrossEntropy=i=1nj=1myij×log(pij)CrossEntropy = - \sum_{i=1}^{n}\sum_{j=1}^{m} y_{ij} \times log(p_{ij})

上面两个交叉熵公式,分别是:

  • 二分类交叉熵:nn 是样本数,yiy_i 是样本 ii 的真实标签,pip_i 是样本 ii 的预测概率。
  • 多分类交叉熵:nn 是样本数,mm 是类别数,yijy_{ij} 是样本 ii 的真实标签为 jj 的概率,pijp_{ij} 是样本 ii 的预测概率。

对于多分类交叉熵,当 m=2m=2 时,可以进一步简化为二分类交叉熵。

误差函数则为交叉熵除以样本数,其公式为

ErrorFunction=1ni=1nj=1myij×log(pij)ErrorFunction = - \frac{1}{n} \sum_{i=1}^{n}\sum_{j=1}^{m} y_{ij} \times log(p_{ij})

其中 nn 是样本数,mm 是类别数,yijy_{ij} 是样本 ii 的真实标签为 jj 的概率,pijp_{ij} 是样本 ii 的预测概率。

在误差函数中,pijp_{ij} 是将第 ii 个样本在第 jj 个类别上的线性输出(logit)经由激活函数(sigmoid 或 softmax)转换后得到的预测概率。

总结:我们的最终目标是要找到 Wx+bWx + b 中的 WWbb。于是我们先随机选择一组 WWbb,用已有的样本代入进去,得到一个值 zz,再把这个 zz 代入到激活函数(根据任务类别选择 sigmoid 或 softmax),得到预测值 pp。接着将多个 pp 拿来计算误差(交叉熵)。然后通过梯度下降(Gradient Descent)来优化 WWbb,重复上面的步骤,并不断寻找让交叉熵变小的方向。经过多次尝试,最终找到一组能使预测结果与实际标签值相似度最大的 WWbb,这就是训练的过程。

4. 梯度下降 (Gradient Descent)

对于只有一个参数的误差函数,它的图像是一条二维坐标系中的曲线。梯度下降的过程可以理解为:从某个初始点出发,计算该点的导数(即梯度),从而确定函数值减小最快的方向——也就是负梯度方向。然后沿着这个方向更新参数,使函数值逐步降低,最终逼近最小值。

在单变量函数中,导数表示函数图像在某点的切线斜率。梯度下降并不是沿着切线方向前进,而是沿着负导数方向移动,因为我们希望让函数值变小。

在实际应用中,误差函数通常是由多个参数构成的多维函数。为了使用梯度下降法优化模型,需要对该函数对所有参数求偏导,得到梯度向量,并沿着负梯度方向更新参数,从而逐步减小误差。

例如 sigmoid 函数为

σ(x)=11+ex\sigma(x) = \frac{1}{1 + e^{-x}}

其导数为

σ(x)=σ(x)×(1σ(x))\sigma'(x) = \sigma(x) \times (1 - \sigma(x))

梯度下降法通常用于优化连续可导的损失函数(如交叉熵),其输出是概率值。当模型预测正确时,损失函数的值可能仍然偏离最优,因此梯度仍然存在,可以继续更新参数 WWbb ,使模型更加稳健。而感知器算法的输出是离散的 0 或 1,使用的是硬阈值函数。当预测正确时,它不会更新参数,因此无法进一步优化模型。