人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新)
Pytorch:Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、前馈全连接层、规范化层、子层连接结构、pyitcast) part1
Pytorch:Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、前馈全连接层、规范化层、子层连接结构、pyitcast) part2
Pytorch:使用Transformer构建语言模型
Pytorch:解码器端的Attention注意力机制、seq2seq模型架构实现英译法任务
BahdanauAttention注意力机制、LuongAttention注意力机制
BahdanauAttention注意力机制:基于seq2seq的西班牙语到英语的机器翻译任务、解码器端的Attention注意力机制、seq2seq模型架构
图片的描述生成任务、使用迁移学习实现图片的描述生成过程、CNN编码器+RNN解码器(GRU)的模型架构、BahdanauAttention注意力机制、解码器端的Attention注意力机制
注意力机制、bmm运算
注意力机制 SENet、CBAM
机器翻译 MXNet(使用含注意力机制的编码器—解码器,即 Encoder编码器-Decoder解码器框架 + Attention注意力机制)
基于Seq2Seq的中文聊天机器人编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制)
基于Transformer的文本情感分析编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制 + Positional Encoding位置编码)
注意:这一文章“基于Transformer的文本情感分析编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制 + Positional Encoding位置编码)”
该文章实现的Transformer的Model类型模型,实际是改造过的特别版的Transformer,因为Transformer的Model类型模型中只实现了Encoder编码器,
而没有对应实现的Decoder解码器,并且因为当前Transformer的Model类型模型处理的是分类任务,
所以我们此处只用了Encoder编码器来提取特征,最后通过全连接层网络来拟合分类。
# coding=utf-8
import configparser
def get_config(config_file='config.ini'):
parser=configparser.ConfigParser()
parser.read(config_file)
# get the ints, floats and strings
_conf_ints = [(key, int(value)) for key, value in parser.items('ints')]
_conf_floats = [(key, float(value)) for key, value in parser.items('floats')]
_conf_strings = [(key, str(value)) for key, value in parser.items('strings')]
return dict(_conf_ints + _conf_floats + _conf_strings)
[strings]
# Mode : train, test, serve
mode = train
train_pos_data_path=aclImdb/train/pos
train_neg_data_path=aclImdb/train/neg
test_pos_data_path=aclImdb/test/pos
test_neg_data_path=aclImdb/test/neg
train_pos_data=train_data/train_pos_data.txt
train_neg_data=train_data/train_neg_data.txt
test_pos_data=train_data/test_pos_data.txt
test_neg_data=train_data/test_neg_data.txt
all_data=all_data.txt
vocabulary_file=train_data/vocab10000.txt
working_directory=train_data/
model_dir=model_data/
npz_data=train_data/imdb.npz
[ints]
vocabulary_size=10000
sentence_size = 100
embedding_size = 80
steps_per_checkpoint = 10
num_layers = 4
num_heads = 8
batch_size=64
epochs = 1
shuffle_size=20000
diff=1024
[floats]
dropout_rate=0.1
# coding=utf-8
import os
import numpy as np
import getConfig
gConfig={}
gConfig=getConfig.get_config(config_file='config.ini')
UNK = "__UNK__" # 标记未出现在词汇表中的字符
START_VOCABULART = [UNK]
UNK_ID = 1
# 定义字典生成函数
#生成字典的原理其实很简单,就是统计所有训练数据中的词频,然后按照词频进行排序,每个词在训练集中出现次数就是其对应的编码
#知识点:函数定义、在函数中函数的调用是不需要声明的、字典类型
"""
词频词典的创建:
1、读取所有的文字
2、统计每个文字的出现次数
3、排序
4、取值保存
"""
def create_vocabulary(input_file,vocabulary_size,output_file):
vocabulary = {}
k=int(vocabulary_size)
with open(input_file,'r') as f:
counter = 0
for line in f:
counter += 1
tokens = [word for word in line.split()]
for word in tokens:
if word in vocabulary:
vocabulary[word] += 1
else:
vocabulary[word] = 1
vocabulary_list = START_VOCABULART + sorted(vocabulary, key=vocabulary.get, reverse=True)
# 根据配置,取vocabulary_size大小的词典
if len(vocabulary_list) > k:
vocabulary_list = vocabulary_list[:k]
#将生成的词典保存到文件中
print(input_file + " 词汇表大小:", len(vocabulary_list))
with open(output_file, 'w') as ff:
for word in vocabulary_list:
ff.write(word + "\n")
#在生成字典之后,我们就需要将我们之前训练集的文字全部用字典进行替换
#知识点:list的append和extend,dict的get操作、文件的写入操作
# 把对话字符串转为向量形式
"""
1、遍历文件
2、找到一个字 然后在词典出来,然后做替换
3、保存文件
"""
def convert_to_vector(input_file, vocabulary_file, output_file):
print('文字转向量...')
tmp_vocab = []
with open(vocabulary_file, "r") as f:#读取字典文件的数据,生成一个dict,也就是键值对的字典
tmp_vocab.extend(f.readlines())
tmp_vocab = [line.strip() for line in tmp_vocab]
vocab = dict([(x, y) for (y, x) in enumerate(tmp_vocab)])#将vocabulary_file中的键值对互换,因为在字典文件里是按照{123:好}这种格式存储的,我们需要换成{好:123}格式
output_f = open(output_file, 'w')
with open(input_file, 'r') as f:
line_out=[]
for line in f:
line_vec = []
for words in line.split():
line_vec.append(vocab.get(words, UNK_ID))#获取words的对应编码,如果找不到就返回UNK_ID
output_f.write(" ".join([str(num) for num in line_vec]) + "\n")#将input_file里的中文字符通过查字典的方式,替换成对应的key,并保存在output_file
#print(line_vec)
line_out.append(line_vec)
output_f.close()
return line_out
def prepare_custom_data(working_directory,train_pos,train_neg,test_pos,test_neg,all_data,vocabulary_size):
# 生成字典的路径,encoder和decoder的字典是分开的
vocab_path = os.path.join(working_directory, "vocab%d.txt" % vocabulary_size)
#生成字典文件
create_vocabulary(all_data,vocabulary_size,vocab_path)
# 将训练数据集的中文用字典进行替换
pos_train_ids_path = train_pos + (".ids%d" % vocabulary_size)
neg_train_ids_path = train_neg + (".ids%d" % vocabulary_size)
train_pos=convert_to_vector(train_pos, vocab_path, pos_train_ids_path)
train_neg=convert_to_vector(train_neg, vocab_path, neg_train_ids_path)
# 将测试数据集的中文用字典进行替换
pos_test_ids_path = test_pos + (".ids%d" % vocabulary_size)
neg_test_ids_path = test_neg + (".ids%d" % vocabulary_size)
test_pos=convert_to_vector(test_pos, vocab_path, pos_test_ids_path)
test_neg=convert_to_vector(test_neg, vocab_path, neg_test_ids_path)
return train_pos,train_neg,test_pos,test_neg
train_pos,train_neg,test_pos,test_neg=prepare_custom_data(gConfig['working_directory'],gConfig['train_pos_data'],gConfig['train_neg_data'],gConfig['test_pos_data'],gConfig['test_neg_data'],gConfig['all_data'],gConfig['vocabulary_size'])
y_trian=[]
y_test=[]
for i in range(len(train_pos)):
y_trian.append(0)
for i in range(len(train_neg)):
y_trian.append(1)
for i in range(len(test_pos)):
y_test.append(0)
for i in range(len(test_neg)):
y_test.append(1)
x_train=np.concatenate((train_pos,train_neg),axis=0)
x_test=np.concatenate((test_pos,test_neg),axis=0)
np.savez("train_data/imdb.npz",x_train,y_trian,x_test,y_test)
注意:
下面实现Transformer的Model类型模型,实际是改造过的特别版的Transformer,因为Transformer的Model类型模型中只实现了Encoder编码器,
而没有对应实现的Decoder解码器,并且因为当前Transformer的Model类型模型处理的是分类任务,
所以我们此处只用了Encoder编码器来提取特征,最后通过全连接层网络来拟合分类。
# coding=utf-8
import tensorflow as tf
import numpy as np
import getConfig
gConfig={}
gConfig=getConfig.get_config(config_file='config.ini')
def get_angles(pos, i, d_model):
angle_rates = 1 / np.power(10000, (2 * (i//2)) / np.float32(d_model))
return pos * angle_rates
def positional_encoding(position, d_model):
angle_rads = get_angles(np.arange(position)[:, np.newaxis],
np.arange(d_model)[np.newaxis, :],
d_model)
sines = np.sin(angle_rads[:, 0::2])
cosines = np.cos(angle_rads[:, 1::2])
pos_encoding = np.concatenate([sines, cosines], axis=-1)
pos_encoding = pos_encoding[np.newaxis, ...]
return tf.cast(pos_encoding, dtype=tf.float32)
def scaled_dot_product_attention(q, k, v,mask):
matmul_qk = tf.matmul(q, k, transpose_b=True) # (..., seq_len_q, seq_len_k)
dk = tf.cast(tf.shape(k)[-1], tf.float32)
scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)
if mask is not None:
scaled_attention_logits += (mask * -1e9)
attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1) # (..., seq_len_q, seq_len_k)
output = tf.matmul(attention_weights, v) # (..., seq_len_v, depth)
return output, attention_weights
class MultiHeadAttention(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
self.num_heads = num_heads
self.d_model = d_model
assert d_model % self.num_heads == 0
self.depth = d_model // self.num_heads
self.wq = tf.keras.layers.Dense(d_model)
self.wk = tf.keras.layers.Dense(d_model)
self.wv = tf.keras.layers.Dense(d_model)
self.dense = tf.keras.layers.Dense(d_model)
def split_heads(self, x, batch_size):
x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))
return tf.transpose(x, perm=[0, 2, 1, 3])
def call(self, v, k, q,mask):
batch_size = tf.shape(q)[0]
q = self.wq(q) # (batch_size, seq_len, d_model)
k = self.wk(k) # (batch_size, seq_len, d_model)
v = self.wv(v) # (batch_size, seq_len, d_model)
q = self.split_heads(q, batch_size) # (batch_size, num_heads, seq_len_q, depth)
k = self.split_heads(k, batch_size) # (batch_size, num_heads, seq_len_k, depth)
v = self.split_heads(v, batch_size) # (batch_size, num_heads, seq_len_v, depth)
scaled_attention, attention_weights = scaled_dot_product_attention(q, k, v,mask)
scaled_attention = tf.transpose(scaled_attention, perm=[0, 2, 1, 3]) # (batch_size, seq_len_v, num_heads, depth)
concat_attention = tf.reshape(scaled_attention,(batch_size, -1, self.d_model)) # (batch_size, seq_len_v, d_model)
output = self.dense(concat_attention) # (batch_size, seq_len_v, d_model)
return output, attention_weights
def point_wise_feed_forward_network(d_model,dff):
return tf.keras.Sequential([
tf.keras.layers.Dense(dff,activation='relu'),
tf.keras.layers.Dense(d_model)
])
class EncoderLayer(tf.keras.layers.Layer):
def __init__(self, d_model, diff,num_heads, rate=0.1):
super(EncoderLayer, self).__init__()
self.mha = MultiHeadAttention(d_model, num_heads)
self.ffn = point_wise_feed_forward_network(d_model,diff)
self.dropout1 = tf.keras.layers.Dropout(rate)
self.dropout2 = tf.keras.layers.Dropout(rate)
self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
def call(self, x, training,mask):
attn_output, _ = self.mha(x, x, x,mask) # (batch_size, input_seq_len, d_model)
attn_output = self.dropout1(attn_output, training=training)
out1 =self.layernorm1 (x + attn_output) # (batch_size, input_seq_len, d_model)
ffn_output = self.ffn(out1) # (batch_size, input_seq_len, d_model)
ffn_output = self.dropout2(ffn_output, training=training)
out2 = self.layernorm2 (out1 + ffn_output) # (batch_size, input_seq_len, d_model)
return out2
class Encoder(tf.keras.layers.Layer):
def __init__(self, num_layers, d_model,dff, num_heads, input_vocab_size,
rate=0.1):
super(Encoder, self).__init__()
self.d_model = d_model
self.num_layers = num_layers
self.embedding = tf.keras.layers.Embedding(input_vocab_size, d_model)
self.pos_encoding = positional_encoding(input_vocab_size, self.d_model)
self.enc_layers = [EncoderLayer(d_model,dff, num_heads, rate)for _ in range(num_layers)]
self.dropout = tf.keras.layers.Dropout(rate)
def call(self, x, training,mask):
seq_len = tf.shape(x)[1]
#print(seq_len)
# adding embedding and position encoding.
x = self.embedding(x) # (batch_size, input_seq_len, d_model)
#print(x)
x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
x += self.pos_encoding[:, :seq_len, :]
x = self.dropout(x, training=training)
for i in range(self.num_layers):
x = self.enc_layers[i](x, training,mask)
#print(x)
return x # (batch_size, input_seq_len, d_model)
class Transformer(tf.keras.Model):
def __init__(self, num_layers, d_model, dff,num_heads, input_vocab_size, rate=0.1):
super(Transformer, self).__init__()
self.encoder = Encoder(num_layers, d_model,dff, num_heads,input_vocab_size, rate)
self.ffn_out=tf.keras.layers.Dense(2,activation='softmax')
self.dropout1 = tf.keras.layers.Dropout(rate)
def call(self, inp, training,enc_padding_mask):
enc_output = self.encoder(inp, training,enc_padding_mask) # (batch_size, inp_seq_len, d_model)
out_shape=gConfig['sentence_size']*gConfig['embedding_size']
enc_output=tf.reshape(enc_output,[-1,out_shape])
ffn=self.dropout1(enc_output,training=training)
ffn_out=self.ffn_out(ffn)
return ffn_out
class CustomSchedule(tf.keras.optimizers.schedules.LearningRateSchedule):
def __init__(self, d_model, warmup_steps=40):
super(CustomSchedule, self).__init__()
self.d_model = d_model
self.d_model = tf.cast(self.d_model, tf.float32)
self.warmup_steps = warmup_steps
def __call__(self, step):
arg1 = tf.math.rsqrt(step)
arg2 = step * (self.warmup_steps ** -1.5)
return tf.math.rsqrt(self.d_model) * tf.math.minimum(arg1, arg2)
learning_rate = CustomSchedule(gConfig['embedding_size'])
optimizer = tf.keras.optimizers.Adam(learning_rate)
#temp_learning_rate_schedule = CustomSchedule(gConfig['embedding_size'])
train_loss = tf.keras.metrics.Mean(name='train_loss')
train_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='train_accuracy')
transformer = Transformer(gConfig['num_layers'],gConfig['embedding_size'],gConfig['diff'] ,gConfig['num_heads'],
gConfig['vocabulary_size'],gConfig['dropout_rate'])
ckpt = tf.train.Checkpoint(transformer=transformer,optimizer=optimizer)
def create_padding_mask(seq):
seq = tf.cast(tf.math.equal(seq, 0), tf.float32)
return seq[:, tf.newaxis, tf.newaxis, :] # (batch_size, 1, 1, seq_len)
def step(inp, tar,train_status=True):
enc_padding_mask=create_padding_mask(inp)
if train_status:
with tf.GradientTape() as tape:
predictions= transformer(inp,True,enc_padding_mask)
tar = tf.keras.utils.to_categorical(tar, 2)
loss = tf.losses.categorical_crossentropy(tar,predictions)
loss= tf.reduce_mean(loss)
print(loss.numpy())
gradients = tape.gradient(loss, transformer.trainable_variables)
optimizer.apply_gradients(zip(gradients, transformer.trainable_variables))
return loss
else:
predictions = transformer(inp,False, enc_padding_mask)
return predictions
def evaluate(inp,tar):
enc_padding_mask = create_padding_mask(inp)
predictions= transformer(inp,False,enc_padding_mask)
tar = tf.keras.utils.to_categorical(tar, 2)
loss =tf.losses.categorical_crossentropy(tar, predictions)
return train_loss(loss)
# coding=utf-8
import tensorflow as tf
import numpy as np
import getConfig
import tensorflow.keras.preprocessing.sequence as sequence
import textClassiferModel as model
import time
UNK_ID=3
gConfig={}
gConfig=getConfig.get_config(config_file='config.ini')
sentence_size=gConfig['sentence_size']
embedding_size = gConfig['embedding_size']
vocab_size=gConfig['vocabulary_size']
model_dir = gConfig['model_dir']
def read_npz(data_file):
r = np.load(data_file)
return r['arr_0'],r['arr_1'],r['arr_2'],r['arr_3']
def pad_sequences(inp):
out_sequences=sequence.pad_sequences(inp, maxlen=gConfig['sentence_size'],padding='post',value=0)
return out_sequences
x_train, y_train, x_test, y_test =read_npz(gConfig['npz_data'])
x_train = pad_sequences(x_train)
x_test = pad_sequences(x_test)
dataset_train = tf.data.Dataset.from_tensor_slices((x_train,y_train)).shuffle(gConfig['shuffle_size'])
dataset_test = tf.data.Dataset.from_tensor_slices((x_test,y_test)).shuffle(gConfig['shuffle_size'])
checkpoint_path = gConfig['model_dir']
ckpt_manager = tf.train.CheckpointManager(model.ckpt, checkpoint_path, max_to_keep=5)
def create_model():
ckpt = tf.io.gfile.listdir(checkpoint_path)
if ckpt:
print("reload pretrained model")
model.ckpt.restore(tf.train.latest_checkpoint(checkpoint_path))
return model
else:
return model
def train():
model=create_model()
while True:
model.train_loss.reset_states()
model.train_accuracy.reset_states()
for (batch,(inp, target)) in enumerate(dataset_train.batch(gConfig['batch_size'])):
start = time.time()
loss = model.step(inp,target)
print ('训练集:Epoch {:} ,Batch {:} ,Loss {:.4f},Prestep {:.4f}'.format(epoch + 1, batch,loss.numpy(),(time.time()-start)))
#for (batch,(inp,target)) in enumerate(dataset_test.batch(gConfig['batch_size'])):
#start = time.time()
# loss = model.evaluate(inp,target)
# print ('测试集:Epoch {:} ,Batch {:} ,Loss {:.4f},Prestep {:.4f}'.format(epoch + 1, batch,loss.numpy(),(time.time()-start)))
ckpt_save_path=ckpt_manager.save()
print ('保存epoch{}模型在 {}'.format(epoch+1, ckpt_save_path))
def text_to_vector(inp):
vocabulary_file=gConfig['vocabulary_file']
tmp_vocab = []
with open(vocabulary_file, "r") as f:#读取字典文件的数据,生成一个dict,也就是键值对的字典
tmp_vocab.extend(f.readlines())
tmp_vocab = [line.strip() for line in tmp_vocab]
vocab = dict([(x, y) for (y, x) in enumerate(tmp_vocab)])
print(vocab)
line_vec = []
for words in inp.split():
line_vec.append(vocab.get(words, UNK_ID)) #
return line_vec
def predict(sentences):
state=['pos','neg']
model=create_model()
indexes = text_to_vector(sentences)
print(indexes)
inp = pad_sequences([indexes])
inp=tf.reshape(inp[0],(1,len(inp[0])))
predictions=model.step(inp,inp,False)
pred = tf.math.argmax(predictions[0])
p=np.int32(pred.numpy())
return state[p]
if __name__ == "__main__":
if gConfig['mode']=='train':
train()
elif gConfig['mode']=='serve':
print('Sever Usage:python3 app.py')
# coding=utf-8
from flask import Flask, render_template, request, make_response
from flask import jsonify
import time
import threading
"""
定义心跳检测函数
"""
def heartbeat():
print (time.strftime('%Y-%m-%d %H:%M:%S - heartbeat', time.localtime(time.time())))
timer = threading.Timer(60, heartbeat)
timer.start()
timer = threading.Timer(60, heartbeat)
timer.start()
app = Flask(__name__,static_url_path="/static")
@app.route('/message', methods=['POST'])
#"""定义应答函数,用于获取输入信息并返回相应的答案"""
def reply():
#从请求中获取参数信息
req_msg = request.form['msg']
res_msg = execute.predict(req_msg)
return jsonify( { 'text': res_msg } )
@app.route("/")
def index():
return render_template("index.html")
import execute
# 启动APP
if (__name__ == "__main__"):
app.run(host = '0.0.0.0', port = 8808)
注意:
下面实现Transformer的Model类型模型,实际是改造过的特别版的Transformer,因为Transformer的Model类型模型中只实现了Encoder编码器,
而没有对应实现的Decoder解码器,并且因为当前Transformer的Model类型模型处理的是分类任务,
所以我们此处只用了Encoder编码器来提取特征,最后通过全连接层网络来拟合分类。
报错:ValueError: Object arrays cannot be loaded when allow_pickle=False
解决:安装 numpy的1.16.2版本,暂时高于1.16.2版本都会出问题
conda install numpy==1.16.2
pip install numpy==1.16.2
例子:
conda 已经安装有1.18.1版本的numpy,即使执行conda install numpy==1.16.2之后,实际安装效果为1.16.2版本的numpy-base。
conda list 显示如下两者共存不影响使用:
numpy 1.18.1
numpy-base 1.16.2
现在重新执行重新读取imdb.npz便不会报错了。
#读取训练集文件train_data/imdb.npz,并返回所读取的数据
def read_npz(data_file):
r = np.load(data_file)
#返回值分别代表 x_train, y_train, x_test, y_test
return r['arr_0'],r['arr_1'],r['arr_2'],r['arr_3']
------------------------------------------------------------------------------------------------------------------------------
报错ValueError: setting an array element with a sequence.
tf.keras.utils.to_categorical函数底层的第一行代码np.array(输入, dtype='int')如果报错ValueError: setting an array element with a sequence.
解决方法一:tf.enable_eager_execution() #开启紧急执行
解决方法二:可以尝试使用tf.one_hot(input, num_classes) 来代替 tf.keras.utils.to_categorical(input, num_classes)
------------------------------------------------------------------------------------------------------------------------------
报错AttributeError: 'Tensor' object has no attribute 'numpy'
解决:tf.enable_eager_execution()
拓展:获取tensor类型的变量值
方法一:在会话中print( sess.run(x) )
import tensorflow as tf
#定义tensor常量
x = tf.random_uniform((2, 3), -1, 1)
#开启会话
with tf.Session() as sess:
print (sess.run(x))
#[[ 0.46468353 -0.61598516 -0.9036629 ]
# [ 0.68292856 0.65335464 0.00641084]]
方法二:使用.eval()相当于将tensor类数据转为numpy,再输出
import tensorflow as tf
#定义tensor常量
x = tf.random_uniform((2, 2), -1, 1)
#开启会话
with tf.Session() as sess:
print (x.eval())
#[[-0.8492842 0.50390124]
# [-0.9012234 0.77892494]]
方法三:
开启紧急执行
tf.enable_eager_execution()
方法四:
tensor类型变量.numpy()
config.ini
[strings]
# 配置执行器的运行模式 Mode : train, test, serve
mode = train
#配置 训练集标注为 积极的 文本数据的路径
train_pos_data_path=aclImdb/train/pos
#配置 训练集标注为 消极的 文本数据的路径
train_neg_data_path=aclImdb/train/neg
#配置 测试集标注为 积极的 文本数据的路径
test_pos_data_path=aclImdb/test/pos
#配置 测试集标注为 消极的 文本数据的路径
test_neg_data_path=aclImdb/test/neg
#配置 训练集 积极的 文本数据
train_pos_data=train_data/train_pos_data.txt
#配置 训练集标注为 消极的 文本数据
train_neg_data=train_data/train_neg_data.txt
#配置 测试集标注为 积极的 文本数据
test_pos_data=train_data/test_pos_data.txt
#配置 测试集标注为 消极的 文本数据
test_neg_data=train_data/test_neg_data.txt
all_data=all_data.txt
#配置字典的路径
vocabulary_file=train_data/vocab10000.txt
#配置训练集的路径
working_directory=train_data/
#配置模型保存路径
model_dir=model_data/
#配置训练集文件
npz_data=train_data/imdb.npz
[ints]
#配置字典的大小
vocabulary_size=10000
#配置句子的最大长度
sentence_size = 100
#配置Embedding的长度
embedding_size = 80
#配置保存点
steps_per_checkpoint = 10
#配置 Encoder Layer的层数
num_layers = 4
#配置多头注意力的多头的头数量
num_heads = 8
#配置 批量大小
batch_size=64
#配置完整训练周期数
epochs = 1
#配置随机打乱参数
shuffle_size=20000
diff=1024
[floats]
#配置神经元失效的比例概率
dropout_rate=0.1
================================================================================================================================
data_util.py
# coding=utf-8
import os
import numpy as np
import getConfig
gConfig={}
gConfig=getConfig.get_config(config_file='config.ini')
UNK = "__UNK__" #用于标记未出现在词汇表中的字符
START_VOCABULART = [UNK]
UNK_ID = 3
# 定义字典生成函数
# 生成字典的原理其实很简单,就是统计所有训练数据中的词频,然后按照词频进行排序,每个词在训练集中出现次数就是其对应的编码
# 知识点:函数定义、在函数中函数的调用是不需要声明的、字典类型
"""
词频词典的创建:
1、读取所有的文字
2、统计每个文字的出现次数
3、排序
4、取值保存
"""
#生成字典文件到本地
def create_vocabulary(input_file, vocabulary_size, output_file):
#字典的路径,encoder和decoder的字典是分开的
vocabulary = {}
#字典的大小10000
k = int(vocabulary_size)
#读取./all_data.txt数据文件
with open(input_file,'r',encoding="utf8") as f:
counter = 0
#默认按行读取
for line in f:
counter += 1
#split() 默认按空格字符对一行数据进行切割
tokens = [word for word in line.split()]
#遍历一行中的每个单词
for word in tokens:
#如果单词已经存在字典中,则value+=1,反之存储键值对到字典中并将value置为1
if word in vocabulary:
vocabulary[word] += 1
else:
vocabulary[word] = 1
#遍历完文件每行之后,对字典按照value值从大到小对键值对进行排序,然后把排序后的数据返回为list
#key=vocabulary.get 通过字典对象vocabulary.get定义排序的规则,按照对字典的value值进行排序
#reverse=True 表示从大到小排序
vocabulary_list = START_VOCABULART + sorted(vocabulary, key=vocabulary.get, reverse=True)
# print(sorted(vocabulary, key=vocabulary.get, reverse=True)[0]) #the
# print(sorted(vocabulary, key=vocabulary.get, reverse=True)[1]) #a
# print(sorted(vocabulary, key=vocabulary.get, reverse=True)[-1]) #Selleca
# print(vocabulary.get("the")) #568735
# print(vocabulary.get("a")) #306960
# print(vocabulary.get("Selleca")) #1
# 根据配置,取vocabulary_size大小的词典
if len(vocabulary_list) > k:
#取出字典的大小10000个单词,即将出现频率最高的前10000个单词
vocabulary_list = vocabulary_list[:k]
#将生成的词典保存到文件中
print(input_file + " 词汇表大小:", len(vocabulary_list))
#将出现频率最高的前10000个单词保存到本地文件train_data/vocab10000.txt
with open(output_file, 'w' ,encoding="utf8") as ff:
#一行一个单词写出到本地文件
for word in vocabulary_list:
ff.write(word + "\n")
#在生成字典之后,我们就需要将我们之前训练集的文字全部用字典进行替换
#知识点:list的append和extend,dict的get操作、文件的写入操作
# 把对话字符串转为向量形式
"""
1、遍历文件
2、找到一个字 然后在词典出来,然后做替换
3、保存文件
"""
# 通过{单词:出现频率次数}的字典 把每行句子中的每个单词都替换为相应的value值(出现频率次数),最后返回该全部value值(出现频率次数)组成的列表
def convert_to_vector(input_file, vocabulary_file, output_file):
print('文字转向量...')
tmp_vocab = []
#读取train_data/vocab10000.txt字典文件的数据,生成一个dict,也就是键值对的字典
with open(vocabulary_file, "r",encoding="utf8") as f:
# 一次性读取多行文件数据 临时保存到tmp_vocab列表中
tmp_vocab.extend(f.readlines())
#遍历每行数据,strip()表示每行数据按照默认空格进行切割
tmp_vocab = [line.strip() for line in tmp_vocab]
#enumerate 遍历的是(y, x) 即为{1:the}、{2:a},所以还要翻转变成{the:1}、{a:2} 存储到 字典中
#tmp_vocab列表中的数据实际是出现频率最高的前10000个单词,出现频率从高到小的单词按照列表索引0值开始排列,
# 因此遍历出来的单词的出现频率是从高到小的
vocab = dict([(x, y) for (y, x) in enumerate(tmp_vocab)])
# print(vocab.get("the")) #1
# print(vocab.get("a")) #2
#将积极的/消极的文本数据的每个单词替换为出现频率 再输出到文件中
output_f = open(output_file, 'w', encoding="utf8")
#读取积极的/消极的文本数据
with open(input_file, 'r',encoding="utf8") as f:
line_out=[]
#默认按行读取按行遍历 积极的/消极的文本数据
for line in f:
line_vec = []
#split() 默认按照空格字符进行切割
for words in line.split():
#获取单词在字典中对应value值(出现频率次数),如果单词不存在字典中的话就默认返回UNK_ID = 3
line_vec.append(vocab.get(words, UNK_ID))
#一行单词全都替换为对应的value值(出现频率次数)之后,重新按照空格作为每个value值之间的分割符组成一行再输出到文件中
output_f.write(" ".join([str(num) for num in line_vec]) + "\n")
#把每行单词都替换之后的value值(出现频率次数)的列表 作为最后的函数返回
line_out.append(line_vec)
output_f.close()
#最后返回该全部value值(出现频率次数)组成的列表
return line_out
def prepare_custom_data(working_directory,train_pos,train_neg,test_pos,test_neg,all_data,vocabulary_size):
# 构建要写出数据到本地保存的文件路径train_data/vocab10000.txt
# 生成字典的路径,encoder和decoder的字典是分开的
vocab_path = os.path.join(working_directory, "vocab%d.txt" % vocabulary_size)
#生成字典,并最终将出现频率最高的前10000个单词
create_vocabulary(all_data, vocabulary_size, vocab_path)
#读取 训练集 积极的 文本数据train_data/train_pos_data.txt,
#将积极的文本数据的每个单词替换为出现频率 再输出到train_data/train_pos_data.txt.ids10000
pos_train_ids_path = train_pos + (".ids%d" % vocabulary_size)
#读取 训练集标注为 消极的 文本数据 train_data/train_neg_data.txt,
#将消极的文本数据的每个单词替换为出现频率 再输出到 train_data/train_neg_data.txt.ids10000
neg_train_ids_path = train_neg + (".ids%d" % vocabulary_size)
#通过{单词:出现频率次数}的字典 把每行句子中的每个单词都替换为相应的value值(出现频率次数),最后返回该全部value值(出现频率次数)组成的列表
train_pos=convert_to_vector(train_pos, vocab_path, pos_train_ids_path)
train_neg=convert_to_vector(train_neg, vocab_path, neg_train_ids_path)
#读取 测试集标注为 积极的 文本数据 train_data/test_pos_data.txt,
#将积极的文本数据的每个单词替换为出现频率 再输出到train_data/test_pos_data.txt.ids10000
pos_test_ids_path = test_pos + (".ids%d" % vocabulary_size)
#读取 测试集标注为 消极的 文本数据 train_data/test_neg_data.txt,
#将消极的文本数据的每个单词替换为出现频率 再输出到train_data/test_neg_data.txt.ids10000
neg_test_ids_path = test_neg + (".ids%d" % vocabulary_size)
# 通过{单词:出现频率次数}的字典 把每行句子中的每个单词都替换为相应的value值(出现频率次数),最后返回该全部value值(出现频率次数)组成的列表
test_pos=convert_to_vector(test_pos, vocab_path, pos_test_ids_path)
test_neg=convert_to_vector(test_neg, vocab_path, neg_test_ids_path)
#返回 积极的/消极的文本数据的每个单词其对应的 value值(出现频率次数)
return train_pos,train_neg,test_pos,test_neg
#把 积极的/消极的文本数据的每个单词 都替换为 其对应的 value值(出现频率次数)
train_pos,train_neg,test_pos,test_neg = prepare_custom_data(gConfig['working_directory'],gConfig['train_pos_data'],
gConfig['train_neg_data'],gConfig['test_pos_data'],
gConfig['test_neg_data'],gConfig['all_data'],
gConfig['vocabulary_size'])
#训练集数据的标签集(包含积极的/消极的文本数据的标签集):积极的文本数据的标签用0表示,消极的文本数据的标签用1表示
y_trian=[]
#测试集数据的标签集(包含积极的/消极的文本数据的标签集):积极的文本数据的标签用0表示,消极的文本数据的标签用1表示
y_test=[]
#训练集数据的标签集:积极的文本数据的标签用0表示
for i in range(len(train_pos)):
y_trian.append(0)
#训练集数据的标签集:消极的文本数据的标签用1表示
for i in range(len(train_neg)):
y_trian.append(1)
#测试集数据的标签集:积极的文本数据的标签用0表示
for i in range(len(test_pos)):
y_test.append(0)
#测试集数据的标签集:消极的文本数据的标签用1表示
for i in range(len(test_neg)):
y_test.append(1)
#把积极的/消极的文本数据都构建到同一个训练集数据中
x_train=np.concatenate((train_pos, train_neg),axis=0)
#把积极的/消极的文本数据都构建到同一个测试集数据中
x_test=np.concatenate((test_pos, test_neg),axis=0)
#保存为npz格式文件
np.savez("train_data/imdb.npz",x_train,y_trian,x_test,y_test)
================================================================================================================================
getConfig.py
# coding=utf-8
import configparser
# configparser为 用于读取配置文件的包
def get_config(config_file='config.ini'):
parser=configparser.ConfigParser()
parser.read(config_file,encoding="utf8")
#获取int、float、string等类型的参数,按照key-value形式保存
_conf_ints = [(key, int(value)) for key, value in parser.items('ints')]
_conf_floats = [(key, float(value)) for key, value in parser.items('floats')]
_conf_strings = [(key, str(value)) for key, value in parser.items('strings')]
#封装为一个字典对象,包含所读取的所有参数
return dict(_conf_ints + _conf_floats + _conf_strings)
================================================================================================================================
execute.py
# coding=utf-8
import tensorflow as tf
import numpy as np
import getConfig
import tensorflow.keras.preprocessing.sequence as sequence
import textClassiferModel as model
import time
UNK_ID=3
#定义一个字典用于接收配置文件的配置参数
gConfig={}
gConfig=getConfig.get_config(config_file='config.ini')
sentence_size=gConfig['sentence_size'] #句子的最大长度100
embedding_size = gConfig['embedding_size']#Embedding的长度80
vocab_size=gConfig['vocabulary_size']#字典的大小10000
model_dir = gConfig['model_dir']#模型保存路径model_data/
checkpoint_path = gConfig['model_dir']#模型保存路径model_data/
#读取训练集文件train_data/imdb.npz,并返回所读取的数据
def read_npz(data_file):
r = np.load(data_file)
#返回值分别代表 x_train, y_train, x_test, y_test
return r['arr_0'],r['arr_1'],r['arr_2'],r['arr_3']
#定义paddding填充函数,对长度不足的语句使用0进行补全。
#把每行句子的长度要么填充到100,要么截取为100。
def pad_sequences(inp):
#对输入语句按照maxlen最大长度进行以0补全,默认补0,可指定padding='post'在后面做填充。
out_sequences=sequence.pad_sequences(inp, maxlen=gConfig['sentence_size'],padding='post',value=0)
return out_sequences
"""
报错:ValueError: Object arrays cannot be loaded when allow_pickle=False
解决:安装 numpy的1.16.2版本,暂时高于1.16.2版本都会出问题
conda install numpy==1.16.2
pip install numpy==1.16.2
例子:
conda 已经安装有1.18.1版本的numpy,即使执行conda install numpy==1.16.2之后,实际安装效果为1.16.2版本的numpy-base。
conda list 显示如下两者共存不影响使用:
numpy 1.18.1
numpy-base 1.16.2
现在重新执行重新读取imdb.npz便不会报错了。
"""
#读取训练集文件train_data/imdb.npz,并返回所读取的数据
# x_train:25000行句子,每行句子的单词数不尽相同。训练集数据(包含积极的/消极的文本数据),读取的数据实际为单词对应的出现频率次数值。
# y_train:25000个标签值。训练集数据的标签集(包含积极的/消极的文本数据的标签),积极的文本数据的标签用0表示,消极的文本数据的标签用1表示
# x_test:25000行句子,每行句子的单词数不尽相同。测试集数据(包含积极的/消极的文本数据),读取的数据实际为单词对应的出现频率次数值
# y_test:25000个标签值。测试集数据的标签集(包含积极的/消极的文本数据的标签),积极的文本数据的标签用0表示,消极的文本数据的标签用1表示
x_train, y_train, x_test, y_test = read_npz(gConfig['npz_data'])
#定义paddding填充函数,对长度不足的语句使用0进行补全。
#把每行句子的长度要么填充到100,要么截取为100。
x_train = pad_sequences(x_train)
x_test = pad_sequences(x_test)
# print("x_train.shape:",x_train.shape) #(25000, 100)
# print("y_train.shape:",y_train.shape) #(25000,)
# print("x_test.shape:",x_test.shape) #(25000, 100)
# print("y_test.shape:",y_test.shape) #(25000,)
#分别将训练数据构成Dataset对象,这样就可以使用Dataset属性/方法对数据进行操作了,然后使用shuffle对数据进行打乱
dataset_train = tf.data.Dataset.from_tensor_slices((x_train, y_train)).shuffle(gConfig['shuffle_size'])
dataset_test = tf.data.Dataset.from_tensor_slices((x_test, y_test)).shuffle(gConfig['shuffle_size'])
"""
使用 tf.train.CheckpointManager 删除旧的 Checkpoint 以及自定义文件编号
在模型的训练过程中,我们往往每隔一定步数保存一个 Checkpoint 并进行编号。
不过很多时候我们会有这样的需求:
在长时间的训练后,程序会保存大量的 Checkpoint,但我们只想保留最后的几个 Checkpoint。
Checkpoint 默认从 1 开始编号,每次累加 1,但我们可能希望使用别的编号方式(例如使用当前 Batch 的编号作为文件编号)。
这时,我们可以使用 TensorFlow 的 tf.train.CheckpointManager 来实现以上需求。
具体而言,在定义 Checkpoint 后接着定义一个 CheckpointManager:
checkpoint = tf.train.Checkpoint(model=model)
manager = tf.train.CheckpointManager(checkpoint, directory='./save', checkpoint_name='model.ckpt', max_to_keep=k)
此处, directory 参数为文件保存的路径, checkpoint_name 为文件名前缀(不提供则默认为 ckpt ), max_to_keep 为保留的 Checkpoint 数目。
在需要保存模型的时候,我们直接使用 manager.save() 即可。如果我们希望自行指定保存的 Checkpoint 的编号,
则可以在保存时加入 checkpoint_number 参数。例如 manager.save(checkpoint_number=100) 。
以下提供一个实例,展示使用 CheckpointManager 限制仅保留最后三个 Checkpoint 文件,并使用 batch 的编号作为 Checkpoint 的文件编号。
manager = tf.train.CheckpointManager(checkpoint, directory='./save', max_to_keep=3)
path = manager.save(checkpoint_number=batch_index)
"""
# 使用tf.train.CheckpointManager管理Checkpoint,用于保存和读取Checkpoint文件
ckpt_manager = tf.train.CheckpointManager(model.ckpt, checkpoint_path, max_to_keep=5)
#模型构建函数:用于加载已经训练好的模型,或者直接使用导入的模型
def create_model():
# 模型保存路径model_data/
ckpt = tf.io.gfile.listdir(checkpoint_path)
if ckpt:
print("reload pretrained model")
"""
当在其他地方需要为模型重新载入之前保存的参数时,需要再次实例化一个 Checkpoint,同时保持键名的一致。
再调用 checkpoint 的 restore 方法。就像下面这样:
model_to_be_restored = MyModel() # 待恢复参数的同一模型
ckpt = tf.train.Checkpoint(myAwesomeModel=model_to_be_restored) # 键名保持为“myAwesomeModel”
ckpt.restore(save_path_with_prefix_and_index)
即可恢复模型变量。 save_path_with_prefix_and_index 是之前保存的文件的目录 + 前缀 + 编号。
例如,调用 checkpoint.restore('./save/model.ckpt-1') 就可以载入前缀为 model.ckpt ,序号为 1 的文件来恢复模型。
当保存了多个文件时,我们往往想载入最近的一个。可以使用 tf.train.latest_checkpoint(save_path)
这个辅助函数返回目录下最近一次 checkpoint 的文件名。
例如如果 save 目录下有 model.ckpt-1.index 到 model.ckpt-10.index 的 10 个保存文件,
tf.train.latest_checkpoint('./save') 即返回 ./save/model.ckpt-10 。
"""
model.ckpt.restore(tf.train.latest_checkpoint(checkpoint_path))
return model
else:
return model
#训练模式
def train():
# 模型构建函数:用于加载已经训练好的模型,或者直接使用导入的模型
model=create_model()
# while True:
for epoch in range(gConfig['epochs']): #按照epoch值进行循环训练
# start = time.time()
model.train_loss.reset_states() #重置清零loss函数Mean
model.train_accuracy.reset_states()#重置清零指标值accuracy
#开始批量循环训练,每一步所训练的数据大小都是一个批量大小64
#遍历的值batch从0开始
for (batch,(inp, target)) in enumerate(dataset_train.batch(gConfig['batch_size'])):
start = time.time()
# print(inp.shape) #(batch_size, 100)
# print(target.shape) #(batch_size,)
#定义一个既能完成训练任务,也能完成预测任务,训练模式时返回的是一步step(一个batch批量大小)的loss值,预测模式时返回的是预测值
loss, trainAccuracy = model.step(inp, target)
print ('训练集:Epoch {:} ,Batch {:} ,Loss {:.4f}, Accuracy {:.4f},Prestep {:.4f}'.format(epoch + 1, batch, loss.numpy(), trainAccuracy.numpy(), (time.time()-start)))
# 定义一个验证函数,使用测试集数据对训练好的模型进行预测验证
#for (batch,(inp,target)) in enumerate(dataset_test.batch(gConfig['batch_size'])):
#start = time.time()
# loss = model.evaluate(inp,target)
# print ('测试集:Epoch {:} ,Batch {:} ,Loss {:.4f},Prestep {:.4f}'.format(epoch + 1, batch,loss.numpy(),(time.time()-start)))
# 使用CheckpointManager保存模型参数到文件并自定义编号 tf.train.CheckpointManager(...).save(checkpoint_number=batch_index)
# ckpt_save_path=ckpt_manager.save()
# print ('保存epoch{}模型在 {}'.format(epoch+1, ckpt_save_path))
#把输入的一行句子中的每个单词 转换为 “单词在字典中对应的”value值(出现频率次数)
def text_to_vector(inp):
#读取记录了每个单词和其对应的出现频率次数的本地文件
vocabulary_file=gConfig['vocabulary_file']
tmp_vocab = []
with open(vocabulary_file, "r") as f:#读取字典文件的数据,生成一个dict,也就是键值对的字典
#readlines() 一次性读取多行
tmp_vocab.extend(f.readlines())
# 遍历每行数据,strip()表示每行数据按照默认空格进行切割
tmp_vocab = [line.strip() for line in tmp_vocab]
#enumerate 遍历的是(y, x) 即为{1:the}、{2:a},所以还要翻转变成{the:1}、{a:2} 存储到 字典中
#tmp_vocab列表中的数据实际是出现频率最高的前10000个单词,出现频率从高到小的单词按照列表索引0值开始排列,
# 因此遍历出来的单词的出现频率是从高到小的。
vocab = dict([(x, y) for (y, x) in enumerate(tmp_vocab)])
print(vocab)
line_vec = []
#strip()表示每行数据按照默认空格进行切割出每个单词
for words in inp.split():
# 获取单词在字典中对应value值(出现频率次数),如果单词不存在字典中的话就默认返回UNK_ID = 3
line_vec.append(vocab.get(words, UNK_ID))
return line_vec
#预测模式
def predict(sentences):
#pos积极、neg消极
state=['pos','neg']
# 模型构建函数:用于加载已经训练好的模型,或者直接使用导入的模型
model=create_model()
#把输入的一行句子中的每个单词 转换为 “单词在字典中对应的”value值(出现频率次数)
indexes = text_to_vector(sentences)
print(indexes)
# 定义paddding填充函数,对长度不足的语句使用0进行补全。
# 把每行句子的长度要么填充到100,要么截取为100。
inp = pad_sequences([indexes])
#inp[0]获取出一个句子,然后转换为(1, 100)
inp=tf.reshape(inp[0], (1, len(inp[0])))
# 定义一个既能完成训练任务,也能完成预测任务,训练模式时返回的是一步step(一个batch批量大小)的loss值,预测模式时返回的是预测值
# transformer返回输出经过softmax的shape为(1, 2)的概率值
predictions = model.step(inp,inp,False)
#predictions[0] 取出shape为(1, 2)中的(2)的概率值
#argmax 获取最大值所在的索引值
pred = tf.math.argmax(predictions[0])
#tensor值转换为numpy值
p=np.int32(pred.numpy())
#获取是pos积极 还是 neg消极的 标签
return state[p]
if __name__ == "__main__":
# 如果配置文件中配置的是训练模式,则开始训练
if gConfig['mode']=='train':
train()
# 如果配置文件中配置的是服务模式,则直接运行web应用程序
elif gConfig['mode']=='serve':
print('Sever Usage:python3 app.py')
================================================================================================================================
textClassiferModel.py
# coding=utf-8
import tensorflow as tf
import numpy as np
import getConfig
tf.enable_eager_execution()
gConfig={}
gConfig=getConfig.get_config(config_file='config.ini')
#传入 shape为(10000, 1)的0到9999一共10000个数字的一个array、shape为(1, 80)的0到79一共80个数字的一个array、Embedding的长度80
def get_angles(pos, i, d_model):
"""
1.i//2:
即 np.arange(80)[np.newaxis, :]//2,表示shape为(1, 80)的0到79一共80个数字的一个array 整除2,即array中每个数字 整除2,
最终效果如下,每两个值均为相同,数值从0到79降到0到39。
最终值:array([[ 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7,
8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15,
16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23,
24, 24, 25, 25, 26, 26, 27, 27, 28, 28, 29, 29, 30, 30, 31, 31,
32, 32, 33, 33, 34, 34, 35, 35, 36, 36, 37, 37, 38, 38, 39, 39]], dtype=int32)
2.2 * (i//2):
即2 * (np.arange(80)[np.newaxis, :]//2),最终效果如下,每两个值均为相同,数值从0到79降到0到39,然后再重新上升回0到78,
但注意的是现在每个值均是偶数,均可以整除2。
最终值:array([[ 0, 0, 2, 2, 4, 4, 6, 6, 8, 8, 10, 10, 12, 12, 14, 14,
16, 16, 18, 18, 20, 20, 22, 22, 24, 24, 26, 26, 28, 28, 30, 30,
32, 32, 34, 34, 36, 36, 38, 38, 40, 40, 42, 42, 44, 44, 46, 46,
48, 48, 50, 50, 52, 52, 54, 54, 56, 56, 58, 58, 60, 60, 62, 62,
64, 64, 66, 66, 68, 68, 70, 70, 72, 72, 74, 74, 76, 76, 78, 78]], dtype=int32)
3.(2 * (i//2)) / np.float32(d_model):
即 (2 * (np.arange(80)[np.newaxis, :]//2)) / np.float32(80)
最终值:array([[0. , 0. , 0.025, 0.025, 0.05 , 0.05 , 0.075, 0.075, 0.1 ,
0.1 , 0.125, 0.125, 0.15 , 0.15 , 0.175, 0.175, 0.2 , 0.2 ,
0.225, 0.225, 0.25 , 0.25 , 0.275, 0.275, 0.3 , 0.3 , 0.325,
0.325, 0.35 , 0.35 , 0.375, 0.375, 0.4 , 0.4 , 0.425, 0.425,
0.45 , 0.45 , 0.475, 0.475, 0.5 , 0.5 , 0.525, 0.525, 0.55 ,
0.55 , 0.575, 0.575, 0.6 , 0.6 , 0.625, 0.625, 0.65 , 0.65 ,
0.675, 0.675, 0.7 , 0.7 , 0.725, 0.725, 0.75 , 0.75 , 0.775,
0.775, 0.8 , 0.8 , 0.825, 0.825, 0.85 , 0.85 , 0.875, 0.875,
0.9 , 0.9 , 0.925, 0.925, 0.95 , 0.95 , 0.975, 0.975]])
4.pos / np.power(10000, (2 * (i//2)) / np.float32(d_model))
即 np.arange(10000)[:, np.newaxis] / np.power(10000, (2 * (np.arange(80)[np.newaxis, :]//2)) / np.float32(80))
最终值的shape为 (10000, 80)
最终值:array([[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, ...,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
[1.00000000e+00, 1.00000000e+00, 7.94328235e-01, ...,
1.58489319e-04, 1.25892541e-04, 1.25892541e-04],
[2.00000000e+00, 2.00000000e+00, 1.58865647e+00, ...,
3.16978638e-04, 2.51785082e-04, 2.51785082e-04],
...,
[9.99700000e+03, 9.99700000e+03, 7.94089936e+03, ...,
1.58441772e+00, 1.25854773e+00, 1.25854773e+00],
[9.99800000e+03, 9.99800000e+03, 7.94169369e+03, ...,
1.58457621e+00, 1.25867363e+00, 1.25867363e+00],
[9.99900000e+03, 9.99900000e+03, 7.94248802e+03, ...,
1.58473470e+00, 1.25879952e+00, 1.25879952e+00]])
"""
angle_rates = 1 / np.power(10000, (2 * (i//2)) / np.float32(d_model))
return pos * angle_rates #shape为 (10000, 80)
"""
在Transformer结构中使用了位置编码(Positional Encoding)来提取各个词的位置信息,并作为Encoder或Decoder的输入。
Transformer位置编码的实现方式是:
通过正弦函数sin、余弦函数cos 交替编码“未来用于提取每个单词信息的”shape为(字典的大小10000,Embedding的长度80)的矩阵,
正弦函数sin负责对矩阵中每行上索引值为偶数(即偶数列,从0开始算)上的元素值进行编码,
余弦函数cos负责对矩阵中每行上索引值为奇数(即奇数列,从0开始算)上的元素值进行编码,
然后把该编码好的shape为(字典的大小10000,Embedding的长度80)的矩阵 拓增维度为 (1,字典的大小10000,Embedding的长度80),
然后再仅截取出其所编码好的矩阵中的(1,句子的最大长度100,Embedding的长度80)这一小部分,
最后用截取出的这一部分位置编码信息和“shape为(batch_size,句子的最大长度100,Embedding的长度80)的”单词Embedding输出信息两者进行相加,
最终求和的值作为Encoder或Decoder的输入。
"""
#定义一个函数对位置编码信息进行处理,以便能够使用正弦函数和余弦函数对位置编码信息进行编码
#位置编码:传入 字典的大小10000、Embedding的长度80
def positional_encoding(position, d_model):
#np.arange(10000):表示从0遍历到9999,一共10000个数字组成一个array。np.arange(80):表示从0遍历到79,一共80个数字组成一个array。
#np.newaxis:表示增加新一个维度,维度值为1
#angle_rads.shape 为 (10000, 80)
angle_rads = get_angles(np.arange(position)[:, np.newaxis], #(10000,) 然后拓增一个维度为 (10000, 1)
np.arange(d_model)[np.newaxis, :], #(80,) 然后拓增一个维度为 (1, 80)
d_model) #Embedding的长度80
print("positional_encoding angle_rads:",angle_rads.shape) #(10000, 80)
"""
a=[1,2,3,4,5,6,7,8,9]
#第一个值1表示从索引值1开始,第二个值2表示每相隔1个地获取,实际即只获取索引值为奇数上的元素值
>>> print(a[1::2])
[2, 4, 6, 8]
#第一个值0表示从索引值0开始,第二个值2表示每相隔1个地获取,实际即只获取索引值为偶数上的元素值
>>> print(a[0::2])
[1, 3, 5, 7, 9]
#翻转数组中元素的排列顺序,即从尾到头重新排列数组元素
>>> print(a[::-1])
[9, 8, 7, 6, 5, 4, 3, 2, 1]
"""
#正弦函数y = sin(x):shape为(10000, 40),实际即只获取每行上索引值为偶数(即偶数列,从0开始算)上的元素值
sines = np.sin(angle_rads[:, 0::2])
#余弦函数y = cos(x):shape为(10000, 40),实际即只获取每行上索引值为奇数(即奇数列,从0开始算)上的元素值
cosines = np.cos(angle_rads[:, 1::2])
print("positional_encoding sines:",sines.shape) #(10000, 40)
print("positional_encoding cosines:",cosines.shape) #(10000, 40)
#对shape为(10000, 40)的sin值 和 shape为(10000, 40)的cos值 在最后一个维度进行合并,结果值shape为(10000, 80)
pos_encoding = np.concatenate([sines, cosines], axis=-1)
print("positional_encoding pos_encoding:",pos_encoding.shape) #(10000, 80)
#np.newaxis:表示增加新一个维度,维度值为1
#shape 从 (10000, 80) 然后拓增一个维度为 (1, 10000, 80)
pos_encoding = pos_encoding[np.newaxis, ...]
print("positional_encoding pos_encoding:",pos_encoding.shape) #(1, 10000, 80)
#对位置编码进行类型转换为float32再返回
return tf.cast(pos_encoding, dtype=tf.float32)
"""
1.Q与K的相似度计算过程是这样的:
首先使用MatMul函数计算Q和K的相似度(MatMul是一种点积函数)。
为了能够更好地控制计算的复杂度,使用Scale函数对MatMul的计算结果进行缩放。
2.每一次放缩点积注意力的计算结果就是一个头注意力,那么计算多次就是多头注意力。
在每次计算时Q、K、V使用不同的参数进行线性变换,这样虽然进行了多次放缩点积注意力的计算,
但每次计算的结果是不同的。对输入数据进行不同的线性变换操作是特征增强的一种手段,
因为至少从理论上增加了有效特征,可以提高神经网络模型的预测效果。
3.放缩点积注意力的计算过程大概是这样的:
首先计算每个Q与K矩阵的相似度,然后使用softmax对这个相似度向量进行归一化处理得到权重向量,
最后将这个权重向量与V矩阵加权求和得到最终的attention值。
"""
#定义放缩点积注意力计算函数
# 传入的 v、k、q 三者均是“shape为(batch_size, 多头注意力的多头的头数量8, 句子的最大长度100, 10)的融合了位置编码信息的”单词Embedding输出信息x、
# (batch_size, 1, 1, 100)的mask掩码,最终返回 放缩点积注意力输出output、注意力权重attention_weights
def scaled_dot_product_attention(q, k, v, mask):
"""
:param q: 维度为 (batch_size, seq_len_q, depth)
:param k: 维度为 (batch_size, seq_len_k, depth)
:param v: 维度为 (batch_size, seq_len_v, depth)
:param mask: 维度为 (batch_size, 1, 1, 100)
"""
# transpose_a: 如果为真, a则在进行乘法计算前进行转置。 transpose_b: 如果为真, b则在进行乘法计算前进行转置。
#使用MatMul点积相乘函数计算Q和K的相似度
matmul_qk = tf.matmul(q, k, transpose_b=True) # (..., seq_len_q, seq_len_k)
#(batch_size, 多头注意力的多头的头数量8, 句子的最大长度100, 句子的最大长度100) 即 (batch_size, 多头注意力的多头的头数量8, seq_len_q, seq_len_k)
print("scaled_dot_product_attention matmul_qk:",matmul_qk.shape) #(64, 8, 100, 100) 即 (batch_size, 多头注意力的多头的头数量8, 句子的最大长度100, 句子的最大长度100)
#shape(k)[-1] 实际取出了(batch_size, 多头注意力的多头的头数量8, 句子的最大长度100, 10)中的最后一个维度10,作为缩放matmul_qk张量的因子(即作为缩放因子)
dk = tf.cast(tf.shape(k)[-1], tf.float32)
# print("scaled_dot_product_attention dk:",dk.numpy()) #10.0
#1.sqrt(10) = 3.1622776601683795,即求10的平方根
#2.“Q和K两个矩阵的点积计算的结果”即相似度向量matmul_qk / 3.1622776601683795:
# 为了能够更好地控制计算的复杂度,使用Scale函数对MatMul的计算结果进行缩放。
# 相似度向量matmul_qk 除以 3.1622776601683795 相当于使用Scale函数的效果 对 MatMul的计算结果的相似度向量matmul_qk 进行了缩放。
#3.通过matmul_qk张量除以“缩放因子dk的”平方根,实现对matmul_qk张量的缩放操作。
scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)
# (batch_size, 多头注意力的多头的头数量8, 句子的最大长度100, 句子的最大长度100)
print("scaled_dot_product_attention scaled_attention_logits:",scaled_attention_logits.shape) #(64, 8, 100, 100)
if mask is not None:
"""
1.mask的 shape为 (batch_size, 1, 1, 100)
每个句子中 为填充值0 的equal之后都返回True,然后cast转换为1.0的mask掩码,最终计算 1.0 * -1e9 = -1000000000.0
每个句子中 为非填充值0 的equal之后都返回False,然后cast转换为0.0的mask掩码,最终计算 0.0 * -1e9 = -0.0
2.对应 填充值0 的为 scaled_attention_logits - 1000000000.0,即抛弃对应 填充值0 的值
对应 非填充值0 的为 scaled_attention_logits - 0.0,即值保持不变
"""
#因为scaled_attention_logits实质还是“Q和K两个矩阵的点积计算的结果”即相似度向量,而Q和K实质都是融合了位置编码信息的单词Embedding输出信息,
#因此需要使用到mask掩码对源输入句子中的填充值0进行去除,最终对应填充值0位置上的Embedding值都被置为了极大的负数(约等于-1000000000.0)
scaled_attention_logits += (mask * -1e9)
print("scaled_dot_product_attention mask:", scaled_attention_logits.shape) #(64, 8, 100, 100) 即 (batch_size, 多头注意力的多头的头数量8, 句子的最大长度100, 句子的最大长度100)
#使用softmax对经过缩放后的相似度向量scaled_attention_logits 进行归一化处理 得到 注意力权重attention_weights
attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1) # (..., seq_len_q, seq_len_k)
print("scaled_dot_product_attention attention_weights:", attention_weights.shape)#(64, 8, 100, 100) 即 (batch_size, 多头注意力的多头的头数量8, 句子的最大长度100, 句子的最大长度100)
#使用 注意力权重attention_weights 最后和 V矩阵(“shape为(batch_size, 多头注意力的多头的头数量8, 句子的最大长度100, 10)的融合了位置编码信息的”
# 单词Embedding输出信息) 两者进行MatMul点积运算 得到最终的 放缩点积注意力输出output
output = tf.matmul(attention_weights, v) # (..., seq_len_v, depth)
print("scaled_dot_product_attention output:", output.shape) #(64, 8, 100, 10) 即 (batch_size, 多头注意力的多头的头数量8, 句子的最大长度100, 10)
#返回 放缩点积注意力输出output、注意力权重attention_weights
return output, attention_weights
#定义一个MultiHeadAttention类:实现多头注意力机制
class MultiHeadAttention(tf.keras.layers.Layer):
# 初始化多头注意力机制的头的头数量和Embedding长度:传入 Embedding的长度80、多头注意力的多头的头数量8
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
self.d_model = d_model #Embedding的长度80
self.num_heads = num_heads #多头注意力的多头的头数量8
#assert作用:断定 多头注意力的多头的头数量 能够被 Embedding长度 进行整除,如果不能整除,则不会再执行后面代码
#判断 Embedding的长度80 % 多头注意力的多头的头数量8 == 0
assert d_model % self.num_heads == 0
#depth 为网络深度
#Embedding的长度80 // 多头注意力的多头的头数量8 = 10,此处为整除运算
self.depth = d_model // self.num_heads
print("MultiHeadAttention depth:",self.depth) #10
#下面wq、wk、wv分别为Dense全连接层,神经元数量均为 Embedding的长度80,分别用于对q、k、v进行线性变换
self.wq = tf.keras.layers.Dense(d_model)
self.wk = tf.keras.layers.Dense(d_model)
self.wv = tf.keras.layers.Dense(d_model)
self.dense = tf.keras.layers.Dense(d_model)
#传入x 实际可以分别为v、k、q,三者均是“shape为(batch_size, 句子的最大长度100, Embedding的长度80)的融合了位置编码信息的”单词Embedding输出信息x
def split_heads(self, x, batch_size):
#实际是把 原本第三维度的Embedding的长度80 分割为 多头注意力的多头的头数量8 * 10,8替换到第三维度,10作为新的第四维度
#最终v、k、q,三者实际转换成 (batch_size, 句子的最大长度100, 多头注意力的多头的头数量8, 10)。
x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))
#transpose:把(batch_size, 句子的最大长度100, 多头注意力的多头的头数量8, 10) 转换为 (batch_size, 多头注意力的多头的头数量8, 句子的最大长度100, 10)
return tf.transpose(x, perm=[0, 2, 1, 3])
#传入 融合了位置编码信息的单词Embedding输出信息x、融合了位置编码信息的单词Embedding输出信息x、
# 融合了位置编码信息的单词Embedding输出信息x、(batch_size, 1, 1, 100)的mask掩码。
#实际即 v、k、q 三者均是 融合了位置编码信息的单词Embedding输出信息x
def call(self, v, k, q, mask):
#q的shape为 (batch_size, 句子的最大长度100, Embedding的长度80),shape(q)[0] 实则为 batch_size
batch_size = tf.shape(q)[0]
# print("MultiHeadAttention batch_size:",batch_size.numpy()) # batch_size 64
# 下面wq、wk、wv分别为Dense全连接层,神经元数量均为 Embedding的长度80,分别用于对q、k、v进行线性变换
# 实际即 v、k、q 三者均是“shape为(batch_size, 句子的最大长度100, Embedding的长度80)的融合了位置编码信息的”单词Embedding输出信息x
q = self.wq(q) # (batch_size, seq_len, d_model)
k = self.wk(k) # (batch_size, seq_len, d_model)
v = self.wv(v) # (batch_size, seq_len, d_model)
print("MultiHeadAttention q:",q.shape) #(64, 100, 80) 即 (batch_size, 句子的最大长度100, Embedding的长度80)
print("MultiHeadAttention k:",k.shape) #(64, 100, 80)
print("MultiHeadAttention v:",v.shape) #(64, 100, 80)
#传入的v、k、q 三者均是“shape为(batch_size, 句子的最大长度100, Embedding的长度80)的融合了位置编码信息的”单词Embedding输出信息x,
#最终函数返回输出的v、k、q的shape 都从原来的(batch_size, 句子的最大长度100, Embedding的长度80) 均转换为了
# (batch_size, 多头注意力的多头的头数量8, 句子的最大长度100, 10)。
q = self.split_heads(q, batch_size) # (batch_size, num_heads, seq_len_q, depth)
k = self.split_heads(k, batch_size) # (batch_size, num_heads, seq_len_k, depth)
v = self.split_heads(v, batch_size) # (batch_size, num_heads, seq_len_v, depth)
print("MultiHeadAttention split_heads q:",q.shape) #(64, 8, 100, 10) 即 (batch_size, 多头注意力的多头的头数量8, 句子的最大长度100, 10)
print("MultiHeadAttention split_heads k:",k.shape) #(64, 8, 100, 10)
print("MultiHeadAttention split_heads v:",v.shape) #(64, 8, 100, 10)
"""
1.多头注意力是由多个Scaled Dot-Product Attention(放缩点积注意力)堆叠而得到的。
放缩点积注意力:点积是我们常用的计算相似度的方法之一,放缩指内积的大小是可控的。
与常见的注意力机制相比,放缩点积注意力机制主要是在相似计算和内积调节控制方面进行了改进。
2.多头注意力又是怎么来的呢?这个其实很好理解,每一次放缩点积注意力的计算结果就是一个头注意力,
那么计算多次就是多头注意力。在每次计算时Q、K、V使用不同的参数进行线性变换,
这样虽然进行了多次放缩点积注意力的计算,但每次计算的结果是不同的。
对输入数据进行不同的线性变换操作是特征增强的一种手段,因为至少从理论上增加了有效特征,
可以提高神经网络模型的预测效果。
3.放缩点积注意力的计算过程大概是这样的:
首先计算每个Q与K矩阵的相似度,然后使用softmax对这个相似度向量进行归一化处理得到权重向量,
最后将这个权重向量与V矩阵加权求和得到最终的attention值。
"""
#调用 放缩点积注意力计算函数 来计算q、k、v的 放缩点积注意力输出 和 注意力权重
# 传入的 v、k、q 三者均是“shape为(batch_size, 多头注意力的多头的头数量8, 句子的最大长度100, 10)的融合了位置编码信息的”单词Embedding输出信息x、
# (batch_size, 1, 1, 100)的mask掩码。
#最终返回 放缩点积注意力输出scaled_attention、注意力权重attention_weights
scaled_attention, attention_weights = scaled_dot_product_attention(q, k, v,mask)
#把 (batch_size, 多头注意力的多头的头数量8, 句子的最大长度100, 10) 转换为 (batch_size, 句子的最大长度100, 多头注意力的多头的头数量8, 10)
scaled_attention = tf.transpose(scaled_attention, perm=[0, 2, 1, 3]) # (batch_size, seq_len_v, num_heads, depth)
print("MultiHeadAttention scaled_attention:",scaled_attention.shape) #(64, 100, 8, 10) 即 (batch_size, 句子的最大长度100, 多头注意力的多头的头数量8, 10)
#把 (batch_size, 句子的最大长度100, 多头注意力的多头的头数量8, 10) 转换为 (batch_size, 句子的最大长度100, Embedding的长度80)
concat_attention = tf.reshape(scaled_attention, (batch_size, -1, self.d_model)) # (batch_size, seq_len_v, d_model)
print("MultiHeadAttention concat_attention:",concat_attention.shape) #(64, 100, 80) 即 (batch_size, 句子的最大长度100, Embedding的长度80)
#最后一次线性变换得到最终的多头注意力输出
#dense全连接层的神经元数量均为 Embedding的长度80,所以dense全连接层的输出的shape仍然为 (batch_size, 句子的最大长度100, Embedding的长度80)
output = self.dense(concat_attention) # (batch_size, seq_len_v, d_model)
print("MultiHeadAttention output:",output.shape) #(64, 100, 80) 即 (batch_size, 句子的最大长度100, Embedding的长度80)
#返回 shape为(batch_size, 句子的最大长度100, Embedding的长度80)的多头注意力输出、注意力权重attention_weights
return output, attention_weights
# 点式前馈网络Feed Forward:
# 有两个Dense全连接层组成,在Transformer结构中,Feed Forward是前馈神经网络层,
# 其作用是将Multi-head Attention(多头注意力)层输出的数据进行非线性变换后输出。
#传入 Embedding的长度80、点式前馈网络Feed Forward中第一个Dense全连接层的神经元数量1024
def point_wise_feed_forward_network(d_model, dff):
return tf.keras.Sequential([
tf.keras.layers.Dense(dff, activation='relu'), #神经元数量为1024 (batch_size, 句子的最大长度100, 神经元数量1024)
tf.keras.layers.Dense(d_model) #神经元数量为 Embedding的长度80 (batch_size, 句子的最大长度100, 神经元数量为80)
])
#定义EncoderLayer神经网络层
class EncoderLayer(tf.keras.layers.Layer):
# 初始化EncoderLayer:传入 Embedding的长度80、点式前馈网络Feed Forward中第一个Dense全连接层的神经元数量1024、多头注意力的多头的头数量8、神经元失效的比例概率0.1
def __init__(self, d_model, diff, num_heads, rate=0.1):
super(EncoderLayer, self).__init__()
#初始化多头注意力机制:传入Embedding的长度80、多头注意力的多头的头数量8
self.mha = MultiHeadAttention(d_model, num_heads)
# 初始化 点式前馈网络(point wise Feed Forward network):
# 有两个Dense全连接层组成,在Transformer结构中,Feed Forward是前馈神经网络层,
# 其作用是将Multi-head Attention(多头注意力)层输出的数据进行非线性变换后输出。
#传入 Embedding的长度80、点式前馈网络Feed Forward中第一个Dense全连接层的神经元数量1024
self.ffn = point_wise_feed_forward_network(d_model, diff)
#Dropout层:神经元失效的比例概率0.1
self.dropout1 = tf.keras.layers.Dropout(rate)
self.dropout2 = tf.keras.layers.Dropout(rate)
#初始化Normalization(归一化)层:进行Normalization(归一化)之后,数据分布更加集中,神经网络可以更快地找到最优解
#epsilon:该参数是一个非常小的数值,防止出现除以零的情况。1e-6是科学计数法,就是1x10的-6次方就是0.000001。
self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
# 传入 “shape为(batch_size, 句子的最大长度100, Embedding的长度80)的融合了位置编码信息的”单词Embedding输出信息x、
# True、(batch_size, 1, 1, 100)的mask掩码
def call(self, x, training, mask):
#1.调用MultiHeadAttention的call函数:
# 传入 融合了位置编码信息的单词Embedding输出信息x、融合了位置编码信息的单词Embedding输出信息x、
# 融合了位置编码信息的单词Embedding输出信息x、(batch_size, 1, 1, 100)的mask掩码
#2.最终MultiHeadAttention的call函数 返回 “shape为(batch_size, 句子的最大长度100, Embedding的长度80)的”多头注意力输出、注意力权重attention_weights
attn_output, _ = self.mha(x, x, x, mask) # (batch_size, input_seq_len, d_model)
# Dropout层:神经元失效的比例概率0.1。实现对神经网络参数的正则化,防止过拟合。
attn_output = self.dropout1(attn_output, training=training)
#1.Normalization层:进行Normalization(归一化)之后,数据分布更加集中,神经网络可以更快地找到最优解
#2.对“shape为(batch_size, 句子的最大长度100, Embedding的长度80)的融合了位置编码信息的”单词Embedding输出信息x 和
# “shape为(batch_size, 句子的最大长度100, Embedding的长度80)的”多头注意力输出attn_output 两者进行求和,
# 然后把求和的值再经过Normalization层(归一化)。
#3.融合了位置编码信息的单词Embedding输出信息x 和 多头注意力输出attn_output 进行相加操作 目的是 用于加强特征
out1 = self.layernorm1 (x + attn_output) # (batch_size, input_seq_len, d_model)
print("EncoderLayer out1:",out1.shape) # (64, 100, 80) 即 (batch_size, 句子的最大长度100, Embedding的长度80)
#1.点式前馈网络(point wise Feed Forward network):
# 有两个Dense全连接层组成,在Transformer结构中,Feed Forward是前馈神经网络层,
# 其作用是将Multi-head Attention(多头注意力)层输出的数据进行非线性变换后输出。
#2.前馈网络中的输出层的神经元数量为Embedding的长度80,那么终于输出数据的shape仍然为(batch_size, 句子的最大长度100, Embedding的长度80)
ffn_output = self.ffn(out1) # (batch_size, input_seq_len, d_model)
print("EncoderLayer ffn_output:",ffn_output.shape) # (64, 100, 80) 即 (batch_size, 句子的最大长度100, Embedding的长度80)
# Dropout层:神经元失效的比例概率0.1。实现对神经网络参数的正则化,防止过拟合。
ffn_output = self.dropout2(ffn_output, training=training)
#把已经经过了Normalization层(归一化)的多头注意力输出out1 和 前馈网络输出ffn_output 进行求和之后,再经过Normalization层(归一化)
#前馈网络的输入out1 和 前馈网络的输出ffn_output 进行相加操作 目的是 用于加强特征
out2 = self.layernorm2 (out1 + ffn_output) # (batch_size, input_seq_len, d_model)
print("EncoderLayer out2:",out2.shape) # (64, 100, 80) 即 (batch_size, 句子的最大长度100, Embedding的长度80)
return out2
#定义的Encoder编码器是Layer类型
class Encoder(tf.keras.layers.Layer):
# 初始化编码器:
# 传入 EncoderLayer层数4、Embedding的长度80、点式前馈网络Feed Forward中第一个Dense全连接层的神经元数量1024、多头注意力的多头的头数量8、
# 字典的大小10000、神经元失效的比例概率0.1。
def __init__(self, num_layers, d_model, dff, num_heads, input_vocab_size, rate=0.1):
super(Encoder, self).__init__()
self.num_layers = num_layers #EncoderLayer层数4
self.d_model = d_model #Embedding的长度80
#创建Embedding层:传入 字典的大小10000、Embedding的长度80
self.embedding = tf.keras.layers.Embedding(input_vocab_size, d_model)
"""
在Transformer结构中使用了位置编码(Positional Encoding)来提取各个词的位置信息,并作为Encoder或Decoder的输入。
Transformer位置编码的实现方式是:
通过正弦函数sin、余弦函数cos 交替编码“未来用于提取每个单词信息的”shape为(字典的大小10000,Embedding的长度80)的矩阵,
正弦函数sin负责对矩阵中每行上索引值为偶数(即偶数列,从0开始算)上的元素值进行编码,
余弦函数cos负责对矩阵中每行上索引值为奇数(即奇数列,从0开始算)上的元素值进行编码,
然后把该编码好的shape为(字典的大小10000,Embedding的长度80)的矩阵 拓增维度为 (1,字典的大小10000,Embedding的长度80),
然后再仅截取出其所编码好的矩阵中的(1,句子的最大长度100,Embedding的长度80)这一小部分,
最后用截取出的这一部分位置编码信息和“shape为(batch_size,句子的最大长度100,Embedding的长度80)的”单词Embedding输出信息两者进行相加,
最终求和的值作为Encoder或Decoder的输入。
"""
#位置编码:传入 字典的大小10000、Embedding的长度80,最后返回的pos_encoding位置编码的shape为(1, 10000, 80)
self.pos_encoding = positional_encoding(input_vocab_size, self.d_model)
print("Encoder pos_encoding:",self.pos_encoding.shape) #(1, 10000, 80)
#num_layers=4:表示EncoderLayer层数为4,即Encoder编码器有4层
#初始化EncoderLayer:传入 Embedding的长度80、点式前馈网络Feed Forward中第一个Dense全连接层的神经元数量1024、多头注意力的多头的头数量8、神经元失效的比例概率0.1
self.enc_layers = [EncoderLayer(d_model, dff, num_heads, rate)for _ in range(num_layers)]
# Dropout层:神经元失效的比例概率0.1
self.dropout = tf.keras.layers.Dropout(rate)
#传入 (batch_size, 100)的特征数据、True、(batch_size, 1, 1, 100)的mask掩码
def call(self, x, training, mask):
#对 (batch_size, 100)的特征数据 获取出seq_len(句子的最大长度)为100
seq_len = tf.shape(x)[1]
# print("Encoder seq_len:",seq_len.numpy()) #100
# 把 每个单词的embedding值x 和 (Positional Encoding)位置编码值pos_encoding 两者进行求和
x = self.embedding(x) # (batch_size, 句子的最大长度100, Embedding的长度80)
print("Encoder embedding(x):",x.shape) #(64, 100, 80) 即 (batch_size, 句子的最大长度100, Embedding的长度80)
#sqrt求平方根:求Embedding的长度80的平方根为8.94427190999916
# 每个单词的embedding值x *= 8.94427190999916
x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
print("Encoder sqrt:",x.shape) # (64, 100, 80) 即 (batch_size, 句子的最大长度100, Embedding的长度80)
"""
将shape为(1,句子的最大长度100,Embedding的长度80)的位置编码信息 和
“shape为(batch_size,句子的最大长度100,Embedding的长度80)的”单词Embedding输出信息两者进行相加,
最终求和的值作为Encoder或Decoder的输入。
"""
#pos_encoding[:, :seq_len, :]:从shape为(1, 10000, 80)的位置编码值pos_encoding 按照[:, :100, :]取出 (1, 句子的最大长度100, 80)
x += self.pos_encoding[:, :seq_len, :]
print("Encoder pos_encoding:",x.shape) # (64, 100, 80) 即 (batch_size, 句子的最大长度100, Embedding的长度80)
# Dropout层:神经元失效的比例概率0.1
x = self.dropout(x, training=training)
#num_layers=4:表示EncoderLayer层数为4,即Encoder编码器有4层
for i in range(self.num_layers):
"""
1.Encoder编码器 此处由4层EncoderLayer组成,每层EncoderLayer由多头注意力机制和Feed Forward前馈神经网络层组成。
2.多头注意力机制的组成:
v、k、q分别均是融合了位置编码信息的单词Embedding输出信息,
v、k、q三者之间进行多次的放缩点积注意力计算(即MatMul点积运算),然后再对计算结果进行缩放(为了能够更好地控制计算的复杂度)。
3.Feed Forward前馈神经网络层:
作用是将(Multi-head Attention)多头注意力输出的数据进行非线性变换后再输出,
最终把该输出作为EncoderLayer层的输出。
4.Encoder编码器把4层EncoderLayer构建成一个相当于Sequential顺序的结构,
第一层EncoderLayer对“融合了位置编码信息的”单词Embedding输出信息进行处理后输出,
第一层EncoderLayer的输出作为第二层EncoderLayer的输入,第二层EncoderLayer的输出作为第三层EncoderLayer的输入,
以此类推。
"""
#调用EncoderLayer的call函数:
# 传入 “shape为(batch_size, 句子的最大长度100, Embedding的长度80)的融合了位置编码信息的”单词Embedding输出信息、
# True、(batch_size, 1, 1, 100)的mask掩码
x = self.enc_layers[i](x, training, mask)
print("Encoder x:", x.shape) # (64, 100, 80) 即 (batch_size, 句子的最大长度100, Embedding的长度80)
return x # (batch_size, input_seq_len, d_model)
#定义的Transformer是Model类型,此处定义的Transformer类实际是改造过的特别版的Transformer,因为此处只有Encoder编码器,而没有对应的Decoder解码器,
#并且当前Transformer模型处理的是分类任务,所以我们此处只用了Encoder编码器来提取特征,最后通过全连接层网络来拟合分类。
class Transformer(tf.keras.Model):
# 初始化Transformer类:
# 传入EncoderLayer层数4、Embedding的长度80、点式前馈网络Feed Forward中第一个Dense全连接层的神经元数量1024、多头注意力的多头的头数量8、
# 字典的大小10000、神经元失效的比例概率0.1。
def __init__(self, num_layers, d_model, dff, num_heads, input_vocab_size, rate=0.1):
super(Transformer, self).__init__()
#初始化编码器:Encoder编码器用来提取特征
# 传入EncoderLayer层数4、Embedding的长度80、点式前馈网络Feed Forward中第一个Dense全连接层的神经元数量1024、多头注意力的多头的头数量8、
# 字典的大小10000、神经元失效的比例概率0.1。
self.encoder = Encoder(num_layers, d_model, dff, num_heads, input_vocab_size, rate)
#输出层:Dense全连接层作为分类层,使用softmax把输出值转换为概率值
self.ffn_out=tf.keras.layers.Dense(2, activation='softmax')
#Dropout层:神经元失效的比例概率0.1
self.dropout1 = tf.keras.layers.Dropout(rate)
# 调用Transformer的call函数:传入(batch_size, 100)的特征数据、True、(batch_size, 1, 1, 100)的mask掩码
def call(self, inp, training, enc_padding_mask):
"""
1.Encoder编码器 此处由4层EncoderLayer组成,每层EncoderLayer由多头注意力机制和Feed Forward前馈神经网络层组成。
2.多头注意力机制的组成:
v、k、q分别均是融合了位置编码信息的单词Embedding输出信息,
v、k、q三者之间进行多次的放缩点积注意力计算(即MatMul点积运算),然后再对计算结果进行缩放(为了能够更好地控制计算的复杂度)。
3.Feed Forward前馈神经网络层:
作用是将(Multi-head Attention)多头注意力输出的数据进行非线性变换后再输出,
最终把该输出作为EncoderLayer层的输出。
4.Encoder编码器把4层EncoderLayer构建成一个相当于Sequential顺序的结构,
第一层EncoderLayer对“融合了位置编码信息的”单词Embedding输出信息进行处理后输出,
第一层EncoderLayer的输出作为第二层EncoderLayer的输入,第二层EncoderLayer的输出作为第三层EncoderLayer的输入,
以此类推。
"""
#对输入语句使用Encoder编码器来提取特征,调用Encoder的call函数:传入 (batch_size, 100)的特征数据、True、(batch_size, 1, 1, 100)的mask掩码
#返回Encoder编码器提取好出来的特征值
enc_output = self.encoder(inp, training, enc_padding_mask) # (batch_size, inp_seq_len, d_model)
print("Transformer enc_output:", enc_output.shape) # (batch_size, 句子的最大长度100, Embedding的长度80)
#设定输出shape:句子的最大长度100 * Embedding的长度80
out_shape=gConfig['sentence_size'] * gConfig['embedding_size']
#把 (batch_size, 句子的最大长度100, Embedding的长度80) 转换为 (batch_size, 句子的最大长度100 * Embedding的长度80)
#即 (batch_size, 800)
enc_output=tf.reshape(enc_output,[-1, out_shape])
print("Transformer enc_output reshape:", enc_output.shape) #(batch_size, 句子的最大长度100 * Embedding的长度80) 即 (batch_size, 800)
# Dropout层:神经元失效的比例概率0.1
ffn = self.dropout1(enc_output,training=training)
#当前Transformer模型处理的是分类任务,所以使用softmax,最后通过全连接层网络来拟合分类
#输出层:Dense全连接层作为分类层,使用softmax把输出值转换为概率值
ffn_out = self.ffn_out(ffn)
print("Transformer ffn_out:", ffn_out.shape) #(batch_size, 2)
return ffn_out
#LearningRateSchedule自适应的学习率:可以按照 epoch次数/自定义step步数 自动调整学习率
#定义一个学习率自动规划类,实现根据不同的训练集和训练速度进行自动设置学习率
class CustomSchedule(tf.keras.optimizers.schedules.LearningRateSchedule):
#传入 Embedding的长度80,还定义有默认的warmup_steps预热学习步数40
def __init__(self, d_model, warmup_steps=40):
super(CustomSchedule, self).__init__()
self.d_model = d_model#Embedding的长度80
self.d_model = tf.cast(self.d_model, tf.float32)
self.warmup_steps = warmup_steps #默认的warmup_steps预热学习步数40
#取 arg1和arg2 两者其中一个最小值 作为 学习率计算的因子之一,
#最终把“Embedding长度80的平方根的倒数 乘以 arg1和arg2两者其中一个最小值的”乘积结果 作为 学习率。
#arg1为训练步数的平方根的倒数,arg2为训练步数乘以warmup_steps预热学习步数40的-1.5次方
def __call__(self, step):
#rsqrt:
# 用于计算传入值x的平方根的倒数,rsqrt等同于 y = 1/sqrt(x),rsqrt(4.0) == 1/sqrt(4.0) == 0.5
# 传入值x必须为以下类型的张量:bfloat16,half,float32,float64,complex64,complex128
arg1 = tf.math.rsqrt(step)
#warmup_steps ** -1.5:40 ** -5 = 9.765625e-09
arg2 = step * (self.warmup_steps ** -1.5)
#tf.math.rsqrt(80.0)的值为0.1118034
#tf.math.minimum:获取两个数值中最小的一个
# 取 arg1和arg2 两者其中一个最小值 作为 学习率计算的因子之一,
# 最终把“Embedding长度80的平方根的倒数 乘以 arg1和arg2两者其中一个最小值的”乘积结果 作为 学习率
return tf.math.rsqrt(self.d_model) * tf.math.minimum(arg1, arg2)
#实例化一个学习率自动规划函数:传入 Embedding的长度80,返回自动规划好的学习率
learning_rate = CustomSchedule(gConfig['embedding_size'])
#Adam优化器
optimizer = tf.keras.optimizers.Adam(learning_rate)
#temp_learning_rate_schedule = CustomSchedule(gConfig['embedding_size'])
#此处的Mean用于对传入的批量的loss值求一个平均值,相当于tf.reduce_mean(loss)的作用
train_loss = tf.keras.metrics.Mean(name='train_loss')
#准确率的评估方法
train_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='train_accuracy')
#初始化Transformer类:
# 传入EncoderLayer层数4、Embedding的长度80、点式前馈网络Feed Forward中第一个Dense全连接层的神经元数量1024、多头注意力的多头的头数量8、
# 字典的大小10000、神经元失效的比例概率0.1。
transformer = Transformer(gConfig['num_layers'], gConfig['embedding_size'], gConfig['diff'], gConfig['num_heads'],
gConfig['vocabulary_size'], gConfig['dropout_rate'])
"""
tf.train.Checkpoint 变量的保存与恢复
1.Checkpoint 只保存模型的参数,不保存模型的计算过程,因此一般用于在具有模型源代码的时候恢复之前训练好的模型参数。
如果需要导出模型(无需源代码也能运行模型).
2.TensorFlow 提供了 tf.train.Checkpoint 这一强大的变量保存与恢复类,可以使用其 save() 和 restore() 方法,
将 TensorFlow 中所有包含 Checkpointable State 的对象进行保存和恢复。具体而言,tf.keras.optimizer 、 tf.Variable 、
tf.keras.Layer 或者 tf.keras.Model 实例都可以被保存。
其使用方法非常简单,我们首先声明一个 Checkpoint:checkpoint = tf.train.Checkpoint(model=model)
3.这里 tf.train.Checkpoint() 接受的初始化参数比较特殊,是一个 **kwargs 。
具体而言,是一系列的键值对,键名可以随意取,值为需要保存的对象。
例如,如果我们希望保存一个继承 tf.keras.Model 的模型实例 model 和一个继承 tf.train.Optimizer 的优化器 optimizer ,
我们可以这样写:checkpoint = tf.train.Checkpoint(myAwesomeModel=model, myAwesomeOptimizer=optimizer)
这里 myAwesomeModel 是我们为待保存的模型 model 所取的任意键名。注意,在恢复变量的时候,我们还将使用这一键名。
接下来,当模型训练完成需要保存的时候,使用:checkpoint.save(save_path_with_prefix) 就可以。
save_path_with_prefix 是保存文件的目录 + 前缀。
"""
#用于保存训练的模型
ckpt = tf.train.Checkpoint(transformer=transformer, optimizer=optimizer)
#创建“对应填充值0的”mask掩码:定义一个padding的遮罩函数,用于去除“输入语句在padding时引入的”噪声
def create_padding_mask(seq):
# 填充值0的equal之后都返回True,然后cast转换为1.0的mask掩码。
# 非填充值0的equal之后都返回False,然后cast转换为0.0的mask掩码。
seq = tf.cast(tf.math.equal(seq, 0), tf.float32)
print("seq.shape:", seq.shape) # (batch_size64, 100)
#tf.newaxis和np.newaxis 均用作增加维度,其增加的维度值为1
return seq[:, tf.newaxis, tf.newaxis, :] # (batch_size64, 1, 1, seq_len) 即 (batch_size64, 1, 1, 100)
#定义一个既能完成训练任务,也能完成预测任务,训练模式时返回的是一步step(一个batch批量大小)的loss值,预测模式时返回的是预测值
def step(inp, tar, train_status = True):
#定义一个padding的遮罩函数,用于去除“输入语句在padding时引入的”噪声
# 创建“对应填充值0的”mask掩码:对应填充值0的mask掩码为1.0,对应非填充值0的mask掩码为0.0。
enc_padding_mask = create_padding_mask(inp)
print("enc_padding_mask.shape:",enc_padding_mask.shape) #(64, 1, 1, 100) 即 (batch_size, 1, 1, 100)
#训练模式
if train_status:
with tf.GradientTape() as tape:
#调用Transformer的call函数:传入(batch_size, 100)的特征数据、True、(batch_size, 1, 1, 100)的mask掩码
#使用 Transformer 对输入语句 进行预测输出
predictions = transformer(inp, True, enc_padding_mask)
print("step predictions:", predictions.shape) #(64, 2) 即 (batch_size, 2)
"""
报错ValueError: setting an array element with a sequence.
tf.keras.utils.to_categorical函数底层的第一行代码np.array(输入, dtype='int')如果报错ValueError: setting an array element with a sequence.
解决方法一:tf.enable_eager_execution() #开启紧急执行
解决方法二:可以尝试使用tf.one_hot(input, num_classes) 来代替 tf.keras.utils.to_categorical(input, num_classes)
"""
#to_categorical:真实标签进行one-hot化,每个标签的维度为2。
#标签值1的one-hot编码为[0,1],标签值0的one-hot编码为[1,0]。而积极的文本数据的标签值用0表示,消极的文本数据的标签值用1表示。
tar_one_hot = tf.keras.utils.to_categorical(tar, 2)
# tar_one_hot = tf.one_hot(tar, 2)
print("step tar_one_hot:", tar_one_hot.shape) # (64, 2) 即 (batch_size, 2)
#使用分类交叉熵来计算loss,传入 one-hot化的真实标签、经过了softmax的预测值,计算两者的loss
loss = tf.keras.losses.categorical_crossentropy(tar_one_hot, predictions)
print("step loss:", loss.shape) # (64,) 即 (batch_size,)
#因为loss仍然为batch_size批量大小的loss,还要求平均得出一个loss
loss = tf.reduce_mean(loss)
#求导出loss对于每个参数的梯度值
gradients = tape.gradient(loss, transformer.trainable_variables)
#使用每个参数对应的梯度值 对参数值本身进行 更新
optimizer.apply_gradients(zip(gradients, transformer.trainable_variables))
"""
此处的train_loss用于对传入的批量的loss值求一个平均值,相当于tf.reduce_mean(loss)的作用
train_loss为 tf.keras.metrics.Mean(name='train_loss') 相当于 tf.reduce_mean(loss)
train_accuracy为 tf.keras.metrics.SparseCategoricalAccuracy
"""
#return train_loss(loss), train_accuracy(tar, predictions)
#此处使用的是SparseCategoricalAccuracy,因此传入的标签值是还没有one-hot化的值
trainAccuracy = train_accuracy(tar, predictions)
# print("trainAccuracy:", trainAccuracy.numpy())
#训练模式时返回的是一步step(一个batch批量大小)的loss值
return loss, trainAccuracy
#预测模式
else:
#使用 Transformer 对输入语句 进行预测输出,transformer返回输出经过softmax的shape为(1, 2)的概率值
predictions = transformer(inp, False, enc_padding_mask)
#预测模式时返回的是预测值
return predictions
#定义一个验证函数,使用测试集数据对训练好的模型进行预测验证
def evaluate(inp,tar):
# 定义一个padding的遮罩函数,用于去除“输入语句在padding时引入的”噪声
# 创建“对应填充值0的”mask掩码:对应填充值0的mask掩码为1.0,对应非填充值0的mask掩码为0.0。
enc_padding_mask = create_padding_mask(inp)
# 使用 Transformer 对输入语句 进行预测输出,transformer返回输出经过softmax的shape为(1, 2)的概率值
predictions= transformer(inp,False,enc_padding_mask)
# to_categorical:真实标签进行one-hot化,每个标签的维度为2。
# 标签值1的one-hot编码为[0,1],标签值0的one-hot编码为[1,0]。而积极的文本数据的标签值用0表示,消极的文本数据的标签值用1表示。
tar = tf.keras.utils.to_categorical(tar, 2)
# 使用分类交叉熵来计算loss,传入 one-hot化的真实标签、经过了softmax的预测值,计算两者的loss
loss =tf.losses.categorical_crossentropy(tar, predictions)
#此处的train_loss用于对传入的批量的loss值求一个平均值,相当于tf.reduce_mean(loss)的作用
#train_loss为 tf.keras.metrics.Mean(name='train_loss') 相当于 tf.reduce_mean(loss)
return train_loss(loss)