CS224n - 任务2 - 依赖性分析

这篇博客,我们实现一个基于神经网络的依赖性分析器,这是CS224n任务2的第2题。我们逐步解析,从而实现基于转移的解析。

  • stack:当前正在处理的单词,初始化为[ROOT]
  • buffer:尚未处理的单词,初始化为按顺序包含句子的所有单词的缓冲区
  • list:解析器预测的依赖关系,初始化为空[]

每次迭代,解析器将转换应用于部分解析,直到buffer为空并且stack大小为1:

  • SHIFT:从buffer中删除第一个单词并将其推入stack
  • LEFT-ARC:标记stack的第二项(刚添加的第二个)作为第一项的依赖项,并从stack删除第二项
  • RIGHT-ARC:标记stack的第一项(刚添加的)作为第二项的依赖项,并从stack删除第一项

解析器将使用神经网络分类器决定每个状态的转换。 首先需要实现部分解析表示和转换函数。

(a) 利用"I parsed this sentence correctly"做例子,每一步在给定的状态下,确定下一步的stack,buffer,转换方式以及新确定的依赖关系:

(b) 句子的每个单词push到stack上然后pop,所以包含n个单词的句子将进行2n步解析

(c) 在PartialParse类中实现初始化(__init__)和分布解析(parse_step),

class PartialParse(object):
    def __init__(self,sentence):
        # The sentence being parsed is kept for bookkeeping purposes. Do not use it in your code.
        self.sentence=sentence
        self.stack=['ROOT']
        self.buffer=sentence[:]
        self.dependencies=[]

    def parse_step(self,transition):
        if transition=="S":
            self.stack.append(self.buffer[0])
            self.buffer.pop(0)
        elif transition=="LA":
            self.dependencies.append((self.stack[-1],self.stack[-2]))
            self.stack.pop(-2)
        else: # "RA"
            self.dependencies.append((self.stack[-2],self.stack[-1]))
            self.stack.pop(-1)

    def parse(self,transitions):
        for transition in transitions:
            self.parse_step(transition)
        return self.dependencies

这部分主要是对stack,buffer,dependencies进行列表初始化[],并根据转换做对应的列表操作,常用操作包含:
1、list.append(obj):在列表末尾添加新的对象
2、list.count(obj):统计某个元素在列表中出现的次数
3、list.extend(seq):在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表)
4、list.index(obj):从列表中找出某个值第一个匹配项的索引位置
5、list.insert(index, obj):将对象插入列表
6、list.pop(obj=list[-1]):移除列表中的一个元素(默认最后一个元素),并且返回该元素的值
7、list.remove(obj):移除列表中某个值的第一个匹配项
8、list.reverse():反向列表中元素
9、list.sort([func]):对原列表进行排序

(d) 在minibatch_parse函数利用Minibatch Dependency Parsing算法实现小批量解析句子

给定stack,buffer以及dependencies的状态,神经网络可以高效地预测许多不同部分解析的下一次转换

(1) 首先模型提取表示当前状态的特征向量:包括token列表(stack的最后一项,buffer的第一项,stack中倒数第二项的独立性)

\lbrack\omega_1,\omega_2,\dots,\omega_m\rbrack,其中0\leq\omega_i<\left|V\right|是词库一个token的索引

(2) 网络查找每个单词的映射并将它们连接成一个输入向量:

x=\lbrack{\boldsymbol L}_{\omega0},{\boldsymbol L}_{\omega0},\dots,{\boldsymbol L}_{\omega0}\rbrack\in R^{dm},其中\boldsymbol L\in R^{\vert V\vert\times d}是一个映射矩阵,\boldsymbol L_i是单词i的训练

(3) 预测

\begin{array}{l}h=RELU(x\boldsymbol W+b_1)\\\widehat y=softmax(h\boldsymbol U+b_2)\end{array}

(4) 交叉熵损失的计算

J(\theta)=CE(y,\widehat y)=-\sum_{i=1}^{N_c}y_i\log{\widehat y}_i

具体参考q2_parser_transitions.py

