60、多层感知器(MLP)软件设计与实现

多层感知器(MLP)软件设计与实现

1. MLP 分类器实现模式

MLP 分类器的实现遵循与之前分类器相同的模式,具体如下:
- 在分类器初始化期间,通过训练初始化一个类型为 Model MLPModel 模型。该模型由 MLPLayer 类型的神经元层组成,这些层通过 MLPConnection 类型的连接器中的 MLPSynapse 类型的突触连接。
- 所有配置参数封装在一个单一的配置类 MLPConfig 中。
- 预测或分类例程实现为数据转换,扩展了 PipeOperator 特征。
- 多层感知器类 MLP 接受三个参数:配置实例、 XTSeries 类的特征集或时间序列,以及 DblMatrix 类型的带标签数据集。

以下是多层感知器的 UML 类图:

classDiagram
    class MLPObjective
    class PipeOperator
    class Model
    class Array[Int]
    class MLPRegression
    class MLPBinClassifier
    class MLPMultiClassifier
    class MLPSynapse
    class MLPConnection
    class MLPLayer
    class Config
    class MLPConfig
    class XTSeries
    class DblMatrix
    class MLP
    class MLPModel

    MLPModel "1" -- "1" MLPConfig : config
    MLPModel "1" -- "1+" MLPLayer : layers
    MLPModel "1" -- "1+" MLPConnection : connections
    MLPModel "1" -- "1" Array[Int] : topology
    MLP "1" -- "1" MLPModel : model
    MLP "1" -- "1" MLPConfig : config
    MLP "1" -- "1" XTSeries : xt
    MLP "1" -- "1" DblMatrix : labels
    MLP "1" -- "1" MLPObjective : objective
2. 模型定义

模型的目的是完全定义网络架构,由 MLPModel 参数化类实现,该类负责创建和管理网络的不同组件,包括层、连接和拓扑。

神经元层的命名约定如下:
- 输入层 inLayer nInputs 个神经元组成。
- 隐藏层 hidLayer nHiddens 个神经元。
- 输出层 outLayer nOutputs 个神经元。

MLPModel 类的实例化至少需要三个参数:

class MLPModel[T <% Double](config: MLPConfig, nInputs: Int, nOutputs: Int) extends Model {
   val layers: Array[MLPLayer]
   val connections: Array[MLPConnection]
   val topology: Array[Int]
}

除了 config 配置外,模型类还有两个参数:输入特征的数量 nInputs 和输出值的数量 nOutputs 。这三个参数足以初始化网络的拓扑。

模型具有以下属性:
- 多个 MLPLayer 类型的层。
- 多个 MLPConnection 类型的连接。
- 一个连接这些层和连接的拓扑数组。

拓扑定义为每层节点数量的数组,从输入节点开始,数组索引遵循网络内的前向路径。输入层的大小自动从观测值中作为特征向量的大小生成,输出层的大小自动从输出向量的大小中提取:

val topology = Array[Int](nInputs) ++ config.hidLayers ++ Array[Int](nOutputs)

隐藏层序列 hidLayers 定义为每个隐藏层的神经元(或节点)数量的数组:

val hidLayers: Array[Int]

例如,一个具有三个输入变量、一个输出变量和两个各有三个神经元的隐藏层的神经网络的拓扑指定为 Array[Int](4, 3, 3, 1)

3. 模型组件
3.1 层(Layers)

MLPLayer 类由其在网络中的位置和包含的节点数量完全指定:

class MLPLayer(val id: Int, val len: Int) {
   val output = new DblVector(len) //1
   val delta = new DblVector(len) //2
  ...output.update(0, 1.0) //3
}

id 参数是层在网络中的顺序(输入层为 0,第一个隐藏层为 1,…,输出层为 n - 1), len 值是该层中的元素或节点数量,包括偏置元素。层的输出向量(第 1 行)是一个未初始化的向量,在正向传播期间更新,除了第一个值(偏置元素)设置为 1(第 3 行)。与输出向量关联的 delta 向量(第 2 行)通过误差反向传播算法更新。

输出值(除偏置元素外)使用 set 方法初始化:

