机器学习-基于LSTM的情感分析(代码实现)

徐大白
7个月前 阅读 50 点赞 1

引言:最近在学神经网络,用TensorFlow简单的写了个情感分析的算法,拿出来和大家分析一下吧。其实主要是为了锻炼自己的语言表达能力。


1.任务介绍

任务是一个针对评价的二分类,也就是简单的指出评价是消极的还是积极的。评价有酒店评价和商品评价两类。其中评价的相关信息大概如下:


评价数量

  • 针对酒店:积极评价2322条,消极评价2443条
  • 针对商品:积极评价10673条,消极评价10428条


评价展示

针对酒店:

  • 消极评价:房间很大味道,服务差,态度差,房单设施差~~永远不会再去了。差就一个字。
  • 积极评价:超赞!虽然窗正对中环,但一点也不觉得吵。房间很干净、整齐,给人很舒服的感觉,服务也很好,价格也不贵,非常满意!


针对购物:

  • 消极评价:客服骗人 不是恒温又说是恒温 噪音大 燃气的味道浓 10号开始使用 17号马上没有热水出 差差差差!
  • 积极评价:超级好的卖家!之前不小心拍错了,客服非常耐心的帮我解答问题,快递也非常给力,必须赞!!


数据共有两份,A数据为酒店评价,B数据为图书评价。每份数据共包含四个txt文件.

  • Pos-train.txt : 训练集中的积极类评价
  • Neg-train.txt : 训练集中的消极类评价
  • Pos-test.txt : 测试集中的积极类评价
  • Neg-test.txt : 测试集中的消极类评价



在每个txt文件中,每一行就是一条评价:



代码是从第122行开始执行的,122行之前都是一些包的调用和函数的定义:

(先不急,文末有详细的代码)


timeA=time.time()
word2vec_path = 'word2vec/word2vec.model'
model=gensim.models.Word2Vec.load(word2vec_path)
dimsh=model.vector_size
MAX_SIZE=25
stopWord = makeStopWord()


在这一块我定义了一个时钟,计时A,在程序跑完后计时B,用来计算程序运行总时间。接下来是加载词向量。dimsh为词向量的维度,在这里,词向量的维度为200维。每条评价的长度是不同的。但是在向神经网络输入数据时,应该保持数据shape的一致性。所以我定义了MAX_SIZE,如果一条评价不够25个词,在将评价转成矩阵时,不够的位置用0补齐,超过25个词汇的评价,第25个词以后的词汇都抛弃掉。stopWord为停用词,有些词汇只是用来表示语气的,并不具有实意,这些词统称停用词,在训练时应该去除掉。


trainData, trainSteps, trainLabels = makeData('data/B/Pos-train.txt',
                                              'data/B/Neg-train.txt')
testData, testSteps, testLabels = makeData('data/B/Pos-test.txt',
                                           'data/B/Neg-test.txt')
trainLabels = np.array(trainLabels)


在这一块,我调用了makeData来制作训练集和测试集。在这块先不了解makeData的内部结构,先来了解一下它的参数和返回值。


makeData有两个参数,posPath和negPath,分别为积极评价的路径和消极评价的路径。然后它有三个返回值,为Data, Steps和Labels。Data为由评价和词向量转换来的矩阵,shape为(length,MAX_SIZE,dimsh)。length为数据的大小(即评价的数目),dimsh为词向量的维度,200维。Steps为对应的每条评价的长度。有些评价是用0补齐的,但是我们在训练时,并不需要再考虑这些0,tensorflow自带的RNN支持传入评价的长度。所以在这里返回了steps。Labels即为对应的评价标签。


在这篇文章中,是将每个评价的每个词都作为输入(shape为(1,25,200))。


def makeData(posPath,negPath):
    #获取词汇,返回类型为[[word1,word2...],[word1,word2...],...]
    pos = getWords(posPath)
    print("The positive data's length is :",len(pos))
    neg = getWords(negPath)
    print("The negative data's length is :",len(neg))
    #将评价数据转换为矩阵,返回类型为array
    posArray, posSteps = words2Array(pos)
    negArray, negSteps = words2Array(neg)
    #将积极数据和消极数据混合在一起打乱,制作数据集
    Data, Steps, Labels = convert2Data(posArray, negArray, posSteps, negSteps)
    return Data, Steps, Labels


接下来看一下makeData函数,makeData函数内部是分为三步执行的:

1.获取词汇,返回的格式为[[word1,word2...],[word1,word2...],...],其中每个列表代表一个评价。

2.将每条评价都转换为对应的矩阵

3.将积极数据和消极数据混合在一起打乱,制作数据集


def getWords(file):
    wordList = []
    trans = []
    lineList = []
    with open(file,'r',encoding='utf-8') as f:
        lines = f.readlines()
    for line in lines:
        trans = jieba.lcut(line.replace('\n',''), cut_all = False)
        for word in trans:
            if word not in stopWord:
                wordList.append(word)
        lineList.append(wordList)
        wordList = []
    return lineList

def words2Array(lineList):
    linesArray=[]
    wordsArray=[]
    steps = []
    for line in lineList:
        t = 0
        p = 0
        for i in range(MAX_SIZE):
            if i<len(line):
                try:
                    wordsArray.append(model.wv.word_vec(line[i]))
                    p = p + 1
                except KeyError:
                    t=t+1
                    continue
            else:
               wordsArray.append(np.array([0.0]*dimsh))
        for i in range(t):
            wordsArray.append(np.array([0.0]*dimsh))
        steps.append(p)
        linesArray.append(wordsArray)
        wordsArray = []
    linesArray = np.array(linesArray)
    steps = np.array(steps)
    return linesArray, steps

