自然语言处理和图像处理不同,作为人类抽象出来的高级表达形式,它和图像、声音不同,图像和声音十分直觉,比如图像的像素的颜色表达可以直接量化成数字输入到神经网络中,当然如果是经过压缩的格式jpeg等必须还要经过一个解码的过程才能变成像素的高阶矩阵的形式,而自然语言则不同,自然语言和数字之间没有那么直接的相关关系,也就不是那么容易作为特征输入到神经网络中去了,所以,用神经网络处理自然语言,不可避免的在数据预处理方面更加繁琐,也更加细致!自然语言处理的另外一个不同之处在于语言之间的相关关系,举一个最简单的例子,在做智能助理机器人的时候,一句“我将在上午十点到达北京”和“我将在上午十点离开北京” 如果你只考虑每个独立的词汇的话,那么“北京”到底是作为始发地还是目的地是不得而知的,也就是说你必须得联系上下文才能够更好的理解整个句子的意思!这也是自然语言的特别之处!当然针对语言的这种特性,也有相应的用于处理的网络——RNN(recurrent neural network)循环神经网络!
作为分类到Tensorflow编程实战里的一篇博客,当然以解释代码为主,具体的理论部分这里不过多解释,开门见山,本次处理的数据集是PTB数据集,它是目前语言模型学习中使用的最为广泛的数据集,PTB数据集的下载地址是: http://www.fit.vutbr.cz/~imikolov/rnnlm/simple-examples.tgz
将数据集解压之后将会得到好几个文件夹,重点看/data文件夹下的三个文件,ptb.test.txt, ptb.train.txt, ptb.valid.txt 这三个数据文件已经经过了预处理,相邻的单词之间用空格隔开,以ptb.train.txt为例,看一下它里面的具体内容:
no it was n't black monday but while the new york stock exchange did n't fall apart friday as the dow jones industrial average plunged N points most of it in the final hour it barely managed to stay this side of chaos
some circuit breakers installed after the october N crash failed their first test traders say unable to cool the selling panic in both stocks and futures
the N stock specialist firms on the big board floor
数据集中包含了9998个词汇,加上稀有词语的特殊符号 <unk>和语句的结束标记,一共是10000个! 为了能够将数据输入到神经网络,我们首先需要做的就是这10000个词汇按照其出现的频率进行编号,形成词汇表,然后按照词汇表的对应关系,将train/test/valid数据集分别转换成编号的形式!
按照单词出现的频率进行编号的代码如下:
1 import codecs 2 import collections 3 from operator import itemgetter 4 5 RAW_DATA = "C:\Users\Yang\Desktop\nlp\ptb.train.txt" #数据的输入路径/ 6 VOCAB_OUTPUT ="ptb.vocab" #词汇表的输出路径 7 8 counter = collections.Counter() #counter 顾名思义是一个计数器 9 10 with codecs.open(RAW_DATA,"r","utf-8") as f: 以read的方式,utf-8编码的形式打开上述路径 11 for line in f: #读行 12 for word in line.strip().split(): #以空格作为划分,将文件里面的每一个词汇都切开! strip()默认用于去掉收尾的空格和换行符 13 counter[word] += 1 #统计单词出现的次数 14 15 sorted_word_to_cnt = sorted(counter.items(),key=itemgetter(1),reverse=True) 按照itemgetter(1)属性进行排序,应该就是按照单词出现的次数排序,例如:and:981 16 sorted_words = [x[0] for x in sorted_word_to_cnt] #之所以取x[0]是因为格式是 the:356 这种形式,x[0]就是为了将单词取出来 排好序的word 17 18 sorted_words = ["<eos>"] + sorted_words #将句子的结束符添加到排好序的list中 19 #因为PTB数据中已经将低频词汇替换成了<"unk">所以下面这步没必要 <"eos">是句子结束符 <"sos">是句子开始符 <"unk">是句子的低频词汇 20 #sorted_words = ["<unk>","<sos>","<eos>"] + sorted_words 21 #if len(sorted_words) >10000: 22 #sorted_words = sorted_words[:10000] 标红的这三句没必要出现! 23 24 with codecs.open(VOCAB_OUTPUT,"w",'utf-8') as file_output : 25 26 for word in sorted_words: 27 file_output.write(word + " ") #将排序好的词汇再写回输出的文件目录里面,那么就算完成了词汇对应表的构建
写完了词汇表的构建代码之后,我们还需要一个将ptb.train/test/valid.txt文件转换成对应的编号的过程! 每一个词汇对应的编号就是其在词汇表里面对应的行号! 注意到之前每一个word的输出后面都跟着" "
下面就来实现将单词转换成对应的编号的部分的代码:
1 import codecs 2 import sys 3 4 RAW_DATA = 'C:\Users\Yang\Desktop\nlp\data\ptb.valid.txt' #这里是待转换的文件的目录/相应的改成ptb.test.txt/ptb.train.txt可以用于其他的转换 5 VOCAB = 'ptb.vocab' #词汇表的目录 6 OUTPUT_DATA = 'ptb.valid' 用于输出的目录 7 8 9 with codecs.open(VOCAB,'r','utf-8') as f_vocab: #首先就是将词汇表以read和utf-8编码的方式打开 10 vocab = [w.strip() for w in f_vocab.readlines()] #我怎么感觉这里只是读到了一行然后进行收尾空格换行符去掉的处理??? 11 #哦,我明白了 因为VOCAB中词汇的存储格式就是一个单词占一行,所以出来的就是一个个单词而不需要.split() 12 word_to_id = {k:v for (k,v) in zip(vocab,range(len(vocab)))} #转换成了单词序号的字典形式 这里是完成了词汇和其对应编号的对应! 13 14 #将词汇转换成了对应的行号序号 15 def get_id(word): 16 return word_to_id[word] if word in word_to_id else word_to_id["<unk>"] #如果是在词汇表里面的就将它转换成对应的编号,否则的话就转换成unk对应的编号 17 18 fin = codecs.open(RAW_DATA,"r","utf-8") #打开待转换文件 19 fout = codecs.open(OUTPUT_DATA,"w",'utf-8') #输出文件目录 20 21 for line in fin: 22 words = line.strip().split() + ["<eos>"] #打开的文件读取每一行然后首尾去除换行符和空格之后对空格进行切分,在末尾加上eos! 23 out_line = ' '.join([str(get_id(w)) for w in words]) +' ' 对其的行进行转换 24 fout.write(out_line) 然后写入到对应的输出文件里面 25 26 fin.close() 27 fout.close()
我们来看一下运行完这个代码之后我们的ptb.train.txt变成了什么样子:
9994 9996 9974 9999 9972 9978 9981 9993 9977 9973 9985 9998 9992 9971 9997 9990 9995 9970 9989 9987 9988 9975 9980 9986 0
没错,全部变成了词汇表对应的行号编号的形式!
进行完上述两步处理之后,我们已经完成了数据预处理部分的一半了,接下来我们需要考虑的问题就是,处理后的编码数据不可能直接输入到网络里面去,网络接受的一般都是许多batch,但是在将数据打包成batch
的时候,又有一个十分严峻的问题需要我们考虑,那就是到底以多长的长度来打包数据呢? 每条句子的长度是不一样的,该如何打包呢? 有两种比较常见的做法,一种是根据batch_size的大小,选取里面最长的句子为
参考,剩下的padding成统一的长度!长度统一之后就可以用于batch了!
而PTB数据是一整段一整段文本,如果一句一句的进行切分的话,会破坏掉数据之间的上下文的关联,而语言模型为了利用上下文信息,必须将前面的信息传递到后面的句子中去,所以PTB通常采用的是另外一种Batch的方法!
该方法将长序列切割成固定长度的子序列,然后循环神经网络处理完这个子序列之后将最后一个隐藏层的结果复制到下一个序列中作为初始值,这样在前向计算的过程中效果就等同于一次性的读取完了全部的文档!
但是如果这样做的话会面临另外一个问题,整个数据流都是串行流动的,这对于可以并行计算的tensorflow来说简直太浪费了,所以,兼顾二者的做法是,根据batch_size的大小将整个文档划分成batch_size部分!然后让batch_size的每一个位置负责一部分数据,如果处理不完,继续横向移动到下一个batch中去,也就是说,对于batch来讲数据之间横向才是连贯的,纵向其实是互不相干的!这样既能够保证数据之间的上下文关系,也能够保证tensorflow可以并行处理数据!
画一个示意图的话,大概是下面这样: