按照大纲要求,考研英语需要识忆6000+个词汇,其中包含一批比较生僻的词汇。但大纲要求的单词不可能全部出现在试卷中,甚至有的可能根本就没出现过吧?如果真是这样,计算出频率较高的词汇,基础较差的孩子只需把握重点,有的放矢,应该能省不少功夫。其实算出来发现不用背6000+的,需要学的连2000都不到!

介绍下步骤,先清洗掉一些你从来都不会去读的东西,比如标题,以及一些标点符号和数字等;然后删去没必要再学的单词,比如’a’,’the’;接下来还得去除词性变化,比如把’congrates’还原到’congrate’;最后再计算词频。简而言之就是大致经过四个阶段:数据清洗$\to$去除停止词$\to$词干化$\to$计算词频。

(一) 数据清洗

考研英语共分四部分:“section I Use of English”、“section II Reading Comprehension”、“section III Translation”和“section IV Writing”,这些标题以及其他语句如:“Read the following text. Choose the best word(s) for each numbered blank and mark A, B, C or D on ANSWER SHEET 1. (10 points)”在每年的试卷中都会出现,因此在统计词频之前应去除这些语句的干扰。

此外文档中还存在除单词以外的其他干扰:

  • 每部分的标题号:Part A, B, C, D;
  • 阅读的题号:Text 1, 2, 3, 4;
  • 数字包括题号等;
  • 选项的标号:[A]到[G];
  • 作文部分

选取2003—2013年的真题进行计算,对数据手工去除作文部分后,运用正则表达式将文档中的其他干扰去除。正则表达式是一种操作字符串的逻辑公式,可以通过它匹配到符合某个语法规则的字符,从而获取我们想要的特定部分。

text = readLines("2003-2013考研英语真题.txt")

## 清洗数据
require(stringr)
text = str_trim(text)  #去除字符串两端的空格

# 去除以‘section’;‘direction’;'reading the
# following';'part','text'开头的段落
text.clean = grep("^section|^direction|read the following|part|text", text, 
    perl = T, value = T, invert = T, ignore.case = T)

# 去除选项[A]-[G]和字母以外的字符变为空格
text.clean = gsub("\\[[A-G]\\]|[^a-zA-Z]", " ", text.clean, perl = T, ignore.case = T)
text.clean = paste(text.clean, collapse = " ")  #连接文档

# 去除多余的空格和tab符、换行符等
text.clean = gsub(" +|\t|\n|\r", " ", text.clean)

(二) 停止词和词干化

停止词

完成第一次清洗后,文档还中包含众多“the”、“a”、“of”等词,这些都是最最简单而又最最常用的单词,不足以再花时间复习一下,因此需要第二次清洗,去除简单词汇。在网络搜索中有stop word即停止词的概念,很多词汇不可避免的要用到但又无实际意义或者非常普遍,那么在搜索排名时就不应将这些词的频率纳入算法计算的范围内。

在各个领域停止词的定义不尽相同,在这里采用新东方考研英语单词作为字典,其中包含3600左右的考研常用单词(已剔除简单单词),故只需从文档中提取字典中的单词即可。

词干化

另一个问题就是:在英语中一个单词有多种形式,如like可以有likes,liking,liked等形式,其含义都是相同的,如《神雕侠侣》中的过儿和杨过、姑姑和小龙女都是指同一个人,所以在计算词频之前,需要再次对数据进行加工处理将其还原,这个过程称为词干化。SnowballC包提供了词干化功能,例:

require(SnowballC)
wordStem(c("liked", "likes", "liking", "confuse", "combine"))
[1] "like"   "like"   "like"   "confus" "combin"

从例子中看出这个算法并不完美,对于“confuse”,“combine”以“e”结尾的单词强制去除“e”,效果并不理想。

基于以上两个问题,如果直接从文档中提取字典中的词汇会忽略词汇的众多变化,将文档词干化后再与字典匹配会由于词干化算法的缺陷出现较大失误。

