自己动手做聊天机器人 四十一-视频教程之环境搭建与python基础

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

视频大纲

  • 虚拟机与操作系统
  • 开发一个仿python的解释器
  • python的变量、函数、分支、循环
  • python的复杂数据结构、输入输出、异常、类、开源库

 

视频截屏

时长1小时12分

 

视频获取方式

考虑到个人的知识整理、视频录制、视频剪辑、以及知识本身的价值,形式上收取9.9元的苦力费以表支持,希望大家多多理解,也算是对我的鼓励,获取方式比较简单,请刷下方微信或支付宝二维码,支付9.9元,之后把订单号以如下任意方式发送给我,我会把视频下载地址奉上(一般我会在下一个工作日统一回复):

把支付订单号的后六位或截图发到我的邮箱shareditor.com@gmail.com,邮件标题请注明:“视频教程之环境搭建与python基础”;

 

另外,欢迎关注我的微信公众号lcsays(也可以扫本网页中的公众号二维码),更欢迎打赏

我还创建了一个聊天机器人技术交流分享微信群,扫如下二维码可以入群

(注:扫二维码加小二兔为好友后发送“加群”即可收到入群邀请)

自己动手做聊天机器人 四十-视频教程之开篇宣言与知识点梳理

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

第一节视频截屏

时长:27分29秒

 

第一节视频内容梗概

面向人群

大二至在职

 

知识线索图

50多个知识节点的梳理和说明

 

我的风格

多讲代码实操,少讲理论公式

不追求表述严谨,但追求理解到位

不追求深刻理解,但追求面面俱到

 

第一节视频的下载方式

扫描关注如下服务号,并发送指令“开篇宣言”

版权所有,翻版必究,如有巧合,纯属雷同

 

机器学习教程 二十四-[吐血整理]涉及面最广的机器学习资料大全

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

资料内容涵盖

小白(前置知识)  

初级(了解初识)  

中级(系统学习)

高级(应用拓展)

 

部分文件清单

 

 /小白(前置知识)/
 /小白(前置知识)/数学/
 /小白(前置知识)/数学/复分析(原书第3版) L
 /小白(前置知识)/数学/复分析笔记
 /小白(前置知识)/数学/小波与傅里叶分析基础
 /小白(前置知识)/数学/线性代数
 /小白(前置知识)/数学/线性代数第8版
 /小白(前置知识)/数学/线性代数第8版笔记
 /初级(了解初识)/
 /初级(了解初识)/分类知识/Python 网页爬虫 & 文本处理 & 科学计算 & 机器学习 & 数据挖掘兵器谱
 /初级(了解初识)/分类知识/R语言参考卡
 /初级(了解初识)/笔记/notes1
 /初级(了解初识)/笔记/第一节监督学习笔记
 /初级(了解初识)/综述/
 /初级(了解初识)/综述/Deep Learning 算法介绍
 /初级(了解初识)/综述/信息时代的计算机科学理论(Foundations of Data Science)
 /初级(了解初识)/综述/信息检索介绍
 /初级(了解初识)/综述/初学者如何查阅自然语言处理(NLP)领域学术资料
 /初级(了解初识)/综述/十张图介绍机器学习概念
 /初级(了解初识)/综述/大数据分析:机器学习算法实现的演化
 /初级(了解初识)/综述/对机器学习初学者的一点建议
 /初级(了解初识)/综述/机器学习从新手到专家
 /初级(了解初识)/综述/机器学习是什么
 /初级(了解初识)/综述/机器学习理论介绍及其应用
 /初级(了解初识)/综述/机器学习算法之旅
 /初级(了解初识)/综述/译文:机器学习简史
 /初级(了解初识)/视频/
 /初级(了解初识)/视频/How to Layout and Manage Your Machine Learning Project
 /初级(了解初识)/视频/NYU 2014 年的深度学习课程资料
 /初级(了解初识)/视频/加州理工学院机器学习视频
 /初级(了解初识)/视频/吴立德《概率主题模型&数据科学基础》课程视频
 /初级(了解初识)/视频/文本与数据挖掘视频汇总
 /初级(了解初识)/视频/斯坦福大学公开课 :机器学习课程_机器学习的动机与应用_网易公开课
 /初级(了解初识)/视频/杨强在 TEDxNanjing 谈智能的起源
 /初级(了解初识)/视频/百度余凯&张潼机器学习视频
 /初级(了解初识)/资源/16本机器学习电子书
 /初级(了解初识)/资源/2014 年国际机器学习大会 ICML 2014 论文
 /初级(了解初识)/资源/NLP常用信息资源
 /初级(了解初识)/资源/国外程序员整理的机器学习资源大全
 /初级(了解初识)/资源/大数据项目列表
 /初级(了解初识)/资源/我的深度学习阅读清单
 /初级(了解初识)/资源/斯坦福《自然语言处理》课程视频
 /初级(了解初识)/资源/机器学习“搜索引擎
 /初级(了解初识)/资源/机器学习入门资源不完全汇总
 /初级(了解初识)/资源/机器学习数据挖掘免费电子书
 /初级(了解初识)/资源/机器学习最佳入门学习资料汇总
 /初级(了解初识)/资源/机器学习经典论文-survey合集
 /初级(了解初识)/资源/深度学习Reading List
 /初级(了解初识)/资源/深度学习文献目录
 /初级(了解初识)/资源/计算机科研论文资源
 /初级(了解初识)/资源/近200篇机器学习&深度学习资料分享 | 数据科学家联盟
 /中级(系统学习)/
 /中级(系统学习)/分类知识/Automatic Construction and Natural-Language Description of Nonparametric Regression Models
 /中级(系统学习)/分类知识/Bayesian network 与python概率编程实战入门
 /中级(系统学习)/分类知识/Deep Learning 101
 /中级(系统学习)/分类知识/NLP中的中文分词技术
 /中级(系统学习)/分类知识/关于(Deep) Neural Networks 在 NLP 和 Text Mining 方面一些 paper 的总结
 /中级(系统学习)/分类知识/学习算法的 Neural Turing Machine
 /中级(系统学习)/分类知识/探索推荐引擎内部的秘密
 /中级(系统学习)/分类知识/支持向量机资源
 /中级(系统学习)/分类知识/机器学习经典算法详解及 Python 实现–基于 SMO 的 SVM 分类器
 /中级(系统学习)/分类知识/机器学习经典算法详解及 Python 实现–线性回归(Linear Regression)算法
 /中级(系统学习)/分类知识/树莓派的人脸识别教程
 /中级(系统学习)/分类知识/概率编程语言与贝叶斯方法实践
 /中级(系统学习)/分类知识/理解卷积神经网络
 /中级(系统学习)/分类知识/计算机视觉入门之前景目标检测1(总结)
 /中级(系统学习)/分类知识/递归神经网络RNN
 /中级(系统学习)/完整教学/AndrewNG教程嘉定的基础知识
 /中级(系统学习)/完整教学/Bengio 组(蒙特利尔大学 LISA 组)深度学习教程
 /中级(系统学习)/完整教学/Data Science with R
 /中级(系统学习)/完整教学/Deep learning from the bottom up
 /中级(系统学习)/完整教学/Deep Learning
 /中级(系统学习)/完整教学/Machine Learning is Fun!
 /中级(系统学习)/完整教学/R机器学习实战
 /中级(系统学习)/完整教学/R语言编程教程
 /中级(系统学习)/完整教学/UFLDL中文教程
 /中级(系统学习)/完整教学/上海交通大学机器学习导论
 /中级(系统学习)/完整教学/基于R语言的统计学习介绍(第六版)
 /中级(系统学习)/完整教学/机器学习入门者学习指南(经验分享)
 /中级(系统学习)/完整教学/机器学习的统计基础在线版
 /中级(系统学习)/完整教学/机器学习速查表
 /中级(系统学习)/完整教学/深度学习初级读本
 /中级(系统学习)/完整教学/神经网络与深度学习
 /中级(系统学习)/完整教学/神经网络黑客指南
 /中级(系统学习)/完整教学/统计机器学习上海交通大学
 /中级(系统学习)/工具/Scikit-Learn
 /中级(系统学习)/工具/Sibyl监督式机器学习系统
 /中级(系统学习)/数学/CNN 的反向求导及练习
 /中级(系统学习)/数学/数值优化博文
 /中级(系统学习)/数学/机器学习中应用数学的笔记
 /中级(系统学习)/数学/矩阵概率不等式介绍
 /中级(系统学习)/数学/神奇的伽玛函数(上)
 /中级(系统学习)/数学/计算机科学中的数学
 /中级(系统学习)/笔记/Deep Learning(深度学习)学习笔记整理系列
 /中级(系统学习)/笔记/图灵奖得主 Donald Knuth 提问记录稿
 /中级(系统学习)/综述/A Tour of Machine Learning Algorithms
 /中级(系统学习)/综述/关于机器学习的若干理论问题
 /中级(系统学习)/综述/如何成为一位数据科学家
 /中级(系统学习)/综述/机器学习常见算法分类汇总
 /中级(系统学习)/综述/机器学习教会了我们什么
 /中级(系统学习)/综述/深度学习与统计学习理论
 /中级(系统学习)/综述/深度学习和浅度学习
 /中级(系统学习)/综述/深度学习概述:从感知机到深度网络
 /中级(系统学习)/综述/深度学习综述
 /中级(系统学习)/综述/神经网络与深度学习综述
 /中级(系统学习)/综述/简明深度学习方法概述
 /高级(应用拓展)/
 /高级(应用拓展)/优化/分布式并行处理的数据
 /高级(应用拓展)/优化/机器学习与优化
 /高级(应用拓展)/优化/机器学习提升之道
 /高级(应用拓展)/优化/构建工业机器学习架构
 /高级(应用拓展)/优化/王益:分布式机器学习的故事
 /高级(应用拓展)/分类知识/对话机器学习大神Michael Jordan:深度模型
 /高级(应用拓展)/分类知识/空间数据挖掘常用方法
 /高级(应用拓展)/应用/Andrej Karpathy 的深度强化学习演示
 /高级(应用拓展)/应用/CIKM Competition数据挖掘竞赛夺冠算法陈运文
 /高级(应用拓展)/应用/Connectionist Temporal Classification- Labelling Unsegmented Sequence Data with Recurrent Neural Networks
 /高级(应用拓展)/应用/Deep Learning for Natural Language Processing and Related Applications
 /高级(应用拓展)/应用/Deep Learning 和 Knowledge Graph 引爆大数据革命
 /高级(应用拓展)/应用/google用deep learning做antispam
 /高级(应用拓展)/应用/GRAMMAR AS A FOREIGN LANGUAGE
 /高级(应用拓展)/应用/Hands-On Data Science with R Text Mining
 /高级(应用拓展)/应用/Image classification with deep learning 常用模型
 /高级(应用拓展)/应用/Learning Deep Architectures for AI
 /高级(应用拓展)/应用/Machine Learning for Industry- A Case Study
 /高级(应用拓展)/应用/mllib 实践经验(1)
 /高级(应用拓展)/应用/NIPS 审稿实验
 /高级(应用拓展)/应用/Reproducing Kernel Hilbert Space
 /高级(应用拓展)/应用/Sequence to Sequence Learning with Neural Networks
 /高级(应用拓展)/应用/The Browsemaps- Collaborative Filtering at LinkedIn_rsweb2014_submission_3
 /高级(应用拓展)/应用/Use Google's Word2Vec for movie reviews
 /高级(应用拓展)/应用/人脸识别必读的N篇文章
 /高级(应用拓展)/应用/使用 deep learning 的人脸关键点检测
 /高级(应用拓展)/应用/使用 Neo4j 做电影评论的情感分析
 /高级(应用拓展)/应用/利用卷积神经网络做音乐推荐
 /高级(应用拓展)/应用/利用机器学习的谣言判别
 /高级(应用拓展)/应用/利用深度学习与大数据构建对话系统
 /高级(应用拓展)/应用/如何选择机器学习分类器
 /高级(应用拓展)/应用/推荐系统经典论文文献及业界应用
 /高级(应用拓展)/应用/浅析人脸检测之Haar分类器方法
 /高级(应用拓展)/应用/深度学习在自然语言处理的应用v0
 /高级(应用拓展)/应用/用大数据和机器学习做股票价格预测
 /高级(应用拓展)/应用/自然语言处理的深度学习理论与实际
 /高级(应用拓展)/应用/计算机视觉数据集不完全汇总
 /高级(应用拓展)/应用/谷歌地图解密:大数据与机器学习的结合
 /高级(应用拓展)/资源/github中最好的100个机器学习项目
 /高级(应用拓展)/资源/ICLR2014论文
 /高级(应用拓展)/资源/NLPIR-ICTCLAS 汉语分词的 Python 接口
 /高级(应用拓展)/资源/【语料库】语料库资源汇总
 /高级(应用拓展)/资源/免费大数据集
 /高级(应用拓展)/资源/机器学习经典书籍

 

