Quantcast
Channel: IT社区推荐资讯 - ITIndex.net
Viewing all 11857 articles
Browse latest View live

Fast Near-Duplicate Image Search using Locality Sensitive Hashing

$
0
0
使用LSH快速搜索相似图片,使用LSH的ANN查询按如下方式执行:1)查找查询项的“桶”(哈希值)2)与桶中的每个其他项进行比较。
  • Locality Sensitive Hashing(LSH)是一种有用的工具,即使对于非常大的数据集也可以很好地扩展执行近似最近邻居查询。
  • 深度学习的时代为我们复活了在向量上相似的图像,文本和音频(简单的欧几里得距离)在原始语义内容上也相似(图像的VGG特征向量,文本的Word2Vec)。

Part 1: why Nearest-Neighbor queries are such a big deal

If you have some education in Machine Learning, the name Nearest Neighbor probably reminds you of the k-nearest neighborsalgorithm. It is a very simple algorithm with seemingly no “learning” actually involved: The kNN rule simply classifies each unlabeled example by the majority label among its k-nearest neighbors in the training set.

k-NN algorithm: with k=3, the green example is labeled as red; with k=5, it is labeled as blue

This seems like a very naive, even “silly”, classification rule. Is it? Well, depends on what you take as your distance metric, i.e: how do you choose to measure the similarity between examples. Yes, the naive choice — using simple Euclidean distance in the “raw” features — often usually leads to very poor results in practical applications. For example, here are two examples (images) whose pixel-values are close in Euclidean distance; but arguably, one would be crazy to classify the left image as a flower, solely based on it being a neighbor of the right image.

Euclidean distance in pixel space = visual/syntactic/low-level similarity

But, as it turns out, coupling the kNN rule with the proper choice of a distance metric can actually be extremely powerful. The field of “metric learning” demonstrated that when machine learning is applied to learning the metric prior to using the kNN rule, results can improve significantly.

The great thing about our current “Deep Learning era” is the abundance of available pre-trained networks. These networks solve certain classification tasks (predicting an image category, or the text surrounding a word), but the interesting thing is not so much their success on those tasks, but actually the extremely useful by-product they provide us with: dense vector representations, for which simple Euclidean distance actually corresponds to high-level, “semantic” similarity.

Euclidean distance in deep embedding space = semantic similarity

The point is that for many tasks (but namely, general-purpose images and text), we already have a good distance metric, so now we actually can just use the simple kNN rule. I’ve talked about this point a lot in the past — e.g, in a previous post I attempted to use such searches to verify the claim that generative models are really learning the underlying distributionand not just memorizing examples from the training set.

This leaves us with the task of actually findingthe nearest neighbors (I will refer to this as a NN query). This problem — now a building block in literally anyML pipeline — has received a lot of traction, both in the CS-theory literature and from companies that need highly optimized solutions for production environments. Here again, the community benefits, because a couple of the big players in this field have actually open-sourced their solutions. These tools use carefully crafted data structures and optimized computation (e.g on GPUs) to efficiently implement NN queries. Two of my favorites are Facbook’s FAISSand Spotify’s Annoy. This post should hopefully bring you up to speed on what happens “behind the hood” of these libraries.

A first distinction when we talk about nearest neighbors queries is between exactand approximatesolutions.

Exact algorithmsneed to return the k nearest neighbors of a given query point in a dataset. Here, the naive solution is to simply compare the query element to each element in the dataset, and choose the k that had the shortest distances. This algorithm takes O(dN), where N is the size of the dataset and d is the dimensionality of the instances. At first glance this might actually seem satisfactory, but think about it: 1) this is only for a single query! 2) while true that d is fixed, it can often be very large, and most importantly 3) in the “big data” paradigm, when datasets can be huge, being linear in the dataset size is no longer satisfactory (even if you’re Google or Facebook!). Other approaches for exact queries use tree structures and can achieve better average complexity but their worst-case complexity still approaches something that’s linear in N.

Approximate algorithmsare given some leeway. There are a couple of different formulations, but the main idea is that they only need to return instances whose distance to the query point is almostthat of the real nearest neighbors (where ‘almost’ is the algorithm’s approximation factor). Allowing for approximate solutions opens the door to randomized algorithms, that can perform an ANN (approximate NN) query in sublineartime.

Part 3: Locality Sensitive Hashing

Generally-speaking, a common and basic building block for implementing sublinear time algorithms are hash functions. A hash function is any function that maps input into data of fixed size (usually of lower dimension). The most famous example, which you might have encountered by simply downloading files off the internet, is that of checksumhashes. The idea behind them is to generate a “finger-print” — i.e, some number that is hopefully unique for a particular chunk of data — that can be used to verify that the data was not corrupted or tampered with when it was transferred from one place to another.

checksum hash: good for exact duplicate detetction

These hash functions were designed with this sole purpose in mind. This means that they are actually very sensitive to small changes in the input data; even a single bit that’s changed will completely change the hash value. While this is really what we need for exact duplicate detection(e.g, flagging when two files are really the same), it’s actually the opposite of what we need for near duplicatedetection.

This is precisely what Locality Sensitive Hashing (LSH) attempts to address. As it’s name suggest, LSH depends on the spatiality of the data; in particular, dataitems that are similar in high-dimension will have a larger chance of receiving the same hash value. This is the goal; there are numerous algorithms that construct hash functions with this property. I will describe one approach, that is amazingly simple and demonstrates the incredibly surprising power of random projections (for another example, see the beautiful Johnson-Lindenstrauss lemma).

The basic idea is that we generate a hash (or signature) of size k using the following procedure: we generate k random hyperplanes; the i-th coordinate of the hash value for an item x is binary: it is equal to 1 if and only if x is above the i-th hyperplane.

the hash value of the orange dot is 101, because it: 1) above the purple hyperplane; 2) below the blue hyperplane; 3) above the yellow hyperplane

The entire algorithms is just repeating this procedure L times:

an LSH algorithm using random projections with parameters k and L

Let’s understand how LSH can be used to perform ANN queries. The intuition is as follows: If similar items have (with high probability) similar hashes, then given a query item, we can replace the “naive” comparison against all the items in the dataset, with a comparison only against items with similar hashes(in the common jargon, we refer to this as items that landed “in the same bucket”). Here we see that the fact that we were willing to settle for accuracy is precisely what allows for sublinear time.

Since inside the bucket we compute exact comparisons, the FP probability (i.e, saying that an item is a NN when it truly isn’t) is zero, so the algorithm always has perfect precision; however, we will only return items from that bucket, so if the true NN was not originally hashed to the bucket, we have no way of returning it. This means that in the context of LSH, when we talk about accuracy we really mean recall.

Formally, an ANN query using LSH is performed as follows: 1) Find the “bucket” (hash value) of the query item 2) Compare against every other item in the bucket.

Let’s analyze the computational complexity of this algorithm. It will be quick and easy, I promise!

Stage 1) costs dk; Stage 2) costs N/2^kin expectation (because there are N points in the dataset and 2^K regions in our partitioned space). Since the entire procedure is repeated L times, the total cost is, on average, LDK+LDN/2^k. When k and L are taken to be about logN, we get the desired O(logN).

Part 4: LSH Hyperparameters, or the accuracy-time tradeoff

We’ve seen the basic algorithm for LSH. It has two parameters, k (size of each hash) and L (the number of hash-tables) — different setting of the values for k,L correspond to different LSH configurations, each with its own time complexity and accuracy.

Analyzing these formally is a little tricky and requires much more math, but the general take-away is this:

By careful setting of these parameters, you can get a system that is arbitrarily accurate (what-ever you definition of a “near duplicate” is), BUT some of these can come at a cost of a very large L , i.e a large computational cost.

Generally a good approach for settling such trade-offs empiricallyis to quantify them on a well-defined task, which you can hopefully design using minimal manual labor. In this case, I used the Caltech101dataset (yes, it’s old; yes, there were image datasets that predated ImageNet!), with images of 101 simple objects. As input to my LSH scheme, I used the 4096-dimensional feature-vectors obtained by passing each image through a pre-trained VGG network. To keep things simple, I assumed that the other images from the same category are true NN in the feature space.

Caltech101

With a “ground truth” at hand, we can try out different hyperparameter combinations and measure their accuracy (recall) and runnning time. Plotting the results gives a nice feel for the accuracy-time trade-off:

We clearly see that better recall comes at the cost of longer run-time. Note that the actual results are task-dependent: generally speaking, the more similar (in high-dimension) the items you consider “near” are, the easier the task will be. Finding distantneighbors efficientlyis a hard task, beware!

Part 5: Putting it all together for an example application

I wanted to piece together this pipeline for a personal project, which is to more efficiently browse my personal photo collection. Returning from a trip, I often have photos from several devices, and many of them are so similar — my appreciation for the views usually leaves me with tens of photos of pretty much the same things. Semantic similarity to the rescue! Here are some of the results.

Each row represents a single query; on the left is the query image, and on the right are the images that were hashed to the same bucket, with their actual distance in green. Pretty cool stuff!

Summary (TL;DR).

We reviewed two really useful ideas:

  1. Locality Sensitive Hashing (LSH) is a useful tool for performing approximate nearest-neighborqueries in a way that scales well even for enormously large datasets.
  2. The era of deep learning has provided us with free “off the shelf” representations of images, text and audio, in which similar vectors (in simple, Euclidean, distance) are semantically similar(VGG feature vector for images, Word2Vec for text).

Finally, we saw how the combination of these two ideas — namely, applying LSH not on the raw data (image, text) but on the deep representation — can be used to perform fast similarity search in huge collections.


建立一个高可用的MQTT物联网集群How to Build an High Availability MQTT Cluster for the Internet of Things

$
0
0

建立一个高可用的MQTT物联网集群

1. Setting up the MQTT broker

MQTT is a machine-to-machine (M2M)/“Internet of Things” connectivity protocol. It was designed as an extremely lightweight publish/subscribe messaging protocol and it is useful for connections with remote locations where a small code footprint is required and network bandwidth is at a premium.

The first time we looked for an MQTT solution was two years ago. We were searching for a secure (auth based), customisable (communicating with our REST API) and easy to use solution (we knew Node.js). We found in Moscathe right solution and, after two years, we’re happy with our choice ☺

The key metrics influencing your MQTT server choice could be different from ours. If so, check out this listof MQTT servers and their capabilities.

Give me some code chatter

We’re not going to describe every single line of code, but we’ll show you two main sections, showing how simple can be setting up an MQTT server.

The code we use to run MQTT server on Lelylan is available on Github.

Setting up the MQTT server

The code below is used to start the MQTT server. First we configure the pub/sub settings using Redis, we pass the pub/sub settings object to our server and we are done.

Node.js code needed to run a simple MQTT server
If you ask yourself, why Redis is needed as pub/sub solution, read the Q1 on FAQ. Being short we need it to enable a communication channel between the MQTT server and other microservicescomposing Lelylan.

Authenticating the physical objects

With Mosca you can authorize a client defining three methods, each of them used to restrict the accessible topics for a specific clients.

#authenticate      
#authorizePublish
#authorizeSubscribe

In Lelylan we use the authenticate methodto verify the client username and password. If the authentication is successful, the device_idis saved in the client object ,and used later on to authorize (or not) the publish and subscribe functionalities.

If you want to learn more about MQTT and Lelylan check out the dev center.

2. Dockerizing our MQTT server

Dockeris an awesome tool to deploy production systems. It allows you to isolate your code in a clean system environment by defining a Dockerfile, an installation “recipe” used to initialize a system environment.

Docker. An awesome tool to deploy production systems

Cool! Lets getting started.

Container definition

To build a container around our application, we first need to create a file named Dockerfile. In here we’ll place all the needed commands Docker uses to initialize the desired environment.

In the Dockerfile used to create a container around the MQTT server we ask for a specific Node.js version ( FROM node:0.10-onbuild), add all files of the repo ( ADD ./ .), install the node packages ( RUN npm install), expose the port 1883 ( EXPOSE 1883)and finally run the node app ( ENTRYPOINT [“node”, “app.js”]).That’s all.

Run the Docker Image

Once we have a Dockerfile, we can build a docker container (if you haven’t Docker installed, do so- it supports all existing platforms, even Windows ☺). Once you have docker installed, we can build the container.

# building a container      
$ docker build -t lelylan/mqtt

Which will eventually output

Successfully built lelylan/mqtt

Once we have built the container, we can run it to get a working image.

docker run -p 1883:1883 -d lelylan/mqtt

And we’re done! We now can make requests to our MQTT server.

When starting with Docker, it’s easy to make little confusion between containers and images. Read out what bothof themmeans to make your mind clearer.
# OSX      
$ http://$(boot2docker ip):1883
# Linux      
$ http://localhost:1883
If you’re using OS X we’re using boot2docker which is actually a Linux VM, we need to use the $DOCKER_HOST environment variable to access the VM’s localhost, otherwise, if you’re using Linux use localhost.

Other commands we were using a lot

While learning how to use Docker, we wrote down a common to use list of commands. They all are basic, but we think it’s good to have a reference to look at when needed.

Container related commands
# build and run a container without a tag      
$ docker build .
$ docker run -p 80:1883 <CONTAINER_ID>
# build and run a container using a tag      
$ docker build -t <USERNAME>/<PROJECT_NAME>:<V1>
$ docker run -p 80:1883 -d <USERNAME>/<PROJECT_NAME>:<V1>
Image related commands
# Run interactively into the image      
$ docker run -i <IMAGE_ID> /bin/bash
# Run image with environment variables (place at the beginning)      
$ docker run -e "VAR=VAL" -p 80:1883 <IMAGE_ID>
# list all running images      
$ docker ps
# List all running and not running images      
# (useful to see also images that exited because of an error).
$ docker ps -a
Kill images
# Kill all images      
docker ps -a -q | xargs docker rm -f
Log related commands
# See logs for a specific image      
docker logs <IMAGE_ID>
# See logs using the tail mode      
docker logs -f <IMAGE_ID>

3. Adding HAProxy as load balancer

At this point we have a dockerized MQTT server being able to receive connections from any physical object (client). The missing thing is that it doesn’t scale, not yet ☺.

Here comes HAProxy, a popular TCP/HTTP load balancer and proxying solution used to improve the performance and the reliability of a server environment, distributing the workload across multiple servers. It is written in C and has a reputation for being fast and efficient.

Terminology

Before showing how we used HAProxy, there are some concepts you need to know when using a load balancing.

If curious, you can find a lot of useful info in this articlewritten by Mitchell Anicas

Access Control List (ACL)

ACLs are used to test some condition and perform an action (e.g. select a server, or block a request) based on the test result. Use of ACLs allows flexible network traffic forwarding based on a different factors like pattern-matching or the number of connections to a backend.

# This ACL matches if the path of user’s request begins with      /blog        
# (this would match a request of http://example.org/blog/entry-1)
acl url_blog path_beg /blog

Backend

A backend is a set of servers that receives forwarded requests. Generally speaking, adding more servers to your backend will increase your potential load capacity and reliability by spreading the load over them. In the following example there is a backend configuration, with two web servers listening on port 80.

backend web-backend      
balance roundrobin
server web1 web1.example.org:80 check
server web2 web2.example.org:80 check

Frontend

A frontend defines how requests are be forwarded to backends. Frontends are defined in the frontendsection of the HAProxy configuration and they put together IP addresses, ACLs and backends. In the following example, if a user requests example.com/blog, it’s forwarded to the blogbackend, which is a set of servers that run a blog application. Other requests are forwarded to web-backend, which might be running another application.

frontend http      
bind *:80
mode http
acl url_blog path_beg /blog      
use_backend blog-backend if url_blog
default_backend web-backend

Stop the theory! Configuring HAProxy ☺

The code we used to run the HAProxy server on Lelylan is defined by a Dockerfile and a configuration file describing how requests are handled.

The code we use to run HAProxy is available on Github

Get your HAProxy container from Docker Hub

To get started download the HAProxy container from the public Docker Hub Registry(it contains an automated build ready to be used).

$ docker pull dockerfile/haproxy

At this point run the HAProxy container.

$ docker run -d -p 80:80 dockerfile/haproxy

The HAProxy container accepts a configuration file as data volume option (as you can see in the example below), where <override-dir>is an absolute path of a directory that contains haproxy.cfg (custom config file) and errors/ (custom error responses).

# Run HAProxy image with a custom configuration file      
$ docker run -d -p 1883:1883 \
-v <override-dir>:/haproxy-override dockerfile/haproxy
This is perfect to test out a configuration file

HAProxy Configuration

Follows the configuration for our MQTT servers, where HAProxy listens for all requests coming to port 1883, forwarding them to two MQTT servers (mosca_1 and mosca_2) using the leastconnbalance mode (selects the server with the least number of connections).

2. To see the final configurations used by Lelylan checkout haproxy.cfgon Github. 1. During the HAProxy introduction we described the ACL, backend and frontend concepts. Here we used listen, a shorter but less expressive way to define all these concepts together. We used it because of some problems we had using backend and frontend. If you find out a working configuration using them, let us know.

To try out the new configuration (useful on development), override the default ones by using the data volume option. In the following example we override haproxy-overridewith the configuration file defined in /root/haproxy-override/.

$ docker run -d -p 80:80 1883:1883 \      
-v /root/haproxy-override:/haproxy-override
dockerfile/haproxy

Create your HAProxy Docker Container

Once we have a working configuration, we can create a new HAProxy container using it. All we need to do is to define a Dockerfile loading the HAProxy container ( FROM dockerfile/haproxy)to which we replace the configuration file defined in /etc/haproxy/haproxy.cfg ( ADD haproxy.cfg /etc/haproxy/haproxy.cfg).We then restart the HAProxy server ( CMD [“bash”, “/haproxy-start”]) and expose the desired ports (80/443/1883/8883).

NOTE. We restart HAProxy, not simply start, because when loading the initial HAProxy container, HAProxy is already running. This means that when we change the configuration file, we need to give a fresh restart to load it.

Extra tips for HAProxy

When having troubles with HAProxy, read the logs! HAProxy uses rsyslog, a rocket-fast system for log processing, used by default in Ubuntu.

# HAProxy log configuration file      
$ vi /etc/rsyslog.d/haproxy.conf
# Files where you can find the HAProxy logs      
$ tail -f /var/lib/haproxy/dev/log
$ tail -f /var/log/haproxy.log

4. Making MQTT secure with SSL

We now have a scalable MQTT infrastructure where all requests are proxied by HAProxy to two (or more) MQTT servers. The next step is to make the communication secure using SSL.

Native SSL support was implemented in HAProxy 1.5.x, which was released as a stable version in June 2014.

What is SSL?

SSL (Secure Sockets Layer) is the accepted standard for encrypted communication between a server and a client ensuring that all data passed between the server and client remain private and integral.

Creating a Combined PEM SSL Certificate/Key File

First of all you need an SSL certificate. To implement SSL with HAProxy, the SSL certificate and key pair must be in the proper format: PEM.

In most cases, you simply combine your SSL certificate (.crt or .cer file provided by a certificate authority) and its respective private key (.key file, generated by you). Assuming that the certificate file is called lelylan.com.crt, and your private key file is called lelylan.com.key, here is an example of how to combine the files creating the PEM file lelylan.com.pem.

cat lelylan.com.crt lelylan.com.key > lelylan.com.pem
As always, be sure to secure any copies of your private key file, including the PEM file (which contains the private key).

Load the PEM File using Docker volumes

Once we’ve created our SSL certificate, we can’t save it in a public repo. You know, security ☺. What we have to do is to place it in the HAProxy server, making it accessible form Docker through data volumes.

What is Docker data volumes?

A data volume is a specially-designated directory within one or more containers that provide useful features shared data. You can add a data volume to a container using the -v flag to share any file/folder, using -v multiple times to mount multiple data volumes (we already used it when loading a configuration file for the HAProxy container).

Using data volumes to share an SSL certificate.

To share our SSL certificate, we placed it in /certs(in the HAProxy server), making it accessible through the /certs folder when running the Docker Container.

$ docker run -d -p 80:80 -p 443:443 -p 1883:1883 -p 8883:8883 \      
-v /certs:/certs
-v /root/haproxy-override:/haproxy-override
Don’t forget to open the port 8883 (the default one for secure MQTT connections)

Loading the SSL certificate

Once we have the SSL certificate available through Docker data volume, we can access it during through the HAProxy configuration file. All we need to do is to add one line of code to map the requests coming to the 8883 port to the SSL certificate placed in /certsand named lelylan.pem.

We’re done!

At this point we have a Secure, High Availability MQTT Cluster for the Internet of Things. Below, you can see an image representing the final result.

Devices connecting to a load balancer, forwarding all connections to two MQTT servers. Image copyright to HiveMQ.

At this point, there’s one thing to make the architecture complete: we need a simple way to deploy it.

5. Configuring nscale to automate the deployment workflow

To make this possible we’ll use nscale, an open source project to configure, build and deploy a set of connected containers.

While we’ll describe some of the most important commands used by nscale, hereyou can find a guide describing step by step how nscale works.

Where do we deploy all of this stuff?

Digital Oceanis a simple cloud hosting, built for developers. For our deployment solution , all the droplets we’ll use, are based on Ubuntu and have Docker already installed.

Droplet definition
Do not have a Digital Ocean account? Sign up through this linkand get 10$ credit.

The first thing we had to do was to create 5 droplets, each of them dedicated to a specific app: 1 management machine (where the nscale logic will live), 1 HAProxy load balancer, 2 MQTT Mosca servers and 1 Redis server.

List of Droplets created in Digital Ocean for this tutorial
List of Droplets created for this tutorial on Digital Ocean.

Installing nscale

We’re now ready to install nscale into the management machinedefined on Digital Ocean. We could also have used our local machine, but having a dedicated server for this, make it simple for all team members to deploy new changes.

Installation

Install Node.js via nvm (Node Version Manager).

curlhttps://raw.githubusercontent.com/creationix/nvm/v0.18.0/install.sh| bash

Logoff, login and run the following commands.

# install needed dependencies      
apt-get update
apt-get install build-essential
# install node and npm      
nvm install v0.10.33
nvm alias default v0.10.33
npm install npm@latest -g --unsafe-perm
# install nscale      
npm install nscale -g --unsafe-perm
The installation could take a while, it’s normal ☺

Github user configuration

To use nscale you need to configure GIT.

git config --global user.name "<YOUR_NAME>"      
git config --global user.email "<YOUR_EMAIL>"

Create your first nscale project

Once all the configurations are done, login into nscale.

$ nsd login

At this point we can create our first nscale project, where you’ll be asked to set a nameand a namespace (we used the same name for both of them ).

$ nsd sys create      
1. Set a name for your project: <NAME>
2. Set a namespace for your project: <NAMESPACE>

This command will result into an automatically generated project folder with the following structure (don’t worry about all the files you see; the only ones we need to take care of are definition/services.jsand system.js).

|— definitions      
| |— machines.js
| `— services.js *
|— deployed.json
|— map.js
|— npm-debug.log
|— README.md
|— sudc-key
|— sudc-key.pub
|— system.js *
|— timeline.json
`— workspace
...

At this point use the list command to see if the new nscale project is up and running. If everything is fine, you’ll see the project name and Id.

$ nsd sys list      
Name Id
lelylan-mqtt 6b4b4e3f-f22e-4516-bffb-e1a8daafb3ea

Secure access (from nscale to other servers)

To access all servers nscale will configure, it needs a new ssh key for secure authentication solution with no passphrase.

ssh-keygen -t rsa

Type no passphrase, and save it with your project name. In our case we called it lelyan-key (remember that the new ssh key needs to live in the nscale project root, not in ~/.ssh/). Once the ssh key is created, setup the public key in all the servers nscale needs to configure: haproxy, mosca 1, mosca 2and redis.

This can be done through the Digital Ocean dashboard or by adding the nscale public key to the authorized_keyswith the following command.

cat lelylan-key.pub | \      
ssh <USER>@<IP-SERVER> "cat ~/.ssh/authorized_keys"
If some problems occur, connect first to the server through SSH
ssh <USER>@<IP-SERVER>

SSH Agent Forwarding

One more thing you need to do on your management server (where the nscale project is defined), is to set the SSH Agent Forwarding. This allows you to use your local SSH keys instead of leaving keys sitting on your server.

# ~/.ssh/config      
Host *
ForwardAgent yes
There is an open issueon this for nscale. If you do not set this up the deployment with nscale will not work out.

nscale configuration

We can now start configuring nscale, starting from the nscale analyzer, which defines the authorizations settings used to access the target machines. To make this possible edit ~/.nscale/config/config.jsonby setting the specificobject from:

{      
...
"modules": {
...
"analysis": {
"require": "nscale-local-analyzer",
"specific": {
}
}
...
}

to:

{      
...
"modules": {
...
"analysis": {
"require": "nscale-direct-analyzer",
"specific": {
"user": "root",
"identityFile": "/root/lelylan/lelylan-key"

}
}
}
Adjust this config if you named your project and your key differently.

All we did was to populate the specificobject with the user(root) and the identity file(ssh key ) (this step will likely not be needed in a next release).

Processes definition

In nscale we can define different processes, where every process is a Docker container identified by a name, a Github repo (with the container source code) and a set of arguments Docker uses to run the image.

If you noticed that redis has not a Github repo, contgrats! At this point of the article shouldn’t be easy☺. For Redis we do not need the Github repo as we directly use the redis image defined in Docker Hub.

In this case we have 3 different type of processes: haproxy, mqttand redis.

System Definition

Now that we’ve defined the processes we want to run, we can tell nscale where each of them should live on Digital Ocean through the system.jsdefinition.

As you can see, system.jsdefines every machine setup. For each of them, we define the running processes (you need to use one between the ones previously defined in services.js), the machine IP address, the user that can log in and and the ssh key name used to authorize the access.

What if I want to add a new MQTT server

Add a new machine to the nscale system.jsdefinition, the new server to the HAproxy configuration and you’re ready to go.

It’s deploying time☺

We can now compile, build and deploy our infrastructure.

# Compile the nscale project      
nsd sys comp direct
# Build all containers      
# (grab a cup of coffee, while nscale build everything)
nsd cont buildall
# Deploy the latest revision on Digital Ocean      
nsd rev dep head
While we described the configurations needed to deploy on Digital Ocean, nscale is also good to run all services locally.

You’re done!

Once the setup is done, with the previous three commands, we’re ready to deploy an high availability MQTT cluster for the Internet of Things, adding new MQTT servers and scaling our infrastructure in a matter of minutes.

Conclusions

This article comes from the work I’ve made in Lelylan, an Open Source Cloud Platform for the Internet of Things. If you find this article useful, give us a star on Github(it will help to reach more developers).

Source Code

In this article we showed how to build an high availability MQTT cluster for the Internet of Things. All of the code we use in production is now released as Open Source as follow.

We’ll soon release also the nscale project (right now it contains some sensible information and we need to remove them from the repo).

Many thanks to nearFormand Matteo Collina(author of Mosca and part of the nscale team) for helping us answering any question we had about nscale and the MQTT infrastructure.

Building, testing and securing such an infrastructure took several months of work. We really hope that releasing it as Open Source will help you guys on building MQTT platforms in a shorter time.

Want to learn more?

Not satisfied? If you want to learn more about some of the topics we talked about, read out the following articles!

[分享创造] 可能是 iOS 上最好用的电视直播软件

$
0
0

因为平时喜欢看电视,在 app store 上又找不到适合的,就想着自己造个轮子,临时抱佛脚学了三天 iOS 开发,写了个 app
功能:
1.自己添加管理 m3u8 直播源,这个没什么好说的,适合动手能力强的
2.订阅列表,订阅后,只要负责维护列表的大神列表更新,用户的列表就会自动更新
目前我自己维护了 4 个列表,加起来频道大概六七十个


放几张截图

V2er
V2er
V2er
V2er
V2er

tf: https://testflight.top/q/Y3yqIj
tg 群 http://t.me/conchplayer


另外请教大神,提交审核 24 小时就被拒,理由
Guideline 4.2 - Design - Minimum Functionality
有什么好方法

在微服务领域Spring Boot自动伸缩如何实现

$
0
0

自动伸缩是每个人都想要的,尤其是在微服务领域。让我们看看如何在基于Spring Boot的应用程序中实现。

我们决定使用 KubernetesPivotal Cloud FoundryHashiCorp's Nomad等工具的一个更重要的原因是为了让系统可以自动伸缩。当然,这些工具也提供了许多其他有用的功能,在这里,我们只是用它们来实现系统的自动伸缩。乍一看,这似乎很困难,但是,如果我们使用 Spring Boot来构建应用程序,并使用 Jenkins来实现 CI,那么就用不了太多工作。

今天,我将向您展示如何使用以下框架/工具实现这样的解决方案:

  • Spring Boot

  • Spring Boot Actuator

  • Spring Cloud Netflix Eureka

  • Jenkins CI

它是如何工作的

每一个包含 Spring Boot Actuator库的 Spring Boot应用程序都可以在 /actuator/metrics端点下公开 metric。许多有价值的 metric都可以提供应用程序运行状态的详细信息。在讨论自动伸缩时,其中一些 metric可能特别重要: JVM、CPU metric、正在运行的线程数和HTTP请求数。有专门的 Jenkins流水线通过按一定频率轮询 /actuator/metrics端点来获取应用程序的指标。如果监控的任何 metric【指标】低于或高于目标范围,则它会启动新实例或使用另一个 Actuator端点 /actuator/shutdown来关闭一些正在运行的实例。在此之前,我们需要知道当前有那些实践在提供服务,只有这样我们才能在需要的时候关闭空闲的实例或启动新的新例。

在讨论了系统架构之后,我们就可以继续开发了。这个应用程序需要满足以下要求:它必须有公开的可以优雅地关闭应用程序和用来获取应用程序运行状态 metric【指标】的端点,它需要在启动完成的同时就完成在Eureka的注册,在关闭时取消注册,最后,它还应该能够从空闲端口池中随机获取一个可用的端口。感谢 Spring Boot,只需要约五分钟,我们可以轻松地实现所有这些机制。

动态端口分配

由于可以在一台机器上运行多个应用程序实例,所以我们必须保证端口号不冲突。幸运的是, Spring Boot为应用程序提供了这样的机制。我们只需要将 application.yml中的 server.port属性设置为 0。因为我们的应用程序会在 Eureka中注册,并且发送唯一的标识 instanceId,默认情况下这个唯一标识是将字段 spring.cloud.client.hostname, spring.application.nameserver.port拼接而成的。

示例应用程序的当前配置如下所示。

可以看到,我通过将端口号替换为随机生成的数字来改变了生成 instanceId字段值的模板。

spring:
  application:
    name: example-service
server:
  port: ${PORT:0}
eureka:
  instance:
    instanceId: ${spring.cloud.client.hostname}:${spring.application.name}:${random.int[1,999999]}

启用Actuator的Metric

为了启用 Spring Boot Actuator,我们需要将下面的依赖添加到 pom.xml

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>

我们还必须通过HTTP API将属性 management.endpoints.web.exposure.include设置为 '*'来暴露 Actuator的端点。现在,所有可用的指标名称列表都可以在 /actuator/metrics端点中找到,每个指标的详细信息可以通过 /actuator/metrics/{metricName}端点查看。

优雅地停止应用程序

除了查看 metric端点外, Spring Boot Actuator还提供了停止应用程序的端点。然而,与其他端点不同的是,缺省情况下,此端点是不可用的。我们必须把 management.endpoint.shutdown.enabled设为 true。在那之后,我们就可以通过发送一个 POST请求到 /actuator/shutdown端点来停止应用程序了。

这种停止应用程序的方法保证了服务在停止之前从 Eureka服务器注销。

启用Eureka自动发现

Eureka是最受欢迎的发现服务器,特别是使用 Spring Cloud来构建微服务的架构。所以,如果你已经有了微服务,并且想要为他们提供自动伸缩机制,那么 Eureka将是一个自然的选择。它包含每个应用程序注册实例的IP地址和端口号。为了启用 Eureka客户端,您只需要将下面的依赖项添加到 pom.xml中。

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>

正如之前提到的,我们还必须保证通过客户端应用程序发送到 Eureka服务器的 instanceId的唯一性。在“动态端口分配”中已经描述了它。

下一步需要创建一个包含内嵌 Eureka服务器的应用程序。为了实现这个功能,首先我们需要在 pom.xml中添加下面这个依赖:

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency>

这个 main类需要添加 @EnableEurekaServer注解。

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryApp {
    public static void main(String[] args) {
        new SpringApplicationBuilder(DiscoveryApp.class).run(args);
    }
}

默认情况下,客户端应用程序尝试使用 8761端口连接 Eureka服务器。我们只需要单独的、独立的 Eureka节点,因此我们将禁用注册,并尝试从另一个 Eureka服务器实例中获取服务列表。

spring:
  application:
    name: discovery-service
server:
  port: ${PORT:8761}
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

我们将使用 Docker容器来测试上面的自动伸缩系统,因此需要使用 Eureka服务器来准备和构建 image

Dockerfileimage的定义如下所示。

我们可以使用命令 docker build -t piomin/discovery-server:2.0来进行构建。

FROM openjdk:8-jre-alpine
ENV APP_FILE discovery-service-1.0-SNAPSHOT.jar
ENV APP_HOME /usr/apps
EXPOSE 8761
COPY target/$APP_FILE $APP_HOME/
WORKDIR $APP_HOME
ENTRYPOINT ["sh", "-c"]
CMD ["exec java -jar $APP_FILE"]

为弹性伸缩构建一个Jenkins流水线

第一步是准备 Jenkins流水线,负责自动伸缩。我们将创建 Jenkins声明式流水线,它每分钟运行一次。可以使用 triggers指令配置执行周期,它定义了自动化触发流水线的方法。我们的流水线将与 Eureka服务器和每个使用 Spring Boot Actuator的微服务中公开的 metric端点进行通信。

测试服务的名称是 EXAMPLE-SERVICE,它和定义在 application.yml文件 spring.application.name的属性值(大写字母)相同。被监控的 metric是运行在Tomcat容器中的HTTP listener线程数。这些线程负责处理客户端的HTTP请求。

pipeline {
    agent any
    triggers {
        cron('* * * * *')
    }
    environment {
        SERVICE_NAME = "EXAMPLE-SERVICE"
        METRICS_ENDPOINT = "/actuator/metrics/tomcat.threads.busy?tag=name:http-nio-auto-1"
        SHUTDOWN_ENDPOINT = "/actuator/shutdown"
    }
    stages { ... }
}

使用Eureka整合Jenkins流水线

流水线的第一个阶段负责获取在 discovery服务器上注册的服务列表。 Eureka发现了几个HTTP API端点。其中一个是 GET /eureka/apps/{serviceName},它返回一个给定服务名称的所有活动实例列表。我们正在保存运行实例的数量和每个实例 metric端点的URL。这些值将在流水线的下一个阶段中被访问。

下面的流水线片段可以用来获取活动应用程序实例列表。 stage名称是 Calculate。我们使用 HTTP请求插件来发起HTTP连接。

stage('Calculate') {
 steps {
  script {
   def response = httpRequest "http://192.168.99.100:8761/eureka/apps/${env.SERVICE_NAME}"
   def app = printXml(response.content)
   def index = 0
   env["INSTANCE_COUNT"] = app.instance.size()
   app.instance.each {
    if (it.status == 'UP') {
     def address = "http://${it.ipAddr}:${it.port}"
     env["INSTANCE_${index++}"] = address
    }
   }
  }
 }
}
@NonCPS
def printXml(String text) {
 return new XmlSlurper(false, false).parseText(text)
}

下面是 EurekaAPI对我们的微服务的示例响应。响应 content-typeXML

使用Spring Boot Actuator整合Jenkins流水线

Spring Boot Actuator使用 metric来公开端点,这使得我们可以通过名称和选择性地使用标签找到 metric。在下面可见的流水线片段中,我试图找到 metric低于或高于阈值的实例。如果有这样的实例,我们就停止循环,以便进入下一个阶段,它执行向下或向上的伸缩。应用程序的IP地址是从带有 INSTANCE_前缀的流水线环境变量获取的,这是在前一阶段中被保存了下来的。

stage('Metrics') {
steps {
script {
def count = env.INSTANCE_COUNT
for(def i=0;i 100)
return "UP"
else if (value.toInteger() < 20)
return "DOWN"
else
return "NONE"
}

关闭应用程序实例

在流水线的最后一个阶段,我们将关闭运行的实例,或者根据在前一阶段保存的结果启动新的实例。通过调用 Spring Boot Actuator端点可以很容易执行停止操作。在接下来的流水线片段中,首先选择了 Eureka实例。然后我们将发送 POST请求到那个ip地址。

如果需要扩展应用程序,我们将调用另一个流水线,它负责构建 fat JAR并让这个应用程序在机器上跑起来。

stage('Scaling') {
 steps {
  script {
   if (env.SCALE_TYPE == 'DOWN') {
    def ip = env["INSTANCE_0"] + env.SHUTDOWN_ENDPOINT
    httpRequest url: ip, contentType: 'APPLICATION_JSON', httpMode: 'POST'
   } else if (env.SCALE_TYPE == 'UP') {
    build job: 'spring-boot-run-pipeline'
   }
   currentBuild.description = env.SCALE_TYPE
  }
 }
}

下面是 spring-boot-run-pipeline流水线的完整定义,它负责启动应用程序的新实例。它先从 git仓库中拉取源代码,然后使用 Maven命令编译并构建二进制的jar文件,最后通过在 java -jar命令中添加 Eureka服务器地址来运行应用程序。

pipeline {
    agent any
    tools {
        maven 'M3'
    }
    stages {
        stage('Checkout') {
            steps {
                git url: 'https://github.com/piomin/sample-spring-boot-autoscaler.git', credentialsId: 'github-piomin', branch: 'master'
            }
        }
        stage('Build') {
            steps {
                dir('example-service') {
                    sh 'mvn clean package'
                }
            }
        }
        stage('Run') {
            steps {
                dir('example-service') {
                    sh 'nohup java -jar -DEUREKA_URL=http://192.168.99.100:8761/eureka target/example-service-1.0-SNAPSHOT.jar 1>/dev/null 2>logs/runlog &'
                }
            }
        }
    }
}

扩展到多个机器

在前几节中讨论的算法只适用于在单个机器上启动的微服务。如果希望将它扩展到更多的机器上,我们将不得不修改我们的架构,如下所示。每台机器都有 Jenkins代理运行并与 Jenkinsmaster通信。如果想在选定的机器上启动一个微服务的新实例,我们就必须使用运行在该机器上的代理来运行流水线。此代理仅负责从源代码构建应用程序并将其启动到目标机器上。这个实例的关闭仍然是通过调用HTTP端点来完成。

假设我们已经成功地在目标机器上启动了一些代理,我们需要对流水线进行参数化,以便能够动态地选择代理(以及目标机器)。

当扩容应用程序时,我们必须将代理标签传递给下游流水线。

build job:'spring-boot-run-pipeline', parameters:[string(name: 'agent', value:"slave-1")]

调用流水线具体由那个标签下的代理运行,是由" ${params.agent}"决定的。

pipeline {
    agent {
        label "${params.agent}"
    }
    stages { ... }
}

如果有一个以上的代理连接到主节点,我们就可以将它们的地址映射到标签中。由于这一点,我们能够将从 Eureka服务器获取的微服务实例的IP地址映射到与 Jenkins代理的目标机器上。

pipeline {
    agent any
    triggers {
        cron('* * * * *')
    }
    environment {
        SERVICE_NAME = "EXAMPLE-SERVICE"
        METRICS_ENDPOINT = "/actuator/metrics/tomcat.threads.busy?tag=name:http-nio-auto-1"
        SHUTDOWN_ENDPOINT = "/actuator/shutdown"
        AGENT_192.168.99.102 = "slave-1"
        AGENT_192.168.99.103 = "slave-2"
    }
    stages { ... }
}

总结

在本文中,我演示了如何使用 Spring Boot Actuatometric来自动伸缩 Spring Boot应用程序。使用 Spring Boot提供的特性以及 Spring Cloud Netflix EurekaJenkins,您就可以实现系统的自动伸缩,而无需借助于任何其他第三方工具。本文也假设远程服务器上也是使用 Jenkins代理来启动新的实例,但是您也可以使用 Ansible这样的工具来启动。如果您决定从 Jenkins运行 Ansible脚本,那么将不需要在远程机器上启动 Jenkins代理。

关于机器学习,你应该知道的 3 个热门专业术语

$
0
0


编者按:如果你是刚入门机器学习的AI探索者,你知道什么是胶囊网络吗?AutoML和元学习又是什么?为了帮大家节省查阅晦涩难懂的论文的时间,我们邀请微软亚洲研究院机器学习组实习生吴郦军、罗人千帮大家用最通俗的语言解释了这三个机器学习领域的热门词汇,赶紧收藏吧!


胶囊网络Capsule Networks

胶囊网络(Capsule Networks)是深度学习三巨头之一的Geoffrey Hinton提出的一种全新的神经网络。最初发表在2017年的NIPS会议上:Dynamic Routing Between Capsules。胶囊网络基于一种新的结构——胶囊(Capsule),通过与现有的卷积神经网络(CNN)相结合,从而在一些图像分类的数据上取得了非常优越的性能。


何谓胶囊?简单来说, 胶囊就是将原有大家熟知的神经网络中的个体神经元替换成了一组神经元组成的向量,这些神经元被包裹在一起,组成了一个胶囊。因此,胶囊网络中的每层神经网络都包含了多个胶囊基本单元,这些胶囊与上层网络中的胶囊进行交互传递。



胶囊网络的主要特点是什么呢?与传统CNN相比优势是什么呢?下图简单比较了胶囊和传统的神经网络中神经元的不同。

 


两者最大的不同在于, 胶囊网络中的神经元是一个整体,包含了特征状态的各类重要信息,比如长度、角度、方向等,而传统的CNN里每个神经元都是独立的个体,无法刻画位置、角度等信息。这也就是为什么CNN通过数据增广的形式(对于同一个物体,加入不同角度、不同位置的图片进行训练),能够大大提高模型最后的结果。


胶囊网络能够保证图像中不同的对象(比如人脸中的鼻子、眼睛、嘴巴)之间的相对关系不受角度改变的影响,这一特性来自于图形图像学的启发。对于3D图像,人类的大脑能够在不同的位置对于这个图像都做出准确的判别。当我们以向量的形式将特性状态封装在胶囊中时,胶囊拥有状态特性的长度(以概率形式加权编码)以及状态的方向(特征向量的方向)。因此对于胶囊来说,长度相同的特征,其方向也存在着变化,而这样的变化对于模型训练就正如不同角度的增广图像。


胶囊的工作原理是基于“囊间动态路由”的算法,这是一种迭代算法。简单地说,两层之间的胶囊信息传递,会通过计算两者之间的一种相关信息来决定下层的胶囊如何将自己的特征传递给上层的胶囊。也就是说,下层胶囊将其输出发送给对此表示“同意”的上层胶囊,利用输入与输出之间的点积相似性,来更新路由间的系数。


跟传统的CNN相比,当前的胶囊网络在实验效果上取得了更好的结果,但是训练过程却慢了很多,因此胶囊网络依然很有很大的发展空间。


自动机器学习AutoML

在实际的AI应用中,如果想让机器学习获得比较好的学习结果,除了对数据进行初步分析、处理,可能还需要依赖领域知识对数据进行进一步的特征提取和特征选择,然后根据不同的任务及数据特征选择合适的机器学习模型,在训练模型时还要调大量的超参数,尝试各种tricks。整个过程中需要花费大量的人工和时间。因此,机器学习从业者都戏称自己是“调参工程师”,称自己的工作是“有多少人工就有多少智能”。对于初入门的小白及大量普通开发者来说,机器学习工具比较难以掌握。


为了减少这些需要人工干预的繁杂工作, 自动机器学习(Automatic Machine Learning,简称AutoML)应运而生。它能 自动选择合适的算法模型以及调整超参数,并最终取得不错的学习效果。简单来说,自动机器学习过程就是用户提供数据集,确定任务目标,之后的工作就交给AutoML来处理,用户将会得到一个训练好的模型。这大大降低了使用机器学习工具的门槛,让机器学习工具的使用过程变得简单、轻松。


我们以AutoML里的一个子领域NAS(Neural Architecture Search,神经网络结构搜索)为例。顾名思义,NAS是自动搜索神经网络的结构。传统神经网络都是由人工设计的,经过长时间的演化迭代,从AlexNet到DenseNet,性能不断上升,效果也不断提升。但正如前文所说,神经网络结构的演化过程耗费了大量的人工。不同的基础网络结构,如AlexNet、VGG、ResNet、DenseNet等需要深度学习的专业研究人员进行研究改进,而它们在具体任务上的应用又需要进一步调整相应的参数和结构。



NAS旨在针对给定的数据集和学习任务,自动搜索出适用于该任务的好的网络结构。决定一个神经网络“区别于其它网络”的关键因素包括网络结构里每层的运算操作(如不同种类、大小的卷积和池化操作)、每层的大小、层与层之间的连接方式、采用的激活函数等。这些关键因素在传统的人工设计的神经网络里都是固定的,但在自动搜索网络结构里可能都是未知的。算法需要通过自动搜索进而最终决定一个神经网络的结构。


2016年Barret Zoph等人发表了Neural architecture search with reinforcement learning一文,文中提出了控制器-子网络的框架,其中子网络即我们要应用在目标任务上的网络,控制器则负责生成子网络的结构。对于图像类任务,子网络采用CNN,搜索其每层的运算操作和连接方式;对于文本类任务,子网络采用RNN时,搜索其每层的激活函数和连接方式。控制器搜索出的子网络结构在目标任务的数据验证集上的性能则作为reward反馈给控制器,通过强化学习进行训练,使得控制器经过不断的学习迭代生成更好的子网络结构。但是这一工作使用了大量GPU资源,耗费了一个月时间才得到了最后的结果。


随后,有一系列的工作对NAS做出了改进:改进搜索空间(搜索单一block里的结构,之后堆叠多个block作为最终网络)、改进搜索算法(使用演化算法、梯度优化等)、提升搜索效率(通过参数共享等)等。这些工作提升了NAS本身的搜索效率和性能,同时搜索出的CNN网络也在主要的数据集(CIFAR10、CIFAR100、IMAGENET)上取得了SOTA,超过了人工设计的网络的性能。微软亚洲研究院机器学习组发表在NIPS 2018上的工作Neural Architecture Optimization [1],利用网络结构在验证集上的性能对网络的梯度信息来优化网络结构。首先将离散的网络结构用编码器转换成连续空间里的向量,然后训练了一个预测器来预测该向量(网络结构)在验证集上的性能,从而可以直接基于预测结果对该向量的梯度进行优化,生成更好的向量(网络结构),最后再通过解码器解码将生成的向量解码成离散的网络结构。我们的算法搜索出的CNN和RNN结构在相应任务(CIFAR10、CIFAR100、PTB、Wikitext-2)上皆取得了超过其它NAS工作的最好性能。


元学习Meta Learning

我们期待的通用人工智能的目标是让人工智能像人一样学会推理、思考,能快速学习。对于现实世界的很多问题,人类之所以能够快速学习是因为人类具有强大的思考推理能力以及学习能力。人类能够利用以往学习到的知识经验来指导新知识的学习,做到“触类旁通”、“举一反三”,这让人类的学习行为变得十分高效。


元学习(Meta Learning)的目的就是研究如何让机器学习系统拥有学习的能力,能够更好、更高效地学习,从而取得更好的学习效果。比如对于数据集,采取什么方式、什么顺序、什么策略进行学习,对于学习效果如何进行评测,这些都会影响到模型学习的效果。



微软亚洲研究院机器学习组今年发表在NIPS 2018上的工作Learning to Teach with Dynamic Loss Functions [2]使用一个teacher model来指导student model(学习具体任务的模型)学习,让student model在学习过程中动态利用学习到的不同的损失函数(loss function)来处理不同数据的学习,学习到的模型在相应任务上取到了很好的结果。


你还想了解哪些AI领域的专业词汇呢?欢迎在评论区留言!


参考文献

[1] Renqian Luo, Fei Tian, Tao Qin, Tie-Yan Liu, Neural Architecture Optimization, NIPS 2018


[2] Lijun Wu, Fei Tian, Yingce Xia, Tao Qin, Tie-Yan Liu, Learning to Teach with Dynamic Loss Functions, NIPS 2018


作者简介


吴郦军,微软亚洲研究院-中山大学联合培养博士生,目前直博四年级在读。研究方向包括机器学习、深度学习、强化学习、机器翻译等,曾在NIPS,EMNLP,AAAI,IJCAI等会议上发表论文。



罗人千,微软亚洲研究院-中国科学技术大学联合培养博士生,目前博士三年级在读。研究方向包括机器学习、深度学习、机器翻译等,曾在NIPS上发表论文。



你也许还想看


  微软AutoML工具,现已加入Azure机器学习服务豪华套餐

  ICLR 2018论文 | Learning to Teach:让AI和机器学习算法教学相长

  书单丨成为机器学习大神,你不能不懂数学



感谢你关注“微软研究院AI头条”,我们期待你的留言和投稿,共建交流平台。来稿请寄:msraai@microsoft.com。



如何快速读Paper – ThoughtWorks洞见

$
0
0

自从上次介绍了 去哪里找paper之后,大家问我的问题就常常变成了: 如何快速阅读一篇paper并准确的提取其中有用的信息。在本文中,我将试图为大家简要解答这个问题,争取告诉大家如何在短时间内通过阅读文献的方式了解一个新的领域。

阅读一篇paper通常见的目的有四种:

  1. 面对一个新的领域,我要快速把握这个领域的研究方向和state-of-the-art方法,来给自己或者团队设计一个大致的技术方案。
  2. 这个领域我很熟悉了,我要看看有没有什么新idea。又或者我马上要写一篇类似的文章,先上来探探路看看别人都干了什么、怎么写的;
  3. 老师/编辑非要让我读,然后给大家讲 or 给审阅意见;
  4. 睡前/早起例行关注新闻,跟刷牙时候听新闻联播没啥区别,就是看看热闹。

(​引用自 ​http://phdcomics.com/comics/archive.php?comicid=963)​

在一一回答以上四个问题之前,先教大家如何避开一个大坑——关于出版机构。虽然正常的人类实在是没什么必要对各种会议组织和出版机构如数家珍,按照出版社级别给paper质量排序就好像根据学校名声给学生确定刻板印象一样在公序良俗上不靠谱,但是我们关起门来说,知道哪个出版方的论文质量比较低、不太值得看确实可以给我们节省不少时间。在这里只举一个大家(包括很多科研水平不错、只是不在英语世界混的学者)常常掉进去的坑: hindawi.com。 非常高产的一个Open Access出版社,主页看上去也很是像那么回事儿,但是很抱歉,其中大部分文章都只会浪费大家的时间。

回到问题1

不妨找一个专业人士,和他大概描述一下自己的问题领域,让他发一篇survey给你。或者自己去google Scholar上自己去找那种以survey/review为题目的文章。这里我以推荐系统为例,大家直接在搜索框里面输入survey recommendation system,点击搜索,就可以得到如下​结果。​

然后挑选前面引用数目破千的来看,基本都不会有什么问题。比如说 第一篇​就会给你介绍很重要的几个概念:Content-based recommendations;. Collaborative recommendations;Hybrid approaches。为你之后的论文阅读打下坚实的基础。大部分写过paper的人,包括我在内,总是默认读者知道领域内的一些基础概念的——这也是我总被人告知要说人话的原因。

找不到survey怎么办呢——要知道并不是每一个领域都有靠谱的、现成的survey可以读的。这个时候,请按照下一项的建议,通读个十几篇行业内引用数较高的文章,如果在这个阶段还读不懂也没关系,尝试着找出它们共同引用的文章,从那里开始。找到领域内高产的第一作者(排名第一的作者,常常是论文的主要贡献者)和通讯作者(排名最后但是名字上带个星号的作者,通常为业内大牛或者付钱的那个人)的主页,上去看下这个人最近在干什么,都在什么会议或期刊上,发表了什么主题的文章。

总体来说,是一个“文章-作者(以及reference的作者)-会议/期刊-文章-作者-……” 的一个大雪球,雪球越滚越大的同时,你的知识领域也会越来越丰富。

对于问题2和3

基本建议采用Waterloo大学S. Keshav的“ 三遍法”(以下为避免翻译不够信达雅,关键字均用英文)。笔者对三遍法基于工程师的阅读习惯做了一些修改——其实这个时候读者已经很熟悉问题背景和常用算法了,Introduction的细读相对来说就不那么重要,需要的是最快速度的十分钟了解文章大概,不过通读Introduction永远是是面对新领域或者没见过上下文的新paper时候的最优选择。

第一遍读Title、Abstract和Conclusion部分,略读Introduction,其他部分只要看章节标题和小标题就可以了。最后快速刷一遍Reference看看有没有自己看过的。这一遍大概只要十分钟,就可以对作者要解决的问题和解决方案有一个大概的把握,进而决定要不要读下去。

对于你觉得值得读的文章,第二遍读文章中的图表和方法,把看不懂的方法和参考文献都标记出来。这一遍大概要花一两个小时,你会详细的知道作者达到的效果,并且对自己的领域(比如说换个数据集或者损失函数什么的)能不能用类似的方案达到类似的效果,做出一个初步的评估。在这一遍成功结束之后,当有其他人问起,你可以大概复述出主要实现方式。

对于实在非常重要的文章,又或者是不得不认真读的文章(比如审稿),我们读第三轮。在这一轮中,往往会亲手根据作者的假设和思路进行一轮推演,发现那些作者不曾写在文章里的思路(常常也是坑),有源码的文章可以把源码搂下来试试看。没有源码的文章可以尝试着把核心部分做个小小的poc。这一遍(我个人)通常也需要带着些批判性思维去做,尽量找出可以提升或者没说清楚的地方——如果让你来做,你会怎么做?有没有看上去更好的解决方案?有哪些细节可以提升一下?

对于问题4

其实也没什么好说的,可以关注几个顺眼的会议列表或者各个企业的公众号。睡前一读娱乐身心,尽量不要搞到一群Reference的Reference的Reference递归看下去睡不着就好。

在最后,再给大家两个小的tips:

  1. 如果对这个领域不够熟悉,真的不要嫌弃排名靠前、引用数多的那几篇“老古董”,相信我,他们比那些2017年之后发布的好懂很多很多很多。越晚发布的,对于高新技术的依赖就越强,引用的参考文献就越是繁杂,对入门者也就越不友好。
  2. 隔行如隔山,很多时候其实你并不清楚你想要寻找的(英语)关键字是什么,比如在预测明天A区域房价的时候,如果你知道的是今天以前的房价,那么你或者应该从“time series survey”开始;而如果你知道的是房子的面积地段楼层户型,那么传统基于特征的预测有可能是你的首选。所以,一个活生生的“人工智能”专家在某些时候能帮你省好多劲儿,不要羞耻的去发问吧!看到哪篇文章实在很感兴趣,直接发信就行!

​P.S.

​分享​一个 小插件。点击安装到Chrome里面可以方便随手搜索。

再分享我用的论文管理软件 Mendeley​,跨平台,好管理,好标注。回头大家用的多了有疑问,我可以写个评测。​ ​


NLP历史突破!谷歌BERT模型狂破11项纪录,全面超越人类!

$
0
0

来源:新智元(AI_era)

(来源:arXiv、知乎;编辑:新智元编辑部)

今天,NLP 领域取得最重大突破!谷歌 AI 团队新发布的 BERT 模型,在机器阅读理解顶级水平测试 SQuAD1.1 中表现出惊人的成绩:全部两个衡量指标上全面超越人类,并且还在 11 种不同 NLP 测试中创出最佳成绩。毋庸置疑,BERT 模型开启了 NLP 的新时代!

今天请记住 BERT 模型这个名字。

谷歌 AI 团队新发布的 BERT 模型,在机器阅读理解顶级水平测试 SQuAD1.1 中表现出惊人的成绩:全部两个衡量指标上全面超越人类!并且还在 11 种不同 NLP 测试中创出最佳成绩,包括将 GLUE 基准推至 80.4%(绝对改进 7.6%),MultiNLI 准确度达到 86.7%(绝对改进率 5.6%)等。

谷歌团队的 Thang Luong 直接定义:BERT 模型开启了 NLP 的新时代!

本文从论文解读、BERT 模型的成绩以及业界的评价三方面做介绍。

硬核阅读:认识 BERT 的新语言表示模型

首先来看下谷歌 AI 团队做的这篇论文(论文地址: https://arxiv.org/abs/1810.04805)。

BERT 的新语言表示模型,它代表 Transformer 的双向编码器表示。与最近的其他语言表示模型不同,BERT 旨在通过联合调节所有层中的上下文来预先训练深度双向表示。因此,预训练的 BERT 表示可以通过一个额外的输出层进行微调,适用于广泛任务的最先进模型的构建,比如问答任务和语言推理,无需针对具体任务做大幅架构修改。

论文作者认为现有的技术严重制约了预训练表示的能力。其主要局限在于标准语言模型是单向的,这使得在模型的预训练中可以使用的架构类型很有限。

在论文中,作者通过提出 BERT:即 Transformer 的双向编码表示来改进基于架构微调的方法。

BERT 提出一种新的预训练目标:遮蔽语言模型(masked language model,MLM),来克服上文提到的单向性局限。MLM 的灵感来自 Cloze 任务(Taylor, 1953)。MLM 随机遮蔽模型输入中的一些 token,目标在于仅基于遮蔽词的语境来预测其原始词汇 id。

与从左到右的语言模型预训练不同,MLM 目标允许表征融合左右两侧的语境,从而预训练一个深度双向 Transformer。除了遮蔽语言模型之外,本文作者还引入了一个“下一句预测”(next sentence prediction)任务,可以和 MLM 共同预训练文本对的表示。

论文的核心:详解 BERT 模型架构

本节介绍 BERT 模型架构和具体实现,并介绍预训练任务,这是这篇论文的核心创新。

模型架构

BERT 的模型架构是基于 Vaswani et al. (2017) 中描述的原始实现 multi-layer bidirectional Transformer 编码器,并在 tensor2tensor 库中发布。由于 Transformer 的使用最近变得无处不在,论文中的实现与原始实现完全相同,因此这里将省略对模型结构的详细描述。

在这项工作中,论文将层数(即 Transformer blocks)表示为L,将隐藏大小表示为H,将 self-attention heads 的数量表示为A。在所有情况下,将 feed-forward/filter 的大小设置为 4H,即 H = 768 时为 3072,H = 1024 时为 4096。论文主要报告了两种模型大小的结果:

- BERT BASE: L=12, H=768, A=12, Total Parameters=110M

- BERT LARGE: L=24, H=1024, A=16, Total Parameters=340M

为了进行比较,论文选择 BERT LARGE,它与 OpenAI GPT 具有相同的模型大小。然而,重要的是,BERT Transformer 使用双向 self-attention,而 GPT Transformer 使用受限制的 self-attention,其中每个 token 只能处理其左侧的上下文。研究团队注意到,在文献中,双向 Transformer 通常被称为“Transformer encoder”,而左侧上下文被称为“Transformer decoder”,因为它可以用于文本生成。BERT,OpenAI GPT 和 ELMo 之间的比较如图 1 所示。

图1:预训练模型架构的差异。BERT 使用双向 Transformer。OpenAI GPT 使用从左到右的 Transformer。ELMo 使用经过独立训练的从左到右和从右到左 LSTM 的串联来生成下游任务的特征。三个模型中,只有 BERT 表示在所有层中共同依赖于左右上下文。

图1:预训练模型架构的差异。BERT 使用双向 Transformer。OpenAI GPT 使用从左到右的 Transformer。ELMo 使用经过独立训练的从左到右和从右到左 LSTM 的串联来生成下游任务的特征。三个模型中,只有 BERT 表示在所有层中共同依赖于左右上下文。

输入表示(input representation)

论文的输入表示(input representation)能够在一个 token 序列中明确地表示单个文本句子或一对文本句子(例如, [Question, Answer])。对于给定 token,其输入表示通过对相应的 token、segment 和 position embeddings 进行求和来构造。图 2 是输入表示的直观表示:

图2:BERT 输入表示。输入嵌入是 token embeddings, segmentation embeddings 和 position embeddings 的总和。

图2:BERT 输入表示。输入嵌入是 token embeddings, segmentation embeddings 和 position embeddings 的总和。

具体如下:

- 使用 WordPiece 嵌入(Wu et al., 2016)和 30,000 个 token 的词汇表。用##表示分词。

- 使用学习的 positional embeddings,支持的序列长度最多为 512 个 token。

- 每个序列的第一个 token 始终是特殊分类嵌入([CLS])。对应于该 token 的最终隐藏状态(即,Transformer 的输出)被用作分类任务的聚合序列表示。对于非分类任务,将忽略此向量。

- 句子对被打包成一个序列。以两种方式区分句子。首先,用特殊标记([SEP])将它们分开。其次,添加一个 learned sentence A 嵌入到第一个句子的每个 token 中,一个 sentence B 嵌入到第二个句子的每个 token 中。

- 对于单个句子输入,只使用 sentence A 嵌入。

关键创新:预训练任务

与 Peters et al. (2018) 和 Radford et al. (2018) 不同,论文不使用传统的从左到右或从右到左的语言模型来预训练 BERT。相反,使用两个新的无监督预测任务对 BERT 进行预训练。

任务1:Masked LM

从直觉上看,研究团队有理由相信,深度双向模型比 left-to-right 模型或 left-to-right and right-to-left 模型的浅层连接更强大。遗憾的是,标准条件语言模型只能从左到右或从右到左进行训练,因为双向条件作用将允许每个单词在多层上下文中间接地“see itself”。

为了训练一个深度双向表示(deep bidirectional representation),研究团队采用了一种简单的方法,即随机屏蔽(masking)部分输入 token,然后只预测那些被屏蔽的 token。论文将这个过程称为“masked LM”(MLM),尽管在文献中它经常被称为 Cloze 任务(Taylor, 1953)。

在这个例子中,与 masked token 对应的最终隐藏向量被输入到词汇表上的输出 softmax 中,就像在标准 LM 中一样。在团队所有实验中,随机地屏蔽了每个序列中 15% 的 WordPiece token。与去噪的自动编码器(Vincent et al., 2008)相反,只预测 masked words 而不是重建整个输入。

虽然这确实能让团队获得双向预训练模型,但这种方法有两个缺点。首先,预训练和 finetuning 之间不匹配,因为在 finetuning 期间从未看到[MASK]token。为了解决这个问题,团队并不总是用实际的[MASK]token 替换被“masked”的词汇。相反,训练数据生成器随机选择 15% 的 token。例如在这个句子“my dog is hairy”中,它选择的 token 是“hairy”。然后,执行以下过程:

数据生成器将执行以下操作,而不是始终用[MASK]替换所选单词:

- 80% 的时间:用[MASK]标记替换单词,例如,my dog is hairy → my dog is [MASK]

- 10% 的时间:用一个随机的单词替换该单词,例如,my dog is hairy → my dog is apple

- 10% 的时间:保持单词不变,例如,my dog is hairy → my dog is hairy. 这样做的目的是将表示偏向于实际观察到的单词。

Transformer encoder 不知道它将被要求预测哪些单词或哪些单词已被随机单词替换,因此它被迫保持每个输入 token 的分布式上下文表示。此外,因为随机替换只发生在所有 token 的 1.5%(即 15% 的 10%),这似乎不会损害模型的语言理解能力。

使用 MLM 的第二个缺点是每个 batch 只预测了 15% 的 token,这表明模型可能需要更多的预训练步骤才能收敛。团队证明 MLM 的收敛速度略慢于 left-to-right 的模型(预测每个 token),但 MLM 模型在实验上获得的提升远远超过增加的训练成本。

任务2:下一句预测

许多重要的下游任务,如问答(QA)和自然语言推理(NLI)都是基于理解两个句子之间的关系,这并没有通过语言建模直接获得。

在为了训练一个理解句子的模型关系,预先训练一个二进制化的下一句测任务,这一任务可以从任何单语语料库中生成。具体地说,当选择句子A和B作为预训练样本时,B有 50% 的可能是A的下一个句子,也有 50% 的可能是来自语料库的随机句子。例如:

Input = [CLS] the man went to [MASK] store [SEP]

he bought a gallon [MASK] milk [SEP]

Label = IsNext

Input = [CLS] the man [MASK] to the store [SEP]

penguin [MASK] are flight ##less birds [SEP]

Label = NotNext

团队完全随机地选择了 NotNext 语句,最终的预训练模型在此任务上实现了 97%-98% 的准确率。

实验结果

如前文所述,BERT 在 11 项 NLP 任务中刷新了性能表现记录!在这一节中,团队直观呈现 BERT 在这些任务的实验结果,具体的实验设置和比较请阅读原论文。

图3:我们的面向特定任务的模型是将 BERT 与一个额外的输出层结合而形成的,因此需要从头开始学习最小数量的参数。在这些任务中,(a)和(b)是序列级任务,而(c)和(d)是 token 级任务。在图中,E表示输入嵌入,Ti 表示 tokeni 的上下文表示,[CLS]是用于分类输出的特殊符号,[SEP]是用于分隔非连续 token 序列的特殊符号。

图3:我们的面向特定任务的模型是将 BERT 与一个额外的输出层结合而形成的,因此需要从头开始学习最小数量的参数。在这些任务中,(a)和(b)是序列级任务,而(c)和(d)是 token 级任务。在图中,E表示输入嵌入,Ti 表示 tokeni 的上下文表示,[CLS]是用于分类输出的特殊符号,[SEP]是用于分隔非连续 token 序列的特殊符号。

图4:GLUE 测试结果,由 GLUE 评估服务器给出。每个任务下方的数字表示训练样例的数量。“平均”一栏中的数据与 GLUE 官方评分稍有不同,因为我们排除了有问题的 WNLI 集。BERT 和 OpenAI GPT 的结果是单模型、单任务下的数据。所有结果来自 https://gluebenchmark.com/leaderboard 和 https://blog.openai.com/language-unsupervised/

图4:GLUE 测试结果,由 GLUE 评估服务器给出。每个任务下方的数字表示训练样例的数量。“平均”一栏中的数据与 GLUE 官方评分稍有不同,因为我们排除了有问题的 WNLI 集。BERT 和 OpenAI GPT 的结果是单模型、单任务下的数据。所有结果来自 https://gluebenchmark.com/leaderboard 和 https://blog.openai.com/language-unsupervised/

图5:SQuAD 结果。BERT 集成是使用不同预训练检查点和微调种子(fine-tuning seed)的 7x 系统。
图5:SQuAD 结果。BERT 集成是使用不同预训练检查点和微调种子(fine-tuning seed)的 7x 系统。

图6:CoNLL-2003 命名实体识别结果。超参数由开发集选择,得出的开发和测试分数是使用这些超参数进行五次随机重启的平均值。

图6:CoNLL-2003 命名实体识别结果。超参数由开发集选择,得出的开发和测试分数是使用这些超参数进行五次随机重启的平均值。

超过人类表现,BERT 刷新了 11 项 NLP 任务的性能记录

论文的主要贡献在于:

- 证明了双向预训练对语言表示的重要性。与之前使用的单向语言模型进行预训练不同,BERT 使用遮蔽语言模型来实现预训练的深度双向表示。

- 论文表明,预先训练的表示免去了许多工程任务需要针对特定任务修改体系架构的需求。BERT 是第一个基于微调的表示模型,它在大量的句子级和 token 级任务上实现了最先进的性能,强于许多面向特定任务体系架构的系统。

- BERT 刷新了 11 项 NLP 任务的性能记录。本文还报告了 BERT 的模型简化研究(ablation study),表明模型的双向性是一项重要的新成果。相关代码和预先训练的模型将会公布在 goo.gl/language/bert 上。

BERT 目前已经刷新的 11 项自然语言处理任务的最新记录包括:将 GLUE 基准推至 80.4%(绝对改进 7.6%),MultiNLI 准确度达到 86.7%(绝对改进率 5.6%),将 SQuAD v1.1 问答测试 F1 得分纪录刷新为 93.2 分(绝对提升 1.5 分),超过人类表现 2.0 分。

BERT 模型重要意义:宣告 NLP 范式的改变

北京航空航天大学计算机专业博士吴俣在知乎上写道:BERT 模型的地位类似于 ResNet 在图像,这是里程碑式的工作,宣告着 NLP 范式的改变。以后研究工作估计很多都要使用他初始化,就像之前大家使用 word2vec 一样自然。

BERT 一出,那几个他论文里做实验的数据集全被轰平了,大家洗洗睡了。心疼 swag 一秒钟,出现 3 月,第一篇做这个数据集的算法,在超了 baseline 20 多点的同时也超过人了。

通过 BERT 模型,吴俣有三个认识:

1、Jacob 在细节上是一等一的高手

这个模型的双向和 Elmo 不一样,大部分人对论文作者之一 Jacob 的双向在 novelty 上的 contribution 的大小有误解,我觉得这个细节可能是他比 Elmo 显著提升的原因。Elmo 是拼一个左到右和一个右到左,他这个是训练中直接开一个窗口,用了个有顺序的 cbow。

2、Reddit 对跑一次 BERT 的价格讨论

For TPU pods:

4 TPUs * ~$2/h (preemptible) * 24 h/day * 4 days = $768 (base model)

16 TPUs = ~$3k (large model)

For TPU:

16 tpus * $8/hr * 24 h/day * 4 days = 12k

64 tpus * $8/hr * 24 h/day * 4 days = 50k

For GPU:

"BERT-Large is 24-layer, 1024-hidden and was trained for 40 epochs over a 3.3 billion word corpus. So maybe 1 year to train on 8 P100s? "

3、不幸的是,基本无法复现,所以模型和数据谁更有用也不好说。

BERT 的成功也说明,好的深度学习研究工作的三大条件:数据,计算资源,工程技能点很高的研究员(Jacob 在微软时候,就以单枪匹马搭大系统,而中外闻名)。

本文链接

基于 TensorFlow Serving 的深度学习在线预估

$
0
0


一、前言

随着深度学习在图像、语言、广告点击率预估等各个领域不断发展,很多团队开始探索深度学习技术在业务层面的实践与应用。而在广告CTR预估方面,新模型也是层出不穷: Wide and Deep[1]、DeepCross Network[2]、DeepFM[3]、xDeepFM[4],美团很多篇深度学习博客也做了详细的介绍。但是,当离线模型需要上线时,就会遇见各种新的问题: 离线模型性能能否满足线上要求、模型预估如何镶入到原有工程系统等等。只有准确的理解深度学习框架,才能更好地将深度学习部署到线上,从而兼容原工程系统、满足线上性能要求。

本文首先介绍下美团平台用户增长组业务场景及离线训练流程,然后主要介绍我们使用TensorFlow Serving部署WDL模型到线上的全过程,以及如何优化线上服务性能,希望能对大家有所启发。

二、业务场景及离线流程


2.1 业务场景


在广告精排的场景下,针对每个用户,最多会有几百个广告召回,模型会根据用户特征与每一个广告相关特征,分别预估该用户对每条广告的点击率,从而进行排序。由于广告交易平台(AdExchange)对于DSP的超时时间限制,我们的排序模块平均响应时间必须控制在10ms以内,同时美团DSP需要根据预估点击率参与实时竞价,因此对模型预估性能要求比较高。


2.2 离线训练


离线数据方面,我们使用Spark生成TensorFlow[5]原生态的数据格式tfrecord,加快数据读取。

模型方面,使用经典的Wide and Deep模型,特征包括用户维度特征、场景维度特征、商品维度特征。Wide 部分有 80多特征输入,Deep部分有60多特征输入,经过Embedding输入层大约有600维度,之后是3层256等宽全连接,模型参数一共有35万参数,对应导出模型文件大小约11M。

离线训练方面,使用TensorFlow同步 + Backup Workers[6]的分布式框架,解决异步更新延迟和同步更新性能慢的问题。

在分布式ps参数分配方面,使用GreedyLoadBalancing方式,根据预估参数大小分配参数,取代Round Robin取模分配的方法,可以使各个PS负载均衡。

计算设备方面,我们发现只使用CPU而不使用GPU,训练速度会更快,这主要是因为尽管GPU计算上性能可能会提升,但是却增加了CPU与GPU之间数据传输的开销,当模型计算并不太复杂时,使用CPU效果会更好些。

同时我们使用了Estimator高级API,将数据读取、分布式训练、模型验证、TensorFlow Serving模型导出进行封装。

使用Estimator的主要好处在于:

  1. 单机训练与分布式训练可以很简单的切换,而且在使用不同设备:CPU、GPU、TPU时,无需修改过多的代码。

  2. Estimator的框架十分清晰,便于开发者之间的交流。

  3. 初学者还可以直接使用一些已经构建好的Estimator模型:DNN模型、XGBoost模型、线性模型等。


三、TensorFlow Serving及性能优化


3.1 TensorFlow Serving介绍


TensorFlow Serving是一个用于机器学习模型Serving的高性能开源库,它可以将训练好的机器学习模型部署到线上,使用gRPC作为接口接受外部调用。TensorFlow Serving支持模型热更新与自动模型版本管理,具有非常灵活的特点。

下图为TensorFlow Serving整个框架图。Client端会不断给Manager发送请求,Manager会根据版本管理策略管理模型更新,并将最新的模型计算结果返回给Client端。

图1. TensorFlow Serving架构,图片来源于TensorFlow Serving官方文档

美团内部由数据平台提供专门TensorFlow Serving通过YARN分布式地跑在集群上,其周期性地扫描HDFS路径来检查模型版本,并自动进行更新。当然,每一台本地机器都可以安装TensorFlow Serving进行试验。

在我们站外广告精排的场景下,每来一位用户时,线上请求端会把该用户和召回所得100个广告的所有信息,转化成模型输入格式,然后作为一个Batch发送给TensorFlow Serving,TensorFlow Serving接受请求后,经过计算得到CTR预估值,再返回给请求端。

部署TensorFlow Serving的第一版时,QPS大约200时,打包请求需要5ms,网络开销需要固定3ms左右,仅模型预估计算需要10ms,整个过程的TP50线大约18ms,性能完全达不到线上的要求。接下来详细介绍下我们性能优化的过程。

3.2 性能优化


3.2.1 请求端优化

线上请求端优化主要是对一百个广告进行并行处理,我们使用OpenMP多线程并行处理数据,将请求时间性能从5ms降低到2ms左右。

#pragma omp parallel for    
for (int i = 0; i < request->ad_feat_size(); ++i) {
    tensorflow::Example example;
    data_processing();
}


3.2.2 构建模型OPS优化

在没有进行优化之前,模型的输入是未进行处理的原格式数据,例如,渠道特征取值可能为:'渠道1'、'渠道2' 这样的string格式,然后在模型里面做One Hot处理。

最初模型使用了大量的高阶tf.feature_column对数据进行处理, 转为One Hot和embedding格式。 使用tf.feature_column的好处是,输入时不需要对原数据做任何处理,可以通过feature_column API在模型内部对特征做很多常用的处理,例如:tf.feature_column.bucketized_column可以做分桶,tf.feature_column.crossed_column可以对类别特征做特征交叉。但特征处理的压力就放在了模型里。

为了进一步分析使用feature_column的耗时,我们使用tf.profiler工具,对整个离线训练流程耗时做了分析。在Estimator框架下使用tf.profiler是非常方便的,只需加一行代码即可。

with tf.contrib.tfprof.ProfileContext(job_dir + ‘/tmp/train_dir’) as pctx:   
   estimator = tf.estimator.Estimator(model_fn=get_model_fn(job_dir),
                                      config=run_config,
                                      params=hparams)    

下图为使用tf.profiler,网络在向前传播的耗时分布图,可以看出使用feature_column API的特征处理耗费了很大时间。

图2. 优化前profiler记录, 前向传播的耗时占总训练时间55.78%,主要耗费在feature_column OPS对原始数据的预处理

为了解决特征在模型内做处理耗时大的问题,我们在处理离线数据时,把所有string格式的原生数据,提前做好One Hot的映射,并且把映射关系落到本地feature_index文件,进而供线上线下使用。这样就相当于把原本需要在模型端计算One Hot的过程省略掉,替代为使用词典做O(1)的查找。同时在构建模型时候,使用更多性能有保证的低阶API替代feature_column这样的高阶API。下图为性能优化后,前向传播耗时在整个训练流程的占比。可以看出,前向传播的耗时占比降低了很多。

图3. 优化后profiler记录,前向传播耗时占总训练时间39.53%


3.2.3 XLA,JIT编译优化

TensorFlow采用有向数据流图来表达整个计算过程,其中Node代表着操作(OPS),数据通过Tensor的方式来表达,不同Node间有向的边表示数据流动方向,整个图就是有向的数据流图。

XLA(Accelerated Linear Algebra)是一种专门对TensorFlow中线性代数运算进行优化的编译器,当打开JIT(Just In Time)编译模式时,便会使用XLA编译器。整个编译流程如下图所示:

图4. TensorFlow计算流程

首先TensorFlow整个计算图会经过优化,图中冗余的计算会被剪掉。HLO(High Level Optimizer)会将优化后的计算图生成HLO的原始操作,XLA编译器会对HLO的原始操作进行一些优化,最后交给LLVM IR,进而根据不同的后端设备,生成不同的机器代码。

JIT的使用,有助于LLVM IR根据 HLO原始操作生成更高效的机器码;同时,对于多个可融合的HLO原始操作,会融合成一个更加高效的计算操作。但是JIT的编译是在代码运行时进行编译,这也意味着运行代码时会有一部分额外的编译开销。

图5. 网络结构、Batch Size对JIT性能影响[7]

上图显示为不同网络结构,不同Batch Size下使用JIT编译后与不使用JIT编译的耗时之比。可以看出,较大的Batch Size性能优化比较明显,层数与神经元个数变化对JIT编译优化影响不大。

在实际的应用中,具体效果会因网络结构、模型参数、硬件设备等原因而异。

3.2.4 最终性能


经过上述一系列的性能优化,模型预估时间从开始的10ms降低到1.1ms,请求时间从5ms降到2ms。整个流程从打包发送请求到收到结果,耗时大约6ms。



图6. 模型计算时间相关参数:QPS:1308,50line:1.1ms,999line:3.0ms。下面四个图分别为:耗时分布图显示大部分耗时控制在1ms内;请求次数显示每分钟请求大约8万次,折合QPS为1308;平均耗时时间为1.1ms;成功率为100%


3.3 模型切换毛刺问题


通过监控发现,当模型进行更新时,会有大量的请求超时。如下图所示,每次更新都会导致有大量请求超时,对系统的影响较大。通过TensorFlow Serving日志和代码分析发现,超时问题主要源于两个方面,一方面,更新、加载模型和处理TensorFlow Serving请求的线程共用一个线程池,导致切换模型时无法处理请求;另一方面,模型加载后,计算图采用Lazy Initialization方式,导致第一次请求需要等待计算图初始化。


图7. 模型切换导致请求超时

问题一主要是因为加载和卸载模型线程池配置问题,在源代码中:

uint32 num_load_threads = 0; uint32 num_unload_threads = 0;

这两个参数默认为 0,表示不使用独立线程池,和Serving Manager在同一个线程中运行。修改成1便可以有效解决此问题。

模型加载的核心操作为RestoreOp,包括从存储读取模型文件、分配内存、查找对应的Variable等操作,其通过调用Session的run方法来执行。而默认情况下,一个进程内所有Session的运算均使用同一个线程池。所以导致模型加载过程中加载操作和处理Serving请求的运算使用同一线程池,导致Serving请求延迟。解决方法是通过配置文件设置,可构造多个线程池,模型加载时指定使用独立的线程池执行加载操作。

对于问题二,模型首次运行耗时较长的问题,采用在模型加载完成后提前进行一次Warm Up运算的方法,可以避免在请求时运算影响请求性能。这里使用Warm Up的方法是,根据导出模型时设置的Signature,拿出输入数据的类型,然后构造出假的输入数据来初始化模型。

通过上述两方面的优化,模型切换后请求延迟问题得到很好的解决。如下图所示,切换模型时毛刺由原来的84ms降低为4ms左右。

图8. 优化后模型切换后,毛刺降低


四、总结与展望

本文主要介绍了用户增长组基于Tensorflow Serving在深度学习线上预估的探索,对性能问题的定位、分析、解决;最终实现了高性能、稳定性强、支持各种深度学习模型的在线服务。

在具备完整的离线训练与在线预估框架基础之后,我们将会加快策略的快速迭代。在模型方面,我们可以快速尝试新的模型,尝试将强化学习与竞价结合;在性能方面,结合工程要求,我们会对TensorFlow的图优化、底层操作算子、操作融合等方面做进一步的探索;除此之外,TensorFlow Serving的预估功能可以用于模型分析,谷歌也基于此推出What-If-Tools来帮助模型开发者对模型深入分析。最后,我们也会结合模型分析,对数据、特征再做重新的审视。

参考文献


[1]  Cheng, H. T., Koc, L., Harmsen, J., Shaked, T., Chandra, T., Aradhye, H., … & Anil, R. (2016, September). Wide & deep learning for recommender systems. In Proceedings of the 1st Workshop on Deep Learning for Recommender Systems (pp. 7-10). ACM.

[2]  Wang, R., Fu, B., Fu, G., & Wang, M. (2017, August). Deep & cross network for ad click predictions. In Proceedings of the ADKDD'17 (p. 12). ACM. 

[3]  Guo, H., Tang, R., Ye, Y., Li, Z., & He, X. (2017). Deepfm: a factorization-machine based neural network for ctr prediction. arXiv preprint arXiv:1703.04247.

[4]  Lian, J., Zhou, X., Zhang, F., Chen, Z., Xie, X., & Sun, G. (2018). xDeepFM: Combining Explicit and Implicit Feature Interactions for Recommender Systems. arXiv preprint arXiv:1803.05170.

[5]  Abadi, M., Barham, P., Chen, J., Chen, Z., Davis, A., Dean, J., … & Kudlur, M. (2016, November). TensorFlow: a system for large-scale machine learning. In OSDI (Vol. 16, pp. 265-283).

[6]  Goyal, P., Dollár, P., Girshick, R., Noordhuis, P., Wesolowski, L., Kyrola, A., … & He, K. (2017). Accurate, large minibatch SGD: training imagenet in 1 hour. arXiv preprint arXiv:1706.02677.

[7]  Neill, R., Drebes, A., Pop, A. (2018). Performance Analysis of Just-in-Time Compilation for Training TensorFlow Multi-Layer Perceptrons.


作者简介

仲达,2017年毕业于美国罗彻斯特大学数据科学专业,后在加州湾区Stentor Technology Company工作,2018年加入美团,主要负责用户增长组深度学习、强化学习落地业务场景工作。

鸿杰,2015年加入美团点评。美团平台与酒旅事业群用户增长组算法负责人,曾就职于阿里,主要致力于通过机器学习提升美团点评平台的活跃用户数,作为技术负责人,主导了美团DSP广告投放、站内拉新等项目的算法工作,有效提升营销效率,降低营销成本。

廷稳,2015年加入美团点评。在美团点评离线计算方向先后从事YARN资源调度及GPU计算平台建设工作。

欢迎加入 美团机器学习群,跟作者零距离交流。如需进群,请加美美同学的微信(微信号: MTDPtech01),回复: TF,美美会自动拉你进群。


图像鉴黄做得好,健康上网少烦恼

$
0
0

引言

根据中国互联网络信息中心发布第 42 次《中国互联网络发展状况统计报告》,截至 2018 年 6 月底,中国网民数量已达 8.02 亿!平均每周上网 27.7 小时,出去每周睡眠时间(以 8 小时 / 天为例),现代人每天有近 1/4 的时间在拥抱网络。

随着科技的发展进步,互联网也成为人们日常生活和工作中离不开的工具,它在给人们带来生活方便、处理事务高效的同时,也会成为一些不法分子的有利工具,利用其传播和散延一些不良信息,如黄色图片、影视等,涉黄案件接踵而来,由此一来,「打黄」也显得尤为重要。

不同于文字鉴黄,图像鉴黄目前仍大量依赖人类鉴黄师,一方面存在审核标准的主观误差,另一方面也不利于鉴黄师这一职业人员的长期心理健康。随着人工智能浪潮的涌动,机器鉴黄领域也在不断呈现出令人耳目一新的硕果。由嵩恒网络与上海交通大学联合首创 local-context aware network(基于局部上下文感知深度神经网络)就带来了一种新的解决方案。

现有解决方案

目前,现有的敏感图像的鉴别技术方案主要分为两种。第一种是基于卷积神经网络 CNN(Convolution Neural Network)的敏感图像分类方法 [1]。作者直接将图像的像素信息分别输入到 AlexNet[2] 与 GoogLeNet[3] 中,基本保留了输入图像的所有信息,通过卷积、池化等操作对特征进行提取和高层抽象,并将两种网络输出图像识别的概率值加权求和来分类。CNN 作为一种端到端的学习方法,应用非常广泛。第二种是 CNN 全局图像分类与局部图像目标检测 Faster RCNN 相结合的敏感图像分类方法 [4]。在给定的图片中,Faster RCNN 可以精确地找到物体所在的位置,并标注物体的类别,即进行图像的识别与定位。作者将局部目标检测和全局特征相结合,进一步提升了敏感图像检测的正确率。

我们提出的解决方案

目前现有的技术无法解决图像中存在敏感区域大小各异情况下的分类问题。而且针对图像的分类没有把各个语境下的特征整合起来进行分类,需要分段训练各个语境的网络再拼接起来,训练过程繁琐。在局部敏感区域网络中还需要大量人力进行图像标注。

本文提供了一种对敏感图片进行鉴定的方法及其终端,该终端将敏感图片分为三个等级:色情、性感、正常;该终端主要由一个 ResNet(残差神经网络)和一个基于特征金字塔的多目标检测网络组合成的系统——LocoaNet,其完整结构如图 1 所示,

图 1 LocoaNet 的架构图

LocoaNet 分为三个部分,ResNet 作为骨干网络(Backbone),敏感身体区域检测网络 (SpNet) 以及全局分类网络 (GcNet)。选择 ResNet 作为骨干网络主要是由于它分类准确率高且计算速度快。其他一些诸如 VGG16,ResNet101 网络也可以作为骨干网络。

传统的全局分类网络应用在敏感图片识别任务的主要缺陷在于全局分类网络比较看重整体图像,易于在分类时过多的考虑背景图像。而对于一些有高鉴别力的局部区域(比如裸体,身体敏感区域)不太关注。而这些区域往往对敏感图像分类起决定性作用。因此,我们设计了敏感身体区域检测网络(SpNet)来使特征提取网络更加关注敏感身体区域,学习了具有强语义信息的多尺度特征表示。

SpNet 的设计使用了特征金字塔网络(FPN)[6] 与 RetinaNet[7]。在骨干网络 ResNet 生成的每一个不同分辨率的残差层 feature map 中引入了后一分辨率缩放两倍的 feature map 并做基于元素的相加操作。从而生成新的 feature map。通过这样的连接操作使生成的 feature map 都融合了不同分辨率、不同语义强度的特征,在不增加额外的计算量的情况下保证了每一层都有合适的分辨率以及强语义特征,提升物体检测的精度。在的 feature map 上进行核为,步长为的卷积而成。在上进行同样的卷积操作生成。之间加入了 ReLU 操作层。

对 P3 至 P7 的每一层 feature map,进行四层核为 3*3,filter 数量为 256 的卷积以及一层 ReLU 操作提取 feature map Mi, i∈[3,7]。Mi 上的每一个点为对应九个不同大小的 Anchor(锚点),与输入图像上的一个以该点为中心的九种尺寸的区域对应。SpNet 的主要目标为对每一个 Anchor 进行多目标检测,检测是否出现敏感身体部位。在此,所谓多目标检测中检测的是敏感图片中人体的一些关键部位,分为胸部(色情)、女性性器官(色情)、男性性器官(色情)、臀部(色情)、阴毛(色情)、胸部(性感)、臀部(性感)、背部(性感)、腿(性感)和上半身(性感)等十个特征部位。对 Mi 进行核为 3*3,filter 数量为 KA(K 为待检测的目标数量,A 为每个 Anchor 对应的尺寸数量,K=10, A=9)的卷积并进行 Sigmoid 操作,得到的 feature map 即为每个 Anchor 包含各个目标的概率。SpNet 可以对 C3 到 C5 特征提取层的参数进行调整,使分类网络 LocoaNet 更关注敏感区域,学习到更高鉴别力的特征。

GcNet 网络起到全局分类的作用,将图片分为正常、性感、色情三个类别中。GcNet 将骨干网络的最后一层 feature map 作为输入,通过五层卷积层生成 feature map。每层卷积后都应用 ReLU 操作进行线性整流。对进行全局均值池化后连接到一个输出为三单元的全连接层,对图像进行三分类。

由于包含分类网络和目标检测网络两种网络,LocoaNet 的训练采用多任务学习的方法。LocoaNet 的损失函数为 SpNet 损失函数和 GcNet 损失函数之和。SpNet 的损失函数使用了 focal loss[7],GcNet 的损失函数交叉熵代价函数 (cross-entropy loss)。骨干网络采用了在 ImageNet的预训练模型上进行 finetune。在实际使用过程中,不运算 SpNet 网络部分,仅计算 GcNet 部分进行图像分类,减少了计算复杂度。

除此之外,我们还使用了递进学习的策略使得 LocoaNet 能够快速的移植到其他数据集上进行训练,达到迁移学习的目的。目标检测网络的训练前期需要大量的样本目标框标注,消耗大量的人力。递进学习方法的引入可以让我们的模型在无样本框标注的数据集上进行训练。递进学习方法的过程如下:

  1. 步骤一: 在有敏感区域标注的数据集上训练 LocoaNet,同时更新骨干网络,SpNet 和 GcNet 的参数

  2. 步骤二: 在仅有类别标注的数据集上训练,固定 SpNet 的参数,仅更新骨干网络和 GcNet 的参数,最小化分类损失。

  3. 步骤三: 在上训练,固定 GcNet 参数,仅更新骨干网络和 SpNet 的参数,最小化目标检测损失。

  4. 重复步骤二和步骤三直到网络收敛

在本文中,我们设计了 LocoaNet,把局部敏感区域检测网络与全局分类网络相结合,采用了多任务学习策略,对敏感图片提取高鉴别力的特征,达到了很高的分类准确率。同时提出了递进学习策略提升网络对其他数据集的泛化能力。不仅如此,计算复杂度相比于现有设计更小。本发明在公开数据集 NPDI[8] 上达到了 92.2% 的三分类准确率,在 AIC(包含有类别标注的 150000 张图像和有敏感区域标注的 14000 张色情图像)上达到了 95.8% 的三分类准确率

论文:Adult Image Classification by a Local-Context Aware Network

论文地址:https://ieeexplore.ieee.org/document/8451366

摘要:在打造一个健康有序的网络环境的过程中,「鉴黄」已经成为一个重要命题。近年来,基于深度学习基础提出的解决方案已经帮助该领域取得了一定的突破,当在识别精准度等方面还有待进一步提升。本发明专利结合深度学习,建立了鉴定敏感图片的模型及其终端,一方面,为类似于鉴黄师等职位的人提高工作效率,另外一方面,通过自动化的手段在第一时间有效的制止了敏感图片在有些网站的流传。

参考文献:

[1] Moustafa, Mohamed. "Applying deep learning to classify pornographic images and videos." arXiv preprint arXiv:1511.08899 (2015).

[2] Krizhevsky, Alex, Ilya Sutskever, and Geoffrey E. Hinton. "Imagenet classification with deep convolutional neural networks." Advances in neural information processing systems. 2012.

[3] Szegedy, Christian, et al. "Going deeper with convolutions." Cvpr, 2015.

[4] Ren, Shaoqing, et al. "Faster r-cnn: Towards real-time object detection with region proposal networks." Advances in neural information processing systems. 2015.

[5] Ou, Xinyu, et al. "Adult Image and Video Recognition by a Deep Multicontext Network and Fine-to-Coarse Strategy." ACM Transactions on Intelligent Systems and Technology (TIST) 8.5 (2017): 68.

[6] Lin, Tsung-Yi, et al. "Feature pyramid networks for object detection." CVPR. Vol. 1. No. 2. 2017.

[7] Lin, Tsung-Yi, et al. "Focal loss for dense object detection." arXiv preprint arXiv:1708.02002 (2017).

[8] Sandra Avila, Nicolas Thome, Matthieu Cord, Eduardo Valle, Arnaldo de A. Araújo. Pooling in Image Representation: the Visual Codeword Point of View. Computer Vision and Image Understanding (CVIU), volume 117, issue 5, p. 453-465, 2013.

你是怎么变自律的? - 知乎

$
0
0

谢邀。有的时候,我们明知道某些东西在长远来说对我们是有益的, 却宁愿选择一时的欢愉。自律确实是很难的,是一种需要长时间训练自己、刻意要求自己才能得到的品质。然而,这并不是说我们完全做不到。

在学会自律的办法之前,我们首先要明白自律的意义。有一个心理学上的实验,也许能够让我们更明白自律到底包含了什么能力。

棉花糖实验(The Marshmallow Study)

五十年前,一位斯坦福的教授Walter Mischel召集了600多名四到五岁的孩子参与一个简单的实验。每一个孩子都会进入一个空房间,房间中央的桌子上放着一个棉花糖。研究人员这时会给孩子一个选择的机会: 研究员将会暂时离开房间十五分钟,如果孩子们在此期间没有吃这个棉花糖,他们将会得到另一个棉花糖作为奖励,或者他们也可以现在就吃掉,只是不会有第二个棉花糖送给他们(Walter,Ebbeson & Raskoff Zeiss,1972)。


让一个四五岁的孩子等十五分钟的棉花糖,他们内心的煎熬简直等同于我们成年人两个小时不看手机。

有的孩子等研究员一走就把棉花糖吃掉了;

有的孩子捂着眼睛不去看棉花糖,假装面前的棉花糖不存在;

有的孩子嗅棉花糖的香味,短暂地平息自己对棉花糖的渴望。

三分之二的孩子,无论中途等待了多久,最终都选择把棉花糖吃掉,只有剩下不到三分之一成功等到研究员回来。

这个实验乍一看平白无奇, 但它的特别在多年后才显露出来:追踪实验显示,那些能够抵抗住棉花糖诱惑的孩子在未来有更高的美国高考分数,药物成瘾或肥胖的几率更低,面对压力有更好的心理承受力,有更好的社交能力等。

总体来说,得到第二个棉花糖的孩子,要比直接享用第一个棉花糖的孩子, 在一系列生活指标中得分更高(Walter, Shoda & Peake, 1988; Walter, Shoda & Rodriguez,1989)。

但是六百多名孩子中, 只有三分之一做到了自律,自律对大多数人来说,到底为什么那么困难?


自律为什么这么难?

自律对于我们来说, 有一个未来自我与现在自我之间的冲突(Ariely,2002;Wertenbroch,1998)。

举一个例子。当我们想要减肥的时候,会指定一个一段时间的节食计划,在这个计划截止的那天,未来的我们理应拥有比现在更好的体型。 但是现在我们遇到了火锅、可乐、芝士蛋糕、炸鸡等等的诱惑,我们还是有可能放弃我们的节食计划开始享用这些美食。

有一个苗条的体型对我们很重要,而会给我们一时的美食也许并不是很重要,毕竟我们可以等到瘦下来了再享用它们。然而,苗条的体型不是我们现在能得到的,需要很长时间的锻炼和克制,这样一来它对我们的重要性就变得很小。

同时美食作为现在能享用的,变成我们此时此刻生活的中心,对现下的自己有更大的影响力。 即时的奖励相对于延迟的奖励来说,会对我们的偏好产生比例失调的影响,促使我们更倾向于选择能够让我们即时满足的事物。


学习亦是同理。考上一个好大学固然很重要,但是在时间的作用下,那些使我们从学习中分心的事物,比如手机、电脑、各种杂事等,就会影响我们的行为。


我们该如何变得自律?

就孩子的表现而言,他们运用了两个粗浅但有效的策略。 一个是将遥远的奖励放到现在,二是远离诱惑物。

前者,孩子们会隔一段时间闻一下棉花糖的香味或是舔一下棉花糖的表面,他们将时间分段,每撑过一段时间就奖励自己一点。后者,孩子们会捂上眼装作眼前的棉花糖不存在,或是玩弄自己的衣角转移自己的注意力。

由此,心理学家们也总结出几点自律的办法:

首先,将遥远的奖励放到现在

1)明确目标(Clarify your goal)


在一切行动开始之前,我们需要明确自己的目标。我们对目标的认知是否清楚,取决了未来我们是否能在未来自我与现在自我起冲突时,能够进行权衡和思考,做出正确的选择。因此,我们需要仔细思考并了解自己,这是自律训练的第一步。

在确立目标的阶段,了解自己的长处与弱点,力所能及也很重要。例如,我想要考一个好大学,但我知道自己不擅长语文中文言文或写作的部分,但是数学和物理不错,总得来说偏向理科。因此,我可以设定目标为重点突击文言文和写作,数学和物理可以凭靠平时的积累来保证自己的水平。由此一来,数学和物理就能弥补语文的分差,而突击训练也会保证语文的分数不会与其他科落差太大。


2)细化目标(Break it down)

将目标拆分成具体的执行方式,通过完成每一个分期目标,能够帮助我们赢取长远目标。

另外,可视化目标(visualize yourgoal)也有利于我们训练自己的自律能力。 我们通过想象的方式,将完成分期目标的每一个方式、步骤,而不仅仅是行动结果,在脑海中进行“可视化”。在此过程中,我们也会对每一种实现方式的重要性有更清晰的认知。


3)做出行动

在第二步骤中,我们将目标拆分成具体的任务,那么接下来就是做出行动的时候了。尽管这个步骤无需过多的解释,但在实际生活中,这却往往是需要最多努力和自律的一步。


4)为每一小步庆祝(Celebrate)

在完成每一个分期任务之后, 我们需要庆祝自己取得的(哪怕只是小小一个)成就。一方面,庆祝作为一种完成任务后的仪式,是一种“延迟的满足”,这本身就是“自律”的一种培养和体现,同时也是对所付出努力的一种自我肯定(self-affirmation)。


其次,我们需要远离诱惑物

1)远离会让自己分心的事物


让那些诱惑自己从目标上分心的事物从视野中消失是最直接的办法。比如,卸载微博,手机关机,把手机电脑锁进柜子里。警戒自己不去碰那些有诱惑力的事物,人是有对自己的行为进行约束,或提前对未来可能做出影响“正途”的行为进行截断或禁止的能力的(Wertenbroch,1998)。


2)规划时间


