React和Redux初探

React是Facebook开发的一套前端框架,与Angular等前端MVC框架不一样的是,React关注于构建UI,其相当于MVC中的V。而React最让人欣喜的一点在于,其使用了声明的方式开发UI,这使得基于组件(Component)的前端开发变得非常直接。然而仅仅使用React,还远远不能构建一个App,还需要有数据存储和逻辑处理,这当然可以使用MVC架构。不过,Facebook认为,MVC架构中,当Model和View增多以后,双向的数据流导致系统的复杂性增加。因而Facebook提出了一个Flux。可以认为Flux是一套应用框架,与MVC不同的是,其数据流是单向的,结构如下图所示 由于数据的单向流动,无须处理view->model的逻辑,因此系统的结构比较清晰。

React

对于如下的html代码,

<div class="post">
    <h1 style="font-size: 1.6rem">Post Title</h1>
    <div class="post-content">Some Content</div>
</div>

我们可以使用这样的React代码来构建

// app.js
var Post = React.createClass({
    render: function() {
        return (
            <div className='post'>
                <PostTitle text='Post Title' />
                <PostContent text='Some Content' />
            </div>
        );
    }
});
var PostTitle = React.createClass({
    render: function() {
        var style = {fontSize: '1.6rem'};
        return <h1 style={style}>{this.props.text}</h1>
    }
});
var PostContent = React.createClass({
    render: function() {
        return <div className='post-content'>{this.props.text}</div>
    }
});
React.render(
    <Post />,
    document.getElementById('root');
);

对应的html文件,则是

<div id="root"></div>
<script type="text/jsx" src="app.js"></script>

上面的app.js文件并不是Javascript纯粹代码,而是夹杂了一些类似HTML的代码。这是JSX代码,并不能被浏览器执行,因此需要预先编译成js代码。

从代码中,可以看到,React将原本的html代码拆成了三个组件,其中PostPostTitlePostContent两个组件构成。最后再通过React.render方法,将组件挂载到dom上。每个组件中,都有一个render方法,表示组件将要渲染的dom结构。与HTML相比,只是可以使用{}将参数传入到组件中,而组件则可以通过props获取属性。这种结构,称之为Virtual Dom,在返回的结果中,div, h1等和自己定义的PostPostTitle等类似,都是Virtual Dom,只不过div这种HTML本身已有的组件,其渲染结果就是与其对应的HTML标签。React约定,自己定义的组件使用大写字母开头,已有HTML标签为小写字母开头。并非所有HTML标签和属性都被React支持,可以在在这里查看支持的标签和属性。其中,classfor分别使用classNamehtmlFor,因为这两个都是js的关键字。

Redux

由于Flux还是属于新生事物,各种Flux实现多如牛毛,比如Reflux、Fluxxor等等,Redux也是其中的一种。Redux借鉴了函数式的编程思想,对于状态的改变,其实是如下的函数

f(state, action) => next_state

这种函数在Redux里称为Reducer, 而Flux中的Store则是多个由Reducer组成。这样,对于一个Action以及当前的状态,可以转移到下一个状态,从而更新View。

我根据其github的breaking-changes-1.0分支(地址)的例子,大致明白了现在的API,但是仍有非常多的东西不是很清楚,需要等到正式版放出之后,查看文档才能明白了。

至于Redux相比其他Flux实现有什么优点,其实我说不上来,最初吸引我的其实不是Redux,而是这篇文章,当时也根本不了解Flux(现在其实也不了解。。),就稀里糊涂的看了Redux的例子。

前端

感觉目前的前端越来越复杂,Javascript的应用也越来越多。甚至都有了以后找个前端工作的念头,谁知道呢。。

写一个Web框架

在知乎上偶然看到了廖雪峰老师的网站上的Python教程,Python实战,目的是从头开始写一个博客。这个从头开始是,自己造轮子,包括ORM、Web框架。对于写好框架之后的写博客等前端部分,我没有什么兴趣,而最前面的几个,都是我不清楚具体实现的,跟着教程一步一步往下走。说明一点,教程用的Python2,我用的是Python3,所以有些地方是略有区别的。

第一部分是编写数据API,这个部分比较简单,只要了解Python的装饰器,写的过程中,查一下mysql-connector-pyhton的文档,没有什么难度。

第二部分ORM,想要写一个功能比较完整的ORM,比较不容易。但是如果目标仅仅是可用,能够实现基本功能,难度并不太大。网站下的评论,说这一章有难度,主要都是集中在Python的metaclass,也就是元类上。我自己对元类也是不懂,查到了这篇文章,深刻理解Python中的元类(metaclass),英文原文是Stack Overflow上的一个回答,What is a metaclass in Python?

我自己的理解,元类就是创建类的类,它控制一个类如何被创建。在Python中,有个Built-in的type类,可以像函数一样调用,当传给它三个函数时,就会创建一个新的类型

class A:
    t = 1
A = type('A', (object,), {'t':1})

