代码链接:github代码
(1)将数据集Case1-classification.zip中的email文件转换成列表数据,利用tf-idf方法提取其中的特征
(2)使用SVM分类文本类型,通过5折交叉验证检测分类结果,输出precision, recall, F1-score(可以使用LIBSVM实现SVM)
数据预处理:Emails_classify/Emails_classify_Proprocessing.py 文件中执行。
下面详细描述提取特征方法:
读取文章内容:从第四行——末尾,避免前两行邮件收发人信息影响内容;
去除标点符号:使用正则表达式去除标点符号,并用一个空格分隔每个单词;
词干提取:将所有的单词转化为小写,利用nltk包SnowballStemmer进行词干提取;
词形还原:判断单词词性,利用nltk包WordNetLemmatizer根据词性将其还原成原始形态,例如running——run;
去除停用词:下载nltk包中的标准stopwords,对每一个单词进行判断,例如是删除’the’等词汇;
特征提取:采用tf-idf方法,计算在每一个邮件中,每一个单词及其相对应的value。
采用两种方法进行分词:词形还原,词干提取
词形还原(lemmatization),是把一个任何形式的语言词汇还原为一般形式(能表达完整语义),而**词干提取(stemming)**是抽取词的词干或词根形式(不一定能够表达完整语义)。词形还原和词干提取是词形规范化的两类重要方式,都能够达到有效归并词形的目的,二者既有联系也有区别。
在原理上,词干提取主要是采用“缩减”的方法,将词转换为词干,如将“cats”处理为“cat”,将“effective”处理为“effect”。而词形还原主要采用“转变”的方法,将词转变为其原形,如将“drove”处理为“drive”,将“driving”处理为“drive”。
在应用领域上,同样各有侧重。虽然二者均被应用于信息检索和文本处理中,但侧重不同。词干提取更多被应用于信息检索领域,如Solr、Lucene等,用于扩展检索,粒度较粗。词形还原更主要被应用于文本挖掘、自然语言处理,用于更细粒度、更为准确的文本分析和表达。
在本课题中,分别采用了这两种方法生成特征数据lemmatization_preed_data.csv 和 stemming_preed_data.csv。通过两文件的对比分析,可以看到,两者分类结果类似,词干提取文件较小,是一种较粗粒度的检索方式,词形还原文件较大,并且大多数单词拥有一定含义,可以被用来文本挖掘情感分析等。
SVM分类:Emails_classify/Emails_classify_svm.py 文件中执行。
SVM分类,调用sklearn-learn中的SVM分类。采用svm.SVC 分类方法,核函数可以通过命令行进行自主选择,一般默认为‘RBF’。运行缓存定位800MB,尽量加快训练速度。
通过数据读取,数据格式划分,建立模型,模型拟合,计算评价指标,完成整个训练。
在训练过程中,可以选择采用不同的核函数类型(常用rbf类型或linear类型);另外,采用KFold五折自动训练(必须训练前打乱数据顺序分组),利用metrics计算precision, recall, f1_score三类评价指标具体数值。
结果:
SVM分类模型一般用线性核和高斯核,也就是Linear核 与RBF核 。在本实验中,在上述两种特征数据的基础上,分别运用两种核函数进行测试,结果保存在文件夹下的txt文件中。
对比两种不同核函数,我们发现,采用线性linear核函数的消耗时间较短,因为,一般情况下,linear适用于线性可分的情况,参数少,速度快,对于一般数据,分类效果已经很理想了,RBF核:主要用于线性不可分的情形。参数多,分类结果非常依赖于参数(在这里使用的是默认设置参数),在参数表现方面,两种方法训练得到的评价指标很接近,在综合性能表现方面,rbf核的表现较好,如果可以采用5折交叉验证自动寻找rbf参数,可能回达到更好的效果。
查阅资料发现,吴恩达的机器学习课程中讲到,如果Feature的数量很大,跟样本数量差不多,这时候的情况大部分都是线性可分的,选用linear核较好;一般情况下,rbf核函数能够胜任绝大多数中情况,它是一种局部性强的核函数,其可以将一个样本映射到一个更高维的空间内,该核函数是应用最广的一个,无论大样本还是小样本都有比较好的性能,因此大多数情况下在不知道用什么核函数的时候,优先使用rbf高斯核函数。但终归来说,如果选用哪种核函数,还是要在测试集上验证之后才可以评判模型好坏,避免出现过拟合的现象。
对比两种不同特征数据集的结果,采用词形还原特征训练时间稍长,因为文件较大,包含的词向量更多,在训练结果的对比分析上,两种特征数据集的结果类似,详细看词形还原的recall值较高,precision和f1 score值较低,综合表现上采用词干提取的训练效果较佳,也正印证了在检索分类的情况下,推荐采取词干提取的方式进行特征提取。
SMO算法:Emails_classify/Emails_classify_SMO.py 文件中执行。
SMO(Sequential Minimal Optimization)序列最小化算法就是一个二次规划优化算法。SMO算法的目标就是求出一些列的alpha和b,一旦求出这些alpha,就可以计算出权重向量,并求出分割超平面。SMO算法的工作原理是:每次循环中选择两个alpha进行优化处理。一旦找到一对合适的alpha,那么就增大其中一个,同时减少另一个。
根据已知SMO算法流程,编写SMO算法。直接运行Emails_classify_SMO.py文件即可,会显示相关b和alpha的输出。
# 数据预处理Emails_classify_Proprocessing.py
import re
import os
import argparse
import pandas as pd
from nltk.corpus import stopwords
from nltk.stem import SnowballStemmer
from sklearn.feature_extraction.text import TfidfVectorizer
from nltk import word_tokenize, pos_tag
from nltk.corpus import wordnet
from nltk.stem import WordNetLemmatizer
def del_punctuation(content):
"""
删除标点,保证单词之间仅有一个空格
:param content: 文件内容
:return: 文件内容
"""
content = re.sub("[\\\'\t\n\.\!\/_,$%^*()\[\]+\"<>\-:]+|[|+——!,。??、~@#¥%……&*()]+", " ", content)
content = ' '.join(content.split())
return content
def stemming(content):
"""
词干提取stemming
:param content: 文件内容
:return: 文件内容
"""
stemmer = SnowballStemmer("english") # 选择目标语言为英语
all_words = content.split(' ')
new_content = []
for word in all_words:
new_word = stemmer.stem(word.lower()) # Stem a word 并且转化为小写
if new_word != ' ':
new_content.append(new_word)
return " ".join(new_content)
def get_wordnet_pos(tag):
"""
获取单词的词性
:param tag: 词性
:return: 词类型
"""
if tag.startswith('J'):
return wordnet.ADJ
elif tag.startswith('V'):
return wordnet.VERB
elif tag.startswith('N'):
return wordnet.NOUN
elif tag.startswith('R'):
return wordnet.ADV
else:
return None
def lemmatization(content):
"""
词形还原 lemmatization
:param content: 文件内容
:return: 文件内容
"""
all_words = word_tokenize(content) # 分词
tagged_sent = pos_tag(all_words) # 获取单词词性
wnl = WordNetLemmatizer()
new_content = []
for tag in tagged_sent:
wordnet_pos = get_wordnet_pos(tag[1]) or wordnet.NOUN
new_content.append(wnl.lemmatize(tag[0], pos=wordnet_pos)) # 词形还原
return " ".join(new_content)
def remove_stop_words(content):
"""
去停用词
:param content: 文件内容
:return: 文件内容
"""
stopwords_list = stopwords.words('english')
all_words = content.split(' ')
new_content = []
for word in all_words:
if word not in stopwords_list:
new_content.append(word)
return " ".join(new_content)
def tfidf(file_list, label):
"""
特征提取 采用tfidf
:param file_list: 合成的总文件
:param label: 每个文件的类别标签
:return: dataframe 表格
"""
vectorizer = TfidfVectorizer()
vectors = vectorizer.fit_transform(file_list)
feature_names = vectorizer.get_feature_names()
dense = vectors.todense()
denselist = dense.tolist()
df = pd.DataFrame(denselist, columns=feature_names)
df['@label'] = label
print(df)
return df
if __name__ == '__main__':
# 可配置式方法选择
parser = argparse.ArgumentParser(description='Choose the method(stemming or lemmatization)')
parser.add_argument('--method', '-m', help='method 词干提取stemming或词性还原lemmatization,非必要参数,但有默认值', default = 'stemming')
args = parser.parse_args()
method = args.method
file_r_list = [] # 存储所有的文件内容
label = [] # 存储类别结果,'baseball':1; 'hockey':-1
for type_name in ['baseball', 'hockey']:
url = 'dataset/Case1-classification/' + type_name
for file_name in os.listdir(url):
try:
file = open(url + '/' + file_name, 'r', encoding='latin-1')
lines = file.readlines()
except Exception as e:
print(url + '/' + file_name + '无法打开')
print(e)
continue
# 读取每一封邮件“第四行——末尾”,并存入content中
content = ''
for i in range(3, len(lines)):
content += lines[i]
# 数据预处理:1. 去除标点符号;2. 词性还原/词干提取; 3. 去除停用词
content = del_punctuation(content)
if method == 'stemming':
content = stemming(content)
elif method == 'lemmatization':
content = lemmatization(content)
content = remove_stop_words(content)
file_r_list.append(content)
# 数据打标签
if type_name == 'baseball':
label.append(1)
elif type_name == 'hockey':
label.append(-1)
preed_data = tfidf(file_r_list, label)
preed_data.to_csv(method + '_preed_data.csv', index = False)
## SVM分类 Emails_classify_svm.py
import time
import argparse
import numpy as np
import pandas as pd
from sklearn import svm
from sklearn.model_selection import KFold
from sklearn import metrics
if __name__ == "__main__":
# 可配置式运行文件
parser = argparse.ArgumentParser(description='Choose the kernel and data_file')
parser.add_argument('--method', '-m', help='method 选择核函数(linear or rbf or poly or sigmoid)', default = 'rbf')
parser.add_argument('--file', '-f', help='file 选择采用数据集类型(stemming or lemmatization)', default='stemming')
args = parser.parse_args()
method = args.method
file = args.file
data = pd.read_table(file + '_preed_data.csv', header=0, encoding='utf-8', sep=",", index_col=0)
data = data.as_matrix()[1:][1:]
X, y = data[:, 0:-1], data[:, -1].astype(int)
kf = KFold(n_splits=5, shuffle = True) # 五折训练,打乱数据顺序
precisionList = []
recallList = []
f1List = []
i = 1 # 表征五折训练的次数
time_start = time.time()
print(file + "_preed_data.csv 数据读取完成,开始五折SVM训练,method = "+ method)
for train_index, val_index in kf.split(X):
time_kfold_start = time.time()
X_train, X_val, y_train, y_val = X[train_index], X[val_index], y[train_index], y[val_index]
# 建立模型
clf = svm.SVC(kernel=method, gamma = 'scale', cache_size = 800)
# 模型拟合
clf.fit(X_train, y_train)
#模型预测
y_pred = clf.predict(X_val)
# 计算评价指标:precision, recall, f1_score
precision = metrics.precision_score(y_val, y_pred)
recall = metrics.recall_score(y_val, y_pred)
f1 = metrics.f1_score(y_val, y_pred)
precisionList.append(precision)
recallList.append(recall)
f1List.append(f1)
time_kfold_end = time.time()
print("KFold_" + str(i))
print("time consumption:" + str(time_kfold_end - time_kfold_start) + "s")
print("precision:" + str(precision))
print("recall:" + str(recall))
print("f1:" + str(f1))
i += 1
time_end = time.time()
print("************五折训练全部完成************")
print("total time consumption:" + str(time_end - time_start) + "s")
print("average precision:" + str(np.mean(precisionList)))
print("average recall:" + str(np.mean(recallList)))
print("average f1:" + str(np.mean(f1List)))
## SMO算法实现Emails_classify_SMO.py
#-*-coding:utf-8-*-
from numpy import *
import numpy as np
#定义存储变量的类
class opStruct():
def __init__(self,dataMatIn,classLabels,C,toler,kTup,kTup1):
self.X = dataMatIn #数据
self.labelMat = classLabels #标签
self.C = C #容忍度
self.toler = toler #误差的容忍度
self.m = shape(dataMatIn)[0] #数据的个数
self.alphas = mat(zeros((self.m,1))) #alpha 值,每个数据对应一个alpha
self.b = 0 # 常数项
self.eCache = mat(zeros((self.m,2))) #保存误差和下表
self.k = mat(zeros((self.m,self.m)))
for i in range(self.m):
self.k[:,i] = kernelTrans(self.X,self.X[i,:],kTup,kTup1)
def getrightfile(filename1,filename2):
"""
载入数据
"""
a = np.load(filename1)
b = np.load(filename2)
dataMatIn = a.tolist()
classLabels = b.tolist()
return dataMatIn,classLabels
def kernelTrans(X,A,kTup,kTup1):
"""
加入核函数
"""
m,n = shape(X)
k = mat(zeros((m,1)))
if kTup == 'lin':
k = X * A.T
elif kTup == 'rbf':
for j in range(m):
deltaRow = X[j,:] - A #每一行减去A,在自己乘
k[j] = deltaRow * deltaRow.T
k = exp(k/(-1 * kTup1 ** 2)) #就是利用的公式
return k
def clipAlpha(ai,H,L):
"""
保证alpha必须在范围内
"""
if ai > H:
ai = H
elif ai < L :
ai = L
return ai
def selectJrand(i,oS):
"""
随机选择第二个不同的alpha
"""
j = i
while i == j:
j = int(np.random.uniform(0,oS.m))
return j
def calcEk(oS,k):
"""
计算误差
"""
fXk = float((multiply(oS.alphas,oS.labelMat)).T*oS.k[:,k] + oS.b) #预测值
Ek = fXk - oS.labelMat[k] #误差值
return Ek
def selectJ(i,oS,Ei):
"""
选择第二个alpha 并且相差最大的
"""
maxK = -1
maxDelaE = 0
Ej = 0
oS.eCache[i] = [1,Ei]
validEcaheList = nonzero(oS.eCache[:,0].A)[0]
if len(validEcaheList) > 0:
for k in validEcaheList:
if k == i: #取不同的 alpha
continue
Ek = calcEk(oS,k) #计算k的与测试与真实值之间的误差
deltaE = abs(Ei - Ek) #找与Ei 距离最远的
if maxDelaE < deltaE:
maxDelaE = deltaE
maxK = k #与Ei差别最大的K
Ej = Ek #K的误差
return maxK,Ej
else:
j = selectJrand(i,oS)
Ej = calcEk(oS,j) #计算预测值和真实值的误差
return j,Ej
def updateEk(oS,k):
"""
更新误差
"""
Ek = calcEk(oS,k)
oS.eCache[k] = [1,Ek]
def innerL(i,oS):
"""
SMO 优化
"""
Ei = calcEk(oS,i)
#在误差允许的范围外,如果小于规定的误差,就不需要更新了
if ((oS.labelMat[i] * Ei ) <= oS.toler and oS.alphas[i] <= oS.C) or\
((oS.labelMat[i] * Ei) <= oS.toler and oS.alphas[i] >= 0):
j,Ej = selectJ(i,oS,Ei) #选择另一个alphaj和预测值与真实值的差
alphaIold = oS.alphas[i].copy() #复制alpha,因为后边会用到
alphaJold = oS.alphas[j].copy()
if (oS.labelMat[i] != oS.labelMat[j]): #两个类别不一样 一个正类 一个负类
L = max(0,oS.labelMat[j] - oS.labelMat[i]) # 约束条件 博客里有
H = min(oS.C,oS.C + oS.alphas[j] - oS.alphas[i])
else:
L = max(0,oS.alphas[j] + oS.alphas[i] - oS.C)
H = min(oS.C,oS.alphas[j] + oS.alphas[i])
if L == H:
print('L == H')
return 0
#利用核函数
eta = 2.0 * oS.k[i,j] - oS.k[i,i] - oS.k[j,j]
if eta > 0:
return 0
oS.alphas[j] -= oS.labelMat[j] * (Ei - Ej)/eta #就是按最后的公式求解
oS.alphas[j] = clipAlpha(oS.alphas[j],H,L) #在L,H范围内
updateEk(oS,j)
if (oS.alphas[j] - alphaJold) < 0.0001:
return 0
oS.alphas[i] += oS.labelMat[j] * oS.labelMat[i] * (alphaJold - oS.alphas[j])
updateEk(oS,i)
#利用核函数的
b1 = oS.b - Ei - oS.labelMat[i] * oS.k[i,i] * (oS.alphas[i] - alphaIold) - oS.labelMat[j] * oS.k[i,j] * (oS.alphas[j] - alphaJold)
b2 = oS.b - Ej - oS.labelMat[i] * oS.k[i,j] * (oS.alphas[i] - alphaIold) - oS.labelMat[j] * oS.k[j,j] * (oS.alphas[j] - alphaJold)
#跟新b
if oS.alphas[i] < oS.C and oS.alphas[i] > 0:
oS.b = b1
elif oS.alphas[j] < oS.C and oS.alphas[j] > 0:
oS.b = b2
else:
oS.b = (b1 + b2)/2.0
return 1
else:
return 0
def calcWs(alpha,dataArr,classLabels):
"""
计算alpha 获得分类的权重向量
:param alpha:
:param dataArr: 训练数据
:param classLabels: 训练标签
:return:
"""
X = mat(dataArr)
labelMat = mat(classLabels).T #变成列向量
m,n = shape(X)
w =zeros((n,1)) #w的个数与 数据的维数一样
for i in range(m):
w += multiply(alpha[i] * labelMat[i],X[i,:].T) #alpha[i] * labelMat[i]就是一个常数 X[i,:]每(行)个数据,因为w为列向量,所以需要转置
return w
def smoP(dataMatIn,classLabels,C,toler,maxIter,kTup ='lin',kTup1=0):
"""
核心主函数
:param dataMatIn: 训练数据
:param classLabels: 训练标签
:param C: 常量
:param toler:容错度
:param maxIter: 最大迭代次数
:param kTup: 核函数类型参数
:param kTup1: 核函数类型参数
:return:
"""
oS = opStruct(mat(dataMatIn),mat(classLabels).T,C,toler,kTup,kTup1)
iter = 0
entireSet = True
alphaPairedChanged = 0
while (iter < maxIter) and ((alphaPairedChanged > 0) or (entireSet)):
alphaPairedChanged = 0
if entireSet:
# 遍历所有的数据 进行更新
for i in range(oS.m):
alphaPairedChanged += innerL(i,oS)
iter += 1
else:
nonBoundIs = nonzero((oS.alphas.A > 0) * (oS.alphas.A < oS.C))[0]
for i in nonBoundIs:
alphaPairedChanged += innerL(i,oS)
iter += 1
if entireSet:
entireSet = False
elif (alphaPairedChanged == 0):
entireSet = True
return oS.b,oS.alphas
if __name__ == '__main__':
dataMatIn,classLabels = getrightfile('smo_mail_list.npy','smo_label_list.npy')
b,alphas =smoP(dataMatIn,classLabels,C=0.6,toler=0.001,maxIter=40,kTup = 'lin',kTup1=1)
print(b,alphas)
## 安装文本处理包install_database.py
import nltk
nltk.download('stopwords')
nltk.download('punkt')
nltk.download('averaged_perceptron_tagger')
代码链接:github代码
如果感觉对你有所帮助,不妨点个赞,关注一波,激励博主持续更新!