Keras 开发基于Bi-LSTM的文本分类器

作者: 外汇分析师_程序员 | 来源:发表于2019-10-23 10:57 被阅读0次

    Overview

    双向LSTM是传统LSTM的扩展,可以改善序列分类问题上的模型性能。

    在输入序列的所有时间步均可用的问题中,双向LSTM在输入序列上训练两个LSTM,而不是一个LSTM。输入序列上的第一个按原样,第二个输入序列的反向副本。这可以为网络提供其他上下文,并导致对问题的更快,甚至更充分的学习。

    在本教程中,您将发现如何使用Keras深度学习库开发用于序列分类的双向LSTM。

    完成本教程后,您将知道:

    • 如何开发一个小的人为和可配置序列分类问题。
    • 如何开发用于序列分类的LSTM和双向LSTM。
    • 如何比较双向LSTM中使用的合并模式的性能。

    本教程分为6个部分。他们是:

    1. 双向LSTM
    2. 序列分类问题
    3. LSTM用于序列分类
    4. 双向LSTM用于序列分类
    5. 比较LSTM和双向LSTM
    6. 比较双向LSTM合并模式

    PART1 - Bi-LSTMs

    双向递归神经网络(RNN)的想法很简单。它涉及到复制网络中的第一个循环层,以便现在并排有两层,然后按原样提供输入序列作为对第一层的输入,并向第二层提供输入序列的反向副本。

    """

    为了克服常规RNN […]的局限性,我们提出了一种双向递归神经网络(BRNN),可以在特定时间范围的过去和将来使用所有可用的输入信息来进行训练。##

    这个想法是将规则RNN的状态神经元分成负责正向时间方向(正向状态)和负向时间方向(后向状态)的部分##

    """

    — Mike Schuster and Kuldip K. Paliwal, Bidirectional Recurrent Neural Networks, 1997

    这种方法已与长短期记忆(LSTM)递归神经网络一起使用,效果很好。最初在语音识别领域证明了使用双向提供序列的合理性,因为有证据表明整个话语的上下文用于解释正在说的内容,而不是线性解释。

    """

    乍一看,依靠未来的知识似乎违反了因果关系。我们如何才能基于尚未说过的话来了解所听到的内容?但是,听众正是这样做的。声音,单词,甚至整个句子最初都意味着没有意义,因此从未来的角度来看是没有意义的。我们必须记住的是,真正在线的任务(每次输入后都需要输出)与仅在某些输入段的末尾需要输出的任务之间的区别。

    — Alex Graves and Jurgen Schmidhuber, Framewise Phoneme Classification with Bidirectional LSTM and Other Neural Network Architectures, 2005

    使用双向LSTM可能无法解决所有序列预测问题,但可以在适当的情况下为这些域提供更好的结果,从而带来一些好处。

    需要明确的是,输入序列中的时间步仍一次处理一次,只是网络同时在两个方向上遍历输入序列。

    PART2 - Bidirectional LSTMs in Keras

    Keras通过双向层包装器支持双向LSTM。它还允许您指定合并模式,即在将前进和后退输出传递到下一层之前应该对其进行组合的方式。选项包括:
    ‘sum‘: The outputs are added together.
    ‘mul‘: The outputs are multiplied together.
    ‘concat‘: The outputs are concatenated together (the default), providing double the number of outputs to the next layer.
    ‘ave‘: The average of the outputs is taken.
    默认模式是连接,这是双向LSTM研究中经常使用的方法。

    Sequence Classification Problem

    我们将定义一个简单的序列分类问题,以探索双向LSTM。
    问题定义为一个介于0到1之间的随机值序列。该序列被用作问题的输入,每个时间步长提供一个数字。每个输入都关联一个二进制标签(0或1)。输出值全为0。一旦序列中输入值的累加总和超过阈值,则输出值将从0翻转为1。使用1/4序列长度的阈值。
    例如,下面是一个步长为10的序列:
    0.63144003 0.29414551 0.91587952 0.95189228 0.32195638 0.60742236 0.83895793 0.18023048 0.84762691 0.29165514
    对应的分类序列(y)是:
    0 0 0 1 1 1 1 1 1 10
    用Python实现:

    from random import random
    from numpy import array
    from numpy import cumsum
    
    # create a sequence of random numbers in [0,1]
    X = array([random() for _ in range(10)])
    
    # calculate cut-off value to change class values
    limit = 10/4.0
    
    # 可以使用cumsum()NumPy函数来计算输入序列的累积和。
    #此函数返回一系列累加和值,例如:pos1, pos1+pos2, pos1+pos2+pos3, ...
    
    # determine the class outcome for each item in cumulative sequence
    y = array([0 if x < limit else 1 for x in cumsum(X)])
    
    # create a sequence classification instance
    def get_sequence(n_timesteps):
        # create a sequence of random numbers in [0,1]
        X = array([random() for _ in range(n_timesteps)])
        # calculate cut-off value to change class values
        limit = n_timesteps/4.0
        # determine the class outcome for each item in cumulative sequence
        y = array([0 if x < limit else 1 for x in cumsum(X)])
        return X, y
    
    X, y = get_sequence(10)
    print(X)
    print(y)
    

    Output:

    [ 0.22228819 0.26882207 0.069623 0.91477783 0.02095862 0.71322527
    0.90159654 0.65000306 0.88845226 0.4037031 ]
    [0 0 0 0 0 0 1 1 1 1]
    

    PART3 - LSTM For Sequence Classification

    我们可以从开发用于序列分类问题的传统LSTM开始。首先,我们必须更新get_sequence()函数以将输入和输出序列整形为3维以满足LSTM的期望。预期的结构具有尺寸[样本,时间步长,特征]。分类问题有1个样本(例如一个序列),可配置数量的时间步长和每个时间步长一个特征。分类问题有1个样本(例如一个序列),可配置数量的时间步长和每个时间步长一个特征。
    因此,我们可以按以下方式重塑序列。

    # reshape input and output data to be suitable for LSTMs
    X = X.reshape(1, n_timesteps, 1)
    y = y.reshape(1, n_timesteps, 1)
    
    # create a sequence classification instance
    def get_sequence(n_timesteps):
        # create a sequence of random numbers in [0,1]
        X = array([random() for _ in range(n_timesteps)])
        # calculate cut-off value to change class values
        limit = n_timesteps/4.0
        # determine the class outcome for each item in cumulative sequence
        y = array([0 if x < limit else 1 for x in cumsum(X)])
        # reshape input and output data to be suitable for LSTMs
        X = X.reshape(1, n_timesteps, 1)
        y = y.reshape(1, n_timesteps, 1)
        return X, y
    

    我们将序列定义为具有10个时间步长。接下来,我们可以为该问题定义一个LSTM。输入层将有10个时间步长,其中1个特征是一个输入块,input_shape =(10,1)。第一个隐藏层将具有20个存储单元,输出层将是一个完全连接的层,每个时间步输出一个值。在输出上使用S型激活函数来预测二进制值。在输出层周围使用一个TimeDistributed包装器层,这样,在给定作为输入提供的完整序列的情况下,可以预测每个时间步长的一个值。这要求LSTM隐藏层返回一个值序列(每个时间步一个),而不是整个输入序列的单个值。最后,由于这是二进制分类问题,因此使用了二进制对数损失(Keras中的binary_crossentropy)。高效的ADAM优化算法用于查找权重,并在每个时期计算并报告准确性指标。

    # define LSTM
    model = Sequential()
    model.add(LSTM(20, input_shape=(10, 1), return_sequences=True))
    model.add(TimeDistributed(Dense(1, activation='sigmoid')))
    model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['acc'])
    

    LSTM将接受1,000 epoches的培训。每个epoch将生成一个新的随机输入序列,以适合网络。这样可以确保模型不存储单个序列,而可以泛化一个解决此问题的所有可能的随机输入序列的解决方案。

    # train LSTM
    for epoch in range(1000):
        # generate new random sequence
        X,y = get_sequence(n_timesteps)
        # fit model for one epoch on this sequence
        model.fit(X, y, epochs=1, batch_size=1, verbose=2)
    

    训练后,将在另一个随机序列上评估网络。然后将预测与预期输出序列进行比较,以提供系统技能的具体示例。

    # evaluate LSTM
    X,y = get_sequence(n_timesteps)
    yhat = model.predict_classes(X, verbose=0)
    for i in range(n_timesteps):
        print('Expected:', y[0, i], 'Predicted', yhat[0, i])
    

    完整代码如下:

    from random import random
    from numpy import array
    from numpy import cumsum
    from keras.models import Sequential
    from keras.layers import LSTM
    from keras.layers import Dense
    from keras.layers import TimeDistributed
     
    # create a sequence classification instance
    def get_sequence(n_timesteps):
        # create a sequence of random numbers in [0,1]
        X = array([random() for _ in range(n_timesteps)])
        # calculate cut-off value to change class values
        limit = n_timesteps/4.0
        # determine the class outcome for each item in cumulative sequence
        y = array([0 if x < limit else 1 for x in cumsum(X)])
        # reshape input and output data to be suitable for LSTMs
        X = X.reshape(1, n_timesteps, 1)
        y = y.reshape(1, n_timesteps, 1)
        return X, y
     
    # define problem properties
    n_timesteps = 10
    # define LSTM
    model = Sequential()
    model.add(LSTM(20, input_shape=(n_timesteps, 1), return_sequences=True))
    model.add(TimeDistributed(Dense(1, activation='sigmoid')))
    model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['acc'])
    # train LSTM
    for epoch in range(1000):
        # generate new random sequence
        X,y = get_sequence(n_timesteps)
        # fit model for one epoch on this sequence
        model.fit(X, y, epochs=1, batch_size=1, verbose=2)
    # evaluate LSTM
    X,y = get_sequence(n_timesteps)
    yhat = model.predict_classes(X, verbose=0)
    for i in range(n_timesteps):
        print('Expected:', y[0, i], 'Predicted', yhat[0, i])
    

    运行该示例将在每个时期的随机序列上显示对数丢失和分类准确性。这提供了一个清晰的思路,可以明确模型对序列分类问题的解决方案的一般化程度。我们可以看到该模型运行良好,最终精度在90%和100%左右徘徊。不完美,但对我们的目的有利。将新随机序列的预测与预期值进行比较,显示出几乎正确的结果,但有一个错误。

    Epoch 1/1
    0s - loss: 0.2039 - acc: 0.9000
    Epoch 1/1
    0s - loss: 0.2985 - acc: 0.9000
    Epoch 1/1
    0s - loss: 0.1219 - acc: 1.0000
    Epoch 1/1
    0s - loss: 0.2031 - acc: 0.9000
    Epoch 1/1
    0s - loss: 0.1698 - acc: 0.9000
    Expected: [0] Predicted [0]
    Expected: [0] Predicted [0]
    Expected: [0] Predicted [0]
    Expected: [0] Predicted [0]
    Expected: [0] Predicted [0]
    Expected: [0] Predicted [1]
    Expected: [1] Predicted [1]
    Expected: [1] Predicted [1]
    Expected: [1] Predicted [1]
    Expected: [1] Predicted [1]
    

    PART4 - Bidirectional LSTM For Sequence Classification

    现在我们知道如何为序列分类问题开发LSTM,我们可以扩展该示例以演示双向LSTM。
    我们可以通过将LSTM隐藏层与双向层包装在一起来做到这一点,如下所示:

    model.add(Bidirectional(LSTM(20, return_sequences=True), input_shape=(n_timesteps, 1)))
    

    这将创建隐藏层的两个副本,一个副本按原样适合输入序列,一个副本在输入序列的反向副本上。默认情况下,这些LSTM的输出值将被串联。这意味着,现在代替接收20个输出的10个时间步长的TimeDistributed层,它现在将接收40个输出(20个单元+ 20个单元)的10个时间步长。下面列出了完整的示例。

    from random import random
    from numpy import array
    from numpy import cumsum
    from keras.models import Sequential
    from keras.layers import LSTM
    from keras.layers import Dense
    from keras.layers import TimeDistributed
    from keras.layers import Bidirectional
    
    # create a sequence classification instance
    def get_sequence(n_timesteps):
        # create a sequence of random numbers in [0,1]
        X = array([random() for _ in range(n_timesteps)])
        # calculate cut-off value to change class values
        limit = n_timesteps/4.0
        # determine the class outcome for each item in cumulative sequence
        y = array([0 if x < limit else 1 for x in cumsum(X)])
        # reshape input and output data to be suitable for LSTMs
        X = X.reshape(1, n_timesteps, 1)
        y = y.reshape(1, n_timesteps, 1)
        return X, y
    
    # define problem properties
    n_timesteps = 10
    # define LSTM
    model = Sequential()
    model.add(Bidirectional(LSTM(20, return_sequences=True), input_shape=(n_timesteps, 1)))
    model.add(TimeDistributed(Dense(1, activation='sigmoid')))
    model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['acc'])
    # train LSTM
    for epoch in range(1000):
        # generate new random sequence
        X,y = get_sequence(n_timesteps)
        # fit model for one epoch on this sequence
        model.fit(X, y, epochs=1, batch_size=1, verbose=2)
    # evaluate LSTM
    X,y = get_sequence(n_timesteps)
    yhat = model.predict_classes(X, verbose=0)
    for i in range(n_timesteps):
        print('Expected:', y[0, i], 'Predicted', yhat[0, i])
    

    Output:

    ...
    Epoch 1/1
    0s - loss: 0.0967 - acc: 0.9000
    Epoch 1/1
    0s - loss: 0.0865 - acc: 1.0000
    Epoch 1/1
    0s - loss: 0.0905 - acc: 0.9000
    Epoch 1/1
    0s - loss: 0.2460 - acc: 0.9000
    Epoch 1/1
    0s - loss: 0.1458 - acc: 0.9000
    Expected: [0] Predicted [0]
    Expected: [0] Predicted [0]
    Expected: [0] Predicted [0]
    Expected: [0] Predicted [0]
    Expected: [0] Predicted [0]
    Expected: [1] Predicted [1]
    Expected: [1] Predicted [1]
    Expected: [1] Predicted [1]
    Expected: [1] Predicted [1]
    Expected: [1] Predicted [1]
    

    PART5 - Compare LSTM to Bidirectional LSTM

    在此示例中,我们将在训练模型的同时将传统LSTM与双向LSTM的性能进行比较。
    我们将调整实验,以便仅针对250个epoches训练模型。这样一来,我们就可以清楚地了解每个模型的学习方式以及双向LSTM的学习行为如何不同。

    我们将比较三种不同的模型:
    LSTM(原样)
    具有反向输入序列的LSTM(例如,您可以通过将LSTM层的“ go_backwards”参数设置为“ True”来执行此操作)
    双向LSTM

    这种比较将有助于表明,双向LSTM实际上可以增加一些东西,而不仅仅是简单地反转输入序列。我们将定义一个函数来创建和返回具有向前或向后输入序列的LSTM,如下所示:

    def get_lstm_model(n_timesteps, backwards):
        model = Sequential()
        model.add(LSTM(20, input_shape=(n_timesteps, 1), return_sequences=True, go_backwards=backwards))
        model.add(TimeDistributed(Dense(1, activation='sigmoid')))
        model.compile(loss='binary_crossentropy', optimizer='adam')
        return model
    

    我们可以为双向LSTM开发类似的功能,其中可以将合并模式指定为参数。可以通过将合并模式设置为值“ concat”来指定默认的串联。

    def get_bi_lstm_model(n_timesteps, mode):
        model = Sequential()
        model.add(Bidirectional(LSTM(20, return_sequences=True), input_shape=(n_timesteps, 1), merge_mode=mode))
        model.add(TimeDistributed(Dense(1, activation='sigmoid')))
        model.compile(loss='binary_crossentropy', optimizer='adam')
        return model
    

    最后,我们定义一个适合模型的函数,并在每个训练时期检索并存储损失,然后在模型拟合后返回收集的损失值列表。这样,我们就可以绘制每个模型配置的对数损失图表并进行比较。

    def train_model(model, n_timesteps):
        loss = list()
        for _ in range(250):
            # generate new random sequence
            X,y = get_sequence(n_timesteps)
            # fit model for one epoch on this sequence
            hist = model.fit(X, y, epochs=1, batch_size=1, verbose=0)
            loss.append(hist.history['loss'][0])
        return loss
    

    综上,下面列出了完整的示例。首先,创建并拟合传统的LSTM,并绘制对数损失值。重复使用具有反向输入序列的LSTM,最后使用具有级联合并的LSTM重复此过程。

    from random import random
    from numpy import array
    from numpy import cumsum
    from matplotlib import pyplot
    from pandas import DataFrame
    from keras.models import Sequential
    from keras.layers import LSTM
    from keras.layers import Dense
    from keras.layers import TimeDistributed
    from keras.layers import Bidirectional
    
    # create a sequence classification instance
    def get_sequence(n_timesteps):
        # create a sequence of random numbers in [0,1]
        X = array([random() for _ in range(n_timesteps)])
        # calculate cut-off value to change class values
        limit = n_timesteps/4.0
        # determine the class outcome for each item in cumulative sequence
        y = array([0 if x < limit else 1 for x in cumsum(X)])
        # reshape input and output data to be suitable for LSTMs
        X = X.reshape(1, n_timesteps, 1)
        y = y.reshape(1, n_timesteps, 1)
        return X, y
    
    def get_lstm_model(n_timesteps, backwards):
        model = Sequential()
        model.add(LSTM(20, input_shape=(n_timesteps, 1), return_sequences=True, go_backwards=backwards))
        model.add(TimeDistributed(Dense(1, activation='sigmoid')))
        model.compile(loss='binary_crossentropy', optimizer='adam')
        return model
    
    def get_bi_lstm_model(n_timesteps, mode):
        model = Sequential()
        model.add(Bidirectional(LSTM(20, return_sequences=True), input_shape=(n_timesteps, 1), merge_mode=mode))
        model.add(TimeDistributed(Dense(1, activation='sigmoid')))
        model.compile(loss='binary_crossentropy', optimizer='adam')
        return model
    
    def train_model(model, n_timesteps):
        loss = list()
        for _ in range(250):
            # generate new random sequence
            X,y = get_sequence(n_timesteps)
            # fit model for one epoch on this sequence
            hist = model.fit(X, y, epochs=1, batch_size=1, verbose=0)
            loss.append(hist.history['loss'][0])
        return loss
    
    
    n_timesteps = 10
    results = DataFrame()
    # lstm forwards
    model = get_lstm_model(n_timesteps, False)
    results['lstm_forw'] = train_model(model, n_timesteps)
    # lstm backwards
    model = get_lstm_model(n_timesteps, True)
    results['lstm_back'] = train_model(model, n_timesteps)
    # bidirectional concat
    model = get_bi_lstm_model(n_timesteps, 'concat')
    results['bilstm_con'] = train_model(model, n_timesteps)
    # line plot of results
    results.plot()
    pyplot.show()
    

    运行示例将创建一个折线图。你的特定绘图可能在细节上有所不同,但将显示相同的趋势。
    我们可以看到,在250个训练时期内,LSTM正向(蓝色)和LSTM向后(橙色)显示出相似的对数损失。我们可以看到,双向LSTM对数损耗是不同的(绿色),下降得更快,并且通常比其他两种配置低。


    image.png

    PART 6 - Comparing Bidirectional LSTM Merge Modes

    可以使用4种不同的合并模式来组合双向LSTM层的结果。它们是串联(默认),乘法,平均值和总和。通过更新上一节中的示例,我们可以比较不同合并模式的行为,如下所示:

    n_timesteps = 10
    results = DataFrame()
    # sum merge
    model = get_bi_lstm_model(n_timesteps, 'sum')
    results['bilstm_sum'] = train_model(model, n_timesteps)
    # mul merge
    model = get_bi_lstm_model(n_timesteps, 'mul')
    results['bilstm_mul'] = train_model(model, n_timesteps)
    # avg merge
    model = get_bi_lstm_model(n_timesteps, 'ave')
    results['bilstm_ave'] = train_model(model, n_timesteps)
    # concat merge
    model = get_bi_lstm_model(n_timesteps, 'concat')
    results['bilstm_con'] = train_model(model, n_timesteps)
    # line plot of results
    results.plot()
    pyplot.show()
    

    运行示例将创建一个折线图,比较每种合并模式的对数损失。你的图片可能有所不同,但将显示相同的行为趋势。不同的合并模式会导致不同的模型性能,并且这将取决于您的特定序列预测问题。在这种情况下,我们可以看到总和(蓝色)和串联(红色)合并模式可能会导致更好的性能,或者至少降低对数损失。


    image.png

    相关文章

      网友评论

        本文标题:Keras 开发基于Bi-LSTM的文本分类器

        本文链接:https://www.haomeiwen.com/subject/pgehvctx.html