不过如果两个词词干化后是一样的,那么这两个词有很大可能是同一个词的不同形式。因此一个较好的做法是将文档和字典同时词干化后再进行匹配统计,即将文档词干化后从中找到与字典中的词干相同的词汇。

# 载入字典
dictionary = scan("新东方考研单词.txt", what = "character")
# 清洗字典中的不相干字符‘List 1-50’ (共50章)
dictionary = dictionary[grep("List|\\d", dictionary, perl = T, invert = T)]
# 将字典词干化
dictionary.stem = wordStem(dictionary)
# 计算词干重复的个数
(repeat_stem = length(dictionary) - length(unique(dictionary.stem)))
[1] 247

通过计算,对字典词干化后共有247个词汇词干化后与其他词汇相同,词干相同的前30个词是:

# 来看哪些词词干化之后是相同的 词干化之后各词的频率
dic.stem.fre = table(dictionary.stem)
# 查找每个词干的频率
dic.fre = mapply(function(x) dic.stem.fre[x], dictionary.stem)
# 构造数据框并按频率排序
dic = data.frame(dictionary, dictionary.stem, dic.fre)
# 按频率和字母排序
dic.order = dic[order(dic$dic.fre, dic$dictionary.stem, decreasing = T), ]
# 查看前30个词干相同的词
print(head(dic.order, 30), row.names = F)
     dictionary dictionary.stem dic.fre
       organize           organ       5
        organic           organ       5
   organization           organ       5
          organ           organ       5
       organism           organ       5
       generate           gener       4
       generous           gener       4
     generalize           gener       4
      generator           gener       4
       critical          critic       4
      criticize          critic       4
         critic          critic       4
      criticism          critic       4
  correspondent      correspond       4
     correspond      correspond       4
 correspondence      correspond       4
  corresponding      correspond       4
      tolerance           toler       3
       tolerate           toler       3
       tolerate           toler       3
    responsible         respons       3
       response         respons       3
 responsibility         respons       3
         resist          resist       3
      resistant          resist       3
     resistance          resist       3
     prosperity         prosper       3
     prosperous         prosper       3
        prosper         prosper       3
      precedent          preced       3

最多有5个单词的词干相同,且词干相同的词汇都具有相同的词根因而具有相似的意义,如“organ”和“organic”均有器官的意思只是词性不同,而“organism”则由器官引申到有机体;“critical”,“criticize”均有批评挑剔的意思,只是词性不同,“critic”则是批评家的意思。其他不一一解释,可以看出词干相同的词汇,其含义大致相近。事实上这也是一种词汇记忆的方法 —词根记忆法,知道“critic”是批评家的意思,看到“critical”便可联想到批评。

因此同时将文档和字典词干化后再匹配,将每个词的词频转换为计算其词干出现的频率, 由于词与词干的意义相似,误差并不大,是可行的。

(三) 词干的词频

如前文所述将文档词干化,首先转换格式变成一个词向量,然后汇总统计,最后词干化,这样可以保留每个词干所对应的词汇。

words = unlist(strsplit(text.clean, " "))  # 分割文档
words.count = table(tolower(words))  # 转换为小写并计数
words.data = data.frame(words.count)  # 转换为数据框
colnames(words.data) = c("word", "fre")  # 更改列名
words.data$word = as.character(words.data$word)
words.data$stem = wordStem(words.data$word)  # 词干化

接下来从中取出与字典相对应的词干,即剔除停止词,并按词干的频率排序。

# 将词干化后的文档对应到词干化后的字典
dic.stem = data.frame(stem = unique(dic.order$dictionary.stem))
words.map = merge(dic.stem, words.data)
words.map = words.map[order(words.map$fre, decreasing = T), ]  # 根据词频排序
# 看看一共出现过多少个词汇
(appear = length(unique(words.map$stem)))
[1] 1600
# 看看有多少词汇未曾出现过
(non_app = nrow(dic.stem) - appear)
[1] 1769

