基于 BOW 的随机森林分类

简介

​ 在自然语言处理领域的早期,计算机无法理解语言的含义,研究人员提出使用统计方法来处理文本数据,通过统计词语的数量和频率,简化了文本处理的复杂性,使得机器可以通过数值化的方式处理文本数据。

词袋模型(Bag of Words)是一种流行且简单的特征提取方法,常用于将非结构化的文本数据转换为机器学习算法可以处理的数值特征。在自然语言处理中,机器学习算法无法直接处理原始的文本数据,因此需要将文本数据转换成数字形式,以便进行分析和建模。词袋模型忽略了文本中词语的顺序和语法结构,只关注每个文本中词语的出现与否,生成一个固定长度的向量表示。

​ 得到了文本的特征,我们利用机器学习分类器对语句意图进行意图分类。分类器有很多种,比如朴素贝叶斯决策树支持向量机(support vector machine, SVM)、随机森林random forest)、卷积神经网络convolutional neural network, CNN)等。

目标任务

【数据集】

​ 任务用到的数据集来自文献Attention-Based Recurrent Neural Network Models for Joint Intent Detection and Slot Filling(Liu and Lane, INTERSPEECH 2016) 文献原文

​ 语料库包括训练集和测试集,为该网址中的 atis.train.w-intent.iobatis.test.w-intent.iob 文件 语料库下载地址

【目标】

进行数据理解,特征表达,根据训练出的模型,判断测试集中每个句子的意图类型(共 18 种意图类别)。

技术介绍

词袋模型

词袋模型(Bag of Words)是常见的文本表示方法,忽略了单词之间的顺序和语法结构,原理是将文本表示为各个单词的计数或出现频率,进而把一个文本转化为向量表示,是比较简单直白的一种特征提取方法。

集成方法

集成方法的目标是把多个使用给定学习算法构建的基估计器的预测结果结合起来,提高整体性能,降低模型的方差和偏差,并提高对未知数据的泛化能力,从而获得比单个估计器更好的鲁棒性。

Bagging

Bagging是一种集成学习方法,通过组合多个基分类器的预测结果来提高整体分类性能。基本原理是通过自助采样(bootstrap sampling)来构建多个训练集,每个训练集都是通过有放回地从原始训练集中随机选取样本得到的。然后,针对每个训练集独立地训练出一个基分类器,最终的预测结果是所有基分类器的预测结果的投票或平均。

随机森林

随机森林(Random Forest),是一种由多个决策树组成的集成学习算法。是Bagging中的一种算法,所以也是有放回抽样,决策树之间互不干扰。不同的是,随机森林将每次未抽到的数据作为测试集来验证模型。相比单个决策树,随机深林可以通过投票或平均来综合多个决策树的预测结果,进而提高整体性能。

问题分析

数据集格式

语料库为.iob文件,格式如下:

1
2
3
BOS 句子 EOS ...... intent 标签 

BOS 句子 EOS ...... intent 标签

语料库为.iob文件,具体内容如下:

1
2
3
BOS what's the airport at orlando EOS O O O O O B-city_name atis_airport 

BOS pittsburgh to denver EOS O B-fromloc.city_name O B-toloc.city_name atis_flight

​ 其中BOS、EOS为语句起始、终止标识。标签部分中O为占位符,标签为槽填充信息,代表对应语句中单词的含义。观察发现,需要特别注意的是标签中的最后一个单词为语句的意图信息。

​ 解决问题时,首先考虑如何处理清洗数据。

​ 解决方法:将语料库逐行读取,分离出句子部分存放到sentences列表中,标签部分分理出最后一个单词存放到labels列表中。

特征提取

分词

​ 经过前面的处理,分离出的sentences中存储着语句,但是字符串形式,计算机无法进行处理。所以要将语句编码成向量形式,利用矩阵运算进行处理。

