在英语中,一个单词常常是另一个单词的“变种”,如:happy=>happiness,这里happy叫做happiness的词干(stem)。在信息检索系统中,我们常常做的一件事,就是在Term规范化过程中,提取词干(stemming),即除去英文单词分词变换形式的结尾。

应用最为广泛的、中等复杂程度的、基于后缀剥离的词干提取算法是波特词干算法,也叫波特词干器(Porter Stemmer)。详见官方网站。比较热门的检索系统包括Lucene、Whoosh等中的词干过滤器就是采用的波特词干算法。

词干提取算法无法达到100%的准确程度,因为语言单词本身的变化存在着许多例外的情况,无法概括到一般的规则中。使用词干提取算法能够帮助提高IR的性能。

波特词干算法的官方网站上,有各个语言的实现版本(其实都是C标准的各个翻译形式)。各位要应用到实际生产中可以直接下载对应的版本。本文将会分析Java语言的源码。在今后的文章中,再介绍使用Python特性优化过的版本。(Python原版几乎就是C语言版本的翻译,这也就意味着不能充分利用Python的语言特性。)

在实际处理中,需要分六步走。首先,我们先定义一个Stemmer类。

[java] view plain copy  print?
  1. class Stemmer
  2. {  private char[] b;
  3. private int i,     /* b中的元素位置(偏移量) */
  4. i_end, /* 要抽取词干单词的结束位置 */
  5. j, k;
  6. private static final int INC = 50;
  7. /* 随着b的大小增加数组要增长的长度(防止溢出) */
  8. public Stemmer()
  9. {  b = new char[INC];
  10. i = 0;
  11. i_end = 0;
  12. }
  13. }

这里,b是一个数组,用来存待词干提取的单词(以char的形式)。这里的变量k会随着词干抽取而变化。

接着,我们要添加单词来进行处理:

[java] view plain copy  print?
  1. /**
  2. * 增加一个字符到要存放待处理的单词的数组。添加完字符时,
  3. * 可以调用stem(void)方法来进行抽取词干的工作。
  4. */
  5. public void add(char ch)
  6. {  if (i == b.length)
  7. {  char[] new_b = new char[i+INC];
  8. for (int c = 0; c < i; c++) new_b[c] = b[c];
  9. b = new_b;
  10. }
  11. b[i++] = ch;
  12. }
  13. /** 增加wLen长度的字符数组到存放待处理的单词的数组b。
  14. */
  15. public void add(char[] w, int wLen)
  16. {  if (i+wLen >= b.length)
  17. {  char[] new_b = new char[i+wLen+INC];
  18. for (int c = 0; c < i; c++) new_b[c] = b[c];
  19. b = new_b;
  20. }
  21. for (int c = 0; c < wLen; c++) b[i++] = w[c];
  22. }

大家可能会觉得这么处理字符串太麻烦了吧,要明白,整个代码是从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)。

简单贴出来这些工具函数的代码。

[java] view plain copy  print?
  1. // cons(i) 为真 <=> b[i] 是一个辅音
  2. private final boolean cons(int i)
  3. {  switch (b[i])
  4. {  case 'a': case 'e': case 'i': case 'o': case 'u': return false; //aeiou
  5. case 'y': return (i==0) ? true : !cons(i-1);
  6. //y开头,为辅;否则看i-1位,如果i-1位为辅,y为元,反之亦然。
  7. default: return true;
  8. }
  9. }
  10. // m() 用来计算在0和j之间辅音序列的个数。 见上面的说明。 */
  11. private final int m()
  12. {  int n = 0; //辅音序列的个数,初始化
  13. int i = 0; //偏移量
  14. while(true)
  15. {  if (i > j) return n; //如果超出最大偏移量,直接返回n
  16. if (! cons(i)) break; //如果是元音,中断
  17. i++; //辅音移一位,直到元音的位置
  18. }
  19. i++; //移完辅音,从元音的第一个字符开始
  20. while(true)//循环计算vc的个数
  21. {  while(true) //循环判断v
  22. {  if (i > j) return n;
  23. if (cons(i)) break; //出现辅音则终止循环
  24. i++;
  25. }
  26. i++;
  27. n++;
  28. while(true) //循环判断c
  29. {  if (i > j) return n;
  30. if (! cons(i)) break;
  31. i++;
  32. }
  33. i++;
  34. }
  35. }
  36. // vowelinstem() 为真 <=> 0,...j 包含一个元音
  37. private final boolean vowelinstem()
  38. {  int i; for (i = 0; i <= j; i++) if (! cons(i)) return true;
  39. return false;
  40. }
  41. // doublec(j) 为真 <=> j,(j-1) 包含两个一样的辅音
  42. private final boolean doublec(int j)
  43. {  if (j < 1) return false;
  44. if (b[j] != b[j-1]) return false;
  45. return cons(j);
  46. }
  47. /* cvc(i) is 为真 <=> i-2,i-1,i 有形式: 辅音 - 元音 - 辅音
  48. 并且第二个c不是 w,x 或者 y. 这个用来处理以e结尾的短单词。 e.g.
  49. cav(e), lov(e), hop(e), crim(e), 但不是
  50. snow, box, tray.
  51. */
  52. private final boolean cvc(int i)
  53. {  if (i < 2 || !cons(i) || cons(i-1) || !cons(i-2)) return false;
  54. {  int ch = b[i];
  55. if (ch == 'w' || ch == 'x' || ch == 'y') return false;
  56. }
  57. return true;
  58. }
  59. private final boolean ends(String s)
  60. {  int l = s.length();
  61. int o = k-l+1;
  62. if (o < 0) return false;
  63. for (int i = 0; i < l; i++) if (b[o+i] != s.charAt(i)) return false;
  64. j = k-l;
  65. return true;
  66. }
  67. // setto(s) 设置 (j+1),...k 到s字符串上的字符, 并且调整k值
  68. private final void setto(String s)
  69. {  int l = s.length();
  70. int o = j+1;
  71. for (int i = 0; i < l; i++) b[o+i] = s.charAt(i);
  72. k = j+l;
  73. }
  74. private final void r(String s) { if (m() > 0) setto(s); }