最后通过计算得到出现过的词汇个数,结果有点令人惊讶,新东方词汇共3619词,词干化后有3370个词干,而近十年的考研试卷中出现过的词干仅有1600个,未出现过的有1770个,这意味这有一半以上的词汇都未曾出现!看来《新东方考研英语单词》只需要背会一半就够了!

那么,来看一下需要背哪一半。出现频率最高的30个词干是:

print(head(words.map, 30), row.names = F)
       stem         word fre
         be           be 241
         or           or 127
  paragraph    paragraph  74
     social       social  72
        out          out  54
     author       author  45
       like         like  39
     accord    according  38
     public       public  38
      state        state  37
       time         time  37
      feder      federal  35
       data         data  33
       educ    education  32
     inform  information  31
      human        human  30
     follow    following  29
   influenc    influence  28
      power        power  28
   intellig intelligence  26
      polit    political  26
      state       states  25
         be        being  24
     govern   government  24
     stress       stress  24
 intellectu intellectual  23
     nation     national  23
     consum    consumers  22
     differ    different  21
     econom     economic  21

结果有点意外,其中竟然包含了’be’,’like’,’time’,’data’,’human’,’power’,’find’这样的简单词汇,直觉上应该属于停止词。

验证一下它们是否真的在字典里:

for (words in c("be", "like", "time", "data", "human", "power", "find")) {
    print(dic.order[dic.order$dictionary.stem == words, ])
    cat("\n")
}
     dictionary dictionary.stem dic.fre
1069      being              be       1

     dictionary dictionary.stem dic.fre
3331     likely            like       1

     dictionary dictionary.stem dic.fre
2373     timely            time       1

    dictionary dictionary.stem dic.fre
256       data            data       1

     dictionary dictionary.stem dic.fre
1489   humanity           human       1

    dictionary dictionary.stem dic.fre
898   powerful           power       1

     dictionary dictionary.stem dic.fre
3277    finding            find       1

通过验证,“data”确实出现在字典中,而“be”则是“being”的词干,“find”是“finding”的词干,可见词干化后一些停止词趁虚而入了。果然英语文化也是博大精深,在词汇变化和一次多义方面令人很难琢磨(准确的说是令机器很难琢磨,或者令我难以琢磨,让我还没有找到一个有效的办法完全去除这些麻烦的停止词)。

不过到这里倒不必吹毛求疵的人工去除停止词,就当是在提醒我们这些简单词的变化形式,比如“finding”不仅是“find”的现代分词,还有’发现物,研究结果’的意思。

(四) 结果输出

将词干出现的频率汇总并由高到低排序,同时显示出词干化前的词汇及其相应的出现频率,写入一个文件方便查看。

# 按词干汇总
stem.count = aggregate(fre ~ stem, data = words.map, sum)
# 排序
stem.count = stem.count[order(stem.count$fre, decreasing = T), ]
# 查看频数最大的十个词干
head(stem.count, 10)
          stem fre
164         be 267
1012        or 127
1042 paragraph  82
1356    social  73
733      human  65
1389     state  65
880       like  59
143     author  56
1020       out  54
1177    public  50
# 写出到文件 
for (i in stem.count$stem) {
  write.table(stem.count[stem.count$stem == i, ], 'words_fre.txt', 
              append=T, row.names=F, col.names=F) 
  write.table(words.data[words.data$stem==i, ], 'words_fre.txt', 
              append=T, row.names=F) 
  write('\n', 'words_fre.txt', append=T) 
}

已经写出来的文件words_fre.txt。

最后,制作一个词云展示前100个高频词干(不是单词):

require(wordcloud)
## 'be'这个单词出现次数远高于其他词汇,其他绘制出的单词都太小,故人工剔除
being_fre = stem.count$fre[1] - words.map$fre[words.map$word == "be"]
wordcloud(stem.count$stem[1:100], 
          c(being_fre, stem.count$fre[2:100]), 
          c(6, 0.5))