????????决策树非常有用,但单独使用时它并不是表现最佳的分类器。改进的方法随机森林和Boosting算法。随机森林与Boosting算法都是在内部循环中使用决策树的,因此继承了决策树的许多优良属性,它们通常是机器学习库中最佳的“开箱即用”监督学习技术。Boosting算法又被称为OpenCV机器学习库中最好用的监督学习算法。
一、Boosting算法原理
1、Boosting算法的基本思想
????????随机森林采用的是Bagging算法。Bagging算法的特点是对多个弱学习器进行独立学习,之后取平均,而Boosting算法是对多个弱学习器依次进行学习。其基本思想是:在逐个训练弱分类器时,每次迭代都会调整样本的权重,改变权重的依据是该样本被上一个弱分类器成功分类的难度;给被错误分类的样本加上较大的权重,让接下来的弱学习器更多地关注这些困难样本,尽量避免将其再分错;最后,通过线性组合的方式集成所有弱学习器,构成强学习器。
????????用于集成的弱学习器都非常简单,通常是只有一个节点的决策树,又称为决策树桩(Decision Stumps),或是只有很少节点的决策树。弱学习器是使用加权样本训练而成的,最后又通过加权“投票”的方式构成强学习器。因此,Boosting算法的关键是样本权重和弱分类器的“投票”权重如何分配。
2、Boosting算法的学习流程
????????Boosting算法使用加权的原始样本集依次训练k个弱学习器,Boosting算法使用加权的原始样本集依次训练k个弱学习器。
????????在上述学习过程中,一开始就能够被正确分类的样本的权重会慢慢变小,此时有可能造成简单的样本反而不能被正确分类的情况。因此,Boosting算法不仅边学习边更新样本的权重,还会把学习过程中得到的所有弱分类器结合在一起,对其加权求和后得到最终的强分类器。?
3、AdaBoost算法
????????Boosting算法更新样本权重的方法有很多,其中最早提出的是AdaBoost(Adaptive Boosting)算法(自适应增强算法),大多数Boosting算法都是在AdaBoost算法基础上改进的。AdaBoost算法流程如下所示:?
二、OpenCV函数实现
????????AdaBoost算法一次学习一组分类器,组中的每个分类器都是一个“弱”分类器(仅略高于随机猜测的表现)。这些弱分类器通常由决策树桩组成。在训练过程中,决策树桩从数据中学习分类决策,并从分类决策对数据的准确性中学习“投票”的权重。在每次训练分类器之前,都要对数据先进行加权,以便更多地关注上一轮中分类错误的数据。此过程将迭代进行,直到最终的强分类器对数据集的总分类误差降至设定的阈值以下,或者达到设置的迭代次数为止。当有大量训练数据可用时,此算法通常很有效。
????????OpenCV实现了下表中列出的四种Boosting算法。
????????其中,Real AdaBoost算法和Gentle AdaBoost算法的效果最好。Real AdaBoost算法是一种利用置信度评估的预测技术,在分类问题中有很好的效果;GentleAdaBoost算法赋予异常数据较小的权重,因此在解决回归问题时效果较好。
????????尽管从理论上讲,LogitBoost算法和Gentle AdaBoost算法都可用于回归任务和二分类任务,但是在OpenCV实现的Boosting算法中,目前还只支持用分类数据进行训练。原始的Boosting算法只能用在二分类任务中,对于多分类任务则需要使用一些技巧才能完成。?
????????创建AdaBoost模型
cv::ml::Boost::create
????????以下是创建AdaBoost模型时用到的主要函数
????????(1)获取AdaBoost算法的类型,用于获取Boost算法的类型,默认值为Boost::REAL。返回int值:DISCRETE = 0,REAL =1, LOGIT = 2, GENTLE = 3。
cv::ml::Boost::getBoostType()
????????(2)设置AdaBoost算法的类型,上表
cv::ml::Boost::setBoostType(int val)
????????(3)获取弱分类器的数量
int getWeakCount()
????????(4)设置弱分类器的数量
void?cv::ml::Boost::setWeakCount(int val)
????????(6)设置权重修剪率,通过setWeightTrimRate函数设置一个介于0和1(含)之间的阈值,该阈值可隐式地丢弃Boosting算法迭代过程中一些不重要的训练样本。
void setWeightTrimRate(double val)
三、二分类任务 - Mushroom数据集
????????mushroom数据集下载地址和介绍请看下文第3节?
Opencv学习笔记 - 使用opencvsharp和随机森林进行分类和回归问题_bashendixie5的博客-CSDN博客随机森林(Random Forest,RF)是一种简单易用的机器学习算法。即使在没有超参数调整的情况下,随机森林在大多数情况下仍可获得还算不错的结果。可用于分类任务和回归任务,是常用的机器学习算法之一。随机森林是一种监督学习算法,它构建的“森林”是决策树的集合,通常使用Bagging算法进行集成。随机森林首先使用训练出来的分类器集合对新样本进行分类,然后用多数投票或者对输出求均值的方法统计所有决策树的结果。由于森林中的每一棵决策树都具有...https://blog.csdn.net/bashendixie5/article/details/121805198
1、c++代码参考
void opencv_adaboost_mushroom()
{
//读取数据
const char* file_name = "D:/Project/deeplearn/dataset/mushroom/agaricus-lepiota.data";
cv::Ptr<TrainData> daraset_forest = TrainData::loadFromCSV(file_name,
0, //从数据文件开头跳过的行数
0, //样本的标签从此列开始(就是说第一列是标签)
1, //样本输入特征向量从此列开始(从第二列开始往后都是数据)
"cat[0-22]");
//验证数据
int n_samples = daraset_forest->getNSamples();
int n_features = daraset_forest->getNVars();
cout << "每个样本有" << n_features << "个特征" << endl;
if (n_samples == 0)
{
cout << "读取文件错误" << file_name << endl;
exit(-1);
}
else
{
cout << "从" << file_name << "中,读取了" << n_samples << "个样本" << endl;
}
//划分训练集与测试集,按80%和20%比例划分
daraset_forest->setTrainTestSplitRatio(0.8, false);
int n_train_samples = daraset_forest->getNTrainSamples();
int n_test_samples = daraset_forest->getNTestSamples();
cout << "Training samples:" << n_train_samples << endl << "Test samples:" << n_test_samples << endl;
//创建模型
Ptr<Boost> boost = Boost::create();
boost->setBoostType(Boost::GENTLE);
boost->setWeakCount(100);
boost->setWeightTrimRate(0.95);
boost->setMaxDepth(5);
boost->setUseSurrogates(false);
//训练模型
//训练随机森林模型
cout << "开始训练..." << endl;
boost->train(daraset_forest);
cout << "训练成功..." << endl;
//测试
cv::Mat results_train, results_test;
float forest_train_error = boost->calcError(daraset_forest, false, results_train);
float forest_test_error = boost->calcError(daraset_forest, true, results_test);
//统计输出结果
int t = 0, f = 0, total = 0;
cv::Mat expected_responses_forest = daraset_forest->getTestResponses();
//获取测试集标签
std::vector<cv::String> names_forest;
daraset_forest->getNames(names_forest);
for (int i = 0; i < daraset_forest->getNTestSamples(); i++)
{
float responses = results_test.at<float>(i, 0);
float expected = expected_responses_forest.at<float>(i, 0);
cv::String r_str = names_forest[(int)responses];
cv::String e_str = names_forest[(int)expected];
if (responses == expected)
{
t++;
}
else
{
f++;
}
total++;
}
cout << "正确答案:" << t << endl;
cout << "错误答案:" << f << endl;
cout << "测试样本数:" << total << endl;
cout << "训练数据集错误:" << forest_train_error << "%" << endl;
cout << "测试数据集错误:" << forest_test_error << "%" << endl;
}
? ? ? ? 训练结果精度如下:
正确答案:1617 错误答案:8 测试样本数:1625 训练数据集错误:0% 测试数据集错误:0.492308%
2、c#、opencvsharp代码参考
? ? ? ??opencvsharp没给实现数据集划分,所以还是人工智能手动划分数据集,打开agaricus-lepiota.data,copy一份,我这里是前7000条进行训练,后1124条用于测试。
int[,] att = GetTArray(@"D:\Project\deeplearn\dataset\mushroom\agaricus-lepiota.handsplit.train.data", 7000);
int[] label = GetTLabel(@"D:\Project\deeplearn\dataset\mushroom\agaricus-lepiota.handsplit.train.data", 7000);
InputArray array = InputArray.Create(att);
InputArray outarray = InputArray.Create(label);
OpenCvSharp.ML.RTrees dtrees = OpenCvSharp.ML.RTrees.Create();
dtrees.ActiveVarCount = false;
dtrees.CalculateVarImportance = true;
dtrees.TermCriteria = new TermCriteria(CriteriaType.Eps | CriteriaType.MaxIter, 100, 0.01);
dtrees.Train(array, OpenCvSharp.ML.SampleTypes.RowSample, outarray);
//输出属性重要性分数
Mat var_importance = dtrees.GetVarImportance();
if (!var_importance.Empty())
{
double rt_imp_sum = Cv2.Sum(var_importance)[0];
int n = (int)var_importance.Total();//矩阵元素总个数
for (int i = 0; i < n; i++)
{
Console.WriteLine(i + ":" + (100f * var_importance.At<float>(i) / rt_imp_sum));
}
}
//测试
int t = 0;
int f = 0;
List<int[]> test_arr = GetTestArray(@"D:\Project\deeplearn\dataset\mushroom\agaricus-lepiota.handsplit.test.data");
int[] test_label = GetTLabel(@"D:\Project\deeplearn\dataset\mushroom\agaricus-lepiota.handsplit.test.data", 1124);
for (int i = 0; i < test_arr.Count; i++)
{
Mat p = new Mat(1, 22, OpenCvSharp.MatType.CV_32F, test_arr[i]);
float rrr = dtrees.Predict(p);
System.Console.WriteLine("" + rrr);
if(test_label[i] == (int)rrr)
{
t++;
}
else
{
f++;
}
}
System.Console.WriteLine("正确数量:" + t);
System.Console.WriteLine("错误数量:" + f);
? ? ? ? 测试集预测结果:
????????正确数量:1124 ????????错误数量:0
四、多分类任务 -?英文字母分类
? ? ? ? 数据集下载地址,对应含义,见官网:?
letter-recognition | Machine Learning Datahttps://networkrepository.com/letter-recognition.php????????与决策树或随机森林分类器不同,目前在OpenCV中实现的Boosting算法仅能处理二分类问题。本节介绍一种展开技巧来解决多分类问题,以增加训练和预测时的计算量为代价。此技巧不仅适用于Boosting分类器,也适用于其他类型的二分类器。
????????示例代码7-2演示的是分类A~Z,共26个字母。数据来源于Letter Recognition数据集。该数据集有20000个样本,下载下来的数据文件的每行表示一个样本,其中,第1个数据为样本标签(26个大写英文字母之一),第2~17个特征为16维整型特征向量。在展开技巧中,训练集从1个扩展成26个。
????????展开时,数据集将从20000个样本扩展到:样本数×类别数=20000×26个样本。原来的标签变成新增的特征。同时,这些扩展后的样本特征向量的新标签变为1或0:即true或false。通过学习数据来回答测试样本是否为a、是否为b、……是否为z的问题,最终得到答案。
????????关键代码分析如下:首先,创建一个数组var_type,该数组指示如何处理每个特征和标签。其次,创建训练数据结构。在原始数据中不仅有var_count个特征(在本示例中,原始样本有16个特征),还有一列用于存放标签(0或1)。另外,标签和原始数据之间还有一列,用于存放原始数据中的标签(即字符型变量)。因此扩展后一共有var_count+2维向量。
????????再次,构造分类器。需要注意的是:在展开技巧中有先验知识,即必须使用setPriors(Mat(priors))方法设置priors参数。我们以预测一个字母是否为a为例:预测一个字母是a与预测这个字母非a的两种分类代价并不相同,因为展开使得非a的可能性更高,所以说一个字母是a(即y=1)要比说一个字母不是a(即y=0)的代价要高25倍。因为展开使得有25个样本向量对应负标签y=0,而只有1个样本向量对应正标签y=1,所以正标签需要相应地增加权重。
????????最后,进行模型创建与设置、训练模型等常规动作。
1、c++代码参考
static bool read_num_class_data(const string& filename, int var_count, Mat* _data, Mat* _responses)
{
const int M = 1024;
char buf[M + 2];
Mat el_ptr(1, var_count, CV_32F);
int i;
vector<int> responses;
_data->release();
_responses->release();
FILE* f = fopen(filename.c_str(), "rt");
if (!f)
{
cout << "Could not read the database " << filename << endl;
return false;
}
for (;;)
{
char* ptr;
if (!fgets(buf, M, f) || !strchr(buf, ','))
break;
responses.push_back((int)buf[0]);
ptr = buf + 2;
for (i = 0; i < var_count; i++)
{
int n = 0;
sscanf(ptr, "%f%n", &el_ptr.at<float>(i), &n);
ptr += n + 1;
}
if (i < var_count)
break;
_data->push_back(el_ptr);
}
fclose(f);
Mat(responses).copyTo(*_responses);
cout << "The database " << filename << " is loaded.\n";
return true;
}
void opencv_adaboost_letterrecognition()
{
time_t now = time(0);
char* dt = ctime(&now);
//读取数据
const char* file_name = "D:/Project/deeplearn/dataset/letter-recognition/letter-recognition.csv";
const int class_count = 26;
Mat data, responses, weak_responses;
bool ok = read_num_class_data(file_name, 16, &data, &responses);
if (!ok)
{
cout << "read num class data fail" << endl;
return;
}
int i, j, k;
Ptr<Boost> boost;
int nsamples_all = data.rows; //样本总数20000
int ntrain_samples = (int)(nsamples_all * 0.5); //一半用于训练
int var_count = data.cols; //特征维数
Mat new_data(ntrain_samples* class_count, var_count+1, CV_32F);
Mat new_responses(ntrain_samples * class_count, 1, CV_32S);
now = time(0);
dt = ctime(&now);
for (i=0; i< ntrain_samples; i++) //遍历训练集
{
const float* data_row = data.ptr<float>(i);
for (j = 0; j < class_count; j++) //遍历训练集
{
float* new_data_row = (float*)new_data.ptr<float>(i * class_count + j);
memcpy(new_data_row, data_row, var_count * sizeof(data_row[0]));
new_data_row[var_count] = (float)j;
new_responses.at<int>(i * class_count + j) = responses.at<int>(i) == j + 'A';
}
}
Mat var_type(1, var_count + 2, CV_8U);
var_type.setTo(Scalar::all(VAR_ORDERED));
var_type.at<uchar>(var_count) = var_type.at<uchar>(var_count + 1) = VAR_CATEGORICAL;
cv::Ptr<TrainData> tdata = TrainData::create(
new_data, //扩展的26倍向量
ROW_SAMPLE,
new_responses, //扩展的响应
cv::noArray(),
cv::noArray(),
cv::noArray(),
var_type
);
vector<double> priors(2);
priors[0] = 1;
priors[1] = 26;
now = time(0);
dt = ctime(&now);
cout << "训练需要一段时间" << endl;
boost = Boost::create();
boost->setBoostType(Boost::GENTLE);
boost->setWeakCount(100);
boost->setWeightTrimRate(0.95);
boost->setMaxDepth(5);
boost->setUseSurrogates(false);
boost->setPriors(Mat(priors));
boost->train(tdata);
//cout << "训练完成" << endl;
Mat temp_sample(1, var_count + 1, CV_32F);
float* tptr = temp_sample.ptr<float>();
now = time(0);
dt = ctime(&now);
cout << "测试开始" << endl;
double train_tr = 0, test_tr = 0;
for (int i=0; i<nsamples_all; i++)
{
int best_class = 0;
double max_sum = -DBL_MAX;
const float* ptr = data.ptr<float>(i);
for (k = 0; k < var_count; k++)
{
tptr[k] = ptr[k];
}
for (j = 0; j < class_count; j++)
{
tptr[var_count] = (float)j;
float s = boost->predict(temp_sample, noArray(), StatModel::RAW_OUTPUT);
if (max_sum < s)
{
max_sum = s;
best_class = j + 'A';
}
}
double r = std::abs(best_class - responses.at<int>(i)) < FLT_EPSILON ? 1 : 0;
if (i < ntrain_samples)
train_tr += r;
else
test_tr += r;
}
test_tr /= nsamples_all - ntrain_samples;
train_tr = ntrain_samples > 0 ? (train_tr / ntrain_samples) : 1.;
now = time(0);
dt = ctime(&now);
printf("识别率:train = %.1f%%, test=%.1f%%\n", train_tr*100., test_tr * 100.);
}
? ? ? ? 当前超参下运行结果:
识别率:train = 84.2%, test=80.2%?
2、?c#、opencvsharp代码参考
? ? ? ? 没有实现,可以参考c++的代码。
五、小结
????????Boosting算法只能直接处理二分类问题,但是可以通过使用展开技巧来处理多分类问题。在使用展开技巧时,将根据类别多少引入额外的计算开销,因此OpenCV中的Boosting算法并不是解决多分类问题的最快或最方便的方法。仅从这点来看,对于多分类问题,随机森林是更好的解决方法。
|