下载方式

小二兔机器人获取

打开小二兔机器人页面:https://www.lcsays.com/chatbot/

问他:“机器学习资料”

他会告诉你最新的下载地址

 

微信订阅号下载

扫码关注我的微信订阅号,并发送:“机器学习资料”

自己动手做聊天机器人 三十九-满腔热血:在家里搭建一台GPU云服务共享给人工智能和大数据爱好者

怀着一番热情想要研发一款开源的聊天机器人,但手中只有一台公司配的mac

每做一次训练要三四天的时间,想要购置一台高配GPU的台式机却囊中羞涩

租阿里云的GPU一小时收我20块钱,每用一个小时心里都在滴血

正在心痛之时突然冒出一个想法:阿里云这么贵,我为什么不能买个高配GPU台式机分享给大家使用呢?

阿里云20元一小时,我来个惊爆的,我20元可以独享一整天!

算了一下设备成本、电费、网费、还要再熬几个通宵开发客户管理系统,真有点对不住自己

于是想到了前阶段和粉丝们分享的《自己动手做聊天机器人 二十九-重磅:近1GB的三千万聊天语料供出》,9.9元辛苦钱,得到了诸多粉丝的赞助

于是乎,我又冒出一个想法:如果每个人愿意赞助9.9,可以独享设备3小时(比阿里云便宜6倍),这不但是一笔很值的有偿赞助,而且如果能找到800个人就可以实现我的人生小愿望了,一举两得,大家受益,何乐而不为呢?

这只是我的一个想法,非常想听听大家的声音,建博客写教程快一年时间了,该是时候建个群拉大家一起交流了,对聊天机器人感兴趣的欢迎扫码入群:

(注:请直接扫二维码加小二兔为好友,然后回复“加群”,即可收到入群邀请)

自己动手做聊天机器人 三十八-原来聊天机器人是这么做出来的

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

seq2seq模型原理

主要参考《Sequence to Sequence Learning with Neural Networks》这篇论文,核心思想如下图:

ABC是输入语句,WXYZ是输出语句,EOS是标识一句话结束,图中的训练单元是lstm,lstm的特点是有长短时记忆,所以能够根据输入的多个字来确定后面的多个字,有关lstm的知识可以参考《http://deeplearning.net/tutorial/lstm.html

上面的模型中编码器和解码器共用了同一个lstm层,也就是共享了参数,牛人们尝试把他们分开像https://github.com/farizrahman4u/seq2seq中提到的样子:

其中绿色是编码器,黄色是解码器,橙色的箭头传递的是lstm层的状态信息也就是记忆信息,编码器唯一传给解码器的就是这个状态信息

我们看到解码器每一时序的输入都是前一个时序的输出,从整体上来看就是:我通过不同时序输入“How are you <EOL>”,模型就能自动一个字一个字的输出“W I am fine <EOL>”,这里的W是一个特殊的标识,它既是编码器最后的输出,同时又是解码器的一个触发信号

那么我们训练的时候输入的X,Y应该是什么呢?X="How are you <EOL>",Y="W I am fine <EOL>"?

这是不行的,因为在解码器还没有训练出靠谱的参数之前,我们无法保证第一个时序的输出就是“I”,那么传给第二个时序的输入就不一定是I,同样第三、四个时序的输入就无法保证是am和fine,那么是无法训练出想要的模型的

我们要这样来做:我们直接把解码器中每一时序的输入强制改为"W I am fine",也就是把这部分从我们训练样本的输入X中传过来,而Y依然是预测输出的"W I am fine <EOL>",这样训练出来的模型就是我们设计的编码器解码器模型了

那么在使用训练好的模型做预测的时候,我们改变处理方式:在解码时以前一时序的输出为输入做预测,这样就能输出我们希望输出的"W I am fine <EOL>"了

基于以上的原理,下面开始我们的工程实践

 

语料准备工作

准备至少300w的聊天语料用于词向量的训练和seq2seq模型的训练,语料越丰富训练出来的词向量质量越好,如果想通过影视剧字幕来获取语料可以参考《自己动手做聊天机器人 二十九-重磅:近1GB的三千万聊天语料供出

获取到原始语料后需要做一些加工处理,首先需要做切词,方法如下:

python word_segment.py ./corpus.raw ./corpus.segment

其中word_segment.py是我写的切词工具,仅供参考

之后要把切词好的文件转成“|”分隔的问答对,如下:

cat ./corpus.segment | awk '{if(last!="")print last"|"$0;last=$0}' | sed 's/| /|/g' > ./corpus.segment.pair

这样语料准备工作就齐全了

 

训练词向量

我们直接利用google的word2vec来训练词向量,如下:

word2vec -train ./corpus.segment -output vectors.bin -cbow 1 -size 200 -window 8 -negative 25 -hs 0 -sample 1e-5 -threads 20 -binary 1 -iter 15

其中corpus.raw是原始语料数据,vectors.bin是生成的词向量二进制文件

了解word2vec的原理请见《自己动手做聊天机器人 二十五-google的文本挖掘深度学习工具word2vec的实现原理

生成的词向量二进制加载方法可以参考我写的:word_vectors_loader.py

 

创建模型

下面就是重点的模型创建过程,这里面我们直接使用tensorflow+tflearn库来实现:

# 首先我们为输入的样本数据申请变量空间,如下。其中self.max_seq_len是指一个切好词的句子最多包含多少个词,self.word_vec_dim是词向量的维度,这里面shape指定了输入数据是不确定数量的样本,每个样本最多包含max_seq_len*2个词,每个词用word_vec_dim维浮点数表示。这里面用2倍的max_seq_len是因为我们训练是输入的X既要包含question句子又要包含answer句子
input_data = tflearn.input_data(shape=[None, self.max_seq_len*2, self.word_vec_dim], dtype=tf.float32, name = "XY")
# 然后我们将输入的所有样本数据的词序列切出前max_seq_len个,也就是question句子部分,作为编码器的输入
encoder_inputs = tf.slice(input_data, [0, 0, 0], [-1, self.max_seq_len, self.word_vec_dim], name="enc_in")
# 再取出后max_seq_len-1个,也就是answer句子部分,作为解码器的输入。注意,这里只取了max_seq_len-1个,是因为还要在前面拼上一组GO标识来告诉解码器我们要开始解码了,也就是下面加上go_inputs拼成最终的go_inputs
decoder_inputs_tmp = tf.slice(input_data, [0, self.max_seq_len, 0], [-1, self.max_seq_len-1, self.word_vec_dim], name="dec_in_tmp")
go_inputs = tf.ones_like(decoder_inputs_tmp)
go_inputs = tf.slice(go_inputs, [0, 0, 0], [-1, 1, self.word_vec_dim])
decoder_inputs = tf.concat(1, [go_inputs, decoder_inputs_tmp], name="dec_in")
# 之后开始编码过程,返回的encoder_output_tensor展开成tflearn.regression回归可以识别的形如(?, 1, 200)的向量;返回的states后面传入给解码器
(encoder_output_tensor, states) = tflearn.lstm(encoder_inputs, self.word_vec_dim, return_state=True, scope='encoder_lstm')
encoder_output_sequence = tf.pack([encoder_output_tensor], axis=1)
# 取出decoder_inputs的第一个词,也就是GO
first_dec_input = tf.slice(decoder_inputs, [0, 0, 0], [-1, 1, self.word_vec_dim])
# 将其输入到解码器中,如下,解码器的初始化状态为编码器生成的states,注意:这里的scope='decoder_lstm'是为了下面重用同一个解码器
decoder_output_tensor = tflearn.lstm(first_dec_input, self.word_vec_dim, initial_state=states, return_seq=False, reuse=False, scope='decoder_lstm')
# 暂时先将解码器的第一个输出存到decoder_output_sequence_list中供最后一起输出
decoder_output_sequence_single = tf.pack([decoder_output_tensor], axis=1)
decoder_output_sequence_list = [decoder_output_tensor]
# 接下来我们循环max_seq_len-1次,不断取decoder_inputs的一个个词向量作为下一轮解码器输入,并将结果添加到decoder_output_sequence_list中,这里面的reuse=True, scope='decoder_lstm'说明和上面第一次解码用的是同一个lstm层
for i in range(self.max_seq_len-1):
   next_dec_input = tf.slice(decoder_inputs, [0, i+1, 0], [-1, 1, self.word_vec_dim])
   decoder_output_tensor = tflearn.lstm(next_dec_input, self.word_vec_dim, return_seq=False, reuse=True, scope='decoder_lstm')
   decoder_output_sequence_single = tf.pack([decoder_output_tensor], axis=1)
   decoder_output_sequence_list.append(decoder_output_tensor)
# 下面我们把编码器第一个输出和解码器所有输出拼接起来,作为tflearn.regression回归的输入
decoder_output_sequence = tf.pack(decoder_output_sequence_list, axis=1)
real_output_sequence = tf.concat(1, [encoder_output_sequence, decoder_output_sequence])
net = tflearn.regression(real_output_sequence, optimizer='sgd', learning_rate=0.1, loss='mean_square')
model = tflearn.DNN(net)

至此模型创建完成,让我们汇总一下里面的思想:

1)训练输入的X、Y分别是编码器解码器的输入和预测的输出;

2)X切分两半,前一半是编码器输入,后一半是解码器输入;

3)编码解码器输出的预测值用Y做回归训练

4)训练时通过样本的真实值作为解码器输入,实际预测时将不会有上图中WXYZ部分,因此上一时序的输出将作为下一时序的输入(后面会详述预测的实现)

 

训练模型

下面我们来实例化模型并喂数据做训练,如下:

model = self.model()
model.fit(trainXY, trainY, n_epoch=1000, snapshot_epoch=False, batch_size=1)
model.load('./model/model')

这里的trainXY和trainY通过加载上面我们准备的语料来赋值

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

首先我们加载词向量并存到word_vector_dict中,然后读取语料文件并挨个词查word_vector_dict并赋值向量给question_seq和answer_seq,如下:

def init_seq(input_file):
    """读取切好词的文本文件,加载全部词序列
    """
    file_object = open(input_file, 'r')
    vocab_dict = {}
    while True:
        question_seq = []
        answer_seq = []
        line = file_object.readline()
        if line:
            line_pair = line.split('|')
            line_question = line_pair[0]
            line_answer = line_pair[1]
            for word in line_question.decode('utf-8').split(' '):
                if word_vector_dict.has_key(word):
                    question_seq.append(word_vector_dict[word])
            for word in line_answer.decode('utf-8').split(' '):
                if word_vector_dict.has_key(word):
                    answer_seq.append(word_vector_dict[word])
        else:
            break
        question_seqs.append(question_seq)
        answer_seqs.append(answer_seq)
    file_object.close()

有了question_seq和answer_seq,我们来构造trainXY和trainY,如下:

    def generate_trainig_data(self):
        xy_data = []
        y_data = []
        for i in range(len(question_seqs)):
            question_seq = question_seqs[i]
            answer_seq = answer_seqs[i]
            if len(question_seq) < self.max_seq_len and len(answer_seq) < self.max_seq_len:
                sequence_xy = [np.zeros(self.word_vec_dim)] * (self.max_seq_len-len(question_seq)) + list(reversed(question_seq))
                sequence_y = answer_seq + [np.zeros(self.word_vec_dim)] * (self.max_seq_len-len(answer_seq))
                sequence_xy = sequence_xy + sequence_y
                sequence_y = [np.ones(self.word_vec_dim)] + sequence_y
                xy_data.append(sequence_xy)
                y_data.append(sequence_y)
        return np.array(xy_data), np.array(y_data)

构造了训练数据也创建好了模型,训练的效果如下:

[root@centos #] python my_seq2seq_v2.py train
begin load vectors
words = 70937
size = 200
load vectors finish
---------------------------------
Run id: 9PZWKM
Log directory: /tmp/tflearn_logs/
---------------------------------
Training samples: 368
Validation samples: 0
--
Training Step: 47  | total loss: 0.62260
| SGD | epoch: 001 | loss: 0.62260 -- iter: 047/368

最终会生成./model/model模型文件

 

效果预测

训练好模型,我们希望能输入一句话来预测一下回答,如下:

predict = model.predict(testXY)

因为我们只有question没有answer,所以testXY中是没有Y部分的,所以需要在程序中做一些改变,即用上一句的输出作为下一句的输入,如下:

for i in range(self.max_seq_len-1):
   # next_dec_input = tf.slice(decoder_inputs, [0, i+1, 0], [-1, 1, self.word_vec_dim])这里改成下面这句
   next_dec_input = decoder_output_sequence_single
   decoder_output_tensor = tflearn.lstm(next_dec_input, self.word_vec_dim, return_seq=False, reuse=True, scope='decoder_lstm')
   decoder_output_sequence_single = tf.pack([decoder_output_tensor], axis=1)
   decoder_output_sequence_list.append(decoder_output_tensor)

因为词向量是多维浮点数,预测出的词向量需要通过余弦相似度来匹配,余弦相似度匹配方法如下:

def vector2word(vector):
    max_cos = -10000
    match_word = ''
    for word in word_vector_dict:
        v = word_vector_dict[word]
        cosine = vector_cosine(vector, v)
        if cosine > max_cos:
            max_cos = cosine
            match_word = word
    return (match_word, max_cos)

其中的vector_cosine实现如下:

def vector_cosine(v1, v2):
    if len(v1) != len(v2):
        sys.exit(1)
    sqrtlen1 = vector_sqrtlen(v1)
    sqrtlen2 = vector_sqrtlen(v2)
    value = 0
    for item1, item2 in zip(v1, v2):
        value += item1 * item2
    return value / (sqrtlen1*sqrtlen2)

其中的vector_sqrtlen实现如下:

def vector_sqrtlen(vector):
    len = 0
    for item in vector:
        len += item * item
    len = math.sqrt(len)
    return len

预测效果如下:

输入是“真 讨厌”

预测结果:

[root@centos #] python my_seq2seq_v2.py test test.data
begin load vectors
words = 70937
size = 200
load vectors finish
predict answer
竟然 0.796628661264 8.13188244428
是 0.361905373571 4.72316883181
你 0.416023172832 3.78265507983
啊 0.454288467277 3.13229596833
不是 0.424590214456 2.90688231062
你 0.489174557107 2.62733802498
啊 0.501460288258 2.87990178439
你 0.560230783333 3.09066126524

输出的第一列是预测的每个时序产生的词,第二列是预测输出向量和最近的词向量的余弦相似度,第三列是预测向量的欧氏距离

因为我们设计的max_seq_len是定长8,所以输出的序列最后会多余一些字,可以根据余弦相似度或者其他指标设定一个阈值来截断

以上列出的是部分代码,全部代码分享在my_seq2seq_v2.py欢迎点击观看

自己动手做聊天机器人 三十七-一张图了解tensorflow中的线性回归工作原理

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

10行关键代码实现的线性回归

# -*- coding: utf-8 -*-
import numpy as np
import tensorflow as tf
# 随机生成1000个点,围绕在y=0.1x+0.3的直线周围
num_points = 1000
vectors_set = []
for i in xrange(num_points):
    x1 = np.random.normal(0.0, 0.55)
    y1 = x1 * 0.1 + 0.3 + np.random.normal(0.0, 0.03)
    vectors_set.append([x1, y1])
# 生成一些样本
x_data = [v[0] for v in vectors_set]
y_data = [v[1] for v in vectors_set]

生成1维的W矩阵,取值是[-1,1]之间的随机数

W = tf.Variable(tf.random_uniform([1], -1.0, 1.0), name=‘W’)

生成1维的b矩阵,初始值是0

b = tf.Variable(tf.zeros([1]), name=‘b’)

经过计算得出预估值y

y = W * x_data + b

以预估值y和实际值y_data之间的均方误差作为损失

loss = tf.reduce_mean(tf.square(y - y_data), name=‘loss’)

采用梯度下降法来优化参数

optimizer = tf.train.GradientDescentOptimizer(0.5)

训练的过程就是最小化这个误差值

train = optimizer.minimize(loss, name=‘train’) sess = tf.Session()

输出图结构

#print sess.graph_def init = tf.initialize_all_variables() sess.run(init)

初始化的W和b是多少

print “W =”, sess.run(W), “b =”, sess.run(b), “loss =”, sess.run(loss)

执行20次训练

for step in xrange(20): sess.run(train) # 输出训练好的W和b print “W =”, sess.run(W), “b =”, sess.run(b), “loss =”, sess.run(loss)

生成summary文件,用于tensorboard使用

writer = tf.train.SummaryWriter("./tmp", sess.graph)

 

整个收敛过程可以从输出结果中看出:

W = [-0.67839384] b = [ 0.] loss = 0.293898
W = [-0.41792655] b = [ 0.30027848] loss = 0.0907883
W = [-0.24463286] b = [ 0.30015084] loss = 0.0407606
W = [-0.12923941] b = [ 0.30006593] loss = 0.0185783
W = [-0.05240078] b = [ 0.30000937] loss = 0.00874262
W = [-0.00123519] b = [ 0.29997173] loss = 0.00438147
W = [ 0.03283515] b = [ 0.29994667] loss = 0.00244773
W = [ 0.05552204] b = [ 0.29992998] loss = 0.00159031
W = [ 0.07062886] b = [ 0.29991883] loss = 0.00121013
W = [ 0.08068825] b = [ 0.29991144] loss = 0.00104155
W = [ 0.08738664] b = [ 0.29990652] loss = 0.000966807
W = [ 0.09184699] b = [ 0.29990324] loss = 0.000933665
W = [ 0.09481706] b = [ 0.29990104] loss = 0.00091897
W = [ 0.09679478] b = [ 0.29989958] loss = 0.000912454
W = [ 0.09811172] b = [ 0.29989862] loss = 0.000909564
W = [ 0.09898864] b = [ 0.29989797] loss = 0.000908283
W = [ 0.09957258] b = [ 0.29989755] loss = 0.000907715
W = [ 0.09996141] b = [ 0.29989725] loss = 0.000907463
W = [ 0.10022032] b = [ 0.29989707] loss = 0.000907352
W = [ 0.10039273] b = [ 0.29989696] loss = 0.000907302
W = [ 0.10050753] b = [ 0.29989687] loss = 0.00090728

 

 

一张图展示线性回归工作原理

执行上面的代码后会在本地生成一个tmp目录,里面会产生用于tensorboard读取的数据,下面我们执行:

tensorboard --logdir=./tmp/

打开http://localhost:6006/的GRAPHS,并展开里面的一系列关键节点,如下图所示:

这张图就是上面这段代码生成的graph结构,这个graph描述了整个梯度下降解决线性回归问题的整个过程,每一个节点都代表了代码中的一步操作,我们来具体看一下每一部分的内容

 

详细分析线性回归的graph

首先来看我们要训练的W和b,如下图:

我们代码中对W共有三种操作:Assign、read、train。

其中assign是基于random_uniform赋值的,对应着下面这句代码:

W = tf.Variable(tf.random_uniform([1], -1.0, 1.0), name='W')

这里的tf.random_uniform的graph部分如下:

其中read对应的这句代码:

y = W * x_data + b

其中train对应的是梯度下降训练过程的操作

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

我们代码中对b同样也有三种操作:Assign、read、train。与W不同的是它是用zeros赋初始化值的

我们的W和b就是通过梯度下降计算update_W和update_b,从而更新W和b的值的。这里的update_W和update_b都是基于三个输入来计算得出的:学习率learning_rate、W/b当前值、梯度gradients,过程如下图:

最关键的梯度下降过程由下面几个过程实现:

下面我们分析一下详细的计算过程,首先看这句代码:

loss = tf.reduce_mean(tf.square(y - y_data), name='loss')

这里的y-y_data对应着这部分:

那么tf.squre(y-y_data)对应着那条长长的曲线

下面我们看这里:

这里的右下角是以y-y_data为输入的,但这里的x不是我们的x_data,而是一个临时常量:2。也即是2(y-y_data),这明显是(y-y_data)^2的导数

继续往上看:

这里是以2(y-y_data)为输入经过各种处理最终生成参数b的增量update_b,至此,我们完成了一半了,因为能够更新b了

我们继续往上看:

这里可以生成update_W用来更新W,反向追溯可以看出这个值是依赖于add_grad(也就是基于y-y_data的部分)和W以及y生成的,至此梯度下降整个过程就分析完了,

在这个大图中,详细计算过程如:http://stackoverflow.com/questions/39580427/how-does-tensorflow-calculate-the-gradients-for-the-tf-train-gradientdescentopti所说,一步简单的操作很普遍的被tensorflow转成了很多个节点的图,所以里面有一些细节的节点就不深入分析,因为只是一些操作的图表达,没有太重要的意义

自己动手做聊天机器人 三十六-深入理解tensorflow的session和graph

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

tensorflow中的基本数学运算用法

import tensorflow as tf
sess = tf.Session()
a = tf.placeholder("float")
b = tf.placeholder("float")
c = tf.constant(6.0)
d = tf.mul(a, b)
y = tf.mul(d, c)
print sess.run(y, feed_dict={a: 3, b: 3})
A = [[1.1,2.3],[3.4,4.1]]
Y = tf.matrix_inverse(A)
print sess.run(Y)
sess.close()

 

主要数字运算还包括:

tf.add
tf.sub
tf.mul
tf.div
tf.mod
tf.abs
tf.neg
tf.sign
tf.inv
tf.square
tf.round
tf.sqrt
tf.pow
tf.exp
tf.log
tf.maximum
tf.minimum
tf.cos
tf.sin

主要矩阵运算还包括:

tf.diag生成对角阵
tf.transpose
tf.matmul
tf.matrix_determinant计算行列式的值
tf.matrix_inverse计算矩阵的逆

 

插播小甜点:tensorboard使用

tensorflow因为代码执行过程是先构建图,然后在执行,所以对中间过程的调试不太方便,所以提供了一个tensorboard工具来便于调试,用法如下:

在训练时会提示写入事件文件到哪个目录(比如:/tmp/tflearn_logs/11U8M4/)

执行如下命令并打开http://192.168.1.101:6006就能看到tensorboard的界面

tensorboard --logdir=/tmp/tflearn_logs/11U8M4/

 

什么是Graph和Session

为了步入正题,我们通过一段代码来展示Graph和Session的使用

import tensorflow as tf
with tf.Graph().as_default() as g:
    with g.name_scope("myscope") as scope: # 有了这个scope,下面的op的name都是类似myscope/Placeholder这样的前缀
        sess = tf.Session(target='', graph = g, config=None) # target表示要连接的tf执行引擎
        print "graph version:", g.version # 0
        a = tf.placeholder("float")
        print a.op # 输出整个operation信息,跟下面g.get_operations返回结果一样
        print "graph version:", g.version # 1
        b = tf.placeholder("float")
        print "graph version:", g.version # 2
        c = tf.placeholder("float")
        print "graph version:", g.version # 3
        y1 = tf.mul(a, b) # 也可以写成a * b
        print "graph version:", g.version # 4
        y2 = tf.mul(y1, c) # 也可以写成y1 * c
        print "graph version:", g.version # 5
        operations = g.get_operations()
        for (i, op) in enumerate(operations):
            print "============ operation", i+1, "==========="
            print op # 一个结构,包括:name、op、attr、input等,不同op不一样
        assert y1.graph is g
        assert sess.graph is g
        print "================ graph object address ================"
        print sess.graph
        print "================ graph define ================"
        print sess.graph_def
        print "================ sess str ================"
        print sess.sess_str
        print sess.run(y1, feed_dict={a: 3, b: 3}) # 9.0 feed_dictgraph中的元素和值的映射
        print sess.run(fetches=[b,y1], feed_dict={a: 3, b: 3}, options=None, run_metadata=None) # 传入的feches和返回值的shape相同
        print sess.run({'ret_name':y1}, feed_dict={a: 3, b: 3}) # {'ret_name': 9.0} 传入的feches和返回值的shape相同
        assert tf.get_default_session() is not sess
        with sess.as_default(): # 把sess作为默认的session,那么tf.get_default_session就是sess, 否则不是
            assert tf.get_default_session() is sess
        h = sess.partial_run_setup([y1, y2], [a, b, c]) # 分阶段运行,参数指明了feches和feed_dict列表
        res = sess.partial_run(h, y1, feed_dict={a: 3, b: 4}) # 12 运行第一阶段
        res = sess.partial_run(h, y2, feed_dict={c: res}) # 144.0 运行第二阶段,其中使用了第一阶段的执行结果
        print "partial_run res:", res
        sess.close()

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

输出如下:

graph version: 0
name: "myscope/Placeholder"
op: "Placeholder"
attr {
  key: "dtype"
  value {
    type: DT_FLOAT
  }
}
attr {
  key: "shape"
  value {
    shape {
    }
  }
}
graph version: 1
graph version: 2
graph version: 3
graph version: 4
graph version: 5
============ operation 1 ===========
name: "myscope/Placeholder"
op: "Placeholder"
attr {
  key: "dtype"
  value {
    type: DT_FLOAT
  }
}
attr {
  key: "shape"
  value {
    shape {
    }
  }
}
============ operation 2 ===========
name: "myscope/Placeholder_1"
op: "Placeholder"
attr {
  key: "dtype"
  value {
    type: DT_FLOAT
  }
}
attr {
  key: "shape"
  value {
    shape {
    }
  }
}
============ operation 3 ===========
name: "myscope/Placeholder_2"
op: "Placeholder"
attr {
  key: "dtype"
  value {
    type: DT_FLOAT
  }
}
attr {
  key: "shape"
  value {
    shape {
    }
  }
}
============ operation 4 ===========
name: "myscope/Mul"
op: "Mul"
input: "myscope/Placeholder"
input: "myscope/Placeholder_1"
attr {
  key: "T"
  value {
    type: DT_FLOAT
  }
}
============ operation 5 ===========
name: "myscope/Mul_1"
op: "Mul"
input: "myscope/Mul"
input: "myscope/Placeholder_2"
attr {
  key: "T"
  value {
    type: DT_FLOAT
  }
}
================ graph object address ================
<tensorflow.python.framework.ops.Graph object at 0x1138702d0>
================ graph define ================
node {
  name: "myscope/Placeholder"
  op: "Placeholder"
  attr {
    key: "dtype"
    value {
      type: DT_FLOAT
    }
  }
  attr {
    key: "shape"
    value {
      shape {
      }
    }
  }
}
node {
  name: "myscope/Placeholder_1"
  op: "Placeholder"
  attr {
    key: "dtype"
    value {
      type: DT_FLOAT
    }
  }
  attr {
    key: "shape"
    value {
      shape {
      }
    }
  }
}
node {
  name: "myscope/Placeholder_2"
  op: "Placeholder"
  attr {
    key: "dtype"
    value {
      type: DT_FLOAT
    }
  }
  attr {
    key: "shape"
    value {
      shape {
      }
    }
  }
}
node {
  name: "myscope/Mul"
  op: "Mul"
  input: "myscope/Placeholder"
  input: "myscope/Placeholder_1"
  attr {
    key: "T"
    value {
      type: DT_FLOAT
    }
  }
}
node {
  name: "myscope/Mul_1"
  op: "Mul"
  input: "myscope/Mul"
  input: "myscope/Placeholder_2"
  attr {
    key: "T"
    value {
      type: DT_FLOAT
    }
  }
}
versions {
  producer: 15
}
================ sess str ================
9.0
[array(3.0, dtype=float32), 9.0]
{'ret_name': 9.0}
partial_run res: 144.0

 

tensorflow的Session是如何工作的

Session是Graph和执行者之间的媒介,Session.run()实际上将graph、fetches、feed_dict序列化到字节数组中,并调用tf_session.TF_Run(参见/usr/local/lib/python2.7/site-packages/tensorflow/python/client/session.py)

而这里的tf_session.TF_Run实际上调用了动态链接库_pywrap_tensorflow.so中实现的_pywrap_tensorflow.TF_Run接口(参见/usr/local/lib/python2.7/site-packages/tensorflow/python/pywrap_tensorflow.py),这个动态链接库是tensorflow提供的诸多语言接口中python语言的接口

事实上这里的_pywrap_tensorflow.so和pywrap_tensorflow.py是通过SWIG工具自动生成,大家都知道tensorflow核心语言是c语言,这里是通过SWIG生成了各种脚本语言的接口

自己动手做聊天机器人 三十五-一个lstm单元让聊天机器人学会甄嬛体

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

下载《甄嬛传》小说原文

上网随便百度一个“甄嬛传 txt”,下载下来,首先要把文件转码成utf-8编码,然后要把windows的回车符都替换成\n,以便后续处理,最终效果如下:

[root@centos $] head zhenhuanzhuan.txt
序文--不过是「情」
    在键盘上敲落一个个文字的时候,窗外有大雨过后的清新。站在十二楼的落地玻璃窗前往外看,有大片大片开阔的深绿蔓延。
  我喜欢这个有山有水的小城,所以在这样一个烦热的下午,背负着窒闷的心情不顾一切逃出暂居的城市,来到这里,在写完了一个整整写了三年多的故事之后。
  终于,写完了《后宫:甄嬛传》的最后一本,第七本。七,是我喜欢的一个数字。甄嬛的故事,最后一个字,是我在初夏的某日坐在师大某个小宾馆的房间里写下的。这个故事,自我在母校时始,又于母校终,像一个有始有终的圆圈,终于完结了。
  这是我的第一部长篇,自己也轻吁一口气,居然写了那么长,那么久。
  可是完结的那一刻,我心里一点也不快活。因?是我自己,把我喜爱的清,把我理想中温润如玉的男子,写到玉碎斑驳。

 

对甄嬛传切词

切词工具word_segment.py请到我的github下载,地址在https://github.com/lcdevelop/ChatBotCourse/blob/master/word_segment.py

执行

python ./word_segment.py zhenhuanzhuan.txt zhenhuanzhuan.segment

生成的文件如下:

[root@centos $] head zhenhuanzhuan.segment
  序文 - - 不过 是 「 情 」
 在 键盘 上 敲落 一个个 文字 的 时候 , 窗外 有 大雨 过后 的 清新 。 站 在 十二楼 的 落地 玻璃窗 前往 外看 , 有 大片大片 开阔 的 深绿 蔓延 。
     我 喜欢 这个 有山有水 的 小城 , 所以 在 这样 一个 烦热 的 下午 , 背负着 窒闷 的 心情 不顾一切 逃出 暂居 的 城市 , 来到 这里 , 在 写 完 了 一个 整整 写 了 三年 多 的 故事 之后 。
     终于 , 写 完 了 《 后宫 : 甄 嬛 传 》 的 最后 一本 , 第七 本 。 七 , 是 我 喜欢 的 一个 数字 。 甄 嬛 的 故事 , 最后 一个 字 , 是 我 在 初夏 的 某日 坐在 师大 某个 小 宾馆 的 房间 里 写下 的 。 这个 故事 , 自我 在 母校 时始 , 又 于 母校 终 , 像 一个 有始有终 的 圆圈 , 终于 完结 了 。
     这 是 我 的 第一部 长篇 , 自己 也 轻吁 一口气 , 居然 写 了 那么 长 , 那么 久 。
     可是 完结 的 那一刻 , 我 心里 一点 也 不 快活 。 因 ? 是 我 自己 , 把 我 喜爱 的 清 , 把 我 理想 中 温润 如玉 的 男子 , 写 到 玉碎 斑驳 。

 

生成词向量

为了能应用词向量,我们利用word2vec来生成词向量,word2vec的源码可以从网上下载(或者可以在我的github上下载https://github.com/lcdevelop/ChatBotCourse/tree/master/word2vec),make编译即可

执行

./word2vec -train ./zhenhuanzhuan.segment -output vectors.bin -cbow 1 -size 200 -window 8 -negative 25 -hs 0 -sample 1e-4 -threads 20 -binary 1 -iter 15

这样会生成一个vectors.bin文件,这就是我们想要的基于甄嬛传原文生成的词向量文件

如果想知道vectors.bin文件里如何存储以及如何加载,可以参考我的另一篇文章《三十二-用三千万影视剧字幕语料库生成词向量

 

训练代码

下面是我的代码原文,仅供参考

# -*- coding: utf-8 -*-
import sys
import math
import tflearn
import chardet
import numpy as np
import struct
seq = []
max_w = 50
float_size = 4
word_vector_dict = {}
def load_vectors(input):
    """从vectors.bin加载词向量,返回一个word_vector_dict的词典,key是词,value是200维的向量
    """
    print "begin load vectors"
    input_file = open(input, "rb")
    # 获取词表数目及向量维度
    words_and_size = input_file.readline()
    words_and_size = words_and_size.strip()
    words = long(words_and_size.split(' ')[0])
    size = long(words_and_size.split(' ')[1])
    print "words =", words
    print "size =", size
    for b in range(0, words):
        a = 0
        word = ''
        # 读取一个词
        while True:
            c = input_file.read(1)
            word = word + c
            if False == c or c == ' ':
                break
            if a < max_w and c != '\n':
                a = a + 1
        word = word.strip()
        vector = []
        for index in range(0, size):
            m = input_file.read(float_size)
            (weight,) = struct.unpack('f', m)
            vector.append(weight)
        # 将词及其对应的向量存到dict中
        word_vector_dict[word.decode('utf-8')] = vector
    input_file.close()
    print "load vectors finish"
def init_seq():
    """读取切好词的文本文件,加载全部词序列
    """
    file_object = open('zhenhuanzhuan.segment', 'r')
    vocab_dict = {}
    while True:
        line = file_object.readline()
        if line:
            for word in line.decode('utf-8').split(' '):
                if word_vector_dict.has_key(word):
                    seq.append(word_vector_dict[word])
        else:
            break
    file_object.close()
def vector_sqrtlen(vector):
    len = 0
    for item in vector:
        len += item * item
    len = math.sqrt(len)
    return len
def vector_cosine(v1, v2):
    if len(v1) != len(v2):
        sys.exit(1)
    sqrtlen1 = vector_sqrtlen(v1)
    sqrtlen2 = vector_sqrtlen(v2)
    value = 0
    for item1, item2 in zip(v1, v2):
        value += item1 * item2
    return value / (sqrtlen1*sqrtlen2)

def vector2word(vector): max_cos = -10000 match_word = '' for word in word_vector_dict: v = word_vector_dict[word] cosine = vector_cosine(vector, v) if cosine > max_cos: max_cos = cosine match_word = word return (match_word, max_cos) def main(): load_vectors("./vectors.bin") init_seq() xlist = [] ylist = [] test_X = None #for i in range(len(seq)-100): for i in range(10): sequence = seq[i:i+20] xlist.append(sequence) ylist.append(seq[i+20]) if test_X is None: test_X = np.array(sequence) (match_word, max_cos) = vector2word(seq[i+20]) print “right answer=”, match_word, max_cos X = np.array(xlist) Y = np.array(ylist) net = tflearn.input_data([None, 20, 200]) net = tflearn.lstm(net, 200) net = tflearn.fully_connected(net, 200, activation=‘linear’) net = tflearn.regression(net, optimizer=‘sgd’, learning_rate=0.1, loss=‘mean_square’) model = tflearn.DNN(net) model.fit(X, Y, n_epoch=500, batch_size=10,snapshot_epoch=False,show_metric=True) model.save(“model”) predict = model.predict([test_X]) #print predict #for v in test_X: # print vector2word(v) (match_word, max_cos) = vector2word(predict[0]) print “predict=”, match_word, max_cos main()

 

解释一下上面的代码,load_vectors是从vectors.bin中加载词向量,init_seq是加载甄嬛传切词后的文本并存到一个序列里,vector2word是求距离某向量最近的词,模型中只有一个lstm单元

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

执行效果如下:

[root@centos $] python one_lstm_sequence_generate.py
begin load vectors
words = 16995
size = 200
load vectors finish
right answer= 站 1.0
---------------------------------
Run id: DQK34Q
Log directory: /tmp/tflearn_logs/
---------------------------------
Training samples: 10
Validation samples: 
--
Training Step: 500  | total loss: 0.33673
| SGD | epoch: 500 | loss: 0.33673 - acc: 0.1748 -- iter: 10/10
--
predict= 站 0.941794432002

可以看出:经过500个epoch的训练,均方损失降到0.33673,并能以0.941794432002的余弦相似度预测出下一个字

如果你有强大的gpu,可以调整上述参数,把整篇文章都训练进去,稍稍修改代码中predict的部分,让他不断的输出下一个字,就可以自动吐出甄嬛体

这段代码是基于tflearn实现的,在tflearn官方文档的examples中实现的seq2seq是直接调用了tensorflow中的tensorflow/python/ops/seq2seq.py,而这部分是基于one-hot的embedding方法,这是一定没有词向量效果好的,因此下一步打算基于上面的代码继续改造,实现基于词向量的seq2seq,相信能够让我的聊天机器人问答效果更好,敬请期待

自己动手做聊天机器人 三十四-最快的深度学习框架torch

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

torch是什么

torch就是诸多深度学习框架中的一种

业界有几大深度学习框架:1)tensorflow,谷歌主推,时下最火,小型试验和大型计算都可以,基于python,缺点是上手相对较难,速度一般;2)torch,facebook主推,用于小型试验,开源应用较多,基于lua,上手较快,网上文档较全,缺点是lua语言相对冷门;3)mxnet,大公司主推,主要用于大型计算,基于python和R,缺点是网上开源项目较少;4)caffe,大公司主推,用于大型计算,基于c++、python,缺点是开发不是很方便;5)theano,速度一般,基于python,评价很好

 

