deep learning(吴恩达)深度学习笔记

1 矢量化

向量化基本上是指在代码中消除显式的for循环。在深度学习时代,尤其是在实际应用中,你通常会在相对较大的数据集上进行训练,因为深度学习算法在这种情况下表现最佳。因此,确保代码运行速度非常重要,否则在处理大数据集时,代码运行可能会非常缓慢,导致你需要等待很长时间才能得到结果。在深度学习时代,掌握向量化技术已经成为一项关键技能。

向量化示例

什么是向量化?在逻辑回归中,你需要计算Z = W^T * X + B,其中WX都是向量,且可能是非常大的向量,特别是在你有大量特征的情况下。WX都是维度为n的向量。

如果使用非向量化的实现方法,你可能会这样写代码:

Z = 0
for i in range(len(X)):
    Z += W[i] * X[i]
Z += B

这种非向量化的实现方法会很慢。相比之下,向量化的实现方法可以直接计算W^T * X,在Python或numpy中,你可以这样写:

Z = np.dot(W, X) + B

这种方法会快得多。下面通过一个小演示来说明。

向量化演示

首先导入必要的库:

import numpy as np
import time

接下来,创建两个一百万维度的随机数组,并测量计算时间:

A = np.random.rand(1000000)
B = np.random.rand(1000000)

# 向量化实现
tic = time.time()
C = np.dot(A, B)
toc = time.time()

print("Vectorized version: " + str(1000*(toc - tic)) + "ms")

向量化的实现大约需要1.5毫秒。接下来,实现非向量化的for循环版本,并比较计算时间:

C = 0
tic = time.time()
for i in range(1000000):
    C += A[i] * B[i]
toc = time.time()

print("For loop version: " + str(1000*(toc - tic)) + "ms")

非向量化的实现大约需要500毫秒。可以看到,非向量化的版本比向量化的版本慢了大约300倍。

向量化的重要性

通过这个例子,你可以看到,如果你记得向量化代码,代码运行速度会快得多。对于深度学习算法,向量化可以显著加快获得结果的速度。

尽量避免在代码中使用显式的for循环是一个重要的经验法则。无论是CPU还是GPU,都有并行化指令(称为SIMD,单指令多数据),这使得使用内置函数(如np.dot)能够更好地利用并行性,从而显著加快计算速度。这不仅适用于GPU上的计算,也适用于CPU上的计算。虽然GPU在这些SIMD计算上非常出色,但CPU在这方面也并不逊色。

总结

通过向量化可以显著加快代码的运行速度。下一步,我们将继续讨论向量化的更多示例,并开始向量化逻辑回归算法。


2 更多矢量化示例

示例1:矩阵与向量相乘

假设你需要计算向量u,它是矩阵A和向量v的乘积。根据矩阵乘法的定义,u[i]等于A[i,j]v[j]的和。

非向量化的实现方法如下:

u = np.zeros(n)
for i in range(n):
    for j in range(m):
        u[i] += A[i][j] * v[j]

这种实现方式使用了两个for循环。然而,向量化实现可以通过以下方式直接计算:

u = np.dot(A, v)

这种向量化的实现方式不仅消除了两个for循环,还大大提高了计算速度。

示例2:向量元素的逐项运算

假设你有一个向量v,需要对其每个元素应用指数运算。非向量化的实现方法可能如下:

u = np.zeros(len(v))
for i in range(len(v)):
    u[i] = np.exp(v[i])

相比之下,使用NumPy的内置函数可以更简洁地实现:

u = np.exp(v)

这种实现方式不仅消除了for循环,还能显著提高计算效率。类似的,NumPy中还有许多其他的向量化函数,如np.log(v)(计算对数)、np.abs(v)(计算绝对值)、np.maximum(v, 0)(逐元素取最大值)、v2(逐元素平方)、1/v(逐元素求倒数)等。

应用于逻辑回归的梯度下降

现在,让我们将这些向量化的概念应用到逻辑回归的梯度下降算法中。之前的代码中有两个for循环,一个是遍历训练样本的for循环,另一个是遍历特征的for循环。假设我们有多个特征,那么我们需要对每个特征分别计算梯度,这里使用了显式的for循环。

要优化这一点,我们可以dw1dw2等初始化为零的过程替换为向量化操作,将dw定义为一个向量:

dw = np.zeros((nx, 1))

然后,将计算梯度的for循环替换为向量化操作:

dw += xi * dz[i]

最后,将dw除以样本数m

dw /= m

通过这种方式,我们将代码中的两个for循环减少到一个for循环。虽然还保留了遍历训练样本的for循环,但已经显著提高了计算效率。

进一步优化

本视频展示了如何通过向量化消除一个for循环来加快代码速度。但事实上,我们可以做得更好。在下一视频中,我们将讨论如何进一步向量化逻辑回归的实现,最终完全消除for循环,甚至无需遍历训练样本,从而大幅提升代码效率。我们将在下一视频中详细探讨这一点。


3 逻辑回归矢量化

我们已经讨论了向量化如何显著加快代码速度。在本视频中,我们将探讨如何向量化实现逻辑回归,使其能够处理整个训练集,即在一次梯度下降迭代中无需使用显式for循环处理整个训练集。这种技术在以后的神经网络中也会广泛应用。让我们开始吧。

前向传播步骤

首先,回顾一下逻辑回归的前向传播步骤。假设有M个训练样本,为了对第一个样本进行预测,你需要计算Z,然后计算激活值A,再得到第一个样本的预测值ŷ。对每一个样本进行预测都需要重复这些步骤,这样如果有M个训练样本,理论上你需要进行M次计算。

然而,通过向量化,可以在不使用显式for循环的情况下一次性完成所有样本的前向传播。具体来说,我们可以将所有的训练输入构成一个矩阵X,这个矩阵的形状是(n_x, M),其中n_x是特征数,M是样本数。

向量化实现Z的计算

为了计算Z(每个样本的Z值),可以使用以下公式:

Z=W^T*X+B

这里,W是权重向量,B是偏置项。通过矩阵乘法W^T*X,你可以一次性计算出所有样本的Z值,并通过广播机制将偏置项B加到每一个Z值上。

在Python中,可以用以下代码实现:

Z = np.dot(W.T, X) + B

其中,Z是一个(1, M)的向量,包含了所有样本的Z值。Python中的广播机制会自动将标量B扩展为与Z匹配的向量

