自然语言情感分析之jieba分词

      之前在CNN和RNN几个模型下分别测试了自然语言情感分析的效果(即能够达到的精确度),但那是调用已有的词典还有语言数据也是函数自带的,所以没有达到我们的目标(帮我们判别某句话是正面还是负面)。

分析内容:

用tensorflow进行中文自然语言处理的情感分析  github链接 https://github.com/aespresso/chinese_sentiment

预训练词向量:
北京师范大学中文信息处理研究所与中国人民大学 DBIIR 实验室的研究者开源的"chinese-word-vectors" github链接为:
https://github.com/Embedding/Chinese-Word-Vectors

具体步骤如下图:

 

其中第二步采用的分词方法是jieba分词,具体可参考  https://github.com/fxsjy/jieba

其实分词是一个很复杂的过程,首先可能是一个字一个字的切分,但如果相邻词之间能合在一起,则就会把它们合并。比如下面“我喜欢文学”后面还有一个“著作”或“书”字能与“文学”二字合在一起,则会考虑把它们分在一起。(当然这只是个简单的例子,具体分法需要考虑的还很多呢,毕竟中华文化博大精深~)

第三步:根据已有的词典建立索引

相当于平时我们查字典,而且便于计算,平时出现频率越大的词索引也越靠前。

第四步:词向量模型
在这个词向量模型里,每一个词是一个索引,对应的是一个长度为300的向量,我们需要构建的LSTM神经网络模型并不能直接处理汉字文本,需要先进行分次并把词汇转换为词向量。

最后就是放到神经网络上去训练了。

训练语料
使用了谭松波老师的酒店评论语料,训练样本分别被放置在两个文件夹里: 分别的pos和neg,每个文件夹里有2000个txt文件,每个文件内有一段评语,共有4000个训练样本。在上面链接里有。

 

# 首先加载必用的库
import numpy as np
import matplotlib.pyplot as plt
import re
import jieba # 结巴分词
# gensim用来加载预训练word vector
from gensim.models import KeyedVectors
# 我们使用tensorflow的keras接口来建模
from tensorflow.python.keras.models import Sequential
from tensorflow.python.keras.layers import Dense, GRU, Embedding, LSTM, Bidirectional
from tensorflow.python.keras.preprocessing.sequence import pad_sequences
from tensorflow.python.keras.optimizers import Adam
from tensorflow.python.keras.callbacks import EarlyStopping, ModelCheckpoint, TensorBoard, ReduceLROnPlateau
from sklearn.model_selection import train_test_split
from tensorflow.python.keras.preprocessing.text import Tokenizer
from tensorflow.python.keras.optimizers import RMSprop

import warnings
warnings.filterwarnings("ignore")
# 获得样本的索引,样本存放于两个文件夹中,
# 分别为 正面评价'pos'文件夹 和 负面评价'neg'文件夹
# 每个文件夹中有2000个txt文件,每个文件中是一例评价
import os
pos_txts = os.listdir('F:/Desktop/chinese_sentiment-master/pos')
neg_txts = os.listdir('F:/Desktop/chinese_sentiment-master/neg')



# 使用gensim加载预训练中文分词embedding
cn_model = KeyedVectors.load_word2vec_format('F:/Desktop/chinese_sentiment-master/sgns.zhihu.bigram', binary=False)
# 由此可见每一个词都对应一个长度为300的向量
embedding_dim = cn_model['大学'].shape[0]
print('词向量的长度为{}'.format(embedding_dim))

print( '样本总共: '+ str(len(pos_txts) + len(neg_txts)) )
# 现在我们将所有的评价内容放置到一个list里

train_texts_orig = [] # 存储所有评价,每例评价为一条string

# 添加完所有样本之后,train_texts_orig为一个含有4000条文本的list
# 其中前2000条文本为正面评价,后2000条为负面评价

for i in range(len(pos_txts)):
    with open('F:/Desktop/chinese_sentiment-master/pos/'+pos_txts[i], 'r', errors='ignore') as f:
        text = f.read().strip()
        train_texts_orig.append(text)
        f.close()
for i in range(len(neg_txts)):
    with open('F:/Desktop/chinese_sentiment-master/neg/'+neg_txts[i], 'r', errors='ignore') as f:
        text = f.read().strip()
        train_texts_orig.append(text)
        f.close()

