首先去《知网》的官方网站上下载WordSimilarity.rar,解压后有两个文件是我们需要的:whole.dat和glossary.dat,关于那个《基于知网的词汇语义相似度计算.doc》建议不要看,那是个老版本的,写的不清楚,可以去这里看Final版(刘群等著),本博客就是按照这个版本来计算词语相似度的,只是个细节略有改动。
现在很多人提出的“改进”算法实际上只是“拓展”,因为他们忘了刘群等人的计算方法是以基于实例的机器翻译为背景的。
一切工作都是在Linux下进行的,所先要把whole.dat和glossary.dat从cp936转换为utf8编码,使用iconv工具。
glossary.dat中有一些像“跟 ... 过不去”、“既...又”这样的词汇,我们使用分词工具不会得到这样的结果,将含有“...”的行删除。
glossary.dat一行有3项,我们希望可以利用空格把这3项分隔开,可不幸的是在第3项内部有时候也会有空格,比如“兰特 N money|货币,(South Africa|南非)”,“South Africa”中间就有空格。所以进行下面的处理:awk 'BEGIN{OFS="\t"}{print $1,$2,$3 $4 $5;}' glossary.dat>gloss.dat
下面是《知网》中的一些概念,你必须清楚:
一个词语有n个“概念”,一个“概念”有n个“义原”。比如
拉平 ADJ aValue|属性值,content|内容,neat|齐,desired|良
拉平 V equal|相等
这里“拉平”就有2个概念,并且词性还不相同,一个是ADJ,一个是V。上面那个概念就有4个义原,义原之间用逗号隔开。
whole.dat中记录了所有的义原,形如
0 event|事件 0
1 static|静态 0
2 relation|关系 1
3 isa|是非关系 2
4 be|是 3
5 become|成为 4
6 mean|指代 4
7 BeNot|非 3
8 possession|领属关系 2
9 own|有 8
每行的第3个数字指示了该义原的父节点是谁。所有的义原按照这种父子关系形成了一个森林,这个森林中有10棵树,它们的根节点分别是:
1) Event|事件
2) entity|实体
3) attribute|属性值
4) aValue|属性值
5) quantity|数量
6) qValue|数量值
7) SecondaryFeature|次要特征
8) syntax|语法
9) EventRole|动态角色
10) EventFeatures|动态属性
所有的义原还进行了分类,在1到7号树上的为“基本义原”,8号树上的是“语法义原”,9号10号树上的是“关系义原”。
同一棵树上的义原存在上下位关系,从根节点“event|事件”我们找到“become|成为”和“own|有”:
event|事件--static|静态--relation|关系--isa|是非关系--be|是--become|成为
event|事件--static|静态--relation|关系--possession|领属关系--own|有
则义原“become|成为”和“own|有”在树上的距离是5。
义原是用来解释概念的,在glossary.dat中你还会看到一些“符号”,形如
# 表示“与其相关”
^ 表示不存在,或没有,或不能
在glossary.dat的第3列中用逗号隔开的是一些“语义描述式”。
实词的“语义描述式”又可分为3类:
a) 独立义原描述式:用“基本义原”,或者“(具体词)”进行描述;
b) 关系义原描述式:用“关系义原=基本义原”或者“关系义原=(具体词)”或者“(关系义原=具体词)”来描述;
c) 符号义原描述式:用“关系符号 基本义原”或者“关系符号(具体词)”加以描述;
在glossary.dat中用小括号括起来的就是“具体词”,具体词不是义原,它们不包含在whole.dat中。
在一行中上述3种语义描述式有谁没谁、谁先出现都是不确定的,但是当有“(具体词)”出现时,就肯定有“基本义原”出现。
虚词的描述式整体被一个大括号括起来,并且大括号里不会出现关系义原描述式和具体词,比如
你 PRON {SecondPerson|你}
你们 PRON {SecondPerson|你,mass|众}
毋庸置疑V{modality|语气,neg|否,#doubt|怀疑}
注意括在大括号里的不一定是虚词概念,但虚词概念的描述式都被括在大括号里。那如何区分哪些是虚词概念呢?由于虚词概念描述式中不没有关系义原描述式,那两个虚词概念之间的相似度又该如何计算呢?我没有在相关的文献上找到答案。
按照基本的语言知识,虚词包括:副词、介词、连词、助词、叹词、拟声词。可是《知网》中的连词是用什么来标注呢?
以及COOR{and|和}
和COOR{and|和}
而 CONJ {but|但}
而 CONJ {EventResult|事件结局}
而 COOR {and|和}
纵然CONJ{concession|让步}
要不然CONJ{transition|转折}
你能分清COOR和CONJ的区别吗?
起来 STRU {Vdirection|动趋,upper|上}
进来 STRU {Vdirection|动趋,internal|内}
上来 STRU {Vdirection|动趋,upper|上}
看完上面的好像STRU表示“方向、方位”,可是再看下面的你就迷惑了:
不了 STRU {^Vable|能力}
了 STRU {MaChinese|语助}
以来 STRU {TimeIni}
及 STRU {Vachieve|达成}
总之,我感觉《知网》有一些工作粗糙的地方,刘群的论文里面也有一些没说清楚(比如哪些是虚词概念,虚词概念相似度的计算)和说错的地方(比如“{}”内从来就没有出现过“=”,“在实词的描述中,第一个描述式总是一个基本义原”这句话也是不对的,比如“不务正业 V ^endeavour|卖力,content=duty|责任”,根本就没有出现基本义原)。
一个虚词概念只有一个义原;而一个实词概念有多个义原,这些义原又分为4部分:
1) 第一独立义原描述式
2) 其他独立义原描述式:语义表达式中除第一独立义原以外的所有其他独立义原(或具体词)
3) 关系义原描述式:语义表达式中所有的用关系义原描述式
4) 符号义原描述式:语义表达式中所有的用符号义原描述式
其他独立义原描述式存在时,第一独立义原描述式就一定存在,但两都可能都不存在。同时考虑到当有“(具体词)”出现时,就肯定有“基本义原”出现,所以第一独立义原描述式肯定不是具体词。
举个例子,假如有个概念的语义表达式是:ContentProduct=text|语文,aValue|属性值,attachment|归属,#country|国家,ProperName|专,(Nicaragua|尼加拉瓜)
则我们在把它存入sqlite数据库按照4部分的顺序存储为:
aValue|属性值,
attachment|归属,ProperName|专,(Nicaragua|尼加拉瓜),
ContentProduct=text|语文,
#country|国家,
概念讲完了,下面讲词语相似度的计算。
假如一个词语有m个概念,另一个词语有n个概念,那么就有m*n种组合,计算每对概念的相似度,取最大者作为词语间的相似度。
那么概念间的相似度又如何计算呢?实词概念和虚词概念之间的相似度设为0。我们知道一个实词概念的语义表达式分为4部分:
1) 第一独立义原描述式: 这一部分的相似度记为sim1
2) 其他独立义原描述式: 这一部分的相似度记为sim2
3) 关系义原描述式: 这一部分的相似度记为sim3
4) 符号义原描述式: 这一部分的相似度记为sim4
总的相似度是这4部分的加权和
\begin{equation} sim=\sum_{i=1}^{4}{{\beta}_{i}*{sim}_{i}} \end{equation}
刘群等人实际应用的是这个公式
\begin{equation} sim=\sum_{i=1}^{4}{{\beta}_{i}\prod_{j=1}^{i}{{sim}_{j}}} \end{equation}
因为刘群等认为由于第一独立义原描述式反映了一个概念最主要的特征,主要部分的相似度值对于次要部分的相似度值应起到制约作用,也就是说,如果主要部分相似度比较低,那么次要部分的相似度对于整体相似度所起到的作用也要降低。
下面分别说明4部分的相似度如何计算。
1) 第一独立义原描述式:由于第一独立义原描述式只包含一个基本义原,因此可以转换为一对基本义原相似度的计算。
2) 其他独立义原描述式:这部分包含多个基本义原(或具体词),按照如下步骤对这些独立义原描述式分组:
a) 先把两个表达式的所有独立义原(第一个除外)任意配对,计算出所有可能的配对的义原相似度;
b) 取相似度最大的一对,并将它们归为一组;
c) 在剩下的独立义原的配对相似度中,取最大的一对,并归为一组,如此反复,直到所有独立义原都完成分组。
最后不对配对的丢弃。计算每对义原的相似度,算术平均后得到sim2
3) 关系义原描述式:把关系义原相同的描述式分为一组,不能配对的舍弃。计算每对义原的相似度,算术平均后得到sim3
4) 符号义原描述式: 与3)类似,把符号义原相同的描述式分为一组。
剩下的问题就是如何计算一对义原的相似度。
义原和具体词的相似度记为0;具体词和具体词相同时相似度记为1,不同时记为0;义原和义原的相似度用公式:
\begin{equation}sim({p}_{1},{p}_{2})=\frac{\alpha}{\alpha+d}\end{equation}
d是两个义原在树上的距离,如果两个义原不在同一棵树上,则相似度记为0。
刘群等人用的参数是:
α= 1.6;
β1 = 0.5, β2 = 0.2,β3 = 0.17,β4 = 0.13
下面是我的代码
注意我粗略地认为括在大括号里的就是虚词概念,并且一对虚词概念它们的描述式中只含有一个语法义原,这显然是非常错误的。
glossary2db.cpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
const int KeyWordLen=60; //“概念”的最大长度
const int POSLen=8; //“词性”的最大长度
const int SememeSetLen=200; //一个“概念”对应的“义原”集合的最大长度
int main(int argc,char *argv[]){
sqlite3 *db;
char *zErrMsg=0;
int rc;
rc=sqlite3_open("glossary.db",&db); //打开数据库
assert(rc==SQLITE_OK);
char sql[500]={0};
sprintf(sql,"create table t_gloss(id integerprimary key,concept varchar(%d),pos char(%d),semset varchar(%d))",KeyWordLen,POSLen,SememeSetLen);
rc=sqlite3_exec(db,sql,0,0,&zErrMsg); //创建表
assert(rc==SQLITE_OK);
ifstream ifs("glossary.dat"); //打开词典文件
assert(ifs);
string line;
int recordid=0;
while(getline(ifs,line)){ //逐行读取词典文件
istringstream stream(line);
string word,pos,sememe;
stream>>word>>pos>>sememe; //由空白把一行分割成:词、词性、义原集合
string set;
if(sememe[0]=='{'){ //该行是虚词,因为虚词的描述只有“{句法义原}”或“{关系义原}”
set=sememe+",";
}
else{ //该行是实词,要按“基本义原描述式\n其他义原描述式\n关系义原描述式\n关系符号义原描述式”存储
string str1,str2,str3,str4;
string::size_type pos1,pos2;
pos1=0;
bool flag=true;
while(flag){
pos2=sememe.find(",",pos1);
string sem;
if(string::npos==pos2){ //已是最后一个义原
flag=false;
sem=sememe.substr(pos1); //提取最后一个义原
}
else{
sem=sememe.substr(pos1,pos2-pos1); //提取下一个义原
}
pos1=pos2+1;
if(sem.find("=")!=string::npos){ //关系义原,加入str3
str3+=sem+",";
}
else{
char c=sem[0];
if((c>64&&c<91) || (c>96&&c<123) || (c==40)){ //义原以大/小写英文字母开始,或者是具体词--单独在小括号里,属于其他义原,加入str2。40是"("的ASCII值
str2+=sem+",";
}
else{ //关系符号义原,加入str4
str4+=sem+",";
}
}
}
//把str2中的第一条取出来,赋给str1
string::size_type pos3=str2.find(",");
if(pos3!=string::npos){
str1=str2.substr(0,pos3+1);
str2.erase(0,pos3+1);
}
set=str1+"\n"+str2+"\n"+str3+"\n"+str4;
}
bzero(sql,sizeof(sql));
sprintf(sql,"insert into t_gloss values(%d,\'%s\',\'%s\',\'%s\')",recordid++,word.c_str(),pos.c_str(),set.c_str());
rc=sqlite3_exec(db,sql,0,0,&zErrMsg);
assert(rc==SQLITE_OK);
}
ifs.close();
//在“概念”上建立索引。以后要经常依据“概念”进行查询
bzero(sql,sizeof(sql));
sprintf(sql,"create index index1 on t_gloss(concept)");
rc=sqlite3_exec(db,sql,0,0,&zErrMsg);
assert(rc==SQLITE_OK);
sqlite3_close(db);
return 0;
}
similary.cpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
const int vec_len=1618; //一共vec_len个基本义原
const double alpha=1.6; //计算基本义原相似度时的参数
const double beta1=0.5; //4种描述式相似度的权值
const double beta2=0.2;
const double beta3=0.17;
const double beta4=0.13;
//const double delta=0.2;
//const double gama=0.2;
class myclass{
public:
int index;
string sememe;
int parent;
myclass(){};
myclass(int i,string sem,int p):index(i),sememe(sem),parent(p){}
//重载关系运算符是为了使用STL中的find()函数
inline bool operator == (const myclass & m){
return sememe.compare(m.sememe)==0;
}
inline bool operator >(const myclass & m) const {
return sememe.compare(m.sememe)>0;
}
inline bool operator <(const myclass & m) const {
return sememe.compare(m.sememe)<0;
}
};
vector semeVec(vec_len);
//把基本义原从文件读入vector
void initSemVec(string filename){
ifstream ifs(filename.c_str());
assert(ifs);
string line;
while(getline(ifs,line)){
istringstream stream(line);
int index,pind;
string seme;
stream>>index>>seme>>pind;
myclass mc(index,seme,pind);
semeVec[index]=mc;
}
ifs.close();
}
//把用逗号分隔的string片段放到vector中
static void splitString(string line,vector &vec){
string::size_type pos1,pos2;
pos1=0;
while((pos2=line.find(",",pos1))!=string::npos){
string sem=line.substr(pos1,pos2-pos1);
//把可能包含的一对小括号去掉
/*string::size_type pp=sem.find("(");
if(pp!=string::npos){
sem.erase(pp,1);
pp=sem.find(")");
sem.erase(pp,1);
}*/
vec.push_back(sem);
pos1=pos2+1;
if(pos1>line.size())
break;
}
}
//计算两个基本义原的相似度
double calSimBase(string sem1,string sem2){
assert(sem1.size()>0 && sem2.size()>0);
if(sem1[0]==40 ^ sem2[0]==40) //有一个是具体词,而另一个不是
return 0;
if(sem1[0]==40 && sem2[0]==40){ //如果两个都是具体词
if(sem1!=sem2)
return 0.0;
}
if(sem1==sem2)
return 1.0;
cout<<"将要计算基本义原["< stk1,stk2;
myclass mc1(0,sem1,0);
myclass mc2(0,sem2,0);
vector::iterator itr=find(semeVec.begin(),semeVec.end(),mc1);
if(itr==semeVec.end()){
cout<<"["<index;
int parent=itr->parent;
while(child!=parent){
stk1.push(semeVec[parent].sememe);
child=parent;
parent=semeVec[parent].parent;
}
itr=find(semeVec.begin(),semeVec.end(),mc2);
if(itr==semeVec.end()){
cout<<"["<index;
parent=itr->parent;
while(child!=parent){
stk2.push(semeVec[parent].sememe);
child=parent;
parent=semeVec[parent].parent;
}
if(stk1.top()!=stk2.top()){
cout<<"["< vec1,vec2;
splitString(line1,vec1);
splitString(line2,vec2);
assert(vec1.size()==1 && vec2.size()==1);
return calSimBase(vec1[0],vec2[0]);
}
//计算其他独立义原描述式的相似度
double calSim2(string line1,string line2){
if(line1=="" || line2=="")
return 0;
cout<<"将要计算其他独立义原描述式["< maxsim_vec;
vector vec1,vec2;
splitString(line1,vec1);
splitString(line2,vec2);
int len1=vec1.size();
int len2=vec2.size();
while(len1 && len2){
int m,n;
double max_sim=0.0;
for(int i=0;imax_sim){
m=i;
n=j;
max_sim=simil;
}
}
}
if(max_sim==0.0)
break;
maxsim_vec.push_back(max_sim);
vec1.erase(vec1.begin()+m);
vec2.erase(vec2.begin()+m);
len1=vec1.size();
len2=vec2.size();
}
//把整体相似度还原为部分相似度的加权平均,这里权值取一样,即计算算术平均
if(maxsim_vec.size()==0)
return 0.0;
double sum=0.0;
vector::const_iterator itr=maxsim_vec.begin();
while(itr!=maxsim_vec.end())
sum+=*itr++;
return sum/maxsim_vec.size();
}
//计算关系义原描述式的相似度
double calSim3(string line1,string line2){
if(line1=="" || line2=="")
return 0;
cout<<"将要计算关系义原描述式["< sim_vec;
vector vec1,vec2;
splitString(line1,vec1);
splitString(line2,vec2);
int len1=vec1.size();
int len2=vec2.size();
while(len1 && len2){
for(int j=0;j::const_iterator itr=sim_vec.begin();
while(itr!=sim_vec.end())
sum+=*itr++;
return sum/sim_vec.size();
}
//计算符号义原描述式的相似度
double calSim4(string line1,string line2){
if(line1=="" || line2=="")
return 0;
cout<<"将要计算符号义原描述式["< sim_vec;
vector vec1,vec2;
splitString(line1,vec1);
splitString(line2,vec2);
int len1=vec1.size();
int len2=vec2.size();
while(len1 && len2){
char sym1=vec1[len1-1][0];
for(int j=0;j::const_iterator itr=sim_vec.begin();
while(itr!=sim_vec.end())
sum+=*itr++;
return sum/sim_vec.size();
}
//计算两个“概念”的相似度
double calConceptSim(string concept1,string concept2){
cout<<"将要计算概念["< *vec=(vector *)output_arg;
string rect(argv[0]);
vec->push_back(rect);
return 0;
}
//计算两个词语的相似度
double calWordSim(string word1,string word2,sqlite3 *db){
cout<<"将要计算词语["< vec1,vec2; //两个词语的概念分别存放在vec1和vec2中
char sql[100]={0};
sprintf(sql,"select semset from t_gloss where concept=\'%s\'",word1.c_str());
rc=sqlite3_exec(db,sql,select_callback,&vec1,&zErrMsg);
assert(rc==SQLITE_OK);
sprintf(sql,"select semset from t_gloss where concept=\'%s\'",word2.c_str());
rc=sqlite3_exec(db,sql,select_callback,&vec2,&zErrMsg);
assert(rc==SQLITE_OK);
int len1=vec1.size();
int len2=vec2.size();
if(len1==0)
cout<maxsim)
maxsim=sim;
}
}
return maxsim;
}
int main(int argc,char *argv[]){
if(argc<3){
cerr<<"Usage:command word1 word2."< vecstr;
string line;
while(getline(ifs,line)){
istringstream stream(line);
string word;
stream>>word;
vecstr.push_back(word);
}
ifs.close();
string fn("whole.dat");
initSemVec(fn);
sqlite3 *db;
char *zErrMsg=0;
int rc;
rc=sqlite3_open("glossary.db",&db); //打开数据库
assert(rc==SQLITE_OK);
int len=vecstr.size();
double sim=0;
for(int i=0;i