2016-12-01 18:01:09 pirage 阅读数 10881
  • Python文本数据分析--实战视频教学

    购买课程后,可扫码进入学习群,获取唐宇迪老师答疑 课程首先概述文本分析的基本概念,整个课程围绕案例进行:新闻分类任务。案例从零开始讲解如何使用Python库进行分析与建模的工作。案例中实例演示如何从杂乱的文本数据开始进行分词预处理到后应用贝叶斯算法进行分类预测。 专属会员卡更超值:http://edu.csdn.net/lecturer/1079

    3905 人正在学习 去看看 唐宇迪

分词原理

本小节内容参考待字闺中的两篇博文:

  1. 97.5%准确率的深度学习中文分词(字嵌入+Bi-LSTM+CRF)
  2. 如何深度理解Koth的深度分词?

简单的说,kcws的分词原理就是:

  1. 对语料进行处理,使用word2vec对语料的字进行嵌入,每个字特征为50维。
  2. 得到字嵌入后,用字嵌入特征喂给双向LSTM, 对输出的隐层加一个线性层,然后加一个CRF就得到本文实现的模型。
  3. 于最优化方法,文本语言模型类的貌似Adam效果更好, 对于分类之类的,貌似AdaDelta效果更好。

另外,字符嵌入的表示可以是纯预训练的,但也可以在训练模型的时候再fine-tune,一般而言后者效果更好。对于fine-tune的情形,可以在字符嵌入后,输入双向LSTM之前加入dropout进一步提升模型效果。

figure1

具体的解决方案,基于双向LSTM与CRF的神经网络结构

如上图所示,单层圆圈代表的word embedding输入层,菱形代表学习输入的决定性方程,双层圆圈代表随机变量。信息流将输入层的word embedding送到双向LSTM, l(i)代表word(i)和从左边传入的历史信号,r(i)代表word(i)以及从右边传入的未来的信号,用c(i)连接这两个向量的信息,代表词word(i)。

先说Bi-LSTM这个双向模型,LSTM的变种有很多,基本流程都是一样的,文献中同样采用上一节说明的三个门,控制送入memory cell的输入信息,以及遗忘之前阶段信息的比例,再对细胞状态进行更新,最后用tanh方程对更新后的细胞状态再做处理,与sigmoid叠加相乘作为最终输出。具体的模型公式见下图,经过上一节的解释,这些符号应该不太陌生了。

公式

双向LSTM说白了,就是先从左至右,顺序学习输入词序列的历史信息,再从右至左,学习输入词序列未来影响现在的信息,结合这两种方式的最终表示有效地描述了词的内容,在多种标注应用上取得了好效果。

如果说双向LSTM并不特殊,这个结构中另一个新的尝试,就是将深度神经网络最后学出来的结果,作为特征,用CRF模型连接起来,用P来表示双向LSTM神经网络学习出来的打分输出矩阵,它是一个 nxk 的矩阵,n是输入词序列个数,k是标记类型的数目, P(ij)指的是在一个输入句子中,第i个词在第j个tag标记上的可能性(打分)。另外一个特征函数是状态转移矩阵 A,A(ij) 代表从tag i转移到tag j的可能性(打分),但这个转移矩阵实际上有k+2维,其中包括句子的开始和结束两个状态,用公式表示如下图:

s(X,y)=i=0nAyi,yi+1+i=1nPi,yi

在给定输入序列X,最终定义的输出y序列的概率,则使用softmax函数表示如下:

p(y|X)=es(X,y)y˘YXes(X,y˘)

而在训练学习目标函数的时候,要优化的就是下面这个预测输出标记序列的log概率,其中 Y(X)代表的是所有可能的tag标记序列集合,那么最后学习得到的输出,就是概率最大的那个标记序列。如果只是模拟输出的bigram交互影响方式,采用动态规划即可求解下列方程。

log(p(y|X))=s(X,y)log(y˘YXes(X,y˘))

=s(X,y)logaddy˘YXs(X,y˘)

y=argmaxy˘YXs(X,y˘)

至此,基于双向LSTM与CRF的神经网络结构已经介绍完毕,文献中介绍的是在命名实体识别方面的一个实践应用,这个思路同样可以用在分词上。具体的实践和调参,也得应场景而异,Koth在上一篇博客中已经给出了自己的践行,读者们可以借鉴参考。

代码结构与实践

koth大神开源的项目地址为:https://github.com/koth/kcws

主要的代码在目录kcws/kcws/train路径下。(写这篇文章的时候发现K大神做了更新,我主要还是分析之前的代码)

  • process_anno_file.py 将人民网2014训练语料进行单字切割,包括标点符号
  • generate_training.py 生成单字的vector之后,处理每篇训练语料,以“。”划分为句子,对每个句子,如果长度大于MAX_LEN(默认设置为80,在代码里改),不处理,Long lines加一,如果长度小于MAX_LEN,则生成长度为160的vector,前80为单字在字典中的index,不足80的补0,后80为每个字对应的SBME标记(S表示单字,B表示开始,M表示中间,E表示结尾)。
  • filter_sentence.py 将语料切分为训练集和测试集,作者将含有两个字以下的句子过滤掉,剩下的按照二八分,测试集最多8000篇。
  • train_cws_lstm.py 主要训练代码。

作者在项目主页上很详细的写了构建和训练的步骤,按照这些步骤来实践一下不算难,我主要遇到了以下几个问题:

  1. 之前安装tensorflow的时候没有用bazel,不了解bazel的工作方式和原理,但是这个项目必须要用,因为需要用到third_party中word2vec的类。(可以将word2vec的某些类构建为python可以import的类?)
  2. 已安装的0.8.0版本的tensorflow没有实现crf,需要升级。
  3. 安装tensorflow 0.11.0版本后运行,出现PyUnicodeUCS4_AsUTF8String的错误,查找后发现是当前安装的python默认是unicode=ucs2,需要重新编译安装python。编译的时候设置./configure –enable-unicode=ucs4 。
  4. numpy,scipy都需要重新build,setup。