def set(x: DblVector): Unit = x.copyToArray(output,1)
3.2 突触(Synapses)

突触定义为一对实值:
- 从先前层的神经元 i 到神经元 j 的连接权重 wij
- 权重调整(或权重梯度) ∆wij

其类型定义为 MLPSynapse

type MLPSynapse = (Double, Double)
3.3 连接(Connections)

两个连续层之间的连接实现突触矩阵 (wij, ∆wij) 对。 MLPConnection 实例使用以下参数创建:
- 配置参数 config
- 源层(有时称为入口层) src
- 目标(或出口)层 dst

MLPConnection 类定义如下:

class MLPConnection(config: MLPConfig, src: MLPLayer, dst: MLPLayer)

MLP 算法初始化的最后一步是选择权重(突触)的初始(通常是随机)值。以下代码片段将非偏置神经元的权重初始化为范围 [0, beta] 内的随机值,其中 beta <= 1.0 。偏置的权重定义为 w0 = +1 ,其权重调整初始化为 ∆w0 = 0

Val beta = 0.1
val synapses = Array.tabulate(dst.len)(n => 
   if(n > 0) Array.fill(src.len)((beta*Random.nextDouble, 0.0))
   else Array.fill(src.len)((1.0, 0.0))
)

初始随机值的范围 [0, beta] 是特定于领域的,一些问题需要非常小的范围(小于 1e - 3),而其他问题使用概率空间 [0, 1] 。初始值会影响收敛到最优权重集所需的迭代次数。

4. MLPModel 初始化

一旦定义了 MLP 算法的拓扑、突触、层和连接, MLPModel 模型的初始化就很简单:

val layers = topology.zipWithIndex
                     .map(t => MLPLayer(t._2, t._1+1))
val connections = Range(0, layers.size-1).map(n =>
    new MLPConnection(config, layers(n), layers(n+1))).toArray

层通过遍历网络拓扑并为每个层实例化其正确的索引和元素数量来创建。连接通过选择索引为 n n + 1 的两个连续层作为源层和目标层来实例化。

5. 封装和模型工厂

为了清晰起见,模型组件(连接、层和突触)实现为顶级类。然而,模型不需要向客户端代码暴露其内部工作原理,这些组件应声明为模型的内部类。此外,模型负责创建其拓扑,工厂设计模式非常适合动态实例化 MLPModel 实例。

6. 训练周期/迭代

模型的训练会多次处理训练观测值,一个训练周期或迭代称为一个 epoch。训练周期的五个步骤如下:
1. 特定 epoch 的输入值正向传播。
2. 计算平方误差之和。
3. 输出误差反向传播。
4. 重新计算突触权重和权重梯度。
5. 评估收敛标准,如果满足标准则退出。

在训练期间计算网络权重时,使用每层的标记数据和实际输出之间的差异不可行,因为隐藏层的输出未知。解决方案是将输出值的误差通过隐藏层反向传播。

输出层 p 个神经元的误差可以通过以下两种方式计算:
- 误差平方和(SSE):为每个输出 yk 计算。
- 均方误差(MSE):计算为 MSE = SSE / p

我们选择误差平方和来初始化误差反向传播算法。

7. 输入正向传播

隐藏层的输出值计算为权重 wij 和输入值 xi 的点积的逻辑函数(激活函数)。

输出层的输出 y 的计算如下:
[ y_k = v_{0k} + \sum_{j = 1}^{m} v_{jk} z_j ]

使用激活函数 σ 进行二元分类的输出值估计:
[ z_j = \sigma(\sum_{i = 0}^{n} w_{ij} x_i) = \frac{1}{1 + e^{-\sum_{i = 0}^{n} w_{ij} x_i}} ]

对于具有多个类的多项式(或多类)分类,输出值使用指数函数(softmax)进行归一化。

输入正向传播的计算模型如下:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    A(Input x1:n):::process --> B(Connection wij):::process
    B --> C(Hidden z1:m):::process
    C --> D(Connection vij):::process
    D --> E(Output y1:p):::process

MLPConnection 类的 connectionForwardPropagation 方法如下:

def connectionForwardPropagation: Unit = {
  val synps= synapses.drop(1)
  val _output = synps.map(x => { 
      val sum = x.zip(src.output)
                  .foldLeft(0.0)((s, xy) => s + xy._1._1*xy._2)
      if(!isOutLayer) config.activation(sum) 
      else sum
  })
  val out = if(isOutLayer) mlpObjective(_output) else _output 
  out.copyToArray(dst.output, 1)     
}
8. 目标(Objective)

不同的目标(二元、多类分类器和回归)封装在 MLPObjective 层次结构(嵌套在 MLP 伴生对象中)中,使用简单的 apply 方法转换输出值 y

trait MLPObjective { def apply(y: DblVector): DblVector }

二元分类器 MLPBinClassifier

class MLPBinClassifier extends MLPObjective {
  override def apply(y: DblVector): DblVector = output
}

多类分类器 MLPMultiClassifier

class MLPMultiClassifier extends MLPObjective {
   override def apply(y:DblVector):DblVector = softmax(y.drop(1))
   def softmax(y: DblVector): DblVector = { 
      val softmaxValues = new DblVector(y.size)
      val expY = y.map( Math.exp(_))
      val expYSum = expY.sum
      expY.map( _ /expYSum).copyToArray(softmaxValues, 1) 
      softmaxValues
   }
}
9. 平方误差之和

输入特征在神经网络中传播后,每个 epoch 计算 MPLayer 类型的输出层的平方误差之和 sse

def sse(labels: DblVector): Double = {
   var _sse = 0.0
   output.drop(1)  
        .zipWithIndex
        .foreach(on => {
     val err = labels(on._2) - on._1  
     delta.update(on._2+1, on._1* (1.0- on._1)*err) 
     _sse += err*err
  })
  _sse*0.5  
}
10. 误差反向传播

误差反向传播算法用于估计隐藏层的误差,以计算网络权重的变化。它以输出的平方误差之和作为输入。

输出层每个权重上的平方输出误差之和的偏导数计算为平方函数的导数与权重和输入 z 的点积的导数的组合。

隐藏层权重上的误差偏导数的计算较为复杂,可以分解为以下三个部分:
- 平方误差之和 ε 对输出值 yk 的导数。
- 已知 sigmoid 函数 σ 的导数为 σ(1 - σ) 时,输出值 yk 对隐藏值 zj 的导数。
- 隐藏层输出 zj 对权重 wij 的导数。

误差反向传播的计算模型与输入正向传播类似,主要区别是导数 delta δ 从输出层向输入层传播。

MLPConnection 类的 connectionBackpropagation 方法如下:

def connectionBackpropagation: Unit =  
  Range(1, src.len).foreach(i => {
    val dot = Range(1, dst.len).foldLeft(0.0)((s, j) => 
                         s + synapses(j)(i)._1*dst.delta(j)) 
    src.delta(i) = src.output(i)*(1.0 - src.output(i))*dot
})

通过以上步骤和方法,我们可以实现一个完整的多层感知器网络,并进行训练和预测。在实际应用中,需要根据具体问题调整参数和算法,以达到最佳性能。

多层感知器(MLP)软件设计与实现

11. 反向传播的详细推导与理解

前面提到了误差反向传播算法用于估计隐藏层的误差来计算网络权重的变化,这里我们详细推导一下相关公式。

11.1 输出层权重的偏导数

输出层每个神经元 $y_k$ 的误差 $\varepsilon_k$ 为预测输出值和标签输出值的差值。输出层平方误差之和(SSE)对输出层权重 $v_{jk}$ 的偏导数为:
[
\frac{\partial \sum_{k = 1}^{p} \varepsilon_k^2}{\partial v_{jk}} = -2 \sum_{k = 1}^{p} \varepsilon_k z_j
]
这里的推导基于复合函数求导法则,先对平方函数求导,再对权重和输入 $z$ 的点积求导。

11.2 隐藏层权重的偏导数

