热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java代码训练一个神经元_神经网络入门第2章编写第一个神经元

前言神经网络是一种很特别的解决问题的方法。本书将用最简单易懂的方式与读者一起从最简单开始,一步一步深入了解神经网络的基础算法。本书将尽量避开让人望而生畏的名词和数学概

前言

神经网络是一种很特别的解决问题的方法。本书将用最简单易懂的方式与读者一起从最简单开始,一步一步深入了解神经网络的基础算法。本书将尽量避开让人望而生畏的名词和数学概念,通过构造可以运行的Java程序来实践相关算法。

关注微信号“逻辑编程"来获取本书的更多信息。

这一章我们来编写一个最简单的神经元来完成一个函数的功能。代码一共几十行。但是这几十行是一个很重要的起点。我们通过本章将掌握神经网络的最基本原理和训练方法。

假设我们有这么一个问题:给出一个x值,需要程序给出对应的y值。我们知道x和y有一定的线性关系,但是我们不知道具体的参数。还好我们有一些已知的数据对(x,y)可供研究学习。我们现在通过一个简单的单个神经元来解决这个问题。

这个最简单的神经元计算这样一个函数: y = w*x + b。 在几何上它是一条直线。其中w表示斜率,b表示对原点的偏移量。这两个参数决定了直线在坐标系的位置。也就是说这个函数和两个参数决定了我们这个神经元的输入与输出的关系。我们把w叫做权重(weight),b叫做偏差(bias)。

我们把这个神经元按照面向对象的方法可以写出如下代码:

public class SingleNeuron {

double weight;

double bias;

public SingleNeuron(double weight, double bias){

this.weight=weight;

this.bias=bias;

}

double f(double x){

return x * weight +bias;

}

}

只要用合适的参数就能构造出一个神经元,它能根据输入的x值给出相应的y值。但是,我们不知道这两个参数,我们需要通过一组已知的输入和输入来训练获取相应的参数。

通常我们提供一些已经知道结果的数据集,通过学习让程序自动找到合适的参数。这里为了方便测试,我们先假设一个目标参数,并且根据这个目标参数生成测试数据。

protected SingleNeuron getTarget() {

return new SingleNeuron(3, 3);

}

public double[][] generateTrainingData(int size) {

Random rand = new Random(System.nanoTime());

double[][] data = new double[size][];

SingleNeuron target = getTarget();

for (int i = 0; i

double x = rand.nextDouble() * 100;

double y = target.f(x);

data[i] = new double[]{x, y};

}

return data;

}

下面我们讨论怎么使用这些训练数据来做训练。训练这个词看上去很神秘,但本质上的原理还是比较简单的。

训练时,我们希望我们的神经元在计算上述x时,能得出与上面y值最接近的值。也就是说如果我们计算的值是a = w*x + b, 我们希望 c = |a - y| 的值最小。也就是 c = |w*x + b - y| 的值最小。现在我们已经知道了一些x和y, 我们需要知道的是w和b。所以对每个输入训练参数,我们可以产生一个不同的c函数。我们要求这个函数 c(w,b)的极小值时的w和b。

这里我们衍生出了一个新的函数c,它完全不是我们神经元原本的函数。它在神经网络里叫做成本函数(cost)。我们给神经元添加下面的函数。我们这里暂且不考虑c = |a - y|中绝对值的问题。直接返回可能是正也可能是负的值。

double cost(double x, double y){

return f(x)-y;

}

那么我们怎么求函数的极值呢?

先从一个低纬度的例子来看一看。如果是一元函数,也就是平面坐标系里的一条曲线(直线没极值),这条曲线的y一般先随x值增大而减小,然后到达极小值,再变为随x 值增大再逐步增大。比如下面这条抛物线 y = x^2 在x=0处取到最小值。

ce0a88ce7b0dec295652333791c247f9.png

假设我们随意选择一个x值,在上边曲线上面像坐滑梯一样向下滑,我们就能到达底部最小值处。用稍微数学一点的语言就是说,我们随意选一个起始点,那我们就沿着斜率向下(与斜率相反)的方向移动x。y = x^2在任意一点的斜率是 2*x 。这个斜率在微积分里叫做导数或者微分。对 y = x^2 我们可以写如下代码来移动x到最低点:

double x = 1;

while ( 2*x > 0.01 ){

if ( 2*x > 0 ) x -= 0.005;

if ( 2*x <0) x &#43;&#61; 0.005;

}

return x;

其中0.005是我们的步长。步长太小&#xff0c;循环次数就会变多&#xff1b;步长太大&#xff0c;可能直接迈过了最低点&#xff0c;反而去不到最小值。这里我们用0.01>0.005作为循环条件就是避免步子迈太大。如果我们用 2*x&#xff1d;&#xff1d;0作为跳出条件&#xff0c; 我们可能永远也达不到&#xff0c;因为步子大小是固定的&#xff0c;可能总是迈过最小点。(并且double值不应该用等号判断相等。)上面的例子里可以可以直接看出函数最小值的点&#xff0c;但我们只是以此演示更基本原理。有时候函数很复杂&#xff0c;不是这么容易找出最小值。