主要代码分析

def main(unused_argv):
    curdir = os.path.dirname(os.path.realpath(__file__))
    trainDataPath = tf.app.flags.FLAGS.train_data_path
    if not trainDataPath.startswith("/"):
        trainDataPath = curdir + "/" + trainDataPath
    graph = tf.Graph()
    with graph.as_default():
        model = Model(FLAGS.embedding_size, FLAGS.num_tags,
                  FLAGS.word2vec_path, FLAGS.num_hidden)
        print("train data path:", trainDataPath)
        # 读取训练集batch大小的feature和label,各为80大小的数组
        X, Y = inputs(trainDataPath)  
        # 读取测试集所有数据的feature和label,各为80大小的数组
        tX, tY = do_load_data(tf.app.flags.FLAGS.test_data_path) 
        # 计算训练集的损失
        total_loss = model.loss(X, Y)  
        # 使用AdamOptimizer优化方法
        train_op = train(total_loss)  
        # 在测试集上做评测
        test_unary_score, test_sequence_length = model.test_unary_score() 
        # 创建Supervisor管理模型的分布式训练
        sv = tf.train.Supervisor(graph=graph, logdir=FLAGS.log_dir) 
        with sv.managed_session(master='') as sess:
            # actual training loop
            training_steps = FLAGS.train_steps
            for step in range(training_steps):
                if sv.should_stop():
                    break
                try:
                    _, trainsMatrix = sess.run(
                    [train_op, model.transition_params])
                    # for debugging and learning purposes, see how the loss gets decremented thru training steps
                    if step % 100 == 0:
                        print("[%d] loss: [%r]" % (step, sess.run(total_loss)))
                    if step % 1000 == 0:
                        test_evaluate(sess, test_unary_score,
                                  test_sequence_length, trainsMatrix,
                                  model.inp, tX, tY)
                except KeyboardInterrupt, e:
                    sv.saver.save(sess,
                              FLAGS.log_dir + '/model',
                              global_step=step + 1)
                    raise e
            sv.saver.save(sess, FLAGS.log_dir + '/finnal-model')
            sess.close()

Class Model:

def __init__(self, embeddingSize, distinctTagNum, c2vPath, numHidden):
    self.embeddingSize = embeddingSize
    self.distinctTagNum = distinctTagNum
    self.numHidden = numHidden
    self.c2v = self.load_w2v(c2vPath)
    self.words = tf.Variable(self.c2v, name="words")
    with tf.variable_scope('Softmax') as scope:
        self.W = tf.get_variable(
            shape=[numHidden * 2, distinctTagNum],
            initializer=tf.truncated_normal_initializer(stddev=0.01),
            name="weights",
            regularizer=tf.contrib.layers.l2_regularizer(0.001))
        self.b = tf.Variable(tf.zeros([distinctTagNum], name="bias"))
    self.trains_params = None
    self.inp = tf.placeholder(tf.int32,
                              shape=[None, FLAGS.max_sentence_len],
                              name="input_placeholder")
    pass

def length(self, data):
    used = tf.sign(tf.reduce_max(tf.abs(data), reduction_indices=2))
    length = tf.reduce_sum(used, reduction_indices=1)
    length = tf.cast(length, tf.int32)
    return length

def inference(self, X, reuse=None, trainMode=True):
    word_vectors = tf.nn.embedding_lookup(self.words, X) # 按照X顺序返回self.words中的第X行,返回的结果组成tensor。
    length = self.length(word_vectors)
    # length是shape为[batch_size]大小值为句子长度的vector
    length_64 = tf.cast(length, tf.int64)
    if trainMode:  # 训练的时候启用dropout,测试的时候关键dropout
        word_vectors = tf.nn.dropout(word_vectors, 0.5)  # 将word_vectors按照50%的概率丢弃某些词,tf增加的一个处理是将其余的词scale 1/0.5
    with tf.variable_scope("rnn_fwbw", reuse=reuse) as scope:
        forward_output, _ = tf.nn.dynamic_rnn(
            tf.nn.rnn_cell.LSTMCell(self.numHidden),
            word_vectors,
            dtype=tf.float32,
            sequence_length=length,
            scope="RNN_forward")
        backward_output_, _ = tf.nn.dynamic_rnn(
            tf.nn.rnn_cell.LSTMCell(self.numHidden),
            inputs=tf.reverse_sequence(word_vectors,
                                       length_64,
                                       seq_dim=1),
            # 训练和测试的时候,inputs的格式不同。训练时,tensor shape是[batch_size, max_time,input_size]
            # 测试时,tensor shape是[max_time,batch_size,input_size].
            # tf.reverse_sequence作用就是指定在列上操作(batch_dim表示按行操作)
            dtype=tf.float32,
            sequence_length=length,
            scope="RNN_backword")
    # tf.nn.dynamic_rnn(cell, inputs, sequence_length,time_major,...)主要参数:
    # cell:搭建好的网络,这里用LSTMCell(num_cell),num_cell表示一个lstm单元输出的维数(100)
    # inputs:word_vectors,它的shape由time_major决定,默认是false,即[batch_size,max_time,input_size],如果是测试
    #           过程,time_major设置为True,shape为[max_time,batch_size,input_size],这里直接做了reverse,省去了time_major设置。
    #         其中,batch_size=100, max_time=80句子最大长度,input_size字的向量的长度。
    # sequence_length:shape[batch_size]大小的值为句子最大长度的tensor。
    # 输出:
    #   outputs:[batch_size, max_time, cell.output_size]
    #   state: shape取决于LSTMCell中state_size的设置,返回Tensor或者tuple。

    backward_output = tf.reverse_sequence(backward_output_,
                                          length_64,
                                          seq_dim=1)
    # 这里的reverse_sequence同上。
    output = tf.concat(2, [forward_output, backward_output])
    # 连接两个三维tensor,2表示按照列连接(0表示纵向,1表示行)
    # 连接后,output的shape:[batch_size, max_time, 2*cell.output_size],即[100, 80, 2*50]
    output = tf.reshape(output, [-1, self.numHidden * 2])
    # reshape后,output的shape:[batch_size, self.numHidden * 2],即[100, 200]
    matricized_unary_scores = tf.batch_matmul(output, self.W)
    # 得到未归一化的CRF输出
    # 点乘W的shape[ 100*2, 4],生成[batch_size, 4]大小的matricized_unary_scores
    unary_scores = tf.reshape(
        matricized_unary_scores,
        [-1, FLAGS.max_sentence_len, self.distinctTagNum])
    # reshape后,unary_scores大小为[batch_size,80, 4]
    return unary_scores, length

