【某航】tf-idf文本特征提取与SVM分类——数据挖掘导论

代码链接:github代码

1.任务要求

(1)将数据集Case1-classification.zip中的email文件转换成列表数据,利用tf-idf方法提取其中的特征

(2)使用SVM分类文本类型,通过5折交叉验证检测分类结果,输出precision, recall, F1-score(可以使用LIBSVM实现SVM)

2.数据预处理与特征提取

数据预处理: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。通过两文件的对比分析,可以看到,两者分类结果类似,词干提取文件较小,是一种较粗粒度的检索方式,词形还原文件较大,并且大多数单词拥有一定含义,可以被用来文本挖掘情感分析等。

3.SVM分类

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文件中。

【某航】tf-idf文本特征提取与SVM分类——数据挖掘导论_第1张图片
【某航】tf-idf文本特征提取与SVM分类——数据挖掘导论_第2张图片
对比两种不同核函数,我们发现,采用线性linear核函数的消耗时间较短,因为,一般情况下,linear适用于线性可分的情况,参数少,速度快,对于一般数据,分类效果已经很理想了,RBF核:主要用于线性不可分的情形。参数多,分类结果非常依赖于参数(在这里使用的是默认设置参数),在参数表现方面,两种方法训练得到的评价指标很接近,在综合性能表现方面,rbf核的表现较好,如果可以采用5折交叉验证自动寻找rbf参数,可能回达到更好的效果。

查阅资料发现,吴恩达的机器学习课程中讲到,如果Feature的数量很大,跟样本数量差不多,这时候的情况大部分都是线性可分的,选用linear核较好;一般情况下,rbf核函数能够胜任绝大多数中情况,它是一种局部性强的核函数,其可以将一个样本映射到一个更高维的空间内,该核函数是应用最广的一个,无论大样本还是小样本都有比较好的性能,因此大多数情况下在不知道用什么核函数的时候,优先使用rbf高斯核函数。但终归来说,如果选用哪种核函数,还是要在测试集上验证之后才可以评判模型好坏,避免出现过拟合的现象。

对比两种不同特征数据集的结果,采用词形还原特征训练时间稍长,因为文件较大,包含的词向量更多,在训练结果的对比分析上,两种特征数据集的结果类似,详细看词形还原的recall值较高,precision和f1 score值较低,综合表现上采用词干提取的训练效果较佳,也正印证了在检索分类的情况下,推荐采取词干提取的方式进行特征提取。

4*.SMO算法

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代码

如果感觉对你有所帮助,不妨点个赞,关注一波,激励博主持续更新!

你可能感兴趣的:(课程学习,算法,python,机器学习,数据挖掘)