曾经有人问过我一个问题什么是TCP复用,我当时没有回答上来,后面我又遇到一些并发性能问题的时候,我才开始慢慢明白为什么会有这个问题,以及这个问题背后的秘密

其实当时应该他想考我的是爬虫的请求优化,准确来说是HTTP持久连接(HTTP persistent connection),并不是TCP复用,这才导致我当时查阅很多资料,并没有发现TCP复用能优化客户端,因为TCP复用是服务端的事,现在就让我从源头开始慢慢解读这个问题

起因

我们知道我们每次发的HTTP请求在底层都是一个套接字的通信,我们可以从底层开始做一个测试

我们使用个for循环,申请1024个socket

import socket
l = [ ]
for  i in range(1024):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('www.baidu.com'),80))
    print(i)
    l.append(sock)

这个过程有点慢,但是你会发现在申请到1000左右的时候,会直接报Open too many file 这个错误,但是我们并没有打开文件,为什么会报这个错误

原来在Unix系统下,我们申请的套接字也就是socket在底层是以文件的形式存在的,客户端通过申请一个socket来写入和接受服务端的请求,这是一个非常重要的概念,对我们后面解析库函数有很大的帮助。

由于系统资源有限而且打开很多文件系统响应会变慢,所以Unix系统或者Windows都对单个进程申请套接字有限制,在Unix系统下我们可以通过ulimit -n查看这个值,在笔者的Ubuntu上这个值为1024,基本没有修改过都是这个值,我们可以通过我们可以在命令行执行ulimit -HSn 4096临时增加至4096,

所以我们一般来说单台机器单个进程最多只能并发1024个请求,在不修改配置的情况下这个值是固定的,所以我们提高并发数只有两种方法

  1. 修改系统配置
  2. 使用多进程

在写这篇文章之前,我一直以为HTTP复用能在作用在并发上提高爬虫性能,但是其实并不是,它能提高性能但是却不是在并发上提高,接下来我们仔细介绍HTTP复用是怎么提高爬虫性能的

HTTP复用

说道HTTP复用,我们不得不介绍一下HTTP和TCP协议,我们都知道Internet是由OSI七层协议构成的,但是OSI只是规定了框架,具体协议我们是通过TCP/IP来实现的

我们先来说说这个TCP,我们都说互联网能够发展到现在这么稳定可靠多亏了这个TCP可靠协议,但是这个可靠是要付出代价的,建立一次连接的过程要经过三次握手,断开的过程也得四次分手,而且这个连接的过程完全不涉及我们要请求的内容,我们知道爬虫一般请求一个站点只有通过一两次请求就行,如果每次请求都得握三次手,还得分四次手,这样的代价也太大了

所以HTTP的复用优化的方向就是减少TCP的连接,谈到如何减少TCP连接,我们就得说说HTTP长连接(HTTP persistent connection)

HTTP长连接

在HTTP1.1规定了默认都是长连接,TCP不断开,并且在请求头添加一个Connection的header,如果是值为keep-alive则保留TCP连接,假如为Close请求完成之后就会关闭,在HTTP1.0的下默认为关闭状态

怎么来理解这个长连接呢,我们都听说过HTTP是无状态的这句话,从HTTP协议上来看,服务器客户端就是一个“Request”,“Response”组成,无论多复杂的页面都是由一个个“Request”组成

为了更好的理解上面的话,我们回到那个套接字,我们把HTTP请求比作打电话,对于每个电话,我们只需要先拨号,然后滴滴滴三下后确定我们同对面连上了(服务器“协商”好),然后我们把我们要说的话通过话筒传给对方,等我们说完之后,由于信号差,对面听完还要想怎么回,然后我们安安静静的在听筒那等,等他想好说什么,在慢慢的说给我们听。

在HTTP1.0的时代,我们每次拨完一次好,说完一句话,听完对面的回应后,我们就会挂断电话,如果我们还想说就得再重复这个过程,在HTTP1.1下我们增加了长连接这个概念,就是如果你想这个电话里多聊几句,那么就在最后加上“你等下不要挂了,我还要说”(在header加上“Connection: Keep-alive”),那么对方就不会挂断电话,等它说完之后也想你一样在听筒那而等着,这样我们就省掉了一次拨号的时间

我们现在了解为什么HTTP复用能够节省爬虫的性能了,接下来我们就从编程语言对HTTP复用的实现上了解如何实现HTTP复用

存贮单元—ConnectionPool

在介绍ConnectionPool之前我们先简单介绍一下HTTP复用的具体表现

TCP与URL的关系

我们知道HTTP复用的是TCP的连接,而TCP连接由四个部分组成

  1. 本地ip
  2. 本地port
  3. 服务器ip
  4. 服务器port

简单来说就是两个二元组(local_ip, local_port), (server_ip, server_port)

但是我们发一次的请求是一般是通过URL,也就是类似“http://www.baidu.com”,这样的url来请求的,这个同我们TCP有什么关系呢?

首先介绍一下“http”代表通信协议,这里使用的是HTTP协议,“://”后面的就是请求的域名,域名后面如果有冒号就是我们请求的端口号这里没有,根据HTTP协议这里默认是80端口(HTTPS是443),域名后面的就是请求路径,这里也没有就默认问“/”,也就是我们通过这个“url”就知道我们这次请求的具体位置了,现在我们找到了端口,但是请求的IP在哪呢?

这里就要介绍一下DNS了,我们为了让我们的站点更好记,我们使用域名代替ip地址,通过在DNS服务上注册我们域名,以及绑定我们域名对应的IP地址,我们就能让计算机通过域名来转换成IP地址,这里就不详细介绍了