隐藏层权重上误差的偏导数计算较为复杂,它可以分解为三个部分:
- 平方误差之和 $\varepsilon$ 对输出值 $y_k$ 的导数:$\frac{\partial \varepsilon}{\partial y_k}$
- 已知 sigmoid 函数 $\sigma$ 的导数为 $\sigma(1 - \sigma)$ 时,输出值 $y_k$ 对隐藏值 $z_j$ 的导数:$\frac{\partial y_k}{\partial z_j}$
- 隐藏层输出 $z_j$ 对权重 $w_{ij}$ 的导数:$\frac{\partial z_j}{\partial w_{ij}}$

最终隐藏层权重上误差的偏导数为:
[
\frac{\partial \varepsilon}{\partial w_{ij}} = \sum_{k = 1}^{p} \frac{\partial \varepsilon}{\partial y_k} \frac{\partial y_k}{\partial z_j} \frac{\partial z_j}{\partial w_{ij}}
]

12. 训练过程的总结与优化思路

整个 MLP 的训练过程可以总结为以下流程图:

graph TD
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    A(开始):::process --> B(输入值正向传播):::process
    B --> C(计算平方误差之和):::process
    C --> D(输出误差反向传播):::process
    D --> E(重新计算突触权重和权重梯度):::process
    E --> F{评估收敛标准}:::process
    F -- 满足 --> G(结束):::process
    F -- 不满足 --> B

在训练过程中,有一些优化思路可以考虑:
- 学习率调整 :初始随机权重的范围和学习率会影响收敛速度。可以采用自适应学习率的方法,在训练初期使用较大的学习率快速收敛,在接近最优解时使用较小的学习率以避免错过最优解。
- 正则化 :为了防止过拟合,可以在损失函数中加入正则化项,如 L1 或 L2 正则化。

13. 不同目标的实现与应用场景

我们之前介绍了 MLPObjective 层次结构,包含二元分类器 MLPBinClassifier 和多类分类器 MLPMultiClassifier ,下面我们总结一下不同目标的实现和应用场景。

目标类型 实现类 应用场景
二元分类 MLPBinClassifier 用于只有两个类别的分类问题,如垃圾邮件检测(是垃圾邮件或不是垃圾邮件)
多类分类 MLPMultiClassifier 用于有多个类别的分类问题,如手写数字识别(0 - 9 共 10 个类别)
回归 未详细展开,可参考相关实现 用于预测连续值,如房价预测
14. 代码示例总结

以下是一个简单的示例,展示如何初始化 MLPModel 并进行训练:

// 假设已经定义了 MLPConfig、MLPLayer、MLPConnection 等类
val config = new MLPConfig()
val nInputs = 3
val nOutputs = 1
val model = new MLPModel(config, nInputs, nOutputs)

// 初始化层和连接
val layers = model.topology.zipWithIndex
                     .map(t => new MLPLayer(t._2, t._1+1))
val connections = Range(0, layers.size-1).map(n =>
    new MLPConnection(config, layers(n), layers(n+1))).toArray

// 模拟训练数据
val xt = new XTSeries()
val labels = new DblMatrix()

// 训练过程
val epochs = 100
for (epoch <- 1 to epochs) {
    // 输入正向传播
    model.forwardPropagation(xt)
    // 计算平方误差之和
    val sse = model.sse(labels)
    // 误差反向传播
    model.errorBackpropagation()
    // 重新计算突触权重和权重梯度
    model.updateWeights()
    // 评估收敛标准
    if (sse < 0.001) {
        println(s"Converged at epoch $epoch")
        break
    }
}
15. 总结

多层感知器(MLP)是一种强大的神经网络模型,通过正向传播和反向传播算法可以有效地进行训练和预测。在实现 MLP 时,需要注意模型的初始化、权重的随机化、训练过程的控制以及不同目标的实现。通过合理调整参数和采用优化策略,可以提高 MLP 的性能和泛化能力。在实际应用中,根据具体问题选择合适的目标类型和参数设置,以达到最佳的分类或预测效果。

通过以上内容,我们对多层感知器的软件设计与实现有了全面的了解,从模型的定义、组件的实现到训练过程的详细步骤,都进行了深入的探讨。希望这些内容能帮助你更好地理解和应用 MLP 模型。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值