赛题数据为新闻文本,并按照字符级别进行匿名处理。整合划分出14个候选分类类别:财经、彩票、房产、股票、家居、教育、科技、社会、时尚、时政、体育、星座、游戏、娱乐的文本数据。
赛题任务:赛题以自然语言处理为背景,要求选手对新闻文本进行分类,这是一个典型的字符识别问题
赛题数据由以下几个部分构成:训练集20w条样本,测试集A包括5w条样本,测试集B包括5w条样本。为了预防选手人工标注测试集的情况,我们将比赛数据的文本按照字符级别进行了匿名处理。处理后的赛题训练数据如下:
label | text |
---|---|
6 | 57 44 66 56 2 3 3 37 5 41 9 57 44 47 45 33 13 63 58 31 17 47 0 1 1 69 26 60 62 15 21 12 49 18 38 20 50 23 57 44 45 33 25 28 47 22 52 35 30 14 24 69 54 7 48 19 11 51 16 43 26 34 53 27 64 8 4 42 36 46 65 69 29 39 15 37 57 44 45 33 69 54 7 25 40 35 30 66 56 47 55 69 61 10 60 42 36 46 65 37 5 41 32 67 6 59 47 0 1 1 68 |
在数据集中标签的对应的关系如下:
{'科技': 0, '股票': 1, '体育': 2, '娱乐': 3, '时政': 4, '社会': 5, '教育': 6, '财经': 7, '家居': 8, '游戏': 9, '房产': 10, '时尚': 11, '彩票': 12, '星座': 13}
赛题数据来源为互联网上的新闻,通过收集并匿名处理得到。因此选手可以自行进行数据分析,可以充分发挥自己的特长来完成各种特征工程,不限制使用任何外部数据和模型。
数据列使用\t进行分割,Pandas读取数据的代码如下:
train_df = pd.read_csv('../input/train_set.csv', sep='\t')
评价标准为类别f1_score的均值,选手提交结果与实际测试集的类别进行对比,结果越大越好。
可以通过sklearn完成f1_score计算:
from sklearn.metrics import f1_score
y_true = [0, 1, 2, 0, 1, 2]
y_pred = [0, 2, 1, 0, 0, 1]
f1_score(y_true, y_pred, average='macro')
赛题思路分析:赛题本质是一个文本分类问题,需要根据每句的字符进行分类。但赛题给出的数据是匿名化的,不能直接使用中文分词等操作,这个是赛题的难点。
因此本次赛题的难点是需要对匿名字符进行建模,进而完成文本分类的过程。由于文本数据是一种典型的非结构化数据,因此可能涉及到特征提取
和分类模型
两个部分。为了减低参赛难度,我们提供了一些解题思路供大家参考:
直接使用TF-IDF对文本提取特征,并使用分类器进行分类。在分类器的选择上,可以使用SVM、LR、或者XGBoost。
FastText是入门款的词向量,利用Facebook提供的FastText工具,可以快速构建出分类器。
WordVec是进阶款的词向量,并通过构建深度学习分类完成分类。深度学习分类的网络结构可以选择TextCNN、TextRNN或者BiLSTM。
Bert是高配款的词向量,具有强大的建模学习能力。
这里的One-hot与数据挖掘任务中的操作是一致的,即将每一个单词使用一个离散的向量表示。具体将每个字/词编码一个索引,然后根据索引进行赋值。
One-hot表示方法的例子如下:
句子1:我 爱 北 京 天 安 门
句子2:我 喜 欢 上 海
首先对所有句子的字进行索引,即将每个字确定一个编号:
{
'我': 1, '爱': 2, '北': 3, '京': 4, '天': 5,
'安': 6, '门': 7, '喜': 8, '欢': 9, '上': 10, '海': 11
}
在这里共包括11个字,因此每个字可以转换为一个11维度稀疏向量:
我:[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
爱:[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
...
海:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
Bag of Words(词袋表示),也称为Count Vectors,每个文档的字/词可以使用其出现次数来进行表示。
句子1:我 爱 北 京 天 安 门
句子2:我 喜 欢 上 海
直接统计每个字出现的次数,并进行赋值:
句子1:我 爱 北 京 天 安 门
转换为 [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0]
句子2:我 喜 欢 上 海
转换为 [1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1]
在sklearn中可以直接CountVectorizer
来实现这一步骤:
from sklearn.feature_extraction.text import CountVectorizer
corpus = [
'This is the first document.',
'This document is the second document.',
'And this is the third one.',
'Is this the first document?',
]
vectorizer = CountVectorizer()
vectorizer.fit_transform(corpus).toarray()
N-gram与Count Vectors类似,不过加入了相邻单词组合成为新的单词,并进行计数。
如果N取值为2,则句子1和句子2就变为:
句子1:我爱 爱北 北京 京天 天安 安门
句子2:我喜 喜欢 欢上 上海
TF-IDF 分数由两部分组成:第一部分是词语频率(Term Frequency),第二部分是逆文档频率(Inverse Document Frequency)。其中计算语料库中文档总数除以含有该词语的文档数量,然后再取对数就是逆文档频率。
TF(t)= 该词语在当前文档出现的次数 / 当前文档中词语的总数
IDF(t)= log_e(文档总数 / 出现该词语的文档总数)
jupyter notebook代码如下:
import pandas as pd
import numpy as np
import os
from collections import Counter
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import RidgeClassifier
from sklearn.metrics import f1_score
os.getcwd()
# 读取数据
path='/Users/weiyi/Applications/jupyter_work/ml/datasets/news_text_classification/'
train = pd.read_csv(path+'train_set.csv', sep='\t')
test = pd.read_csv(path+'test_a.csv', sep='\t')
# 1.数据分析
此步骤我们读取了所有的训练集数据,在此我们通过数据分析希望得出以下结论:
赛题数据中,新闻文本的长度是多少?
赛题数据的类别分布是怎么样的,哪些类别比较多?
赛题数据中,字符分布是怎么样的?
# 数据探索
train.head()
test.head()
### 1.1句子长度分析
在赛题数据中每行句子的字符使用空格进行隔开,所以可以直接统计单词的个数来得到每个句子的长度。统计并如下:
%pylab inline
train['text_len'] = train['text'].apply(lambda x: len(x.split(' ')))
print(train['text_len'].describe())
对新闻句子的统计可以得出,本次赛题给定的文本比较长,每个句子平均由907个字符构成,最短的句子长度为2,最长的句子长度为57921。
下图将句子长度绘制了直方图,可见大部分句子的长度都几乎在2000以内。
_ = plt.hist(train['text_len'], bins=200)
plt.xlabel('Text length')
plt.title("Histogram of text length")
plt.xlim(-0.5,7000) # x轴度量范围
### 1.2新闻类别分布
接下来可以对数据集的类别进行分布统计,具体统计每类新闻的样本个数。
train['label'].value_counts().plot(kind='bar')
plt.title('News class count')
plt.xlabel("category")
在数据集中标签的对应的关系如下:{'科技': 0, '股票': 1, '体育': 2, '娱乐': 3, '时政': 4, '社会': 5, '教育': 6, '财经': 7, '家居': 8, '游戏': 9, '房产': 10, '时尚': 11, '彩票': 12, '星座': 13}
从统计结果可以看出,赛题的数据集类别分布存在较为不均匀的情况。在训练集中科技类新闻最多,其次是股票类新闻,最少的新闻是星座新闻。
### 1.3字符分布统计
接下来可以统计每个字符出现的次数,首先可以将训练集中所有的句子进行拼接进而划分为字符,并统计每个字符的个数。
从统计结果中可以看出,在训练集中总共包括6869个字,其中编号3750的字出现的次数最多,编号3133的字出现的次数最少。
all_lines = ' '.join(list(train['text']))
word_count = Counter(all_lines.split(" "))
word_count
# dict.items() 返回可遍历的(键, 值) 元组数组
# key -- 主要是用来进行比较的元素,只有一个参数,具体的函数的参数就是取自于可迭代对象中,指定可迭代对象中的一个元素来进行排序。
word_count = sorted(word_count.items(), key=lambda x:x[1], reverse = True) # reverse = True降序
print(len(word_count))
print(word_count[0])
print(word_count[-1])
word_count
这里还可以根据字在每个句子的出现情况,反推出标点符号。
下面代码统计了不同字符在句子中出现的次数(新闻次数),其中字符3750,字符900和字符648在20w条新闻的覆盖率接近99%,很有可能是标点符号。
train['text_unique'] = train['text'].apply(lambda x: ' '.join(list(set(x.split(' ')))))
all_lines = ' '.join(list(train['text_unique']))
word_count = Counter(all_lines.split(" "))
word_count = sorted(word_count.items(), key=lambda d:int(d[1]), reverse = True)
print(word_count[0])
print(word_count[1])
print(word_count[2])
# 问题1: 假设字符900是句号,分析赛题每篇新闻平均由多少个句子构成? len() 返回元素个数或字符长度
train['text_juzilen'] = train['text'].apply(lambda x:len(x.split(' 900 ')))
train['text_juzilen'].mean()
# 问题2: 统计每类新闻中出现次数最多的字符
label_text = train.groupby('label')['text'].apply(lambda x: ' '.join(list(x)))
# 聚合之后为series,不能进行split操作,先转datafram
new_label_text = label_text.reset_index()
new_label_text
label_word_count = new_label_text['text'].apply(lambda x:Counter(x.split(' '))) # Series
# label_word_count = sorted(label_word_count.items(), key=lambda x:x[1], reverse = True)
# label_word_count
### 数据分析的结论
通过上述分析我们可以得出以下结论:
赛题中每个新闻包含的字符个数平均为1000个,还有一些新闻字符较长;
赛题中新闻类别分布不均匀,科技类新闻样本量接近4w,星座类新闻样本量不到1k;
赛题总共包括7000-8000个字符;
通过数据分析,我们还可以得出以下结论:
每个新闻平均字符个数较多,可能需要截断;
由于类别不均衡,会严重影响模型的精度;
# 2.模型预测
# 方法1:
# ngram_range: tuple 有时候我们觉得单个的词语作为特征还不足够,能够加入一些词组更好,就可以设置这个参数,如(1,2)下面允许词表使用1个词语,或者2个词语的组合
# max_feature: int 在大规模语料上训练TFIDF会得到非常多的词语,如果再使用了上一个设置加入了词组,那么我们词表的大小就会爆炸。出于时间和空间效率的考虑,可以限制最多使用多少个词语,模型会优先选取词频高的词语留下
tfidf = TfidfVectorizer(ngram_range=(1,3), max_features=3000)
train_test = tfidf.fit_transform(train['text'])
clf = RidgeClassifier()
clf.fit(train_test[:10000], train['label'].values[:10000])
val_pred = clf.predict(train_test[10000:])
print(f1_score(train['label'].values[10000:], val_pred, average='macro'))
# 方法2: (利用网格搜索)
tfidf = TfidfVectorizer(ngram_range=(1,3), max_features=3000)
X_train = tfidf.fit_transform(train['text'])
# 训练集、验证集划分
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X_train, train['label'], test_size=0.2)
# 模型训练
clf = RidgeClassifier()
clf.fit(x_train, y_train)
# 模型预测,利用验证集预测
y_pred = clf.predict(x_test)
# F1评价函数
print(f1_score(y_test, y_pred, average='macro'))