那么回到我们的c(w,b)函数&#xff0c;它是一个二元函数&#xff0c;如何求它取最小值(或者说足够小的值)时的w和b呢&#xff1f;

二元函数在三维空间坐标系里上可以形成一个曲面&#xff0c;我们要找这个曲面的最低点。好比在一个山谷里&#xff0c; 我们要沿着一条线下到谷底(高度最低处)。跟上边二维坐标里的曲线类似。但是我们现在有两个变量&#xff0c;好比我们在山谷里有东西和南北两个维度。沿着东西方向走&#xff0c;我们可以选择东方和西方两个方向中下降的方向&#xff1b;沿着南北方向&#xff0c;我们可以选择南方或者北方。或许东西方向一样高度&#xff0c;正南方向或者正北方向就是下山谷最快的方向&#xff1b;也或许向西是下降方向&#xff0c;向南也是下降方向&#xff0c;此时某个西南方向肯定是下降最快的方向&#xff0c;这个西南方向是西方和南方两个下降速度的综合&#xff0c;是两个矢量&#xff0c;类似于物理里的两个不同方向力的合力。这个下降最快的方向我们称之为梯度。我们现在要按照这个梯度方向下降&#xff0c;所以我们迈开步子&#xff0c;朝这个西南方向出发。具体的方向取决于两个方向下降的速度的比值。但是在程序里其实很好处理&#xff0c;我们有两个变量&#xff0c;让它们各自按照自己的下降速度(或者说斜率、偏导数、偏微分)下降就行了。

就像在山谷中找出东西和南北两个方向的斜率一样&#xff0c;我们可以从两个变量各自的维度考虑c(w,b)这个二元函数的梯度。由于c &#61; |w*x &#43; b - y|中绝对值的存在&#xff0c;我们需要对函数c(w,b)的斜率分段考虑。我们先去掉绝对值符号。

只考虑w维度&#xff1a;c(w) &#61; w*x 这个函数是一条直线&#xff0c;斜率是x。

只考虑b维度&#xff1a;c(b) &#61; b 这个函数也是一条直线&#xff0c;斜率是1。

我们可以总结出求多元函数的在某个维度的斜率(偏导数)时仅仅需要将其它变量看作常数。

这两个斜率在给定的某个训练数据(x,y)时&#xff0c;都是常数。所以我们这座山非常简单&#xff0c;就是从两个坐标方向看都是固定斜率的斜坡。根本没有谷底。这是因为我们忽略了绝对值符号。

如果考虑绝对值符号&#xff0c;当cost&#61;w*x&#43;b-y>0和cost<0时&#xff0c;其梯度方向是相反的。我们将会有一条谷底是直线&#xff0c;并非一个点。这也是因为&#xff0c;二元函数y&#61;w*x&#43;b在只给出一个(x,y)时是有无穷多个(w,b)的解的。这些解组成一条直线。当有两组(x,y)时我们可以确定(w,b)的值。

下图是二元函数c &#61; |w*x &#43; b - y|的图像化表示&#xff0c;其中c值较大的显示红色&#xff0c;较小的显示黑色。实际上黑色山谷的横截面是一个V字型。而黑色最低处形成一条直线。当我们有两个输入数据时&#xff0c;我们就有两条直线山谷&#xff0c;它们的交点就是我们的目的地。当有三条或者更多时&#xff0c;它们可能不相交于同一点&#xff0c;现实世界中的很多数据虽然接近某个模型&#xff0c;但是难免有误差。这时我们找到一个接近几个交点的地方就可以了。

4fff7e7fcc48b7119307b566db6eda59.png

上图在工具中使用的变量名根据工具的要求&#xff0c;必须使用red, x, y来代替c, w, b。其中的(5,20)实际上相当于训练时已知的(x,y)。

我们去掉c(w,b)的绝对值的话&#xff0c;山谷就消失了&#xff0c;变成了一个空间中的倾斜平面。即c(w,b) &#61; w*x &#43; b - y&#xff0c;它的的梯度是(x, 1)。

// c &#61; w*x &#43; b - y

double[] gradient(double x, double y){

return new double[]{x, 1};

}

我们需要根据cost()返回值的正负号来获得带绝对值的cost函数的方向&#xff0c;这样才能靠近c接近零的点-也就是绝对值最小的点。我们这里干脆让两个偏导数乘以c获得带绝对值符号的cost函数的导数。除以较大数的绝对值是因为斜率虽然大&#xff0c;我们距离目的地或许不远&#xff0c;步子太大就跨过最小点了。后面乘以-1&#xff0c;向梯度反方向移动。因此我们的成本函数梯度可以写成这样&#xff1a;

