Keras学习笔记-初探Keras模型

时间:2024-04-03 16:35:56

 

 

初探Sequential模型

Sequential模型就是简单的“线性”堆叠,一层接着一层的连接,可通过.add()方法将一个一个层添加至模型,例:

 network = Sequential()
 network.add(Dense(512, activation='relu', input_shape=(28*28,)))
 network.add(Dense(10,activation='softmax'))

也可以向Sequential模型传递一个包含层参数的list,直接构造模型:

 network = Sequential([Dense(512, activation='relu', input_shape=(28*28,)),
                         Dense(10,activation='softmax')])

指定输入数据shape

基于计算图的模型有一个特点,就是需要先定义好模型结构,再传入数据,这样做的好处是利于优化程序。就如同铺下水管道一样,先把各个管道路线设计好,最后再通水(传入数据)。模型是需要知道输入的shape(如同管道一样,知道输入口的大小,后面管道才能设计)。

Sequential模型的第一层需要接受输入数据shape参数,后面的层会依次推导出数据shape,有几种方法可以指定输入层的shape:

  • 传入一个input_shape参数给第一层,input_shape 是一个元组,可以为None(None表示正整数),这个参数不包含batch信息。这也是前面用到的方法:

    network.add(Dense(512, activation='relu', input_shape=(28*28,)))
  • 传递一个batch_input_shape参数给第一层,该参数包含数据的batch大小。该参数在指定固定大小batch时比较有用(常用于stateful RNN)。事实上,Keras在内部会通过添加一个None将input_shape转化为batch_input_shape

    network.add(Dense(512, activation='relu', batch_input_shape=(None,28*28)))
  • 有些2D层,如Dense,支持通过指定其输入维度input_dim来隐含的指定输入数据shape。一些3D的时域层支持通过参数input_diminput_length来指定输入shape。

    network.add(Dense(512, activation='relu', input_dim=784))

下面例子是相互等价的:

#使用input_shape
model = Sequential()
model.add(LSTM(32, input_shape=(10, 64)))

#使用batch_input_shape
model = Sequential()
model.add(LSTM(32, batch_input_shape=(None, 10, 64)))


#使用input_length和`nput_dim
model = Sequential()
model.add(LSTM(32, input_length=10, input_dim=64))

编译

在构建好模型架构后,开始训练之前,我们需要通过compile方法配置学习过程,compile方法接收三个参数:

  • 优化器optimizer:指定模型优化方法标识。例如rmsprop or adagrad,或者是Optimizer class的一个实例对象.

  • 损失函数loss function:这是模型需要优化的目标。例如categorical_crossentropy or mse,或者是 一个objective function.

  • 度量列表metrics list:对于分类问题,常设为metrics=['accuracy'],度量可以预设的度量或自定义度量函数。度量函数应该返回单个张量,或一个完成metric_name->metric_value映射的字典

下面是一个使用案例:

# For a multi-class classification problem
model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['accuracy'])

# For a binary classification problem
model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['accuracy'])

# For a mean squared error regression problem
model.compile(optimizer='rmsprop',
              loss='mse')

# 自定义度量标准
import keras.backend as K

def mean_pred(y_true, y_pred):
    return K.mean(y_pred)

model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['accuracy', mean_pred])

训练

Keras模型在输入数据和标签(在Numpy arrays数据类型的)上训练。训练一个模型,通常会使用fit函数,下面是一些例子:

  • 二分类

     # For a single-input model with 2 classes (binary classification):
    
     model = Sequential()
     model.add(Dense(32, activation='relu', input_dim=100))
     model.add(Dense(1, activation='sigmoid'))
     model.compile(optimizer='rmsprop',
                   loss='binary_crossentropy',
                   metrics=['accuracy'])
    
     # Generate dummy data
     import numpy as np
     data = np.random.random((1000, 100))
     labels = np.random.randint(2, size=(1000, 1))
    
     # Train the model, iterating on the data in batches of 32 samples
     model.fit(data, labels, epochs=10, batch_size=32)
  • 多分类:

     # For a single-input model with 10 classes (categorical classification):
    
     model = Sequential()
     model.add(Dense(32, activation='relu', input_dim=100))
     model.add(Dense(10, activation='softmax'))
     model.compile(optimizer='rmsprop',
                   loss='categorical_crossentropy',
                   metrics=['accuracy'])
    
     # Generate dummy data
     import numpy as np
     data = np.random.random((1000, 100))
     labels = np.random.randint(10, size=(1000, 1))
    
     # Convert labels to categorical one-hot encoding
     one_hot_labels = keras.utils.to_categorical(labels, num_classes=10)
    
     # Train the model, iterating on the data in batches of 32 samples
     model.fit(data, one_hot_labels, epochs=10, batch_size=32)