def loss(self, X, Y):
    P, sequence_length = self.inference(X)
    # CRF损失计算,训练的时候使用,测试的时候用viterbi解码
    log_likelihood, self.transition_params = tf.contrib.crf.crf_log_likelihood(
        P, Y, sequence_length)
    # crf_log_likelihood参数(inputs,tag_indices, sequence_lengths)
    #   inputs:大小为[100, 80, 4]的tensor,CRF层的输入
    #   tag_indices:大小为[100, 80]的矩阵
    #   sequence_length:大小 [100]值为80的向量。
    # 输出:
    #   log_likelihood:[batch_size]大小的vector,log-likelihood值
    #   transition_params:[4,4]大小的矩阵
    loss = tf.reduce_mean(-log_likelihood)
    return loss

def load_w2v(self, path): #返回(num+2)*50大小的二维矩阵,其中第一行全是0,最后一行是每个词向量维度的平均值。
    fp = open(path, "r")
    print("load data from:", path)
    line = fp.readline().strip()
    ss = line.split(" ")
    total = int(ss[0])
    dim = int(ss[1])
    assert (dim == (FLAGS.embedding_size))
    ws = []
    mv = [0 for i in range(dim)]
    # The first for 0
    ws.append([0 for i in range(dim)])
    for t in range(total):
        line = fp.readline().strip()
        ss = line.split(" ")
        assert (len(ss) == (dim + 1))
        vals = []
        for i in range(1, dim + 1):
            fv = float(ss[i])
            mv[i - 1] += fv
            vals.append(fv)
        ws.append(vals)
    for i in range(dim):
        mv[i] = mv[i] / total
    ws.append(mv)
    fp.close()
    return np.asarray(ws, dtype=np.float32)

def test_unary_score(self):
    P, sequence_length = self.inference(self.inp,
                                        reuse=True,
                                        trainMode=False)
    return P, sequence_length

其他的代码比较简单,个人觉得不必要做深入分析。

总结

近一两个月开始学习TensorFlow,代码看了一些,但是总感觉临门差那么一脚。革命尚未完成,同志们仍需努力!

2019-09-10 17:20:31 isxixi 阅读数 135
  • Python文本数据分析--实战视频教学

    购买课程后,可扫码进入学习群,获取唐宇迪老师答疑 课程首先概述文本分析的基本概念,整个课程围绕案例进行:新闻分类任务。案例从零开始讲解如何使用Python库进行分析与建模的工作。案例中实例演示如何从杂乱的文本数据开始进行分词预处理到后应用贝叶斯算法进行分类预测。 专属会员卡更超值:http://edu.csdn.net/lecturer/1079

    3905 人正在学习 去看看 唐宇迪

一、中文分词方法

  1. 基于字典的分词方法
  2. 基础规则的分词方法
  3. 基于统计的分词方法(统计在一段话中出现频率最的那个分词字段)
  4. 基于深度学的的方法(通过大量的数据的学习)

二、词向量化

  1. one-hot
  2. word2vec 将词映射到多维空间里

三、神经网络

  1. RNN(循环神经网络)
    特点:记忆是短期,梯度消失以及梯度爆炸‘’
  2. LSTM(长短期记忆网络) 是 RNN 的进一步优化
    特点:克服 RNN 梯度消失的问题,而且能学习到长距离的信息
  3. BILSTM (双向长短期神经网络)

四、统计模型

CRF:条件随机场是标记、分割结构化数据的统计模型。CRF 优于隐马尔可夫模型在于放松了 HMM 所需的独立性假设。另外避免了标签偏差问题。CRF 训练的损失函数是凸函数,全局收敛,具有非常好的实用性。
特点:隐马尔可夫是有向图,每个状态依赖于上个状态,而线性链条件随机场是无向
图,当前状态依赖于周围结点的状态,可以捕捉全局信息,所以效果更好

五、jieba 分词
jieba 是结合基于规则和基于统计的分词工具。
jieba 有三种分词模式,分别是精确模式,全模式和搜索引擎模式
六、词向量

  1. 输入层
    使用one-hot编码。
  2. 隐藏层
    隐藏层的神经单元数量代表着每一个词用向量表示的维度。
  3. 输出层
    七、分词的评估标准
    精确率:正确的分词个数/总分词的个数
    召回率:正确的个数/标准分词的个数
    F值:正确率召回率2/(正确率+召回率)
    错误率:错误分词的个数/标准分词的个数
2019-08-27 09:53:03 emdsh 阅读数 82
  • Python文本数据分析--实战视频教学

    购买课程后,可扫码进入学习群,获取唐宇迪老师答疑 课程首先概述文本分析的基本概念,整个课程围绕案例进行:新闻分类任务。案例从零开始讲解如何使用Python库进行分析与建模的工作。案例中实例演示如何从杂乱的文本数据开始进行分词预处理到后应用贝叶斯算法进行分类预测。 专属会员卡更超值:http://edu.csdn.net/lecturer/1079

    3905 人正在学习 去看看 唐宇迪

第六章,深度学习用于文本和序列

 

