文本挖掘学习笔记(三):文档相似度、文档分类和情感分析

注:学习笔记基于文彤老师文本挖掘的系列课程

全文基于《射雕英雄传》语料库,下面是读入数据的一个基于Pandas的通用操作框架。

#准备工作,具体可参照学习笔记一,将处理的文章分章节
import pandas as pd
# 有的环境配置下read_table出错,也可用用read_csv
raw = pd.read_table("金庸-射雕英雄传txt精校版.txt",
                  names = ['txt'],  encoding ="GBK")
# 章节判断用变量预处理
def m_head(tmpstr):
    return tmpstr[:1]

def m_mid(tmpstr):
    return tmpstr.find("回 ")
#取出进行判断的变量
raw['head'] = raw.txt.apply(m_head)
raw['mid'] = raw.txt.apply(m_mid)
raw['len'] = raw.txt.apply(len)#防止特殊情况,设置长度
chapnum = 0
for i in range(len(raw)):
    if raw['head'][i] == "第" and raw['mid'][i] > 0 and raw['len'][i] < 30 :
        chapnum += 1
    if chapnum >= 40 and raw['txt'][i] == "附录一:成吉思汗家族" :
        chapnum = 0
    raw.loc[i, 'chap'] = chapnum   
# 删除临时变量,这里必须删除,否则后续sum函数处会出错
del raw['head']
del raw['mid']
del raw['len']
rawgrp = raw.groupby('chap')
chapter = rawgrp.agg(sum) # 只有字符串列的情况下,sum函数自动转为合并字符串
chapter = chapter[chapter.index != 0]#排除前言和附录

1.文档相似度

计算两个词相似度的原理:简单的说,就是将每个词的向量在空间上进行余弦运算,当cos越接近0时候,两者越相似。

1.1词条相似度:word2vec

词袋模型不考虑词条之间的相关性,因此无法用于计算词条相似度。

分布式表达会考虑词条的上下文关联,因此能够提取出词条上下文中的相关性信息,而词条之间的相似度就可以直接利用此类信息加以计算。

目前主要使用gensim实现相应的算法。
gensim 通用格式list of list格式

gensim也提供了sklearn的API接口:sklearn_api.w2vmodel,可以在sklearn中直接使用。

1.1.1 设置word2vec模型

class gensim.models.word2vec.Word2Vec(

sentences = None : 类似list of list的格式,对于特别大的文本,尽量考虑流式处理
size = 100 : 词条向量的维度,数据量充足时,300/500的效果会更好
window = 5 : 上下文窗口大小
workers = 3 : 同时运行的线程数,多核系统可明显加速计算

其余细节参数设定:

min_count = 5 : 低频词过滤阈值,低于该词频的不纳入模型
max_vocab_size = None : 每1千万词条需要1G内存,必要时设定该参数以节约内存
sample=0.001 : 负例采样的比例设定
negative=5 : 一般为5-20,设为0时不进行负例采样
iter = 5 : 模型在语料库上的迭代次数,该参数将被取消

与神经网络模型有关的参数设定:

seed=1, alpha=0.025, min_alpha=0.0001, sg=0, hs=0

)

chapter.head()
txt
chap
1.0第一回 风雪惊变      那说话人五十来岁年纪,一件青布长袍早洗得褪成了蓝灰带白。只听他两...
2.0第二回 江南七怪    颜烈跨出房门,过道中一个中年士人拖着鞋皮,踢跶踢跶的直响,一路打着哈...
3.0第三回 黄沙莽莽    寺里僧众见焦木圆寂,尽皆悲哭。有的便为伤者包扎伤处,抬入客舍。  忽...
4.0第四回 黑风双煞    完颜洪熙笑道:“好,再打他个痛快。”蒙古兵前哨报来:“王罕亲自前来迎...
5.0第五回 弯弓射雕    一行人下得山来,走不多时,忽听前面猛兽大吼声一阵阵传来。韩宝驹一提缰...

生成list of list格式,注意方法,后面要用到