案例-使用Keras做二分类问题

二分类是机器学习中比较常见的一个问题.在本案例中,我们将依据电影评论(简称影评)本身内容,对影评分类为(正评价)和(负评价)。

IMDB数据集

IMDB数据集内含50000个高度分化的网络影评,共分为两个部分:25000个训练集,25000个测试集。每部分包含50%的正评价,50%的负评价。

为什么我们要分开训练集和测试集,因为一个模型在训练集上表现的很好,不代表同样在未知数据上表现很好,而我们关心的正是模型在未知数据上的表现,这就引出了过拟合欠拟合的概念(这里不做讨论了,可以参考Andrew Ng的教程)。

正如同MNIST数据集一样,Keras中内含对IMDB数据集的预处理模块:预处理将影评转换为一系列整形序列(原本是字符序列),转换是按照字典对应过来的。

载入数据集(第一次会下载,大概80M):

from keras.datasets import imdb

(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)

参数num_words=10000表示我们只保持训练集中出现频率最高的10000个词,少部分的词会被丢弃,这样便于管理序列大小。

变量train_datatest_data是影评序列列表,每个影评都是一串词序列. train_labelstest_labels是只含0和1构成的列表,其中0代表负评价,1代表正评价:

>>>train_data[0]    # 影评内容
[1,
 14,
 22,
 16,
 43,
 ...
 ]
 >>>train_labels[0] # 影评标签
 1

因为我们限制了只考虑前10000个频率词汇,故最大的词index不会超过10000:

>>>max([max(sequence) for sequence in train_data])
9999

也可以把影评转化为英语,下面就是一个方法,实际过程中还是数字序列方便处理:

# word_index is a dictionary mapping words to an integer index
>>>word_index = imdb.get_word_index()
# We reverse it, mapping integer indices to words
>>>reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])
# We decode the review; note that our indices were offset by 3
# because 0, 1 and 2 are reserved indices for "padding", "start of sequence", and "unknown".
>>>decoded_review = ' '.join([reverse_word_index.get(i - 3, '?') for i in train_data[0]])

>>>decoded_review
"? this film was just brilliant casting ..."

准备数据

我们不能直接将整型数据直接塞给神经网络,需要将其转为张量,有两种方法可以转化:

  • 填充list,直到所有的list都有相同长度,然后转换为整型tensor,其shape为(samples, word_indices),神经网络的第一层能够处理这样的张量(其实就是Embedding层,做NLP必备嘛)

  • 我们可以使用one-hot的编码格式来转化,具体地,例如将序列[3,5]转到转为一个10000维度的向量,除了index为3和5的编码为1,其余都为0参考神经语言模型。这样做的好处是,神经网络可用Dense层来处理了

这里我们选用的后面的方法,将数据向量化,清晰化:

import numpy as np

def vectorize_sequences(sequences, dimension=10000):
    # Create an all-zero matrix of shape (len(sequences), dimension)
    results = np.zeros((len(sequences), dimension))
    for i, sequence in enumerate(sequences):
        results[i, sequence] = 1.  # set specific indices of results[i] to 1s
    return results

# Our vectorized training data
x_train = vectorize_sequences(train_data)
# Our vectorized test data
x_test = vectorize_sequences(test_data)

处理后的数据样式为:

>>>x_train[0] # 这里只输出
array([ 0.,  1.,  1., ...,  0.,  0.,  0.])

同样的也需要处理标签:

y_train = np.asarray(train_labels).astype('float32')
y_test = np.asarray(test_labels).astype('float32')

到此算是准备好数据了

建立网络模型

我们是输入是个简单的向量,标签是标量,针对这样的问题,简单的堆叠使用relu**函数的全连接层(Dense)很有效:

Dense(16, activation='relu')

参数16表示隐藏单元数量,以为这这里权重矩阵W

的shape为(input_dimension, 16).改层的输出为:

output=relu(dot(W,input)+b)

隐藏单元数量越多,模型能够学习更为复杂的表达式,但是相对的来说,计算花费就会上升且可能造成模型的泛化能力变差。

 

这里我们使用的模型架构为:

Keras学习笔记-初探Keras模型

使用Keras实现如下:

from keras import models
from keras import layers

model = models.Sequential()
model.add(layers.Dense(16, activation='relu',input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1,  activation='sigmoid'))

为模型配置compile参数,即优化器、损失函数、度量标准等;对于损失函数,二分类问题输出概率值,使用binary_crossentropy比较好,配置代码如下:

model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['accuracy'])

验证模型

为了观察模型的泛化能力,我们从训练集上摘取10000个数据用于校验集(validation set):

x_val = x_train[:10000]
partial_x_train = x_train[10000:]

y_val = y_train[:10000]
partial_y_train = y_train[10000:]

下面训练模型20个epochs,mini-batches取512,同时观察模型在校验集上的表现,这可以通过validation_data参数:

>>>history = model.fit(partial_x_train,
                        partial_y_train,
                        epochs=20,
                        batch_size=512,
                        validation_data=(x_val, y_val))

Train on 15000 samples, validate on 10000 samples
Epoch 1/20
15000/15000 [==============================] - 1s - loss: 0.5103 - acc: 0.7911 - val_loss: 0.4016 - val_acc: 0.8628
Epoch 2/20
15000/15000 [==============================] - 1s - loss: 0.3110 - acc: 0.9031 - val_loss: 0.3085 - val_acc: 0.8
...
Epoch 20/20
15000/15000 [==============================] - 1s - loss: 0.0031 - acc: 0.9998 - val_loss: 0.6796 - val_acc: 0.8683

注意调用model.fit()会返回一个History对象,其包含一个history成员,这其中包含了训练期间的数据信息。我们瞅瞅都有啥:

>>>history_dict = history.history
>>>history_dict.keys()
dict_keys(['val_acc', 'acc', 'val_loss', 'loss'])

包含了4个entries:在训练集和校验集上每个度量标准都被记录下来,使用Matplotlib将训练和校验的loss绘制出来:

import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

# "bo" is for "blue dot"
plt.plot(epochs, loss, 'bo', label='Training loss')
# b is for "solid blue line"
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()

plt.show()

Keras学习笔记-初探Keras模型

plt.clf()   # clear figure

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()

plt.show()

Keras学习笔记-初探Keras模型

可以看到,每个epochs,训练集上loss一直在下降,acc慢慢上升。这是使用SGD优化可以预见的结果。但是同时,模型在校验集上表现缺不尽人意,大概在第4次迭代时loss达到了最优,后面反而上升了,这是过拟合的一个表现。

预防过拟合有很多手段,这里就不多讲,既然模型在第四轮表现的还可以,那么就在第四轮停止:

model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['accuracy'])

model.fit(x_train, y_train, epochs=4, batch_size=512)
results = model.evaluate(x_test, y_test)


Epoch 1/4
25000/25000 [==============================] - 1s - loss: 0.4738 - acc: 0.8044     
Epoch 2/4
25000/25000 [==============================] - 1s - loss: 0.2660 - acc: 0.9076     
Epoch 3/4
25000/25000 [==============================] - 1s - loss: 0.2028 - acc: 0.9277     
Epoch 4/4
25000/25000 [==============================] - 1s - loss: 0.1700 - acc: 0.9397     
24544/25000 [============================>.] - ETA: 0s


>>>results
[0.29184698499679568, 0.88495999999999997]

我们这个简易的模型,达到了88%的准确率。

使用模型预测新数据

得到训练好的模型,接下来就可以对测试数据做预测了:

>>>model.predict(x_test)
array([[ 0.91966152],
       [ 0.86563045],
       [ 0.99936908],
       ..., 
       [ 0.45731062],
       [ 0.0038014 ],
       [ 0.79525089]], dtype=float32)

可以看到,模型对有些样本很有自信 (0.99 or more, or 0.01 or less),对有些样本比较判断比较模糊 (0.6, 0.4)。



