您的位置:首页 > 其它

基于K-Means的文本聚类算法

2008-09-06 15:33 225 查看
源代码下载:TDIDF_Demo.rar
声明:本文代码思路完全来自蛙蛙池塘的博客,只为技术交流用途,无其他目的

昨天有幸拜读了蛙蛙池塘的《蛙蛙推荐:蛙蛙教你文本聚类》这篇文章,受益匪浅,于是今天就动手尝试照着他的C#代码,用C++和STL标准库重新实现一遍,因此就有了这篇文章。本文将重新温习蛙蛙池塘那篇文章,并且加入我个人在用C++重写这份代码过程中学到的一些知识。

TF-IDF(term frequency–inverse document frequency)

这是一种用于信息检索的一种常用加权技术。它是一种统计方法,用以评估一个字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。

假如一篇文件的总词语数是100个,而词语“母牛”出现了3次,那么“母牛”一词在该文件中的词频就是 0.03 (3/100)。一个计算文件频率 (DF) 的方法是测定有多少份文件出现过“母牛”一词,然后除以文件集里包含的文件总数。所以,如果“母牛”一词在1,000份文件出现过,而文件总数是 10,000,000份的话,其文件频率就是 0.0001 (1000/10,000,000)。最后,TF-IDF分数就可以由计算词频除以文件频率而得到。以上面的例子来说,“母牛”一词在该文件集的TF- IDF分数会是 300 (0.03/0.0001)。这条公式的另一个形式是将文件频率取对数。

具体的计算原理,请参考维基百科tf–idf条目。下面简单介绍下基本的计算步骤:

1,文档预处理:1)文档分词;2)移除停用词;3)单词正规化处理

2,分出的单词就作为索引项(或单词表),它们代表的就是向量空间的项向量

3,计算项权值:这包括要计算1)词频 ; 2)倒排文件频率;3)TF-IDF权值

4,计算文档之间的相似度,一般用余弦相似度(cosine similarity)一同使用于向量空间模型中,用以判断两份文件之间的相似性

#include "ITokeniser.h"
#include <map>
class TFIDFMeasure
{
private:
StrVec _docs;//文档集合,每一行字符串代表一份文档
int _numDocs;//文档数目
int _numTerms;//单词数目
StrVec _terms;//单词集合
Int2DVec _termFreq;//每个单词出现在每份文档中的频率
Double2DVec _termWeight;//每个单词在每份文档的权重
IntVec _maxTermFreq;//记录每一份文档的最大词频
IntVec _docFreq;//出现单词的文档频率
ITokeniser* _tokenizer;//分词器
map<string,int> _wordsIndex;//单词映射表,保存每一个单词及其对应的下标
public:
TFIDFMeasure(const StrVec& documents,ITokeniser* tokeniser);
public:
~TFIDFMeasure(void);
protected:
void Init();//初始化TF-IDF计算器
void GenerateTerms(const StrVec& docs,StrVec& terms);//分词处理
void GenerateTermFrequency();//计算词频
void GenerateTermWeight();//计算词的权重
void GetWordFrequency(string& input,map<string,int>& freq); //实际统计词频函数
int CountWords(string& word, const StrVec& words);//统计词数
int GetTermIndex(const string& term);//查询词语对应的下标
double ComputeTermWeight(int term, int doc);//计算词语在指定文档中的权重值
double GetTermFrequency(int term, int doc);//获取词语在指定文档的词频
double GetInverseDocumentFrequency(int term);//计算倒排文件频率
public:
inline int NumTerms()const
{
return this->_numTerms;
}
void GetTermVector(int doc,DoubleVec& vec);//获取项向量
};

"<<endl;
//1、重新计算每个聚类的均值
for (i = 0; i < _k; i++)
{
_clusters[i]->UpdateMean(_coordinates);
}
//2、计算每个数据和每个聚类中心的距离
for (i = 0; i < _coordCount; i++)
{
for (j = 0; j < _k; j++)
{
double dist = getDistance(_coordinates[i], _clusters[j]->Mean);
_distanceCache[i][j] = dist;
}
}
//3、计算每个数据离哪个聚类最近
for (i = 0; i < _coordCount; i++)
{
_nearestCluster[i] = this->NearestCluster(i);
}
//4、比较每个数据最近的聚类是否就是它所属的聚类
//如果全相等表示所有的点已经是最佳距离了,直接返回;
int k = 0;
for (i = 0; i < _coordCount; i++)
{
if (_nearestCluster[i] == _clusterAssignments[i])
k++;
}
if (k == _coordCount)
break;
//5、否则需要重新调整资料点和群聚类的关系,调整完毕后再重新开始循环;
//需要修改每个聚类的成员和表示某个数据属于哪个聚类的变量
for (j = 0; j < _k; j++)
{
_clusters[j]->CurrentMembership.clear();
}
for (i = 0; i < _coordCount; i++)
{
_clusters[_nearestCluster[i]]->CurrentMembership.push_back(i);
_clusterAssignments[i] = _nearestCluster[i];
}
}

}

double KMeans::getDistance(const DoubleVec& coord, const DoubleVec& center)
{
return 1- TermVector::ComputeCosineSimilarity(coord, center);
}
int KMeans::NearestCluster(int ndx)
{
int nearest = -1;
double min = numeric_limits<double>::max();
for (int c = 0; c < _k; c++)
{
double d = _distanceCache[ndx][c];
if (d < min)
{
min = d;
nearest = c;
}

}
return nearest;
}
KMeans::~KMeans(void)
{
vector<Cluster*>::iterator iter;
for (iter=this->_clusters.begin();iter!=_clusters.end();++iter)
{
delete (*iter);
}
_clusters.clear();
}

最后使用《蛙蛙推荐:蛙蛙教你文本聚类》这篇文章中的数据测试所得:



Reference

1, 蛙蛙推荐:蛙蛙教你文本聚类

2,Term frequency/Inverse document frequency implementation in C#

3, 维基百科tf–idf条目

4, K-Means算法java实现

附:

最后我想请教一个问题:蛙蛙池塘的代码中分词算法使用了一个正则表达式

Regex r=new Regex("([ ""t{}():;. "n])");

它产生的结果是将”asp.net”分成了两个单词”asp”和”net”,请问,为什么不直接将其看作是一个单词”asp.net”呢?
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: