文本相似度

 

原理

基于词频

统计词频,构建词频特征向量,利用特征向量夹角余弦表示文本相似度。

统计文本中每个词出现的次数,即词频,通过词频构建文本向量,通过计算两个文本向量之间的余弦相似度,反映两个文本之间的相似度。这类算法也有缺点,没有很好地解决文本数据中存在的自然语言问题,即同义词和多义词。

中文分词

词是最小的能够独立活动的有意义的语言成分,英文单词之间是以空格作为自然分界符的,而汉语是以字为基本的书写单位,词语之间没有明显的区分标记,因此中文是一定要分词的。而且nlp的基础任务中,关键词抽取,词性标注,命名实体识别,语法分析,句法分析等等都默认了词是基本单位。

推荐一个库Jieba (C++, Java, python)https://github.com/fxsjy/jieba

停用词

人类语言包含很多功能词。与其他词相比,功能词没有什么实际含义。 停用词主要包括数字、标点符号及使用频率

特高的词(代词,语气助词、副词、介词、连接词 )等。

我们

怎么办

总之

此外 然而 不如 不妨 。 , ?

........

停用词不代表实际意义,所以不需要统计停用词的词频,停用词不参与构建词频向量

 

词频

词频即为单词在文章中出现的次数。

词频的大小一般可以反映一个词在一篇文章中的重要性,词频越大,可以认为该词越重要。

一片文章的语义可以由一组关键词简要概括,比如"今天早上八点钟,我要去学校上课",关键词"八点,学校,上课"。

分词编码

在构建文本词频向量时,需要考虑向量的意义,也必须保证向量的一致性,这样才有可比性。

意义:文本的语义,用词频来表示

一致性:如何保证一致性?向量中的每一维值都应该表示相同的意思。

更具体的说,一致性就体现在两个文本向量的每一维都应该表示同一个词的词频。

举个栗子:

文档1:今天/有事/,/没办法/去/学校/上课/了

文档2:真想/去/学校/上课/,/但是/今天/有事/,/去不了/学校/了

文档1中的词频:[今天:1,有事:1,没办法:1,去:1,学校:1,上课:1,了:1]

文档2中的词频:[真想:1,去:1,学校:2,上课:1,但是:1, 今天:1,有事:1,去不了:1,了:1]

去掉停用词之后:

文档1中的词频:[有事:1,没办法:1,去:1,学校:1,上课:1]

文档2中的词频:[真想:1,去:1,学校:2,上课:1, 有事:1,去不了:1]

直接用上述词频构建每一个文本的词频向量无意义,每一维表示的意思不同,两个向量没有可比性。

构建一致的词频向量:给每一维的词频编码,然后去看每一维的词频向量。

把两个文本中的所有有效词全部编码,对于长文本可以按词频从大到小排序,取前n个关键词

按照码值构建词频向量

比如:

文档1中的词频:[有事:1,没办法:1,去:1,学校:1,上课:1]

文档2中的词频:[真想:1,去:1,学校:2,上课:1, 有事:1,去不了:1]

所有有效词:学校,去,真想,上课,有事,去不了,没办法

给所有有效词编码:学校:0,去:1,真想:2,上课:3,有事:4,去不了:5,没办法:6

词频向量

通过上述词的编码值,构建词频向量

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

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

文档1词频向量:[1,1,0,1,1,0,1]

文档2词频向量:[2,1,1,1, 1,1,0]

 

向量相似度

常用计算向量相似度的方式:欧几里得距离,余弦相似度,jaccard系数(类似余弦相似度),曼哈顿距离(类似欧几里得距离)。

余弦相似度,是通过计算两个向量的夹角余弦值来评估他们的相似度。

 

代码如下

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include <fstream>
#include <unordered_map>
#include <string>
#include <unordered_set>
#include <cppjieba/jieba.hpp>
#include <windows.h>
#include <algorithm>
#include <assert.h>

using namespace std;

class TextSimilarity
{
public:
	typedef std::unordered_map<std::string, int> wordFreq;
	typedef std::unordered_set<std::string> wordSet;

	TextSimilarity(std::string dict);
	double getTextSimilarity(const char* file1, const char* file2);
	wordFreq getWordFreq(const char* file);
	std::string UTF8ToGBK(std::string str);
	std::string GBKToUTF8(std::string str);
	std::vector<std::pair<std::string, int>> sortByValueReverse(wordFreq& wf);
	void selectAimWords(std::vector<std::pair<std::string, int>>& wfvec, wordSet& wset);
	std::vector<double> getOneHot(wordSet& wset, wordFreq& wf);
	double cosine(std::vector<double> oneHot1, std::vector<double> oneHot2);
private:

	void getStopWordTable(const char* stopWordFile);

	std::string DICT;
	std::string DICT_PATH;
	std::string HMM_PATH;
	std::string USER_DICT_PATH;
	std::string IDF_PATH;
	std::string STOP_WORD_PATH;
	cppjieba::Jieba _jieba;

	wordSet _stopWordSet;
	int _maxWordNumber;

};

TextSimilarity::TextSimilarity(string dict)
	:DICT(dict)
	, DICT_PATH(dict + "/jieba.dict.utf8")
	, HMM_PATH(dict + "/hmm_model.utf8")
	, USER_DICT_PATH(dict + "/user.dict.utf8")
	, IDF_PATH(dict + "/idf.utf8")
	, STOP_WORD_PATH(dict + "/stop_words.utf8")
	, _jieba(DICT_PATH,
	         HMM_PATH,
	         USER_DICT_PATH,
             IDF_PATH,
	         STOP_WORD_PATH)
	, _maxWordNumber(50)
{
	getStopWordTable(STOP_WORD_PATH.c_str());
}

TextSimilarity::wordFreq TextSimilarity::getWordFreq(const char* filename)
{
	ifstream fin(filename);
	if (!fin.is_open())
	{
		cout << "open file" << filename << "error" << endl;
		return wordFreq();
	}

	string line;
	wordFreq wf;
	while (!fin.eof())
	{
		//从文件中读取每一行
		getline(fin,line);
		//CBK-->UTF8
		line = GBKToUTF8(line);
		vector<string> words;
		//对文本当前分词
		_jieba.Cut(line, words, true);
		//统计词频
		for (const auto& e : words)
		{
			//去掉停用词
			if (_stopWordSet.count(e) > 0)
				continue;
			
			else
			{
				//统计词频
				if (wf.count(e) > 0)
					wf[e]++;
				else
					wf[e] = 1;
			}
		}
	}
	return wf;
}

void TextSimilarity::getStopWordTable(const char* stopWordFile)
{
	ifstream fin(stopWordFile);
	if (!fin.is_open())
	{
		cout << "open file" << stopWordFile << "faile open" << endl;
		return;
	}

	string line;
	while (!fin.eof())
	{
		getline(fin, line);
		//UTF8
		_stopWordSet.insert(line);
	}
	fin.close();
}

bool cmpReverse(pair<string, int>lp, pair<string, int>rp)
{
	return lp.second > rp.second;
}

std::vector<std::pair<std::string, int>> TextSimilarity::sortByValueReverse(wordFreq& wf)
{
	//unordered_map
	vector<pair<string, int>> wfvector(wf.begin(), wf.end());
	sort(wfvector.begin(), wfvector.end(), cmpReverse);
	return wfvector;
}

string TextSimilarity::GBKToUTF8(string str)
{
	//获取buffer大小
	int len = MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, NULL, 0);
	wchar_t* wstr = new wchar_t[len];

	//GBK--->UTF16
	MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, wstr, len);
	len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL);
	char* utf8char = new char[len];

	//UTF16-->UTF8
	WideCharToMultiByte(CP_UTF8, 0, wstr, -1, utf8char, len, NULL, NULL);
	string out(utf8char);
	if (wstr)
	{
		delete[] wstr;
		wstr = NULL;
	}
	if (utf8char)
	{
		delete[] utf8char;
		utf8char = NULL;
	}

	return out;
}

string TextSimilarity::UTF8ToGBK(string str)
{
	//获取buffer大小
	int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, NULL, 0);
	wchar_t* wstr = new wchar_t[len];

	//UTF8--->UTF16
	MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, wstr, len);
	len = WideCharToMultiByte(CP_ACP, 0, wstr, -1, NULL, 0, NULL, NULL);
	char* gbkchar = new char[len];

	//UTF16-->GBK
	WideCharToMultiByte(CP_ACP, 0, wstr, -1, gbkchar, len, NULL, NULL);
	string out(gbkchar);
	if (wstr)
	{
		delete[] wstr;
		wstr = NULL;
	}
	if (gbkchar)
	{
		delete[] gbkchar;
		gbkchar = NULL;
	}

	return out;
}

void TextSimilarity::selectAimWords(std::vector<std::pair<std::string, int>>& wfvec, wordSet& wset)
{
	int len = wfvec.size();
	int sz = len > _maxWordNumber ? _maxWordNumber : len;
	for (int i = 0; i < sz; i++)
	{
		//pair<string, int>
		wset.insert(wfvec[i].first);

	}
}

std::vector<double> TextSimilarity::getOneHot(wordSet& wset, wordFreq& wf)
{
	//遍历wordset中的每一个词
	vector<double> oneHot;
	for (const auto& e : wset)
	{
		if (wf.count(e))
			//oneHot存放的是value
			oneHot.push_back(wf[e]);
		else
			oneHot.push_back(0);
	}
	return oneHot;
}

double TextSimilarity::cosine(std::vector<double> oneHot1, std::vector<double> oneHot2)
{
	double modular1 = 0, modular2 = 0;
	double products = 0;
	assert(oneHot1.size() == oneHot2.size());
	for (int i = 0; i < oneHot1.size(); i++)
	{
		//sum(a(i)*b(i))
		products += oneHot1[i] * oneHot2[i];
	}
	for (int i = 0; i < oneHot1.size(); i++)
	{
		modular1 += pow(oneHot1[i], 2);//每项的平方和
	}
	modular1 = pow(modular1, 0.5);//平方和的开方

	for (int i = 0; i < oneHot2.size(); i++)
	{
		modular2 += pow(oneHot1[i], 2);
	}
	modular2 = pow(modular2, 0.5);

	return products / (modular1 * modular2)
		;
}

void testwordfreq()
{
	TextSimilarity ts("dict");
	TextSimilarity::wordFreq wf = ts.getWordFreq("test.txt");
	for (const auto& w : wf)
	{
		cout << ts.UTF8ToGBK(w.first) << ":" << w.second << endl;
	}
	cout << endl;

	vector<pair<string, int>>wfvec = ts.sortByValueReverse(wf);
	for (const auto& w : wfvec)
	{
		cout << ts.UTF8ToGBK(w.first) << ":" << w.second << endl;
	}
}

void testoneHot()
{
	TextSimilarity ts("dict");
	TextSimilarity::wordFreq wf = ts.getWordFreq("test.txt");
	TextSimilarity::wordFreq wf2 = ts.getWordFreq("test2.txt");
	vector<pair<string, int>> wfvec = ts.sortByValueReverse(wf);
	vector<pair<string, int>> wfvec2 = ts.sortByValueReverse(wf2);
	cout << "wfvec: " << endl;
	for (int i = 0; i < 10; i++)
	{
		cout << ts.UTF8ToGBK(wfvec[i].first) << ": " << wfvec[i].second << "\t";
	}
	cout << endl;
	cout << "wfvec2: " << endl;
	for (int i = 0; i < 10; i++)
	{
		cout << ts.UTF8ToGBK(wfvec2[i].first) << ": " << wfvec2[i].second << "\t";
	}
	cout << endl;
	TextSimilarity::wordSet wset;
	//用排好序的词频挑选前n个候选词
	ts.selectAimWords(wfvec, wset);
	ts.selectAimWords(wfvec2, wset);
	cout << "wset:" << endl;
	for (const auto& e : wset)
	{
		cout << ts.UTF8ToGBK(e) << " " ;
	}
	cout << endl;
	//根据候选词构建词频向量
	vector<double> oneHot = ts.getOneHot(wset, wf);
	vector<double> oneHot2 = ts.getOneHot(wset, wf2);
	cout << "oneHot:" << endl;
	for (const auto& v : oneHot)
	{
		cout << v << " ";
	}
	cout << endl;

	cout << "oneHot2:" << endl;
	for (const auto& v : oneHot2)
	{
		cout << v << " ";
	}
	cout << endl;

	cout << "cosine:" << ts.cosine(oneHot, oneHot2) << endl;
}

int main()
{
	/*testwordfreq();*/
	testoneHot();
	system("pause");
	return 0;
}

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值