[关闭]
@vivounicorn 2020-11-06T18:08:41.000000Z 字数 42247 阅读 1401

机器学习与人工智能技术分享-第六章 循环神经网络

机器学习 RNN LSTM 第六章

回到目录


6. 循环神经网络

6.1 RNN

6.1.1 基本原理

序列类问题是我们日常生活中常见的一类问题:我们读的文章,我们说话的语音等等,要么是在空间上的序列,要么是在时间的序列,序列的每个单元之间有前驱后继的语义或序列相关性,比如:当我们说,“这是我们伟大的××”,这里××是“祖国”的概率远远大于“板凳”,所以在NLP领域,应用大概可以分为几种:
1、根据当前上下文语义预测接下来出现某个文本的概率;
2、通过语言模型生成新的文本;
3、文本的通用NLP任务,例如词性标注、文本分类等;
4、机器翻译;
5、文本表示及编码解码。
传统的神经网络并没有很好的解决这种序列问题,于是Recurrent Neural Networks这种网络被提了出来:


乍一看就是节点自带环路的网络,广义来看,可以在这个节点上展开,只是这种展开和输入的字数有关,比如输入为10个字,则展开10层。

从一个角度看,不同于传统神经网络会假设所有输入及输出是相互独立的,RNN正相反,认为节点间天然有相关性;另一个角度是,认为RNN具有“记忆”能力,它能把历史上相关节点状态“全部”记住,但实际情况是,我们当前说的一句话和较久前说的话未必有很强的关系,如果“全部”记住,一没必要、二计算量巨大。

6.1.2 BPTT 原理

以最简单的RNN为例,说明背后算法原理:


定义以下符号:

:输入层第个节点;
:前一个状态的隐藏层第个节点;
:当前状态的隐藏层第个节点;
:输出层第个节点;
:输入层到隐藏层权重矩阵;
:前一状态隐藏层到后一状态隐藏层权重矩阵;
:隐藏层到输出层权重矩阵;
:隐藏层激活函数;
:输出层激活函数。

网络的前向传播关系为:

这里需要注意的一个关键点是:权重矩阵在不同时刻是共享的。

网络的反向传播关系:
只要网络的损失函数可微,那么任意一个前馈神经网络都可以通过误差反向传播(BP)做参数学习,BP本质是利用链式求导,使用梯度下降(GD)算法的最优化求解过程,而翻看前面第四章最优化原理,其求解就是给定目标函数,确定搜索步长和搜索方向的故事,GD的权重更新公式为(其中O为目标函数):


同样回看第四章,目标函数多种多样,比如常见的有:
SSE:

cross entropy:

但不管哪种目标函数,一般总可以分为线性部分(变量的线性组合)和非线性部分(激活函数),显然求导过程中线性部分最简单,非线性部分最复杂,所以上述权重更新公式可以拆解为:

显然:很容易计算,而比较难计算,定义:
为每个节点的误差向量,那么整个权重的更新核心考量就是怎么计算和传播

基于以上推导得到:

于是误差反向传播公式变为:


其中:是在时刻的任何一个隐层节点,是在时刻的任何一个隐层节点,高层的可以通过循环递归的计算出来,所有计算完毕后累加求和并应用在的权重更新中。

6.1.3 代码实践

问题描述:给定一个字符,生成(预测)之后的n个字符,并使得整个句子看上去有语义含义。
1、训练数据生成如下图:


2、过程说明如下图:


  1. # -*- coding:utf-8 -*-
  2. import numpy as np
  3. import os
  4. import pickle
  5. class RnnModeling:
  6. bert_len = 768 # length of bert nector.
  7. txt_data_size = 0 # text data size of char level.
  8. iteration = 1000 # iteration of training.
  9. sequence_length = 5 # window of text context.
  10. batch_size = 0 # training batch.
  11. input_size = 0 # size of input layer.
  12. hidden_size = 100 # size of hidden layer.
  13. output_size = 0 # size of output layer.
  14. learning_rate = 0.001 # learning rate of optimization algorithm.
  15. bert_path = "" # the path of bert model,you can download it through https://storage.googleapis.com/bert_models/2018_11_03/chinese_L-12_H-768_A-12.zip.
  16. word2vec_path = "" # the path of word2vec model,you can download the pre-train model from internet.
  17. chars_set = [] # all chars in text data.
  18. check_point_dir = "" # path of check point.
  19. char_to_int = {} # char level encoding, char->int
  20. int_to_char = {} # char level decoding, int->char
  21. char_encoded = {} # one hot encoding
  22. V = [] # weight matrix: from input to hidden.
  23. U = [] # weight matrix: from hidden to hidden.
  24. W = [] # weight matrix: from hidden to output.
  25. b_h = [] # bias vector of hidden layer.
  26. b_y = [] # bias vector of output layer.
  27. h_prev = [] # previous hidden state.
  28. def __init__(self):
  29. pass
  30. def set_fine_tuning_path(self, bert_path="", word2vec_path="", check_point_dir=""):
  31. self.bert_path = bert_path
  32. self.word2vec_path = word2vec_path
  33. self.check_point_dir = check_point_dir
  34. if word2vec_path != "" and not os.path.exists(word2vec_path):
  35. print("[Error] the path of word2vec is not exists.")
  36. exit(0)
  37. if bert_path != "" and not os.path.exists(bert_path):
  38. print("[Error] the path of bert is not exists.")
  39. exit(0)
  40. if check_point_dir != "" and not os.path.exists(check_point_dir):
  41. print("[Info] the path of check point is not exists, we'll create make it.")
  42. os.makedirs(check_point_dir)
  43. def copy_model(self, rnn):
  44. self.bert_len = rnn.bert_len
  45. self.txt_data_size = rnn.bert_len
  46. self.iteration = rnn.iteration
  47. self.sequence_length = rnn.sequence_length
  48. self.batch_size = rnn.batch_size
  49. self.input_size = rnn.input_size
  50. self.hidden_size = rnn.hidden_size
  51. self.output_size = rnn.output_size
  52. self.learning_rate = rnn.learning_rate
  53. self.chars_set = rnn.chars_set
  54. self.char_to_int = rnn.char_to_int
  55. self.int_to_char = rnn.int_to_char
  56. self.char_encoded = rnn.char_encoded
  57. self.V = rnn.V
  58. self.U = rnn.U
  59. self.W = rnn.W
  60. self.b_h = rnn.b_h
  61. self.b_y = rnn.b_y
  62. self.h_prev = rnn.h_prev
  63. def training_data_analysis(self, txt_data, mode):
  64. self.txt_data_size = len(txt_data)
  65. chars = list(set(txt_data))
  66. self.output_size = len(chars)
  67. self.char_to_int = dict((c, i) for i, c in enumerate(chars))
  68. self.int_to_char = dict((i, c) for i, c in enumerate(chars))
  69. if mode == "one-hot":
  70. self.input_size = len(chars)
  71. for i, c in enumerate(chars):
  72. letter = [0 for _ in range(len(chars))]
  73. letter[self.char_to_int[c]] = 1
  74. self.char_encoded[c] = np.array(letter)
  75. elif mode == "bert":
  76. self.input_size = self.bert_len
  77. from bert_serving.client import BertClient
  78. bc = BertClient(timeout=1000)
  79. # start server: bert-serving-start -model_dir=D:\Github\bert\chinese_L-12_H-768_A-12 -num_worker=1
  80. for i, c in enumerate(chars):
  81. if not c.strip():
  82. self.char_encoded[c] = np.array(bc.encode(['<S>']))
  83. else:
  84. self.char_encoded[c] = np.array(bc.encode([c]))
  85. if i % 100 == 0:
  86. print('[Debug] bert vector length: %d' % (len(self.char_encoded)))
  87. elif mode == "w2v":
  88. import gensim
  89. model = gensim.models.KeyedVectors.load_word2vec_format(self.word2vec_path, binary=False)
  90. self.input_size = model.vector_size
  91. for i, c in enumerate(chars):
  92. if model.__contains__(c):
  93. self.char_encoded[c] = np.array(model[c])
  94. else:
  95. self.char_encoded[c] = np.zeros((self.input_size, 1))
  96. else:
  97. print("[Error] mode type error. it should be one-hot or bert or w2v.")
  98. exit(0)
  99. def model_building(self, itr, seq_len, lr, h_size):
  100. self.iteration = itr
  101. self.sequence_length = seq_len
  102. self.learning_rate = lr
  103. self.batch_size = round((self.txt_data_size / self.sequence_length) + 0.5)
  104. self.hidden_size = h_size
  105. self.V = np.random.randn(self.hidden_size, self.input_size) * 0.01 # weight input -> hidden.
  106. self.U = np.random.randn(self.hidden_size, self.hidden_size) * 0.01 # weight hidden -> hidden
  107. self.W = np.random.randn(self.output_size, self.hidden_size) * 0.01 # weight hidden -> output
  108. self.b_h = np.zeros((self.hidden_size, 1))
  109. self.b_y = np.zeros((self.output_size, 1))
  110. self.h_prev = np.zeros((self.hidden_size, 1))
  111. def forwardprop(self, labeling, inputs, h_prev):
  112. x, s, y, p = {}, {}, {}, {}
  113. s[-1] = np.copy(h_prev)
  114. loss = 0
  115. for t in range(len(inputs)): # t is a "time step".
  116. x[t] = self.char_encoded[inputs[t]].reshape(-1, 1) # input vector.
  117. s[t] = np.tanh(np.dot(self.V, x[t]) + np.dot(self.U, s[t - 1]) + self.b_h) # hidden state. f(x(t)*V + s(t-1)*U + b), f=tanh.
  118. y[t] = np.dot(self.W, s[t]) + self.b_y # f(s(t)*W + b), f=x.
  119. p[t] = np.exp(y[t]) / np.sum(np.exp(y[t])) # softmax. f(x)=exp(x)/sum(exp(x))
  120. loss += -np.log(p[t][self.char_to_int[labeling[t]]]) # cross-entropy loss.
  121. return loss, p, s, x
  122. def backprop(self, p, labeling, inputs, s, x):
  123. dV, dU, dW = np.zeros_like(self.V), np.zeros_like(self.U), np.zeros_like(self.W) # make all zero matrices.
  124. dbh, dby = np.zeros_like(self.b_h), np.zeros_like(self.b_y)
  125. delta_pj_1 = np.zeros_like(s[0])
  126. # error reversed
  127. for t in reversed(range(len(inputs))):
  128. dy = np.copy(p[t]) # "dy" means "δpk"
  129. dy[self.char_to_int[labeling[t]]] -= 1 # when using cross entropy loss, δpk=d-y.
  130. dW += np.dot(dy, s[t].T) # dw/η=δpk * s(t)
  131. dby += dy
  132. delta_pk_w = np.dot(self.W.T, dy) + delta_pj_1 # δpk * w.
  133. delta_pj = (1 - s[t] * s[t]) * delta_pk_w # δpj = δpk * w * f'(x); f(x)=tanh; f'(x)= tanh'(x) = 1-tanh^2(x)
  134. dbh += delta_pj
  135. dV += np.dot(delta_pj, x[t].T) # dv/η = δpj * x(t)
  136. dU += np.dot(delta_pj, s[t - 1].T) # du/η = δpj * s(t-1)
  137. delta_pj_1 = np.dot(self.U.T, delta_pj) # δpj (t-1) = δpj * u
  138. for dparam in [dV, dU, dW, dbh, dby]:
  139. np.clip(dparam, -1, 1, out=dparam)
  140. return dV, dU, dW, dbh, dby
  141. def model_reading(self, model_read_path):
  142. if not os.path.exists(model_read_path):
  143. print("[Error] the model path %s is not exists." % model_read_path)
  144. exit(0)
  145. f = open(model_read_path, 'rb')
  146. rnn = pickle.load(f)
  147. ce = pickle.load(f)
  148. rnn.char_encoded = ce
  149. f.close()
  150. self.copy_model(rnn)
  151. def model_saving(self, model_save_path):
  152. if not os.path.exists(model_save_path):
  153. os.system(r"touch {}".format(model_save_path))
  154. f = open(model_save_path, 'wb')
  155. pickle.dump(self, f, protocol=-1)
  156. pickle.dump(self.char_encoded, f, protocol=-1)
  157. f.close()
  158. def model_training(self, txt_data, ischeck=False):
  159. chk_path = self.check_point_dir + "/final.p"
  160. if ischeck and os.path.exists(chk_path):
  161. rnn.model_reading(chk_path)
  162. mV, mU, mW = np.zeros_like(self.V), np.zeros_like(self.U), np.zeros_like(self.W)
  163. mbh, mby = np.zeros_like(self.b_h), np.zeros_like(self.b_y)
  164. loss = 0.0
  165. for i in range(self.iteration):
  166. self.h_prev = np.zeros((self.hidden_size, 1))
  167. data_pointer = 0
  168. for b in range(self.batch_size):
  169. inputs = [ch for ch in txt_data[data_pointer:data_pointer + self.sequence_length]]
  170. targets = [ch for ch in txt_data[data_pointer + 1:data_pointer + self.sequence_length + 1]]
  171. if (data_pointer + self.sequence_length + 1 >= len(txt_data) and b == self.batch_size - 1):
  172. targets.append(' ')
  173. loss, ps, hs, xs = self.forwardprop(targets, inputs, self.h_prev)
  174. dV, dU, dW, dbh, dby = self.backprop(ps, targets, inputs, hs, xs)
  175. for weight, g, his in zip([self.V, self.U, self.W, self.b_h, self.b_y],
  176. [dV, dU, dW, dbh, dby],
  177. [mV, mU, mW, mbh, mby]):
  178. his += g * g # RMSProp updata
  179. e = 0.5 * his / (i + 1) + 0.5 * g * g # RMSProp updata
  180. weight += -self.learning_rate * g / np.sqrt(e + 1e-8) # RMSProp update
  181. data_pointer += self.sequence_length
  182. if i % 100 == 0:
  183. print('[Debug] iteration %d, loss value: %f' % (i, loss))
  184. if ischeck:
  185. self.model_saving(self.check_point_dir+"/chk"+str(i)+".p")
  186. self.model_saving(chk_path)
  187. self.model_saving(chk_path)
  188. def model_inference(self, test_char, length):
  189. x = self.char_encoded[test_char].reshape(-1, 1)
  190. idx = []
  191. h = np.zeros((self.hidden_size,1))
  192. for t in range(length):
  193. h = np.tanh(np.dot(self.V, x) + np.dot(self.U, h) + self.b_h)
  194. y = np.dot(self.W, h) + self. b_y
  195. p = np.exp(y) / np.sum(np.exp(y))
  196. ix = list(p).index(max(list(p)))
  197. x = self.char_encoded[self.int_to_char[ix]].reshape(-1, 1)
  198. idx.append(ix)
  199. txt = ''.join(self.int_to_char[i] for i in idx)
  200. print ('[Debug] %s-%s' % (test_char, txt))
  201. def run(self, txt_data, bert_path="", word2vec_path="", check_point_path="", ischeck=True, model_type="bert", \
  202. itr=1000, seq_len=10, lr=0.001, h_size=100):
  203. self.set_fine_tuning_path(bert_path, word2vec_path, check_point_path)
  204. if not ischeck:
  205. self.training_data_analysis(txt_data, model_type)
  206. self.model_building(itr, seq_len, lr, h_size)
  207. self.model_training(txt_data, ischeck)
  208. if __name__ == "__main__":
  209. txt_data = "当地时间6月17日,第53届巴黎-布尔歇国际航空航天展览会(即巴黎航展)开幕。 开幕当天,法国总统马克龙亲自为法国、德国与西班牙三国联合研制的“新一代战斗机”(NGF)的全尺寸模型揭幕。法、德、西三国防长也出席了模型揭幕仪式,并在仪式后签署了三方合作协议,正式欢迎西班牙加入“新一代战机”的联合研制。NGF与美国的F-22、F-35、俄罗斯的苏-57以及中国的歼-20一样,同属第五代战斗机。"
  210. rnn = RnnModeling()
  211. rnn.set_fine_tuning_path(check_point_dir="e://", word2vec_path='E:\\BaiduNetdiskDownload\\zhwiki\\zhwiki_2017_03.sg_50d.word2vec')
  212. rnn.run(txt_data, ischeck=False, check_point_path="e://")
  213. rnn.model_inference('法', 10)
  214. rnn.model_inference('巴', 10)
  215. rnn.model_inference('歼', 10)
  216. rnn.model_inference('新', 10)

