作者 | dylan wenzlau
来源 | Medium
编辑 | 代码医生团队
本文介绍如何构建深度转换网络实现端到端的文本生成。在这一过程中,包括有关数据清理,训练,模型设计和预测算法相关的内容。
第1步:构建训练数据
数据集使用了Imgflip Meme Generator(一款根据文本生成表情包的工具)用户的~100M公共memes标题。为了加速训练并降低模型的复杂性,仅使用48个最受欢迎的Meme(表情包)和每个Meme(表情包)准确的20,000个字幕,总计960,000个字幕作为训练数据。每个角色都会有一个训练示例在标题中,总计约45,000,000个训练样例。这里选择了角色级生成而不是单词级别,因为Meme(表情包)倾向于使用拼写和语法。此外字符级深度学习是单词级深度学习的超集,因此如果有足够的数据并且模型设计足以了解所有复杂性,则可以实现更高的准确性。如果尝试下面的完成模型,还会看到char级别可以更有趣!
https://imgflip.com/memegenerator
以下是第一个Meme(表情包)标题是“制作所有memes”时的训练数据。省略了从数据库中读取代码并执行初始清理的代码,因为它非常标准,可以通过多种方式完成。
training_data = [
["000000061533 0 ", "m"],
["000000061533 0 m", "a"],
["000000061533 0 ma", "k"],
["000000061533 0 mak", "e"],
["000000061533 0 make", "|"],
["000000061533 1 make|", "a"],
["000000061533 1 make|a", "l"],
["000000061533 1 make|al", "l"],
["000000061533 1 make|all", " "],
["000000061533 1 make|all ", "t"],
["000000061533 1 make|all t", "h"],
["000000061533 1 make|all th", "e"],
["000000061533 1 make|all the", " "],
["000000061533 1 make|all the ", "m"],
["000000061533 1 make|all the m", "e"],
["000000061533 1 make|all the me", "m"],
["000000061533 1 make|all the mem", "e"],
["000000061533 1 make|all the meme", "s"],
["000000061533 1 make|all the memes", "|"],
... 45 million more rows here ...
]
# we'll need our feature text and labels as separate arrays later
texts = [row[0] for row in training_data]
labels = [row[1] for row in training_data]
像机器学习中的大多数事情一样,这只是一个分类问题。将左侧的文本字符串分类为~70个不同的buckets 中的一个,其中buckets 是字符。
解压缩格式:
在训练之前,数据使用了几种清洗技术:
数据现在已准备就绪,可以输入神经网络!
第2步:数据转换
首先,在代码中导入python库:
from keras import Sequential
from keras.preprocessing.sequence import pad_sequences
from keras.callbacks import ModelCheckpoint
from keras.layers import Dense, Dropout, GlobalMaxPooling1D, Conv1D, MaxPooling1D, Embedding
from keras.layers.normalization import BatchNormalization
import numpy as np
import util # util is a custom file I wrote, see github link below
因为神经网络只能对张量(向量/矩阵/多维数组)进行操作,所以需要对文本进行转化。每个训练文本将通过从数据中找到的约70个唯一字符的数组中用相应的索引替换每个字符,将其转换为整数数组(等级1张量)。字符数组的顺序是任意的,但选择按字符频率对其进行排序,以便在更改训练数据量时保持大致一致。Keras有一个Tokenizer类,可以使用它(使用char_level = True),这里使用的是自己的util函数,因为它比Keras tokenizer更快。
# output: {' ': 1, '0': 2, 'e': 3, ... }
char_to_int = util.map_char_to_int(texts)
# output: [[2, 2, 27, 11, ...], ... ]
sequences = util.texts_to_sequences(texts, char_to_int)
labels = [char_to_int[char] for char in labels]
这些是数据按频率顺序包含的字符:
0etoains|rhl1udmy2cg4p53wf6b897kv."!?j:x,*"z-q/&$)(#%+_@=>;<][~`^}{\
接下来将填充带有前导零的整数序列,因此它们的长度都相同,因为模型的张量数学要求每个训练示例的形状相同。(注意:可以在这里使用低至100的长度,因为文本只有100个字符,但希望以后所有的池操作都可以被2完全整除。)
SEQUENCE_LENGTH = 128
data = pad_sequences(sequences, maxlen=SEQUENCE_LENGTH)
最后将调整训练数据并将其分为训练和验证集。改组(随机化顺序)确保数据的特定子集不总是用于验证准确性的子集。将一些数据拆分成验证集使能够衡量模型在不允许它用于训练的示例上的表现。
# randomize order of training data
indices = np.arange(data.shape[0])
np.random.shuffle(indices)
data = data[indices]
labels = labels[indices]
# validation set can be much smaller if we use a lot of data
validation_ratio = 0.2 if data.shape[0] < 1000000 else 0.02
num_validation_samples = int(validation_ratio * data.shape[0])
x_train = data[:-num_validation_samples]
y_train = labels[:-num_validation_samples]
x_val = data[-num_validation_samples:]
y_val = labels[-num_validation_samples:]
第3步:模型设计
这里选择使用卷积网络,在Keras上构建conv网络模型的代码如下:
EMBEDDING_DIM = 16
model = Sequential()
model.add(Embedding(len(char_to_int) + 1, EMBEDDING_DIM, input_length=SEQUENCE_LENGTH))
model.add(Conv1D(1024, 5, activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(MaxPooling1D(2))
model.add(Dropout(0.25))
model.add(Conv1D(1024, 5, activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(MaxPooling1D(2))
model.add(Dropout(0.25))
model.add(Conv1D(1024, 5, activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(MaxPooling1D(2))
model.add(Dropout(0.25))
model.add(Conv1D(1024, 5, activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(MaxPooling1D(2))
model.add(Dropout(0.25))
model.add(Conv1D(1024, 5, activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(GlobalMaxPooling1D())
model.add(Dropout(0.25))
model.add(Dense(1024, activation='relu'))
model.add(BatchNormalization())
model.add(Dropout(0.25))
model.add(Dense(len(labels_index), activation='softmax'))
model.compile(loss='sparse_categorical_crossentropy', optimizer='rmsprop', metrics=['acc'])
代码步骤如下:
首先,模型使用Keras嵌入将每个输入示例从128个整数的数组(每个表示一个文本字符)转换为128x16矩阵。嵌入是一个层,它学习将每个字符转换为表示为整数的最佳方式,而不是表示为16个浮点数的数组[0.02, ..., -0.91]。这允许模型通过在16维空间中将它们彼此靠近地嵌入来了解哪些字符的使用类似,并最终提高模型预测的准确性。
接下来,添加5个卷积层,每个层的内核大小为5,1024个过滤器,以及ReLU激活。从概念上讲,第一个转换层正在学习如何从字符构造单词,后来的层正在学习构建更长的单词和单词链(n-gram),每个单词都比前一个更抽象。
在所有转换图层之后,使用全局最大合并图层,它与普通的最大合并图层相同,只是它会自动选择缩小输入尺寸以匹配下一图层的大小。最后一层只是标准的密集(完全连接)层,有1024个神经元,最后是70个神经元,因为分类器需要为70个不同的标签输出概率。
model.compile步骤非常标准。RMSprop优化器是一个不错的优化器,没有尝试为这个神经网络改变它。loss=sparse_categorical_crossentropy告诉希望它优化的模型,以便在一组2个或更多类别(又名标签)中选择最佳类别。“稀疏”部分指的是标签是0到70之间的整数,而不是长度为70的一个one-hot阵列。使用一个one-hot阵列作为标签需要更多的内存,更多的处理时间,并且不会影响模型的准确性。不要使用一个one-hot标签!
Keras有一个很好的model.summary()功能,可以查看模型:
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_1 (Embedding) (None, 128, 16) 1136
_________________________________________________________________
conv1d_1 (Conv1D) (None, 128, 1024) 82944
_________________________________________________________________
batch_normalization_1 (Batch (None, 128, 1024) 4096
_________________________________________________________________
max_pooling1d_1 (MaxPooling1 (None, 64, 1024) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 64, 1024) 0
_________________________________________________________________
conv1d_2 (Conv1D) (None, 64, 1024) 5243904
_________________________________________________________________
batch_normalization_2 (Batch (None, 64, 1024) 4096
_________________________________________________________________
max_pooling1d_2 (MaxPooling1 (None, 32, 1024) 0
_________________________________________________________________
dropout_2 (Dropout) (None, 32, 1024) 0
_________________________________________________________________
conv1d_3 (Conv1D) (None, 32, 1024) 5243904
_________________________________________________________________
batch_normalization_3 (Batch (None, 32, 1024) 4096
_________________________________________________________________
max_pooling1d_3 (MaxPooling1 (None, 16, 1024) 0
_________________________________________________________________
dropout_3 (Dropout) (None, 16, 1024) 0
_________________________________________________________________
conv1d_4 (Conv1D) (None, 16, 1024) 5243904
_________________________________________________________________
batch_normalization_4 (Batch (None, 16, 1024) 4096
_________________________________________________________________
max_pooling1d_4 (MaxPooling1 (None, 8, 1024) 0
_________________________________________________________________
dropout_4 (Dropout) (None, 8, 1024) 0
_________________________________________________________________
conv1d_5 (Conv1D) (None, 8, 1024) 5243904
_________________________________________________________________
batch_normalization_5 (Batch (None, 8, 1024) 4096
_________________________________________________________________
global_max_pooling1d_1 (Glob (None, 1024) 0
_________________________________________________________________
dropout_5 (Dropout) (None, 1024) 0
_________________________________________________________________
dense_1 (Dense) (None, 1024) 1049600
_________________________________________________________________
batch_normalization_6 (Batch (None, 1024) 4096
_________________________________________________________________
dropout_6 (Dropout) (None, 1024) 0
_________________________________________________________________
dense_2 (Dense) (None, 70) 71750
=================================================================
Total params: 22,205,622
Trainable params: 22,193,334
Non-trainable params: 12,288
_________________________________________________________________
在调整上面讨论的超参数时,关注模型的参数计数很有用,它大致代表模型的学习能力总量。
第4步:训练
现在将让模型训练并使用“检查点”来保存历史和最佳模型,以便可以在训练期间的任何时候检查进度并使用最新模型进行预测。
# the path where you want to save all of this model's files
MODEL_PATH = '/home/ubuntu/imgflip/models/conv_model'
# just make this large since you can stop training at any time
NUM_EPOCHS = 48
# batch size below 256 will reduce training speed since
# CPU (non-GPU) work must be done between each batch
BATCH_SIZE = 256
# callback to save the model whenever validation loss improves
checkpointer = ModelCheckpoint(filepath=MODEL_PATH + '/model.h5', verbose=1, save_best_only=True)
# custom callback to save history and plots after each epoch
history_checkpointer = util.SaveHistoryCheckpoint(MODEL_PATH)
# the main training function where all the magic happens!
history = model.fit(x_train, y_train, validation_data=(x_val, y_val), epochs=NUM_EPOCHS, batch_size=BATCH_SIZE, callbacks=[checkpointer, history_checkpointer])
这就是坐下来观看神奇数字在几个小时内上升的地方......
Train on 44274928 samples, validate on 903569 samples
Epoch 1/48
44274928/44274928 [==============================] - 16756s 378us/step - loss: 1.5516 - acc: 0.5443 - val_loss: 1.3723 - val_acc: 0.5891
Epoch 00001: val_loss improved from inf to 1.37226, saving model to /home/ubuntu/imgflip/models/gen_2019_04_04_03_28_00/model.h5
Epoch 2/48
44274928/44274928 [==============================] - 16767s 379us/step - loss: 1.4424 - acc: 0.5748 - val_loss: 1.3416 - val_acc: 0.5979
Epoch 00002: val_loss improved from 1.37226 to 1.34157, saving model to /home/ubuntu/imgflip/models/gen_2019_04_04_03_28_00/model.h5
Epoch 3/48
44274928/44274928 [==============================] - 16798s 379us/step - loss: 1.4192 - acc: 0.5815 - val_loss: 1.3239 - val_acc: 0.6036
Epoch 00003: val_loss improved from 1.34157 to 1.32394, saving model to /home/ubuntu/imgflip/models/gen_2019_04_04_03_28_00/model.h5
Epoch 4/48
44274928/44274928 [==============================] - 16798s 379us/step - loss: 1.4015 - acc: 0.5857 - val_loss: 1.3127 - val_acc: 0.6055
Epoch 00004: val_loss improved from 1.32394 to 1.31274, saving model to /home/ubuntu/imgflip/models/gen_2019_04_04_03_28_00/model.h5
Epoch 5/48
1177344/44274928 [..............................] - ETA: 4:31:59 - loss: 1.3993 - acc: 0.5869
发现当训练损失/准确性比验证损失/准确性更差时,这表明该模型学习良好且不过度拟合。
如果使用AWS服务器进行训练,发现最佳实例为p3.2xlarge。这使用了自2019年4月以来最快的GPU(Tesla V100),并且该实例只有一个GPU,因为模型无法非常有效地使用多个GPU。确实尝试过使用Keras的multi_gpu_model,但它需要使批量大小更大,以实际实现速度提升,这可能会影响模型的收敛能力,即使使用4个GPU也几乎不会快2倍。带有4个GPU的p3.8xlarge的成本是4倍。
第5步:预测
现在有一个模型可以输出meme标题中下一个字符应该出现的概率,但是如何使用它来实际创建一个完整的meme(表情包)标题?
基本前提是用想要为其生成文本的Memes(表情包标题)初始化一个字符串,然后model.predict为每个字符调用一次,直到模型输出结束文本字符的|次数与文本框中的文本框一样多次。对于上面看到的“X All The Y”memes,默认的文本框数为2,初始文本为:
"000000061533 0 "
考虑到模型输出的70个概率,尝试了几种不同的方法来选择下一个字符:
这里选择使用方法2,因为速度快,效果好。以下是一些随机生成的例子:
在imgflip.com/ai-meme的48个Meme(表情包)中生成。
https://imgflip.com/ai-meme
使用方法2进行运行时预测的代码如下。Github上的完整实现是一种通用的Beam搜索算法,因此只需将波束宽度增加到1以上即可启用Beam搜索。
# min score as percentage of the maximum score, not absolute
MIN_SCORE = 0.1
int_to_char = {v: k for k, v in char_to_int.items()}
def predict_meme_text(template_id, num_boxes, init_text = ''):
template_id = str(template_id).zfill(12)
final_text = ''
for char_count in range(len(init_text), SEQUENCE_LENGTH):
box_index = str(final_text.count('|'))
texts = [template_id + ' ' + box_index + ' ' + final_text]
sequences = util.texts_to_sequences(texts, char_to_int)
data = pad_sequences(sequences, maxlen=SEQUENCE_LENGTH)
predictions_list = model.predict(data)
predictions = []
for j in range(0, len(predictions_list[0])):
predictions.append({
'text': final_text + int_to_char[j],
'score': predictions_list[0][j]
})
predictions = sorted(predictions, key=lambda p: p['score'], reverse=True)
top_predictions = []
top_score = predictions[0]['score']
rand_int = random.randint(int(MIN_SCORE * 1000), 1000)
for prediction in predictions:
# give each char a chance of being chosen based on its score
if prediction['score'] >= rand_int / 1000 * top_score:
top_predictions.append(prediction)
random.shuffle(top_predictions)
final_text = top_predictions[0]['text']
if char_count >= SEQUENCE_LENGTH - 1 or final_text.count('|') == num_boxes - 1:
return final_text
在github中,该文档对应的代码如下:
https://github.com/dylanwenzlau/ml-scripts/tree/master/meme_text_gen_convnet
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有