使用python和sklearn的中文文本多分类实战开发

文本分类一般可以分为二分类、多分类、多标签分类三种情况,二分类是指将一组文本分成两个类(0或1),比较常见的应用如垃圾邮件分类、电商网站的用户评价数据的正负面分类等,多分类是指将文本分成若干个类中的某一个类,比如说门户网站新闻可以归属到不同的栏目中(如政治、体育、社会、科技、金融等栏目)去。多标签分类指的是可以将文本分成若干个类中的多个类,比如一篇文章里即描写政治又描写金融等内容,那么这篇文章可能会别贴上政治和金融两个标签。今天我尝试使用Python和sklearn来实现一下文本的多分类实战开发。

数据分析

我们可以从这里下载数据,数据中包含了10 个类别(书籍、平板、手机、水果、洗发水、热水器、蒙牛、衣服、计算机、酒店),共 6 万多条评论数据 首先查看一下我们的数据,这些数据都是来自于电商网站的用户评价数据,我们想要把不同评价数据分到不同的分类中去,且每条数据只能对应10个类中的一个类。

%matplotlib inline
import pandas as pd
import matplotlib
import numpy as np
import matplotlib.pyplot as plt
import jieba as jb
import re

df = pd.read_csv('./data/shopping.csv')
df=df[['cat','review']]
print("数据总量: %d ." % len(df))
df.sample(10)

我们的数据有两个字段,其中cat字段表示类别,review表示用户的评价信息,数据总量为62774,且评价内容全部为中文。接下来我们要清洗掉数据中的空值。

print("在 cat 列中总共有 %d 个空值." % df['cat'].isnull().sum())
print("在 review 列中总共有 %d 个空值." % df['review'].isnull().sum())
df[df.isnull().values==True]
df = df[pd.notnull(df['review'])]

还好只有一个空值,接下来我们统计一下各个类别的数据量

d = {'cat':df['cat'].value_counts().index, 'count': df['cat'].value_counts()}
df_cat = pd.DataFrame(data=d).reset_index(drop=True)
df_cat

 我们看到各个类别的数据量不一致,洗发水、衣服、酒店、水果、平板的数据量最多各有1万条,而热水器的数据量最少只有574条,分布很不均匀,接下来我们用图形化的方式再查看一下各个类别的分布。

df_cat.plot(x='cat', y='count', kind='bar', legend=False,  figsize=(8, 5))
plt.title("类目数量分布")
plt.ylabel('数量', fontsize=18)
plt.xlabel('类目', fontsize=18)

 

数据预处理

接下来我们要将cat类转换成id,这样便于以后的分类模型的训练。

df['cat_id'] = df['cat'].factorize()[0]
cat_id_df = df[['cat', 'cat_id']].drop_duplicates().sort_values('cat_id').reset_index(drop=True)
cat_to_id = dict(cat_id_df.values)
id_to_cat = dict(cat_id_df[['cat_id', 'cat']].values)
df.sample(10)

cat_id_df

 

 我们将cat转换成了Id(0到9),由于我们的评价内容都是中文,所以要对中文进行一些预处理工作,这包括删除文本中的标点符号,特殊符号,还要删除一些无意义的常用词(stopword),因为这些词和符号对系统分析预测文本的内容没有任何帮助,反而会增加计算的复杂度和增加系统开销,所有在使用这些文本数据之前必须要将它们清理干净。

#定义删除除字母,数字,汉字以外的所有符号的函数
def remove_punctuation(line):
    line = str(line)
    if line.strip()=='':
        return ''
    rule = re.compile(u"[^a-zA-Z0-9\u4E00-\u9FA5]")
    line = rule.sub('',line)
    return line

def stopwordslist(filepath):  
    stopwords = [line.strip() for line in open(filepath, 'r', encoding='utf-8').readlines()]  
    return stopwords  

#加载停用词
stopwords = stopwordslist("./data/chineseStopWords.txt")

中文停用词包含了很多日常使用频率很高的常用词,如 吧,吗,呢,啥等一些感叹词等,这些高频常用词无法反应出文本的主要意思,所以要被过滤掉。可以在这里下载中文停用词。

#删除除字母,数字,汉字以外的所有符号
df['clean_review'] = df['review'].apply(remove_punctuation)
df.sample(10)

我们过滤掉了review中的标点符号和一些特殊符号,并生成了一个新的字段 clean_review。接下来我们要在clean_review的基础上进行分词,把每个评论内容分成由空格隔开的一个一个单独的词语。

#分词,并过滤停用词
df['cut_review'] = df['clean_review'].apply(lambda x: " ".join([w for w in list(jb.cut(x)) if w not in stopwords]))
df.head()

 经过分词以后我们生成了cut_review字段。在cut_review中每个词语中间都是由空格隔开,接下来我要在cut_review的基础上生成每个分类的词云,我们要在每个分类中罗列前100个高频词。然后我们要画出这些高频词的词云

from collections import Counter
from wordcloud import WordCloud

