16、深度网络组合与Keras定制实践

深度网络组合与Keras定制实践

1. 深度网络组合概述

在深度学习中,全连接网络(FCN)、卷积神经网络(CNN)和循环神经网络(RNN)是三种基础网络。虽然它们各自有最适合的应用场景,但我们也可以像搭乐高积木一样,将这些基础网络组合起来,利用Keras的函数式API以新颖有趣的方式构建更大、更实用的模型。

这些组合模型通常是为特定任务量身定制的,难以一概而论。不过,它们往往涉及从多个输入中学习或生成多个输出。以下是一些示例:
- 问答网络 :该网络学习根据故事和问题预测答案。
- 孪生网络 :计算一对图像之间的相似度,网络通过输入一对图像进行训练,预测二元(相似/不相似)或分类(相似度等级)标签。
- 目标分类与定位网络 :从图像中联合学习预测图像类别以及图像在图片中的位置。

前两个例子是多输入组合网络,最后一个是多输出组合网络。

2. Keras实现问答记忆网络
2.1 背景与数据集

我们将构建一个用于问答的记忆网络。记忆网络是一种特殊的架构,除了通常的可学习单元(如RNN)外,还包含一个记忆单元。每个输入都会更新记忆状态,最终输出通过结合记忆和可学习单元的输出计算得出。

该架构在2014年的一篇论文中被提出。一年后,另一篇论文提出了合成数据集和一套标准的20个问答任务,每个任务的难度都比前一个更高,并应用各种深度学习网络来解决这些任务。其中,记忆网络在所有任务中取得了最佳效果。这个数据集后来通过Facebook的bAbI项目向公众开放。

我们的记忆网络实现与2015年一篇论文中描述的最为接近,所有训练都在单个网络中联合进行,使用bAbI数据集解决第一个问答任务。

2.2 代码实现步骤
  1. 导入必要的库
from keras.layers import Input
from keras.layers.core import Activation, dense, Dropout, Permute
from keras.layers.embeddings import Embedding
from keras.layers.merge import add, concatenate, dot
from keras.layers.recurrent import LSTM
from keras.models import Model
from keras.preprocessing.sequence import pad_sequences
from keras.utils import np_utils
import collections
import itertools
import nltk
import numpy as np
import matplotlib.pyplot as plt
import os
  1. 解析数据
    bAbI数据的第一个问答任务的训练集和测试集各包含10000个短句子。一个故事由两到三个句子组成,后面跟着一个问题,每个故事的最后一句话末尾附有问题和答案。
DATA_DIR = "../data"
TRAIN_FILE = os.path.join(DATA_DIR, "qa1_single-supporting-fact_train.txt")
TEST_FILE = os.path.join(DATA_DIR, "qa1_single-supporting-fact_test.txt")

def get_data(infile):
    stories, questions, answers = [], [], []
    story_text = []
    fin = open(TRAIN_FILE, "rb")
    for line in fin:
        line = line.decode("utf-8").strip()
        lno, text = line.split(" ", 1)
        if "t" in text:
            question, answer, _ = text.split("t")
            stories.append(story_text)
            questions.append(question)
            answers.append(answer)
            story_text = []
        else:
            story_text.append(text)
    fin.close()
    return stories, questions, answers

data_train = get_data(TRAIN_FILE)
data_test = get_data(TEST_FILE)
  1. 构建词汇表
def build_vocab(train_data, test_data):
    counter = collections.Counter()
    for stories, questions, answers in [train_data, test_data]:
        for story in stories:
            for sent in story:
                for word in nltk.word_tokenize(sent):
                    counter[word.lower()] += 1
        for question in questions:
            for word in nltk.word_tokenize(question):
                counter[word.lower()] += 1
        for answer in answers:
            for word in nltk.word_tokenize(answer):
                counter[word.lower()] += 1
    word2idx = {w:(i+1) for i, (w, _) in enumerate(counter.most_common())}
    word2idx["PAD"] = 0
    idx2word = {v:k for k, v in word2idx.items()}
    return word2idx, idx2word

word2idx, idx2word = build_vocab(data_train, data_test)
vocab_size = len(word2idx)
  1. 获取最大序列长度
    记忆网络基于RNN,故事和问题中的每个句子都被视为一个单词序列,因此需要找出故事和问题的最大序列长度。
