这两天正好在项目中需要提取词干(word stemming),词干是什么?比如documentation这个词,它的词干就是document。再比如tables这个复数形式,它的词干就是tabl。词干也许可以理解为类似于词根一样的概念。我没有去查准确的定义,不过我想它的用处是显而易见的。我们如果想比较两个词的相似程度,比如下面两个词:go和went。这怎么办,其实从目的上讲我们是希望这两个词有较高的相似度的(语义上极为相似),然而从简单的字符串处理方法上,比如编辑距离的处理方式,这两个词也许就很不相似了。然而经过提取词干以后,一切就不一样了,went能够被还原成go,很有用的方法。再比如典型的应用:stem和stemming这两个词如果要考虑语义相似性,那应当是非常相似的(只不过是两种时态而已),可是从编辑距离或者VSM的角度考虑,也许他们的相似性要大打折扣。然而stemming提取词干以后,就还原成了stem。
我想过自己去实现一个这样的工具,然而翻阅了一些经典的英语语法书籍,发现要考虑的事情太多了,感兴趣的可以去这个地方看看:http://www.phon.ucl.ac.uk/home/dick/enc/intro.htm 于是我寻找各种现有的开源项目。其实首先接触到的是一个叫做KIMMO的工具,我也是无意中通过它才知道了提取词干这回事。它是用脚本语言编写的,我对这个方面不很熟悉,不敢贸然使用。然后才知道提取词干方面也许是一个很权威的方法:Porter Stemming算法,它的主页是:http://www.tartarus.org/~martin/PorterStemmer/ 幸运的,我找到了它的一个开源的应用,是他们自己的工作结晶,一个叫做Snowball的项目,地址是:http://snowball.tartarus.org/ 他们的库可以在这儿下载:http://snowball.tartarus.org/dist/libstemmer_java.tgz
集成这个工具来提取词干是很方便的,要注意的是这个工具不仅支持提取英文词干,也支持法语、俄语等多种其他语言(当然不包括中文)。下面是一个典型的应用实例,我将它集成到了我的分词程序中,以下是全部源代码:
import java.util.*;
import java.lang.reflect.Method;
import org.tartarus.snowball.*;
public class SplitWords {
/* 分隔符的集合 */
private final String delimiters = " \t\n\r\f~!@#$%^&*()_+|`-=\\{}[]:\";'<>?,./'";
/* 语言 */
private final String language = "english";
public String[] split(String source) {
/* 根据分隔符分词 */
StringTokenizer stringTokenizer = new StringTokenizer(source,
delimiters);
/* 所有的词 */
Vector vector = new Vector();
/* 全大写的词 -- 不用提词干所以单独处理 */
Vector vectorForAllUpperCase = new Vector();
/* 根据大写字母分词 */
flag0: while (stringTokenizer.hasMoreTokens()) {
String token = stringTokenizer.nextToken();
/* 全大写的词单独处理 */
boolean allUpperCase = true;
for (int i = 0; i < token.length(); i++) {
if (!Character.isUpperCase(token.charAt(i))) {
allUpperCase = false;
}
}
if (allUpperCase) {
vectorForAllUpperCase.addElement(token);
continue flag0;
}
/* 非全大写的词 */
int index = 0;
flag1: while (index < token.length()) {
flag2: while (true) {
index++;
if ((index == token.length())
|| !Character.isLowerCase(token.charAt(index))) {
break flag2;
}
}
vector.addElement(token.substring(0, index).toLowerCase());
token = token.substring(index);
index = 0;
continue flag1;
}
}
/* 提词干 */
try {
Class stemClass = Class.forName("org.tartarus.snowball.ext."
+ language + "Stemmer");
SnowballProgram stemmer = (SnowballProgram) stemClass.newInstance();
Method stemMethod = stemClass.getMethod("stem", new Class[0]);
Object[] emptyArgs = new Object[0];
for (int i = 0; i < vector.size(); i++) {
stemmer.setCurrent((String) vector.elementAt(i));
stemMethod.invoke(stemmer, emptyArgs);
vector.setElementAt(stemmer.getCurrent(), i);
}
} catch (Exception e) {
e.printStackTrace();
}
/* 合并全大写的词 */
for (int i = 0; i < vectorForAllUpperCase.size(); i++) {
vector.addElement(vectorForAllUpperCase.elementAt(i));
}
/* 转为数组形式 */
String[] array = new String[vector.size()];
Enumeration enumeration = vector.elements();
int index = 0;
while (enumeration.hasMoreElements()) {
array[index] = (String) enumeration.nextElement();
index++;
}
/* 打印显示 */
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
/* 返回 */
return array;
}
public static void main(String args[]) {
SplitWords sw = new SplitWords();
sw.split("These tables are for ARE-Company using only.");
}
}