当你掌握了机器学习和深度学习一些基本知识后,实现一些相关代码是非常必要的。本文提供了一个实战任务和代码实现:在给定的20类新闻文本数据集下,分别使用朴素贝叶斯、K近邻、神经网络进行文本分类,使用5次交叉验证,并对分类结果进行分析讨论。如果你能跟着实现文章中的代码,相信你的代码能力一定能有很大提高。(有些模块使用sklearn可以更简洁地解决,但是我尽量不使用sklearn, 因为那样对代码能力帮助较小)
下面的介绍包含文字叙述和部分代码,完整代码先从这里下载。代码地址
数据集包含了 20个 新闻群组, 总数 19997 个文档。下载网页
数据集中共有19997篇文档,对每一篇文档,依次进行如下步骤:
# 完整代码参考DataSet.py
bigString = f.read()
# 使用正则表达式匹配出非字母、非数字
token_list = re.compile(r'\b[a-zA-Z]+\b', re.I).findall(bigString)
# 去除一些太短的字符
token_list = [tok.lower() for tok in token_list if len(tok) > 1]
# 提取词干
porter = nltk.PorterStemmer()
token_list = [porter.stem(t) for t in token_list]
# 去除停用词
stop_words = set(stop_words1)
token_list = [tok for tok in token_list if tok not in stop_words]
# 提取名词等
pos_tags = nltk.pos_tag(token_list)
token_list = [word for word, pos in pos_tags if pos in tags]
Doclist.append(token_list)
ClassVec.append(i)
如果将上述19997篇文档的词表取交集,将得到长度为8w+的词汇表,这对后续的处理带来困难。因此,我还进行以下两个步骤:
# 完整代码参考DataSet.py
def find_common_highFreq(DocList, ClassVec):
n_class = len(set(ClassVec))
HighFreq_set = set()
for i in tqdm(range(n_class)):
p_vec = []
class_index = list(np.where(ClassVec == i))[0]
for id in class_index:
p_vec += DocList[id]
freq = Counter(p_vec)
# f = open(f'HIgh_Freq.txt', "a")
# 按值从大到小排序
freq = sorted(freq.items(), key=lambda x: x[1], reverse=True)
hign_freq_i = []
for idx in range(len(freq[:200])):
key, value = freq[idx]
hign_freq_i.append(key)
if i == 0:
HighFreq_set = set(hign_freq_i)
else:
HighFreq_set = HighFreq_set & set(hign_freq_i)
with open('Data/High_Freq.txt', 'w') as f:
for word in HighFreq_set:
f.write(f'{word} ')
f.write('\n')
return HighFreq_set
def find_low_freq(DocList, ClassVec):
n_class = len(set(ClassVec))
LowFreq_set = set()
for i in tqdm(range(n_class)):
p_vec = []
class_index = list(np.where(ClassVec == i))[0]
for id in class_index:
p_vec += DocList[id]
freq = Counter(p_vec)
freq = sorted(freq.items(), key=lambda x: x[1], reverse=False) # 按值从小到大排序
low_freq_i = []
for idx in range(len(freq)):
key, value = freq[idx]
if value < 3:
low_freq_i.append(key)
if i == 0:
LowFreq_set = set(low_freq_i)
else:
LowFreq_set = LowFreq_set | set(low_freq_i)
# print(LowFreq_set)
with open('Data/LowFreq_words.txt', 'w', encoding='utf-8') as f:
for line in list(LowFreq_set):
f.write(f'{line}\n')
return LowFreq_set
对每一篇新闻处理后得到的分词列表,使用python的Counter工具统计其每个词的词频,使用词频权重转化成维度为15306的特征向量,特征向量的每一维是该词出现的频次。最后得到19997个特征向量
将19997个15306维特征向量拼接成特征矩阵(19997*15306, 并保存
# 完整代码参考DataSet.py
def Doc2Mat(VocabList, DocList):
M = len(DocList)
D = len(VocabList)
P_mat = np.zeros((M, D))
for i in tqdm(range(M)):
freq = Counter(DocList[i])
for key, value in freq.items():
if key in VocabList:
P_mat[i][VocabList.index(key)] += value
return P_mat
np.save('Data/DataMat.npy', DataMat)
np.save('Data/ClassVec.npy', ClassVec)
np.save('Data/VocabList.npy', VocabList)
tips:注意不要每一次跑模型的时候都运行一遍上面的代码,这是没有必要的,上述代码运行保存成功后以后只需要加载处理后的数据就行。
# 完整代码参考main.py
X = np.load('Data/DataMat.npy', allow_pickle=True)
y = np.load('Data/ClassVec.npy', allow_pickle=True)
VocabList = list(np.load('Data/VocabList.npy', allow_pickle=True))
# 完整代码参考models/Naive_Bayes.py
def train(self, TrainMat, Label):
"""
:param TrainMat:[M, D]
:param Label:[M,1]
:return: P_mat:[n_class,D], P_class:[n_class,1]
"""
M, D = TrainMat.shape
n_class = len(set(list(Label)))
P_class = np.zeros(n_class)
for i in range(n_class):
idx = list(np.where(Label == i))[0]
P_class[i] = len(idx) / M
P_mat = np.zeros((n_class, D))
for i in range(n_class):
idx = list(np.where(Label == i))[0]
freq_vec = TrainMat[idx].sum(axis=0) # 该类的词汇频次求和
freq_vec += 1 # Laplace smooth
num_word = np.sum(freq_vec)
P_mat[i] = np.log(freq_vec / num_word)
return P_mat, P_class
# 完整代码参考model/Knn.py
def __init__(self, K):
self.K = K
self.model = KNeighborsClassifier(n_neighbors=K, metric='euclidean')
def train(self, Train_mat, Train_ClassVec):
self.model.fit(Train_mat, Train_ClassVec) # 训练
return Train_mat, Train_ClassVec
def test(self, Test_mat, gt_label, train_mat, train_label):
# pred_label = self.model.predict(Test_mat) # 预测
_, indices = self.model.kneighbors(Test_mat) # [M, K]
pred_label = self.vote(indices, train_label)
precision, recall, _, _ = precision_recall_fscore_support(gt_label,
pred_label)
diff = pred_label - gt_label
idx = list(np.where(diff == 0))[0]
accuracy = len(idx) / len(gt_label)
return accuracy, recall
网络结构: 使用最简单的MLP
{fc(15306,256), sigmoid, fc(256,20), softmax}
其它参数: 使用指数衰减学习率,初值5,gamma=0.98, 每20个epoch更新,训练1000个epoch,使用SGD优化器。
(注:如果没有配cuda,可以将代码中出现.cuda(), to(‘cuda’) 删去 )
# 完整代码参考model/MLPs.py
def train(self, X_train, gt_label):
self.net.train()
self.net.cuda()
X_mean = np.mean(X_train, axis=1, keepdims=True)
X_std = np.std(X_train, axis=1, keepdims=True)
X_train = (X_train - X_mean) / X_std
input = torch.from_numpy(X_train)[None, :, :].permute(0, 2, 1).to(torch.float32)
input = input.to('cuda')
for epoch in tqdm(range(1000)):
# 归一化
# in your training loop:
self.optimizer.zero_grad() # zero the gradient buffers
pred = self.net(input).squeeze(0).permute(1, 0)
gt = torch.from_numpy(gt_label).view(-1).long()
gt = gt.to('cuda')
criterion = nn.CrossEntropyLoss()
loss = criterion(pred, gt)
pred = torch.max(pred, 1)[1]
pred = pred.squeeze()
precision, recall, _, _ = precision_recall_fscore_support(gt.detach().cpu().numpy(), pred.detach().cpu().numpy())
loss.backward() # 计算梯度
self.optimizer.step() # 更新参数
if epoch % 20 == 0:
if self.scheduler.get_last_lr()[0] > 0.001:
self.scheduler.step()
logging.info(f"epoch:{epoch}\t"
f"loss: {loss.item():.4f}\t"
f"recall: {recall.mean():.4f}\t"
f"precision:{precision.mean():.4f}")
stat = {'state_dict': self.net.state_dict()}
torch.save(stat, 'snapshot/MLPs_10_08_3.pth')
评测指标采用总的分类准确率(Acc), 还将细致讨论各类的召回率 ( R ) (R) (R),最小召回率的类别(R_cmin)及其召回率(R_min), 最大召回率的类别 (R_cmax) 及其召回率 (R_max), 所有类的召回率的 均值 (R_mean) 和 标准差 (R_std). 分别对三种方法进行讨论,如下表,其中准确率上神经网络高于朴素贝叶斯,高于KNN,可见神经网络的强大学习能力,而KNN受限于模型过于简单,特征距离表征能力不够,朴素贝叶斯则受限于需要大量的训练数据以及词之间相互独立的假设。
还可以注意到,分的最差的类别中,Bayes的召回率高于神经网络,这从原理上可以理解,因为神经网络计算的是总体的loss,不去考虑每一个loss,而朴素贝叶斯对于每一类的计算方法都相同,所以各类之间不会差别太大。
另外,86.67%并不是神经网络的极限,当我试图加宽第二层网络时,取得了6%的突破,如果以后继续加深加宽这个网络,应该能取得更大的突破。
Acc(%) | R_mean(%) | R_std(%) | R_min(%)/R_cmin | R_max(%)/R_cmax | |
---|---|---|---|---|---|
Bayes | 82.27 | 82.30 | 7.71 | 63.90 /religion.misc | 98.89 /Religion.christian |
Knn | 63.83 | 63.85 | 10.46 | 45.64 /religion.misc | 90.89 /Religion.christian |
MLP | 86.67 | 87.02 | 9.69 | 54.38 /religion.misc | 98.96 /Religion.christian |
我还测试了不同K值对KNN性能的影响,如下图,可见准确率随着K值的上升而下降,这说明各类之间的距离比较小,K值扩大容易引入噪声(其他类的点)。