def minibatch_parse(sentences,model,batch_size):
    partial_parses=[PartialParse(s) for s in sentences]
    unfinished_parse=partial_parses
    while len(unfinished_parse)>0:
        minibatch=unfinished_parse[0:batch_size]
        # perform transition and single step parser on the minibatch until it is empty
        while len(minibatch)>0:
            transitions=model.predict(minibatch)
            for index,action in enumerate(transitions):
                minibatch[index].parse_step(action)
            minibatch=[parse for parse in minibatch if len(parse.stack)>1 or len(parse.buffer)>0]
        # move to the next batch
        unfinished_parse=unfinished_parse[batch_size:]
    dependencies=[]
    for n in range(len(sentences)):
        dependencies.append(partial_parses[n].dependencies)
    return dependencies

(e) 为了避免神经元相关性太强导致较差的局部最优,我们利用Xavier对参数随机初始化:

给定一个m\times n为的矩阵A,Xavier随机初始化选定值A_{ij},从\lbrack-\varepsilon,\varepsilon\rbrack中均匀地选值

\varepsilon=\frac{\sqrt6}{\sqrt{m+n}}

我们实现xavier_weight_init函数,具体参考q2_initialization.py

def _xavier_initializer(shape, **kwargs):
    epsilon = np.sqrt(6 / np.sum(shape))
    out = tf.Variable(tf.random_uniform(shape=shape, minval=-epsilon, maxval=epsilon))
    return out

(f) 我们通过Dropout来规范网络,训练中,随机将隐藏层h以概率p_{drop}设置为零(每个小批量丢弃不同的网络单元)

{\boldsymbol h}_{drop}=\gamma\boldsymbol d\circ\boldsymbol h,其中\boldsymbol d\in{\{0,1\}}^{D_h}(D_h\boldsymbol h大小一样)

我们选择\gamma=\frac1{(1-p_{drop}}),则有E_{p_{drop}}h_{drop}_i=E_{p_{drop}}{\lbrack\gamma d_ih_i\rbrack}=p_{drop}(0)+(1-p_{drop})\gamma h_i=(1-p_{drop})\gamma h_i=h_i

(g) 我们利用Adam优化器训练模型,先回顾下随机梯度下降(SGD)的更新规则

\boldsymbol\theta\leftarrow\boldsymbol\theta-\alpha\nabla J_{minibatch}(\boldsymbol\theta),其中\boldsymbol\theta是一个包含所有模型参数的向量,J是损失函数,\nabla J_{minibatch}(\boldsymbol\theta)是小批量数据集上损失函数关于参数的梯度,\alpha是学习率。

Adam采用更加复杂的更新规则

(i) 首先使用momentum获得梯度的滚动平均值

\begin{array}{l}\boldsymbol m\leftarrow\beta_1\boldsymbol m+(1-\beta_1)\nabla J_{minibatch}(\boldsymbol\theta)\\\boldsymbol\theta\leftarrow\boldsymbol\theta-\alpha\boldsymbol m\end{array}

\beta_1为超参数,通常取0.9。每次更新都与前一次更新大致相同(只有1-\beta_1的比例更改),因此变化较小。 一方面,当接近局部最优时,它将阻止参数“弹跳”。 另一方面,滚动平均值有点像在较大小批量数据上计算梯度,因此每次更新将更接近整个数据集上的真实梯度(较低的方差则梯度估计更接近平均值)

(ii) Adam利用自适应学习率获得梯度大小的滚动平均值

\begin{array}{l}\boldsymbol m\leftarrow\beta_1\boldsymbol m+(1-\beta_1)\nabla J_{minibatch}(\boldsymbol\theta)\\\boldsymbol v\leftarrow\beta_2\boldsymbol v+(1-\beta_2)(\nabla J_{minibatch}(\boldsymbol\theta)\circ\nabla J_{minibatch}(\boldsymbol\theta))\\\boldsymbol\theta\leftarrow\boldsymbol\theta-\frac{\mathbf\alpha\boldsymbol\circ\mathbf m}{\sqrt{\mathbf v}}\end{array}