深度学习不会接收原始文本作为输入,只能处理数值张量,文本向量化是指将文本转换为数值张量的过程。(文本可分割为标记:单词,字符,n-gram,分解为标记的过程叫做分词,然后将其转换为张量)

one-hot将每个单词与一个唯一的整数索引相关联,将整数索引i转换为长度为N的二进制向量。One-hot编码是标记转化为向量最基本最常用的方法,得到的是二进制的,稀疏的维度很高的向量。

 

词向量(词嵌入):是从数据中学习到的,得到低维的浮点型向量。

学习方式一:在完成主任务的同时学习词嵌入,对随机的词向量进行学习。

学习方式二:预训练词嵌入,计算好词嵌入加载到模型上。

利用Embedding层学习词嵌入:对每个新的任务都学习一个新的嵌入空间,利用Embedding层学习一个层的权重,将Embedding层理解为一个字典,将帧数索引映射为密集向量。Embedding层是一个二维整数张量(samples,sequence_length),返回形状为(samples,sequence_length,embedding_dimensionality)的三维浮点数张量。

       使用预训练的词嵌入:原理同图像分类中使用的预训练的卷积神经网络一样,这种词嵌入利用词频统计计算得出。

 

处理IMDB数据的原始文本
import os
imdb_dir='D:\\jupyter_code\\GloVe\\aclImdb'
train_dir=os.path.join(imdb_dir,'train')

labels=[]
texts=[]

for label_type in ['neg','pos']:
    dir_name=os.path.join(train_dir,label_type)
    for fname in os.listdir(dir_name):
        if fname[-4:]=='.txt':
            f=open(os.path.join(dir_name,fname),encoding='UTF-8')
            texts.append(f.read())
            f.close()
            if label_type=='neg':
                labels.append(0)
            else:
                labels.append(1)
                
#对imdb原始数据的文本进行分词
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import numpy as np
#在100个单词后截断评论
maxlen=100
#200个样本上训练
training_samples=200
#在10000个样本上验证
validation_samples=10000
#只考虑前10000个最常见的单词
max_words=10000

tokenizer=Tokenizer(num_words=max_words)
#根据文本列表更新词汇表
tokenizer.fit_on_texts(texts)
sequences=tokenizer.texts_to_sequences(texts)

word_index=tokenizer.word_index
print('Found %s unique tokens.'%len(word_index))

data=pad_sequences(sequences,maxlen=maxlen)

labels=np.asarray(labels)
print(data.shape)
print(labels.shape)

#打乱数据在划分数据
indices=np.arange(data.shape[0])
np.random.shuffle(indices)
data=data[indices]
labels=labels[indices]

x_train=data[:training_samples]
y_train=labels[:training_samples]
x_val=data[training_samples:training_samples+validation_samples]
y_val=labels[training_samples:training_samples+validation_samples]

#解析GloVe嵌入文件
glove_dir='D:\\jupyter_code\\GloVe\\glove.6B'

embeddings_index={}
f=open(os.path.join(glove_dir,'glove.6B.100d.txt'),encoding='UTF-8')
for line in f:
    values=line.split()
    word=values[0]
    coefs=np.asarray(values[1:],dtype='float32')
    embeddings_index[word]=coefs
f.close()

print('Found %s word vectors'%len(embeddings_index))

#准备GloVe词嵌入词库
embedding_dim=100
embedding_matrix=np.zeros((max_words,embedding_dim))
for word, i in word_index.items():
    if i<max_words:
        embedding_vector=embeddings_index.get(word)
        if embedding_vector is not None:
            embedding_matrix[i]=embedding_vector

#定义模型
from keras.models import Sequential
from keras.layers import Embedding,Flatten,Dense

model=Sequential()
model.add(Embedding(max_words,embedding_dim,input_length=maxlen))
model.add(Flatten())
model.add(Dense(32,activation='relu'))
model.add(Dense(1,activation='sigmoid'))
model.summary()

#预训练的词嵌入加载到Embedding层,冻结Embedding
model.layers[0].set_weights([embedding_matrix])
model.layers[0].trainable=False


#模型训练与评估
model.compile(optimizer='rmsprop',loss='binary_crossentropy',metrics=['acc'])
history=model.fit(x_train,y_train,epochs=10,batch_size=32,validation_data=(x_val,y_val))
model.save_weights('pre_trained_glove_model.h5')

#在不使用预训练词嵌入的情况下,训练相同的模型
from keras.models import Sequential
from keras.layers import Embedding,Flatten,Dense

model=Sequential()
model.add(Embedding(max_words,embedding_dim,input_length=maxlen))
model.add(Flatten())
model.add(Dense(32,activation='relu'))
model.add(Dense(1,activation='sigmoid'))
model.summary()

model.compile(optimizer='rmsprop',loss='binary_crossentropy',metrics=['acc'])
history=model.fit(x_train,y_train,epochs=10,batch_size=32,validation_data=(x_val,y_val))

#对测试集数据进行分词
test_dir=os.path.join(imdb_dir,'test')
labels=[]
texts=[]

for label_type in ['neg','pos']:
    dir_name=os.path.join(test_dir,label_type)
    for fname in sorted(os.listdir(dir_name)):
        if fname[-4:]=='.txt':
            f=open(os.path.join(dir_name,fname),encoding='UTF-8')
            texts.append(f.read())
            f.close()
            if label_type=='neg':
                labels.append(0)
            else:
                labels.append(1)

sequences=tokenizer.texts_to_sequences(texts)
x_test=pad_sequences(sequences,maxlen=maxlen)
y_test=np.asarray(labels)

model.load_weights('pre_trained_glove_model.h5')
model.evaluate(x_test,y_test)

 