def get_maxlens(train_data, test_data):
    story_maxlen, question_maxlen = 0, 0
    for stories, questions, _ in [train_data, test_data]:
        for story in stories:
            story_len = 0
            for sent in story:
                swords = nltk.word_tokenize(sent)
                story_len += len(swords)
            if story_len > story_maxlen:
                story_maxlen = story_len
        for question in questions:
            question_len = len(nltk.word_tokenize(question))
            if question_len > question_maxlen:
                question_maxlen = question_len
    return story_maxlen, question_maxlen

story_maxlen, question_maxlen = get_maxlens(data_train, data_test)
  1. 数据向量化
    将(故事、问题和答案)三元组转换为整数单词ID序列,并对结果序列进行零填充。
def vectorize(data, word2idx, story_maxlen, question_maxlen):
    Xs, Xq, Y = [], [], []
    stories, questions, answers = data
    for story, question, answer in zip(stories, questions, answers):
        xs = [[word2idx[w.lower()] for w in nltk.word_tokenize(s)] 
                   for s in story]
        xs = list(itertools.chain.from_iterable(xs))
        xq = [word2idx[w.lower()] for w in nltk.word_tokenize(question)]
        Xs.append(xs)
        Xq.append(xq)
        Y.append(word2idx[answer.lower()])
    return pad_sequences(Xs, maxlen=story_maxlen), \
        pad_sequences(Xq, maxlen=question_maxlen), \
        np_utils.to_categorical(Y, num_classes=len(word2idx))

Xstrain, Xqtrain, Ytrain = vectorize(data_train, word2idx, story_maxlen, question_maxlen)
Xstest, Xqtest, Ytest = vectorize(data_test, word2idx, story_maxlen, question_maxlen)
  1. 定义模型
    模型有两个输入:问题的单词ID序列和句子的单词ID序列。每个输入都通过嵌入层转换为64维嵌入空间中的向量。故事序列还通过另一个嵌入层投影到最大问题长度的嵌入空间。所有嵌入层的权重初始为随机值,并与网络的其余部分联合训练。
EMBEDDING_SIZE = 64
LATENT_SIZE = 32

# inputs
story_input = Input(shape=(story_maxlen,))
question_input = Input(shape=(question_maxlen,))

# story encoder memory
story_encoder = Embedding(input_dim=vocab_size,
                          output_dim=EMBEDDING_SIZE,
                          input_length=story_maxlen)(story_input)
story_encoder = Dropout(0.3)(story_encoder)

# question encoder
question_encoder = Embedding(input_dim=vocab_size,
                             output_dim=EMBEDDING_SIZE,
                             input_length=question_maxlen)(question_input)
question_encoder = Dropout(0.3)(question_encoder)

# match between story and question
match = dot([story_encoder, question_encoder], axes=[2, 2])

# encode story into vector space of question
story_encoder_c = Embedding(input_dim=vocab_size,
                            output_dim=question_maxlen,
                            input_length=story_maxlen)(story_input)
story_encoder_c = Dropout(0.3)(story_encoder_c)

# combine match and story vectors
response = add([match, story_encoder_c])
response = Permute((2, 1))(response)

# combine response and question vectors
answer = concatenate([response, question_encoder], axis=-1)
answer = LSTM(LATENT_SIZE)(answer)
answer = Dropout(0.3)(answer)
answer = dense(vocab_size)(answer)
output = Activation("softmax")(answer)

model = Model(inputs=[story_input, question_input], outputs=output)
model.compile(optimizer="rmsprop", loss="categorical_crossentropy",
              metrics=["accuracy"])
  1. 训练模型
BATCH_SIZE = 32
NUM_EPOCHS = 50
history = model.fit([Xstrain, Xqtrain], [Ytrain], batch_size=BATCH_SIZE, 
                    epochs=NUM_EPOCHS,
                    validation_data=([Xstest, Xqtest], [Ytest]))
  1. 验证预测结果
ytest = np.argmax(Ytest, axis=1)
Ytest_ = model.predict([Xstest, Xqtest])
ytest_ = np.argmax(Ytest_, axis=1)
NUM_DISPLAY = 10
for i in range(NUM_DISPLAY):
    story = " ".join([idx2word[x] for x in Xstest[i].tolist() if x != 0])
    question = " ".join([idx2word[x] for x in Xqtest[i].tolist()])
    label = idx2word[ytest[i]]
    prediction = idx2word[ytest_[i]]
    print(story, question, label, prediction)