接下来,就是分六步来进行处理的过程。

第一步,处理复数,以及ed和ing结束的单词。

[java] view plain copy  print?
  1. /* step1() 处理复数,ed或者ing结束的单词。比如:
  2. caresses  ->  caress
  3. ponies    ->  poni
  4. ties      ->  ti
  5. caress    ->  caress
  6. cats      ->  cat
  7. feed      ->  feed
  8. agreed    ->  agree
  9. disabled  ->  disable
  10. matting   ->  mat
  11. mating    ->  mate
  12. meeting   ->  meet
  13. milling   ->  mill
  14. messing   ->  mess
  15. meetings  ->  meet
  16. */
  17. private final void step1()
  18. {  if (b[k] == 's')
  19. {  if (ends("sses")) k -= 2; //以“sses结尾”
  20. else if (ends("ies")) setto("i"); //以ies结尾,置为i
  21. else if (b[k-1] != 's') k--; //两个s结尾不处理
  22. }
  23. if (ends("eed")) { if (m() > 0) k--; } //以“eed”结尾,当m>0时,左移一位
  24. else if ((ends("ed") || ends("ing")) && vowelinstem())
  25. {  k = j;
  26. if (ends("at")) setto("ate"); else
  27. if (ends("bl")) setto("ble"); else
  28. if (ends("iz")) setto("ize"); else
  29. if (doublec(k))//如果有两个相同辅音
  30. {  k--;
  31. {  int ch = b[k];
  32. if (ch == 'l' || ch == 's' || ch == 'z') k++;
  33. }
  34. }
  35. else if (m() == 1 && cvc(k)) setto("e");
  36. }
  37. }

第二步,如果单词中包含元音,并且以y结尾,将y改为i。代码很简单:

[java] view plain copy  print?
  1. private final void step2() { if (ends("y") && vowelinstem()) b[k] = 'i'; }

第三步,将双后缀的单词映射为单后缀。

[java] view plain copy  print?
  1. /* step3() 将双后缀的单词映射为单后缀。 所以 -ization ( = -ize 加上
  2. -ation) 被映射到 -ize 等等。 注意在去除后缀之前必须确保
  3. m() > 0. */
  4. private final void step3() { if (k == 0) return;  switch (b[k-1])
  5. {
  6. case 'a': if (ends("ational")) { r("ate"); break; }
  7. if (ends("tional")) { r("tion"); break; }
  8. break;
  9. case 'c': if (ends("enci")) { r("ence"); break; }
  10. if (ends("anci")) { r("ance"); break; }
  11. break;
  12. case 'e': if (ends("izer")) { r("ize"); break; }
  13. break;
  14. case 'l': if (ends("bli")) { r("ble"); break; }
  15. if (ends("alli")) { r("al"); break; }
  16. if (ends("entli")) { r("ent"); break; }
  17. if (ends("eli")) { r("e"); break; }
  18. if (ends("ousli")) { r("ous"); break; }
  19. break;
  20. case 'o': if (ends("ization")) { r("ize"); break; }
  21. if (ends("ation")) { r("ate"); break; }
  22. if (ends("ator")) { r("ate"); break; }
  23. break;
  24. case 's': if (ends("alism")) { r("al"); break; }
  25. if (ends("iveness")) { r("ive"); break; }
  26. if (ends("fulness")) { r("ful"); break; }
  27. if (ends("ousness")) { r("ous"); break; }
  28. break;
  29. case 't': if (ends("aliti")) { r("al"); break; }
  30. if (ends("iviti")) { r("ive"); break; }
  31. if (ends("biliti")) { r("ble"); break; }
  32. break;
  33. case 'g': if (ends("logi")) { r("log"); break; }
  34. } }

