文本挖掘从小白到精通--- 7种简单易行的文本特征提取方法
Posted Social Listening与文本挖掘
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了文本挖掘从小白到精通--- 7种简单易行的文本特征提取方法相关的知识,希望对你有一定的参考价值。
写在前面:本文是笔者近期带的一位实习生写的实操笔记,特分享给大家,enjoy~
特别推荐|【文本挖掘系列教程】:
1 加载数据
这里使用的数据是抽样得到的1,000个的新闻样本数据。文章正文部分业已分词且经过去停用词处理。
import pandas as pd
df = pd.read_excel('/home/kesci/input/cluster_dataset8319/1000-samples.xlsx')
2 文本特征提取大集合
2.1 CountVectorizer
CountVectorizer是通过统计词汇出现的次数,并用词汇出现的次数的稀疏矩阵来表示文本的特征。它会统计所有出现的词汇,每个词汇出现了多少次,最后得到的稀疏矩阵的列就是词汇的数量(每个词汇就是一个特征/维度)
from sklearn.feature_extraction.text import CountVectorizer
ctv = CountVectorizer()
这里的CountVectorizer使用的是默认的参数。
我曾经自己设定过的参数主要是:
关于停用词的使用,现在举个例子:
stop_words = [word for word in open('/home/kesci/input/cluster_dataset8319/chinese_stopwords.txt').readlines()]
ctv = CountVectorizer(stop_words = stop_words)
x_ctv = ctv.fit_transform(df['clean_sentence'])
print(x_ctv[0])
(0, 25500) 1
(0, 38430) 1
(0, 39738) 1
(0, 28) 1
(0, 398) 1
(0, 5553) 1
: :
(0, 40208) 1
(0, 39018) 1
(0, 24830) 1
(0, 10489) 1
(0, 25982) 1
(0, 37995) 1
(0, 9814) 1
查看得到的结果。要先明确的是:得到的x_ctv是个稀疏矩阵。如果要得到正常的二维数据稠密表达的矩阵,需要使用x_ctv.toarray()。
注意,稀疏矩阵是不可以进行切片操作,比如x_ctv[1][2]。
vocabulary = ctv.get_feature_names()
len(vocabulary)
45809
一共得到了45809个单词作为特征。可以看到返回的这个vocabulary是个list。当比如使用KMeans得到聚类结果之后,可以通过vocabulary看到聚类中心有哪些词语。
2.2 TfidfVectorizer
和CountVectorizer很像,TfidfVectorizer提取的特征是:在一个文本中各个有效词汇对应的TFIDF值是多少,同时,每个文本特征向量会自动进行normalization(归一化)操作。
from sklearn.feature_extraction.text import TfidfVectorizertfv = TfidfVectorizer()
这里的TfidfVectorizer使用的是默认的参数。
我曾经自己设定过的参数主要是(和CountVectorizer很像):
(1)ngram_range=(x,y)。其中,x,y 为数字,即n元语法。
(2)stop_words = stop_words。其中,stop_words为自己从停用词文件中获取到的。(3)max_features = n。其中,n为词汇表的数量。表示根据词频大小降序排列后的TOP n词汇数。
可以感受到:CountVectorizer和TfidfVectorizer很相似。因为TfidfVectorizer = CountVectorizer + TfidfTransformer。也就是说,CountVectorizer得到的是词频(term frequency)特征,那么TfidfVectorizer就像一个给词频term frequency加权的这么个作用:也就是如果这个词特征在别的样本里经常出现,那么这个词在这个样本中的term frequency的加权就低了。这个加权是利用带默认参数的TfidfTransformer来计算出来的。
特别注意:这里的TfidfVectorizer会自动给文本特征向量进行归一化操作,能起到消除文本长短不一的问题。(其实从这个角度看,我个人觉得CountVectorizer才应该归一化一下~)
x_tfv = tfv.fit_transform(df['clean_sentence'])print(x_tfv[0])
(0, 9814) 0.03644325520745695
(0, 37995) 0.03903038473141272
: :
(0, 5553) 0.01897783467880405
(0, 398) 0.013384132069498216
(0, 28) 0.029201349468525248
(0, 39738) 0.00992908598253803
(0, 38430) 0.024893592049250953
(0, 25500) 0.023539781440642706
验证看看:TfidfVectorizer = CountVectorizer + TfidfTransformer。
注意:使用TfidfTransformer输入为一个numpy.array,形状是(n_samples, n_features)。因为2个方法的输入设定不同,对于CountVectorizer和TfidfVectorizer只要是iterable(可迭代)的就可以了。
根据设定,TfidfTransformer是将CountVectorizer的输出作为输入的。
from sklearn.feature_extraction.text import TfidfTransformer
tft = TfidfTransformer()
x_tft = tft.fit_transform(x_ctv)
print(x_tft[0])
(0, 45273) 0.040193714135109256
(0, 44596) 0.09208048277751922
(0, 44249) 0.05795488832300332
(0, 43855) 0.08323502851073701
(0, 43638) 0.03238523570742493
: :
(0, 396) 0.01940495185481739
(0, 392) 0.055555098696532024
(0, 378) 0.1579043530170612
(0, 372) 0.036443255207456955
(0, 28) 0.02920134946852525
format_x_tfv = ['%.14f' % number for number in x_tfv[0].toarray().flatten()]
format_x_tft = ['%.14f' % number for number in x_tft[0].toarray().flatten()]
print( (format_x_tfv == format_x_tft) )
True
再随便从0-999中抽取一个来试试看。
format_x_tfv = ['%.14f' % number for number in x_tfv[563].toarray().flatten()]
format_x_tft = ['%.14f' % number for number in x_tft[563].toarray().flatten()]
print( (format_x_tfv == format_x_tft) )
True
也就是说:TfidfVectorizer是一种包装的比较好的写法罢了。(这里要说明的是上面format中到小数14位是最远的返回True的,我估计是还是有float计算上的误差,所以不能再往后比较了~)
vocabulary = tfv.get_feature_names()
len(vocabulary)
45809
那么,这里得到和CountVectorizer一样多的features就一点都不奇怪了。
2.3 HashingVectorizer
说到CountVectorizer和TfidfVectorizer,使得好像要带一笔HashingVectorizer。在我的印象中,HashingVectorizer就是--- CountVectorizer省略了vocabulary这个映射(不管是CountVectorizer还是TfidfVectorizer,都是在内存中有一个word2id的映射。),而直接使用Hash的方式来映射。无论多少个词汇,都可以设定为固定维度,这么做节省了内存,但会有存在冲突的可能了,因为可能存在多个词汇共用一个id的情形。
不过,没有这个vocabulary的缺点也就很明显了,就是不能再倒回来看这个列对应的词是什么。而且,它对于Count 的结果还默认归一化了。(我个人觉得Count的结果是真的可以归一化的,因为如果归一化的目的就是使得不同长度的文本都可以进行互相比较的话,那么对Count的结果归一化是最方便的、最直观的了。)
这里带来了一个特别的好处 --- 可以文件流的形式来输入数据,也就是说,不需要一开始就把文件全部读进来了。因为这里的HashingVectorizer是把word通过hash对应到一个下标上,一开始的n_features如果你自己不指定,它自己也是指定好的,而且它是个Count,只依赖每一行自己的特征的情况,和别人无关;而且它的输出的列的数量又是一定的,也就是每个输出格式一定。这样完全可以文件不在内存中,我就一点点读到内存再处理就好了。
但我没有使用过,无法说常调参的参数是哪些。
从HashingVectorizer的实现上来看,HashingVectorizer主要就是FeatureHasher(当然增加了stop_words、normalization这些了)。此类没有fit这个过程,所以fit_transform和transform等价,效果是一样的。
from sklearn.feature_extraction.text import HashingVectorizer
hv = HashingVectorizer()
x_hv = hv.transform(df['clean_sentence'])
x_hv[0]
(0, 9819) 0.0323254091917618
(0, 28307) 0.0323254091917618
(0, 46014) 0.09697622757528539
(0, 49628) 0.0646508183835236
(0, 60743) 0.0323254091917618
(0, 61902) 0.0323254091917618
: :
(0, 1008494) -0.0646508183835236
(0, 1012037) -0.0646508183835236
(0, 1048038) 0.2586032735340944
为了对比和CountVectorizer的关系,把norm设定为None。
hv = HashingVectorizer(norm = None)x_hv = hv.transform(df['clean_sentence'])print(x_hv[0])
(0, 9819) 1.0
(0, 28307) 1.0
(0, 46014) 3.0
(0, 49628) 2.0
(0, 60743) 1.0
(0, 61902) 1.0
(0, 73561) -1.0
(0, 91501) 1.0
(0, 97738) -2.0
: :
(0, 959451) -1.0
(0, 975414) -1.0
(0, 1004860) -1.0
(0, 1008494) -2.0
(0, 1012037) -2.0
(0, 1048038) 8.0
x_hv[0]
<1x1048576 sparse matrix of type '<class 'numpy.float64'>'
with 131 stored elements in Compressed Sparse Row format>
x_ctv[0]
<1x45809 sparse matrix of type '<class 'numpy.int64'>'
with 131 stored elements in Compressed Sparse Row format>
x_hv_pos = [abs(x) for x in x_hv[456].toarray().flatten()]x_ctv_value = [int(x) for x in x_ctv[456].toarray().flatten() if x != 0]x_hv_value = [int(x) for x in x_hv_pos if x != 0]x_ctv_value = x_ctv_value.sort()x_hv_value = x_hv_value.sort()x_ctv_value == x_hv_value
True
通过上面2个方面可以大致确定HashingVectorizer和CountVectorizer功能一致。(当然由于HashingVectorizer会有冲突的现象产生,有不一致的也有可能的。)一个是每一个样本的稀疏矩阵中有值的特征的数量一致。另一个就是随机找出一个样本来做对比,看是否非零的值都相等。(由于HashingVectorizer有可能会有相反数,也就是负值,所以我把负值都先abs转化了正的。)
想想这里还是挺有意思的,就是随机给count值赋予正负号。应该是和哈希冲突的处理相关,但我没大懂。
2.4 Word2Vec + Mean
这里使用到的方法是先使用Word2Vec得到抽取的作为特征的词语的向量。然后,针对每一个样本中包含的特征,特征对应的向量相加,再求平均值,得到这个样本的向量表示。当然,由于这里求得是一个平均值,想要直接倒回去看比如最后得到的聚类中心是什么,就不行了。
from gensim.models import Word2Vec
clean_words = [word.split() for word in df['clean_sentence']]
wv_model = Word2Vec(clean_words, min_count = 3, workers = 8, window = 5)
print(wv_model)
Word2Vec(vocab=15942, size=100, alpha=0.025)
我会可以考虑自己设置的参数:
min_count:总出现次数少于多少次就不把这个单词作为vocabulary了。(这里注意:说明并不是所有的词都会在vocabulary中的。)
workers:并行处理,即使用多少个worker threads来并行同时训练模型。
iter:对于有的语料,在神经网络上训练多少轮。(就相当于神经网络上的epoch参数。)可以通过增加iter来对小数据(即:样本总数量少的时候)多进行几次训练。
window:在一个句子中,当前词语和预测的词语之间的最大距离可以多大。如果你的语料多,就可以设置的大一点。
这里的结果表示:有15942个词被列入到vocabulary中,每一个特征是一个100个维度的向量来表示。
def get_wv_mean(one_sample, model, size):
result = np.zeros(size).reshape(1, size)
count = 0
for word in one_sample:
try:
result = result + model.wv[word]
count = count + 1
except:
pass
if count != 0:
result = result / count
return result
size = 100
x_wv_mean = get_wv_mean(clean_words[0], wv_model, size)
for i in range(1, len(clean_words)):
x_wv_mean = np.concatenate((x_wv_mean, get_wv_mean(clean_words[i], wv_model, size)), axis = 0)
x_wv_mean.shape
(1000, 100)
2.5 Word2Vec + Similarity
这种方法是偶然不记得在哪里看到的。其实我感觉仿照的是CountVectorizer/TfidfVectorizer。因为CountVectorizer/TfidfVectorizer的思路都是:根据一个整体的所有样本,找到需要的单词作为特征,然后每个样本看在所有的特征上,这个样本和这个整体的特征有什么关系:比如:Count、比如:Tfidf。而这里也是一样,先找到一个整体的语料(其实,如果已知现在的样本是哪个专业领域的话,那应该找这个专业领域的一个比较好的语料库来训练Word2Vec是更好的。这里只能靠自己搜集语料训练模型了。)
训练Word2Vec,假设找到了n个特征,那么对于每一个样本,我都设定一个n维的向量,然后对于向量的每个维度,计算该维度对应的单词和标题中的每个单词的相似度,最后使用那个最大的相似度的值作为这一位的值,计算每一位,得到一个完整的向量。
但这个方法有一点说清楚就是:计算特别慢。我这里使用的是自己写的方法,也许有更好的方法来写。
所以,这里我自己写的只能作为一种写法,真实中很难使用,因为太慢了。
row_length = len(df)
col_length = len(wv_model.wv.vocab)
matrix = np.zeros([row_length, col_length])
for row in range(0, row_length):
for vocab in wv_model.wv.vocab:
max_similarity = 0
for col in range(0, len(clean_words[row])):
clean_word = clean_words[row][col]
similarity = 0
try:
similarity = wv_model.wv.similarity(vocab, clean_word)
except:
pass
if similarity > max_similarity:
max_similarity = similarity
max_similarity =
2.6 Doc2Vec
接下来使用Doc2Vec来直接抽取语句特征,不是像之前那样做词汇向量叠加的简单平均,那样会丢失语句词序和句法信息。
Doc2Vec 或者叫做 paragraph2vec, sentence embeddings,是一种非监督式算法,可以获得 sentences/paragraphs/documents 的向量表达,是 word2vec 的拓展。学出来的向量可以通过计算距离来找 sentences/paragraphs/documents 之间的相似性,可以用于文本聚类,对于有标签的数据,还可以用监督学习的方法进行文本分类,例如经典的情感分析问题。
from gensim.models.doc2vec import Doc2Vec, TaggedDocument
clean_words = [word.split() for word in df['clean_sentence']]
documents = [TaggedDocument(doc, [i]) for i, doc in enumerate(clean_words)]
dv_model = Doc2Vec(documents, vector_size=5, window=2, min_count=1, workers=4)
matrix_dv = []
for sentence, _ in documents:
matrix_dv.append(dv_model.infer_vector(sentence))
matrix_dv[0]
array([-3.6228316 , -3.269969 , 0.02452112, 3.18907 , 1.8077784 ],
dtype=float32)
2.7 texts_to_sequences
texts_to_sequence从名字上看虽然和doc2vec挺像的,但texts_to_sequences可比doc2vec做的事情简单多了。说白了就是建立一个word2id的dictionary,然后利用id把句子表示出来。当然也可以不使用id,使用别的东西了,但那个就要自己设置了。
这个操作是深度学习文本预处理的必要操作,比如喂到CNN、RNN神经网络的Embedding层里。
from tensorflow.python.keras.preprocessing import sequence
from tensorflow.python.keras.preprocessing import text
tokenizer = text.Tokenizer(num_words = 1000)
tokenizer.fit_on_texts(df['clean_sentence'])
print(len(tokenizer.word_index))
x_seq = tokenizer.texts_to_sequences(df['clean_sentence'])
print(x_seq[:1])
46470
[[5, 2, 58, 8, 329, 185, 9, 146, 70, 9, 424, 714, 146, 133, 172, 73, 168, 116, 146, 152, 168, 146, 307, 21, 87, 119, 50, 9, 98, 15, 803, 552, 9, 146, 133, 600, 572, 521, 164, 284, 98, 59, 766, 477, 714, 926, 237, 715, 135, 150, 394, 235, 9, 527, 146, 133, 164, 284, 146, 133, 364, 9, 982, 600, 146, 133, 431, 714, 573, 767, 146, 152, 146, 215, 186, 146, 133, 235, 573, 477, 714, 926, 237, 89, 586, 119, 477, 164, 284, 364, 164, 284, 714, 9, 527, 146, 133, 235, 573, 846, 364, 146, 133, 235, 19, 146, 235, 116, 152, 751, 714, 9, 186, 364, 168, 572, 490, 146, 133, 235, 573, 146, 152, 173, 7, 2, 6, 733, 509]]
print(df['clean_sentence'][:1])
4377 制图 蔡华伟 本报 北京 月 日电 记者 赵贝佳 日前 国家气象中心 单位 编制 2017 ...
Name: clean_sentence, dtype: object
print(len(tokenizer.word_index))
print(type(tokenizer.word_index))
print(tokenizer.word_index)
46470
<class 'dict'>
{'中国': 1, '月': 2, '问题': 3, '企业': 4, '北京': 5, '责编': 6, '2018': 7, '记者': 8,...}
我一般只设定num_words这个参数,保留n个词汇特征。
但是这里要注意:虽然设定了num_words,但是word_index是所有的单词都会被记录到这个dictionary中去(所以如果你看word_index的长度会是46470而非设定的1000)。但在使用word_index来表示样本的时候,才会真正使用到这个num_words,即仅仅使用num_words个最开头的单词来表示样本(可以从上面的check_set中表示出来)。(个人觉得这个设置浪费空间啊。)
还有一点需要注意:word_index是从1开始的,而非从0开始。
由于这里实际上就是word2id而已,那么不同的样本长度不一,得到的向量的长度就会不同的。可以通过word_index找到每个单词对应的id。所以为了之后的操作方便,需要把不同的向量的统一为一个长度。比如:这里可以找出sequence中最长的那一个,然后padding为这个长度。
max_len = np.max([len(s) for s in x_seq])
print('max_train_len: ', max_len)
x_seq = sequence.pad_sequences(x_seq, maxlen = max_len)
print(x_seq[0])print(len(x_seq[0]))
max_train_len: 676
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
...
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 5 2 58 8 329 185 9 146 70 9 424
714 146 133 172 73 168 116 146 152 168 146 307 21 87 119 50 9 98
15 803 552 9 146 133 600 572 521 164 284 98 59 766 477 714 926 237
715 135 150 394 235 9 527 146 133 164 284 146 133 364 9 982 600 146
133 431 714 573 767 146 152 146 215 186 146 133 235 573 477 714 926 237
89 586 119 477 164 284 364 164 284 714 9 527 146 133 235 573 846 364
146 133 235 19 146 235 116 152 751 714 9 186 364 168 572 490 146 133
235 573 146 152 173 7 2 6 733 509]
676
可以看到前面补充的位置都设为了0。
本文算是近段时间对于文本特征提取方法的实操总结,更为先进的文本特征提取方法,比如ELMO、BERT等还未学习到,后面有余力去学习。当然,针对不同的文版数据和应用场景去选择合适的特征提取方法,我还需要不断的增加实操经验和理论学习,下一篇会写一写文本特征降维的话题,敬请期待~
以上是关于文本挖掘从小白到精通--- 7种简单易行的文本特征提取方法的主要内容,如果未能解决你的问题,请参考以下文章