def generate_wordcloud(tup):
    wordcloud = WordCloud(background_color='white',
                          font_path='simhei.ttf',
                          max_words=50, max_font_size=40,
                          random_state=42
                         ).generate(str(tup))
    return wordcloud

cat_desc = dict()
for cat in cat_id_df.cat.values: 
    
    text = df.loc[df['cat']==cat, 'cut_review']
    text = (' '.join(map(str,text))).split(' ')
    cat_desc[cat]=text
    
fig,axes = plt.subplots(5, 2, figsize=(30, 38))
k=0
for i in range(5):
    for j in range(2):
        cat = id_to_cat[k]
        most100=Counter(cat_desc[cat]).most_common(100)
        ax = axes[i, j]
        ax.imshow(generate_wordcloud(most100), interpolation="bilinear")
        ax.axis('off')
        ax.set_title("{} Top 100".format(cat), fontsize=30)
        k+=1

 

 

 

接下来我要计算cut_review的 TF-IDF的特征值,TF-IDF(term frequency–inverse document frequency)是一种用于信息检索与数据挖掘的常用加权技术。TF意思是词频(Term Frequency),IDF意思是逆文本频率指数(Inverse Document Frequency)。TF-IDF是在单词计数的基础上,降低了常用高频词的权重,增加罕见词的权重。因为罕见词更能表达文章的主题思想,比如在一篇文章中出现了“中国”和“卷积神经网络”两个词,那么后者将更能体现文章的主题思想,而前者是常见的高频词,它不能表达文章的主题思想。所以“卷积神经网络”的TF-IDF值要高于“中国”的TF-IDF值。这里我们会使用sklearn.feature_extraction.text.TfidfVectorizer方法来抽取文本的TF-IDF的特征值。这里我们使用了参数ngram_range=(1,2),这表示我们除了抽取评论中的每个词语外,还要抽取每个词相邻的词并组成一个“词语对”,如: 词1,词2,词3,词4,(词1,词2),(词2,词3),(词3,词4)。这样就扩展了我们特征集的数量,有了丰富的特征集才有可能提高我们分类文本的准确度。参数norm='l2',是一种数据标准划处理的方式,可以将数据限制在一点的范围内比如说(-1,1)

from sklearn.feature_extraction.text import TfidfVectorizer

tfidf = TfidfVectorizer(norm='l2', ngram_range=(1, 2))
features = tfidf.fit_transform(df.cut_review)
labels = df.cat_id
print(features.shape)
print('-----------------------------')
print(features)

我们看到我们的features的维度是(62773,657425),这里的62773表示我们总共有62773条评价数据,657425表示我们的特征数量这包括全部评论中的所有词语数+词语对(相邻两个单词的组合)的总数。下面我们要是卡方检验的方法来找出每个分类中关联度最大的两个词语和两个词语对。卡方检验是一种统计学的工具,用来检验数据的拟合度和关联度。在这里我们使用sklearn中的chi2方法。

from sklearn.feature_selection import chi2
import numpy as np

N = 2
for cat, cat_id in sorted(cat_to_id.items()):
    features_chi2 = chi2(features, labels == cat_id)
    indices = np.argsort(features_chi2[0])
    feature_names = np.array(tfidf.get_feature_names())[indices]
    unigrams = [v for v in feature_names if len(v.split(' ')) == 1]
    bigrams = [v for v in feature_names if len(v.split(' ')) == 2]
    print("# '{}':".format(cat))
    print("  . Most correlated unigrams:\n       . {}".format('\n       . '.join(unigrams[-N:])))
    print("  . Most correlated bigrams:\n       . {}".format('\n       . '.join(bigrams[-N:])))

 我们可以看到经过卡方(chi2)检验后,找出了每个分类中关联度最强的两个词和两个词语对。这些词和词语对能很好的反映出分类的主题。

分类器的选择

  • 为了训练监督学习的分类器,我们首先将“review”转变为包含数字的词向量。例如我们前面已经转换好的tf-idf的features。
  • 当我们有了词向量以后我们就可以开始训练我们的分类器。分类器训练完成后,就可以对没有见过的review进行预测。

朴素贝叶斯分类器

朴素贝叶斯分类器最适合用于基于词频的高维数据分类器,最典型的应用如垃圾邮件分类器等,准确率可以高达95%以上。这里我们使用的是sklearn的朴素贝叶斯分类器MultinomialNB,我们首先将review转换成词频向量,然后将词频向量再转换成TF-IDF向量,还有一种简化的方式是直接使用TfidfVectorizer来生成TF-IDF向量(正如前面生成features的过程),这里我们还是按照一般的方式将生成TF-IDF向量分成两个步骤:1.生成词频向量. 2.生成TF-IDF向量。最后我们开始训练我们的MultinomialNB分类器。

from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.naive_bayes import MultinomialNB

X_train, X_test, y_train, y_test = train_test_split(df['cut_review'], df['cat_id'], random_state = 0)
count_vect = CountVectorizer()
X_train_counts = count_vect.fit_transform(X_train)

tfidf_transformer = TfidfTransformer()
X_train_tfidf = tfidf_transformer.fit_transform(X_train_counts)