初探函数式模型(Functional API)

Keras Functional API用于构建复杂的models,例如多输出models,向无环图(directed acyclic graphs),或共享层Models.

第一个示例:全连接网络

实际上实现这个网络用Sequential模型更佳,这里是为了快速入门。

  • 层接受tensor为输入,同时返回一个tensor
  • 输入tensor(s)和输出tensor(s)都可以用于定义Model
  • 这样的模型可以像Sequential模型一样训练
from keras.layers import Input, Dense
from keras.models import Model

# This returns a tensor
inputs = Input(shape=(784,))

# a layer instance is callable on a tensor, and returns a tensor
x = Dense(64, activation='relu')(inputs)
x = Dense(64, activation='relu')(x)
predictions = Dense(10, activation='softmax')(x)

# This creates a model that includes
# the Input layer and three Dense layers
model = Model(inputs=inputs, outputs=predictions)
model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['accuracy'])
model.fit(data, labels)  # starts training

所有模型都可调用,如同层一样

对于functional API,可以很方便的重用训练好的模型:可以把模型当做一个层来使用,通过一个tensor来调用。注意这里你不只是重用这个模型的架构,同时你也重用的模型的权重

x = Input(shape=(784,))
# This works, and returns the 10-way softmax we defined above.
y = model(x)

这可以让你快速创建model用于处理序列输入。可以将处理图像分类的模型用于处理视频分类模型,只需要一行代码:

from keras.layers import TimeDistributed

# Input tensor for sequences of 20 timesteps,
# each containing a 784-dimensional vector
input_sequences = Input(shape=(20, 784))

# This applies our previous model to every timestep in the input sequences.
# the output of the previous model was a 10-way softmax,
# so the output of the layer below will be a sequence of 20 vectors of size 10.
processed_sequences = TimeDistributed(model)(input_sequences)

多输入和多输出模型

Functional API有一个非常好的应用案例:多输入多输出模型。Functional API更有利于控制大规模复杂的数据流。

看看下图的model.我们试图预测一个新闻标题会在Twitter上会收到多少转发和点赞。主要的输入是新闻标题本身(词序列),同时模型还有一些辅助的输入,接收外部的数据例如:推送新闻的时间等。

整个model有两个loss function,,辅助的损失函数评估是基于新闻本身做出预测,主损失函数评估基于新闻和辅助信息做出预测。在模型中早期使用主要的损失函数是对于深度模型有一个好的正则调整。该模型框图如下:

Keras学习笔记-初探Keras模型

主要输入是新闻本身,即一串整型序列(词转为编码格式,编码值1-10000之间),序列长度为100.

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

# Headline input: meant to receive sequences of 100 integers, between 1 and 10000.
# Note that we can name any layer by passing it a "name" argument.
main_input = Input(shape=(100,), dtype='int32', name='main_input')

# This embedding layer will encode the input sequence
# into a sequence of dense 512-dimensional vectors.
x = Embedding(output_dim=512, input_dim=10000, input_length=100)(main_input)

# A LSTM will transform the vector sequence into a single vector,
# containing information about the entire sequence
lstm_out = LSTM(32)(x)

接下来插入辅助的损失函数,即使模型主损失函数很高的情况下,LSTM和Embedding层都能够平滑的训练:

auxiliary_output = Dense(1, activation='sigmoid', name='aux_output')(lstm_out)

此时,我们将辅助输入送入模型并LSTM的输出串联到一起:

auxiliary_input = Input(shape=(5,), name='aux_input')
x = keras.layers.concatenate([lstm_out, auxiliary_input])

# We stack a deep densely-connected network on top
x = Dense(64, activation='relu')(x)
x = Dense(64, activation='relu')(x)
x = Dense(64, activation='relu')(x)

# And finally we add the main logistic regression layer
main_output = Dense(1, activation='sigmoid', name='main_output')(x)

我们定义模型有两个输入,两个输出:

model = Model(inputs=[main_input, auxiliary_input], outputs=[main_output, auxiliary_output])

在编译模型是,指定辅助损失的权重为0.2,可通过参数loss_weightsloss为不同输出设置不同的损失函数或权值。这里我们给loss传递单个损失函数,该损失函数会被应用到所有输出上。