第四步,处理-ic-,-full,-ness等等后缀。和步骤3有着类似的处理。

[java] view plain copy  print?
  1. private final void step4() { switch (b[k])
  2. {
  3. case 'e': if (ends("icate")) { r("ic"); break; }
  4. if (ends("ative")) { r(""); break; }
  5. if (ends("alize")) { r("al"); break; }
  6. break;
  7. case 'i': if (ends("iciti")) { r("ic"); break; }
  8. break;
  9. case 'l': if (ends("ical")) { r("ic"); break; }
  10. if (ends("ful")) { r(""); break; }
  11. break;
  12. case 's': if (ends("ness")) { r(""); break; }
  13. break;
  14. } }

第五步,在<c>vcvc<v>情形下,去除-ant,-ence等后缀。

[java] view plain copy  print?
  1. private final void step5()
  2. {   if (k == 0) return;  switch (b[k-1])
  3. {  case 'a': if (ends("al")) break; return;
  4. case 'c': if (ends("ance")) break;
  5. if (ends("ence")) break; return;
  6. case 'e': if (ends("er")) break; return;
  7. case 'i': if (ends("ic")) break; return;
  8. case 'l': if (ends("able")) break;
  9. if (ends("ible")) break; return;
  10. case 'n': if (ends("ant")) break;
  11. if (ends("ement")) break;
  12. if (ends("ment")) break;
  13. /* element etc. not stripped before the m */
  14. if (ends("ent")) break; return;
  15. case 'o': if (ends("ion") && j >= 0 && (b[j] == 's' || b[j] == 't')) break;
  16. /* j >= 0 fixes Bug 2 */
  17. if (ends("ou")) break; return;
  18. /* takes care of -ous */
  19. case 's': if (ends("ism")) break; return;
  20. case 't': if (ends("ate")) break;
  21. if (ends("iti")) break; return;
  22. case 'u': if (ends("ous")) break; return;
  23. case 'v': if (ends("ive")) break; return;
  24. case 'z': if (ends("ize")) break; return;
  25. default: return;
  26. }
  27. if (m() > 1) k = j;
  28. }

第六步,也就是最后一步,在m()>1的情况下,移除末尾的“e”。

[java] view plain copy  print?
  1. private final void step6()
  2. {  j = k;
  3. if (b[k] == 'e')
  4. {  int a = m();
  5. if (a > 1 || a == 1 && !cvc(k-1)) k--;
  6. }
  7. if (b[k] == 'l' && doublec(k) && m() > 1) k--;
  8. }

在了解了步骤之后,我们写一个stem()方法,来完成得到词干的工作。

[java] view plain copy  print?
  1. /** 通过调用add()方法来讲单词放入词干器数组b中
  2. * 可以通过下面的方法得到结果:
  3. * getResultLength()/getResultBuffer() or toString().
  4. */
  5. public void stem()
  6. {  k = i - 1;
  7. if (k > 1) { step1(); step2(); step3(); step4(); step5(); step6(); }
  8. i_end = k+1; i = 0;
  9. }

最后要提醒的就是,传入的单词必须是小写。关于Porter Stemmer的实现,就看到这里。如果是Java代码这么写,无可厚非(实际上也不是很美观)。对于Python来说,如果写成这样,实在是让人难以接受。以后的文章,将会实现符合Python习惯的写法。

需要测试数据这里是样本文件。而相应的输出文件在这里。更多内容请参考官方网站。

另外,波特词干算法有第二个版本,它的处理结果要比文中所介绍的算法准确度高,但是,相应地也就更复杂,消耗的时间也就更多。本文就不作解释,详细参考官方网站The Porter2 stemming algorithm。