所以呢我们现在了解了,一个TCP连接只是涉及到URL的域名和端口号,我们请求站点的时候主要是通过不同的路径来获取内容,所以我们可以很清楚的知道,只要我们URL的域名和端口一样,那么我们所以的URL都能共用这个TCP接口

ConnectionPool的实现

简单来说为了实现HTTP复用,我们只需要保存TCP连接就行了,但是通过前面我们知道,我们保留的TCP连接必须和你要请求的url要域名端口一样,有时候一个站点的服务可能由多个域名多个端口组成,所以原本我们只要用一个变量保留上一次请求的TCP连接,为了程序更加健壮,我们需要一个TCP连接池,存贮不同的TCP连接。

每次新的URL来的时候我们就是先从TCP连接池中查看有没有相同的域名和端口,如果有就用它发请求,如果没有就新建一个TCP连接,这就是TCP连接的基本原理,当然还要一点编程的时候要注意,我们从池子里面取出一个用完必须放回,否则池子用完了又得新建,那就完全丢掉了复用这个概念了

HTTP复用在Requests的具体表现

前面介绍了一大堆概念,但是从头到尾如果让我们自己来做一个实在太难了,幸好我们有Requests这个库,它的Session对象在文档介绍了它就维护了一个TCP连接池并且能够复用TCP连接

接下来我们就从代码入手来更好的理解这个进程池的高级用法,我们为了更好看到每一次请求底层的操作,我们这里自己先自己搭建一个本地服务器,我们使用Flask来搭建一个本地服务器
新建一个web.py文件,在运行

from flask import Flask, request
from werkzeug.serving import WSGIRequestHandler

app = Flask(__name__)
WSGIRequestHandler.protocol_version = "HTTP/1.1"


@app.route('/')
def hello_world():
    return '%s %s' % (request.remote_addr, request.environ.get('REMOTE_PORT'))


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

这里我们在8000端口开了一个服务器并且设置为HTTP/1.1协议,我们返回用户请求的ip和端口

接下来我们开一个Python解释器来看看这个进程池的用法

>>> import requests, logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> session = requests.Session()
>>> session.get('https://baidu.com')
DEBUG:requests.packages.urllib3.connectionpool:Starting new HTTP connection (1): www.baidu.com
            DEBUG:requests.packages.urllib3.connectionpool:http://www.baidu.com:80 "GET / HTTP/1.1" 200 None
<Response [200]>

看我们可以从打印的logging日志看到我们在进程池中新建了一个TCP连接,我们在试着再请求一次

>>> session.get('https://www.baidu.com')
    DEBUG:requests.packages.urllib3.connectionpool:https://www.baidu.com:443 "GET / HTTP/1.1" 200 None
<Response [200]>

看我们的HTTP复用实现了,在同一个TCP连接中我们请求了两次

深入requests的ConnectionPool

在上面我们验证了requestsSession对象的确实现连接池,但是似乎requests并没有给我们接口来操作这个值,通过分析代码和资料,我们发现在Session初始化的时候,绑定了一个 HTTPAdapter对象,这个对象就是requests封装了urllib3.connectionpool.ConnectionPool来实现TCP池

我们查看这个HTTPAdapter文档发现它的用法是这个

>>> import requests
>>> s = requests.Session()
>>> a = requests.adapters.HTTPAdapter(max_retries=3)
>>> s.mount('http://', a)

我们可以通过创建将一个TCP池绑定到一个session对象上,我们可以看一下这个创建一个HTTPAdapter的参数

HTTPAdapter(self, pool_connections=10, pool_maxsize=10, max_retries=0, pool_block=False)

我们主要看这两个参数pool_connectionspool_maxsize,通过一番测试(比较长就不演示了,可以参考引用来进行实验),我们发现这个pool_connections主要控制TCP池的种类数,我们知道在进程池中我们可以有很多相同的TCP连接(主要是并发新建的),这些连接有些是连接相同的域名和端口,这个pool_connections就是控制有多少种类的域名和端口同时能够存在池中,那么这个pool_maxsize代表的就是池中不管种类有多少总共的TCP连接数

当然这里有个非常重要的知识点,TCP池并不会限制新建TCP连接,它只是限制存贮量和种类,这个知识点非常重要,这对后面我们理解异步请求时候为什么要限制并发数有非常大的帮助

TCP连接池的作用

经过上面的探索,我们知道TCP连接池一方面能够实现HTTP复用达到减少TCP连接时耗的作用,另一方面我们通过复用TCP连接可以节省套接字,避免经常碰到”Too many file“的错误,顺便提一下,由于TCP连接具有冷启动的特点,在刚连接上TCP时,速度会非常慢,只有系统发现负载不多才会恢复正常速度,所以这就是我们有时候用浏览器打开一个新页面要加载很久的原因。

前面一直在介绍HTTP复用的理论基础,最后我们实战演练一下在异步框架aiohttp使用HTTP复用

异步框架下HTTP复用

在这里我们使用Pythonaiohttp异步请求框架(在这里我们要求Python的版本必须大于等于3.5),aiohttp也提供了TCP连接池的功能,要想共享TCP连接池,我们先新建一个Session对象

connector = aiohttp.TCPConnector(limit=50)
session = aiohttp.ClientSession(connector=connector)

我们直接创建了一个最大容量为50的TCP池,并把它绑定到session对象上,接下来先试试跑个200个请求(要先在按照前面的代码搭建本地服务器)

async def fetch(url, session):
    async with session.get(url, timeout=500) as resp:
        print(resp.status, await resp.text())

loop = asyncio.get_event_loop()

引用:

Requests’ secret: pool_connections and pool_maxsize

Making 1 million requests with python-aiohttp

平常在Django项目中大量使用自增这个键,平常都是使用ORM,很少去了解这个东西在数据库中具体使用,最近遇到要备份和复原数据的事情,趁着这次好好探索一下这个自增键的使用

