2、Keras函数式API:Sequential模型回顾、函数式API入门、多输入多输出模型构建
2.1 Sequential模型回顾——简单场景的利器
我们先快速回顾一下Sequential模型。说白了,它就是一层叠一层的线性堆叠。你想想看,如果你的网络是一条直线,输入从一头进去,输出从另一头出来,中间没有分叉、没有合并,那Sequential就是最省事的选择。
我个人习惯在快速验证想法时先用Sequential。比如做一个简单的图像分类,几层卷积加全连接,代码写起来特别清爽:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Conv2D, Flatten
model = Sequential([
Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),
Flatten(),
Dense(128, activation='relu'),
Dense(10, activation='softmax')
])
嗯,这里要注意:Sequential模型要求每一层只有一个输入和一个输出。一旦你的网络结构出现分支,比如一个输入要同时送给两个不同的层,或者要把两个层的输出合并起来,Sequential就无能为力了。
2.2 函数式API入门——从线性到图结构
函数式API的核心思想是什么?就是把层当作函数来调用。你想想看,在Sequential里我们是把层像搭积木一样叠起来,而在函数式API里,我们是把层当作函数,输入张量经过层函数处理后得到输出张量。
这样做的好处很明显:你可以自由地定义张量的流向。一个张量可以流向多个层,多个张量可以合并,甚至可以形成环(比如循环神经网络)。
来看一个最简单的例子,用函数式API实现上面那个Sequential模型:
from tensorflow.keras.layers import Input, Dense, Conv2D, Flatten
from tensorflow.keras.models import Model
inputs = Input(shape=(28, 28, 1))
x = Conv2D(32, (3, 3), activation='relu')(inputs)
x = Flatten()(x)
x = Dense(128, activation='relu')(x)
outputs = Dense(10, activation='softmax')(x)
model = Model(inputs=inputs, outputs=outputs)
看到区别了吗?在函数式API中,我们显式地定义了输入张量 inputs,然后一层层传递下去,最后用 Model 类把输入和输出包装起来。这样做的好处是,你可以随时查看中间层的输出,也可以轻松地构建复杂的网络结构。
2.3 多输入多输出模型构建——实战中的刚需
为什么要多输入多输出?我在实际项目中遇到过这样一个场景:要预测一个商品的销量,输入数据包括商品图片、文字描述、历史价格走势。这三个输入的数据类型完全不同,图片用CNN处理,文字用RNN处理,价格用全连接层处理。最后要把三个分支的特征合并起来,输出两个结果:一个是销量预测值,一个是推荐指数。
这种场景用Sequential根本没法做,但函数式API处理起来就游刃有余。
来看一个具体的例子,假设我们要构建一个模型,输入两个向量,输出两个结果:
from tensorflow.keras.layers import Input, Dense, concatenate
from tensorflow.keras.models import Model
# 定义两个输入
input_a = Input(shape=(64,), name='input_a')
input_b = Input(shape=(128,), name='input_b')
# 第一个分支
x_a = Dense(32, activation='relu')(input_a)
x_a = Dense(16, activation='relu')(x_a)
# 第二个分支
x_b = Dense(64, activation='relu')(input_b)
x_b = Dense(32, activation='relu')(x_b)
# 合并两个分支
combined = concatenate([x_a, x_b])
# 输出1:分类结果
output_1 = Dense(10, activation='softmax', name='output_1')(combined)
# 输出2:回归结果
output_2 = Dense(1, activation='linear', name='output_2')(combined)
# 构建模型
model = Model(inputs=[input_a, input_b], outputs=[output_1, output_2])
# 编译时可以为每个输出指定不同的损失函数和权重
model.compile(
optimizer='adam',
loss={
'output_1': 'sparse_categorical_crossentropy',
'output_2': 'mse'
},
loss_weights={
'output_1': 0.7,
'output_2': 0.3
},
metrics={
'output_1': 'accuracy',
'output_2': 'mae'
}
)
这里有几个关键点我要强调一下:
- 输入命名:给每个Input层和输出层起个名字,这样在训练和预测时可以通过名字来传递数据,代码可读性会好很多。
- 合并方式:常用的合并操作有
concatenate(拼接)、add(相加)、multiply(相乘)。选择哪种取决于你的业务逻辑。 - 多损失函数:每个输出可以有自己的损失函数和权重。比如分类任务用交叉熵,回归任务用均方误差,通过
loss_weights来平衡它们的重要性。
loss_weights。
2.4 训练多输入多输出模型
训练的时候,数据也要对应地组织成字典或列表形式:
# 假设我们有训练数据
import numpy as np
# 生成模拟数据
x_a_train = np.random.randn(1000, 64)
x_b_train = np.random.randn(1000, 128)
y_1_train = np.random.randint(0, 10, size=(1000,))
y_2_train = np.random.randn(1000, 1)
# 方式一:用字典传递
history = model.fit(
x={'input_a': x_a_train, 'input_b': x_b_train},
y={'output_1': y_1_train, 'output_2': y_2_train},
epochs=10,
batch_size=32,
validation_split=0.2
)
# 方式二:用列表传递(注意顺序要和定义时一致)
# history = model.fit(
# [x_a_train, x_b_train],
# [y_1_train, y_2_train],
# epochs=10,
# batch_size=32
# )
我个人更推荐用字典方式,因为顺序搞错了模型也不会报错,但结果会完全不对。用字典的话,名字对上了就万无一失。
2.5 函数式API的更多玩法
除了多输入多输出,函数式API还能做很多事情:
- 共享层:同一个层可以被多个输入共享。比如在孪生网络中,两个输入共享同一个特征提取层。
- 残差连接:可以把某一层的输出跳过几层直接加到后面的层上。
- 条件分支:虽然Keras本身不支持动态条件,但你可以通过自定义层来实现类似的效果。
来看一个共享层的例子:
# 定义一个共享的Dense层
shared_dense = Dense(64, activation='relu')
input_1 = Input(shape=(32,))
input_2 = Input(shape=(32,))
# 同一个层处理两个不同的输入
output_1 = shared_dense(input_1)
output_2 = shared_dense(input_2)
# 合并
merged = concatenate([output_1, output_2])
final_output = Dense(1, activation='sigmoid')(merged)
model = Model(inputs=[input_1, input_2], outputs=final_output)
你想想看,这种共享层的设计在自然语言处理中特别常见。比如处理两个句子时,用同一个词嵌入层和LSTM层来提取特征,这样能保证两个句子被同等对待。
好了,这一章的内容就到这里。函数式API是Keras中最灵活的部分,也是构建复杂模型的基础。下一章我们会深入自定义层,到时候你会看到函数式API和自定义层结合起来能发挥多大的威力。