前言
代码已经传到github,没有整理过有点乱:https://github.com/ZesenChen/author_predict
在2017年9到12月份参加了kaggle平台上的一个文本分类比赛:Spooky author identification,这个比赛会给出三个恐怖小说作家作品里的一些英文句子。参赛者所要做的是用训练数据训练出合适的模型,让模型在测试数据上有好的泛化效果。笔者认为这个比赛对于提高文本方面的特征工程能力有挺多启发,因此写下这篇博客。有兴趣的人可以去下载这个比赛的数据集练手。如果因为墙的原因访问速度太慢,可以从我分享的百度网盘链接(密码:kudo)中下载数据。
#数据描述
比赛给我们提供的文本数据中一共有三个作者:Edgar Allan Poe,HP Lovecraft和Mary Shelley。分别简写为:EAP,HPL,MWS。取出train.csv中的前10条数据如下:
def clean_text(x):
tmp = x.lower()
for p in punctuation:
tmp = tmp.replace(p, ‘’)
return tmp
train_df = pd.read_csv(“train.csv”)
test_df = pd.read_csv(“test.csv”)
eng_stopwords = set(stopwords.words(“english”))
pd.options.mode.chained_assignment = None
punctuation = string.punctuation
train_df[‘text_cleaned’] = train_df[‘text’].apply(lambda x: clean_text(x))
test_df[‘text_cleaned’] = test_df[‘text’].apply(lambda x: clean_text(x))
train_df[“num_words”] = train_df[“text”].apply(lambda x: len(str(x).split()))
test_df[“num_words”] = test_df[“text”].apply(lambda x: len(str(x).split()))
train_df[“num_unique_words”] = train_df[“text”].apply(lambda x: len(set(str(x).split())))
test_df[“num_unique_words”] = test_df[“text”].apply(lambda x: len(set(str(x).split())))
train_df[“num_chars”] = train_df[“text”].apply(lambda x: len(str(x)))
test_df[“num_chars”] = test_df[“text”].apply(lambda x: len(str(x)))
train_df[“num_words_upper”] = train_df[‘text’].apply(lambda x: len([w for w in str(x).split() if w.isupper()]))
test_df[“num_words_upper”] = test_df[‘text’].apply(lambda x: len([w for w in str(x).split() if w.isupper()]))
train_df[“num_stopwords”] = train_df[“text_cleaned”].apply(lambda x: len([w for w in str(x).lower().split() if w in eng_stopwords]))
test_df[“num_stopwords”] = test_df[“text_cleaned”].apply(lambda x: len([w for w in str(x).lower().split() if w in eng_stopwords]))
train_df[“num_punctuations”] = train_df[“text”].apply(lambda x: len([w for w in str(x) if w in string.punctuation]))
test_df[“num_punctuations”] = test_df[“text”].apply(lambda x: len([w for w in str(x) if w in string.punctuation]))
train_df[“num_words_title”] = train_df[“text_cleaned”].apply(lambda x: len([w for w in str(x).split() if w.istitle()]))
test_df[“num_words_title”] = test_df[“text_cleaned”].apply(lambda x: len([w for w in str(x).split() if w.istitle()]))
train_df[“mean_word_len”] = train_df[“text_cleaned”].apply(lambda x: np.mean([len(w) for w in str(x).split()]))
test_df[“mean_word_len”] = test_df[“text_cleaned”].apply(lambda x: np.mean([len(w) for w in str(x).split()]))
##基于文本的特征
###统计特征
   上面提到的元特征是我们直观感觉上觉得有用的特征,还有一些是基于文本统计的,物理意义可能没那么明显的特征,这里我们称之为基于文本的特征。最简单的基于文本的特征就是词袋模型,整个文本数据中有多少个单词,词袋向量就有多少个维度,一个句子中哪些单词出现了,对应的元素位置就置为1,其他位置都置0。这种方法非常直白但也是一种有效的文本特征,它的缺点在于仅仅体现了某个单词出现与否,但没办法体现单词的重要程度,比如冠词的重要程度显然比不上人物名字的重要程度。所以一般情况下我们会采用更有效的文本特征。
  scikit-learn中有[feature extraction模块](http://scikit-learn.org/stable/modules/feature_extraction.html#text-feature-extraction)里面有专门用于文本特征抽取的api:我们用到的是**CountVectorizer**,**TfidfVectorizer**。简单来说**CountVectorizer**就是一个计数函数,对于整个文本数据,它能统计出每个单词出现的次数并将其转换成数值型的特征向量;而**TfidfVectorizer**不仅能表示单词出现与否,还能表示单词的重要程度以体现出文本之间的区分性。具体细节可参考scikit-learn文档。这里直接给出抽取这两种特征的python代码:
```python
tfidf_vec = TfidfVectorizer(stop_words='english', ngram_range=(1,3))
full_tfidf = tfidf_vec.fit_transform(train_df['text'].values.tolist() + test_df['text'].values.tolist())
train_tfidf = tfidf_vec.transform(train_df['text'].values.tolist())
test_tfidf = tfidf_vec.transform(test_df['text'].values.tolist())
tfidf_vec1 = CountVectorizer(ngram_range=(1,3))
tfidf_vec1.fit(train_df['text'].values.tolist() + test_df['text'].values.tolist())
train_tfidf1 = tfidf_vec.transform(train_df['text'].values.tolist())
test_tfidf1 = tfidf_vec.transform(test_df['text'].values.tolist())
这里的代码仅仅是以最简单的形式展现如何提取这两种特征,具体场景下还要参考文档中的参数说明来进行调参。
n_comp = 20
svd_obj = TruncatedSVD(n_components=n_comp, algorithm=‘arpack’)
svd_obj.fit(full_tfidf)
train_svd = pd.DataFrame(svd_obj.transform(train_tfidf))
test_svd = pd.DataFrame(svd_obj.transform(test_tfidf))
train_svd.columns = [‘svd_word2_’+str(i) for i in range(n_comp)]
test_svd.columns = [‘svd_word2_’+str(i) for i in range(n_comp)]
train_df = pd.concat([train_df, train_svd], axis=1)
test_df = pd.concat([test_df, test_svd], axis=1)
del full_tfidf, train_tfidf, test_tfidf, train_svd, test_svd
###word embedding
  关于word embedding的原理在这个博客我不打算多讲,后面考虑单独写一篇进行word embedding的原理介绍和工具的使用介绍。这里简单说一下,文本数据都是非数值型的字符串,要将它们作为数据进行模型训练,势必要把它们转换成维度相同的数值特征向量。word embedding利用了神经网络方法,将每个文本词汇都映射到一个维度恒定的数值特征空间中,这样一来每个单词都能单独表示成一个独一无二的特征向量。那么句子如何表示呢,单词向量的均值+单词向量的方差;这样我们就得到了每个句子的向量化表示。
  常用的工具包有**word2vec**,**Glove**,**Deep Walk**,**Node2vec**,**fasttext**等,在这里我们使用了[**Glove**](https://github.com/stanfordnlp/GloVe)方法,链接是**Glove**的github地址,里面有预训练好的word vec文件,文件中包含了所有单词的数值向量表示,维度是300。
```python
import numpy as np
def sent2vec(s):
words = str(s).lower().decode('utf-8')
words = word_tokenize(words)
words = [w for w in words if not w in eng_stopwords]
words = [w for w in words if w.isalpha()]
M = []
for w in words:
try:
M.append(embeddings_index[w])
except:
continue
M = np.array(M)
v = M.sum(axis=0)
if type(v) != np.ndarray:
return np.zeros(300)
return v / np.sqrt((v ** 2).sum())
f = open('glove.840B.300d.txt')
for line in tqdm(f):
values = line.split()
word = values[0]
coefs = np.asarray(values[1:], dtype='float32')
embeddings_index[word] = coefs
f.close()
print('Found %s word vectors.' % len(embeddings_index))
xtrain_glove = [sent2vec(x) for x in tqdm(train_df.text)]
xtest_glove = [sent2vec(x) for x in tqdm(test_df.text)]
xtrain_glove = np.array(xtrain_glove)
xtest_glove = np.array(xtest_glove)
train_df = pd.concat([train_df, pd.DataFrame(xtrain_glove)], axis=1)
test_df = pd.concat([test_df, pd.DataFrame(xtest_glove)], axis=1)
###其他特征
上面说到的那些特征其实有一定参赛经验的人都能想到,一堆特征工程的套路+XGBoost/RF/GBDT/deep learning+stacking已经是习以为常的套路。但如果每个人都这么做的话大家的最后的score大概都会扎堆在一个地方。所以想要在kaggle比赛中取得一个稍微靠前的名次有时就需要一些奇技淫巧。像这个文本分类的比赛,大家基本都会用上面提到的那些features。但有些人就能想到一些其他比较特殊的,举一些例子:
1、26个字母大小写数目的统计,52维的特征;
2、文本中出现的城市名称,这也能构成一类特征;
3、统计文本数据中出现次数最高的30个词语,给出每个句子中这些词语出现次数,这就又多了个30维的特征;
4、统计常用的那些标点符号的出现次数;
#结语
笔者与同学合作参加了这个文本分类比赛,使用了上面提到的特征工程思路和技巧,最终取得了还不错的名次,写下这篇博客一方面是复习下文本特征工程的一些技巧,另一方面给参加这方面比赛的人一点参考。