25、图像字幕生成项目实战

图像字幕生成项目实战

1. 项目概述

在图像字幕生成网络模型的创建中,合适的训练和测试数据至关重要。有多个公开数据集可供选择:
| 数据集名称 | 图像数量 |
| ---- | ---- |
| Flickr8k | 约8000张 |
| Flickr30k | 约30000张 |
| MS COCO | 约180000张 |

对于学习目的而言,Flickr8k数据集已足够,该数据集中每张图像配有5条相关字幕。

2. 创建项目

首先,打开一个新的Colab项目并将其重命名为ImageCaptioning,然后导入所需的库:

import os
import time
import pickle
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from sklearn.utils import shuffle
from sklearn.model_selection import train_test_split
from tensorflow.keras.applications import InceptionV3
from os import listdir
from tqdm import tqdm
from PIL import Image
3. 下载数据

此项目需要下载两种数据库:图像及其对应的字幕。
- 下载字幕数据

!wget --no-check-certificate -r  'https://drive.google.com/uc? export=download&id=1c7yGTpizf5egVD9dc3Q2lrxS8wtOAV42' -O Flickr8k_text.zip
!mkdir captions images
!unzip 'Flickr8k_text.zip' -d '/content/captions'
  • 下载图像数据
!wget --no-check-certificate -r 'https://drive.google.com/uc? export=download&id=1126G_E2OpvULyvTm0Kz_oMhOzv8CkiW1' -O Flickr8k_Dataset.zip
!unzip 'Flickr8k_Dataset.zip' -d '/content/images'

可以使用以下代码检查数据库中的图像数量:

image_dir = '/content/images/Flicker8k_Dataset'
images = listdir(image_dir)
print("The number of jpg flies in Flicker8k: {}".format(len(images)))

运行结果显示数据库中有8091张图像,足以用于实验和学习。

4. 解析令牌文件

接下来,解析令牌文件以创建图像名称和对应字幕的列表。为了缩短训练时间,这里每张图像仅使用一条字幕。
- 加载数据

# load doc into memory
def load(filename):
    file = open(filename, 'r')
    text = file.read()
    file.close()
    return text

filename = '/content/captions/Flickr8k.token.txt'
doc = load(filename)
  • 创建迭代器
dirs = listdir('/content/images/Flicker8k_Dataset')
dirs[:5]

输出示例:

['3583065748_7d149a865c.jpg',
 '3358621566_12bac2e9d2.jpg',
 '509778093_21236bb64d.jpg',
 '2094323311_27d58b1513.jpg',
 '3314180199_2121e80368.jpg']
  • 创建列表
def load_small(doc):
    PATH = '/content/images/Flicker8k_Dataset/'
    img_path = []
    img_id = []
    img_cap = []
    for line in doc.split('\n'):
        tokens = line.split()
        if len(line) < 2:
            continue
        image_id , image_desc = tokens[0] , tokens[1:]
        image_id = image_id.split('.')[0]
        image_id = image_id + '.jpg'
        image_desc = ' '.join(image_desc)
        if image_id not in img_id:
            if len(img_id) <= 8000:
                img_id.append(image_id)
                image_path = PATH + image_id
                image_desc = '<start> ' + image_desc + ' <end>'
                if image_id in dirs:
                    img_path.append(image_path)
                    img_cap.append(image_desc)
            else:
                continue
    return img_path , img_cap

all_image_path , all_image_captions = load_small(doc)
print('Number of images: ', len(all_image_path))
all_image_path[:5]
print('Number of captions: ', len(all_image_captions))
all_image_captions[:5]

输出结果显示有8000张图像和8000条字幕,且每条字幕都添加了 <start> <end> 标签。
- 打乱训练数据

train_captions, img_name_vector = shuffle(all_image_captions, all_image_path, random_state=1)
5. 加载InceptionV3模型

使用InceptionV3模型进行图像特征提取,加载模型的代码如下:

image_model = InceptionV3(include_top=False, weights='imagenet')

这里使用预训练的权重,并且去除了用于图像分类的顶层。接下来创建自己的 tf.keras 模型用于提取图像特征:

new_input = image_model.input
hidden_layer = image_model.layers[-1].output
image_features_extract_model = tf.keras.Model(new_input, hidden_layer)

该输出层的形状为8x8x2048,是InceptionV3模型的最后一个卷积层。

6. 准备数据集

InceptionV3模型要求图像大小为299x299,并且图像像素值需归一化到 -1 到 1 的范围。
- 加载和调整图像大小的函数

def load_image(image_path):
    img = tf.io.read_file(image_path)
    img = tf.image.decode_jpeg(img, channels=3)
    img = tf.image.resize(img, (299, 299))
    img = tf.keras.applications.inception_v3.preprocess_input(img)
    return img, image_path
  • 创建图像数据集
encode_train = sorted(set(img_name_vector))
image_dataset = tf.data.Dataset.from_tensor_slices(encode_train)
image_dataset = image_dataset.map(load_image, num_parallel_calls=tf.data.experimental.AUTOTUNE).batch(16)
7. 提取特征

对于数据集中的每张图像,调用之前创建的 image_features_extract_model 提取特征,然后将数据重塑并保存到物理文件中:

for img, path in tqdm(image_dataset):
    batch_features = image_features_extract_model(img)
    batch_features = tf.reshape(batch_features, (batch_features.shape[0], -1, batch_features.shape[3]))
    for bf, p in zip(batch_features, path):
        path_of_feature = p.numpy().decode("utf-8")
        np.save(path_of_feature, bf.numpy())

虽然将特征保存到内存更高效,但会消耗大量资源。在GPU上运行上述循环大约需要2分钟。

8. 创建词汇表

创建所有独特单词的词汇表:

tokenizer = tf.keras.preprocessing.text.Tokenizer(filters='!"#$%&()*+.,-/:;=?@[\]^_`{|}~ ')
tokenizer.fit_on_texts(train_captions)
max_size = len(tokenizer.word_index)
9. 创建输入序列

使用以下代码创建分词后单词的输入序列:

train_seqs = tokenizer.texts_to_sequences(train_captions)
train_seqs[:5]

输出示例:

[[2, 1, 2339, 8, 155, 2340, 1198, 19, 2341, 1390, 24, 480, 554, 3],
 [2, 21, 1714, 7, 1199, 1715, 1, 108, 2342, 19, 5, 173, 3],
 [2, 1, 11, 4, 1, 28, 32, 506, 1, 507, 3],
 [2, 1, 101, 102, 12, 1, 26, 3],
 [2, 63, 34, 4, 1, 272, 3]]

由于这些序列长度不同,需要对其进行填充:

max_length = max(len(t) for t in train_seqs)
cap_vector = tf.keras.preprocessing.sequence.pad_sequences(train_seqs, padding='post')
cap_vector[:5]

填充后所有分词后的单词序列长度相等。

10. 创建训练数据集

声明一些变量用于创建数据集:

BATCH_SIZE = 64
BUFFER_SIZE = 1000
embedding_dim = 256
units = 512
vocab_size = max_size + 1
num_steps = len(img_name_vector) // BATCH_SIZE

定义加载图像特征向量的函数:

def map_func(img_name, cap):
    img_tensor = np.load(img_name.decode('utf-8')+'.npy')
    return img_tensor, cap

创建数据集的函数:

def create_dataset(img_name_train,caption_train):
    dataset = tf.data.Dataset.from_tensor_slices((img_name_train, caption_train))
    dataset = dataset.map(lambda item1, item2: tf.numpy_function(map_func, [item1, item2], [tf.float32, tf.int32]), num_parallel_calls=tf.data.experimental.AUTOTUNE)
    dataset = dataset.shuffle(BUFFER_SIZE).batch(BATCH_SIZE).prefetch(buffer_size=tf.data.experimental.AUTOTUNE)
    return dataset

dataset = create_dataset(img_name_vector,cap_vector)
11. 创建模型

创建带有Bahdanau注意力和门控循环单元(GRU)的序列到序列模型。GRU在RNN中提供了门控机制,与LSTM类似,但参数更少,训练速度更快,内存使用更少,但在处理长序列时准确性不如LSTM。

