基于机器学习的新闻文本分类

Task1-数据探索分析

数据存储

由于用pandas一次性读取20w条数据显示memoryerror,内存不够,所以想到把数据存到数据库中,随用随取比较简便。把训练集20w条数据存到了mongodb数据库中。

import pandas as pd
import pymongo

df = pd.read_csv(r'D:\Datawhale学习资料\15期-NLP新闻文本分类\data\train_set.csv', sep='\t')
texts_num = len(df.index)  # 计算出新闻文本的总数量
print(df.columns)  # 读取到训练集的列名['label','text']

texts_arr = []
for i in range(texts_num):
    dict1 = dict()
    dict1['label'] = int(df.iloc[i, 0])
    dict1['text'] = str(df.iloc[i, 1])
    texts_arr.append(dict1)
print(len(texts_arr))  # 验证arr的准确性
# print(texts_arr[:5])

# 建立mongodb数据库
client = pymongo.MongoClient('localhost', 27017)
db = client['newsTextClassification']
table = db['train_set']

table.insert(texts_arr)
print(table.find().count())
# 关闭连接
client.close()

mongodb的数据操作详见资料,这里使用insert函数一次性把20w条数据添加到集合train_set中,注意texts_arr是列表,元素是字典型数据。

数据探索

新闻类别分布

统计新闻文本类别的分布,探究数据集的均衡性:

labels_iterator = table.find({}, {'label': 1, '_id': 0})
labels = map(lambda x: int(x['label']), labels_iterator)
labels_series = pd.Series(list(labels))
# print(texts_series.describe())  # 输出describe信息
label_counts = labels_series.groupby(labels_series.values).count()  # 对series对象进行分组统计

技术知识:

  1. mongodb数据库的find()函数,1)第一个参数是“where”,第二个参数是显示哪些标签,注意除了“_id”,剩下的标签不能是相反的数值。2)本例中返回的是游标对象Cursor,是迭代器,元素是字典型数据。
  2. map()函数生成的对象是map对象,为迭代器。
  3. describe()函数针对的是Series对象和DataFrame对象,np.array数组没有describe()函数;
  4. Series对象的groupby()函数,参数需要指定为Series对象的数值

并结合pyecharts库可视化展现类别分布情况:

from pyecharts.charts import Bar, Line
from pyecharts import options as opts
b = (
    Bar()
        .add_xaxis(list(label_counts.index))
        .add_yaxis("新闻文本类别数量", list(map(lambda x: int(x), label_counts.values)), bar_width=20)
        # .set_series_opts(label_opts=opts.LabelOpts(is_show=False))
        # .set_global_opts(
        #     xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=-15)),
        #     title_opts=opts.TitleOpts(title="Bar-旋转X轴标签", subtitle="解决标签名字过长的问题"),
        .render("label_counts_graph.html")
)

注意,pyecharts库更新后,采用一键式制作图表,详细内容见中文文档:


从上图的统计结果可以看到,第0、1、2类占了大部分,这就很容易对这三类的数据过拟合,而其他类会欠拟合,是一个典型的非均衡数据集,在后续模型训练中需要考虑分类标签的权重,或者训练时采用分层抽样的方法,进行K折交叉验证,以最大程度消除局部标签过拟合的问题。

新闻文本长度(字符数目)分布

# 统计新闻文本长度分布
texts_iterator = table.find({}, {'text': 1, '_id': 0})
texts_length = map(lambda x: len(x['text'].split(' ')), texts_iterator)
texts_series = pd.Series(list(texts_length))
# print(texts_series.describe())  # 输出describe信息
length_counts = texts_series.groupby(texts_series.values).count()  # 对series对象进行分组统计


知识:

  1. 有超长文本(5万字符以上),但这部分数据很少,所以可以通过截取的方法来进行处理。
  2. 中位数是676个字符,平均数是907个字符;但这是未处理过的文本长度,处理后会进一步缩减(几百个字符实在太多了,BERT最多才能处理512个字符的长度,所以要经过去停留词、去高频词等方法来减少字符)。

并结合pyecharts库制作条形图和文本数目累计占比折线图,为后续构建td-idf和词向量事设置max_feature参数提供参考:

可以看到95%以上的新闻文本长度都在3000词以内,所以大致可以限制在3000以内(在处理高频词和低频词等停用词后再进行统计会更精准)

统计每类新闻中出现最多的字符(作业2)

标点符号筛选

由于对新闻文本做了匿名处理,无法直接判断出标点符号等停用词,只能通过高文本覆盖率来筛选出可能是标点符号的字符。
Tips:对于本项目的停用词来说,应包含两部分,一是高覆盖率的字符,二是超低频次的字符,前者主要指标点符号,后者主要指无价值字符。
所以在统计出现最多的字符之前,先筛选出高覆盖率的字符,这里覆盖率从80%~100%依次进行探索:

from collection import Counter
texts_iterator = table.find({}, {'text': 1, '_id': 0})
texts_unique = list(map(lambda x: ' '.join(list(set(x['text'].split(' ')))), texts_iterator))
unique_allines = ' '.join(texts_unique)
word_count = Counter(unique_allines.split(' '))
# print(len(word_count))  # 共有6869个不重复字符
word_count = sorted(word_count.items(), key=lambda d: int(d[1]), reverse=True)
example = range(80, 100)
dict1 = {}
for i in example:
    stopwords = []
    for word in word_count:
        if word[1] >= int(0.01 * i * 200000):
            stopwords.append(word[0])
        else:
            break
    dict1['{}%'.format(i)] = len(stopwords)

