【机器学习实战入门】使用CNN和LSTM构建图像描述生成器 Learn to Build Image Caption Generator with CNN & LSTM

在这里插入图片描述
基于 Python 的项目 – 图像字幕生成器

你看到一张图片,你的大脑可以轻松地判断图片的内容,但是计算机能否判断图片所展示的内容呢?计算机视觉研究人员在这方面做了大量的工作,在此之前他们认为这是不可能实现的!随着深度学习技术的发展、大量数据集的可用性和计算机算力的提升,我们现在可以构建能够为图片生成字幕的模型。

在本基于 Python 的项目中,我们将使用卷积神经网络 (Convolutional Neural Networks, CNN) 和一种递归神经网络 (Recurrent Neural Network, RNN) — 长短期记忆网络 (LSTM) 相结合的深度学习技术来实现这一目标。

什么是图像字幕生成器?

图像字幕生成器是一项结合了计算机视觉和自然语言处理概念的任务,旨在识别图像的上下文,并用自然语言(如英语)描述图像内容。

使用 CNN 的图像字幕生成器 – 关于基于 Python 的项目

本项目的目标是学习 CNN 和 LSTM 模型的概念,并通过将 CNN 与 LSTM 相结合来构建一个能够生成图像字幕的工作模型。

在本 Python 项目中,我们将使用 CNN(卷积神经网络)和 LSTM(长短期记忆网络)来实现字幕生成器。图像特征将从训练于 ImageNet 数据集的 Xception 模型中提取,然后将这些特征输入到 LSTM 模型中,LSTM 模型负责生成图像字幕。

项目的数据集

对于图像字幕生成器,我们将使用 Flickr_8K 数据集。尽管还有其他较大的数据集如 Flickr_30K 和 MSCOCO 数据集,但由于训练网络可能需要数周时间,我们将使用较小的 Flickr8k 数据集。一个巨大的数据集的优势是可以构建更好的模型。

感谢 Jason Brownlee 提供了直接下载数据集的链接(大小:1GB)。

  • Flicker8k_Dataset:数据集文件夹,包含 8091 张图像。
  • Flickr_8k_text:数据集文件夹,包含图像的文本文件和字幕。
    • Flickr8k.token:这是我们的数据集中最核心的文件,包含了图像名称和相应的字幕,每一行由换行符(“\n”)分隔。

前提条件

本项目需要具备深厚的深度学习、Python、Jupyter 笔记本操作、Keras 库、Numpy 和自然语言处理知识。
请确保你已经安装了以下所有必要的库:

pip install tensorflow
pip install keras
pip install pillow
pip install numpy
pip install tqdm
pip install jupyterlab

什么是 CNN?

卷积神经网络(CNN)是一种专门的深度神经网络,能够处理具有类似 2D 矩阵输入形状的数据。图像可以轻松地表示为 2D 矩阵,因此 CNN 在图像处理中非常有用。
在这里插入图片描述
CNN 主要用于图像分类,可以识别图像中是否包含鸟、飞机或超人等对象。

深度 CNN 的工作原理 – 基于 Python 的项目

CNN 从左到右、从上到下扫描图像,提取图像中的重要特征,并将这些特征组合起来对图像进行分类。它可以处理经过平移、旋转、缩放和透视变换的图像。

什么是 LSTM?

LSTM 是长短期记忆网络的缩写,是一种适用于序列预测问题的 RNN(递归神经网络)。根据之前的文本,我们可以预测下一个单词。LSTM 通过克服传统 RNN 的短期记忆局限性,证明了其有效性。LSTM 可以在整个输入处理过程中保留相关信息,并通过遗忘门机制丢弃无关信息。
在这里插入图片描述

图像字幕生成器模型

因此,为了构建我们的图像字幕生成器模型,我们将结合这两种架构。这种模型也被称为 CNN-RNN 模型。

  • CNN:用于从图像中提取特征。我们将使用预训练的 Xception 模型。
  • LSTM:将使用 CNN 提取的信息来帮助生成图像的描述。
    在这里插入图片描述

项目文件结构