Bahdanau注意力的工作流程如下:
1. 为给定输入图像生成编码器隐藏状态。
2. 计算对齐分数,即每个先前编码器隐藏状态与先前解码器隐藏状态之间的分数。
3. 对对齐分数进行Softmax操作。
4. 计算上下文向量。
5. 解码输出。
6. 重复步骤2到5,直到遇到结束标记。

以下是mermaid格式的流程图展示Bahdanau注意力的工作流程:

graph LR
    A[输入图像] --> B[生成编码器隐藏状态]
    B --> C[计算对齐分数]
    C --> D[Softmax对齐分数]
    D --> E[计算上下文向量]
    E --> F[解码输出]
    F --> G{是否遇到结束标记}
    G -- 否 --> C
    G -- 是 --> H[结束]
12. 创建编码器

编码器将提取的特征作为输入,并将其传递到全连接层:

class Inception_Encoder(tf.keras.Model):
    def __init__(self, embedding_dim):
        super(Inception_Encoder, self).__init__()
        self.fc = tf.keras.layers.Dense(embedding_dim)

    def call(self, x):
        x = self.fc(x)
        x = tf.nn.relu(x)
        return x
13. 创建解码器

解码器是此应用中最重要的部分,使用Bahdanau注意力机制。

一般来说,有两种注意力机制:
1. Bahdanau注意力 - 确定性“软”注意力
2. Luong注意力 - 随机“硬”注意力

这里使用的是Bahdanau注意力,该机制计算每个输入向量(即图像的提取特征)的注意力权重和上下文向量。上下文向量是图像输入在时间t时相关部分的动态表示。

解码器的功能可以总结为以下三个简单步骤:
1. 上下文向量(注意力权重和编码器输出的加权乘法)
2. 前一个时间步的解码器输出
3. 前一个解码器的隐藏状态

解码器的初始化代码如下:

class RNN_Decoder(tf.keras.Model):
    def __init__(self, units):
        super(RNN_Decoder, self).__init__()
        self.gru = tf.keras.layers.GRU(units, return_sequences=True, return_state=True, recurrent_initializer='glorot_uniform')
        self.batchnormalization = tf.keras.layers.BatchNormalization(axis=-1, momentum=0.99, epsilon=0.001, center=True, scale=True, beta_initializer='zeros', gamma_initializer='ones', moving_mean_initializer='zeros', moving_variance_initializer='ones', beta_regularizer=None, gamma_regularizer=None, beta_constraint=None, gamma_constraint=None)
        self.W1 = tf.keras.layers.Dense(units)
        self.W2 = tf.keras.layers.Dense(units)
        self.V = tf.keras.layers.Dense(1)

    def call(self, x, features, hidden):
        hidden_with_time_axis = tf.expand_dims(hidden, 1)
        # 计算注意力分数
        score = self.V(tf.nn.tanh(self.W1(features) + self.W2(hidden_with_time_axis)))
        attention_weights = tf.nn.softmax(score, axis=1)
        context_vector = attention_weights * features
        context_vector = tf.reduce_sum(context_vector, axis=1)
        x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)
        output, state = self.gru(x)
        output = tf.reshape(output, (-1, output.shape[2]))
        x = self.fc(output)
        return x, state, attention_weights

通过以上步骤,我们完成了图像字幕生成项目的整个流程,从数据准备到模型创建,为后续的训练和预测奠定了基础。

图像字幕生成项目实战

14. 解码器调用方法

解码器需要三个输入:
1. 编码器输出
2. 隐藏状态(初始化为 0)
3. 解码器输入(即起始标记)

调用方法的代码如下:

def call(self, x, features, hidden):
    hidden_with_time_axis = tf.expand_dims(hidden, 1)
    # 注意力分数计算公式
    score = self.V(tf.nn.tanh(self.W1(features) + self.W2(hidden_with_time_axis)))
    attention_weights = tf.nn.softmax(score, axis = 1)
    context_vector = attention_weights * features
    context_vector = tf.reduce_sum(context_vector, axis = 1)
    x = tf.concat([tf.expand_dims(context_vector, 1), x], axis = -1)
    output, state = self.gru(x)
    output = tf.reshape(output, (-1, output.shape[2]))
    x = self.fc(output)
    return x, state, attention_weights