Django里面大部分都是将其作为Int自增主键来使用,第一个不需要维护一个唯一值,第二个使用Int作为主键的话,搜索和外键关联速度比较快。

我们这次从原生SQL出发,探索一下这个自增主键在数据库中的具体使用

新建数据库

我们先新建一个数据库

create table inc(
id serial not null,
name text
);

PG里面简单的使用serial关键字就会生成一个自增键,默认会在数据库新建一个索引表,例如上面就会新建一个inc_id_seq的索引表,这个字段类型为int,如果数据库很大,我们可以使用BIGSERIAL键申请一个bigint类型的字段

我们可以看一下这个索引表里面有什么

         Sequence "public.inc_id_seq"
    Column     |  Type   |        Value        
---------------+---------+---------------------
 sequence_name | name    | inc_id_seq
 last_value    | bigint  | 1
 start_value   | bigint  | 1
 increment_by  | bigint  | 1
 max_value     | bigint  | 9223372036854775807
 min_value     | bigint  | 1
 cache_value   | bigint  | 1
 log_cnt       | bigint  | 30
 is_cycled     | boolean | f
 is_called     | boolean | t

我们可以这个索引表其实就是维护了一个参数,通过字段我们可以知道,这是一个自增为1的键,下一个值为2,目前没有插入一个值

增删查减

我们通过一些基本操作来看看这个自增键的作用

  • 首先是插入
insert into inc (name) values ('1'),('2'), ('3');

我们插入三个值,我们再查看索引表,发现last_value变成了3

这个是没有指定id的值插入,我们试试显式声明插入

insert into inc  values (1, '1'),(2, '2'), (3, '3'), (4, '4');

我们惊奇的发现,在我们显式声明自增键的值的时候,索引表并没有变化,last_value还是3,这说明只有在不声明自增键,让数据库自己新建的时候,索引表才会更新

我们可以把自增键看做一个默认值,当没有给自增键赋值的时候,这个自增键会从这个键的索引表中得到下一次自增的值

所以我们再尝试使用不声明自增键值的方法插入一个新值

insert into inc (name) values ('4')

我们发现索引表中last_value变成了4

主键自增

由于我们在Django里面使用自增,一般都是将其声明为主键,设为唯一值,所以如果我们将声明表的结构变成

    create table inc(
id serial not null PRIMARY KEY,
name text
    );

上面的情况就不可能发生了,因为我们把自增键声明为主键,不过有意思的事就是如果你像上面一样指定了一个自增主键值为4,然后不指定再插入4,你会发现第一次会报主键不允许重复的错误,第二次则会成功插入,而且索引表的last_value变成了5

看来并不是每次成功的时候才会更新last_value值,只要让系统自己去申请自增值就会更新索引表,我尝试了对表的增删查改,发现只有insert并且申请自增值的时候才会更新索引表,而且这个索引表之后增加,不会减少,所以有时候你删掉最大的值,自增键默认又从最后一次开始更新

总结

在对单个数据表备份还原的时候,由于简单的使用了COPY命令进行备份还原,通过上面的探索我们发现如果涉及到自增主键的导入导出,在新表导入旧数据是不会出错的,但是由于我们没有考虑自增键的影响(我们导入自增键是显示赋值),在后面插入数据的时候有可能会报主键重复的错误

为了避免以后插入入数据出现这样的错误,我们有两种措施

  1. 使用COPY命令导入导出时候不获取自增键值
  2. COPY导入新表后自己更新索引表

第一种的话SQL比较繁琐(必须写出表所有字段值),推荐使用第二种

我们可以简单的使用

SELECT MAX(id) FROM your_table;

先获取自增键最大值,然后更新索引值(999为上面你获取的最大值)

SELECT setval('your_table_id_seq', 999, false);

当然我们可以将这条语句合正一句话

SELECT setval('your_table_id_seq', COALESCE((SELECT MAX(id)+1 FROM your_table), 1), false);

这样我们就可以开心的完成单表导入导出了

ps:
在使用COPY命令时必须是superuser才能从文件中读取和导入数据,最简单的方法是用superuser账号加权使用alter user xxx superuser,待倒完数据后再降权alter user xxx nosuperuser

其实这篇文章很早就像写了,但是自己也一直没有明白自己想写什么,直到最近自己慢慢才有一点思路

这篇文章并不想高谈阔论,只是自己的一些碎碎念,把自己对人生的一些看法的小总结。你可以把它当做一篇小说来看,我也想把它当做小说来写。

引子

中秋回家,同自己表妹聊天,她突然问了一句,大学哥哥没有谈一个女朋友,接着说道没有恋爱的大学是不完整的,我楞了一愣,一本正经的对她说道,大学其实就是培养自己一个完整人格的过程,在这个过程中我们学习并且养成自己一个完整独立认知。

回去之后我仔细想想,我好像并没有回答表妹的问题,但是我自己也陷入了深思,大学这几年到底对我干了什么。

错觉

先不说大学对我做了什么,我仔细想了想我自己对自己定位。

你觉得你很努力?

我一直觉得自己在大学还是很努力的,我没有沉迷游戏超过两天,我没有放弃学习新知识,在大学图书馆借了几百本书,可能比全班人加起来都多,大一到大三我经常去图书馆读书,尤其是大三,有时候会在图书馆读一天书

但是上面的上面全部我自己的错觉,我虽然不沉迷游戏但是经常会被游戏分心,我虽然读过很多书但是我一直是读那些别人认为是必读的书,而且我读过的书大部分都没有转换成为我真正的源泉