model.compile(optimizer='rmsprop', loss='binary_crossentropy',
              loss_weights=[1., 0.2])

通过传递数据训练模型:

model.fit([headline_data, additional_data], [labels, labels],
          epochs=50, batch_size=32)

因为我们的输入和输出都是命名过的(通过name参数),这里可这样编译模型:

model.compile(optimizer='rmsprop',
              loss={'main_output': 'binary_crossentropy', 'aux_output': 'binary_crossentropy'},
              loss_weights={'main_output': 1., 'aux_output': 0.2})

# And trained it via:
model.fit({'main_input': headline_data, 'aux_input': additional_data},
          {'main_output': labels, 'aux_output': labels},
          epochs=50, batch_size=32)

共享层

另一个使用functional API的绝佳场合就是共享层。

这次数据集是tweets,我们的模型试图分辨两个tweets是否源自一个人。

一种方法:建立一个模型可对两个tweets编码成两个向量,将两个向量串联后塞给logistic回归;输出这两个tweets来源一个人的概率值,这样的模型需要一对对正负样本训练。

因为问题本身的对称性,系统能处理第一个tweets自然也能处理第二条tweets。这里使用一个共享的LSTM层来tweets映射。
我们将tweets转为shape为(140,256)的矩阵,即140个大小为256的向量。(ASCLL码共256个)。

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

tweet_a = Input(shape=(140, 256))
tweet_b = Input(shape=(140, 256))

对不同输入共享一层,简单的实例化一次该层,然后多次调用即可:

# This layer can take as input a matrix
# and will return a vector of size 64
shared_lstm = LSTM(64)

# When we reuse the same layer instance
# multiple times, the weights of the layer
# are also being reused
# (it is effectively *the same* layer)
encoded_a = shared_lstm(tweet_a)
encoded_b = shared_lstm(tweet_b)

# We can then concatenate the two vectors:
merged_vector = keras.layers.concatenate([encoded_a, encoded_b], axis=-1)

# And add a logistic regression on top
predictions = Dense(1, activation='sigmoid')(merged_vector)

# We define a trainable model linking the
# tweet inputs to the predictions
model = Model(inputs=[tweet_a, tweet_b], outputs=predictions)

model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['accuracy'])
model.fit([data_a, data_b], labels, epochs=10)

层节点(layer”node”)

在Keras中,你可以通过layer.output()获得层的输出张量,也可以通过layer.output_shape获取输出张量的shape。如果一个层与多个输入相连,该怎么办?

如果一个层只与一个输入相连,.output返回该层的输出:

a = Input(shape=(140, 256))

lstm = LSTM(32)
encoded_a = lstm(a)

assert lstm.output == encoded_a

但如果一个层有多个输入,就会有问题:

a = Input(shape=(140, 256))
b = Input(shape=(140, 256))

lstm = LSTM(32)
encoded_a = lstm(a)
encoded_b = lstm(b)

lstm.output

>> AttributeError: Layer lstm_1 has multiple inbound nodes,
hence the notion of "layer output" is ill-defined.
Use `get_output_at(node_index)` instead.

你可以用如下方法:

assert lstm.get_output_at(0) == encoded_a
assert lstm.get_output_at(1) == encoded_b

是不是很简单~

这对于input_shapeoutput_shape属性也一样:只要层只有一个节点,或者所有节点都有着相同输入/输出的shape,那么 input/output shape就是”layer output/input shape” 。但是,例如你使用一个Conv2D层接收一个shape(32, 32, 3)的输入,再接收一个(64, 64, 3),此时改层有多个输入和输出了,你需要指定获取内容的下标:

a = Input(shape=(32, 32, 3))
b = Input(shape=(64, 64, 3))

conv = Conv2D(16, (3, 3), padding='same')
conved_a = conv(a)

# Only one input so far, the following will work:
assert conv.input_shape == (None, 32, 32, 3)

conved_b = conv(b)
# now the `.input_shape` property wouldn't work, but this does:
assert conv.get_input_shape_at(0) == (None, 32, 32, 3)
assert conv.get_input_shape_at(1) == (None, 64, 64, 3)

参考资料

Keras中文手册

《Deep Learning with Python》