以下是对解码器调用过程的详细解释:
|步骤|操作|说明|
| ---- | ---- | ---- |
|1| hidden_with_time_axis = tf.expand_dims(hidden, 1) |改变前一个解码器隐藏状态的形状,增加一个维度|
|2| score = self.V(tf.nn.tanh(self.W1(features) + self.W2(hidden_with_time_axis))) |计算注意力分数|
|3| attention_weights = tf.nn.softmax(score, axis = 1) |对注意力分数进行 Softmax 操作,得到注意力权重|
|4| context_vector = attention_weights * features |计算上下文向量|
|5| context_vector = tf.reduce_sum(context_vector, axis = 1) |对上下文向量进行求和|
|6| x = tf.concat([tf.expand_dims(context_vector, 1), x], axis = -1) |将上下文向量和输入进行拼接|
|7| output, state = self.gru(x) |通过 GRU 层得到输出和状态|
|8| output = tf.reshape(output, (-1, output.shape[2])) |重塑输出形状|
|9| x = self.fc(output) |通过全连接层得到最终输出|

15. 模型训练

在完成模型的创建后,接下来进行模型的训练。
- 定义优化器和损失函数

optimizer = tf.keras.optimizers.Adam()
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True, reduction='none')

def loss_function(real, pred):
    mask = tf.math.logical_not(tf.math.equal(real, 0))
    loss_ = loss_object(real, pred)
    mask = tf.cast(mask, dtype=loss_.dtype)
    loss_ *= mask
    return tf.reduce_mean(loss_)
  • 训练步骤
@tf.function
def train_step(img_tensor, target):
    loss = 0
    hidden = decoder.reset_state(batch_size=target.shape[0])
    dec_input = tf.expand_dims([tokenizer.word_index['<start>']] * target.shape[0], 1)
    with tf.GradientTape() as tape:
        features = encoder(img_tensor)
        for i in range(1, target.shape[1]):
            predictions, hidden, _ = decoder(dec_input, features, hidden)
            loss += loss_function(target[:, i], predictions)
            dec_input = tf.expand_dims(target[:, i], 1)
    total_loss = (loss / int(target.shape[1]))
    trainable_variables = encoder.trainable_variables + decoder.trainable_variables
    gradients = tape.gradient(loss, trainable_variables)
    optimizer.apply_gradients(zip(gradients, trainable_variables))
    return loss, total_loss

训练步骤的流程图如下:

graph LR
    A[输入图像张量和目标字幕] --> B[初始化损失和隐藏状态]
    B --> C[获取编码器特征]
    C --> D{是否遍历完目标字幕}
    D -- 否 --> E[解码器预测]
    E --> F[计算损失]
    F --> G[更新解码器输入]
    G --> D
    D -- 是 --> H[计算总损失]
    H --> I[计算梯度]
    I --> J[应用梯度更新参数]
    J --> K[返回损失和总损失]
  • 训练循环
EPOCHS = 20
for epoch in range(EPOCHS):
    start = time.time()
    total_loss = 0
    for (batch, (img_tensor, target)) in enumerate(dataset):
        batch_loss, t_loss = train_step(img_tensor, target)
        total_loss += t_loss
        if batch % 100 == 0:
            print(f'Epoch {epoch+1} Batch {batch} Loss {batch_loss.numpy():.4f}')
    print(f'Epoch {epoch+1} Loss {total_loss/num_steps:.4f}')
    print(f'Time taken for 1 epoch {time.time()-start:.2f} sec\n')
16. 模型评估

训练完成后,需要对模型进行评估。这里我们通过生成图像字幕并与真实字幕进行对比来评估模型的性能。