向量化实现A的计算

接下来,你需要计算激活值A,即A = σ(Z)。通过将所有样本的Z值堆叠为矩阵Z,我们也可以一次性计算出所有样本的A值。类似地,将所有A值堆叠后得到矩阵A

在编程作业中,你会看到如何实现一个向量化的sigmoid函数,它接收矩阵Z作为输入,输出矩阵A。这使得你无需遍历每个训练样本就能一次性计算出所有样本的A值。

总结

通过向量化技术,你可以非常高效地同时计算出所有样本的激活值A,而无需逐一遍历样本。在下一视频中,我们将探讨如何利用向量化技术高效地进行反向传播计算,以获得梯度。


4 将逻辑回归的梯度输出矢量化

在上一个视频中,我们讨论了如何使用向量化同时计算整个训练集的预测值。在本视频中,我们将继续探讨如何使用向量化同时计算所有M个训练样本的梯度。最后,我们将结合所有内容,展示如何推导出一个高效的逻辑回归实现。

梯度计算

回忆一下,我们在计算梯度时,会先对每个样本计算dz。具体来说,dz1等于a1y1dz2等于a2y2,依此类推,对所有M个训练样本都进行类似的操作。我们将这些dz值堆叠成一个新变量dZ,这个dZ是一个1xM的矩阵或M维行向量

根据前面的定义,dZ可以通过AY直接计算得到,其中A表示所有a值的集合,Y表示所有y值的集合。这样,你可以用一行代码来计算dZ

dZ = A - Y

向量化的梯度计算

在之前的实现中,我们已经消除了一个for循环,但仍然保留了对训练样本的循环。我们初始化了一个全零的向量dw,然后在每次迭代中对每个样本执行dw的更新操作。现在,让我们进一步向量化这些操作。

首先,db的计算本质上是在求所有dz的和,再除以样本数M

db = (1/M) * np.sum(dZ)

dw的计算则可以表示为:

dw = (1/M) * np.dot(X, dZ.T)

其中,X是包含所有样本特征的矩阵。这样,我们可以在一行代码中完成dw的计算,而无需for循环。

将所有操作整合

现在,我们可以将前向传播和反向传播的向量化实现整合在一起,形成一个高效的逻辑回归实现。以下是完整的代码:

Z = np.dot(W.T, X) + B
A = sigmoid(Z)
dZ = A - Y
dw = (1/M) * np.dot(X, dZ.T)
db = (1/M) * np.sum(dZ)
W = W - learning_rate * dw
B = B - learning_rate * db

这段代码在不使用for循环的情况下,完成了所有样本的预测值计算和梯度计算,极大地提高了计算效率。

总结

虽然我们建议尽量避免显式的for循环,但在需要多次迭代的梯度下降中,仍然需要在迭代次数上使用一个外部的for循环。不过,能够在单次梯度下降中完全消除for循环,仍然是一个非常酷的技巧。

在下一个视频中,我们将讨论Python和NumPy中的广播机制,它能够使代码的某些部分更加高效。


5 用python进行广播

示例:计算食物中卡路里的百分比

我们来看一个例子。下图展示了四种不同食物(苹果、牛肉、鸡蛋和土豆)中每100克所含的碳水化合物、蛋白质和脂肪的卡路里数。例如,100克苹果含有56卡路里的碳水化合物,而牛肉则含有104卡路里的蛋白质和135卡路里的脂肪。

假设我们要计算每种食物中来自碳水化合物、蛋白质和脂肪的卡路里所占的百分比。例如,苹果的总卡路里为56 + 1.2 + 1.8 = 59卡路里,其中来自碳水化合物的卡路里占比为56/59,大约是94.9%。

要实现这一计算,我们首先需要对矩阵的每一列求和,得到四种食物的总卡路里数,然后将矩阵中的每个元素除以对应列的总和,以得到卡路里的百分比。

代码实现

我们可以使用以下两行Python代码实现上述操作,无需显式的for循环:

cal = A.sum(axis=0)  # 纵向求和
percentage = (A / cal.reshape(1, 4)) * 100  # 计算百分比

广播机制的详细解释

在上述代码中,axis=0参数指示Python沿着纵向(列)求和,而reshape(1, 4)将求和后的向量重新调整为1x4的矩阵,以便与原始矩阵进行元素级别的除法运算。

广播机制的核心在于,Python会自动扩展参与运算的矩阵或向量,使其维度匹配。例如,3x4的矩阵可以与1x4的矩阵进行除法运算,其中1x4的矩阵会被复制三次,以匹配3x4矩阵的形状。

广播的其他示例

  • 标量与向量相加:如果将一个标量与一个4x1的向量相加,Python会自动将标量扩展为一个4x1的向量,然后进行逐元素加法。
  • 矩阵与行向量相加:如果有一个2x3的矩阵与一个1x3的行向量相加,Python会将行向量复制两次,扩展为2x3的矩阵,再进行加法运算。
  • 矩阵与列向量相加:如果一个m×n的矩阵与m×1的列向量相加,Python会将列向量复制n次,扩展为m×n的矩阵,再进行加法运算。

广播的广泛应用

在实现神经网络时,这些广播操作可以大幅减少代码量并提高效率。MATLAB或Octave中也有类似的功能,但Python中的广播更为直观且强大。

希望通过学习广播机制,您能在编写Python代码时不仅提高运行速度,还能用更少的代码实现目标。下一视频中,我们将分享一些减少Python代码中错误的技巧。


6 关于Python/Numpy的向量说明

Python支持广播操作,这为程序语言提供了很大的灵活性,我认为这既是优点也是缺点。优点在于,Python语言的高度表达性和灵活性让你可以用一行代码完成许多工作。然而,缺点在于,这种灵活性有时会引入一些微妙的错误,或者导致一些奇怪的错误,特别是当你不熟悉广播或相关特性时。例如,如果你将一个列向量与一个行向量相加,你可能期望出现维度不匹配的错误,但实际上你可能会得到一个矩阵。这种现象背后有一定的逻辑,但对于不熟悉Python的用户来说,可能会导致难以发现的错误。

为了帮助你更轻松地编写无错误的Python和Numpy代码,下面我分享一些小技巧,这些技巧对我来说非常有用,可以减少或简化代码中的错误。

示例:不直观的Python-Numpy行为

