本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本课程围绕Andrew Ng教授广受欢迎的机器学习课程展开,重点讲解编程作业5(Ex5)中的核心内容。该作业深入探讨神经网络与反向传播算法,涵盖从神经网络基础、损失函数设计到梯度检查、训练验证、正则化与测试评估的完整实现流程。通过该作业实践,学习者可以加深对深度学习基础的理解,掌握神经网络的构建与调优方法,为后续高级机器学习课题打下坚实基础。
andrew ng machine learning编程作业代码5

1. Andrew Ng机器学习课程概述

Andrew Ng在Coursera平台上开设的机器学习课程,是全球范围内最受欢迎的入门级机器学习课程之一。该课程以理论与实践并重的方式,系统讲解了机器学习的核心算法与应用技巧。课程内容涵盖监督学习、非监督学习、神经网络基础以及优化算法等关键主题。

编程作业5作为课程的核心实践环节之一,引导学习者实现一个完整的神经网络模型,涵盖前馈计算、反向传播、参数更新等关键流程。通过该作业,学员不仅能掌握机器学习项目的开发流程,还能理解工业级模型实现的基本范式。

2. 神经网络基础结构设计与实现

神经网络是深度学习的核心,它模仿人类大脑处理信息的方式,通过层层结构对数据进行抽象与建模。在本章中,我们将从神经网络的基本构成入手,逐步讲解输入层、隐藏层与输出层的构建逻辑,并深入探讨激活函数的选择与实现。随后,我们将结合编程作业5的实际需求,介绍如何在Octave/Matlab中实现前馈计算,并通过向量化编程优化计算效率。最后,我们将围绕代码结构与模块化设计展开讨论,包括函数划分、接口设计、参数初始化以及权重范围设定等内容,为后续的反向传播与训练打下坚实基础。

2.1 神经网络的基本构成

神经网络由多个层级组成,每一层都承担着不同的功能。理解每一层的职责是构建神经网络模型的第一步。

2.1.1 输入层、隐藏层与输出层的作用

神经网络的结构通常由三层组成: 输入层(Input Layer) 隐藏层(Hidden Layer) 输出层(Output Layer) 。它们各自的功能如下:

层级 功能描述
输入层 接收原始数据输入,通常不进行计算,仅作为数据传递的入口。
隐藏层 对输入数据进行特征提取和非线性变换,是模型学习能力的核心所在。
输出层 输出模型的预测结果,根据任务类型(如分类或回归)决定输出形式。

以编程作业5为例,该作业要求构建一个用于手写数字识别的神经网络,输入层接收400维的图像特征(20×20像素图像),隐藏层使用25个神经元,输出层则对应10个数字类别(0~9)。

神经网络的层级结构可以用以下 mermaid 流程图进行直观展示:

graph TD
    A[输入层 400维] --> B[隐藏层 25个神经元]
    B --> C[输出层 10个神经元]

2.1.2 激活函数的选择与实现

神经网络之所以具有强大的非线性建模能力,关键在于 激活函数(Activation Function) 的引入。激活函数决定了一个神经元是否被激活,并影响模型的表达能力。

常见的激活函数有:

激活函数 表达式 特点
Sigmoid $ \sigma(x) = \frac{1}{1+e^{-x}} $ 输出范围为(0,1),适用于二分类输出,但存在梯度消失问题。
Tanh $ \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} $ 输出范围为(-1,1),比Sigmoid更中心化,但同样存在梯度消失。
ReLU $ \text{ReLU}(x) = \max(0, x) $ 简单高效,缓解梯度消失问题,广泛用于隐藏层。

在编程作业5中,隐藏层通常使用 Sigmoid函数 作为激活函数,其定义如下:

function g = sigmoid(z)
    g = 1 ./ (1 + exp(-z));
end

逐行分析:

  • 第1行:定义函数 sigmoid ,输入为 z
  • 第2行:使用向量化方式计算Sigmoid函数, exp(-z) 计算指数函数, 1 ./ 实现矩阵点除运算,最终输出激活值。

参数说明:
- z :输入可以是标量、向量或矩阵,支持向量化运算。
- g :输出与 z 具有相同维度的激活值矩阵。

激活函数在神经网络中起到非线性映射的作用,使得网络能够学习复杂的函数关系。如果省略激活函数,神经网络将退化为线性模型。

2.2 前馈计算的实现

神经网络的 前馈传播(Feedforward) 是指数据从输入层依次经过隐藏层最终到达输出层的过程。该过程是模型预测的核心步骤,也是后续反向传播的前置条件。

2.2.1 矩阵运算在神经网络中的应用

神经网络中的计算通常以矩阵运算为主,这样可以充分利用现代计算平台的向量化优势,提升运算效率。

在编程作业5中,前馈计算的数学表达如下:

  • 输入层:$ a^{(1)} = x $
  • 隐藏层计算:
    $$
    z^{(2)} = \Theta^{(1)} a^{(1)} \quad , \quad a^{(2)} = \sigma(z^{(2)})
    $$
  • 输出层计算:
    $$
    z^{(3)} = \Theta^{(2)} a^{(2)} \quad , \quad a^{(3)} = \sigma(z^{(3)})
    $$

其中:

  • $ a^{(l)} $:第 $ l $ 层的激活值;
  • $ \Theta^{(l)} $:第 $ l $ 层的权重矩阵;
  • $ \sigma $:Sigmoid激活函数。

