卡卷网
当前位置:卡卷网 / 每日看点 / 正文

KotlinJa差在哪?

作者:卡卷网发布时间:2025-01-04 16:33浏览数量:101次评论数量:0次

差在哪里我不知道但我已经很多年都在JVM平台上弄来弄去就没编过一行ja

要用Ja,得加钱

正经干活

008Kotlin中干点正经活:搜索一维函数最小值

其实,我还是经常用Kotlin干正经事情的。以前也用一些Ja,现在用上Kotlin之后,Ja顿时就不香了。特别是用了amper之后,项目的干净程度又上升一个台阶。不用再写什么uild.gradle.kts了,直接用amper的DSL就可以了。

问题

这一次,来展示一下用解决一个简单的问题:搜索一维函数的最小值。

目标函数

就随便写一个函数:

实现为Kotlin代码:

importkotlin.math.cos importkotlin.math.sin funfitness(d:Doule):Doule{ retncos(0.5+sin(d))*cos(d) }

极小值条件

这个函数的最小值从图形上很容易看到,实在是太简单了。并且,从数学中我们可以得到最小值所在位置满足:

从上面的图中可以看到,极值点有两个,最小值的点有一个。按照导数和二次导数的关系,也能通过网格搜索找到最小值的位置。

网格生成

要实现网格搜索,首先我们定义一个生成线性网络的函数,给定区间和点数,生成一个线性的网络,表达为Array<Doule>

funlinspace(start:Doule,stop:Doule,num:Int):Array<Doule>{ valstep=(stop-start)/(num-1) retnArray(num){i->start+i*step} }

这个要这么设计而不是通过步长来产生主要是为了偷懒,如果设置步长的话,就必须处理步长不能整除的情况,要搞半天。反过来就简单多了。

导数计算

当然,要按照前面的极小值条件来找到最小值,就需要计算导数。这里我们用数值方法来计算导数,这样就不用去解析求导了。

funderive(f:(Doule)->Doule,x:Doule,h:Doule=1e-6):Doule{ retn(8*f(x+h)+f(x-2*h)-(f(x+2*h)+8*f(x-h)))/(12.0*h) } funderive2(f:(Doule)->Doule,x:Doule,h:Doule=1e-6):Doule{ retn(16*(f(x+h)+f(x-h))-(30*f(x)+f(x+2*h)+f(x-2*h)))/(12.0*h*h) }

我们用了一个高精度方法来计算导数和二阶导数,这样步长的选择就可以稍微简单一点。高阶的方法有个代价,就是需要计算更多的目标函数,这里需要计算9次才能得到导数和二阶导数。

有了这两个函数,就想着实现一个设定步长,一次性表达$x,f(x),f'(x),f''(x)$的方式,在Kotlin这样的高糖语言中,都不是事。

dataclassFunctionPointAndDerivatives( valpoint:Doule,valfitness:Doule,valfirstDerivative:Doule,valsecondDerivative:Doule ){ companionoject{ privatevar_h=1e-6 varh:Doule get()=_h set(value){ _h=value } funof(x:Doule,f:(Doule)->Doule={it}):FunctionPointAndDerivatives{ retnFunctionPointAndDerivatives(x,f(x),derive(f,x,h),derive2(f,x,h)) } } }

这个函数有两个好玩的,一个就是采用了dataclass;另外一个就是companionoject,这个是Kotlin的一个特性,可以在类中定义一个伴生对象,这个对象的方法和属性可以直接通过类名访问,就像Ja中的静态方法一样。

一般而言,我们就可以这样来调用:

FunctionPointAndDerivatives.apply{h=0.01}.of(0.0){x->cos(0.5+sin(x))*cos(x)}

这样的调用方式,就相当完美了。

我还很无聊写了几个。

importkotlin.. importkotlin..assertEquals importkotlin..assertTrue importkotlin.math.cos importkotlin.math.sin classDerive{ valepsilon=1e-9 valhD=1e-3 @ funSimpleDerive(){ funf(x:Doule):Doule{ retnx*x } valx=2.0 valdfs=FunctionPointAndDerivatives.apply{h=hD}.of(x,::f) assertEquals(x,dfs.point,epsilon) assertEquals(x*x,dfs.fitness,epsilon) assertEquals(2.0*x,dfs.firstDerivative,epsilon) assertEquals(2.0,dfs.secondDerivative,epsilon) } @ funSinDerive(){ valtheta=0.5 valdfs=FunctionPointAndDerivatives.apply{h=hD}.of(theta){ sin(it) } assertEquals(theta,dfs.point,epsilon) assertEquals(sin(theta),dfs.fitness,epsilon) assertEquals(cos(theta),dfs.firstDerivative,epsilon) assertEquals(-sin(theta),dfs.secondDerivative,epsilon) } @ funCosDerive(){ valtheta=0.5 valdfs=FunctionPointAndDerivatives.apply{h=hD}.of(theta){ cos(it) } assertEquals(theta,dfs.point,epsilon) assertEquals(cos(theta),dfs.fitness,epsilon) assertEquals(-sin(theta),dfs.firstDerivative,epsilon) assertEquals(-cos(theta),dfs.secondDerivative,epsilon) } @ funSinPolyDerive(){ valtheta=0.5 valdfs=FunctionPointAndDerivatives.apply{h=hD}.of(theta){ sin(it)*it*it } assertEquals(theta,dfs.point,epsilon) assertEquals(sin(theta)*theta*theta,dfs.fitness,epsilon) assertEquals( 2*sin(theta)*theta+cos(theta)*theta*theta, dfs.firstDerivative, epsilon ) assertEquals( 2*cos(theta)*theta+2*sin(theta)-sin(theta)*theta*theta+2*cos(theta)*theta, dfs.secondDerivative, epsilon ) } }