我们来看一个简单的演示。首先,设定 a = np.random.randn(5),这会生成5个随机的高斯变量并存储在数组a中。打印a的内容,发现它的形状是 (5,),这在Python中被称为一维数组(rank 1 array),既不是行向量,也不是列向量。这种结构可能会导致一些不直观的效果。

例如,打印a.transpose()时,结果与a相同。如果尝试计算a与其转置的内积,可能预期得到一个矩阵,但实际上得到的是一个标量。这是因为一维数组的行为并不完全像行向量或列向量。

避免使用一维数组

为了避免上述问题,我建议在编写代码时,避免使用形状为 (n,) 的一维数组。取而代之,可以明确地将数组设为 (n, 1) 的列向量或 (1, n) 的行向量。例如,定义 a = np.random.randn(5, 1) 会创建一个 (5, 1) 的列向量,此时 aa.transpose() 的行为将更加直观。与之前的情况不同,转置后的 a 现在变成了行向量。

使用断言与重塑操作

在编写代码时,如果不确定向量的维度,可以添加断言语句。例如,可以使用 assert a.shape == (5, 1) 来确保a是一个 (5, 1) 的列向量。这些断言语句执行开销很小,同时也可以作为代码的文档,不要犹豫使用它们。

如果在某些情况下,你最终得到了一个一维数组,可以通过 reshape 操作将其重塑为 (n, 1)(1, n) 的数组,以确保其行为一致。

总结

为了简化代码并减少错误,不建议使用一维数组。相反,建议始终使用形状为 (n, 1) 的列向量或 (1, n) 的行向量。此外,随时使用断言语句来检查矩阵和数组的维度,并且不要犹豫使用 reshape 操作,以确保你的数据结构具有你需要的维度。这些建议可以帮助你消除Python代码中的错误,并让编程任务变得更加轻松。


第二周练习总结

步骤一(预处理)

展平训练和测试数据集

# 展平训练数据集
train_set_x_flatten = train_set_x_orig.reshape(train_set_x_orig.shape[0], -1).T

# 展平测试数据集
test_set_x_flatten = test_set_x_orig.reshape(test_set_x_orig.shape[0], -1).T

# 打印展平后的数据集形状以验证
print("train_set_x_flatten shape: ", train_set_x_flatten.shape)
print("test_set_x_flatten shape: ", test_set_x_flatten.shape)

'train_set_x_orig.reshape(train_set_x_orig.shape[0], -1)':将每张图像展平为一维向量,'.T':对展平后的数据进行转置,得到 (num_px*num_px*3, m_train),使得每列是一个展平后的图像

步骤二(预处理)

对数据集进行中心化和标准化

将数据集中每个像素的值除以 255(像素通道的最大值),这样做可以使图像的像素值范围变为 [0, 1],有助于加快模型的收敛速度。

# 标准化训练数据集和测试数据集
train_set_x = train_set_x_flatten / 255.0
test_set_x = test_set_x_flatten / 255.0

# 打印标准化后的数据集形状以验证
print("train_set_x shape: ", train_set_x.shape)
print("test_set_x shape: ", test_set_x.shape)


预处理结束,下面开始正式的神经网络构建:

在构建一个简单的算法来区分猫图像和非猫图像时,使用逻辑回归是一个很好的起点。逻辑回归可以被视为一个非常简单的神经网络模型,具有以下特点:

逻辑回归与神经网络的关系

逻辑回归

  • 是一个线性分类器,它通过一个线性决策边界来将数据分成两类(例如猫与非猫)。
  • 它的输出是一个概率值,通过将线性组合的结果传递给 sigmoid 激活函数来得到。

神经网络的基本组成部分

  • 输入层:接受输入特征(例如图像的像素值)。
  • 线性部分:计算加权和加偏置。
  • 激活函数:将线性部分的输出转换为概率值,这里使用的是 sigmoid 函数。

算法架构

  1. 初始化参数

    • 初始化权重和偏置,这些参数在训练过程中会被优化。
  2. 前向传播

    • 计算预测值(概率),通过计算线性组合的结果并将其通过 sigmoid 函数进行激活。
  3. 计算成本

    • 使用交叉熵损失函数计算预测值和真实标签之间的误差。
  4. 反向传播

    • 计算成本函数对参数的梯度,并使用梯度下降等优化算法更新参数。
  5. 训练模型

    • 通过多次迭代优化参数,最终得到一个能够准确分类图像的模型。

步骤三

实现 Sigmoid 函数

Sigmoid 函数是逻辑回归和神经网络中的关键组件,它将输入 z(输入和权重的线性组合)转换为 0 到 1 之间的输出,可以解释为概率。

 以下是如何使用 numpy 在 Python 中实现 Sigmoid 函数的代码:

import numpy as np

def sigmoid(z):
    """
    计算 z 的 sigmoid 值

    参数:
    z -- 标量或任意大小的 numpy 数组

    返回:
    s -- sigmoid(z) 的结果
    """
    # 使用 np.exp() 函数计算 z 的 sigmoid
    s = 1 / (1 + np.exp(-z))
    
    return s

步骤四

初始化参数

在开始训练模型之前,你需要初始化模型的参数。对于逻辑回归模型,参数包括权重向量 w 和偏置 b

initialize_with_zeros,在下方的代码单元中实现参数初始化。

def initialize_with_zeros(dim):
    """
    此函数会将权重向量 w 初始化为全零向量,并将偏置 b 初始化为 0。

    参数:
    dim -- 权重向量 w 的大小(这也对应于输入特征的数量)

    返回:
    w -- 形状为 (dim, 1) 的初始化为零的权重向量
    b -- 初始化为 0 的偏置(标量)
    """
    
    w = np.zeros((dim, 1))
    b = 0.0
    
    return w, b

现在需要实现前向传播和反向传播步骤,以便学习参数。


步骤五

propagate 函数

你需要实现一个 propagate() 函数,该函数用于计算成本函数(cost function)及其梯度(gradient)

