如何使用Keras功能API结合多个特征和多个输出

356 阅读11分钟

我们都使用公共数据集(如CIFAR-10MNISTPima Indians Diabetes)编写了我们的第一个深度学习代码,用于回归、分类等。

我们可以注意到的一个共同点是,在一个给定的项目中,每个特征的数据类型都是一样的。我们要么有图像要分类,要么有数值要输入到回归模型中。

你有没有想过,我们如何将文本、图像和数字等各种类型的数据结合起来,以获得不仅仅是一个输出,而是多种输出,如分类和回归?

现实生活中的问题在形式上不是连续的或同质的。你很可能要在实践中把多个输入和输出纳入你的深度学习模型。

本文深入探讨了构建一个深度学习模型,该模型接收文本和数字输入并返回回归和分类输出。

概述

  • 数据清理
  • 文本预处理
  • 神奇的模型
  • 结论

数据清理

当我们看到一个有多个文本和数字输入的问题,并且要生成一个回归和分类输出时,我们应该首先清理我们的数据集。

首先,我们应该避免有大量NullNaN值特征的数据。这样的值应该用平均数、中位数等来代替,这样这些记录可以被使用,而不会对整体数据产生太大的影响。

我们可以将与其他特征相比通常较大的数值转换成小数值,以确保对神经网络的权重没有影响。为了应对这种影响,可以使用标准化或最小-最大比例等技术,将数据转化为更小的数值范围,同时仍保留它们之间的关系:

from sklearn.preprocessing import MinMaxScaler

data = [[-1010, 20000], [-50000, 6345], [10000, 1000], [19034, 18200]]

# Instantiate the MinMaxScaler object
scaler = MinMaxScaler()

# Fit it to the data
scaler.fit(data)

# Use transform to output the transformed data
print(scaler.transform(data))
# Output:
[[0.70965032 1.        ]
 [0.         0.28131579]
 [0.86913695 0.        ]
 [1.         0.90526316]]

文本预处理

我们在这里可以通过两种方式处理多个文本输入:

  • 为每个文本特征建立专门的LSTM(长短时记忆网络),然后再结合其中的数字输出
  • 先结合文本特征,然后用单个LSTM进行训练

在本文中,我们将探讨第二种方法,因为它在处理具有不同长度的大量文本特征时非常有效。

合并文本特征

原始数据集有多个文本特征,我们必须将其连接起来。仅仅把这些字符串加起来是没有效率的。我们必须与模型沟通,这些是单个字符串中的不同特征。

我们处理这个问题的方法是在它们之间用一个特殊的""标签连接它们,表示特征的结束。最后,所有的文本特征将被转换为一个单一的输入:

"Feature 1 <EOF> Feature 2 <EOF> ….. <EOF> Feature N"

现在我们有一个单一的文本输入和一组数字输入。

小写字母和停止词的去除

以下技术在预处理过程中是有用的。

小写字母是将单词转换为小写字母的过程,以提供更好的清晰度。这在预处理的过程中和以后进行解析的阶段都很有帮助。

去除停顿词是指去除常用词的过程,以便更多关注文本的内容特征。我们可以使用**NLTK** 来删除传统的停顿词:

from nltk.corpus import stopwords
stop_words = set(stopwords.words('english'))

# Example sentence 
text = "there is a stranger out there who seems to be the owner of this mansion"

# Split our sentence at each space using .split()
words = text.split()

text = ""

# For each word, if the word isnt in stop words, then add to our new text variable
for word in words:
    if word not in stop_words:
        text += (word+" ")
        
print(text)

Output>> stranger seems owner mansion

词干和词缀化

这些技术是用来改善语义分析的。

词根化是指去除单词的前缀和后缀,以简化它们。这种技术被用来确定领域分析中的领域词汇。它有助于通过将一个词转换为其词根形式来减少该词的变体。

举例来说,programprogramsprogrammer是program的变体。

