目录
1. Embedding与变长输入处理
1.1 Embedding
1.1.1 One-hot编码
Word——> index ——>[0,0,0,…,0,1,0,…,0]
1.1.2 Dense embedding
Word——>index——>[1.2, 4.2, 2.9, …,0.1]
才开始[1.2, 4.2, 2.9, …,0.1]是随机的,训练过之后就可以代表word了。
1.2 变长输入
假设要求输入的长度都10
1.2.1 Padding
Word index:[3, 2, 5, 9, 1, 8, 6, 3]
Padding: [3, 2, 5, 9, 1, 8, 6, 3, 0, 0]
1.2.2 cut
Word index:[3, 2, 5, 9, 1, 8, 6, 3, 2, 5, 9, 1]
cut: [3, 2, 5, 9, 1, 8, 6, 3, 2, 5]
1.2.3 合并
MLP:MultiLayer Perceptron,即多层的全连接神经网络。输入的长度需要是固定的。
例子中“慕课网”“TF2.0”“实战”“值得”“拥有”总共有5个词,假设每个词embedding之后是长度为10的向量,输出组合为5*10;假如有n个词呢?输出组合为n*10,此时,输出组合就不固定了。为了解决这个问题,可以使用合并(比如使用global average pooling),将每个词向量的每一个维度合并,最终输出一个长度为10的向量。
1.3实战
这里使用skleran中的一个电影评价的数据集
1.3.1 载入数据集
num_words:通过设置 num_words来设置词表的个数,比如:把num_words限制在10000,会按照频次进行统计,统计出的词语前10000个作为词表,保留下来,后边的被当作特殊字符处理。
index_from 从几开始算
train_data中存储的是word的id,比如
index_from = 0,train_data[0] = [1, 11, 19, 13, 40, ......]
index_from = 3,train_data[0] = [1, 14, 22, 16, 43, ......]
注意:每个元素的第一个id都是1,不变
imdb = keras.datasets.imdb
vocab_size = 10000
index_from = 3
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(
num_words = vocab_size, index_from = index_from)
1.3.2 载入数据集的词表
word_index = imdb.get_word_index()
对词表进行修正,并输出train_data中的id输出单词
# 由于我们的index_from = 3, 所以word_index 中的value(index) 需要后移3
word_index = {k: (v+3) for k, v in word_index.items()}
# 将补充符<PAD>,开始符<START>,结束符<END>,未知符<UNK>填在前面3位
word_index['<PAD>'] = 0
word_index['<START>'] = 1
word_index['<UNK>'] = 2
word_index['<END>'] = 3
# 翻译train_data,因为train_data中存储的都是index
reverse_word_index = dict([(v, k) for k, v in word_index.items()])
def decode_review(text_ids):
return " ".join(
[reverse_word_index.get(word_id, "<UNK>") for word_id in text_ids])
print("train_data[0]", train_data[0])
print("decode train_data[0]", decode_review(train_data[0]))
1.3.3 对变长输入做处理
padding
# padding
max_length = 500
train_data = keras.preprocessing.sequence.pad_sequences(
train_data, # list of list
value = word_index['<PAD>'],
padding = 'post', # poost: 表示padding补在最后, pre:表示padding补充在前面
maxlen= max_length)
test_data = keras.preprocessing.sequence.pad_sequences(
test_data, # list of list
value = word_index['<PAD>'],
padding = 'post', # poost: 表示padding补在最后, pre:表示padding补充在前面
maxlen= max_length)
print(train_data[0])
1.3.4 构建模型
对变长做处理——合并
embedding_dim = 16
batch_size = 128
model = keras.models.Sequential([
# 1. define matrix: [vocab_size, embeding_dim]
# 2. train_data[*], max_length * embedding_dim
# 3. batch_size * max_length * embedding_dim
keras.layers.Embedding(vocab_size, embedding_dim,
input_length = max_length),
# 合并(注:输入已经是定长的了,可以将其变为batch_size * (max_length * embedding_dim)
# 所以合并可要可不要,这里只是为了演示)
# batch_size * max_length * embedding_dim ——> batch_size * embedding_dim
keras.layers.GlobalAveragePooling1D(),
keras.layers.Dense(64, activation = 'relu'),
keras.layers.Dense(1, activation = 'sigmoid'),
])
model.summary()
训练
model.compile(optimizer = 'adam', loss = 'binary_crossentropy',
metrics = ['accuracy'])
# validation_split = 0.2, 表示训练数据集中有20%的被用作验证数据集
history = model.fit(train_data, train_labels,
epochs = 30,
batch_size = batch_size,
validation_split = 0.2)
评估
# 将loss 和 acc 分开两个图画,plot_key 可取 loss 或 acc
def plot_learning_curves(history, plot_key, epochs, min_value, max_value):
data = {}
data[plot_key] = history.history[plot_key]
data['val_' + plot_key] = history.history['val_' + plot_key]
pd.DataFrame(data).plot(figsize=(8, 5))
plt.grid(True)
plt.axis([0, epochs, min_value, max_value])
plt.show()
plot_learning_curves(history, 'accuracy', 30, 0.5, 1)
plot_learning_curves(history, 'loss', 30, 0, 1)
测试
model.evaluate(
test_data, test_labels,
batch_size = batch_size,
)
1.4 变长输入处理——padding + 合并的缺点
1.4.1 信息丢失
(1)pad噪音
在均值合并的时候,每个句子里的pad也都参与了计算,相当于真实的句子来说,它们即为噪音,噪音过多,会把真实的意思给稀释掉。
(2)多个embeding合并,无主次
即便句子中全部都是真实的词语,没有pad,直接去合并,没有主次,对于分类来说,某些词语对分类的结果,影响会更大。比如说一些表达情感的词语。类似于主语,或者连接词(the and等) 就没有那么重要,如果直接做合并,就会使它们没有主次,造成真实的信息被稀释掉,即造成信息的丢失,最后的结果可能会不准确。
1.4.2 无效计算太多,低效
太多的padding。比如做线上测试的时候,最长的句子是500,但其中有一个句子A的长度是20,为了保持句子的长度一样,故需要对句子A做480个padding,而这480个padding都是要参与到计算中去的,因此这个就大大的降低了计算的速度。
故需要引入循环神经网络.
2. 循环神经网路
2.1 为什么需要循环神经网络——序列式问题
序号 | 模型描述 | 输入输出形式 | 输入 | 输出 |
(1) | 普通神经网络 (anilla neural networks) | 1对1 | 一张图片 | 某个大类 |
(2) | 图片生成描述 | 1对多 | 一张图片 | 一个句子 |
(3) | 文本分类 | 多对1 | 一个文本 | 积极”/“消极” |
(4) | 机器翻译(非实时,输入完了,才会有输出) | 多对多 | 中文句子 | 英文句子 |
(5) | 视频解说(实时) | 多对多 | 一帧一帧的图像 | 一个一个句子 |
注:机器翻译中的非实时即为输入完了,才有输出。
视频解说中的实时为,有一帧图像之后,生成第一个句子,有第二帧图像之后,生成第二个句子。
2.2 循环神经网络的结构
注:每一步使用同样的激活函数和参数,即每一步的W和U都是一样的
2.3 循环神经网络举例
2.3.1 字符语言模型
在做序列式预测的时候,会有一些风险,比如说在开头预测错的时候,后边就是全错的。
2.3.2 循环神经网络做文本分类
和之前文本分类网络结构(见1.2.3合并)不一样的是,不需要对每一个的输出作合并,只是用最后一步的输出来做MLP的输入。因为在循环神经网路中,会维护一个state,这个state包含了句子的所有信息,因而,在最后的时候,会认为state是拥有整个句子的信息的。
箭头是双向的,表明其是双向的循环神经网络。双向循环神经网络和单项循环神经网络的区别是,会多维护一个状态。S1表明是从前到后的一个状态,S2表明是从后向前的一个状态。
这样,整个模型中就会包含上下文信息。
2.4实战
2.4.1文本分类
(1) 载入skleran中的数据集imdb(有关电影评价的一个数据集,分正面和负面)
imdb = keras.datasets.imdb
vocab_size = 10000
index_from = 3
# 通过设置 num_words来设置词表的个数,比如:把num_words限制在10000,会按照频次进行统计,
# 统计出的词语前10000个作为词表,保留下来,后边的被当作特殊字符处理。
# index_from 从几开始算.
# train_data中存储的是word的id,比如index_from = 0,train_data[0] = [1, 11, 19, 13, 40, ......]
# index_from = 3,train_data[0] = [1, 14, 22, 16, 43, ......]
# 注意:每个元素的第一个id都是1,不变
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(
num_words = vocab_size, index_from = index_from)
载入数据集词表
word_index = imdb.get_word_index()
对词表进行修正
# 由于我们的index_from = 3, 所以word_index 中的value(index) 需要后移3
word_index = {k: (v+3) for k, v in word_index.items()}
# 将补充符<PAD>,开始符<START>,结束符<END>,未知符<UNK>填在前面3位
word_index['<PAD>'] = 0
word_index['<START>'] = 1
word_index['<UNK>'] = 2
word_index['<END>'] = 3
# 翻译train_data,因为train_data中存储的都是index
reverse_word_index = dict([(v, k) for k, v in word_index.items()])
def decode_review(text_ids):
return " ".join(
[reverse_word_index.get(word_id, "<UNK>") for word_id in text_ids])
print("train_data[0]", train_data[0])
print("decode train_data[0]", decode_review(train_data[0]))
对变长输入做处理
# padding
max_length = 500
train_data = keras.preprocessing.sequence.pad_sequences(
train_data, # list of list
value = word_index['<PAD>'],
padding = 'post', # poost: 表示padding补在最后, pre:表示padding补充在前面
maxlen= max_length)
test_data = keras.preprocessing.sequence.pad_sequences(
test_data, # list of list
value = word_index['<PAD>'],
padding = 'post', # poost: 表示padding补在最后, pre:表示padding补充在前面
maxlen= max_length)
(2)构建模型
单层单向RNN
embedding_dim = 16
batch_size = 128
single_rnn_model = keras.models.Sequential([
# 1. define matrix: [vocab_size, embeding_dim]
# 2. train_data[*], max_length * embedding_dim
# 3. batch_size * max_length * embedding_dim
keras.layers.Embedding(vocab_size, embedding_dim,
input_length = max_length),
keras.layers.SimpleRNN(units = 64, return_sequences = False),
keras.layers.Dense(64, activation = 'relu'),
keras.layers.Dense(1, activation = 'sigmoid'),
])
single_rnn_model.summary()
训练
single_rnn_model.compile(optimizer = 'adam',
loss = 'binary_crossentropy',
metrics = ['accuracy'])
# validation_split = 0.2, 表示训练数据集中有20%的被用作验证数据集
history = single_rnn_model.fit(train_data, train_labels,
epochs = 30,
batch_size = batch_size,
validation_split = 0.2)
查看训练结果
# 将loss 和 acc 分开两个图画,plot_key 可取 loss 或 acc
def plot_learning_curves(history, plot_key, epochs, min_value, max_value):
data = {}
data[plot_key] = history.history[plot_key]
data['val_' + plot_key] = history.history['val_' + plot_key]
pd.DataFrame(data).plot(figsize=(8, 5))
plt.grid(True)
plt.axis([0, epochs, min_value, max_value])
plt.show()
plot_learning_curves(history, 'accuracy', 30, 0.5, 1)
plot_learning_curves(history, 'loss', 30, 0, 1)
从图中可以看到,在验证集上,loss先下降后上升,说明有些过拟合
测试
single_rnn_model.evaluate(
test_data, test_labels,
batch_size = batch_size,
)
双层双向RNN
embedding_dim = 16
batch_size = 128
two_bi_rnn_model = keras.models.Sequential([
# 1. define matrix: [vocab_size, embeding_dim]
# 2. train_data[*], max_length * embedding_dim
# 3. batch_size * max_length * embedding_dim
keras.layers.Embedding(vocab_size, embedding_dim,
input_length = max_length),
keras.layers.Bidirectional(
keras.layers.SimpleRNN(
units = 64, return_sequences = True)),
keras.layers.Bidirectional(
keras.layers.SimpleRNN(
units = 64, return_sequences = False)),
keras.layers.Dense(64, activation = 'relu'),
keras.layers.Dense(1, activation = 'sigmoid'),
])
two_bi_rnn_model.summary()
训练
查看训练结果
测试
由上可以看出,模型显然是过拟合了。
防止过拟合————降低模型尺寸;加正则项;dropout等。
单层双向RNN
embedding_dim = 16
batch_size = 128
single_bi_rnn_model = keras.models.Sequential([
# 1. define matrix: [vocab_size, embeding_dim]
# 2. train_data[*], max_length * embedding_dim
# 3. batch_size * max_length * embedding_dim
keras.layers.Embedding(vocab_size, embedding_dim,
input_length = max_length),
keras.layers.Bidirectional(
keras.layers.SimpleRNN(
units = 64, return_sequences = False)),
keras.layers.Dense(64, activation = 'relu'),
keras.layers.Dense(1, activation = 'sigmoid'),
])
single_bi_rnn_model.summary()
2.4.2文本生成
(1)载入数据集
# 数据集下载
# https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt
input_filepath = "/content/drive/MyDrive/data/shakespeare.txt"
text = open(input_filepath, 'r').read()
generate vacab
import pprint
vocab = sorted(set(text))
print(len(vocab))
print(vocab)
build mapping char->id
char2idx = {char:idx for idx, char in enumerate(vocab)}
print(char2idx)
idx2char = np.array(vocab)
print(idx2char)
data ——> id_data
text_as_int = np.array([char2idx[c] for c in text])
print(text_as_int[0:10])
print(text[0:10])
(2)构造数据集
def split_input_target(id_text):
"""
abcde --> 输入:abcd,输出bcde
"""
return id_text[0:-1], id_text[1:]
char_dataset = tf.data.Dataset.from_tensor_slices(text_as_int)
seq_length = 100
seq_dataset = char_dataset.batch(seq_length + 1,
drop_remainder = True)
print("\nch_id")
for ch_id in char_dataset.take(2):
print(ch_id, idx2char[ch_id.numpy()])
print("\nseq_id")
for seq_id in seq_dataset.take(2):
print(seq_id, ''.join(idx2char[seq_id.numpy()]))
seq_dataset = seq_dataset.map(split_input_target)
print("\nitem_input, item_output")
for item_input, item_output in seq_dataset.take(2):
print(item_input.numpy())
print(item_output.numpy())
batch_size = 64
buffer_size = 10000
# 这里有个疑问,上边已经batch过一次了,为什么这里还要batch?
# drop_remainder = True 即最后一次数据不够的话,就把最后一次的数据给丢掉
seq_dataset = seq_dataset.shuffle(buffer_size).batch(
batch_size, drop_remainder = True)
(3)构建模型
vocab_size = len(vocab)
embedding_dim = 256 # 因为vocab_size 比较小,所以embedding_dim,run_units可以大一些
run_units = 1024
def build_model(vocab_size, embedding_dim, run_units, batch_size):
model = keras.models.Sequential([
keras.layers.Embedding(vocab_size, embedding_dim,
batch_input_shape = [batch_size, None]),
keras.layers.SimpleRNN(units = run_units, return_sequences = True),
keras.layers.Dense(vocab_size)
])
return model
model = build_model(
vocab_size = vocab_size,
embedding_dim = embedding_dim,
run_units = run_units,
batch_size = batch_size
)
model.summary()
可视化输入输出
for input_example_batch, target_example_batch in seq_dataset.take(1):
example_batch_predictions = model(input_example_batch)
print(example_batch_predictions.shape) # 64 * 100 * 65
print(example_batch_predictions)
sample_indices = tf.random.categorical(
logits = example_batch_predictions[0], num_samples = 1)
print(sample_indices) # 100 * 1
# 将sample_indices 变成一个向量
sample_indices = tf.squeeze(sample_indices, axis = -1)
print(sample_indices)
print("\nInput: ", repr(''.join(idx2char[input_example_batch[0]])))
print("\nOutput: ", repr(''.join(idx2char[target_example_batch[0]])))
print("\nPrediction: ", repr(''.join(idx2char[tf.reshape(sample_indices, [100])])))
计算loss
def loss(labels, logits):
return keras.losses.sparse_categorical_crossentropy(
labels, logits, from_logits=True)
example_loss = loss(target_example_batch, example_batch_predictions)
print(example_loss.shape)
print(example_loss.numpy().mean())
(4)训练
import os
output_dir = "/content/drive/MyDrive/result/text_generation_checkpoints"
if not os.path.exists(output_dir):
os.mkdir(output_dir)
checkpoint_prefix = os.path.join(output_dir, 'ckpt_{epoch}')
checkpoint_callback = keras.callbacks.ModelCheckpoint(
filepath = checkpoint_prefix,
save_weights_only = True)
epochs = 100
model.compile(optimizer = 'adam', loss=loss)
history = model.fit(seq_dataset, epochs = epochs,
callbacks = [checkpoint_callback])
(5)从checkpoint中载入最新存储的模型
model2 = build_model(vocab_size,
embedding_dim,
run_units,
batch_size = 1)
model2.load_weights(tf.train.latest_checkpoint(output_dir))
model2.build(tf.TensorShape([1, None])) # 1指的是一个样本,None表示该样本可以是一个变长的序列
model2.summary()
(6)文本生成
文本生成流程
start char sequence A,
A --> model --> b.
A.append(b) --> B.
B(Ab) --> model --> c.
B.append(c) --> moodel --> c.
B.append(c) --> C.
C(Abc) --> model --> ...
def generate_text(model, start_string, num_gengrate = 1000):
input_eval = [char2idx[ch] for ch in start_string]
# 因为输入的是[1, None] 二维的,故需要做维度的扩展
input_eval = tf.expand_dims(input_eval, 0)
text_generated = []
model.reset_states()
for _ in range(num_generate):
# model inference --> predictions
# sample --> ch --> text_generated
# update input_eval
# predictions: [batch_size, input_eval_len, vocab_size]
predictions = model(input_eval)
# batch_size 总是1(why?)所以可以将batch_size 那个维度消掉
# predictions: [input_eval_len, vocab_size]
predictions = tf.squeeze(predictions, 0)
# predicted_ids: [input_eval_len, 1]
# a b c --> b c d (其中,b是对a的预测,c是对ab的预测,d是对abc的预测,所以只有最后一个d是有用的)
predicted_id = tf.random.categorical(predictions, num_sample = 1)[-1, 0].numpy()
text_generated.append(idx2char[predicted_id])
# 更新input_eval
# 循环神经网络模型 s, x --> run --> s', y
input_eval = tf.expand_dims([predicted_id], 0)
return start_string + ''.join(text_generated)
new_text = generated_text(model2, "All: ")
print(new_text)