#分词和tokenize
#首先我们去掉每个样本的标点符号,然后用jieba分词,jieba分词返回一个生成器,没法直接进行tokenize,
#所以我们将分词结果转换成一个list,并将它索引化,这样每一例评价的文本变成一段索引数字,对应着预训练词向量模型中的词。
# 进行分词和tokenize
# train_tokens是一个长长的list,其中含有4000个小list,对应每一条评价
train_tokens = []
for text in train_texts_orig:
    # 去掉标点
    text = re.sub("[\s+\.\!\/_,$%^*(+\"\']+|[+——!,。?、~@#¥%……&*()]+", "", text)
    # 结巴分词
    cut = jieba.cut(text)
    # 结巴分词的输出结果为一个生成器
    # 把生成器转换为list
    cut_list = [ i for i in cut ]
    for i, word in enumerate(cut_list):
        try:
            # 将词转换为索引index
            cut_list[i] = cn_model.vocab[word].index
        except KeyError:
            # 如果词不在字典中,则输出0
            cut_list[i] = 0
    train_tokens.append(cut_list)
# 获得所有tokens的长度
num_tokens = [ len(tokens) for tokens in train_tokens ]
num_tokens = np.array(num_tokens)
# 平均tokens的长度
np.mean(num_tokens)
# 最长的评价tokens的长度
np.max(num_tokens)
plt.hist(np.log(num_tokens), bins=100)
plt.xlim((0, 10))
plt.ylabel('number of tokens')
plt.xlabel('length of tokens')
plt.title('Distribution of tokens length')
plt.show()

# 取tokens平均值并加上两个tokens的标准差,
# 假设tokens长度的分布为正态分布,则max_tokens这个值可以涵盖95%左右的样本
max_tokens = np.mean(num_tokens) + 2 * np.std(num_tokens)
max_tokens = int(max_tokens)

# 取tokens的长度为236时,大约95%的样本被涵盖
# 我们对长度不足的进行padding,超长的进行修剪
np.sum( num_tokens < max_tokens ) / len(num_tokens)
# 用来将tokens转换为文本


def reverse_tokens(tokens):
    text1 = ''
    for i in tokens:
        if i != 0:
            text1 = text1 + cn_model.index2word[i]
        else:
            text1 = text1 + ' '
    return text


reverse = reverse_tokens(train_tokens[0])
print(reverse)

# 经过tokenize再恢复成文本
# 可见标点符号都没有了
# 原始文本
a = train_texts_orig[0]
print(a)

# 只使用前20000个词
num_words = 50000

# 初始化embedding_matrix,之后在keras上进行应用
embedding_matrix = np.zeros((num_words, embedding_dim))
# embedding_matrix为一个 [num_words,embedding_dim] 的矩阵
# 维度为 50000 * 300
for i in range(num_words):

    embedding_matrix[i, :] = cn_model[cn_model.index2word[i]]
    embedding_matrix = embedding_matrix.astype('float32')
# 检查index是否对应,
# 输出300意义为长度为300的embedding向量一一对应
np.sum( cn_model[cn_model.index2word[333]] == embedding_matrix[333] )
# embedding_matrix的维度,
# 这个维度为keras的要求,后续会在模型中用到
print(embedding_matrix.shape)
# 进行padding和truncating, 输入的train_tokens是一个list
# 返回的train_pad是一个numpy array
train_pad = pad_sequences(train_tokens, maxlen=max_tokens, padding='pre', truncating='pre')
# 超出五万个词向量的词用0代替
train_pad[ train_pad>=num_words ] = 0
# 可见padding之后前面的tokens全变成0,文本在最后面
print(train_pad[33])
# 准备target向量,前2000样本为1,后2000为0
train_target = np.concatenate( (np.ones(2000), np.zeros(2000)) )
# 进行训练和测试样本的分割

# 90%的样本用来训练,剩余10%用来测试
X_train, X_test, y_train, y_test = train_test_split(train_pad, train_target, test_size=0.1, random_state=12)
# 查看训练样本,确认无误
print(reverse_tokens(X_train[35]))
print('class: ', y_train[35])

# 用LSTM对样本进行分类
model = Sequential()
# 模型第一层为embedding
model.add(Embedding(num_words,
                    embedding_dim,
                    weights=[embedding_matrix],
                    input_length=max_tokens,
                    trainable=False))
model.add(Bidirectional(LSTM(units=32, return_sequences=True)))
model.add(LSTM(units=16, return_sequences=False))
# GRU的代码
# model.add(GRU(units=32, return_sequences=True))
# model.add(GRU(units=16, return_sequences=True))
# model.add(GRU(units=4, return_sequences=False))