clf = MultinomialNB().fit(X_train_tfidf, y_train)

当模型训练完成后,我们让它预测一些自定义的review的分类。不过我们首先编写一个预测函数myPredict

def myPredict(sec):
    format_sec=" ".join([w for w in list(jb.cut(remove_punctuation(sec))) if w not in stopwords])
    pred_cat_id=clf.predict(count_vect.transform([format_sec]))
    print(id_to_cat[pred_cat_id[0]])

 貌似预测效果还算可以。呵呵!

模型的选择

接下来我们尝试不同的机器学习模型,并评估它们的准确率,我们将使用如下四种模型:

  • Logistic Regression(逻辑回归)
  • (Multinomial) Naive Bayes(多项式朴素贝叶斯)
  • Linear Support Vector Machine(线性支持向量机)
  • Random Forest(随机森林)
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.naive_bayes import MultinomialNB
from sklearn.svm import LinearSVC

from sklearn.model_selection import cross_val_score


models = [
    RandomForestClassifier(n_estimators=200, max_depth=3, random_state=0),
    LinearSVC(),
    MultinomialNB(),
    LogisticRegression(random_state=0),
]
CV = 5
cv_df = pd.DataFrame(index=range(CV * len(models)))
entries = []
for model in models:
    model_name = model.__class__.__name__
    accuracies = cross_val_score(model, features, labels, scoring='accuracy', cv=CV)
    for fold_idx, accuracy in enumerate(accuracies):
        entries.append((model_name, fold_idx, accuracy))
cv_df = pd.DataFrame(entries, columns=['model_name', 'fold_idx', 'accuracy'])
import seaborn as sns

sns.boxplot(x='model_name', y='accuracy', data=cv_df)
sns.stripplot(x='model_name', y='accuracy', data=cv_df, 
              size=8, jitter=True, edgecolor="gray", linewidth=2)
plt.show()

从可以箱体图上可以看出随机森林分类器的准确率是最低的,因为随机森林属于集成分类器(有若干个子分类器组合而成),一般来说集成分类器不适合处理高维数据(如文本数据),因为文本数据有太多的特征值,使得集成分类器难以应付,另外三个分类器的平均准确率都在80%以上。其中线性支持向量机的准确率最高。

cv_df.groupby('model_name').accuracy.mean()

我们看到线性支持向量机的平均准确率达到了86.7%,其次是逻辑回归和朴素贝叶斯。

模型的评估

下面我们就针对平均准确率最高的LinearSVC模型,我们将查看混淆矩阵,并显示预测标签和实际标签之间的差异。

from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, confusion_matrix

#训练模型
model = LinearSVC()
X_train, X_test, y_train, y_test, indices_train, indices_test = train_test_split(features, labels, df.index, 
                                                                                 test_size=0.33, stratify=labels, random_state=0)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)



#生成混淆矩阵
conf_mat = confusion_matrix(y_test, y_pred)
fig, ax = plt.subplots(figsize=(10,8))
sns.heatmap(conf_mat, annot=True, fmt='d',
            xticklabels=cat_id_df.cat.values, yticklabels=cat_id_df.cat.values)
plt.ylabel('实际结果',fontsize=18)
plt.xlabel('预测结果',fontsize=18)
plt.show()

 

混淆矩阵的主对角线表示预测正确的数量,除主对角线外其余都是预测错误的数量.从上面的混淆矩阵可以看出"蒙牛"类预测最准确,只有一例预测错误。“平板”和“衣服”预测的错误数量教多。

 多分类模型一般不使用准确率(accuracy)来评估模型的质量,因为accuracy不能反应出每一个分类的准确性,因为当训练数据不平衡(有的类数据很多,有的类数据很少)时,accuracy不能反映出模型的实际预测精度,这时候我们就需要借助于F1分数、ROC等指标来评估模型。

下面我们将查看各个类的F1分数.

from sklearn.metrics import classification_report

print('accuracy %s' % accuracy_score(y_pred, y_test))
print(classification_report(y_test, y_pred,target_names=cat_id_df['cat'].values))

从以上F1分数上看,"蒙牛"类的F1分数最大(只有一个预测错误),“热水器”类F1分数最差只有66%,究其原因可能是因为“热水器”分类的训练数据最少只有574条,使得模型学习的不够充分,导致预测失误较多吧。

下面我们来查看一些预测失误的例子,希望大家能从中发现一些奥秘,来改善我们的分类器。

from IPython.display import display

for predicted in cat_id_df.cat_id:
    for actual in cat_id_df.cat_id:
        if predicted != actual and conf_mat[actual, predicted] >= 6:
            print("{} 预测为 {} : {} 例.".format(id_to_cat[actual], id_to_cat[predicted], conf_mat[actual, predicted]))
            display(df.loc[indices_test[(y_test == actual) & (y_pred == predicted)]][['cat', 'review']])
            print('')

 

 

 

 

 完整的代码可以在这里下载

猜你喜欢

转载自blog.csdn.net/weixin_42608414/article/details/88046380