上面代码中,两块代码的作用是一样的。传递给type的三个参数,分别是类型名,父类型(必须为元组),类变量和类方法的字典。可以认为,如果不指定元类,那么type则充当类型的元类。而如果想要控制一个类型的创建,就需要自定义元类,通过元类来创建对象。文章中也说了,元类不一定是类,任意可以可以被调用(callable)的对象都可以作为元类。

Python中有个__new__方法,这个方法的目的控制一个对象的创建,通过重写__new__就可以对一个类型创建进行自定义。比如教程里的ModelMetaclass

class ModelMetaclass(type):
    def __new__(mcs, name, bases, attrs):
        if name == 'Model':
            return super(ModelMetaclass, mcs).__new__(name, bases, attrs)
        mapping = [x: y for x, y in attrs.items() if isinstance(y, Field)]
        table = attrs['__table__']
        real_attrs = {
            '__mapping__': mapping,
            '__table__': table,
        }
    return super(ModelMetaclass, mcs).__new__(name, bases, real_attrs)

其中mapping是名称和类型的映射,通过这个元类,作为基类的Model,其创建过程保持不动,而实际的与数据库表相映射的类型,在创建时,其类变量均被放进mapping中。实际进行实例化时,给定的实际上是实例变量,其数据库属性可以在mapping中找到,也就是实现了数据库类型的和Python类型的映射。在ORM里,这一步是最重要的。此外,在赋值时,可以通过mapping来检验变量类型与定义时的数据库类型是否匹配。

至于后面的Web框架,我还没有写完,不过根据网站上的代码架构,在适当填补一些需要的代码,比如路由表等,应该就没有大问题。当然,我在写的过程中,遇到了一些麻烦,比如对于跳转的实现不理解,现在好像明白了,实现跳转,只需要在response中设置Location的头部就行了,不过现在没有环境,不能确认是否是这样实现。

另外就是,由于网站上只给出了一个大致的架构,自己写出类的框架和网站github上给出的,可能会有比较大的不同。

Django Session在Apache服务器失效的问题的解决记录

实验室项目,用Django写的Web端,需要登陆功能,因为Django自带的登陆比较庞大,自己写了一个了,没有权限等一些列东西,比较easy的,其中,require_signin这个装饰器是这样的

def require_signin(f):
@wraps(f)
def deco(request, *args, **kwargs):
    if 'user' in request.session:
        return f(request, *args, **kwargs)
    url = '%s?next=%s' % (LOGIN_URL, request.path)
    return redirect(url)
return deco

在本地开发的时候,没有任何问题。但是上传到服务器的时候,其中的'user' in request.session总是返回False,打印了session.keys()来看,确实是空的。以为是数据库访问的问题(session的后端是数据库),将本地和服务器的配置文件分开了,结果仍然如此,至此陷入困境。在网上搜索,偶然发现apache的MaxRequestsPerChild设置选项可能和其相关,因为之前为了使提交到SVN的代码能够立刻生效,将这个值设置为了1,这导致了每次请求重开进程,而session在进程之间应该不能保存的(不知道是不是有办法设置为可以保存),将这个值改大就会正常了。

之后就有了一个新问题,提交到SVN之后,改动不会在服务器立即显示出来,需要手动重启服务器,这就很不方便。可以利用SVN的post-commit hook来重启服务器。SVN的hooks是在提交的时候,执行一些特定的操作,post-commit hook就是在提交之后,执行操作,windows下,可以使用批处理文件,其他语言如Python等也可以,但是现在这个是一个easy的任务,不必祭出Python,

/path/to/apache/bin/httpd.exe -k restart

这一句就足够了。利用hooks,可以实现很多功能,如果程序的部署,要求输入“提交信息”等,很强大的工具。git当然也有

ActiveMQ笔记

实验室的项目需要使用ActiveMQ,目的是为了进行一些耗时间的处理时,不会阻塞程序的主流程。调研ActiveMQ的工作就交给我来做了。

我们使用ActiveMQ,需要达到的目的有这么几个

  • 主流程发送消息,不阻塞
  • 可用于集群
  • 故障恢复
  • 负载均衡

从网上看了一些资料,ActiveMQ可以满足我们的要求

First Step

ActiveMQ官网下载即可,目前的最新版本是5.10.0。我下载了Windows版本,进入bin目录,运行

activemq start

即可启动一个ActiveMQ的Broker。注意,需要设置环境变量JAVA_HOME

发送接收消息

这一步应该是初接触ActiveMQ最想要做的事情。我们首先需要一个消息的发送者,同时需要一个消息的接受者。不多说,直接贴代码

// Sender.java
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;

import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;

public class Sender {
    private static final String HOST = "tcp://localhost:61616"; // ActiveMQ的监听地址,

    public static void main(String[] args) throws InterruptedException {
        ConnectionFactory factory; // JMS连接的工厂
        Connection conn = null; // JMS连接
        Session session; // JMS会话
        Destination destination; // 目的地,对于PTP模式,目的地是Queue;对于订阅模式,目的地是Topic
        MessageProducer producer; // 生产者

        factory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER, ActiveMQConnection.DEFAULT_PASSWORD, HOST);