代码实现:
def propagate(w, b, X, Y):
    """
    实现前向和反向传播步骤
    
    参数:
    w -- 权重,形状为 (num_px * num_px * 3, 1)
    b -- 偏置,标量
    X -- 数据矩阵,形状为 (num_px * num_px * 3, m)
    Y -- 实际标签向量,形状为 (1, m)
    
    返回:
    cost -- 逻辑回归的负对数似然成本
    dw -- 成本函数对 w 的导数,与 w 形状相同
    db -- 成本函数对 b 的导数,与 b 形状相同
    """
    
    m = X.shape[1]  # 训练样本数量

    # 前向传播 (从 X 计算 A 和成本)
    A = sigmoid(np.dot(w.T, X) + b)  # 激活值 A
    cost = -1/m * np.sum(Y * np.log(A) + (1 - Y) * np.log(1 - A))  # 成本函数
    
    # 反向传播 (计算 dw 和 db)
    dw = 1/m * np.dot(X, (A - Y).T)
    db = 1/m * np.sum(A - Y)

    # 保证成本值是一个标量
    cost = np.squeeze(cost)
    
    grads = {"dw": dw,
             "db": db}
    
    return grads, cost

步骤六

optimize 函数

你现在需要编写一个优化函数,用来通过最小化成本函数J来学习参数 w和b。对于一个参数 θ,其更新规则为:,其中,α是学习率。

需要实现一个函数 optimize(),它将:

  1. 通过前向传播和反向传播计算成本和梯度。
  2. 使用梯度下降法更新参数w和b。
  3. 每次迭代后记录成本值,供后续分析。
    def optimize(w, b, X, Y, num_iterations, learning_rate, print_cost = False):
        """
        使用梯度下降法优化 w 和 b
        
        参数:
        w -- 权重,形状为 (num_px * num_px * 3, 1)
        b -- 偏置,标量
        X -- 数据矩阵,形状为 (num_px * num_px * 3, m)
        Y -- 实际标签向量,形状为 (1, m)
        num_iterations -- 优化的迭代次数
        learning_rate -- 学习率
        print_cost -- 是否在每100次迭代后打印成本,布尔值
        
        返回:
        params -- 包含权重 w 和偏置 b 的字典
        grads -- 包含权重梯度 dw 和偏置梯度 db 的字典
        costs -- 每次迭代的成本值列表
        """
        
        costs = []
        
        for i in range(num_iterations):
            
            # 计算成本和梯度
            grads, cost = propagate(w, b, X, Y)
            
            # 从 grads 字典中提取 dw 和 db
            dw = grads["dw"]
            db = grads["db"]
            
            # 更新参数
            w = w - learning_rate * dw
            b = b - learning_rate * db
            
            # 记录成本值
            if i % 100 == 0:
                costs.append(cost)
            
            # 打印成本值
            if print_cost and i % 100 == 0:
                print(f"Cost after iteration {i}: {cost}")
        
        params = {"w": w,
                  "b": b}
        
        grads = {"dw": dw,
                 "db": db}
        
        return params, grads, costs
    '''
    循环结构:for 循环用来执行指定次数的迭代。
    前向传播和反向传播:在每次迭代中,调用之前定义的 propagate 函数来计算当前的成本和梯度。
    参数更新:使用梯度下降法更新参数𝑤和𝑏。
    记录成本:每100次迭代后记录一次成本值,以便分析学习过程的变化情况。
    打印成本:如果 print_cost 为 True,则每100次迭代后打印成本值。
    这个 optimize 函数将学习到的参数和梯度返回,同时还会返回一个记录了每100次迭代后成本值的列表。
    '''

步骤七

predict 函数

在前面的步骤中,你已经得到了学习到的参数w和b。现在,你可以使用这些参数对数据集X进行预测。

代码实现:

def predict(w, b, X):
    """
    使用学习到的参数 w 和 b 预测标签
    
    参数:
    w -- 权重,形状为 (num_px * num_px * 3, 1)
    b -- 偏置,标量
    X -- 数据矩阵,形状为 (num_px * num_px * 3, m)
    
    返回:
    Y_prediction -- 包含X中所有图片的预测(0/1)的向量,形状为 (1, m)
    """
    
    m = X.shape[1]
    Y_prediction = np.zeros((1, m))
    
    # 计算激活值 A
    A = sigmoid(np.dot(w.T, X) + b)
    
    # 将激活值转换为 0 或 1
    Y_prediction = np.where(A > 0.5, 1, 0)
    
    return Y_prediction


第三周练习总结

步骤一

可以使用 numpy 数组的 shape 属性。shape 属性提供了数组的维度,这可以帮助你了解训练样本的数量,以及特征和标签的形状。

# 假设 X 和 Y 是你的 numpy 数组
m = X.shape[0]  # 训练样本的数量
n = X.shape[1]  # 特征的数量 (假设 X 是 2D 数组)

print("训练样本的数量:", m)
print("X 的形状:", X.shape)
print("Y 的形状:", Y.shape)

步骤二

layer_sizes

需要定义神经网络的层大小。以下是每个变量的含义以及如何定义它们:

  1. n_x: 输入层的大小,即输入特征的数量。
  2. n_h: 隐藏层的大小。在这个练习中,隐层的大小被指定为 4。
  3. n_y: 输出层的大小,即输出类别的数量。 
# 假设 X 和 Y 是已经定义好的 numpy 数组

n_x = X.shape[0]  # 输入层的大小,即输入特征的数量
n_h = 4           # 隐藏层的大小(在这个练习中被硬编码为4)
n_y = Y.shape[0]  # 输出层的大小,即输出类别的数量

 步骤三

initialize_parameters() 初始化参数

你需要实现一个函数 initialize_parameters() 来初始化神经网络的参数。这个函数将为每一层的权重矩阵和偏置向量分配初始值

实现步骤:
  1. 权重矩阵 W:使用 np.random.randn(a, b) * 0.01 生成形状为 (a, b) 的随机矩阵,并乘以 0.01 以保持值较小,防止梯度消失或爆炸。
  2. 偏置向量 b:使用 np.zeros((a, b)) 初始化形状为 (a, b) 的零矩阵。
    def initialize_parameters(n_x, n_h, n_y):
        """
        参数:
        n_x -- 输入层的神经元数量
        n_h -- 隐藏层的神经元数量
        n_y -- 输出层的神经元数量
    
        返回:
        parameters -- 包含权重矩阵 W1, W2 和偏置向量 b1, b2 的字典
        """
        
        np.random.seed(1)  # 为了确保结果的可重复性,可以设置一个随机种子
        
        # 初始化权重矩阵和偏置向量
        W1 = np.random.randn(n_h, n_x) * 0.01
        b1 = np.zeros((n_h, 1))
        W2 = np.random.randn(n_y, n_h) * 0.01
        b2 = np.zeros((n_y, 1))
        
        # 将参数存储在字典中
        parameters = {
            "W1": W1,
            "b1": b1,
            "W2": W2,
            "b2": b2
        }
        
        return parameters

