Python语言凭借其强大的特性,其众多的外部库支持下,在机器学习和数据挖掘等领域发挥着强大的作用。本文基于python的机器学习库scikit-learn和完备的中文分词工具jieba 来对垃圾短信信息进行分类。完整代码位于Github(https://github.com/ZPdesu/Junk-Message-Classifier-sklearn)
训练数据
0 xx计算机单招班彭雯同学表示将不负学校的培育
0 但是没有首都的在CBD一家好吃
0 今天给大家介绍一款Bookbook电脑包
1 亲爱的家长朋友你们好,我是张老师:新学期现在开始报名啦!本学期对老生家长的义务宣传有个回馈活动,老生带一个新生报名,赠送老生二百元兴趣
0 浙江宁波市马路突然爆裂
1 各位亲们好,新世纪奉节店迎接三八节,贝因美奶粉全场x.x折,欢迎惠额。活动时间x月x日至x月x日。
0 一架小型飞机在伦敦西南部一停车场坠毁
测试数据
刚刚坐电梯突然脚下踏板一软
佰特机器人俱乐部第七期大课堂在昨天圆满落幕了
训练集的标签域0代表是正常短信,1代表垃圾短信
原始短信数据共有数十万条,为了便于快速检验模型正确性,我们暂且取出其中的10000条左右用于训练和验证。对于大数据量情况,则可以使用数据库来代替文件,以同样的方式进行训练判别。处理前的第一步需要从存储信息的原始txt文档中,分割出文本域和标签域。作为content 和label分别存入对应的json文件中。
# -*- coding: utf-8 -*-
from numpy import *
import json
# 加载原始数据,进行分割
def load_message():
content = []
label = []
with open('RawData/message.txt') as fr:
for i in range(10000):
line = fr.readline()
lines.append(line)
num = len(lines)
for i in range(num):
message = lines[i].split('\t')
label.append(message[0])
content.append(message[1])
return num, content, label
# 将分割后的原始数据存到json
def data_storage(content, label):
with open('RawData/train_content.json', 'w') as f:
json.dump(content, f)
with open('RawData/train_label.json', 'w') as f:
json.dump(label, f)
if '__main__' == __name__:
num, content, label = load_message()
data_storage(content, label)
数据读取后下一步需要进行文本域的分词,在使用jieba进行精准模式分词之前需要对一些非规范数据进行,如对电话号码xxx,特殊字符&“】等进行统一转换。对一些反复出现的无意义词汇,如‘的’等当用词进行筛选。
这里首先介绍一下jieba分词的样式规范
In
# encoding=utf-8
import jieba
seg_list = jieba.cut("我来到北京清华大学", cut_all=True)
print("Full Mode: " + "/ ".join(seg_list)) # 全模式
seg_list = jieba.cut("我来到北京清华大学", cut_all=False)
print("Default Mode: " + "/ ".join(seg_list)) # 精确模式
seg_list = jieba.cut("他来到了网易杭研大厦") # 默认是精确模式
print(", ".join(seg_list))
seg_list = jieba.cut_for_search("小明硕士毕业于中国科学院计算所,后在日本京都大学深造") # 搜索引擎模式
print(", ".join(seg_list))
Out
【全模式】: 我/ 来到/ 北京/ 清华/ 清华大学/ 华大/ 大学
【精确模式】: 我/ 来到/ 北京/ 清华大学
【新词识别】:他, 来到, 了, 网易, 杭研, 大厦 (此处,“杭研”并没有在词典中,但是也被Viterbi算法识别出来了)
【搜索引擎模式】: 小明, 硕士, 毕业, 于, 中国, 科学, 学院, 科学院, 中国科学院, 计算, 计算所, 后, 在, 日本, 京都, 大学, 日本京都大学, 深造
生成词向量方式是建立词表,通过统计词表中词汇的出现次数来构建向量。但这种构建向量的方式,会导致每个词的重要过于均衡,无法体现词向量中词汇的重要性,导致分类结果不理想。因此改进中采用了TF-IDF方式来构建词向量。
# -*- coding: utf-8 -*-
import numpy as np
import jieba
import jieba.posseg as pseg
import sklearn.feature_extraction.text
import json
import re
from scipy import sparse, io
import load_data
# 将连续的数字转变为长度的维度
def process_cont_numbers(content):
digits_features = np.zeros((len(content), 16))
for i, line in enumerate(content):
for digits in re.findall(r'\d+', line):
length = len(digits)
if 0 < length <= 15:
digits_features[i, length-1] += 1
elif length > 15:
digits_features[i, 15] += 1
return process_cont_numbers
# 正常分词,非TFID
class MessageCountVectorizer(sklearn.feature_extraction.text.CountVectorizer):
def build_analyzer(self):
def analyzer(doc):
words = pseg.cut(doc)
new_doc = ''.join(w.word for w in words if w.flag != 'x')
words = jieba.cut(new_doc)
return words
return analyzer
# 用TFID生成对应词向量
class TfidfVectorizer(sklearn.feature_extraction.text.TfidfVectorizer):
def build_analyzer(self):
#analyzer = super(TfidfVectorizer, self).build_analyzer()
def analyzer(doc):
words = pseg.cut(doc)
new_doc = ''.join(w.word for w in words if w.flag != 'x')
words = jieba.cut(new_doc)
return words
return analyzer
# 生成词向量并进行存储
def vector_word():
with open('RawData/train_content.json', 'r') as f:
content = json.load(f)
with open('RawData/train_label.json', 'r') as f:
label = json.load(f)
'''
vec_count = MessageCountVectorizer(min_df=2, max_df=0.8)
data_count = vec_count.fit_transform(content)
name_count_feature = vec_count.get_feature_names()
'''
vec_tfidf = TfidfVectorizer(min_df=2, max_df=0.8)
data_tfidf = vec_tfidf.fit_transform(content)
name_tfidf_feature = vec_tfidf.get_feature_names()
io.mmwrite('Data/word_vector.mtx', data_tfidf)
with open('Data/train_label.json', 'w') as f:
json.dump(label, f)
with open('Data/vector_type.json', 'w') as f:
json.dump(name_tfidf_feature, f)
if '__main__' == __name__:
vector_word()
print ' OK '
当数据量过大,或是需要保存一些细节信息时(包括词向量索引所代表的语意信息等),我们可以通过数据库保存信息,也可以便于进一步的分析,和单个样本的存取。这里我们通过pymongo来把数据写入MongoDB数据库
import numpy as np
from pymongo import MongoClient
from scipy import sparse, io
import json
class DB_manager:
client = MongoClient()
db = client.test1
training_data = db.training_datas
def import_training_data(self, word_vector_file, train_label_file):
self.training_data.delete_many({})
self.training_data.create_index('training_num')
word_vector = io.mmread(word_vector_file)
vector = np.array(word_vector.todense())
with open(train_label_file, 'r') as f:
label = json.load(f)
num = len(label)
for i in range(num):
dic = {}
dic['training_num'] = i
dic['vector'] = list(vector[i])
dic['label'] = int(label[i])
self.training_data.insert_one(dic)
if '__main__' == __name__:
DB_manager().import_training_data('Data/word_vector.mtx', 'Data/train_label.json')
存储的词向量矩阵和对应标签文件中包含了所需的训练集数据和测试集数据,因此在模型训练前首先需要将数据集进行分割,这里将数据取出后进行一定比例的随机分割。
import json
from scipy import sparse, io
from sklearn.externals import joblib
from sklearn.model_selection import train_test_split
def split_data(content, label):
training_data, test_data, training_target, test_target = train_test_split(
content, label, test_size=0.1, random_state=0)
return training_data, test_data, training_target, test_target
with open('../Data/train_label.json', 'r') as f:
label = json.load(f)
training_data, test_data, training_target, test_target = split_data(content, label)
通常来说对数据进行标准化处理,能够更有效地提高分类的精度和准确率。常用的标准化方法包括0-1归一化和均值为0方差为1两种形式。这里给出了同时对数据集进行分割和实现数据标准化的方法。
def standardized_data(content, label):
training_data, test_data, training_target, test_target = split_data(content, label)
scalar = preprocessing.StandardScaler().fit(training_data)
training_data_transformed = scalar.transform(training_data)
test_data_transformed = scalar.transform(test_data)
return training_data_transformed, test_data_transformed, training_target, test_target
从分割后的训练集和测试集中取出的数据,通常都是高度稀疏的向量形式,即使是以csr稀疏矩阵的形式进行存储,在实际运算过程中也是转为高维向量的形式在进行计算。因此无论从节省时间,空间,还是减少数据噪声,提高分类精度的角度,降维过程都必不可少。在数据稀疏时,通常采用的降维方式包括了PCA(主成分分析法),Random projections(稀疏随机投影)和Feature agglomeration(特征聚类)。这里以PCA为例,将训练集和测试集的维度降低到1000维。其中生成降维矩阵的时间占了整个降维过程总时间的90%以上。
from scipy import sparse, io
from sklearn.decomposition import PCA
def dimensionality_reduction(training_data, test_data, type='pca'):
if type == 'pca':
n_components = 1000
t0 = time()
pca = PCA(n_components=n_components, svd_solver='randomized', whiten=True)
pca.fit(training_data)
print("done in %0.3fs" % (time() - t0))
t0 = time()
training_data_transform = sparse.csr_matrix(pca.transform(training_data))
test_data_transform = sparse.csr_matrix(pca.transform(test_data))
print("done in %0.3fs" % (time() - t0))
return training_data_transform, test_data_transform
(9000, 9957)
done in 42.173s
done in 4.302s
(9000, 1000)
scikit-learn 中有着完备的关于SVM的算法,这里我们将其封装成一个训练器类,便于训练,调参以及交叉验证。SVM根据其核函数的不同,可以分为各种不同类型,其中最典型的就是线性SVM分类器(kernel=’linear’)。其构造函数中参数选择的不同会导致不同的分类效果,但通常来说线性SVM相对稳定,默认参数也可以获得不错的效果
# -*- coding: utf-8 -*-
import numpy as np
from sklearn import svm
from sklearn import metrics
import json
from scipy import sparse, io
from sklearn.model_selection import cross_val_score
from sklearn.externals import joblib
from sklearn.svm import SVC
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import StratifiedShuffleSplit
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import ShuffleSplit
class TrainerLinear:
def __init__(self, training_data, training_target):
self.training_data = training_data
self.training_target = training_target
self.clf = svm.SVC(C=1, class_weight=None, coef0=0.0, decision_function_shape=None, degree=3, gamma='auto', kernel='linear', max_iter=-1, probability=False, random_state=None, shrinking=True, tol=0.001, verbose=False)
def learn_best_param(self):
C_range = np.logspace(-2, 10, 13)
param_grid = dict(C=C_range)
cv = StratifiedShuffleSplit(n_splits=5, test_size=0.2, random_state=42)
grid = GridSearchCV(SVC(), param_grid=param_grid, cv=cv)
grid.fit(self.training_data, self.training_target)
self.clf.set_params(C=grid.best_params_['C'])
print("The best parameters are %s with a score of %0.2f"
% (grid.best_params_, grid.best_score_))
def train_classifier(self):
self.clf.fit(self.training_data, self.training_target)
joblib.dump(self.clf, 'model/SVM_linear_estimator.pkl')
training_result = self.clf.predict(self.training_data)
print metrics.classification_report(self.training_target, training_result)
def cross_validation(self):
cv = ShuffleSplit(n_splits=5, test_size=0.2, random_state=20)
scores = cross_val_score(self.clf, self.training_data, self.training_target, cv=cv, scoring='f1_macro')
print scores
print("Accuracy: %0.2f (+/- %0.2f)" % (scores.mean(), scores.std() * 2))
与线性SVM有所差别,采用rbf(径向基函数)核的非线性算法时对参数C和gamma的要求非常高,两者分别代表着单个样本的影响范围和支持向量的影响程度。调整数值的同时也是在分类精度和运算成本之间做出均衡,不合适的数值还会造成模型的过拟合等情况。
import matplotlib.pyplot as plt
from matplotlib.colors import Normalize
class MidpointNormalize(Normalize):
def __init__(self, vmin=None, vmax=None, midpoint=None, clip=False):
self.midpoint = midpoint
Normalize.__init__(self, vmin, vmax, clip)
def __call__(self, value, clip=None):
x, y = [self.vmin, self.midpoint, self.vmax], [0, 0.5, 1]
return np.ma.masked_array(np.interp(value, x, y))
class TrainerRbf:
def __init__(self, training_data, training_target):
self.training_data = training_data
self.training_target = training_target
self.clf = svm.SVC(C=100, class_weight=None, coef0=0.0, decision_function_shape=None, degree=3, gamma=0.01, kernel='rbf', max_iter=-1, probability=False, random_state=None, shrinking=True, tol=0.001, verbose=False)
def learn_best_param(self):
C_range = np.logspace(-2, 10, 13)
gamma_range = np.logspace(-9, 3, 13)
param_grid = dict(gamma=gamma_range, C=C_range)
cv = StratifiedShuffleSplit(n_splits=5, test_size=0.2, random_state=42)
grid = GridSearchCV(SVC(), param_grid=param_grid, cv=cv)
grid.fit(self.training_data, self.training_target)
self.clf.set_params(C=grid.best_params_['C'], gamma=grid.best_params_['gamma'])
print("The best parameters are %s with a score of %0.2f"
% (grid.best_params_, grid.best_score_))
#self.draw_visualization_param_effect(grid, C_range, gamma_range)
def draw_visualization_param_effect(self, grid, C_range, gamma_range):
scores = grid.cv_results_['mean_test_score'].reshape(len(C_range),
len(gamma_range))
plt.figure(figsize=(8, 6))
plt.subplots_adjust(left=.2, right=0.95, bottom=0.15, top=0.95)
plt.imshow(scores, interpolation='nearest',
norm=MidpointNormalize(vmin=0.2, midpoint=0.92))
plt.xlabel('gamma')
plt.ylabel('C')
plt.colorbar()
plt.xticks(np.arange(len(gamma_range)), gamma_range, rotation=45)
plt.yticks(np.arange(len(C_range)), C_range)
plt.title('Validation accuracy')
plt.show()
plt.savefig('fig/param_effect.png')
def train_classifier(self):
self.clf.fit(self.training_data, self.training_target)
joblib.dump(self.clf, 'model/SVM_rbf_estimator.pkl')
training_result = self.clf.predict(self.training_data)
print metrics.classification_report(self.training_target, training_result)
def cross_validation(self):
cv = ShuffleSplit(n_splits=5, test_size=0.2, random_state=20)
scores = cross_val_score(self.clf, self.training_data, self.training_target, cv=cv, scoring='f1_macro')
print scores
print("Accuracy: %0.2f (+/- %0.2f)" % (scores.mean(), scores.std() * 2))
C与gamma对分类准确率的影响
在选择完相应的分类器类型后,就可以使用训练数据对模型进行训练了,采用上文提到的搜索方式查找到最佳的C值和gamma参数,通过模型对训练数据分类的精度(precision),召回率(recall), F度量(f1-score)来对模型进行初步评估。
In
from scipy import sparse, io
import json
from SVM_Trainer import TrainerLinear
from preprocessing_data import split_data
from preprocessing_data import dimensionality_reduction
content = io.mmread('../Data/word_vector.mtx')
with open('../Data/train_label.json', 'r') as f:
label = json.load(f)
training_data, test_data, training_target, test_target = split_data(content, label)
training_data, test_data = dimensionality_reduction(training_data.todense(), test_data.todense(), type='pca')
Trainer = TrainerLinear(training_data, training_target)
#Trainer.learn_best_param()
Trainer.train_classifier()
Out
The best parameters are {'C': 10000.0} with a score of 0.97
precision recall f1-score support
0 1.00 1.00 1.00 8128
1 1.00 1.00 1.00 872
avg / total 1.00 1.00 1.00 9000
这里采用的是线性SVM分类器。从输出的结果中可以看到,模型首先对寻找到的参数C进行了评分,然后对在训练数据上的表现作出了评价。因为垃圾短信类型分布的不平衡性,总体的评分本身没有多大的意义,因为将短信全部分为正常短信也能获得0.9以上的平均precision,recall和f1-score(前两者的调和平均数)。这里我们主要关注的是1类型(垃圾短信类型)的precision,只有将垃圾短信正确的分类出来,模型本身才有意义,而不是将本身正常的短信划分为正常。幸好我们看出1类型的precision表现还不错。
在训练数据较少的情况下,可以用 N折交差验证来一定程度的反映模型的准确性,同时可以做一个平均的度量,使得评价更佳稳定。
In
Trainer.cross_validation()
Out
[ 0.89272305 0.88408158 0.90056112 0.89901823 0.89448291]
Accuracy: 0.89 (+/- 0.01)
通过观察N折交差验证的表现,我们可以看出模型的实际效果还是不错的,因为交差验证所采用的数据集相当于是独立于模型的,真实地模拟了现实中的数据。此时我们将全部的训练集数据用于模型训练,将得出的模型持久化为pkl存储到 SVM目录的model文件夹下,便于训练测试集数据时直接调用。
在得到最终训练模型后,我们终于可以用来检验测试数据了。之前我们分割了大约1/10的数据作为测试集,现在将它们导入模型中进行类别预测。整个预测和评估的方法可以封装成两个类。
import json
from scipy import sparse, io
from sklearn.externals import joblib
from SVM_Trainer import TrainerLinear
from SVM_Predictor import Predictor
from preprocessing_data import split_data
from preprocessing_data import dimensionality_reduction
class Predictor:
def __init__(self, test_data, test_target):
self.test_data = test_data
self.test_target = test_target
def sample_predict(self, clf):
test_result = clf.predict(self.test_data)
print metrics.classification_report(self.test_target, test_result)
print metrics.confusion_matrix(self.test_target, test_result)
def new_predict(self, clf):
test_result = clf.predict(self.test_data)
with open('result/predict_label.txt', 'wt') as f:
for i in range(len(test_result)):
f.writelines(test_result[i])
self.test_target = test_result
print 'write over'
class Evaluator:
clf = joblib.load('model/SVM_linear_estimator.pkl')
def __init__(self, training_data, training_target, test_data, test_target):
self.trainer = TrainerLinear(training_data, training_target)
self.predictor = Predictor(test_data, test_target)
def train(self):
#self.trainer.learn_best_param()
self.trainer.train_classifier()
joblib.dump(self.clf, 'model/Terminal_estimator.pkl')
Evaluator.clf = joblib.load('model/Terminal_estimator.pkl')
def cross_validation(self):
self.trainer.cross_validation()
def predict(self, type):
if type == 'sample_data':
self.predictor.sample_predict(Evaluator.clf)
elif type == 'new_data':
self.predictor.new_predict(Evaluator.clf)
In
training_data, test_data, training_target, test_target = split_data(content, label)
training_data, test_data = dimensionality_reduction(training_data.todense(), test_data.todense(), type='pca')
evaluator = Evaluator(training_data, training_target, test_data, test_target)
evaluator.train()
#evaluator.cross_validation()
evaluator.predict(type='sample_data')
Out
precision recall f1-score support
0 0.98 0.98 0.98 906
1 0.80 0.82 0.81 94
avg / total 0.96 0.96 0.96 1000
[[887 19]
[ 17 77]]
除了之前classification report式的评估外,还加入了混淆矩阵。可以清楚的看到,正类样本中有一个被错分,负类样本中有两个被错分。即906个正常短信中有19个被错分为垃圾短信,94个垃圾短信中有17个被错分为正常短信。在只使用了10000多个样本数据的情况下,这样的分类精度还是可以接受的。
在整个处理垃圾短信分类的过程中,我们可以看到其与其他分类问题根本的不同在于需要处理语意信息。如何更好地处理文本数据,构建合理的特征向量矩阵显得至关重要。其次在于如何正确地选择分类器,调整分类器的参数,使其自适应地达到最佳的分类效果。
简单谈一下处理过程中会遇到的问题:
OK再谈一下很多没有处理的问题:
OK, 最后祝大家阅读愉快。完整代码详见https://github.com/ZPdesu/Junk-Message-Classifier-sklearn ,其中除朴素贝叶斯,SVM外后续会加入一些神经网络, RNN, LSTM等高级算法模型…当然这是不可能的,这只是一门课作业… PS(看我P卿大兄弟写的这么欢,我也试着写一下,城会玩!黑人问好脸? luoli控链接)