我就像一个饥渴的行者,在大河面前用手拼命的往口里塞水,我的确看了很多的书,但是这么多书就像流水一样,全部都流走了;这些知识对于我来说只是解渴之物,当我非常饥渴的时候,我会拼命的想得到它,但是当我满足的时候,这些东西就像泥土一样对我一文不值

所以这就可以解释为什么我每次借书的时候都是兴高采烈,但是当借回来时候往往翻了几页,然后就束之高阁,然后循环往复

你觉得你懂很多?

没有出去之前,我在大学社团里面干过不少项目,所以我有时候觉得自己技术很牛逼,我是大神级别的人。我懂很多别人不知道的知识,我用过很多框架,我知道怎么搭集群,我知道什么是机器学习,什么是分布式,什么是代码规范。

然而出去之后才发现这都是错觉,你做过很多项目,你经历过百万规模的并发吗,你了解很多框架,随便挑一个框架出来,你能说出它的优点和不足吗,你看过源代码吗,你知道如何保证上千集群的容错率,你知道什么是大规模机器学习吗

挑出任何一个你会发现自己一直处在皮毛阶段,有时候你会用你还年轻但是学习能力强来掩饰你的不足,但是这只是你的错觉吧,不懂就是不懂。

你觉得你不需要明确方向?

有段时间我一直很困惑自己未来发展方向,我搞过UI,搞过前端、后端,搞过机器学习、数据分析,搞过分布式、爬虫。编程语言更是多,C、C#、Python、Java、C++,node,JS。我对我自己的定位一直很模糊,我不知道我未来到底想干什么,我很羡慕那些从小就明确目标的人。

我一直为此苦恼,我也看过很多人的书籍、博客,我也看到过很多人写的相同的文章,在很长一段时间我都认为它是正确的,它告诉我你不需要明确你的职业规划,它给了很多有名的人例子,奥巴马、马云、马化腾、李开复他们在大学都不知道自己要干什么。在很长一段时间我都觉得大学就是应该多学东西,把东西学杂。

但是我仔细想想,这个也是错觉

我从大二就开始有转行的念头,当时我是web后端开发,我当时觉得有没有方向无所谓,只要你多学就行,就这样陆陆续续混杂看了一年多书,直到七八个月前,我才开始反思。

我开始明确我的目标,把它当做我要干一辈子的方向去搞,我开始扣书,像一个干涸的大地一样汲取天空飘下的雨滴,我这时候发现知识是那么的宝贵,自己是多么的“native”;从一窍不通到入门到小小成就只花了短短几个月的时间,完成了一年多都完成不了的入门。

当然我最终没有选择这个方向,但是这个过程我从来没有后悔过,而且在这个学习过程中,它帮助我更加了解我自己,而且节省了我选择的时间。

总结

好像我的大学一直全部都是由错误组成的,但是这些错误真的对我来说毫无作用吗。其实未必,当我学完第一门编程语言C的时候,所以的“错误”都在默默的发挥的作用,我用静态语言的辩证思想学Python,我用动态语言的思想反过来学习Java,好像全部的“错误”全部融合成为一个圈,

我们好像一直在害怕自己出错,其实慢慢的发现那些没有错误的人生不是完整的。

小时候很羡慕那些一直走在正确的道路上的人,也有时候会幻想成为他们一样的人。没错那样的人生固然完美,但是我更喜欢一直跌跌撞撞的我,或许我经常走在错误的道路上,但是我享受了沿途的风景,不论最终结果如何,人生的意义还是沿途的风景吧!

最近打了两个比赛,一直忙着工作和打比赛,没有时间总结,今天抽空好好总结一番

先说一下比赛结果吧,队名全为OfferGo唯品会购买预测第五名,携程房屋预测复赛第六名,两个比赛打的都不算太好,只能算勉强及格,虽然离大神的距离还有十万八千米,不过总算可以称的上入了门,现在来总结一下我入门的经验吧。

观察数据

我参加过很多群,发现很多新手缺乏观察数据的能力,他们每次进入一个群总是嚷嚷这让大神发baseline

这一点对于新手来说很不利的,比赛考的就是你对数据的掌握能力,你对数据把握的越好,你的比赛成绩就越好,要真正掌握数据就要从观察数据入手


在我看来观察数据主要从四个方面来,我总结为望闻问切

观察数据缺失值,缺失值对数据影响很大,有时候我们能够从缺失值里面了解很多信息,而且对于缺失值,后期我们对不同的缺失值要采取不同的手段,比如补全、统计占比、丢弃等等。

对于缺失值我一般从两个方面来观察

  • 全局观察
    • 一般采用datafram.info(null_counts=True, verbose=True)方法来观察全局数据缺失情况
  • 局部观察
    • 一般采用series.isnull().count()series.loc[series.notnull()]观察单一列表缺失情况

这个阶段我们主要从大的方向远远的一下数据,主要建立对数据的全局观。

对于数据来说,一般分为三种,一种为数值型数据(整数、浮点数、时间等),一种为字符型,最后一种为图像型,三种类型数据处理难度依次增强

对于大多数比赛都是设计前两种数据,第三种只有牵扯到图像处理才能遇到。对于前两种数据,我们在闻的阶段,主要是探查数据分布情况,了解数据分布情况,我们才能对症下药。

了解数据分布情况有两种方法

  1. 图像观察
  2. 数学统计观察

图像观察主要使用PandasMatplotlib绘图接口,或者使用seaborn(一个友好的封装了Matplotlib的包),一般我们可以从直方图、饼图、频率图方向来观察数据

数学统计我们主要采用Pandasdescribe方法,对于数值型数据,主要从平均值(mean)、中位数(50%)、标准差(std)、最大(max)、最小(min)、非空总数(count)来探测数据,对于字符型我们主要从最频繁的值(top)、最频繁的值的个数(freq),非空总数(count)、不相同的值(unique)。