从数据集中下载的文件:

  • Flicker8k_Dataset:数据集文件夹,包含 8091 张图像。 1G大小数据集:
    https://github.com/jbrownlee/Datasets/releases/download/Flickr8k/Flickr8k_Dataset.zip
    https://drive.google.com/file/d/13oJ_9jeylTmW7ivmuNmadwraWceHoQbK/view
  • Flickr_8k_text:数据集文件夹,包含图像的文本文件和字幕。

在制作项目过程中,我们将创建以下文件:

  • Models:将包含我们的训练模型。
  • Descriptions.txt:预处理后的图像名称及其对应的字幕文本文件。
  • Features.p:包含图像及其特征向量的 Pickle 对象,特征向量从预训练的 Xception CNN 模型中提取。
  • Tokenizer.p:包含映射到索引值的 tokens。
  • Model.png:项目的维度可视化图。
  • Testing_caption_generator.py:用于生成任何图像字幕的 Python 文件。
  • Training_caption_generator.ipynb:在其中训练和构建图像字幕生成器的 Jupyter 笔记本。

你可以从以下链接下载所有文件:

  • Image Caption Generator – Python 项目文件:https://github.com/jbrownlee/Datasets/releases/download/Flickr8k/Flickr8k_Dataset.zip
    链接: 源码

在这里插入图片描述
构建基于 Python 的项目
让我们从初始化项目文件夹中的 Jupyter Notebook 服务器开始,只需在控制台中输入 jupyter lab。这将打开一个交互式的 Python 笔记本,在这里你可以运行你的代码。创建一个 Python3 笔记本并命名为 training_caption_generator.ipynb
在这里插入图片描述

1. 导入必要的包

import string
import numpy as np
from PIL import Image
import os
from pickle import dump, load
import numpy as np
from keras.applications.xception import Xception, preprocess_input
from keras.preprocessing.image import load_img, img_to_array
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.utils import to_categorical
from keras.layers.merge import add
from keras.models import Model, load_model
from keras.layers import Input, Dense, LSTM, Embedding, Dropout
# 用于查看循环进度的小库
from tqdm import tqdm_notebook as tqdm
tqdm().pandas()

2. 获取并执行数据清理

包含所有图像描述的主要文本文件是我们的 Flickr_8k_text 文件夹中的 Flickr8k.token

查看文件内容:
在这里插入图片描述

文件的格式是图像和描述之间用换行符(“\n”)隔开。
在这里插入图片描述

每张图像有 5 个描述,我们可以看到每个描述都被分配了一个编号(从 0 到 5)。

我们将定义 5 个函数:

  • load_doc(filename) – 用于加载文档文件并读取文件中的内容到一个字符串中。
  • all_img_captions(filename) – 该函数将创建一个描述字典,将图像与 5 个描述列表映射。描述字典看起来像这样:
  • cleaning_text(descriptions) – 该函数对所有描述执行数据清理。当我们处理文本数据时,这是一个重要的步骤,根据我们的目标,决定要对文本执行哪些类型的数据清理。在我们的例子中,我们将删除标点符号,将所有文本转换为小写,并删除包含数字的单词。所以,一个描述如“一个男人骑在三轮轮椅上”将被转换为“一个男人骑在三轮轮椅上”,但经过清理后为“男人 骑在 三轮 轮椅”。
  • text_vocabulary(descriptions) – 这是一个简单的函数,它将分离所有独特的单词并从所有描述中创建词汇表。
  • save_descriptions(descriptions, filename) – 该函数将创建一个包含所有预处理描述的列表并将其存储到一个文件中。我们将创建一个 descriptions.txt 文件来存储所有的描述。它看起来像这样:
    在这里插入图片描述

代码

# 加载文本文件到内存中
def load_doc(filename):
    # 以只读方式打开文件
    file = open(filename, 'r')
    text = file.read()
    file.close()
    return text

# 获取所有带有描述的图像
def all_img_captions(filename):
    file = load_doc(filename)
    captions = file.split('\n')
    descriptions = {}
    for caption in captions[:-1]:
        img, caption = caption.split('\t')
        if img[:-2] not in descriptions:
            descriptions[img[:-2]] = [caption]
        else:
            descriptions[img[:-2]].append(caption)
    return descriptions

