应用

现今,文本分类在生活中有非常多的应用:
  我们经常使用的百度,每次输入关键词或关键句,搜索系统匹配与输入相似的文本,反馈给我们想要看到的词条;
  或是使用的翻译工具,利用语句中每个词的语法和语义来分析,文本相似度直接影响到了翻译语句的准确性;
  再就是一些论文检测,通过对两份文本提取的关键词进行相似度分析,得出文本相似度,以检测是否存在文章抄袭的可能。

原理

大体上文本分类原理可以分为:
  1. 基于词频:一般用于句子段落这些较大粒度文本。(粒度:数据细化和综合程度)
  2. 基于语义:一般用于词语或句子等较小粒度文本。 
  
笔者这里主要介绍基于词频的方式。

流程

python 文本相似度 改进 文本相似度分析_字符串


首先对文本进行分词,同时去除一些无关紧要的停用词;

再对我们分好的词进行出现次数的统计,提取合适数量的高频词;

通过高频词构建文本向量;

最后通过余弦相似度算法得出两个文本的相似度。

实现

我们要先明确为什么要分词?
  因为通常我们要处理的文本都是中文的,它不像英文分词就简单的以空格为分界符,而在我们中文中词是最小的能独立表达意义的单位,但词语之间没有明确的界限,所以一定要先分词,这种做法就类似于我们高中语文上的句读。

那么现在问题是怎么分词呢?
  分词方法有三:
         1. 基于字典分词;
         2. 基于词频统计分词;
         3. 基于字标记分词。
   然而大佬无处不在,在github上就可以找到很多大佬写的分词库,通过引用这些库及其方法就可以实现我们的分词了。

这里分享几个库,当然也可以自行查找。
   Jieba (C++, Java, python)https://github.com/fxsjy/jieba    HanLP (Java)https://github.com/hankcs/HanLP    FudanNLP (Java)https://github.com/FudanNLP/fnlp    LTP (C++, Java, python)https://github.com/HIT-SCIR/ltp

笔者这里使用的是第一个Jieba库,在使用时我们要注意很重要的一点:
   Jieba库使用的是UTF-8的编码,不同编译器的可能会使用不同的编码格式,如果不转码强行使用就会出现乱码情况。

像我这里使用的VS2013它是GBK的编码格式,所以需要进行转换。转换的方式是通过Windows提供的两个接口:
  由于没有直接将GBK转换为UTF-8的接口,所以需要GBK先转为UTF-16,再由UTF-6转为UTF-8。

//  GBK转UTF16
int MultiByteToWideChar(
	UINT CodePage,	// 指定执行转换的字符集。可指定为:CP_ACP:ANSI字符集。 CP_UTF8:使用UTF-8转换。
	DWORD dwFlags,	// 一组位标记用以指出是否未转换成预作或宽字符(若组合形式存在)
	_In_NLS_string_(cbMultiByte)LPCCH lpMultiByteStr,	// 指向将被转换字符串的字符。.
	int cbMultiByte,	// 指定由参数lpMultiByteStr指向的字符串中字节的个数。
	LPWSTR lpWideCharStr,	// 指向接收被转换字符串的缓冲区。
	int cchWideChar	// 指定由参数lpWideCharStr指向的缓冲区的宽字符个数。
);
函数功能:一个字符串到一个宽字符的字符串的映射。
返回值:函数运行成功,并且cchWideChar不为零,返回值是由lpWideCharStr指向的缓冲区中写入的宽字符数;如果函数运行成功,并且cchWideChar为零,返回值是接收到待转换字符串的缓冲区所需求的宽字符数大小。
//  UTF16转UTF8
int WideCharToMultiByte(
	UINT CodePage,	 //指定执行转换的代码页
	DWORD dwFlags,	//允许你进行额外的控制,它会影响使用了读音符号(比如重音)的字符
	_In_NLS_string_(cchWideChar)LPCWCH lpWideCharStr,  //指定要转换为宽字节字符串的缓冲区
	int cchWideChar,	//指定由参数lpWideCharStr指向的缓冲区的字符个数
	LPSTR lpMultiByteStr,	//指向接收被转换字符串的缓冲区
	int cbMultiByte,	 //指定由参数lpMultiByteStr指向的缓冲区最大值
	LPCCH lpDefaultChar,	//遇到一个不能转换的宽字符,函数便会使用pDefaultChar参数指向的字符
	LPBOOL lpUsedDefaultChar	 //至少有一个字符不能转换为其多字节形式,函数就会把这个变量设为TRUE
);
函数功能:映射一个unicode字符串到一个多字节字符串。
返回值:如果函数运行成功,并且cchMultiByte不为零,返回值是由 lpMultiByteStr指向的缓冲区中写入的字节数;如果函数运行成功,并且cchMultiByte为零,返回值是接收到待转换字符串的缓冲区所必需的字节数。

转换之后就需要分词,分词依然是通过Jieba库所提供的Cut方法。

例:
  seg_list = jieba.cut(“他来到了网易杭研大厦”) ; # 默认是精确模式
  print(", ".join(seg_list));
 结果: 他, 来到, 了, 网易, 杭研, 大厦

这里分词有了,但是这些词一定都是有意义的吗?或是说通过这些词的分析得出来的结论是准确的吗?
  我们可以想想,以前写过的作文,或是生活中的用语。是不是除了要表达真实含义的语句之外,我们还要有些代词(我,你,我们 …),或是些语气助词(啊,呀…),还有介词,副词,连接词等等。这些词一定会有且不占少数,但它们对于我们文章的却没有什么实际意义。因此我们要去掉这些词。

具体实现:Jieba中有STOP_WORD_PATH.c_str()停用词文件,我们可以将我们的分词与文件中的词比较,相同的就去掉。

通常我们认为一个文本中出现频率高的词,极大的能概括我们文本所要表达的意思。

具体实现:使用一个神器—> C++的STL容器中unordered_map。
     调用map中的count方法来统计我们的词频;
     map中的KV值刚好能对应我们的词和词频;
     其底层是哈希桶实现的,所以查找起来也比较快。
将统计好的[词,词频] 放入数组中,通过sort方法,依据词频进行排序。再取出一定范围内的高频词用于构建词频向量。###  词频向量
假使有两个句子:

句1: 我喜欢吃米饭,不喜欢吃面
句2: 我不喜欢吃米饭,也不喜欢吃面

列出所有词:

我 喜欢 吃 米饭 不 面 也

句1中的词频:[0:1, 1:2, 2:2, 3:1, 4:1, 5:1, 6:0]
句2中的词频:[0:1, 1:2, 2:2, 3:1, 4:2, 5:1, 6:1]

句1词频向量:[1, 2, 2, 1, 1, 1, 0]
句2词频向量:[1, 2, 2, 1, 2, 1, 1]

这里介绍几种计算向量相似度的方法:

  欧几里得距离

  余弦相似度

  jaccard系数(类似余弦相似度)

  曼哈顿距离(类似欧几里得距离)

  

我呢是用余弦相似度计算的:当两个向量的夹角越小就代表越相似,所以我们可以通过计算两个向量的夹角余弦值来评估他们的相似度。

python 文本相似度 改进 文本相似度分析_python 文本相似度 改进_02

效果展示:

python 文本相似度 改进 文本相似度分析_文本分类_03

项目代码实现:

https://github.com/Timecur/TestSimilarity