知错能改演算法-感知机
要实现上面的,得做一些准备工作。首先矩阵运算少不了吧。
Eigen is a C++ template library for linear algebra: matrices, vectors, numerical solvers, and related algorithms
说人话就是:Eigen是一个用于线性代数的C++模板库:矩阵、向量、数值求解器和相关算法
首先去官网,由于笔者用的是win10,所以选择zip格式的。
下载完成后解压到自定义文件夹中,建议放到C盘,避免误删除
接下来就是在Code Blocks里面配置,也很简单,如下图
在setting-compiler-Search directories点击Add添加Eigen所在目录即可。
试运行一下:
#include <iostream>
#include <Eigen/Dense>
using Eigen::MatrixXd;
using namespace std;
int main()
{
MatrixXd m(2,2);
m(0,0) = 3;
m(1,0) = 2.5;
m(0,1) = -1;
m(1,1) = m(1,0) + m(0,1);
cout << m << endl;
}
输出正常,代表配置完成
官方文档有这么一段解释:
意思是啥呢,简单应用使用MatrixXd就够了,MatrixXd代表的是任意大小的矩阵,其中的每一个元素都是double类型;头文件Eigen/Dense包含了MatrixXd类型的所有成员函数,这些类和成员函数都是Eigen命名空间内
所以不管程序咋写,前面最省心的是写上这几行:
#include <iostream>
#include <Eigen/Dense>
using namespace Eigen;
using namespace std;
输出矩阵和向量也很简单,直接cout就行,不用每行每列。
下图是另一段包含矩阵和向量运算的代码,以及运算结果
第二个示例首先声明一个3×3矩阵m,该矩阵m使用Random()方法初始化,内容是介于-1和1之间的随机值。下一行应用线性映射,使值介于10和110之间。函数调用MatrixXd::Constant(3,3,1.2)返回一个3×3矩阵表达式,其所有系数均等于1.2。其余为标准算术。
main的下一行引入了一种新类型:VectorXd。这表示任意大小的(列)向量。这里,向量v被创建为包含3个未初始化的系数。最后一行使用所谓的逗号初始值设定项(是不是感觉很牛逼)。
官方后面还给了一个例子:
#include <iostream>
#include <Eigen/Dense>
using Eigen::Matrix3d;
using Eigen::Vector3d;
int main()
{
Matrix3d m = Matrix3d::Random();
m = (m + Matrix3d::Constant(1.2)) * 50;
std::cout << "m =" << std::endl << m << std::endl;
Vector3d v(1,2,3);
std::cout << "m * v =" << std::endl << m * v << std::endl;
}
也没啥,就是引用了固定大小的Matrix3d和Vector3d,说是建议矩阵大小小于等于4*4时用固定大小的比较好,效率高。
官方的这一小段话告诉我们动态大小的矩阵是怎么来的。
后面还有不少介绍,沾个网址方便下次查询:Eigen文档
再回到文章最前面那张图
w是m1维的向量,每一维都代表特征值的权重;
X是mn的矩阵,每一列代表一条数据,一共有n列,也就是n条数据;Xn代表第n条数据。
不管怎么组织数据,都要保证维数匹配,不然无法运算。
下面再看看sign是啥,该怎么实现:
也就是一个符号函数:
当x>0,sign(x)=1;
当x=0,sign(x)=0;
当x<0, sign(x)=-1;
再想想还需要什么,要从文件中读取数据到Matrix和Vector中,为了直观显示,还要将这些数据显示在平面坐标中。
先实现从文件读取数据到Matrix中
方法1:C++ >>和<<读写文本文件
fstream 或者 ifstream 类负责实现对文件的读取,它们内部都对 >> 输出流运算符做了重载;同样,fstream 和 ofstream 类负责实现对文件的写入,它们的内部也都对 << 输出流运算符做了重载。
所以,当 fstream 或者 ifstream 类对象打开文件(通常以 ios::in 作为打开模式)之后,就可以直接借助 >> 输入流运算符,读取文件中存储的字符(或字符串);当 fstream 或者 ofstream 类对象打开文件(通常以 ios::out 作为打开模式)后,可以直接借助 << 输出流运算符向文件中写入字符(或字符串)。
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
int x,sum=0;
ifstream srcFile("in.txt", ios::in); //以文本模式打开in.txt备读
if (!srcFile) { //打开失败
cout << "error opening source file." << endl;
return 0;
}
ofstream destFile("out.txt", ios::out); //以文本模式打开out.txt备写
if (!destFile) {
srcFile.close(); //程序结束前不能忘记关闭以前打开过的文件
cout << "error opening destination file." << endl;
return 0;
}
//可以像用cin那样用ifstream对象
while (srcFile >> x) {
sum += x;
//可以像 cout 那样使用 ofstream 对象
destFile << x << " ";
}
cout << "sum:" << sum << endl;
destFile.close();
srcFile.close();
return 0;
}
上述这一小段是从一个博客C++文件怎么进行读取和写入操作中截取的,C++还有其他读写方法,不过既然这个这么直观,就它了。
下面结合Eigen写了一小段代码,虽然功能单一了 些,不过也勉强实现了从文件读到Matrix以及从Matrix写入文件。
#include <iostream>
#include<fstream>// Save to local file.
#include <Eigen/Dense>
using namespace std;
using namespace Eigen;
void save_file(MatrixXd mat, string filename)
{
ofstream outfile(filename, ios::trunc);
outfile << mat;
outfile.close();
}
int readfile(string infile,MatrixXd& mat,int row,int column)
{
//假设已经知道文件的数据格式了
double x;
ifstream srcFile(infile, ios::in); //以文本模式打开infile备读
if (!srcFile) { //打开失败
cout << "error opening source file." << endl;
return 0;
}
for(int i=0;i<row;i++){
for(int j=0;j<column;j++){
srcFile >> x;
mat(i,j) = x;
}
}
srcFile.close();
return 0;
}
int main()
{
MatrixXd mat(24, 3);
readfile("data.txt",mat,24,3);
cout<<mat<<endl;
return 0;
}
正常情况下,一个数据集中,前 n-1列都是特征值x,最后一列是标签y,所以需要对Matrix进行预处理,把特征值和标签分开。
VectorXd v =mat.col(2);//把矩阵的第二列赋值给向量v
取出来就没事了吗?非也,像笔者使用的数据集,标签是0和1,就得转化为-1和1
for(int i=0;i<DataSize;i++) v(i) = 2* v(i) - 1;
那要删除最后一列就更简单了
mat.conservativeResize(24,2);//使用resize会出现一些未初始化的值,还是conservativeResize靠谱
另外为了统一操作,需要给特征值加一列X0,也就是b这个角色,在此也可以直接把标签所在的第二列更改为全1.
mat.col(2) = MatrixXd::Constant(100,1,1);
到此为止,准备工作算告一段落,PS:可视化的事挪到下一篇章,打算采用code blocks和QT来实现
下面是完整代码,测试案例一共100组,最终识别出96组,96%的成功率,第一次跑可以了。测试样本的地址为:数据集,提取码为:watb
#include <iostream>
#include<fstream>// Save to local file.
#include <Eigen/Dense>
#include <vector>
#include <ctime>
using namespace std;
using namespace Eigen;
void save_file(MatrixXd mat, string filename)
{
ofstream outfile(filename, ios::trunc);
outfile << mat;
outfile.close();
}
int readfile(string infile,MatrixXd& mat,int row,int column)
{
//假设已经知道文件的数据格式了
double x;
ifstream srcFile(infile, ios::in); //以文本模式打开infile备读
if (!srcFile) { //打开失败
cout << "error opening source file." << endl;
return 0;
}
for(int i=0;i<row;i++){
for(int j=0;j<column;j++){
srcFile >> x;
mat(i,j) = x;
}
}
srcFile.close();
return 0;
}
int computeDiff(VectorXd& l,VectorXd& y){
int sum = 0;
for(int i=0;i<100;i++){
if(l(i)*y(i)<=0) sum++;
}
return sum;
}
int getRand(int mi, int ma) {
return ( rand() % (ma - mi + 1) ) + mi ;
}
int main(){
int DataSize,weightSize;//DataSize代表数据集大小,weightSize代表特征值数量,也代表权值数量
DataSize = 100;weightSize = 3;
MatrixXd mat(DataSize, weightSize);
readfile("DataSet.txt",mat,DataSize,weightSize);
VectorXd v = mat.col(weightSize-1);//label在最后一列
for(int i=0;i<DataSize;i++) v(i) = 2* v(i) - 1;//数据集本来的标签是0和1,要转化为-1和1
mat.col(weightSize-1) = MatrixXd::Constant(DataSize,1,1);
VectorXd w(weightSize);//初始化权值
for(int i=0;i<weightSize;i++) w(i) = 0;
double pace = 0.001;//步长设定为0.001
//至此完成了特征值以及标签的提取以及权重的初始化
int t = 0;//t代表次数
int N = 3000;
srand(time(0));
while(t++<N){
int i = getRand(0,DataSize-1);//随便找一个点
double l = w.dot(mat.row(i));//点乘
if(l*v(i)>0) continue;//这个点是正确的
//按照公式w(t+1) = w(t) + yn(t)*Xt;
//w(0) += pace*v(i)*mat(i,0);w(1) += pace*v(i)*mat(i,1);w(2) += pace*v(i)*mat(i,2);
w += pace*v(i)*mat.row(i);
}
VectorXd y = mat*w;
int sum = computeDiff(v,y);
cout<<"time:"<<t<<endl;
cout<<"W:"<<endl;
cout<<w<<endl;
cout<<"accuracy:"<<(DataSize-sum)*100.0/DataSize<<"%"<<endl;
return 0;
}
上面关于sum是否需要计算的表述,其实是有问题的,如果确定数据是线性可分的,当然不需要跑sum = computeDiff(v,y),但是如果不确定是否线性可分,或者说数据集噪声点比较大,只能找到一条不错的犯错误最少的线,则sum必须要计算,而且还要保存sum最小时候的weight。
int main()
{
int DataSize,weightSize;//DataSize代表数据集大小,weightSize代表特征值数量,也代表权值数量
DataSize = 100;weightSize = 3;
MatrixXd mat(DataSize, weightSize);
readfile("DataSet.txt",mat,DataSize,weightSize);
VectorXd v = mat.col(weightSize-1);//label在最后一列
for(int i=0;i<DataSize;i++) v(i) = 2* v(i) - 1;//转化label为-1和1
mat.col(weightSize-1) = MatrixXd::Constant(DataSize,1,1);
VectorXd w(weightSize);//初始化权值
VectorXd bestw(weightSize);//最佳权值
int minLoss = DataSize;
for(int i=0;i<weightSize;i++) w(i) = 0;
double pace = 0.001;//步长设定为0.001
//至此完成了特征值以及标签的提取以及权重的初始化
int t = 0,sum = 0,mintime = 0;//t代表次数
int i = 0;
srand(time(0));
int N = 3000;
while(t++<N){
VectorXd y = mat*w;//因为每次要计算整体错误率,所以要计算这个
sum = computeDiff(v,y);
if(sum<minLoss){
minLoss = sum;
bestw = w;
mintime = t;
}
i = getRand(0,DataSize-1);//随机找一个点
if(y(i)*v(i)>0) continue;//这个点是正确的
//按照公式w(t+1) = w(t) + yn(t)*Xt;
//w(0) += pace*v(i)*mat(i,0);w(1) += pace*v(i)*mat(i,1);w(2) += pace*v(i)*mat(i,2);
w += pace*v(i)*mat.row(i);
}
cout<<"time:"<<mintime<<endl;
cout<<"bestW:"<<endl;
cout<<bestw<<endl;
cout<<"accuracy:"<<(DataSize-minLoss)*100.0/DataSize<<"%"<<endl;
return 0;
}
跑出下面的结果:
其实PLA还没有完,上面实现还有不少不足,比如跑的次数N取多少,步长pace选多少,没有一个确定的标准。
在考虑这个之前,先温习一下高中知识。
2条直线的夹角,不就是2个向量的夹角么。
两个向量越接近,这两个向量的内积越大。
额,数学不行,上面那个推导还是看不明白,还有下面的,先搁置,后面再分析。
这个博客有不错的证明,码了,有余力时再看。