# 数据清理 - 转换为小写,移除标点符号和包含数字的单词
def cleaning_text(captions):
    table = str.maketrans('', '', string.punctuation)
    for img, caps in captions.items():
        for i, img_caption in enumerate(caps):
            img_caption = img_caption.replace("-", " ")
            desc = img_caption.split()
            # 转换为小写
            desc = [word.lower() for word in desc]
            # 从每个标记中移除标点符号
            desc = [word.translate(table) for word in desc]
            # 移除挂起的 's 和 a
            desc = [word for word in desc if len(word) > 1]
            # 移除包含数字的标记
            desc = [word for word in desc if word.isalpha()]
            # 转换回字符串
            img_caption = ' '.join(desc)
            captions[img][i] = img_caption
    return captions

# 生成所有独特单词的词汇表
def text_vocabulary(descriptions):
    vocab = set()
    for key in descriptions.keys():
        [vocab.update(d.split()) for d in descriptions[key]]
    return vocab

# 将所有描述保存在一个文件中
def save_descriptions(descriptions, filename):
    lines = []
    for key, desc_list in descriptions.items():
        for desc in desc_list:
            lines.append(key + '\t' + desc)
    data = "\n".join(lines)
    file = open(filename, "w")
    file.write(data)
    file.close()

# 设置这些路径以匹配你系统中的项目文件夹
dataset_text = "D:\\dataflair projects\\Project - Image Caption Generator\\Flickr_8k_text"
dataset_images = "D:\\dataflair projects\\Project - Image Caption Generator\\Flicker8k_Dataset"

# 准备我们的文本数据
filename = dataset_text + "/" + "Flickr8k.token.txt"
# 加载包含所有数据的文件
# 将它们映射到描述字典中,每张图像对应 5 个描述
descriptions = all_img_captions(filename)
print("描述的数量 =", len(descriptions))
# 清理描述
clean_descriptions = cleaning_text(descriptions)
# 构建词汇表
vocabulary = text_vocabulary(clean_descriptions)
print("词汇表数量 =", len(vocabulary))
# 将每个描述保存到文件中
save_descriptions(clean_descriptions, "descriptions.txt")

3. 提取所有图像的特征向量

这种技术也称为迁移学习,我们不必从头开始做所有事情,而是使用已经在大型数据集上训练过的预训练模型,从这些模型中提取特征并用于我们的任务。我们使用的是在 ImageNet 数据集上训练的 Xception 模型,该数据集包含 1000 个不同的分类。我们可以直接从 keras.applications 导入此模型。确保你已连接到互联网,因为模型权重会自动下载。由于 Xception 模型是专门为 ImageNet 构建的,我们将进行一些小的更改以与我们的模型集成。需要注意的是,Xception 模型的输入图像大小为 2992993。我们将移除最后一层分类层并获取 2048 维特征向量。

model = Xception(include_top=False, pooling='avg')

extract_features() 函数将为所有图像提取特征,并将图像名称与其相应的特征数组映射。然后,我们将特征字典保存到名为 “features.p” 的 Pickle 文件中。

代码

def extract_features(directory):
    model = Xception(include_top=False, pooling='avg')
    features = {}
    for img in tqdm(os.listdir(directory)):
        filename = directory + "/" + img
        image = Image.open(filename)
        image = image.resize((299, 299))
        image = np.expand_dims(image, axis=0)
        image = image / 127.5
        image = image - 1.0
        feature = model.predict(image)
        features[img] = feature
    return features

# 提取 2048 维特征向量
features = extract_features(dataset_images)
dump(features, open("features.p", "wb"))

在这里插入图片描述

这个过程可能需要很长时间,具体取决于你的系统。我使用的是 Nvidia 1050 GPU 进行训练,因此大约需要 7 分钟来完成这个任务。然而,如果你使用的是 CPU,这个过程可能需要 1-2 小时。你可以注释掉上述代码,直接从我们的 Pickle 文件中加载特征。

features = load(open("features.p", "rb"))

4. 加载训练数据集