当我们实在抵挡不了诱惑物对我们的影响,或是诱惑物对我们来说也是通向目标需要的辅助物时,我们还有个备用计划,那就是规划时间。比如,规划一个时间表,完成三个小时的学习任务后,可以玩一个小时的手机。

这样一来,不仅可以满足我们对诱惑物的渴望,也可以作为庆祝的一种培养我们的“自律”。但是,能够严格按照时间表来完成,也需要我们自身的意志力。

3)不要在家学习/工作

很多时候,环境会影响我们的学习/工作状态(Stefanie,2016)。 家作为一个非常放松的环境,通常和舒适划上等号,同时家也是一个充满了诱惑物的地方。在这样的氛围下,人很难进入专心学习/工作的状态。所以,当我们需要学习的时候,选择一个合适的环境也是非常重要的。比如说,图书馆就是一个非常适合学习的地方,不仅安静且诱惑物稀少,图书馆学习的人也会激发自己想要学习的动机。


自律是件很困难的事情,达成这样的品质需要不断地练习与锻炼。当发现自己做不到自律,或者达不到你自己设立的标准时,不要气馁。 会被即时的诱惑吸引是人之常情。但是,自律中最重要的,是能否在出现问题时,也能及时整理心情,坚持到最后。

以上。

References:

Ariely, D. & Wertenbroch, K. (2002). Procrastination, Deadlines, and Performance: Self-Control by Precommitment. Psychological Science, 13, 219-224.

Stefanie, Buck. (2016). In Their Own Voices: Study Habits of Distance Education Students. Journal of Library & Information Services in Distance Learning, 10, 137-143.

Tessina, T.B. (2009). The Ten Smartest Decisions a Woman Can Make After Forty.

Walter, M., Ebbeson, E. B., & Raskoff Zeiss, A. (1972). Cognitive and attentional mechanisms in delay of gratification. Journal of Personality and Social Psychology, 21, 204-218.

Walter, M., Shoda, Y., & Peake, PK. (1988). The nature of adolescent competencies predicted by preschool delay of gratification. Journal of Personality and Social Psychology, 54, 687-696.

Walter, M., Shoda, Y. & Rodriguez, MI. (1989)Delay of gratification in children. Science, 244, 933-938.

Wertenbroch, K. (1998). Consumption self-control by rationing purchase quantities ot virtue and vice. Marketing Science, 17, 317-337


想好好恋爱,收获平等且长久的关系,请戳私家课(限时半价中): 好好谈恋爱私家课程


点击查看过往高赞回答:
最理性的暗恋是什么样子的?

有哪些常人不知道的「常识」?

人在迷茫时该干什么?

戳此免费领取:心理学习资料包

基于深度学习的智能问答-博客-云栖社区-阿里云

$
0
0

作者:周小强 陈清财 曾华军


1引言

纵观自动问答系统的技术发展历史,从1950年代因图灵测试而诞生至今,已经有几十年的历史。但真正在产业界得到大家的广泛关注,则得益于2011年Siri和Watson成功所带来的示范效应。自此,自动问答系统较以往任何时候都显得离实际应用更近。这一方面归功于机器学习与自然语言处理技术的长足进步,另一方面得益于维基百科等大规模知识库以及海量网络信息的出现。然而,现有的自动问答系统所面临的问题远没有完全解决。事实上,无论是业界应用还是学术研究,问句的真实意图分析、问句与答案之间的匹配关系判别仍然是制约自动问答系统性能的两个关键难题。


2 问答系统概述

问答系统能够更为准确地理解以自然语言形式描述的用户提问,并通过检索异构语料库或问答知识库返回简洁、精确的匹配答案。相对于搜索引擎,问答系统能更好地理解用户提问的真实意图, 同时更有效地满足用户的信息需求。


2.1问答系统的发展历程