2019-03-11 11:13:37 qq_32768743 阅读数 228
  • Python文本数据分析--实战视频教学

    购买课程后,可扫码进入学习群,获取唐宇迪老师答疑 课程首先概述文本分析的基本概念,整个课程围绕案例进行:新闻分类任务。案例从零开始讲解如何使用Python库进行分析与建模的工作。案例中实例演示如何从杂乱的文本数据开始进行分词预处理到后应用贝叶斯算法进行分类预测。 专属会员卡更超值:http://edu.csdn.net/lecturer/1079

    3905 人正在学习 去看看 唐宇迪

本文实验中使用的分词器是哈工大LTP分词器OLTP分词模块融合了算法和 字典等外部资源,利用机器学习算法来实现消歧。LTP将分词定义为序列标注问 题,通过对每个字标注一个词边界来实现分词。并且r i’LIP分词器加入一些优化策 略:英文、URI一类特殊词识别规则,利用空格等自然标注线索,在统计模型中 融入词典信息,从大规模未标注数据中统计字间互信息、上下文丰富程度。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2019-12-13 15:03:24 SangrealLilith 阅读数 104
  • Python文本数据分析--实战视频教学

    购买课程后,可扫码进入学习群,获取唐宇迪老师答疑 课程首先概述文本分析的基本概念,整个课程围绕案例进行:新闻分类任务。案例从零开始讲解如何使用Python库进行分析与建模的工作。案例中实例演示如何从杂乱的文本数据开始进行分词预处理到后应用贝叶斯算法进行分类预测。 专属会员卡更超值:http://edu.csdn.net/lecturer/1079

    3905 人正在学习 去看看 唐宇迪

1. 前言

上一篇文章中我们已经描述了 Transformer 的整个模型搭建过程,并逐层逐行地解释了其正向传播的原理和细节。接下来,我们将着手定义优化训练的方式,处理语料,并最终使用搭建好的 Transformer 实现一个由葡萄牙语翻译至英语的翻译器。

为了训练一个由葡萄牙语翻译至英语的翻译器,首先来观察如何处理数据从而能够正确地输入我们已经设计好的 Tranformer 模型:

class Transformer(tf.keras.Model):
...
    
  def call(self, inp, tar, training, enc_padding_mask, 
           look_ahead_mask, dec_padding_mask):

... 

只摘取模型的调用 call 部分,可以看出 Transformer 需要的输入:

  • inp:输入序列,这里需要的是源语言(葡萄牙语)的编码表示。(嵌入表示将在编码器中完成)
  • tar:目标序列,这里需要的是目标语言(英语)的编码表示。(嵌入表示将在编码器中完成)
  • training:布尔量,规定模型是否可以训练。
  • enc_padding_mask:编码器,填充遮挡。
  • look_ahead_mask:前瞻遮挡。两个遮挡将在后面详细描述。
  • dec_padding_mask:解码器,填充遮挡。

由此,我们知道,为了达成目的,我们需要完成以下几个步骤:

  1. 创造原训练集(输入句子和目标句子)的嵌入表示
  2. 为我们的 Transformer 设计优化器和损失函数
  3. 根据情况创造填充遮挡
  4. 为了实现自回归创建前瞻遮挡
  5. 将数据输入进行训练
  6. 最终对训练好的模型进行评估

2. 创造原训练集的编码表示

2.1. 数据下载与读取

