赞
踩
经过万能近似定理的铺垫,我们知道理论上如何实现万能近似函数了,接下来我们常识用 TS 实现它,不用任何库函数,只用最原生的代码编写所有逻辑。
再次强调,就像看再多全栈入门到精通也不如自己从零写一套博客系统一样,虽然现在机器学习的 python 库非常多,但自己动手从零开始实现程序对学习新知识来说非常重要。理论与实践要相互结合。
首先,我们考虑 API 如何设计。
API 设计是开发任何接口的第一步,我们要实现的万能近似函数包括两个过程,第一是创建神经网络,第二是训练(可以训练任意多次,每次训练得到此时的 loss)。
先定义训练。为了训练,首先要定义 Training data 的结构,它由神经网络的输入输出决定。假定我们要实现的万能近似函数的输入输出都是数字,即可以表示一个高维函数,那么 Training data 的类型可以这么定义:
- type TrainingData = TrainingItem[]
- type TrainingItem = [number[], number[]]
也就是说,输入与输出可以是任意数量的数字,并且长度可以不等。
比如对于一个一元方程的 Training data 可以这么表示:
- const trainingData: TrainingData = [
- [[1], [3]],
- [[2], [6]],
- [[3], [9]],
- ];
每组 Training data input 和 output 的数量越多,表示的函数图像维度就越高,这里为了方便理解,我们用了单输入与单输出举例。
再定义创建神经网络。神经网络的架构包括该神经网络有几层,每层有几个神经元,每个神经元的启动函数是怎样的。为了简化,我们假设神经网络是 full connected(全连接) 的,即每个神经元的输入来自上一层所有神经元的输入。
每个输入层的定义如下:
- interface Layer {
- count: number;
- activation: "sigmoid" | "relu"; // 支持 sigmoid 和 relu 两种启动函数
- inputCount?: number; // 输入长度,仅第一层需要
- }
一个最简单的神经网络创建函数设计如下:
- const neuralNetwork = new NeuralNetwork({
- trainingData: trainingData,
- layers: [
- { count: 4, activation: "sigmoid", inputCount: 1 },
- { count: 1, activation: "sigmoid" },
- ],
- });
上面的 layers
描述了两层神经网络,输入层有 1 个节点,中间层有 4 个节点,输出层有 1 个节点(只有第一个层需要定义 inputCount
,因为输入长度是未知的)。
一个最小化神经网络,至少要定义 training data 与神经网络每一层的结构,其中每一层结构的 count
代表神经元的数量,activation
代表启动函数类型。当然这样设计有一个小瑕疵,即每个 layer 的神经元启动函数都相同。
接着,就可以调用 fit()
进行训练:
neuralNetwork.fit(); // { loss: .. }
调用几次就训练几次,每次都返回当前的 loss 值,以判断训练是否有效收敛。
接下我们讨论 NeuralNetwork
类的实现。首先这个函数显然包含以下几个要素:
定义神经网络对象实体,并存储各节点参数,以便训练时可以不断调整该网络的参数。
定义 model function。
定义 loss function。
定义 optimization。
其中 optimization 环节是最难的,因为我们要根据输出的结果,对每一个参数求导,这个过程被称为 “反向传播”,我们后续单开一篇文章来说明。
这次我们先采用一种模拟的方式绕过反向传播的具体实现,让代码能跑起来,方便理解全流程。
回到神经网络对象实体的设计,我们再来看一次神经网络的结构图:
可以看到从第二层网络开始,每一层网络的值都是上一层所有节点分别乘以不同的 w 系数,再加上 b 系数,最后乘以 c 系数,因此我们可以把参数存在第二层节点上,分别是:
w: number[]
,表示上一层每个节点连接到该节点乘以的系数 w。
b: number
,表示该节点的常数系数 b。
c: number
,表示该节点的常数系数 c。
我们可以定义神经网络数据结构如下:
- type NetworkStructor = Array<{
- // 启动函数类型
- activation: "sigmoid" | "relu";
- // 节点
- neurals: Array<{
- /** 当前该节点的值 */
- value: number | undefined;
- /** 上一层每个节点连接到该节点乘以的系数 w */
- w: Array<number>;
- /** 该节点的常数系数 b */
- b: number;
- /** 该节点的常数系数 c */
- c: number;
- }>;
- }>;
则我们根据用户传入的 layers
来初始化神经网络对象,并对每个参数赋予一个初始值:
- class NeuralNetwork {
- // 输入长度
- private inputCount = 0;
- // 网络结构
- private networkStructor: NetworkStructor;
- // 训练数据
- private trainingData: TraningData;
-
- constructor({
- trainingData,
- layers,
- }: {
- trainingData: TraningData;
- layers: Layer[];
- }) {
- this.trainingData = trainingData;
- this.inputCount = layers[0].inputCount!;
- this.networkStructor = layers.map(({ activation, count }, index) => ({
- activation,
- neurals: Array.from({ length: count }).map(() => ({
- value: undefined,
- w: Array.from({
- length: index === 0 ? this.inputCount : layers[index - 1].count,
- }).map(() => getRandomNumber()),
- b: getRandomNumber(),
- c: getRandomNumber(),
- })),
- }));
- }
- }
如上代码所示,将参数中输入长度、神经网络结构、训练数据都分别记录了下来,其中给每个神经节点 w、b、c 从 [-3, 3] 的随机初始值。
这样,我们就拥有了包含完整信息的神经网络结构,接下来只要分别实现 model function、loss function、optimization 就行了。
梳理一下 model function 的逻辑:Training data 每一条输入的长度为 inputCount
,它们 full connect 到 networkStructor
的第一层,然后经过后续所有层的处理最后达到输出层,输出层是一个数组。
modelFunction
的输入就是一个训练数据,输出是一个字符串数组,TraningItem
的结构上面已经提过:
type TrainingItem = [number[], number[]]
所以拿 traningItem
的第 0 项就是输入,modelFunction
就是根据输入得到预测的输出。
- class NeuralNetwork {
- /** 获取上一层神经网络各节点的值 */
- private getPreviousLayerValues(layerIndex: number, trainingItem: TraningItem) {
- if (layerIndex >= 0) {
- return this.networkStructor[layerIndex].neurals.map((neural) => neural.value);
- }
- return trainingItem[0];
- }
-
- private modelFunction(trainingItem: TraningItem) {
- this.networkStructor.forEach((layer, layerIndex) => {
- layer.neurals.forEach((neural) => {
- // 前置节点的值 * w 的总和
- let weightCount = 0;
- this.getPreviousLayerValues(layerIndex - 1, trainingItem).forEach(
- (value, index) => {
- weightCount += value * neural.w[index];
- },
- );
- const activateResult = activate(layer.activation)(weightCount + neural.b);
- neural.value = neural.c * activateResult;
- });
- });
-
- // 输出最后一层网络的值
- return this.networkStructor[this.networkStructor.length - 1].neurals.map(
- (neural) => neural.value
- );
- }
- }
modelFunction
函数的流程很简单,首先遍历类神经网络的每一层,计算其中每个神经元的值。
而计算这个值,取决于该网络上一层的每一个值,乘以权重 w,加上系数 b,并通过启动函数(activate
实现我们待会说)后再乘以系数 c。
第一层类神经网络的上一层来自于输入,从第二层开始,输入就来自于上一层神经网络,所以为了方便统一处理,定义了 visitPreviousLayerInputs
函数拿到上一层输入,兼容了第一层要从输入(Training data)拿的情况。
接下来是 activate
函数,它可以根据启动函数名生成对应的启动函数实现,代码如下:
- const functionByType = (functionType: ActivationType) => (x: number) => {
- switch (functionType) {
- case "sigmoid":
- return sigmoid(x);
- // ...
- }
- };
-
- const sigmoid = (z: number) => {
- return 1 / (1 + Math.pow(Math.E, -z));
- };
loss function 的输入也是 Training item,输出也是一个数字,这个数字就是 loss,越小越好。
计算 loss 有很多种选择,我们选择一种最简单的均方差:
- class NeuralNetwork {
- private lossFunction(trainingItem: TraningItem) {
- // 预测值
- const y = this.modelFunction(trainingItem);
- // 实际值
- const t = trainingItem[1];
- // loss 最终值
- let loss = 0;
-
- for (let i = 0; i < y.length; i++) {
- // l(t,y) = (t-y)²
- loss += Math.pow(t[i] - y[i]!, 2);
- }
-
- return loss / y.length;
- }
- }
首先执行 modelFunction
拿到预测值,再把与实际值的平方差加总,除以 Training item 的长度,就得到了均方差。
本来 optimazation 应该使用反向传播来实现,但因为实现比较复杂,我们在下一篇文章再说明。
但为了让故事完整,我们也可以采用迂回手段模拟 optimization 实现的:模拟求导。
把要求导的参数增加一个极小的值,其他参数不变,此时得到函数的输出减去上一次的函数值,就可以作为近似的偏导值,它反而最符合求导的本质:
optimization 的入参是 traningData
,返回值是此时的 loss。入参和出参与 lossFunction
一样,但这个函数是真的有在做优化,而 lossFunction
仅仅计算 loss。
模拟求导的实现有两个问题:
性能差,需要反复执行函数。
结果不精确,训练效果不好。
我们下一篇就来介绍反向传播。
我们从零到一构建一个神经网络,包括以下几个部分:
神经网络 API 的设计。
神经网络类的框架实现。
model function、loss function 的实现。
我们把 Training data 的每一项命名为 Training item,其中 model function、loss function 的入参是 Training item, optimization 的入参是 Training data,它们的含义分别如下:
modelFunction(traningItem)
: 输入一项训练数据,输出预测结果。
lossFunction(traningItem)
: 输入一项训练数据,输出当前神经网络的 loss。
optimization(traningData)
: 输入一组训练数据,让神经网络对该组训练数据进行一次 fit。
可以看到,神经网络训练好后,只需要调用 modelFunction
即可,不再需要求导,也不需要大量训练,调用成本相比训练时下降了好几个数量级。
因为篇幅问题,我们没有详细介绍 optimization
阶段,下一篇文章我们进入反向传播知识点。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。