这两天正好在项目中需要提取词干(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."); } }
|