为什么使用torch

只是因为github上lstm的实现项目比较多,完全为了学习用,个人不觉得语言和框架有好坏之分,各有千秋,谁行谁上

 

在mac上安装torch

如果你使用的是其他操作系统,可以参考https://github.com/torch/torch7/wiki/Cheatsheet#installing-and-running-torch

执行

git clone https://github.com/torch/distro.git ~/torch --recursive
cd ~/torch; bash install-deps;
./install.sh

可能会遇到qt安装不成功的问题,那么可以自己单独安装,如下:

brew install cartr/qt4/qt

安装后需要手工把下面这句加到~/.bash_profile中

. ~/torch/install/bin/torch-activate

这里的路径就是你安装的路径

source ~/.bash_profile之后就可以执行th使用torch了,如下:

[root@centos $] th
  ______             __   |  Torch7
 /_  __/__  ________/ /   |  Scientific computing for Lua.
  / / / _ \/ __/ __/ _ \  |  Type ? for help
 /_/  \___/_/  \__/_//_/  |  https://github.com/torch
                          |  http://torch.ch
th>

为了方便调试,我们还要安装itorch,首先安装依赖

brew install zeromq
brew install openssl
luarocks install luacrypto OPENSSL_DIR=/usr/local/opt/openssl/

然后按如下安装

git clone https://github.com/facebook/iTorch.git
cd iTorch
luarocks make 

 

用卷积神经网络实现图像识别

创建pattern_recognition.lua,内容如下:

require 'nn'
require 'paths'
if (not paths.filep("cifar10torchsmall.zip")) then
    os.execute('wget -c https://s3.amazonaws.com/torch7/data/cifar10torchsmall.zip')
    os.execute('unzip cifar10torchsmall.zip')