技术知识:

  1. set()函数可以直接去重得到无重复的元素集合。
  2. Count()函数返回的是Counter对象,类似于字典型数据。详细内容见资料:
  3. sorted()函数的参数设置详细见资料:

并结合pyecharts库制作折线变化图:

index = [x for x in dict1.keys()]
num = [x for x in dict1.values()]
line2 = (
    Line()
        .add_xaxis(index)
        .add_yaxis(
        series_name="字符个数与文本覆盖率的关系",
        y_axis=num,
        label_opts=opts.LabelOpts(is_show=False),
    )
        .set_global_opts(
        xaxis_opts=opts.AxisOpts(type_="category"),
        yaxis_opts=opts.AxisOpts(name='字符个数', type_='value')
    )
)


可以看到覆盖率在90%以上时只有2~3个字符,并且保持了比较高的平稳性,所以选择超过90%覆盖率的这三个字符作为标点符号,分别是‘3750’、‘900’、‘648’。
知识点:其实筛选标点符号,不能只关注文本覆盖率,也要关注频次,即采用tf-idf的方法衡量字符的重要性,从重要性的角度来筛选标点符号

统计字符数目
def mostcommon_cate(cate):
    iterator = table.find({'label': cate})
    texts_cate = map(lambda x: x['text'], iterator)
    texts_str = ' '.join(list(texts_cate)).replace(' 3750', '').replace(' 900', '').replace(' 648', '')  # 去掉停用词
    mostcommon_word = Counter(texts_str.split(' ')).most_common(1)

    return cate, mostcommon_word


index = [int(x) for x in np.arange(14)]  # 这里需要进行强制转换类型,将numpy.int64转变成int型,因为pymongo读取不能读取numpy对象
result = map(mostcommon_cate, index)
for i in result:
    print(i[0], i[1][0])

得到结果如下:

统计新闻文本句子数量分布(作业1)

根据假定的标点符号(通过作业2中的覆盖率探索,也可以判断出假定的三个标点符号具有较高的准确度),对新闻文本进行分割:

texts_iterator = table.find({}, {'text': 1, '_id': 0})  # 疑问:为什么当这里用上面的texts_iterator就取不到数了?
texts_map = map(lambda x: x['text'], texts_iterator)


# 假定字符3750、900、648是标点符号
def sentence_count(text_str):
    sentence_list = re.split(' 3750 | 900 | 648 ', text_str)
    count = len(sentence_list)
    return count


sentence_counts = list(map(sentence_count, texts_map))
sentence_count_series = pd.Series(sentence_counts)
print(sentence_count_series.describe())
sentence_counts_distru = sentence_count_series.groupby(sentence_count_series.values).count()

技术知识:

  1. 从mongodb数据库中采用find()函数取出数据后,得到的是Cursor迭代器对象,如果对Cursor进行操作后,在后续二次操作时,需要重新取数,否则数据为0??
  2. 单纯的str.split()函数中,只能有1个分割符号,所以采用re.split()函数,用‘|’把多个分隔符号隔开即可。

并结合pyecharts库进行可视化:

可以看到98%左右的新闻文本句子数目都在300以内。

Task 2 基于机器学习的文本分类

1 文本向量化

首先从mongodb数据库中读取出20w条训练数据集,采用TfidfVectorizer对文档进行向量化表示,代码如下:

client = pymongo.MongoClient('localhost', 27017)
db = client['newsTextClassification']
table = db['train_set']
data_all = table.find({}, {"_id": 0})
data_df = pd.DataFrame(data_all)
X_data = data_df.text
tf_idf = TfidfVectorizer(ngram_range=(1, 3), max_features=2000)
X = tf_idf.fit_transform(X_data)

注意返回的X是一个稀疏矩阵,在后续训练集拆分时需要用X.toarray()转换为数组来运算。

为了防止每次tfidf向量化耗费大量时间,将训练好的tfidf模型保存在本地,采用pickle方法:

# 把训练好的tfidf保存到本地
with open('tfidf_word.pkl', 'wb') as f:
    pickle.dump(X, f)
# 把本地保存的tfidf载入
X = pickle.load(open('tfidf_word.pkl', "rb"))

2 训练集拆分

这里使用了K折交叉验证的方法对训练集进行拆分,并且注意到训练集中的新闻文本是一个高度不均衡的数据集,所以在拆分时想要将训练集和验证集按照源文本中的类别比例划分,这里采用StratifiedKFold方法,来进行分层抽样:

# 训练集:验证集=4:1,并且进行5次模型训练
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)  
X = X.toarray()  # 注意需要强制转换一下
for train_index, verify_index in skf.split(X, y):
    X_train, y_train = X[train_index], y[train_index]
    X_verify, y_verify = X[verify_index], y[verify_index]  

朴素贝叶斯文本分类

直接采用sklearn库中的多项式朴素贝叶斯模型进行文本分类:

from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import f1_score

clf = MultinomialNB(fit_prior=True)
clf.fit(X_train, y_train)
val_pred = clf.predict(X_verify)
f1_res = f1_score(y_verify, val_pred, average='macro')

由此得到了f1值,因为做了K折交叉验证,所以要运行5次,最后得到f1的平均值。

结果如下:

五次的f1值分别为:0.831、0.833、0.828、0.829、0.833
平均f1值为0.831.

  • 6
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值