在Octave/Matlab中,可以通过矩阵乘法实现这一过程。以下是一个简化的前馈计算函数示例:

function [a3] = feedforward(X, Theta1, Theta2)
    m = size(X, 1);  % 样本数量
    a1 = [ones(m, 1) X];  % 添加偏置项
    % 计算隐藏层
    z2 = a1 * Theta1';
    a2 = sigmoid(z2);
    a2 = [ones(m, 1) a2];  % 添加偏置项
    % 计算输出层
    z3 = a2 * Theta2';
    a3 = sigmoid(z3);
end

逐行分析:

  • 第1行:定义函数 feedforward ,输入样本 X 和两组权重 Theta1 Theta2
  • 第2行:获取样本数量 m
  • 第3行:在输入层添加偏置项(1列全为1的向量),以便后续矩阵运算。
  • 第6~7行:计算隐藏层的加权输入 z2 ,并通过 sigmoid 函数得到激活值 a2
  • 第8行:同样在隐藏层添加偏置项。
  • 第11~12行:计算输出层结果 a3 ,即最终预测值。

参数说明:
- X :输入样本矩阵,每行是一个样本;
- Theta1 :输入层到隐藏层的权重矩阵;
- Theta2 :隐藏层到输出层的权重矩阵;
- a3 :输出层的预测结果,维度为 m x 10 (对应10个类别)。

2.2.2 向量化编程的实现方法

向量化编程是Octave/Matlab中提升效率的关键。相比传统的 for 循环,矩阵运算可以一次性处理大量数据,显著提高性能。

以计算 z2 = a1 * Theta1' 为例,假设 a1 的维度是 m x 401 Theta1 25 x 401 ,那么:

  • a1 * Theta1' 的结果是 m x 25 ,表示每个样本在隐藏层的加权输入;
  • 这一操作在Matlab中只需一行代码即可完成,而如果使用循环则需要嵌套两个 for 循环,效率低下。

因此,在神经网络的实现中,应尽可能使用向量化方式,避免显式循环。

2.3 代码结构与模块化设计

一个良好的神经网络实现不仅要求功能正确,还需要具备清晰的代码结构和模块化设计,便于调试与维护。

2.3.1 函数划分与接口设计

在编程作业5中,推荐将代码划分为多个独立函数,每个函数完成单一功能,降低耦合度。常见的函数划分如下:

函数名称 功能描述
sigmoid.m 实现Sigmoid激活函数
feedforward.m 实现前馈传播计算
nnCostFunction.m 实现损失函数与梯度计算
randInitializeWeights.m 实现权重初始化
predict.m 实现模型预测

randInitializeWeights.m 为例,其实现如下:

function W = randInitializeWeights(L_in, L_out)
    epsilon_init = 0.12;
    W = rand(L_out, 1 + L_in) * 2 * epsilon_init - epsilon_init;
end

逐行分析:

  • 第1行:定义函数 randInitializeWeights ,输入为前一层神经元数 L_in 和当前层神经元数 L_out
  • 第2行:设置初始化范围 epsilon_init
  • 第3行:生成一个 L_out x (L_in + 1) 的随机矩阵,并将其缩放到 $[- \epsilon, \epsilon]$ 范围内。

参数说明:
- L_in :前一层神经元数量;
- L_out :当前层神经元数量;
- W :返回初始化后的权重矩阵。

该函数通过随机初始化权重,避免所有权重初始值相同导致的“对称性”问题,从而提高模型训练效果。

2.3.2 参数初始化与权重范围设定

权重初始化对神经网络的训练至关重要。不当的初始化会导致梯度消失或爆炸,影响模型收敛。

常见的初始化方法包括:

方法 描述
零初始化 所有权重初始化为0,会导致对称性问题,不推荐。
随机初始化 权重初始化为小范围随机数,如上面的 randInitializeWeights 函数所示。
Xavier初始化 根据输入输出神经元数量调整初始化范围,适用于Sigmoid/Tanh激活函数。
He初始化 适用于ReLU激活函数,初始化范围更大,防止神经元死亡。

在编程作业5中,推荐使用 随机初始化 ,其范围通常设定为 $[-\epsilon, \epsilon]$,其中 $\epsilon$ 取值为0.12左右。

例如,在训练前可以这样初始化权重:

initial_Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size);
initial_Theta2 = randInitializeWeights(hidden_layer_size, num_labels);

这段代码为输入层到隐藏层、隐藏层到输出层分别初始化了权重矩阵。

参数说明:
- input_layer_size :输入层神经元数量(如400);
- hidden_layer_size :隐藏层神经元数量(如25);
- num_labels :输出层神经元数量(如10);
- initial_Theta1 initial_Theta2 :分别为两层的初始权重矩阵。

良好的权重初始化有助于模型更快地收敛,并避免陷入局部最优。

本章系统讲解了神经网络的基本结构设计与实现方式,包括输入层、隐藏层与输出层的作用,激活函数的选择与实现,前馈传播的矩阵运算与向量化编程技巧,以及代码结构与模块化设计策略。通过具体代码示例和数学公式推导,帮助读者建立对神经网络结构的全面理解,为后续章节的反向传播与优化打下坚实基础。

3. 反向传播算法原理与编程实现

3.1 反向传播的数学基础

3.1.1 链式法则与梯度计算

