• 新闻组语料库
    • 把不要的东西丢掉!
    • 常用词和停词
    • 编写Python代码

    新闻组语料库

    我们下面要处理的数据集是新闻,这些新闻可以分为不同的新闻组,我们会构造一个分类器来判断某则新闻是属于哪个新闻组的:

    新闻组语料库 - 图1

    比如下面这则新闻是属于rec.motorcycles组的:

    新闻组语料库 - 图2

    注意到这则新闻中还有一些拼写错误(如accesories、ussually等),这对分类器是一个不小的挑战。

    这些数据集都来自 http://qwone.com/~jason/20Newsgroups/ (我们使用的是20news-bydate数据集),你也可以从 这里 获得。

    这个数据集包含18,846个文档,并将训练集(60%)和测试集放在了不同的目录中,每个子目录都是一个新闻组,目录中的文件即新闻文本。

    把不要的东西丢掉!

    比如我们要对下面这篇新闻做分类:

    新闻组语料库 - 图3

    让我们看看哪些单词是比较重要的:

    新闻组语料库 - 图4

    (helpful - 重要,not helpful - 不重要)

    如果我们将英语中最常用的200个单词剔除掉,这篇新闻就成了这样:

    新闻组语料库 - 图5

    去除掉这些单词后,新闻就只剩下一半大小了。而且,这些单词看上去并不会对分类结果产生影响。H.P. Luhn在他的论文中说“这些组成语法结构的单词是没有意义的,反而会产生很多噪音”。

    也就是说,将这些“噪音”单词去除后是会提升分类正确率的。我们将这些单词称为“停词”,有专门的停词表可供使用。去除这些词的理由是:

    1. 能够减少需要处理的数据量;
    2. 这些词的存在会对分类效果产生负面影响。

    常用词和停词

    虽然像the、a这种单词的确没有意义,但有些常用词如work、write、school等在某些场合下还是有作用的,如果将他们也列进停词表里可能会有问题。

    新闻组语料库 - 图6

    年轻人,那些常用词是不能随便丢弃的!

    因此在定制停词表时还是需要做些考虑的。比如要判别阿拉伯语文档是在哪个地区书写的,可以只看文章中最常出现的词(和上面的方式相反)。如果你有兴趣,可以到我的 个人网站 上看看这篇论文。

    而在分析聊天记录时,强奸犯会使用更多I、me、you这样的词汇,如果在分析前将这些单词去除了,效果就会变差。

    新闻组语料库 - 图7

    不要盲目地使用停词表!

    编写Python代码

    首先让我们实现朴素贝叶斯分类器的训练部分。训练集的格式是这样的:

    新闻组语料库 - 图8

    最上层的目录是训练集(20news-bydate-train),其下的子目录代表不同的新闻组(如alt.atheism),子目录中有多个文本文件,即新闻内容。测试集的目录结构也是相同的。因此,分类器的初始化代码要完成以下工作:

    1. 读取停词列表;
    2. 获取训练集中各目录(分类)的名称;
    3. 对于各个分类,调用train方法,统计单词出现的次数;
    4. 计算下面的公式:

    新闻组语料库 - 图9

    1. from __future__ import print_function
    2. import os, codecs, math
    3. class BayesText:
    4. def __init__(self, trainingdir, stopwordlist):
    5. """朴素贝叶斯分类器
    6. trainingdir 训练集目录,子目录是分类,子目录中包含若干文本
    7. stopwordlist 停词列表(一行一个)
    8. """
    9. self.vocabulary = {}
    10. self.prob = {}
    11. self.totals = {}
    12. self.stopwords = {}
    13. f = open(stopwordlist)
    14. for line in f:
    15. self.stopwords[line.strip()] = 1
    16. f.close()
    17. categories = os.listdir(trainingdir)
    18. # 将不是目录的元素过滤掉
    19. self.categories = [filename for filename in categories
    20. if os.path.isdir(trainingdir + filename)]
    21. print("Counting ...")
    22. for category in self.categories:
    23. print(' ' + category)
    24. (self.prob[category],
    25. self.totals[category]) = self.train(trainingdir, category)
    26. # 删除出现次数小于3次的单词
    27. toDelete = []
    28. for word in self.vocabulary:
    29. if self.vocabulary[word] < 3:
    30. # 遍历列表时不能删除元素,因此做一个标记
    31. toDelete.append(word)
    32. # 删除
    33. for word in toDelete:
    34. del self.vocabulary[word]
    35. # 计算概率
    36. vocabLength = len(self.vocabulary)
    37. print("Computing probabilities:")
    38. for category in self.categories:
    39. print(' ' + category)
    40. denominator = self.totals[category] + vocabLength
    41. for word in self.vocabulary:
    42. if word in self.prob[category]:
    43. count = self.prob[category][word]
    44. else:
    45. count = 1
    46. self.prob[category][word] = (float(count + 1)
    47. / denominator)
    48. print ("DONE TRAINING\n\n")
    49. def train(self, trainingdir, category):
    50. """计算分类下各单词出现的次数"""
    51. currentdir = trainingdir + category
    52. files = os.listdir(currentdir)
    53. counts = {}
    54. total = 0
    55. for file in files:
    56. #print(currentdir + '/' + file)
    57. f = codecs.open(currentdir + '/' + file, 'r', 'iso8859-1')
    58. for line in f:
    59. tokens = line.split()
    60. for token in tokens:
    61. # 删除标点符号,并将单词转换为小写
    62. token = token.strip('\'".,?:-')
    63. token = token.lower()
    64. if token != '' and not token in self.stopwords:
    65. self.vocabulary.setdefault(token, 0)
    66. self.vocabulary[token] += 1
    67. counts.setdefault(token, 0)
    68. counts[token] += 1
    69. total += 1
    70. f.close()
    71. return(counts, total)

    训练结果存储在一个名为prop的字典里,字典的键是分类,值是另一个字典——键是单词,值是概率。

    新闻组语料库 - 图10

    god这个词在rec.motorcycles新闻组中出现的概率是0.00013,而在soc.religion.christian新闻组中出现的概率是0.00424。

    训练阶段的另一个产物是分类列表:

    新闻组语料库 - 图11

    新闻组语料库 - 图12

    训练结束了,下面让我们开始进行文本分类吧。

    请尝试编写一个分类器,达成以下效果:

    新闻组语料库 - 图13

    新闻组语料库 - 图14

    1. def classify(self, filename):
    2. results = {}
    3. for category in self.categories:
    4. results[category] = 0
    5. f = codecs.open(filename, 'r', 'iso8859-1')
    6. for line in f:
    7. tokens = line.split()
    8. for token in tokens:
    9. #print(token)
    10. token = token.strip('\'".,?:-').lower()
    11. if token in self.vocabulary:
    12. for category in self.categories:
    13. if self.prob[category][token] == 0:
    14. print("%s %s" % (category, token))
    15. results[category] += math.log(
    16. self.prob[category][token])
    17. f.close()
    18. results = list(results.items())
    19. results.sort(key=lambda tuple: tuple[1], reverse = True)
    20. # 如果要调试,可以打印出整个列表。
    21. return results[0][0]

    最后我们编写一个函数对测试集中的所有文档进行分类,并计算准确率:

    1. def testCategory(self, directory, category):
    2. files = os.listdir(directory)
    3. total = 0
    4. correct = 0
    5. for file in files:
    6. total += 1
    7. result = self.classify(directory + file)
    8. if result == category:
    9. correct += 1
    10. return (correct, total)
    11. def test(self, testdir):
    12. """测试集的目录结构和训练集相同"""
    13. categories = os.listdir(testdir)
    14. # 过滤掉不是目录的元素
    15. categories = [filename for filename in categories if
    16. os.path.isdir(testdir + filename)]
    17. correct = 0
    18. total = 0
    19. for category in categories:
    20. print(".", end="")
    21. (catCorrect, catTotal) = self.testCategory(
    22. testdir + category + '/', category)
    23. correct += catCorrect
    24. total += catTotal
    25. print("\n\nAccuracy is %f%% (%i test instances)" %
    26. ((float(correct) / total) * 100, total))

    在不使用停词列表的情况下,这个分类器的效果是:

    新闻组语料库 - 图15

    新闻组语料库 - 图16

    准确率77.77%,看起来很不错。如果用了停词列表效果会如何呢?

    那让我们来测试一下吧!

    请自行到网络上查找一些停词列表,并填写以下表格:

    新闻组语料库 - 图17

    我找到了两个停词列表,分别是包含25个词 和174个词 的列表,结果如下:

    新闻组语料库 - 图18

    看来第二个停词列表能提升2%的效果,你的结果如何?