我们都使用公共数据集(如CIFAR-10、MNIST或Pima Indians Diabetes)编写了我们的第一个深度学习代码,用于回归、分类等。
我们可以注意到的一个共同点是,在一个给定的项目中,每个特征的数据类型都是一样的。我们要么有图像要分类,要么有数值要输入到回归模型中。
你有没有想过,我们如何将文本、图像和数字等各种类型的数据结合起来,以获得不仅仅是一个输出,而是多种输出,如分类和回归?
现实生活中的问题在形式上不是连续的或同质的。你很可能要在实践中把多个输入和输出纳入你的深度学习模型。
本文深入探讨了构建一个深度学习模型,该模型接收文本和数字输入并返回回归和分类输出。
概述
- 数据清理
- 文本预处理
- 神奇的模型
- 结论
数据清理
当我们看到一个有多个文本和数字输入的问题,并且要生成一个回归和分类输出时,我们应该首先清理我们的数据集。
首先,我们应该避免有大量Null或NaN值特征的数据。这样的值应该用平均数、中位数等来代替,这样这些记录可以被使用,而不会对整体数据产生太大的影响。
我们可以将与其他特征相比通常较大的数值转换成小数值,以确保对神经网络的权重没有影响。为了应对这种影响,可以使用标准化或最小-最大比例等技术,将数据转化为更小的数值范围,同时仍保留它们之间的关系:
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
词干和词缀化
这些技术是用来改善语义分析的。
词根化是指去除单词的前缀和后缀,以简化它们。这种技术被用来确定领域分析中的领域词汇。它有助于通过将一个词转换为其词根形式来减少该词的变体。
举例来说,program、programs和programmer是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的这些功能的最佳效果。
祝你在自己的实验中取得好成绩,并感谢你的阅读!