参考 Tensorflow 的官方教程,我们同样使用 TFDS 来进行数据的下载和载入。(应首先在本机环境或虚拟环境中安装 tensorflow_datasets 模块。

import tensorflow_datasets as tfds

examples, metadata = tfds.load('ted_hrlr_translate/pt_to_en', with_info=True,
                               as_supervised=True)
train_examples, val_examples = examples['train'], examples['validation']

第一行代码会访问用户目录下(Windows和Unix系系统各有不同,请参考官方文档)是否已经下载好了葡萄牙翻译至英文翻译器所需的数据集,如果不存在,则会自动下载。第二行,则将其自动转换为训练集合和测试集合两个 tf.data.Dataset 实例。

2.2. 创建子词分词器

tfds 独立于 Tensorflow,是专门用来管理和下载一些成熟的数据集的Python库。但其中有很多我认为通用性很强的函数。比如子词分词器:

tokenizer_en = tfds.features.text.SubwordTextEncoder.build_from_corpus(
    (en.numpy() for pt, en in train_examples), target_vocab_size=2**13)

tokenizer_pt = tfds.features.text.SubwordTextEncoder.build_from_corpus(
    (pt.numpy() for pt, en in train_examples), target_vocab_size=2**13)

上方代码分别创建了两个子词分词器,分别读取了训练集合中的全部英文和葡萄牙文,并基于这些大段的文字形成了子词分词器。

子词分词器的作用是将输入句子中的每一个单词编码为一个独一无二的数字,如果出现了子词分词器不能识别的新单词,那么就将其打散成多个可以识别的子词来编码成数字。

同样的,分词器也可以将用数字表示的句子重新转换回原有的句子。

sample_string = 'Transformer is awesome.'

tokenized_string = tokenizer_en.encode(sample_string)
print ('Tokenized string is {}'.format(tokenized_string))

# Tokenized string is [7915, 1248, 7946, 7194, 13, 2799, 7877]

original_string = tokenizer_en.decode(tokenized_string)
print ('The original string: {}'.format(original_string))

# The original string: Transformer is awesome.

assert original_string == sample_string

# 分词器转换回的句子和原始句子一定是相同的。

2.3. 数据处理

为了方便后期使用,编写一个将编码后句子加上开始标记和结束标记。利用 tf.data.Datasetmap 功能来批量完成这一任务。首先需要定义一个函数:

def encode(lang1, lang2):
  lang1 = [tokenizer_pt.vocab_size] + tokenizer_pt.encode(
      lang1.numpy()) + [tokenizer_pt.vocab_size+1]

  lang2 = [tokenizer_en.vocab_size] + tokenizer_en.encode(
      lang2.numpy()) + [tokenizer_en.vocab_size+1]
  
  return lang1, lang2

显然这里我们使用了原生 Python 编写这个函数,这样的函数是不能为 map 所用的。我们需要使用 tf.py_function 将这个函数转换为计算图。(此函数可以将原生 Python 编写的计算过程转换为 Tensorflow 流程控制的计算图,详情请参考 py_function)

def tf_encode(pt, en):
  return tf.py_function(encode, [pt, en], [tf.int64, tf.int64]) 
  # 第一个参数是包装的函数,第二个参数是输入的参数列表,第三个参数是输出的数据类型

于是,我们可以给训练数据集和验证数据集中每个句子加上开始标记和结束标记:

train_dataset = train_examples.map(tf_encode)
val_dataset = val_examples.map(tf_encode)

为了能够让这个模型较小,我们只使用句子短于 40 个单词的句子来作为输入数据。这里利用 tf.data.Datasetfilter 过滤器功能来快速筛选出需要的数据。

为了使用 filter,首先要定义一个过滤器函数。这是一个布尔函数,如果一条数据符合要求,则返回真,否则返回假。显然,对于葡萄牙句子翻译至英文句子数据集,我们要筛选出所有成对相同意思的句子,并且两条句子都短于 40 个单词(编码后并加上了开始和终结标记后的长度)。

def filter_max_length(x, y, max_length=MAX_LENGTH):
  return tf.logical_and(tf.size(x) <= max_length,
                        tf.size(y) <= max_length)

类似的,对数据集进行筛选:

train_dataset = train_dataset.filter(filter_max_length)

我们已经知道, 输入给 Transformer 的句子通常不会单句地输入,而是把句子叠成一批输入。将一批有长有短的句子叠成一批,需要将较短的句子补 0 使其长度匹配当前一批中最长的句子。

# 将数据集缓存到内存中以加快读取速度。
train_dataset = train_dataset.cache()
# shuffle 函数定义一个随机方式,首先定义一个缓存大小,取一部分数据放入缓存(BUFFER_SIZE大小),然后进行随机洗牌,最后从缓存中取。显然,若想实现全数据集的完美随机,需要让缓存的大小大于等于整个数据集。
# 首先将数据进行随机打散之后,对较短的数据进行填充。
train_dataset = train_dataset.shuffle(BUFFER_SIZE).padded_batch(
    BATCH_SIZE, padded_shapes=([-1], [-1]))
train_dataset = train_dataset.prefetch(tf.data.experimental.AUTOTUNE)

显然,验证集合也需要进行类似的处理操作(验证操作无需随机)。

val_dataset = val_dataset.filter(filter_max_length).padded_batch(
    BATCH_SIZE, padded_shapes=([-1], [-1]))

取出一个数据看一看:

pt_batch, en_batch = next(iter(val_dataset))
"""
pt_batch:
(<tf.Tensor: id=207688, shape=(64, 40), dtype=int64, numpy=
 array([[8214, 1259,    5, ...,    0,    0,    0],
        [8214,  299,   13, ...,    0,    0,    0],
        [8214,   59,    8, ...,    0,    0,    0],
        ...,
        [8214,   95,    3, ...,    0,    0,    0],
        [8214, 5157,    1, ...,    0,    0,    0],
        [8214, 4479, 7990, ...,    0,    0,    0]])>,
en_batch:
 <tf.Tensor: id=207689, shape=(64, 40), dtype=int64, numpy=
 array([[8087,   18,   12, ...,    0,    0,    0],
        [8087,  634,   30, ...,    0,    0,    0],
        [8087,   16,   13, ...,    0,    0,    0],
        ...,
        [8087,   12,   20, ...,    0,    0,    0],
        [8087,   17, 4981, ...,    0,    0,    0],
        [8087,   12, 5453, ...,    0,    0,    0]])>)
"""

3. 损失函数设计

损失函数的设计较为简单,需要考虑输出的句子和真正的目标句子是否为同一句子。只需要使用一个交叉熵函数。有一点需要注意,由上一章数据处理可以看出,数据中含有大量的填充(补0),这些填充不能作为真正的输入来考虑,因此在损失函数的计算中,需要将这些部分屏蔽掉。

loss_object = tf.keras.losses.SparseCategoricalCrossentropy(
    from_logits=True, reduction='none')

def loss_function(real, pred):
# 对于mask,如果编码句子中出现了值为 0 的数据,则将其置 0
  mask = tf.math.logical_not(tf.math.equal(real, 0))
# 输出句子和真正的句子计算交叉熵
  loss_ = loss_object(real, pred)
# 将无效的交叉熵删除
  mask = tf.cast(mask, dtype=loss_.dtype)
  loss_ *= mask
# 返回平均值
  return tf.reduce_mean(loss_)

同时,定义两个指标用于展示训练过程中的模型变化:

train_loss = tf.keras.metrics.Mean(name='train_loss')
train_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(
    name='train_accuracy')

4. 优化器与学习率

Transformer 使用 Adam 优化器,其 β1\beta_1 为 0.9, β2\beta_2 为0.98, ϵ\epsilon10910^{-9}。其学习率随着训练的进程变化:

在这里插入图片描述

其中,这个 warmup_step 设定为 4000。如此设计,学习率随着训练(Train Step)的变化就如下图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fAeEaGlP-1576220404322)(attachments/lrate2.png)]

学习率的变化,我们通过继承 tf.keras.optimizers.schedules.LearningRateSchedule来实现。顾名思义,这个类会创建一个可序列化的学习率衰减(也可能增加)时间表

class CustomSchedule(tf.keras.optimizers.schedules.LearningRateSchedule):
  def __init__(self, d_model, warmup_steps=4000):
    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): # 这个时间表被调用时,按照 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(d_model)

optimizer = tf.keras.optimizers.Adam(learning_rate, beta_1=0.9, beta_2=0.98, 
                                     epsilon=1e-9)

5. 自回归原理

至今位置,我们已经拥有了 Transformer 的完整模型,数据输入和优化器。

