这篇博客,我们实现一个基于神经网络的依赖性分析器,这是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中倒数第二项的独立性)
,其中是词库一个token的索引
(2) 网络查找每个单词的映射并将它们连接成一个输入向量:
,其中是一个映射矩阵,是单词i的训练
(3) 预测
(4) 交叉熵损失的计算
具体参考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对参数随机初始化:
给定一个为的矩阵A,Xavier随机初始化选定值,从中均匀地选值
我们实现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以概率设置为零(每个小批量丢弃不同的网络单元)
,其中(和大小一样)
我们选择,则有
(g) 我们利用Adam优化器训练模型,先回顾下随机梯度下降(SGD)的更新规则
,其中是一个包含所有模型参数的向量,是损失函数,是小批量数据集上损失函数关于参数的梯度,是学习率。
Adam采用更加复杂的更新规则
(i) 首先使用momentum获得梯度的滚动平均值
为超参数,通常取0.9。每次更新都与前一次更新大致相同(只有的比例更改),因此变化较小。 一方面,当接近局部最优时,它将阻止参数“弹跳”。 另一方面,滚动平均值有点像在较大小批量数据上计算梯度,因此每次更新将更接近整个数据集上的真实梯度(较低的方差则梯度估计更接近平均值)
(ii) Adam利用自适应学习率获得梯度大小的滚动平均值
为超参数,通常取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"))
得到如下输出