Porter Algorithm ---------词干提取算法相关推荐

  1. [搜索]波特词干(Porter Streamming)提取算法详解(2)

     接[搜索]波特词干(Porter Streamming)提取算法详解(1), http://blog.csdn.net/zhanghaiyang9999/article/details/4162 ...

  2. [搜索]波特词干(Porter Streamming)提取算法详解(3)

     接上 [搜索]波特词干(Porter Streamming)提取算法详解(2) 下面分为5大步骤来使用前面提到的替换条件来进行词干提取. 左边是规则,右边是提取成功或者失败的例子(用小写字母表示 ...

  3. [搜索]波特词干(Porter Streamming)提取算法详解(1)

    英语词汇由两部分构成,词干和词缀,词缀又分前缀和后缀,这里的词干提取仅只去除后缀的操作. 波特词干提取算法的原文在这里 http://tartarus.org/~martin/PorterStemme ...

  4. (1)英文分词——波特词干提取算法

    英文分词相比中文分词要简单得多,可以根据空格和标点符号来分词,然后对每一个单词进行词干还原和词形还原,去掉停用词和非英文内容.词干还原算法最经典的就是波特算法(Porter Algorithm官网ht ...

  5. 词形变换和词干提取工具(英文)

    转载自: http://www.cnblogs.com/kaituorensheng/p/3437807.html 词形变换和词干提取工具(英文) 在信息检索和文本挖掘中,需要对一个词的不同形态进行归 ...

  6. java lucene词干提取_词形变换和词干提取工具(英文)

    在信息检索和文本挖掘中,需要对一个词的不同形态进行归并,即词形规范化,从而提高文本处理的效率.例如:词根run有不同的形式running.ran另外runner也和run有关.这里涉及到两个概念: 词 ...

  7. 自然语言处理——词性标注、词干提取、词形还原

    目录 词性标注 方法 工具 实例 词干提取和词形还原 算法 步骤 词性标注 一般而言,文本里的动词可能比较重要,而助词可能不太重要: 我今天真好看 我今天真好看啊 甚至有时候同一个词有着不同的意思: ...

  8. NLTK自带的词干提取器

    代码来自<Python自然语言处理>P116 (python2.7) appleyuchi@ubuntu:~/.virtualenvs/python2.7/bin$ python Pyth ...

  9. 中线提取算法_综述|线结构光中心提取算法研究发展

    摘 要: 线结构光扫描是三维重建领域的关键技术.光条纹中心提取算法是决定线结构光三维重建精度以及光条纹轮廓定位准确性的重要因素.本文详细阐述了光条纹中心提取算法的理论基础及发展历程,将现有算法分为三类 ...

最新文章

  1. 专家认为自动驾驶汽车需要很多年的五个原因
  2. 国外设计师眼中的原型工具Mockplus
  3. 敏感词库 php,敏感词过滤的php类库
  4. python代码规范化_最流行的Python代码规范
  5. CodeForces - 765D Artsem and Saunders(数学化简+构造+思维)
  6. C#中常用的几种读取XML文件的方法
  7. 国家特级数学教授李毓佩:我们欠孩子真正的数学阅读 !
  8. 淘宝网的软件质量属性分析
  9. SQL server 查询语句
  10. python基础教程菜鸟教程-Python 基础教程
  11. 3位格雷码的顺序编码_格雷码编码规则_格雷码有什么规律
  12. 计算机控制技术毕业论文题目,计算机控制方面论文选题 计算机控制论文题目怎样定...
  13. Mongodb索引及explain
  14. java pdf to word_java pdf转word 高效不失真
  15. LT-mapper,LT-SLAM代码运行与学习
  16. STM32 中断向量表的位置 、重定向
  17. 计算机电脑显示器都有多大的,电脑显示器买多大的合适
  18. Python发送微信消息(文字、图片、文件)给指定好友和微信群,零基础可看懂(附源码和教程)
  19. html静态网站基于游戏网站设计与实现共计10个页面 (仿地下城与勇士游戏网页)...
  20. WinCE平台USB摄像头驱动开发

热门文章

  1. java批量下载demo_Java批量入库Demo
  2. 关于各种PLMN的选择
  3. 未来是RFID物联网的世界
  4. 功能预测之Tax4Fun
  5. 杜洋单片机pcb百度云_[分享][下载]杜洋工作室的面包板入门单片机基础版和提高版完整下载...
  6. 物通博联·工业智能PLC物联网网关
  7. Revit学习之路01_Revit基础
  8. 以B2C物流的前世今生,看未来电商物流的决胜之道
  9. 机器学习 matlab工具箱,[matlab]机器学习及SVM工具箱学习笔记
  10. 【NLP】如何评价一个摘要是合适的