在我们的 Flickr_8k_test 文件夹中,有一个 Flickr_8k.trainImages.txt 文件,其中包含 6000 个图像名称列表,我们将用于训练。

为了加载训练数据集,我们需要更多函数:

  • load_photos(filename) – 将加载文本文件并返回图像名称列表。
  • load_clean_descriptions(filename, photos) – 该函数将创建一个字典,包含列表中每张照片的描述。我们还为每个描述添加了 <start><end> 标记。我们需要这些标记,以便 LSTM 模型可以识别描述的开始和结束。
  • load_features(photos) – 该函数将返回我们之前从 Xception 模型中提取的图像名称及其特征向量的字典。

代码

# 加载数据
def load_photos(filename):
    file = load_doc(filename)
    photos = file.split("\n")[:-1]
    return photos

def load_clean_descriptions(filename, photos):
    # 加载清理后的描述
    file = load_doc(filename)
    descriptions = {}
    for line in file.split("\n"):
        words = line.split()
        if len(words) < 1:
            continue
        image, image_caption = words[0], words[1:]
        if image in photos:
            if image not in descriptions:
                descriptions[image] = []
            desc = '<start> ' + " ".join(image_caption) + ' <end>'
            descriptions[image].append(desc)
    return descriptions

def load_features(photos):
    # 加载所有特征
    all_features = load(open("features.p", "rb"))
    # 仅选择需要的特征
    features = {k: all_features[k] for k in photos}
    return features

filename = dataset_text + "/" + "Flickr_8k.trainImages.txt"
# train = loading_data(filename)
train_imgs = load_photos(filename)
train_descriptions = load_clean_descriptions("descriptions.txt", train_imgs)
train_features = load_features(train_imgs)

5. 词汇分割

计算机不理解英文单词,对于计算机而言,我们必须要用数字来表示这些单词。因此,我们将词汇表中的每个单词映射到一个唯一的索引值。Keras 库提供了我们一个分词器函数,我们将使用该函数从我们的词汇表中创建分词,并将它们保存到一个名为“tokenizer.p”的pickle文件中。

代码:

# 将字典转换为干净的描述列表
def dict_to_list(descriptions):
    all_desc = []
    for key in descriptions.keys():
        [all_desc.append(d) for d in descriptions[key]]
    return all_desc

# 创建分词器类
# 它将向量化文本语料库
# 每个整数将代表字典中的一个分词
from keras.preprocessing.text import Tokenizer
def create_tokenizer(descriptions):
    desc_list = dict_to_list(descriptions)
    tokenizer = Tokenizer()
    tokenizer.fit_on_texts(desc_list)
    return tokenizer

# 给每个单词分配索引,并将其存储到 tokenizer.p pickle 文件中
tokenizer = create_tokenizer(train_descriptions)
dump(tokenizer, open('tokenizer.p', 'wb'))
vocab_size = len(tokenizer.word_index) + 1
vocab_size

我们的词汇表包含 7577 个单词。

我们计算描述的最大长度。这对于决定模型结构参数非常重要。描述的最大长度为 32。

# 计算描述的最大长度
def max_length(descriptions):
    desc_list = dict_to_list(descriptions)
    return max(len(d.split()) for d in desc_list)
    
max_length = max_length(descriptions)
max_length

6. 创建数据生成器

让我们先看看模型的输入和输出将会是什么样子。为了将此任务转化为监督学习任务,我们必须为模型提供训练用的输入和输出。我们必须使用 6000 张图片来训练模型,每张图片将包含一个长度为 2048 的特征向量,而描述也被表示为数字。对于 6000 张图片的这些数据量,我们无法完全保存在内存中,因此我们将使用一个生成器方法来提供批量数据。

生成器将提供输入和输出序列。

例如:

我们的模型的输入是 [x1, x2],输出是 y,其中 x1 是该图片的 2048 特征向量,x2 是输入文本序列,而 y 是模型要预测的输出文本序列。