反向传播(Backpropagation)是神经网络训练过程中最关键的算法之一,其核心思想基于链式法则(Chain Rule)进行梯度计算。梯度计算的目标是找出损失函数对网络中每一层权重的偏导数,从而指导权重的更新方向。

以一个简单的三层神经网络为例:

  • 输入层(Input Layer):$ x \in \mathbb{R}^n $
  • 隐藏层(Hidden Layer):$ h = \sigma(W_1x + b_1) $
  • 输出层(Output Layer):$ \hat{y} = W_2h + b_2 $
  • 损失函数(Loss Function):$ L = \frac{1}{2}(\hat{y} - y)^2 $

其中:
- $ \sigma $ 是激活函数(如Sigmoid、ReLU)
- $ W_1, W_2 $ 分别为输入到隐藏层和隐藏到输出层的权重矩阵
- $ b_1, b_2 $ 是偏置项
- $ y $ 是真实标签,$ \hat{y} $ 是预测输出

反向传播的核心是计算:

\frac{\partial L}{\partial W_2}, \quad \frac{\partial L}{\partial W_1}

根据链式法则,我们有:

\frac{\partial L}{\partial W_2} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial W_2}

\frac{\partial L}{\partial W_1} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial h} \cdot \frac{\partial h}{\partial W_1}

这正是误差反向传播的过程:误差从输出层逐步回传到输入层,并逐层计算梯度。

链式法则的物理意义 :每一层的权重更新依赖于当前层的输入和输出对最终损失的贡献,这种逐层回传的方式使得误差可以精确地分配到每个神经元上。

3.1.2 损失函数对权重的偏导数推导

我们继续以均方误差(MSE)为例,推导损失函数对各层权重的偏导数。

输出层误差项:

\delta_2 = \frac{\partial L}{\partial z_2} = (\hat{y} - y)

隐藏层误差项:

\delta_1 = \frac{\partial L}{\partial z_1} = W_2^T \cdot \delta_2 \odot \sigma’(z_1)

其中:
- $ z_1 = W_1x + b_1 $
- $ z_2 = W_2h + b_2 $
- $ \odot $ 表示逐元素乘法
- $ \sigma’(z) $ 是激活函数的导数

权重更新公式:

\frac{\partial L}{\partial W_2} = \delta_2 \cdot h^T
\frac{\partial L}{\partial W_1} = \delta_1 \cdot x^T

这些梯度将在训练过程中被用于更新权重:

W_2 := W_2 - \alpha \cdot \frac{\partial L}{\partial W_2}
W_1 := W_1 - \alpha \cdot \frac{\partial L}{\partial W_1}

其中 $ \alpha $ 是学习率。

总结: 这一推导过程展示了反向传播算法如何利用链式法则将误差从输出回传到输入,逐层计算梯度并更新参数,从而实现神经网络的学习能力。

3.2 算法实现细节

3.2.1 层间误差的传递机制

误差的传递机制是反向传播的核心,它决定了误差如何从输出层回传到输入层,并指导每一层参数的更新方向。

在实现中,误差的传递可以通过矩阵运算高效完成。以一个具有一个隐藏层的神经网络为例,误差的传播流程如下:

graph TD
    A[输入 x] --> B(前向传播)
    B --> C[隐藏层激活 h = σ(W1x + b1)]
    C --> D[输出层预测 ŷ = W2h + b2]
    D --> E[计算损失 L]
    E --> F[反向传播]
    F --> G[计算输出层误差 δ2]
    G --> H[计算隐藏层误差 δ1]
    H --> I[计算梯度 ∂L/∂W1, ∂L/∂W2]
    I --> J[权重更新 W1, W2]

误差的传递遵循以下步骤:

  1. 输出层误差计算:
    $$
    \delta_2 = (\hat{y} - y)
    $$

  2. 隐藏层误差计算:
    $$
    \delta_1 = W_2^T \cdot \delta_2 \odot \sigma’(z_1)
    $$

  3. 梯度计算:
    $$
    \frac{\partial L}{\partial W_2} = \delta_2 \cdot h^T
    $$
    $$
    \frac{\partial L}{\partial W_1} = \delta_1 \cdot x^T
    $$

3.2.2 权重更新的实现步骤

权重更新是反向传播的最后一步,其核心是利用计算得到的梯度,按一定学习率调整权重矩阵。

以下是权重更新的伪代码实现步骤:

% 假设已计算出梯度 dW1 和 dW2
alpha = 0.01; % 学习率

% 更新权重
W1 = W1 - alpha * dW1;
W2 = W2 - alpha * dW2;
示例:Octave/Matlab 实现
function [W1, W2] = update_weights(W1, W2, dW1, dW2, learning_rate)
    % 权重更新函数
    W1 = W1 - learning_rate * dW1;
    W2 = W2 - learning_rate * dW2;
end

逐行解读:
- learning_rate 控制每次更新的步长,过大会导致震荡,过小会减慢收敛。
- dW1 dW2 是通过反向传播计算得到的梯度。
- W1 = W1 - learning_rate * dW1; 表示沿着负梯度方向更新权重,使得损失函数值降低。

参数说明:
- W1 , W2 : 权重矩阵
- dW1 , dW2 : 损失函数对权重的偏导数
- learning_rate : 学习率,控制更新步长

3.3 代码调试与验证

3.3.1 梯度值的输出与分析

