- 由之前extract_vectorize.py保存权重说起
- 观察train_x到train_y的模型
- extract_model.py训练完成之后使用指标评估验证集的分数
之前extract_vectorize.py之中的
np.save(data_extract_npy, embeddings)
是将输出的权重内容保存到对应的文件之中,这里extract_model.py之中首先将先前保存的内容读取出来
data = load_data(data_extract_json) data_x = np.load(data_extract_npy) data_y = np.zeros_like(data_x[...,:1])
这里的data是之前抽取的data,还是data[0]为切分出来的字符串,data[1]为对应的下标,data[2]为正式的summaries文本字符串
首先解读这里的data_x[…,:1]的操作
用类似的案例来解读一下这里进行的截取的操作
import tensorflow as tf
import numpy as np
a = np.array([[[1,2,21],[3,4,34]],[[5,6,56],[7,8,78]]])
print('a.shape:',a.shape)
b = a[...,0:2]
print('b :',b)
print('shape.b:',b.shape)
得到的结果如下:
a.shape: (2, 2, 3) b : [[[1 2] [3 4]] [[5 6] [7 8]]] shape.b: (2, 2, 2)
可以看出,这里的
b = a[...,0:2]
是截取的最后一维的0开始的两个数值,得到的b的结果
如果操作为
a = np.array([[[1,2,21],[3,4,34]],[[5,6,56],[7,8,78]]]) b = a[...,1]
只取出一个,得到的坐标会减少一维
得到的b的结果为
a.shape = (2,2,3)
b: [[2 4]
[6 8]]
shape.b: (2,2)
如果想要只取出一个数值并且最后得到的结果保持不变,则写法应该像作者所写的那样,在1的前面加上一个冒号
data_y = np.zeros_like(data_x[...,:1])
然后进入对于data的循环处理之中
for i, d in enumerate(data):
for j in d[1]:
j = int(j)
data_y[i, j] = 1
这里的i,j依次代表第一维,第二维的参数,[i,j]引申起来之后可以不仅仅代表一个对应的数值,也可以代表一整个list
import numpy as np data = np.array([[2,2],[2,2]]) data[0] = 1
这里得到的结果为
data = [[1 1] [2 2]]
原因在于在使用data[0] = 1的时候,指向的是[2,2]这整个list,所以整个list都会变成1
同理,这里的data_y[i,j]指向的是第(i,j)个位置的list,数值为[0],这里标记为data_y[i,j] = 1之后[0]变换为[1],这里同样为整个list的变换过程
接下来,为了弄清楚抽取文本这部分是如何学习的,我们需要弄明白前面预测是如何得来的
(具体内容详见上一篇博客文章,这里简单概述一下,就是(256,80)->(256,80,768)->(256,768)
(117,82)->(117,82,768)->(117,768)
…
最后这无数的向量拼在一起,并且如果第一维如果没有到达最长长度的时候,补充零矩阵,最终的结果为
(20,256,768)
)
而这里的data_y初始化全为零矩阵,如果这一句话被选中为能够作为摘要的备选句子,则将对应的标志置为0,256代表每一个摘要总共有256个句子,这里的train_y = (20,256,1),代表着总共20个摘要,每个摘要256个句子,每个句子1个标签
其实
data = load_data(data_extract_json)
这一波操作没有什么用,这一个现象我们从训练过程中就能看到,主要是train_x到train_y的操作过程
这里切出来的train_x = (18,256,768)
model.fit(
train_x,
train_y,
epochs=epochs,
batch_size=batch_size,
callbacks=[evaluator]
)
对应经历过的网络层结构如下所示:
x = Masking()(x) x = Dropout(0.1)(x) #x = (18,256,768) x = Dense(hidden_size, use_bias=False)(x) x = Dropout(0.1)(x) #x = (18,256,384) x = ResidualGatedConv1D(hidden_size, 3, dilation_rate=1)(x) x = Dropout(0.1)(x) #x = (18,256,384) x = ResidualGatedConv1D(hidden_size, 3, dilation_rate=2)(x) x = Dropout(0.1)(x) #x = (18,256,384) x = ResidualGatedConv1D(hidden_size, 3, dilation_rate=4)(x) x = Dropout(0.1)(x) #x = (18,256,384) x = ResidualGatedConv1D(hidden_size, 3, dilation_rate=8)(x) x = Dropout(0.1)(x) #x = (18,256,384) x = ResidualGatedConv1D(hidden_size, 3, dilation_rate=1)(x) x = Dropout(0.1)(x) #x = (18,256,384) x = ResidualGatedConv1D(hidden_size, 3, dilation_rate=1)(x) x = Dropout(0.1)(x) #x = (18,256,384) x = Dense(1, activation='sigmoid')(x) #x = (18,256,1)
这里的keras.Masking()网络层的作用,我写了一段代码测试了一下:
from keras.layers import *
import numpy as np
from keras.models import Model
import tensorflow as tf
data = tf.convert_to_tensor([[1,1,1,2,3],[1,1,0,0,0]],dtype=tf.float32)
x_in = Input(shape=(None,5))
x_out = Masking()(x_in)
model = Model(x_in,x_out)
data = model(data)
print('data = ')
print(data)
sess = tf.InteractiveSession()
print(data.eval())
得到的结果
data =
Tensor("model_1/masking_1/mul:0", shape=(2, 5), dtype=float32)
[[1. 1. 1. 2. 3.]
[1. 1. 0. 0. 0.]]
???感觉这里没变化???
有大佬知道Masking掩码层的作用,可以评论区指教一下
接下来越过一个Dense的网络层
x = Dense(hidden_size,use_bias=False)(x) x = Dropout(0.1)(x)
得到x的对应维度(18,256,384)
接下来进入一个作者定义的ResidualGatedConv1D网络层门控卷积的神经网络层之中
class ResidualGatedConv1D(Layer):
"""门控卷积
"""
def __init__(self, filters, kernel_size, dilation_rate=1, **kwargs):
super(ResidualGatedConv1D, self).__init__(**kwargs)
self.filters = filters
self.kernel_size = kernel_size
self.dilation_rate = dilation_rate
self.supports_masking = True
def build(self, input_shape):
super(ResidualGatedConv1D, self).build(input_shape)
self.conv1d = Conv1D(
filters=self.filters * 2,
kernel_size=self.kernel_size,
dilation_rate=self.dilation_rate,
padding='same',
)
self.layernorm = LayerNormalization()
if self.filters != input_shape[-1]:
self.dense = Dense(self.filters, use_bias=False)
self.alpha = self.add_weight(
name='alpha', shape=[1], initializer='zeros'
)
def call(self, inputs, mask=None):
if mask is not None:
mask = K.cast(mask, K.floatx())
inputs = inputs * mask[:, :, None]
outputs = self.conv1d(inputs)
gate = K.sigmoid(outputs[..., self.filters:])
outputs = outputs[..., :self.filters] * gate
outputs = self.layernorm(outputs)
if hasattr(self, 'dense'):
inputs = self.dense(inputs)
return inputs + self.alpha * outputs
def compute_output_shape(self, input_shape):
shape = self.conv1d.compute_output_shape(input_shape)
return (shape[0], shape[1], shape[2] // 2)
def get_config(self):
config = {
'filters': self.filters,
'kernel_size': self.kernel_size,
'dilation_rate': self.dilation_rate
}
base_config = super(ResidualGatedConv1D, self).get_config()
return dict(list(base_config.items()) + list(config.items()))
关于门控卷积神经网络层的具体内容回头再作详细的分析,经历了若干个门控卷积层之后,最终进入sigmoid激活函数一下,然后得到最终结果
x = ResidualGatedConv1D(hidden_size, 3, dilation_rate=1)(x) x = Dropout(0.1)(x) #x = (18,256,384) x = Dense(1, activation='sigmoid')(x) #x = (18,256,1)
这里使用的损失函数为二分交叉熵损失函数,优化器为adam优化器
model = Model(x_in, x)
model.compile(
loss='binary_crossentropy', optimizer=Adam(), metrics=['accuracy']
)
#抽取式摘要每次只有选与不选,所以是binary_crossentropy
model.summary()
extract_model.py训练完成之后使用指标评估验证集的分数
每一次训练完成之后,使用相应的评测指标
def evaluate(data, data_x, threshold=0.2):
"""验证集评估
"""
y_pred = model.predict(data_x)[:, :, 0]
total_metrics = {k: 0.0 for k in metric_keys}
for d, yp in tqdm(zip(data, y_pred), desc=u'评估中'):
yp = yp[:len(d[0])]
yp = np.where(yp > threshold)[0]
pred_summary = ''.join([d[0][i] for i in yp])
metrics = compute_metrics(pred_summary, d[2], 'char')
for k, v in metrics.items():
total_metrics[k] += v
return {k: v / len(data) for k, v in total_metrics.items()}
class evaluator(keras.callbacks.Callback):
"""训练回调
"""
def __init__(self):
self.best_metric = 0.0
def on_epoch_end(self, epoch, logs=None):
metrics = evaluate(valid_data, valid_x, threshold + 0.1)
#对应指标的限制为0.3
if metrics['main'] >= self.best_metric: # 保存最优
self.best_metric = metrics['main']
model.save_weights('./weights/extract_model.%s.weights' % fold)
metrics['best'] = self.best_metric
print(metrics)
可以看出,这里evaluator后面的部分为老生常谈的每一个epoch之后保存最佳的分数以及对应的权重值,这里重点看每次预测的内容,每次预测的时候放入对应的valid_data(原装的valid_data数组)以及对应的valid_x的输入内容
目前这里的threshold = 0.3
进入到验证集的评估之中
def evaluate(data,data_x,threshold=0.3): y_pred = model.predict(data_x)[:,:,0]
这里的data_x = (2,256,1),使用了[:,:,0]之后,得到的结果为(2,256)
接下来初始化总的metric_keys指标分数全为0
total_metrics = {k: 0.0 for k in metric_keys}
然后进入到对于每一个data的计算之中
for d,yp in tqdm(zip(data,y_pred),desc=u'评估中'): yp = yp[:len(d[0])] yp = np.where(yp > threshold)[0]
首先yp = yp[:len(d[0])]这里将后面填充的0去除掉,只考虑数组中有的数值。
接下来
yp = np.where(yp > threshold)[0]
这里将超出threshold的数值(目前threshold为0.3)的坐标保留下来
接下来将得到的有可能能够匹配的文本取出来并且拼接起来,
pred_summary = ''.join([d[0][i] for i in yp])
得到从原始文本中拼接之后的summary的文本内容,
然后计算拼接得到的文本内容的各项指标
metrics = compute_metrics(pred_summary, d[2], 'char')
for k, v in metrics.items():
total_metrics[k] += v
最后返回各项指标的平均值
return {k: v / len(data) for k, v in total_metrics.items()}
当计算的平均值较好的时候,保留该模型
注意点:
这里训练抽取模型的时候使用了交叉验证的方法,使得模型训练的效果更好,具体交叉验证切分代码如下:
train_data = data_split(data, fold, num_folds, 'train') valid_data = data_split(data, fold, num_folds, 'valid') train_x = data_split(data_x, fold, num_folds, 'train') valid_x = data_split(data_x, fold, num_folds, 'valid') train_y = data_split(data_y, fold, num_folds, 'train') valid_y = data_split(data_y, fold, num_folds, 'valid')



