本章代码大部分没跑,只供学习
第四节特征工程里提到,有连续特征和离散特征,对于文本数据,文本特征可以看作第三种特征
给定一个影评(输入),输出影评是正面还是负面
sklearn无法处理文本数据,需要将文本数据转换为数值表示,然后再用机器学习算法处理
词袋 即统计每个单词出现的频率
词袋构造步骤
1 划分原始字符串:将原始字符串用空格或标点负号分隔,获取单词拼写
2 构建词表,可进行编号
3 统计单词频率
通过sklearn.feature_extraction.text.CountVectorizer构造词袋。构造完了可访问.vocabulary_访问词表,然后调用transform获取词袋,看下词表和词袋
def test_workds_bag(self):
bards_words = ['the fool doth think he is wise,', 'but the wise man knows himself to be a fool']
vect = CountVectorizer().fit(bards_words)
print(f'vocabulary type: {type(vect.vocabulary_)}, vocabulary: {vect.vocabulary_}')
bag_words_trans = vect.transform(bards_words)
print(f'words bag transform type:{type(bag_words_trans)},\n'
f'words bag transfrom shape:{bag_words_trans.shape},\n'
f'words bag transform repr: {repr(bag_words_trans)}\n'
f'words bag transform:\n {bag_words_trans.toarray()}')
注意,vocabulary只是单词排序,字典的值不是单词出现次数,只是在句子里的下标,注意区分词表和词袋的概念。
词袋用稀疏矩阵表示(sparse matrix)
CountVectorier默认使用的正则是"\b\w\w+\b",含义是提起至少两个字符以上的字母数字且被单词边界分开。所以不会提取长度为1的作为单词,所以上述句子提取的词袋也没提取到a
先构造词袋,然后用LogisticRegression交叉验证,然后网格搜索最优的C
def test_movies_bag(self):
movie_train, movie_test = load_files('train_path'), load_files('test_path')
text_tr, ytr, text_te, yte = movie_train.data, movie_train.target, movie_test.data, movie_test.target
text_tr, test_te = [doc.replace(b"
", b"") for doc in text_tr], [doc.replace(b"
", b"") for doc in text_te]
vect = CountVectorizer().fit(text_tr)
xtr = vect.transform(text_tr)
print(f'vect transform features :{vect.get_feature_names()}') # shape: (25000, n) feature num is n, sort by alphabet
# cross validation
print(f'mean logistic regression cross score: {cross_val_score(LogisticRegression(), xtr, ytr, cv=5)}')
# grid search
params_grid = {'C': [0.001, 0.01, 0.1, 1, 10]}
grid = GridSearchCV(LogisticRegression(), params_grid, cv=5).fit(xtr, ytr)
print(f'grid best score: {grid.best_score_:.3f}')
print(f'grid best params: {grid.best_params_}') # C:0.1
print(f'grid test score: {grid.score(text_te, yte)}')
词袋数据存在稀疏矩阵,对于高维稀疏矩阵,线性模型的LogisticRegression性能最好,约为88%
其实仅靠词袋还有很多问题,动词有进行时,过去时,单三等形式,还可能有很多写错的字符,这些需要考虑影响程度。可考虑在词袋基础做单词改进(不识别大小写,即大小写不同的单词会被识别为同一单词)
考虑CountVectorizer提取单词原理,使用"\b\w\w+\b",对于doesn't,bilibili.txt这类单词,会拆开识别
通过CountVectorizer的min_df参数实现(仅出现一次的单词可能没什么用,先这么试试)
def get_movie_data_test(self):
movie_train, movie_test = load_files('train_path'), load_files('test_path')
text_tr, ytr, text_te, yte = movie_train.data, movie_train.target, movie_test.data, movie_test.target
text_tr, test_te = [doc.replace(b"
", b"") for doc in text_tr], [doc.replace(b"
", b"") for doc in
text_te]
return text_tr, test_te, ytr, yte
def test_movies_bag_5_appear(self):
xtr, xte, ytr, yte = self.get_movie_data_test()
vect = CountVectorizer(min_df=5).fit(xtr)
xtr_trans = vect.transform(xtr)
grid = GridSearchCV(LogisticRegression(), {'C': [0.001, 0.01, 0.1, 1, 10]}, cv=5).fit(xtr, ytr)
print(f'logistic regression best score: {grid.score(xte, yte)}')
结论 精度大概为89%,发现处理单词出现频率后,精度没明显的提升,但减少了约三分之二的特征,可提升处理速度
删除没有意义的词语还有一种方法:删除出现频率过高的词语。有两种方法:1使用特定的语言停用词词表(sklearn.feature_extraction.text.ENGLISH_STOP_WORDS提供了停用词词表) 2指定特定频率,舍弃频率在该频率以上的词语。比如说above, into, well, anyone等词
可以从数据集里删除停用词。虽然减少不了多少特征,但可能会提升性能,因为停用词出现频率可能高一些
tf-idf概念 也叫词频-逆向文档频率(term frequency - inverse document frequency, tf-idf)。给词语赋予权重,对于语料库中经常出现的词语,不会赋予很高权重;在某个文档出现频率次数较高的词被识别为术语,赋予较高的权重。最后通过一个量化指标 tf-idf分数来反映单词权重。sklearn里有两个类实现了tf-idf:TfidfTransformer和TfidfVectorizer,前者接受稀疏矩阵并转换,后者接受文本数据完成词袋特征提取和tfidf变换,计算公式如下
N是文档总数量,Nw是出现某个单词的文档数量,tf是单词在查询文档中出现的次数
可以看下tfidf得分最高和最低的单词
tfidf得分较小时,说明单词要么出现频率很低,要么就是在很多文档里都有使用
tfidf得分较大时,说明词汇较高频率出现在某些文档中。但这类词语有的对影评情感分类并没有显著的作用,比如电影标题
看下idf得分最低的单词(出现频率最高,只按频率排序,idf和tfidf不一样)
这些单词主要是停用词
def test_tfidf_show_features(self):
xtr, xte, ytr, yte = self.get_movie_data_test()
pipe = make_pipeline(TfidfVectorizer(min_df=5), LogisticRegression())
grid = GridSearchCV(pipe, {'logisticregression__C': [0.001, 0.01, 0.1, 1, 10]}, cv=5).fit(xtr, ytr)
print(f'best cross score: {grid.best_score_}')
# show tfidf words
vectorizer = grid.best_estimator_.named_steps["tfidfvectorizer"]
xtr_trans = vectorizer.transform(xtr)
max_val = xtr_trans.max(axis=0).toarray().ravel()
sorted_by_tfidf = max_val.argsort()
feature_name = np.array(vectorizer.get_feature_names())
# show tf-idf score
print(f'tfidf score lowest 20 ea: {feature_name[:20]}')
print(f'tfidf score highest 20 ea: {feature_name[-20:]}')
# show idf score
print(f' idf score lowest 20 ea: {vectorizer.idf_[:20]}')
看下训练的logistic模型系数的最大最小值
mglearn.tools.visualize_coefficients(grid.best_estimator_.named_steps["logisticregression"].coef_,
feature_names=feature_names, n_top_features=40)
看x轴发现最小得分的单词大多是负面情绪的单词,比如worst,waste等,得分高的单词大部分也是正面单词:great, excellent等
词袋缺点 舍弃了单词顺序
词袋解决方案 有一种词袋考虑上下文中单词的计数,即某个单词相邻某几个单词的计数
二元分词 两个词例,以此类推三元等,词例范围可通过vector类的ngram_range参数传入来指定词例个数。ngram_range是一个元组,包括了词例的最小长度和最大长度。CountVectorizer默认是(1,1)的ngram_range
def test_word_bag_ngram(self):
bards_words = ['the fool doth think he is wise,', 'but the wise man knows himself to be a fool']
vector = CountVectorizer(ngram_range=(1, 1)).fit(bards_words)
print(f'length of feature: {len(vector.vocabulary_)},\nvector feature names: {vector.get_feature_names_out()}')
仅查看二元分词
def test_word_bag_ngram(self):
bards_words = ['the fool doth think he is wise,', 'but the wise man knows himself to be a fool']
vector = CountVectorizer(ngram_range=(1, 1)).fit(bards_words)
print(f'length of feature: {len(vector.vocabulary_)},\nvector feature names: {vector.get_feature_names_out()}')
# show 2 dimension words
vector = CountVectorizer(ngram_range=(2, 2)).fit(bards_words)
print(f'feature len: {len(vector.vocabulary_)},\n2 di vector feature names: {vector.get_feature_names_out()}')
优缺点 多元分词可能导致过拟合,也会增加计算量,n元分词计算量是一元分词的n倍
可以同时使用一元,二元,三元分词
vector = CountVectorizer(ngram_range=(1, 3)).fit(bards_words)
print(f'feature len: {len(vector.vocabulary_)},\n vector feature names: {vector.get_feature_names_out()}')
对影评数据应用1-3元词袋,然后网格搜索出最佳参数,然后热图可视化(没跑,用的教材的图)
二元的精度提升了约一个百分点,发现一元到二元精度提升很多,二元到三元没提升多少,表明三元可能没太大作用,
看下特征系数,绘制bar图
发现三元特征的特征系数普遍较低,也验证了三元分词没起太多作用
目的 很多单词有不同分词形式,将分词形式作为单独特征可能会导致过拟合,将词干提取或合并可减少次问题导致的误差
词干提取(stemming) 删除单词不同分词形式的通用分词后缀,然后合并词干
词形还原(lemmatization) 将单词不同分词形式按照已有分词字典进行合并还原
标准化 词干提取和词形还原都叫标准化,即将一个单词还原成标准形式
先看下词干提取
def test_word_stem(self):
en_nlp = spacy.load('en_core_web_sm')
stemmer = nltk.stem.PorterStemmer()
def compare_normalization(doc):
doc_spacy = en_nlp(doc)
print(f'show word split result: {[token.lemma_ for token in doc_spacy]}')
print(f'show word stem found result: {[stemmer.stem(token.norm_.lower()) for token in doc_spacy]}')
test_text = "our meeting today was worse than yesterday, I'm scared of meeting the clients tomorrow"
compare_normalization(test_text)
was词干提取后变成wa,因为词干提取原理是删分词后缀
worse变成wors,meeting变成meet
sklearn里没支持词干提取和词形还原,单CountVectorizer可以使用tokenizer指定分词器将文档转换为词例列表
看下词形还原
def test_lemmatization(self):
regexp = re.compile('(?u)\\b\\w\\w+\\b')
en_nlp = spacy.load('en_core_web_sm')
old_tokenizer = en_nlp.tokenizer
en_nlp.tokenizer = lambda string: old_tokenizer.tokens_from_list(regexp.findall(string))
def custom_tokenizer(doc):
doc_spacy = en_nlp(doc, entity=False, parse=False)
return [token.lemma_ for token in doc_spacy]
lemma_vect = CountVectorizer(tokenizer=custom_tokenizer, min_df=5)
xtr, xte, ytr, yte = self.get_movie_data_test()
xtr_lemma = lemma_vect.fit_transform(xtr)
print(f'words lemmatization shape: {xtr_lemma.shape}')
词形还原可以合并特征,可以看作正则化,因为选的特征变少了。数据集比较小时,词形还原可以有较大的性能提升。
另一种常用的文本建模方法是主题建模,比如每个新闻都涉及一些主题,比如财经,体育,科技等。给一个新闻预测是哪个主题,是主题建模要考虑的问题。一般主题建模指隐含狄利克雷分布(Latent Dirichlet Allocation,LDA)的分解方法
机器学习学习到的主题,和我们日常提到的主题可能不太一样。机器学习可能按词频学到词频较高的词语作为主题,类似于PCA的主成分,没有什么让人直观理解的含义,只是个计算量。
预处理 应用LDA前应删掉常见的频率很高的非主题词,可以在CountVctorizer构造传入参数min_df=.3,表示删除至少在30%文档出现的词语
任务 现在设置学习目标是10个主题。主题类似于NMF中的分量,没有内在的顺序,但改变主题数量会改变所有主题(其实LDA和NMF有一定相似性,也可试着用NMF提取主题)。此处用batch学习方法,比online方法稍慢,但结果可能会更好,然后增大max_iter,可以得到更好的模型
def test_topic_modeling(self):
vect = CountVectorizer(max_features=10000, max_df=.15)
x = vect.fit_transform('test train')
lda = LatentDirichletAllocation(n_topics=10, learning_method='batch', max_iter=25, random_state=0)
topics = lda.fit_transform(x)
print(f'lda component shape: {lda.components_.shape}') # (10, 10000)
查看每个主题最重要的词语
从词汇重要程度看,topic1可能和战争有关,主题2可能和喜剧有关,主题3可能和电视连续剧有关
lda100 = LatentDirichletAllocation(n_topics=100, learning_method='batch', max_iter=25, random_state=0)
topics_100 = lda100.fit_transform(x)
# randomly select several topics
topics_sample = np.array([11, 21, 31, 41, 51])
sorting_100 = np.argsort(lda100.components_, axis=1)[:, ::-1]
feature_names_100 = np.array(vect.get_feature_names_out())
mglearn.tools.print_topics(topics=topics_sample, feature_names=feature_names_100,
sorting=sorting_100, topic_per_chunk=7, n_words=20)
还有一种量化指标是将所有文档的主题重要性汇总,然后按会总量从大到小可视化,或者按topic可视化
主题建模是无监督学习,最好有已知标签进行进一步验证。学习结果和random_state挂钩
讨论了CountVectorizer和TfidfVectorizer,是相对简单的方法,其他高级的方法可使用py的包spacy(相对较新,较高效,设计良好),nltk(很完整的库,有些过时),gensim(着重于主题建模的nlp包)
研究方向
1 使用连续向量表示。
2 递归神经网络(RNN)。很适合自动翻译和摘要,