在实现反向传播算法后,梯度的正确性至关重要。我们可以通过打印中间梯度值来验证其合理性。

% 打印梯度
disp('Gradient dW2:');
disp(dW2);
disp('Gradient dW1:');
disp(dW1);

分析要点:
- 梯度值不应为 NaN Inf ,否则说明前向传播或激活函数存在数值问题。
- 梯度值的大小应在合理范围内,过大可能说明学习率过高,过小则可能说明网络“卡住”。
- 多次迭代后,梯度应逐渐减小,表明网络正在收敛。

3.3.2 常见实现错误与修正方法

错误类型 表现 修正方法
梯度爆炸 梯度值极大,训练不稳定 使用梯度裁剪(gradient clipping)或降低学习率
梯度消失 梯度值趋近于零,训练停滞 使用ReLU激活函数,避免Sigmoid;使用Batch Normalization
输入未归一化 损失函数值不下降 数据预处理:标准化或归一化
维度不匹配 矩阵乘法报错 检查矩阵维度是否匹配,如 $ W \in \mathbb{R}^{n \times m} $
代码逻辑错误 损失函数值上升 检查前向传播与反向传播是否对称,误差是否正确回传
示例:检查维度是否匹配
% 检查维度
if ~isequal(size(dW1), size(W1))
    error('dW1 dimension does not match W1');
end

if ~isequal(size(dW2), size(W2))
    error('dW2 dimension does not match W2');
end

逐行解读:
- size(dW1) 获取梯度矩阵的维度
- isequal 判断两个矩阵维度是否一致
- error 抛出错误信息,帮助调试

示例:梯度裁剪(防止梯度爆炸)
% 梯度裁剪
max_grad_norm = 5;
grad_norm = norm(dW1, 'fro'); % 计算Frobenius范数
if grad_norm > max_grad_norm
    dW1 = dW1 * (max_grad_norm / grad_norm);
end

逐行解读:
- norm(dW1, 'fro') 计算梯度的Frobenius范数
- 如果范数超过阈值,则按比例缩放梯度

交互式思考:
在调试过程中,你是否遇到过梯度为零的情况?是否尝试过更换激活函数?在编程作业5中,如果你使用的是Sigmoid激活函数,尝试改用ReLU后,是否观察到训练速度提升?

拓展讨论:
在更复杂的神经网络中,如卷积神经网络(CNN)或循环神经网络(RNN),反向传播的实现会更加复杂。例如,RNN中存在“时间维度”的反向传播(BPTT),而CNN中则涉及卷积核梯度的计算。这些内容将在后续章节中进一步探讨。

4. 数据预处理技术应用(特征缩放、one-hot编码)

在机器学习项目中,数据预处理是构建高质量模型的基础环节。Andrew Ng在Coursera的机器学习课程中,通过编程作业5详细展示了如何对输入数据进行有效的预处理操作,以提升模型训练效率和泛化能力。本章将围绕编程作业5中涉及的关键数据预处理技术—— 特征缩放 one-hot编码 ,进行深入剖析,涵盖其原理、实现方法及其在神经网络训练中的实际作用。

4.1 特征缩放技术

在实际应用中,原始数据往往存在量纲差异、分布不均等问题。 特征缩放 (Feature Scaling)是通过标准化或归一化手段,使得不同特征在数值上具有可比性,从而加快模型收敛速度,提高训练效率。

4.1.1 标准化与归一化方法

特征缩放主要包含两种常用方法: 标准化 (Standardization)和 归一化 (Normalization)。

方法 公式 适用场景
标准化 $ x’ = \frac{x - \mu}{\sigma} $ 特征分布接近正态时
归一化 $ x’ = \frac{x - x_{min}}{x_{max} - x_{min}} $ 特征最大最小值已知,且分布不规则

其中:
- $ \mu $:特征均值
- $ \sigma $:特征标准差
- $ x_{min}, x_{max} $:特征最小值和最大值

标准化示例代码(Octave/Matlab):
function [X_norm, mu, sigma] = featureNormalize(X)
    mu = mean(X);
    sigma = std(X);
    X_norm = (X - mu) ./ sigma;
end

逐行分析:

  1. mu = mean(X); :计算每个特征的均值,形成一个行向量。
  2. sigma = std(X); :计算每个特征的标准差,同样形成行向量。
  3. X_norm = (X - mu) ./ sigma; :对每个特征进行标准化处理。

标准化后的特征通常分布在 $ \mu=0 $、$ \sigma=1 $ 附近,适合梯度下降类算法。

4.1.2 特征缩放在神经网络中的重要性

在神经网络中,特征缩放对训练过程有显著影响:

  • 加速收敛 :不同特征的尺度差异会导致梯度更新方向不稳定,缩放后可使优化路径更加平滑。
  • 防止梯度爆炸/消失 :输入值过大可能导致激活函数饱和,进而影响梯度传播。
  • 统一输入范围 :如Sigmoid、ReLU等激活函数对输入值范围敏感,缩放后有助于提升非线性表达能力。

示意图说明:

下图通过mermaid流程图展示特征缩放前后对梯度下降过程的影响:

graph LR
    A[原始特征] --> B(未缩放)
    B --> C{梯度更新方向不稳定}
    C --> D[收敛慢,易震荡]
    A --> E(标准化)
    E --> F{梯度更新方向稳定}
    F --> G[收敛快,更平稳]