\beta_2为超参数,通常取0.99。平均梯度最小的参数将获得较大更新。在损失非常平坦的地方,参数将获得较大更新从而摆脱高原

(h)  实现神经网络分类器来管理依赖解析器(dependency parser)

具体参考q2_parser_model

在类Config中对超参数进行配置:

class Config(object):
    n_features=36
    n_classes=3
    dropout=0.5
    embed_size=50
    hidden_size=200
    batch_size=2048
    n_epochs=10
    lr=0.001

模型配置:

class ParserModel(Model):
    "输入的占位符"
    def add_placeholders(self):
        self.input_placeholder=tf.placeholder(tf.int32,[None,self.config.n_features])
        self.labels_placeholder=tf.placeholder(tf.float32,[None,self.config.n_classes])
        self.dropout_placeholder=tf.placeholder(tf.float32)
        self.beta_regul=tf.placeholder(tf.float32)
    
    "向占位符喂输入数据"
    def create_feed_dict(self,inputs_batch,labels_batch=None,dropout=1,beta_regul=10e-7):
        feed_dict={self.input_placeholder:inputs_batch,\
                   self.dropout_placeholder:dropout,\
                   self.beta_regul:beta_regul}
        if labels_batch is not None:
            feed_dict[self.labels_placeholder]=labels_batch
        return feed_dict

    "输入数据的特征映射"
    def add_embedding(self):
        embedded=tf.Variable(self.pretrained_embeddings)
        embeddings=tf.nn.embedding_lookup(embedded,self.input_placeholder)
        embeddings=tf.reshape(embeddings,[-1,self.config.n_features*self.config.embed_size])
        return embeddings

    "给定输入,进行预测"
    def add_prediction_op(self):
        x=self.add_embedding()
        xavier=xavier_weight_init()
        with tf.variable_scope("transformation"):
            b1=tf.Variable(tf.random_uniform([self.config.hidden_size,]))
            b2=tf.Variable(tf.random_uniform([self.config.n_classes]))

            self.W=W=xavier([self.config.n_features*self.config.embed_size,self.config.hidden_size])
            U=xavier([self.config.hidden_size,self.config.n_classes])

            z1=tf.matmul(x,W)+b1
            h=tf.nn.relu(z1)
            h_drop=tf.nn.dropout(h,self.dropout_placeholder)
            pred=tf.matmul(h_drop,U)+b2
        return pred

    "交叉熵损失的定义,取平均值"
    def add_loss_op(self, pred):
        loss=tf.nn.softmax_cross_entropy_with_logits(logits=pred,
                                    labels=self.labels_placeholder)
        loss+=self.beta_regul*tf.nn.l2_loss(self.W)
        loss=tf.reduce_mean(loss)
        return loss

    "给定用于训练的优化器以及损失函数"
    def add_training_op(self, loss):
        adam_optim=tf.train.AdamOptimizer(self.config.lr)
        train_op=adam_optim.minimize(loss)
        return train_op

    def train_on_batch(self,sess,inputs_batch,labels_batch):
        feed=self.create_feed_dict(inputs_batch,labels_batch=labels_batch,
                                   dropout=self.config.dropout)
        _,loss=sess.run([self.train_op,self.loss],feed_dict=feed)
        return loss

    "进行训练,并在验证集验证,利用Progbar记录训练情况"
    def run_epoch(self,sess,parser,train_examples,dev_set):
        prog=Progbar(target=1+len(train_examples)/self.config.batch_size)
        for i,(train_x,train_y) in enumerate(minibatches(train_examples,self.config.batch_size)):
            loss=self.train_on_batch(sess,train_x,train_y)
            prog.update(i+1,[("train loss",loss)])

        print("Evaluation on dev set")
        dev_UAS,_=parser.parse(dev_set)
        print("-dev UAS: {:.2f}".format(dev_UAS*100.0))
        return dev_UAS

    def fit(self,sess,saver,parser,train_examples,dev_set):
        best_dev_UAS=0
        for epoch in range(self.config.n_epochs):
            print("Epoch {:} out of {:}".format(epoch+1,self.config.n_epochs))
            dev_UAS=self.run_epoch(sess,parser,train_examples,dev_set)
            if dev_UAS>best_dev_UAS:
                best_dev_UAS=dev_UAS
                if saver:
                    print("New best dev UAS! Saving model in ./data/weights/parser.weights")
                    saver.save(sess,'./data/weights/parser.weights')
            print()

    def __init__(self,config,pretrained_embeddings):
        self.pretrained_embeddings=pretrained_embeddings
        self.config=config
        self.build()