下面是一个使用NLTK进行词干处理的例子:

from nltk.stem import PorterStemmer  
# Instantiate our stemmer as ps
ps = PorterStemmer()
 
# Example sentence 
sentence = "he is likely to have more likes for the post he posted recently"

# Split our sentence at each space using .split()
words = sentence.split()

sentence = ""

# For each word, get the stem and add to our new sentence variable
for w in words:
    sentence += (ps.stem(w) + " ")
print(sentence)

Output >> he is like to have more like for the post he post recent

Lemmatization是对一个词的转折形式进行分组的过程。lemmatization不是把一个词还原成它的词干,而是确定该词的相应字典形式。这有助于模型确定单个单词的含义:

from nltk.stem import WordNetLemmatizer
lemmatizer = WordNetLemmatizer()

sentence = "It is dangerous to jump to feet on rocky surfaces"
words = sentence.split()
sentence = ""

for word in words:
    sentence+= (lemmatizer.lemmatize(word) + " ")

print(sentence)

Output >> It is dangerous to jump to foot on rocky surface

培训 测试 分割

一个重要的步骤是确保我们对数据集进行适当的抽样,并在每个历时后获得足够的数据来测试我们的模型。目前,我们有两个输入和输出,分别是文本和一个数字输入阵列。我们将把它们分成训练集和验证集,如下所示:

from sklearn.model_selection import train_test_split

y_reg_train, y_reg_val, y_clf_train, y_clf_val, X_text_train, X_text_val, X_num_train, X_num_val = train_test_split(y_reg,y_clf,X_text,X_num,test_size=0.25,random_state=42)

符号化

在实际做任何NLP建模之前,我们需要数值输入机器,以便它进行所有这些数学运算。例如,字符串 "猫 "应该被转换为数字或有意义的张量,以便模型处理。符号化可以帮助我们做到这一点,用一个数字代表每个单词:

from keras.preprocessing.text import Tokenizer

# instantiate our tokenizer
tokenizer = Tokenizer(num_words = 10)

# Create a sample list of words to tokenize
text = ["python","is","cool","but","C++","is","the","classic"]

# Fit the Tokenizer to the words
tokenizer.fit_on_texts(text)

# Create a word_index to track tokens
word_index = tokenizer.word_index
print(word_index['python'])

Output >> 2

在我们的解决方案中,我们将不得不在训练文本特征上拟合标记器。这是预处理中的一个关键点,因为如果我们想防止过度拟合,我们不应该让模型或标记器知道我们的测试输入。

我们将把它作为一个字典储存在word_index中。我们可以使用pickle来保存标记化器,以备将来使用,就像在预测中只使用模型一样。

让我们看看在对我们的语料库进行拟合之后,我们将如何在我们的案例中使用标记化器:

# Tokenize and sequence training data
X_text_train_sequences = tokenizer.texts_to_sequences(X_text_train)

# Use pad_sequences to transforms a list (of length num_samples) of sequences (lists of integers) into a 2D Numpy array of shape (num_samples, num_timesteps)
X_text_train_padded = pad_sequences(X_text_train_sequences,maxlen=max_length,
                    padding=padding_type, truncating=trunction_type)
                    
# Tokenize and sequence validation data                   
X_text_val_sequences = tokenizer.texts_to_sequences(X_text_val)

# pad_sequences for validation set
X_text_val_padded = pad_sequences(X_text_val_sequences,maxlen=max_length,
                    padding=padding_type, truncating=trunction_type)

用GloVe嵌入层

嵌入层为每个词提供了维度。想象一下,"国王 "在我们的标记器中被存储为102。这有什么意义吗?我们需要表达一个词的维度,嵌入层可以帮助我们做到这一点。

通常情况下,使用预先训练好的嵌入层,如 GloVe来最大限度地利用我们的数据。嵌入将tokenizer中的单词_index变成一个大小为(1, N)的矩阵,给定单词的N个维度。