​ 在 NLP 中,一个句子通常是被看成一系列词的组合,利用nltk库中的word_tokenize将语句按照空格进行分词存储在tokens中,便于计算机处理。

停用词过滤

​ 停用词过滤是一种在文本预处理中常用的技术,用于去除那些对文本分析没有意义或没有价值的常见词语,例如 “a”、“the”、“in”、“is” 等。这些词语通常在文本中频繁出现,但对于理解文本的含义并不重要。

nltk库中的stop_words(停用词列表)提供了常见的停用词,遍历tokens筛选出不在停用词列表中的单词,得到经过停用词过滤后的单词列表filtered_tokens

特征表示

特征词典

​ 经过停用词过滤,剩下的词汇作为特征,统计各词汇出现的次数,将单词作为key,次数作为value存储在vocabulary词典当中。

语句向量化

​ 词袋模型(Bag of Words)是常见的文本表示方法,原理是将文本表示为各个单词的计数或出现频率,而忽略了单词之间的顺序和语法结构。

​ 具体方法是先定义一个维度和vocabulary词典词汇个数相同的零向量vector,然后遍历sentences,如果单词在vocabulary中,对应的vector分量+1,最终得到一个vectors向量列表,其中每一个向量的每个分量对应词典中词出现在该句的次数。

分类器的训练

​ 本问题采用随机森林(Random Forest)进行意图分类。

一个模型中往往有着许多超参数,我们往往在寻找最优的超参数组合,所以模型的调优和评估至关重要,关乎着模型的效果和效率。

  1. 网格搜索自动调参

​ 常用的调参方法有网格搜索自动调参,这是一种通过遍历给定参数组合的方法来确定机器学习模型最佳超参数的技术。它可以用于调整模型的参数,以获得更好的性能。具体方法是:定义param_grid参数网格字典,其中存放着重要超参数与取值范围,接着利用sklearn.model_selection中的GridSearchCVd函数在训练集上进行网格搜索调参,最后可以输出查看最佳的模型参数与最佳的模型得分等等。由于此次要求利用KFold进行模型验证,故不用此方法进行模型调优。

  1. KFold交叉验证

​ k-折交叉验证将训练集划分为 k 个较小的集合,每一个 k 折都会遵循下面的过程:

  • k-1 份训练集子集作为 training data (训练集)训练模型

  • 将剩余的1份训练集子集用于模型验证(相当于测试集)计算模型的性能指标,例如准确率等。

​ k-折交叉验证得出的性能指标是循环计算中每个值的平均值。


实验

实验具体流程

  1. 使用Scikit-Learn机器学习库中的随机森林分类器RandomForestClassifier()
  2. 接着将训练集train_x(语句向量)、train_y(语句意图)重新划分为KFold训练集和测试集。
  3. 训练随机森林模型。
  4. 计算并输出模型k折交叉验证的准确度。
  5. 用训练好的模型对测试集数据中的语句进行意图预测分类。
  6. 输出分类结果。

代码

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import nltk
import numpy as np
from nltk.corpus import stopwords
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import KFold

# 加载训练集数据
with open("./data/atis.train.w-intent.txt", 'r', encoding='utf-8') as file:
train_data = file.readlines()

# 加载测试集数据
with open("./data/atis.test.w-intent.txt", 'r', encoding='utf-8') as file:
test_data = file.readlines()

# nltk.download()
# 下载停用词列表
# nltk.download('stopwords')
# 停用词列表
stop_words = set(stopwords.words('english'))
# 停用词列表查看
# print(stopwords.raw('english'))

train_sentences = [] # 训练集语句
train_labels = [] # 训练集意图

test_sentences = [] # 测试集语句
test_labels = [] # 测试集意图

# 读取每一行数据,分离出语句与意图(训练集)
for line in train_data:
if line:
parts = line.split('\t') # 去除制表符
sentence = parts[0].split()[1:-1] # 提取句子部分,去除开头的BOS和结尾的EOS
label = parts[1].split()[1:] # 提取意图标签部分
train_sentences.append(' '.join(sentence))
train_labels.append(' '.join(label[-1]))

