接下来我们来看build_data.py中的接下来几句。
# Generators
# 获取训练集、测试集、开发集
dev = CoNLLDataset(config.filename_dev, processing_word)
test = CoNLLDataset(config.filename_test, processing_word)
train = CoNLLDataset(config.filename_train, processing_word)
Generator,字面意思是生成器,也比较好理解:这是生成了开发集(dev)、测试集(test)和训练集(train)。
CoNLLDateset是data_utils.py中定义的一个类,传入的参数是config.filename_(dev/test/train)和processing_word。
config.filename_(dev/test/train)是Config类中定义的几个参数。
# dataset
# filename_dev = "data/coNLL/eng/eng.testa.iob"
# filename_test = "data/coNLL/eng/eng.testb.iob"
# filename_train = "data/coNLL/eng/eng.train.iob"
filename_dev = filename_test = filename_train = "data/test.txt" # test
这里有一个注释,原本是有训练集、开发集和测试集的,不过文件太大,而且我们的目的是跑一下程序,初步学习一下,所以这里就注释掉统一换成 "data/test.txt"路径的这个迷你文件。
至于另一个参数processing_word,之前我们介绍过,它返回一个函数闭包函数的引用,用于处理word,进行数据清理和(word--id)对标。
接下来我们进去data_utils.py文件看一下CoNLLDataset类。
class CoNLLDataset(object):
"""Class that iterates over CoNLL Dataset
__iter__ method yields a tuple (words, tags)
words: list of raw words
tags: list of raw tags
If processing_word and processing_tag are not None,
optional preprocessing is appplied
Example:
```python
data = CoNLLDataset(filename)
for sentence, tags in data:
pass
```
"""
def __init__(self, filename, processing_word=None, processing_tag=None,
max_iter=None):
"""
Args:
filename: path to the file
processing_words: (optional) function that takes a word as input
processing_tags: (optional) function that takes a tag as input
max_iter: (optional) max number of sentences to yield
"""
self.filename = filename
self.processing_word = processing_word
self.processing_tag = processing_tag
self.max_iter = max_iter
self.length = None
def __iter__(self):
niter = 0
with open(self.filename) as f:
words, tags = [], []
for line in f:
line = line.strip()
if (len(line) == 0 or line.startswith("-DOCSTART-")):
if len(words) != 0:
niter += 1
if self.max_iter is not None and niter > self.max_iter:
break
yield words, tags
words, tags = [], []
else:
ls = line.split(' ')
word, tag = ls[0],ls[1]
if self.processing_word is not None:
word = self.processing_word(word)
if self.processing_tag is not None:
tag = self.processing_tag(tag)
words += [word]
tags += [tag]
def __len__(self):
"""Iterates once over the corpus to set and store length"""
if self.length is None:
self.length = 0
for _ in self:
self.length += 1
return self.length
所以这里是建了三个类的实例化看一下类的介绍,这是基于CoNLL数据集的迭代。CoNLL 系列评测是自然语言处理领域影响力最大的技术评测,每年由 ACL 的计算自然语言学习会议(Conference on Computational Natural Language Learning,CoNLL)主办。我还在网上找到了一个CoNLL的NER数据集,这是地址,密码i0nq,有需求的同志们可以自行下载。
__iter__方法生成了一个(单词,标签)对的元组。如果processing_word和processing_tag是非空的,则应用可选择的预处理。
由于这里我们实例化的时候,给参数processing_word赋了值,接受一个word作为输入。
接下来是一个__iter__()函数的定义。首先__init__()函数我们知道是类的初始化函数,__iter__()是一个可迭代对象。在Python中,list、tuple等都是可迭代对象,我们可以通过iter()函数获取这些可迭代对象的迭代器。然后我们可以对获取到的迭代器不断使用next()函数来获取下条数据。__iter__()函数实际上就是调用了可迭代对象的__iter__()法。
>>> li = [11, 22, 33, 44, 55]
>>> li_iter = iter(li)
>>> next(li_iter) 11
>>> next(li_iter) 22
>>> next(li_iter) 33
>>> next(li_iter) 44
>>> next(li_iter) 55
>>> next(li_iter)
Traceback (most recent call last):
File "", line 1, in
StopIteration
可以看到,迭代完最后一个就会抛出StopIteration异常。
下面我们分析一下__iter__()函数体。
niter是当前迭代次数,初始化为0,然后这时打开文件,将其对象赋给f,创建words和tags两个空列表用于将来存放单词和标签,然后用一个for函数按行读取数据,每一行赋给line变量。
strip()函数用来移除字符串部头和尾部指定的字符,默认情况是空格或换行符,但是这里注意,strip()只能移除字符串头部或尾部的,中间字符不行。
下一句,如果长度为0或者以"-DOCSTART-"开头:如果长度不为0,即如果以"-DOCSTART-"开拓,则迭代数niter+1,如果最大迭代数max_iter已经有了定义且niter超过最大max_iter则跳出循环,返回words和tags,同时将words和tags两个列表置空。
yield是Python的关键字,关于yield的深入剖析,后期我还会出一个单独章节来讲解,这里先简要说一下。
- yield返回一个可迭代的生成器,和传统的for循环相比,for循环所有的数据都存放在内存里,而yield不同,它就是return返回一个值,但比return多一步就是记录了当前返回值的位置,下次执行的时候从这个位置开始。举个栗子。
#encoding:UTF-8
def yield_test(n):
for i in range(n):
yield call(i)
print("i=",i)
#做一些其它的事情
print("do something.")
print("end.")
def call(i):
return i*2
#使用for循环
for n in yield_test(5):
print("n = ", n)
这里定义了一个yield_test函数和一个call函数,其中yield_test()是iterable类型,它的作用是从i到n打印并且调用call()函数,在for循环的外边,还有两个打印函数。
接下来 for i in yield_test(5),我们看一下运行结果。
- 首先是一个0-5的循环(0、1、2、3、4),先进入yield_test()函数中是一个for循环(也是0-5),先返回一个call(0),也就是返回0*2 =0 给 n,此时 n仍然是0,接下来执行yield_test(1),此时先从yield的下一行进行,即先输出( i = 0 ),然后i变为1,同时返回call(1),call(1)返回2给 n,这是输出n就成了2,以此类推。
回到之前的函数,else:如果长度不为0且并不是以-DOCSTART-开头,用split()函数对文本进行切分,split(‘ ’)意思是以空格为分隔符,切分word-tag键值对并赋给word和tag,如果processing_word选项非空则运行processing_word()函数,如果processing_tag选项非空则运行processing_tag函数,这里我们传入的参数只有processing_word,就只进行processing_word处理。
最后,将word 和 tag分别追加到words和tags列表中去。
- 最后是一个定义长度的函数__len__(),遍历语料库,如果self.length为None,则设置长度为0,否则,不断自增。