通过上面两种方法,我们能够从数据分布的角度大致勾画出数据的轮廓。

比赛的目的就是找到最优解,而最优解的跟相关特征紧密联系的,你的特征对结果影响越大,你就要审这个特征

举个例子,我们要预测三组数据

  • 1 1.8 2
  • 2 3.5 4
  • 3 5.4 16

第一行为我们要训练的值,我们发现第二行的数据是第一行的1.8倍,而第三行只是2的次方,对于这两个特征来说,第二个特征就是最好的特征,我们只要建立一个映射,准确率能接近100%,而第三个特征对预测结果毫无联系,这个特征不但对结果没有作用,而且有时候会起到反作用

当然在这里我们举这么一个例子在实际中不可能遇到,我们遇到是更多数据,而数据之间的联系并不是这么简单的线性关系,但是线性关系有的时候能让我察觉到特征与预测值的关联,毕竟如果特征值是随机值那么与预测值之间的相关性是非常低的。

Python里面探测线性关系最简单的方法是调用特征值和预测值的相关性系数(corr),我们可以简单的使用df[['feature', 'target']].corr()就可以得到线性相关系数,这个数的绝对值越接近1,相关性越大,一般来说相关性越大和越好对结果都不好,最好的特征相关性处于中间位置。

相关性低我们可以理解,为什么相关性高反而不好呢,因为数据比赛里面给我们的数据大部分都是不平衡的数据,正负样本失衡,一般相关性很高的值一般为分类同预测值相同,比如一个二分类问题,预测值为0和1,给的样本正负比为1000:1,那么如果有一个特征全为0或者其他,那么他与预测值的相关性会达到90%以上,然而这个值是毫无作用的。

所以我们通过简单的相关系数并不能很好的观察特征真正相关性,一般我们要辅助图像法和统计法。

图像法就是通过将特征值分布与预测值相关性图表画在同一个图表里,具体可以参考可视化特征

统计法类似图标,使用统计方法观察,特征值与预测值的相关性,一般使用groupby方法对两个特征进行统计就可以进行简单的观察

只是一个简单的手段,一般我们在大量添加特征的时候,为了节省模型训练时间,在将特征放入管道之前进行一个简单的过滤删除的工作,真正重要的步骤在这个方面

这个步骤放在最后是因为,这个步骤也是我们一趟循环下来的最后一步

数据比赛中前期大家最喜欢用的模型是树模型,比如随机森林、XgboostLightBoost等,这些模型属于弱学习器组合模型,我们最后可以从训练结果得到每个特征在模型占的比重

对于这个比重,是非常重要的,他代表了每个特征对应在模型中占的权重,也可以理解特征与结果的相关性

对于相关性很强的不同的特征,我们可以将他们组合,有时候这种强强组合生成出来的特征会比原来母特征相关性更强,当然组合的方法有千种万种,如何验证他们有效就要从头开始对数据进行望闻问切

总结: 数据比赛就如同问诊,我们不断对特征进行望闻问切,对于高手来说他们能很快的从原始特征中挑选出病根,对症下药,而新手的话,一阵摸瞎,经常会碰到在比赛中期做出一个很好的结果,接下来很长一段时间都没有进步的情况。掌握科学有效的挑选特征方法需要一个“医者心”,必须学会对特征“负责”,要学会望闻问切

并行化算法

由于Python本身对多核利用不好,如何利用多核加快特征生成对于比赛来说意义重大

就拿我来举例子,我每天下班打比赛的时间不超过8个小时,前期算法没有并行化的时候,走一遍管道要四个小时,这意味着我一天只能跑两次,而进行并行化优化以后,我跑一遍四线程全开(笔记本双核四线程)只要十分钟就能跑完,每次生成新特征只有10分钟就能拿到特征相关性数据,来验证特征的好坏。

下面我从三个方面来谈谈怎么实现并行话算法

1 . 使用系统自带函数,拒绝for循环

举个例子,作为新手,实现对两个个特征求平均,一般采用for循环将每一行两个特征值加起来然后除以2,假如有1000万行,每行加法和除法运算花0.001ms,那1000万也要10秒钟,只是进行一个最简单的求平均,你就花掉10秒钟,上百个特征你得运行几天

学过矩阵的都知道,矩阵就是一种高效的并行化结构,它将集合统一进行计算,可能一个大矩阵运算要比单一计算要慢,但是单一计算要1000万次的话,大矩阵运算只需要两次就够了,这个效率比就出来了

Python由于是一门解释性语言,比其他静态语音速度要慢许多,你一方面使用for循环加大运算次数,一方面执行一次时间长,这相重叠加你的算法会跑的比蜗牛还慢

所以我们避免使用我们写的函数,尽量使用库系统函数,因为库系统函数底层是使用CC++实现的,而且他们在底层进行使用矩阵话运算代替单一浮点计算,我们使用库的函数(比如meangroupby等)一方面能底层能使用C加快速度,一方面使用矩阵运算加快速度,两个叠加你的算法跑的比飞机还快。

2 . 使用多进程,充分发挥使用多核性能

由于PythonGIL锁,使得Python无法利用多核进行计算,所以我们只能使用多个进程来充分利用多核

实现多进程有两个要点(具体可以参考我携程比赛代码 Github地址)

  • 特征提取模块化
  • 进程池的搭建和维护

我在携程比赛中的mult_run.ipynb中搭建了一个进程池,通过第三方调度和监控进程内存CPU等信息,达到充分“榨干”每个核的功效

3 . 压缩数据,让矩阵运算更快

由于在对特征进行提取过程中,Python会自动将低位制值转换成高位制值,比如float16在进行一次groupby之后就会转换成float64,由于在矩阵运算时候,高进制值会占更多内存和运行时间,所以为了加快算法运行,我们要将其压缩,一方面节省内存,一方面能够让算法运行的更快

