Nano-neuron-7个简单的JavaScript函数,显示机器如何“学习”

纳米神经元是神经网络概念的简化版本。 纳米神经元执行最简单的任务,并受过训练,可以将温度从摄氏温度转换为华氏温度。


NanoNeuron.js代码由7个简单的JavaScript函数组成,这些函数涉及模型信号的学习,训练,预测以及正向和反向传播。 编写这些功能的目的是为读者提供有关机器如何能够“学习”的最少的基本解释(直觉)。 该代码不使用第三方库。 俗话说,只有简单的“香草” JavaScript函数。


这些功能绝不是机器学习的详尽指南。 许多机器学习概念缺失或简化了! 简化的唯一目的是使读者对机器如何在原理上“学习”有最基本的了解和直觉,因此,“机器学习的魔力”对读者来说越来越像“机器学习的数学”。


纳米神经元


我们的纳米神经元将“学习”什么


您可能听说过神经网络中的神经元 。 纳米神经元是同一神经元的简化版本。 在此示例中,我们将从头开始编写其实现。 为简单起见,我们不会建立纳米神经元网络。 我们将专注于创建单个纳米神经元,并尝试教他如何将温度从摄氏温度转换为华氏温度。 换句话说,我们将教他基于摄氏温度来预测华氏温度。


顺便说一下,将摄氏度转换为华氏度的公式如下:


天鹅到华氏度


但是目前,我们的纳米神经元对此公式一无所知...


纳米神经元模型


首先创建一个描述纳米神经元模型的函数。 该模型是xy之间的简单线性关系,如下所示: y = w * x + b 。 简而言之,我们的纳米神经元是可以在XY坐标系中绘制直线的孩子。


变量wb是模型参数 。 纳米神经元仅知道线性函数的这两个参数。 这些参数正是我们的纳米神经元将在训练过程中学习的参数。


纳米神经元在此阶段唯一可以做的就是模拟线性关系。 他在predict()方法中执行此操作,该方法在输入处获取变量x ,并在输出处预测变量y 。 没魔术


 function NanoNeuron(w, b) { this.w = w; this.b = b; this.predict = (x) => { return x * this.w + this.b; } } 

_(...等等... 线性回归是您还是什么?)_


将摄氏度转换为华氏度


可以根据以下公式将以摄氏度为单位的温度转换为华氏度: f = 1.8 * c + 32 ,其中c是以摄氏度为单位的温度, f是以华氏度为单位的温度。


 function celsiusToFahrenheit(c) { const w = 1.8; const b = 32; const f = c * w + b; return f; }; 

结果,我们希望纳米神经元能够模拟这一特定功能。 他将不得不猜测(学习)参数w = 1.8b = 32而无需事先知道。


这就是转换函数在图表上的外观。 这就是我们的纳米神经“婴儿”必须学会“吸引”的东西:


摄氏到华氏转换


资料产生


在经典编程中,我们知道输入数据( x )和用于转换该数据的算法(参数wb ),但是输出数据( y )是未知的。 使用已知算法基于输入来计算输出。 相反,在机器学习中,仅输入和输出数据( xy )是已知的,但是从x切换到y的算法y未知的(参数wb )。


这就是我们现在要做的输入和输出的生成。 我们需要生成用于训练模型的数据和用于测试模型的数据。 celsiusToFahrenheit()辅助函数将帮助我们解决此问题。 每个训练和测试数据集都是xy对的集合。 例如,如果x = 2 ,则y = 35,6 ,依此类推。


在现实世界中,大多数数据很可能是收集的而不是生成的 。 例如,这样收集的数据可以是成对的“脸部照片”->“人的名字”的集合。

我们将使用TRAINING数据集来训练我们的纳米神经元。 在他长大并能够自己做出决定之前,我们必须使用训练集中的“正确”数据来教他什么是“真”,什么是“假”。


