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

源代码下载:TDIDF_Demo.rar 

       声明:本文代码思路完全来自蛙蛙池塘的博客,只为技术交流用途,无其他目的

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

TF-IDFterm 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)倒排文件频率;3TF-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);//获取项向量
};

 

TF-IDF具体实现代码

分词算法

      为了便于使用不同的分词算法,我们定义一个抽象的分词算法接口,具体的分词算法由用户自行实现

class ITokeniser
{
public:
    
virtual void Partition(string input,StrVec& retWords)=0;//分词算法
};

     这里只实现了一个最简单的空格符分词算法:

#include "Tokeniser.h"
#include 
"StopWordsHandler.h"

Tokeniser::Tokeniser(
void)
{
}
Tokeniser::
~Tokeniser(void)
{
}
void Tokeniser::Partition(string input,StrVec& retWords)
{
//分词算法,input为输入串,retWords为处理后所分开的单词,这里就简单化处理了,以空格符为分隔符进行分词
    transform(input.begin(),input.end(),input.begin(),tolower);
    
string::iterator start = input.begin();
    string::iterator end = input.end();
    StopWordsHandler stopHandler;
    
do 
    {
        
string temp;
        pos 
= find(start,input.end(),' ');//找到分隔符
        copy(start,end,back_inserter(temp));
        
if (!stopHandler.IsStopWord(temp))
        {
//不是停用词则保存
            retWords.push_back(temp);//保存分出的单词
        }
        
if (end == input.end())
        {
//最后一个单词了
            break;
        }

         start = ++end;
    } 
while (end != input.end());
}

停用词处理

      去掉文档中无意思的词语也是必须的一项工作,这里简单的定义了一些常见的停用词,并根据这些常用停用词在分词时进行判断

#include "StopWordsHandler.h"
string stopWordsList[] ={"""我们","","自己","","","","","","","","","","","","",
"","","","","","","","","","","","",""};//常用停用词
int stopWordsLen = sizeof(stopWordsList)/sizeof(stopWordsList[0]);

StopWordsHandler::StopWordsHandler(
void)
{
    
for (int i=0;i<stopWordsLen;++i)
    {
        stopWords.push_back(stopWordsList[i]);
    }
}
StopWordsHandler::
~StopWordsHandler(void)
{
}
bool StopWordsHandler::IsStopWord(string& str)
{
//是否是停用词
    transform(str.begin(),str.end(),str.begin(),tolower);//确保小写化
    return find(stopWords.begin(),stopWords.end(),str)!=stopWords.end();
}

K-Means算法

       k-means 算法接受输入量 k ;然后将n个数据对象划分为 k个聚类以便使得所获得的聚类满足:同一聚类中的对象相似度较高;而不同聚类中的对象相似度较小。聚类相似度是利用各聚类中对象的均值所获得一个中心对象(引力中心)来进行计算的。

 k-means 算法的工作过程说明如下:首先从n个数据对象任意选择 k 个对象作为初始聚类中心;而对于所剩下其它对象,则根据它们与这些聚类中心的相似度(距离),分别将它们分配给与其最相似的(聚类中心所代表的)聚类;然 后再计算每个所获新聚类的聚类中心(该聚类中所有对象的均值);不断重复这一过程直到标准测度函数开始收敛为止。一般都采用均方差作为标准测度函数. k个聚类具有以下特点:各聚类本身尽可能的紧凑,而各聚类之间尽可能的分开。  

#include "Common.h"

class Cluster;

class KMeans
{
public:
    vector
<Cluster*> _clusters;//聚类
private:
    
int _coordCount;//数据的数量
    Double2DVec _coordinates;//原始数据
    int _k;//聚类的数量
    
//定义一个变量用于记录和跟踪每个资料点属于哪个群聚类
    
// _clusterAssignments[j]=i; 表示第j 个资料点对象属于第i 个群聚类
    IntVec _clusterAssignments;
    
// 定义一个变量用于记录和跟踪每个资料点离聚类最近
    IntVec _nearestCluster;
    
/// 定义一个变量,来表示资料点到中心点的距离,
    
/// 其中—_distanceCache[i][j]表示第i个资料点到第j个群聚对象中心点的距离;
    Double2DVec _distanceCache;
    
void InitRandom();
    
static double getDistance(const DoubleVec& coord, const DoubleVec& center);
    
int NearestCluster(int ndx);

public:
    KMeans(Double2DVec
& data, int K);
    
void Start();
public:
    
~KMeans(void);
};

 

K-Means算法具体实现

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

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”呢?

posted on 2008-09-06 15:33  Phinecos(洞庭散人)  阅读(27565)  评论(23编辑  收藏  举报

导航