但显然,Transformer 和 传统的 RNN 按时序依次读取输入和输出的训练方式“看起来”不同——它一次输入整个句子。而 encoder-decoder 架构是自回归的:通过上一步产生的符号和这一步的输入来预测这一步的输出。开始训练之前,需要了解 Transformer 是如何实现自回归的。

Tranformer 使用导师监督(teacher-forcing)法,即在预测过程中无论模型在当前时间步骤下预测出什么,teacher-forcing 方法都会将真实的输出传递到下一个时间步骤上。

当 transformer 预测每个词时,自注意力(self-attention)功能使它能够查看输入序列中前面的单词,从而更好地预测下一个单词。为了仅能让其查看输入序列中前面的单词,则需要前瞻遮挡来屏蔽后方的单词。

也就是说,若输入一个葡萄牙文句子,Tranformer 将第一次仅预测出英文句子的第一个单词,然后再次基础上依次预测第二个,第三个。

而训练过程也应该模拟这样的预测过程,每次仅增加一个目标序列的单词。

因此,我们将目标句子改写成两种:

原目标句子:sentence = "SOS A lion in the jungle is sleeping EOS"

改写为:

tar_inp = "SOS A lion in the jungle is sleeping"

tar_real = "A lion in the jungle is sleeping EOS"

SOSEOS 是开始标记和结束标记。)

真正输入给 Decoder 部分的是前者,配合前瞻遮挡它将模拟逐个单词产生的模型历史预测。而后者,则代表着模型当前步骤应该依次预测出的单词序列。很显然,他们应该仅仅只有一个单词的位移。

6. 训练

6.1. 超参数

Transformer 的基础模型使用的数值为:num_layers=6,d_model = 512,dff = 2048。

num_layers = 4
d_model = 128
dff = 512
num_heads = 8

input_vocab_size = tokenizer_pt.vocab_size + 2
target_vocab_size = tokenizer_en.vocab_size + 2
dropout_rate = 0.1

6.2. 训练

transformer = Transformer(num_layers, d_model, num_heads, dff,
                          input_vocab_size, target_vocab_size, 
                          pe_input=input_vocab_size, 
                          pe_target=target_vocab_size,
                          rate=dropout_rate)

6.2.1. 创建遮挡

  • 首先要对输入数据(原始句子和目标句子)创建填充遮挡(填充了 0 的位置标记为 1,其余部分标记为 0,这里与损失函数的部分刚好相反)。
  • 对于编码器解码器结构,当编码器预测后方的单词,只使用前方已经预测出的单词。为了实现这一效果,需要使用前瞻遮挡。

无论哪种遮挡,0 标记着保留的部分,1 标记着要遮挡的部分。


# 这里 inp 和 tar 都是来自第 2 章的编码后数据,形状显然为 (batch_size, len)
def create_masks(inp, tar):
  # 编码器填充遮挡,编码器自注意力时使用,在自注意力编码时排除掉没有含义的填充
  enc_padding_mask = create_padding_mask(inp)
  
  # 在解码器的第二个注意力模块使用。
  # 该填充遮挡用于遮挡编码器的输出,其输出输送给解码器使用,排除掉没有含义的填充
  dec_padding_mask = create_padding_mask(inp)
  
  # 在解码器的第一个注意力模块使用。
  # 遮挡(mask)解码器获取到的输入的后续标记(future tokens)。
  # 自然,填充的 padding 也不能忘记考虑,所以把两个遮挡合在一起两全其美
  look_ahead_mask = create_look_ahead_mask(tf.shape(tar)[1])
  dec_target_padding_mask = create_padding_mask(tar)
  combined_mask = tf.maximum(dec_target_padding_mask, look_ahead_mask)
  
  return enc_padding_mask, combined_mask, dec_padding_mask

6.2.2. 创建训练步骤及保存模型

为了保存模型,需要创建一个检查点管理器,在需要时使用此管理器来保存模型:

checkpoint_path = "./checkpoints/train"

ckpt = tf.train.Checkpoint(transformer=transformer,
                           optimizer=optimizer)

ckpt_manager = tf.train.CheckpointManager(ckpt, checkpoint_path, max_to_keep=5)

# 如果检查点存在,则恢复最新的检查点。
if ckpt_manager.latest_checkpoint:
  ckpt.restore(ckpt_manager.latest_checkpoint)
  print ('Latest checkpoint restored!!')

根据 5. 节的描述,将对目标序列进行调整和创建遮挡。最终实现训练过程。

在TF2.0中,由于使用了 eager excution 导致的性能下降,将使用@tf.function 装饰器将代码转换为传统的计算图提升性能。但这种转换并非完全智能,若没有良好的限制,则会因为输入 Tensor 的变化导致无法复用已有的计算图,导致冗余的转换。详情请参考Graph Execution 模式

# 该 @tf.function 将追踪-编译 train_step 并将其转换为计算图,以便更快地执行。
# 该函数专用于参数张量的精确形状。为了避免由于可变序列长度或可变批次大小(最后一批次较小)导致的多次冗余转换
# 使用 input_signature 指定更多的通用形状。

train_step_signature = [
    tf.TensorSpec(shape=(None, None), dtype=tf.int64),
    tf.TensorSpec(shape=(None, None), dtype=tf.int64),
]

@tf.function(input_signature=train_step_signature)
def train_step(inp, tar):
  tar_inp = tar[:, :-1]
  tar_real = tar[:, 1:]
  
  enc_padding_mask, combined_mask, dec_padding_mask = create_masks(inp, tar_inp)
  
  with tf.GradientTape() as tape:
    predictions, _ = transformer(inp, tar_inp, 
                                 True, 
                                 enc_padding_mask, 
                                 combined_mask, 
                                 dec_padding_mask)
    loss = loss_function(tar_real, predictions)

  gradients = tape.gradient(loss, transformer.trainable_variables)    
  optimizer.apply_gradients(zip(gradients, transformer.trainable_variables))
  
  train_loss(loss)
  train_accuracy(tar_real, predictions)