public double[] gradient(double x, double y) {

double c &#61; cost(x, y);

double dw &#61; x * c;

double db &#61; 1 * c;

double d &#61; Math.max(Math.abs(dw), Math.abs(db));

if (d &#61;&#61; 0) { d &#61; 1; }

return new double[]{-dw / d, -db / d};

}

接下来&#xff0c;我们可以考虑开始训练。对每一个输入的训练数据&#xff0c;我们按照上边说的方法&#xff0c;分别在两个变量上迈开步子往谷底走一小步。这就是梯度下降算法。

public void train(double[][] data, double rate) {

for (int i &#61; 0; i

double x &#61; data[i][0];

double y &#61; data[i][1];

double[] gradient &#61; gradient(x, y);

weight &#43;&#61; gradient[0] * rate;

bias &#43;&#61; gradient[1] * rate;

}

}

上面的函数我们引入了rate参数来控制步子的大小。同时我们循环在每个输入样本上作。当我们有2个的训练数据时&#xff0c;我们每次往1条山谷垂直方向迈小步&#xff0c;然后向另外1条山谷的垂直方向迈一小步&#xff0c;走出一条之字形折线。这样最终我们能走到两条山谷的交点附近。

bb0e938c62475ac7f7a9c4d50073ec22.png

从上图中我们也可以看到&#xff0c;当接近终点时有可能在某个维度上摇摆或者先到达目标值附近。

最后&#xff0c;我们用一个main方法来实现一个训练过程。先任意给出我们的(w,b)初始值&#xff0c;这里给了(0,0)。然后在这个程序里循环使用了这些样本100次&#xff0c;因为我们的步子很小&#xff0c;不重复走&#xff0c;我们迈不到谷底。

public static void main(String... args) {

SingleNeuron n &#61; new SingleNeuron(0, 0);

//target: y &#61; 3*x &#43; 3;

double rate &#61; 0.1;

int epoch &#61; 100;

int trainingSize &#61; 20;

for (int i &#61; 0; i

double[][] data &#61; n.generateTrainingData(trainingSize);

n.train(data, rate);

System.out.printf("Epoch: %3d,  W: %f, B: %f \n", i, n.weight, n.bias);

}

}

下面是我们可以运行的完整程序。读者可以试着运行它。

package com.luoxq.ann.single;

import java.util.Random;

public class SingleNeuron {

double weight;

double bias;

public SingleNeuron(double weight, double bias) {

this.weight &#61; weight;

this.bias &#61; bias;

}

public double f(double x) {

return x * weight &#43; bias;

}

public double cost(double x, double y) {

return f(x) - y;

}

// c &#61; w*x &#43; b - y

public double[] gradient(double x, double y) {

double c &#61; cost(x, y);

double dw &#61; x * c;

double db &#61; 1 * c;

double d &#61; Math.max(Math.abs(dw), Math.abs(db));

return new double[]{-dw / d, -db / d};

}

public void train(double[][] data, double rate) {

for (int i &#61; 0; i

double x &#61; data[i][0];

double y &#61; data[i][1];

double[] gradient &#61; gradient(x, y);

weight &#43;&#61; gradient[0] * rate;

bias &#43;&#61; gradient[1] * rate;

}

}

protected SingleNeuron getTarget() {

return new SingleNeuron(3, 3);

}

public double[][] generateTrainingData(int size) {

Random rand &#61; new Random(System.nanoTime());

double[][] data &#61; new double[size][];

SingleNeuron target &#61; getTarget();

for (int i &#61; 0; i

double x &#61; rand.nextDouble() * 100;

double y &#61; target.f(x);

data[i] &#61; new double[]{x, y};

}

return data;

}

public static void main(String... args) {

SingleNeuron n &#61; new SingleNeuron(0, 0);

//target: y &#61; 3*x &#43; 3;

double rate &#61; 0.1;

int epoch &#61; 100;

int trainingSize &#61; 20;

for (int i &#61; 0; i

double[][] data &#61; n.generateTrainingData(trainingSize);

n.train(data, rate);

System.out.printf("Epoch: %3d,  W: %f, B: %f \n", i, n.weight, n.bias);

}

}

}

思考

1. 请读者改变rate、epoch或者trainingSize看对学习的速度和精度有哪些影响。

2. 如果c&#61;|a-y|不用绝对值方法&#xff0c;而改用c&#61;(a-y)*(a-y)&#xff0c;如何求导&#xff0c;会有什么效果。它的梯度还是一个平面吗。

3. 试对训练数据作些修改&#xff0c;使之有一定偏移量&#xff0c;看效果如何。

关注微信号“逻辑编程"来获取本书的更多信息。



推荐阅读
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文介绍了一个Java猜拳小游戏的代码,通过使用Scanner类获取用户输入的拳的数字,并随机生成计算机的拳,然后判断胜负。该游戏可以选择剪刀、石头、布三种拳,通过比较两者的拳来决定胜负。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 从零学Java(10)之方法详解,喷打野你真的没我6!
    本文介绍了从零学Java系列中的第10篇文章,详解了Java中的方法。同时讨论了打野过程中喷打野的影响,以及金色打野刀对经济的增加和线上队友经济的影响。指出喷打野会导致线上经济的消减和影响队伍的团结。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
author-avatar
多米音乐_34306427
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有