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对象进行分组统计
技术知识:
- mongodb数据库的find()函数,1)第一个参数是“where”,第二个参数是显示哪些标签,注意除了“_id”,剩下的标签不能是相反的数值。2)本例中返回的是游标对象Cursor,是迭代器,元素是字典型数据。
- map()函数生成的对象是map对象,为迭代器。
- describe()函数针对的是Series对象和DataFrame对象,np.array数组没有describe()函数;
- 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对象进行分组统计
知识:
- 有超长文本(5万字符以上),但这部分数据很少,所以可以通过截取的方法来进行处理。
- 中位数是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)
技术知识:
- set()函数可以直接去重得到无重复的元素集合。
- Count()函数返回的是Counter对象,类似于字典型数据。详细内容见资料:
- 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()
技术知识:
- 从mongodb数据库中采用find()函数取出数据后,得到的是Cursor迭代器对象,如果对Cursor进行操作后,在后续二次操作时,需要重新取数,否则数据为0??
- 单纯的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.