在主函数进行初始化,训练以及测试:

def main(debug=False):
    print(80*"=")
    print("INITIALIZING")
    print(80*"=")
    config=Config()
    parser,embeddings,train_examples,dev_set,test_set=load_and_preprocess_data()
    #print(parser) # utils.parser_utils.Parser object at 0x0000021B09CF66A0
    #print(embeddings.shape) # (5157, 50)
    #print(len(train_examples)) # 48390
    #print(len(dev_set)) # 500
    #print(len(test_set)) # 500
    if not os.path.exists('./data/weights'):
        os.makedirs('./data/weights/')

    with tf.Graph().as_default():
        print("Building model...",)
        start=time.time()
        model=ParserModel(config,embeddings)
        parser.model=model
        print("took {:.2f} seconds\n".format(time.time()-start))

        init=tf.global_variables_initializer()
        saver=None if debug else tf.train.Saver()

        with tf.Session() as session:
            parser.session=session
            session.run(init)

            print(80*"=")
            print("TRAINING")
            print(80*"=")
            model.fit(session,saver,parser,train_examples,dev_set)

            if not debug:
                print(80*"=")
                print("TESTING")
                print(80*"=")
                print("Restoring the best model weights found on the dev set")
                saver.restore(session,'./data/weights/parser.weights')
                print("Final evaluation on test set")
                UAS,dependencies=parser.parse(test_set)
                print("- test UAS: {:.2f}".format(UAS*100.0))
                print("Writing predictions")
                with open('q2_test.predicted.pkl','wb') as f:
                    pickle.dump(dependencies,f,-1)
                print("Done!")

下面我具体说下python中Pickle模块的dump()和load()函数,在CS224d的任务一二中均有用到

1.序列化VS反序列化

  • 数据写入:***序列化***把内存里面的对象转换为字节序列的过程
  • 文件读取:***反序列化***把文件中一连串的字节序列恢复为对象存放的过程

2.Python实现序列化:Pickle模块

  • 只能在Python中使用,仅支持Python的基本数据类型
  • 可以处理复杂的序列化语法(如自定义的类方法,游戏存档)
  • 序列化整个对象,而不是内存地址
import pickle

(1)dump()

pickle.dump(obj,file,[,protocol])

参数:

  • obj:对象
  • file:保存到的类文件对象,必须有write()接口,可以是一个"w"打开的文件/StringIO对象/任意实现write()接口的对象
  • protocol:序列化模式,默认0(ASCII协议以文本形式序列化)。还可以1(老式二进制协议)/2(新二进制协议)
import pickle
test=r'test.txt'
#反序列化也要定义相同函数名,函数体没限制
def sayhi(name):
    print("hello",name)

info={
    '':'',
    'age':32,
    'func':sayhi
}

print(pickle.dumps(info))

with open(test,'wb') as f:
    #f.write(pickle.dumps(info))
    pickle.dump(info,f) #与f.write(pickle.dumps(info))语意相同

(2) load()

pickle.load(file):反序列化对象,将文件中的数据解析为一个python对象

参数:

file:有read()接口和readline()接口

import pickle
test=r'test.txt'
#需要定义序列化代码中同样的函数名,函数体没限制
def sayhi(name):
    print("hello",name)
    print("hello2",name)

with open(test,'rb') as f:
    #data=pickle.loads(f.read())
    data=pickle.load(f) # 和上面data=pickle.loads(f.read())语意相同
    print('data>>>',data)
print(data['func']("Alex"))

得到如下输出

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值