├── README.md ├── RNN.py └── char-rnn.py /README.md: -------------------------------------------------------------------------------- 1 | # char-rnn 2 | 字符级别的RNN模型,[karpathy](https://github.com/karpathy)公布了一个用python实现的100来行的轻量级[char-rnn模型](https://gist.github.com/karpathy/d4dee566867f8291f086)。模型核心是一个RNN,输入单元为一个字符,训练语料为若干篇英文文本。模型通过RNN对连续序列有很好的学习能力,对文本进行建模,学习得到字符级别上的连贯性,从而可以利用训练好的模型来进行文本生成。这里还有一个用tensorflow实现的版本[char-rnn-tf](https://github.com/hit-computer/char-rnn-tf),这个程序和karpathy的完整版char-rnn模型更相似,模型性能更强大,最后生成文本的效果也会比该程序好,并且tensorflow版本程序还支持多种生成策略(max,sample,以及beam-search)。 3 | 4 | 参照karpathy公布的代码,我用theano重写了该模型,使模型能够支持中文文本,同时将源程序中的RNN模型替换成了[GRU(Cho et al., 2014b)](http://arxiv.org/abs/1406.1078)。GRU,和LSTM一样解决了基本RNN模型在长序列上的梯度消失或梯度爆炸问题([Bengio et al., 1994](http://ieeexplore.ieee.org/xpl/login.jsp?tp=&arnumber=279181&url=http%3A%2F%2Fieeexplore.ieee.org%2Fxpls%2Fabs_all.jsp%3Farnumber%3D279181)),然而GRU和LSTM的效果差不多却比LSTM更简单([Greff et al., 2015](http://arxiv.org/abs/1503.04069))。陆陆续续有研究者开始在英文上使用char-rnn,有论文证实char-rnn比以词为输入的rnn模型要好,它有一个优势就是可以解决未登录词的问题([Dhingra et al., 2016](http://arxiv.org/abs/1605.03481))。然而,char-rnn训练中文语料还有另外一个优势是不用对中文进行分词,模型输入单元为一个一个的字,这样可以避免分词带来的一些错误,所以采用char-rnn在中文语料上就两个好处。利用在大规模中文文本上训练好的模型,可以生成一篇短文(生成的时候也是一个字一个字的产生)。具体可以参考karpathy写的一篇[blog](http://karpathy.github.io/2015/05/21/rnn-effectiveness/)。 5 | 6 | ------------------------------------------- 7 | 8 | ## 运行说明 9 | 在命令行中输入: 10 | 11 | THEANO_FLAGS='mode=FAST_RUN,floatX=float32' python char-rnn.py [训练语料] 12 | 13 | 若机器上有GPU,可以使用GPU进行训练,速度比CPU能快很多,输入命令改为: 14 | 15 | THEANO_FLAGS='mode=FAST_RUN,device=gpu,floatX=float32' python char-rnn.py [训练语料] 16 | 17 | 注意:训练语料为文本文件,请采用utf-8编码,可以考虑在每一个语义段落前加上起始符‘^’。 18 | 19 | 20 | char-rnn.py文件里面有以下参数可以设定: 21 | - hidden_size:神经网络隐含层的维度 22 | - seq_length:RNN展开的步骤数(每次训练多少个字符) 23 | - learning_rate:学习率 24 | - iter:迭代次数 25 | - save_freq:每迭代多少次保存一次模型,同时进行一次生成 26 | - idx_of_begin:生成语段的起始字符 27 | - len_of_sample:生成语段的字符数目 28 | 29 | ## 实验结果 30 | 本实验选取了大量和“选择”相关的作文作为训练语料,在生成的时候起始符设定为“选”字,生成字符设定为100个字符,以下是部分生成结果(训练语料规模:1.12M): 31 | 32 | 运行环境:CentOS Linux release 7.2.1511,一块显卡Tesla K40m,Theano 0.7.0。全部数据迭代一轮大致需要45分钟左右。 33 | 34 | 迭代50次: 35 | 36 | >选择了,我们的选择是一种美丽的人生,就是一个人的人生,就是一个人的人生,就是一个人的人生,就是一个人的人生,就是一个人的人生,就是一个人的人生,就是一个人的人生,就是一个人的人生,就是一个人的人生,就是 37 | 38 | 迭代100次: 39 | >选择了,我们不能选择自己的人生,就是一个人的人生,不是因为我们的选择,不是一个人的人生,就是一个人的人生,不是因为我们的选择,不是一个人的人生,就是一个人的人生,不是因为我们的选择,不是一个人的人生,就 40 | 41 | 迭代200次: 42 | >选择了,我们不能选择自己的人生,不是因为我们的选择,不要放弃,就是一个人生的价值,我们的选择是一种选择,而是一个人的人生,不是因为我们的选择,不是一个人的人生,不是因为我们的选择,不是一个人的人生,不是 43 | 44 | 从实验结果看出,模型生成每个短句(由逗号隔开的)还算通顺,但整段连起来看就不知所云了。当然,这和选用的语料以及参数设定有很大关系,但我们依旧可以看出一些问题,就是后半段生成的内容会出现重复,这表明序列太长即使是GRU仍然会有信息丢失。所以,目前在自然语言生成方面的研究大多都是在做短句的生成,例如生成诗歌或者生成歌词这些,并且不是整段一并生成的。 45 | 46 | 对于出现文本重复现象的一点经验性总结(同时可以参考[char-rnn-tf](https://github.com/hit-computer/char-rnn-tf)的实验结果以及实验结果分析):其实在做文本生成的时候有两种策略,一种是max还有一种是sample(在karpathy的程序中有体现),本程序用的是max策略,这种策略会导致重复的现象,而sample策略不会但句子连贯性方面会比max策略稍差一些,随着语料和迭代次数的增加sample策略所生产的文本连贯性会越好(karpathy的程序默认采用的是sample策略)。我最近用tensorflow重写这个模型([char-rnn-tf](https://github.com/hit-computer/char-rnn-tf))后发现增加训练语料以及采用多层RNN能使重复现象出现时序列长度更长(采用max策略时)。 47 | -------------------------------------------------------------------------------- /RNN.py: -------------------------------------------------------------------------------- 1 | #coding:utf-8 2 | import theano 3 | import theano.tensor as T 4 | import numpy as np 5 | 6 | def NormalInit(n, m): 7 | return np.float32(np.random.randn(n, m)*0.01) 8 | 9 | def add_to_params(params, new_param): 10 | params.append(new_param) 11 | return new_param 12 | 13 | class GRU(): 14 | def init_params(self): 15 | self.Ws_in = add_to_params(self.params, theano.shared(value=NormalInit(self.input_dim, self.sdim), name='Ws_in'+self.name)) 16 | self.Ws_hh = add_to_params(self.params, theano.shared(value=NormalInit(self.sdim, self.sdim), name='Ws_hh'+self.name)) 17 | self.bs_hh = add_to_params(self.params, theano.shared(value=np.zeros((self.sdim,), dtype='float32'), name='bs_hh'+self.name)) 18 | 19 | self.Ws_in_r = add_to_params(self.params, theano.shared(value=NormalInit(self.input_dim, self.sdim), name='Ws_in_r'+self.name)) 20 | self.Ws_in_z = add_to_params(self.params, theano.shared(value=NormalInit(self.input_dim, self.sdim), name='Ws_in_z'+self.name)) 21 | self.Ws_hh_r = add_to_params(self.params, theano.shared(value=NormalInit(self.sdim, self.sdim), name='Ws_hh_r'+self.name)) 22 | self.Ws_hh_z = add_to_params(self.params, theano.shared(value=NormalInit(self.sdim, self.sdim), name='Ws_hh_z'+self.name)) 23 | self.bs_z = add_to_params(self.params, theano.shared(value=np.zeros((self.sdim,), dtype='float32'), name='bs_z'+self.name)) 24 | self.bs_r = add_to_params(self.params, theano.shared(value=np.zeros((self.sdim,), dtype='float32'), name='bs_r'+self.name)) 25 | self.Why = add_to_params(self.params, theano.shared(value=NormalInit(self.sdim, self.input_dim), name='Why'+self.name)) 26 | self.by = add_to_params(self.params, theano.shared(value=np.zeros((self.input_dim,), dtype='float32'), name='by'+self.name)) 27 | 28 | def recurrent_fn(self, idx, ht): 29 | xi = theano.shared(np.zeros((self.input_dim, 1), dtype='float32'),name='xi') 30 | x = T.set_subtensor(xi[idx], 1.0) 31 | x = x.T 32 | 33 | rs_t = T.nnet.sigmoid(T.dot(x, self.Ws_in_r) + T.dot(ht, self.Ws_hh_r) + self.bs_r) 34 | zs_t = T.nnet.sigmoid(T.dot(x, self.Ws_in_z) + T.dot(ht, self.Ws_hh_z) + self.bs_z) 35 | hs_tilde = T.tanh(T.dot(x, self.Ws_in) + T.dot(rs_t * ht, self.Ws_hh) + self.bs_hh) 36 | hs_update = zs_t * ht + (np.float32(1.) - zs_t) * hs_tilde 37 | 38 | ys = T.dot(hs_update, self.Why) + self.by 39 | ps = T.exp(ys)/T.sum(T.exp(ys)) 40 | ps = ps.flatten() 41 | 42 | return hs_update, ps 43 | 44 | def build_GRU(self, training_x, h0): 45 | _res, _ = theano.scan(self.recurrent_fn, sequences=[training_x], outputs_info=[h0, None]) 46 | 47 | Probs = _res[1] 48 | return Probs 49 | 50 | def get_params(self): 51 | return self.params 52 | 53 | def sample(self): 54 | def recurrent_gn(hs, idx): 55 | h_t, ps = self.recurrent_fn(idx, hs) 56 | 57 | y_i = T.argmax(ps) #using a greedy strategy 58 | 59 | return h_t, y_i 60 | 61 | h_0 = theano.shared(value=np.zeros((1, self.sdim),dtype='float32'), name='h0') 62 | 63 | sod = T.lscalar('sod') 64 | n = T.lscalar('n') 65 | [h, y_idx], _ = theano.scan(recurrent_gn, 66 | outputs_info = [h_0, sod], 67 | n_steps = n) 68 | 69 | sample_model = theano.function(inputs=[sod, n], outputs=[y_idx], on_unused_input='ignore') 70 | 71 | return sample_model 72 | 73 | def __init__(self, vocab_size, hidden_size): 74 | self.params = [] 75 | self.input_dim = vocab_size 76 | self.sdim = hidden_size 77 | self.name = 'GRU' 78 | self.init_params() 79 | -------------------------------------------------------------------------------- /char-rnn.py: -------------------------------------------------------------------------------- 1 | #coding:utf-8 2 | import theano 3 | import theano.tensor as T 4 | import sys 5 | import numpy as np 6 | import cPickle 7 | from collections import OrderedDict 8 | import RNN 9 | 10 | file = sys.argv[1] 11 | data = open(file,'r').read() 12 | data = data.decode('utf-8') 13 | chars = list(set(data)) #char vocabulary 14 | 15 | data_size, vocab_size = len(data), len(chars) 16 | print 'data has %d characters, %d unique.' % (data_size, vocab_size) 17 | char_to_ix = { ch:i for i,ch in enumerate(chars) } 18 | ix_to_char = { i:ch for i,ch in enumerate(chars) } 19 | 20 | hidden_size = 100 # size of hidden layer of neurons 21 | seq_length = 25 # number of steps to unroll the RNN for 22 | learning_rate = 0.005 23 | iter = 50 24 | save_freq = 5 #The step (counted by the number of iterations) at which the model is saved to hard disk. 25 | idx_of_begin = chars.index(u'^') #begin character 26 | len_of_sample = 100 #The number of characters by sample 27 | 28 | print 'Compile the model...' 29 | 30 | training_x = T.ivector('x_data') 31 | training_y = T.ivector('y_data') 32 | 33 | h0 = theano.shared(value=np.zeros((1, hidden_size),dtype='float32'), name='h0') 34 | 35 | gru = RNN.GRU(vocab_size, hidden_size) 36 | Probs = gru.build_GRU(training_x, h0) #the t-th line of Probs denote probability distribution of vocabulary in t-time step 37 | 38 | target_probs = T.diag(Probs.T[training_y]) #T.diag reture the diagonal of matrix 39 | cost = -T.log(target_probs) 40 | training_cost = T.sum(cost) 41 | 42 | def sharedX(value, name=None, borrow=False, dtype=None): 43 | if dtype is None: 44 | dtype = theano.config.floatX 45 | return theano.shared(theano._asarray(value, dtype=dtype), 46 | name=name, 47 | borrow=borrow) 48 | 49 | def compute_updates(training_cost, params): #adagrad update 50 | updates = [] 51 | 52 | grads = T.grad(training_cost, params) 53 | grads = OrderedDict(zip(params, grads)) 54 | 55 | for p, g in grads.items(): 56 | m = sharedX(p.get_value() * 0.) 57 | m_t = m + g * g 58 | p_t = p - learning_rate * g / T.sqrt(m_t + 1e-8) 59 | updates.append((m, m_t)) 60 | updates.append((p, p_t)) 61 | 62 | return updates 63 | 64 | params = gru.get_params() 65 | updates = compute_updates(training_cost, params) 66 | 67 | train_model = theano.function(inputs=[training_x, training_y], outputs=[training_cost], updates=updates, on_unused_input='ignore', name="train_fn") 68 | sample_model = gru.sample() 69 | print 'Done!' 70 | 71 | 72 | def dumpModel(filename): 73 | save_file = open(filename, 'wb') # this will overwrite current contents 74 | for param in params: 75 | cPickle.dump(param.get_value(borrow = True),save_file,-1) 76 | save_file.close() 77 | 78 | def loadModel(filename): 79 | load_file = open(filename,'rb') 80 | param_list = params 81 | for i in range(len(param_list)): 82 | param_list[i].set_value(cPickle.load(load_file), borrow = True) 83 | load_file.close() 84 | 85 | def sample(seed_ix, n): #generate a text that contains n characters. 86 | out = sample_model(seed_ix, n) 87 | out = out[0].tolist() 88 | essay = ''.join([ix_to_char[i] for i in out]) 89 | return essay 90 | 91 | p = 0 92 | n = 0 93 | loss = 0 94 | i = 1 95 | 96 | print 'Begin training...' 97 | while(i<=iter): 98 | if p+seq_length+1 >= len(data): 99 | h0.set_value(np.zeros((1, hidden_size),dtype='float32')) 100 | p = 0 # go from start of data 101 | print 'the iter is:',i 102 | print 'the loss is:',loss 103 | print 'average loss: ',loss/n 104 | 105 | if i%save_freq == 0: 106 | print('save model:iter = %i' % i) 107 | dumpModel('model'+str(i)) #save the model 108 | out = sample(idx_of_begin, len_of_sample) #generate a text that contains len_of_sample characters 109 | print 'sample:',out 110 | 111 | loss = 0 112 | n = 0 113 | i += 1 114 | inputs = [char_to_ix[ch] for ch in data[p:p+seq_length]] 115 | targets = [char_to_ix[ch] for ch in data[p+1:p+seq_length+1]] 116 | 117 | loss_ = train_model(inputs,targets) 118 | loss += loss_[0] 119 | 120 | n += 1 121 | p += seq_length 122 | --------------------------------------------------------------------------------