        try {
            conn = factory.createConnection();
            conn.start(); // 必须显式调用start方法
            session = conn.createSession(true, Session.AUTO_ACKNOWLEDGE);

            destination = session.createQueue("Test");
            producer = session.createProducer(destination);
            producer.setDeliveryMode(DeliveryMode.PERSISTENT); // 设置消息持久化
            for (int i = 0;; i+=2) {
                send(session, producer, i);
                Thread.sleep(1000);
            }
        } catch (JMSException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (JMSException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }

    static void send(Session session, MessageProducer producer, int label) {
        try {
            String s = "消息: " + label;
            TextMessage msg = session.createTextMessage(s);
            System.out.println("sending: <" + s + ">");
            producer.send(msg);
            session.commit(); // 提交之后,消息才会发送。之后立即进入下一个事务
        } catch (JMSException e) {

        }

    }
}


// Reciever.java
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MessageConsumer;
import javax.jms.Session;
import javax.jms.TextMessage;

import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;

public class Reciever {

    private static final String HOST = "tcp://localhost:61616";

    public static void main(String[] args) throws InterruptedException {
        ConnectionFactory factory;
        Connection conn = null;
        Session session = null;
        Destination destination = null;
        MessageConsumer consumer;
        factory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER, ActiveMQConnection.DEFAULT_PASSWORD, HOST);
        try {
            conn = factory.createConnection();
            conn.start();
            session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
            destination = session.createQueue("Test");
            consumer = session.createConsumer(destination);

            for (;;) {
                try {
                    TextMessage msg = (TextMessage)consumer.receive(1000);
                    if (msg != null) {
                        System.out.println("recieved: " + msg.getText());
                    }
                    Thread.sleep(3000);
                } catch (IllegalStateException e) {

                }
            }

        } catch (JMSException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (JMSException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
}

两段代码,非常容易,这里都是使用点对点(PTP)方式,一个消息只能由一个接受者接收并处理。其中使用过的API都是JMS的标准接口,可以查看JMS的API doc来获取更多信息。注释中,有一句“设置消息持久化”,所谓的消息持久化,是指将消息保存介质中,即使broker突然死掉了,重新启动broker之后,也可以获得之前的未处理的消息,持久化的方式在配置文件一节会说到

配置文件

有了初步认识之后,再来看看配置文件。我主要关注broker这个节点,这个节点的属性brokerName应该是唯一的,dataDirectory指定了当前broker存放数据(比如持久化的消息)的目录。

persistenceAdapter节点

这个节点配置消息持久化的方式,有AMQ、KahaDB、JDBC、LevelDB四种,从5.4版本起,KahaDB作为默认持久化方式。其中,JDBC是将消息持久化到数据库,KahaDB和LevelDB是基于文件的本地数据库,而AMQ则是一种文件存储形式。具体可以参考ActiveMQ持久化方式

transportConnectors

这个节点配置客户端连接到ActiveMQ Broker的方式。ActiveMQ支持多种连接方式,包括tcp、vm、amqp、stomp、mqtt等多种。一个连接是一个transportConnector节点,每个Broker可以配置多个连接,连接最重要的是uri属性,其指明了客户端连接Broker时的地址。具体请参见官方文档Configuring Transports

集群

集群分为两种,一种是Master Slave Cluster,另一种是Broker Cluster。

Master Slave Cluster

主从模式,可以完成故障恢复,但是没有负载均衡的能力,即同一时刻只有一个Broker(Master)在处理,其他的Broker等待(只是复制Master的状态,但是不进行任何处理)。主从模式又可以分为三种

Pure Master Savle Cluster

这是最简单的方式,这种方式下,只能有一个Slave Broker。Master无需额外配置,Slave可以采用如下的配置文件

<broker masterConnectorURI="tcp://masterhost:61617" shutdownOnMasterFailure="false"> 
    ...
    <transportConnectors>
        <transportConnector uri="tcp://slavehost:61616"/>
    </transportConnectors>
</broker>

其中,masterConnectorURI指明了Master,shutdownOnMasterFailure指明在Master失效后,Slave是停止还是成为新的Master继续运行。

在客户端连接的时候,应该采用failover://(tcp://masterhost:61617,tcp://slavehost:61616)?randomize=false作为URL连接ActiveMQ

Shared File System Master Slave

共享文件系统的主从模式,这个模式是多个Broker使用相同的目录作为消息持久化的存储地址,利用文件锁实现主从模式。获得文件锁的Broker是当前的Master,Master失效后,其余的Slave中,获得文件锁的Broker成为新的Master。所有Broker的配置文件中,都需要做如下配置

<persistenceAdapter>
    <kahaDB directory="/activemq/data"/>
</persistenceAdapter> 

其中directory属性值必须保持相同,可以采用其他的持久化方式

JDBC Master Slave

和Shared File System Master Salve相同,只不过是持久化方式改为了数据库,配置如下

<broker ...>
    <persistenceAdapter>
        <jdbcPersistenceAdapter dataSource="#mysql-ds"/> 
    </persistenceAdapter> 
</broker>
<bean id="mysql-ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/test?relaxAutoCommit=true"/>
    <property name="username" value="username"/>
    <property name="password" value="passward"/>
    <property name="poolPreparedStatements" value="true"/>
</bean> 

其中dataSource指定了数据库源,需要在配置文件中设置一个id与之相等的bean配置详细的数据库信息。

Broker Cluster

多个Broker组成网络,这种集群有负载均衡的能力,采用这种方式的集群,在一个Broker失效后,会连接到另外一个Broker上,但是失效的Broker上的消息,在该Broker恢复之前,不能被其他Broker获得并处理。失效的Broker恢复之后,持久化消息恢复,非持久化消息将会丢失。

这种集群有动态发现和静态发现两种配置方式。区别是,静态发现需要配置在配置文件中制定所有的Broker的地址,而动态发现则无需指明,由Broker自己去发现其他的Broker。

静态配置文件如下

<broker brokerName="receiver" persistent="false" useJmx="false">
    <transportConnectors>
        <transportConnector uri="tcp://localhost:61616"/>
    </transportConnectors>
    <networkConnectors>
        <networkConnector uri="static:(tcp://localhost:61616,tcp://remotehost:61616)"/>
    </networkConnectors>
</broker> 

此时客户端连接时,应使用failover://static://(tcp://localhost:61616,tcp://remotehost://61616)作为URL。

动态配置文件如下

<broker brokerName="receiver" persistent="false" useJmx="false">
    <transportConnectors>
        <transportConnector uri="tcp://localhost:61616" discoveryUri="multicast://default"/>
    </transportConnectors>
    <networkConnectors>
        <networkConnector uri="multicast://default"/>
    </networkConnectors>
</broker> 

此时客户端在连接到ActiveMQ时,应使用discovery://(multicast://default)作为URL。

总结

ActiveMQ作为一个高性能的消息队列,可以满足我们的使用需求,并且,其配置使用都还算简单,没有门槛,这是我最喜欢的地方。

最后,集群一节中,大量参考了Ac​t​i​v​e​M​Q​集​群​的​使​用​与​配​置一文,有关更多的集群配置内容,可以参考。

《机器学习》读书笔记一

机器学习已经取得了巨大的成就,语音识别、自动驾驶、人机对弈等等,这些都说明了机器学习的强大之处。而自己研究生阶段的读研方向,已经基本确定,就是数据挖掘和机器学习,趁现在还有时间,可以先自己学一下(暑假总想看,结果每次都是看一会儿就打开电脑,然后就没有然后了。。。)

选这本书,是因为这本书适合入门,比较简单。而这个系列博客就是自己的笔记了,当个备忘录,或许还会有一些自己的想法在里面

OK,Go

学习问题的标准描述

*学习*的定义如下,这里的“学习”和我们日常生活中的“学习”意义并不完全相同

对于某类任务 T 和性能度量 P,如果一个计算机程序在 T 上以 P 衡量的性能随着经验 E 而自我完善,那么我们称这个计算机程序从经验 E 中学习

也就是说,为了很好的定义一个学习问题,必须明确三个特征:*任务的种类 T*,*衡量任务提高的标准 P*,*经验的来源 E*,书上给了一些例子,这里只写出关于西洋跳棋的例子

对于西洋跳棋问题

  • 任务T:下西洋跳棋
  • 性能标准P:比赛中击败对手的百分比
  • 训练经验E:和自己对弈

我们当然可以提出不同于以上的T、P、E,这些都没有问题,只要有足够的理由即可。

设计一个学习系统

用的就是西洋跳棋的例子,问题是这样的,我们需要设计一个下西洋跳棋的程序,让这个程序在世界锦标赛与人类选手进行比赛,用其获胜的百分比作为性能的衡量标准。

如前面所说,一个学习问题有三个特征,现在任务 T 已经确定了和性能标准 P 已经确定,还有一个经验 E 需要确定。对于经验E,有三个关键的属性:

  • 训练经验能否为系统的决策提供直接或者间接的反馈
    对于西洋跳棋,直接的样例如各种棋盘状态和相应的正确的走子,间接的样例如过去的对弈序列和最终的胜负。间接样例中,非终局的走子必须通过最终对弈的输赢来判断,这又涉及到信用分配问题,也就是每次走子对最终结果的贡献(这是一个非常困难的问题)
  • 学习器在多大的程度上控制训练序列 学习器依赖施教者选取棋盘状态和正确走子,或者提出自己认为模糊的棋盘状态并向施教者询问,或者完全自主控制训练。而完全自主训练,也可以分为每次训练一个全新的棋盘状态,或者从已知的最有效的走子路线稍作变化
  • 训练样例能多好的表示实例分布 训练样例和实例分布在绝大多数情况下书不同的,而系统最终的性能是通过实例分布来衡量的。目前多数的机器学习理论都依赖于训练样例与实例分布一致这一假设,但是在实践中需要清楚,这一假设经常不成立

在这个例子中,我们选择程序自己和自己对弈,因为,训练经验可以为系统提供间接的反馈(因为自己和自己对弈,对于对弈之中的棋盘状态,不能确定其最佳走子,而只能提供给系统一次对弈的走子路线以及结果),完全自主的训练(这个显然,自己和自己下棋,当然不需要外界的施教者),训练样例不能完全的表示实例分布(这个是当然的,甚至不能较好的表示实例分布,这取决于自我对弈的选择棋盘状态的策略)

现在,学习问题已经确定,不过,对于实现这个系统而言,这显然还不够,因为我们需要的操作上的定义,而不是理论的定义。因此现在需要选择:

  • 要学习的知识的确切类型
  • 对于这个目标知识的表示
  • 一种学习机制

选择目标函数

这一步是决定要学习的知识的确切类型以及执行程序如何使用这些知识。我们的目标其实是在西洋跳棋的合法走子中选择最佳的走子,这种任务代表了一类学习任务:已知一个巨大的搜索空间(合法的走子),但是最佳的搜索策略未知。很多最优化问题都可归于此。

我们学习的目标其实就是一个对于一个给定的棋盘状态,能够作出最佳走子选择的一个函数,这里称之为 ChooseMove,可以表示为$$ChooseMove:B \to M$$。现在我们已经把我们的提高任务 T 的性能 P 的问题简化为了寻找 ChooseMove 这样的一个特定函数。但是这个 ChooseMove 函数的学习是非常困难的,因为提供给系统的是间接的经验,从一个棋盘状态选择一个最佳走子,就像之前所说,会涉及到信用分配问题。

而另外的一个选择是,对于每个棋盘状态,给出一个打分,最后我们就可以通过打分来决定一个棋盘状态的最佳走法。相比于ChooseMove,这应该是一个更简单的目标函数。这里称之为 V,表示为 $$V: B \to \mathscr R $$

现在任务就是确定这个函数 V,虽然任何一个能够对较好的棋盘状态打出较高分的函数都可以,我们还是应该确定一个特定的函数 V,我以为,这个函数应该越简单越好。

书中是如下定义,对于集合 B 中的任意一个棋盘状态 b

$$ V(b) = \begin{cases} 100, & \text{若 $b$ 是最终的胜局}
-100, & \text{若 $b$ 是最终的负局}
0, & \text{若 $b$ 是最终的和局}
V(b’), & \text{$b’$ 是从 $b$ 开始双方都采取最有对弈后可到达的终局} \end{cases} $$

可以看出,这是一个递归性的定义,也正是因此,其运算效率不高。对于最后一种情况,决定 V(b) 需要向前搜索到达终局的所有路线,这显然不现实。因此这个定义是一个*不可操作*的定义。这种情况下,学习任务被简化为发现一个理想的目标函数 V 的可操作描述。通常完美的学习一个 V 的可操作定义是非常困难的,因此,事实上,我们一般仅仅希望我们的学习算法得到一个近似的目标函数,故而学习目标函数的过程通常称之为函数逼近。下面将会用$$\hat V $$表示学习到的函数。

选择目标函数的表示

$$\hat V $$的选择有很多,一般有两个方面要考虑:一是我们总希望选取一个非常有表征能力的描述,以尽最大可能的逼近理想函数 V;另一方面,表征能力越强的描述需要越多的训练数据。书中为了简化,选择了一种非常的简单的表示方法,即

$$\hat V(b) = w_0 + w_1x_1 + w_2x_2 + w_3x_3 + w_4x_4 w_5x_5 + w_6x_6 $$

其中,

  • $$ x_1 $$:棋盘上黑子的数量
  • $$ x_2 $$:棋盘上红子的数量
  • $$ x_3 $$:棋盘上黑王的数量
  • $$ x_4 $$:棋盘上红王的数量
  • $$ x_5 $$:棋盘被红子威胁上黑子的数量
  • $$ x_6 $$:棋盘上黑子威胁的红子的数量
  • $$ w_0 \text{到} w_6 $$:权,由学习算法来选择,学习的目标就是确定这些权

选择函数逼近算法

首先,每个训练样例都是一个有序偶,$$ $$,表示棋盘状态以及对应的训练值。之后,需要估计训练值,对于非终局的棋盘状态,要确定其评分并不容易,最终的结果并不能确定一个中间状态的好坏,这很容易理解。虽然如此,确有一个比较简单的办法可以取得不错的效果,这个方法可以如下表示

$$ V_{train}(b) \leftarrow \hat V(Successor(b)), \text{$successor(b)$ 表示 $b$ 之后再次轮到程序走棋时的棋盘状态} $$

虽然不容易理解,不过在数学上已经证明,这种方法可以近乎完美的收敛到$$ V_{train} $$ 的估计值

接下来要做的就是调整权值,选择最适合权。什么叫做最适合呢?这是需要定义,也就是定义最佳拟合,一种常用的方法是把最佳的假设定义为是训练值和假设预测除的值之间的误差的平方和最小,即最小误差平方逼近,公式如下

$$ E \equiv \sum{<b, V{train}(b)> \in training\ examples }(V_{train}(b) - \hat V(b))^2 $$

现在需要一种算法,在新的训练样例来到的时候,能够更新权值,并且对估计的训练数据中的差错有较好的健壮性。LMS(least mean squares,最小均方法)就是一个这样的算法。对于每一个训练样例,LMS会将权值调整到减小误差的方向。其算真如下

对于每一个训练样例 $$ $$

  • 使用当前的权值计算 $$ \hat V(b) $$
  • 对于每一个权值,进行如下更新 $$ w_i \leftarrow wi + \eta(V{train}(b) - \hat V(b))x_i,\ i \in {0, 1, 2, 3, 4, 5, 6 } $$,其中 $$ \eta $$是一个小的常数,如0.1,用来调整权值更新的幅度

在一定的条件下,LMS可以证明收敛到 $$ V_{train} $$ 的最小误差平方逼近

最终设计

最后的,我们的设计是一个循环的圈(书上有图,这里不画了,用语言描述以下),是这样的一个循环

新问题(初始棋局)—> [执行系统] — 解答路线(对弈历史)—> [鉴定器] —训练样例(有序偶序列)—> [泛化器] — 假设 —> [实验生成器] — 新问题

  • 执行系统:用学会的目标函数解决任务,生成解答
  • 鉴定器:以执行系统输出的解答作为输入,输出一系列的训练样例
  • 泛化器:根据鉴定器输出的训练样例学习新的目标函数,新的目标函数可以覆盖这些样例以及一些样例之外的情形
  • 实验生成器:以当前的假设作为输入,输出一个新的问题供执行系统取探索

许多机器学习系统都可以用这四个模块刻画。

对于西洋跳棋问题来说,如果目标函数真的可以表示为这些特定参数的线性组合,那么程序学习到这个目标函数的可能性很大,也就是说程序的正确性会很好,否则最多可以学到一个合理近似,毕竟一个程序无法学习这个程序根本无法表达的东西(比如目标函数是个二次函数,而这个函数只能表示一次函数,当然不能学习到这个二次的函数)。

机器学习的一些观点

在机器学习方面,一个有效的观点是机器学习问题经常可以归结于一个搜索问题,即对非常大的假设空间进行搜索,以确定最佳拟合观察到的数据和学习器已有知识的假设。意思就是在这个非常大的假设空间进行搜索,寻找到特定的假设,这个假设能够最佳的拟合观察到的数据与学习器已有的知识。。好吧,这句话比较绕口,是一个语法问题。。。

好了,第一次终于写完了,是到目前为止写的最长的一篇博客。。基本上照搬书上的东西,不过由于要写出来,相比于之前读本章,读得更加透彻了,毕竟写出来,不能通篇都是错的的东西。。

为了写数学公式,特意给博客加上了MathJax支持,还换了Markdown的解析器。。。不知道下一篇是什么时候呢。。。

尝试MongoEngine

MongoEngine之于MongoDB来说,就类似于SQLAlchemy之于关系型数据库,是ODM。许多人对于MongoDB之类的文档型数据库,不愿意用ODM,认为会拖累速度,而且似乎是又回到了关系型数据库。对于此,我只能说,我是支持使用ODM的,因为我确实感觉到ODM给我的编程提供了许多便利。

回到MongoEngine,使用MongoEngine,首先要有定义数据库模式,如下

from mongoengine import *

class User(Document):
    id = SequenceField(primary_key=True)
    name = StringField(unique=True)

class Reply(EmbeddedDocument):
    user = ReferenceField('User', required=True)
    content = StringField()
    time = DateTimeField(default=dt.utcnow())

class Article(Document):
    title = StringField(required=True)
    author = ReferenceField('User', dbref=True)
    content = StringField()
    replies = ListField(EmbeddedDocumentField(Reply))
    create_time = DateTimeField(default=dt.utcnow())
    update_time = DateTimeField()

以上的几个类,就完全可以表示一个博客系统的数据库模式,包括了用户、文章、和回复。其中User和Article是两个独立的实体,而回复则是文章的一个属性,所以回复不是一个完整的实体,因此是一个EmbeddedDocument的类型,表示内嵌文档。而Document类的子类会在MongoDB中建立一个collection。这样,以上的模型最终的collection是两个,user和article。关于正常模型,还有几点要说的地方

  • SequenceField是一个自增字段。MongoDB本身不提供自增字段,其实现方法一般是findAndModify方法,看这里Create an Auto-Incrementing Sequence Field
  • ReferenceField表示一个引用,类似于关系型数据库的外键,其中dbref默认为False,表示在建立的模型中,这个字段只保存对应的文档记录的主键,为True则表示存储为dbref(关于dbref,可以看着里Database References),查询的时候,MongoEngine会自动帮你把这个字段的东西取出来
  • DateTimeField,MongoDB中的时间没有时区,所以保存为UTC时间是最好的选择

之后,查询就比较简单了,比如在首页,想要显示全部的文章,

articles = Article.objects

就是全部的文章的生成器,排序或者限定结果数量,可以这样

articles = Article.objects.order_by('-create_time').skip(10).limit(10)

这表示取按创建时间降序排列的第10到第20篇文章。而现在,如果要显示作者的信息,则可以用article.author.name来表示作者的名字,articleArticle类的实例。而添加回复,也很简单

r = Reply(user=User(id=1)) # 必须用主键表明User,或者可以从数据中选择一个User的实例
r.content = 'xxxxxxxxxx'
article.replies.append(r) # 如果原本没有回复,article.replies是[]
article.save()

MongoEngine简化了程序员的劳动,相比于直接用PyMongo操纵MongoDB,程序员可以把一些重复性的代码直接交给MongoEngine,自己只需关心数据的逻辑即可。之前用SQL的时候,感觉ORM好麻烦,不灵活,结果数据层搞得一团糟。而且,貌似MongoEngine带来的性能损失是在可接受范围内,不会对程序的性能造成太大影响。当然,如果觉得自己完全可以驾驭PyMongo,自己写数据模型,当然也是没问题的。

Linux下关闭多个进程

很多时候,需要同时关闭一系列的有着相同的关键字的进程,比如说,开着十多个网页的chrome死掉了这种情况。。

在Linux下面,敲几个命令就OK了,以下每行都可以,有些区别

ps -e | grep chorme | cut -c 1-5 | xargs kill -9
ps -e | awk '$4=="chrome" {print $1}' | xargs kill -9

ps -e的输出如下

12040 ?        00:00:00 kworker/1:1
12041 ?        00:00:00 kworker/0:0
12052 ?        00:00:09 chrome
12119 ?        00:00:00 kworker/3:0

这个命令输出了当前的所有进程信息,最前面的就是进程的pid。第四列是进程的名称

grep chrome就是查找含有chrome的行并输出。cut -c 1-5意思是对输入的每一行取第1到5个字符(恰好是进程的pid)输出。两个命令合起来,就是输出进程名含有chrome的进程的pid。

awk '$4=="chrome" {print $1}'做了同样的事,不过这个命令输出的是进程名恰好是chrome的进程的pid,而不仅仅是包含。

xargs kill -9这个命令,输入以换行或者空白分割之后,作为参数执行一遍或者多遍后面跟随的命令

最后感叹一下,shell很强大!

安装GitLab

在V2EX上面看了一个如何管理自己的代码的帖子,里面有个回答是使用gitlab,于是搜了一下。。可以这么说,gitlab是开源的github,可以在自己的电脑上搭一个私人的git托管服务,相当不错。对于自己来说,其实就是折腾啦,可以不用把所有的代码都扔到github上面,但是有个不好就是在自己的机器上面,一旦系统挂了,代码就没了。。

安装就是跟着官方的安装文档,一路下来就可以了,由于网络速度以及各种其他问题,我装了两天。需要提醒的一点是,文档里面,安装路径是/home/git/gitlab,建议不要修改,因为有些地方是硬编码了这个路径的,如果修改可能会导致网站不能跑起来。

在安装过程中的需要bundle install的地方,由于众所周知的原因,建议把Gemfile中的地址修改为http://ruby.taobao.org,加快速度。

在安装过程中,我遇到的一些问题:

  • 在Gemfile中有些这样的写法require: :xxx,如果bundle install出现问题,可以把这种写法改成:require => xxx。这个原因是ruby 的版本太老不支持新的语法(但是我的ruby是最新的也报错了。。)
  • 在执行bundle install时,提示需要 ruby的版本大于1.9.2,而实际上我的机器上ruby是1.9.3,这时执行sudo apt-get install gem之后重新执行改命令
  • 安装数据库时,未修改配置文件,导致登录数据库错误。这个只要修改了配置文件即可,注意应该修改production中的配置

安装完成后,有个管理员账户

email:    admin@local.host
password: 5iveL!fe

如果要增加用户,只能通过管理员账户新建用户,不能自由注册,毕竟gitlab是私人的托管服务,面向的是小型的团队。

装完之后用了一下,虽然和github很像,功能也比较齐全,和github还是有所不同。建立一个新的项目的时候,不会自动建立新的repo而是需要自己手动建立并push。。当然还有其他区别,在此不一一列举了

自己写sublime text插件

起因是,自己写Python基本上使用sublime text 2。但是,文件头部的代码

#! /usr/bin/env python
# -*- coding: utf-8 -*-

如果每次都要敲的话,很麻烦。而如果不敲的化,一旦涉及中文,又会报错,就产生了写了一个插件的想法。。问题好久了,直到今天才动手,惭愧一下。。

思路应该是比较简单,就是判断文件类型,然后在头部插入特定的字符串。于是乎,开始在网上查找sumblime text 2的API还有插件开发教程。这是API文档中文翻译版,看一下Default里面的自带插件,就可以动手了。

最初的代码中,字符串是硬编码的。后来,则是从模板文件中读取字符串。在这一步,遇到了一个很诡异的问题,是这样的:模板文件保存在templates文件夹下面,在HeadTemplate.py中(我的插件的名字是HeadTemplate),

def get_tpl_file(settings, filename):
    template_root = settings.get('template_root')
    path = os.path.join(os.path.dirname(__file__), template_root)
    return os.path.join(path, filename)

但是这个函数的返回的结果是/home/username/templates/filename,而实际上,我认为应该返回的是/home/username/.config/sublime-text-2/Packages/HeadTemplate/templates/filename。打印os.path.abspath(__file__),得到的结果是/home/username也就是我的家目录。开始以为是由于隐藏文件夹的缘故,不过试验之后,发现与之无关。为什么会有这个结果,具体原因到现在我也还弄不清楚。不过,解决方案倒是找到了,感谢SErHo的代码

PACKAGE_NAME = 'HeadTemplate'
PACKAGES_PATH = sublime.packages_path()

def get_tpl_file(settings, filename):
    template_root = os.path.join(
                    PACKAGES_PATH, 
                    PACKAGE_NAME, 
                    settings.get('template_root'))
    return os.path.join(template_root, filename)

在其他就没有什么了,按部就班的写下来就好了。代码放在了github上,https://github.com/iEverX/HeadTemplate

算上空行不到50,却写了一个下午。。

Javascript遍历DOM树

这个博客是利用Jekyll在github pages上搭建的,显示在首页的文章,如果用{{ post.content | truncate: 200 }},原有的格式不能完全保持,且有时在最后会有乱码。而{{ post.content | truncatewords: 50 }}也有不能保持格式的问题,而且对于中文来说,word的概念大概就变成了句子,截取的长度不能确定。本来truncatehtml这个插件可以解决格式保持的问题,但是出于安全的考虑,github pages不允许运行插件,所以。。。

这个问题有很长时间了,今天闲的没事,用js写了一下。就是遍历DOM树,叠加文本节点的长度,当长度达到既定长度时,其后所有的节点修改style为display: none,用jQuery就是hide(),具体到自己的博客,代码如下,

$(function() {
    function traverse($node, len, maxCount) {
      var reachMaxCount = len > maxCount;
      if (reachMaxCount) {
        $node.hide();
      }
      var $contents = $node.contents();
      for (var i = 0; i < $contents.length; ++i) {
        if (reachMaxCount) {
          $contents.eq(i).hide();
          continue;
        }
        if ($contents[i].nodeType == 3) { // TextNode
          var tmp = len;
          var s = $contents[i].nodeValue;
          len += s.length;
          reachMaxCount = len > maxCount;
          if (reachMaxCount) {
            $contents[i].nodeValue = s.substring(0, maxCount - tmp);
          }
        }
        else if ($contents[i].nodeType == 1) { // Element
          len = traverse($contents.eq(i), len, maxCount);
        }
      }
      return len;
    }

    $('.post_at_index').each(function() {
      traverse($(this), 0, {{ site.description_length }});
      var thisUrl = $(this).siblings().first().children().attr('href');
      $(this).after('\n<a href="' + thisUrl + '" rel="nofollow">' + 'Read More ...</a>');
    });
});

对于js,我都会使用jQuery,这个同样如此,需要jQuery的支持。在最后加上了了read more,指向页面。写完之后,发现代码不长,不过却花了我一个下午的时间,主要是对js太不熟悉了,上网各种查,开始是用的children()这个方法,但是这个不能处理很好,后来改成了contents(),不过已经浪费了很长时间了。。

另外吐槽一下Liquid这个模板,规避标签太麻烦了,比如要打一个

{{ some tag }}
{% some tag %}

需要如下这样才行,

{{'{'}}% raw %}{{ some tag }}{{'{'}}% endraw %}
{{'{'}}% raw %}{% some tag %}{{'{'}}% endraw %}
或者
{{'{'}}{ some tag }}
{{'{'}}% some tag %}

其他的模板带语言,要输出自身的控制标签确实都不容易,但是Liquid已经不是不容易,而是复杂了。。我感觉,如果有两个停止解析的标志,比如说

{! here is not parsed !}
{@ here is not parsed @}

那么,如果有如果有显示{! !},只需要{@ {! !} @},目前,Liquid有标签rawliteral可以使用,但是我的试验结果是literalraw似乎不太协调,不知什么原因,渲染结果总是与预想的结果不一致。。看了Liquid的issue,发现有可能是Jekyll的问题,也不清楚到底是怎么回事,不去想了。。