x1(特征向量)x2(文本序列)y(待预测的词)
featurestart,two
featurestart, twodogs
featurestart, two, dogsdrink
featurestart, two, dogs, drinkwater
featurestart, two, dogs, drink, waterend
# 从图像描述中创建输入-输出序列对
# 用于 model.fit_generator() 的数据生成器
def data_generator(descriptions, features, tokenizer, max_length):
    while 1:
        for key, description_list in descriptions.items():
            # 获取图片特征
            feature = features[key][0]
            input_image, input_sequence, output_word = create_sequences(tokenizer, max_length, description_list, feature)
            yield [[input_image, input_sequence], output_word]

def create_sequences(tokenizer, max_length, desc_list, feature):
    X1, X2, y = list(), list(), list()
    # 遍历每张图片的每个描述
    for desc in desc_list:
        # 对序列进行编码
        seq = tokenizer.texts_to_sequences([desc])[0]
        # 将一个序列拆分为多个 X,y 对
        for i in range(1, len(seq)):
            # 拆分为输入和输出对
            in_seq, out_seq = seq[:i], seq[i]
            # 对输入序列进行填充
            in_seq = pad_sequences([in_seq], maxlen=max_length)[0]
            # 对输出序列进行编码
            out_seq = to_categorical([out_seq], num_classes=vocab_size)[0]
            # 存储
            X1.append(feature)
            X2.append(in_seq)
            y.append(out_seq)
    return np.array(X1), np.array(X2), np.array(y)

# 您可以检查模型输入和输出的形状
[a,b],c = next(data_generator(train_descriptions, features, tokenizer, max_length))
a.shape, b.shape, c.shape
# ((47, 2048), (47, 32), (47, 7577))

7. 定义 CNN-RNN 模型

为了定义模型的结构,我们将使用 Keras 的 Functional API 中的 Model。它将包括三个主要部分:

  1. 特征提取器: 从图像中提取的特征大小为 2048,通过一个密集层,我们将维度减少到 256 个节点。
  2. 序列处理器: 一个嵌入层将处理文本输入,后面跟随着 LSTM 层。
  3. 解码器: 通过合并上述两个层的输出,我们使用另一个密集层来做出最终的预测。最终层的节点数将等于我们的词汇表大小。

下面是最终模型的视觉表示:
在这里插入图片描述

from keras.utils import plot_model
# 定义描述模型
def define_model(vocab_size, max_length):
    # CNN 模型中提取的特征从 2048 减少到 256 个节点
    inputs1 = Input(shape=(2048,))
    fe1 = Dropout(0.5)(inputs1)
    fe2 = Dense(256, activation='relu')(fe1)
    # LSTM 序列模型
    inputs2 = Input(shape=(max_length,))
    se1 = Embedding(vocab_size, 256, mask_zero=True)(inputs2)
    se2 = Dropout(0.5)(se1)
    se3 = LSTM(256)(se2)
    # 合并两个模型
    decoder1 = add([fe2, se3])
    decoder2 = Dense(256, activation='relu')(decoder1)
    outputs = Dense(vocab_size, activation='softmax')(decoder2)
    # 组合模型 [image, seq] [word]
    model = Model(inputs=[inputs1, inputs2], outputs=outputs)
    model.compile(loss='categorical_crossentropy', optimizer='adam')
    # 模型概要
    print(model.summary())
    plot_model(model, to_file='model.png', show_shapes=True)
    return model

8. 训练模型

为了训练模型,我们将使用 6000 张训练图片,通过生成器方法批量生成输入和输出序列,并使用 model.fit_generator() 方法将这些序列拟合到模型中。我们还会将模型保存到模型目录中。这将根据您的系统能力花费一些时间。

# 训练我们的模型
print('Dataset: ', len(train_imgs))
print('Descriptions: train=', len(train_descriptions))
print('Photos: train=', len(train_features))
print('Vocabulary Size:', vocab_size)
print('Description Length: ', max_length)
model = define_model(vocab_size, max_length)
epochs = 10
steps = len(train_descriptions)
# 创建一个目录 models 以保存我们的模型
os.mkdir("models")
for i in range(epochs):
    generator = data_generator(train_descriptions, train_features, tokenizer, max_length)
    model.fit_generator(generator, epochs=1, steps_per_epoch= steps, verbose=1)
    model.save("models/model_" + str(i) + ".h5")

