【问题标题】:Keras: compute cosine distance between two flattened outputsKeras:计算两个展平输出之间的余弦距离
【发布时间】:2018-04-25 04:27:55
【问题描述】:

EDIT2:我的代码https://github.com/hcl14/my_simple_LSTM

我有以下结构的模型:两个 LSTM(问题和答案)和额外的注意力层,可以考虑在答案之上。这是使用 sum 和 softmax 比较两个输出的版本:

#question
qenc = Sequential()
qenc.add(Embedding(output_dim=WORD2VEC_EMBED_SIZE, input_dim=vocab_size,
                   input_length=seq_maxlen,
                   weights=[embedding_weights]))
qenc.add(Bidirectional(LSTM(QA_EMBED_SIZE, return_sequences=True), 
                       merge_mode="sum"))
qenc.add(Dropout(0.3))
qenc.add(Convolution1D(QA_EMBED_SIZE // 2, 5, border_mode="valid"))
qenc.add(MaxPooling1D(pool_length=2, border_mode="valid"))
qenc.add(Dropout(0.3))

# answer
aenc = Sequential()
aenc.add(Embedding(output_dim=WORD2VEC_EMBED_SIZE, input_dim=vocab_size,
                   input_length=seq_maxlen,
                   weights=[embedding_weights]))
aenc.add(Bidirectional(LSTM(QA_EMBED_SIZE, return_sequences=True),
                       merge_mode="sum"))
aenc.add(Dropout(0.3))
aenc.add(Convolution1D(QA_EMBED_SIZE // 2, 5, border_mode="valid"))
aenc.add(MaxPooling1D(pool_length=2, border_mode="valid"))
aenc.add(Dropout(0.3))

# attention model
attn = Sequential()
attn.add(Merge([qenc, aenc], mode="dot", dot_axes=[1, 1]))
attn.add(Flatten())
#attn.add(Dense((seq_maxlen * QA_EMBED_SIZE)))
#attn.add(Reshape((seq_maxlen, QA_EMBED_SIZE)))
attn.add(Dense((qenc.output_shape[1]*(QA_EMBED_SIZE // 2))))
attn.add(Reshape((qenc.output_shape[1], QA_EMBED_SIZE // 2)))

# Plain sum - not working properly!
model = Sequential()
model.add(Merge([qenc, attn], mode="sum"))
model.add(Flatten())
model.add(Dense(1, activation="softmax"))

这里的网络可以正常工作,但是简单的 sum + softmax 是错误的选择,并且没有给出想要的结果。我想要的是在qencattn 之间使用余弦相似度,但它们的形状是(None, 48, 32)(这些数字因使用的数据而异)。我正在考虑的是扁平化两者并使用余弦相似度,与 0-1 标签进行比较。

问题是如何在那里使用余弦?我无法展平qenc,因为它在合并时使用attn 计算并且形状很重要。我试过了:

Lambda - 不起作用。我不接受顺序模型,只是layers输出,不是layer,而是tensor,所以不能加。

def cosine_distance(vests):
    x, y = vests
    x = K.batch_flatten(x)
    y = K.batch_flatten(y)
    x = K.l2_normalize(x, axis=-1)
    y = K.l2_normalize(y, axis=-1)
    return -K.mean(x * y, axis=-1)

model = Sequential()
model.add(Lambda(cosine_distance)([qenc.layers[-1].output,attn.layers[-1].output]))

中间展平模型 - 导致诸如“合并对象没有 batch_size 属性”之类的错误:

flattened_attn = Sequential()    
flattened_attn.add(attn)    
flattened_attn.add(Flatten())

flattened_qenc = ...

model = Sequential()
model.add(Merge([flattened_attn, flattned_qenc], mode="cos", dot_axes=1))

最后,我实现了以(None, 1536) 形状传递扁平数据:

qenc = Sequential()
qenc.add(Embedding(output_dim=WORD2VEC_EMBED_SIZE, input_dim=vocab_size,
                   input_length=seq_maxlen,
                   weights=[embedding_weights]))
qenc.add(Bidirectional(LSTM(QA_EMBED_SIZE, return_sequences=True), 
                       merge_mode="sum"))
qenc.add(Dropout(0.3))
qenc.add(Convolution1D(QA_EMBED_SIZE // 2, 5, border_mode="valid"))
qenc.add(MaxPooling1D(pool_length=2, border_mode="valid"))
qenc.add(Dropout(0.3))
qenc.add(Flatten())

aenc = Sequential()
aenc.add(Embedding(output_dim=WORD2VEC_EMBED_SIZE, input_dim=vocab_size,
                   input_length=seq_maxlen,
                   weights=[embedding_weights]))
aenc.add(Bidirectional(LSTM(QA_EMBED_SIZE, return_sequences=True),
                       merge_mode="sum"))
aenc.add(Dropout(0.3))
aenc.add(Convolution1D(QA_EMBED_SIZE // 2, 5, border_mode="valid"))
aenc.add(MaxPooling1D(pool_length=2, border_mode="valid"))
aenc.add(Dropout(0.3))


unflattened_qenc = Sequential()
unflattened_qenc.add(qenc)
unflattened_qenc.add(Reshape((aenc.output_shape[1],aenc.output_shape[2])))

# attention model
attn = Sequential()
attn.add(Merge([unflattened_qenc, aenc], mode="dot", dot_axes=[1, 1]))
attn.add(Flatten())
#attn.add(Dense((seq_maxlen * QA_EMBED_SIZE)))
#attn.add(Reshape((seq_maxlen, QA_EMBED_SIZE)))
attn.add(Dense((aenc.output_shape[1]*(QA_EMBED_SIZE // 2))))
attn.add(Reshape((aenc.output_shape[1], QA_EMBED_SIZE // 2)))
attn.add(Flatten())


model = Sequential()
attn.add(Merge([qenc, attn], mode="cos", dot_axes=1))

得到了错误:

  attn.add(Merge([qenc, attn], mode="cos", dot_axes=1))
Traceback (most recent call last):
  File "qa-lstm-attn.py", line 175, in <module>
    attn.add(Merge([qenc, attn], mode="cos", dot_axes=1))
  File "/home/hcl/.local/lib/python3.5/site-packages/keras/models.py", line 492, in add
    output_tensor = layer(self.outputs[0])
  File "/home/hcl/.local/lib/python3.5/site-packages/keras/engine/topology.py", line 617, in __call__
    output = self.call(inputs, **kwargs)
  File "/home/hcl/.local/lib/python3.5/site-packages/keras/legacy/layers.py", line 202, in call
    '(at least 2). Got: ' + str(inputs))
TypeError: Merge must be called on a list of tensors (at least 2). Got: Tensor("flatten_3/Reshape:0", shape=(?, ?), dtype=float32)
>>> qenc.output_shape
(None, 1536)
>>> aenc.output_shape
(None, 48, 32)
>>> attn.output_shape
(None, 1536)

那么余弦怎么做呢?

Keras v. 2.1.4

UPD:修复 model.add() 复制粘贴错误后,我有:

model = Sequential()
model.add(Merge([qenc, attn], mode="cos", dot_axes=1))

错误信息:

  File "qa-lstm-attn.py", line 195, in <module>
    callbacks=[checkpoint])
  File "/home/hcl/.local/lib/python3.5/site-packages/keras/models.py", line 963, in fit
    validation_steps=validation_steps)
  File "/home/hcl/.local/lib/python3.5/site-packages/keras/engine/training.py", line 1637, in fit
    batch_size=batch_size)
  File "/home/hcl/.local/lib/python3.5/site-packages/keras/engine/training.py", line 1483, in _standardize_user_data
    exception_prefix='input')
  File "/home/hcl/.local/lib/python3.5/site-packages/keras/engine/training.py", line 86, in _standardize_input_data
    str(len(data)) + ' arrays: ' + str(data)[:200] + '...')
ValueError: Error when checking model input: the list of Numpy arrays that you are passing to your model is not the size the model expected. Expected to see 3 array(s), but instead got the following list of 2 arrays: [array([[ 1676,    19,   328, ...,  1612,    29,  4220],
       [    0,     0,     0, ...,     4,    27,  4807],
       [ 2928,     9,  1652, ...,   125,     9,   181],
       ...,
       [ 5970,   14...

如何调用回调:

model.compile(optimizer="adam", loss="mean_squared_error",
              metrics=["accuracy"])

print("Training...")
checkpoint = ModelCheckpoint(
    filepath=os.path.join(MODEL_DIR, "qa-lstm-attn-best.hdf5"),
    verbose=1, save_best_only=True)
model.fit([Xqtrain, Xatrain], Ytrain, batch_size=BATCH_SIZE,
          nb_epoch=NBR_EPOCHS, validation_split=0.1,
          callbacks=[checkpoint])

我认为 Keras 不理解其中一个模型被重用并期望额外的输入。

我的模型实际上是这段代码的修改版本,它不能正常工作,因为模型只是学会了总是回答 False(作者警告它):

https://github.com/sujitpal/dl-models-for-qa

https://github.com/sujitpal/dl-models-for-qa/blob/master/src/qa-blstm-attn.py


编辑

@daniel-möller 的解释:我想实现文章 https://arxiv.org/abs/1511.04108 中的模型。只要模型计算问题和答案之间的余弦,我的标签就是 0 和 1(答案匹配问题而不匹配)。数据集由一个问题和 4 个答案变体组成,其中一个是正确的。下面是我如何通过创建 4 个数据对来准备它 (kaggle.py),其中一个具有 True:

def get_question_answer_pairs(question_file, is_test=False):
    qapairs = []
    fqa = open(question_file, "r")
    
    data = json.load(fqa)
    for l, line in enumerate(data):
        
        if l%100==0:
            print(l)
        
        question = line["question"]+" "+line["support"]
        
        qwords = tokenizer(question)
        
        #qwords = nltk.word_tokenize(question)
        
        if len(qwords)>100:
            qwords=qwords[:100]
        
        if not is_test:
            correct_ans = line["correct_answer"],
            answers = [line["distractor1"],line["distractor2"],line["distractor3"],correct_ans[0]]
            
            new_order = [0,1,2,3]
            random.shuffle(new_order)
            
            answers = [ answers[i] for i in new_order]
            
            correct_ans_idx = new_order[-1]
            
            # training file parsing
            #correct_ans_idx = ord(correct_ans) - ord('A')
            for idx, answer in enumerate(answers):
                #awords = nltk.word_tokenize(answer)
                #print(answer)
                awords = tokenizer(answer)
                qapairs.append((qwords, awords, idx == correct_ans_idx))
        else:
            # test file parsing (no correct answer)
            answers = cols[2:]
            for answer in answers:
                awords = nltk.word_tokenize(answer)
                qapairs.append((qwords, awords, None))
    fqa.close()
    return qapairs
    

您不需要重新计算 qapairs,它们已经保存并通过主程序中的行加载:

with open("processed_input.pickle", 'rb') as f:
    qapairs = pickle.load(f)

这是示例(请向右滚动查看答案和真假标签):

>>> qapairs[0]
(['what', 'type', 'of', 'organism', 'is', 'commonly', 'used', 'in', 'preparation', 'of', 'foods', 'such', 'as', 'cheese', 'and', 'yogurt', '', 'mesophiles', 'grow', 'best', 'in', 'moderate', 'temperature', 'typically', 'between', '25°c', 'and', '40°c', '(77°f', 'and', '104°f)', 'mesophiles', 'are', 'often', 'found', 'living', 'in', 'or', 'on', 'the', 'bodies', 'of', 'humans', 'or', 'other', 'animals', 'the', 'optimal', 'growth', 'temperature', 'of', 'many', 'pathogenic', 'mesophiles', 'is', '37°c', '(98°f)', 'the', 'normal_human', 'body', 'temperature', 'mesophilic', 'organisms', 'have', 'important', 'uses', 'in', 'food', 'preparation', 'including', 'cheese', 'yogurt', 'beer', 'and', 'wine'], ['viruses'], False)
>>> qapairs[1]
(['what', 'type', 'of', 'organism', 'is', 'commonly', 'used', 'in', 'preparation', 'of', 'foods', 'such', 'as', 'cheese', 'and', 'yogurt', '', 'mesophiles', 'grow', 'best', 'in', 'moderate', 'temperature', 'typically', 'between', '25°c', 'and', '40°c', '(77°f', 'and', '104°f)', 'mesophiles', 'are', 'often', 'found', 'living', 'in', 'or', 'on', 'the', 'bodies', 'of', 'humans', 'or', 'other', 'animals', 'the', 'optimal', 'growth', 'temperature', 'of', 'many', 'pathogenic', 'mesophiles', 'is', '37°c', '(98°f)', 'the', 'normal_human', 'body', 'temperature', 'mesophilic', 'organisms', 'have', 'important', 'uses', 'in', 'food', 'preparation', 'including', 'cheese', 'yogurt', 'beer', 'and', 'wine'], ['mesophilic', 'organisms'], True)
>>> qapairs[2]
(['what', 'type', 'of', 'organism', 'is', 'commonly', 'used', 'in', 'preparation', 'of', 'foods', 'such', 'as', 'cheese', 'and', 'yogurt', '', 'mesophiles', 'grow', 'best', 'in', 'moderate', 'temperature', 'typically', 'between', '25°c', 'and', '40°c', '(77°f', 'and', '104°f)', 'mesophiles', 'are', 'often', 'found', 'living', 'in', 'or', 'on', 'the', 'bodies', 'of', 'humans', 'or', 'other', 'animals', 'the', 'optimal', 'growth', 'temperature', 'of', 'many', 'pathogenic', 'mesophiles', 'is', '37°c', '(98°f)', 'the', 'normal_human', 'body', 'temperature', 'mesophilic', 'organisms', 'have', 'important', 'uses', 'in', 'food', 'preparation', 'including', 'cheese', 'yogurt', 'beer', 'and', 'wine'], ['protozoa'], False)
>>> qapairs[3]
(['what', 'type', 'of', 'organism', 'is', 'commonly', 'used', 'in', 'preparation', 'of', 'foods', 'such', 'as', 'cheese', 'and', 'yogurt', '', 'mesophiles', 'grow', 'best', 'in', 'moderate', 'temperature', 'typically', 'between', '25°c', 'and', '40°c', '(77°f', 'and', '104°f)', 'mesophiles', 'are', 'often', 'found', 'living', 'in', 'or', 'on', 'the', 'bodies', 'of', 'humans', 'or', 'other', 'animals', 'the', 'optimal', 'growth', 'temperature', 'of', 'many', 'pathogenic', 'mesophiles', 'is', '37°c', '(98°f)', 'the', 'normal_human', 'body', 'temperature', 'mesophilic', 'organisms', 'have', 'important', 'uses', 'in', 'food', 'preparation', 'including', 'cheese', 'yogurt', 'beer', 'and', 'wine'], ['gymnosperms'], False)

下一步由kaggle.py/中的函数vectorize_qapairs()完成,在github上它使用余弦距离,我已将其更改为余弦相似度(1 - 最相似(零角度),0 - 不相似(正交) ) 根据您的评论:

def vectorize_qapairs(qapairs, word2idx, seq_maxlen):
    Xq, Xa, Y = [], [], []
    for qapair in qapairs:
        Xq.append([word2idx[qword] for qword in qapair[0]])
        Xa.append([word2idx[aword] for aword in qapair[1]])
        #Y.append(np.array([1, 0]) if qapair[2] else np.array([0, 1]))
        # cosine similarity: 1 for 0 degree angle
        Y.append(np.array([1]) if qapair[2] else np.array([0]))
    return (pad_sequences(Xq, maxlen=seq_maxlen), 
            pad_sequences(Xa, maxlen=seq_maxlen),
            np.array(Y))

如您所见,如果有“True”标签,则为 1,否则为 0。

现在我希望模型计算余弦,就像图片上一样,然后将其与 0-1 标签进行比较。我相信你所做的是正确的,模型现在正在工作,但我希望它开始学习,而不是输出精度 = 0.75 左右的数字,这对应于始终输出 False。我什至现在为了调试目的简化了代码,去掉了卷积:

#question
qenc = Sequential()
qenc.add(Embedding(output_dim=WORD2VEC_EMBED_SIZE, input_dim=vocab_size,
                   input_length=seq_maxlen))
qenc.add(Bidirectional(LSTM(QA_EMBED_SIZE, return_sequences=True), 
                       merge_mode="sum"))

aenc = Sequential()
aenc.add(Embedding(output_dim=WORD2VEC_EMBED_SIZE, input_dim=vocab_size,
                   input_length=seq_maxlen))
aenc.add(Bidirectional(LSTM(QA_EMBED_SIZE, return_sequences=True),
                       merge_mode="sum"))

# attention model

#notice that I'm taking "tensors" qenc.output and aenc.output
#I'm not passing "models" to a layer, I'm passing tensors 
#that was the problem with your lambda

attOut = Dot(axes=1)([qenc.output, aenc.output]) 
    #shape = (samples,QA_EMBED_SIZE//2, QA_EMBED_SIZE//2)
    #I really don't understand this output shape.... 
    #I'd swear it should be (samples, 1, QA_EMBED_SIZE//2)
attOut = Flatten()(attOut) #shape is now only (samples,)
#attOut = Dense((qenc.output_shape[1]*(QA_EMBED_SIZE // 2)))(attOut)
#attOut = Reshape((qenc.output_shape[1], QA_EMBED_SIZE // 2))(attOut) 
attOut = Dense((qenc.output_shape[1]*(QA_EMBED_SIZE)))(attOut)
attOut = Reshape((qenc.output_shape[1], QA_EMBED_SIZE))(attOut) 



flatAttOut = Flatten()(attOut)
flatQencOut = Flatten()(qenc.output)
similarity = Dot(axes=1,normalize=True)([flatQencOut,flatAttOut])

model = Model([qenc.input,aenc.input],similarity)

# I tried MSE and binary crossentropy
model.compile(optimizer="adam", loss="binary_crossentropy",
              metrics=["accuracy"])

print("Training...")
checkpoint = ModelCheckpoint(
    filepath=os.path.join(MODEL_DIR, "qa-lstm-attn-best.hdf5"),
    verbose=1, save_best_only=True)
model.fit([Xqtrain, Xatrain], Ytrain, batch_size=BATCH_SIZE,
          nb_epoch=NBR_EPOCHS, validation_split=0.1,
          callbacks=[checkpoint])

当然,代码不完全是我的,我使用了来自 https://github.com/sujitpal/dl-models-for-qa 的实现,它计算了 Dense(2) 层,并且遇到了学习只输出 false 的相同问题。

我想知道我是否犯了一些我无法理解的错误。谢谢!

【问题讨论】:

    标签: keras


    【解决方案1】:

    您正在使用分支。不要使用带有分支的顺序模型。

    您可以将qencaenc 用作Sequential 模型,没问题,因为它们是单一路径,没有任何后果。

    我在这里从您的代码的第一部分中举例。

    更新使用 keras 1 的调用:

    #question
    qenc = Sequential()
    qenc.add(Embedding(output_dim=WORD2VEC_EMBED_SIZE, input_dim=vocab_size,
                       input_length=seq_maxlen))
    qenc.add(Bidirectional(LSTM(QA_EMBED_SIZE, return_sequences=True), 
                           merge_mode="sum"))
    qenc.add(Dropout(0.3))
    qenc.add(Convolution1D(QA_EMBED_SIZE // 2, 5, padding="valid"))
    qenc.add(MaxPooling1D(pool_size=2, padding="valid"))
    qenc.add(Dropout(0.3))
    
    # answer
    aenc = Sequential()
    aenc.add(Embedding(output_dim=WORD2VEC_EMBED_SIZE, input_dim=vocab_size,
                       input_length=seq_maxlen))
    aenc.add(Bidirectional(LSTM(QA_EMBED_SIZE, return_sequences=True),
                           merge_mode="sum"))
    aenc.add(Dropout(0.3))
    aenc.add(Convolution1D(QA_EMBED_SIZE // 2, 5, padding="valid"))
    aenc.add(MaxPooling1D(pool_size=2, padding="valid"))
    aenc.add(Dropout(0.3))
    

    注意观察每个模型的输入和输出形状:

    • qenc 输出形状为:(samples, (seq_maxlen-4)/2, QA_EMBED_SIZE//2)
    • aenc 输出形状为:(samples, (seq_maxlen-4)/2, QA_EMBED_SIZE//2)

    但是attn正在合并两个分支,让它成为一个函数式APIModel

    # attention model
    
    #notice that I'm taking "tensors" qenc.output and aenc.output
    #I'm not passing "models" to a layer, I'm passing tensors 
    #that was the problem with your lambda
    
    attOut = Dot(axes=1)([qenc.output, aenc.output]) 
        #shape = (samples,QA_EMBED_SIZE//2, QA_EMBED_SIZE//2)
        #I really don't understand this output shape.... 
        #I'd swear it should be (samples, 1, QA_EMBED_SIZE//2)
    attOut = Flatten()(attOut) #shape is now only (samples,)
    attOut = Dense((qenc.output_shape[1]*(QA_EMBED_SIZE // 2)))(attOut)
    attOut = Reshape((qenc.output_shape[1], QA_EMBED_SIZE // 2))(attOut) 
    
    • 注意输出形状:(samples, (seq_maxlen-4)/2, QA_EMBED_SIZE // 2)
    • 还要注意,这个注意力部分需要两个输入

    如果您出于某种原因“需要”将 attn 模型与其他模型分开,请告诉我,因为上面的代码需要稍作改动

    现在,您可以展平qencattn 的输出,没问题,您不能在qenc 模型的“内部”执行此操作。

    flatAttOut = Flatten()(attOut)
    flatQencOut = Flatten()(qenc.output)
    similarity = Dot(axes=1,normalize=True)([flatQencOut,flatAttOut])
    

    最后创建完整的模型:

    model = Model([qenc.input,aenc.input],similarity)
    

    警告:此模型输出相似度 - 你确定 y_train 是相似度吗? (形状 =(样本,1))。
    如果是的话,好的。如果不是,请详细说明您的问题,并解释您的模型的输出、您的训练数据以及您希望这种相似性出现的时间和地点。


    平衡类的损失函数:

    您可以尝试自定义损失函数来平衡类,因为您有 75%-25% 的假真输出比率。

    import keras.backend as K
    
    def balanceLoss(yTrue,yPred):
    
        loss = K.binary_crossentropy(yTrue,yPred)
        scaledTrue = (2*yTrue) + 1 
            #true values are 3 times worth the false values
            #contains 3 for true and 1 for false
    
        return scaledTrue * loss
    
    model.compile(optimizer='adam', loss=balanceLoss)
    

    不确定 binary_crossentropy 是否适合这种类型的平衡,但您也可以尝试均方误差。

    【讨论】:

    • 如果它很快达到,你可以继续训练(很长)一段时间,看看以后会不会好起来。模型最初通常会找到最简单的方法来预测与当前输出相等的所有内容。
    • 降低学习率可能有助于该过程,尽管“adam”通常会在很长一段时间后找到它的方式。
    • 你也可以考虑在Conv层和Dense层中加入一些激活函数,比如tanh或者sigmoid,这样可以让模型训练更流畅。
    • 我曾经有一个模型,我必须训练很长时间,损失似乎冻结了(变化小于屏幕显示的数字数量)。该模型显然预测所有内容都为零,但我找不到问题所在。所以有一天我让它训练了很长时间,突然它找到了一条路。
    • 我建议使用自定义损失来降低真假结果之间的差异。
    【解决方案2】:

    我认为问题在于您使用的是Sequential 模型,并且以下代码块导致了问题(注意您使用attn.add() 而不是model.add())。

    model = Sequential()
    attn.add(Merge([qenc, attn], mode="cos", dot_axes=1))
    

    我认为在您的案例中使用Graph 模型更有意义。

    还有,你这里弄错了

    # Plain sum - not working properly!
    model = Sequential()
    model.add(Merge([qenc, attn], mode="sum"))
    model.add(Flatten())
    model.add(Dense(1, activation="softmax")) # <--- ERROR
    

    单个神经元上的 Softmax 毫无意义!您应该改用Dense(1, activation='sigmoid')。或者,您可以使用Dense(2, activation='softmax')

    【讨论】:

    • 感谢您的回答!请原谅我的错误,我只是尝试了很多东西,到处都是错误,写了这段代码来总结我的努力。不幸的是,在修复 model.add() 之后,我仍然收到错误,我想 Keras 不明白我正在重用已使用的模型。我将更新我的问题以显示发生了什么
    猜你喜欢
    • 1970-01-01
    • 2017-09-15
    • 2016-11-29
    • 1970-01-01
    • 2017-01-13
    • 1970-01-01
    • 2021-07-15
    • 2017-08-03
    • 2014-08-21
    相关资源
    最近更新 更多