自己动手做聊天机器人 三十二-用三千万影视剧字幕语料库生成词向量

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

对语料库切词

因为word2vec的输入需要是切好词的文本文件,但我们的影视剧字幕语料库是回车换行分隔的完整句子,所以我们先对其做切词,有关中文切词的方法请见《教你成为全栈工程师(Full Stack Developer) 三十四-基于python的高效中文文本切词》,为了对影视剧字幕语料库切词,我们来创建word_segment.py文件,内容如下:

# coding:utf-8
import sys
reload(sys)
sys.setdefaultencoding( "utf-8" )
import jieba
from jieba import analyse
def segment(input, output):
    input_file = open(input, "r")
    output_file = open(output, "w")
    while True:
        line = input_file.readline()
        if line:
            line = line.strip()
            seg_list = jieba.cut(line)
            segments = ""
            for str in seg_list:
                segments = segments + " " + str
            output_file.write(segments)
        else:
            break
    input_file.close()
    output_file.close()
if __name__ == '__main__':
    if 3 != len(sys.argv):
        print "Usage: ", sys.argv[0], "input output"
        sys.exit(-1)
    segment(sys.argv[1], sys.argv[2]);

使用方法:

python word_segment.py subtitle/raw_subtitles/subtitle.corpus segment_result

生成的segment_result文件内容就像下面的样子:

……
这件 事 对不起 我 并 不 直率 只有 在 梦 中 才能 表白 在 思绪 错乱 之前 现在 只想 马上 见到 你 夜晚 在 月光 下 哭泣 深夜里 无法 打电话 给 你 纯情 的 我 该 怎么办 心 就 如 万花筒 一般 月亮 的 光芒 引导 着 我 与 你 相会 无数次 星座 闪耀 的 瞬间 来 占卜 恋爱 的 方向 同样 都 是 在 地球 上 出生 的 奇迹 的 罗曼史 我 相信 奇迹 的 罗曼史 小小 兔 在 哪 你 在 哪里 找到 了 吗 到处 都 没 看到 难道 被 敌人 … 总之 分头 寻找 继续 找 吧 暗黑 女王   Black   Lady 的 诞生 不 愉快 的 往日 回忆 会 在 内心深处 留下 创伤 想起 坏心肠 的 妈妈 及 冷酷 的 父亲 吧 是 青蛙 会 跌倒 的 谁 叫 你 不 听 妈妈 的话 不 可以 哭 妈妈 最 讨厌 了 爸爸   拉 我 起来 自己 站 起来 不向 你 伸出手 的 父母 就是 不爱 你 的 证据 是 你 自己 跌倒 不好 站 起来 快 回想起来 那些 更 可恨 的 事情 吧 怎么 一个 人站 着 发呆 今天 是 我 的 生日 生日 ? 对 呀 可是 爸
……

统计一下这个文件如下:

[root@centos $] ls -lh segment_result
-rw-r--r-- 1 lichuang staff 1.1G 10  9 18:59 segment_result
[root@centos $] wc segment_result
         0  191925623 1093268485 segment_result

0行是因为没有在行尾加回车符,一共是191925623列,1093268485个字符

 

用word2vec生成词向量

想了解word2vec的原理请见《自己动手做聊天机器人 二十五-google的文本挖掘深度学习工具word2vec的实现原理》,如果因为被墙获取不到word2vec可以从https://github.com/lcdevelop/ChatBotCourse/tree/master/word2vec获取,直接make编译好后会生成一些二进制文件,我要用到的是word2vec

执行

./word2vec -train ../segment_result -output vectors.bin -cbow 1 -size 200 -window 8 -negative 25 -hs 0 -sample 1e-4 -threads 20 -binary 1 -iter 15
Starting training using file ../segment_result
Vocab size: 260499
Words in train file: 191353657
Alpha: 0.039254  Progress: 21.50%  Words/thread/sec: 96.67k

生成的vectors.bin就是我们想要的词向量,只不过是二进制格式,这时我们可以用word2vec自带的distance工具来验证一下:

./distance vectors.bin
Enter word or sentence (EXIT to break): 漂亮
Word: 漂亮  Position in vocabulary: 722
                                              Word       Cosine distance
------------------------------------------------------------------------
                                         很漂亮		0.532610
                                            厉害		0.440603
                                            身材		0.430269
                                         beautiful		0.413831
                                               棒		0.410241
                                         帅呆了		0.409414
                                            干得		0.407550
                                            不错		0.402978
                                            好美		0.401329
                                            可爱		0.399667
                                         猕猴桃		0.391512
                                               好		0.388109
                                            配当		0.387999
                                            真棒		0.387924
                                         太棒了		0.384184
                                         真不错		0.377484
……

 

词向量二进制文件的格式及加载

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

word2vec生成的词向量二进制格式是这样的:

词数目(空格)向量维度
第一个词(空格)词向量(大小为200*sizeof(float))第二个词(空格)词向量(大小为200*sizeof(float))……

所以写了一个加载词向量二进制文件的python脚本,后面会用到,如下:

# coding:utf-8
import sys
import struct
import math
import numpy as np
reload(sys)
sys.setdefaultencoding( "utf-8" )
max_w = 50
float_size = 4
def load_vectors(input):
    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
    word_vector = {}
    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 = np.empty([200])
        for index in range(0, size):
            m = input_file.read(float_size)
            (weight,) = struct.unpack('f', m)
            vector[index] = weight
        # 将词及其对应的向量存到dict中
        word_vector[word.decode('utf-8')] = vector
    input_file.close()
    print "load vectors finish"
    return word_vector
if __name__ == '__main__':
    if 2 != len(sys.argv):
        print "Usage: ", sys.argv[0], "vectors.bin"
        sys.exit(-1)
    d = load_vectors(sys.argv[1])
    print d[u'真的']

运行方式如下:

python word_vectors_loader.py vectors.bin

效果如下:

begin load vectors
words = 49804
size = 200
load vectors finish
[-1.09570336  2.03501272  0.3151325   0.17603125  0.30261561  0.15273243
 -0.6409803   0.06317     0.20631203  0.22687016  0.59229285 -1.10883808
  1.12569952  0.16838464  1.27895844 -1.18480754  1.6270808  -2.62790298
  0.43835989 -0.21364243  0.05743926 -0.77541786 -0.19709823  0.33360079
  0.43415883 -1.28643405 -0.95402282  0.01350032 -0.20490573  0.80880177
 -1.47243023 -0.09673293  0.05514769  1.00915158 -0.11268988  0.68446255
  0.08493964  0.27009442  0.33748865 -0.03105624 -0.19079798  0.46264866
 -0.53616458 -0.35288206  0.76765436 -1.0328685   0.92285776 -0.97560757
  0.5561474  -0.05574715 -0.1951212   0.5258466  -0.07396954  1.42198348
  1.12321162  0.03646624 -1.54316568  0.34798017  0.64197171 -0.57232529
  0.14402699  1.75856864 -0.72602183 -1.37281013  0.73600221  0.4458617
 -1.32631493  0.25921029 -0.97459841 -1.4394536   0.18724895 -0.74114919
  1.50315142  0.56819481  0.37238419 -0.0501433   0.36490002 -0.14456141
 -0.15503241 -0.04504468  1.18127966  1.465729   -0.13834922 -0.1232961
 -0.14927664  0.67862391  2.46567917 -1.10682511  0.71275675  1.04118025
  0.23883103 -1.99175942  0.40641201  0.73883104 -0.37824577  0.88882846
 -0.87234962  0.71112823  0.33647302 -1.2701565  -1.15415645  1.41575384
 -2.01556969 -0.85669023 -0.0378141  -0.60975027  0.0738821   0.19649875
  0.02519603 -0.78310513  0.40809572  0.55079561  1.79861426 -0.01188554
 -0.14823757 -0.97098011 -2.75159121  1.52366722 -0.41585007  0.78664345
  0.43792239  1.03834045  1.18758595  0.18793568 -1.44434023 -1.55205989
  0.24251698  1.05706048 -1.52376628 -0.60226047 -0.41849345 -0.30082899
 -1.32461691  0.29701442  0.36680841 -0.72046149 -0.16455257 -0.02307599
 -0.74143982  0.10319671 -0.5436908  -0.85527682 -0.81110024 -1.14968359
 -1.45617366  0.57568634 -1.10673392 -0.48830599  1.38728273 -0.46238521
  1.40288961 -0.92997569  0.90154368  0.09381612 -0.61220604 -0.40820527
  1.2660408  -1.02075434  0.98662543  0.81696391  0.06962785  0.83282673
 -0.12462004  1.16540051  0.10254569  1.03875697  0.05073663  1.50608146
  0.49252063  0.09693919  0.38897502 -0.0673333  -0.30629408 -2.1759603
  0.5477249  -1.46633601  1.54695141 -0.83080739 -0.49649978  1.05921662
 -0.60124737 -0.72645563 -1.44115663 -0.6903789   0.38817915 -0.11854757
 -0.18087701 -0.41152322 -0.98559368 -1.46712041  1.63777673 -0.64418262
 -0.56800991  1.79656076 -0.80431151  0.99533188  0.06813133 -0.73489577
 -0.67567319  0.64855355]

 

总结

考虑到运行时间的关系,以上示例中多数是我抽取了部分语料数据,因此你真正执行时数值可能会不一样。至此,我们已经做到用影视剧字幕语料库(获取方式《自己动手做聊天机器人 二十九-重磅:近1GB的三千万聊天语料供出》)生成词向量并能够通过python加载起来,下一步就是如何运用了,敬请关注

自己动手做聊天机器人 三十一-如何把网站流量导向小二兔机器人

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

小二兔漏两招

为了先给各位客官热热身,我们找一段最近小二兔被调戏的场景给各位欣赏一下(日期 时间 ip [用户说的话] [机器人说的话]):