步骤四

forward_propagation() 函数

你需要实现 forward_propagation() 函数,该函数通过前向传播计算神经网络的输出。以下是实现步骤和代码示例:

实现步骤:
  1. 从参数字典中获取权重矩阵和偏置向量

    • 使用 parameters["W1"] 获取第一层的权重矩阵 W1
    • 使用 parameters["b1"] 获取第一层的偏置向量 b1
    • 使用 parameters["W2"] 获取第二层的权重矩阵 W2
    • 使用 parameters["b2"] 获取第二层的偏置向量 b2
  2. 前向传播

    • 计算第一层的线性部分:Z[1] = W[1] * X + b[1]
    • 计算第一层的激活值:A[1] = tanh(Z[1])
    • 计算第二层的线性部分:Z[2] = W[2] * A[1] + b[2]
    • 计算输出层的激活值:A[2] = sigmoid(Z[2]),这就是预测值 Y_hat
  3. 将中间计算结果存储在缓存字典 cache,以便在反向传播时使用。

    def forward_propagation(X, parameters):
        """
        参数:
        X -- 输入数据,形状为 (n_x, m)
        parameters -- 包含参数 "W1", "b1", "W2", "b2" 的字典
    
        返回:
        A2 -- 使用 sigmoid() 函数的输出
        cache -- 包含 "Z1", "A1", "Z2", "A2" 的字典
        """
    
        # 从字典中检索参数
        W1 = parameters["W1"]
        b1 = parameters["b1"]
        W2 = parameters["W2"]
        b2 = parameters["b2"]
        
        # 实现前向传播
        Z1 = np.dot(W1, X) + b1  # 第1层线性部分
        A1 = np.tanh(Z1)         # 第1层激活值
        Z2 = np.dot(W2, A1) + b2  # 第2层线性部分
        A2 = sigmoid(Z2)          # 第2层激活值,也就是预测值 Y_hat
        
        # 将结果存入缓存以备后续使用
        cache = {
            "Z1": Z1,
            "A1": A1,
            "Z2": Z2,
            "A2": A2
        }
        
        return A2, cache

步骤五

compute_cost() 函数计算成本J

在这个练习中,你需要实现 compute_cost() 函数,该函数计算成本函数 J。成本函数用于评估模型预测的准确性,通常在二元分类中使用交叉熵损失函数。 

 代码:

def compute_cost(A2, Y):
    """
    参数:
    A2 -- 输出层的激活值,即预测值,形状为 (1, m)
    Y -- 实际标签向量,形状为 (1, m)
    
    返回:
    cost -- 成本函数的值
    """

    m = Y.shape[1]  # 样本数量
    
    # 计算交叉熵损失
    logprobs = np.multiply(np.log(A2), Y) + np.multiply(np.log(1 - A2), 1 - Y)
    cost = - np.sum(logprobs) / m
    
    # 确保成本是标量
    cost = np.squeeze(cost) 
    
    return cost

步骤六

backward_propagation

在这一步,你需要实现反向传播(backward_propagation)函数。反向传播是深度学习中最复杂的部分之一,用于计算神经网络的梯度,从而更新模型的参数。

 代码:

def backward_propagation(parameters, cache, X, Y):
    """
    参数:
    parameters -- 字典,包含了W1, W2, b1, b2
    cache -- 字典,包含了Z1, A1, Z2, A2
    X -- 输入数据集,形状为 (n_x, m)
    Y -- 标签,形状为 (1, m)
    
    返回:
    grads -- 包含了 dW1, db1, dW2, db2 的字典
    """
    
    m = X.shape[1]
    
    # 从 cache 中获取 Z1, A1, Z2, A2
    A1 = cache['A1']
    A2 = cache['A2']
    Z1 = cache['Z1']
    
    # 计算 dZ2
    dZ2 = A2 - Y
    
    # 计算 dW2 和 db2
    dW2 = (1 / m) * np.dot(dZ2, A1.T)
    db2 = (1 / m) * np.sum(dZ2, axis=1, keepdims=True)
    
    # 计算 dZ1
    dZ1 = np.dot(parameters['W2'].T, dZ2) * (1 - np.power(A1, 2))
    
    # 计算 dW1 和 db1
    dW1 = (1 / m) * np.dot(dZ1, X.T)
    db1 = (1 / m) * np.sum(dZ1, axis=1, keepdims=True)
    
    # 将梯度存储在字典 grads 中
    grads = {"dW1": dW1,
             "db1": db1,
             "dW2": dW2,
             "db2": db2}
    
    return grads

步骤七

更新参数