4.2 类别变量编码

在机器学习任务中,我们常常会遇到 类别型特征 (Categorical Features),例如性别、颜色、城市等。这些特征无法直接输入到神经网络中,需要进行 编码 处理。 one-hot编码 是处理此类问题的常用方式。

4.2.1 one-hot编码原理

one-hot编码的核心思想是将一个具有 $ n $ 个不同取值的类别特征转换为一个长度为 $ n $ 的二进制向量,其中只有一个位置为1,其余为0。

例如,一个颜色特征包含“红”、“绿”、“蓝”三个类别,其one-hot编码如下:

原始值 编码结果
[1, 0, 0]
绿 [0, 1, 0]
[0, 0, 1]

这种编码方式可以有效避免类别之间的数值大小关系对模型造成误导。

4.2.2 编码后的数据在模型输入中的使用

在神经网络中,one-hot编码后的特征作为输入可以直接输入到网络中,但需要注意以下几点:

  • 维度爆炸问题 :如果类别数量过多,one-hot编码会导致特征维度急剧上升,从而增加模型复杂度和训练成本。
  • 嵌入层替代 :在深度学习中,常使用 Embedding层 替代one-hot编码,以压缩维度并保留语义信息。
  • 避免多重共线性 :在回归任务中,one-hot编码后通常去掉一个类别以防止多重共线性。
