python3 extract_model.py对应代码解读抽取式提取+生成式提取摘要代码解读------摘要代码解读3

由之前extract_vectorize.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个标签

观察train_x到train_y的模型

其实

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网络层的源码解读

Masking对应的网络层结构在keras中的core.py中的Masking之中,查看定义

class Masking(Layer):
    """Masks a sequence by using a mask value to skip timesteps.

    If all features for a given sample timestep are equal to `mask_value`,
    then the sample timestep will be masked (skipped) in all downstream layers
    (as long as they support masking).

    If any downstream layer does not support masking yet receives such
    an input mask, an exception will be raised.

    # Example

    Consider a Numpy data array `x` of shape `(samples, timesteps, features)`,
    to be fed to an LSTM layer.
    You want to mask sample #0 at timestep #3, and sample #2 at timestep #5,
    because you lack features for these sample timesteps. You can do:

        - set `x[0, 3, :] = 0.` and `x[2, 5, :] = 0.`
        - insert a `Masking` layer with `mask_value=0.` before the LSTM layer:

    ```python
        model = Sequential()
        model.add(Masking(mask_value=0., input_shape=(timesteps, features)))
        model.add(LSTM(32))
    ```

    # Arguments
        mask_value: Either None or mask value to skip
    """

    def __init__(self, mask_value=0., **kwargs):
        super(Masking, self).__init__(**kwargs)
        self.supports_masking = True
        self.mask_value = mask_value

    def compute_mask(self, inputs, mask=None):
        output_mask = K.any(K.not_equal(inputs, self.mask_value), axis=-1)
        return output_mask

    def call(self, inputs):
        boolean_mask = K.any(K.not_equal(inputs, self.mask_value),
                             axis=-1, keepdims=True)
        return inputs * K.cast(boolean_mask, K.dtype(inputs))

    def get_config(self):
        config = {
    
    'mask_value': self.mask_value}
        base_config = super(Masking, self).get_config()
        return dict(list(base_config.items()) + list(config.items()))

    def compute_output_shape(self, input_shape):
        return input_shape

这里发现mask对于网络层的结构很重要
最常见的一种情况, 在NLP问题的句子补全方法中, 按照一定的长度, 对句子进行填补和截取操作. 一般使用keras.preprocessing.sequence包中的pad_sequences方法, 在句子前面或者后面补0. 但是这些零是我们不需要的, 只是为了组成可以计算的结构才填补的. 因此计算过程中, 我们希望用mask的思想, 在计算中, 屏蔽这些填补0值得作用. keras中提供了mask相关的操作方法.
也就是说,我们填充的0值为了不影响后续的操作,必须使用mask将自己加上的0值掩盖掉,这点是之前我一直忽略的。

复习keras中的compute_mask函数

如果你看到一个网络层的call函数如下所示:

class MultiHeadAttention(keras.layers.Layer):
	def __init__():

	def call(self,inputs,mask=None,**kwargs):

发现调用的时候mask并不为None,但却找不到mask定义的位置,有可能是前面的网络层调用了mask内容。

def compute_mask(self,inputs,mask=None):
    if not self.mask_zero:
       return None
    output_mask = K.not_equal(inputs, 0)
    return output_mask

前面定义的mask会一直传下去,不断地向下面的网络层进行传递。
这里我们通过一个例子来说明mask的作用

import numpy as np
data = np.array([[[1,2,3,0,0],
        [1,2,3,4,5]]])
masks = np.array([[[True],[False]]])
result = data*masks
print('result = ')
print(result)

结果为

result = 
[[[1 2 3 0 0]
  [0 0 0 0 0]]]

可以看出,由于第二个id为False,所以第二个向量[1,2,3,4,5]相乘之后被全部置0了,如果数值为后面的补零数值的话,这里输出的共有768个维度,通过这一操作之后768个维度将被全部置0。

继续网络结构的代码解读

接下来越过一个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)

这里起初调用的准则

total_metrics = {
    
    k:0.0 for k in metric_keys}

开始的时候metric_keys的对应值

metric_keys = ['main', 'rouge-1', 'rouge-2', 'rouge-l']

经过初始化

total_metrics = {
    
    k:0.0 for k in metric_keys}

之后的结果

{'main': 0.0, 'rouge-1': 0.0, 'rouge-2': 0.0, 'rouge-l': 0.0}

可以看出,这里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')

最关键的部分:交叉验证的使用

在这段代码的外面,有一部分用于交叉验证的循环内容
之前我阅读代码的过程之中忘记了这部分内容,导致没有读懂作者使用的交叉验证方法

for ((i=0; i<15; i++));
    do
        python extract_model.py $i
    done

可以看出这里对于每一部分数据都使用了抽取,总共有15折的数据,对于每一折的数据中,交叉验证抽出来进行若干个epoch的训练,这就形成了代码内部的内容

model.fit(
    train_x,
    train_y,
    epochs=epochs,
    batch_size=batch_size,
    callbacks=[evaluator]
)

每一个交叉验证独立训练epochs,训练完成之后保存每一折的最好epochs的权重,以便于后续使用

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)

交叉验证后续的使用

后续的使用在seq2seq_convert.py之中进行,每一折的验证数据用每一折的模型进行预测

valid_data = data_split(data, fold, num_folds, 'valid')
valid_x = data_split(data_x, fold, num_folds, 'valid')
 model.load_weights('./weights/extract_model.%s.weights' % fold)
#num_folds = 2的时候,拆出不同的fold进行预测
y_pred = model.predict(valid_x)[:, :, 0]

不同折用不同折的最好模型进行相应的预测,从而得到全部数据的预测结果。
在这里有多个不同的模型,回头预测的时候大概率会将这多个模型的内容进行融合得到结果

猜你喜欢

转载自blog.csdn.net/znevegiveup1/article/details/120872621