题记:2012年4月1日回到家,南大计算机研究僧复试以后,等待着的就是独坐家中无聊的潇洒。不知哪日,无意中和未来的同学潘潘聊到了图像处理,聊到了她的论文《基于LDA的行人检测》,出于有一年半工作经验的IT男人的本能,就一起开始学习研究这篇“论文”了。众所周知,老师给学生设置论文题目的,起初都是很模糊的——自己没有思考清楚实践上的可行性和具体思路,仅从理论了解上就给学生设置一些“难以实现”的论文任务。几经修改和商讨,最后的论文实际上就是“基于SIFT+Kmeans+LDA的图片分类器的实现”了。至此,代码已经编写完毕,图片分类的效果还算满意。
——copyright:由于是一起学习研究的结果,相关所有内容潘潘童鞋可以以第一作者身份使用!
一、实现思路
分类器的功能是:输入一组图片,给定需要分类的类别数lda_k(>1);输出lda_k个文件夹,每个文件夹内的图片为一类图片。
第一步是SIFT特征提取:输入图片,输出图片的特征点集,即feature列表,每个feature代表一个图片的某个局部特征,每个feature的数据结构由一个128维浮点数组表示。至此,可以将一幅图片转换成一个feature集。
第二步是Kmeans聚类:输入是所有图片的feature集的综合,给定参数km_k代表需要聚类的类别数;输出是km_k个feature,在LDA的视角看来就是“单词表”,用“单词表”中的一个“单词”(类中的质心feature)代表kmeans聚类里面一类的所有feature。
第三步是统计词频:(对每个图片)输入是图片的feature集和“单词表”,分别计算该图片feature集中每个feature对应的“单词”,并统计每个“单词”在该feature集中出现的次数即词频;输出是词频统计数据。
最后一步是LDA训练潜在主题:输入是所有图片文件的词频统计数据,以及给定的需要训练出来的主题类别数lda_k;LDA输出参数较多,其中最有用的就是文档-主题条件概率矩阵(theta矩阵),即举证中每个元素表示P(主题k|文档m)——在文档m中,主题是k的概率——通过该概率即可判断当前文档最可能的主题,实现了将所有文档分类为lda_k个主题。
总之,理论上LDA研究的实体是一组文档,每个文档由若干单词组成,通过无监督学习,能够发现lda_k个主题,并且确定theta矩阵——文档确定的情况下生成主题k的概率,以及phi矩阵——主题确定的情况下生成单词v的概率。分类器通过SIFT算法将图片转换为若干feature,即将图片看成是“文档”feature看成是“单词”。而仅通过SIFT处理后的feature并不能直接单做“单词”作为LDA的输入,因为几乎每个feature都不一样,还需要Kmeans算法对所有图片的feature集的总和做一次聚类,得到km_k个类别的中心feature,即生成km_k个“单词”的“单词表”,并以此中心feature代替一个类别内的所有其他feature,从而将一个图片“文档”中的所有feature均在“单词表”中能够找到代表它的“单词”,这样图片就真正转换为了LDA能够处理的“文档”。
二、软件环境
VS2010,MFC,C++。
安装并配置Opencv,参见VS2010+Opencv-2.4.0的配置攻略。
下载并集成SIFT源码,参见在VS2010中应用SIFT(C)源码。
下载并集成LDA源码,参见在VS2010中应用LDA(C)源码。
Kmeans为Opencv自带函数,无需应用其他源码。建立好自己的工程,集成算法源码后,工程文件夹大致结构应为下图所示:
设计好自己的例程界面,并关联好响应函数和成员变量,本例程界面如下:
三、Step1——SIFT应用
在该步骤内,程序依据“图片源目录”给出的图片目录路径,扫描目录内的所有图片文件,对每个执行如下操作:
...
n= _sift_features(img, &features, SIFT_INTVLS/*3*/, SIFT_SIGMA/*1.6*/, SIFT_CONTR_THR/*0.04*/,
SIFT_CURV_THR/*10*/, SIFT_IMG_DBL/*1*/, SIFT_DESCR_WIDTH/*4*/, SIFT_DESCR_HIST_BINS/*8*/); //SIFTfeature提取
...
export_features(out_file_name, features, n);//将features导出为文件
...if(勾选了“保存SIFT特征图”)
{
draw_features(img, features, n);//在img图片上标记出features
cvSaveImage(out_img_name, img, NULL); //将标记后的图片保存
}
...
其中最主要的三个函数就是_sift_features(…), export_features(…), draw_features(…)均为sift源码所提供。(注:feature有两种类型——OXFD和LOWE,本程序只涉及LOWE类型,所有OXFD相关格式均自动忽略。)
_sift_features(…)函数第一个参数img为传入图片的IplImage指针格式,为Opencv所定义的图片数据结构;features后面的参数均为SIFT算法的输入参数,具体含义见作者的源码注释。
需要注意和理解的是features这个参数,其指向的为一个结构体feature的数组,feature结构为:
/**
Structure to represent an affine invariant image feature. The fields
x, y, a, b, c represent the affine region around the feature:
a(x-u)(x-u) + 2b(x-u)(y-v) + c(y-v)(y-v) = 1*/
structfeature
{double x; /**< x coord*/
double y; /**< y coord*/
double a; /**< Oxford-type affine region parameter*/
double b; /**< Oxford-type affine region parameter*/
double c; /**< Oxford-type affine region parameter*/
double scl; /**< scale of a Lowe-style feature*/
double ori; /**< orientation of a Lowe-style feature*/
int d; /**< descriptor length*/
double descr[FEATURE_MAX_D]; /**< descriptor*/
int type; /**< feature type, OXFD or LOWE*/
int category; /**< all-purpose feature category*/
struct feature* fwd_match; /**< matching feature from forward image*/
struct feature* bck_match; /**< matching feature from backmward image*/
struct feature* mdl_match; /**< matching feature from model*/CvPoint2D64f img_pt;/**< location in image*/CvPoint2D64f mdl_pt;/**< location in model*/
void* feature_data; /**< user-definable data*/};
其中x,y表示feature在图片中的坐标,scl, ori表示在图中标记特征的强度和方向,descr是最重要的特征信息即128维的特征向量(FEATURE_MAX_D==128)。实际上所保存的特征文件里面也只保存了feature的这些信息。features就是feature的一个数组,包含了SIFT算法所提取的图片的所有feature,返回值n表示features数组中有多少个feature元素。
export_features(…)就是将上一步中的features保存为文件,文件格式如下:
441 128
178.616459 111.621902 34.241822 1.292619
0 0 0 0 0 0 0 0 12 0 0 0 1 3 0 8 140 4 0 0
0 0 0 30 43 3 0 0 0 0 0 2 0 0 0 0 2 9 0 0
94 0 0 0 8 72 6 37 172 15 0 0 0 2 1 119 122 5 0 0
0 0 0 12 0 0 0 0 0 2 0 0 57 5 0 0 2 73 9 14
172 133 0 0 0 11 3 14 128 45 0 0 0 0 0 0 0 0 0 0
0 0 0 0 4 1 2 2 4 13 0 1 172 152 2 4 4 6 0 2
145 116 0 0 0 0 0 0
220.732993 432.886679 20.895674 -1.740551
6 0 0 1 168 133 17 5 60 0 0 1 37 20 24 25 3 0 0 0
14 26 17 3 0 0 0 0 0 0 0 0 7 0 0 0 168 144 2 1
123 0 0 0 80 38 2 15 13 0 0 0 12 44 4 4 0 0 0 0
0 0 0 0 2 0 0 1 168 67 0 0 121 13 0 2 144 16 0 3
22 2 0 4 30 7 0 0 0 0 0 0 1 0 0 0 0 0 0 0
168 81 0 0 87 9 0 1 125 38 2 5 24 2 0 3 20 16 2 2
0 0 0 0 1 3 0 0……
其中第一行的第一个int表示文件中feature的个数,第二个int表示feature的维数。接下来每段132个浮点数(后128个近似为整数)为一个feautre,前4个分别为x, y, scr, ori,后128个为表征feature的128维向量。
draw_features(…)就是将feature信息示例性地标注在图上了,图片如下:
cvSaveImage(…)是Opencv的函数,将加过标注的图片保存为文件。
这一步实际上要做的重点就是输入为图片,输出为SIFT算法后为每张图片生成的feature文件集:
这样每张图片就相当于一个“文档”,而feature就是“文档”中的“预备单词”。
四、Step2——Kmeans应用
Step1里面的feature只是“预备单词”,在成为单词之前还要通过Step2生成“单词表”和Step3将“文档”中的“预备单词”找到“单词表”中最相近的“单词”替换之(并不是真正操作上的替换,只是当成“单词表”中的“单词”统计出来而已)。
在Step2中,关键操作如下:
...
CvMat*samples=cvCreateMat(featureNum, dims, CV_32FC1); //包含所有图片的所有feature信息的矩阵,featureNum个feature,每个feature为dims(128)维向量,每一维的元素类型为32位浮点数
CvMat *clusters=cvCreateMat(featureNum, 1, CV_32SC1); //每个feature所在“质心”的指针(实际上本例程中没有用到该信息)
CvMat *centers=cvCreateMat(k, dims, CV_32FC1); //“质心”信息的数组,k个“质心”每个质心都是dims(128)维向量,每一维的元素类型为32位浮点数
cvSetZero(clusters); //将矩阵初始化为0
cvSetZero(centers); //将矩阵初始化为0
while(file.ReadString(strLine))
{
...
n= import_features(CIni::CStrToChar(fileName), FEATURE_LOWE, &features); //导入feature文件,n为导入的feature个数
...//将feature文件内所有feature信息存入samples矩阵结构内
for(int i = 0; i < n; i++)
{for(int j = 0; j < dims; j++)
{
samples->data.fl[temp++] =features[i].descr[j];
}
}
}
cvKMeans2(samples, k, clusters,cvTermCriteria(CV_TERMCRIT_EPS,10,1.0), 3, (CvRNG *)0, KMEANS_USE_INITIAL_LABELS, centers); //Kmeans聚类
...
cvSave(CIni::CStrToChar(ini.getWordListFilePath()), centers);//保存单词表
...
其中关键函数当然是import_features(...)和cvKMeans2(...),前者是sift源码里的方法,用来导入feature文件使之成为内存数据结构,后者是Opencv里的kmeans算法之一(cvKMeans2(...)内部调用了kmeans(...))。
在说明他们之前首先要了解Opencv内部通用数据结构——用来存储矩阵类型的CvMat,其声明如下:
typedef structCvMat
{inttype;intstep;/*for internal use only*/
int*refcount;inthdr_refcount;
union
{
uchar*ptr;short*s;int*i;float*fl;double*db;
} data;
#ifdef __cplusplus
union
{introws;intheight;
};
union
{intcols;intwidth;
};#else
introws;intcols;#endif}
其中最重要的就是用来存储元素信息的一维数组data,data为一个联合体,包含了各种通用类型。data虽然是一维结构,但通过rows, cols标识,可以将data看成二维的矩阵结构,type就标识了元素类型,而step就标识了一行的步长(占用字节数)。
cvCreateMat(int rows, int cols, int type);包含了三个参数,rows和cols分别表示data二维矩阵的行数和列数,type表示元素的类型。元素类型通过宏来定义,CV_32FC1中的32表示元素的位数,F表示是浮点数,Cn表示通道数,C1表示1通道,C3表示3通道(只有在类似RGB的三元表示一个像素点颜色时才用到多通道,而通道实际上就是说一个元素应该由几列来表示)。
有了上面的知识,就知道了samples矩阵是featureNum行,128列的矩阵,而矩阵元素类型为32位浮点数。featureNum的值为所有图片所有feature的总和,也就是说把所有的feature文件里面的feature信息都读入到samples矩阵内用来作为Kmeans的输入。clusters矩阵是featureNum行1列的矩阵,实际上就是featureNum个int元素的数组,每个int元素都相当于一个下标,标识对应feature(一行)属于centers中的哪个类(“质心”),而本程序里面没有使用到clusters提供的信息,featuresNum个feature分别对应于哪个“质心”是在Step3算词频里面实现的。centers就是保存“单词表”信息的矩阵了,k行128列的32位浮点矩阵,即k个feature,每个feature都是一类feature的“质心”——该类所有feature中最中间那个(通过欧式距离来计算的)feature。
关键操作来了,import_features(...)就是将feature文件读入内存,使之成为feature的内存数据结构。
int import_features(char* filename, int type, struct feature** feat)第一个参数就是feature文件的路径,第二个参数是feature的类型,本文是FEATURE_LOWE类型,第三个参数就是传出参数feature数组了,返回值为读入内存的feature个数。在接下来的操作中就需要把features里面的数据全部拷贝到samples矩阵结构内,这样所有文件的feature信息都拷贝到samples内后就完成了kmeans传入数据的工作。
cvKMeans2(...)是Opencv在kmeans(...)之上封装的一个函数,参数含义可以参照Hongquan的博客中的《OpenCV中kmean算法的实现》,本文需要说明的几个参数是:第一个参数samples——传入所有图片feature信息的总和;第二个参数k即kmeans的聚类个数(本程序通过界面的类别数设置);第七个参数flags表示生成随机数的方式,可能每次运行程序对相同的输入输出的单词表都不同,那么就跟这个参数相关;第八个参数centers,这就是我们需要的输出了,即“单词表”信息,每一行就是一个feature也就是一个“单词”。
这样,读入Step1中输出的所有feature文件,然后综合起来并输入参数k(聚类类别数),然后就可以生成一张包含k个“单词”的“单词表”了,用Opencv的cvSave(...)函数把这张“单词表”保存为yml文件,就是本步骤的主要输出了。输出“单词表”文件wordList.yml格式类似:
%YAML:1.0
wordList: !!opencv-matrix
rows: 80
cols: 128
dt: f
data: [ 1.64593754e+001, 2.11062511e+001, 30.,
3.48312492e+001, 2.39375000e+001, 1.28156252e+001,
1.25531254e+001, 1.16812506e+001, 4.92906265e+001,
3.61625023e+001, 2.94968758e+001, 23., 1.70375004e+001,
1.12718754e+001, 1.71968746e+001, 3.13437500e+001,
3.27593765e+001, 2.25156250e+001, 2.02625008e+001,
2.26406250e+001, 2.73750000e+001, 1.90531254e+001,
2.41375008e+001, 2.85156250e+001, 1.92875004e+001,
............................................. ]
五、Step3——数词频的实现
前面已经说过,Step2是生成“单词表”,而Step3就是通过计算欧式距离来确定原始feature文档中每个feature对应“单词表”中的哪个“单词”,然后统计出来,生成对应的统计文件——词频文件wordF.data。主要操作如下:
...
FileStorage readfs(CIni::CStrToChar(ini.getWordListFilePath()), FileStorage::READ);//以只读的形式打开yml。
Mat wordList; //单词表矩阵
readfs[CIni::CStrToChar(CIni::removeSuffix(CIni::getFileNameFromPath(ini.getWordListFilePath())))] >> wordList; //读取单词表
...while(“对于每个feature文档”)
{
...int n = import_features(CIni::CStrToChar(fileName), FEATURE_LOWE, &features);
...for(int i = 0; i < n; i++)
{
...
distMin= normL2Sqr_(pa, pb, dims); //计算欧式距离
...for(int j = 1; j < wordNum; j++)
{
...
dist= normL2Sqr_(pa, pb, dims); //计算欧式距离
...
}
}
}
...
显而易见,前三个操作就是利用Opencv的函数,将Step2中生成的“单词表”读入矩阵结构(Mat)的变量wordList中。而对于每个feature文档,通过sift源码的import_features(...)函数读入以feature结构体为元素的数组features中。接下来对于每个feature计算它与wordList中哪个“单词”最接近,最后统计出该文档中包含的单词及其个数。
normL2Sqr_(pa, pb, dims)是Opencv中计算欧式距离的函数,其中pa指向一个feature,而pb指向单词表中的一个“单词”(“单词”也是feature),dims表示待计算数据的维数,在这里就是128了。
经过上面的操作,统计好每个图片文档中每个“单词”出现的次数后,统一保存为一个词频文档,即将所有图片文档的词频信息保存在一个文件wordF.data内,其文件结构为:
72 0:3 1:4 2:3 3:39 4:6 5:10 6:1 7:4 8:4 9:30 10:8 11:3 13:5 14:7 15:20 16:3 17:2 18:5 19:6 20:7 21:2 22:8 23:11 24:15 25:8 26:9 27:9 28:4 29:3 30:14 31:2 34:4 35:4 36:3 37:4 38:8 39:7 40:15 42:7 44:6 45:4 46:8 47:15 48:9 49:3 50:3 51:1 53:4 54:2 55:3 56:2 57:3 58:9 59:1 60:1 61:8 62:4 63:4 64:7 65:3 66:1 68:3 69:1 70:4 71:1 72:1 73:1 74:6 76:2 77:6 78:5 79:3
60 0:2 1:1 3:29 4:3 5:13 6:1 7:1 8:5 9:30 10:4 11:2 13:1 14:2 15:17 17:1 18:4 19:2 20:1 22:3 23:3 24:4 25:2 26:4 27:4 28:1 29:2 30:8 31:1 32:3 34:1 35:3 36:4 37:4 38:4 39:6 40:15 42:3 44:3 45:2 47:13 48:6 49:3 51:1 52:2 54:2 56:1 57:2 58:13 61:2 63:3 64:6 66:1 67:2 71:1 73:1 74:1 76:1 77:2 78:1 79:4
75 1:3 2:2 3:30 4:4 5:7 6:1 7:3 8:2 9:32 10:9 11:3 12:3 13:9 14:2 15:22 16:5 17:2 18:5 19:1 20:3 21:3 22:1 23:11 24:6 25:2 26:11 27:6 28:3 29:9 30:5 31:2 32:5 34:3 35:6 36:2 37:10 38:2 39:8 40:20 42:8 43:1 44:7 45:4 46:2 47:14 48:3 49:3 50:3 51:4 52:2 53:5 54:2 55:2 56:1 57:3 58:12 59:1 60:2 61:1 62:4 64:2 65:4 66:4 67:3 68:1 69:1 70:5 71:3 72:2 73:1 74:3 76:5 77:2 78:6 79:4
73 0:2 1:2 2:1 3:29 4:8 5:6 6:4 7:6 8:3 9:26 10:10 11:2 12:2 13:6 14:1 15:16 16:2 17:8 18:5 19:3 20:2 21:6 22:5 23:4 24:5 25:5 26:12 27:2 28:1 29:10 30:6 31:1 32:2 33:1 34:2 35:9 36:2 37:7 39:5 40:16 42:6 43:1 44:4 46:2 47:17 48:9 49:2 50:3 52:5 53:1 54:2 55:3 56:2 57:3 58:9 59:2 60:1 62:2 63:3 64:1 65:4 66:2 67:1 68:3 69:3 70:7 72:2 73:1 74:3 75:1 76:7 78:8 79:5
70 0:1 1:5 3:23 4:5 5:2 6:2 7:4 8:5 9:27 10:11 11:2 12:1 13:6 14:4 15:15 16:4 17:4 18:5 19:6 20:4 21:4 22:6 23:7 24:4 25:2 26:12 27:3 28:2 29:13 30:7 31:1 32:4 33:2 34:5 35:12 36:4 37:8 38:3 40:21 42:6 43:1 44:5 45:4 46:3 47:14 48:6 49:5 50:3 51:1 52:6 53:2 54:3 55:2 57:7 58:12 59:2 62:3 63:4 65:5 66:3 67:3 69:1 70:5 71:2 72:3 73:4 74:3 76:6 78:8 79:2
70 0:1 1:2 2:2 3:25 4:2 5:6 7:1 8:3 9:35 10:7 12:1 13:4 14:4 15:17 16:2 17:7 18:7 19:3 20:3 21:2 22:6 23:5 24:1 25:2 26:9 27:4 28:1 29:7 31:1 32:2 34:3 35:4 36:1 37:8 38:1 39:1 40:19 42:6 44:5 45:2 47:15 48:7 49:5 51:2 52:4 53:1 54:2 55:2 56:2 57:3 58:16 59:2 60:1 61:3 62:4 63:6 65:3 66:3 67:2 68:1 69:1 70:3 71:3 72:4 73:3 74:4 76:2 77:3 78:7 79:9
其中每一行代表一个文档(图片文档),%d:%d的结构表示单词ID:单词个数,第一个数字表示后面的元素项个数。这些数据就是Step3统计词频的输出了,也是Step4LDA运算的输入。
六、Step4——LDA应用
作者对LDA的实现并不是像sift和kmeans那样由一个函数通过参数传入传出来给出,而是作者的main函数的一个实现过程。本例程LDA的主要过程为:
...int topic_num = lda_k; //LDA分类数
struct corpus *cps;structest_param param;
...
cps= read_corpus(data); //读取训练集
init_param(cps,¶m,topic_num); //初始化参数//迭代计算
while (1)
{//对每个文档使用sampling方法计算
for (int m=0; mnum_docs; m++)
{
...for (int l=0; ldocs[m].length; l++)
{for (int c=0; cdocs[m].words[l].count; c++)
{
param.z[m][word_index]= sampling(m,word_index,cps->docs[m].words[l].id,topic_num,cps,¶m,alpha,beta,p,s_talpha,vbeta); //sampling计算
...
}
}
}if ((iter_time >= burn_in_num) && (iter_time % SAMPLE_LAG == 0))
{
calcu_param(¶m, cps,topic_num,alpha,beta); //计算参数
...
}//迭代结束条件
if (sample_time ==sample_num)
{break;
}
}
average_param(¶m, cps,topic_num,alpha,beta,sample_num); //计算theta,phi平均值
save_model(cps,¶m,model_name,alpha,beta,topic_num,sample_num); //保存结果数据
...
对于没有接触过LDA的人要看懂函数过程还是很困难的,但对于有语言功底的程序员,如果只是应用LDA过程,只需要了解几个主要的LDA概念就行了。(其他部分请参考作者代码注释)
1、LDA的输入。LDA的输入除了Step3生成的词频统计信息外,还需要一些参数。这些参数一般设置为通用参数就行了,本例程只有lda_k为界面输入,表示LDA训练的主题个数。
read_corpus(data)方法中,data为字符串,表示输入数据的路径,该方法将文件读取为struct corpus的格式。corpus结构体表示的是一个文档集,其结构可以从声明中看出:
structword
{intid;intcount;
};structdocument
{int id; //文档id
int num_term; //文档包含的单词个数(count的总和)
int length; //文档包含的单词类别个数(id:count结构的个数)
struct word*words;
};structcorpus
{struct document*docs;int num_docs; //文档个数
int num_terms; //单词表中单词总数(实际上是所有单词中最大id+1)
};
对比Step3中生成的词频文件,不难看懂上面这些结构体的意义。
2、LDA的输出。LDA的输出包含很多数据,由save_model(...)函数输出为文件:
lda.other文件保存参数alpha, beta, topic_num, sample_num的值。lda.topic_assgin文件保存z矩阵,z[m][n]==k表示文档m中的单词n所对应的主题为k。lda.theta文件保存theta矩阵,theta[m][k]表示在文档为m时,生成主题k的概率,即条件概率p(主题k|文档m)。lda.phi文件保存phi矩阵,phi[k][v]表示在主题为k时,生成单词v的概率,即条件概率p(单词v|主题k)。
其中需要关注的只有theta和phi两个矩阵,而本例程只用到了theta矩阵的信息。theta矩阵和phi矩阵的例子如下表:
P(Topic_k|Doc_m)
Topic1
Topic2
Doc1
0.45
0.55
Doc2
0.1
0.9
Doc3
0.6
0.4
P(Word_v|Topic_k)
Word1
Word2
Word3
Word4
Topic1
0.45
0.15
0.2
0.2
Topic2
0.1
0.5
0.3
0.1
theta矩阵
phi矩阵
其中的Topic都是由LDA通过无监督学习得到的潜在主题,只需要用户告诉LDA主题数目就行了。从上面的表格中不难看出,在theta矩阵中,我们知道了每个文档生成每个主题的概率,通过比较概率大小就可以确定文档所对应的主题了。
3、LDA算法及其运算所需结构体struct est_param。
structest_param
{int **z; //z[m][n] stands for topic assigned to nth word in mth document
double **theta; //theta[m][k] stands for the topic mixture proportion for document m
double **phi; //phi[k][v] stands for the probability of vth word in vocabulary is assigned to topic k//count statistics
int **nd; //nd[m][k] stands for the number of words assigned to kth topic in mth document
int **nw; //nw[k][t] stands for the number of kth topic assigned to tth term
int *nd_sum; //nd_sum[m] total number of word in mth document
int *nw_sum; //nw_sum[k] total number of terms assigned to kth topic
};
该结构体内theta和phi二维数组是我们熟悉的输出,z数组也是前面提到的输出之一,后面的四个变量是LDA运算中间过程的必备临时变量。LDA算法,如果了解了相关的变量名和LDA过程,通过下面这幅图是不难了解算法过程的:
综上,知道了每个图片文档对应哪个主题的概率最大后,就可以根据主题个数新建lda_k个文件夹,然后把分类到对应主题的图片拷贝过去,从而实现了对图片的LDA分类。分类效果如图:
七、参考
【Kmeans】Kmeans为Opencv自带函数。
OpenCV中kmean算法的实现:http://blog.hongquan.me/?p=8