在携程的比赛中,原始数据有一个G,我将其压缩之后只占用300M内存空间,这为我后面在一台12G内存的笔记本实现并行化算法提供了巨大帮助,当然我每次在生成新特征的时候也会进行压缩,具体可以参考我携程的utils.py文件

总结: 这两次比赛,我从菜鸟出发慢慢的从一个程序员变成了数据挖掘机,在模块化和并行化方面,我觉得我的进步不错,但是在数据特征挖掘方面我与大神之间的差距还是巨大的,这也是我止步于前五的主要原因,接下来我要加强对数据方向的锻炼,希望能够在工作和比赛之中得到更好的进步

在最下面贴一下我的携程比赛代码(基于Notebook)

https://www.github.com/mrzhangboss/ctrip_room_predict

基于:大航杯“智造扬中”电力AI大赛参赛经验

大航杯AI大赛

赛题背景

主办方为大航集团提供21个月江苏省杨中市1454家企业日用电量,来估计下一个月日总用电量

从给的数据分析,这次给的数据只有历史企业日用电量,用来估计日总用电量,是一个典型的时域分析问题

但是这同我们以往的时序问题不一样,向往常时序问题预测的是每个企业的未来每日的用电量,而这个比赛却是求全部企业的总数.

由于我报名比赛时候比较晚,比赛已经接近尾声,比赛5月18号开始,6月8号中午切换数据,13号截止,我6月8号晚上下载数据,由于我以前已经做了几个类似的比赛,但是一直没有系统的做一个,抱着锻炼的自己的态度,决定系统做一次,权当练手.

首先分析一下提交的结果,预测一个月的日总用电量,总共为31个数据,给的历史数据只有21个月的,按月的比例来看,只有21个值去训练值去预测一个值,根据往常的比赛经验来看,这种比赛适合使用规则方法来做,然而我剩下的验证机会不多了,只能用模型,但是过拟合的危险非常大,如果不能找到一个好的方法克服过拟合,复赛都进不去.

当然最后还是没有找到一个很好的办法,止步于复赛,不过这次比赛让我学到很多,主要通过这次比赛自己琢磨出来自己如何搭建基于IPython Notebook的管道结构,这个管道帮我自动生成上万特征.

管道

什么是管道,在数据挖掘比赛中很多大神都着重讲了一定要搭建一个自动化的架构,我们暂且称他为”管道”,这个”管道”我们要能够把数据倒进去,结果倒出来.

这个管道用专业的术语来看要有以下几个功能

  • 能够自由添加Feture
  • 能够自动评判得到添加的Feture的效果
  • 管道能够自己选择合适的参数训练模型
  • 能够输出结果

其实简单来说,我们要做的是一个能够非常方便的扩展的脚手架,我们不可以第一次就把所以的特征全部找出来,所以我们要搭建一个能够实时添加Feture的框架.

其实很早以前就看过类似文章,也有很多人推荐大神开源的一个脚手架,然而找到的大多是用python实现

我因为一开始就是使用Notebook进行数据挖掘,主要Notebook能够提供一个实时的反馈,而纯python,对于复杂多变的数据来说,显得非常笨重,你经常有个好想法想验证一下,又得重新跑一遍,尤其是对于我的机器配置来说,重新跑一边的时间都够我喝杯茶了.而且notebook有个特点,可视化特别方便,有时候从数据上看不到,可以画个图表

好了,夸了这么久,现在就来仔细讲讲脚手架如何搭建.

我们先回到赛题,第一步审题,当时我看到日平均两个字,直接把日字省略,看成平均用电量,结果白白浪费了两个验证机会…..

审完了题我们来看我们要提交的数据,换数据后要预测十月日用电总量.我们来看看给我们数据,只有一份数据,表头如下

record_date,user_id,power_consumption
2015/1/1,1,1135
2015/1/2,1,570
2015/1/3,1,3418
2015/1/4,1,3968
2015/1/5,1,3986

解释一下字段,record_date–日期,user_id–企业id, power_consumption–日用电量
非常简单,就这么简单单单的数据,我现在要教大家怎么从这么简单的数据上抽取6000维度的

我把代码已经推到Github上了(由于数据比较少,我把数据也推上去了,方便大家本地跑跑,看完如果对你有帮助的话,请不要吝啬你的star哦),我就对照我的代码解释如何搭建一个可以跑出上万维度的脚手架

数据划分(split_samples.ipynb)

首先要搭建本地预测集,也就是线下样本(这个很重要,有时候线下的结果很大程度对应你线上的结果)

给的数据要我们从前面21个月预测下一个月的日总用电量,我们很容易就能想到,那我们用前面20个月预测第21个月来做线下测试,但是这样我们就只有30个训练样本,要来预测30个,99.999%过拟合啊,首先我们要扩大样本,我们采用滑动移窗的方法把预测的样本按照月份推移,也就是分别预测9月8月7月等等

这种方法在实现Notebook有几个难点,首先你划分了预测集,那么就也要划分训练集,就相当于把一份数据切分成好几份,切分完之后有个问题,你必须要隔离每个部分

举个例子,我们把训练集划分成为2份,1月到7月预测8月,2月到8月预测9月,训练1-7月数据集的时候,我们不能让这个训练集接触到2月到8月的数据,因为8月对于前一个训练集来说是未知的,
如果我们让第一个训练集接触倒第二个训练集我们称为信息泄露,很影响线上的结果

我们知道这个问题之后,我们就要用巧妙的方法来解决,首先我们要考虑我们代码的复杂度,以前我的解决训练集隔离的方法采用的是循环法,使用一个列表存贮所以训练集,然后使用for循环分别传参到函数里面,这个方法能解决隔离训练集,但是有几个问题