#Embedding之后第,一层我们用BiLSTM返回sequences,然后第二层16个单元的LSTM不返回sequences,只返回最终结果,
# 最后是一个全链接层,用sigmoid激活函数输出结果。
model.add(Dense(1, activation='sigmoid'))
# 我们使用adam以0.001的learning rate进行优化
optimizer = Adam(lr=1e-3)
model.compile(loss='binary_crossentropy',
              optimizer=optimizer,
              metrics=['accuracy'])
# 我们来看一下模型的结构,一共90k左右可训练的变量
model.summary()
# 建立一个权重的存储点
path_checkpoint = 'sentiment_checkpoint.keras'
checkpoint = ModelCheckpoint(filepath=path_checkpoint, monitor='val_loss',
                                      verbose=1, save_weights_only=True,
                                      save_best_only=True)
# 尝试加载已训练模型
try:
    model.load_weights(path_checkpoint)
except Exception as e:
    print(e)
    # 定义early stoping如果3个epoch内validation loss没有改善则停止训练
    earlystopping = EarlyStopping(monitor='val_loss', patience=3, verbose=1)
    # 自动降低learning rate
    lr_reduction = ReduceLROnPlateau(monitor='val_loss',
                                     factor=0.1, min_lr=1e-5, patience=0,
                                     verbose=1)
    # 定义callback函数
    callbacks = [
        earlystopping,
        checkpoint,
        lr_reduction
    ]
    # 开始训练
    model.fit(X_train, y_train,
              validation_split=0.1,
              epochs=10,
              batch_size=128,
              callbacks=callbacks)
    result = model.evaluate(X_test, y_test)
    print('Accuracy:{0:.2%}'.format(result[1]))

    def predict_sentiment(text):
        print(text)
        # 去标点
        text = re.sub("[\s+\.\!\/_,$%^*(+\"\']+|[+——!,。?、~@#¥%……&*()]+", "", text)
        # 分词
        cut = jieba.cut(text)
        cut_list = [i for i in cut]
        # tokenize
        for i, word in enumerate(cut_list):
            try:
                cut_list[i] = cn_model.vocab[word].index
            except KeyError:
                cut_list[i] = 0
        # padding
        tokens_pad = pad_sequences([cut_list], maxlen=max_tokens,
                                   padding='pre', truncating='pre')
        # 预测
        result = model.predict(x=tokens_pad)
        coef = result[0][0]
        if coef >= 0.5:
            print('是一例正面评价', 'output=%.2f' % coef)
        else:
            print('是一例负面评价', 'output=%.2f' % coef)

            test_list = [
                '酒店设施不是新的,服务态度很不好',
                '酒店卫生条件非常不好',
                '床铺非常舒适',
                '房间很凉,不给开暖气',
                '房间很凉爽,空调冷气很足',
                '酒店环境不好,住宿体验很不好',
                '房间隔音不到位',
                '晚上回来发现没有打扫卫生',
                '因为过节所以要我临时加钱,比团购的价格贵'
            ]

            for text in test_list:
                predict_sentiment(text)
                y_pred = model.predict(X_test)
                y_pred = y_pred.T[0]
                y_pred = [1 if p >= 0.5 else 0 for p in y_pred]
                y_pred = np.array(y_pred)
                y_actual = np.array(y_test)
                # 找出错误分类的索引
                misclassified = np.where(y_pred != y_actual)[0]
                # 输出所有错误分类的索引
                len(misclassified)
                print(len(X_test))
                # 我们来找出错误分类的样本看看
                idx = 101
                print(reverse_tokens(X_test[idx]))
                print('预测的分类', y_pred[idx])
                print('实际的分类', y_actual[idx])
                idx = 1
                print(reverse_tokens(X_test[idx]))
                print('预测的分类', y_pred[idx])
                print('实际的分类', y_actual[idx])

 

result:

 

错误分类的文本 经过查看,发现错误分类的文本的含义大多比较含糊,就算人类也不容易判断极性,如index为101的这个句子,好像没有一点满意的成分,但这例子评价在训练样本中被标记成为了正面评价,而我们的模型做出的负面评价的预测似乎是合理的。

 

what's more,电脑跑了半天都没跑玩,不知是哪里陷入了死循环还是我的电脑配置都被这玩意给吃了 ̄へ ̄,真让人绝望?。

  • 13
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值