def convert2Data(posArray, negArray, posStep, negStep):
    randIt = []
    data = []
    steps = []
    labels = []
    for i in range(len(posArray)):
        randIt.append([posArray[i], posStep[i], [1,0]])
    for i in range(len(negArray)):
        randIt.append([negArray[i], negStep[i], [0,1]])
    shuffle(randIt)
    for i in range(len(randIt)):
        data.append(randIt[i][0])
        steps.append(randIt[i][1])
        labels.append(randIt[i][2])
    data = np.array(data)
    steps = np.array(steps)
    return data, steps, labels


这三个函数贴在这里了,大家可以看一下,不再细讲了。如果看着头疼不想看的话,可以直接跳过,知道每个函数的功能就好。


2.用tensorflow搭建模型

数据预处理完后,接下来就要用tensorflow来搭建模型了。tensorflow是基于计算图来运行的。我们需要先把计算图搭好,每次运行时,我们可以用run函数来向计算图内填充数据和获取一些参数。


graph = tf.Graph()
with graph.as_default():

第一句是用来定义一个计算图,第二句是用来构建计算图:


tf_train_dataset = tf.placeholder(tf.float32,shape=(batch_size,MAX_SIZE,dimsh))
tf_train_steps = tf.placeholder(tf.int32,shape=(batch_size))
tf_train_labels = tf.placeholder(tf.float32,shape=(batch_size,output_size))

tf_test_dataset = tf.constant(testData,tf.float32)
tf_test_steps = tf.constant(testSteps,tf.int32)

lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units = num_nodes,
                                         state_is_tuple=True)

 w1 = tf.Variable(tf.truncated_normal([num_nodes,num_nodes // 2], stddev=0.1))
 b1 = tf.Variable(tf.truncated_normal([num_nodes // 2], stddev=0.1))

 w2 = tf.Variable(tf.truncated_normal([num_nodes // 2, 2], stddev=0.1))
 b2 = tf.Variable(tf.truncated_normal([2], stddev=0.1))


训练数据是通过占位符来传入的,每次传入一批(batch_size大小)的数据。在这里我们需要传入的不止有训练数据,还有标签和每条评价对应的长度。测试数据在计算图内是常量(constant)的形式。


需要注意的是,在这里,我的LSTM是用tensorflow自带的函数来实现的,比自己重复造轮子要方便多了。运算速度也要快很多。推荐大家使用,不要再重复造轮子了。要使用tensorflow自带的rnn模型,首先也是需要定义参数。在这里我定义了lstm_cell。需要定义它的输出维度,在这里我传入参数num_nodes,num_nodes我设的是128.w1、b1、w2、b2为普通的矩阵,LSTM的输出值经过w1、b1、w2、b2的变换后就是我们最后的输出值。


def model(dataset, steps):
        outputs, last_states = tf.nn.dynamic_rnn(cell = lstm_cell,
                                                 dtype = tf.float32,
                                                 sequence_length = steps,
                                                 inputs = dataset)
        hidden = last_states[-1]

        hidden = tf.matmul(hidden, w1) + b1
        logits = tf.matmul(hidden, w2) + b2
        return logits
    train_logits = model(tf_train_dataset, tf_train_steps)
    loss = tf.reduce_mean(
        tf.nn.softmax_cross_entropy_with_logits(labels=tf_train_labels,
                                                logits=train_logits))
    optimizer = tf.train.GradientDescentOptimizer(0.01).minimize(loss)

    test_prediction = tf.nn.softmax(model(tf_test_dataset, tf_test_steps))


接下来来看具体的计算过程。train_logits为最后的输出值,它传入的参数有tf_train_dataset, tf_trainsteps。tf.nn.dynamicrnn为tensorflow自带的rnn函数,是用来执行rnn计算过程的。它需要传入我们定义lstm_cell,sequencelength(即为每条评价对应的长度),inputs(数据)。需要注意的是,rnn会在每一个步数产生一个输出值,在这里我们只取最后一个步数的输出值作为输出。last_states的shape为(batch_size,num_nodes,2)。分别为对应的state和最后一个步数的输出值h。我们用last_states[-1]来获取h。经过w1,w2的变换后我们得到logits作为最后的输出。tf.nn.softmax_cross_entropy_with_logits函数直接完成了softmax和计算交叉熵的操作。然后用tf.reduce_mean计算出了我们的loss。optimizer为优化器,优化目标为loss,学习率为0.01。test_prediction为在测试集上的预测。到这里,计算图基本上是搭建完了,接下来就是调用的过程。


num_steps = 20001
summary_frequency = 500
with tf.Session(graph = graph) as session:
    tf.global_variables_initializer().run()
    print('Initialized')
    mean_loss = 0
    for step in range(num_steps):
        offset = (step * batch_size) % (len(trainLabels)-batch_size)
        feed_dict={tf_train_dataset:trainData[offset:offset + batch_size],
                   tf_train_labels:trainLabels[offset:offset + batch_size],
                   tf_train_steps:trainSteps[offset:offset + batch_size]}
        _, l = session.run([optimizer,loss],
                           feed_dict = feed_dict)
        mean_loss += l
        if step >0 and step % summary_frequency == 0:
            mean_loss = mean_loss / summary_frequency
            print("The step is: %d"%(step))
            print("In train data,the loss is:%.4f"%(mean_loss))
            mean_loss = 0
            acrc = 0
            prediction = session.run(test_prediction)
            for i in range(len(prediction)):
                if prediction[i][testLabels[i].index(1)] > 0.5:
                    acrc = acrc + 1
            print("In test data,the accuracy is:%.2f%%"%((acrc/len(testLabels))*100))


最终,在测试集上最好的结果达到了97.49%

| 1
登录后可评论,马上登录吧~
评论 ( 1 )
像徐大神学习
回复
1个月前