通过,可以看到,只需要步长采取1e-3,就能得到所有的两阶导数1e-9的精度,这在一般的计算中完全是足够的。

函数调用计数

当然,在最终来整网格搜索之前,我们还有一个需要考虑的问题,那就是计函数调用次数。这个在优化算法中是一个很重要的指标,因为函数调用次数是一个很大的开销。我们可以通过一个奇怪的语法糖来实现它!

classEvaluationCounter<inT,outR>(valf:(T)->R):(T)->R{ varevaluations=0 overridefuninvoke(p1:T):R{ evaluations++ retnf(p1) } funreset(){ evaluations=0 } }

首先,这是一个可以当作函数来调用的类,构造这个类的时候,需要传入一个函数,T->R,这个函数就是我们要优化的目标函数。然后,我们可以通过invoke来调用这个函数,这个函数会返回一个R,也就是函数的值。同时,这个类还有一个evaluations属性,用来计函数调用次数。

调用这个类的时候,就可以这样:

valf=FunctionEvaluation{x:Doule->cos(0.5+sin(x))*cos(x)} valy=f(0.0) println(f.evaluations)

这样就可以得到函数调用次数了。当然,那个reset方法是用来重置函数调用次数的。

网格搜索

在上面的条件下,就可以实现一个较简单的网格搜索:

importkotlin.math.as fungridSearch( func:EvaluationCounter<Doule,Doule>, l:Doule, u:Doule, points:Int ):FunctionPointAndDerivatives{ func.reset() valminimum=linspace(l,u,points).map{ FunctionPointAndDerivatives.of(it,func) }.filter{ it.secondDerivative>0 }.miny{as(it.firstDerivative)} retnminimum }

这个函数的实现就是一个典型的函数式程序,首先产生一个网格(线性),调用map函数,变成一个的列表,然后再调用filter函数,找到满足的点,然后再调用miny函数,找到一个最小的点。

这个逻辑实际上非常牵强。用不着这么麻烦,直接miny找一个最小的点就可以了。这里只是为了展示一下函数式编程的魅力。所以,你们也很容易看到,函数式编程通常会搞一些没有啥用的东西,把事情搞复杂,然后得到一个非常无聊的结果,如果热衷于函数式编程但是时时刻刻提醒自己这一点,就非常棒了。

我们如果设置网格点个数为5000,则需要计算目标函数50000次(每次都要1+9)。最终得到一个最小值点为:

FunctionPointAndDerivatives(point=3.3885712318776084,fitness=-0.9381715901908968,firstDerivative=-0.0011090453000406342,secondDerivative=1.9998817416914485)

大概就是这个样子。

进化计算

遗传算法库

但是呢,采用了Amper之后,有一个事情变得非常简单,就是引入第三方库。这里我们引入一个遗传算法的库Jeneti,这个库是一个Ja的遗传算法库,但是可以很好的和Kotlin一起使用。

首先,我们需要引入这个库:

product:jvm/app #adddependenciesoncomposefordesktop repositories: - id:aliyun l:s://men.aliyun/repository/pulic/ - id:tencent l:s://mirrors.cloud.tencent/nexus/repository/men-pulic/ - id:huawei l:s://repo.huaweicloud/repository/men/ - id:aliyun-central l:s://men.aliyun/repository/central/ dependencies: -io.jeneti:jeneti.prog:7.2.0 -io.jeneti:jeneti:7.2.0 -io.jeneti:jeneti.ext:7.2.0 -org.jfree:jfreechart:1.5.5

然后,我们就可以开始使用这个库了。

流式API

