图像描述生成是一个具有挑战性的人工智能问题,需要为照片生成文本描述。
它需要计算机视觉方法来理解图像内容,还需要自然语言处理领域的语言模型将图像理解转化为正确顺序的词语。最近,深度学习方法在此类问题上取得了最先进的成果。
开发自己的图像描述生成模型可能很困难,主要是因为数据集和模型都非常庞大,需要数天才能训练完成。一种替代方法是使用完整数据集的一小部分样本来探索模型配置。
在本教程中,您将了解如何使用标准照片描述数据集的一小部分样本来探索不同的深度模型设计。
完成本教程后,您将了解:
- 如何为照片描述建模准备数据。
- 如何设计基线和测试工具来评估模型的技能并控制其随机性。
- 如何评估模型技能、特征提取模型和词嵌入等属性,以提升模型技能。
通过我的新书《深度学习自然语言处理》**启动您的项目**,其中包括所有示例的**分步教程**和 **Python 源代码**文件。
让我们开始吧。
- 2019 年 4 月/2 月:提供了 Flickr8k_Dataset 数据集的直接链接,因为官方网站已下线。

如何在 Keras 中使用小实验开发图像标题生成模型
照片由 Per 拍摄,保留部分权利。
教程概述
本教程分为6个部分;它们是
- 数据准备
- 基线描述生成模型
- 网络大小参数
- 配置特征提取模型
- 词嵌入模型
- 结果分析
Python 环境
本教程假设您已安装 Python SciPy 环境,最好是 Python 3。
您必须安装 Keras(2.0 或更高版本),并使用 TensorFlow 或 Theano 后端。
本教程还假设您已安装 scikit-learn、Pandas、NumPy 和 Matplotlib。
如果您需要环境方面的帮助,请参阅本教程
我建议在具有 GPU 的系统上运行代码。
您可以在 Amazon Web Services 上廉价访问 GPU。在本教程中了解如何操作
让我们开始吧。
需要深度学习处理文本数据的帮助吗?
立即参加我的免费7天电子邮件速成课程(附代码)。
点击注册,同时获得该课程的免费PDF电子书版本。
数据准备
首先,我们需要准备数据集以训练模型。
我们将使用 Flickr8K 数据集,它包含 8,000 多张照片及其描述。
您可以从此处下载数据集
更新(2019 年 4 月):官方网站似乎已下线(尽管表格仍然有效)。以下是我 GitHub 存储库中一些直接下载链接
将照片和描述分别解压缩到当前工作目录的 `Flicker8k_Dataset` 和 `Flickr8k_text` 目录中。
数据准备分为两个部分:
- 准备文本
- 准备照片
准备文本
数据集包含每张照片的多个描述,并且描述文本需要进行一些基本的清理。
首先,我们将加载包含所有描述的文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 加载文档到内存 def load_doc(filename): # 以只读方式打开文件 file = open(filename, 'r') # 读取所有文本 text = file.read() # 关闭文件 file.close() return text filename = 'Flickr8k_text/Flickr8k.token.txt' # 加载描述 doc = load_doc(filename) |
每张照片都有一个唯一的标识符。此标识符用于照片文件名和描述文本文件中。接下来,我们将遍历照片描述列表,并保存每张照片的第一个描述。下面定义了一个名为 `load_descriptions()` 的函数,该函数在给定已加载文档文本的情况下,将返回一个从照片标识符到描述的字典。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# 提取图像描述 def load_descriptions(doc): mapping = dict() # 处理行 for line in doc.split('\n'): # 按空格分割行 tokens = line.split() if len(line) < 2: continue # 将第一个标记作为图像 id,其余作为描述 image_id, image_desc = tokens[0], tokens[1:] # 从图像 id 中删除文件名 image_id = image_id.split('.')[0] # 将描述标记转换回字符串 image_desc = ' '.join(image_desc) # store the first description for each image if image_id not in mapping: mapping[image_id] = image_desc return mapping # 解析描述 descriptions = load_descriptions(doc) print('Loaded: %d ' % len(descriptions)) |
接下来,我们需要清理描述文本。
描述已被分词,易于处理。我们将以下列方式清理文本,以减少我们需要处理的词汇量:
- 将所有单词转换为小写。
- 删除所有标点符号。
- 删除所有长度为一个字符或更短的单词(例如“a”)。
下面定义了 `clean_descriptions()` 函数,该函数给定图像标识符到描述的字典,遍历每个描述并清理文本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import string def clean_descriptions(descriptions): # 准备用于删除标点符号的转换表 table = str.maketrans('', '', string.punctuation) for key, desc in descriptions.items(): # 分词 desc = desc.split() # 转换为小写 desc = [word.lower() for word in desc] # 从每个标记中删除标点符号 desc = [w.translate(table) for w in desc] # 删除悬挂的“s”和“a” desc = [word for word in desc if len(word)>1] # 存储为字符串 descriptions[key] = ' '.join(desc) # 清理描述 clean_descriptions(descriptions) # 总结词汇表 all_tokens = ' '.join(descriptions.values()).split() vocabulary = set(all_tokens) print('Vocabulary Size: %d' % len(vocabulary)) |
最后,我们将图像标识符和描述的字典保存到一个名为 `descriptions.txt` 的新文件中,每行一个图像标识符和描述。
下面定义了 `save_doc()` 函数,该函数给定包含标识符到描述映射的字典和文件名,将映射保存到文件。
1 2 3 4 5 6 7 8 9 10 11 12 |
# 将描述保存到文件,每行一个 def save_doc(descriptions, filename): lines = list() for key, desc in descriptions.items(): lines.append(key + ' ' + desc) data = '\n'.join(lines) file = open(filename, 'w') file = file.write(data) file.close() # 保存描述 save_doc(descriptions, 'descriptions.txt') |
综合以上所有代码,完整列表如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
import string # 加载文档到内存 def load_doc(filename): # 以只读方式打开文件 file = open(filename, 'r') # 读取所有文本 text = file.read() # 关闭文件 file.close() return text # 提取图像描述 def load_descriptions(doc): mapping = dict() # 处理行 for line in doc.split('\n'): # 按空格分割行 tokens = line.split() if len(line) < 2: continue # 将第一个标记作为图像 id,其余作为描述 image_id, image_desc = tokens[0], tokens[1:] # 从图像 id 中删除文件名 image_id = image_id.split('.')[0] # 将描述标记转换回字符串 image_desc = ' '.join(image_desc) # store the first description for each image if image_id not in mapping: mapping[image_id] = image_desc return mapping def clean_descriptions(descriptions): # 准备用于删除标点符号的转换表 table = str.maketrans('', '', string.punctuation) for key, desc in descriptions.items(): # 分词 desc = desc.split() # 转换为小写 desc = [word.lower() for word in desc] # 从每个标记中删除标点符号 desc = [w.translate(table) for w in desc] # 删除悬挂的“s”和“a” desc = [word for word in desc if len(word)>1] # 存储为字符串 descriptions[key] = ' '.join(desc) # 将描述保存到文件,每行一个 def save_doc(descriptions, filename): lines = list() for key, desc in descriptions.items(): lines.append(key + ' ' + desc) data = '\n'.join(lines) file = open(filename, 'w') file = file.write(data) file.close() filename = 'Flickr8k_text/Flickr8k.token.txt' # 加载描述 doc = load_doc(filename) # 解析描述 descriptions = load_descriptions(doc) print('Loaded: %d ' % len(descriptions)) # 清理描述 clean_descriptions(descriptions) # 总结词汇表 all_tokens = ' '.join(descriptions.values()).split() vocabulary = set(all_tokens) print('Vocabulary Size: %d' % len(vocabulary)) # 保存描述 save_doc(descriptions, 'descriptions.txt') |
运行示例后,首先会打印加载的照片描述数量 (8,092) 和清理后的词汇表大小 (4,484 个单词)。
1 2 |
已加载: 8092 词汇表大小: 4484 |
清理后的描述随后被写入“`descriptions.txt`”文件。查看文件,我们可以看到描述已准备好进行建模。
查看文件,我们可以看到描述已准备好进行建模。
1 2 3 4 5 6 |
3621647714_fc67ab2617 男人站在雪地里,周围是树木和山脉 365128300_6966058139 一群人在河流急流中漂流 2751694538_fffa3d307d 男人和男孩坐在驾驶座上 537628742_146f2c24f8 小女孩在田野里奔跑 2320125735_27fe729948 一只戴蓝色项圈的黑棕色狗在草地上的足球旁警惕起来 ... |
准备照片
我们将使用预训练模型来解释照片内容。
有许多模型可供选择。在这种情况下,我们将使用牛津视觉几何组 (Oxford Visual Geometry Group) 或 VGG 模型,该模型赢得了 2014 年 ImageNet 竞赛。在此处了解更多有关该模型的信息
Keras 直接提供了这个预训练模型。请注意,第一次使用此模型时,Keras 将从互联网下载模型权重,大约 500 兆字节。这可能需要几分钟,具体取决于您的互联网连接速度。
我们可以将此模型作为更广泛的图像描述模型的一部分。问题是,它是一个大型模型,每次我们想要测试新的语言模型配置(下游)时,都要让每张照片通过网络,这是多余的。
相反,我们可以使用预训练模型预先计算“照片特征”并将其保存到文件中。然后,我们可以在以后加载这些特征,并将它们作为数据集中给定照片的解释输入到我们的模型中。这与将照片通过完整的 VGG 模型运行没有什么不同,只是我们提前完成了一次。
这是一种优化,将使我们的模型训练更快并消耗更少的内存。
我们可以在 Keras 中使用 VGG 类加载 VGG 模型。我们将加载不带顶部的模型;这意味着不带网络末端用于解释从输入中提取的特征并将其转换为类别预测的层。我们对图像的网络分类不感兴趣,我们将训练我们自己对图像特征的解释。
Keras 还提供了将加载的照片重塑为模型首选大小(例如 3 通道 224 x 224 像素图像)的工具。
下面是一个名为 `extract_features()` 的函数,它给定一个目录名,将加载每张照片,为其准备 VGG,并从 VGG 模型中收集预测特征。图像特征是一个形状为 (7, 7, 512) 的 3 维数组。
该函数返回一个从图像标识符到图像特征的字典。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# 从目录中的每张照片中提取特征 def extract_features(directory): # 加载模型 in_layer = Input(shape=(224, 224, 3)) model = VGG16(include_top=False, input_tensor=in_layer) print(model.summary()) # 从每张照片中提取特征 features = dict() for name in listdir(directory): # 从文件中加载图像 filename = directory + '/' + name image = load_img(filename, target_size=(224, 224)) # 将图像像素转换为 numpy 数组 image = img_to_array(image) # 重塑数据以适应模型 image = image.reshape((1, image.shape[0], image.shape[1], image.shape[2])) # 准备图像以适应 VGG 模型 image = preprocess_input(image) # 获取特征 feature = model.predict(image, verbose=0) # 获取图像 id image_id = name.split('.')[0] # 存储特征 features[image_id] = feature print('>%s' % name) return features |
我们可以调用此函数来准备照片数据以测试我们的模型,然后将生成的字典保存到名为“`features.pkl`”的文件中。
完整的示例如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
from os import listdir from pickle import dump from keras.applications.vgg16 import VGG16 from keras.preprocessing.image import load_img from keras.preprocessing.image import img_to_array from keras.applications.vgg16 import preprocess_input from keras.layers import Input # 从目录中的每张照片中提取特征 def extract_features(directory): # 加载模型 in_layer = Input(shape=(224, 224, 3)) model = VGG16(include_top=False, input_tensor=in_layer) print(model.summary()) # 从每张照片中提取特征 features = dict() for name in listdir(directory): # 从文件中加载图像 filename = directory + '/' + name image = load_img(filename, target_size=(224, 224)) # 将图像像素转换为 numpy 数组 image = img_to_array(image) # 重塑数据以适应模型 image = image.reshape((1, image.shape[0], image.shape[1], image.shape[2])) # 准备图像以适应 VGG 模型 image = preprocess_input(image) # 获取特征 feature = model.predict(image, verbose=0) # 获取图像 id image_id = name.split('.')[0] # 存储特征 features[image_id] = feature print('>%s' % name) return features # 从所有图像中提取特征 directory = 'Flicker8k_Dataset' features = extract_features(directory) print('Extracted Features: %d' % len(features)) # 保存到文件 dump(features, open('features.pkl', 'wb')) |
根据您的硬件情况,此数据准备步骤可能需要一段时间,在配备现代工作站的 CPU 上可能需要一个小时。
运行结束时,您将获得存储在“`features.pkl`”中的提取特征,以备后用。
基线描述生成模型
在本节中,我们将定义一个用于生成照片描述的基线模型,以及如何评估它,以便将其与该基线的变体进行比较。
本节分为 5 个部分
- 加载数据。
- 拟合模型。
- 评估模型。
- 完整示例
- “A”与“A”测试
- 生成照片描述
1. 加载数据
我们不会在所有字幕数据上训练模型,甚至不会在大量数据样本上训练模型。
在本教程中,我们旨在快速测试一套不同配置的字幕模型,以了解在此数据上哪些有效。这意味着模型配置的评估需要快速完成。为此,我们将在 100 张照片和字幕上训练模型,然后在其训练数据集和 100 张照片和字幕的新测试集上进行评估。
首先,我们需要加载预定义的照片子集。提供的数据集有单独的训练集、测试集和开发集,它们实际上只是不同的照片标识符组。我们将加载开发集,并使用前 100 个标识符作为训练集,后 100 个标识符(例如从 100 到 200)作为测试集。
下面的函数 `load_set()` 将加载一组预定义的标识符,我们将以“`Flickr_8k.devImages.txt`”文件名作为参数调用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 加载预定义的照片标识符列表 def load_set(filename): doc = load_doc(filename) dataset = list() # 逐行处理 for line in doc.split('\n'): # 跳过空行 if len(line) < 1: continue # 获取图像标识符 identifier = line.split('.')[0] dataset.append(identifier) return set(dataset) |
接下来,我们需要将集合分成训练集和测试集。
我们将首先对标识符进行排序,以确保它们在不同机器和运行中始终保持一致的分裂,然后将前 100 个用于训练,接下来的 100 个(例如从 100 到 200)用于测试。
下面的 `train_test_split()` 函数将根据加载的标识符集合创建此划分。
1 2 3 4 5 6 |
# 将数据集分成训练/测试元素 def train_test_split(dataset): # 排序键以确保划分一致 ordered = sorted(dataset) # 将分割后的数据集作为两个新集合返回 return set(ordered[:100]), set(ordered[100:200]) |
现在,我们可以使用预定义的训练或测试标识符集加载照片描述。
下面是 `load_clean_descriptions()` 函数,它从 `descriptions.txt` 中加载给定标识符集的清理文本描述,并返回一个标识符到文本的字典。
我们将开发的模型将根据一张照片生成一个描述,并且该描述将一次生成一个单词。先前生成的单词序列将作为输入提供。因此,我们需要一个“`first word`”来启动生成过程,以及一个“`last word`”来表示描述的结束。我们将为此目的使用字符串“`startseq`”和“`endseq`”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# 将清理过的描述加载到内存中 def load_clean_descriptions(filename, dataset): # 加载文档 doc = load_doc(filename) descriptions = dict() for line in doc.split('\n'): # 按空格分割行 tokens = line.split() # 将 id 从描述中分离出来 image_id, image_desc = tokens[0], tokens[1:] # 跳过不在集合中的图像 if image_id in dataset: # 存储 descriptions[image_id] = 'startseq ' + ' '.join(image_desc) + ' endseq' return descriptions |
接下来,我们可以为给定的数据集加载照片特征。
下面定义了一个名为 `load_photo_features()` 的函数,该函数加载所有照片描述的完整集合,然后返回给定照片标识符集合中感兴趣的子集。这不是很高效,因为加载的所有照片特征字典大约有 700 兆字节。尽管如此,这将使我们能够快速启动和运行。
注意:如果您有更好的方法,请在下面的评论中分享。
1 2 3 4 5 6 7 |
# 加载照片特征 def load_photo_features(filename, dataset): # 加载所有特征 all_features = load(open(filename, 'rb')) # 过滤特征 features = {k: all_features[k] for k in dataset} return features |
我们可以在这里暂停,测试到目前为止开发的所有内容。
完整的代码示例如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
from pickle import load # 加载文档到内存 def load_doc(filename): # 以只读方式打开文件 file = open(filename, 'r') # 读取所有文本 text = file.read() # 关闭文件 file.close() return text # 加载预定义的照片标识符列表 def load_set(filename): doc = load_doc(filename) dataset = list() # 逐行处理 for line in doc.split('\n'): # 跳过空行 if len(line) < 1: continue # 获取图像标识符 identifier = line.split('.')[0] dataset.append(identifier) return set(dataset) # 将数据集分成训练/测试元素 def train_test_split(dataset): # 排序键以确保划分一致 ordered = sorted(dataset) # 将分割后的数据集作为两个新集合返回 return set(ordered[:100]), set(ordered[100:200]) # 将清理过的描述加载到内存中 def load_clean_descriptions(filename, dataset): # 加载文档 doc = load_doc(filename) descriptions = dict() for line in doc.split('\n'): # 按空格分割行 tokens = line.split() # 将 id 从描述中分离出来 image_id, image_desc = tokens[0], tokens[1:] # 跳过不在集合中的图像 if image_id in dataset: # 存储 descriptions[image_id] = 'startseq ' + ' '.join(image_desc) + ' endseq' return descriptions # 加载照片特征 def load_photo_features(filename, dataset): # 加载所有特征 all_features = load(open(filename, 'rb')) # 过滤特征 features = {k: all_features[k] for k in dataset} return features # 加载开发集 filename = 'Flickr8k_text/Flickr_8k.devImages.txt' dataset = load_set(filename) print('数据集: %d' % len(dataset)) # 训练-测试集划分 train, test = train_test_split(dataset) print('训练集=%d, 测试集=%d' % (len(train), len(test))) # 描述 train_descriptions = load_clean_descriptions('descriptions.txt', train) test_descriptions = load_clean_descriptions('descriptions.txt', test) print('描述: 训练集=%d, 测试集=%d' % (len(train_descriptions), len(test_descriptions))) # 图片特征 train_features = load_photo_features('features.pkl', train) test_features = load_photo_features('features.pkl', test) print('图片: 训练集=%d, 测试集=%d' % (len(train_features), len(test_features))) |
运行此示例首先加载开发数据集中1,000个图片标识符。选择一个训练集和测试集,并用它们来过滤干净的图片描述集和准备好的图像特征。
我们快要完成了。
1 2 3 4 |
数据集: 1,000 训练集=100, 测试集=100 描述: 训练集=100, 测试集=100 图片: 训练集=100, 测试集=100 |
描述文本需要先编码成数字,然后才能作为输入呈现给模型,或者与模型的预测进行比较。
编码数据的第一步是创建一个从单词到唯一整数值的一致映射。Keras提供了Tokenizer类,可以从加载的描述数据中学习这个映射。
下面定义了`create_tokenizer()`函数,它将根据加载的图片描述文本来拟合一个Tokenizer。
1 2 3 4 5 6 7 8 9 10 11 |
# 给定描述,拟合分词器 def create_tokenizer(descriptions): lines = list(descriptions.values()) tokenizer = Tokenizer() tokenizer.fit_on_texts(lines) return tokenizer # 准备分词器 tokenizer = create_tokenizer(descriptions) vocab_size = len(tokenizer.word_index) + 1 print('词汇表大小: %d' % vocab_size) |
我们现在可以对文本进行编码了。
每个描述都将被分成单词。模型将接收一个单词和图片作为输入,并生成下一个单词。然后,描述的前两个单词将与图片一起作为输入提供给模型,以生成下一个单词。这就是模型训练的方式。
例如,输入序列“小女孩在田野里奔跑”将被分成6个输入-输出对来训练模型:
1 2 3 4 5 6 7 |
X1,X2(文本序列),y(单词) 照片 startseq, little 照片 startseq, little, girl 照片 startseq, little, girl, running 照片 startseq, little, girl, running, in 照片 startseq, little, girl, running, in, field 照片 startseq, little, girl, running, in, field, endseq |
稍后当模型用于生成描述时,生成的词将被连接起来,并递归地作为输入提供,以生成图像的标题。
下面的`create_sequences()`函数,给定分词器、一个干净的描述、一张照片的特征和最大描述长度,将为模型训练准备一组输入-输出对。调用此函数将返回`X1`和`X2`(用于图像数据和输入序列数据的数组)以及`y`值(用于输出词)。
输入序列被整数编码,输出词被独热编码,以表示预期词在所有可能词汇中的概率分布。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# 为图像创建图像序列、输入序列和输出单词 def create_sequences(tokenizer, desc, image, max_length): Ximages, XSeq, y = list(), list(),list() vocab_size = len(tokenizer.word_index) + 1 # 整数编码描述 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] # 存储 Ximages.append(image) XSeq.append(in_seq) y.append(out_seq) # Ximages, XSeq, y = array(Ximages), array(XSeq), array(y) return [Ximages, XSeq, y] |
2. 拟合模型
我们快要准备好拟合模型了。
模型的一部分已经讨论过了,但让我们再重申一下。
该模型基于论文“Show and Tell: A Neural Image Caption Generator”(2015年)中提出的示例。
该模型包含三个部分:
- 图片特征提取器。这是一个在ImageNet数据集上预训练的16层VGG模型。我们已经使用VGG模型(不包括顶部层)对照片进行了预处理,并将使用该模型预测的提取特征作为输入。
- 序列处理器。这是一个用于处理文本输入的词嵌入层,后跟一个LSTM层。LSTM的输出由一个Dense层一次一个输出地解释。
- 解释器(暂无更好的名称)。特征提取器和序列处理器都输出一个固定长度的向量,其长度是最大序列的长度。这些向量被连接在一起,然后经过一个LSTM和Dense层处理,最后进行最终预测。
基础模型中使用了保守数量的神经元。具体来说,特征提取器之后是一个128的Dense层,一个50维的词嵌入,接着是一个256单元的LSTM和序列处理器之后一个128神经元的Dense层,最后在网络末端是一个500单元的LSTM,接着是一个500神经元的Dense层。
该模型预测词汇表上的概率分布,因此在拟合网络时使用softmax激活函数并最小化分类交叉熵损失函数。
函数`define_model()`根据词汇表大小和照片描述的最大长度定义了基线模型。Keras函数式API用于定义模型,因为它提供了定义一个接受两个输入流并将其组合的模型所需的灵活性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# 定义图片字幕生成模型 def define_model(vocab_size, max_length): # 特征提取器 (编码器) inputs1 = Input(shape=(7, 7, 512)) fe1 = GlobalMaxPooling2D()(inputs1) fe2 = Dense(128, activation='relu')(fe1) fe3 = RepeatVector(max_length)(fe2) # 嵌入 inputs2 = Input(shape=(max_length,)) emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2) emb3 = LSTM(256, return_sequences=True)(emb2) emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3) # 合并输入 merged = concatenate([fe3, emb4]) # 语言模型 (解码器) lm2 = LSTM(500)(merged) lm3 = Dense(500, activation='relu')(lm2) outputs = Dense(vocab_size, activation='softmax')(lm3) # 将 [图像, 序列] 与 [词] 联系起来 model = Model(inputs=[inputs1, inputs2], outputs=outputs) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) print(model.summary()) plot_model(model, show_shapes=True, to_file='plot.png') return model |
为了了解模型的结构,特别是层的形状,请参阅下面的摘要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
____________________________________________________________________________________________________ 层 (类型) 输出形状 参数数量 连接到 ==================================================================================================== input_1 (InputLayer) (None, 7, 7, 512) 0 ____________________________________________________________________________________________________ input_2 (InputLayer) (None, 25) 0 ____________________________________________________________________________________________________ global_max_pooling2d_1 (GlobalMa (None, 512) 0 input_1[0][0] ____________________________________________________________________________________________________ embedding_1 (Embedding) (None, 25, 50) 18300 input_2[0][0] ____________________________________________________________________________________________________ dense_1 (Dense) (None, 128) 65664 global_max_pooling2d_1[0][0] ____________________________________________________________________________________________________ lstm_1 (LSTM) (None, 25, 256) 314368 embedding_1[0][0] ____________________________________________________________________________________________________ repeat_vector_1 (RepeatVector) (None, 25, 128) 0 dense_1[0][0] ____________________________________________________________________________________________________ time_distributed_1 (TimeDistribu (None, 25, 128) 32896 lstm_1[0][0] ____________________________________________________________________________________________________ concatenate_1 (Concatenate) (None, 25, 256) 0 repeat_vector_1[0][0] time_distributed_1[0][0] ____________________________________________________________________________________________________ lstm_2 (LSTM) (None, 500) 1514000 concatenate_1[0][0] ____________________________________________________________________________________________________ dense_3 (Dense) (None, 500) 250500 lstm_2[0][0] ____________________________________________________________________________________________________ dense_4 (Dense) (None, 366) 183366 dense_3[0][0] ==================================================================================================== 总参数: 2,379,094 可训练参数: 2,379,094 不可训练参数: 0 ____________________________________________________________________________________________________ |
我们还创建了一个图来可视化网络结构,这有助于更好地理解两个输入流。

基线字幕深度学习模型图
我们将使用数据生成器来训练模型。这并非严格要求,因为字幕和提取的照片特征可能作为单个数据集存储在内存中。然而,当您在整个数据集上训练最终模型时,这是一个很好的实践。
生成器在被调用时会产生一个结果。在Keras中,它会产生一个单一批次的输入-输出样本,用于估计误差梯度和更新模型权重。
函数`data_generator()`定义了数据生成器,给定已加载照片描述的字典、照片特征、用于整数编码序列的分词器以及数据集中最大序列长度。
生成器会无限循环,并在需要时不断生成批次的输入-输出对。我们还有一个`n_step`参数,它允许我们调整每个批次要生成多少张图像的输入-输出对。平均序列有10个单词,即10个输入-输出对,一个好的批次大小可能是30个样本,大约是2到3张图像的量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# 数据生成器,旨在用于调用 model.fit_generator() def data_generator(descriptions, features, tokenizer, max_length, n_step): # 循环直到训练结束 while 1: # 遍历数据集中照片的标识符 keys = list(descriptions.keys()) for i in range(0, len(keys), n_step): Ximages, XSeq, y = list(), list(),list() for j in range(i, min(len(keys), i+n_step)): image_id = keys[j] # 获取照片特征输入 image = features[image_id][0] # 获取文本输入 desc = descriptions[image_id] # 生成输入-输出对 in_img, in_seq, out_word = create_sequences(tokenizer, desc, image, max_length) for k in range(len(in_img)): Ximages.append(in_img[k]) XSeq .append(in_seq[k]) y .append(out_word[k]) # 将此批次样本提供给模型 yield [[array(Ximages), array(XSeq)], array(y)] |
可以通过调用`fit_generator()`并将数据生成器以及所有必要的参数传递给它来拟合模型。在拟合模型时,我们还可以指定每个 epoch 运行的批次数和 epoch 的数量。
1 |
model.fit_generator(data_generator(train_descriptions, train_features, tokenizer, max_length, n_photos_per_update), steps_per_epoch=n_batches_per_epoch, epochs=n_epochs, verbose=verbose) |
在这些实验中,我们将使用每批2张图片,每个epoch 50批(即100张图片),以及50个训练epoch。您可以在自己的实验中尝试不同的配置。
3. 评估模型
现在我们知道了如何准备数据和定义模型,我们必须定义一个测试工具来评估给定的模型。
我们将通过以下方式评估模型:在数据集上训练模型,为训练数据集中所有照片生成描述,使用成本函数评估这些预测,然后多次重复此评估过程。
结果将是模型技能分数的分布,我们可以通过计算平均值和标准差来总结。这是评估深度学习模型的首选方法。请参阅此帖子。
首先,我们需要能够使用训练好的模型为照片生成描述。
这涉及传入起始描述标记“`startseq`”,生成一个单词,然后递归调用模型,以生成的单词作为输入,直到达到序列结束标记“`endseq`”或达到最大描述长度。
下面名为`generate_desc()`的函数实现了这种行为,给定一个训练好的模型和准备好的照片作为输入,它会生成文本描述。它调用`word_for_id()`函数将整数预测映射回单词。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# 将整数映射到单词 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 = 'startseq' # 遍历整个序列长度 for i in range(max_length): # 整数编码输入序列 sequence = tokenizer.texts_to_sequences([in_text])[0] # 填充输入 sequence = pad_sequences([sequence], maxlen=max_length) # 预测下一个词 yhat = model.predict([photo,sequence], verbose=0) # 将概率转换为整数 yhat = argmax(yhat) # 将整数映射到单词 word = word_for_id(yhat, tokenizer) # 如果无法映射单词则停止 if word is None: break # 附加作为生成下一个单词的输入 in_text += ' ' + word # 如果我们预测到序列的结束,则停止 if word == 'endseq': break return in_text |
我们将为训练数据集和测试数据集中的所有照片生成预测。
下面名为 evaluate_model() 的函数将根据给定的照片描述和照片特征数据集评估训练过的模型。实际和预测的描述将使用语料库BLEU分数进行收集和集体评估,该分数总结了生成的文本与预期文本的接近程度。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 评估模型的技能 def evaluate_model(model, descriptions, photos, tokenizer, max_length): actual, predicted = list(), list() # 遍历整个集合 for key, desc in descriptions.items(): # 生成描述 yhat = generate_desc(model, tokenizer, photos[key], max_length) # 存储实际和预测值 actual.append([desc.split()]) predicted.append(yhat.split()) # 计算 BLEU 分数 bleu = corpus_bleu(actual, predicted) return bleu |
BLEU 分数用于文本翻译中,以评估翻译文本与一个或多个参考翻译的对比。实际上,我们确实可以访问每个图像的多个参考描述,可以进行比较,但为了简单起见,我们将使用数据集中每张照片的第一个描述(例如,清理后的版本)。
您可以在这里了解有关 BLEU 分数的更多信息
NLTK Python 库在 corpus_bleu() 函数中实现了 BLEU 分数计算。分数越高越好,接近 1.0 的分数更好,接近零的分数更差。
最后,我们只需在循环中多次定义、拟合和评估模型,然后报告最终的平均分数。
理想情况下,我们应该重复实验 30 次或更多次,但这对于我们的小型测试工具来说太耗时了。相反,我们将评估模型 3 次。它会更快,但平均分数会有更高的方差。
下面定义了模型评估循环。运行结束时,训练集和测试集的 BLEU 分数分布将保存到文件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# 运行实验 train_results, test_results = list(), list() for i in range(n_repeats): # 定义模型 model = define_model(vocab_size, max_length) # 拟合模型 model.fit_generator(data_generator(train_descriptions, train_features, tokenizer, max_length, n_photos_per_update), steps_per_epoch=n_batches_per_epoch, epochs=n_epochs, verbose=verbose) # 在训练数据上评估模型 train_score = evaluate_model(model, train_descriptions, train_features, tokenizer, max_length) test_score = evaluate_model(model, test_descriptions, test_features, tokenizer, max_length) # 存储 train_results.append(train_score) test_results.append(test_score) print('>%d: train=%f test=%f' % ((i+1), train_score, test_score)) # 将结果保存到文件 df = DataFrame() df['train'] = train_results df['test'] = test_results print(df.describe()) df.to_csv(model_name+'.csv', index=False) |
我们将运行参数化如下,允许我们命名每次运行并将结果保存到单独的文件中。
1 2 3 4 5 6 7 |
# 定义实验 model_name = 'baseline1' verbose = 2 n_epochs = 50 n_photos_per_update = 2 n_batches_per_epoch = int(len(train) / n_photos_per_update) n_repeats = 3 |
4. 完整示例
完整的示例如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
from os import listdir from numpy import array from numpy import argmax from pandas import DataFrame from nltk.translate.bleu_score import corpus_bleu from pickle import load from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences from keras.utils import to_categorical from keras.preprocessing.image import load_img from keras.preprocessing.image import img_to_array from keras.applications.vgg16 import preprocess_input from keras.applications.vgg16 import VGG16 from keras.utils import plot_model from keras.models import Model from keras.layers import Input from keras.layers import Dense from keras.layers import Flatten 从 keras.layers 导入 LSTM from keras.layers import RepeatVector from keras.layers import TimeDistributed from keras.layers import Embedding from keras.layers.merge import concatenate from keras.layers.pooling import GlobalMaxPooling2D # 加载文档到内存 def load_doc(filename): # 以只读方式打开文件 file = open(filename, 'r') # 读取所有文本 text = file.read() # 关闭文件 file.close() return text # 加载预定义的照片标识符列表 def load_set(filename): doc = load_doc(filename) dataset = list() # 逐行处理 for line in doc.split('\n'): # 跳过空行 if len(line) < 1: continue # 获取图像标识符 identifier = line.split('.')[0] dataset.append(identifier) return set(dataset) # 将数据集分成训练/测试元素 def train_test_split(dataset): # 排序键以确保划分一致 ordered = sorted(dataset) # 将分割后的数据集作为两个新集合返回 return set(ordered[:100]), set(ordered[100:200]) # 将清理过的描述加载到内存中 def load_clean_descriptions(filename, dataset): # 加载文档 doc = load_doc(filename) descriptions = dict() for line in doc.split('\n'): # 按空格分割行 tokens = line.split() # 将 id 从描述中分离出来 image_id, image_desc = tokens[0], tokens[1:] # 跳过不在集合中的图像 if image_id in dataset: # 存储 descriptions[image_id] = 'startseq ' + ' '.join(image_desc) + ' endseq' return descriptions # 加载照片特征 def load_photo_features(filename, dataset): # 加载所有特征 all_features = load(open(filename, 'rb')) # 过滤特征 features = {k: all_features[k] for k in dataset} return features # 给定描述,拟合分词器 def create_tokenizer(descriptions): lines = list(descriptions.values()) tokenizer = Tokenizer() tokenizer.fit_on_texts(lines) return tokenizer # 为图像创建图像序列、输入序列和输出单词 def create_sequences(tokenizer, desc, image, max_length): Ximages, XSeq, y = list(), list(),list() vocab_size = len(tokenizer.word_index) + 1 # 整数编码描述 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] # 存储 Ximages.append(image) XSeq.append(in_seq) y.append(out_seq) # Ximages, XSeq, y = array(Ximages), array(XSeq), array(y) return [Ximages, XSeq, y] # 定义图片字幕生成模型 def define_model(vocab_size, max_length): # 特征提取器 (编码器) inputs1 = Input(shape=(7, 7, 512)) fe1 = GlobalMaxPooling2D()(inputs1) fe2 = Dense(128, activation='relu')(fe1) fe3 = RepeatVector(max_length)(fe2) # 嵌入 inputs2 = Input(shape=(max_length,)) emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2) emb3 = LSTM(256, return_sequences=True)(emb2) emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3) # 合并输入 merged = concatenate([fe3, emb4]) # 语言模型 (解码器) lm2 = LSTM(500)(merged) lm3 = Dense(500, activation='relu')(lm2) outputs = Dense(vocab_size, activation='softmax')(lm3) # 将 [图像, 序列] 与 [词] 联系起来 model = Model(inputs=[inputs1, inputs2], outputs=outputs) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) print(model.summary()) plot_model(model, show_shapes=True, to_file='plot.png') return model # 数据生成器,旨在用于调用 model.fit_generator() def data_generator(descriptions, features, tokenizer, max_length, n_step): # 循环直到训练结束 while 1: # 遍历数据集中照片的标识符 keys = list(descriptions.keys()) for i in range(0, len(keys), n_step): Ximages, XSeq, y = list(), list(),list() for j in range(i, min(len(keys), i+n_step)): image_id = keys[j] # 获取照片特征输入 image = features[image_id][0] # 获取文本输入 desc = descriptions[image_id] # 生成输入-输出对 in_img, in_seq, out_word = create_sequences(tokenizer, desc, image, max_length) for k in range(len(in_img)): Ximages.append(in_img[k]) XSeq .append(in_seq[k]) y .append(out_word[k]) # 将此批次样本提供给模型 yield [[array(Ximages), array(XSeq)], array(y)] # 将整数映射到单词 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 = 'startseq' # 遍历整个序列长度 for i in range(max_length): # 整数编码输入序列 sequence = tokenizer.texts_to_sequences([in_text])[0] # 填充输入 sequence = pad_sequences([sequence], maxlen=max_length) # 预测下一个词 yhat = model.predict([photo,sequence], verbose=0) # 将概率转换为整数 yhat = argmax(yhat) # 将整数映射到单词 word = word_for_id(yhat, tokenizer) # 如果无法映射单词则停止 if word is None: break # 附加作为生成下一个单词的输入 in_text += ' ' + word # 如果我们预测到序列的结束,则停止 if word == 'endseq': break return in_text # 评估模型的技能 def evaluate_model(model, descriptions, photos, tokenizer, max_length): actual, predicted = list(), list() # 遍历整个集合 for key, desc in descriptions.items(): # 生成描述 yhat = generate_desc(model, tokenizer, photos[key], max_length) # 存储实际和预测值 actual.append([desc.split()]) predicted.append(yhat.split()) # 计算 BLEU 分数 bleu = corpus_bleu(actual, predicted) return bleu # 加载开发集 filename = 'Flickr8k_text/Flickr_8k.devImages.txt' dataset = load_set(filename) print('数据集: %d' % len(dataset)) # 训练-测试集划分 train, test = train_test_split(dataset) # 描述 train_descriptions = load_clean_descriptions('descriptions.txt', train) test_descriptions = load_clean_descriptions('descriptions.txt', test) print('描述: 训练集=%d, 测试集=%d' % (len(train_descriptions), len(test_descriptions))) # 图片特征 train_features = load_photo_features('features.pkl', train) test_features = load_photo_features('features.pkl', test) print('图片: 训练集=%d, 测试集=%d' % (len(train_features), len(test_features))) # 准备分词器 tokenizer = create_tokenizer(train_descriptions) vocab_size = len(tokenizer.word_index) + 1 print('词汇表大小: %d' % vocab_size) # 确定最大序列长度 max_length = max(len(s.split()) for s in list(train_descriptions.values())) print('Description Length: %d' % max_length) # 定义实验 model_name = 'baseline1' verbose = 2 n_epochs = 50 n_photos_per_update = 2 n_batches_per_epoch = int(len(train) / n_photos_per_update) n_repeats = 3 # 运行实验 train_results, test_results = list(), list() for i in range(n_repeats): # 定义模型 model = define_model(vocab_size, max_length) # 拟合模型 model.fit_generator(data_generator(train_descriptions, train_features, tokenizer, max_length, n_photos_per_update), steps_per_epoch=n_batches_per_epoch, epochs=n_epochs, verbose=verbose) # 在训练数据上评估模型 train_score = evaluate_model(model, train_descriptions, train_features, tokenizer, max_length) test_score = evaluate_model(model, test_descriptions, test_features, tokenizer, max_length) # 存储 train_results.append(train_score) test_results.append(test_score) print('>%d: train=%f test=%f' % ((i+1), train_score, test_score)) # 将结果保存到文件 df = DataFrame() df['train'] = train_results df['test'] = test_results print(df.describe()) df.to_csv(model_name+'.csv', index=False) |
运行示例首先打印加载的训练数据的摘要统计信息。
1 2 3 4 5 |
数据集: 1,000 描述: 训练集=100, 测试集=100 图片: 训练集=100, 测试集=100 词汇量大小:366 描述长度:25 |
该示例在 GPU 硬件上大约需要 20 分钟,在 CPU 硬件上会稍长一些。
注意:考虑到算法或评估过程的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑多次运行示例并比较平均结果。
运行结束时,训练集上的平均 BLEU 为 0.06,测试集上的平均 BLEU 为 0.04。结果存储在 baseline1.csv 中。
1 2 3 4 5 6 7 8 9 |
训练 测试 计数 3.000000 3.000000 平均值 0.060617 0.040978 标准差 0.023498 0.025105 最小值 0.042882 0.012101 25% 0.047291 0.032658 50% 0.051701 0.053215 75% 0.069484 0.055416 最大值 0.087268 0.057617 |
这提供了一个基线模型,用于与不同配置进行比较。
“A”与“A”测试
在开始测试模型的变体之前,了解测试工具是否稳定很重要。
也就是说,模型在 5 次运行中的总结能力是否足以控制模型的随机性。
我们可以通过在 A/B 测试中再次运行 A 对 A 测试来了解这一点。如果再次运行相同的实验,我们期望得到相同的结果;如果不是,可能需要额外的重复来控制方法和数据集的随机性。
注意:考虑到算法或评估过程的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑多次运行示例并比较平均结果。
以下是算法第二次运行的结果。
1 2 3 4 5 6 7 8 9 |
训练 测试 计数 3.000000 3.000000 平均值 0.036902 0.043003 标准差 0.020281 0.017295 最小值 0.018522 0.026055 25% 0.026023 0.034192 50% 0.033525 0.042329 75% 0.046093 0.051477 最大值 0.058660 0.060624 |
我们可以看到,这次运行获得了非常相似的平均值和标准差 BLEU 分数。具体来说,训练集上的平均 BLEU 从 0.06 变为 0.03,测试集上为 0.04 到 0.04。
该工具略有噪音,但足以进行比较。
这个模型好吗?
生成照片描述
我们期望该模型训练不足,甚至可能配置不足,但它能生成任何可读文本吗?
重要的是,基线模型应具备一定的能力,以便我们将基线的 BLEU 分数与所生成描述的质量联系起来。
让我们训练一个单一模型,并从训练集和测试集中生成一些描述作为健全性检查。
将重复次数更改为 1,将运行名称更改为“baseline_generate”。
1 2 |
model_name = 'baseline_generate' n_repeats = 1 |
然后更新 evaluate_model() 函数,使其仅评估数据集中前 5 张照片并打印描述,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# 评估模型的技能 def evaluate_model(model, descriptions, photos, tokenizer, max_length): actual, predicted = list(), list() # 遍历整个集合 for key, desc in descriptions.items(): # 生成描述 yhat = generate_desc(model, tokenizer, photos[key], max_length) # 存储实际和预测值 actual.append([desc.split()]) predicted.append(yhat.split()) print('Actual: %s' % desc) print('Predicted: %s' % yhat) if len(actual) >= 5: break # 计算 BLEU 分数 bleu = corpus_bleu(actual, predicted) return bleu |
重新运行示例。
注意:考虑到算法或评估过程的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑多次运行示例并比较平均结果。
您应该看到训练集的结果如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
实际: startseq 男孩在户外坐着,用力咬着零食 endseq 预测: startseq 男孩 男孩 当他 在外面 endseq 实际: startseq 男人在田野里,背景是美国国旗 endseq 预测: startseq 男人在城市中站立 endseq 实际: startseq 两个女孩正在公园的土路上行走 endseq 预测: startseq 男人走在公园的路上 endseq 实际: startseq 女孩躺在树上,男孩跪在她面前 endseq 预测: startseq 男孩 在水里 endseq 实际: startseq 穿条纹衬衫的男孩在喷泉前跳跃 endseq 预测: startseq 男人 穿着衬衫 在自行车上 endseq |
您应该看到测试数据集的结果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
实际: startseq 三个人正在看摄影设备 endseq 预测: startseq 男孩 赛车手 在自行车上 endseq 实际: startseq 男孩靠在椅子上,另一个男孩用绳子拉着他 endseq 预测: startseq 女孩 在玩 剑 endseq 实际: startseq 黑褐色狗在田野附近空中跳跃 endseq 预测: startseq 狗 狗 奔跑 和 狗 在草地 endseq 实际: startseq 狗把头放在男人的脸上 endseq 预测: startseq 棕色狗 狗 去 拿 球 endseq 实际: startseq 戴绿帽子的男人在高处 endseq 预测: startseq 男人 在高处 挥手 endseq |
我们可以看到,描述并不完美,有些有点粗糙,但总的来说,模型正在生成一些可读的文本。这是一个很好的改进起点。
接下来,让我们看看一些实验,以改变不同子模型的大小或容量。
网络大小参数
在本节中,我们将了解网络结构的粗略变化如何影响模型技能。
我们将研究模型大小的以下方面
- “编码器”的固定向量输出大小。
- 序列编码器模型的大小。
- 语言模型的大小。
让我们开始吧。
固定长度向量的大小
在基线模型中,照片特征提取器和文本序列编码器都输出一个 128 元素的向量。然后这些向量被连接起来,由语言模型处理。
每个子模型中的 128 元素向量包含有关输入序列和照片的所有已知信息。我们可以改变这个向量的大小,看看它是否影响模型技能
首先,我们可以将大小减半,从 128 元素减到 64 元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# 定义图片字幕生成模型 def define_model(vocab_size, max_length): # 特征提取器 (编码器) inputs1 = Input(shape=(7, 7, 512)) fe1 = GlobalMaxPooling2D()(inputs1) fe2 = Dense(64, activation='relu')(fe1) fe3 = RepeatVector(max_length)(fe2) # 嵌入 inputs2 = Input(shape=(max_length,)) emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2) emb3 = LSTM(256, return_sequences=True)(emb2) emb4 = TimeDistributed(Dense(64, activation='relu'))(emb3) # 合并输入 merged = concatenate([fe3, emb4]) # 语言模型 (解码器) lm2 = LSTM(500)(merged) lm3 = Dense(500, activation='relu')(lm2) outputs = Dense(vocab_size, activation='softmax')(lm3) # 将 [图像, 序列] 与 [词] 联系起来 model = Model(inputs=[inputs1, inputs2], outputs=outputs) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) return model |
我们将此模型命名为“size_sm_fixed_vec”。
1 |
model_name = 'size_sm_fixed_vec' |
注意:考虑到算法或评估过程的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑多次运行示例并比较平均结果。
运行此实验会产生以下 BLEU 分数,在测试集上可能比基线略有提高。
1 2 3 4 5 6 7 8 9 |
训练 测试 计数 3.000000 3.000000 平均值 0.204421 0.063148 标准差 0.026992 0.003264 最小值 0.174769 0.059391 25% 0.192849 0.062074 50% 0.210929 0.064757 75% 0.219246 0.065026 最大值 0.227564 0.065295 |
我们还可以将固定长度向量的大小从 128 增加一倍到 256 个单位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# 定义图片字幕生成模型 def define_model(vocab_size, max_length): # 特征提取器 (编码器) inputs1 = Input(shape=(7, 7, 512)) fe1 = GlobalMaxPooling2D()(inputs1) fe2 = Dense(256, activation='relu')(fe1) fe3 = RepeatVector(max_length)(fe2) # 嵌入 inputs2 = Input(shape=(max_length,)) emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2) emb3 = LSTM(256, return_sequences=True)(emb2) emb4 = TimeDistributed(Dense(256, activation='relu'))(emb3) # 合并输入 merged = concatenate([fe3, emb4]) # 语言模型 (解码器) lm2 = LSTM(500)(merged) lm3 = Dense(500, activation='relu')(lm2) outputs = Dense(vocab_size, activation='softmax')(lm3) # 将 [图像, 序列] 与 [词] 联系起来 model = Model(inputs=[inputs1, inputs2], outputs=outputs) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) return model |
我们将此配置命名为“size_lg_fixed_vec”。
1 |
model_name = 'size_lg_fixed_vec' |
运行此实验表明 BLEU 分数表明模型并没有变得更好。
注意:考虑到算法或评估过程的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑多次运行示例并比较平均结果。
可能随着更多数据和/或更长的训练,我们会看到不同的结果。
1 2 3 4 5 6 7 8 9 |
训练 测试 计数 3.000000 3.000000 平均值 0.023517 0.027813 标准差 0.009951 0.010525 最小值 0.012037 0.021737 25% 0.020435 0.021737 50% 0.028833 0.021737 75% 0.029257 0.030852 最大值 0.029682 0.039966 |
序列编码器大小
我们可以将到目前为止解释已生成的词语输入序列的子模型称为序列编码器。
首先,我们可以尝试查看减少序列编码器的表示能力是否会影响模型技能。我们可以将 LSTM 层中的内存单元数量从 256 减少到 128。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# 定义图片字幕生成模型 def define_model(vocab_size, max_length): # 特征提取器 (编码器) inputs1 = Input(shape=(7, 7, 512)) fe1 = GlobalMaxPooling2D()(inputs1) fe2 = Dense(128, activation='relu')(fe1) fe3 = RepeatVector(max_length)(fe2) # 嵌入 inputs2 = Input(shape=(max_length,)) emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2) emb3 = LSTM(128, return_sequences=True)(emb2) emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3) # 合并输入 merged = concatenate([fe3, emb4]) # 语言模型 (解码器) lm2 = LSTM(500)(merged) lm3 = Dense(500, activation='relu')(lm2) outputs = Dense(vocab_size, activation='softmax')(lm3) # 将 [图像, 序列] 与 [词] 联系起来 model = Model(inputs=[inputs1, inputs2], outputs=outputs) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) return model model_name = 'size_sm_seq_model' |
运行此示例,我们可以看到训练和测试集上都可能比基线略有提升。这可能是小训练集大小造成的假象。
1 2 3 4 5 6 7 8 9 |
训练 测试 计数 3.000000 3.000000 平均值 0.074944 0.053917 标准差 0.014263 0.013264 最小值 0.066292 0.039142 25% 0.066713 0.048476 50% 0.067134 0.057810 75% 0.079270 0.061304 最大值 0.091406 0.064799 |
另一方面,我们可以将 LSTM 层数从一个增加到两个,看看是否会产生显著差异。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# 定义图片字幕生成模型 def define_model(vocab_size, max_length): # 特征提取器 (编码器) inputs1 = Input(shape=(7, 7, 512)) fe1 = GlobalMaxPooling2D()(inputs1) fe2 = Dense(128, activation='relu')(fe1) fe3 = RepeatVector(max_length)(fe2) # 嵌入 inputs2 = Input(shape=(max_length,)) emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2) emb3 = LSTM(256, return_sequences=True)(emb2) emb4 = LSTM(256, return_sequences=True)(emb3) emb5 = TimeDistributed(Dense(128, activation='relu'))(emb4) # 合并输入 merged = concatenate([fe3, emb5]) # 语言模型 (解码器) lm2 = LSTM(500)(merged) lm3 = Dense(500, activation='relu')(lm2) outputs = Dense(vocab_size, activation='softmax')(lm3) # 将 [图像, 序列] 与 [词] 联系起来 model = Model(inputs=[inputs1, inputs2], outputs=outputs) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) return model model_name = 'size_lg_seq_model' |
运行此实验表明训练集和测试集上的 BLEU 都有显著提升。
1 2 3 4 5 6 7 8 9 |
训练 测试 计数 3.000000 3.000000 平均值 0.094937 0.096970 标准差 0.022394 0.079270 最小值 0.069151 0.046722 25% 0.087656 0.051279 50% 0.106161 0.055836 75% 0.107830 0.122094 最大值 0.109499 0.188351 |
我们还可以尝试将词嵌入的表示能力从 50 维增加一倍到 100 维。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# 定义图片字幕生成模型 def define_model(vocab_size, max_length): # 特征提取器 (编码器) inputs1 = Input(shape=(7, 7, 512)) fe1 = GlobalMaxPooling2D()(inputs1) fe2 = Dense(128, activation='relu')(fe1) fe3 = RepeatVector(max_length)(fe2) # 嵌入 inputs2 = Input(shape=(max_length,)) emb2 = Embedding(vocab_size, 100, mask_zero=True)(inputs2) emb3 = LSTM(256, return_sequences=True)(emb2) emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3) # 合并输入 merged = concatenate([fe3, emb4]) # 语言模型 (解码器) lm2 = LSTM(500)(merged) lm3 = Dense(500, activation='relu')(lm2) outputs = Dense(vocab_size, activation='softmax')(lm3) # 将 [图像, 序列] 与 [词] 联系起来 model = Model(inputs=[inputs1, inputs2], outputs=outputs) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) return model model_name = 'size_em_seq_model' |
注意:考虑到算法或评估过程的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑多次运行示例并比较平均结果。
我们看到训练数据集上有很大的变化,但测试数据集上变化不大。
1 2 3 4 5 6 7 8 |
计数 3.000000 3.000000 平均值 0.112743 0.050935 标准差 0.017136 0.006860 最小值 0.096121 0.043741 25% 0.103940 0.047701 50% 0.111759 0.051661 75% 0.121055 0.054533 最大值 0.130350 0.057404 |
语言模型大小
我们可以将从连接序列和照片特征输入中学习的模型称为语言模型。它负责生成词语。
首先,我们可以通过将 LSTM 和密集层从 500 个神经元减少到 256 个来查看对模型技能的影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# 定义图片字幕生成模型 def define_model(vocab_size, max_length): # 特征提取器 (编码器) inputs1 = Input(shape=(7, 7, 512)) fe1 = GlobalMaxPooling2D()(inputs1) fe2 = Dense(128, activation='relu')(fe1) fe3 = RepeatVector(max_length)(fe2) # 嵌入 inputs2 = Input(shape=(max_length,)) emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2) emb3 = LSTM(256, return_sequences=True)(emb2) emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3) # 合并输入 merged = concatenate([fe3, emb4]) # 语言模型 (解码器) lm2 = LSTM(256)(merged) lm3 = Dense(256, activation='relu')(lm2) outputs = Dense(vocab_size, activation='softmax')(lm3) # 将 [图像, 序列] 与 [词] 联系起来 model = Model(inputs=[inputs1, inputs2], outputs=outputs) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) return model model_name = 'size_sm_lang_model' |
注意:考虑到算法或评估过程的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑多次运行示例并比较平均结果。
我们可以看到,这对训练和测试数据集的 BLEU 都产生了小的积极影响,这再次可能与数据集的小尺寸有关。
1 2 3 4 5 6 7 8 9 |
训练 测试 计数 3.000000 3.000000 平均值 0.063632 0.056059 标准差 0.018521 0.009064 最小值 0.045127 0.048916 25% 0.054363 0.050961 50% 0.063599 0.053005 75% 0.072884 0.059630 最大值 0.082169 0.066256 |
我们还可以通过添加第二个相同大小的 LSTM 层来查看将语言模型容量翻倍的影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# 定义图片字幕生成模型 def define_model(vocab_size, max_length): # 特征提取器 (编码器) inputs1 = Input(shape=(7, 7, 512)) fe1 = GlobalMaxPooling2D()(inputs1) fe2 = Dense(128, activation='relu')(fe1) fe3 = RepeatVector(max_length)(fe2) # 嵌入 inputs2 = Input(shape=(max_length,)) emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2) emb3 = LSTM(256, return_sequences=True)(emb2) emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3) # 合并输入 merged = concatenate([fe3, emb4]) # 语言模型 (解码器) lm2 = LSTM(500, return_sequences=True)(merged) lm3 = LSTM(500)(lm2) lm4 = Dense(500, activation='relu')(lm3) outputs = Dense(vocab_size, activation='softmax')(lm4) # 将 [图像, 序列] 与 [词] 联系起来 model = Model(inputs=[inputs1, inputs2], outputs=outputs) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) return model model_name = 'size_lg_lang_model' |
同样,我们看到 BLEU 的微小变动,可能噪音和数据集大小的假象。测试的改进。
注意:考虑到算法或评估过程的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑多次运行示例并比较平均结果。
测试数据集的改进可能是一个好兆头。这可能是一个值得探索的改变。
1 2 3 4 5 6 7 8 9 |
训练 测试 计数 3.000000 3.000000 平均值 0.043838 0.067658 标准差 0.037580 0.045813 最小值 0.017990 0.015757 25% 0.022284 0.050252 50% 0.026578 0.084748 75% 0.056763 0.093608 最大值 0.086948 0.102469 |
在更小的数据集上调整模型大小具有挑战性。
配置特征提取模型
预训练的 VGG16 模型的使用提供了一些额外的配置点。
基线模型从 VGG 模型中移除了顶部,包括一个全局最大池化层,然后将其馈入特征编码为 128 元素向量。
在本节中,我们将研究对基线模型的以下修改
- 在 VGG 模型后使用全局平均池化层。
- 不使用任何全局池化。
全局平均池化
我们可以用 GlobalAveragePooling2D 层替换 GlobalMaxPooling2D 层,以实现平均池化。
全局平均池化是为了减少图像分类问题中的过拟合而开发的,但可能在解释从图像中提取的特征方面提供一些好处。
有关全局平均池化的更多信息,请参阅论文
- 网络中的网络, 2013.
更新的 define_model() 函数和实验名称如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# 定义图片字幕生成模型 def define_model(vocab_size, max_length): # 特征提取器 (编码器) inputs1 = Input(shape=(7, 7, 512)) fe1 = GlobalAveragePooling2D()(inputs1) fe2 = Dense(128, activation='relu')(fe1) fe3 = RepeatVector(max_length)(fe2) # 嵌入 inputs2 = Input(shape=(max_length,)) emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2) emb3 = LSTM(256, return_sequences=True)(emb2) emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3) # 合并输入 merged = concatenate([fe3, emb4]) # 语言模型 (解码器) lm2 = LSTM(500)(merged) lm3 = Dense(500, activation='relu')(lm2) outputs = Dense(vocab_size, activation='softmax')(lm3) # 将 [图像, 序列] 与 [词] 联系起来 model = Model(inputs=[inputs1, inputs2], outputs=outputs) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) return model model_name = 'fe_avg_pool' |
注意:考虑到算法或评估过程的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑多次运行示例并比较平均结果。
结果表明训练数据集的性能显著提高,这可能是过拟合的迹象。我们还看到测试技能略有提升。这可能是一个值得探索的改变。
我们还看到测试技能略有提升。这可能是一个值得探索的改变。
1 2 3 4 5 6 7 8 9 |
训练 测试 计数 3.000000 3.000000 平均值 0.834627 0.060847 标准差 0.083259 0.040463 最小值 0.745074 0.017705 25% 0.797096 0.042294 50% 0.849118 0.066884 75% 0.879404 0.082418 最大值 0.909690 0.097952 |
无池化
我们可以移除 GlobalMaxPooling2D 并将 3D 图片特征展平,然后直接将其馈送到 Dense 层。
我预计这不是一个好的模型设计,但值得测试这个假设。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# 定义图片字幕生成模型 def define_model(vocab_size, max_length): # 特征提取器 (编码器) inputs1 = Input(shape=(7, 7, 512)) fe1 = Flatten()(inputs1) fe2 = Dense(128, activation='relu')(fe1) fe3 = RepeatVector(max_length)(fe2) # 嵌入 inputs2 = Input(shape=(max_length,)) emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2) emb3 = LSTM(256, return_sequences=True)(emb2) emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3) # 合并输入 merged = concatenate([fe3, emb4]) # 语言模型 (解码器) lm2 = LSTM(500)(merged) lm3 = Dense(500, activation='relu')(lm2) outputs = Dense(vocab_size, activation='softmax')(lm3) # 将 [图像, 序列] 与 [词] 联系起来 model = Model(inputs=[inputs1, inputs2], outputs=outputs) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) return model model_name = 'fe_flat' |
注意:考虑到算法或评估过程的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑多次运行示例并比较平均结果。
令人惊讶的是,我们在训练数据上看到了小幅提升,在测试数据上看到了大幅提升。这(对我来说)很令人惊讶,可能值得进一步调查。
1 2 3 4 5 6 7 8 9 |
训练 测试 计数 3.000000 3.000000 平均值 0.055988 0.135231 标准差 0.017566 0.079714 最小值 0.038605 0.044177 25% 0.047116 0.106633 50% 0.055627 0.169089 75% 0.064679 0.180758 最大值 0.073731 0.192428 |
我们可以尝试重复这个实验,并为解释提取的图片特征提供更多的能力。在 Flatten 层之后添加了一个包含 500 个神经元的新 Dense 层。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# 定义图片字幕生成模型 def define_model(vocab_size, max_length): # 特征提取器 (编码器) inputs1 = Input(shape=(7, 7, 512)) fe1 = Flatten()(inputs1) fe2 = Dense(500, activation='relu')(fe1) fe3 = Dense(128, activation='relu')(fe2) fe4 = RepeatVector(max_length)(fe3) # 嵌入 inputs2 = Input(shape=(max_length,)) emb2 = Embedding(vocab_size, 50, mask_zero=True)(inputs2) emb3 = LSTM(256, return_sequences=True)(emb2) emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3) # 合并输入 merged = concatenate([fe4, emb4]) # 语言模型 (解码器) lm2 = LSTM(500)(merged) lm3 = Dense(500, activation='relu')(lm2) outputs = Dense(vocab_size, activation='softmax')(lm3) # 将 [图像, 序列] 与 [词] 联系起来 model = Model(inputs=[inputs1, inputs2], outputs=outputs) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) return model model_name = 'fe_flat2' |
注意:考虑到算法或评估过程的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑多次运行示例并比较平均结果。
这导致了不太令人印象深刻的变化,并且在测试数据集上 BLEU 结果可能更差。
1 2 3 4 5 6 7 8 9 |
训练 测试 计数 3.000000 3.000000 平均值 0.060126 0.029487 标准差 0.030300 0.013205 最小值 0.031235 0.020850 25% 0.044359 0.021887 50% 0.057483 0.022923 75% 0.074572 0.033805 最大值 0.091661 0.044688 |
词嵌入模型
模型的一个关键部分是序列学习模型,它必须解释为图片生成的词序列。
该子模型的输入是词嵌入,改进词嵌入的一种好方法是使用预训练的词嵌入,而不是将其作为模型的一部分从头开始学习(如基线模型中所示)。
在本节中,我们将探讨使用预训练词嵌入对模型的影响。具体来说
- 训练 Word2Vec 模型
- 训练 Word2Vec 模型 + 微调
训练好的 word2vec 嵌入
用于从文本语料库预训练词嵌入的有效学习算法是 word2vec 算法。
你可以在这里了解更多关于 word2vec 算法的信息
我们可以使用此算法,利用数据集中清理过的图片描述来训练一组新的独立词向量。
Gensim 库提供了对该算法实现的访问,我们可以使用它来预训练嵌入。
首先,我们必须像以前一样加载训练数据集的干净图片描述。
接下来,我们可以在所有干净的描述上拟合 word2vec 模型。我们应该注意,这包括比训练数据集中使用的 50 个描述更多的描述。这些实验的更公平的模型应该只在训练数据集中的那些描述上进行训练。
一旦拟合,我们可以将单词和词向量保存到 ASCII 文件中,或许是为了以后检查或可视化。
1 2 3 4 5 6 7 8 9 10 |
# 训练 word2vec 模型 lines = [s.split() for s in train_descriptions.values()] model = Word2Vec(lines, size=100, window=5, workers=8, min_count=1) # 总结模型中的词汇表大小 words = list(model.wv.vocab) print('词汇表大小: %d' % len(words)) # 以 ASCII (word2vec) 格式保存模型 filename = 'custom_embedding.txt' model.wv.save_word2vec_format(filename, binary=False) |
词嵌入保存到文件“custom_embedding.txt”中。
现在,我们可以将嵌入加载到内存中,只检索我们词汇表中单词的词向量,然后将它们保存到新文件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# 将整个嵌入加载到内存中 embedding = dict() file = open('custom_embedding.txt') for line in file: values = line.split() word = values[0] coefs = asarray(values[1:], dtype='float32') embedding[word] = coefs 文件.close() print('嵌入大小: %d' % len(embedding)) # 总结词汇表 all_tokens = ' '.join(train_descriptions.values()).split() vocabulary = set(all_tokens) print('Vocabulary Size: %d' % len(vocabulary)) # 获取词汇表中单词的向量 cust_embedding = dict() for word in vocabulary: # 检查单词是否在嵌入中 if word not in embedding: continue cust_embedding[word] = embedding[word] print('自定义嵌入 %d' % len(cust_embedding)) # 保存 dump(cust_embedding, open('word2vec_embedding.pkl', 'wb')) print('嵌入已保存') |
完整的示例如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# 为字幕模型准备词向量 from numpy import asarray from pickle import dump from gensim.models import Word2Vec # 加载文档到内存 def load_doc(filename): # 以只读方式打开文件 file = open(filename, 'r') # 读取所有文本 text = file.read() # 关闭文件 file.close() return text # 加载预定义的照片标识符列表 def load_set(filename): doc = load_doc(filename) dataset = list() # 逐行处理 for line in doc.split('\n'): # 跳过空行 if len(line) < 1: continue # 获取图像标识符 identifier = line.split('.')[0] dataset.append(identifier) return set(dataset) # 将数据集分成训练/测试元素 def train_test_split(dataset): # 排序键以确保划分一致 ordered = sorted(dataset) # 将分割后的数据集作为两个新集合返回 return set(ordered[:100]), set(ordered[100:200]) # 将清理过的描述加载到内存中 def load_clean_descriptions(filename, dataset): # 加载文档 doc = load_doc(filename) descriptions = dict() for line in doc.split('\n'): # 按空格分割行 tokens = line.split() # 将 id 从描述中分离出来 image_id, image_desc = tokens[0], tokens[1:] # 跳过不在集合中的图像 if image_id in dataset: # 存储 descriptions[image_id] = 'startseq ' + ' '.join(image_desc) + ' endseq' return descriptions # 加载开发集 filename = 'Flickr8k_text/Flickr_8k.devImages.txt' dataset = load_set(filename) print('数据集: %d' % len(dataset)) # 训练-测试集划分 train, test = train_test_split(dataset) print('训练集=%d, 测试集=%d' % (len(train), len(test))) # 描述 train_descriptions = load_clean_descriptions('descriptions.txt', train) print('描述: train=%d' % len(train_descriptions)) # 训练 word2vec 模型 lines = [s.split() for s in train_descriptions.values()] model = Word2Vec(lines, size=100, window=5, workers=8, min_count=1) # 总结模型中的词汇表大小 words = list(model.wv.vocab) print('词汇表大小: %d' % len(words)) # 以 ASCII (word2vec) 格式保存模型 filename = 'custom_embedding.txt' model.wv.save_word2vec_format(filename, binary=False) # 将整个嵌入加载到内存中 embedding = dict() file = open('custom_embedding.txt') for line in file: values = line.split() word = values[0] coefs = asarray(values[1:], dtype='float32') embedding[word] = coefs 文件.close() print('嵌入大小: %d' % len(embedding)) # 总结词汇表 all_tokens = ' '.join(train_descriptions.values()).split() vocabulary = set(all_tokens) print('Vocabulary Size: %d' % len(vocabulary)) # 获取词汇表中单词的向量 cust_embedding = dict() for word in vocabulary: # 检查单词是否在嵌入中 if word not in embedding: continue cust_embedding[word] = embedding[word] print('自定义嵌入 %d' % len(cust_embedding)) # 保存 dump(cust_embedding, open('word2vec_embedding.pkl', 'wb')) print('嵌入已保存') |
运行此示例将创建一个新的词到词向量的字典映射,存储在文件“word2vec_embedding.pkl”中。
1 2 3 4 5 6 7 8 |
数据集:1000 训练集=100, 测试集=100 描述:训练=100 词汇表大小:365 嵌入大小:366 词汇表大小:365 自定义嵌入 365 嵌入已保存 |
接下来,我们可以加载此嵌入并使用词向量作为 Embedding 层中的固定权重。
下面提供了 load_embedding() 函数,该函数加载自定义 word2vec 嵌入并返回新的 Embedding 层,供模型使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# 加载词嵌入 def load_embedding(tokenizer, vocab_size, max_length): # 加载分词器 embedding = load(open('word2vec_embedding.pkl', 'rb')) dimensions = 100 trainable = False # 为训练文档中的单词创建权重矩阵 weights = zeros((vocab_size, dimensions)) # 按分词器词汇表顺序遍历单词,以确保向量位于正确的索引处 for word, i in tokenizer.word_index.items(): if word not in embedding: continue weights[i] = embedding[word] layer = Embedding(vocab_size, dimensions, weights=[weights], input_length=max_length, trainable=trainable, mask_zero=True) return layer |
我们可以在模型中直接调用 define_model() 函数来使用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# 定义图片字幕生成模型 def define_model(tokenizer, vocab_size, max_length): # 特征提取器 (编码器) inputs1 = Input(shape=(7, 7, 512)) fe1 = GlobalMaxPooling2D()(inputs1) fe2 = Dense(128, activation='relu')(fe1) fe3 = RepeatVector(max_length)(fe2) # 嵌入 inputs2 = Input(shape=(max_length,)) emb2 = load_embedding(tokenizer, vocab_size, max_length)(inputs2) emb3 = LSTM(256, return_sequences=True)(emb2) emb4 = TimeDistributed(Dense(128, activation='relu'))(emb3) # 合并输入 merged = concatenate([fe3, emb4]) # 语言模型 (解码器) lm2 = LSTM(500)(merged) lm3 = Dense(500, activation='relu')(lm2) outputs = Dense(vocab_size, activation='softmax')(lm3) # 将 [图像, 序列] 与 [词] 联系起来 model = Model(inputs=[inputs1, inputs2], outputs=outputs) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) return model model_name = 'seq_w2v_fixed' |
注意:考虑到算法或评估过程的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑多次运行示例并比较平均结果。
我们可以看到训练数据集有一些提升,测试数据集可能没有真正的显著变化。
1 2 3 4 5 6 7 8 9 |
训练 测试 计数 3.000000 3.000000 平均值 0.096780 0.047540 标准差 0.055073 0.008445 最小值 0.033511 0.038340 25% 0.078186 0.043840 50% 0.122861 0.049341 75% 0.128414 0.052140 最大值 0.133967 0.054939 |
训练好的 word2vec 嵌入与微调
我们可以重复之前的实验,并允许模型在拟合模型时调整词向量。
下面列出了允许对嵌入层进行微调的更新的 load_embedding() 函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# 加载词嵌入 def load_embedding(tokenizer, vocab_size, max_length): # 加载分词器 embedding = load(open('word2vec_embedding.pkl', 'rb')) dimensions = 100 trainable = True # 为训练文档中的单词创建权重矩阵 weights = zeros((vocab_size, dimensions)) # 按分词器词汇表顺序遍历单词,以确保向量位于正确的索引处 for word, i in tokenizer.word_index.items(): if word not in embedding: continue weights[i] = embedding[word] layer = Embedding(vocab_size, dimensions, weights=[weights], input_length=max_length, trainable=trainable, mask_zero=True) return layer model_name = 'seq_w2v_tuned' |
注意:考虑到算法或评估过程的随机性,或者数值精度的差异,您的结果可能会有所不同。考虑多次运行示例并比较平均结果。
同样,我们没有看到使用这些预训练词嵌入向量与基线模型相比有太大差异。
1 2 3 4 5 6 7 8 9 |
训练 测试 计数 3.000000 3.000000 平均值 0.065297 0.042712 标准差 0.080194 0.007697 最小值 0.017675 0.034593 25% 0.019003 0.039117 50% 0.020332 0.043641 75% 0.089108 0.046772 最大值 0.157885 0.049904 |
结果分析
我们对 Flickr8k 训练数据集中的 8,000 张图片进行了少量样本(1.6%)的实验。
样本可能太小,模型训练时间不够长,以及每个模型重复 3 次导致方差过大。这些方面也可以通过设计实验来测试和评估,例如:
- 模型技能是否随数据集大小而变化?
- 更多训练周期是否会带来更好的技能?
- 更多重复是否会带来方差较小的技能?
尽管如此,我们对如何为更完整的数据集配置模型有了一些想法。
下面是本教程中进行的实验的平均结果摘要。
回顾结果图表很有帮助。如果重复次数更多,每个分数分布的箱线图可能是一个很好的可视化。这里我们使用一个简单的条形图。请记住,BLEU 分数越大越好。
训练数据集上的结果

实验与训练数据集上模型技能的条形图
测试数据集上的结果

实验与测试数据集上模型技能的条形图
仅从测试数据集的平均结果来看,我们可以提出以下建议:
- 也许在图片特征提取器之后不需要池化(fe_flat 为 0.135231)。
- 也许在图片特征提取器之后,平均池化比最大池化更有优势(fe_avg_pool 为 0.060847)。
- 也许在子模型之后使用较小尺寸的固定长度向量是一个好主意(size_sm_fixed_vec 为 0.063148)。
- 也许在语言模型中添加更多层会带来一些好处(size_lg_lang_model 为 0.067658)。
- 也许在序列模型中添加更多层会带来一些好处(size_lg_seq_model 为 0.09697)。
我还建议探索这些建议的组合。
我们还可以查看结果分布。
下面是一些代码,用于加载每个实验的保存结果,并创建训练集和测试集结果的箱线图以供查看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
from os import listdir from pandas import read_csv from pandas import DataFrame from matplotlib import pyplot # 将所有 .csv 结果加载到数据帧中 train, test = DataFrame(), DataFrame() directory = 'results' for name in listdir(directory): if not name.endswith('csv'): continue filename = directory + '/' + name data = read_csv(filename, header=0) experiment = name.split('.')[0] train[experiment] = data['train'] test[experiment] = data['test'] # 绘制训练结果 train.boxplot(vert=False) pyplot.show() # 绘制测试结果 test.boxplot(vert=False) pyplot.show() |
训练数据集上的结果分布。

实验与训练数据集上模型技能的箱线图
测试数据集上的结果分布。

实验与测试数据集上模型技能的箱线图
对这些分布的审查表明:
- 平面结果的扩散很大;也许使用平均池化会更安全。
- 大型语言模型的扩散很大,并且向错误/危险的方向倾斜。
- 大型序列模型的扩散很大,并且向正确的方向倾斜。
- 较小的固定长度向量尺寸可能会带来一些好处。
我预计将重复次数增加到 5、10 或 30 会在某种程度上收紧这些分布。
进一步阅读
如果您想深入了解此主题,本节提供了更多资源。
论文
- 展示与讲述:一个神经图像字幕生成器, 2015.
- 展示、关注和讲述:带视觉注意力的神经图像标题生成, 2016.
- 网络中的网络, 2013.
相关字幕项目
其他
API
总结
在本教程中,你学习了如何使用图片字幕数据集的小样本来探索不同的模型设计。
具体来说,你学到了:
- 如何为照片描述建模准备数据。
- 如何设计基线和测试工具来评估模型的技能并控制其随机性。
- 如何评估模型技能、特征提取模型和词嵌入等属性,以提升模型技能。
你能想出什么实验?
你还尝试过什么?
在训练和测试数据集上你能获得最好的结果是什么?
在下面的评论中告诉我。
脱帽致敬,又一篇精彩的教程!
我很好奇 TimeDistributed 层在拼接之前如何影响数据。是否可以跳过它?另外,除了内存/计算限制之外,你使用 VGG 而不是 InceptionResNetV2 类是否有原因?
谢谢!
我选择 VGG 是因为它更小、更简单。你可以使用任何你想要的。
你可以跳过 TimeDistributed,因为我相信 Dense 现在可以支持时间步。我喜欢把它放在那里,因为它提醒我正在发生什么(例如,输出时间步)。
你好 Jason,为什么你不在与不同图像相关的输入之间重置 LSTM 状态?因为它们与相同的序列无关。
为了训练速度。
不过这是一个很好的建议,试试看它是否能提高技能!告诉我你的进展。
谢谢!为了尝试这种方法,我是否应该设置 stateful=True(避免 LSTM 自动重置)并在训练单个批次之前手动运行 model.reset_states()?(每个批次都与单个图像的序列相关)。
是的。
当你创建词汇表长度时,“vocab_size = len(tokenizer.word_index) + 1”中的 +1 的逻辑是什么?是为了留出 0 吗?
谢谢
好问题,是为了为 0 腾出空间——词汇表中的单词从 1 开始。
这是否意味着我们将索引 0 留给 'endseq' 标记?
不,开始和结束标记是问题的合法部分。模型必须指定序列何时结束。
我收到一个错误:检查输入时出错:预期 input_11 有 4 个维度,但得到的数组形状为 (28, 4096)
您能确认您的库是最新的,并且您复制了帖子中的所有代码吗?
我也遇到过这种情况。但这发生在我尝试将预训练的 word2vec 与最初定义的数据生成器等代码一起使用时。我正在尝试查看哪些修改可以使预训练的 word2vec 运行
我可以在 github 上获取代码吗?
代码在帖子中,你为什么需要它在 github 上?
你好,Jason。
非常感谢您的文章。
我有一个关于训练编码器-解码器网络的实际问题。
所以基本上我有一些序列号的图像,我想预测完整的序列号。(例如 018F6176)
所以如果我训练一个网络来预测下一个字符,我实际上必须创建一个 for 循环来预测,直到达到最大长度或停止词。
这我明白了。但是,我如何组织我的数据?
我拥有以下内容
数据是一个 numpy 浮点数组,形状为 (nb_samples 宽度,高度,nb_channels=3,)。
标签是一个 numpy 整数数组,形状为 (nb_samples, max_caption_len)
因此,如果我要构建一个具有与 create_sequences() 相同的结构的数据集,我的样本数量将增加,我如何确保图像以正确的顺序加载,以便每个图像的序列都以正确的顺序出现(或者这是否重要?)
希望你理解我的意思。
此致
尼尔斯
很好的问题。
我相信您想使用字幕模型方法。
请参阅这篇帖子,特别是标题为“逐词模型”的部分
https://machinelearning.org.cn/prepare-photo-caption-dataset-training-deep-learning-model/
它将向您展示如何准备数据以及如何思考数据。
谢谢。这正是我所寻找的。不过有一个问题。输入的顺序是否重要,例如图像 1 的所有样本按顺序出现,然后是图像 2 的所有样本等等。
因为那样我将不得不创建一个批生成器,但我希望避免这样做。
我最初的想法是这并不重要,因为你只是训练算法来识别根据之前的输入接下来会发生什么。这个假设正确吗?
此致
尼尔斯
我认为是这样,直觉上,一张照片的所有样本应该放在一起(LSTM 在批次之间具有记忆),但测试所有假设是个好主意。
好的。我使用 data_generator 和随机拆分训练、测试(图像顺序打乱)两种方式创建了模型。训练、测试模型似乎没有收敛(最大验证准确率为 17%)。然而,data_generator 模型(其中顺序保留)达到了 99.5% 的验证准确率,这非常棒。
谢谢你的时间 ????
非常好。感谢您运行此实验!
为什么 input_1 的维度是 (7, 7, 512)?
这是我们保存从照片中提取的特征的形状。
感谢您的教程,Jason。我可以运行这个教程,与关于相同数据集的教程 https://machinelearning.org.cn/develop-a-deep-learning-caption-generation-model-in-python/ 相比,后者出现了内存错误。
我比较了这两个示例,发现图像的特征提取方式不同。这个示例的特征是形状为 (7, 7, 512) 的三维数组,而另一个示例的结果是一个一维的 4,096 元素向量。这个实验的 features.pkl 文件比另一个示例大得多。造成特征形状不同的原因是什么?
也许 VGG 模型在不同的地方被裁剪了?例如,保留全连接层或丢弃它并使用 CNN 输出。我不记得了,但比较代码会更清楚。
感谢您的教程,Jason。这里您使用的是预训练 CNN 模型的特征。但我也想通过 CNN 反向传播误差,从而联合训练 CNN 和 LSTM。也就是说,通过预训练模型加载 CNN,然后进一步训练它。您能提供一个方法吗?谢谢。
是的,您可以将 CNN 作为内存中模型的一部分加载。
你好 Jason,
感谢这篇精彩的文章和这里的这篇 (https://machinelearning.org.cn/develop-a-deep-learning-caption-generation-model-in-python/)!!
我正在尝试运行您在这里发布的示例。我能够使用不同的学习率和更多时期在您的“平均池化”模型上进行训练,并将它们保存到 .h5 文件中。
然而,当我尝试使用 (https://machinelearning.org.cn/develop-a-deep-learning-caption-generation-model-in-python/) 中的代码用我训练的“平均模型”生成新标题时,我遇到了这个错误:
ValueError: Error when checking : expected input_1 to have 4 dimensions, but got array with shape (1, 4096)
只是想知道您是否知道如何使用我刚刚训练的这个 .h5 文件对另一张照片进行预测?谢谢!!
看起来您的照片特征可能维度过多。更改您的代码以直接提供照片像素。例如:photo[0]
嗨 Jason,
这篇博文很棒。真的对我很有帮助。但是,我收到了以下错误:
File “”, line 1, in
runfile(‘C:/Users/33083707/Codes/Projects/Final.py’, wdir=’C:/Users/33083707/Codes/Projects’)
File “C:\Users\33083707\Anaconda3\lib\site-packages\spyder\utils\site\sitecustomize.py”, line 866, in runfile
execfile(filename, namespace)
File “C:\Users\33083707\Anaconda3\lib\site-packages\spyder\utils\site\sitecustomize.py”, line 102, in execfile
exec(compile(f.read(), filename, 'exec'), namespace)
File “C:/Users/33083707/Codes/Projects/Final.py”, line 244, in
test_score = evaluate_model(model, test_descriptions, test_features, tokenizer, max_length)
File “C:/Users/33083707/Codes/Projects/Final.py”, line 202, in evaluate_model
bleu = corpus_bleu(actual, predicted)
File “C:\Users\33083707\Anaconda3\lib\site-packages\nltk\translate\bleu_score.py”, line 146, in corpus_bleu
p_i = modified_precision(references, hypothesis, i)
File “C:\Users\33083707\Anaconda3\lib\site-packages\nltk\translate\bleu_score.py”, line 287, in modified_precision
return Fraction(numerator, denominator, _normalize=False)
File “C:\Users\33083707\Anaconda3\lib\fractions.py”, line 186, in __new__
raise ZeroDivisionError(‘Fraction(%s, 0)’ % numerator)
ZeroDivisionError: Fraction(0, 0)
很抱歉听到这个消息,我有一些建议可以在这里尝试。
https://machinelearning.org.cn/faq/single-faq/why-does-the-code-in-the-tutorial-not-work-for-me
Jason博士您好,
我想在我自己的数据上尝试这个,但我不知道如何准备它。我有一个图像文件夹和一个 CSV 文件。CSV 文件中的每一行都包含一个图像 ID/名称和一个描述该图像的标题。关于如何进行,有什么建议吗?
谢谢你
首先编写代码将图像加载到内存中。
也许你可以使用 PIL 或 Pillo 来加载图像?
在代码的以下部分
# 运行实验
train_results, test_results = list(), list()
for i in range(n_repeats)
# 定义模型
model = define_model(vocab_size, max_length)
# 拟合模型
model.fit_generator(data_generator(train_descriptions, train_features, tokenizer, max_length, n_photos_per_update), steps_per_epoch=n_batches_per_epoch, epochs=n_epochs, verbose=verbose)
# evaluate model on training data
train_score = evaluate_model(model, train_descriptions, train_features, tokenizer, max_length)
test_score = evaluate_model(model, test_descriptions, test_features, tokenizer, max_length)
# 存储
train_results.append(train_score)
test_results.append(test_score)
print(‘>%d: train=%f test=%f’ % ((i+1), train_score, test_score))
你为什么在 for 循环中定义模型?它不应该定义在循环外部吗?
不,我希望每次重复都有一个新模型(随机权重)。
我为什么要定义在循环外部?
您能解释一下为什么吗?我认为模型必须被定义,并且权重通过从示例中学习来训练。但是每次定义一个新模型都会删除之前学习到的权重。我哪里错了?
是的,我们希望将其丢弃。我们每次都用一个模型重复实验,这样我们就可以看到模型在我们的数据上的平均表现。而不是任何一个随机拟合模型的表现。
你可以在这里了解更多
https://machinelearning.org.cn/evaluate-skill-deep-learning-models/
嗨,Jason,非常感谢。
我尝试使用 DenseNet-121 模型而不是 VGG16 模型来提取特征。但我得到了一个错误,例如:
ValueError: Error when checking input: expected input_1 to have 4 dimensions, but got array with shape (21, 1024)。
您能给我一些关于这个的建议吗?
这是因为两个 CNN 模型的最后一层输出维度不同,你从中提取图像特征。最好的猜测是将 DenseNet 模型的输出维度转换为与 VGG-16 相同的维度。
此外,如果你使用 Flatten() 而不是 GlobalMaxPooling2D(),我想你就不会遇到同样的问题。
不完全是,我不了解你正在尝试的事情。
嗨 Jason
一如既往,感谢您的精彩教程
我得到了 70 个时期和 3 次重复的结果
似乎当我们有大量时期时,准确性会提高
但我不明白为什么我得到 train 和 test = 0
这是我的结果
时期 70/70
– 14s – 损失:1.8873 – 准确率:0.4100
实际:startseq 孩子和女人在大城市的水边 endseq
预测:startseq 孩子女人在边缘边缘在大 endseq
实际:startseq 拿着棍子的男孩跪在守门员网前 endseq
预测:startseq 男孩男孩在在前面和和和的的在前面男孩前面和的在前面
实际:startseq 女人蹲在田野里三只狗旁边 endseq
预测:startseq 两只狗在旁边旁边旁边旁边旁边旁边旁边旁边旁边旁边旁边旁边旁边旁边旁边旁边旁边旁边旁边旁边
实际:startseq 男孩在外面坐着时用力咬零食 endseq
预测:startseq 男孩咬坐着同时同时外面 endseq
实际:startseq 人边看小电视边吃外卖 endseq
预测:startseq 人吃同时外卖小小电视 endseq
实际:startseq 一对夫妇带着裹在毯子里的幼儿坐在水泥台阶上 endseq
预测:startseq 男孩女孩在在在自行车 endseq
实际:startseq 成人和儿童站在靠近林区台阶前玩耍 endseq
预测:startseq 男孩和年轻男孩和和和和和和和和和和和和和和和和和和和
实际:startseq 穿灰色睡衣的男孩在沙发上跳 endseq
预测:startseq 男孩男人如果粗糙 endseq
实际:startseq 男孩拿着厨具做出威胁性的表情 endseq
预测:startseq 女孩拿着短裤短裤短裤和她 endseq
实际:startseq 戴绿帽子的男人在高处 endseq
预测:startseq 男孩在和和波浪 endseq
>3:train=0.000000 test=0.000000
训练 测试
计数 3.000000e+00 3.000000e+00
平均值 6.038250e-02 4.013559e-155
标准差 1.045856e-01 2.160777e-156
最小值 2.413005e-78 3.808820e-155
25% 2.799294e-78 3.900627e-155
50% 3.185584e-78 3.992435e-155
75% 9.057374e-02 4.115929e-155
最大值 1.811475e-01 4.239423e-155
此外,我收到了这个警告
用户警告
假设包含 0 个 3-gram 重叠。
因此,BLEU 分数评估为 0,独立于
它包含的较低阶 N-gram 重叠的数量。
考虑使用较低的 n-gram 阶或使用 SmoothingFunction()
warnings.warn(_msg)
您介意就我的实验提出您的意见并为警告提供更好的解决方案吗?
谢谢
干得好!
当某些预测比预期短时,BLEU 可能会发出警告。这可能有助于更好地理解 BLEU 分数计算
https://machinelearning.org.cn/calculate-bleu-score-for-text-python/
图像字幕模型所能达到的最高准确率是多少?
我们不衡量准确性,而是衡量困惑度或 BLEU 分数。
最佳分数将取决于所使用的特定基准数据集。
嗨,杰森,你的文章很好。
但我无法获得高于 0.3 的准确率。
我也尝试增加 epoch,但准确率没有变化。
您能帮助我将准确率提高到至少 0.7 或 0.8 吗?
我不建议将准确性用于字幕生成,而是使用 BLEU 分数或困惑度。
我这里有一些关于改进深度学习模型的通用建议
https://machinelearning.org.cn/start-here/#better
嗨,Jason!我无法下载数据集。Framing_Image_Description。404 未找到
谢谢,我已添加了数据集的直接链接。
您好,先生,我看了您的博客“如何从零开始开发深度学习照片字幕生成器”。在该教程中,您没有使用 RepeatVector 和 TimeDistributed 函数。在此教程中,您使用了这两个函数。由于我是深度学习新手,您能通过解释 RepeatVector 和 TimeDistributed 函数的用法来帮助我吗?如果我们不应用这些函数,会影响模型性能吗?
谢谢,
Ankit
您可以选择任何您想要的模型架构。没有一个最好的模型。
也许是另一篇文章中的那个,或者是你自己设计的,或者两者都测试一下,看看哪个最适合你?
杰森,很棒的教程!我想问一个关于 CNN 的问题:教程中 CNN 使用的窗口大小是多少?
你说的窗口大小具体是什么意思?
还可以尝试什么来进一步提高 BLEU 分数?
我在这里有一些建议
https://machinelearning.org.cn/improve-deep-learning-performance/
我的 pyplot 库运行不正常。请提供关于在 Python 中安装和设置 pyplot 库的详细教程。提前感谢
也许暂时注释掉 plot_model 行?
n_repeats = 3 是什么意思?我是说,如果我将 n_repeats = 1,会有什么不同?
实验重复的次数,结果会取平均值。
更多的重复次数将更好地反映该方法的预期性能。
改变 CNN 架构能提高 BLUE 分数吗?
其次,我如何解释这些结果?请深入概述一下…哪些分数应该高才能获得更好的模型性能?
>3:train=0.000000 test=0.000000
训练 测试
计数 3.000000e+00 3.000000e+00
平均值 1.219064e-78 3.030008e-155
标准差 2.247343e-79 8.613845e-156
最小值 1.078167e-78 2.152162e-155
25% 1.089478e-78 2.608047e-155
50% 1.100788e-78 3.063932e-155
75% 1.289512e-78 3.468930e-155
最大值 1.478235e-78 3.873929e-155
也许尝试一些更改并比较结果?
Jason,我有一些问题。请再解释清楚一些。
在教程的“固定长度向量大小”部分。
fe2 = Dense(64/128/256, activation=’relu’)(fe1)
1) 64/128/256 代表什么?是照片特征向量还是其他什么?
2) 我们如何以及为什么要增加或减少特征向量?
3) VGG16 的输出在这种情况下是 4096 维向量……那么我们把(图像)特征作为模型的输入在哪里?
3) 为什么我们取 64 的倍数?为什么我们不能随机取?
这是层中的节点数量。
你说的增加/减少特征向量是什么意思?你是说照片的特征向量的长度吗?如果是这样,也许可以尝试使用不同的预训练模型。
特征向量是 X1,也许再读一遍教程?
64 的倍数在模型节点中很常见,没有特别的原因。
那么,我们可以改变 vgg16 中节点的数量吗?如果可以,请解释 vgg16 的哪一层中的节点正在改变?
其次,请告诉我我们是否在实验中使用验证集?
您要么指定使用验证数据集,要么不使用。
也许我不理解你的问题?
是的,但是您需要重新拟合模型。
我的问题是,我们只是将 VGG16 的哪一层的节点数量从 128 更改为 64/256?
vgg_16 有 16 层,对吧……那么在这个教程中,哪一层节点正在改变?
如果你想改变层数或层中节点的数量,那就去改变吧。
我们在这个教程中没有做这些改变。要了解更多关于改变节点和层的信息,请参阅这个教程。
https://machinelearning.org.cn/how-to-control-neural-network-model-capacity-with-nodes-and-layers/
其次,教程中是否使用了不同的验证和测试数据集?如果没有,该如何操作?
是的,我们为训练和测试使用了不同的数据集,更多关于区别的信息在这里
https://machinelearning.org.cn/difference-test-validation-datasets/
我如何知道我的训练是正确的?我是说,我如何确认我的模型学习得很好?
使用鲁棒的测试工具,如 K 折交叉验证进行评估。
或者一个大的训练/测试拆分。
我该如何使用 K 折交叉验证?您有任何教程可以给我提供一些思路吗?
这会有帮助
https://machinelearning.org.cn/k-fold-cross-validation/
我的测试数据集上的 BLUE 分数很低,接近于零。我的训练出了什么问题?我的训练平均分是 0.194386,而我的测试平均分是 0.01468624
也许您的模型过度拟合了训练数据集?
也许可以尝试较小的学习率或提前停止训练等等?
我如何更新教程的当前代码,以便我可以对我的数据集进行 K 折交叉验证,然后在此基础上评估我的模型性能(以确保低偏置或无偏置)?
也许这会有帮助。
https://machinelearning.org.cn/k-fold-cross-validation/
在上一篇文章中,您使用了形状为 4096 的一维图像特征,而在这篇文章中,您使用了形状为 (7,7,512) 的三维图像特征……为什么?这两种不同的特征有什么区别?它们都是 VGG 特征,但来自不同的层吗?
链接的帖子介绍了最普遍意义上的k折交叉验证。
请再检查一下我的问题……我问的是图像特征维度……而不是k折
你好 Jason,
当我应用您的代码时,这一部分
我收到“NotImplementedError: 无法将符号张量 (args_2:0) 转换为numpy数组。”
我不确定如何解决这个问题。您能帮我解决吗?
谢谢你。
很抱歉听到这个消息,这可能会有所帮助。
https://machinelearning.org.cn/faq/single-faq/why-does-the-code-in-the-tutorial-not-work-for-me
当我尝试将Tensorflow从2.1降级到1.14时,它奏效了。我的意思是代码没有错误提示就运行了。但我不能确定它是否完全正常工作。因为结果没有给我附加的train_results和test_results。它只是空白。由于它是一个for循环,它应该重复运行,结果也应该相应地附加。我检查了您的建议,我相信我做到了所有。
有什么线索可以解释为什么它不能顺利运行吗?
谢谢你。
您是从命令行运行的吗(不是笔记本或IDE)
https://machinelearning.org.cn/faq/single-faq/how-do-i-run-a-script-from-the-command-line