示例:使用Octave/Matlab实现one-hot编码
function encoded = oneHotEncode(labels, num_classes)
    encoded = zeros(numel(labels), num_classes);
    encoded(sub2ind(size(encoded), (1:numel(labels))', labels)) = 1;
end

逐行分析:

  1. zeros(numel(labels), num_classes) :初始化一个全0的矩阵,行数为样本数,列数为类别数。
  2. sub2ind(...) :将类别标签转换为对应的索引位置。
  3. encoded(...) = 1 :在相应位置设置为1,完成one-hot编码。

参数说明:
- labels :类别标签向量(从1开始编号)
- num_classes :类别总数

4.3 数据预处理流程实现

在编程作业5中,数据预处理是一个完整的流程,包括数据加载、缺失值处理、特征缩放、类别编码等多个步骤。以下我们以Octave/Matlab为例,展示一个完整的预处理流程实现。

4.3.1 Octave/Matlab中的预处理函数编写

function [X_processed, y_encoded] = preprocessData(rawData, num_classes)
    % 1. 分离特征与标签
    X = rawData(:, 1:end-1);
    y = rawData(:, end);
    % 2. 特征缩放
    [X_scaled, ~, ~] = featureNormalize(X);
    % 3. 类别标签 one-hot 编码
    y_encoded = oneHotEncode(y, num_classes);
    % 4. 返回处理后的数据
    X_processed = X_scaled;
end

逐行分析:

  1. X = rawData(:, 1:end-1); y = rawData(:, end); :将最后一列作为标签,其余为特征。
  2. [X_scaled, ~, ~] = featureNormalize(X); :调用之前定义的标准化函数。
  3. y_encoded = oneHotEncode(y, num_classes); :进行one-hot编码。
  4. 返回处理后的特征矩阵和编码后的标签。

参数说明:
- rawData :原始数据矩阵(每一行是一个样本)
- num_classes :类别总数

4.3.2 多维度数据的处理与整合

在实际应用中,数据可能包含多种类型:数值型、类别型、文本型等。因此,预处理流程往往需要分而治之,再整合输入。

整合流程示意图:

graph TD
    A[原始数据] --> B{分列处理}
    B --> C[数值列]
    B --> D[类别列]
    C --> E[标准化]
    D --> F[one-hot编码]
    E --> G[水平拼接]
    F --> G
    G --> H[最终输入矩阵]

在Octave/Matlab中,可以使用 [X_num, X_cat] 进行拼接操作,整合不同类型的处理结果。

此外,还需要注意以下问题:

  • 类别特征顺序 :不同类别特征应分别处理后再拼接,避免混淆。
  • 缺失值处理 :在预处理前应对缺失值进行填充或删除,否则会影响后续操作。
  • 数据划分一致性 :训练集和测试集应使用相同的预处理参数(如标准化中的均值和标准差)以防止信息泄露。

总结与延伸

在本章中,我们系统讲解了在编程作业5中使用的 特征缩放 one-hot编码 技术,涵盖了其原理、实现方法及其在神经网络训练中的作用。这些预处理步骤虽看似简单,但却是构建高效模型不可或缺的一环。

下一章我们将深入探讨 损失函数 的实现与优化,了解如何通过 交叉熵 均方误差 等指标来衡量模型性能,并实现其向量化代码。这些内容将进一步完善我们对神经网络训练流程的理解。

5. 损失函数实现与优化(交叉熵、均方误差)

在机器学习模型中,损失函数是衡量模型预测值与真实值之间差异的关键指标。在Andrew Ng的机器学习课程编程作业5中,重点涉及了两种常用的损失函数: 交叉熵损失函数(Cross-Entropy Loss) 均方误差损失函数(Mean Squared Error, MSE) 。本章将深入分析这两种损失函数的数学原理、代码实现方式及其在实际任务中的适用性,并探讨如何通过引入正则化项来优化模型性能,防止过拟合。

5.1 损失函数类型与选择

在构建神经网络模型时,选择合适的损失函数对于模型训练的效率和最终性能至关重要。损失函数的类型通常取决于任务的性质,例如分类任务和回归任务所使用的损失函数是不同的。

5.1.1 分类任务与交叉熵函数

交叉熵(Cross-Entropy) 是衡量两个概率分布差异的一种方式,广泛用于分类问题中,尤其是二分类和多分类任务。

在多分类任务中,通常使用 Softmax 函数 将神经网络输出转化为概率分布,然后使用 Softmax 交叉熵损失函数 来衡量预测概率与真实标签之间的差异。

其数学表达式如下:

对于第 $ i $ 个样本:

L = -\sum_{j=1}^{C} y_j \log(p_j)

其中:

  • $ y_j $:真实标签(one-hot 编码形式)
  • $ p_j $:模型预测的概率(通过 Softmax 得出)

在Octave/Matlab中,该损失函数的实现如下:

function [cost] = softmax_cross_entropy_loss(y_true, y_pred)
    m = size(y_true, 1);  % 样本数量
    epsilon = 1e-12;      % 防止log(0)出现
    y_pred = max(min(y_pred, 1 - epsilon), epsilon);  % 限制预测值范围
    cost = -1/m * sum(sum(y_true .* log(y_pred)));
end
代码逻辑分析:
  • y_true 是 one-hot 编码的真实标签矩阵,形状为 (m, C) ,其中 m 是样本数, C 是类别数。
  • y_pred 是模型输出的概率分布,形状也为 (m, C)
  • 使用 max/min 函数防止出现 log(0) 的无穷值。
  • .* 表示逐元素相乘。
  • sum(sum(...)) 对所有样本的所有类别的损失值求和。
  • 最终损失值除以样本数 m 得到平均损失。

5.1.2 回归任务与均方误差函数

均方误差(Mean Squared Error, MSE) 是回归任务中最常用的损失函数之一。它衡量模型预测值与真实值之间的平均平方差异。

其数学表达式为:

L = \frac{1}{m} \sum_{i=1}^{m} (y_i - \hat{y}_i)^2

其中:

  • $ y_i $:第 $ i $ 个样本的真实值
  • $ \hat{y}_i $:模型预测值
  • $ m $:样本数量

在Octave/Matlab中的实现如下:

function [cost] = mean_squared_error(y_true, y_pred)
    m = size(y_true, 1);
    cost = 1/(2*m) * sum((y_pred - y_true).^2);
end
代码逻辑分析:
  • y_true 是形状为 (m, 1) 的真实值向量。
  • y_pred 是模型预测值,形状也为 (m, 1)
  • (y_pred - y_true).^2 对预测值与真实值差值的平方进行计算。
  • sum(...) 对所有样本的损失值求和。
  • 最后除以 2*m 是为了在后续梯度下降中简化求导运算。

损失函数选择对比表:

任务类型 损失函数 适用场景
分类任务 交叉熵损失 二分类或多分类任务,输出为概率分布
回归任务 均方误差 连续值预测任务,如房价预测、气温预测等

5.2 损失函数的代码实现

在编程作业5中,损失函数的实现不仅要准确计算当前模型的损失值,还需要支持后续梯度下降的优化过程。因此,代码实现需具备向量化能力,以提升计算效率。

5.2.1 向量化实现技巧

向量化是提高神经网络计算效率的重要手段。通过使用矩阵运算替代循环操作,可以显著提升Octave/Matlab程序的运行速度。

以交叉熵损失函数的向量化实现为例,其核心步骤如下:

% 假设 y_true 和 y_pred 已经是矩阵形式
epsilon = 1e-12;
y_pred = max(min(y_pred, 1 - epsilon), epsilon);
loss = -1/m * sum(sum(y_true .* log(y_pred)));
  • y_true .* log(y_pred) :逐元素乘法,避免使用循环。
  • sum(sum(...)) :对矩阵进行两次求和,先列后行,得到总损失值。
  • max/min(...) :确保数值稳定性,避免对0取对数。

5.2.2 损失值的计算与输出

在训练过程中,我们通常会每隔一定迭代次数输出当前的损失值,以监控训练过程是否收敛。

例如,在主训练循环中添加如下代码:

for i = 1:num_iterations
    % 前向传播
    y_pred = forward(X, W1, b1, W2, b2);
    % 计算损失
    loss = softmax_cross_entropy_loss(y_true, y_pred);
    % 反向传播与参数更新
    [dW1, db1, dW2, db2] = backward(X, y_true, y_pred, W1, W2, cache);
    [W1, b1, W2, b2] = update_parameters(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate);
    % 输出损失值
    if mod(i, 100) == 0
        fprintf('Iteration %d, Loss: %.4f\n', i, loss);
    end
end
代码逻辑分析:
  • mod(i, 100) == 0 表示每100次迭代输出一次损失值。
  • fprintf 用于格式化输出当前迭代次数和损失值。
  • 通过观察损失值是否持续下降,可以判断模型是否正常训练。

5.3 正则化项的加入

在机器学习模型中,过拟合是一个常见问题。为了解决这一问题,可以在损失函数中加入 正则化项(Regularization Term) ,以惩罚模型的复杂度,从而提升其泛化能力。

5.3.1 正则化对损失函数的影响

正则化的主要思想是通过对模型参数的大小进行约束,防止模型过于复杂,从而提升泛化能力。常见的正则化方式包括:

  • L1 正则化(Lasso) :对参数的绝对值进行惩罚
  • L2 正则化(Ridge) :对参数的平方进行惩罚

在损失函数中,正则化项通常表示为:

L_{reg} = L + \lambda R(W)

其中:

  • $ L $:原始损失函数(如交叉熵或MSE)
  • $ R(W) $:正则化项
  • $ \lambda $:正则化系数,控制惩罚力度

5.3.2 L1与L2正则化在损失函数中的实现

在Octave/Matlab中,我们可以在损失函数中加入正则化项。以L2正则化为例:

function [cost] = softmax_cross_entropy_with_l2(y_true, y_pred, W1, W2, lambda)
    m = size(y_true, 1);
    epsilon = 1e-12;
    y_pred = max(min(y_pred, 1 - epsilon), epsilon);
    % 原始交叉熵损失
    ce_loss = -1/m * sum(sum(y_true .* log(y_pred)));
    % L2正则化项
    reg_term = (lambda / (2*m)) * (sum(W1(:).^2) + sum(W2(:).^2));
    % 总损失
    cost = ce_loss + reg_term;
end
代码逻辑分析:
  • W1 W2 是模型的权重参数。
  • sum(W1(:).^2) :将权重矩阵展开为列向量后计算平方和。
  • lambda 是正则化系数,控制惩罚强度。
  • 最终损失值等于原始交叉熵损失加上正则化项。
L1与L2正则化对比:
特性 L1 正则化 L2 正则化
形式 $ R(W) = \sum w_i
效果 促使部分参数趋近于0,实现特征选择 均匀惩罚参数,防止过拟合
实现复杂度 相对简单 稍复杂,需平方和开根
适用场景 特征稀疏、特征选择重要 一般性过拟合预防

正则化流程图(Mermaid格式):

graph TD
A[输入数据] --> B[前向传播计算预测值]
B --> C[计算原始损失函数]
C --> D{是否加入正则化?}
D -- 是 --> E[计算L1/L2正则化项]
D -- 否 --> F[使用原始损失值]
E --> G[总损失 = 原始损失 + 正则化项]
F --> H[梯度下降更新参数]
G --> H

通过本章内容的学习,读者应能够理解交叉熵和均方误差损失函数的数学原理,掌握其在Octave/Matlab中的实现方式,并了解正则化项在优化模型性能中的作用。下一章将结合编程作业5的整体结构,详细解析代码实现的每一个关键模块。

6. 编程作业5完整代码实现与解析

本章将详细展示并解析编程作业5的完整代码实现,涵盖神经网络训练与预测的核心流程。通过结构化代码分析,帮助读者理解主函数、辅助函数之间的调用关系,并深入讲解前向传播、反向传播、成本计算与梯度更新等关键模块的实现逻辑。

6.1 代码文件结构与功能划分

编程作业5通常由多个 .m 文件组成,分别承担主程序、数据预处理、模型训练、预测与评估等职责。以下是一个典型的文件结构:

文件名 功能描述
nnCostFunction.m 实现前向传播与反向传播,计算成本与梯度
predict.m 根据训练好的参数进行预测
randInitializeWeights.m 随机初始化权重矩阵
sigmoid.m sigmoid 激活函数实现
ex4.m 主程序脚本,负责加载数据、调用优化器、评估模型等

6.1.1 主函数与辅助函数的调用关系

主程序 ex4.m 通常包含以下关键步骤:

% 加载数据
load('ex4data1.mat'); % 输入数据 X, 标签 y
load('ex4weights.mat'); % 预设权重参数

% 参数设置
input_layer_size = 400;  % 输入层神经元数
hidden_layer_size = 25;  % 隐藏层神经元数
num_labels = 10;         % 输出类别数
lambda = 1;              % 正则化参数

% 合并权重参数
initial_nn_params = [Theta1(:); Theta2(:)];

% 优化参数
options = optimset('MaxIter', 50);
costFunction = @(p) nnCostFunction(p, ...
    input_layer_size, hidden_layer_size, ...
    num_labels, X, y, lambda);

[nn_params, cost] = fmincg(costFunction, initial_nn_params, options);

% 预测准确率
pred = predict(Theta1, Theta2, X);
fprintf('\nTraining Set Accuracy: %f\n', mean(double(pred == y)) * 100);

这段代码清晰地展示了主程序如何调用各辅助函数,流程如下:

  • 加载训练数据与初始权重;
  • 合并参数以适应优化器;
  • 定义目标函数并调用优化器 fmincg
  • 使用训练后的参数进行预测并评估准确率。

6.1.2 各模块的功能描述与参数说明

  • nnCostFunction.m :核心模块,计算损失函数与梯度;
  • predict.m :用于模型预测,基于训练好的权重;
  • randInitializeWeights.m :实现权重初始化,避免对称性问题;
  • sigmoid.m :实现激活函数;
  • fmincg :第三方优化器,用于梯度下降。

6.2 核心模块代码详解

6.2.1 前向传播与反向传播函数实现

nnCostFunction.m 是整个项目的核心文件,其主要功能包括:

  • 前向传播计算输出;
  • 计算损失函数;
  • 反向传播计算梯度;
  • 加入正则化项。
function [J, grad] = nnCostFunction(nn_params, ...
                                   input_layer_size, ...
                                   hidden_layer_size, ...
                                   num_labels, ...
                                   X, y, lambda)
    % 分离参数
    Theta1 = reshape(nn_params(1:hidden_layer_size * (input_layer_size + 1)), ...
                     hidden_layer_size, (input_layer_size + 1));
    Theta2 = reshape(nn_params((1 + (hidden_layer_size * (input_layer_size + 1))):end), ...
                     num_labels, (hidden_layer_size + 1));

    m = size(X, 1);

    % 添加偏置项
    X = [ones(m, 1) X];

    % 前向传播
    a2 = sigmoid(X * Theta1');  % 隐藏层输出
    a2 = [ones(m, 1) a2];       % 添加偏置项
    a3 = sigmoid(a2 * Theta2');  % 输出层预测

    % 标签 one-hot 编码
    y_matrix = dummyvar(y);  % 生成 one-hot 矩阵
    y_matrix = y_matrix(:, 2:end);  % 移除第1类以避免冗余

    % 计算损失函数(交叉熵)
    J = (1/m) * sum(sum(-y_matrix .* log(a3) - (1 - y_matrix) .* log(1 - a3)));

    % 正则化项
    reg = (lambda/(2*m)) * (sum(sum(Theta1(:,2:end).^2)) + sum(sum(Theta2(:,2:end).^2)));
    J = J + reg;

    % 反向传播
    delta3 = a3 - y_matrix;  % 输出层误差
    delta2 = (delta3 * Theta2(:,2:end)) .* sigmoidGradient([ones(m,1) X * Theta1'](:,2:end));

    % 累加梯度
    Theta1_grad = zeros(size(Theta1));
    Theta2_grad = zeros(size(Theta2));

    Theta1_grad = Theta1_grad + delta2' * X;
    Theta2_grad = Theta2_grad + delta3' * a2;

    % 梯度正则化
    Theta1_grad(:,2:end) = Theta1_grad(:,2:end) + lambda * Theta1(:,2:end);
    Theta2_grad(:,2:end) = Theta2_grad(:,2:end) + lambda * Theta2(:,2:end);

    Theta1_grad = Theta1_grad / m;
    Theta2_grad = Theta2_grad / m;

    % 合并梯度
    grad = [Theta1_grad(:); Theta2_grad(:)];
end
参数说明:
  • nn_params :合并后的参数向量;
  • input_layer_size :输入层神经元数量;
  • hidden_layer_size :隐藏层神经元数量;
  • num_labels :输出类别数;
  • X :输入特征矩阵;
  • y :标签向量;
  • lambda :正则化参数。
执行逻辑说明:
  1. 参数从向量形式恢复为矩阵;
  2. 添加偏置项并进行前向传播;
  3. 使用交叉熵损失函数计算误差;
  4. 加入正则化项防止过拟合;
  5. 反向传播计算每一层的误差;
  6. 累加梯度并正则化;
  7. 返回损失与梯度供优化器使用。

6.2.2 成本计算与梯度更新函数分析

损失函数的计算部分如下:

J = (1/m) * sum(sum(-y_matrix .* log(a3) - (1 - y_matrix) .* log(1 - a3)));

这段代码实现了交叉熵损失函数的向量化计算。其中:

  • a3 是预测输出;
  • y_matrix 是 one-hot 编码后的标签;
  • log(a3) log(1 - a3) 分别对应类别为1和0时的对数似然。

梯度更新逻辑则通过矩阵运算实现:

Theta1_grad = Theta1_grad + delta2' * X;
Theta2_grad = Theta2_grad + delta3' * a2;

通过误差 delta2 delta3 与输入数据的乘积,完成梯度的累计。

6.3 代码运行与结果分析

6.3.1 训练过程的可视化输出

ex4.m 中,可以加入以下代码实现训练过程的可视化:

figure;
hold on;
plot(1:max_iterations, cost_history, 'b-o', 'LineWidth', 2);
xlabel('Iterations');
ylabel('Cost');
title('Training Cost over Iterations');
grid on;
hold off;

这段代码假设 cost_history 是一个记录每轮迭代损失值的数组,输出结果如下图所示(使用 mermaid 流程图模拟):

graph TD
    A[Start Training] --> B[Iteration 1: Cost=3.2]
    B --> C[Iteration 2: Cost=2.9]
    C --> D[Iteration 3: Cost=2.6]
    D --> E[Iteration 4: Cost=2.3]
    E --> F[Iteration 5: Cost=2.0]
    F --> G[...]
    G --> H[Final Iteration: Cost=0.2]

随着迭代进行,损失值逐渐下降,说明模型正在逐步学习。

6.3.2 最终模型性能评估与分析

训练完成后,通过 predict.m 对训练集进行预测并计算准确率:

pred = predict(Theta1, Theta2, X);
fprintf('\nTraining Set Accuracy: %f\n', mean(double(pred == y)) * 100);

输出示例:

Training Set Accuracy: 94.320000

说明模型在训练集上的准确率达到 94.32%,表明模型具有较好的拟合能力。

(本章完)

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本课程围绕Andrew Ng教授广受欢迎的机器学习课程展开,重点讲解编程作业5(Ex5)中的核心内容。该作业深入探讨神经网络与反向传播算法,涵盖从神经网络基础、损失函数设计到梯度检查、训练验证、正则化与测试评估的完整实现流程。通过该作业实践,学习者可以加深对深度学习基础的理解,掌握神经网络的构建与调优方法,为后续高级机器学习课题打下坚实基础。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