问答系统最早的实现构想可以追溯到图灵测试。为了测试机器是否具备人类智能,图灵测试要求电脑能在5分钟内回答由人类测试者提出的一系列问题,且其达到超过30%的回答让测试者误认为是人类所答。随着人工智能、自然语言处理等相关技术的发展,针对不同的数据形态的变化也衍生出不同种类的问答系统。早期由于智能技术和领域数据规模的局限性,问答系统主要是面向限定领域的AI系统或专家系统,例如STUDENT[1]、LUNAR[2]系统。该时期的问答系统处理的数据类型主要是结构化数据,系统一般是将输入问题转化为数据库查询语句,然后进行数据库检索反馈答案。随着互联网的飞速发展以及自然语言处理技术的兴起,问答系统进入了面向开放领域、基于自由文本数据的发展时期,例如英文问答式检索系统Ask Jeeves (http://www.ask.com)、START (http://start.csail.mit.edu)。这种问答系统的处理流程主要包括:问题分析、文档及段落检索、候选答案抽取、答案验证。特别自1999年文本检索会议(Text Retrieval Conference,简称TREC)引入问答系统评测专项(Question Answering Track,简称QA Track)以来,极大推动了基于自然语言处理技术在问答领域中的研究发展。随后网络上出现的社区问答(community question answering, CQA)提供了大规模的用户交互衍生的问题答案对(question-answer pair, QA pair)数据,这为基于问答对的问答系统提供了稳定可靠的问答数据来源。随着苹果公司Siri系统的问世,问答系统进入了智能交互式问答的发展阶段,这种形式的问答系统能够让用户体验更为自然的人机交互过程,并且也使信息服务的相关应用更为方便可行。


问答系统处理的数据对象主要包括用户问题和答案。依据用户问题的所属数据领域,问答系统可分为面向限定域的问答系统、面向开放域的问答系统、以及面向常用问题集(frequent asked questions, FAQ)的问答系统。依据答案的不同数据来源,问答系统可划分为基于结构化数据的问答系统、基于自由文本的问答系统、以及基于问答对的问答系统。此外,按照答案的生成反馈机制划分,问答系统可以分为基于检索式的问答系统和基于生成式的问答系统。本文主要阐述基于检索式的问答系统的处理框架和相关研究。


2.2 问答系统的处理框架

不同类型的问答系统对于数据处理的方法存在不同。例如,相对于面向FAQ的问答系统的问句检索直接得到候选答案,面向开放领域的问答系统首先需要根据问题分析的结果进行相关文档、文本片段信息的检索,然后进行候选答案的抽取。虽然不同类型的问答系统对于系统模块的功能分工和具体实现存在差异,但依据数据流在问答系统中的处理流程,一般问答系统的处理框架中都包括问句理解、信息检索、答案生成三个功能组成部分,如图2.1所示。


21769f02eefc709bf648dc9b0f3deee8d648dc92 


2.2.1 问句理解

问句理解是问答系统理解用户意图的关键一环,问句理解模块的性能直接制约着后续处理模块的效果。用户意图是一个抽象的概念,要想作为答案检索的依据,需要把它转换成机器可以理解的形式。用户的检索意图导致信息需求的产生,因此,研究中往往将信息需求作为用户意图的代表,根据问句的语义结构,可以从问题类别和问题内容两方面来表示。通常采用自然语言技术对问题进行深层次的理解,包括命名实体识别、依存句法分析、词义消歧等。

问句理解主要包括问句分类、主题焦点提取、问题扩展处理。问句分类是将用户提问归入不同的类别,使系统能够针对不同问题类型采用不同的答案反馈机制得到候选答案集合。问答系统通常使用机器学习算法训练问题分类器[3,4]来实现用户提问的分类。主题焦点提取主要完成用户问题的信息需求的精确定位,其中主题表示问句的主要背景或者用户的感兴趣的对象,焦点则是用户询问的有关主题的内容,通常是问句话题的相关信息或对话题起到描述性的作用,比如属性、动作、实例等等。问题扩展是将用户在提问中没有充分表达的意思补充出来,对问题中潜在的信息显化出来,从而提高答案检索的召回率。


2.2.2 信息检索

根据问句理解得到的查询表示,信息检索模块负责从异构语料库、问答知识库中检索相关信息,传递给后续的答案生成处理模块。对于基于不同的问答系统,系统的检索模型以及检索数据形式也不同。对于基于自由文本数据的问答系统,信息检索过程是一个逐渐缩小答案范围的过滤过程,主要包括文档检索和段落句群检索。对于基于问句答案对的问答系统,信息检索处理是通过问句检索得到与用户提问相似的候选问句,返回对应的候选答案列表。


首先,文档检索是根据问题理解的结果检索用户提问的相关文档集合。最简单的方法是直接用已有的检索系统(如Smart,Lucene等)对问题的非停用词进行全文索引,直接检索得到用户提问的相关文档集合,但是这种方法很难获得好的效果。通常问答系统中的文档检索模型包括布尔模型、向量空间模型、语言模型、概率模型等。布尔模型是最简单的一种检索模型,它把关键词组织成一个布尔表达式,使得文档中出现的关键词需要满足这个布尔表达式。向量空间模型把文档和查询都表示成向量,根据查询和文档对应向量的相似度(通常是两个向量夹角的余弦值)对文档进行排序。概率模型估计计算文档和查询相关的概率,并按照相关性概率对文档进行排序。语言模型是把查询和文档分别表示成语言模型,通过两个语言模型之间的KL距离来估计两者的相似度。其次,段落句群检索就是从候选文档集合中检索出最有可能含有答案的段落(自然段落或者文档片段),进一步过滤噪声信息,得到更为精确的答案相关信息。广泛使用的段落检索算法有三个:MultiText算法[6]、IBM 的算法[7,8]和SiteQ算法[9]。Tellex[10]等人的实验结果表明基于密度的算法可以获得相对较好的效果。所谓基于密度的算法, 就是通过考虑查询关键词在段落中的出现次数和接近程度来决定这个段落的相关性。相比之下,Cui[5]提出的检索算法通过把问句和答案都解析成语法树,从两者语法树的结构中找出一些相关性的信息。


问句检索的主要问题在于如何缩小用户提问与知识库中问句之间的语义鸿沟。近几年,研究人员采用基于翻模模型的方法计算从用户提问“翻译”到检索问句的翻译概率,从而实现相似性问句检索。例如,算法[11-14]都是把两个问句看作是不同表达方式的语句,计算两个问句之间的翻译概率。为了计算这种翻译的概率,就必须估计词与词之间进行翻译的概率。这种方法首先需要通过答案相似度计算得到同义或近义的问答对集合,该集合中的相似问题集合就构成了一个估计翻译概率的训练集,类似于机器翻译中多语言平行语料库。实验证明,这样做的效果会比语言模型,Okapi BM25和空间向量模型都好。


2.2.3 答案生成

基于信息检索得到的检索信息,答案生成模块主要实现候选答案的抽取和答案的置信度计算,最终返回简洁性、正确性的答案。按照答案信息粒度,候选答案抽取可以分为段落答案抽取、句子答案抽取、词汇短语答案抽取。段落答案抽取是将一个问题的多个相关答案信息进行汇总、压缩,整理出一个完整简洁的答案。句子答案抽取是将候选答案信息进行提纯,通过匹配计算过滤表面相关,实际语义不匹配的错误答案。词汇短语抽取是采用语言的深层结构分析技术从候选答案中准确地提取答案词或短语。


答案置信度计算是将问题与候选答案进行句法和语义层面上的验证处理,从而保证返回答案是与用户提问最为匹配的结果。应用最广泛是基于统计机器学习的置信度计算方法。这种方法通常定义一系列词法、句法、语义以及其他相关特征(如编辑距离、BM25等)来表示问题与候选答案之间的匹配关系,并使用分类器的分类置信度作为答案的置信度。例如IBM Waston中使用的答案融合和特征排序方法[15],以及基于关系主题空间特征的多核SVM分类方法[16]。近几年,基于自然语言处理的问答匹配验证通常是使用句子的浅层分析获得句子的浅层句法语法信息,然后将问句与答案的句法树(短语句法树或依存句法树)进行相似性计算[17-20]。然而,问答系统的答案正确性更需满足问题和答案之间的语义匹配,比如问“苹果6s plus最新活动价多少”,如果回答“红富士苹果降到了12元”,就属于所答非所问。常用的方法是通过引入诸如语义词典(WordNet),语义知识库(Freebase)等外部语义资源进行问答语义匹配建模[21-23],以此提高问句答案间的语义匹配计算性能。


传统问答系统中构建的机器学习模型基本属于浅层模型。譬如,问句分类过程中常用的基于支持向量机(SVM)的分类模型[24],答案抽取使用的基于条件随机场(CRF)的序列标注模型[25],以及候选答案验证过程中使用的基于逻辑回归(LR)的问答匹配模型[26]等。这种基于浅层模型研发的问答系统往往存在人工依赖性高,并且缺少对不同领域数据处理的泛化能力。人工依赖性主要表现在浅层模型的特征工程上,由于浅层模型缺乏对数据的表示学习的能力,于是在面对不同领域的问答数据以及不同的问答任务的情况下,研究人员不得不进行针对性的数据标注,并且需要依据研究人员的观察和经验来提取模型所需的有效特征,这也就造成了此类问答系统可移植性低的结果。


3 基于深度学习的相关问答技术

近年来,深度神经网络在诸如图像分类、语音识别等任务上被深入探索并取得了突出的效果,表现出了优异的表示学习能力。与此同时,通过深度神经网络对语言学习表示已逐渐成为一个新的研究趋势。然而,由于人类语言的灵活多变以及语义信息的复杂抽象,使得深度神经网络模型在语言表示学习上的应用面临比在图像、语音更大的挑战。其一,相比于语音和图像,语言是非自然信号,完全是人类文明进程中,由大脑产生和处理的符号系统,是人类文明智慧的高度体现。语言的变化性和灵活度远远超过图像和语音信号。其二,图像和语音具有明确的数学表示,例如灰度图像为数学上的数值矩阵,而且其表示的最小粒度元素都有确定的物理意义,图像像素的每个点的值表示一定的灰度色彩值。相比而言,以往的词袋表示方法会导致语言表示存在维数灾难、高度稀疏以及语义信息损失的问题。


当前,研究人员越来越对深度学习模型在NLP领域的应用感兴趣,其主要集中在对词语、句子和篇章的表示学习以及相关应用。例如,Bengio等使用神经网络模型得到一种名为词嵌入(Word Embedding)或词向量的新型向量表示[27],这种向量是一种低维、稠密、连续的向量表示,同时包含了词的语义以及语法信息。当前,基于神经网络的自然语言处理方法大都是基于词向量的表示基础上进行的。在此基础上,相关研究人员设计深度神经网络模型学习句子的向量表示,相关工作包括递归神经网络(Recursive Neural Network)、循环神经网络(Recurrent Neural Network,RNN)、卷积神经网络(Convolutional Neural Network, CNN)的句子建模[28-30]。句子表示被应用于大量的自然语言处理任务上,并在一些任务上取得了较为突出的效果。例如机器翻译[31, 32]、情感分析等[33, 34]。从句子的表示到篇章的表示学习仍然较为困难, 相关工作也较少,比较有代表性是Li等人通过层次循环神经网络对篇章进行编码,然后通过层次循环神经网络进行解码,从而实现对篇章的表示[35]。然而,NLP领域涵盖了不同性质, 不同层次的具体问题,这就需要针对不同问题的特点,设计深度模型学习到任务特定的本质特征。


问答领域所需解决的两个关键问题:一是如何实现问句及答案的语义表示。无论是对于用户提问的理解,还是答案的抽取验证,都需抽象出问题和答案的本质信息的表示。这不仅需要表示问答语句的句法语法信息,更需表示问句及答案在语义层面上的用户意图信息和语义层匹配信息。二是如何实现问句答案间的语义匹配。为了保证反馈用户提问的答案满足严格语义匹配,系统必须合理利用语句高层抽象的语义表示去捕捉到两个文本之间关键而细致的语义匹配模式。鉴于近几年卷积神经网络(CNN)和循环神经网络(RNN)在NLP领域任务中表现出来的语言表示能力,越来越多的研究人员尝试深度学习的方法完成问答领域的关键任务。例如问题分类(question classification),答案选择(answer selection),答案自动生成(answer generation)。此外,互联网用户为了交流信息而产生的大规模诸如微博回复、社区问答对的自然标注数据[50],给训练深度神经网络模型提供了可靠的数据资源,并很大程度上解决自动问答研究领域的数据匮乏问题。


接下来内容安排:首先,分别介绍基于CNN和RNN的问答语句的语义表示方法;然后,介绍基于DCNN的两种语义匹配架构;最后,介绍基于RNN的答案自动生成方法。


3.1 基于深度神经网络的语义表示方法

3.1.1 基于卷积神经网络(CNN)的语义表示方法

基于CNN的语义表示学习是通过CNN对句子进行扫描,抽取特征,选择特征,最后组合成句子的表示向量。首先从左到右用一个滑动窗口对句子进行扫描,每个滑动窗口内有多个单词,每个单词由一个向量表示。在滑动窗口内,通过卷积(convolution)操作,进行特征抽取。这样,在各个位置上得到一系列特征。之后再通过最大池化(max pooling)操作,对特征进行选择。重复以上操作多次,得到多个向量表示,将这些向量连接起来得到整个句子的语义表示。如图3.1所示,基于CNN的句子建模的输入是词向量矩阵,矩阵的每一行的多个点的值在一起才有明确的物理意义,其代表句子中对应的一个词。词向量矩阵是通过将句子中的词转换为对应的词向量,然后按照词的顺序排列得到。该模型通过多层交叠的卷积和最大池化操作,最终将句子表示为一个固定长度的向量。该架构可以通过在模型顶层增加一个分类器用于多种有监督的自然语言处理任务上。

47e1ee4242936d0991f2fddb4626bba9175ae516 

图3.1 基于CNN的句子建模


基于CNN的句子建模可以表现为具有局部选择功能的“组合算子”,随着模型层次的不断加深,模型得到的表示输出能够覆盖的句内词的范围越广,最后通过多层的运算得到固定维度的句子表示向量。该过程的功能与“递归自动编码”的循环操作机制[33]具有一定的功能类似。对于只使用了一层卷积操作和一层全局最大池化操作的句子建模,称之为浅层卷积神经网络模型,这种模型被广泛应用于自然语言处理中句子级分类任务上,如句子分类[36], 关系分类[37]。但是,浅层的卷积神经网络模型不能对句子中复杂的局部语义关系进行建模,也不能对句子中深层次的语义组合进行很好的表示,并且全局最大池化操作丢失了句子中的词序特征,所以浅层的卷积网络模型只能对语句间的局部特征匹配进行建模。面对问答中复杂多样化的自然语言表示形式(如多语同现,异构信息,表情符号等),问答匹配模型[38-40]往往使用深层卷积神经网络(DCNN)来完成问句和答案的句子建模,并将高层输出的问答语义表示传递给多层感知器(MLP)进行问答匹配。


面对开放领域中的关系性推理问题,例如“微软公司的创始人是谁?”,往往通过引入外部语义知识推理得到问题的答案,此时单一的句子建模很难实现逻辑关系的语义表示。通常先需要对问题进行语义解析(Semantic Parse),然后针对问句实体、实体关系等不同类型的语义信息进行表示学习。Yih将关系性问题拆分成实体集合和关系模板[41],其中实体集合为问题中连续词语的子序列,关系模板为问句实体被特殊符号替换后的句子,针对实体集合和关系模板分别使用CNN进行句子建模,从而实现问句在实体及关系两个层面上的语义表示。Dong提出多栏(Multi-Column)卷积神经网络模型[42]对关系推理性问题进行不同层面(词语表达层、实体关系层、语境信息层)的语义表示学习,并实现从关系知识库中抽取候选答案的多层面语义信息,最后与候选答案进行多层次匹配打分。


3.1.2 基于循环神经网络(RNN)的语义表示方法

基于RNN的句子建模是把一句话看成单词的序列,每个单词由一个向量表示,每一个位置上有一个中间表示,由向量组成,表示从句首到这个位置的语义。这里假设,每一个位置的中间表示由当前位置的单词向量以及前一个位置的中间表示决定,通过一个循环神经网络模型化。RNN把句末的中间表示当作整个句子的语义表示,如图3.2所示。

922ddb6e0bafcf4fab21a3ff8aa6fac8b5a1af35 

图3.2 基于RNN的语句建模


RNN与隐马尔可夫模型有相似的结构,但是具有更强的表达能力,中间表示没有马尔可夫假设,而且模型是非线性的。然而,随着序列长度的增加,RNN在训练的过程中存在梯度消失(Vanishing gradient problem)的问题[43]。为了解决这个问题,研究人员对循环神经网络中的循环计算单元进行改善设计,提出了不同的变形,如常用的长短记忆(Long Short Term Memory, LSTM)[44, 45]和门控循环单元(Gated Recurrent Unit, GRU)[56]。这两种RNN可以处理远距离依存关系,能够更好地表示整句的语义。Wang和Nyberg [47]通过双向LSTM学习问题答案对的语义表示,并将得到的表示输入到分类器计算分类置信度。


此外,对于近几年的看图回答的任务(Image QA),研究人员通过整合CNN和RNN完成问题的图像场景下的语义表示学习。基本想法:模型在RNN对问句进行词语序列扫描的过程中,使用基于深度学习的联合学习机制完成“图文并茂”的联合学习,从而实现图像场景下的问句建模,用于最终的问答匹配。例如,Malinowski等人[48]提出的学习模型在RNN遍历问句词语的过程中,直接将CNN得到的图像表示与当前词语位置的词向量作为RNN学习当前中间表示的输入信息,从而实现图像与问句的联合学习。相比之下,Gao等人[49]则是先用RNN完成问题的句子建模,然后在答案生成的过程中,将问句的语义表示向量和CNN得到的图像表示向量都作为生成答案的场景信息。


3.2 基于DCNN的语义匹配架构

问答系统中的语义匹配涉及到主要功能模块包括:问句检索,即问句的复述检测(paraphrase);答案抽取,即问句与候选文本语句的匹配计算;答案置信度排序,即问题与候选答案间的语义匹配打分。


3.2.1 并列匹配架构

第一种基于DCNN的语义匹配架构为并列匹配 [38-40]架构。这种架构的匹配模型分别将两句话输入到两个CNN句子模型,可以得到它们的语义表示(实数值向量)。之后,再将这两个语义表示输入到一个多层神经网络,判断两句话语义的匹配程度,从而判断给定的两句话和是否可以成为一对句子匹配对(问答对)。这就是基于DCNN的并列语义匹配模型的基本想法。如果有大量的信息和回复对的数据,就可以训练这个模型。

f6569854b61005de635f9df30b89e0ebffcad9ce 

图3.3 基于DCNN的并列匹配架构


从图3.3所示的并列匹配架构可以看出,这种匹配模型的特点是两个句子的表示分别通过两个独立的卷积神经网络(CNN)得到,在得到它们各自的表示之前,两个句子间的信息互不影响。这种模型是对两个需要匹配的句子从全局语义上进行匹配,但是忽略了两个句子间更为精细的局部匹配特征。然而,在语句匹配的相关问题中,两个待匹配的句子中往往存在相互间的局部匹配,例如问题答案对:


Sx: 好饿啊,今天去哪里吃饭呢。

Sy: 听说肯德基最近出了新品,要不要去尝尝。


在这一问答对中,“吃饭”和“肯德基”之间具有较强的相关性匹配关系,而并列匹配则是对句子两个句子全局的表示上进行匹配,在得到整个句子的表示之前,“吃饭”和“肯德基”之间并不会互相影响,然而,随着深度卷积句子模型对句子的表示层次不断深入,而句子中的细节信息会部分丢失,而更关注整个句子的整体语义信息。


3.2.2 交互匹配架构

第二种基于DCNN的语义匹配架构为交互匹配[39]架构。与并列匹配不同,交互匹配的基本想法是直接对两个句子的匹配模式进行学习,在模型的不同深度对两个句子间不同粒度的局部之间进行交互,学习得到句子匹配在不同层次上的表示,最终得到句子对固定维度的匹配表示,并对匹配表示进行打分。

4e134fd447d4980739056526a8a03dff29abd58e 

图3.4 基于DCNN的交互匹配架构


如图3.4所示,交互匹配架构在第一层通过两个句子间的滑动窗口的卷积匹配操作直接得到了两个句子间较为底层的局部匹配表示,并且在后续的高层学习中采用类似于图像领域处理过程中的二维卷积操作和二维局部最大池化操作,从而学到问句与答案句子之间的高层匹配表示。通过这种形式,使得匹配模型既能对两个句子的局部之间的匹配关系进行丰富建模,也使模型能够对每个句子内的信息进行建模。很显然,交互匹配学习得到的结果向量不仅包含来自两个句子的滑动窗口的位置信息,同时具有两个滑动窗口的匹配表示。


对于问答的语义匹配,交互匹配可以充分考虑到问句与答案间的内部匹配关系,并通过二维的卷积操作与二维局部最大池化操作学习得到问句与答案间的匹配表示向量。在整个过程中,交互匹配更为关注句子间的匹配关系,对两个句子进行更为细致的匹配。


相比于并列匹配,交互匹配不仅考虑到单个句子中滑动窗口内的词的组合质量,而且同时考虑到来自两个句子组合间的匹配关系的质量。并列匹配的优势在于匹配过程中可以很好的保持两个句子各自的词序信息,因为并列匹配是分别对两个句子在顺序的滑动窗口上进行建模。相对而言,交互匹配的问答匹配过程是学习语句间局部信息的交互模式。此外,由于交互匹配的局部卷积运算和局部最大池化操作都不改变两个句子的局部匹配表示的整体顺序,所以交互匹配模型同样可以保持问句与答案的词序信息。总之,交互匹配通过对问句与答案的匹配模式进行建模,可以学习到两个句子间的局部匹配模式,而这种匹配模式在正常顺序的句子中具备很大的学习价值。


3.3基于RNN的答案自动生成方法

与基于检索式的回复机制对比而言,基于生成式的答案反馈机制是根据当前用户输入信息自动生成由词语序列组成的答案,而非通过检索知识库中用户编辑产生答案语句。这种机制主要是利用大量交互数据对构建自然语言生成模型,给定一个信息,系统能够自动生成一个自然语言表示的回复。其中的关键问题是如何实现这个语言生成模型。


答案自动生成需要解决两个重要问题,其一是句子表示,其二是语言生成。近年来,循环神经网络在语言的表示以及生成方面都表现出了优异的性能,尤其是基于循环神经网络的编码-解码架构在机器翻译[31, 32]和自动文摘[51]任务上取得了突破。Shang[52]等人基于CRU(Gated Recurrent Unit, GRU)[46]循环神经网络的编码-解码框架,提出了完全基于神经网络的对话模型“神经响应机”(Neural Responding Machine,NRM),该模型用于实现人机之间的单轮对话(single-turn dialog)。NRM是从大规模的信息对(问题-答案对,微博-回复对)学习人的回复模式,并将学到的模式存于系统的近四百万的模型参数中,即学习得到一个自然语言生成模型。


如图3.5所示,NRM的基本想法是将输入的一句话看作一个单词表示的序列,通过编码器(Encoder),即一个RNN模型,将转换成一个中间表示的序列,再通过解码器(Decoder),是另一个RNN模型,将转换成一个单词的系列,作为一句话输出。由于NRM在编码部分采用一种混合机制,从而使编码得到中间表示的序列不仅能够实现用户语句信息的整体把握,同时还能充分保留句子的细节信息。并且在解码部分采用了注意力(attention)机制[31],从而使生成模型可以相对容易的掌握问答过程中的复杂交互模式。[52]中的实验结果表明基于生成式的问答机制与基于检索式的答案反馈机制各具特点:在表达形式个性化的微博数据上,生成式比检索式的准确率会高一些,检索系统的准确率是70%,生成系统的准确率是76%。但是,生成式得到的答案会出现语法不通,连贯性差的问题,而检索式的答案来源于真实的微博用户编辑,所以语句的表述更为合理可靠。

a6cd1208668cf92843df7520373e12ddba2c2ebb 

图3.5 基于编码-解码结构的答案生成模型


目前,NRM以及Google的Neural Conversational Model(NCM)[53]主要还是在对复杂语言模式记忆和组合上层面上实现语言生成,尚无法在交互过程使用外界的知识。例如,在对“五一期间杭州西湖相比去年怎么样吗?”这样的句子,无法给出真实的状况(旅游人数的对比结果)相关的回复。虽然如此,但是NRM和NCM的真正意义在于初步实现了类人的语言自动反馈,因为此前的近几十年,研究人员不懈努力而生成的问答或对话系统(dialogue model),大都是基于规则和模板,或者是在一个较大的数据库中进行搜索,而这种两种方式并非真正的产生反馈,并且缺乏有效的语言理解和表示。这往往是由于模板/例子的数量和表示的局限性,这些方式在准确性和灵活性上都存在一定不足,很难兼顾语言的自然通顺和语义内容上的匹配。


4 结语

本文简单介绍了问答系统的发展历程、基本体系结构。并针对问答系统所需解决的关键问题,介绍了基于深度神经网络的语义表示方法,不同匹配架构的语义匹配模型,以及答案生成模型。当前深度学习在解决问答领域中的关键问题取得了不错的效果,但是问答系统的技术研究仍然存在有待解决问题,比如,如何理解连续交互问答场景下的用户提问,例如与Siri系统交互中的语言理解。以及如何学习外部语义知识,使问答系统能够进行简单知识推理回复关系推理性问题,例如“胸闷总咳嗽,上医院应该挂什么科”。再者,随着最近注意(attention)机制、记忆网络(Memory Network)[54,55]在自然语言理解,知识推理上的研究推广,这也必将给自动问答的研究提供的新的发展方向和契机。



注:文章来源于阿里巴巴-哈尔滨工业大学的合作项目:基于深度学习的智能问答。

 

参 考 文 献

[1] Terry Winograd. Five Lectures on Artificial Intelligence [J]. Linguistic Structures Processing, volume 5 of Fundamental Studies in Computer Science, pages 399- 520, North Holland, 1977.

[2] Woods W A. Lunar rocks in natural English: explorations in natural language question answering [J]. Linguistic Structures Processing, 1977, 5: 521−569.

[3] Dell Zhang and Wee Sun Lee. Question classification using support vector machines. In SIGIR, pages 26–32. ACM, 2003

[4] Xin Li and Dan Roth. Learning question classifiers. In COLING, 2002

[5] Hang Cui, Min-Yen Kan, and Tat-Seng Chua. Unsupervised learning of soft patterns for generating definitions from online news. In Stuart I. Feldman, Mike Uretsky, Marc Najork, and Craig E. Wills, editors, Proceedings of the 13th international conference on World Wide Web, WWW 2004, New York, NY, USA, May 17-20, 2004, pages 90–99. ACM, 2004.

[6] Clarke C, Cormack G, Kisman D, et al. Question answering by passage selection (multitext experiments for TREC-9) [C]//Proceedings of the 9th Text Retrieval Conference(TREC-9), 2000.

[7] Ittycheriah A, Franz M, Zhu W-J, et al. IBM’s statistical question answering system[C]//Proceedings of the 9th Text Retrieval Conference (TREC-9), 2000.

[8] Ittycheriah A, Franz M, Roukos S. IBM’s statistical question answering system—TREC-10[C]//Proceedings of the 10th Text Retrieval Conference (TREC 2001), 2001.

[9] Lee G G, Seo J, Lee S, et al. SiteQ: engineering high performance QA system using lexico-semantic pattern.

[10] Tellex S, Katz B, Lin J, et al. Quantitative evaluation of passage retrieval algorithms for question answering[C]// Proceedings of the 26th Annual International ACM SIGIR Conference on Research and Development in Information Retrieval (SIGIR ’03). New York, NY, USA: ACM, 2003:41–47.

[11] Jiwoon Jeon, W. Bruce Croft, and Joon Ho Lee. Finding similar questions in large question and answer archives. In Proceedings of the 2005 ACM CIKM International Conference on Information and Knowledge Management, Bremen, Germany, October 31 – November 5, 2005, pages 84–90. ACM, 2005.

[12] S. Riezler, A. Vasserman, I. Tsochantaridis, V. Mittal, Y. Liu, Statistical machine translation for query expansion in answer retrieval, in: Proceedings of the 45th Annual Meeting of the Association of Computational Linguistics, Association for Computational Linguistics, Prague, Czech Republic, 2007, pp. 464–471.

[13] M. Surdeanu, M. Ciaramita, H. Zaragoza, Learning to rank answers on large online qa collections., in: ACL, The Association for Computer Linguistics, 2008, pp. 719–727.

[14] A. Berger, R. Caruana, D. Cohn, D. Freitag, V. Mittal, Bridging the lexical chasm: statistical approaches to answer-finding, in: SIGIR ’00: Proceedings of the 23rd annual international ACM SIGIR conference on Research and development in information retrieval, ACM, New York, NY, USA, 2000, pp. 192–199.

[15] Gondek, D. C., et al. "A framework for merging and ranking of answers in DeepQA." IBM Journal of Research and Development 56.3.4 (2012): 14-1.

[16] Wang, Chang, et al. "Relation extraction and scoring in DeepQA." IBM Journal of Research and Development 56.3.4 (2012): 9-1.

[17] Kenneth C. Litkowski. Question-Answering Using Semantic Triples[C]. Eighth Text REtrieval Conference (TREC-8). Gaithersburg, MD. November 17-19, 1999.

[18] H. Cui, R. Sun, K. Li, M.-Y. Kan, T.-S. Chua, Question answering passage retrieval using dependency relations., in: R. A. Baeza-Yates, N. Ziviani, G. Marchionini, A. Moffat, J. Tait (Eds.), SIGIR, ACM, 2005, pp. 400–407.

[19] M. Wang, N. A. Smith, T. Mitamura, What is the jeopardy model? a quasisynchronous grammar for qa., in: J. Eisner (Ed.), EMNLP-CoNLL, The Association for Computer Linguistics, 2007, pp. 22–32.

[20] K. Wang, Z. Ming, T.-S. Chua, A syntactic tree matching approach to finding similar questions in community-based qa services, in: Proceedings of the 32Nd International ACM SIGIR Conference on Research and Development in Information Retrieval, SIGIR ’09, 2009, pp. 187–194.

[21] Hovy, E.H., U. Hermjakob, and Chin-Yew Lin. 2001. The Use of External Knowledge of Factoid QA. In Proceedings of the 10th Text Retrieval Conference (TREC 2001) [C], Gaithersburg, MD, U.S.A., November 13-16, 2001.

[22] Jongwoo Ko, Laurie Hiyakumoto, Eric Nyberg. Exploiting Multiple Semantic Resources for Answer Selection. InProceedings of of LREC(Vol. 2006).

[23] Kasneci G, Suchanek F M, Ifrim G, et al. Naga: Searching and ranking knowledge. IEEE, 2008:953-962.

[24] Zhang D, Lee W S. Question Classification Using Support Vector Machines[C]. Proceedings of the 26th Annual International ACM SIGIR Conference on Research and Development in Information Retrieval. 2003. New York, NY, USA: ACM, SIGIR’03.

[25] X. Yao, B. V. Durme, C. Callison-Burch, P. Clark, Answer extraction as sequence tagging with tree edit distance., in: HLT-NAACL, The Association for Computer Linguistics, 2013, pp. 858–867.

[26] C. Shah, J. Pomerantz, Evaluating and predicting answer quality in community qa, in: Proceedings of the 33rd International ACM SIGIR Conference on Research and Development in Information Retrieval, SIGIR ’10, 2010, pp. 411–418.

[27] T. Mikolov, K. Chen, G. Corrado, J. Dean, Efficient estimation of word representations in vector space, CoRR abs/1301.3781.

[28] Socher R, Lin C C, Manning C, et al. Parsing natural scenes and natural language with recursive neural networks[C]. Proceddings of International Conference on Machine Learning. Haifa, Israel: Omnipress, 2011: 129-136.

[29] A. Graves, Generating sequences with recurrent neural networks, CoRR abs/1308.0850.

[30] Kalchbrenner N, Grefenstette E, Blunsom P. A Convolutional Neural Network for Modelling Sentences[C]. Proceedings of ACL. Baltimore and USA: Association for Computational Linguistics, 2014: 655-665.

[31] Bahdanau D, Cho K, Bengio Y. Neural machine translation by jointly learning to align and translate [J]. arXiv, 2014.

[32] Sutskever I, Vinyals O, Le Q V V. Sequence to Sequence Learning with Neural Networks[M]. Advances in Neural Information Processing Systems 27. 2014: 3104-3112.

[33] Socher R, Pennington J, Huang E H, et al. Semi-supervised recursive autoencoders for predicting sentiment distributions[C]. EMNLP 2011

[34] Tang D, Wei F, Yang N, et al. Learning Sentiment-Specific Word Embedding for Twitter Sentiment Classification[C]. Proceedings of the 52nd Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers). Baltimore, Maryland: Association for Computational Linguistics, 2014: 1555-1565.

[35] Li J, Luong M T, Jurafsky D. A Hierarchical Neural Autoencoder for Paragraphs and Documents[C]. Proceedings of ACL. 2015.

[36] Kim Y. Convolutional Neural Networks for Sentence Classification[C]. Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP). Doha, Qatar: Association for Computational Linguistics, 2014: 1746–1751.

[37] Zeng D, Liu K, Lai S, et al. Relation Classification via Convolutional Deep Neural Network[C]. Proceedings of COLING 2014, the 25th International Conference on Computational Linguistics: Technical Papers. Dublin, Ireland: Association for Computational Linguistics, 2014: 2335–2344.

[38] L. Yu, K. M. Hermann, P. Blunsom, and S. Pulman. Deep learning for answer sentence selection. CoRR, 2014.

[39] B. Hu, Z. Lu, H. Li, Q. Chen, Convolutional neural network architectures for matching natural language sentences., in: Z. Ghahramani, M. Welling, C. Cortes, N. D. Lawrence, K. Q. Weinberger (Eds.), NIPS, 2014, pp. 2042–2050.

[40] A. Severyn, A. Moschitti, Learning to rank short text pairs with convolutional deep neural networks., in: R. A. Baeza-Yates, M. Lalmas, A. Moffat, B. A. Ribeiro-Neto (Eds.), SIGIR, ACM, 2015, pp. 373-382.

[41] Wen-tau Yih, Xiaodong He, and Christopher Meek. 2014. Semantic parsing for single-relation question answering. In Proceedings of the 52nd Annual Meeting of the Association for Computational Linguistics, pages 643–648. Association for Computational Linguistics.

[42] Li Dong, Furu Wei, Ming Zhou, and Ke Xu. 2015. Question Answering over Freebase with Multi-Column Convolutional Neural Networks. In Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics (ACL) and the 7th International Joint Conference on Natural Language Processing.

[43] Hochreiter S, Bengio Y, Frasconi P, et al. Gradient flow in recurrent nets: the difficulty of learning long-term dependencies[M]. A Field Guide to Dynamical Recurrent Neural Networks. New York, NY, USA: IEEE Press, 2001.

[44] Hochreiter S, Schmidhuber J. Long Short-Term Memory[J]. Neural Comput., 1997, 9(8): 1735-1780.

[45] Graves A. Generating Sequences With Recurrent Neural Networks[J]. CoRR, 2013, abs/1308.0850.

[46] Chung J, Gülçehre Ç, Cho K, et al. Gated Feedback Recurrent Neural Networks[C]. Proceedings of the 32nd International Conference on Machine Learning (ICML-15). Lille, France: JMLR Workshop and Conference Proceedings, 2015: 2067-2075.

[47] D.Wang, E. Nyberg, A long short-term memory model for answer sentence selection in question answering., in: ACL, The Association for Computer Linguistics, 2015, pp. 707–712.

[48] Malinowski M, Rohrbach M, Fritz M. Ask your neurons: A neural-based approach to answering questions about images[C]//Proceedings of the IEEE International Conference on Computer Vision. 2015: 1-9.

[49] Gao H, Mao J, Zhou J, et al. Are You Talking to a Machine? Dataset and Methods for Multilingual Image Question[C]//Advances in Neural Information Processing Systems. 2015: 2287-2295.

[50] Sun M S. Natural Language Procesing Based on Naturaly Annotated Web Resources [J]. Journal of Chinese Information Processing, 2011, 25(6): 26-32.

[51] Hu B, Chen Q, Zhu F. LCSTS: a large scale chinese short text summarization dataset[J]. arXiv preprint arXiv:1506.05865, 2015.

[52] Shang L, Lu Z, Li H. Neural Responding Machine for Short-Text Conversation[C]. Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics and the 7th International Joint Conference on Natural Language Processing. Beijing, China: Association for Computational Linguistics, 2015: 1577-1586.

[53] O. Vinyals, and Q. V. Le. A Neural Conversational Model. arXiv: 1506.05869,2015.

[54] Kumar A, Irsoy O, Su J, et al. Ask me anything: Dynamic memory networks for natural language processing[J]. arXiv preprint arXiv:1506.07285, 2015.

[55] Sukhbaatar S, Weston J, Fergus R. End-to-end memory networks[C]//Advances in Neural Information Processing Systems. 2015: 2431-2439.

 

开源数据平台Kafka落选!InfoWorld最佳开源数据平台奖公布

$
0
0

AI前线导读:一年一度由世界知名科技媒体InfoWorld评选的Bossie Awards于9月26日公布,本次Bossie Awards评选出了最佳数据库与数据分析平台奖、最佳软件开发工具奖、最佳机器学习项目奖等多个奖项。在最佳开源数据库与数据分析平台奖中,Spark和Beam再次入选,连续两年入选的Kafka这次意外滑铁卢,取而代之的是新兴项目Pulsar;开源数据库入选的还有PingCAP的TiDB。 

Bossie Awards是知名英文科技媒体InfoWorld针对开源软件颁发的年度奖项,根据这些软件对开源界的贡献,以及在业界的影响力评判获奖对象,由InfoWorld编辑独立评选,目前已经持续超过十年,是IT届最具影响力和含金量奖项之一。一起来看看接下来你需要了解和学习的新晋数据库和数据分析工具有哪些。

现如今,没有什么东西能够比数据更大的了!我们有比以前多得多的数据,我们有更多方式来存储和分析数据:SQL数据库、NoSQL数据库、分布式OLTP数据库、分布式OLAP平台、分布式混合OLTP/OLAP平台。2018年数据库和数据分析平台方面的Bossie大奖获得者也包括了流式处理方面的创新者。

Apache Spark

尽管新的产品层出不穷, Apache Spark在数据分析领域仍然占据着举足轻重的地位。如果你需要从事分布式计算、数据科学或者机器学习相关的工作,就使用Apache Spark吧。Apache Spark 2.3在二月份发布,它依然着重于开发、集成并加强它的Structured Streaming API。另外,新版本中添加了Kubernetes调度程序,因此在容器平台上直接运行Spark变得非常简单。总体来说,现在的Spark版本经过调整和改进,似乎焕然一新。

Apache Pulsar

Apache Pulsar最初由雅虎开发,后来进入Apache孵化器,最近正式毕业,成为Apache顶级项目。Pulsar旨在取代Apache Kafka多年的主宰地位。Pulsar在很多情况下提供了比Kafka更快的吞吐量和更低的延迟,并为开发人员提供了一组兼容的API,让他们可以很轻松地从Kafka切换到Pulsar。

Pulsar的最大优点在于它提供了比Apache Kafka更简单明了、更健壮的一系列操作功能,特别在解决可观察性、地域复制和多租户方面的问题。在运行大型Kafka集群方面感觉有困难的企业可以考虑转向使用Pulsar。

Apache Beam

多年来,批处理和流式处理之间的差异正在慢慢缩小。批次数据变得越来越小,变成了微批次数据,随着批次的大小接近于一,也就变成了流式数据。有很多不同的处理架构也正在尝试将这种转变映射成为一种编程范式。

Apache Beam就是谷歌提出的解决方案。Beam结合了一个编程模型和多个语言特定的SDK,可用于定义数据处理管道。在定义好管道之后,这些管道就可以在不同的处理框架上运行,比如Hadoop、Spark和Flink。当为开发数据密集型应用程序而选择数据处理管道时(现如今还有什么应用程序不是数据密集的呢?),Beam应该在你的考虑范围之内。

Apache Solr

尽管大家都认为 Apache Solr是基于Lucene索引技术而构建的搜索引擎,但它实际上是面向文本的文档数据库,而且是一个非常优秀的文档数据库。不管你是要“大海捞针”,还是要运行空间信息查询,Solr都可以帮上忙。

Solr 7系列目前已经发布了,新版本在运行更多分析查询的情况下仍然能保证闪电般的速度。你可以加入很多文档,不到一秒钟就能返回结果。它还改进了对日志和事件数据的支持。灾备(CDCR)现在也是双向的。Solr全新的自动扩展功能简化了集群负载增长时的扩展操作。

JupyterLab

JupyterLab是新一代的 Jupyter,一个基于Web的notebook服务器,颇受全世界数据科学家的喜爱。经过三年开发,JupyterLab完全改变了人们对notebook的理解,支持对单元格进行拖放重新排布、标签式的notebook、实时预览Markdown编辑,以及改良的扩展系统,与GitHub等服务的集成变得非常简单。预计在2018年底,JupyterLab将发布1.0稳定版。

KNIME分析平台

KNIME分析平台是用来创建数据科学应用程序和服务的开源软件。它提供了可拖放的图形界面,用来创建可视化工作流,还支持R和Python脚本、机器学习,支持和Apache Spark连接器。KNIME目前有大概2000个模块可用作工作流的节点。

KNIME还提供了商业版,商业版旨在提升生产效率和支持协作。不过,开源版KNIME分析平台并不存在人为限制,可以处理包含数亿行数据的项目。

CockroachDB

CockroachDB是基于事务性和一致性键值存储而构建的分布式SQL数据库。它的设计目标是能够在磁盘、机器、机架甚至是数据中心的故障中存活下来,最小化延迟中断,不需要人工干预。CockroachDB v1.13曾经获得过五星的高分,虽然仍然缺少很多功能,不过现在情况有所改变。

四月份发布的CockroachDB v2.0版本有了明显的性能改进,通过添加对JSON(和其他类型)的支持扩展了与PostgreSQL的兼容性,还提供了生产环境的跨区域集群管理功能。CockroachDB v2.1的路线图中包含了基于成本的查询优化器(用于查询性能的改进)、相关子查询(ORM)、更好地支持模式变更以及企业版产品的加密。

Vitess

Vitess是通过分片实现MySQL水平扩展的数据库集群系统,主要使用Go语言开发。Vitess将MySQL的很多重要功能与NoSQL数据库的扩展性结合在一起。它的内置分片功能可以让用户在不需要给应用程序添加分片逻辑的情况下对数据库进行扩展。Vitess从2011年开始就是YouTube数据库基础设施的核心组件,它已经发展到成千上万个MySQL节点。

Vitess并没有使用标准的MySQL连接,因为这会消耗很多RAM,也会限制每个节点的连接数量。它使用了更有效的基于gRPC的协议。另外,Vitess会自动重写会损害数据库性能的查询,通过缓存机制来调解查询,防止相同的查询同时进入数据库。

TiDB

TiDB是一款兼容MySQL、支持混合事务和分析处理(HTAP)的分布式数据库。它基于事务性键值存储而构建,提供全面的水平扩展性(通过增加节点)以及持续可用性。大多数早期的TiDB用户都在中国,因为TiDB的开发者在北京。TiDB的源代码主要用Go语言编写。

TiDB的底层是RocksDB,RocksDB是Facebook的日志结构键值数据库引擎,用C++编写,因此能获得最好的性能。RocksDB上面是Raft共识层、事务层,然后是支持MySQL协议的SQL层。

YugaByte DB

YugaByte DB结合了分布式ACID事务、多区域部署、对Cassandra和Redis API的支持,对PostgreSQL的支持即将推出。相对Cassandra而言,YugaByte是强一致性,而Cassandra时最终一致性。 YugaByte的基准测试也比开源的Cassandra要好,但比商用的Cassandra要差一些,而DataStax Enterprise 6具备可调一致性。YugaByte相当于快速、具有更强一致性的分布式Redis和Cassandra。它可以对单个数据库进行标准化处理,比如将Cassandra数据库和Redis缓存结合在一起。

Neo4j

Neo4j图形数据库在处理相关性网络的任务时,执行速度比SQL和NoSQL数据库更快,但图模型和Cypher查询语言需要进行专门的学习。最近, 俄罗斯Twitter流氓分析、ICIJ的 Panama Papers分析以及 Paradise Papers的分析指出,Neo4j是非常有价值的。

经过18年的开发,Neo4j已经成为了一个成熟的图数据库平台,可以在Windows、MacOS、Linux、Docker容器、VM和集群中运行。即使是Neo4j的开源版本也可以处理很大的图,而在企业版中对图的大小没有限制。(开源版本的Neo4j只能在一台服务器上运行。)

InfluxDB

InfluxDB是没有外部依赖的开源时间序列数据库,旨在处理高负载的写入和查询,在记录指标、事件以及进行分析时非常有用。它可以运行在MacOS、Docker、Ubuntu/Debian、Red Hat/CentOS和Windows平台上。它提供了一个内置的HTTP API和SQL风格的查询语言,并旨在提供实时的查询响应(100毫秒之内)。

查看英文原文: The best open source software for data storage and analytics

https://www.infoworld.com/article/3306454/big-data/the-best-open-source-software-for-data-storage-and-analytics.html#slide1

Adrian小哥教程:如何使用Tesseract和OpenCV执行OCR和文本识别

$
0
0

近期,Adrian Rosebrock 发布一篇教程,介绍了如何使用 OpenCV、Python 和 Tesseract 执行文本检测和文本识别。从安装软件和环境、项目流程、review 代码、实验结果,到展示局限、提出建议,这篇教程可以说十分详细了。机器之心对该教程进行了摘要编译介绍。

本教程将介绍如何使用 OpenCV OCR。我们将使用 OpenCV、Python 和 Tesseract 执行文本检测和文本识别。

