波特词干算法 - 残阳似血的博客

波特词干算法 - 残阳似血的博客

波特词干算法

位于分类 自然语言处理

在英语中,一个单词常常是另一个单词的“变种”,如: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类。

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

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

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

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

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

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

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

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

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

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

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

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* step3() 将双后缀的单词映射为单后缀。 所以 -ization ( = -ize 加上
    -ation) 被映射到 -ize 等等。 注意在去除后缀之前必须确保
    m() > 0. */
private final void step3() { 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有着类似的处理。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private final void step4() { 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等后缀。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private final void step5()
{   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”。

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

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

?
1
2
3
4
5
6
7
8
9
/** 通过调用add()方法来讲单词放入词干器数组b中
   * 可以通过下面的方法得到结果:
   * getResultLength()/getResultBuffer() or toString().
   */
public void stem()
{  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。

posted on 2013-01-15 23:38  lexus 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/lexus/archive/2013/01/15/2862062.html

波特词干算法 - 残阳似血的博客相关推荐

  1. 动态规划求编辑距离 - 残阳似血的博客

    动态规划求编辑距离 - 残阳似血的博客 动态规划求编辑距离 - 残阳似血的博客 动态规划求编辑距离 位于分类 自然语言处理 这两天在写一个简单的单词拼写检查器(Spell checker),本来求编辑 ...

  2. 结构之法算法之道CSDN博客-第一期全部博文集锦[CHM 文件下载]

    结构之法算法之道CSDN博客- 博文集锦第一期CHM 文件 首先恭喜你,你得到了一份非常宝贵的资源. 此份文件是截止到2011年2月27日,本博客.结构之法算法之道博客内的第一期全部内容的集锦,同时, ...

  3. 算法java语言描述_java语言描述数据结构与算法崔笑颜的博客

    java语言描述数据结构与算法崔笑颜的博客 冒泡排序 插入排序 选择排序 希尔排序 快速排序 归并排序 二分查找package com.demo.test; import java.util.Arra ...

  4. 百度算法不断更新,博客做SEO还有效果吗

    几年前,博客群发软件可以让SEO外链员轻松了不少,自动或半自动化操作确实很方便.然而,随着百度算法的更新,这些博客群发软件产出的外链变得毫无价值,被称为了垃圾外链,不仅对网站没有丝毫益处,反而可能会拖 ...

  5. 博弈论算法Java,博弈论-java-51CTO博客

    有一种很有意思的游戏,就是有物体若干堆,可以是火柴棍或是围棋子等等均可.两个人轮流从堆中取物体若干,规定最后取光物体者取胜.这是我国民间很古老的一个游戏,别看这游戏极其简单,却蕴含着深刻的数学原理.下 ...

  6. 支持向量机python代码_Python机器学习SVM支持向量机算法理论 | kTWO-个人博客

    PS:这篇文章讲的是SVM的算法的基础理论知识,因为博主也是刚学习没多久,对SVM的数学思想了解的不是很深,所以这篇文章就简单介绍一下什么是SVM以及SVM是怎么工作的. 1.什么是支持向量机(SVM ...

  7. 这是一名既能打比赛,又会发论文JD AI实验室的算法工程师,CSDN博客专家

    今天小编给大家推荐一位AI学习者:yuquanle,硕士刚毕业的他目前是京东AI lab一名算法工程师.硕士期间曾在IJCAI.TALSP.PAKDD.ICANN.ICTAI.HPCC.CCKS等会议 ...

  8. Prime算法和Krustal算法(转自博客园华山大师兄)

     Prim算法 1.概览 普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树.意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex ( ...

  9. 波特词干算法(英文分词)

    原文链接:https://blog.csdn.net/shuangshuang37278752/article/details/9314131

最新文章

  1. Database Vault注冊
  2. vue.js ui_UI / UX开发:考虑Vue.js
  3. java开机自启动 Linux,java项目jar包开机自启(WINDOWS,Linux)
  4. vsftpd 安装配置
  5. PLC与C语言的本质区别,一文告诉你PLC与计算机的本质区别在哪里!
  6. nodejs linux复制文本,Nodejs 复制文件/文件夹的方法
  7. Java学习(四)异常
  8. vscode使用:tab键插入空格而不是tab
  9. python求解括号匹配的相关问题
  10. Rust: Atom中cargo build error
  11. springboot快速入门【小白也能看懂】
  12. C/C++结构体语法总结
  13. 推荐项目| 微信小程序富文本解析组件-wxParse
  14. 吃鸡ios和android灵敏度,吃鸡手游pc版怎么调灵敏度参数教程 | 手游网游页游攻略大全...
  15. UG NX 12 取消选择对象
  16. IT战略规划与企业架构
  17. 分解gif动图如何操作?手把手教你动图分解方法
  18. 直通车测图需要满足哪些数据?
  19. Pvr_UnitySDKAPI
  20. 计算机无纸化考试合卷答题笔记卡,高级会计师无纸化考试攻略都在这 第一次考也不用慌...

热门文章

  1. 【示波器专题】应用练习
  2. 计算机应用专业毕业证好拿吗,好不容易自考全过了,可以直接拿毕业证书吗?...
  3. 由一道acm题目所想到
  4. IMS高压发生器维修易姆斯电源维修XRG40/200/XRG800
  5. git查看本地和远程分支及关联情况
  6. 利用BS模型计算欧式看涨期权价格——基于中国沪深300ETF看涨期权_20200524_
  7. MySQL数据表--创建多表、以及多表关系(添加外键)
  8. 窗口置顶工具v2.5.1
  9. 力士乐INDRAMAT伺服驱动器维修DKC01.1-040-7-FW注意事项
  10. andorid demo