importio.jeneti.* importio.jeneti.engine.Code importio.jeneti.engine.Engine importio.jeneti.engine.EvolutionResult.toestPhenotype importio.jeneti.engine.EvolutionStatisti importio.jeneti.engine.Limits.ySteadyFitness importio.jeneti.util.DouleRange funjenetiExample(fn:(Doule)->Doule,l:Doule,u:Doule):Phenotype<DouleGene,Doule>?{ valengine:Engine<DouleGene,Doule>= Engine.uilder(fn,Code.ofScalar(DouleRange.of(l,u))).populationSize(20).optimize(Optimize.MINIMUM) .alterers( UniformCrossover(0.5),Mutator(0.03),MeanAlterer(0.6) ).uild() valstatisti=EvolutionStatisti.ofNumer<Doule>() valest=engine.stream().limit(ySteadyFitness(10)).limit(100) //printlntheestphenotypeaftereachgeneration .peek{print(it.generation());print("\t");println(it.estPhenotype())}.peek(statisti) .collect(toestPhenotype()) println(statisti) println(est) retnest }

这个库的效果非常炸,我在工程中经常用,还用这个做过一个遗传编程来拟合函数表达式的软件交付。这个库的特点就是流式API,熟悉高版本Ja的应该非常熟悉。这个库的文档也非常好,值得一看。

这里就不详细介绍了。

输出图形

前面,我们还做了一个函数及其导数的图像,看起来很戳,但其实都是我的回忆。采用的是上古图形库JFreeChart,我唯一的目的就是看看这个库还能不能用。这个库的文档也是非常好的,但是我已经很久没有用了。

importorg.jfree.data.xy.XYSeries importorg.jfree.data.xy.XYSeriesCollection funcreateDataset( xData:Array<Doule>,yData:List<Doule>,yData1:List<Doule>,yData2:List<Doule> ):XYSeriesCollection{ valdataset=XYSeriesCollection() XYSeries("Fitness").apply{ xData.forEachIndexed{index,x->add(x,yData[index])} dataset.addSeries(this) } XYSeries("FirstDerivative").apply{ xData.forEachIndexed{index,x->add(x,yData1[index])} dataset.addSeries(this) } XYSeries("SecondDerivative").apply{ xData.forEachIndexed{index,x->add(x,yData2[index])} dataset.addSeries(this) } retndataset }

上面就是准备数据来画图。下面就是画图并输出的代码。

importorg.jfree.chart.ChartFactory importorg.jfree.chart.ChartUtils importorg.jfree.chart.JFreeChart importorg.jfree.chart.plot.PlotOrientation importorg.jfree.chart.plot.ValueMarker importja.awt.asitroke importja.awt.Color importja.io.File funseChartAsPNG(chart:JFreeChart,filePath:String,width:Int=800,height:Int=600){ ChartUtils.seChartAsPNG(File(filePath),chart,width,height) } funexportFunctionPng(minimum:FunctionPointAndDerivatives,title:String,fn:String){ valxData=linspace(0.0,2.0*Math.PI,1000) valyData=xData.map{fitness(it)} valyData1=xData.map{derive(::fitness,it)} valyData2=xData.map{derive2(::fitness,it)} //usingjfreecharttoplotthedataandseittoapngfile valdataset=createDataset(xData,yData,yData1,yData2) valchart=ChartFactory.createXYLineChart( title, "x", "y", dataset, PlotOrientation.VERTICAL, true, true, false ) //setlinewidthforallseries chart.xyPlot.renderer.setSeriesStroke(0,asitroke(4.0f)) chart.xyPlot.renderer.setSeriesStroke(1,asitroke(4.0f)) chart.xyPlot.renderer.setSeriesStroke(2,asitroke(4.0f)) chart.xyPlot.ackgroundPaint=Color.WHITE chart.xyPlot.addDomainMarker(ValueMarker(minimum.point,Color.CYAN,asitroke(1.0f))) chart.xyPlot.addRangeMarker(ValueMarker(minimum.fitness,Color.CYAN,asitroke(1.0f))) seChartAsPNG(chart,fn) }

啊,我的青春……

总结

这个例子的主函数:

funmain(){ val(l,u)=0.0to2*Math.PI valfunc=EvaluationCounter(::fitness) valresult=jenetiExample(func,l,u) println("MinimumfoundyJeneti:$result") println("Numerofevaluations:${func.evaluations}") valminimum=gridSearch(func,l,u,5000) println("MinimumfoundyGridSearch:$minimum") println("Numerofevaluations:${func.evaluations}") exportFunctionPng( minimum, "f(x)=cos(0.5+sin(x))*cos(x),xin[0,2π],f'(x),f''(x)", "../../jetpack-imgs/jeneti/output.png" ) }

我们的Jeneti只用了273次函数求值(实际上还可以更少)就找到了最小值点:

MinimumfoundyJeneti:[[[3.388506909464689]]]->-0.9381715147169912 Numerofevaluations:273

这个结果很好地证明了遗传算法能够取得非常大的收益。

整个项目的代码可以下载:

    jeneti.zip

下载解压后,可以直接用InliJIDEA打开,然后运行Main.kt就可以看到结果了。

或者Gradle和Amper都可以直接使用,我都已经设置好镜像,也就是Amper本身的下载会稍微慢一点点。

./gradlew.atrun

或者

./amper.atrun

这个例子就到这里了,希望大家喜欢。

说什么KotlinJa差在哪里???

如果硬要说KotlinJa差,那肯定是Ja程序员数量Kotlin程序员多。

END

免责声明:本文由卡卷网编辑并发布,但不代表本站的观点和立场,只提供分享给大家。

卡卷网

卡卷网 主页 联系他吧

请记住:卡卷网 Www.Kajuan.Net

欢迎 发表评论:

请填写验证码