9. 测试模型

模型已经训练完毕,现在我们将创建一个单独的文件 testing_caption_generator.py,该文件将加载模型并生成预测。预测包含了索引值的最大长度,因此我们将使用同一个 tokenizer.p 的pickle文件来从其索引值中获取单词。

代码:

import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import argparse
ap = argparse.ArgumentParser()
ap.add_argument('-i', '--image', required=True, help="Image Path")
args = vars(ap.parse_args())
img_path = args['image']

def extract_features(filename, model):
    try:
        image = Image.open(filename)
    except:
        print("ERROR: Couldn't open image! Make sure the image path and extension is correct")
    image = image.resize((299,299))
    image = np.array(image)
    # 对于有 4 个通道的图片,我们将它们转换为 3 个通道
    if image.shape[2] == 4: 
        image = image[..., :3]
    image = np.expand_dims(image, axis=0)
    image = image/127.5
    image = image - 1.0
    feature = model.predict(image)
    return feature

def word_for_id(integer, tokenizer):
    for word, index in tokenizer.word_index.items():
        if index == integer:
            return word
    return None

def generate_desc(model, tokenizer, photo, max_length):
    in_text = 'start'
    for i in range(max_length):
        sequence = tokenizer.texts_to_sequences([in_text])[0]
        sequence = pad_sequences([sequence], maxlen=max_length)
        pred = model.predict([photo,sequence], verbose=0)
        pred = np.argmax(pred)
        word = word_for_id(pred, tokenizer)
        if word is None:
            break
        in_text += ' ' + word
        if word == 'end':
            break
    return in_text

# 示例路径
max_length = 32
tokenizer = load(open("tokenizer.p","rb"))
model = load_model('models/model_9.h5')
xception_model = Xception(include_top=False, pooling="avg")
photo = extract_features(img_path, xception_model)
img = Image.open(img_path)
description = generate_desc(model, tokenizer, photo, max_length)
print("\n\n")
print(description)
plt.imshow(img)

结果:

  • 图像标题生成器 - 一个站在岩石上的男人
    在这里插入图片描述

  • 图像标题生成器 - 玩耍的女孩
    在这里插入图片描述

  • Python 项目图像标题生成器 - 一个坐在皮艇上的男人
    在这里插入图片描述

总结

在这个高级的 Python 项目中,我们实现了一个 CNN-RNN 模型来构建图像标题生成器。需要注意的是,我们的模型依赖于数据,因此它无法预测超出其词汇表之外的单词。我们使用了一个包含 8000 张图片的小数据集。为了生产级别的模型,我们需要在超过 100,000 张图片的大型数据集上进行训练,这可以生成更准确的模型。

参考资料
资料名称链接
Keras 中文文档https://keras.io/zh/
TensorFlow 中文文档https://tensorflow.google.cn/guide
PyTorch 文档https://pytorch.org/docs/
词嵌入向量化https://www.zhuanzhi.ai/document/6f82d2a48a7a4c0329f69a2d9d6a3ac0
词嵌入词典https://www.jianshu.com/p/9b470a4ccf96
LSTM 网络简介https://towardsdatascience.com/understanding-lstm-and-its-variants-c7b3c3be2b9f
LSTM 中文教程https://www.jiqizhixin.com/articles/2019-04-27-18
图像标题生成综述https://arxiv.org/pdf/1908.09251.pdf
图像标题生成器研究http://gr.xjtu.edu.cn/web/submission…
NLP 和图像标题生成https://www.cnblogs.com/grandyang/p/8513091.html
图像特征提取教程https://developer.nvidia.com/blog/image-feature-extraction-with-deep-learning/
图像特征提取技术http://blog.csdn.net/u012162613/article/details/43639031
使用 Keras 构建图像标题生成模型https://machinelearningmastery.com/develop-a-deep-learning-caption-generation-model-in-python/
Deep Learning with Pythonhttps://www.manning.com/books/deep-learning-with-python-second-edition
Python 中的 LSTM 图像标题生成https://www.datacamp.com/community/tutorials/lstm-python-stock-market
posted @   爱上编程技术  阅读(14)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示