3. 定制Keras

Keras已经内置了很多功能,大多数情况下我们可以使用提供的组件构建所有模型,无需定制。但如果确实需要定制,Keras也能满足需求。

Keras是一个高级API,将计算密集型任务委托给TensorFlow或Theano后端。为了使代码在两个后端之间具有可移植性,自定义代码应使用Keras后端API(https://keras.io/backend/),该API提供了一组函数,就像所选后端的门面一样。根据所选后端,对后端门面的调用将转换为相应的TensorFlow或Theano调用。

使用后端API不仅能保证代码的可移植性,还能使代码更易于维护,因为Keras代码通常比等效的TensorFlow或Theano代码更高级、更紧凑。在极少数情况下,如果需要直接使用后端,Keras组件可以直接在TensorFlow代码中使用。

定制Keras通常意味着编写自定义层或自定义距离函数。下面将介绍一些简单的Keras层定制示例。

3.1 使用Lambda层

Keras提供了Lambda层,可以包装自定义函数。例如,构建一个将输入张量逐元素平方的层:

model.add(lambda(lambda x: x ** 2))

也可以在Lambda层中包装函数。例如,构建一个计算两个输入张量逐元素欧几里得距离的自定义层:

from keras import backend as K
from keras.layers import Input
from keras.layers.core import dense

VECTOR_SIZE = 10

def euclidean_distance(vecs):
    x, y = vecs
    return K.sqrt(K.sum(K.square(x - y), axis=1, keepdims=True))

def euclidean_distance_output_shape(shapes):
    shape1, shape2 = shapes
    return (shape1[0], 1)

lhs_input = Input(shape=(VECTOR_SIZE,))
lhs = dense(1024, kernel_initializer="glorot_uniform", activation="relu")(lhs_input)
rhs_input = Input(shape=(VECTOR_SIZE,))
rhs = dense(1024, kernel_initializer="glorot_uniform", activation="relu")(rhs_input)
sim = lambda(euclidean_distance, output_shape=euclidean_distance_output_shape)([lhs, rhs])

4. 构建自定义归一化层

虽然Lambda层很有用,但有时我们需要更多的控制。以局部响应归一化层为例,该技术通过对局部输入区域进行归一化来工作,但由于不如其他正则化方法(如Dropout和批量归一化)以及更好的初始化方法有效,已逐渐失宠。

4.1 测试工具

为了方便开发后端API代码,可以使用一个小测试工具来验证代码是否按预期工作。

from keras.models import Sequential
from keras.layers.core import Dropout, Reshape
import numpy as np

def test_layer(layer, x):
    layer_config = layer.get_config()
    layer_config["input_shape"] = x.shape
    layer = layer.__class__.from_config(layer_config)
    model = Sequential()
    model.add(layer)
    model.compile("rmsprop", "mse")
    x_ = np.expand_dims(x, axis=0)
    return model.predict(x_)[0]

# 测试工具验证
x = np.random.randn(10, 10)
layer = Dropout(0.5)
y = test_layer(layer, x)
assert(x.shape == y.shape)

x = np.random.randn(10, 10, 3)
layer = ZeroPadding2D(padding=(1,1))
y = test_layer(layer, x)
assert(x.shape[0] + 2 == y.shape[0])
assert(x.shape[1] + 2 == y.shape[1])

x = np.random.randn(10, 10)
layer = Reshape((5, 20))
y = test_layer(layer, x)
assert(y.shape == (5, 20))
4.2 自定义局部响应归一化层

我们将实现WITHIN_CHANNEL模式的局部响应归一化。该层的代码遵循标准结构:

from keras import backend as K
from keras.engine.topology import Layer, InputSpec

class LocalResponseNormalization(Layer):
    def __init__(self, n=5, alpha=0.0005, beta=0.75, k=2, **kwargs):
        self.n = n
        self.alpha = alpha
        self.beta = beta
        self.k = k
        super(LocalResponseNormalization, self).__init__(**kwargs)

    def build(self, input_shape):
        self.shape = input_shape
        super(LocalResponseNormalization, self).build(input_shape)

    def call(self, x, mask=None):
        if K.image_dim_ordering == "th":
            _, f, r, c = self.shape
        else:
            _, r, c, f = self.shape
        squared = K.square(x)
        pooled = K.pool2d(squared, (self.n, self.n), strides=(1, 1),
                          padding="same", pool_mode="avg")
        if K.image_dim_ordering == "th":
            summed = K.sum(pooled, axis=1, keepdims=True)
            averaged = self.alpha * K.repeat_elements(summed, f, axis=1)
        else:
            summed = K.sum(pooled, axis=3, keepdims=True)
            averaged = self.alpha * K.repeat_elements(summed, f, axis=3)
        denom = K.pow(self.k + averaged, self.beta)
        return x / denom

    def get_output_shape_for(self, input_shape):
        return input_shape
4.3 测试自定义层
x = np.random.randn(225, 225, 3)
layer = LocalResponseNormalization()
y = test_layer(layer, x)
assert(x.shape == y.shape)

虽然有经验的Keras开发者经常构建自定义层,但网上可用的示例并不多。这可能是因为自定义层通常是为特定狭窄目的而构建的,可能不具有广泛的实用性。不过,了解如何构建自定义Keras层后,查看一些他人的实现(如Keunwoo Choi的melspectogram和Shashank Gupta的NodeEmbeddingLayer)会很有启发。

深度网络组合与Keras定制实践

5. 组合模型与定制层的优势与应用场景
5.1 组合模型的优势

组合模型能够将不同基础网络的优势结合起来,以适应更复杂的任务。例如,问答网络结合了故事和问题的信息,通过记忆网络的机制更好地理解上下文并生成准确的答案;孪生网络利用基础网络的特征提取能力,计算图像之间的相似度,可应用于图像匹配、人脸识别等领域;目标分类与定位网络则同时完成了图像分类和定位任务,提高了模型的综合性能。

组合模型类型 优势 应用场景
问答网络 结合多源信息,利用记忆机制理解上下文 智能客服、知识问答系统
孪生网络 有效计算相似度 图像匹配、人脸识别、签名验证
目标分类与定位网络 同时完成分类和定位任务 自动驾驶、安防监控
5.2 定制层的应用场景

定制层可以根据特定任务的需求,对模型进行精细调整。Lambda层提供了一种简单的方式来实现自定义操作,适用于一些简单的逐元素计算或函数包装。而自定义归一化层则可以针对特定的数据分布或任务需求,设计独特的归一化方法,提高模型的稳定性和性能。

  • Lambda层 :适用于简单的自定义操作,如数据变换、特征提取等。
  • 自定义归一化层 :在数据分布特殊或需要特定归一化策略时使用,如局部响应归一化层在早期的图像识别任务中有一定应用。
6. 总结与展望

通过本文的介绍,我们了解了如何将基础的深度学习网络组合成更复杂、更强大的模型,以及如何使用Keras进行定制开发。组合模型和定制层为我们提供了更多的灵活性和创造力,使我们能够根据具体任务的需求,设计出更适合的模型。

在未来的深度学习发展中,组合模型和定制层的应用将会更加广泛。随着任务的日益复杂,单一的基础网络可能无法满足需求,组合模型将成为解决复杂问题的重要手段。同时,定制层的发展也将更加多样化,开发者可以根据不同的领域和任务,设计出更多高效、独特的定制层。

为了更好地应用组合模型和定制层,我们需要不断学习和实践,深入理解各种基础网络的原理和特点,掌握Keras等深度学习框架的使用方法。同时,要关注行业的最新发展动态,借鉴他人的优秀经验,不断提升自己的技术水平。

以下是一个简单的流程图,展示了构建组合模型和定制层的基本流程:

graph LR
    A[明确任务需求] --> B[选择基础网络]
    B --> C[组合基础网络]
    C --> D[定义模型结构]
    D --> E[数据处理与准备]
    E --> F[训练模型]
    F --> G[评估模型性能]
    G --> H{性能是否满足要求}
    H -- 是 --> I[应用模型]
    H -- 否 --> J[调整模型结构或参数]
    J --> D
    K[确定定制需求] --> L[选择定制方式]
    L -- Lambda层 --> M[编写自定义函数]
    L -- 自定义层 --> N[编写层代码]
    M --> O[集成到模型中]
    N --> O
    O --> D

希望本文能够为你在深度学习的道路上提供一些帮助和启发,让你能够更加自信地应对各种复杂的任务。在实践中不断探索和创新,相信你一定能够构建出更加优秀的深度学习模型。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值