1、概述
本项目的 GitHub 地址:https://github.com/junzhengdi/sklearn/blob/master/news-36kr.ipynb
商业活动中有很多文本分类应用。例如,新闻报道通常是按照主题进行构架;内容或产品通常是根据类别添加标签;可以根据用户如何在线讨论某个产品或品牌将其分为多个群组......
然而,互联网上绝大多数的文本分类文章和教程都是二分类,比如垃圾邮件过滤,情感分析。大多数情况下,现实世界的问题更为复杂。
这就是我们今天要做的事情:将爬过来的36kr网站的新闻分为 11 个预定义的类别。使用Python 并借助 Scikit-Learn 来完成。
问题表述
该问题是监督式文本分类问题,我们的目标是调查哪种监督式机器学习方法最适合解决它。
对于一个新的新闻,我们希望将其分配到 11 个类别中的一个。分类器假设每个新闻都被分配到(当且仅当) 一个类别之中。这是典型的多类别文本分类问题。看看我们能实现什么!
2、数据分析
数据探索
在深入训练机器学习模型之前,我们先研究下我们的数据长什么样子,看一些实例,以及每个类别的新闻和标题:
本文的这些记录是用jupyter notebook 来完成的。
%matplotlib inline
import pandas as pd
import matplotlib
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号
import jieba as jb
import re
df = pd.read_csv('./data/36kr20200506.csv')
df.head()
对于这个项目,我们只需要三栏——['category', 'content','title'],因为标题和内容都是我们需要的,我们后边会把他们合在一起作为一个新的content
输入: content+title
实例:「编者按:本文来自微信公众号“大摩财经”(ID:damofinance),撰文 | 蔚然,36... 」 这里有省略
输出:category
实例:金融
from io import StringIO
col = ['category', 'content','title']
df = df[col]
df = df[pd.notnull(df['content'])]
df.columns = ['category', 'content','title']
df.head()
[图片上传失败...(image-93def3-1589098206119)]
不平衡类
我们来一下各个类别的数量
import matplotlib.pyplot as plt
import matplotlib
fig = plt.figure(figsize=(8,6))
plt.title("类目数量分布")
plt.ylabel('数量', fontsize=18)
df.groupby('category').content.count().plot.bar(ylim=0)
plt.xlabel('类目', fontsize=18)
plt.show()
[图片上传失败...(image-31d9ca-1589098206119)]
显然,各个类别的数量不一致。这是个问题。
当我们遇到这样的问题时,我们使用标准算法解决这些问题必然会遇到困难。常规算法往往偏向于多数类别,而不考虑数据分布。在最糟糕的情况下,少数类别被视为异常值并被忽略。对于某些情况,如欺诈检测或癌症预测,我们则需要仔细配置我们的模型或人为地平衡数据集,比如欠采样或过采样每个类别。
但是,在学习不平衡数据的情况下,我们最感兴趣的是多数类。我们想有一个分类器,能够对多数类提供较高的预测精度,同时对少数类保持合理的准确度。因此我们会保持原样。简单说,这个问题对我们的目标影响不大。
特征向量
分类器和学习算法不能直接处理原始文本文档,因为它们大多数都期望大小固定的数字特征向量,而原始文本是可变长度的。因此,在预处理步骤中,文本需要被转换为大小固定的数字特征向量,即向量。
将类别转化为向量
首先,我们将删除「content」栏中的缺失值,并添加一列来将产品编码为整数,即实现了将类别 从文本变成了向量
我们还创建了几个字典供将来使用。
df['category_id'] = df['category'].factorize()[0]
# 几个字典
category_id_df = df[['category', 'category_id']].drop_duplicates().sort_values('category_id')
category_to_id = dict(category_id_df.values)
id_to_category = dict(category_id_df[['category_id', 'category']].values)
df.head()
新闻的数据清洗和分词
我们的文本评价内容都是中文, 中文的一个显著特点是必须分词断句,否则计算机读不懂。这还包括删除文本中的标点符号,特殊符号,还要删除一些无意义的常用词(stopword), 因为这些词和符号对系统分析预测文本的内容没有任何帮助, 反而会增加计算的复杂度和增加系统开销, 所有在使用这些文本数据之前必须要将它们清理干净。特别,针对36kr新闻的 新闻头和尾,有一些无意义的法律声明、作者声明之类的内容,也需要清洗。
可以在这里下载中文停用词
定义几个函数
#定义删除除字母,数字,汉字以外的所有符号的函数
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()}
remove_chars = '[·’!"\#$%&\'()#!()*+,-./:;<=>?\@,:?¥★、….>【】[]《》?“”‘’\[\\]^_`{|}~]+'
for x in remove_chars:
stopwords.add(x)
stopwords.add('xa0')
stopwords.add('Xa0')
stopwords.add('请点')
return stopwords
#加载停用词
stopwords = stopwordslist("./data/chineseStopWords.txt")
def startswith_bian_zhe_an(line):
tezheng = [
"编者按",
"文",
"编辑",
"本文作者",
"分析师 ",
"来源",
"作者",
"口述",
"采访",
"神译局",
"策划",
"视觉",
]
for guize in tezheng:
if line.startswith(guize):
return True
else:
pass
return False
def touzhong_shenming(line):
tezheng = [
"投中研究院隶属于投中信息",
"投中信息是一家领先的中国股权投资市场专业服务机构",
"法律声明",
"本报告为上海投中信息咨询股份有限公司(以下简称投中信息)制作",
"本报告包含的所有内容(包括但不限于文本、数据、图片、图标、LOGO等)的所有权归属投中信息",
"免责声明",
"关于作者",
"————",
"推荐阅读",
"译者",
"作者",
"编辑",
]
for guize in tezheng:
if line.startswith(guize):
return True
else:
pass
return False
def handel(text):
array = text.splitlines()
result_array = []
# 前3行
count = 0
# 是否是 前6行
is_qian_6 = False
# 是否是 后6行
is_hou_6 = False
for x in array:
count +=1
if count<=6:
is_qian_6 = True
else:
is_qian_6 = False
if len(array)>=6 and len(array)-count<=6:
is_hou_6 = True
else:
is_hou_6 = False
x = x.strip()
if len(x.strip()) ==0 or (is_qian_6 and startswith_bian_zhe_an(x)):
continue
elif is_hou_6 and touzhong_shenming(x):
break
else:
result_array.append(x)
return ''.join(result_array)
然后用结巴分词进行分词,分词后,得到cut_content是我们分好词之后的内容(空格分开各个词)
#进度条
from tqdm import tqdm
tqdm.pandas()
#分词,并过滤停用词
# df['cut_content'] = df['content'].apply(lambda x: " ".join([w for w in list(jb.cut(x)) if w not in stopwords]))
#带进度条的形式 不带进度条,将progress_apply 改为apply
df['clean_content'] = df['title'].progress_apply(lambda x: handel(x)) + df['content'].progress_apply(lambda x: handel(x))
df['cut_content'] = df['clean_content'].progress_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='/root/anaconda3/envs/sklearn/lib/python3.7/site-packages/matplotlib/mpl-data/fonts/ttf/simhei.ttf',
max_words=50, max_font_size=40,
random_state=42
).generate(str(tup))
return wordcloud
category_desc = dict()
for category in category_id_df.category.values:
text = df.loc[df['category']==category, 'cut_content']
text = (' '.join(map(str,text))).split(' ')
category_desc[category]=text
fig,axes = plt.subplots(5, 2, figsize=(30, 38))
k=0
for i in range(5):
for j in range(2):
category = id_to_category[k]
most100=Counter(category_desc[category]).most_common(100)
ax = axes[i, j]
ax.imshow(generate_wordcloud(most100), interpolation="bilinear")
ax.axis('off')
ax.set_title("{} Top 100".format(category), fontsize=30)
k+=1
[图片上传失败...(image-bebe6c-1589098206119)]
从上图我们看到,金融中的关键字:
公司、企业、小微、发展,这些都还可以,基本说的通。但是有一些词是不合理的,例如 更、日,等这些词压根没什么意义。不能代表这个分类的主题,所以我们单单用词频来进行向量化,是有问题的。
注:有一些噪音词,通过分析,我们在停用词中过滤掉了:"xa0","请点"
tfidf 进行 特征提取
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的特征值。
TfidfVectorizer 的一些参数:
- sublinear_df 设为 True 表示使用频率的对数形式
- min_df 是单词必须存在的最小文档数量
- norm 设为 'l2', 是一种数据标准划处理的方式,可以将数据限制在 一定范围内(-1,1),以确保我们所有特征向量的欧几里德范数为 1
- ngram_range 设为 (1, 2),表示考虑 unigrams 和 bigrams。
- 即,我们除了抽取新闻中的每个词语外,还要抽取每个词相邻的词并组成一个“词语对”。
- 例如原始词是4个词(词1 词2 词3 词4),得到的结果是: 词1,词2,词3,词4,(词1,词2),(词2,词3),(词3,词4)
- 这样就扩展了我们特征集的数量, 有了丰富的特征集才有可能提高我们分类文本的准确度。
接下来我们要计算cut_content的 TF-IDF的特征值
from sklearn.feature_extraction.text import TfidfVectorizer
all_tfidf = TfidfVectorizer(sublinear_tf=True,min_df=5,norm='l2', ngram_range=(1, 2))
all_features = all_tfidf.fit_transform(df.cut_content)
labels = df.category_id
print(all_features.shape)
print('-----------------------------')
print(features)
多说一句,我们还可以用两个接口进行tfidf的计算,先进行词频计算,生成词频向量,再进一步对词频进行tfidf计算,生成TF-IDF向量。
即下面的形式
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
count_vect = CountVectorizer()
features_train_counts = count_vect.fit_transform(df.cut_content)
tfidf_transformer = TfidfTransformer()
features = tfidf_transformer.fit_transform(features_train_counts)
作者习惯用TfidfVectorizer一步进行,所以下面涉及到的,统统都使用TfidfVectorizer
卡方检验
我们看到features的维度是(7088, 3501154),这里的7088表示我们共有7088条新闻数据,3501154表示我们的特征数量这包括全部新闻中的所有词语数+词语对(相邻两个单词的组合)的总数。下面我们要是卡方检验的方法来找出每个分类中关联度最大的两个词语和两个词语对。卡方检验是一种统计学的工具,用来检验数据的拟合度和关联度。这里我们使用sklearn中的chi2方法。
from sklearn.feature_selection import chi2
import numpy as np
N = 2
for cat, cat_id in sorted(category_to_id.items()):
features_chi2 = chi2(all_features, labels == cat_id)
indices = np.argsort(features_chi2[0])
feature_names = np.array(all_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)检验后,找出了每个分类中关联度最强的两个词和两个词语对。这些词和词语对能很好的反映出分类的主题。
所以,我们以上的tfidf抽取的特征,是没有问题的。可以使用其作为特征向量。
多类别分类器:特征和设计
为了训练监督分类器,我们首先将「36kr新闻」转化为数字向量。我们研究了向量表示, TF-IDF 加权向量。
有了这个向量表达的文本后,我们可以训练监督式分类器来训练看不到的「36kr新闻」并预测它们的「类别」。
在完成上述数据转换之后,现在我们拥有所有的特征,是时候训练分类器了。我们可以使用很多算法来解决这类问题。
3、真正开始数据训练
从朴素贝叶斯 入手
朴素贝叶斯分类器:最适合用于基于词频的高维数据分类器,最典型的应用如垃圾邮件分类器等,准确率可以高达95%以上。现在,我们就用利用sklean进行朴素贝叶斯分类器 训练:MultinomialNB
用贝叶斯来一次尝试
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import TfidfVectorizer
X_train, X_test, y_train, y_test = train_test_split(df['cut_content'], df['category_id'], test_size=0.25)
# 注意这个(fit_transform后的)tfidf 非常有用,在预测方法中要用,如果训练和预测不在一段代码中,应该持久化,以便后期预测时从文件中加载
tfidf = TfidfVectorizer(sublinear_tf=True,min_df=5,norm='l2', ngram_range=(1, 2))
X_train = tfidf.fit_transform(X_train)
clf = MultinomialNB().fit(X_train, y_train)
# 预测的代码
def myPredict(sec):
format_sec=" ".join([w for w in list(jb.cut(remove_punctuation(handle(sec)))) if w not in stopwords])
# tfidf 是训练中用到的,不能自行初始化
pred_cat_id=clf.predict(tfidf.transform([format_sec]))
#print(pred_cat_id)
print(id_to_category[pred_cat_id[0]])
例子:
# 中概股
a="""
去年年底,微盟曾密集回购45次。
5月7日,电商SaaS企业微盟集团(02013.HK)发布早间公告称,5月6日,公司及其全资附属公司Weimob Investment Limited、经办人三方已订立债券认购协议,微盟为担保人,Weimob Investment Limited为债券发行人,经办人为认购人,包括瑞信、摩根士丹利、中金公司,债券拟在香港联交所上市。
据悉,该债券为本金总额1.5亿美元于2025年到期的美元计值1.50%有担保可换股债券。初步换股价为每股6.72港元,较5月6日收市价5.95港元溢价约12.94%。
微盟成立于2013年,2019年在港交所主板上市,是一家SaaS产品和精准营销服务提供商,腾讯是其第二大股东和重要的合作伙伴。
若按照初步换股价6.72港元悉数兑换债券,可兑换最多1.73亿股新股,占公司现有已发行股本约7.73%,占经根据债券配发及发行新股扩大后公司已发行股本约7.18%。兑换后,孙涛勇、方桐舒及游凤椿作为一致行动人占公司现有已发行股本下降1.03个百分点至13.32%,腾讯仍为其第二大股东,占公司现有已发行股本下降0.58个百分点至7.5%。
这是微盟上市以来第二次公开筹资。
微盟表示,过去12个月内,公司仅于2019年7月26日通过新股配售筹资11.57亿港元,截至3月31日,约5920万港元已按计划动用,公司拟于2021年12月31日前悉数动用剩余款项。
而本次筹资净额约为1.466亿美元(约合11.36亿港元),将被用于提升综合研发实力(主要包括购置硬件设备及支付雇员薪酬)、升级营销系统、建立产业基金、补充营运资金及一般公司用途。
值得注意的是,去年10-12月间,微盟密集回购达45次,斥资约1亿港元,累计购回约2799万股,占已发行股份数约1.39%。
另据微盟2019年财报,去年公司营收同比上涨66.1%至14.36亿元,实现转亏为盈,净利润为3.31亿元,去年亏损10.94亿元。
财报发布当天,微盟股价止跌收涨近6%,之后股价走势一路向上。此次筹资消息公布后,今日港股开盘,微盟微跌,截至发稿,跌3.53%至5.74港元。
原创文章,作者:宋子乔。转载或内容合作请点击 转载说明 ,违规转载法律必究。
寻求报道,请 点击这里 。
本文图片来自:Pexels 正版图库
"""
myPredict(a)
不是太寒酸!
模型选择
我们现在准备尝试不同的机器学习模型,评估它们的准确性并找出潜在问题的根源。
我们将对以下四种模型进行基准测试:
- 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
import time
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__
print(model_name,'开始训练')
start = time.process_time() #记下开始时刻
accuracies = cross_val_score(model, all_features, labels, scoring='accuracy', cv=CV)
print(model_name,'训练完成,用时', time.process_time()-start)
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()
从可以箱体图上可以看出随机森林分类器的准确率是最低的,因为随机森林属于集成分类器(有若干个子分类器组合而成),一般来说集成分类器不适合处理高维数据(如文本数据),因为文本数据有太多的特征值,使得集成分类器难以应付,另外三个分类器的平均准确率都在60%以上。其中线性支持向量机的准确率最高。
cv_df.groupby('model_name').accuracy.mean()
我们看到线性支持向量机的平均准确率达到了74.8%,其次是逻辑回归和朴素贝叶斯。
4、模型评估
下面我们就针对平均准确率最高的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(all_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=category_id_df.category.values, yticklabels=category_id_df.category.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=category_id_df['category'].values))
从以上F1分数上看,"骑车"类的F1分数最大 ,其次是“房产”,“创投”类F1分数最差只有57%,究其原因可能是因为“创投”这个类别不是很清晰,跟其他分类有交叉导致的。
下面我们来查看一些预测失误的例子,希望大家能从中发现一些奥秘,来改善我们的分类器。
from IPython.display import display
for predicted in category_id_df.category_id:
for actual in category_id_df.category_id:
if predicted != actual and conf_mat[actual, predicted] >= 6:
print("{} 预测为 {} : {} 例.".format(id_to_category[actual], id_to_category[predicted], conf_mat[actual, predicted]))
display(df.loc[indices_test[(y_test == actual) & (y_pred == predicted)]][['category', 'clean_content']])
print('')
如你所见,一些错误分类的投诉涉及多个主题(比如涉及房产和创投)。这种错误总是发生。
再次,我们使用卡方检验来找到与每个类别最相关的项:
tfidf = TfidfVectorizer(sublinear_tf=True,min_df=5,norm='l2', ngram_range=(1, 2))
features = tfidf.fit_transform(df.cut_content)
labels = df.category_id
model.fit(features, labels)
N = 2
for category, category_id in sorted(category_to_id.items()):
indices = np.argsort(model.coef_[category_id])
feature_names = np.array(tfidf.get_feature_names())[indices]
unigrams = [v for v in reversed(feature_names) if len(v.split(' ')) == 1][:N]
bigrams = [v for v in reversed(feature_names) if len(v.split(' ')) == 2][:N]
print("# '{}':".format(category))
print(" . Top unigrams:\n . {}".format('\n . '.join(unigrams)))
print(" . Top bigrams:\n . {}".format('\n . '.join(bigrams)))
5、完整实现
单独写一篇
参考
https://www.jiqizhixin.com/articles/2018-03-05-3
https://blog.csdn.net/weixin_42608414/article/details/88046380