2016-09-29 17:49:28 60.191.99.129 [你看过加勒比海盗吗] [我不是啊]
2016-09-29 17:49:38 60.191.99.129 [那你是谁] [你想让我告诉你接下来会发生的事吗?]
2016-09-29 17:49:52 60.191.99.129 [神探狄仁杰] [昨晚那集看了吗?我快被吓死了]
2016-09-29 17:50:11 60.191.99.129 [没有,你怎么吓死了] [这家伙疯了 他是谁?]
2016-09-29 17:50:21 60.191.99.129 [路唇不对马嘴] [真的?]
2016-09-29 17:50:27 60.191.99.129 [假的] [说我们在交往什么的都是假的]
2016-09-29 17:50:36 60.191.99.129 [真的] [虽然我没有确认过]
2016-09-29 17:50:49 60.191.99.129 [动手!] [放心,你会得救的]
2016-09-29 17:51:00 60.191.99.129 [打他] [打!]
2016-09-29 17:51:14 60.191.99.129 [说你呢] [老爷子  您又上街泡妞去]
2016-09-29 17:51:24 60.191.99.129 [上海滩] [它只是让我思考]

好了,看看得了,下面介绍一下我是怎么给小二兔机器人导流的,给您压压惊

 

统计网站流量情况

通过cnzz统计看了一下最近半个月的受访页面流量情况如下:

从统计数据看出,用户的主要访问还是集中在首页以及各个博客页面,因此要想导流,还是从这些页面下手

 

增加懒人图库动态按钮

为了吸引用户点击,我打算在每个页面的右下角放置一个动态出现的小图标,页面滚动时它不动,这样用户点了直接跳到想要引流的小二兔页面。于是我上网扒代码,搜了一下客服漂浮代码,发现了懒人图库lrtk的代码,于是做了一些修改,达成了我的目的,我是怎么做的呢?

创建一个js文件,比如叫做lrtk.js,内容如下:

$(function()
{
    var tophtml="<a href=\"https://www.lcsays.com/chatbot/\" target=\"_blank\"><div id=\"izl_rmenu\" class=\"izl-rmenu\"><div class=\"btn btn-phone\"></div><div class=\"btn btn-top\"></div></div></a>";
    $("#top").html(tophtml);
    $("#izl_rmenu").each(function()
    {
        $(this).find(".btn-phone").mouseenter(function()
        {
            $(this).find(".phone").fadeIn("fast");
        });
        $(this).find(".btn-phone").mouseleave(function()
        {
            $(this).find(".phone").fadeOut("fast");
        });
        $(this).find(".btn-top").click(function()
        {
            $("html, body").animate({
                "scroll-top":0
            },"fast");
        });
    });
    var lastRmenuStatus=false;
    $(window).scroll(function()
    {
        var _top=$(window).scrollTop();
        if(_top>=0)
        {
            $("#izl_rmenu").data("expanded",true);
        }
        else
        {
            $("#izl_rmenu").data("expanded",false);
        }
        if($("#izl_rmenu").data("expanded")!=lastRmenuStatus)
        {
            lastRmenuStatus=$("#izl_rmenu").data("expanded");
            if(lastRmenuStatus)
            {
                $("#izl_rmenu .btn-top").slideDown();
            }
            else
            {
                $("#izl_rmenu .btn-top").slideUp();
            }
        }
    });
});

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

解释一下,上半部分是定义了id=top的div标签的内容:一个id为izl_rmenu的div,这个div的css格式定义在另一个文件lrtk.css里,如下:

.izl-rmenu{position:fixed;left:85%;bottom:10px;padding-bottom:73px;z-index:999;}
.izl-rmenu .btn{width:72px;height:73px;margin-bottom:1px;cursor:pointer;position:relative;}
.izl-rmenu .btn-top{background:url(https://www.lcsays.com/uploads/media/default/0001/01/thumb_416_default_big.png) 0px 0px no-repeat;background-size: 70px 70px;display:none;}

另外js文件的下半部分是说当页面滚动时这个div才展开

以上代码里有一些***phone之类的内容,是因为我扒的代码是用来显示一个客服按钮的,因为时间的关系没有删干净,不过不影响我们的效果,您可以继续加工整理

最后在我们所有页面的公共代码部分增加这样一句

<div id="top"></div>

OK,大功告成,看下效果:

我们的小二兔图标出来了,点击就会跳到我们的小二兔聊天页面https://www.lcsays.com/chatbot/

小二兔的表现还差强人意,后面我会继续优化算法,逐渐提高它的智商,敬请期待

自己动手做聊天机器人 三十-第一版聊天机器人诞生——吃了字幕长大的小二兔

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

第一版思路

首先要考虑到我的影视剧字幕聊天语料库特点,它是把影视剧里面的说话内容一句一句以回车换行罗列的三千多万条中国话,那么相邻的第二句其实就很可能是第一句的最好的回答,另外,如果对于一个问句有很多种回答,那么我们可以根据相关程度以及历史聊天记录来把所有回答排个序,找到最优的那个,这么说来这是一个搜索和排序的过程。对!没错!我们可以借助搜索技术来做第一版。

 

lucene+ik

lucene是一款开源免费的搜索引擎库,java语言开发。ik全称是IKAnalyzer,是一个开源中文切词工具。我们可以利用这两个工具来对语料库做切词建索引,并通过文本搜索的方式做文本相关性检索,然后把下一句取出来作为答案候选集,然后再通过各种方式做答案排序,当然这个排序是很有学问的,聊天机器人有没有智能一半程度上体现在了这里(还有一半体现在对问题的分析上),本节我们的主要目的是打通这一套机制,至于“智能”这件事我们以后逐个拆解开来不断研究。

 

建索引

首先用eclipse创建一个maven工程,如下:

maven帮我们自动生成了pom.xml文件,这配置了包依赖信息,我们在dependencies标签中添加如下依赖:

<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-core</artifactId>
    <version>4.10.4</version>
</dependency>
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-queryparser</artifactId>
    <version>4.10.4</version>
</dependency>
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-analyzers-common</artifactId>
    <version>4.10.4</version>
</dependency>
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>5.0.0.Alpha2</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.1.41</version>
</dependency>

并在project标签中增加如下配置,使得依赖的jar包都能自动拷贝到lib目录下:

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-dependency-plugin</artifactId>
      <executions>
        <execution>
          <id>copy-dependencies</id>
          <phase>prepare-package</phase>
          <goals>
            <goal>copy-dependencies</goal>
          </goals>
          <configuration>
            <outputDirectory>${project.build.directory}/lib</outputDirectory>
            <overWriteReleases>false</overWriteReleases>
            <overWriteSnapshots>false</overWriteSnapshots>
            <overWriteIfNewer>true</overWriteIfNewer>
          </configuration>
        </execution>
      </executions>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <configuration>
        <archive>
          <manifest>
            <addClasspath>true</addClasspath>
            <classpathPrefix>lib/</classpathPrefix>
            <mainClass>theMainClass</mainClass>
          </manifest>
        </archive>
      </configuration>
    </plugin>
  </plugins>
</build>

 

从https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/ik-analyzer/IK%20Analyzer%202012FF_hf1_source.rar下载ik的源代码并把其中的src/org目录拷贝到chatbotv1工程的src/main/java下,然后刷新maven工程,效果如下:

在com.shareditor.chatbotv1包下maven帮我们自动生成了App.java,为了辨识我们改成Indexer.java,关键代码如下:

Analyzer analyzer = new IKAnalyzer(true);
IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_4_9, analyzer);
iwc.setOpenMode(OpenMode.CREATE);
iwc.setUseCompoundFile(true);
IndexWriter indexWriter = new IndexWriter(FSDirectory.open(new File(indexPath)), iwc);
BufferedReader br = new BufferedReader(new InputStreamReader(
        new FileInputStream(corpusPath), "UTF-8"));
String line = "";
String last = "";
long lineNum = 0;
while ((line = br.readLine()) != null) {
	line = line.trim();
	if (0 == line.length()) {
		continue;
	}
	if (!last.equals("")) {
		Document doc = new Document();
		doc.add(new TextField("question", last, Store.YES));
		doc.add(new StoredField("answer", line));
		indexWriter.addDocument(doc);
	}
	last = line;
	lineNum++;
	if (lineNum % 100000 == 0) {
		System.out.println("add doc " + lineNum);
	}
}
br.close();
indexWriter.forceMerge(1);
indexWriter.close();

 

编译好后拷贝src/main/resources下的所有文件到target目录下,并在target目录下执行

java -cp $CLASSPATH:./lib/:./chatbotv1-0.0.1-SNAPSHOT.jar com.shareditor.chatbotv1.Indexer ../../subtitle/raw_subtitles/subtitle.corpus ./index

 

最终生成的索引目录index通过lukeall-4.9.0.jar查看如下:

 

检索服务

基于netty创建一个http服务server,代码共享在https://github.com/lcdevelop/ChatBotCourse的chatbotv1目录下,关键代码如下:

Analyzer analyzer = new IKAnalyzer(true);
QueryParser qp = new QueryParser(Version.LUCENE_4_9, "question", analyzer);
if (topDocs.totalHits == 0) {
	qp.setDefaultOperator(Operator.AND);
	query = qp.parse(q);
	System.out.println(query.toString());
	indexSearcher.search(query, collector);
	topDocs = collector.topDocs();
}
if (topDocs.totalHits == 0) {
	qp.setDefaultOperator(Operator.OR);
	query = qp.parse(q);
	System.out.println(query.toString());
	indexSearcher.search(query, collector);
	topDocs = collector.topDocs();
}

ret.put(“total”, topDocs.totalHits); ret.put(“q”, q); JSONArray result = new JSONArray(); for (ScoreDoc d : topDocs.scoreDocs) { Document doc = indexSearcher.doc(d.doc); String question = doc.get(“question”); String answer = doc.get(“answer”); JSONObject item = new JSONObject(); item.put(“question”, question); item.put(“answer”, answer); item.put(“score”, d.score); item.put(“doc”, d.doc); result.add(item); } ret.put(“result”, result);

其实就是查询建好的索引,通过query词做切词拼lucene query,然后检索索引的question字段,匹配上的返回answer字段的值作为候选集,使用时挑出候选集里的一条作为答案

这个server可以通过http访问,如http://127.0.0.1:8765/?q=hello(注意:如果是中文需要转成urlcode发送,因为java端读取时按照urlcode解析),server的启动方法是:

java -cp $CLASSPATH:./lib/:./chatbotv1-0.0.1-SNAPSHOT.jar com.shareditor.chatbotv1.Searcher

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

 

聊天界面

先看下我们的界面是什么样的,然后再说怎么做的

首先需要有一个可以展示聊天内容的框框,我们选择ckeditor,因为它支持html格式内容的展示,然后就是一个输入框和发送按钮,html代码如下:

<div class="col-sm-4 col-xs-10">
    <div class="row">
        <textarea id="chatarea">
            <div style='color: blue; text-align: left; padding: 5px;'>机器人: 喂,大哥您好,您终于肯跟我聊天了,来侃侃呗,我来者不拒!</div>
            <div style='color: blue; text-align: left; padding: 5px;'>机器人: 啥?你问我怎么这么聪明会聊天?因为我刚刚吃了一堆影视剧字幕!</div>
        </textarea>
    </div>
    <br />
    <div class="row">
        <div class="input-group">
            <input type="text" id="input" class="form-control" autofocus="autofocus" onkeydown="submitByEnter()" />
            <span class="input-group-btn">
            <button class="btn btn-default" type="button" onclick="submit()">发送</button>
          </span>
        </div>
    </div>
</div>

<script type=“text/javascript”> CKEDITOR.replace(‘chatarea’, { readOnly: true, toolbar: [‘Source’], height: 500, removePlugins: ‘elementspath’, resize_enabled: false, allowedContent: true });

</script>

为了调用上面的聊天server,需要实现一个发送请求获取结果的控制器,如下:

public function queryAction(Request $request)
{
    $q = $request->get('input');
    $opts = array(
        'http'=>array(
            'method'=>"GET",
            'timeout'=>60,
        )
    );
    $context = stream_context_create($opts);
    $clientIp = $request->getClientIp();
    $response = file_get_contents('http://127.0.0.1:8765/?q=' . urlencode($q) . '&clientIp=' . $clientIp, false, $context);
    $res = json_decode($response, true);
    $total = $res['total'];
    $result = '';
    if ($total > 0) {
        $result = $res['result'][0]['answer'];
    }
    return new Response($result);
}

这个控制器的路由配置为:

chatbot_query:
    path:     /chatbot/query
    defaults: { _controller: AppBundle:ChatBot:query }

因为聊天server响应时间比较长,为了不导致web界面卡住,我们在执行submit的时候异步发请求和收结果,如下:

    var xmlHttp;
    function submit() {
        if (window.ActiveXObject) {
            xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
        }
        else if (window.XMLHttpRequest) {
            xmlHttp = new XMLHttpRequest();
        }
        var input = $("#input").val().trim();
        if (input == '') {
            jQuery('#input').val('');
            return;
        }
        addText(input, false);
        jQuery('#input').val('');
        var datastr = "input=" + input;
        datastr = encodeURI(datastr);
        var url = "/chatbot/query";
        xmlHttp.open("POST", url, true);
        xmlHttp.onreadystatechange = callback;
        xmlHttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        xmlHttp.send(datastr);
    }
    function callback() {
        if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
            var responseText = xmlHttp.responseText;
            addText(responseText, true);
        }
    }

