波特词干算法
位于分类 自然语言处理
在英语中,一个单词常常是另一个单词的“变种”,如:happy=>happiness,这里happy叫做happiness的词干(stem)。在信息检索系统中,我们常常做的一件事,就是在Term规范化过程中,提取词干(stemming),即除去英文单词分词变换形式的结尾。
应用最为广泛的、中等复杂程度的、基于后缀剥离的词干提取算法是波特词干算法,也叫波特词干器(Porter Stemmer)。详见官方网站。比较热门的检索系统包括Lucene、Whoosh等中的词干过滤器就是采用的波特词干算法。
简单说一下历史:
马丁.波特博士(Dr. Martin Porter)于1979年,在英国剑桥大学,计算机实验室,发明了波特词干算法。
波特词干算法当时是作为一个大型IR项目的一部分被提出的。它的原始论文为:
C.J. van Rijsbergen, S.E. Robertson and M.F. Porter, 1980. New models in probabilistic information retrieval. London: British Library. (British Library Research and Development Report, no. 5587).
最初的波特词干提取算法是使用BCPL语言编写的。作者在其网站上公布了各种语言的实现版本,其中C语言的版本是作者编写的最权威的版本。
波特词干器适用于涉及到提取词干的IR研究工作,其实验结果是可重复的,言外之意是说,波特词干器的输出结果是确定性的,不是随机的。(还有基于随机的高级词干提取算法,虽然会更准确,但同时也更加复杂)。词干提取算法无法达到100%的准确程度,因为语言单词本身的变化存在着许多例外的情况,无法概括到一般的规则中。使用词干提取算法能够帮助提高IR的性能。
波特词干算法的官方网站上,有各个语言的实现版本(其实都是C标准的各个翻译形式)。各位要应用到实际生产中可以直接下载对应的版本。本文将会分析Java语言的源码。在今后的文章中,再介绍使用Python特性优化过的版本。(Python原版几乎就是C语言版本的翻译,这也就意味着不能充分利用Python的语言特性。)
在实际处理中,需要分六步走。首先,我们先定义一个Stemmer类。
12345678910111213classStemmer{privatechar[] b;privateinti,/* b中的元素位置(偏移量) */i_end, /* 要抽取词干单词的结束位置 */j, k;private static final int INC = 50;/* 随着b的大小增加数组要增长的长度(防止溢出) */publicStemmer(){ b =newchar[INC];i =0;i_end =0;}}这里,b是一个数组,用来存待词干提取的单词(以char的形式)。这里的变量k会随着词干抽取而变化。
接着,我们要添加单词来进行处理:
1234567891011121314151617181920212223/*** 增加一个字符到要存放待处理的单词的数组。添加完字符时,* 可以调用stem(void)方法来进行抽取词干的工作。*/publicvoidadd(charch){if(i == b.length){char[] new_b =newchar[i+INC];for(intc =0; c < i; c++) new_b[c] = b[c];b = new_b;}b[i++] = ch;}/** 增加wLen长度的字符数组到存放待处理的单词的数组b。*/publicvoidadd(char[] w,intwLen){if(i+wLen >= b.length){char[] new_b =newchar[i+wLen+INC];for(intc =0; c < i; c++) new_b[c] = b[c];b = new_b;}for(intc =0; c < wLen; c++) b[i++] = w[c];}大家可能会觉得这么处理字符串太麻烦了吧,要明白,整个代码是从C移植过来的。
接下来,是一系列工具函数。首先先介绍一下它们:
- cons(i):参数i:int型;返回值bool型。当i为辅音时,返回真;否则为假。
- m():返回值:int型。表示单词b介于0和j之间辅音序列的个度。现假设c代表辅音序列,而v代表元音序列。<..>表示任意存在。于是有如下定义;
- <c><v> 结果为 0
- <c>vc<v> 结果为 1
- <c>vcvc<v> 结果为 2
- <c>vcvcvc<v> 结果为 3
- ....
- vowelinstem():返回值:bool型。从名字就可以看得出来,表示单词b介于0到i之间是否存在元音。
- doublec(j):参数j:int型;返回值bool型。这个函数用来表示在j和j-1位置上的两个字符是否是相同的辅音。
- cvc(i):参数i:int型;返回值bool型。对于i,i-1,i-2位置上的字符,它们是“辅音-元音-辅音”的形式,并且对于第二个辅音,它不能为w、x、y中的一个。这个函数用来处理以e结尾的短单词。比如说cav(e),lov(e),hop(e),crim(e)。但是像snow,box,tray就辅符合条件。
- ends(s):参数:String;返回值:bool型。顾名思义,判断b是否以s结尾。
- setto(s):参数:String;void类型。把b在(j+1)...k位置上的字符设为s,同时,调整k的大小。
- r(s):参数:String;void类型。在m()>0的情况下,调用setto(s)。
简单贴出来这些工具函数的代码。
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283// cons(i) 为真 <=> b[i] 是一个辅音privatefinalbooleancons(inti){switch(b[i]){case'a':case'e':case'i':case'o':case'u':returnfalse;//aeioucase'y':return(i==0) ?true: !cons(i-1);//y开头,为辅;否则看i-1位,如果i-1位为辅,y为元,反之亦然。default:returntrue;}}// m() 用来计算在0和j之间辅音序列的个数。 见上面的说明。 */privatefinalintm(){intn =0;//辅音序列的个数,初始化inti =0;//偏移量while(true){if(i > j)returnn;//如果超出最大偏移量,直接返回nif(! cons(i))break;//如果是元音,中断i++;//辅音移一位,直到元音的位置}i++;//移完辅音,从元音的第一个字符开始while(true)//循环计算vc的个数{while(true)//循环判断v{if(i > j)returnn;if(cons(i))break;//出现辅音则终止循环i++;}i++;n++;while(true)//循环判断c{if(i > j)returnn;if(! cons(i))break;i++;}i++;}}// vowelinstem() 为真 <=> 0,...j 包含一个元音privatefinalbooleanvowelinstem(){inti;for(i =0; i <= j; i++)if(! cons(i))returntrue;returnfalse;}// doublec(j) 为真 <=> j,(j-1) 包含两个一样的辅音privatefinalbooleandoublec(intj){if(j <1)returnfalse;if(b[j] != b[j-1])returnfalse;returncons(j);}/* cvc(i) is 为真 <=> i-2,i-1,i 有形式: 辅音 - 元音 - 辅音并且第二个c不是 w,x 或者 y. 这个用来处理以e结尾的短单词。 e.g.cav(e), lov(e), hop(e), crim(e), 但不是snow, box, tray.*/privatefinalbooleancvc(inti){if(i <2|| !cons(i) || cons(i-1) || !cons(i-2))returnfalse;{intch = b[i];if(ch =='w'|| ch =='x'|| ch =='y')returnfalse;}returntrue;}privatefinalbooleanends(String s){intl = s.length();into = k-l+1;if(o <0)returnfalse;for(inti =0; i < l; i++)if(b[o+i] != s.charAt(i))returnfalse;j = k-l;returntrue;}// setto(s) 设置 (j+1),...k 到s字符串上的字符, 并且调整k值privatefinalvoidsetto(String s){intl = s.length();into = j+1;for(inti =0; i < l; i++) b[o+i] = s.charAt(i);k = j+l;}privatefinalvoidr(String s) {if(m() >0) setto(s); }接下来,就是分六步来进行处理的过程。
第一步,处理复数,以及ed和ing结束的单词。
123456789101112131415161718192021222324252627282930313233343536373839404142/* step1() 处理复数,ed或者ing结束的单词。比如:caresses -> caressponies -> ponities -> ticaress -> caresscats -> catfeed -> feedagreed -> agreedisabled -> disablematting -> matmating -> matemeeting -> meetmilling -> millmessing -> messmeetings -> meet*/privatefinalvoidstep1(){if(b[k] =='s'){if(ends("sses")) k -=2;//以“sses结尾”elseif(ends("ies")) setto("i");//以ies结尾,置为ielseif(b[k-1] !='s') k--;//两个s结尾不处理}if(ends("eed")) {if(m() >0) k--; }//以“eed”结尾,当m>0时,左移一位elseif((ends("ed") || ends("ing")) && vowelinstem()){ k = j;if(ends("at")) setto("ate");elseif(ends("bl")) setto("ble");elseif(ends("iz")) setto("ize");elseif(doublec(k))//如果有两个相同辅音{ k--;{intch = b[k];if(ch =='l'|| ch =='s'|| ch =='z') k++;}}elseif(m() ==1&& cvc(k)) setto("e");}}第二步,如果单词中包含元音,并且以y结尾,将y改为i。代码很简单:
1privatefinalvoidstep2() {if(ends("y") && vowelinstem()) b[k] ='i'; }第三步,将双后缀的单词映射为单后缀。
12345678910111213141516171819202122232425262728293031323334/* step3() 将双后缀的单词映射为单后缀。 所以 -ization ( = -ize 加上-ation) 被映射到 -ize 等等。 注意在去除后缀之前必须确保m() > 0. */privatefinalvoidstep3() {if(k ==0)return;switch(b[k-1]){case'a':if(ends("ational")) { r("ate");break; }if(ends("tional")) { r("tion");break; }break;case'c':if(ends("enci")) { r("ence");break; }if(ends("anci")) { r("ance");break; }break;case'e':if(ends("izer")) { r("ize");break; }break;case'l':if(ends("bli")) { r("ble");break; }if(ends("alli")) { r("al");break; }if(ends("entli")) { r("ent");break; }if(ends("eli")) { r("e");break; }if(ends("ousli")) { r("ous");break; }break;case'o':if(ends("ization")) { r("ize");break; }if(ends("ation")) { r("ate");break; }if(ends("ator")) { r("ate");break; }break;case's':if(ends("alism")) { r("al");break; }if(ends("iveness")) { r("ive");break; }if(ends("fulness")) { r("ful");break; }if(ends("ousness")) { r("ous");break; }break;case't':if(ends("aliti")) { r("al");break; }if(ends("iviti")) { r("ive");break; }if(ends("biliti")) { r("ble");break; }break;case'g':if(ends("logi")) { r("log");break; }} }第四步,处理-ic-,-full,-ness等等后缀。和步骤3有着类似的处理。
1234567891011121314privatefinalvoidstep4() {switch(b[k]){case'e':if(ends("icate")) { r("ic");break; }if(ends("ative")) { r("");break; }if(ends("alize")) { r("al");break; }break;case'i':if(ends("iciti")) { r("ic");break; }break;case'l':if(ends("ical")) { r("ic");break; }if(ends("ful")) { r("");break; }break;case's':if(ends("ness")) { r("");break; }break;} }第五步,在<c>vcvc<v>情形下,去除-ant,-ence等后缀。
12345678910111213141516171819202122232425262728privatefinalvoidstep5(){if(k ==0)return;switch(b[k-1]){case'a':if(ends("al"))break;return;case'c':if(ends("ance"))break;if(ends("ence"))break;return;case'e':if(ends("er"))break;return;case'i':if(ends("ic"))break;return;case'l':if(ends("able"))break;if(ends("ible"))break;return;case'n':if(ends("ant"))break;if(ends("ement"))break;if(ends("ment"))break;/* element etc. not stripped before the m */if (ends("ent")) break; return;case 'o': if (ends("ion") && j >= 0 && (b[j] == 's' || b[j] == 't')) break;/* j >= 0 fixes Bug 2 */if (ends("ou")) break; return;/* takes care of -ous */case's':if(ends("ism"))break;return;case't':if(ends("ate"))break;if(ends("iti"))break;return;case'u':if(ends("ous"))break;return;case'v':if(ends("ive"))break;return;case'z':if(ends("ize"))break;return;default:return;}if(m() >1) k = j;}第六步,也就是最后一步,在m()>1的情况下,移除末尾的“e”。
12345678privatefinalvoidstep6(){ j = k;if(b[k] =='e'){inta = m();if(a >1|| a ==1&& !cvc(k-1)) k--;}if(b[k] =='l'&& doublec(k) && m() >1) k--;}在了解了步骤之后,我们写一个stem()方法,来完成得到词干的工作。
123456789/** 通过调用add()方法来讲单词放入词干器数组b中* 可以通过下面的方法得到结果:* getResultLength()/getResultBuffer() or toString().*/publicvoidstem(){ k = i -1;if(k >1) { step1(); step2(); step3(); step4(); step5(); step6(); }i_end = k+1; i =0;}最后要提醒的就是,传入的单词必须是小写。关于Porter Stemmer的实现,就看到这里。如果是Java代码这么写,无可厚非(实际上也不是很美观)。对于Python来说,如果写成这样,实在是让人难以接受。以后的文章,将会实现符合Python习惯的写法。
需要测试数据这里是样本文件。而相应的输出文件在这里。更多内容请参考官方网站。
另外,波特词干算法有第二个版本,它的处理结果要比文中所介绍的算法准确度高,但是,相应地也就更复杂,消耗的时间也就更多。本文就不作解释,详细参考官方网站The Porter2 stemming algorithm。
浙公网安备 33010602011771号