6.2.3. 开始训练

EPOCHS = 20

for epoch in range(EPOCHS):
  start = time.time()
  
  train_loss.reset_states()
  train_accuracy.reset_states()
  
  # inp -> portuguese, tar -> english
  for (batch, (inp, tar)) in enumerate(train_dataset):
    train_step(inp, tar)
    
    if batch % 50 == 0:
      print ('Epoch {} Batch {} Loss {:.4f} Accuracy {:.4f}'.format(
          epoch + 1, batch, train_loss.result(), train_accuracy.result()))
      
  if (epoch + 1) % 5 == 0:
    ckpt_save_path = ckpt_manager.save()
    print ('Saving checkpoint for epoch {} at {}'.format(epoch+1,
                                                         ckpt_save_path))
    
  print ('Epoch {} Loss {:.4f} Accuracy {:.4f}'.format(epoch + 1, 
                                                train_loss.result(), 
                                                train_accuracy.result()))

  print ('Time taken for 1 epoch: {} secs\n'.format(time.time() - start))

效果如下:

Epoch 1 Batch 0 Loss 4.4721 Accuracy 0.0000
Epoch 1 Batch 50 Loss 4.2211 Accuracy 0.0076
Epoch 1 Batch 100 Loss 4.1943 Accuracy 0.0173
Epoch 1 Batch 150 Loss 4.1539 Accuracy 0.0205
Epoch 1 Batch 200 Loss 4.0675 Accuracy 0.0221
...

7. 评估

以下步骤用于评估:

  • 用葡萄牙语分词器(tokenizer_pt)编码输入语句。此外,添加开始和结束标记,这样输入就与模型训练的内容相同。这是编码器输入。
  • 解码器输入为 start token == tokenizer_en.vocab_size。
  • 计算填充遮挡和前瞻遮挡。
  • 解码器通过查看编码器输出和它自身的输出(自注意力)给出预测。
  • 选择最后一个词并计算它的 argmax。将预测的词连接到解码器输入,然后传递给解码器。在这种方法中,解码器根据它预测的之前的词预测下一个。

评估函数:

def evaluate(inp_sentence):
  start_token = [tokenizer_pt.vocab_size]
  end_token = [tokenizer_pt.vocab_size + 1]
  
  # 输入语句是葡萄牙语,增加开始和结束标记
  inp_sentence = start_token + tokenizer_pt.encode(inp_sentence) + end_token
  encoder_input = tf.expand_dims(inp_sentence, 0)
  
  # 因为目标是英语,输入 transformer 的第一个词应该是
  # 英语的开始标记。
  decoder_input = [tokenizer_en.vocab_size]
  output = tf.expand_dims(decoder_input, 0)
    
  for i in range(MAX_LENGTH):
    enc_padding_mask, combined_mask, dec_padding_mask = create_masks(
        encoder_input, output)
  
    # predictions.shape == (batch_size, seq_len, vocab_size)
    predictions, attention_weights = transformer(encoder_input, 
                                                 output,
                                                 False,
                                                 enc_padding_mask,
                                                 combined_mask,
                                                 dec_padding_mask)
    
    # 从 seq_len 维度选择最后一个词
    predictions = predictions[: ,-1:, :]  # (batch_size, 1, vocab_size)

    predicted_id = tf.cast(tf.argmax(predictions, axis=-1), tf.int32)
    
    # 如果 predicted_id 等于结束标记,就返回结果
    if predicted_id == tokenizer_en.vocab_size+1:
      return tf.squeeze(output, axis=0), attention_weights
    
    # 连接 predicted_id 与输出,作为解码器的输入传递到解码器。
    output = tf.concat([output, predicted_id], axis=-1)

  return tf.squeeze(output, axis=0), attention_weights

可视化注意力:

def plot_attention_weights(attention, sentence, result, layer):
  fig = plt.figure(figsize=(16, 8))
  
  sentence = tokenizer_pt.encode(sentence)
  
  attention = tf.squeeze(attention[layer], axis=0)
  
  for head in range(attention.shape[0]):
    ax = fig.add_subplot(2, 4, head+1)
    
    # 画出注意力权重
    ax.matshow(attention[head][:-1, :], cmap='viridis')

    fontdict = {'fontsize': 10}
    
    ax.set_xticks(range(len(sentence)+2))
    ax.set_yticks(range(len(result)))
    
    ax.set_ylim(len(result)-1.5, -0.5)
        
    ax.set_xticklabels(
        ['<start>']+[tokenizer_pt.decode([i]) for i in sentence]+['<end>'], 
        fontdict=fontdict, rotation=90)
    
    ax.set_yticklabels([tokenizer_en.decode([i]) for i in result 
                        if i < tokenizer_en.vocab_size], 
                       fontdict=fontdict)
    
    ax.set_xlabel('Head {}'.format(head+1))
  
  plt.tight_layout()
  plt.show()

单句子测试:

  def translate(sentence, plot=''):
    result, attention_weights = evaluate(sentence)
    
    predicted_sentence = tokenizer_en.decode([i for i in result 
                                              if i < tokenizer_en.vocab_size])  

    print('Input: {}'.format(sentence))
    print('Predicted translation: {}'.format(predicted_sentence))
    
    if plot:
      plot_attention_weights(attention_weights, sentence, result, plot)

  translate("este é um problema que temos que resolver.")
print ("Real translation: this is a problem we have to solve .")

效果:

Input: este é um problema que temos que resolver.
Predicted translation: this is a problem that we have to solve .... now .
Real translation: this is a problem we have to solve .

NLP工作内容总结

阅读数 450

人工智能学习之路

阅读数 580

维特比算法

阅读数 983

HMM之Viterbi算法

阅读数 2150

没有更多推荐了,返回首页