顺便说一句,这里明确地遵循了生活原则“入口处的垃圾-出口处的垃圾”。 如果纳米神经元对训练套件“撒谎”,将5°C转换为1000°F,则在多次迭代训练后,他会相信这一点,并将正确转换 5°C 以外的所有温度值。 我们需要非常谨慎,每天将训练数据加载到大脑神经网络中。

分心。 让我们继续。


我们将使用TEST数据集来评估我们的纳米神经元的训练水平,并可以对他在训练期间未看到的新数据做出正确的预测。


 function generateDataSets() { // xTrain -> [0, 1, 2, ...], // yTrain -> [32, 33.8, 35.6, ...] const xTrain = []; const yTrain = []; for (let x = 0; x < 100; x += 1) { const y = celsiusToFahrenheit(x); xTrain.push(x); yTrain.push(y); } // xTest -> [0.5, 1.5, 2.5, ...] // yTest -> [32.9, 34.7, 36.5, ...] const xTest = []; const yTest = []; //   0.5    1,       //   ,       . for (let x = 0.5; x < 100; x += 1) { const y = celsiusToFahrenheit(x); xTest.push(x); yTest.push(y); } return [xTrain, yTrain, xTest, yTest]; } 

预测误差估计


我们需要一个确定的指标(度量,数量,等级),以显示纳米神经元的预测与真实情况的接近程度。 换句话说,此数字/度量/功能应显示纳米神经元的正确与否。 就像在学校一样,学生的控制成绩为52


对于纳米神经元,其y的真实值与预测的预测值之间的误差(误差)将由以下公式产生:


预测成本


从公式可以看出,我们将误差视为两个值之间的简单差异。 值彼此越接近,差异越小。 我们在这里使用平方来消除符号,因此最后(1 - 2) ^ 2等效于(2 - 1) ^ 2 。 除以2只是为了简化该函数的导数在信号反向传播的公式中的含义(下面对此有更多说明)。


在这种情况下,错误函数将如下所示:


 function predictionCost(y, prediction) { return (y - prediction) ** 2 / 2; // ie -> 235.6 } 

直接信号传播


通过我们的模型直接传播信号意味着要从xTrainyTrain训练数据集中对所有对进行预测,并计算这些预测的平均误差(错误)。


我们只是让我们的纳米神经元“说出来”,使其做出预测(转换温度)。 同时,在这个阶段的纳米神经元可能是非常错误的。 预测误差的平均值将向我们显示当前模型与模型的真实距离。 此处的误差值非常重要,因为通过更改参数wb并再次直接传播信号,我们可以评估我们的纳米神经元是否已通过新参数变得“更智能”。


纳米神经元的平均预测误差将使用以下公式执行:


平均费用


其中m是训练副本的数量(在我们的例子中,我们有100数据对)。


这是我们可以在代码中实现的方法:


 function forwardPropagation(model, xTrain, yTrain) { const m = xTrain.length; const predictions = []; let cost = 0; for (let i = 0; i < m; i += 1) { const prediction = nanoNeuron.predict(xTrain[i]); cost += predictionCost(yTrain[i], prediction); predictions.push(prediction); } //     . cost /= m; return [predictions, cost]; } 

信号反向传播


现在我们知道我们的纳米神经元的预测是对还是错(基于误差的平均值),如何使预测更准确?


反向信号传播将对此有所帮助。 信号反向传播是评估纳米神经元的误差,然后调整其参数wb ,以使整个训练数据集对纳米神经元的下一个预测变得更加精确。


这是机器学习变得神奇的地方。 这里的关键概念是函数派生 ,它显示了要达到函数的最小值(在我们的情况下为误差函数的最小值),我们需要采取什么大小步长和采取哪种方式。


训练纳米神经元的最终目标是找到误差函数的最小值(请参见上面的函数)。 如果我们能够找到wb这样的值,它们的误差函数的平均值很小,那么这意味着我们的纳米中子能很好地应对以华氏度为单位的温度预测。


派生类是一个很大的主题,我们将不在本文中介绍。 MathIsFun是一个很好的资源,可以提供对派生的基本了解。


我们必须从导数的本质中学习到的一件事,这将有助于我们理解信号的反向传播是如何定义的,根据定义,该函数在特定点xy处的导数是该函数在xy处的曲线的切线。 y并向我们指示函数最小值的方向


导数斜率


图片取自MathIsFun


例如,在上图中,您看到在点(x=2, y=4)切线(x=2, y=4)斜率表明我们需要 才能达到函数的最小值。 还要注意,切线的斜率越大,我们必须更快地移到最小点。


averageCost参数wb平均误差函数averageCost的导数将如下所示:


dW


分贝


其中m是训练副本的数量(在我们的例子中,我们有100数据对)。


您可以在此处详细了解如何使用复杂函数的派生。


 function backwardPropagation(predictions, xTrain, yTrain) { const m = xTrain.length; //           'w'  'b'. //      0. let dW = 0; let dB = 0; for (let i = 0; i < m; i += 1) { dW += (yTrain[i] - predictions[i]) * xTrain[i]; dB += yTrain[i] - predictions[i]; } //    . dW /= m; dB /= m; return [dW, dB]; } 

模型训练


现在我们知道了如何针对所有训练数据(直接信号传播)估算纳米神经元模型预测的误差。 我们还知道如何调整纳米神经元模型的参数wb (信号的反向传播)以提高预测的准确性。 问题是,如果仅对信号执行一次正向和反向传播,那么对于我们的模型来说,这不足以识别和学习训练数据中的依存关系和定律。 您可以将其与学生的一日游进行比较。 他/她必须日复一日,年复一年地定期上学,以便学习所有材料。


因此,我们必须多次重复信号正向和反向传播。 这trainModel()函数的trainModel() 。 她就像我们的纳米神经元模型的“老师”:


  • 她将花一些时间( epochs )与我们仍然愚蠢的纳米神经元,试图训练他,
  • 她将使用特殊书籍( xTrainyTrain数据集)进行培训,
  • 它鼓励我们的“学生”使用alpha参数更努力地(更快地)学习,该参数实质上控制了学习的速度。

关于alpha参数的几句话。 这只是变量dWdB的值的系数(乘数),我们在信号的反向传播期间进行了计算。 因此,导数向我们显示了误差函数最小值的方向( dWdB值的符号告诉我们)。 导数还向我们展示了我们需要多快才能朝函数的最小值移动( dWdB的绝对值告诉我们)。 现在,我们需要将步长乘以alpha ,以便将我们的方法的速度调整到最小(总步长)。 有时,如果我们对alpha使用较大的值,则步幅可能很大,以至于我们可以简单地跳过函数最小值,从而跳过该函数。


类似于“老师”,她强迫我们的“纳米学生”学习的能力越强,他学习的速度就越快,但是,如果您用力对他施加压力,那么我们的“纳米学生”可能会感到精神崩溃,完全无动于衷,他根本不会学到任何东西。


我们将更新模型wb的参数,如下所示:


w


b


这就是训练本身的样子:


 function trainModel({model, epochs, alpha, xTrain, yTrain}) { //     -.  . const costHistory = []; //    ()  for (let epoch = 0; epoch < epochs; epoch += 1) { //   . const [predictions, cost] = forwardPropagation(model, xTrain, yTrain); costHistory.push(cost); //   . const [dW, dB] = backwardPropagation(predictions, xTrain, yTrain); //    -,    . nanoNeuron.w += alpha * dW; nanoNeuron.b += alpha * dB; } return costHistory; } 

将所有功能放在一起


是时候一起使用所有以前创建的功能了。


创建纳米神经元模型的实例。 此刻,纳米神经元对参数wb应该是什么一无所知。 因此,让我们随机设置wb


 const w = Math.random(); // ie -> 0.9492 const b = Math.random(); // ie -> 0.4570 const nanoNeuron = new NanoNeuron(w, b); 

我们生成培训和测试数据集。


 const [xTrain, yTrain, xTest, yTest] = generateDataSets(); 

现在,让我们尝试使用小步长( 0.0005 )训练我们的模型70000时代。 您可以试验这些参数,它们是凭经验确定的。


 const epochs = 70000; const alpha = 0.0005; const trainingCostHistory = trainModel({model: nanoNeuron, epochs, alpha, xTrain, yTrain}); 

让我们检查一下模型的误差值在训练过程中如何变化。 我们期望训练后的误差值应明显小于训练前的误差值。 这将意味着我们的纳米神经元更明智。 相反的选择也是可能的,当训练后,预测误差仅增加(例如,学习步骤alpha值较大)。


 console.log('  :', trainingCostHistory[0]); // ie -> 4694.3335043 console.log('  :', trainingCostHistory[epochs - 1]); // ie -> 0.0000024 

这是训练期间模型误差的值如何变化。 x轴上是历元(以千为单位)。 我们预计图表将下降。


培训过程


让我们看看我们的纳米神经元“学习了”哪些参数。 我们希望参数wbcelsiusToFahrenheit()函数中的同名参数相似( w = 1.8b = 32 ),因为我试图模拟的是她的纳米神经元。


 console.log(' -:', {w: nanoNeuron.w, b: nanoNeuron.b}); // ie -> {w: 1.8, b: 31.99} 

如您所见,纳米神经元非常接近celsiusToFahrenheit()函数。


现在,让我们看看我们的纳米神经元的预测对于他在训练期间未看到的测试数据的准确性如何。 测试数据的预测误差应接近训练数据的预测误差。 这将意味着纳米神经元已经学会了正确的依赖性,并且可以从以前未知的数据(这是模型的全部价值)中正确地提取其经验。


 [testPredictions, testCost] = forwardPropagation(nanoNeuron, xTest, yTest); console.log('   :', testCost); // ie -> 0.0000023 

现在,由于我们的“纳米婴儿”在“学校”中受过良好的训练,并且现在即使对于他看不见的数据,也知道如何将摄氏度准确转换为华氏度,因此我们可以称他为“聪明”。 现在我们甚至可以向他咨询温度转换方面的建议,这就是整个培训的目的。


 const tempInCelsius = 70; const customPrediction = nanoNeuron.predict(tempInCelsius); console.log(`- "",  ${tempInCelsius}°C   :`, customPrediction); // -> 158.0002 console.log('  :', celsiusToFahrenheit(tempInCelsius)); // -> 158 

很近! 像人一样,我们的纳米神经元是好的,但不是完美的:)


编码成功!


如何运行和测试纳米神经元


您可以克隆存储库并在本地运行纳米神经元:


 git clone https://github.com/trekhleb/nano-neuron.git cd nano-neuron 

 node ./NanoNeuron.js 

遗漏的概念


为了便于说明,以下机器学习概念已被省略或简化。


分离训练和测试数据集


通常,您只有一个大数据集。 根据该套中的份数,可以按70/30的比例将其分为训练和测试套。 集合中的数据必须先随机混合后才能拆分。 如果数据量很大(例如,数百万),则可以按接近90/10或95/5的比例划分测试集和训练集。


在线电源


通常,仅使用一个神经元时,您不会找到病例。 力量在这种神经元的网络中 。 神经网络可以学习更复杂的依赖关系。


同样在上面的示例中,我们的纳米神经元看起来更像是简单的线性回归,而不是神经网络。


输入归一化


在训练之前,习惯上将输入数据标准化


向量执行


对于神经网络,矢量(矩阵)计算比for循环中的计算快得多。 通常,使用例如Python Numpy库的矩阵运算执行正向和反向信号传播。


最小误差函数


我们用于纳米神经元的误差函数非常简化。 它应该包含对数成分 。 误差函数公式的改变也将导致信号向前和向后传播的公式的改变。


激活功能


通常,神经元的输出值通过激活函数。 为了激活,可以使用诸如SigmoidReLU和其他函数。

Source: https://habr.com/ru/post/zh-CN479220/


All Articles