import numpy as np
# Download glove beforehand

# Here we are using the 100 Dimensional embedding of GloVe  
f = open('glove.6B/glove.6B.100d.txt')   
for line in f:       
   values = line.split()       
   word = values[0]       
   coefs = np.asarray(values[1:], dtype='float32') 
   embeddings_index[word] = coefs   
f.close()

# Creating embedding matrix for each word in Our corpus
embedding_matrix = np.zeros((len(word_index) + 1, 100))   
for word, i in word_index.items():   
   embedding_vector = embeddings_index.get(word)   
   if embedding_vector is not None:   
       # words not found in the embedding index will be all-zeros.  
       embedding_matrix[i] = embedding_vector

现在我们有一个嵌入矩阵,可以作为权重输入到我们的嵌入层中。

神奇的模型

我们已经完成了所有需要的预处理,现在我们有了X和Y的值,可以输入到模型中。我们将在这里使用Keras Functional API来建立这个特殊的模型。

在我们直接进入代码之前,重要的是要理解为什么一个连续的模型是不够的。初学者会熟悉顺序模型,因为它能帮助我们快速建立一个线性流动的模型:

from keras.layers import Dense, Embedding, LSTM
from keras.models import Model
from keras.models import Sequential

# Instantiate a sequential NN
model = Sequential([       
  Embedding(len(word_index) + 1,
            100,
            weights=[matrix_embedding],
            input_length=max_length,
            trainable=False)
  LSTM(embedding_dim,),
  Dense(100, activation='relu'),
  Dense(5, activation='sigmoid')
])

在顺序模型中,我们对输入、输出和流动没有太多的控制。顺序模型没有能力共享层或层的分支,而且,也不能有多个输入或输出。如果我们想要处理多个输入和输出,那么我们必须使用Keras功能API。

Keras函数式API

Keras函数式API允许我们细化地建立每个层,部分或全部输入直接连接到输出层,并且能够将任何层连接到任何其他层。像连接值、共享层、分支层以及提供多个输入和输出等功能是选择函数式api而不是顺序式的最有力的理由。

这里我们有一个文本输入和一个由9个数字特征组成的数组作为模型的输入,以及两个输出,正如前面几节所讨论的。max_length 是我们可以设置的文本输入的最大长度。embedding_matrix是我们之前为嵌入层得到的权重。

双向LSTMs

递归神经网络(RNN)是一个具有内部存储器的前馈神经网络。由于它对每一个数据输入都执行相同的功能,所以RNN在本质上是递归的,而当前输入的输出取决于过去的输出。

双向LSTM是RNN的一种类型,对长序列有更好的效果,有更好的记忆力,能保留时间序列的背景。双向LSTM在输入序列上训练两个而不是一个LSTM,在这些问题上,输入序列的所有时间段都可以通过从两个方向遍历得到,如下图所示。

这是个过于简单的解释,所以我鼓励你 阅读更多上,以获得更好的清晰度。我希望这能帮助你建立一个关于LSTM的小概念。源于此。用于序列标记的双向LSTM-CRF模型

下面是我们的模型架构,用于解决这个问题:

from keras.layers import Dense, Embedding, LSTM, Bidirectional, Input
from keras.models import Model

