文章目录
导读
终于,使用Ubuntu
系统的我遇到了最尴尬的问题:建模工具Matlab
没有linux
发行版。虽然可以使用wine
勉强跑起来,但毕竟不是运行在Windows
内核下,所以往往有很多事情都非常尴尬。所以,我看上了这一款:octave
。不仅支持Ubuntu
,还能够兼容Matlab
,是非常不错的替代品。
界面展示
先看看octave
的软件界面,体验一下吧。
可以看到左边由上至下依次是:文件目录、工作空间和历史记录,而右边则是命令行界面。
是不是非常熟悉?没错,这简直不能再像Matlab
了。即使顶部功能菜单并没有Matlab
那么丰富,却也足够了。
更换命令行
如果你有丰富的shell
编程经验,那么你应该对这个>>
非常熟悉。Matlab
使用的是>>
,MySQL
使用的是>
,sqlplus
(Oracle
)使用的是>
,Python
使用的是>>>
,Ubuntu
普通用户使用的是$
,Ubuntu
超管使用的是#
……
如果说你用惯了想要换,还是可以的。直接输入
# 这段字符串复制自我的Ubuntu终端
PS1("sakebow@Lenovo-V310-14IKB:~$ ")
这样就能够保持和终端一模一样的风格了。当然为了区分也可以改称其他的,比如
# ……懂的都懂……
PS1("お兄ちゃん、頑張るね、大好きだよ~❤ ")
基于Linux
的特点
当然,因为开源免费的octave
是使用C
语言基于Linux
系统开发的,所以也会保留一些Linux
系统的命令。比如:
pwd
显示当前所在文件夹ls
显示当前文件夹中所有的文件cd
跳转到制定文件夹exit
退出程序
即使是安装在Windows
系统下的octave
也是能够使用这些命令的。
清空面板
在Ubuntu
系统中你可以直接使用Ctrl+L
清空命令行。所有的指令会存在历史记录Command History
栏内
当然你也可以输入clc
并回车就能够清空命令行
快速矩阵
这些
Octave
内置的快速矩阵摘自摘自油土鳖上一位印度阿三的教程(英语略微有一些咖喱味,但已经很不错了)
o n e s ( n , m ) = [ 1 ⋯ 1 ⋮ 1 ⋯ 1 ] n × m z e r o s ( n , m ) = [ 0 ⋯ 0 ⋮ 0 ⋯ 0 ] n × m e y e ( n ) = [ 1 0 0 ⋯ 0 0 1 0 ⋯ 0 0 0 1 ⋯ 0 ⋮ 0 0 0 ⋯ 1 ] n = E e y e ( n , n + 1 ) = [ 1 0 0 ⋯ 0 0 0 1 0 ⋯ 0 0 0 0 1 ⋯ 0 0 ⋮ 0 0 0 ⋯ 1 0 ] n × ( n + 1 ) \begin{matrix} ones(n, m)&=& \left[\begin{matrix} 1&\cdots&1\\ &\vdots\\ 1&\cdots&1 \end{matrix}\right]_{n\times m} \\ zeros(n, m)&=& \left[\begin{matrix} 0&\cdots&0\\ &\vdots\\ 0&\cdots&0 \end{matrix}\right]_{n\times m} \\ eye(n)&=&\left[ \begin{matrix} 1&0&0&\cdots&0\\ 0&1&0&\cdots&0\\ 0&0&1&\cdots&0\\ &&&\vdots\\ 0&0&0&\cdots&1 \end{matrix} \right]_n&=&E \\ eye(n, n + 1)&=&\left[ \begin{matrix} 1&0&0&\cdots&0&0\\ 0&1&0&\cdots&0&0\\ 0&0&1&\cdots&0&0\\ &&&\vdots\\ 0&0&0&\cdots&1&0 \end{matrix}\right]_{n\times(n+1)} \end{matrix} ones(n,m)zeros(n,m)eye(n)eye(n,n+1)====⎣⎢⎡11⋯⋮⋯11⎦⎥⎤n×m⎣⎢⎡00⋯⋮⋯00⎦⎥⎤n×m⎣⎢⎢⎢⎢⎢⎡100001000010⋯⋯⋯⋮⋯0001⎦⎥⎥⎥⎥⎥⎤n⎣⎢⎢⎢⎢⎢⎡100001000010⋯⋯⋯⋮⋯00010000⎦⎥⎥⎥⎥⎥⎤n×(n+1)=E
而rand(n,m)
则是生成n行m列的均匀分布随机数矩阵(uniformly random matrix),每个数字都是介于0到1之间的7位小数,且每个数字出现概率服从均匀分布
U
(
0
,
1
)
U(0, 1)
U(0,1);
randn(n,m)
是生成一共n列m行的正态分布随机数矩阵(normally random matrix),每个数字服从正态分布
N
(
0
,
1
)
N(0, 1)
N(0,1)。乍一看这组数据毫无特点,实际上是
P
(
μ
−
σ
<
x
<
μ
+
σ
)
≈
0.6826
P(\mu-\sigma<x<\mu+\sigma)\approx0.6826
P(μ−σ<x<μ+σ)≈0.6826。这也就说明了为什么数字里面会有大于
1
1
1和小于
−
1
-1
−1的数字了
矩阵构造
在C
、C++
、Java
、C#
等等语言中构造矩阵永远是一个很难受的工程,拿Java
中的List<List<?>>
来说,向其中添加元素是一项无比复杂的工程。
而在octave
和Matlab
中,就只需要使用
>> s = [1, 2; 3, 4]
s =
1 2
3 4
在这里你可以使用逗号作为分割符,也可以仅用空格作为分割符。
如果你要用克莱姆法则解多元一次方程组,那么就需要几个矩阵拆拆解解。这个时候可以把一整个矩阵当作数字:
[A B]
像是[1 2]
,将两个数字横着拼起来[A; B]
则是[1; 2]
,将两个数字竖着拼起来A'
是矩阵的转置,就是 A T A^T AT
当然,我们还有矩阵的选择:
S(m, n)
是选取矩阵S
中第m行第n列的一个元素S(m, [x, y, z])
是选择矩阵中第m行第x、y、z列的一共三个元素。如果还需要添加元素则在[]
内追加- 下方冒号表达式继续介绍
冒号表达式
如果你熟悉C
、C++
、Java
、C#
等等(Python
除外),你应该会经常使用到冒号表达式,即是:
// 输出该学生是否可以购买打折的火车票
cout << 学生.学生证 === null ? "火车票不能打折" : "火车票可以打折" << endl;
但是在octave
和Matlab
就完全不一样了。冒号表达式会这么用:start[:step]:end
,即创建一个从start
到end
的数组,其中每个数字之间间隔step
,默认step
值为1,即依次递增。
>> s = 1 : 5
>> ans =
[1 2 3 4 5]
>>
>> s = 1:3:10
>> ans =
[1 4 7 10]
当然冒号不止这一个用途。刚刚我们说了矩阵的构造,在矩阵中冒号还能够用作选择器。
我们在选择矩阵中元素的时候会使用一个数组作为范围,而如果这个范围正好可以用冒号表示的时候,就可以是这样:
-
S(m, [start : end])
,也就是说选取第m行从start开始到end结束的所有元素 -
如果没有规定
start
和end
,也就像是:S(m, :)
,那么就是选取第m行一整行
个数计算
Java
中,对集合使用size()
方法求集合内元素个数,对数组使用length()
方法求数组内元素个数。
在octave
中,我们两个都可以用,但是size()
方法返回的是行和列两个数字,而length
方法则是从size()
方法返回的两个数字中选择一个最大的。举个例子:
假设你这个时候又闲着没事创建了一个随机数组rand(5, 1)
,那么size()
方法返回的是[5 1]
两个数字,前者是行数,后者是列数;而length()
方法返回的是5,是5和1中较大的数。
同样的,如果你只有一个数组[1 2 4]
,size()
方法返回的依然是[1 3]
两个数,而length()
方法就返回3。
其他数学函数
ceil(x)
- 取比x大的第一个最近的整数,即向上取整floor(x)
- 取比x小的第一个最近的整数,即向下取整round(x)
- 取距离x最近的一个整数。如果 x = 0.7 x=0.7 x=0.7,则结果为 1 1 1;如果 x = 0.3 x=0.3 x=0.3,则结果为 0 0 0max(x)
/min(x)
- 取一个数组中最大/最小的数
对于max(x)
和min(x)
在官方给的说明里面指明的是vector,也就是说这个函数同样适用于矩阵、向量等等。但是实际测试的时候是这样的:
>> rand(5, 3)
ans =
0.097667 0.295360 0.710065
0.858128 0.141471 0.729432
0.073253 0.503299 0.379730
0.329683 0.839168 0.076656
0.662468 0.514276 0.020735
>>
>> min(ans)
ans =
0.073253 0.141471 0.020735
不难发现,对比的时候将一整列作为一个数组进行纵向的对比,每一列选出了一个最小的。在上面这个例子中,数组ans[0..4][0..2]
每一列选出了最小的,也就是ans[2, 0]
、ans[1, 1]
和ans[3, 2]
。max(ans)
也是一样的算法,只不过会每列找出一个最大的。
数据集的导入和导出
如果有一天我们使用计算机去证明歌德巴赫猜想,把所有的数字塞进去日复一日地算,终于算出来了一大批数字。我们不可能使用人工一个个抄下来,肯定是让计算机自己保存。这个时候就用save
命令让计算机保存所有的数字。
例如,我现在弄出了很大一批随机数randn(1000, 1000)
,但是我突然发现笔记本电不够了,得保存下来。就这么使用:save ${文件名} ${变量名}
,意思就是:将变量的值保存在指定中。当然你也可以指定别的后缀名,比如我这里选择:
save randn.json ans
于是就有了如下效果:
但是呢,为了能够更直观地区分各个文件,我们尽可能按照大家默认的规矩来,这里推荐万能的.csv
文件;官方更是推荐使用.mat
文件。更何况,我这里导出的文件并不符合json
文件的编写规范。
就像是Java
的导包一样,我们会使用import
导入一整个jar
包。而在octave
中,我们将会使用load
命令导入文件。
如果再次使用,我们就使用load
命令,即:load randn.dat
(我这里是.json)
看起来很简单?我们再来点奇葩的:
有一天你们哥几个凑一起跑数据,结果突然发现两个人风格极其相近以至于命名都完全一样。现在重新改已经很头疼了,怎么办?
我们就用临时文件。
% 创建临时文件
f = tmpfile
% 将变量存入临时文件
save f variable
% {
拿同名变量处理一大堆事情
} %
% 读取临时文件
load f
% 这个时候同名变量就被覆盖了
打个比方的话,就像是C
语言的局部变量和全局变量一样。
当然,这只是一个非常极端的例子,我们强烈建议各小组之间事先商量好再码字,正如SpringBoot
所倡导的:“约束大于业务”、“习惯大于编码”
统计图绘制
统计图必定是有数据支持的,但是现在没有数据……那么就随机生成吧:rand(1, 100)
。于是工作空间中就有了ans
这个变量,同时还有了一个共1行、100列的矩阵,也就是数组。
然后,我们就画图:plot(ans)
,于是产生了一个很简单的折线图:
因为是随机生成的数字,所以也看不出什么规律。
你可能会有点惊讶,为什么明明只有ans
一个变量,谈不上所谓因变量和自变量,他是怎么画出来的?实际上这里是将下标作为横坐标、ans
的值作为纵坐标画出来的。
统计图绘制样式
如果说你不需要折线图,而是散点图,那么就更改绘图样式吧:plot(ans, '*')
当然你觉得*
不太美观的话也可以换成中文的·
(中文输入下波浪线按钮不加shift
)或者日文的・
(日文输入下问号按钮不加shift
)
为了防止很多图像看混了,可以使用不同的颜色标记:plot(ans, '1')
各种各样的数字代表了各种各样的颜色,而各种各样的字符代表了各种各样的描点。
如果你什么都不写,那么默认就是使用-
,也就是折线图。
散点图
好吧,用plot
画出来的散点图多少还是有点点奇怪。那么我们干脆用专门画散点图的方法:scatter
。
还是一样的,我们拿到了一组数据:x = randn(1, 100)
,另外我们再来一组数据:y = randn(1, 100)
,然后画出随机散点图:
同样的,我们能够使用家参数的方式改变图像的样式:scatter(x, y, '*')
。只不过这里支持的字符就比较少了,经测试似乎仅支持将圆圈更换为*
。
柱状图
柱状图也是一种重要的工具,在统计总数并比较多条数据的差值时用得多。就比如100个服从正态分布的随机数构成的柱状图中,能够清晰地看出各个概率之间分布的差值:
我们知道, P ( 0 − 1 < x < 0 + 1 ) ≈ 0.6826 P(0-1<x<0+1)\approx0.6826 P(0−1<x<0+1)≈0.6826,所以数据比较倾向于向 ( − 1 , 1 ) (-1, 1) (−1,1)这个区间集中。
等高线
就像是地理的等高线、电磁场的等势线一样,存在的目的就是为了说明哪一段是数值上相等的。在绘制的时候,我们需要提供一个至少两行两列的矩阵,然后就由contour
方法寻找矩阵中值算出等高线。
比如对于
a
n
s
=
[
1
2
3
4
5
6
7
8
]
ans=\left[\begin{matrix}1&2\\3&4\\5&6\\7&8\end{matrix}\right]
ans=⎣⎢⎢⎡13572468⎦⎥⎥⎤这个矩阵画出来的等高线就是contour(ans)
:
极坐标
高中我们认识并狠狠地练习了极坐标这些题目,所以我们就直接从最简短的开始: ρ = 2 cos θ \rho=2\cos\theta ρ=2cosθ。相信你很清楚这个等式可以化为 x 2 + y 2 = x x^2+y^2=x x2+y2=x,是以 ( 1 , 0 ) (1,0) (1,0)为圆心、 1 1 1为半径的圆。
那么就确定两个变量:p = linspace(0, 1)
和o = linspace(0, 2 * pi)
注:linspace(x, y) = x:0.01:y,即从x到y中每隔0.01取一个点
然后,画极坐标:polar(p, o)
就出现了这样的图:
似乎和我们平常见到的直角坐标系不一样。不过没有关系,是不是在很多影视作品中看到类似的?雷达嘛。在战场上,你发送了敌机的 ( x , y , z ) (x, y, z) (x,y,z)坐标,这谁懂啊。干脆点,报哪个方向方向多少度,往那打。这种情况下极坐标比直角坐标好用多了。
还有其他很有意思的:比如我们将刚刚的p
和o
调换一下位置,就成了螺旋线。
饼状图
饼状图只是为了查看占比,所以大批量的随机数在这里并不适合作为样本。所以这里就随机生成三个数:ans = randn(1, 3)
由于饼状图是组内比较,所以只能使用一组数据,即行向量或者列向量作为参数。如果生成ans = randn(10, 10)
作为样本则会报错。使用时还请注意。
可以看到,octave
拿到数据之后立马进行了权重的计算,最终得出了这个结果。所以我们在实际实验中就只需要把数据整理成行向量或者列向量传入,octave
就能够在一定程度上。
既然饼状图是查看一个部分或者多个部分的占比,为了让图片更清晰,就可以让一些部分联系起来。
时间回溯到你收集猫娘数据的那些日子(别吐槽了,听就是了)
你了解到全世界的猫娘们的年龄从0岁开始往上的数量依次是neko = [16247, 18752, 19165, 17251, 11471, 18714, 10961, 5428, 13725, 8715, 10031, 13298, 8715, 9716, 4562, 9612, 3610]
(随手瞎打的)。你想清晰地看出在0到3岁的猫娘占比,于是你使用第二个参数:explode
。这个参数应当是一个数组。如果数值为0,则会与下一个块区相接;而如果非0,即使是负数也会明显分开。
于是我们为了清晰地看出0到3岁的小猫娘的数量,我们使用pie (neko, [1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
,就会产生这样的效果:
可以看到,设置为非零的前三个部分被分开了,占比 8 % + 9 % + 10 % = 27 % 8\%+9\%+10\%=27\% 8%+9%+10%=27%。这是不是比不突出任何元素的饼图看起来更直观呢?
选择支
当然,作为一门高级语言,选择支是必要的。
如果你是想通过选择支筛选不同年龄段,那么就使用:
if age < 3,
% filter for age < 3
elseif age < 10,
% filter for 3 < age < 10
else,
% filter for age > 10
end;
这个逗号估计就是在命令划分的时候不太好识别标志加上的。虽然和Java
、Python
略有出入,但我相信这绝对有他的道理。
循环
while
循环相信各位都熟悉得不能再熟悉了。实际上while
和if
的用法实际上是一样的。毕竟都是判断条件并转入选择支。唯一的区别估计就是if
语句能够接else if
和else
来筛选不同的条件,而while
只能始终选择同一个条件。
for
循环相比while
循环又多了一个神奇的功能:将一个集合里的每一个元素创建索引,并且这个索引仅作用于for
循环中,是区域变量。
% 假设nekos里面保存了每只猫娘的年龄(age)和姓名(name)
for neko = nekos,
if neko.age > 10,
sprintf("%i", neko.name)
end;
end;
如果你确定这些猫娘有着不同的名字,并且你一定要找到那只叫做“chocola”的猫娘(没……没夹带私货……真的!),那么就这么来:
for neko = nekos,
if neko.name == 'chocola',
sprintf("%i", neko.age)
break; % 跳出一层循环
end;
end;
有了这个break
,一旦找到了chocola就会退出并显示她的年龄。
而如果这些猫娘有重名却有着不容的铃铛编号呢?那么就全找出来。
for neko = nekos,
if neko.name == 'chocola',
sprintf("%i", neko.id)
continue; % 强制结束本次循环
end;
end;
有了continue
,程序就不会结束,反而是直接结束continue
后面的所有步骤,强行结束本次循环。全部显示出来之后我们再对比这些编号是不是我们想要的编号。
和Java
、Python
等等有着非常多的相似之处呢。即使完全没接触过matlab
,相信你们也能用丰富的编程经验理解用法。
函数
函数看起来非常高端,实际上你可以看成多个普通命令的合集。
函数是有固定的套路的:
function ${variable} = ${function name} (${argument list})
% to do list
endfunction;
但是你得明白:函数毕竟是一个包含了很多行的整体。如果你使用纯命令行编辑整个函数,一旦某个地方有所疏忽,一运行函数就有可能会报错,然后整个函数重写。这是相当考验熟练度、耐心和细心的工作。当然,肯定不会让你有这么难受的体验的。
octave
为了能够在数学处理上由所优势,就决定兼容matlab
所使用的.m
文件。在这个文件中,我们能够使用各种各样的变量记录函数的定义,这样即使出现了错误我们也能够通过错误信息定位到不正确的地方并修改。这样就避免了整个函数重写的惨痛经历。
如果你只有命令行,或者说只愿意使用命令行,octave
也是提供了专门的命令:edit ${file name}
。如果是Ubuntu
系统,octave
将会在~/octave
文件夹下创建或者修改文件。在这里尤其注意文件的命名,一方面是因为这样子能够很清晰地弄清楚什么文件保存了什么函数,另一方面也是因为有些文件名是octave
内定的特殊文件,比如test.m
是octave
规定的测试文件,其中含有非常多的内置函数和测试函数供用户调用、测试。而如果只是一般的命名就没有什么特殊的,只是会加上很多版权说明,并默认提供一个空函数模板以供用户根据需求修改。
有点难懂?那我们举个例子,主人公还是那个热爱猫娘的你。这次的你尽了全力也没能收集全所有的数据,有些猫娘一被问到年龄就狠狠地挠你的嘴,有些则因为怕生逃掉了,也有些则在接近之前你就被主人当成可疑人物带走了……总之,你虽然找到了20万只猫娘,却只收集了5000条残缺不全的数据,不是掉了这个就是少了那个。但是该寻找的还是要寻找,那就是查清相同年龄里成绩最好和成绩最差的两只猫娘的信息。
在写这个函数之前,我们首先明白几个非常重要的点:
octave
是以matlab
为模板创建的全平台兼容的软件,所以有什么不懂可以直接查询matlab
的教程;octave
与matlab
非常相似,都是以C
为基础搭建的,所以并没有面向对象编程的概念,整个程序就是为了面向过程而设定的。
所以,我们如果使用octave
处理猫娘们的数据的时候,只能把所有想要的全部手打或者通过脚本筛选,最终达成只包含需要被处理的部分的、单纯列出所有而不是分类整合的数据集。如果是使用各种各样括号分开的json
数据则无法被处理为一个个对象,却只能分成许许多多的单条数据。
% 文件名:filter.m
% 从一堆数据中寻找最小的数据
function filter_mini = filter(x)
ret = x(1)
for i = 2:length(x),
if x(i) < ret,
ret = x(i)
endif;
endfor;
endfunction;
在这里需要非常注意的是:一个文件只能编辑一个函数,每个函数的名称必须和文件名保持一致,否则会报警告:函数名和文件名不匹配。调用则是很普通的使用文件名即可,代入参数便能得到结果。
可是,我要找出猫娘的id
啊,你这只能找出分数欸。那就这样,我们先按照猫娘的id
顺序排列,我们找出最大和最小的索引,再从这些索引中找出id
就好咯。
function [low, low_index, high, high_index] = filter(x)
low = x(1); high = x(1); low_index = 0; high_index = 0;
for i = 2:length(x),
if x(i) < low,
low = x(i)
low_index = i
elseif x(i) > high,
high = x(i)
high_index = i
endif
endfor
endfunction
这样看起来没什么问题。但是却依然引出了另一个问题:一旦找出符合if
语句的分数,程序就会立马返回并打印,然后你就发现了一大堆数字堆在那里,并只能通过肉眼筛选真正符合条件的数字。如果说明得学术一点,那就是:“打印出大量的局部最优解,并只能通过肉眼寻找全局最优解”。是不是很让人抓狂?
还是有解决办法的。也就是在调用的时候规定返回值:
[a, b, c, d] = filter(param)
这样的话虽然还是会打印一大堆数字,却能够很明显地看出全局最优解的结果了。
当然,有函数就一定有错误处理:
function [low, low_index, high, high_index] = filter(x)
% 如果不是列向量或者行向量,报错。
if isvector(x),
error("filter expects a vector")
endif;
low = x(1); high = x(1); low_index = 0; high_index = 0;
for i = 2:length(x),
if x(i) < low,
low = x(i)
low_index = i
elseif x(i) > high,
high = x(i)
high_index = i
endif; % 这里的endif和endfor也可以换成end
endfor;
endfunction;
其他
这些就已经是全部的基本操作了。如果还需要精进,还需要尝试攻克数学建模的一些经典书籍。接下来将说明一些次要的事情:
如果你是Windows
环境下的纯命令行,那么你在执行了上面提到的一些画图命令之后,还需要执行hold on
命令让图像显示出来,而close
命令则是关闭图像。当然,毕竟是Windows
窗体程序,你也可以选择点击右上角的关闭窗口按钮关掉这张图片。