这里的addText是往ckeditor里添加一段文本,方法如下:

function addText(text, is_response) {
    var oldText = CKEDITOR.instances.chatarea.getData();
    var prefix = '';
    if (is_response) {
        prefix = "<div style='color: blue; text-align: left; padding: 5px;'>机器人: "
    } else {
        prefix = "<div style='color: darkgreen; text-align: right; padding: 5px;'>我: "
    }
    CKEDITOR.instances.chatarea.setData(oldText + "" + prefix + text + "</div>");
}

以上所有代码全都共享在https://github.com/lcdevelop/ChatBotCoursehttps://github.com/lcdevelop/shareditor.com中供参考

 

和机器人对话初体验

经过以上几部,我们的整套聊天机器人体系就搭建好了,地址在:https://www.lcsays.com/chatbot/,看下效果吧

虽然效果暂时还不咋地,但是整体流程建起来了,后面就是不断完善算法了,也希望牛人们多指点方案

自己动手做聊天机器人 二十九-重磅:近1GB的三千万聊天语料供出

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

注意:本文提到的程序和脚本都分享在https://github.com/lcdevelop/ChatBotCourse。如需直接获取最终语料库,请见文章末尾。

第一步:爬取影视剧字幕

请见我的这篇文章《二十八-脑洞大开:基于美剧字幕的聊天语料库建设方案

 

第二步:压缩格式分类

下载的字幕有zip格式和rar格式,因为数量比较多,需要做筛选分类,以便后面的处理,这步看似简单实则不易,因为要解决:文件多无法ls的问题、文件名带特殊字符的问题、文件名重名误覆盖问题、扩展名千奇百怪的问题,我写成了python脚本mv_zip.py如下:

import glob
import os
import fnmatch
import shutil
import sys
def iterfindfiles(path, fnexp):
    for root, dirs, files in os.walk(path):
        for filename in fnmatch.filter(files, fnexp):
            yield os.path.join(root, filename)
i=0
for filename in iterfindfiles(r"./input/", "*.zip"):
    i=i+1
    newfilename = "zip/" + str(i) + "_" + os.path.basename(filename)
    print filename + " <===> " + newfilename
    shutil.move(filename, newfilename)

其中的扩展名根据压缩文件可能有的扩展名修改成*.rar、*.RAR、*.zip、*.ZIP等

 

第三步:解压

解压这一步需要根据所用的操作系统下载不同的解压工具,建议使用unrar和unzip,为了解决解压后文件名可能重名覆盖的问题,我总结出如下两句脚本来实现批量解压:

i=0; for file in `ls`; do mkdir output/${i}; echo "unzip $file -d output/${i}";unzip -P abc $file -d output/${i} > /dev/null; ((i++)); done
i=0; for file in `ls`; do mkdir output/${i}; echo "${i} unrar x $file output/${i}";unrar x $file output/${i} > /dev/null; ((i++)); done

 

第四步:srt、ass、ssa字幕文件分类整理

当你下载大量字幕并解压后你会发现字幕文件类型有很多种,包括srt、lrc、ass、ssa、sup、idx、str、vtt,但是整体量级上来看srt、ass、ssa占绝对优势,因此简单起见,我们抛弃掉其他格式,只保留这三种,具体分类整理的脚本可以参考第二部压缩格式分类的方法按扩展名整理

 

第五步:清理目录

在我边整理边分析的过程中发现,我为了避免重名把文件放到不同目录里后,如果再经过一步文件类型整理,会产生非常多的空目录,每次ls都要拉好几屏,所以写了一个自动清理空目录的脚本clear_empty_dir.py,如下:

import glob
import os
import fnmatch
import shutil
import sys
def iterfindfiles(path, fnexp):
    for root, dirs, files in os.walk(path):
        if 0 == len(files) and len(dirs) == 0:
            print root
            os.rmdir(root)
iterfindfiles(r"./input/", "")

 

第六步:清理非字幕文件

在整个字幕文件分析过程中,总有很多其他文件干扰你的视线,比如txt、html、doc、docx,因为不是我们想要的,因此干脆直接干掉,批量删除脚本del_file.py如下:

import glob
import os
import fnmatch
import shutil
import sys
def iterfindfiles(path, fnexp):
    for root, dirs, files in os.walk(path):
        for filename in fnmatch.filter(files, fnexp):
            yield os.path.join(root, filename)
for suffix in ("*.mp4", "*.txt", "*.JPG", "*.htm", "*.doc", "*.docx", "*.nfo", "*.sub", "*.idx"):
    for filename in iterfindfiles(r"./input/", suffix):
        print filename
        os.remove(filename)

 

第七步:多层解压缩

把抓取到的字幕压缩包解压后有的文件里面依然还有压缩包,继续解压才能看到字幕文件,因此上面这些步骤再来一次,不过要做好心理准备,没准需要再来n次!

 

第八步:舍弃剩余的少量文件

经过以上几步的处理后剩下一批无扩展名的、特殊扩展名如:“srt.简体”,7z等、少量压缩文件,总体不超过50M,想想伟大思想家马克思教导我们要抓主要矛盾,因此这部分我们直接抛弃掉

 

第九步:编码识别与转码

字幕文件就是这样的没有规范,乃至于各种编码齐聚,什么utf-8、utf-16、gbk、unicode、iso8859琳琅满目应有尽有,我们要统一到一种编码方便使用,索性我们统一到utf-8,get_charset_and_conv.py如下:

import chardet
import sys
import os
if __name__ == '__main__':
    if len(sys.argv) == 2:
        for root, dirs, files in os.walk(sys.argv[1]):
            for file in files:
                file_path = root + "/" + file
                f = open(file_path,'r')
                data = f.read()
                f.close()
                encoding = chardet.detect(data)["encoding"]
                if encoding not in ("UTF-8-SIG", "UTF-16LE", "utf-8", "ascii"):
                    try:
                        gb_content = data.decode("gb18030")
                        gb_content.encode('utf-8')
                        f = open(file_path, 'w')
                        f.write(gb_content.encode('utf-8'))
                        f.close()
                    except:
                        print "except:", file_path

 

第十步:筛选中文

考虑到我朝广大人民的爱国热情,我只做中文,所以什么英文、韩文、日文、俄文、火星文、鸟语……全都不要,参考extract_sentence_srt.py如下:

# coding:utf-8
import chardet
import os
import re
cn=ur"([\u4e00-\u9fa5]+)"
pattern_cn = re.compile(cn)
jp1=ur"([\u3040-\u309F]+)"
pattern_jp1 = re.compile(jp1)
jp2=ur"([\u30A0-\u30FF]+)"
pattern_jp2 = re.compile(jp2)
for root, dirs, files in os.walk("./srt"):
    file_count = len(files)
    if file_count > 0:
        for index, file in enumerate(files):
            f = open(root + "/" + file, "r")
            content = f.read()
            f.close()
            encoding = chardet.detect(content)["encoding"]
            try:
                for sentence in content.decode(encoding).split('\n'):
                    if len(sentence) > 0:
                        match_cn =  pattern_cn.findall(sentence)
                        match_jp1 =  pattern_jp1.findall(sentence)
                        match_jp2 =  pattern_jp2.findall(sentence)
                        sentence = sentence.strip()
                        if len(match_cn)>0 and len(match_jp1)==0 and len(match_jp2) == 0 and len(sentence)>1 and len(sentence.split(' ')) < 10:
                            print sentence.encode('utf-8')
            except:
                continue

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

 

第十一步:字幕中的句子提取

不同格式的字幕有特定的格式,除了句子之外还有很多字幕的控制语句,我们一律过滤掉,只提取我们想要的重点内容,因为不同的格式都不一样,在这里不一一举例了,感兴趣可以去我的github查看,在这里单独列出ssa格式字幕的部分代码供参考:

if line.find('Dialogue') == 0 and len(line) < 500:
    fields = line.split(',')
    sentence = fields[len(fields)-1]
    tag_fields = sentence.split('}')
    if len(tag_fields) > 1:
        sentence = tag_fields[len(tag_fields)-1]

 

第十二步:内容过滤

经过上面几步的处理,其实已经形成了完整的语料库了,只是里面还有一些不像聊天的内容我们需要进一步做优化,包括:过滤特殊的unicode字符、过滤特殊的关键词(如:字幕、时间轴、校对……)、去除字幕样式标签、去除html标签、去除连续特殊字符、去除转义字符、去除剧集信息等,具体代码如下:

# coding:utf-8
import sys
import re
import chardet
if __name__ == '__main__':
    #illegal=ur"([\u2000-\u2010]+)"
    illegal=ur"([\u0000-\u2010]+)"
    pattern_illegals = [re.compile(ur"([\u2000-\u2010]+)"), re.compile(ur"([\u0090-\u0099]+)")]
    filters = ["字幕", "时间轴:", "校对:", "翻译:", "后期:", "监制:"]
    filters.append("时间轴:")
    filters.append("校对:")
    filters.append("翻译:")
    filters.append("后期:")
    filters.append("监制:")
    filters.append("禁止用作任何商业盈利行为")
    filters.append("http")
    htmltagregex = re.compile(r'<[^>]+>',re.S)
    brace_regex = re.compile(r'\{.*\}',re.S)
    slash_regex = re.compile(r'\\\w',re.S)
    repeat_regex = re.compile(r'[-=]{10}',re.S)
    f = open("./corpus/all.out", "r")
    count=0
    while True:
        line = f.readline()
        if line:
            line = line.strip()
            # 编码识别,不是utf-8就过滤
            gb_content = ''
            try:
                gb_content = line.decode("utf-8")
            except Exception as e:
                sys.stderr.write("decode error:  ", line)
                continue
            # 中文识别,不是中文就过滤
            need_continue = False
            for pattern_illegal in pattern_illegals:
                match_illegal = pattern_illegal.findall(gb_content)
                if len(match_illegal) > 0:
                    sys.stderr.write("match_illegal error: %s\n" % line)
                    need_continue = True
                    break
            if need_continue:
                continue
            # 关键词过滤
            need_continue = False
            for filter in filters:
                try:
                    line.index(filter)
                    sys.stderr.write("filter keyword of %s %s\n" % (filter, line))
                    need_continue = True
                    break
                except:
                    pass
            if need_continue:
                continue
            # 去掉剧集信息
            if re.match('.*第.*季.*', line):
                sys.stderr.write("filter copora %s\n" % line)
                continue
            if re.match('.*第.*集.*', line):
                sys.stderr.write("filter copora %s\n" % line)
                continue
            if re.match('.*第.*帧.*', line):
                sys.stderr.write("filter copora %s\n" % line)
                continue
            # 去html标签
            line = htmltagregex.sub('',line)
            # 去花括号修饰
            line = brace_regex.sub('', line)
            # 去转义
            line = slash_regex.sub('', line)
            # 去重复
            new_line = repeat_regex.sub('', line)
            if len(new_line) != len(line):
                continue
            # 去特殊字符
            line = line.replace('-', '').strip()
            if len(line) > 0:
                sys.stdout.write("%s\n" % line)
            count+=1
        else:
            break
    f.close()
    pass

 

直接获取语料数据

如果你不想经历上面这么痛苦的过程,可以直接获取我建设好的三千万(实际是33042896条)语料文件,考虑到个人的人工成本以及数据本身的真正价值,形式上收取9.9元的苦力费以表支持,希望大家多多理解,也算是对我的鼓励,打赏方式比较简单,直接扫码加我微信,备注上“聊天预料”即可

数据样例如下:

这是什么
是寄给医院的
井崎…为什么?
是为了小雪的事情
怎么回事?
您不记得了吗
在她说小雪…
就是在这种非常时期和我们一起舍弃休息时间来工作的护士失踪时…
医生 小雪她失踪了
你不是回了一句「是吗」吗
是吗…
不 对不起
跟我道歉也没用啊
而且我们都知道您是因为夫人的事情而操劳
但是 我想小聪是受不了医生一副漠不关心的样子
事到如今再责备医生也没有用了
是我的错吗…
我就是这个意思 您听不出来吗
我也难以接受
……

本数据仅作为大数据相关领域学习和研究使用,如果数据本身无意侵犯了您的版权,请发送邮件至shareditor.com^_^gmail.com告知,并提供相应的证明,我会立即评估和解决。

机器学习教程 二十三-R语言强大工具包ggplot绘图以外的那些事

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

画布定位

先看这张图:

> x <- c(1,2,3)
> y <- c(1,3,4)
> data <- data.frame(x,y)
> ggplot(data, aes(x = x, y = y)) + geom_point()

如果我们希望让画布再大一些,让这三个点集中一些怎么办?我们可以调整画布的坐标范围,以下两种方法效果是一样的:

> ggplot(data, aes(x = x, y = y)) + geom_point() + expand_limits(x = c(0, 4), y = c(0, 5))
> ggplot(data, aes(x = x, y = y)) + geom_point() + xlim(0, 4) + ylim(0, 5)

 

修改点的形状

我们可以画出多种点的形状

> ggplot(data,  aes(x, z)) + geom_point(aes(shape = y))

我们也可以把形状画成空心状的:

> ggplot(data,  aes(x, z)) + geom_point(aes(shape = y))+ scale_shape(solid = FALSE)

当然我们还可以调整点的大小:

> ggplot(data,  aes(x, z, size=z)) + geom_point(aes(shape = y))+ scale_shape(solid = FALSE)

 

各种标注方法

可以通过如下两种方式来添加title、x轴标签、y轴标签,效果是一样的,如下:

> ggplot(data,  aes(x,y)) + geom_point() + labs(title = "my title") +labs(x = "New x label") +labs(y = "New y label")
> ggplot(data,  aes(x,y)) + geom_point() + ggtitle("my title") + xlab("New x label") + ylab("New y label")

我们还可以在某一个坐标位置写一句话

> ggplot(data,  aes(x,y)) + geom_point() + ggtitle("my title") + xlab("New x label") + ylab("New y label") + annotate("text", x = 2, y = 25, label = "Some text")

我们还可以在某一个范围画一个矩形来重点标注

> ggplot(data,  aes(x,y)) + geom_point() + ggtitle("my title") + xlab("New x label") + ylab("New y label") + annotate("rect", xmin = 1.75, xmax = 2.25, ymin = 18, ymax = 22, alpha = .2)

也可以在某一个范围画一条线段

> ggplot(data,  aes(x,y)) + geom_point() + ggtitle("my title") + xlab("New x label") + ylab("New y label") + annotate("segment", x = 1.75, xend = 2.25, y = 18, yend = 22, colour = "blue")

 

坐标系统

我们看下面这个例子:

> x <- c(1,2,3)
> y <- c(10,20,30)
> data <- data.frame(x, y)
>  ggplot(data,  aes(x,y)) + geom_point()

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

我们发现坐标上x和y是不等比例的,x的宽度1相当于y宽度10,怎么样可以让其等比例显示呢?

>  ggplot(data,  aes(x,y)) + geom_point() + coord_fixed(ratio = 1)

如果我们希望横过来显示,那么也可以这样让坐标轴对调:

>  ggplot(data,  aes(x,y)) + geom_point() + coord_flip()

有时我们希望把坐标变换成极坐标,如下:

>  ggplot(data,  aes(x,y)) + geom_point() +coord_polar(theta = "y")

 

分网格展示

分网格显示便于把不同组数据分离观察

按x的值分成多行

>  ggplot(data,  aes(x,y)) + geom_point() + facet_grid(x ~ .)

按y的值分成多列

>  ggplot(data,  aes(x,y)) + geom_point() + facet_grid(. ~ y)

按x和y分成网格

>  ggplot(data,  aes(x,y)) + geom_point() + facet_grid(x ~ y)

按照某一个类别分成多个网格

> x
[1] 1 2 3
> y
[1] 10 20 30
> z <- c("type1", "type2", "type1")
> data <- data.frame(x, y, z)
>  ggplot(data,  aes(x,y)) + geom_point() + facet_wrap(~z)

如果我们希望分两行展示,那么可以:

>  ggplot(data,  aes(x,y)) + geom_point() + facet_wrap(~z, nrow=2)

 

主题风格

我们可以选择不同的主题风格,像如下几种,当然还有很多:

> ggplot(data,  aes(x,y)) + geom_point() +  theme_light()
> ggplot(data,  aes(x,y)) + geom_point() +  theme_dark()
> ggplot(data,  aes(x,y)) + geom_point() +  theme_gray()

快速画图

如果我们需求比较简单,不想写那么长的命令,可以使用qplot来简单画图,它实际上也是自动转化成ggplot来画图的

比如我们要画一个y = x^2的曲线,那么可以这样:

> a <- -100:100
> b <- a ^ 2
> qplot(a, b)

机器学习教程 二十二-一小时掌握R语言数据可视化

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

展开一张画布

ggplot2和其他作图工具不同,它是以图层覆盖图层的方式画出一个完美图像的,就像是photoshop里的图层,那么首先我们得有一张画布(如果没有安装R语言和ggplot2请见《十八-R语言特征工程实战》)

[root@centos $] R
> library(ggplot2)
> ggplot()

 

使用geom_abline、geom_hline、geom_vline画直线

下面我们来在这张画布上画一条横线:

> ggplot() + geom_hline(yintercept = 5)

我们也可以画一条竖线

> ggplot() + geom_vline(xintercept = 5)

当然我们也可以画斜线,

> geom_abline(intercept = 2.5, slope=1)

本应该画一条斜率为1,截距为2.5的斜线,但是因为画布不会自动移动到这条直线所在的位置,所以我们要实现几个点来定位一下画布,那么怎么画点呢,我们先来研究一下

 

使用geom_point画点

下面我们来一张空画布上画一个点,画点和画线不同在于:线可以指定一个x或y的截距就可以了,可以作为一个简单的参数传给geom_hline或geom_vline,但是画点涉及到的是一些x、y的数据值,ggplot是把数据和作图撇清的,也就是数据是数据,成像是成像

我们先来构造点:

> x <- c(1,2,3)
> y <- c(1,3,4)
> data <- data.frame(x,y)
> str(data)
'data.frame':      3 obs. of  2 variables:
 $ x: num  1 2 3
 $ y: num  1 3 4

我们其实构建了一个frame,里面包含了三个点:(1,1), (2,3), (3,4)

那么如果要画出这些点的话应该这样:

> ggplot(data, aes(x = x, y = y)) + geom_point()

前面是声明数据部分,后面是声明怎么成像

下面我们开始调整geom_point的参数,比如展示不同的颜色(左),和展示不同的形状(右)

> ggplot(data, aes(x, y)) + geom_point(aes(colour = factor(y)))
> ggplot(data, aes(x, y)) + geom_point(aes(shape = factor(y)))

如果颜色不是按factor区分,而是按连续值来区分,那么就是渐变形式,即

> ggplot(data, aes(x, y)) + geom_point(aes(colour = y))

还可以展示不同的大小,可以固定大小(左),也可以根据数据确定大小(右)

> ggplot(data, aes(x, y)) + geom_point(aes(size = 3))
> ggplot(data, aes(x, y)) + geom_point(aes(size = y))

这里我们要说明一下aes的作用,看下面两个用法(如图左、右):左边的含义就是画红色点,右边是按照指定的一个维度展示不同的颜色

> ggplot(data, aes(x, y)) + geom_point(colour="red")
> ggplot(data, aes(x, y)) + geom_point(aes(colour="red"))

接着上面划线一节,我们在已经画了点的画布上再画一条斜线:一条斜率为1,截距为1的直线,也就是y=x+1,那么一定是经过(2,3),(3,4)两个点的

> ggplot(data, aes(x, y)) + geom_point(aes(colour = y)) + geom_abline(slope = 1, intercept = 1)

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

 

使用geom_bar来画直方图

直观上看,直方图是表达一种累积量,因此默认的直方图的高度是counts或sum,也就是像下面这样子:因为我们的x只有1、2、3单独的三个值,所以直接geom_bar()高度相同,但如果判断x<2,那么有一个满足,两个不满足,所以高度分别是1和2

> ggplot(data, aes(x)) + geom_bar()
> ggplot(data, aes(x<2)) + geom_bar()

当然我们可以自己指定直方图的高度的计算方法,以下两种方法效果相同

> ggplot(data, aes(x)) + geom_bar(aes(weight=y))
> ggplot(data, aes(x,y)) + geom_bar(stat = "identity")

如果我们想要把多种取值的统计数目累加显示在柱状图上,可以这样:这里面对同一个x,不同y出现总数不一样,累加起来就像下图展示,其中如果y是数字,那么想把他当成类别,需要转成factor

> x <- rep(c(1,2), c(2,3))
> y <- rep(c(3,2), c(1,4))
> data <- data.frame(x,y)
> ggplot(data, aes(x)) + geom_bar(aes(fill=factor(y)))

当然我们也可以不简单堆叠起来,比如扁平放置(左),或拉伸至顶部(右)

> ggplot(data, aes(x)) + geom_bar(aes(fill=factor(y)), position="dodge")
> ggplot(data, aes(x)) + geom_bar(aes(fill=factor(y)), position="fill")

 

利用geom_density画概率密度曲线

概率密度就是某些值出现的频次多少的一个曲线,并做平滑,如下:

> x <- rep(c(1,3,7,11,23,50,60),c(1,30,400,60,4,55,11))
> y <- rep(c(1,3,7,11,23,50,60),c(1,30,400,60,4,55,11))
> data <- data.frame(x,y)
> ggplot(data, aes(x)) + geom_density()

我们可以调整平滑的宽度:

> ggplot(data, aes(x)) + geom_density(adjust = 1/5)

如果我们想按照不同的y值来分开画密度图,并且用不同颜色来表示不同的y值,那么我们可以用描边的方式(左),也可以用填充的方式(中),当然也可以两者结合

> ggplot(data, aes(x, colour = factor(y))) + geom_density(adjust = 1/5)
> ggplot(data, aes(x, fill = factor(y))) + geom_density(adjust = 1/5)
> ggplot(data, aes(x, colour = factor(y), fill = factor(y))) + geom_density(adjust = 1/5, alpha = 0.1)

和柱状图一样,我们也可以通过geom_density的position参数来显示累计情况:

> ggplot(data, aes(x, fill = factor(y))) + geom_density(adjust = 1/5, position='fill')
> ggplot(data, aes(x, fill = factor(y))) + geom_density(adjust = 1/5, position='stack')

 

用geom_text和geom_label写标注文本

为了让图像更清晰,我们需要把关键数据打上标签展示出来,我们可以这样做:

> ggplot(data, aes(x, y, label=rownames(data))) + geom_point(aes(colour = y)) + geom_abline(slope = 1, intercept = 1) + geom_text(check_overlap = TRUE)
> ggplot(data, aes(x, y, label=rownames(data))) + geom_point(aes(colour = y)) + geom_abline(slope = 1, intercept = 1) + geom_label()

 

总结

本节介绍了ggplot作图原理以及基本的几种作图方式,基于这些知识相信你很容易能做出精美的图像了

机器学习教程 二十一-R语言炫技必备基本功

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

注意:本文中实际使用的样本数据是根据具体命令任意挑选某组样本数据,不具有针对性,因此自己试验可以随意找样本尝试

 

一个table引发的血案

table函数就是用来输出指定字段的统计表格,可以用来分析数据比例情况,像下面的样子:

> table(full$Title, full$Survived)
               0   1
  Master      17  23
  Miss        55 130
  Mr         436  81
  Mrs         26 100
  Rare Title  15   8

那么为了让table够直观,各路大侠纷纷使出了洪荒之力,注意,下面开始炫技部分:

第一种作图方式(用于观察标准残差的场景):

> mosaicplot(table(full$Title, full$Survived), shade=TRUE)

第二种作图方式(用于观察总数目的场景):

> barplot(table(full$Survived, full$Title), sub="Survival by Title", ylab="number of passengers", col=c("steelblue4","steelblue2"))
> legend("topleft",legend = c("Died","Survived"),fill=c("steelblue4","steelblue2"),inset = .05)

第三种作图方式(用于观察比例情况的场景):

> barplot(prop.table(table(full$Survived, full$Title),2), sub="Survival by Title", ylab="number of passengers", col=c("steelblue4","steelblue2"))
> legend("topleft",legend = c("Died","Survived"),fill=c("steelblue4","steelblue2"),inset = .05)

当然还可以有第四种作图方式(同样是用于观察比例情况的场景):

> library('ggthemes')
> ggplot(full, aes(x = Title, fill = factor(Survived))) + geom_bar(stat='count', position='fill') + theme_few()

 

不同风格的决策树

在上节数据缺失填补中我们见过这样一棵决策树:

> library("rpart")
> library("rpart.plot")
> my_tree <- rpart(Fare ~ Pclass + Fsize + Embarked, data = train, method = "class", control=rpart.control(cp=0.0001))
> prp(my_tree, type = 4, extra = 100)

如果我们想看到每个分支的比例关系还可以在枝干上下文章:

> prp(my_tree, type = 2, extra = 100,branch.type=1)

图中根据不同的枝干粗细能看出样本集中在那个分支上

 

数据总览方式

第一种:按列总览

优点:可以看到有哪些列,什么类型,每一列取值举几个例子,也能看到有多少行

> str(train)
'data.frame':  	2197291 obs. of  15 variables:
 $ people_id        : chr  "ppl_100" "ppl_100" "ppl_100" "ppl_100" ...
 $ activity_id      : chr  "act2_1734928" "act2_2434093" "act2_3404049" "act2_3651215" ...
 $ date             : chr  "2023-08-26" "2022-09-27" "2022-09-27" "2023-08-04" ...
 $ activity_category: chr  "type 4" "type 2" "type 2" "type 2" ...
 $ char_1           : chr  "" "" "" "" ...
 $ char_2           : chr  "" "" "" "" ...
 $ char_3           : chr  "" "" "" "" ...
 $ char_4           : chr  "" "" "" "" ...
 $ char_5           : chr  "" "" "" "" ...
 $ char_6           : chr  "" "" "" "" ...
 $ char_7           : chr  "" "" "" "" ...
 $ char_8           : chr  "" "" "" "" ...
 $ char_9           : chr  "" "" "" "" ...
 $ char_10          : chr  "type 76" "type 1" "type 1" "type 1" ...
 $ outcome          : int  0 0 0 0 0 0 1 1 1 1 ...

第二种:分布总览

优点:能看出每一列的最大值、最小值、均值、中位数等分布数据

> summary(train)
comment_count      sex         has_free_course     score
 Min.   :   0.0        Min.   :0.0000   Min.   :0.0000        Min.   :0.00
 1st Qu.:   0.0        1st Qu.:0.0000   1st Qu.:0.0000        1st Qu.:0.00
 Median :   9.0        Median :1.0000   Median :0.0000        Median :4.90
 Mean   : 397.6        Mean   :0.6259   Mean   :0.3786        Mean   :2.92
 3rd Qu.: 169.0        3rd Qu.:1.0000   3rd Qu.:1.0000        3rd Qu.:5.00
 Max.   :5409.0        Max.   :2.0000   Max.   :1.0000        Max.   :5.00

第三种:采样浏览

优点:可以抽出其中少数样本看全部信息

> library(dplyr)
> sample_n(train, 4)
> sample_n(train, 4)
         people_id  activity_id       date activity_category char_1 char_2
513235  ppl_184793 act2_3805654 2023-02-25            type 2
1127284  ppl_29203 act2_1960547 2022-09-16            type 5
1174958 ppl_294918 act2_3624924 2022-10-19            type 3
1794311 ppl_390987  act2_633897 2023-02-10            type 2
        char_3 char_4 char_5 char_6 char_7 char_8 char_9   char_10 outcome
513235                                                      type 1       0
1127284                                                  type 1349       1
1174958                                                    type 23       0
1794311                                                     type 1       0

第四种:用户友好的表格采样浏览

优点:不自动换行,按表格形式组织,直观

> library(knitr)
> kable(sample_n(train, 4))
> kable(sample_n(train, 4))
people_id activity_id date activity_category char_1 char_2 char_3 char_4 char_5 char_6 char_7 char_8 char_9 char_10 outcome
1784154 ppl_389138 act2_2793972 2022-11-03 type 5 type 649 1
1138360 ppl_294144 act2_149226 2022-09-18 type 5 type 1058 0
1698603 ppl_373844 act2_3579388 2022-08-27 type 4 type 230 0
1505324 ppl_351017 act2_2570186 2022-09-30 type 5 type 248 0

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

 

R语言中的管道

shell中管道非常方便,比如把一个文件中第二列按数字排序后去重可以写成cat file | awk  '{print $2}' | sort -n -k 1 | uniq,那么R语言中的管道怎么用呢?我们先来看一个例子:

> library(dplyr)
> ggplot(filter(train, char_5 != ""), aes(x = outcome, fill = char_5)) + geom_bar(width = 0.6, position = "fill")

这个例子中有以下处理步骤:

1. 拿出train数据

2. 对train数据做过滤,过滤掉char_5这一列为空的样本

3. 用过滤好的数据执行ggplot画图

这三部如果用一层层管道操作就方便多了,实际上R语言为我们提供了这样的管道,即把函数的第一个参数单独提出来作为管道输入,管道操作符是%>%,也就是可以这样执行:

> train %>%
+ filter(char_5 != "") %>%
+ ggplot(aes(x=outcome, fill=char_10))+geom_bar(width=0.6, position="fill")

那么管道到底有什么好处呢?我们来追踪一下实际的过程来体会

假设我们样本长这个样子:

> library(knitr)
> kable(sample_n(train, 4))
people_id activity_id date activity_category char_1 char_2 char_3 char_4 char_5 char_6 char_7 char_8 char_9 char_10 outcome
567545 ppl_194099 act2_1420548 2023-02-08 type 2 type 1 0
115164 ppl_112033 act2_2209862 2022-10-23 type 5 type 481 1
1616290 ppl_369463 act2_2515098 2023-07-11 type 4 type 295 0
1714893 ppl_376799 act2_1464019 2022-10-01 type 5 type 1907 0

这时我们发现有一些列是空值,如果我希望了解一下其中的char_5都有哪些取值以及比例情况,我们可以这样来做:

> train %>%
+ count(char_5)
# A tibble: 8 × 2
  char_5       n
   <chr>   <int>
1        2039676
2 type 1   49214
3 type 2   26982
4 type 3    6013
5 type 4    1995
6 type 5    5421
7 type 6   67989
8 type 7       1

现在我们看到了输出了char_5和n两列分别表示可能取值和频次,但是还是不够直观,希望画图来看,那么我们继续:

> train %>%
+ count(char_5) %>%
+ ggplot(aes (x = reorder(char_5,n), y = n)) +
+ geom_bar(stat = "identity", fill = "light blue")

发现我们有很多空值,这时我们继续调整:

> train %>%
+ filter(char_5!="") %>%
+ count(char_5) %>%
+ ggplot(aes (x = reorder(char_5,n), y = n)) +
+ geom_bar(stat = "identity", fill = "light blue")

这就是我们的管道的作用:一步一步调试,不需要总想着把参数插到函数的哪个位置

 

回到本源,最基本的作图

有人会说,R语言怎么总是画这么复杂的图像,但是却连最基本的散点图和折线图都不能画吗?下面回到本源,来展示一下R语言的最基本的作图功能。

散点图

> a <- c(49, 26, 69, 19, 54, 67, 19, 33)
> plot(a)

如果希望看到变化趋势,我们可以画折线图,加上type即可

> plot(a, type='b')

如果这是一个每日消费金额,我们想看累积消费怎么办?我们可以利用累积函数cumsum,它的功能像这个样子:

> a
[1] 49 26 69 19 54 67 19 33
> cumsum(a)
[1]  49  75 144 163 217 284 303 336
>

那么可以这样作图:

> plot(cumsum(a), type='b')

最后让我们用一个完美的正弦曲线收笔:

> x1 <- 0:100
> x2 <- x1 * 2 * pi / 100
> Y = sin(x2)
> par(family='STXihei') # 这句是为了解决图像中中文乱码问题
> plot(x2, Y, type='l', main='正弦曲线', xlab='x轴', ylab='y轴')

机器学习教程 二十-看数据科学家是如何找回丢失的数据的(二)

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

连续型变量是如何做数据填补的

上一节中讲的Embarked的填补是一种离散型变量的填补方式,也就是通过统计规律来预测。那么对于连续型变量如果使用这种方法就不合适了,而应该使用某一种插值方式。比如Age这种数据,根据统计规律,假设其他人年龄多数是50岁,其他人都小于50岁,那么就预测是50岁吗?显然不对,而应该是小于50的某个值。那么如何根据统计规律来计算插值呢?我们来介绍一下mice

mice就是链式方程多元插值(Multivariate Imputation by Chained Equations)的简写

mice包可以对缺失数据的模式做一个很好的理解,为了说明这个事情,我们依然使用泰坦尼克数据集(不了解请见《十八-R语言特征工程实战》)

首先我们选择一些我们想要观察的列:

> full1 <- cbind(PassengerId=full$PassengerId,Pclass=full$Pclass,Sex=full$Sex,Age=full$Age,Fare=full$Fare,Embarked=full$Embarked,Title=full$Title,Fsize=full$Fsize)

然后我们利用mice查看一下数据缺失的模式:

> library(mice)
> md.pattern(full1)
     PassengerId Pclass Sex Embarked Title Fsize Fare Age
1045           1      1   1        1     1     1    1   1   0
 263           1      1   1        1     1     1    1   0   1
   1           1      1   1        1     1     1    0   1   1
               0      0   0        0     0     0    1 263 264

从上面可以方便的看出一共有1045个样本字段完整,263个样本缺失Age,1个样本缺失Fare

我们还可以通过VIM来图形化的展示数据缺失情况:

> library(VIM)
> aggr_plot <- aggr(full1, col = c('navyblue', 'red'), numbers=TRUE, sortVars=TRUE,labels=names(full1), cex.axis=.7, gap=3,ylab=c("Histogram of missing data", "Pattern"))

从图中可以看出Age缺失最多(20%左右),其次是Fare(比例很小)

下面我们利用mice的数据填补方法来填补,我们选用随机森林模型(rf),如下:

> set.seed(129)
> mice_mod <- mice(full[, !names(full) %in% c('PassengerId','Name','Ticket','Cabin','Family','Surname','Survived')], method='rf')

经过随机森林模型迭代训练,生成了mice_mod这个模型,下面我们生成完整数据并取出来,这里面实际包含了多个完整的副本,每个副本都对缺失值做了插补不同的值,complete默认会取出其中一个

> mice_output <- complete(mice_mod)

这里生成的mice_output就是在full基础上填充了缺失值的数据

为了观察新旧数据分布是否有过大的变化,我们画出分布直方图如下:

> par(mfrow=c(1,2))
> hist(full$Age, freq=F, main='Age: Original Data',
+   col='darkgreen', ylim=c(0,0.04))
> hist(mice_output$Age, freq=F, main='Age: MICE Output',
+   col='lightgreen', ylim=c(0,0.04))

讲解一下:这里的par(mfrow=c(1,2))指的是准备一张一行两列的画布,这样可以把两个直方图画在一起,freq=F这里的F是False,表示展示的列不是频次而是比例,ylim=c(0,0.04)是y轴的取值范围

 

我们也可以通过直观的看填补值的散点图来看是否合理:

> library(lattice)
> xyplot(mice_mod,Fare ~ Age,pch=18,cex=1)

xyplot的第一个参数是mice训练出的模型数据,第二个参数Fare ~ Age指明了x轴是Age,y轴是Fare,图中洋红色的点是自动填补的,看起来还是比较符合分布情况的,在这里,我们主要看的是y轴填补数据情况,如果想看Age的填补情况则把两个属性调过来,如下:

> xyplot(mice_mod,Age ~ Fare,pch=18,cex=1)

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

我们还可以画出密度图:

> densityplot(mice_mod)

从图中可以看出洋红色表示填补的数据集的分布密度情况,蓝色是原始数据的分布密度,如果填补效果较好,分布应该相似

 

上面我们说到mice实际上返回了多个完整数据副本,每个副本插的值都不同,那么我们还可以看下这些副本的插值情况:

> stripplot(mice_mod, pch = 20, cex = 1.2)

注意观察图中的Age和Fare两个图,每张图中都可以看到5种插值副本的分布情况

 

利用数据预测做数据填补

和上面的mice类似,也是建立一个模型来预测,但是不是采用插值的方式了,而是通过训练模型来预测数据,一般用来填补离散型变量的值,假设我们认为Fare是离散的值(因为票价一般是固定的几个价钱),我们把和Fare有关的几个变量(乘客等级、家庭人员数目、登船港口)都作为特征来训练一棵决策树

> library("rpart")
> library("rpart.plot")
> my_tree <- rpart(Fare ~ Pclass + Fsize + Embarked, data = train, method = "class", control=rpart.control(cp=0.0001))
> prp(my_tree, type = 4, extra = 100)

我们可以看到通过乘客等级、家庭人员数目、登船港口几个特征的训练,我们得出了一棵决策树,利用这棵决策树我们可以预测缺失值:

> full$PassengerId[is.na(full$Fare)]
[1] 1044

我们看到1044号乘客没有Fare,那么对他做预测

> predict(my_tree, full[1044,], type = "class")
1044
8.05
248 Levels: 0 4.0125 5 6.2375 6.4375 6.45 6.4958 6.75 6.8583 6.95 ... 512.3292

可以看出预测的结果是8.05

机器学习教程 十九-看数据科学家是如何找回丢失的数据的(一)

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

补全数据的纯手工方案

我们以泰坦尼克号数据集为例(不了解这个数据集请见《十八-R语言特征工程实战》)。

先重温一下这个数据集里面都有哪些字段:

PassengerId,Survived,Pclass,Name,Sex,Age,SibSp,Parch,Ticket,Fare,Cabin,Embarked

分别表示:

样本编号、是否得救、乘客级别、姓名、性别、年龄、船上的兄弟姐妹数、船上的父母子女数、船票编号、票价、客舱编号、登船港口。

我们检查一下Embarked这个字段哪些乘客是缺失的:

> full$PassengerId[full$Embarked == '']
[1] 62  830

看来在这1309位乘客中PassengerId 为62和830的乘客缺失了Embarked字段值,那么我们如何来补全这个数据呢?我们分析一下哪个字段可能和Embarked(登船港口)的值有关,我们猜测票价有可能和Embarked有关,但是不同级别的票价一定又是不一样的,那么我们可以看一下不同级别票价的统计规律,庆幸的是Embarked只有三个取值:C Q S分别表示C = Cherbourg; Q = Queenstown; S = Southampton

我们先来看一下62  830的票价和乘客级别是多少:

> full[c(62, 830), 'Fare']
[1] 80 80
> full[c(62, 830), 'Pclass']
[1] 1 1

等级都是1级,票价都是80

现在我们再看下这三个港口对应不同级别的乘客平均票价是多少,在此之前我们先排除掉62  830这两位乘客的数据:

> library("dplyr")
> embark_fare <- full %>% filter(PassengerId != 62 & PassengerId != 830)

下面我们利用强大的ggplot2画出盒图(boxplot),首先说一下什么是盒图,盒图由五个数值点组成:最小值(min),下四分位数(Q1),中位数(median),上四分位数(Q3),最大值(max)。也可以往盒图里面加入平均值(mean)。下四分位数、中位数、上四分位数组成一个“带有隔间的盒子”。上四分位数到最大值之间建立一条延伸线,这个延伸线成为“胡须(whisker)”。盒图用来反映离散数据的分布情况。

下面我们画出不同Embarked、不同等级乘客对应的Fare的盒图

> library("ggplot2")
> library('ggthemes')
> ggplot(embark_fare, aes(x = Embarked, y = Fare, fill = factor(Pclass))) +geom_boxplot()+geom_hline(aes(yintercept=80),colour='red', linetype='dashed', lwd=2)+theme_few()

讲解一下这个命令,geom_boxplot表示画盒图,geom_hline表示沿着横轴方向画线,如果想沿着纵轴那么就用geom_vline,lwd表示线宽

为了能找到和62, 830两位乘客相似的情况,单独把Fare为80的位置画出了一条横线,用来参照。我们发现Pclass=1的乘客Fare均值最接近80的是C港口,因此我们把这两位乘客的Embarked就赋值为C:

> full$Embarked[c(62, 830)] <- 'C'

当然我们还可以画这样一张图来看待这个事情:

> ggplot(full[full$Pclass == '1' & full$Embarked == 'C', ],
+ aes(x = Fare)) +
+ geom_density(fill = '#99d6ff', alpha=0.4) +
+ geom_vline(aes(xintercept=median(Fare, na.rm=T)),
+ colour='red', linetype='dashed', lwd=1) +
+ geom_vline(aes(xintercept=80),colour='green',linetype='dashed', lwd=1) +
+ theme_few()

讲解一下:这里选择Pclass==1,Embarked == 'C'的数据,画出了概率密度曲线,同时把Fare的均值画了一条红色的竖线,也在Fare=80的位置画了一条绿色的竖线作为参照,可以直观看出均值和80很接近

本文部分内容参考:https://www.kaggle.com/mrisdal/titanic/exploring-survival-on-the-titanic/comments

机器学习教程 十八-R语言特征工程实战

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

R语言介绍

熟悉R语言的朋友请直接略过。R语言是贝尔实验室开发的S语言(数据统计分析和作图的解释型语言)的一个分支,主要用于统计分析和绘图,R可以理解为是一种数学计算软件,可编程,有很多有用的函数库和数据集。

 

R的安装和使用

https://mirrors.tuna.tsinghua.edu.cn/CRAN/下载对应操作系统的安装包安装。安装好后单独创建一个目录作为工作目录(因为R会自动在目录里创建一些有用的隐藏文件,用来存储必要的数据)

执行

R

即可进入R的交互运行环境

简单看一个实例看一下R是如何工作的:

[root@centos:~/Developer/r_work $] R
R version 3.3.1 (2016-06-21) -- "Bug in Your Hair"
Copyright (C) 2016 The R Foundation for Statistical Computing
Platform: x86_64-apple-darwin13.4.0 (64-bit)
> x <- c(1,2,3,4,5,6,7,8,9,10)
> y <- x*x
> plot(x,y,type="l")
>

以上看得出我们画了y = x^2的曲线

R语言的语法和C类似,但是稍有不同,R语言里向量和矩阵的操作和python的sci-learn类似,但是稍有不同:

1. R的赋值语句的符号是"<-"而不是"="

2. R里的向量用c()函数定义,R里没有真正的矩阵类型,矩阵就是一系列向量组成的list结构

有时候如果我们想要加载一个库发现没有安装,就像这样:

> library(xgboost)
Error in library(xgboost) : 不存在叫‘xgboost’这个名字的程辑包

那么就这样来安装:

> install.packages("xgboost")

输入后会提示选择下载镜像,选择好后点ok就能自动安装完成,这时就可以正常加载了:

> library(xgboost)
>

想了解R语言的全部用法,推荐《权威的R语言入门教程《R导论》-丁国徽译.pdf》,请自行下载阅读,也可以继续看我下面的内容边用边学

 

特征工程

按我的经验,特征工程就是选择和使用特征的过程和方法,这个说起来容易,做起来真的不易,想要对实际问题设计一套机器学习方法,几乎大部分时间都花在了特征工程上,相反最后的模型开发花不了多长时间(因为都是拿来就用了),再有需要花一点时间的就是最后的模型参数调优了。花费时间排序一般是:特征工程>模型调参>模型开发

 

Titanic数据集特征工程实战

Titanic数据集是这样的数据:Titanic(泰坦尼克号)沉船灾难死亡了很多人也有部分人成功得救,数据集里包括了这些字段:乘客级别、姓名、性别、年龄、船上的兄弟姐妹数、船上的父母子女数、船票编号、票价、客舱编号、登船港口、是否得救。

我们要做的事情就是把Titanic数据集中部分数据作为训练数据,然后用来根据测试数据中的字段值来预测这位乘客是否得救

 

数据加载

训练数据可以在https://www.kaggle.com/c/titanic/download/train.csv下载,测试数据可以在https://www.kaggle.com/c/titanic/download/test.csv下载

下面开始我们的R语言特征工程,创建一个工作目录r_work,下载train.csv和test.csv到这个目录,看下里面的内容:

[root@centos:~/Developer/r_work $] head train.csv
PassengerId,Survived,Pclass,Name,Sex,Age,SibSp,Parch,Ticket,Fare,Cabin,Embarked
1,0,3,"Braund, Mr. Owen Harris",male,22,1,0,A/5 21171,7.25,,S
2,1,1,"Cumings, Mrs. John Bradley (Florence Briggs Thayer)",female,38,1,0,PC 17599,71.2833,C85,C
3,1,3,"Heikkinen, Miss. Laina",female,26,0,0,STON/O2. 3101282,7.925,,S
4,1,1,"Futrelle, Mrs. Jacques Heath (Lily May Peel)",female,35,1,0,113803,53.1,C123,S
5,0,3,"Allen, Mr. William Henry",male,35,0,0,373450,8.05,,S
6,0,3,"Moran, Mr. James",male,,0,0,330877,8.4583,,Q
7,0,1,"McCarthy, Mr. Timothy J",male,54,0,0,17463,51.8625,E46,S
8,0,3,"Palsson, Master. Gosta Leonard",male,2,3,1,349909,21.075,,S
9,1,3,"Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg)",female,27,0,2,347742,11.1333,,S