在单个ipy文件中训练所以的样本,在测试的时候跑起来太慢,而且要把数据全部加载在内存里面,这次数据量还算小,但是对于某些小内存的电脑来说,这种方法时不时就得报Memory Error,而且感觉调试起来特别麻烦,所以一直在寻找更好的解决方案.

这次想到了一种巧妙的方法,虽然有点取巧但是效果我很满意.

我们先看到split_samples.ipynb文件,首先我把数据划分为9个样本,一个预测样本.分别放入不同文件夹进行物理隔离.但是名字相同.

再其次我让ipy能够获取参数,这样我通过外部参数就能更换数据集,平常添加Feture的时候默认选取一个训练集,这样我开发的时候调试就非常方便,而且可以丢掉for循环,还我一个清新脱俗的ipy.

这里说一个小细节,因为我传参必须要外部调用这种,对于运行ipy我使用了runipy这个工具,然后我死活没有找到,如何使用runipy把参数传倒ipy里面去的方法(如果找到了请告诉我),我一拍脑袋那就转换成py文件传过去,通过sys.argv很轻松就能获取到,所以我又用jupyter nbconvert的工具把ipy转换成py文件

所以绕了一圈最后又回到了py上(手动滑稽).不过我们工作还是在ipy上进行,生成的py文件我好像没打开过….

特征提取(extract_fetures.ipynb)

聊完如何划分数据集,现在我们进入如何特征提取,我们可以看到这次数据其实就三个特征:时间-企业-用电量.由于企业的信息只有一个id,所以我首先提取的是时序的特征,首先把时间分解为八个维度

  • dayofweek
  • dayofyear
  • days_in_month
  • quarter
  • week
  • weekofyear
  • month
  • year

我们可以通过pandas轻松提取出来

然后我们再从两个方向来看,第一个就是我们日总用电量特征,从全部企业日总用电量

第二个就是日用电量特征,从每个企业日用电量来看,这些特征我们使用简单统计又可以得到10个维度数据(mean,std,等等)

看完这些之后我们又可以从多个时间维度来看这些特征,比如30天前,90天前等等(我划分了30,60, 90,180,360五个),

这样我们就有了 8 * 2 * 5 * 10个特征,但是这远远达不到我们说的上万维度,

现在我们从业务逻辑上来思考,因为我们知道,其实我们中国节假日和周末,天气这些对用电量影响非常大(我们老家打雷就停电…..)

所以我们要引入外部数据集,我采用两个爬虫分别是weather_crawl,holiday_crawl爬取了天气和节假日的数据

我们按照前面的思路,从天气节假日的角度又可以划分出n多特征(这时候我的特征已经达到3000了)

完了这些基础特征后,我发现有些特征重要性特别大(使用Randomforest得到),这时候我们又要请出我们第二大神器,交叉特征,比如月和假期的特征融合,这一波操作直接让我的特征到了6000+维度(如果将窗口扩大轻轻松松上万)

在这里要介绍一个特征生成的方法,有时候我们特征少,我们会采用自己命名的,自己生成,然后这个由于规律性比较大,
如果我们自己手动一个一个写的话,这上万Fetures够你写的,所以要让他自己生成特征,我们只要建好模子就行,由于这次
时间仓促,基本上我没有自己手动命名feture,全部都由程序生成,省掉很多代码量,具体可以看看代码实现,原理很简单.

训练模型(train_model.ipynb)

训练模型的话,一般比赛都推荐先使用树模型,一方面速度快,第二个可以看到feture的重要性,这对于你挑选交叉特征非常有用,模型调参的我这里就不讲,一方面我自己也不是很懂,第二个方面也网上教程也多,我讲的不一定比他好

这里要推荐一个发现有趣的包,mlxtend,我用他来进行stacking特别方便,有意思的时,我用他融合了四个模型,最后我的训练结果竟然为1,完全拟合了……

这个包可以很简单的进行模型的stacking,然而这个比赛我没有把他用好(手动滑稽)

通过训练模型后我们把模型存到pkl文件中,然后在用他来预测数据,这样在文件夹里转一圈的原因,因为原来打过部分比赛数据量太大,训练模型后内存不足,只能先del,清空内存,再预测,存到文件夹后,结束进程,清空内存,这样就能省下空间来读取下一步数据.

总结

其实在这个脚手架上可以扩展很多东西,比如最后搏一搏单车变摩托的时候,我就在分割数据和训练数据之间加了一个过滤清洗数据层,在训练模型和融合特征之间加了一个降维的中间层.

建立一个好的脚手架只是能让你在增添特征,选择特征时更加轻松,其实比赛看的还是你对数据的一种掌控力,建立这个脚手架主要是为了节省更多时间给提取特征、选择特征上.特征决定你的上限.

这次比赛比较特殊,模型在这个比赛效果可能没有规则好,因为数据量太小,我stacking一下直接完全拟合了.可惜验证的次数还是
太少,除去前面两次错误的提交,我只有三次验证机会,如果次数多一点的话,选择特征降维或者模型调参一下遏制拟合结果可能会好很多吧.

但是这次比赛自己学到了如何搭一个ipy的管道和增加了一些特征调参、特征降维的经验。因为以前看到的搭建管道资料都是基于py,很少基于ipy的,所以把自己搭建ipy管道经验分享出来,也希望自己写的这篇博文能够抛砖引玉,帮助大家搭建自己的完美管道.

附上我的开源示例: https://github.com/mrzhangboss/electricAI
大家觉得有帮助就给我点个star吧

自我介绍

前端 - > 后端 -> 数据挖掘机

ML DM AI 的区别

我的自学之旅

