Textsum模型在toy dataset上的运行过程参见上一篇博文:Textsum运行录
为了进行更深入的实验,我们需要使用更大更有效的数据集,同时也需要将其他格式的原始数据集转换成Textsum模型可以读入的格式。这里我准备使用CNN新闻数据。我没有使用surmenok的数据转换代码(他那份代码实在是太长太难懂了),而是自己编写了一套工具。主要分为两个方面,一个是data数据集的构造,一个是vocab词表的生成。
1 下载原始数据集
我们在纽约大学的网站:DeepMind Q&A Dataset上下载CNN的stories。文件一共151MB,建议先下载到本地再传到服务器上,如果你的服务器不支持外网的话。
下载后的文件是一个压缩包,不要尝试在本地直接解压它,里面包含了近十万个文件,解压软件会崩溃掉。为了观察数据的格式,我们在服务器上解压,尝试输出众多文件中的随意一个。实际上能观察出文件名是有规律的,它的开头大致是一个十六进制的序号。
$ cat cnn/stories/a01e832224d2ccffd1d8b38f1020a68c779f0f06.story
我们能看到结果如下:查看文档内容
分析这个文件的格式,可以看出每个段落占一行,每两个段落之间有一个空行,下面有若干个@highlight标签,标签下面是对特定问题的回答,因为这个数据集本来是用来问答的。
我们可以认为第一行就是这篇新闻的摘要,至少我们可以这么假设。之后我会再下载HTML格式的story,看看能不能找到它的标题。
2 data数据集的构造
在Textsum的工程中有个data_convert_example.py
文件,这个文件给出了文本文档与TensorFlow的二进制文档的转换规则。下面是文本转二进制的过程:
def _text_to_binary():
inputs = open(FLAGS.in_file, 'r').readlines()
writer = open(FLAGS.out_file, 'wb')
for inp in inputs:
tf_example = example_pb2.Example()
for feature in inp.strip().split('\t'):
(k, v) = feature.split('=')
tf_example.features.feature[k].bytes_list.value.extend([v])
tf_example_str = tf_example.SerializeToString()
str_len = len(tf_example_str)
writer.write(struct.pack('q', str_len))
writer.write(struct.pack('%ds' % str_len, tf_example_str))
writer.close()
下面是二进制转文本的过程:
def _binary_to_text():
reader = open(FLAGS.in_file, 'rb')
writer = open(FLAGS.out_file, 'w')
while True:
len_bytes = reader.read(8)
if not len_bytes:
sys.stderr.write('Done reading\n')
return
str_len = struct.unpack('q', len_bytes)[0]
tf_example_str = struct.unpack('%ds' % str_len, reader.read(str_len))[0]
tf_example = example_pb2.Example.FromString(tf_example_str)
examples = []
for key in tf_example.features.feature:
examples.append('%s=%s' % (key, tf_example.features.feature[key].bytes_list.value[0]))
writer.write('%s\n' % '\t'.join(examples))
reader.close()
writer.close()
我们可以从代码中大概看出模型对文本文件的格式要求,为了进一步分析,我们可以将示例的toy data解析成文本。将模型给出的示例代码data_convert_example.py
拷贝到服务器上,保证toy data文件与脚本在同一目录,CNN的story文件所在路径相对这个脚本的路径为raw_data/cnn/stories/
。然后根据脚本中规定的命令行格式,我们使用以下命令:
$ python data_convert_example.py --command binary_to_text --in_file data --out_file text_data
我们可以看到生成的文本文件的内容如下:查看完整内容。其中一行为:
abstract=<d> <p> <s> sri lanka closes schools as war escalates . </s> </p> </d> article=<d> <p> <s> the sri lankan government on wednesday announced the closure of government schools with immediate effect as a military campaign against tamil separatists escalated in the north of the country . </s> <s> the cabinet wednesday decided to advance the december holidays by one month because of a threat from the liberation tigers of tamil eelam -lrb- ltte -rrb- against school children , a government official said . </s> <s> `` there are intelligence reports that the tigers may try to kill a lot of children to provoke a backlash against tamils in colombo . </s> <s> `` if that happens , troops will have to be withdrawn from the north to maintain law and order here , '' a police official said . </s> <s> he said education minister richard pathirana visited several government schools wednesday before the closure decision was taken . </s> <s> the government will make alternate arrangements to hold end of term examinations , officials said . </s> <s> earlier wednesday , president chandrika kumaratunga said the ltte may step up their attacks in the capital to seek revenge for the ongoing military offensive which she described as the biggest ever drive to take the tiger town of jaffna . . </s> </p> </d> publisher=AFP
文件的每一行对应一条训练数据,其中有三个Feature,用字符\t
分隔开:abstract, article和publisher。在Feature的关键字的后面用字符=
连接了对应的值。每个值用标签包围起来,其中<d></d>
表示文档Document,<p></p>
表示段落Paragraph,<s></s>
表示句子Sentence。句子中的每个词、标点符号和标签都用空格隔开。下面开始编写脚本。
2.1 去除story文本中的answer
index = len(text)
for i, t in enumerate(text):
if t.startswith("@highlight"):
index = i
break
text = text[:index]
其中text为story中的readlines()列表。
2.2 使用nltk.tokenize.sent_tokenize进行分句
参数为一个ascii码编码的段落字符串,返回值为分句字符串组成的列表。
2.3 使用nltk.tokenize.word_tokenize进行分词
参数为一个ascii码编码的句子字符串,返回值为分词字符串组成的列表。
关于nltk的安装 NLTK是宾夕法尼亚大学计算机和信息科学系开发的自然语言处理工具包。我们通过pip install nltk
进行安装。安装后,要想使用它的分词和分句功能还需在python的shell中下载内置数据集:
>> import nltk
>> nltk.download('punkt')
2.4 替换非法字符
注意以上两个函数 只支持ascii码格式的字符串的处理,所以要先检测非ascii码字符,将其替换。另外,由于TensorFlow对数据的解析是通过字符=
分割的,所以不允许文段中出现=
。我们使用正则表达式匹配的方式:
for i in range(index):
text[i] = re.sub(r'[^\x00-\x7F]+', '*', text[i].replace('=', '*'))
其中text为story中的readlines()列表。
剩下的是都是一些读写文件的细枝末节,下面给出完整代码。注意本文中的代码用到了TensorFlow环境,需要先使用conda进入上一篇博客中提到的虚拟环境。
# data_convert.py
# -*- coding: utf-8
import struct, sys, glob, random, re
from nltk.tokenize import sent_tokenize, word_tokenize
from tensorflow.core.example import example_pb2
def para_tokenize(para):
sentences = []
para_format = "<p> {} </p>"
sent_format = "<s> {} </s>"
for sentence in sent_tokenize(para):
sentences.append(sent_format.format(' '.join(word_tokenize(sentence))))
return para_format.format(' '.join(sentences))
def raw2text(in_file, out_file):
doc_format = "<d> {} </d>"
dat_format = "abstract={}\tarticle={}\tpublisher=CNN\r\n"
text = open(in_file, 'r').readlines()
writer = open(out_file, 'a')
index = len(text)
for i, t in enumerate(text):
if t.startswith("@highlight"):
index = i
break
text = text[:index]
for i in range(index):
text[i] = re.sub(r'[^\x00-\x7F]+', '*', text[i].replace('=', '*'))
abstract = doc_format.format(para_tokenize(text[0].strip()))
article = doc_format.format(' '.join([para_tokenize(line.strip()) for line in text[1:] if len(line) > 2]))
writer.write(dat_format.format(abstract, article))
writer.close()
def text2bin(in_file, out_file):
inputs = open(in_file, 'r').readlines()
writer = open(out_file, 'wb')
for inp in inputs:
tf_example = example_pb2.Example()
for feature in inp.strip().split('\t'):
(k, v) = feature.split('=')
tf_example.features.feature[k].bytes_list.value.extend([v])
tf_example_str = tf_example.SerializeToString()
str_len = len(tf_example_str)
writer.write(struct.pack('q', str_len))
writer.write(struct.pack('%ds' % str_len, tf_example_str))
writer.close()
def main():
filelist = glob.glob("raw_data/cnn/stories/*.story")
random.shuffle(filelist)
i = 0
for file in filelist:
raw2text(file, "story.txt")
i += 1
if i % 10 == 0:
print "%d / %d" % (i, len(filelist))
text2bin("story.txt", "story.bin")
if __name__ == '__main__':
main()
这个过程没有完成,我们最终还需要划分训练集、验证集和解码测试集,但我打算在确定了文章标题后再进行划分,毕竟划分数据集是一件容易的事儿。
3 vocab词表的生成
观察示例给出的vocab词表实际上是一个词频统计表,大致上单词按照词频从高到低排列,但由于<s></s>
的出现频率太高了,所以作者故意将它调到了最后。另外如果度过Textsum的源码就会知道,源码中有对词表的assert断言,它要求必须有以下几种token出现:<p>,</p>,<s>,</s>,<UNK>,<PAD>,<d>,</d>
。
我编写的词频统计工具是基于之前生成好的模型格式的文本文件的。词频统计的代码跑的出乎意料的慢,大概一天一夜只能跑两万条数据。按理说python的dict使用的是hash表,时间复杂度最好情况下是O(1)的,不知道为什么这么的慢。所以我修改代码,让每一步词频统计都刷新一下输出,而不是在最后输出。我打算用不完全的词表先试一下模型。以下是完整的代码:
# new_count.py
# -*- coding: utf-8
import re
def word_count(text, vocab):
for word in text.split():
word = re.sub(r'[^\x00-\x7F]+', '*', word)
if word in vocab.keys():
vocab[word] += 1
else:
vocab[word] = 1
def main():
vocab = {
'<p>' : 0,
'</p>' : 0,
'<s>' : 0,
'</s>' : 0,
'<UNK>': 0,
'<PAD>': 0,
'<d>' : 0,
'</d>' : 0,
}
inputs = open("story.txt", 'r').readlines()
total = len(inputs)
i = 0
for line in inputs:
for feature in line.strip().split('\t')[:-1]:
word_count(feature.split('=')[1], vocab)
with open("story.voc.new", "w") as writer:
for word, freq in vocab.items():
writer.write(word + " " + str(freq) + "\r\n")
i += 1
if i % 100 == 0:
print "%d out of %d done" % (i, total)
s_start = vocab.pop('<s>')
s_end = vocab.pop('</s>')
vocab = sorted(vocab.items(), key=lambda x: x[1], reverse=True)
vocab.append(('<s>', s_start))
vocab.append(('</s>', s_end))
with open("story.voc.new", 'w') as writer:
for word, freq in vocab:
writer.write(word + " " + str(freq) + "\r\n")
if __name__ == '__main__':
main()