我们看到文件内容是用逗号分隔的多个字段,第一行是schema,第二行开始是数据部分,其中还有很多空值,事实上csv就是Comma-Separated Values,也就是用“逗号分隔的数值”,它也可以用excel直接打开成表格形式

R语言为我们提供了加载csv文件的函数,如下:

> train <- read.csv('train.csv', stringsAsFactors = F)
> test <- read.csv('test.csv', stringsAsFactors = F)

如果想看train和test变量的类型,可以执行:

> mode(train)
[1] "list"

我们看到类型是列表类型

如果想预览数据内容,可以执行:

> str(train)
'data.frame':  	891 obs. of  12 variables:
 $ PassengerId: int  1 2 3 4 5 6 7 8 9 10 ...
 $ Survived   : int  0 1 1 1 0 0 0 0 1 1 ...
 $ Pclass     : int  3 1 3 1 3 3 1 3 3 2 ...
 $ Name       : chr  "Braund, Mr. Owen Harris" "Cumings, Mrs. John Bradley (Florence Briggs Thayer)" "Heikkinen, Miss. Laina" "Futrelle, Mrs. Jacques Heath (Lily May Peel)" ...
 $ Sex        : chr  "male" "female" "female" "female" ...
 $ Age        : num  22 38 26 35 35 NA 54 2 27 14 ...
 $ SibSp      : int  1 1 0 1 0 0 0 3 0 1 ...
 $ Parch      : int  0 0 0 0 0 0 0 1 2 0 ...
 $ Ticket     : chr  "A/5 21171" "PC 17599" "STON/O2. 3101282" "113803" ...
 $ Fare       : num  7.25 71.28 7.92 53.1 8.05 ...
 $ Cabin      : chr  "" "C85" "" "C123" ...
 $ Embarked   : chr  "S" "C" "S" "S" ...

可以看到其实train和test变量把原始的csv文件解析成了特定的数据结构,train里有891行、12列,每一列的字段名、类型以及可能的值都能预览到

因为test数据集也是真实数据的一部分,所以在做特征工程的时候可以把test和train合并到一起,生成full这个变量,后面我们都分析full:

> library('dplyr')
> full  <- bind_rows(train, test)

 

头衔特征的提取

因为并不是所有的字段都应该用来作为训练的特征,也不是只有给定的字段才能作为特征,下面我们开始我们的特征选择工作,首先我们从乘客的姓名入手,我们看到每一个姓名都是这样的结构:"名字, Mr/Mrs/Capt等. 姓",这里面的"Mr/Mrs/Capt等"其实是一种称谓(Title),虽然人物的姓名想必和是否得救无关,但是称谓也许和是否得救有关,我们把所有的Title都筛出来:

> table(gsub('(.*, )|(\\..*)', '', full$Name))
        Capt          Col          Don         Dona           Dr     Jonkheer
           1            4            1            1            8            1
        Lady        Major       Master         Miss         Mlle          Mme
           1            2           61          260            2            1
          Mr          Mrs           Ms          Rev          Sir the Countess
         757          197            2            8            1            1

解释一下,这里面的full$Name表示取full里的Name字段的内容,gsub是做字符串替换,table是把结果做一个分类统计(相当于group by title),得出数目

通过结果我们看到不同Title的人数目差别比较大

我们把这个Title加到full的属性里:

> full$Title <- gsub('(.*, )|(\\..*)', '', full$Name)

这时我们可以按性别和title分两级统计(相当于group by sex, title):

> table(full$Sex, full$Title)
         Capt Col Don Dona  Dr Jonkheer Lady Major Master Miss Mlle Mme  Mr Mrs
  female    0   0   0    1   1        0    1     0      0  260    2   1   0 197
  male      1   4   1    0   7        1    0     2     61    0    0   0 757   0
          Ms Rev Sir the Countess
  female   2   0   0            1
  male     0   8   1            0

 

为了让这个特征更具有辨别性,我们想办法去掉那些稀有的值,比如总次数小于10的,我们都把title改成“Rare Title”

> rare_title <- c('Dona', 'Lady', 'the Countess','Capt', 'Col', 'Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer')
> full$Title[full$Title %in% rare_title]  <- 'Rare Title'

同时把具有相近含义的title做个归一化

> full$Title[full$Title == 'Mlle']        <- 'Miss'
> full$Title[full$Title == 'Ms']          <- 'Miss'
> full$Title[full$Title == 'Mme']         <- 'Mrs'

这回我们看下title和是否得救的关系情况

> table(full$Title, full$Survived)
               0   1
  Master      17  23
  Miss        55 130
  Mr         436  81
  Mrs         26 100
  Rare Title  15   8

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

还不够直观,我们可以通过马赛克图来形象的看:

> mosaicplot(table(full$Sex, full$Title), shade=TRUE)

这回看出比例情况的差异了,比如title为Mr的死亡和得救的比例比较明显,说明这和是否得救关系密切,title作为一个特征是非常有意义的

这样第一个具有代表意义的特征就提取完了

 

家庭成员数特征的提取

看过电影的应该了解当时的场景,大家是按照一定秩序逃生的,所以很有可能上有老下有小的家庭会被优先救援,所以我们统计一下一个家庭成员的数目和是否得救有没有关系。

为了计算家庭成员数目,我们只要计算父母子女兄弟姐妹的数目加上自己就可以,所以:

> full$Fsize <- full$SibSp + full$Parch + 1

下面我们做一个Fsize和是否得救的图像

> library("ggplot2")
> library('ggthemes')
> ggplot(full[1:891,], aes(x = Fsize, fill = factor(Survived))) + geom_bar(stat='count', position='dodge') + scale_x_continuous(breaks=c(1:11)) + labs(x = 'Family Size') + theme_few()

我们先解释一下上面的ggplot语句

第一个参数full[1:891,]表示我们取全部数据的前891行的所有列,取891是因为train数据一共有891行

aes(x = Fsize, fill = factor(Survived))表示坐标轴的x轴我们取Fsize的值,这里的fill是指用什么变量填充统计值,factor(Survived)表示把Survived当做一种因子,也就是只有0或1两种“情况”而不是数值0和1,这样才能分成红绿两部分统计,不然如果去掉factor()函数包裹就会像这个样子(相当于把0和1加了起来):

这里的“+”表示多个图层,是ggplot的用法

geom_bar就是画柱状图,其中stat='count'表示统计总数目,也就是相当于count(*) group by factor(Survived),position表示重叠的点放到什么位置,这里设置的是“dodge”表示规避开的展示方式,如果设置为"fill"就会是这样的效果:

scale_x_continuous(breaks=c(1:11))就是说x轴取值范围是1到11,labs(x = 'Family Size')是说x轴的label是'Family Size',theme_few()就是简要主题

下面我们详细分析一下这个图说明了什么事情。我们来比较不同家庭成员数目里面成功逃生的和死亡的总数的比例情况可以看出来:家庭人数是1或者大于4的情况下红色比例较大,也就是死亡的多,而人数为2、3、4的情况下逃生的多,因此家庭成员数是一个有意义的特征,那么把这个特征总结成singleton、small、large三种情况,即:

> full$FsizeD[full$Fsize == 1] <- 'singleton'
> full$FsizeD[full$Fsize < 5 & full$Fsize > 1] <- 'small'
> full$FsizeD[full$Fsize > 4] <- 'large'

再看下马赛克图:

> mosaicplot(table(full$FsizeD, full$Survived), main='Family Size by Survival', shade=TRUE)

从图中可以看出差异明显,特征有意义

 

模型训练

处理好特征我们就可以开始建立模型和训练模型了,我们选择随机森林作为模型训练。首先我们要把要作为factor的变量转成factor:

> factor_vars <- c('PassengerId','Pclass','Sex','Embarked','Title','FsizeD')
> full[factor_vars] <- lapply(full[factor_vars], function(x) as.factor(x))

然后我们重新提取出train数据和test数据

> train <- full[1:891,]
> test <- full[892:1309,]

接下来开始训练我们的模型

> library('randomForest')
> set.seed(754)
> rf_model <- randomForest(factor(Survived) ~ Pclass + Sex + Embarked + Title + FsizeD, data = train)

下面画出我们的模型误差变化:

> plot(rf_model, ylim=c(0,0.36))
> legend('topright', colnames(rf_model$err.rate), col=1:3, fill=1:3)

图像表达的是不同树个数情况下的误差率,黑色是整体情况,绿色是成功获救的情况,红色是死亡的情况,可以看出通过我们给定的几个特征,对死亡的预测误差更小更准确

我们还可以利用importance函数计算特征重要度:

> importance(rf_model)
         MeanDecreaseGini
Pclass          40.273719
Sex             53.240211
Embarked         8.566492
Title           85.214085
FsizeD          23.543209

可以看出特征按重要程度从高到底排序是:Title > Sex > Pclass > FsizeD > Embarked

 

数据预测

有了训练好的模型,我们可以进行数据预测了

> prediction <- predict(rf_model, test)

这样prediction中就存储了预测好的结果,以0、1表示

为了能输出我们的结果,我们把test数据中的PassengerId和prediction组合成csv数据输出

> solution <- data.frame(PassengerID = test$PassengerId, Survived = prediction)
> write.csv(solution, file = 'solution.csv', row.names = F)

最终的solution.csv的内容如下:

[root@centos:~/Developer/r_work $] head solution.csv
"PassengerID","Survived"
"892","0"
"893","1"
"894","0"
"895","0"
"896","1"
"897","0"
"898","1"
"899","0"
"900","1"

本文部分内容参考:https://www.kaggle.com/mrisdal/titanic/exploring-survival-on-the-titanic/comments