【天池学习赛】零基础入门NLP - 新闻文本分类

一、赛题描述

赛题数据为新闻文本,并按照字符级别进行匿名处理。整合划分出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的均值,选手提交结果与实际测试集的类别进行对比,结果越大越好。

【天池学习赛】零基础入门NLP - 新闻文本分类_第1张图片

可以通过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')

 三、解题思路

赛题思路分析:赛题本质是一个文本分类问题,需要根据每句的字符进行分类。但赛题给出的数据是匿名化的,不能直接使用中文分词等操作,这个是赛题的难点。

因此本次赛题的难点是需要对匿名字符进行建模,进而完成文本分类的过程。由于文本数据是一种典型的非结构化数据,因此可能涉及到特征提取分类模型两个部分。为了减低参赛难度,我们提供了一些解题思路供大家参考:

  • 思路1:TF-IDF + 机器学习分类器

直接使用TF-IDF对文本提取特征,并使用分类器进行分类。在分类器的选择上,可以使用SVM、LR、或者XGBoost。

  • 思路2:FastText

FastText是入门款的词向量,利用Facebook提供的FastText工具,可以快速构建出分类器。

  • 思路3:WordVec + 深度学习分类器

WordVec是进阶款的词向量,并通过构建深度学习分类完成分类。深度学习分类的网络结构可以选择TextCNN、TextRNN或者BiLSTM。

  • 思路4:Bert词向量

Bert是高配款的词向量,具有强大的建模学习能力。

四、相关知识 - 文本表示方法

One-hot

这里的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

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

N-gram与Count Vectors类似,不过加入了相邻单词组合成为新的单词,并进行计数。

如果N取值为2,则句子1和句子2就变为:

句子1:我爱 爱北 北京 京天 天安 安门
句子2:我喜 喜欢 欢上 上海

TF-IDF

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'))

你可能感兴趣的:(机器学习-大赛案例,机器学习,nlp,python)