在这一步中,你需要实现参数更新函数 update_parameters(),使用梯度下降法来更新神经网络的参数。这意味着你将使用反向传播过程中计算出的梯度(dW1, db1, dW2, db2)来调整权重和偏置(W1, b1, W2, b2

代码: 

def update_parameters(parameters, grads, learning_rate):
    """
    参数:
    parameters -- 字典,包含了参数 W1, b1, W2, b2
    grads -- 字典,包含了梯度 dW1, db1, dW2, db2
    learning_rate -- 学习率,用于控制梯度下降步长
    
    返回:
    parameters -- 更新后的参数字典
    """
    
    # 从参数字典中提取参数
    W1 = parameters["W1"]
    b1 = parameters["b1"]
    W2 = parameters["W2"]
    b2 = parameters["b2"]
    
    # 从梯度字典中提取梯度
    dW1 = grads["dW1"]
    db1 = grads["db1"]
    dW2 = grads["dW2"]
    db2 = grads["db2"]
    
    # 更新参数
    W1 = W1 - learning_rate * dW1
    b1 = b1 - learning_rate * db1
    W2 = W2 - learning_rate * dW2
    b2 = b2 - learning_rate * db2
    
    # 将更新后的参数存储回字典中
    parameters = {"W1": W1,
                  "b1": b1,
                  "W2": W2,
                  "b2": b2}
    
    return parameters

步骤八

nn_model() 函数

nn_model() 函数中,你将构建并训练神经网络模型。这个函数需要将之前实现的函数组合起来,以实现整个模型的训练过程。下面是构建神经网络模型的步骤:

步骤概述

  1. 初始化参数:使用 initialize_parameters() 初始化权重和偏置。
  2. 前向传播:使用 forward_propagation() 计算预测值。
  3. 计算成本:使用 compute_cost() 计算成本函数。
  4. 反向传播:使用 backward_propagation() 计算梯度。
  5. 更新参数:使用 update_parameters() 更新模型参数。
  6. 重复迭代:在多次迭代中执行前向传播、计算成本、反向传播和更新参数,以优化模型。
def nn_model(X, Y, n_h, num_iterations, learning_rate, print_cost=False):
    """
    实现一个神经网络模型: 隐藏层有 n_h 个神经元
    
    参数:
    X -- 输入数据,形状为 (输入特征数量, 样本数量)
    Y -- 标签,形状为 (输出层神经元数量, 样本数量)
    n_h -- 隐藏层中的神经元数量
    num_iterations -- 迭代次数
    learning_rate -- 学习率
    print_cost -- 每 100 次迭代打印一次成本
    
    返回:
    parameters -- 更新后的模型参数
    """
    
    # 确定输入和输出层的维度
    n_x = X.shape[0]
    n_y = Y.shape[0]
    
    # 初始化参数
    parameters = initialize_parameters(n_x, n_h, n_y)
    
    for i in range(num_iterations):
        # 前向传播
        A2, cache = forward_propagation(X, parameters)
        
        # 计算成本
        cost = compute_cost(A2, Y)
        
        # 反向传播
        grads = backward_propagation(parameters, cache, X, Y)
        
        # 更新参数
        parameters = update_parameters(parameters, grads, learning_rate)
        
        # 打印成本
        if print_cost and i % 100 == 0:
            print(f"Cost after iteration {i}: {cost}")
    
    return parameters

步骤九

预测

实现步骤

  1. 使用前向传播计算预测值:使用训练好的参数进行前向传播,计算每个样本的激活值。
  2. 应用阈值:对激活值应用阈值判断,将激活值大于 0.5 的位置设为 1,否则为 0。
  3. 返回预测结果:返回一个与输入 X 维度相同的预测结果矩阵。
    def predict(parameters, X):
        """
        使用训练好的参数来预测标签
        
        参数:
        parameters -- 包含权重和偏置的字典
        X -- 输入数据,形状为 (输入特征数量, 样本数量)
        
        返回:
        predictions -- 预测结果的向量 (0/1)
        """
        
        # 前向传播
        A2, cache = forward_propagation(X, parameters)
        
        # 将激活值大于0.5的值设为1,其他设为0
        predictions = (A2 > 0.5)
        
        return predictions


第四周练习总结

步骤一

初始化参数

创建并初始化 2 层神经网络的参数。

指示:

模型的结构是:线性 -> ReLU -> 线性 -> Sigmoid。

  • 对于权重矩阵使用随机初始化:np.random.randn(d0, d1, ..., dn) * 0.01,其中 d0, d1, ..., dn 是正确的形状。有关 np.random.randn 的文档,请参阅。
  • 对于偏置使用零初始化:np.zeros(shape)。有关 np.zeros 的文档,请参阅。
import numpy as np

def initialize_parameters(n_x, n_h, n_y):
    """
    初始化 2 层神经网络的参数:
    线性 -> ReLU -> 线性 -> Sigmoid。

    参数:
    n_x -- 输入特征的数量
    n_h -- 隐藏层单元的数量
    n_y -- 输出单元的数量

    返回:
    parameters -- 包含以下内容的字典:
                    W1 -- 形状为 (n_h, n_x) 的权重矩阵
                    b1 -- 形状为 (n_h, 1) 的偏置向量
                    W2 -- 形状为 (n_y, n_h) 的权重矩阵
                    b2 -- 形状为 (n_y, 1) 的偏置向量
    """
    np.random.seed(1)  # 设定随机种子以确保可重复性

    W1 = np.random.randn(n_h, n_x) * 0.01
    b1 = np.zeros((n_h, 1))
    W2 = np.random.randn(n_y, n_h) * 0.01
    b2 = np.zeros((n_y, 1))

    parameters = {
        "W1": W1,
        "b1": b1,
        "W2": W2,
        "b2": b2
    }

    return parameters

步骤二

初始化深度神经网络的参数

实现 L 层神经网络的初始化

指示:

模型的结构是 [线性 -> ReLU] × (L-1) -> 线性 -> Sigmoid。即,它有 L−1层使用 ReLU 激活函数,后跟一个使用 Sigmoid 激活函数的输出层。

  • 对于权重矩阵使用随机初始化。使用 np.random.randn(d0, d1, ..., dn) * 0.01
  • 对于偏置使用零初始化。使用 np.zeros(shape)

你将把每一层的单元数量存储在 layer_dims 变量中。例如,上周的 Planar Data 分类模型的 layer_dims 是 [2,4,1]:表示有两个输入,一个隐藏层有 4 个单元和一个输出层有 1 个单元。这意味着 W1 的形状是 (4,2),b1 的形状是 (4,1),W2 的形状是 (1,4),b2 的形状是 (1,1)。现在你需要将其推广到 L 层!

以下是 L=1L=1L=1(一层神经网络)的实现。它应该启发你实现一般情况(L 层神经网络)的代码。

import numpy as np

def initialize_parameters_deep(layer_dims):
    """
    初始化一个 L 层深度神经网络的参数。

    参数:
    layer_dims -- 一个包含每一层单元数量的列表。例:[2, 4, 1] 表示一个具有两个输入单元,四个隐藏单元和一个输出单元的网络。

    返回:
    parameters -- 包含所有权重矩阵和偏置向量的字典
    """
    np.random.seed(1)  # 设定随机种子以确保可重复性
    parameters = {}
    L = len(layer_dims) - 1  # 神经网络的层数(减去输入层)

    for l in range(1, L + 1):
        parameters["W" + str(l)] = np.random.randn(layer_dims[l], layer_dims[l-1]) * 0.01
        parameters["b" + str(l)] = np.zeros((layer_dims[l], 1))

    return parameters

步骤三

线性前向传播

构建前向传播中的线性部分

提示:
  • 数学表示为:Z[l]=W[l]⋅A[l−1]+b[l]
  • np.dot() 可以帮助进行矩阵乘法。如果维度不匹配,打印 W.shape 可能有用。
实现步骤:
  1. 从参数字典中提取权重矩阵和偏置向量。
  2. 计算线性部分: 使用矩阵乘法 np.dot() 计算W[l]⋅A[l−1],然后加上偏置 b[l]。
  3. 返回计算结果和缓存: 缓存A[l−1]和Z[l]以便后续使用。

下面是实现 linear_forward() 的代码:

import numpy as np

def linear_forward(A_prev, W, b):
    """
    实现线性前向传播。

    参数:
    A_prev -- 前一层的激活值,形状为 (n_prev, m)
    W -- 当前层的权重矩阵,形状为 (n, n_prev)
    b -- 当前层的偏置向量,形状为 (n, 1)

    返回:
    Z -- 当前层的线性部分,形状为 (n, m)
    cache -- 一个包含 (A_prev, W, b) 的元组,用于反向传播
    """
    Z = np.dot(W, A_prev) + b
    cache = (A_prev, W, b)
    return Z, cache

步骤四

线性激活前向传播

实现 LINEAR -> ACTIVATION 层的前向传播

数学表示:

A[l]=g(Z[l])=g(W[l]⋅A[l−1]+b[l])

其中激活函数 g可以是 sigmoid()relu()

实现步骤:
  1. 调用 linear_forward(): 获取线性部分Z[l]和缓存。
  2. 应用激活函数: 对 Z[l]使用激活函数g(sigmoid()relu())。
  3. 返回结果: 返回激活值 A[l]和缓存(包含线性部分的缓存)。

代码:

import numpy as np

def sigmoid(Z):
    """
    实现 sigmoid 激活函数
    """
    return 1 / (1 + np.exp(-Z))

def relu(Z):
    """
    实现 ReLU 激活函数
    """
    return np.maximum(0, Z)

def linear_activation_forward(A_prev, W, b, activation):
    """
    实现 LINEAR -> ACTIVATION 前向传播。

    参数:
    A_prev -- 前一层的激活值,形状为 (n_prev, m)
    W -- 当前层的权重矩阵,形状为 (n, n_prev)
    b -- 当前层的偏置向量,形状为 (n, 1)
    activation -- 激活函数的名称,"sigmoid" 或 "relu"

    返回:
    A -- 当前层的激活值,形状为 (n, m)
    cache -- 包含线性部分的缓存 (A_prev, W, b, Z)
    """
    Z, linear_cache = linear_forward(A_prev, W, b)

    if activation == "sigmoid":
        A = sigmoid(Z)
    elif activation == "relu":
        A = relu(Z)
    else:
        raise ValueError("激活函数必须是 'sigmoid' 或 'relu'")

    activation_cache = Z
    cache = (linear_cache, activation_cache)
    return A, cache

步骤五

L 层模型前向传播

实现上述模型的前向传播

说明:
  • 在代码中,变量 AL 将表示 A[L]=σ(Z[L])=σ(W[L]A[L−1]+b[L])。这有时也称为Y^。
实现步骤:
  1. 初始化:

    • 从参数字典中获取权重矩阵W 和偏置向量 b。
    • 获取层数 L和各层的尺寸。
  2. 前向传播:

    • 使用循环进行 [LINEAR -> RELU] 操作 (L−1)次。
    • 在每次循环中调用 linear_activation_forward() 函数。
    • 在最后一层应用 sigmoid() 激活函数。
  3. 缓存:

    • 保存每层的缓存信息,以便在反向传播中使用。
def L_model_forward(X, parameters):
    """
    实现 L 层神经网络的前向传播。

    参数:
    X -- 输入数据,形状为 (n_x, m)
    parameters -- 包含所有参数的字典

    返回:
    AL -- 最后一层的激活值,形状为 (1, m)
    caches -- 包含每层的缓存信息的列表
    """
    caches = []
    A = X
    L = len(parameters) // 2  # L 层神经网络中的层数

    # [LINEAR -> RELU] * (L-1) 层
    for l in range(1, L):
        A_prev = A
        A, cache = linear_activation_forward(A_prev, parameters["W" + str(l)], parameters["b" + str(l)], activation="relu")
        caches.append(cache)

    # 最后一层 [LINEAR -> SIGMOID]
    AL, cache = linear_activation_forward(A, parameters["W" + str(L)], parameters["b" + str(L)], activation="sigmoid")
    caches.append(cache)

    return AL, caches

步骤六

计算代价函数

import numpy as np

def compute_cost(AL, Y):
    """
    计算交叉熵代价函数 J。

    参数:
    AL -- 最后一层的激活值 (预测值),形状为 (1, m)
    Y -- 实际标签,形状为 (1, m)

    返回:
    cost -- 计算得到的代价
    """
    m = Y.shape[1]  # 样本数量

    # 计算代价
    cost = -np.sum(np.multiply(Y, np.log(AL)) + np.multiply(1 - Y, np.log(1 - AL))) / m
    
    return cost

步骤七

实现 linear_backward

在反向传播中,我们需要计算线性部分的梯度。具体来说,给定损失函数相对于线性层输出 Z 的梯度,我们需要计算权重 W和偏置 b的梯度,并更新它们。这里是如何计算这些梯度的步骤。

import numpy as np

def linear_backward(dZ, A_prev, W, b):
    """
    实现线性部分的反向传播

    参数:
    dZ -- 当前层的梯度,形状为 (n[l], m)
    A_prev -- 前一层的激活值,形状为 (n[l-1], m)
    W -- 当前层的权重,形状为 (n[l], n[l-1])
    b -- 当前层的偏置,形状为 (n[l], 1)

    返回:
    dA_prev -- 前一层的梯度,形状为 (n[l-1], m)
    dW -- 当前层的权重梯度,形状为 (n[l], n[l-1])
    db -- 当前层的偏置梯度,形状为 (n[l], 1)
    """
    m = A_prev.shape[1]  # 样本数

    # 计算梯度
    dW = np.dot(dZ, A_prev.T) / m
    db = np.sum(dZ, axis=1, keepdims=True) / m
    dA_prev = np.dot(W.T, dZ)

    return dA_prev, dW, db

步骤八

实现 linear_activation_backward

linear_activation_backward 中,我们将结合线性部分和激活函数的反向传播。具体来说,我们需要计算经过激活函数后的梯度,并将其传递到线性部分的反向传播函数

步骤:
  1. 计算激活函数的梯度:

    • 根据激活函数的不同(例如 ReLU 或 Sigmoid),我们需要计算其导数(即梯度)。
  2. 调用 linear_backward:

    • 使用激活函数的梯度和线性部分的反向传播函数来计算权重和偏置的梯度。

代码: 

import numpy as np

def sigmoid_backward(dA, Z):
    """
    实现 sigmoid 激活函数的反向传播

    参数:
    dA -- 激活函数的梯度,形状为 (n[l], m)
    Z -- 当前层的线性激活值,形状为 (n[l], m)

    返回:
    dZ -- 当前层的线性部分梯度,形状为 (n[l], m)
    """
    A = sigmoid(Z)  # 使用激活函数的前向传播计算激活值
    dZ = dA * A * (1 - A)
    return dZ

def relu_backward(dA, Z):
    """
    实现 ReLU 激活函数的反向传播

    参数:
    dA -- 激活函数的梯度,形状为 (n[l], m)
    Z -- 当前层的线性激活值,形状为 (n[l], m)

    返回:
    dZ -- 当前层的线性部分梯度,形状为 (n[l], m)
    """
    dZ = np.array(dA, copy=True)  # 复制 dA 以避免修改原始数据
    dZ[Z <= 0] = 0
    return dZ

def linear_activation_backward(dA, cache, activation):
    """
    实现 LINEAR->ACTIVATION 层的反向传播

    参数:
    dA -- 当前层的梯度,形状为 (n[l], m)
    cache -- 前向传播中的缓存,包含 Z 和 A_prev
    activation -- 激活函数类型,"sigmoid" 或 "relu"

    返回:
    dA_prev -- 前一层的梯度,形状为 (n[l-1], m)
    dW -- 当前层的权重梯度,形状为 (n[l], n[l-1])
    db -- 当前层的偏置梯度,形状为 (n[l], 1)
    """
    A_prev, Z = cache

    if activation == "sigmoid":
        dZ = sigmoid_backward(dA, Z)
    elif activation == "relu":
        dZ = relu_backward(dA, Z)
    else:
        raise ValueError("Unsupported activation function")

    dA_prev, dW, db = linear_backward(dZ, A_prev, W, b)

    return dA_prev, dW, db

 步骤九

为了实现 L_model_backward,你需要在多层神经网络中执行反向传播。该网络的结构是 [LINEAR -> RELU] × (L-1) -> LINEAR -> SIGMOID。通过反向传播计算每层的梯度,并将这些梯度存储在字典 grads 中。

实现步骤

  1. 初始化 grads 字典:用于存储每一层的梯度。
  2. 计算输出层的梯度:首先计算最后一层的激活值 AL 对应的损失函数的导数 dAL
  3. 反向传播输出层 (LINEAR -> SIGMOID)
    • 使用 linear_activation_backward 函数来计算输出层的 dAdWdb
    • 存储这些梯度到 grads 字典中。
  4. 反向传播隐藏层 (LINEAR -> RELU)
    • 从倒数第二层开始,逐层向前反向传播。
    • 使用 linear_activation_backward 函数来计算每一层的 dAdWdb
    • 存储这些梯度到 grads 字典中。
  5. 返回 grads 字典:返回存储所有梯度的字典。
# GRADED FUNCTION: L_model_backward

def L_model_backward(AL, Y, caches):
    """
    Implement the backward propagation for the [LINEAR->RELU] * (L-1) -> LINEAR -> SIGMOID group
    
    Arguments:
    AL -- probability vector, output of the forward propagation (L_model_forward())
    Y -- true "label" vector (containing 0 if non-cat, 1 if cat)
    caches -- list of caches containing:
                every cache of linear_activation_forward() with "relu" (it's caches[l], for l in range(L-1) i.e l = 0...L-2)
                the cache of linear_activation_forward() with "sigmoid" (it's caches[L-1])
    
    Returns:
    grads -- A dictionary with the gradients
             grads["dA" + str(l)] = ... 
             grads["dW" + str(l)] = ...
             grads["db" + str(l)] = ... 
    """
    grads = {}
    L = len(caches)  # the number of layers
    m = AL.shape[1]
    Y = Y.reshape(AL.shape)  # after this line, Y is the same shape as AL
    
    # Initializing the backpropagation
    dAL = - (np.divide(Y, AL) - np.divide(1 - Y, 1 - AL))
    
    # Lth layer (SIGMOID -> LINEAR) gradients. Inputs: "dAL, current_cache". Outputs: "grads["dAL-1"], grads["dWL"], grads["dbL"]
    current_cache = caches[L-1]
    dA_prev_temp, dW_temp, db_temp = linear_activation_backward(dAL, current_cache, activation="sigmoid")
    grads["dA" + str(L-1)] = dA_prev_temp
    grads["dW" + str(L)] = dW_temp
    grads["db" + str(L)] = db_temp
    
    # Loop from l=L-2 to l=0
    for l in reversed(range(L-1)):
        # lth layer: (RELU -> LINEAR) gradients.
        # Inputs: "grads["dA" + str(l + 1)], current_cache". Outputs: "grads["dA" + str(l)] , grads["dW" + str(l + 1)] , grads["db" + str(l + 1)]"
        current_cache = caches[l]
        dA_prev_temp, dW_temp, db_temp = linear_activation_backward(grads["dA" + str(l + 1)], current_cache, activation="relu")
        grads["dA" + str(l)] = dA_prev_temp
        grads["dW" + str(l + 1)] = dW_temp
        grads["db" + str(l + 1)] = db_temp

    return grads

步骤十

更新参数

# GRADED FUNCTION: update_parameters

def update_parameters(parameters, grads, learning_rate):
    """
    Update parameters using gradient descent
    
    Arguments:
    parameters -- python dictionary containing your parameters 
    grads -- python dictionary containing your gradients, output of L_model_backward
    learning_rate -- the learning rate, scalar
    
    Returns:
    parameters -- python dictionary containing your updated parameters 
                  parameters["W" + str(l)] = ... 
                  parameters["b" + str(l)] = ...
    """
    
    L = len(parameters) // 2  # number of layers in the neural network

    # Update rule for each parameter
    for l in range(L):
        parameters["W" + str(l+1)] = parameters["W" + str(l+1)] - learning_rate * grads["dW" + str(l+1)]
        parameters["b" + str(l+1)] = parameters["b" + str(l+1)] - learning_rate * grads["db" + str(l+1)]
        
    return parameters

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值