之前的教程展示了如何使用 OpenCV的 EAST 深度学习模型执行文本检测(参见 https://www.pyimagesearch.com/2018/08/20/opencv-text-detection-east-text-detector/)。使用该模型能够检测和定位图像中文本的边界框坐标。

那么下一步就是使用 OpenCV和 Tesseract 处理每一个包含文本的图像区域,识别这些文本并进行 OCR 处理。

本教程将介绍如何构建自己的 OpenCV OCR 和文本识别系统!

使用 Tesseract 进行 OpenCV OCR 和文本识别

为了执行 OpenCV OCR 和文本识别任务,我们首先需要安装 Tesseract v4,包括一个用于文本识别的高度准确的深度学习模型。

然后,我将展示如何写一个 Python 脚本,使其能够:

  1. 使用 OpenCV EAST 文本检测器执行文本检测,该模型是一个高度准确的深度学习文本检测器,可用于检测自然场景图像中的文本。

  2. 使用 OpenCV检测出图像中的文本区域后,我们提取出每个文本 ROI 并将其输入 Tesseract,从而构建完整的 OpenCV OCR 流程!

最后,我将展示一些使用 OpenCV应用文本识别的示例,并讨论该方法的缺陷。

下面就开始本教程的正式内容吧!

如何安装 Tesseract v4

图 1:Tesseract OCR 引擎于 20 世纪 80 年代出现,到 2018 年,它已经包括内置的深度学习模型,变成了更加稳健的 OCR 工具。Tesseract 和 OpenCV的 EAST 检测器是一个很棒的组合。

Tesseract 是一个很流行的 OCR 引擎,20 世纪 80 年代由 Hewlett Packard 开发,2005 年开源,自 2006 年起由谷歌赞助开发。该工具在受控条件下也能很好地运行,但是如果存在大量噪声或者图像输入 Tesseract 前未经恰当处理,则性能较差。

深度学习计算机视觉的各个方面都产生了影响,字符识别和手写字体识别也不例外。基于深度学习的模型能够实现前所未有的文本识别准确率,远超传统的特征提取和机器学习方法。Tesseract 纳入深度学习模型来进一步提升 OCR 准确率只是时间问题,事实上,这个时间已经到来。

Tesseract (v4) 最新版本支持基于深度学习的 OCR,准确率显著提高。底层的 OCR 引擎使用的是一种循环神经网络(RNN)——LSTM网络。

安装 OpenCV

要运行本教程的脚本,你需要先安装 3.4.2 或更高版本的 OpenCV。安装教程可参考 https://www.pyimagesearch.com/opencv-tutorials-resources-guides/,该教程可确保你下载合适的 OpenCVOpenCV-contrib 版本。

在 Ubuntu 上安装 Tesseract 4

在 Ubuntu 上安装 Tesseract 4 的具体命令因你使用的 Ubuntu 版本而异(Ubuntu 18.04、Ubuntu 17.04 或更早版本)。你可使用 lsb_release 命令检查 Ubuntu 版本:

如上所示,我的机器上运行的是 Ubuntu 18.04,不过你在继续操作之前需要先检查自己的 Ubuntu 版本。

对于 Ubuntu 18.04 版本的用户,Tesseract 4 是主 apt-get 库的一部分,这使得通过下列命令安装 Tesseract 非常容易:

如果你正在使用 Ubuntu 14、16 或 17 版本,那么由于依赖需求,你需要额外的命令行。

Alexander Pozdnyakov 创建了用于 Tesseract 的 Ubuntu PPA(https://launchpad.net/~alex-p/+archive/ubuntu/tesseract-ocr),大大简化了在 Ubuntu 旧版本上安装 Tesseract 4 的过程。

只需要向系统添加 alex-p/tesseract-ocr PPA 库,更新你的包定义,然后安装 Tesseract:

如果没有错误,那么你应该已经在自己的机器上成功安装了 Tesseract 4。

在 macOS 上安装 Tesseract 4

如果你的系统中安装有 Homebrew(macOS「非官方」包管理器),那么在 macOS 上安装 Tesseract 4 很简单。

只需要运行以下命令,确保指定 --HEAD,即可在 Mac 电脑上安装 Tesseract v4:

安装好之后,你可能想删除初始安装的链接:

接下来就可以运行安装命令了。

验证你的 Tesseract 版本

图 2:我的系统终端截图。我输入 tesseract -v 命令来检查 Tesseract 版本。

确保安装了 Tesseract 以后,你应该执行以下命令验证 Tesseract 版本:

只要输出中包含 tesseract 4,那么你就成功在系统中安装了 Tesseract 的最新版本。

安装 Tesseract + Python 捆绑

安装好 Tesseract 库之后,我们需要安装 Tesseract + Python 捆绑,这样我们的 Python 脚本就可以与 Tesseract 通信,并对 OpenCV处理过的图像执行 OCR。

如果你使用的是 Python 虚拟环境(非常推荐,你可以拥有独立的 Python 环境),那么使用 workon 命令访问虚拟环境:

如上所示,我访问了一个叫做 cv 的 Python 虚拟环境(cv 是「计算机视觉」的缩写),你也可以用其他名字命名虚拟环境。

接下来,我们将使用 pip 来安装 Pillow(PIL 的 Python 版本),然后安装 pytesseract 和 imutils:

现在打开 Python shell,确认你导入了 OpenCV和 pytesseract:

恭喜!如果没有出现导入错误,那么你的机器现在已经安装好,可以使用 OpenCV执行 OCR 和文本识别任务了。

理解 OpenCV OCR 和 Tesseract 文本识别

图 3:OpenCV OCR 流程图。

现在我们已经在系统上成功安装了 OpenCV和 Tesseract,下面我们来简单回顾一下流程和相关命令。

首先,我们使用 OpenCV的 EAST 文本检测器来检测图像中的文本。EAST 文本检测器将提供文本 ROI 的边界框坐标。我们将提取每个文本 ROI,将其输入到 Tesseract v4 的 LSTM深度学习文本识别算法。LSTM的输出将提供实际 OCR 结果。最后,我们将在输出图像上绘制 OpenCV OCR 结果。

过程中使用到的 Tesseract 命令必须在 pytesseract 库下调用。在调用 tessarct 库时,我们需要提供大量 flag。最重要的三个 flag 是 -l、--oem 和 --ism。

-l flag 控制输入文本的语言,本教程示例中使用的是 eng(英语),在这里你可以看到 Tesseract 支持的所有语言:https://github.com/tesseract-ocr/tesseract/wiki/Data-Files

--oem(OCR 引擎模式)控制 Tesseract 使用的算法类型。执行以下命令即可看到可用的 OCR 引擎模式:

我们将使用--oem 1,这表明我们希望仅使用深度学习LSTM引擎。

最后一个重要的 flag --psm 控制 Tesseract 使用的自动页面分割模式:

对文本 ROI 执行 OCR,我发现模式 6 和 7 性能较好,但是如果你对大量文本执行 OCR,那么你可以试试 3(默认模式)。

如果你得到的 OCR 结果不正确,那么我强烈推荐调整 --psm,它可以对你的输出 OCR 结果产生极大的影响。

项目结构

你可以从本文「Downloads」部分下载 zip。然后解压缩,进入目录。下面的 tree 命令使得我们可以在终端阅览目录结构:

我们的项目包含一个目录和两个重要文件:

  • images/:该目录包含六个含有场景文本的测试图像。我们将使用这些图像进行 OpenCV OCR 操作。

  • frozen_east_text_detection.pb:EAST 文本检测器。该 CNN 已经经过预训练,可用于文本检测。它是由 OpenCV提供的,你也可以在「Downloads」部分下载它。

  • text_recognition.py:我们的 OCR 脚本。我们将逐行 review 该脚本。它使用 EAST 文本检测器找到图像中的文本区域,然后利用 Tesseract v4 执行文本识别。

实现我们的 OpenCV OCR 算法

现在开始用 OpenCV执行文本识别吧!

打开 text_recognition.py 文件,插入下列代码:

本教程中的 OCR 脚本需要五个导入,其中一个已经内置入 OpenCV

最显著的一点是,我们将使用 pytesseract 和 OpenCV。我的 imutils 包将用于非极大值抑制,因为 OpenCV的 NMSBoxes 函数无法适配 Python API。我注意到 NumPy 是 OpenCV的依赖项。

argparse 包被包含在 Python 中,用于处理命令行参数,这里无需安装。

现在已经处理好导入了,接下来就来实现 decode_predictions 函数:

decode_predictions 函数从第 8 行开始,在这篇文章中有详细介绍(https://www.pyimagesearch.com/2018/08/20/opencv-text-detection-east-text-detector/)。该函数:

  1. 使用基于深度学习的文本检测器来检测(不是识别)图像中的文本区域。

  2. 该文本检测器生成两个阵列,一个包括给定区域包含文本的概率,另一个阵列将该概率映射到输入图像中的边界框位置。

EAST 文本检测器生成两个变量:

  • scores:文本区域的概率。

  • geometry:文本区域的边界框位置。

两个变量都是 decode_predictions 函数的参数

该函数处理输入数据,得出一个包含文本边界框位置和该区域包含文本的相应概率的元组:

  • rects:该值基于 geometry,其格式更加紧凑,方便我们稍后将其应用于 NMS。

  • confidences:该列表中的置信度值对应 rects 中的每个矩形。

这两个值都由 decode_predictions 函数得出。

注意:完美情况下,旋转的边界框也在 rects 内,但是提取旋转边界框不利于解释本教程的概念。因此,我计算了水平的边界框矩形(把 angle 考虑在内)。如果你想提取文本的旋转边界框输入 Tesseract,你可以在第 41 行获取 angle。

关于上述代码块的更多细节,参见 https://www.pyimagesearch.com/2018/08/20/opencv-text-detection-east-text-detector/

下面我们来解析命令行参数

我们的脚本需要两个命令行参数

  • --image:输入图像的路径。

  • --east:预训练 EAST 文本检测器的路径。

下列命令行参数是可选的:

  • --min-confidence:检测到的文本区域的最小概率。

  • --width:图像输入 EAST 文本检测器之前需要重新调整的宽度,我们的检测器要求宽度是 32 的倍数。

  • --height:与宽度类似。检测器要求调整后的高度是 32 的倍数。

  • --padding:添加到每个 ROI 边框的(可选)填充数量。如果你发现 OCR 结果不正确,那么你可以尝试 0.05、0.10 等值。

下面,我们将加载和预处理图像,并初始化关键变量:

第 82 行和 83 行,将图像加载到内存中,并复制(这样稍后我们可以在上面绘制输出结果)。

获取原始宽度和高度(第 84 行),然后从 args 词典中提取新的宽度和高度(第 88 行)。我们使用原始和新的维度计算比率,用于稍后在脚本中扩展边界框坐标(第 89 和 90 行)。

然后调整图像大小,此处忽略长宽比(第 93 行)。

接下来,我们将使用 EAST 文本检测器:

第 99 到 101 行,将两个输出层名称转换成列表格式。然后,将预训练 EAST 神经网络加载到内存中(第 105 行)。

必须强调一点:你至少需要 OpenCV 3.4.2 版本,它有 cv2.dnn.readNet 实现。

接下来就是见证第一个「奇迹」的时刻:

为确定文本位置,我们:

  • 在第 109 和 110 行构建 blob。详情参见 https://www.pyimagesearch.com/2017/11/06/deep-learning-opencvs-blobfromimage-works/

  • 将 blob 输入 EAST 神经网络中,获取 scores 和 geometry(第 111 和 112 行)。

  • 使用之前定义的 decode_predictions 函数解码预测(第 116 行)。

  • 通过 imutils 方法进行非极大值抑制(第 117 行)。NMS 高效使用概率最高的文本区域,删除其他重叠区域。

现在我们知道文本区域的位置了,接下来需要识别文本。我们开始在边界框上循环,并处理结果,为实际的文本识别做准备:

我们初始化 results 列表,使其包含我们的 OCR 边界框和文本(第 120 行)。然后在 boxes 上进行循环(第 123 行),我们:

  • 基于之前计算的比率扩展边界框(第 126-129 行)。

  • 填充边界框(第 134-141 行)。

  • 最后,提取被填充的 roi(第 144 行)。

本文的 OpenCV OCR 流程可以使用一点 Tesseract v4「魔术」来完成:

第 151 行,我们设置 Tesseract config 参数(英语、LSTM神经网络和单行文本)。

注:如果你获取了错误的 OCR 结果,那么你可能需要使用本教程开头的指令配置 --psm 值。

第 152 行,pytesseract 库进行剩下的操作,调用 pytesseract.image_to_string,将 roi 和 config string 输入其中。

只用两行代码,你就使用 Tesseract v4 识别了图像中的一个文本 ROI。记住,很多过程在底层发生。

我们的结果(边界框值和实际的 text 字符串)附加在 results 列表(第 156 行)中。

接下来,我们继续该流程,在循环的基础上处理其他 ROI。

现在,我们来打印出结果,查看它是否真正有效:

第 159 行基于边界框的 y 坐标按自上而下的顺序对结果进行了排序。

对结果进行循环,我们:

  • 将 OCR 处理过的文本打印到终端(第 164-166 行)。

  • 从文本中去掉非 ASCII 字符,因为 OpenCV在 cv2.putText 函数中不支持非 ASCII 字符(第 171 行)。

  • 基于 ROI 绘制 ROI 周围的边界框和结果文本(第 173-176 行)。

  • 展示输出,等待即将按下的键(第 179、180 行)。

OpenCV文本识别结果

现在我们已经实现了 OpenCV OCR 流程。

确保使用本教程「Downloads」部分下载源代码、OpenCV EAST 文本检测器模型和示例图像。

打开命令行,导航至下载和提取压缩包的位置,然后执行以下命令:

图 4:对 OpenCV OCR 的第一次尝试成功!

我们从一个简单示例开始。

注意我们的 OpenCV OCR 系统如何正确检测图像中的文本,然后识别文本。

下一个示例更具代表性,是一个现实世界图像:

图 5:更复杂的图像示例,我们使用 OpenCV和 Tesseract 4 对这个白色背景的标志牌进行了 OCR 处理。

再次,注意我们的 OpenCV OCR 系统如何正确定位文本位置和识别文本。但是,在终端输出中,我们看到了一个注册商标 Unicode 符号,这里 Tesseract 可能被欺骗,因为 OpenCV EAST 文本检测器报告的边界框与标志牌后面的植物发生重叠。

下面我们来看另一个 OpenCV OCR 和文本识别示例:

图 6:使用 OpenCV、Python 和 Tesseract 对包含三个单词的大标志牌进行 OCR 处理。

该示例中有三个单独的文本区域。OpenCV的文本检测器能够定位每一个文本区域,然后我们使用 OCR 准确识别每个文本区域。

下一个示例展示了在特定环境下添加填充的重要性:

 图 7:在这个烘培店场景图像中,我们的 OpenCV OCR 流程在处理 OpenCV EAST 文本检测器确定的文本区域时遇到了问题。记住,没有一个 OCR 系统完美适用于所有情况。那么我们能否通过更改参数来做得更好呢?

首先尝试对这家烘培店的店面进行 OCR,我们看到「SHOP」被正确识别,但是:

  1. 「CAPUTO」中的「U」被错误识别为「TI」。

  2. 「CAPUTO'S」中的「'S」被漏掉。

  3. 「BAKE」被错误识别为「|.」。

现在我们添加填充,从而扩展 ROI 的边界框坐标,准确识别文本:

图 8:通过向 EAST 文本检测器确定的文本区域添加额外的填充,我们能够使用 OpenCV和 Tesseract 对烘培店招牌中的三个单词进行恰当的 OCR 处理。

仅仅在边界框的四角周围添加 5% 的填充,我们就能够准确识别出「BAKE」、「U」和「'S」。

当然,也有 OpenCV的失败案例:

图 9:添加了 25% 的填充后,我们的 OpenCV OCR 系统能够识别招牌中的「Designer」,但是它无法识别较小的单词,因为它们的颜色与背景色太接近了。我们甚至无法检测到单词「SUIT」,「FACTORY」能够检测到,但无法使用 Tesseract 识别。我们的 OCR 系统离完美还很远。

下面介绍了该 OCR 系统的一些局限和不足,以及对改进 OpenCV文本识别流程的建议。

局限和不足

记住,没有完美的 OCR 系统,尤其是在现实世界条件下。期望 100% 的 OCR 准确率也是不切实际的。

我们的 OpenCV OCR 系统可以很好地处理一些图像,但在处理另外一些图像时会失败。该文本识别流程失败存在两个主要原因:

  1. 文本被扭曲或旋转。

  2. 文本字体与 Tesseract 模型训练的字体相差太远。

即使 Tesseract v4 与 v3 相比更加强大、准确,但该深度学习模型仍然受限于训练数据。如果你的文本字体与训练数据字体相差太远,那么 Tesseract 很可能无法对该文本进行 OCR 处理。

其次,Tesseract 仍然假设输入图像/ROI 已经经过恰当清洁。而当我们在自然场景图像上执行文本识别时,该假设不总是准确。

总结

本教程介绍了如何使用 OpenCV OCR 系统执行文本检测和文本识别。

为了实现该任务,我们

  1. 利用 OpenCV EAST 文本检测器定位图像中的文本区域。

  2. 提取每个文本 ROI,然后使用 OpenCV和 Tesseract v4 进行文本识别。

我们还查看了执行文本检测和文本识别的 Python 代码。

OpenCV OCR 流程在一些情况下效果很好,另一些情况下并不那么准确。要想获得最好的 OpenCV文本识别结果,我建议你确保:

  1. 输入 ROI 尽量经过清理和预处理。在理想世界中,你的文本应该能够与图像的其他部分完美分割,但是在现实情况下,分割并不总是那么完美。

  2. 文本是在摄像机 90 度角的情况下拍摄的,类似于自上而下、鸟瞰的角度。如果不是,那么角度变换可以帮助你获得更好的结果。

以上就是这次的教程,希望对大家有所帮助!


原文链接:https://www.pyimagesearch.com/2018/09/17/opencv-ocr-and-text-recognition-with-tesseract/

Python日志库logging总结

$
0
0

在部署项目时,不可能直接将所有的信息都输出到控制台中,我们可以将这些信息记录到日志文件中,这样不仅方便我们查看程序运行时的情况,也可以在项目出现故障时根据运行时产生的日志快速定位问题出现的位置。

1、日志级别

Python 标准库 logging 用作记录日志,默认分为六种日志级别(括号为级别对应的数值),NOTSET(0)、DEBUG(10)、INFO(20)、WARNING(30)、ERROR(40)、CRITICAL(50)。我们自定义日志级别时注意不要和默认的日志级别数值相同,logging 执行时输出大于等于设置的日志级别的日志信息,如设置日志级别是 INFO,则 INFO、WARNING、ERROR、CRITICAL 级别的日志都会输出。

2、logging 流程

官方的 logging 模块工作流程图如下:

从下图中我们可以看出看到这几种 Python 类型, LoggerLogRecordFilterHandlerFormatter

类型说明:

Logger:日志,暴露函数给应用程序,基于日志记录器和过滤器级别决定哪些日志有效。

LogRecord:日志记录器,将日志传到相应的处理器处理。

Handler:处理器, 将(日志记录器产生的)日志记录发送至合适的目的地。

Filter:过滤器, 提供了更好的粒度控制,它可以决定输出哪些日志记录。

Formatter:格式化器, 指明了最终输出中日志记录的布局。

  1. 判断 Logger 对象对于设置的级别是否可用,如果可用,则往下执行,否则,流程结束。
  2. 创建 LogRecord 对象,如果注册到 Logger 对象中的 Filter 对象过滤后返回 False,则不记录日志,流程结束,否则,则向下执行。
  3. LogRecord 对象将 Handler 对象传入当前的 Logger 对象,(图中的子流程)如果 Handler 对象的日志级别大于设置的日志级别,再判断注册到 Handler 对象中的 Filter 对象过滤后是否返回 True 而放行输出日志信息,否则不放行,流程结束。
  4. 如果传入的 Handler 大于 Logger 中设置的级别,也即 Handler 有效,则往下执行,否则,流程结束。
  5. 判断这个 Logger 对象是否还有父 Logger 对象,如果没有(代表当前 Logger 对象是最顶层的 Logger 对象 root Logger),流程结束。否则将 Logger 对象设置为它的父 Logger 对象,重复上面的 3、4 两步,输出父类 Logger 对象中的日志输出,直到是 root Logger 为止。

3、日志输出格式

日志的输出格式可以认为设置,默认格式为下图所示。

4、基本使用

logging 使用非常简单,使用 basicConfig() 方法就能满足基本的使用需要,如果方法没有传入参数,会根据默认的配置创建Logger 对象,默认的日志级别被设置为 WARNING,默认的日志输出格式如上图,该函数可选的参数如下表所示。

参数名称参数描述
filename日志输出到文件的文件名
filemode文件模式,r[+]、w[+]、a[+]
format日志输出的格式
datefat日志附带日期时间的格式
style格式占位符,默认为 “%” 和 “{}”
level设置日志输出级别
stream定义输出流,用来初始化 StreamHandler 对象,不能 filename 参数一起使用,否则会ValueError 异常
handles定义处理器,用来创建 Handler 对象,不能和 filename 、stream 参数一起使用,否则也会抛出 ValueError 异常

示例代码如下:

import logging

logging.basicConfig()
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')

输出结果如下:

WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message

传入常用的参数,示例代码如下(这里日志格式占位符中的变量放到后面介绍):

import logging

logging.basicConfig(filename="test.log", filemode="w", format="%(asctime)s %(name)s:%(levelname)s:%(message)s", datefmt="%d-%M-%Y %H:%M:%S", level=logging.DEBUG)
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')

生成的日志文件 test.log ,内容如下:

13-10-18 21:10:32 root:DEBUG:This is a debug message
13-10-18 21:10:32 root:INFO:This is an info message
13-10-18 21:10:32 root:WARNING:This is a warning message
13-10-18 21:10:32 root:ERROR:This is an error message
13-10-18 21:10:32 root:CRITICAL:This is a critical message

但是当发生异常时,直接使用无参数的 debug()、info()、warning()、error()、critical() 方法并不能记录异常信息,需要设置 exc_info 参数为 True 才可以,或者使用 exception() 方法,还可以使用 log() 方法,但还要设置日志级别和 exc_info 参数。

import logging

logging.basicConfig(filename="test.log", filemode="w", format="%(asctime)s %(name)s:%(levelname)s:%(message)s", datefmt="%d-%M-%Y %H:%M:%S", level=logging.DEBUG)
a = 5
b = 0
try:
    c = a / b
except Exception as e:
    # 下面三种方式三选一,推荐使用第一种
    logging.exception("Exception occurred")
    logging.error("Exception occurred", exc_info=True)
    logging.log(level=logging.DEBUG, msg="Exception occurred", exc_info=True)

5、自定义 Logger

上面的基本使用可以让我们快速上手 logging 模块,但一般并不能满足实际使用,我们还需要自定义 Logger。

一个系统只有一个 Logger 对象,并且该对象不能被直接实例化,没错,这里用到了单例模式,获取 Logger 对象的方法为 getLogger

注意:这里的单例模式并不是说只有一个 Logger 对象,而是指整个系统只有一个根 Logger 对象,Logger 对象在执行 info()、error() 等方法时实际上调用都是根 Logger 对象对应的 info()、error() 等方法。

我们可以创造多个 Logger 对象,但是真正输出日志的是根 Logger 对象。每个 Logger 对象都可以设置一个名字,如果设置 logger = logging.getLogger(__name__),__name__ 是 Python 中的一个特殊内置变量,他代表当前模块的名称(默认为 __main__)。则 Logger 对象的 name 为建议使用使用以点号作为分隔符的命名空间等级制度。

Logger 对象可以设置多个 Handler 对象和 Filter 对象,Handler 对象又可以设置 Formatter 对象。Formatter 对象用来设置具体的输出格式,常用变量格式如下表所示,所有参数见 Python(3.7)官方文档

变量格式变量描述
asctime%(asctime)s将日志的时间构造成可读的形式,默认情况下是精确到毫秒,如 2018-10-13 23:24:57,832,可以额外指定 datefmt 参数来指定该变量的格式
name%(name)日志对象的名称
filename%(filename)s不包含路径的文件名
pathname%(pathname)s包含路径的文件名
funcName%(funcName)s日志记录所在的函数名
levelname%(levelname)s日志的级别名称
message%(message)s具体的日志信息
lineno%(lineno)d日志记录所在的行号
pathname%(pathname)s完整路径
process%(process)d当前进程ID
processName%(processName)s当前进程名称
thread%(thread)d当前线程ID
threadName%threadName)s当前线程名称

Logger 对象和 Handler 对象都可以设置级别,而默认 Logger 对象级别为 30 ,也即 WARNING,默认 Handler 对象级别为 0,也即 NOTSET。logging 模块这样设计是为了更好的灵活性,比如有时候我们既想在控制台中输出DEBUG 级别的日志,又想在文件中输出WARNING级别的日志。可以只设置一个最低级别的 Logger 对象,两个不同级别的 Handler 对象,示例代码如下:

import logging
import logging.handlers

logger = logging.getLogger("logger")

handler1 = logging.StreamHandler()
handler2 = logging.FileHandler(filename="test.log")

logger.setLevel(logging.DEBUG)
handler1.setLevel(logging.WARNING)
handler2.setLevel(logging.DEBUG)

formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")
handler1.setFormatter(formatter)
handler2.setFormatter(formatter)

logger.addHandler(handler1)
logger.addHandler(handler2)

# 分别为 10、30、30
# print(handler1.level)
# print(handler2.level)
# print(logger.level)

logger.debug('This is a customer debug message')
logger.info('This is an customer info message')
logger.warning('This is a customer warning message')
logger.error('This is an customer error message')
logger.critical('This is a customer critical message')

控制台输出结果为:

2018-10-13 23:24:57,832 logger WARNING This is a customer warning message
2018-10-13 23:24:57,832 logger ERROR This is an customer error message
2018-10-13 23:24:57,832 logger CRITICAL This is a customer critical message

文件中输出内容为:

2018-10-13 23:44:59,817 logger DEBUG This is a customer debug message
2018-10-13 23:44:59,817 logger INFO This is an customer info message
2018-10-13 23:44:59,817 logger WARNING This is a customer warning message
2018-10-13 23:44:59,817 logger ERROR This is an customer error message
2018-10-13 23:44:59,817 logger CRITICAL This is a customer critical message

创建了自定义的 Logger 对象,就不要在用 logging 中的日志输出方法了,这些方法使用的是默认配置的 Logger 对象,否则会输出的日志信息会重复。

import logging
import logging.handlers

logger = logging.getLogger("logger")
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.debug('This is a customer debug message')
logging.info('This is an customer info message')
logger.warning('This is a customer warning message')
logger.error('This is an customer error message')
logger.critical('This is a customer critical message')

输出结果如下(可以看到日志信息被输出了两遍):

2018-10-13 22:21:35,873 logger WARNING This is a customer warning message
WARNING:logger:This is a customer warning message
2018-10-13 22:21:35,873 logger ERROR This is an customer error message
ERROR:logger:This is an customer error message
2018-10-13 22:21:35,873 logger CRITICAL This is a customer critical message
CRITICAL:logger:This is a customer critical message

说明:在引入有日志输出的 python 文件时,如 import test.py,在满足大于当前设置的日志级别后就会输出导入文件中的日志。

6、Logger 配置

通过上面的例子,我们知道创建一个 Logger 对象所需的配置了,上面直接硬编码在程序中配置对象,配置还可以从字典类型的对象和配置文件获取。打开 logging.config Python 文件,可以看到其中的配置解析转换函数。

从字典中获取配置信息:

import logging.config

config = {
    'version': 1,'formatters': {'simple': {'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        },
        # 其他的 formatter
    },
    'handlers': {'console': {'class': 'logging.StreamHandler','level': 'DEBUG','formatter': 'simple'
        },'file': {'class': 'logging.FileHandler','filename': 'logging.log','level': 'DEBUG','formatter': 'simple'
        },
        # 其他的 handler
    },
    'loggers':{'StreamLogger': {'handlers': ['console'],'level': 'DEBUG',
        },'FileLogger': {
            # 既有 console Handler,还有 file Handler
            'handlers': ['console', 'file'],'level': 'DEBUG',
        },
        # 其他的 Logger
    }
}

logging.config.dictConfig(config)
StreamLogger = logging.getLogger("StreamLogger")
FileLogger = logging.getLogger("FileLogger")
# 省略日志输出

从配置文件中获取配置信息:

常见的配置文件有 ini 格式、yaml 格式、JSON 格式,或者从网络中获取都是可以的,只要有相应的文件解析器解析配置即可,下面只展示了 ini 格式和 yaml 格式的配置。

test.ini 文件

[loggers]
keys=root,sampleLogger

[handlers]
keys=consoleHandler

[formatters]
keys=sampleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_sampleLogger]
level=DEBUG
handlers=consoleHandler
qualname=sampleLogger
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=sampleFormatter
args=(sys.stdout,)

[formatter_sampleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s

testinit.py 文件

import logging.config

logging.config.fileConfig(fname='test.ini', disable_existing_loggers=False)
logger = logging.getLogger("sampleLogger")
# 省略日志输出

test.yaml 文件

version: 1
formatters:
  simple:
    format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
  console:
    class: logging.StreamHandler
    level: DEBUG
    formatter: simple
loggers:
  simpleExample:
    level: DEBUG
    handlers: [console]
    propagate: no
root:
  level: DEBUG
  handlers: [console]

testyaml.py 文件

import logging.config
# 需要安装 pyymal 库
import yaml

with open('test.yaml', 'r') as f:
    config = yaml.safe_load(f.read())
    logging.config.dictConfig(config)

logger = logging.getLogger("sampleLogger")
# 省略日志输出

7、实战中的问题

1、中文乱码

上面的例子中日志输出都是英文内容,发现不了将日志输出到文件中会有中文乱码的问题,如何解决到这个问题呢?FileHandler 创建对象时可以设置文件编码,如果将文件编码设置为 “utf-8”(utf-8 和 utf8 等价),就可以解决中文乱码问题啦。一种方法是自定义 Logger 对象,需要写很多配置,另一种方法是使用默认配置方法 basicConfig(),传入 handlers 处理器列表对象,在其中的 handler 设置文件的编码。网上很多都是无效的方法,关键参考代码如下:

# 自定义 Logger 配置
handler = logging.FileHandler(filename="test.log", encoding="utf-8")
# 使用默认的 Logger 配置
logging.basicConfig(handlers=[logging.FileHandler("test.log", encoding="utf-8")], level=logging.DEBUG)

2、临时禁用日志输出

有时候我们又不想让日志输出,但在这后又想输出日志。如果我们打印信息用的是 print() 方法,那么就需要把所有的 print() 方法都注释掉,而使用了 logging 后,我们就有了一键开关闭日志的 “魔法”。一种方法是在使用默认配置时,给 logging.disabled() 方法传入禁用的日志级别,就可以禁止设置级别以下的日志输出了,另一种方法时在自定义 Logger 时,Logger 对象的 disable 属性设为 True,默认值是 False,也即不禁用。

logging.disable(logging.INFO)
logger.disabled = True

3、日志文件按照时间划分或者按照大小划分

如果将日志保存在一个文件中,那么时间一长,或者日志一多,单个日志文件就会很大,既不利于备份,也不利于查看。我们会想到能不能按照时间或者大小对日志文件进行划分呢?答案肯定是可以的,并且还很简单,logging 考虑到了我们这个需求。logging.handlers 文件中提供了 TimedRotatingFileHandlerRotatingFileHandler类分别可以实现按时间和大小划分。打开这个 handles 文件,可以看到还有其他功能的 Handler 类,它们都继承自基类 BaseRotatingHandler

# TimedRotatingFileHandler 类构造函数
def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None):
# RotatingFileHandler 类的构造函数
def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False)

示例代码如下:

# 每隔 1000 Byte 划分一个日志文件,备份文件为 3 个
file_handler = logging.handlers.RotatingFileHandler("test.log", mode="w", maxBytes=1000, backupCount=3, encoding="utf-8")
# 每隔 1小时 划分一个日志文件,interval 是时间间隔,备份文件为 10 个
handler2 = logging.handlers.TimedRotatingFileHandler("test.log", when="H", interval=1, backupCount=10)

Python 官网虽然说 logging 库是线程安全的,但在多进程、多线程、多进程多线程环境中仍然还有值得考虑的问题,比如,如何将日志按照进程(或线程)划分为不同的日志文件,也即一个进程(或线程)对应一个文件。由于本文篇幅有限,故不在这里做详细说明,只是起到引发读者思考的目的,这些问题我会在另一篇文章中讨论。

总结:Python logging 库设计的真的非常灵活,如果有特殊的需要还可以在这个基础的 logging 库上进行改进,创建新的 Handler 类解决实际开发中的问题。

seq2seq 模型实现聊天机器人

$
0
0

这是一个用Python+Tensorflow实现的聊天机器人程序,使用seq2seq模型训练。示例所用训练数据集是IMDB600多部电影中的英文台词对话部分,训练时间为3天左右(2012款MacBook Pro i7),目前仅支持英文。另外程序包含一个简单的Python+Flask WebUI,并实现了微信公众号对接功能。请扫码关注公众号 easybot 体验效果:

Easybot

  • 直接上代码

GitHub:  https://github.com/undersail/easybot

  • 用法说明

execute.py为Python主程序,程序有三种模式:训练、测试和服务,可通过修改配置文件   seq2seq.ini来改变模式,如训练模式:

mode = train

然后运行如下命令启动程序:

python execute.py

测试模式:

mode = test

*注意:服务模式请直接启动 webui/app.py (需预先安装 Flask 环境,见setup.sh/requirements.txt):

python webui/app.py

若需后台运行,请使用启动脚本:

sh webui/startup.sh

  • 示例效果

屏幕快照 2017-01-03 01.15.152016-12-18 2357202016-12-18 235729

  • 参考资料

GitHub原版:https://github.com/llSourcell/tensorflow_chatbot

seq2seq论文: Sequence to Sequence Learning with Neural Networks

seq2seq模型:http://blog.csdn.net/sunlylorn/article/details/50607376

递归神经网络:http://wiki.jikexueyuan.com/project/tensorflow-zh/tutorials/recurrent.html


人人恐惧AI寒冬,他却希望泡沫再破裂一次

$
0
0

AI应用落地,核心是工程问题,不是算法问题,更不是“哲学”问题。一定要特别特别“土”,踏踏实实从朴素的运维、数据库、数据清洗做起,从实际的工程中逐步演化。只有扎扎实实从工程出发,才能实事求是地发展出低成本的、有生命力的AI系统。

没有银弹,没有奇迹。都是扎扎实实的工程,多年细节的打磨才能解决一点小事。也从来没有一个所谓的伟大的想法,能跳过工程的考验就成功的。工程才是做好AI的钥匙。——鲍捷

鲍捷是谁?他是拥有近5.3W粉丝的微博博主“西瓜大丸子汤”,也是智能金融创业公司文因互联的创始人和CEO,还是知乎专栏“文因互联”的主要撰稿人。这几年鲍捷笔耕不辍,在微博、知乎、微信上撰文无数,其中不乏爆款和经典之作(比如《 确保搞砸人工智能项目的十种方法》和《 工程才是做好AI的钥匙》)。当被问及如何在繁忙的工作之余保持如此高频度的写作产出时,他只一句:“无他,唯压力尔。”正因为压力太大,天天不分昼夜地琢磨问题,自然就会想把正在思虑的问题都写下来。这些作品逐渐成为了文因互联的风格,吸引来一批志趣相投的工作伙伴,而“西瓜大丸子汤”也不再代表鲍捷个人,早已成为文因互联的重要公司资产。

鲍捷所写的文章主要有两类,一类是知识图谱技术的分析总结,另一类则是AI落地的经验之谈,这两类文章也代表了他的过去和现在:知识图谱是鲍捷进入人工智能领域学习、研究和工作几十年来最主要的研究方向,而AI应用落地则是当前他要带领文因互联去攻克的重要课题。

文章之外亦有玄机。文因互联是国内为数不多以知识图谱为核心技术的AI公司,它与其他人工智能企业有何不同?知识图谱到底能做些什么?在智能金融领域,知识图谱的商业化落地目前进展到哪了?带着这些问题,AI前线记者对鲍捷进行了专访,进一步了解文因互联在金融知识图谱的落地进展和经验,并探讨了知识图谱未来发展的可能性。

关于创业和文因互联

知识图谱其实一点都不新,如果从最早的知识工程开始算起,它几乎和人工智能这个领域的出现一样古老。2012年谷歌提出了一个叫“Knowledge Graph”的项目,知识图谱因此得名,而直到最近四、五年这一概念才被越来越多的人所知。

为什么选择创业做金融知识图谱?

鲍捷曾经表示:“不是为了一个技术而创业,而是为了解决一个问题而创业。”2011年,鲍捷选择离开学术界,2015年创办文因互联,就是为了解决知识图谱的落地问题。

为什么创业?因为只有创业、进入工业界才能解决知识图谱落地的核心问题。

鲍捷认为, 这个领域核心的问题是工程问题,本质上就是成本问题,但学术界不关心成本。他在各个地方演讲,几乎每次都会讲到”成本”两字,几乎所有的演讲都是围绕如何降低成本展开,而“成本”也是这次采访中鲍捷提到次数最多的一个词。

“为什么我们这个领域落地不了呢?我后来发现,其实我们有很好的 “发动机”,比如各种规则引擎、推理机、各种查询引擎。但是如果我们想造一辆汽车,我们需要轮子、车厢、传动装置、刹车装置,这些全都要有。所以这个领域要落地,当前的发展瓶颈是解决人工智能的传动问题。我们有了问题,也有了引擎,要解决问题必须要把中间的这些环节全部做了,而且必须极大地降低成本。把成本降到现在的1%,才能work,这就是实验室和工程的区别。 你要想商业化,核心问题不是需求问题在我看来,这是从VC到创业者犯的最大的一个错误,他们都盯着需求,但是核心问题其实不是需求问题。需求就摆在那里,问题是怎么解锁这个需求问题,而 怎么解锁需求的核心问题主要是降低成本的问题。你把成本降低到原来的1%,需求自然就能解决了。我们要想能够做到这一点,就必须在工业、企业才能做到,这就是我一开始创业的初衷。”

至于为什么选择金融领域这个方向,首要原因是金融领域拥有大量数据,其次是金融的客户非常多,有不同规模大小的客户,金融内部又有各种各样的门类,便于进行各种探索,而且也比较方便冷启动。

文因互联是一家什么样的公司?

从技术层面上讲,文因互联是一家认知智能公司,主要利用自然语言处理和知识图谱技术来解决金融领域知识的产生、管理、查询、应用的全周期问题。当前主要做的事情是给金融机构赋能,提供认知智能各个环节能力的输出,包括文档自动化阅读,用机器去理解金融文档;也包括金融知识建模和流程自动化,比如监管自动化、审计自动化、信贷自动化等。具体来讲,第一个层面是解决用机器怎么理解文档的问题,用专业术语来说就是知识提取;第二个层面是知识提取之后,如何把业务系统的流程自动化,包括智能投研,科技监管3.0,银行要做数据治理、自动化信贷流程,自动化审计,财务机器人,这些都是有了知识图谱以后就可以去做的事情。

从产品层面上讲,文因互联的产品聚焦于不同金融场景,比如最早推出的智能搜索,金融搜索;后来的自动化写报告,包括银行领域的自动化信贷报告,金融企业的CRM等,表面上看是自动化报告,本质上是将企业的知识沉淀下来并实现智能化和流程的自动化。再进行场景细分,在监管上有面向监管的产品,包括整个公告的结构化和自动化,以及后面的企业画像、预警和监管规则的执行,在银行业会有信贷的流程自动化,包括非结构化数据的数据治理,PDF文件、财务报表自动化审计和复核,信贷流程知识的建模等等。

从市场定位层面上讲,鲍捷提到了他早前提出的场景跃迁理论。他认为市场定位是不断变化的,像文因互联这样的公司不可能一步到位,因为这是一个革命的新兴市场,因此文因互联的市场定位也会随着时间的发展不断发生变化。鲍捷将现在的文因互联定位为一个做能力输出的公司,即为金融机构赋能。“中国现在有上百万的客户经理,5年或者10年之后,至少一半以上的客户经理的重复性低创造力的工作都会被机器取代,而文因互联就是在帮助银行实现这个过程。”

鲍捷认为,现在人工智能在金融领域的落地只达成了前一半,就是所谓基于 现有的结构化数据的,比如说像大数据、机器学习,已经有了一些成功的实践,但这些只能算作低枝上的桃子,而高枝上的桃子还很难摘得到。所谓高枝上的桃子主要围绕的是如何解决非结构化数据的问题,比如各种PDF文件、票据里的数据如何解锁,这块现在基本上还没有能做到的,这也是文因互联想帮助客户解决的核心问题。文因互联当前在监管上做了很多工作,鲍捷认为这可能是解锁这个场景早期最主要的一个推动力。

现在很多公司都在宣传各种名为“XX大脑”的行业人工智能解决方案,相比其他公司的“金融大脑”,文因互联正在做的“金融神经系统”又有什么不同之处?

鲍捷表示,文因互联要解决的不仅是一个机构的问题,而是机构和机构之间互通的问题。虽然从当前的产品来看,文因互联其实也在帮助不同的机构构建他们的“大脑”,但在鲍捷看来, 智能金融真正最有价值的事情,在于把机构串起来,构造一个机构之间的金融数据高速公路,这才是一个能创造出万亿产值的方向。这是文因互联长期的努力方向。

“文因互联要构造的金融神经系统,是指把中国几千家金融机构连通起来,当然不是我们自己一家来做,可能到时候会有上百家不同的机构一起做,文因互联只是负责其中一部分。现在中国金融的脉搏跳动可能还是以天、以周,有时候甚至是以月为单位来计,十年之后中国金融的脉搏应该是以秒为单位来计算的。到那时候, 上百万家金融机构和企业之间的数据交换、数据的互通、文档的互通,都可以达到几乎实时的状态,这是我们努力的目标。

知识图谱能为各行业做什么?

“知识图谱就像数据库,用户可能感知不到,但没人离得了它”

2016年大家开始谈AI技术,2017年话题变为应用场景,到了2018年,业界更关注的是技术到底能带来哪些可衡量的用户价值。当前知识图谱技术的落地应用多见于金融行业,而它所带来的最直观的价值是十倍甚至百倍的效率提升。

原来交易所做公告处理,即使团队没日没夜工作也只能处理一小部分,有了知识图谱的帮助之后,至少可以节约80%的重复性劳动;原来银行做一次小微贷款可能要花一个月时间,现在有的银行半天就可以做完,主要是因为有了企业画像,而企业画像又怎么做到的呢?背后靠的就是知识图谱的力量,通过知识图谱把各种担保链条、违规情况挖掘出来了。其次,知识图谱可以帮助金融机构扩大现在的业务。以银行为例,要从一万个企业客户扩张到五万个客户,通常靠的不是把客户经理扩大到五倍,而是通过提高效率来做到这一点,知识图谱技术就是这里面核心的一环。

知识图谱之所以在金融行业落地产品多,很大一部分是因为这个行业方便团队去做事情。首先,数据比较全,因为有强制性披露,而且场景相对比较明晰。像财务分析就很适合团队快速入手,财务分析不会涉及到太深入的常识知识或者领域知识,因为它是有国家标准的。接下来就可以从财务分析,到行业分析,到宏观分析一层一层往上做。鲍捷表示,金融还有个特别大的好处,就是它的数据有很强的渗透性,基本上能够把金融这个行业做好,就可以很自然地渗透到很多其他应用,这对公司下一步突破自己的天花板有好处。

当然,金融行业并非唯一适合知识图谱落地的行业。除了文因互联现在主攻的金融,鲍捷未来还看好知识图谱在医疗、法律、国防等行业的应用和商业化落地,而这些行业的知识图谱落地也是美国早就证明可行的方向。

鲍捷表示,知识图谱从本质上来说, 在当前的语境下是指结构化数据的应用,特别是把网页数据转化成结构化数据这个过程,如果按照这个标准来看,那知识图谱应用的成功案例就太多了。现在每个人的手机上基本都有一个问答引擎,很多人家里会有智能音箱,我们会有各种听歌机器人、智能后视镜,所有这些全都是知识图谱在应用层面的实例,没有知识图谱就不会有这些东西。其实还有很多应用,用户自己不一定知道背后的技术是什么。“ 知识图谱很像数据库,很少有公司会在最终产品里面说他用了Oracle数据库,实际上现在很多产品背后都在用知识图谱,像搜索引擎是最典型的了,搜索引擎没有知识图谱根本不可能做到现在这样。”

反过来看,也 不是每一个应用都适合知识图谱。知识图谱相对机器学习更适合需要快速冷启动的应用,而且它可解释性很好。对于很多像金融行业Mission Critical的应用,必须是可解释的,不能给出一个投资策略却不能告诉用户为什么,这种情况就非常适合使用 知识图谱。还有很多应用像搜索、问答、客服,必须精确理解用户在说什么,这是机器学习本身解决不了的问题,只有知识图谱能解决。

知识图谱商业化落地进展到哪了?

“知识图谱应用,中国和美国相比既领先也落后”

提到AI,中国和美国在技术和应用的进展情况差异一直是备受关注的话题。当被问及现阶段知识图谱技术在行业应用上国内外的进展有何差异时,鲍捷给出了一个非常有哲理的回答。

“我们既领先也落后,是辩证的,这是一件事情的两面。首先,知识图谱早在十几年前就已经被应用了。知识图谱的第一波商业化是在2005-2006年就开始的,2005年到2008年是知识图谱的第一波应用,后来被金融危机打断了一段时间。到了2012年又开始了, 现在属于第二波应用。从这个角度来说,中国是落后的。2012-2013年的时候,一些大厂开始了知识图谱的实践,这是国内第一波应用,比美国落后了差不多十年。后来那一波灭了,到2015-2016年的时候才陆陆续续又有一些新的公司出现。专门做知识图谱的公司非常少,当然我的信息可能不完备,但据我所知,国内正儿八经以知识图谱作为核心基础的公司也就三四家,这是认真做的。从这个角度来说,我们确实发展的比美国慢,但是我们比欧洲快。”

“从另一个角度来说,中国又不比美国慢。国内 现在在人工智能上的很多应用真的比美国快,美国没有那么多应用。我在华尔街的同学和老同事很多,他们认为国内在智能金融上的发展,如无人银行、信贷自动化、智能搜索处于较为领先的地位。由于国内场景丰富,实业提出了很多鲜活落地需求,使国内的发展比美国更快,场景更丰富。

但是 美国涉及的面非常广。经过这十几年的发展,美国几乎在所有行业都有了知识图谱的应用,而且每个行业都已经出现了有竞争力的公司,比如说石油、医药、政府、化工等,每一个行业都有有竞争力的知识图谱公司。中国现在还没有多少, 到目前为止金融有一些,法律有一些,医疗有一些,但是真正以知识图谱作为核心技术(核心的标准是指公司有科班出身的知识图谱负责人,公司掌握知识图谱的核心技术),满足条件的企业数量非常少,有些行业完全是一片空白。我认为 从行业广度的角度,中国大概要再发展十年左右才能赶上美国。”

“大厂关注头部问题,小厂关注垂直问题”

同样是做知识图谱,大公司和小公司之间有何差异?小公司的优势在哪里?

鲍捷表示, 大厂关注的是头部问题,小厂关注的是专业度更高的问题,大家在投入上肯定是不一样的,专注度也不一样。“大厂也会关注我们的问题,比如金融,但我可以扎两百个人在金融,一般的大厂还下不了决心扎两百人进去,坚定不移地做。从这个角度来说,小公司只要能 抓住一个点是可以做得比大厂更好的。但那些大型头部问题,比如大型问答系统、大型搜索引擎,我们肯定不会去做这方面的应用,在这些问题上大厂可能会做得比其他人更好。”

对于文因互联当前在金融知识图谱领域的落地进展,鲍捷认为是成功的:“我们已经获得了客户的认可,现在几乎不需要做任何商务拓展工作,都是客户主动来找我们。市场口碑已经建立起来了,现在整个工作是完全饱和的,制约我们发展的唯一因素就是团队不够大。头两年大家可能比较迷茫,但现在我们已经比较清楚客户的需求,包括整个行业的逻辑和大方向,接下来的问题是如何加速执行。我们的很多认识是早于同行一年甚至两年的。”

“投资现在就处于寒冬阶段,寒冬就是最好的状态”

鲍捷在之前的文章《 确保搞砸人工智能项目的十种方法》中曾说,知识图谱大概率到2030年能够实现,但是在近期的另一个采访中又表示2018年第四季度就是智能金融的决战季。对于这两个看似自相矛盾的说法,鲍捷进一步做了解释。

“2030年实现知识图谱是指大的宏观愿景,就如我刚才说的, 在中国光是做到行业渗透就要花十年时间,现在是2018年,到2030年也只有12年时间, 12年的时间能够做到这一点就已经谢天谢地了。要把一个行业支撑起来至少得要一万名人才,现在中国这个领域的人才一千个都不到,光人才培养就得花10年时间。至于智能金融的竞争,现在已经到了决战的时候,我认为一个很重要的点就是,因为现在在寒冬状态,我觉得 寒冬是最好的状态。”

鲍捷认为,今年是投资的寒冬年,新增的领域内公司很少,但正是在这个季节,领域内发生了非常多有利于下一步发展的变化,特别是金融宏观层面,从监管机构和金融机构内业务变革中能感受到有强有力的脉搏跳动。你亲身去做,就能真切感受到,这是你坐在书斋里或者读行业报告读不出来的。从认识到这些变化到建立有战斗力的组织去做,要几年时间。我们现在做的很多事情是2016年就预见到,在几乎所有人都不太明白的时候就开始做了。2019年,需求交付的时机和组织已经成熟了。但是如果之前狐疑、犹豫而不敢投入的,现在投入也要两年后才能形成战斗力,这可能就已经太晚了。

知识图谱商业化应用什么最难?

“知识图谱是发动机,但只有知识图谱远远不够”

只有好技术也可能赚不了钱。鲍捷表示, 知识图谱本身并不能成为一个把客户服务好的原因,因为 知识图谱从某种程度上来说是数据库技术的一个前进所有的行业都需要数据库,但是单纯用数据库是没有办法建立产品或者商业模式的,知识图谱也是一样。核心是场景的落地问题,这个问题就不仅仅是一个单纯的技术问题了,本质上是传动。“知识图谱是一个很好的发动机,但是想把这种发动机的力量传到轮子上去,还要加上一大堆各种其他的东西, 目前文因互联主要的工作就是在做其他的那些东西。” 比如基础金融数据云、流程自动化技术、报告自动化技术、金融机器人问答、金融搜索引擎等。

而打造这些“传动装置”,需要投入大量的时间。通常一个领域差不多要花十年的时间才能落地,这其中需要经过很多轮迭代。第一步是要把现有的基础数据汇总在一起,这可能就要花两三年的时间。但这还只是低枝上的桃子,把这些桃子摘下来之后,剩下的就是苦活。苦活可能涉及到大量的文本分析;把文本分析解决掉之后就是规则,要把业务规则做进去又要花两三年时间;接下来是场景落地,一步一步走完整个过程正常都要花十年时间。”  每一步的中间结果要拿出来做商业化,一步步做场景跃迁,随着结果的日渐丰富就能逐渐解锁更大的场景。

这个过程中,团队、客户、投资人,绝大多数人一开始都是持怀疑态度,肯定要经历一个大浪淘沙的淘汰过程,最后留下来少数相信你的人跟你往前走。

“知识图谱落地,最核心的就是成本问题”

除了时间以外,鲍捷认为知识图谱落地最核心的问题是成本问题。

“知识图谱,你只要愿意砸足够多的钱,什么问题都能解决,没有什么理论上难的问题。什么都能做,但是你要在有限的资金内把问题给解决了,这是最难的。”

而鲍捷所指的成本不光是钱,他认为 最大的成本来自于人的认知的冲突,包括客户和企业的认知、开发人员和领域内各种专家的认知。知识图谱落地当中最大的问题就是如何降低人的认知冲突的成本。

“知识图谱是为了人,不是为了机器。而且人是会犯错误的,知识会演化的。大多数人完全不具备面向知识图谱进行思考的能力,包括开发人员在内,这是成本的最大的来源。抓住这个最核心的问题就能够搞定知识图谱。但理解刚才我说的这句话呢,在没有足够的工程经验之前,你又听不懂,所以恐怕没有什么能够让你做得更快的途径。只能不断地去做,实践、实践、再实践,失败一百次之后你就懂了。”

知识图谱和人工智能的未来会怎样?

“基础理论不会有太大的变化,成本、工程、工具才是主要障碍”

肖仰华教授在他近期的一篇文章中提到,知识图谱技术与各行业的深度融合已经成为一个重要趋势。

鲍捷表示,不同行业、不同垂直领域的知识图谱从逻辑上来说是可以连通的,但目前还没有到那个节点。“不过这不是技术问题”,鲍捷又一次强调,“你只要有足够多的钱,这些问题都能做到,是成本和规模问题,等知识图谱领域全行业投入一百亿人民币的时候,这些事情就都能做好了。”

鲍捷始终认为,成本、工程、工具才是最主要的障碍,基础理论其实没什么太大的变化。“可能其他老师会有不同的观点,他们可能认为是表达力问题,或者我们不能够真正地去刻画知识、真正地产生智能。问题是什么叫 ‘真正的’?只要砸足够多的钱, ‘真正的’都会到。关键是钱从哪里来? 你把成本降下来钱就来了。”

“人工智能的泡沫已经破裂了两次,我希望它再破裂第三次”

最近人工智能寒冬说又开始兴起,也有不少人唱衰说人工智能的泡沫马上就要破了。但从鲍捷一直以来在社交平台上的发言和所发表的文章来看,能感觉到他对于人工智能总体上信心很足。

鲍捷认同人工智能存在泡沫,但他觉得泡沫是好事。“人工智能永远是在泡沫和寒冬中振荡,这是我们的宿命,我们不可能打破这一点。人工智能是最美妙的东西,也是最邪恶的东西,所以人类永远都会像追求爱情一样去追求它,又会不断地失恋。我们人类就是这样一种生物,这是必然的。但泡沫破裂是好事,可以把劣质的竞争者都驱逐出去。我希望它破裂,我已经经历过两次破裂了,我希望再破裂第三次。”

对于文因互联、知识图谱和人工智能未来的发展,鲍捷满怀期待。

对于文因互联:“长期的目标太大,短期内我们要扎扎实实地把金融客户服务好,比如真正做好帮助银行实现流程自动化、极大地提高效率这件事,比如说帮客户一年扩张一万个中小企业的贷款客户,或者帮助客户减少一半的客户经理数量,这就是文因互联今后两三年内要做到的事情。”

对于知识图谱:“这个领域肯定还会和历史上一样,不停在泡沫和寒冬之间振荡, 估计再振荡两三次都是正常的伟大的公司都是在振荡中始终坚持下来的那些公司。”

对于人工智能:“人工智能从来没有停滞过,不管它是高潮还是泡沫,人工智能一直在前进。所以对于真正相信这个技术的人,不存在冬天,一天都不存在。”

采访嘉宾介绍

鲍捷,文因互联 CEO,联合创始人。曾是三星美国研发中心研究员,伦斯勒理工学院(RPI)博士后。目前担任中国中文信息学会语言与知识计算专委会委员,W3C 顾问委员会委员,中国计算机协会会刊编委,中文开放知识图谱联盟(OpenKG)发起人之一。研究领域涉及人工智能诸多方向,如自然语言处理、语义网、机器学习、描述逻辑、信息论、神经网络、图像识别等,已发表 70 多篇论文。


Redis基础、高级特性与性能调优

$
0
0

本文将从Redis的基本特性入手,通过讲述Redis的数据结构和主要命令对Redis的基本能力进行直观介绍。之后概览Redis提供的高级能力,并在部署、维护、性能调优等多个方面进行更深入的介绍和指导。
本文适合使用Redis的普通开发人员,以及对Redis进行选型、架构设计和性能调优的架构设计人员。

目录

  • 概述
  • Redis的数据结构和相关常用命令
  • 数据持久化
  • 内存管理与数据淘汰机制
  • Pipelining
  • 事务与Scripting
  • Redis性能调优
  • 主从复制与集群分片
  • Redis Java客户端的选择

概述

Redis是一个开源的,基于内存的结构化数据存储媒介,可以作为数据库、缓存服务或消息服务使用。
Redis支持多种数据结构,包括字符串、哈希表、链表、集合、有序集合、位图、Hyperloglogs等。
Redis具备LRU淘汰、事务实现、以及不同级别的硬盘持久化等能力,并且支持副本集和通过Redis Sentinel实现的高可用方案,同时还支持通过Redis Cluster实现的数据自动分片能力。

Redis的主要功能都基于单线程模型实现,也就是说Redis使用一个线程来服务所有的客户端请求,同时Redis采用了非阻塞式IO,并精细地优化各种命令的算法时间复杂度,这些信息意味着:

  • Redis是线程安全的(因为只有一个线程),其所有操作都是原子的,不会因并发产生数据异常
  • Redis的速度非常快(因为使用非阻塞式IO,且大部分命令的算法时间复杂度都是O(1))
  • 使用高耗时的Redis命令是很危险的,会占用唯一的一个线程的大量处理时间,导致所有的请求都被拖慢。(例如时间复杂度为O(N)的KEYS命令,严格禁止在生产环境中使用)

Redis的数据结构和相关常用命令

本节中将介绍Redis支持的主要数据结构,以及相关的常用Redis命令。本节只对Redis命令进行扼要的介绍,且只列出了较常用的命令。如果想要了解完整的Redis命令集,或了解某个命令的详细使用方法,请参考官方文档: https://redis.io/commands

Key

Redis采用Key-Value型的基本数据结构,任何二进制序列都可以作为Redis的Key使用(例如普通的字符串或一张JPEG图片)
关于Key的一些注意事项:

  • 不要使用过长的Key。例如使用一个1024字节的key就不是一个好主意,不仅会消耗更多的内存,还会导致查找的效率降低
  • Key短到缺失了可读性也是不好的,例如”u1000flw”比起”user:1000:followers”来说,节省了寥寥的存储空间,却引发了可读性和可维护性上的麻烦
  • 最好使用统一的规范来设计Key,比如”object-type:id:attr”,以这一规范设计出的Key可能是”user:1000″或”comment:1234:reply-to”
  • Redis允许的最大Key长度是512MB(对Value的长度限制也是512MB)

String

String是Redis的基础数据类型,Redis没有Int、Float、Boolean等数据类型的概念,所有的基本类型在Redis中都以String体现。

与String相关的常用命令:

  • SET:为一个key设置value,可以配合EX/PX参数指定key的有效期,通过NX/XX参数针对key是否存在的情况进行区别操作,时间复杂度O(1)
  • GET:获取某个key对应的value,时间复杂度O(1)
  • GETSET:为一个key设置value,并返回该key的原value,时间复杂度O(1)
  • MSET:为多个key设置value,时间复杂度O(N)
  • MSETNX:同MSET,如果指定的key中有任意一个已存在,则不进行任何操作,时间复杂度O(N)
  • MGET:获取多个key对应的value,时间复杂度O(N)

上文提到过,Redis的基本数据类型只有String,但Redis可以把String作为整型或浮点型数字来使用,主要体现在INCR、DECR类的命令上:

  • INCR:将key对应的value值自增1,并返回自增后的值。只对可以转换为整型的String数据起作用。时间复杂度O(1)
  • INCRBY:将key对应的value值自增指定的整型数值,并返回自增后的值。只对可以转换为整型的String数据起作用。时间复杂度O(1)
  • DECR/DECRBY:同INCR/INCRBY,自增改为自减。

INCR/DECR系列命令要求操作的value类型为String,并可以转换为64位带符号的整型数字,否则会返回错误。
也就是说,进行INCR/DECR系列命令的value,必须在[-2^63 ~ 2^63 – 1]范围内。

前文提到过,Redis采用单线程模型,天然是线程安全的,这使得INCR/DECR命令可以非常便利的实现高并发场景下的精确控制。

例1:库存控制

在高并发场景下实现库存余量的精准校验,确保不出现超卖的情况。

设置库存总量:

SET inv:remain "100"

库存扣减+余量校验:

DECR inv:remain

当DECR命令返回值大于等于0时,说明库存余量校验通过,如果返回小于0的值,则说明库存已耗尽。

假设同时有300个并发请求进行库存扣减,Redis能够确保这300个请求分别得到99到-200的返回值,每个请求得到的返回值都是唯一的,绝对不会找出现两个请求得到一样的返回值的情况。

例2:自增序列生成

实现类似于RDBMS的Sequence功能,生成一系列唯一的序列号

设置序列起始值:

SET sequence "10000"

获取一个序列值:

INCR sequence

直接将返回值作为序列使用即可。

获取一批(如100个)序列值:

INCRBY sequence 100

假设返回值为N,那么[N – 99 ~ N]的数值都是可用的序列值。

当多个客户端同时向Redis申请自增序列时,Redis能够确保每个客户端得到的序列值或序列范围都是全局唯一的,绝对不会出现不同客户端得到了重复的序列值的情况。

List

Redis的List是链表型的数据结构,可以使用LPUSH/RPUSH/LPOP/RPOP等命令在List的两端执行插入元素和弹出元素的操作。虽然List也支持在特定index上插入和读取元素的功能,但其时间复杂度较高(O(N)),应小心使用。

与List相关的常用命令:

  • LPUSH:向指定List的左侧(即头部)插入1个或多个元素,返回插入后的List长度。时间复杂度O(N),N为插入元素的数量
  • RPUSH:同LPUSH,向指定List的右侧(即尾部)插入1或多个元素
  • LPOP:从指定List的左侧(即头部)移除一个元素并返回,时间复杂度O(1)
  • RPOP:同LPOP,从指定List的右侧(即尾部)移除1个元素并返回
  • LPUSHX/RPUSHX:与LPUSH/RPUSH类似,区别在于,LPUSHX/RPUSHX操作的key如果不存在,则不会进行任何操作
  • LLEN:返回指定List的长度,时间复杂度O(1)
  • LRANGE:返回指定List中指定范围的元素(双端包含,即LRANGE key 0 10会返回11个元素),时间复杂度O(N)。应尽可能控制一次获取的元素数量,一次获取过大范围的List元素会导致延迟,同时对长度不可预知的List,避免使用LRANGE key 0 -1这样的完整遍历操作。

应谨慎使用的List相关命令:

  • LINDEX:返回指定List指定index上的元素,如果index越界,返回nil。index数值是回环的,即-1代表List最后一个位置,-2代表List倒数第二个位置。时间复杂度O(N)
  • LSET:将指定List指定index上的元素设置为value,如果index越界则返回错误,时间复杂度O(N),如果操作的是头/尾部的元素,则时间复杂度为O(1)
  • LINSERT:向指定List中指定元素之前/之后插入一个新元素,并返回操作后的List长度。如果指定的元素不存在,返回-1。如果指定key不存在,不会进行任何操作,时间复杂度O(N)

由于Redis的List是链表结构的,上述的三个命令的算法效率较低,需要对List进行遍历,命令的耗时无法预估,在List长度大的情况下耗时会明显增加,应谨慎使用。

换句话说,Redis的List实际是设计来用于实现队列,而不是用于实现类似ArrayList这样的列表的。如果你不是想要实现一个双端出入的队列,那么请尽量不要使用Redis的List数据结构。

为了更好支持队列的特性,Redis还提供了一系列阻塞式的操作命令,如BLPOP/BRPOP等,能够实现类似于BlockingQueue的能力,即在List为空时,阻塞该连接,直到List中有对象可以出队时再返回。针对阻塞类的命令,此处不做详细探讨,请参考官方文档( https://redis.io/topics/data-types-intro) 中”Blocking operations on lists”一节。

Hash

Hash即哈希表,Redis的Hash和传统的哈希表一样,是一种field-value型的数据结构,可以理解成将HashMap搬入Redis。
Hash非常适合用于表现对象类型的数据,用Hash中的field对应对象的field即可。
Hash的优点包括:

  • 可以实现二元查找,如”查找ID为1000的用户的年龄”
  • 比起将整个对象序列化后作为String存储的方法,Hash能够有效地减少网络传输的消耗
  • 当使用Hash维护一个集合时,提供了比List效率高得多的随机访问命令

与Hash相关的常用命令:

  • HSET:将key对应的Hash中的field设置为value。如果该Hash不存在,会自动创建一个。时间复杂度O(1)
  • HGET:返回指定Hash中field字段的值,时间复杂度O(1)
  • HMSET/HMGET:同HSET和HGET,可以批量操作同一个key下的多个field,时间复杂度:O(N),N为一次操作的field数量
  • HSETNX:同HSET,但如field已经存在,HSETNX不会进行任何操作,时间复杂度O(1)
  • HEXISTS:判断指定Hash中field是否存在,存在返回1,不存在返回0,时间复杂度O(1)
  • HDEL:删除指定Hash中的field(1个或多个),时间复杂度:O(N),N为操作的field数量
  • HINCRBY:同INCRBY命令,对指定Hash中的一个field进行INCRBY,时间复杂度O(1)

应谨慎使用的Hash相关命令:

  • HGETALL:返回指定Hash中所有的field-value对。返回结果为数组,数组中field和value交替出现。时间复杂度O(N)
  • HKEYS/HVALS:返回指定Hash中所有的field/value,时间复杂度O(N)

上述三个命令都会对Hash进行完整遍历,Hash中的field数量与命令的耗时线性相关,对于尺寸不可预知的Hash,应严格避免使用上面三个命令,而改为使用HSCAN命令进行游标式的遍历,具体请见 https://redis.io/commands/scan

Set

Redis Set是无序的,不可重复的String集合。

与Set相关的常用命令:

  • SADD:向指定Set中添加1个或多个member,如果指定Set不存在,会自动创建一个。时间复杂度O(N),N为添加的member个数
  • SREM:从指定Set中移除1个或多个member,时间复杂度O(N),N为移除的member个数
  • SRANDMEMBER:从指定Set中随机返回1个或多个member,时间复杂度O(N),N为返回的member个数
  • SPOP:从指定Set中随机移除并返回count个member,时间复杂度O(N),N为移除的member个数
  • SCARD:返回指定Set中的member个数,时间复杂度O(1)
  • SISMEMBER:判断指定的value是否存在于指定Set中,时间复杂度O(1)
  • SMOVE:将指定member从一个Set移至另一个Set

慎用的Set相关命令:

  • SMEMBERS:返回指定Hash中所有的member,时间复杂度O(N)
  • SUNION/SUNIONSTORE:计算多个Set的并集并返回/存储至另一个Set中,时间复杂度O(N),N为参与计算的所有集合的总member数
  • SINTER/SINTERSTORE:计算多个Set的交集并返回/存储至另一个Set中,时间复杂度O(N),N为参与计算的所有集合的总member数
  • SDIFF/SDIFFSTORE:计算1个Set与1或多个Set的差集并返回/存储至另一个Set中,时间复杂度O(N),N为参与计算的所有集合的总member数

上述几个命令涉及的计算量大,应谨慎使用,特别是在参与计算的Set尺寸不可知的情况下,应严格避免使用。可以考虑通过SSCAN命令遍历获取相关Set的全部member(具体请见 https://redis.io/commands/scan),如果需要做并集/交集/差集计算,可以在客户端进行,或在不服务实时查询请求的Slave上进行。

Sorted Set

Redis Sorted Set是有序的、不可重复的String集合。Sorted Set中的每个元素都需要指派一个分数(score),Sorted Set会根据score对元素进行升序排序。如果多个member拥有相同的score,则以字典序进行升序排序。

Sorted Set非常适合用于实现排名。

Sorted Set的主要命令:

  • ZADD:向指定Sorted Set中添加1个或多个member,时间复杂度O(Mlog(N)),M为添加的member数量,N为Sorted Set中的member数量
  • ZREM:从指定Sorted Set中删除1个或多个member,时间复杂度O(Mlog(N)),M为删除的member数量,N为Sorted Set中的member数量
  • ZCOUNT:返回指定Sorted Set中指定score范围内的member数量,时间复杂度:O(log(N))
  • ZCARD:返回指定Sorted Set中的member数量,时间复杂度O(1)
  • ZSCORE:返回指定Sorted Set中指定member的score,时间复杂度O(1)
  • ZRANK/ZREVRANK:返回指定member在Sorted Set中的排名,ZRANK返回按升序排序的排名,ZREVRANK则返回按降序排序的排名。时间复杂度O(log(N))
  • ZINCRBY:同INCRBY,对指定Sorted Set中的指定member的score进行自增,时间复杂度O(log(N))

慎用的Sorted Set相关命令:

  • ZRANGE/ZREVRANGE:返回指定Sorted Set中指定排名范围内的所有member,ZRANGE为按score升序排序,ZREVRANGE为按score降序排序,时间复杂度O(log(N)+M),M为本次返回的member数
  • ZRANGEBYSCORE/ZREVRANGEBYSCORE:返回指定Sorted Set中指定score范围内的所有member,返回结果以升序/降序排序,min和max可以指定为-inf和+inf,代表返回所有的member。时间复杂度O(log(N)+M)
  • ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除Sorted Set中指定排名范围/指定score范围内的所有member。时间复杂度O(log(N)+M)

上述几个命令,应尽量避免传递[0 -1]或[-inf +inf]这样的参数,来对Sorted Set做一次性的完整遍历,特别是在Sorted Set的尺寸不可预知的情况下。可以通过ZSCAN命令来进行游标式的遍历(具体请见 https://redis.io/commands/scan),或通过LIMIT参数来限制返回member的数量(适用于ZRANGEBYSCORE和ZREVRANGEBYSCORE命令),以实现游标式的遍历。

Bitmap和HyperLogLog

Redis的这两种数据结构相较之前的并不常用,在本文中只做简要介绍,如想要详细了解这两种数据结构与其相关的命令,请参考官方文档 https://redis.io/topics/data-types-intro中的相关章节

Bitmap在Redis中不是一种实际的数据类型,而是一种将String作为Bitmap使用的方法。可以理解为将String转换为bit数组。使用Bitmap来存储true/false类型的简单数据极为节省空间。

HyperLogLogs是一种主要用于数量统计的数据结构,它和Set类似,维护一个不可重复的String集合,但是HyperLogLogs并不维护具体的member内容,只维护member的个数。也就是说,HyperLogLogs只能用于计算一个集合中不重复的元素数量,所以它比Set要节省很多内存空间。

其他常用命令

  • EXISTS:判断指定的key是否存在,返回1代表存在,0代表不存在,时间复杂度O(1)
  • DEL:删除指定的key及其对应的value,时间复杂度O(N),N为删除的key数量
  • EXPIRE/PEXPIRE:为一个key设置有效期,单位为秒或毫秒,时间复杂度O(1)
  • TTL/PTTL:返回一个key剩余的有效时间,单位为秒或毫秒,时间复杂度O(1)
  • RENAME/RENAMENX:将key重命名为newkey。使用RENAME时,如果newkey已经存在,其值会被覆盖;使用RENAMENX时,如果newkey已经存在,则不会进行任何操作,时间复杂度O(1)
  • TYPE:返回指定key的类型,string, list, set, zset, hash。时间复杂度O(1)
  • CONFIG GET:获得Redis某配置项的当前值,可以使用*通配符,时间复杂度O(1)
  • CONFIG SET:为Redis某个配置项设置新值,时间复杂度O(1)
  • CONFIG REWRITE:让Redis重新加载redis.conf中的配置

数据持久化

Redis提供了将数据定期自动持久化至硬盘的能力,包括RDB和AOF两种方案,两种方案分别有其长处和短板,可以配合起来同时运行,确保数据的稳定性。

必须使用数据持久化吗?

Redis的数据持久化机制是可以关闭的。如果你只把Redis作为缓存服务使用,Redis中存储的所有数据都不是该数据的主体而仅仅是同步过来的备份,那么可以关闭Redis的数据持久化机制。
但通常来说,仍然建议至少开启RDB方式的数据持久化,因为:

  • RDB方式的持久化几乎不损耗Redis本身的性能,在进行RDB持久化时,Redis主进程唯一需要做的事情就是fork出一个子进程,所有持久化工作都由子进程完成
  • Redis无论因为什么原因crash掉之后,重启时能够自动恢复到上一次RDB快照中记录的数据。这省去了手工从其他数据源(如DB)同步数据的过程,而且要比其他任何的数据恢复方式都要快
  • 现在硬盘那么大,真的不缺那一点地方

RDB

采用RDB持久方式,Redis会定期保存数据快照至一个rbd文件中,并在启动时自动加载rdb文件,恢复之前保存的数据。可以在配置文件中配置Redis进行快照保存的时机:

save [seconds] [changes]

意为在[seconds]秒内如果发生了[changes]次数据修改,则进行一次RDB快照保存,例如

save 60 100

会让Redis每60秒检查一次数据变更情况,如果发生了100次或以上的数据变更,则进行RDB快照保存。
可以配置多条save指令,让Redis执行多级的快照保存策略。
Redis默认开启RDB快照,默认的RDB策略如下:

save 900 1
save 300 10
save 60 10000

也可以通过 BGSAVE命令手工触发RDB快照保存。

RDB的优点:

  • 对性能影响最小。如前文所述,Redis在保存RDB快照时会fork出子进程进行,几乎不影响Redis处理客户端请求的效率。
  • 每次快照会生成一个完整的数据快照文件,所以可以辅以其他手段保存多个时间点的快照(例如把每天0点的快照备份至其他存储媒介中),作为非常可靠的灾难恢复手段。
  • 使用RDB文件进行数据恢复比使用AOF要快很多。

RDB的缺点:

  • 快照是定期生成的,所以在Redis crash时或多或少会丢失一部分数据。
  • 如果数据集非常大且CPU不够强(比如单核CPU),Redis在fork子进程时可能会消耗相对较长的时间(长至1秒),影响这期间的客户端请求。

AOF

采用AOF持久方式时,Redis会把每一个写请求都记录在一个日志文件里。在Redis重启时,会把AOF文件中记录的所有写操作顺序执行一遍,确保数据恢复到最新。

AOF默认是关闭的,如要开启,进行如下配置:

appendonly yes

AOF提供了三种fsync配置,always/everysec/no,通过配置项[appendfsync]指定:

  • appendfsync no:不进行fsync,将flush文件的时机交给OS决定,速度最快
  • appendfsync always:每写入一条日志就进行一次fsync操作,数据安全性最高,但速度最慢
  • appendfsync everysec:折中的做法,交由后台线程每秒fsync一次

随着AOF不断地记录写操作日志,必定会出现一些无用的日志,例如某个时间点执行了命令 SET key1 “abc”,在之后某个时间点又执行了 SET key1 “bcd”,那么第一条命令很显然是没有用的。大量的无用日志会让AOF文件过大,也会让数据恢复的时间过长。
所以Redis提供了AOF rewrite功能,可以重写AOF文件,只保留能够把数据恢复到最新状态的最小写操作集。
AOF rewrite可以通过 BGREWRITEAOF命令触发,也可以配置Redis定期自动进行:

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

上面两行配置的含义是,Redis在每次AOF rewrite时,会记录完成rewrite后的AOF日志大小,当AOF日志大小在该基础上增长了100%后,自动进行AOF rewrite。同时如果增长的大小没有达到64mb,则不会进行rewrite。

AOF的优点:

  • 最安全,在启用appendfsync always时,任何已写入的数据都不会丢失,使用在启用appendfsync everysec也至多只会丢失1秒的数据。
  • AOF文件在发生断电等问题时也不会损坏,即使出现了某条日志只写入了一半的情况,也可以使用redis-check-aof工具轻松修复。
  • AOF文件易读,可修改,在进行了某些错误的数据清除操作后,只要AOF文件没有rewrite,就可以把AOF文件备份出来,把错误的命令删除,然后恢复数据。

AOF的缺点:

  • AOF文件通常比RDB文件更大
  • 性能消耗比RDB高
  • 数据恢复速度比RDB慢

内存管理与数据淘汰机制

最大内存设置

默认情况下,在32位OS中,Redis最大使用3GB的内存,在64位OS中则没有限制。

在使用Redis时,应该对数据占用的最大空间有一个基本准确的预估,并为Redis设定最大使用的内存。否则在64位OS中Redis会无限制地占用内存(当物理内存被占满后会使用swap空间),容易引发各种各样的问题。

通过如下配置控制Redis使用的最大内存:

maxmemory 100mb

在内存占用达到了maxmemory后,再向Redis写入数据时,Redis会:

  • 根据配置的数据淘汰策略尝试淘汰数据,释放空间
  • 如果没有数据可以淘汰,或者没有配置数据淘汰策略,那么Redis会对所有写请求返回错误,但读请求仍然可以正常执行

在为Redis设置maxmemory时,需要注意:

  • 如果采用了Redis的主从同步,主节点向从节点同步数据时,会占用掉一部分内存空间,如果maxmemory过于接近主机的可用内存,导致数据同步时内存不足。所以设置的maxmemory不要过于接近主机可用的内存,留出一部分预留用作主从同步。

数据淘汰机制

Redis提供了5种数据淘汰策略:

  • volatile-lru:使用LRU算法进行数据淘汰(淘汰上次使用时间最早的,且使用次数最少的key),只淘汰设定了有效期的key
  • allkeys-lru:使用LRU算法进行数据淘汰,所有的key都可以被淘汰
  • volatile-random:随机淘汰数据,只淘汰设定了有效期的key
  • allkeys-random:随机淘汰数据,所有的key都可以被淘汰
  • volatile-ttl:淘汰剩余有效期最短的key

最好为Redis指定一种有效的数据淘汰策略以配合maxmemory设置,避免在内存使用满后发生写入失败的情况。

一般来说,推荐使用的策略是volatile-lru,并辨识Redis中保存的数据的重要性。对于那些重要的,绝对不能丢弃的数据(如配置类数据等),应不设置有效期,这样Redis就永远不会淘汰这些数据。对于那些相对不是那么重要的,并且能够热加载的数据(比如缓存最近登录的用户信息,当在Redis中找不到时,程序会去DB中读取),可以设置上有效期,这样在内存不够时Redis就会淘汰这部分数据。

配置方法:

maxmemory-policy volatile-lru   #默认是noeviction,即不进行数据淘汰

Pipelining

Pipelining

Redis提供许多批量操作的命令,如MSET/MGET/HMSET/HMGET等等,这些命令存在的意义是减少维护网络连接和传输数据所消耗的资源和时间。
例如连续使用5次SET命令设置5个不同的key,比起使用一次MSET命令设置5个不同的key,效果是一样的,但前者会消耗更多的RTT(Round Trip Time)时长,永远应优先使用后者。

然而,如果客户端要连续执行的多次操作无法通过Redis命令组合在一起,例如:

SET a "abc"
INCR b
HSET c name "hi"

此时便可以使用Redis提供的pipelining功能来实现在一次交互中执行多条命令。
使用pipelining时,只需要从客户端一次向Redis发送多条命令(以rn)分隔,Redis就会依次执行这些命令,并且把每个命令的返回按顺序组装在一起一次返回,比如:

$ (printf "PINGrnPINGrnPINGrn"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG

大部分的Redis客户端都对Pipelining提供支持,所以开发者通常并不需要自己手工拼装命令列表。

Pipelining的局限性

Pipelining只能用于执行 连续且无相关性的命令,当某个命令的生成需要依赖于前一个命令的返回时,就无法使用Pipelining了。

通过Scripting功能,可以规避这一局限性

事务与Scripting

Pipelining能够让Redis在一次交互中处理多条命令,然而在一些场景下,我们可能需要在此基础上确保这一组命令是连续执行的。

比如获取当前累计的PV数并将其清0

> GET vCount
12384> SET vCount 0
OK

如果在GET和SET命令之间插进来一个INCR vCount,就会使客户端拿到的vCount不准确。

Redis的事务可以确保复数命令执行时的原子性。也就是说Redis能够保证:一个事务中的一组命令是绝对连续执行的,在这些命令执行完成之前,绝对不会有来自于其他连接的其他命令插进去执行。

通过MULTI和EXEC命令来把这两个命令加入一个事务中:

> MULTI
OK> GET vCount
QUEUED> SET vCount 0
QUEUED> EXEC
1) 12384
2) OK

Redis在接收到MULTI命令后便会开启一个事务,这之后的所有读写命令都会保存在队列中但并不执行,直到接收到EXEC命令后,Redis会把队列中的所有命令连续顺序执行,并以数组形式返回每个命令的返回结果。

可以使用DISCARD命令放弃当前的事务,将保存的命令队列清空。

需要注意的是, Redis事务不支持回滚
如果一个事务中的命令出现了语法错误,大部分客户端驱动会返回错误,2.6.5版本以上的Redis也会在执行EXEC时检查队列中的命令是否存在语法错误,如果存在,则会自动放弃事务并返回错误。
但如果一个事务中的命令有非语法类的错误(比如对String执行HSET操作),无论客户端驱动还是Redis都无法在真正执行这条命令之前发现,所以事务中的所有命令仍然会被依次执行。在这种情况下,会出现一个事务中部分命令成功部分命令失败的情况,然而与RDBMS不同,Redis不提供事务回滚的功能,所以只能通过其他方法进行数据的回滚。

通过事务实现CAS

Redis提供了WATCH命令与事务搭配使用,实现CAS乐观锁的机制。

假设要实现将某个商品的状态改为已售:

if(exec(HGET stock:1001 state) == "in stock")
    exec(HSET stock:1001 state "sold");

这一伪代码执行时,无法确保并发安全性,有可能多个客户端都获取到了”in stock”的状态,导致一个库存被售卖多次。

使用WATCH命令和事务可以解决这一问题:

exec(WATCH stock:1001);
if(exec(HGET stock:1001 state) == "in stock") {
    exec(MULTI);
    exec(HSET stock:1001 state "sold");
    exec(EXEC);
}

WATCH的机制是:在事务EXEC命令执行时,Redis会检查被WATCH的key,只有被WATCH的key从WATCH起始时至今没有发生过变更,EXEC才会被执行。如果WATCH的key在WATCH命令到EXEC命令之间发生过变化,则EXEC命令会返回失败。

Scripting

通过EVAL与EVALSHA命令,可以让Redis执行LUA脚本。这就类似于RDBMS的存储过程一样,可以把客户端与Redis之间密集的读/写交互放在服务端进行,避免过多的数据交互,提升性能。

Scripting功能是作为事务功能的替代者诞生的,事务提供的所有能力Scripting都可以做到。Redis官方推荐使用LUA Script来代替事务,前者的效率和便利性都超过了事务。

关于Scripting的具体使用,本文不做详细介绍,请参考官方文档 https://redis.io/commands/eval

Redis性能调优

尽管Redis是一个非常快速的内存数据存储媒介,也并不代表Redis不会产生性能问题。
前文中提到过,Redis采用单线程模型,所有的命令都是由一个线程串行执行的,所以当某个命令执行耗时较长时,会拖慢其后的所有命令,这使得Redis对每个任务的执行效率更加敏感。

针对Redis的性能优化,主要从下面几个层面入手:

  • 最初的也是最重要的,确保没有让Redis执行耗时长的命令
  • 使用pipelining将连续执行的命令组合执行
  • 操作系统的Transparent huge pages功能必须关闭:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

  • 如果在虚拟机中运行Redis,可能天然就有虚拟机环境带来的固有延迟。可以通过./redis-cli –intrinsic-latency 100命令查看固有延迟。同时如果对Redis的性能有较高要求的话,应尽可能在物理机上直接部署Redis。
  • 检查数据持久化策略
  • 考虑引入读写分离机制

长耗时命令

Redis绝大多数读写命令的时间复杂度都在O(1)到O(N)之间,在文本和官方文档中均对每个命令的时间复杂度有说明。

通常来说,O(1)的命令是安全的,O(N)命令在使用时需要注意,如果N的数量级不可预知,则应避免使用。例如对一个field数未知的Hash数据执行HGETALL/HKEYS/HVALS命令,通常来说这些命令执行的很快,但如果这个Hash中的field数量极多,耗时就会成倍增长。
又如使用SUNION对两个Set执行Union操作,或使用SORT对List/Set执行排序操作等时,都应该严加注意。

避免在使用这些O(N)命令时发生问题主要有几个办法:

  • 不要把List当做列表使用,仅当做队列来使用
  • 通过机制严格控制Hash、Set、Sorted Set的大小
  • 可能的话,将排序、并集、交集等操作放在客户端执行
  • 绝对禁止使用KEYS命令
  • 避免一次性遍历集合类型的所有成员,而应使用SCAN类的命令进行分批的,游标式的遍历

Redis提供了SCAN命令,可以对Redis中存储的所有key进行游标式的遍历,避免使用KEYS命令带来的性能问题。同时还有SSCAN/HSCAN/ZSCAN等命令,分别用于对Set/Hash/Sorted Set中的元素进行游标式遍历。SCAN类命令的使用请参考官方文档: https://redis.io/commands/scan

Redis提供了Slow Log功能,可以自动记录耗时较长的命令。相关的配置参数有两个:

slowlog-log-slower-than xxxms  #执行时间慢于xxx毫秒的命令计入Slow Log
slowlog-max-len xxx  #Slow Log的长度,即最大纪录多少条Slow Log

使用 SLOWLOG GET [number]命令,可以输出最近进入Slow Log的number条命令。
使用 SLOWLOG RESET命令,可以重置Slow Log

网络引发的延迟

  • 尽可能使用长连接或连接池,避免频繁创建销毁连接
  • 客户端进行的批量数据操作,应使用Pipeline特性在一次交互中完成。具体请参照本文的Pipelining章节

数据持久化引发的延迟

Redis的数据持久化工作本身就会带来延迟,需要根据数据的安全级别和性能要求制定合理的持久化策略:

  • AOF + fsync always的设置虽然能够绝对确保数据安全,但每个操作都会触发一次fsync,会对Redis的性能有比较明显的影响
  • AOF + fsync every second是比较好的折中方案,每秒fsync一次
  • AOF + fsync never会提供AOF持久化方案下的最优性能
  • 使用RDB持久化通常会提供比使用AOF更高的性能,但需要注意RDB的策略配置
  • 每一次RDB快照和AOF Rewrite都需要Redis主进程进行fork操作。fork操作本身可能会产生较高的耗时,与CPU和Redis占用的内存大小有关。根据具体的情况合理配置RDB快照和AOF Rewrite时机,避免过于频繁的fork带来的延迟

Redis在fork子进程时需要将内存分页表拷贝至子进程,以占用了24GB内存的Redis实例为例,共需要拷贝24GB / 4kB * 8 = 48MB的数据。在使用单Xeon 2.27Ghz的物理机上,这一fork操作耗时216ms。

可以通过 INFO命令返回的latest_fork_usec字段查看上一次fork操作的耗时(微秒)

Swap引发的延迟

当Linux将Redis所用的内存分页移至swap空间时,将会阻塞Redis进程,导致Redis出现不正常的延迟。Swap通常在物理内存不足或一些进程在进行大量I/O操作时发生,应尽可能避免上述两种情况的出现。

/proc/<pid>/smaps文件中会保存进程的swap记录,通过查看这个文件,能够判断Redis的延迟是否由Swap产生。如果这个文件中记录了较大的Swap size,则说明延迟很有可能是Swap造成的。

数据淘汰引发的延迟

当同一秒内有大量key过期时,也会引发Redis的延迟。在使用时应尽量将key的失效时间错开。

引入读写分离机制

Redis的主从复制能力可以实现一主多从的多节点架构,在这一架构下,主节点接收所有写请求,并将数据同步给多个从节点。
在这一基础上,我们可以让从节点提供对实时性要求不高的读请求服务,以减小主节点的压力。
尤其是针对一些使用了长耗时命令的统计类任务,完全可以指定在一个或多个从节点上执行,避免这些长耗时命令影响其他请求的响应。

关于读写分离的具体说明,请参见后续章节

主从复制与集群分片

主从复制

Redis支持一主多从的主从复制架构。一个Master实例负责处理所有的写请求,Master将写操作同步至所有Slave。
借助Redis的主从复制,可以实现读写分离和高可用:

  • 实时性要求不是特别高的读请求,可以在Slave上完成,提升效率。特别是一些周期性执行的统计任务,这些任务可能需要执行一些长耗时的Redis命令,可以专门规划出1个或几个Slave用于服务这些统计任务
  • 借助Redis Sentinel可以实现高可用,当Master crash后,Redis Sentinel能够自动将一个Slave晋升为Master,继续提供服务

启用主从复制非常简单,只需要配置多个Redis实例,在作为Slave的Redis实例中配置:

slaveof 192.168.1.1 6379  #指定Master的IP和端口

当Slave启动后,会从Master进行一次冷启动数据同步,由Master触发BGSAVE生成RDB文件推送给Slave进行导入,导入完成后Master再将增量数据通过Redis Protocol同步给Slave。之后主从之间的数据便一直以Redis Protocol进行同步

使用Sentinel做自动failover

Redis的主从复制功能本身只是做数据同步,并不提供监控和自动failover能力,要通过主从复制功能来实现Redis的高可用,还需要引入一个组件:Redis Sentinel

Redis Sentinel是Redis官方开发的监控组件,可以监控Redis实例的状态,通过Master节点自动发现Slave节点,并在监测到Master节点失效时选举出一个新的Master,并向所有Redis实例推送新的主从配置。

Redis Sentinel需要至少部署3个实例才能形成选举关系。

关键配置:

sentinel monitor mymaster 127.0.0.1 6379 2  #Master实例的IP、端口,以及选举需要的赞成票数
sentinel down-after-milliseconds mymaster 60000  #多长时间没有响应视为Master失效
sentinel failover-timeout mymaster 180000  #两次failover尝试间的间隔时长
sentinel parallel-syncs mymaster 1  #如果有多个Slave,可以通过此配置指定同时从新Master进行数据同步的Slave数,避免所有Slave同时进行数据同步导致查询服务也不可用

另外需要注意的是,Redis Sentinel实现的自动failover不是在同一个IP和端口上完成的,也就是说自动failover产生的新Master提供服务的IP和端口与之前的Master是不一样的,所以要实现HA,还要求客户端必须支持Sentinel,能够与Sentinel交互获得新Master的信息才行。

集群分片

为何要做集群分片:

  • Redis中存储的数据量大,一台主机的物理内存已经无法容纳
  • Redis的写请求并发量大,一个Redis实例以无法承载

当上述两个问题出现时,就必须要对Redis进行分片了。
Redis的分片方案有很多种,例如很多Redis的客户端都自行实现了分片功能,也有向Twemproxy这样的以代理方式实现的Redis分片方案。然而首选的方案还应该是Redis官方在3.0版本中推出的Redis Cluster分片方案。

本文不会对Redis Cluster的具体安装和部署细节进行介绍,重点介绍Redis Cluster带来的好处与弊端。

Redis Cluster的能力

  • 能够自动将数据分散在多个节点上
  • 当访问的key不在当前分片上时,能够自动将请求转发至正确的分片
  • 当集群中部分节点失效时仍能提供服务

其中第三点是基于主从复制来实现的,Redis Cluster的每个数据分片都采用了主从复制的结构,原理和前文所述的主从复制完全一致,唯一的区别是省去了Redis Sentinel这一额外的组件,由Redis Cluster负责进行一个分片内部的节点监控和自动failover。

Redis Cluster分片原理

Redis Cluster中共有16384个hash slot,Redis会计算每个key的CRC16,将结果与16384取模,来决定该key存储在哪一个hash slot中,同时需要指定Redis Cluster中每个数据分片负责的Slot数。Slot的分配在任何时间点都可以进行重新分配。

客户端在对key进行读写操作时,可以连接Cluster中的任意一个分片,如果操作的key不在此分片负责的Slot范围内,Redis Cluster会自动将请求重定向到正确的分片上。

hash tags

在基础的分片原则上,Redis还支持hash tags功能,以hash tags要求的格式明明的key,将会确保进入同一个Slot中。例如:{uiv}user:1000和{uiv}user:1001拥有同样的hash tag {uiv},会保存在同一个Slot中。

使用Redis Cluster时,pipelining、事务和LUA Script功能涉及的key必须在同一个数据分片上,否则将会返回错误。如要在Redis Cluster中使用上述功能,就必须通过hash tags来确保一个pipeline或一个事务中操作的所有key都位于同一个Slot中。

有一些客户端(如Redisson)实现了集群化的pipelining操作,可以自动将一个pipeline里的命令按key所在的分片进行分组,分别发到不同的分片上执行。但是Redis不支持跨分片的事务,事务和LUA Script还是必须遵循所有key在一个分片上的规则要求。

主从复制 vs 集群分片

在设计软件架构时,要如何在主从复制和集群分片两种部署方案中取舍呢?

从各个方面看,Redis Cluster都是优于主从复制的方案

  • Redis Cluster能够解决单节点上数据量过大的问题
  • Redis Cluster能够解决单节点访问压力过大的问题
  • Redis Cluster包含了主从复制的能力

那是不是代表Redis Cluster永远是优于主从复制的选择呢?

并不是。

软件架构永远不是越复杂越好,复杂的架构在带来显著好处的同时,一定也会带来相应的弊端。采用Redis Cluster的弊端包括:

  • 维护难度增加。在使用Redis Cluster时,需要维护的Redis实例数倍增,需要监控的主机数量也相应增加,数据备份/持久化的复杂度也会增加。同时在进行分片的增减操作时,还需要进行reshard操作,远比主从模式下增加一个Slave的复杂度要高。
  • 客户端资源消耗增加。当客户端使用连接池时,需要为每一个数据分片维护一个连接池,客户端同时需要保持的连接数成倍增多,加大了客户端本身和操作系统资源的消耗。
  • 性能优化难度增加。你可能需要在多个分片上查看Slow Log和Swap日志才能定位性能问题。
  • 事务和LUA Script的使用成本增加。在Redis Cluster中使用事务和LUA Script特性有严格的限制条件,事务和Script中操作的key必须位于同一个分片上,这就使得在开发时必须对相应场景下涉及的key进行额外的规划和规范要求。如果应用的场景中大量涉及事务和Script的使用,如何在保证这两个功能的正常运作前提下把数据平均分到多个数据分片中就会成为难点。

所以说,在主从复制和集群分片两个方案中做出选择时,应该从应用软件的功能特性、数据和访问量级、未来发展规划等方面综合考虑,只在 确实有必要引入数据分片时再使用Redis Cluster。
下面是一些建议:

  1. 需要在Redis中存储的数据有多大?未来2年内可能发展为多大?这些数据是否都需要长期保存?是否可以使用LRU算法进行非热点数据的淘汰?综合考虑前面几个因素,评估出Redis需要使用的物理内存。
  2. 用于部署Redis的主机物理内存有多大?有多少可以分配给Redis使用?对比(1)中的内存需求评估,是否足够用?
  3. Redis面临的并发写压力会有多大?在不使用pipelining时,Redis的写性能可以超过10万次/秒(更多的benchmark可以参考 https://redis.io/topics/benchmarks
  4. 在使用Redis时,是否会使用到pipelining和事务功能?使用的场景多不多?

综合上面几点考虑,如果单台主机的可用物理内存完全足以支撑对Redis的容量需求,且Redis面临的并发写压力距离Benchmark值还尚有距离,建议采用主从复制的架构,可以省去很多不必要的麻烦。同时,如果应用中大量使用pipelining和事务,也建议尽可能选择主从复制架构,可以减少设计和开发时的复杂度。

Redis Java客户端的选择

Redis的Java客户端很多,官方推荐的有三种:Jedis、Redisson和lettuce。

在这里对Jedis和Redisson进行对比介绍

Jedis:

  • 轻量,简洁,便于集成和改造
  • 支持连接池
  • 支持pipelining、事务、LUA Scripting、Redis Sentinel、Redis Cluster
  • 不支持读写分离,需要自己实现
  • 文档差(真的很差,几乎没有……)

Redisson:

  • 基于Netty实现,采用非阻塞IO,性能高
  • 支持异步请求
  • 支持连接池
  • 支持pipelining、LUA Scripting、Redis Sentinel、Redis Cluster
  • 不支持事务,官方建议以LUA Scripting代替事务
  • 支持在Redis Cluster架构下使用pipelining
  • 支持读写分离,支持读负载均衡,在主从复制和Redis Cluster架构下都可以使用
  • 内建Tomcat Session Manager,为Tomcat 6/7/8提供了会话共享功能
  • 可以与Spring Session集成,实现基于Redis的会话共享
  • 文档较丰富,有中文文档

对于Jedis和Redisson的选择,同样应遵循前述的原理,尽管Jedis比起Redisson有各种各样的不足,但也应该在需要使用Redisson的高级特性时再选用Redisson,避免造成不必要的程序复杂度提升。

Jedis:
github: https://github.com/xetorthio/jedis
文档: https://github.com/xetorthio/jedis/wiki

Redisson:
github: https://github.com/redisson/redisson
文档: https://github.com/redisson/redisson/wiki

Redis基础、高级特性与性能调优,首发于 文章 - 伯乐在线

美团点评基于 Flink 的实时数仓建设实践

$
0
0

引言

近些年,企业对数据服务实时化服务的需求日益增多。本文整理了常见实时数据组件的性能特点和适用场景,介绍了美团如何通过 Flink 引擎构建实时数据仓库,从而提供高效、稳健的实时数据服务。此前我们美团技术博客发布过一篇文章《 流计算框架 Flink 与 Storm 的性能对比》,对 Flink 和 Storm 俩个引擎的计算性能进行了比较。本文主要阐述使用 Flink 在实际数据生产上的经验。

实时平台初期架构

在实时数据系统建设初期,由于对实时数据的需求较少,形成不了完整的数据体系。我们采用的是“一路到底”的开发模式:通过在实时计算平台上部署 Storm 作业处理实时数据队列来提取数据指标,直接推送到实时应用服务中。


图1 初期实时数据架构

但是,随着产品和业务人员对实时数据需求的不断增多,新的挑战也随之发生。

  1. 数据指标越来越多,“烟囱式”的开发导致代码耦合问题严重。
  2. 需求越来越多,有的需要明细数据,有的需要 OLAP 分析。单一的开发模式难以应付多种需求。
  3. 缺少完善的监控系统,无法在对业务产生影响之前发现并修复问题。

实时数据仓库的构建

为解决以上问题,我们根据生产离线数据的经验,选择使用分层设计方案来建设实时数据仓库,其分层架构如下图所示:


图2 实时数仓数据分层架构

该方案由以下四层构成:

  1. ODS 层:Binlog 和流量日志以及各业务实时队列。
  2. 数据明细层:业务领域整合提取事实数据,离线全量和实时变化数据构建实时维度数据。
  3. 数据汇总层:使用宽表模型对明细数据补充维度数据,对共性指标进行汇总。
  4. App 层:为了具体需求而构建的应用层,通过 RPC 框架对外提供服务。

通过多层设计我们可以将处理数据的流程沉淀在各层完成。比如在数据明细层统一完成数据的过滤、清洗、规范、脱敏流程;在数据汇总层加工共性的多维指标汇总数据。提高了代码的复用率和整体生产效率。同时各层级处理的任务类型相似,可以采用统一的技术方案优化性能,使数仓技术架构更简洁。

技术选型

1.存储引擎的调研

实时数仓在设计中不同于离线数仓在各层级使用同种储存方案,比如都存储在 Hive 、DB 中的策略。首先对中间过程的表,采用将结构化的数据通过消息队列存储和高速 KV 存储混合的方案。实时计算引擎可以通过监听消息消费消息队列内的数据,进行实时计算。而在高速 KV 存储上的数据则可以用于快速关联计算,比如维度数据。 其次在应用层上,针对数据使用特点配置存储方案直接写入。避免了离线数仓应用层同步数据流程带来的处理延迟。 为了解决不同类型的实时数据需求,合理的设计各层级存储方案,我们调研了美团内部使用比较广泛的几种存储方案。

表1 存储方案列表
方案优势劣势
MySQL1. 具有完备的事务功能,可以对数据进行更新。2. 支持 SQL,开发成本低。1. 横向扩展成本大,存储容易成为瓶颈; 2. 实时数据的更新和查询频率都很高,线上单个实时应用请求就有 1000+ QPS;使用 MySQL 成本太高。
Elasticsearch1. 吞吐量大,单个机器可以支持 2500+ QPS,并且集群可以快速横向扩展。2. Term 查询时响应速度很快,单个机器在 2000+ QPS时,查询延迟在 20 ms以内。1. 没有原生的 SQL 支持,查询 DSL 有一定的学习门槛;2. 进行聚合运算时性能下降明显。
Druid1. 支持超大数据量,通过 Kafka 获取实时数据时,单个作业可支持 6W+ QPS;2. 可以在数据导入时通过预计算对数据进行汇总,减少的数据存储。提高了实际处理数据的效率;3. 有很多开源 OLAP 分析框架。实现如 Superset。1. 预聚合导致无法支持明细的查询;2. 无法支持 Join 操作;3. Append-only 不支持数据的修改。只能以 Segment 为单位进行替换。
Cellar1. 支持超大数据量,采用内存加分布式存储的架构,存储性价比很高;2. 吞吐性能好,经测试处理 3W+ QPS 读写请求时,平均延迟在 1ms左右;通过异步读写线上最高支持 10W+ QPS。1. 接口仅支持 KV,Map,List 以及原子加减等;2. 单个 Key 值不得超过 1KB ,而 Value 的值超过 100KB 时则性能下降明显。

根据不同业务场景,实时数仓各个模型层次使用的存储方案大致如下:


图3 实时数仓存储分层架构
  1. 数据明细层对于维度数据部分场景下关联的频率可达 10w+ TPS,我们选择 Cellar(美团内部存储系统) 作为存储,封装维度服务为实时数仓提供维度数据。
  2. 数据汇总层对于通用的汇总指标,需要进行历史数据关联的数据,采用和维度数据一样的方案通过 Cellar 作为存储,用服务的方式进行关联操作。
  3. 数据应用层应用层设计相对复杂,再对比了几种不同存储方案后。我们制定了以数据读写频率 1000 QPS 为分界的判断依据。对于读写平均频率高于 1000 QPS 但查询不太复杂的实时应用,比如商户实时的经营数据。采用 Cellar 为存储,提供实时数据服务。对于一些查询复杂的和需要明细列表的应用,使用 Elasticsearch 作为存储则更为合适。而一些查询频率低,比如一些内部运营的数据。 Druid 通过实时处理消息构建索引,并通过预聚合可以快速的提供实时数据 OLAP 分析功能。对于一些历史版本的数据产品进行实时化改造时,也可以使用 MySQL 存储便于产品迭代。

2.计算引擎的调研

在实时平台建设初期我们使用 Storm 引擎来进行实时数据处理。Storm 引擎虽然在灵活性和性能上都表现不错。但是由于 API 过于底层,在数据开发过程中需要对一些常用的数据操作进行功能实现。比如表关联、聚合等,产生了很多额外的开发工作,不仅引入了很多外部依赖比如缓存,而且实际使用时性能也不是很理想。同时 Storm 内的数据对象 Tuple 支持的功能也很简单,通常需要将其转换为 Java 对象来处理。对于这种基于代码定义的数据模型,通常我们只能通过文档来进行维护。不仅需要额外的维护工作,同时在增改字段时也很麻烦。综合来看使用 Storm 引擎构建实时数仓难度较大。我们需要一个新的实时处理方案,要能够实现:

  1. 提供高级 API,支持常见的数据操作比如关联聚合,最好是能支持 SQL。
  2. 具有状态管理和自动支持久化方案,减少对存储的依赖。
  3. 便于接入元数据服务,避免通过代码管理数据结构。
  4. 处理性能至少要和 Storm 一致。

我们对主要的实时计算引擎进行了技术调研。总结了各类引擎特性如下表所示:

表2 实时计算方案列表
项目/引擎StormFlinkspark-treaming
API灵活的底层 API 和具有事务保证的 Trident API流 API 和更加适合数据开发的 Table API 和 Flink SQL 支持流 API 和 Structured-Streaming API 同时也可以使用更适合数据开发的 Spark SQL
容错机制ACK 机制State 分布式快照保存点RDD 保存点
状态管理Trident State状态管理Key State 和 Operator State两种 State 可以使用,支持多种持久化方案有 UpdateStateByKey 等 API 进行带状态的变更,支持多种持久化方案
处理模式单条流式处理单条流式处理Mic batch处理
延迟毫秒级毫秒级秒级
语义保障At Least Once,Exactly OnceExactly Once,At Least OnceAt Least Once

从调研结果来看,Flink 和 Spark Streaming 的 API 、容错机制与状态持久化机制都可以解决一部分我们目前使用 Storm 中遇到的问题。但 Flink 在数据延迟上和 Storm 更接近,对现有应用影响最小。而且在公司内部的测试中 Flink 的吞吐性能对比 Storm 有十倍左右提升。综合考量我们选定 Flink 引擎作为实时数仓的开发引擎。

更加引起我们注意的是,Flink 的 Table 抽象和 SQL 支持。虽然使用 Strom 引擎也可以处理结构化数据。但毕竟依旧是基于消息的处理 API ,在代码层层面上不能完全享受操作结构化数据的便利。而 Flink 不仅支持了大量常用的 SQL 语句,基本覆盖了我们的开发场景。而且 Flink 的 Table 可以通过 TableSchema 进行管理,支持丰富的数据类型和数据结构以及数据源。可以很容易的和现有的元数据管理系统或配置管理系统结合。通过下图我们可以清晰的看出 Storm 和 Flink 在开发统过程中的区别。


图4 Flink - Storm 对比图

在使用 Storm 开发时处理逻辑与实现需要固化在 Bolt 的代码。Flink 则可以通过 SQL 进行开发,代码可读性更高,逻辑的实现由开源框架来保证可靠高效,对特定场景的优化只要修改 Flink SQL 优化器功能实现即可,而不影响逻辑代码。使我们可以把更多的精力放到到数据开发中,而不是逻辑的实现。当需要离线数据和实时数据口径统一的场景时,我们只需对离线口径的 SQL 脚本稍加改造即可,极大地提高了开发效率。同时对比图中 Flink 和 Storm 使用的数据模型,Storm 需要通过一个 Java 的 Class 去定义数据结构,Flink Table 则可以通过元数据来定义。可以很好的和数据开发中的元数据,数据治理等系统结合,提高开发效率。

Flink使用心得

在利用 Flink-Table 构建实时数据仓库过程中。我们针对一些构建数据仓库的常用操作,比如数据指标的维度扩充,数据按主题关联,以及数据的聚合运算通过 Flink 来实现总结了一些使用心得。

1.维度扩充

数据指标的维度扩充,我们采用的是通过维度服务获取维度信息。虽然基于 Cellar 的维度服务通常的响应延迟可以在 1ms 以下。但是为了进一步优化 Flink 的吞吐,我们对维度数据的关联全部采用了异步接口访问的方式,避免了使用 RPC 调用影响数据吞吐。
对于一些数据量很大的流,比如流量日志数据量在 10W 条/秒这个量级。在关联 UDF 的时候内置了缓存机制,可以根据命中率和时间对缓存进行淘汰,配合用关联的 Key 值进行分区,显著减少了对外部服务的请求次数,有效的减少了处理延迟和对外部系统的压力。

2.数据关联

数据主题合并,本质上就是多个数据源的关联,简单的来说就是 Join 操作。Flink 的 Table 是建立在无限流这个概念上的。在进行 Join 操作时并不能像离线数据一样对两个完整的表进行关联。采用的是在窗口时间内对数据进行关联的方案,相当于从两个数据流中各自截取一段时间的数据进行 Join 操作。有点类似于离线数据通过限制分区来进行关联。同时需要注意 Flink 关联表时必须有至少一个“等于”关联条件,因为等号两边的值会用来分组。
由于 Flink 会缓存窗口内的全部数据来进行关联,缓存的数据量和关联的窗口大小成正比。因此 Flink 的关联查询,更适合处理一些可以通过业务规则限制关联数据时间范围的场景。比如关联下单用户购买之前 30 分钟内的浏览日志。过大的窗口不仅会消耗更多的内存,同时会产生更大的 Checkpoint ,导致吞吐下降或 Checkpoint 超时。在实际生产中可以使用 RocksDB 和启用增量保存点模式,减少 Checkpoint 过程对吞吐产生影响。对于一些需要关联窗口期很长的场景,比如关联的数据可能是几天以前的数据。对于这些历史数据,我们可以将其理解为是一种已经固定不变的"维度"。可以将需要被关联的历史数据采用和维度数据一致的处理方法:"缓存 + 离线"数据方式存储,用接口的方式进行关联。另外需要注意 Flink 对多表关联是直接顺序链接的,因此需要注意先进行结果集小的关联。

3.聚合运算

使用聚合运算时,Flink 对常见的聚合运算如求和、极值、均值等都有支持。美中不足的是对于 Distinct 的支持,Flink-1.6 之前的采用的方案是通过先对去重字段进行分组再聚合实现。对于需要对多个字段去重聚合的场景,只能分别计算再进行关联处理效率很低。为此我们开发了自定义的 UDAF,实现了 MapView 精确去重、BloomFilter 非精确去重、 HyperLogLog 超低内存去重方案应对各种实时去重场景。但是在使用自定义的 UDAF 时,需要注意 RocksDBStateBackend 模式对于较大的 Key 进行更新操作时序列化和反序列化耗时很多。可以考虑使用 FsStateBackend 模式替代。另外要注意的一点 Flink 框架在计算比如 Rank 这样的分析函数时,需要缓存每个分组窗口下的全部数据才能进行排序,会消耗大量内存。建议在这种场景下优先转换为 TopN 的逻辑,看是否可以解决需求。

下图展示一个完整的使用 Flink 引擎生产一张实时数据表的过程:


图5 实时计算流程图

实时数仓成果

通过使用实时数仓代替原有流程,我们将数据生产中的各个流程抽象到实时数仓的各层当中。实现了全部实时数据应用的数据源统一,保证了应用数据指标、维度的口径的一致。在几次数据口径发生修改的场景中,我们通过对仓库明细和汇总进行改造,在完全不用修改应用代码的情况下就完成全部应用的口径切换。在开发过程中通过严格的把控数据分层、主题域划分、内容组织标准规范和命名规则。使数据开发的链路更为清晰,减少了代码的耦合。再配合上使用 Flink SQL 进行开发,代码加简洁。单个作业的代码量从平均 300+ 行的 JAVA 代码 ,缩减到几十行的 SQL 脚本。项目的开发时长也大幅减短,一人日开发多个实时数据指标情况也不少见。

除此以外我们通过针对数仓各层级工作内容的不同特点,可以进行针对性的性能优化和参数配置。比如 ODS 层主要进行数据的解析、过滤等操作,不需要 RPC 调用和聚合运算。 我们针对数据解析过程进行优化,减少不必要的 JSON 字段解析,并使用更高效的 JSON 包。在资源分配上,单个 CPU 只配置 1GB 的内存即可满需求。而汇总层主要则主要进行聚合与关联运算,可以通过优化聚合算法、内外存共同运算来提高性能、减少成本。资源配置上也会分配更多的内存,避免内存溢出。通过这些优化手段,虽然相比原有流程实时数仓的生产链路更长,但数据延迟并没有明显增加。同时实时数据应用所使用的计算资源也有明显减少。

展望

我们的目标是将实时仓库建设成可以和离线仓库数据准确性,一致性媲美的数据系统。为商家,业务人员以及美团用户提供及时可靠的数据服务。同时作为到餐实时数据的统一出口,为集团其他业务部门助力。未来我们将更加关注在数据可靠性和实时数据指标管理。建立完善的数据监控,数据血缘检测,交叉检查机制。及时对异常数据或数据延迟进行监控和预警。同时优化开发流程,降低开发实时数据学习成本。让更多有实时数据需求的人,可以自己动手解决问题。

参考文献

流计算框架 Flink 与 Storm 的性能对比

关于作者

伟伦,美团到店餐饮技术部实时数据负责人,2017年加入美团,长期从事数据平台、实时数据计算、数据架构方面的开发工作。在使用 Flink 进行实时数据生产和提高生产效率上,有一些心得和产出。同时也积极推广 Flink 在实时数据处理中的实战经验。

招聘信息

对数据工程和将数据通过服务业务释放价值感兴趣的同学,可以发送简历到 huangweilun@meituan.com。我们在实时数据仓库、实时数据治理、实时数据产品开发框架、面向销售和商家侧的数据型创新产品层面,都有很多未知但有意义的领域等你来开拓。

Netty堆外内存泄露排查盛宴

$
0
0

导读

Netty 是一个异步事件驱动的网络通信层框架,用于快速开发高可用高性能的服务端网络框架与客户端程序,它极大地简化了 TCP 和 UDP 套接字服务器等网络编程。

Netty 底层基于 JDK 的 NIO,我们为什么不直接基于 JDK 的 NIO 或者其他NIO框架:

  1. 使用 JDK 自带的 NIO 需要了解太多的概念,编程复杂。
  2. Netty 底层 IO 模型随意切换,而这一切只需要做微小的改动。
  3. Netty自带的拆包解包,异常检测等机制让我们从 NIO 的繁重细节中脱离出来,只需关心业务逻辑即可。
  4. Netty解决了JDK 的很多包括空轮训在内的 Bug。
  5. Netty底层对线程,Selector 做了很多细小的优化,精心设计的 Reactor 线程做到非常高效的并发处理。
  6. 自带各种协议栈,让我们处理任何一种通用协议都几乎不用亲自动手。
  7. Netty社区活跃,遇到问题随时邮件列表或者 issue。
  8. Netty已经历各大RPC框架(Dubbo),消息中间件(RocketMQ),大数据通信(Hadoop)框架的广泛的线上验证,健壮性无比强大。

背景

最近在做一个基于 Websocket 的长连中间件,服务端使用实现了 Socket.IO 协议(基于WebSocket协议,提供长轮询降级能力) 的 netty-socketio框架,该框架为 Netty 实现,鉴于本人对 Netty 比较熟,并且对比同样实现了 Socket.IO 协议的其他框架,Netty 的口碑都要更好一些,因此选择这个框架作为底层核心。

诚然,任何开源框架都避免不了 Bug 的存在,我们在使用这个开源框架时,就遇到一个堆外内存泄露的 Bug。美团的价值观一直都是“追求卓越”,所以我们就想挑战一下,找到那只臭虫(Bug),而本文就是遇到的问题以及排查的过程。当然,想看结论的同学可以直接跳到最后,阅读总结即可。

问题

某天早上,我们突然收到告警,Nginx 服务端出现大量5xx。

image.png

我们使用 Nginx 作为服务端 WebSocket 的七层负载,5xx的爆发通常表明服务端不可用。由于目前 Nginx 告警没有细分具体哪台机器不可用,接下来,我们就到 CAT(美团点评统一监控平台,目前已经开源)去检查一下整个集群的各项指标,就发现如下两个异常:

image.png

某台机器在同一时间点爆发 GC(垃圾回收),而且在同一时间,JVM 线程阻塞。

image.png

接下来,我们就就开始了漫长的堆外内存泄露“排查之旅”。

排查过程

阶段1: 怀疑是log4j2

因为线程被大量阻塞,我们首先想到的是定位哪些线程被阻塞,最后查出来是 Log4j2 狂打日志导致 Netty 的 NIO 线程阻塞(由于没有及时保留现场,所以截图缺失)。NIO 线程阻塞之后,因我们的服务器无法处理客户端的请求,所以对Nginx来说就是5xx。

接下来,我们查看了 Log4j2 的配置文件。

image.png

我们发现打印到控制台的这个 appender 忘记注释掉了,所以初步猜测:因为这个项目打印的日志过多,而 Log4j2 打印到控制台是同步阻塞打印的,所以就导致了这个问题。那么接下来,我们把线上所有机器的这行注释掉,本以为会“大功告成”,但没想到仅仅过了几天,5xx告警又来“敲门”。看来,这个问题并没我们最初想象的那么简单。

阶段2:可疑日志浮现

接下来,我们只能硬着头皮去查日志,特别是故障发生点前后的日志,于是又发现了一处可疑的地方:

image.png

可以看到:在极短的时间内,狂打 failed to allocate 64(bytes) of direct memory(...)日志(瞬间十几个日志文件,每个日志文件几百M),日志里抛出一个 Netty 自己封装的 OutOfDirectMemoryError。说白了,就是堆外内存不够用,Netty 一直在“喊冤”。

堆外内存泄露,听到这个名词就感到很沮丧。因为这个问题的排查就像 C 语言内存泄露一样难以排查,首先能想到的就是,在 OOM 爆发之前,查看有无异常。然后查遍了 CAT 上与机器相关的所有指标,查遍了 OOM 日志之前的所有日志,均未发现任何异常!这个时候心里已经“万马奔腾”了......

阶段3:定位OOM源

没办法,只能看着这堆讨厌的 OOM 日志发着呆,希望答案能够“蹦到”眼前,但是那只是妄想。一筹莫展之际,突然一道光在眼前一闪而过,在 OOM 下方的几行日志变得耀眼起来(为啥之前就没想认真查看日志?估计是被堆外内存泄露这几个词吓怕了吧 ==!),这几行字是 ....PlatformDepedeng.incrementMemory()...

原来,堆外内存是否够用,是 Netty 这边自己统计的,那么是不是可以找到统计代码,找到统计代码之后我们就可以看到 Netty 里面的对外内存统计逻辑了?于是,接下来翻翻代码,找到这段逻辑,就在 PlatformDepedent这个类里面。

image.png

这个地方,是一个对已使用堆外内存计数的操作,计数器为 DIRECT_MEMORY_COUNTER,如果发现已使用内存大于堆外内存的上限(用户自行指定),就抛出一个自定义 OOM Error,异常里面的文本内容正是我们在日志里面看到的。

接下来,就验证一下这个方法是否是在堆外内存分配的时候被调用。

image.png

果然,在 Netty 每次分配堆外内存之前,都会计数。想到这,思路就开始慢慢清晰,而心情也开始从“秋风瑟瑟”变成“春光明媚”。

阶段4:反射进行堆外内存监控

CAT上关于堆外内存的监控没有任何异常(应该是没有统计准确,一直维持在 1M),而这边我们又确认堆外内存已快超过上限,并且已经知道 Netty 底层是使用的哪个字段来统计。那么接下来要做的第一件事情,就是反射拿到这个字段,然后我们自己统计 Netty 使用堆外内存的情况。

image.png

堆外内存统计字段是 DIRECT_MEMORY_COUNTER,我们可以通过反射拿到这个字段,然后定期 Check 这个值,就可以监控 Netty 堆外内存的增长情况。

image.png

于是我们通过反射拿到这个字段,然后每隔一秒打印,为什么要这样做?

因为,通过我们前面的分析,在爆发大量 OOM 现象之前,没有任何可疑的现象。那么只有两种情况,一种是突然某个瞬间分配了大量的堆外内存导致OOM;一种是堆外内存缓慢增长,到达某个点之后,最后一根稻草将机器压垮。在这段代码加上去之后,我们打包上线。

阶段5:到底是缓慢增长还是瞬间飙升?

代码上线之后,初始内存为 16384k(16M),这是因为线上我们使用了池化堆外内存,默认一个 chunk 为16M,这里不必过于纠结。

image.png

但是没过一会,内存就开始缓慢飙升,并且没有释放的迹象,二十几分钟之后,内存使用情况如下:

image.png

走到这里,我们猜测可能是前面提到的第二种情况,也就是内存缓慢增长造成的 OOM,由于内存实在增长太慢,于是调整机器负载权重为其他机器的两倍,但是仍然是以数K级别在持续增长。那天刚好是周五,索性就过一个周末再开看。

周末之后,我们到公司第一时间就连上了跳板机,登录线上机器,开始 tail -f 继续查看日志。在输完命令之后,怀着期待的心情重重的敲下了回车键:

image.png

果然不出所料,内存一直在缓慢增长,一个周末的时间,堆外内存已经飙到快一个 G 了。这个时候,我竟然想到了一句成语:“只要功夫深,铁杵磨成针”。虽然堆外内存以几个K的速度在缓慢增长,但是只要一直持续下去,总有把内存打爆的时候(线上堆外内存上限设置的是2G)。

此时,我们开始自问自答环节:内存为啥会缓慢增长,伴随着什么而增长?因为我们的应用是面向用户端的WebSocket,那么,会不会是每一次有用户进来,交互完之后离开,内存都会增长一些,然后不释放呢?带着这个疑问,我们开始了线下模拟过程。

阶段6:线下模拟

本地起好服务,把监控堆外内存的单位改为以B为单位(因为本地流量较小,打算一次一个客户端连接),另外,本地也使用非池化内存(内存数字较小,容易看出问题),在服务端启动之后,控制台打印信息如下

image.png

在没有客户端接入的时候,堆外内存一直是0,在意料之中。接下来,怀着着无比激动的心情,打开浏览器,然后输入网址,开始我们的模拟之旅。

我们的模拟流程是:新建一个客户端链接->断开链接->再新建一个客户端链接->再断开链接。

image.png

如上图所示,一次 Connect 和 Disconnect 为一次连接的建立与关闭,上图绿色框框的日志分别是两次连接的生命周期。我们可以看到,内存每次都是在连接被关闭的的时候暴涨 256B,然后也不释放。走到这里,问题进一步缩小,肯定是连接被关闭的时候,触发了框架的一个Bug,而且这个Bug在触发之前分配了 256B 的内存,随着Bug被触发,内存也没有释放。问题缩小之后,接下来开始“撸源码”,捉虫!

阶段7:线下排查

接下来,我们将本地服务重启,开始完整的线下排查过程。同时将目光定位到 netty-socketio 这个框架的 Disconnect 事件(客户端WebSocket连接关闭时会调用到这里),基本上可以确定,在 Disconnect 事件前后申请的内存并没有释放。

image.png

在使用 idea debug 时,要选择只挂起当前线程,这样我们在单步跟踪的时候,控制台仍然可以看到堆外内存统计线程在打印日志。

在客户端连接上之后然后关闭,断点进入到 onDisconnect回调,我们特意在此多停留了一会,发现控制台内存并没有飙升(7B这个内存暂时没有去分析,只需要知道,客户端连接断开之后,我们断点hold住,内存还未开始涨)。接下来,神奇的一幕出现了,我们将断点放开,让程序跑完:

image.png

Debug 松掉之后,内存立马飙升了!!此时,我们已经知道,这只“臭虫”飞不了多远了。在 Debug 时,挂起的是当前线程,那么肯定是当前线程某个地方申请了堆外内存,然后没有释放,继续“快马加鞭“,深入源码。

其实,每一次单步调试,我们都会观察控制台的内存飙升的情况。很快,我们来到了这个地方:

image.png

在这一行没执行之前,控制台的内存依然是 263B。然后,当执行完该行之后,立刻从 263B涨到519B(涨了256B)。

image.png

于是,Bug 范围进一步缩小。我们将本次程序跑完,释然后客户端再来一次连接,断点打在 client.send()这行, 然后关闭客户端连接,之后直接进入到这个方法,随后的过程有点长,因为与 Netty 的时间传播机制有关,这里就省略了。最后,我们跟踪到了如下代码, handleWebsocket

image.png

在这个地方,我们看到一处非常可疑的地方,在上图的断点上一行,调用 encoder分配了一段内存,调用完之后,我们的控制台立马就彪了 256B。所以,我们怀疑肯定是这里申请的内存没有释放,它这里接下来调用 encoder.encodePacket()方法,猜想是把数据包的内容以二进制的方式写到这段 256B的内存。接下来,我们追踪到这段 encode 代码,单步执行之后,就定位到这行代码:

image.png

这段代码是把 packet 里面一个字段的值转换为一个 char。然而,当我们使用 idea 预执行的时候,却抛出类一个愤怒的 NPE!!也就是说,框架申请到一段内存之后,在 encoder 的时候,自己 GG 了,还给自己挖了个NPE的深坑,最后导致内存无法释放(最外层有堆外内存释放逻辑,现在无法执行到了)。而且越攒越多,直到被“最后一根稻草”压垮,堆外内存就这样爆了。这里的源码,有兴趣的读者可以自己去分析一下,限于篇幅原因,这里就不再展开叙述了。

阶段8:Bug解决

既然 Bug 已经找到,接下来就要解决问题了。这里只需要解决这个NPE异常,就可以 Fix 掉。我们的目标就是,让这个 subType字段不为空。于是我们先通过 idea 的线程调用栈,定位到这个 packet 是在哪个地方定义的:

image.png

我们找到 idea 的 debugger 面板,眼睛盯着 packet 这个对象不放,然后上线移动光标,便光速定位到。原来,定义 packet 对象这个地方在我们前面的代码其实已经出现过,我们查看了一下 subType这个字段,果然是 null。接下来,解决 Bug 就很容易了。

image.png

我们给这个字段赋值即可,由于这里是连接关闭事件,所以我们给他指定了一个名为 DISCONNECT 的字段(可以改天深入去研究 Socket.IO 的协议),反正这个 Bug 是在连接关闭的时候触发的,就粗暴一点了 !

解决这个 Bug 的过程是:将这个框架的源码下载到本地,然后加上这一行,最后重新 Build一下,pom 里改了一下名字,推送到我们公司的仓库。这样,项目就可以直接进行使用了。

改完 Bug 之后,习惯性地去 GitHub上找到引发这段 Bug 的 Commit:
image.png
好奇的是,为啥这位 dzn commiter 会写出这么一段如此明显的 Bug,而且时间就在今年3月30号,项目启动的前夕!

阶段9:线下验证

一切准备就绪之后,我们就来进行本地验证,在服务起来之后,我们疯狂地建立连接,疯狂地断开连接,并观察堆外内存的情况:

image.png

Bingo!不管我们如何断开连接,堆外内存不涨了。至此,Bug 基本 Fix,当然最后一步,我们把代码推到线上验证。

阶段10:线上验证

这次线上验证,我们避免了比较土的打日志方法,我们把堆外内存的这个指标“喷射”到 CAT 上,然后再来观察一段时间的堆外内存的情况:

image.png

过完一段时间,堆外内存已经稳定不涨了。此刻,我们的“捉虫之旅”到此结束。最后,我们还为大家做一个小小的总结,希望对您有所帮助。

总结

  1. 遇到堆外内存泄露不要怕,仔细耐心分析,总能找到思路,要多看日志,多分析。

  2. 如果使用了 Netty 堆外内存,那么可以自行监控堆外内存的使用情况,不需要借助第三方工具,我们是使用的“反射”拿到的堆外内存的情况。

  3. 逐渐缩小范围,直到 Bug 被找到。当我们确认某个线程的执行带来 Bug 时,可单步执行,可二分执行,定位到某行代码之后,跟到这段代码,然后继续单步执行或者二分的方式来定位最终出 Bug 的代码。这个方法屡试不爽,最后总能找到想要的 Bug。

  4. 熟练掌握 idea 的调试,让我们的“捉虫”速度快如闪电(“闪电侠”就是这么来的)。这里,最常见的调试方式是预执行表达式,以及通过线程调用栈,死盯某个对象,就能够掌握这个对象的定义、赋值之类。

最后,祝愿大家都能找到自己的“心仪已久” Bug!

作者简介

闪电侠,2014年加入美团点评,主要负责美团点评移动端统一长连工作,欢迎同行进行技术交流。

招聘

目前我们团队负责美团点评长连基础设施的建设,支持美团酒旅、外卖、到店、打车、金融等几乎公司所有业务的快速发展。加入我们,你可以亲身体验到千万级在线连接、日吞吐百亿请求的场景,你会直面互联网高并发、高可用的挑战,有机会接触到 Netty 在长连领域的各个场景。我们诚邀有激情、有想法、有经验、有能力的同学,和我们一起并肩奋斗!欢迎感兴趣的同学投递简历至 chao.yu#dianping.com 咨询。

参考文献

  1. Netty 是什么
  2. Netty 源码分析之服务端启动全解析

绑定地产商 这款智能家居产品很快会出现在你家 | 界面新闻

$
0
0


10月17日,智能家居领域公司深圳欧瑞博发布新品“MixPad超级智能面板”,将灯光、地暖、新风等原有的碎片化的家居控制,整合到一个简洁的智能墙面面板中,同时兼容按键、触摸屏、语音、APP等多种交互方式,还内置四核A7处理器、环境传感器、BLE/Wi-Fi/Zigbee协议,以实现便捷交互和家居智能化。

欧瑞博的MixPad实现多个方面的创新。第一,这是继路由器、智能网关、超级APP、智能音箱之后,又一创新的智能家居控制入口,并且是在前装市场切入;第二,内置蓝牙、Wi-Fi、Zigbee等通讯协议,可以打通其他协议连接的硬件设备;第三,将实体按键、触摸屏、语音控制和手机App打通,覆盖家居空间中近场、中场、远场三层交互场景。

“好的交互就是少交互。”欧瑞博CEO王雄辉告诉界面新闻,家居环境是多用户、交互场景复杂的空间,MixPad将物理按键与触摸屏结合,同时强大的配置和兼容性,可以满足家居场景里面的多品类、快交互、复杂交互的需求。

此次发布中,欧瑞博也推出了AISense智能场景机制,AISense基于人工智能与大数据技术,通过智能联动等来支持商业智能、服务智能、设备智能中不同的场景需求,以实现更好服务体验。

欧瑞博由80后创业者王雄辉创立于2011年,先后获得软银赛富、拓邦股份、联想之星等数亿元融资。过去的7年时间里,欧瑞博已发布智能燃气报警器、智能插线板、智能插座、门锁安防、智能照明、遮阳门窗等多款智能家居单品与系统。

爆款单品加上此次发布的MixPad,欧瑞博阶段性完成了智能家居生态系统的搭建,同时与家电巨头、互联网企业、智能家居同行的协议兼容,欧瑞博智能家居系统未来将进一步向人工智能方面深入推进。

欧瑞博是一家设计能力颇强的智能家居公司。2017年,欧瑞博设计的智能插座S31与苹果蓝牙无线耳机AirPods等产品一起获得iF设计金奖。

穿着乔布斯式的牛仔裤和衬衣、黑框眼镜、带着浓重客家口音的普通话,典型“理工男”王雄辉演讲能力显然不如他们的产品设计能力。这位来自广东梅州的小镇青年,一路带领欧瑞博从一家籍籍无名的公司成长为中国智能家居领域的一匹“黑马”,甚至曾经拒绝了小米的战略投资。

这家小公司的成长之路,是中国智能家居领域发展的一个剖面,从“风口”现象级单品转向全屋智能家居系统深入发展。数据显示,2017年中国广义智能家居市场规模突破3000亿元,2018年这个数字将增长到接近4000亿元。在世界范围内,智能家居行业的2022年市场规模预计将达到1550亿美元。

尽管如此,围绕着智能家居“痛点”、“难点”的解决方案之争从来没有停过,以何种设备作为智能家居控制入口,如何更好以AI驱动智能家居,各大厂商之间以何种通讯协议标准进行兼容与迭代升级。

从1988年比尔盖茨耗费7年时间、砸下6300万美元打造智能豪宅;到2014年,谷歌32亿美元智能恒温器制造商Nest,以及2014年亚马逊发布以一款AI语音助Alexa驱动的Echo智能音箱,智能家居终于找到了用语音交互的“入口”——智能音箱,国内互联网企业小米、百度、阿里巴巴、腾讯等均后续跟进投入,试图抢占这一巨大流量入口。

在欧瑞博发布会的前20多天,亚马逊一口气发布十多款基于Alexa的智能家居设备,智能音箱产品系列之外,还有还有智能摄像头、智能挂钟、智能微波炉、智能网关等;9月25日,小米生态链企业云米科技在纳斯达克上市,则充分打开了全屋智能家居的资本想象力与市场前景。

“入口不是定义出来的,而是做出来的,它要与用户共进,它要高频交互。”王雄辉坦言,在智能面板之前,欧瑞博也试过路由器、传感器,最后都失败了,同类型的还有超级APP、智能音箱等入口,但终究觉得“不够自然”。“听音乐不是刚需,买一个智能音箱很多余,用APP控制马桶感觉更怪。”

王雄辉认为,开关面板是家庭环境中已经存在的品类,而且未来很长时间内不会消失,如今欧瑞博只是将面板、连接和计算能力整合到智能面板里面,另外一个品类就是智能门锁,所以欧瑞博选择通过面板和门锁,作为与用户高频交互的产品。

智能家居最大痛点莫过于系统与单品的连接方式,以及用何种渠道来切入用户的生活。此次发布会中,欧瑞博联合以三星、松下、格力、联想、施耐德、索菲亚、拓邦等为代表的家居家电企业,以红星美凯龙、百安居、花样年等为代表的地产与渠道,以彩生活、京东等为代表的运营服务企业共同发布了智家创新生态计划,欲以MixPad为基础,以用户为中心,联合产品公司、新渠道、新市场,打智能家居系统与开放应用平台。

值得一提的是,王雄辉在发布会上透露,这一款售价2000元左右的超级面板,将针对全国的精装房样板间免费送,以及出现在百安居等家居卖场的样板间中。另外,MixPad其他理想高频应用场景还有酒店、写字楼等。

精装商品房是智能家居最理想的前置安装场景,欧瑞博试图绑定地产商推广的思路正在逐渐被接纳。花样年(01777.HK)董事长潘军在演讲中透露,欧瑞博是花样年打造智慧社区重要的参与者,未来花样年的智慧社区均会采用欧瑞博的智能家居系统产品。

“物理概念的小区正在转向有文化主张的小区。”潘军认为,只有软件、硬件的结合,同时人与人、人与物、人与社区有连接,有服务,才能形成智慧生活,“因此在行业价格下行背景,部分开发商重新采用毛坯交房是不符合潮流的。”

由此来看,随着产品的升级,以及地产渠道的打通,智能家居单品以及全屋智能家居系统正在加快速度走进普通社区。

Viewing all 11857 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>