def make_model(max_length,embedding_matrix):

    # Defining the embedding layer
    embedding_dim = 64 
    input1=Input(shape=(max_length,))
    
    embedding_layer = Embedding(len(word_index) + 1,
                                100,
                                weights=[embedding_matrix],
                                input_length=max_length,
                                trainable=False)(input1)
                                
    # Building LSTM for text features                          
    bi_lstm_1 = Bidirectional(LSTM(embedding_dim,return_sequences=True))(embedding_layer)
    
    bi_lstm_2 = Bidirectional(LSTM(embedding_dim))(bi_lstm_1)   
    lstm_output =  Model(inputs = input1,outputs = bi_lstm_2)
    
    #Inputting Number features
    input2=Input(shape=(9,))  
    
    # Merging inputs
    merge = concatenate([lstm_output.output,input2])
    
    # Building dense layers for classification with number features
    reg_dense1 = Dense(64, activation='relu')(merge)
    reg_dense2 = Dense(16, activation='relu')(reg_dense1) 
    output1 = Dense(1, activation='sigmoid')(reg_dense2)
    
    # Building dense layers for classification with number features
    clf_dense1 = Dense(64, activation='relu')(merge)
    clf_dense2 = Dense(16, activation='relu')(clf_dense1)
    
    # 5 Categories in classification
    output2 = Dense(5, activation='sigmoid')(clf_dense2)
 
    model = Model(inputs=[lstm_output.input,input2], outputs=[output1,output2])
 
    return model

模型摘要


Layer (type)                    Output Shape         Param #  
   Connected to
==============================================================================
input_1 (InputLayer)            [(None, 2150)]       0        

______________________________________________________________________________
embedding (Embedding)           (None, 2150, 100)    1368500  
   input_1[0][0]
______________________________________________________________________________
bidirectional (Bidirectional)   (None, 2150, 128)    84480    
   embedding[0][0]
______________________________________________________________________________
bidirectional_1 (Bidirectional) (None, 128)          98816    
   bidirectional[0][0]
______________________________________________________________________________
input_2 (InputLayer)            [(None, 9)]          0        

______________________________________________________________________________
concatenate (Concatenate)       (None, 137)          0        
   bidirectional_1[0][0]

   input_2[0][0]
______________________________________________________________________________
dense (Dense)                   (None, 64)           8832     
   concatenate[0][0]
______________________________________________________________________________
dense_3 (Dense)                 (None, 64)           8832     
   concatenate[0][0]
______________________________________________________________________________
dense_1 (Dense)                 (None, 16)           1040     
   dense[0][0]
______________________________________________________________________________
dense_4 (Dense)                 (None, 16)           1040     
   dense_3[0][0]
______________________________________________________________________________
dense_2 (Dense)                 (None, 1)            17       
   dense_1[0][0]
______________________________________________________________________________
dense_5 (Dense)                 (None, 5)            85          dense_4[0][0]
==============================================================================
Total params: 1,571,642
Trainable params: 203,142
Non-trainable params: 1,368,500

鉴于我们有多个输入和输出,模型摘要可能看起来很吓人。让我们把模型可视化,以获得一个大画面。

模型的可视化

模型结构的可视化

现在,我们所要做的就是编译和拟合这个模型。让我们看看它与普通情况有什么不同。我们可以为我们模型的输入和输出值输入数组:

import keras
from keras.optimizers import Adam

model = make_model(max_length,embedding_matrix)
    
# Defining losses for each output
losses ={ 'dense_2':keras.losses.MeanSquaredError(),
    'dense_5':keras.losses.CategoricalCrossentropy()}
 
opt = Adam(lr=0.01)

# Model Compiling
model.compile(optimizer=opt, loss=losses,metrics="accuracy")

# Model Fitting
H = model.fit(x=[X_text_train_padded, X_num_train],
y={'dense_2': y_reg_train, 'dense_5': y_clf_train},
	validation_data=([X_text_val_padded, X_num_val],
    	{'dense_2': y_reg_val, 'dense_5': y_clf_val}), epochs=10,verbose=1)

这就是它的全部内容了!

总结

**Keras Functional API**帮助我们建立了如此强大的模型,所以可能性确实是巨大而令人兴奋的。对输入、输出、层和流程进行更好的控制,有助于人们以高水平的精度和灵活性来设计模型。我鼓励大家尝试不同的层、参数和一切可能的方法,以获得使用Hypertuning的这些功能的最佳效果。

祝你在自己的实验中取得好成绩,并感谢你的阅读!