Keras函数式API
之前所有的神经网络都是基于Sequential模型实现的,而且网络都是层的线性叠加。但是在实际情况下,有些网络需要多个独立的输入,有些网络需要多个输出;而且有些层之间具有内部分支。
不用Sequential序贯模型的解决方案:Keras函数式API
在线阅读地址:https://livebook.manning.com/book/deep-learning-with-python/about-this-book
多输入模型
有些任务需要多模态输入(multimodal),这些任务合并来自不同输入的数据源,并使用不同类型的神经网络层来处理不同类型的数据。
一个案例来理解:利用输入数据来预测一件二手衣服的价格
函数式API简介
In [1]:
代码语言:javascript复制import tensorflow as tf
from keras import Input, layers
input_tensor = Input(shape=(32,)) # 张量
dense = layers.Dense(32, activation="relu") # 一层是一个函数
output_tensor = dense(input_tensor) # 张量上调用一个层,返回一个张量
函数式API和Sequential模型对比
In [2]:
代码语言:javascript复制from keras.models import Sequential, Model
from keras import layers
from keras import Input
1、对应的Sequential模型代码:
In [3]:
代码语言:javascript复制# # Sequential模型
# model = Sequential()
# model.add(layers.Dense(32, activation="relu", input_shape=(64,)))
# model.add(layers.Dense(32, activation="relu"))
# model.add(layers.Dense(10, activation="softmax"))
2、对应的函数式API版本:
In [4]:
代码语言:javascript复制# 对应的函数式API版本
input_tensor = Input(shape=(64, ))
x = layers.Dense(32, activation="relu")(input_tensor)
x = layers.Dense(32, activation="relu")(x)
output_tensor = layers.Dense(10, activation="softmax")(x)
# 将输入张量和输出张量转换为一个模型
model = Model(input_tensor, output_tensor)
# 查看模型
model.summary()
Model: "model"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_2 (InputLayer) [(None, 64)] 0
dense_1 (Dense) (None, 32) 2080
dense_2 (Dense) (None, 32) 1056
dense_3 (Dense) (None, 10) 330
=================================================================
Total params: 3,466
Trainable params: 3,466
Non-trainable params: 0
_________________________________________________________________
绘制模型计算图(新增)
In [5]:
代码语言:javascript复制# 绘制模型计算图
import pydot
# keras ---> tf.keras
tf.keras.utils.plot_model(model,"my_first_model.png",show_shapes = True)
在将Model类实例化的过程中,Keras会在后台检索从 input_tensor 到 output_sensor所包含的每层,并将这些组成一个类图的数据结构,即一个Model。
我们可以看到outputs_tensor是通过input_tensor多次变换得到的。
In [6]:
代码语言:javascript复制# 模型编译、训练等
model.compile(optimizer="rmsprop", loss="categorical_crossentropy")
In [7]:
代码语言:javascript复制# 对于这种Model实例进行编译、训练或者评估时,其API和Sequential相同
import numpy as np
import pandas as pd
X = np.random.random((1000, 64))
y = np.random.random((1000, 10))
# 模型训练
model.fit(X,
y,
epochs=10, # 训练10轮
batch_size=128 # 每个轮次的大小
)
# 模型评估
score = model.evaluate(X,y)
score
Epoch 1/10
8/8 [==============================] - 1s 4ms/step - loss: 12.6609
Epoch 2/10
8/8 [==============================] - 0s 2ms/step - loss: 14.8846
Epoch 3/10
8/8 [==============================] - 0s 2ms/step - loss: 17.9791
Epoch 4/10
8/8 [==============================] - 0s 3ms/step - loss: 21.3085
Epoch 5/10
8/8 [==============================] - 0s 4ms/step - loss: 25.0077
Epoch 6/10
8/8 [==============================] - 0s 2ms/step - loss: 29.1692
Epoch 7/10
8/8 [==============================] - 0s 2ms/step - loss: 34.0520
Epoch 8/10
8/8 [==============================] - 0s 2ms/step - loss: 39.3982
Epoch 9/10
8/8 [==============================] - 0s 2ms/step - loss: 45.5565
Epoch 10/10
8/8 [==============================] - 0s 2ms/step - loss: 52.2696
32/32 [==============================] - 0s 4ms/step - loss: 56.2226
Out[7]:
代码语言:javascript复制56.22259521484375
实现多输入模型
函数式API能够构建多个输入的模型。一般使用keras.layers.add、keras.layers.concatenate等方法将不同的层进入合并。
一个典型的模型有两个输入:
- 一个自然语言描述的问题
- 一个文本片段(新闻等)
模型需要生成一个回答,通常这个回答只包含一个词语,可以通过对某个预定义的词表做softmax得到。
函数式API实现双输入问答模型
下面函数式API构建的模型设置两个分支:文本输入和问题输入;分别编码为向量,连接这两个向量。在连接好的基础上添加一个softmax分类器:
In [8]:
代码语言:javascript复制from keras.models import Model
from keras import layers
from keras import Input
text_word_size = 10000
question_word_size = 10000
answer_word_size = 500
# 1、文本输入:长度可变的整数序列
text_input = Input(shape=(None,),
dtype="int32",
name="text" # 输入命名
)
# 将输入嵌入长度为64的向量
embedded_text = layers.Embedding(text_word_size, 64)(text_input)
# 利用LSTM将向量编码为单个向量
encoded_text = layers.LSTM(32)(embedded_text)
# 2、问题输入
# 对相同的问题进行处理,使用不同的层实例
question_input = Input(shape=(None, ),
dtype="int32",
name="question"
)
# 将输入嵌入长度为32的向量
embedded_question = layers.Embedding(question_word_size, 32)(question_input)
encoded_question = layers.LSTM(16)(embedded_question)
# 3、将编码后的问题输入和文本输入连接起来
concatenated = layers.concatenate([encoded_text, encoded_question]
,axis=-1)
# 4、在上面的基础之上添加一个softmax分类器
answer = layers.Dense(answer_word_size, activation="softmax")(concatenated)
模型实例化时输入两个输入和输出:
In [9]:
代码语言:javascript复制model = Model([text_input, question_input], answer) # 指定两个输入和输出
model.compile(optimizer="rmsprop",
loss="categorical_crossentropy",
metrics=["acc"])
训练双输入模型
提供2个方法:
- 向模型输入一个由numpy数组组成的列表
- 输入一个由输入名称映射为numpy数组的字典
In [10]:
代码语言:javascript复制# 将数据输入到多输入模型中
import numpy as np
import tensorflow as tf # 添加内容
# import tensorflow.compat.v1 as tf
# tf.disable_v2_behavior()
n_samples = 1000
max_length = 100
# 生成虚构的Numpy数据
text = np.random.randint(1, text_word_size,
size=(n_samples, max_length))
question = np.random.randint(1, question_word_size,
size=(n_samples, max_length))
answers = np.random.randint(answer_word_size, size=(n_samples))
# 回答是one-hot编码
# 修改内容:keras--->tf.keras
answers = tf.keras.utils.to_categorical(answers, answer_word_size)
answers
Out[10]:
代码语言:javascript复制array([[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
...,
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.]], dtype=float32)
In [11]:
代码语言:javascript复制# # 方法1、向模型输入一个由numpy数组组成的列表
# model.fit([text, question],
# answers,
# epochs=10,
# batch_size=128)
In [12]:
代码语言:javascript复制# 方式2:传入一个将输入名称映射成Numpy数组的字典
model.fit({"text": text, "question": question},
answers,
epochs=10,
batch_size=128
)
Epoch 1/10
8/8 [==============================] - 9s 210ms/step - loss: 6.2151 - acc: 0.0030
Epoch 2/10
8/8 [==============================] - 1s 151ms/step - loss: 6.1974 - acc: 0.0430
Epoch 3/10
8/8 [==============================] - 1s 149ms/step - loss: 6.1434 - acc: 0.0020
Epoch 4/10
8/8 [==============================] - 1s 152ms/step - loss: 6.0633 - acc: 0.0060
Epoch 5/10
8/8 [==============================] - 1s 146ms/step - loss: 6.0020 - acc: 0.0080
Epoch 6/10
8/8 [==============================] - 1s 148ms/step - loss: 5.9075 - acc: 0.0100
Epoch 7/10
8/8 [==============================] - 1s 160ms/step - loss: 5.7991 - acc: 0.0170
Epoch 8/10
8/8 [==============================] - 1s 156ms/step - loss: 5.7253 - acc: 0.0330
Epoch 9/10
8/8 [==============================] - 1s 167ms/step - loss: 5.6553 - acc: 0.0380
Epoch 10/10
8/8 [==============================] - 1s 151ms/step - loss: 5.5947 - acc: 0.0500
Out[12]:
代码语言:javascript复制<keras.callbacks.History at 0x15cd18dd0>
多输出模型
用函数式API实现一个三输出的模型。一个简单的例子就是网络试图同时预测数据的不同性质,比如根据数据同时预测用户的年龄、性别和收入水平等
搭建多输出模型
In [13]:
代码语言:javascript复制# 作用:用函数式API实现一个三输出模型
from keras import layers, Input
from keras.models import Model
word_size = 50000
num_income_groups = 10
# 1、输入
posts_input = Input(shape=(None,), dtype="int32", name="posts")
# 2、使用Embedding层生成一个词向量
embedded_posts = layers.Embedding(256, word_size)(posts_input)
# 3、池化过程
x = layers.Conv1D(128, 5, activation="relu")(embedded_posts)
x = layers.MaxPooling1D(5)(x)
x = layers.Conv1D(256, 5, activation="relu")(x)
x = layers.Conv1D(256, 5, activation="relu")(x)
x = layers.MaxPooling1D(5)(x)
x = layers.Conv1D(256, 5, activation="relu")(x)
x = layers.Conv1D(256, 5, activation="relu")(x)
# global池化主要是用来解决全连接的问题,其主要是将最后一层的特征图进行整张图的一个池化
x = layers.GlobalMaxPooling1D()(x)
x = layers.Dense(128, activation="relu")(x)
# 每个输出层都有自己的名称
age_prediction = layers.Dense(1, name="age")(x) # 1、预测年龄是回归任务
income_prediction = layers.Dense(num_income_groups,
activation="softmax",
name="income")(x) # 2、预测收入是回归任务
gender_prediction = layers.Dense(1,
activation="sigmoid",
name="gender")(x) # 3、预测性别是分类任务
model = Model(posts_input, [age_prediction,income_prediction, gender_prediction])
model.summary()
Model: "model_2"
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
posts (InputLayer) [(None, None)] 0 []
embedding_2 (Embedding) (None, None, 50000) 12800000 ['posts[0][0]']
conv1d (Conv1D) (None, None, 128) 32000128 ['embedding_2[0][0]']
max_pooling1d (MaxPooling1D) (None, None, 128) 0 ['conv1d[0][0]']
conv1d_1 (Conv1D) (None, None, 256) 164096 ['max_pooling1d[0][0]']
conv1d_2 (Conv1D) (None, None, 256) 327936 ['conv1d_1[0][0]']
max_pooling1d_1 (MaxPooling1D) (None, None, 256) 0 ['conv1d_2[0][0]']
conv1d_3 (Conv1D) (None, None, 256) 327936 ['max_pooling1d_1[0][0]']
conv1d_4 (Conv1D) (None, None, 256) 327936 ['conv1d_3[0][0]']
global_max_pooling1d (GlobalMa (None, 256) 0 ['conv1d_4[0][0]']
xPooling1D)
dense_5 (Dense) (None, 128) 32896 ['global_max_pooling1d[0][0]']
age (Dense) (None, 1) 129 ['dense_5[0][0]']
income (Dense) (None, 10) 1290 ['dense_5[0][0]']
gender (Dense) (None, 1) 129 ['dense_5[0][0]']
==================================================================================================
Total params: 45,982,476
Trainable params: 45,982,476
Non-trainable params: 0
__________________________________________________________________________________________________
通过上面的代码我们看到,每个网络的各个头都有指定不同的损失函数。预测不同的目标是不同的任务。
合并不同损失最简单的方法就是:对所有的损失求和
编译选项:多重损失
In [14]:
代码语言:javascript复制# 写法1
# model.compile(optimizer="rmsprop",
# loss = ["mse","categorical_crossentropy","binary_crossentropy"])
In [15]:
代码语言:javascript复制# # 上面的等效写法:只有输出层指定了名称下面的写法才生效
# model.compile(optimizer="rmsprop",
# loss={"age": "mse",
# "income": "categorical_crossentropy",
# "gender": "binary_crossentropy"
# })
损失加权
严重不平衡的损失贡献会导致模型针对单个损失值最大的任务优先优化,而不考虑其他的优化。
为了解决这个问题,我们可以为每个最终损失的最大贡献分配不同大小的重要性:损失加权
In [16]:
代码语言:javascript复制# model.compile(optimizer="rmsprop",
# loss = ["mse","categorical_crossentropy","binary_crossentropy"],
# loss_weights=[0.25, 1., 10.] # 不同损失的加权
# )
In [17]:
代码语言:javascript复制# 上面的等效写法:只有输出层指定了名称下面的写法才生效
model.compile(optimizer="rmsprop",
loss={"age": "mse",
"income": "categorical_crossentropy",
"gender": "binary_crossentropy"
},
loss_weights={"age":0.25,
"income":1.,
"gender":10.})
训练模型(修改)
多输出模型的训练输入可以是Numpy数组组成的列表或者字典。
假设posts
输入数据,age_targets、income_targets、gender_targets
是样本标签,那么向模型输入数据的代码如下:
#写法1
#修改:posts---> posts_input
model.fit(posts_input,
[age_targets,income_targets,gender_targets], # 假设的y标签
epochs=10,
batch_size=64
)
代码语言:javascript复制# 写法2
#修改:posts---> posts_input
model.fit(posts_input,
{"age": age_targets,
"income": income_targets,
"gender": gender_targets},
epochs=10,
batch_size=64
)