def evaluate(image):
    attention_plot = np.zeros((max_length, 64))
    hidden = decoder.reset_state(batch_size = 1)
    temp_input = tf.expand_dims(load_image(image)[0], 0)
    img_tensor_val = image_features_extract_model(temp_input)
    img_tensor_val = tf.reshape(img_tensor_val, (img_tensor_val.shape[0], -1, img_tensor_val.shape[3]))
    features = encoder(img_tensor_val)
    dec_input = tf.expand_dims([tokenizer.word_index['<start>']], 0)
    result = []
    for i in range(max_length):
        predictions, hidden, attention_weights = decoder(dec_input, features, hidden)
        attention_plot[i] = tf.reshape(attention_weights, (-1, )).numpy()
        predicted_id = tf.random.categorical(predictions, 1)[0][0].numpy()
        result.append(tokenizer.index_word[predicted_id])
        if tokenizer.index_word[predicted_id] == '<end>':
            return result, attention_plot
        dec_input = tf.expand_dims([predicted_id], 0)
    attention_plot = attention_plot[:len(result), :]
    return result, attention_plot

评估步骤如下:
1. 初始化注意力图和隐藏状态。
2. 提取图像特征。
3. 通过编码器得到特征表示。
4. 从起始标记开始,逐步生成字幕。
5. 当遇到结束标记时,停止生成。

17. 结果展示

我们可以选择一些图像进行测试,并展示生成的字幕。

# 选择一张图像进行测试
image_path = '/content/images/Flicker8k_Dataset/1000268201_693b08cb0e.jpg'
result, attention_plot = evaluate(image_path)
print('Prediction Caption:', ' '.join(result))
# 显示图像
img = Image.open(image_path)
plt.imshow(img)
18. 总结

通过本次图像字幕生成项目,我们完成了从数据准备、模型创建到训练和评估的整个流程。具体步骤总结如下:
1. 数据准备 :下载并解析图像和字幕数据,创建图像路径和字幕列表。
2. 特征提取 :使用 InceptionV3 模型提取图像特征,并保存到文件中。
3. 词汇表和序列创建 :创建词汇表,将字幕转换为输入序列并进行填充。
4. 模型创建 :创建带有 Bahdanau 注意力和 GRU 的序列到序列模型,包括编码器和解码器。
5. 模型训练 :定义优化器和损失函数,进行模型训练。
6. 模型评估 :编写评估函数,对模型进行评估并展示结果。

通过不断调整模型参数和训练策略,可以进一步提高模型的性能,生成更准确、更自然的图像字幕。

以上就是整个图像字幕生成项目的详细过程,希望对大家有所帮助。

关于 阿里云盘CLI。仿 Linux shell 文件处理命令的阿里云盘命令行客户端,支持JavaScript插件,支持同步备份功能,支持相册批量下载。 特色 多平台支持, 支持 Windows, macOS, linux(x86/x64/arm), android, iOS 等 阿里云盘多用户支持 支持备份盘,资源库无缝切换 下载网盘内文件, 支持多个文件或目录下载, 支持断点续传和单文件并行下载。支持软链接(符号链接)文件。 上传本地文件, 支持多个文件或目录上传,支持排除指定文件夹/文件(正则表达式)功能。支持软链接(符号链接)文件。 同步备份功能支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份保持本地文件和网盘文件同步。常用于嵌入式或者NAS等设备,支持docker镜像部署。 命令和文件路径输入支持Tab键自动补全,路径支持通配符匹配模式 支持JavaScript插件,你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求 支持共享相册的相关操作,支持批量下载相册所有普通照片、实况照片文件到本地 支持多用户联合下载功能,对下载速度有极致追求的用户可以尝试使用该选项。详情请查看文档多用户联合下载 如果大家有打算开通阿里云盘VIP会员,可以使用阿里云盘APP扫描下面的优惠推荐码进行开通。 注意:您需要开通【三方应用权益包】,这样使用本程序下载才能加速,否则下载无法提速。 Windows不第二步打开aliyunpan命令行程序,任何云盘命令都有类似如下日志输出 如何登出和下线客户端 阿里云盘单账户最多只允许同时登录 10 台设备 当出现这个提示:你账号已超出最大登录设备数量,请先下线一台设备,然后重启本应用,才可以继续使用 说明你的账号登录客户端已经超过数量,你需要先登出其他客户端才能继续使用,如下所示
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值