# 读取每一行数据,分离出语句与意图(测试集)
for line in test_data:
if line:
parts = line.split('\t') # 去除制表符
sentence = parts[0].split()[1:-1] # 提取句子部分,去除开头的BOS和结尾的EOS
label = parts[1].split()[1:] # 提取意图标签部分
test_sentences.append(' '.join(sentence))
test_labels.append(' '.join(label[-1]))

# 分词和过滤停用词
features = {} # 语句的特征向量
for sentence in train_sentences:
tokens = nltk.word_tokenize(sentence) # 分词
filtered_tokens = [token for token in tokens if token.lower() not in stop_words] # 去除停用词
for token in filtered_tokens:
features[token] = features.get(token, 0) + 1 # 将单词映射到词典中,value代表单词出现次数

# 创建词汇表
vocabulary = list(features.keys()) # 将过滤后词典的键作为词汇表中的词
# print(vocabulary)

# 向量化每个句子(训练集)
train_vectors = []
for sentence in train_sentences:
vector = np.zeros(len(vocabulary))
for word in sentence.split():
if word in vocabulary:
vector[vocabulary.index(word)] += 1 # 将语句表示成特征向量,每一维度代表词典中该词出现的次数
train_vectors.append(vector)

# 向量化每个句子(测试集)
test_vectors = []
for sentence in test_sentences:
vector = np.zeros(len(vocabulary))
for word in sentence.split():
if word in vocabulary:
vector[vocabulary.index(word)] += 1 # 将语句表示成特征向量,每一维度代表词典中该词出现的次数
test_vectors.append(vector)

# 转化为numpy数组
# 训练集
train_x = np.array(train_vectors) # 语句向量
train_y = np.array(train_labels) # 语句意图
# 测试集
test_x = np.array(test_vectors) # 语句向量
test_y = np.array(test_labels) # 语句意图

# 创建随机森林分类器
clf_rf = RandomForestClassifier() # n_estimators=250, random_state=42

# KFold交叉验证
kf = KFold(n_splits=10, shuffle=True, random_state=41)
accuracies = [] # 存储每次交叉验证的准确度

for train, test in kf.split(train_x):
# 划分训练集和验证集
train_x_KFold, test_x_KFold = [train_x[i] for i in train], [train_x[i] for i in test]
train_y_KFold, test_y_KFold = [train_y[i] for i in train], [train_y[i] for i in test]

# 训练模型
clf_rf.fit(train_x_KFold, train_y_KFold)

# 在验证集上预测
pre_y = clf_rf.predict(test_x_KFold)

# 计算准确度
accuracy = accuracy_score(test_y_KFold, pre_y)
accuracies.append(accuracy)

# 输出交叉验证的准确度
print("KFold交叉验证准确度:", sum(accuracies) / len(accuracies))

# 在测试集上进行预测
pre_labels = clf_rf.predict(test_x)
# 将预测结果进行输出
for sentence, label, pre_label in zip(test_sentences, train_labels, pre_labels):
print("sentence:[", sentence, "] ",
"(true Label:", label, ") ",
"predicted Label:", pre_label)

程序运行结果

输出格式如下:

1
sentence:[ ...... ] ( true Label: ...... ) predicted Label: ......

训练结果

​ 本次训练的随机森林意图分类模型,KFold交叉验证准确度为:94.737%,接近95%,取得较高的分类准度,说明对于意图分类能够做到较好效果。

遇到的问题

​ 本次任务还尝试了网格搜索和cross_val_score(另一种交叉验证),其中在切分数据集时,显示如下报错信息:

ValueError: The least populated class in y has only 1 member, which is too few. The minimum number of groups for any class cannot be less than 2.

​ 意思是最少类别的成员数量只有1个,也就是出现了类别不平衡问题,分类时有些种类过少。