训练及测试结果:

[Debug] iteration 0, loss value: 28.059744
[Debug] iteration 100, loss value: 2.966153
[Debug] iteration 200, loss value: 1.247683
[Debug] iteration 300, loss value: 0.931227
[Debug] iteration 400, loss value: 0.848960
[Debug] iteration 500, loss value: 0.812240
[Debug] iteration 600, loss value: 0.791726
[Debug] iteration 700, loss value: 0.779109
[Debug] iteration 800, loss value: 0.770437
[Debug] iteration 900, loss value: 0.764572
[Debug] 法-国、德国与美国的F-
[Debug] 巴-黎航展)开幕。 开幕
[Debug] 歼--20一样,同属第五
[Debug] 新-一代战斗机”(NGF

6.2 混沌理论

关于混沌(Chaos)一词,西方和东方在哲学认知和神话传说上惊人的相似。例如古希腊神话中描述的:万物之初,先有混沌,是一个无边无际、空空如也的空间,在发生某种扰动后,诞生了大地之母Gaea等等,世界从此开始。中国古代神话中,天地未开之前,宇宙以混沌状模糊一团,盘古开天辟地后世界从此开始。
而在现代自然科学中,混沌理论的发展反映了人们对客观世界认知一步步演化的过程。人类对自然规律的认知,也从确定性(Deterministic)认知主导逐步演进到概率性(Probabilistic)认知主导。

下面从理论方面做一些简单介绍,帮助理解未来我们会用到的一些概念。

6.2.1 一维映射

1、动态系统(Dynamical System)
一个动态系统由一组可能的状态组成,再加上一个用过去的状态来确定现在的状态的规则。最典型的动态系统是时间离散动态系统(discrete-time dynamical system)和时间连续动态系统(m continuous-time dynamical system),前面我们介绍的RNN就是一种离散动态系统。
很多现实当中问题往往是随着时间演化的动态系统,例如:模拟细菌生长过程,在给定初始细菌数后,随着时间流逝,细菌数量增长的模型如下:


表示初始细菌数,表示随时间演化,显然这个增长过程是以指数增长的。
2、固点(Fixed Points)
如果动态系统有映射,且满足,则被称为固点。还以上面细菌生长过程为例,几何意义如下图表示:


利用直线发现动态系统的固点只有x=0这一点,画出动态系统的演化轨迹(虚线部分),随着时间流逝,细菌种群规模趋向于正无穷。
但真实情况是,受限于环境、资源等因素,细菌种群规模不可能无限大,所以修改动态系统为:


几何意义如下图表示:


利用直线发现动态系统的固点有x=0和x=0.5这两个点,画出动态系统的演化轨迹(虚线),随着时间流逝,不管初始种群取多少,细菌种群规模最终趋向于0.5(被吸引到0.5),用R做个简单模拟:

  1. g <- function(x){
  2. return(2*x*(1-x))
  3. }
  4. gk <- function(k, x){
  5. for(i in 1:k){
  6. x = g(x)
  7. print(x)
  8. }
  9. }
  10. k=10
  11. for (i in 1:10){
  12. x=runif(1)
  13. gk(k, x)
  14. print("=====")
  15. }

部分结果如下:

t y(x=0.941631) y(x=0.6455615 ) y(x=0.1207315 )
1 0.1099241 0.4576237 0.2123107
2 0.1956815 0.4964085 0.3344698
3 0.3147805 0.4999742 0.4451995
4 0.4313875 0.5 0.4939938
5 0.4905846 0.5 0.4999279
6 0.4998227 0.5 0.5
7 0.4999999 0.5 0.5
8 0.5 0.5 0.5
9 0.5 0.5 0.5
10 0.5 0.5 0.5

3、稳定的固点(Stability of Fixed Points)
假设动态系统的映射为,几何形态如下:

利用直线发现动态系统的固点有这三个点,画出动态系统的演化轨迹(虚线),其中两个点被称为稳定固点,在两个值的某个邻域内,y会分别收敛于1和-1两个值,为不稳定固点,在它的+邻域内y会被推到上半区,-邻域内y会被推到下半区。

4、吸引固点(Sink)与排斥固点(Source)
首先对点定义它的邻域:


其次,假设动态系统有映射,点为实数值,且满足,如果存在的邻域,使得所有邻域内的点会被吸引到点,即:

则点被称作Sink,反之如果邻域内的点会被排斥远离点,则点被称作Source。
数学化表示如下,记住这个表示,未来解释为什么RNN无法利用梯度下降学到长依赖关系时会用到:

如果是一个在实数集上的平滑映射,假设的固点,则:
1、如果,则是吸引固点Sink;
2、如果,则是排斥固点Source。

证明
假设是介于和1之间的任意实数,对于:


存在一个的邻域,使得:

换句话说,相比更接近,也说明,如果,则,以此类推,也满足此性质,归纳下变成:

所以是一个吸引固点Sink。
换一个角度,从一阶泰勒展开式或导数的定义来看:
点的一阶泰勒展开式:

1、如果,则是吸引固点Sink;
2、如果,则是排斥固点Source。

5、k周期点
举一个例子:,其图形如下:

利用直线发现动态系统的固点有两个点,而这两个点都是排斥固点,那么吸引固点去哪儿了呢?做一个简单模拟:

  1. g <- function(x){
  2. return(3.3*x*(1-x))
  3. }
  4. gk <- function(k, x){
  5. for(i in 1:k){
  6. x = g(x)
  7. print(x)
  8. }
  9. }
  10. k=20
  11. for (i in 1:10){
  12. x=runif(1)
  13. gk(k, x)
  14. print("=====")
  15. }

部分结果如下:

t y(x=0.1156445) y(x=0.3317354) y(x=0.0.9131461)
1 0.3374938 0.7315672 0.2617241
2 0.7378527 0.6480429 0.6376411
3 0.6383061 0.7526749 0.7624812
4 0.7618757 0.6143128 0.5976419
5 0.5986897 0.7818775 0.793538
6 0.7928592 0.5627987 0.540657
7 0.5419706 0.8119858 0.8195451
8 0.8191869 0.5037939 0.48804
9 0.488795 0.8249525 0.824528
10 0.8245857 0.4765394 0.4774493
11 0.4773257 0.8231837 0.8233218
12 0.8233034 0.4803226 0.4800279
13 0.4800672 0.8237222 0.8236837
14 0.8236889 0.4791729 0.4792553
15 0.4792442 0.8235686 0.8235799
16 0.8235784 0.4795012 0.479477
17 0.4794803 0.8236133 0.8236101
18 0.8236105 0.4794056 0.4794125
19 0.4794116 0.8236004 0.8236013
20 0.8236012 0.4794332 0.4794312

一个有意思的现象出现,交替出现且为吸引固点,换个角度就是:,,也就是吸引固点以2为周期出现,在两个点间循环往复。
形式化定义为:
假设动态系统有实数集上的映射,点为实数值,且满足为正整数,则被称为k周期点。
扩展下之前吸引固点的定义到k周期点:

如果是一个在实数集上的平滑映射,假设构成了周期点,则:
1、如果,则是吸引固点Sink;
2、如果,则是排斥固点Source。

还是上面的那个例子:

则有:

k周期点为:
因为,所以它是吸引固点。

6.2.2 二维映射

把一维映射扩展到多维映射,看看会出现什么有趣的现象,由于二维映射是多维映射的最简单形式且各种性质与多维映射一致,固以此为基础讨论。
1、邻域
扩展一维映射时的邻域概念如下:
在欧式空间实数域下,向量的范数定义为:


定义它的邻域:

有时候也叫-,举个例子。
二维下():

三维下():

2、固点
其次,假设动态系统有实数域的映射为实数域固点,即满足,如果存在的邻域,使得所有邻域内会被吸引到,即:


被称作Sink,反之如果邻域内的点会被排斥远离点,则点被称作Source
在二维映射下还会出现一个一维映射时不会出现的固点,叫做鞍点(Saddle),可以把它看做介于吸引固点和排斥固点间的一种状态,它拥有至少一个吸引方向和至少一个排斥方向。

图中代表映射,代表点的邻域。图代表是一个吸引固点,进入其邻域的点会被吸引到点、图代表是一个排斥固点,进入其邻域的点会被排斥而远离点、图代表是一个鞍点,进入其邻域的点先会被吸引到点,然后会被排斥而远离点。来个更直观的图:

在《最优化原理-梯度下降》这一章我们曾经介绍过常用的一阶最优化方法,给定初始值后,不同的优化方法的优化轨迹不一样,但大的方向都是先被迭代吸引到鞍点,然后再从鞍点被排斥走,而因为待优化问题往往有很多局部最优点,所以我们希望优化算法能尽可能跳出当前点去寻找更优的局部最优点。
综上所述,显然排斥固点Source和鞍点Saddle的最大特点是:它们都是固点,都不是稳定固点,因为它们对初始条件很敏感,但对研究一个动态系统它们很重要。

6.2.3 线性映射

1、线性映射
所谓线性映射是指:
给定实数及实数向量,有的映射满足:


显然原点(0,0)是所有线性映射的固点,且是稳定的,如果它邻域内的点在迭代映射时都趋向于接近固点,则该固点是一个吸引子,稍微正式点的定义如下:

在一个随时间演变的动态系统中,吸引子是一个代表某种稳定状态的数值集合,在给定动态系统初始状态后,系统有着朝该集合所表示的稳态演化的趋势,在吸引子的某个邻域(basin of attraction)范围内,即使系统受到扰动,也会趋向于该稳态。

后面会大量出现吸引子这个概念。
2、鞍点
如果实数和实数向量满足:


则它们分别被称为A的特征值和特征向量。
假设有以下向量关系:

则有递推关系:

上的映射为例:

表示成矩阵形式:

以上过程迭代了次后得到:

这里就有意思了,迭代了次后,把它映射在一个二维平面上,看上去应该是个椭圆形,其中横坐标长度为,纵坐标为,对于原点的某个邻域同样也是个椭圆,横纵坐标长度分别为,假设,则会有三种情况:
1、如果,则整个椭圆会收缩到原点(0,0),原点是Sink;
2、如果,则整个椭圆会无限过大并远离原点(0,0),原点是Source;
3、如果,则整个椭圆的横坐标会无限扩大,而纵坐标会收缩到0,此时原点既不是Sink也不是Source,人们把它叫做Saddle(鞍点)。

假设取:,则:


经过次迭代后,得到下图:


3、双曲(hyperbolic)
假设A是实数域矩阵,基于它定义了的线性映射,则:

如果的所有特征值的绝对值都小于1,则原点是一个吸引固点Sink;
如果的所有特征值的绝对值都大于1,则原点是一个排斥固点Source;
如果的所有特征值中至少有一个其绝对值大于1,且最少有一个其绝对值小于1,则原点是一个鞍点Saddle。

如果一个映射,没有一个特征值的绝对值等于1,则我们把叫做是双曲的,显然有三类双曲映射:Sink、Source、Saddle。

6.2.4 非线性映射

真实世界中,非线性系统远远多于线性系统,而当非线性程度足够高时,系统将出现混沌状态,不过从概念和定义上与线性映射区别不大。前面说的吸引固点和k周期吸引固点都是运动状态可预测的,它们被叫做平庸吸引子,而运动状态不可预测的叫做奇异吸引子(Strange Attractor)。
同样利用泰勒展开式,在非线性高维空间,导数被扩展为雅克比矩阵(Jacobian matrix):


其中:
1、上的映射,
2、雅可比矩阵为:

假设为固点,满足,则:

即,在点邻域内对其做一个微小扰动,输出会有的变化,显然类似线性映射,可以有下面结论:
假设:上的映射,且,满足,则:
1、如果没有取值为1的特征值,则被称作双曲(hyperbolic)的,这个词很重要,会在后面多次出现,直观的也挺好理解,1的多少次方都还是1,只有大于1或小于1才会在某个方向上要么吸引要么排斥;
2、如果的每个特征值的绝对值都小于1,那么是一个吸引固点Sink,也有人叫做双曲吸引子(hyperbolic attractor)
3、如果的每个特征值的绝对值都大于1,那么是一个排斥固点Source;
4、如果是双曲的,至少有一个特征值的绝对值大于1且至少有一个特征值的绝对值小于1,则是一个鞍点Saddle。
举个例子
有非线性映射:

因为,则有
所以有两个固点:
其雅克比矩阵为:

于是:

特征值为:


特征值为:,显然,为双曲吸引子,为鞍点。

6.2.5 混沌的演化及结构

用一个简单的抛物线做说明:

将其转化为迭代形式(一般来说,越复杂的非线性方程越无解析解,常常用数值计算中的迭代方法得到解):


程序模拟迭代过程:

  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. # 抛物线函数
  4. def parabola(r, x):
  5. return 1 - r * x**2
  6. def plot_bifu(iterations, r, x0, last):
  7. ax = plt.subplot(111)
  8. x = x0
  9. for i in range(iterations):
  10. x = parabola(r, x)
  11. if i >= (iterations - last):
  12. ax.plot(r, x, ',k', alpha=.25)
  13. ax.set_xlim(0, 2)
  14. ax.set_title("Bifurcation: y=1-rx^2")
  15. plt.show()
  16. def main():
  17. n = 10000
  18. r = np.linspace(0, 2.0, n)
  19. iterations = 1000 # 迭代次数
  20. last = 200 # 输出最后若干次迭代
  21. x0 = 0.1 * np.ones(n) # 初始点
  22. plot_bifu(iterations, r, x0, last)

其分叉图如下,结构上按照指数级周期性分裂,当时,系统进入混沌状态:

对下图红框部分放大看,可以发现一个有趣的东西:

放大的部分其结构与开始时的整体结构相同,一般叫做分形,于是在混沌中再次出现周期性:

随着复杂度的提升,系统经历了:稳定态->周期态->类周期态->混沌态。
还可以再次放大类似的红框区域,但会发现一个普适的规律:


其中是发生混沌现象时的分界点。上面这个常数叫做Feigenbaum常数,它可能是比圆周率更神秘的常数,我没有做更深入的了解,详情可参见论文《Quantitative universality for a class of nonlinear transformations》,换句话说,混沌演化的过程中存在内部规律性,且这种演化过程存在某种“普适性”。

6.2.6 RNN长依赖学习问题

这一节主要基于对Yoshua Bengio《Learning Long-Term Dependencies with Gradient Descent is Difficult》一文的学习,个人认为它是少有的对长依赖学习做出精彩理论研究和证明的文章。
文章从实验和理论角度证明了:梯度下降算法无法有效学习长依赖(模型在时间t的输出依赖更早时间时的系统状态)。
一个能学习长依赖的动态系统,至少应该满足以下几个条件:

1、系统能够存储任意时长的信息;
2、系统鲁棒性强,即使对系统输入做随机波动也不影响系统做出正确输出;
3、系统参数可在合理有限的时间内学习到。

6.3 LSTM

上面两节从原理角度说明了RNN为什么很难学到长依赖,而本节的LSTM是一个伟大的和具有里程碑的模型,最著名的论文是Sepp Hochreiter 与 Jurgen Schmidhuber的《Long Short-Term Memory 》(没错,就是那位怼天怼地怼各种权威的Schmidhuber),从原理上分析解决了RNN学习长依赖中的梯度爆炸(blow up)和梯度消失(vanish)问题,大部分文章只介绍了LSTM的结构,我希望通过本文能抛砖引玉,了解作者为什么这么设计结构。

6.3.1 基本原理

回忆6.1节末的RNN任意两层隐藏层


其中:是在时刻的任何一个隐层节点,是在时刻的任何一个隐层节点,高层的可以通过循环递归的计算出来,所有计算完毕后累加求和并应用在的权重更新中。
误差从时刻的隐藏层节点经过任意步往时刻的隐藏层节点做反向传播的传播速度如下:

把上面式子完全展开后得到:

大家会发现整个误差反向传播速度是由决定的:
1、如果,则连乘的结果会随着的增加呈指数形式增大,误差反向传播出现梯度爆炸;
2、如果,则连乘的结果会随着的增加呈指数形式减小,误差反向传播出现梯度消失。
假设以最简单的RNN为例,即:

时刻的反向误差传播为:


其中:。要想不出现梯度爆炸或消失,只能满足:

对上式积分下,得到:

这意味着,函数必须是线性的,显然,当时,有恒等映射函数,上述关系也叫constant error carrousel(CEC),CEC在LSTM的结构设计中举足轻重。
以输入权重为例,由于实际场景中除了自连接节点外,还会有其他输入节点,为简单起见,我们只关注一个额外的输入权重 。假设通过响应某个输入而开启神经网络单元,并为了减少总误差,希望它能被长时间激活。显然,对同一个输入权重一方面要存储某些输入范式,一方面又要忽略其他输入范式,而涉及节点的函数(上面的CEC)又是线性的,所以对而言,这些信号会试图让它既得通过开启单元对输入做存储又需要防止单元被其他输入关闭,这种情况使得学习变得困难。

对于输出权重,也存在类似的输出冲突,这里就不在赘述。
为了解决上面的输入和输出冲突,LSTM抽象了1个记忆单元(Memory Cel l)、设计了1个基础结构——遗忘门(Forget Gate)和2个组合结构——输入门(Input Gate)和输出门(Output Gate)来解决冲突。
1、记忆单元是对包含CEC线性单元的抽象,如下图(以RNN作为对比),包含当前时刻输入、上个隐层节点的状态、当前时刻输出、当前时刻隐层节点状态。:

RNN

LSTM

1、遗忘门的结构如下图:它将上一个隐层的状态和当前输入合并后送入logistic函数,由于该函数输出为0~1之间,输出接近1的被保留,接近0的被丢掉,也就是说,遗忘门决定了哪些历史信息要被保留

Forget Gate

2、输入门的结构如下图:它将上一个隐层的状态和当前输入合并后送入Logistic函数,输出介于0~1之间,同样的,0表示信息不重要,1表示信息重要;同时,合并后的输入被送入Tanh函数,输出介于-1~1之间,Logistic的输出与Tanh的输出相乘后决定哪些Tanh的输出信息需要保留,哪些要丢掉,也就是说,输入门决定了哪些新的信息要被加进来

Input Gate

前一个记忆单元的输出与遗忘门输出相乘后,可以选择性忘记不重要的信息,之后与输入门的结果相加,把新的输入信息纳入进来,最终得到当前记忆单元的输出,比较好解决了输入冲突,如下图:

Cell Output

3、输出门的结构如下图:它主要解决隐藏层状态的输出冲突问题,它将上一个隐层的状态和当前输入合并后送入Logistic函数,输出介于0~1之间,然后与当前记忆单元的输出通过Tanh函数变换后的结果相乘,得到当前隐藏层的状态,也就是说,输出门决定了当前隐藏层要携带哪些历史信息,比较好解决了输出冲突。

Output Gate

以上图片来源于:《Understanding LSTM Networks》一文,非常不错的一篇LSTM入门文章。后续也有各种各样对经典LSTM的改进(如GRU),但整体上不如LSTM经典(截止2020.10.10在Google Scholar上查寻到该论文已经被引用了37851次,成为20世纪“最火论文”)。
除了比较完美解决了输入输出冲突外,LSTM的计算和存储复杂度并不高,权重更新计算的复杂度为O(W),即与权重总个数线性相关;存储方面也不像使用全流程BPTT的传统方法,要存储大量历史节点信息,LSTM只需要存储一定历史时间步的局部信息。

6.3.2 代码实践

本节以经典的《古诗词生成》为例子,介绍下LSTM的一种应用,以下例子只供娱乐使用。
问题描述如下:

给定五言绝句的首句,生成整首共4句的五言绝句。

例如,输入:“月暗竹亭幽,”,输出“月暗竹亭幽,碧昏时尽黄。园春歌雪光,云落分白草。”。
完整代码在:https://github.com/vivounicorn/LstmApp.git,其中,data文件夹里包含了训练好的word2vec模型和迭代了2k+次的模型,可以直接做fine-tune。

1、算法步骤

Step-1:爬取古诗词作为原始数据;
Step-2:清洗原始数据,去掉不符合五言绝句的诗词;
Step-3:准备训练数据和相应的标注;
Step-4:若使用word2vec生成的词向量,则需要生成相关模型;
Step-5:构建以LSTM层和全连接层为主的神经网络;
Step-6:训练和验证模型,并做应用。

2、实现详情

  1. def _build_base(self,
  2. file_path,
  3. vocab=None,
  4. word2idx=None,
  5. idx2word=None) -> None:
  6. """
  7. To scan the file and build vocabulary and so on.
  8. :param file_path: the file path of poetic corpus, one poem per line.
  9. :param vocab: the vocabulary.
  10. :param word2idx: the mapping of word to index.
  11. :param idx2word: the mapping of index to word
  12. :return: None.
  13. """
  14. # 去掉无关字符
  15. pattern = re.compile(u"_|\(|(|《")
  16. with open(file_path, "r", encoding='UTF-8') as f:
  17. for line in f:
  18. try:
  19. line = line.strip(u'\n')
  20. title, content = line.strip(SPACE).split(u':')
  21. content = content.replace(SPACE, u'')
  22. idx = re.search(pattern, content)
  23. if idx is not None:
  24. content = content[:idx.span()[0]]
  25. # 把指定长度的诗词选出来,如:五言绝句。
  26. if len(content) < self.embedding_input_length: # Filter data according to embedding input
  27. # length to improve accuracy.
  28. continue
  29. words = []
  30. for i in range(0, len(content)):
  31. word = content[i:i + 1]
  32. if (i + 1) % self.embedding_input_length == 0 and word not in [',', ',', ',', '.', '。']:
  33. words = []
  34. break
  35. words.append(word)
  36. self.all_words.append(word)
  37. if len(words) > 0:
  38. self.poetrys.append(words)
  39. except Exception as e:
  40. log.error(str(e))
  41. # 生成词汇表,保留出现频次top n的字
  42. if vocab is None:
  43. top_n = Counter(self.all_words).most_common(self.vocab_size - 1)
  44. top_n.append(SPACE)
  45. self.vocab = sorted(set([i[0] for i in top_n]))
  46. else:
  47. top_n = list(vocab)[:self.vocab_size - 1]
  48. top_n.append(SPACE)
  49. self.vocab = sorted(set([i for i in top_n])) # cut vocab with threshold.
  50. log.debug(self.vocab)
  51. # 生成“字”到“编号”的映射,把每个字做了唯一编号,“空格”也做编号
  52. if word2idx is None:
  53. self.word2idx = dict((c, i) for i, c in enumerate(self.vocab))
  54. else:
  55. self.word2idx = word2idx
  56. # 生成“编号”到“字”的映射
  57. if idx2word is None:
  58. self.idx2word = dict((i, c) for i, c in enumerate(self.vocab))
  59. else:
  60. self.idx2word = idx2word
  61. # Function of mapping word to index.
  62. # 以 “字” 查找 “编号”的函数,没在词汇表的“字”用“空格”的编号代替
  63. self.w2i = lambda word: self.word2idx.get(str(word)) if self.word2idx.get(word) is not None \
  64. else self.word2idx.get(SPACE)
  65. # Function of mapping index to word.
  66. # 以 “编号”查找 “字” 的函数,找不到的“字”用“空格”代替
  67. self.i2w = lambda idx: self.idx2word.get(int(idx)) if self.idx2word.get(int(idx)) is not None \
  68. else SPACE
  69. # Full vectors.
  70. # 把文本表示的诗词变成由“编号”表示的向量,如:“床前明月光,”变成[1,2,3,4,5,6]
  71. self.poetrys_vector = [list(map(self.w2i, poetry)) for poetry in self.poetrys]
  72. self._data_size = len(self.poetrys_vector)
  73. self._data_index = np.arange(self._data_size)
特征 标注
菩提本无树,
提本无树,明
本无树,明镜
无树,明镜亦
树,明镜亦非
,明镜亦非台

对每个字,支持两种编码方式:基于词汇表的one hot和基于语义distributed representation的word2vec。
1、One-hot

  1. def _one_hot_encoding(self, sample):
  2. """
  3. One-hot encoding for a sample, a sample will be split into multiple samples.
  4. :param sample: a sample. [1257, 6219, 3946]
  5. :return: feature and label. feature:[[0,0,0,1,0,0,......],
  6. [0,0,0,0,0,1,......],
  7. [1,0,0,0,0,0,......]];
  8. label: [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0......]
  9. """
  10. if type(sample) != list or 0 == len(sample):
  11. log.error("type or length of sample is invalid.")
  12. return None, None
  13. feature_samples = []
  14. label_samples = []
  15. idx = 0
  16. # embedding_input_length即为输入窗口长度,五言绝句为6,当然也可以取其他值,但会影响训练精度和时间。
  17. while idx < len(sample) - self.embedding_input_length:
  18. feature = sample[idx: idx + self.embedding_input_length]
  19. label = sample[idx + self.embedding_input_length]
  20. label_vector = np.zeros(
  21. shape=(1, self.vocab_size),
  22. dtype=np.float
  23. )
  24. # 序列的下一个字为标注
  25. label_vector[0, label] = 1.0
  26. feature_vector = np.zeros(
  27. shape=(1, self.embedding_input_length, self.vocab_size),
  28. dtype=np.float
  29. )
  30. # 根据词汇表,相应的编号赋值为1,其余都是0.
  31. for i, f in enumerate(feature):
  32. feature_vector[0, i, f] = 1.0
  33. idx += 1
  34. feature_samples.append(feature_vector)
  35. label_samples.append(label_vector)
  36. return feature_samples, label_samples

假设输入长度为6,词汇表维度为8000,则,对于一个样本有:
特征矩阵为:1×6*8000
标注向量为:1*8000

2、Word2vec

  1. def _word2vec_encoding(self, sample):
  2. """
  3. word2vec encoding for sample, a sample will be split into multiple samples.
  4. :param sample: a sample. [1257, 6219, 3946]
  5. :return: feature and label.feature:[[0.01,0.23,0.05,0.1,0.33,0.25,......],
  6. [0.23,0.45,0.66,0.32,0.11,1.03,......],
  7. [1.22,0.99,0.68,0.7,0.8,0.001,......]];
  8. label: [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0......]
  9. """
  10. if type(sample) != list or 0 == len(sample):
  11. log.error("type or length of sample is invalid.")
  12. return None, None
  13. feature_samples = []
  14. label_samples = []
  15. idx = 0
  16. while idx < len(sample) - self.embedding_input_length:
  17. feature = sample[idx: idx + self.embedding_input_length]
  18. label = sample[idx + self.embedding_input_length]
  19. if self.w2v_model is None:
  20. log.error("word2vec model is none.")
  21. return None, None
  22. label_vector = np.zeros(
  23. shape=(1, self.vocab_size),
  24. dtype=np.float
  25. )
  26. # 序列的下一个字为标注
  27. label_vector[0, label] = 1.0
  28. feature_vector = np.zeros(
  29. shape=(1, self.embedding_input_length, self.w2v_model.size),
  30. dtype=np.float
  31. )
  32. # 用训练好的word2vec模型获取相应“字”的语义向量
  33. for i in range(self.embedding_input_length):
  34. feature_vector[0, i] = self.w2v_model.get_vector(feature[i])
  35. idx += 1
  36. feature_samples.append(feature_vector)
  37. label_samples.append(label_vector)
  38. return feature_samples, label_samples

假设输入长度为6,词的语义向量维度为200,则,对于一个样本有:
特征矩阵为:1×6*200
标注向量为:1*8000

  1. def dump_data(self) -> None:
  2. """
  3. To dump: poetry's words list, poetry's words vectors, poetry's words vectors for training,
  4. poetry's words vectors for testing, poetry's words vectors for validation,
  5. poetry's words vocabulary, poetry's word to index mapping,poetry's index to word mapping.
  6. :return: None
  7. """
  8. org_filename = self.dump_dir + 'poetrys_words.dat'
  9. self._dump_list(org_filename, self.poetrys)
  10. vec_filename = self.dump_dir + 'poetrys_words_vector.dat'
  11. self._dump_list(vec_filename, self.poetrys_vector)
  12. train_vec_filename = self.dump_dir + 'poetrys_words_train_vector.dat'
  13. self._dump_list(train_vec_filename, self.poetrys_vector_train)
  14. valid_vec_filename = self.dump_dir + 'poetrys_words_valid_vector.dat'
  15. self._dump_list(valid_vec_filename, self.poetrys_vector_valid)
  16. test_vec_filename = self.dump_dir + 'poetrys_words_test_vector.dat'
  17. self._dump_list(test_vec_filename, self.poetrys_vector_test)
  18. vocab_filename = self.dump_dir + 'poetrys_vocab.dat'
  19. self._dump_list(vocab_filename, list(self.vocab))
  20. w2i_filename = self.dump_dir + 'poetrys_word2index.dat'
  21. self._dump_dict(w2i_filename, self.word2idx)
  22. i2w_filename = self.dump_dir + 'poetrys_index2word.dat'
  23. self._dump_dict(i2w_filename, self.idx2word)
模型方面直接使用gensim包,定义如下,根据参数不同,可以训练得到基于CBOW或SkipGram的语义向量,我们这种规模下,本质上没有太大差别,我们这里使用SkipGram。
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. import gensim
  4. from gensim.models import Word2Vec
  5. from gensim.models.word2vec import LineSentence
  6. import multiprocessing
  7. import numpy as np
  8. from src.config import Config
  9. from src.utils import Logger
  10. class Word2vecModel(object):
  11. """
  12. Word2vec model class.
  13. """
  14. def __init__(self,
  15. cfg_path='/home/zhanglei/Gitlab/LstmApp/config/cfg.ini',
  16. is_ns=False):
  17. """
  18. To initialize model.
  19. :param cfg_path: he path of configration file.
  20. :param model_type:
  21. """
  22. cfg = Config(cfg_path)
  23. global log
  24. log = Logger(cfg.model_log_path())
  25. self.model = None
  26. self.is_ns = is_ns
  27. self.vec_out = cfg.vec_out()
  28. self.corpus_file = cfg.corpus_file()
  29. self.window = cfg.window()
  30. self.size = cfg.size()
  31. self.sg = cfg.sg()
  32. self.hs = cfg.hs()
  33. self.negative = cfg.negative()
  34. def train_vec(self) -> None:
  35. """
  36. To train a word2vec model.
  37. :return: None
  38. """
  39. output_model = self.vec_out + 'w2v_size{0}_sg{1}_hs{2}_ns{3}.model'.format(self.size,
  40. self.sg,
  41. self.hs,
  42. self.negative)
  43. output_vector = self.vec_out + 'w2v_size{0}_sg{1}_hs{2}_ns{3}.vector'.format(self.size,
  44. self.sg,
  45. self.hs,
  46. self.negative)
  47. # 是否做负采样
  48. if not self.is_ns:
  49. self.model = Word2Vec(LineSentence(self.corpus_file),
  50. size=self.size,
  51. window=self.window,
  52. sg=self.sg,
  53. hs=self.hs,
  54. workers=multiprocessing.cpu_count())
  55. else:
  56. self.model = Word2Vec(LineSentence(self.corpus_file),
  57. size=self.size,
  58. window=self.window,
  59. sg=self.sg,
  60. hs=self.hs,
  61. negative=self.negative,
  62. workers=multiprocessing.cpu_count())
  63. self.model.save(output_model)
  64. self.model.wv.save_word2vec_format(output_vector, binary=False)
  65. def load(self, path):
  66. """
  67. To load a word2vec model.
  68. :param path: the model file path.
  69. :return: success True otherwise False.
  70. """
  71. try:
  72. self.model = gensim.models.Word2Vec.load(path)
  73. return True
  74. except:
  75. return False
  76. def most_similar(self, word):
  77. """
  78. Return the most similar words.
  79. :param word: a word.
  80. :return: similar word list.
  81. """
  82. word = self.model.most_similar(word)
  83. for text in word:
  84. log.info("word:{0} similar:{1}".format(text[0], text[1]))
  85. return word
  86. # 获取某个字 的语义向量
  87. def get_vector(self, word):
  88. """
  89. To get a word's vector.
  90. :param word: a word.
  91. :return: word's word2vec vector.
  92. """
  93. try:
  94. return self.model.wv.get_vector(str(word))
  95. except KeyError:
  96. return np.zeros(
  97. shape=(self.size,),
  98. dtype=np.float
  99. )
  100. # 也可以直接把keras的embedding层给拿出来,
  101. # 为了直观,我这里没有直接用它,如果要用,记着把语义向量的权重冻结下。
  102. def get_embedding_layer(self, train_embeddings=False):
  103. """
  104. To get keras embedding layer from model.
  105. :param train_embeddings: if frozen the layer.
  106. :return: embedding layer.
  107. """
  108. try:
  109. return self.model.wv.get_keras_embedding(train_embeddings)
  110. except KeyError:
  111. return None
  1. def _build(self,
  2. lstm_layers_num,
  3. dense_layers_num):
  4. """
  5. To build a lstm model with lstm layers and densse layers.
  6. :param lstm_layers_num: The number of lstm layers.
  7. :param dense_layers_num:The number of dense layers.
  8. :return: model.
  9. """
  10. units = 256
  11. model = Sequential()
  12. # 样本特征向量的维度,onehot为词汇表大小,word2vec为语义向量维度
  13. if self.mode == WORD2VEC:
  14. dim = self.data_sets.w2v_model.size
  15. elif self.mode == ONE_HOT:
  16. dim = self.vocab_size
  17. else:
  18. raise ValueError("mode must be word2vec or one-hot.")
  19. # embedding_input_length为输入序列窗口大小,如:五言绝句取为6
  20. model.add(Input(shape=(self.embedding_input_length, dim)))
  21. # 可以加多个LSTM层提取序列特征,这里会把之前每隔时刻的隐层都输出出来
  22. for i in range(lstm_layers_num - 1):
  23. model.add(LSTM(units=units * (i + 1),
  24. return_sequences=True))
  25. model.add(Dropout(0.6))
  26. # 注意这里我只要最后一个隐层的输出
  27. model.add(LSTM(units=units * lstm_layers_num,
  28. return_sequences=False))
  29. model.add(Dropout(0.6))
  30. # 可以加多个稠密层,用于对之前提取出来特征的组合
  31. for i in range(dense_layers_num - 1):
  32. model.add(Dense(units=units * (i + 1)))
  33. model.add(Dropout(0.6))
  34. # 最后一层,用softmax做分类
  35. model.add(Dense(units=self.vocab_size,
  36. activation='softmax'))
  37. # 使用交叉熵损失函数,优化器选择默认参数的adam(ps:随便选的,没做调参)
  38. model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
  39. model.summary()
  40. # 可视化输出模型结构
  41. plot_model(model, to_file='../model.png', show_shapes=True, expand_nested=True)
  42. self.model = model
  43. return model

例如:使用200维语义向量、输入长度6、词汇量8000、两层LSTM,一层Dense的模型结构如下:

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. from src.lstm_model import LstmModel
  4. from src.data_processing import PoetrysDataSet
  5. from src.word2vec import Word2vecModel
  6. def train_word2vec(base_data) -> None:
  7. w2v = Word2vecModel()
  8. w2v.train_vec()
  9. # test.
  10. a = w2v.most_similar(str(base_data.w2i('床')))
  11. for i in range(len(a)):
  12. print(base_data.i2w(a[i][0]), a[i][1])
  13. def train_lstm(base_data, finetune=None, mode='word2vec'):
  14. model = LstmModel(cfg_file_path, base_data, mode)
  15. # fine tune.
  16. if finetune is not None:
  17. model.load(finetune)
  18. model.train_batch(mode=mode)
  19. return model
  20. def test_lstm(base_data, sentence, model_path=None, mode='word2vec'):
  21. model = LstmModel(cfg_file_path, base_data, mode)
  22. if model_path is not None:
  23. model.load(model_path)
  24. return model.generate_poetry(sentence, mode=mode)
  25. if __name__ == '__main__':
  26. cfg_file_path = '/home/zhanglei/Gitlab/LstmApp/config/cfg.ini'
  27. w2vmodel_path = '/home/zhanglei/Gitlab/LstmApp/data/w2v_models/w2v_size200_sg1_hs0_ns3.model'
  28. model_path = '/home/zhanglei/Gitlab/LstmApp/data/models/model-2117.hdf5'
  29. base_data = PoetrysDataSet(cfg_file_path)
  30. train_word2vec(base_data)
  31. base_data.load_word2vec_model(w2vmodel_path)
  32. train_lstm(base_data=base_data, finetune=model_path)
  33. sentence = '惜彼落日暮,'
  34. print(test_lstm(base_data=base_data, sentence=sentence, model_path=model_path))

在model.log里会看到训练时的中间信息,如下,随着迭代次数变多,效果会越来越好,包括标点符号的规律也会学进去:

  1. [2020-11-05 12:58:48,723] - lstm_model.py [Line:127] - [DEBUG]-[thread:140045784893248]-[process:29513] - begin training
  2. [2020-11-05 12:58:48,723] - lstm_model.py [Line:132] - [DEBUG]-[thread:140045784893248]-[process:29513] - batch_size:32,steps_per_epoch:355,epochs:5000,validation_steps152
  3. [2020-11-05 12:59:10,260] - lstm_model.py [Line:194] - [INFO]-[thread:140045784893248]-[process:29513] - ==================Epoch 0, Loss 7.93123197555542=====================
  4. [2020-11-05 12:59:11,968] - lstm_model.py [Line:197] - [INFO]-[thread:140045784893248]-[process:29513] - 欲别牵郎衣,粳酗蓦釱北,鈒静槃遍衫。恸阳日搦蛆,
  5. [2020-11-05 12:59:12,816] - lstm_model.py [Line:197] - [INFO]-[thread:140045784893248]-[process:29513] - 金庭仙树枝,莨行查娇乂。具撅日霈韂,帝鸟 维。。
  6. [2020-11-05 12:59:13,659] - lstm_model.py [Line:197] - [INFO]-[thread:140045784893248]-[process:29513] - 素艳拥行舟,母 佶翕何,藁澡 。一 钺辗。,
  7. [2020-11-05 12:59:14,494] - lstm_model.py [Line:197] - [INFO]-[thread:140045784893248]-[process:29513] - 白鹭拳一足,芾 乡诏秩,启窑 展赢,酪溜劫騊
  8. [2020-11-05 12:59:15,385] - lstm_model.py [Line:197] - [INFO]-[thread:140045784893248]-[process:29513] - 恩酬期必报,闾瞢,颾钏。啾,。耴望,薖,州耒朿。
  9. [2020-11-05 12:59:16,277] - lstm_model.py [Line:197] - [INFO]-[thread:140045784893248]-[process:29513] - 君去方为宰,沈乡看一帷,柳跂 仁柳,营空长日韍。
  10. [2020-11-05 12:59:16,278] - lstm_model.py [Line:198] - [INFO]-[thread:140045784893248]-[process:29513] - ==================End=====================
  11. ......
  12. [2020-11-06 02:12:12,971] - lstm_model.py [Line:194] - [INFO]-[thread:140348029687616]-[process:31309] - ==================Epoch 2106, Loss 7.458868980407715=====================
  13. [2020-11-06 02:12:13,732] - lstm_model.py [Line:197] - [INFO]-[thread:140348029687616]-[process:31309] - 新开窗犹偏,回雨草花天。谁因家群应,人年功日未。
  14. [2020-11-06 02:12:14,498] - lstm_model.py [Line:197] - [INFO]-[thread:140348029687616]-[process:31309] - 此心非一事,白物郡期旧。相爱含将回,更相日见光。
  15. [2020-11-06 02:12:15,274] - lstm_model.py [Line:197] - [INFO]-[thread:140348029687616]-[process:31309] - 刻舟寻已化,有恨多两开。去闻难乱东,地中当如来。
  16. [2020-11-06 02:12:16,069] - lstm_model.py [Line:197] - [INFO]-[thread:140348029687616]-[process:31309] - 带水摘禾穗,鸟独光冥客。拂船不自已,远年必年非。
  17. [2020-11-06 02:12:16,846] - lstm_model.py [Line:197] - [INFO]-[thread:140348029687616]-[process:31309] - 茕茕孤思逼,此前路去地。如事别自以,闻阳近高酒。
  18. [2020-11-06 02:12:17,613] - lstm_model.py [Line:197] - [INFO]-[thread:140348029687616]-[process:31309] - 西陆蝉声唱,日衣烟东云。春出不饥家,马白贵风御。
  19. [2020-11-06 02:12:17,614] - lstm_model.py [Line:198] - [INFO]-[thread:140348029687616]-[process:31309] - ==================End=====================
  20. [2020-11-06 02:13:56,069] - lstm_model.py [Line:194] - [INFO]-[thread:140348029687616]-[process:31309] - ==================Epoch 2112, Loss 7.728175163269043=====================
  21. [2020-11-06 02:13:56,946] - lstm_model.py [Line:197] - [INFO]-[thread:140348029687616]-[process:31309] - 旅泊多年岁,发知期东今。君自舟岁未,应当折君新。
  22. [2020-11-06 02:13:57,731] - lstm_model.py [Line:197] - [INFO]-[thread:140348029687616]-[process:31309] - 吾师师子儿,花其重前相。鸟千人身一,清相无道因。
  23. [2020-11-06 02:13:58,532] - lstm_model.py [Line:197] - [INFO]-[thread:140348029687616]-[process:31309] - 和吹度穹旻,此外更高可。来闻人成独,故去深看春。
  24. [2020-11-06 02:13:59,317] - lstm_model.py [Line:197] - [INFO]-[thread:140348029687616]-[process:31309] - 下直遇春日,独与时相飞。江君贤犹名,清清曲河人。
  25. [2020-11-06 02:14:00,089] - lstm_model.py [Line:197] - [INFO]-[thread:140348029687616]-[process:31309] - 月暗竹亭幽,碧昏时尽黄。园春歌雪光,云落分白草。
  26. [2020-11-06 02:14:00,861] - lstm_model.py [Line:197] - [INFO]-[thread:140348029687616]-[process:31309] - 睢阳陷虏日,远平然多岩。公水三共朝,月看同出人。
  27. [2020-11-06 02:14:00,861] - lstm_model.py [Line:198] - [INFO]-[thread:140348029687616]-[process:31309] - ==================End=====================
  28. [2020-11-06 02:15:22,836] - lstm_model.py [Line:148] - [DEBUG]-[thread:140348029687616]-[process:31309] - end training

6.4 Sequence to Sequence应用

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注