# 分词和预处理,生成list of list格式
import jieba
chapter['cut'] = chapter.txt.apply(jieba.lcut)
chapter.head()
txtcut
chap
1.0第一回 风雪惊变      那说话人五十来岁年纪,一件青布长袍早洗得褪成了蓝灰带白。只听他两...[第一回, , 风雪, 惊变,  ,  ,  ,  ,  ,  , 那, 说话, 人, 五...
2.0第二回 江南七怪    颜烈跨出房门,过道中一个中年士人拖着鞋皮,踢跶踢跶的直响,一路打着哈...[第二回, , 江南七怪,  ,  ,  ,  , 颜烈, 跨出, 房门, ,, 过道, ...
3.0第三回 黄沙莽莽    寺里僧众见焦木圆寂,尽皆悲哭。有的便为伤者包扎伤处,抬入客舍。  忽...[第三回, , 黄沙, 莽莽,  ,  ,  ,  , 寺里, 僧众, 见, 焦木, 圆寂...
4.0第四回 黑风双煞    完颜洪熙笑道:“好,再打他个痛快。”蒙古兵前哨报来:“王罕亲自前来迎...[第四回, , 黑风双, 煞,  ,  ,  ,  , 完颜洪熙, 笑, 道, :, “,...
5.0第五回 弯弓射雕    一行人下得山来,走不多时,忽听前面猛兽大吼声一阵阵传来。韩宝驹一提缰...[第五回, , 弯弓, 射雕,  ,  ,  ,  , 一行, 人下, 得, 山来, ,,...
# 初始化word2vec模型和词表
from gensim.models.word2vec import Word2Vec
n_dim = 300 # 指定向量维度,大样本量时300~500较好
w2vmodel = Word2Vec(size = n_dim, min_count = 10)
w2vmodel.build_vocab(chapter.cut) # 生成词表
w2vmodel#这是生成的模型
<gensim.models.word2vec.Word2Vec at 0x230bb928da0>

1.1.2 对word2vec模型进行训练

word2vecmodel.train(

sentences : iterable of iterables格式,对于特别大量的文本,尽量考虑流式处理
total_examples = None : 句子总数,int,可直接使用model.corpus_count指定
total_words = None : 句中词条总数,int,该参数和total_examples至少要指定一个
epochs = None : 模型迭代次数,需要指定

其他带默认值的参数设定:

start_alpha=None, end_alpha=None, word_count=0, queue_factor=2,
report_delay=1.0, compute_loss=False, callbacks=()

)

# 在评论训练集上建模(大数据集时可能会花费几分钟)
# 本例消耗内存较少,w2vmodel.corpus_count是模型可以直接返回的值
w2vmodel.train(chapter.cut, \
               total_examples = w2vmodel.corpus_count, epochs = 10)
(3444321, 6113050)
# 训练完毕的模型实质
print(w2vmodel.wv["郭靖"].shape)#因为前面规定了是300
w2vmodel.wv["郭靖"]
(300,)
array([-5.71486771e-01, -6.11459553e-01,  9.88356650e-01, -2.19969201e+00,
       -6.24286681e-02, -2.08215073e-01, -4.94846284e-01,  4.11184639e-01,
        3.51217866e-01, -7.16320872e-01,  2.41676435e-01, -5.17797768e-01,
        ......           
       -5.74352980e-01,  3.34270656e-01, -8.82370621e-02, -2.53521979e-01,
        6.62185490e-01, -1.17333210e+00, -5.62149405e-01, -1.21558267e-04,
       -1.54235452e-01,  4.67433631e-01, -3.35694969e-01, -1.63811371e-01],
      dtype=float32)

w2v模型的保存和复用:
w2vmodel.save(存盘路径及文件名称)

w2vmodel.load(存盘路径及文件名称)

1.1.3 词向量间的相似度

w2vmodel.wv.most_similar(词条)

w2vmodel.wv.most_similar("郭靖",topn = 20)#又结果可以看出和黄蓉相似度最高
[('黄蓉', 0.932035505771637),
 ('欧阳克', 0.8728183507919312),
 ('穆念慈', 0.7740538120269775),
 ('梅超风', 0.7715764045715332),
 ('李萍', 0.7549037337303162),
 ('完颜康', 0.7546292543411255),
 ('黄药师', 0.7424267530441284),
 ('陆冠英', 0.7395749688148499),
 ('杨康', 0.7385720014572144),
 ('鲁有脚', 0.7347890138626099),
 ('周伯通', 0.7241591811180115),
 ('裘千仞', 0.722597599029541),
 ('欧阳锋', 0.7211623787879944),
 ('华筝', 0.7194427847862244),
 ('洪七公', 0.7096649408340454),
 ('程瑶迦', 0.7055790424346924),
 ('穆易', 0.7018767595291138),
 ('那公子', 0.6987137198448181),
 ('那道人', 0.6943396329879761),
 ('一灯', 0.6929605007171631)]
# 寻找对应关系
w2vmodel.wv.most_similar(positive=['郭靖', '小红马'], \
                         negative=['黄药师'], topn = 5)#positive negative可以省
#意思为由'郭靖'和'小红马'的关系,寻找'黄药师'的对应关系
[('奔', 0.8444687128067017),
 ('过去', 0.7988641858100891),
 ('晕', 0.7967531681060791),
 ('刚', 0.7961419224739075),
 ('几步', 0.7911766767501831)]
# 计算两个词的相似度/相关程度
print(w2vmodel.wv.similarity("郭靖", "黄蓉"))
print(w2vmodel.wv.similarity("郭靖", "杨康"))
print(w2vmodel.wv.similarity("郭靖", "杨铁心"))
0.93203545
0.73857194
0.6454911
# 寻找不合群的词
w2vmodel.wv.doesnt_match("小红马 黄药师 鲁有脚".split())
'小红马'

1.2文档相似度

1.2.1 基于词袋模型计算

1.sklearn实现

sklearn.metrics.pairwise.pairwise_distances(

X : 用于计算距离的数组

[n_samples_a, n_samples_a] if metric == 'precomputed'

[n_samples_a, n_features] otherwise

Y = None : 用于计算距离的第二数组,当metric != 'precomputed’时可用

metric = ‘euclidean’ : 空间距离计算方式

scikit-learn原生支持 : ['cityblock', 'cosine', 'euclidean', 
    'l1', 'l2', 'manhattan'],可直接使用稀疏矩阵格式
来自scipy.spatial.distance : ['braycurtis', 'canberra', 
    'chebyshev', 'correlation', 'dice', 'hamming', 'jaccard',
    'kulsinski', 'mahalanobis', 'matching', 'minkowski',
    'rogerstanimoto', 'russellrao', 'seuclidean', 'sokalmichener',
    'sokalsneath', 'sqeuclidean', 'yule'] 不支持稀疏矩阵格式

n_jobs = 1 : 用于计算的线程数,为-1时,所有CPU内核都用于计算

)

#得出词频矩阵
cleanchap = [ " ".join(m_cut(w)) for w in chapter.txt.iloc[:5]] 

from sklearn.feature_extraction.text import CountVectorizer

countvec = CountVectorizer() 

resmtx = countvec.fit_transform(cleanchap)
resmtx
<5x11623 sparse matrix of type '<class 'numpy.int64'>'
	with 16884 stored elements in Compressed Sparse Row format>
from sklearn.metrics.pairwise import pairwise_distances

pairwise_distances(resmtx, metric = 'cosine')#得出两两的关联度
#结果阅读竖着和横着都表示第几章,同理,对角线看一半即可
array([[0.        , 0.63104216, 0.77665559, 0.78745025, 0.82985512],
       [0.63104216, 0.        , 0.62572437, 0.61666388, 0.73192845],
       [0.77665559, 0.62572437, 0.        , 0.51645443, 0.5299046 ],
       [0.78745025, 0.61666388, 0.51645443, 0.        , 0.42108002],
       [0.82985512, 0.73192845, 0.5299046 , 0.42108002, 0.        ]])
pairwise_distances(resmtx) # 默认值为euclidean,欧式距离,得出词条的空间直线距离
array([[  0.        , 290.85391522, 312.60678176, 315.54872841,
        311.46267834],
       [290.85391522,   0.        , 266.95130642, 265.77622166,
        277.24898557],
       [312.60678176, 266.95130642,   0.        , 233.9615353 ,
        226.09290126],
       [315.54872841, 265.77622166, 233.9615353 ,   0.        ,
        202.57344347],
       [311.46267834, 277.24898557, 226.09290126, 202.57344347,
          0.        ]])
# 使用TF-IDF矩阵进行相似度计算
from sklearn.feature_extraction.text import TfidfTransformer
txtlist = [ " ".join(m_cut(w)) for w in chapter.txt.iloc[:5]] 
vectorizer = CountVectorizer() #构造一个实例,联想C语言
X = vectorizer.fit_transform(txtlist) # 将文本中的词语转换为词频矩阵  
transformer = TfidfTransformer()  
tfidf = transformer.fit_transform(X)  #基于词频矩阵X计算TF-IDF值  
tfidf#结果是一个稀疏矩阵,较大,压缩存储,所以结果没有展示出来
pairwise_distances(tfidf[:5], metric = 'cosine')
array([[0.        , 0.69112864, 0.84741572, 0.85725434, 0.89178365],
       [0.69112864, 0.        , 0.74381846, 0.70586805, 0.81759092],
       [0.84741572, 0.74381846, 0.        , 0.60083695, 0.63539482],
       [0.85725434, 0.70586805, 0.60083695, 0.        , 0.54123168],
       [0.89178365, 0.81759092, 0.63539482, 0.54123168, 0.        ]])
2.gensim实现

主要是用于基于LDA计算余弦相似度
需要使用的信息:

拟合完毕的lda模型
按照拟合模型时矩阵种类转换的需检索文本
需检索的文本
建模时使用的字典
#计算相似矩阵,不过很难使用
from gensim import similarities
simmtx = similarities.MatrixSimilarity(corpus)
simmtx
<gensim.similarities.docsim.MatrixSimilarity at 0x230bb8ec550>

检索和第1章内容最相似(所属主题相同)的章节

simmtx = similarities.MatrixSimilarity(corpus) # 使用的矩阵种类需要和拟合模型时相同
simmtx
<gensim.similarities.docsim.MatrixSimilarity at 0x230bb8ec438>
simmtx.index[:2]#一列为一章节,内容为其相应词条在空间的坐标值 
array([[0.00370857, 0.11125696, 0.00370857, ..., 0.        , 0.        ,
        0.        ],
       [0.        , 0.15784042, 0.        , ..., 0.        , 0.        ,
        0.        ]], dtype=float32)
# 使用gensim的LDA拟合结果进行演示
query = chapter.txt[1] 
query_bow = dictionary.doc2bow(m_cut(query))#转化为bow向量

lda_vec = ldamodel[query_bow] # 转换为lda模型下的向量
sims = simmtx[lda_vec] # 进行矩阵内向量和所提供向量的余弦相似度查询
sims = sorted(enumerate(sims), key=lambda item: -item[1])#进行排序
sims#有结果可以看出,和第一章相似度最的就是第一章
#有于数据量等问题,较好的拟合后的模型应该是接近于1的
[(38, 0.011200049),
 (1, 0.009635941),
 (26, 0.0077462136),
 ......    
 (20, 0.0016734296),
 (19, 0.0012708347),
 (13, 0.0012344222)]

1.2.2 doc2vec(应多使用词袋模型的方式)

doc2vec建议多大量文本时候使用

word2vec用来计算词条相似度非常合适。

较短的文档如果希望计算文本相似度,可以将各自内部的word2vec向量分别进行平均,用平均后的向量作为文本向量,从而用于计算相似度。

但是对于长文档,这种平均的方式显然过于粗糙。

doc2vec是word2vec的拓展,它可以直接获得sentences/paragraphs/documents的向量表达,从而可以进一步通过计算距离来得到sentences/paragraphs/documents之间的相似性。

模型概况

分析目的:获得文档的一个固定长度的向量表达。
数据:多个文档,以及它们的标签,一般可以用标题作为标签。 
影响模型准确率的因素:语料的大小,文档的数量,越多越高;文档的相似性,越相似越好。
import jieba 
import gensim
from gensim.models import doc2vec

def m_doc(doclist):
    reslist = []
    for i, doc in enumerate(doclist):
        reslist.append(doc2vec.TaggedDocument(jieba.lcut(doc), [i]))#添加每个文档的标题
    return reslist

corp = m_doc(chapter.txt)
corp[:2]
[TaggedDocument(words=['第一回', ' ', '风雪', '惊变', '那', '说话', '人', '五十', '来', '岁', '年纪', ',', '一件', '青布', '长袍', '早洗', '得', '褪成', '了', '蓝灰', '带白', '。', '只',...... '听', '他', '两片', '梨花', '木, '过', '了', '一会', ',', '颜烈', '道', ':', '“', '娘子', '请', '自', '宽', '便', ',', '小人', '出去', '买', '了', '物品', '就', '回', '。', '”', '包惜弱', '点', '了', '点头', ',', '道', ':', '“', '相公', '可别', '太多', '花费', '了', '。', '”', '颜烈', '微笑', '道', ':', '“', '就', '可惜', '娘子', '在', '服丧', ',', '不能', '戴用', '珠宝', ',', '要', '多花钱', '也', '花', '不了', '。', '”', '], tags=[0]),
 TaggedDocument(words=['第二回', ' ', '江南七怪', '颜烈', '跨出', '房门', ',', '过道', '中', '一个', ......'中年', '士人', '拖', '着', '鞋', '皮', ',', '踢', '跶', '踢', '跶', '的', '直响', ',', '一路打', '着', '哈欠', '迎面', '过来', ',', '那', '士人', '似笑非笑', ',', '挤眉弄眼', ',', ''没', '洗脸', '了', ',', '拿', '着', '一', '柄', '破烂', '的', '油纸', '黑扇', ',', '边摇边行', '。段天德', '吓', '得', '魂不附体', ',', '哪里', '还敢', '停留', ',', '拉', '了', '李萍', ',', '急奔', '而出', '。', '李萍大', '叫', ':', '“', '救命', '啊', ',', '我', '不', '去', ',', '救命', '啊', '!', '”', '终于', '声音', '越来越', '远', '。], tags=[1])]
#进行模型的设定与拟合
d2vmodel = gensim.models.Doc2Vec(vector_size = 300, 
                window = 20, min_count = 5)#300维,窗口为20
%time d2vmodel.build_vocab(corp)#进行拟合
Wall time: 1.88 s
d2vmodel.wv.vocab#查看生成的结果,为每一个词都生成一个向量
{' ': <gensim.models.keyedvectors.Vocab at 0x230b88bef98>,
 '风雪': <gensim.models.keyedvectors.Vocab at 0x230b88bedd8>,
 '那': <gensim.models.keyedvectors.Vocab at 0x230c4fea780>,
 '说话': <gensim.models.keyedvectors.Vocab at 0x230c4fea828>,
  ......
 '曲三': <gensim.models.keyedvectors.Vocab at 0x230c7f9f470>,
 '掘': <gensim.models.keyedvectors.Vocab at 0x230c7fb49e8>,
 '最后': <gensim.models.keyedvectors.Vocab at 0x230c7fb4a20>,
 '一具': <gensim.models.keyedvectors.Vocab at 0x230c7fb4a58>,
 ...}
# 如果有个新文本想要呈现的话,将新文本转换为相应维度空间下的向量
newvec = d2vmodel.infer_vector(jieba.lcut(chapter.txt[1]))
#检索出和新文本相比最相似的章节
d2vmodel.docvecs.most_similar([newvec], topn = 10)#如果只有一个list,需要打上[]
[(33, 0.17437607049942017),
 (36, 0.10029015690088272),
 (22, 0.07945042848587036),
 (18, 0.04506886377930641),
 (14, 0.03940583020448685),
 (23, 0.035881608724594116),
 (6, 0.03350480645895004),
 (31, 0.03314536064863205),
 (16, 0.028236132115125656),
 (13, 0.022862425073981285)]

1.3文档聚类

在得到文档相似度的计算结果后,文档聚类问题在本质上已经和普通的聚类分析没有区别。

注意:最常用的Kmeans使用的是平方欧氏距离,这在文本聚类中很可能无法得到最佳结果。

算法的速度和效果同样重要。

# 为章节增加名称标签
chapter.index = [raw.txt[raw.chap == i].iloc[0] for i in chapter.index]
chapter.head()
txtcut
第一回 风雪惊变第一回 风雪惊变      那说话人五十来岁年纪,一件青布长袍早洗得褪成了蓝灰带白。只听他两...[第一回, , 风雪, 惊变,  ,  ,  ,  ,  ,  , 那, 说话, 人, 五...
第二回 江南七怪第二回 江南七怪    颜烈跨出房门,过道中一个中年士人拖着鞋皮,踢跶踢跶的直响,一路打着哈...[第二回, , 江南七怪,  ,  ,  ,  , 颜烈, 跨出, 房门, ,, 过道, ...
第三回 黄沙莽莽第三回 黄沙莽莽    寺里僧众见焦木圆寂,尽皆悲哭。有的便为伤者包扎伤处,抬入客舍。  忽...[第三回, , 黄沙, 莽莽,  ,  ,  ,  , 寺里, 僧众, 见, 焦木, 圆寂...
第四回 黑风双煞第四回 黑风双煞    完颜洪熙笑道:“好,再打他个痛快。”蒙古兵前哨报来:“王罕亲自前来迎...[第四回, , 黑风双, 煞,  ,  ,  ,  , 完颜洪熙, 笑, 道, :, “,...
第五回 弯弓射雕第五回 弯弓射雕    一行人下得山来,走不多时,忽听前面猛兽大吼声一阵阵传来。韩宝驹一提缰...[第五回, , 弯弓, 射雕,  ,  ,  ,  , 一行, 人下, 得, 山来, ,,...
#进行文本清理
import jieba
cuttxt = lambda x: " ".join(m_cut(x)) #循环
cleanchap = chapter.txt.apply(cuttxt) 
cleanchap[:2]
第一回 风雪惊变    第一回 风雪 惊变 说话 五十 年纪 一件 青布 长袍 早洗 褪成 蓝灰 带白 两片 梨花 ...
第二回 江南七怪    第二回 江南七怪 颜烈 跨出 房门 过道 一个 中年 士人 直响 一路打 哈欠 迎面 士人 ...
Name: txt, dtype: object
# 计算TF-IDF矩阵
from sklearn.feature_extraction.text import TfidfTransformer

vectorizer = CountVectorizer() 
wordmtx = vectorizer.fit_transform(cleanchap) # 将文本中的词语转换为词频矩阵  

transformer = TfidfTransformer()  
tfidf = transformer.fit_transform(wordmtx)  #基于词频矩阵计算TF-IDF值  
tfidf
<40x43931 sparse matrix of type '<class 'numpy.float64'>'
	with 129387 stored elements in Compressed Sparse Row format>
# 进行聚类分析
#kmeas用词频矩阵不合适,因此使用tfidf比较合适
from sklearn.cluster import KMeans  

clf = KMeans(n_clusters = 5)  #聚为5类,即:原来分40个章节,现在聚为4大类
s = clf.fit(tfidf)  
print(s)  
clf.cluster_centers_ #把中心的情况打印出来
KMeans(n_clusters=5)

array([[0.        , 0.        , 0.        , ..., 0.        , 0.00281383,
        0.        ],
       [0.00048249, 0.        , 0.        , ..., 0.00098597, 0.        ,
        0.00048249],
       [0.        , 0.        , 0.        , ..., 0.        , 0.        ,
        0.        ],
       [0.        , 0.        , 0.        , ..., 0.        , 0.        ,
        0.        ],
       [0.        , 0.00080214, 0.00080214, ..., 0.        , 0.        ,
        0.        ]])
clf.cluster_centers_.shape
(5, 43931)
clf.labels_#分别看下每个章节都被归为哪一类
array([2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 1, 4, 1, 1, 1, 1, 1, 1, 1,
       1, 4, 1, 1, 1, 1, 0, 0, 0, 1, 4, 1, 1, 1, 1, 1, 1, 1])
#为了便于阅读,直接把类别情况加入数据框中
chapter['clsres'] = clf.labels_
chapter.head()
txtcutclsres
第一回 风雪惊变第一回 风雪惊变      那说话人五十来岁年纪,一件青布长袍早洗得褪成了蓝灰带白。只听他两...[第一回, , 风雪, 惊变,  ,  ,  ,  ,  ,  , 那, 说话, 人, 五...2
第二回 江南七怪第二回 江南七怪    颜烈跨出房门,过道中一个中年士人拖着鞋皮,踢跶踢跶的直响,一路打着哈...[第二回, , 江南七怪,  ,  ,  ,  , 颜烈, 跨出, 房门, ,, 过道, ...2
第三回 黄沙莽莽第三回 黄沙莽莽    寺里僧众见焦木圆寂,尽皆悲哭。有的便为伤者包扎伤处,抬入客舍。  忽...[第三回, , 黄沙, 莽莽,  ,  ,  ,  , 寺里, 僧众, 见, 焦木, 圆寂...3
第四回 黑风双煞第四回 黑风双煞    完颜洪熙笑道:“好,再打他个痛快。”蒙古兵前哨报来:“王罕亲自前来迎...[第四回, , 黑风双, 煞,  ,  ,  ,  , 完颜洪熙, 笑, 道, :, “,...3
第五回 弯弓射雕第五回 弯弓射雕    一行人下得山来,走不多时,忽听前面猛兽大吼声一阵阵传来。韩宝驹一提缰...[第五回, , 弯弓, 射雕,  ,  ,  ,  , 一行, 人下, 得, 山来, ,,...3
chapter.sort_values('clsres').clsres#进行排序
第三十回 一灯大师      0
第三十一回 鸳鸯锦帕     0
第二十九回 黑沼隐女     0
......  
第十三回 五湖废人      4
第九回 铁枪破犁       4
Name: clsres, dtype: int32
#把对应类别的文档归为一个大文档在进行处理
chapgrp = chapter.groupby('clsres')
chapcls = chapgrp.agg(sum) # 只有字符串列的情况下,sum函数自动转为合并字符串

cuttxt = lambda x: " ".join(m_cut(x)) 
chapclsres = chapcls.txt.apply(cuttxt) 
chapclsres
clsres
0    第二十九回 黑沼 隐女 郭靖 呼叫 召唤 小红马 地下 转眼之间 双雕 飞出 老远 雌雄 双...
1    第十四回 桃花 岛主 五男一女 走进 厅来 却是 江南 六怪 北南 故乡 日近 这天 太湖 ...
2    第一回 风雪 惊变 说话 五十 年纪 一件 青布 长袍 早洗 褪成 蓝灰 带白 两片 梨花 ...
3    第三回 黄沙 莽莽 寺里 僧众 焦木 圆寂 悲哭 伤者 包扎 伤处 抬入 客舍 巨钟 缸内 ...
4    第七回 比武招亲 江南 六怪 郭靖 晓行夜宿 东南 进发 非止 一日 大漠 草原 这天 张家...
Name: txt, dtype: object
# 列出关键词以刻画类别特征
import jieba.analyse as ana

ana.set_stop_words('停用词.txt')
#为了方便,直接使用jieba分词里面列关键词的功能
for item in chapclsres:
    print(ana.extract_tags(item, topK = 10))
    
['黄蓉', '郭靖', '一灯', '黄蓉道', '那书生', '渔人', '一灯大师', '农夫', '段皇爷', '郭靖道']
['郭靖', '黄蓉', '欧阳锋', '黄药师', '洪七公', '周伯通', '裘千仞', '武功', '郭靖道', '欧阳克']
['丘处机', '杨铁心', '包惜弱', '郭啸天', '段天德', '颜烈', '完颜洪烈', '柯镇恶', '金兵', '道长']
['郭靖', '铁木真', '柯镇恶', '韩小莹', '梅超风', '哲别', '朱聪', '韩宝驹', '拖雷', '华筝']
['郭靖', '黄蓉', '洪七公', '完颜康', '师父', '彭连虎', '陆冠英', '梁子翁', '欧阳克', '穆念慈']

实战练习

在计算词条相似度时进行停用词清理,然后再进行拟合,思考为什么会有这样的结果出现。

在基于词袋模型,使用原始词频计算文本余弦相似度时,比较清理停用词前后的结果。

#第一题进行停用词清理,在计算词条相似度
import jieba
import pandas as pd
tmpdf = pd.read_table('停用词.txt',names = ['w'], encoding = 'utf-8')
chapter['cut'] = chapter.txt.apply(jieba.lcut)
chapter['cut'] =[ w for w in chapter['cut']  if w not in list(tmpdf.w) ] 
chapter.head()
Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\A\AppData\Local\Temp\jieba.cache
Loading model cost 0.557 seconds.
Prefix dict has been built successfully.
txtcut
chap
1.0第一回 风雪惊变      那说话人五十来岁年纪,一件青布长袍早洗得褪成了蓝灰带白。只听他两...[第一回, , 风雪, 惊变,  ,  ,  ,  ,  ,  , 那, 说话, 人, 五...
2.0第二回 江南七怪    颜烈跨出房门,过道中一个中年士人拖着鞋皮,踢跶踢跶的直响,一路打着哈...[第二回, , 江南七怪,  ,  ,  ,  , 颜烈, 跨出, 房门, ,, 过道, ...
3.0第三回 黄沙莽莽    寺里僧众见焦木圆寂,尽皆悲哭。有的便为伤者包扎伤处,抬入客舍。  忽...[第三回, , 黄沙, 莽莽,  ,  ,  ,  , 寺里, 僧众, 见, 焦木, 圆寂...
4.0第四回 黑风双煞    完颜洪熙笑道:“好,再打他个痛快。”蒙古兵前哨报来:“王罕亲自前来迎...[第四回, , 黑风双, 煞,  ,  ,  ,  , 完颜洪熙, 笑, 道, :, “,...
5.0第五回 弯弓射雕    一行人下得山来,走不多时,忽听前面猛兽大吼声一阵阵传来。韩宝驹一提缰...[第五回, , 弯弓, 射雕,  ,  ,  ,  , 一行, 人下, 得, 山来, ,,...
# 初始化word2vec模型和词表
from gensim.models.word2vec import Word2Vec
n_dim = 300 # 指定向量维度,大样本量时300~500较好
w2vmodel = Word2Vec(size = n_dim, min_count = 10)
w2vmodel.build_vocab(chapter.cut) # 生成词表

# 在评论训练集上建模(大数据集时可能会花费几分钟)
w2vmodel.train(chapter.cut, \
               total_examples = w2vmodel.corpus_count, epochs = 10)
    
# 训练完毕的模型实质
print(w2vmodel.wv["郭靖"].shape)
w2vmodel.wv["郭靖"]
(300,)

array([ 0.34924605,  0.21265522,  0.5632632 ,  0.3580753 ,  0.17115285,
        0.48449898,  0.11169011,  0.33496168,  0.8114401 ,  0.6040344 ,
        0.27803087, -0.56468904, -0.05460463, -0.11949269, -0.08181898,
        ......
        0.2831581 ,  0.60918677, -0.17329499,  0.11961225, -0.30500183,
       -0.31079102, -0.64060545,  0.03041249,  0.66229236,  0.88437986],
      dtype=float32)
#基于词袋模型计算文本余弦相似度时,比较清理停用词前后的结果
#实例给出了加入停用词前的,基于前面的现在计算停用词后的
import jieba
import pandas as pd
tmpdf = pd.read_table('停用词.txt',names = ['w'], encoding = 'utf-8')
chapter['cut'].iloc[:5]= [ " ".join(m_cut(w)) for w in chapter.txt.iloc[:5] ]
chapter['cut'].iloc[:5]=[ w for w in chapter['cut'].iloc[:5] if w not in list(tmpdf.w) ] 

from sklearn.feature_extraction.text import CountVectorizer

countvec = CountVectorizer() 

resmtx = countvec.fit_transform(chapter['cut'].iloc[:5])
resmtx
<5x11623 sparse matrix of type '<class 'numpy.int64'>'
	with 16884 stored elements in Compressed Sparse Row format>
from sklearn.metrics.pairwise import pairwise_distances

pairwise_distances(resmtx, metric = 'cosine')
#根据结果,停用词前后结果相同
array([[0.        , 0.63104216, 0.77665559, 0.78745025, 0.82985512],
       [0.63104216, 0.        , 0.62572437, 0.61666388, 0.73192845],
       [0.77665559, 0.62572437, 0.        , 0.51645443, 0.5299046 ],
       [0.78745025, 0.61666388, 0.51645443, 0.        , 0.42108002],
       [0.82985512, 0.73192845, 0.5299046 , 0.42108002, 0.        ]])

2.文档分类

朴素贝叶斯算法(优先考虑使用)

2.1 sklearn实现

sklearn是标准的数据挖掘建模工具包,在语料转换为d2m矩阵结构之后,就可以使用所有标准的DM建模手段在sklearn中进行分析。

在sklearn中也实现了朴素贝叶斯算法,使用方式上也和其他模型非常相似。

2.1.1 使用贝叶斯进行分类

生成D2M矩阵
# 从原始语料df中提取出所需的前两章段落
raw12 = raw[raw.chap.isin([1,2])]
raw12ana = raw12.iloc[list(raw12.txt.apply(len) > 50), :] # 只使用超过50字的段落
raw12ana.reset_index(drop = True, inplace = True)#对索引再排序
print(len(raw12ana))
raw12ana.head()
376
txtchap
0那说话人五十来岁年纪,一件青布长袍早洗得褪成了蓝灰带白。只听他两片梨花木板碰了几下,左手...1.0
1那说话人将木板敲了几下,说道:“这首七言诗,说的是兵火过后,原来的家家户户,都变成了断墙...1.0
2“叶老汉和叶妈妈吓得呆了,扑将上去,搂住了儿子的尸体,放声大哭。那长官提起狼牙棒,一棒一...1.0
3“可是那金兵占了我大宋天下,杀人放火,奸淫掳掠,无恶不作,却又不见他遭到什么报应。只怪我...1.0
4村民中走出一个二十来岁的大汉,说道:“张先生,你可是从北方来吗?”说的是北方口音。张十五...1.0
import jieba
cuttxt = lambda x: " ".join(jieba.lcut(x)) # 这里不做任何清理工作,以保留情感词
raw12ana["cleantxt"] = raw12ana.txt.apply(cuttxt) 
raw12ana.head()
<ipython-input-12-b42f436ca4f7>:3: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  raw12ana["cleantxt"] = raw12ana.txt.apply(cuttxt)
txtchapcleantxt
0那说话人五十来岁年纪,一件青布长袍早洗得褪成了蓝灰带白。只听他两片梨花木板碰了几下,左手...1.0那 说话 人 五十 来 岁 年纪 , 一件 青布 长袍 早洗 得 褪成 了 蓝灰 带...
1那说话人将木板敲了几下,说道:“这首七言诗,说的是兵火过后,原来的家家户户,都变成了断墙...1.0那 说话 人 将 木板 敲 了 几下 , 说道 : “ 这首 七言诗 , 说 的 是...
2“叶老汉和叶妈妈吓得呆了,扑将上去,搂住了儿子的尸体,放声大哭。那长官提起狼牙棒,一棒一...1.0“ 叶老汉 和 叶 妈妈 吓 得 呆 了 , 扑 将 上去 , 搂住 了 儿子 的 ...
3“可是那金兵占了我大宋天下,杀人放火,奸淫掳掠,无恶不作,却又不见他遭到什么报应。只怪我...1.0“ 可是 那金兵 占 了 我 大宋 天下 , 杀人放火 , 奸淫掳掠 , 无恶不作 ...
4村民中走出一个二十来岁的大汉,说道:“张先生,你可是从北方来吗?”说的是北方口音。张十五...1.0村民 中 走出 一个二十 来 岁 的 大汉 , 说道 : “ 张 先生 , 你 可是...
#转换为词频矩阵
from sklearn.feature_extraction.text import CountVectorizer
countvec = CountVectorizer() 
wordmtx = countvec.fit_transform(raw12ana.cleantxt)
wordmtx
<376x6724 sparse matrix of type '<class 'numpy.int64'>'
	with 13642 stored elements in Compressed Sparse Row format>
# 作用:将数据集划分为 训练集和测试集
from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(wordmtx, raw12ana.chap, 
    test_size = 0.3, random_state = 111)
#要训练的矩阵,期待的结果,测试集的比例(30%),随机数(相同则相同结果重现)
#拟合朴素贝叶斯模型

from sklearn import naive_bayes#导入贝叶斯
NBmodel = naive_bayes.MultinomialNB()
# 拟合模型
NBmodel.fit(x_train, y_train)
# 进行验证集预测
x_test
<113x6724 sparse matrix of type '<class 'numpy.int64'>'
	with 4127 stored elements in Compressed Sparse Row format>
NBmodel.predict(x_test)#查看预测结果
array([2., 2., 1., 2., 1., 2., 1., 1., 2., 1., 2., 1., 2., 2., 1., 2., 1.,
       2., 1., 1., 2., 2., 1., 1., 2., 1., 2., 1., 1., 2., 1., 2., 1., 1.,
       2., 1., 1., 1., 1., 1., 1., 2., 1., 1., 2., 2., 2., 1., 1., 1., 2.,
       1., 1., 1., 2., 1., 2., 1., 2., 2., 2., 2., 1., 1., 1., 1., 1., 1.,
       1., 1., 2., 2., 1., 1., 1., 1., 1., 2., 1., 2., 1., 1., 2., 2., 2.,
       1., 1., 1., 2., 1., 1., 2., 1., 1., 1., 2., 2., 2., 2., 2., 1., 1.,
       2., 1., 1., 2., 2., 2., 2., 1., 1., 2., 2.])
# 预测准确率(给模型打分)
print('训练集:', NBmodel.score(x_train, y_train), 
      ',验证集:', NBmodel.score(x_test, y_test))
训练集: 1.0 ,验证集: 0.9557522123893806
#做出更详细的验证报告
from sklearn.metrics import classification_report

print(classification_report(y_test, NBmodel.predict(x_test)))#验证集的实测结果与验证集的预测结果相比较
              precision    recall  f1-score   support

         1.0       0.94      0.98      0.96        61
         2.0       0.98      0.92      0.95        52

    accuracy                           0.96       113
   macro avg       0.96      0.95      0.96       113
weighted avg       0.96      0.96      0.96       113
模型预测

将需要预测的文本转换为和建模时格式完全对应的d2m矩阵格式,随后即可进行预测。

countvec.vocabulary_#查看转换的时候的字典
{'说话': 5703,
 '五十': 765,
 '年纪': 2855,
 ......
 '握住': 3565,
 '猎叉': 4588,
 '随即': 6438,
 ...}
string = "杨铁心和包惜弱收养穆念慈"
words = " ".join(jieba.lcut(string))
words_vecs = countvec.transform([words]) #需要用[] 转换为可迭代的list格式
words_vecs
<1x6724 sparse matrix of type '<class 'numpy.int64'>'
	with 2 stored elements in Compressed Sparse Row format>
NBmodel.predict(words_vecs)
array([1.])

2.1.2 使用Logistic回归模型进行分类

from sklearn.linear_model import LogisticRegression

logitmodel = LogisticRegression() # 定义Logistic回归模型
# 拟合模型
logitmodel.fit(x_train, y_train)
print(classification_report(y_test, logitmodel.predict(x_test)))
              precision    recall  f1-score   support

         1.0       0.94      0.98      0.96        61
         2.0       0.98      0.92      0.95        52

    accuracy                           0.96       113
   macro avg       0.96      0.95      0.96       113
weighted avg       0.96      0.96      0.96       113

2.2 NLTK实现

NLTK中内置了朴素贝叶斯算法,可直接实现文档分类。

数据集中语料的格式
用于训练的语料必须是分词完毕的字典形式,词条为键名,键值则可以是数值、字符、或者T/F

{‘张三’ : True, ‘李四’ : True, ‘王五’ : False}

{‘张三’ : 1, ‘李四’ : 1, ‘王五’ : 0}

{‘张三’ : ‘有’, ‘李四’ : ‘有’, ‘王五’ : ‘无’}

训练用数据集的格式

训练用数据集为list of list格式,每个成员为list[语料字典, 结果变量]

[

[{‘张三’ : 1, ‘李四’ : 1, ‘王五’ : 0}, ‘合格’],

[{‘张三’ : 0, ‘李四’ : 1, ‘王五’ : 0}, ‘不合格’]

]

构建模型

考虑到过拟合问题,此处需要先拆分好训练集和测试集

model = NaiveBayesClassifier.train(training_data)

# 这里直接以章节为一个单元进行分析,以简化程序结构
import nltk
from nltk import FreqDist

# 生成完整的词条频数字典,这部分也可以用遍历方式实现
fdist1 = FreqDist(m_cut(chapter.txt[1])) 
fdist2 = FreqDist(m_cut(chapter.txt[2])) 
fdist3 = FreqDist(m_cut(chapter.txt[3])) 
fdist1
FreqDist({'第一回': 1,
          '风雪': 3,
          '惊变': 1,
          '说话': 9,
          ......
          '一具': 1,
          '黑色': 1,
          '盘形': 1,
          ...})
#导入贝叶斯
from nltk.classify import NaiveBayesClassifier

#分别引入第一二三章,生成训练数据
training_data = [ [fdist1, 'chap1'], [fdist2, 'chap2'], [fdist3, 'chap3'] ]
# 训练分类模型
NLTKmodel = NaiveBayesClassifier.train(training_data)
#进行预测
print(NLTKmodel.classify(FreqDist(m_cut("杨铁心收养穆念慈"))))
print(NLTKmodel.classify(FreqDist(m_cut("钱塘江 日日夜夜 包惜弱 颜烈 使出杨家枪"))))
chap3
chap2
#模型拟合效果的考察
nltk.classify.accuracy(NLTKmodel, training_data) # 准确度评价
#参数:哪个模型、相应的数据集
1.0
#显示出最有价值的词条特征
NLTKmodel.show_most_informative_features(5)#得到似然比,检测对于哪些特征有用
Most Informative Features
                      飞将 = None            chap3 : chap2  =      1.0 : 1.0
                      几茎 = None            chap2 : chap1  =      1.0 : 1.0

实战作业

对射雕的前两个章节提取关键字,然后使用关键字而不是原始文本进行文档分类,比较这样两种方式的分类效果有何变化。

减少用于训练的样本量,考察使用朴素贝叶斯算法或者其他标准分类算法时,模型效果的变化趋势。

自行实现基于NLTK的按段落为单位进行章节分类的程序。

import jieba
import jieba.analyse
import jieba
chapter['cut'] = chapter.txt.apply(jieba.lcut)
chapter.head()
Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\A\AppData\Local\Temp\jieba.cache
Loading model cost 0.555 seconds.
Prefix dict has been built successfully.
txtcut
chap
1.0第一回 风雪惊变      那说话人五十来岁年纪,一件青布长袍早洗得褪成了蓝灰带白。只听他两...[第一回, , 风雪, 惊变,  ,  ,  ,  ,  ,  , 那, 说话, 人, 五...
2.0第二回 江南七怪    颜烈跨出房门,过道中一个中年士人拖着鞋皮,踢跶踢跶的直响,一路打着哈...[第二回, , 江南七怪,  ,  ,  ,  , 颜烈, 跨出, 房门, ,, 过道, ...
3.0第三回 黄沙莽莽    寺里僧众见焦木圆寂,尽皆悲哭。有的便为伤者包扎伤处,抬入客舍。  忽...[第三回, , 黄沙, 莽莽,  ,  ,  ,  , 寺里, 僧众, 见, 焦木, 圆寂...
4.0第四回 黑风双煞    完颜洪熙笑道:“好,再打他个痛快。”蒙古兵前哨报来:“王罕亲自前来迎...[第四回, , 黑风双, 煞,  ,  ,  ,  , 完颜洪熙, 笑, 道, :, “,...
5.0第五回 弯弓射雕    一行人下得山来,走不多时,忽听前面猛兽大吼声一阵阵传来。韩宝驹一提缰...[第五回, , 弯弓, 射雕,  ,  ,  ,  , 一行, 人下, 得, 山来, ,,...
#对前两章提取关键词
key1=jieba.analyse.extract_tags(chapter.txt[1])
key2=jieba.analyse.extract_tags(chapter.txt[2])
key=key1+key2
key
['杨铁心',
 '包惜弱',
 '郭啸天',
 '颜烈',
 '丘处机',
 '杨二人',
 ......
 '金兵',
 '武功',
 '道长',
 '南希仁',
 '全金发',
 '李萍']
# 转为D2M矩阵
from sklearn.feature_extraction.text import CountVectorizer
countvec = CountVectorizer() 
wordmtx = countvec.fit_transform(key)
wordmtx
<40x34 sparse matrix of type '<class 'numpy.int64'>'
	with 40 stored elements in Compressed Sparse Row format>
# 作用:将数据集划分为 训练集和测试集
from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(wordmtx, raw12ana.chap[:40], 
    test_size = 0.3, random_state = 1)
#测试图片(train_images)和测试标签(train_labels)的长度必须一致
from sklearn import naive_bayes

NBmodel = naive_bayes.MultinomialNB()
# 拟合模型
NBmodel.fit(x_train, y_train)
# 进行验证集预测
x_test
# 预测准确率(给模型打分)
print('训练集:', NBmodel.score(x_train, y_train), 
      ',验证集:', NBmodel.score(x_test, y_test))
训练集: 1.0 ,验证集: 1.0
#减少用于训练的样本量
from sklearn.feature_extraction.text import CountVectorizer
countvec = CountVectorizer() 

wordmtx = countvec.fit_transform(raw12ana.cleantxt)
wordmtx
<376x6724 sparse matrix of type '<class 'numpy.int64'>'
	with 13642 stored elements in Compressed Sparse Row format>
# 作用:将数据集划分为 训练集和测试集,训练样本量改为20%
from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(wordmtx, raw12ana.chap, 
    test_size = 0.8, random_state = 111)
from sklearn import naive_bayes

NBmodel = naive_bayes.MultinomialNB()
# 拟合模型
NBmodel.fit(x_train, y_train)
#模型评估
from sklearn.metrics import classification_report

print(classification_report(y_test, NBmodel.predict(x_test)))
              precision    recall  f1-score   support

         1.0       0.84      0.96      0.89       156
         2.0       0.94      0.81      0.87       145

    accuracy                           0.88       301
   macro avg       0.89      0.88      0.88       301
weighted avg       0.89      0.88      0.88       301

可见训练集过少会直接影响训练的效果

3. 情感分析

情感分析概述

3.1 基于词袋模型的分析

数据概况:

取自购物网站的正向、负向评价各约1万条。

# 读入原始数据集
import pandas as pd

dfpos = pd.read_excel("购物评论.xlsx", sheet_name = "正向", header=None)
dfpos['y'] = 1
dfneg = pd.read_excel("购物评论.xlsx", sheet_name = "负向", header=None)
dfneg['y'] = 0
df0 = dfpos.append(dfneg, ignore_index = True)#两个表格接到一起
df0.head()
0y
0感觉用这个手机下载MP3听很方便,用T-Flash卡装上歌,就可以当一个小MP3用了,很不错...1
1外观美观,速度也不错。上面一排触摸键挺实用。应该对得起这个价格。当然再降点大家肯定也不反对。...1
2我刚拿到书,也没有仔细阅读,就是粗粗的翻了点,觉得还行。里面是蓝黑两种颜色的,有些单词的下面...1
3对于二胡曲的精选,应该出版一个系列套装,可分别出售,单独购买。精品二胡曲是有年代和阶段性的,...1
4用了一年半的 e680 终于送小偷了。刚刚买了一台e850,用了三天,说说我自己的感受。1:...1
# 分词和预处理
import jieba

cuttxt = lambda x: " ".join(jieba.lcut(x)) # 这里不去停用词,以保留情感词
df0["cleantxt"] = df0[0].apply(cuttxt) 
df0.tail()
0ycleantxt
20577服务员太差,房间很吵,隔音不好,房间味道很重,很一般,不如稀土国际大酒店好,收费有低,还是四...0服务员 太 差 , 房间 很 吵 , 隔音 不好 , 房间 味道 很 重 , 很 一般 , ...
20578很早我就说等“爱氏晨曦”吃进蒙牛股份后,对蒙牛的质疑就会停止,如今已应验。想知道单是这一笔...0很早 我 就 说 等 “ 爱氏 晨曦 ” 吃进 蒙牛 股份 后 , 对 蒙牛 的 质疑 ...
205792——10岁是什么意思?我不认为有一本书可以覆盖这样的范围,事实上,我两岁的女儿不能理解这套...02 — — 10 岁 是 什么 意思 ? 我 不 认为 有 一 本书 可以 覆盖 这样 的 ...
20580刚住过这里,感觉不好,房间味道太大,一股子霉味,房间各种设施都很陈旧,我所预订的大床间,却没...0刚住 过 这里 , 感觉不好 , 房间 味道 太 大 , 一股 子 霉味 , 房间 各种 设...
20581一本书200页,综合一下只有3页可看 却要花200页的钱 唉 唉0一 本书 200 页 , 综合 一下 只有 3 页 可 看 却 要 花 200 页 ...
#构建矩阵
from sklearn.feature_extraction.text import CountVectorizer
countvec = CountVectorizer(min_df = 5) # 出现5次以上的才纳入

wordmtx = countvec.fit_transform(df0.cleantxt)
wordmtx
<20582x12014 sparse matrix of type '<class 'numpy.int64'>'
	with 513919 stored elements in Compressed Sparse Row format>
# 按照7:3的比例生成训练集和测试集
from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(
    wordmtx, df0.y, test_size=0.3) # 这里可以直接使用稀疏矩阵格式
x_train[0]
<1x12014 sparse matrix of type '<class 'numpy.int64'>'
	with 14 stored elements in Compressed Sparse Row format>
# 使用SVM进行建模,也可考虑多个建模方法
from sklearn.svm import SVC

clf=SVC(kernel = 'rbf', verbose = True)#常规设定
clf.fit(x_train, y_train) # 内存占用可能较高
clf.score(x_train, y_train)#得出模型的评分
[LibSVM]
0.9535642396057472
# 对模型效果进行评估
from sklearn.metrics import classification_report

print(classification_report(y_test, clf.predict(x_test)))
              precision    recall  f1-score   support

           0       0.87      0.90      0.88      3055
           1       0.89      0.86      0.88      3120

    accuracy                           0.88      6175
   macro avg       0.88      0.88      0.88      6175
weighted avg       0.88      0.88      0.88      6175
#将相应的数据集转为向量,进行预测。下面的函数更为清晰
clf.predict(countvec.transform([df0.cleantxt[0]]))[0]
1
# 模型预测,书写函数,使结果易读
import jieba

def m_pred(string, countvec, model) : 
    words = " ".join(jieba.lcut(string))
    words_vecs = countvec.transform([words]) # 数据需要转换为可迭代格式
     
    result = model.predict(words_vecs)
    
    if int(result[0]) == 1:
        print(string, ":正向")
    else:
        print(string, ":负向")
        

comment = "外观美观,速度也不错。上面一排触摸键挺实用。应该对得起这个价格。当然再降点大家肯定也不反对。风扇噪音也不大。"
m_pred(comment, countvec, clf)
外观美观,速度也不错。上面一排触摸键挺实用。应该对得起这个价格。当然再降点大家肯定也不反对。风扇噪音也不大。 :正向
comment = "作为女儿6.1的礼物。虽然晚到了几天。等拿到的时候,女儿爱不释手,上洗手间也看,告知不好。上周末,告诉我她把火鞋和风鞋拿到学校,好多同学羡慕她。呵呵,我也看了其中的人鸦,只可惜没有看完就在老公的催促下睡了。说了这么多,归纳为一句:这套书买的值。"
m_pred(comment, countvec, clf)
作为女儿6.1的礼物。虽然晚到了几天。等拿到的时候,女儿爱不释手,上洗手间也看,告知不好。上周末,告诉我她把火鞋和风鞋拿到学校,好多同学羡慕她。呵呵,我也看了其中的人鸦,只可惜没有看完就在老公的催促下睡了。说了这么多,归纳为一句:这套书买的值。 :正向

3.2 基于分布式表达的分析(对于短文本来说,效果更优)

和词袋模型相比,分布式表达主要是改变了文本信息的提取方式。

目前主要使用gensim实现相应的算法。

注意:由于矩阵不再是频数值(正负想间),因此不能使用朴素贝叶斯算法来进行拟合。

# 读入原始数据集,和上面完全相同
import pandas as pd

dfpos = pd.read_excel("购物评论.xlsx", sheet_name = "正向", header=None)
dfpos['y'] = 1
dfneg = pd.read_excel("购物评论.xlsx", sheet_name = "负向", header=None)
dfneg['y'] = 0
df0 = dfpos.append(dfneg, ignore_index = True)
df0.head()
0y
0感觉用这个手机下载MP3听很方便,用T-Flash卡装上歌,就可以当一个小MP3用了,很不错...1
1外观美观,速度也不错。上面一排触摸键挺实用。应该对得起这个价格。当然再降点大家肯定也不反对。...1
2我刚拿到书,也没有仔细阅读,就是粗粗的翻了点,觉得还行。里面是蓝黑两种颜色的,有些单词的下面...1
3对于二胡曲的精选,应该出版一个系列套装,可分别出售,单独购买。精品二胡曲是有年代和阶段性的,...1
4用了一年半的 e680 终于送小偷了。刚刚买了一台e850,用了三天,说说我自己的感受。1:...1
# 分词和预处理,生成list of list格式
import jieba

df0['cut'] = df0[0].apply(jieba.lcut)
df0.head()
0ycut
0感觉用这个手机下载MP3听很方便,用T-Flash卡装上歌,就可以当一个小MP3用了,很不错...1[感觉, 用, 这个, 手机, 下载, MP3, 听, 很, 方便, ,, 用, T, -,...
1外观美观,速度也不错。上面一排触摸键挺实用。应该对得起这个价格。当然再降点大家肯定也不反对。...1[外观, 美观, ,, 速度, 也, 不错, 。, 上面, 一排, 触摸, 键, 挺, 实用...
2我刚拿到书,也没有仔细阅读,就是粗粗的翻了点,觉得还行。里面是蓝黑两种颜色的,有些单词的下面...1[我刚, 拿到, 书, ,, 也, 没有, 仔细阅读, ,, 就是, 粗粗, 的, 翻, 了...
3对于二胡曲的精选,应该出版一个系列套装,可分别出售,单独购买。精品二胡曲是有年代和阶段性的,...1[对于, 二胡曲, 的, 精选, ,, 应该, 出版, 一个系列, 套装, ,, 可, 分别...
4用了一年半的 e680 终于送小偷了。刚刚买了一台e850,用了三天,说说我自己的感受。1:...1[用, 了, 一年, 半, 的, , e680, , 终于, 送, 小偷, 了, 。, ...
 # 按照7:3的比例生成训练集和测试集
from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(
    df0.cut, df0.y, test_size=0.3)
x_train[:2]
10965    [儿子, 1, 年, 7, 个, 月, ,, 虽然, 好多, 都, 不是, 很, 懂, ,,...
17209    [这家, 酒店, 绝对, 没有, 四星级, ,, 而且, 房间, 的, 隔音, 太, 差, ...
Name: cut, dtype: object
# 初始化word2vec模型和词表
from gensim.models.word2vec import Word2Vec
n_dim = 300 # 指定向量维度,大样本量时300~500较好
w2vmodel = Word2Vec(vector_size = n_dim, min_count = 10)
w2vmodel.build_vocab(x_train) # 生成词表
# 在评论训练集上建模(大数据集时可能会花费几分钟)
# 本例消耗内存较少
%time w2vmodel.train(x_train, \
               total_examples = w2vmodel.corpus_count, epochs = 10)
Wall time: 4.67 s

(6346434, 9495160)
# 情感词向量间的相似度
w2vmodel.wv.most_similar("不错")
[('好', 0.6664835810661316),
 ('行', 0.6593450903892517),
 ('挺不错', 0.658033549785614),
 ('比较满意', 0.6418668627738953),
 ('很漂亮', 0.5996091365814209),
 ('还行', 0.5758983492851257),
 ('很快', 0.5542255640029907),
 ('整体', 0.5516241788864136),
 ('漂亮', 0.5488754510879517),
 ('说得过去', 0.5477145910263062)]
w2vmodel.wv.most_similar("失望")
[('遗憾', 0.7359662652015686),
 ('不爽', 0.6579391360282898),
 ('气愤', 0.647860586643219),
 ('后悔', 0.6342949271202087),
 ('不满', 0.6023170351982117),
 ('生气', 0.5882432460784912),
 ('上当', 0.5846213698387146),
 ('难得', 0.5696483254432678),
 ('糟糕', 0.5675068497657776),
 ('浅', 0.5560178756713867)]
生成整句向量用于情感分值预测

对购物评论、微博等短文本而言,一般是将所有词向量的平均值作为分类算法的输入值。

    # 生成整句所对应的所有词条的词向量矩阵
pd.DataFrame([w2vmodel.wv[w] for w in df0.cut[0] if w in w2vmodel.wv]).head()    
0123456789...290291292293294295296297298299
0-0.9113322.487151-0.4453550.959004-0.077999-0.157231-0.5110120.6362100.123468-1.307333...-0.798626-0.1331262.0846860.0841970.662218-0.7118581.7726251.529452-0.640140-0.091147
10.046230-0.0357921.241354-1.3543660.447580-0.5604201.2724241.154507-0.583416-0.777637...-1.0289600.141009-0.139010-0.353615-0.9384530.0849640.510275-0.6980840.632293-0.493197
20.5882860.292953-0.215384-0.420847-0.803060-0.0338200.0183330.384169-0.659887-0.146808...0.2775451.0462610.7420070.2306530.011729-1.6647410.1186310.510197-0.013587-0.138566
30.191487-0.297445-0.385975-1.030350-1.255563-1.2140320.6162300.202745-1.1332470.830488...1.589015-0.153522-0.118775-1.095460-0.661065-1.3192110.029742-0.847319-0.7235971.276379
4-0.704394-0.0719390.1009930.326895-0.685714-0.722568-0.2050360.934630-0.2402660.022127...0.595830-0.101508-0.191669-0.558731-0.897231-0.376409-0.159503-0.5682330.4716850.287301

5 rows × 300 columns

# 用各个词向量直接平均的方式生成整句对应的向量
def m_avgvec(words, w2vmodel):
    return pd.DataFrame([w2vmodel.wv[w] 
                  for w in words if w in w2vmodel.wv]).agg("mean")
# 生成建模用矩阵,耗时较长
%time train_vecs = pd.DataFrame([m_avgvec(s, w2vmodel) for s in x_train])
train_vecs.head()
Wall time: 2min 55s
0123456789...290291292293294295296297298299
0-0.1388820.2439060.155527-0.057517-0.2433590.0385050.123457-0.029797-0.2510020.130891...-0.1585220.3286100.4685590.006440-0.0713260.0464290.262878-0.0735970.083408-0.133627
10.1239530.2491460.031028-0.103072-0.012683-0.0612410.0167360.236264-0.238410-0.349272...-0.0948740.3161480.2838090.0352150.100668-0.1503880.1515850.2022990.011277-0.346626
20.0096750.0635080.298085-0.113564-0.394117-0.197484-0.143916-0.264382-0.1652450.044551...-0.0770130.2193530.319499-0.054280-0.0175930.068092-0.043105-0.3146320.061136-0.315324
3-0.2014780.054600-0.1972190.132496-0.1308590.0599310.0908970.447195-0.066142-0.080050...0.2165200.3367890.2427570.1923320.255502-0.0002680.000050-0.1004860.013850-0.163652
40.0124180.231921-0.169535-0.0271080.1622330.0893450.1239970.351209-0.036844-0.328844...0.1541050.4719110.1310340.205431-0.0077950.1226200.1653090.214215-0.148311-0.170526

5 rows × 300 columns

%time train_vecs_t = pd.DataFrame([m_avgvec(x, w2vmodel) for x in x_test])
train_vecs_t
Wall time: 1min 14s
0123456789...290291292293294295296297298299
0-0.360456-0.168126-0.1879020.139328-0.580499-0.4768090.3828650.8964780.0045190.060998...0.3379080.313125-0.248374-0.1927840.191395-0.0032970.034540-0.0243450.3077050.060049
1-0.0065730.3641380.0565960.062842-0.164288-0.0388410.0045680.271801-0.263575-0.250043...-0.1078460.2474520.2252570.0200390.135461-0.1123770.059987-0.0933460.092084-0.338679
2-0.342170-0.1075180.150887-0.012432-0.119799-0.1514690.0628100.317839-0.264509-0.119258...-0.1460240.2517000.4285590.1123630.0885080.0108390.1339460.158355-0.091741-0.333099
3-0.0361960.3237470.038649-0.041602-0.247251-0.055818-0.0801030.083953-0.244907-0.281211...-0.2270690.3630510.4721900.0760830.067558-0.1418180.051008-0.112334-0.019767-0.421210
40.0850830.1897930.0490330.066349-0.216909-0.0713970.0100420.301876-0.342764-0.218100...-0.1644970.4300710.1228700.0714520.140936-0.096815-0.089997-0.0065230.104536-0.479253
..................................................................
6170-0.2649000.0843840.0112390.049277-0.1414440.1007490.3151260.146341-0.133169-0.027791...0.1471900.3365360.4516920.1759380.1920360.1654600.1625850.089151-0.057888-0.233666
61710.026205-0.0718740.270545-0.027888-0.195445-0.299105-0.0790340.288189-0.127330-0.068026...-0.1921920.2486960.3235430.067723-0.0307340.0417970.171027-0.0150460.115095-0.231721
6172-0.4050610.0058830.0379200.075662-0.0390780.1620150.0933000.189479-0.148963-0.012335...0.2102630.3911040.3873800.1361650.4232610.1318360.346203-0.0706950.047509-0.027406
6173-0.062726-0.054550-0.029740-0.169576-0.004502-0.0923100.1810260.379483-0.213536-0.264653...0.0600710.4085610.3005020.1743860.0601650.0399830.1387490.1120430.105036-0.180624
6174-0.1226030.1167250.177533-0.111880-0.154657-0.210409-0.0839180.100732-0.1648700.042741...-0.1960230.1223200.098776-0.089089-0.085931-0.149250-0.006701-0.1962860.078928-0.180735

6175 rows × 300 columns

train_vecs_t.isnull().any()
0      True
1      True
2      True
3      True
4      True
       ... 
295    True
296    True
297    True
298    True
299    True
Length: 300, dtype: bool
情感分析模型拟合
# 用转换后的矩阵拟合SVM模型
from sklearn.svm import SVC

clf2 = SVC(kernel = 'rbf', verbose = True)
clf2.fit(train_vecs, y_train) # 占用内存小于1G
clf2.score(train_vecs, y_train)
[LibSVM]
0.8899146248351496
from sklearn.metrics import classification_report

print(classification_report(y_train, clf2.predict(train_vecs))) 
              precision    recall  f1-score   support

           0       0.89      0.89      0.89      7129
           1       0.89      0.89      0.89      7278

    accuracy                           0.89     14407
   macro avg       0.89      0.89      0.89     14407
weighted avg       0.89      0.89      0.89     14407
# 模型预测
import jieba

def m_pred(string, model):
    words = jieba.lcut(string)
    words_vecs = pd.DataFrame(m_avgvec(words, w2vmodel)).T
     
    result = model.predict(words_vecs)
    
    if int(result[0]) == 1:
        print(string, ":正向")
    else:
        print(string, ":负向")
        

comment = "作为女儿6.1的礼物。虽然晚到了几天。等拿到的时候,女儿爱不释手,上洗手间也看,告知不好。上周末,告诉我她把火鞋和风鞋拿到学校,好多同学羡慕她。呵呵,我也看了其中的人鸦,只可惜没有看完就在老公的催促下睡了。说了这么多,归纳为一句:这套书买的值。"
m_pred(comment, clf2)  
作为女儿6.1的礼物。虽然晚到了几天。等拿到的时候,女儿爱不释手,上洗手间也看,告知不好。上周末,告诉我她把火鞋和风鞋拿到学校,好多同学羡慕她。呵呵,我也看了其中的人鸦,只可惜没有看完就在老公的催促下睡了。说了这么多,归纳为一句:这套书买的值。 :正向

实战作业

在本章所用数据中,各抽取1千条正向、负向评论,重新拟合基于词袋的和基于分布式表达的模型,比较前两种模型效果的改变情况。

# 只需在读入原始数据集时增加nrows属性,然后在进行训练,训练集减少,模型的效果会变差
import pandas as pd

dfpos = pd.read_excel("购物评论.xlsx", sheet_name = "正向", header=None,nrows=1000)
dfpos['y'] = 1
dfneg = pd.read_excel("购物评论.xlsx", sheet_name = "负向", header=None,nrows=1000)
dfneg['y'] = 0
df0 = dfpos.append(dfneg, ignore_index = True)#两个表格接到一起
df0.head()
0y
0感觉用这个手机下载MP3听很方便,用T-Flash卡装上歌,就可以当一个小MP3用了,很不错...1
1外观美观,速度也不错。上面一排触摸键挺实用。应该对得起这个价格。当然再降点大家肯定也不反对。...1
2我刚拿到书,也没有仔细阅读,就是粗粗的翻了点,觉得还行。里面是蓝黑两种颜色的,有些单词的下面...1
3对于二胡曲的精选,应该出版一个系列套装,可分别出售,单独购买。精品二胡曲是有年代和阶段性的,...1
4用了一年半的 e680 终于送小偷了。刚刚买了一台e850,用了三天,说说我自己的感受。1:...1

4. 文档自动摘要

4.1 自动摘要的python实现

chapter.txt[1]
'第一回 风雪惊变那说话人五十来岁年纪,一件青布长袍早洗得褪成了蓝灰带白。只听他两片梨花木板碰了几下,左手中竹棒在一面小羯鼓上敲起得得连声。唱道:“小桃无主自开花,烟草茫茫带晚鸦。几处败垣围故井,向来一一是人家。”那说话人将木板敲了几下,说道:“这首七言诗,,.......”颜烈道:“既有姓”包惜弱点了点头,道:“相公可别太多花费了。”颜烈微笑道:“就可惜娘子在服丧,不能戴用珠宝,要多花钱也花不了。’
#将句子以。!?分开,并转化为表格
def cut_sentence(intxt):  
    delimiters = frozenset('。!?')  
    buf = []  
    for ch in intxt:  
        buf.append(ch)  
        if delimiters.__contains__(ch):  
            yield ''.join(buf)  
            buf = []  
    if buf:  
        yield ''.join(buf)  
sentdf = pd.DataFrame(cut_sentence(chapter.txt[1]))
sentdf
0
0第一回 风雪惊变      那说话人五十来岁年纪,一件青布长袍早洗得褪成了蓝灰带白。
1只听他两片梨花木板碰了几下,左手中竹棒在一面小羯鼓上敲起得得连声。
2唱道:“小桃无主自开花,烟草茫茫带晚鸦。
3几处败垣围故井,向来一一是人家。
4”  那说话人将木板敲了几下,说道:“这首七言诗,说的是兵火过后,原来的家家户户,都变成了断...
......
908包惜弱想要他另要一间客房,却又不知如何启齿才好,脸上一阵红一阵白,心事重重。
909过了一会,颜烈道:“娘子请自宽便,小人出去买了物品就回。
910”包惜弱点了点头,道:“相公可别太多花费了。
911”颜烈微笑道:“就可惜娘子在服丧,不能戴用珠宝,要多花钱也花不了。
912

913 rows × 1 columns

# 去除过短的句子,避免摘要出现无意义的内容
sentdf['txtlen'] = sentdf[0].apply(len)
sentlist = sentdf[0][sentdf.txtlen > 20]
print(len(sentlist))
sentlist
583

0             第一回 风雪惊变      那说话人五十来岁年纪,一件青布长袍早洗得褪成了蓝灰带白。
1                      只听他两片梨花木板碰了几下,左手中竹棒在一面小羯鼓上敲起得得连声。
4      ”  那说话人将木板敲了几下,说道:“这首七言诗,说的是兵火过后,原来的家家户户,都变成了断...
5                         小人刚才说到那叶老汉一家四口,悲欢离合,聚了又散,散了又聚。
6      他四人给金兵冲散,好容易又再团聚,欢天喜地地回到故乡卫州,却见房屋已给金兵烧得干干净净,无可...
                             ...                        
907                          漱洗罢,颜烈与包惜弱一起吃了些点心,两人相对坐在房中。
908               包惜弱想要他另要一间客房,却又不知如何启齿才好,脸上一阵红一阵白,心事重重。
909                         过了一会,颜烈道:“娘子请自宽便,小人出去买了物品就回。
910                               ”包惜弱点了点头,道:“相公可别太多花费了。
911                    ”颜烈微笑道:“就可惜娘子在服丧,不能戴用珠宝,要多花钱也花不了。
Name: 0, Length: 583, dtype: object
 # 将文本中的词语转换为词频矩阵 
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer

txtlist = [ " ".join(jieba.lcut(w)) for w in sentlist]

vectorizer = CountVectorizer() 
X = vectorizer.fit_transform(txtlist) 
Building prefix dict from the default dictionary ...
Dumping model to file cache C:\Users\lenovo\AppData\Local\Temp\jieba.cache
Loading model cost 0.758 seconds.
Prefix dict has been built successfully.
#转为tfidf矩阵
tfidf_matrix = TfidfTransformer().fit_transform(X)  
# 利用nx包实现pagerank算法
import networkx as nx  

similarity = nx.from_scipy_sparse_matrix(tfidf_matrix * tfidf_matrix.T)  
scores = nx.pagerank(similarity)  
scores#算出每个句子的分值,越大,越适合做摘要
{0: 0.0010912193346531665,
 1: 0.0010514692451123236,
 2: 0.0016169794514779936,
 3: 0.0013256313810434896,
 ......
 579: 0.0022975642232750905,
 580: 0.0027147294491056168,
 581: 0.0016757809711476844,
 582: 0.0017710726716536757}
#按得分值进行由高到低排序
tops = sorted(scores.items(), key = lambda x: x[1], reverse = True)
tops[:3]
[(104, 0.003887937743683278),
 (440, 0.0035158614971836305),
 (442, 0.0034495123422054637)]
#显示出分值最高的前3列的内容
print(sentlist.iloc[tops[0][0]])
print(sentlist.iloc[tops[1][0]])
sentlist.iloc[tops[2][0]]
”郭啸天道:“自己兄弟,说什么还请不还请?
杨铁心再是一箭,射死了武官,抢将过去,只见那女子在地下挣扎着坐起身来,正是自己妻子。

'”包惜弱道:“在前面,给……给官兵捉去啦!'
#取出前五列,按照......将其拼接
topn = 5
topsent = sorted(tops[:topn])

abstract = ''
for item in topsent:
    abstract = abstract + sentlist.iloc[item[0]] + "......"
abstract[:-6]#去除最后六个字符的省略号
'”郭啸天道:“自己兄弟,说什么还请不还请?......杨铁心提起铁枪要出屋助战,郭啸天一把拉住,低声道:“道长叫咱们别出去。......杨铁心再是一箭,射死了武官,抢将过去,只见那女子在地下挣扎着坐起身来,正是自己妻子。......”包惜弱道:“在前面,给……给官兵捉去啦!......这时颜烈已走出房去,包惜弱问道:“这是什么?'

文本挖掘系列已经更完,还有一些地方需要后续更新完善。

  • 13
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Python学习笔记文档可以通过以下方式进行下载: 1. 在Python官方网站上下载:可以直接访问Python官方网站(https://www.python.org/),找到并点击"Downloads"页面。在该页面上,可以选择合适的Python版本下载链接,根据操作系统的类型(Windows、Mac或Linux)选择下载对应的安装包。下载完成后,双击安装包进行安装,并按照安装向导中的步骤完成安装过程。 2. 在第方网站上下载:除了Python官方网站,还有一些第方网站也提供Python学习笔记文档的下载。例如,可以在GitHub等代码托管平台上搜索Python学习笔记相关的项目,找到相应的仓库后,可以选择下载最新的代码或者文档压缩包。 3. 在在线教育平台下载:如果你正在参加一个Python学习课程,很可能课程平台会提供Python学习笔记文档的下载链接。你可以登录该平台,进入对应的课程页面,找到相应的学习笔记下载链接,并点击下载。 总之,Python学习笔记文档的下载可以在Python官方网站、第方网站或在线教育平台上进行。根据自己的需求和情况,选择合适的下载方式,下载Python学习笔记文档以便学习和参考。 ### 回答2: 要下载Python学习笔记文档,可以按照以下步骤进行: 1. 打开一个浏览器,比如Chrome、Firefox等。 2. 在地址栏中输入Python学习笔记文档的下载链接,这可以是一个网址或者一个文件链接。 3. 按下回车键,浏览器会开始加载该链接。 4. 如果是一个网址,浏览器会打开一个网页,你可以在网页上找到学习笔记文档的下载链接。 5. 如果是一个文件链接,浏览器会直接开始下载该文件。 6. 等待下载完成,下载的速度取决于你的网络连接。 7. 下载完成后,你可以在浏览器的下载文件夹中找到学习笔记文档,通常在默认情况下会保存在电脑的“下载”文件夹中。 8. 双击打开学习笔记文档,就可以开始学习了。 请注意,在下载文件时要注意文件的来源和安全性,确保文件没有包含恶意软件或病毒。如果你不确定文件的可信度,建议通过官方网站或权威来源下载或购买学习笔记

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值