end
trainset = torch.load('cifar10-train.t7')
testset = torch.load('cifar10-test.t7')
classes = {'airplane', 'automobile', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck'}
setmetatable(trainset,
{__index = function(t, i)
    return {t.data[i], t.label[i]}
end}
);
trainset.data = trainset.data:double() -- convert the data from a ByteTensor to a DoubleTensor.
function trainset:size()
    return self.data:size(1)
end
mean = {} -- store the mean, to normalize the test set in the future
stdv  = {} -- store the standard-deviation for the future
for i=1,3 do -- over each image channel
    mean[i] = trainset.data[{ {}, {i}, {}, {}  }]:mean() -- mean estimation
    print('Channel ' .. i .. ', Mean: ' .. mean[i])
    trainset.data[{ {}, {i}, {}, {}  }]:add(-mean[i]) -- mean subtraction
    stdv[i] = trainset.data[{ {}, {i}, {}, {}  }]:std() -- std estimation
    print('Channel ' .. i .. ', Standard Deviation: ' .. stdv[i])
    trainset.data[{ {}, {i}, {}, {}  }]:div(stdv[i]) -- std scaling
end
net = nn.Sequential()
net:add(nn.SpatialConvolution(3, 6, 5, 5)) -- 3 input image channels, 6 output channels, 5x5 convolution kernel
net:add(nn.ReLU())                       -- non-linearity
net:add(nn.SpatialMaxPooling(2,2,2,2))     -- A max-pooling operation that looks at 2x2 windows and finds the max.
net:add(nn.SpatialConvolution(6, 16, 5, 5))
net:add(nn.ReLU())                       -- non-linearity
net:add(nn.SpatialMaxPooling(2,2,2,2))
net:add(nn.View(16*5*5))                    -- reshapes from a 3D tensor of 16x5x5 into 1D tensor of 16*5*5
net:add(nn.Linear(16*5*5, 120))             -- fully connected layer (matrix multiplication between input and weights)
net:add(nn.ReLU())                       -- non-linearity
net:add(nn.Linear(120, 84))
net:add(nn.ReLU())                       -- non-linearity
net:add(nn.Linear(84, 10))                   -- 10 is the number of outputs of the network (in this case, 10 digits)
net:add(nn.LogSoftMax())                     -- converts the output to a log-probability. Useful for classification problems
criterion = nn.ClassNLLCriterion()
trainer = nn.StochasticGradient(net, criterion)
trainer.learningRate = 0.001
trainer.maxIteration = 5
trainer:train(trainset)
testset.data = testset.data:double()   -- convert from Byte tensor to Double tensor
for i=1,3 do -- over each image channel
    testset.data[{ {}, {i}, {}, {}  }]:add(-mean[i]) -- mean subtraction
    testset.data[{ {}, {i}, {}, {}  }]:div(stdv[i]) -- std scaling
end
predicted = net:forward(testset.data[100])
print(classes[testset.label[100]])
print(predicted:exp())
for i=1,predicted:size(1) do
    print(classes[i], predicted[i])
end
correct = 0
for i=1,10000 do
    local groundtruth = testset.label[i]
    local prediction = net:forward(testset.data[i])
    local confidences, indices = torch.sort(prediction, true)  -- true means sort in descending order
    if groundtruth == indices[1] then
        correct = correct + 1
    end
end
print(correct, 100*correct/10000 .. ' % ')
class_performance = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
for i=1,10000 do
    local groundtruth = testset.label[i]
    local prediction = net:forward(testset.data[i])
    local confidences, indices = torch.sort(prediction, true)  -- true means sort in descending order
    if groundtruth == indices[1] then
        class_performance[groundtruth] = class_performance[groundtruth] + 1
    end
end
for i=1,#classes do
    print(classes[i], 100*class_performance[i]/1000 .. ' %')
end

 

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

执行th pattern_recognition.lua,效果如下:

Channel 1, Mean: 125.83175029297
Channel 1, Standard Deviation: 63.143400842609
Channel 2, Mean: 123.26066621094
Channel 2, Standard Deviation: 62.369209019002
Channel 3, Mean: 114.03068681641
Channel 3, Standard Deviation: 66.965808411114
# StochasticGradient: training
# current error = 2.1494370275157
# current error = 1.8157547050302
# current error = 1.6500230258568
# current error = 1.5525866306324
# current error = 1.4768901429074
# StochasticGradient: you have reached the maximum number of iterations
# training error = 1.4768901429074
horse
 0.0150
 0.0034
 0.0350
 0.0490
 0.0294
 0.0942
 0.0161
 0.6958
 0.0030
 0.0591
[torch.DoubleTensor of size 10]
airplane	0.014967687525305
automobile	0.0034308404388647
bird	0.034966173679979
cat	0.04902915779719
deer	0.029421766137696
dog	0.094170890240524
frog	0.016105971372693
horse	0.69584966710414
ship	0.0029695320731486
truck	0.059088313630455
4519	45.19 %
airplane	61.3 %
automobile	40.6 %
bird	30.5 %
cat	22 %
deer	16.3 %
dog	47.4 %
frog	62 %
horse	61.4 %
ship	41.1 %
truck	69.3 %

 

 

解释一下

首先是下载cifar10torchsmall.zip样本,里面有50000张训练用的图片,10000张测试用的图片,分别都标注了是什么内容,包括airplane、automobile等10种分类,然后对trainset绑定__index和size方法,以兼容nn.Sequential的使用,有关绑定函数的内容可以看这个15分钟lua教程:http://tylerneylon.com/a/learn-lua/,然后对trainset数据做正规化,把数据都转成均值为1方差为1的double类型的张量

然后就是初始化卷积神经网络模型,这个神经网络模型包括了了两层卷积、两层池化、一个全连接以及一个softmax层,接着进行训练,学习率为0.001,迭代5次,模型训练好后对测试机中的第100号图片做预测,然后就是打印出整体的正确率以及每种分类的准确率

 

也可以参考https://github.com/soumith/cvpr2015/blob/master/Deep%20Learning%20with%20Torch.ipynb

 

torch可以方便的支持gpu计算,如果使用gpu需要对代码做修改,这是与tensorflow相比的不足之处,后续就是继续学习基于torch有关lstm代码的内容了,有收获会随时发文

自己动手做聊天机器人 三十三-两套代码详解LSTM-RNN——有记忆的神经网络

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

掌握LSTM有什么用

在读代码之前我们要先知道我们读这段代码的意义是什么。LSTM(Long Short Tem Memory)是一种特殊递归神经网络,它的特殊性在于它的神经元的设计能够保存历史记忆,这样可以解决自然语言处理的统计方法中只能考虑最近n个词语而忽略了更久之前的词语的问题。它的用途有:word representation(embedding)(也就是怎么样把词语表达成向量)、sequence to sequence learning(也就是输入一个句子能预测另一个句子)、以及机器翻译、语音识别等

 

100多行原始python代码实现的基于LSTM的二进制加法器

这份代码来源于https://iamtrask.github.io/2015/11/15/anyone-can-code-lstm/,国人也做了翻译http://blog.csdn.net/zzukun/article/details/49968129,这里我再次引用意在一边读一边学习一边理解,并从制作聊天机器人的目标上做一些扩充解读,下面我就一句一句的解释(没有缩进,请参考原始代码):

import copy, numpy as np
np.random.seed(0)

最开始引入了numpy库,这是为了矩阵操作的方便

def sigmoid(x):
    output = 1/(1+np.exp(-x))
    return output

这是声明sigmoid激活函数,这是神经网络的基础内容,常用的激活函数有sigmoid、tan、relu等,sigmoid的取值范围是[0, 1],tan的取值范围是[-1,1],这里的x是一个向量,那么返回的output也是一个向量

def sigmoid_output_to_derivative(output):
    return output*(1-output)

这是声明sigmoid的求导函数,那么这个求导函数怎么来的呢?请看下面的推导你就明白了

在这里先说一下这个加法器的思路:二进制的加法是一个一个二进制位相加,同时会记录一个满二进一的进位,那么训练时,随机找个c=a+b就是一个样本,输入a、b输出c就是整个lstm的预测过程,我们要训练的就是由a、b的二进制向c转换的各种转换矩阵和权重等,也就是我们要设计的神经网络

int2binary = {}

这里声明了一个词典,用于由整型数字转成二进制,这样存起来是为了不用随时计算,提前存好能够使得读取更快

binary_dim = 8
largest_number = pow(2,binary_dim)

这是声明二进制数字的维度,如果是8,那么二进制能表达的最大整数是2^8=256,也就是这里的largest_number

binary = np.unpackbits(
                       np.array([range(largest_number)],dtype=np.uint8).T,axis=1)
for i in range(largest_number):
    int2binary[i] = binary[i]

这就是所说的预先把整数到二进制的转换词典存起来

alpha = 0.1
input_dim = 2
hidden_dim = 16
output_dim = 1

这里设置了几个参数,alpha是学习速度,input_dim是输入层向量的维度,因为输入a、b两个数,所以是2,hidden_dim是隐藏层向量的维度,也就是隐藏层神经元的个数,output_dim是输出层向量的维度,因为输出一个c,所以是1维。这样从输入层到隐藏层的权重矩阵一定是2*16维的,从隐藏层到输出层的权重矩阵一定是16*1维的,而隐藏层到隐藏层的权重矩阵一定是16*16维的,也就是如下:

synapse_0 = 2*np.random.random((input_dim,hidden_dim)) - 1
synapse_1 = 2*np.random.random((hidden_dim,output_dim)) - 1
synapse_h = 2*np.random.random((hidden_dim,hidden_dim)) - 1

这里用2x-1是因为np.random.random会生成从0到1之间的随机浮点数,那么2x-1就是使其取值范围在[-1, 1]

synapse_0_update = np.zeros_like(synapse_0)
synapse_1_update = np.zeros_like(synapse_1)
synapse_h_update = np.zeros_like(synapse_h)

这是声明了三个矩阵的更新,也就是δ,后面会用到

for j in range(10000):

这表示我们要对下面的过程进行10000次迭代

a_int = np.random.randint(largest_number/2)
a = int2binary[a_int]
b_int = np.random.randint(largest_number/2)
b = int2binary[b_int]
c_int = a_int + b_int
c = int2binary[c_int]

这里其实是随机生成一个样本,这个样本包含二进制的a、b、c,其中c=a+b,a_int、b_int、c_int分别是是a、b、c对应的整数格式

d = np.zeros_like(c)

这个d在后面用来存我们模型对c的预测值

overallError = 0

这个是全局误差,用来观察模型效果

layer_2_deltas = list()

这用来存储第二层(也就是输出层)的残差,对于输出层,残差计算公式推导如下(公式可以在http://deeplearning.stanford.edu/wiki/index.php/%E5%8F%8D%E5%90%91%E4%BC%A0%E5%AF%BC%E7%AE%97%E6%B3%95找到):

这个公式下面会用到

layer_1_values = list()
layer_1_values.append(np.zeros(hidden_dim))

这用来存储第一层(也就是隐藏层)的输出值,首先我们赋0值作为上一个时间的值

for position in range(binary_dim):

这里我们来遍历二进制的每一位

X = np.array([[a[binary_dim - position - 1],b[binary_dim - position - 1]]])
y = np.array([[c[binary_dim - position - 1]]]).T

这里的X和y分别是样本的输入和输出的二进制值的第position位,其中X对于每个样本有两个值,分别是a和b对应的第position位。把样本拆成每个二进制位用于训练是因为二进制加法中存在进位标记正好适合利用LSTM的长短期记忆来训练,每个样本的8个二进制位刚好是一个时间序列

layer_1 = sigmoid(np.dot(X,synapse_0) + np.dot(layer_1_values[-1],synapse_h))

这里使用的公式是Ct = σ(W0·Xt + Wh·Ct-1)

layer_2 = sigmoid(np.dot(layer_1,synapse_1))

这里使用的公式是C2 = σ(W1·C1)

layer_2_error = y - layer_2

这里计算预测值和真实值的误差

layer_2_deltas.append((layer_2_error)*sigmoid_output_to_derivative(layer_2))

这里开始反向传导的过程,通过如下公式计算delta(上面提到的公式用在这里),并添加到数组layer_2_deltas中,此数组对于每个二进制位(position)有一个值

overallError += np.abs(layer_2_error[0])

这里计算累加总误差,后面用于展示和观察

d[binary_dim - position - 1] = np.round(layer_2[0][0])

这里存储预测出来的position位的输出值

layer_1_values.append(copy.deepcopy(layer_1))

这里存储中间过程生成的隐藏层的值

future_layer_1_delta = np.zeros(hidden_dim)

这用来存储用于下一个时间周期用到的隐藏层的历史记忆值,还是先赋一个空值

for position in range(binary_dim):

这里我们再次遍历二进制的每一位

X = np.array([[a[position],b[position]]])

和前面一样取出X的值,不同的是我们从大位开始做更新,因为反向传导是按时序逆着一级一级更新的

layer_1 = layer_1_values[-position-1]

取出这一位对应隐藏层的输出

prev_layer_1 = layer_1_values[-position-2]

取出这一位对应隐藏层的上一时序的输出

layer_2_delta = layer_2_deltas[-position-1]

取出这一位对应输出层的delta

layer_1_delta = (future_layer_1_delta.dot(synapse_h.T) + layer_2_delta.dot(synapse_1.T)) * sigmoid_output_to_derivative(layer_1)

这里其实是基于下面的神经网络反向传导公式,并额外加上了隐藏层的δ值得出的

synapse_1_update += np.atleast_2d(layer_1).T.dot(layer_2_delta)

这里累加权重矩阵的更新,基于下面的公式,也就是对权重(权重矩阵)的偏导等于本层的输出与下一层的delta的点乘

synapse_h_update += np.atleast_2d(prev_layer_1).T.dot(layer_1_delta)

对前一时序的隐藏层权重矩阵的更新和上面公式类似,只不过改成前一时序的隐藏层输出与本时序的delta的点乘

synapse_0_update += X.T.dot(layer_1_delta)

对输入层权重矩阵的更新也是类似

future_layer_1_delta = layer_1_delta

记录下本时序的隐藏层的delta用来在下一时序使用

synapse_0 += synapse_0_update * alpha
synapse_1 += synapse_1_update * alpha
synapse_h += synapse_h_update * alpha

对权重矩阵做更新

synapse_0_update *= 0
synapse_1_update *= 0
synapse_h_update *= 0

更新变量归零

if(j % 1000 == 0):
        print "Error:" + str(overallError)
        print "Pred:" + str(d)
        print "True:" + str(c)
        out = 0
        for index,x in enumerate(reversed(d)):
            out += x*pow(2,index)
        print str(a_int) + " + " + str(b_int) + " = " + str(out)
        print "------------"

这里在每训练了1000个样本后输出总误差信息,运行时可以看到收敛的过程

这套代码可以说是LSTM最简单的实现,没有考虑偏置变量,只有两个神经元,因此可以作为一个demo来学习,相比来说下面这个LSTM就是一个比较完善的实现了,几乎是按照论文great intro paper描述的完整实现

 

另一个完整的LSTM的python实现

这个实现比上一个更符合LSTM,完全参照论文great intro paper来实现的,不到200行代码,原始代码来源于https://github.com/nicodjimenez/lstm,作者解释在http://nicodjimenez.github.io/2014/08/08/lstm.html,具体过程还可以参考http://colah.github.io/posts/2015-08-Understanding-LSTMs/中的图来理解。下面我就一句一句的解释(具体缩进请参考原始代码):

import random
import numpy as np
import math
def sigmoid(x):
    return 1. / (1 + np.exp(-x))

这里同样是声明sigmoid函数

def rand_arr(a, b, *args):
    np.random.seed(0)
    return np.random.rand(*args) * (b - a) + a

用于生成随机矩阵,取值范围是[a,b),shape用args指定

class LstmParam:
    def __init__(self, mem_cell_ct, x_dim):
        self.mem_cell_ct = mem_cell_ct
        self.x_dim = x_dim
        concat_len = x_dim + mem_cell_ct
        # weight matrices
        self.wg = rand_arr(-0.1, 0.1, mem_cell_ct, concat_len)
        self.wi = rand_arr(-0.1, 0.1, mem_cell_ct, concat_len)
        self.wf = rand_arr(-0.1, 0.1, mem_cell_ct, concat_len)
        self.wo = rand_arr(-0.1, 0.1, mem_cell_ct, concat_len)
        # bias terms
        self.bg = rand_arr(-0.1, 0.1, mem_cell_ct)
        self.bi = rand_arr(-0.1, 0.1, mem_cell_ct)
        self.bf = rand_arr(-0.1, 0.1, mem_cell_ct)
        self.bo = rand_arr(-0.1, 0.1, mem_cell_ct)
        # diffs (derivative of loss function w.r.t. all parameters)
        self.wg_diff = np.zeros((mem_cell_ct, concat_len))
        self.wi_diff = np.zeros((mem_cell_ct, concat_len))
        self.wf_diff = np.zeros((mem_cell_ct, concat_len))
        self.wo_diff = np.zeros((mem_cell_ct, concat_len))
        self.bg_diff = np.zeros(mem_cell_ct)
        self.bi_diff = np.zeros(mem_cell_ct)
        self.bf_diff = np.zeros(mem_cell_ct)
        self.bo_diff = np.zeros(mem_cell_ct)

LstmParam类用于传递相关参数,其中mem_cell_ct是lstm的神经元数目,x_dim是输入数据的维度,concat_len是mem_cell_ct与x_dim的长度和,wg是输入节点的权重矩阵(这里的g不要理解为gate,原始论文里有解释),wi是输入门的权重矩阵,wf是忘记门的权重矩阵,wo是输出门的权重矩阵,bg、bi、bf、bo分别是输入节点、输入门、忘记门、输出门的偏置,wg_diff、wi_diff、wf_diff、wo_diff分别是输入节点、输入门、忘记门、输出门的权重损失,bg_diff、bi_diff、bf_diff、bo_diff分别是输入节点、输入门、忘记门、输出门的偏置损失,这里初始化时会按照矩阵维度初始化,并把损失矩阵归零

    def apply_diff(self, lr = 1):
        self.wg -= lr * self.wg_diff
        self.wi -= lr * self.wi_diff
        self.wf -= lr * self.wf_diff
        self.wo -= lr * self.wo_diff
        self.bg -= lr * self.bg_diff
        self.bi -= lr * self.bi_diff
        self.bf -= lr * self.bf_diff
        self.bo -= lr * self.bo_diff
        # reset diffs to zero
        self.wg_diff = np.zeros_like(self.wg)
        self.wi_diff = np.zeros_like(self.wi)
        self.wf_diff = np.zeros_like(self.wf)
        self.wo_diff = np.zeros_like(self.wo)
        self.bg_diff = np.zeros_like(self.bg)
        self.bi_diff = np.zeros_like(self.bi)
        self.bf_diff = np.zeros_like(self.bf)
        self.bo_diff = np.zeros_like(self.bo)

这里定义了权重的更新过程,先减掉损失,再把损失矩阵归零

class LstmState:
    def __init__(self, mem_cell_ct, x_dim):
        self.g = np.zeros(mem_cell_ct)
        self.i = np.zeros(mem_cell_ct)
        self.f = np.zeros(mem_cell_ct)
        self.o = np.zeros(mem_cell_ct)
        self.s = np.zeros(mem_cell_ct)
        self.h = np.zeros(mem_cell_ct)
        self.bottom_diff_h = np.zeros_like(self.h)
        self.bottom_diff_s = np.zeros_like(self.s)
        self.bottom_diff_x = np.zeros(x_dim)

LstmState用于存储LSTM神经元的状态,这里包括g、i、f、o、s、h,其中s是内部状态矩阵(可以理解为记忆),h是隐藏层神经元的输出矩阵

class LstmNode:
    def __init__(self, lstm_param, lstm_state):
        # store reference to parameters and to activations
        self.state = lstm_state
        self.param = lstm_param
        # non-recurrent input to node
        self.x = None
        # non-recurrent input concatenated with recurrent input
        self.xc = None

一个LstmNode对应一个样本的输入,其中x就是输入样本的x,xc是用hstack把x和递归的输入节点拼接出来的矩阵(hstack是横着拼成矩阵,vstack就是纵着拼成矩阵)

    def bottom_data_is(self, x, s_prev = None, h_prev = None):
        # if this is the first lstm node in the network
        if s_prev == None: s_prev = np.zeros_like(self.state.s)
        if h_prev == None: h_prev = np.zeros_like(self.state.h)
        # save data for use in backprop
        self.s_prev = s_prev
        self.h_prev = h_prev
        # concatenate x(t) and h(t-1)
        xc = np.hstack((x,  h_prev))
        self.state.g = np.tanh(np.dot(self.param.wg, xc) + self.param.bg)
        self.state.i = sigmoid(np.dot(self.param.wi, xc) + self.param.bi)
        self.state.f = sigmoid(np.dot(self.param.wf, xc) + self.param.bf)
        self.state.o = sigmoid(np.dot(self.param.wo, xc) + self.param.bo)
        self.state.s = self.state.g * self.state.i + s_prev * self.state.f
        self.state.h = self.state.s * self.state.o
        self.x = x
        self.xc = xc

这里的bottom和后面遇到的top分别是两个方向,输入样本时认为是从底部输入,反向传导时认为是从顶部开始向底部传导,这里的bottom_data_is是输入样本的过程,首先把x和先前的输入拼接成矩阵,然后用公式wx+b分别计算g、i、f、o的值的值,这里面的激活函数有tanh和sigmoid,实际上可以看下面的图来理解,先说代码里g和i的计算的示意图:

这里的[ht-1,xt]其实就是np.hstack((x,  h_prev)),这里的Ct其实就是g

再看代码里f的计算示意图:

再看代码里s的计算示意图(s对应的是图里的C):

再看代码里o和h的计算示意图(这里的h的计算公式和代码里有不同,不确定作者为什么要修改这一部分,我们可以尝试按照论文来修改看效果哪个更好):

所以其实整个就是这样一个过程:

每个时序的神经网络可以理解为有四个神经网络层(图中黄色的激活函数部分),最左边的是忘记门,它直接生效到记忆C上,第二个是输入门,它主要依赖于输入的样本数据,之后按照一定“比例”影响记忆C,这里的“比例”是通过第三个层(tanh)来实现的,因为它取值范围是[-1,1]可以是正向影响也可以是负向影响,最后一个是输出门,每一时序产生的输出既依赖于输入的样本x和上一时序的输出,还依赖于记忆C,整个设计几乎模仿了生物神经元的记忆功能,应该容易理解。

请尊重原创,转载请注明来源网站www.lcsays.com以及原始链接地址

    def top_diff_is(self, top_diff_h, top_diff_s):
        # notice that top_diff_s is carried along the constant error carousel
        ds = self.state.o * top_diff_h + top_diff_s
        do = self.state.s * top_diff_h
        di = self.state.g * ds
        dg = self.state.i * ds
        df = self.s_prev * ds
        # diffs w.r.t. vector inside sigma / tanh function
        di_input = (1. - self.state.i) * self.state.i * di
        df_input = (1. - self.state.f) * self.state.f * df
        do_input = (1. - self.state.o) * self.state.o * do
        dg_input = (1. - self.state.g ** 2) * dg
        # diffs w.r.t. inputs
        self.param.wi_diff += np.outer(di_input, self.xc)
        self.param.wf_diff += np.outer(df_input, self.xc)
        self.param.wo_diff += np.outer(do_input, self.xc)
        self.param.wg_diff += np.outer(dg_input, self.xc)
        self.param.bi_diff += di_input
        self.param.bf_diff += df_input
        self.param.bo_diff += do_input
        self.param.bg_diff += dg_input
        # compute bottom diff
        dxc = np.zeros_like(self.xc)
        dxc += np.dot(self.param.wi.T, di_input)
        dxc += np.dot(self.param.wf.T, df_input)
        dxc += np.dot(self.param.wo.T, do_input)
        dxc += np.dot(self.param.wg.T, dg_input)
        # save bottom diffs
        self.state.bottom_diff_s = ds * self.state.f
        self.state.bottom_diff_x = dxc[:self.param.x_dim]
        self.state.bottom_diff_h = dxc[self.param.x_dim:]

这里是反向传导的过程,这个过程是整个训练过程的核心,如果不了解原理,这部分是不可能理解的,所以我们首先了解一下公式推导:

假设在t时刻lstm输出的预测值为h(t),而实际的输出值是y(t),那么他们之间的差别就是损失,我们假设损失函数为l(t) = f(h(t), y(t)) = ||h(t) - y(t)||^2,也就是欧式距离,那么整体损失函数就是

L(t) = ∑l(t),其中t从1到T,T表示整个事件序列的最大长度

我们的最终目标就是用梯度下降法来让L(t)最小化,也就是找到一个最优的权重w使得L(t)最小,那么权重w在什么情况下L(t)最小呢?就是在:当w发生微小变化时L(t)不再变化,也就是达到局部最优,即L对w的偏导也就是梯度为0

下面我们来分析L对w的微分怎么算

这个公式这样来理解:dL/dw表示当w发生单位变化时L变化了多少,dh(t)/dw表示当w发生单位变化时h(t)变化了多少,dL/dh(t)表示当h(t)发生单位变化时L变化了多少,那么(dL/dh(t)) * (dh(t)/dw)就表示对于第t时序第i个记忆单元当w发生单位变化时L变化了多少,那么把所有由1到M的i和所有由1到T的t累加起来自然就是整体上的dL/dw

下面单独来看左边的dL/dh(t),它其实可以写成

右面表示:对于第i个记忆单元,当h(t)发生单位变化时,整个从1到T的时序上所有局部损失l的累加和,这其实刚好就是dL/dh(t),那么实际上h(t)只会影响从t到T这段时序上的局部损失l,所以式子可以写成

下面我们假设L(t)表示从t到T的损失和,那么有L(t) = ∑l(s),其中s由t到T,那么我们上面的式子可以写成

那么我们上面的梯度公式就可以写成

右边其实就是h(t)对w的导数,下面我们来专门看左边的式子

我们知道L(t) = l(t) + L(t+1),那么dL(t)/dh(t) = dl(t)/dh(t) + dL(t+1)/dh(t),右边的式子其实就是LSTM带有记忆的真谛之所在,也就是用下一时序的导数可以得出当前时序的导数,那么我们就按照这个规律来做推导,首先我们计算T时刻的导数然后往前推,在T时刻,dL(T)/dh(T) = dl(T)/dh(T)

为了方便理解,我们暂时先略过上面那段top_diff_is,看之后的一段

class LstmNetwork():
    def __init__(self, lstm_param):
        self.lstm_param = lstm_param
        self.lstm_node_list = []
        # input sequence
        self.x_list = []
    def y_list_is(self, y_list, loss_layer):
        """
        Updates diffs by setting target sequence
        with corresponding loss layer.
        Will *NOT* update parameters.  To update parameters,
        call self.lstm_param.apply_diff()
        """
        assert len(y_list) == len(self.x_list)
        idx = len(self.x_list) - 1
        # first node only gets diffs from label ...
        loss = loss_layer.loss(self.lstm_node_list[idx].state.h, y_list[idx])
        diff_h = loss_layer.bottom_diff(self.lstm_node_list[idx].state.h, y_list[idx])
        # here s is not affecting loss due to h(t+1), hence we set equal to zero
        diff_s = np.zeros(self.lstm_param.mem_cell_ct)
        self.lstm_node_list[idx].top_diff_is(diff_h, diff_s)
        idx -= 1
        ### ... following nodes also get diffs from next nodes, hence we add diffs to diff_h
        ### we also propagate error along constant error carousel using diff_s
        while idx >= 0:
            loss += loss_layer.loss(self.lstm_node_list[idx].state.h, y_list[idx])
            diff_h = loss_layer.bottom_diff(self.lstm_node_list[idx].state.h, y_list[idx])
            diff_h += self.lstm_node_list[idx + 1].state.bottom_diff_h
            diff_s = self.lstm_node_list[idx + 1].state.bottom_diff_s
            self.lstm_node_list[idx].top_diff_is(diff_h, diff_s)
            idx -= 1
        return loss

重点关注这里的diff_h(表达的是预测结果误差发生单位变化时损失L是多少,也就相当于公式中的dL(t)/dh(t)的一个数值计算),我们看到这里的过程是由idx从T往前遍历到1,计算loss_layer.bottom_diff和下一个时序的bottom_diff_h的和作为diff_h(其中第一次遍历即T时不加bottom_diff_h和公式一样)

其中loss_layer.bottom_diff的实现如下:

    def bottom_diff(self, pred, label):
        diff = np.zeros_like(pred)
        diff[0] = 2 * (pred[0] - label)
        return diff

这里求的就是l(t) = f(h(t), y(t)) = ||h(t) - y(t)||^2的导数l'(t) = 2 * (h(t) - y(t))

与上面推导dL(t)/dh(t) 的过程类似,我们来推导dL(t)/ds(t) 。当s(t)发生变化时,L(t)的变化来源于s(t)影响了h(t)和h(t+1),从而影响了L(t),即

因为h(t+1)不会影响l(t),所以

因此有

这里如果能求得左边的式子(dL(t)/dh(t)) * (dh(t)/ds(t)),那么就可以由t+1到t来逐级反推dL(t)/ds(t)了

下面我们就来看怎么来计算左边的式子

因为我们神经元设计有:self.state.h = self.state.s * self.state.o也就是h(t) = s(t) * o(t),那么dh(t)/ds(t) = o(t),而因为dL(t)/dh(t)就是top_diff_h,所以有:

所以得到

这回可以回来看top_diff_is代码了,我自己读到这里的top和bottom的时候,没明白含义,所以特地咨询了代码原作者,他的解释是:Bottom means input to the layer, top means output of the layer. Caffe also uses this terminology.也就是说bottom表示神经网络层的输入,top表示神经网络层的输出,和caffe中概念一致

def top_diff_is(self, top_diff_h, top_diff_s):

首先看传递过来的两个参数:top_diff_h表示当前t时序的dL(t)/dh(t), top_diff_s表示t+1时序记忆单元的dL(t)/ds(t)

        ds = self.state.o * top_diff_h + top_diff_s
        do = self.state.s * top_diff_h
        di = self.state.g * ds
        dg = self.state.i * ds
        df = self.s_prev * ds

这里面的前缀d表达的是误差L对某一项的导数(directive)

其中ds一行是在根据上面的公式dL(t)/ds(t)计算当前t时序的dL(t)/ds(t)

其中do一行是计算dL(t)/do(t),因为h(t) = s(t) * o(t),所以dh(t)/do(t) = s(t),所以dL(t)/do(t) = (dL(t)/dh(t)) * (dh(t)/do(t)) = top_diff_h * s(t)

其中di一行是计算dL(t)/di(t),考虑到

换个符号表示就是

s(t) = f(t) * s(t-1) + i(t) * g(t)

所以dL(t)/di(t) = (dL(t)/ds(t)) * (ds(t)/di(t)) = ds * g(t)

其中dg一行是计算dL(t)/dg(t),同上有dL(t)/dg(t) = (dL(t)/ds(t)) * (ds(t)/dg(t)) = ds * i(t)

其中df一行是计算dL(t)/df(t),同上有dL(t)/df(t) = (dL(t)/ds(t)) * (ds(t)/df(t)) = ds * s(t-1)

        di_input = (1. - self.state.i) * self.state.i * di
        df_input = (1. - self.state.f) * self.state.f * df
        do_input = (1. - self.state.o) * self.state.o * do
        dg_input = (1. - self.state.g ** 2) * dg

前三行用了sigmoid函数的导数,后一行用了tanh函数的导数。以第一行di_input为例,(1. - self.state.i) * self.state.i是sigmoid导数,表示当i神经元的输入发生单位变化时输出值有多大变化,那么再乘以di就表示当i神经元的输入发生单位变化时误差L(t)发生多大变化,也就是dL(t)/d i_input(t),下面几个都同理

        self.param.wi_diff += np.outer(di_input, self.xc)
        self.param.wf_diff += np.outer(df_input, self.xc)
        self.param.wo_diff += np.outer(do_input, self.xc)
        self.param.wg_diff += np.outer(dg_input, self.xc)
        self.param.bi_diff += di_input
        self.param.bf_diff += df_input
        self.param.bo_diff += do_input
        self.param.bg_diff += dg_input

这里的w*_diff是权重矩阵的误差,b*_diff是偏置的误差,用于做更新,这里面为什么是d*_input和xc的外积没看懂

        dxc = np.zeros_like(self.xc)
        dxc += np.dot(self.param.wi.T, di_input)
        dxc += np.dot(self.param.wf.T, df_input)
        dxc += np.dot(self.param.wo.T, do_input)
        dxc += np.dot(self.param.wg.T, dg_input)

这里是在累加输入x的diff,因为x在四处起作用,所以四处的diff加和之后才算作x的diff

        self.state.bottom_diff_s = ds * self.state.f
        self.state.bottom_diff_x = dxc[:self.param.x_dim]
        self.state.bottom_diff_h = dxc[self.param.x_dim:]

这里bottom_diff_s是根据如下公式得出的推导关系,也就是在t-1时序上s的变化和t时序上s的变化时f倍的关系

因为dxc是x和h横向合并出来的矩阵,所以分别取出两部分的diff信息就是bottom_diff_x和bottom_diff_h(这里面的bottom_diff_x代码里面没有真正作用)

    def x_list_clear(self):
        self.x_list = []
    def x_list_add(self, x):
        self.x_list.append(x)
        if len(self.x_list) > len(self.lstm_node_list):
            # need to add new lstm node, create new state mem
            lstm_state = LstmState(self.lstm_param.mem_cell_ct, self.lstm_param.x_dim)
            self.lstm_node_list.append(LstmNode(self.lstm_param, lstm_state))
        # get index of most recent x input
        idx = len(self.x_list) - 1
        if idx == 0:
            # no recurrent inputs yet
            self.lstm_node_list[idx].bottom_data_is(x)
        else:
            s_prev = self.lstm_node_list[idx - 1].state.s
            h_prev = self.lstm_node_list[idx - 1].state.h
            self.lstm_node_list[idx].bottom_data_is(x, s_prev, h_prev)

这一部分是添加训练样本的过程,也就是输入x数据,那么整个执行过程如下:

def example_0():
    # learns to repeat simple sequence from random inputs
    np.random.seed(0)
    # parameters for input data dimension and lstm cell count
    mem_cell_ct = 100
    x_dim = 50
    concat_len = x_dim + mem_cell_ct
    lstm_param = LstmParam(mem_cell_ct, x_dim)
    lstm_net = LstmNetwork(lstm_param)
    y_list = [-0.5,0.2,0.1, -0.5]
    input_val_arr = [np.random.random(x_dim) for _ in y_list]
    for cur_iter in range(100):
        print "cur iter: ", cur_iter
        for ind in range(len(y_list)):
            lstm_net.x_list_add(input_val_arr[ind])
            print "y_pred[%d] : %f" % (ind, lstm_net.lstm_node_list[ind].state.h[0])
        loss = lstm_net.y_list_is(y_list, ToyLossLayer)
        print "loss: ", loss
        lstm_param.apply_diff(lr=0.1)
        lstm_net.x_list_clear()

首先初始化LstmParam,指定记忆存储单元数为100,指定输入样本x维度是50,下面初始化一个LstmNetwork用于训练模型,然后生成了4组各50个随机数,并分别以[-0.5,0.2,0.1, -0.5]作为y值来训练,每次喂给50个随机数和一个y值,共迭代100次

这个测试样例最终执行效果自己来尝试吧

 

利用lstm在输入一串连续质数时预估下一个质数

为了测试这个强大的lstm,我写了这样一个小测试,首先生成100以内质数,然后循环地拿出50个质数序列作为x,第51个质数作为y,这样拿出10个样本参与训练1w次,均方误差由一开始的0.17973最终达到了1.05172e-06,几乎完全正确,效果相当赞啊,程序如下:

import numpy as np
import sys
from lstm import LstmParam, LstmNetwork
class ToyLossLayer:
    """
    Computes square loss with first element of hidden layer array.
    """
    @classmethod
    def loss(self, pred, label):
        return (pred[0] - label) ** 2
    @classmethod
    def bottom_diff(self, pred, label):
        diff = np.zeros_like(pred)
        diff[0] = 2 * (pred[0] - label)
        return diff
class Primes:
    def __init__(self):
        self.primes = list()
        for i in range(2, 100):
            is_prime = True
            for j in range(2, i-1):
                if i % j == 0:
                    is_prime = False
            if is_prime:
                self.primes.append(i)
        self.primes_count = len(self.primes)
    def get_sample(self, x_dim, y_dim, index):
        result = np.zeros((x_dim+y_dim))
        for i in range(index, index + x_dim + y_dim):
            result[i-index] = self.primes[i%self.primes_count]/100.0
        return result

def example_0(): mem_cell_ct = 100 x_dim = 50 concat_len = x_dim + mem_cell_ct lstm_param = LstmParam(mem_cell_ct, x_dim) lstm_net = LstmNetwork(lstm_param) primes = Primes() x_list = [] y_list = [] for i in range(0, 10): sample = primes.get_sample(x_dim, 1, i) x = sample[0:x_dim] y = sample[x_dim:x_dim+1].tolist()[0] x_list.append(x) y_list.append(y) for cur_iter in range(10000): if cur_iter % 1000 == 0: print “y_list=”, y_list for ind in range(len(y_list)): lstm_net.x_list_add(x_list[ind]) if cur_iter % 1000 == 0: print “y_pred[%d] : %f” % (ind, lstm_net.lstm_node_list[ind].state.h[0]) loss = lstm_net.y_list_is(y_list, ToyLossLayer) if cur_iter % 1000 == 0: print “loss: “, loss lstm_param.apply_diff(lr=0.01) lstm_net.x_list_clear() if name == “main": example_0()

最终的运行效果最后一次迭代结果输出如下(注:这里的质数列表我全都除以了100,因为这个代码训练的数据必须是小于1的数值),可以看到预测基本上正确:

y_list= [0.02, 0.03, 0.05, 0.07, 0.11, 0.13, 0.17, 0.19, 0.23, 0.29]
y_pred[0] : 0.019828
y_pred[1] : 0.030286
y_pred[2] : 0.049671
y_pred[3] : 0.070302
y_pred[4] : 0.109682
y_pred[5] : 0.130395
y_pred[6] : 0.169550
y_pred[7] : 0.190424
y_pred[8] : 0.229697
y_pred[9] : 0.290101
loss:  1.05172051911e-06