给新手的推荐

  • 机器学习课程(MOOC)
  • Kaggle、天池、数据城堡

掌握的技能

  • Java + Python
  • 数据可视化
  • 训练团队感

未来的发展方向

  • 全栈数据挖掘工程师
  • 增长黑客
  • ML算法工程师

由于最近在一家数据服务公司实习,项目需要了解分布式,所以在这里基于scrapy的分布式总结一下爬虫的分布式实习

分布式起因

单机无法完成全部工作任务所以要使用集群加速完成工作任务

分布式有点像蚁群,一只蚂蚁举不起一只卡壳虫,但是几百只就能轻松的把他运回家

但是分布式设计必须科学,否则就像下面一样,一个和尚挑水,其他和尚围观

分工不合理,来源网络

分布式设计

分布式设计原理在于分工

首先我们来看看爬虫怎么进行分工,单个爬虫运行根据url获取响应报文,然后通过解析报文返回结果或者下一次爬取目标,如果单个爬虫我们只要在内存维持一个set变量记住爬取过的url,这就是scrapy默认的方法。

但是我们无数个爬虫由于不在同一个进程,无法共享变量,所以我们只要让一个“variable(变量)”能够被被所以爬虫共享到就完成了主要功能

现在我们来完善具体细节
要求:

  • 爬虫能够轻松读取所以已爬取变量
  • 爬虫能够加入已读取变量
  • 爬虫能够获取下一次请求具体参数

原则上我们可以使用内存映射来构建这个变量,但是读取,修改都不便利,所以可以先使用redis作为存贮变量的地方,使用redis提供的set我们替代scrapy框架的set变量。

现在我们已经决定我们要使用什么容器来存贮变量,接下来我们要考虑存什么变量。

我们先看scrapy-redis存贮了什么,分析源代码可知,scrapy-redis将返回的Requestpickle话存入数据库,并且计算这个Request的32位hash值存入redisset中过滤列表。

scrapy-redis通过修改scrapy的调度器(scheduler)让其当爬虫没有Request需要处理时在redis中提取Request,实现分布式。

我们来分析一下这种方法,爬虫在爬取的过程中从master端获取Request,并不断生成Requestmaster端,master只是一个redis数据库,负责对url去重,分发任务。

我们来比较一下直接存取url这种方法,这种方法好处在于,slaver能够从上一个Request中获取全部信息,假如上一个Request需要存取获取的表单提取地址,我们下一次爬虫发起Request就能从上一个Request中获取参数。

当然由于我们存贮的是Request,一个Request pickle化之后的字符串比较长,当我们的任务列表里面有很多Request的时候,redis占用的内存会非常巨大。

当然如果爬虫启动的够多,生成一个就能把任务被调度下去,那么这个任务列表就能稳定在一个可控的范围。

总结

每个爬虫即负责爬取数据,又负责生成下一个任务,即无主次之分,我们可以一次性在docker中启动上百个实例,我们只是用redis充当一个存放变量的地方。

但是这种方法也有一个缺点,我们不能自由的添加初始url,要想添加新的爬取任务,必须新建一个爬虫更新初始url,我们如果是想搭建一个自由添加url的爬虫,这种实现方式不大优雅。

分布式改良

我们要修改程序框架,达到随时可以添加要爬取新任务,然而不影响爬虫集群

爬虫框架

我们独立出来mastermaster负责生成Request去重以及任务调度,而slaver只负责从master获取任务爬取。

这种方法我们可以很轻松对master改良而不影响slaver,通过让master定时从数据库中获取新的任务生成到任务列表,我们可以轻松添加新的任务到slaver集群中去。

下一步我们就介绍如何修改scrapy-redis达到我们新框架需要

重构scrapy-redis

参考:
基于Redis的三种分布式爬虫策略

第一次听说这个比赛还是去年在知乎上,当时也不知道这个比赛具体是怎么回事,当时自己还是一个小白,忙着搞懂各种主流的机器学习模型算法。

当时在我心中,模型算法是数据挖掘的最重要的组成部分,搞懂这些才能真正搞定数据挖掘。我当时对算法模型和数据的理解是:模型就是风车,数据就是流水。我要做的事就是撘一个强健的风车,让数据流过。

当我还没接触实际的工作前,我还没有没有从编程工转向挖掘工。我太注重编程本身了,而忘记我自己真正要挖掘的宝藏。

我以前在Quora上搜如何成为数据科学家,我发现很多有经验的数据科学家他们都把“对数据的敏感和兴趣”作为数据科学家最重要的特征,而“了解各种算法模型并能应用到数据上”才是第二重要的。我当时不是太理解,我觉得后者才是更重要的。

参赛感想

这次参赛算是我学习数据挖掘第一次实际的挖掘,以前学习各种算法模型都是准备的很好的数据,只要套上算法模型就能跑的很好。所以我一开始就拼命的去找类似的大赛,看看获胜者他们用的模型是什么。

这几天我好像抱着一堆瓶子,拼命的想把巨大的石头(数据)塞进瓶口里,看起来工作量很大,流了很多汗,其实什么都没有干。今天在看一个类似的比赛选手答辩的时候的视频,突然明白自己好像走了一个死胡同。自己拼命的想这找一个合适的瓶子(模型),其实我更应该做的是把石头(数据)磨碎。

模型本身不重要,他只是一个载体,更重要的是数据。

第一次参加这样大型比赛,有点激动也有点惶恐,如何将所学的应用到实际,还有在实际中提高自己还有待自己“挖掘”。虽然这个比赛奖金“丰富”,但是我觉得在这个比赛中得到的体会乐趣比奖金更诱人。


比赛还有一个月,在这里立个小目标,争取跑到到前五页,我也会尽量抽时间把自己感想写出来。
未完待续。